From 1768ed7b04eca837514eda5a35e432685ff8c3eb Mon Sep 17 00:00:00 2001 From: Marc Rejohn Castillano Date: Fri, 5 Jun 2026 07:10:38 +0800 Subject: [PATCH] Fixed custom shift type not appearing in dropdown. Fixed inconsitent time reflection on edit. --- lib/screens/workforce/workforce_screen.dart | 257 ++++++++++++++------ 1 file changed, 186 insertions(+), 71 deletions(-) diff --git a/lib/screens/workforce/workforce_screen.dart b/lib/screens/workforce/workforce_screen.dart index 5bae24cf..99324bc1 100644 --- a/lib/screens/workforce/workforce_screen.dart +++ b/lib/screens/workforce/workforce_screen.dart @@ -808,7 +808,10 @@ class _ScheduleGeneratorPanelState final isRamadan = ref.watch(isRamadanActiveProvider); + final colorScheme = Theme.of(context).colorScheme; + return Card( + clipBehavior: Clip.antiAlias, child: Padding( padding: const EdgeInsets.all(16), child: Column( @@ -827,11 +830,9 @@ class _ScheduleGeneratorPanelState Chip( label: const Text('Ramadan'), avatar: const Icon(Icons.nights_stay, size: 16), - backgroundColor: Theme.of( - context, - ).colorScheme.tertiaryContainer, + backgroundColor: colorScheme.tertiaryContainer, labelStyle: TextStyle( - color: Theme.of(context).colorScheme.onTertiaryContainer, + color: colorScheme.onTertiaryContainer, ), ), ], @@ -843,6 +844,14 @@ class _ScheduleGeneratorPanelState ), ], ), + if (_isGenerating) ...[ + const SizedBox(height: 8), + LinearProgressIndicator( + backgroundColor: colorScheme.surfaceContainerHighest, + color: colorScheme.primary, + borderRadius: BorderRadius.circular(2), + ), + ], const SizedBox(height: 12), _dateField( context, @@ -861,15 +870,19 @@ class _ScheduleGeneratorPanelState Row( children: [ Expanded( - child: FilledButton( + child: FilledButton.icon( onPressed: _isGenerating ? null : _handleGenerate, - child: _isGenerating + icon: _isGenerating ? const SizedBox( - height: 18, - width: 18, - child: CircularProgressIndicator(strokeWidth: 2), + height: 16, + width: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), ) - : const Text('Generate preview'), + : const Icon(Icons.auto_awesome, size: 18), + label: const Text('Generate preview'), ), ), const SizedBox(width: 12), @@ -891,22 +904,25 @@ class _ScheduleGeneratorPanelState const SizedBox(height: 12), Row( children: [ - OutlinedButton.icon( + FilledButton.tonalIcon( onPressed: _isSaving ? null : _addDraft, - icon: const Icon(Icons.add), + icon: const Icon(Icons.add, size: 18), label: const Text('Add shift'), ), const Spacer(), FilledButton.icon( onPressed: _isSaving ? null : _commitDraft, - icon: const Icon(Icons.check_circle), - label: _isSaving + icon: _isSaving ? const SizedBox( - height: 18, - width: 18, - child: CircularProgressIndicator(strokeWidth: 2), + height: 16, + width: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), ) - : const Text('Commit schedule'), + : const Icon(Icons.check_circle_outline, size: 18), + label: const Text('Commit schedule'), ), ], ), @@ -924,9 +940,13 @@ class _ScheduleGeneratorPanelState required VoidCallback onTap, }) { return InkWell( + borderRadius: BorderRadius.circular(4), onTap: onTap, child: InputDecorator( - decoration: InputDecoration(labelText: label), + decoration: InputDecoration( + labelText: label, + suffixIcon: const Icon(Icons.calendar_today_outlined, size: 18), + ), child: Text(value == null ? 'Select date' : AppTime.formatDate(value)), ), ); @@ -1013,17 +1033,25 @@ class _ScheduleGeneratorPanelState Widget _buildWarningPanel(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return Container( - padding: const EdgeInsets.all(12), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration( - color: colorScheme.tertiaryContainer, + color: colorScheme.errorContainer.withValues(alpha: 0.6), borderRadius: BorderRadius.circular( AppSurfaces.of(context).compactCardRadius, ), + border: Border.all( + color: colorScheme.error.withValues(alpha: 0.3), + width: 1, + ), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Icon(Icons.warning_amber, color: colorScheme.onTertiaryContainer), + Icon( + Icons.info_outline, + size: 18, + color: colorScheme.onErrorContainer, + ), const SizedBox(width: 8), Expanded( child: Column( @@ -1031,17 +1059,20 @@ class _ScheduleGeneratorPanelState children: [ Text( 'Uncovered shifts', - style: Theme.of(context).textTheme.titleSmall?.copyWith( + style: Theme.of(context).textTheme.labelMedium?.copyWith( fontWeight: FontWeight.w700, - color: colorScheme.onTertiaryContainer, + color: colorScheme.onErrorContainer, ), ), - const SizedBox(height: 6), + const SizedBox(height: 4), for (final warning in _warnings) - Text( - warning, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: colorScheme.onTertiaryContainer, + Padding( + padding: const EdgeInsets.only(top: 2), + child: Text( + warning, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: colorScheme.onErrorContainer, + ), ), ), ], @@ -1090,6 +1121,14 @@ class _ScheduleGeneratorPanelState : id, ) .toList(); + final colorScheme = Theme.of(context).colorScheme; + final shiftInitial = + _shiftLabel(draft.shiftType, rotationConfig).isNotEmpty + ? _shiftLabel( + draft.shiftType, + rotationConfig, + )[0].toUpperCase() + : '?'; return Card( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), @@ -1097,6 +1136,24 @@ class _ScheduleGeneratorPanelState children: [ Row( children: [ + Container( + width: 36, + height: 36, + margin: const EdgeInsets.only(right: 10), + decoration: BoxDecoration( + color: colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(8), + ), + alignment: Alignment.center, + child: Text( + shiftInitial, + style: TextStyle( + color: colorScheme.onPrimaryContainer, + fontWeight: FontWeight.w700, + fontSize: 14, + ), + ), + ), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -1106,9 +1163,9 @@ class _ScheduleGeneratorPanelState style: Theme.of(context).textTheme.bodyMedium ?.copyWith(fontWeight: FontWeight.w600), ), - const SizedBox(height: 4), + const SizedBox(height: 2), Text( - '${AppTime.formatDate(draft.startTime)} · ${AppTime.formatTime(draft.startTime)} - ${AppTime.formatTime(draft.endTime)}', + '${AppTime.formatDate(draft.startTime)} · ${AppTime.formatTime(draft.startTime)} – ${AppTime.formatTime(draft.endTime)}', style: Theme.of(context).textTheme.bodySmall, ), ], @@ -1117,12 +1174,16 @@ class _ScheduleGeneratorPanelState IconButton( tooltip: 'Edit', onPressed: () => _editDraft(draft), - icon: const Icon(Icons.edit), + icon: const Icon(Icons.edit_outlined, size: 18), ), IconButton( tooltip: 'Delete', onPressed: () => _deleteDraft(draft), - icon: const Icon(Icons.delete_outline), + icon: Icon( + Icons.delete_outline, + size: 18, + color: colorScheme.error, + ), ), ], ), @@ -1180,10 +1241,16 @@ class _ScheduleGeneratorPanelState return; } + final rotationConfig = ref.read(rotationConfigProvider).valueOrNull; + final shiftTypeConfigs = + rotationConfig?.shiftTypes ?? RotationConfig().shiftTypes; + final start = existing?.startTime ?? _startDate ?? AppTime.now(); var selectedDate = DateTime(start.year, start.month, start.day); var selectedUserId = existing?.userId ?? staff.first.id; - var selectedShift = existing?.shiftType ?? 'am'; + var selectedShift = + existing?.shiftType ?? + (shiftTypeConfigs.isNotEmpty ? shiftTypeConfigs.first.id : 'am'); var startTime = TimeOfDay.fromDateTime(existing?.startTime ?? start); var endTime = TimeOfDay.fromDateTime( existing?.endTime ?? start.add(const Duration(hours: 8)), @@ -1194,6 +1261,17 @@ class _ScheduleGeneratorPanelState builder: (dialogContext) { return StatefulBuilder( builder: (context, setDialogState) { + final startMinutes = startTime.hour * 60 + startTime.minute; + final endMinutes = endTime.hour * 60 + endTime.minute; + final durationMinutes = endMinutes > startMinutes + ? endMinutes - startMinutes + : (endMinutes + 1440) - startMinutes; + final durationHours = durationMinutes ~/ 60; + final durationRem = durationMinutes % 60; + final durationLabel = durationRem == 0 + ? '${durationHours}h' + : '${durationHours}h ${durationRem}m'; + return AlertDialog( title: Text(existing == null ? 'Add shift' : 'Edit shift'), content: SingleChildScrollView( @@ -1217,22 +1295,20 @@ class _ScheduleGeneratorPanelState if (value == null) return; setDialogState(() => selectedUserId = value); }, - decoration: const InputDecoration(labelText: 'Assignee'), + decoration: const InputDecoration( + labelText: 'Assignee', + filled: true, + ), ), const SizedBox(height: 12), DropdownButtonFormField( initialValue: selectedShift, - items: const [ - DropdownMenuItem(value: 'am', child: Text('AM Duty')), - DropdownMenuItem( - value: 'normal', - child: Text('Normal Duty'), - ), - DropdownMenuItem( - value: 'on_call', - child: Text('On Call'), - ), - DropdownMenuItem(value: 'pm', child: Text('PM Duty')), + items: [ + for (final s in shiftTypeConfigs) + DropdownMenuItem( + value: s.id, + child: Text(s.label), + ), ], onChanged: (value) { if (value == null) return; @@ -1240,6 +1316,7 @@ class _ScheduleGeneratorPanelState }, decoration: const InputDecoration( labelText: 'Shift type', + filled: true, ), ), const SizedBox(height: 12), @@ -1260,30 +1337,56 @@ class _ScheduleGeneratorPanelState }, ), const SizedBox(height: 12), - _dialogTimeField( - label: 'Start time', - value: startTime, - onTap: () async { - final picked = await showTimePicker( - context: context, - initialTime: startTime, - ); - if (picked == null) return; - setDialogState(() => startTime = picked); - }, + Row( + children: [ + Expanded( + child: _dialogTimeField( + label: 'Start time', + value: startTime, + onTap: () async { + final picked = await showTimePicker( + context: context, + initialTime: startTime, + ); + if (picked == null) return; + setDialogState(() => startTime = picked); + }, + ), + ), + const SizedBox(width: 8), + Expanded( + child: _dialogTimeField( + label: 'End time', + value: endTime, + onTap: () async { + final picked = await showTimePicker( + context: context, + initialTime: endTime, + ); + if (picked == null) return; + setDialogState(() => endTime = picked); + }, + ), + ), + ], ), const SizedBox(height: 12), - _dialogTimeField( - label: 'End time', - value: endTime, - onTap: () async { - final picked = await showTimePicker( - context: context, - initialTime: endTime, - ); - if (picked == null) return; - setDialogState(() => endTime = picked); - }, + Align( + alignment: Alignment.centerLeft, + child: Chip( + avatar: const Icon(Icons.schedule, size: 16), + label: Text('Duration: $durationLabel'), + visualDensity: VisualDensity.compact, + backgroundColor: Theme.of( + context, + ).colorScheme.secondaryContainer, + labelStyle: TextStyle( + color: Theme.of( + context, + ).colorScheme.onSecondaryContainer, + fontSize: 12, + ), + ), ), ], ), @@ -1295,7 +1398,7 @@ class _ScheduleGeneratorPanelState ), FilledButton( onPressed: () { - final startDateTime = DateTime( + var startDateTime = DateTime( selectedDate.year, selectedDate.month, selectedDate.day, @@ -1312,6 +1415,8 @@ class _ScheduleGeneratorPanelState if (!endDateTime.isAfter(startDateTime)) { endDateTime = endDateTime.add(const Duration(days: 1)); } + startDateTime = AppTime.toAppTime(startDateTime); + endDateTime = AppTime.toAppTime(endDateTime); final draft = existing ?? @@ -1361,9 +1466,14 @@ class _ScheduleGeneratorPanelState required VoidCallback onTap, }) { return InkWell( + borderRadius: BorderRadius.circular(4), onTap: onTap, child: InputDecorator( - decoration: InputDecoration(labelText: label), + decoration: InputDecoration( + labelText: label, + filled: true, + suffixIcon: const Icon(Icons.calendar_today_outlined, size: 18), + ), child: Text(value), ), ); @@ -1375,9 +1485,14 @@ class _ScheduleGeneratorPanelState required VoidCallback onTap, }) { return InkWell( + borderRadius: BorderRadius.circular(4), onTap: onTap, child: InputDecorator( - decoration: InputDecoration(labelText: label), + decoration: InputDecoration( + labelText: label, + filled: true, + suffixIcon: const Icon(Icons.schedule, size: 18), + ), child: Text(value.format(context)), ), );