import 'package:flutter/material.dart'; import '../db/enums.dart'; import '../i18n/strings.g.dart'; import '../services/plantare_service.dart'; import '../services/profile_cache.dart' show shortPubkey; import 'theme.dart'; /// Opens the "propose a signed Plantaré" sheet against a specific peer, whose /// [peerPubkey] is already in hand (the flow starts from a chat or a closed /// offer). Fills the terms, self-signs, and sends the proposal via /// [PlantareService]; the peer counter-signs to close it. Returns true if a /// proposal was sent. Future showProposePlantareSheet( BuildContext context, { required PlantareService service, required String peerPubkey, String? peerName, String? varietyId, String? seedLabel, }) { return showModalBottomSheet( context: context, isScrollControlled: true, builder: (_) => _ProposeSheet( service: service, peerPubkey: peerPubkey, peerName: peerName, varietyId: varietyId, seedLabel: seedLabel, ), ); } class _ProposeSheet extends StatefulWidget { const _ProposeSheet({ required this.service, required this.peerPubkey, this.peerName, this.varietyId, this.seedLabel, }); final PlantareService service; final String peerPubkey; final String? peerName; final String? varietyId; final String? seedLabel; @override State<_ProposeSheet> createState() => _ProposeSheetState(); } class _ProposeSheetState extends State<_ProposeSheet> { // Default: I gave the seed and they'll return some (I'm the creditor) — the // common "I'm sharing seed" case. PlantareDirection _direction = PlantareDirection.owedToMe; PlantareReturnKind _returnKind = PlantareReturnKind.similar; late final TextEditingController _seed = TextEditingController(text: widget.seedLabel ?? ''); final _owed = TextEditingController(); final _hours = TextEditingController(); DateTime? _dueBy; bool _saving = false; @override void dispose() { _seed.dispose(); _owed.dispose(); _hours.dispose(); super.dispose(); } String? _nullIfBlank(String s) => s.trim().isEmpty ? null : s.trim(); Future _pickDueBy() async { final now = DateTime.now(); final picked = await showDatePicker( context: context, initialDate: _dueBy ?? DateTime(now.year + 1, now.month, now.day), firstDate: DateTime(now.year - 1), lastDate: DateTime(now.year + 10), ); if (picked != null) setState(() => _dueBy = picked); } Future _send() async { final label = _seed.text.trim(); if (label.isEmpty) return; // the seed name is the one thing we need setState(() => _saving = true); await widget.service.propose( direction: _direction, counterpartyKey: widget.peerPubkey, counterpartyName: widget.peerName, varietyId: widget.varietyId, label: label, owedDescription: _nullIfBlank(_owed.text), returnKind: _returnKind, workHours: _returnKind == PlantareReturnKind.workHours ? double.tryParse(_hours.text.trim().replaceAll(',', '.')) : null, dueBy: _dueBy, ); if (mounted) Navigator.of(context).pop(true); } @override Widget build(BuildContext context) { final t = context.t; final peer = widget.peerName ?? shortPubkey(widget.peerPubkey); return Padding( padding: EdgeInsets.only( left: 16, right: 16, top: 16, bottom: MediaQuery.of(context).viewInsets.bottom + 16, ), child: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text(t.plantare.propose, style: Theme.of(context).textTheme.titleLarge), const SizedBox(height: 4), Text(t.plantare.proposeTo(name: peer), style: const TextStyle(color: seedGreen, fontSize: 13)), const SizedBox(height: 8), Text(t.plantare.proposeHelp, style: const TextStyle(color: seedMuted, fontSize: 13)), const SizedBox(height: 16), Text(t.plantare.direction, style: Theme.of(context).textTheme.labelLarge), const SizedBox(height: 6), for (final d in PlantareDirection.values) ListTile( key: Key('propose.direction.${d.name}'), contentPadding: EdgeInsets.zero, leading: Icon( _direction == d ? Icons.radio_button_checked : Icons.radio_button_unchecked, color: _direction == d ? seedGreen : seedMuted, ), title: Text(d == PlantareDirection.iReturn ? t.plantare.iReturn : t.plantare.owedToMe), onTap: () => setState(() => _direction = d), ), const SizedBox(height: 8), TextField( key: const Key('propose.seed'), controller: _seed, textCapitalization: TextCapitalization.sentences, decoration: InputDecoration( labelText: t.plantare.seedLabel, helperText: t.plantare.seedHint, border: const OutlineInputBorder(), ), ), const SizedBox(height: 16), Text(t.plantare.returnKindLabel, style: Theme.of(context).textTheme.labelLarge), const SizedBox(height: 6), _ReturnChoice( value: PlantareReturnKind.similar, group: _returnKind, title: t.plantare.returnSimilar, note: t.plantare.returnSimilarNote, onTap: (v) => setState(() => _returnKind = v), ), _ReturnChoice( value: PlantareReturnKind.workHours, group: _returnKind, title: t.plantare.returnWork, onTap: (v) => setState(() => _returnKind = v), ), _ReturnChoice( value: PlantareReturnKind.other, group: _returnKind, title: t.plantare.returnOther, onTap: (v) => setState(() => _returnKind = v), ), if (_returnKind == PlantareReturnKind.workHours) ...[ const SizedBox(height: 8), TextField( key: const Key('propose.hours'), controller: _hours, keyboardType: const TextInputType.numberWithOptions(decimal: true), decoration: InputDecoration( labelText: t.plantare.workHoursLabel, border: const OutlineInputBorder(), ), ), ], const SizedBox(height: 12), TextField( key: const Key('propose.owed'), controller: _owed, decoration: InputDecoration( labelText: t.plantare.owed, helperText: t.plantare.owedHint, helperMaxLines: 2, border: const OutlineInputBorder(), ), ), const SizedBox(height: 12), InputDecorator( decoration: InputDecoration( labelText: t.plantare.dueByLabel, helperText: t.plantare.dueByHint, border: const OutlineInputBorder(), ), child: Row( children: [ Expanded( child: Text( _dueBy == null ? t.plantare.pickDate : MaterialLocalizations.of(context) .formatShortDate(_dueBy!), style: TextStyle( color: _dueBy == null ? seedMuted : seedOnSurface, ), ), ), if (_dueBy != null) IconButton( icon: const Icon(Icons.clear, size: 20), tooltip: t.plantare.clearDate, onPressed: () => setState(() => _dueBy = null), ), IconButton( key: const Key('propose.dueBy.pick'), icon: const Icon(Icons.event_outlined), tooltip: t.plantare.pickDate, onPressed: _pickDueBy, ), ], ), ), const SizedBox(height: 20), FilledButton.icon( key: const Key('propose.send'), onPressed: _saving ? null : _send, icon: const Icon(Icons.draw_outlined), label: Text(t.plantare.propose), ), ], ), ), ); } } class _ReturnChoice extends StatelessWidget { const _ReturnChoice({ required this.value, required this.group, required this.title, required this.onTap, this.note, }); final PlantareReturnKind value; final PlantareReturnKind group; final String title; final String? note; final ValueChanged onTap; @override Widget build(BuildContext context) { final selected = value == group; return ListTile( key: Key('propose.return.${value.name}'), contentPadding: EdgeInsets.zero, leading: Icon( selected ? Icons.radio_button_checked : Icons.radio_button_unchecked, color: selected ? seedGreen : seedMuted, ), title: Text(title), subtitle: note == null ? null : Text(note!, style: const TextStyle(color: seedMuted, fontSize: 12)), onTap: () => onTap(value), ); } }