/// 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); }