diff --git a/apps/mobile/packages/features/client/orders/orders_common/lib/client_orders_common.dart b/apps/mobile/packages/features/client/orders/orders_common/lib/client_orders_common.dart index 410be326..cec30ce5 100644 --- a/apps/mobile/packages/features/client/orders/orders_common/lib/client_orders_common.dart +++ b/apps/mobile/packages/features/client/orders/orders_common/lib/client_orders_common.dart @@ -1,10 +1,13 @@ // UI Models export 'src/presentation/widgets/order_ui_models.dart'; +// Shared Widgets +export 'src/presentation/widgets/order_bottom_action_button.dart'; + // One Time Order Widgets export 'src/presentation/widgets/one_time_order/one_time_order_date_picker.dart'; export 'src/presentation/widgets/one_time_order/one_time_order_event_name_input.dart'; -export 'src/presentation/widgets/one_time_order/one_time_order_header.dart'; +export 'src/presentation/widgets/one_time_order/one_time_order_form.dart'; export 'src/presentation/widgets/one_time_order/one_time_order_location_input.dart'; export 'src/presentation/widgets/one_time_order/one_time_order_position_card.dart'; export 'src/presentation/widgets/one_time_order/one_time_order_section_header.dart'; @@ -13,8 +16,9 @@ export 'src/presentation/widgets/one_time_order/one_time_order_view.dart'; // Permanent Order Widgets export 'src/presentation/widgets/permanent_order/permanent_order_date_picker.dart'; +export 'src/presentation/widgets/permanent_order/permanent_order_days_selector.dart'; export 'src/presentation/widgets/permanent_order/permanent_order_event_name_input.dart'; -export 'src/presentation/widgets/permanent_order/permanent_order_header.dart'; +export 'src/presentation/widgets/permanent_order/permanent_order_form.dart'; export 'src/presentation/widgets/permanent_order/permanent_order_position_card.dart'; export 'src/presentation/widgets/permanent_order/permanent_order_section_header.dart'; export 'src/presentation/widgets/permanent_order/permanent_order_success_view.dart'; @@ -22,8 +26,9 @@ export 'src/presentation/widgets/permanent_order/permanent_order_view.dart'; // Recurring Order Widgets export 'src/presentation/widgets/recurring_order/recurring_order_date_picker.dart'; +export 'src/presentation/widgets/recurring_order/recurring_order_days_selector.dart'; export 'src/presentation/widgets/recurring_order/recurring_order_event_name_input.dart'; -export 'src/presentation/widgets/recurring_order/recurring_order_header.dart'; +export 'src/presentation/widgets/recurring_order/recurring_order_form.dart'; export 'src/presentation/widgets/recurring_order/recurring_order_position_card.dart'; export 'src/presentation/widgets/recurring_order/recurring_order_section_header.dart'; export 'src/presentation/widgets/recurring_order/recurring_order_success_view.dart'; diff --git a/apps/mobile/packages/features/client/orders/orders_common/lib/src/presentation/widgets/one_time_order/one_time_order_form.dart b/apps/mobile/packages/features/client/orders/orders_common/lib/src/presentation/widgets/one_time_order/one_time_order_form.dart new file mode 100644 index 00000000..a21092a0 --- /dev/null +++ b/apps/mobile/packages/features/client/orders/orders_common/lib/src/presentation/widgets/one_time_order/one_time_order_form.dart @@ -0,0 +1,242 @@ +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 '../hub_manager_selector.dart'; +import '../order_ui_models.dart'; +import 'one_time_order_date_picker.dart'; +import 'one_time_order_event_name_input.dart'; +import 'one_time_order_position_card.dart'; +import 'one_time_order_section_header.dart'; + +/// The scrollable form body for the one-time order creation flow. +/// +/// Displays fields for event name, vendor selection, date, hub, hub manager, +/// and a dynamic list of position cards. +class OneTimeOrderForm extends StatelessWidget { + /// Creates a [OneTimeOrderForm]. + const OneTimeOrderForm({ + required this.eventName, + required this.selectedVendor, + required this.vendors, + required this.date, + required this.selectedHub, + required this.hubs, + required this.selectedHubManager, + required this.hubManagers, + required this.positions, + required this.roles, + required this.onEventNameChanged, + required this.onVendorChanged, + required this.onDateChanged, + required this.onHubChanged, + required this.onHubManagerChanged, + required this.onPositionAdded, + required this.onPositionUpdated, + required this.onPositionRemoved, + super.key, + }); + + /// The current event name value. + final String eventName; + + /// The currently selected vendor, if any. + final Vendor? selectedVendor; + + /// The list of available vendors to choose from. + final List vendors; + + /// The selected date for the one-time order. + final DateTime date; + + /// The currently selected hub, if any. + final OrderHubUiModel? selectedHub; + + /// The list of available hubs to choose from. + final List hubs; + + /// The currently selected hub manager, if any. + final OrderManagerUiModel? selectedHubManager; + + /// The list of available hub managers for the selected hub. + final List hubManagers; + + /// The list of position entries in the order. + final List positions; + + /// The list of available roles for position assignment. + final List roles; + + /// Called when the event name text changes. + final ValueChanged onEventNameChanged; + + /// Called when a vendor is selected. + final ValueChanged onVendorChanged; + + /// Called when the date is changed. + final ValueChanged onDateChanged; + + /// Called when a hub is selected. + final ValueChanged onHubChanged; + + /// Called when a hub manager is selected or cleared. + final ValueChanged onHubManagerChanged; + + /// Called when the user requests adding a new position. + final VoidCallback onPositionAdded; + + /// Called when a position at [index] is updated with new values. + final void Function(int index, OrderPositionUiModel position) + onPositionUpdated; + + /// Called when a position at [index] is removed. + final void Function(int index) onPositionRemoved; + + @override + Widget build(BuildContext context) { + final TranslationsClientCreateOrderOneTimeEn labels = + t.client_create_order.one_time; + + return ListView( + padding: const EdgeInsets.all(UiConstants.space5), + children: [ + OneTimeOrderEventNameInput( + label: 'ORDER NAME', + value: eventName, + onChanged: onEventNameChanged, + ), + 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: selectedVendor, + icon: const Icon( + UiIcons.chevronDown, + size: 18, + color: UiColors.iconSecondary, + ), + onChanged: (Vendor? vendor) { + if (vendor != null) { + onVendorChanged(vendor); + } + }, + items: 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: date, + onChanged: onDateChanged, + ), + 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: selectedHub, + icon: const Icon( + UiIcons.chevronDown, + size: 18, + color: UiColors.iconSecondary, + ), + onChanged: (OrderHubUiModel? hub) { + if (hub != null) { + onHubChanged(hub); + } + }, + items: hubs.map((OrderHubUiModel hub) { + return DropdownMenuItem( + value: hub, + child: Text(hub.name, style: UiTypography.body2m.textPrimary), + ); + }).toList(), + ), + ), + ), + const SizedBox(height: UiConstants.space4), + + HubManagerSelector( + label: labels.hub_manager_label, + description: labels.hub_manager_desc, + hintText: labels.hub_manager_hint, + noManagersText: labels.hub_manager_empty, + noneText: labels.hub_manager_none, + managers: hubManagers, + selectedManager: selectedHubManager, + onChanged: onHubManagerChanged, + ), + const SizedBox(height: UiConstants.space6), + + OneTimeOrderSectionHeader( + title: labels.positions_title, + actionLabel: labels.add_position, + onAction: onPositionAdded, + ), + const SizedBox(height: UiConstants.space3), + + // Positions List + ...positions.asMap().entries.map(( + MapEntry entry, + ) { + final int index = entry.key; + final OrderPositionUiModel position = entry.value; + return Padding( + padding: const EdgeInsets.only(bottom: UiConstants.space3), + child: OneTimeOrderPositionCard( + index: index, + position: position, + isRemovable: 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: roles, + onUpdated: (OrderPositionUiModel updated) { + onPositionUpdated(index, updated); + }, + onRemoved: () { + onPositionRemoved(index); + }, + ), + ); + }), + ], + ); + } +} diff --git a/apps/mobile/packages/features/client/orders/orders_common/lib/src/presentation/widgets/one_time_order/one_time_order_header.dart b/apps/mobile/packages/features/client/orders/orders_common/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/orders_common/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/orders_common/lib/src/presentation/widgets/one_time_order/one_time_order_view.dart b/apps/mobile/packages/features/client/orders/orders_common/lib/src/presentation/widgets/one_time_order/one_time_order_view.dart index 97d0bb68..3f2050f5 100644 --- a/apps/mobile/packages/features/client/orders/orders_common/lib/src/presentation/widgets/one_time_order/one_time_order_view.dart +++ b/apps/mobile/packages/features/client/orders/orders_common/lib/src/presentation/widgets/one_time_order/one_time_order_view.dart @@ -2,13 +2,10 @@ 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 '../order_bottom_action_button.dart'; import '../order_ui_models.dart'; -import '../hub_manager_selector.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_form.dart'; import 'one_time_order_success_view.dart'; /// The main content of the One-Time Order page as a dumb widget. @@ -98,322 +95,92 @@ class OneTimeOrderView extends StatelessWidget { ); } + return Scaffold( + appBar: UiAppBar( + showBackButton: true, + onLeadingPressed: onBack, + title: title ?? labels.title, + subtitle: subtitle ?? labels.subtitle, + ), + body: _buildBody(context, labels), + ); + } + + /// Builds the main body of the One-Time Order page, showing either the form or a loading indicator. + Widget _buildBody( + BuildContext context, + TranslationsClientCreateOrderOneTimeEn labels, + ) { if (vendors.isEmpty && status != OrderFormStatus.loading) { - return Scaffold( - body: Column( - children: [ - OneTimeOrderHeader( - title: title ?? labels.title, - subtitle: subtitle ?? labels.subtitle, - onBack: onBack, - ), - 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 Column( + children: [ + 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: title ?? labels.title, - subtitle: subtitle ?? labels.subtitle, - onBack: onBack, - ), - Expanded( - child: Stack( - children: [ - _OneTimeOrderForm( - eventName: eventName, - selectedVendor: selectedVendor, - vendors: vendors, - date: date, - selectedHub: selectedHub, - hubs: hubs, - selectedHubManager: selectedHubManager, - hubManagers: hubManagers, - positions: positions, - roles: roles, - onEventNameChanged: onEventNameChanged, - onVendorChanged: onVendorChanged, - onDateChanged: onDateChanged, - onHubChanged: onHubChanged, - onHubManagerChanged: onHubManagerChanged, - onPositionAdded: onPositionAdded, - onPositionUpdated: onPositionUpdated, - onPositionRemoved: onPositionRemoved, - ), - if (status == OrderFormStatus.loading) - const Center(child: CircularProgressIndicator()), - ], - ), - ), - _BottomActionButton( - label: status == OrderFormStatus.loading - ? labels.creating - : labels.create_order, - isLoading: status == OrderFormStatus.loading, - onPressed: isValid ? onSubmit : null, - ), - ], - ), - ); - } -} - -class _OneTimeOrderForm extends StatelessWidget { - const _OneTimeOrderForm({ - required this.eventName, - required this.selectedVendor, - required this.vendors, - required this.date, - required this.selectedHub, - required this.hubs, - required this.selectedHubManager, - required this.hubManagers, - required this.positions, - required this.roles, - required this.onEventNameChanged, - required this.onVendorChanged, - required this.onDateChanged, - required this.onHubChanged, - required this.onHubManagerChanged, - required this.onPositionAdded, - required this.onPositionUpdated, - required this.onPositionRemoved, - }); - - final String eventName; - final Vendor? selectedVendor; - final List vendors; - final DateTime date; - final OrderHubUiModel? selectedHub; - final List hubs; - final OrderManagerUiModel? selectedHubManager; - final List hubManagers; - final List positions; - final List roles; - - final ValueChanged onEventNameChanged; - final ValueChanged onVendorChanged; - final ValueChanged onDateChanged; - final ValueChanged onHubChanged; - final ValueChanged onHubManagerChanged; - final VoidCallback onPositionAdded; - final void Function(int index, OrderPositionUiModel position) - onPositionUpdated; - final void Function(int index) onPositionRemoved; - - @override - Widget build(BuildContext context) { - final TranslationsClientCreateOrderOneTimeEn labels = - t.client_create_order.one_time; - - return ListView( - padding: const EdgeInsets.all(UiConstants.space5), + return Column( children: [ - Text( - labels.create_your_order, - style: UiTypography.headline3m.textPrimary, - ), - const SizedBox(height: UiConstants.space4), - - OneTimeOrderEventNameInput( - label: 'ORDER NAME', - value: eventName, - onChanged: onEventNameChanged, - ), - 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: selectedVendor, - icon: const Icon( - UiIcons.chevronDown, - size: 18, - color: UiColors.iconSecondary, + Expanded( + child: Stack( + children: [ + OneTimeOrderForm( + eventName: eventName, + selectedVendor: selectedVendor, + vendors: vendors, + date: date, + selectedHub: selectedHub, + hubs: hubs, + selectedHubManager: selectedHubManager, + hubManagers: hubManagers, + positions: positions, + roles: roles, + onEventNameChanged: onEventNameChanged, + onVendorChanged: onVendorChanged, + onDateChanged: onDateChanged, + onHubChanged: onHubChanged, + onHubManagerChanged: onHubManagerChanged, + onPositionAdded: onPositionAdded, + onPositionUpdated: onPositionUpdated, + onPositionRemoved: onPositionRemoved, ), - onChanged: (Vendor? vendor) { - if (vendor != null) { - onVendorChanged(vendor); - } - }, - items: vendors.map((Vendor vendor) { - return DropdownMenuItem( - value: vendor, - child: Text( - vendor.name, - style: UiTypography.body2m.textPrimary, - ), - ); - }).toList(), - ), + if (status == OrderFormStatus.loading) + const Center(child: CircularProgressIndicator()), + ], ), ), - const SizedBox(height: UiConstants.space4), - - OneTimeOrderDatePicker( - label: labels.date_label, - value: date, - onChanged: onDateChanged, + OrderBottomActionButton( + label: status == OrderFormStatus.loading + ? labels.creating + : labels.create_order, + isLoading: status == OrderFormStatus.loading, + onPressed: isValid ? onSubmit : null, ), - 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: selectedHub, - icon: const Icon( - UiIcons.chevronDown, - size: 18, - color: UiColors.iconSecondary, - ), - onChanged: (OrderHubUiModel? hub) { - if (hub != null) { - onHubChanged(hub); - } - }, - items: hubs.map((OrderHubUiModel hub) { - return DropdownMenuItem( - value: hub, - child: Text(hub.name, style: UiTypography.body2m.textPrimary), - ); - }).toList(), - ), - ), - ), - const SizedBox(height: UiConstants.space4), - - HubManagerSelector( - label: labels.hub_manager_label, - description: labels.hub_manager_desc, - hintText: labels.hub_manager_hint, - noManagersText: labels.hub_manager_empty, - noneText: labels.hub_manager_none, - managers: hubManagers, - selectedManager: selectedHubManager, - onChanged: onHubManagerChanged, - ), - const SizedBox(height: UiConstants.space6), - - OneTimeOrderSectionHeader( - title: labels.positions_title, - actionLabel: labels.add_position, - onAction: onPositionAdded, - ), - const SizedBox(height: UiConstants.space3), - - // Positions List - ...positions.asMap().entries.map(( - MapEntry entry, - ) { - final int index = entry.key; - final OrderPositionUiModel position = entry.value; - return Padding( - padding: const EdgeInsets.only(bottom: UiConstants.space3), - child: OneTimeOrderPositionCard( - index: index, - position: position, - isRemovable: 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: roles, - onUpdated: (OrderPositionUiModel updated) { - onPositionUpdated(index, updated); - }, - onRemoved: () { - onPositionRemoved(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/orders_common/lib/src/presentation/widgets/order_bottom_action_button.dart b/apps/mobile/packages/features/client/orders/orders_common/lib/src/presentation/widgets/order_bottom_action_button.dart new file mode 100644 index 00000000..03f7ffd8 --- /dev/null +++ b/apps/mobile/packages/features/client/orders/orders_common/lib/src/presentation/widgets/order_bottom_action_button.dart @@ -0,0 +1,49 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// A bottom-pinned action button used across all order type views. +/// +/// Renders a full-width primary button with safe-area padding at the bottom +/// and a top border separator. Disables the button while [isLoading] is true. +class OrderBottomActionButton extends StatelessWidget { + /// Creates an [OrderBottomActionButton]. + const OrderBottomActionButton({ + required this.label, + required this.onPressed, + this.isLoading = false, + super.key, + }); + + /// The text displayed on the button. + final String label; + + /// Callback invoked when the button is pressed. Pass `null` to disable. + final VoidCallback? onPressed; + + /// Whether the form is currently submitting. Disables the button when true. + 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, width: 0.5)), + ), + 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/orders_common/lib/src/presentation/widgets/permanent_order/permanent_order_days_selector.dart b/apps/mobile/packages/features/client/orders/orders_common/lib/src/presentation/widgets/permanent_order/permanent_order_days_selector.dart new file mode 100644 index 00000000..37fbd915 --- /dev/null +++ b/apps/mobile/packages/features/client/orders/orders_common/lib/src/presentation/widgets/permanent_order/permanent_order_days_selector.dart @@ -0,0 +1,68 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// A horizontal row of circular day-of-week toggle buttons for permanent orders. +/// +/// Displays seven circles labeled S, M, T, W, T, F, S representing the days +/// of the week. Selected days are highlighted with the primary color. +class PermanentOrderDaysSelector extends StatelessWidget { + /// Creates a [PermanentOrderDaysSelector]. + const PermanentOrderDaysSelector({ + required this.selectedDays, + required this.onToggle, + super.key, + }); + + /// The list of currently selected day abbreviations (e.g. 'MON', 'TUE'). + final List selectedDays; + + /// Called when a day circle is tapped, with the day index (0 = Sunday). + 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, + ), + ), + ), + ); + }), + ); + } +} diff --git a/apps/mobile/packages/features/client/orders/orders_common/lib/src/presentation/widgets/permanent_order/permanent_order_form.dart b/apps/mobile/packages/features/client/orders/orders_common/lib/src/presentation/widgets/permanent_order/permanent_order_form.dart new file mode 100644 index 00000000..a9185ce3 --- /dev/null +++ b/apps/mobile/packages/features/client/orders/orders_common/lib/src/presentation/widgets/permanent_order/permanent_order_form.dart @@ -0,0 +1,271 @@ +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' show Vendor; + +import '../hub_manager_selector.dart'; +import '../order_ui_models.dart'; +import 'permanent_order_date_picker.dart'; +import 'permanent_order_days_selector.dart'; +import 'permanent_order_event_name_input.dart'; +import 'permanent_order_position_card.dart'; +import 'permanent_order_section_header.dart'; + +/// The scrollable form body for the permanent order creation flow. +/// +/// Displays fields for event name, vendor selection, start date, +/// permanent day toggles, hub, hub manager, and a dynamic list of +/// position cards. +class PermanentOrderForm extends StatelessWidget { + /// Creates a [PermanentOrderForm]. + const PermanentOrderForm({ + required this.eventName, + required this.selectedVendor, + required this.vendors, + required this.startDate, + required this.permanentDays, + required this.selectedHub, + required this.hubs, + required this.positions, + required this.roles, + required this.onEventNameChanged, + required this.onVendorChanged, + required this.onStartDateChanged, + required this.onDayToggled, + required this.onHubChanged, + required this.onHubManagerChanged, + required this.onPositionAdded, + required this.onPositionUpdated, + required this.onPositionRemoved, + required this.hubManagers, + required this.selectedHubManager, + super.key, + }); + + /// The current event name value. + final String eventName; + + /// The currently selected vendor, if any. + final Vendor? selectedVendor; + + /// The list of available vendors to choose from. + final List vendors; + + /// The start date for the permanent order. + final DateTime startDate; + + /// The list of selected permanent day abbreviations (e.g. 'MON', 'TUE'). + final List permanentDays; + + /// The currently selected hub, if any. + final OrderHubUiModel? selectedHub; + + /// The list of available hubs to choose from. + final List hubs; + + /// The list of position entries in the order. + final List positions; + + /// The list of available roles for position assignment. + final List roles; + + /// Called when the event name text changes. + final ValueChanged onEventNameChanged; + + /// Called when a vendor is selected. + final ValueChanged onVendorChanged; + + /// Called when the start date is changed. + final ValueChanged onStartDateChanged; + + /// Called when a day-of-week toggle is tapped, with the day index (0=Sun). + final ValueChanged onDayToggled; + + /// Called when a hub is selected. + final ValueChanged onHubChanged; + + /// Called when a hub manager is selected or cleared. + final ValueChanged onHubManagerChanged; + + /// Called when the user requests adding a new position. + final VoidCallback onPositionAdded; + + /// Called when a position at [index] is updated with new values. + final void Function(int index, OrderPositionUiModel position) + onPositionUpdated; + + /// Called when a position at [index] is removed. + final void Function(int index) onPositionRemoved; + + /// The list of available hub managers for the selected hub. + final List hubManagers; + + /// The currently selected hub manager, if any. + final OrderManagerUiModel? selectedHubManager; + + @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: eventName, + onChanged: onEventNameChanged, + ), + 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: selectedVendor, + icon: const Icon( + UiIcons.chevronDown, + size: 18, + color: UiColors.iconSecondary, + ), + onChanged: (Vendor? vendor) { + if (vendor != null) { + onVendorChanged(vendor); + } + }, + items: 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: startDate, + onChanged: onStartDateChanged, + ), + const SizedBox(height: UiConstants.space4), + + Text('Permanent Days', style: UiTypography.footnote2r.textSecondary), + const SizedBox(height: UiConstants.space2), + PermanentOrderDaysSelector( + selectedDays: permanentDays, + onToggle: onDayToggled, + ), + 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: selectedHub, + icon: const Icon( + UiIcons.chevronDown, + size: 18, + color: UiColors.iconSecondary, + ), + onChanged: (OrderHubUiModel? hub) { + if (hub != null) { + onHubChanged(hub); + } + }, + items: hubs.map((OrderHubUiModel hub) { + return DropdownMenuItem( + value: hub, + child: Text( + hub.name, + style: UiTypography.body2m.textPrimary, + ), + ); + }).toList(), + ), + ), + ), + const SizedBox(height: UiConstants.space4), + + HubManagerSelector( + label: oneTimeLabels.hub_manager_label, + description: oneTimeLabels.hub_manager_desc, + hintText: oneTimeLabels.hub_manager_hint, + noManagersText: oneTimeLabels.hub_manager_empty, + noneText: oneTimeLabels.hub_manager_none, + managers: hubManagers, + selectedManager: selectedHubManager, + onChanged: onHubManagerChanged, + ), + const SizedBox(height: UiConstants.space6), + + PermanentOrderSectionHeader( + title: oneTimeLabels.positions_title, + actionLabel: oneTimeLabels.add_position, + onAction: onPositionAdded, + ), + const SizedBox(height: UiConstants.space3), + + // Positions List + ...positions.asMap().entries.map(( + MapEntry entry, + ) { + final int index = entry.key; + final OrderPositionUiModel position = entry.value; + return Padding( + padding: const EdgeInsets.only(bottom: UiConstants.space3), + child: PermanentOrderPositionCard( + index: index, + position: position, + isRemovable: 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: roles, + onUpdated: (OrderPositionUiModel updated) { + onPositionUpdated(index, updated); + }, + onRemoved: () { + onPositionRemoved(index); + }, + ), + ); + }), + ], + ); + } +} diff --git a/apps/mobile/packages/features/client/orders/orders_common/lib/src/presentation/widgets/permanent_order/permanent_order_header.dart b/apps/mobile/packages/features/client/orders/orders_common/lib/src/presentation/widgets/permanent_order/permanent_order_header.dart deleted file mode 100644 index 8943f5f1..00000000 --- a/apps/mobile/packages/features/client/orders/orders_common/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/orders_common/lib/src/presentation/widgets/permanent_order/permanent_order_view.dart b/apps/mobile/packages/features/client/orders/orders_common/lib/src/presentation/widgets/permanent_order/permanent_order_view.dart index abcf7a20..8c1bbf80 100644 --- a/apps/mobile/packages/features/client/orders/orders_common/lib/src/presentation/widgets/permanent_order/permanent_order_view.dart +++ b/apps/mobile/packages/features/client/orders/orders_common/lib/src/presentation/widgets/permanent_order/permanent_order_view.dart @@ -2,13 +2,10 @@ 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' show Vendor; + +import '../order_bottom_action_button.dart'; import '../order_ui_models.dart'; -import '../hub_manager_selector.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_form.dart'; import 'permanent_order_success_view.dart'; /// The main content of the Permanent Order page. @@ -65,7 +62,8 @@ class PermanentOrderView extends StatelessWidget { final ValueChanged onHubChanged; final ValueChanged onHubManagerChanged; final VoidCallback onPositionAdded; - final void Function(int index, OrderPositionUiModel position) onPositionUpdated; + final void Function(int index, OrderPositionUiModel position) + onPositionUpdated; final void Function(int index) onPositionRemoved; final VoidCallback onSubmit; final VoidCallback onDone; @@ -98,398 +96,95 @@ class PermanentOrderView extends StatelessWidget { ); } + return Scaffold( + appBar: UiAppBar( + showBackButton: true, + onLeadingPressed: onBack, + title: labels.title, + subtitle: labels.subtitle, + ), + body: _buildBody(context, labels, oneTimeLabels), + ); + } + + /// Builds the main body of the Permanent Order page based on the current state. + Widget _buildBody( + BuildContext context, + TranslationsClientCreateOrderPermanentEn labels, + TranslationsClientCreateOrderOneTimeEn oneTimeLabels, + ) { if (vendors.isEmpty && status != OrderFormStatus.loading) { - return Scaffold( - body: Column( - children: [ - PermanentOrderHeader( - title: labels.title, - subtitle: labels.subtitle, - onBack: onBack, - ), - 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 Column( + children: [ + 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: onBack, - ), - Expanded( - child: Stack( - children: [ - _PermanentOrderForm( - eventName: eventName, - selectedVendor: selectedVendor, - vendors: vendors, - startDate: startDate, - permanentDays: permanentDays, - selectedHub: selectedHub, - hubs: hubs, - positions: positions, - roles: roles, - onEventNameChanged: onEventNameChanged, - onVendorChanged: onVendorChanged, - onStartDateChanged: onStartDateChanged, - onDayToggled: onDayToggled, - onHubChanged: onHubChanged, - onHubManagerChanged: onHubManagerChanged, - onPositionAdded: onPositionAdded, - onPositionUpdated: onPositionUpdated, - onPositionRemoved: onPositionRemoved, - hubManagers: hubManagers, - selectedHubManager: selectedHubManager, - ), - if (status == OrderFormStatus.loading) - const Center(child: CircularProgressIndicator()), - ], - ), - ), - _BottomActionButton( - label: status == OrderFormStatus.loading - ? oneTimeLabels.creating - : oneTimeLabels.create_order, - isLoading: status == OrderFormStatus.loading, - onPressed: isValid ? onSubmit : null, - ), - ], - ), - ); - } -} - -class _PermanentOrderForm extends StatelessWidget { - const _PermanentOrderForm({ - required this.eventName, - required this.selectedVendor, - required this.vendors, - required this.startDate, - required this.permanentDays, - required this.selectedHub, - required this.hubs, - required this.positions, - required this.roles, - required this.onEventNameChanged, - required this.onVendorChanged, - required this.onStartDateChanged, - required this.onDayToggled, - required this.onHubChanged, - required this.onHubManagerChanged, - required this.onPositionAdded, - required this.onPositionUpdated, - required this.onPositionRemoved, - required this.hubManagers, - required this.selectedHubManager, - }); - - final String eventName; - final Vendor? selectedVendor; - final List vendors; - final DateTime startDate; - final List permanentDays; - final OrderHubUiModel? selectedHub; - final List hubs; - final List positions; - final List roles; - - final ValueChanged onEventNameChanged; - final ValueChanged onVendorChanged; - final ValueChanged onStartDateChanged; - final ValueChanged onDayToggled; - final ValueChanged onHubChanged; - final ValueChanged onHubManagerChanged; - final VoidCallback onPositionAdded; - final void Function(int index, OrderPositionUiModel position) onPositionUpdated; - final void Function(int index) onPositionRemoved; - - final List hubManagers; - final OrderManagerUiModel? selectedHubManager; - - @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), + return Column( children: [ - Text( - labels.title, - style: UiTypography.headline3m.textPrimary, - ), - const SizedBox(height: UiConstants.space4), - - PermanentOrderEventNameInput( - label: 'ORDER NAME', - value: eventName, - onChanged: onEventNameChanged, - ), - 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: selectedVendor, - icon: const Icon( - UiIcons.chevronDown, - size: 18, - color: UiColors.iconSecondary, + Expanded( + child: Stack( + children: [ + PermanentOrderForm( + eventName: eventName, + selectedVendor: selectedVendor, + vendors: vendors, + startDate: startDate, + permanentDays: permanentDays, + selectedHub: selectedHub, + hubs: hubs, + positions: positions, + roles: roles, + onEventNameChanged: onEventNameChanged, + onVendorChanged: onVendorChanged, + onStartDateChanged: onStartDateChanged, + onDayToggled: onDayToggled, + onHubChanged: onHubChanged, + onHubManagerChanged: onHubManagerChanged, + onPositionAdded: onPositionAdded, + onPositionUpdated: onPositionUpdated, + onPositionRemoved: onPositionRemoved, + hubManagers: hubManagers, + selectedHubManager: selectedHubManager, ), - onChanged: (Vendor? vendor) { - if (vendor != null) { - onVendorChanged(vendor); - } - }, - items: vendors.map((Vendor vendor) { - return DropdownMenuItem( - value: vendor, - child: Text( - vendor.name, - style: UiTypography.body2m.textPrimary, - ), - ); - }).toList(), - ), + if (status == OrderFormStatus.loading) + const Center(child: CircularProgressIndicator()), + ], ), ), - const SizedBox(height: UiConstants.space4), - - PermanentOrderDatePicker( - label: 'Start Date', - value: startDate, - onChanged: onStartDateChanged, + OrderBottomActionButton( + label: status == OrderFormStatus.loading + ? oneTimeLabels.creating + : oneTimeLabels.create_order, + isLoading: status == OrderFormStatus.loading, + onPressed: isValid ? onSubmit : null, ), - const SizedBox(height: UiConstants.space4), - - Text('Permanent Days', style: UiTypography.footnote2r.textSecondary), - const SizedBox(height: UiConstants.space2), - _PermanentDaysSelector( - selectedDays: permanentDays, - onToggle: onDayToggled, - ), - 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: selectedHub, - icon: const Icon( - UiIcons.chevronDown, - size: 18, - color: UiColors.iconSecondary, - ), - onChanged: (OrderHubUiModel? hub) { - if (hub != null) { - onHubChanged(hub); - } - }, - items: hubs.map((OrderHubUiModel hub) { - return DropdownMenuItem( - value: hub, - child: Text( - hub.name, - style: UiTypography.body2m.textPrimary, - ), - ); - }).toList(), - ), - ), - ), - const SizedBox(height: UiConstants.space4), - - HubManagerSelector( - label: oneTimeLabels.hub_manager_label, - description: oneTimeLabels.hub_manager_desc, - hintText: oneTimeLabels.hub_manager_hint, - noManagersText: oneTimeLabels.hub_manager_empty, - noneText: oneTimeLabels.hub_manager_none, - managers: hubManagers, - selectedManager: selectedHubManager, - onChanged: onHubManagerChanged, - ), - const SizedBox(height: UiConstants.space6), - - PermanentOrderSectionHeader( - title: oneTimeLabels.positions_title, - actionLabel: oneTimeLabels.add_position, - onAction: onPositionAdded, - ), - const SizedBox(height: UiConstants.space3), - - // Positions List - ...positions.asMap().entries.map(( - MapEntry entry, - ) { - final int index = entry.key; - final OrderPositionUiModel position = entry.value; - return Padding( - padding: const EdgeInsets.only(bottom: UiConstants.space3), - child: PermanentOrderPositionCard( - index: index, - position: position, - isRemovable: 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: roles, - onUpdated: (OrderPositionUiModel updated) { - onPositionUpdated(index, updated); - }, - onRemoved: () { - onPositionRemoved(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/orders_common/lib/src/presentation/widgets/recurring_order/recurring_order_days_selector.dart b/apps/mobile/packages/features/client/orders/orders_common/lib/src/presentation/widgets/recurring_order/recurring_order_days_selector.dart new file mode 100644 index 00000000..08ce04c4 --- /dev/null +++ b/apps/mobile/packages/features/client/orders/orders_common/lib/src/presentation/widgets/recurring_order/recurring_order_days_selector.dart @@ -0,0 +1,68 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// A horizontal row of circular day-of-week toggle buttons for recurring orders. +/// +/// Displays seven circles labeled S, M, T, W, T, F, S representing the days +/// of the week. Selected days are highlighted with the primary color. +class RecurringOrderDaysSelector extends StatelessWidget { + /// Creates a [RecurringOrderDaysSelector]. + const RecurringOrderDaysSelector({ + required this.selectedDays, + required this.onToggle, + super.key, + }); + + /// The list of currently selected day abbreviations (e.g. 'MON', 'TUE'). + final List selectedDays; + + /// Called when a day circle is tapped, with the day index (0 = Sunday). + 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, + ), + ), + ), + ); + }), + ); + } +} diff --git a/apps/mobile/packages/features/client/orders/orders_common/lib/src/presentation/widgets/recurring_order/recurring_order_form.dart b/apps/mobile/packages/features/client/orders/orders_common/lib/src/presentation/widgets/recurring_order/recurring_order_form.dart new file mode 100644 index 00000000..7a0421d9 --- /dev/null +++ b/apps/mobile/packages/features/client/orders/orders_common/lib/src/presentation/widgets/recurring_order/recurring_order_form.dart @@ -0,0 +1,286 @@ +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' show Vendor; + +import '../hub_manager_selector.dart'; +import '../order_ui_models.dart'; +import 'recurring_order_date_picker.dart'; +import 'recurring_order_days_selector.dart'; +import 'recurring_order_event_name_input.dart'; +import 'recurring_order_position_card.dart'; +import 'recurring_order_section_header.dart'; + +/// The scrollable form body for the recurring order creation flow. +/// +/// Displays fields for event name, vendor selection, start/end dates, +/// recurring day toggles, hub, hub manager, and a dynamic list of +/// position cards. +class RecurringOrderForm extends StatelessWidget { + /// Creates a [RecurringOrderForm]. + const RecurringOrderForm({ + required this.eventName, + required this.selectedVendor, + required this.vendors, + required this.startDate, + required this.endDate, + required this.recurringDays, + required this.selectedHub, + required this.hubs, + required this.positions, + required this.roles, + required this.onEventNameChanged, + required this.onVendorChanged, + required this.onStartDateChanged, + required this.onEndDateChanged, + required this.onDayToggled, + required this.onHubChanged, + required this.onHubManagerChanged, + required this.onPositionAdded, + required this.onPositionUpdated, + required this.onPositionRemoved, + required this.hubManagers, + required this.selectedHubManager, + super.key, + }); + + /// The current event name value. + final String eventName; + + /// The currently selected vendor, if any. + final Vendor? selectedVendor; + + /// The list of available vendors to choose from. + final List vendors; + + /// The start date for the recurring period. + final DateTime startDate; + + /// The end date for the recurring period. + final DateTime endDate; + + /// The list of selected recurring day abbreviations (e.g. 'MON', 'TUE'). + final List recurringDays; + + /// The currently selected hub, if any. + final OrderHubUiModel? selectedHub; + + /// The list of available hubs to choose from. + final List hubs; + + /// The list of position entries in the order. + final List positions; + + /// The list of available roles for position assignment. + final List roles; + + /// Called when the event name text changes. + final ValueChanged onEventNameChanged; + + /// Called when a vendor is selected. + final ValueChanged onVendorChanged; + + /// Called when the start date is changed. + final ValueChanged onStartDateChanged; + + /// Called when the end date is changed. + final ValueChanged onEndDateChanged; + + /// Called when a day-of-week toggle is tapped, with the day index (0=Sun). + final ValueChanged onDayToggled; + + /// Called when a hub is selected. + final ValueChanged onHubChanged; + + /// Called when a hub manager is selected or cleared. + final ValueChanged onHubManagerChanged; + + /// Called when the user requests adding a new position. + final VoidCallback onPositionAdded; + + /// Called when a position at [index] is updated with new values. + final void Function(int index, OrderPositionUiModel position) + onPositionUpdated; + + /// Called when a position at [index] is removed. + final void Function(int index) onPositionRemoved; + + /// The list of available hub managers for the selected hub. + final List hubManagers; + + /// The currently selected hub manager, if any. + final OrderManagerUiModel? selectedHubManager; + + @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: eventName, + onChanged: onEventNameChanged, + ), + 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: selectedVendor, + icon: const Icon( + UiIcons.chevronDown, + size: 18, + color: UiColors.iconSecondary, + ), + onChanged: (Vendor? vendor) { + if (vendor != null) { + onVendorChanged(vendor); + } + }, + items: 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: startDate, + onChanged: onStartDateChanged, + ), + const SizedBox(height: UiConstants.space4), + + RecurringOrderDatePicker( + label: 'End Date', + value: endDate, + onChanged: onEndDateChanged, + ), + const SizedBox(height: UiConstants.space4), + + Text('Recurring Days', style: UiTypography.footnote2r.textSecondary), + const SizedBox(height: UiConstants.space2), + RecurringOrderDaysSelector( + selectedDays: recurringDays, + onToggle: onDayToggled, + ), + 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: selectedHub, + icon: const Icon( + UiIcons.chevronDown, + size: 18, + color: UiColors.iconSecondary, + ), + onChanged: (OrderHubUiModel? hub) { + if (hub != null) { + onHubChanged(hub); + } + }, + items: hubs.map((OrderHubUiModel hub) { + return DropdownMenuItem( + value: hub, + child: Text( + hub.name, + style: UiTypography.body2m.textPrimary, + ), + ); + }).toList(), + ), + ), + ), + const SizedBox(height: UiConstants.space4), + + HubManagerSelector( + label: oneTimeLabels.hub_manager_label, + description: oneTimeLabels.hub_manager_desc, + hintText: oneTimeLabels.hub_manager_hint, + noManagersText: oneTimeLabels.hub_manager_empty, + noneText: oneTimeLabels.hub_manager_none, + managers: hubManagers, + selectedManager: selectedHubManager, + onChanged: onHubManagerChanged, + ), + const SizedBox(height: UiConstants.space6), + + RecurringOrderSectionHeader( + title: oneTimeLabels.positions_title, + actionLabel: oneTimeLabels.add_position, + onAction: onPositionAdded, + ), + const SizedBox(height: UiConstants.space3), + + // Positions List + ...positions.asMap().entries.map(( + MapEntry entry, + ) { + final int index = entry.key; + final OrderPositionUiModel position = entry.value; + return Padding( + padding: const EdgeInsets.only(bottom: UiConstants.space3), + child: RecurringOrderPositionCard( + index: index, + position: position, + isRemovable: 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: roles, + onUpdated: (OrderPositionUiModel updated) { + onPositionUpdated(index, updated); + }, + onRemoved: () { + onPositionRemoved(index); + }, + ), + ); + }), + ], + ); + } +} diff --git a/apps/mobile/packages/features/client/orders/orders_common/lib/src/presentation/widgets/recurring_order/recurring_order_header.dart b/apps/mobile/packages/features/client/orders/orders_common/lib/src/presentation/widgets/recurring_order/recurring_order_header.dart deleted file mode 100644 index 5913b205..00000000 --- a/apps/mobile/packages/features/client/orders/orders_common/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/orders_common/lib/src/presentation/widgets/recurring_order/recurring_order_view.dart b/apps/mobile/packages/features/client/orders/orders_common/lib/src/presentation/widgets/recurring_order/recurring_order_view.dart index fbc00c07..ffd3ad51 100644 --- a/apps/mobile/packages/features/client/orders/orders_common/lib/src/presentation/widgets/recurring_order/recurring_order_view.dart +++ b/apps/mobile/packages/features/client/orders/orders_common/lib/src/presentation/widgets/recurring_order/recurring_order_view.dart @@ -1,14 +1,11 @@ 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:krow_domain/krow_domain.dart' show Vendor; + +import '../order_bottom_action_button.dart'; import '../order_ui_models.dart'; -import '../hub_manager_selector.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_form.dart'; import 'recurring_order_success_view.dart'; /// The main content of the Recurring Order page. @@ -69,7 +66,8 @@ class RecurringOrderView extends StatelessWidget { final ValueChanged onHubChanged; final ValueChanged onHubManagerChanged; final VoidCallback onPositionAdded; - final void Function(int index, OrderPositionUiModel position) onPositionUpdated; + final void Function(int index, OrderPositionUiModel position) + onPositionUpdated; final void Function(int index) onPositionRemoved; final VoidCallback onSubmit; final VoidCallback onDone; @@ -105,412 +103,97 @@ class RecurringOrderView extends StatelessWidget { ); } + return Scaffold( + appBar: UiAppBar( + showBackButton: true, + onLeadingPressed: onBack, + title: labels.title, + subtitle: labels.subtitle, + ), + body: _buildBody(context, labels, oneTimeLabels), + ); + } + + /// Builds the main body of the Recurring Order page, including the form and handling empty vendor state. + Widget _buildBody( + BuildContext context, + TranslationsClientCreateOrderRecurringEn labels, + TranslationsClientCreateOrderOneTimeEn oneTimeLabels, + ) { if (vendors.isEmpty && status != OrderFormStatus.loading) { - return Scaffold( - body: Column( - children: [ - RecurringOrderHeader( - title: labels.title, - subtitle: labels.subtitle, - onBack: onBack, - ), - 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 Column( + children: [ + 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: onBack, - ), - Expanded( - child: Stack( - children: [ - _RecurringOrderForm( - eventName: eventName, - selectedVendor: selectedVendor, - vendors: vendors, - startDate: startDate, - endDate: endDate, - recurringDays: recurringDays, - selectedHub: selectedHub, - hubs: hubs, - positions: positions, - roles: roles, - onEventNameChanged: onEventNameChanged, - onVendorChanged: onVendorChanged, - onStartDateChanged: onStartDateChanged, - onEndDateChanged: onEndDateChanged, - onDayToggled: onDayToggled, - onHubChanged: onHubChanged, - onHubManagerChanged: onHubManagerChanged, - onPositionAdded: onPositionAdded, - onPositionUpdated: onPositionUpdated, - onPositionRemoved: onPositionRemoved, - hubManagers: hubManagers, - selectedHubManager: selectedHubManager, - ), - if (status == OrderFormStatus.loading) - const Center(child: CircularProgressIndicator()), - ], - ), - ), - _BottomActionButton( - label: status == OrderFormStatus.loading - ? oneTimeLabels.creating - : oneTimeLabels.create_order, - isLoading: status == OrderFormStatus.loading, - onPressed: isValid ? onSubmit : null, - ), - ], - ), - ); - } -} - -class _RecurringOrderForm extends StatelessWidget { - const _RecurringOrderForm({ - required this.eventName, - required this.selectedVendor, - required this.vendors, - required this.startDate, - required this.endDate, - required this.recurringDays, - required this.selectedHub, - required this.hubs, - required this.positions, - required this.roles, - required this.onEventNameChanged, - required this.onVendorChanged, - required this.onStartDateChanged, - required this.onEndDateChanged, - required this.onDayToggled, - required this.onHubChanged, - required this.onHubManagerChanged, - required this.onPositionAdded, - required this.onPositionUpdated, - required this.onPositionRemoved, - required this.hubManagers, - required this.selectedHubManager, - }); - - final String eventName; - final Vendor? selectedVendor; - final List vendors; - final DateTime startDate; - final DateTime endDate; - final List recurringDays; - final OrderHubUiModel? selectedHub; - final List hubs; - final List positions; - final List roles; - - final ValueChanged onEventNameChanged; - final ValueChanged onVendorChanged; - final ValueChanged onStartDateChanged; - final ValueChanged onEndDateChanged; - final ValueChanged onDayToggled; - final ValueChanged onHubChanged; - final ValueChanged onHubManagerChanged; - final VoidCallback onPositionAdded; - final void Function(int index, OrderPositionUiModel position) onPositionUpdated; - final void Function(int index) onPositionRemoved; - - final List hubManagers; - final OrderManagerUiModel? selectedHubManager; - - - @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), + return Column( children: [ - Text( - labels.title, - style: UiTypography.headline3m.textPrimary, - ), - const SizedBox(height: UiConstants.space4), - - RecurringOrderEventNameInput( - label: 'ORDER NAME', - value: eventName, - onChanged: onEventNameChanged, - ), - 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: selectedVendor, - icon: const Icon( - UiIcons.chevronDown, - size: 18, - color: UiColors.iconSecondary, + Expanded( + child: Stack( + children: [ + RecurringOrderForm( + eventName: eventName, + selectedVendor: selectedVendor, + vendors: vendors, + startDate: startDate, + endDate: endDate, + recurringDays: recurringDays, + selectedHub: selectedHub, + hubs: hubs, + positions: positions, + roles: roles, + onEventNameChanged: onEventNameChanged, + onVendorChanged: onVendorChanged, + onStartDateChanged: onStartDateChanged, + onEndDateChanged: onEndDateChanged, + onDayToggled: onDayToggled, + onHubChanged: onHubChanged, + onHubManagerChanged: onHubManagerChanged, + onPositionAdded: onPositionAdded, + onPositionUpdated: onPositionUpdated, + onPositionRemoved: onPositionRemoved, + hubManagers: hubManagers, + selectedHubManager: selectedHubManager, ), - onChanged: (Vendor? vendor) { - if (vendor != null) { - onVendorChanged(vendor); - } - }, - items: vendors.map((Vendor vendor) { - return DropdownMenuItem( - value: vendor, - child: Text( - vendor.name, - style: UiTypography.body2m.textPrimary, - ), - ); - }).toList(), - ), + if (status == OrderFormStatus.loading) + const Center(child: CircularProgressIndicator()), + ], ), ), - const SizedBox(height: UiConstants.space4), - - RecurringOrderDatePicker( - label: 'Start Date', - value: startDate, - onChanged: onStartDateChanged, + OrderBottomActionButton( + label: status == OrderFormStatus.loading + ? oneTimeLabels.creating + : oneTimeLabels.create_order, + isLoading: status == OrderFormStatus.loading, + onPressed: isValid ? onSubmit : null, ), - const SizedBox(height: UiConstants.space4), - - RecurringOrderDatePicker( - label: 'End Date', - value: endDate, - onChanged: onEndDateChanged, - ), - const SizedBox(height: UiConstants.space4), - - Text('Recurring Days', style: UiTypography.footnote2r.textSecondary), - const SizedBox(height: UiConstants.space2), - _RecurringDaysSelector( - selectedDays: recurringDays, - onToggle: onDayToggled, - ), - 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: selectedHub, - icon: const Icon( - UiIcons.chevronDown, - size: 18, - color: UiColors.iconSecondary, - ), - onChanged: (OrderHubUiModel? hub) { - if (hub != null) { - onHubChanged(hub); - } - }, - items: hubs.map((OrderHubUiModel hub) { - return DropdownMenuItem( - value: hub, - child: Text( - hub.name, - style: UiTypography.body2m.textPrimary, - ), - ); - }).toList(), - ), - ), - ), - const SizedBox(height: UiConstants.space4), - - HubManagerSelector( - label: oneTimeLabels.hub_manager_label, - description: oneTimeLabels.hub_manager_desc, - hintText: oneTimeLabels.hub_manager_hint, - noManagersText: oneTimeLabels.hub_manager_empty, - noneText: oneTimeLabels.hub_manager_none, - managers: hubManagers, - selectedManager: selectedHubManager, - onChanged: onHubManagerChanged, - ), - const SizedBox(height: UiConstants.space6), - - RecurringOrderSectionHeader( - title: oneTimeLabels.positions_title, - actionLabel: oneTimeLabels.add_position, - onAction: onPositionAdded, - ), - const SizedBox(height: UiConstants.space3), - - // Positions List - ...positions.asMap().entries.map(( - MapEntry entry, - ) { - final int index = entry.key; - final OrderPositionUiModel position = entry.value; - return Padding( - padding: const EdgeInsets.only(bottom: UiConstants.space3), - child: RecurringOrderPositionCard( - index: index, - position: position, - isRemovable: 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: roles, - onUpdated: (OrderPositionUiModel updated) { - onPositionUpdated(index, updated); - }, - onRemoved: () { - onPositionRemoved(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, - ), - ), - ); - } -}