Merge pull request #640 from Oloodi/588-implement-review-order-before-submitting-in-frontend

Completed the implementation of the review order before submitting in frontend
This commit is contained in:
Achintha Isuru
2026-03-10 11:09:32 -04:00
committed by GitHub
39 changed files with 1548 additions and 1455 deletions

View File

@@ -58,6 +58,7 @@ and load any additional skills as needed for specific review challenges.
6. Missing tests for use cases or repositories
7. Complex BLoC without bloc_test coverage
8. Test coverage below 70% for business logic
9. Hardcoded user-facing strings — must use `core_localization` (Slang) via `t.<section>.<key>`. All `Text('...')` with literal English/Spanish strings in presentation layer must be replaced with localized keys
### MODERATE (Request Fix, can be deferred with justification):
1. Missing doc comments on public APIs
@@ -103,7 +104,7 @@ Verify:
- Business logic resides exclusively in use cases
- Entities are in domain, models in data, widgets in presentation
### Step 3: Design System Compliance
### Step 3: Design System & Localization Compliance
```bash
# Hardcoded colors
@@ -117,9 +118,22 @@ grep -rn -E "EdgeInsets\.(all|symmetric|only)\(" apps/mobile/apps/*/lib/features
# Direct icon imports
grep -rn "^import.*icons" apps/mobile/apps/*/lib/features/
# Hardcoded user-facing strings (look for Text('...') with literal strings)
grep -rn "Text(['\"]" apps/mobile/packages/features/
# Also check for hardcoded strings in SnackBar, AlertDialog, AppBar title, etc.
grep -rn "title: ['\"]" apps/mobile/packages/features/
grep -rn "label: ['\"]" apps/mobile/packages/features/
grep -rn "hintText: ['\"]" apps/mobile/packages/features/
```
All styling must come from the design system. No exceptions.
All styling must come from the design system. All user-facing strings must come from `core_localization` via Slang (`t.<section>.<key>`). No exceptions.
**Localization rules:**
- Strings defined in `packages/core_localization/lib/src/l10n/en.i18n.json` and `es.i18n.json`
- Accessed via `t.<section>.<key>` (e.g., `t.client_create_order.review.invalid_arguments`)
- Both `en` and `es` JSON files must be updated together
- Regenerate with `dart run slang` from `packages/core_localization/` directory
### Step 4: State Management Review
For every BLoC in changed files, verify:

View File

@@ -1,12 +1,12 @@
---
name: mobile-feature-builder
name: mobile-builder
description: "Use this agent when implementing new mobile features or modifying existing features in the KROW Workforce staff or client mobile apps. This includes creating new feature modules, adding screens, implementing BLoCs, writing use cases, building repository implementations, integrating Firebase Data Connect, and writing tests for mobile features. Examples:\\n\\n- User: \"Add a shift swap feature to the staff app\"\\n Assistant: \"I'll use the mobile-feature-builder agent to implement the shift swap feature following Clean Architecture principles.\"\\n <commentary>Since the user is requesting a new mobile feature, use the Agent tool to launch the mobile-feature-builder agent to plan and implement the feature with proper domain/data/presentation layers.</commentary>\\n\\n- User: \"Create a new notifications screen in the client app with real-time updates\"\\n Assistant: \"Let me launch the mobile-feature-builder agent to implement the notifications feature with proper BLoC state management and Firebase integration.\"\\n <commentary>Since the user wants a new mobile screen with state management, use the Agent tool to launch the mobile-feature-builder agent to build it with correct architecture.</commentary>\\n\\n- User: \"The timesheet feature needs a new use case for calculating overtime\"\\n Assistant: \"I'll use the mobile-feature-builder agent to add the overtime calculation use case to the timesheet feature's domain layer.\"\\n <commentary>Since the user is requesting business logic additions to a mobile feature, use the Agent tool to launch the mobile-feature-builder agent to implement it in the correct layer.</commentary>\\n\\n- User: \"Write tests for the job listing BLoC in the staff app\"\\n Assistant: \"Let me use the mobile-feature-builder agent to write comprehensive BLoC tests using bloc_test and mocktail.\"\\n <commentary>Since the user wants mobile feature tests written, use the Agent tool to launch the mobile-feature-builder agent which knows the testing patterns and conventions.</commentary>"
model: opus
color: blue
memory: project
---
You are the **Mobile Feature Agent**, an elite Flutter/Dart engineer specializing in Clean Architecture mobile development for the KROW Workforce platform. You have deep expertise in BLoC state management, feature-first packaging, and design system compliance. You enforce **zero tolerance for architectural violations**.
You are the **Mobile Development Agent**, an elite Flutter/Dart engineer specializing in Clean Architecture mobile development for the KROW Workforce platform. You have deep expertise in BLoC state management, feature-first packaging, and design system compliance. You enforce **zero tolerance for architectural violations**.
## Initial Setup

View File

@@ -266,11 +266,29 @@ design_system/
- Export `TranslationProvider` for `context.strings` access
- Map domain failures to localized error messages via `ErrorTranslator`
**String Definition:**
- Strings are defined in `packages/core_localization/lib/src/l10n/en.i18n.json` (English) and `es.i18n.json` (Spanish)
- Both files MUST be updated together when adding/modifying strings
- Generated output: `strings.g.dart`, `strings_en.g.dart`, `strings_es.g.dart`
- Regenerate with: `cd packages/core_localization && dart run slang`
**Feature Integration:**
```dart
// Features access strings
Text(context.strings.loginButton)
// ✅ CORRECT: Access via Slang's global `t` accessor
import 'package:core_localization/core_localization.dart';
Text(t.client_create_order.review.invalid_arguments)
Text(t.errors.order.creation_failed)
// ❌ FORBIDDEN: Hardcoded user-facing strings
Text('Invalid review arguments') // Must use localized key
Text('Order created!') // Must use localized key
```
**RESTRICTION:** ALL user-facing strings in the presentation layer (Text widgets, SnackBars, AppBar titles, hints, labels, error messages, dialogs) MUST use localized keys via `t.<section>.<key>`. No hardcoded English or Spanish strings.
**BLoC Error Flow:**
```dart
// BLoCs emit domain failures (not strings)
emit(AuthError(InvalidCredentialsFailure()));
@@ -879,6 +897,11 @@ Navigator.push(context, MaterialPageRoute(...)); // ← Use Modular
Modular.to.navigate('/profile'); // ← Use safe extensions
```
**Hardcoded user-facing strings**
```dart
Text('Order created successfully!'); // ← Use t.section.key from core_localization
```
## Summary
The architecture enforces:

View File

@@ -399,7 +399,31 @@
"placeholder": "Permanent Order Flow (Work in Progress)"
},
"review": {
"invalid_arguments": "Unable to load order review. Please go back and try again."
"invalid_arguments": "Unable to load order review. Please go back and try again.",
"title": "Review & Submit",
"subtitle": "Confirm details before posting",
"edit": "Edit",
"basics": "Basics",
"order_name": "Order Name",
"hub": "Hub",
"shift_contact": "Shift Contact",
"schedule": "Schedule",
"date": "Date",
"time": "Time",
"duration": "Duration",
"start_date": "Start Date",
"end_date": "End Date",
"repeat": "Repeat",
"positions": "POSITIONS",
"total": "Total",
"estimated_total": "Estimated Total",
"estimated_weekly_total": "Estimated Weekly Total",
"post_order": "Post Order",
"hours_suffix": "hrs"
},
"rapid_draft": {
"title": "Rapid Order",
"subtitle": "Verify the order details"
}
},
"client_main": {

View File

@@ -399,7 +399,31 @@
"placeholder": "Flujo de Orden Permanente (Trabajo en Progreso)"
},
"review": {
"invalid_arguments": "No se pudo cargar la revisi\u00f3n de la orden. Por favor, regresa e intenta de nuevo."
"invalid_arguments": "No se pudo cargar la revisi\u00f3n de la orden. Por favor, regresa e intenta de nuevo.",
"title": "Revisar y Enviar",
"subtitle": "Confirma los detalles antes de publicar",
"edit": "Editar",
"basics": "Datos B\u00e1sicos",
"order_name": "Nombre de la Orden",
"hub": "Hub",
"shift_contact": "Contacto del Turno",
"schedule": "Horario",
"date": "Fecha",
"time": "Hora",
"duration": "Duraci\u00f3n",
"start_date": "Fecha de Inicio",
"end_date": "Fecha de Fin",
"repeat": "Repetir",
"positions": "POSICIONES",
"total": "Total",
"estimated_total": "Total Estimado",
"estimated_weekly_total": "Total Semanal Estimado",
"post_order": "Publicar Orden",
"hours_suffix": "hrs"
},
"rapid_draft": {
"title": "Orden R\u00e1pida",
"subtitle": "Verifica los detalles de la orden"
}
},
"client_main": {

View File

@@ -1,5 +1,6 @@
import 'package:equatable/equatable.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../utils/time_parsing_utils.dart';
enum OneTimeOrderStatus { initial, loading, success, failure }
@@ -162,9 +163,8 @@ class OneTimeOrderState extends Equatable {
buffer.write('$minutes min');
}
if (first.lunchBreak != null &&
first.lunchBreak != 'NO_BREAK' &&
first.lunchBreak!.isNotEmpty) {
if (first.lunchBreak != 'NO_BREAK' &&
first.lunchBreak.isNotEmpty) {
buffer.write(' (${first.lunchBreak} break)');
}

View File

@@ -170,10 +170,10 @@ class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
await _loadRolesForVendor(event.vendor.id, emit);
}
void _onHubsLoaded(
Future<void> _onHubsLoaded(
PermanentOrderHubsLoaded event,
Emitter<PermanentOrderState> emit,
) {
) async {
final PermanentOrderHubOption? selectedHub = event.hubs.isNotEmpty
? event.hubs.first
: null;
@@ -186,16 +186,16 @@ class PermanentOrderBloc extends Bloc<PermanentOrderEvent, PermanentOrderState>
);
if (selectedHub != null) {
_loadManagersForHub(selectedHub.id, emit);
await _loadManagersForHub(selectedHub.id, emit);
}
}
void _onHubChanged(
Future<void> _onHubChanged(
PermanentOrderHubChanged event,
Emitter<PermanentOrderState> emit,
) {
) async {
emit(state.copyWith(selectedHub: event.hub, location: event.hub.name));
_loadManagersForHub(event.hub.id, emit);
await _loadManagersForHub(event.hub.id, emit);
}
void _onHubManagerChanged(

View File

@@ -148,8 +148,8 @@ class PermanentOrderState extends Equatable {
sum + (p.count * roleCostById(p.role)),
);
/// Estimated total cost: sum of (count * costPerHour * hours) per position.
double get estimatedTotal {
/// Daily cost: sum of (count * costPerHour * hours) per position.
double get dailyCost {
double total = 0;
for (final PermanentOrderPosition p in positions) {
final double hours = parseHoursFromTimes(p.startTime, p.endTime);
@@ -158,6 +158,12 @@ class PermanentOrderState extends Equatable {
return total;
}
/// Estimated weekly total cost for the permanent order.
///
/// Calculated as [dailyCost] multiplied by the number of selected
/// [permanentDays] per week.
double get estimatedTotal => dailyCost * permanentDays.length;
/// Formatted repeat days (e.g. "Mon, Tue, Wed").
String get formattedRepeatDays => permanentDays.map(
(String day) => day[0] + day.substring(1).toLowerCase(),

View File

@@ -1,5 +1,6 @@
import 'package:equatable/equatable.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../utils/schedule_utils.dart';
import '../../utils/time_parsing_utils.dart';
enum RecurringOrderStatus { initial, loading, success, failure }
@@ -155,8 +156,8 @@ class RecurringOrderState extends Equatable {
sum + (p.count * roleCostById(p.role)),
);
/// Estimated total cost: sum of (count * costPerHour * hours) per position.
double get estimatedTotal {
/// Daily cost: sum of (count * costPerHour * hours) per position.
double get dailyCost {
double total = 0;
for (final RecurringOrderPosition p in positions) {
final double hours = parseHoursFromTimes(p.startTime, p.endTime);
@@ -165,6 +166,31 @@ class RecurringOrderState extends Equatable {
return total;
}
/// Total number of working days between [startDate] and [endDate]
/// (inclusive) that match the selected [recurringDays].
///
/// Iterates day-by-day and counts each date whose weekday label
/// (e.g. "MON", "TUE") appears in [recurringDays].
int get totalWorkingDays {
final Set<String> selectedSet = recurringDays.toSet();
int count = 0;
for (
DateTime day = startDate;
!day.isAfter(endDate);
day = day.add(const Duration(days: 1))
) {
if (selectedSet.contains(weekdayLabel(day))) {
count++;
}
}
return count;
}
/// Estimated total cost for the entire recurring order period.
///
/// Calculated as [dailyCost] multiplied by [totalWorkingDays].
double get estimatedTotal => dailyCost * totalWorkingDays;
/// Formatted repeat days (e.g. "Mon, Tue, Wed").
String get formattedRepeatDays => recurringDays.map(
(String day) => day[0] + day.substring(1).toLowerCase(),

View File

@@ -25,6 +25,7 @@ class ReviewOrderArguments {
this.scheduleStartDate,
this.scheduleEndDate,
this.scheduleRepeatDays,
this.totalLabel,
});
final ReviewOrderType orderType;
@@ -45,4 +46,7 @@ class ReviewOrderArguments {
final String? scheduleStartDate;
final String? scheduleEndDate;
final String? scheduleRepeatDays;
/// Optional label override for the total banner (e.g. "Estimated Weekly Total").
final String? totalLabel;
}

View File

@@ -1,7 +1,8 @@
import 'package:client_orders_common/client_orders_common.dart';
import 'package:core_localization/core_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:client_orders_common/client_orders_common.dart';
import 'package:intl/intl.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
@@ -9,6 +10,7 @@ import '../blocs/one_time_order/one_time_order_bloc.dart';
import '../blocs/one_time_order/one_time_order_event.dart';
import '../blocs/one_time_order/one_time_order_state.dart';
import '../models/review_order_arguments.dart';
import '../utils/time_parsing_utils.dart';
import '../widgets/review_order/review_order_positions_card.dart';
/// Page for creating a one-time staffing order.
@@ -59,8 +61,8 @@ class OneTimeOrderPage extends StatelessWidget {
: null,
hubManagers: state.managers.map(_mapManager).toList(),
isValid: state.isValid,
title: state.isRapidDraft ? 'Rapid Order' : null,
subtitle: state.isRapidDraft ? 'Verify the order details' : null,
title: state.isRapidDraft ? t.client_create_order.rapid_draft.title : null,
subtitle: state.isRapidDraft ? t.client_create_order.rapid_draft.subtitle : null,
onEventNameChanged: (String val) =>
bloc.add(OneTimeOrderEventNameChanged(val)),
onVendorChanged: (Vendor val) =>
@@ -116,6 +118,9 @@ class OneTimeOrderPage extends StatelessWidget {
roleName: state.roleNameById(p.role) ?? p.role,
workerCount: p.count,
costPerHour: state.roleCostById(p.role),
hours: parseHoursFromTimes(p.startTime, p.endTime),
startTime: p.startTime,
endTime: p.endTime,
),
).toList();

View File

@@ -1,3 +1,4 @@
import 'package:core_localization/core_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
@@ -10,6 +11,7 @@ import '../blocs/permanent_order/permanent_order_event.dart';
import '../blocs/permanent_order/permanent_order_state.dart';
import '../models/review_order_arguments.dart';
import '../utils/schedule_utils.dart';
import '../utils/time_parsing_utils.dart';
import '../widgets/review_order/review_order_positions_card.dart';
/// Page for creating a permanent staffing order.
@@ -128,6 +130,9 @@ class PermanentOrderPage extends StatelessWidget {
roleName: state.roleNameById(p.role) ?? p.role,
workerCount: p.count,
costPerHour: state.roleCostById(p.role),
hours: parseHoursFromTimes(p.startTime, p.endTime),
startTime: p.startTime,
endTime: p.endTime,
),
).toList();
@@ -143,6 +148,7 @@ class PermanentOrderPage extends StatelessWidget {
estimatedTotal: state.estimatedTotal,
scheduleStartDate: DateFormat.yMMMd().format(state.startDate),
scheduleRepeatDays: state.formattedRepeatDays,
totalLabel: t.client_create_order.review.estimated_weekly_total,
),
);

View File

@@ -10,6 +10,7 @@ import '../blocs/recurring_order/recurring_order_event.dart';
import '../blocs/recurring_order/recurring_order_state.dart';
import '../models/review_order_arguments.dart';
import '../utils/schedule_utils.dart';
import '../utils/time_parsing_utils.dart';
import '../widgets/review_order/review_order_positions_card.dart';
/// Page for creating a recurring staffing order.
@@ -138,6 +139,9 @@ class RecurringOrderPage extends StatelessWidget {
roleName: state.roleNameById(p.role) ?? p.role,
workerCount: p.count,
costPerHour: state.roleCostById(p.role),
hours: parseHoursFromTimes(p.startTime, p.endTime),
startTime: p.startTime,
endTime: p.endTime,
),
).toList();

View File

@@ -52,6 +52,7 @@ class ReviewOrderPage extends StatelessWidget {
totalWorkers: args.totalWorkers,
totalCostPerHour: args.totalCostPerHour,
estimatedTotal: args.estimatedTotal,
totalLabel: args.totalLabel,
showEditButtons: showEdit,
onEditBasics: showEdit ? () => Modular.to.popSafe() : null,
onEditSchedule: showEdit ? () => Modular.to.popSafe() : null,

View File

@@ -1,3 +1,4 @@
import 'package:core_localization/core_localization.dart';
import 'package:flutter/material.dart';
import 'review_order_info_row.dart';
import 'review_order_section_card.dart';
@@ -20,11 +21,11 @@ class OneTimeScheduleSection extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ReviewOrderSectionCard(
title: 'Schedule',
title: t.client_create_order.review.schedule,
children: <Widget>[
ReviewOrderInfoRow(label: 'Date', value: date),
ReviewOrderInfoRow(label: 'Time', value: time),
ReviewOrderInfoRow(label: 'Duration', value: duration),
ReviewOrderInfoRow(label: t.client_create_order.review.date, value: date),
ReviewOrderInfoRow(label: t.client_create_order.review.time, value: time),
ReviewOrderInfoRow(label: t.client_create_order.review.duration, value: duration),
],
);
}

View File

@@ -1,3 +1,4 @@
import 'package:core_localization/core_localization.dart';
import 'package:flutter/material.dart';
import 'review_order_info_row.dart';
import 'review_order_section_card.dart';
@@ -20,11 +21,11 @@ class PermanentScheduleSection extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ReviewOrderSectionCard(
title: 'Schedule',
title: t.client_create_order.review.schedule,
onEdit: onEdit,
children: <Widget>[
ReviewOrderInfoRow(label: 'Start Date', value: startDate),
ReviewOrderInfoRow(label: 'Repeat', value: repeatDays),
ReviewOrderInfoRow(label: t.client_create_order.review.start_date, value: startDate),
ReviewOrderInfoRow(label: t.client_create_order.review.repeat, value: repeatDays),
],
);
}

View File

@@ -1,3 +1,4 @@
import 'package:core_localization/core_localization.dart';
import 'package:flutter/material.dart';
import 'review_order_info_row.dart';
import 'review_order_section_card.dart';
@@ -22,12 +23,12 @@ class RecurringScheduleSection extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ReviewOrderSectionCard(
title: 'Schedule',
title: t.client_create_order.review.schedule,
onEdit: onEdit,
children: <Widget>[
ReviewOrderInfoRow(label: 'Start Date', value: startDate),
ReviewOrderInfoRow(label: 'End Date', value: endDate),
ReviewOrderInfoRow(label: 'Repeat', value: repeatDays),
ReviewOrderInfoRow(label: t.client_create_order.review.start_date, value: startDate),
ReviewOrderInfoRow(label: t.client_create_order.review.end_date, value: endDate),
ReviewOrderInfoRow(label: t.client_create_order.review.repeat, value: repeatDays),
],
);
}

View File

@@ -1,3 +1,4 @@
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
@@ -9,14 +10,14 @@ class ReviewOrderActionBar extends StatelessWidget {
const ReviewOrderActionBar({
required this.onBack,
required this.onSubmit,
this.submitLabel = 'Post Order',
this.submitLabel,
this.isLoading = false,
super.key,
});
final VoidCallback onBack;
final VoidCallback? onSubmit;
final String submitLabel;
final String? submitLabel;
final bool isLoading;
@override
@@ -32,38 +33,19 @@ class ReviewOrderActionBar extends StatelessWidget {
),
child: Row(
children: <Widget>[
SizedBox(
width: 80,
height: 52,
child: OutlinedButton(
onPressed: onBack,
style: OutlinedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: UiConstants.radiusXl,
),
side: const BorderSide(
color: UiColors.border,
width: 1.5,
),
),
child: const Icon(
UiIcons.chevronLeft,
size: UiConstants.iconMd,
color: UiColors.iconPrimary,
),
),
UiButton.secondary(
leadingIcon: UiIcons.chevronLeft,
onPressed: onBack,
size: UiButtonSize.large,
text: '',
),
const SizedBox(width: UiConstants.space3),
Expanded(
child: SizedBox(
height: 52,
child: UiButton.primary(
text: submitLabel,
onPressed: onSubmit,
isLoading: isLoading,
size: UiButtonSize.large,
fullWidth: true,
),
child: UiButton.primary(
text: submitLabel ?? t.client_create_order.review.post_order,
onPressed: onSubmit,
isLoading: isLoading,
size: UiButtonSize.large,
),
),
],

View File

@@ -1,3 +1,4 @@
import 'package:core_localization/core_localization.dart';
import 'package:flutter/material.dart';
import 'review_order_info_row.dart';
import 'review_order_section_card.dart';
@@ -21,12 +22,12 @@ class ReviewOrderBasicsCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ReviewOrderSectionCard(
title: 'Basics',
title: t.client_create_order.review.basics,
onEdit: onEdit,
children: <Widget>[
ReviewOrderInfoRow(label: 'Order Name', value: orderName),
ReviewOrderInfoRow(label: 'Hub', value: hubName),
ReviewOrderInfoRow(label: 'Shift Contact', value: shiftContactName),
ReviewOrderInfoRow(label: t.client_create_order.review.order_name, value: orderName),
ReviewOrderInfoRow(label: t.client_create_order.review.hub, value: hubName),
ReviewOrderInfoRow(label: t.client_create_order.review.shift_contact, value: shiftContactName),
],
);
}

View File

@@ -1,33 +0,0 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// Displays the "Review & Submit" title and subtitle at the top of the
/// review order page.
class ReviewOrderHeader extends StatelessWidget {
const ReviewOrderHeader({super.key});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(
left: UiConstants.space6,
right: UiConstants.space6,
top: UiConstants.space4,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'Review & Submit',
style: UiTypography.headline2m,
),
const SizedBox(height: UiConstants.space1),
Text(
'Confirm details before posting',
style: UiTypography.body2r.textSecondary,
),
],
),
);
}
}

View File

@@ -19,18 +19,18 @@ class ReviewOrderInfoRow extends StatelessWidget {
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
spacing: UiConstants.space2,
children: <Widget>[
Flexible(
child: Text(
label,
style: UiTypography.body3r.textSecondary,
style: UiTypography.body2r.textSecondary,
),
),
const SizedBox(width: UiConstants.space3),
Flexible(
child: Text(
value,
style: UiTypography.body3m,
style: UiTypography.body2m,
textAlign: TextAlign.end,
),
),

View File

@@ -1,12 +1,16 @@
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'review_order_info_row.dart';
/// Displays a summary of all positions with a divider and total row.
///
/// Each position shows the role name and "N workers . $X/hr".
/// Each position is rendered as a two-line layout:
/// - Line 1: role name (left) and worker count with cost/hr (right).
/// - Line 2: time range and shift hours (right-aligned, muted style).
///
/// A divider separates the individual positions from the total.
class ReviewOrderPositionsCard extends StatelessWidget {
/// Creates a [ReviewOrderPositionsCard].
const ReviewOrderPositionsCard({
required this.positions,
required this.totalWorkers,
@@ -15,9 +19,16 @@ class ReviewOrderPositionsCard extends StatelessWidget {
super.key,
});
/// The list of position items to display.
final List<ReviewPositionItem> positions;
/// The total number of workers across all positions.
final int totalWorkers;
/// The combined cost per hour across all positions.
final double totalCostPerHour;
/// Optional callback invoked when the user taps "Edit".
final VoidCallback? onEdit;
@override
@@ -26,7 +37,7 @@ class ReviewOrderPositionsCard extends StatelessWidget {
decoration: BoxDecoration(
color: UiColors.white,
borderRadius: UiConstants.radiusXl,
border: Border.all(color: UiColors.border),
border: Border.all(color: UiColors.border, width: 0.5),
),
padding: const EdgeInsets.all(UiConstants.space4),
child: Column(
@@ -36,29 +47,20 @@ class ReviewOrderPositionsCard extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
'POSITIONS',
t.client_create_order.review.positions,
style: UiTypography.titleUppercase4b.textSecondary,
),
if (onEdit != null)
GestureDetector(
onTap: onEdit,
child: Text(
'Edit',
t.client_create_order.review.edit,
style: UiTypography.body3m.primary,
),
),
],
),
...positions.map(
(ReviewPositionItem position) => Padding(
padding: const EdgeInsets.only(top: UiConstants.space3),
child: ReviewOrderInfoRow(
label: position.roleName,
value:
'${position.workerCount} workers \u00B7 \$${position.costPerHour.toStringAsFixed(0)}/hr',
),
),
),
...positions.map(_buildPositionItem),
Padding(
padding: const EdgeInsets.only(top: UiConstants.space3),
child: Container(
@@ -72,12 +74,13 @@ class ReviewOrderPositionsCard extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
'Total',
style: UiTypography.body3m,
t.client_create_order.review.total,
style: UiTypography.body2m,
),
Text(
'$totalWorkers workers \u00B7 \$${totalCostPerHour.toStringAsFixed(0)}/hr',
style: UiTypography.body3b.primary,
'$totalWorkers workers \u00B7 '
'\$${totalCostPerHour.toStringAsFixed(0)}/hr',
style: UiTypography.body2b.primary,
),
],
),
@@ -86,17 +89,83 @@ class ReviewOrderPositionsCard extends StatelessWidget {
),
);
}
/// Builds a two-line widget for a single position.
///
/// Line 1 shows the role name on the left and worker count with cost on
/// the right. Line 2 shows the time range and shift hours, right-aligned
/// in a secondary/muted style.
Widget _buildPositionItem(ReviewPositionItem position) {
final String formattedHours = position.hours % 1 == 0
? position.hours.toInt().toString()
: position.hours.toStringAsFixed(1);
return Padding(
padding: const EdgeInsets.only(top: UiConstants.space3),
child: Column(
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Flexible(
child: Text(
position.roleName,
style: UiTypography.body2m.textSecondary,
),
),
Text(
'${position.workerCount} workers \u00B7 '
'\$${position.costPerHour.toStringAsFixed(0)}/hr',
style: UiTypography.body2m,
),
],
),
const SizedBox(height: UiConstants.space1),
Align(
alignment: Alignment.centerRight,
child: Text(
'${position.startTime} - ${position.endTime} \u00B7 '
'$formattedHours '
'${t.client_create_order.review.hours_suffix}',
style: UiTypography.body3r.textTertiary,
),
),
],
),
);
}
}
/// A single position item for the positions card.
///
/// Contains the role name, worker count, shift hours, hourly cost,
/// and the start/end times for one position in the review summary.
class ReviewPositionItem {
/// Creates a [ReviewPositionItem].
const ReviewPositionItem({
required this.roleName,
required this.workerCount,
required this.costPerHour,
required this.hours,
required this.startTime,
required this.endTime,
});
/// The display name of the role for this position.
final String roleName;
/// The number of workers requested for this position.
final int workerCount;
/// The cost per hour for this role.
final double costPerHour;
/// The number of shift hours (derived from start/end time).
final double hours;
/// The formatted start time of the shift (e.g. "08:00 AM").
final String startTime;
/// The formatted end time of the shift (e.g. "04:00 PM").
final String endTime;
}

View File

@@ -1,3 +1,4 @@
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
@@ -23,7 +24,7 @@ class ReviewOrderSectionCard extends StatelessWidget {
decoration: BoxDecoration(
color: UiColors.white,
borderRadius: UiConstants.radiusXl,
border: Border.all(color: UiColors.border),
border: Border.all(color: UiColors.border, width: 0.5),
),
padding: const EdgeInsets.all(UiConstants.space4),
child: Column(
@@ -39,10 +40,7 @@ class ReviewOrderSectionCard extends StatelessWidget {
if (onEdit != null)
GestureDetector(
onTap: onEdit,
child: Text(
'Edit',
style: UiTypography.body3m.primary,
),
child: Text(t.client_create_order.review.edit, style: UiTypography.body3m.primary),
),
],
),

View File

@@ -1,17 +1,24 @@
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// A highlighted banner displaying the estimated total cost.
///
/// Uses the primary inverse background color with a bold price display.
/// An optional [label] can override the default "Estimated Total" text.
class ReviewOrderTotalBanner extends StatelessWidget {
const ReviewOrderTotalBanner({
required this.totalAmount,
this.label,
super.key,
});
/// The total monetary amount to display.
final double totalAmount;
/// Optional label override. Defaults to the localized "Estimated Total".
final String? label;
@override
Widget build(BuildContext context) {
return Container(
@@ -27,7 +34,7 @@ class ReviewOrderTotalBanner extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
'Estimated Total',
label ?? t.client_create_order.review.estimated_total,
style: UiTypography.body2m,
),
Text(

View File

@@ -1,8 +1,8 @@
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'review_order_action_bar.dart';
import 'review_order_basics_card.dart';
import 'review_order_header.dart';
import 'review_order_positions_card.dart';
import 'review_order_total_banner.dart';
@@ -31,7 +31,8 @@ class ReviewOrderView extends StatelessWidget {
this.onEditBasics,
this.onEditSchedule,
this.onEditPositions,
this.submitLabel = 'Post Order',
this.submitLabel,
this.totalLabel,
this.isLoading = false,
super.key,
});
@@ -50,16 +51,21 @@ class ReviewOrderView extends StatelessWidget {
final VoidCallback? onEditBasics;
final VoidCallback? onEditSchedule;
final VoidCallback? onEditPositions;
final String submitLabel;
final String? submitLabel;
/// Optional label override for the total banner. When `null`, the default
/// localized "Estimated Total" text is used.
final String? totalLabel;
final bool isLoading;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: UiColors.bgMenu,
appBar: UiAppBar(
showBackButton: true,
onLeadingPressed: onBack,
title: t.client_create_order.review.title,
subtitle: t.client_create_order.review.subtitle,
),
body: Column(
children: <Widget>[
@@ -68,7 +74,6 @@ class ReviewOrderView extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const ReviewOrderHeader(),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space6,
@@ -92,7 +97,10 @@ class ReviewOrderView extends StatelessWidget {
onEdit: showEditButtons ? onEditPositions : null,
),
const SizedBox(height: UiConstants.space3),
ReviewOrderTotalBanner(totalAmount: estimatedTotal),
ReviewOrderTotalBanner(
totalAmount: estimatedTotal,
label: totalLabel,
),
const SizedBox(height: UiConstants.space4),
],
),
@@ -104,7 +112,7 @@ class ReviewOrderView extends StatelessWidget {
ReviewOrderActionBar(
onBack: onBack,
onSubmit: onSubmit,
submitLabel: submitLabel,
submitLabel: submitLabel ?? t.client_create_order.review.post_order,
isLoading: isLoading,
),
],

View File

@@ -1,10 +1,13 @@
// UI Models
export 'src/presentation/widgets/order_ui_models.dart';
// Shared Widgets
export 'src/presentation/widgets/order_bottom_action_button.dart';
// One Time Order Widgets
export 'src/presentation/widgets/one_time_order/one_time_order_date_picker.dart';
export 'src/presentation/widgets/one_time_order/one_time_order_event_name_input.dart';
export 'src/presentation/widgets/one_time_order/one_time_order_header.dart';
export 'src/presentation/widgets/one_time_order/one_time_order_form.dart';
export 'src/presentation/widgets/one_time_order/one_time_order_location_input.dart';
export 'src/presentation/widgets/one_time_order/one_time_order_position_card.dart';
export 'src/presentation/widgets/one_time_order/one_time_order_section_header.dart';
@@ -13,8 +16,9 @@ export 'src/presentation/widgets/one_time_order/one_time_order_view.dart';
// Permanent Order Widgets
export 'src/presentation/widgets/permanent_order/permanent_order_date_picker.dart';
export 'src/presentation/widgets/permanent_order/permanent_order_days_selector.dart';
export 'src/presentation/widgets/permanent_order/permanent_order_event_name_input.dart';
export 'src/presentation/widgets/permanent_order/permanent_order_header.dart';
export 'src/presentation/widgets/permanent_order/permanent_order_form.dart';
export 'src/presentation/widgets/permanent_order/permanent_order_position_card.dart';
export 'src/presentation/widgets/permanent_order/permanent_order_section_header.dart';
export 'src/presentation/widgets/permanent_order/permanent_order_success_view.dart';
@@ -22,8 +26,9 @@ export 'src/presentation/widgets/permanent_order/permanent_order_view.dart';
// Recurring Order Widgets
export 'src/presentation/widgets/recurring_order/recurring_order_date_picker.dart';
export 'src/presentation/widgets/recurring_order/recurring_order_days_selector.dart';
export 'src/presentation/widgets/recurring_order/recurring_order_event_name_input.dart';
export 'src/presentation/widgets/recurring_order/recurring_order_header.dart';
export 'src/presentation/widgets/recurring_order/recurring_order_form.dart';
export 'src/presentation/widgets/recurring_order/recurring_order_position_card.dart';
export 'src/presentation/widgets/recurring_order/recurring_order_section_header.dart';
export 'src/presentation/widgets/recurring_order/recurring_order_success_view.dart';

View File

@@ -0,0 +1,242 @@
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:krow_domain/krow_domain.dart';
import '../hub_manager_selector.dart';
import '../order_ui_models.dart';
import 'one_time_order_date_picker.dart';
import 'one_time_order_event_name_input.dart';
import 'one_time_order_position_card.dart';
import 'one_time_order_section_header.dart';
/// The scrollable form body for the one-time order creation flow.
///
/// Displays fields for event name, vendor selection, date, hub, hub manager,
/// and a dynamic list of position cards.
class OneTimeOrderForm extends StatelessWidget {
/// Creates a [OneTimeOrderForm].
const OneTimeOrderForm({
required this.eventName,
required this.selectedVendor,
required this.vendors,
required this.date,
required this.selectedHub,
required this.hubs,
required this.selectedHubManager,
required this.hubManagers,
required this.positions,
required this.roles,
required this.onEventNameChanged,
required this.onVendorChanged,
required this.onDateChanged,
required this.onHubChanged,
required this.onHubManagerChanged,
required this.onPositionAdded,
required this.onPositionUpdated,
required this.onPositionRemoved,
super.key,
});
/// The current event name value.
final String eventName;
/// The currently selected vendor, if any.
final Vendor? selectedVendor;
/// The list of available vendors to choose from.
final List<Vendor> vendors;
/// The selected date for the one-time order.
final DateTime date;
/// The currently selected hub, if any.
final OrderHubUiModel? selectedHub;
/// The list of available hubs to choose from.
final List<OrderHubUiModel> hubs;
/// The currently selected hub manager, if any.
final OrderManagerUiModel? selectedHubManager;
/// The list of available hub managers for the selected hub.
final List<OrderManagerUiModel> hubManagers;
/// The list of position entries in the order.
final List<OrderPositionUiModel> positions;
/// The list of available roles for position assignment.
final List<OrderRoleUiModel> roles;
/// Called when the event name text changes.
final ValueChanged<String> onEventNameChanged;
/// Called when a vendor is selected.
final ValueChanged<Vendor> onVendorChanged;
/// Called when the date is changed.
final ValueChanged<DateTime> onDateChanged;
/// Called when a hub is selected.
final ValueChanged<OrderHubUiModel> onHubChanged;
/// Called when a hub manager is selected or cleared.
final ValueChanged<OrderManagerUiModel?> onHubManagerChanged;
/// Called when the user requests adding a new position.
final VoidCallback onPositionAdded;
/// Called when a position at [index] is updated with new values.
final void Function(int index, OrderPositionUiModel position)
onPositionUpdated;
/// Called when a position at [index] is removed.
final void Function(int index) onPositionRemoved;
@override
Widget build(BuildContext context) {
final TranslationsClientCreateOrderOneTimeEn labels =
t.client_create_order.one_time;
return ListView(
padding: const EdgeInsets.all(UiConstants.space5),
children: <Widget>[
OneTimeOrderEventNameInput(
label: 'ORDER NAME',
value: eventName,
onChanged: onEventNameChanged,
),
const SizedBox(height: UiConstants.space4),
// Vendor Selection
Text('SELECT VENDOR', style: UiTypography.footnote2r.textSecondary),
const SizedBox(height: UiConstants.space2),
Container(
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3),
height: 48,
decoration: BoxDecoration(
color: UiColors.white,
borderRadius: UiConstants.radiusMd,
border: Border.all(color: UiColors.border),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<Vendor>(
isExpanded: true,
value: selectedVendor,
icon: const Icon(
UiIcons.chevronDown,
size: 18,
color: UiColors.iconSecondary,
),
onChanged: (Vendor? vendor) {
if (vendor != null) {
onVendorChanged(vendor);
}
},
items: vendors.map((Vendor vendor) {
return DropdownMenuItem<Vendor>(
value: vendor,
child: Text(
vendor.name,
style: UiTypography.body2m.textPrimary,
),
);
}).toList(),
),
),
),
const SizedBox(height: UiConstants.space4),
OneTimeOrderDatePicker(
label: labels.date_label,
value: date,
onChanged: onDateChanged,
),
const SizedBox(height: UiConstants.space4),
Text('HUB', style: UiTypography.footnote2r.textSecondary),
const SizedBox(height: UiConstants.space2),
Container(
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3),
height: 48,
decoration: BoxDecoration(
color: UiColors.white,
borderRadius: UiConstants.radiusMd,
border: Border.all(color: UiColors.border),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<OrderHubUiModel>(
isExpanded: true,
value: selectedHub,
icon: const Icon(
UiIcons.chevronDown,
size: 18,
color: UiColors.iconSecondary,
),
onChanged: (OrderHubUiModel? hub) {
if (hub != null) {
onHubChanged(hub);
}
},
items: hubs.map((OrderHubUiModel hub) {
return DropdownMenuItem<OrderHubUiModel>(
value: hub,
child: Text(hub.name, style: UiTypography.body2m.textPrimary),
);
}).toList(),
),
),
),
const SizedBox(height: UiConstants.space4),
HubManagerSelector(
label: labels.hub_manager_label,
description: labels.hub_manager_desc,
hintText: labels.hub_manager_hint,
noManagersText: labels.hub_manager_empty,
noneText: labels.hub_manager_none,
managers: hubManagers,
selectedManager: selectedHubManager,
onChanged: onHubManagerChanged,
),
const SizedBox(height: UiConstants.space6),
OneTimeOrderSectionHeader(
title: labels.positions_title,
actionLabel: labels.add_position,
onAction: onPositionAdded,
),
const SizedBox(height: UiConstants.space3),
// Positions List
...positions.asMap().entries.map((
MapEntry<int, OrderPositionUiModel> entry,
) {
final int index = entry.key;
final OrderPositionUiModel position = entry.value;
return Padding(
padding: const EdgeInsets.only(bottom: UiConstants.space3),
child: OneTimeOrderPositionCard(
index: index,
position: position,
isRemovable: positions.length > 1,
positionLabel: labels.positions_title,
roleLabel: labels.select_role,
workersLabel: labels.workers_label,
startLabel: labels.start_label,
endLabel: labels.end_label,
lunchLabel: labels.lunch_break_label,
roles: roles,
onUpdated: (OrderPositionUiModel updated) {
onPositionUpdated(index, updated);
},
onRemoved: () {
onPositionRemoved(index);
},
),
);
}),
],
);
}
}

View File

@@ -1,71 +0,0 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// A header widget for the one-time order flow with a colored background.
class OneTimeOrderHeader extends StatelessWidget {
/// Creates a [OneTimeOrderHeader].
const OneTimeOrderHeader({
required this.title,
required this.subtitle,
required this.onBack,
super.key,
});
/// The title of the page.
final String title;
/// The subtitle or description.
final String subtitle;
/// Callback when the back button is pressed.
final VoidCallback onBack;
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.only(
top: MediaQuery.of(context).padding.top + UiConstants.space5,
bottom: UiConstants.space5,
left: UiConstants.space5,
right: UiConstants.space5,
),
color: UiColors.primary,
child: Row(
children: <Widget>[
GestureDetector(
onTap: onBack,
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: UiColors.white.withValues(alpha: 0.2),
borderRadius: UiConstants.radiusMd,
),
child: const Icon(
UiIcons.chevronLeft,
color: UiColors.white,
size: 24,
),
),
),
const SizedBox(width: UiConstants.space3),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
title,
style: UiTypography.headline3m.copyWith(color: UiColors.white),
),
Text(
subtitle,
style: UiTypography.footnote2r.copyWith(
color: UiColors.white.withValues(alpha: 0.8),
),
),
],
),
],
),
);
}
}

View File

@@ -2,13 +2,10 @@ import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:krow_domain/krow_domain.dart';
import '../order_bottom_action_button.dart';
import '../order_ui_models.dart';
import '../hub_manager_selector.dart';
import 'one_time_order_date_picker.dart';
import 'one_time_order_event_name_input.dart';
import 'one_time_order_header.dart';
import 'one_time_order_position_card.dart';
import 'one_time_order_section_header.dart';
import 'one_time_order_form.dart';
import 'one_time_order_success_view.dart';
/// The main content of the One-Time Order page as a dumb widget.
@@ -98,322 +95,92 @@ class OneTimeOrderView extends StatelessWidget {
);
}
return Scaffold(
appBar: UiAppBar(
showBackButton: true,
onLeadingPressed: onBack,
title: title ?? labels.title,
subtitle: subtitle ?? labels.subtitle,
),
body: _buildBody(context, labels),
);
}
/// Builds the main body of the One-Time Order page, showing either the form or a loading indicator.
Widget _buildBody(
BuildContext context,
TranslationsClientCreateOrderOneTimeEn labels,
) {
if (vendors.isEmpty && status != OrderFormStatus.loading) {
return Scaffold(
body: Column(
children: <Widget>[
OneTimeOrderHeader(
title: title ?? labels.title,
subtitle: subtitle ?? labels.subtitle,
onBack: onBack,
),
Expanded(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Icon(
UiIcons.search,
size: 64,
color: UiColors.iconInactive,
),
const SizedBox(height: UiConstants.space4),
Text(
'No Vendors Available',
style: UiTypography.headline3m.textPrimary,
),
const SizedBox(height: UiConstants.space2),
Text(
'There are no staffing vendors associated with your account.',
style: UiTypography.body2r.textSecondary,
textAlign: TextAlign.center,
),
],
),
return Column(
children: <Widget>[
Expanded(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Icon(
UiIcons.search,
size: 64,
color: UiColors.iconInactive,
),
const SizedBox(height: UiConstants.space4),
Text(
'No Vendors Available',
style: UiTypography.headline3m.textPrimary,
),
const SizedBox(height: UiConstants.space2),
Text(
'There are no staffing vendors associated with your account.',
style: UiTypography.body2r.textSecondary,
textAlign: TextAlign.center,
),
],
),
),
],
),
),
],
);
}
return Scaffold(
body: Column(
children: <Widget>[
OneTimeOrderHeader(
title: title ?? labels.title,
subtitle: subtitle ?? labels.subtitle,
onBack: onBack,
),
Expanded(
child: Stack(
children: <Widget>[
_OneTimeOrderForm(
eventName: eventName,
selectedVendor: selectedVendor,
vendors: vendors,
date: date,
selectedHub: selectedHub,
hubs: hubs,
selectedHubManager: selectedHubManager,
hubManagers: hubManagers,
positions: positions,
roles: roles,
onEventNameChanged: onEventNameChanged,
onVendorChanged: onVendorChanged,
onDateChanged: onDateChanged,
onHubChanged: onHubChanged,
onHubManagerChanged: onHubManagerChanged,
onPositionAdded: onPositionAdded,
onPositionUpdated: onPositionUpdated,
onPositionRemoved: onPositionRemoved,
),
if (status == OrderFormStatus.loading)
const Center(child: CircularProgressIndicator()),
],
),
),
_BottomActionButton(
label: status == OrderFormStatus.loading
? labels.creating
: labels.create_order,
isLoading: status == OrderFormStatus.loading,
onPressed: isValid ? onSubmit : null,
),
],
),
);
}
}
class _OneTimeOrderForm extends StatelessWidget {
const _OneTimeOrderForm({
required this.eventName,
required this.selectedVendor,
required this.vendors,
required this.date,
required this.selectedHub,
required this.hubs,
required this.selectedHubManager,
required this.hubManagers,
required this.positions,
required this.roles,
required this.onEventNameChanged,
required this.onVendorChanged,
required this.onDateChanged,
required this.onHubChanged,
required this.onHubManagerChanged,
required this.onPositionAdded,
required this.onPositionUpdated,
required this.onPositionRemoved,
});
final String eventName;
final Vendor? selectedVendor;
final List<Vendor> vendors;
final DateTime date;
final OrderHubUiModel? selectedHub;
final List<OrderHubUiModel> hubs;
final OrderManagerUiModel? selectedHubManager;
final List<OrderManagerUiModel> hubManagers;
final List<OrderPositionUiModel> positions;
final List<OrderRoleUiModel> roles;
final ValueChanged<String> onEventNameChanged;
final ValueChanged<Vendor> onVendorChanged;
final ValueChanged<DateTime> onDateChanged;
final ValueChanged<OrderHubUiModel> onHubChanged;
final ValueChanged<OrderManagerUiModel?> onHubManagerChanged;
final VoidCallback onPositionAdded;
final void Function(int index, OrderPositionUiModel position)
onPositionUpdated;
final void Function(int index) onPositionRemoved;
@override
Widget build(BuildContext context) {
final TranslationsClientCreateOrderOneTimeEn labels =
t.client_create_order.one_time;
return ListView(
padding: const EdgeInsets.all(UiConstants.space5),
return Column(
children: <Widget>[
Text(
labels.create_your_order,
style: UiTypography.headline3m.textPrimary,
),
const SizedBox(height: UiConstants.space4),
OneTimeOrderEventNameInput(
label: 'ORDER NAME',
value: eventName,
onChanged: onEventNameChanged,
),
const SizedBox(height: UiConstants.space4),
// Vendor Selection
Text('SELECT VENDOR', style: UiTypography.footnote2r.textSecondary),
const SizedBox(height: UiConstants.space2),
Container(
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3),
height: 48,
decoration: BoxDecoration(
color: UiColors.white,
borderRadius: UiConstants.radiusMd,
border: Border.all(color: UiColors.border),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<Vendor>(
isExpanded: true,
value: selectedVendor,
icon: const Icon(
UiIcons.chevronDown,
size: 18,
color: UiColors.iconSecondary,
Expanded(
child: Stack(
children: <Widget>[
OneTimeOrderForm(
eventName: eventName,
selectedVendor: selectedVendor,
vendors: vendors,
date: date,
selectedHub: selectedHub,
hubs: hubs,
selectedHubManager: selectedHubManager,
hubManagers: hubManagers,
positions: positions,
roles: roles,
onEventNameChanged: onEventNameChanged,
onVendorChanged: onVendorChanged,
onDateChanged: onDateChanged,
onHubChanged: onHubChanged,
onHubManagerChanged: onHubManagerChanged,
onPositionAdded: onPositionAdded,
onPositionUpdated: onPositionUpdated,
onPositionRemoved: onPositionRemoved,
),
onChanged: (Vendor? vendor) {
if (vendor != null) {
onVendorChanged(vendor);
}
},
items: vendors.map((Vendor vendor) {
return DropdownMenuItem<Vendor>(
value: vendor,
child: Text(
vendor.name,
style: UiTypography.body2m.textPrimary,
),
);
}).toList(),
),
if (status == OrderFormStatus.loading)
const Center(child: CircularProgressIndicator()),
],
),
),
const SizedBox(height: UiConstants.space4),
OneTimeOrderDatePicker(
label: labels.date_label,
value: date,
onChanged: onDateChanged,
OrderBottomActionButton(
label: status == OrderFormStatus.loading
? labels.creating
: labels.create_order,
isLoading: status == OrderFormStatus.loading,
onPressed: isValid ? onSubmit : null,
),
const SizedBox(height: UiConstants.space4),
Text('HUB', style: UiTypography.footnote2r.textSecondary),
const SizedBox(height: UiConstants.space2),
Container(
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3),
height: 48,
decoration: BoxDecoration(
color: UiColors.white,
borderRadius: UiConstants.radiusMd,
border: Border.all(color: UiColors.border),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<OrderHubUiModel>(
isExpanded: true,
value: selectedHub,
icon: const Icon(
UiIcons.chevronDown,
size: 18,
color: UiColors.iconSecondary,
),
onChanged: (OrderHubUiModel? hub) {
if (hub != null) {
onHubChanged(hub);
}
},
items: hubs.map((OrderHubUiModel hub) {
return DropdownMenuItem<OrderHubUiModel>(
value: hub,
child: Text(hub.name, style: UiTypography.body2m.textPrimary),
);
}).toList(),
),
),
),
const SizedBox(height: UiConstants.space4),
HubManagerSelector(
label: labels.hub_manager_label,
description: labels.hub_manager_desc,
hintText: labels.hub_manager_hint,
noManagersText: labels.hub_manager_empty,
noneText: labels.hub_manager_none,
managers: hubManagers,
selectedManager: selectedHubManager,
onChanged: onHubManagerChanged,
),
const SizedBox(height: UiConstants.space6),
OneTimeOrderSectionHeader(
title: labels.positions_title,
actionLabel: labels.add_position,
onAction: onPositionAdded,
),
const SizedBox(height: UiConstants.space3),
// Positions List
...positions.asMap().entries.map((
MapEntry<int, OrderPositionUiModel> entry,
) {
final int index = entry.key;
final OrderPositionUiModel position = entry.value;
return Padding(
padding: const EdgeInsets.only(bottom: UiConstants.space3),
child: OneTimeOrderPositionCard(
index: index,
position: position,
isRemovable: positions.length > 1,
positionLabel: labels.positions_title,
roleLabel: labels.select_role,
workersLabel: labels.workers_label,
startLabel: labels.start_label,
endLabel: labels.end_label,
lunchLabel: labels.lunch_break_label,
roles: roles,
onUpdated: (OrderPositionUiModel updated) {
onPositionUpdated(index, updated);
},
onRemoved: () {
onPositionRemoved(index);
},
),
);
}),
],
);
}
}
class _BottomActionButton extends StatelessWidget {
const _BottomActionButton({
required this.label,
required this.onPressed,
this.isLoading = false,
});
final String label;
final VoidCallback? onPressed;
final bool isLoading;
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.only(
left: UiConstants.space5,
right: UiConstants.space5,
top: UiConstants.space5,
bottom: MediaQuery.of(context).padding.bottom + UiConstants.space5,
),
decoration: const BoxDecoration(
color: UiColors.white,
border: Border(top: BorderSide(color: UiColors.border)),
),
child: SizedBox(
width: double.infinity,
child: UiButton.primary(
text: label,
onPressed: isLoading ? null : onPressed,
size: UiButtonSize.large,
),
),
);
}
}

View File

@@ -0,0 +1,49 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// A bottom-pinned action button used across all order type views.
///
/// Renders a full-width primary button with safe-area padding at the bottom
/// and a top border separator. Disables the button while [isLoading] is true.
class OrderBottomActionButton extends StatelessWidget {
/// Creates an [OrderBottomActionButton].
const OrderBottomActionButton({
required this.label,
required this.onPressed,
this.isLoading = false,
super.key,
});
/// The text displayed on the button.
final String label;
/// Callback invoked when the button is pressed. Pass `null` to disable.
final VoidCallback? onPressed;
/// Whether the form is currently submitting. Disables the button when true.
final bool isLoading;
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.only(
left: UiConstants.space5,
right: UiConstants.space5,
top: UiConstants.space5,
bottom: MediaQuery.of(context).padding.bottom + UiConstants.space5,
),
decoration: const BoxDecoration(
color: UiColors.white,
border: Border(top: BorderSide(color: UiColors.border, width: 0.5)),
),
child: SizedBox(
width: double.infinity,
child: UiButton.primary(
text: label,
onPressed: isLoading ? null : onPressed,
size: UiButtonSize.large,
),
),
);
}
}

View File

@@ -0,0 +1,68 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// A horizontal row of circular day-of-week toggle buttons for permanent orders.
///
/// Displays seven circles labeled S, M, T, W, T, F, S representing the days
/// of the week. Selected days are highlighted with the primary color.
class PermanentOrderDaysSelector extends StatelessWidget {
/// Creates a [PermanentOrderDaysSelector].
const PermanentOrderDaysSelector({
required this.selectedDays,
required this.onToggle,
super.key,
});
/// The list of currently selected day abbreviations (e.g. 'MON', 'TUE').
final List<String> selectedDays;
/// Called when a day circle is tapped, with the day index (0 = Sunday).
final ValueChanged<int> onToggle;
@override
Widget build(BuildContext context) {
const List<String> labelsShort = <String>[
'S',
'M',
'T',
'W',
'T',
'F',
'S',
];
const List<String> labelsLong = <String>[
'SUN',
'MON',
'TUE',
'WED',
'THU',
'FRI',
'SAT',
];
return Wrap(
spacing: UiConstants.space2,
children: List<Widget>.generate(labelsShort.length, (int index) {
final bool isSelected = selectedDays.contains(labelsLong[index]);
return GestureDetector(
onTap: () => onToggle(index),
child: Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: isSelected ? UiColors.primary : UiColors.white,
shape: BoxShape.circle,
border: Border.all(color: UiColors.border),
),
alignment: Alignment.center,
child: Text(
labelsShort[index],
style: UiTypography.body2m.copyWith(
color: isSelected ? UiColors.white : UiColors.textSecondary,
),
),
),
);
}),
);
}
}

View File

@@ -0,0 +1,271 @@
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:krow_domain/krow_domain.dart' show Vendor;
import '../hub_manager_selector.dart';
import '../order_ui_models.dart';
import 'permanent_order_date_picker.dart';
import 'permanent_order_days_selector.dart';
import 'permanent_order_event_name_input.dart';
import 'permanent_order_position_card.dart';
import 'permanent_order_section_header.dart';
/// The scrollable form body for the permanent order creation flow.
///
/// Displays fields for event name, vendor selection, start date,
/// permanent day toggles, hub, hub manager, and a dynamic list of
/// position cards.
class PermanentOrderForm extends StatelessWidget {
/// Creates a [PermanentOrderForm].
const PermanentOrderForm({
required this.eventName,
required this.selectedVendor,
required this.vendors,
required this.startDate,
required this.permanentDays,
required this.selectedHub,
required this.hubs,
required this.positions,
required this.roles,
required this.onEventNameChanged,
required this.onVendorChanged,
required this.onStartDateChanged,
required this.onDayToggled,
required this.onHubChanged,
required this.onHubManagerChanged,
required this.onPositionAdded,
required this.onPositionUpdated,
required this.onPositionRemoved,
required this.hubManagers,
required this.selectedHubManager,
super.key,
});
/// The current event name value.
final String eventName;
/// The currently selected vendor, if any.
final Vendor? selectedVendor;
/// The list of available vendors to choose from.
final List<Vendor> vendors;
/// The start date for the permanent order.
final DateTime startDate;
/// The list of selected permanent day abbreviations (e.g. 'MON', 'TUE').
final List<String> permanentDays;
/// The currently selected hub, if any.
final OrderHubUiModel? selectedHub;
/// The list of available hubs to choose from.
final List<OrderHubUiModel> hubs;
/// The list of position entries in the order.
final List<OrderPositionUiModel> positions;
/// The list of available roles for position assignment.
final List<OrderRoleUiModel> roles;
/// Called when the event name text changes.
final ValueChanged<String> onEventNameChanged;
/// Called when a vendor is selected.
final ValueChanged<Vendor> onVendorChanged;
/// Called when the start date is changed.
final ValueChanged<DateTime> onStartDateChanged;
/// Called when a day-of-week toggle is tapped, with the day index (0=Sun).
final ValueChanged<int> onDayToggled;
/// Called when a hub is selected.
final ValueChanged<OrderHubUiModel> onHubChanged;
/// Called when a hub manager is selected or cleared.
final ValueChanged<OrderManagerUiModel?> onHubManagerChanged;
/// Called when the user requests adding a new position.
final VoidCallback onPositionAdded;
/// Called when a position at [index] is updated with new values.
final void Function(int index, OrderPositionUiModel position)
onPositionUpdated;
/// Called when a position at [index] is removed.
final void Function(int index) onPositionRemoved;
/// The list of available hub managers for the selected hub.
final List<OrderManagerUiModel> hubManagers;
/// The currently selected hub manager, if any.
final OrderManagerUiModel? selectedHubManager;
@override
Widget build(BuildContext context) {
final TranslationsClientCreateOrderPermanentEn labels =
t.client_create_order.permanent;
final TranslationsClientCreateOrderOneTimeEn oneTimeLabels =
t.client_create_order.one_time;
return ListView(
padding: const EdgeInsets.all(UiConstants.space5),
children: <Widget>[
Text(
labels.title,
style: UiTypography.headline3m.textPrimary,
),
const SizedBox(height: UiConstants.space4),
PermanentOrderEventNameInput(
label: 'ORDER NAME',
value: eventName,
onChanged: onEventNameChanged,
),
const SizedBox(height: UiConstants.space4),
// Vendor Selection
Text('SELECT VENDOR', style: UiTypography.footnote2r.textSecondary),
const SizedBox(height: UiConstants.space2),
Container(
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3),
height: 48,
decoration: BoxDecoration(
color: UiColors.white,
borderRadius: UiConstants.radiusMd,
border: Border.all(color: UiColors.border),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<Vendor>(
isExpanded: true,
value: selectedVendor,
icon: const Icon(
UiIcons.chevronDown,
size: 18,
color: UiColors.iconSecondary,
),
onChanged: (Vendor? vendor) {
if (vendor != null) {
onVendorChanged(vendor);
}
},
items: vendors.map((Vendor vendor) {
return DropdownMenuItem<Vendor>(
value: vendor,
child: Text(
vendor.name,
style: UiTypography.body2m.textPrimary,
),
);
}).toList(),
),
),
),
const SizedBox(height: UiConstants.space4),
PermanentOrderDatePicker(
label: 'Start Date',
value: startDate,
onChanged: onStartDateChanged,
),
const SizedBox(height: UiConstants.space4),
Text('Permanent Days', style: UiTypography.footnote2r.textSecondary),
const SizedBox(height: UiConstants.space2),
PermanentOrderDaysSelector(
selectedDays: permanentDays,
onToggle: onDayToggled,
),
const SizedBox(height: UiConstants.space4),
Text('HUB', style: UiTypography.footnote2r.textSecondary),
const SizedBox(height: UiConstants.space2),
Container(
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3),
height: 48,
decoration: BoxDecoration(
color: UiColors.white,
borderRadius: UiConstants.radiusMd,
border: Border.all(color: UiColors.border),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<OrderHubUiModel>(
isExpanded: true,
value: selectedHub,
icon: const Icon(
UiIcons.chevronDown,
size: 18,
color: UiColors.iconSecondary,
),
onChanged: (OrderHubUiModel? hub) {
if (hub != null) {
onHubChanged(hub);
}
},
items: hubs.map((OrderHubUiModel hub) {
return DropdownMenuItem<OrderHubUiModel>(
value: hub,
child: Text(
hub.name,
style: UiTypography.body2m.textPrimary,
),
);
}).toList(),
),
),
),
const SizedBox(height: UiConstants.space4),
HubManagerSelector(
label: oneTimeLabels.hub_manager_label,
description: oneTimeLabels.hub_manager_desc,
hintText: oneTimeLabels.hub_manager_hint,
noManagersText: oneTimeLabels.hub_manager_empty,
noneText: oneTimeLabels.hub_manager_none,
managers: hubManagers,
selectedManager: selectedHubManager,
onChanged: onHubManagerChanged,
),
const SizedBox(height: UiConstants.space6),
PermanentOrderSectionHeader(
title: oneTimeLabels.positions_title,
actionLabel: oneTimeLabels.add_position,
onAction: onPositionAdded,
),
const SizedBox(height: UiConstants.space3),
// Positions List
...positions.asMap().entries.map((
MapEntry<int, OrderPositionUiModel> entry,
) {
final int index = entry.key;
final OrderPositionUiModel position = entry.value;
return Padding(
padding: const EdgeInsets.only(bottom: UiConstants.space3),
child: PermanentOrderPositionCard(
index: index,
position: position,
isRemovable: positions.length > 1,
positionLabel: oneTimeLabels.positions_title,
roleLabel: oneTimeLabels.select_role,
workersLabel: oneTimeLabels.workers_label,
startLabel: oneTimeLabels.start_label,
endLabel: oneTimeLabels.end_label,
lunchLabel: oneTimeLabels.lunch_break_label,
roles: roles,
onUpdated: (OrderPositionUiModel updated) {
onPositionUpdated(index, updated);
},
onRemoved: () {
onPositionRemoved(index);
},
),
);
}),
],
);
}
}

View File

@@ -1,71 +0,0 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// A header widget for the permanent order flow with a colored background.
class PermanentOrderHeader extends StatelessWidget {
/// Creates a [PermanentOrderHeader].
const PermanentOrderHeader({
required this.title,
required this.subtitle,
required this.onBack,
super.key,
});
/// The title of the page.
final String title;
/// The subtitle or description.
final String subtitle;
/// Callback when the back button is pressed.
final VoidCallback onBack;
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.only(
top: MediaQuery.of(context).padding.top + UiConstants.space5,
bottom: UiConstants.space5,
left: UiConstants.space5,
right: UiConstants.space5,
),
color: UiColors.primary,
child: Row(
children: <Widget>[
GestureDetector(
onTap: onBack,
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: UiColors.white.withValues(alpha: 0.2),
borderRadius: UiConstants.radiusMd,
),
child: const Icon(
UiIcons.chevronLeft,
color: UiColors.white,
size: 24,
),
),
),
const SizedBox(width: UiConstants.space3),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
title,
style: UiTypography.headline3m.copyWith(color: UiColors.white),
),
Text(
subtitle,
style: UiTypography.footnote2r.copyWith(
color: UiColors.white.withValues(alpha: 0.8),
),
),
],
),
],
),
);
}
}

View File

@@ -2,13 +2,10 @@ import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:krow_domain/krow_domain.dart' show Vendor;
import '../order_bottom_action_button.dart';
import '../order_ui_models.dart';
import '../hub_manager_selector.dart';
import 'permanent_order_date_picker.dart';
import 'permanent_order_event_name_input.dart';
import 'permanent_order_header.dart';
import 'permanent_order_position_card.dart';
import 'permanent_order_section_header.dart';
import 'permanent_order_form.dart';
import 'permanent_order_success_view.dart';
/// The main content of the Permanent Order page.
@@ -65,7 +62,8 @@ class PermanentOrderView extends StatelessWidget {
final ValueChanged<OrderHubUiModel> onHubChanged;
final ValueChanged<OrderManagerUiModel?> onHubManagerChanged;
final VoidCallback onPositionAdded;
final void Function(int index, OrderPositionUiModel position) onPositionUpdated;
final void Function(int index, OrderPositionUiModel position)
onPositionUpdated;
final void Function(int index) onPositionRemoved;
final VoidCallback onSubmit;
final VoidCallback onDone;
@@ -98,398 +96,95 @@ class PermanentOrderView extends StatelessWidget {
);
}
return Scaffold(
appBar: UiAppBar(
showBackButton: true,
onLeadingPressed: onBack,
title: labels.title,
subtitle: labels.subtitle,
),
body: _buildBody(context, labels, oneTimeLabels),
);
}
/// Builds the main body of the Permanent Order page based on the current state.
Widget _buildBody(
BuildContext context,
TranslationsClientCreateOrderPermanentEn labels,
TranslationsClientCreateOrderOneTimeEn oneTimeLabels,
) {
if (vendors.isEmpty && status != OrderFormStatus.loading) {
return Scaffold(
body: Column(
children: <Widget>[
PermanentOrderHeader(
title: labels.title,
subtitle: labels.subtitle,
onBack: onBack,
),
Expanded(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Icon(
UiIcons.search,
size: 64,
color: UiColors.iconInactive,
),
const SizedBox(height: UiConstants.space4),
Text(
'No Vendors Available',
style: UiTypography.headline3m.textPrimary,
),
const SizedBox(height: UiConstants.space2),
Text(
'There are no staffing vendors associated with your account.',
style: UiTypography.body2r.textSecondary,
textAlign: TextAlign.center,
),
],
),
return Column(
children: <Widget>[
Expanded(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Icon(
UiIcons.search,
size: 64,
color: UiColors.iconInactive,
),
const SizedBox(height: UiConstants.space4),
Text(
'No Vendors Available',
style: UiTypography.headline3m.textPrimary,
),
const SizedBox(height: UiConstants.space2),
Text(
'There are no staffing vendors associated with your account.',
style: UiTypography.body2r.textSecondary,
textAlign: TextAlign.center,
),
],
),
),
],
),
),
],
);
}
return Scaffold(
body: Column(
children: <Widget>[
PermanentOrderHeader(
title: labels.title,
subtitle: labels.subtitle,
onBack: onBack,
),
Expanded(
child: Stack(
children: <Widget>[
_PermanentOrderForm(
eventName: eventName,
selectedVendor: selectedVendor,
vendors: vendors,
startDate: startDate,
permanentDays: permanentDays,
selectedHub: selectedHub,
hubs: hubs,
positions: positions,
roles: roles,
onEventNameChanged: onEventNameChanged,
onVendorChanged: onVendorChanged,
onStartDateChanged: onStartDateChanged,
onDayToggled: onDayToggled,
onHubChanged: onHubChanged,
onHubManagerChanged: onHubManagerChanged,
onPositionAdded: onPositionAdded,
onPositionUpdated: onPositionUpdated,
onPositionRemoved: onPositionRemoved,
hubManagers: hubManagers,
selectedHubManager: selectedHubManager,
),
if (status == OrderFormStatus.loading)
const Center(child: CircularProgressIndicator()),
],
),
),
_BottomActionButton(
label: status == OrderFormStatus.loading
? oneTimeLabels.creating
: oneTimeLabels.create_order,
isLoading: status == OrderFormStatus.loading,
onPressed: isValid ? onSubmit : null,
),
],
),
);
}
}
class _PermanentOrderForm extends StatelessWidget {
const _PermanentOrderForm({
required this.eventName,
required this.selectedVendor,
required this.vendors,
required this.startDate,
required this.permanentDays,
required this.selectedHub,
required this.hubs,
required this.positions,
required this.roles,
required this.onEventNameChanged,
required this.onVendorChanged,
required this.onStartDateChanged,
required this.onDayToggled,
required this.onHubChanged,
required this.onHubManagerChanged,
required this.onPositionAdded,
required this.onPositionUpdated,
required this.onPositionRemoved,
required this.hubManagers,
required this.selectedHubManager,
});
final String eventName;
final Vendor? selectedVendor;
final List<Vendor> vendors;
final DateTime startDate;
final List<String> permanentDays;
final OrderHubUiModel? selectedHub;
final List<OrderHubUiModel> hubs;
final List<OrderPositionUiModel> positions;
final List<OrderRoleUiModel> roles;
final ValueChanged<String> onEventNameChanged;
final ValueChanged<Vendor> onVendorChanged;
final ValueChanged<DateTime> onStartDateChanged;
final ValueChanged<int> onDayToggled;
final ValueChanged<OrderHubUiModel> onHubChanged;
final ValueChanged<OrderManagerUiModel?> onHubManagerChanged;
final VoidCallback onPositionAdded;
final void Function(int index, OrderPositionUiModel position) onPositionUpdated;
final void Function(int index) onPositionRemoved;
final List<OrderManagerUiModel> hubManagers;
final OrderManagerUiModel? selectedHubManager;
@override
Widget build(BuildContext context) {
final TranslationsClientCreateOrderPermanentEn labels =
t.client_create_order.permanent;
final TranslationsClientCreateOrderOneTimeEn oneTimeLabels =
t.client_create_order.one_time;
return ListView(
padding: const EdgeInsets.all(UiConstants.space5),
return Column(
children: <Widget>[
Text(
labels.title,
style: UiTypography.headline3m.textPrimary,
),
const SizedBox(height: UiConstants.space4),
PermanentOrderEventNameInput(
label: 'ORDER NAME',
value: eventName,
onChanged: onEventNameChanged,
),
const SizedBox(height: UiConstants.space4),
// Vendor Selection
Text('SELECT VENDOR', style: UiTypography.footnote2r.textSecondary),
const SizedBox(height: UiConstants.space2),
Container(
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3),
height: 48,
decoration: BoxDecoration(
color: UiColors.white,
borderRadius: UiConstants.radiusMd,
border: Border.all(color: UiColors.border),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<Vendor>(
isExpanded: true,
value: selectedVendor,
icon: const Icon(
UiIcons.chevronDown,
size: 18,
color: UiColors.iconSecondary,
Expanded(
child: Stack(
children: <Widget>[
PermanentOrderForm(
eventName: eventName,
selectedVendor: selectedVendor,
vendors: vendors,
startDate: startDate,
permanentDays: permanentDays,
selectedHub: selectedHub,
hubs: hubs,
positions: positions,
roles: roles,
onEventNameChanged: onEventNameChanged,
onVendorChanged: onVendorChanged,
onStartDateChanged: onStartDateChanged,
onDayToggled: onDayToggled,
onHubChanged: onHubChanged,
onHubManagerChanged: onHubManagerChanged,
onPositionAdded: onPositionAdded,
onPositionUpdated: onPositionUpdated,
onPositionRemoved: onPositionRemoved,
hubManagers: hubManagers,
selectedHubManager: selectedHubManager,
),
onChanged: (Vendor? vendor) {
if (vendor != null) {
onVendorChanged(vendor);
}
},
items: vendors.map((Vendor vendor) {
return DropdownMenuItem<Vendor>(
value: vendor,
child: Text(
vendor.name,
style: UiTypography.body2m.textPrimary,
),
);
}).toList(),
),
if (status == OrderFormStatus.loading)
const Center(child: CircularProgressIndicator()),
],
),
),
const SizedBox(height: UiConstants.space4),
PermanentOrderDatePicker(
label: 'Start Date',
value: startDate,
onChanged: onStartDateChanged,
OrderBottomActionButton(
label: status == OrderFormStatus.loading
? oneTimeLabels.creating
: oneTimeLabels.create_order,
isLoading: status == OrderFormStatus.loading,
onPressed: isValid ? onSubmit : null,
),
const SizedBox(height: UiConstants.space4),
Text('Permanent Days', style: UiTypography.footnote2r.textSecondary),
const SizedBox(height: UiConstants.space2),
_PermanentDaysSelector(
selectedDays: permanentDays,
onToggle: onDayToggled,
),
const SizedBox(height: UiConstants.space4),
Text('HUB', style: UiTypography.footnote2r.textSecondary),
const SizedBox(height: UiConstants.space2),
Container(
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3),
height: 48,
decoration: BoxDecoration(
color: UiColors.white,
borderRadius: UiConstants.radiusMd,
border: Border.all(color: UiColors.border),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<OrderHubUiModel>(
isExpanded: true,
value: selectedHub,
icon: const Icon(
UiIcons.chevronDown,
size: 18,
color: UiColors.iconSecondary,
),
onChanged: (OrderHubUiModel? hub) {
if (hub != null) {
onHubChanged(hub);
}
},
items: hubs.map((OrderHubUiModel hub) {
return DropdownMenuItem<OrderHubUiModel>(
value: hub,
child: Text(
hub.name,
style: UiTypography.body2m.textPrimary,
),
);
}).toList(),
),
),
),
const SizedBox(height: UiConstants.space4),
HubManagerSelector(
label: oneTimeLabels.hub_manager_label,
description: oneTimeLabels.hub_manager_desc,
hintText: oneTimeLabels.hub_manager_hint,
noManagersText: oneTimeLabels.hub_manager_empty,
noneText: oneTimeLabels.hub_manager_none,
managers: hubManagers,
selectedManager: selectedHubManager,
onChanged: onHubManagerChanged,
),
const SizedBox(height: UiConstants.space6),
PermanentOrderSectionHeader(
title: oneTimeLabels.positions_title,
actionLabel: oneTimeLabels.add_position,
onAction: onPositionAdded,
),
const SizedBox(height: UiConstants.space3),
// Positions List
...positions.asMap().entries.map((
MapEntry<int, OrderPositionUiModel> entry,
) {
final int index = entry.key;
final OrderPositionUiModel position = entry.value;
return Padding(
padding: const EdgeInsets.only(bottom: UiConstants.space3),
child: PermanentOrderPositionCard(
index: index,
position: position,
isRemovable: positions.length > 1,
positionLabel: oneTimeLabels.positions_title,
roleLabel: oneTimeLabels.select_role,
workersLabel: oneTimeLabels.workers_label,
startLabel: oneTimeLabels.start_label,
endLabel: oneTimeLabels.end_label,
lunchLabel: oneTimeLabels.lunch_break_label,
roles: roles,
onUpdated: (OrderPositionUiModel updated) {
onPositionUpdated(index, updated);
},
onRemoved: () {
onPositionRemoved(index);
},
),
);
}),
],
);
}
}
class _PermanentDaysSelector extends StatelessWidget {
const _PermanentDaysSelector({
required this.selectedDays,
required this.onToggle,
});
final List<String> selectedDays;
final ValueChanged<int> onToggle;
@override
Widget build(BuildContext context) {
const List<String> labelsShort = <String>[
'S',
'M',
'T',
'W',
'T',
'F',
'S',
];
const List<String> labelsLong = <String>[
'SUN',
'MON',
'TUE',
'WED',
'THU',
'FRI',
'SAT',
];
return Wrap(
spacing: UiConstants.space2,
children: List<Widget>.generate(labelsShort.length, (int index) {
final bool isSelected = selectedDays.contains(labelsLong[index]);
return GestureDetector(
onTap: () => onToggle(index),
child: Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: isSelected ? UiColors.primary : UiColors.white,
shape: BoxShape.circle,
border: Border.all(color: UiColors.border),
),
alignment: Alignment.center,
child: Text(
labelsShort[index],
style: UiTypography.body2m.copyWith(
color: isSelected ? UiColors.white : UiColors.textSecondary,
),
),
),
);
}),
);
}
}
class _BottomActionButton extends StatelessWidget {
const _BottomActionButton({
required this.label,
required this.onPressed,
this.isLoading = false,
});
final String label;
final VoidCallback? onPressed;
final bool isLoading;
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.only(
left: UiConstants.space5,
right: UiConstants.space5,
top: UiConstants.space5,
bottom: MediaQuery.of(context).padding.bottom + UiConstants.space5,
),
decoration: const BoxDecoration(
color: UiColors.white,
border: Border(top: BorderSide(color: UiColors.border)),
),
child: SizedBox(
width: double.infinity,
child: UiButton.primary(
text: label,
onPressed: isLoading ? null : onPressed,
size: UiButtonSize.large,
),
),
);
}
}

View File

@@ -0,0 +1,68 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// A horizontal row of circular day-of-week toggle buttons for recurring orders.
///
/// Displays seven circles labeled S, M, T, W, T, F, S representing the days
/// of the week. Selected days are highlighted with the primary color.
class RecurringOrderDaysSelector extends StatelessWidget {
/// Creates a [RecurringOrderDaysSelector].
const RecurringOrderDaysSelector({
required this.selectedDays,
required this.onToggle,
super.key,
});
/// The list of currently selected day abbreviations (e.g. 'MON', 'TUE').
final List<String> selectedDays;
/// Called when a day circle is tapped, with the day index (0 = Sunday).
final ValueChanged<int> onToggle;
@override
Widget build(BuildContext context) {
const List<String> labelsShort = <String>[
'S',
'M',
'T',
'W',
'T',
'F',
'S',
];
const List<String> labelsLong = <String>[
'SUN',
'MON',
'TUE',
'WED',
'THU',
'FRI',
'SAT',
];
return Wrap(
spacing: UiConstants.space2,
children: List<Widget>.generate(labelsShort.length, (int index) {
final bool isSelected = selectedDays.contains(labelsLong[index]);
return GestureDetector(
onTap: () => onToggle(index),
child: Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: isSelected ? UiColors.primary : UiColors.white,
shape: BoxShape.circle,
border: Border.all(color: UiColors.border),
),
alignment: Alignment.center,
child: Text(
labelsShort[index],
style: UiTypography.body2m.copyWith(
color: isSelected ? UiColors.white : UiColors.textSecondary,
),
),
),
);
}),
);
}
}

View File

@@ -0,0 +1,286 @@
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:krow_domain/krow_domain.dart' show Vendor;
import '../hub_manager_selector.dart';
import '../order_ui_models.dart';
import 'recurring_order_date_picker.dart';
import 'recurring_order_days_selector.dart';
import 'recurring_order_event_name_input.dart';
import 'recurring_order_position_card.dart';
import 'recurring_order_section_header.dart';
/// The scrollable form body for the recurring order creation flow.
///
/// Displays fields for event name, vendor selection, start/end dates,
/// recurring day toggles, hub, hub manager, and a dynamic list of
/// position cards.
class RecurringOrderForm extends StatelessWidget {
/// Creates a [RecurringOrderForm].
const RecurringOrderForm({
required this.eventName,
required this.selectedVendor,
required this.vendors,
required this.startDate,
required this.endDate,
required this.recurringDays,
required this.selectedHub,
required this.hubs,
required this.positions,
required this.roles,
required this.onEventNameChanged,
required this.onVendorChanged,
required this.onStartDateChanged,
required this.onEndDateChanged,
required this.onDayToggled,
required this.onHubChanged,
required this.onHubManagerChanged,
required this.onPositionAdded,
required this.onPositionUpdated,
required this.onPositionRemoved,
required this.hubManagers,
required this.selectedHubManager,
super.key,
});
/// The current event name value.
final String eventName;
/// The currently selected vendor, if any.
final Vendor? selectedVendor;
/// The list of available vendors to choose from.
final List<Vendor> vendors;
/// The start date for the recurring period.
final DateTime startDate;
/// The end date for the recurring period.
final DateTime endDate;
/// The list of selected recurring day abbreviations (e.g. 'MON', 'TUE').
final List<String> recurringDays;
/// The currently selected hub, if any.
final OrderHubUiModel? selectedHub;
/// The list of available hubs to choose from.
final List<OrderHubUiModel> hubs;
/// The list of position entries in the order.
final List<OrderPositionUiModel> positions;
/// The list of available roles for position assignment.
final List<OrderRoleUiModel> roles;
/// Called when the event name text changes.
final ValueChanged<String> onEventNameChanged;
/// Called when a vendor is selected.
final ValueChanged<Vendor> onVendorChanged;
/// Called when the start date is changed.
final ValueChanged<DateTime> onStartDateChanged;
/// Called when the end date is changed.
final ValueChanged<DateTime> onEndDateChanged;
/// Called when a day-of-week toggle is tapped, with the day index (0=Sun).
final ValueChanged<int> onDayToggled;
/// Called when a hub is selected.
final ValueChanged<OrderHubUiModel> onHubChanged;
/// Called when a hub manager is selected or cleared.
final ValueChanged<OrderManagerUiModel?> onHubManagerChanged;
/// Called when the user requests adding a new position.
final VoidCallback onPositionAdded;
/// Called when a position at [index] is updated with new values.
final void Function(int index, OrderPositionUiModel position)
onPositionUpdated;
/// Called when a position at [index] is removed.
final void Function(int index) onPositionRemoved;
/// The list of available hub managers for the selected hub.
final List<OrderManagerUiModel> hubManagers;
/// The currently selected hub manager, if any.
final OrderManagerUiModel? selectedHubManager;
@override
Widget build(BuildContext context) {
final TranslationsClientCreateOrderRecurringEn labels =
t.client_create_order.recurring;
final TranslationsClientCreateOrderOneTimeEn oneTimeLabels =
t.client_create_order.one_time;
return ListView(
padding: const EdgeInsets.all(UiConstants.space5),
children: <Widget>[
Text(
labels.title,
style: UiTypography.headline3m.textPrimary,
),
const SizedBox(height: UiConstants.space4),
RecurringOrderEventNameInput(
label: 'ORDER NAME',
value: eventName,
onChanged: onEventNameChanged,
),
const SizedBox(height: UiConstants.space4),
// Vendor Selection
Text('SELECT VENDOR', style: UiTypography.footnote2r.textSecondary),
const SizedBox(height: UiConstants.space2),
Container(
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3),
height: 48,
decoration: BoxDecoration(
color: UiColors.white,
borderRadius: UiConstants.radiusMd,
border: Border.all(color: UiColors.border),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<Vendor>(
isExpanded: true,
value: selectedVendor,
icon: const Icon(
UiIcons.chevronDown,
size: 18,
color: UiColors.iconSecondary,
),
onChanged: (Vendor? vendor) {
if (vendor != null) {
onVendorChanged(vendor);
}
},
items: vendors.map((Vendor vendor) {
return DropdownMenuItem<Vendor>(
value: vendor,
child: Text(
vendor.name,
style: UiTypography.body2m.textPrimary,
),
);
}).toList(),
),
),
),
const SizedBox(height: UiConstants.space4),
RecurringOrderDatePicker(
label: 'Start Date',
value: startDate,
onChanged: onStartDateChanged,
),
const SizedBox(height: UiConstants.space4),
RecurringOrderDatePicker(
label: 'End Date',
value: endDate,
onChanged: onEndDateChanged,
),
const SizedBox(height: UiConstants.space4),
Text('Recurring Days', style: UiTypography.footnote2r.textSecondary),
const SizedBox(height: UiConstants.space2),
RecurringOrderDaysSelector(
selectedDays: recurringDays,
onToggle: onDayToggled,
),
const SizedBox(height: UiConstants.space4),
Text('HUB', style: UiTypography.footnote2r.textSecondary),
const SizedBox(height: UiConstants.space2),
Container(
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3),
height: 48,
decoration: BoxDecoration(
color: UiColors.white,
borderRadius: UiConstants.radiusMd,
border: Border.all(color: UiColors.border),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<OrderHubUiModel>(
isExpanded: true,
value: selectedHub,
icon: const Icon(
UiIcons.chevronDown,
size: 18,
color: UiColors.iconSecondary,
),
onChanged: (OrderHubUiModel? hub) {
if (hub != null) {
onHubChanged(hub);
}
},
items: hubs.map((OrderHubUiModel hub) {
return DropdownMenuItem<OrderHubUiModel>(
value: hub,
child: Text(
hub.name,
style: UiTypography.body2m.textPrimary,
),
);
}).toList(),
),
),
),
const SizedBox(height: UiConstants.space4),
HubManagerSelector(
label: oneTimeLabels.hub_manager_label,
description: oneTimeLabels.hub_manager_desc,
hintText: oneTimeLabels.hub_manager_hint,
noManagersText: oneTimeLabels.hub_manager_empty,
noneText: oneTimeLabels.hub_manager_none,
managers: hubManagers,
selectedManager: selectedHubManager,
onChanged: onHubManagerChanged,
),
const SizedBox(height: UiConstants.space6),
RecurringOrderSectionHeader(
title: oneTimeLabels.positions_title,
actionLabel: oneTimeLabels.add_position,
onAction: onPositionAdded,
),
const SizedBox(height: UiConstants.space3),
// Positions List
...positions.asMap().entries.map((
MapEntry<int, OrderPositionUiModel> entry,
) {
final int index = entry.key;
final OrderPositionUiModel position = entry.value;
return Padding(
padding: const EdgeInsets.only(bottom: UiConstants.space3),
child: RecurringOrderPositionCard(
index: index,
position: position,
isRemovable: positions.length > 1,
positionLabel: oneTimeLabels.positions_title,
roleLabel: oneTimeLabels.select_role,
workersLabel: oneTimeLabels.workers_label,
startLabel: oneTimeLabels.start_label,
endLabel: oneTimeLabels.end_label,
lunchLabel: oneTimeLabels.lunch_break_label,
roles: roles,
onUpdated: (OrderPositionUiModel updated) {
onPositionUpdated(index, updated);
},
onRemoved: () {
onPositionRemoved(index);
},
),
);
}),
],
);
}
}

View File

@@ -1,71 +0,0 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// A header widget for the recurring order flow with a colored background.
class RecurringOrderHeader extends StatelessWidget {
/// Creates a [RecurringOrderHeader].
const RecurringOrderHeader({
required this.title,
required this.subtitle,
required this.onBack,
super.key,
});
/// The title of the page.
final String title;
/// The subtitle or description.
final String subtitle;
/// Callback when the back button is pressed.
final VoidCallback onBack;
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.only(
top: MediaQuery.of(context).padding.top + UiConstants.space5,
bottom: UiConstants.space5,
left: UiConstants.space5,
right: UiConstants.space5,
),
color: UiColors.primary,
child: Row(
children: <Widget>[
GestureDetector(
onTap: onBack,
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: UiColors.white.withValues(alpha: 0.2),
borderRadius: UiConstants.radiusMd,
),
child: const Icon(
UiIcons.chevronLeft,
color: UiColors.white,
size: 24,
),
),
),
const SizedBox(width: UiConstants.space3),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
title,
style: UiTypography.headline3m.copyWith(color: UiColors.white),
),
Text(
subtitle,
style: UiTypography.footnote2r.copyWith(
color: UiColors.white.withValues(alpha: 0.8),
),
),
],
),
],
),
);
}
}

View File

@@ -1,14 +1,11 @@
import 'package:core_localization/core_localization.dart';
import 'package:krow_domain/krow_domain.dart' show Vendor;
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:krow_domain/krow_domain.dart' show Vendor;
import '../order_bottom_action_button.dart';
import '../order_ui_models.dart';
import '../hub_manager_selector.dart';
import 'recurring_order_date_picker.dart';
import 'recurring_order_event_name_input.dart';
import 'recurring_order_header.dart';
import 'recurring_order_position_card.dart';
import 'recurring_order_section_header.dart';
import 'recurring_order_form.dart';
import 'recurring_order_success_view.dart';
/// The main content of the Recurring Order page.
@@ -69,7 +66,8 @@ class RecurringOrderView extends StatelessWidget {
final ValueChanged<OrderHubUiModel> onHubChanged;
final ValueChanged<OrderManagerUiModel?> onHubManagerChanged;
final VoidCallback onPositionAdded;
final void Function(int index, OrderPositionUiModel position) onPositionUpdated;
final void Function(int index, OrderPositionUiModel position)
onPositionUpdated;
final void Function(int index) onPositionRemoved;
final VoidCallback onSubmit;
final VoidCallback onDone;
@@ -105,412 +103,97 @@ class RecurringOrderView extends StatelessWidget {
);
}
return Scaffold(
appBar: UiAppBar(
showBackButton: true,
onLeadingPressed: onBack,
title: labels.title,
subtitle: labels.subtitle,
),
body: _buildBody(context, labels, oneTimeLabels),
);
}
/// Builds the main body of the Recurring Order page, including the form and handling empty vendor state.
Widget _buildBody(
BuildContext context,
TranslationsClientCreateOrderRecurringEn labels,
TranslationsClientCreateOrderOneTimeEn oneTimeLabels,
) {
if (vendors.isEmpty && status != OrderFormStatus.loading) {
return Scaffold(
body: Column(
children: <Widget>[
RecurringOrderHeader(
title: labels.title,
subtitle: labels.subtitle,
onBack: onBack,
),
Expanded(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Icon(
UiIcons.search,
size: 64,
color: UiColors.iconInactive,
),
const SizedBox(height: UiConstants.space4),
Text(
'No Vendors Available',
style: UiTypography.headline3m.textPrimary,
),
const SizedBox(height: UiConstants.space2),
Text(
'There are no staffing vendors associated with your account.',
style: UiTypography.body2r.textSecondary,
textAlign: TextAlign.center,
),
],
),
return Column(
children: <Widget>[
Expanded(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Icon(
UiIcons.search,
size: 64,
color: UiColors.iconInactive,
),
const SizedBox(height: UiConstants.space4),
Text(
'No Vendors Available',
style: UiTypography.headline3m.textPrimary,
),
const SizedBox(height: UiConstants.space2),
Text(
'There are no staffing vendors associated with your account.',
style: UiTypography.body2r.textSecondary,
textAlign: TextAlign.center,
),
],
),
),
],
),
),
],
);
}
return Scaffold(
body: Column(
children: <Widget>[
RecurringOrderHeader(
title: labels.title,
subtitle: labels.subtitle,
onBack: onBack,
),
Expanded(
child: Stack(
children: <Widget>[
_RecurringOrderForm(
eventName: eventName,
selectedVendor: selectedVendor,
vendors: vendors,
startDate: startDate,
endDate: endDate,
recurringDays: recurringDays,
selectedHub: selectedHub,
hubs: hubs,
positions: positions,
roles: roles,
onEventNameChanged: onEventNameChanged,
onVendorChanged: onVendorChanged,
onStartDateChanged: onStartDateChanged,
onEndDateChanged: onEndDateChanged,
onDayToggled: onDayToggled,
onHubChanged: onHubChanged,
onHubManagerChanged: onHubManagerChanged,
onPositionAdded: onPositionAdded,
onPositionUpdated: onPositionUpdated,
onPositionRemoved: onPositionRemoved,
hubManagers: hubManagers,
selectedHubManager: selectedHubManager,
),
if (status == OrderFormStatus.loading)
const Center(child: CircularProgressIndicator()),
],
),
),
_BottomActionButton(
label: status == OrderFormStatus.loading
? oneTimeLabels.creating
: oneTimeLabels.create_order,
isLoading: status == OrderFormStatus.loading,
onPressed: isValid ? onSubmit : null,
),
],
),
);
}
}
class _RecurringOrderForm extends StatelessWidget {
const _RecurringOrderForm({
required this.eventName,
required this.selectedVendor,
required this.vendors,
required this.startDate,
required this.endDate,
required this.recurringDays,
required this.selectedHub,
required this.hubs,
required this.positions,
required this.roles,
required this.onEventNameChanged,
required this.onVendorChanged,
required this.onStartDateChanged,
required this.onEndDateChanged,
required this.onDayToggled,
required this.onHubChanged,
required this.onHubManagerChanged,
required this.onPositionAdded,
required this.onPositionUpdated,
required this.onPositionRemoved,
required this.hubManagers,
required this.selectedHubManager,
});
final String eventName;
final Vendor? selectedVendor;
final List<Vendor> vendors;
final DateTime startDate;
final DateTime endDate;
final List<String> recurringDays;
final OrderHubUiModel? selectedHub;
final List<OrderHubUiModel> hubs;
final List<OrderPositionUiModel> positions;
final List<OrderRoleUiModel> roles;
final ValueChanged<String> onEventNameChanged;
final ValueChanged<Vendor> onVendorChanged;
final ValueChanged<DateTime> onStartDateChanged;
final ValueChanged<DateTime> onEndDateChanged;
final ValueChanged<int> onDayToggled;
final ValueChanged<OrderHubUiModel> onHubChanged;
final ValueChanged<OrderManagerUiModel?> onHubManagerChanged;
final VoidCallback onPositionAdded;
final void Function(int index, OrderPositionUiModel position) onPositionUpdated;
final void Function(int index) onPositionRemoved;
final List<OrderManagerUiModel> hubManagers;
final OrderManagerUiModel? selectedHubManager;
@override
Widget build(BuildContext context) {
final TranslationsClientCreateOrderRecurringEn labels =
t.client_create_order.recurring;
final TranslationsClientCreateOrderOneTimeEn oneTimeLabels =
t.client_create_order.one_time;
return ListView(
padding: const EdgeInsets.all(UiConstants.space5),
return Column(
children: <Widget>[
Text(
labels.title,
style: UiTypography.headline3m.textPrimary,
),
const SizedBox(height: UiConstants.space4),
RecurringOrderEventNameInput(
label: 'ORDER NAME',
value: eventName,
onChanged: onEventNameChanged,
),
const SizedBox(height: UiConstants.space4),
// Vendor Selection
Text('SELECT VENDOR', style: UiTypography.footnote2r.textSecondary),
const SizedBox(height: UiConstants.space2),
Container(
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3),
height: 48,
decoration: BoxDecoration(
color: UiColors.white,
borderRadius: UiConstants.radiusMd,
border: Border.all(color: UiColors.border),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<Vendor>(
isExpanded: true,
value: selectedVendor,
icon: const Icon(
UiIcons.chevronDown,
size: 18,
color: UiColors.iconSecondary,
Expanded(
child: Stack(
children: <Widget>[
RecurringOrderForm(
eventName: eventName,
selectedVendor: selectedVendor,
vendors: vendors,
startDate: startDate,
endDate: endDate,
recurringDays: recurringDays,
selectedHub: selectedHub,
hubs: hubs,
positions: positions,
roles: roles,
onEventNameChanged: onEventNameChanged,
onVendorChanged: onVendorChanged,
onStartDateChanged: onStartDateChanged,
onEndDateChanged: onEndDateChanged,
onDayToggled: onDayToggled,
onHubChanged: onHubChanged,
onHubManagerChanged: onHubManagerChanged,
onPositionAdded: onPositionAdded,
onPositionUpdated: onPositionUpdated,
onPositionRemoved: onPositionRemoved,
hubManagers: hubManagers,
selectedHubManager: selectedHubManager,
),
onChanged: (Vendor? vendor) {
if (vendor != null) {
onVendorChanged(vendor);
}
},
items: vendors.map((Vendor vendor) {
return DropdownMenuItem<Vendor>(
value: vendor,
child: Text(
vendor.name,
style: UiTypography.body2m.textPrimary,
),
);
}).toList(),
),
if (status == OrderFormStatus.loading)
const Center(child: CircularProgressIndicator()),
],
),
),
const SizedBox(height: UiConstants.space4),
RecurringOrderDatePicker(
label: 'Start Date',
value: startDate,
onChanged: onStartDateChanged,
OrderBottomActionButton(
label: status == OrderFormStatus.loading
? oneTimeLabels.creating
: oneTimeLabels.create_order,
isLoading: status == OrderFormStatus.loading,
onPressed: isValid ? onSubmit : null,
),
const SizedBox(height: UiConstants.space4),
RecurringOrderDatePicker(
label: 'End Date',
value: endDate,
onChanged: onEndDateChanged,
),
const SizedBox(height: UiConstants.space4),
Text('Recurring Days', style: UiTypography.footnote2r.textSecondary),
const SizedBox(height: UiConstants.space2),
_RecurringDaysSelector(
selectedDays: recurringDays,
onToggle: onDayToggled,
),
const SizedBox(height: UiConstants.space4),
Text('HUB', style: UiTypography.footnote2r.textSecondary),
const SizedBox(height: UiConstants.space2),
Container(
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3),
height: 48,
decoration: BoxDecoration(
color: UiColors.white,
borderRadius: UiConstants.radiusMd,
border: Border.all(color: UiColors.border),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<OrderHubUiModel>(
isExpanded: true,
value: selectedHub,
icon: const Icon(
UiIcons.chevronDown,
size: 18,
color: UiColors.iconSecondary,
),
onChanged: (OrderHubUiModel? hub) {
if (hub != null) {
onHubChanged(hub);
}
},
items: hubs.map((OrderHubUiModel hub) {
return DropdownMenuItem<OrderHubUiModel>(
value: hub,
child: Text(
hub.name,
style: UiTypography.body2m.textPrimary,
),
);
}).toList(),
),
),
),
const SizedBox(height: UiConstants.space4),
HubManagerSelector(
label: oneTimeLabels.hub_manager_label,
description: oneTimeLabels.hub_manager_desc,
hintText: oneTimeLabels.hub_manager_hint,
noManagersText: oneTimeLabels.hub_manager_empty,
noneText: oneTimeLabels.hub_manager_none,
managers: hubManagers,
selectedManager: selectedHubManager,
onChanged: onHubManagerChanged,
),
const SizedBox(height: UiConstants.space6),
RecurringOrderSectionHeader(
title: oneTimeLabels.positions_title,
actionLabel: oneTimeLabels.add_position,
onAction: onPositionAdded,
),
const SizedBox(height: UiConstants.space3),
// Positions List
...positions.asMap().entries.map((
MapEntry<int, OrderPositionUiModel> entry,
) {
final int index = entry.key;
final OrderPositionUiModel position = entry.value;
return Padding(
padding: const EdgeInsets.only(bottom: UiConstants.space3),
child: RecurringOrderPositionCard(
index: index,
position: position,
isRemovable: positions.length > 1,
positionLabel: oneTimeLabels.positions_title,
roleLabel: oneTimeLabels.select_role,
workersLabel: oneTimeLabels.workers_label,
startLabel: oneTimeLabels.start_label,
endLabel: oneTimeLabels.end_label,
lunchLabel: oneTimeLabels.lunch_break_label,
roles: roles,
onUpdated: (OrderPositionUiModel updated) {
onPositionUpdated(index, updated);
},
onRemoved: () {
onPositionRemoved(index);
},
),
);
}),
],
);
}
}
class _RecurringDaysSelector extends StatelessWidget {
const _RecurringDaysSelector({
required this.selectedDays,
required this.onToggle,
});
final List<String> selectedDays;
final ValueChanged<int> onToggle;
@override
Widget build(BuildContext context) {
const List<String> labelsShort = <String>[
'S',
'M',
'T',
'W',
'T',
'F',
'S',
];
const List<String> labelsLong = <String>[
'SUN',
'MON',
'TUE',
'WED',
'THU',
'FRI',
'SAT',
];
return Wrap(
spacing: UiConstants.space2,
children: List<Widget>.generate(labelsShort.length, (int index) {
final bool isSelected = selectedDays.contains(labelsLong[index]);
return GestureDetector(
onTap: () => onToggle(index),
child: Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: isSelected ? UiColors.primary : UiColors.white,
shape: BoxShape.circle,
border: Border.all(color: UiColors.border),
),
alignment: Alignment.center,
child: Text(
labelsShort[index],
style: UiTypography.body2m.copyWith(
color: isSelected ? UiColors.white : UiColors.textSecondary,
),
),
),
);
}),
);
}
}
class _BottomActionButton extends StatelessWidget {
const _BottomActionButton({
required this.label,
required this.onPressed,
this.isLoading = false,
});
final String label;
final VoidCallback? onPressed;
final bool isLoading;
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.only(
left: UiConstants.space5,
right: UiConstants.space5,
top: UiConstants.space5,
bottom: MediaQuery.of(context).padding.bottom + UiConstants.space5,
),
decoration: const BoxDecoration(
color: UiColors.white,
border: Border(top: BorderSide(color: UiColors.border)),
),
child: SizedBox(
width: double.infinity,
child: UiButton.primary(
text: label,
onPressed: isLoading ? null : onPressed,
size: UiButtonSize.large,
),
),
);
}
}

View File

@@ -1,5 +1,5 @@
specVersion: "v1"
serviceId: "krow-workforce-db-validation"
serviceId: "krow-workforce-db"
location: "us-central1"
schema:
source: "./schema"
@@ -7,7 +7,7 @@ schema:
postgresql:
database: "krow_db"
cloudSql:
instanceId: "krow-sql-validation"
instanceId: "krow-sql"
# schemaValidation: "STRICT" # STRICT mode makes Postgres schema match Data Connect exactly.
# schemaValidation: "COMPATIBLE" # COMPATIBLE mode makes Postgres schema compatible with Data Connect.
connectorDirs: ["./connector"]