From b2cfd93b8f13b6da71de0b0e1504fe2d5c3415e6 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Sat, 21 Feb 2026 19:36:23 -0500 Subject: [PATCH] refactor: remove recurring order widgets and related functionality - Deleted RecurringOrderDatePicker, RecurringOrderEventNameInput, RecurringOrderHeader, RecurringOrderPositionCard, RecurringOrderSectionHeader, RecurringOrderSuccessView, and RecurringOrderView. - Removed associated imports and references in the codebase. - Updated pubspec.yaml to include client_orders_common dependency. - Cleaned up the RapidOrderActions widget by removing debug print statement. --- .../pages/one_time_order_page.dart | 108 ++++- .../pages/permanent_order_page.dart | 134 ++++- .../pages/recurring_order_page.dart | 142 +++++- .../one_time_order_date_picker.dart | 74 --- .../one_time_order_event_name_input.dart | 56 --- .../one_time_order/one_time_order_header.dart | 71 --- .../one_time_order_location_input.dart | 62 --- .../one_time_order_position_card.dart | 349 ------------- .../one_time_order_section_header.dart | 52 -- .../one_time_order_success_view.dart | 107 ---- .../one_time_order/one_time_order_view.dart | 328 ------------- .../permanent_order_date_picker.dart | 74 --- .../permanent_order_event_name_input.dart | 56 --- .../permanent_order_header.dart | 71 --- .../permanent_order_position_card.dart | 345 ------------- .../permanent_order_section_header.dart | 52 -- .../permanent_order_success_view.dart | 104 ---- .../permanent_order/permanent_order_view.dart | 440 ----------------- .../widgets/rapid_order/rapid_order_view.dart | 1 - .../recurring_order_date_picker.dart | 74 --- .../recurring_order_event_name_input.dart | 56 --- .../recurring_order_header.dart | 71 --- .../recurring_order_position_card.dart | 345 ------------- .../recurring_order_section_header.dart | 52 -- .../recurring_order_success_view.dart | 104 ---- .../recurring_order/recurring_order_view.dart | 457 ------------------ .../client/orders/create_order/pubspec.yaml | 2 + 27 files changed, 377 insertions(+), 3410 deletions(-) delete mode 100644 apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_date_picker.dart delete mode 100644 apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_event_name_input.dart delete mode 100644 apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_header.dart delete mode 100644 apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_location_input.dart delete mode 100644 apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_position_card.dart delete mode 100644 apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_section_header.dart delete mode 100644 apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_success_view.dart delete mode 100644 apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_view.dart delete mode 100644 apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/permanent_order/permanent_order_date_picker.dart delete mode 100644 apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/permanent_order/permanent_order_event_name_input.dart delete mode 100644 apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/permanent_order/permanent_order_header.dart delete mode 100644 apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/permanent_order/permanent_order_position_card.dart delete mode 100644 apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/permanent_order/permanent_order_section_header.dart delete mode 100644 apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/permanent_order/permanent_order_success_view.dart delete mode 100644 apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/permanent_order/permanent_order_view.dart delete mode 100644 apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/recurring_order/recurring_order_date_picker.dart delete mode 100644 apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/recurring_order/recurring_order_event_name_input.dart delete mode 100644 apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/recurring_order/recurring_order_header.dart delete mode 100644 apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/recurring_order/recurring_order_position_card.dart delete mode 100644 apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/recurring_order/recurring_order_section_header.dart delete mode 100644 apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/recurring_order/recurring_order_success_view.dart delete mode 100644 apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/recurring_order/recurring_order_view.dart diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/pages/one_time_order_page.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/pages/one_time_order_page.dart index 32d381a5..1a42dad4 100644 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/pages/one_time_order_page.dart +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/pages/one_time_order_page.dart @@ -1,15 +1,19 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_modular/flutter_modular.dart'; +import 'package:client_orders_common/client_orders_common.dart'; +import 'package:krow_core/core.dart'; +import 'package:krow_domain/krow_domain.dart'; import '../blocs/one_time_order/one_time_order_bloc.dart'; -import '../widgets/one_time_order/one_time_order_view.dart'; +import '../blocs/one_time_order/one_time_order_event.dart'; +import '../blocs/one_time_order/one_time_order_state.dart'; /// Page for creating a one-time staffing order. /// Users can specify the date, location, and multiple staff positions required. /// -/// This page initializes the [OneTimeOrderBloc] and displays the [OneTimeOrderView]. -/// It follows the Krow Clean Architecture by being a [StatelessWidget] and -/// delegating its state and UI to other components. +/// This page initializes the [OneTimeOrderBloc] and displays the [OneTimeOrderView] +/// from the common orders package. It follows the Krow Clean Architecture by being +/// a [StatelessWidget] and mapping local BLoC state to generic UI models. class OneTimeOrderPage extends StatelessWidget { /// Creates a [OneTimeOrderPage]. const OneTimeOrderPage({super.key}); @@ -18,7 +22,101 @@ class OneTimeOrderPage extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (BuildContext context) => Modular.get(), - child: const OneTimeOrderView(), + child: BlocBuilder( + builder: (BuildContext context, OneTimeOrderState state) { + final OneTimeOrderBloc bloc = BlocProvider.of(context); + + return OneTimeOrderView( + status: _mapStatus(state.status), + errorMessage: state.errorMessage, + eventName: state.eventName, + selectedVendor: state.selectedVendor, + vendors: state.vendors, + date: state.date, + selectedHub: state.selectedHub != null ? _mapHub(state.selectedHub!) : null, + hubs: state.hubs.map(_mapHub).toList(), + positions: state.positions.map(_mapPosition).toList(), + roles: state.roles.map(_mapRole).toList(), + isValid: state.isValid, + onEventNameChanged: (String val) => bloc.add(OneTimeOrderEventNameChanged(val)), + onVendorChanged: (Vendor val) => bloc.add(OneTimeOrderVendorChanged(val)), + onDateChanged: (DateTime val) => bloc.add(OneTimeOrderDateChanged(val)), + onHubChanged: (OrderHubUiModel val) { + final OneTimeOrderHubOption originalHub = state.hubs.firstWhere((OneTimeOrderHubOption h) => h.id == val.id); + bloc.add(OneTimeOrderHubChanged(originalHub)); + }, + onPositionAdded: () => bloc.add(const OneTimeOrderPositionAdded()), + onPositionUpdated: (int index, OrderPositionUiModel val) { + final OneTimeOrderPosition original = state.positions[index]; + final OneTimeOrderPosition updated = original.copyWith( + role: val.role, + count: val.count, + startTime: val.startTime, + endTime: val.endTime, + lunchBreak: val.lunchBreak, + ); + bloc.add(OneTimeOrderPositionUpdated(index, updated)); + }, + onPositionRemoved: (int index) => bloc.add(OneTimeOrderPositionRemoved(index)), + onSubmit: () => bloc.add(const OneTimeOrderSubmitted()), + onDone: () => Modular.to.pushNamedAndRemoveUntil( + ClientPaths.orders, + (_) => false, + arguments: { + 'initialDate': state.date.toIso8601String(), + }, + ), + onBack: () => Modular.to.navigate(ClientPaths.createOrder), + ); + }, + ), + ); + } + + OrderFormStatus _mapStatus(OneTimeOrderStatus status) { + switch (status) { + case OneTimeOrderStatus.initial: + return OrderFormStatus.initial; + case OneTimeOrderStatus.loading: + return OrderFormStatus.loading; + case OneTimeOrderStatus.success: + return OrderFormStatus.success; + case OneTimeOrderStatus.failure: + return OrderFormStatus.failure; + } + } + + OrderHubUiModel _mapHub(OneTimeOrderHubOption hub) { + return OrderHubUiModel( + id: hub.id, + name: hub.name, + address: hub.address, + placeId: hub.placeId, + latitude: hub.latitude, + longitude: hub.longitude, + city: hub.city, + state: hub.state, + street: hub.street, + country: hub.country, + zipCode: hub.zipCode, + ); + } + + OrderRoleUiModel _mapRole(OneTimeOrderRoleOption role) { + return OrderRoleUiModel( + id: role.id, + name: role.name, + costPerHour: role.costPerHour, + ); + } + + OrderPositionUiModel _mapPosition(OneTimeOrderPosition pos) { + return OrderPositionUiModel( + role: pos.role, + count: pos.count, + startTime: pos.startTime, + endTime: pos.endTime, + lunchBreak: pos.lunchBreak, ); } } diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/pages/permanent_order_page.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/pages/permanent_order_page.dart index 3a16fc93..75a4d8ea 100644 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/pages/permanent_order_page.dart +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/pages/permanent_order_page.dart @@ -1,8 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_modular/flutter_modular.dart'; +import 'package:client_orders_common/client_orders_common.dart'; +import 'package:krow_core/core.dart'; +import 'package:krow_domain/krow_domain.dart' hide PermanentOrderPosition; import '../blocs/permanent_order/permanent_order_bloc.dart'; -import '../widgets/permanent_order/permanent_order_view.dart'; +import '../blocs/permanent_order/permanent_order_event.dart'; +import '../blocs/permanent_order/permanent_order_state.dart'; /// Page for creating a permanent staffing order. class PermanentOrderPage extends StatelessWidget { @@ -13,7 +17,133 @@ class PermanentOrderPage extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (BuildContext context) => Modular.get(), - child: const PermanentOrderView(), + child: BlocBuilder( + builder: (BuildContext context, PermanentOrderState state) { + final PermanentOrderBloc bloc = BlocProvider.of(context); + + return PermanentOrderView( + status: _mapStatus(state.status), + errorMessage: state.errorMessage, + eventName: state.eventName, + selectedVendor: state.selectedVendor, + vendors: state.vendors, + startDate: state.startDate, + permanentDays: state.permanentDays, + selectedHub: state.selectedHub != null ? _mapHub(state.selectedHub!) : null, + hubs: state.hubs.map(_mapHub).toList(), + positions: state.positions.map(_mapPosition).toList(), + roles: state.roles.map(_mapRole).toList(), + isValid: state.isValid, + onEventNameChanged: (String val) => bloc.add(PermanentOrderEventNameChanged(val)), + onVendorChanged: (Vendor val) => bloc.add(PermanentOrderVendorChanged(val)), + onStartDateChanged: (DateTime val) => bloc.add(PermanentOrderStartDateChanged(val)), + onDayToggled: (int index) => bloc.add(PermanentOrderDayToggled(index)), + onHubChanged: (OrderHubUiModel val) { + final PermanentOrderHubOption originalHub = state.hubs.firstWhere((PermanentOrderHubOption h) => h.id == val.id); + bloc.add(PermanentOrderHubChanged(originalHub)); + }, + onPositionAdded: () => bloc.add(const PermanentOrderPositionAdded()), + onPositionUpdated: (int index, OrderPositionUiModel val) { + final PermanentOrderPosition original = state.positions[index]; + final PermanentOrderPosition updated = original.copyWith( + role: val.role, + count: val.count, + startTime: val.startTime, + endTime: val.endTime, + lunchBreak: val.lunchBreak, + ); + bloc.add(PermanentOrderPositionUpdated(index, updated)); + }, + onPositionRemoved: (int index) => bloc.add(PermanentOrderPositionRemoved(index)), + onSubmit: () => bloc.add(const PermanentOrderSubmitted()), + onDone: () { + final DateTime initialDate = _firstPermanentShiftDate( + state.startDate, + state.permanentDays, + ); + Modular.to.pushNamedAndRemoveUntil( + ClientPaths.orders, + (_) => false, + arguments: { + 'initialDate': initialDate.toIso8601String(), + }, + ); + }, + onBack: () => Modular.to.navigate(ClientPaths.createOrder), + ); + }, + ), + ); + } + + DateTime _firstPermanentShiftDate( + DateTime startDate, + List permanentDays, + ) { + final DateTime start = DateTime(startDate.year, startDate.month, startDate.day); + final DateTime end = start.add(const Duration(days: 29)); + final Set selected = permanentDays.toSet(); + for (DateTime day = start; !day.isAfter(end); day = day.add(const Duration(days: 1))) { + if (selected.contains(_weekdayLabel(day))) { + return day; + } + } + return start; + } + + String _weekdayLabel(DateTime date) { + switch (date.weekday) { + case DateTime.monday: return 'MON'; + case DateTime.tuesday: return 'TUE'; + case DateTime.wednesday: return 'WED'; + case DateTime.thursday: return 'THU'; + case DateTime.friday: return 'FRI'; + case DateTime.saturday: return 'SAT'; + case DateTime.sunday: return 'SUN'; + default: return 'SUN'; + } + } + + OrderFormStatus _mapStatus(PermanentOrderStatus status) { + switch (status) { + case PermanentOrderStatus.initial: return OrderFormStatus.initial; + case PermanentOrderStatus.loading: return OrderFormStatus.loading; + case PermanentOrderStatus.success: return OrderFormStatus.success; + case PermanentOrderStatus.failure: return OrderFormStatus.failure; + } + } + + OrderHubUiModel _mapHub(PermanentOrderHubOption hub) { + return OrderHubUiModel( + id: hub.id, + name: hub.name, + address: hub.address, + placeId: hub.placeId, + latitude: hub.latitude, + longitude: hub.longitude, + city: hub.city, + state: hub.state, + street: hub.street, + country: hub.country, + zipCode: hub.zipCode, + ); + } + + OrderRoleUiModel _mapRole(PermanentOrderRoleOption role) { + return OrderRoleUiModel( + id: role.id, + name: role.name, + costPerHour: role.costPerHour, + ); + } + + OrderPositionUiModel _mapPosition(PermanentOrderPosition pos) { + return OrderPositionUiModel( + role: pos.role, + count: pos.count, + startTime: pos.startTime, + endTime: pos.endTime, + lunchBreak: pos.lunchBreak ?? 'NO_BREAK', ); } } diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/pages/recurring_order_page.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/pages/recurring_order_page.dart index 728f0ce3..32b74f72 100644 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/pages/recurring_order_page.dart +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/pages/recurring_order_page.dart @@ -1,8 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_modular/flutter_modular.dart'; +import 'package:client_orders_common/client_orders_common.dart'; +import 'package:krow_core/core.dart'; +import 'package:krow_domain/krow_domain.dart' hide RecurringOrderPosition; import '../blocs/recurring_order/recurring_order_bloc.dart'; -import '../widgets/recurring_order/recurring_order_view.dart'; +import '../blocs/recurring_order/recurring_order_event.dart'; +import '../blocs/recurring_order/recurring_order_state.dart'; /// Page for creating a recurring staffing order. class RecurringOrderPage extends StatelessWidget { @@ -13,7 +17,141 @@ class RecurringOrderPage extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (BuildContext context) => Modular.get(), - child: const RecurringOrderView(), + child: BlocBuilder( + builder: (BuildContext context, RecurringOrderState state) { + final RecurringOrderBloc bloc = BlocProvider.of(context); + + return RecurringOrderView( + status: _mapStatus(state.status), + errorMessage: state.errorMessage, + eventName: state.eventName, + selectedVendor: state.selectedVendor, + vendors: state.vendors, + startDate: state.startDate, + endDate: state.endDate, + recurringDays: state.recurringDays, + selectedHub: state.selectedHub != null ? _mapHub(state.selectedHub!) : null, + hubs: state.hubs.map(_mapHub).toList(), + positions: state.positions.map(_mapPosition).toList(), + roles: state.roles.map(_mapRole).toList(), + isValid: state.isValid, + onEventNameChanged: (String val) => bloc.add(RecurringOrderEventNameChanged(val)), + onVendorChanged: (Vendor val) => bloc.add(RecurringOrderVendorChanged(val)), + onStartDateChanged: (DateTime val) => bloc.add(RecurringOrderStartDateChanged(val)), + onEndDateChanged: (DateTime val) => bloc.add(RecurringOrderEndDateChanged(val)), + onDayToggled: (int index) => bloc.add(RecurringOrderDayToggled(index)), + onHubChanged: (OrderHubUiModel val) { + final RecurringOrderHubOption originalHub = state.hubs.firstWhere((RecurringOrderHubOption h) => h.id == val.id); + bloc.add(RecurringOrderHubChanged(originalHub)); + }, + onPositionAdded: () => bloc.add(const RecurringOrderPositionAdded()), + onPositionUpdated: (int index, OrderPositionUiModel val) { + final RecurringOrderPosition original = state.positions[index]; + final RecurringOrderPosition updated = original.copyWith( + role: val.role, + count: val.count, + startTime: val.startTime, + endTime: val.endTime, + lunchBreak: val.lunchBreak, + ); + bloc.add(RecurringOrderPositionUpdated(index, updated)); + }, + onPositionRemoved: (int index) => bloc.add(RecurringOrderPositionRemoved(index)), + onSubmit: () => bloc.add(const RecurringOrderSubmitted()), + onDone: () { + final DateTime maxEndDate = state.startDate.add(const Duration(days: 29)); + final DateTime effectiveEndDate = + state.endDate.isAfter(maxEndDate) ? maxEndDate : state.endDate; + final DateTime initialDate = _firstRecurringShiftDate( + state.startDate, + effectiveEndDate, + state.recurringDays, + ); + + Modular.to.pushNamedAndRemoveUntil( + ClientPaths.orders, + (_) => false, + arguments: { + 'initialDate': initialDate.toIso8601String(), + }, + ); + }, + onBack: () => Modular.to.navigate(ClientPaths.createOrder), + ); + }, + ), + ); + } + + DateTime _firstRecurringShiftDate( + DateTime startDate, + DateTime endDate, + List recurringDays, + ) { + final DateTime start = DateTime(startDate.year, startDate.month, startDate.day); + final DateTime end = DateTime(endDate.year, endDate.month, endDate.day); + final Set selected = recurringDays.toSet(); + for (DateTime day = start; !day.isAfter(end); day = day.add(const Duration(days: 1))) { + if (selected.contains(_weekdayLabel(day))) { + return day; + } + } + return start; + } + + String _weekdayLabel(DateTime date) { + switch (date.weekday) { + case DateTime.monday: return 'MON'; + case DateTime.tuesday: return 'TUE'; + case DateTime.wednesday: return 'WED'; + case DateTime.thursday: return 'THU'; + case DateTime.friday: return 'FRI'; + case DateTime.saturday: return 'SAT'; + case DateTime.sunday: return 'SUN'; + default: return 'SUN'; + } + } + + OrderFormStatus _mapStatus(RecurringOrderStatus status) { + switch (status) { + case RecurringOrderStatus.initial: return OrderFormStatus.initial; + case RecurringOrderStatus.loading: return OrderFormStatus.loading; + case RecurringOrderStatus.success: return OrderFormStatus.success; + case RecurringOrderStatus.failure: return OrderFormStatus.failure; + } + } + + OrderHubUiModel _mapHub(RecurringOrderHubOption hub) { + return OrderHubUiModel( + id: hub.id, + name: hub.name, + address: hub.address, + placeId: hub.placeId, + latitude: hub.latitude, + longitude: hub.longitude, + city: hub.city, + state: hub.state, + street: hub.street, + country: hub.country, + zipCode: hub.zipCode, + ); + } + + OrderRoleUiModel _mapRole(RecurringOrderRoleOption role) { + return OrderRoleUiModel( + id: role.id, + name: role.name, + costPerHour: role.costPerHour, + ); + } + + OrderPositionUiModel _mapPosition(RecurringOrderPosition pos) { + return OrderPositionUiModel( + role: pos.role, + count: pos.count, + startTime: pos.startTime, + endTime: pos.endTime, + lunchBreak: pos.lunchBreak ?? 'NO_BREAK', ); } } diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_date_picker.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_date_picker.dart deleted file mode 100644 index 5a0eb751..00000000 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_date_picker.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:design_system/design_system.dart'; -import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; - -/// A date picker field for the one-time order form. -/// Matches the prototype input field style. -class OneTimeOrderDatePicker extends StatefulWidget { - /// Creates a [OneTimeOrderDatePicker]. - const OneTimeOrderDatePicker({ - required this.label, - required this.value, - required this.onChanged, - super.key, - }); - - /// The label text to display above the field. - final String label; - - /// The currently selected date. - final DateTime value; - - /// Callback when a new date is selected. - final ValueChanged onChanged; - - @override - State createState() => _OneTimeOrderDatePickerState(); -} - -class _OneTimeOrderDatePickerState extends State { - late final TextEditingController _controller; - - @override - void initState() { - super.initState(); - _controller = TextEditingController( - text: DateFormat('yyyy-MM-dd').format(widget.value), - ); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - void didUpdateWidget(OneTimeOrderDatePicker oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.value != oldWidget.value) { - _controller.text = DateFormat('yyyy-MM-dd').format(widget.value); - } - } - - @override - Widget build(BuildContext context) { - return UiTextField( - label: widget.label, - controller: _controller, - readOnly: true, - prefixIcon: UiIcons.calendar, - onTap: () async { - final DateTime? picked = await showDatePicker( - context: context, - initialDate: widget.value, - firstDate: DateTime.now(), - lastDate: DateTime.now().add(const Duration(days: 365)), - ); - if (picked != null) { - widget.onChanged(picked); - } - }, - ); - } -} diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_event_name_input.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_event_name_input.dart deleted file mode 100644 index 2fe608d0..00000000 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_event_name_input.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'package:design_system/design_system.dart'; -import 'package:flutter/material.dart'; - -/// A text input for the order name in the one-time order form. -class OneTimeOrderEventNameInput extends StatefulWidget { - const OneTimeOrderEventNameInput({ - required this.label, - required this.value, - required this.onChanged, - super.key, - }); - - final String label; - final String value; - final ValueChanged onChanged; - - @override - State createState() => - _OneTimeOrderEventNameInputState(); -} - -class _OneTimeOrderEventNameInputState - extends State { - late final TextEditingController _controller; - - @override - void initState() { - super.initState(); - _controller = TextEditingController(text: widget.value); - } - - @override - void didUpdateWidget(OneTimeOrderEventNameInput oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.value != _controller.text) { - _controller.text = widget.value; - } - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return UiTextField( - label: widget.label, - controller: _controller, - onChanged: widget.onChanged, - hintText: 'Order name', - prefixIcon: UiIcons.briefcase, - ); - } -} diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_header.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_header.dart deleted file mode 100644 index d39f6c8b..00000000 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_header.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'package:design_system/design_system.dart'; -import 'package:flutter/material.dart'; - -/// A header widget for the one-time order flow with a colored background. -class OneTimeOrderHeader extends StatelessWidget { - /// Creates a [OneTimeOrderHeader]. - const OneTimeOrderHeader({ - required this.title, - required this.subtitle, - required this.onBack, - super.key, - }); - - /// The title of the page. - final String title; - - /// The subtitle or description. - final String subtitle; - - /// Callback when the back button is pressed. - final VoidCallback onBack; - - @override - Widget build(BuildContext context) { - return Container( - padding: EdgeInsets.only( - top: MediaQuery.of(context).padding.top + UiConstants.space5, - bottom: UiConstants.space5, - left: UiConstants.space5, - right: UiConstants.space5, - ), - color: UiColors.primary, - child: Row( - children: [ - GestureDetector( - onTap: onBack, - child: Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: UiColors.white.withValues(alpha: 0.2), - borderRadius: UiConstants.radiusMd, - ), - child: const Icon( - UiIcons.chevronLeft, - color: UiColors.white, - size: 24, - ), - ), - ), - const SizedBox(width: UiConstants.space3), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: UiTypography.headline3m.copyWith(color: UiColors.white), - ), - Text( - subtitle, - style: UiTypography.footnote2r.copyWith( - color: UiColors.white.withValues(alpha: 0.8), - ), - ), - ], - ), - ], - ), - ); - } -} diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_location_input.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_location_input.dart deleted file mode 100644 index 7eb8baf1..00000000 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_location_input.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'package:design_system/design_system.dart'; -import 'package:flutter/material.dart'; - -/// A location input field for the one-time order form. -/// Matches the prototype input field style. -class OneTimeOrderLocationInput extends StatefulWidget { - /// Creates a [OneTimeOrderLocationInput]. - const OneTimeOrderLocationInput({ - required this.label, - required this.value, - required this.onChanged, - super.key, - }); - - /// The label text to display above the field. - final String label; - - /// The current location value. - final String value; - - /// Callback when the location value changes. - final ValueChanged onChanged; - - @override - State createState() => - _OneTimeOrderLocationInputState(); -} - -class _OneTimeOrderLocationInputState extends State { - late final TextEditingController _controller; - - @override - void initState() { - super.initState(); - _controller = TextEditingController(text: widget.value); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - void didUpdateWidget(OneTimeOrderLocationInput oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.value != _controller.text) { - _controller.text = widget.value; - } - } - - @override - Widget build(BuildContext context) { - return UiTextField( - label: widget.label, - controller: _controller, - onChanged: widget.onChanged, - hintText: 'Enter address', - prefixIcon: UiIcons.mapPin, - ); - } -} diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_position_card.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_position_card.dart deleted file mode 100644 index 7794a356..00000000 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_position_card.dart +++ /dev/null @@ -1,349 +0,0 @@ -import 'package:core_localization/core_localization.dart'; -import 'package:design_system/design_system.dart'; -import 'package:flutter/material.dart'; -import 'package:krow_domain/krow_domain.dart'; -import '../../blocs/one_time_order/one_time_order_state.dart'; - -/// A card widget for editing a specific position in a one-time order. -/// Matches the prototype layout while using design system tokens. -class OneTimeOrderPositionCard extends StatelessWidget { - /// Creates a [OneTimeOrderPositionCard]. - const OneTimeOrderPositionCard({ - required this.index, - required this.position, - required this.isRemovable, - required this.onUpdated, - required this.onRemoved, - required this.positionLabel, - required this.roleLabel, - required this.workersLabel, - required this.startLabel, - required this.endLabel, - required this.lunchLabel, - required this.roles, - super.key, - }); - - /// The index of the position in the list. - final int index; - - /// The position entity data. - final OneTimeOrderPosition position; - - /// Whether this position can be removed (usually if there's more than one). - final bool isRemovable; - - /// Callback when the position data is updated. - final ValueChanged onUpdated; - - /// Callback when the position is removed. - final VoidCallback onRemoved; - - /// Label for positions (e.g., "Position"). - final String positionLabel; - - /// Label for the role selection. - final String roleLabel; - - /// Label for the worker count. - final String workersLabel; - - /// Label for the start time. - final String startLabel; - - /// Label for the end time. - final String endLabel; - - /// Label for the lunch break. - final String lunchLabel; - - /// Available roles for the selected vendor. - final List roles; - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.all(UiConstants.space4), - decoration: BoxDecoration( - color: UiColors.white, - borderRadius: UiConstants.radiusLg, - border: Border.all(color: UiColors.border), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '$positionLabel #${index + 1}', - style: UiTypography.footnote1m.textSecondary, - ), - if (isRemovable) - GestureDetector( - onTap: onRemoved, - child: Text( - t.client_create_order.one_time.remove, - style: UiTypography.footnote1m.copyWith( - color: UiColors.destructive, - ), - ), - ), - ], - ), - const SizedBox(height: UiConstants.space3), - - // Role (Dropdown) - Container( - padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3), - height: 44, - decoration: BoxDecoration( - borderRadius: UiConstants.radiusMd, - border: Border.all(color: UiColors.border), - ), - child: DropdownButtonHideUnderline( - child: DropdownButton( - isExpanded: true, - hint: Text( - roleLabel, - style: UiTypography.body2r.textPlaceholder, - ), - value: position.role.isEmpty ? null : position.role, - icon: const Icon( - UiIcons.chevronDown, - size: 18, - color: UiColors.iconSecondary, - ), - onChanged: (String? val) { - if (val != null) { - onUpdated(position.copyWith(role: val)); - } - }, - items: _buildRoleItems(), - ), - ), - ), - const SizedBox(height: UiConstants.space3), - - // Start/End/Workers Row - Row( - children: [ - // Start Time - Expanded( - child: _buildTimeInput( - context: context, - label: startLabel, - value: position.startTime, - onTap: () async { - final TimeOfDay? picked = await showTimePicker( - context: context, - initialTime: TimeOfDay.now(), - ); - if (picked != null && context.mounted) { - onUpdated( - position.copyWith(startTime: picked.format(context)), - ); - } - }, - ), - ), - const SizedBox(width: UiConstants.space2), - // End Time - Expanded( - child: _buildTimeInput( - context: context, - label: endLabel, - value: position.endTime, - onTap: () async { - final TimeOfDay? picked = await showTimePicker( - context: context, - initialTime: TimeOfDay.now(), - ); - if (picked != null && context.mounted) { - onUpdated( - position.copyWith(endTime: picked.format(context)), - ); - } - }, - ), - ), - const SizedBox(width: UiConstants.space2), - // Workers Count - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - workersLabel, - style: UiTypography.footnote2r.textSecondary, - ), - const SizedBox(height: UiConstants.space1), - Container( - height: 40, - decoration: BoxDecoration( - color: UiColors.bgSecondary, - borderRadius: UiConstants.radiusSm, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - GestureDetector( - onTap: () { - if (position.count > 1) { - onUpdated( - position.copyWith(count: position.count - 1), - ); - } - }, - child: const Icon(UiIcons.minus, size: 12), - ), - Text( - '${position.count}', - style: UiTypography.body2b.textPrimary, - ), - GestureDetector( - onTap: () { - onUpdated( - position.copyWith(count: position.count + 1), - ); - }, - child: const Icon(UiIcons.add, size: 12), - ), - ], - ), - ), - ], - ), - ), - ], - ), - const SizedBox(height: UiConstants.space4), - - // Lunch Break - Text(lunchLabel, style: UiTypography.footnote2r.textSecondary), - const SizedBox(height: UiConstants.space1), - Container( - padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3), - height: 44, - decoration: BoxDecoration( - borderRadius: UiConstants.radiusMd, - border: Border.all(color: UiColors.border), - ), - child: DropdownButtonHideUnderline( - child: DropdownButton( - isExpanded: true, - value: position.lunchBreak, - icon: const Icon( - UiIcons.chevronDown, - size: 18, - color: UiColors.iconSecondary, - ), - onChanged: (String? val) { - if (val != null) { - onUpdated(position.copyWith(lunchBreak: val)); - } - }, - items: [ - 'NO_BREAK', - 'MIN_10', - 'MIN_15', - 'MIN_30', - 'MIN_45', - 'MIN_60', - ].map(( - String value, - ) { - final String label = switch (value) { - 'NO_BREAK' => 'No Break', - 'MIN_10' => '10 min (Paid)', - 'MIN_15' => '15 min (Paid)', - 'MIN_30' => '30 min (Unpaid)', - 'MIN_45' => '45 min (Unpaid)', - 'MIN_60' => '60 min (Unpaid)', - _ => value, - }; - return DropdownMenuItem( - value: value, - child: Text( - label, - style: UiTypography.body2r.textPrimary, - ), - ); - }).toList(), - ), - ), - ), - ], - ), - ); - } - - Widget _buildTimeInput({ - required BuildContext context, - required String label, - required String value, - required VoidCallback onTap, - }) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(label, style: UiTypography.footnote2r.textSecondary), - const SizedBox(height: UiConstants.space1), - GestureDetector( - onTap: onTap, - child: Container( - height: 40, - padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3), - decoration: BoxDecoration( - borderRadius: UiConstants.radiusSm, - border: Border.all(color: UiColors.border), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - value.isEmpty ? '--:--' : value, - style: UiTypography.body2r.textPrimary, - ), - const Icon( - UiIcons.clock, - size: 14, - color: UiColors.iconSecondary, - ), - ], - ), - ), - ), - ], - ); - } - - List> _buildRoleItems() { - final List> items = roles - .map( - (OneTimeOrderRoleOption role) => DropdownMenuItem( - value: role.id, - child: Text( - '${role.name} - \$${role.costPerHour.toStringAsFixed(0)}', - style: UiTypography.body2r.textPrimary, - ), - ), - ) - .toList(); - - final bool hasSelected = roles.any((OneTimeOrderRoleOption role) => role.id == position.role); - if (position.role.isNotEmpty && !hasSelected) { - items.add( - DropdownMenuItem( - value: position.role, - child: Text( - position.role, - style: UiTypography.body2r.textPrimary, - ), - ), - ); - } - - return items; - } -} diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_section_header.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_section_header.dart deleted file mode 100644 index 66d076f5..00000000 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_section_header.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'package:design_system/design_system.dart'; -import 'package:flutter/material.dart'; - -/// A header widget for sections in the one-time order form. -class OneTimeOrderSectionHeader extends StatelessWidget { - /// Creates a [OneTimeOrderSectionHeader]. - const OneTimeOrderSectionHeader({ - required this.title, - this.actionLabel, - this.onAction, - super.key, - }); - - /// The title text for the section. - final String title; - - /// Optional label for an action button on the right. - final String? actionLabel; - - /// Callback when the action button is tapped. - final VoidCallback? onAction; - - @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(title, style: UiTypography.headline4m.textPrimary), - if (actionLabel != null && onAction != null) - TextButton( - onPressed: onAction, - style: TextButton.styleFrom( - padding: EdgeInsets.zero, - minimumSize: Size.zero, - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(UiIcons.add, size: 16, color: UiColors.primary), - const SizedBox(width: UiConstants.space2), - Text( - actionLabel!, - style: UiTypography.body2m.primary, - ), - ], - ), - ), - ], - ); - } -} diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_success_view.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_success_view.dart deleted file mode 100644 index a9981270..00000000 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_success_view.dart +++ /dev/null @@ -1,107 +0,0 @@ -import 'package:design_system/design_system.dart'; -import 'package:flutter/material.dart'; - -/// A view to display when a one-time order has been successfully created. -/// Matches the prototype success view layout with a gradient background and centered card. -class OneTimeOrderSuccessView extends StatelessWidget { - /// Creates a [OneTimeOrderSuccessView]. - const OneTimeOrderSuccessView({ - required this.title, - required this.message, - required this.buttonLabel, - required this.onDone, - super.key, - }); - - /// The title of the success message. - final String title; - - /// The body of the success message. - final String message; - - /// Label for the completion button. - final String buttonLabel; - - /// Callback when the completion button is tapped. - final VoidCallback onDone; - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Container( - width: double.infinity, - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [UiColors.primary, UiColors.buttonPrimaryHover], - ), - ), - child: SafeArea( - child: Center( - child: Container( - margin: const EdgeInsets.symmetric(horizontal: UiConstants.space10), - padding: const EdgeInsets.all(UiConstants.space8), - decoration: BoxDecoration( - color: UiColors.white, - borderRadius: UiConstants.radiusLg * 1.5, - boxShadow: [ - BoxShadow( - color: UiColors.black.withValues(alpha: 0.2), - blurRadius: 20, - offset: const Offset(0, UiConstants.space2 + 2), - ), - ], - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: UiConstants.space16, - height: UiConstants.space16, - decoration: const BoxDecoration( - color: UiColors.accent, - shape: BoxShape.circle, - ), - - - child: const Center( - child: Icon( - UiIcons.check, - color: UiColors.black, - size: UiConstants.space8, - ), - ), - ), - const SizedBox(height: UiConstants.space6), - Text( - title, - style: UiTypography.headline2m.textPrimary, - textAlign: TextAlign.center, - ), - const SizedBox(height: UiConstants.space3), - Text( - message, - textAlign: TextAlign.center, - style: UiTypography.body2r.textSecondary.copyWith( - height: 1.5, - ), - ), - const SizedBox(height: UiConstants.space8), - SizedBox( - width: double.infinity, - child: UiButton.primary( - text: buttonLabel, - onPressed: onDone, - size: UiButtonSize.large, - ), - ), - ], - ), - ), - ), - ), - ), - ); - } -} diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_view.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_view.dart deleted file mode 100644 index a55f4147..00000000 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/one_time_order/one_time_order_view.dart +++ /dev/null @@ -1,328 +0,0 @@ -import 'package:core_localization/core_localization.dart'; -import 'package:design_system/design_system.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_modular/flutter_modular.dart'; -import 'package:krow_core/core.dart'; -import 'package:krow_domain/krow_domain.dart'; -import '../../blocs/one_time_order/one_time_order_bloc.dart'; -import '../../blocs/one_time_order/one_time_order_event.dart'; -import '../../blocs/one_time_order/one_time_order_state.dart'; -import 'one_time_order_date_picker.dart'; -import 'one_time_order_event_name_input.dart'; -import 'one_time_order_header.dart'; -import 'one_time_order_position_card.dart'; -import 'one_time_order_section_header.dart'; -import 'one_time_order_success_view.dart'; - -/// The main content of the One-Time Order page. -class OneTimeOrderView extends StatelessWidget { - /// Creates a [OneTimeOrderView]. - const OneTimeOrderView({super.key}); - - @override - Widget build(BuildContext context) { - final TranslationsClientCreateOrderOneTimeEn labels = - t.client_create_order.one_time; - - return BlocConsumer( - listener: (BuildContext context, OneTimeOrderState state) { - if (state.status == OneTimeOrderStatus.failure && - state.errorMessage != null) { - UiSnackbar.show( - context, - message: translateErrorKey(state.errorMessage!), - type: UiSnackbarType.error, - margin: const EdgeInsets.only(bottom: 140, left: 16, right: 16), - ); - } - }, - builder: (BuildContext context, OneTimeOrderState state) { - if (state.status == OneTimeOrderStatus.success) { - return OneTimeOrderSuccessView( - title: labels.success_title, - message: labels.success_message, - buttonLabel: labels.back_to_orders, - onDone: () => Modular.to.pushNamedAndRemoveUntil( - ClientPaths.orders, - (_) => false, - arguments: { - 'initialDate': state.date.toIso8601String(), - }, - ), - ); - } - - if (state.vendors.isEmpty && - state.status != OneTimeOrderStatus.loading) { - return Scaffold( - body: Column( - children: [ - OneTimeOrderHeader( - title: labels.title, - subtitle: labels.subtitle, - onBack: () => Modular.to.navigate(ClientPaths.createOrder), - ), - Expanded( - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon( - UiIcons.search, - size: 64, - color: UiColors.iconInactive, - ), - const SizedBox(height: UiConstants.space4), - Text( - 'No Vendors Available', - style: UiTypography.headline3m.textPrimary, - ), - const SizedBox(height: UiConstants.space2), - Text( - 'There are no staffing vendors associated with your account.', - style: UiTypography.body2r.textSecondary, - textAlign: TextAlign.center, - ), - ], - ), - ), - ), - ], - ), - ); - } - - return Scaffold( - body: Column( - children: [ - OneTimeOrderHeader( - title: labels.title, - subtitle: labels.subtitle, - onBack: () => Modular.to.navigate(ClientPaths.createOrder), - ), - Expanded( - child: Stack( - children: [ - _OneTimeOrderForm(state: state), - if (state.status == OneTimeOrderStatus.loading) - const Center(child: CircularProgressIndicator()), - ], - ), - ), - _BottomActionButton( - label: state.status == OneTimeOrderStatus.loading - ? labels.creating - : labels.create_order, - isLoading: state.status == OneTimeOrderStatus.loading, - onPressed: state.isValid - ? () => BlocProvider.of( - context, - ).add(const OneTimeOrderSubmitted()) - : null, - ), - ], - ), - ); - }, - ); - } -} - -class _OneTimeOrderForm extends StatelessWidget { - const _OneTimeOrderForm({required this.state}); - final OneTimeOrderState state; - - @override - Widget build(BuildContext context) { - final TranslationsClientCreateOrderOneTimeEn labels = - t.client_create_order.one_time; - - return ListView( - padding: const EdgeInsets.all(UiConstants.space5), - children: [ - Text( - labels.create_your_order, - style: UiTypography.headline3m.textPrimary, - ), - const SizedBox(height: UiConstants.space4), - - OneTimeOrderEventNameInput( - label: 'ORDER NAME', - value: state.eventName, - onChanged: (String value) => BlocProvider.of( - context, - ).add(OneTimeOrderEventNameChanged(value)), - ), - const SizedBox(height: UiConstants.space4), - - // Vendor Selection - Text('SELECT VENDOR', style: UiTypography.footnote2r.textSecondary), - const SizedBox(height: UiConstants.space2), - Container( - padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3), - height: 48, - decoration: BoxDecoration( - color: UiColors.white, - borderRadius: UiConstants.radiusMd, - border: Border.all(color: UiColors.border), - ), - child: DropdownButtonHideUnderline( - child: DropdownButton( - isExpanded: true, - value: state.selectedVendor, - icon: const Icon( - UiIcons.chevronDown, - size: 18, - color: UiColors.iconSecondary, - ), - onChanged: (Vendor? vendor) { - if (vendor != null) { - BlocProvider.of( - context, - ).add(OneTimeOrderVendorChanged(vendor)); - } - }, - items: state.vendors.map((Vendor vendor) { - return DropdownMenuItem( - value: vendor, - child: Text( - vendor.name, - style: UiTypography.body2m.textPrimary, - ), - ); - }).toList(), - ), - ), - ), - const SizedBox(height: UiConstants.space4), - - OneTimeOrderDatePicker( - label: labels.date_label, - value: state.date, - onChanged: (DateTime date) => BlocProvider.of( - context, - ).add(OneTimeOrderDateChanged(date)), - ), - const SizedBox(height: UiConstants.space4), - - Text('HUB', style: UiTypography.footnote2r.textSecondary), - const SizedBox(height: UiConstants.space2), - Container( - padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3), - height: 48, - decoration: BoxDecoration( - color: UiColors.white, - borderRadius: UiConstants.radiusMd, - border: Border.all(color: UiColors.border), - ), - child: DropdownButtonHideUnderline( - child: DropdownButton( - isExpanded: true, - value: state.selectedHub, - icon: const Icon( - UiIcons.chevronDown, - size: 18, - color: UiColors.iconSecondary, - ), - onChanged: (OneTimeOrderHubOption? hub) { - if (hub != null) { - BlocProvider.of( - context, - ).add(OneTimeOrderHubChanged(hub)); - } - }, - items: state.hubs.map((OneTimeOrderHubOption hub) { - return DropdownMenuItem( - value: hub, - child: Text( - hub.name, - style: UiTypography.body2m.textPrimary, - ), - ); - }).toList(), - ), - ), - ), - const SizedBox(height: UiConstants.space6), - - OneTimeOrderSectionHeader( - title: labels.positions_title, - actionLabel: labels.add_position, - onAction: () => BlocProvider.of( - context, - ).add(const OneTimeOrderPositionAdded()), - ), - const SizedBox(height: UiConstants.space3), - - // Positions List - ...state.positions.asMap().entries.map(( - MapEntry entry, - ) { - final int index = entry.key; - final OneTimeOrderPosition position = entry.value; - return Padding( - padding: const EdgeInsets.only(bottom: UiConstants.space3), - child: OneTimeOrderPositionCard( - index: index, - position: position, - isRemovable: state.positions.length > 1, - positionLabel: labels.positions_title, - roleLabel: labels.select_role, - workersLabel: labels.workers_label, - startLabel: labels.start_label, - endLabel: labels.end_label, - lunchLabel: labels.lunch_break_label, - roles: state.roles, - onUpdated: (OneTimeOrderPosition updated) { - BlocProvider.of( - context, - ).add(OneTimeOrderPositionUpdated(index, updated)); - }, - onRemoved: () { - BlocProvider.of( - context, - ).add(OneTimeOrderPositionRemoved(index)); - }, - ), - ); - }), - ], - ); - } -} - -class _BottomActionButton extends StatelessWidget { - const _BottomActionButton({ - required this.label, - required this.onPressed, - this.isLoading = false, - }); - final String label; - final VoidCallback? onPressed; - final bool isLoading; - - @override - Widget build(BuildContext context) { - return Container( - padding: EdgeInsets.only( - left: UiConstants.space5, - right: UiConstants.space5, - top: UiConstants.space5, - bottom: MediaQuery.of(context).padding.bottom + UiConstants.space5, - ), - decoration: const BoxDecoration( - color: UiColors.white, - border: Border(top: BorderSide(color: UiColors.border)), - ), - child: SizedBox( - width: double.infinity, - child: UiButton.primary( - text: label, - onPressed: isLoading ? null : onPressed, - size: UiButtonSize.large, - ), - ), - ); - } -} diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/permanent_order/permanent_order_date_picker.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/permanent_order/permanent_order_date_picker.dart deleted file mode 100644 index 7fe41016..00000000 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/permanent_order/permanent_order_date_picker.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:design_system/design_system.dart'; -import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; - -/// A date picker field for the permanent order form. -class PermanentOrderDatePicker extends StatefulWidget { - /// Creates a [PermanentOrderDatePicker]. - const PermanentOrderDatePicker({ - required this.label, - required this.value, - required this.onChanged, - super.key, - }); - - /// The label text to display above the field. - final String label; - - /// The currently selected date. - final DateTime value; - - /// Callback when a new date is selected. - final ValueChanged onChanged; - - @override - State createState() => - _PermanentOrderDatePickerState(); -} - -class _PermanentOrderDatePickerState extends State { - late final TextEditingController _controller; - - @override - void initState() { - super.initState(); - _controller = TextEditingController( - text: DateFormat('yyyy-MM-dd').format(widget.value), - ); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - void didUpdateWidget(PermanentOrderDatePicker oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.value != oldWidget.value) { - _controller.text = DateFormat('yyyy-MM-dd').format(widget.value); - } - } - - @override - Widget build(BuildContext context) { - return UiTextField( - label: widget.label, - controller: _controller, - readOnly: true, - prefixIcon: UiIcons.calendar, - onTap: () async { - final DateTime? picked = await showDatePicker( - context: context, - initialDate: widget.value, - firstDate: DateTime.now(), - lastDate: DateTime.now().add(const Duration(days: 365)), - ); - if (picked != null) { - widget.onChanged(picked); - } - }, - ); - } -} diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/permanent_order/permanent_order_event_name_input.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/permanent_order/permanent_order_event_name_input.dart deleted file mode 100644 index 4eb0baa4..00000000 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/permanent_order/permanent_order_event_name_input.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'package:design_system/design_system.dart'; -import 'package:flutter/material.dart'; - -/// A text input for the order name in the permanent order form. -class PermanentOrderEventNameInput extends StatefulWidget { - const PermanentOrderEventNameInput({ - required this.label, - required this.value, - required this.onChanged, - super.key, - }); - - final String label; - final String value; - final ValueChanged onChanged; - - @override - State createState() => - _PermanentOrderEventNameInputState(); -} - -class _PermanentOrderEventNameInputState - extends State { - late final TextEditingController _controller; - - @override - void initState() { - super.initState(); - _controller = TextEditingController(text: widget.value); - } - - @override - void didUpdateWidget(PermanentOrderEventNameInput oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.value != _controller.text) { - _controller.text = widget.value; - } - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return UiTextField( - label: widget.label, - controller: _controller, - onChanged: widget.onChanged, - hintText: 'Order name', - prefixIcon: UiIcons.briefcase, - ); - } -} diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/permanent_order/permanent_order_header.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/permanent_order/permanent_order_header.dart deleted file mode 100644 index 8943f5f1..00000000 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/permanent_order/permanent_order_header.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'package:design_system/design_system.dart'; -import 'package:flutter/material.dart'; - -/// A header widget for the permanent order flow with a colored background. -class PermanentOrderHeader extends StatelessWidget { - /// Creates a [PermanentOrderHeader]. - const PermanentOrderHeader({ - required this.title, - required this.subtitle, - required this.onBack, - super.key, - }); - - /// The title of the page. - final String title; - - /// The subtitle or description. - final String subtitle; - - /// Callback when the back button is pressed. - final VoidCallback onBack; - - @override - Widget build(BuildContext context) { - return Container( - padding: EdgeInsets.only( - top: MediaQuery.of(context).padding.top + UiConstants.space5, - bottom: UiConstants.space5, - left: UiConstants.space5, - right: UiConstants.space5, - ), - color: UiColors.primary, - child: Row( - children: [ - GestureDetector( - onTap: onBack, - child: Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: UiColors.white.withValues(alpha: 0.2), - borderRadius: UiConstants.radiusMd, - ), - child: const Icon( - UiIcons.chevronLeft, - color: UiColors.white, - size: 24, - ), - ), - ), - const SizedBox(width: UiConstants.space3), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: UiTypography.headline3m.copyWith(color: UiColors.white), - ), - Text( - subtitle, - style: UiTypography.footnote2r.copyWith( - color: UiColors.white.withValues(alpha: 0.8), - ), - ), - ], - ), - ], - ), - ); - } -} diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/permanent_order/permanent_order_position_card.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/permanent_order/permanent_order_position_card.dart deleted file mode 100644 index e3fb7404..00000000 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/permanent_order/permanent_order_position_card.dart +++ /dev/null @@ -1,345 +0,0 @@ -import 'package:core_localization/core_localization.dart'; -import 'package:design_system/design_system.dart'; -import 'package:flutter/material.dart'; -import '../../blocs/permanent_order/permanent_order_state.dart'; - -/// A card widget for editing a specific position in a permanent order. -class PermanentOrderPositionCard extends StatelessWidget { - /// Creates a [PermanentOrderPositionCard]. - const PermanentOrderPositionCard({ - required this.index, - required this.position, - required this.isRemovable, - required this.onUpdated, - required this.onRemoved, - required this.positionLabel, - required this.roleLabel, - required this.workersLabel, - required this.startLabel, - required this.endLabel, - required this.lunchLabel, - required this.roles, - super.key, - }); - - /// The index of the position in the list. - final int index; - - /// The position entity data. - final PermanentOrderPosition position; - - /// Whether this position can be removed (usually if there's more than one). - final bool isRemovable; - - /// Callback when the position data is updated. - final ValueChanged onUpdated; - - /// Callback when the position is removed. - final VoidCallback onRemoved; - - /// Label for positions (e.g., "Position"). - final String positionLabel; - - /// Label for the role selection. - final String roleLabel; - - /// Label for the worker count. - final String workersLabel; - - /// Label for the start time. - final String startLabel; - - /// Label for the end time. - final String endLabel; - - /// Label for the lunch break. - final String lunchLabel; - - /// Available roles for the selected vendor. - final List roles; - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.all(UiConstants.space4), - decoration: BoxDecoration( - color: UiColors.white, - borderRadius: UiConstants.radiusLg, - border: Border.all(color: UiColors.border), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '$positionLabel #${index + 1}', - style: UiTypography.footnote1m.textSecondary, - ), - if (isRemovable) - GestureDetector( - onTap: onRemoved, - child: Text( - t.client_create_order.one_time.remove, - style: UiTypography.footnote1m.copyWith( - color: UiColors.destructive, - ), - ), - ), - ], - ), - const SizedBox(height: UiConstants.space3), - - // Role (Dropdown) - Container( - padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3), - height: 44, - decoration: BoxDecoration( - borderRadius: UiConstants.radiusMd, - border: Border.all(color: UiColors.border), - ), - child: DropdownButtonHideUnderline( - child: DropdownButton( - isExpanded: true, - hint: Text( - roleLabel, - style: UiTypography.body2r.textPlaceholder, - ), - value: position.role.isEmpty ? null : position.role, - icon: const Icon( - UiIcons.chevronDown, - size: 18, - color: UiColors.iconSecondary, - ), - onChanged: (String? val) { - if (val != null) { - onUpdated(position.copyWith(role: val)); - } - }, - items: _buildRoleItems(), - ), - ), - ), - const SizedBox(height: UiConstants.space3), - - // Start/End/Workers Row - Row( - children: [ - // Start Time - Expanded( - child: _buildTimeInput( - context: context, - label: startLabel, - value: position.startTime, - onTap: () async { - final TimeOfDay? picked = await showTimePicker( - context: context, - initialTime: TimeOfDay.now(), - ); - if (picked != null && context.mounted) { - onUpdated( - position.copyWith(startTime: picked.format(context)), - ); - } - }, - ), - ), - const SizedBox(width: UiConstants.space2), - // End Time - Expanded( - child: _buildTimeInput( - context: context, - label: endLabel, - value: position.endTime, - onTap: () async { - final TimeOfDay? picked = await showTimePicker( - context: context, - initialTime: TimeOfDay.now(), - ); - if (picked != null && context.mounted) { - onUpdated( - position.copyWith(endTime: picked.format(context)), - ); - } - }, - ), - ), - const SizedBox(width: UiConstants.space2), - // Workers Count - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - workersLabel, - style: UiTypography.footnote2r.textSecondary, - ), - const SizedBox(height: UiConstants.space1), - Container( - height: 40, - decoration: BoxDecoration( - color: UiColors.bgSecondary, - borderRadius: UiConstants.radiusSm, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - GestureDetector( - onTap: () { - if (position.count > 1) { - onUpdated( - position.copyWith(count: position.count - 1), - ); - } - }, - child: const Icon(UiIcons.minus, size: 12), - ), - Text( - '${position.count}', - style: UiTypography.body2b.textPrimary, - ), - GestureDetector( - onTap: () { - onUpdated( - position.copyWith(count: position.count + 1), - ); - }, - child: const Icon(UiIcons.add, size: 12), - ), - ], - ), - ), - ], - ), - ), - ], - ), - const SizedBox(height: UiConstants.space4), - - // Lunch Break - Text(lunchLabel, style: UiTypography.footnote2r.textSecondary), - const SizedBox(height: UiConstants.space1), - Container( - padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3), - height: 44, - decoration: BoxDecoration( - borderRadius: UiConstants.radiusMd, - border: Border.all(color: UiColors.border), - ), - child: DropdownButtonHideUnderline( - child: DropdownButton( - isExpanded: true, - value: position.lunchBreak, - icon: const Icon( - UiIcons.chevronDown, - size: 18, - color: UiColors.iconSecondary, - ), - onChanged: (String? val) { - if (val != null) { - onUpdated(position.copyWith(lunchBreak: val)); - } - }, - items: [ - 'NO_BREAK', - 'MIN_10', - 'MIN_15', - 'MIN_30', - 'MIN_45', - 'MIN_60', - ].map((String value) { - final String label = switch (value) { - 'NO_BREAK' => 'No Break', - 'MIN_10' => '10 min (Paid)', - 'MIN_15' => '15 min (Paid)', - 'MIN_30' => '30 min (Unpaid)', - 'MIN_45' => '45 min (Unpaid)', - 'MIN_60' => '60 min (Unpaid)', - _ => value, - }; - return DropdownMenuItem( - value: value, - child: Text( - label, - style: UiTypography.body2r.textPrimary, - ), - ); - }).toList(), - ), - ), - ), - ], - ), - ); - } - - Widget _buildTimeInput({ - required BuildContext context, - required String label, - required String value, - required VoidCallback onTap, - }) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(label, style: UiTypography.footnote2r.textSecondary), - const SizedBox(height: UiConstants.space1), - GestureDetector( - onTap: onTap, - child: Container( - height: 40, - padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3), - decoration: BoxDecoration( - borderRadius: UiConstants.radiusSm, - border: Border.all(color: UiColors.border), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - value.isEmpty ? '--:--' : value, - style: UiTypography.body2r.textPrimary, - ), - const Icon( - UiIcons.clock, - size: 14, - color: UiColors.iconSecondary, - ), - ], - ), - ), - ), - ], - ); - } - - List> _buildRoleItems() { - final List> items = roles - .map( - (PermanentOrderRoleOption role) => DropdownMenuItem( - value: role.id, - child: Text( - '${role.name} - \$${role.costPerHour.toStringAsFixed(0)}', - style: UiTypography.body2r.textPrimary, - ), - ), - ) - .toList(); - - final bool hasSelected = roles.any((PermanentOrderRoleOption role) => role.id == position.role); - if (position.role.isNotEmpty && !hasSelected) { - items.add( - DropdownMenuItem( - value: position.role, - child: Text( - position.role, - style: UiTypography.body2r.textPrimary, - ), - ), - ); - } - - return items; - } -} diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/permanent_order/permanent_order_section_header.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/permanent_order/permanent_order_section_header.dart deleted file mode 100644 index 21d47825..00000000 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/permanent_order/permanent_order_section_header.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'package:design_system/design_system.dart'; -import 'package:flutter/material.dart'; - -/// A header widget for sections in the permanent order form. -class PermanentOrderSectionHeader extends StatelessWidget { - /// Creates a [PermanentOrderSectionHeader]. - const PermanentOrderSectionHeader({ - required this.title, - this.actionLabel, - this.onAction, - super.key, - }); - - /// The title text for the section. - final String title; - - /// Optional label for an action button on the right. - final String? actionLabel; - - /// Callback when the action button is tapped. - final VoidCallback? onAction; - - @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(title, style: UiTypography.headline4m.textPrimary), - if (actionLabel != null && onAction != null) - TextButton( - onPressed: onAction, - style: TextButton.styleFrom( - padding: EdgeInsets.zero, - minimumSize: Size.zero, - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(UiIcons.add, size: 16, color: UiColors.primary), - const SizedBox(width: UiConstants.space2), - Text( - actionLabel!, - style: UiTypography.body2m.primary, - ), - ], - ), - ), - ], - ); - } -} diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/permanent_order/permanent_order_success_view.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/permanent_order/permanent_order_success_view.dart deleted file mode 100644 index a4b72cbc..00000000 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/permanent_order/permanent_order_success_view.dart +++ /dev/null @@ -1,104 +0,0 @@ -import 'package:design_system/design_system.dart'; -import 'package:flutter/material.dart'; - -/// A view to display when a permanent order has been successfully created. -class PermanentOrderSuccessView extends StatelessWidget { - /// Creates a [PermanentOrderSuccessView]. - const PermanentOrderSuccessView({ - required this.title, - required this.message, - required this.buttonLabel, - required this.onDone, - super.key, - }); - - /// The title of the success message. - final String title; - - /// The body of the success message. - final String message; - - /// Label for the completion button. - final String buttonLabel; - - /// Callback when the completion button is tapped. - final VoidCallback onDone; - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Container( - width: double.infinity, - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [UiColors.primary, UiColors.buttonPrimaryHover], - ), - ), - child: SafeArea( - child: Center( - child: Container( - margin: const EdgeInsets.symmetric(horizontal: UiConstants.space10), - padding: const EdgeInsets.all(UiConstants.space8), - decoration: BoxDecoration( - color: UiColors.white, - borderRadius: UiConstants.radiusLg * 1.5, - boxShadow: [ - BoxShadow( - color: UiColors.black.withValues(alpha: 0.2), - blurRadius: 20, - offset: const Offset(0, UiConstants.space2 + 2), - ), - ], - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: UiConstants.space16, - height: UiConstants.space16, - decoration: const BoxDecoration( - color: UiColors.accent, - shape: BoxShape.circle, - ), - child: const Center( - child: Icon( - UiIcons.check, - color: UiColors.black, - size: UiConstants.space8, - ), - ), - ), - const SizedBox(height: UiConstants.space6), - Text( - title, - style: UiTypography.headline2m.textPrimary, - textAlign: TextAlign.center, - ), - const SizedBox(height: UiConstants.space3), - Text( - message, - textAlign: TextAlign.center, - style: UiTypography.body2r.textSecondary.copyWith( - height: 1.5, - ), - ), - const SizedBox(height: UiConstants.space8), - SizedBox( - width: double.infinity, - child: UiButton.primary( - text: buttonLabel, - onPressed: onDone, - size: UiButtonSize.large, - ), - ), - ], - ), - ), - ), - ), - ), - ); - } -} diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/permanent_order/permanent_order_view.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/permanent_order/permanent_order_view.dart deleted file mode 100644 index 538ac7e7..00000000 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/permanent_order/permanent_order_view.dart +++ /dev/null @@ -1,440 +0,0 @@ -import 'package:core_localization/core_localization.dart'; -import 'package:design_system/design_system.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_modular/flutter_modular.dart'; -import 'package:krow_core/core.dart'; -import 'package:krow_domain/krow_domain.dart' show Vendor; -import '../../blocs/permanent_order/permanent_order_bloc.dart'; -import '../../blocs/permanent_order/permanent_order_event.dart'; -import '../../blocs/permanent_order/permanent_order_state.dart'; -import 'permanent_order_date_picker.dart'; -import 'permanent_order_event_name_input.dart'; -import 'permanent_order_header.dart'; -import 'permanent_order_position_card.dart'; -import 'permanent_order_section_header.dart'; -import 'permanent_order_success_view.dart'; - -/// The main content of the Permanent Order page. -class PermanentOrderView extends StatelessWidget { - /// Creates a [PermanentOrderView]. - const PermanentOrderView({super.key}); - - DateTime _firstPermanentShiftDate( - DateTime startDate, - List permanentDays, - ) { - final DateTime start = DateTime(startDate.year, startDate.month, startDate.day); - final DateTime end = start.add(const Duration(days: 29)); - final Set selected = permanentDays.toSet(); - for (DateTime day = start; !day.isAfter(end); day = day.add(const Duration(days: 1))) { - if (selected.contains(_weekdayLabel(day))) { - return day; - } - } - return start; - } - - String _weekdayLabel(DateTime date) { - switch (date.weekday) { - case DateTime.monday: - return 'MON'; - case DateTime.tuesday: - return 'TUE'; - case DateTime.wednesday: - return 'WED'; - case DateTime.thursday: - return 'THU'; - case DateTime.friday: - return 'FRI'; - case DateTime.saturday: - return 'SAT'; - case DateTime.sunday: - return 'SUN'; - default: - return 'SUN'; - } - } - - @override - Widget build(BuildContext context) { - final TranslationsClientCreateOrderPermanentEn labels = - t.client_create_order.permanent; - final TranslationsClientCreateOrderOneTimeEn oneTimeLabels = - t.client_create_order.one_time; - - return BlocConsumer( - listener: (BuildContext context, PermanentOrderState state) { - if (state.status == PermanentOrderStatus.failure && - state.errorMessage != null) { - final String message = translateErrorKey(state.errorMessage!); - UiSnackbar.show( - context, - message: message, - type: UiSnackbarType.error, - margin: const EdgeInsets.only(bottom: 140, left: 16, right: 16), - ); - } - }, - builder: (BuildContext context, PermanentOrderState state) { - if (state.status == PermanentOrderStatus.success) { - final DateTime initialDate = _firstPermanentShiftDate( - state.startDate, - state.permanentDays, - ); - return PermanentOrderSuccessView( - title: labels.title, - message: labels.subtitle, - buttonLabel: oneTimeLabels.back_to_orders, - onDone: () => Modular.to.pushNamedAndRemoveUntil( - ClientPaths.orders, - (_) => false, - arguments: { - 'initialDate': initialDate.toIso8601String(), - }, - ), - ); - } - - if (state.vendors.isEmpty && - state.status != PermanentOrderStatus.loading) { - return Scaffold( - body: Column( - children: [ - PermanentOrderHeader( - title: labels.title, - subtitle: labels.subtitle, - onBack: () => Modular.to.navigate(ClientPaths.createOrder), - ), - Expanded( - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon( - UiIcons.search, - size: 64, - color: UiColors.iconInactive, - ), - const SizedBox(height: UiConstants.space4), - Text( - 'No Vendors Available', - style: UiTypography.headline3m.textPrimary, - ), - const SizedBox(height: UiConstants.space2), - Text( - 'There are no staffing vendors associated with your account.', - style: UiTypography.body2r.textSecondary, - textAlign: TextAlign.center, - ), - ], - ), - ), - ), - ], - ), - ); - } - - return Scaffold( - body: Column( - children: [ - PermanentOrderHeader( - title: labels.title, - subtitle: labels.subtitle, - onBack: () => Modular.to.navigate(ClientPaths.createOrder), - ), - Expanded( - child: Stack( - children: [ - _PermanentOrderForm(state: state), - if (state.status == PermanentOrderStatus.loading) - const Center(child: CircularProgressIndicator()), - ], - ), - ), - _BottomActionButton( - label: state.status == PermanentOrderStatus.loading - ? oneTimeLabels.creating - : oneTimeLabels.create_order, - isLoading: state.status == PermanentOrderStatus.loading, - onPressed: state.isValid - ? () => BlocProvider.of( - context, - ).add(const PermanentOrderSubmitted()) - : null, - ), - ], - ), - ); - }, - ); - } -} - -class _PermanentOrderForm extends StatelessWidget { - const _PermanentOrderForm({required this.state}); - final PermanentOrderState state; - - @override - Widget build(BuildContext context) { - final TranslationsClientCreateOrderPermanentEn labels = - t.client_create_order.permanent; - final TranslationsClientCreateOrderOneTimeEn oneTimeLabels = - t.client_create_order.one_time; - - return ListView( - padding: const EdgeInsets.all(UiConstants.space5), - children: [ - Text( - labels.title, - style: UiTypography.headline3m.textPrimary, - ), - const SizedBox(height: UiConstants.space4), - - PermanentOrderEventNameInput( - label: 'ORDER NAME', - value: state.eventName, - onChanged: (String value) => BlocProvider.of( - context, - ).add(PermanentOrderEventNameChanged(value)), - ), - const SizedBox(height: UiConstants.space4), - - // Vendor Selection - Text('SELECT VENDOR', style: UiTypography.footnote2r.textSecondary), - const SizedBox(height: UiConstants.space2), - Container( - padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3), - height: 48, - decoration: BoxDecoration( - color: UiColors.white, - borderRadius: UiConstants.radiusMd, - border: Border.all(color: UiColors.border), - ), - child: DropdownButtonHideUnderline( - child: DropdownButton( - isExpanded: true, - value: state.selectedVendor, - icon: const Icon( - UiIcons.chevronDown, - size: 18, - color: UiColors.iconSecondary, - ), - onChanged: (Vendor? vendor) { - if (vendor != null) { - BlocProvider.of( - context, - ).add(PermanentOrderVendorChanged(vendor)); - } - }, - items: state.vendors.map((Vendor vendor) { - return DropdownMenuItem( - value: vendor, - child: Text( - vendor.name, - style: UiTypography.body2m.textPrimary, - ), - ); - }).toList(), - ), - ), - ), - const SizedBox(height: UiConstants.space4), - - PermanentOrderDatePicker( - label: 'Start Date', - value: state.startDate, - onChanged: (DateTime date) => BlocProvider.of( - context, - ).add(PermanentOrderStartDateChanged(date)), - ), - const SizedBox(height: UiConstants.space4), - - Text('Permanent Days', style: UiTypography.footnote2r.textSecondary), - const SizedBox(height: UiConstants.space2), - _PermanentDaysSelector( - selectedDays: state.permanentDays, - onToggle: (int dayIndex) => BlocProvider.of( - context, - ).add(PermanentOrderDayToggled(dayIndex)), - ), - const SizedBox(height: UiConstants.space4), - - Text('HUB', style: UiTypography.footnote2r.textSecondary), - const SizedBox(height: UiConstants.space2), - Container( - padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3), - height: 48, - decoration: BoxDecoration( - color: UiColors.white, - borderRadius: UiConstants.radiusMd, - border: Border.all(color: UiColors.border), - ), - child: DropdownButtonHideUnderline( - child: DropdownButton( - isExpanded: true, - value: state.selectedHub, - icon: const Icon( - UiIcons.chevronDown, - size: 18, - color: UiColors.iconSecondary, - ), - onChanged: (PermanentOrderHubOption? hub) { - if (hub != null) { - BlocProvider.of( - context, - ).add(PermanentOrderHubChanged(hub)); - } - }, - items: state.hubs.map((PermanentOrderHubOption hub) { - return DropdownMenuItem( - value: hub, - child: Text( - hub.name, - style: UiTypography.body2m.textPrimary, - ), - ); - }).toList(), - ), - ), - ), - const SizedBox(height: UiConstants.space6), - - PermanentOrderSectionHeader( - title: oneTimeLabels.positions_title, - actionLabel: oneTimeLabels.add_position, - onAction: () => BlocProvider.of( - context, - ).add(const PermanentOrderPositionAdded()), - ), - const SizedBox(height: UiConstants.space3), - - // Positions List - ...state.positions.asMap().entries.map(( - MapEntry entry, - ) { - final int index = entry.key; - final PermanentOrderPosition position = entry.value; - return Padding( - padding: const EdgeInsets.only(bottom: UiConstants.space3), - child: PermanentOrderPositionCard( - index: index, - position: position, - isRemovable: state.positions.length > 1, - positionLabel: oneTimeLabels.positions_title, - roleLabel: oneTimeLabels.select_role, - workersLabel: oneTimeLabels.workers_label, - startLabel: oneTimeLabels.start_label, - endLabel: oneTimeLabels.end_label, - lunchLabel: oneTimeLabels.lunch_break_label, - roles: state.roles, - onUpdated: (PermanentOrderPosition updated) { - BlocProvider.of( - context, - ).add(PermanentOrderPositionUpdated(index, updated)); - }, - onRemoved: () { - BlocProvider.of( - context, - ).add(PermanentOrderPositionRemoved(index)); - }, - ), - ); - }), - ], - ); - } -} - -class _PermanentDaysSelector extends StatelessWidget { - const _PermanentDaysSelector({ - required this.selectedDays, - required this.onToggle, - }); - - final List selectedDays; - final ValueChanged onToggle; - - @override - Widget build(BuildContext context) { - const List labelsShort = [ - 'S', - 'M', - 'T', - 'W', - 'T', - 'F', - 'S', - ]; - const List labelsLong = [ - 'SUN', - 'MON', - 'TUE', - 'WED', - 'THU', - 'FRI', - 'SAT', - ]; - return Wrap( - spacing: UiConstants.space2, - children: List.generate(labelsShort.length, (int index) { - final bool isSelected = selectedDays.contains(labelsLong[index]); - return GestureDetector( - onTap: () => onToggle(index), - child: Container( - width: 36, - height: 36, - decoration: BoxDecoration( - color: isSelected ? UiColors.primary : UiColors.white, - shape: BoxShape.circle, - border: Border.all(color: UiColors.border), - ), - alignment: Alignment.center, - child: Text( - labelsShort[index], - style: UiTypography.body2m.copyWith( - color: isSelected ? UiColors.white : UiColors.textSecondary, - ), - ), - ), - ); - }), - ); - } -} - -class _BottomActionButton extends StatelessWidget { - const _BottomActionButton({ - required this.label, - required this.onPressed, - this.isLoading = false, - }); - final String label; - final VoidCallback? onPressed; - final bool isLoading; - - @override - Widget build(BuildContext context) { - return Container( - padding: EdgeInsets.only( - left: UiConstants.space5, - right: UiConstants.space5, - top: UiConstants.space5, - bottom: MediaQuery.of(context).padding.bottom + UiConstants.space5, - ), - decoration: const BoxDecoration( - color: UiColors.white, - border: Border(top: BorderSide(color: UiColors.border)), - ), - child: SizedBox( - width: double.infinity, - child: UiButton.primary( - text: label, - onPressed: isLoading ? null : onPressed, - size: UiButtonSize.large, - ), - ), - ); - } -} diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_view.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_view.dart index 3c51d7cb..08837105 100644 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_view.dart +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/rapid_order/rapid_order_view.dart @@ -295,7 +295,6 @@ class _RapidOrderActions extends StatelessWidget { onPressed: isSubmitting || isMessageEmpty ? null : () { - print('RapidOrder send pressed'); BlocProvider.of( context, ).add(const RapidOrderSubmitted()); diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/recurring_order/recurring_order_date_picker.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/recurring_order/recurring_order_date_picker.dart deleted file mode 100644 index f9b7df68..00000000 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/recurring_order/recurring_order_date_picker.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:design_system/design_system.dart'; -import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; - -/// A date picker field for the recurring order form. -class RecurringOrderDatePicker extends StatefulWidget { - /// Creates a [RecurringOrderDatePicker]. - const RecurringOrderDatePicker({ - required this.label, - required this.value, - required this.onChanged, - super.key, - }); - - /// The label text to display above the field. - final String label; - - /// The currently selected date. - final DateTime value; - - /// Callback when a new date is selected. - final ValueChanged onChanged; - - @override - State createState() => - _RecurringOrderDatePickerState(); -} - -class _RecurringOrderDatePickerState extends State { - late final TextEditingController _controller; - - @override - void initState() { - super.initState(); - _controller = TextEditingController( - text: DateFormat('yyyy-MM-dd').format(widget.value), - ); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - void didUpdateWidget(RecurringOrderDatePicker oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.value != oldWidget.value) { - _controller.text = DateFormat('yyyy-MM-dd').format(widget.value); - } - } - - @override - Widget build(BuildContext context) { - return UiTextField( - label: widget.label, - controller: _controller, - readOnly: true, - prefixIcon: UiIcons.calendar, - onTap: () async { - final DateTime? picked = await showDatePicker( - context: context, - initialDate: widget.value, - firstDate: DateTime.now(), - lastDate: DateTime.now().add(const Duration(days: 365)), - ); - if (picked != null) { - widget.onChanged(picked); - } - }, - ); - } -} diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/recurring_order/recurring_order_event_name_input.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/recurring_order/recurring_order_event_name_input.dart deleted file mode 100644 index 22d7cae9..00000000 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/recurring_order/recurring_order_event_name_input.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'package:design_system/design_system.dart'; -import 'package:flutter/material.dart'; - -/// A text input for the order name in the recurring order form. -class RecurringOrderEventNameInput extends StatefulWidget { - const RecurringOrderEventNameInput({ - required this.label, - required this.value, - required this.onChanged, - super.key, - }); - - final String label; - final String value; - final ValueChanged onChanged; - - @override - State createState() => - _RecurringOrderEventNameInputState(); -} - -class _RecurringOrderEventNameInputState - extends State { - late final TextEditingController _controller; - - @override - void initState() { - super.initState(); - _controller = TextEditingController(text: widget.value); - } - - @override - void didUpdateWidget(RecurringOrderEventNameInput oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.value != _controller.text) { - _controller.text = widget.value; - } - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return UiTextField( - label: widget.label, - controller: _controller, - onChanged: widget.onChanged, - hintText: 'Order name', - prefixIcon: UiIcons.briefcase, - ); - } -} diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/recurring_order/recurring_order_header.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/recurring_order/recurring_order_header.dart deleted file mode 100644 index 5913b205..00000000 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/recurring_order/recurring_order_header.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'package:design_system/design_system.dart'; -import 'package:flutter/material.dart'; - -/// A header widget for the recurring order flow with a colored background. -class RecurringOrderHeader extends StatelessWidget { - /// Creates a [RecurringOrderHeader]. - const RecurringOrderHeader({ - required this.title, - required this.subtitle, - required this.onBack, - super.key, - }); - - /// The title of the page. - final String title; - - /// The subtitle or description. - final String subtitle; - - /// Callback when the back button is pressed. - final VoidCallback onBack; - - @override - Widget build(BuildContext context) { - return Container( - padding: EdgeInsets.only( - top: MediaQuery.of(context).padding.top + UiConstants.space5, - bottom: UiConstants.space5, - left: UiConstants.space5, - right: UiConstants.space5, - ), - color: UiColors.primary, - child: Row( - children: [ - GestureDetector( - onTap: onBack, - child: Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: UiColors.white.withValues(alpha: 0.2), - borderRadius: UiConstants.radiusMd, - ), - child: const Icon( - UiIcons.chevronLeft, - color: UiColors.white, - size: 24, - ), - ), - ), - const SizedBox(width: UiConstants.space3), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: UiTypography.headline3m.copyWith(color: UiColors.white), - ), - Text( - subtitle, - style: UiTypography.footnote2r.copyWith( - color: UiColors.white.withValues(alpha: 0.8), - ), - ), - ], - ), - ], - ), - ); - } -} diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/recurring_order/recurring_order_position_card.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/recurring_order/recurring_order_position_card.dart deleted file mode 100644 index a52be4b4..00000000 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/recurring_order/recurring_order_position_card.dart +++ /dev/null @@ -1,345 +0,0 @@ -import 'package:core_localization/core_localization.dart'; -import 'package:design_system/design_system.dart'; -import 'package:flutter/material.dart'; -import '../../blocs/recurring_order/recurring_order_state.dart'; - -/// A card widget for editing a specific position in a recurring order. -class RecurringOrderPositionCard extends StatelessWidget { - /// Creates a [RecurringOrderPositionCard]. - const RecurringOrderPositionCard({ - required this.index, - required this.position, - required this.isRemovable, - required this.onUpdated, - required this.onRemoved, - required this.positionLabel, - required this.roleLabel, - required this.workersLabel, - required this.startLabel, - required this.endLabel, - required this.lunchLabel, - required this.roles, - super.key, - }); - - /// The index of the position in the list. - final int index; - - /// The position entity data. - final RecurringOrderPosition position; - - /// Whether this position can be removed (usually if there's more than one). - final bool isRemovable; - - /// Callback when the position data is updated. - final ValueChanged onUpdated; - - /// Callback when the position is removed. - final VoidCallback onRemoved; - - /// Label for positions (e.g., "Position"). - final String positionLabel; - - /// Label for the role selection. - final String roleLabel; - - /// Label for the worker count. - final String workersLabel; - - /// Label for the start time. - final String startLabel; - - /// Label for the end time. - final String endLabel; - - /// Label for the lunch break. - final String lunchLabel; - - /// Available roles for the selected vendor. - final List roles; - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.all(UiConstants.space4), - decoration: BoxDecoration( - color: UiColors.white, - borderRadius: UiConstants.radiusLg, - border: Border.all(color: UiColors.border), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '$positionLabel #${index + 1}', - style: UiTypography.footnote1m.textSecondary, - ), - if (isRemovable) - GestureDetector( - onTap: onRemoved, - child: Text( - t.client_create_order.one_time.remove, - style: UiTypography.footnote1m.copyWith( - color: UiColors.destructive, - ), - ), - ), - ], - ), - const SizedBox(height: UiConstants.space3), - - // Role (Dropdown) - Container( - padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3), - height: 44, - decoration: BoxDecoration( - borderRadius: UiConstants.radiusMd, - border: Border.all(color: UiColors.border), - ), - child: DropdownButtonHideUnderline( - child: DropdownButton( - isExpanded: true, - hint: Text( - roleLabel, - style: UiTypography.body2r.textPlaceholder, - ), - value: position.role.isEmpty ? null : position.role, - icon: const Icon( - UiIcons.chevronDown, - size: 18, - color: UiColors.iconSecondary, - ), - onChanged: (String? val) { - if (val != null) { - onUpdated(position.copyWith(role: val)); - } - }, - items: _buildRoleItems(), - ), - ), - ), - const SizedBox(height: UiConstants.space3), - - // Start/End/Workers Row - Row( - children: [ - // Start Time - Expanded( - child: _buildTimeInput( - context: context, - label: startLabel, - value: position.startTime, - onTap: () async { - final TimeOfDay? picked = await showTimePicker( - context: context, - initialTime: TimeOfDay.now(), - ); - if (picked != null && context.mounted) { - onUpdated( - position.copyWith(startTime: picked.format(context)), - ); - } - }, - ), - ), - const SizedBox(width: UiConstants.space2), - // End Time - Expanded( - child: _buildTimeInput( - context: context, - label: endLabel, - value: position.endTime, - onTap: () async { - final TimeOfDay? picked = await showTimePicker( - context: context, - initialTime: TimeOfDay.now(), - ); - if (picked != null && context.mounted) { - onUpdated( - position.copyWith(endTime: picked.format(context)), - ); - } - }, - ), - ), - const SizedBox(width: UiConstants.space2), - // Workers Count - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - workersLabel, - style: UiTypography.footnote2r.textSecondary, - ), - const SizedBox(height: UiConstants.space1), - Container( - height: 40, - decoration: BoxDecoration( - color: UiColors.bgSecondary, - borderRadius: UiConstants.radiusSm, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - GestureDetector( - onTap: () { - if (position.count > 1) { - onUpdated( - position.copyWith(count: position.count - 1), - ); - } - }, - child: const Icon(UiIcons.minus, size: 12), - ), - Text( - '${position.count}', - style: UiTypography.body2b.textPrimary, - ), - GestureDetector( - onTap: () { - onUpdated( - position.copyWith(count: position.count + 1), - ); - }, - child: const Icon(UiIcons.add, size: 12), - ), - ], - ), - ), - ], - ), - ), - ], - ), - const SizedBox(height: UiConstants.space4), - - // Lunch Break - Text(lunchLabel, style: UiTypography.footnote2r.textSecondary), - const SizedBox(height: UiConstants.space1), - Container( - padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3), - height: 44, - decoration: BoxDecoration( - borderRadius: UiConstants.radiusMd, - border: Border.all(color: UiColors.border), - ), - child: DropdownButtonHideUnderline( - child: DropdownButton( - isExpanded: true, - value: position.lunchBreak, - icon: const Icon( - UiIcons.chevronDown, - size: 18, - color: UiColors.iconSecondary, - ), - onChanged: (String? val) { - if (val != null) { - onUpdated(position.copyWith(lunchBreak: val)); - } - }, - items: [ - 'NO_BREAK', - 'MIN_10', - 'MIN_15', - 'MIN_30', - 'MIN_45', - 'MIN_60', - ].map((String value) { - final String label = switch (value) { - 'NO_BREAK' => 'No Break', - 'MIN_10' => '10 min (Paid)', - 'MIN_15' => '15 min (Paid)', - 'MIN_30' => '30 min (Unpaid)', - 'MIN_45' => '45 min (Unpaid)', - 'MIN_60' => '60 min (Unpaid)', - _ => value, - }; - return DropdownMenuItem( - value: value, - child: Text( - label, - style: UiTypography.body2r.textPrimary, - ), - ); - }).toList(), - ), - ), - ), - ], - ), - ); - } - - Widget _buildTimeInput({ - required BuildContext context, - required String label, - required String value, - required VoidCallback onTap, - }) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(label, style: UiTypography.footnote2r.textSecondary), - const SizedBox(height: UiConstants.space1), - GestureDetector( - onTap: onTap, - child: Container( - height: 40, - padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3), - decoration: BoxDecoration( - borderRadius: UiConstants.radiusSm, - border: Border.all(color: UiColors.border), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - value.isEmpty ? '--:--' : value, - style: UiTypography.body2r.textPrimary, - ), - const Icon( - UiIcons.clock, - size: 14, - color: UiColors.iconSecondary, - ), - ], - ), - ), - ), - ], - ); - } - - List> _buildRoleItems() { - final List> items = roles - .map( - (RecurringOrderRoleOption role) => DropdownMenuItem( - value: role.id, - child: Text( - '${role.name} - \$${role.costPerHour.toStringAsFixed(0)}', - style: UiTypography.body2r.textPrimary, - ), - ), - ) - .toList(); - - final bool hasSelected = roles.any((RecurringOrderRoleOption role) => role.id == position.role); - if (position.role.isNotEmpty && !hasSelected) { - items.add( - DropdownMenuItem( - value: position.role, - child: Text( - position.role, - style: UiTypography.body2r.textPrimary, - ), - ), - ); - } - - return items; - } -} diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/recurring_order/recurring_order_section_header.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/recurring_order/recurring_order_section_header.dart deleted file mode 100644 index 85326cb6..00000000 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/recurring_order/recurring_order_section_header.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'package:design_system/design_system.dart'; -import 'package:flutter/material.dart'; - -/// A header widget for sections in the recurring order form. -class RecurringOrderSectionHeader extends StatelessWidget { - /// Creates a [RecurringOrderSectionHeader]. - const RecurringOrderSectionHeader({ - required this.title, - this.actionLabel, - this.onAction, - super.key, - }); - - /// The title text for the section. - final String title; - - /// Optional label for an action button on the right. - final String? actionLabel; - - /// Callback when the action button is tapped. - final VoidCallback? onAction; - - @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(title, style: UiTypography.headline4m.textPrimary), - if (actionLabel != null && onAction != null) - TextButton( - onPressed: onAction, - style: TextButton.styleFrom( - padding: EdgeInsets.zero, - minimumSize: Size.zero, - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(UiIcons.add, size: 16, color: UiColors.primary), - const SizedBox(width: UiConstants.space2), - Text( - actionLabel!, - style: UiTypography.body2m.primary, - ), - ], - ), - ), - ], - ); - } -} diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/recurring_order/recurring_order_success_view.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/recurring_order/recurring_order_success_view.dart deleted file mode 100644 index 3739c5ad..00000000 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/recurring_order/recurring_order_success_view.dart +++ /dev/null @@ -1,104 +0,0 @@ -import 'package:design_system/design_system.dart'; -import 'package:flutter/material.dart'; - -/// A view to display when a recurring order has been successfully created. -class RecurringOrderSuccessView extends StatelessWidget { - /// Creates a [RecurringOrderSuccessView]. - const RecurringOrderSuccessView({ - required this.title, - required this.message, - required this.buttonLabel, - required this.onDone, - super.key, - }); - - /// The title of the success message. - final String title; - - /// The body of the success message. - final String message; - - /// Label for the completion button. - final String buttonLabel; - - /// Callback when the completion button is tapped. - final VoidCallback onDone; - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Container( - width: double.infinity, - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [UiColors.primary, UiColors.buttonPrimaryHover], - ), - ), - child: SafeArea( - child: Center( - child: Container( - margin: const EdgeInsets.symmetric(horizontal: UiConstants.space10), - padding: const EdgeInsets.all(UiConstants.space8), - decoration: BoxDecoration( - color: UiColors.white, - borderRadius: UiConstants.radiusLg * 1.5, - boxShadow: [ - BoxShadow( - color: UiColors.black.withValues(alpha: 0.2), - blurRadius: 20, - offset: const Offset(0, UiConstants.space2 + 2), - ), - ], - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: UiConstants.space16, - height: UiConstants.space16, - decoration: const BoxDecoration( - color: UiColors.accent, - shape: BoxShape.circle, - ), - child: const Center( - child: Icon( - UiIcons.check, - color: UiColors.black, - size: UiConstants.space8, - ), - ), - ), - const SizedBox(height: UiConstants.space6), - Text( - title, - style: UiTypography.headline2m.textPrimary, - textAlign: TextAlign.center, - ), - const SizedBox(height: UiConstants.space3), - Text( - message, - textAlign: TextAlign.center, - style: UiTypography.body2r.textSecondary.copyWith( - height: 1.5, - ), - ), - const SizedBox(height: UiConstants.space8), - SizedBox( - width: double.infinity, - child: UiButton.primary( - text: buttonLabel, - onPressed: onDone, - size: UiButtonSize.large, - ), - ), - ], - ), - ), - ), - ), - ), - ); - } -} diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/recurring_order/recurring_order_view.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/recurring_order/recurring_order_view.dart deleted file mode 100644 index 3265e800..00000000 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/recurring_order/recurring_order_view.dart +++ /dev/null @@ -1,457 +0,0 @@ -import 'package:core_localization/core_localization.dart'; -import 'package:krow_domain/krow_domain.dart' show Vendor; -import 'package:design_system/design_system.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_modular/flutter_modular.dart'; -import 'package:krow_core/core.dart'; -import '../../blocs/recurring_order/recurring_order_bloc.dart'; -import '../../blocs/recurring_order/recurring_order_event.dart'; -import '../../blocs/recurring_order/recurring_order_state.dart'; -import 'recurring_order_date_picker.dart'; -import 'recurring_order_event_name_input.dart'; -import 'recurring_order_header.dart'; -import 'recurring_order_position_card.dart'; -import 'recurring_order_section_header.dart'; -import 'recurring_order_success_view.dart'; - -/// The main content of the Recurring Order page. -class RecurringOrderView extends StatelessWidget { - /// Creates a [RecurringOrderView]. - const RecurringOrderView({super.key}); - - DateTime _firstRecurringShiftDate( - DateTime startDate, - DateTime endDate, - List recurringDays, - ) { - final DateTime start = DateTime(startDate.year, startDate.month, startDate.day); - final DateTime end = DateTime(endDate.year, endDate.month, endDate.day); - final Set selected = recurringDays.toSet(); - for (DateTime day = start; !day.isAfter(end); day = day.add(const Duration(days: 1))) { - if (selected.contains(_weekdayLabel(day))) { - return day; - } - } - return start; - } - - String _weekdayLabel(DateTime date) { - switch (date.weekday) { - case DateTime.monday: - return 'MON'; - case DateTime.tuesday: - return 'TUE'; - case DateTime.wednesday: - return 'WED'; - case DateTime.thursday: - return 'THU'; - case DateTime.friday: - return 'FRI'; - case DateTime.saturday: - return 'SAT'; - case DateTime.sunday: - return 'SUN'; - default: - return 'SUN'; - } - } - - @override - Widget build(BuildContext context) { - final TranslationsClientCreateOrderRecurringEn labels = - t.client_create_order.recurring; - final TranslationsClientCreateOrderOneTimeEn oneTimeLabels = - t.client_create_order.one_time; - - return BlocConsumer( - listener: (BuildContext context, RecurringOrderState state) { - if (state.status == RecurringOrderStatus.failure && - state.errorMessage != null) { - final String message = state.errorMessage == 'placeholder' - ? labels.placeholder - : translateErrorKey(state.errorMessage!); - UiSnackbar.show( - context, - message: message, - type: UiSnackbarType.error, - margin: const EdgeInsets.only(bottom: 140, left: 16, right: 16), - ); - } - }, - builder: (BuildContext context, RecurringOrderState state) { - if (state.status == RecurringOrderStatus.success) { - final DateTime maxEndDate = - state.startDate.add(const Duration(days: 29)); - final DateTime effectiveEndDate = - state.endDate.isAfter(maxEndDate) ? maxEndDate : state.endDate; - final DateTime initialDate = _firstRecurringShiftDate( - state.startDate, - effectiveEndDate, - state.recurringDays, - ); - return RecurringOrderSuccessView( - title: labels.title, - message: labels.subtitle, - buttonLabel: oneTimeLabels.back_to_orders, - onDone: () => Modular.to.pushNamedAndRemoveUntil( - ClientPaths.orders, - (_) => false, - arguments: { - 'initialDate': initialDate.toIso8601String(), - }, - ), - ); - } - - if (state.vendors.isEmpty && - state.status != RecurringOrderStatus.loading) { - return Scaffold( - body: Column( - children: [ - RecurringOrderHeader( - title: labels.title, - subtitle: labels.subtitle, - onBack: () => Modular.to.navigate(ClientPaths.createOrder), - ), - Expanded( - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon( - UiIcons.search, - size: 64, - color: UiColors.iconInactive, - ), - const SizedBox(height: UiConstants.space4), - Text( - 'No Vendors Available', - style: UiTypography.headline3m.textPrimary, - ), - const SizedBox(height: UiConstants.space2), - Text( - 'There are no staffing vendors associated with your account.', - style: UiTypography.body2r.textSecondary, - textAlign: TextAlign.center, - ), - ], - ), - ), - ), - ], - ), - ); - } - - return Scaffold( - body: Column( - children: [ - RecurringOrderHeader( - title: labels.title, - subtitle: labels.subtitle, - onBack: () => Modular.to.navigate(ClientPaths.createOrder), - ), - Expanded( - child: Stack( - children: [ - _RecurringOrderForm(state: state), - if (state.status == RecurringOrderStatus.loading) - const Center(child: CircularProgressIndicator()), - ], - ), - ), - _BottomActionButton( - label: state.status == RecurringOrderStatus.loading - ? oneTimeLabels.creating - : oneTimeLabels.create_order, - isLoading: state.status == RecurringOrderStatus.loading, - onPressed: state.isValid - ? () => BlocProvider.of( - context, - ).add(const RecurringOrderSubmitted()) - : null, - ), - ], - ), - ); - }, - ); - } -} - -class _RecurringOrderForm extends StatelessWidget { - const _RecurringOrderForm({required this.state}); - final RecurringOrderState state; - - @override - Widget build(BuildContext context) { - final TranslationsClientCreateOrderRecurringEn labels = - t.client_create_order.recurring; - final TranslationsClientCreateOrderOneTimeEn oneTimeLabels = - t.client_create_order.one_time; - - return ListView( - padding: const EdgeInsets.all(UiConstants.space5), - children: [ - Text( - labels.title, - style: UiTypography.headline3m.textPrimary, - ), - const SizedBox(height: UiConstants.space4), - - RecurringOrderEventNameInput( - label: 'ORDER NAME', - value: state.eventName, - onChanged: (String value) => BlocProvider.of( - context, - ).add(RecurringOrderEventNameChanged(value)), - ), - const SizedBox(height: UiConstants.space4), - - // Vendor Selection - Text('SELECT VENDOR', style: UiTypography.footnote2r.textSecondary), - const SizedBox(height: UiConstants.space2), - Container( - padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3), - height: 48, - decoration: BoxDecoration( - color: UiColors.white, - borderRadius: UiConstants.radiusMd, - border: Border.all(color: UiColors.border), - ), - child: DropdownButtonHideUnderline( - child: DropdownButton( - isExpanded: true, - value: state.selectedVendor, - icon: const Icon( - UiIcons.chevronDown, - size: 18, - color: UiColors.iconSecondary, - ), - onChanged: (Vendor? vendor) { - if (vendor != null) { - BlocProvider.of( - context, - ).add(RecurringOrderVendorChanged(vendor)); - } - }, - items: state.vendors.map((Vendor vendor) { - return DropdownMenuItem( - value: vendor, - child: Text( - vendor.name, - style: UiTypography.body2m.textPrimary, - ), - ); - }).toList(), - ), - ), - ), - const SizedBox(height: UiConstants.space4), - - RecurringOrderDatePicker( - label: 'Start Date', - value: state.startDate, - onChanged: (DateTime date) => BlocProvider.of( - context, - ).add(RecurringOrderStartDateChanged(date)), - ), - const SizedBox(height: UiConstants.space4), - - RecurringOrderDatePicker( - label: 'End Date', - value: state.endDate, - onChanged: (DateTime date) => BlocProvider.of( - context, - ).add(RecurringOrderEndDateChanged(date)), - ), - const SizedBox(height: UiConstants.space4), - - Text('Recurring Days', style: UiTypography.footnote2r.textSecondary), - const SizedBox(height: UiConstants.space2), - _RecurringDaysSelector( - selectedDays: state.recurringDays, - onToggle: (int dayIndex) => BlocProvider.of( - context, - ).add(RecurringOrderDayToggled(dayIndex)), - ), - const SizedBox(height: UiConstants.space4), - - Text('HUB', style: UiTypography.footnote2r.textSecondary), - const SizedBox(height: UiConstants.space2), - Container( - padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3), - height: 48, - decoration: BoxDecoration( - color: UiColors.white, - borderRadius: UiConstants.radiusMd, - border: Border.all(color: UiColors.border), - ), - child: DropdownButtonHideUnderline( - child: DropdownButton( - isExpanded: true, - value: state.selectedHub, - icon: const Icon( - UiIcons.chevronDown, - size: 18, - color: UiColors.iconSecondary, - ), - onChanged: (RecurringOrderHubOption? hub) { - if (hub != null) { - BlocProvider.of( - context, - ).add(RecurringOrderHubChanged(hub)); - } - }, - items: state.hubs.map((RecurringOrderHubOption hub) { - return DropdownMenuItem( - value: hub, - child: Text( - hub.name, - style: UiTypography.body2m.textPrimary, - ), - ); - }).toList(), - ), - ), - ), - const SizedBox(height: UiConstants.space6), - - RecurringOrderSectionHeader( - title: oneTimeLabels.positions_title, - actionLabel: oneTimeLabels.add_position, - onAction: () => BlocProvider.of( - context, - ).add(const RecurringOrderPositionAdded()), - ), - const SizedBox(height: UiConstants.space3), - - // Positions List - ...state.positions.asMap().entries.map(( - MapEntry entry, - ) { - final int index = entry.key; - final RecurringOrderPosition position = entry.value; - return Padding( - padding: const EdgeInsets.only(bottom: UiConstants.space3), - child: RecurringOrderPositionCard( - index: index, - position: position, - isRemovable: state.positions.length > 1, - positionLabel: oneTimeLabels.positions_title, - roleLabel: oneTimeLabels.select_role, - workersLabel: oneTimeLabels.workers_label, - startLabel: oneTimeLabels.start_label, - endLabel: oneTimeLabels.end_label, - lunchLabel: oneTimeLabels.lunch_break_label, - roles: state.roles, - onUpdated: (RecurringOrderPosition updated) { - BlocProvider.of( - context, - ).add(RecurringOrderPositionUpdated(index, updated)); - }, - onRemoved: () { - BlocProvider.of( - context, - ).add(RecurringOrderPositionRemoved(index)); - }, - ), - ); - }), - ], - ); - } -} - -class _RecurringDaysSelector extends StatelessWidget { - const _RecurringDaysSelector({ - required this.selectedDays, - required this.onToggle, - }); - - final List selectedDays; - final ValueChanged onToggle; - - @override - Widget build(BuildContext context) { - const List labelsShort = [ - 'S', - 'M', - 'T', - 'W', - 'T', - 'F', - 'S', - ]; - const List labelsLong = [ - 'SUN', - 'MON', - 'TUE', - 'WED', - 'THU', - 'FRI', - 'SAT', - ]; - return Wrap( - spacing: UiConstants.space2, - children: List.generate(labelsShort.length, (int index) { - final bool isSelected = selectedDays.contains(labelsLong[index]); - return GestureDetector( - onTap: () => onToggle(index), - child: Container( - width: 36, - height: 36, - decoration: BoxDecoration( - color: isSelected ? UiColors.primary : UiColors.white, - shape: BoxShape.circle, - border: Border.all(color: UiColors.border), - ), - alignment: Alignment.center, - child: Text( - labelsShort[index], - style: UiTypography.body2m.copyWith( - color: isSelected ? UiColors.white : UiColors.textSecondary, - ), - ), - ), - ); - }), - ); - } -} - -class _BottomActionButton extends StatelessWidget { - const _BottomActionButton({ - required this.label, - required this.onPressed, - this.isLoading = false, - }); - final String label; - final VoidCallback? onPressed; - final bool isLoading; - - @override - Widget build(BuildContext context) { - return Container( - padding: EdgeInsets.only( - left: UiConstants.space5, - right: UiConstants.space5, - top: UiConstants.space5, - bottom: MediaQuery.of(context).padding.bottom + UiConstants.space5, - ), - decoration: const BoxDecoration( - color: UiColors.white, - border: Border(top: BorderSide(color: UiColors.border)), - ), - child: SizedBox( - width: double.infinity, - child: UiButton.primary( - text: label, - onPressed: isLoading ? null : onPressed, - size: UiButtonSize.large, - ), - ), - ); - } -} diff --git a/apps/mobile/packages/features/client/orders/create_order/pubspec.yaml b/apps/mobile/packages/features/client/orders/create_order/pubspec.yaml index 86955f24..20a70779 100644 --- a/apps/mobile/packages/features/client/orders/create_order/pubspec.yaml +++ b/apps/mobile/packages/features/client/orders/create_order/pubspec.yaml @@ -24,6 +24,8 @@ dependencies: path: ../../../../core krow_data_connect: path: ../../../../data_connect + client_orders_common: + path: ../orders_common firebase_data_connect: ^0.2.2+2 firebase_auth: ^6.1.4