feat: Add forms and UI components for one-time, permanent, and recurring orders

- Implemented OneTimeOrderForm widget for creating one-time orders with fields for event name, vendor selection, date, hub, hub manager, and positions.
- Created OrderBottomActionButton for a consistent bottom action button across order views.
- Developed PermanentOrderForm for permanent orders, including event name, vendor selection, start date, permanent days, hub, hub manager, and positions.
- Added RecurringOrderForm for recurring orders with fields for event name, vendor selection, start/end dates, recurring days, hub, hub manager, and positions.
- Introduced PermanentOrderDaysSelector and RecurringOrderDaysSelector for selecting days of the week in permanent and recurring orders respectively.
This commit is contained in:
Achintha Isuru
2026-03-10 11:08:03 -04:00
parent 0d241844dd
commit 3d6b49f500
13 changed files with 1236 additions and 1315 deletions

View File

@@ -1,10 +1,13 @@
// UI Models // UI Models
export 'src/presentation/widgets/order_ui_models.dart'; export 'src/presentation/widgets/order_ui_models.dart';
// Shared Widgets
export 'src/presentation/widgets/order_bottom_action_button.dart';
// One Time Order Widgets // 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_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_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_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_position_card.dart';
export 'src/presentation/widgets/one_time_order/one_time_order_section_header.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 // Permanent Order Widgets
export 'src/presentation/widgets/permanent_order/permanent_order_date_picker.dart'; 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_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_position_card.dart';
export 'src/presentation/widgets/permanent_order/permanent_order_section_header.dart'; export 'src/presentation/widgets/permanent_order/permanent_order_section_header.dart';
export 'src/presentation/widgets/permanent_order/permanent_order_success_view.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 // Recurring Order Widgets
export 'src/presentation/widgets/recurring_order/recurring_order_date_picker.dart'; 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_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_position_card.dart';
export 'src/presentation/widgets/recurring_order/recurring_order_section_header.dart'; export 'src/presentation/widgets/recurring_order/recurring_order_section_header.dart';
export 'src/presentation/widgets/recurring_order/recurring_order_success_view.dart'; export 'src/presentation/widgets/recurring_order/recurring_order_success_view.dart';

View File

@@ -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<Vendor> 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<OrderHubUiModel> hubs;
/// The currently selected hub manager, if any.
final OrderManagerUiModel? selectedHubManager;
/// The list of available hub managers for the selected hub.
final List<OrderManagerUiModel> hubManagers;
/// The list of position entries in the order.
final List<OrderPositionUiModel> positions;
/// The list of available roles for position assignment.
final List<OrderRoleUiModel> roles;
/// Called when the event name text changes.
final ValueChanged<String> onEventNameChanged;
/// Called when a vendor is selected.
final ValueChanged<Vendor> onVendorChanged;
/// Called when the date is changed.
final ValueChanged<DateTime> onDateChanged;
/// Called when a hub is selected.
final ValueChanged<OrderHubUiModel> onHubChanged;
/// Called when a hub manager is selected or cleared.
final ValueChanged<OrderManagerUiModel?> 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: <Widget>[
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<Vendor>(
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<Vendor>(
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<OrderHubUiModel>(
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<OrderHubUiModel>(
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<int, OrderPositionUiModel> 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);
},
),
);
}),
],
);
}
}

View File

@@ -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: <Widget>[
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: <Widget>[
Text(
title,
style: UiTypography.headline3m.copyWith(color: UiColors.white),
),
Text(
subtitle,
style: UiTypography.footnote2r.copyWith(
color: UiColors.white.withValues(alpha: 0.8),
),
),
],
),
],
),
);
}
}

View File

@@ -2,13 +2,10 @@ import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart'; import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:krow_domain/krow_domain.dart'; import 'package:krow_domain/krow_domain.dart';
import '../order_bottom_action_button.dart';
import '../order_ui_models.dart'; import '../order_ui_models.dart';
import '../hub_manager_selector.dart'; import 'one_time_order_form.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'; import 'one_time_order_success_view.dart';
/// The main content of the One-Time Order page as a dumb widget. /// 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) { if (vendors.isEmpty && status != OrderFormStatus.loading) {
return Scaffold( return Column(
body: Column( children: <Widget>[
children: <Widget>[ Expanded(
OneTimeOrderHeader( child: Center(
title: title ?? labels.title, child: Column(
subtitle: subtitle ?? labels.subtitle, mainAxisAlignment: MainAxisAlignment.center,
onBack: onBack, children: <Widget>[
), const Icon(
Expanded( UiIcons.search,
child: Center( size: 64,
child: Column( color: UiColors.iconInactive,
mainAxisAlignment: MainAxisAlignment.center, ),
children: <Widget>[ const SizedBox(height: UiConstants.space4),
const Icon( Text(
UiIcons.search, 'No Vendors Available',
size: 64, style: UiTypography.headline3m.textPrimary,
color: UiColors.iconInactive, ),
), const SizedBox(height: UiConstants.space2),
const SizedBox(height: UiConstants.space4), Text(
Text( 'There are no staffing vendors associated with your account.',
'No Vendors Available', style: UiTypography.body2r.textSecondary,
style: UiTypography.headline3m.textPrimary, textAlign: TextAlign.center,
), ),
const SizedBox(height: UiConstants.space2), ],
Text(
'There are no staffing vendors associated with your account.',
style: UiTypography.body2r.textSecondary,
textAlign: TextAlign.center,
),
],
),
), ),
), ),
], ),
), ],
); );
} }
return Scaffold( return Column(
body: Column(
children: <Widget>[
OneTimeOrderHeader(
title: title ?? labels.title,
subtitle: subtitle ?? labels.subtitle,
onBack: onBack,
),
Expanded(
child: Stack(
children: <Widget>[
_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<Vendor> vendors;
final DateTime date;
final OrderHubUiModel? selectedHub;
final List<OrderHubUiModel> hubs;
final OrderManagerUiModel? selectedHubManager;
final List<OrderManagerUiModel> hubManagers;
final List<OrderPositionUiModel> positions;
final List<OrderRoleUiModel> roles;
final ValueChanged<String> onEventNameChanged;
final ValueChanged<Vendor> onVendorChanged;
final ValueChanged<DateTime> onDateChanged;
final ValueChanged<OrderHubUiModel> onHubChanged;
final ValueChanged<OrderManagerUiModel?> 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),
children: <Widget>[ children: <Widget>[
Text( Expanded(
labels.create_your_order, child: Stack(
style: UiTypography.headline3m.textPrimary, children: <Widget>[
), OneTimeOrderForm(
const SizedBox(height: UiConstants.space4), eventName: eventName,
selectedVendor: selectedVendor,
OneTimeOrderEventNameInput( vendors: vendors,
label: 'ORDER NAME', date: date,
value: eventName, selectedHub: selectedHub,
onChanged: onEventNameChanged, hubs: hubs,
), selectedHubManager: selectedHubManager,
const SizedBox(height: UiConstants.space4), hubManagers: hubManagers,
positions: positions,
// Vendor Selection roles: roles,
Text('SELECT VENDOR', style: UiTypography.footnote2r.textSecondary), onEventNameChanged: onEventNameChanged,
const SizedBox(height: UiConstants.space2), onVendorChanged: onVendorChanged,
Container( onDateChanged: onDateChanged,
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3), onHubChanged: onHubChanged,
height: 48, onHubManagerChanged: onHubManagerChanged,
decoration: BoxDecoration( onPositionAdded: onPositionAdded,
color: UiColors.white, onPositionUpdated: onPositionUpdated,
borderRadius: UiConstants.radiusMd, onPositionRemoved: onPositionRemoved,
border: Border.all(color: UiColors.border),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<Vendor>(
isExpanded: true,
value: selectedVendor,
icon: const Icon(
UiIcons.chevronDown,
size: 18,
color: UiColors.iconSecondary,
), ),
onChanged: (Vendor? vendor) { if (status == OrderFormStatus.loading)
if (vendor != null) { const Center(child: CircularProgressIndicator()),
onVendorChanged(vendor); ],
}
},
items: vendors.map((Vendor vendor) {
return DropdownMenuItem<Vendor>(
value: vendor,
child: Text(
vendor.name,
style: UiTypography.body2m.textPrimary,
),
);
}).toList(),
),
), ),
), ),
const SizedBox(height: UiConstants.space4), OrderBottomActionButton(
label: status == OrderFormStatus.loading
OneTimeOrderDatePicker( ? labels.creating
label: labels.date_label, : labels.create_order,
value: date, isLoading: status == OrderFormStatus.loading,
onChanged: onDateChanged, 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<OrderHubUiModel>(
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<OrderHubUiModel>(
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<int, OrderPositionUiModel> 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,
),
),
);
}
}

View File

@@ -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,
),
),
);
}
}

View File

@@ -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<String> selectedDays;
/// Called when a day circle is tapped, with the day index (0 = Sunday).
final ValueChanged<int> onToggle;
@override
Widget build(BuildContext context) {
const List<String> labelsShort = <String>[
'S',
'M',
'T',
'W',
'T',
'F',
'S',
];
const List<String> labelsLong = <String>[
'SUN',
'MON',
'TUE',
'WED',
'THU',
'FRI',
'SAT',
];
return Wrap(
spacing: UiConstants.space2,
children: List<Widget>.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,
),
),
),
);
}),
);
}
}

View File

@@ -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<Vendor> vendors;
/// The start date for the permanent order.
final DateTime startDate;
/// The list of selected permanent day abbreviations (e.g. 'MON', 'TUE').
final List<String> permanentDays;
/// The currently selected hub, if any.
final OrderHubUiModel? selectedHub;
/// The list of available hubs to choose from.
final List<OrderHubUiModel> hubs;
/// The list of position entries in the order.
final List<OrderPositionUiModel> positions;
/// The list of available roles for position assignment.
final List<OrderRoleUiModel> roles;
/// Called when the event name text changes.
final ValueChanged<String> onEventNameChanged;
/// Called when a vendor is selected.
final ValueChanged<Vendor> onVendorChanged;
/// Called when the start date is changed.
final ValueChanged<DateTime> onStartDateChanged;
/// Called when a day-of-week toggle is tapped, with the day index (0=Sun).
final ValueChanged<int> onDayToggled;
/// Called when a hub is selected.
final ValueChanged<OrderHubUiModel> onHubChanged;
/// Called when a hub manager is selected or cleared.
final ValueChanged<OrderManagerUiModel?> 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<OrderManagerUiModel> 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: <Widget>[
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<Vendor>(
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<Vendor>(
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<OrderHubUiModel>(
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<OrderHubUiModel>(
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<int, OrderPositionUiModel> 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);
},
),
);
}),
],
);
}
}

View File

@@ -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: <Widget>[
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: <Widget>[
Text(
title,
style: UiTypography.headline3m.copyWith(color: UiColors.white),
),
Text(
subtitle,
style: UiTypography.footnote2r.copyWith(
color: UiColors.white.withValues(alpha: 0.8),
),
),
],
),
],
),
);
}
}

View File

@@ -2,13 +2,10 @@ import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart'; import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:krow_domain/krow_domain.dart' show Vendor; import 'package:krow_domain/krow_domain.dart' show Vendor;
import '../order_bottom_action_button.dart';
import '../order_ui_models.dart'; import '../order_ui_models.dart';
import '../hub_manager_selector.dart'; import 'permanent_order_form.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'; import 'permanent_order_success_view.dart';
/// The main content of the Permanent Order page. /// The main content of the Permanent Order page.
@@ -65,7 +62,8 @@ class PermanentOrderView extends StatelessWidget {
final ValueChanged<OrderHubUiModel> onHubChanged; final ValueChanged<OrderHubUiModel> onHubChanged;
final ValueChanged<OrderManagerUiModel?> onHubManagerChanged; final ValueChanged<OrderManagerUiModel?> onHubManagerChanged;
final VoidCallback onPositionAdded; 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 void Function(int index) onPositionRemoved;
final VoidCallback onSubmit; final VoidCallback onSubmit;
final VoidCallback onDone; 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) { if (vendors.isEmpty && status != OrderFormStatus.loading) {
return Scaffold( return Column(
body: Column( children: <Widget>[
children: <Widget>[ Expanded(
PermanentOrderHeader( child: Center(
title: labels.title, child: Column(
subtitle: labels.subtitle, mainAxisAlignment: MainAxisAlignment.center,
onBack: onBack, children: <Widget>[
), const Icon(
Expanded( UiIcons.search,
child: Center( size: 64,
child: Column( color: UiColors.iconInactive,
mainAxisAlignment: MainAxisAlignment.center, ),
children: <Widget>[ const SizedBox(height: UiConstants.space4),
const Icon( Text(
UiIcons.search, 'No Vendors Available',
size: 64, style: UiTypography.headline3m.textPrimary,
color: UiColors.iconInactive, ),
), const SizedBox(height: UiConstants.space2),
const SizedBox(height: UiConstants.space4), Text(
Text( 'There are no staffing vendors associated with your account.',
'No Vendors Available', style: UiTypography.body2r.textSecondary,
style: UiTypography.headline3m.textPrimary, textAlign: TextAlign.center,
), ),
const SizedBox(height: UiConstants.space2), ],
Text(
'There are no staffing vendors associated with your account.',
style: UiTypography.body2r.textSecondary,
textAlign: TextAlign.center,
),
],
),
), ),
), ),
], ),
), ],
); );
} }
return Scaffold( return Column(
body: Column(
children: <Widget>[
PermanentOrderHeader(
title: labels.title,
subtitle: labels.subtitle,
onBack: onBack,
),
Expanded(
child: Stack(
children: <Widget>[
_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<Vendor> vendors;
final DateTime startDate;
final List<String> permanentDays;
final OrderHubUiModel? selectedHub;
final List<OrderHubUiModel> hubs;
final List<OrderPositionUiModel> positions;
final List<OrderRoleUiModel> roles;
final ValueChanged<String> onEventNameChanged;
final ValueChanged<Vendor> onVendorChanged;
final ValueChanged<DateTime> onStartDateChanged;
final ValueChanged<int> onDayToggled;
final ValueChanged<OrderHubUiModel> onHubChanged;
final ValueChanged<OrderManagerUiModel?> onHubManagerChanged;
final VoidCallback onPositionAdded;
final void Function(int index, OrderPositionUiModel position) onPositionUpdated;
final void Function(int index) onPositionRemoved;
final List<OrderManagerUiModel> 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),
children: <Widget>[ children: <Widget>[
Text( Expanded(
labels.title, child: Stack(
style: UiTypography.headline3m.textPrimary, children: <Widget>[
), PermanentOrderForm(
const SizedBox(height: UiConstants.space4), eventName: eventName,
selectedVendor: selectedVendor,
PermanentOrderEventNameInput( vendors: vendors,
label: 'ORDER NAME', startDate: startDate,
value: eventName, permanentDays: permanentDays,
onChanged: onEventNameChanged, selectedHub: selectedHub,
), hubs: hubs,
const SizedBox(height: UiConstants.space4), positions: positions,
roles: roles,
// Vendor Selection onEventNameChanged: onEventNameChanged,
Text('SELECT VENDOR', style: UiTypography.footnote2r.textSecondary), onVendorChanged: onVendorChanged,
const SizedBox(height: UiConstants.space2), onStartDateChanged: onStartDateChanged,
Container( onDayToggled: onDayToggled,
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3), onHubChanged: onHubChanged,
height: 48, onHubManagerChanged: onHubManagerChanged,
decoration: BoxDecoration( onPositionAdded: onPositionAdded,
color: UiColors.white, onPositionUpdated: onPositionUpdated,
borderRadius: UiConstants.radiusMd, onPositionRemoved: onPositionRemoved,
border: Border.all(color: UiColors.border), hubManagers: hubManagers,
), selectedHubManager: selectedHubManager,
child: DropdownButtonHideUnderline(
child: DropdownButton<Vendor>(
isExpanded: true,
value: selectedVendor,
icon: const Icon(
UiIcons.chevronDown,
size: 18,
color: UiColors.iconSecondary,
), ),
onChanged: (Vendor? vendor) { if (status == OrderFormStatus.loading)
if (vendor != null) { const Center(child: CircularProgressIndicator()),
onVendorChanged(vendor); ],
}
},
items: vendors.map((Vendor vendor) {
return DropdownMenuItem<Vendor>(
value: vendor,
child: Text(
vendor.name,
style: UiTypography.body2m.textPrimary,
),
);
}).toList(),
),
), ),
), ),
const SizedBox(height: UiConstants.space4), OrderBottomActionButton(
label: status == OrderFormStatus.loading
PermanentOrderDatePicker( ? oneTimeLabels.creating
label: 'Start Date', : oneTimeLabels.create_order,
value: startDate, isLoading: status == OrderFormStatus.loading,
onChanged: onStartDateChanged, 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<OrderHubUiModel>(
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<OrderHubUiModel>(
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<int, OrderPositionUiModel> 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<String> selectedDays;
final ValueChanged<int> onToggle;
@override
Widget build(BuildContext context) {
const List<String> labelsShort = <String>[
'S',
'M',
'T',
'W',
'T',
'F',
'S',
];
const List<String> labelsLong = <String>[
'SUN',
'MON',
'TUE',
'WED',
'THU',
'FRI',
'SAT',
];
return Wrap(
spacing: UiConstants.space2,
children: List<Widget>.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,
),
),
);
}
}

View File

@@ -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<String> selectedDays;
/// Called when a day circle is tapped, with the day index (0 = Sunday).
final ValueChanged<int> onToggle;
@override
Widget build(BuildContext context) {
const List<String> labelsShort = <String>[
'S',
'M',
'T',
'W',
'T',
'F',
'S',
];
const List<String> labelsLong = <String>[
'SUN',
'MON',
'TUE',
'WED',
'THU',
'FRI',
'SAT',
];
return Wrap(
spacing: UiConstants.space2,
children: List<Widget>.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,
),
),
),
);
}),
);
}
}

View File

@@ -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<Vendor> 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<String> recurringDays;
/// The currently selected hub, if any.
final OrderHubUiModel? selectedHub;
/// The list of available hubs to choose from.
final List<OrderHubUiModel> hubs;
/// The list of position entries in the order.
final List<OrderPositionUiModel> positions;
/// The list of available roles for position assignment.
final List<OrderRoleUiModel> roles;
/// Called when the event name text changes.
final ValueChanged<String> onEventNameChanged;
/// Called when a vendor is selected.
final ValueChanged<Vendor> onVendorChanged;
/// Called when the start date is changed.
final ValueChanged<DateTime> onStartDateChanged;
/// Called when the end date is changed.
final ValueChanged<DateTime> onEndDateChanged;
/// Called when a day-of-week toggle is tapped, with the day index (0=Sun).
final ValueChanged<int> onDayToggled;
/// Called when a hub is selected.
final ValueChanged<OrderHubUiModel> onHubChanged;
/// Called when a hub manager is selected or cleared.
final ValueChanged<OrderManagerUiModel?> 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<OrderManagerUiModel> 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: <Widget>[
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<Vendor>(
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<Vendor>(
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<OrderHubUiModel>(
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<OrderHubUiModel>(
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<int, OrderPositionUiModel> 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);
},
),
);
}),
],
);
}
}

View File

@@ -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: <Widget>[
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: <Widget>[
Text(
title,
style: UiTypography.headline3m.copyWith(color: UiColors.white),
),
Text(
subtitle,
style: UiTypography.footnote2r.copyWith(
color: UiColors.white.withValues(alpha: 0.8),
),
),
],
),
],
),
);
}
}

View File

@@ -1,14 +1,11 @@
import 'package:core_localization/core_localization.dart'; 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:design_system/design_system.dart';
import 'package:flutter/material.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 '../order_ui_models.dart';
import '../hub_manager_selector.dart'; import 'recurring_order_form.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'; import 'recurring_order_success_view.dart';
/// The main content of the Recurring Order page. /// The main content of the Recurring Order page.
@@ -69,7 +66,8 @@ class RecurringOrderView extends StatelessWidget {
final ValueChanged<OrderHubUiModel> onHubChanged; final ValueChanged<OrderHubUiModel> onHubChanged;
final ValueChanged<OrderManagerUiModel?> onHubManagerChanged; final ValueChanged<OrderManagerUiModel?> onHubManagerChanged;
final VoidCallback onPositionAdded; 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 void Function(int index) onPositionRemoved;
final VoidCallback onSubmit; final VoidCallback onSubmit;
final VoidCallback onDone; 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) { if (vendors.isEmpty && status != OrderFormStatus.loading) {
return Scaffold( return Column(
body: Column( children: <Widget>[
children: <Widget>[ Expanded(
RecurringOrderHeader( child: Center(
title: labels.title, child: Column(
subtitle: labels.subtitle, mainAxisAlignment: MainAxisAlignment.center,
onBack: onBack, children: <Widget>[
), const Icon(
Expanded( UiIcons.search,
child: Center( size: 64,
child: Column( color: UiColors.iconInactive,
mainAxisAlignment: MainAxisAlignment.center, ),
children: <Widget>[ const SizedBox(height: UiConstants.space4),
const Icon( Text(
UiIcons.search, 'No Vendors Available',
size: 64, style: UiTypography.headline3m.textPrimary,
color: UiColors.iconInactive, ),
), const SizedBox(height: UiConstants.space2),
const SizedBox(height: UiConstants.space4), Text(
Text( 'There are no staffing vendors associated with your account.',
'No Vendors Available', style: UiTypography.body2r.textSecondary,
style: UiTypography.headline3m.textPrimary, textAlign: TextAlign.center,
), ),
const SizedBox(height: UiConstants.space2), ],
Text(
'There are no staffing vendors associated with your account.',
style: UiTypography.body2r.textSecondary,
textAlign: TextAlign.center,
),
],
),
), ),
), ),
], ),
), ],
); );
} }
return Scaffold( return Column(
body: Column(
children: <Widget>[
RecurringOrderHeader(
title: labels.title,
subtitle: labels.subtitle,
onBack: onBack,
),
Expanded(
child: Stack(
children: <Widget>[
_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<Vendor> vendors;
final DateTime startDate;
final DateTime endDate;
final List<String> recurringDays;
final OrderHubUiModel? selectedHub;
final List<OrderHubUiModel> hubs;
final List<OrderPositionUiModel> positions;
final List<OrderRoleUiModel> roles;
final ValueChanged<String> onEventNameChanged;
final ValueChanged<Vendor> onVendorChanged;
final ValueChanged<DateTime> onStartDateChanged;
final ValueChanged<DateTime> onEndDateChanged;
final ValueChanged<int> onDayToggled;
final ValueChanged<OrderHubUiModel> onHubChanged;
final ValueChanged<OrderManagerUiModel?> onHubManagerChanged;
final VoidCallback onPositionAdded;
final void Function(int index, OrderPositionUiModel position) onPositionUpdated;
final void Function(int index) onPositionRemoved;
final List<OrderManagerUiModel> 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),
children: <Widget>[ children: <Widget>[
Text( Expanded(
labels.title, child: Stack(
style: UiTypography.headline3m.textPrimary, children: <Widget>[
), RecurringOrderForm(
const SizedBox(height: UiConstants.space4), eventName: eventName,
selectedVendor: selectedVendor,
RecurringOrderEventNameInput( vendors: vendors,
label: 'ORDER NAME', startDate: startDate,
value: eventName, endDate: endDate,
onChanged: onEventNameChanged, recurringDays: recurringDays,
), selectedHub: selectedHub,
const SizedBox(height: UiConstants.space4), hubs: hubs,
positions: positions,
// Vendor Selection roles: roles,
Text('SELECT VENDOR', style: UiTypography.footnote2r.textSecondary), onEventNameChanged: onEventNameChanged,
const SizedBox(height: UiConstants.space2), onVendorChanged: onVendorChanged,
Container( onStartDateChanged: onStartDateChanged,
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3), onEndDateChanged: onEndDateChanged,
height: 48, onDayToggled: onDayToggled,
decoration: BoxDecoration( onHubChanged: onHubChanged,
color: UiColors.white, onHubManagerChanged: onHubManagerChanged,
borderRadius: UiConstants.radiusMd, onPositionAdded: onPositionAdded,
border: Border.all(color: UiColors.border), onPositionUpdated: onPositionUpdated,
), onPositionRemoved: onPositionRemoved,
child: DropdownButtonHideUnderline( hubManagers: hubManagers,
child: DropdownButton<Vendor>( selectedHubManager: selectedHubManager,
isExpanded: true,
value: selectedVendor,
icon: const Icon(
UiIcons.chevronDown,
size: 18,
color: UiColors.iconSecondary,
), ),
onChanged: (Vendor? vendor) { if (status == OrderFormStatus.loading)
if (vendor != null) { const Center(child: CircularProgressIndicator()),
onVendorChanged(vendor); ],
}
},
items: vendors.map((Vendor vendor) {
return DropdownMenuItem<Vendor>(
value: vendor,
child: Text(
vendor.name,
style: UiTypography.body2m.textPrimary,
),
);
}).toList(),
),
), ),
), ),
const SizedBox(height: UiConstants.space4), OrderBottomActionButton(
label: status == OrderFormStatus.loading
RecurringOrderDatePicker( ? oneTimeLabels.creating
label: 'Start Date', : oneTimeLabels.create_order,
value: startDate, isLoading: status == OrderFormStatus.loading,
onChanged: onStartDateChanged, 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<OrderHubUiModel>(
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<OrderHubUiModel>(
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<int, OrderPositionUiModel> 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<String> selectedDays;
final ValueChanged<int> onToggle;
@override
Widget build(BuildContext context) {
const List<String> labelsShort = <String>[
'S',
'M',
'T',
'W',
'T',
'F',
'S',
];
const List<String> labelsLong = <String>[
'SUN',
'MON',
'TUE',
'WED',
'THU',
'FRI',
'SAT',
];
return Wrap(
spacing: UiConstants.space2,
children: List<Widget>.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,
),
),
);
}
}