feat: Add recurring order form components including date picker, event name input, header, position card, section header, success view, and main view logic
- Implemented RecurringOrderDatePicker for selecting start and end dates. - Created RecurringOrderEventNameInput for entering the order name. - Developed RecurringOrderHeader for displaying the title and subtitle with a back button. - Added RecurringOrderPositionCard for editing individual positions in the order. - Introduced RecurringOrderSectionHeader for section titles with optional action buttons. - Built RecurringOrderSuccessView to show a success message after order creation. - Integrated all components into RecurringOrderView to manage the overall order creation flow.
This commit is contained in:
@@ -0,0 +1,30 @@
|
|||||||
|
// UI Models
|
||||||
|
export 'src/presentation/widgets/order_ui_models.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_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';
|
||||||
|
export 'src/presentation/widgets/one_time_order/one_time_order_success_view.dart';
|
||||||
|
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_event_name_input.dart';
|
||||||
|
export 'src/presentation/widgets/permanent_order/permanent_order_header.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';
|
||||||
|
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_event_name_input.dart';
|
||||||
|
export 'src/presentation/widgets/recurring_order/recurring_order_header.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';
|
||||||
|
export 'src/presentation/widgets/recurring_order/recurring_order_view.dart';
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
/// A date picker field for the one-time order form.
|
||||||
|
/// Matches the prototype input field style.
|
||||||
|
class OneTimeOrderDatePicker extends StatefulWidget {
|
||||||
|
/// Creates a [OneTimeOrderDatePicker].
|
||||||
|
const OneTimeOrderDatePicker({
|
||||||
|
required this.label,
|
||||||
|
required this.value,
|
||||||
|
required this.onChanged,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The label text to display above the field.
|
||||||
|
final String label;
|
||||||
|
|
||||||
|
/// The currently selected date.
|
||||||
|
final DateTime value;
|
||||||
|
|
||||||
|
/// Callback when a new date is selected.
|
||||||
|
final ValueChanged<DateTime> onChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<OneTimeOrderDatePicker> createState() => _OneTimeOrderDatePickerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _OneTimeOrderDatePickerState extends State<OneTimeOrderDatePicker> {
|
||||||
|
late final TextEditingController _controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = TextEditingController(
|
||||||
|
text: DateFormat('yyyy-MM-dd').format(widget.value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(OneTimeOrderDatePicker oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (widget.value != oldWidget.value) {
|
||||||
|
_controller.text = DateFormat('yyyy-MM-dd').format(widget.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return UiTextField(
|
||||||
|
label: widget.label,
|
||||||
|
controller: _controller,
|
||||||
|
readOnly: true,
|
||||||
|
prefixIcon: UiIcons.calendar,
|
||||||
|
onTap: () async {
|
||||||
|
final DateTime? picked = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: widget.value,
|
||||||
|
firstDate: DateTime.now(),
|
||||||
|
lastDate: DateTime.now().add(const Duration(days: 365)),
|
||||||
|
);
|
||||||
|
if (picked != null) {
|
||||||
|
widget.onChanged(picked);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// A text input for the order name in the one-time order form.
|
||||||
|
class OneTimeOrderEventNameInput extends StatefulWidget {
|
||||||
|
const OneTimeOrderEventNameInput({
|
||||||
|
required this.label,
|
||||||
|
required this.value,
|
||||||
|
required this.onChanged,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String label;
|
||||||
|
final String value;
|
||||||
|
final ValueChanged<String> onChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<OneTimeOrderEventNameInput> createState() =>
|
||||||
|
_OneTimeOrderEventNameInputState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _OneTimeOrderEventNameInputState
|
||||||
|
extends State<OneTimeOrderEventNameInput> {
|
||||||
|
late final TextEditingController _controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = TextEditingController(text: widget.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(OneTimeOrderEventNameInput oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (widget.value != _controller.text) {
|
||||||
|
_controller.text = widget.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return UiTextField(
|
||||||
|
label: widget.label,
|
||||||
|
controller: _controller,
|
||||||
|
onChanged: widget.onChanged,
|
||||||
|
hintText: 'Order name',
|
||||||
|
prefixIcon: UiIcons.briefcase,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// A location input field for the one-time order form.
|
||||||
|
/// Matches the prototype input field style.
|
||||||
|
class OneTimeOrderLocationInput extends StatefulWidget {
|
||||||
|
/// Creates a [OneTimeOrderLocationInput].
|
||||||
|
const OneTimeOrderLocationInput({
|
||||||
|
required this.label,
|
||||||
|
required this.value,
|
||||||
|
required this.onChanged,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The label text to display above the field.
|
||||||
|
final String label;
|
||||||
|
|
||||||
|
/// The current location value.
|
||||||
|
final String value;
|
||||||
|
|
||||||
|
/// Callback when the location value changes.
|
||||||
|
final ValueChanged<String> onChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<OneTimeOrderLocationInput> createState() =>
|
||||||
|
_OneTimeOrderLocationInputState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _OneTimeOrderLocationInputState extends State<OneTimeOrderLocationInput> {
|
||||||
|
late final TextEditingController _controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = TextEditingController(text: widget.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(OneTimeOrderLocationInput oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (widget.value != _controller.text) {
|
||||||
|
_controller.text = widget.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return UiTextField(
|
||||||
|
label: widget.label,
|
||||||
|
controller: _controller,
|
||||||
|
onChanged: widget.onChanged,
|
||||||
|
hintText: 'Enter address',
|
||||||
|
prefixIcon: UiIcons.mapPin,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,322 @@
|
|||||||
|
import 'package:core_localization/core_localization.dart';
|
||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../order_ui_models.dart';
|
||||||
|
|
||||||
|
/// A card widget for editing a specific position in a one-time order.
|
||||||
|
class OneTimeOrderPositionCard extends StatelessWidget {
|
||||||
|
const OneTimeOrderPositionCard({
|
||||||
|
required this.index,
|
||||||
|
required this.position,
|
||||||
|
required this.isRemovable,
|
||||||
|
required this.onUpdated,
|
||||||
|
required this.onRemoved,
|
||||||
|
required this.positionLabel,
|
||||||
|
required this.roleLabel,
|
||||||
|
required this.workersLabel,
|
||||||
|
required this.startLabel,
|
||||||
|
required this.endLabel,
|
||||||
|
required this.lunchLabel,
|
||||||
|
required this.roles,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final int index;
|
||||||
|
final OrderPositionUiModel position;
|
||||||
|
final bool isRemovable;
|
||||||
|
final ValueChanged<OrderPositionUiModel> onUpdated;
|
||||||
|
final VoidCallback onRemoved;
|
||||||
|
final String positionLabel;
|
||||||
|
final String roleLabel;
|
||||||
|
final String workersLabel;
|
||||||
|
final String startLabel;
|
||||||
|
final String endLabel;
|
||||||
|
final String lunchLabel;
|
||||||
|
final List<OrderRoleUiModel> roles;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.white,
|
||||||
|
borderRadius: UiConstants.radiusLg,
|
||||||
|
border: Border.all(color: UiColors.border),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
'$positionLabel #${index + 1}',
|
||||||
|
style: UiTypography.footnote1m.textSecondary,
|
||||||
|
),
|
||||||
|
if (isRemovable)
|
||||||
|
GestureDetector(
|
||||||
|
onTap: onRemoved,
|
||||||
|
child: Text(
|
||||||
|
t.client_create_order.one_time.remove,
|
||||||
|
style: UiTypography.footnote1m.copyWith(
|
||||||
|
color: UiColors.destructive,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space3),
|
||||||
|
|
||||||
|
// Role (Dropdown)
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3),
|
||||||
|
height: 44,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: UiConstants.radiusMd,
|
||||||
|
border: Border.all(color: UiColors.border),
|
||||||
|
),
|
||||||
|
child: DropdownButtonHideUnderline(
|
||||||
|
child: DropdownButton<String>(
|
||||||
|
isExpanded: true,
|
||||||
|
hint: Text(
|
||||||
|
roleLabel,
|
||||||
|
style: UiTypography.body2r.textPlaceholder,
|
||||||
|
),
|
||||||
|
value: position.role.isEmpty ? null : position.role,
|
||||||
|
icon: const Icon(
|
||||||
|
UiIcons.chevronDown,
|
||||||
|
size: 18,
|
||||||
|
color: UiColors.iconSecondary,
|
||||||
|
),
|
||||||
|
onChanged: (String? val) {
|
||||||
|
if (val != null) {
|
||||||
|
onUpdated(position.copyWith(role: val));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
items: _buildRoleItems(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space3),
|
||||||
|
|
||||||
|
// Start/End/Workers Row
|
||||||
|
Row(
|
||||||
|
children: <Widget>[
|
||||||
|
// Start Time
|
||||||
|
Expanded(
|
||||||
|
child: _buildTimeInput(
|
||||||
|
context: context,
|
||||||
|
label: startLabel,
|
||||||
|
value: position.startTime,
|
||||||
|
onTap: () async {
|
||||||
|
final TimeOfDay? picked = await showTimePicker(
|
||||||
|
context: context,
|
||||||
|
initialTime: TimeOfDay.now(),
|
||||||
|
);
|
||||||
|
if (picked != null && context.mounted) {
|
||||||
|
onUpdated(
|
||||||
|
position.copyWith(startTime: picked.format(context)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space2),
|
||||||
|
// End Time
|
||||||
|
Expanded(
|
||||||
|
child: _buildTimeInput(
|
||||||
|
context: context,
|
||||||
|
label: endLabel,
|
||||||
|
value: position.endTime,
|
||||||
|
onTap: () async {
|
||||||
|
final TimeOfDay? picked = await showTimePicker(
|
||||||
|
context: context,
|
||||||
|
initialTime: TimeOfDay.now(),
|
||||||
|
);
|
||||||
|
if (picked != null && context.mounted) {
|
||||||
|
onUpdated(
|
||||||
|
position.copyWith(endTime: picked.format(context)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space2),
|
||||||
|
// Workers Count
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
workersLabel,
|
||||||
|
style: UiTypography.footnote2r.textSecondary,
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space1),
|
||||||
|
Container(
|
||||||
|
height: 40,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.bgSecondary,
|
||||||
|
borderRadius: UiConstants.radiusSm,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: <Widget>[
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
if (position.count > 1) {
|
||||||
|
onUpdated(
|
||||||
|
position.copyWith(count: position.count - 1),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Icon(UiIcons.minus, size: 12),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'${position.count}',
|
||||||
|
style: UiTypography.body2b.textPrimary,
|
||||||
|
),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
onUpdated(
|
||||||
|
position.copyWith(count: position.count + 1),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: const Icon(UiIcons.add, size: 12),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space4),
|
||||||
|
|
||||||
|
// Lunch Break
|
||||||
|
Text(lunchLabel, style: UiTypography.footnote2r.textSecondary),
|
||||||
|
const SizedBox(height: UiConstants.space1),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3),
|
||||||
|
height: 44,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: UiConstants.radiusMd,
|
||||||
|
border: Border.all(color: UiColors.border),
|
||||||
|
),
|
||||||
|
child: DropdownButtonHideUnderline(
|
||||||
|
child: DropdownButton<String>(
|
||||||
|
isExpanded: true,
|
||||||
|
value: position.lunchBreak,
|
||||||
|
icon: const Icon(
|
||||||
|
UiIcons.chevronDown,
|
||||||
|
size: 18,
|
||||||
|
color: UiColors.iconSecondary,
|
||||||
|
),
|
||||||
|
onChanged: (String? val) {
|
||||||
|
if (val != null) {
|
||||||
|
onUpdated(position.copyWith(lunchBreak: val));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
items: <String>[
|
||||||
|
'NO_BREAK',
|
||||||
|
'MIN_10',
|
||||||
|
'MIN_15',
|
||||||
|
'MIN_30',
|
||||||
|
'MIN_45',
|
||||||
|
'MIN_60',
|
||||||
|
].map((String value) {
|
||||||
|
final String label = switch (value) {
|
||||||
|
'NO_BREAK' => 'No Break',
|
||||||
|
'MIN_10' => '10 min (Paid)',
|
||||||
|
'MIN_15' => '15 min (Paid)',
|
||||||
|
'MIN_30' => '30 min (Unpaid)',
|
||||||
|
'MIN_45' => '45 min (Unpaid)',
|
||||||
|
'MIN_60' => '60 min (Unpaid)',
|
||||||
|
_ => value,
|
||||||
|
};
|
||||||
|
return DropdownMenuItem<String>(
|
||||||
|
value: value,
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: UiTypography.body2r.textPrimary,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTimeInput({
|
||||||
|
required BuildContext context,
|
||||||
|
required String label,
|
||||||
|
required String value,
|
||||||
|
required VoidCallback onTap,
|
||||||
|
}) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(label, style: UiTypography.footnote2r.textSecondary),
|
||||||
|
const SizedBox(height: UiConstants.space1),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Container(
|
||||||
|
height: 40,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: UiConstants.radiusSm,
|
||||||
|
border: Border.all(color: UiColors.border),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
value.isEmpty ? '--:--' : value,
|
||||||
|
style: UiTypography.body2r.textPrimary,
|
||||||
|
),
|
||||||
|
const Icon(
|
||||||
|
UiIcons.clock,
|
||||||
|
size: 14,
|
||||||
|
color: UiColors.iconSecondary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<DropdownMenuItem<String>> _buildRoleItems() {
|
||||||
|
final List<DropdownMenuItem<String>> items = roles
|
||||||
|
.map(
|
||||||
|
(OrderRoleUiModel role) => DropdownMenuItem<String>(
|
||||||
|
value: role.id,
|
||||||
|
child: Text(
|
||||||
|
'${role.name} - \$${role.costPerHour.toStringAsFixed(0)}',
|
||||||
|
style: UiTypography.body2r.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final bool hasSelected =
|
||||||
|
roles.any((OrderRoleUiModel role) => role.id == position.role);
|
||||||
|
if (position.role.isNotEmpty && !hasSelected) {
|
||||||
|
items.add(
|
||||||
|
DropdownMenuItem<String>(
|
||||||
|
value: position.role,
|
||||||
|
child: Text(
|
||||||
|
position.role,
|
||||||
|
style: UiTypography.body2r.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// A header widget for sections in the one-time order form.
|
||||||
|
class OneTimeOrderSectionHeader extends StatelessWidget {
|
||||||
|
/// Creates a [OneTimeOrderSectionHeader].
|
||||||
|
const OneTimeOrderSectionHeader({
|
||||||
|
required this.title,
|
||||||
|
this.actionLabel,
|
||||||
|
this.onAction,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The title text for the section.
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
/// Optional label for an action button on the right.
|
||||||
|
final String? actionLabel;
|
||||||
|
|
||||||
|
/// Callback when the action button is tapped.
|
||||||
|
final VoidCallback? onAction;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(title, style: UiTypography.headline4m.textPrimary),
|
||||||
|
if (actionLabel != null && onAction != null)
|
||||||
|
TextButton(
|
||||||
|
onPressed: onAction,
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
minimumSize: Size.zero,
|
||||||
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
const Icon(UiIcons.add, size: 16, color: UiColors.primary),
|
||||||
|
const SizedBox(width: UiConstants.space2),
|
||||||
|
Text(
|
||||||
|
actionLabel!,
|
||||||
|
style: UiTypography.body2m.primary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// A view to display when a one-time order has been successfully created.
|
||||||
|
/// Matches the prototype success view layout with a gradient background and centered card.
|
||||||
|
class OneTimeOrderSuccessView extends StatelessWidget {
|
||||||
|
/// Creates a [OneTimeOrderSuccessView].
|
||||||
|
const OneTimeOrderSuccessView({
|
||||||
|
required this.title,
|
||||||
|
required this.message,
|
||||||
|
required this.buttonLabel,
|
||||||
|
required this.onDone,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The title of the success message.
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
/// The body of the success message.
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
/// Label for the completion button.
|
||||||
|
final String buttonLabel;
|
||||||
|
|
||||||
|
/// Callback when the completion button is tapped.
|
||||||
|
final VoidCallback onDone;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: <Color>[UiColors.primary, UiColors.buttonPrimaryHover],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: SafeArea(
|
||||||
|
child: Center(
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: UiConstants.space10),
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.white,
|
||||||
|
borderRadius: UiConstants.radiusLg * 1.5,
|
||||||
|
boxShadow: <BoxShadow>[
|
||||||
|
BoxShadow(
|
||||||
|
color: UiColors.black.withValues(alpha: 0.2),
|
||||||
|
blurRadius: 20,
|
||||||
|
offset: const Offset(0, UiConstants.space2 + 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
Container(
|
||||||
|
width: UiConstants.space16,
|
||||||
|
height: UiConstants.space16,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: UiColors.accent,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
|
||||||
|
|
||||||
|
child: const Center(
|
||||||
|
child: Icon(
|
||||||
|
UiIcons.check,
|
||||||
|
color: UiColors.black,
|
||||||
|
size: UiConstants.space8,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space6),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: UiTypography.headline2m.textPrimary,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space3),
|
||||||
|
Text(
|
||||||
|
message,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: UiTypography.body2r.textSecondary.copyWith(
|
||||||
|
height: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space8),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: UiButton.primary(
|
||||||
|
text: buttonLabel,
|
||||||
|
onPressed: onDone,
|
||||||
|
size: UiButtonSize.large,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,388 @@
|
|||||||
|
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_ui_models.dart';
|
||||||
|
import 'one_time_order_date_picker.dart';
|
||||||
|
import 'one_time_order_event_name_input.dart';
|
||||||
|
import 'one_time_order_header.dart';
|
||||||
|
import 'one_time_order_position_card.dart';
|
||||||
|
import 'one_time_order_section_header.dart';
|
||||||
|
import 'one_time_order_success_view.dart';
|
||||||
|
|
||||||
|
/// The main content of the One-Time Order page as a dumb widget.
|
||||||
|
class OneTimeOrderView extends StatelessWidget {
|
||||||
|
const OneTimeOrderView({
|
||||||
|
required this.status,
|
||||||
|
required this.errorMessage,
|
||||||
|
required this.eventName,
|
||||||
|
required this.selectedVendor,
|
||||||
|
required this.vendors,
|
||||||
|
required this.date,
|
||||||
|
required this.selectedHub,
|
||||||
|
required this.hubs,
|
||||||
|
required this.positions,
|
||||||
|
required this.roles,
|
||||||
|
required this.isValid,
|
||||||
|
required this.onEventNameChanged,
|
||||||
|
required this.onVendorChanged,
|
||||||
|
required this.onDateChanged,
|
||||||
|
required this.onHubChanged,
|
||||||
|
required this.onPositionAdded,
|
||||||
|
required this.onPositionUpdated,
|
||||||
|
required this.onPositionRemoved,
|
||||||
|
required this.onSubmit,
|
||||||
|
required this.onDone,
|
||||||
|
required this.onBack,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final OrderFormStatus status;
|
||||||
|
final String? errorMessage;
|
||||||
|
final String eventName;
|
||||||
|
final Vendor? selectedVendor;
|
||||||
|
final List<Vendor> vendors;
|
||||||
|
final DateTime date;
|
||||||
|
final OrderHubUiModel? selectedHub;
|
||||||
|
final List<OrderHubUiModel> hubs;
|
||||||
|
final List<OrderPositionUiModel> positions;
|
||||||
|
final List<OrderRoleUiModel> roles;
|
||||||
|
final bool isValid;
|
||||||
|
|
||||||
|
final ValueChanged<String> onEventNameChanged;
|
||||||
|
final ValueChanged<Vendor> onVendorChanged;
|
||||||
|
final ValueChanged<DateTime> onDateChanged;
|
||||||
|
final ValueChanged<OrderHubUiModel> onHubChanged;
|
||||||
|
final VoidCallback onPositionAdded;
|
||||||
|
final void Function(int index, OrderPositionUiModel position) onPositionUpdated;
|
||||||
|
final void Function(int index) onPositionRemoved;
|
||||||
|
final VoidCallback onSubmit;
|
||||||
|
final VoidCallback onDone;
|
||||||
|
final VoidCallback onBack;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final TranslationsClientCreateOrderOneTimeEn labels =
|
||||||
|
t.client_create_order.one_time;
|
||||||
|
|
||||||
|
// React to error messages
|
||||||
|
if (status == OrderFormStatus.failure && errorMessage != null) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
UiSnackbar.show(
|
||||||
|
context,
|
||||||
|
message: translateErrorKey(errorMessage!),
|
||||||
|
type: UiSnackbarType.error,
|
||||||
|
margin: const EdgeInsets.only(bottom: 140, left: 16, right: 16),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status == OrderFormStatus.success) {
|
||||||
|
return OneTimeOrderSuccessView(
|
||||||
|
title: labels.success_title,
|
||||||
|
message: labels.success_message,
|
||||||
|
buttonLabel: labels.back_to_orders,
|
||||||
|
onDone: onDone,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vendors.isEmpty && status != OrderFormStatus.loading) {
|
||||||
|
return Scaffold(
|
||||||
|
body: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
OneTimeOrderHeader(
|
||||||
|
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 Scaffold(
|
||||||
|
body: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
OneTimeOrderHeader(
|
||||||
|
title: labels.title,
|
||||||
|
subtitle: labels.subtitle,
|
||||||
|
onBack: onBack,
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Stack(
|
||||||
|
children: <Widget>[
|
||||||
|
_OneTimeOrderForm(
|
||||||
|
eventName: eventName,
|
||||||
|
selectedVendor: selectedVendor,
|
||||||
|
vendors: vendors,
|
||||||
|
date: date,
|
||||||
|
selectedHub: selectedHub,
|
||||||
|
hubs: hubs,
|
||||||
|
positions: positions,
|
||||||
|
roles: roles,
|
||||||
|
onEventNameChanged: onEventNameChanged,
|
||||||
|
onVendorChanged: onVendorChanged,
|
||||||
|
onDateChanged: onDateChanged,
|
||||||
|
onHubChanged: onHubChanged,
|
||||||
|
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.positions,
|
||||||
|
required this.roles,
|
||||||
|
required this.onEventNameChanged,
|
||||||
|
required this.onVendorChanged,
|
||||||
|
required this.onDateChanged,
|
||||||
|
required this.onHubChanged,
|
||||||
|
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 List<OrderPositionUiModel> positions;
|
||||||
|
final List<OrderRoleUiModel> roles;
|
||||||
|
|
||||||
|
final ValueChanged<String> onEventNameChanged;
|
||||||
|
final ValueChanged<Vendor> onVendorChanged;
|
||||||
|
final ValueChanged<DateTime> onDateChanged;
|
||||||
|
final ValueChanged<OrderHubUiModel> onHubChanged;
|
||||||
|
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>[
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
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.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,96 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
enum OrderFormStatus { initial, loading, success, failure }
|
||||||
|
|
||||||
|
class OrderHubUiModel extends Equatable {
|
||||||
|
const OrderHubUiModel({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.address,
|
||||||
|
this.placeId,
|
||||||
|
this.latitude,
|
||||||
|
this.longitude,
|
||||||
|
this.city,
|
||||||
|
this.state,
|
||||||
|
this.street,
|
||||||
|
this.country,
|
||||||
|
this.zipCode,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
final String name;
|
||||||
|
final String address;
|
||||||
|
final String? placeId;
|
||||||
|
final double? latitude;
|
||||||
|
final double? longitude;
|
||||||
|
final String? city;
|
||||||
|
final String? state;
|
||||||
|
final String? street;
|
||||||
|
final String? country;
|
||||||
|
final String? zipCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
address,
|
||||||
|
placeId,
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
|
city,
|
||||||
|
state,
|
||||||
|
street,
|
||||||
|
country,
|
||||||
|
zipCode,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderRoleUiModel extends Equatable {
|
||||||
|
const OrderRoleUiModel({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.costPerHour,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
final String name;
|
||||||
|
final double costPerHour;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[id, name, costPerHour];
|
||||||
|
}
|
||||||
|
|
||||||
|
class OrderPositionUiModel extends Equatable {
|
||||||
|
const OrderPositionUiModel({
|
||||||
|
required this.role,
|
||||||
|
required this.count,
|
||||||
|
required this.startTime,
|
||||||
|
required this.endTime,
|
||||||
|
this.lunchBreak = 'NO_BREAK',
|
||||||
|
});
|
||||||
|
|
||||||
|
final String role;
|
||||||
|
final int count;
|
||||||
|
final String startTime;
|
||||||
|
final String endTime;
|
||||||
|
final String lunchBreak;
|
||||||
|
|
||||||
|
OrderPositionUiModel copyWith({
|
||||||
|
String? role,
|
||||||
|
int? count,
|
||||||
|
String? startTime,
|
||||||
|
String? endTime,
|
||||||
|
String? lunchBreak,
|
||||||
|
}) {
|
||||||
|
return OrderPositionUiModel(
|
||||||
|
role: role ?? this.role,
|
||||||
|
count: count ?? this.count,
|
||||||
|
startTime: startTime ?? this.startTime,
|
||||||
|
endTime: endTime ?? this.endTime,
|
||||||
|
lunchBreak: lunchBreak ?? this.lunchBreak,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[role, count, startTime, endTime, lunchBreak];
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
/// A date picker field for the permanent order form.
|
||||||
|
class PermanentOrderDatePicker extends StatefulWidget {
|
||||||
|
/// Creates a [PermanentOrderDatePicker].
|
||||||
|
const PermanentOrderDatePicker({
|
||||||
|
required this.label,
|
||||||
|
required this.value,
|
||||||
|
required this.onChanged,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The label text to display above the field.
|
||||||
|
final String label;
|
||||||
|
|
||||||
|
/// The currently selected date.
|
||||||
|
final DateTime value;
|
||||||
|
|
||||||
|
/// Callback when a new date is selected.
|
||||||
|
final ValueChanged<DateTime> onChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PermanentOrderDatePicker> createState() =>
|
||||||
|
_PermanentOrderDatePickerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PermanentOrderDatePickerState extends State<PermanentOrderDatePicker> {
|
||||||
|
late final TextEditingController _controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = TextEditingController(
|
||||||
|
text: DateFormat('yyyy-MM-dd').format(widget.value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(PermanentOrderDatePicker oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (widget.value != oldWidget.value) {
|
||||||
|
_controller.text = DateFormat('yyyy-MM-dd').format(widget.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return UiTextField(
|
||||||
|
label: widget.label,
|
||||||
|
controller: _controller,
|
||||||
|
readOnly: true,
|
||||||
|
prefixIcon: UiIcons.calendar,
|
||||||
|
onTap: () async {
|
||||||
|
final DateTime? picked = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: widget.value,
|
||||||
|
firstDate: DateTime.now(),
|
||||||
|
lastDate: DateTime.now().add(const Duration(days: 365)),
|
||||||
|
);
|
||||||
|
if (picked != null) {
|
||||||
|
widget.onChanged(picked);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// A text input for the order name in the permanent order form.
|
||||||
|
class PermanentOrderEventNameInput extends StatefulWidget {
|
||||||
|
const PermanentOrderEventNameInput({
|
||||||
|
required this.label,
|
||||||
|
required this.value,
|
||||||
|
required this.onChanged,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String label;
|
||||||
|
final String value;
|
||||||
|
final ValueChanged<String> onChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PermanentOrderEventNameInput> createState() =>
|
||||||
|
_PermanentOrderEventNameInputState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PermanentOrderEventNameInputState
|
||||||
|
extends State<PermanentOrderEventNameInput> {
|
||||||
|
late final TextEditingController _controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = TextEditingController(text: widget.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(PermanentOrderEventNameInput oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (widget.value != _controller.text) {
|
||||||
|
_controller.text = widget.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return UiTextField(
|
||||||
|
label: widget.label,
|
||||||
|
controller: _controller,
|
||||||
|
onChanged: widget.onChanged,
|
||||||
|
hintText: 'Order name',
|
||||||
|
prefixIcon: UiIcons.briefcase,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,321 @@
|
|||||||
|
import 'package:core_localization/core_localization.dart';
|
||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../order_ui_models.dart';
|
||||||
|
|
||||||
|
/// A card widget for editing a specific position in a permanent order.
|
||||||
|
class PermanentOrderPositionCard extends StatelessWidget {
|
||||||
|
const PermanentOrderPositionCard({
|
||||||
|
required this.index,
|
||||||
|
required this.position,
|
||||||
|
required this.isRemovable,
|
||||||
|
required this.onUpdated,
|
||||||
|
required this.onRemoved,
|
||||||
|
required this.positionLabel,
|
||||||
|
required this.roleLabel,
|
||||||
|
required this.workersLabel,
|
||||||
|
required this.startLabel,
|
||||||
|
required this.endLabel,
|
||||||
|
required this.lunchLabel,
|
||||||
|
required this.roles,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final int index;
|
||||||
|
final OrderPositionUiModel position;
|
||||||
|
final bool isRemovable;
|
||||||
|
final ValueChanged<OrderPositionUiModel> onUpdated;
|
||||||
|
final VoidCallback onRemoved;
|
||||||
|
final String positionLabel;
|
||||||
|
final String roleLabel;
|
||||||
|
final String workersLabel;
|
||||||
|
final String startLabel;
|
||||||
|
final String endLabel;
|
||||||
|
final String lunchLabel;
|
||||||
|
final List<OrderRoleUiModel> roles;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.white,
|
||||||
|
borderRadius: UiConstants.radiusLg,
|
||||||
|
border: Border.all(color: UiColors.border),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
'$positionLabel #${index + 1}',
|
||||||
|
style: UiTypography.footnote1m.textSecondary,
|
||||||
|
),
|
||||||
|
if (isRemovable)
|
||||||
|
GestureDetector(
|
||||||
|
onTap: onRemoved,
|
||||||
|
child: Text(
|
||||||
|
t.client_create_order.one_time.remove,
|
||||||
|
style: UiTypography.footnote1m.copyWith(
|
||||||
|
color: UiColors.destructive,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space3),
|
||||||
|
|
||||||
|
// Role (Dropdown)
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3),
|
||||||
|
height: 44,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: UiConstants.radiusMd,
|
||||||
|
border: Border.all(color: UiColors.border),
|
||||||
|
),
|
||||||
|
child: DropdownButtonHideUnderline(
|
||||||
|
child: DropdownButton<String>(
|
||||||
|
isExpanded: true,
|
||||||
|
hint: Text(
|
||||||
|
roleLabel,
|
||||||
|
style: UiTypography.body2r.textPlaceholder,
|
||||||
|
),
|
||||||
|
value: position.role.isEmpty ? null : position.role,
|
||||||
|
icon: const Icon(
|
||||||
|
UiIcons.chevronDown,
|
||||||
|
size: 18,
|
||||||
|
color: UiColors.iconSecondary,
|
||||||
|
),
|
||||||
|
onChanged: (String? val) {
|
||||||
|
if (val != null) {
|
||||||
|
onUpdated(position.copyWith(role: val));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
items: _buildRoleItems(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space3),
|
||||||
|
|
||||||
|
// Start/End/Workers Row
|
||||||
|
Row(
|
||||||
|
children: <Widget>[
|
||||||
|
// Start Time
|
||||||
|
Expanded(
|
||||||
|
child: _buildTimeInput(
|
||||||
|
context: context,
|
||||||
|
label: startLabel,
|
||||||
|
value: position.startTime,
|
||||||
|
onTap: () async {
|
||||||
|
final TimeOfDay? picked = await showTimePicker(
|
||||||
|
context: context,
|
||||||
|
initialTime: TimeOfDay.now(),
|
||||||
|
);
|
||||||
|
if (picked != null && context.mounted) {
|
||||||
|
onUpdated(
|
||||||
|
position.copyWith(startTime: picked.format(context)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space2),
|
||||||
|
// End Time
|
||||||
|
Expanded(
|
||||||
|
child: _buildTimeInput(
|
||||||
|
context: context,
|
||||||
|
label: endLabel,
|
||||||
|
value: position.endTime,
|
||||||
|
onTap: () async {
|
||||||
|
final TimeOfDay? picked = await showTimePicker(
|
||||||
|
context: context,
|
||||||
|
initialTime: TimeOfDay.now(),
|
||||||
|
);
|
||||||
|
if (picked != null && context.mounted) {
|
||||||
|
onUpdated(
|
||||||
|
position.copyWith(endTime: picked.format(context)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space2),
|
||||||
|
// Workers Count
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
workersLabel,
|
||||||
|
style: UiTypography.footnote2r.textSecondary,
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space1),
|
||||||
|
Container(
|
||||||
|
height: 40,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.bgSecondary,
|
||||||
|
borderRadius: UiConstants.radiusSm,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: <Widget>[
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
if (position.count > 1) {
|
||||||
|
onUpdated(
|
||||||
|
position.copyWith(count: position.count - 1),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Icon(UiIcons.minus, size: 12),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'${position.count}',
|
||||||
|
style: UiTypography.body2b.textPrimary,
|
||||||
|
),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
onUpdated(
|
||||||
|
position.copyWith(count: position.count + 1),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: const Icon(UiIcons.add, size: 12),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space4),
|
||||||
|
|
||||||
|
// Lunch Break
|
||||||
|
Text(lunchLabel, style: UiTypography.footnote2r.textSecondary),
|
||||||
|
const SizedBox(height: UiConstants.space1),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3),
|
||||||
|
height: 44,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: UiConstants.radiusMd,
|
||||||
|
border: Border.all(color: UiColors.border),
|
||||||
|
),
|
||||||
|
child: DropdownButtonHideUnderline(
|
||||||
|
child: DropdownButton<String>(
|
||||||
|
isExpanded: true,
|
||||||
|
value: position.lunchBreak,
|
||||||
|
icon: const Icon(
|
||||||
|
UiIcons.chevronDown,
|
||||||
|
size: 18,
|
||||||
|
color: UiColors.iconSecondary,
|
||||||
|
),
|
||||||
|
onChanged: (String? val) {
|
||||||
|
if (val != null) {
|
||||||
|
onUpdated(position.copyWith(lunchBreak: val));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
items: <String>[
|
||||||
|
'NO_BREAK',
|
||||||
|
'MIN_10',
|
||||||
|
'MIN_15',
|
||||||
|
'MIN_30',
|
||||||
|
'MIN_45',
|
||||||
|
'MIN_60',
|
||||||
|
].map((String value) {
|
||||||
|
final String label = switch (value) {
|
||||||
|
'NO_BREAK' => 'No Break',
|
||||||
|
'MIN_10' => '10 min (Paid)',
|
||||||
|
'MIN_15' => '15 min (Paid)',
|
||||||
|
'MIN_30' => '30 min (Unpaid)',
|
||||||
|
'MIN_45' => '45 min (Unpaid)',
|
||||||
|
'MIN_60' => '60 min (Unpaid)',
|
||||||
|
_ => value,
|
||||||
|
};
|
||||||
|
return DropdownMenuItem<String>(
|
||||||
|
value: value,
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: UiTypography.body2r.textPrimary,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTimeInput({
|
||||||
|
required BuildContext context,
|
||||||
|
required String label,
|
||||||
|
required String value,
|
||||||
|
required VoidCallback onTap,
|
||||||
|
}) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(label, style: UiTypography.footnote2r.textSecondary),
|
||||||
|
const SizedBox(height: UiConstants.space1),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Container(
|
||||||
|
height: 40,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: UiConstants.radiusSm,
|
||||||
|
border: Border.all(color: UiColors.border),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
value.isEmpty ? '--:--' : value,
|
||||||
|
style: UiTypography.body2r.textPrimary,
|
||||||
|
),
|
||||||
|
const Icon(
|
||||||
|
UiIcons.clock,
|
||||||
|
size: 14,
|
||||||
|
color: UiColors.iconSecondary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<DropdownMenuItem<String>> _buildRoleItems() {
|
||||||
|
final List<DropdownMenuItem<String>> items = roles
|
||||||
|
.map(
|
||||||
|
(OrderRoleUiModel role) => DropdownMenuItem<String>(
|
||||||
|
value: role.id,
|
||||||
|
child: Text(
|
||||||
|
'${role.name} - \$${role.costPerHour.toStringAsFixed(0)}',
|
||||||
|
style: UiTypography.body2r.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final bool hasSelected = roles.any((OrderRoleUiModel role) => role.id == position.role);
|
||||||
|
if (position.role.isNotEmpty && !hasSelected) {
|
||||||
|
items.add(
|
||||||
|
DropdownMenuItem<String>(
|
||||||
|
value: position.role,
|
||||||
|
child: Text(
|
||||||
|
position.role,
|
||||||
|
style: UiTypography.body2r.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// A header widget for sections in the permanent order form.
|
||||||
|
class PermanentOrderSectionHeader extends StatelessWidget {
|
||||||
|
/// Creates a [PermanentOrderSectionHeader].
|
||||||
|
const PermanentOrderSectionHeader({
|
||||||
|
required this.title,
|
||||||
|
this.actionLabel,
|
||||||
|
this.onAction,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The title text for the section.
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
/// Optional label for an action button on the right.
|
||||||
|
final String? actionLabel;
|
||||||
|
|
||||||
|
/// Callback when the action button is tapped.
|
||||||
|
final VoidCallback? onAction;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(title, style: UiTypography.headline4m.textPrimary),
|
||||||
|
if (actionLabel != null && onAction != null)
|
||||||
|
TextButton(
|
||||||
|
onPressed: onAction,
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
minimumSize: Size.zero,
|
||||||
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
const Icon(UiIcons.add, size: 16, color: UiColors.primary),
|
||||||
|
const SizedBox(width: UiConstants.space2),
|
||||||
|
Text(
|
||||||
|
actionLabel!,
|
||||||
|
style: UiTypography.body2m.primary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// A view to display when a permanent order has been successfully created.
|
||||||
|
class PermanentOrderSuccessView extends StatelessWidget {
|
||||||
|
/// Creates a [PermanentOrderSuccessView].
|
||||||
|
const PermanentOrderSuccessView({
|
||||||
|
required this.title,
|
||||||
|
required this.message,
|
||||||
|
required this.buttonLabel,
|
||||||
|
required this.onDone,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The title of the success message.
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
/// The body of the success message.
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
/// Label for the completion button.
|
||||||
|
final String buttonLabel;
|
||||||
|
|
||||||
|
/// Callback when the completion button is tapped.
|
||||||
|
final VoidCallback onDone;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: <Color>[UiColors.primary, UiColors.buttonPrimaryHover],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: SafeArea(
|
||||||
|
child: Center(
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: UiConstants.space10),
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.white,
|
||||||
|
borderRadius: UiConstants.radiusLg * 1.5,
|
||||||
|
boxShadow: <BoxShadow>[
|
||||||
|
BoxShadow(
|
||||||
|
color: UiColors.black.withValues(alpha: 0.2),
|
||||||
|
blurRadius: 20,
|
||||||
|
offset: const Offset(0, UiConstants.space2 + 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
Container(
|
||||||
|
width: UiConstants.space16,
|
||||||
|
height: UiConstants.space16,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: UiColors.accent,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: const Center(
|
||||||
|
child: Icon(
|
||||||
|
UiIcons.check,
|
||||||
|
color: UiColors.black,
|
||||||
|
size: UiConstants.space8,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space6),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: UiTypography.headline2m.textPrimary,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space3),
|
||||||
|
Text(
|
||||||
|
message,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: UiTypography.body2r.textSecondary.copyWith(
|
||||||
|
height: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space8),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: UiButton.primary(
|
||||||
|
text: buttonLabel,
|
||||||
|
onPressed: onDone,
|
||||||
|
size: UiButtonSize.large,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,466 @@
|
|||||||
|
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_ui_models.dart';
|
||||||
|
import 'permanent_order_date_picker.dart';
|
||||||
|
import 'permanent_order_event_name_input.dart';
|
||||||
|
import 'permanent_order_header.dart';
|
||||||
|
import 'permanent_order_position_card.dart';
|
||||||
|
import 'permanent_order_section_header.dart';
|
||||||
|
import 'permanent_order_success_view.dart';
|
||||||
|
|
||||||
|
/// The main content of the Permanent Order page.
|
||||||
|
class PermanentOrderView extends StatelessWidget {
|
||||||
|
const PermanentOrderView({
|
||||||
|
required this.status,
|
||||||
|
required this.errorMessage,
|
||||||
|
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.isValid,
|
||||||
|
required this.onEventNameChanged,
|
||||||
|
required this.onVendorChanged,
|
||||||
|
required this.onStartDateChanged,
|
||||||
|
required this.onDayToggled,
|
||||||
|
required this.onHubChanged,
|
||||||
|
required this.onPositionAdded,
|
||||||
|
required this.onPositionUpdated,
|
||||||
|
required this.onPositionRemoved,
|
||||||
|
required this.onSubmit,
|
||||||
|
required this.onDone,
|
||||||
|
required this.onBack,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final OrderFormStatus status;
|
||||||
|
final String? errorMessage;
|
||||||
|
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 bool isValid;
|
||||||
|
|
||||||
|
final ValueChanged<String> onEventNameChanged;
|
||||||
|
final ValueChanged<Vendor> onVendorChanged;
|
||||||
|
final ValueChanged<DateTime> onStartDateChanged;
|
||||||
|
final ValueChanged<int> onDayToggled;
|
||||||
|
final ValueChanged<OrderHubUiModel> onHubChanged;
|
||||||
|
final VoidCallback onPositionAdded;
|
||||||
|
final void Function(int index, OrderPositionUiModel position) onPositionUpdated;
|
||||||
|
final void Function(int index) onPositionRemoved;
|
||||||
|
final VoidCallback onSubmit;
|
||||||
|
final VoidCallback onDone;
|
||||||
|
final VoidCallback onBack;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final TranslationsClientCreateOrderPermanentEn labels =
|
||||||
|
t.client_create_order.permanent;
|
||||||
|
final TranslationsClientCreateOrderOneTimeEn oneTimeLabels =
|
||||||
|
t.client_create_order.one_time;
|
||||||
|
|
||||||
|
if (status == OrderFormStatus.failure && errorMessage != null) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
UiSnackbar.show(
|
||||||
|
context,
|
||||||
|
message: translateErrorKey(errorMessage!),
|
||||||
|
type: UiSnackbarType.error,
|
||||||
|
margin: const EdgeInsets.only(bottom: 140, left: 16, right: 16),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status == OrderFormStatus.success) {
|
||||||
|
return PermanentOrderSuccessView(
|
||||||
|
title: labels.title,
|
||||||
|
message: labels.subtitle,
|
||||||
|
buttonLabel: oneTimeLabels.back_to_orders,
|
||||||
|
onDone: onDone,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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,
|
||||||
|
onPositionAdded: onPositionAdded,
|
||||||
|
onPositionUpdated: onPositionUpdated,
|
||||||
|
onPositionRemoved: onPositionRemoved,
|
||||||
|
),
|
||||||
|
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.onPositionAdded,
|
||||||
|
required this.onPositionUpdated,
|
||||||
|
required this.onPositionRemoved,
|
||||||
|
});
|
||||||
|
|
||||||
|
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 VoidCallback onPositionAdded;
|
||||||
|
final void Function(int index, OrderPositionUiModel position) onPositionUpdated;
|
||||||
|
final void Function(int index) onPositionRemoved;
|
||||||
|
|
||||||
|
@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),
|
||||||
|
_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.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,74 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
/// A date picker field for the recurring order form.
|
||||||
|
class RecurringOrderDatePicker extends StatefulWidget {
|
||||||
|
/// Creates a [RecurringOrderDatePicker].
|
||||||
|
const RecurringOrderDatePicker({
|
||||||
|
required this.label,
|
||||||
|
required this.value,
|
||||||
|
required this.onChanged,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The label text to display above the field.
|
||||||
|
final String label;
|
||||||
|
|
||||||
|
/// The currently selected date.
|
||||||
|
final DateTime value;
|
||||||
|
|
||||||
|
/// Callback when a new date is selected.
|
||||||
|
final ValueChanged<DateTime> onChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<RecurringOrderDatePicker> createState() =>
|
||||||
|
_RecurringOrderDatePickerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RecurringOrderDatePickerState extends State<RecurringOrderDatePicker> {
|
||||||
|
late final TextEditingController _controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = TextEditingController(
|
||||||
|
text: DateFormat('yyyy-MM-dd').format(widget.value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(RecurringOrderDatePicker oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (widget.value != oldWidget.value) {
|
||||||
|
_controller.text = DateFormat('yyyy-MM-dd').format(widget.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return UiTextField(
|
||||||
|
label: widget.label,
|
||||||
|
controller: _controller,
|
||||||
|
readOnly: true,
|
||||||
|
prefixIcon: UiIcons.calendar,
|
||||||
|
onTap: () async {
|
||||||
|
final DateTime? picked = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: widget.value,
|
||||||
|
firstDate: DateTime.now(),
|
||||||
|
lastDate: DateTime.now().add(const Duration(days: 365)),
|
||||||
|
);
|
||||||
|
if (picked != null) {
|
||||||
|
widget.onChanged(picked);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// A text input for the order name in the recurring order form.
|
||||||
|
class RecurringOrderEventNameInput extends StatefulWidget {
|
||||||
|
const RecurringOrderEventNameInput({
|
||||||
|
required this.label,
|
||||||
|
required this.value,
|
||||||
|
required this.onChanged,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String label;
|
||||||
|
final String value;
|
||||||
|
final ValueChanged<String> onChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<RecurringOrderEventNameInput> createState() =>
|
||||||
|
_RecurringOrderEventNameInputState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RecurringOrderEventNameInputState
|
||||||
|
extends State<RecurringOrderEventNameInput> {
|
||||||
|
late final TextEditingController _controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = TextEditingController(text: widget.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(RecurringOrderEventNameInput oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (widget.value != _controller.text) {
|
||||||
|
_controller.text = widget.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return UiTextField(
|
||||||
|
label: widget.label,
|
||||||
|
controller: _controller,
|
||||||
|
onChanged: widget.onChanged,
|
||||||
|
hintText: 'Order name',
|
||||||
|
prefixIcon: UiIcons.briefcase,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,321 @@
|
|||||||
|
import 'package:core_localization/core_localization.dart';
|
||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../order_ui_models.dart';
|
||||||
|
|
||||||
|
/// A card widget for editing a specific position in a recurring order.
|
||||||
|
class RecurringOrderPositionCard extends StatelessWidget {
|
||||||
|
const RecurringOrderPositionCard({
|
||||||
|
required this.index,
|
||||||
|
required this.position,
|
||||||
|
required this.isRemovable,
|
||||||
|
required this.onUpdated,
|
||||||
|
required this.onRemoved,
|
||||||
|
required this.positionLabel,
|
||||||
|
required this.roleLabel,
|
||||||
|
required this.workersLabel,
|
||||||
|
required this.startLabel,
|
||||||
|
required this.endLabel,
|
||||||
|
required this.lunchLabel,
|
||||||
|
required this.roles,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final int index;
|
||||||
|
final OrderPositionUiModel position;
|
||||||
|
final bool isRemovable;
|
||||||
|
final ValueChanged<OrderPositionUiModel> onUpdated;
|
||||||
|
final VoidCallback onRemoved;
|
||||||
|
final String positionLabel;
|
||||||
|
final String roleLabel;
|
||||||
|
final String workersLabel;
|
||||||
|
final String startLabel;
|
||||||
|
final String endLabel;
|
||||||
|
final String lunchLabel;
|
||||||
|
final List<OrderRoleUiModel> roles;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.white,
|
||||||
|
borderRadius: UiConstants.radiusLg,
|
||||||
|
border: Border.all(color: UiColors.border),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
'$positionLabel #${index + 1}',
|
||||||
|
style: UiTypography.footnote1m.textSecondary,
|
||||||
|
),
|
||||||
|
if (isRemovable)
|
||||||
|
GestureDetector(
|
||||||
|
onTap: onRemoved,
|
||||||
|
child: Text(
|
||||||
|
t.client_create_order.one_time.remove,
|
||||||
|
style: UiTypography.footnote1m.copyWith(
|
||||||
|
color: UiColors.destructive,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space3),
|
||||||
|
|
||||||
|
// Role (Dropdown)
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3),
|
||||||
|
height: 44,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: UiConstants.radiusMd,
|
||||||
|
border: Border.all(color: UiColors.border),
|
||||||
|
),
|
||||||
|
child: DropdownButtonHideUnderline(
|
||||||
|
child: DropdownButton<String>(
|
||||||
|
isExpanded: true,
|
||||||
|
hint: Text(
|
||||||
|
roleLabel,
|
||||||
|
style: UiTypography.body2r.textPlaceholder,
|
||||||
|
),
|
||||||
|
value: position.role.isEmpty ? null : position.role,
|
||||||
|
icon: const Icon(
|
||||||
|
UiIcons.chevronDown,
|
||||||
|
size: 18,
|
||||||
|
color: UiColors.iconSecondary,
|
||||||
|
),
|
||||||
|
onChanged: (String? val) {
|
||||||
|
if (val != null) {
|
||||||
|
onUpdated(position.copyWith(role: val));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
items: _buildRoleItems(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space3),
|
||||||
|
|
||||||
|
// Start/End/Workers Row
|
||||||
|
Row(
|
||||||
|
children: <Widget>[
|
||||||
|
// Start Time
|
||||||
|
Expanded(
|
||||||
|
child: _buildTimeInput(
|
||||||
|
context: context,
|
||||||
|
label: startLabel,
|
||||||
|
value: position.startTime,
|
||||||
|
onTap: () async {
|
||||||
|
final TimeOfDay? picked = await showTimePicker(
|
||||||
|
context: context,
|
||||||
|
initialTime: TimeOfDay.now(),
|
||||||
|
);
|
||||||
|
if (picked != null && context.mounted) {
|
||||||
|
onUpdated(
|
||||||
|
position.copyWith(startTime: picked.format(context)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space2),
|
||||||
|
// End Time
|
||||||
|
Expanded(
|
||||||
|
child: _buildTimeInput(
|
||||||
|
context: context,
|
||||||
|
label: endLabel,
|
||||||
|
value: position.endTime,
|
||||||
|
onTap: () async {
|
||||||
|
final TimeOfDay? picked = await showTimePicker(
|
||||||
|
context: context,
|
||||||
|
initialTime: TimeOfDay.now(),
|
||||||
|
);
|
||||||
|
if (picked != null && context.mounted) {
|
||||||
|
onUpdated(
|
||||||
|
position.copyWith(endTime: picked.format(context)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space2),
|
||||||
|
// Workers Count
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
workersLabel,
|
||||||
|
style: UiTypography.footnote2r.textSecondary,
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space1),
|
||||||
|
Container(
|
||||||
|
height: 40,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.bgSecondary,
|
||||||
|
borderRadius: UiConstants.radiusSm,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: <Widget>[
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
if (position.count > 1) {
|
||||||
|
onUpdated(
|
||||||
|
position.copyWith(count: position.count - 1),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Icon(UiIcons.minus, size: 12),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'${position.count}',
|
||||||
|
style: UiTypography.body2b.textPrimary,
|
||||||
|
),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
onUpdated(
|
||||||
|
position.copyWith(count: position.count + 1),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: const Icon(UiIcons.add, size: 12),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space4),
|
||||||
|
|
||||||
|
// Lunch Break
|
||||||
|
Text(lunchLabel, style: UiTypography.footnote2r.textSecondary),
|
||||||
|
const SizedBox(height: UiConstants.space1),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3),
|
||||||
|
height: 44,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: UiConstants.radiusMd,
|
||||||
|
border: Border.all(color: UiColors.border),
|
||||||
|
),
|
||||||
|
child: DropdownButtonHideUnderline(
|
||||||
|
child: DropdownButton<String>(
|
||||||
|
isExpanded: true,
|
||||||
|
value: position.lunchBreak,
|
||||||
|
icon: const Icon(
|
||||||
|
UiIcons.chevronDown,
|
||||||
|
size: 18,
|
||||||
|
color: UiColors.iconSecondary,
|
||||||
|
),
|
||||||
|
onChanged: (String? val) {
|
||||||
|
if (val != null) {
|
||||||
|
onUpdated(position.copyWith(lunchBreak: val));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
items: <String>[
|
||||||
|
'NO_BREAK',
|
||||||
|
'MIN_10',
|
||||||
|
'MIN_15',
|
||||||
|
'MIN_30',
|
||||||
|
'MIN_45',
|
||||||
|
'MIN_60',
|
||||||
|
].map((String value) {
|
||||||
|
final String label = switch (value) {
|
||||||
|
'NO_BREAK' => 'No Break',
|
||||||
|
'MIN_10' => '10 min (Paid)',
|
||||||
|
'MIN_15' => '15 min (Paid)',
|
||||||
|
'MIN_30' => '30 min (Unpaid)',
|
||||||
|
'MIN_45' => '45 min (Unpaid)',
|
||||||
|
'MIN_60' => '60 min (Unpaid)',
|
||||||
|
_ => value,
|
||||||
|
};
|
||||||
|
return DropdownMenuItem<String>(
|
||||||
|
value: value,
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: UiTypography.body2r.textPrimary,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTimeInput({
|
||||||
|
required BuildContext context,
|
||||||
|
required String label,
|
||||||
|
required String value,
|
||||||
|
required VoidCallback onTap,
|
||||||
|
}) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(label, style: UiTypography.footnote2r.textSecondary),
|
||||||
|
const SizedBox(height: UiConstants.space1),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Container(
|
||||||
|
height: 40,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: UiConstants.radiusSm,
|
||||||
|
border: Border.all(color: UiColors.border),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
value.isEmpty ? '--:--' : value,
|
||||||
|
style: UiTypography.body2r.textPrimary,
|
||||||
|
),
|
||||||
|
const Icon(
|
||||||
|
UiIcons.clock,
|
||||||
|
size: 14,
|
||||||
|
color: UiColors.iconSecondary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<DropdownMenuItem<String>> _buildRoleItems() {
|
||||||
|
final List<DropdownMenuItem<String>> items = roles
|
||||||
|
.map(
|
||||||
|
(OrderRoleUiModel role) => DropdownMenuItem<String>(
|
||||||
|
value: role.id,
|
||||||
|
child: Text(
|
||||||
|
'${role.name} - \$${role.costPerHour.toStringAsFixed(0)}',
|
||||||
|
style: UiTypography.body2r.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final bool hasSelected = roles.any((OrderRoleUiModel role) => role.id == position.role);
|
||||||
|
if (position.role.isNotEmpty && !hasSelected) {
|
||||||
|
items.add(
|
||||||
|
DropdownMenuItem<String>(
|
||||||
|
value: position.role,
|
||||||
|
child: Text(
|
||||||
|
position.role,
|
||||||
|
style: UiTypography.body2r.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// A header widget for sections in the recurring order form.
|
||||||
|
class RecurringOrderSectionHeader extends StatelessWidget {
|
||||||
|
/// Creates a [RecurringOrderSectionHeader].
|
||||||
|
const RecurringOrderSectionHeader({
|
||||||
|
required this.title,
|
||||||
|
this.actionLabel,
|
||||||
|
this.onAction,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The title text for the section.
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
/// Optional label for an action button on the right.
|
||||||
|
final String? actionLabel;
|
||||||
|
|
||||||
|
/// Callback when the action button is tapped.
|
||||||
|
final VoidCallback? onAction;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(title, style: UiTypography.headline4m.textPrimary),
|
||||||
|
if (actionLabel != null && onAction != null)
|
||||||
|
TextButton(
|
||||||
|
onPressed: onAction,
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
minimumSize: Size.zero,
|
||||||
|
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
const Icon(UiIcons.add, size: 16, color: UiColors.primary),
|
||||||
|
const SizedBox(width: UiConstants.space2),
|
||||||
|
Text(
|
||||||
|
actionLabel!,
|
||||||
|
style: UiTypography.body2m.primary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// A view to display when a recurring order has been successfully created.
|
||||||
|
class RecurringOrderSuccessView extends StatelessWidget {
|
||||||
|
/// Creates a [RecurringOrderSuccessView].
|
||||||
|
const RecurringOrderSuccessView({
|
||||||
|
required this.title,
|
||||||
|
required this.message,
|
||||||
|
required this.buttonLabel,
|
||||||
|
required this.onDone,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The title of the success message.
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
/// The body of the success message.
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
/// Label for the completion button.
|
||||||
|
final String buttonLabel;
|
||||||
|
|
||||||
|
/// Callback when the completion button is tapped.
|
||||||
|
final VoidCallback onDone;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: Container(
|
||||||
|
width: double.infinity,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: <Color>[UiColors.primary, UiColors.buttonPrimaryHover],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: SafeArea(
|
||||||
|
child: Center(
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: UiConstants.space10),
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.white,
|
||||||
|
borderRadius: UiConstants.radiusLg * 1.5,
|
||||||
|
boxShadow: <BoxShadow>[
|
||||||
|
BoxShadow(
|
||||||
|
color: UiColors.black.withValues(alpha: 0.2),
|
||||||
|
blurRadius: 20,
|
||||||
|
offset: const Offset(0, UiConstants.space2 + 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
Container(
|
||||||
|
width: UiConstants.space16,
|
||||||
|
height: UiConstants.space16,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: UiColors.accent,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: const Center(
|
||||||
|
child: Icon(
|
||||||
|
UiIcons.check,
|
||||||
|
color: UiColors.black,
|
||||||
|
size: UiConstants.space8,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space6),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: UiTypography.headline2m.textPrimary,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space3),
|
||||||
|
Text(
|
||||||
|
message,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: UiTypography.body2r.textSecondary.copyWith(
|
||||||
|
height: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space8),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: UiButton.primary(
|
||||||
|
text: buttonLabel,
|
||||||
|
onPressed: onDone,
|
||||||
|
size: UiButtonSize.large,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,486 @@
|
|||||||
|
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 '../order_ui_models.dart';
|
||||||
|
import 'recurring_order_date_picker.dart';
|
||||||
|
import 'recurring_order_event_name_input.dart';
|
||||||
|
import 'recurring_order_header.dart';
|
||||||
|
import 'recurring_order_position_card.dart';
|
||||||
|
import 'recurring_order_section_header.dart';
|
||||||
|
import 'recurring_order_success_view.dart';
|
||||||
|
|
||||||
|
/// The main content of the Recurring Order page.
|
||||||
|
class RecurringOrderView extends StatelessWidget {
|
||||||
|
const RecurringOrderView({
|
||||||
|
required this.status,
|
||||||
|
required this.errorMessage,
|
||||||
|
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.isValid,
|
||||||
|
required this.onEventNameChanged,
|
||||||
|
required this.onVendorChanged,
|
||||||
|
required this.onStartDateChanged,
|
||||||
|
required this.onEndDateChanged,
|
||||||
|
required this.onDayToggled,
|
||||||
|
required this.onHubChanged,
|
||||||
|
required this.onPositionAdded,
|
||||||
|
required this.onPositionUpdated,
|
||||||
|
required this.onPositionRemoved,
|
||||||
|
required this.onSubmit,
|
||||||
|
required this.onDone,
|
||||||
|
required this.onBack,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final OrderFormStatus status;
|
||||||
|
final String? errorMessage;
|
||||||
|
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 bool isValid;
|
||||||
|
|
||||||
|
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 VoidCallback onPositionAdded;
|
||||||
|
final void Function(int index, OrderPositionUiModel position) onPositionUpdated;
|
||||||
|
final void Function(int index) onPositionRemoved;
|
||||||
|
final VoidCallback onSubmit;
|
||||||
|
final VoidCallback onDone;
|
||||||
|
final VoidCallback onBack;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final TranslationsClientCreateOrderRecurringEn labels =
|
||||||
|
t.client_create_order.recurring;
|
||||||
|
final TranslationsClientCreateOrderOneTimeEn oneTimeLabels =
|
||||||
|
t.client_create_order.one_time;
|
||||||
|
|
||||||
|
if (status == OrderFormStatus.failure && errorMessage != null) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
final String message = errorMessage == 'placeholder'
|
||||||
|
? labels.placeholder
|
||||||
|
: translateErrorKey(errorMessage!);
|
||||||
|
UiSnackbar.show(
|
||||||
|
context,
|
||||||
|
message: message,
|
||||||
|
type: UiSnackbarType.error,
|
||||||
|
margin: const EdgeInsets.only(bottom: 140, left: 16, right: 16),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status == OrderFormStatus.success) {
|
||||||
|
return RecurringOrderSuccessView(
|
||||||
|
title: labels.title,
|
||||||
|
message: labels.subtitle,
|
||||||
|
buttonLabel: oneTimeLabels.back_to_orders,
|
||||||
|
onDone: onDone,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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,
|
||||||
|
onPositionAdded: onPositionAdded,
|
||||||
|
onPositionUpdated: onPositionUpdated,
|
||||||
|
onPositionRemoved: onPositionRemoved,
|
||||||
|
),
|
||||||
|
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.onPositionAdded,
|
||||||
|
required this.onPositionUpdated,
|
||||||
|
required this.onPositionRemoved,
|
||||||
|
});
|
||||||
|
|
||||||
|
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 VoidCallback onPositionAdded;
|
||||||
|
final void Function(int index, OrderPositionUiModel position) onPositionUpdated;
|
||||||
|
final void Function(int index) onPositionRemoved;
|
||||||
|
|
||||||
|
@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),
|
||||||
|
_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.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