Bug Fixes

faiz_dev
FaizHashmi 13 hours ago
parent 67cf63f732
commit c56aa5d32e

@ -1,4 +1,3 @@
import 'package:car_provider_app/views/appoinments/add_new_service_appointment_page.dart';
import 'package:car_provider_app/views/appoinments/appoinment_detail_list_page.dart';
import 'package:car_provider_app/views/appoinments/appointment_page.dart';
import 'package:car_provider_app/views/appoinments/merge_appointment_page.dart';
@ -20,7 +19,6 @@ import 'package:car_provider_app/views/subscriptions/my_subscritions_page.dart';
import 'package:car_provider_app/views/subscriptions/all_subscriptions_page.dart';
import 'package:mc_common_app/config/routes.dart';
import 'package:flutter/material.dart';
import 'package:mc_common_app/models/appointments_models/appointment_list_model.dart';
import 'package:mc_common_app/models/provider_branches_models/branch_detail_model.dart';
import 'package:mc_common_app/utils/enums.dart';
import 'package:mc_common_app/views/advertisement/favourite_ads_view.dart';
@ -44,7 +42,6 @@ class ProviderAppRoutes {
AppRoutes.appointment: (context) => AppointmentPage(branch: ModalRoute.of(context)!.settings.arguments as BranchDetailModel),
AppRoutes.appointmentDetailList: (context) => AppointmentDetailListPage(),
AppRoutes.updateAppointmentPage: (context) => const UpdateAppointmentPage(),
AppRoutes.addServiceInAppointment: (context) => AddNewServiceAppointmentPage(ModalRoute.of(context)!.settings.arguments as AppointmentListModel),
AppRoutes.mergeAppointments: (context) => const MergeAppointmentListPage(),
AppRoutes.appoinmentSearchFilterPage: (context) => const AppointmentsFilterView(),

@ -1,433 +0,0 @@
import 'dart:async';
import 'package:car_provider_app/views/dashboard/widget/general_appointment_widget.dart';
import 'package:flutter/material.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:provider/provider.dart';
import 'package:mc_common_app/classes/consts.dart';
import 'package:mc_common_app/extensions/int_extensions.dart';
import 'package:mc_common_app/extensions/string_extensions.dart';
import 'package:mc_common_app/generated/locale_keys.g.dart';
import 'package:mc_common_app/models/appointments_models/appointment_list_model.dart';
import 'package:mc_common_app/models/general_models/generic_resp_model.dart';
import 'package:mc_common_app/theme/colors.dart';
import 'package:mc_common_app/utils/navigator.dart';
import 'package:mc_common_app/utils/enums.dart';
import 'package:mc_common_app/utils/utils.dart';
import 'package:mc_common_app/view_models/appointments_view_model.dart';
import 'package:mc_common_app/view_models/service_view_model.dart';
import 'package:mc_common_app/widgets/button/show_fill_button.dart';
import 'package:mc_common_app/widgets/common_widgets/app_bar.dart';
import 'package:mc_common_app/widgets/dropdown/dropdow_field.dart';
import 'package:mc_common_app/widgets/extensions/extensions_widget.dart';
import 'package:mc_common_app/widgets/txt_field.dart';
import 'package:mc_common_app/models/appointments_models/selected_appointment_service_model.dart';
import 'package:car_provider_app/views/appoinments/widget/add_appointment_service_bottom_sheet.dart';
class AddNewServiceAppointmentPage extends StatefulWidget {
final AppointmentListModel appointmentListModel;
const AddNewServiceAppointmentPage(this.appointmentListModel, {Key? key})
: super(key: key);
@override
State<AddNewServiceAppointmentPage> createState() =>
_AddNewServiceAppointmentPageState();
}
class _AddNewServiceAppointmentPageState
extends State<AddNewServiceAppointmentPage> {
final List<SelectedAppointmentServiceModel> appointmentSelectedServices = [];
ServiceVM? serviceVM;
bool isSubmitting = false;
DropValue? selectedCurrency;
final List<DropValue> currencies = [
DropValue(1, 'SAR', 'SAR'),
DropValue(2, 'AED', 'AED'),
DropValue(3, 'USD', 'USD'),
];
@override
void initState() {
super.initState();
scheduleMicrotask(() {
if (!mounted) return;
serviceVM = context.read<ServiceVM>();
serviceVM!.fetchBranchCategory(
EasyLocalization.of(context)?.currentLocale?.countryCode ?? 'SA',
);
});
}
Future<void> _openAddServiceSheet({int? editIndex}) async {
if (isSubmitting) return;
final existing = editIndex == null
? null
: appointmentSelectedServices[editIndex];
final result = await showModalBottomSheet<SelectedAppointmentServiceModel>(
context: context,
isScrollControlled: true,
enableDrag: true,
builder: (_) => AddAppointmentServiceBottomSheet(
branchId: widget.appointmentListModel.branchId ?? 0,
initialService: existing,
),
);
if (!mounted || result == null) return;
setState(() {
if (editIndex == null) {
appointmentSelectedServices.add(result);
} else {
appointmentSelectedServices[editIndex] = result;
}
});
}
double get _totalItemsPrice => appointmentSelectedServices.fold<double>(
0,
(sum, selected) => sum + selected.totalPrice,
);
List<int> get _allSelectedItemIds => appointmentSelectedServices
.expand((selected) => selected.itemIds)
.where((id) => id > 0)
.toSet()
.toList();
Future<void> _submit() async {
if (isSubmitting) return;
final vm = serviceVM ?? context.read<ServiceVM>();
Map<String, dynamic> postParams;
if (vm.customServiceEnabledStatus) {
if (!vm.isCustomServiceDetailValidated()) return;
postParams = {
'providerBranchID': widget.appointmentListModel.branchId,
'appointmentID': widget.appointmentListModel.id,
'serviceItemsFreeText': [
{
'name': vm.customServiceTitle,
'price': vm.customServicePrice,
'tax': vm.customerServiceTax,
}
],
};
} else {
final ids = _allSelectedItemIds;
if (ids.isEmpty) {
Utils.showToast(LocaleKeys.pleaseSelectItems.tr());
return;
}
postParams = {
'providerBranchID': widget.appointmentListModel.branchId,
'appointmentID': widget.appointmentListModel.id,
'serviceItemID': ids,
};
}
setState(() => isSubmitting = true);
bool loadingVisible = false;
try {
Utils.showLoading(context);
loadingVisible = true;
final GenericRespModel res =
await vm.addNewServiceInAppointment(postParams);
if (!mounted) return;
Utils.hideLoading(context);
loadingVisible = false;
if (res.messageStatus == 1) {
vm.resetCustomServiceForm();
await _updateAppointment(context, widget.appointmentListModel.branchId ?? 0);
if (!mounted) return;
Utils.showToast(LocaleKeys.itemsAddedSuccessfully.tr());
pop(context);
} else {
Utils.showToast(res.message.toString());
}
} catch (e) {
if (mounted) Utils.showToast(e.toString());
} finally {
if (mounted && loadingVisible) Utils.hideLoading(context);
if (mounted) setState(() => isSubmitting = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: MyColors.white,
appBar: CustomAppBar(title: LocaleKeys.addService.tr()),
body: Padding(
padding: const EdgeInsets.all(21.0),
child: Stack(
fit: StackFit.expand,
children: [
SingleChildScrollView(
child: Column(
children: [
GeneralAppointmentWidget(
appointmentListModel: widget.appointmentListModel,
isNeedTotalPayment: true,
isNeedToShowItems: true,
isNeedToShowToMoreText: false,
onTap: () {},
),
12.height,
Consumer<ServiceVM>(
builder: (context, ServiceVM serviceVm, _) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
LocaleKeys.customService.tr().toText(fontSize: 18),
8.width,
Center(
child: Checkbox(
value: serviceVm.customServiceEnabledStatus,
onChanged: (value) {
serviceVm.updateCustomServiceEnabledStatus(
value ?? false,
);
},
activeColor: MyColors.darkPrimaryColor,
checkColor: MyColors.white,
fillColor: serviceVm.customServiceEnabledStatus
? WidgetStateProperty.all(MyColors.darkPrimaryColor)
: WidgetStateProperty.all(MyColors.white),
),
),
],
),
5.height,
if (serviceVm.customServiceEnabledStatus) ...[
LocaleKeys.addServiceDetails.tr().toText(fontSize: 16),
12.height,
Column(
children: [
8.height,
TxtField(
value: serviceVm.customServiceTitle,
errorValue: serviceVm.customServiceTitleError,
hint: LocaleKeys.serviceName.tr(),
onChanged: (v) => serviceVm.updateCustomServiceTitle(v),
),
8.height,
Row(
children: [
Expanded(
flex: 2,
child: TxtField(
value: serviceVm.customServicePrice,
errorValue: serviceVm.customServicePriceError,
keyboardType: TextInputType.number,
numbersOnly: true,
hint: LocaleKeys.price.tr(),
onChanged: (v) => serviceVm.updateCustomServicePrice(v),
),
),
8.width,
Expanded(
flex: 1,
child: DropdownField(
(DropValue value) {
setState(() => selectedCurrency = value);
},
dropdownValue: selectedCurrency,
list: currencies,
hint: 'Currency',
),
),
],
),
8.height,
TxtField(
postfixWidget: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
'%'.toText(
fontWeight: MyFonts.Medium,
fontSize: 15,
color: borderColor,
textAlign: TextAlign.center,
),
],
),
isNeedClickAll: true,
isBackgroundEnabled: true,
hint: LocaleKeys.tax.tr(),
onTap: () {},
value: serviceVm.customerServiceTax,
errorValue: '',
keyboardType: TextInputType.number,
numbersOnly: true,
onChanged: (v) => serviceVm.updateCustomerServiceTax(v),
),
],
),
] else ...[
// READY-MADE SERVICES: select via Bottom Sheet, display cards.
LocaleKeys.selectServiceDetails.tr().toText(fontSize: 16),
12.height,
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: isSubmitting ? null : () => _openAddServiceSheet(),
icon: const Icon(Icons.add_circle_outline),
label: Text(LocaleKeys.addServices.tr()),
style: OutlinedButton.styleFrom(
minimumSize: const Size.fromHeight(52),
foregroundColor: MyColors.darkPrimaryColor,
side: const BorderSide(color: MyColors.darkPrimaryColor),
),
),
),
12.height,
...appointmentSelectedServices.asMap().entries.map((entry) {
final index = entry.key;
final selected = entry.value;
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
selected.service.value,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
// Edit
MyAssets.icEdit.buildSvg(
height: 17,
width: 17,
).onPress(() {
_openAddServiceSheet(editIndex: index);
}),
const SizedBox(width: 10),
// Delete
MyAssets.closeWithOrangeBg.buildSvg(
height: 30,
width: 30,
).onPress(() {
setState(() {
appointmentSelectedServices.removeAt(index);
});
}),
],
),
Text(
selected.category.value,
style: const TextStyle(color: Colors.grey),
),
8.height,
...selected.items.map(
(item) => Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Row(
children: [
Expanded(child: Text(item.name ?? '')),
Text('${item.price ?? '0'} SAR'),
],
),
),
),
const Divider(),
Text(
'${selected.totalPrice.toStringAsFixed(2)} SAR',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
],
),
),
);
}),
if (appointmentSelectedServices.isNotEmpty) ...[
12.height,
LocaleKeys.totalAdditionalAmount.tr().toText(
fontSize: 14,
isBold: true,
color: MyColors.lightTextColor,
),
Text(
'${_totalItemsPrice.toStringAsFixed(2)} SAR',
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
),
),
],
],
],
).toWhiteContainer(width: double.infinity, allPading: 12);
},
),
85.height,
],
),
),
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Row(
children: [
Expanded(
child: ShowFillButton(
title: LocaleKeys.cancel.tr(),
maxWidth: double.infinity,
isFilled: false,
txtColor: MyColors.redColor,
borderColor: MyColors.redColor,
onPressed: () {
(serviceVM ?? context.read<ServiceVM>())
.resetCustomServiceForm();
pop(context);
},
),
),
12.width,
Expanded(
child: ShowFillButton(
title: LocaleKeys.add.tr(),
maxWidth: double.infinity,
onPressed: _submit,
),
),
],
).toWhiteContainer(width: double.infinity),
),
],
),
),
);
}
Future<void> _updateAppointment(BuildContext context, int branchId) async {
await context.read<AppointmentsVM>().applyFilterOnAppointmentsVMForProviders(
branchID: branchId,
appointmentStatusEnum: AppointmentStatusEnum.allAppointments,
isNeedCustomerFilter: true,
);
}
}

@ -1,18 +1,20 @@
import 'package:car_provider_app/views/appoinments/widget/add_appointment_service_bottom_sheet.dart';
import 'package:car_provider_app/views/appoinments/widget/sheets.dart';
import 'package:car_provider_app/views/dashboard/widget/general_appointment_widget.dart';
import 'package:flutter/material.dart';
import 'package:mc_common_app/classes/app_state.dart';
import 'package:mc_common_app/config/dependency_injection.dart';
import 'package:mc_common_app/config/routes.dart';
import 'package:mc_common_app/classes/consts.dart';
import 'package:mc_common_app/extensions/int_extensions.dart';
import 'package:mc_common_app/extensions/string_extensions.dart';
import 'package:mc_common_app/generated/locale_keys.g.dart';
import 'package:mc_common_app/models/appointments_models/appointment_list_model.dart';
import 'package:mc_common_app/models/appointments_models/selected_appointment_service_model.dart';
import 'package:mc_common_app/models/general_models/generic_resp_model.dart';
import 'package:mc_common_app/theme/colors.dart';
import 'package:mc_common_app/utils/enums.dart';
import 'package:mc_common_app/utils/navigator.dart';
import 'package:mc_common_app/utils/utils.dart';
import 'package:mc_common_app/view_models/appointments_view_model.dart';
import 'package:mc_common_app/widgets/bottom_sheet.dart';
import 'package:mc_common_app/view_models/service_view_model.dart';
import 'package:mc_common_app/widgets/button/show_fill_button.dart';
import 'package:mc_common_app/widgets/common_widgets/app_bar.dart';
import 'package:mc_common_app/widgets/extensions/extensions_widget.dart';
@ -29,6 +31,15 @@ class UpdateAppointmentPage extends StatefulWidget {
class _UpdateAppointmentPageState extends State<UpdateAppointmentPage> {
late AppointmentListModel appointmentListModel;
// Ready-made services picked via the bottom sheet, staged locally (not yet submitted
// to the API) -- one sheet open per service, each pop appends one card here. Custom
// services bypass this entirely: the sheet submits those itself and just signals
// "refresh" on success (see _openAddServiceSheet). While this list is non-empty, the
// footer swaps to Add/Cancel for it instead of the normal status-driven buttons
// (Arrived, Pay Now, etc.), since both sets of actions can't compete for the same bar.
final List<SelectedAppointmentServiceModel> pendingNewServices = [];
bool isSubmittingNewService = false;
@override
Widget build(BuildContext context) {
return Scaffold(
@ -38,17 +49,11 @@ class _UpdateAppointmentPageState extends State<UpdateAppointmentPage> {
height: double.infinity,
child: Consumer(builder: (BuildContext context, AppointmentsVM appointmentsVM, Widget? child) {
appointmentListModel = appointmentsVM.myFilteredAppointmentsForProvider[appointmentsVM.selectedAppointmentIndex].customerAppointmentList![appointmentsVM.selectedAppointmentSubIndex];
print('========== SELECTED APPOINTMENT ==========');
print('id: ${appointmentListModel.id}');
print('servicePaymentStatus: ${appointmentListModel.servicePaymentStatus}');
print(
'appointmentPaymentStatusEnum: '
'${appointmentListModel.appointmentPaymentStatusEnum}',
);
if (appointmentsVM.state == ViewState.busy) {
return const Center(child: CircularProgressIndicator());
} else {
final bool hasPendingAdd = pendingNewServices.isNotEmpty;
return RefreshIndicator(
onRefresh: () async => _updateAppointment(context, appointmentListModel.branchId ?? 0),
child: Column(
@ -69,136 +74,121 @@ class _UpdateAppointmentPageState extends State<UpdateAppointmentPage> {
if (appointmentListModel.appointmentStatusEnum == AppointmentStatusEnum.confirmed ||
appointmentListModel.appointmentStatusEnum == AppointmentStatusEnum.arrived ||
appointmentListModel.appointmentStatusEnum == AppointmentStatusEnum.workStarted) ...[
if (pendingNewServices.isNotEmpty) ...[
..._buildPendingCards(),
12.height,
],
ShowFillButton(
title: ("+ ${LocaleKeys.addNewService.tr()}"),
txtColor: MyColors.darkPrimaryColor,
isFilled: false,
onPressed: () {
navigateWithName(
context,
AppRoutes.addServiceInAppointment,
arguments: appointmentListModel,
);
},
onPressed: isSubmittingNewService ? null : () => _openAddServiceSheet(context),
),
],
],
),
),
if (appointmentListModel.appointmentStatusEnum == AppointmentStatusEnum.cancelled) ...[
Utils.buildStatusContainer(LocaleKeys.appointmentCancelled.tr()).paddingAll(10),
],
if (appointmentListModel.appointmentStatusEnum == AppointmentStatusEnum.booked)
if (hasPendingAdd) ...[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(child: showPayNowButton(context, appointmentsVM, color: MyColors.greenColor)),
Expanded(
child: ShowFillButton(
title: LocaleKeys.cancel.tr(),
maxWidth: double.infinity,
isFilled: false,
txtColor: MyColors.redColor,
borderColor: MyColors.redColor,
onPressed: isSubmittingNewService ? null : _cancelPendingNewServices,
),
),
12.width,
Expanded(child: showCancelButton(context, appointmentsVM)),
],
).paddingAll(21),
if (appointmentListModel.appointmentStatusEnum == AppointmentStatusEnum.confirmed &&
(appointmentListModel.appointmentPaymentStatusEnum == AppointmentPaymentStatusEnum.payPartial ||
appointmentListModel.appointmentPaymentStatusEnum == AppointmentPaymentStatusEnum.defaultStatus))
Column(
children: [
Row(
children: [
Expanded(child: showArrivedButton(context, appointmentsVM)),
12.width,
Expanded(child: showCancelButton(context, appointmentsVM)),
],
Expanded(
child: ShowFillButton(
title: LocaleKeys.done.tr(),
maxWidth: double.infinity,
onPressed: isSubmittingNewService ? null : _submitPendingNewServices,
),
),
],
).paddingAll(21),
] else ...[
if (appointmentListModel.appointmentStatusEnum == AppointmentStatusEnum.cancelled) ...[
Utils.buildStatusContainer(LocaleKeys.appointmentCancelled.tr()).paddingAll(10),
],
if (appointmentListModel.appointmentStatusEnum == AppointmentStatusEnum.booked)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(child: showPayNowButton(context, appointmentsVM, color: MyColors.greenColor)),
12.width,
Expanded(child: showCancelButton(context, appointmentsVM)),
],
).paddingAll(21),
if (appointmentListModel.appointmentStatusEnum == AppointmentStatusEnum.arrived &&
(appointmentListModel.appointmentPaymentStatusEnum == AppointmentPaymentStatusEnum.payPartial ||
appointmentListModel.appointmentPaymentStatusEnum == AppointmentPaymentStatusEnum.defaultStatus))
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(child: showPayNowButton(context, appointmentsVM)),
12.width,
Expanded(child: showPayLaterButton(context, appointmentsVM)),
],
).paddingAll(21),
if (appointmentListModel.appointmentStatusEnum == AppointmentStatusEnum.confirmed &&
(appointmentListModel.appointmentPaymentStatusEnum == AppointmentPaymentStatusEnum.payPartial ||
appointmentListModel.appointmentPaymentStatusEnum == AppointmentPaymentStatusEnum.defaultStatus))
Column(
children: [
Row(
children: [
Expanded(child: showArrivedButton(context, appointmentsVM)),
12.width,
Expanded(child: showCancelButton(context, appointmentsVM)),
],
),
],
).paddingAll(21),
if ((appointmentListModel.appointmentStatusEnum == AppointmentStatusEnum.arrived || appointmentListModel.appointmentStatusEnum == AppointmentStatusEnum.workStarted) &&
appointmentListModel.appointmentPaymentStatusEnum == AppointmentPaymentStatusEnum.payNow)
Utils.buildStatusContainer(LocaleKeys.waitingPaymentfromtheCustomer.tr()).paddingAll(10),
if (appointmentListModel.appointmentStatusEnum == AppointmentStatusEnum.arrived &&
(appointmentListModel.appointmentPaymentStatusEnum == AppointmentPaymentStatusEnum.payPartial ||
appointmentListModel.appointmentPaymentStatusEnum == AppointmentPaymentStatusEnum.defaultStatus))
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(child: showPayNowButton(context, appointmentsVM)),
12.width,
Expanded(child: showPayLaterButton(context, appointmentsVM)),
],
).paddingAll(21),
if (appointmentListModel.appointmentStatusEnum == AppointmentStatusEnum.arrived &&
(appointmentListModel.appointmentPaymentStatusEnum == AppointmentPaymentStatusEnum.paid ||
appointmentListModel.appointmentPaymentStatusEnum == AppointmentPaymentStatusEnum.payLater))
Column(
children: [
showWorkStartButton(context, appointmentsVM),
],
).paddingAll(21),
if ((appointmentListModel.appointmentStatusEnum == AppointmentStatusEnum.arrived || appointmentListModel.appointmentStatusEnum == AppointmentStatusEnum.workStarted) &&
appointmentListModel.appointmentPaymentStatusEnum == AppointmentPaymentStatusEnum.payNow)
Utils.buildStatusContainer(LocaleKeys.waitingPaymentfromtheCustomer.tr()).paddingAll(10),
if (appointmentListModel.appointmentStatusEnum == AppointmentStatusEnum.workStarted && appointmentListModel.appointmentPaymentStatusEnum == AppointmentPaymentStatusEnum.payPartial)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(child: showPayNowButton(context, appointmentsVM)),
12.width,
Expanded(child: showPayLaterButton(context, appointmentsVM)),
],
).paddingAll(21),
// "Show Pay Now".toText(),
if (appointmentListModel.appointmentStatusEnum == AppointmentStatusEnum.arrived &&
(appointmentListModel.appointmentPaymentStatusEnum == AppointmentPaymentStatusEnum.paid ||
appointmentListModel.appointmentPaymentStatusEnum == AppointmentPaymentStatusEnum.payLater))
Column(
children: [
showWorkStartButton(context, appointmentsVM),
],
).paddingAll(21),
if (appointmentListModel.appointmentStatusEnum == AppointmentStatusEnum.workStarted && appointmentListModel.appointmentPaymentStatusEnum == AppointmentPaymentStatusEnum.paid)
Column(
children: [
showCompleteButton(context, appointmentsVM),
],
).paddingAll(21),
// "Show Complete Button".toText(),
if (appointmentListModel.appointmentStatusEnum == AppointmentStatusEnum.workStarted && appointmentListModel.appointmentPaymentStatusEnum == AppointmentPaymentStatusEnum.payLater)
showPayNowButton(context, appointmentsVM).paddingAll(21),
if (appointmentListModel.appointmentStatusEnum == AppointmentStatusEnum.workStarted && appointmentListModel.appointmentPaymentStatusEnum == AppointmentPaymentStatusEnum.payPartial)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(child: showPayNowButton(context, appointmentsVM)),
12.width,
Expanded(child: showPayLaterButton(context, appointmentsVM)),
],
).paddingAll(21),
if (appointmentListModel.appointmentStatusEnum == AppointmentStatusEnum.visitCompleted) ...[
Utils.buildStatusContainer(LocaleKeys.appointmentisCompleted.tr()).paddingAll(10),
],
if (appointmentListModel.appointmentStatusEnum == AppointmentStatusEnum.workStarted && appointmentListModel.appointmentPaymentStatusEnum == AppointmentPaymentStatusEnum.paid)
Column(
children: [
showCompleteButton(context, appointmentsVM),
],
).paddingAll(21),
//TODO: THIS NEEDS TO BE CHECKED. IMPORTANT
// Padding(
// padding: const EdgeInsets.all(21.0),
// child: Column(
// children: [
// ShowFillButton(
// title: "Confirm Arrive",
// maxWidth: double.infinity,
// onPressed: () {
// showMyBottomSheet(
// context,
// child: ShowCollectPaymentSheet(
// onClickYes: () {},
// onClickNo: () {},
// ),
// );
// },
// ),
// 12.height,
// ShowFillButton(
// title: "Cancel Appointment",
// maxWidth: double.infinity,
// isFilled: false,
// txtColor: MyColors.redColor,
// borderColor: MyColors.redColor,
// onPressed: () {
// showMyBottomSheet(context,
// child: CancelAppointmentReasonSheet(
// onCancelClick: (String reason) {},
// ));
// },
// ),
// ],
// ),
// ),
if (appointmentListModel.appointmentStatusEnum == AppointmentStatusEnum.workStarted && appointmentListModel.appointmentPaymentStatusEnum == AppointmentPaymentStatusEnum.payLater)
showPayNowButton(context, appointmentsVM).paddingAll(21),
if (appointmentListModel.appointmentStatusEnum == AppointmentStatusEnum.visitCompleted) ...[
Utils.buildStatusContainer(LocaleKeys.appointmentisCompleted.tr()).paddingAll(10),
],
],
],
),
);
@ -207,6 +197,139 @@ class _UpdateAppointmentPageState extends State<UpdateAppointmentPage> {
);
}
List<Widget> _buildPendingCards() {
return pendingNewServices.asMap().entries.map((entry) {
final index = entry.key;
final selected = entry.value;
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
selected.service.value,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
),
MyAssets.icEdit.buildSvg(height: 17, width: 17).onPress(
isSubmittingNewService ? () {} : () => _editPendingCard(index),
),
const SizedBox(width: 10),
MyAssets.closeWithOrangeBg.buildSvg(height: 30, width: 30).onPress(
isSubmittingNewService
? () {}
: () {
setState(() => pendingNewServices.removeAt(index));
},
),
],
),
Text(selected.category.value, style: const TextStyle(color: Colors.grey)),
8.height,
...selected.items.map(
(item) => Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Row(
children: [
Expanded(child: Text(item.name ?? '')),
Text('${item.price ?? '0'} SAR'),
],
),
),
),
const Divider(),
Text(
'${selected.totalPrice.toStringAsFixed(2)} SAR',
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
),
],
),
),
);
}).toList();
}
List<int> get _allPendingItemIds => pendingNewServices.expand((selected) => selected.itemIds).where((id) => id > 0).toSet().toList();
// The sheet returns either:
// - a SelectedAppointmentServiceModel (ready-made pick -- stage it locally), or
// - `true` (a custom service was already submitted by the sheet itself -- refresh), or
// - null (dismissed without choosing anything -- do nothing).
Future<void> _openAddServiceSheet(BuildContext context, {int? editIndex}) async {
final existing = editIndex == null ? null : pendingNewServices[editIndex];
final result = await showModalBottomSheet<dynamic>(
context: context,
isScrollControlled: true,
enableDrag: true,
builder: (_) => AddAppointmentServiceBottomSheet(appointmentListModel: appointmentListModel, initialService: existing),
);
if (!mounted || result == null) return;
if (result is SelectedAppointmentServiceModel) {
setState(() {
if (editIndex == null) {
pendingNewServices.add(result);
} else {
pendingNewServices[editIndex] = result;
}
});
} else if (result == true) {
await _updateAppointment(context, appointmentListModel.branchId ?? 0);
}
}
void _editPendingCard(int index) {
_openAddServiceSheet(context, editIndex: index);
}
void _cancelPendingNewServices() {
setState(() => pendingNewServices.clear());
}
Future<void> _submitPendingNewServices() async {
if (isSubmittingNewService) return;
final ids = _allPendingItemIds;
if (ids.isEmpty) {
Utils.showToast(LocaleKeys.pleaseSelectItems.tr());
return;
}
final postParams = {
'providerBranchID': appointmentListModel.branchId,
'appointmentID': appointmentListModel.id,
'serviceItemID': ids,
};
setState(() => isSubmittingNewService = true);
bool loadingVisible = false;
try {
Utils.showLoading(context);
loadingVisible = true;
final GenericRespModel res = await context.read<ServiceVM>().addNewServiceInAppointment(postParams);
if (!mounted) return;
Utils.hideLoading(context);
loadingVisible = false;
if (res.messageStatus == 1) {
await _updateAppointment(context, appointmentListModel.branchId ?? 0);
if (!mounted) return;
Utils.showToast(LocaleKeys.itemsAddedSuccessfully.tr(), isSuccess: true);
setState(() => pendingNewServices.clear());
} else {
Utils.showToast(res.message.toString());
}
} catch (e) {
if (mounted) Utils.showToast(e.toString());
} finally {
if (mounted && loadingVisible) Utils.hideLoading(context);
if (mounted) setState(() => isSubmittingNewService = false);
}
}
Widget showWorkStartButton(BuildContext context, AppointmentsVM appointmentsVM) {
return ShowFillButton(
title: LocaleKeys.workStart.tr(),
@ -220,15 +343,6 @@ class _UpdateAppointmentPageState extends State<UpdateAppointmentPage> {
}
Utils.hideLoading(context);
// pop(context);
// showMyBottomSheet(
// context,
// child: ShowCollectPaymentSheet(
// onClickYes: () {},
// onClickNo: () {},
// ),
// );
},
);
}

@ -1,54 +1,88 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:mc_common_app/classes/consts.dart';
import 'package:mc_common_app/extensions/int_extensions.dart';
import 'package:mc_common_app/extensions/string_extensions.dart';
import 'package:mc_common_app/generated/locale_keys.g.dart';
import 'package:mc_common_app/models/appointments_models/appointment_list_model.dart';
import 'package:mc_common_app/models/general_models/generic_resp_model.dart';
import 'package:mc_common_app/models/services_models/item_model.dart';
import 'package:mc_common_app/theme/colors.dart';
import 'package:mc_common_app/utils/utils.dart';
import 'package:mc_common_app/view_models/service_view_model.dart';
import 'package:mc_common_app/widgets/button/show_fill_button.dart';
import 'package:mc_common_app/widgets/dropdown/dropdow_field.dart';
import 'package:mc_common_app/widgets/bottom_sheet.dart';
import 'package:mc_common_app/widgets/txt_field.dart';
import 'package:car_provider_app/views/appoinments/widget/select_items_sheet.dart';
import 'package:car_provider_app/views/branch_management/schedule/widgets/chips_picker_item.dart';
import 'package:mc_common_app/models/appointments_models/selected_appointment_service_model.dart';
import 'package:car_provider_app/views/branch_management/schedule/widgets/chips_picker_item.dart';
// Two different outcomes on pop, by design:
// - Ready-made mode: "Add" just validates the current category/service/items pick and
// pops with a SelectedAppointmentServiceModel -- nothing is submitted here. The caller
// (update_appointment_page.dart) stages it locally as a card; the sheet gets reopened
// once per service the provider wants to add.
// - Custom-service mode (toggle under the header): "Add" submits that one custom
// service via the API immediately and pops with `true` once it succeeds -- there's no
// local staging for this path, the caller just needs to know to refresh.
class AddAppointmentServiceBottomSheet extends StatefulWidget {
final int branchId;
final AppointmentListModel appointmentListModel;
final SelectedAppointmentServiceModel? initialService;
const AddAppointmentServiceBottomSheet({
super.key,
required this.branchId,
required this.appointmentListModel,
this.initialService,
});
@override
State<AddAppointmentServiceBottomSheet> createState() =>
_AddAppointmentServiceBottomSheetState();
State<AddAppointmentServiceBottomSheet> createState() => _AddAppointmentServiceBottomSheetState();
}
class _AddAppointmentServiceBottomSheetState
extends State<AddAppointmentServiceBottomSheet> {
class _AddAppointmentServiceBottomSheetState extends State<AddAppointmentServiceBottomSheet> {
DropValue? category;
DropValue? service;
List<ItemData> selectedItems = [];
bool isLoadingServices = false;
String? loadError;
DropValue? selectedCurrency;
final List<DropValue> currencies = [
DropValue(1, 'SAR', 'SAR'),
DropValue(2, 'AED', 'AED'),
DropValue(3, 'USD', 'USD'),
];
bool isSubmitting = false;
// Cached here (rather than calling context.read<ServiceVM>() directly inside
// dispose()) because by the time dispose() runs the element is deactivated, and
// looking up an ancestor Provider at that point throws "Looking up a deactivated
// widget's ancestor is unsafe". didChangeDependencies() is the framework-recommended
// place to grab a reference that needs to survive into dispose().
late ServiceVM _serviceVM;
@override
void initState() {
super.initState();
category = widget.initialService?.category;
service = widget.initialService?.service;
selectedItems = List.of(widget.initialService?.items ?? []);
if (category != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) _loadServices(category!.id);
});
}
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
context.read<ServiceVM>().fetchBranchCategory(
EasyLocalization.of(context)?.currentLocale?.countryCode ?? 'SA',
);
if (category != null) _loadServices(category!.id);
});
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_serviceVM = context.read<ServiceVM>();
}
Future<void> _loadServices(int categoryId) async {
@ -58,9 +92,9 @@ class _AddAppointmentServiceBottomSheetState
});
try {
await context.read<ServiceVM>().fetchProviderServices(
widget.branchId.toString(),
categoryId.toString(),
);
(widget.appointmentListModel.branchId ?? 0).toString(),
categoryId.toString(),
);
} catch (e) {
if (mounted) loadError = e.toString();
} finally {
@ -71,11 +105,9 @@ class _AddAppointmentServiceBottomSheetState
Future<void> _openItemsSheet() async {
if (service == null) return;
final serviceId = service!.id;
final pickedItems = selectedItems
.map((item) => PickerItem(id: item.id ?? 0, title: item.name ?? ''))
.toList();
final pickedItems = selectedItems.map((item) => PickerItem(id: item.id ?? 0, title: item.name ?? '')).toList();
showMyBottomSheet(
showMyBottomSheet(
context,
child: SelectItemsSheet(
serviceId: serviceId,
@ -88,22 +120,68 @@ class _AddAppointmentServiceBottomSheetState
);
}
void _confirm() {
void _confirmReadyMade() {
if (category == null || service == null || selectedItems.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(LocaleKeys.pleaseSelectItems.tr())),
);
Utils.showToast(LocaleKeys.pleaseSelectItems.tr());
return;
}
Navigator.of(context).pop(
SelectedAppointmentServiceModel(
category: category!,
service: service!,
items: List.of(selectedItems),
),
SelectedAppointmentServiceModel(category: category!, service: service!, items: List.of(selectedItems)),
);
}
Future<void> _submitCustomService() async {
if (isSubmitting) return;
final vm = context.read<ServiceVM>();
if (!vm.isCustomServiceDetailValidated()) return;
final postParams = {
'providerBranchID': widget.appointmentListModel.branchId,
'appointmentID': widget.appointmentListModel.id,
'serviceItemsFreeText': [
{
'name': vm.customServiceTitle,
'price': vm.customServicePrice,
'tax': vm.customerServiceTax,
}
],
};
setState(() => isSubmitting = true);
bool loadingVisible = false;
try {
Utils.showLoading(context);
loadingVisible = true;
final GenericRespModel res = await vm.addNewServiceInAppointment(postParams);
if (!mounted) return;
Utils.hideLoading(context);
loadingVisible = false;
if (res.messageStatus == 1) {
vm.resetCustomServiceForm();
Utils.showToast(LocaleKeys.itemsAddedSuccessfully.tr(), isSuccess: true);
Navigator.of(context).pop(true);
} else {
Utils.showToast(res.message.toString());
}
} catch (e) {
if (mounted) Utils.showToast(e.toString());
} finally {
if (mounted && loadingVisible) Utils.hideLoading(context);
if (mounted) setState(() => isSubmitting = false);
}
}
@override
void dispose() {
// Leaving the sheet without submitting shouldn't leave custom-mode state dangling
// for the next time it's opened (same instance-reuse risk as the long-lived VMs
// elsewhere in this app). Uses the cached _serviceVM, not context.read -- see
// didChangeDependencies() above.
_serviceVM.resetCustomServiceForm();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SizedBox(
@ -112,113 +190,192 @@ class _AddAppointmentServiceBottomSheetState
top: false,
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Container(
width: 60,
height: 6,
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(20),
child: Consumer<ServiceVM>(
builder: (context, serviceVm, _) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Container(
width: 60,
height: 6,
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(20),
),
),
),
),
),
const SizedBox(height: 20),
Text(
LocaleKeys.selectServiceDetails.tr(),
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
),
const SizedBox(height: 20),
Expanded(
child: SingleChildScrollView(
child: Consumer<ServiceVM>(
builder: (context, serviceVm, _) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DropdownField(
(value) {
setState(() {
category = value;
service = null;
selectedItems.clear();
});
_loadServices(value.id);
},
dropdownValue: category,
list: serviceVm.categoryDropList,
hint: LocaleKeys.selectServiceCategory.tr(),
isSelectAble: !isLoadingServices,
),
if (category != null) ...[
const SizedBox(height: 16),
if (isLoadingServices)
const Center(child: CircularProgressIndicator())
else if (loadError != null)
TextButton(
onPressed: () => _loadServices(category!.id),
child: const Text('Retry loading services'),
)
else
DropdownField(
(value) {
setState(() {
service = value;
selectedItems.clear();
});
},
dropdownValue: service,
list: serviceVm.servicesDropList,
hint: LocaleKeys.defineServices.tr(),
),
],
if (service != null) ...[
const SizedBox(height: 16),
ChipsPickerItem(
isNeedLabelOnTop: true,
hint: LocaleKeys.selectItems.tr(),
itemsList: selectedItems.map((item) {
return PickerItem(
id: item.id ?? 0,
title: item.name ?? "",
);
}).toList(),
onClick: () {
_openItemsSheet();
const SizedBox(height: 20),
Text(
LocaleKeys.selectServiceDetails.tr(),
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: LocaleKeys.lookingToAddCustomService.tr().toText(fontSize: 14, color: MyColors.textColor),
),
8.width,
LocaleKeys.customService.tr().toText(fontSize: 14, isBold: true),
Switch(
value: serviceVm.customServiceEnabledStatus,
activeColor: MyColors.darkPrimaryColor,
onChanged: isSubmitting
? null
: (value) {
serviceVm.updateCustomServiceEnabledStatus(value);
},
),
...selectedItems.map(
(item) => ListTile(
contentPadding: EdgeInsets.zero,
title: Text(item.name ?? ''),
trailing: Text('${item.price ?? '0'} SAR'),
),
),
],
],
);
},
),
],
),
),
),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: isLoadingServices ? null : _confirm,
style: ElevatedButton.styleFrom(
backgroundColor: MyColors.darkPrimaryColor,
foregroundColor: Colors.white,
minimumSize: const Size.fromHeight(50),
const SizedBox(height: 12),
Expanded(
child: SingleChildScrollView(
child: serviceVm.customServiceEnabledStatus ? _buildCustomServiceForm(serviceVm) : _buildReadyMadeSection(serviceVm),
),
),
child: Text(LocaleKeys.next.tr()),
),
SizedBox(
width: double.infinity,
child: ShowFillButton(
title: LocaleKeys.add.tr(),
maxWidth: double.infinity,
onPressed: isSubmitting
? null
: (serviceVm.customServiceEnabledStatus ? _submitCustomService : _confirmReadyMade),
),
),
],
);
},
),
),
),
);
}
Widget _buildCustomServiceForm(ServiceVM serviceVm) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
LocaleKeys.addServiceDetails.tr().toText(fontSize: 16),
12.height,
TxtField(
value: serviceVm.customServiceTitle,
errorValue: serviceVm.customServiceTitleError,
hint: LocaleKeys.serviceName.tr(),
onChanged: (v) => serviceVm.updateCustomServiceTitle(v),
),
8.height,
Row(
children: [
Expanded(
flex: 2,
child: TxtField(
value: serviceVm.customServicePrice,
errorValue: serviceVm.customServicePriceError,
keyboardType: TextInputType.number,
numbersOnly: true,
hint: LocaleKeys.price.tr(),
onChanged: (v) => serviceVm.updateCustomServicePrice(v),
),
),
8.width,
Expanded(
flex: 1,
child: DropdownField(
(DropValue value) {
setState(() => selectedCurrency = value);
},
dropdownValue: selectedCurrency,
list: currencies,
hint: 'Currency',
),
),
],
),
8.height,
TxtField(
postfixWidget: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
'%'.toText(fontWeight: MyFonts.Medium, fontSize: 15, color: borderColor, textAlign: TextAlign.center),
],
),
isNeedClickAll: true,
isBackgroundEnabled: true,
hint: LocaleKeys.tax.tr(),
onTap: () {},
value: serviceVm.customerServiceTax,
errorValue: '',
keyboardType: TextInputType.number,
numbersOnly: true,
onChanged: (v) => serviceVm.updateCustomerServiceTax(v),
),
),
],
);
}
Widget _buildReadyMadeSection(ServiceVM serviceVm) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DropdownField(
(value) {
setState(() {
category = value;
service = null;
selectedItems.clear();
});
_loadServices(value.id);
},
dropdownValue: category,
list: serviceVm.categoryDropList,
hint: LocaleKeys.selectServiceCategory.tr(),
isSelectAble: !isLoadingServices,
),
if (category != null) ...[
const SizedBox(height: 16),
if (isLoadingServices)
const Center(child: CircularProgressIndicator())
else if (loadError != null)
TextButton(
onPressed: () => _loadServices(category!.id),
child: const Text('Retry loading services'),
)
else
DropdownField(
(value) {
setState(() {
service = value;
selectedItems.clear();
});
},
dropdownValue: service,
list: serviceVm.servicesDropList,
hint: LocaleKeys.defineServices.tr(),
),
],
if (service != null) ...[
const SizedBox(height: 16),
ChipsPickerItem(
isNeedLabelOnTop: true,
hint: LocaleKeys.selectItems.tr(),
itemsList: selectedItems.map((item) => PickerItem(id: item.id ?? 0, title: item.name ?? "")).toList(),
onClick: () {
_openItemsSheet();
},
),
...selectedItems.map(
(item) => ListTile(
contentPadding: EdgeInsets.zero,
title: Text(item.name ?? ''),
trailing: Text('${item.price ?? '0'} SAR'),
),
),
],
],
);
}
}

Loading…
Cancel
Save