feat: abweichende Lieferadresse und Empfänger vor Ort anzeigen

Belege können im ERP eine eigene Lieferadresse (LieferAdressId) mit
anderer Person tragen als der Besteller. Die App zeigt jetzt überall den
Empfänger vor Ort:

- TourDetails.recipientOf liefert Empfänger (Name aus der Lieferadresse,
  sonst Besteller), Lieferanschrift und differsFromOrderer.
- Sortieren, Auswählen, Beladen und Liefer-Übersicht: Empfängername +
  Lieferadresse, bei Abweichung „Bestellt von: <Kunde>".
- Lieferdetails: hervorgehobene Karte „Abweichende Lieferadresse" mit
  Empfänger, Anschrift und Kontakt; Kunden-Block als „Besteller (Kunde)"
  mit Adresse, Kundennummer und Kontaktdaten.
- Maps navigiert zur Lieferadresse statt zur Kundenadresse; AppBar,
  Unterschrift und Beladen-Avatar nutzen den Empfänger.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Dennis Nemec
2026-09-25 15:51:31 +02:00
parent 99247b4b67
commit 857c92031a
14 changed files with 600 additions and 61 deletions

View File

@ -23,6 +23,7 @@ import 'package:hl_lieferservice/widget/scanner/article_scanner_stripe.dart';
import 'package:hl_lieferservice/widget/scanner/item_matcher.dart';
import 'package:hl_lieferservice/widget/scanner/manual_entry_dialog.dart';
import 'package:hl_lieferservice/widget/scanner/scan_code_parser.dart';
import 'package:hl_lieferservice/widget/orderer_hint.dart';
/// Vollbild-Sicht eines Kunden in der Beladen-Phase mit aktivem Scanner.
///
@ -523,6 +524,7 @@ class _CustomerBody extends StatelessWidget {
@override
Widget build(BuildContext context) {
final customer = details.customerOf(delivery);
final recipient = details.recipientOf(delivery);
// Items werden vom Aggregat-Helper schon nach Lager gruppiert
// geliefert: Standardlager zuerst, danach Filiale alphabetisch.
// Nicht-scanbare Positionen und `removed`-Items sind dabei schon
@ -611,7 +613,7 @@ class _CustomerBody extends StatelessWidget {
// Kunden" wird dadurch räumlich klar.
child: Row(
children: [
_CustomerAvatar(customer: customer),
_CustomerAvatar(customer: customer, name: recipient.name),
const SizedBox(width: 12),
Expanded(
child: Column(
@ -630,14 +632,18 @@ class _CustomerBody extends StatelessWidget {
),
const SizedBox(height: 2),
Text(
customer?.name ?? '⟨Unbekannter Kunde⟩',
recipient.name,
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 4),
Text(
delivery.deliveryAddressSnapshot.oneLine,
recipient.address.oneLine,
style: Theme.of(context).textTheme.bodyMedium,
),
if (recipient.differsFromOrderer)
OrdererHint(
ordererName: customer?.name ?? '⟨Unbekannter Kunde⟩',
),
if (delivery.state != DeliveryState.active) ...[
const SizedBox(height: 8),
_DeliveryStateBadge(delivery: delivery),
@ -987,12 +993,13 @@ class _NothingToLoadHint extends StatelessWidget {
///
/// Farbe ist deterministisch aus der `customer.id` abgeleitet, damit
/// Re-Builds und Page-Wechsel den Avatar nicht „flackern" lassen.
/// Initialen kommen aus dem Namen — Vor- und Zunamen kombiniert,
/// einzelne Worte mit einem Buchstaben.
/// Initialen kommen aus dem angezeigten Namen (Empfänger vor Ort) — Vor-
/// und Zunamen kombiniert, einzelne Worte mit einem Buchstaben.
class _CustomerAvatar extends StatelessWidget {
const _CustomerAvatar({required this.customer});
const _CustomerAvatar({required this.customer, required this.name});
final Customer? customer;
final String name;
/// Kleine kuratierte Palette — kräftig genug zum Erkennen, aber nicht
/// schreiend. Reihenfolge ist Absicht: die ersten Farben fallen am
@ -1009,9 +1016,9 @@ class _CustomerAvatar extends StatelessWidget {
];
String get _initials {
final name = customer?.name.trim() ?? '';
if (name.isEmpty) return '?';
final parts = name
final trimmed = name.trim();
if (trimmed.isEmpty) return '?';
final parts = trimmed
.split(RegExp(r'\s+'))
.where((p) => p.isNotEmpty)
.toList(growable: false);