Final commit.
This commit is contained in:
226
lib/domain/entity/contact_source.dart
Normal file
226
lib/domain/entity/contact_source.dart
Normal file
@ -0,0 +1,226 @@
|
||||
/// 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(' · ');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user