wake-alarm/phone_app/lib/screens/sick_days_body.dart
Krzysztof kuhy Rudnicki 34037e55c1 phone: sick days — picker, silent ring, stick stand-down, pre-alarm sync
- Sick days screen (own name, sick icon, cat-2 fill): tap today or a later
  day to mark it sick, tap again to clear; past days inert. Writes the
  sickdays wire format (state sick/cleared + actor, one HLC per write) to
  sickdays-sync/devices via crdt_sync syncLog.
- Mutual exclusion in both pickers: each syncs the other pool on open and
  refuses a date it holds, naming the pool; an unreadable pool refuses all.
- Native SickDays cache (fail closed via DayCache); AlarmReceiver skips the
  ring with reason "sick_day" (no markFired, no ring_started); StickCheck
  verdict sick_day, streak skips sick days; stakes card says so.
- Pre-alarm pool sync: allow-while-idle alarm at 21:00 and A-30 (request
  code 1006) runs a headless engine (poolSyncMain) that pulls both pools
  from RTDB and rewrites both native caches, so a PC mark arrives with the
  app closed. The ring decision stays a cache read.
- JVM unit tests (JUnit 4.13.2) for DayCache, RingDecision,
  PoolSyncSchedule, StickCheck; Dart tests; Dart->Python interop check.

pylint hook skipped: verify.py imports sickdays, not in the hook env until
the sickdays-v0.1.0 tag exists; run by hand in a sickdays venv (10.00/10).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xfj1sxKH9VBPqpcAni8Jry
2026-10-16 13:22:40 +02:00

85 lines
2.9 KiB
Dart

import 'package:flutter/material.dart';
import 'package:wake_alarm_sync/screens/day_calendar.dart';
import 'package:wake_alarm_sync/services/free_day_pool.dart';
import 'package:wake_alarm_sync/ui/theme.dart';
/// The sick-day picker's body: what a sick day does, the status line, the
/// month calendar and the sick days still ahead.
///
/// Stateless and fed plain values, like `FreeDaysBody`: the screen owns the
/// pool and the sync ordering, this only draws. No count and no budget --
/// sick days have none.
class SickDaysBody extends StatelessWidget {
/// Creates the body from already-resolved state.
const SickDaysBody({
required this.upcoming,
required this.status,
required this.refused,
required this.calendar,
super.key,
});
/// The month grid, built by the screen, which owns the displayed month.
final DayCalendar calendar;
/// Sick days from today on, ascending.
final List<DateTime> upcoming;
/// The status line; may be a raw `Error: …`.
final String status;
/// Whether [status] is a refused tap, drawn like an error.
final bool refused;
static const _weekdays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
String _label(DateTime day) =>
'${_weekdays[day.weekday - 1]} ${FreeDayPool.toIso(day)}';
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final muted = theme.colorScheme.onSurfaceVariant;
final statusColors = theme.extension<AppStatusColors>()!;
final failed = refused || status.contains('Error');
return SafeArea(
child: ListView(
padding: const EdgeInsets.all(16),
children: [
Text(
'Tap a day to mark it sick; tap it again to clear it. Today or '
'later only. No limit and no reason needed.',
style: theme.textTheme.bodyMedium,
),
const SizedBox(height: 8),
Text(
'On a sick day the alarm stays silent, nothing locks, and work '
'left undone is dropped rather than carried forward.',
style: theme.textTheme.bodySmall?.copyWith(color: muted),
),
if (status.isNotEmpty) ...[
const SizedBox(height: 16),
Text(
status,
maxLines: 4,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: failed ? theme.colorScheme.error : statusColors.success,
),
),
],
const SizedBox(height: 16),
calendar,
const SizedBox(height: 24),
Text('Marked sick', style: theme.textTheme.titleMedium),
const SizedBox(height: 8),
if (upcoming.isEmpty)
Text('None ahead.', style: TextStyle(color: muted)),
for (final day in upcoming)
ListTile(contentPadding: EdgeInsets.zero, title: Text(_label(day))),
],
),
);
}
}