offline_first_fhir_client 0.0.7 copy "offline_first_fhir_client: ^0.0.7" to clipboard
offline_first_fhir_client: ^0.0.7 copied to clipboard

The offline_first_fhir_client library provides a comprehensive solution for managing and syncing FHIR resources in Flutter applications with offline-first capabilities. It is designed for developers b [...]

example/lib/main.dart

import 'package:example/edit_patient_data.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:offline_first_fhir_client/offline_first_fhir_client.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Initialize Hive
  await HiveService.initHive();


  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: 'Patient Data Manager',
      home: PatientInputScreen(),
    );
  }
}

class PatientInputScreen extends StatefulWidget {
  const PatientInputScreen({super.key});

  @override
  PatientInputScreenState createState() => PatientInputScreenState();
}

class PatientInputScreenState extends State<PatientInputScreen> {
  final _formKey = GlobalKey<FormState>();
  final _nameController = TextEditingController();
  final _genderController = TextEditingController();
  final _birthDateController = TextEditingController();

  // Instantiate services
  final hiveService = HiveService();
  final apiService = ApiService(baseUrl: "http://20.8.73.160:8080/fhir", authToken: "your-auth-token");

  final List<Map<String, dynamic>> _patients = [];

  bool isLoading = false;

  @override
  void dispose() {
    // Dispose of controllers to free resources
    _nameController.dispose();
    _genderController.dispose();
    _birthDateController.dispose();
    super.dispose();
  }

  @override
  void initState() {
    // Initialize state and fetch local patients
    _getPatients();
    super.initState();
  }

  /// Fetches all patient data from the local Hive database and updates the state.
  void _getPatients() async {
    _patients.clear();
    List<Map<String, dynamic>>? patientsData = await hiveService.getAllData("Patient");
    setState(() {
      _patients.addAll(patientsData);
    });
    }

  /// Add a new patient or update an existing one
  Future<void> _addPatient() async {
    if (_formKey.currentState!.validate()) {
      var name = _nameController.text.trim().split(" ");
      var firstName = name[0];
      var remainArr = name.sublist(1);
      var lastName = remainArr.join(" ");

      List<Map<String, dynamic>> searchResult = await hiveService.searchResourceByGivenKeys(
        "Patient",
        [
          {r'$.body.name[0].family': lastName},
          {r'$.body.name[0].given': [firstName]},
          {r'$.body.birthDate': {'type': 'equals', 'value': _birthDateController.text}},
        ],
      );

      var uniqueId = DateTime.now().millisecondsSinceEpoch.toString();

      final patient = {
        "id": uniqueId,
        "localVersionId": "1",
        "isSynced" : false,
        "body": {
          "resourceType": "Patient",
          "id": uniqueId,
          "name": [
            {"family": lastName, "given": [firstName]},
          ],
          "meta": {
            "versionId": "1",
            "lastUpdated": DateFormat("yyyy-MM-ddTHH:mm:ss.SSSXXX").format(DateTime.now().toUtc()),
            "source": DateTime.now().millisecondsSinceEpoch,
          },
          "birthDate": _birthDateController.text,
        },
      };

      var uniqueOb1 = DateTime.now().millisecondsSinceEpoch.toString();
      var uniqueOb2 = DateTime.now().millisecondsSinceEpoch.toString();

      final observation = [
        {
          "id": uniqueOb1,
          "localVersionId": "1",
          "isSynced" : false,
          "body" : {
              "resourceType": "Observation",
              "id" : uniqueOb1,
              "status": "final",
              "category": [
                {
                  "coding": [
                    {
                      "system": "http://terminology.hl7.org/CodeSystem/observation-category",
                      "code": "vital-signs",
                      "display": "Vital Signs"
                    }
                  ]
                }
              ],
              "code": {
                "coding": [
                  {
                    "system": "http://loinc.org",
                    "code": "91942-7",
                    "display": "Gestational age"
                  }
                ]
              },
              "subject": {
                "reference": "Patient/$uniqueId"
              },
              "effectiveDateTime": "2025-01-10T08:00:00+00:00",
              "valueQuantity": {
                "value": 38,
                "unit": "weeks",
                "system": "http://unitsofmeasure.org",
                "code": "wk"
              },
              "extension": [
                {
                  "url": "http://example.org/fhir/StructureDefinition/determination-method",
                  "valueString": "Date of last menstrual period"
                },
                {
                  "url": "http://example.org/fhir/StructureDefinition/determination-date",
                  "valueDateTime": "2025-01-01"
                }
              ]
            }
        },
        {
          "id": uniqueOb2,
          "localVersionId": "1",
          "isSynced" : false,
          "body" : {
            "resourceType": "Observation",
            "id" : uniqueOb2,
            "status": "final",
            "category": [
              {
                "coding": [
                  {
                    "system": "http://terminology.hl7.org/CodeSystem/observation-category",
                    "code": "vital-signs",
                    "display": "Vital Signs"
                  }
                ]
              }
            ],
            "code": {
              "coding": [
                {
                  "system": "http://loinc.org",
                  "code": "10220-3",
                  "display": "Start of labor"
                }
              ]
            },
            "subject": {
              "reference": "Patient/$uniqueId"
            },
            "effectiveDateTime": "2025-01-10T08:00:00+00:00",
            "valueString": "Spontaneous"
          }
        }
      ];

      var uniqueCon1 = DateTime.now().millisecondsSinceEpoch.toString();
      var uniqueCon2 = DateTime.now().millisecondsSinceEpoch.toString();

      final condition = [
        {
        "id": uniqueCon1,
        "localVersionId": "1",
        "isSynced" : false,
        "body" : {
            "resourceType": "Condition",
            "id" : uniqueCon1,
            "clinicalStatus": {
              "coding": [
                {
                  "system": "http://terminology.hl7.org/CodeSystem/condition-clinical",
                  "code": "active"
                }
              ]
            },
            "verificationStatus": {
              "coding": [
                {
                  "system": "http://terminology.hl7.org/CodeSystem/condition-ver-status",
                  "code": "confirmed"
                }
              ]
            },
            "code": {
              "coding": [
                {
                  "system": "http://snomed.info/sct",
                  "code": "38341003",
                  "display": "Chronic hypertension"
                }
              ]
            },
            "subject": {
              "reference": "Patient/$uniqueId"
            }
          },

      },
        {
          "id": uniqueCon2,
          "localVersionId": "1",
          "isSynced" : false,
          "body" : {
          "resourceType": "Condition",
          "id" : uniqueCon2,
          "clinicalStatus": {
            "coding": [
              {
                "system": "http://terminology.hl7.org/CodeSystem/condition-clinical",
                "code": "active"
              }
            ]
          },
          "verificationStatus": {
            "coding": [
              {
                "system": "http://terminology.hl7.org/CodeSystem/condition-ver-status",
                "code": "confirmed"
              }
            ]
          },
          "code": {
            "coding": [
              {
                "system": "http://snomed.info/sct",
                "code": "6342002",
                "display": "Preeclampsia"
              }
            ]
          },
          "subject": {
            "reference": "Patient/$uniqueId"
          }
        }
        }
      ];

      var uniqueFlag1 = DateTime.now().millisecondsSinceEpoch.toString();

      final flag = [
        {
        "id": uniqueFlag1,
        "localVersionId": "1",
        "isSynced" : false,
        "body" : {
          "resourceType": "Flag",
          "id" : uniqueFlag1,
          "status": "active",
          "category": {
            "coding": [
              {
                "system": "http://terminology.hl7.org/CodeSystem/flag-category",
                "code": "safety",
                "display": "Safety"
              }
            ]
          },
          "code": {
            "coding": [
              {
                "system": "http://snomed.info/sct",
                "code": "431855005",
                "display": "HIV infection"
              }
            ]
          },
          "subject": {
            "reference": "Patient/$uniqueId"
          },
          "note": [
            {
              "text": "Patient diagnosed with HIV."
            }
          ]
        }
      }
      ];

      if (searchResult.isNotEmpty) {
        _showDuplicateDialog(patient, observation, condition, flag, searchResult[0]);
      } else {
        setState(() {
          _patients.add(patient);
        });
        await _savePatientToLocal(patient);
        await _saveObservationToLocal(observation);
        await _saveConditionToLocal(condition);
        await _saveFlagToLocal(flag);
        _clearFormInputs();
      }
    }
  }

  void _showDuplicateDialog(Map<String, dynamic> newPatient, List<Map<String, Object>> observation, List<Map<String, Object>> condition, List<Map<String, Object>> flag, Map<String, dynamic> existingPatient) {
    showDialog(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          title: const Text('Duplicate Patient'),
          content: const Text('A similar patient exists. Override or create a new entry?'),
          actions: [
            TextButton(
              onPressed: () {
                Navigator.of(context).pop();
                setState(() {
                  _patients.add(newPatient);
                });
                _savePatientToLocal(newPatient);
                _saveObservationToLocal(observation);
                _saveConditionToLocal(condition);
                _saveFlagToLocal(flag);
                _clearFormInputs();
                ScaffoldMessenger.of(context).showSnackBar(
                  const SnackBar(content: Text('New patient added locally!')),
                );
              },
              child: const Text('Create New'),
            ),
            TextButton(
              onPressed: () {
                Navigator.of(context).pop();
                existingPatient['body']['name'][0]['family'] = newPatient['body']['name'][0]['family'];
                existingPatient['body']['name'][0]['given'][0] = newPatient['body']['name'][0]['given'][0];
                existingPatient['body']['birthDate'] = newPatient['body']['birthDate'];
                _savePatientToLocal(existingPatient);
                _clearFormInputs();
                ScaffoldMessenger.of(context).showSnackBar(
                  const SnackBar(content: Text('Existing patient updated locally!')),
                );
              },
              child: const Text('Override'),
            ),
          ],
        );
      },
    );
  }

  Future<void> _savePatientToLocal(Map<String, dynamic> patient) async {
    final identifier = patient['id'];
    if (identifier != null) {
      await hiveService.saveData("Patient", identifier, patient);
    } else {
      throw Exception('Patient data must have a valid identifier.');
    }
  }

  Future<void> _saveObservationToLocal(List<Map<String, dynamic>> observation) async {

    for (var obs in observation) {
      final identifier = obs['id'];
      if (identifier != null) {
        await hiveService.saveData("Observation", identifier, obs);
      } else {
        throw Exception('Patient data must have a valid identifier.');
      }
    }

  }

  Future<void> _saveConditionToLocal(List<Map<String, dynamic>> condition) async {

    for (var con in condition) {

      final identifier = con['id'];
      if (identifier != null) {
        await hiveService.saveData("Condition", identifier, con);
      } else {
        throw Exception('Patient data must have a valid identifier.');
      }

    }


  }

  Future<void> _saveFlagToLocal(List<Map<String, dynamic>> flag) async {

    for (var f in flag) {

      final identifier = f['id'];
      if (identifier != null) {
        await hiveService.saveData("Flag", identifier, f);
      } else {
        throw Exception('Patient data must have a valid identifier.');
      }

    }


  }

  void _clearFormInputs() {
    _nameController.clear();
    _birthDateController.clear();
  }

  /// Sync all resources (Patients, Observations, Conditions, Flags) to the server
  Future<void> _syncPatientsToServer() async {
    setState(() {
      isLoading = true;
    });

    try {
      // Fetch local data for all resource types
      List<Map<String, dynamic>>? localPatients = await hiveService.getAllData("Patient");
      List<Map<String, dynamic>>? localObservations = await hiveService.getAllData("Observation");
      List<Map<String, dynamic>>? localConditions = await hiveService.getAllData("Condition");
      List<Map<String, dynamic>>? localFlags = await hiveService.getAllData("Flag");

      // Fetch all server data for all resources
      String? nextUrl = "/Patient/\$everything";
      List<dynamic> serverEntries = await apiService.getAllResourceDataWithPagination(nextUrl);

      // Sync Patients first to get updated IDs
      final updatedPatientIds = await SyncHelper().syncResources("Patient", localPatients, serverEntries, r'$[?(@.resource.resourceType == "Patient")].resource', apiService);

      // Sync other resources, updating their references with the new patient IDs
      await SyncHelper().syncResourcesWithUpdatedReferences("Observation", localObservations, serverEntries, r'$[?(@.resource.resourceType == "Observation")].resource', updatedPatientIds, apiService);
      await SyncHelper().syncResourcesWithUpdatedReferences("Condition", localConditions, serverEntries, r'$[?(@.resource.resourceType == "Condition")].resource', updatedPatientIds, apiService);
      await SyncHelper().syncResourcesWithUpdatedReferences("Flag", localFlags, serverEntries, r'$[?(@.resource.resourceType == "Flag")].resource', updatedPatientIds, apiService);
      _getPatients();
    } catch (e) {
      if (kDebugMode) {
        print("Sync error: $e");
      }
    } finally {
      setState(() {
        isLoading = false;
      });
    }
  }

 /* /// Synchronize a specific resource type and return updated patient IDs (if applicable)
  Future<Map<String, String>> _syncResources(
      String resourceType,
      List<Map<String, dynamic>>? localResources,
      List<dynamic> serverEntries,
      String jsonPathQuery,
      ) async {
    // Extract resources of the specified type from server entries
    final resources = await hiveService.extractResources(serverEntries, jsonPathQuery);
    List<Map<String, dynamic>> remainingResources = List.from(localResources ?? []);
    Map<String, String> updatedPatientIds = {}; // Mapping of local patient references to server IDs

    for (var serverResource in resources) {
      var matchedResource = remainingResources.firstWhere(
            (localResource) => _isResourceMatch(localResource, serverResource),
        orElse: () => {},
      );

      if (matchedResource.isNotEmpty) {
        remainingResources.remove(matchedResource);

        if (_isServerResourceNewer(serverResource, matchedResource)) {
          await _updateLocalResource(resourceType, serverResource, matchedResource['id']);
        } else {
          await _updateServerResource(resourceType, matchedResource);
        }
      } else {
        await _updateLocalResource(resourceType, serverResource, DateTime.now().millisecondsSinceEpoch.toString());
      }
    }

    // Post remaining resources and track updated patient IDs (for Patient resource only)
    if (remainingResources.isNotEmpty) {
      if (resourceType == "Patient") {
        updatedPatientIds = await _postAllResourcesAndGetIds(resourceType, remainingResources);
      } else {
        await _postAllResources(resourceType, remainingResources);
      }
    }

    return updatedPatientIds;
  }

  Future<void> _postAllResources(String resourceType, List<Map<String, dynamic>> resources) async {
    for (var resource in resources) {
      if (!resource['isSynced']) {
        try {
          if (kDebugMode) {
            print("Posting $resourceType to server...");
          }

          dynamic response = await apiService.postData("/$resourceType", resource['body']);

          if (response != null) {
            final updatedResourceData = {
              'id': resource['id'],
              'localVersionId': response['meta']['versionId'].toString(),
              'isSynced': true,
              'body': response,
            };

            // Save updated resource data in local Hive storage
            await hiveService.saveData(
              resourceType,
              updatedResourceData['id'],
              updatedResourceData,
            );

            if (kDebugMode) {
              print("$resourceType posted successfully: ${updatedResourceData['id']}");
            }
          }
        } catch (e) {
          if (kDebugMode) {
            print("Error posting $resourceType to server: $e");
          }
        }
      }
    }
  }

  /// Sync resources like Observation, Condition, or Flag, updating their patient references
  Future<void> _syncResourcesWithUpdatedReferences(
      String resourceType,
      List<Map<String, dynamic>>? localResources,
      List<dynamic> serverEntries,
      String jsonPathQuery,
      Map<String, String> updatedPatientIds,
      ) async {
    // Extract resources of the specified type from server entries
    final serverResources = await hiveService.extractResources(serverEntries, jsonPathQuery);
    List<Map<String, dynamic>> remainingLocalResources = List.from(localResources ?? []);

    for (var serverResource in serverResources) {
      // Find matching local resource
      var matchedLocalResource = remainingLocalResources.firstWhere(
            (localResource) => _isResourceMatch(localResource, serverResource),
        orElse: () => {},
      );

      if (matchedLocalResource.isNotEmpty) {
        remainingLocalResources.remove(matchedLocalResource);

        // Check for updates
        if (_isServerResourceNewer(serverResource, matchedLocalResource)) {
          // Update local resource with server data
          await _updateLocalResource(resourceType, serverResource, matchedLocalResource['id']);
        } else {
          // Update server with local resource data
          var updatedResource = _updatePatientReferenceInResource(matchedLocalResource, updatedPatientIds);
          await _updateServerResource(resourceType, updatedResource);
        }
      } else {
        // If no matching local resource, save the server resource locally
        await _updateLocalResource(resourceType, serverResource, DateTime.now().millisecondsSinceEpoch.toString());
      }
    }

    // Post any remaining local resources (new ones) to the server
    if (remainingLocalResources.isNotEmpty) {
      for (var localResource in remainingLocalResources) {
        // Update patient reference in the resource
        var updatedResource = _updatePatientReferenceInResource(localResource, updatedPatientIds);

        // Post the updated resource to the server
        dynamic response = await apiService.postData("/$resourceType", updatedResource['body']);
        if (response != null) {
          // Save the server response locally
          final updatedResourceData = {
            'id': localResource['id'],
            'localVersionId': response['meta']['versionId'].toString(),
            'isSynced': true,
            'body': response,
          };
          await hiveService.saveData(
            resourceType,
            updatedResourceData['id'],
            updatedResourceData,
          );
        }
      }
    }
  }

  /// Update the patient reference in the resource JSON
  Map<String, dynamic> _updatePatientReferenceInResource(Map<String, dynamic> resource, Map<String, String> updatedPatientIds) {
    final body = resource['body'];
    final subject = body['subject'];
    if (subject != null && subject['reference'] != null) {
      final localReference = subject['reference'];
      final updatedReference = updatedPatientIds[localReference];
      if (updatedReference != null) {
        subject['reference'] = updatedReference;
      }
    }
    return resource;
  }

  /// Post all resources of a given type and return updated patient IDs (for Patient resource)
  Future<Map<String, String>> _postAllResourcesAndGetIds(String resourceType, List<Map<String, dynamic>> resources) async {
    Map<String, String> updatedPatientIds = {};

    for (var resource in resources) {
      if (!resource['isSynced']) {
        dynamic response = await apiService.postData("/$resourceType", resource['body']);
        if (response != null) {
          final updatedResource = {
            'id': resource['id'],
            'localVersionId': response['meta']['versionId'].toString(),
            'isSynced': true,
            'body': response,
          };
          await hiveService.saveData(
            resourceType,
            updatedResource['id'],
            updatedResource,
          );

          // Track updated patient IDs (if resourceType is Patient)
          if (resourceType == "Patient") {
            final localReference = "Patient/${resource['id']}";
            final serverId = "Patient/${response['id']}";
            updatedPatientIds[localReference] = serverId;
          }
        }
      }
    }

    return updatedPatientIds;
  }

  bool _isResourceMatch(Map<String, dynamic> local, Map<String, dynamic> server) {
    try {
      final localBody = local['body'];
      final serverBody = server;
      return serverBody['id'] == localBody['id'];
    } catch (e) {
      return false;
    }
  }

  bool _isServerResourceNewer(Map<String, dynamic> server, Map<String, dynamic> local) {
    try {
      final serverVersionId = int.parse(server['meta']['versionId'].toString());
      final localVersionId = int.parse(local['localVersionId'].toString());
      return serverVersionId > localVersionId;
    } catch (e) {
      return false;
    }
  }

  Future<void> _updateLocalResource(String resourceType, Map<String, dynamic> serverResource, String id) async {
    try {
      final updatedResource = {
        'id': id,
        'localVersionId': serverResource['meta']['versionId'].toString(),
        'isSynced': true,
        'body': serverResource,
      };

      await hiveService.saveData(
        resourceType,
        updatedResource['id'].toString(),
        updatedResource,
      );
    } catch (e) {
      if (kDebugMode) {
        print("Error updating local resource: $e");
      }
    }
  }

  Future<void> _updateServerResource(String resourceType, Map<String, dynamic> localResource) async {
    try {
      final resourceId = localResource['body']['id'];
      dynamic response = await apiService.putData(
        "/$resourceType/$resourceId",
        localResource['body'],
      );

      if (response != null) {
        final updatedResource = {
          'id': localResource['id'],
          'localVersionId': response['meta']['versionId'].toString(),
          'isSynced': true,
          'body': response,
        };

        await hiveService.saveData(
          resourceType,
          updatedResource['id'],
          updatedResource,
        );
      }
    } catch (e) {
      if (kDebugMode) {
        print("Error updating server resource: $e");
      }
    }
  }
*/
  /*/// Sync all patients to the server
  Future<void> _syncPatientsToServer() async {
    setState(() {
      isLoading = true;
    });

    try {
      List<Map<String, dynamic>>? localPatients = await hiveService.getAllData("Patient");
      String? nextUrl = "/Patient/\$everything";

      List<dynamic> serverEntries = await apiService.getAllResourceDataWithPagination(nextUrl);

      // Define JSONPath queries for specific resource types
      const String patientQuery = r'$[?(@.resource.resourceType == "Patient")].resource';
      const String observationQuery = r'$[?(@.resource.resourceType == "Observation")].resource';
      const String conditionQuery = r'$[?(@.resource.resourceType == "Condition")].resource';
      const String flagQuery = r'$[?(@.resource.resourceType == "Flag")].resource';

      // Extract Patient resources
      final patients = await hiveService.extractResources(serverEntries, patientQuery);
      print('Patients: ${jsonEncode(patients)}');

      // Extract Observation resources
      final observations = await hiveService.extractResources(serverEntries, observationQuery);
      print('Observations: ${jsonEncode(observations)}');

      // Extract Condition resources
      final conditions = await hiveService.extractResources(serverEntries, conditionQuery);
      print('Conditions: ${jsonEncode(conditions)}');

      // Extract Condition resources
      final flags = await hiveService.extractResources(serverEntries, flagQuery);
      print('Flags: ${jsonEncode(flags)}');

      if (serverEntries.isNotEmpty) {
        await _syncPatients(localPatients, serverEntries);
      } else {
        if (localPatients.isNotEmpty) {
          //await _postAllPatients(localPatients);
        }
      }
      //_getPatients();
    } catch (e) {
      if (kDebugMode) {
        print("Sync error: $e");
      }
    } finally {
      setState(() {
        isLoading = false;
      });
    }
  }

  /// Synchronizes local patients with server patients, resolving conflicts as needed.
  Future<void> _syncPatients(List<Map<String, dynamic>>? localPatients,
      List<dynamic> serverEntries) async {

    List<Map<String, dynamic>> remainingPatients =
    List.from(localPatients ?? []);

    for (var serverEntry in serverEntries) {
      var serverPatient = serverEntry['resource'];

      var matchedPatient = remainingPatients.firstWhere(
            (localPatient) => _isPatientMatch(localPatient, serverPatient),
        orElse: () => {},
      );

      if (matchedPatient.isNotEmpty) {
        remainingPatients.remove(matchedPatient);

        if (_isServerPatientNewer(serverPatient, matchedPatient)) {
          await _updateLocalPatient(serverPatient, matchedPatient['id']);
        } else {
          await _updateServerPatient(matchedPatient);
        }
      } else {
        await _updateLocalPatient(serverPatient, DateTime.now().millisecondsSinceEpoch.toString());
      }
    }

    if (remainingPatients.isNotEmpty) {
      await _postAllPatients(remainingPatients);
    }
  }

// Helper: Check if two patients match based on name, gender, birthdate, or identifier
  bool _isPatientMatch(Map<String, dynamic> local, Map<String, dynamic> server) {
    try {
      final localBody = local['body'];
      final serverBody = server;
      return serverBody['id'] == localBody['id'];
    } catch (e) {
      return false;
    }
  }

// Helper: Compare version IDs between server and local patient data
  bool _isServerPatientNewer(Map<String, dynamic> server, Map<String, dynamic> local) {
    final serverVersionId = int.parse(server['meta']['versionId'].toString());
    final localVersionId = int.parse(local['localVersionId'].toString());
    return serverVersionId >= localVersionId;
  }

// Helper: Update local patient data after successful server sync
  Future<void> _updateLocalPatient(Map<String, dynamic> serverPatient, String id) async {
    // Update the localVersionId with the server's meta.versionId
    var patient = {
      'id' : id,
      'localVersionId' : serverPatient['meta']['versionId'].toString(),
      'isSynced' : true,
      'body' : serverPatient
    };

    await hiveService.saveData(
      "Patient",
      patient['id'].toString(),
      patient,
    );
  }

// Helper: Update server with the local patient data
  Future<void> _updateServerPatient(Map<String, dynamic> localPatient) async {
    dynamic response = await apiService.putData(
      "/Patient/${localPatient['body']['id']}",
      localPatient['body'],
    );
    if (response != null) {
      final updatedPatient = {
        "id" : localPatient['id'],
        "localVersionId": response['meta']['versionId'].toString(),
        "isSynced" : true,
        "body": response,
      };
      await hiveService.saveData(
        "Patient",
        updatedPatient['id'],
        updatedPatient,
      );
    }
  }

// Helper: Post all unsynced patients to the server
  Future<void> _postAllPatients(List<Map<String, dynamic>> patients) async {
    for (var patient in patients) {
      if (!patient['isSynced']) {
        if (kDebugMode) {
          print("Posting patient to server...");
        }
        dynamic response = await apiService.postData("/Patient", patient['body']);
        if (response != null) {
          final updatedPatient = {
            'id' : patient['id'],
            "localVersionId": response['meta']['versionId'].toString(),
            "isSynced" : true,
            "body": response,
          };
          await hiveService.saveData(
            "Patient",
            updatedPatient['id'],
            updatedPatient,
          );
        }
      }
    }
  }*/

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Patient Data Input'),
      ),
      body: Stack(
        children: [
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: Column(
              children: [
                Form(
                  key: _formKey,
                  child: Column(
                    children: [
                      TextFormField(
                        controller: _nameController,
                        decoration: const InputDecoration(labelText: 'Name'),
                        validator: (value) {
                          if (value == null || value.isEmpty) {
                            return 'Please enter a name';
                          }
                          final namePattern = RegExp(r'^[A-Za-z]+( [A-Za-z]+)+$');
                          if (!namePattern.hasMatch(value.trim())) {
                            return 'Please enter at least first name and last name';
                          }
                          return null;
                        },
                      ),
                      TextFormField(
                        controller: _birthDateController,
                        decoration: const InputDecoration(labelText: 'Birth Date (YYYY-MM-DD)'),
                        validator: (value) {
                          if (value == null || value.isEmpty) {
                            return 'Please enter a birth date';
                          }
                          return null;
                        },
                      ),
                      const SizedBox(height: 20),
                      ElevatedButton(
                        onPressed: _addPatient,
                        child: const Text('Add Patient to Local'),
                      ),
                    ],
                  ),
                ),
                const SizedBox(height: 20),
                const Text('Patients From Local Database'),
                Expanded(
                  child: ListView.builder(
                    itemCount: _patients.length,
                    itemBuilder: (context, index) {
                      final patient = _patients[index];
                      return ListTile(
                        title: Text("${patient['body']['name'][0]['given'][0].toString()} ${patient['body']['name'][0]['family'].toString()}"),
                        subtitle: Text('${patient['body']['birthDate']}'),
                        trailing: Container(
                          width: 100,
                          alignment: Alignment.centerRight,
                          child: Row(
                            mainAxisAlignment: MainAxisAlignment.end,
                            children: [
                              GestureDetector(
                                  onTap: () {
                                    Navigator.of(context).push(CupertinoPageRoute(builder: (context) => EditPatientData(patient))).then((onValue) => _getPatients());
                                  },
                                  child: const Icon(Icons.edit, size: 18,)
                              ),
                              const SizedBox(width: 16,),
                              !patient['isSynced'] ? GestureDetector(
                                  onTap: () {
                                    showDialog(
                                      context: context,
                                      builder: (BuildContext context) {
                                        return AlertDialog(
                                          title: const Text('Alert'),
                                          content: const Text('This data is not synced to server, so it will be deleted permanently from local database, are you sure you want to delete?'),
                                          actions: [
                                            TextButton(
                                              onPressed: () async {
                                                Navigator.of(context).pop(); // Close the dialog
                                                await hiveService.deleteDataByKey("Patient", patient['id'].toString());
                                                setState(() {
                                                  _patients.removeAt(index);
                                                });
                                              },
                                              child: const Text('Delete', style: TextStyle(color: Colors.red),),
                                            ),
                                            TextButton(
                                              onPressed: () {
                                                Navigator.of(context).pop(); // Close the dialog
                                              },
                                              child: const Text('No'),
                                            ),
                                          ],
                                        );
                                      },
                                    );
                                  },
                                  child: const Icon(Icons.delete, size: 18, color: Colors.red,)
                              ) : Container(),
                            ],
                          ),
                        ),
                      );
                    },
                  ),
                ),
                const Text('New data will reflect on server within 5 mins'),
                ElevatedButton(
                  onPressed: isLoading ? null : _syncPatientsToServer,
                  child: Text( isLoading ? 'Syncing' : 'Sync All to Server'),
                ),
              ],
            ),
          ),
          isLoading ? Container(
            color: Colors.white54,
            child: const Center(
              child: CircularProgressIndicator(),
            ),
          ) : Container()
        ],
      ),
    );
  }
}
2
likes
0
points
170
downloads

Publisher

verified publishertdh.org

Weekly Downloads

The offline_first_fhir_client library provides a comprehensive solution for managing and syncing FHIR resources in Flutter applications with offline-first capabilities. It is designed for developers building healthcare applications where reliable data access is critical, even in offline scenarios.

Homepage
Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

flutter, flutter_secure_storage, hive_flutter, hive_generator, http, intl, json_path, mockito, path_provider, path_provider_platform_interface, workmanager

More

Packages that depend on offline_first_fhir_client