tane/apps/app_seeds/lib/ui/app_drawer.dart
vjrj 44337497d0 feat(calendar): "what's due this month" — dedicated screen + inventory filter
Reuses the existing per-variety crop-calendar masks (sow/transplant/
flowering/fruiting/seed-harvest) — no schema change, no migration — to
answer "qué toca este mes" across the whole inventory. Since the months
are the grower's own record, there's no hemisphere assumption.

- Repo: CalendarEntry + watchCalendar() (varieties with any recorded
  phase); VarietyListItem now carries sowMonths.
- Dedicated CalendarScreen (/calendar): a month strip (defaults to the
  current month, intl month names) and varieties grouped by phase —
  actions (sow/transplant/harvest seed) above informational (flowering/
  fruiting), each tinted, tap → variety detail; empty-month state.
- Inventory filter: a 'this month' chip (shown only when some variety
  has a calendar) keeps what sows in the current month; folded into the
  attributes group + clear-filters.
- Entry points: a live Home card + a drawer item.
- i18n calendar block + menu.calendar (en/es/pt/ast).
- Tests: calendar_test (watchCalendar, list carries sow-months, pure
  filter), calendar_screen_test (phase grouping, month switch, empty),
  plus the screen added to the small-screen overflow guard.
2026-07-11 06:27:57 +02:00

240 lines
7.4 KiB
Dart

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../i18n/strings.g.dart';
import 'seed_glyph.dart';
import 'theme.dart';
import 'unread_badge.dart';
/// The app's navigation drawer (redesign screen 05). A white sheet: Inventory is
/// the live destination (green seed glyph), the social items (market, profile,
/// chat…) belong to Block 2 and are greyed with a "soon" tag, and Settings sits
/// pinned at the bottom.
class AppDrawer extends StatelessWidget {
const AppDrawer({this.marketEnabled = false, super.key});
/// When the Block 2 social layer is wired, the Market becomes a live drawer
/// destination; other social items stay "soon" until they're built.
final bool marketEnabled;
@override
Widget build(BuildContext context) {
final t = context.t;
return Drawer(
child: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const _DrawerHeader(),
_DrawerItem(
icon: const SeedGlyph(SeedGlyphs.jars, size: 23),
label: t.menu.inventory,
onTap: () {
Navigator.of(context).pop();
context.push('/inventory');
},
),
// Local, works offline — a reproduction-commitment ledger.
_DrawerItem(
icon: const Icon(Icons.volunteer_activism_outlined),
label: t.menu.plantares,
onTap: () {
Navigator.of(context).pop();
context.push('/plantares');
},
),
// Local sales ledger — also offline.
_DrawerItem(
icon: const Icon(Icons.sell_outlined),
label: t.menu.sales,
onTap: () {
Navigator.of(context).pop();
context.push('/sales');
},
),
// "What's due this month" — the aggregate crop calendar, offline.
_DrawerItem(
icon: const Icon(Icons.calendar_month),
label: t.menu.calendar,
onTap: () {
Navigator.of(context).pop();
context.push('/calendar');
},
),
_DrawerItem(
icon: const Icon(Symbols.storefront),
label: t.menu.market,
divider: true,
onTap: marketEnabled
? () {
Navigator.of(context).pop();
context.push('/market');
}
: null,
),
_DrawerItem(
icon: const Icon(Icons.person),
label: t.menu.profile,
onTap: marketEnabled
? () {
Navigator.of(context).pop();
context.push('/profile');
}
: null,
),
_DrawerItem(
icon: const UnreadBadge(child: Icon(Icons.chat_bubble)),
label: t.menu.chat,
onTap: marketEnabled
? () {
Navigator.of(context).pop();
context.push('/messages');
}
: null,
),
_DrawerItem(
icon: const Icon(Icons.favorite),
label: t.menu.wishlist,
),
_DrawerItem(icon: const Icon(Icons.group), label: t.menu.following),
const Spacer(),
const Divider(height: 1),
_DrawerItem(
icon: const Icon(Icons.auto_stories_outlined),
label: t.intro.menuEntry,
onTap: () {
Navigator.of(context).pop();
context.push('/intro');
},
),
_DrawerItem(
icon: const Icon(Icons.settings),
label: t.menu.settings,
onTap: () {
Navigator.of(context).pop();
context.push('/settings');
},
),
_DrawerItem(
icon: const Icon(Icons.info_outline),
label: t.about.title,
onTap: () {
Navigator.of(context).pop();
context.push('/about');
},
),
],
),
),
);
}
}
/// The drawer's brand header. Tapping it closes the drawer and returns to the
/// home screen.
class _DrawerHeader extends StatelessWidget {
const _DrawerHeader();
@override
Widget build(BuildContext context) {
return InkWell(
onTap: () {
Navigator.of(context).pop();
context.go('/');
},
child: Container(
padding: const EdgeInsets.fromLTRB(22, 18, 22, 16),
decoration: const BoxDecoration(
border: Border(bottom: BorderSide(color: seedDivider)),
),
child: Row(
children: [
Image.asset('assets/logo.png', height: 34),
const SizedBox(width: 12),
Text(
context.t.app.title,
style: const TextStyle(
color: seedGreen,
fontSize: 22,
fontWeight: FontWeight.w700,
letterSpacing: -0.2,
),
),
],
),
),
);
}
}
/// A 52-tall drawer row. A live destination has an [onTap] (dark text, green
/// icon); a disabled Block-2 destination is greyed with a "soon" tag. [divider]
/// draws a hairline under the row (separating live from upcoming items).
class _DrawerItem extends StatelessWidget {
const _DrawerItem({
required this.icon,
required this.label,
this.onTap,
this.divider = false,
});
final Widget icon;
final String label;
final VoidCallback? onTap;
final bool divider;
@override
Widget build(BuildContext context) {
final enabled = onTap != null;
final fg = enabled ? seedOnSurface : const Color(0xFF9AA88F);
final iconColor = enabled ? seedGreen : const Color(0xFF9AA88F);
final row = InkWell(
onTap: onTap,
child: Container(
height: 52,
padding: const EdgeInsets.symmetric(horizontal: 22),
decoration: divider
? const BoxDecoration(
border: Border(bottom: BorderSide(color: seedDivider)),
)
: null,
child: Row(
children: [
SizedBox(
width: 24,
child: Center(
child: IconTheme.merge(
data: IconThemeData(size: 23, color: iconColor),
child: icon,
),
),
),
const SizedBox(width: 18),
Expanded(
child: Text(
label,
style: TextStyle(
color: fg,
fontSize: 15,
fontWeight: enabled ? FontWeight.w500 : FontWeight.w400,
),
),
),
if (!enabled)
Text(
context.t.common.comingSoon.toUpperCase(),
style: const TextStyle(
color: Color(0xFFB3BDA8),
fontSize: 11,
fontWeight: FontWeight.w500,
letterSpacing: 0.5,
),
),
],
),
),
);
return row;
}
}