import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart'; import '../core/formatters.dart'; import '../core/theme.dart'; import '../models/compliance.dart'; import '../models/shift.dart'; import '../state/compliance_controller.dart'; import '../state/shifts_controller.dart'; import '../widgets/common.dart'; class ShiftDetailScreen extends ConsumerWidget { const ShiftDetailScreen({super.key, required this.shiftId}); final String shiftId; @override Widget build(BuildContext context, WidgetRef ref) { final shift = ref.watch(shiftsProvider.notifier).byId(shiftId); if (shift == null) { return Scaffold( appBar: AppBar(), body: const Center(child: Text('That shift is no longer listed.')), ); } return Scaffold( body: ListView( padding: EdgeInsets.zero, children: [ BrandHeader( padding: const EdgeInsets.fromLTRB(20, 8, 20, 24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ HeaderIconButton( icon: LucideIcons.arrowLeft, onPressed: () => context.pop(), ), HeaderIconButton( icon: LucideIcons.messageCircle, tooltip: 'Message the manager', onPressed: () => context.push('/support'), ), ], ), const SizedBox(height: 18), StatusChip( shift.status.label, tone: switch (shift.status) { ShiftStatus.active || ShiftStatus.booked => ChipTone.yellow, ShiftStatus.noShow => ChipTone.danger, _ => ChipTone.neutral, }, ), const SizedBox(height: 12), Text( shift.role, style: const TextStyle( fontSize: 30, fontWeight: FontWeight.w700, letterSpacing: -1.1, color: Colors.white, ), ), const SizedBox(height: 4), Text( '${shift.business} · ${Fmt.dayShort(shift.start)}', style: TextStyle( fontSize: 14, color: Colors.white.withValues(alpha: 0.62), ), ), ], ), ), Padding( padding: const EdgeInsets.fromLTRB(20, 16, 20, 28), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ StatSlabRow( slabs: [ StatSlab( shift.status == ShiftStatus.completed ? 'Paid' : 'Est.', Fmt.money(shift.pay), ), StatSlab('Rate', '${Fmt.money(shift.rate)}/hr'), StatSlab( 'Hours', Fmt.duration( shift.workedDuration ?? shift.scheduledDuration, ), ), ], ), const SectionLabel('The shift'), KrowCard( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( children: [ DetailRow('Date', Fmt.dayShort(shift.start)), const Divider(height: 1), DetailRow( 'Scheduled', Fmt.window(shift.start, shift.end), ), const Divider(height: 1), DetailRow('Location', shift.address), const Divider(height: 1), DetailRow('Break, unpaid', '${shift.breakMinutes} min'), if (shift.manager != null) ...[ const Divider(height: 1), DetailRow('Report to', shift.manager!), ], if (shift.clockIn != null) ...[ const Divider(height: 1), DetailRow( 'Clocked', shift.clockOut == null ? '${Fmt.time(shift.clockIn!)} - running' : Fmt.window(shift.clockIn!, shift.clockOut!), valueColor: KrowPalette.blue, ), ], ], ), ), const SectionLabel('Pay breakdown'), KrowCard( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( children: [ DetailRow('Base rate', '${Fmt.money(shift.rate)}/hr'), const Divider(height: 1), DetailRow( 'Regular · ' '${Fmt.duration(shift.workedDuration ?? shift.scheduledDuration)}', Fmt.money(shift.pay), ), const Divider(height: 1), DetailRow( 'Total, pre-tax', Fmt.money(shift.pay), emphasis: true, ), ], ), ), SectionLabel( 'Requirements', trailing: TextButton( onPressed: () => context.push('/shift/${shift.id}/requirements'), child: const Text('See all'), ), ), ShiftRequirements(shift: shift), const SizedBox(height: 18), _Actions(shift: shift), ], ), ), ], ), ); } } /// What this business asks for, checked against what is already on file. /// Shown inline on the shift, and on its own screen from the link above it. class ShiftRequirements extends ConsumerWidget { const ShiftRequirements({super.key, required this.shift}); final Shift shift; @override Widget build(BuildContext context, WidgetRef ref) { final held = ref.watch(complianceProvider).all; var met = 0; final rows = []; for (final req in shift.requirements) { final item = held.where((i) => i.title == req).firstOrNull; final ok = item?.state == ComplianceState.verified; if (ok) met++; rows.add( Padding( padding: const EdgeInsets.symmetric(vertical: 11), child: Row( children: [ Container( width: 22, height: 22, decoration: BoxDecoration( color: ok ? KrowPalette.blueTint : const Color(0xFFF7E4AE), borderRadius: BorderRadius.circular(7), ), child: Icon( ok ? LucideIcons.check : LucideIcons.clock, size: 12, color: ok ? KrowPalette.blue : KrowPalette.ink, ), ), const SizedBox(width: 12), Expanded( child: Text( req, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, ), ), ), Text( ok ? 'On file' : (item?.badge ?? 'Missing'), style: TextStyle( fontSize: 12, color: ok ? KrowPalette.mute : KrowPalette.ink, fontWeight: ok ? FontWeight.w400 : FontWeight.w600, ), ), ], ), ), ); } final total = shift.requirements.length; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ KrowCard( padding: const EdgeInsets.all(16), child: Row( children: [ SizedBox( width: 54, height: 54, child: Stack( alignment: Alignment.center, children: [ CircularProgressIndicator( value: total == 0 ? 1 : met / total, strokeWidth: 6, backgroundColor: const Color(0xFFDEEBE2), valueColor: const AlwaysStoppedAnimation( KrowPalette.blue, ), ), Text( '$met/$total', style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w700, color: KrowPalette.blue, ), ), ], ), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( met == total ? 'You are cleared to work' : 'Some items still missing', style: const TextStyle( fontSize: 17, fontWeight: FontWeight.w700, letterSpacing: -0.4, ), ), const SizedBox(height: 4), Text( met == total ? 'Everything ${shift.business} asks for is on file.' : 'Clear them from the compliance hub to apply.', style: const TextStyle( fontSize: 13, height: 1.45, color: KrowPalette.mute, ), ), ], ), ), ], ), ), const SizedBox(height: 10), KrowCard( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), child: Column(children: rows), ), ], ); } } class _Actions extends ConsumerWidget { const _Actions({required this.shift}); final Shift shift; Future _confirmCancel(BuildContext context, WidgetRef ref) async { final ok = await showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Give this shift back?'), content: const Text( 'It goes back on the open market straight away, and the ' 'cancellation counts against your trust index.', ), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), child: const Text('Keep it'), ), FilledButton( onPressed: () => Navigator.pop(context, true), style: FilledButton.styleFrom(minimumSize: const Size(0, 44)), child: const Text('Cancel shift'), ), ], ), ); if (ok != true || !context.mounted) return; ref.read(shiftsProvider.notifier).cancel(shift.id); ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: Text('Shift cancelled'))); context.pop(); } @override Widget build(BuildContext context, WidgetRef ref) { switch (shift.status) { case ShiftStatus.open: return FilledButton.icon( onPressed: () { ref.read(shiftsProvider.notifier).apply(shift.id); ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: Text('Shift booked'))); }, icon: const Icon(LucideIcons.check, size: 18), label: const Text('Apply for this shift'), ); case ShiftStatus.booked: return Column( children: [ FilledButton.icon( onPressed: () => context.go('/clock'), icon: const Icon(LucideIcons.clock, size: 18), label: const Text('Go to clock in'), ), const SizedBox(height: 10), OutlinedButton( onPressed: () => _confirmCancel(context, ref), child: const Text('Cancel this shift'), ), ], ); case ShiftStatus.active: return FilledButton.icon( onPressed: () => context.go('/clock'), icon: const Icon(LucideIcons.squareCheck, size: 18), label: const Text('You are on shift - clock out'), ); default: return OutlinedButton.icon( onPressed: () => context.push('/support'), icon: const Icon(LucideIcons.lifeBuoy, size: 18), label: const Text('Something wrong? Get help'), ); } } }