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:
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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: <Widget>[
|
||||
OneTimeOrderHeader(
|
||||
title: title ?? labels.title,
|
||||
subtitle: subtitle ?? labels.subtitle,
|
||||
onBack: onBack,
|
||||
),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
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: <Widget>[
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
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: <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),
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
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<Vendor>(
|
||||
isExpanded: true,
|
||||
value: selectedVendor,
|
||||
icon: const Icon(
|
||||
UiIcons.chevronDown,
|
||||
size: 18,
|
||||
color: UiColors.iconSecondary,
|
||||
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,
|
||||
),
|
||||
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(),
|
||||
),
|
||||
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<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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
},
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<OrderHubUiModel> onHubChanged;
|
||||
final ValueChanged<OrderManagerUiModel?> 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: <Widget>[
|
||||
PermanentOrderHeader(
|
||||
title: labels.title,
|
||||
subtitle: labels.subtitle,
|
||||
onBack: onBack,
|
||||
),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
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: <Widget>[
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
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: <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),
|
||||
return Column(
|
||||
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,
|
||||
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,
|
||||
),
|
||||
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(),
|
||||
),
|
||||
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<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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
},
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<OrderHubUiModel> onHubChanged;
|
||||
final ValueChanged<OrderManagerUiModel?> 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: <Widget>[
|
||||
RecurringOrderHeader(
|
||||
title: labels.title,
|
||||
subtitle: labels.subtitle,
|
||||
onBack: onBack,
|
||||
),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
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: <Widget>[
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
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: <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),
|
||||
return Column(
|
||||
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,
|
||||
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,
|
||||
),
|
||||
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(),
|
||||
),
|
||||
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<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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user