From 335cf02a9fadf1aa8cd4f74a128b5ddcf4278b99 Mon Sep 17 00:00:00 2001 From: Dennis Nemec Date: Fri, 25 Sep 2026 16:13:28 +0200 Subject: [PATCH] feat: Adresszusatz (Ortsteil/Etage/Hinweis) anzeigen, Land nur im Ausland MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Address.addition aus dem Backend (ERP-Freitext, praktisch Ortsteil, Etage oder Hinweis); API-Client neu generiert. - Listen (Sortieren, Beladen, Liefer-Übersicht): Adresse mit Zusatz in Klammern, z. B. „… 41472 Neuss (Holzheim)". - Lieferdetails: „Zusatz: …" unter der Anschrift von Empfänger und Besteller; Land nur außerhalb Deutschlands. - Maps-Suche weiter ohne Zusatz (sonst stört „3 Etage" die Suche). Co-Authored-By: Claude Opus 5.5 --- lib/data/mapper/tour_mapper.dart | 1 + lib/data/repository/tour_repository_impl.dart | 1 + lib/domain/entity/address.dart | 41 ++++++++++++++++--- .../detail/presentation/steps/step_info.dart | 20 +++++++++ .../presentation/delivery_overview.dart | 4 +- .../presentation/loading_customer_page.dart | 2 +- .../presentation/loading_overview_page.dart | 2 +- lib/widget/orderer_hint.dart | 2 +- openapi/holzleitner.json | 10 ++++- packages/holzleitner_api/doc/Address.md | 3 +- .../lib/src/model/address.dart | 23 ++++++++++- .../lib/src/model/address.g.dart | 14 ++++++- test/delivery_recipient_test.dart | 33 +++++++++++++++ 13 files changed, 142 insertions(+), 14 deletions(-) diff --git a/lib/data/mapper/tour_mapper.dart b/lib/data/mapper/tour_mapper.dart index a01cab6..4ff4e57 100644 --- a/lib/data/mapper/tour_mapper.dart +++ b/lib/data/mapper/tour_mapper.dart @@ -31,6 +31,7 @@ extension ApiAddressMapper on api.Address { postalCode: postalCode, city: city, country: country, + addition: addition, ); } diff --git a/lib/data/repository/tour_repository_impl.dart b/lib/data/repository/tour_repository_impl.dart index 6380332..7f3c5db 100644 --- a/lib/data/repository/tour_repository_impl.dart +++ b/lib/data/repository/tour_repository_impl.dart @@ -535,6 +535,7 @@ class TourRepositoryImpl implements TourRepository { postalCode: snap['postalCode'] as String, city: snap['city'] as String, country: snap['country'] as String, + addition: snap['addition'] as String?, ), erpBelegartId: (j['erpBelegartId'] as num).toInt(), erpBelegnummer: j['erpBelegnummer'] as String, diff --git a/lib/domain/entity/address.dart b/lib/domain/entity/address.dart index 6afeff4..9062566 100644 --- a/lib/domain/entity/address.dart +++ b/lib/domain/entity/address.dart @@ -12,17 +12,44 @@ class Address { required this.postalCode, required this.city, required this.country, + this.addition, }); final String street; final String houseNumber; final String postalCode; final String city; + /// Land laut ERP-Länderstamm (z. B. „Deutschland"), leer wenn unbekannt. final String country; - /// Einzeilige Darstellung für Listen/Header. - String get oneLine => - '$street $houseNumber, $postalCode $city'; + /// Freier Adresszusatz aus dem ERP — praktisch Ortsteil, Etage oder + /// Hinweis („Holzheim", „3 Etage", „Parterre"). `null`, wenn leer. + final String? addition; + + static const Set _germany = { + 'd', 'de', 'deu', 'deutschland', 'germany', 'brd', + }; + + /// Land für die Anzeige — nur außerhalb Deutschlands, sonst `null`. + String? get foreignCountry { + final c = country.trim(); + if (c.isEmpty || _germany.contains(c.toLowerCase())) return null; + return c; + } + + /// Einzeilige Anschrift „Straße Nr, PLZ Ort" (+ Land, falls Ausland). + /// Ohne Zusatz — so auch als Such-Query für Maps geeignet. + String get oneLine => [ + '$street $houseNumber, $postalCode $city', + if (foreignCountry case final c?) c, + ].join(', '); + + /// Wie [oneLine], mit Adresszusatz in Klammern — für Listen, damit der + /// Fahrer Ortsteil/Etage auf einen Blick sieht. + String get oneLineWithAddition { + final a = addition?.trim(); + return a == null || a.isEmpty ? oneLine : '$oneLine ($a)'; + } Address copyWith({ String? street, @@ -30,6 +57,7 @@ class Address { String? postalCode, String? city, String? country, + String? addition, }) { return Address( street: street ?? this.street, @@ -37,6 +65,7 @@ class Address { postalCode: postalCode ?? this.postalCode, city: city ?? this.city, country: country ?? this.country, + addition: addition ?? this.addition, ); } @@ -48,8 +77,10 @@ class Address { other.houseNumber == houseNumber && other.postalCode == postalCode && other.city == city && - other.country == country; + other.country == country && + other.addition == addition; @override - int get hashCode => Object.hash(street, houseNumber, postalCode, city, country); + int get hashCode => + Object.hash(street, houseNumber, postalCode, city, country, addition); } diff --git a/lib/feature/delivery/detail/presentation/steps/step_info.dart b/lib/feature/delivery/detail/presentation/steps/step_info.dart index 2479d98..329923c 100644 --- a/lib/feature/delivery/detail/presentation/steps/step_info.dart +++ b/lib/feature/delivery/detail/presentation/steps/step_info.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:hl_lieferservice/domain/entity/address.dart'; import 'package:hl_lieferservice/domain/entity/contact_source.dart'; import 'package:hl_lieferservice/domain/entity/customer.dart'; import 'package:hl_lieferservice/domain/entity/delivery.dart'; @@ -116,6 +117,23 @@ class _SectionHeader extends StatelessWidget { } } +// ─── Adresse ──────────────────────────────────────────────────────────── + +/// Zeilen unter „PLZ Ort": Land (nur Ausland) und Adresszusatz aus dem ERP +/// (Ortsteil, Etage, Hinweis — für den Fahrer vor Ort wichtig). +List _addressExtras(Address address, {required double fontSize}) { + final addition = address.addition?.trim(); + return [ + if (address.foreignCountry case final country?) + Text(country, style: TextStyle(fontSize: fontSize)), + if (addition != null && addition.isNotEmpty) + Text( + 'Zusatz: $addition', + style: TextStyle(fontSize: fontSize, fontWeight: FontWeight.w700), + ), + ]; +} + // ─── Abweichender Empfänger ───────────────────────────────────────────── /// Hervorgehobene Karte, wenn Lieferadresse oder Empfänger vom Besteller @@ -210,6 +228,7 @@ class _DifferentRecipientCard extends StatelessWidget { fontWeight: FontWeight.w600, ), ), + ..._addressExtras(address, fontSize: 15), ], ), ), @@ -701,6 +720,7 @@ class _CustomerCard extends StatelessWidget { '${c.address.postalCode} ${c.address.city}', style: const TextStyle(fontSize: 14), ), + ..._addressExtras(c.address, fontSize: 14), ], ), ), diff --git a/lib/feature/delivery/overview/presentation/delivery_overview.dart b/lib/feature/delivery/overview/presentation/delivery_overview.dart index 3fca515..55207a9 100644 --- a/lib/feature/delivery/overview/presentation/delivery_overview.dart +++ b/lib/feature/delivery/overview/presentation/delivery_overview.dart @@ -439,7 +439,7 @@ class _NextDeliveryCard extends StatelessWidget { const SizedBox(height: 10), _InfoRow( icon: Icons.location_on_outlined, - text: recipient.address.oneLine, + text: recipient.address.oneLineWithAddition, emphasized: true, ), if (recipient.differsFromOrderer) @@ -846,7 +846,7 @@ class _DeliveryTile extends StatelessWidget { ), const SizedBox(height: 2), Text( - recipient.address.oneLine, + recipient.address.oneLineWithAddition, style: TextStyle( fontSize: 12, color: theme.colorScheme.onSurfaceVariant, diff --git a/lib/feature/loading/presentation/loading_customer_page.dart b/lib/feature/loading/presentation/loading_customer_page.dart index 04a0ca8..37bc2f8 100644 --- a/lib/feature/loading/presentation/loading_customer_page.dart +++ b/lib/feature/loading/presentation/loading_customer_page.dart @@ -637,7 +637,7 @@ class _CustomerBody extends StatelessWidget { ), const SizedBox(height: 4), Text( - recipient.address.oneLine, + recipient.address.oneLineWithAddition, style: Theme.of(context).textTheme.bodyMedium, ), if (recipient.differsFromOrderer) diff --git a/lib/feature/loading/presentation/loading_overview_page.dart b/lib/feature/loading/presentation/loading_overview_page.dart index 0c2a61f..3849d32 100644 --- a/lib/feature/loading/presentation/loading_overview_page.dart +++ b/lib/feature/loading/presentation/loading_overview_page.dart @@ -556,7 +556,7 @@ class _OverviewTile extends StatelessWidget { ), const SizedBox(height: 2), Text( - recipient.address.oneLine, + recipient.address.oneLineWithAddition, style: TextStyle( fontSize: 12, color: theme.colorScheme.onSurfaceVariant, diff --git a/lib/widget/orderer_hint.dart b/lib/widget/orderer_hint.dart index c0998ce..837d87d 100644 --- a/lib/widget/orderer_hint.dart +++ b/lib/widget/orderer_hint.dart @@ -60,7 +60,7 @@ class RecipientSubtitle extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - Text(recipient.address.oneLine, style: addressStyle), + Text(recipient.address.oneLineWithAddition, style: addressStyle), if (recipient.differsFromOrderer) OrdererHint(ordererName: ordererName), ], diff --git a/openapi/holzleitner.json b/openapi/holzleitner.json index 3f96ac8..833aaab 100644 --- a/openapi/holzleitner.json +++ b/openapi/holzleitner.json @@ -2021,11 +2021,19 @@ "country" ], "properties": { + "addition": { + "type": [ + "string", + "null" + ], + "description": "Freier Adresszusatz. Kommt aus dem ERP-Textfeld `Adressen.Land`, das\ndie Sachbearbeitung praktisch f\u00fcr Ortsteil, Etage oder Hinweise nutzt\n(z. B. \u201eHolzheim\", \u201e3. Etage\", \u201eParterre\"). `None`, wenn leer." + }, "city": { "type": "string" }, "country": { - "type": "string" + "type": "string", + "description": "Land laut ERP-L\u00e4nderstamm (`Adressen.LandID` \u2192 `Laender.Land`), z. B.\n\u201eDeutschland\" oder \u201eBelgien\". Leer, wenn im ERP kein Land verkn\u00fcpft ist." }, "houseNumber": { "type": "string" diff --git a/packages/holzleitner_api/doc/Address.md b/packages/holzleitner_api/doc/Address.md index a8ef211..ef193e4 100644 --- a/packages/holzleitner_api/doc/Address.md +++ b/packages/holzleitner_api/doc/Address.md @@ -8,8 +8,9 @@ import 'package:holzleitner_api/api.dart'; ## Properties Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- +**addition** | **String** | Freier Adresszusatz. Kommt aus dem ERP-Textfeld `Adressen.Land`, das die Sachbearbeitung praktisch für Ortsteil, Etage oder Hinweise nutzt (z. B. „Holzheim\", „3. Etage\", „Parterre\"). `None`, wenn leer. | [optional] **city** | **String** | | -**country** | **String** | | +**country** | **String** | Land laut ERP-Länderstamm (`Adressen.LandID` → `Laender.Land`), z. B. „Deutschland\" oder „Belgien\". Leer, wenn im ERP kein Land verknüpft ist. | **houseNumber** | **String** | | **postalCode** | **String** | | **street** | **String** | | diff --git a/packages/holzleitner_api/lib/src/model/address.dart b/packages/holzleitner_api/lib/src/model/address.dart index e179316..4e81b4a 100644 --- a/packages/holzleitner_api/lib/src/model/address.dart +++ b/packages/holzleitner_api/lib/src/model/address.dart @@ -11,16 +11,22 @@ part 'address.g.dart'; /// Postanschrift — wird sowohl als aktuelle Kundenanschrift in [`Customer`] als auch als unveränderlicher Snapshot in [`crate::domain::Delivery`] verwendet (`delivery_address_snapshot`). Bewusst als Value Object modelliert: gleiche Adresse = gleicher Wert. Strikte Equality erleichtert Sync-Diffs zwischen ERP und Backend. [`Customer`]: crate::domain::Customer /// /// Properties: +/// * [addition] - Freier Adresszusatz. Kommt aus dem ERP-Textfeld `Adressen.Land`, das die Sachbearbeitung praktisch für Ortsteil, Etage oder Hinweise nutzt (z. B. „Holzheim\", „3. Etage\", „Parterre\"). `None`, wenn leer. /// * [city] -/// * [country] +/// * [country] - Land laut ERP-Länderstamm (`Adressen.LandID` → `Laender.Land`), z. B. „Deutschland\" oder „Belgien\". Leer, wenn im ERP kein Land verknüpft ist. /// * [houseNumber] /// * [postalCode] /// * [street] @BuiltValue() abstract class Address implements Built { + /// Freier Adresszusatz. Kommt aus dem ERP-Textfeld `Adressen.Land`, das die Sachbearbeitung praktisch für Ortsteil, Etage oder Hinweise nutzt (z. B. „Holzheim\", „3. Etage\", „Parterre\"). `None`, wenn leer. + @BuiltValueField(wireName: r'addition') + String? get addition; + @BuiltValueField(wireName: r'city') String get city; + /// Land laut ERP-Länderstamm (`Adressen.LandID` → `Laender.Land`), z. B. „Deutschland\" oder „Belgien\". Leer, wenn im ERP kein Land verknüpft ist. @BuiltValueField(wireName: r'country') String get country; @@ -56,6 +62,13 @@ class _$AddressSerializer implements PrimitiveSerializer
{ Address object, { FullType specifiedType = FullType.unspecified, }) sync* { + if (object.addition != null) { + yield r'addition'; + yield serializers.serialize( + object.addition, + specifiedType: const FullType.nullable(String), + ); + } yield r'city'; yield serializers.serialize( object.city, @@ -104,6 +117,14 @@ class _$AddressSerializer implements PrimitiveSerializer
{ final key = serializedList[i] as String; final value = serializedList[i + 1]; switch (key) { + case r'addition': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType.nullable(String), + ) as String?; + if (valueDes == null) continue; + result.addition = valueDes; + break; case r'city': final valueDes = serializers.deserialize( value, diff --git a/packages/holzleitner_api/lib/src/model/address.g.dart b/packages/holzleitner_api/lib/src/model/address.g.dart index c3c79e2..f24dca3 100644 --- a/packages/holzleitner_api/lib/src/model/address.g.dart +++ b/packages/holzleitner_api/lib/src/model/address.g.dart @@ -7,6 +7,8 @@ part of 'address.dart'; // ************************************************************************** class _$Address extends Address { + @override + final String? addition; @override final String city; @override @@ -22,7 +24,8 @@ class _$Address extends Address { (AddressBuilder()..update(updates))._build(); _$Address._( - {required this.city, + {this.addition, + required this.city, required this.country, required this.houseNumber, required this.postalCode, @@ -39,6 +42,7 @@ class _$Address extends Address { bool operator ==(Object other) { if (identical(other, this)) return true; return other is Address && + addition == other.addition && city == other.city && country == other.country && houseNumber == other.houseNumber && @@ -49,6 +53,7 @@ class _$Address extends Address { @override int get hashCode { var _$hash = 0; + _$hash = $jc(_$hash, addition.hashCode); _$hash = $jc(_$hash, city.hashCode); _$hash = $jc(_$hash, country.hashCode); _$hash = $jc(_$hash, houseNumber.hashCode); @@ -61,6 +66,7 @@ class _$Address extends Address { @override String toString() { return (newBuiltValueToStringHelper(r'Address') + ..add('addition', addition) ..add('city', city) ..add('country', country) ..add('houseNumber', houseNumber) @@ -73,6 +79,10 @@ class _$Address extends Address { class AddressBuilder implements Builder { _$Address? _$v; + String? _addition; + String? get addition => _$this._addition; + set addition(String? addition) => _$this._addition = addition; + String? _city; String? get city => _$this._city; set city(String? city) => _$this._city = city; @@ -100,6 +110,7 @@ class AddressBuilder implements Builder { AddressBuilder get _$this { final $v = _$v; if ($v != null) { + _addition = $v.addition; _city = $v.city; _country = $v.country; _houseNumber = $v.houseNumber; @@ -126,6 +137,7 @@ class AddressBuilder implements Builder { _$Address _build() { final _$result = _$v ?? _$Address._( + addition: addition, city: BuiltValueNullFieldError.checkNotNull(city, r'Address', 'city'), country: BuiltValueNullFieldError.checkNotNull( country, r'Address', 'country'), diff --git a/test/delivery_recipient_test.dart b/test/delivery_recipient_test.dart index d46d204..77543be 100644 --- a/test/delivery_recipient_test.dart +++ b/test/delivery_recipient_test.dart @@ -152,4 +152,37 @@ void main() { expect(r.name, 'Kraus Heike'); expect(r.differsFromOrderer, isTrue); }); + + group('Adressanzeige', () { + const base = Address( + street: 'Am Sandberg', + houseNumber: '15', + postalCode: '41472', + city: 'Neuss', + country: 'Deutschland', + addition: 'Holzheim', + ); + + test('Deutschland erscheint nicht, Zusatz nur in der Listenzeile', () { + expect(base.oneLine, 'Am Sandberg 15, 41472 Neuss'); + expect(base.oneLineWithAddition, 'Am Sandberg 15, 41472 Neuss (Holzheim)'); + }); + + test('Ausland wird angehängt', () { + final belgien = base.copyWith(country: 'Belgien'); + expect(belgien.foreignCountry, 'Belgien'); + expect(belgien.oneLine, 'Am Sandberg 15, 41472 Neuss, Belgien'); + }); + + test('Zusatz zählt nicht als abweichende Adresse', () { + final details = _details( + deliveryAddress: _ordererAddress.copyWith(addition: '3 Etage'), + sources: [_source(ContactRole.header, 'Kraus Heike')], + ); + expect( + details.recipientOf(details.deliveries.single).differsFromOrderer, + isFalse, + ); + }); + }); }