Enhanced material design 3 implementation
This commit is contained in:
@@ -14,6 +14,8 @@ import '../../providers/rotation_config_provider.dart';
|
||||
import '../../providers/workforce_provider.dart';
|
||||
import '../../providers/chat_provider.dart';
|
||||
import '../../providers/ramadan_provider.dart';
|
||||
import '../../widgets/app_page_header.dart';
|
||||
import '../../widgets/app_state_view.dart';
|
||||
import '../../widgets/responsive_body.dart';
|
||||
import '../../theme/app_surfaces.dart';
|
||||
import '../../utils/snackbar.dart';
|
||||
@@ -30,59 +32,71 @@ class WorkforceScreen extends ConsumerWidget {
|
||||
role == 'admin' || role == 'programmer' || role == 'dispatcher';
|
||||
|
||||
return ResponsiveBody(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final isWide = constraints.maxWidth >= 980;
|
||||
final schedulePanel = _SchedulePanel(isAdmin: isAdmin);
|
||||
final swapsPanel = _SwapRequestsPanel(isAdmin: isAdmin);
|
||||
final generatorPanel = _ScheduleGeneratorPanel(enabled: isAdmin);
|
||||
child: Column(
|
||||
children: [
|
||||
const AppPageHeader(
|
||||
title: 'Workforce',
|
||||
subtitle: 'Duty schedules and shift management',
|
||||
),
|
||||
Expanded(
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final isWide = constraints.maxWidth >= 980;
|
||||
final schedulePanel = _SchedulePanel(isAdmin: isAdmin);
|
||||
final swapsPanel = _SwapRequestsPanel(isAdmin: isAdmin);
|
||||
final generatorPanel = _ScheduleGeneratorPanel(
|
||||
enabled: isAdmin,
|
||||
);
|
||||
|
||||
if (isWide) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(flex: 3, child: schedulePanel),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
if (isWide) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(flex: 3, child: schedulePanel),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Column(
|
||||
children: [
|
||||
if (isAdmin) generatorPanel,
|
||||
if (isAdmin) const SizedBox(height: 16),
|
||||
Expanded(child: swapsPanel),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return DefaultTabController(
|
||||
length: isAdmin ? 3 : 2,
|
||||
child: Column(
|
||||
children: [
|
||||
if (isAdmin) generatorPanel,
|
||||
if (isAdmin) const SizedBox(height: 16),
|
||||
Expanded(child: swapsPanel),
|
||||
const SizedBox(height: 8),
|
||||
TabBar(
|
||||
tabs: [
|
||||
const Tab(text: 'Schedule'),
|
||||
const Tab(text: 'Swaps'),
|
||||
if (isAdmin) const Tab(text: 'Generator'),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
children: [
|
||||
schedulePanel,
|
||||
swapsPanel,
|
||||
if (isAdmin) generatorPanel,
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return DefaultTabController(
|
||||
length: isAdmin ? 3 : 2,
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
TabBar(
|
||||
tabs: [
|
||||
const Tab(text: 'Schedule'),
|
||||
const Tab(text: 'Swaps'),
|
||||
if (isAdmin) const Tab(text: 'Generator'),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
children: [
|
||||
schedulePanel,
|
||||
swapsPanel,
|
||||
if (isAdmin) generatorPanel,
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -140,7 +154,12 @@ class _SchedulePanel extends ConsumerWidget {
|
||||
.toList();
|
||||
|
||||
if (schedules.isEmpty) {
|
||||
return const Center(child: Text('No schedules yet.'));
|
||||
return const AppEmptyView(
|
||||
icon: Icons.calendar_month_outlined,
|
||||
title: 'No schedules yet',
|
||||
subtitle:
|
||||
'Generated schedules will appear here. Use the Generator tab to create them.',
|
||||
);
|
||||
}
|
||||
|
||||
final Map<String, Profile> profileById = {
|
||||
@@ -192,6 +211,7 @@ class _SchedulePanel extends ConsumerWidget {
|
||||
),
|
||||
isMine: schedule.userId == currentUserId,
|
||||
isAdmin: isAdmin,
|
||||
role: profileById[schedule.userId]?.role,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -201,8 +221,10 @@ class _SchedulePanel extends ConsumerWidget {
|
||||
);
|
||||
},
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (error, _) =>
|
||||
Center(child: Text('Failed to load schedules: $error')),
|
||||
error: (error, _) => AppErrorView(
|
||||
error: error,
|
||||
onRetry: () => ref.invalidate(dutySchedulesProvider),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -280,6 +302,7 @@ class _ScheduleTile extends ConsumerWidget {
|
||||
required this.relieverLabels,
|
||||
required this.isMine,
|
||||
required this.isAdmin,
|
||||
this.role,
|
||||
});
|
||||
|
||||
final DutySchedule schedule;
|
||||
@@ -287,29 +310,27 @@ class _ScheduleTile extends ConsumerWidget {
|
||||
final List<String> relieverLabels;
|
||||
final bool isMine;
|
||||
final bool isAdmin;
|
||||
final String? role;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final currentUserId = ref.watch(currentUserIdProvider);
|
||||
final swaps = ref.watch(swapRequestsProvider).valueOrNull ?? [];
|
||||
// Use .select() so this tile only rebuilds when its own swap status changes,
|
||||
// not every time any swap in the list is updated.
|
||||
final hasRequestedSwap = ref.watch(
|
||||
swapRequestsProvider.select(
|
||||
(async) => (async.valueOrNull ?? const []).any(
|
||||
(swap) =>
|
||||
swap.requesterScheduleId == schedule.id &&
|
||||
swap.requesterId == currentUserId &&
|
||||
swap.status == 'pending',
|
||||
),
|
||||
),
|
||||
);
|
||||
final now = AppTime.now();
|
||||
final isPast = schedule.startTime.isBefore(now);
|
||||
final hasRequestedSwap = swaps.any(
|
||||
(swap) =>
|
||||
swap.requesterScheduleId == schedule.id &&
|
||||
swap.requesterId == currentUserId &&
|
||||
swap.status == 'pending',
|
||||
);
|
||||
final canRequestSwap = isMine && schedule.status != 'absent' && !isPast;
|
||||
|
||||
final profiles = ref.watch(profilesProvider).valueOrNull ?? [];
|
||||
Profile? profile;
|
||||
try {
|
||||
profile = profiles.firstWhere((p) => p.id == schedule.userId);
|
||||
} catch (_) {
|
||||
profile = null;
|
||||
}
|
||||
final role = profile?.role;
|
||||
final rotationConfig = ref.watch(rotationConfigProvider).valueOrNull;
|
||||
|
||||
ShiftTypeConfig? shiftTypeConfig;
|
||||
@@ -889,15 +910,16 @@ class _ScheduleTile extends ConsumerWidget {
|
||||
}
|
||||
|
||||
Color _statusColor(BuildContext context, String status) {
|
||||
final cs = Theme.of(context).colorScheme;
|
||||
switch (status) {
|
||||
case 'arrival':
|
||||
return Colors.green;
|
||||
return cs.tertiary;
|
||||
case 'late':
|
||||
return Colors.orange;
|
||||
return cs.secondary;
|
||||
case 'absent':
|
||||
return Colors.red;
|
||||
return cs.error;
|
||||
default:
|
||||
return Theme.of(context).colorScheme.onSurfaceVariant;
|
||||
return cs.onSurfaceVariant;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user