Initial commit: Krow Worker App
This commit is contained in:
172
lib/state/shifts_controller.dart
Normal file
172
lib/state/shifts_controller.dart
Normal file
@@ -0,0 +1,172 @@
|
||||
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();
|
||||
});
|
||||
Reference in New Issue
Block a user