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

@ -125,6 +125,17 @@ class ContactSource {
return parts.join(' ');
}
/// Nur der Namensblock `Name1..3` ohne Anrede/Titel, so wie der
/// Kundenname im Listen-Kopf erscheint. `null` ohne Namen.
String? get fullName {
final parts = <String>[
for (final n in [name1, name2, name3])
if (n != null && n.trim().isNotEmpty) n.trim(),
];
if (parts.isEmpty) return null;
return parts.join(' ');
}
/// Funktionale Zusatzinfo (z. B. „Buchhaltung · Leitung"). Leere
/// Komponenten werden ausgeblendet.
String? get subtitle {

View File

@ -0,0 +1,35 @@
import 'address.dart';
import 'contact_source.dart';
/// Wer die Ware vor Ort annimmt und wohin geliefert wird.
///
/// Im ERP kann ein Beleg neben der Belegadresse (= Besteller, Kunde) eine
/// eigene Lieferadresse (`Belegkopf.LieferAdressId`) tragen, z. B. wenn ein
/// Kunde für Angehörige bestellt. Dann unterscheiden sich Empfänger und
/// Besteller in Person und/oder Anschrift. Fahrer navigieren und klingeln
/// beim Empfänger; Zahlung und Beleg laufen weiter über den Besteller.
///
/// Gebaut über `TourDetails.recipientOf`.
class DeliveryRecipient {
const DeliveryRecipient({
required this.name,
required this.address,
required this.differsFromOrderer,
this.contact,
});
/// Name der Person/Firma vor Ort. Ohne eigenen Namen an der Lieferadresse
/// der Name des Bestellers.
final String name;
/// Anschrift, an die geliefert wird (Lieferadress-Snapshot des Belegs).
final Address address;
/// Kontaktquelle der Lieferadresse (Rolle `delivery`), falls das ERP
/// dort Namen oder Kontaktkanäle führt.
final ContactSource? contact;
/// `true`, wenn Person oder Anschrift vom Besteller abweichen. Die UI
/// weist dann auf den Besteller hin.
final bool differsFromOrderer;
}

View File

@ -1,9 +1,11 @@
import 'address.dart';
import 'article.dart';
import 'contact_source.dart';
import 'customer.dart';
import 'delivery.dart';
import 'delivery_credit.dart';
import 'delivery_payment.dart';
import 'delivery_recipient.dart';
import 'delivery_item.dart';
import 'delivery_note.dart';
import 'delivery_service_value.dart';
@ -97,6 +99,52 @@ class TourDetails {
Customer? customerOf(Delivery delivery) => customers[delivery.customerId];
/// Empfänger vor Ort: Name und Kontakt aus der ERP-Lieferadresse, sonst
/// der Besteller. [DeliveryRecipient.differsFromOrderer] ist gesetzt,
/// sobald Anschrift oder Name vom Besteller abweichen.
///
/// Der Namensvergleich läuft ERP-Adresse gegen ERP-Adresse (Lieferadresse
/// vs. Belegadresse, jeweils `Name1..3`), damit ein nur in `Name2`
/// geführter Vorname keinen Fehlalarm auslöst. Groß-/Kleinschreibung und
/// Leerzeichen zählen nicht als Abweichung.
DeliveryRecipient recipientOf(Delivery delivery) {
final customer = customerOf(delivery);
final deliverySource = contactSourceWithRole(delivery, ContactRole.delivery);
final ordererName = contactSourceWithRole(delivery, ContactRole.header)?.fullName ??
customer?.name ??
'';
final recipientName = deliverySource?.fullName;
final addressDiffers = customer != null &&
!_sameAddress(delivery.deliveryAddressSnapshot, customer.address);
final nameDiffers = recipientName != null &&
_normalized(recipientName) != _normalized(ordererName);
return DeliveryRecipient(
name: recipientName ?? customer?.name ?? '⟨Unbekannter Kunde⟩',
address: delivery.deliveryAddressSnapshot,
contact: deliverySource,
differsFromOrderer: addressDiffers || nameDiffers,
);
}
/// Erste Adress-Quelle einer Lieferung mit der Rolle [role], oder `null`.
ContactSource? contactSourceWithRole(Delivery delivery, ContactRole role) {
for (final s in contactSourcesOf(delivery)) {
if (s.role == role) return s;
}
return null;
}
static String _normalized(String value) =>
value.trim().toLowerCase().replaceAll(RegExp(r'\s+'), ' ');
static bool _sameAddress(Address a, Address b) =>
_normalized(a.street) == _normalized(b.street) &&
_normalized(a.houseNumber) == _normalized(b.houseNumber) &&
_normalized(a.postalCode) == _normalized(b.postalCode) &&
_normalized(a.city) == _normalized(b.city);
Iterable<CustomerContact> contactsOf(Delivery delivery) sync* {
for (final id in delivery.contactPersonIds) {
final c = contacts[id];