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

173 lines
4.8 KiB
Dart

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<List<Shift>> {
@override
List<Shift> 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, List<Shift>>(
ShiftsController.new,
);
final bookedShiftsProvider = Provider<List<Shift>>((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<List<Shift>>((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<List<Shift>>((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<Shift?>((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<Shift?>((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<double>((ref) {
return ref
.watch(shiftsProvider)
.where((s) => s.status == ShiftStatus.completed)
.fold<double>(0, (sum, s) => sum + s.pay);
});
final hoursLoggedProvider = Provider<Duration>((ref) {
return ref
.watch(shiftsProvider)
.where((s) => s.status == ShiftStatus.completed)
.fold<Duration>(
Duration.zero,
(sum, s) => sum + (s.workedDuration ?? Duration.zero),
);
});
/// Booked but not yet worked: the money still coming.
final scheduledEarningsProvider = Provider<double>((ref) {
return ref
.watch(bookedShiftsProvider)
.fold<double>(0, (sum, s) => sum + s.scheduledPay);
});
/// Shifts falling on a given calendar day, used by the week strip.
final shiftsOnDayProvider = Provider.family<List<Shift>, DateTime>((ref, day) {
return ref
.watch(bookedShiftsProvider)
.where((s) => Fmt.sameDay(s.start, day))
.toList();
});