import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../core/formatters.dart'; import '../models/shift.dart'; import '../models/timecard.dart'; import 'seed.dart'; import 'timecards_controller.dart'; class ShiftsController extends Notifier> { @override List build() => Seed.shifts(); Shift? byId(String id) { for (final s in state) { if (s.id == id) return s; } return null; } void _replace(Shift updated) { state = [ for (final s in state) if (s.id == updated.id) updated else s, ]; } /// Accept an open shift. It moves straight into Booked. void apply(String id) { final shift = byId(id); if (shift == null || shift.status != ShiftStatus.open) return; _replace(shift.copyWith(status: ShiftStatus.booked)); } /// Give a booked shift back. Counts against the cancellation tally. void cancel(String id) { final shift = byId(id); if (shift == null) return; _replace(shift.copyWith(status: ShiftStatus.cancelled)); } void clockIn(String id) { final shift = byId(id); if (shift == null || shift.status != ShiftStatus.booked) return; _replace( shift.copyWith(status: ShiftStatus.active, clockIn: DateTime.now()), ); } /// Clocking out completes the shift and files the timecard automatically. Timecard? clockOut(String id) { final shift = byId(id); if (shift == null || shift.status != ShiftStatus.active) return null; final now = DateTime.now(); final done = shift.copyWith(status: ShiftStatus.completed, clockOut: now); _replace(done); final card = Timecard( id: 'tc-${shift.id}-${now.millisecondsSinceEpoch}', shiftId: shift.id, role: shift.role, business: shift.business, scheduledStart: shift.start, scheduledEnd: shift.end, clockIn: done.clockIn!, clockOut: now, breakMinutes: shift.breakMinutes, rate: shift.rate, status: TimecardStatus.needsReview, submittedAt: now, ); ref.read(timecardsProvider.notifier).add(card); return card; } } final shiftsProvider = NotifierProvider>( ShiftsController.new, ); final bookedShiftsProvider = Provider>((ref) { final list = ref .watch(shiftsProvider) .where( (s) => s.status == ShiftStatus.booked || s.status == ShiftStatus.active, ) .toList() ..sort((a, b) => a.start.compareTo(b.start)); return list; }); final openShiftsProvider = Provider>((ref) { final list = ref .watch(shiftsProvider) .where((s) => s.status == ShiftStatus.open) .toList() ..sort((a, b) => b.rate.compareTo(a.rate)); return list; }); final historyShiftsProvider = Provider>((ref) { final list = ref .watch(shiftsProvider) .where( (s) => const { ShiftStatus.completed, ShiftStatus.cancelled, ShiftStatus.noShow, }.contains(s.status), ) .toList() ..sort((a, b) => b.start.compareTo(a.start)); return list; }); /// The shift the Today screen leads with: whatever is running now, else the /// next one on the calendar. final nextShiftProvider = Provider((ref) { final booked = ref.watch(bookedShiftsProvider); for (final s in booked) { if (s.status == ShiftStatus.active) return s; } final now = DateTime.now(); for (final s in booked) { if (s.end.isAfter(now)) return s; } return booked.isEmpty ? null : booked.first; }); final activeShiftProvider = Provider((ref) { for (final s in ref.watch(shiftsProvider)) { if (s.status == ShiftStatus.active) return s; } return null; }); /// Gross pay, pre-tax, already banked from completed shifts. final completedEarningsProvider = Provider((ref) { return ref .watch(shiftsProvider) .where((s) => s.status == ShiftStatus.completed) .fold(0, (sum, s) => sum + s.pay); }); final hoursLoggedProvider = Provider((ref) { return ref .watch(shiftsProvider) .where((s) => s.status == ShiftStatus.completed) .fold( Duration.zero, (sum, s) => sum + (s.workedDuration ?? Duration.zero), ); }); /// Booked but not yet worked: the money still coming. final scheduledEarningsProvider = Provider((ref) { return ref .watch(bookedShiftsProvider) .fold(0, (sum, s) => sum + s.scheduledPay); }); /// Shifts falling on a given calendar day, used by the week strip. final shiftsOnDayProvider = Provider.family, DateTime>((ref, day) { return ref .watch(bookedShiftsProvider) .where((s) => Fmt.sameDay(s.start, day)) .toList(); });