Implement Asia/Manila Time Zone
Handled saving of Relievers
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
@@ -7,6 +9,7 @@ import '../../models/profile.dart';
|
||||
import '../../models/swap_request.dart';
|
||||
import '../../providers/profile_provider.dart';
|
||||
import '../../providers/workforce_provider.dart';
|
||||
import '../../utils/app_time.dart';
|
||||
import '../../widgets/responsive_body.dart';
|
||||
|
||||
class WorkforceScreen extends ConsumerWidget {
|
||||
@@ -137,6 +140,10 @@ class _SchedulePanel extends ConsumerWidget {
|
||||
schedule,
|
||||
isAdmin,
|
||||
),
|
||||
relieverLabels: _relieverLabelsFromIds(
|
||||
schedule.relieverIds,
|
||||
profileById,
|
||||
),
|
||||
isMine: schedule.userId == currentUserId,
|
||||
),
|
||||
),
|
||||
@@ -217,30 +224,46 @@ class _SchedulePanel extends ConsumerWidget {
|
||||
final weekday = weekdays[value.weekday - 1];
|
||||
return '$weekday, $month $day, ${value.year}';
|
||||
}
|
||||
|
||||
List<String> _relieverLabelsFromIds(
|
||||
List<String> relieverIds,
|
||||
Map<String, Profile> profileById,
|
||||
) {
|
||||
if (relieverIds.isEmpty) return const [];
|
||||
return relieverIds
|
||||
.map(
|
||||
(id) => profileById[id]?.fullName.isNotEmpty == true
|
||||
? profileById[id]!.fullName
|
||||
: id,
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
class _ScheduleTile extends ConsumerWidget {
|
||||
const _ScheduleTile({
|
||||
required this.schedule,
|
||||
required this.displayName,
|
||||
required this.relieverLabels,
|
||||
required this.isMine,
|
||||
});
|
||||
|
||||
final DutySchedule schedule;
|
||||
final String displayName;
|
||||
final List<String> relieverLabels;
|
||||
final bool isMine;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final currentUserId = ref.watch(currentUserIdProvider);
|
||||
final swaps = ref.watch(swapRequestsProvider).valueOrNull ?? [];
|
||||
final now = DateTime.now();
|
||||
final now = AppTime.now();
|
||||
final isPast = schedule.startTime.isBefore(now);
|
||||
final canCheckIn =
|
||||
isMine &&
|
||||
schedule.checkInAt == null &&
|
||||
(schedule.status == 'scheduled' || schedule.status == 'late') &&
|
||||
now.isAfter(schedule.startTime.subtract(const Duration(minutes: 15))) &&
|
||||
now.isAfter(schedule.startTime.subtract(const Duration(hours: 2))) &&
|
||||
now.isBefore(schedule.endTime);
|
||||
final hasRequestedSwap = swaps.any(
|
||||
(swap) =>
|
||||
@@ -253,56 +276,79 @@ class _ScheduleTile extends ConsumerWidget {
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
displayName,
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${_formatTime(schedule.startTime)} - ${_formatTime(schedule.endTime)}',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_statusLabel(schedule.status),
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: _statusColor(context, schedule.status),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
Row(
|
||||
children: [
|
||||
if (canCheckIn)
|
||||
FilledButton.icon(
|
||||
onPressed: () => _handleCheckIn(context, ref, schedule),
|
||||
icon: const Icon(Icons.location_on),
|
||||
label: const Text('Check in'),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
displayName,
|
||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${_formatTime(schedule.startTime)} - ${_formatTime(schedule.endTime)}',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_statusLabel(schedule.status),
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: _statusColor(context, schedule.status),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (canRequestSwap) ...[
|
||||
if (canCheckIn) const SizedBox(height: 8),
|
||||
OutlinedButton.icon(
|
||||
onPressed: hasRequestedSwap
|
||||
? () => _openSwapsTab(context)
|
||||
: () => _requestSwap(context, ref, schedule),
|
||||
icon: const Icon(Icons.swap_horiz),
|
||||
label: Text(
|
||||
hasRequestedSwap ? 'Swap Requested' : 'Request swap',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
if (canCheckIn)
|
||||
FilledButton.icon(
|
||||
onPressed: () => _handleCheckIn(context, ref, schedule),
|
||||
icon: const Icon(Icons.location_on),
|
||||
label: const Text('Check in'),
|
||||
),
|
||||
if (canRequestSwap) ...[
|
||||
if (canCheckIn) const SizedBox(height: 8),
|
||||
OutlinedButton.icon(
|
||||
onPressed: hasRequestedSwap
|
||||
? () => _openSwapsTab(context)
|
||||
: () => _requestSwap(context, ref, schedule),
|
||||
icon: const Icon(Icons.swap_horiz),
|
||||
label: Text(
|
||||
hasRequestedSwap ? 'Swap Requested' : 'Request swap',
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
if (relieverLabels.isNotEmpty)
|
||||
ExpansionTile(
|
||||
tilePadding: EdgeInsets.zero,
|
||||
title: const Text('Relievers'),
|
||||
children: [
|
||||
for (final label in relieverLabels)
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 8,
|
||||
right: 8,
|
||||
bottom: 6,
|
||||
),
|
||||
child: Text(label),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -317,14 +363,22 @@ class _ScheduleTile extends ConsumerWidget {
|
||||
final geofence = await ref.read(geofenceProvider.future);
|
||||
if (geofence == null) {
|
||||
if (!context.mounted) return;
|
||||
_showMessage(context, 'Geofence is not configured.');
|
||||
await _showAlert(
|
||||
context,
|
||||
title: 'Geofence missing',
|
||||
message: 'Geofence is not configured.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
||||
if (!serviceEnabled) {
|
||||
if (!context.mounted) return;
|
||||
_showMessage(context, 'Location services are disabled.');
|
||||
await _showAlert(
|
||||
context,
|
||||
title: 'Location disabled',
|
||||
message: 'Location services are disabled.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -335,28 +389,43 @@ class _ScheduleTile extends ConsumerWidget {
|
||||
if (permission == LocationPermission.denied ||
|
||||
permission == LocationPermission.deniedForever) {
|
||||
if (!context.mounted) return;
|
||||
_showMessage(context, 'Location permission denied.');
|
||||
await _showAlert(
|
||||
context,
|
||||
title: 'Permission denied',
|
||||
message: 'Location permission denied.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final position = await Geolocator.getCurrentPosition(
|
||||
locationSettings: const LocationSettings(accuracy: LocationAccuracy.high),
|
||||
);
|
||||
|
||||
final distance = Geolocator.distanceBetween(
|
||||
position.latitude,
|
||||
position.longitude,
|
||||
geofence.lat,
|
||||
geofence.lng,
|
||||
);
|
||||
|
||||
if (distance > geofence.radiusMeters) {
|
||||
if (!context.mounted) return;
|
||||
_showMessage(context, 'You are outside the geofence. Wala ka sa CRMC.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!context.mounted) return;
|
||||
final progressContext = await _showCheckInProgress(context);
|
||||
try {
|
||||
final position = await Geolocator.getCurrentPosition(
|
||||
locationSettings: const LocationSettings(
|
||||
accuracy: LocationAccuracy.high,
|
||||
),
|
||||
);
|
||||
|
||||
final isInside = geofence.hasPolygon
|
||||
? geofence.containsPolygon(position.latitude, position.longitude)
|
||||
: geofence.hasCircle &&
|
||||
Geolocator.distanceBetween(
|
||||
position.latitude,
|
||||
position.longitude,
|
||||
geofence.lat!,
|
||||
geofence.lng!,
|
||||
) <=
|
||||
geofence.radiusMeters!;
|
||||
|
||||
if (!isInside) {
|
||||
if (!context.mounted) return;
|
||||
await _showAlert(
|
||||
context,
|
||||
title: 'Outside geofence',
|
||||
message: 'You are outside the geofence. Wala ka sa CRMC.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final status = await ref
|
||||
.read(workforceControllerProvider)
|
||||
.checkIn(
|
||||
@@ -366,10 +435,22 @@ class _ScheduleTile extends ConsumerWidget {
|
||||
);
|
||||
ref.invalidate(dutySchedulesProvider);
|
||||
if (!context.mounted) return;
|
||||
_showMessage(context, 'Checked in ($status).');
|
||||
await _showAlert(
|
||||
context,
|
||||
title: 'Checked in',
|
||||
message: 'Checked in ($status).',
|
||||
);
|
||||
} catch (error) {
|
||||
if (!context.mounted) return;
|
||||
_showMessage(context, 'Check-in failed: $error');
|
||||
await _showAlert(
|
||||
context,
|
||||
title: 'Check-in failed',
|
||||
message: 'Check-in failed: $error',
|
||||
);
|
||||
} finally {
|
||||
if (progressContext.mounted) {
|
||||
Navigator.of(progressContext).pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -446,6 +527,61 @@ class _ScheduleTile extends ConsumerWidget {
|
||||
).showSnackBar(SnackBar(content: Text(message)));
|
||||
}
|
||||
|
||||
Future<void> _showAlert(
|
||||
BuildContext context, {
|
||||
required String title,
|
||||
required String message,
|
||||
}) async {
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (dialogContext) {
|
||||
return AlertDialog(
|
||||
title: Text(title),
|
||||
content: Text(message),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(),
|
||||
child: const Text('OK'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<BuildContext> _showCheckInProgress(BuildContext context) {
|
||||
final completer = Completer<BuildContext>();
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (dialogContext) {
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete(dialogContext);
|
||||
}
|
||||
return AlertDialog(
|
||||
title: const Text('Validating location'),
|
||||
content: Row(
|
||||
children: [
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
width: 24,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Flexible(
|
||||
child: Text(
|
||||
'Please wait while we verify your location.',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
void _openSwapsTab(BuildContext context) {
|
||||
final controller = DefaultTabController.maybeOf(context);
|
||||
if (controller != null) {
|
||||
@@ -500,13 +636,15 @@ class _DraftSchedule {
|
||||
required this.shiftType,
|
||||
required this.startTime,
|
||||
required this.endTime,
|
||||
});
|
||||
List<String>? relieverIds,
|
||||
}) : relieverIds = relieverIds ?? <String>[];
|
||||
|
||||
final int localId;
|
||||
String userId;
|
||||
String shiftType;
|
||||
DateTime startTime;
|
||||
DateTime endTime;
|
||||
List<String> relieverIds;
|
||||
}
|
||||
|
||||
class _RotationEntry {
|
||||
@@ -669,7 +807,7 @@ class _ScheduleGeneratorPanelState
|
||||
}
|
||||
|
||||
Future<void> _pickDate({required bool isStart}) async {
|
||||
final now = DateTime.now();
|
||||
final now = AppTime.now();
|
||||
final initial = isStart ? _startDate ?? now : _endDate ?? _startDate ?? now;
|
||||
final picked = await showDatePicker(
|
||||
context: context,
|
||||
@@ -807,43 +945,73 @@ class _ScheduleGeneratorPanelState
|
||||
separatorBuilder: (context, index) => const SizedBox(height: 8),
|
||||
itemBuilder: (context, index) {
|
||||
final draft = _draftSchedules[index];
|
||||
final profile = _profileById()[draft.userId];
|
||||
final profileById = _profileById();
|
||||
final profile = profileById[draft.userId];
|
||||
final userLabel = profile?.fullName.isNotEmpty == true
|
||||
? profile!.fullName
|
||||
: draft.userId;
|
||||
final relieverLabels = draft.relieverIds
|
||||
.map(
|
||||
(id) => profileById[id]?.fullName.isNotEmpty == true
|
||||
? profileById[id]!.fullName
|
||||
: id,
|
||||
)
|
||||
.toList();
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
child: Row(
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${_shiftLabel(draft.shiftType)} · $userLabel',
|
||||
style: Theme.of(context).textTheme.bodyMedium
|
||||
?.copyWith(fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${_formatDate(draft.startTime)} · ${_formatTime(draft.startTime)} - ${_formatTime(draft.endTime)}',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: 'Edit',
|
||||
onPressed: () => _editDraft(draft),
|
||||
icon: const Icon(Icons.edit),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: 'Delete',
|
||||
onPressed: () => _deleteDraft(draft),
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (relieverLabels.isNotEmpty)
|
||||
ExpansionTile(
|
||||
tilePadding: EdgeInsets.zero,
|
||||
title: const Text('Relievers'),
|
||||
children: [
|
||||
Text(
|
||||
'${_shiftLabel(draft.shiftType)} · $userLabel',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
for (final label in relieverLabels)
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 8,
|
||||
right: 8,
|
||||
bottom: 6,
|
||||
),
|
||||
child: Text(label),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${_formatDate(draft.startTime)} · ${_formatTime(draft.startTime)} - ${_formatTime(draft.endTime)}',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: 'Edit',
|
||||
onPressed: () => _editDraft(draft),
|
||||
icon: const Icon(Icons.edit),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: 'Delete',
|
||||
onPressed: () => _deleteDraft(draft),
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -864,8 +1032,8 @@ class _ScheduleGeneratorPanelState
|
||||
setState(() {
|
||||
_draftSchedules.removeWhere((item) => item.localId == draft.localId);
|
||||
_warnings = _buildWarnings(
|
||||
_startDate ?? DateTime.now(),
|
||||
_endDate ?? DateTime.now(),
|
||||
_startDate ?? AppTime.now(),
|
||||
_endDate ?? AppTime.now(),
|
||||
_draftSchedules,
|
||||
);
|
||||
});
|
||||
@@ -878,7 +1046,7 @@ class _ScheduleGeneratorPanelState
|
||||
return;
|
||||
}
|
||||
|
||||
final start = existing?.startTime ?? _startDate ?? DateTime.now();
|
||||
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';
|
||||
@@ -1099,6 +1267,7 @@ class _ScheduleGeneratorPanelState
|
||||
'start_time': draft.startTime.toIso8601String(),
|
||||
'end_time': draft.endTime.toIso8601String(),
|
||||
'status': 'scheduled',
|
||||
'reliever_ids': draft.relieverIds,
|
||||
},
|
||||
)
|
||||
.toList();
|
||||
@@ -1249,6 +1418,10 @@ class _ScheduleGeneratorPanelState
|
||||
final nextWeekPmUserId = staff.isEmpty
|
||||
? null
|
||||
: staff[(pmBaseIndex + 1) % staff.length].id;
|
||||
final pmRelievers = _buildRelievers(pmBaseIndex, staff);
|
||||
final nextWeekRelievers = staff.isEmpty
|
||||
? <String>[]
|
||||
: _buildRelievers((pmBaseIndex + 1) % staff.length, staff);
|
||||
var weekendNormalOffset = 0;
|
||||
|
||||
for (
|
||||
@@ -1273,6 +1446,7 @@ class _ScheduleGeneratorPanelState
|
||||
'normal',
|
||||
staff[normalIndex].id,
|
||||
day,
|
||||
const [],
|
||||
);
|
||||
weekendNormalOffset += 1;
|
||||
}
|
||||
@@ -1284,15 +1458,40 @@ class _ScheduleGeneratorPanelState
|
||||
'on_call',
|
||||
nextWeekPmUserId,
|
||||
day,
|
||||
nextWeekRelievers,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (amUserId != null) {
|
||||
_tryAddDraft(draft, existing, templates, 'am', amUserId, day);
|
||||
_tryAddDraft(
|
||||
draft,
|
||||
existing,
|
||||
templates,
|
||||
'am',
|
||||
amUserId,
|
||||
day,
|
||||
const [],
|
||||
);
|
||||
}
|
||||
if (pmUserId != null) {
|
||||
_tryAddDraft(draft, existing, templates, 'pm', pmUserId, day);
|
||||
_tryAddDraft(draft, existing, templates, 'on_call', pmUserId, day);
|
||||
_tryAddDraft(
|
||||
draft,
|
||||
existing,
|
||||
templates,
|
||||
'pm',
|
||||
pmUserId,
|
||||
day,
|
||||
pmRelievers,
|
||||
);
|
||||
_tryAddDraft(
|
||||
draft,
|
||||
existing,
|
||||
templates,
|
||||
'on_call',
|
||||
pmUserId,
|
||||
day,
|
||||
pmRelievers,
|
||||
);
|
||||
}
|
||||
|
||||
final assignedToday = <String?>[
|
||||
@@ -1301,7 +1500,15 @@ class _ScheduleGeneratorPanelState
|
||||
].whereType<String>().toSet();
|
||||
for (final profile in staff) {
|
||||
if (assignedToday.contains(profile.id)) continue;
|
||||
_tryAddDraft(draft, existing, templates, 'normal', profile.id, day);
|
||||
_tryAddDraft(
|
||||
draft,
|
||||
existing,
|
||||
templates,
|
||||
'normal',
|
||||
profile.id,
|
||||
day,
|
||||
const [],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1319,6 +1526,7 @@ class _ScheduleGeneratorPanelState
|
||||
String shiftType,
|
||||
String userId,
|
||||
DateTime day,
|
||||
List<String> relieverIds,
|
||||
) {
|
||||
final template = templates[_normalizeShiftType(shiftType)]!;
|
||||
final start = template.buildStart(day);
|
||||
@@ -1329,6 +1537,7 @@ class _ScheduleGeneratorPanelState
|
||||
shiftType: shiftType,
|
||||
startTime: start,
|
||||
endTime: end,
|
||||
relieverIds: relieverIds,
|
||||
);
|
||||
|
||||
if (_hasConflict(candidate, draft, existing)) {
|
||||
@@ -1364,6 +1573,16 @@ class _ScheduleGeneratorPanelState
|
||||
return defaultIndex % staff.length;
|
||||
}
|
||||
|
||||
List<String> _buildRelievers(int primaryIndex, List<Profile> staff) {
|
||||
if (staff.length <= 1) return const [];
|
||||
final relievers = <String>[];
|
||||
for (var offset = 1; offset < staff.length; offset += 1) {
|
||||
relievers.add(staff[(primaryIndex + offset) % staff.length].id);
|
||||
if (relievers.length == 3) break;
|
||||
}
|
||||
return relievers;
|
||||
}
|
||||
|
||||
bool _hasConflict(
|
||||
_DraftSchedule candidate,
|
||||
List<_DraftSchedule> drafts,
|
||||
@@ -1545,11 +1764,11 @@ class _SwapRequestsPanel extends ConsumerWidget {
|
||||
final profilesAsync = ref.watch(profilesProvider);
|
||||
final currentUserId = ref.watch(currentUserIdProvider);
|
||||
|
||||
final scheduleById = {
|
||||
final Map<String, DutySchedule> scheduleById = {
|
||||
for (final schedule in schedulesAsync.valueOrNull ?? [])
|
||||
schedule.id: schedule,
|
||||
};
|
||||
final profileById = {
|
||||
final Map<String, Profile> profileById = {
|
||||
for (final profile in profilesAsync.valueOrNull ?? [])
|
||||
profile.id: profile,
|
||||
};
|
||||
@@ -1577,6 +1796,9 @@ class _SwapRequestsPanel extends ConsumerWidget {
|
||||
final subtitle = schedule == null
|
||||
? 'Shift not found'
|
||||
: '${_shiftLabel(schedule.shiftType)} · ${_formatDate(schedule.startTime)} · ${_formatTime(schedule.startTime)}';
|
||||
final relieverLabels = schedule == null
|
||||
? const <String>[]
|
||||
: _relieverLabelsFromIds(schedule.relieverIds, profileById);
|
||||
|
||||
final isPending = item.status == 'pending';
|
||||
final canRespond =
|
||||
@@ -1600,6 +1822,25 @@ class _SwapRequestsPanel extends ConsumerWidget {
|
||||
const SizedBox(height: 6),
|
||||
Text('Status: ${item.status}'),
|
||||
const SizedBox(height: 12),
|
||||
if (relieverLabels.isNotEmpty)
|
||||
ExpansionTile(
|
||||
tilePadding: EdgeInsets.zero,
|
||||
title: const Text('Relievers'),
|
||||
children: [
|
||||
for (final label in relieverLabels)
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 8,
|
||||
right: 8,
|
||||
bottom: 6,
|
||||
),
|
||||
child: Text(label),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
if (canRespond) ...[
|
||||
@@ -1703,4 +1944,18 @@ class _SwapRequestsPanel extends ConsumerWidget {
|
||||
final weekday = weekdays[value.weekday - 1];
|
||||
return '$weekday, $month $day, ${value.year}';
|
||||
}
|
||||
|
||||
List<String> _relieverLabelsFromIds(
|
||||
List<String> relieverIds,
|
||||
Map<String, Profile> profileById,
|
||||
) {
|
||||
if (relieverIds.isEmpty) return const [];
|
||||
return relieverIds
|
||||
.map(
|
||||
(id) => profileById[id]?.fullName.isNotEmpty == true
|
||||
? profileById[id]!.fullName
|
||||
: id,
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user