Fixed custom shift type not appearing in dropdown.
Fixed inconsitent time reflection on edit.
This commit is contained in:
@@ -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<String>(
|
||||
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)),
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user