Files
Holzleitner-Lieferservice-App/lib/domain/entity/contact_source.dart
Dennis Nemec 857c92031a 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>
2026-09-25 15:51:31 +02:00

238 lines
7.3 KiB
Dart

/// Adress-Rolle eines Beleg-Kontakts. Spiegelt die fünf Adress-FKs am
/// ERP-`Belegkopf` (bzw. den Umweg über `Kunden.AdressId`). Die App nutzt
/// das primär als Gruppierungs-Label in der Detail-Ansicht.
enum ContactRole {
/// `Belegkopf.AdressId` — die „eigentliche" Belegadresse.
header,
/// `Belegkopf.LieferAdressId` — kann von der Belegadresse abweichen.
delivery,
/// `Belegkopf.RechnungsAdressId`.
billing,
/// `Belegkopf.AnsprechpartnerId` — verlinkt eine Person, nicht eine Firma.
contactPerson,
/// `Kunden.AdressId` (über `Belegkopf.KundenId`). Die Stammadresse des
/// Kunden — dient als Fallback, wenn die belegspezifischen Adressen leer
/// sind.
customerMaster;
/// Wire-Repräsentation aus dem Backend (serde `snake_case`).
static ContactRole fromWire(String value) {
switch (value) {
case 'header':
return ContactRole.header;
case 'delivery':
return ContactRole.delivery;
case 'billing':
return ContactRole.billing;
case 'contact_person':
return ContactRole.contactPerson;
case 'customer_master':
return ContactRole.customerMaster;
default:
throw StateError('Unbekannte ContactRole vom Backend: $value');
}
}
/// Deutscher Label-Text für die UI.
String get label {
switch (this) {
case ContactRole.header:
return 'Belegadresse';
case ContactRole.delivery:
return 'Lieferadresse';
case ContactRole.billing:
return 'Rechnungsadresse';
case ContactRole.contactPerson:
return 'Ansprechpartner';
case ContactRole.customerMaster:
return 'Kundenstamm';
}
}
}
/// Art eines Kommunikationskanals. `fax` wird vom Backend bewusst nicht
/// mitgeführt — die App braucht es nicht.
enum ContactKind {
phone,
mobile,
email,
web;
static ContactKind fromWire(String value) {
switch (value) {
case 'phone':
return ContactKind.phone;
case 'mobile':
return ContactKind.mobile;
case 'email':
return ContactKind.email;
case 'web':
return ContactKind.web;
default:
throw StateError('Unbekannter ContactKind vom Backend: $value');
}
}
}
/// Eine Adress-Quelle, die am Beleg hängt — z. B. Lieferadresse oder
/// Ansprechpartner. Der Namensblock kommt direkt aus ERP-`Adressen`
/// (`Anrede`/`Titel`/`Name1..3`/`Abteilung`/`Funktion`); die eigentlichen
/// Telefonnummern, E-Mails etc. liegen verteilt in zugehörigen
/// [ContactChannel]s und werden in [TourDetails.channelsOf] zusammengeführt.
class ContactSource {
const ContactSource({
required this.id,
required this.deliveryId,
required this.role,
this.anrede,
this.titel,
this.name1,
this.name2,
this.name3,
this.abteilung,
this.funktion,
});
final String id;
final String deliveryId;
final ContactRole role;
final String? anrede;
final String? titel;
final String? name1;
final String? name2;
final String? name3;
final String? abteilung;
final String? funktion;
/// Zusammengesetzte Anzeige des Namens — Anrede + Titel + Name1..3 in
/// dieser Reihenfolge, leere Felder werden übersprungen. Gibt `null`
/// zurück, wenn die Quelle gar keinen Namen trägt (kann vorkommen, wenn
/// nur Telefonnummern hinterlegt sind).
String? get displayName {
final parts = <String>[
if (anrede != null && anrede!.isNotEmpty) anrede!,
if (titel != null && titel!.isNotEmpty) titel!,
if (name1 != null && name1!.isNotEmpty) name1!,
if (name2 != null && name2!.isNotEmpty) name2!,
if (name3 != null && name3!.isNotEmpty) name3!,
];
if (parts.isEmpty) return null;
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 {
final parts = <String>[
if (abteilung != null && abteilung!.isNotEmpty) abteilung!,
if (funktion != null && funktion!.isNotEmpty) funktion!,
];
if (parts.isEmpty) return null;
return parts.join(' · ');
}
}
/// Ein einzelner Kommunikationskanal (Telefon, Mobil, E-Mail, Web). Mehrere
/// pro [ContactSource] möglich; die [position] (1-basiert) erhält die
/// ERP-Reihenfolge — Position 1 ist der primäre Kanal, Position 2 das
/// erste Zusatzfeld usw.
class ContactChannel {
const ContactChannel({
required this.id,
required this.sourceId,
required this.kind,
required this.position,
required this.value,
});
final String id;
final String sourceId;
final ContactKind kind;
final int position;
final String value;
}
/// Zusammengeführte Sicht auf 1..n [ContactSource]s, die fachlich denselben
/// Kontakt darstellen — gleicher Namensblock UND gleiche Channel-Liste
/// (also exakt dieselbe Adresse im ERP, nur über verschiedene FKs am
/// `Belegkopf` referenziert: typischerweise `AdressId` und `Kunden.AdressId`,
/// die in den allermeisten Belegen identisch sind).
///
/// Die App rendert pro Lieferung eine Karte je Eintrag; `roles` listet
/// alle Rollen auf, die zu diesem Eintrag beitragen (z. B. „Belegadresse ·
/// Kundenstamm"). Die Channels werden 1:1 von der ersten Quelle übernommen
/// — alle Quellen in einer Gruppe haben dieselben.
class MergedContactSource {
const MergedContactSource({
required this.roles,
required this.anrede,
required this.titel,
required this.name1,
required this.name2,
required this.name3,
required this.abteilung,
required this.funktion,
required this.channels,
});
/// Alle Rollen, die diesen zusammengeführten Kontakt liefern.
/// Reihenfolge wie in der enum-Definition: header → delivery → billing
/// → contactPerson → customerMaster, damit das Label stabil bleibt.
final List<ContactRole> roles;
final String? anrede;
final String? titel;
final String? name1;
final String? name2;
final String? name3;
final String? abteilung;
final String? funktion;
/// Channels in der gleichen Reihenfolge, wie das Backend sie pro Quelle
/// liefert (kind + ERP-Position).
final List<ContactChannel> channels;
/// Zusammengesetzter Anzeigename — identisch zu [ContactSource.displayName].
String? get displayName {
final parts = <String>[
if (anrede != null && anrede!.isNotEmpty) anrede!,
if (titel != null && titel!.isNotEmpty) titel!,
if (name1 != null && name1!.isNotEmpty) name1!,
if (name2 != null && name2!.isNotEmpty) name2!,
if (name3 != null && name3!.isNotEmpty) name3!,
];
if (parts.isEmpty) return null;
return parts.join(' ');
}
String? get subtitle {
final parts = <String>[
if (abteilung != null && abteilung!.isNotEmpty) abteilung!,
if (funktion != null && funktion!.isNotEmpty) funktion!,
];
if (parts.isEmpty) return null;
return parts.join(' · ');
}
/// Header-Label für die UI — alle Rollen mit `·` getrennt, in
/// Enum-Reihenfolge.
String get rolesLabel => roles.map((r) => r.label).join(' · ');
}