feat(delivery): Abschluss-Navigation, Mengen-Hinweis, Set-Handling
A) Nach erfolgreichem Abschluss (aktiv→completed) poppt die Detail-Page automatisch zurück zur Übersicht. Scaffold ist jetzt StatefulWidget mit BlocListener<TourBloc>; nur „gearmt", wenn die Lieferung beim Öffnen aktiv war → erneutes Öffnen einer fertigen Lieferung poppt nicht. B) Step „Info": Artikelliste zeigt weiter die Ursprungsmenge (requiredQuantity). Bei entfernten/teilweise gutgeschriebenen Positionen erscheint pro Zeile ein „Menge geändert"-Hinweis + ein tappbares Banner, das zu Step 3 „Artikel" springt. C) Beladen: nicht-scanbare Set-Köpfe (Parent-Komponenten) werden jetzt IMMER mit ihrem Set gezeigt — als Kopf in der Lagergruppe ihrer Komponenten statt isoliert unter „Dienstleistungen". _ItemRow leitet scanNotRequired aus der Artikel-Scanbarkeit ab. D) Step „Übersicht": Wording der Zahlungsweise-Sperre bei offen==0 präzisiert („Keine Zahlung mehr offen (bereits bezahlt)"). E) Step „Artikel": Komponenten eines Sets sind einzeln nicht mehr entfernbar (kein Button + Hinweis). Das Entfernen/Wiederherstellen läuft nur über den Oberartikel und kaskadiert auf das ganze Set (ganz oder gar nix). Set-Entfernen ist blockiert, solange eine Komponente noch nicht verladen ist. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -49,6 +49,24 @@ class StepArticles extends StatelessWidget {
|
||||
return (a.komponentenArtikelNr ?? '')
|
||||
.compareTo(b.komponentenArtikelNr ?? '');
|
||||
});
|
||||
|
||||
// Komponenten je Oberartikel (Artikelnummer des Parents → Komponenten).
|
||||
// Grundlage für E: Komponenten sind einzeln NICHT entfernbar — nur der
|
||||
// Oberartikel kann (ganzes Set) entfernt werden, was auf die Komponenten
|
||||
// kaskadiert.
|
||||
final componentsByParentNr = <String, List<DeliveryItem>>{};
|
||||
for (final it in items) {
|
||||
final pNr = it.parentArtikelNr;
|
||||
if (it.isComponent && pNr != null) {
|
||||
componentsByParentNr.putIfAbsent(pNr, () => []).add(it);
|
||||
}
|
||||
}
|
||||
List<DeliveryItem> componentsOf(DeliveryItem it) {
|
||||
final nr = details.articleOf(it.articleId)?.articleNumber;
|
||||
if (nr == null) return const [];
|
||||
return componentsByParentNr[nr] ?? const [];
|
||||
}
|
||||
|
||||
return ListView(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
|
||||
children: [
|
||||
@ -73,6 +91,7 @@ class StepArticles extends StatelessWidget {
|
||||
details: details,
|
||||
deliveryId: delivery.id,
|
||||
deliveryActive: delivery.state == DeliveryState.active,
|
||||
components: componentsOf(items[i]),
|
||||
),
|
||||
if (i < items.length - 1)
|
||||
const Divider(height: 1, indent: 16, endIndent: 16),
|
||||
@ -149,6 +168,7 @@ class _ArticleManagementRow extends StatelessWidget {
|
||||
required this.details,
|
||||
required this.deliveryId,
|
||||
required this.deliveryActive,
|
||||
this.components = const [],
|
||||
});
|
||||
|
||||
final DeliveryItem item;
|
||||
@ -156,6 +176,10 @@ class _ArticleManagementRow extends StatelessWidget {
|
||||
final String deliveryId;
|
||||
final bool deliveryActive;
|
||||
|
||||
/// Komponenten dieses Items, falls es ein Set-Oberartikel ist (sonst leer).
|
||||
/// Entfernen/Wiederherstellen kaskadiert auf diese Komponenten.
|
||||
final List<DeliveryItem> components;
|
||||
|
||||
Future<void> _openCreditDialog(
|
||||
BuildContext context, {
|
||||
required int remaining,
|
||||
@ -193,6 +217,51 @@ class _ArticleManagementRow extends StatelessWidget {
|
||||
));
|
||||
}
|
||||
|
||||
/// Entfernt das GANZE Set (Oberartikel + alle Komponenten) auf einmal —
|
||||
/// „entweder Parent komplett oder gar nix". Kein Mengen-Stepper
|
||||
/// (`maxQuantity: null` ⇒ jede Zeile komplett).
|
||||
Future<void> _openSetRemoveDialog(BuildContext context) async {
|
||||
final tourBloc = context.read<TourBloc>();
|
||||
final actorCarId = _actorCarId(context);
|
||||
final result = await showReasonPickerSheet(
|
||||
context: context,
|
||||
title: 'Grund für das Entfernen (ganzes Set)',
|
||||
presets: ReasonCatalog.itemRemove,
|
||||
confirmLabel: 'Set entfernen',
|
||||
maxQuantity: null,
|
||||
);
|
||||
if (result == null) return;
|
||||
// Oberartikel + jede Komponente komplett entfernen (quantity null).
|
||||
tourBloc.add(RemoveItem(
|
||||
deliveryItemId: item.id,
|
||||
reason: result.reason,
|
||||
actorCarId: actorCarId,
|
||||
quantity: null,
|
||||
saveReasonAsNote: true,
|
||||
));
|
||||
for (final c in components) {
|
||||
tourBloc.add(RemoveItem(
|
||||
deliveryItemId: c.id,
|
||||
reason: result.reason,
|
||||
actorCarId: actorCarId,
|
||||
quantity: null,
|
||||
// Grund nur einmal (am Oberartikel) als Notiz festhalten.
|
||||
saveReasonAsNote: false,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Stellt das ganze Set wieder her (Oberartikel + Komponenten).
|
||||
void _restoreSet(BuildContext context) {
|
||||
final tourBloc = context.read<TourBloc>();
|
||||
final actorCarId = _actorCarId(context);
|
||||
tourBloc.add(UnremoveItem(deliveryItemId: item.id, actorCarId: actorCarId));
|
||||
for (final c in components) {
|
||||
tourBloc
|
||||
.add(UnremoveItem(deliveryItemId: c.id, actorCarId: actorCarId));
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
@ -216,11 +285,27 @@ class _ArticleManagementRow extends StatelessWidget {
|
||||
final fullyRemoved = item.isRemoved; // Status == removed (voll gutgeschr.)
|
||||
final partiallyCredited = credited > 0 && !fullyRemoved;
|
||||
|
||||
// E: Komponenten eines Sets sind einzeln NICHT entfernbar; nur der
|
||||
// Oberartikel kann (ganzes Set) entfernt werden.
|
||||
final isComponent = item.isComponent;
|
||||
final hasComponents = components.isNotEmpty;
|
||||
|
||||
// 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;
|
||||
// Beim Set zusätzlich: jede scanbare Komponente muss erst verladen sein,
|
||||
// sonst würde das Backend deren Entfernen ablehnen.
|
||||
final componentsBlocked = components.any((c) {
|
||||
final cArticle = details.articleOf(c.articleId);
|
||||
final cScannable = cArticle?.scannable ?? false;
|
||||
final cDone = c.scanProgress.status == ScanStatus.done;
|
||||
return cScannable && !cDone && !c.isRemoved;
|
||||
});
|
||||
final canCredit = deliveryActive &&
|
||||
!blockedByScan &&
|
||||
!componentsBlocked &&
|
||||
remaining > 0;
|
||||
|
||||
final Color avatarColor;
|
||||
final String avatarText;
|
||||
@ -285,53 +370,83 @@ class _ArticleManagementRow extends StatelessWidget {
|
||||
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,
|
||||
// E: Komponenten-Hinweis — einzeln nicht entfernbar.
|
||||
if (isComponent && deliveryActive && !fullyRemoved)
|
||||
_StatusLine(
|
||||
text: 'Nur über den Oberartikel entfernbar',
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
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,
|
||||
),
|
||||
// Set-Oberartikel: blockiert, solange eine Komponente noch nicht
|
||||
// verladen ist.
|
||||
if (hasComponents &&
|
||||
componentsBlocked &&
|
||||
deliveryActive &&
|
||||
!fullyRemoved)
|
||||
_StatusLine(
|
||||
text: 'Erst alle Set-Teile verladen — dann ganzes Set entfernbar',
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
],
|
||||
),
|
||||
// E: Komponenten haben KEINE eigenen Aktionen — Entfernen/Wiederherstellen
|
||||
// läuft ausschließlich über den Oberartikel (kaskadiert auf das Set).
|
||||
trailing: isComponent
|
||||
? null
|
||||
: 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
|
||||
? (hasComponents
|
||||
? 'Set wiederherstellen'
|
||||
: 'Gutschrift zurücknehmen')
|
||||
: 'Nur bei aktiver Lieferung',
|
||||
icon: Icon(
|
||||
Icons.restore,
|
||||
color: deliveryActive
|
||||
? theme.colorScheme.primary
|
||||
: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
onPressed: deliveryActive
|
||||
? () => hasComponents
|
||||
? _restoreSet(context)
|
||||
: _restoreAll(context)
|
||||
: null,
|
||||
),
|
||||
if (!fullyRemoved)
|
||||
IconButton.outlined(
|
||||
tooltip: (blockedByScan || componentsBlocked)
|
||||
? 'Erst scannen/verladen'
|
||||
: (!deliveryActive
|
||||
? 'Nur bei aktiver Lieferung'
|
||||
: (hasComponents
|
||||
? 'Ganzes Set entfernen'
|
||||
: 'Gutschrift / entfernen')),
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStatePropertyAll(
|
||||
canCredit
|
||||
? Colors.redAccent
|
||||
: theme.colorScheme.surfaceContainerHighest,
|
||||
),
|
||||
),
|
||||
onPressed: canCredit
|
||||
? () => hasComponents
|
||||
? _openSetRemoveDialog(context)
|
||||
: _openCreditDialog(context, remaining: remaining)
|
||||
: null,
|
||||
icon: Icon(
|
||||
Icons.delete,
|
||||
color: canCredit
|
||||
? theme.colorScheme.onPrimary
|
||||
: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user