Added configurable shift types and holiday settings
This commit is contained in:
@@ -7,6 +7,8 @@ import '../../providers/profile_provider.dart';
|
||||
import '../../providers/rotation_config_provider.dart';
|
||||
import '../../theme/app_surfaces.dart';
|
||||
import '../../theme/m3_motion.dart';
|
||||
import '../../services/holidays_service.dart';
|
||||
import '../../utils/app_time.dart';
|
||||
import '../../utils/snackbar.dart';
|
||||
|
||||
/// Opens rotation settings as a dialog (desktop/tablet) or bottom sheet
|
||||
@@ -53,7 +55,7 @@ class _RotationSettingsDialogState
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabCtrl = TabController(length: 4, vsync: this);
|
||||
_tabCtrl = TabController(length: 6, vsync: this);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -83,6 +85,8 @@ class _RotationSettingsDialogState
|
||||
Tab(text: 'Friday AM'),
|
||||
Tab(text: 'Excluded'),
|
||||
Tab(text: 'Initial Duty'),
|
||||
Tab(text: 'Shift Types'),
|
||||
Tab(text: 'Holidays'),
|
||||
],
|
||||
),
|
||||
Flexible(
|
||||
@@ -93,6 +97,8 @@ class _RotationSettingsDialogState
|
||||
_FridayAmTab(),
|
||||
_ExcludedTab(),
|
||||
_InitialDutyTab(),
|
||||
_ShiftTypesTab(),
|
||||
_HolidaySettingsTab(),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -148,7 +154,7 @@ class _RotationSettingsSheetState extends ConsumerState<_RotationSettingsSheet>
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabCtrl = TabController(length: 4, vsync: this);
|
||||
_tabCtrl = TabController(length: 6, vsync: this);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -192,6 +198,8 @@ class _RotationSettingsSheetState extends ConsumerState<_RotationSettingsSheet>
|
||||
Tab(text: 'Friday AM'),
|
||||
Tab(text: 'Excluded'),
|
||||
Tab(text: 'Initial Duty'),
|
||||
Tab(text: 'Shift Types'),
|
||||
Tab(text: 'Holidays'),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
@@ -202,6 +210,8 @@ class _RotationSettingsSheetState extends ConsumerState<_RotationSettingsSheet>
|
||||
_FridayAmTab(),
|
||||
_ExcludedTab(),
|
||||
_InitialDutyTab(),
|
||||
_ShiftTypesTab(),
|
||||
_HolidaySettingsTab(),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -795,20 +805,643 @@ class _SaveButton extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton.icon(
|
||||
onPressed: saving ? null : onSave,
|
||||
icon: saving
|
||||
? const SizedBox(
|
||||
height: 18,
|
||||
width: 18,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.save_outlined),
|
||||
label: const Text('Save'),
|
||||
),
|
||||
child: FilledButton.icon(
|
||||
onPressed: saving ? null : onSave,
|
||||
icon: saving
|
||||
? const SizedBox(
|
||||
height: 18,
|
||||
width: 18,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.save_outlined),
|
||||
label: const Text('Save'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Tab 5: Shift Types (configurable shift type definitions & role mapping)
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
class _ShiftTypesTab extends ConsumerStatefulWidget {
|
||||
const _ShiftTypesTab();
|
||||
|
||||
@override
|
||||
ConsumerState<_ShiftTypesTab> createState() => _ShiftTypesTabState();
|
||||
}
|
||||
|
||||
class _ShiftTypesTabState extends ConsumerState<_ShiftTypesTab> {
|
||||
List<ShiftTypeConfig>? _shiftTypes;
|
||||
Map<String, int> _weeklyHours = {};
|
||||
bool _saving = false;
|
||||
|
||||
static const _knownRoles = [
|
||||
'it_staff',
|
||||
'admin',
|
||||
'dispatcher',
|
||||
'programmer',
|
||||
'standard',
|
||||
];
|
||||
|
||||
void _ensureLoaded() {
|
||||
if (_shiftTypes != null) return;
|
||||
final config = ref.read(rotationConfigProvider).valueOrNull;
|
||||
|
||||
// The default rotation config uses const shift type definitions.
|
||||
// Make a mutable copy so the UI can edit allowedRoles safely.
|
||||
final baseShiftTypes = config?.shiftTypes ?? RotationConfig().shiftTypes;
|
||||
_shiftTypes = baseShiftTypes
|
||||
.map(
|
||||
(s) => ShiftTypeConfig(
|
||||
id: s.id,
|
||||
label: s.label,
|
||||
startHour: s.startHour,
|
||||
startMinute: s.startMinute,
|
||||
durationMinutes: s.durationMinutes,
|
||||
allowedRoles: List<String>.from(s.allowedRoles),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
_weeklyHours = config?.roleWeeklyHours ?? {};
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final configAsync = ref.watch(rotationConfigProvider);
|
||||
if (configAsync.isLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
_ensureLoaded();
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Weekly hours per role (hard cap). Leave blank for no limit.',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: [
|
||||
for (final role in _knownRoles)
|
||||
SizedBox(
|
||||
width: 140,
|
||||
child: TextFormField(
|
||||
initialValue: _weeklyHours[role]?.toString() ?? '',
|
||||
decoration: InputDecoration(
|
||||
labelText: role,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
onChanged: (value) {
|
||||
final hours = int.tryParse(value);
|
||||
setState(() {
|
||||
if (hours != null) {
|
||||
_weeklyHours[role] = hours;
|
||||
} else {
|
||||
_weeklyHours.remove(role);
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: _shiftTypes!.length,
|
||||
separatorBuilder: (context, index) => const Divider(),
|
||||
itemBuilder: (context, index) {
|
||||
final shift = _shiftTypes![index];
|
||||
final startTime = TimeOfDay(
|
||||
hour: shift.startHour,
|
||||
minute: shift.startMinute,
|
||||
);
|
||||
return ExpansionTile(
|
||||
key: ValueKey(shift.id),
|
||||
title: Text(shift.label),
|
||||
subtitle: Text('ID: ${shift.id}'),
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Start: ${startTime.format(context)}',
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Duration: ${shift.durationMinutes ~/ 60}h ${shift.durationMinutes % 60}m',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Allowed roles',
|
||||
style: Theme.of(context).textTheme.labelLarge,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
for (final role in _knownRoles)
|
||||
FilterChip(
|
||||
label: Text(role),
|
||||
selected: shift.allowedRoles.contains(role),
|
||||
onSelected: (selected) {
|
||||
setState(() {
|
||||
if (selected) {
|
||||
shift.allowedRoles.add(role);
|
||||
} else {
|
||||
shift.allowedRoles.remove(role);
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
OutlinedButton.icon(
|
||||
onPressed: () => _editShiftType(context, index),
|
||||
icon: const Icon(Icons.edit, size: 18),
|
||||
label: const Text('Edit'),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () => _removeShiftType(index),
|
||||
icon: const Icon(Icons.delete_outline, size: 18),
|
||||
label: const Text('Delete'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
OutlinedButton.icon(
|
||||
onPressed: _addShiftType,
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Add shift type'),
|
||||
),
|
||||
const Spacer(),
|
||||
_SaveButton(saving: _saving, onSave: () => _save(context)),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _addShiftType() async {
|
||||
final newShift = ShiftTypeConfig(
|
||||
id: 'new_shift_${DateTime.now().millisecondsSinceEpoch}',
|
||||
label: 'New Shift',
|
||||
startHour: 8,
|
||||
startMinute: 0,
|
||||
durationMinutes: 480,
|
||||
noonBreakMinutes: 60,
|
||||
allowedRoles: <String>[],
|
||||
);
|
||||
setState(() {
|
||||
_shiftTypes = [...?_shiftTypes, newShift];
|
||||
});
|
||||
await _editShiftType(context, _shiftTypes!.length - 1);
|
||||
}
|
||||
|
||||
Future<void> _removeShiftType(int index) async {
|
||||
setState(() {
|
||||
_shiftTypes = [...?_shiftTypes]..removeAt(index);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _editShiftType(BuildContext context, int index) async {
|
||||
final shift = _shiftTypes![index];
|
||||
var label = shift.label;
|
||||
var start = TimeOfDay(hour: shift.startHour, minute: shift.startMinute);
|
||||
var durationMinutes = shift.durationMinutes;
|
||||
var noonBreakMinutes = shift.noonBreakMinutes;
|
||||
|
||||
final confirmed = await m3ShowDialog<bool>(
|
||||
context: context,
|
||||
builder: (dialogContext) {
|
||||
return StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
return AlertDialog(
|
||||
shape: AppSurfaces.of(context).dialogShape,
|
||||
title: const Text('Edit Shift Type'),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextFormField(
|
||||
initialValue: label,
|
||||
decoration: const InputDecoration(labelText: 'Label'),
|
||||
onChanged: (v) => setState(() => label = v),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
InkWell(
|
||||
onTap: () async {
|
||||
final picked = await showTimePicker(
|
||||
context: context,
|
||||
initialTime: start,
|
||||
);
|
||||
if (picked != null) setState(() => start = picked);
|
||||
},
|
||||
child: InputDecorator(
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Start time',
|
||||
),
|
||||
child: Text(start.format(context)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
initialValue: (durationMinutes ~/ 60).toString(),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Hours (total)',
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
onChanged: (v) {
|
||||
final h = int.tryParse(v) ?? 0;
|
||||
setState(() {
|
||||
durationMinutes =
|
||||
h * 60 + (durationMinutes % 60);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
initialValue: (durationMinutes % 60).toString(),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Minutes',
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
onChanged: (v) {
|
||||
final m = int.tryParse(v) ?? 0;
|
||||
setState(() {
|
||||
durationMinutes =
|
||||
(durationMinutes ~/ 60) * 60 + m;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
initialValue: noonBreakMinutes.toString(),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Noon break (minutes)',
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
onChanged: (v) {
|
||||
final m = int.tryParse(v) ?? 0;
|
||||
setState(() {
|
||||
noonBreakMinutes = m;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(false),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(true),
|
||||
child: const Text('Save'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (confirmed != true) return;
|
||||
|
||||
setState(() {
|
||||
_shiftTypes = [...?_shiftTypes];
|
||||
_shiftTypes![index] = ShiftTypeConfig(
|
||||
id: shift.id,
|
||||
label: label,
|
||||
startHour: start.hour,
|
||||
startMinute: start.minute,
|
||||
durationMinutes: durationMinutes,
|
||||
noonBreakMinutes: noonBreakMinutes,
|
||||
allowedRoles: shift.allowedRoles,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _save(BuildContext context) async {
|
||||
final ctx = context;
|
||||
setState(() => _saving = true);
|
||||
try {
|
||||
await ref
|
||||
.read(rotationConfigControllerProvider)
|
||||
.updateShiftTypes(_shiftTypes!);
|
||||
await ref
|
||||
.read(rotationConfigControllerProvider)
|
||||
.updateRoleWeeklyHours(_weeklyHours);
|
||||
if (mounted) {
|
||||
// ignore: use_build_context_synchronously
|
||||
showSuccessSnackBar(ctx, 'Shift types saved.');
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
// ignore: use_build_context_synchronously
|
||||
showErrorSnackBar(ctx, 'Save failed: $e');
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _saving = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// Tab 6: Holiday settings (sync + custom holidays)
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
class _HolidaySettingsTab extends ConsumerStatefulWidget {
|
||||
const _HolidaySettingsTab();
|
||||
|
||||
@override
|
||||
ConsumerState<_HolidaySettingsTab> createState() =>
|
||||
_HolidaySettingsTabState();
|
||||
}
|
||||
|
||||
class _HolidaySettingsTabState extends ConsumerState<_HolidaySettingsTab> {
|
||||
bool _syncEnabled = false;
|
||||
int _year = DateTime.now().year;
|
||||
bool _saving = false;
|
||||
List<Holiday> _holidays = [];
|
||||
bool _loaded = false;
|
||||
|
||||
void _ensureLoaded() {
|
||||
if (_loaded) return;
|
||||
final config = ref.read(rotationConfigProvider).valueOrNull;
|
||||
_holidays = config?.holidays ?? [];
|
||||
_syncEnabled = config?.syncPhilippinesHolidays ?? false;
|
||||
_year = config?.holidaysYear ?? DateTime.now().year;
|
||||
_loaded = true;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final configAsync = ref.watch(rotationConfigProvider);
|
||||
if (configAsync.isLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
_ensureLoaded();
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Holidays are used to flag shifts that occur on special dates. '
|
||||
'You can sync Philippine national holidays or add custom dates.',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SwitchListTile(
|
||||
value: _syncEnabled,
|
||||
title: const Text('Sync Philippine holidays'),
|
||||
onChanged: (value) => setState(() {
|
||||
_syncEnabled = value;
|
||||
}),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
SizedBox(
|
||||
width: 120,
|
||||
child: TextFormField(
|
||||
initialValue: _year.toString(),
|
||||
decoration: const InputDecoration(labelText: 'Year'),
|
||||
keyboardType: TextInputType.number,
|
||||
onChanged: (value) {
|
||||
final parsed = int.tryParse(value);
|
||||
if (parsed != null) {
|
||||
setState(() => _year = parsed);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
FilledButton(
|
||||
onPressed: _syncEnabled ? _syncHolidays : null,
|
||||
child: const Text('Sync now'),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
OutlinedButton(
|
||||
onPressed: _addCustomHoliday,
|
||||
child: const Text('Add custom holiday'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: _holidays.length,
|
||||
separatorBuilder: (context, index) => const Divider(),
|
||||
itemBuilder: (context, index) {
|
||||
final holiday = _holidays[index];
|
||||
return ListTile(
|
||||
title: Text(holiday.name),
|
||||
subtitle: Text(AppTime.formatDate(holiday.date)),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
onPressed: () => _removeHoliday(index),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
_SaveButton(saving: _saving, onSave: () => _save(context)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _syncHolidays() async {
|
||||
final ctx = context;
|
||||
setState(() => _saving = true);
|
||||
try {
|
||||
final holidays = await HolidaysService.fetchPhilippinesHolidays(_year);
|
||||
setState(() {
|
||||
_holidays = holidays;
|
||||
});
|
||||
await ref
|
||||
.read(rotationConfigControllerProvider)
|
||||
.setHolidaySync(
|
||||
enabled: _syncEnabled,
|
||||
year: _year,
|
||||
holidays: _holidays,
|
||||
);
|
||||
if (mounted) {
|
||||
// ignore: use_build_context_synchronously
|
||||
showSuccessSnackBar(ctx, 'Holidays synced for $_year.');
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
// ignore: use_build_context_synchronously
|
||||
showErrorSnackBar(ctx, 'Sync failed: $e');
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _saving = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _addCustomHoliday() async {
|
||||
DateTime selectedDate = DateTime.now();
|
||||
String name = '';
|
||||
|
||||
final confirmed = await m3ShowDialog<bool>(
|
||||
context: context,
|
||||
builder: (dialogContext) {
|
||||
return StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
return AlertDialog(
|
||||
shape: AppSurfaces.of(context).dialogShape,
|
||||
title: const Text('Add custom holiday'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () async {
|
||||
final picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: selectedDate,
|
||||
firstDate: DateTime(2000),
|
||||
lastDate: DateTime(2100),
|
||||
);
|
||||
if (picked != null) {
|
||||
setState(() => selectedDate = picked);
|
||||
}
|
||||
},
|
||||
child: InputDecorator(
|
||||
decoration: const InputDecoration(labelText: 'Date'),
|
||||
child: Text(AppTime.formatDate(selectedDate)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
decoration: const InputDecoration(labelText: 'Name'),
|
||||
onChanged: (value) => setState(() => name = value),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(false),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(true),
|
||||
child: const Text('Add'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (confirmed != true || name.isEmpty) return;
|
||||
|
||||
setState(() {
|
||||
_holidays = [
|
||||
..._holidays,
|
||||
Holiday(date: selectedDate, name: name, source: 'custom'),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
void _removeHoliday(int index) {
|
||||
setState(() {
|
||||
_holidays = [..._holidays]..removeAt(index);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _save(BuildContext context) async {
|
||||
final ctx = context;
|
||||
setState(() => _saving = true);
|
||||
try {
|
||||
await ref
|
||||
.read(rotationConfigControllerProvider)
|
||||
.setHolidaySync(
|
||||
enabled: _syncEnabled,
|
||||
year: _year,
|
||||
holidays: _holidays,
|
||||
);
|
||||
if (mounted) {
|
||||
// ignore: use_build_context_synchronously
|
||||
showSuccessSnackBar(ctx, 'Holidays saved.');
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
// ignore: use_build_context_synchronously
|
||||
showErrorSnackBar(ctx, 'Save failed: $e');
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _saving = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user