feat: Anschrift unter „Alle Kontaktinfos" anzeigen

Jede Kontaktquelle zeigt ihre Anschrift (inkl. Zusatz, Land nur im
Ausland), sofern das ERP eine pflegt. Die Anschrift zählt beim
Zusammenführen gleicher Quellen mit — gleicher Name an anderer Adresse
bleibt eine eigene Karte. API-Client neu generiert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Dennis Nemec
2026-09-25 16:26:01 +02:00
parent 335cf02a9f
commit 5a9e16dc10
12 changed files with 205 additions and 17 deletions

View File

@ -117,6 +117,7 @@ extension ApiContactSourceMapper on api.ContactSource {
name3: name3,
abteilung: abteilung,
funktion: funktion,
address: address?.toDomain(),
);
}

View File

@ -1,3 +1,5 @@
import 'address.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.
@ -95,6 +97,7 @@ class ContactSource {
this.name3,
this.abteilung,
this.funktion,
this.address,
});
final String id;
@ -109,6 +112,10 @@ class ContactSource {
final String? abteilung;
final String? funktion;
/// Anschrift dieses ERP-Adress-Datensatzes; `null`, wenn dort weder
/// Straße noch Ort gepflegt sind (z. B. reiner Ansprechpartner).
final Address? address;
/// 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
@ -189,6 +196,7 @@ class MergedContactSource {
required this.abteilung,
required this.funktion,
required this.channels,
this.address,
});
/// Alle Rollen, die diesen zusammengeführten Kontakt liefern.
@ -208,6 +216,9 @@ class MergedContactSource {
/// liefert (kind + ERP-Position).
final List<ContactChannel> channels;
/// Anschrift, falls im ERP gepflegt (bei allen Quellen der Gruppe gleich).
final Address? address;
/// Zusammengesetzter Anzeigename — identisch zu [ContactSource.displayName].
String? get displayName {
final parts = <String>[

View File

@ -211,6 +211,9 @@ class TourDetails {
s.name3 ?? '',
s.abteilung ?? '',
s.funktion ?? '',
// Anschrift gehört zur Identität: gleicher Name an anderer Adresse
// ist ein anderer Datensatz.
s.address?.oneLineWithAddition ?? '',
].join('|');
final sortedChannels = List<ContactChannel>.of(channels)
..sort((a, b) {
@ -240,6 +243,7 @@ class TourDetails {
abteilung: first.abteilung,
funktion: first.funktion,
channels: channelsOf(first),
address: first.address,
);
}

View File

@ -1151,6 +1151,36 @@ class _ContactSourceTile extends StatelessWidget {
color: theme.colorScheme.onSurfaceVariant,
),
),
if (source.address case final address?) ...[
const SizedBox(height: 6),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.home_outlined,
size: 18,
color: theme.colorScheme.primary,
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${address.street} ${address.houseNumber}'.trim(),
style: const TextStyle(fontSize: 14),
),
Text(
'${address.postalCode} ${address.city}'.trim(),
style: const TextStyle(fontSize: 14),
),
..._addressExtras(address, fontSize: 14),
],
),
),
],
),
],
if (channels.isEmpty) ...[
const SizedBox(height: 6),
Text(