offline_first_fhir_client 0.0.7
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()
],
),
);
}
}