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"> <string name="GEOFENCE_REQUEST_TOO_FREQUENT">
Geofence requests happened too frequently. Geofence requests happened too frequently.
</string> </string>
<string name="mapbox_access_token" translatable="false">sk.eyJ1IjoicndhaWQiLCJhIjoiY2x6NWo0bTMzMWZodzJrcGZpemYzc3Z4dSJ9.uSSZuwNSGCcCdPAiORECmg</string> <!-- <string name="mapbox_access_token" translatable="false">sk.eyJ1IjoicndhaWQiLCJhIjoiY2x6NWo0bTMzMWZodzJrcGZpemYzc3Z4dSJ9.uSSZuwNSGCcCdPAiORECmg</string>-->
</resources> </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_ORDER_HISTORY = '/api/orders/items';
var PACKAGES_TAMARA_OPT = '/api/orders/paymentoptions/tamara'; var PACKAGES_TAMARA_OPT = '/api/orders/paymentoptions/tamara';
// var BASE_URL = 'http://10.50.100.198:2018/'; // var BASE_URL = 'http://10.50.100.198:2018/';
var BASE_URL = 'https://uat.hmgwebservices.com/'; // var BASE_URL = 'https://uat.hmgwebservices.com/';
// var BASE_URL = 'https://hmgwebservices.com/'; var BASE_URL = 'https://hmgwebservices.com/';
// var BASE_URL = 'http://10.201.204.103/'; // var BASE_URL = 'http://10.201.204.103/';
// var BASE_URL = 'https://orash.cloudsolutions.com.sa/'; // var BASE_URL = 'https://orash.cloudsolutions.com.sa/';
// var BASE_URL = 'https://vidauat.cloudsolutions.com.sa/'; // var BASE_URL = 'https://vidauat.cloudsolutions.com.sa/';

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

@ -80,6 +80,12 @@ class LabViewModel extends ChangeNotifier {
double minY = double.infinity; double minY = double.infinity;
double maxX = 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( LabViewModel(
{required this.labRepo, {required this.labRepo,
required this.errorHandlerService, required this.errorHandlerService,
@ -92,6 +98,9 @@ class LabViewModel extends ChangeNotifier {
labOrderTests.clear(); labOrderTests.clear();
isLabOrdersLoading = true; isLabOrdersLoading = true;
isLabResultsLoading = true; isLabResultsLoading = true;
patientLabOrdersByClinic.clear();
patientLabOrdersByHospital.clear();
patientLabOrdersViewList.clear();
getPatientLabOrders(); getPatientLabOrders();
// } // }
notifyListeners(); notifyListeners();
@ -102,6 +111,12 @@ class LabViewModel extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
void setIsSortByClinic(bool value) {
isSortByClinic = value;
patientLabOrdersViewList = isSortByClinic ? patientLabOrdersByClinic : patientLabOrdersByHospital;
notifyListeners();
}
Future<void> getPatientLabOrders({Function(dynamic)? onSuccess, Function(String)? onError}) async { Future<void> getPatientLabOrders({Function(dynamic)? onSuccess, Function(String)? onError}) async {
// if (!isLabNeedToLoad) return; // if (!isLabNeedToLoad) return;
@ -110,6 +125,9 @@ class LabViewModel extends ChangeNotifier {
uniqueTests.clear(); uniqueTests.clear();
labOrderTests.clear(); labOrderTests.clear();
uniqueTests = {}; uniqueTests = {};
patientLabOrdersByClinic.clear();
patientLabOrdersByHospital.clear();
patientLabOrdersViewList.clear();
notifyListeners(); notifyListeners();
final result = await labRepo.getPatientLabOrders(); final result = await labRepo.getPatientLabOrders();
@ -120,7 +138,7 @@ class LabViewModel extends ChangeNotifier {
isLabResultsLoading = false; isLabResultsLoading = false;
notifyListeners(); notifyListeners();
}, },
// => await errorHandlerService.handleError(failure: failure), // => await errorHandler_service.handleError(failure: failure),
(apiResponse) { (apiResponse) {
if (apiResponse.messageStatus == 2) { if (apiResponse.messageStatus == 2) {
// dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {}); // dialogService.showErrorDialog(message: apiResponse.errorMessage!, onOkPressed: () {});
@ -130,6 +148,23 @@ class LabViewModel extends ChangeNotifier {
tempLabOrdersList = apiResponse.data!; tempLabOrdersList = apiResponse.data!;
isLabOrdersLoading = false; isLabOrdersLoading = false;
isLabResultsLoading = 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(); filterSuggestions();
getUniqueTestDescription(); getUniqueTestDescription();
isLabNeedToLoad = false; isLabNeedToLoad = false;
@ -204,7 +239,6 @@ class LabViewModel extends ChangeNotifier {
createdOn: item.createdOn, createdOn: item.createdOn,
model: item)) model: item))
}; };
var sortedResult = SplayTreeSet<TestDetails>.from(uniqueTests, (a, b) => a.description?[0].toUpperCase().compareTo(b.description?[0] ?? "") ?? -1);
uniqueTestsList = uniqueTests.toList(); uniqueTestsList = uniqueTests.toList();
uniqueTestsList.sort((a, b) { uniqueTestsList.sort((a, b) {
return a.description!.toLowerCase().compareTo(b.description!.toLowerCase()); return a.description!.toLowerCase().compareTo(b.description!.toLowerCase());
@ -289,7 +323,7 @@ class LabViewModel extends ChangeNotifier {
PatientLabOrdersResponseModel laborder) async { PatientLabOrdersResponseModel laborder) async {
isLabResultByHospitalLoading = true; isLabResultByHospitalLoading = true;
notifyListeners(); notifyListeners();
mainLabResultsByHospitals.clear; mainLabResultsByHospitals.clear();
final result = await labRepo.getPatientLabResultsByHospitals(laborder, final result = await labRepo.getPatientLabResultsByHospitals(laborder,
Utils.isVidaPlusProject(int.parse(laborder.projectID ?? "0"))); Utils.isVidaPlusProject(int.parse(laborder.projectID ?? "0")));
@ -332,8 +366,7 @@ class LabViewModel extends ChangeNotifier {
LoaderBottomSheet.hideLoader(); LoaderBottomSheet.hideLoader();
if (apiResponse.messageStatus == 2) { if (apiResponse.messageStatus == 2) {
} else if (apiResponse.messageStatus == 1) { } else if (apiResponse.messageStatus == 1) {
var sortedResponse = sortByFlagAndValue(apiResponse.data!); var recentThree = sort(apiResponse.data!);
var recentThree = sort(sortedResponse);
mainLabResults = recentThree; mainLabResults = recentThree;
@ -615,19 +648,6 @@ class LabViewModel extends ChangeNotifier {
} }
List<LabResult> sort(List<LabResult> original) { 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); final copy = List<LabResult>.from(original);
copy.sort((a, b) { copy.sort((a, b) {
@ -647,8 +667,8 @@ class LabViewModel extends ChangeNotifier {
final copy = List<DataPoint>.from(original); final copy = List<DataPoint>.from(original);
copy.sort((a, b) { copy.sort((a, b) {
final aDate =a.time; final aDate = a.time;
final bDate = a.time; final bDate = b.time;
final now = DateTime.now(); final now = DateTime.now();
if (aDate == now && bDate == now) return 0; if (aDate == now && bDate == now) return 0;
if (aDate == now) return 1; if (aDate == now) return 1;

@ -35,6 +35,12 @@ class MedicalFileViewModel extends ChangeNotifier {
List<PatientVaccineResponseModel> patientVaccineList = []; List<PatientVaccineResponseModel> patientVaccineList = [];
List<PatientSickLeavesResponseModel> patientSickLeaveList = []; List<PatientSickLeavesResponseModel> patientSickLeaveList = [];
List<SickLeaveList> patientSickLeavesByClinic = [];
List<SickLeaveList> patientSickLeavesByHospital = [];
List<SickLeaveList> patientSickLeavesViewList = [];
bool isSickLeavesSortByClinic = true;
List<GetAllergiesResponseModel> patientAllergiesList = []; List<GetAllergiesResponseModel> patientAllergiesList = [];
List<PatientMedicalReportResponseModel> patientMedicalReportList = []; List<PatientMedicalReportResponseModel> patientMedicalReportList = [];
@ -43,6 +49,12 @@ class MedicalFileViewModel extends ChangeNotifier {
List<PatientMedicalReportResponseModel> patientMedicalReportReadyList = []; List<PatientMedicalReportResponseModel> patientMedicalReportReadyList = [];
List<PatientMedicalReportResponseModel> patientMedicalReportCancelledList = []; List<PatientMedicalReportResponseModel> patientMedicalReportCancelledList = [];
List<MedicalReportList> patientMedicalReportsByClinic = [];
List<MedicalReportList> patientMedicalReportsByHospital = [];
List<MedicalReportList> patientMedicalReportsViewList = [];
bool isMedicalReportsSortByClinic = true;
List<PatientAppointmentHistoryResponseModel> patientMedicalReportAppointmentHistoryList = []; List<PatientAppointmentHistoryResponseModel> patientMedicalReportAppointmentHistoryList = [];
PatientAppointmentHistoryResponseModel? patientMedicalReportSelectedAppointment; PatientAppointmentHistoryResponseModel? patientMedicalReportSelectedAppointment;
@ -89,6 +101,49 @@ class MedicalFileViewModel extends ChangeNotifier {
} else if (index == 2) { } else if (index == 2) {
patientMedicalReportList = patientMedicalReportCancelledList; 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(); notifyListeners();
} }
@ -109,17 +164,35 @@ class MedicalFileViewModel extends ChangeNotifier {
setIsPatientSickLeaveListLoading(bool val) { setIsPatientSickLeaveListLoading(bool val) {
if (val) { if (val) {
patientSickLeaveList.clear(); patientSickLeaveList.clear();
patientSickLeavesByClinic.clear();
patientSickLeavesByHospital.clear();
patientSickLeavesViewList.clear();
patientSickLeavePDFBase64 = ""; patientSickLeavePDFBase64 = "";
isSickLeavesSortByClinic = true;
} }
isPatientSickLeaveListLoading = val; isPatientSickLeaveListLoading = val;
notifyListeners(); notifyListeners();
} }
setIsSickLeavesSortByClinic(bool value) {
isSickLeavesSortByClinic = value;
if (isSickLeavesSortByClinic) {
patientSickLeavesViewList = patientSickLeavesByClinic;
} else {
patientSickLeavesViewList = patientSickLeavesByHospital;
}
notifyListeners();
}
setIsPatientMedicalReportsLoading(bool val) { setIsPatientMedicalReportsLoading(bool val) {
if (val) { if (val) {
onMedicalReportTabChange(0); onMedicalReportTabChange(0);
patientMedicalReportList.clear(); patientMedicalReportList.clear();
patientMedicalReportsByClinic.clear();
patientMedicalReportsByHospital.clear();
patientMedicalReportsViewList.clear();
patientMedicalReportPDFBase64 = ""; patientMedicalReportPDFBase64 = "";
isMedicalReportsSortByClinic = true;
} }
isPatientMedicalReportsListLoading = val; isPatientMedicalReportsListLoading = val;
notifyListeners(); notifyListeners();
@ -213,6 +286,29 @@ class MedicalFileViewModel extends ChangeNotifier {
} else if (apiResponse.messageStatus == 1) { } else if (apiResponse.messageStatus == 1) {
patientSickLeaveList = apiResponse.data!; patientSickLeaveList = apiResponse.data!;
isPatientSickLeaveListLoading = false; 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(); notifyListeners();
if (onSuccess != null) { if (onSuccess != null) {
onSuccess(apiResponse); onSuccess(apiResponse);

@ -190,3 +190,16 @@ class PatientMedicalReportResponseModel {
return data; 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; 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 = []; List<PatientAppointmentHistoryResponseModel> patientEyeMeasurementsAppointmentsHistoryList = [];
// Grouping by Clinic and Hospital
List<PatientAppointmentList> patientAppointmentsByClinic = [];
List<PatientAppointmentList> patientAppointmentsByHospital = [];
List<PatientAppointmentList> patientAppointmentsViewList = [];
bool isAppointmentsSortByClinic = true;
List<AskDocRequestType> askDoctorRequestTypeList = []; List<AskDocRequestType> askDoctorRequestTypeList = [];
PatientAppointmentShareResponseModel? patientAppointmentShareResponseModel; PatientAppointmentShareResponseModel? patientAppointmentShareResponseModel;
@ -82,6 +88,43 @@ class MyAppointmentsViewModel extends ChangeNotifier {
notifyListeners(); 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() { initAppointmentsViewModel() {
if (isAppointmentDataToBeLoaded) { if (isAppointmentDataToBeLoaded) {
patientAppointmentsHistoryList.clear(); patientAppointmentsHistoryList.clear();
@ -513,6 +556,7 @@ class MyAppointmentsViewModel extends ChangeNotifier {
break; break;
} }
getFiltersForSelectedAppointmentList(filteredAppointmentList); getFiltersForSelectedAppointmentList(filteredAppointmentList);
_groupAppointmentsByClinicAndHospital();
notifyListeners(); notifyListeners();
} }
@ -570,6 +614,7 @@ class MyAppointmentsViewModel extends ChangeNotifier {
} catch (e) {} } catch (e) {}
} }
} }
_groupAppointmentsByClinicAndHospital();
notifyListeners(); notifyListeners();
} }

@ -29,6 +29,18 @@ class RadiologyViewModel extends ChangeNotifier {
late PatientRadiologyResponseModel patientRadiologyOrderByAppointment; 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}); RadiologyViewModel({required this.radiologyRepo, required this.errorHandlerService, required this.navigationService});
initRadiologyViewModel() { initRadiologyViewModel() {

@ -4,6 +4,9 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.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_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/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/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/string_extensions.dart';
@ -35,6 +38,7 @@ class MyAppointmentsPage extends StatefulWidget {
} }
class _MyAppointmentsPageState extends State<MyAppointmentsPage> { class _MyAppointmentsPageState extends State<MyAppointmentsPage> {
int? expandedIndex;
late MyAppointmentsViewModel myAppointmentsViewModel; late MyAppointmentsViewModel myAppointmentsViewModel;
late BookAppointmentsViewModel bookAppointmentsViewModel; late BookAppointmentsViewModel bookAppointmentsViewModel;
@ -68,6 +72,9 @@ class _MyAppointmentsPageState extends State<MyAppointmentsPage> {
CustomTabBarModel(null, "Completed".needTranslation), CustomTabBarModel(null, "Completed".needTranslation),
], ],
onTabChange: (index) { onTabChange: (index) {
setState(() {
expandedIndex = null;
});
myAppointmentsViewModel.onTabChange(index); myAppointmentsViewModel.onTabChange(index);
myAppointmentsViewModel.updateListWRTTab(index); myAppointmentsViewModel.updateListWRTTab(index);
context.read<DateRangeSelectorRangeViewModel>().flush(); context.read<DateRangeSelectorRangeViewModel>().flush();
@ -91,17 +98,55 @@ class _MyAppointmentsPageState extends State<MyAppointmentsPage> {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ 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)), Visibility(visible: myAppointmentsVM.availableFilters.isNotEmpty, child: getAppointmentFilters(myAppointmentsVM)),
ListView.separated( SizedBox(height: 8.h),
padding: EdgeInsets.only(top: 16.h), // Expandable list
ListView.builder(
padding: EdgeInsets.only(top: 8.h),
shrinkWrap: true, shrinkWrap: true,
physics: NeverScrollableScrollPhysics(), physics: NeverScrollableScrollPhysics(),
itemCount: myAppointmentsVM.isMyAppointmentsLoading itemCount: myAppointmentsVM.isMyAppointmentsLoading
? 5 ? 4
: filteredAppointmentList.isNotEmpty : filteredAppointmentList.isNotEmpty
? filteredAppointmentList.length ? myAppointmentsVM.patientAppointmentsViewList.length
: 1, : 1,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final isExpanded = expandedIndex == index;
return myAppointmentsVM.isMyAppointmentsLoading return myAppointmentsVM.isMyAppointmentsLoading
? Container( ? Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true),
@ -123,15 +168,149 @@ class _MyAppointmentsPageState extends State<MyAppointmentsPage> {
child: AnimatedContainer( child: AnimatedContainer(
duration: Duration(milliseconds: 300), duration: Duration(milliseconds: 300),
curve: Curves.easeInOut, curve: Curves.easeInOut,
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), margin: EdgeInsets.symmetric(vertical: 8.h),
child: AppointmentCard( decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: true),
patientAppointmentHistoryResponseModel: filteredAppointmentList[index], child: InkWell(
myAppointmentsViewModel: myAppointmentsViewModel, onTap: () {
bookAppointmentsViewModel: bookAppointmentsViewModel, setState(() {
isLoading: false, expandedIndex = isExpanded ? null : index;
isFromHomePage: false, });
},
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,
),
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), ),
), ),
), ),
) )
@ -160,8 +339,7 @@ class _MyAppointmentsPageState extends State<MyAppointmentsPage> {
).paddingSymmetrical(48.h, 0.h), ).paddingSymmetrical(48.h, 0.h),
); );
}, },
separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), ).paddingSymmetrical(24.h, 0.h),
),
SizedBox(height: 24.h), SizedBox(height: 24.h),
], ],
); );

@ -1,6 +1,5 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.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_assets.dart';
import 'package:hmg_patient_app_new/core/app_state.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/dependencies.dart';
@ -21,169 +20,377 @@ import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../../widgets/chip/app_custom_chip_widget.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}); MyDoctorsPage({super.key});
@override
State<MyDoctorsPage> createState() => _MyDoctorsPageState();
}
class _MyDoctorsPageState extends State<MyDoctorsPage> {
late MyAppointmentsViewModel myAppointmentsViewModel; late MyAppointmentsViewModel myAppointmentsViewModel;
late BookAppointmentsViewModel bookAppointmentsViewModel; late BookAppointmentsViewModel bookAppointmentsViewModel;
bool isSortByClinic = true; // default like lab page
int? expandedIndex;
final Map<int, GlobalKey> _groupKeys = {};
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
AppState appState = getIt.get<AppState>();
myAppointmentsViewModel = Provider.of<MyAppointmentsViewModel>(context, listen: false); myAppointmentsViewModel = Provider.of<MyAppointmentsViewModel>(context, listen: false);
bookAppointmentsViewModel = Provider.of<BookAppointmentsViewModel>(context, listen: false); bookAppointmentsViewModel = Provider.of<BookAppointmentsViewModel>(context, listen: false);
return Scaffold(
backgroundColor: AppColors.bgScaffoldColor, return CollapsingListView(
body: CollapsingListView(
title: LocaleKeys.myDoctor.tr(context: context), title: LocaleKeys.myDoctor.tr(context: context),
child: SingleChildScrollView( child: Consumer<MyAppointmentsViewModel>(builder: (context, myAppointmentsVM, child) {
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( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SizedBox(height: 16.h), 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( ListView.separated(
scrollDirection: Axis.vertical, scrollDirection: Axis.vertical,
itemCount: myAppointmentsVM.isPatientMyDoctorsLoading ? 5 : myAppointmentsVM.patientMyDoctorsList.length, itemCount: myAppointmentsVM.isPatientMyDoctorsLoading ? 5 : (patientMyDoctorsViewList.isNotEmpty ? patientMyDoctorsViewList.length : 0),
shrinkWrap: true, shrinkWrap: true,
physics: NeverScrollableScrollPhysics(), physics: NeverScrollableScrollPhysics(),
padding: EdgeInsets.only(left: 24.h, right: 24.h), padding: EdgeInsets.only(left: 24.h, right: 24.h),
itemBuilder: (context, index) { itemBuilder: (context, index) {
return myAppointmentsVM.isPatientMyDoctorsLoading if (myAppointmentsVM.isPatientMyDoctorsLoading) {
? Container( return Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration( decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor, color: AppColors.whiteColor,
borderRadius: 20.h, borderRadius: 20.h,
hasShadow: true, hasShadow: true,
), ),
child: Padding( child: Padding(
padding: EdgeInsets.all(14.h), padding: EdgeInsets.all(14.h),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Image.network(
crossAxisAlignment: CrossAxisAlignment.start, "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png",
children: [ width: 63.h,
Image.network( height: 63.h,
"https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png", fit: BoxFit.cover,
width: 63.h, ).circle(100).toShimmer2(isShow: true),
height: 63.h, SizedBox(width: 16.h),
fit: BoxFit.cover, Expanded(
).circle(100).toShimmer2(isShow: true),
SizedBox(width: 16.h),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
"Dr John Smith".toText16(isBold: true).toShimmer2(isShow: true),
SizedBox(height: 8.h),
Wrap(
direction: Axis.horizontal,
spacing: 3.h,
runSpacing: 4.h,
children: [
AppCustomChipWidget(labelText: "").toShimmer2(isShow: true, width: 16.h),
AppCustomChipWidget(labelText: "").toShimmer2(isShow: true, width: 16.h),
],
),
],
),
),
],
),
],
),
),
)
: AnimationConfiguration.staggeredList(
position: index,
duration: const Duration(milliseconds: 1000),
child: SlideAnimation(
verticalOffset: 100.0,
child: FadeInAnimation(
child: Container(
decoration: RoundedRectangleBorder().toSmoothCornerDecoration(
color: AppColors.whiteColor,
borderRadius: 20.h,
hasShadow: true,
),
child: Padding(
padding: EdgeInsets.all(14.h),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( "Dr John Smith".toText16(isBold: true).toShimmer2(isShow: true),
crossAxisAlignment: CrossAxisAlignment.start, SizedBox(height: 8.h),
children: [ Wrap(
Image.network( direction: Axis.horizontal,
myAppointmentsVM.patientMyDoctorsList[index].doctorImageURL!, spacing: 3.h,
width: 63.h, runSpacing: 4.h,
height: 63.h,
fit: BoxFit.cover,
).circle(100).toShimmer2(isShow: false),
SizedBox(width: 16.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),
],
),
],
),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
"".toText16(), AppCustomChipWidget(labelText: "").toShimmer2(isShow: true, width: 16.h),
Transform.flip( AppCustomChipWidget(labelText: "").toShimmer2(isShow: true, width: 16.h),
flipX: appState.isArabic(),
child:
Utils.buildSvgWithAssets(icon: AppAssets.forward_arrow_icon_small, width: 15.h, height: 15.h, fit: BoxFit.contain, iconColor: AppColors.textColor)),
], ],
), ),
], ],
), ),
), ),
).onPress(() async { ],
bookAppointmentsViewModel.setSelectedDoctor(DoctorsListResponseModel(
clinicID: myAppointmentsVM.patientMyDoctorsList[index].clinicID,
projectID: myAppointmentsVM.patientMyDoctorsList[index].projectID,
doctorID: myAppointmentsVM.patientMyDoctorsList[index].doctorID,
));
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,
);
});
}),
), ),
],
),
),
);
}
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(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.network(
(doctor?.doctorImageURL ?? doctor?.doctorImage ?? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png"),
width: 24.h,
height: 24.h,
fit: BoxFit.cover,
).circle(100),
SizedBox(width: 8.h),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
(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(
children: [
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: 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,
);
});
}),
),
],
),
],
),
),
);
}).toList(),
),
)
: SizedBox.shrink(),
),
],
),
);
}, },
separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h),
), ),
@ -191,8 +398,6 @@ class MyDoctorsPage extends StatelessWidget {
], ],
); );
}), }),
),
),
); );
} }
} }

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

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

@ -106,17 +106,23 @@ class _LandingPageState extends State<LandingPage> {
insuranceViewModel.initInsuranceProvider(); insuranceViewModel.initInsuranceProvider();
emergencyServicesViewModel.checkPatientERAdvanceBalance(); emergencyServicesViewModel.checkPatientERAdvanceBalance();
myAppointmentsViewModel.getPatientAppointmentQueueDetails(); myAppointmentsViewModel.getPatientAppointmentQueueDetails();
if(!appState.isRatedVisible) {
appointmentRatingViewModel.getLastRatingAppointment(onSuccess: (response) { appointmentRatingViewModel.getLastRatingAppointment(onSuccess: (response) {
if (appointmentRatingViewModel.appointmentRatedList.isNotEmpty) { if (appointmentRatingViewModel.appointmentRatedList.isNotEmpty) {
appointmentRatingViewModel.getAppointmentDetails(appointmentRatingViewModel.appointmentRatedList.last.appointmentNo!, appointmentRatingViewModel.appointmentRatedList.last.projectID!, appointmentRatingViewModel.getAppointmentDetails(appointmentRatingViewModel.appointmentRatedList.last.appointmentNo!, appointmentRatingViewModel.appointmentRatedList.last.projectID!,
onSuccess: ((response) { onSuccess: ((response) {
appointmentRatingViewModel.setClinicOrDoctor(false);
appointmentRatingViewModel.setTitle("Rate Doctor".needTranslation); appointmentRatingViewModel.setClinicOrDoctor(false);
appointmentRatingViewModel.setSubTitle("How was your last visit with doctor?".needTranslation); appointmentRatingViewModel.setTitle("Rate Doctor".needTranslation);
openLastRating(); appointmentRatingViewModel.setSubTitle("How was your last visit with doctor?".needTranslation);
openLastRating();
appState.setRatedVisible(true);
})); }));
} }
}); });
}
} }
}); });
super.initState(); 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:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.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/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/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/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/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.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/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 'package:provider/provider.dart';
import 'widgets/patient_sick_leave_card.dart'; import 'widgets/patient_sick_leave_card.dart';
@ -24,6 +31,7 @@ class PatientSickleavesListPage extends StatefulWidget {
} }
class _PatientSickleavesListPageState extends State<PatientSickleavesListPage> { class _PatientSickleavesListPageState extends State<PatientSickleavesListPage> {
int? expandedIndex;
late MedicalFileViewModel medicalFileViewModel; late MedicalFileViewModel medicalFileViewModel;
@override @override
@ -37,53 +45,257 @@ class _PatientSickleavesListPageState extends State<PatientSickleavesListPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
AppState appState = getIt.get<AppState>();
medicalFileViewModel = Provider.of<MedicalFileViewModel>(context, listen: false); medicalFileViewModel = Provider.of<MedicalFileViewModel>(context, listen: false);
return Scaffold( return Scaffold(
backgroundColor: AppColors.bgScaffoldColor, backgroundColor: AppColors.bgScaffoldColor,
body: CollapsingListView( body: CollapsingListView(
title: "${LocaleKeys.sick.tr(context: context)} ${LocaleKeys.sickSubtitle.tr(context: context)}", title: "${LocaleKeys.sick.tr(context: context)} ${LocaleKeys.sickSubtitle.tr(context: context)}",
child: SingleChildScrollView( child: SingleChildScrollView(
child: Consumer<MedicalFileViewModel>(builder: (context, medicalFileVM, child) { child: Consumer<MedicalFileViewModel>(builder: (context, model, child) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SizedBox(height: 24.h), SizedBox(height: 16.h),
ListView.separated( // Clinic & Hospital Sort
padding: EdgeInsets.zero, Row(
scrollDirection: Axis.vertical, children: [
itemCount: medicalFileVM.isPatientSickLeaveListLoading CustomButton(
? 3 text: LocaleKeys.byClinic.tr(context: context),
: medicalFileVM.patientSickLeaveList.isNotEmpty onPressed: () {
? medicalFileVM.patientSickLeaveList.length 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, : 1,
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(), physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
padding: const EdgeInsets.only(left: 0, right: 8),
itemBuilder: (context, index) { itemBuilder: (context, index) {
return medicalFileVM.isPatientSickLeaveListLoading final isExpanded = expandedIndex == index;
return model.isPatientSickLeaveListLoading
? PatientSickLeaveCard( ? PatientSickLeaveCard(
patientSickLeavesResponseModel: PatientSickLeavesResponseModel(), patientSickLeavesResponseModel: PatientSickLeavesResponseModel(),
isLoading: true, isLoading: true,
).paddingSymmetrical(24.h, 0.0) ).paddingSymmetrical(24.h, 0.0)
: medicalFileVM.patientSickLeaveList.isNotEmpty : model.patientSickLeaveList.isNotEmpty
? AnimationConfiguration.staggeredList( ? AnimationConfiguration.staggeredList(
position: index, position: index,
duration: const Duration(milliseconds: 1000), duration: const Duration(milliseconds: 500),
child: SlideAnimation( child: SlideAnimation(
verticalOffset: 100.0, verticalOffset: 100.0,
child: FadeInAnimation( child: FadeInAnimation(
child: PatientSickLeaveCard( child: AnimatedContainer(
patientSickLeavesResponseModel: medicalFileVM.patientSickLeaveList.first, duration: Duration(milliseconds: 300),
isLoading: false, curve: Curves.easeInOut,
isSickLeaveListPage: true, margin: EdgeInsets.symmetric(vertical: 8.h),
).paddingSymmetrical(24.h, 0.0), 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); : Utils.getNoDataWidget(context, noDataText: "You don't have any sick leaves yet.".needTranslation);
}, },
separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 8.h), ).paddingSymmetrical(24.h, 0.h),
),
SizedBox(height: 60.h),
], ],
); );
}), }),

@ -2,6 +2,9 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.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_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/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/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/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/common_bottom_sheet.dart';
import 'package:hmg_patient_app_new/widgets/loader/bottomsheet_loader.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:hmg_patient_app_new/widgets/routes/custom_page_route.dart';
import 'package:open_filex/open_filex.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class MedicalReportsPage extends StatefulWidget { class MedicalReportsPage extends StatefulWidget {
@ -27,10 +31,12 @@ class MedicalReportsPage extends StatefulWidget {
} }
class _MedicalReportsPageState extends State<MedicalReportsPage> { class _MedicalReportsPageState extends State<MedicalReportsPage> {
int? expandedIndex;
late MedicalFileViewModel medicalFileViewModel; late MedicalFileViewModel medicalFileViewModel;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
AppState appState = getIt.get<AppState>();
medicalFileViewModel = Provider.of<MedicalFileViewModel>(context, listen: false); medicalFileViewModel = Provider.of<MedicalFileViewModel>(context, listen: false);
return Scaffold( return Scaffold(
backgroundColor: AppColors.bgScaffoldColor, backgroundColor: AppColors.bgScaffoldColor,
@ -44,16 +50,53 @@ class _MedicalReportsPageState extends State<MedicalReportsPage> {
return Column( return Column(
children: [ children: [
SizedBox(height: 16.h), 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( Row(
children: [ children: [
CustomButton( CustomButton(
text: "Requested".needTranslation, text: "Requested".needTranslation,
onPressed: () { onPressed: () {
setState(() {
expandedIndex = null;
});
medicalFileViewModel.onMedicalReportTabChange(0); medicalFileViewModel.onMedicalReportTabChange(0);
}, },
backgroundColor: medicalFileVM.selectedMedicalReportsTabIndex == 0 ? AppColors.bgRedLightColor : AppColors.whiteColor, backgroundColor: medicalFileVM.selectedMedicalReportsTabIndex == 0 ? AppColors.bgRedLightColor : AppColors.whiteColor,
borderColor: borderColor: medicalFileVM.selectedMedicalReportsTabIndex == 0 ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2),
medicalFileVM.selectedMedicalReportsTabIndex == 0 ? AppColors.primaryRedColor : AppColors.textColor.withOpacity(0.2),
textColor: medicalFileVM.selectedMedicalReportsTabIndex == 0 ? AppColors.primaryRedColor : AppColors.blackColor, textColor: medicalFileVM.selectedMedicalReportsTabIndex == 0 ? AppColors.primaryRedColor : AppColors.blackColor,
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
@ -65,11 +108,13 @@ class _MedicalReportsPageState extends State<MedicalReportsPage> {
CustomButton( CustomButton(
text: LocaleKeys.ready.tr(context: context), text: LocaleKeys.ready.tr(context: context),
onPressed: () { onPressed: () {
setState(() {
expandedIndex = null;
});
medicalFileViewModel.onMedicalReportTabChange(1); medicalFileViewModel.onMedicalReportTabChange(1);
}, },
backgroundColor: medicalFileVM.selectedMedicalReportsTabIndex == 1 ? AppColors.bgRedLightColor : AppColors.whiteColor, backgroundColor: medicalFileVM.selectedMedicalReportsTabIndex == 1 ? AppColors.bgRedLightColor : AppColors.whiteColor,
borderColor: borderColor: medicalFileVM.selectedMedicalReportsTabIndex == 1 ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2),
medicalFileVM.selectedMedicalReportsTabIndex == 1 ? AppColors.primaryRedColor : AppColors.textColor.withOpacity(0.2),
textColor: medicalFileVM.selectedMedicalReportsTabIndex == 1 ? AppColors.primaryRedColor : AppColors.blackColor, textColor: medicalFileVM.selectedMedicalReportsTabIndex == 1 ? AppColors.primaryRedColor : AppColors.blackColor,
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
@ -81,11 +126,13 @@ class _MedicalReportsPageState extends State<MedicalReportsPage> {
CustomButton( CustomButton(
text: LocaleKeys.cancelled.tr(context: context), text: LocaleKeys.cancelled.tr(context: context),
onPressed: () { onPressed: () {
setState(() {
expandedIndex = null;
});
medicalFileViewModel.onMedicalReportTabChange(2); medicalFileViewModel.onMedicalReportTabChange(2);
}, },
backgroundColor: medicalFileVM.selectedMedicalReportsTabIndex == 2 ? AppColors.bgRedLightColor : AppColors.whiteColor, backgroundColor: medicalFileVM.selectedMedicalReportsTabIndex == 2 ? AppColors.bgRedLightColor : AppColors.whiteColor,
borderColor: borderColor: medicalFileVM.selectedMedicalReportsTabIndex == 2 ? AppColors.primaryRedColor : AppColors.textColor.withValues(alpha: 0.2),
medicalFileVM.selectedMedicalReportsTabIndex == 2 ? AppColors.primaryRedColor : AppColors.textColor.withOpacity(0.2),
textColor: medicalFileVM.selectedMedicalReportsTabIndex == 2 ? AppColors.primaryRedColor : AppColors.blackColor, textColor: medicalFileVM.selectedMedicalReportsTabIndex == 2 ? AppColors.primaryRedColor : AppColors.blackColor,
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
@ -95,34 +142,27 @@ class _MedicalReportsPageState extends State<MedicalReportsPage> {
), ),
], ],
).paddingSymmetrical(24.h, 0.h), ).paddingSymmetrical(24.h, 0.h),
// CustomTabBar( SizedBox(height: 12.h),
// activeTextColor: Color(0xffED1C2B), // Clinic & Hospital Sort
// activeBackgroundColor: Color(0xffED1C2B).withValues(alpha: .1),
// tabs: [ // Expandable list
// CustomTabBarModel(null, "Requested".needTranslation), ListView.builder(
// 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(),
itemCount: medicalFileViewModel.isPatientMedicalReportsListLoading itemCount: medicalFileViewModel.isPatientMedicalReportsListLoading
? 3 ? 4
: medicalFileViewModel.patientMedicalReportList.isNotEmpty : medicalFileViewModel.patientMedicalReportList.isNotEmpty
? medicalFileViewModel.patientMedicalReportList.length ? medicalFileViewModel.patientMedicalReportsViewList.length
: 1, : 1,
physics: NeverScrollableScrollPhysics(),
shrinkWrap: true,
padding: const EdgeInsets.only(left: 0, right: 8),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final isExpanded = expandedIndex == index;
return medicalFileViewModel.isPatientMedicalReportsListLoading return medicalFileViewModel.isPatientMedicalReportsListLoading
? PatientMedicalReportCard( ? PatientMedicalReportCard(
patientMedicalReportResponseModel: PatientMedicalReportResponseModel(), patientMedicalReportResponseModel: PatientMedicalReportResponseModel(),
medicalFileViewModel: medicalFileVM, medicalFileViewModel: medicalFileVM,
isLoading: true, isLoading: true,
).paddingSymmetrical(24.h, 0.h) ).paddingSymmetrical(24.h, 8.h)
: medicalFileViewModel.patientMedicalReportList.isNotEmpty : medicalFileViewModel.patientMedicalReportList.isNotEmpty
? AnimationConfiguration.staggeredList( ? AnimationConfiguration.staggeredList(
position: index, position: index,
@ -133,22 +173,185 @@ class _MedicalReportsPageState extends State<MedicalReportsPage> {
child: AnimatedContainer( child: AnimatedContainer(
duration: Duration(milliseconds: 300), duration: Duration(milliseconds: 300),
curve: Curves.easeInOut, curve: Curves.easeInOut,
decoration: RoundedRectangleBorder() margin: EdgeInsets.symmetric(vertical: 8.h),
.toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 24.h, hasShadow: true), decoration: RoundedRectangleBorder().toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: true),
child: PatientMedicalReportCard( child: InkWell(
patientMedicalReportResponseModel: medicalFileVM.patientMedicalReportList[index], onTap: () {
medicalFileViewModel: medicalFileVM, setState(() {
isLoading: false, 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) : Utils.getNoDataWidget(context, noDataText: "You don't have any medical reports yet.".needTranslation)
.paddingSymmetrical(24.h, 24.h); .paddingSymmetrical(24.h, 24.h);
}, },
separatorBuilder: (BuildContext cxt, int index) => SizedBox(height: 16.h), ).paddingSymmetrical(24.h, 0.h),
),
SizedBox(height: 24.h), SizedBox(height: 24.h),
], ],
); );

@ -3,13 +3,13 @@ import 'dart:async';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.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/date_util.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.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/core/utils/utils.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.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/extensions/widget_extensions.dart';
import 'package:hmg_patient_app_new/features/lab/lab_view_model.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/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/lab/lab_result_item_view.dart';
import 'package:hmg_patient_app_new/presentation/radiology/radiology_result_page.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 'package:provider/provider.dart';
import '../../features/radiology/radiology_view_model.dart'; import '../../features/radiology/radiology_view_model.dart';
import 'package:hmg_patient_app_new/core/app_assets.dart';
class RadiologyOrdersPage extends StatefulWidget { class RadiologyOrdersPage extends StatefulWidget {
const RadiologyOrdersPage({super.key}); const RadiologyOrdersPage({super.key});
@ -34,226 +35,401 @@ class _RadiologyOrdersPageState extends State<RadiologyOrdersPage> {
late RadiologyViewModel radiologyViewModel; late RadiologyViewModel radiologyViewModel;
String selectedFilterText = ''; String selectedFilterText = '';
int? expandedIndex; int? expandedIndex;
// Scroll controller to ensure expanded group is visible
late ScrollController _scrollController;
final Map<int, GlobalKey> _groupKeys = {};
@override @override
void initState() { void initState() {
_scrollController = ScrollController();
scheduleMicrotask(() { scheduleMicrotask(() {
radiologyViewModel.initRadiologyViewModel(); radiologyViewModel.initRadiologyViewModel();
}); });
super.initState(); super.initState();
} }
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
radiologyViewModel = Provider.of<RadiologyViewModel>(context); radiologyViewModel = Provider.of<RadiologyViewModel>(context);
return Scaffold( return Scaffold(
backgroundColor: AppColors.bgScaffoldColor, backgroundColor: AppColors.bgScaffoldColor,
body: CollapsingListView( body: CollapsingListView(
title: LocaleKeys.radiology.tr(context: context), title: LocaleKeys.radiology.tr(context: context),
search: () async { search: () async {
final lavVM = Provider.of<LabViewModel>(context, listen: false); final lavVM = Provider.of<LabViewModel>(context, listen: false);
if (lavVM.isLabOrdersLoading) { if (lavVM.isLabOrdersLoading) {
return; return;
} else { } else {
String? value = await Navigator.of(context).push( String? value = await Navigator.of(context).push(
CustomPageRoute( CustomPageRoute(
page: SearchRadiologyContent(radiologySuggestionsList: radiologyViewModel.radiologySuggestions), page: SearchRadiologyContent(radiologySuggestionsList: radiologyViewModel.radiologySuggestions),
fullScreenDialog: true, fullScreenDialog: true,
direction: AxisDirection.down, direction: AxisDirection.down,
), ),
); );
if (value != null) { if (value != null) {
selectedFilterText = value; selectedFilterText = value;
radiologyViewModel.filterRadiologyReports(value); radiologyViewModel.filterRadiologyReports(value);
}
} }
} },
}, child: SingleChildScrollView(
child: SingleChildScrollView( controller: _scrollController,
child: Consumer<RadiologyViewModel>( physics: NeverScrollableScrollPhysics(),
builder: (context, model, child) { child: Consumer<RadiologyViewModel>(
return Padding( builder: (context, model, child) {
padding: EdgeInsets.symmetric(horizontal: 24.h), // Build grouping lists if we have data and none constructed yet
child: Column( if (!model.isRadiologyOrdersLoading && model.patientRadiologyOrders.isNotEmpty && model.patientRadiologyOrdersViewList.isEmpty) {
crossAxisAlignment: CrossAxisAlignment.start, final clinicMap = <String, List<PatientRadiologyResponseModel>>{};
children: [ final hospitalMap = <String, List<PatientRadiologyResponseModel>>{};
selectedFilterText!.isNotEmpty for (var order in model.patientRadiologyOrders) {
? AppCustomChipWidget( final clinicKey = (order.clinicDescription ?? 'Unknown').trim();
padding: EdgeInsets.symmetric(horizontal: 5.h), clinicMap.putIfAbsent(clinicKey, () => []).add(order);
labelText: selectedFilterText!, final hospitalKey = (order.projectName ?? order.projectID ?? 'Unknown').toString().trim();
deleteIcon: 'assets/images/svg/cross_circle.svg', hospitalMap.putIfAbsent(hospitalKey, () => []).add(order);
backgroundColor: AppColors.alertColor, }
textColor: AppColors.whiteColor, model.patientRadiologyOrdersByClinic = clinicMap.values.toList();
deleteIconColor: AppColors.whiteColor, model.patientRadiologyOrdersByHospital = hospitalMap.values.toList();
deleteIconHasColor: true, model.patientRadiologyOrdersViewList = model.isSortByClinic ? model.patientRadiologyOrdersByClinic : model.patientRadiologyOrdersByHospital;
onDeleteTap: () { }
setState(() {
selectedFilterText = ''; return Padding(
model.filterRadiologyReports(''); padding: EdgeInsets.symmetric(horizontal: 24.h),
}); child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 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);
}, },
// chipType: ChipTypeEnum.alert, backgroundColor: model.isSortByClinic ? AppColors.whiteColor : AppColors.bgRedLightColor,
// isSelected: true, borderColor: model.isSortByClinic ? AppColors.textColor.withValues(alpha: 0.2) : AppColors.primaryRedColor,
) textColor: model.isSortByClinic ? AppColors.blackColor : AppColors.primaryRedColor,
: SizedBox(), fontSize: 12,
ListView.builder( fontWeight: FontWeight.w500,
shrinkWrap: true, borderRadius: 10,
physics: NeverScrollableScrollPhysics(), padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
itemCount: model.isRadiologyOrdersLoading height: 40.h,
? 5 ),
: model.patientRadiologyOrders.isNotEmpty ],
? model.patientRadiologyOrders.length ),
: 1,
itemBuilder: (context, index) { SizedBox(height: 8.h),
final isExpanded = expandedIndex == index; selectedFilterText.isNotEmpty
return model.isRadiologyOrdersLoading ? AppCustomChipWidget(
? LabResultItemView( padding: EdgeInsets.symmetric(horizontal: 5.h),
onTap: () {}, labelText: selectedFilterText,
labOrder: null, deleteIcon: 'assets/images/svg/cross_circle.svg',
index: index, backgroundColor: AppColors.alertColor,
isLoading: true, textColor: AppColors.whiteColor,
) deleteIconColor: AppColors.whiteColor,
: model.patientRadiologyOrders.isNotEmpty deleteIconHasColor: true,
? AnimationConfiguration.staggeredList( onDeleteTap: () {
position: index, setState(() {
duration: const Duration(milliseconds: 500), selectedFilterText = '';
child: SlideAnimation( model.filterRadiologyReports('');
verticalOffset: 100.0, });
child: FadeInAnimation( },
child: AnimatedContainer( // chipType: ChipTypeEnum.alert,
duration: Duration(milliseconds: 300), // isSelected: true,
curve: Curves.easeInOut, )
margin: EdgeInsets.symmetric(vertical: 8.h), : SizedBox(),
decoration: RoundedRectangleBorder()
.toSmoothCornerDecoration(color: AppColors.whiteColor, borderRadius: 20.h, hasShadow: true), // Grouped view when available
child: InkWell( Builder(builder: (ctx) {
onTap: () { if (model.isRadiologyOrdersLoading) {
setState(() { return ListView.builder(
expandedIndex = isExpanded ? null : index; shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
padding: EdgeInsets.zero,
itemCount: 5,
itemBuilder: (context, index) => LabResultItemView(
onTap: () {},
labOrder: null,
index: index,
isLoading: true,
),
);
}
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: 400),
child: SlideAnimation(
verticalOffset: 50.0,
child: FadeInAnimation(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Group header container with key so we can scroll to it
GestureDetector(
onTap: () {
setState(() {
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: Column( });
crossAxisAlignment: CrossAxisAlignment.start, },
children: [ child: Container(
Padding( key: _groupKeys.putIfAbsent(index, () => GlobalKey()),
padding: EdgeInsets.all(16.h), margin: EdgeInsets.only(bottom: 8.h),
child: Column( padding: EdgeInsets.all(12.h),
crossAxisAlignment: CrossAxisAlignment.start, decoration: BoxDecoration(
children: [ color: Colors.white,
AppCustomChipWidget( borderRadius: BorderRadius.circular(8.h),
labelText: LocaleKeys.resultsAvailable.tr(context: context), boxShadow: [
backgroundColor: AppColors.successColor.withOpacity(0.15), BoxShadow(
textColor: AppColors.successColor, color: AppColors.blackColor.withValues(alpha: 0.03),
).toShimmer2(isShow: model.isRadiologyOrdersLoading, width: 100), blurRadius: 6,
SizedBox(height: 8.h), offset: Offset(0, 2),
Row( )
children: [ ],
Image.network( ),
model.isRadiologyOrdersLoading child: Row(
? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png" mainAxisAlignment: MainAxisAlignment.spaceBetween,
: model.patientRadiologyOrders[index].doctorImageURL!, children: [
width: 24.h, Expanded(
height: 24.h, child: Column(
fit: BoxFit.fill, crossAxisAlignment: CrossAxisAlignment.start,
).circle(100).toShimmer2(isShow: model.isRadiologyOrdersLoading), children: [
SizedBox(width: 4.h), Row(
(model.isRadiologyOrdersLoading mainAxisAlignment: MainAxisAlignment.spaceBetween,
? "Dr John Smith" children: [
: model.patientRadiologyOrders[index].doctorName!) CustomButton(
.toText16(isBold: true) text: "${group.length} ${'results'.needTranslation}",
.toShimmer2(isShow: model.isRadiologyOrdersLoading) 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,
), ),
SizedBox(height: 8.h), child: Padding(
Wrap( padding: EdgeInsets.all(14.h),
direction: Axis.horizontal, child: Column(
spacing: 3.h, crossAxisAlignment: CrossAxisAlignment.start,
runSpacing: 4.h, children: [
children: [ Row(
AppCustomChipWidget( crossAxisAlignment: CrossAxisAlignment.start,
icon: AppAssets.doctor_calendar_icon, children: [
labelText: model.isRadiologyOrdersLoading Image.network(
? "01 Jan 2025" order.doctorImageURL ?? "https://hmgwebservices.com/Images/MobileImages/DUBAI/unkown_female.png",
: DateUtil.formatDateToDate(model.patientRadiologyOrders[index].orderDate!, false), width: 24.h,
).toShimmer2(isShow: model.isRadiologyOrdersLoading), height: 24.h,
AppCustomChipWidget( fit: BoxFit.cover,
labelText: model.isRadiologyOrdersLoading ).circle(100),
? "01 Jan 2025" SizedBox(width: 8.h),
: model.patientRadiologyOrders[index].clinicDescription!, Expanded(
).toShimmer2(isShow: model.isRadiologyOrdersLoading), child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
// AppCustomChipWidget(labelText: "").toShimmer2(isShow: model.isRadiologyOrdersLoading, width: 16.h), children: [
// AppCustomChipWidget(labelText: "").toShimmer2(isShow: model.isRadiologyOrdersLoading, width: 16.h), (order.doctorName ?? "").toString().toText14(weight: FontWeight.w500),
], SizedBox(height: 6.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,
children: [
Padding(
padding: EdgeInsets.only(bottom: 8.h),
child: '${model.patientRadiologyOrders[index].description}'
.toText14(weight: FontWeight.w500),
),
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(
CustomPageRoute(
page: RadiologyResultPage(
patientRadiologyResponseModel: model.patientRadiologyOrders[index]),
),
);
},
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,
), ),
], ),
), ],
], ),
), 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,
)
]
),
SizedBox(height: 6.h),
Row(
children: [
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(
children: [
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: order),
),
);
}),
),
],
),
],
), ),
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); ),
}, ),
), );
], },
), );
); }),
}, ],
),
);
},
),
), ),
), ),
), );
);
} }
Color getLabOrderStatusColor(num status) { Color getLabOrderStatusColor(num status) {

Loading…
Cancel
Save