Files
krow-worker-app/lib/screens/shift_detail_screen.dart
2026-09-17 22:26:11 +05:30

380 lines
13 KiB
Dart

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,
),
],
),
),
const SectionLabel('Requirements'),
_Requirements(shift: shift),
const SizedBox(height: 18),
_Actions(shift: shift),
],
),
),
],
),
);
}
}
/// What this business asks for, checked against what is already on file.
class _Requirements extends ConsumerWidget {
const _Requirements({required this.shift});
final Shift shift;
@override
Widget build(BuildContext context, WidgetRef ref) {
final held = ref.watch(complianceProvider).all;
var met = 0;
final rows = <Widget>[];
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<void> _confirmCancel(BuildContext context, WidgetRef ref) async {
final ok = await showDialog<bool>(
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'),
);
}
}
}