Merge pull request 'dev_sultan' (#338) from dev_sultan into master

Reviewed-on: https://34.17.182.140/Haroon6138/HMG_Patient_App_New/pulls/338
master
Haroon6138 15 hours ago
commit 66240df2f4

File diff suppressed because it is too large Load Diff

@ -468,6 +468,7 @@
"serviceInformation": "معلومات الخدمة", "serviceInformation": "معلومات الخدمة",
"homeHealthCare": "الرعاية الصحية المنزلية", "homeHealthCare": "الرعاية الصحية المنزلية",
"noAppointmentAvailable": "لا توجد مواعيد متاحة", "noAppointmentAvailable": "لا توجد مواعيد متاحة",
"noScheduleAvailable": "لا يوجد جدول متاح حاليًا",
"loginRegister": "تسجيل الدخول / التسجيل", "loginRegister": "تسجيل الدخول / التسجيل",
"orderLog": "سجل الطلب", "orderLog": "سجل الطلب",
"infoLab": "تتيح لك هذه الخدمة عرض نتائج جميع الفحوصات المخبرية التي أجريت في مجموعة الحبيب الطبية بالإضافة إلى إرسال التقرير عبر البريد الإلكتروني.", "infoLab": "تتيح لك هذه الخدمة عرض نتائج جميع الفحوصات المخبرية التي أجريت في مجموعة الحبيب الطبية بالإضافة إلى إرسال التقرير عبر البريد الإلكتروني.",
@ -1961,5 +1962,9 @@
"parkingQRDesc": "امسح رمز الاستجابة السريعة (QR Code) أدناه عند بوابة الخروج من موقف السيارات:", "parkingQRDesc": "امسح رمز الاستجابة السريعة (QR Code) أدناه عند بوابة الخروج من موقف السيارات:",
"livecareUpdateInsuranceSent": "تم استلام طلب تحديث بيانات التأمين الخاص بك، ويجري العمل عليه حالياً. سنقوم بإشعارك فور اكتمال التحديث.", "livecareUpdateInsuranceSent": "تم استلام طلب تحديث بيانات التأمين الخاص بك، ويجري العمل عليه حالياً. سنقوم بإشعارك فور اكتمال التحديث.",
"livecareUpdateInsurance": "سيتم تحويل طلب تحديث بيانات التأمين إلى فريق LiveCare المختص لاستكمال الإجراءات. هل تود المتابعة؟", "livecareUpdateInsurance": "سيتم تحويل طلب تحديث بيانات التأمين إلى فريق LiveCare المختص لاستكمال الإجراءات. هل تود المتابعة؟",
"schedule": "جدول" "schedule": "جدول",
"doctorSchedule": "جدول الطبيب",
"weeklyShift": "الفترات الأسبوعية",
"dayOff": "إجازة",
"notAvailable": "غير متاح"
} }

@ -460,6 +460,7 @@
"serviceInformation": "Service Information", "serviceInformation": "Service Information",
"homeHealthCare": "Home Health Care", "homeHealthCare": "Home Health Care",
"noAppointmentAvailable": "No Available Appointments", "noAppointmentAvailable": "No Available Appointments",
"noScheduleAvailable": "No schedule currently available",
"homeHealthCareText": "Clinical excellence and specialized care in the comfort of your home", "homeHealthCareText": "Clinical excellence and specialized care in the comfort of your home",
"loginRegister": "Login/Register", "loginRegister": "Login/Register",
"orderLog": "Order Log", "orderLog": "Order Log",
@ -1950,7 +1951,11 @@
"parkingQRDesc": "Scan the below QR Code at the exit parking gate:", "parkingQRDesc": "Scan the below QR Code at the exit parking gate:",
"livecareUpdateInsuranceSent": "Your insurance update request is being processed. You will be notified once the update is completed.", "livecareUpdateInsuranceSent": "Your insurance update request is being processed. You will be notified once the update is completed.",
"livecareUpdateInsurance": "Please note that your insurance update request will be sent to the our LiveCare team for update. Would you like to proceed?", "livecareUpdateInsurance": "Please note that your insurance update request will be sent to the our LiveCare team for update. Would you like to proceed?",
"schedule": "Schedule" "schedule": "Schedule",
"doctorSchedule": "Doctor Schedule",
"weeklyShift": "Weekly Shift",
"dayOff": "Off",
"notAvailable": "Not Available"
} }

@ -113,6 +113,8 @@ abstract class BookAppointmentsRepo {
Future<Either<Failure, GenericApiModel<dynamic>>> insertFavouriteDoctor( Future<Either<Failure, GenericApiModel<dynamic>>> insertFavouriteDoctor(
{required int patientID, required int projectID, required int clinicID, required int doctorID, required bool isActive, Function(dynamic)? onSuccess, Function(String)? onError}); {required int patientID, required int projectID, required int clinicID, required int doctorID, required bool isActive, Function(dynamic)? onSuccess, Function(String)? onError});
Future<Either<Failure, GenericApiModel<dynamic>>> getDoctorSchedule({required int doctorID, required int projectID, required int clinicID, int doctorWorkingHoursDays = 90, Function(dynamic)? onSuccess, Function(String)? onError});
} }
class BookAppointmentsRepoImp implements BookAppointmentsRepo { class BookAppointmentsRepoImp implements BookAppointmentsRepo {
@ -1228,4 +1230,49 @@ class BookAppointmentsRepoImp implements BookAppointmentsRepo {
return Left(UnknownFailure(e.toString())); return Left(UnknownFailure(e.toString()));
} }
} }
@override
Future<Either<Failure, GenericApiModel<dynamic>>> getDoctorSchedule({required int doctorID, required int projectID, required int clinicID, int doctorWorkingHoursDays = 90, Function(dynamic)? onSuccess, Function(String)? onError}) async {
Map<String, dynamic> mapRequest = {
"DoctorID": doctorID,
"ProjectID": projectID,
"ClinicID": clinicID,
"DoctorWorkingHoursDays": doctorWorkingHoursDays,
};
try {
GenericApiModel<dynamic>? apiResponse;
Failure? failure;
await apiClient.post(
DOCTOR_SCHEDULE_URL,
body: mapRequest,
onFailure: (error, statusCode, {messageStatus, failureType}) {
failure = failureType;
if (onError != null) {
onError(error);
}
},
onSuccess: (response, statusCode, {messageStatus, errorMessage}) {
try {
apiResponse = GenericApiModel<dynamic>(
messageStatus: messageStatus,
statusCode: statusCode,
errorMessage: null,
data: response,
);
if (onSuccess != null) {
onSuccess(response);
}
} catch (e) {
failure = DataParsingFailure(e.toString());
}
},
);
if (failure != null) return Left(failure!);
if (apiResponse == null) return Left(ServerFailure("Unknown error"));
return Right(apiResponse!);
} catch (e) {
return Left(UnknownFailure(e.toString()));
}
}
} }

@ -1816,4 +1816,48 @@ class BookAppointmentsViewModel extends ChangeNotifier {
}, },
); );
} }
bool isDoctorScheduleLoading = false;
List<String> doctorFreeSlotDates = [];
dynamic doctorScheduleResponse;
Future<void> getDoctorSchedule({required int doctorID, required int projectID, required int clinicID, int doctorWorkingHoursDays = 90, Function(dynamic)? onSuccess, Function(String)? onError}) async {
isDoctorScheduleLoading = true;
notifyListeners();
final result = await bookAppointmentsRepo.getDoctorSchedule(
doctorID: doctorID,
projectID: projectID,
clinicID: clinicID,
doctorWorkingHoursDays: doctorWorkingHoursDays,
);
result.fold(
(failure) async {
isDoctorScheduleLoading = false;
notifyListeners();
if (onError != null) {
onError(failure.message);
}
},
(apiResponse) {
isDoctorScheduleLoading = false;
if (apiResponse.messageStatus == 2) {
notifyListeners();
if (onError != null) {
onError(apiResponse.errorMessage ?? "Failed to fetch doctor schedule");
}
} else if (apiResponse.messageStatus == 1) {
doctorScheduleResponse = apiResponse.data;
if (doctorScheduleResponse != null && doctorScheduleResponse['FreeTimeSlots'] != null) {
doctorFreeSlotDates = List<String>.from(doctorScheduleResponse['FreeTimeSlots']);
}
notifyListeners();
if (onSuccess != null) {
onSuccess(apiResponse.data);
}
}
},
);
}
} }

@ -0,0 +1,26 @@
class DoctorScheduleResponseModel {
String? date;
String? dayName;
String? workingHours;
DoctorScheduleResponseModel({
this.date,
this.dayName,
this.workingHours,
});
DoctorScheduleResponseModel.fromJson(Map<String, dynamic> json) {
date = json['Date'];
dayName = json['DayName'];
workingHours = json['WorkingHours'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['Date'] = date;
data['DayName'] = dayName;
data['WorkingHours'] = workingHours;
return data;
}
}

@ -469,6 +469,7 @@ abstract class LocaleKeys {
static const serviceInformation = 'serviceInformation'; static const serviceInformation = 'serviceInformation';
static const homeHealthCare = 'homeHealthCare'; static const homeHealthCare = 'homeHealthCare';
static const noAppointmentAvailable = 'noAppointmentAvailable'; static const noAppointmentAvailable = 'noAppointmentAvailable';
static const noScheduleAvailable = 'noScheduleAvailable';
static const loginRegister = 'loginRegister'; static const loginRegister = 'loginRegister';
static const orderLog = 'orderLog'; static const orderLog = 'orderLog';
static const infoLab = 'infoLab'; static const infoLab = 'infoLab';
@ -1943,5 +1944,8 @@ abstract class LocaleKeys {
static const livecareUpdateInsuranceSent = 'livecareUpdateInsuranceSent'; static const livecareUpdateInsuranceSent = 'livecareUpdateInsuranceSent';
static const livecareUpdateInsurance = 'livecareUpdateInsurance'; static const livecareUpdateInsurance = 'livecareUpdateInsurance';
static const schedule = 'schedule'; static const schedule = 'schedule';
static const doctorSchedule = 'doctorSchedule';
static const weeklyShift = 'weeklyShift';
static const dayOff = 'dayOff';
} }

@ -12,6 +12,8 @@ import 'package:hmg_patient_app_new/features/my_appointments/my_appointments_vie
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/book_appointment/widgets/appointment_calendar.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/appointment_calendar.dart';
import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/doctor_rating_details.dart'; import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/doctor_rating_details.dart';
import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/doctor_schedule_page.dart';
import 'package:hmg_patient_app_new/widgets/routes/custom_page_route.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/appbar/collapsing_list_view.dart'; import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart'; import 'package:hmg_patient_app_new/widgets/buttons/custom_button.dart';
@ -200,7 +202,9 @@ class DoctorProfilePage extends StatelessWidget {
LocaleKeys.schedule.tr(context: context).toText12(isBold: true, color: AppColors.greyTextColor), LocaleKeys.schedule.tr(context: context).toText12(isBold: true, color: AppColors.greyTextColor),
LocaleKeys.view.tr(context: context).toText16(isBold: true, color: AppColors.textColor, isUnderLine: true, decorationColor: AppColors.textColor, fontFamily: "Poppins"), LocaleKeys.view.tr(context: context).toText16(isBold: true, color: AppColors.textColor, isUnderLine: true, decorationColor: AppColors.textColor, fontFamily: "Poppins"),
], ],
).onPress(() {}) ).onPress(() {
_openDoctorSchedule(context);
})
], ],
), ),
SizedBox(height: 16.h), SizedBox(height: 16.h),
@ -303,4 +307,75 @@ class DoctorProfilePage extends StatelessWidget {
), ),
); );
} }
// Guard against double-tap opening two bottom sheets simultaneously
static bool _scheduleInFlight = false;
void _openDoctorSchedule(BuildContext context) async {
if (_scheduleInFlight) return;
_scheduleInFlight = true;
final bookAppointmentsViewModel = Provider.of<BookAppointmentsViewModel>(context, listen: false);
LoaderBottomSheet.showLoader(loadingText: LocaleKeys.fetchingDoctorSchedulePleaseWait.tr(context: context));
// Yield to the frame scheduler so the loader is fully painted
// before the API calls start (prevents the "loader appears late" glitch).
await Future<void>.delayed(Duration.zero);
if (bookAppointmentsViewModel.freeSlotsResponse == null || bookAppointmentsViewModel.freeSlotsResponse.isEmpty) {
await bookAppointmentsViewModel.getDoctorFreeSlots(
isBookingForLiveCare: false,
onSuccess: (dynamic respData) async {
await _fetchDoctorSchedule(context, bookAppointmentsViewModel);
},
onError: (err) {
_scheduleInFlight = false;
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: err),
callBackFunc: () {},
isFullScreen: true,
isCloseButtonVisible: true,
);
},
);
} else {
await _fetchDoctorSchedule(context, bookAppointmentsViewModel);
}
}
Future<void> _fetchDoctorSchedule(BuildContext context, BookAppointmentsViewModel bookAppointmentsViewModel) async {
await bookAppointmentsViewModel.getDoctorSchedule(
doctorID: bookAppointmentsViewModel.selectedDoctor.doctorID!,
projectID: bookAppointmentsViewModel.selectedDoctor.projectID!,
clinicID: bookAppointmentsViewModel.selectedDoctor.clinicID!,
doctorWorkingHoursDays: 90,
onSuccess: (dynamic respData) async {
_scheduleInFlight = false;
LoaderBottomSheet.hideLoader();
if (context.mounted) {
await Navigator.of(context).push(
CustomPageRoute(
page: const DoctorSchedulePage(),
fullScreenDialog: true,
direction: AxisDirection.down,
),
);
}
},
onError: (err) {
_scheduleInFlight = false;
LoaderBottomSheet.hideLoader();
showCommonBottomSheetWithoutHeight(
context,
child: Utils.getErrorWidget(loadingText: err),
callBackFunc: () {},
isFullScreen: false,
isCloseButtonVisible: true,
);
},
);
}
} }

@ -0,0 +1,400 @@
import 'dart:ui' as ui;
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/app_state.dart';
import 'package:hmg_patient_app_new/core/dependencies.dart';
import 'package:hmg_patient_app_new/core/utils/date_util.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/core/utils/utils.dart';
import 'package:hmg_patient_app_new/features/book_appointments/book_appointments_view_model.dart';
import 'package:hmg_patient_app_new/features/book_appointments/models/resp_models/doctor_schedule_response_model.dart';
import 'package:hmg_patient_app_new/extensions/string_extensions.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/services/navigation_service.dart';
import 'package:hmg_patient_app_new/theme/colors.dart';
import 'package:provider/provider.dart';
class DoctorScheduleBottomSheet extends StatefulWidget {
const DoctorScheduleBottomSheet({super.key});
@override
State<DoctorScheduleBottomSheet> createState() => _DoctorScheduleBottomSheetState();
}
class _DoctorScheduleBottomSheetState extends State<DoctorScheduleBottomSheet>
with SingleTickerProviderStateMixin {
late PageController _pageController;
late AnimationController _fadeController;
late Animation<double> _fadeAnimation;
double currentPage = 0;
List<List<Map<String, dynamic>>> weeks = [];
List<DoctorScheduleResponseModel> doctorScheduleResponse = [];
List<String> freeSlots = [];
/// True while weeks are being computed off the build thread
bool _isInitializing = false;
late BookAppointmentsViewModel bookAppointmentsViewModel;
late AppState appState;
@override
void initState() {
super.initState();
_pageController = PageController();
_fadeController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 400),
);
_fadeAnimation = CurvedAnimation(parent: _fadeController, curve: Curves.easeIn);
}
@override
void dispose() {
_pageController.dispose();
_fadeController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
bookAppointmentsViewModel = Provider.of<BookAppointmentsViewModel>(context, listen: true);
appState = getIt.get<AppState>();
final isArabic = appState.isArabic();
// Trigger initialization after the first frame so the UI never freezes
final responseReady = bookAppointmentsViewModel.doctorScheduleResponse != null;
if (responseReady && weeks.isEmpty && !_isInitializing) {
WidgetsBinding.instance.addPostFrameCallback((_) => _triggerInit());
}
// API loading
if (bookAppointmentsViewModel.isDoctorScheduleLoading || _isInitializing) {
return Center(
child: CircularProgressIndicator(
color: AppColors.primaryRedColor,
strokeWidth: 2.5,
),
);
}
// No data
if (weeks.isEmpty) {
return Center(
child: Utils.getNoDataWidget(
context,
noDataText: LocaleKeys.noScheduleAvailable.tr(context: context),
),
);
}
// Schedule ready fade in
return FadeTransition(
opacity: _fadeAnimation,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(context, isArabic),
SizedBox(height: 20.h),
Expanded(
child: PageView.builder(
controller: _pageController,
itemCount: weeks.length,
onPageChanged: (index) => setState(() => currentPage = index.toDouble()),
itemBuilder: (context, weekIndex) => _buildWeekView(weekIndex, isArabic),
),
),
],
),
);
}
// Initialization
void _triggerInit() {
if (!mounted || _isInitializing || weeks.isNotEmpty) return;
setState(() => _isInitializing = true);
// Defer heavy work to the next microtask so the spinner is painted first
Future.microtask(() {
_buildWeeksFromResponse();
if (mounted) {
setState(() => _isInitializing = false);
_fadeController.forward();
}
});
}
void _buildWeeksFromResponse() {
final response = bookAppointmentsViewModel.doctorScheduleResponse;
if (response == null) return;
final scheduleList = response['List_DoctorWorkingHoursTable'] as List?;
if (scheduleList == null || scheduleList.isEmpty) return;
doctorScheduleResponse =
scheduleList.map((item) => DoctorScheduleResponseModel.fromJson(item)).toList();
// Resolve free-slot dates
if (bookAppointmentsViewModel.doctorFreeSlotDates.isNotEmpty) {
freeSlots = bookAppointmentsViewModel.doctorFreeSlotDates;
} else if (bookAppointmentsViewModel.freeSlotsResponse != null &&
bookAppointmentsViewModel.freeSlotsResponse.isNotEmpty) {
freeSlots = List<String>.from(bookAppointmentsViewModel.freeSlotsResponse);
} else if (response['FreeTimeSlots'] != null) {
freeSlots = List<String>.from(response['FreeTimeSlots']);
}
_generateWeeksSchedules();
}
/// Splits ALL API days into 7-day chunks one chunk per page.
/// Supports full 3-month schedules (~13 weeks) returned by the API.
void _generateWeeksSchedules() {
weeks.clear();
// Convert every API day into a flat map and mark free slots
final allDays = doctorScheduleResponse.map((workDay) {
return <String, dynamic>{
'Date': workDay.date ?? '',
'DayName': workDay.dayName ?? '',
'WorkingHours': workDay.workingHours ?? '',
'fullDay': !_hasFreeSlot(workDay.date ?? ''),
};
}).toList();
// Slice into pages of 7 days each
const daysPerPage = 7;
for (var i = 0; i < allDays.length; i += daysPerPage) {
final end = (i + daysPerPage).clamp(0, allDays.length);
weeks.add(allDays.sublist(i, end));
}
}
// Header
Widget _buildHeader(BuildContext context, bool isArabic) {
final currentWeek = weeks[currentPage.round()];
final firstDate = DateUtil.convertStringToDate(currentWeek.first['Date'] as String);
final lastDate = DateUtil.convertStringToDate(currentWeek.last['Date'] as String);
final dateRangeLabel = isArabic
? '${firstDate.day} ${DateUtil.getMonthArabic(firstDate.month).trim()} '
'${lastDate.day} ${DateUtil.getMonthArabic(lastDate.month).trim()}, ${lastDate.year}'
: '${DateFormat('MMM dd', 'en_US').format(firstDate)} '
'${DateFormat('MMM dd', 'en_US').format(lastDate)}, ${lastDate.year}';
return Padding(
padding: EdgeInsets.symmetric(horizontal: 5.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Text(
// "Doctor Schedule",
// style: TextStyle(
// fontSize: 24.h,
// fontWeight: FontWeight.bold,
// color: const Color(0xFF1A1A1A),
// ),
// ),
SizedBox(height: 16.h),
// Navigation: < date-pill >
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildNavArrow(
icon: Icons.chevron_left,
enabled: currentPage.round() > 0,
onTap: () => _animateToPage(currentPage.round() - 1),
),
SizedBox(width: 12.w),
Container(
padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 10.h),
decoration: BoxDecoration(
color: AppColors.secondaryLightRedColor,
borderRadius: BorderRadius.circular(12.r),
),
child: dateRangeLabel.toText14(
weight: FontWeight.w600,
color: AppColors.primaryRedColor,
),
),
SizedBox(width: 12.w),
_buildNavArrow(
icon: Icons.chevron_right,
enabled: currentPage.round() < weeks.length - 1,
onTap: () => _animateToPage(currentPage.round() + 1),
),
],
),
SizedBox(height: 20.h),
LocaleKeys.weeklyShift.tr(context: context).toText18(
weight: FontWeight.w600,
color: AppColors.textColor,
),
],
),
);
}
void _animateToPage(int index) {
_pageController.animateToPage(
index.clamp(0, weeks.length - 1),
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
Widget _buildNavArrow({
required IconData icon,
required bool enabled,
required VoidCallback onTap,
}) {
return GestureDetector(
onTap: enabled ? onTap : null,
child: Container(
width: 34.w,
height: 34.w,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8.h),
color: enabled ? AppColors.secondaryLightRedColor : AppColors.greyF7Color,
),
child: Icon(
icon,
size: 28.h,
color: enabled ? AppColors.primaryRedColor : AppColors.checkBoxBorderColor,
),
),
);
}
// Week / day views
Widget _buildWeekView(int weekIndex, bool isArabic) {
final weekSchedule = weeks[weekIndex];
return Directionality(
textDirection: isArabic ? ui.TextDirection.rtl : ui.TextDirection.ltr,
child: ListView.builder(
// primary: false detaches this ListView from the PrimaryScrollController
// so it doesn't fight with CollapsingListView's outer CustomScrollView.
primary: false,
physics: const ClampingScrollPhysics(),
padding: EdgeInsets.only(top: 8.h, bottom: 24.h),
itemCount: weekSchedule.length,
itemBuilder: (context, dayIndex) => _buildDayCard(context, weekSchedule[dayIndex]),
),
);
}
Widget _buildDayCard(BuildContext context, Map<String, dynamic> daySchedule) {
final date = DateUtil.convertStringToDate(daySchedule['Date'] as String);
final hasFreeSlots = !(daySchedule['fullDay'] as bool? ?? true);
final workingHours = (daySchedule['WorkingHours'] as String?) ?? '';
final isOff = workingHours.isEmpty ||
workingHours.toLowerCase().contains('off') ||
workingHours.toLowerCase().contains('not available');
final shifts = workingHours.split(',').map((s) => s.trim()).where((s) => s.isNotEmpty).toList();
return Material(
color: Colors.transparent,
child: InkWell(
onTap: hasFreeSlots ? () => getIt.get<NavigationService>().pop() : null,
borderRadius: BorderRadius.circular(20.r),
child: Container(
margin: EdgeInsets.only(bottom: 12.h, left: 5.w, right: 5.w),
padding: EdgeInsets.symmetric(horizontal: 18.w, vertical: 16.h),
decoration: BoxDecoration(
color: AppColors.whiteColor,
borderRadius: BorderRadius.circular(20.r),
boxShadow: [
BoxShadow(
blurRadius: 16,
spreadRadius: 0,
color: Colors.black.withValues(alpha: 0.02),
offset: const Offset(0, 6),
),
],
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Left day name + short date
Expanded(
flex: 2,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
daySchedule['DayName'].toString().toText16(
weight: FontWeight.w700,
color: AppColors.textColor,
),
SizedBox(height: 4.h),
_getDayMonth(date).toText13(
color: AppColors.greyTextColorLight,
),
],
),
),
// Right time slots or "Off"
if (isOff)
LocaleKeys.dayOff.tr(context: context).toText14(
weight: FontWeight.w500,
color: AppColors.greyTextColorLight,
)
else
Expanded(
flex: 3,
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
for (var i = 0; i < shifts.length; i++) ...[
shifts[i].toText14(
weight: FontWeight.w500,
color: AppColors.textColor,
),
if (i < shifts.length - 1) SizedBox(height: 6.h),
],
],
),
),
],
),
),
),
);
}
// Helpers
bool _hasFreeSlot(String jsonDate) {
if (jsonDate.isEmpty) return false;
final date = DateUtil.convertStringToDate(jsonDate);
final scheduleDay = DateTime(date.year, date.month, date.day);
for (final event in freeSlots) {
try {
final eventDate = DateUtil.convertStringToDate(event);
final nDate = DateTime(eventDate.year, eventDate.month, eventDate.day);
if (nDate.millisecondsSinceEpoch == scheduleDay.millisecondsSinceEpoch) return true;
} catch (_) {
continue;
}
}
return false;
}
String _getDayMonth(DateTime dateTime) {
final isArabic = appState.isArabic();
if (isArabic) {
return '${dateTime.day} ${DateUtil.getMonthArabic(dateTime.month)}';
}
return '${dateTime.day} ${DateUtil.getMonth(dateTime.month).substring(0, 3)}';
}
}

@ -0,0 +1,39 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hmg_patient_app_new/core/utils/size_utils.dart';
import 'package:hmg_patient_app_new/generated/locale_keys.g.dart';
import 'package:hmg_patient_app_new/presentation/book_appointment/widgets/doctor_schedule_bottom_sheet.dart';
import 'package:hmg_patient_app_new/widgets/appbar/collapsing_list_view.dart';
class DoctorSchedulePage extends StatelessWidget {
const DoctorSchedulePage({super.key});
@override
Widget build(BuildContext context) {
return CollapsingListView(
title: LocaleKeys.doctorSchedule.tr(),
isClose: true,
child: Builder(
builder: (ctx) {
final mq = MediaQuery.of(ctx);
// SliverToBoxAdapter is unbounded give explicit height so
// the PageView (Expanded) inside DoctorScheduleBottomSheet works.
final height = mq.size.height - mq.padding.top - 56.h;
return SizedBox(
height: height,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 24.w),
// Stop scroll notifications from bubbling up to CollapsingListView's
// outer CustomScrollView, which would steal vertical scroll gestures.
child: NotificationListener<ScrollNotification>(
onNotification: (_) => true,
child: const DoctorScheduleBottomSheet(),
),
),
);
},
),
);
}
}
Loading…
Cancel
Save