Merge branch 'master' into dev_aamir

# Conflicts:
#	lib/presentation/lab/lab_orders_page.dart
pull/129/head
aamir-csol 3 weeks ago
commit 54f8391066

@ -19,5 +19,5 @@
<string name="GEOFENCE_REQUEST_TOO_FREQUENT">
Geofence requests happened too frequently.
</string>
<string name="mapbox_access_token" translatable="false">sk.eyJ1IjoicndhaWQiLCJhIjoiY2x6NWo0bTMzMWZodzJrcGZpemYzc3Z4dSJ9.uSSZuwNSGCcCdPAiORECmg</string>
<!-- <string name="mapbox_access_token" translatable="false">sk.eyJ1IjoicndhaWQiLCJhIjoiY2x6NWo0bTMzMWZodzJrcGZpemYzc3Z4dSJ9.uSSZuwNSGCcCdPAiORECmg</string>-->
</resources>

@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32.381" height="37.391" viewBox="0 0 32.381 37.391">
<g id="affiliate-marketing_1_" data-name="affiliate-marketing (1)" transform="translate(-34.298 0)">
<path id="Path_4982" data-name="Path 4982" d="M63.419,23.145l.415,1.991-2.684-1.55a11.718,11.718,0,0,0,0-9.782l2.684-1.55-.415,1.991,2.145.447,1.115-5.345L61.492,7.641l-.685,2.081,1.932.636-2.687,1.551a11.736,11.736,0,0,0-8.469-4.893v-3.1L53.1,5.272l1.46-1.634L50.489,0,46.417,3.639l1.46,1.634,1.517-1.355v3.1a11.736,11.736,0,0,0-8.469,4.893l-2.687-1.551,1.932-.636-.685-2.081L34.3,9.348l1.115,5.346,2.145-.447-.415-1.991,2.684,1.55a11.718,11.718,0,0,0,0,9.782l-2.684,1.55.415-1.991L35.413,22.7,34.3,28.043l5.187,1.707.685-2.081-1.932-.636,2.687-1.551a11.736,11.736,0,0,0,8.469,4.893v3.1l-1.517-1.355-1.46,1.634,4.072,3.639,4.072-3.639L53.1,32.118l-1.517,1.355v-3.1a11.736,11.736,0,0,0,8.469-4.893l2.687,1.551-1.932.636.685,2.081,5.187-1.707L65.564,22.7Z" fill="#2e303a"/>
<g id="user_24_" data-name="user (24)" transform="translate(42.096 9.104)">
<path id="Path_4983" data-name="Path 4983" d="M10.267,8.536a3.028,3.028,0,0,0,1.792.581,3.125,3.125,0,0,0,3.059-3.059,3.059,3.059,0,1,0-4.85,2.478Z" transform="translate(-3.378 0)" fill="#fff"/>
<path id="Path_4984" data-name="Path 4984" d="M11.372,16.55a3.919,3.919,0,0,1-5.384,0,5.689,5.689,0,0,0-2.989,5,.438.438,0,0,0,.437.437H13.924a.438.438,0,0,0,.437-.437A5.689,5.689,0,0,0,11.372,16.55Z" transform="translate(0 -7.629)" fill="#fff"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" width="30.776" height="24.337" viewBox="0 0 30.776 24.337">
<g id="folder" transform="translate(0 -42.679)">
<g id="Group_8637" data-name="Group 8637" transform="translate(0 45.895)">
<g id="Group_8636" data-name="Group 8636" transform="translate(0 0)">
<path id="Path_5021" data-name="Path 5021" d="M25.043,88.08H13.94l-2.209-2.625a.377.377,0,0,0-.3-.136H2.471A2.489,2.489,0,0,0,0,87.808v16.145a2.489,2.489,0,0,0,2.486,2.486H25.043a2.489,2.489,0,0,0,2.486-2.486V90.566A2.489,2.489,0,0,0,25.043,88.08Z" transform="translate(0 -85.319)" fill="#2bb8a6"/>
</g>
</g>
<g id="Group_8639" data-name="Group 8639" transform="translate(3.274 42.679)">
<g id="Group_8638" data-name="Group 8638" transform="translate(0 0)">
<path id="Path_5022" data-name="Path 5022" d="M68.431,45.44H57.324l-2.209-2.625a.377.377,0,0,0-.3-.136H45.859A2.486,2.486,0,0,0,43.4,44.827h8.168a1.445,1.445,0,0,1,1.11.516l1.886,2.242H65.169a3.56,3.56,0,0,1,3.554,3.554V63.775A2.486,2.486,0,0,0,70.9,61.31V47.923A2.489,2.489,0,0,0,68.431,45.44Z" transform="translate(-43.4 -42.679)" fill="#2bb8a6"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" width="30.996" height="28.414" viewBox="0 0 30.996 28.414">
<g id="image-gallery" transform="translate(-0.001 -2)">
<g id="Group_8635" data-name="Group 8635" transform="translate(0.001 5.875)">
<g id="Group_8634" data-name="Group 8634" transform="translate(0 5.078)">
<path id="Path_5018" data-name="Path 5018" d="M8.072,22.581A4.836,4.836,0,0,1,3.5,19.286l-.045-.149a4.711,4.711,0,0,1-.223-1.4V8.932L.1,19.391a2.933,2.933,0,0,0,2.056,3.558L22.123,28.3a2.962,2.962,0,0,0,.744.1,2.88,2.88,0,0,0,2.791-2.112l1.164-3.7Z" transform="translate(-0.001 -8.932)" fill="#2bb8a6"/>
</g>
<path id="Path_5019" data-name="Path 5019" d="M9.583,10.166A2.583,2.583,0,1,0,7,7.583,2.586,2.586,0,0,0,9.583,10.166Z" transform="translate(2.04 -5)" fill="#2bb8a6"/>
</g>
<path id="Path_5020" data-name="Path 5020" d="M26.6,2H7.229A3.233,3.233,0,0,0,4,5.229V19.436a3.233,3.233,0,0,0,3.229,3.229H26.6a3.233,3.233,0,0,0,3.229-3.229V5.229A3.233,3.233,0,0,0,26.6,2ZM7.229,4.583H26.6a.646.646,0,0,1,.646.646V14.4l-4.08-4.761a2.313,2.313,0,0,0-1.732-.794,2.258,2.258,0,0,0-1.726.815l-4.8,5.758-1.563-1.559a2.267,2.267,0,0,0-3.2,0L6.583,17.42V5.229a.646.646,0,0,1,.646-.646Z" transform="translate(1.166)" fill="#2bb8a6"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="31.908" height="29.134" viewBox="0 0 31.908 29.134">
<g id="Group_8579" data-name="Group 8579" transform="translate(0 -100)">
<path id="Path_4556" data-name="Path 4556" d="M88.972,88.108h-4.4l-2.539-2.775H72.324v4.162H68.162v4.162H64V107.53a2.783,2.783,0,0,0,2.775,2.775h22.2a2.783,2.783,0,0,0,2.775-2.775V90.882A2.783,2.783,0,0,0,88.972,88.108Zm-11.1,18.035a6.937,6.937,0,1,1,6.937-6.937A6.939,6.939,0,0,1,77.873,106.143Z" transform="translate(-59.838 18.829)" fill="#2bb8a6"/>
<path id="Path_4557" data-name="Path 4557" d="M4.162,32.432H6.937V28.27H11.1V25.495H6.937V21.333H4.162v4.162H0V28.27H4.162Z" transform="translate(0 78.667)" fill="#2bb8a6"/>
<path id="Path_4558" data-name="Path 4558" d="M213.505,230.4a4.435,4.435,0,0,0-4.439,4.439h0a4.439,4.439,0,1,0,4.439-4.439Z" transform="translate(-195.47 -116.804)" fill="#2bb8a6"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 910 B

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="37.982" height="37.982" viewBox="0 0 37.982 37.982">
<path id="Path_4586" data-name="Path 4586" d="M18.991,0A18.991,18.991,0,1,0,37.982,18.991,19.013,19.013,0,0,0,18.991,0ZM9.5,17.8A3.555,3.555,0,0,1,11.2,14.777L8.75,12.793a1.187,1.187,0,1,1,1.491-1.847l5.052,4.088A3.561,3.561,0,1,1,9.5,17.8Zm16.29,12.688a1.188,1.188,0,0,1-1.678.04,7.42,7.42,0,0,0-10.234,0,1.187,1.187,0,1,1-1.636-1.721,9.787,9.787,0,0,1,13.5,0A1.187,1.187,0,0,1,25.785,30.492Zm3.447-17.7-2.455,1.985a3.549,3.549,0,1,1-4.09.256l5.054-4.088a1.187,1.187,0,1,1,1.491,1.847Z" fill="#EFEFF0"/>
</svg>

After

Width:  |  Height:  |  Size: 614 B

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="37.982" height="37.982" viewBox="0 0 37.982 37.982">
<path id="Path_4587" data-name="Path 4587" d="M18.991,0A18.991,18.991,0,1,0,37.982,18.991,19.013,19.013,0,0,0,18.991,0ZM9.5,14.243a2.374,2.374,0,1,1,2.374,2.374A2.378,2.378,0,0,1,9.5,14.243ZM27.383,28.895a1.192,1.192,0,0,1-1.681,0,9.72,9.72,0,0,0-13.429,0,1.187,1.187,0,0,1-1.678-1.678,11.869,11.869,0,0,1,16.788,0A1.188,1.188,0,0,1,27.383,28.895Zm-1.27-12.278a2.374,2.374,0,1,1,2.374-2.374A2.378,2.378,0,0,1,26.113,16.617Z" fill="#EFEFF0"/>
</svg>

After

Width:  |  Height:  |  Size: 551 B

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="37.982" height="37.982" viewBox="0 0 37.982 37.982">
<path id="Path_4588" data-name="Path 4588" d="M18.991,0A18.991,18.991,0,1,0,37.982,18.991,19.013,19.013,0,0,0,18.991,0ZM9.5,14.243a2.374,2.374,0,1,1,2.374,2.374A2.378,2.378,0,0,1,9.5,14.243Zm15.43,14.243H13.056a1.187,1.187,0,1,1,0-2.374H24.926a1.187,1.187,0,1,1,0,2.374Zm1.187-11.869a2.374,2.374,0,1,1,2.374-2.374A2.378,2.378,0,0,1,26.113,16.617Z" fill="#EFEFF0"/>
</svg>

After

Width:  |  Height:  |  Size: 474 B

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="37.981" height="37.982" viewBox="0 0 37.981 37.982">
<path id="Path_4589" data-name="Path 4589" d="M18.991,0A18.991,18.991,0,1,0,37.981,18.991,19.013,19.013,0,0,0,18.991,0ZM8.308,14.243A3.561,3.561,0,1,1,11.869,17.8,3.565,3.565,0,0,1,8.308,14.243ZM27.382,27.382a11.866,11.866,0,0,1-16.785,0A1.187,1.187,0,0,1,12.275,25.7a9.72,9.72,0,0,0,13.429,0,1.187,1.187,0,0,1,1.678,1.678ZM26.112,17.8a3.561,3.561,0,1,1,3.561-3.561A3.565,3.565,0,0,1,26.112,17.8Z" fill="#EFEFF0"/>
</svg>

After

Width:  |  Height:  |  Size: 524 B

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="37.982" height="37.982" viewBox="0 0 37.982 37.982">
<path id="Path_4590" data-name="Path 4590" d="M18.991,0A18.991,18.991,0,1,0,37.982,18.991,19.013,19.013,0,0,0,18.991,0Zm7.122,11.869a2.374,2.374,0,1,1-2.374,2.374A2.378,2.378,0,0,1,26.113,11.869Zm-14.243,0A2.374,2.374,0,1,1,9.5,14.243,2.378,2.378,0,0,1,11.869,11.869Zm7.122,21.365a14.275,14.275,0,0,1-13.7-10.36,1.188,1.188,0,0,1,1.142-1.51H31.549a1.183,1.183,0,0,1,.945.47,1.2,1.2,0,0,1,.2,1.04A14.275,14.275,0,0,1,18.991,33.234Z" fill="#EFEFF0"/>
</svg>

After

Width:  |  Height:  |  Size: 558 B

@ -14,8 +14,8 @@ var PACKAGES_ORDERS = '/api/orders';
var PACKAGES_ORDER_HISTORY = '/api/orders/items';
var PACKAGES_TAMARA_OPT = '/api/orders/paymentoptions/tamara';
// var BASE_URL = 'http://10.50.100.198:2018/';
var BASE_URL = 'https://uat.hmgwebservices.com/';
// var BASE_URL = 'https://hmgwebservices.com/';
// var BASE_URL = 'https://uat.hmgwebservices.com/';
var BASE_URL = 'https://hmgwebservices.com/';
// var BASE_URL = 'http://10.201.204.103/';
// var BASE_URL = 'https://orash.cloudsolutions.com.sa/';
// var BASE_URL = 'https://vidauat.cloudsolutions.com.sa/';

@ -43,7 +43,7 @@ class AppState {
bool isChildLoggedIn = false;
bool isGMSAvailable = true;
bool isAndroid = true;
bool isRatedVisible =false;
void setAuthenticatedUser(AuthenticatedUser? authenticatedUser, {bool isFamily = false}) {
if (isFamily) {
_authenticatedChildUser = authenticatedUser;
@ -170,4 +170,8 @@ class AppState {
userLong = 0.0;
userLong = 0.0;
}
setRatedVisible(bool value) {
isRatedVisible = value;
}
}

@ -80,6 +80,12 @@ class LabViewModel extends ChangeNotifier {
double minY = double.infinity;
double maxX = double.infinity;
// --- New grouping fields ---
bool isSortByClinic = true;
List<List<PatientLabOrdersResponseModel>> patientLabOrdersByClinic = [];
List<List<PatientLabOrdersResponseModel>> patientLabOrdersByHospital = [];
List<List<PatientLabOrdersResponseModel>> patientLabOrdersViewList = [];
LabViewModel(
{required this.labRepo,
required this.errorHandlerService,
@ -92,6 +98,9 @@ class LabViewModel extends ChangeNotifier {
labOrderTests.clear();
isLabOrdersLoading = true;
isLabResultsLoading = true;
patientLabOrdersByClinic.clear();
patientLabOrdersByHospital.clear();
patientLabOrdersViewList.clear();
getPatientLabOrders();
// }
notifyListeners();
@ -102,6 +111,12 @@ class LabViewModel extends ChangeNotifier {
notifyListeners();
}
void setIsSortByClinic(bool value) {
isSortByClinic = value;
patientLabOrdersViewList = isSortByClinic ? patientLabOrdersByClinic : patientLabOrdersByHospital;
notifyListeners();
}
Future<void> getPatientLabOrders({Function(dynamic)? onSuccess, Function(String)? onError}) async {
// if (!isLabNeedToLoad) return;
@ -110,6 +125,9 @@ class LabViewModel extends ChangeNotifier {
uniqueTests.clear();
labOrderTests.clear();
uniqueTests = {};
patientLabOrdersByClinic.clear();
patientLabOrdersByHospital.clear();
patientLabOrdersViewList.clear();
notifyListeners();
final result = await labRepo.getPatientLabOrders();
@ -120,7 +138,7 @@ class LabViewModel extends ChangeNotifier {
isLabResultsLoading = false;
notifyListeners();
},
// => await errorHandlerService.handleError(failure: failure),
// => await errorHandler_service.handleError(failure: failure),
(apiResponse) {
if (apiResponse.messageStatus == 2) {
// dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {});
@ -130,6 +148,23 @@ class LabViewModel extends ChangeNotifier {
tempLabOrdersList = apiResponse.data!;
isLabOrdersLoading = false;
isLabResultsLoading = false;
// --- Build groups by clinic and by hospital (projectName) ---
final clinicMap = <String, List<PatientLabOrdersResponseModel>>{};
final hospitalMap = <String, List<PatientLabOrdersResponseModel>>{};
for (var order in patientLabOrders) {
final clinicKey = (order.clinicDescription ?? 'Unknown').trim();
clinicMap.putIfAbsent(clinicKey, () => []).add(order);
final hospitalKey = (order.projectName ?? order.projectID ?? 'Unknown').toString().trim();
hospitalMap.putIfAbsent(hospitalKey, () => []).add(order);
}
patientLabOrdersByClinic = clinicMap.values.toList();
patientLabOrdersByHospital = hospitalMap.values.toList();
patientLabOrdersViewList = isSortByClinic ? patientLabOrdersByClinic : patientLabOrdersByHospital;
filterSuggestions();
getUniqueTestDescription();
isLabNeedToLoad = false;
@ -204,7 +239,6 @@ class LabViewModel extends ChangeNotifier {
createdOn: item.createdOn,
model: item))
};
var sortedResult = SplayTreeSet<TestDetails>.from(uniqueTests, (a, b) => a.description?[0].toUpperCase().compareTo(b.description?[0] ?? "") ?? -1);
uniqueTestsList = uniqueTests.toList();
uniqueTestsList.sort((a, b) {
return a.description!.toLowerCase().compareTo(b.description!.toLowerCase());
@ -289,7 +323,7 @@ class LabViewModel extends ChangeNotifier {
PatientLabOrdersResponseModel laborder) async {
isLabResultByHospitalLoading = true;
notifyListeners();
mainLabResultsByHospitals.clear;
mainLabResultsByHospitals.clear();
final result = await labRepo.getPatientLabResultsByHospitals(laborder,
Utils.isVidaPlusProject(int.parse(laborder.projectID ?? "0")));
@ -332,8 +366,7 @@ class LabViewModel extends ChangeNotifier {
LoaderBottomSheet.hideLoader();
if (apiResponse.messageStatus == 2) {
} else if (apiResponse.messageStatus == 1) {
var sortedResponse = sortByFlagAndValue(apiResponse.data!);
var recentThree = sort(sortedResponse);
var recentThree = sort(apiResponse.data!);
mainLabResults = recentThree;
@ -615,19 +648,6 @@ class LabViewModel extends ChangeNotifier {
}
List<LabResult> sort(List<LabResult> original) {
DateTime? parseVerifiedDate(String? raw) {
if (raw == null) return null;
final regex = RegExp(r'\/Date\((\d+)\)\/');
final match = regex.firstMatch(raw);
if (match != null) {
final millis = int.tryParse(match.group(1)!);
if (millis != null) {
return DateTime.fromMillisecondsSinceEpoch(millis);
}
}
return null;
}
final copy = List<LabResult>.from(original);
copy.sort((a, b) {
@ -648,7 +668,7 @@ class LabViewModel extends ChangeNotifier {
final copy = List<DataPoint>.from(original);
copy.sort((a, b) {
final aDate = a.time;
final bDate = a.time;
final bDate = b.time;
final now = DateTime.now();
if (aDate == now && bDate == now) return 0;
if (aDate == now) return 1;

@ -35,6 +35,12 @@ class MedicalFileViewModel extends ChangeNotifier {
List<PatientVaccineResponseModel> patientVaccineList = [];
List<PatientSickLeavesResponseModel> patientSickLeaveList = [];
List<SickLeaveList> patientSickLeavesByClinic = [];
List<SickLeaveList> patientSickLeavesByHospital = [];
List<SickLeaveList> patientSickLeavesViewList = [];
bool isSickLeavesSortByClinic = true;
List<GetAllergiesResponseModel> patientAllergiesList = [];
List<PatientMedicalReportResponseModel> patientMedicalReportList = [];
@ -43,6 +49,12 @@ class MedicalFileViewModel extends ChangeNotifier {
List<PatientMedicalReportResponseModel> patientMedicalReportReadyList = [];
List<PatientMedicalReportResponseModel> patientMedicalReportCancelledList = [];
List<MedicalReportList> patientMedicalReportsByClinic = [];
List<MedicalReportList> patientMedicalReportsByHospital = [];
List<MedicalReportList> patientMedicalReportsViewList = [];
bool isMedicalReportsSortByClinic = true;
List<PatientAppointmentHistoryResponseModel> patientMedicalReportAppointmentHistoryList = [];
PatientAppointmentHistoryResponseModel? patientMedicalReportSelectedAppointment;
@ -89,6 +101,49 @@ class MedicalFileViewModel extends ChangeNotifier {
} else if (index == 2) {
patientMedicalReportList = patientMedicalReportCancelledList;
}
// Re-group by clinic/hospital for the selected tab
_groupMedicalReportsByClinicAndHospital();
notifyListeners();
}
void _groupMedicalReportsByClinicAndHospital() {
patientMedicalReportsByClinic.clear();
patientMedicalReportsByHospital.clear();
for (var element in patientMedicalReportList) {
// Group by clinic
List<MedicalReportList> reportsByClinic = patientMedicalReportsByClinic.where((elementClinic) => elementClinic.filterName == element.clinicDescription).toList();
if (reportsByClinic.isNotEmpty) {
patientMedicalReportsByClinic[patientMedicalReportsByClinic.indexOf(reportsByClinic[0])].medicalReportsList!.add(element);
} else {
patientMedicalReportsByClinic.add(MedicalReportList(filterName: element.clinicDescription, medicalReport: element));
}
// Group by hospital
List<MedicalReportList> reportsByHospital = patientMedicalReportsByHospital.where((elementHospital) => elementHospital.filterName == element.projectName).toList();
if (reportsByHospital.isNotEmpty) {
patientMedicalReportsByHospital[patientMedicalReportsByHospital.indexOf(reportsByHospital[0])].medicalReportsList!.add(element);
} else {
patientMedicalReportsByHospital.add(MedicalReportList(filterName: element.projectName, medicalReport: element));
}
}
if (isMedicalReportsSortByClinic) {
patientMedicalReportsViewList = patientMedicalReportsByClinic;
} else {
patientMedicalReportsViewList = patientMedicalReportsByHospital;
}
}
setIsMedicalReportsSortByClinic(bool value) {
isMedicalReportsSortByClinic = value;
if (isMedicalReportsSortByClinic) {
patientMedicalReportsViewList = patientMedicalReportsByClinic;
} else {
patientMedicalReportsViewList = patientMedicalReportsByHospital;
}
notifyListeners();
}
@ -109,17 +164,35 @@ class MedicalFileViewModel extends ChangeNotifier {
setIsPatientSickLeaveListLoading(bool val) {
if (val) {
patientSickLeaveList.clear();
patientSickLeavesByClinic.clear();
patientSickLeavesByHospital.clear();
patientSickLeavesViewList.clear();
patientSickLeavePDFBase64 = "";
isSickLeavesSortByClinic = true;
}
isPatientSickLeaveListLoading = val;
notifyListeners();
}
setIsSickLeavesSortByClinic(bool value) {
isSickLeavesSortByClinic = value;
if (isSickLeavesSortByClinic) {
patientSickLeavesViewList = patientSickLeavesByClinic;
} else {
patientSickLeavesViewList = patientSickLeavesByHospital;
}
notifyListeners();
}
setIsPatientMedicalReportsLoading(bool val) {
if (val) {
onMedicalReportTabChange(0);
patientMedicalReportList.clear();
patientMedicalReportsByClinic.clear();
patientMedicalReportsByHospital.clear();
patientMedicalReportsViewList.clear();
patientMedicalReportPDFBase64 = "";
isMedicalReportsSortByClinic = true;
}
isPatientMedicalReportsListLoading = val;
notifyListeners();
@ -213,6 +286,29 @@ class MedicalFileViewModel extends ChangeNotifier {
} else if (apiResponse.messageStatus == 1) {
patientSickLeaveList = apiResponse.data!;
isPatientSickLeaveListLoading = false;
// Group by clinic and hospital
for (var element in patientSickLeaveList) {
// Group by clinic
List<SickLeaveList> sickLeavesByClinic = patientSickLeavesByClinic.where((elementClinic) => elementClinic.filterName == element.clinicName).toList();
if (sickLeavesByClinic.isNotEmpty) {
patientSickLeavesByClinic[patientSickLeavesByClinic.indexOf(sickLeavesByClinic[0])].sickLeavesList!.add(element);
} else {
patientSickLeavesByClinic.add(SickLeaveList(filterName: element.clinicName, sickLeaves: element));
}
// Group by hospital
List<SickLeaveList> sickLeavesByHospital = patientSickLeavesByHospital.where((elementHospital) => elementHospital.filterName == element.projectName).toList();
if (sickLeavesByHospital.isNotEmpty) {
patientSickLeavesByHospital[patientSickLeavesByHospital.indexOf(sickLeavesByHospital[0])].sickLeavesList!.add(element);
} else {
patientSickLeavesByHospital.add(SickLeaveList(filterName: element.projectName, sickLeaves: element));
}
}
patientSickLeavesViewList = patientSickLeavesByClinic;
notifyListeners();
if (onSuccess != null) {
onSuccess(apiResponse);

@ -190,3 +190,16 @@ class PatientMedicalReportResponseModel {
return data;
}
}
class MedicalReportList {
String? filterName;
List<PatientMedicalReportResponseModel>? medicalReportsList;
MedicalReportList({this.filterName, PatientMedicalReportResponseModel? medicalReport}) {
medicalReportsList = [];
if (medicalReport != null) {
medicalReportsList!.add(medicalReport);
}
}
}

@ -174,3 +174,16 @@ class PatientSickLeavesResponseModel {
return data;
}
}
class SickLeaveList {
String? filterName;
List<PatientSickLeavesResponseModel>? sickLeavesList;
SickLeaveList({this.filterName, PatientSickLeavesResponseModel? sickLeaves}) {
sickLeavesList = [];
if (sickLeaves != null) {
sickLeavesList!.add(sickLeaves);
}
}
}

@ -60,6 +60,12 @@ class MyAppointmentsViewModel extends ChangeNotifier {
List<PatientAppointmentHistoryResponseModel> patientEyeMeasurementsAppointmentsHistoryList = [];
// Grouping by Clinic and Hospital
List<PatientAppointmentList> patientAppointmentsByClinic = [];
List<PatientAppointmentList> patientAppointmentsByHospital = [];
List<PatientAppointmentList> patientAppointmentsViewList = [];
bool isAppointmentsSortByClinic = true;
List<AskDocRequestType> askDoctorRequestTypeList = [];
PatientAppointmentShareResponseModel? patientAppointmentShareResponseModel;
@ -82,6 +88,43 @@ class MyAppointmentsViewModel extends ChangeNotifier {
notifyListeners();
}
setIsAppointmentsSortByClinic(bool value) {
isAppointmentsSortByClinic = value;
_groupAppointmentsByClinicAndHospital();
notifyListeners();
}
void _groupAppointmentsByClinicAndHospital() {
patientAppointmentsByClinic.clear();
patientAppointmentsByHospital.clear();
for (var element in filteredAppointmentList) {
// Group by clinic
List<PatientAppointmentList> appointmentsByClinic = patientAppointmentsByClinic.where((elementClinic) => elementClinic.filterName == element.clinicName).toList();
if (appointmentsByClinic.isNotEmpty) {
patientAppointmentsByClinic[patientAppointmentsByClinic.indexOf(appointmentsByClinic[0])].patientDoctorAppointmentList!.add(element);
} else {
patientAppointmentsByClinic.add(PatientAppointmentList(filterName: element.clinicName, patientDoctorAppointment: element));
}
// Group by hospital
List<PatientAppointmentList> appointmentsByHospital = patientAppointmentsByHospital.where((elementHospital) => elementHospital.filterName == element.projectName).toList();
if (appointmentsByHospital.isNotEmpty) {
patientAppointmentsByHospital[patientAppointmentsByHospital.indexOf(appointmentsByHospital[0])].patientDoctorAppointmentList!.add(element);
} else {
patientAppointmentsByHospital.add(PatientAppointmentList(filterName: element.projectName, patientDoctorAppointment: element));
}
}
if (isAppointmentsSortByClinic) {
patientAppointmentsViewList = patientAppointmentsByClinic;
} else {
patientAppointmentsViewList = patientAppointmentsByHospital;
}
}
initAppointmentsViewModel() {
if (isAppointmentDataToBeLoaded) {
patientAppointmentsHistoryList.clear();
@ -513,6 +556,7 @@ class MyAppointmentsViewModel extends ChangeNotifier {
break;
}
getFiltersForSelectedAppointmentList(filteredAppointmentList);
_groupAppointmentsByClinicAndHospital();
notifyListeners();
}
@ -570,6 +614,7 @@ class MyAppointmentsViewModel extends ChangeNotifier {
} catch (e) {}
}
}
_groupAppointmentsByClinicAndHospital();
notifyListeners();
}

@ -29,6 +29,18 @@ class RadiologyViewModel extends ChangeNotifier {
late PatientRadiologyResponseModel patientRadiologyOrderByAppointment;
// --- Grouping fields: By Clinic / By Hospital ---
bool isSortByClinic = true;
List<List<PatientRadiologyResponseModel>> patientRadiologyOrdersByClinic = [];
List<List<PatientRadiologyResponseModel>> patientRadiologyOrdersByHospital = [];
List<List<PatientRadiologyResponseModel>> patientRadiologyOrdersViewList = [];
void setIsSortByClinic(bool value) {
isSortByClinic = value;
patientRadiologyOrdersViewList = isSortByClinic ? patientRadiologyOrdersByClinic : patientRadiologyOrdersByHospital;
notifyListeners();
}
RadiologyViewModel({required this.radiologyRepo, required this.errorHandlerService, required this.navigationService});
initRadiologyViewModel() {

@ -4,6 +4,9 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/utils/date_util.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
@ -35,6 +38,7 @@ class MyAppointmentsPage extends StatefulWidget {
}
class _MyAppointmentsPageState extends State<MyAppointmentsPage> {
int? expandedIndex;
late MyAppointmentsViewModel myAppointmentsViewModel;
late BookAppointmentsViewModel bookAppointmentsViewModel;
@ -68,6 +72,9 @@ class _MyAppointmentsPageState extends State<MyAppointmentsPage> {
CustomTabBarModel(null, "Completed".needTranslation),
],
onTabChange: (index) {
setState(() {
expandedIndex = null;
});
myAppointmentsViewModel.onTabChange(index);
myAppointmentsViewModel.updateListWRTTab(index);
context.read<DateRangeSelectorRangeViewModel>().flush();
@ -91,17 +98,55 @@ class _MyAppointmentsPageState extends State<MyAppointmentsPage> {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 16.h),
// Clinic & Hospital Sort
Row(
children: [
CustomButton(
text: LocaleKeys.byClinic.tr(context: context),
onPressed: () {
myAppointmentsVM.setIsAppointmentsSortByClinic(true);
},
backgroundColor: myAppointmentsVM.isAppointmentsSortByClinic ? AppColors.bgRedLightColor : AppColors.whiteColor,
borderColor: myAppointmentsVM.isAppointmentsSortByClinic ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2),
textColor: myAppointmentsVM.isAppointmentsSortByClinic ? AppColors.primaryRedColor : AppColors.blackColor,
fontSize: 12,
fontWeight: FontWeight.w500,
borderRadius: 10,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 40.h,
),
SizedBox(width: 8.h),
CustomButton(
text: LocaleKeys.byHospital.tr(context: context),
onPressed: () {
myAppointmentsVM.setIsAppointmentsSortByClinic(false);
},
backgroundColor: myAppointmentsVM.isAppointmentsSortByClinic ? AppColors.whiteColor : AppColors.bgRedLightColor,
borderColor: myAppointmentsVM.isAppointmentsSortByClinic ? AppColors.textColor.withValues(alpha: 0.2) : AppColors.primaryRedColor,
textColor: myAppointmentsVM.isAppointmentsSortByClinic ? AppColors.blackColor : AppColors.primaryRedColor,
fontSize: 12,
fontWeight: FontWeight.w500,
borderRadius: 10,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 40.h,
),
],
).paddingSymmetrical(24.h, 0.h),
Visibility(visible: myAppointmentsVM.availableFilters.isNotEmpty, child: getAppointmentFilters(myAppointmentsVM)),
ListView.separated(
padding: EdgeInsets.only(top: 16.h),
SizedBox(height: 8.h),
// Expandable list
ListView.builder(
padding: EdgeInsets.only(top: 8.h),
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: myAppointmentsVM.isMyAppointmentsLoading
? 5
? 4
: filteredAppointmentList.isNotEmpty
? filteredAppointmentList.length
? myAppointmentsVM.patientAppointmentsViewList.length
: 1,
itemBuilder: (context, index) {
final isExpanded = expandedIndex == index;
return myAppointmentsVM.isMyAppointmentsLoading
? Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true),
@ -123,15 +168,149 @@ class _MyAppointmentsPageState extends State<MyAppointmentsPage> {
child: AnimatedContainer(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true),
child: AppointmentCard(
patientAppointmentHistoryResponseModel: filteredAppointmentList[index],
margin: EdgeInsets.symmetric(vertical: 8.h),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: true),
child: InkWell(
onTap: () {
setState(() {
expandedIndex = isExpanded ? null : index;
});
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.all(16.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
CustomButton(
text: "${myAppointmentsVM.patientAppointmentsViewList[index].patientDoctorAppointmentList!.length} Appointments",
onPressed: () {},
backgroundColor: AppColors.greyColor,
borderColor: AppColors.greyColor,
textColor: AppColors.blackColor,
fontSize: 10,
fontWeight: FontWeight.w500,
borderRadius: 8,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 30.h,
),
Icon(isExpanded ? Icons.expand_less : Icons.expand_more),
],
),
SizedBox(height: 8.h),
myAppointmentsVM.patientAppointmentsViewList[index].filterName!.toText16(isBold: true)
],
),
),
AnimatedSwitcher(
duration: Duration(milliseconds: 500),
switchInCurve: Curves.easeIn,
switchOutCurve: Curves.easeOut,
transitionBuilder: (Widget child, Animation<double> animation) {
return FadeTransition(
opacity: animation,
child: SizeTransition(
sizeFactor: animation,
axisAlignment: 0.0,
child: child,
),
);
},
child: isExpanded
? Container(
key: ValueKey<int>(index),
padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 8.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...myAppointmentsVM.patientAppointmentsViewList[index].patientDoctorAppointmentList!.map((appointment) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Image.network(
appointment.doctorImageURL!,
width: 24.h,
height: 24.h,
fit: BoxFit.fill,
).circle(100),
SizedBox(width: 8.h),
Expanded(child: appointment.doctorNameObj!.toText14(weight: FontWeight.w500)),
],
),
SizedBox(height: 8.h),
Row(
children: [
CustomButton(
text: DateUtil.formatDateToDate(DateUtil.convertStringToDate(appointment.appointmentDate), false),
onPressed: () {},
backgroundColor: AppColors.greyColor,
borderColor: AppColors.greyColor,
textColor: AppColors.blackColor,
fontSize: 10,
fontWeight: FontWeight.w500,
borderRadius: 8,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 24.h,
),
SizedBox(width: 8.h),
CustomButton(
text: myAppointmentsVM.isAppointmentsSortByClinic ? appointment.projectName! : appointment.clinicName!,
onPressed: () {},
backgroundColor: AppColors.greyColor,
borderColor: AppColors.greyColor,
textColor: AppColors.blackColor,
fontSize: 10,
fontWeight: FontWeight.w500,
borderRadius: 8,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 24.h,
),
SizedBox(width: 8.h),
CustomButton(
text: appointment.statusDesc ?? "",
onPressed: () {},
backgroundColor: AppColors.greyColor,
borderColor: AppColors.greyColor,
textColor: AppColors.blackColor,
fontSize: 10,
fontWeight: FontWeight.w500,
borderRadius: 8,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 24.h,
),
],
),
SizedBox(height: 8.h),
AppointmentCard(
patientAppointmentHistoryResponseModel: appointment,
myAppointmentsViewModel: myAppointmentsViewModel,
bookAppointmentsViewModel: bookAppointmentsViewModel,
isLoading: false,
isFromHomePage: false,
),
).paddingSymmetrical(24.h, 0.h),
SizedBox(height: 12.h),
Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.05), height: 1.h),
SizedBox(height: 12.h),
],
);
}),
],
),
)
: SizedBox.shrink(),
),
],
),
),
),
),
),
)
@ -160,8 +339,7 @@ class _MyAppointmentsPageState extends State<MyAppointmentsPage> {
).paddingSymmetrical(48.h, 0.h),
);
},
separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h),
),
).paddingSymmetrical(24.h, 0.h),
SizedBox(height: 24.h),
],
);

@ -1,6 +1,5 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
@ -21,37 +20,107 @@ import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
import 'package:provider/provider.dart';
import '../../widgets/chip/app_custom_chip_widget.dart';
import '../../widgets/buttons/custom_button.dart';
class MyDoctorsPage extends StatelessWidget {
class MyDoctorsPage extends StatefulWidget {
MyDoctorsPage({super.key});
@override
State<MyDoctorsPage> createState() => _MyDoctorsPageState();
}
class _MyDoctorsPageState extends State<MyDoctorsPage> {
late MyAppointmentsViewModel myAppointmentsViewModel;
late BookAppointmentsViewModel bookAppointmentsViewModel;
bool isSortByClinic = true; // default like lab page
int? expandedIndex;
final Map<int, GlobalKey> _groupKeys = {};
@override
Widget build(BuildContext context) {
AppState appState = getIt.get<AppState>();
myAppointmentsViewModel = Provider.of<MyAppointmentsViewModel>(context, listen: false);
bookAppointmentsViewModel = Provider.of<BookAppointmentsViewModel>(context, listen: false);
return Scaffold(
backgroundColor: AppColors.bgScaffoldColor,
body: CollapsingListView(
return CollapsingListView(
title: LocaleKeys.myDoctor.tr(context: context),
child: SingleChildScrollView(
child: Consumer<MyAppointmentsViewModel>(builder: (context, myAppointmentsVM, child) {
// build grouped lists from the flat list
final clinicMap = <String, List<dynamic>>{};
final hospitalMap = <String, List<dynamic>>{};
for (var doc in myAppointmentsVM.patientMyDoctorsList) {
final clinicKey = (doc.clinicName ?? 'Unknown').trim();
clinicMap.putIfAbsent(clinicKey, () => []).add(doc);
final hospitalKey = (doc.projectName ?? doc.projectID?.toString() ?? 'Unknown').toString().trim();
hospitalMap.putIfAbsent(hospitalKey, () => []).add(doc);
}
final patientMyDoctorsByClinic = clinicMap.values.toList();
final patientMyDoctorsByHospital = hospitalMap.values.toList();
final patientMyDoctorsViewList = isSortByClinic ? patientMyDoctorsByClinic : patientMyDoctorsByHospital;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 16.h),
// Add Clinic / Hospital toggle buttons styled like LabOrdersPage
Padding(
padding: EdgeInsets.symmetric(horizontal: 24.h),
child: Row(
children: [
CustomButton(
text: LocaleKeys.byClinic.tr(context: context),
onPressed: () {
setState(() {
isSortByClinic = true;
expandedIndex = null;
});
},
backgroundColor: isSortByClinic ? AppColors.bgRedLightColor : AppColors.whiteColor,
borderColor: isSortByClinic ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2),
textColor: isSortByClinic ? AppColors.primaryRedColor : AppColors.blackColor,
fontSize: 12,
fontWeight: FontWeight.w500,
borderRadius: 10,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 40.h,
),
SizedBox(width: 8.h),
CustomButton(
text: LocaleKeys.byHospital.tr(context: context),
onPressed: () {
setState(() {
isSortByClinic = false;
expandedIndex = null;
});
},
backgroundColor: isSortByClinic ? AppColors.whiteColor : AppColors.bgRedLightColor,
borderColor: isSortByClinic ? AppColors.textColor.withValues(alpha: 0.2) : AppColors.primaryRedColor,
textColor: isSortByClinic ? AppColors.blackColor : AppColors.primaryRedColor,
fontSize: 12,
fontWeight: FontWeight.w500,
borderRadius: 10,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 40.h,
),
],
),
),
SizedBox(height: 16.h),
ListView.separated(
scrollDirection: Axis.vertical,
itemCount: myAppointmentsVM.isPatientMyDoctorsLoading ? 5 : myAppointmentsVM.patientMyDoctorsList.length,
itemCount: myAppointmentsVM.isPatientMyDoctorsLoading ? 5 : (patientMyDoctorsViewList.isNotEmpty ? patientMyDoctorsViewList.length : 0),
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
padding: EdgeInsets.only(left: 24.h, right: 24.h),
itemBuilder: (context, index) {
return myAppointmentsVM.isPatientMyDoctorsLoading
? Container(
if (myAppointmentsVM.isPatientMyDoctorsLoading) {
return Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 20.h,
@ -95,19 +164,87 @@ class MyDoctorsPage extends StatelessWidget {
],
),
),
)
: AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 1000),
child: SlideAnimation(
verticalOffset: 100.0,
child: FadeInAnimation(
child: Container(
);
}
final group = patientMyDoctorsViewList[index];
final displayName = isSortByClinic ? (group.first.clinicName ?? 'Unknown') : (group.first.projectName ?? 'Unknown');
final isExpanded = expandedIndex == index;
return Container(
key: _groupKeys.putIfAbsent(index, () => GlobalKey()),
margin: EdgeInsets.only(bottom: 12.h),
padding: EdgeInsets.all(16.h),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 20.h,
hasShadow: true,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: () {
setState(() {
expandedIndex = isExpanded ? null : index;
});
WidgetsBinding.instance.addPostFrameCallback((_) {
final key = _groupKeys[index];
if (key != null && key.currentContext != null && expandedIndex == index) {
Scrollable.ensureVisible(
key.currentContext!,
duration: Duration(milliseconds: 350),
curve: Curves.easeInOut,
alignment: 0.1,
);
}
});
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
CustomButton(
text: "${group.length} ${'doctors'.needTranslation}",
onPressed: () {},
backgroundColor: AppColors.greyColor,
borderColor: AppColors.greyColor,
textColor: AppColors.blackColor,
fontSize: 10,
fontWeight: FontWeight.w500,
borderRadius: 8,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 30.h,
),
Icon(isExpanded ? Icons.expand_less : Icons.chevron_right, color: AppColors.greyColor),
],
),
SizedBox(height: 8.h),
Text(
displayName,
style: TextStyle(fontSize: 16.h, fontWeight: FontWeight.w600),
overflow: TextOverflow.ellipsis,
),
],
),
),
AnimatedSwitcher(
duration: Duration(milliseconds: 400),
child: isExpanded
? Container(
key: ValueKey<int>(index),
padding: EdgeInsets.only(top: 12.h),
child: Column(
children: group.map<Widget>((doctor) {
return Container(
margin: EdgeInsets.only(bottom: 12.h),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 12.h,
hasShadow: true,
),
child: Padding(
padding: EdgeInsets.all(14.h),
child: Column(
@ -117,50 +254,108 @@ class MyDoctorsPage extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.network(
myAppointmentsVM.patientMyDoctorsList[index].doctorImageURL!,
width: 63.h,
height: 63.h,
(doctor?.doctorImageURL ?? doctor?.doctorImage ?? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png"),
width: 24.h,
height: 24.h,
fit: BoxFit.cover,
).circle(100).toShimmer2(isShow: false),
SizedBox(width: 16.h),
).circle(100),
SizedBox(width: 8.h),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
(myAppointmentsVM.patientMyDoctorsList[index].doctorName).toString().toText16(isBold: true).toShimmer2(isShow: false),
SizedBox(height: 8.h),
Wrap(
direction: Axis.horizontal,
spacing: 3.h,
runSpacing: 4.h,
children: [
AppCustomChipWidget(labelText: myAppointmentsVM.patientMyDoctorsList[index].clinicName).toShimmer2(isShow: false, width: 16.h),
AppCustomChipWidget(labelText: myAppointmentsVM.patientMyDoctorsList[index].projectName).toShimmer2(isShow: false, width: 16.h),
(doctor?.doctorName ?? "").toString().toText14(weight: FontWeight.w500),
SizedBox(height: 6.h),
],
),
),
],
),
SizedBox(height: 8.h),
Row(
children: [
CustomButton(
text: isSortByClinic ? (doctor?.clinicName ?? "") : (doctor?.projectName ?? ""),
onPressed: () {},
backgroundColor: AppColors.greyColor,
borderColor: AppColors.greyColor,
textColor: AppColors.blackColor,
fontSize: 10,
fontWeight: FontWeight.w500,
borderRadius: 8,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 24.h,
),
],
),
SizedBox(height: 8.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
"".toText16(),
Transform.flip(
flipX: appState.isArabic(),
child:
Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon_small, width: 15.h, height: 15.h, fit: BoxFit.contain, iconColor: AppColors.textColor)),
],
Expanded(
flex: 6,
child: CustomButton(
text: "View Profile".needTranslation.tr(context: context),
onPressed: () async {
bookAppointmentsViewModel.setSelectedDoctor(DoctorsListResponseModel(
clinicID: doctor?.clinicID ?? 0,
projectID: doctor?.projectID ?? 0,
doctorID: doctor?.doctorID ?? 0,
));
LoaderBottomSheet.showLoader();
await bookAppointmentsViewModel.getDoctorProfile(onSuccess: (dynamic respData) {
LoaderBottomSheet.hideLoader();
Navigator.of(context).push(
CustomPageRoute(
page: DoctorProfilePage(),
),
);
}, onError: (err) {
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: err),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
});
},
backgroundColor: AppColors.bgRedLightColor,
borderColor: AppColors.primaryRedColor,
textColor: AppColors.primaryRedColor,
fontSize: 14,
fontWeight: FontWeight.w500,
borderRadius: 12,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 40.h,
),
),
SizedBox(width: 8.h),
Expanded(
flex: 1,
child: Container(
height: 40.h,
width: 40.w,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.textColor,
borderRadius: 12,
),
child: Padding(
padding: EdgeInsets.all(12.h),
child: Transform.flip(
flipX: getIt<AppState>().isArabic(),
child: Utils.buildSvgWithAssets(
icon: AppAssets.forward_arrow_icon_small,
iconColor: AppColors.whiteColor,
fit: BoxFit.contain,
),
],
),
),
).onPress(() async {
bookAppointmentsViewModel.setSelectedDoctor(DoctorsListResponseModel(
clinicID: myAppointmentsVM.patientMyDoctorsList[index].clinicID,
projectID: myAppointmentsVM.patientMyDoctorsList[index].projectID,
doctorID: myAppointmentsVM.patientMyDoctorsList[index].doctorID,
clinicID: doctor?.clinicID ?? 0,
projectID: doctor?.projectID ?? 0,
doctorID: doctor?.doctorID ?? 0,
));
LoaderBottomSheet.showLoader();
await bookAppointmentsViewModel.getDoctorProfile(onSuccess: (dynamic respData) {
@ -182,6 +377,18 @@ class MyDoctorsPage extends StatelessWidget {
});
}),
),
],
),
],
),
),
);
}).toList(),
),
)
: SizedBox.shrink(),
),
],
),
);
},
@ -191,8 +398,6 @@ class MyDoctorsPage extends StatelessWidget {
],
);
}),
),
),
);
}
}

@ -352,14 +352,14 @@ class AppointmentCard extends StatelessWidget {
backgroundColor: AppColors.greyColor,
borderColor: AppColors.greyColor,
textColor: AppColors.blackColor,
fontSize: 14.f,
fontSize: 12.f,
fontWeight: FontWeight.w500,
borderRadius: 12.r,
padding: EdgeInsets.symmetric(horizontal: 10.w),
height: 40.h,
icon: AppAssets.rebook_appointment_icon,
iconColor: AppColors.blackColor,
iconSize: 16.h,
iconSize: 14.h,
);
}

@ -167,14 +167,14 @@ class AppointmentDoctorCard extends StatelessWidget {
backgroundColor: AppColors.greyColor,
borderColor: AppColors.greyColor,
textColor: AppColors.blackColor,
fontSize: 14.f,
fontSize: 12.f,
fontWeight: FontWeight.w500,
borderRadius: 12.r,
padding: EdgeInsets.fromLTRB(10.w, 0, 10.w, 0),
height: 40.h,
icon: AppAssets.rebook_appointment_icon,
iconColor: AppColors.blackColor,
iconSize: 16.h,
iconSize: 14.h,
);
} else {
return patientAppointmentHistoryResponseModel.isLiveCareAppointment ?? false

@ -106,18 +106,24 @@ class _LandingPageState extends State<LandingPage> {
insuranceViewModel.initInsuranceProvider();
emergencyServicesViewModel.checkPatientERAdvanceBalance();
myAppointmentsViewModel.getPatientAppointmentQueueDetails();
if(!appState.isRatedVisible) {
appointmentRatingViewModel.getLastRatingAppointment(onSuccess: (response) {
if (appointmentRatingViewModel.appointmentRatedList.isNotEmpty) {
appointmentRatingViewModel.getAppointmentDetails(appointmentRatingViewModel.appointmentRatedList.last.appointmentNo!, appointmentRatingViewModel.appointmentRatedList.last.projectID!,
onSuccess: ((response) {
appointmentRatingViewModel.setClinicOrDoctor(false);
appointmentRatingViewModel.setTitle("Rate Doctor".needTranslation);
appointmentRatingViewModel.setSubTitle("How was your last visit with doctor?".needTranslation);
openLastRating();
appState.setRatedVisible(true);
}));
}
});
}
}
});
super.initState();
}

File diff suppressed because one or more lines are too long

@ -3,6 +3,10 @@ import 'dart:async';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/utils/date_util.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
@ -12,6 +16,9 @@ import 'package:hmg_patient_app_new/features/medical_file/models/patient_sicklea
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart';
import 'package:open_filex/open_filex.dart';
import 'package:provider/provider.dart';
import 'widgets/patient_sick_leave_card.dart';
@ -24,6 +31,7 @@ class PatientSickleavesListPage extends StatefulWidget {
}
class _PatientSickleavesListPageState extends State<PatientSickleavesListPage> {
int? expandedIndex;
late MedicalFileViewModel medicalFileViewModel;
@override
@ -37,53 +45,257 @@ class _PatientSickleavesListPageState extends State<PatientSickleavesListPage> {
@override
Widget build(BuildContext context) {
AppState appState = getIt.get<AppState>();
medicalFileViewModel = Provider.of<MedicalFileViewModel>(context, listen: false);
return Scaffold(
backgroundColor: AppColors.bgScaffoldColor,
body: CollapsingListView(
title: "${LocaleKeys.sick.tr(context: context)} ${LocaleKeys.sickSubtitle.tr(context: context)}",
child: SingleChildScrollView(
child: Consumer<MedicalFileViewModel>(builder: (context, medicalFileVM, child) {
child: Consumer<MedicalFileViewModel>(builder: (context, model, child) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(height: 24.h),
ListView.separated(
padding: EdgeInsets.zero,
scrollDirection: Axis.vertical,
itemCount: medicalFileVM.isPatientSickLeaveListLoading
? 3
: medicalFileVM.patientSickLeaveList.isNotEmpty
? medicalFileVM.patientSickLeaveList.length
SizedBox(height: 16.h),
// Clinic & Hospital Sort
Row(
children: [
CustomButton(
text: LocaleKeys.byClinic.tr(context: context),
onPressed: () {
model.setIsSickLeavesSortByClinic(true);
},
backgroundColor: model.isSickLeavesSortByClinic ? AppColors.bgRedLightColor : AppColors.whiteColor,
borderColor: model.isSickLeavesSortByClinic ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2),
textColor: model.isSickLeavesSortByClinic ? AppColors.primaryRedColor : AppColors.blackColor,
fontSize: 12,
fontWeight: FontWeight.w500,
borderRadius: 10,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 40.h,
),
SizedBox(width: 8.h),
CustomButton(
text: LocaleKeys.byHospital.tr(context: context),
onPressed: () {
model.setIsSickLeavesSortByClinic(false);
},
backgroundColor: model.isSickLeavesSortByClinic ? AppColors.whiteColor : AppColors.bgRedLightColor,
borderColor: model.isSickLeavesSortByClinic ? AppColors.textColor.withValues(alpha: 0.2) : AppColors.primaryRedColor,
textColor: model.isSickLeavesSortByClinic ? AppColors.blackColor : AppColors.primaryRedColor,
fontSize: 12,
fontWeight: FontWeight.w500,
borderRadius: 10,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 40.h,
),
],
).paddingSymmetrical(24.h, 0.h),
SizedBox(height: 20.h),
// Expandable list
ListView.builder(
itemCount: model.isPatientSickLeaveListLoading
? 4
: model.patientSickLeaveList.isNotEmpty
? model.patientSickLeavesViewList.length
: 1,
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
padding: const EdgeInsets.only(left: 0, right: 8),
itemBuilder: (context, index) {
return medicalFileVM.isPatientSickLeaveListLoading
final isExpanded = expandedIndex == index;
return model.isPatientSickLeaveListLoading
? PatientSickLeaveCard(
patientSickLeavesResponseModel: PatientSickLeavesResponseModel(),
isLoading: true,
).paddingSymmetrical(24.h, 0.0)
: medicalFileVM.patientSickLeaveList.isNotEmpty
: model.patientSickLeaveList.isNotEmpty
? AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 1000),
duration: const Duration(milliseconds: 500),
child: SlideAnimation(
verticalOffset: 100.0,
child: FadeInAnimation(
child: PatientSickLeaveCard(
patientSickLeavesResponseModel: medicalFileVM.patientSickLeaveList.first,
isLoading: false,
isSickLeaveListPage: true,
).paddingSymmetrical(24.h, 0.0),
child: AnimatedContainer(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
margin: EdgeInsets.symmetric(vertical: 8.h),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: true),
child: InkWell(
onTap: () {
setState(() {
expandedIndex = isExpanded ? null : index;
});
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.all(16.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
CustomButton(
text: "${model.patientSickLeavesViewList[index].sickLeavesList!.length} ${LocaleKeys.sickSubtitle.tr(context: context)} Available",
onPressed: () {},
backgroundColor: AppColors.greyColor,
borderColor: AppColors.greyColor,
textColor: AppColors.blackColor,
fontSize: 10,
fontWeight: FontWeight.w500,
borderRadius: 8,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 30.h,
),
Icon(isExpanded ? Icons.expand_less : Icons.expand_more),
],
),
SizedBox(height: 8.h),
model.patientSickLeavesViewList[index].filterName!.toText16(isBold: true)
],
),
),
AnimatedSwitcher(
duration: Duration(milliseconds: 500),
switchInCurve: Curves.easeIn,
switchOutCurve: Curves.easeOut,
transitionBuilder: (Widget child, Animation<double> animation) {
return FadeTransition(
opacity: animation,
child: SizeTransition(
sizeFactor: animation,
axisAlignment: 0.0,
child: child,
),
);
},
child: isExpanded
? Container(
key: ValueKey<int>(index),
padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 8.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...model.patientSickLeavesViewList[index].sickLeavesList!.map((sickLeave) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Image.network(
sickLeave.doctorImageURL!,
width: 24.h,
height: 24.h,
fit: BoxFit.fill,
).circle(100),
SizedBox(width: 8.h),
Expanded(child: sickLeave.doctorName!.toText14(weight: FontWeight.w500)),
],
),
SizedBox(height: 8.h),
Row(
children: [
CustomButton(
text: DateUtil.formatDateToDate(DateUtil.convertStringToDate(sickLeave.appointmentDate), false),
onPressed: () {},
backgroundColor: AppColors.greyColor,
borderColor: AppColors.greyColor,
textColor: AppColors.blackColor,
fontSize: 10,
fontWeight: FontWeight.w500,
borderRadius: 8,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 24.h,
),
SizedBox(width: 8.h),
CustomButton(
text: model.isSickLeavesSortByClinic ? sickLeave.projectName! : sickLeave.clinicName!,
onPressed: () {},
backgroundColor: AppColors.greyColor,
borderColor: AppColors.greyColor,
textColor: AppColors.blackColor,
fontSize: 10,
fontWeight: FontWeight.w500,
borderRadius: 8,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 24.h,
),
SizedBox(width: 8.h),
CustomButton(
text: "${sickLeave.sickLeaveDays} Days",
onPressed: () {},
backgroundColor: AppColors.greyColor,
borderColor: AppColors.greyColor,
textColor: AppColors.blackColor,
fontSize: 10,
fontWeight: FontWeight.w500,
borderRadius: 8,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 24.h,
),
],
),
SizedBox(height: 8.h),
Row(
children: [
Expanded(
flex: 6,
child: CustomButton(
text: "Download Report".needTranslation,
onPressed: () async {
LoaderBottomSheet.showLoader();
await medicalFileViewModel.getPatientSickLeavePDF(sickLeave, appState.getAuthenticatedUser()!).then((val) async {
LoaderBottomSheet.hideLoader();
if (medicalFileViewModel.patientSickLeavePDFBase64.isNotEmpty) {
String path = await Utils.createFileFromString(medicalFileViewModel.patientSickLeavePDFBase64, "pdf");
try {
OpenFilex.open(path);
} catch (ex) {
debugPrint("Error opening file: $ex");
}
}
});
},
backgroundColor: AppColors.secondaryLightRedColor,
borderColor: AppColors.secondaryLightRedColor,
textColor: AppColors.primaryRedColor,
fontSize: 14,
fontWeight: FontWeight.w500,
borderRadius: 12,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 40.h,
icon: AppAssets.download,
iconColor: AppColors.primaryRedColor,
iconSize: 14.h,
),
),
],
),
SizedBox(height: 12.h),
Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.05), height: 1.h),
SizedBox(height: 12.h),
],
);
}),
],
),
)
: SizedBox.shrink(),
),
],
),
),
),
),
),
)
: Utils.getNoDataWidget(context, noDataText: "You don't have any sick leaves yet.".needTranslation);
},
separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 8.h),
),
SizedBox(height: 60.h),
).paddingSymmetrical(24.h, 0.h),
],
);
}),

@ -2,6 +2,9 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/utils/date_util.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
@ -17,6 +20,7 @@ import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
import 'package:hmg_patient_app_new/widgets/common_bottom_sheet.dart';
import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.dart';
import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
import 'package:open_filex/open_filex.dart';
import 'package:provider/provider.dart';
class MedicalReportsPage extends StatefulWidget {
@ -27,10 +31,12 @@ class MedicalReportsPage extends StatefulWidget {
}
class _MedicalReportsPageState extends State<MedicalReportsPage> {
int? expandedIndex;
late MedicalFileViewModel medicalFileViewModel;
@override
Widget build(BuildContext context) {
AppState appState = getIt.get<AppState>();
medicalFileViewModel = Provider.of<MedicalFileViewModel>(context, listen: false);
return Scaffold(
backgroundColor: AppColors.bgScaffoldColor,
@ -44,16 +50,53 @@ class _MedicalReportsPageState extends State<MedicalReportsPage> {
return Column(
children: [
SizedBox(height: 16.h),
// Status filter tabs
Row(
children: [
CustomButton(
text: LocaleKeys.byClinic.tr(context: context),
onPressed: () {
medicalFileVM.setIsMedicalReportsSortByClinic(true);
},
backgroundColor: medicalFileVM.isMedicalReportsSortByClinic ? AppColors.bgRedLightColor : AppColors.whiteColor,
borderColor: medicalFileVM.isMedicalReportsSortByClinic ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2),
textColor: medicalFileVM.isMedicalReportsSortByClinic ? AppColors.primaryRedColor : AppColors.blackColor,
fontSize: 12,
fontWeight: FontWeight.w500,
borderRadius: 10,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 40.h,
),
SizedBox(width: 8.h),
CustomButton(
text: LocaleKeys.byHospital.tr(context: context),
onPressed: () {
medicalFileVM.setIsMedicalReportsSortByClinic(false);
},
backgroundColor: medicalFileVM.isMedicalReportsSortByClinic ? AppColors.whiteColor : AppColors.bgRedLightColor,
borderColor: medicalFileVM.isMedicalReportsSortByClinic ? AppColors.textColor.withValues(alpha: 0.2) : AppColors.primaryRedColor,
textColor: medicalFileVM.isMedicalReportsSortByClinic ? AppColors.blackColor : AppColors.primaryRedColor,
fontSize: 12,
fontWeight: FontWeight.w500,
borderRadius: 10,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 40.h,
),
],
).paddingSymmetrical(24.h, 0.h),
SizedBox(height: 20.h),
Row(
children: [
CustomButton(
text: "Requested".needTranslation,
onPressed: () {
setState(() {
expandedIndex = null;
});
medicalFileViewModel.onMedicalReportTabChange(0);
},
backgroundColor: medicalFileVM.selectedMedicalReportsTabIndex == 0 ? AppColors.bgRedLightColor : AppColors.whiteColor,
borderColor:
medicalFileVM.selectedMedicalReportsTabIndex == 0 ? AppColors.primaryRedColor : AppColors.textColor.withOpacity(0.2),
borderColor: medicalFileVM.selectedMedicalReportsTabIndex == 0 ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2),
textColor: medicalFileVM.selectedMedicalReportsTabIndex == 0 ? AppColors.primaryRedColor : AppColors.blackColor,
fontSize: 12,
fontWeight: FontWeight.w500,
@ -65,11 +108,13 @@ class _MedicalReportsPageState extends State<MedicalReportsPage> {
CustomButton(
text: LocaleKeys.ready.tr(context: context),
onPressed: () {
setState(() {
expandedIndex = null;
});
medicalFileViewModel.onMedicalReportTabChange(1);
},
backgroundColor: medicalFileVM.selectedMedicalReportsTabIndex == 1 ? AppColors.bgRedLightColor : AppColors.whiteColor,
borderColor:
medicalFileVM.selectedMedicalReportsTabIndex == 1 ? AppColors.primaryRedColor : AppColors.textColor.withOpacity(0.2),
borderColor: medicalFileVM.selectedMedicalReportsTabIndex == 1 ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2),
textColor: medicalFileVM.selectedMedicalReportsTabIndex == 1 ? AppColors.primaryRedColor : AppColors.blackColor,
fontSize: 12,
fontWeight: FontWeight.w500,
@ -81,11 +126,13 @@ class _MedicalReportsPageState extends State<MedicalReportsPage> {
CustomButton(
text: LocaleKeys.cancelled.tr(context: context),
onPressed: () {
setState(() {
expandedIndex = null;
});
medicalFileViewModel.onMedicalReportTabChange(2);
},
backgroundColor: medicalFileVM.selectedMedicalReportsTabIndex == 2 ? AppColors.bgRedLightColor : AppColors.whiteColor,
borderColor:
medicalFileVM.selectedMedicalReportsTabIndex == 2 ? AppColors.primaryRedColor : AppColors.textColor.withOpacity(0.2),
borderColor: medicalFileVM.selectedMedicalReportsTabIndex == 2 ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2),
textColor: medicalFileVM.selectedMedicalReportsTabIndex == 2 ? AppColors.primaryRedColor : AppColors.blackColor,
fontSize: 12,
fontWeight: FontWeight.w500,
@ -95,34 +142,27 @@ class _MedicalReportsPageState extends State<MedicalReportsPage> {
),
],
).paddingSymmetrical(24.h, 0.h),
// CustomTabBar(
// activeTextColor: Color(0xffED1C2B),
// activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1),
// tabs: [
// CustomTabBarModel(null, "Requested".needTranslation),
// CustomTabBarModel(null, "Ready".needTranslation),
// CustomTabBarModel(null, "Cancelled".needTranslation),
// ],
// onTabChange: (index) {
// medicalFileViewModel.onMedicalReportTabChange(index);
// },
// ).paddingSymmetrical(24.h, 0.h),
ListView.separated(
padding: EdgeInsets.only(top: 24.h),
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
SizedBox(height: 12.h),
// Clinic & Hospital Sort
// Expandable list
ListView.builder(
itemCount: medicalFileViewModel.isPatientMedicalReportsListLoading
? 3
? 4
: medicalFileViewModel.patientMedicalReportList.isNotEmpty
? medicalFileViewModel.patientMedicalReportList.length
? medicalFileViewModel.patientMedicalReportsViewList.length
: 1,
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
padding: const EdgeInsets.only(left: 0, right: 8),
itemBuilder: (context, index) {
final isExpanded = expandedIndex == index;
return medicalFileViewModel.isPatientMedicalReportsListLoading
? PatientMedicalReportCard(
patientMedicalReportResponseModel: PatientMedicalReportResponseModel(),
medicalFileViewModel: medicalFileVM,
isLoading: true,
).paddingSymmetrical(24.h, 0.h)
).paddingSymmetrical(24.h, 8.h)
: medicalFileViewModel.patientMedicalReportList.isNotEmpty
? AnimationConfiguration.staggeredList(
position: index,
@ -133,22 +173,185 @@ class _MedicalReportsPageState extends State<MedicalReportsPage> {
child: AnimatedContainer(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
decoration: RoundedRectangleBorder()
.toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true),
child: PatientMedicalReportCard(
patientMedicalReportResponseModel: medicalFileVM.patientMedicalReportList[index],
medicalFileViewModel: medicalFileVM,
isLoading: false,
margin: EdgeInsets.symmetric(vertical: 8.h),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: true),
child: InkWell(
onTap: () {
setState(() {
expandedIndex = isExpanded ? null : index;
});
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.all(16.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
CustomButton(
text: "${medicalFileVM.patientMedicalReportsViewList[index].medicalReportsList!.length} Reports Available",
onPressed: () {},
backgroundColor: AppColors.greyColor,
borderColor: AppColors.greyColor,
textColor: AppColors.blackColor,
fontSize: 10,
fontWeight: FontWeight.w500,
borderRadius: 8,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 30.h,
),
Icon(isExpanded ? Icons.expand_less : Icons.expand_more),
],
),
SizedBox(height: 8.h),
medicalFileVM.patientMedicalReportsViewList[index].filterName!.toText16(isBold: true)
],
),
),
AnimatedSwitcher(
duration: Duration(milliseconds: 500),
switchInCurve: Curves.easeIn,
switchOutCurve: Curves.easeOut,
transitionBuilder: (Widget child, Animation<double> animation) {
return FadeTransition(
opacity: animation,
child: SizeTransition(
sizeFactor: animation,
axisAlignment: 0.0,
child: child,
),
);
},
child: isExpanded
? Container(
key: ValueKey<int>(index),
padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 8.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...medicalFileVM.patientMedicalReportsViewList[index].medicalReportsList!.map((report) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Image.network(
report.doctorImageURL!,
width: 24.h,
height: 24.h,
fit: BoxFit.fill,
).circle(100),
SizedBox(width: 8.h),
Expanded(child: report.doctorName!.toText14(weight: FontWeight.w500)),
],
),
SizedBox(height: 8.h),
Row(
children: [
CustomButton(
text: DateUtil.formatDateToDate(DateUtil.convertStringToDate(report.appointmentDate), false),
onPressed: () {},
backgroundColor: AppColors.greyColor,
borderColor: AppColors.greyColor,
textColor: AppColors.blackColor,
fontSize: 10,
fontWeight: FontWeight.w500,
borderRadius: 8,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 24.h,
),
SizedBox(width: 8.h),
CustomButton(
text: medicalFileVM.isMedicalReportsSortByClinic ? report.projectName! : report.clinicDescription!,
onPressed: () {},
backgroundColor: AppColors.greyColor,
borderColor: AppColors.greyColor,
textColor: AppColors.blackColor,
fontSize: 10,
fontWeight: FontWeight.w500,
borderRadius: 8,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 24.h,
),
SizedBox(width: 8.h),
CustomButton(
text: report.statusDesc!,
onPressed: () {},
backgroundColor: AppColors.greyColor,
borderColor: AppColors.greyColor,
textColor: AppColors.blackColor,
fontSize: 10,
fontWeight: FontWeight.w500,
borderRadius: 8,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 24.h,
),
],
),
SizedBox(height: 8.h),
Row(
children: [
if (medicalFileVM.selectedMedicalReportsTabIndex == 1)
Expanded(
flex: 6,
child: CustomButton(
text: "Download Report".needTranslation,
onPressed: () async {
LoaderBottomSheet.showLoader();
await medicalFileViewModel.getPatientMedicalReportPDF(report, appState.getAuthenticatedUser()!).then((val) async {
LoaderBottomSheet.hideLoader();
if (medicalFileViewModel.patientMedicalReportPDFBase64.isNotEmpty) {
String path = await Utils.createFileFromString(medicalFileViewModel.patientMedicalReportPDFBase64, "pdf");
try {
OpenFilex.open(path);
} catch (ex) {
debugPrint("Error opening file: $ex");
}
}
});
},
backgroundColor: AppColors.secondaryLightRedColor,
borderColor: AppColors.secondaryLightRedColor,
textColor: AppColors.primaryRedColor,
fontSize: 14,
fontWeight: FontWeight.w500,
borderRadius: 12,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 40.h,
icon: AppAssets.download,
iconColor: AppColors.primaryRedColor,
iconSize: 14.h,
),
),
],
),
SizedBox(height: 12.h),
Divider(color: AppColors.borderOnlyColor.withValues(alpha: 0.05), height: 1.h),
SizedBox(height: 12.h),
],
);
}),
],
),
)
: SizedBox.shrink(),
),
],
),
),
),
).paddingSymmetrical(24.h, 0.h),
),
),
)
: Utils.getNoDataWidget(context, noDataText: "You don't have any medical reports yet.".needTranslation)
.paddingSymmetrical(24.h, 24.h);
},
separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h),
),
).paddingSymmetrical(24.h, 0.h),
SizedBox(height: 24.h),
],
);

@ -3,13 +3,13 @@ import 'dart:async';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
import 'package:hmg_patient_app_new/core/utils/date_util.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/lab/lab_view_model.dart';
import 'package:hmg_patient_app_new/features/radiology/models/resp_models/patient_radiology_response_model.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/lab/lab_result_item_view.dart';
import 'package:hmg_patient_app_new/presentation/radiology/radiology_result_page.dart';
@ -22,6 +22,7 @@ import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
import 'package:provider/provider.dart';
import '../../features/radiology/radiology_view_model.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
class RadiologyOrdersPage extends StatefulWidget {
const RadiologyOrdersPage({super.key});
@ -34,15 +35,25 @@ class _RadiologyOrdersPageState extends State<RadiologyOrdersPage> {
late RadiologyViewModel radiologyViewModel;
String selectedFilterText = '';
int? expandedIndex;
// Scroll controller to ensure expanded group is visible
late ScrollController _scrollController;
final Map<int, GlobalKey> _groupKeys = {};
@override
void initState() {
_scrollController = ScrollController();
scheduleMicrotask(() {
radiologyViewModel.initRadiologyViewModel();
});
super.initState();
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
radiologyViewModel = Provider.of<RadiologyViewModel>(context);
@ -69,17 +80,71 @@ class _RadiologyOrdersPageState extends State<RadiologyOrdersPage> {
}
},
child: SingleChildScrollView(
controller: _scrollController,
physics: NeverScrollableScrollPhysics(),
child: Consumer<RadiologyViewModel>(
builder: (context, model, child) {
// Build grouping lists if we have data and none constructed yet
if (!model.isRadiologyOrdersLoading && model.patientRadiologyOrders.isNotEmpty && model.patientRadiologyOrdersViewList.isEmpty) {
final clinicMap = <String, List<PatientRadiologyResponseModel>>{};
final hospitalMap = <String, List<PatientRadiologyResponseModel>>{};
for (var order in model.patientRadiologyOrders) {
final clinicKey = (order.clinicDescription ?? 'Unknown').trim();
clinicMap.putIfAbsent(clinicKey, () => []).add(order);
final hospitalKey = (order.projectName ?? order.projectID ?? 'Unknown').toString().trim();
hospitalMap.putIfAbsent(hospitalKey, () => []).add(order);
}
model.patientRadiologyOrdersByClinic = clinicMap.values.toList();
model.patientRadiologyOrdersByHospital = hospitalMap.values.toList();
model.patientRadiologyOrdersViewList = model.isSortByClinic ? model.patientRadiologyOrdersByClinic : model.patientRadiologyOrdersByHospital;
}
return Padding(
padding: EdgeInsets.symmetric(horizontal: 24.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
selectedFilterText!.isNotEmpty
// Clinic / Hospital toggle
SizedBox(height: 16.h),
Row(
children: [
CustomButton(
text: LocaleKeys.byClinic.tr(context: context),
onPressed: () {
model.setIsSortByClinic(true);
},
backgroundColor: model.isSortByClinic ? AppColors.bgRedLightColor : AppColors.whiteColor,
borderColor: model.isSortByClinic ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2),
textColor: model.isSortByClinic ? AppColors.primaryRedColor : AppColors.blackColor,
fontSize: 12,
fontWeight: FontWeight.w500,
borderRadius: 10,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 40.h,
),
SizedBox(width: 8.h),
CustomButton(
text: LocaleKeys.byHospital.tr(context: context),
onPressed: () {
model.setIsSortByClinic(false);
},
backgroundColor: model.isSortByClinic ? AppColors.whiteColor : AppColors.bgRedLightColor,
borderColor: model.isSortByClinic ? AppColors.textColor.withValues(alpha: 0.2) : AppColors.primaryRedColor,
textColor: model.isSortByClinic ? AppColors.blackColor : AppColors.primaryRedColor,
fontSize: 12,
fontWeight: FontWeight.w500,
borderRadius: 10,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 40.h,
),
],
),
SizedBox(height: 8.h),
selectedFilterText.isNotEmpty
? AppCustomChipWidget(
padding: EdgeInsets.symmetric(horizontal: 5.h),
labelText: selectedFilterText!,
labelText: selectedFilterText,
deleteIcon: 'assets/images/svg/cross_circle.svg',
backgroundColor: AppColors.alertColor,
textColor: AppColors.whiteColor,
@ -95,157 +160,268 @@ class _RadiologyOrdersPageState extends State<RadiologyOrdersPage> {
// isSelected: true,
)
: SizedBox(),
ListView.builder(
// Grouped view when available
Builder(builder: (ctx) {
if (model.isRadiologyOrdersLoading) {
return ListView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: model.isRadiologyOrdersLoading
? 5
: model.patientRadiologyOrders.isNotEmpty
? model.patientRadiologyOrders.length
: 1,
itemBuilder: (context, index) {
final isExpanded = expandedIndex == index;
return model.isRadiologyOrdersLoading
? LabResultItemView(
padding: EdgeInsets.zero,
itemCount: 5,
itemBuilder: (context, index) => LabResultItemView(
onTap: () {},
labOrder: null,
index: index,
isLoading: true,
)
: model.patientRadiologyOrders.isNotEmpty
? AnimationConfiguration.staggeredList(
),
);
}
if (model.patientRadiologyOrdersViewList.isEmpty) {
return Utils.getNoDataWidget(ctx, noDataText: "You don't have any radiology results yet.".needTranslation);
}
return ListView.builder(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
padding: EdgeInsets.zero,
itemCount: model.patientRadiologyOrdersViewList.length,
itemBuilder: (context, index) {
final group = model.patientRadiologyOrdersViewList[index];
final displayName = model.isSortByClinic ? (group.first.clinicDescription ?? 'Unknown') : (group.first.projectName ?? 'Unknown');
return AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 500),
duration: const Duration(milliseconds: 400),
child: SlideAnimation(
verticalOffset: 100.0,
verticalOffset: 50.0,
child: FadeInAnimation(
child: AnimatedContainer(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
margin: EdgeInsets.symmetric(vertical: 8.h),
decoration: RoundedRectangleBorder()
.toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: true),
child: InkWell(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Group header container with key so we can scroll to it
GestureDetector(
onTap: () {
setState(() {
expandedIndex = isExpanded ? null : index;
expandedIndex = expandedIndex == index ? null : index;
});
WidgetsBinding.instance.addPostFrameCallback((_) {
final key = _groupKeys.putIfAbsent(index, () => GlobalKey());
if (key.currentContext != null && expandedIndex == index) {
// Delay scrolling to wait for expansion animation
Future.delayed(Duration(milliseconds: 450), () {
if (key.currentContext != null) {
Scrollable.ensureVisible(
key.currentContext!,
duration: Duration(milliseconds: 350),
curve: Curves.easeInOut,
alignment: 0.0,
);
}
});
}
});
},
child: Container(
key: _groupKeys.putIfAbsent(index, () => GlobalKey()),
margin: EdgeInsets.only(bottom: 8.h),
padding: EdgeInsets.all(12.h),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8.h),
boxShadow: [
BoxShadow(
color: AppColors.blackColor.withValues(alpha: 0.03),
blurRadius: 6,
offset: Offset(0, 2),
)
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.all(16.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
CustomButton(
text: "${group.length} ${'results'.needTranslation}",
onPressed: () {},
backgroundColor: AppColors.greyColor,
borderColor: AppColors.greyColor,
textColor: AppColors.blackColor,
fontSize: 10,
fontWeight: FontWeight.w500,
borderRadius: 8,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 30.h,
),
Icon(expandedIndex == index ? Icons.expand_less : Icons.expand_more),
],
),
SizedBox(height: 8.h),
Text(
displayName,
style: TextStyle(fontSize: 16.h, fontWeight: FontWeight.w600),
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
),
),
AnimatedSwitcher(
duration: Duration(milliseconds: 400),
child: expandedIndex == index
? Container(
key: ValueKey<int>(index),
padding: EdgeInsets.only(top: 12.h),
child: Column(
children: group.map<Widget>((order) {
return Container(
margin: EdgeInsets.only(bottom: 12.h),
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 12.h,
hasShadow: true,
),
child: Padding(
padding: EdgeInsets.all(14.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AppCustomChipWidget(
labelText: LocaleKeys.resultsAvailable.tr(context: context),
backgroundColor: AppColors.successColor.withOpacity(0.15),
textColor: AppColors.successColor,
).toShimmer2(isShow: model.isRadiologyOrdersLoading, width: 100),
SizedBox(height: 8.h),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.network(
model.isRadiologyOrdersLoading
? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png"
: model.patientRadiologyOrders[index].doctorImageURL!,
order.doctorImageURL ?? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png",
width: 24.h,
height: 24.h,
fit: BoxFit.fill,
).circle(100).toShimmer2(isShow: model.isRadiologyOrdersLoading),
SizedBox(width: 4.h),
(model.isRadiologyOrdersLoading
? "Dr John Smith"
: model.patientRadiologyOrders[index].doctorName!)
.toText16(isBold: true)
.toShimmer2(isShow: model.isRadiologyOrdersLoading)
],
),
SizedBox(height: 8.h),
Wrap(
direction: Axis.horizontal,
spacing: 3.h,
runSpacing: 4.h,
fit: BoxFit.cover,
).circle(100),
SizedBox(width: 8.h),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AppCustomChipWidget(
icon: AppAssets.doctor_calendar_icon,
labelText: model.isRadiologyOrdersLoading
? "01 Jan 2025"
: DateUtil.formatDateToDate(model.patientRadiologyOrders[index].orderDate!, false),
).toShimmer2(isShow: model.isRadiologyOrdersLoading),
AppCustomChipWidget(
labelText: model.isRadiologyOrdersLoading
? "01 Jan 2025"
: model.patientRadiologyOrders[index].clinicDescription!,
).toShimmer2(isShow: model.isRadiologyOrdersLoading),
(order.doctorName ?? "").toString().toText14(weight: FontWeight.w500),
SizedBox(height: 6.h),
// AppCustomChipWidget(labelText: "").toShimmer2(isShow: model.isRadiologyOrdersLoading, width: 16.h),
// AppCustomChipWidget(labelText: "").toShimmer2(isShow: model.isRadiologyOrdersLoading, width: 16.h),
],
),
),
],
),
SizedBox(height: 8.h),
Row(children: [
CustomButton(
text: order.description!,
onPressed: () {},
backgroundColor: AppColors.greyColor,
borderColor: AppColors.greyColor,
textColor: AppColors.blackColor,
fontSize: 10,
fontWeight: FontWeight.w500,
borderRadius: 8,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 24.h,
)
]
),
model.isRadiologyOrdersLoading
? SizedBox.shrink()
: AnimatedCrossFade(
firstChild: SizedBox.shrink(),
secondChild: Padding(
padding: EdgeInsets.symmetric(horizontal: 16.h, vertical: 8.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
SizedBox(height: 6.h),
Row(
children: [
Padding(
padding: EdgeInsets.only(bottom: 8.h),
child: '${model.patientRadiologyOrders[index].description}'
.toText14(weight: FontWeight.w500),
CustomButton(
text: DateUtil.formatDateToDate(order.orderDate ?? order.appointmentDate ?? "", false),
onPressed: () {},
backgroundColor: AppColors.greyColor,
borderColor: AppColors.greyColor,
textColor: AppColors.blackColor,
fontSize: 10,
fontWeight: FontWeight.w500,
borderRadius: 8,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 24.h,
),
SizedBox(width: 8.h),
CustomButton(
text: model.isSortByClinic ? (order.clinicDescription ?? "") : (order.projectName ?? ""),
onPressed: () {},
backgroundColor: AppColors.greyColor,
borderColor: AppColors.greyColor,
textColor: AppColors.blackColor,
fontSize: 10,
fontWeight: FontWeight.w500,
borderRadius: 8,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 24.h,
),
],
),
SizedBox(height: 8.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
SizedBox(),
CustomButton(
icon: AppAssets.view_report_icon,
iconColor: AppColors.primaryRedColor,
iconSize: 16.h,
text: LocaleKeys.viewReport.tr(context: context),
onPressed: () {
Navigator.of(context).push(
Expanded(
flex: 6,
child: SizedBox(),
),
SizedBox(width: 8.h),
Expanded(
flex: 1,
child: Container(
height: 40.h,
width: 40.w,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.textColor,
borderRadius: 12,
),
child: Padding(
padding: EdgeInsets.all(12.h),
child: Transform.flip(
flipX: false,
child: Utils.buildSvgWithAssets(
icon: AppAssets.forward_arrow_icon_small,
iconColor: AppColors.whiteColor,
fit: BoxFit.contain,
),
),
),
).onPress(() {
model.navigationService.push(
CustomPageRoute(
page: RadiologyResultPage(
patientRadiologyResponseModel: model.patientRadiologyOrders[index]),
page: RadiologyResultPage(patientRadiologyResponseModel: order),
),
);
},
backgroundColor: AppColors.secondaryLightRedColor,
borderColor: AppColors.secondaryLightRedColor,
textColor: AppColors.primaryRedColor,
fontSize: 14,
fontWeight: FontWeight.bold,
borderRadius: 12,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
height: 40.h,
}),
),
],
),
],
),
),
crossFadeState: isExpanded ? CrossFadeState.showSecond : CrossFadeState.showFirst,
duration: Duration(milliseconds: 300),
),
],
);
}).toList(),
),
)
: SizedBox.shrink(),
),
],
),
),
),
)
: Utils.getNoDataWidget(context, noDataText: "You don't have any radiology results yet.".needTranslation);
);
},
),
);
}),
],
),
);

Loading…
Cancel
Save