Fixed custom shift type not appearing in dropdown.

Fixed inconsitent time reflection on edit.
This commit is contained in:
2026-06-05 07:10:38 +08:00
parent ce0be25136
commit 1768ed7b04
+163 -48
View File
@@ -808,7 +808,10 @@ class _ScheduleGeneratorPanelState
final isRamadan = ref.watch(isRamadanActiveProvider); final isRamadan = ref.watch(isRamadanActiveProvider);
final colorScheme = Theme.of(context).colorScheme;
return Card( return Card(
clipBehavior: Clip.antiAlias,
child: Padding( child: Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Column( child: Column(
@@ -827,11 +830,9 @@ class _ScheduleGeneratorPanelState
Chip( Chip(
label: const Text('Ramadan'), label: const Text('Ramadan'),
avatar: const Icon(Icons.nights_stay, size: 16), avatar: const Icon(Icons.nights_stay, size: 16),
backgroundColor: Theme.of( backgroundColor: colorScheme.tertiaryContainer,
context,
).colorScheme.tertiaryContainer,
labelStyle: TextStyle( 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), const SizedBox(height: 12),
_dateField( _dateField(
context, context,
@@ -861,15 +870,19 @@ class _ScheduleGeneratorPanelState
Row( Row(
children: [ children: [
Expanded( Expanded(
child: FilledButton( child: FilledButton.icon(
onPressed: _isGenerating ? null : _handleGenerate, onPressed: _isGenerating ? null : _handleGenerate,
child: _isGenerating icon: _isGenerating
? const SizedBox( ? const SizedBox(
height: 18, height: 16,
width: 18, width: 16,
child: CircularProgressIndicator(strokeWidth: 2), 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), const SizedBox(width: 12),
@@ -891,22 +904,25 @@ class _ScheduleGeneratorPanelState
const SizedBox(height: 12), const SizedBox(height: 12),
Row( Row(
children: [ children: [
OutlinedButton.icon( FilledButton.tonalIcon(
onPressed: _isSaving ? null : _addDraft, onPressed: _isSaving ? null : _addDraft,
icon: const Icon(Icons.add), icon: const Icon(Icons.add, size: 18),
label: const Text('Add shift'), label: const Text('Add shift'),
), ),
const Spacer(), const Spacer(),
FilledButton.icon( FilledButton.icon(
onPressed: _isSaving ? null : _commitDraft, onPressed: _isSaving ? null : _commitDraft,
icon: const Icon(Icons.check_circle), icon: _isSaving
label: _isSaving
? const SizedBox( ? const SizedBox(
height: 18, height: 16,
width: 18, width: 16,
child: CircularProgressIndicator(strokeWidth: 2), 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, required VoidCallback onTap,
}) { }) {
return InkWell( return InkWell(
borderRadius: BorderRadius.circular(4),
onTap: onTap, onTap: onTap,
child: InputDecorator( 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)), child: Text(value == null ? 'Select date' : AppTime.formatDate(value)),
), ),
); );
@@ -1013,17 +1033,25 @@ class _ScheduleGeneratorPanelState
Widget _buildWarningPanel(BuildContext context) { Widget _buildWarningPanel(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme; final colorScheme = Theme.of(context).colorScheme;
return Container( return Container(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
decoration: BoxDecoration( decoration: BoxDecoration(
color: colorScheme.tertiaryContainer, color: colorScheme.errorContainer.withValues(alpha: 0.6),
borderRadius: BorderRadius.circular( borderRadius: BorderRadius.circular(
AppSurfaces.of(context).compactCardRadius, AppSurfaces.of(context).compactCardRadius,
), ),
border: Border.all(
color: colorScheme.error.withValues(alpha: 0.3),
width: 1,
),
), ),
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Icon(Icons.warning_amber, color: colorScheme.onTertiaryContainer), Icon(
Icons.info_outline,
size: 18,
color: colorScheme.onErrorContainer,
),
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Expanded(
child: Column( child: Column(
@@ -1031,17 +1059,20 @@ class _ScheduleGeneratorPanelState
children: [ children: [
Text( Text(
'Uncovered shifts', 'Uncovered shifts',
style: Theme.of(context).textTheme.titleSmall?.copyWith( style: Theme.of(context).textTheme.labelMedium?.copyWith(
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
color: colorScheme.onTertiaryContainer, color: colorScheme.onErrorContainer,
), ),
), ),
const SizedBox(height: 6), const SizedBox(height: 4),
for (final warning in _warnings) for (final warning in _warnings)
Text( Padding(
padding: const EdgeInsets.only(top: 2),
child: Text(
warning, warning,
style: Theme.of(context).textTheme.bodySmall?.copyWith( style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: colorScheme.onTertiaryContainer, color: colorScheme.onErrorContainer,
),
), ),
), ),
], ],
@@ -1090,6 +1121,14 @@ class _ScheduleGeneratorPanelState
: id, : id,
) )
.toList(); .toList();
final colorScheme = Theme.of(context).colorScheme;
final shiftInitial =
_shiftLabel(draft.shiftType, rotationConfig).isNotEmpty
? _shiftLabel(
draft.shiftType,
rotationConfig,
)[0].toUpperCase()
: '?';
return Card( return Card(
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
@@ -1097,6 +1136,24 @@ class _ScheduleGeneratorPanelState
children: [ children: [
Row( Row(
children: [ 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( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -1106,9 +1163,9 @@ class _ScheduleGeneratorPanelState
style: Theme.of(context).textTheme.bodyMedium style: Theme.of(context).textTheme.bodyMedium
?.copyWith(fontWeight: FontWeight.w600), ?.copyWith(fontWeight: FontWeight.w600),
), ),
const SizedBox(height: 4), const SizedBox(height: 2),
Text( 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, style: Theme.of(context).textTheme.bodySmall,
), ),
], ],
@@ -1117,12 +1174,16 @@ class _ScheduleGeneratorPanelState
IconButton( IconButton(
tooltip: 'Edit', tooltip: 'Edit',
onPressed: () => _editDraft(draft), onPressed: () => _editDraft(draft),
icon: const Icon(Icons.edit), icon: const Icon(Icons.edit_outlined, size: 18),
), ),
IconButton( IconButton(
tooltip: 'Delete', tooltip: 'Delete',
onPressed: () => _deleteDraft(draft), 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; return;
} }
final rotationConfig = ref.read(rotationConfigProvider).valueOrNull;
final shiftTypeConfigs =
rotationConfig?.shiftTypes ?? RotationConfig().shiftTypes;
final start = existing?.startTime ?? _startDate ?? AppTime.now(); final start = existing?.startTime ?? _startDate ?? AppTime.now();
var selectedDate = DateTime(start.year, start.month, start.day); var selectedDate = DateTime(start.year, start.month, start.day);
var selectedUserId = existing?.userId ?? staff.first.id; 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 startTime = TimeOfDay.fromDateTime(existing?.startTime ?? start);
var endTime = TimeOfDay.fromDateTime( var endTime = TimeOfDay.fromDateTime(
existing?.endTime ?? start.add(const Duration(hours: 8)), existing?.endTime ?? start.add(const Duration(hours: 8)),
@@ -1194,6 +1261,17 @@ class _ScheduleGeneratorPanelState
builder: (dialogContext) { builder: (dialogContext) {
return StatefulBuilder( return StatefulBuilder(
builder: (context, setDialogState) { 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( return AlertDialog(
title: Text(existing == null ? 'Add shift' : 'Edit shift'), title: Text(existing == null ? 'Add shift' : 'Edit shift'),
content: SingleChildScrollView( content: SingleChildScrollView(
@@ -1217,22 +1295,20 @@ class _ScheduleGeneratorPanelState
if (value == null) return; if (value == null) return;
setDialogState(() => selectedUserId = value); setDialogState(() => selectedUserId = value);
}, },
decoration: const InputDecoration(labelText: 'Assignee'), decoration: const InputDecoration(
labelText: 'Assignee',
filled: true,
),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
DropdownButtonFormField<String>( DropdownButtonFormField<String>(
initialValue: selectedShift, initialValue: selectedShift,
items: const [ items: [
DropdownMenuItem(value: 'am', child: Text('AM Duty')), for (final s in shiftTypeConfigs)
DropdownMenuItem( DropdownMenuItem(
value: 'normal', value: s.id,
child: Text('Normal Duty'), child: Text(s.label),
), ),
DropdownMenuItem(
value: 'on_call',
child: Text('On Call'),
),
DropdownMenuItem(value: 'pm', child: Text('PM Duty')),
], ],
onChanged: (value) { onChanged: (value) {
if (value == null) return; if (value == null) return;
@@ -1240,6 +1316,7 @@ class _ScheduleGeneratorPanelState
}, },
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Shift type', labelText: 'Shift type',
filled: true,
), ),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
@@ -1260,7 +1337,10 @@ class _ScheduleGeneratorPanelState
}, },
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
_dialogTimeField( Row(
children: [
Expanded(
child: _dialogTimeField(
label: 'Start time', label: 'Start time',
value: startTime, value: startTime,
onTap: () async { onTap: () async {
@@ -1272,8 +1352,10 @@ class _ScheduleGeneratorPanelState
setDialogState(() => startTime = picked); setDialogState(() => startTime = picked);
}, },
), ),
const SizedBox(height: 12), ),
_dialogTimeField( const SizedBox(width: 8),
Expanded(
child: _dialogTimeField(
label: 'End time', label: 'End time',
value: endTime, value: endTime,
onTap: () async { onTap: () async {
@@ -1285,6 +1367,27 @@ class _ScheduleGeneratorPanelState
setDialogState(() => endTime = picked); setDialogState(() => endTime = picked);
}, },
), ),
),
],
),
const SizedBox(height: 12),
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( FilledButton(
onPressed: () { onPressed: () {
final startDateTime = DateTime( var startDateTime = DateTime(
selectedDate.year, selectedDate.year,
selectedDate.month, selectedDate.month,
selectedDate.day, selectedDate.day,
@@ -1312,6 +1415,8 @@ class _ScheduleGeneratorPanelState
if (!endDateTime.isAfter(startDateTime)) { if (!endDateTime.isAfter(startDateTime)) {
endDateTime = endDateTime.add(const Duration(days: 1)); endDateTime = endDateTime.add(const Duration(days: 1));
} }
startDateTime = AppTime.toAppTime(startDateTime);
endDateTime = AppTime.toAppTime(endDateTime);
final draft = final draft =
existing ?? existing ??
@@ -1361,9 +1466,14 @@ class _ScheduleGeneratorPanelState
required VoidCallback onTap, required VoidCallback onTap,
}) { }) {
return InkWell( return InkWell(
borderRadius: BorderRadius.circular(4),
onTap: onTap, onTap: onTap,
child: InputDecorator( child: InputDecorator(
decoration: InputDecoration(labelText: label), decoration: InputDecoration(
labelText: label,
filled: true,
suffixIcon: const Icon(Icons.calendar_today_outlined, size: 18),
),
child: Text(value), child: Text(value),
), ),
); );
@@ -1375,9 +1485,14 @@ class _ScheduleGeneratorPanelState
required VoidCallback onTap, required VoidCallback onTap,
}) { }) {
return InkWell( return InkWell(
borderRadius: BorderRadius.circular(4),
onTap: onTap, onTap: onTap,
child: InputDecorator( child: InputDecorator(
decoration: InputDecoration(labelText: label), decoration: InputDecoration(
labelText: label,
filled: true,
suffixIcon: const Icon(Icons.schedule, size: 18),
),
child: Text(value.format(context)), child: Text(value.format(context)),
), ),
); );