332 lines
10 KiB
Dart
332 lines
10 KiB
Dart
/// Rotation configuration for the duty schedule generator.
|
|
///
|
|
/// Stored in `app_settings` with key `rotation_config`. Contains:
|
|
/// - Ordered list of IT Staff IDs for general rotation
|
|
/// - Ordered list of non-Islam IT Staff IDs for Friday AM rotation
|
|
/// - Set of excluded IT Staff IDs
|
|
/// - Initial AM and PM duty assignments for the next generated schedule
|
|
/// - Shift type definitions, role → shift type mappings, weekly hour caps
|
|
/// - Holiday settings (national / custom)
|
|
class RotationConfig {
|
|
RotationConfig({
|
|
this.rotationOrder = const [],
|
|
this.fridayAmOrder = const [],
|
|
this.excludedStaffIds = const [],
|
|
this.initialAmStaffId,
|
|
this.initialPmStaffId,
|
|
List<ShiftTypeConfig>? shiftTypes,
|
|
Map<String, int>? roleWeeklyHours,
|
|
List<Holiday>? holidays,
|
|
this.syncPhilippinesHolidays = false,
|
|
this.holidaysYear,
|
|
}) : shiftTypes = shiftTypes ?? _defaultShiftTypes(),
|
|
roleWeeklyHours = roleWeeklyHours ?? {},
|
|
holidays = holidays ?? [];
|
|
|
|
/// Ordered IDs for standard AM/PM rotation.
|
|
final List<String> rotationOrder;
|
|
|
|
/// Ordered IDs for Friday AM duty (non-Islam staff only).
|
|
final List<String> fridayAmOrder;
|
|
|
|
/// Staff IDs excluded from all rotation.
|
|
final List<String> excludedStaffIds;
|
|
|
|
/// The staff member that should take AM duty on the first day of the next
|
|
/// generated schedule. `null` means "continue from last week".
|
|
final String? initialAmStaffId;
|
|
|
|
/// The staff member that should take PM duty on the first day of the next
|
|
/// generated schedule. `null` means "continue from last week".
|
|
final String? initialPmStaffId;
|
|
|
|
/// Configured shift types with their time definitions and allowed roles.
|
|
final List<ShiftTypeConfig> shiftTypes;
|
|
|
|
/// Weekly hour cap per role (e.g. { 'it_staff': 40 }).
|
|
final Map<String, int> roleWeeklyHours;
|
|
|
|
/// Holidays to render/flag in the schedule. Includes both synced and local.
|
|
final List<Holiday> holidays;
|
|
|
|
/// Whether Philippine national holiday sync is enabled.
|
|
final bool syncPhilippinesHolidays;
|
|
|
|
/// The year used for the last synced holidays. Null means none synced.
|
|
final int? holidaysYear;
|
|
|
|
factory RotationConfig.fromJson(Map<String, dynamic> json) {
|
|
return RotationConfig(
|
|
rotationOrder: _toStringList(json['rotation_order']),
|
|
fridayAmOrder: _toStringList(json['friday_am_order']),
|
|
excludedStaffIds: _toStringList(json['excluded_staff_ids']),
|
|
initialAmStaffId: json['initial_am_staff_id'] as String?,
|
|
initialPmStaffId: json['initial_pm_staff_id'] as String?,
|
|
shiftTypes: _toShiftTypes(json['shift_types']),
|
|
roleWeeklyHours: _toStringIntMap(json['role_weekly_hours']),
|
|
holidays: _toHolidays(json['holidays']),
|
|
syncPhilippinesHolidays:
|
|
json['sync_philippines_holidays'] as bool? ?? false,
|
|
holidaysYear: json['holidays_year'] as int?,
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic> toJson() => {
|
|
'rotation_order': rotationOrder,
|
|
'friday_am_order': fridayAmOrder,
|
|
'excluded_staff_ids': excludedStaffIds,
|
|
'initial_am_staff_id': initialAmStaffId,
|
|
'initial_pm_staff_id': initialPmStaffId,
|
|
'shift_types': shiftTypes.map((e) => e.toJson()).toList(),
|
|
'role_weekly_hours': roleWeeklyHours,
|
|
'holidays': holidays.map((e) => e.toJson()).toList(),
|
|
'sync_philippines_holidays': syncPhilippinesHolidays,
|
|
'holidays_year': holidaysYear,
|
|
};
|
|
|
|
RotationConfig copyWith({
|
|
List<String>? rotationOrder,
|
|
List<String>? fridayAmOrder,
|
|
List<String>? excludedStaffIds,
|
|
String? initialAmStaffId,
|
|
String? initialPmStaffId,
|
|
bool clearInitialAm = false,
|
|
bool clearInitialPm = false,
|
|
List<ShiftTypeConfig>? shiftTypes,
|
|
Map<String, int>? roleWeeklyHours,
|
|
List<Holiday>? holidays,
|
|
bool? syncPhilippinesHolidays,
|
|
int? holidaysYear,
|
|
}) {
|
|
return RotationConfig(
|
|
rotationOrder: rotationOrder ?? this.rotationOrder,
|
|
fridayAmOrder: fridayAmOrder ?? this.fridayAmOrder,
|
|
excludedStaffIds: excludedStaffIds ?? this.excludedStaffIds,
|
|
initialAmStaffId: clearInitialAm
|
|
? null
|
|
: (initialAmStaffId ?? this.initialAmStaffId),
|
|
initialPmStaffId: clearInitialPm
|
|
? null
|
|
: (initialPmStaffId ?? this.initialPmStaffId),
|
|
shiftTypes: shiftTypes ?? this.shiftTypes,
|
|
roleWeeklyHours: roleWeeklyHours ?? this.roleWeeklyHours,
|
|
holidays: holidays ?? this.holidays,
|
|
syncPhilippinesHolidays:
|
|
syncPhilippinesHolidays ?? this.syncPhilippinesHolidays,
|
|
holidaysYear: holidaysYear ?? this.holidaysYear,
|
|
);
|
|
}
|
|
|
|
static List<ShiftTypeConfig> _defaultShiftTypes() {
|
|
return const [
|
|
ShiftTypeConfig(
|
|
id: 'am',
|
|
label: 'AM Duty',
|
|
startHour: 7,
|
|
startMinute: 0,
|
|
durationMinutes: 8 * 60,
|
|
allowedRoles: ['it_staff'],
|
|
),
|
|
ShiftTypeConfig(
|
|
id: 'pm',
|
|
label: 'PM Duty',
|
|
startHour: 15,
|
|
startMinute: 0,
|
|
durationMinutes: 8 * 60,
|
|
allowedRoles: ['it_staff'],
|
|
),
|
|
ShiftTypeConfig(
|
|
id: 'on_call',
|
|
label: 'On Call',
|
|
startHour: 23,
|
|
startMinute: 0,
|
|
durationMinutes: 8 * 60,
|
|
allowedRoles: ['it_staff'],
|
|
),
|
|
ShiftTypeConfig(
|
|
id: 'on_call_saturday',
|
|
label: 'On Call (Saturday)',
|
|
startHour: 17,
|
|
startMinute: 0,
|
|
durationMinutes: 15 * 60,
|
|
allowedRoles: ['it_staff'],
|
|
),
|
|
ShiftTypeConfig(
|
|
id: 'on_call_sunday',
|
|
label: 'On Call (Sunday)',
|
|
startHour: 17,
|
|
startMinute: 0,
|
|
durationMinutes: 14 * 60,
|
|
allowedRoles: ['it_staff'],
|
|
),
|
|
ShiftTypeConfig(
|
|
id: 'normal',
|
|
label: 'Normal',
|
|
startHour: 8,
|
|
startMinute: 0,
|
|
durationMinutes: 9 * 60,
|
|
allowedRoles: ['admin', 'dispatcher', 'programmer', 'it_staff'],
|
|
),
|
|
ShiftTypeConfig(
|
|
id: 'normal_ramadan_islam',
|
|
label: 'Normal (Ramadan - Islam)',
|
|
startHour: 8,
|
|
startMinute: 0,
|
|
durationMinutes: 8 * 60,
|
|
allowedRoles: ['admin', 'dispatcher', 'programmer', 'it_staff'],
|
|
),
|
|
ShiftTypeConfig(
|
|
id: 'normal_ramadan_other',
|
|
label: 'Normal (Ramadan - Other)',
|
|
startHour: 8,
|
|
startMinute: 0,
|
|
durationMinutes: 9 * 60,
|
|
allowedRoles: ['admin', 'dispatcher', 'programmer', 'it_staff'],
|
|
),
|
|
];
|
|
}
|
|
|
|
static List<String> _toStringList(dynamic value) {
|
|
if (value is List) return value.map((e) => e.toString()).toList();
|
|
return [];
|
|
}
|
|
|
|
static List<ShiftTypeConfig> _toShiftTypes(dynamic value) {
|
|
if (value is List) {
|
|
return value
|
|
.whereType<Map<String, dynamic>>()
|
|
.map(ShiftTypeConfig.fromJson)
|
|
.toList();
|
|
}
|
|
return _defaultShiftTypes();
|
|
}
|
|
|
|
static Map<String, int> _toStringIntMap(dynamic value) {
|
|
if (value is Map) {
|
|
return value.map((key, val) {
|
|
final intVal = val is int
|
|
? val
|
|
: int.tryParse(val?.toString() ?? '') ?? 0;
|
|
return MapEntry(key.toString(), intVal);
|
|
});
|
|
}
|
|
return {};
|
|
}
|
|
|
|
static List<Holiday> _toHolidays(dynamic value) {
|
|
if (value is List) {
|
|
return value
|
|
.whereType<Map<String, dynamic>>()
|
|
.map(Holiday.fromJson)
|
|
.toList();
|
|
}
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/// A configurable shift type used by the schedule generator.
|
|
class ShiftTypeConfig {
|
|
const ShiftTypeConfig({
|
|
required this.id,
|
|
required this.label,
|
|
required this.startHour,
|
|
required this.startMinute,
|
|
required this.durationMinutes,
|
|
this.noonBreakMinutes = 60,
|
|
this.allowedRoles = const [],
|
|
});
|
|
|
|
/// Unique shift type ID (e.g. "am", "pm", "on_call").
|
|
final String id;
|
|
|
|
/// Human-readable label (e.g. "AM Duty").
|
|
final String label;
|
|
|
|
/// Hour of day (0-23) when this shift begins.
|
|
final int startHour;
|
|
|
|
/// Minute (0-59) when this shift begins.
|
|
final int startMinute;
|
|
|
|
/// Duration in minutes.
|
|
final int durationMinutes;
|
|
|
|
/// Minutes reserved for a noon/meal break during the shift.
|
|
final int noonBreakMinutes;
|
|
|
|
/// Roles that are allowed to be assigned this shift type.
|
|
final List<String> allowedRoles;
|
|
|
|
Duration get duration => Duration(minutes: durationMinutes);
|
|
|
|
Map<String, dynamic> toJson() => {
|
|
'id': id,
|
|
'label': label,
|
|
'start_hour': startHour,
|
|
'start_minute': startMinute,
|
|
'duration_minutes': durationMinutes,
|
|
'noon_break_minutes': noonBreakMinutes,
|
|
'allowed_roles': allowedRoles,
|
|
};
|
|
|
|
factory ShiftTypeConfig.fromJson(Map<String, dynamic> json) {
|
|
return ShiftTypeConfig(
|
|
id: json['id'] as String,
|
|
label: json['label'] as String? ?? json['id'] as String,
|
|
startHour: json['start_hour'] is int
|
|
? json['start_hour'] as int
|
|
: int.tryParse(json['start_hour']?.toString() ?? '') ?? 0,
|
|
startMinute: json['start_minute'] is int
|
|
? json['start_minute'] as int
|
|
: int.tryParse(json['start_minute']?.toString() ?? '') ?? 0,
|
|
durationMinutes: json['duration_minutes'] is int
|
|
? json['duration_minutes'] as int
|
|
: int.tryParse(json['duration_minutes']?.toString() ?? '') ?? 0,
|
|
noonBreakMinutes: json['noon_break_minutes'] is int
|
|
? json['noon_break_minutes'] as int
|
|
: int.tryParse(json['noon_break_minutes']?.toString() ?? '') ?? 60,
|
|
allowedRoles: json['allowed_roles'] is List
|
|
? (json['allowed_roles'] as List).map((e) => e.toString()).toList()
|
|
: [],
|
|
);
|
|
}
|
|
}
|
|
|
|
/// A holiday used to flag schedule days.
|
|
class Holiday {
|
|
Holiday({required this.date, required this.name, this.source});
|
|
|
|
/// Date of the holiday (only date portion is used).
|
|
final DateTime date;
|
|
|
|
/// Holiday name (e.g. "Independence Day").
|
|
final String name;
|
|
|
|
/// Optional source (e.g. "nager") to distinguish synced vs custom.
|
|
final String? source;
|
|
|
|
Map<String, dynamic> toJson() => {
|
|
'date': date.toIso8601String(),
|
|
'name': name,
|
|
'source': source,
|
|
};
|
|
|
|
factory Holiday.fromJson(Map<String, dynamic> json) {
|
|
final dateRaw = json['date'];
|
|
DateTime date;
|
|
if (dateRaw is String) {
|
|
date = DateTime.parse(dateRaw);
|
|
} else if (dateRaw is DateTime) {
|
|
date = dateRaw;
|
|
} else {
|
|
date = DateTime.now();
|
|
}
|
|
|
|
return Holiday(
|
|
date: DateTime(date.year, date.month, date.day),
|
|
name: json['name'] as String? ?? 'Holiday',
|
|
source: json['source'] as String?,
|
|
);
|
|
}
|
|
}
|