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 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(
Padding(
padding: const EdgeInsets.only(top: 2),
child: Text(
warning,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: colorScheme.onTertiaryContainer,
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<String>(
initialValue: selectedShift,
items: const [
DropdownMenuItem(value: 'am', child: Text('AM Duty')),
items: [
for (final s in shiftTypeConfigs)
DropdownMenuItem(
value: 'normal',
child: Text('Normal Duty'),
value: s.id,
child: Text(s.label),
),
DropdownMenuItem(
value: 'on_call',
child: Text('On Call'),
),
DropdownMenuItem(value: 'pm', child: Text('PM Duty')),
],
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,7 +1337,10 @@ class _ScheduleGeneratorPanelState
},
),
const SizedBox(height: 12),
_dialogTimeField(
Row(
children: [
Expanded(
child: _dialogTimeField(
label: 'Start time',
value: startTime,
onTap: () async {
@@ -1272,8 +1352,10 @@ class _ScheduleGeneratorPanelState
setDialogState(() => startTime = picked);
},
),
const SizedBox(height: 12),
_dialogTimeField(
),
const SizedBox(width: 8),
Expanded(
child: _dialogTimeField(
label: 'End time',
value: endTime,
onTap: () async {
@@ -1285,6 +1367,27 @@ class _ScheduleGeneratorPanelState
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(
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)),
),
);