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/delivery_item.dart'; import 'package:hl_lieferservice/domain/entity/scan_progress.dart'; import 'package:hl_lieferservice/domain/entity/tour_details.dart'; import 'package:hl_lieferservice/feature/car_selection/bloc/bloc.dart'; import 'package:hl_lieferservice/feature/car_selection/bloc/state.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/detail/presentation/widget/discount_editor.dart'; import 'package:hl_lieferservice/feature/loading/widget/reason_catalog.dart'; import 'package:hl_lieferservice/feature/loading/widget/reason_picker_sheet.dart'; /// Step 3 — Artikel & Gutschriften. /// /// * **Mengen-Gutschrift** (Belegzeile ganz ODER teilweise entfernen) ist /// voll funktional über `RemoveItem`/`UnremoveItem` (mit `quantity`) → /// Backend Audit-Action `remove`/`unremove`. Die `creditedQuantity` des /// Items ist die Wahrheit (vom Server), kein lokaler Draft mehr. /// Regeln (serverseitig erzwungen): scannbare Position muss `done` sein, /// Lieferung muss `active` sein. Die UI spiegelt das als Gate + Hinweis. /// * **Betrags-Gutschrift** (≤ 150 € in 10er-Schritten + Grund) ist /// backend-gestützt über `SetDeliveryCredit`/`RemoveDeliveryCredit` am /// `TourBloc` → `POST /deliveries/{id}/credit`. Wahrheit ist der Server /// (`TourDetails.creditOf`), kein lokaler Draft mehr. class StepArticles extends StatelessWidget { const StepArticles({ super.key, required this.delivery, required this.details, }); final Delivery delivery; final TourDetails details; @override Widget build(BuildContext context) { // Innerhalb einer Belegzeile: Oberartikel vor seinen Komponenten, damit // Komponenten direkt darunter eingerückt erscheinen. final items = List.of(delivery.items) ..sort((a, b) { final byLine = a.belegzeilenNr.compareTo(b.belegzeilenNr); if (byLine != 0) return byLine; final byParent = (a.isComponent ? 1 : 0).compareTo(b.isComponent ? 1 : 0); if (byParent != 0) return byParent; return (a.komponentenArtikelNr ?? '') .compareTo(b.komponentenArtikelNr ?? ''); }); return ListView( padding: const EdgeInsets.fromLTRB(16, 16, 16, 24), children: [ _SectionHeader(text: 'Artikel'), const SizedBox(height: 8), if (delivery.state != DeliveryState.active) ...[ const _LockedHint( text: 'Nur bei aktiver Lieferung änderbar.', ), const SizedBox(height: 8), ], if (items.isEmpty) const _EmptyHint(text: 'Keine Artikel hinterlegt.') else Card( margin: EdgeInsets.zero, child: Column( children: [ for (int i = 0; i < items.length; i++) ...[ _ArticleManagementRow( item: items[i], details: details, deliveryId: delivery.id, deliveryActive: delivery.state == DeliveryState.active, ), if (i < items.length - 1) const Divider(height: 1, indent: 16, endIndent: 16), ], ], ), ), const SizedBox(height: 24), _SectionHeader(text: 'Gutschriften'), const SizedBox(height: 8), Card( margin: EdgeInsets.zero, child: Padding( padding: const EdgeInsets.all(16), child: DiscountEditor( deliveryId: delivery.id, active: delivery.state == DeliveryState.active, ), ), ), ], ); } } class _SectionHeader extends StatelessWidget { const _SectionHeader({required this.text}); final String text; @override Widget build(BuildContext context) { return Text( text, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w700, ), ); } } class _EmptyHint extends StatelessWidget { const _EmptyHint({required this.text}); final String text; @override Widget build(BuildContext context) { final theme = Theme.of(context); return Card( margin: EdgeInsets.zero, child: Padding( padding: const EdgeInsets.all(16), child: Text( text, style: TextStyle( color: theme.colorScheme.onSurfaceVariant, fontStyle: FontStyle.italic, ), ), ), ); } } /// Eine Zeile in der Artikel-Verwaltung. Quelle der Wahrheit ist /// `item.scanProgress.creditedQuantity` (vom Backend) — kein lokaler Draft. /// Zeigt: /// - verbleibende Liefermenge (Soll − Gutschrift) /// - Gutschrift-Button → Mengen-Dialog (1…Restmenge + Grund), gesperrt für /// scannbare, noch nicht gescannte Positionen oder inaktive Lieferung /// - Wiederherstellen-Button, sobald etwas gutgeschrieben ist class _ArticleManagementRow extends StatelessWidget { const _ArticleManagementRow({ required this.item, required this.details, required this.deliveryId, required this.deliveryActive, }); final DeliveryItem item; final TourDetails details; final String deliveryId; final bool deliveryActive; Future _openCreditDialog( BuildContext context, { required int remaining, }) async { final tourBloc = context.read(); final actorCarId = _actorCarId(context); // Identische Auswahl wie im Beladen-/Scan-Screen: Grund-Picker // (Presets + „Anderer Grund") + Mengen-Stepper (Teilmengen-Gutschrift). final result = await showReasonPickerSheet( context: context, title: 'Grund für das Entfernen', presets: ReasonCatalog.itemRemove, confirmLabel: 'Entfernen', maxQuantity: remaining, ); if (result == null) return; // Eine Aktion für ganz UND teilweise: das Backend kippt die Zeile auf // `removed`, sobald die volle Menge gutgeschrieben ist. tourBloc.add(RemoveItem( deliveryItemId: item.id, reason: result.reason, actorCarId: actorCarId, quantity: result.quantity, // Gutschrift-Grund zusätzlich als Lieferungs-Notiz festhalten. saveReasonAsNote: true, )); } void _restoreAll(BuildContext context) { // quantity: null → gesamte Gutschrift zurücknehmen. context.read().add(UnremoveItem( deliveryItemId: item.id, actorCarId: _actorCarId(context), )); } /// Holt die aktuelle Auto-ID aus dem CarSelectBloc — gleiche Konvention /// wie der Loading-Flow: das aktiv gewählte Fahrzeug ist der „Akteur" /// des Audit-Events. Falls (defensiv) noch keine Auswahl getroffen ist, /// fallback auf einen Null-UUID-String, damit der Backend-Call nicht /// validation-failt. String _actorCarId(BuildContext context) { final state = context.read().state; if (state is CarSelectComplete) return state.selectedCar.id; return '00000000-0000-0000-0000-000000000000'; } @override Widget build(BuildContext context) { final theme = Theme.of(context); final article = details.articleOf(item.articleId); final warehouse = details.warehouseOf(item.warehouseId); final required = item.requiredQuantity; final credited = item.scanProgress.creditedQuantity; final remaining = required - credited; final fullyRemoved = item.isRemoved; // Status == removed (voll gutgeschr.) final partiallyCredited = credited > 0 && !fullyRemoved; // Gate: scannbare Position muss `done` sein, sonst keine Gutschrift. final scannable = article?.scannable ?? false; final isDone = item.scanProgress.status == ScanStatus.done; final blockedByScan = scannable && !isDone && !fullyRemoved; final canCredit = deliveryActive && !blockedByScan && remaining > 0; final Color avatarColor; final String avatarText; if (fullyRemoved) { avatarColor = Colors.red.shade400; avatarText = '0×'; } else if (partiallyCredited) { avatarColor = Colors.amber.shade700; avatarText = '$remaining×'; } else { avatarColor = theme.colorScheme.primary; avatarText = '$required×'; } return ListTile( // Komponenten um eine Stufe eingerückt (gehören zum Oberartikel darüber). contentPadding: EdgeInsets.only(left: item.isComponent ? 40 : 16, right: 16), leading: CircleAvatar( backgroundColor: avatarColor, foregroundColor: theme.colorScheme.onPrimary, child: Text( avatarText, style: const TextStyle(fontSize: 11, fontWeight: FontWeight.bold), ), ), title: Text( '${item.isComponent ? '↳ ' : ''}${article?.name ?? '⟨Unbekannter Artikel⟩'}', style: TextStyle( fontWeight: FontWeight.w600, decoration: fullyRemoved ? TextDecoration.lineThrough : null, color: fullyRemoved ? theme.colorScheme.onSurfaceVariant : null, ), ), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( [ article?.articleNumber ?? item.articleId, if (warehouse != null) warehouse.name, if (article?.scannable == false) 'Dienstleistung', ].join(' · '), style: TextStyle( fontSize: 12, color: theme.colorScheme.onSurfaceVariant, ), ), if (fullyRemoved) _StatusLine( text: 'Komplett gutgeschrieben' '${item.scanProgress.heldReason != null ? ' – ${item.scanProgress.heldReason}' : ''}', color: Colors.red.shade400, ) else if (partiallyCredited) _StatusLine( text: '$credited von $required gutgeschrieben', color: Colors.amber.shade800, ), if (blockedByScan) _StatusLine( text: 'Erst scannen/verladen — dann Gutschrift möglich', color: theme.colorScheme.onSurfaceVariant, ), ], ), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ if (credited > 0) IconButton( // Wiederherstellen nur bei aktiver Lieferung — bei // abgeschlossener/abgebrochener Lieferung gesperrt (greift auch // backend-seitig, hier zusätzlich in der UI). tooltip: deliveryActive ? 'Gutschrift zurücknehmen' : 'Nur bei aktiver Lieferung', icon: Icon( Icons.restore, color: deliveryActive ? theme.colorScheme.primary : theme.colorScheme.onSurfaceVariant, ), onPressed: deliveryActive ? () => _restoreAll(context) : null, ), if (!fullyRemoved) IconButton.outlined( tooltip: blockedByScan ? 'Erst scannen/verladen' : (!deliveryActive ? 'Nur bei aktiver Lieferung' : 'Gutschrift / entfernen'), style: ButtonStyle( backgroundColor: WidgetStatePropertyAll( canCredit ? Colors.redAccent : theme.colorScheme.surfaceContainerHighest, ), ), onPressed: canCredit ? () => _openCreditDialog(context, remaining: remaining) : null, icon: Icon( Icons.delete, color: canCredit ? theme.colorScheme.onPrimary : theme.colorScheme.onSurfaceVariant, ), ), ], ), ); } } /// Kleine farbige Statuszeile unter dem Artikelnamen. class _StatusLine extends StatelessWidget { const _StatusLine({required this.text, required this.color}); final String text; final Color color; @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(top: 2), child: Text( text, style: TextStyle( fontSize: 11, fontWeight: FontWeight.w600, color: color, ), ), ); } } /// Lock-Hinweis (analog DiscountEditor): zeigt mit Schloss-Symbol an, dass die /// Artikel-Aktionen (Entfernen / Wiederherstellen) nur bei aktiver Lieferung /// möglich sind. class _LockedHint extends StatelessWidget { const _LockedHint({required this.text}); final String text; @override Widget build(BuildContext context) { final theme = Theme.of(context); return Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: theme.colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(8), ), child: Row( children: [ Icon(Icons.lock_outline, size: 16, color: theme.colorScheme.onSurfaceVariant), const SizedBox(width: 8), Expanded( child: Text( text, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), ), ], ), ); } }