import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import '../app.dart' show materialLocaleFor; import '../data/variety_repository.dart'; import '../domain/crop_calendar.dart'; import '../i18n/strings.g.dart'; import 'category_palette.dart'; import 'edge_fade.dart'; import 'theme.dart'; /// One crop-calendar phase as shown on the "this month" screen: its label, /// which month mask on a [CalendarEntry] it reads, an icon and a tint. class _Phase { const _Phase(this.label, this.maskOf, this.icon, this.color); final String Function(Translations t) label; final int? Function(CalendarEntry e) maskOf; final IconData icon; final Color color; } // Actions first (sow / transplant / harvest seed), then the informational // phases (flowering / fruiting) — "qué toca hacer" above "qué está pasando". const _phases = <_Phase>[ _Phase(_sowLabel, _sowMask, Icons.grass, Color(0xFF2F7D34)), _Phase(_transplantLabel, _transplantMask, Icons.spa, Color(0xFF3E7168)), _Phase(_harvestLabel, _harvestMask, Icons.grain, Color(0xFF8A6D1E)), _Phase(_floweringLabel, _floweringMask, Icons.local_florist, Color(0xFF9B5566)), _Phase(_fruitingLabel, _fruitingMask, Icons.eco, Color(0xFF9C5B3B)), ]; String _sowLabel(Translations t) => t.cropCalendar.sow; String _transplantLabel(Translations t) => t.cropCalendar.transplant; String _harvestLabel(Translations t) => t.cropCalendar.seedHarvest; String _floweringLabel(Translations t) => t.cropCalendar.flowering; String _fruitingLabel(Translations t) => t.cropCalendar.fruiting; int? _sowMask(CalendarEntry e) => e.sowMonths; int? _transplantMask(CalendarEntry e) => e.transplantMonths; int? _harvestMask(CalendarEntry e) => e.seedHarvestMonths; int? _floweringMask(CalendarEntry e) => e.floweringMonths; int? _fruitingMask(CalendarEntry e) => e.fruitingMonths; /// "What's due this month" across the whole inventory: a month strip plus the /// varieties whose recorded calendar has an action in the picked month, grouped /// by phase. Reuses the per-variety calendar data — no schema of its own. class CalendarScreen extends StatefulWidget { const CalendarScreen({this.initialMonth, super.key}); /// The month (1..12) to open on; defaults to the current month. Injected in /// tests so they don't depend on the wall clock. final int? initialMonth; @override State createState() => _CalendarScreenState(); } class _CalendarScreenState extends State { late int _month = widget.initialMonth ?? DateTime.now().month; @override Widget build(BuildContext context) { final t = context.t; final repo = context.read(); return Scaffold( appBar: AppBar(title: Text(t.calendar.title)), body: Column( children: [ _MonthStrip( month: _month, onSelect: (m) => setState(() => _month = m), ), const Divider(height: 1), Expanded( child: StreamBuilder>( stream: repo.watchCalendar(), builder: (context, snapshot) { final entries = snapshot.data ?? const []; final sections = []; for (final phase in _phases) { final matches = [ for (final e in entries) if (maskHasMonth(phase.maskOf(e), _month)) e, ]; if (matches.isEmpty) continue; sections.add(_PhaseGroup(phase: phase, entries: matches)); } if (sections.isEmpty) { return _EmptyMonth(monthName: _monthName(context, _month)); } return ListView( padding: const EdgeInsets.only(bottom: 24), children: sections, ); }, ), ), ], ), ); } } /// A horizontally scrolling strip of the twelve months; the picked one is /// filled. Scrolls the picked month into view (so late-year months aren't left /// off-screen when the screen opens in, say, November). class _MonthStrip extends StatefulWidget { const _MonthStrip({required this.month, required this.onSelect}); final int month; final ValueChanged onSelect; @override State<_MonthStrip> createState() => _MonthStripState(); } class _MonthStripState extends State<_MonthStrip> { final _selectedKey = GlobalKey(); @override void initState() { super.initState(); _revealSelected(); } @override void didUpdateWidget(_MonthStrip old) { super.didUpdateWidget(old); if (old.month != widget.month) _revealSelected(); } void _revealSelected() { WidgetsBinding.instance.addPostFrameCallback((_) { final ctx = _selectedKey.currentContext; if (ctx != null) { Scrollable.ensureVisible(ctx, alignment: 0.5, duration: const Duration(milliseconds: 250)); } }); } @override Widget build(BuildContext context) { final locale = materialLocaleFor(Localizations.localeOf(context)); final fmt = DateFormat.MMM(locale.toLanguageTag()); return SizedBox( height: 56, child: EdgeFade( child: ListView.separated( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), itemCount: 12, separatorBuilder: (_, _) => const SizedBox(width: 8), itemBuilder: (context, i) { final m = i + 1; final selected = m == widget.month; return ChoiceChip( key: Key('calendar.month.$m'), labelPadding: const EdgeInsets.symmetric(horizontal: 6), label: Text( _capitalise(fmt.format(DateTime(2000, m))), key: selected ? _selectedKey : null, ), selected: selected, onSelected: (_) => widget.onSelect(m), selectedColor: seedGreen, labelStyle: TextStyle( color: selected ? Colors.white : seedMuted, fontWeight: selected ? FontWeight.w600 : FontWeight.w400, ), ); }, ), ), ); } } class _PhaseGroup extends StatelessWidget { const _PhaseGroup({required this.phase, required this.entries}); final _Phase phase; final List entries; @override Widget build(BuildContext context) { final t = context.t; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.fromLTRB(16, 18, 16, 6), child: Row( children: [ Icon(phase.icon, size: 20, color: phase.color), const SizedBox(width: 8), Text( phase.label(t), style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, color: phase.color, ), ), const SizedBox(width: 6), Text('· ${entries.length}', style: const TextStyle(color: seedMuted)), ], ), ), for (final e in entries) _CalendarRow(entry: e), ], ); } } class _CalendarRow extends StatelessWidget { const _CalendarRow({required this.entry}); final CalendarEntry entry; @override Widget build(BuildContext context) { final swatch = categorySwatch(entry.category ?? ''); return ListTile( key: Key('calendar.entry.${entry.id}'), leading: CircleAvatar( radius: 20, backgroundColor: swatch.fill, foregroundImage: entry.photo == null ? null : MemoryImage(entry.photo!), child: entry.photo == null ? Text( entry.label.isEmpty ? '?' : entry.label.substring(0, 1).toUpperCase(), style: TextStyle(color: swatch.ink, fontWeight: FontWeight.w600), ) : null, ), title: Text(entry.label), subtitle: entry.category == null ? null : Text(entry.category!), onTap: () => context.push('/variety/${entry.id}'), ); } } class _EmptyMonth extends StatelessWidget { const _EmptyMonth({required this.monthName}); final String monthName; @override Widget build(BuildContext context) { final t = context.t; return Center( child: Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.event_available, size: 64, color: seedGreen.withValues(alpha: 0.5)), const SizedBox(height: 16), Text( t.calendar.nothing(month: monthName), textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyLarge, ), ], ), ), ); } } String _monthName(BuildContext context, int month) { final locale = materialLocaleFor(Localizations.localeOf(context)); return _capitalise( DateFormat.MMMM(locale.toLanguageTag()).format(DateTime(2000, month))); } String _capitalise(String s) => s.isEmpty ? s : s[0].toUpperCase() + s.substring(1);