Workforce rotation settings and location permission handling
This commit is contained in:
@@ -6,15 +6,18 @@ import 'package:timezone/timezone.dart' as tz;
|
||||
|
||||
import '../../models/duty_schedule.dart';
|
||||
import '../../models/profile.dart';
|
||||
import '../../models/rotation_config.dart';
|
||||
import '../../models/swap_request.dart';
|
||||
|
||||
import '../../providers/profile_provider.dart';
|
||||
import '../../providers/rotation_config_provider.dart';
|
||||
import '../../providers/workforce_provider.dart';
|
||||
import '../../providers/chat_provider.dart';
|
||||
import '../../providers/ramadan_provider.dart';
|
||||
import '../../widgets/responsive_body.dart';
|
||||
import '../../theme/app_surfaces.dart';
|
||||
import '../../utils/snackbar.dart';
|
||||
import 'rotation_settings_dialog.dart';
|
||||
|
||||
class WorkforceScreen extends ConsumerWidget {
|
||||
const WorkforceScreen({super.key});
|
||||
@@ -879,6 +882,12 @@ class _ScheduleGeneratorPanelState
|
||||
),
|
||||
),
|
||||
],
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
tooltip: 'Rotation settings',
|
||||
icon: const Icon(Icons.settings_outlined),
|
||||
onPressed: () => showRotationSettings(context, ref),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
@@ -1009,6 +1018,7 @@ class _ScheduleGeneratorPanelState
|
||||
return;
|
||||
}
|
||||
|
||||
final rotationConfig = ref.read(rotationConfigProvider).valueOrNull;
|
||||
final schedules = ref.read(dutySchedulesProvider).valueOrNull ?? [];
|
||||
final templates = _buildTemplates(schedules);
|
||||
final generated = _generateDrafts(
|
||||
@@ -1017,6 +1027,7 @@ class _ScheduleGeneratorPanelState
|
||||
staff,
|
||||
schedules,
|
||||
templates,
|
||||
rotationConfig: rotationConfig,
|
||||
);
|
||||
generated.sort((a, b) => a.startTime.compareTo(b.startTime));
|
||||
final warnings = _buildWarnings(start, end, generated);
|
||||
@@ -1544,18 +1555,54 @@ class _ScheduleGeneratorPanelState
|
||||
DateTime end,
|
||||
List<Profile> staff,
|
||||
List<DutySchedule> schedules,
|
||||
Map<String, _ShiftTemplate> templates,
|
||||
) {
|
||||
Map<String, _ShiftTemplate> templates, {
|
||||
RotationConfig? rotationConfig,
|
||||
}) {
|
||||
final draft = <_DraftSchedule>[];
|
||||
final normalizedStart = DateTime(start.year, start.month, start.day);
|
||||
final normalizedEnd = DateTime(end.year, end.month, end.day);
|
||||
final existing = schedules;
|
||||
|
||||
// Only IT Staff rotate through AM/PM/on_call shifts
|
||||
final itStaff = staff.where((p) => p.role == 'it_staff').toList();
|
||||
final excludedIds = rotationConfig?.excludedStaffIds ?? [];
|
||||
|
||||
// Only IT Staff rotate through AM/PM/on_call shifts (minus excluded)
|
||||
final allItStaff = staff.where((p) => p.role == 'it_staff').toList();
|
||||
final itStaff = allItStaff
|
||||
.where((p) => !excludedIds.contains(p.id))
|
||||
.toList();
|
||||
|
||||
// Sort IT staff by configured rotation order if available
|
||||
if (rotationConfig != null && rotationConfig.rotationOrder.isNotEmpty) {
|
||||
final order = rotationConfig.rotationOrder;
|
||||
itStaff.sort((a, b) {
|
||||
final ai = order.indexOf(a.id);
|
||||
final bi = order.indexOf(b.id);
|
||||
final aIdx = ai == -1 ? order.length : ai;
|
||||
final bIdx = bi == -1 ? order.length : bi;
|
||||
return aIdx.compareTo(bIdx);
|
||||
});
|
||||
}
|
||||
|
||||
// Non-Islam IT staff for Friday AM rotation
|
||||
final fridayAmStaff = itStaff.where((p) => p.religion != 'islam').toList();
|
||||
if (rotationConfig != null && rotationConfig.fridayAmOrder.isNotEmpty) {
|
||||
final order = rotationConfig.fridayAmOrder;
|
||||
fridayAmStaff.sort((a, b) {
|
||||
final ai = order.indexOf(a.id);
|
||||
final bi = order.indexOf(b.id);
|
||||
final aIdx = ai == -1 ? order.length : ai;
|
||||
final bIdx = bi == -1 ? order.length : bi;
|
||||
return aIdx.compareTo(bIdx);
|
||||
});
|
||||
}
|
||||
|
||||
// Admin/Dispatcher always get normal shift (no rotation)
|
||||
final nonRotating = staff.where((p) => p.role != 'it_staff').toList();
|
||||
|
||||
// Track Friday AM rotation index separately
|
||||
var fridayAmRotationIdx = 0;
|
||||
var isFirstWeek = true;
|
||||
|
||||
var weekStart = _startOfWeek(normalizedStart);
|
||||
|
||||
while (!weekStart.isAfter(normalizedEnd)) {
|
||||
@@ -1592,18 +1639,45 @@ class _ScheduleGeneratorPanelState
|
||||
];
|
||||
|
||||
// Rotation indices only for IT Staff
|
||||
final amBaseIndex = _nextIndexFromLastWeek(
|
||||
shiftType: 'am',
|
||||
staff: itStaff,
|
||||
lastWeek: lastWeek,
|
||||
defaultIndex: 0,
|
||||
);
|
||||
final pmBaseIndex = _nextIndexFromLastWeek(
|
||||
shiftType: 'pm',
|
||||
staff: itStaff,
|
||||
lastWeek: lastWeek,
|
||||
defaultIndex: itStaff.length > 1 ? 1 : 0,
|
||||
);
|
||||
int amBaseIndex;
|
||||
int pmBaseIndex;
|
||||
|
||||
if (isFirstWeek && rotationConfig?.initialAmStaffId != null) {
|
||||
// Use configured initial AM
|
||||
final idx = itStaff.indexWhere(
|
||||
(p) => p.id == rotationConfig!.initialAmStaffId,
|
||||
);
|
||||
amBaseIndex = idx != -1 ? idx : 0;
|
||||
} else {
|
||||
amBaseIndex = _nextIndexFromLastWeek(
|
||||
shiftType: 'am',
|
||||
staff: itStaff,
|
||||
lastWeek: lastWeek,
|
||||
defaultIndex: 0,
|
||||
);
|
||||
}
|
||||
|
||||
if (isFirstWeek && rotationConfig?.initialPmStaffId != null) {
|
||||
// Use configured initial PM
|
||||
final idx = itStaff.indexWhere(
|
||||
(p) => p.id == rotationConfig!.initialPmStaffId,
|
||||
);
|
||||
pmBaseIndex = idx != -1 ? idx : (itStaff.length > 1 ? 2 : 0);
|
||||
} else {
|
||||
// PM duty is 2 positions after AM in the rotation to avoid conflicts
|
||||
final defaultPmIndex = itStaff.length > 2
|
||||
? (amBaseIndex + 2) % itStaff.length
|
||||
: (itStaff.length > 1 ? (amBaseIndex + 1) % itStaff.length : 0);
|
||||
pmBaseIndex = _nextIndexFromLastWeek(
|
||||
shiftType: 'pm',
|
||||
staff: itStaff,
|
||||
lastWeek: lastWeek,
|
||||
defaultIndex: defaultPmIndex,
|
||||
);
|
||||
}
|
||||
|
||||
isFirstWeek = false;
|
||||
|
||||
final amUserId = itStaff.isEmpty
|
||||
? null
|
||||
: itStaff[amBaseIndex % itStaff.length].id;
|
||||
@@ -1664,16 +1738,31 @@ class _ScheduleGeneratorPanelState
|
||||
final isFriday = day.weekday == DateTime.friday;
|
||||
final profileMap = _profileById();
|
||||
final amProfile = amUserId != null ? profileMap[amUserId] : null;
|
||||
final skipAmForRamadan =
|
||||
dayIsRamadan && isFriday && amProfile?.religion == 'islam';
|
||||
|
||||
if (amUserId != null && !skipAmForRamadan) {
|
||||
// Friday AM: use separate non-Islam rotation
|
||||
String? effectiveAmUserId = amUserId;
|
||||
if (isFriday && (dayIsRamadan || true)) {
|
||||
// On Fridays, only non-Muslim IT Staff can take AM
|
||||
if (amProfile?.religion == 'islam') {
|
||||
// Use Friday AM rotation list
|
||||
if (fridayAmStaff.isNotEmpty) {
|
||||
effectiveAmUserId =
|
||||
fridayAmStaff[fridayAmRotationIdx % fridayAmStaff.length]
|
||||
.id;
|
||||
fridayAmRotationIdx++;
|
||||
} else {
|
||||
effectiveAmUserId = null; // No eligible staff
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (effectiveAmUserId != null) {
|
||||
_tryAddDraft(
|
||||
draft,
|
||||
existing,
|
||||
templates,
|
||||
'am',
|
||||
amUserId,
|
||||
effectiveAmUserId,
|
||||
day,
|
||||
const [],
|
||||
);
|
||||
@@ -1699,12 +1788,12 @@ class _ScheduleGeneratorPanelState
|
||||
);
|
||||
}
|
||||
|
||||
// Remaining IT Staff get normal shift
|
||||
// Remaining IT Staff (including excluded) get normal shift
|
||||
final assignedToday = <String?>[
|
||||
amUserId,
|
||||
effectiveAmUserId,
|
||||
pmUserId,
|
||||
].whereType<String>().toSet();
|
||||
for (final profile in itStaff) {
|
||||
for (final profile in allItStaff) {
|
||||
if (assignedToday.contains(profile.id)) continue;
|
||||
final normalKey = dayIsRamadan && profile.religion == 'islam'
|
||||
? 'normal_ramadan_islam'
|
||||
|
||||
Reference in New Issue
Block a user