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 760a09b3..c7cc5f99 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,8 +1,11 @@ -import 'package:core_localization/core_localization.dart'; import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; /// A bottom sheet form for creating or reordering shifts. +/// +/// This widget provides a comprehensive form matching the design patterns +/// used in view_order_card.dart for consistency across the app. class ShiftOrderFormSheet extends StatefulWidget { /// Initial data for the form (e.g. from a reorder action). final Map? initialData; @@ -26,285 +29,314 @@ class ShiftOrderFormSheet extends StatefulWidget { } class _ShiftOrderFormSheetState extends State { - late Map _formData; + late TextEditingController _dateController; + late TextEditingController _globalLocationController; + + late List> _positions; + final List _roles = [ 'Server', 'Bartender', - 'Busser', 'Cook', + 'Busser', + 'Host', + 'Barista', 'Dishwasher', 'Event Staff', 'Warehouse Worker', 'Retail Associate', - 'Host/Hostess', ]; + // 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, + }, + }, + ]; + + String? _selectedVendorId; + + final List _lunchBreakOptions = [0, 30, 45, 60]; + @override void initState() { super.initState(); - final defaultPosition = { - 'title': '', - 'start_time': '', - 'end_time': '', - 'workers_needed': 1, - 'hourly_rate': 18.0, - }; - final defaults = { - 'date': '', - 'location': '', - 'recurring': false, - 'duration_days': null, - 'permanent': false, - 'duration_months': null, - 'positions': [Map.from(defaultPosition)], - }; + // Initialize date controller + final DateTime tomorrow = DateTime.now().add(const Duration(days: 1)); + final String initialDate = widget.initialData?['date'] ?? + tomorrow.toIso8601String().split('T')[0]; + _dateController = TextEditingController(text: initialDate); - if (widget.initialData != null) { - final input = widget.initialData!; - final firstPosition = { - ...defaultPosition, - 'title': input['title'] ?? input['role'] ?? '', - 'start_time': input['startTime'] ?? input['start_time'] ?? '', - 'end_time': input['endTime'] ?? input['end_time'] ?? '', - 'hourly_rate': (input['hourlyRate'] ?? input['hourly_rate'] ?? 18.0) - .toDouble(), - 'workers_needed': (input['workers'] ?? input['workers_needed'] ?? 1) - .toInt(), - }; + // Initialize location controller + _globalLocationController = TextEditingController( + text: widget.initialData?['location'] ?? + widget.initialData?['locationAddress'] ?? + '', + ); - _formData = { - ...defaults, - ...input, - 'positions': [firstPosition], - }; - } else { - _formData = Map.from(defaults); - } + // Initialize vendor selection + _selectedVendorId = _vendors.first['id']; - if (_formData['date'] == null || _formData['date'] == '') { - final tomorrow = DateTime.now().add(const Duration(days: 1)); - _formData['date'] = tomorrow.toIso8601String().split('T')[0]; - } + // 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, + 'location': null, + }, + ]; } - void _updateField(String field, dynamic value) { - setState(() { - _formData[field] = value; - }); - } - - void _updatePositionField(int index, String field, dynamic value) { - setState(() { - _formData['positions'][index][field] = value; - }); + @override + void dispose() { + _dateController.dispose(); + _globalLocationController.dispose(); + super.dispose(); } void _addPosition() { setState(() { - _formData['positions'].add({ - 'title': '', - 'start_time': '', - 'end_time': '', - 'workers_needed': 1, + _positions.add({ + 'role': '', + 'count': 1, + 'start_time': '09:00', + 'end_time': '17:00', 'hourly_rate': 18.0, + 'lunch_break': 0, + 'location': null, }); }); } void _removePosition(int index) { - if (_formData['positions'].length > 1) { - setState(() { - _formData['positions'].removeAt(index); - }); + if (_positions.length > 1) { + setState(() => _positions.removeAt(index)); } } + void _updatePosition(int index, String key, dynamic value) { + setState(() => _positions[index][key] = value); + } + + double _calculateTotalCost() { + double total = 0; + for (final Map pos in _positions) { + double hours = 8.0; + try { + final List startParts = pos['start_time'].toString().split(':'); + final List endParts = pos['end_time'].toString().split(':'); + final double startH = + int.parse(startParts[0]) + int.parse(startParts[1]) / 60; + final double endH = + int.parse(endParts[0]) + int.parse(endParts[1]) / 60; + hours = endH - startH; + if (hours < 0) hours += 24; + } catch (_) {} + + final double rate = pos['hourly_rate'] ?? 18.0; + total += hours * rate * (pos['count'] as int); + } + return total; + } + String _getShiftType() { - if (_formData['permanent'] == true || - _formData['duration_months'] != null) { - return 'Long Term'; + // Determine shift type based on initial data + final dynamic initialData = widget.initialData; + if (initialData != null) { + if (initialData['permanent'] == true || initialData['duration_months'] != null) { + return 'Long Term'; + } + if (initialData['recurring'] == true || initialData['duration_days'] != null) { + return 'Multi-Day'; + } } - if (_formData['recurring'] == true || _formData['duration_days'] != null) { - return 'Multi-Day'; - } - return 'One Day'; + return 'One-Time Order'; + } + + void _handleSubmit() { + final Map formData = { + 'date': _dateController.text, + 'location': _globalLocationController.text, + 'positions': _positions, + 'total_cost': _calculateTotalCost(), + }; + widget.onSubmit(formData); } @override Widget build(BuildContext context) { - final i18n = t.client_home.form; - return Container( - height: MediaQuery.of(context).size.height * 0.9, + height: MediaQuery.of(context).size.height * 0.95, decoration: const BoxDecoration( - color: UiColors.white, - borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + color: UiColors.bgPrimary, + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), child: Column( - children: [ - Container( - padding: const EdgeInsets.all(UiConstants.space5), - decoration: BoxDecoration( - border: Border(bottom: BorderSide(color: UiColors.border)), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ + children: [ + _buildHeader(), + Expanded( + child: ListView( + padding: const EdgeInsets.all(UiConstants.space5), + children: [ + Text( + widget.initialData != null ? 'Edit Your Order' : 'Create New Order', + style: UiTypography.headline3m.textPrimary, + ), + const SizedBox(height: UiConstants.space2), Text( widget.initialData != null - ? i18n.edit_reorder - : i18n.post_new, - style: UiTypography.headline3m.copyWith( - fontWeight: FontWeight.bold, - ), + ? 'Review and adjust the details below' + : 'Fill in the details for your staffing needs', + style: UiTypography.body2r.textSecondary, ), - IconButton( - icon: const Icon( - UiIcons.close, - color: UiColors.iconSecondary, - ), - onPressed: () => Navigator.pop(context), - ), - ], - ), - ), - if (widget.initialData != null) - Padding( - padding: const EdgeInsets.only( - left: UiConstants.space5, - right: UiConstants.space5, - bottom: UiConstants.space5, - ), - child: Text( - i18n.review_subtitle, - style: UiTypography.body2r.textSecondary, - ), - ), - Expanded( - child: SingleChildScrollView( - padding: const EdgeInsets.symmetric( - horizontal: UiConstants.space5, - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Shift Type Badge - Container( - margin: const EdgeInsets.only(bottom: UiConstants.space5), - padding: const EdgeInsets.symmetric( - horizontal: UiConstants.space3, - vertical: UiConstants.space2, - ), - decoration: BoxDecoration( - color: const Color(0xFFEFF6FF), - borderRadius: UiConstants.radiusFull, - border: Border.all(color: const Color(0xFFBFDBFE)), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: 8, - height: 8, - decoration: const BoxDecoration( - shape: BoxShape.circle, - color: Color(0xFF3B82F6), - ), - ), - const SizedBox(width: UiConstants.space2), - Text( - _getShiftType(), - style: UiTypography.footnote1b.copyWith( - color: const Color(0xFF1D4ED8), - ), - ), - ], - ), - ), + const SizedBox(height: UiConstants.space5), - _buildLabel(i18n.date_label), - UiTextField( - hintText: i18n.date_hint, - controller: TextEditingController(text: _formData['date']), - readOnly: true, - onTap: () async { - final selectedDate = await showDatePicker( - context: context, - initialDate: - _formData['date'] != null && - _formData['date'].isNotEmpty - ? DateTime.parse(_formData['date']) - : DateTime.now(), - firstDate: DateTime.now(), - lastDate: DateTime.now().add( - const Duration(days: 365 * 5), - ), - ); - if (selectedDate != null) { - _updateField( - 'date', - selectedDate.toIso8601String().split('T')[0], - ); - } - }, + // Shift Type Badge + Container( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space3, + vertical: UiConstants.space2, ), - const SizedBox(height: UiConstants.space5), - - _buildLabel(i18n.location_label), - UiTextField( - hintText: i18n.location_hint, - controller: TextEditingController( - text: _formData['location'], + decoration: BoxDecoration( + color: UiColors.primary.withValues(alpha: 0.1), + borderRadius: UiConstants.radiusFull, + border: Border.all( + color: UiColors.primary.withValues(alpha: 0.3), ), - onChanged: (value) => _updateField('location', value), ), - const SizedBox(height: UiConstants.space5), - - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(i18n.positions_title, style: UiTypography.body1b), - UiButton.text( - onPressed: _addPosition, - text: i18n.add_position, - leadingIcon: UiIcons.add, - size: UiButtonSize.small, - style: TextButton.styleFrom( - minimumSize: const Size(0, 48), - maximumSize: const Size(double.infinity, 48), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 8, + height: 8, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: UiColors.primary, + ), + ), + const SizedBox(width: UiConstants.space2), + Text( + _getShiftType(), + style: UiTypography.footnote1b.copyWith( + color: UiColors.primary, ), ), ], ), - const SizedBox(height: UiConstants.space4), + ), + const SizedBox(height: UiConstants.space5), - ...(_formData['positions'] as List).asMap().entries.map(( - entry, - ) { - final index = entry.key; - final position = entry.value; - return _PositionCard( - index: index, - position: position, - showDelete: _formData['positions'].length > 1, - onDelete: () => _removePosition(index), - roles: _roles, - onUpdate: (field, value) => - _updatePositionField(index, field, value), - labels: i18n, - ); - }), - const SizedBox(height: UiConstants.space10), - ], - ), - ), - ), - Padding( - padding: const EdgeInsets.all(UiConstants.space5), - child: UiButton.primary( - text: i18n.post_shift, - onPressed: () => widget.onSubmit(_formData), + _buildSectionHeader('VENDOR'), + _buildVendorDropdown(), + const SizedBox(height: UiConstants.space4), + + _buildSectionHeader('DATE'), + _buildDateField(), + const SizedBox(height: UiConstants.space4), + + _buildSectionHeader('LOCATION'), + _buildLocationField(), + const SizedBox(height: UiConstants.space5), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'POSITIONS', + style: UiTypography.footnote2r.textSecondary, + ), + GestureDetector( + onTap: _addPosition, + child: Row( + children: [ + const Icon( + UiIcons.add, + size: 16, + color: UiColors.primary, + ), + const SizedBox(width: UiConstants.space1), + Text( + 'Add Position', + style: UiTypography.footnote1m.copyWith( + color: UiColors.primary, + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: UiConstants.space3), + + ..._positions.asMap().entries.map((MapEntry> entry) { + return _buildPositionCard(entry.key, entry.value); + }), + + const SizedBox(height: UiConstants.space5), + + // Total Cost Display + Container( + padding: const EdgeInsets.all(UiConstants.space4), + decoration: BoxDecoration( + color: UiColors.bgSecondary, + borderRadius: UiConstants.radiusLg, + border: Border.all(color: UiColors.border), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Estimated Total', + style: UiTypography.body1b.textPrimary, + ), + Text( + '\$${_calculateTotalCost().toStringAsFixed(2)}', + style: UiTypography.headline3m.copyWith( + color: UiColors.primary, + ), + ), + ], + ), + ), + const SizedBox(height: UiConstants.space5), + + UiButton.primary( + text: widget.initialData != null ? 'Update Order' : 'Post Order', + onPressed: widget.isLoading ? null : _handleSubmit, + ), + SizedBox(height: MediaQuery.of(context).padding.bottom + UiConstants.space5), + ], ), ), ], @@ -312,127 +344,502 @@ class _ShiftOrderFormSheetState extends State { ); } - Widget _buildLabel(String text) { - return Padding( - padding: const EdgeInsets.only(bottom: UiConstants.space2), - child: Text(text, style: UiTypography.body2b), - ); - } -} - -class _PositionCard extends StatelessWidget { - final int index; - final Map position; - final bool showDelete; - final VoidCallback onDelete; - final List roles; - final Function(String field, dynamic value) onUpdate; - final dynamic labels; - - const _PositionCard({ - required this.index, - required this.position, - required this.showDelete, - required this.onDelete, - required this.roles, - required this.onUpdate, - required this.labels, - }); - - @override - Widget build(BuildContext context) { - return Card( - elevation: 0, - margin: const EdgeInsets.only(bottom: UiConstants.space5), - shape: RoundedRectangleBorder(borderRadius: UiConstants.radiusLg), - color: const Color(0xFFF8FAFC), - child: Padding( - padding: const EdgeInsets.all(UiConstants.space4), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Container( - width: 32, - height: 32, - decoration: const BoxDecoration( - shape: BoxShape.circle, - color: UiColors.primary, - ), - child: Center( - child: Text( - '${index + 1}', - style: UiTypography.footnote1b.copyWith( - color: UiColors.white, - ), - ), - ), - ), - const SizedBox(width: UiConstants.space2), - Text('Position ${index + 1}', style: UiTypography.body2b), - ], - ), - if (showDelete) - IconButton( - icon: const Icon( - UiIcons.close, - size: 18, - color: UiColors.iconError, - ), - onPressed: onDelete, - ), - ], - ), - const SizedBox(height: UiConstants.space4), - - // Simplified for brevity in prototype-to-feature move - DropdownButtonFormField( - initialValue: position['title'].isEmpty - ? null - : position['title'], - hint: Text(labels.role_hint), - items: roles - .map( - (role) => DropdownMenuItem(value: role, child: Text(role)), - ) - .toList(), - onChanged: (value) => onUpdate('title', value), - decoration: InputDecoration( - labelText: labels.role_label, - border: const OutlineInputBorder(), + Widget _buildHeader() { + return Container( + padding: const EdgeInsets.all(UiConstants.space5), + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + UiColors.primary, + Color(0xFF1E3A8A), + ], + ), + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + ), + 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( + _getShiftType(), + style: UiTypography.headline3m.copyWith(color: UiColors.white), + ), + Text( + 'Configure your staffing needs', + style: UiTypography.footnote2r.copyWith( + color: UiColors.white.withValues(alpha: 0.8), + ), + ), + ], + ), + ], + ), + ); + } - const SizedBox(height: UiConstants.space4), - Row( - children: [ - Expanded( - child: UiTextField( - label: labels.start_time, - controller: TextEditingController( - text: position['start_time'], - ), - onChanged: (v) => onUpdate('start_time', v), - ), + Widget _buildSectionHeader(String title) { + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Text(title, style: UiTypography.footnote2r.textSecondary), + ); + } + + Widget _buildVendorDropdown() { + return 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: _selectedVendorId, + icon: const Icon( + UiIcons.chevronDown, + size: 18, + color: UiColors.iconSecondary, + ), + style: UiTypography.body2r.textPrimary, + items: _vendors + .map( + (vendor) => DropdownMenuItem( + value: vendor['id'], + child: Text(vendor['name']), ), - const SizedBox(width: UiConstants.space2), - Expanded( - child: UiTextField( - label: labels.end_time, - controller: TextEditingController( - text: position['end_time'], - ), - onChanged: (v) => onUpdate('end_time', v), - ), - ), - ], + ) + .toList(), + onChanged: (String? newValue) { + if (newValue != null) { + setState(() { + _selectedVendorId = newValue; + }); + } + }, + ), + ), + ); + } + + Widget _buildDateField() { + return GestureDetector( + onTap: () async { + final DateTime? selectedDate = await showDatePicker( + context: context, + initialDate: _dateController.text.isNotEmpty + ? DateTime.parse(_dateController.text) + : DateTime.now().add(const Duration(days: 1)), + firstDate: DateTime.now(), + lastDate: DateTime.now().add(const Duration(days: 365 * 2)), + ); + if (selectedDate != null) { + setState(() { + _dateController.text = + selectedDate.toIso8601String().split('T')[0]; + }); + } + }, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space3, + vertical: UiConstants.space3, + ), + decoration: BoxDecoration( + color: UiColors.white, + borderRadius: UiConstants.radiusMd, + border: Border.all(color: UiColors.border), + ), + child: Row( + children: [ + const Icon(UiIcons.calendar, size: 20, color: UiColors.iconSecondary), + const SizedBox(width: UiConstants.space2), + Expanded( + child: Text( + _dateController.text.isNotEmpty + ? DateFormat('EEEE, MMM d, y') + .format(DateTime.parse(_dateController.text)) + : 'Select date', + style: _dateController.text.isNotEmpty + ? UiTypography.body2r.textPrimary + : UiTypography.body2r.textSecondary, + ), + ), + const Icon( + UiIcons.chevronDown, + size: 18, + color: UiColors.iconSecondary, ), ], ), ), ); } + + Widget _buildLocationField() { + return Container( + padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3), + decoration: BoxDecoration( + color: UiColors.white, + borderRadius: UiConstants.radiusMd, + border: Border.all(color: UiColors.border), + ), + child: Row( + children: [ + const Icon(UiIcons.mapPin, size: 20, color: UiColors.iconSecondary), + const SizedBox(width: UiConstants.space2), + Expanded( + child: TextField( + controller: _globalLocationController, + decoration: const InputDecoration( + hintText: 'Enter location address', + border: InputBorder.none, + ), + style: UiTypography.body2r.textPrimary, + ), + ), + ], + ), + ); + } + + 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['role'], + items: _roles, + itemBuilder: (dynamic role) { + final Map? vendor = _vendors.firstWhere( + (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'; + }, + onChanged: (dynamic val) => _updatePosition(index, 'role', val), + ), + + const SizedBox(height: UiConstants.space3), + + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Lunch Break', + style: UiTypography.footnote2r.textSecondary, + ), + const SizedBox(height: UiConstants.space1), + _buildDropdownField( + hint: 'None', + value: pos['lunch_break'], + items: _lunchBreakOptions, + itemBuilder: (dynamic minutes) { + if (minutes == 0) return 'None'; + return '$minutes min'; + }, + onChanged: (dynamic val) => _updatePosition(index, 'lunch_break', val), + ), + ], + ), + ), + ], + ), + + const SizedBox(height: UiConstants.space3), + + Row( + children: [ + Expanded( + child: _buildInlineTimeInput( + label: 'Start', + value: pos['start_time'], + onTap: () async { + final TimeOfDay? time = await showTimePicker( + context: context, + initialTime: TimeOfDay.now(), + ); + if (time != null) { + _updatePosition( + index, + 'start_time', + '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}', + ); + } + }, + ), + ), + const SizedBox(width: UiConstants.space2), + Expanded( + child: _buildInlineTimeInput( + label: 'End', + value: pos['end_time'], + onTap: () async { + final TimeOfDay? time = await showTimePicker( + context: context, + initialTime: TimeOfDay.now(), + ); + if (time != null) { + _updatePosition( + index, + 'end_time', + '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}', + ); + } + }, + ), + ), + 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: [ + Text( + 'Custom Location', + style: UiTypography.footnote2r.textSecondary, + ), + GestureDetector( + onTap: () => _updatePosition(index, 'location', null), + child: Text( + 'Remove', + style: UiTypography.footnote1m.copyWith( + color: UiColors.destructive, + ), + ), + ), + ], + ), + const SizedBox(height: UiConstants.space2), + Container( + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space3, + vertical: UiConstants.space2, + ), + decoration: BoxDecoration( + color: UiColors.bgSecondary, + borderRadius: UiConstants.radiusSm, + border: Border.all(color: UiColors.border), + ), + child: TextField( + controller: TextEditingController(text: pos['location']), + decoration: const InputDecoration( + hintText: 'Enter custom location', + border: InputBorder.none, + isDense: true, + contentPadding: EdgeInsets.zero, + ), + style: UiTypography.body2r.textPrimary, + onChanged: (String value) => + _updatePosition(index, 'location', value), + ), + ), + ], + ), + ], + ), + ); + } + + Widget _buildDropdownField({ + required String hint, + required dynamic value, + required List items, + required String Function(T) itemBuilder, + required void Function(T?) onChanged, + }) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: UiConstants.space3), + height: 48, + decoration: BoxDecoration( + color: UiColors.bgSecondary, + borderRadius: UiConstants.radiusMd, + border: Border.all(color: UiColors.border), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + isExpanded: true, + value: value.toString().isEmpty ? null : value as T?, + hint: Text(hint, style: UiTypography.body2r.textSecondary), + icon: const Icon(UiIcons.chevronDown, size: 18), + style: UiTypography.body2r.textPrimary, + items: items + .map( + (T item) => DropdownMenuItem( + value: item, + child: Text(itemBuilder(item)), + ), + ) + .toList(), + onChanged: onChanged, + ), + ), + ); + } + + 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.space2), + decoration: BoxDecoration( + color: UiColors.bgSecondary, + borderRadius: UiConstants.radiusSm, + border: Border.all(color: UiColors.border), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(UiIcons.clock, size: 14, color: UiColors.iconSecondary), + const SizedBox(width: UiConstants.space1), + Text( + value.isEmpty ? '--:--' : value, + style: UiTypography.body2r.textPrimary, + ), + ], + ), + ), + ), + ], + ); + } }