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/payment_method.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/payment_methods/bloc/payment_methods_cubit.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/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_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: Die im // Summary gewählte Methode (`paymentMethodOverrideId` im Workflow-State) // reist beim Abschluss am `/complete`-Endpoint mit und wird atomar auf // der Lieferung persistiert (Server prüft existiert + aktiv). // // * **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 5 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 StatelessWidget { const _DeliveryDetailScaffold({required this.deliveryId}); final String deliveryId; @override Widget build(BuildContext context) { final theme = Theme.of(context); return 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 $deliveryId nicht in der Tour gefunden.'), ), ); } final customer = details.customerOf(delivery); return Scaffold( appBar: AppBar( backgroundColor: theme.primaryColor, foregroundColor: theme.colorScheme.onPrimary, title: Text(customer?.name ?? 'Lieferung'), ), body: Column( children: [ const _StepHeader(), const Divider(height: 1), Expanded( child: _StepBody(delivery: delivery, details: details), ), const Divider(height: 1), _BottomNav(delivery: delivery, details: details), ], ), ); }, ); } Delivery? _findDelivery(TourDetails details) { for (final d in details.deliveries) { if (d.id == deliveryId) return d; } return null; } } // ─── Step-Header (Pills) ──────────────────────────────────────────────── class _StepHeader extends StatelessWidget { const _StepHeader(); @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, 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.onTap, }); final int index; final WorkflowStep step; final bool isActive; final bool isPassed; 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: onTap, borderRadius: BorderRadius.circular(8), 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.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(); // Die im Summary-Step gewählte Zahlungsmethode lebt im Workflow-State. // Beim Abschluss reisen wir sie mit ans Backend (atomar mit der Signatur); // `null` = die am Beleg hinterlegte Methode bleibt. final paymentMethodOverrideId = context.read().state.paymentMethodOverrideId; // Offener Betrag = Warenwert − Anzahlung − Gutschrift (≥ 0). EXAKT die // Formel aus StepSummary und dem Backend-Inkasso-Gate. final creditEuros = (details.creditOf(delivery.id)?.amountCents ?? 0) / 100.0; final warenwert = delivery.items.fold(0, (acc, item) => acc + item.lineTotal); final open = (warenwert - delivery.prepaidAmount - creditEuros) .clamp(0.0, double.infinity) .toDouble(); // Effektive Methode (Override > Beleg) auflösen, um Vor-Ort-Inkasso // (Bar/EC) von „Auf Rechnung" zu unterscheiden. final effectiveMethodId = paymentMethodOverrideId ?? delivery.paymentMethodId; final pmState = context.read().state; PaymentMethod? method; if (pmState is PaymentMethodsLoaded) { for (final m in pmState.methods) { if (m.id == effectiveMethodId) { method = m; break; } } } // Inkasso-Pflicht: offener Betrag > 0 UND Bar/EC. „Auf Rechnung" → nein. final requiresCollection = open > 0 && (method?.code == 'cash' || method?.code == 'ec_card'); Navigator.of(context).push( MaterialPageRoute( builder: (routeContext) => SignatureView( delivery: delivery, details: details, requiresCollection: requiresCollection, openAmount: open, paymentMethodLabel: method?.name ?? '', onSigned: (result) { tourBloc.add(CompleteDelivery( deliveryId: delivery.id, customerSignaturePng: result.customerSignaturePng, driverSignaturePng: result.driverSignaturePng, receiptConfirmed: result.receiptConfirmed, notesAcknowledged: result.notesAcknowledged, acknowledgedNoteIds: result.acknowledgedNoteIds, paymentMethodId: paymentMethodOverrideId, paymentCollected: result.paymentCollected, )); 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( onPressed: () => context .read() .add(const WorkflowNextStep()), icon: const Icon(Icons.arrow_forward), label: const Text('Weiter'), ), ], ), ); }, ), ); } }