173 lines
4.8 KiB
Dart
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();
|
|
});
|