import 'package:flutter/material.dart'; import 'package:flutter/services.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/shift.dart'; import '../state/shifts_controller.dart'; import '../widgets/common.dart'; import '../widgets/glass_segments.dart'; import '../widgets/krow_wordmark.dart'; import '../widgets/shift_card.dart'; import '../widgets/week_strip.dart'; enum _Tab { booked, open, history } class ShiftsScreen extends ConsumerStatefulWidget { const ShiftsScreen({super.key}); @override ConsumerState createState() => _ShiftsScreenState(); } class _ShiftsScreenState extends ConsumerState { _Tab _tab = _Tab.booked; /// Where we came from, so the new list can slide in from the right side. _Tab _previousTab = _Tab.booked; DateTime _selectedDay = DateTime.now(); String _query = ''; String _filter = 'All'; void _selectTab(_Tab tab) { if (tab == _tab) return; setState(() { _previousTab = _tab; _tab = tab; }); } List get _week { final now = DateTime.now(); return [for (var i = 0; i < 7; i++) now.add(Duration(days: i))]; } @override Widget build(BuildContext context) { return Scaffold( body: Column( children: [ BrandHeader( padding: const EdgeInsets.fromLTRB(20, 8, 20, 20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ HeaderBrandLine( trailing: HeaderEyebrow(Fmt.monthYear(DateTime.now())), ), const SizedBox(height: 8), const Text( 'Shifts', style: TextStyle( fontSize: 27, fontWeight: FontWeight.w700, letterSpacing: -0.9, color: Colors.white, ), ), const SizedBox(height: 18), _HeaderSegments(value: _tab, onChanged: _selectTab), ], ), ), Expanded( // The list slides in from the side the new tab sits on, so the // content moves the same way your eye just did. child: AnimatedSwitcher( duration: const Duration(milliseconds: 280), switchInCurve: Curves.easeOutCubic, switchOutCurve: Curves.easeIn, layoutBuilder: (current, previous) => Stack( alignment: Alignment.topCenter, children: [...previous, ?current], ), transitionBuilder: (child, animation) { final incoming = child.key == ValueKey(_tab); final direction = _tab.index >= _previousTab.index ? 1.0 : -1.0; return FadeTransition( opacity: animation, child: SlideTransition( position: Tween( begin: Offset(incoming ? 0.08 * direction : 0, 0), end: Offset.zero, ).animate(animation), child: child, ), ); }, child: KeyedSubtree( key: ValueKey(_tab), child: switch (_tab) { _Tab.booked => _BookedTab( week: _week, selected: _selectedDay, onSelect: (d) => setState(() => _selectedDay = d), ), _Tab.open => _OpenTab( query: _query, filter: _filter, onQuery: (q) => setState(() => _query = q), onFilter: (f) => setState(() => _filter = f), ), _Tab.history => const _HistoryTab(), }, ), ), ), ], ), ); } } class _HeaderSegments extends StatelessWidget { const _HeaderSegments({required this.value, required this.onChanged}); final _Tab value; final ValueChanged<_Tab> onChanged; @override Widget build(BuildContext context) { return GlassSegments<_Tab>( segments: const [ (_Tab.booked, 'Booked'), (_Tab.open, 'Open'), (_Tab.history, 'History'), ], value: value, onChanged: onChanged, ); } } class _BookedTab extends ConsumerWidget { const _BookedTab({ required this.week, required this.selected, required this.onSelect, }); final List week; final DateTime selected; final ValueChanged onSelect; @override Widget build(BuildContext context, WidgetRef ref) { final booked = ref.watch(bookedShiftsProvider); final forDay = booked.where((s) => Fmt.sameDay(s.start, selected)).toList(); final marked = { for (var i = 0; i < week.length; i++) if (booked.any((s) => Fmt.sameDay(s.start, week[i]))) i, }; return ListView( padding: const EdgeInsets.fromLTRB(20, 18, 20, 24), children: [ WeekStrip( days: week, selected: selected, onSelect: onSelect, marked: marked, ), SectionLabel( '${Fmt.dayFull(selected)} · ${forDay.length} ' '${forDay.length == 1 ? 'shift' : 'shifts'}', ), if (forDay.isEmpty) const EmptyState( icon: LucideIcons.ticket, title: 'Nothing booked this day', message: 'Switch to Open above to claim a shift near you.', ) else for (final shift in forDay) Padding( padding: const EdgeInsets.only(bottom: 11), child: ShiftCard( shift: shift, onTap: () => context.push('/shift/${shift.id}'), ), ), const SizedBox(height: 6), const InfoBanner( 'Auto-Match only offers you shifts inside the availability blocks you ' 'have switched on.', ), ], ); } } class _OpenTab extends ConsumerWidget { const _OpenTab({ required this.query, required this.filter, required this.onQuery, required this.onFilter, }); final String query; final String filter; final ValueChanged onQuery; final ValueChanged onFilter; static const _filters = ['All', 'Today', 'This week', 'Top pay']; @override Widget build(BuildContext context, WidgetRef ref) { var shifts = ref.watch(openShiftsProvider); final now = DateTime.now(); if (query.isNotEmpty) { final q = query.toLowerCase(); shifts = shifts .where( (s) => s.role.toLowerCase().contains(q) || s.business.toLowerCase().contains(q) || s.venue.toLowerCase().contains(q), ) .toList(); } shifts = switch (filter) { 'Today' => shifts.where((s) => Fmt.sameDay(s.start, now)).toList(), 'This week' => shifts.where((s) => s.start.difference(now).inDays < 7).toList(), 'Top pay' => shifts.where((s) => s.rate >= 22).toList(), _ => shifts, }; return ListView( padding: const EdgeInsets.fromLTRB(20, 18, 20, 24), children: [ TextField( onChanged: onQuery, decoration: const InputDecoration( hintText: 'Role, business or area', prefixIcon: Icon(LucideIcons.search, size: 18), ), ), const SizedBox(height: 14), SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: [ for (final f in _filters) Padding( padding: const EdgeInsets.only(right: 8), child: ChoiceChip( label: Text(f), selected: f == filter, showCheckmark: false, selectedColor: KrowPalette.blue, labelStyle: TextStyle( fontSize: 12.5, fontWeight: FontWeight.w500, color: f == filter ? Colors.white : KrowPalette.mute, ), onSelected: (_) => onFilter(f), ), ), ], ), ), SectionLabel('${shifts.length} shifts · highest paying first'), if (shifts.isEmpty) const EmptyState( icon: LucideIcons.search, title: 'Nothing matches', message: 'Try a different filter or clear your search.', ) else for (final shift in shifts) Padding( padding: const EdgeInsets.only(bottom: 11), child: ShiftCard( shift: shift, showDate: true, onTap: () => context.push('/shift/${shift.id}'), trailingAction: _ApplyButton(shift: shift), ), ), ], ); } } class _ApplyButton extends ConsumerWidget { const _ApplyButton({required this.shift}); final Shift shift; @override Widget build(BuildContext context, WidgetRef ref) { return Row( mainAxisSize: MainAxisSize.min, children: [ Text( Fmt.money(shift.scheduledPay), style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w700, letterSpacing: -0.6, ), ), const SizedBox(width: 10), FilledButton( onPressed: () { HapticFeedback.selectionClick(); ref.read(shiftsProvider.notifier).apply(shift.id); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( '${shift.role} at ${shift.business} booked for ' '${Fmt.dayShort(shift.start)}', ), ), ); }, style: FilledButton.styleFrom( minimumSize: const Size(0, 34), padding: const EdgeInsets.symmetric(horizontal: 16), textStyle: const TextStyle( fontSize: 12.5, fontWeight: FontWeight.w700, ), ), child: const Text('Apply'), ), ], ); } } class _HistoryTab extends ConsumerWidget { const _HistoryTab(); @override Widget build(BuildContext context, WidgetRef ref) { final history = ref.watch(historyShiftsProvider); final completed = history .where((s) => s.status == ShiftStatus.completed) .length; final cancelled = history .where((s) => s.status == ShiftStatus.cancelled) .length; final logged = ref.watch(hoursLoggedProvider); return ListView( padding: const EdgeInsets.fromLTRB(20, 18, 20, 24), children: [ StatRail( stats: [ ('Completed', '$completed'), ('Cancelled', '$cancelled'), ('Logged', Fmt.duration(logged)), ], ), const SectionLabel('Everything you have worked'), if (history.isEmpty) const EmptyState( icon: LucideIcons.history, title: 'No history yet', message: 'Shifts you finish or cancel will show up here.', ) else for (final shift in history) Padding( padding: const EdgeInsets.only(bottom: 11), child: ShiftCard( shift: shift, showDate: true, onTap: () => context.push('/shift/${shift.id}'), ), ), ], ); } }