From ea326727f1d3a4e4eab4efad70b4aa16aaa3f42a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Salazar?= <73718835+joshrs23@users.noreply.github.com> Date: Sun, 25 Jan 2026 16:37:02 -0500 Subject: [PATCH] reoder and creation of reaoder --- .../widgets/shift_order_form_sheet.dart | 497 ++++++++++++++---- 1 file changed, 400 insertions(+), 97 deletions(-) diff --git a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/shift_order_form_sheet.dart b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/shift_order_form_sheet.dart index c05ed644..7df94dfd 100644 --- a/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/shift_order_form_sheet.dart +++ b/apps/mobile/packages/features/client/home/lib/src/presentation/widgets/shift_order_form_sheet.dart @@ -1,6 +1,27 @@ import 'package:design_system/design_system.dart'; +import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; +import 'package:krow_data_connect/krow_data_connect.dart' as dc; + +class _RoleOption { + const _RoleOption({ + required this.id, + required this.name, + required this.costPerHour, + }); + + final String id; + final String name; + final double costPerHour; +} + +class _VendorOption { + const _VendorOption({required this.id, required this.name}); + + final String id; + final String name; +} /// A bottom sheet form for creating or reordering shifts. /// @@ -34,63 +55,18 @@ class _ShiftOrderFormSheetState extends State { late List> _positions; - final List _roles = [ - 'Server', - 'Bartender', - 'Cook', - 'Busser', - 'Host', - 'Barista', - 'Dishwasher', - 'Event Staff', - 'Warehouse Worker', - 'Retail Associate', - ]; - - // Vendor options - final List> _vendors = >[ - { - 'id': 'v1', - 'name': 'Elite Staffing', - 'rates': { - 'Server': 25.0, - 'Bartender': 30.0, - 'Cook': 28.0, - 'Busser': 18.0, - 'Host': 20.0, - 'Barista': 22.0, - 'Dishwasher': 17.0, - 'Event Staff': 19.0, - }, - }, - { - 'id': 'v2', - 'name': 'Premier Workforce', - 'rates': { - 'Server': 22.0, - 'Bartender': 28.0, - 'Cook': 25.0, - 'Busser': 16.0, - 'Host': 18.0, - 'Barista': 20.0, - 'Dishwasher': 15.0, - 'Event Staff': 18.0, - }, - }, - ]; - + final dc.ExampleConnector _dataConnect = dc.ExampleConnector.instance; + List<_VendorOption> _vendors = const <_VendorOption>[]; + List<_RoleOption> _roles = const <_RoleOption>[]; String? _selectedVendorId; - final List _lunchBreakOptions = [0, 30, 45, 60]; - @override void initState() { super.initState(); - // Initialize date controller - final DateTime tomorrow = DateTime.now().add(const Duration(days: 1)); - final String initialDate = widget.initialData?['date'] ?? - tomorrow.toIso8601String().split('T')[0]; + // Initialize date controller (always today for reorder sheet) + final DateTime today = DateTime.now(); + final String initialDate = today.toIso8601String().split('T')[0]; _dateController = TextEditingController(text: initialDate); // Initialize location controller @@ -100,21 +76,27 @@ class _ShiftOrderFormSheetState extends State { '', ); - // Initialize vendor selection - _selectedVendorId = _vendors.first['id']; - // Initialize positions _positions = >[ { - 'role': widget.initialData?['title'] ?? widget.initialData?['role'] ?? '', - 'count': widget.initialData?['workersNeeded'] ?? widget.initialData?['workers_needed'] ?? 1, - 'start_time': widget.initialData?['startTime'] ?? widget.initialData?['start_time'] ?? '09:00', - 'end_time': widget.initialData?['endTime'] ?? widget.initialData?['end_time'] ?? '17:00', - 'hourly_rate': widget.initialData?['hourlyRate']?.toDouble() ?? 18.0, - 'lunch_break': 0, + 'roleId': widget.initialData?['roleId'] ?? '', + 'roleName': widget.initialData?['title'] ?? widget.initialData?['role'] ?? '', + 'count': widget.initialData?['workersNeeded'] ?? + widget.initialData?['workers_needed'] ?? + 1, + 'start_time': widget.initialData?['startTime'] ?? + widget.initialData?['start_time'] ?? + '09:00', + 'end_time': widget.initialData?['endTime'] ?? + widget.initialData?['end_time'] ?? + '17:00', + 'lunch_break': 'NO_BREAK', 'location': null, }, ]; + + _loadVendors(); + _loadOrderDetails(); } @override @@ -127,12 +109,12 @@ class _ShiftOrderFormSheetState extends State { void _addPosition() { setState(() { _positions.add({ - 'role': '', + 'roleId': '', + 'roleName': '', 'count': 1, 'start_time': '09:00', 'end_time': '17:00', - 'hourly_rate': 18.0, - 'lunch_break': 0, + 'lunch_break': 'NO_BREAK', 'location': null, }); }); @@ -162,14 +144,27 @@ class _ShiftOrderFormSheetState extends State { hours = endH - startH; if (hours < 0) hours += 24; } catch (_) {} - - final double rate = pos['hourly_rate'] ?? 18.0; + final String roleId = pos['roleId']?.toString() ?? ''; + final double rate = _rateForRole(roleId); total += hours * rate * (pos['count'] as int); } return total; } String _getShiftType() { + final String? type = widget.initialData?['type']?.toString(); + if (type != null && type.isNotEmpty) { + switch (type) { + case 'PERMANENT': + return 'Long Term'; + case 'RECURRING': + return 'Multi-Day'; + case 'RAPID': + return 'Rapid'; + case 'ONE_TIME': + return 'One-Time Order'; + } + } // Determine shift type based on initial data final dynamic initialData = widget.initialData; if (initialData != null) { @@ -183,14 +178,304 @@ class _ShiftOrderFormSheetState extends State { return 'One-Time Order'; } - void _handleSubmit() { - final Map formData = { - 'date': _dateController.text, - 'location': _globalLocationController.text, - 'positions': _positions, - 'total_cost': _calculateTotalCost(), - }; - widget.onSubmit(formData); + Future _handleSubmit() async { + await _submitNewOrder(); + } + + Future _submitNewOrder() async { + final String? businessId = dc.ClientSessionStore.instance.session?.business?.id; + if (businessId == null || businessId.isEmpty) { + return; + } + + final DateTime date = DateTime.parse(_dateController.text); + final fdc.Timestamp orderTimestamp = _toTimestamp(date); + final dc.OrderType orderType = + _orderTypeFromValue(widget.initialData?['type']?.toString()); + + final fdc.OperationResult + orderResult = await _dataConnect + .createOrder( + businessId: businessId, + orderType: orderType, + ) + .vendorId(_selectedVendorId) + .location(_globalLocationController.text) + .status(dc.OrderStatus.POSTED) + .date(orderTimestamp) + .execute(); + + final String? orderId = orderResult.data?.order_insert.id; + if (orderId == null) { + return; + } + + final int workersNeeded = _positions.fold( + 0, + (int sum, Map pos) => sum + (pos['count'] as int), + ); + final String shiftTitle = + 'Shift 1 ${DateFormat('yyyy-MM-dd').format(date)}'; + final double shiftCost = _calculateTotalCost(); + + final fdc.OperationResult + shiftResult = await _dataConnect + .createShift(title: shiftTitle, orderId: orderId) + .date(orderTimestamp) + .location(_globalLocationController.text) + .locationAddress(_globalLocationController.text) + .status(dc.ShiftStatus.PENDING) + .workersNeeded(workersNeeded) + .filled(0) + .durationDays(1) + .cost(shiftCost) + .execute(); + + final String? shiftId = shiftResult.data?.shift_insert.id; + if (shiftId == null) { + return; + } + + for (final Map pos in _positions) { + final String roleId = pos['roleId']?.toString() ?? ''; + if (roleId.isEmpty) { + continue; + } + 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 int count = pos['count'] as int; + final double rate = _rateForRole(roleId); + final double totalValue = rate * hours * count; + final String lunchBreak = pos['lunch_break'] as String; + + await _dataConnect + .createShiftRole( + shiftId: shiftId, + roleId: roleId, + count: count, + ) + .startTime(_toTimestamp(start)) + .endTime(_toTimestamp(normalizedEnd)) + .hours(hours) + .breakType(_breakDurationFromValue(lunchBreak)) + .totalValue(totalValue) + .execute(); + } + + await _dataConnect + .updateOrder(id: orderId) + .shifts(fdc.AnyValue([shiftId])) + .execute(); + + widget.onSubmit({ + 'orderId': orderId, + }); + } + + Future _loadVendors() async { + try { + final fdc.QueryResult result = + await _dataConnect.listVendors().execute(); + final List<_VendorOption> vendors = result.data.vendors + .map( + (dc.ListVendorsVendors vendor) => + _VendorOption(id: vendor.id, name: vendor.companyName), + ) + .toList(); + if (!mounted) return; + setState(() { + _vendors = vendors; + final String? current = _selectedVendorId; + if (current == null || + !vendors.any((_VendorOption v) => v.id == current)) { + _selectedVendorId = vendors.isNotEmpty ? vendors.first.id : null; + } + }); + if (_selectedVendorId != null) { + await _loadRolesForVendor(_selectedVendorId!); + } + } catch (_) { + if (!mounted) return; + setState(() { + _vendors = const <_VendorOption>[]; + _roles = const <_RoleOption>[]; + }); + } + } + + Future _loadRolesForVendor(String vendorId) async { + try { + final fdc.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) return; + setState(() => _roles = roles); + } catch (_) { + if (!mounted) return; + setState(() => _roles = const <_RoleOption>[]); + } + } + + Future _loadOrderDetails() async { + final String? orderId = widget.initialData?['orderId']?.toString(); + if (orderId == null || orderId.isEmpty) { + return; + } + + final String? businessId = dc.ClientSessionStore.instance.session?.business?.id; + if (businessId == null || businessId.isEmpty) { + return; + } + + try { + final fdc.QueryResult< + dc.ListShiftRolesByBusinessAndOrderData, + dc.ListShiftRolesByBusinessAndOrderVariables> result = await _dataConnect + .listShiftRolesByBusinessAndOrder( + businessId: businessId, + orderId: orderId, + ) + .execute(); + + final List shiftRoles = + result.data.shiftRoles; + if (shiftRoles.isEmpty) { + return; + } + + final dc.ListShiftRolesByBusinessAndOrderShiftRolesShift firstShift = + shiftRoles.first.shift; + _globalLocationController.text = firstShift.order.location ?? + firstShift.locationAddress ?? + firstShift.location ?? + _globalLocationController.text; + + final String? vendorId = firstShift.order.vendorId; + if (mounted) { + setState(() { + _selectedVendorId = vendorId; + }); + } + if (vendorId != null && vendorId.isNotEmpty) { + await _loadRolesForVendor(vendorId); + } + + final List> positions = + shiftRoles.map((dc.ListShiftRolesByBusinessAndOrderShiftRoles role) { + return { + 'roleId': role.roleId, + 'roleName': role.role.name, + 'count': role.count, + 'start_time': _formatTimeForField(role.startTime), + 'end_time': _formatTimeForField(role.endTime), + 'lunch_break': _breakValueFromDuration(role.breakType), + 'location': null, + }; + }).toList(); + + if (!mounted) return; + setState(() { + _positions = positions; + }); + } catch (_) { + // Keep defaults on failure. + } + } + + String _formatTimeForField(fdc.Timestamp? value) { + if (value == null) return ''; + try { + return DateFormat('HH:mm').format(value.toDateTime()); + } catch (_) { + return ''; + } + } + + String _breakValueFromDuration(dc.EnumValue? breakType) { + final dc.BreakDuration? value = + breakType is dc.Known ? breakType.value : null; + switch (value) { + case dc.BreakDuration.MIN_15: + return 'MIN_15'; + case dc.BreakDuration.MIN_30: + return 'MIN_30'; + case dc.BreakDuration.NO_BREAK: + case null: + return 'NO_BREAK'; + } + } + + dc.BreakDuration _breakDurationFromValue(String value) { + switch (value) { + case 'MIN_15': + return dc.BreakDuration.MIN_15; + case 'MIN_30': + return dc.BreakDuration.MIN_30; + default: + return dc.BreakDuration.NO_BREAK; + } + } + + dc.OrderType _orderTypeFromValue(String? value) { + switch (value) { + case 'PERMANENT': + return dc.OrderType.PERMANENT; + case 'RECURRING': + return dc.OrderType.RECURRING; + case 'RAPID': + return dc.OrderType.RAPID; + case 'ONE_TIME': + default: + return dc.OrderType.ONE_TIME; + } + } + + _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 _parseTime(DateTime date, String time) { + 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, + ); + } + + fdc.Timestamp _toTimestamp(DateTime date) { + final int millis = date.millisecondsSinceEpoch; + final int seconds = millis ~/ 1000; + final int nanos = (millis % 1000) * 1000000; + return fdc.Timestamp(nanos, seconds); } @override @@ -425,19 +710,18 @@ class _ShiftOrderFormSheetState extends State { color: UiColors.iconSecondary, ), style: UiTypography.body2r.textPrimary, - items: _vendors - .map( - (Map vendor) => DropdownMenuItem( - value: vendor['id'], - child: Text(vendor['name']), - ), - ) - .toList(), + items: _vendors.map((_VendorOption vendor) { + return DropdownMenuItem( + value: vendor.id, + child: Text(vendor.name), + ); + }).toList(), onChanged: (String? newValue) { if (newValue != null) { setState(() { _selectedVendorId = newValue; }); + _loadRolesForVendor(newValue); } }, ), @@ -561,19 +845,32 @@ class _ShiftOrderFormSheetState extends State { _buildDropdownField( hint: 'Select role', - value: pos['role'], - items: _roles, - itemBuilder: (dynamic role) { - final Map? vendor = _vendors.firstWhere( - (Map v) => v['id'] == _selectedVendorId, - orElse: () => _vendors.first, - ); - final Map? rates = vendor?['rates'] as Map?; - final double? rate = rates?[role]; - if (rate == null) return role.toString(); - return '$role - \$${rate.toStringAsFixed(0)}/hr'; + 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 ?? ''; + }); }, - onChanged: (dynamic val) => _updatePosition(index, 'role', val), ), const SizedBox(height: UiConstants.space3), @@ -592,10 +889,16 @@ class _ShiftOrderFormSheetState extends State { _buildDropdownField( hint: 'None', value: pos['lunch_break'], - items: _lunchBreakOptions, - itemBuilder: (dynamic minutes) { - if (minutes == 0) return 'None'; - return '$minutes min'; + items: ['NO_BREAK', 'MIN_15', 'MIN_30'], + itemBuilder: (dynamic value) { + switch (value.toString()) { + case 'MIN_15': + return '15 min'; + case 'MIN_30': + return '30 min'; + default: + return 'No Break'; + } }, onChanged: (dynamic val) => _updatePosition(index, 'lunch_break', val), ),