/// 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 = [ 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(' '); } /// Funktionale Zusatzinfo (z. B. „Buchhaltung · Leitung"). Leere /// Komponenten werden ausgeblendet. String? get subtitle { final parts = [ 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 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 channels; /// Zusammengesetzter Anzeigename — identisch zu [ContactSource.displayName]. String? get displayName { final parts = [ 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 = [ 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(' · '); }