Files
Holzleitner-Lieferservice-App/lib/domain/entity/contact_source.dart
Dennis Nemec a9bf8ecdd1 Final commit.
2026-06-01 17:12:28 +02:00

227 lines
7.0 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(' ');
}
/// 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(' · ');
}