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