import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:hl_lieferservice/domain/entity/delivery.dart'; import 'package:hl_lieferservice/domain/entity/tour_details.dart'; import 'package:hl_lieferservice/feature/delivery/bloc/tour_bloc.dart'; import 'package:hl_lieferservice/feature/delivery/bloc/tour_event.dart'; import 'package:hl_lieferservice/feature/delivery/bloc/tour_state.dart'; import 'package:hl_lieferservice/feature/delivery/detail/presentation/delivery_sign.dart'; import 'package:hl_lieferservice/feature/delivery/detail/bloc/workflow_bloc.dart'; import 'package:hl_lieferservice/feature/delivery/detail/bloc/workflow_event.dart'; import 'package:hl_lieferservice/feature/delivery/detail/bloc/workflow_state.dart'; import 'package:hl_lieferservice/feature/delivery/detail/model/delivery_payment_status.dart'; import 'package:hl_lieferservice/feature/delivery/detail/presentation/steps/step_articles.dart'; import 'package:hl_lieferservice/feature/delivery/detail/presentation/steps/step_info.dart'; import 'package:hl_lieferservice/feature/delivery/detail/presentation/steps/step_notes.dart'; import 'package:hl_lieferservice/feature/delivery/detail/presentation/steps/step_payment.dart'; import 'package:hl_lieferservice/feature/delivery/detail/presentation/steps/step_services.dart'; import 'package:hl_lieferservice/feature/delivery/detail/presentation/steps/step_summary.dart'; // ───────────────────────────────────────────────────────────────────────── // Backend-TODOs (siehe Roadmap unten in diesem File) // ───────────────────────────────────────────────────────────────────────── // // Die folgenden UI-Features sind aktuell als lokaler Stub im // `DeliveryWorkflowBloc` gebaut. Sie persistieren nichts am Server und // gehen verloren, sobald die Detail-Page geschlossen wird: // // * **B1 Bild-Notizen**: Foto-Upload-Endpoint fehlt. Domain-Feld // `DeliveryNote.image_attachment` existiert bereits als // Storage-Key — wir brauchen einen Endpoint, der Multipart-Upload // entgegennimmt und den Key zurückgibt, dann hier // `tourBloc.add(AddDeliveryNote(imageAttachment: key))` aufrufen. // // * ~~B4 Zahlungsmethode beim Abschluss ändern~~ — ERLEDIGT und abgelöst: // Die Zahlung wird im eigenen Step „Zahlung" abgewickelt und am Server // protokolliert (`POST /deliveries/{id}/payment`). Beim Abschluss reist // die Methode der gültigen Zahlung mit. // // * **B5 Unterschrift**: Signature-Pad-Bilder (Kunde + Fahrer) // hochladen + auf der Lieferung speichern. Backend hat dafür weder // Felder noch Endpoint. Explizit „nach der Session" verschoben. // // * **B6 Notiz-Templates**: Stammdaten (vordefinierte Notiz-Texte // zum Auswählen). Im alten Stand schon UI-seitig im NoteAddDialog // vorbereitet; aktuell zeigen wir nur das freie Textfeld. // // * **B7 `completeDelivery` im Frontend**: Repository-Methode + // Bloc-Event fehlt. Backend-Endpoint existiert (parameterlos). // Trigger: „Unterschreiben"-Button im Summary-Step — derzeit // SnackBar-Stub. // ───────────────────────────────────────────────────────────────────────── /// Multi-Step Detail-Page einer einzelnen Lieferung. Hülle für 6 Steps; /// jeder Step bekommt die aktuelle `Delivery` + `TourDetails` als Props, /// damit die Steps keine eigenen Bloc-Subscriptions auf die Tour brauchen. /// /// Lifetime: die Page bringt ihren eigenen `DeliveryWorkflowBloc` mit /// (siehe `BlocProvider` unten). Beim Pop wird der Bloc samt Drafts /// disposed — gewollt, denn ein neuer Besuch der Detail-Page startet /// frisch im Step „Info". class DeliveryDetail extends StatelessWidget { const DeliveryDetail({super.key, required this.deliveryId}); final String deliveryId; @override Widget build(BuildContext context) { return BlocProvider( create: (_) => DeliveryWorkflowBloc(deliveryId: deliveryId), child: _DeliveryDetailScaffold(deliveryId: deliveryId), ); } } class _DeliveryDetailScaffold extends StatefulWidget { const _DeliveryDetailScaffold({required this.deliveryId}); final String deliveryId; @override State<_DeliveryDetailScaffold> createState() => _DeliveryDetailScaffoldState(); } class _DeliveryDetailScaffoldState extends State<_DeliveryDetailScaffold> { /// „Gearmt" = die Lieferung war während dieser Page-Session aktiv. Nur dann /// poppen wir bei `completed` automatisch zurück zur Übersicht. Öffnet der /// Fahrer eine bereits abgeschlossene Lieferung, bleibt `_armed == false` /// und die Page bleibt offen (kein ungewolltes Zurückspringen). bool _armed = false; bool _popped = false; @override void initState() { super.initState(); final s = context.read().state; if (s is TourLoaded && _findDelivery(s.details)?.state == DeliveryState.active) { _armed = true; } } Delivery? _findDelivery(TourDetails details) { for (final d in details.deliveries) { if (d.id == widget.deliveryId) return d; } return null; } /// Nach erfolgreichem Abschluss (aktiv → completed) zurück zur Übersicht. void _onTourState(BuildContext context, TourState state) { if (_popped || state is! TourLoaded) return; final d = _findDelivery(state.details); if (d == null) return; if (d.state == DeliveryState.active) { _armed = true; } else if (_armed && d.state == DeliveryState.completed) { _popped = true; Navigator.of(context).pop(); } } @override Widget build(BuildContext context) { final theme = Theme.of(context); return BlocListener( listener: _onTourState, child: BlocBuilder( builder: (context, tourState) { if (tourState is! TourLoaded) { return const Scaffold( body: Center(child: CircularProgressIndicator()), ); } final details = tourState.details; final delivery = _findDelivery(details); if (delivery == null) { return Scaffold( appBar: AppBar(title: const Text('Lieferung')), body: Center( child: Text( 'Lieferung ${widget.deliveryId} nicht in der Tour gefunden.', ), ), ); } // Titel = Empfänger vor Ort, wie in den Listen-Kacheln. final recipient = details.recipientOf(delivery); return Scaffold( appBar: AppBar( backgroundColor: theme.primaryColor, foregroundColor: theme.colorScheme.onPrimary, title: Text(recipient.name), ), body: Column( children: [ _StepHeader(delivery: delivery, details: details), const Divider(height: 1), Expanded( child: _StepBody(delivery: delivery, details: details), ), const Divider(height: 1), _BottomNav(delivery: delivery, details: details), ], ), ); }, ), ); } } // ─── Step-Header (Pills) ──────────────────────────────────────────────── /// Steps hinter „Zahlung" sind gesperrt, solange bei einer aktiven /// Lieferung ein offener Betrag noch nicht gültig abgewickelt ist. bool _lockedByPayment( WorkflowStep step, Delivery delivery, TourDetails details, ) { if (step.index <= WorkflowStep.payment.index) return false; if (delivery.state != DeliveryState.active) return false; return !DeliveryPaymentStatus.of(delivery, details).isSettled; } class _StepHeader extends StatelessWidget { const _StepHeader({required this.delivery, required this.details}); final Delivery delivery; final TourDetails details; @override Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { return Padding( padding: const EdgeInsets.fromLTRB(8, 10, 8, 10), child: Row( children: [ for (int i = 0; i < WorkflowStep.values.length; i++) ...[ Expanded( child: _StepPill( index: i, step: WorkflowStep.values[i], isActive: state.step.index == i, isPassed: state.step.index > i, enabled: !_lockedByPayment( WorkflowStep.values[i], delivery, details, ), onTap: () => context.read().add( WorkflowGoToStep(WorkflowStep.values[i]), ), ), ), if (i < WorkflowStep.values.length - 1) _StepConnector(isPassed: state.step.index > i), ], ], ), ); }, ); } } class _StepPill extends StatelessWidget { const _StepPill({ required this.index, required this.step, required this.isActive, required this.isPassed, required this.enabled, required this.onTap, }); final int index; final WorkflowStep step; final bool isActive; final bool isPassed; /// `false` = Step gesperrt (z. B. Übersicht vor abgewickelter Zahlung). final bool enabled; final VoidCallback onTap; @override Widget build(BuildContext context) { final theme = Theme.of(context); final primary = theme.colorScheme.primary; final Color circleColor; final Color circleFg; final Color labelColor; final FontWeight labelWeight; if (isActive) { circleColor = primary; circleFg = theme.colorScheme.onPrimary; labelColor = primary; labelWeight = FontWeight.w700; } else if (isPassed) { circleColor = primary.withValues(alpha: 0.85); circleFg = theme.colorScheme.onPrimary; labelColor = primary; labelWeight = FontWeight.w600; } else { circleColor = theme.colorScheme.surfaceContainerHighest; circleFg = theme.colorScheme.onSurfaceVariant; labelColor = theme.colorScheme.onSurfaceVariant; labelWeight = FontWeight.w500; } return InkWell( onTap: enabled ? onTap : null, borderRadius: BorderRadius.circular(8), child: Opacity( opacity: enabled ? 1 : 0.4, child: Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Column( mainAxisSize: MainAxisSize.min, children: [ Stack( alignment: Alignment.center, children: [ Container( width: 30, height: 30, decoration: BoxDecoration( color: circleColor, shape: BoxShape.circle, border: isActive ? Border.all(color: primary, width: 2) : null, ), child: Center( child: isPassed && !isActive ? Icon(Icons.check, color: circleFg, size: 16) : Text( '${index + 1}', style: TextStyle( color: circleFg, fontWeight: FontWeight.bold, fontSize: 13, ), ), ), ), ], ), const SizedBox(height: 4), Text( step.shortName, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( color: labelColor, fontSize: 11, fontWeight: labelWeight, ), ), ], ), ), ), ); } } class _StepConnector extends StatelessWidget { const _StepConnector({required this.isPassed}); final bool isPassed; @override Widget build(BuildContext context) { final theme = Theme.of(context); return Padding( padding: const EdgeInsets.only(bottom: 18), child: Container( width: 10, height: 2, color: isPassed ? theme.colorScheme.primary : theme.colorScheme.surfaceContainerHighest, ), ); } } // ─── Step-Body Router ─────────────────────────────────────────────────── class _StepBody extends StatelessWidget { const _StepBody({required this.delivery, required this.details}); final Delivery delivery; final TourDetails details; @override Widget build(BuildContext context) { return BlocBuilder( buildWhen: (prev, curr) => prev.step != curr.step, builder: (context, state) { switch (state.step) { case WorkflowStep.info: return StepInfo(delivery: delivery, details: details); case WorkflowStep.notes: return StepNotes(delivery: delivery, details: details); case WorkflowStep.articles: return StepArticles(delivery: delivery, details: details); case WorkflowStep.services: return StepServices(delivery: delivery, details: details); case WorkflowStep.payment: return StepPayment(delivery: delivery, details: details); case WorkflowStep.summary: return StepSummary(delivery: delivery, details: details); } }, ); } } // ─── Bottom-Navigation ────────────────────────────────────────────────── class _BottomNav extends StatelessWidget { const _BottomNav({required this.delivery, required this.details}); final Delivery delivery; final TourDetails details; /// Öffnet den zweistufigen Unterschrift-Flow (Kunde → Fahrer). Erst nach /// beiden Unterschriften triggert die View den Backend-Abschluss via /// `CompleteDelivery`; danach poppt sie zurück auf die Detail-Page, die /// dann den `completed`-Status zeigt. void _onSign(BuildContext context) { final tourBloc = context.read(); // Kassiert wurde bereits im Step „Zahlung" (serverseitig protokolliert). // Beim Abschluss reisen Methode und Inkasso-Flag der gültigen Zahlung // mit; ohne offenen Betrag bleibt die Methode vom Beleg. final payment = DeliveryPaymentStatus.of(delivery, details).validPayment; Navigator.of(context).push( MaterialPageRoute( builder: (routeContext) => SignatureView( delivery: delivery, details: details, onSigned: (result) { tourBloc.add( CompleteDelivery( deliveryId: delivery.id, customerSignaturePng: result.customerSignaturePng, driverSignaturePng: result.driverSignaturePng, receiptConfirmed: result.receiptConfirmed, notesAcknowledged: result.notesAcknowledged, acknowledgedNoteIds: result.acknowledgedNoteIds, paymentMethodId: payment?.paymentMethodId, paymentCollected: payment?.isCollectedOnSite ?? false, internalNote: result.internalNote, ), ); Navigator.of(routeContext).pop(); }, ), ), ); } @override Widget build(BuildContext context) { return SafeArea( top: false, child: BlocBuilder( builder: (context, state) { final isFirst = state.step.index == 0; final isLast = state.step.index == WorkflowStep.values.length - 1; return Padding( padding: const EdgeInsets.fromLTRB(16, 10, 16, 10), child: Row( children: [ OutlinedButton.icon( onPressed: isFirst ? null : () => context.read().add( const WorkflowPreviousStep(), ), icon: const Icon(Icons.arrow_back), label: const Text('Zurück'), ), const Spacer(), if (isLast) // Unterschreiben/Abschließen nur bei aktiver Lieferung. // Ist sie bereits abgeschlossen (oder pausiert/abgebrochen), // bleibt der Button gesperrt. Builder( builder: (context) { final isActive = delivery.state == DeliveryState.active; final isCompleted = delivery.state == DeliveryState.completed; return FilledButton.icon( onPressed: isActive ? () => _onSign(context) : null, icon: Icon( isCompleted ? Icons.check_circle_outline : Icons.draw_outlined, ), label: Text( isCompleted ? 'Abgeschlossen' : 'Unterschreiben', ), ); }, ) else FilledButton.icon( // Aus „Zahlung" erst weiter, wenn abgewickelt/nichts offen. onPressed: _lockedByPayment( WorkflowStep.values[state.step.index + 1], delivery, details, ) ? null : () => context.read().add( const WorkflowNextStep(), ), icon: const Icon(Icons.arrow_forward), label: const Text('Weiter'), ), ], ), ); }, ), ); } }