From 269623ea1575188cbaf52b3c496d85cb16ba2f65 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Sat, 21 Feb 2026 21:02:04 -0500 Subject: [PATCH] feat: add OrderEditSheet for editing existing orders with detailed role and position management --- .../widgets/order_edit_sheet.dart | 1506 ++++++++++++++++ .../presentation/widgets/view_order_card.dart | 1593 +---------------- 2 files changed, 1571 insertions(+), 1528 deletions(-) create mode 100644 apps/mobile/packages/features/client/orders/view_orders/lib/src/presentation/widgets/order_edit_sheet.dart diff --git a/apps/mobile/packages/features/client/orders/view_orders/lib/src/presentation/widgets/order_edit_sheet.dart b/apps/mobile/packages/features/client/orders/view_orders/lib/src/presentation/widgets/order_edit_sheet.dart new file mode 100644 index 00000000..7e13f228 --- /dev/null +++ b/apps/mobile/packages/features/client/orders/view_orders/lib/src/presentation/widgets/order_edit_sheet.dart @@ -0,0 +1,1506 @@ +import 'package:design_system/design_system.dart'; +import 'package:firebase_auth/firebase_auth.dart' as firebase; +import 'package:firebase_data_connect/firebase_data_connect.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:krow_data_connect/krow_data_connect.dart' as dc; +import 'package:krow_domain/krow_domain.dart'; + +class _RoleOption { + const _RoleOption({ + required this.id, + required this.name, + required this.costPerHour, + }); + + final String id; + final String name; + final double costPerHour; +} + +class _ShiftRoleKey { + const _ShiftRoleKey({required this.shiftId, required this.roleId}); + + final String shiftId; + final String roleId; +} + +/// A sophisticated bottom sheet for editing an existing order, +/// following the Unified Order Flow prototype and matching OneTimeOrderView. +class OrderEditSheet extends StatefulWidget { + const OrderEditSheet({ + required this.order, + this.onUpdated, + super.key, + }); + + final OrderItem order; + final VoidCallback? onUpdated; + + @override + State createState() => OrderEditSheetState(); +} + +class OrderEditSheetState extends State { + bool _showReview = false; + bool _isLoading = false; + + late TextEditingController _dateController; + late TextEditingController _globalLocationController; + late TextEditingController _orderNameController; + + late List> _positions; + + final dc.ExampleConnector _dataConnect = dc.ExampleConnector.instance; + final firebase.FirebaseAuth _firebaseAuth = firebase.FirebaseAuth.instance; + + List _vendors = const []; + Vendor? _selectedVendor; + List<_RoleOption> _roles = const <_RoleOption>[]; + List _hubs = const []; + dc.ListTeamHubsByOwnerIdTeamHubs? _selectedHub; + + String? _shiftId; + List<_ShiftRoleKey> _originalShiftRoles = const <_ShiftRoleKey>[]; + + @override + void initState() { + super.initState(); + _dateController = TextEditingController(text: widget.order.date); + _globalLocationController = TextEditingController( + text: widget.order.locationAddress, + ); + _orderNameController = TextEditingController(); + + _positions = >[ + { + 'shiftId': null, + 'roleId': '', + 'roleName': '', + 'originalRoleId': null, + 'count': widget.order.workersNeeded, + 'start_time': widget.order.startTime, + 'end_time': widget.order.endTime, + 'lunch_break': 'NO_BREAK', + 'location': null, + }, + ]; + + _loadOrderDetails(); + } + + @override + void dispose() { + _dateController.dispose(); + _globalLocationController.dispose(); + _orderNameController.dispose(); + super.dispose(); + } + + Future _loadOrderDetails() async { + final String? businessId = + dc.ClientSessionStore.instance.session?.business?.id; + if (businessId == null || businessId.isEmpty) { + await _firebaseAuth.signOut(); + return; + } + + if (widget.order.orderId.isEmpty) { + return; + } + + try { + final QueryResult< + dc.ListShiftRolesByBusinessAndOrderData, + dc.ListShiftRolesByBusinessAndOrderVariables> result = await _dataConnect + .listShiftRolesByBusinessAndOrder( + businessId: businessId, + orderId: widget.order.orderId, + ) + .execute(); + + final List shiftRoles = + result.data.shiftRoles; + if (shiftRoles.isEmpty) { + await _loadHubsAndSelect(); + return; + } + + final dc.ListShiftRolesByBusinessAndOrderShiftRolesShift firstShift = + shiftRoles.first.shift; + final DateTime? orderDate = firstShift.order.date?.toDateTime(); + final String dateText = orderDate == null + ? widget.order.date + : DateFormat('yyyy-MM-dd').format(orderDate); + final String location = firstShift.order.teamHub.hubName; + + _dateController.text = dateText; + _globalLocationController.text = location; + _orderNameController.text = firstShift.order.eventName ?? ''; + _shiftId = shiftRoles.first.shiftId; + + final List> positions = + shiftRoles.map((dc.ListShiftRolesByBusinessAndOrderShiftRoles role) { + return { + 'shiftId': role.shiftId, + 'roleId': role.roleId, + 'roleName': role.role.name, + 'originalRoleId': role.roleId, + 'count': role.count, + 'start_time': _formatTimeForField(role.startTime), + 'end_time': _formatTimeForField(role.endTime), + 'lunch_break': _breakValueFromDuration(role.breakType), + 'location': null, + }; + }).toList(); + + if (positions.isEmpty) { + positions.add(_emptyPosition()); + } + + final List<_ShiftRoleKey> originalShiftRoles = + shiftRoles + .map( + (dc.ListShiftRolesByBusinessAndOrderShiftRoles role) => + _ShiftRoleKey(shiftId: role.shiftId, roleId: role.roleId), + ) + .toList(); + + await _loadVendorsAndSelect(firstShift.order.vendorId); + final dc.ListShiftRolesByBusinessAndOrderShiftRolesShiftOrderTeamHub + teamHub = firstShift.order.teamHub; + await _loadHubsAndSelect( + placeId: teamHub.placeId, + hubName: teamHub.hubName, + address: teamHub.address, + ); + + if (mounted) { + setState(() { + _positions = positions; + _originalShiftRoles = originalShiftRoles; + }); + } + } catch (_) { + // Keep current state on failure. + } + } + + Future _loadHubsAndSelect({ + String? placeId, + String? hubName, + String? address, + }) async { + final String? businessId = + dc.ClientSessionStore.instance.session?.business?.id; + if (businessId == null || businessId.isEmpty) { + return; + } + + try { + final QueryResult< + dc.ListTeamHubsByOwnerIdData, + dc.ListTeamHubsByOwnerIdVariables> result = await _dataConnect + .listTeamHubsByOwnerId(ownerId: businessId) + .execute(); + + final List hubs = result.data.teamHubs; + dc.ListTeamHubsByOwnerIdTeamHubs? selected; + + if (placeId != null && placeId.isNotEmpty) { + for (final dc.ListTeamHubsByOwnerIdTeamHubs hub in hubs) { + if (hub.placeId == placeId) { + selected = hub; + break; + } + } + } + + if (selected == null && hubName != null && hubName.isNotEmpty) { + for (final dc.ListTeamHubsByOwnerIdTeamHubs hub in hubs) { + if (hub.hubName == hubName) { + selected = hub; + break; + } + } + } + + if (selected == null && address != null && address.isNotEmpty) { + for (final dc.ListTeamHubsByOwnerIdTeamHubs hub in hubs) { + if (hub.address == address) { + selected = hub; + break; + } + } + } + + selected ??= hubs.isNotEmpty ? hubs.first : null; + + if (mounted) { + setState(() { + _hubs = hubs; + _selectedHub = selected; + if (selected != null) { + _globalLocationController.text = selected.address; + } + }); + } + } catch (_) { + if (mounted) { + setState(() { + _hubs = const []; + _selectedHub = null; + }); + } + } + } + + Future _loadVendorsAndSelect(String? selectedVendorId) async { + try { + final QueryResult result = + await _dataConnect.listVendors().execute(); + final List vendors = result.data.vendors + .map( + (dc.ListVendorsVendors vendor) => Vendor( + id: vendor.id, + name: vendor.companyName, + rates: const {}, + ), + ) + .toList(); + + Vendor? selectedVendor; + if (selectedVendorId != null && selectedVendorId.isNotEmpty) { + for (final Vendor vendor in vendors) { + if (vendor.id == selectedVendorId) { + selectedVendor = vendor; + break; + } + } + } + selectedVendor ??= vendors.isNotEmpty ? vendors.first : null; + + if (mounted) { + setState(() { + _vendors = vendors; + _selectedVendor = selectedVendor; + }); + } + + if (selectedVendor != null) { + await _loadRolesForVendor(selectedVendor.id); + } + } catch (_) { + if (mounted) { + setState(() { + _vendors = const []; + _selectedVendor = null; + _roles = const <_RoleOption>[]; + }); + } + } + } + + Future _loadRolesForVendor(String vendorId) async { + try { + final QueryResult + result = await _dataConnect + .listRolesByVendorId(vendorId: vendorId) + .execute(); + final List<_RoleOption> roles = result.data.roles + .map( + (dc.ListRolesByVendorIdRoles role) => _RoleOption( + id: role.id, + name: role.name, + costPerHour: role.costPerHour, + ), + ) + .toList(); + if (mounted) { + setState(() => _roles = roles); + } + } catch (_) { + if (mounted) { + setState(() => _roles = const <_RoleOption>[]); + } + } + } + + Map _emptyPosition() { + return { + 'shiftId': _shiftId, + 'roleId': '', + 'roleName': '', + 'originalRoleId': null, + 'count': 1, + 'start_time': '09:00', + 'end_time': '17:00', + 'lunch_break': 'NO_BREAK', + 'location': null, + }; + } + + String _formatTimeForField(Timestamp? value) { + if (value == null) return ''; + try { + return DateFormat('HH:mm').format(value.toDateTime().toLocal()); + } catch (_) { + return ''; + } + } + + String _breakValueFromDuration(dc.EnumValue? breakType) { + final dc.BreakDuration? value = + breakType is dc.Known ? breakType.value : null; + switch (value) { + case dc.BreakDuration.MIN_10: + return 'MIN_10'; + case dc.BreakDuration.MIN_15: + return 'MIN_15'; + case dc.BreakDuration.MIN_30: + return 'MIN_30'; + case dc.BreakDuration.MIN_45: + return 'MIN_45'; + case dc.BreakDuration.MIN_60: + return 'MIN_60'; + case dc.BreakDuration.NO_BREAK: + case null: + return 'NO_BREAK'; + } + } + + dc.BreakDuration _breakDurationFromValue(String value) { + switch (value) { + case 'MIN_10': + return dc.BreakDuration.MIN_10; + case 'MIN_15': + return dc.BreakDuration.MIN_15; + case 'MIN_30': + return dc.BreakDuration.MIN_30; + case 'MIN_45': + return dc.BreakDuration.MIN_45; + case 'MIN_60': + return dc.BreakDuration.MIN_60; + default: + return dc.BreakDuration.NO_BREAK; + } + } + + bool _isBreakPaid(String value) { + return value == 'MIN_10' || value == 'MIN_15'; + } + + _RoleOption? _roleById(String roleId) { + for (final _RoleOption role in _roles) { + if (role.id == roleId) { + return role; + } + } + return null; + } + + double _rateForRole(String roleId) { + return _roleById(roleId)?.costPerHour ?? 0; + } + + DateTime _parseDate(String value) { + try { + return DateFormat('yyyy-MM-dd').parse(value); + } catch (_) { + return DateTime.now(); + } + } + + DateTime _parseTime(DateTime date, String time) { + if (time.trim().isEmpty) { + throw Exception('Shift time is missing.'); + } + + DateTime parsed; + try { + parsed = DateFormat.Hm().parse(time); + } catch (_) { + parsed = DateFormat.jm().parse(time); + } + + return DateTime( + date.year, + date.month, + date.day, + parsed.hour, + parsed.minute, + ); + } + + Timestamp _toTimestamp(DateTime date) { + final DateTime utc = date.toUtc(); + final int millis = utc.millisecondsSinceEpoch; + final int seconds = millis ~/ 1000; + final int nanos = (millis % 1000) * 1000000; + return Timestamp(nanos, seconds); + } + + double _calculateTotalCost() { + double total = 0; + for (final Map pos in _positions) { + final String roleId = pos['roleId']?.toString() ?? ''; + if (roleId.isEmpty) { + continue; + } + final DateTime date = _parseDate(_dateController.text); + final DateTime start = _parseTime(date, pos['start_time'].toString()); + final DateTime end = _parseTime(date, pos['end_time'].toString()); + final DateTime normalizedEnd = + end.isBefore(start) ? end.add(const Duration(days: 1)) : end; + final double hours = normalizedEnd.difference(start).inMinutes / 60.0; + final double rate = _rateForRole(roleId); + final int count = pos['count'] as int; + total += rate * hours * count; + } + return total; + } + + Future _saveOrderChanges() async { + if (_shiftId == null || _shiftId!.isEmpty) { + return; + } + + final String? businessId = + dc.ClientSessionStore.instance.session?.business?.id; + if (businessId == null || businessId.isEmpty) { + await _firebaseAuth.signOut(); + return; + } + + final DateTime orderDate = _parseDate(_dateController.text); + final dc.ListTeamHubsByOwnerIdTeamHubs? selectedHub = _selectedHub; + if (selectedHub == null) { + return; + } + + int totalWorkers = 0; + double shiftCost = 0; + + final List<_ShiftRoleKey> remainingOriginal = + List<_ShiftRoleKey>.from(_originalShiftRoles); + + for (final Map pos in _positions) { + final String roleId = pos['roleId']?.toString() ?? ''; + if (roleId.isEmpty) { + continue; + } + + final String shiftId = pos['shiftId']?.toString() ?? _shiftId!; + final int count = pos['count'] as int; + final DateTime start = _parseTime(orderDate, pos['start_time'].toString()); + final DateTime end = _parseTime(orderDate, pos['end_time'].toString()); + final DateTime normalizedEnd = + end.isBefore(start) ? end.add(const Duration(days: 1)) : end; + final double hours = normalizedEnd.difference(start).inMinutes / 60.0; + final double rate = _rateForRole(roleId); + final double totalValue = rate * hours * count; + final String lunchBreak = pos['lunch_break'] as String; + + totalWorkers += count; + shiftCost += totalValue; + + final String? originalRoleId = pos['originalRoleId']?.toString(); + remainingOriginal.removeWhere( + (_ShiftRoleKey key) => + key.shiftId == shiftId && key.roleId == originalRoleId, + ); + + if (originalRoleId != null && originalRoleId.isNotEmpty) { + if (originalRoleId != roleId) { + await _dataConnect + .deleteShiftRole(shiftId: shiftId, roleId: originalRoleId) + .execute(); + await _dataConnect + .createShiftRole( + shiftId: shiftId, + roleId: roleId, + count: count, + ) + .startTime(_toTimestamp(start)) + .endTime(_toTimestamp(normalizedEnd)) + .hours(hours) + .breakType(_breakDurationFromValue(lunchBreak)) + .isBreakPaid(_isBreakPaid(lunchBreak)) + .totalValue(totalValue) + .execute(); + } else { + await _dataConnect + .updateShiftRole(shiftId: shiftId, roleId: roleId) + .count(count) + .startTime(_toTimestamp(start)) + .endTime(_toTimestamp(normalizedEnd)) + .hours(hours) + .breakType(_breakDurationFromValue(lunchBreak)) + .isBreakPaid(_isBreakPaid(lunchBreak)) + .totalValue(totalValue) + .execute(); + } + } else { + await _dataConnect + .createShiftRole( + shiftId: shiftId, + roleId: roleId, + count: count, + ) + .startTime(_toTimestamp(start)) + .endTime(_toTimestamp(normalizedEnd)) + .hours(hours) + .breakType(_breakDurationFromValue(lunchBreak)) + .isBreakPaid(_isBreakPaid(lunchBreak)) + .totalValue(totalValue) + .execute(); + } + } + + for (final _ShiftRoleKey key in remainingOriginal) { + await _dataConnect + .deleteShiftRole(shiftId: key.shiftId, roleId: key.roleId) + .execute(); + } + + final DateTime orderDateOnly = DateTime( + orderDate.year, + orderDate.month, + orderDate.day, + ); + + await _dataConnect + .updateOrder(id: widget.order.orderId, teamHubId: selectedHub.id) + .vendorId(_selectedVendor?.id) + .date(_toTimestamp(orderDateOnly)) + .eventName(_orderNameController.text) + .execute(); + + await _dataConnect + .updateShift(id: _shiftId!) + .title('shift 1 ${DateFormat('yyyy-MM-dd').format(orderDate)}') + .date(_toTimestamp(orderDateOnly)) + .location(selectedHub.hubName) + .locationAddress(selectedHub.address) + .latitude(selectedHub.latitude) + .longitude(selectedHub.longitude) + .placeId(selectedHub.placeId) + .city(selectedHub.city) + .state(selectedHub.state) + .street(selectedHub.street) + .country(selectedHub.country) + .workersNeeded(totalWorkers) + .cost(shiftCost) + .durationDays(1) + .execute(); + } + + void _addPosition() { + setState(() { + _positions.add(_emptyPosition()); + }); + } + + void _removePosition(int index) { + if (_positions.length > 1) { + setState(() => _positions.removeAt(index)); + } + } + + void _updatePosition(int index, String key, dynamic value) { + setState(() => _positions[index][key] = value); + } + + @override + Widget build(BuildContext context) { + if (_isLoading && _showReview) { + return _buildSuccessView(); + } + + return _showReview ? _buildReviewView() : _buildFormView(); + } + + Widget _buildFormView() { + return Container( + height: MediaQuery.of(context).size.height * 0.95, + decoration: const BoxDecoration( + color: UiColors.bgPrimary, + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + ), + child: Column( + children: [ + _buildHeader(), + Expanded( + child: ListView( + padding: const EdgeInsets.all(UiConstants.space5), + children: [ + Text( + 'Edit Your Order', + style: UiTypography.headline3m.textPrimary, + ), + const SizedBox(height: UiConstants.space4), + + _buildSectionHeader('VENDOR'), + Container( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space3, + ), + height: 48, + decoration: BoxDecoration( + color: UiColors.white, + borderRadius: UiConstants.radiusMd, + border: Border.all(color: UiColors.border), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + isExpanded: true, + value: _selectedVendor, + icon: const Icon( + UiIcons.chevronDown, + size: 18, + color: UiColors.iconSecondary, + ), + onChanged: (Vendor? vendor) { + if (vendor != null) { + setState(() => _selectedVendor = vendor); + _loadRolesForVendor(vendor.id); + } + }, + items: _vendors.map((Vendor vendor) { + return DropdownMenuItem( + value: vendor, + child: Text( + vendor.name, + style: UiTypography.body2m.textPrimary, + ), + ); + }).toList(), + ), + ), + ), + const SizedBox(height: UiConstants.space4), + + _buildSectionHeader('DATE'), + UiTextField( + controller: _dateController, + hintText: 'mm/dd/yyyy', + prefixIcon: UiIcons.calendar, + readOnly: true, + onTap: () {}, + ), + const SizedBox(height: UiConstants.space4), + + _buildSectionHeader('ORDER NAME'), + UiTextField( + controller: _orderNameController, + hintText: 'Order name', + prefixIcon: UiIcons.briefcase, + ), + const SizedBox(height: UiConstants.space4), + + _buildSectionHeader('HUB'), + Container( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space3, + ), + height: 48, + decoration: BoxDecoration( + color: UiColors.white, + borderRadius: UiConstants.radiusMd, + border: Border.all(color: UiColors.border), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + isExpanded: true, + value: _selectedHub, + icon: const Icon( + UiIcons.chevronDown, + size: 18, + color: UiColors.iconSecondary, + ), + onChanged: + (dc.ListTeamHubsByOwnerIdTeamHubs? hub) { + if (hub != null) { + setState(() { + _selectedHub = hub; + _globalLocationController.text = hub.address; + }); + } + }, + items: _hubs.map( + (dc.ListTeamHubsByOwnerIdTeamHubs hub) { + return DropdownMenuItem< + dc.ListTeamHubsByOwnerIdTeamHubs>( + value: hub, + child: Text( + hub.hubName, + style: UiTypography.body2m.textPrimary, + ), + ); + }, + ).toList(), + ), + ), + ), + const SizedBox(height: UiConstants.space6), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'POSITIONS', + style: UiTypography.headline4m.textPrimary, + ), + TextButton( + onPressed: _addPosition, + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + minimumSize: Size.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + spacing: UiConstants.space2, + children: [ + const Icon(UiIcons.add, size: 16, color: UiColors.primary), + Text( + 'Add Position', + style: UiTypography.body2m.primary, + ), + ], + ), + ), + ], + ), + const SizedBox(height: UiConstants.space3), + + ..._positions.asMap().entries.map(( + MapEntry> entry, + ) { + return _buildPositionCard(entry.key, entry.value); + }), + + const SizedBox(height: 40), + ], + ), + ), + _buildBottomAction( + label: 'Review ${_positions.length} Positions', + onPressed: () => setState(() => _showReview = true), + ), + ], + ), + ); + } + + Widget _buildHeader() { + return Container( + padding: const EdgeInsets.fromLTRB(20, 24, 20, 20), + decoration: const BoxDecoration( + color: UiColors.primary, + borderRadius: BorderRadius.vertical(top: Radius.circular(UiConstants.space6)), + ), + child: Row( + children: [ + GestureDetector( + onTap: () => Navigator.pop(context), + child: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: UiColors.white.withValues(alpha: 0.2), + borderRadius: UiConstants.radiusMd, + ), + child: const Icon( + UiIcons.chevronLeft, + color: UiColors.white, + size: 24, + ), + ), + ), + const SizedBox(width: UiConstants.space3), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'One-Time Order', + style: UiTypography.headline3m.copyWith(color: UiColors.white), + ), + Text( + 'Refine your staffing needs', + style: UiTypography.footnote2r.copyWith( + color: UiColors.white.withValues(alpha: 0.8), + ), + ), + ], + ), + ], + ), + ); + } + + Widget _buildSectionHeader(String title) { + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Text(title, style: UiTypography.footnote2r.textSecondary), + ); + } + + Widget _buildPositionCard(int index, Map pos) { + return Container( + margin: const EdgeInsets.only(bottom: UiConstants.space3), + padding: const EdgeInsets.all(UiConstants.space4), + decoration: BoxDecoration( + color: UiColors.white, + borderRadius: UiConstants.radiusLg, + border: Border.all(color: UiColors.border), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'POSITION #${index + 1}', + style: UiTypography.footnote1m.textSecondary, + ), + if (_positions.length > 1) + GestureDetector( + onTap: () => _removePosition(index), + child: Text( + 'Remove', + style: UiTypography.footnote1m.copyWith( + color: UiColors.destructive, + ), + ), + ), + ], + ), + const SizedBox(height: UiConstants.space3), + + _buildDropdownField( + hint: 'Select role', + value: pos['roleId'], + items: [ + ..._roles.map((_RoleOption role) => role.id), + if (pos['roleId'] != null && + pos['roleId'].toString().isNotEmpty && + !_roles.any( + (_RoleOption role) => role.id == pos['roleId'].toString(), + )) + pos['roleId'].toString(), + ], + itemBuilder: (dynamic roleId) { + final _RoleOption? role = _roleById(roleId.toString()); + if (role == null) { + final String fallback = pos['roleName']?.toString() ?? ''; + return fallback.isEmpty ? roleId.toString() : fallback; + } + return '${role.name} - \$${role.costPerHour.toStringAsFixed(0)}/hr'; + }, + onChanged: (dynamic val) { + final String roleId = val?.toString() ?? ''; + final _RoleOption? role = _roleById(roleId); + setState(() { + _positions[index]['roleId'] = roleId; + _positions[index]['roleName'] = role?.name ?? ''; + }); + }, + ), + + const SizedBox(height: UiConstants.space3), + + Row( + children: [ + Expanded( + child: _buildInlineTimeInput( + label: 'Start', + value: pos['start_time'], + onTap: () async { + final TimeOfDay? picked = await showTimePicker( + context: context, + initialTime: TimeOfDay.now(), + ); + if (picked != null && context.mounted) { + _updatePosition( + index, + 'start_time', + picked.format(context), + ); + } + }, + ), + ), + const SizedBox(width: UiConstants.space2), + Expanded( + child: _buildInlineTimeInput( + label: 'End', + value: pos['end_time'], + onTap: () async { + final TimeOfDay? picked = await showTimePicker( + context: context, + initialTime: TimeOfDay.now(), + ); + if (picked != null && context.mounted) { + _updatePosition( + index, + 'end_time', + picked.format(context), + ); + } + }, + ), + ), + const SizedBox(width: UiConstants.space2), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Workers', + style: UiTypography.footnote2r.textSecondary, + ), + const SizedBox(height: UiConstants.space1), + Container( + height: 40, + decoration: BoxDecoration( + color: UiColors.bgSecondary, + borderRadius: UiConstants.radiusSm, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + GestureDetector( + onTap: () { + if ((pos['count'] as int) > 1) { + _updatePosition( + index, + 'count', + (pos['count'] as int) - 1, + ); + } + }, + child: const Icon(UiIcons.minus, size: 12), + ), + Text( + '${pos['count']}', + style: UiTypography.body2b.textPrimary, + ), + GestureDetector( + onTap: () => _updatePosition( + index, + 'count', + (pos['count'] as int) + 1, + ), + child: const Icon(UiIcons.add, size: 12), + ), + ], + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: UiConstants.space4), + + if (pos['location'] == null) + GestureDetector( + onTap: () => _updatePosition(index, 'location', ''), + child: Row( + children: [ + const Icon(UiIcons.mapPin, size: 14, color: UiColors.primary), + const SizedBox(width: UiConstants.space1), + Text( + 'Use different location for this position', + style: UiTypography.footnote1m.copyWith( + color: UiColors.primary, + ), + ), + ], + ), + ) + else + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + const Icon( + UiIcons.mapPin, + size: 14, + color: UiColors.iconSecondary, + ), + const SizedBox(width: UiConstants.space1), + Text( + 'Different Location', + style: UiTypography.footnote1m.textSecondary, + ), + ], + ), + GestureDetector( + onTap: () => _updatePosition(index, 'location', null), + child: const Icon( + UiIcons.close, + size: 14, + color: UiColors.destructive, + ), + ), + ], + ), + const SizedBox(height: UiConstants.space2), + UiTextField( + controller: TextEditingController(text: pos['location']), + hintText: 'Enter different address', + onChanged: (String val) => + _updatePosition(index, 'location', val), + ), + ], + ), + + const SizedBox(height: UiConstants.space3), + + _buildSectionHeader('LUNCH BREAK'), + _buildDropdownField( + hint: 'No Break', + value: pos['lunch_break'], + items: [ + 'NO_BREAK', + 'MIN_10', + 'MIN_15', + 'MIN_30', + 'MIN_45', + 'MIN_60', + ], + itemBuilder: (dynamic val) { + switch (val.toString()) { + case 'MIN_10': + return '10 min (Paid)'; + case 'MIN_15': + return '15 min (Paid)'; + case 'MIN_30': + return '30 min (Unpaid)'; + case 'MIN_45': + return '45 min (Unpaid)'; + case 'MIN_60': + return '60 min (Unpaid)'; + default: + return 'No Break'; + } + }, + onChanged: (dynamic val) => + _updatePosition(index, 'lunch_break', val), + ), + ], + ), + ); + } + + Widget _buildDropdownField({ + required String hint, + required dynamic value, + required List items, + String Function(dynamic)? itemBuilder, + required ValueChanged onChanged, + }) { + return Container( + height: 44, + padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3), + decoration: BoxDecoration( + borderRadius: UiConstants.radiusMd, + border: Border.all(color: UiColors.border), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + isExpanded: true, + hint: Text(hint, style: UiTypography.body2r.textPlaceholder), + value: value == '' || value == null ? null : value, + icon: const Icon( + UiIcons.chevronDown, + size: 18, + color: UiColors.iconSecondary, + ), + onChanged: onChanged, + items: items.toSet().map((dynamic item) { + String label = item.toString(); + if (itemBuilder != null) label = itemBuilder(item); + return DropdownMenuItem( + value: item, + child: Text(label, style: UiTypography.body2r.textPrimary), + ); + }).toList(), + ), + ), + ); + } + + Widget _buildInlineTimeInput({ + required String label, + required String value, + required VoidCallback onTap, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: UiTypography.footnote2r.textSecondary), + const SizedBox(height: UiConstants.space1), + GestureDetector( + onTap: onTap, + child: Container( + height: 40, + padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3), + decoration: BoxDecoration( + borderRadius: UiConstants.radiusSm, + border: Border.all(color: UiColors.border), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + value.isEmpty ? '--:--' : value, + style: UiTypography.body2r.textPrimary, + ), + const Icon( + UiIcons.clock, + size: 14, + color: UiColors.iconSecondary, + ), + ], + ), + ), + ), + ], + ); + } + + Widget _buildBottomAction({ + required String label, + required VoidCallback onPressed, + }) { + return Container( + padding: EdgeInsets.fromLTRB( + UiConstants.space5, + UiConstants.space5, + UiConstants.space5, + 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: onPressed, + size: UiButtonSize.large, + ), + ), + ); + } + + Widget _buildReviewView() { + final int totalWorkers = _positions.fold( + 0, + (int sum, Map p) => sum + (p['count'] as int), + ); + final double totalCost = _calculateTotalCost(); + + return Container( + height: MediaQuery.of(context).size.height * 0.95, + decoration: const BoxDecoration( + color: UiColors.bgSecondary, + borderRadius: BorderRadius.vertical(top: Radius.circular(UiConstants.space6)), + ), + child: Column( + children: [ + _buildHeader(), + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Summary Card + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + UiColors.primary.withValues(alpha: 0.05), + UiColors.primary.withValues(alpha: 0.1), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: UiColors.primary.withValues(alpha: 0.2), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _buildSummaryItem('${_positions.length}', 'Positions'), + _buildSummaryItem('$totalWorkers', 'Workers'), + _buildSummaryItem( + '\$${totalCost.round()}', + 'Est. Cost', + ), + ], + ), + ), + const SizedBox(height: 20), + + // Order Details + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: UiColors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: UiColors.separatorPrimary), + ), + child: Column( + children: [ + Row( + children: [ + const Icon( + UiIcons.calendar, + size: 16, + color: UiColors.primary, + ), + const SizedBox(width: 8), + Text( + _dateController.text, + style: UiTypography.body2m.textPrimary, + ), + ], + ), + if (_globalLocationController + .text + .isNotEmpty) ...[ + const SizedBox(height: 12), + Row( + children: [ + const Icon( + UiIcons.mapPin, + size: 16, + color: UiColors.primary, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + _globalLocationController.text, + style: UiTypography.body2r.textPrimary, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ], + ], + ), + ), + const SizedBox(height: 24), + + Text( + 'Positions Breakdown', + style: UiTypography.body2b.textPrimary, + ), + const SizedBox(height: 12), + + ..._positions.map( + (Map pos) => _buildReviewPositionCard(pos), + ), + + const SizedBox(height: 40), + ], + ), + ), + ), + + // Footer + Container( + padding: EdgeInsets.fromLTRB( + UiConstants.space5, + UiConstants.space5, + UiConstants.space5, + MediaQuery.of(context).padding.bottom + UiConstants.space5, + ), + decoration: const BoxDecoration( + color: UiColors.white, + border: Border(top: BorderSide(color: UiColors.border)), + ), + child: Row( + children: [ + Expanded( + child: UiButton.secondary( + text: 'Edit', + onPressed: () => setState(() => _showReview = false), + ), + ), + const SizedBox(width: 12), + Expanded( + child: UiButton.primary( + text: 'Confirm & Save', + onPressed: () async { + setState(() => _isLoading = true); + await _saveOrderChanges(); + if (mounted) { + widget.onUpdated?.call(); + Navigator.pop(context); + } + }, + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildSummaryItem(String value, String label) { + return Column( + children: [ + Text( + value, + style: UiTypography.headline2m.copyWith( + color: UiColors.primary, + fontWeight: FontWeight.bold, + ), + ), + Text( + label.toUpperCase(), + style: UiTypography.titleUppercase4m.copyWith( + color: UiColors.textSecondary, + ), + ), + ], + ); + } + + Widget _buildReviewPositionCard(Map pos) { + final String roleId = pos['roleId']?.toString() ?? ''; + final _RoleOption? role = _roleById(roleId); + final double rate = role?.costPerHour ?? 0; + + return Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: UiColors.white, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: UiColors.separatorSecondary), + ), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + (role?.name ?? pos['roleName']?.toString() ?? '').isEmpty + ? 'Position' + : (role?.name ?? pos['roleName']?.toString() ?? ''), + style: UiTypography.body2b.textPrimary, + ), + Text( + '${pos['count']} worker${pos['count'] > 1 ? 's' : ''}', + style: UiTypography.footnote2r.textSecondary, + ), + ], + ), + Text( + '\$${rate.round()}/hr', + style: UiTypography.body2b.copyWith(color: UiColors.primary), + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + const Icon( + UiIcons.clock, + size: 14, + color: UiColors.iconSecondary, + ), + const SizedBox(width: 6), + Text( + '${pos['start_time']} - ${pos['end_time']}', + style: UiTypography.footnote2r.textSecondary, + ), + ], + ), + ], + ), + ); + } + + Widget _buildSuccessView() { + return Container( + width: double.infinity, + height: MediaQuery.of(context).size.height * 0.95, + decoration: const BoxDecoration( + color: UiColors.primary, + borderRadius: BorderRadius.vertical(top: Radius.circular(UiConstants.space6)), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 80, + height: 80, + decoration: const BoxDecoration( + color: UiColors.accent, + shape: BoxShape.circle, + ), + child: const Center( + child: Icon( + UiIcons.success, + size: 40, + color: UiColors.foreground, + ), + ), + ), + const SizedBox(height: 24), + Text( + 'Order Updated!', + style: UiTypography.headline1m.copyWith(color: UiColors.white), + ), + const SizedBox(height: 12), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 40), + child: Text( + 'Your shift has been updated successfully.', + textAlign: TextAlign.center, + style: UiTypography.body1r.copyWith( + color: UiColors.white.withValues(alpha: 0.7), + ), + ), + ), + const SizedBox(height: 40), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 40), + child: UiButton.secondary( + text: 'Back to Orders', + fullWidth: true, + style: OutlinedButton.styleFrom( + backgroundColor: UiColors.white, + foregroundColor: UiColors.primary, + ), + onPressed: () => Navigator.pop(context), + ), + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/orders/view_orders/lib/src/presentation/widgets/view_order_card.dart b/apps/mobile/packages/features/client/orders/view_orders/lib/src/presentation/widgets/view_order_card.dart index e010a8be..7960f636 100644 --- a/apps/mobile/packages/features/client/orders/view_orders/lib/src/presentation/widgets/view_order_card.dart +++ b/apps/mobile/packages/features/client/orders/view_orders/lib/src/presentation/widgets/view_order_card.dart @@ -1,11 +1,7 @@ import 'package:core_localization/core_localization.dart'; import 'package:design_system/design_system.dart'; -import 'package:firebase_auth/firebase_auth.dart' as firebase; -import 'package:firebase_data_connect/firebase_data_connect.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:intl/intl.dart'; -import 'package:krow_data_connect/krow_data_connect.dart' as dc; import 'package:krow_domain/krow_domain.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -15,6 +11,8 @@ import '../blocs/view_orders_cubit.dart'; /// /// This widget complies with the KROW Design System by using /// tokens from `package:design_system`. +import 'order_edit_sheet.dart'; + class ViewOrderCard extends StatefulWidget { /// Creates a [ViewOrderCard] for the given [order]. const ViewOrderCard({required this.order, super.key}); @@ -34,7 +32,7 @@ class _ViewOrderCardState extends State { context: context, isScrollControlled: true, backgroundColor: UiColors.transparent, - builder: (BuildContext context) => _OrderEditSheet( + builder: (BuildContext context) => OrderEditSheet( order: order, onUpdated: () => this.context.read().updateWeekOffset(0), ), @@ -96,6 +94,19 @@ class _ViewOrderCardState extends State { } } + String _getOrderTypeLabel(OrderType type) { + switch (type) { + case OrderType.oneTime: + return 'ONE-TIME'; + case OrderType.permanent: + return 'PERMANENT'; + case OrderType.recurring: + return 'RECURRING'; + case OrderType.rapid: + return 'RAPID'; + } + } + @override Widget build(BuildContext context) { final OrderItem order = widget.order; @@ -136,37 +147,60 @@ class _ViewOrderCardState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Status Badge - Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 2, - ), - decoration: BoxDecoration( - color: statusColor.withValues(alpha: 0.1), - borderRadius: UiConstants.radiusSm, - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: 6, - height: 6, - decoration: BoxDecoration( - color: statusColor, - shape: BoxShape.circle, - ), + // Status and Type Badges + Wrap( + spacing: UiConstants.space2, + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 2, ), - const SizedBox(width: UiConstants.space1 + 2), - Text( - statusLabel.toUpperCase(), + decoration: BoxDecoration( + color: statusColor.withValues(alpha: 0.1), + borderRadius: UiConstants.radiusSm, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 6, + height: 6, + decoration: BoxDecoration( + color: statusColor, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: UiConstants.space1 + 2), + Text( + statusLabel.toUpperCase(), + style: UiTypography.footnote2b.copyWith( + color: statusColor, + letterSpacing: 0.5, + ), + ), + ], + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 2, + ), + decoration: BoxDecoration( + color: UiColors.bgSecondary, + borderRadius: UiConstants.radiusSm, + border: Border.all(color: UiColors.border), + ), + child: Text( + _getOrderTypeLabel(order.orderType), style: UiTypography.footnote2b.copyWith( - color: statusColor, + color: UiColors.textSecondary, letterSpacing: 0.5, ), ), - ], - ), + ), + ], ), const SizedBox(height: UiConstants.space3), // Title @@ -687,1500 +721,3 @@ class _ViewOrderCardState extends State { } } -class _RoleOption { - const _RoleOption({ - required this.id, - required this.name, - required this.costPerHour, - }); - - final String id; - final String name; - final double costPerHour; -} - -class _ShiftRoleKey { - const _ShiftRoleKey({required this.shiftId, required this.roleId}); - - final String shiftId; - final String roleId; -} - -/// A sophisticated bottom sheet for editing an existing order, -/// following the Unified Order Flow prototype and matching OneTimeOrderView. -class _OrderEditSheet extends StatefulWidget { - const _OrderEditSheet({ - required this.order, - this.onUpdated, - }); - - final OrderItem order; - final VoidCallback? onUpdated; - - @override - State<_OrderEditSheet> createState() => _OrderEditSheetState(); -} - -class _OrderEditSheetState extends State<_OrderEditSheet> { - bool _showReview = false; - bool _isLoading = false; - - late TextEditingController _dateController; - late TextEditingController _globalLocationController; - late TextEditingController _orderNameController; - - late List> _positions; - - final dc.ExampleConnector _dataConnect = dc.ExampleConnector.instance; - final firebase.FirebaseAuth _firebaseAuth = firebase.FirebaseAuth.instance; - - List _vendors = const []; - Vendor? _selectedVendor; - List<_RoleOption> _roles = const <_RoleOption>[]; - List _hubs = const []; - dc.ListTeamHubsByOwnerIdTeamHubs? _selectedHub; - - String? _shiftId; - List<_ShiftRoleKey> _originalShiftRoles = const <_ShiftRoleKey>[]; - - @override - void initState() { - super.initState(); - _dateController = TextEditingController(text: widget.order.date); - _globalLocationController = TextEditingController( - text: widget.order.locationAddress, - ); - _orderNameController = TextEditingController(); - - _positions = >[ - { - 'shiftId': null, - 'roleId': '', - 'roleName': '', - 'originalRoleId': null, - 'count': widget.order.workersNeeded, - 'start_time': widget.order.startTime, - 'end_time': widget.order.endTime, - 'lunch_break': 'NO_BREAK', - 'location': null, - }, - ]; - - _loadOrderDetails(); - } - - @override - void dispose() { - _dateController.dispose(); - _globalLocationController.dispose(); - _orderNameController.dispose(); - super.dispose(); - } - - Future _loadOrderDetails() async { - final String? businessId = - dc.ClientSessionStore.instance.session?.business?.id; - if (businessId == null || businessId.isEmpty) { - await _firebaseAuth.signOut(); - return; - } - - if (widget.order.orderId.isEmpty) { - return; - } - - try { - final QueryResult< - dc.ListShiftRolesByBusinessAndOrderData, - dc.ListShiftRolesByBusinessAndOrderVariables> result = await _dataConnect - .listShiftRolesByBusinessAndOrder( - businessId: businessId, - orderId: widget.order.orderId, - ) - .execute(); - - final List shiftRoles = - result.data.shiftRoles; - if (shiftRoles.isEmpty) { - await _loadHubsAndSelect(); - return; - } - - final dc.ListShiftRolesByBusinessAndOrderShiftRolesShift firstShift = - shiftRoles.first.shift; - final DateTime? orderDate = firstShift.order.date?.toDateTime(); - final String dateText = orderDate == null - ? widget.order.date - : DateFormat('yyyy-MM-dd').format(orderDate); - final String location = firstShift.order.teamHub.hubName; - - _dateController.text = dateText; - _globalLocationController.text = location; - _orderNameController.text = firstShift.order.eventName ?? ''; - _shiftId = shiftRoles.first.shiftId; - - final List> positions = - shiftRoles.map((dc.ListShiftRolesByBusinessAndOrderShiftRoles role) { - return { - 'shiftId': role.shiftId, - 'roleId': role.roleId, - 'roleName': role.role.name, - 'originalRoleId': role.roleId, - 'count': role.count, - 'start_time': _formatTimeForField(role.startTime), - 'end_time': _formatTimeForField(role.endTime), - 'lunch_break': _breakValueFromDuration(role.breakType), - 'location': null, - }; - }).toList(); - - if (positions.isEmpty) { - positions.add(_emptyPosition()); - } - - final List<_ShiftRoleKey> originalShiftRoles = - shiftRoles - .map( - (dc.ListShiftRolesByBusinessAndOrderShiftRoles role) => - _ShiftRoleKey(shiftId: role.shiftId, roleId: role.roleId), - ) - .toList(); - - await _loadVendorsAndSelect(firstShift.order.vendorId); - final dc.ListShiftRolesByBusinessAndOrderShiftRolesShiftOrderTeamHub - teamHub = firstShift.order.teamHub; - await _loadHubsAndSelect( - placeId: teamHub.placeId, - hubName: teamHub.hubName, - address: teamHub.address, - ); - - if (mounted) { - setState(() { - _positions = positions; - _originalShiftRoles = originalShiftRoles; - }); - } - } catch (_) { - // Keep current state on failure. - } - } - - Future _loadHubsAndSelect({ - String? placeId, - String? hubName, - String? address, - }) async { - final String? businessId = - dc.ClientSessionStore.instance.session?.business?.id; - if (businessId == null || businessId.isEmpty) { - return; - } - - try { - final QueryResult< - dc.ListTeamHubsByOwnerIdData, - dc.ListTeamHubsByOwnerIdVariables> result = await _dataConnect - .listTeamHubsByOwnerId(ownerId: businessId) - .execute(); - - final List hubs = result.data.teamHubs; - dc.ListTeamHubsByOwnerIdTeamHubs? selected; - - if (placeId != null && placeId.isNotEmpty) { - for (final dc.ListTeamHubsByOwnerIdTeamHubs hub in hubs) { - if (hub.placeId == placeId) { - selected = hub; - break; - } - } - } - - if (selected == null && hubName != null && hubName.isNotEmpty) { - for (final dc.ListTeamHubsByOwnerIdTeamHubs hub in hubs) { - if (hub.hubName == hubName) { - selected = hub; - break; - } - } - } - - if (selected == null && address != null && address.isNotEmpty) { - for (final dc.ListTeamHubsByOwnerIdTeamHubs hub in hubs) { - if (hub.address == address) { - selected = hub; - break; - } - } - } - - selected ??= hubs.isNotEmpty ? hubs.first : null; - - if (mounted) { - setState(() { - _hubs = hubs; - _selectedHub = selected; - if (selected != null) { - _globalLocationController.text = selected.address; - } - }); - } - } catch (_) { - if (mounted) { - setState(() { - _hubs = const []; - _selectedHub = null; - }); - } - } - } - - Future _loadVendorsAndSelect(String? selectedVendorId) async { - try { - final QueryResult result = - await _dataConnect.listVendors().execute(); - final List vendors = result.data.vendors - .map( - (dc.ListVendorsVendors vendor) => Vendor( - id: vendor.id, - name: vendor.companyName, - rates: const {}, - ), - ) - .toList(); - - Vendor? selectedVendor; - if (selectedVendorId != null && selectedVendorId.isNotEmpty) { - for (final Vendor vendor in vendors) { - if (vendor.id == selectedVendorId) { - selectedVendor = vendor; - break; - } - } - } - selectedVendor ??= vendors.isNotEmpty ? vendors.first : null; - - if (mounted) { - setState(() { - _vendors = vendors; - _selectedVendor = selectedVendor; - }); - } - - if (selectedVendor != null) { - await _loadRolesForVendor(selectedVendor.id); - } - } catch (_) { - if (mounted) { - setState(() { - _vendors = const []; - _selectedVendor = null; - _roles = const <_RoleOption>[]; - }); - } - } - } - - Future _loadRolesForVendor(String vendorId) async { - try { - final QueryResult - result = await _dataConnect - .listRolesByVendorId(vendorId: vendorId) - .execute(); - final List<_RoleOption> roles = result.data.roles - .map( - (dc.ListRolesByVendorIdRoles role) => _RoleOption( - id: role.id, - name: role.name, - costPerHour: role.costPerHour, - ), - ) - .toList(); - if (mounted) { - setState(() => _roles = roles); - } - } catch (_) { - if (mounted) { - setState(() => _roles = const <_RoleOption>[]); - } - } - } - - Map _emptyPosition() { - return { - 'shiftId': _shiftId, - 'roleId': '', - 'roleName': '', - 'originalRoleId': null, - 'count': 1, - 'start_time': '09:00', - 'end_time': '17:00', - 'lunch_break': 'NO_BREAK', - 'location': null, - }; - } - - String _formatTimeForField(Timestamp? value) { - if (value == null) return ''; - try { - return DateFormat('HH:mm').format(value.toDateTime().toLocal()); - } catch (_) { - return ''; - } - } - - String _breakValueFromDuration(dc.EnumValue? breakType) { - final dc.BreakDuration? value = - breakType is dc.Known ? breakType.value : null; - switch (value) { - case dc.BreakDuration.MIN_10: - return 'MIN_10'; - case dc.BreakDuration.MIN_15: - return 'MIN_15'; - case dc.BreakDuration.MIN_30: - return 'MIN_30'; - case dc.BreakDuration.MIN_45: - return 'MIN_45'; - case dc.BreakDuration.MIN_60: - return 'MIN_60'; - case dc.BreakDuration.NO_BREAK: - case null: - return 'NO_BREAK'; - } - } - - dc.BreakDuration _breakDurationFromValue(String value) { - switch (value) { - case 'MIN_10': - return dc.BreakDuration.MIN_10; - case 'MIN_15': - return dc.BreakDuration.MIN_15; - case 'MIN_30': - return dc.BreakDuration.MIN_30; - case 'MIN_45': - return dc.BreakDuration.MIN_45; - case 'MIN_60': - return dc.BreakDuration.MIN_60; - default: - return dc.BreakDuration.NO_BREAK; - } - } - - bool _isBreakPaid(String value) { - return value == 'MIN_10' || value == 'MIN_15'; - } - - _RoleOption? _roleById(String roleId) { - for (final _RoleOption role in _roles) { - if (role.id == roleId) { - return role; - } - } - return null; - } - - double _rateForRole(String roleId) { - return _roleById(roleId)?.costPerHour ?? 0; - } - - DateTime _parseDate(String value) { - try { - return DateFormat('yyyy-MM-dd').parse(value); - } catch (_) { - return DateTime.now(); - } - } - - DateTime _parseTime(DateTime date, String time) { - if (time.trim().isEmpty) { - throw Exception('Shift time is missing.'); - } - - DateTime parsed; - try { - parsed = DateFormat.Hm().parse(time); - } catch (_) { - parsed = DateFormat.jm().parse(time); - } - - return DateTime( - date.year, - date.month, - date.day, - parsed.hour, - parsed.minute, - ); - } - - Timestamp _toTimestamp(DateTime date) { - final DateTime utc = date.toUtc(); - final int millis = utc.millisecondsSinceEpoch; - final int seconds = millis ~/ 1000; - final int nanos = (millis % 1000) * 1000000; - return Timestamp(nanos, seconds); - } - - double _calculateTotalCost() { - double total = 0; - for (final Map pos in _positions) { - final String roleId = pos['roleId']?.toString() ?? ''; - if (roleId.isEmpty) { - continue; - } - final DateTime date = _parseDate(_dateController.text); - final DateTime start = _parseTime(date, pos['start_time'].toString()); - final DateTime end = _parseTime(date, pos['end_time'].toString()); - final DateTime normalizedEnd = - end.isBefore(start) ? end.add(const Duration(days: 1)) : end; - final double hours = normalizedEnd.difference(start).inMinutes / 60.0; - final double rate = _rateForRole(roleId); - final int count = pos['count'] as int; - total += rate * hours * count; - } - return total; - } - - Future _saveOrderChanges() async { - if (_shiftId == null || _shiftId!.isEmpty) { - return; - } - - final String? businessId = - dc.ClientSessionStore.instance.session?.business?.id; - if (businessId == null || businessId.isEmpty) { - await _firebaseAuth.signOut(); - return; - } - - final DateTime orderDate = _parseDate(_dateController.text); - final dc.ListTeamHubsByOwnerIdTeamHubs? selectedHub = _selectedHub; - if (selectedHub == null) { - return; - } - - int totalWorkers = 0; - double shiftCost = 0; - - final List<_ShiftRoleKey> remainingOriginal = - List<_ShiftRoleKey>.from(_originalShiftRoles); - - for (final Map pos in _positions) { - final String roleId = pos['roleId']?.toString() ?? ''; - if (roleId.isEmpty) { - continue; - } - - final String shiftId = pos['shiftId']?.toString() ?? _shiftId!; - final int count = pos['count'] as int; - final DateTime start = _parseTime(orderDate, pos['start_time'].toString()); - final DateTime end = _parseTime(orderDate, pos['end_time'].toString()); - final DateTime normalizedEnd = - end.isBefore(start) ? end.add(const Duration(days: 1)) : end; - final double hours = normalizedEnd.difference(start).inMinutes / 60.0; - final double rate = _rateForRole(roleId); - final double totalValue = rate * hours * count; - final String lunchBreak = pos['lunch_break'] as String; - - totalWorkers += count; - shiftCost += totalValue; - - final String? originalRoleId = pos['originalRoleId']?.toString(); - remainingOriginal.removeWhere( - (_ShiftRoleKey key) => - key.shiftId == shiftId && key.roleId == originalRoleId, - ); - - if (originalRoleId != null && originalRoleId.isNotEmpty) { - if (originalRoleId != roleId) { - await _dataConnect - .deleteShiftRole(shiftId: shiftId, roleId: originalRoleId) - .execute(); - await _dataConnect - .createShiftRole( - shiftId: shiftId, - roleId: roleId, - count: count, - ) - .startTime(_toTimestamp(start)) - .endTime(_toTimestamp(normalizedEnd)) - .hours(hours) - .breakType(_breakDurationFromValue(lunchBreak)) - .isBreakPaid(_isBreakPaid(lunchBreak)) - .totalValue(totalValue) - .execute(); - } else { - await _dataConnect - .updateShiftRole(shiftId: shiftId, roleId: roleId) - .count(count) - .startTime(_toTimestamp(start)) - .endTime(_toTimestamp(normalizedEnd)) - .hours(hours) - .breakType(_breakDurationFromValue(lunchBreak)) - .isBreakPaid(_isBreakPaid(lunchBreak)) - .totalValue(totalValue) - .execute(); - } - } else { - await _dataConnect - .createShiftRole( - shiftId: shiftId, - roleId: roleId, - count: count, - ) - .startTime(_toTimestamp(start)) - .endTime(_toTimestamp(normalizedEnd)) - .hours(hours) - .breakType(_breakDurationFromValue(lunchBreak)) - .isBreakPaid(_isBreakPaid(lunchBreak)) - .totalValue(totalValue) - .execute(); - } - } - - for (final _ShiftRoleKey key in remainingOriginal) { - await _dataConnect - .deleteShiftRole(shiftId: key.shiftId, roleId: key.roleId) - .execute(); - } - - final DateTime orderDateOnly = DateTime( - orderDate.year, - orderDate.month, - orderDate.day, - ); - - await _dataConnect - .updateOrder(id: widget.order.orderId, teamHubId: selectedHub.id) - .vendorId(_selectedVendor?.id) - .date(_toTimestamp(orderDateOnly)) - .eventName(_orderNameController.text) - .execute(); - - await _dataConnect - .updateShift(id: _shiftId!) - .title('shift 1 ${DateFormat('yyyy-MM-dd').format(orderDate)}') - .date(_toTimestamp(orderDateOnly)) - .location(selectedHub.hubName) - .locationAddress(selectedHub.address) - .latitude(selectedHub.latitude) - .longitude(selectedHub.longitude) - .placeId(selectedHub.placeId) - .city(selectedHub.city) - .state(selectedHub.state) - .street(selectedHub.street) - .country(selectedHub.country) - .workersNeeded(totalWorkers) - .cost(shiftCost) - .durationDays(1) - .execute(); - } - - void _addPosition() { - setState(() { - _positions.add(_emptyPosition()); - }); - } - - void _removePosition(int index) { - if (_positions.length > 1) { - setState(() => _positions.removeAt(index)); - } - } - - void _updatePosition(int index, String key, dynamic value) { - setState(() => _positions[index][key] = value); - } - - @override - Widget build(BuildContext context) { - if (_isLoading && _showReview) { - return _buildSuccessView(); - } - - return _showReview ? _buildReviewView() : _buildFormView(); - } - - Widget _buildFormView() { - return Container( - height: MediaQuery.of(context).size.height * 0.95, - decoration: const BoxDecoration( - color: UiColors.bgPrimary, - borderRadius: BorderRadius.vertical(top: Radius.circular(24)), - ), - child: Column( - children: [ - _buildHeader(), - Expanded( - child: ListView( - padding: const EdgeInsets.all(UiConstants.space5), - children: [ - Text( - 'Edit Your Order', - style: UiTypography.headline3m.textPrimary, - ), - const SizedBox(height: UiConstants.space4), - - _buildSectionHeader('VENDOR'), - Container( - padding: const EdgeInsets.symmetric( - horizontal: UiConstants.space3, - ), - height: 48, - decoration: BoxDecoration( - color: UiColors.white, - borderRadius: UiConstants.radiusMd, - border: Border.all(color: UiColors.border), - ), - child: DropdownButtonHideUnderline( - child: DropdownButton( - isExpanded: true, - value: _selectedVendor, - icon: const Icon( - UiIcons.chevronDown, - size: 18, - color: UiColors.iconSecondary, - ), - onChanged: (Vendor? vendor) { - if (vendor != null) { - setState(() => _selectedVendor = vendor); - _loadRolesForVendor(vendor.id); - } - }, - items: _vendors.map((Vendor vendor) { - return DropdownMenuItem( - value: vendor, - child: Text( - vendor.name, - style: UiTypography.body2m.textPrimary, - ), - ); - }).toList(), - ), - ), - ), - const SizedBox(height: UiConstants.space4), - - _buildSectionHeader('DATE'), - UiTextField( - controller: _dateController, - hintText: 'mm/dd/yyyy', - prefixIcon: UiIcons.calendar, - readOnly: true, - onTap: () {}, - ), - const SizedBox(height: UiConstants.space4), - - _buildSectionHeader('ORDER NAME'), - UiTextField( - controller: _orderNameController, - hintText: 'Order name', - prefixIcon: UiIcons.briefcase, - ), - const SizedBox(height: UiConstants.space4), - - _buildSectionHeader('HUB'), - Container( - padding: const EdgeInsets.symmetric( - horizontal: UiConstants.space3, - ), - height: 48, - decoration: BoxDecoration( - color: UiColors.white, - borderRadius: UiConstants.radiusMd, - border: Border.all(color: UiColors.border), - ), - child: DropdownButtonHideUnderline( - child: DropdownButton( - isExpanded: true, - value: _selectedHub, - icon: const Icon( - UiIcons.chevronDown, - size: 18, - color: UiColors.iconSecondary, - ), - onChanged: - (dc.ListTeamHubsByOwnerIdTeamHubs? hub) { - if (hub != null) { - setState(() { - _selectedHub = hub; - _globalLocationController.text = hub.address; - }); - } - }, - items: _hubs.map( - (dc.ListTeamHubsByOwnerIdTeamHubs hub) { - return DropdownMenuItem< - dc.ListTeamHubsByOwnerIdTeamHubs>( - value: hub, - child: Text( - hub.hubName, - style: UiTypography.body2m.textPrimary, - ), - ); - }, - ).toList(), - ), - ), - ), - const SizedBox(height: UiConstants.space6), - - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'POSITIONS', - style: UiTypography.headline4m.textPrimary, - ), - TextButton( - onPressed: _addPosition, - style: TextButton.styleFrom( - padding: EdgeInsets.zero, - minimumSize: Size.zero, - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - child: Row( - mainAxisSize: MainAxisSize.min, - spacing: UiConstants.space2, - children: [ - const Icon(UiIcons.add, size: 16, color: UiColors.primary), - Text( - 'Add Position', - style: UiTypography.body2m.primary, - ), - ], - ), - ), - ], - ), - const SizedBox(height: UiConstants.space3), - - ..._positions.asMap().entries.map(( - MapEntry> entry, - ) { - return _buildPositionCard(entry.key, entry.value); - }), - - const SizedBox(height: 40), - ], - ), - ), - _buildBottomAction( - label: 'Review ${_positions.length} Positions', - onPressed: () => setState(() => _showReview = true), - ), - ], - ), - ); - } - - Widget _buildHeader() { - return Container( - padding: const EdgeInsets.fromLTRB(20, 24, 20, 20), - decoration: const BoxDecoration( - color: UiColors.primary, - borderRadius: BorderRadius.vertical(top: Radius.circular(UiConstants.space6)), - ), - child: Row( - children: [ - GestureDetector( - onTap: () => Navigator.pop(context), - child: Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: UiColors.white.withValues(alpha: 0.2), - borderRadius: UiConstants.radiusMd, - ), - child: const Icon( - UiIcons.chevronLeft, - color: UiColors.white, - size: 24, - ), - ), - ), - const SizedBox(width: UiConstants.space3), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'One-Time Order', - style: UiTypography.headline3m.copyWith(color: UiColors.white), - ), - Text( - 'Refine your staffing needs', - style: UiTypography.footnote2r.copyWith( - color: UiColors.white.withValues(alpha: 0.8), - ), - ), - ], - ), - ], - ), - ); - } - - Widget _buildSectionHeader(String title) { - return Padding( - padding: const EdgeInsets.only(bottom: 8), - child: Text(title, style: UiTypography.footnote2r.textSecondary), - ); - } - - Widget _buildPositionCard(int index, Map pos) { - return Container( - margin: const EdgeInsets.only(bottom: UiConstants.space3), - padding: const EdgeInsets.all(UiConstants.space4), - decoration: BoxDecoration( - color: UiColors.white, - borderRadius: UiConstants.radiusLg, - border: Border.all(color: UiColors.border), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'POSITION #${index + 1}', - style: UiTypography.footnote1m.textSecondary, - ), - if (_positions.length > 1) - GestureDetector( - onTap: () => _removePosition(index), - child: Text( - 'Remove', - style: UiTypography.footnote1m.copyWith( - color: UiColors.destructive, - ), - ), - ), - ], - ), - const SizedBox(height: UiConstants.space3), - - _buildDropdownField( - hint: 'Select role', - value: pos['roleId'], - items: [ - ..._roles.map((_RoleOption role) => role.id), - if (pos['roleId'] != null && - pos['roleId'].toString().isNotEmpty && - !_roles.any( - (_RoleOption role) => role.id == pos['roleId'].toString(), - )) - pos['roleId'].toString(), - ], - itemBuilder: (dynamic roleId) { - final _RoleOption? role = _roleById(roleId.toString()); - if (role == null) { - final String fallback = pos['roleName']?.toString() ?? ''; - return fallback.isEmpty ? roleId.toString() : fallback; - } - return '${role.name} - \$${role.costPerHour.toStringAsFixed(0)}/hr'; - }, - onChanged: (dynamic val) { - final String roleId = val?.toString() ?? ''; - final _RoleOption? role = _roleById(roleId); - setState(() { - _positions[index]['roleId'] = roleId; - _positions[index]['roleName'] = role?.name ?? ''; - }); - }, - ), - - const SizedBox(height: UiConstants.space3), - - Row( - children: [ - Expanded( - child: _buildInlineTimeInput( - label: 'Start', - value: pos['start_time'], - onTap: () async { - final TimeOfDay? picked = await showTimePicker( - context: context, - initialTime: TimeOfDay.now(), - ); - if (picked != null && context.mounted) { - _updatePosition( - index, - 'start_time', - picked.format(context), - ); - } - }, - ), - ), - const SizedBox(width: UiConstants.space2), - Expanded( - child: _buildInlineTimeInput( - label: 'End', - value: pos['end_time'], - onTap: () async { - final TimeOfDay? picked = await showTimePicker( - context: context, - initialTime: TimeOfDay.now(), - ); - if (picked != null && context.mounted) { - _updatePosition( - index, - 'end_time', - picked.format(context), - ); - } - }, - ), - ), - const SizedBox(width: UiConstants.space2), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Workers', - style: UiTypography.footnote2r.textSecondary, - ), - const SizedBox(height: UiConstants.space1), - Container( - height: 40, - decoration: BoxDecoration( - color: UiColors.bgSecondary, - borderRadius: UiConstants.radiusSm, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - GestureDetector( - onTap: () { - if ((pos['count'] as int) > 1) { - _updatePosition( - index, - 'count', - (pos['count'] as int) - 1, - ); - } - }, - child: const Icon(UiIcons.minus, size: 12), - ), - Text( - '${pos['count']}', - style: UiTypography.body2b.textPrimary, - ), - GestureDetector( - onTap: () => _updatePosition( - index, - 'count', - (pos['count'] as int) + 1, - ), - child: const Icon(UiIcons.add, size: 12), - ), - ], - ), - ), - ], - ), - ), - ], - ), - const SizedBox(height: UiConstants.space4), - - if (pos['location'] == null) - GestureDetector( - onTap: () => _updatePosition(index, 'location', ''), - child: Row( - children: [ - const Icon(UiIcons.mapPin, size: 14, color: UiColors.primary), - const SizedBox(width: UiConstants.space1), - Text( - 'Use different location for this position', - style: UiTypography.footnote1m.copyWith( - color: UiColors.primary, - ), - ), - ], - ), - ) - else - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - const Icon( - UiIcons.mapPin, - size: 14, - color: UiColors.iconSecondary, - ), - const SizedBox(width: UiConstants.space1), - Text( - 'Different Location', - style: UiTypography.footnote1m.textSecondary, - ), - ], - ), - GestureDetector( - onTap: () => _updatePosition(index, 'location', null), - child: const Icon( - UiIcons.close, - size: 14, - color: UiColors.destructive, - ), - ), - ], - ), - const SizedBox(height: UiConstants.space2), - UiTextField( - controller: TextEditingController(text: pos['location']), - hintText: 'Enter different address', - onChanged: (String val) => - _updatePosition(index, 'location', val), - ), - ], - ), - - const SizedBox(height: UiConstants.space3), - - _buildSectionHeader('LUNCH BREAK'), - _buildDropdownField( - hint: 'No Break', - value: pos['lunch_break'], - items: [ - 'NO_BREAK', - 'MIN_10', - 'MIN_15', - 'MIN_30', - 'MIN_45', - 'MIN_60', - ], - itemBuilder: (dynamic val) { - switch (val.toString()) { - case 'MIN_10': - return '10 min (Paid)'; - case 'MIN_15': - return '15 min (Paid)'; - case 'MIN_30': - return '30 min (Unpaid)'; - case 'MIN_45': - return '45 min (Unpaid)'; - case 'MIN_60': - return '60 min (Unpaid)'; - default: - return 'No Break'; - } - }, - onChanged: (dynamic val) => - _updatePosition(index, 'lunch_break', val), - ), - ], - ), - ); - } - - Widget _buildDropdownField({ - required String hint, - required dynamic value, - required List items, - String Function(dynamic)? itemBuilder, - required ValueChanged onChanged, - }) { - return Container( - height: 44, - padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3), - decoration: BoxDecoration( - borderRadius: UiConstants.radiusMd, - border: Border.all(color: UiColors.border), - ), - child: DropdownButtonHideUnderline( - child: DropdownButton( - isExpanded: true, - hint: Text(hint, style: UiTypography.body2r.textPlaceholder), - value: value == '' || value == null ? null : value, - icon: const Icon( - UiIcons.chevronDown, - size: 18, - color: UiColors.iconSecondary, - ), - onChanged: onChanged, - items: items.toSet().map((dynamic item) { - String label = item.toString(); - if (itemBuilder != null) label = itemBuilder(item); - return DropdownMenuItem( - value: item, - child: Text(label, style: UiTypography.body2r.textPrimary), - ); - }).toList(), - ), - ), - ); - } - - Widget _buildInlineTimeInput({ - required String label, - required String value, - required VoidCallback onTap, - }) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(label, style: UiTypography.footnote2r.textSecondary), - const SizedBox(height: UiConstants.space1), - GestureDetector( - onTap: onTap, - child: Container( - height: 40, - padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3), - decoration: BoxDecoration( - borderRadius: UiConstants.radiusSm, - border: Border.all(color: UiColors.border), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - value.isEmpty ? '--:--' : value, - style: UiTypography.body2r.textPrimary, - ), - const Icon( - UiIcons.clock, - size: 14, - color: UiColors.iconSecondary, - ), - ], - ), - ), - ), - ], - ); - } - - Widget _buildBottomAction({ - required String label, - required VoidCallback onPressed, - }) { - return Container( - padding: EdgeInsets.fromLTRB( - UiConstants.space5, - UiConstants.space5, - UiConstants.space5, - 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: onPressed, - size: UiButtonSize.large, - ), - ), - ); - } - - Widget _buildReviewView() { - final int totalWorkers = _positions.fold( - 0, - (int sum, Map p) => sum + (p['count'] as int), - ); - final double totalCost = _calculateTotalCost(); - - return Container( - height: MediaQuery.of(context).size.height * 0.95, - decoration: const BoxDecoration( - color: UiColors.bgSecondary, - borderRadius: BorderRadius.vertical(top: Radius.circular(UiConstants.space6)), - ), - child: Column( - children: [ - _buildHeader(), - Expanded( - child: SingleChildScrollView( - padding: const EdgeInsets.all(20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Summary Card - Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - UiColors.primary.withValues(alpha: 0.05), - UiColors.primary.withValues(alpha: 0.1), - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: UiColors.primary.withValues(alpha: 0.2), - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - _buildSummaryItem('${_positions.length}', 'Positions'), - _buildSummaryItem('$totalWorkers', 'Workers'), - _buildSummaryItem( - '\$${totalCost.round()}', - 'Est. Cost', - ), - ], - ), - ), - const SizedBox(height: 20), - - // Order Details - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: UiColors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: UiColors.separatorPrimary), - ), - child: Column( - children: [ - Row( - children: [ - const Icon( - UiIcons.calendar, - size: 16, - color: UiColors.primary, - ), - const SizedBox(width: 8), - Text( - _dateController.text, - style: UiTypography.body2m.textPrimary, - ), - ], - ), - if (_globalLocationController - .text - .isNotEmpty) ...[ - const SizedBox(height: 12), - Row( - children: [ - const Icon( - UiIcons.mapPin, - size: 16, - color: UiColors.primary, - ), - const SizedBox(width: 8), - Expanded( - child: Text( - _globalLocationController.text, - style: UiTypography.body2r.textPrimary, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ], - ], - ), - ), - const SizedBox(height: 24), - - Text( - 'Positions Breakdown', - style: UiTypography.body2b.textPrimary, - ), - const SizedBox(height: 12), - - ..._positions.map( - (Map pos) => _buildReviewPositionCard(pos), - ), - - const SizedBox(height: 40), - ], - ), - ), - ), - - // Footer - Container( - padding: EdgeInsets.fromLTRB( - UiConstants.space5, - UiConstants.space5, - UiConstants.space5, - MediaQuery.of(context).padding.bottom + UiConstants.space5, - ), - decoration: const BoxDecoration( - color: UiColors.white, - border: Border(top: BorderSide(color: UiColors.border)), - ), - child: Row( - children: [ - Expanded( - child: UiButton.secondary( - text: 'Edit', - onPressed: () => setState(() => _showReview = false), - ), - ), - const SizedBox(width: 12), - Expanded( - child: UiButton.primary( - text: 'Confirm & Save', - onPressed: () async { - setState(() => _isLoading = true); - await _saveOrderChanges(); - if (mounted) { - widget.onUpdated?.call(); - Navigator.pop(context); - } - }, - ), - ), - ], - ), - ), - ], - ), - ); - } - - Widget _buildSummaryItem(String value, String label) { - return Column( - children: [ - Text( - value, - style: UiTypography.headline2m.copyWith( - color: UiColors.primary, - fontWeight: FontWeight.bold, - ), - ), - Text( - label.toUpperCase(), - style: UiTypography.titleUppercase4m.copyWith( - color: UiColors.textSecondary, - ), - ), - ], - ); - } - - Widget _buildReviewPositionCard(Map pos) { - final String roleId = pos['roleId']?.toString() ?? ''; - final _RoleOption? role = _roleById(roleId); - final double rate = role?.costPerHour ?? 0; - - return Container( - margin: const EdgeInsets.only(bottom: 12), - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: UiColors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: UiColors.separatorSecondary), - ), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - (role?.name ?? pos['roleName']?.toString() ?? '').isEmpty - ? 'Position' - : (role?.name ?? pos['roleName']?.toString() ?? ''), - style: UiTypography.body2b.textPrimary, - ), - Text( - '${pos['count']} worker${pos['count'] > 1 ? 's' : ''}', - style: UiTypography.footnote2r.textSecondary, - ), - ], - ), - Text( - '\$${rate.round()}/hr', - style: UiTypography.body2b.copyWith(color: UiColors.primary), - ), - ], - ), - const SizedBox(height: 12), - Row( - children: [ - const Icon( - UiIcons.clock, - size: 14, - color: UiColors.iconSecondary, - ), - const SizedBox(width: 6), - Text( - '${pos['start_time']} - ${pos['end_time']}', - style: UiTypography.footnote2r.textSecondary, - ), - ], - ), - ], - ), - ); - } - - Widget _buildSuccessView() { - return Container( - width: double.infinity, - height: MediaQuery.of(context).size.height * 0.95, - decoration: const BoxDecoration( - color: UiColors.primary, - borderRadius: BorderRadius.vertical(top: Radius.circular(UiConstants.space6)), - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - width: 80, - height: 80, - decoration: const BoxDecoration( - color: UiColors.accent, - shape: BoxShape.circle, - ), - child: const Center( - child: Icon( - UiIcons.success, - size: 40, - color: UiColors.foreground, - ), - ), - ), - const SizedBox(height: 24), - Text( - 'Order Updated!', - style: UiTypography.headline1m.copyWith(color: UiColors.white), - ), - const SizedBox(height: 12), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 40), - child: Text( - 'Your shift has been updated successfully.', - textAlign: TextAlign.center, - style: UiTypography.body1r.copyWith( - color: UiColors.white.withValues(alpha: 0.7), - ), - ), - ), - const SizedBox(height: 40), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 40), - child: UiButton.secondary( - text: 'Back to Orders', - fullWidth: true, - style: OutlinedButton.styleFrom( - backgroundColor: UiColors.white, - foregroundColor: UiColors.primary, - ), - onPressed: () => Navigator.pop(context), - ), - ), - ], - ), - ); - } -}