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 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)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user