import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../models/availability.dart'; import 'session_controller.dart'; /// Availability is stored as `yyyy-mm-dd` -> what the worker said about it. /// /// Days the worker has not answered are simply absent, and read back as /// unavailable: nobody is on the hook for a day they never opened. typedef AvailabilityMap = Map; String dayKey(DateTime d) => '${d.year}-${d.month.toString().padLeft(2, '0')}-${d.day.toString().padLeft(2, '0')}'; class AvailabilityController extends Notifier { static const _key = 'availability_v2'; SharedPreferences get _prefs => ref.read(prefsProvider); @override AvailabilityMap build() { final raw = _prefs.getStringList(_key) ?? const []; final map = {}; for (final entry in raw) { // Stored as "2026-10-05:prefers:9-17", or "…:available", or // "…:prefers:any" when no hours were given. final parts = entry.split(':'); if (parts.length < 2) continue; final status = DayAvailability.values.firstWhere( (s) => s.name == parts[1], orElse: () => DayAvailability.unavailable, ); if (status == DayAvailability.unavailable) continue; map[parts[0]] = DayPlan( status: status, window: parts.length > 2 ? _parseWindow(parts[2]) : PreferredWindow.any, ); } return map; } static PreferredWindow _parseWindow(String raw) { if (raw == 'any') return PreferredWindow.any; final ends = raw.split('-'); if (ends.length != 2) return PreferredWindow.any; return PreferredWindow( from: int.tryParse(ends[0]), to: int.tryParse(ends[1]), ); } static String _encodeWindow(PreferredWindow w) => w.isAnyTime ? 'any' : '${w.from ?? ''}-${w.to ?? ''}'; void _persist(AvailabilityMap map) { _prefs.setStringList(_key, [ for (final e in map.entries) if (e.value.isSet) '${e.key}:${e.value.status.name}:${_encodeWindow(e.value.window)}', ]); state = map; } AvailabilityMap get _copy => {for (final e in state.entries) e.key: e.value}; DayPlan planFor(DateTime day) => state[dayKey(day)] ?? const DayPlan(); bool isDayOff(DateTime day) => !planFor(day).isSet; void setStatus(DateTime day, DayAvailability status) { final map = _copy; final key = dayKey(day); if (status == DayAvailability.unavailable) { map.remove(key); } else { // The window rides along, so a worker who flips to Unavailable and back // does not have to set their hours a second time. map[key] = (map[key] ?? const DayPlan()).copyWith(status: status); } _persist(map); } void setWindow(DateTime day, PreferredWindow window) { final key = dayKey(day); final current = state[key]; // A window without a status to hang it on is not a day the worker has // answered, so there is nothing to store. if (current == null) return; final map = _copy..[key] = current.copyWith(window: window); _persist(map); } /// Presets across the visible week: all days, weekdays only, weekends only. /// Days outside the preset are cleared, which is what makes the four /// buttons read as four answers rather than four additive filters. void applyPreset( List week, { required bool Function(DateTime) test, }) { final map = _copy; for (final day in week) { final key = dayKey(day); if (test(day)) { map[key] = (map[key] ?? const DayPlan()).copyWith( status: DayAvailability.available, ); } else { map.remove(key); } } _persist(map); } void clearWeek(List week) { final map = _copy; for (final day in week) { map.remove(dayKey(day)); } _persist(map); } } final availabilityProvider = NotifierProvider( AvailabilityController.new, ); /// How many days in the coming week the worker has said yes to, either way. final daysSetProvider = Provider((ref) { final map = ref.watch(availabilityProvider); final now = DateTime.now(); var count = 0; for (var i = 0; i < 7; i++) { if (map[dayKey(now.add(Duration(days: i)))]?.isSet ?? false) count++; } return count; });