import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import '../data/variety_repository.dart'; import '../db/enums.dart'; import '../di/injector.dart'; import '../i18n/strings.g.dart'; import '../services/plantare_service.dart'; import 'plantare_sheet.dart'; import 'theme.dart'; /// The Plantares screen — your reproduction commitments (data-model §2.7). /// Lists what's open (still to grow out & return) and what's done, and lets you /// add or settle one. Local-first; reads the encrypted inventory DB. class PlantaresScreen extends StatelessWidget { const PlantaresScreen({super.key}); @override Widget build(BuildContext context) { final t = context.t; final repo = context.read(); final service = getIt.isRegistered() ? getIt() : null; return Scaffold( appBar: AppBar(title: Text(t.plantare.title)), floatingActionButton: FloatingActionButton.extended( key: const Key('plantares.add'), onPressed: () => showPlantareSheet(context, repository: repo), icon: const Icon(Icons.add), label: Text(t.plantare.add), ), body: StreamBuilder>( stream: repo.watchPlantareViews(), builder: (context, snapshot) { final all = snapshot.data; if (all == null) { return const Center(child: CircularProgressIndicator()); } if (all.isEmpty) { return Center( child: Padding( padding: const EdgeInsets.all(32), child: Text( t.plantare.empty, textAlign: TextAlign.center, style: const TextStyle(color: seedMuted, fontSize: 15), ), ), ); } final open = all.where((v) => v.plantare.status == PlantareStatus.open); final settled = all.where((v) => v.plantare.status != PlantareStatus.open); return ListView( padding: const EdgeInsets.only(bottom: 88), children: [ if (open.isNotEmpty) _SectionHeader(t.plantare.openSection), for (final v in open) _PlantareTile(v, repo, service), if (settled.isNotEmpty) _SectionHeader(t.plantare.settledSection), for (final v in settled) _PlantareTile(v, repo, service), ], ); }, ), ); } } /// A small chip for the bilateral handshake state — "awaiting signature", /// "signed by both", or "declined". Absent for a plain v1 local note. class _RemoteStateBadge extends StatelessWidget { const _RemoteStateBadge(this.state); final PlantareRemoteState state; @override Widget build(BuildContext context) { final t = context.t; final (label, color, icon) = switch (state) { PlantareRemoteState.proposed => ( t.plantare.badgeAwaiting, seedMuted, Icons.hourglass_empty, ), PlantareRemoteState.accepted => ( t.plantare.badgeSigned, seedGreen, Icons.verified_outlined, ), PlantareRemoteState.declined => ( t.plantare.badgeDeclined, seedWarning, Icons.do_not_disturb_on_outlined, ), }; return Padding( padding: const EdgeInsets.only(top: 4), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 14, color: color), const SizedBox(width: 4), Text( label, style: TextStyle( color: color, fontSize: 12, fontWeight: FontWeight.w600), ), ], ), ); } } class _SectionHeader extends StatelessWidget { const _SectionHeader(this.label); final String label; @override Widget build(BuildContext context) => Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 4), child: Text( label.toUpperCase(), style: const TextStyle( color: seedMuted, fontSize: 12, fontWeight: FontWeight.w600, letterSpacing: 0.5, ), ), ); } /// One commitment row. An open one shows a big "mark returned"; a settled one /// reads muted. It names the seed and (when linked) taps through to its detail, /// shows when it was made and — for a promise with a return-by date — whether /// it's still ahead or overdue. The overflow menu carries the rest (let go / /// reopen / remove). class _PlantareTile extends StatelessWidget { const _PlantareTile(this.view, this.repo, this.service); final PlantareView view; final VarietyRepository repo; final PlantareService? service; /// A proposal is "awaiting me" (I still have to accept/decline) when the /// handshake is `proposed` and MY stub is missing — my slot is the debtor one /// if I'm the one who returns, else the creditor one. bool get _awaitingMe { final p = view.plantare; if (p.remoteState != PlantareRemoteState.proposed) return false; final mySig = p.direction == PlantareDirection.iReturn ? p.debtorSignature : p.creditorSignature; return mySig == null; } Future _accept(BuildContext context) async { final id = view.plantare.pledgeId; if (service == null || id == null) return; final t = context.t; try { await service!.accept(id); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(t.plantare.acceptedToast)), ); } } catch (_) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(t.plantare.offline)), ); } } } Future _decline(BuildContext context) async { final id = view.plantare.pledgeId; if (service == null || id == null) return; await service!.decline(id); } @override Widget build(BuildContext context) { final t = context.t; final p = view.plantare; final varietyLabel = view.varietyLabel; final done = p.status != PlantareStatus.open; final iReturn = p.direction == PlantareDirection.iReturn; final title = (p.owedDescription?.trim().isNotEmpty ?? false) ? p.owedDescription!.trim() : (iReturn ? t.plantare.iReturn : t.plantare.owedToMe); final locals = MaterialLocalizations.of(context); final made = locals .formatShortDate(DateTime.fromMillisecondsSinceEpoch(p.madeOn)); final subtitleParts = [ iReturn ? t.plantare.iReturn : t.plantare.owedToMe, ?varietyLabel, if (p.counterparty?.trim().isNotEmpty ?? false) p.counterparty!.trim(), made, if (done) p.status == PlantareStatus.returned ? t.plantare.statusReturned : t.plantare.statusForgiven, ]; // A return-by date is a gentle nudge, not a debt: only an open, past-due // promise reads as overdue. final overdue = !done && p.dueBy != null && p.dueBy! < DateTime.now().millisecondsSinceEpoch; final returnBy = p.dueBy == null ? null : t.plantare.returnBy( date: locals.formatShortDate( DateTime.fromMillisecondsSinceEpoch(p.dueBy!), ), ); final linkedId = varietyLabel == null ? null : p.varietyId; return ListTile( onTap: linkedId == null ? null : () => context.push('/variety/$linkedId'), leading: Icon( iReturn ? Icons.volunteer_activism_outlined : Icons.eco_outlined, color: done ? seedMuted : seedGreen, ), title: Text( title, style: TextStyle( decoration: done ? TextDecoration.lineThrough : null, color: done ? seedMuted : seedOnSurface, ), ), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(subtitleParts.join(' · '), maxLines: 2, overflow: TextOverflow.ellipsis), if (returnBy != null) Text( overdue ? '$returnBy · ${t.plantare.overdue}' : returnBy, style: TextStyle( color: overdue ? seedWarning : seedMuted, fontWeight: overdue ? FontWeight.w600 : null, ), ), if (p.remoteState != null) _RemoteStateBadge(p.remoteState!), // An incoming proposal awaiting my signature: sign it or turn it down, // right here. if (_awaitingMe && service != null) Padding( padding: const EdgeInsets.only(top: 6), child: Row( children: [ FilledButton.icon( key: Key('plantare.accept.${p.id}'), onPressed: () => _accept(context), icon: const Icon(Icons.draw_outlined, size: 18), label: Text(t.plantare.accept), ), const SizedBox(width: 8), TextButton( key: Key('plantare.decline.${p.id}'), onPressed: () => _decline(context), child: Text(t.plantare.declineAction), ), ], ), ), ], ), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ // A returned/mark button only makes sense once the deal is agreed // (or for a plain local note). Hide it while a proposal is pending. if (!done && !_awaitingMe) IconButton( key: Key('plantare.return.${p.id}'), icon: const Icon(Icons.check_circle_outline), tooltip: t.plantare.markReturned, color: seedGreen, onPressed: () => repo.setPlantareStatus(p.id, PlantareStatus.returned), ), PopupMenuButton( key: Key('plantare.menu.${p.id}'), onSelected: (v) async { switch (v) { case 'forgiven': await repo.setPlantareStatus(p.id, PlantareStatus.forgiven); case 'reopen': await repo.setPlantareStatus(p.id, PlantareStatus.open); case 'delete': await repo.deletePlantare(p.id); } }, itemBuilder: (context) => [ if (!done) PopupMenuItem( value: 'forgiven', child: Text(t.plantare.markForgiven)), if (done) PopupMenuItem( value: 'reopen', child: Text(t.plantare.reopen)), PopupMenuItem(value: 'delete', child: Text(t.plantare.delete)), ], ), ], ), ); } }