Files
krow-worker-app/lib/models/availability.dart
2026-10-05 22:14:58 +05:30

98 lines
3.3 KiB
Dart

/// What a worker has said about a given day.
///
/// Three states rather than an on/off switch, because "I can work" and "I want
/// work" are different answers and Auto-Match treats them differently: an
/// available day is one it may fill, a preferred day is one it should fill
/// first.
enum DayAvailability { available, prefers, unavailable }
extension DayAvailabilityX on DayAvailability {
String get label => switch (this) {
DayAvailability.available => 'Available',
DayAvailability.prefers => 'Prefers to work',
DayAvailability.unavailable => 'Unavailable',
};
/// The line under the option, in the picker.
String get blurb => switch (this) {
DayAvailability.available => 'Can be scheduled any time',
DayAvailability.prefers => 'Wants shifts — optionally within a time window',
DayAvailability.unavailable => 'Marked off (vacation, sick, personal)',
};
/// The line under the date, once the day is set.
String get summary => switch (this) {
DayAvailability.available => 'Can be scheduled any time',
DayAvailability.prefers => 'Wants shifts',
DayAvailability.unavailable => 'Not available',
};
}
/// The hours a worker would rather be given, on a day they want work.
///
/// Both ends are optional and independent: "from 6pm, any end" is a normal
/// thing to want, and forcing a full range would make the worker invent one.
/// Null at both ends means any time, which is also the default.
class PreferredWindow {
const PreferredWindow({this.from, this.to});
/// Hour of the day, 0-23. Null is "any time".
final int? from;
final int? to;
static const any = PreferredWindow();
bool get isAnyTime => from == null && to == null;
/// How the window reads on its own, as a label.
String get label {
if (isAnyTime) return 'Any time';
if (from != null && to != null) return '${_hhmm(from!)} - ${_hhmm(to!)}';
if (from != null) return 'From ${_hhmm(from!)}';
return 'Until ${_hhmm(to!)}';
}
/// How the window reads inside a sentence — "look for shifts $sentence
/// first". The label does not survive being dropped mid-sentence: "look for
/// From 09:00 first" is not English.
String get sentence {
if (isAnyTime) return 'at any time';
if (from != null && to != null) {
return 'between ${_hhmm(from!)} and ${_hhmm(to!)}';
}
if (from != null) return 'from ${_hhmm(from!)}';
return 'until ${_hhmm(to!)}';
}
PreferredWindow copyWith({
int? from,
int? to,
bool clearFrom = false,
bool clearTo = false,
}) => PreferredWindow(
from: clearFrom ? null : (from ?? this.from),
to: clearTo ? null : (to ?? this.to),
);
static String _hhmm(int hour) => '${hour.toString().padLeft(2, '0')}:00';
}
/// A day's answer: the status, and the window if one was given.
class DayPlan {
const DayPlan({
this.status = DayAvailability.unavailable,
this.window = PreferredWindow.any,
});
final DayAvailability status;
/// Only meaningful while [status] is [DayAvailability.prefers]; kept across
/// a status change so flipping away and back does not lose the hours.
final PreferredWindow window;
bool get isSet => status != DayAvailability.unavailable;
DayPlan copyWith({DayAvailability? status, PreferredWindow? window}) =>
DayPlan(status: status ?? this.status, window: window ?? this.window);
}