From dd7fe9a8eb4df45e33b094679520413a736de23f Mon Sep 17 00:00:00 2001 From: Dennis Nemec Date: Fri, 25 Sep 2026 14:04:36 +0200 Subject: [PATCH] feat(zahlung): eigener Step "Zahlung" vor der Uebersicht - Neuer Workflow-Step "Zahlung" zeigt ausgelieferte Artikel mit Menge und Preis, Betragsaufstellung, Zahlungsstatus und einen grossen Button "Zahlung abwickeln" (fest unten) - Zahlungs-Modal: grosser offener Betrag, Methoden als Cards aus den Backend-Stammdaten, bei EC-Karte gross die Kundennummer; schliesst nur ueber Abbrechen, "Zahlung erhalten" oder "Lieferung abbrechen" (mit Grund) - Bestaetigung wird am Server protokolliert (RecordDeliveryPayment); bis dahin sind "Weiter" und die Uebersicht gesperrt, ausser es ist nichts offen (bereits bezahlt). Aendert sich der Betrag danach, wird die Zahlung als veraltet markiert und muss erneut abgewickelt werden - Uebersicht zeigt die abgewickelte Zahlung statt eigener Methodenauswahl; Unterschrift-Flow ohne doppelte Inkasso-Stufe; Abschluss sendet Methode und Inkasso-Flag der gueltigen Zahlung - DeliveryPaymentStatus als einzige Quelle fuer offenen Betrag (gleiche Formel wie Backend); Artikelliste/Betragskarte als geteilte Widgets; Begruendungs-Dialog geteilt mit Info-Step - API-Client aus aktueller Backend-Spec neu generiert - Tests fuer Zahlungsstatus und Modal Co-Authored-By: Claude Opus 5.5 --- lib/data/mapper/tour_mapper.dart | 18 + lib/data/repository/tour_repository_impl.dart | 31 + lib/domain/entity/delivery_payment.dart | 32 + lib/domain/entity/tour_details.dart | 12 + lib/domain/repository/tour_repository.dart | 13 + lib/feature/delivery/bloc/tour_bloc.dart | 41 + lib/feature/delivery/bloc/tour_event.dart | 16 + .../delivery/detail/bloc/workflow_bloc.dart | 3 - .../delivery/detail/bloc/workflow_event.dart | 7 - .../delivery/detail/bloc/workflow_state.dart | 22 +- .../detail/model/delivery_payment_status.dart | 80 ++ .../presentation/delivery_detail_page.dart | 290 ++++---- .../detail/presentation/delivery_sign.dart | 195 +---- .../detail/presentation/steps/step_info.dart | 38 +- .../presentation/steps/step_payment.dart | 185 +++++ .../presentation/steps/step_summary.dart | 531 +------------ .../widget/delivered_items_card.dart | 139 ++++ .../widget/payment_breakdown_card.dart | 134 ++++ .../widget/payment_method_icon.dart | 10 + .../widget/payment_record_card.dart | 138 ++++ .../presentation/widget/payment_sheet.dart | 407 ++++++++++ .../presentation/widget/reason_dialog.dart | 38 + openapi/holzleitner.json | 703 +++++++++++++++++- .../holzleitner_api/.openapi-generator/FILES | 39 +- packages/holzleitner_api/README.md | 18 +- packages/holzleitner_api/doc/AdminApi.md | 233 ++++++ .../doc/CompletedDeliveriesResponse.md | 18 + .../doc/CompletedDeliveryItem.md | 16 + packages/holzleitner_api/doc/DeliveriesApi.md | 44 ++ .../holzleitner_api/doc/DeliveryDetails.md | 27 + .../holzleitner_api/doc/DeliveryPayment.md | 22 + .../doc/DeliveryPaymentResponse.md | 15 + .../doc/PendingReviewResponse.md | 23 + .../doc/PositionsModifiedResponse.md | 16 + .../doc/RecordDeliveryPaymentRequest.md | 18 + .../doc/ResolveReviewRequest.md | 16 + .../doc/ReviewedItemResponse.md | 20 + packages/holzleitner_api/doc/TourDetails.md | 1 + packages/holzleitner_api/doc/ToursApi.md | 18 +- .../holzleitner_api/lib/holzleitner_api.dart | 10 + .../lib/src/api/admin_api.dart | 419 +++++++++++ .../lib/src/api/deliveries_api.dart | 105 +++ .../lib/src/api/tours_api.dart | 10 +- .../model/completed_deliveries_response.dart | 166 +++++ .../completed_deliveries_response.g.dart | 147 ++++ .../src/model/completed_delivery_item.dart | 124 +++ .../src/model/completed_delivery_item.g.dart | 110 +++ .../lib/src/model/delivery_details.dart | 334 +++++++++ .../lib/src/model/delivery_details.g.dart | 291 ++++++++ .../lib/src/model/delivery_payment.dart | 223 ++++++ .../lib/src/model/delivery_payment.g.dart | 190 +++++ .../src/model/delivery_payment_response.dart | 107 +++ .../model/delivery_payment_response.g.dart | 108 +++ .../src/model/pending_review_response.dart | 243 ++++++ .../src/model/pending_review_response.g.dart | 218 ++++++ .../model/positions_modified_response.dart | 124 +++ .../model/positions_modified_response.g.dart | 113 +++ .../record_delivery_payment_request.dart | 161 ++++ .../record_delivery_payment_request.g.dart | 144 ++++ .../lib/src/model/resolve_review_request.dart | 127 ++++ .../src/model/resolve_review_request.g.dart | 106 +++ .../lib/src/model/reviewed_item_response.dart | 189 +++++ .../src/model/reviewed_item_response.g.dart | 164 ++++ .../lib/src/model/tour_details.dart | 18 + .../lib/src/model/tour_details.g.dart | 16 + .../holzleitner_api/lib/src/serializers.dart | 24 + .../lib/src/serializers.g.dart | 46 ++ .../completed_deliveries_response_test.dart | 35 + .../test/completed_delivery_item_test.dart | 23 + .../test/delivery_details_test.dart | 89 +++ .../test/delivery_payment_response_test.dart | 16 + .../test/delivery_payment_test.dart | 53 ++ .../test/pending_review_response_test.dart | 60 ++ .../positions_modified_response_test.dart | 23 + .../record_delivery_payment_request_test.dart | 35 + .../test/resolve_review_request_test.dart | 23 + .../test/reviewed_item_response_test.dart | 41 + test/payment_step_test.dart | 278 +++++++ 78 files changed, 7466 insertions(+), 874 deletions(-) create mode 100644 lib/domain/entity/delivery_payment.dart create mode 100644 lib/feature/delivery/detail/model/delivery_payment_status.dart create mode 100644 lib/feature/delivery/detail/presentation/steps/step_payment.dart create mode 100644 lib/feature/delivery/detail/presentation/widget/delivered_items_card.dart create mode 100644 lib/feature/delivery/detail/presentation/widget/payment_breakdown_card.dart create mode 100644 lib/feature/delivery/detail/presentation/widget/payment_method_icon.dart create mode 100644 lib/feature/delivery/detail/presentation/widget/payment_record_card.dart create mode 100644 lib/feature/delivery/detail/presentation/widget/payment_sheet.dart create mode 100644 lib/feature/delivery/detail/presentation/widget/reason_dialog.dart create mode 100644 packages/holzleitner_api/doc/CompletedDeliveriesResponse.md create mode 100644 packages/holzleitner_api/doc/CompletedDeliveryItem.md create mode 100644 packages/holzleitner_api/doc/DeliveryDetails.md create mode 100644 packages/holzleitner_api/doc/DeliveryPayment.md create mode 100644 packages/holzleitner_api/doc/DeliveryPaymentResponse.md create mode 100644 packages/holzleitner_api/doc/PendingReviewResponse.md create mode 100644 packages/holzleitner_api/doc/PositionsModifiedResponse.md create mode 100644 packages/holzleitner_api/doc/RecordDeliveryPaymentRequest.md create mode 100644 packages/holzleitner_api/doc/ResolveReviewRequest.md create mode 100644 packages/holzleitner_api/doc/ReviewedItemResponse.md create mode 100644 packages/holzleitner_api/lib/src/model/completed_deliveries_response.dart create mode 100644 packages/holzleitner_api/lib/src/model/completed_deliveries_response.g.dart create mode 100644 packages/holzleitner_api/lib/src/model/completed_delivery_item.dart create mode 100644 packages/holzleitner_api/lib/src/model/completed_delivery_item.g.dart create mode 100644 packages/holzleitner_api/lib/src/model/delivery_details.dart create mode 100644 packages/holzleitner_api/lib/src/model/delivery_details.g.dart create mode 100644 packages/holzleitner_api/lib/src/model/delivery_payment.dart create mode 100644 packages/holzleitner_api/lib/src/model/delivery_payment.g.dart create mode 100644 packages/holzleitner_api/lib/src/model/delivery_payment_response.dart create mode 100644 packages/holzleitner_api/lib/src/model/delivery_payment_response.g.dart create mode 100644 packages/holzleitner_api/lib/src/model/pending_review_response.dart create mode 100644 packages/holzleitner_api/lib/src/model/pending_review_response.g.dart create mode 100644 packages/holzleitner_api/lib/src/model/positions_modified_response.dart create mode 100644 packages/holzleitner_api/lib/src/model/positions_modified_response.g.dart create mode 100644 packages/holzleitner_api/lib/src/model/record_delivery_payment_request.dart create mode 100644 packages/holzleitner_api/lib/src/model/record_delivery_payment_request.g.dart create mode 100644 packages/holzleitner_api/lib/src/model/resolve_review_request.dart create mode 100644 packages/holzleitner_api/lib/src/model/resolve_review_request.g.dart create mode 100644 packages/holzleitner_api/lib/src/model/reviewed_item_response.dart create mode 100644 packages/holzleitner_api/lib/src/model/reviewed_item_response.g.dart create mode 100644 packages/holzleitner_api/test/completed_deliveries_response_test.dart create mode 100644 packages/holzleitner_api/test/completed_delivery_item_test.dart create mode 100644 packages/holzleitner_api/test/delivery_details_test.dart create mode 100644 packages/holzleitner_api/test/delivery_payment_response_test.dart create mode 100644 packages/holzleitner_api/test/delivery_payment_test.dart create mode 100644 packages/holzleitner_api/test/pending_review_response_test.dart create mode 100644 packages/holzleitner_api/test/positions_modified_response_test.dart create mode 100644 packages/holzleitner_api/test/record_delivery_payment_request_test.dart create mode 100644 packages/holzleitner_api/test/resolve_review_request_test.dart create mode 100644 packages/holzleitner_api/test/reviewed_item_response_test.dart create mode 100644 test/payment_step_test.dart diff --git a/lib/data/mapper/tour_mapper.dart b/lib/data/mapper/tour_mapper.dart index 33a0c8c..a01cab6 100644 --- a/lib/data/mapper/tour_mapper.dart +++ b/lib/data/mapper/tour_mapper.dart @@ -6,6 +6,7 @@ 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'; import 'package:hl_lieferservice/domain/entity/delivery_credit.dart'; +import 'package:hl_lieferservice/domain/entity/delivery_payment.dart'; import 'package:hl_lieferservice/domain/entity/delivery_item.dart'; import 'package:hl_lieferservice/domain/entity/delivery_note.dart'; import 'package:hl_lieferservice/domain/entity/delivery_service_value.dart'; @@ -335,6 +336,11 @@ extension ApiTourDetailsMapper on api.TourDetails { for (final c in credits) c.deliveryId: c.toDomain(), }; + // Zahlungen: höchstens eine pro Lieferung (jüngster Eintrag). + final paymentsMap = { + for (final p in payments) p.deliveryId: p.toDomain(), + }; + // Service-Definitionen (aktiv, sortiert) + Pro-Lieferung-Werte indizieren. final servicesList = services.map((s) => s.toDomain()).toList(growable: false); @@ -367,6 +373,7 @@ extension ApiTourDetailsMapper on api.TourDetails { warehouses: warehousesMap, notesByDeliveryId: notesGrouped, creditsByDeliveryId: creditsMap, + paymentsByDeliveryId: paymentsMap, services: servicesList, serviceValuesByDeliveryId: serviceValues, contactSourcesByDeliveryId: sourcesGrouped, @@ -406,3 +413,14 @@ extension ApiDeliveryCreditMapper on api.DeliveryCredit { reason: reason, ); } + +extension ApiDeliveryPaymentMapper on api.DeliveryPayment { + DeliveryPayment toDomain() => DeliveryPayment( + id: id, + deliveryId: deliveryId, + paymentMethodId: paymentMethodId, + paymentMethodCode: paymentMethodCode, + amountCents: amountCents, + recordedAt: recordedAt.toLocal(), + ); +} diff --git a/lib/data/repository/tour_repository_impl.dart b/lib/data/repository/tour_repository_impl.dart index 7091124..e630f63 100644 --- a/lib/data/repository/tour_repository_impl.dart +++ b/lib/data/repository/tour_repository_impl.dart @@ -7,6 +7,7 @@ import 'package:hl_lieferservice/data/mapper/tour_mapper.dart'; import 'package:hl_lieferservice/domain/entity/address.dart'; import 'package:hl_lieferservice/domain/entity/delivery.dart'; import 'package:hl_lieferservice/domain/entity/delivery_credit.dart'; +import 'package:hl_lieferservice/domain/entity/delivery_payment.dart'; import 'package:hl_lieferservice/domain/entity/delivery_note.dart'; import 'package:hl_lieferservice/domain/entity/delivery_service_value.dart'; import 'package:hl_lieferservice/domain/entity/scan_intent.dart'; @@ -435,6 +436,36 @@ class TourRepositoryImpl implements TourRepository { } } + @override + Future recordDeliveryPayment({ + required String deliveryId, + required String clientEventId, + required String paymentMethodId, + required int expectedAmountCents, + String? actorCarId, + }) async { + try { + final request = api.RecordDeliveryPaymentRequest((b) { + b + ..clientEventId = clientEventId + ..paymentMethodId = paymentMethodId + ..expectedAmountCents = expectedAmountCents; + if (actorCarId != null) b.authorCarId = actorCarId; + }); + final response = await _api.getDeliveriesApi().recordPayment( + deliveryId: deliveryId, + recordDeliveryPaymentRequest: request, + ); + final payment = response.data?.payment; + if (payment == null) { + throw TourRepositoryException('Zahlung protokollieren: leere Antwort'); + } + return payment.toDomain(); + } on DioException catch (e) { + throw TourRepositoryException(_describe(e, 'Zahlung protokollieren'), e); + } + } + @override Future setDeliveryService({ required String deliveryId, diff --git a/lib/domain/entity/delivery_payment.dart b/lib/domain/entity/delivery_payment.dart new file mode 100644 index 0000000..b71e2b1 --- /dev/null +++ b/lib/domain/entity/delivery_payment.dart @@ -0,0 +1,32 @@ +/// Protokollierte Zahlungsabwicklung („Abkassieren") einer Lieferung — +/// spiegelt das Backend-Aggregat `DeliveryPayment`. +/// +/// Das Backend liefert pro Lieferung nur den jüngsten Eintrag. Der Betrag ist +/// der offene Betrag zum Zeitpunkt der Bestätigung. Ändert sich der offene +/// Betrag danach (z. B. neue Gutschrift), ist der Eintrag veraltet — siehe +/// `DeliveryPaymentStatus`. +class DeliveryPayment { + const DeliveryPayment({ + required this.id, + required this.deliveryId, + required this.paymentMethodId, + required this.paymentMethodCode, + required this.amountCents, + required this.recordedAt, + }); + + final String id; + final String deliveryId; + final String paymentMethodId; + + /// Snapshot des Methoden-Codes (`cash`, `ec_card`, `invoice`, …). + final String paymentMethodCode; + + /// Abgewickelter Betrag in Cent. + final int amountCents; + final DateTime recordedAt; + + /// Bar/EC = vor Ort kassiert. „Auf Rechnung" bleibt offen. + bool get isCollectedOnSite => + paymentMethodCode == 'cash' || paymentMethodCode == 'ec_card'; +} diff --git a/lib/domain/entity/tour_details.dart b/lib/domain/entity/tour_details.dart index 737cf24..ba7c783 100644 --- a/lib/domain/entity/tour_details.dart +++ b/lib/domain/entity/tour_details.dart @@ -3,6 +3,7 @@ import 'contact_source.dart'; import 'customer.dart'; import 'delivery.dart'; import 'delivery_credit.dart'; +import 'delivery_payment.dart'; import 'delivery_item.dart'; import 'delivery_note.dart'; import 'delivery_service_value.dart'; @@ -28,6 +29,7 @@ class TourDetails { required this.warehouses, required this.notesByDeliveryId, required this.creditsByDeliveryId, + required this.paymentsByDeliveryId, required this.services, required this.serviceValuesByDeliveryId, required this.contactSourcesByDeliveryId, @@ -56,6 +58,10 @@ class TourDetails { /// der Eintrag, gibt es aktuell keine Gutschrift. final Map creditsByDeliveryId; + /// Pro Lieferung die jüngste protokollierte Zahlungsabwicklung. Fehlt der + /// Eintrag, wurde für diese Lieferung noch keine Zahlung abgewickelt. + final Map paymentsByDeliveryId; + /// Aktive Service-Definitionen (Stammdaten), nach `sortOrder`. Daraus /// rendert Phase 4 die Auswahl. final List services; @@ -200,6 +206,10 @@ class TourDetails { DeliveryCredit? creditOf(String deliveryId) => creditsByDeliveryId[deliveryId]; + /// Jüngste protokollierte Zahlung dieser Lieferung, oder `null`. + DeliveryPayment? paymentOf(String deliveryId) => + paymentsByDeliveryId[deliveryId]; + /// Gesetzter Service-Wert dieser Lieferung für einen Service, oder `null`. DeliveryServiceValue? serviceValueOf(String deliveryId, String serviceId) => serviceValuesByDeliveryId[deliveryId]?[serviceId]; @@ -398,6 +408,7 @@ class TourDetails { List? deliveries, Map>? notesByDeliveryId, Map? creditsByDeliveryId, + Map? paymentsByDeliveryId, Map>? serviceValuesByDeliveryId, }) { return TourDetails( @@ -409,6 +420,7 @@ class TourDetails { warehouses: warehouses, notesByDeliveryId: notesByDeliveryId ?? this.notesByDeliveryId, creditsByDeliveryId: creditsByDeliveryId ?? this.creditsByDeliveryId, + paymentsByDeliveryId: paymentsByDeliveryId ?? this.paymentsByDeliveryId, services: services, serviceValuesByDeliveryId: serviceValuesByDeliveryId ?? this.serviceValuesByDeliveryId, diff --git a/lib/domain/repository/tour_repository.dart b/lib/domain/repository/tour_repository.dart index 293feda..5d35561 100644 --- a/lib/domain/repository/tour_repository.dart +++ b/lib/domain/repository/tour_repository.dart @@ -1,5 +1,6 @@ import 'package:hl_lieferservice/domain/entity/delivery.dart'; import 'package:hl_lieferservice/domain/entity/delivery_credit.dart'; +import 'package:hl_lieferservice/domain/entity/delivery_payment.dart'; import 'package:hl_lieferservice/domain/entity/delivery_note.dart'; import 'package:hl_lieferservice/domain/entity/delivery_service_value.dart'; import 'package:hl_lieferservice/domain/entity/scan_intent.dart'; @@ -166,6 +167,18 @@ abstract interface class TourRepository { String? actorCarId, }); + /// Protokolliert die Zahlungsabwicklung („Abkassieren") einer Lieferung. + /// Append-only + idempotent über [clientEventId]. [expectedAmountCents] ist + /// der Betrag, den der Fahrer gesehen hat; weicht der Server-Betrag ab, + /// lehnt das Backend ab. Rückgabe: der protokollierte Eintrag. + Future recordDeliveryPayment({ + required String deliveryId, + required String clientEventId, + required String paymentMethodId, + required int expectedAmountCents, + String? actorCarId, + }); + /// Setzt (Upsert) den Wert eines Service für eine Lieferung. Genau das zum /// Service-Typ passende Feld angeben. Rückgabe: der gespeicherte Wert. Future setDeliveryService({ diff --git a/lib/feature/delivery/bloc/tour_bloc.dart b/lib/feature/delivery/bloc/tour_bloc.dart index e636eaf..e1bfbfd 100644 --- a/lib/feature/delivery/bloc/tour_bloc.dart +++ b/lib/feature/delivery/bloc/tour_bloc.dart @@ -7,6 +7,7 @@ import 'package:uuid/uuid.dart'; import 'package:hl_lieferservice/data/cache/attachment_cache.dart'; import 'package:hl_lieferservice/domain/entity/delivery.dart'; import 'package:hl_lieferservice/domain/entity/delivery_credit.dart'; +import 'package:hl_lieferservice/domain/entity/delivery_payment.dart'; import 'package:hl_lieferservice/domain/entity/delivery_item.dart'; import 'package:hl_lieferservice/domain/entity/delivery_note.dart'; import 'package:hl_lieferservice/domain/entity/delivery_service_value.dart'; @@ -50,6 +51,7 @@ class TourBloc extends Bloc { on(_onUploadDeliveryNoteImage); on(_onSetDeliveryCredit); on(_onRemoveDeliveryCredit); + on(_onRecordDeliveryPayment); on(_onSetDeliveryServiceValue); on(_onRemoveDeliveryServiceValue); } @@ -613,6 +615,45 @@ class TourBloc extends Bloc { } } + // ─── Zahlungsabwicklung ────────────────────────────────────────────── + + /// Protokolliert die Zahlung. Bewusst NICHT optimistisch: Der Server + /// berechnet den Betrag autoritativ und kann ablehnen (z. B. Betrag hat + /// sich zwischenzeitlich geändert). Erst die Server-Antwort gibt den + /// Zahlungs-Step frei. + Future _onRecordDeliveryPayment( + RecordDeliveryPayment event, + Emitter emit, + ) async { + if (state is! TourLoaded) return; + + opBloc.add(StartOperation(message: 'Zahlung wird protokolliert …')); + try { + final payment = await tourRepository.recordDeliveryPayment( + deliveryId: event.deliveryId, + clientEventId: _uuid.v4(), + paymentMethodId: event.paymentMethodId, + expectedAmountCents: event.expectedAmountCents, + actorCarId: event.actorCarId, + ); + final latest = state; + if (latest is TourLoaded) { + final next = Map.from( + latest.details.paymentsByDeliveryId, + )..[event.deliveryId] = payment; + emit(latest.copyWith( + details: latest.details.copyWith(paymentsByDeliveryId: next), + )); + } + opBloc.add(FinishOperation(message: 'Zahlung protokolliert')); + } catch (e, st) { + debugPrint('TourBloc.RecordDeliveryPayment fehlgeschlagen: $e\n$st'); + opBloc.add(FailOperation( + message: _messageOf(e, 'Zahlung konnte nicht protokolliert werden'), + )); + } + } + // ─── Betrags-Gutschrift ────────────────────────────────────────────── /// Tauscht die Gutschrift einer Lieferung im Aggregat aus (`null` = entfernt). diff --git a/lib/feature/delivery/bloc/tour_event.dart b/lib/feature/delivery/bloc/tour_event.dart index 64d23b9..98fdaa6 100644 --- a/lib/feature/delivery/bloc/tour_event.dart +++ b/lib/feature/delivery/bloc/tour_event.dart @@ -274,6 +274,22 @@ class RemoveDeliveryCredit extends TourEvent { final String actorCarId; } +/// Protokolliert die Zahlungsabwicklung („Abkassieren") einer Lieferung. +/// [expectedAmountCents] ist der offene Betrag, den der Fahrer bestätigt hat. +class RecordDeliveryPayment extends TourEvent { + const RecordDeliveryPayment({ + required this.deliveryId, + required this.paymentMethodId, + required this.expectedAmountCents, + required this.actorCarId, + }); + + final String deliveryId; + final String paymentMethodId; + final int expectedAmountCents; + final String? actorCarId; +} + /// Setzt/ändert den Wert eines Service für eine Lieferung (Checkbox/Zahl). class SetDeliveryServiceValue extends TourEvent { const SetDeliveryServiceValue({ diff --git a/lib/feature/delivery/detail/bloc/workflow_bloc.dart b/lib/feature/delivery/detail/bloc/workflow_bloc.dart index 328fc20..10d2c56 100644 --- a/lib/feature/delivery/detail/bloc/workflow_bloc.dart +++ b/lib/feature/delivery/detail/bloc/workflow_bloc.dart @@ -39,8 +39,5 @@ class DeliveryWorkflowBloc final next = [...state.pendingImageNotes]..removeAt(e.index); emit(state.copyWith(pendingImageNotes: next)); }); - on((e, emit) => emit( - state.copyWith(paymentMethodOverrideId: e.paymentMethodId), - )); } } diff --git a/lib/feature/delivery/detail/bloc/workflow_event.dart b/lib/feature/delivery/detail/bloc/workflow_event.dart index 4529bc9..fbc8c51 100644 --- a/lib/feature/delivery/detail/bloc/workflow_event.dart +++ b/lib/feature/delivery/detail/bloc/workflow_event.dart @@ -34,10 +34,3 @@ class WorkflowRemovePendingImage extends DeliveryWorkflowEvent { const WorkflowRemovePendingImage(this.index); final int index; } - -// ─── Payment-Auswahl ──────────────────────────────────────────────────── - -class WorkflowOverridePaymentMethod extends DeliveryWorkflowEvent { - const WorkflowOverridePaymentMethod({required this.paymentMethodId}); - final String? paymentMethodId; -} diff --git a/lib/feature/delivery/detail/bloc/workflow_state.dart b/lib/feature/delivery/detail/bloc/workflow_state.dart index 5c175b5..2ab2fd3 100644 --- a/lib/feature/delivery/detail/bloc/workflow_state.dart +++ b/lib/feature/delivery/detail/bloc/workflow_state.dart @@ -1,11 +1,12 @@ import 'package:image_picker/image_picker.dart'; -/// Die 5 Steps der Auslieferungs-Detail-Page. Reihenfolge ≙ Index. +/// Die 6 Steps der Auslieferungs-Detail-Page. Reihenfolge ≙ Index. enum WorkflowStep { info, notes, articles, services, + payment, summary, } @@ -15,6 +16,7 @@ extension WorkflowStepX on WorkflowStep { WorkflowStep.notes => 'Notizen', WorkflowStep.articles => 'Artikel & Gutschriften', WorkflowStep.services => 'Checkliste', + WorkflowStep.payment => 'Zahlung', WorkflowStep.summary => 'Übersicht', }; @@ -24,6 +26,7 @@ extension WorkflowStepX on WorkflowStep { WorkflowStep.notes => 'Notizen', WorkflowStep.articles => 'Artikel', WorkflowStep.services => 'Checkliste', + WorkflowStep.payment => 'Zahlung', WorkflowStep.summary => 'Übersicht', }; } @@ -38,8 +41,9 @@ class PendingImageNote { final DateTime pickedAt; } -/// State des Detail-Workflows. Ein State, ein Bloc — der Step-Wechsel, -/// die Drafts und die Payment-Auswahl liegen alle hier. So sieht jede +/// State des Detail-Workflows. Ein State, ein Bloc — der Step-Wechsel und +/// die Drafts liegen hier. Die Zahlung ist KEIN lokaler Draft mehr: Sie wird +/// im Step „Zahlung" am Server protokolliert und kommt über die Tour zurück. So sieht jede /// Step-Page denselben kohärenten Zustand und ein Step kann Daten aus /// einem anderen lesen (z. B. Summary liest Article-Drafts). class DeliveryWorkflowState { @@ -47,7 +51,6 @@ class DeliveryWorkflowState { required this.deliveryId, required this.step, required this.pendingImageNotes, - required this.paymentMethodOverrideId, }); factory DeliveryWorkflowState.initial(String deliveryId) => @@ -55,7 +58,6 @@ class DeliveryWorkflowState { deliveryId: deliveryId, step: WorkflowStep.info, pendingImageNotes: const [], - paymentMethodOverrideId: null, ); final String deliveryId; @@ -64,24 +66,14 @@ class DeliveryWorkflowState { /// Lokal gehaltene Bild-Notizen — solange kein Upload-Endpoint da ist. final List pendingImageNotes; - /// Wenn der Fahrer im Summary die Zahlungsmethode überschreibt, landet - /// die neue Id hier. `null` = Methode der Lieferung bleibt. - final String? paymentMethodOverrideId; - DeliveryWorkflowState copyWith({ WorkflowStep? step, List? pendingImageNotes, - Object? paymentMethodOverrideId = _sentinel, }) { return DeliveryWorkflowState( deliveryId: deliveryId, step: step ?? this.step, pendingImageNotes: pendingImageNotes ?? this.pendingImageNotes, - paymentMethodOverrideId: identical(paymentMethodOverrideId, _sentinel) - ? this.paymentMethodOverrideId - : paymentMethodOverrideId as String?, ); } } - -const Object _sentinel = Object(); diff --git a/lib/feature/delivery/detail/model/delivery_payment_status.dart b/lib/feature/delivery/detail/model/delivery_payment_status.dart new file mode 100644 index 0000000..e82c666 --- /dev/null +++ b/lib/feature/delivery/detail/model/delivery_payment_status.dart @@ -0,0 +1,80 @@ +import 'package:intl/intl.dart'; + +import 'package:hl_lieferservice/domain/entity/delivery.dart'; +import 'package:hl_lieferservice/domain/entity/delivery_payment.dart'; +import 'package:hl_lieferservice/domain/entity/tour_details.dart'; + +/// Zahlungsstand einer Lieferung — einzige Quelle für offenen Betrag und +/// „ist die Zahlung erledigt?". Genutzt von Zahlungs-Step, Übersicht und +/// Abschluss, damit alle exakt dieselben Zahlen sehen. +/// +/// Offener Betrag = Warenwert (Σ Stückpreis × ausgelieferte Menge) − +/// Anzahlung − Gutschrift, nie negativ. Identische Formel und Rundung wie +/// im Backend (`open_amount_cents`), das den Betrag beim Protokollieren +/// gegenprüft. +class DeliveryPaymentStatus { + const DeliveryPaymentStatus._({ + required this.goodsValue, + required this.prepaidAmount, + required this.creditAmount, + required this.openCents, + required this.latestPayment, + }); + + factory DeliveryPaymentStatus.of(Delivery delivery, TourDetails details) { + final goods = delivery.items.fold( + 0, + (acc, item) => acc + item.lineTotal, + ); + final credit = (details.creditOf(delivery.id)?.amountCents ?? 0) / 100.0; + final open = goods - delivery.prepaidAmount - credit; + return DeliveryPaymentStatus._( + goodsValue: goods, + prepaidAmount: delivery.prepaidAmount, + creditAmount: credit, + openCents: open <= 0 ? 0 : (open * 100).round(), + latestPayment: details.paymentOf(delivery.id), + ); + } + + /// Warenwert in Euro. + final double goodsValue; + + /// Bei Bestellung bezahlt (Anzahlung) in Euro. + final double prepaidAmount; + + /// Aktuelle Geld-Gutschrift in Euro (0 = keine). + final double creditAmount; + + /// Offener Betrag in Cent. + final int openCents; + + /// Jüngste protokollierte Zahlung, egal ob noch gültig. + final DeliveryPayment? latestPayment; + + double get openAmount => openCents / 100.0; + + bool get hasOpenAmount => openCents > 0; + + /// Die jüngste Zahlung, sofern sie zum aktuellen offenen Betrag passt. + DeliveryPayment? get validPayment { + final p = latestPayment; + if (p == null || !hasOpenAmount || p.amountCents != openCents) return null; + return p; + } + + /// Es wurde abgewickelt, aber der offene Betrag hat sich seitdem geändert. + bool get isStale => + latestPayment != null && hasOpenAmount && validPayment == null; + + /// Zahlungs-Step erledigt: nichts offen oder gültig abgewickelt. + bool get isSettled => !hasOpenAmount || validPayment != null; +} + +final NumberFormat _euro = NumberFormat.currency(locale: 'de_DE', symbol: '€'); + +/// Betrag in Euro im deutschen Format, z. B. `1.234,50 €`. +String formatEuro(double amount) => _euro.format(amount); + +/// Cent-Betrag im deutschen Format, z. B. `1.234,50 €`. +String formatEuroCents(int cents) => _euro.format(cents / 100.0); diff --git a/lib/feature/delivery/detail/presentation/delivery_detail_page.dart b/lib/feature/delivery/detail/presentation/delivery_detail_page.dart index eedd062..1e4d188 100644 --- a/lib/feature/delivery/detail/presentation/delivery_detail_page.dart +++ b/lib/feature/delivery/detail/presentation/delivery_detail_page.dart @@ -2,19 +2,19 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:hl_lieferservice/domain/entity/delivery.dart'; -import 'package:hl_lieferservice/domain/entity/payment_method.dart'; import 'package:hl_lieferservice/domain/entity/tour_details.dart'; import 'package:hl_lieferservice/feature/delivery/bloc/tour_bloc.dart'; -import 'package:hl_lieferservice/feature/payment_methods/bloc/payment_methods_cubit.dart'; import 'package:hl_lieferservice/feature/delivery/bloc/tour_event.dart'; import 'package:hl_lieferservice/feature/delivery/bloc/tour_state.dart'; import 'package:hl_lieferservice/feature/delivery/detail/presentation/delivery_sign.dart'; import 'package:hl_lieferservice/feature/delivery/detail/bloc/workflow_bloc.dart'; import 'package:hl_lieferservice/feature/delivery/detail/bloc/workflow_event.dart'; import 'package:hl_lieferservice/feature/delivery/detail/bloc/workflow_state.dart'; +import 'package:hl_lieferservice/feature/delivery/detail/model/delivery_payment_status.dart'; import 'package:hl_lieferservice/feature/delivery/detail/presentation/steps/step_articles.dart'; import 'package:hl_lieferservice/feature/delivery/detail/presentation/steps/step_info.dart'; import 'package:hl_lieferservice/feature/delivery/detail/presentation/steps/step_notes.dart'; +import 'package:hl_lieferservice/feature/delivery/detail/presentation/steps/step_payment.dart'; import 'package:hl_lieferservice/feature/delivery/detail/presentation/steps/step_services.dart'; import 'package:hl_lieferservice/feature/delivery/detail/presentation/steps/step_summary.dart'; @@ -32,10 +32,10 @@ import 'package:hl_lieferservice/feature/delivery/detail/presentation/steps/step // entgegennimmt und den Key zurückgibt, dann hier // `tourBloc.add(AddDeliveryNote(imageAttachment: key))` aufrufen. // -// * ~~B4 Zahlungsmethode beim Abschluss ändern~~ — ERLEDIGT: Die im -// Summary gewählte Methode (`paymentMethodOverrideId` im Workflow-State) -// reist beim Abschluss am `/complete`-Endpoint mit und wird atomar auf -// der Lieferung persistiert (Server prüft existiert + aktiv). +// * ~~B4 Zahlungsmethode beim Abschluss ändern~~ — ERLEDIGT und abgelöst: +// Die Zahlung wird im eigenen Step „Zahlung" abgewickelt und am Server +// protokolliert (`POST /deliveries/{id}/payment`). Beim Abschluss reist +// die Methode der gültigen Zahlung mit. // // * **B5 Unterschrift**: Signature-Pad-Bilder (Kunde + Fahrer) // hochladen + auf der Lieferung speichern. Backend hat dafür weder @@ -51,7 +51,7 @@ import 'package:hl_lieferservice/feature/delivery/detail/presentation/steps/step // SnackBar-Stub. // ───────────────────────────────────────────────────────────────────────── -/// Multi-Step Detail-Page einer einzelnen Lieferung. Hülle für 5 Steps; +/// Multi-Step Detail-Page einer einzelnen Lieferung. Hülle für 6 Steps; /// jeder Step bekommt die aktuelle `Delivery` + `TourDetails` als Props, /// damit die Steps keine eigenen Bloc-Subscriptions auf die Tour brauchen. /// @@ -95,7 +95,8 @@ class _DeliveryDetailScaffoldState extends State<_DeliveryDetailScaffold> { void initState() { super.initState(); final s = context.read().state; - if (s is TourLoaded && _findDelivery(s.details)?.state == DeliveryState.active) { + if (s is TourLoaded && + _findDelivery(s.details)?.state == DeliveryState.active) { _armed = true; } } @@ -153,7 +154,7 @@ class _DeliveryDetailScaffoldState extends State<_DeliveryDetailScaffold> { ), body: Column( children: [ - const _StepHeader(), + _StepHeader(delivery: delivery, details: details), const Divider(height: 1), Expanded( child: _StepBody(delivery: delivery, details: details), @@ -171,8 +172,23 @@ class _DeliveryDetailScaffoldState extends State<_DeliveryDetailScaffold> { // ─── Step-Header (Pills) ──────────────────────────────────────────────── +/// Steps hinter „Zahlung" sind gesperrt, solange bei einer aktiven +/// Lieferung ein offener Betrag noch nicht gültig abgewickelt ist. +bool _lockedByPayment( + WorkflowStep step, + Delivery delivery, + TourDetails details, +) { + if (step.index <= WorkflowStep.payment.index) return false; + if (delivery.state != DeliveryState.active) return false; + return !DeliveryPaymentStatus.of(delivery, details).isSettled; +} + class _StepHeader extends StatelessWidget { - const _StepHeader(); + const _StepHeader({required this.delivery, required this.details}); + + final Delivery delivery; + final TourDetails details; @override Widget build(BuildContext context) { @@ -189,9 +205,16 @@ class _StepHeader extends StatelessWidget { step: WorkflowStep.values[i], isActive: state.step.index == i, isPassed: state.step.index > i, - onTap: () => context - .read() - .add(WorkflowGoToStep(WorkflowStep.values[i])), + enabled: + !_lockedByPayment( + WorkflowStep.values[i], + delivery, + details, + ), + onTap: + () => context.read().add( + WorkflowGoToStep(WorkflowStep.values[i]), + ), ), ), if (i < WorkflowStep.values.length - 1) @@ -211,6 +234,7 @@ class _StepPill extends StatelessWidget { required this.step, required this.isActive, required this.isPassed, + required this.enabled, required this.onTap, }); @@ -218,6 +242,9 @@ class _StepPill extends StatelessWidget { final WorkflowStep step; final bool isActive; final bool isPassed; + + /// `false` = Step gesperrt (z. B. Übersicht vor abgewickelter Zahlung). + final bool enabled; final VoidCallback onTap; @override @@ -248,53 +275,58 @@ class _StepPill extends StatelessWidget { } return InkWell( - onTap: onTap, + onTap: enabled ? onTap : null, borderRadius: BorderRadius.circular(8), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Stack( - alignment: Alignment.center, - children: [ - Container( - width: 30, - height: 30, - decoration: BoxDecoration( - color: circleColor, - shape: BoxShape.circle, - border: isActive - ? Border.all(color: primary, width: 2) - : null, + child: Opacity( + opacity: enabled ? 1 : 0.4, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Stack( + alignment: Alignment.center, + children: [ + Container( + width: 30, + height: 30, + decoration: BoxDecoration( + color: circleColor, + shape: BoxShape.circle, + border: + isActive + ? Border.all(color: primary, width: 2) + : null, + ), + child: Center( + child: + isPassed && !isActive + ? Icon(Icons.check, color: circleFg, size: 16) + : Text( + '${index + 1}', + style: TextStyle( + color: circleFg, + fontWeight: FontWeight.bold, + fontSize: 13, + ), + ), + ), ), - child: Center( - child: isPassed && !isActive - ? Icon(Icons.check, color: circleFg, size: 16) - : Text( - '${index + 1}', - style: TextStyle( - color: circleFg, - fontWeight: FontWeight.bold, - fontSize: 13, - ), - ), - ), - ), - ], - ), - const SizedBox(height: 4), - Text( - step.shortName, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: labelColor, - fontSize: 11, - fontWeight: labelWeight, + ], ), - ), - ], + const SizedBox(height: 4), + Text( + step.shortName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: labelColor, + fontSize: 11, + fontWeight: labelWeight, + ), + ), + ], + ), ), ), ); @@ -313,9 +345,10 @@ class _StepConnector extends StatelessWidget { child: Container( width: 10, height: 2, - color: isPassed - ? theme.colorScheme.primary - : theme.colorScheme.surfaceContainerHighest, + color: + isPassed + ? theme.colorScheme.primary + : theme.colorScheme.surfaceContainerHighest, ), ); } @@ -343,6 +376,8 @@ class _StepBody extends StatelessWidget { return StepArticles(delivery: delivery, details: details); case WorkflowStep.services: return StepServices(delivery: delivery, details: details); + case WorkflowStep.payment: + return StepPayment(delivery: delivery, details: details); case WorkflowStep.summary: return StepSummary(delivery: delivery, details: details); } @@ -365,62 +400,33 @@ class _BottomNav extends StatelessWidget { /// dann den `completed`-Status zeigt. void _onSign(BuildContext context) { final tourBloc = context.read(); - // Die im Summary-Step gewählte Zahlungsmethode lebt im Workflow-State. - // Beim Abschluss reisen wir sie mit ans Backend (atomar mit der Signatur); - // `null` = die am Beleg hinterlegte Methode bleibt. - final paymentMethodOverrideId = - context.read().state.paymentMethodOverrideId; - - // Offener Betrag = Warenwert − Anzahlung − Gutschrift (≥ 0). EXAKT die - // Formel aus StepSummary und dem Backend-Inkasso-Gate. - final creditEuros = - (details.creditOf(delivery.id)?.amountCents ?? 0) / 100.0; - final warenwert = - delivery.items.fold(0, (acc, item) => acc + item.lineTotal); - final open = (warenwert - delivery.prepaidAmount - creditEuros) - .clamp(0.0, double.infinity) - .toDouble(); - - // Effektive Methode (Override > Beleg) auflösen, um Vor-Ort-Inkasso - // (Bar/EC) von „Auf Rechnung" zu unterscheiden. - final effectiveMethodId = - paymentMethodOverrideId ?? delivery.paymentMethodId; - final pmState = context.read().state; - PaymentMethod? method; - if (pmState is PaymentMethodsLoaded) { - for (final m in pmState.methods) { - if (m.id == effectiveMethodId) { - method = m; - break; - } - } - } - // Inkasso-Pflicht: offener Betrag > 0 UND Bar/EC. „Auf Rechnung" → nein. - final requiresCollection = - open > 0 && (method?.code == 'cash' || method?.code == 'ec_card'); + // Kassiert wurde bereits im Step „Zahlung" (serverseitig protokolliert). + // Beim Abschluss reisen Methode und Inkasso-Flag der gültigen Zahlung + // mit; ohne offenen Betrag bleibt die Methode vom Beleg. + final payment = DeliveryPaymentStatus.of(delivery, details).validPayment; Navigator.of(context).push( MaterialPageRoute( - builder: (routeContext) => SignatureView( - delivery: delivery, - details: details, - requiresCollection: requiresCollection, - openAmount: open, - paymentMethodLabel: method?.name ?? '', - onSigned: (result) { - tourBloc.add(CompleteDelivery( - deliveryId: delivery.id, - customerSignaturePng: result.customerSignaturePng, - driverSignaturePng: result.driverSignaturePng, - receiptConfirmed: result.receiptConfirmed, - notesAcknowledged: result.notesAcknowledged, - acknowledgedNoteIds: result.acknowledgedNoteIds, - paymentMethodId: paymentMethodOverrideId, - paymentCollected: result.paymentCollected, - )); - Navigator.of(routeContext).pop(); - }, - ), + builder: + (routeContext) => SignatureView( + delivery: delivery, + details: details, + onSigned: (result) { + tourBloc.add( + CompleteDelivery( + deliveryId: delivery.id, + customerSignaturePng: result.customerSignaturePng, + driverSignaturePng: result.driverSignaturePng, + receiptConfirmed: result.receiptConfirmed, + notesAcknowledged: result.notesAcknowledged, + acknowledgedNoteIds: result.acknowledgedNoteIds, + paymentMethodId: payment?.paymentMethodId, + paymentCollected: payment?.isCollectedOnSite ?? false, + ), + ); + Navigator.of(routeContext).pop(); + }, + ), ), ); } @@ -438,11 +444,12 @@ class _BottomNav extends StatelessWidget { child: Row( children: [ OutlinedButton.icon( - onPressed: isFirst - ? null - : () => context - .read() - .add(const WorkflowPreviousStep()), + onPressed: + isFirst + ? null + : () => context.read().add( + const WorkflowPreviousStep(), + ), icon: const Icon(Icons.arrow_back), label: const Text('Zurück'), ), @@ -451,26 +458,37 @@ class _BottomNav extends StatelessWidget { // Unterschreiben/Abschließen nur bei aktiver Lieferung. // Ist sie bereits abgeschlossen (oder pausiert/abgebrochen), // bleibt der Button gesperrt. - Builder(builder: (context) { - final isActive = - delivery.state == DeliveryState.active; - final isCompleted = - delivery.state == DeliveryState.completed; - return FilledButton.icon( - onPressed: isActive ? () => _onSign(context) : null, - icon: Icon(isCompleted - ? Icons.check_circle_outline - : Icons.draw_outlined), - label: Text( - isCompleted ? 'Abgeschlossen' : 'Unterschreiben', - ), - ); - }) + Builder( + builder: (context) { + final isActive = delivery.state == DeliveryState.active; + final isCompleted = + delivery.state == DeliveryState.completed; + return FilledButton.icon( + onPressed: isActive ? () => _onSign(context) : null, + icon: Icon( + isCompleted + ? Icons.check_circle_outline + : Icons.draw_outlined, + ), + label: Text( + isCompleted ? 'Abgeschlossen' : 'Unterschreiben', + ), + ); + }, + ) else FilledButton.icon( - onPressed: () => context - .read() - .add(const WorkflowNextStep()), + // Aus „Zahlung" erst weiter, wenn abgewickelt/nichts offen. + onPressed: + _lockedByPayment( + WorkflowStep.values[state.step.index + 1], + delivery, + details, + ) + ? null + : () => context.read().add( + const WorkflowNextStep(), + ), icon: const Icon(Icons.arrow_forward), label: const Text('Weiter'), ), diff --git a/lib/feature/delivery/detail/presentation/delivery_sign.dart b/lib/feature/delivery/detail/presentation/delivery_sign.dart index 64572a4..c77757a 100644 --- a/lib/feature/delivery/detail/presentation/delivery_sign.dart +++ b/lib/feature/delivery/detail/presentation/delivery_sign.dart @@ -17,7 +17,6 @@ class SignatureResult { required this.receiptConfirmed, required this.notesAcknowledged, required this.acknowledgedNoteIds, - required this.paymentCollected, }); final Uint8List customerSignaturePng; @@ -25,18 +24,13 @@ class SignatureResult { final bool receiptConfirmed; final bool notesAcknowledged; final List acknowledgedNoteIds; - - /// Fahrer hat das Inkasso (Bar/EC) des offenen Betrags bestätigt. `false`, - /// wenn kein Inkasso anfiel (offen == 0 oder „Auf Rechnung"). - final bool paymentCollected; } /// Mehrstufiger Unterschrift-Flow zum Abschließen einer Lieferung. /// -/// Stufe 0 (Fahrer, optional): nur wenn beim Abschluss ein offener Betrag -/// per Vor-Ort-Inkasso (Bar/EC) zu kassieren ist ([requiresCollection]). -/// Der Fahrer bestätigt, dass der Betrag erhalten/abgerechnet wurde — VOR -/// beiden Unterschriften. +/// Das Kassieren passiert vorher im eigenen Workflow-Step „Zahlung" und +/// wird dort protokolliert — dieser Flow enthält nur noch die Unterschriften. +/// /// Stufe 1 (Kunde): sieht die Anmerkungen zur Lieferung, hakt zwei /// Bestätigungen ab (Anmerkungen-Kenntnisnahme — nur Pflicht, wenn Notizen /// vorhanden; Empfangsbestätigung — immer Pflicht) und unterschreibt. @@ -51,31 +45,18 @@ class SignatureView extends StatefulWidget { required this.delivery, required this.details, required this.onSigned, - this.requiresCollection = false, - this.openAmount = 0, - this.paymentMethodLabel = '', }); final Delivery delivery; final TourDetails details; final void Function(SignatureResult result) onSigned; - /// Offener Betrag muss vor Ort kassiert werden (offen > 0 UND Bar/EC). - /// Schaltet Stufe 0 (Inkasso-Bestätigung) frei. - final bool requiresCollection; - - /// Offener Betrag in Euro (nur für die Anzeige in Stufe 0). - final double openAmount; - - /// Anzeigename der Zahlungsmethode (z. B. „Bar", „EC-Karte"). - final String paymentMethodLabel; - @override State createState() => _SignatureViewState(); } /// Stufen des Abschluss-Flows. -enum _SignStage { payment, customer, driver } +enum _SignStage { customer, driver } class _SignatureViewState extends State { static const String _receiptText = @@ -97,8 +78,7 @@ class _SignatureViewState extends State { ); late final List _notes; - late _SignStage _stage; - bool _paymentConfirmed = false; + _SignStage _stage = _SignStage.customer; bool _receiptAccepted = false; bool _notesAccepted = false; bool _customerEmpty = true; @@ -109,10 +89,8 @@ class _SignatureViewState extends State { @override void initState() { super.initState(); - // Inkasso-Bestätigung (Stufe 0) nur wenn gefordert, sonst direkt zum Kunden. - _stage = - widget.requiresCollection ? _SignStage.payment : _SignStage.customer; - _notes = widget.details.notesByDeliveryId[widget.delivery.id] ?? + _notes = + widget.details.notesByDeliveryId[widget.delivery.id] ?? const []; _customerController.addListener(() { if (_customerEmpty != _customerController.isEmpty) { @@ -139,8 +117,6 @@ class _SignatureViewState extends State { /// Ist der Primär-Button auf der aktuellen Stufe aktiv? bool get _stageValid { switch (_stage) { - case _SignStage.payment: - return _paymentConfirmed; case _SignStage.customer: return _customerStepValid; case _SignStage.driver: @@ -150,9 +126,6 @@ class _SignatureViewState extends State { Future _onPrimaryPressed() async { switch (_stage) { - case _SignStage.payment: - setState(() => _stage = _SignStage.customer); - return; case _SignStage.customer: setState(() => _stage = _SignStage.driver); return; @@ -168,33 +141,20 @@ class _SignatureViewState extends State { notesAcknowledged: _notesEmpty ? false : _notesAccepted, acknowledgedNoteIds: _notesEmpty ? const [] : _notes.map((n) => n.id).toList(), - paymentCollected: widget.requiresCollection && _paymentConfirmed, ), ); } } - String get _paymentText { - final amount = widget.openAmount.toStringAsFixed(2).replaceAll('.', ','); - final via = widget.paymentMethodLabel.isEmpty - ? '' - : ' per ${widget.paymentMethodLabel}'; - return 'Ich bestätige, dass der offene Betrag von $amount €$via ' - 'erhalten bzw. abgerechnet wurde.'; - } - @override Widget build(BuildContext context) { final theme = Theme.of(context); final customer = widget.details.customerOf(widget.delivery); final date = DateFormat('dd.MM.yyyy').format(DateTime.now()); - final isPayment = _stage == _SignStage.payment; final isDriver = _stage == _SignStage.driver; final String title; switch (_stage) { - case _SignStage.payment: - title = 'Zahlung bestätigen'; case _SignStage.customer: title = 'Unterschrift des Kunden'; case _SignStage.driver: @@ -207,56 +167,42 @@ class _SignatureViewState extends State { child: ListView( padding: const EdgeInsets.all(16), children: [ - if (isPayment) ...[ - // Stufe 0 — Fahrer kassiert (Bar/EC) und bestätigt VOR den - // Unterschriften. - _PaymentDueCard( - openAmount: widget.openAmount, - methodLabel: widget.paymentMethodLabel, - ), + if (!isDriver) ...[ + _NotesSection(notes: _notes), const SizedBox(height: 16), _ConfirmTile( - value: _paymentConfirmed, + value: _notesAccepted, + enabled: !_notesEmpty, + label: _notesText, + onChanged: (v) => setState(() => _notesAccepted = v), + ), + _ConfirmTile( + value: _receiptAccepted, enabled: true, - label: _paymentText, - onChanged: (v) => setState(() => _paymentConfirmed = v), - ), - ] else ...[ - if (!isDriver) ...[ - _NotesSection(notes: _notes), - const SizedBox(height: 16), - _ConfirmTile( - value: _notesAccepted, - enabled: !_notesEmpty, - label: _notesText, - onChanged: (v) => setState(() => _notesAccepted = v), - ), - _ConfirmTile( - value: _receiptAccepted, - enabled: true, - label: _receiptText, - onChanged: (v) => setState(() => _receiptAccepted = v), - ), - const SizedBox(height: 16), - ], - Text( - 'Lieferung an: ${customer?.name ?? '⟨Unbekannter Kunde⟩'}', - style: theme.textTheme.titleSmall - ?.copyWith(fontWeight: FontWeight.w700), - ), - const SizedBox(height: 8), - _SignaturePad( - controller: isDriver ? _driverController : _customerController, - onClear: () => - (isDriver ? _driverController : _customerController) - .clear(), - ), - const SizedBox(height: 4), - Text( - '${customer?.address.city ?? ''}, den $date', - style: theme.textTheme.bodySmall, + label: _receiptText, + onChanged: (v) => setState(() => _receiptAccepted = v), ), + const SizedBox(height: 16), ], + Text( + 'Lieferung an: ${customer?.name ?? '⟨Unbekannter Kunde⟩'}', + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 8), + _SignaturePad( + controller: isDriver ? _driverController : _customerController, + onClear: + () => + (isDriver ? _driverController : _customerController) + .clear(), + ), + const SizedBox(height: 4), + Text( + '${customer?.address.city ?? ''}, den $date', + style: theme.textTheme.bodySmall, + ), const SizedBox(height: 24), FilledButton.icon( onPressed: _stageValid ? _onPrimaryPressed : null, @@ -270,66 +216,6 @@ class _SignatureViewState extends State { } } -// ─── Inkasso-Hinweis (Stufe 0) ────────────────────────────────────────────── - -class _PaymentDueCard extends StatelessWidget { - const _PaymentDueCard({required this.openAmount, required this.methodLabel}); - - final double openAmount; - final String methodLabel; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final amount = openAmount.toStringAsFixed(2).replaceAll('.', ','); - return Card( - margin: EdgeInsets.zero, - color: theme.colorScheme.primary.withValues(alpha: 0.07), - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Offener Betrag', - style: theme.textTheme.titleMedium - ?.copyWith(fontWeight: FontWeight.w700), - ), - const SizedBox(height: 8), - Row( - children: [ - Icon(Icons.payments_outlined, color: theme.colorScheme.primary), - const SizedBox(width: 10), - Text( - '$amount €', - style: theme.textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.w800, - color: theme.colorScheme.primary, - ), - ), - const Spacer(), - if (methodLabel.isNotEmpty) - Chip( - label: Text(methodLabel), - visualDensity: VisualDensity.compact, - ), - ], - ), - const SizedBox(height: 8), - Text( - 'Bitte den Betrag bar entgegennehmen oder über das EC-Gerät ' - 'abrechnen und anschließend bestätigen.', - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ), - ); - } -} - // ─── Notizen-Block ──────────────────────────────────────────────────────── class _NotesSection extends StatelessWidget { @@ -345,8 +231,9 @@ class _NotesSection extends StatelessWidget { children: [ Text( 'Anmerkungen zur Lieferung', - style: - theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700), + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w700, + ), ), const SizedBox(height: 8), if (notes.isEmpty) diff --git a/lib/feature/delivery/detail/presentation/steps/step_info.dart b/lib/feature/delivery/detail/presentation/steps/step_info.dart index c574dbf..03e6e47 100644 --- a/lib/feature/delivery/detail/presentation/steps/step_info.dart +++ b/lib/feature/delivery/detail/presentation/steps/step_info.dart @@ -12,6 +12,7 @@ import 'package:hl_lieferservice/feature/delivery/bloc/tour_event.dart'; import 'package:hl_lieferservice/feature/delivery/detail/bloc/workflow_bloc.dart'; import 'package:hl_lieferservice/feature/delivery/detail/bloc/workflow_event.dart'; import 'package:hl_lieferservice/feature/delivery/detail/bloc/workflow_state.dart'; +import 'package:hl_lieferservice/feature/delivery/detail/presentation/widget/reason_dialog.dart'; /// Step 1 — Informationen zur Lieferung. /// @@ -245,39 +246,6 @@ class _QuickActions extends StatelessWidget { await _call(context, picked.value); } - Future _askReason(BuildContext context, String title) async { - final controller = TextEditingController(); - final result = await showDialog( - context: context, - builder: (dialogContext) => AlertDialog( - title: Text(title), - content: TextField( - controller: controller, - autofocus: true, - decoration: const InputDecoration( - labelText: 'Grund', - border: OutlineInputBorder(), - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(dialogContext).pop(null), - child: const Text('Abbrechen'), - ), - FilledButton( - onPressed: () { - final text = controller.text.trim(); - if (text.isEmpty) return; - Navigator.of(dialogContext).pop(text); - }, - child: const Text('Bestätigen'), - ), - ], - ), - ); - return result; - } - Future _onStatusSelected( BuildContext context, _StatusAction action, @@ -285,11 +253,11 @@ class _QuickActions extends StatelessWidget { final tourBloc = context.read(); switch (action) { case _StatusAction.hold: - final reason = await _askReason(context, 'Lieferung pausieren'); + final reason = await showReasonDialog(context, 'Lieferung pausieren'); if (reason == null) return; tourBloc.add(HoldDelivery(deliveryId: delivery.id, reason: reason)); case _StatusAction.cancel: - final reason = await _askReason(context, 'Lieferung abbrechen'); + final reason = await showReasonDialog(context, 'Lieferung abbrechen'); if (reason == null) return; tourBloc.add(CancelDelivery(deliveryId: delivery.id, reason: reason)); case _StatusAction.resume: diff --git a/lib/feature/delivery/detail/presentation/steps/step_payment.dart b/lib/feature/delivery/detail/presentation/steps/step_payment.dart new file mode 100644 index 0000000..e9edde6 --- /dev/null +++ b/lib/feature/delivery/detail/presentation/steps/step_payment.dart @@ -0,0 +1,185 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:hl_lieferservice/domain/entity/delivery.dart'; +import 'package:hl_lieferservice/domain/entity/tour_details.dart'; +import 'package:hl_lieferservice/feature/car_selection/bloc/bloc.dart'; +import 'package:hl_lieferservice/feature/car_selection/bloc/state.dart'; +import 'package:hl_lieferservice/feature/delivery/bloc/tour_bloc.dart'; +import 'package:hl_lieferservice/feature/delivery/bloc/tour_event.dart'; +import 'package:hl_lieferservice/feature/delivery/detail/model/delivery_payment_status.dart'; +import 'package:hl_lieferservice/feature/delivery/detail/presentation/widget/delivered_items_card.dart'; +import 'package:hl_lieferservice/feature/delivery/detail/presentation/widget/payment_breakdown_card.dart'; +import 'package:hl_lieferservice/feature/delivery/detail/presentation/widget/payment_record_card.dart'; +import 'package:hl_lieferservice/feature/delivery/detail/presentation/widget/payment_sheet.dart'; + +/// Step „Zahlung" — Zahlungsabwicklung („Abkassieren") vor der Übersicht. +/// +/// Zeigt die ausgelieferten Artikel mit Menge und Preis, die Betrags- +/// aufstellung und den Zahlungsstatus. Über „Zahlung abwickeln" öffnet sich +/// das Zahlungs-Modal; die Bestätigung wird am Server protokolliert. +/// +/// Ist nichts offen (bereits bezahlt), kann der Fahrer den Step +/// überspringen. Sonst gibt die Detail-Page „Weiter" erst frei, wenn eine +/// gültige Zahlung protokolliert ist (siehe `DeliveryPaymentStatus`). +class StepPayment extends StatelessWidget { + const StepPayment({super.key, required this.delivery, required this.details}); + + final Delivery delivery; + final TourDetails details; + + @override + Widget build(BuildContext context) { + final status = DeliveryPaymentStatus.of(delivery, details); + final canCollect = + delivery.state == DeliveryState.active && status.hasOpenAmount; + + return Column( + children: [ + Expanded( + child: ListView( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 24), + children: [ + const _SectionHeader(text: 'Ausgelieferte Artikel'), + const SizedBox(height: 8), + DeliveredItemsCard(delivery: delivery, details: details), + const SizedBox(height: 24), + const _SectionHeader(text: 'Betrag'), + const SizedBox(height: 8), + PaymentBreakdownCard( + status: status, + credit: details.creditOf(delivery.id), + ), + const SizedBox(height: 16), + PaymentRecordCard(status: status), + ], + ), + ), + if (canCollect) + _CollectPanel( + status: status, + onCollect: () => _openPaymentSheet(context, status), + ), + ], + ); + } + + Future _openPaymentSheet( + BuildContext context, + DeliveryPaymentStatus status, + ) async { + final tourBloc = context.read(); + final actorCarId = _actorCarId(context); + final customer = details.customerOf(delivery); + + final result = await showPaymentSheet( + context, + openCents: status.openCents, + customerNumber: customer?.erpCustomerId ?? 0, + // Vorauswahl: zuletzt abgewickelte Methode, sonst die vom Beleg. + preselectedMethodId: + status.latestPayment?.paymentMethodId ?? delivery.paymentMethodId, + ); + + switch (result) { + case PaymentConfirmed(:final method): + tourBloc.add( + RecordDeliveryPayment( + deliveryId: delivery.id, + paymentMethodId: method.id, + expectedAmountCents: status.openCents, + actorCarId: actorCarId, + ), + ); + case DeliveryCancelRequested(:final reason): + tourBloc.add(CancelDelivery(deliveryId: delivery.id, reason: reason)); + case null: + break; + } + } + + String? _actorCarId(BuildContext context) { + final state = context.read().state; + return state is CarSelectComplete ? state.selectedCar.id : null; + } +} + +/// Fester Bereich unten: offener Betrag + großer „Zahlung abwickeln"-Button. +/// Bleibt sichtbar, auch wenn die Artikelliste lang ist. +class _CollectPanel extends StatelessWidget { + const _CollectPanel({required this.status, required this.onCollect}); + + final DeliveryPaymentStatus status; + final VoidCallback onCollect; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final settled = status.validPayment != null; + return Material( + elevation: 6, + color: theme.colorScheme.surface, + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Text('Offener Betrag', style: theme.textTheme.titleMedium), + const Spacer(), + Text( + formatEuroCents(status.openCents), + style: theme.textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.w800, + color: theme.colorScheme.primary, + ), + ), + ], + ), + const SizedBox(height: 10), + if (settled) + // Bereits abgewickelt: Korrektur möglich, aber nicht mehr im + // Vordergrund — „Weiter" ist jetzt der nächste Schritt. + OutlinedButton.icon( + style: OutlinedButton.styleFrom( + minimumSize: const Size.fromHeight(52), + ), + onPressed: onCollect, + icon: const Icon(Icons.edit_outlined), + label: const Text('Zahlung korrigieren'), + ) + else + FilledButton.icon( + style: FilledButton.styleFrom( + minimumSize: const Size.fromHeight(68), + textStyle: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.w700, + ), + ), + onPressed: onCollect, + icon: const Icon(Icons.point_of_sale, size: 28), + label: const Text('Zahlung abwickeln'), + ), + ], + ), + ), + ); + } +} + +class _SectionHeader extends StatelessWidget { + const _SectionHeader({required this.text}); + final String text; + + @override + Widget build(BuildContext context) { + return Text( + text, + style: Theme.of( + context, + ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700), + ); + } +} diff --git a/lib/feature/delivery/detail/presentation/steps/step_summary.dart b/lib/feature/delivery/detail/presentation/steps/step_summary.dart index d91ef45..4cd420d 100644 --- a/lib/feature/delivery/detail/presentation/steps/step_summary.dart +++ b/lib/feature/delivery/detail/presentation/steps/step_summary.dart @@ -1,68 +1,49 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:hl_lieferservice/domain/entity/delivery.dart'; -import 'package:hl_lieferservice/domain/entity/delivery_credit.dart'; -import 'package:hl_lieferservice/domain/entity/delivery_item.dart'; import 'package:hl_lieferservice/domain/entity/tour_details.dart'; -import 'package:hl_lieferservice/feature/delivery/detail/bloc/workflow_bloc.dart'; -import 'package:hl_lieferservice/feature/delivery/detail/bloc/workflow_event.dart'; -import 'package:hl_lieferservice/feature/delivery/detail/bloc/workflow_state.dart'; -import 'package:hl_lieferservice/feature/payment_methods/bloc/payment_methods_cubit.dart'; +import 'package:hl_lieferservice/feature/delivery/detail/model/delivery_payment_status.dart'; +import 'package:hl_lieferservice/feature/delivery/detail/presentation/widget/delivered_items_card.dart'; +import 'package:hl_lieferservice/feature/delivery/detail/presentation/widget/payment_breakdown_card.dart'; +import 'package:hl_lieferservice/feature/delivery/detail/presentation/widget/payment_record_card.dart'; -/// Step 5 — Übersicht & Abschluss. +/// Step 6 — Übersicht & Abschluss. /// -/// Listet alle Artikel mit der **tatsächlich auszuliefernden Menge** auf -/// (Original-Soll minus lokaler Partial-Remove-Drafts minus -/// Komplett-Removes). Dazu Anzahlung-Anzeige, optionale Gutschrift, -/// Zahlungsmethoden-Dropdown. +/// Listet alle Artikel mit der **tatsächlich auszuliefernden Menge** auf, +/// dazu die Betragsaufstellung und den Stand der Zahlung. Die Zahlungsart +/// wird nicht mehr hier gewählt, sondern im Step „Zahlung" abgewickelt und +/// protokolliert — die Übersicht zeigt das Ergebnis nur an. /// /// Der „Unterschreiben"-Button lebt in der Bottom-Navigation des /// Page-Wrappers; hier zeigen wir den Resümee-Block, der direkt vor der /// Unterschrift steht. class StepSummary extends StatelessWidget { - const StepSummary({ - super.key, - required this.delivery, - required this.details, - }); + const StepSummary({super.key, required this.delivery, required this.details}); final Delivery delivery; final TourDetails details; @override Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, wfState) { - return ListView( - padding: const EdgeInsets.fromLTRB(16, 16, 16, 24), - children: [ - _SectionHeader(text: 'Ausgelieferte Artikel'), - const SizedBox(height: 8), - _DeliveredItems( - delivery: delivery, - details: details, - ), - const SizedBox(height: 24), - _SectionHeader(text: 'Zahlung'), - const SizedBox(height: 8), - _PaymentSummary( - delivery: delivery, - credit: details.creditOf(delivery.id), - ), - const SizedBox(height: 24), - _SectionHeader(text: 'Zahlungsmethode'), - const SizedBox(height: 8), - _PaymentMethodPicker( - delivery: delivery, - overrideId: wfState.paymentMethodOverrideId, - credit: details.creditOf(delivery.id), - ), - const SizedBox(height: 16), - const _SignHint(), - ], - ); - }, + final status = DeliveryPaymentStatus.of(delivery, details); + return ListView( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 24), + children: [ + const _SectionHeader(text: 'Ausgelieferte Artikel'), + const SizedBox(height: 8), + DeliveredItemsCard(delivery: delivery, details: details), + const SizedBox(height: 24), + const _SectionHeader(text: 'Zahlung'), + const SizedBox(height: 8), + PaymentBreakdownCard( + status: status, + credit: details.creditOf(delivery.id), + ), + const SizedBox(height: 12), + PaymentRecordCard(status: status), + const SizedBox(height: 16), + const _SignHint(), + ], ); } } @@ -75,452 +56,9 @@ class _SectionHeader extends StatelessWidget { Widget build(BuildContext context) { return Text( text, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w700, - ), - ); - } -} - -class _DeliveredItems extends StatelessWidget { - const _DeliveredItems({ - required this.delivery, - required this.details, - }); - - final Delivery delivery; - final TourDetails details; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - // Innerhalb einer Belegzeile: Oberartikel vor seinen Komponenten, damit - // Komponenten direkt darunter eingerückt erscheinen. - final items = List.of(delivery.items) - ..sort((a, b) { - final byLine = a.belegzeilenNr.compareTo(b.belegzeilenNr); - if (byLine != 0) return byLine; - final byParent = - (a.isComponent ? 1 : 0).compareTo(b.isComponent ? 1 : 0); - if (byParent != 0) return byParent; - return (a.komponentenArtikelNr ?? '') - .compareTo(b.komponentenArtikelNr ?? ''); - }); - if (items.isEmpty) { - return Card( - margin: EdgeInsets.zero, - child: Padding( - padding: const EdgeInsets.all(16), - child: Text( - 'Keine Artikel hinterlegt.', - style: TextStyle( - color: theme.colorScheme.onSurfaceVariant, - fontStyle: FontStyle.italic, - ), - ), - ), - ); - } - return Card( - margin: EdgeInsets.zero, - child: Column( - children: [ - for (int i = 0; i < items.length; i++) ...[ - _DeliveredRow( - item: items[i], - details: details, - ), - if (i < items.length - 1) - const Divider(height: 1, indent: 16, endIndent: 16), - ], - ], - ), - ); - } -} - -class _DeliveredRow extends StatelessWidget { - const _DeliveredRow({ - required this.item, - required this.details, - }); - - final DeliveryItem item; - final TourDetails details; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final article = details.articleOf(item.articleId); - // Ausgeliefert = Soll − Gutschrift (vom Backend). Voll gutgeschrieben - // (status removed) ⇒ credited == required ⇒ delivered 0. - final credited = item.scanProgress.creditedQuantity; - final delivered = (item.requiredQuantity - credited).clamp( - 0, - item.requiredQuantity, - ); - - final Color avatarColor; - if (delivered == 0) { - avatarColor = Colors.red.shade400; - } else if (delivered < item.requiredQuantity) { - avatarColor = Colors.amber.shade700; - } else { - avatarColor = Colors.green.shade600; - } - - return ListTile( - // Komponenten um eine Stufe eingerückt (gehören zum Oberartikel darüber). - contentPadding: - EdgeInsets.only(left: item.isComponent ? 40 : 16, right: 16), - leading: CircleAvatar( - backgroundColor: avatarColor, - foregroundColor: theme.colorScheme.onPrimary, - child: Text( - '$delivered×', - style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold), - ), - ), - title: Text( - '${item.isComponent ? '↳ ' : ''}${article?.name ?? '⟨Unbekannter Artikel⟩'}', - style: TextStyle( - fontWeight: FontWeight.w600, - decoration: delivered == 0 ? TextDecoration.lineThrough : null, - color: delivered == 0 ? theme.colorScheme.onSurfaceVariant : null, - ), - ), - subtitle: Text( - [ - if (delivered < item.requiredQuantity) - 'von ${item.requiredQuantity} bestellt · Gutschrift: $credited' - else - 'Artikelnr. ${article?.articleNumber ?? item.articleId}', - '${item.unitPrice.toStringAsFixed(2)} € / Stück', - ].join(' · '), - style: TextStyle( - fontSize: 12, - color: theme.colorScheme.onSurfaceVariant, - ), - ), - trailing: Text( - '${item.lineTotal.toStringAsFixed(2)} €', - style: theme.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w700, - decoration: delivered == 0 ? TextDecoration.lineThrough : null, - color: delivered == 0 ? theme.colorScheme.onSurfaceVariant : null, - ), - ), - ); - } -} - -/// Offener Betrag der Lieferung in Euro: Warenwert (Σ Stückpreis × gelieferte -/// Menge) − Anzahlung − Gutschrift, nie negativ. Einzige Quelle dieser Formel — -/// genutzt von der Zahlungs-Übersicht UND der Zahlungsmethoden-Auswahl. -double _openAmount(Delivery delivery, DeliveryCredit? credit) { - final creditEuros = (credit?.amountCents ?? 0) / 100.0; - final warenwert = - delivery.items.fold(0, (acc, item) => acc + item.lineTotal); - return (warenwert - delivery.prepaidAmount - creditEuros) - .clamp(0.0, double.infinity); -} - -class _PaymentSummary extends StatelessWidget { - const _PaymentSummary({required this.delivery, required this.credit}); - - final Delivery delivery; - final DeliveryCredit? credit; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - // Warenwert = Σ Stückpreis × ausgelieferte Menge (entfernte/teil-entfernte - // Positionen fallen automatisch raus). - final warenwert = delivery.items - .fold(0, (acc, item) => acc + item.lineTotal); - // Offener Betrag über den gemeinsamen Helper (gleiche Formel wie die - // Zahlungsmethoden-Auswahl). - final open = _openAmount(delivery, credit); - return Card( - margin: EdgeInsets.zero, - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - children: [ - _SummaryRow( - icon: Icons.receipt_long_outlined, - label: 'Warenwert', - valueText: '${warenwert.toStringAsFixed(2)} €', - valueColor: theme.colorScheme.onSurface, - ), - const SizedBox(height: 12), - _SummaryRow( - icon: Icons.savings_outlined, - label: 'Bei Bestellung bezahlt', - valueText: '− ${delivery.prepaidAmount.toStringAsFixed(2)} €', - valueColor: delivery.prepaidAmount > 0 - ? Colors.green.shade700 - : theme.colorScheme.onSurfaceVariant, - ), - if (credit != null) ...[ - const SizedBox(height: 12), - _SummaryRow( - icon: Icons.card_giftcard_outlined, - label: 'Gutschrift', - valueText: '− ${(credit!.amountCents / 100).toStringAsFixed(2)} €', - valueColor: Colors.amber.shade800, - subtitle: credit!.reason, - ), - ], - const Divider(height: 24), - _SummaryRow( - icon: Icons.account_balance_wallet_outlined, - label: 'Offener Betrag', - valueText: '${open.toStringAsFixed(2)} €', - valueColor: open > 0 - ? theme.colorScheme.primary - : Colors.green.shade700, - emphasize: true, - ), - ], - ), - ), - ); - } -} - -class _SummaryRow extends StatelessWidget { - const _SummaryRow({ - required this.icon, - required this.label, - required this.valueText, - required this.valueColor, - this.subtitle, - this.emphasize = false, - }); - - final IconData icon; - final String label; - final String valueText; - final Color valueColor; - final String? subtitle; - - /// Hebt Label + Wert hervor (für den „Offener Betrag"-Abschluss). - final bool emphasize; - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(top: 2), - child: Icon(icon, color: theme.colorScheme.primary), - ), - const SizedBox(width: 10), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - label, - style: emphasize - ? const TextStyle(fontWeight: FontWeight.w700) - : null, - ), - if (subtitle != null) - Text( - subtitle!, - style: TextStyle( - fontSize: 12, - color: theme.colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ), - Text( - valueText, - style: (emphasize - ? theme.textTheme.titleLarge - : theme.textTheme.titleMedium) - ?.copyWith( - fontWeight: FontWeight.w700, - color: valueColor, - ), - ), - ], - ); - } -} - -class _PaymentMethodPicker extends StatelessWidget { - const _PaymentMethodPicker({ - required this.delivery, - required this.overrideId, - required this.credit, - }); - - final Delivery delivery; - final String? overrideId; - final DeliveryCredit? credit; - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - if (state is PaymentMethodsLoading || state is PaymentMethodsInitial) { - return const Card( - margin: EdgeInsets.zero, - child: Padding( - padding: EdgeInsets.all(16), - child: Row( - children: [ - SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator(strokeWidth: 2), - ), - SizedBox(width: 12), - Text('Zahlungsmethoden laden …'), - ], - ), - ), - ); - } - if (state is PaymentMethodsFailed) { - final onError = Theme.of(context).colorScheme.onErrorContainer; - return Card( - margin: EdgeInsets.zero, - color: Theme.of(context).colorScheme.errorContainer, - child: Padding( - padding: const EdgeInsets.fromLTRB(16, 8, 8, 8), - child: Row( - children: [ - Expanded( - child: Text( - state.message, - style: TextStyle(color: onError), - ), - ), - // Fehler ist kein Endzustand: der Fahrer kann das Laden - // direkt hier erneut anstoßen (z. B. nach Netz-Aussetzer). - TextButton.icon( - onPressed: () => - context.read().load(), - icon: Icon(Icons.refresh, color: onError), - label: Text( - 'Erneut laden', - style: TextStyle(color: onError), - ), - ), - ], - ), - ), - ); - } - final loaded = state as PaymentMethodsLoaded; - // Ausschließlich die Backend-Methoden — keine frontend-seitige - // Fabrikation/Hardcodierung. Es werden genau die angezeigt, die im - // Backend (Postgres `payment_methods`, aktiv) hinterlegt sind. - final methods = loaded.methods; - final selectedId = overrideId ?? delivery.paymentMethodId; - // Als Dropdown-Value nur setzen, wenn die Methode tatsächlich in der - // Backend-Liste ist (sonst würde Flutter asserten). Ist die zugewiesene - // Methode zwischenzeitlich deaktiviert/entfernt, bleibt das Feld leer. - final selectedValue = - methods.any((m) => m.id == selectedId) ? selectedId : null; - // Zahlungsmethode nur bei aktiver Lieferung änderbar. Bei - // abgeschlossener/abgebrochener/pausierter Lieferung zeigt das - // Dropdown den gewählten Stand, ist aber gesperrt. - final active = delivery.state == DeliveryState.active; - // Steht kein offener Betrag mehr aus (vollständig vorab bezahlt - // oder per Gutschrift ausgeglichen), ist keine Zahlungsweise zu - // wählen → Auswahl deaktivieren. - final hasOpenAmount = _openAmount(delivery, credit) > 0; - final editable = active && hasOpenAmount; - return Card( - margin: EdgeInsets.zero, - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - DropdownButtonFormField( - initialValue: selectedValue, - decoration: const InputDecoration( - labelText: 'Zahlungsmethode', - border: OutlineInputBorder(), - ), - items: [ - for (final m in methods) - DropdownMenuItem( - value: m.id, - child: Text(m.name), - ), - ], - // `null` deaktiviert das Dropdown (Flutter-Konvention). - onChanged: editable - ? (newId) { - if (newId == null) return; - context.read().add( - WorkflowOverridePaymentMethod( - // Zurück auf die Original-Methode → Override - // löschen, damit das Domain-Modell "no - // override" kennt. - paymentMethodId: - newId == delivery.paymentMethodId - ? null - : newId, - ), - ); - } - : null, - ), - if (!active) ...[ - const SizedBox(height: 8), - const _PickerHint( - text: 'Lieferung abgeschlossen — Zahlungsmethode nicht ' - 'mehr änderbar.', - ), - ] else if (!hasOpenAmount) ...[ - const SizedBox(height: 8), - const _PickerHint( - text: 'Keine Zahlung mehr offen (bereits bezahlt) — ' - 'Auswahl der Zahlungsweise nicht erforderlich.', - ), - ], - ], - ), - ), - ); - }, - ); - } -} - -/// Dezenter Sperr-/Info-Hinweis unter dem Zahlungsmethoden-Dropdown -/// (Schloss-Icon + Text in gedämpfter Farbe). -class _PickerHint extends StatelessWidget { - const _PickerHint({required this.text}); - - final String text; - - @override - Widget build(BuildContext context) { - final muted = Theme.of(context).colorScheme.onSurfaceVariant; - return Row( - children: [ - Icon(Icons.lock_outline, size: 16, color: muted), - const SizedBox(width: 8), - Expanded( - child: Text( - text, - style: Theme.of(context).textTheme.bodySmall?.copyWith(color: muted), - ), - ), - ], + style: Theme.of( + context, + ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700), ); } } @@ -547,10 +85,7 @@ class _SignHint extends StatelessWidget { Expanded( child: Text( 'Mit „Unterschreiben" unten schließt der Kunde den Vorgang ab.', - style: TextStyle( - fontSize: 13, - color: theme.colorScheme.primary, - ), + style: TextStyle(fontSize: 13, color: theme.colorScheme.primary), ), ), ], diff --git a/lib/feature/delivery/detail/presentation/widget/delivered_items_card.dart b/lib/feature/delivery/detail/presentation/widget/delivered_items_card.dart new file mode 100644 index 0000000..fb3115b --- /dev/null +++ b/lib/feature/delivery/detail/presentation/widget/delivered_items_card.dart @@ -0,0 +1,139 @@ +import 'package:flutter/material.dart'; + +import 'package:hl_lieferservice/domain/entity/delivery.dart'; +import 'package:hl_lieferservice/domain/entity/delivery_item.dart'; +import 'package:hl_lieferservice/domain/entity/tour_details.dart'; +import 'package:hl_lieferservice/feature/delivery/detail/model/delivery_payment_status.dart'; + +/// Ausgelieferte Artikel einer Lieferung mit Menge, Stückpreis und +/// Zeilensumme. Gemeinsam genutzt von Zahlungs-Step und Übersicht. +/// +/// Menge = tatsächlich ausgeliefert (Soll − Gutschrift). Komplett +/// gutgeschriebene Positionen erscheinen durchgestrichen. +class DeliveredItemsCard extends StatelessWidget { + const DeliveredItemsCard({ + super.key, + required this.delivery, + required this.details, + }); + + final Delivery delivery; + final TourDetails details; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + // Innerhalb einer Belegzeile: Oberartikel vor seinen Komponenten, damit + // Komponenten direkt darunter eingerückt erscheinen. + final items = List.of(delivery.items)..sort((a, b) { + final byLine = a.belegzeilenNr.compareTo(b.belegzeilenNr); + if (byLine != 0) return byLine; + final byParent = (a.isComponent ? 1 : 0).compareTo(b.isComponent ? 1 : 0); + if (byParent != 0) return byParent; + return (a.komponentenArtikelNr ?? '').compareTo( + b.komponentenArtikelNr ?? '', + ); + }); + if (items.isEmpty) { + return Card( + margin: EdgeInsets.zero, + child: Padding( + padding: const EdgeInsets.all(16), + child: Text( + 'Keine Artikel hinterlegt.', + style: TextStyle( + color: theme.colorScheme.onSurfaceVariant, + fontStyle: FontStyle.italic, + ), + ), + ), + ); + } + return Card( + margin: EdgeInsets.zero, + child: Column( + children: [ + for (int i = 0; i < items.length; i++) ...[ + _DeliveredRow(item: items[i], details: details), + if (i < items.length - 1) + const Divider(height: 1, indent: 16, endIndent: 16), + ], + ], + ), + ); + } +} + +class _DeliveredRow extends StatelessWidget { + const _DeliveredRow({required this.item, required this.details}); + + final DeliveryItem item; + final TourDetails details; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final article = details.articleOf(item.articleId); + // Ausgeliefert = Soll − Gutschrift (vom Backend). Voll gutgeschrieben + // (status removed) ⇒ credited == required ⇒ delivered 0. + final credited = item.scanProgress.creditedQuantity; + final delivered = (item.requiredQuantity - credited).clamp( + 0, + item.requiredQuantity, + ); + + final Color avatarColor; + if (delivered == 0) { + avatarColor = Colors.red.shade400; + } else if (delivered < item.requiredQuantity) { + avatarColor = Colors.amber.shade700; + } else { + avatarColor = Colors.green.shade600; + } + + return ListTile( + // Komponenten um eine Stufe eingerückt (gehören zum Oberartikel darüber). + contentPadding: EdgeInsets.only( + left: item.isComponent ? 40 : 16, + right: 16, + ), + leading: CircleAvatar( + backgroundColor: avatarColor, + foregroundColor: theme.colorScheme.onPrimary, + child: Text( + '$delivered×', + style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold), + ), + ), + title: Text( + '${item.isComponent ? '↳ ' : ''}${article?.name ?? '⟨Unbekannter Artikel⟩'}', + style: TextStyle( + fontWeight: FontWeight.w600, + decoration: delivered == 0 ? TextDecoration.lineThrough : null, + color: delivered == 0 ? theme.colorScheme.onSurfaceVariant : null, + ), + ), + subtitle: Text( + [ + if (delivered < item.requiredQuantity) + 'von ${item.requiredQuantity} bestellt · Gutschrift: $credited' + else + 'Artikelnr. ${article?.articleNumber ?? item.articleId}', + '${formatEuro(item.unitPrice)} / Stück', + ].join(' · '), + style: TextStyle( + fontSize: 12, + color: theme.colorScheme.onSurfaceVariant, + ), + ), + trailing: Text( + formatEuro(item.lineTotal), + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w700, + decoration: delivered == 0 ? TextDecoration.lineThrough : null, + color: delivered == 0 ? theme.colorScheme.onSurfaceVariant : null, + ), + ), + ); + } +} diff --git a/lib/feature/delivery/detail/presentation/widget/payment_breakdown_card.dart b/lib/feature/delivery/detail/presentation/widget/payment_breakdown_card.dart new file mode 100644 index 0000000..e0abb84 --- /dev/null +++ b/lib/feature/delivery/detail/presentation/widget/payment_breakdown_card.dart @@ -0,0 +1,134 @@ +import 'package:flutter/material.dart'; + +import 'package:hl_lieferservice/domain/entity/delivery_credit.dart'; +import 'package:hl_lieferservice/feature/delivery/detail/model/delivery_payment_status.dart'; + +/// Betragsaufstellung einer Lieferung: Warenwert − Anzahlung − Gutschrift = +/// offener Betrag. Gemeinsam genutzt von Zahlungs-Step und Übersicht; die +/// Zahlen kommen aus [DeliveryPaymentStatus] (gleiche Formel wie Backend). +class PaymentBreakdownCard extends StatelessWidget { + const PaymentBreakdownCard({ + super.key, + required this.status, + required this.credit, + }); + + final DeliveryPaymentStatus status; + final DeliveryCredit? credit; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Card( + margin: EdgeInsets.zero, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + _AmountRow( + icon: Icons.receipt_long_outlined, + label: 'Warenwert', + valueText: formatEuro(status.goodsValue), + valueColor: theme.colorScheme.onSurface, + ), + const SizedBox(height: 12), + _AmountRow( + icon: Icons.savings_outlined, + label: 'Bei Bestellung bezahlt', + valueText: '− ${formatEuro(status.prepaidAmount)}', + valueColor: + status.prepaidAmount > 0 + ? Colors.green.shade700 + : theme.colorScheme.onSurfaceVariant, + ), + if (credit != null) ...[ + const SizedBox(height: 12), + _AmountRow( + icon: Icons.card_giftcard_outlined, + label: 'Gutschrift', + valueText: '− ${formatEuroCents(credit!.amountCents)}', + valueColor: Colors.amber.shade800, + subtitle: credit!.reason, + ), + ], + const Divider(height: 24), + _AmountRow( + icon: Icons.account_balance_wallet_outlined, + label: 'Offener Betrag', + valueText: formatEuroCents(status.openCents), + valueColor: + status.hasOpenAmount + ? theme.colorScheme.primary + : Colors.green.shade700, + emphasize: true, + ), + ], + ), + ), + ); + } +} + +class _AmountRow extends StatelessWidget { + const _AmountRow({ + required this.icon, + required this.label, + required this.valueText, + required this.valueColor, + this.subtitle, + this.emphasize = false, + }); + + final IconData icon; + final String label; + final String valueText; + final Color valueColor; + final String? subtitle; + + /// Hebt Label + Wert hervor (für den „Offener Betrag"-Abschluss). + final bool emphasize; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(top: 2), + child: Icon(icon, color: theme.colorScheme.primary), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: + emphasize + ? const TextStyle(fontWeight: FontWeight.w700) + : null, + ), + if (subtitle != null) + Text( + subtitle!, + style: TextStyle( + fontSize: 12, + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + Text( + valueText, + style: (emphasize + ? theme.textTheme.titleLarge + : theme.textTheme.titleMedium) + ?.copyWith(fontWeight: FontWeight.w700, color: valueColor), + ), + ], + ); + } +} diff --git a/lib/feature/delivery/detail/presentation/widget/payment_method_icon.dart b/lib/feature/delivery/detail/presentation/widget/payment_method_icon.dart new file mode 100644 index 0000000..74a2a3d --- /dev/null +++ b/lib/feature/delivery/detail/presentation/widget/payment_method_icon.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +/// Icon je Zahlungsmethode anhand des stabilen Backend-Codes. Unbekannte +/// Codes (neue Methoden aus den Stammdaten) bekommen ein neutrales Icon. +IconData paymentMethodIcon(String code) => switch (code) { + 'cash' => Icons.payments_outlined, + 'ec_card' => Icons.credit_card, + 'invoice' => Icons.receipt_long_outlined, + _ => Icons.account_balance_wallet_outlined, +}; diff --git a/lib/feature/delivery/detail/presentation/widget/payment_record_card.dart b/lib/feature/delivery/detail/presentation/widget/payment_record_card.dart new file mode 100644 index 0000000..ef33b3f --- /dev/null +++ b/lib/feature/delivery/detail/presentation/widget/payment_record_card.dart @@ -0,0 +1,138 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:intl/intl.dart'; + +import 'package:hl_lieferservice/domain/entity/delivery_payment.dart'; +import 'package:hl_lieferservice/feature/delivery/detail/model/delivery_payment_status.dart'; +import 'package:hl_lieferservice/feature/delivery/detail/presentation/widget/payment_method_icon.dart'; +import 'package:hl_lieferservice/feature/payment_methods/bloc/payment_methods_cubit.dart'; + +/// Status-Karte der Zahlungsabwicklung: erledigt, veraltet, noch offen oder +/// nicht nötig. Gemeinsam genutzt von Zahlungs-Step und Übersicht. +class PaymentRecordCard extends StatelessWidget { + const PaymentRecordCard({super.key, required this.status}); + + final DeliveryPaymentStatus status; + + @override + Widget build(BuildContext context) { + final valid = status.validPayment; + if (!status.hasOpenAmount) { + return const _StatusCard( + tone: _Tone.success, + icon: Icons.verified_outlined, + title: 'Bereits bezahlt', + text: + 'Es ist kein Betrag mehr offen. Dieser Schritt kann ' + 'übersprungen werden.', + ); + } + if (valid != null) { + return _StatusCard( + tone: _Tone.success, + icon: paymentMethodIcon(valid.paymentMethodCode), + title: + valid.isCollectedOnSite + ? 'Zahlung erhalten' + : 'Zahlung auf Rechnung bestätigt', + text: + '${formatEuroCents(valid.amountCents)} per ' + '${_methodName(context, valid)} · protokolliert um ' + '${DateFormat('HH:mm').format(valid.recordedAt)} Uhr', + ); + } + if (status.isStale) { + return const _StatusCard( + tone: _Tone.warning, + icon: Icons.sync_problem_outlined, + title: 'Betrag hat sich geändert', + text: + 'Seit der letzten Zahlungsabwicklung hat sich der offene Betrag ' + 'geändert (z. B. durch eine Gutschrift). Bitte die Zahlung erneut ' + 'abwickeln.', + ); + } + return const _StatusCard( + tone: _Tone.open, + icon: Icons.pending_outlined, + title: 'Zahlung noch nicht abgewickelt', + text: + 'Bitte die Zahlung abwickeln, bevor es mit der Übersicht ' + 'weitergeht.', + ); + } + + /// Anzeigename aus den Stammdaten; Fallback auf den Code-Snapshot, falls + /// die Methode inzwischen deaktiviert/umbenannt wurde. + String _methodName(BuildContext context, DeliveryPayment payment) { + final state = context.watch().state; + if (state is PaymentMethodsLoaded) { + for (final m in state.methods) { + if (m.id == payment.paymentMethodId) return m.name; + } + } + return switch (payment.paymentMethodCode) { + 'cash' => 'Bar', + 'ec_card' => 'EC-Karte', + 'invoice' => 'Rechnung', + final code => code, + }; + } +} + +enum _Tone { success, warning, open } + +class _StatusCard extends StatelessWidget { + const _StatusCard({ + required this.tone, + required this.icon, + required this.title, + required this.text, + }); + + final _Tone tone; + final IconData icon; + final String title; + final String text; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final Color color = switch (tone) { + _Tone.success => Colors.green.shade700, + _Tone.warning => Colors.orange.shade800, + _Tone.open => theme.colorScheme.primary, + }; + return Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.08), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: color.withValues(alpha: 0.4)), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(icon, color: color), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w700, + color: color, + ), + ), + const SizedBox(height: 2), + Text(text, style: theme.textTheme.bodySmall), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/feature/delivery/detail/presentation/widget/payment_sheet.dart b/lib/feature/delivery/detail/presentation/widget/payment_sheet.dart new file mode 100644 index 0000000..a7eb03c --- /dev/null +++ b/lib/feature/delivery/detail/presentation/widget/payment_sheet.dart @@ -0,0 +1,407 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:hl_lieferservice/domain/entity/payment_method.dart'; +import 'package:hl_lieferservice/feature/delivery/detail/model/delivery_payment_status.dart'; +import 'package:hl_lieferservice/feature/delivery/detail/presentation/widget/payment_method_icon.dart'; +import 'package:hl_lieferservice/feature/delivery/detail/presentation/widget/reason_dialog.dart'; +import 'package:hl_lieferservice/feature/payment_methods/bloc/payment_methods_cubit.dart'; + +/// Ergebnis des Zahlungs-Modals. `null` = abgebrochen, nichts passiert. +sealed class PaymentSheetResult { + const PaymentSheetResult(); +} + +/// Der Fahrer hat bestätigt, dass die Zahlung per [method] erhalten wurde +/// (bzw. bei „Rechnung": dass auf Rechnung abgewickelt wird). +class PaymentConfirmed extends PaymentSheetResult { + const PaymentConfirmed(this.method); + final PaymentMethod method; +} + +/// Zahlung ist nicht möglich — die ganze Lieferung soll abgebrochen werden. +class DeliveryCancelRequested extends PaymentSheetResult { + const DeliveryCancelRequested(this.reason); + final String reason; +} + +/// Öffnet das Zahlungs-Modal. +/// +/// Zeigt groß den offenen Betrag, die Zahlungsmethoden als Cards (dynamisch +/// aus den Backend-Stammdaten) und bei EC-Karte groß die Kundennummer, die +/// der Fahrer am EC-Gerät eingibt. Schließt sich nur über „Abbrechen", +/// „Zahlung erhalten" oder „Lieferung abbrechen". +Future showPaymentSheet( + BuildContext context, { + required int openCents, + required int customerNumber, + required String? preselectedMethodId, +}) { + return showModalBottomSheet( + context: context, + isScrollControlled: true, + useSafeArea: true, + // Kein versehentliches Wegwischen: Das Modal endet nur über die Buttons. + isDismissible: false, + enableDrag: false, + showDragHandle: false, + builder: + (_) => _PaymentSheet( + openCents: openCents, + customerNumber: customerNumber, + preselectedMethodId: preselectedMethodId, + ), + ); +} + +class _PaymentSheet extends StatefulWidget { + const _PaymentSheet({ + required this.openCents, + required this.customerNumber, + required this.preselectedMethodId, + }); + + final int openCents; + final int customerNumber; + final String? preselectedMethodId; + + @override + State<_PaymentSheet> createState() => _PaymentSheetState(); +} + +class _PaymentSheetState extends State<_PaymentSheet> { + String? _selectedId; + + @override + void initState() { + super.initState(); + // Vorauswahl = am Beleg hinterlegte Methode. Der Fahrer kann wechseln. + _selectedId = widget.preselectedMethodId; + } + + Future _cancelDelivery() async { + final navigator = Navigator.of(context); + final reason = await showReasonDialog(context, 'Lieferung abbrechen'); + if (reason == null || !mounted) return; + navigator.pop(DeliveryCancelRequested(reason)); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return BlocBuilder( + builder: (context, pmState) { + final methods = + pmState is PaymentMethodsLoaded + ? pmState.methods.where((m) => m.active).toList() + : const []; + PaymentMethod? selected; + for (final m in methods) { + if (m.id == _selectedId) selected = m; + } + final isInvoice = selected?.code == 'invoice'; + + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Flexible( + child: ListView( + shrinkWrap: true, + padding: const EdgeInsets.fromLTRB(20, 20, 20, 8), + children: [ + Text( + 'Zahlung abwickeln', + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 16), + _AmountBanner(openCents: widget.openCents), + const SizedBox(height: 20), + Text( + 'Zahlungsart', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 8), + ..._methodSection(context, pmState, methods), + if (selected != null) ...[ + const SizedBox(height: 8), + _MethodHint( + method: selected, + customerNumber: widget.customerNumber, + ), + ], + ], + ), + ), + SafeArea( + top: false, + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 8, 20, 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + FilledButton.icon( + style: FilledButton.styleFrom( + minimumSize: const Size.fromHeight(60), + textStyle: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + ), + ), + onPressed: + selected == null + ? null + : () => Navigator.of( + context, + ).pop(PaymentConfirmed(selected!)), + icon: const Icon(Icons.check_circle_outline), + label: Text( + isInvoice + ? 'Auf Rechnung bestätigen' + : 'Zahlung erhalten', + ), + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + flex: 3, + child: OutlinedButton.icon( + style: OutlinedButton.styleFrom( + minimumSize: const Size.fromHeight(48), + padding: const EdgeInsets.symmetric( + horizontal: 12, + ), + foregroundColor: theme.colorScheme.error, + side: BorderSide(color: theme.colorScheme.error), + ), + onPressed: _cancelDelivery, + icon: const Icon(Icons.block), + label: const Text('Lieferung abbrechen'), + ), + ), + const SizedBox(width: 12), + Expanded( + flex: 2, + child: TextButton( + style: TextButton.styleFrom( + minimumSize: const Size.fromHeight(48), + ), + onPressed: () => Navigator.of(context).pop(), + child: const Text('Abbrechen'), + ), + ), + ], + ), + ], + ), + ), + ), + ], + ); + }, + ); + } + + List _methodSection( + BuildContext context, + PaymentMethodsState state, + List methods, + ) { + if (state is PaymentMethodsFailed) { + return [ + Card( + margin: EdgeInsets.zero, + color: Theme.of(context).colorScheme.errorContainer, + child: ListTile( + title: Text(state.message), + trailing: TextButton.icon( + onPressed: () => context.read().load(), + icon: const Icon(Icons.refresh), + label: const Text('Erneut laden'), + ), + ), + ), + ]; + } + if (state is! PaymentMethodsLoaded) { + return const [ + Padding( + padding: EdgeInsets.all(16), + child: Center(child: CircularProgressIndicator()), + ), + ]; + } + if (methods.isEmpty) { + return const [Text('Keine Zahlungsmethoden hinterlegt.')]; + } + return [ + for (final m in methods) + _MethodCard( + method: m, + selected: m.id == _selectedId, + onTap: () => setState(() => _selectedId = m.id), + ), + ]; + } +} + +/// Der zu zahlende Betrag, bewusst sehr groß. +class _AmountBanner extends StatelessWidget { + const _AmountBanner({required this.openCents}); + + final int openCents; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final primary = theme.colorScheme.primary; + return Container( + padding: const EdgeInsets.symmetric(vertical: 18, horizontal: 16), + decoration: BoxDecoration( + color: primary.withValues(alpha: 0.08), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: primary.withValues(alpha: 0.35)), + ), + child: Column( + children: [ + Text('Noch zu zahlen', style: theme.textTheme.titleSmall), + const SizedBox(height: 4), + FittedBox( + fit: BoxFit.scaleDown, + child: Text( + formatEuroCents(openCents), + style: theme.textTheme.displayMedium?.copyWith( + fontWeight: FontWeight.w800, + color: primary, + ), + ), + ), + ], + ), + ); + } +} + +class _MethodCard extends StatelessWidget { + const _MethodCard({ + required this.method, + required this.selected, + required this.onTap, + }); + + final PaymentMethod method; + final bool selected; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final scheme = Theme.of(context).colorScheme; + return Card( + margin: const EdgeInsets.only(bottom: 10), + clipBehavior: Clip.antiAlias, + elevation: selected ? 2 : 0, + color: selected ? scheme.primaryContainer : scheme.surface, + surfaceTintColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), + side: BorderSide( + color: selected ? scheme.primary : scheme.outlineVariant, + width: selected ? 2 : 1, + ), + ), + child: InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 18), + child: Row( + children: [ + Icon( + paymentMethodIcon(method.code), + size: 30, + color: selected ? scheme.primary : scheme.onSurfaceVariant, + ), + const SizedBox(width: 16), + Expanded( + child: Text( + method.name, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + ), + Icon( + selected ? Icons.radio_button_checked : Icons.radio_button_off, + color: selected ? scheme.primary : scheme.outline, + ), + ], + ), + ), + ), + ); + } +} + +/// Hinweis zur gewählten Methode. Bei EC-Karte groß die Kundennummer, die +/// der Fahrer am EC-Gerät eingeben muss. +class _MethodHint extends StatelessWidget { + const _MethodHint({required this.method, required this.customerNumber}); + + final PaymentMethod method; + final int customerNumber; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + if (method.code == 'ec_card') { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: theme.colorScheme.secondaryContainer, + borderRadius: BorderRadius.circular(16), + ), + child: Column( + children: [ + Text( + 'Kundennummer am EC-Gerät eingeben', + style: theme.textTheme.titleSmall?.copyWith( + color: theme.colorScheme.onSecondaryContainer, + ), + ), + const SizedBox(height: 6), + FittedBox( + fit: BoxFit.scaleDown, + child: SelectableText( + '$customerNumber', + style: theme.textTheme.displayMedium?.copyWith( + fontWeight: FontWeight.w800, + letterSpacing: 4, + fontFeatures: const [FontFeature.tabularFigures()], + color: theme.colorScheme.onSecondaryContainer, + ), + ), + ), + ], + ), + ); + } + final text = switch (method.code) { + 'cash' => 'Betrag bar entgegennehmen und danach bestätigen.', + 'invoice' => + 'Der Betrag wird per Rechnung beglichen. Es wird vor Ort ' + 'nichts kassiert.', + _ => 'Zahlung abwickeln und danach bestätigen.', + }; + return Text( + text, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ); + } +} diff --git a/lib/feature/delivery/detail/presentation/widget/reason_dialog.dart b/lib/feature/delivery/detail/presentation/widget/reason_dialog.dart new file mode 100644 index 0000000..e92b496 --- /dev/null +++ b/lib/feature/delivery/detail/presentation/widget/reason_dialog.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +/// Fragt eine Pflicht-Begründung ab (z. B. für Pausieren/Abbrechen einer +/// Lieferung). Liefert den getrimmten Text oder `null` bei Abbruch. +Future showReasonDialog(BuildContext context, String title) async { + // Controller bewusst nicht disposen: Der Dialog nutzt ihn noch während + // der Schließ-Animation. Ohne Listener räumt der GC ihn ab. + final controller = TextEditingController(); + return showDialog( + context: context, + builder: + (dialogContext) => AlertDialog( + title: Text(title), + content: TextField( + controller: controller, + autofocus: true, + decoration: const InputDecoration( + labelText: 'Grund', + border: OutlineInputBorder(), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(dialogContext).pop(null), + child: const Text('Abbrechen'), + ), + FilledButton( + onPressed: () { + final text = controller.text.trim(); + if (text.isEmpty) return; + Navigator.of(dialogContext).pop(text); + }, + child: const Text('Bestätigen'), + ), + ], + ), + ); +} diff --git a/openapi/holzleitner.json b/openapi/holzleitner.json index e7562ef..b53cfc7 100644 --- a/openapi/holzleitner.json +++ b/openapi/holzleitner.json @@ -57,6 +57,154 @@ ] } }, + "/admin/belege/{belegnummer}": { + "get": { + "tags": [ + "admin" + ], + "summary": "Liefert das **volle Detail-Paket** zu einer ERP-Belegnummer: die Lieferung\nmit Positionen (inkl. Scan-St\u00e4nden), Kunde + Ansprechpartner, referenzierte\nArtikel/Lager, Notizen, Geld-Gutschrift, Dienstleistungswerte und Kontakte \u2014\ndestilliert aus dem Tour-Aggregat. Jede Lieferung, unabh\u00e4ngig vom Status.\n`404`, wenn die Belegnummer unbekannt ist.", + "operationId": "belege_details", + "parameters": [ + { + "name": "belegnummer", + "in": "path", + "description": "ERP-Belegnummer, z. B. V-30690291", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Volle Lieferdetails", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeliveryDetails" + } + } + } + }, + "401": { + "description": "Admin-API-Key fehlt/ung\u00fcltig" + }, + "404": { + "description": "Belegnummer unbekannt" + } + }, + "security": [ + { + "admin_api_key": [] + } + ] + } + }, + "/admin/belege/{belegnummer}/positions-modified": { + "get": { + "tags": [ + "admin" + ], + "summary": "Liefert zu **einer** ERP-Belegnummer, ob an der Lieferung Positionen\nver\u00e4ndert wurden (Menge reduziert/Zeile entfernt oder Geld-Gutschrift) \u2014\nunabh\u00e4ngig vom Zustand der Lieferung. `404`, wenn die Belegnummer unbekannt\nist. Gibt es mehrere Lieferungen mit derselben Belegnummer, ist das Flag\n`true`, sobald **eine** davon ver\u00e4ndert ist.", + "operationId": "positions_modified", + "parameters": [ + { + "name": "belegnummer", + "in": "path", + "description": "ERP-Belegnummer, z. B. V-30690291", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "\u00c4nderungs-Flag der Belegpositionen", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PositionsModifiedResponse" + } + } + } + }, + "401": { + "description": "Admin-API-Key fehlt/ung\u00fcltig" + }, + "404": { + "description": "Belegnummer unbekannt" + } + }, + "security": [ + { + "admin_api_key": [] + } + ] + } + }, + "/admin/completed-deliveries": { + "get": { + "tags": [ + "admin" + ], + "summary": "Liefert **alle** abgeschlossenen (ausgelieferten) Lieferungen \u2014 optional auf\neinen Datumsbereich eingegrenzt, unabh\u00e4ngig vom Mail-Versand-Status.\nGefiltert wird \u00fcber den **Berliner** Kalendertag des Abschluss-Zeitpunkts\n(`completed_at`).", + "description": "Parameter (alle `DD-MM-YYYY`, alle optional): `day` = Einzeltag (Kurzform\n`from=to=day`), sonst `from`/`to` als **inklusive** Bereichsgrenzen (je\noffen). **Ohne jeden Parameter \u2192 ALLE ausgelieferten Belege** (kein\nDatumsfilter). Pro Lieferung: Belegnummer + `positions_modified` (Menge\nreduziert/Zeile entfernt oder Geld-Gutschrift). Die Halb-Grenzen-Variante ist\nf\u00fcr Range-Filter gedacht: `?from=\u2026` und `?to=\u2026` liefern je eine Menge, deren\nSQL-`AND`-Schnitt den Zeitraum ergibt.", + "operationId": "completed_deliveries", + "parameters": [ + { + "name": "day", + "in": "query", + "description": "Einzeltag DD-MM-YYYY (Kurzform from=to)", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "from", + "in": "query", + "description": "Untere Grenze DD-MM-YYYY (inklusive)", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "to", + "in": "query", + "description": "Obere Grenze DD-MM-YYYY (inklusive)", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Ausgelieferte Lieferungen (optional bereichsgefiltert)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CompletedDeliveriesResponse" + } + } + } + }, + "400": { + "description": "Ung\u00fcltiges Datum" + }, + "401": { + "description": "Admin-API-Key fehlt/ung\u00fcltig" + } + }, + "security": [ + { + "admin_api_key": [] + } + ] + } + }, "/admin/delivered-belegnummern": { "get": { "tags": [ @@ -227,6 +375,87 @@ ] } }, + "/admin/reviews": { + "get": { + "tags": [ + "admin" + ], + "summary": "Listet alle ge\u00e4nderten Lieferscheine, die noch auf eine manuelle\nBest\u00e4tigung (Vier-Augen) warten \u2014 Entfernungen und/oder Geld-Gutschriften.", + "operationId": "list_reviews", + "responses": { + "200": { + "description": "Offene Pr\u00fcfungen", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PendingReviewResponse" + } + } + } + } + }, + "401": { + "description": "Admin-API-Key fehlt/ung\u00fcltig" + } + }, + "security": [ + { + "admin_api_key": [] + } + ] + } + }, + "/admin/reviews/{delivery_id}/resolve": { + "post": { + "tags": [ + "admin" + ], + "summary": "Best\u00e4tigt die Pr\u00fcfung einer ge\u00e4nderten Lieferung (Vier-Augen) \u2014 die\nLieferung verschwindet danach aus `GET /admin/reviews` (sofern nicht\nerneut ge\u00e4ndert).", + "operationId": "resolve_review", + "parameters": [ + { + "name": "delivery_id", + "in": "path", + "description": "UUID der Lieferung", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResolveReviewRequest" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Pr\u00fcfung best\u00e4tigt" + }, + "400": { + "description": "Ung\u00fcltige delivery_id / Bearbeiter leer" + }, + "401": { + "description": "Admin-API-Key fehlt/ung\u00fcltig" + }, + "404": { + "description": "Lieferung nicht gefunden" + } + }, + "security": [ + { + "admin_api_key": [] + } + ] + } + }, "/attachments/{id}": { "get": { "tags": [ @@ -814,6 +1043,62 @@ ] } }, + "/deliveries/{delivery_id}/payment": { + "post": { + "tags": [ + "deliveries" + ], + "summary": "Protokolliert die Zahlungsabwicklung (\u201eAbkassieren\") einer Lieferung.\nAppend-only, idempotent \u00fcber `clientEventId`. Der Server berechnet den\noffenen Betrag selbst; er muss > 0 sein und `expectedAmountCents`\nentsprechen. Nur bei aktiver Lieferung und aktiver Zahlungsmethode.", + "operationId": "record_payment", + "parameters": [ + { + "name": "delivery_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RecordDeliveryPaymentRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Zahlung protokolliert", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeliveryPaymentResponse" + } + } + } + }, + "400": { + "description": "Lieferung nicht aktiv, Methode ung\u00fcltig, kein offener Betrag oder Betrag abweichend" + }, + "401": { + "description": "Authentifizierung fehlgeschlagen" + }, + "404": { + "description": "Lieferung nicht gefunden" + } + }, + "security": [ + { + "bearer_auth": [] + } + ] + } + }, "/deliveries/{delivery_id}/resume": { "post": { "tags": [ @@ -1129,16 +1414,27 @@ ] } }, - "/me/tours/today": { + "/me/tours": { "get": { "tags": [ "tours" ], - "summary": "Listet heutige Touren des angemeldeten Fahrers (Filter aus dem JWT).", - "operationId": "list_my_tours_today", + "summary": "Listet die Touren des angemeldeten Fahrers (Filter aus dem JWT) f\u00fcr ein\nZiel-Datum. Ohne `?date=` gilt \u201eheute\". Der Fahrer w\u00e4hlt das Datum aktiv\nim Kopf-Kalender der App (z. B. um die Tour von morgen vorab zu sehen).", + "operationId": "list_my_tours", + "parameters": [ + { + "name": "date", + "in": "query", + "description": "Ziel-Tourdatum YYYY-MM-DD (Default: heute)", + "required": false, + "schema": { + "type": "string" + } + } + ], "responses": { "200": { - "description": "Liste der heutigen Touren", + "description": "Liste der Touren am Ziel-Datum", "content": { "application/json": { "schema": { @@ -1147,6 +1443,9 @@ } } }, + "400": { + "description": "Ung\u00fcltiges Datum" + }, "401": { "description": "Authentifizierung fehlgeschlagen" } @@ -1932,6 +2231,59 @@ } } }, + "CompletedDeliveriesResponse": { + "type": "object", + "required": [ + "count", + "deliveries" + ], + "properties": { + "count": { + "type": "integer", + "description": "Anzahl der abgeschlossenen Lieferungen im Bereich.", + "minimum": 0 + }, + "deliveries": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CompletedDeliveryItem" + }, + "description": "Die abgeschlossenen Lieferungen, aufsteigend nach Abschluss-Zeitpunkt." + }, + "from": { + "type": [ + "string", + "null" + ], + "description": "Wirksame untere Grenze (ISO `YYYY-MM-DD`) oder `null` (offen)." + }, + "to": { + "type": [ + "string", + "null" + ], + "description": "Wirksame obere Grenze (ISO `YYYY-MM-DD`) oder `null` (offen)." + } + } + }, + "CompletedDeliveryItem": { + "type": "object", + "description": "Eine abgeschlossene Lieferung im Tagesabruf.", + "required": [ + "belegnummer", + "positions_modified" + ], + "properties": { + "belegnummer": { + "type": "string", + "description": "ERP-Belegnummer der Lieferung." + }, + "positions_modified": { + "type": "boolean", + "description": "`true`, wenn an der Lieferung Positionen ver\u00e4ndert wurden \u2014 eine Zeile\nwurde entfernt oder in der Menge reduziert (St\u00fcck-Gutschrift) **oder** es\nliegt eine aktive Geld-Gutschrift vor." + } + } + }, "ContactChannel": { "type": "object", "description": "Ein einzelner Kontaktkanal (Telefonnummer / Mobil / E-Mail / Web).\nMehrere pro [`ContactSource`] m\u00f6glich, die `position` h\u00e4lt die\n1-basierte ERP-Reihenfolge (`Telefon` \u2192 1, `Telefon2` \u2192 2 usw.) fest,\ndamit der \u201eprim\u00e4re\" Kanal je Art stabil identifizierbar bleibt.", @@ -2421,6 +2773,120 @@ } } }, + "DeliveryDetails": { + "type": "object", + "required": [ + "tour", + "delivery", + "customerContacts", + "articles", + "warehouses", + "notes", + "services", + "deliveryServices", + "contactSources", + "contactChannels" + ], + "properties": { + "articles": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Article" + }, + "description": "Nur die von den Positionen referenzierten Artikel." + }, + "contactChannels": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ContactChannel" + }, + "description": "Die zu `contactSources` geh\u00f6renden Einzel-Kan\u00e4le. Join per `sourceId`." + }, + "contactSources": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ContactSource" + }, + "description": "Kontaktquellen der Lieferung (Liefer-/Rechnungsadresse, Ansprechpartner \u2026)." + }, + "credit": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/DeliveryCredit", + "description": "Aktuelle Betrags-Gutschrift (`null`, wenn keine aktiv)." + } + ] + }, + "customer": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/Customer", + "description": "Der Kunde der Lieferung (`null`, falls unauffindbar \u2014 sollte nicht\nvorkommen)." + } + ] + }, + "customerContacts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CustomerContact" + }, + "description": "Ansprechpartner des Kunden." + }, + "delivery": { + "$ref": "#/components/schemas/DeliveryWithItems", + "description": "Die Lieferung selbst inkl. Positionen (mit Scan-St\u00e4nden) und `sortOrder`." + }, + "deliveryServices": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DeliveryServiceValue" + }, + "description": "F\u00fcr diese Lieferung gesetzte Service-Werte." + }, + "notes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DeliveryNote" + }, + "description": "Notizen der Lieferung, aufsteigend nach `createdAt`." + }, + "payment": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/DeliveryPayment", + "description": "J\u00fcngste protokollierte Zahlungsabwicklung (`None` = keine)." + } + ] + }, + "services": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Service" + }, + "description": "Aktive Service-Definitionen (Stammdaten)." + }, + "tour": { + "$ref": "#/components/schemas/Tour", + "description": "Die Tour, zu der die Lieferung geh\u00f6rt." + }, + "warehouses": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Warehouse" + }, + "description": "Nur die von den Positionen referenzierten Lager." + } + } + }, "DeliveryItem": { "type": "object", "description": "Einzelposition einer Lieferung. Vereint regul\u00e4re Belegzeilen und\nSt\u00fccklisten-Komponenten zu einer flachen Liste \u2014 die St\u00fccklisten-\nHierarchie ist ein ERP-Konstrukt und wird beim Sync aufgel\u00f6st.\n\n\u00dcber die Felder `belegzeilen_nr` und `komponenten_artikel_nr` bleibt\ndie ERP-Herkunft aufl\u00f6sbar.", @@ -2579,6 +3045,68 @@ } } }, + "DeliveryPayment": { + "type": "object", + "description": "Protokollierte Zahlungsabwicklung (\u201eAbkassieren\") einer Lieferung.\n\nAppend-only: jede Best\u00e4tigung des Fahrers ist ein eigener Eintrag; im\nTour-Aggregat wird pro Lieferung nur der j\u00fcngste mitgeliefert. Der Betrag\nist der server-seitig berechnete offene Betrag zum Zeitpunkt der\nBest\u00e4tigung. Weicht der aktuelle offene Betrag davon ab (z. B. nach einer\nneuen Gutschrift), ist der Eintrag veraltet und muss erneuert werden.", + "required": [ + "id", + "deliveryId", + "paymentMethodId", + "paymentMethodCode", + "amountCents", + "recordedByPersonalnummer", + "recordedAt" + ], + "properties": { + "amountCents": { + "type": "integer", + "format": "int64", + "description": "Abgewickelter Betrag in Cent (offener Betrag bei Best\u00e4tigung)." + }, + "deliveryId": { + "type": "string", + "format": "uuid" + }, + "id": { + "type": "string", + "format": "uuid" + }, + "paymentMethodCode": { + "type": "string", + "description": "Snapshot des Methoden-Codes (`cash`, `ec_card`, `invoice`, \u2026)." + }, + "paymentMethodId": { + "type": "string", + "format": "uuid" + }, + "recordedAt": { + "type": "string", + "format": "date-time" + }, + "recordedByCarId": { + "type": [ + "string", + "null" + ], + "format": "uuid" + }, + "recordedByPersonalnummer": { + "type": "integer", + "format": "int64" + } + } + }, + "DeliveryPaymentResponse": { + "type": "object", + "required": [ + "payment" + ], + "properties": { + "payment": { + "$ref": "#/components/schemas/DeliveryPayment" + } + } + }, "DeliveryResponse": { "type": "object", "required": [ @@ -2814,6 +3342,165 @@ } } }, + "PendingReviewResponse": { + "type": "object", + "required": [ + "delivery_id", + "erp_belegart_id", + "erp_belegnummer", + "customer_name", + "tour_date", + "last_change_at", + "credited_items", + "money_credit_cents" + ], + "properties": { + "credited_items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ReviewedItemResponse" + }, + "description": "Entfernte/teil-gutgeschriebene Positionen." + }, + "customer_name": { + "type": "string" + }, + "delivery_id": { + "type": "string" + }, + "erp_belegart_id": { + "type": "integer", + "format": "int64" + }, + "erp_belegnummer": { + "type": "string" + }, + "last_change_at": { + "type": "string", + "description": "Zeitpunkt der letzten beleg-\u00e4ndernden Aktion (RFC 3339)." + }, + "money_credit_cents": { + "type": "integer", + "format": "int64", + "description": "Geld-Gutschrift in Cent (0 = keine)." + }, + "money_credit_reason": { + "type": [ + "string", + "null" + ] + }, + "tour_date": { + "type": "string", + "description": "Tourdatum (ISO `YYYY-MM-DD`)." + } + } + }, + "PositionsModifiedResponse": { + "type": "object", + "required": [ + "belegnummer", + "positions_modified" + ], + "properties": { + "belegnummer": { + "type": "string", + "description": "ERP-Belegnummer, nach der gefragt wurde." + }, + "positions_modified": { + "type": "boolean", + "description": "`true`, wenn an der Lieferung Positionen ver\u00e4ndert wurden \u2014 eine Zeile\nwurde entfernt oder in der Menge reduziert (St\u00fcck-Gutschrift) **oder** es\nliegt eine aktive Geld-Gutschrift vor." + } + } + }, + "RecordDeliveryPaymentRequest": { + "type": "object", + "required": [ + "clientEventId", + "paymentMethodId", + "expectedAmountCents" + ], + "properties": { + "authorCarId": { + "type": [ + "string", + "null" + ], + "format": "uuid", + "description": "Fahrzeug des Akteurs (Audit-Spur). Muss zum Account geh\u00f6ren." + }, + "clientEventId": { + "type": "string", + "format": "uuid", + "description": "Idempotenz-Schl\u00fcssel \u2014 pro Best\u00e4tigung genau einmal vergeben." + }, + "expectedAmountCents": { + "type": "integer", + "format": "int64", + "description": "Betrag in Cent, den der Fahrer in der App gesehen und best\u00e4tigt hat.\nDer Server berechnet den offenen Betrag selbst und lehnt ab, wenn er\ndavon abweicht \u2014 so landet nie ein Betrag im Protokoll, den der\nFahrer nicht gesehen hat." + }, + "paymentMethodId": { + "type": "string", + "format": "uuid", + "description": "Gew\u00e4hlte Zahlungsmethode. Muss existieren und aktiv sein." + } + } + }, + "ResolveReviewRequest": { + "type": "object", + "required": [ + "resolved_by" + ], + "properties": { + "note": { + "type": [ + "string", + "null" + ], + "description": "Optionale Notiz zur getroffenen Entscheidung." + }, + "resolved_by": { + "type": "string", + "description": "Bearbeiter (Name/K\u00fcrzel), der die Pr\u00fcfung best\u00e4tigt." + } + } + }, + "ReviewedItemResponse": { + "type": "object", + "required": [ + "belegzeilen_nr", + "artikel_nr", + "article_name", + "required_quantity", + "credited_quantity" + ], + "properties": { + "article_name": { + "type": "string" + }, + "artikel_nr": { + "type": "string" + }, + "belegzeilen_nr": { + "type": "integer", + "format": "int32" + }, + "credited_quantity": { + "type": "integer", + "format": "int32" + }, + "reason": { + "type": [ + "string", + "null" + ] + }, + "required_quantity": { + "type": "integer", + "format": "int32" + } + } + }, "ScanEvent": { "type": "object", "required": [ @@ -3416,6 +4103,7 @@ "warehouses", "notes", "credits", + "payments", "services", "deliveryServices", "contactSources", @@ -3481,6 +4169,13 @@ }, "description": "Alle Notizen aller Lieferungen dieser Tour, in einer Liste.\nDie App joint clientseitig per `delivery_id`. Reihenfolge:\npro Lieferung aufsteigend nach `created_at`." }, + "payments": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DeliveryPayment" + }, + "description": "J\u00fcngste protokollierte Zahlungsabwicklung pro Lieferung (nur\nLieferungen mit mindestens einem Eintrag). Join per `delivery_id`." + }, "services": { "type": "array", "items": { diff --git a/packages/holzleitner_api/.openapi-generator/FILES b/packages/holzleitner_api/.openapi-generator/FILES index 43712ea..e07b1de 100644 --- a/packages/holzleitner_api/.openapi-generator/FILES +++ b/packages/holzleitner_api/.openapi-generator/FILES @@ -17,6 +17,8 @@ doc/CarResponse.md doc/CarsApi.md doc/CarsList.md doc/CompleteDeliveryAcknowledgements.md +doc/CompletedDeliveriesResponse.md +doc/CompletedDeliveryItem.md doc/ContactChannel.md doc/ContactKind.md doc/ContactRole.md @@ -34,10 +36,13 @@ doc/Delivery.md doc/DeliveryCredit.md doc/DeliveryCreditEventRequest.md doc/DeliveryCreditResponse.md +doc/DeliveryDetails.md doc/DeliveryItem.md doc/DeliveryNote.md doc/DeliveryNoteResponse.md doc/DeliveryOrderEntry.md +doc/DeliveryPayment.md +doc/DeliveryPaymentResponse.md doc/DeliveryResponse.md doc/DeliveryServiceResponse.md doc/DeliveryServiceValue.md @@ -52,6 +57,11 @@ doc/PaymentMethod.md doc/PaymentMethodResponse.md doc/PaymentMethodsApi.md doc/PaymentMethodsList.md +doc/PendingReviewResponse.md +doc/PositionsModifiedResponse.md +doc/RecordDeliveryPaymentRequest.md +doc/ResolveReviewRequest.md +doc/ReviewedItemResponse.md doc/ScanEvent.md doc/ScanResult.md doc/ScanResultStatus.md @@ -115,6 +125,8 @@ lib/src/model/car.dart lib/src/model/car_response.dart lib/src/model/cars_list.dart lib/src/model/complete_delivery_acknowledgements.dart +lib/src/model/completed_deliveries_response.dart +lib/src/model/completed_delivery_item.dart lib/src/model/contact_channel.dart lib/src/model/contact_kind.dart lib/src/model/contact_role.dart @@ -132,10 +144,13 @@ lib/src/model/delivery.dart lib/src/model/delivery_credit.dart lib/src/model/delivery_credit_event_request.dart lib/src/model/delivery_credit_response.dart +lib/src/model/delivery_details.dart lib/src/model/delivery_item.dart lib/src/model/delivery_note.dart lib/src/model/delivery_note_response.dart lib/src/model/delivery_order_entry.dart +lib/src/model/delivery_payment.dart +lib/src/model/delivery_payment_response.dart lib/src/model/delivery_response.dart lib/src/model/delivery_service_response.dart lib/src/model/delivery_service_value.dart @@ -148,6 +163,11 @@ lib/src/model/mark_mail_sent_response.dart lib/src/model/payment_method.dart lib/src/model/payment_method_response.dart lib/src/model/payment_methods_list.dart +lib/src/model/pending_review_response.dart +lib/src/model/positions_modified_response.dart +lib/src/model/record_delivery_payment_request.dart +lib/src/model/resolve_review_request.dart +lib/src/model/reviewed_item_response.dart lib/src/model/scan_event.dart lib/src/model/scan_result.dart lib/src/model/scan_result_status.dart @@ -177,12 +197,13 @@ lib/src/model/update_service_request.dart lib/src/model/warehouse.dart lib/src/serializers.dart pubspec.yaml -test/contact_channel_test.dart -test/contact_kind_test.dart -test/contact_role_test.dart -test/contact_source_test.dart -test/delivered_belegnummern_response_test.dart -test/mark_mail_sent_request_test.dart -test/mark_mail_sent_response_test.dart -test/sync_contact_channel_test.dart -test/sync_contact_source_test.dart +test/completed_deliveries_response_test.dart +test/completed_delivery_item_test.dart +test/delivery_details_test.dart +test/delivery_payment_response_test.dart +test/delivery_payment_test.dart +test/pending_review_response_test.dart +test/positions_modified_response_test.dart +test/record_delivery_payment_request_test.dart +test/resolve_review_request_test.dart +test/reviewed_item_response_test.dart diff --git a/packages/holzleitner_api/README.md b/packages/holzleitner_api/README.md index c14b6c3..9c161f7 100644 --- a/packages/holzleitner_api/README.md +++ b/packages/holzleitner_api/README.md @@ -66,10 +66,15 @@ All URIs are relative to *http://localhost* Class | Method | HTTP request | Description ------------ | ------------- | ------------- | ------------- [*AccountsApi*](doc/AccountsApi.md) | [**getAccount**](doc/AccountsApi.md#getaccount) | **GET** /accounts/{personalnummer} | Liest den Account zu einer Personalnummer. +[*AdminApi*](doc/AdminApi.md) | [**belegeDetails**](doc/AdminApi.md#belegedetails) | **GET** /admin/belege/{belegnummer} | Liefert das **volle Detail-Paket** zu einer ERP-Belegnummer: die Lieferung mit Positionen (inkl. Scan-Ständen), Kunde + Ansprechpartner, referenzierte Artikel/Lager, Notizen, Geld-Gutschrift, Dienstleistungswerte und Kontakte — destilliert aus dem Tour-Aggregat. Jede Lieferung, unabhängig vom Status. `404`, wenn die Belegnummer unbekannt ist. +[*AdminApi*](doc/AdminApi.md) | [**completedDeliveries**](doc/AdminApi.md#completeddeliveries) | **GET** /admin/completed-deliveries | Liefert **alle** abgeschlossenen (ausgelieferten) Lieferungen — optional auf einen Datumsbereich eingegrenzt, unabhängig vom Mail-Versand-Status. Gefiltert wird über den **Berliner** Kalendertag des Abschluss-Zeitpunkts (`completed_at`). [*AdminApi*](doc/AdminApi.md) | [**deliveredBelegnummern**](doc/AdminApi.md#deliveredbelegnummern) | **GET** /admin/delivered-belegnummern | Liefert die Belegnummern ausgelieferter (abgeschlossener) Lieferungen, **deren Liefermail noch nicht versendet wurde** (`mail_sent_at IS NULL`). „Ausgeliefert\" = es existiert ein Abschluss. Mit `day` (DD-MM-YYYY) nur Abschlüsse dieses Berliner Kalendertages; **ohne `day` alle offenen** (über alle Tage) — so bleiben Belege über Mitternacht nicht hängen. [*AdminApi*](doc/AdminApi.md) | [**importErp**](doc/AdminApi.md#importerp) | **POST** /admin/import-erp | Stößt den ERP-Import für ein Datum an und liefert die Zusammenfassung. +[*AdminApi*](doc/AdminApi.md) | [**listReviews**](doc/AdminApi.md#listreviews) | **GET** /admin/reviews | Listet alle geänderten Lieferscheine, die noch auf eine manuelle Bestätigung (Vier-Augen) warten — Entfernungen und/oder Geld-Gutschriften. [*AdminApi*](doc/AdminApi.md) | [**markMailSent**](doc/AdminApi.md#markmailsent) | **POST** /admin/mark-mail-sent | Markiert die Liefermails der angegebenen Belegnummern als **versendet** (`mail_sent_at = now()`, nur wo noch offen). Vom Mailclient aufzurufen, NACHDEM ERPframe die Mails erfolgreich verschickt hat — danach erscheinen die Belege nicht mehr in `GET /admin/delivered-belegnummern`. +[*AdminApi*](doc/AdminApi.md) | [**positionsModified**](doc/AdminApi.md#positionsmodified) | **GET** /admin/belege/{belegnummer}/positions-modified | Liefert zu **einer** ERP-Belegnummer, ob an der Lieferung Positionen verändert wurden (Menge reduziert/Zeile entfernt oder Geld-Gutschrift) — unabhängig vom Zustand der Lieferung. `404`, wenn die Belegnummer unbekannt ist. Gibt es mehrere Lieferungen mit derselben Belegnummer, ist das Flag `true`, sobald **eine** davon verändert ist. [*AdminApi*](doc/AdminApi.md) | [**pushCompletion**](doc/AdminApi.md#pushcompletion) | **POST** /admin/push-completion | Stößt das ERP-Rückschreiben eines bereits lokal abgeschlossenen Lieferabschlusses erneut an (idempotenter Retry, falls der automatische Push beim Abschluss fehlschlug). +[*AdminApi*](doc/AdminApi.md) | [**resolveReview**](doc/AdminApi.md#resolvereview) | **POST** /admin/reviews/{delivery_id}/resolve | Bestätigt die Prüfung einer geänderten Lieferung (Vier-Augen) — die Lieferung verschwindet danach aus `GET /admin/reviews` (sofern nicht erneut geändert). [*AttachmentsApi*](doc/AttachmentsApi.md) | [**getAttachment**](doc/AttachmentsApi.md#getattachment) | **GET** /attachments/{id} | Liefert ein gerendertes Vorschaubild des Attachments (Bytes), geladen aus DOCUframe. Auflösung/Format über Query-Parameter steuerbar (`?w=&h=&q=&ext=&page=`). [*CarsApi*](doc/CarsApi.md) | [**createMyCar**](doc/CarsApi.md#createmycar) | **POST** /me/cars | Legt ein neues Fahrzeug für den angemeldeten Fahrer an. [*CarsApi*](doc/CarsApi.md) | [**listMyCars**](doc/CarsApi.md#listmycars) | **GET** /me/cars | Listet die Fahrzeuge des angemeldeten Fahrers. @@ -82,6 +87,7 @@ Class | Method | HTTP request | Description [*DeliveriesApi*](doc/DeliveriesApi.md) | [**deleteNote**](doc/DeliveriesApi.md#deletenote) | **DELETE** /deliveries/{delivery_id}/notes/{note_id} | Löscht eine Notiz. Antwortet mit `204 No Content`. [*DeliveriesApi*](doc/DeliveriesApi.md) | [**deleteServiceValue**](doc/DeliveriesApi.md#deleteservicevalue) | **DELETE** /deliveries/{delivery_id}/services/{service_id} | Entfernt den Service-Wert einer Lieferung (Service „nicht gesetzt\"). Nur bei aktiver Lieferung. Antwort `204`. [*DeliveriesApi*](doc/DeliveriesApi.md) | [**hold**](doc/DeliveriesApi.md#hold) | **POST** /deliveries/{delivery_id}/hold | Setzt die Lieferung auf `held`. Nur aus `active` zulässig. +[*DeliveriesApi*](doc/DeliveriesApi.md) | [**recordPayment**](doc/DeliveriesApi.md#recordpayment) | **POST** /deliveries/{delivery_id}/payment | Protokolliert die Zahlungsabwicklung („Abkassieren\") einer Lieferung. Append-only, idempotent über `clientEventId`. Der Server berechnet den offenen Betrag selbst; er muss > 0 sein und `expectedAmountCents` entsprechen. Nur bei aktiver Lieferung und aktiver Zahlungsmethode. [*DeliveriesApi*](doc/DeliveriesApi.md) | [**resume**](doc/DeliveriesApi.md#resume) | **POST** /deliveries/{delivery_id}/resume | Setzt die Lieferung zurück auf `active`. Nur aus `held` zulässig. [*DeliveriesApi*](doc/DeliveriesApi.md) | [**setService**](doc/DeliveriesApi.md#setservice) | **PUT** /deliveries/{delivery_id}/services/{service_id} | Setzt (Upsert) den Wert eines Service für eine Lieferung. Genau das zum Service-Typ passende Feld (`boolValue`/`numericValue`) muss gesetzt sein; numerische Werte werden gegen min/max geprüft. Nur bei aktiver Lieferung. [*DeliveriesApi*](doc/DeliveriesApi.md) | [**updateNote**](doc/DeliveriesApi.md#updatenote) | **PATCH** /deliveries/{delivery_id}/notes/{note_id} | Ändert Text/Bild einer Notiz. Innerhalb des (geteilten) Accounts darf jeder Fahrer Notizen pflegen — kein Autor-Check. `delivery_id` ist Teil des Pfads (REST-Konsistenz), die Notiz wird über `note_id` adressiert. @@ -98,7 +104,7 @@ Class | Method | HTTP request | Description [*ServicesApi*](doc/ServicesApi.md) | [**updateService**](doc/ServicesApi.md#updateservice) | **PATCH** /services/{id} | Patcht Name/Grenzen/Aktiv-Flag/Sortierung. `kind` ist nicht änderbar. [*SyncApi*](doc/SyncApi.md) | [**syncTour**](doc/SyncApi.md#synctour) | **POST** /sync/tour | Sync-Endpoint für das ERP: legt eine Tagestour samt Lieferungen und Positionen idempotent an. Identität pro Tour `(driver_personalnummer, tour_date)`, pro Lieferung `(belegart_id, belegnummer)`. [*ToursApi*](doc/ToursApi.md) | [**getTour**](doc/ToursApi.md#gettour) | **GET** /tours/{tour_id} | Lädt eine Tour mit allen Lieferungen, Positionen und referenzierten Stammdaten — die App nutzt das als einzigen großen Read. -[*ToursApi*](doc/ToursApi.md) | [**listMyToursToday**](doc/ToursApi.md#listmytourstoday) | **GET** /me/tours/today | Listet heutige Touren des angemeldeten Fahrers (Filter aus dem JWT). +[*ToursApi*](doc/ToursApi.md) | [**listMyTours**](doc/ToursApi.md#listmytours) | **GET** /me/tours | Listet die Touren des angemeldeten Fahrers (Filter aus dem JWT) für ein Ziel-Datum. Ohne `?date=` gilt „heute\". Der Fahrer wählt das Datum aktiv im Kopf-Kalender der App (z. B. um die Tour von morgen vorab zu sehen). [*ToursApi*](doc/ToursApi.md) | [**setDeliveryOrder**](doc/ToursApi.md#setdeliveryorder) | **PUT** /tours/{tour_id}/delivery-order | Schreibt die Sortier-Reihenfolge aller Lieferungen einer Tour neu. Der Client schickt die **vollständige** neue Reihenfolge; fehlende oder fremde Lieferungs-Ids werden mit `400 validation` abgelehnt. @@ -116,6 +122,8 @@ Class | Method | HTTP request | Description - [CarResponse](doc/CarResponse.md) - [CarsList](doc/CarsList.md) - [CompleteDeliveryAcknowledgements](doc/CompleteDeliveryAcknowledgements.md) + - [CompletedDeliveriesResponse](doc/CompletedDeliveriesResponse.md) + - [CompletedDeliveryItem](doc/CompletedDeliveryItem.md) - [ContactChannel](doc/ContactChannel.md) - [ContactKind](doc/ContactKind.md) - [ContactRole](doc/ContactRole.md) @@ -132,10 +140,13 @@ Class | Method | HTTP request | Description - [DeliveryCredit](doc/DeliveryCredit.md) - [DeliveryCreditEventRequest](doc/DeliveryCreditEventRequest.md) - [DeliveryCreditResponse](doc/DeliveryCreditResponse.md) + - [DeliveryDetails](doc/DeliveryDetails.md) - [DeliveryItem](doc/DeliveryItem.md) - [DeliveryNote](doc/DeliveryNote.md) - [DeliveryNoteResponse](doc/DeliveryNoteResponse.md) - [DeliveryOrderEntry](doc/DeliveryOrderEntry.md) + - [DeliveryPayment](doc/DeliveryPayment.md) + - [DeliveryPaymentResponse](doc/DeliveryPaymentResponse.md) - [DeliveryResponse](doc/DeliveryResponse.md) - [DeliveryServiceResponse](doc/DeliveryServiceResponse.md) - [DeliveryServiceValue](doc/DeliveryServiceValue.md) @@ -148,6 +159,11 @@ Class | Method | HTTP request | Description - [PaymentMethod](doc/PaymentMethod.md) - [PaymentMethodResponse](doc/PaymentMethodResponse.md) - [PaymentMethodsList](doc/PaymentMethodsList.md) + - [PendingReviewResponse](doc/PendingReviewResponse.md) + - [PositionsModifiedResponse](doc/PositionsModifiedResponse.md) + - [RecordDeliveryPaymentRequest](doc/RecordDeliveryPaymentRequest.md) + - [ResolveReviewRequest](doc/ResolveReviewRequest.md) + - [ReviewedItemResponse](doc/ReviewedItemResponse.md) - [ScanEvent](doc/ScanEvent.md) - [ScanResult](doc/ScanResult.md) - [ScanResultStatus](doc/ScanResultStatus.md) diff --git a/packages/holzleitner_api/doc/AdminApi.md b/packages/holzleitner_api/doc/AdminApi.md index 9dc0772..dddc44d 100644 --- a/packages/holzleitner_api/doc/AdminApi.md +++ b/packages/holzleitner_api/doc/AdminApi.md @@ -9,12 +9,113 @@ All URIs are relative to *http://localhost* Method | HTTP request | Description ------------- | ------------- | ------------- +[**belegeDetails**](AdminApi.md#belegedetails) | **GET** /admin/belege/{belegnummer} | Liefert das **volle Detail-Paket** zu einer ERP-Belegnummer: die Lieferung mit Positionen (inkl. Scan-Ständen), Kunde + Ansprechpartner, referenzierte Artikel/Lager, Notizen, Geld-Gutschrift, Dienstleistungswerte und Kontakte — destilliert aus dem Tour-Aggregat. Jede Lieferung, unabhängig vom Status. `404`, wenn die Belegnummer unbekannt ist. +[**completedDeliveries**](AdminApi.md#completeddeliveries) | **GET** /admin/completed-deliveries | Liefert **alle** abgeschlossenen (ausgelieferten) Lieferungen — optional auf einen Datumsbereich eingegrenzt, unabhängig vom Mail-Versand-Status. Gefiltert wird über den **Berliner** Kalendertag des Abschluss-Zeitpunkts (`completed_at`). [**deliveredBelegnummern**](AdminApi.md#deliveredbelegnummern) | **GET** /admin/delivered-belegnummern | Liefert die Belegnummern ausgelieferter (abgeschlossener) Lieferungen, **deren Liefermail noch nicht versendet wurde** (`mail_sent_at IS NULL`). „Ausgeliefert\" = es existiert ein Abschluss. Mit `day` (DD-MM-YYYY) nur Abschlüsse dieses Berliner Kalendertages; **ohne `day` alle offenen** (über alle Tage) — so bleiben Belege über Mitternacht nicht hängen. [**importErp**](AdminApi.md#importerp) | **POST** /admin/import-erp | Stößt den ERP-Import für ein Datum an und liefert die Zusammenfassung. +[**listReviews**](AdminApi.md#listreviews) | **GET** /admin/reviews | Listet alle geänderten Lieferscheine, die noch auf eine manuelle Bestätigung (Vier-Augen) warten — Entfernungen und/oder Geld-Gutschriften. [**markMailSent**](AdminApi.md#markmailsent) | **POST** /admin/mark-mail-sent | Markiert die Liefermails der angegebenen Belegnummern als **versendet** (`mail_sent_at = now()`, nur wo noch offen). Vom Mailclient aufzurufen, NACHDEM ERPframe die Mails erfolgreich verschickt hat — danach erscheinen die Belege nicht mehr in `GET /admin/delivered-belegnummern`. +[**positionsModified**](AdminApi.md#positionsmodified) | **GET** /admin/belege/{belegnummer}/positions-modified | Liefert zu **einer** ERP-Belegnummer, ob an der Lieferung Positionen verändert wurden (Menge reduziert/Zeile entfernt oder Geld-Gutschrift) — unabhängig vom Zustand der Lieferung. `404`, wenn die Belegnummer unbekannt ist. Gibt es mehrere Lieferungen mit derselben Belegnummer, ist das Flag `true`, sobald **eine** davon verändert ist. [**pushCompletion**](AdminApi.md#pushcompletion) | **POST** /admin/push-completion | Stößt das ERP-Rückschreiben eines bereits lokal abgeschlossenen Lieferabschlusses erneut an (idempotenter Retry, falls der automatische Push beim Abschluss fehlschlug). +[**resolveReview**](AdminApi.md#resolvereview) | **POST** /admin/reviews/{delivery_id}/resolve | Bestätigt die Prüfung einer geänderten Lieferung (Vier-Augen) — die Lieferung verschwindet danach aus `GET /admin/reviews` (sofern nicht erneut geändert). +# **belegeDetails** +> DeliveryDetails belegeDetails(belegnummer) + +Liefert das **volle Detail-Paket** zu einer ERP-Belegnummer: die Lieferung mit Positionen (inkl. Scan-Ständen), Kunde + Ansprechpartner, referenzierte Artikel/Lager, Notizen, Geld-Gutschrift, Dienstleistungswerte und Kontakte — destilliert aus dem Tour-Aggregat. Jede Lieferung, unabhängig vom Status. `404`, wenn die Belegnummer unbekannt ist. + +### Example +```dart +import 'package:holzleitner_api/api.dart'; +// TODO Configure API key authorization: admin_api_key +//defaultApiClient.getAuthentication('admin_api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('admin_api_key').apiKeyPrefix = 'Bearer'; + +final api = HolzleitnerApi().getAdminApi(); +final String belegnummer = belegnummer_example; // String | ERP-Belegnummer, z. B. V-30690291 + +try { + final response = api.belegeDetails(belegnummer); + print(response); +} catch on DioException (e) { + print('Exception when calling AdminApi->belegeDetails: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **belegnummer** | **String**| ERP-Belegnummer, z. B. V-30690291 | + +### Return type + +[**DeliveryDetails**](DeliveryDetails.md) + +### Authorization + +[admin_api_key](../README.md#admin_api_key) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + +# **completedDeliveries** +> CompletedDeliveriesResponse completedDeliveries(day, from, to) + +Liefert **alle** abgeschlossenen (ausgelieferten) Lieferungen — optional auf einen Datumsbereich eingegrenzt, unabhängig vom Mail-Versand-Status. Gefiltert wird über den **Berliner** Kalendertag des Abschluss-Zeitpunkts (`completed_at`). + +Parameter (alle `DD-MM-YYYY`, alle optional): `day` = Einzeltag (Kurzform `from=to=day`), sonst `from`/`to` als **inklusive** Bereichsgrenzen (je offen). **Ohne jeden Parameter → ALLE ausgelieferten Belege** (kein Datumsfilter). Pro Lieferung: Belegnummer + `positions_modified` (Menge reduziert/Zeile entfernt oder Geld-Gutschrift). Die Halb-Grenzen-Variante ist für Range-Filter gedacht: `?from=…` und `?to=…` liefern je eine Menge, deren SQL-`AND`-Schnitt den Zeitraum ergibt. + +### Example +```dart +import 'package:holzleitner_api/api.dart'; +// TODO Configure API key authorization: admin_api_key +//defaultApiClient.getAuthentication('admin_api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('admin_api_key').apiKeyPrefix = 'Bearer'; + +final api = HolzleitnerApi().getAdminApi(); +final String day = day_example; // String | Einzeltag DD-MM-YYYY (Kurzform from=to) +final String from = from_example; // String | Untere Grenze DD-MM-YYYY (inklusive) +final String to = to_example; // String | Obere Grenze DD-MM-YYYY (inklusive) + +try { + final response = api.completedDeliveries(day, from, to); + print(response); +} catch on DioException (e) { + print('Exception when calling AdminApi->completedDeliveries: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **day** | **String**| Einzeltag DD-MM-YYYY (Kurzform from=to) | [optional] + **from** | **String**| Untere Grenze DD-MM-YYYY (inklusive) | [optional] + **to** | **String**| Obere Grenze DD-MM-YYYY (inklusive) | [optional] + +### Return type + +[**CompletedDeliveriesResponse**](CompletedDeliveriesResponse.md) + +### Authorization + +[admin_api_key](../README.md#admin_api_key) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + # **deliveredBelegnummern** > DeliveredBelegnummernResponse deliveredBelegnummern(day) @@ -105,6 +206,47 @@ Name | Type | Description | Notes [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) +# **listReviews** +> BuiltList listReviews() + +Listet alle geänderten Lieferscheine, die noch auf eine manuelle Bestätigung (Vier-Augen) warten — Entfernungen und/oder Geld-Gutschriften. + +### Example +```dart +import 'package:holzleitner_api/api.dart'; +// TODO Configure API key authorization: admin_api_key +//defaultApiClient.getAuthentication('admin_api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('admin_api_key').apiKeyPrefix = 'Bearer'; + +final api = HolzleitnerApi().getAdminApi(); + +try { + final response = api.listReviews(); + print(response); +} catch on DioException (e) { + print('Exception when calling AdminApi->listReviews: $e\n'); +} +``` + +### Parameters +This endpoint does not need any parameter. + +### Return type + +[**BuiltList<PendingReviewResponse>**](PendingReviewResponse.md) + +### Authorization + +[admin_api_key](../README.md#admin_api_key) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + # **markMailSent** > MarkMailSentResponse markMailSent(markMailSentRequest) @@ -150,6 +292,51 @@ Name | Type | Description | Notes [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) +# **positionsModified** +> PositionsModifiedResponse positionsModified(belegnummer) + +Liefert zu **einer** ERP-Belegnummer, ob an der Lieferung Positionen verändert wurden (Menge reduziert/Zeile entfernt oder Geld-Gutschrift) — unabhängig vom Zustand der Lieferung. `404`, wenn die Belegnummer unbekannt ist. Gibt es mehrere Lieferungen mit derselben Belegnummer, ist das Flag `true`, sobald **eine** davon verändert ist. + +### Example +```dart +import 'package:holzleitner_api/api.dart'; +// TODO Configure API key authorization: admin_api_key +//defaultApiClient.getAuthentication('admin_api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('admin_api_key').apiKeyPrefix = 'Bearer'; + +final api = HolzleitnerApi().getAdminApi(); +final String belegnummer = belegnummer_example; // String | ERP-Belegnummer, z. B. V-30690291 + +try { + final response = api.positionsModified(belegnummer); + print(response); +} catch on DioException (e) { + print('Exception when calling AdminApi->positionsModified: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **belegnummer** | **String**| ERP-Belegnummer, z. B. V-30690291 | + +### Return type + +[**PositionsModifiedResponse**](PositionsModifiedResponse.md) + +### Authorization + +[admin_api_key](../README.md#admin_api_key) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + # **pushCompletion** > pushCompletion(deliveryId) @@ -194,3 +381,49 @@ void (empty response body) [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) +# **resolveReview** +> resolveReview(deliveryId, resolveReviewRequest) + +Bestätigt die Prüfung einer geänderten Lieferung (Vier-Augen) — die Lieferung verschwindet danach aus `GET /admin/reviews` (sofern nicht erneut geändert). + +### Example +```dart +import 'package:holzleitner_api/api.dart'; +// TODO Configure API key authorization: admin_api_key +//defaultApiClient.getAuthentication('admin_api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('admin_api_key').apiKeyPrefix = 'Bearer'; + +final api = HolzleitnerApi().getAdminApi(); +final String deliveryId = deliveryId_example; // String | UUID der Lieferung +final ResolveReviewRequest resolveReviewRequest = ; // ResolveReviewRequest | + +try { + api.resolveReview(deliveryId, resolveReviewRequest); +} catch on DioException (e) { + print('Exception when calling AdminApi->resolveReview: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **deliveryId** | **String**| UUID der Lieferung | + **resolveReviewRequest** | [**ResolveReviewRequest**](ResolveReviewRequest.md)| | + +### Return type + +void (empty response body) + +### Authorization + +[admin_api_key](../README.md#admin_api_key) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: Not defined + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + diff --git a/packages/holzleitner_api/doc/CompletedDeliveriesResponse.md b/packages/holzleitner_api/doc/CompletedDeliveriesResponse.md new file mode 100644 index 0000000..057daf2 --- /dev/null +++ b/packages/holzleitner_api/doc/CompletedDeliveriesResponse.md @@ -0,0 +1,18 @@ +# holzleitner_api.model.CompletedDeliveriesResponse + +## Load the model package +```dart +import 'package:holzleitner_api/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**count** | **int** | Anzahl der abgeschlossenen Lieferungen im Bereich. | +**deliveries** | [**BuiltList<CompletedDeliveryItem>**](CompletedDeliveryItem.md) | Die abgeschlossenen Lieferungen, aufsteigend nach Abschluss-Zeitpunkt. | +**from** | **String** | Wirksame untere Grenze (ISO `YYYY-MM-DD`) oder `null` (offen). | [optional] +**to** | **String** | Wirksame obere Grenze (ISO `YYYY-MM-DD`) oder `null` (offen). | [optional] + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/packages/holzleitner_api/doc/CompletedDeliveryItem.md b/packages/holzleitner_api/doc/CompletedDeliveryItem.md new file mode 100644 index 0000000..432a8e9 --- /dev/null +++ b/packages/holzleitner_api/doc/CompletedDeliveryItem.md @@ -0,0 +1,16 @@ +# holzleitner_api.model.CompletedDeliveryItem + +## Load the model package +```dart +import 'package:holzleitner_api/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**belegnummer** | **String** | ERP-Belegnummer der Lieferung. | +**positionsModified** | **bool** | `true`, wenn an der Lieferung Positionen verändert wurden — eine Zeile wurde entfernt oder in der Menge reduziert (Stück-Gutschrift) **oder** es liegt eine aktive Geld-Gutschrift vor. | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/packages/holzleitner_api/doc/DeliveriesApi.md b/packages/holzleitner_api/doc/DeliveriesApi.md index 4f5a3c3..ba0cb8b 100644 --- a/packages/holzleitner_api/doc/DeliveriesApi.md +++ b/packages/holzleitner_api/doc/DeliveriesApi.md @@ -17,6 +17,7 @@ Method | HTTP request | Description [**deleteNote**](DeliveriesApi.md#deletenote) | **DELETE** /deliveries/{delivery_id}/notes/{note_id} | Löscht eine Notiz. Antwortet mit `204 No Content`. [**deleteServiceValue**](DeliveriesApi.md#deleteservicevalue) | **DELETE** /deliveries/{delivery_id}/services/{service_id} | Entfernt den Service-Wert einer Lieferung (Service „nicht gesetzt\"). Nur bei aktiver Lieferung. Antwort `204`. [**hold**](DeliveriesApi.md#hold) | **POST** /deliveries/{delivery_id}/hold | Setzt die Lieferung auf `held`. Nur aus `active` zulässig. +[**recordPayment**](DeliveriesApi.md#recordpayment) | **POST** /deliveries/{delivery_id}/payment | Protokolliert die Zahlungsabwicklung („Abkassieren\") einer Lieferung. Append-only, idempotent über `clientEventId`. Der Server berechnet den offenen Betrag selbst; er muss > 0 sein und `expectedAmountCents` entsprechen. Nur bei aktiver Lieferung und aktiver Zahlungsmethode. [**resume**](DeliveriesApi.md#resume) | **POST** /deliveries/{delivery_id}/resume | Setzt die Lieferung zurück auf `active`. Nur aus `held` zulässig. [**setService**](DeliveriesApi.md#setservice) | **PUT** /deliveries/{delivery_id}/services/{service_id} | Setzt (Upsert) den Wert eines Service für eine Lieferung. Genau das zum Service-Typ passende Feld (`boolValue`/`numericValue`) muss gesetzt sein; numerische Werte werden gegen min/max geprüft. Nur bei aktiver Lieferung. [**updateNote**](DeliveriesApi.md#updatenote) | **PATCH** /deliveries/{delivery_id}/notes/{note_id} | Ändert Text/Bild einer Notiz. Innerhalb des (geteilten) Accounts darf jeder Fahrer Notizen pflegen — kein Autor-Check. `delivery_id` ist Teil des Pfads (REST-Konsistenz), die Notiz wird über `note_id` adressiert. @@ -365,6 +366,49 @@ Name | Type | Description | Notes [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) +# **recordPayment** +> DeliveryPaymentResponse recordPayment(deliveryId, recordDeliveryPaymentRequest) + +Protokolliert die Zahlungsabwicklung („Abkassieren\") einer Lieferung. Append-only, idempotent über `clientEventId`. Der Server berechnet den offenen Betrag selbst; er muss > 0 sein und `expectedAmountCents` entsprechen. Nur bei aktiver Lieferung und aktiver Zahlungsmethode. + +### Example +```dart +import 'package:holzleitner_api/api.dart'; + +final api = HolzleitnerApi().getDeliveriesApi(); +final String deliveryId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | +final RecordDeliveryPaymentRequest recordDeliveryPaymentRequest = ; // RecordDeliveryPaymentRequest | + +try { + final response = api.recordPayment(deliveryId, recordDeliveryPaymentRequest); + print(response); +} catch on DioException (e) { + print('Exception when calling DeliveriesApi->recordPayment: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **deliveryId** | **String**| | + **recordDeliveryPaymentRequest** | [**RecordDeliveryPaymentRequest**](RecordDeliveryPaymentRequest.md)| | + +### Return type + +[**DeliveryPaymentResponse**](DeliveryPaymentResponse.md) + +### Authorization + +[bearer_auth](../README.md#bearer_auth) + +### HTTP request headers + + - **Content-Type**: application/json + - **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + # **resume** > DeliveryResponse resume(deliveryId) diff --git a/packages/holzleitner_api/doc/DeliveryDetails.md b/packages/holzleitner_api/doc/DeliveryDetails.md new file mode 100644 index 0000000..032b6e4 --- /dev/null +++ b/packages/holzleitner_api/doc/DeliveryDetails.md @@ -0,0 +1,27 @@ +# holzleitner_api.model.DeliveryDetails + +## Load the model package +```dart +import 'package:holzleitner_api/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**articles** | [**BuiltList<Article>**](Article.md) | Nur die von den Positionen referenzierten Artikel. | +**contactChannels** | [**BuiltList<ContactChannel>**](ContactChannel.md) | Die zu `contactSources` gehörenden Einzel-Kanäle. Join per `sourceId`. | +**contactSources** | [**BuiltList<ContactSource>**](ContactSource.md) | Kontaktquellen der Lieferung (Liefer-/Rechnungsadresse, Ansprechpartner …). | +**credit** | [**DeliveryCredit**](DeliveryCredit.md) | Aktuelle Betrags-Gutschrift (`null`, wenn keine aktiv). | [optional] +**customer** | [**Customer**](Customer.md) | Der Kunde der Lieferung (`null`, falls unauffindbar — sollte nicht vorkommen). | [optional] +**customerContacts** | [**BuiltList<CustomerContact>**](CustomerContact.md) | Ansprechpartner des Kunden. | +**delivery** | [**DeliveryWithItems**](DeliveryWithItems.md) | Die Lieferung selbst inkl. Positionen (mit Scan-Ständen) und `sortOrder`. | +**deliveryServices** | [**BuiltList<DeliveryServiceValue>**](DeliveryServiceValue.md) | Für diese Lieferung gesetzte Service-Werte. | +**notes** | [**BuiltList<DeliveryNote>**](DeliveryNote.md) | Notizen der Lieferung, aufsteigend nach `createdAt`. | +**payment** | [**DeliveryPayment**](DeliveryPayment.md) | Jüngste protokollierte Zahlungsabwicklung (`None` = keine). | [optional] +**services** | [**BuiltList<Service>**](Service.md) | Aktive Service-Definitionen (Stammdaten). | +**tour** | [**Tour**](Tour.md) | Die Tour, zu der die Lieferung gehört. | +**warehouses** | [**BuiltList<Warehouse>**](Warehouse.md) | Nur die von den Positionen referenzierten Lager. | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/packages/holzleitner_api/doc/DeliveryPayment.md b/packages/holzleitner_api/doc/DeliveryPayment.md new file mode 100644 index 0000000..4794a32 --- /dev/null +++ b/packages/holzleitner_api/doc/DeliveryPayment.md @@ -0,0 +1,22 @@ +# holzleitner_api.model.DeliveryPayment + +## Load the model package +```dart +import 'package:holzleitner_api/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**amountCents** | **int** | Abgewickelter Betrag in Cent (offener Betrag bei Bestätigung). | +**deliveryId** | **String** | | +**id** | **String** | | +**paymentMethodCode** | **String** | Snapshot des Methoden-Codes (`cash`, `ec_card`, `invoice`, …). | +**paymentMethodId** | **String** | | +**recordedAt** | [**DateTime**](DateTime.md) | | +**recordedByCarId** | **String** | | [optional] +**recordedByPersonalnummer** | **int** | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/packages/holzleitner_api/doc/DeliveryPaymentResponse.md b/packages/holzleitner_api/doc/DeliveryPaymentResponse.md new file mode 100644 index 0000000..09b5a16 --- /dev/null +++ b/packages/holzleitner_api/doc/DeliveryPaymentResponse.md @@ -0,0 +1,15 @@ +# holzleitner_api.model.DeliveryPaymentResponse + +## Load the model package +```dart +import 'package:holzleitner_api/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**payment** | [**DeliveryPayment**](DeliveryPayment.md) | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/packages/holzleitner_api/doc/PendingReviewResponse.md b/packages/holzleitner_api/doc/PendingReviewResponse.md new file mode 100644 index 0000000..d4cf89c --- /dev/null +++ b/packages/holzleitner_api/doc/PendingReviewResponse.md @@ -0,0 +1,23 @@ +# holzleitner_api.model.PendingReviewResponse + +## Load the model package +```dart +import 'package:holzleitner_api/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**creditedItems** | [**BuiltList<ReviewedItemResponse>**](ReviewedItemResponse.md) | Entfernte/teil-gutgeschriebene Positionen. | +**customerName** | **String** | | +**deliveryId** | **String** | | +**erpBelegartId** | **int** | | +**erpBelegnummer** | **String** | | +**lastChangeAt** | **String** | Zeitpunkt der letzten beleg-ändernden Aktion (RFC 3339). | +**moneyCreditCents** | **int** | Geld-Gutschrift in Cent (0 = keine). | +**moneyCreditReason** | **String** | | [optional] +**tourDate** | **String** | Tourdatum (ISO `YYYY-MM-DD`). | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/packages/holzleitner_api/doc/PositionsModifiedResponse.md b/packages/holzleitner_api/doc/PositionsModifiedResponse.md new file mode 100644 index 0000000..6554048 --- /dev/null +++ b/packages/holzleitner_api/doc/PositionsModifiedResponse.md @@ -0,0 +1,16 @@ +# holzleitner_api.model.PositionsModifiedResponse + +## Load the model package +```dart +import 'package:holzleitner_api/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**belegnummer** | **String** | ERP-Belegnummer, nach der gefragt wurde. | +**positionsModified** | **bool** | `true`, wenn an der Lieferung Positionen verändert wurden — eine Zeile wurde entfernt oder in der Menge reduziert (Stück-Gutschrift) **oder** es liegt eine aktive Geld-Gutschrift vor. | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/packages/holzleitner_api/doc/RecordDeliveryPaymentRequest.md b/packages/holzleitner_api/doc/RecordDeliveryPaymentRequest.md new file mode 100644 index 0000000..78ccac9 --- /dev/null +++ b/packages/holzleitner_api/doc/RecordDeliveryPaymentRequest.md @@ -0,0 +1,18 @@ +# holzleitner_api.model.RecordDeliveryPaymentRequest + +## Load the model package +```dart +import 'package:holzleitner_api/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**authorCarId** | **String** | Fahrzeug des Akteurs (Audit-Spur). Muss zum Account gehören. | [optional] +**clientEventId** | **String** | Idempotenz-Schlüssel — pro Bestätigung genau einmal vergeben. | +**expectedAmountCents** | **int** | Betrag in Cent, den der Fahrer in der App gesehen und bestätigt hat. Der Server berechnet den offenen Betrag selbst und lehnt ab, wenn er davon abweicht — so landet nie ein Betrag im Protokoll, den der Fahrer nicht gesehen hat. | +**paymentMethodId** | **String** | Gewählte Zahlungsmethode. Muss existieren und aktiv sein. | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/packages/holzleitner_api/doc/ResolveReviewRequest.md b/packages/holzleitner_api/doc/ResolveReviewRequest.md new file mode 100644 index 0000000..10acbb3 --- /dev/null +++ b/packages/holzleitner_api/doc/ResolveReviewRequest.md @@ -0,0 +1,16 @@ +# holzleitner_api.model.ResolveReviewRequest + +## Load the model package +```dart +import 'package:holzleitner_api/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**note** | **String** | Optionale Notiz zur getroffenen Entscheidung. | [optional] +**resolvedBy** | **String** | Bearbeiter (Name/Kürzel), der die Prüfung bestätigt. | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/packages/holzleitner_api/doc/ReviewedItemResponse.md b/packages/holzleitner_api/doc/ReviewedItemResponse.md new file mode 100644 index 0000000..e529947 --- /dev/null +++ b/packages/holzleitner_api/doc/ReviewedItemResponse.md @@ -0,0 +1,20 @@ +# holzleitner_api.model.ReviewedItemResponse + +## Load the model package +```dart +import 'package:holzleitner_api/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**articleName** | **String** | | +**artikelNr** | **String** | | +**belegzeilenNr** | **int** | | +**creditedQuantity** | **int** | | +**reason** | **String** | | [optional] +**requiredQuantity** | **int** | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/packages/holzleitner_api/doc/TourDetails.md b/packages/holzleitner_api/doc/TourDetails.md index eeac1c6..a1b875c 100644 --- a/packages/holzleitner_api/doc/TourDetails.md +++ b/packages/holzleitner_api/doc/TourDetails.md @@ -17,6 +17,7 @@ Name | Type | Description | Notes **deliveries** | [**BuiltList<DeliveryWithItems>**](DeliveryWithItems.md) | | **deliveryServices** | [**BuiltList<DeliveryServiceValue>**](DeliveryServiceValue.md) | Pro-Lieferung gesetzte Service-Werte. Join per `delivery_id` + `service_id`. | **notes** | [**BuiltList<DeliveryNote>**](DeliveryNote.md) | Alle Notizen aller Lieferungen dieser Tour, in einer Liste. Die App joint clientseitig per `delivery_id`. Reihenfolge: pro Lieferung aufsteigend nach `created_at`. | +**payments** | [**BuiltList<DeliveryPayment>**](DeliveryPayment.md) | Jüngste protokollierte Zahlungsabwicklung pro Lieferung (nur Lieferungen mit mindestens einem Eintrag). Join per `delivery_id`. | **services** | [**BuiltList<Service>**](Service.md) | Aktive Service-Definitionen (Stammdaten) — die App rendert daraus Phase 4. Bewusst hier mitgeliefert, damit die Detailseite alles aus dem Tour-Aggregat hat. | **tour** | [**Tour**](Tour.md) | | **warehouses** | [**BuiltList<Warehouse>**](Warehouse.md) | | diff --git a/packages/holzleitner_api/doc/ToursApi.md b/packages/holzleitner_api/doc/ToursApi.md index bf5ddf4..4fafe70 100644 --- a/packages/holzleitner_api/doc/ToursApi.md +++ b/packages/holzleitner_api/doc/ToursApi.md @@ -10,7 +10,7 @@ All URIs are relative to *http://localhost* Method | HTTP request | Description ------------- | ------------- | ------------- [**getTour**](ToursApi.md#gettour) | **GET** /tours/{tour_id} | Lädt eine Tour mit allen Lieferungen, Positionen und referenzierten Stammdaten — die App nutzt das als einzigen großen Read. -[**listMyToursToday**](ToursApi.md#listmytourstoday) | **GET** /me/tours/today | Listet heutige Touren des angemeldeten Fahrers (Filter aus dem JWT). +[**listMyTours**](ToursApi.md#listmytours) | **GET** /me/tours | Listet die Touren des angemeldeten Fahrers (Filter aus dem JWT) für ein Ziel-Datum. Ohne `?date=` gilt „heute\". Der Fahrer wählt das Datum aktiv im Kopf-Kalender der App (z. B. um die Tour von morgen vorab zu sehen). [**setDeliveryOrder**](ToursApi.md#setdeliveryorder) | **PUT** /tours/{tour_id}/delivery-order | Schreibt die Sortier-Reihenfolge aller Lieferungen einer Tour neu. Der Client schickt die **vollständige** neue Reihenfolge; fehlende oder fremde Lieferungs-Ids werden mit `400 validation` abgelehnt. @@ -55,27 +55,31 @@ Name | Type | Description | Notes [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) -# **listMyToursToday** -> TourSummaryList listMyToursToday() +# **listMyTours** +> TourSummaryList listMyTours(date) -Listet heutige Touren des angemeldeten Fahrers (Filter aus dem JWT). +Listet die Touren des angemeldeten Fahrers (Filter aus dem JWT) für ein Ziel-Datum. Ohne `?date=` gilt „heute\". Der Fahrer wählt das Datum aktiv im Kopf-Kalender der App (z. B. um die Tour von morgen vorab zu sehen). ### Example ```dart import 'package:holzleitner_api/api.dart'; final api = HolzleitnerApi().getToursApi(); +final String date = date_example; // String | Ziel-Tourdatum YYYY-MM-DD (Default: heute) try { - final response = api.listMyToursToday(); + final response = api.listMyTours(date); print(response); } catch on DioException (e) { - print('Exception when calling ToursApi->listMyToursToday: $e\n'); + print('Exception when calling ToursApi->listMyTours: $e\n'); } ``` ### Parameters -This endpoint does not need any parameter. + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **date** | **String**| Ziel-Tourdatum YYYY-MM-DD (Default: heute) | [optional] ### Return type diff --git a/packages/holzleitner_api/lib/holzleitner_api.dart b/packages/holzleitner_api/lib/holzleitner_api.dart index 275b779..025b201 100644 --- a/packages/holzleitner_api/lib/holzleitner_api.dart +++ b/packages/holzleitner_api/lib/holzleitner_api.dart @@ -34,6 +34,8 @@ export 'package:holzleitner_api/src/model/car.dart'; export 'package:holzleitner_api/src/model/car_response.dart'; export 'package:holzleitner_api/src/model/cars_list.dart'; export 'package:holzleitner_api/src/model/complete_delivery_acknowledgements.dart'; +export 'package:holzleitner_api/src/model/completed_deliveries_response.dart'; +export 'package:holzleitner_api/src/model/completed_delivery_item.dart'; export 'package:holzleitner_api/src/model/contact_channel.dart'; export 'package:holzleitner_api/src/model/contact_kind.dart'; export 'package:holzleitner_api/src/model/contact_role.dart'; @@ -50,10 +52,13 @@ export 'package:holzleitner_api/src/model/delivery.dart'; export 'package:holzleitner_api/src/model/delivery_credit.dart'; export 'package:holzleitner_api/src/model/delivery_credit_event_request.dart'; export 'package:holzleitner_api/src/model/delivery_credit_response.dart'; +export 'package:holzleitner_api/src/model/delivery_details.dart'; export 'package:holzleitner_api/src/model/delivery_item.dart'; export 'package:holzleitner_api/src/model/delivery_note.dart'; export 'package:holzleitner_api/src/model/delivery_note_response.dart'; export 'package:holzleitner_api/src/model/delivery_order_entry.dart'; +export 'package:holzleitner_api/src/model/delivery_payment.dart'; +export 'package:holzleitner_api/src/model/delivery_payment_response.dart'; export 'package:holzleitner_api/src/model/delivery_response.dart'; export 'package:holzleitner_api/src/model/delivery_service_response.dart'; export 'package:holzleitner_api/src/model/delivery_service_value.dart'; @@ -66,6 +71,11 @@ export 'package:holzleitner_api/src/model/mark_mail_sent_response.dart'; export 'package:holzleitner_api/src/model/payment_method.dart'; export 'package:holzleitner_api/src/model/payment_method_response.dart'; export 'package:holzleitner_api/src/model/payment_methods_list.dart'; +export 'package:holzleitner_api/src/model/pending_review_response.dart'; +export 'package:holzleitner_api/src/model/positions_modified_response.dart'; +export 'package:holzleitner_api/src/model/record_delivery_payment_request.dart'; +export 'package:holzleitner_api/src/model/resolve_review_request.dart'; +export 'package:holzleitner_api/src/model/reviewed_item_response.dart'; export 'package:holzleitner_api/src/model/scan_event.dart'; export 'package:holzleitner_api/src/model/scan_result.dart'; export 'package:holzleitner_api/src/model/scan_result_status.dart'; diff --git a/packages/holzleitner_api/lib/src/api/admin_api.dart b/packages/holzleitner_api/lib/src/api/admin_api.dart index 3980f7d..2cb0486 100644 --- a/packages/holzleitner_api/lib/src/api/admin_api.dart +++ b/packages/holzleitner_api/lib/src/api/admin_api.dart @@ -8,11 +8,17 @@ import 'package:built_value/json_object.dart'; import 'package:built_value/serializer.dart'; import 'package:dio/dio.dart'; +import 'package:built_collection/built_collection.dart'; import 'package:holzleitner_api/src/api_util.dart'; +import 'package:holzleitner_api/src/model/completed_deliveries_response.dart'; import 'package:holzleitner_api/src/model/delivered_belegnummern_response.dart'; +import 'package:holzleitner_api/src/model/delivery_details.dart'; import 'package:holzleitner_api/src/model/import_summary.dart'; import 'package:holzleitner_api/src/model/mark_mail_sent_request.dart'; import 'package:holzleitner_api/src/model/mark_mail_sent_response.dart'; +import 'package:holzleitner_api/src/model/pending_review_response.dart'; +import 'package:holzleitner_api/src/model/positions_modified_response.dart'; +import 'package:holzleitner_api/src/model/resolve_review_request.dart'; class AdminApi { @@ -22,6 +28,181 @@ class AdminApi { const AdminApi(this._dio, this._serializers); + /// Liefert das **volle Detail-Paket** zu einer ERP-Belegnummer: die Lieferung mit Positionen (inkl. Scan-Ständen), Kunde + Ansprechpartner, referenzierte Artikel/Lager, Notizen, Geld-Gutschrift, Dienstleistungswerte und Kontakte — destilliert aus dem Tour-Aggregat. Jede Lieferung, unabhängig vom Status. `404`, wenn die Belegnummer unbekannt ist. + /// + /// + /// Parameters: + /// * [belegnummer] - ERP-Belegnummer, z. B. V-30690291 + /// * [cancelToken] - A [CancelToken] that can be used to cancel the operation + /// * [headers] - Can be used to add additional headers to the request + /// * [extras] - Can be used to add flags to the request + /// * [validateStatus] - A [ValidateStatus] callback that can be used to determine request success based on the HTTP status of the response + /// * [onSendProgress] - A [ProgressCallback] that can be used to get the send progress + /// * [onReceiveProgress] - A [ProgressCallback] that can be used to get the receive progress + /// + /// Returns a [Future] containing a [Response] with a [DeliveryDetails] as data + /// Throws [DioException] if API call or serialization fails + Future> belegeDetails({ + required String belegnummer, + CancelToken? cancelToken, + Map? headers, + Map? extra, + ValidateStatus? validateStatus, + ProgressCallback? onSendProgress, + ProgressCallback? onReceiveProgress, + }) async { + final _path = r'/admin/belege/{belegnummer}'.replaceAll('{' r'belegnummer' '}', encodeQueryParameter(_serializers, belegnummer, const FullType(String)).toString()); + final _options = Options( + method: r'GET', + headers: { + ...?headers, + }, + extra: { + 'secure': >[ + { + 'type': 'apiKey', + 'name': 'admin_api_key', + 'keyName': 'X-Admin-Api-Key', + 'where': 'header', + }, + ], + ...?extra, + }, + validateStatus: validateStatus, + ); + + final _response = await _dio.request( + _path, + options: _options, + cancelToken: cancelToken, + onSendProgress: onSendProgress, + onReceiveProgress: onReceiveProgress, + ); + + DeliveryDetails? _responseData; + + try { + final rawResponse = _response.data; + _responseData = rawResponse == null ? null : _serializers.deserialize( + rawResponse, + specifiedType: const FullType(DeliveryDetails), + ) as DeliveryDetails; + + } catch (error, stackTrace) { + throw DioException( + requestOptions: _response.requestOptions, + response: _response, + type: DioExceptionType.unknown, + error: error, + stackTrace: stackTrace, + ); + } + + return Response( + data: _responseData, + headers: _response.headers, + isRedirect: _response.isRedirect, + requestOptions: _response.requestOptions, + redirects: _response.redirects, + statusCode: _response.statusCode, + statusMessage: _response.statusMessage, + extra: _response.extra, + ); + } + + /// Liefert **alle** abgeschlossenen (ausgelieferten) Lieferungen — optional auf einen Datumsbereich eingegrenzt, unabhängig vom Mail-Versand-Status. Gefiltert wird über den **Berliner** Kalendertag des Abschluss-Zeitpunkts (`completed_at`). + /// Parameter (alle `DD-MM-YYYY`, alle optional): `day` = Einzeltag (Kurzform `from=to=day`), sonst `from`/`to` als **inklusive** Bereichsgrenzen (je offen). **Ohne jeden Parameter → ALLE ausgelieferten Belege** (kein Datumsfilter). Pro Lieferung: Belegnummer + `positions_modified` (Menge reduziert/Zeile entfernt oder Geld-Gutschrift). Die Halb-Grenzen-Variante ist für Range-Filter gedacht: `?from=…` und `?to=…` liefern je eine Menge, deren SQL-`AND`-Schnitt den Zeitraum ergibt. + /// + /// Parameters: + /// * [day] - Einzeltag DD-MM-YYYY (Kurzform from=to) + /// * [from] - Untere Grenze DD-MM-YYYY (inklusive) + /// * [to] - Obere Grenze DD-MM-YYYY (inklusive) + /// * [cancelToken] - A [CancelToken] that can be used to cancel the operation + /// * [headers] - Can be used to add additional headers to the request + /// * [extras] - Can be used to add flags to the request + /// * [validateStatus] - A [ValidateStatus] callback that can be used to determine request success based on the HTTP status of the response + /// * [onSendProgress] - A [ProgressCallback] that can be used to get the send progress + /// * [onReceiveProgress] - A [ProgressCallback] that can be used to get the receive progress + /// + /// Returns a [Future] containing a [Response] with a [CompletedDeliveriesResponse] as data + /// Throws [DioException] if API call or serialization fails + Future> completedDeliveries({ + String? day, + String? from, + String? to, + CancelToken? cancelToken, + Map? headers, + Map? extra, + ValidateStatus? validateStatus, + ProgressCallback? onSendProgress, + ProgressCallback? onReceiveProgress, + }) async { + final _path = r'/admin/completed-deliveries'; + final _options = Options( + method: r'GET', + headers: { + ...?headers, + }, + extra: { + 'secure': >[ + { + 'type': 'apiKey', + 'name': 'admin_api_key', + 'keyName': 'X-Admin-Api-Key', + 'where': 'header', + }, + ], + ...?extra, + }, + validateStatus: validateStatus, + ); + + final _queryParameters = { + if (day != null) r'day': encodeQueryParameter(_serializers, day, const FullType(String)), + if (from != null) r'from': encodeQueryParameter(_serializers, from, const FullType(String)), + if (to != null) r'to': encodeQueryParameter(_serializers, to, const FullType(String)), + }; + + final _response = await _dio.request( + _path, + options: _options, + queryParameters: _queryParameters, + cancelToken: cancelToken, + onSendProgress: onSendProgress, + onReceiveProgress: onReceiveProgress, + ); + + CompletedDeliveriesResponse? _responseData; + + try { + final rawResponse = _response.data; + _responseData = rawResponse == null ? null : _serializers.deserialize( + rawResponse, + specifiedType: const FullType(CompletedDeliveriesResponse), + ) as CompletedDeliveriesResponse; + + } catch (error, stackTrace) { + throw DioException( + requestOptions: _response.requestOptions, + response: _response, + type: DioExceptionType.unknown, + error: error, + stackTrace: stackTrace, + ); + } + + return Response( + data: _responseData, + headers: _response.headers, + isRedirect: _response.isRedirect, + requestOptions: _response.requestOptions, + redirects: _response.redirects, + statusCode: _response.statusCode, + statusMessage: _response.statusMessage, + extra: _response.extra, + ); + } + /// Liefert die Belegnummern ausgelieferter (abgeschlossener) Lieferungen, **deren Liefermail noch nicht versendet wurde** (`mail_sent_at IS NULL`). „Ausgeliefert\" = es existiert ein Abschluss. Mit `day` (DD-MM-YYYY) nur Abschlüsse dieses Berliner Kalendertages; **ohne `day` alle offenen** (über alle Tage) — so bleiben Belege über Mitternacht nicht hängen. /// /// @@ -196,6 +377,86 @@ class AdminApi { ); } + /// Listet alle geänderten Lieferscheine, die noch auf eine manuelle Bestätigung (Vier-Augen) warten — Entfernungen und/oder Geld-Gutschriften. + /// + /// + /// Parameters: + /// * [cancelToken] - A [CancelToken] that can be used to cancel the operation + /// * [headers] - Can be used to add additional headers to the request + /// * [extras] - Can be used to add flags to the request + /// * [validateStatus] - A [ValidateStatus] callback that can be used to determine request success based on the HTTP status of the response + /// * [onSendProgress] - A [ProgressCallback] that can be used to get the send progress + /// * [onReceiveProgress] - A [ProgressCallback] that can be used to get the receive progress + /// + /// Returns a [Future] containing a [Response] with a [BuiltList] as data + /// Throws [DioException] if API call or serialization fails + Future>> listReviews({ + CancelToken? cancelToken, + Map? headers, + Map? extra, + ValidateStatus? validateStatus, + ProgressCallback? onSendProgress, + ProgressCallback? onReceiveProgress, + }) async { + final _path = r'/admin/reviews'; + final _options = Options( + method: r'GET', + headers: { + ...?headers, + }, + extra: { + 'secure': >[ + { + 'type': 'apiKey', + 'name': 'admin_api_key', + 'keyName': 'X-Admin-Api-Key', + 'where': 'header', + }, + ], + ...?extra, + }, + validateStatus: validateStatus, + ); + + final _response = await _dio.request( + _path, + options: _options, + cancelToken: cancelToken, + onSendProgress: onSendProgress, + onReceiveProgress: onReceiveProgress, + ); + + BuiltList? _responseData; + + try { + final rawResponse = _response.data; + _responseData = rawResponse == null ? null : _serializers.deserialize( + rawResponse, + specifiedType: const FullType(BuiltList, [FullType(PendingReviewResponse)]), + ) as BuiltList; + + } catch (error, stackTrace) { + throw DioException( + requestOptions: _response.requestOptions, + response: _response, + type: DioExceptionType.unknown, + error: error, + stackTrace: stackTrace, + ); + } + + return Response>( + data: _responseData, + headers: _response.headers, + isRedirect: _response.isRedirect, + requestOptions: _response.requestOptions, + redirects: _response.redirects, + statusCode: _response.statusCode, + statusMessage: _response.statusMessage, + extra: _response.extra, + ); + } + /// Markiert die Liefermails der angegebenen Belegnummern als **versendet** (`mail_sent_at = now()`, nur wo noch offen). Vom Mailclient aufzurufen, NACHDEM ERPframe die Mails erfolgreich verschickt hat — danach erscheinen die Belege nicht mehr in `GET /admin/delivered-belegnummern`. /// /// @@ -298,6 +559,88 @@ class AdminApi { ); } + /// Liefert zu **einer** ERP-Belegnummer, ob an der Lieferung Positionen verändert wurden (Menge reduziert/Zeile entfernt oder Geld-Gutschrift) — unabhängig vom Zustand der Lieferung. `404`, wenn die Belegnummer unbekannt ist. Gibt es mehrere Lieferungen mit derselben Belegnummer, ist das Flag `true`, sobald **eine** davon verändert ist. + /// + /// + /// Parameters: + /// * [belegnummer] - ERP-Belegnummer, z. B. V-30690291 + /// * [cancelToken] - A [CancelToken] that can be used to cancel the operation + /// * [headers] - Can be used to add additional headers to the request + /// * [extras] - Can be used to add flags to the request + /// * [validateStatus] - A [ValidateStatus] callback that can be used to determine request success based on the HTTP status of the response + /// * [onSendProgress] - A [ProgressCallback] that can be used to get the send progress + /// * [onReceiveProgress] - A [ProgressCallback] that can be used to get the receive progress + /// + /// Returns a [Future] containing a [Response] with a [PositionsModifiedResponse] as data + /// Throws [DioException] if API call or serialization fails + Future> positionsModified({ + required String belegnummer, + CancelToken? cancelToken, + Map? headers, + Map? extra, + ValidateStatus? validateStatus, + ProgressCallback? onSendProgress, + ProgressCallback? onReceiveProgress, + }) async { + final _path = r'/admin/belege/{belegnummer}/positions-modified'.replaceAll('{' r'belegnummer' '}', encodeQueryParameter(_serializers, belegnummer, const FullType(String)).toString()); + final _options = Options( + method: r'GET', + headers: { + ...?headers, + }, + extra: { + 'secure': >[ + { + 'type': 'apiKey', + 'name': 'admin_api_key', + 'keyName': 'X-Admin-Api-Key', + 'where': 'header', + }, + ], + ...?extra, + }, + validateStatus: validateStatus, + ); + + final _response = await _dio.request( + _path, + options: _options, + cancelToken: cancelToken, + onSendProgress: onSendProgress, + onReceiveProgress: onReceiveProgress, + ); + + PositionsModifiedResponse? _responseData; + + try { + final rawResponse = _response.data; + _responseData = rawResponse == null ? null : _serializers.deserialize( + rawResponse, + specifiedType: const FullType(PositionsModifiedResponse), + ) as PositionsModifiedResponse; + + } catch (error, stackTrace) { + throw DioException( + requestOptions: _response.requestOptions, + response: _response, + type: DioExceptionType.unknown, + error: error, + stackTrace: stackTrace, + ); + } + + return Response( + data: _responseData, + headers: _response.headers, + isRedirect: _response.isRedirect, + requestOptions: _response.requestOptions, + redirects: _response.redirects, + statusCode: _response.statusCode, + statusMessage: _response.statusMessage, + extra: _response.extra, + ); + } + /// Stößt das ERP-Rückschreiben eines bereits lokal abgeschlossenen Lieferabschlusses erneut an (idempotenter Retry, falls der automatische Push beim Abschluss fehlschlug). /// /// @@ -357,4 +700,80 @@ class AdminApi { return _response; } + /// Bestätigt die Prüfung einer geänderten Lieferung (Vier-Augen) — die Lieferung verschwindet danach aus `GET /admin/reviews` (sofern nicht erneut geändert). + /// + /// + /// Parameters: + /// * [deliveryId] - UUID der Lieferung + /// * [resolveReviewRequest] + /// * [cancelToken] - A [CancelToken] that can be used to cancel the operation + /// * [headers] - Can be used to add additional headers to the request + /// * [extras] - Can be used to add flags to the request + /// * [validateStatus] - A [ValidateStatus] callback that can be used to determine request success based on the HTTP status of the response + /// * [onSendProgress] - A [ProgressCallback] that can be used to get the send progress + /// * [onReceiveProgress] - A [ProgressCallback] that can be used to get the receive progress + /// + /// Returns a [Future] + /// Throws [DioException] if API call or serialization fails + Future> resolveReview({ + required String deliveryId, + required ResolveReviewRequest resolveReviewRequest, + CancelToken? cancelToken, + Map? headers, + Map? extra, + ValidateStatus? validateStatus, + ProgressCallback? onSendProgress, + ProgressCallback? onReceiveProgress, + }) async { + final _path = r'/admin/reviews/{delivery_id}/resolve'.replaceAll('{' r'delivery_id' '}', encodeQueryParameter(_serializers, deliveryId, const FullType(String)).toString()); + final _options = Options( + method: r'POST', + headers: { + ...?headers, + }, + extra: { + 'secure': >[ + { + 'type': 'apiKey', + 'name': 'admin_api_key', + 'keyName': 'X-Admin-Api-Key', + 'where': 'header', + }, + ], + ...?extra, + }, + contentType: 'application/json', + validateStatus: validateStatus, + ); + + dynamic _bodyData; + + try { + const _type = FullType(ResolveReviewRequest); + _bodyData = _serializers.serialize(resolveReviewRequest, specifiedType: _type); + + } catch(error, stackTrace) { + throw DioException( + requestOptions: _options.compose( + _dio.options, + _path, + ), + type: DioExceptionType.unknown, + error: error, + stackTrace: stackTrace, + ); + } + + final _response = await _dio.request( + _path, + data: _bodyData, + options: _options, + cancelToken: cancelToken, + onSendProgress: onSendProgress, + onReceiveProgress: onReceiveProgress, + ); + + return _response; + } + } diff --git a/packages/holzleitner_api/lib/src/api/deliveries_api.dart b/packages/holzleitner_api/lib/src/api/deliveries_api.dart index e8ed3d7..5d09e0f 100644 --- a/packages/holzleitner_api/lib/src/api/deliveries_api.dart +++ b/packages/holzleitner_api/lib/src/api/deliveries_api.dart @@ -15,9 +15,11 @@ import 'package:holzleitner_api/src/model/create_delivery_note_request.dart'; import 'package:holzleitner_api/src/model/delivery_credit_event_request.dart'; import 'package:holzleitner_api/src/model/delivery_credit_response.dart'; import 'package:holzleitner_api/src/model/delivery_note_response.dart'; +import 'package:holzleitner_api/src/model/delivery_payment_response.dart'; import 'package:holzleitner_api/src/model/delivery_response.dart'; import 'package:holzleitner_api/src/model/delivery_service_response.dart'; import 'package:holzleitner_api/src/model/hold_delivery_request.dart'; +import 'package:holzleitner_api/src/model/record_delivery_payment_request.dart'; import 'package:holzleitner_api/src/model/set_delivery_service_request.dart'; import 'package:holzleitner_api/src/model/update_delivery_note_request.dart'; @@ -736,6 +738,109 @@ class DeliveriesApi { ); } + /// Protokolliert die Zahlungsabwicklung („Abkassieren\") einer Lieferung. Append-only, idempotent über `clientEventId`. Der Server berechnet den offenen Betrag selbst; er muss > 0 sein und `expectedAmountCents` entsprechen. Nur bei aktiver Lieferung und aktiver Zahlungsmethode. + /// + /// + /// Parameters: + /// * [deliveryId] + /// * [recordDeliveryPaymentRequest] + /// * [cancelToken] - A [CancelToken] that can be used to cancel the operation + /// * [headers] - Can be used to add additional headers to the request + /// * [extras] - Can be used to add flags to the request + /// * [validateStatus] - A [ValidateStatus] callback that can be used to determine request success based on the HTTP status of the response + /// * [onSendProgress] - A [ProgressCallback] that can be used to get the send progress + /// * [onReceiveProgress] - A [ProgressCallback] that can be used to get the receive progress + /// + /// Returns a [Future] containing a [Response] with a [DeliveryPaymentResponse] as data + /// Throws [DioException] if API call or serialization fails + Future> recordPayment({ + required String deliveryId, + required RecordDeliveryPaymentRequest recordDeliveryPaymentRequest, + CancelToken? cancelToken, + Map? headers, + Map? extra, + ValidateStatus? validateStatus, + ProgressCallback? onSendProgress, + ProgressCallback? onReceiveProgress, + }) async { + final _path = r'/deliveries/{delivery_id}/payment'.replaceAll('{' r'delivery_id' '}', encodeQueryParameter(_serializers, deliveryId, const FullType(String)).toString()); + final _options = Options( + method: r'POST', + headers: { + ...?headers, + }, + extra: { + 'secure': >[ + { + 'type': 'http', + 'scheme': 'bearer', + 'name': 'bearer_auth', + }, + ], + ...?extra, + }, + contentType: 'application/json', + validateStatus: validateStatus, + ); + + dynamic _bodyData; + + try { + const _type = FullType(RecordDeliveryPaymentRequest); + _bodyData = _serializers.serialize(recordDeliveryPaymentRequest, specifiedType: _type); + + } catch(error, stackTrace) { + throw DioException( + requestOptions: _options.compose( + _dio.options, + _path, + ), + type: DioExceptionType.unknown, + error: error, + stackTrace: stackTrace, + ); + } + + final _response = await _dio.request( + _path, + data: _bodyData, + options: _options, + cancelToken: cancelToken, + onSendProgress: onSendProgress, + onReceiveProgress: onReceiveProgress, + ); + + DeliveryPaymentResponse? _responseData; + + try { + final rawResponse = _response.data; + _responseData = rawResponse == null ? null : _serializers.deserialize( + rawResponse, + specifiedType: const FullType(DeliveryPaymentResponse), + ) as DeliveryPaymentResponse; + + } catch (error, stackTrace) { + throw DioException( + requestOptions: _response.requestOptions, + response: _response, + type: DioExceptionType.unknown, + error: error, + stackTrace: stackTrace, + ); + } + + return Response( + data: _responseData, + headers: _response.headers, + isRedirect: _response.isRedirect, + requestOptions: _response.requestOptions, + redirects: _response.redirects, + statusCode: _response.statusCode, + statusMessage: _response.statusMessage, + extra: _response.extra, + ); + } + /// Setzt die Lieferung zurück auf `active`. Nur aus `held` zulässig. /// /// diff --git a/packages/holzleitner_api/lib/src/api/tours_api.dart b/packages/holzleitner_api/lib/src/api/tours_api.dart index 8a9deba..736c602 100644 --- a/packages/holzleitner_api/lib/src/api/tours_api.dart +++ b/packages/holzleitner_api/lib/src/api/tours_api.dart @@ -103,11 +103,11 @@ class ToursApi { ); } - /// Listet die Touren des angemeldeten Fahrers (Filter aus dem JWT) für ein - /// Ziel-Datum. Ohne [date] gilt serverseitig „heute". + /// Listet die Touren des angemeldeten Fahrers (Filter aus dem JWT) für ein Ziel-Datum. Ohne `?date=` gilt „heute\". Der Fahrer wählt das Datum aktiv im Kopf-Kalender der App (z. B. um die Tour von morgen vorab zu sehen). + /// /// /// Parameters: - /// * [date] - Ziel-Tourdatum im Format YYYY-MM-DD (Default: heute) + /// * [date] - Ziel-Tourdatum YYYY-MM-DD (Default: heute) /// * [cancelToken] - A [CancelToken] that can be used to cancel the operation /// * [headers] - Can be used to add additional headers to the request /// * [extras] - Can be used to add flags to the request @@ -117,7 +117,7 @@ class ToursApi { /// /// Returns a [Future] containing a [Response] with a [TourSummaryList] as data /// Throws [DioException] if API call or serialization fails - Future> listMyTours({ + Future> listMyTours({ String? date, CancelToken? cancelToken, Map? headers, @@ -146,7 +146,7 @@ class ToursApi { ); final _queryParameters = { - if (date != null) r'date': date, + if (date != null) r'date': encodeQueryParameter(_serializers, date, const FullType(String)), }; final _response = await _dio.request( diff --git a/packages/holzleitner_api/lib/src/model/completed_deliveries_response.dart b/packages/holzleitner_api/lib/src/model/completed_deliveries_response.dart new file mode 100644 index 0000000..ea24326 --- /dev/null +++ b/packages/holzleitner_api/lib/src/model/completed_deliveries_response.dart @@ -0,0 +1,166 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// + +// ignore_for_file: unused_element +import 'package:holzleitner_api/src/model/completed_delivery_item.dart'; +import 'package:built_collection/built_collection.dart'; +import 'package:built_value/built_value.dart'; +import 'package:built_value/serializer.dart'; + +part 'completed_deliveries_response.g.dart'; + +/// CompletedDeliveriesResponse +/// +/// Properties: +/// * [count] - Anzahl der abgeschlossenen Lieferungen im Bereich. +/// * [deliveries] - Die abgeschlossenen Lieferungen, aufsteigend nach Abschluss-Zeitpunkt. +/// * [from] - Wirksame untere Grenze (ISO `YYYY-MM-DD`) oder `null` (offen). +/// * [to] - Wirksame obere Grenze (ISO `YYYY-MM-DD`) oder `null` (offen). +@BuiltValue() +abstract class CompletedDeliveriesResponse implements Built { + /// Anzahl der abgeschlossenen Lieferungen im Bereich. + @BuiltValueField(wireName: r'count') + int get count; + + /// Die abgeschlossenen Lieferungen, aufsteigend nach Abschluss-Zeitpunkt. + @BuiltValueField(wireName: r'deliveries') + BuiltList get deliveries; + + /// Wirksame untere Grenze (ISO `YYYY-MM-DD`) oder `null` (offen). + @BuiltValueField(wireName: r'from') + String? get from; + + /// Wirksame obere Grenze (ISO `YYYY-MM-DD`) oder `null` (offen). + @BuiltValueField(wireName: r'to') + String? get to; + + CompletedDeliveriesResponse._(); + + factory CompletedDeliveriesResponse([void updates(CompletedDeliveriesResponseBuilder b)]) = _$CompletedDeliveriesResponse; + + @BuiltValueHook(initializeBuilder: true) + static void _defaults(CompletedDeliveriesResponseBuilder b) => b; + + @BuiltValueSerializer(custom: true) + static Serializer get serializer => _$CompletedDeliveriesResponseSerializer(); +} + +class _$CompletedDeliveriesResponseSerializer implements PrimitiveSerializer { + @override + final Iterable types = const [CompletedDeliveriesResponse, _$CompletedDeliveriesResponse]; + + @override + final String wireName = r'CompletedDeliveriesResponse'; + + Iterable _serializeProperties( + Serializers serializers, + CompletedDeliveriesResponse object, { + FullType specifiedType = FullType.unspecified, + }) sync* { + yield r'count'; + yield serializers.serialize( + object.count, + specifiedType: const FullType(int), + ); + yield r'deliveries'; + yield serializers.serialize( + object.deliveries, + specifiedType: const FullType(BuiltList, [FullType(CompletedDeliveryItem)]), + ); + if (object.from != null) { + yield r'from'; + yield serializers.serialize( + object.from, + specifiedType: const FullType.nullable(String), + ); + } + if (object.to != null) { + yield r'to'; + yield serializers.serialize( + object.to, + specifiedType: const FullType.nullable(String), + ); + } + } + + @override + Object serialize( + Serializers serializers, + CompletedDeliveriesResponse object, { + FullType specifiedType = FullType.unspecified, + }) { + return _serializeProperties(serializers, object, specifiedType: specifiedType).toList(); + } + + void _deserializeProperties( + Serializers serializers, + Object serialized, { + FullType specifiedType = FullType.unspecified, + required List serializedList, + required CompletedDeliveriesResponseBuilder result, + required List unhandled, + }) { + for (var i = 0; i < serializedList.length; i += 2) { + final key = serializedList[i] as String; + final value = serializedList[i + 1]; + switch (key) { + case r'count': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType(int), + ) as int; + result.count = valueDes; + break; + case r'deliveries': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType(BuiltList, [FullType(CompletedDeliveryItem)]), + ) as BuiltList; + result.deliveries.replace(valueDes); + break; + case r'from': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType.nullable(String), + ) as String?; + if (valueDes == null) continue; + result.from = valueDes; + break; + case r'to': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType.nullable(String), + ) as String?; + if (valueDes == null) continue; + result.to = valueDes; + break; + default: + unhandled.add(key); + unhandled.add(value); + break; + } + } + } + + @override + CompletedDeliveriesResponse deserialize( + Serializers serializers, + Object serialized, { + FullType specifiedType = FullType.unspecified, + }) { + final result = CompletedDeliveriesResponseBuilder(); + final serializedList = (serialized as Iterable).toList(); + final unhandled = []; + _deserializeProperties( + serializers, + serialized, + specifiedType: specifiedType, + serializedList: serializedList, + unhandled: unhandled, + result: result, + ); + return result.build(); + } +} + diff --git a/packages/holzleitner_api/lib/src/model/completed_deliveries_response.g.dart b/packages/holzleitner_api/lib/src/model/completed_deliveries_response.g.dart new file mode 100644 index 0000000..227ce14 --- /dev/null +++ b/packages/holzleitner_api/lib/src/model/completed_deliveries_response.g.dart @@ -0,0 +1,147 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'completed_deliveries_response.dart'; + +// ************************************************************************** +// BuiltValueGenerator +// ************************************************************************** + +class _$CompletedDeliveriesResponse extends CompletedDeliveriesResponse { + @override + final int count; + @override + final BuiltList deliveries; + @override + final String? from; + @override + final String? to; + + factory _$CompletedDeliveriesResponse( + [void Function(CompletedDeliveriesResponseBuilder)? updates]) => + (CompletedDeliveriesResponseBuilder()..update(updates))._build(); + + _$CompletedDeliveriesResponse._( + {required this.count, required this.deliveries, this.from, this.to}) + : super._(); + @override + CompletedDeliveriesResponse rebuild( + void Function(CompletedDeliveriesResponseBuilder) updates) => + (toBuilder()..update(updates)).build(); + + @override + CompletedDeliveriesResponseBuilder toBuilder() => + CompletedDeliveriesResponseBuilder()..replace(this); + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + return other is CompletedDeliveriesResponse && + count == other.count && + deliveries == other.deliveries && + from == other.from && + to == other.to; + } + + @override + int get hashCode { + var _$hash = 0; + _$hash = $jc(_$hash, count.hashCode); + _$hash = $jc(_$hash, deliveries.hashCode); + _$hash = $jc(_$hash, from.hashCode); + _$hash = $jc(_$hash, to.hashCode); + _$hash = $jf(_$hash); + return _$hash; + } + + @override + String toString() { + return (newBuiltValueToStringHelper(r'CompletedDeliveriesResponse') + ..add('count', count) + ..add('deliveries', deliveries) + ..add('from', from) + ..add('to', to)) + .toString(); + } +} + +class CompletedDeliveriesResponseBuilder + implements + Builder { + _$CompletedDeliveriesResponse? _$v; + + int? _count; + int? get count => _$this._count; + set count(int? count) => _$this._count = count; + + ListBuilder? _deliveries; + ListBuilder get deliveries => + _$this._deliveries ??= ListBuilder(); + set deliveries(ListBuilder? deliveries) => + _$this._deliveries = deliveries; + + String? _from; + String? get from => _$this._from; + set from(String? from) => _$this._from = from; + + String? _to; + String? get to => _$this._to; + set to(String? to) => _$this._to = to; + + CompletedDeliveriesResponseBuilder() { + CompletedDeliveriesResponse._defaults(this); + } + + CompletedDeliveriesResponseBuilder get _$this { + final $v = _$v; + if ($v != null) { + _count = $v.count; + _deliveries = $v.deliveries.toBuilder(); + _from = $v.from; + _to = $v.to; + _$v = null; + } + return this; + } + + @override + void replace(CompletedDeliveriesResponse other) { + _$v = other as _$CompletedDeliveriesResponse; + } + + @override + void update(void Function(CompletedDeliveriesResponseBuilder)? updates) { + if (updates != null) updates(this); + } + + @override + CompletedDeliveriesResponse build() => _build(); + + _$CompletedDeliveriesResponse _build() { + _$CompletedDeliveriesResponse _$result; + try { + _$result = _$v ?? + _$CompletedDeliveriesResponse._( + count: BuiltValueNullFieldError.checkNotNull( + count, r'CompletedDeliveriesResponse', 'count'), + deliveries: deliveries.build(), + from: from, + to: to, + ); + } catch (_) { + late String _$failedField; + try { + _$failedField = 'deliveries'; + deliveries.build(); + } catch (e) { + throw BuiltValueNestedFieldError( + r'CompletedDeliveriesResponse', _$failedField, e.toString()); + } + rethrow; + } + replace(_$result); + return _$result; + } +} + +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/packages/holzleitner_api/lib/src/model/completed_delivery_item.dart b/packages/holzleitner_api/lib/src/model/completed_delivery_item.dart new file mode 100644 index 0000000..0149b7e --- /dev/null +++ b/packages/holzleitner_api/lib/src/model/completed_delivery_item.dart @@ -0,0 +1,124 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// + +// ignore_for_file: unused_element +import 'package:built_value/built_value.dart'; +import 'package:built_value/serializer.dart'; + +part 'completed_delivery_item.g.dart'; + +/// Eine abgeschlossene Lieferung im Tagesabruf. +/// +/// Properties: +/// * [belegnummer] - ERP-Belegnummer der Lieferung. +/// * [positionsModified] - `true`, wenn an der Lieferung Positionen verändert wurden — eine Zeile wurde entfernt oder in der Menge reduziert (Stück-Gutschrift) **oder** es liegt eine aktive Geld-Gutschrift vor. +@BuiltValue() +abstract class CompletedDeliveryItem implements Built { + /// ERP-Belegnummer der Lieferung. + @BuiltValueField(wireName: r'belegnummer') + String get belegnummer; + + /// `true`, wenn an der Lieferung Positionen verändert wurden — eine Zeile wurde entfernt oder in der Menge reduziert (Stück-Gutschrift) **oder** es liegt eine aktive Geld-Gutschrift vor. + @BuiltValueField(wireName: r'positions_modified') + bool get positionsModified; + + CompletedDeliveryItem._(); + + factory CompletedDeliveryItem([void updates(CompletedDeliveryItemBuilder b)]) = _$CompletedDeliveryItem; + + @BuiltValueHook(initializeBuilder: true) + static void _defaults(CompletedDeliveryItemBuilder b) => b; + + @BuiltValueSerializer(custom: true) + static Serializer get serializer => _$CompletedDeliveryItemSerializer(); +} + +class _$CompletedDeliveryItemSerializer implements PrimitiveSerializer { + @override + final Iterable types = const [CompletedDeliveryItem, _$CompletedDeliveryItem]; + + @override + final String wireName = r'CompletedDeliveryItem'; + + Iterable _serializeProperties( + Serializers serializers, + CompletedDeliveryItem object, { + FullType specifiedType = FullType.unspecified, + }) sync* { + yield r'belegnummer'; + yield serializers.serialize( + object.belegnummer, + specifiedType: const FullType(String), + ); + yield r'positions_modified'; + yield serializers.serialize( + object.positionsModified, + specifiedType: const FullType(bool), + ); + } + + @override + Object serialize( + Serializers serializers, + CompletedDeliveryItem object, { + FullType specifiedType = FullType.unspecified, + }) { + return _serializeProperties(serializers, object, specifiedType: specifiedType).toList(); + } + + void _deserializeProperties( + Serializers serializers, + Object serialized, { + FullType specifiedType = FullType.unspecified, + required List serializedList, + required CompletedDeliveryItemBuilder result, + required List unhandled, + }) { + for (var i = 0; i < serializedList.length; i += 2) { + final key = serializedList[i] as String; + final value = serializedList[i + 1]; + switch (key) { + case r'belegnummer': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType(String), + ) as String; + result.belegnummer = valueDes; + break; + case r'positions_modified': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType(bool), + ) as bool; + result.positionsModified = valueDes; + break; + default: + unhandled.add(key); + unhandled.add(value); + break; + } + } + } + + @override + CompletedDeliveryItem deserialize( + Serializers serializers, + Object serialized, { + FullType specifiedType = FullType.unspecified, + }) { + final result = CompletedDeliveryItemBuilder(); + final serializedList = (serialized as Iterable).toList(); + final unhandled = []; + _deserializeProperties( + serializers, + serialized, + specifiedType: specifiedType, + serializedList: serializedList, + unhandled: unhandled, + result: result, + ); + return result.build(); + } +} + diff --git a/packages/holzleitner_api/lib/src/model/completed_delivery_item.g.dart b/packages/holzleitner_api/lib/src/model/completed_delivery_item.g.dart new file mode 100644 index 0000000..8d88898 --- /dev/null +++ b/packages/holzleitner_api/lib/src/model/completed_delivery_item.g.dart @@ -0,0 +1,110 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'completed_delivery_item.dart'; + +// ************************************************************************** +// BuiltValueGenerator +// ************************************************************************** + +class _$CompletedDeliveryItem extends CompletedDeliveryItem { + @override + final String belegnummer; + @override + final bool positionsModified; + + factory _$CompletedDeliveryItem( + [void Function(CompletedDeliveryItemBuilder)? updates]) => + (CompletedDeliveryItemBuilder()..update(updates))._build(); + + _$CompletedDeliveryItem._( + {required this.belegnummer, required this.positionsModified}) + : super._(); + @override + CompletedDeliveryItem rebuild( + void Function(CompletedDeliveryItemBuilder) updates) => + (toBuilder()..update(updates)).build(); + + @override + CompletedDeliveryItemBuilder toBuilder() => + CompletedDeliveryItemBuilder()..replace(this); + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + return other is CompletedDeliveryItem && + belegnummer == other.belegnummer && + positionsModified == other.positionsModified; + } + + @override + int get hashCode { + var _$hash = 0; + _$hash = $jc(_$hash, belegnummer.hashCode); + _$hash = $jc(_$hash, positionsModified.hashCode); + _$hash = $jf(_$hash); + return _$hash; + } + + @override + String toString() { + return (newBuiltValueToStringHelper(r'CompletedDeliveryItem') + ..add('belegnummer', belegnummer) + ..add('positionsModified', positionsModified)) + .toString(); + } +} + +class CompletedDeliveryItemBuilder + implements Builder { + _$CompletedDeliveryItem? _$v; + + String? _belegnummer; + String? get belegnummer => _$this._belegnummer; + set belegnummer(String? belegnummer) => _$this._belegnummer = belegnummer; + + bool? _positionsModified; + bool? get positionsModified => _$this._positionsModified; + set positionsModified(bool? positionsModified) => + _$this._positionsModified = positionsModified; + + CompletedDeliveryItemBuilder() { + CompletedDeliveryItem._defaults(this); + } + + CompletedDeliveryItemBuilder get _$this { + final $v = _$v; + if ($v != null) { + _belegnummer = $v.belegnummer; + _positionsModified = $v.positionsModified; + _$v = null; + } + return this; + } + + @override + void replace(CompletedDeliveryItem other) { + _$v = other as _$CompletedDeliveryItem; + } + + @override + void update(void Function(CompletedDeliveryItemBuilder)? updates) { + if (updates != null) updates(this); + } + + @override + CompletedDeliveryItem build() => _build(); + + _$CompletedDeliveryItem _build() { + final _$result = _$v ?? + _$CompletedDeliveryItem._( + belegnummer: BuiltValueNullFieldError.checkNotNull( + belegnummer, r'CompletedDeliveryItem', 'belegnummer'), + positionsModified: BuiltValueNullFieldError.checkNotNull( + positionsModified, r'CompletedDeliveryItem', 'positionsModified'), + ); + replace(_$result); + return _$result; + } +} + +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/packages/holzleitner_api/lib/src/model/delivery_details.dart b/packages/holzleitner_api/lib/src/model/delivery_details.dart new file mode 100644 index 0000000..fe8c3ae --- /dev/null +++ b/packages/holzleitner_api/lib/src/model/delivery_details.dart @@ -0,0 +1,334 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// + +// ignore_for_file: unused_element +import 'package:holzleitner_api/src/model/delivery_credit.dart'; +import 'package:holzleitner_api/src/model/delivery_note.dart'; +import 'package:holzleitner_api/src/model/delivery_with_items.dart'; +import 'package:holzleitner_api/src/model/delivery_payment.dart'; +import 'package:holzleitner_api/src/model/article.dart'; +import 'package:holzleitner_api/src/model/contact_channel.dart'; +import 'package:holzleitner_api/src/model/customer.dart'; +import 'package:holzleitner_api/src/model/contact_source.dart'; +import 'package:holzleitner_api/src/model/tour.dart'; +import 'package:holzleitner_api/src/model/warehouse.dart'; +import 'package:built_collection/built_collection.dart'; +import 'package:holzleitner_api/src/model/customer_contact.dart'; +import 'package:holzleitner_api/src/model/service.dart'; +import 'package:holzleitner_api/src/model/delivery_service_value.dart'; +import 'package:built_value/built_value.dart'; +import 'package:built_value/serializer.dart'; + +part 'delivery_details.g.dart'; + +/// DeliveryDetails +/// +/// Properties: +/// * [articles] - Nur die von den Positionen referenzierten Artikel. +/// * [contactChannels] - Die zu `contactSources` gehörenden Einzel-Kanäle. Join per `sourceId`. +/// * [contactSources] - Kontaktquellen der Lieferung (Liefer-/Rechnungsadresse, Ansprechpartner …). +/// * [credit] - Aktuelle Betrags-Gutschrift (`null`, wenn keine aktiv). +/// * [customer] - Der Kunde der Lieferung (`null`, falls unauffindbar — sollte nicht vorkommen). +/// * [customerContacts] - Ansprechpartner des Kunden. +/// * [delivery] - Die Lieferung selbst inkl. Positionen (mit Scan-Ständen) und `sortOrder`. +/// * [deliveryServices] - Für diese Lieferung gesetzte Service-Werte. +/// * [notes] - Notizen der Lieferung, aufsteigend nach `createdAt`. +/// * [payment] - Jüngste protokollierte Zahlungsabwicklung (`None` = keine). +/// * [services] - Aktive Service-Definitionen (Stammdaten). +/// * [tour] - Die Tour, zu der die Lieferung gehört. +/// * [warehouses] - Nur die von den Positionen referenzierten Lager. +@BuiltValue() +abstract class DeliveryDetails implements Built { + /// Nur die von den Positionen referenzierten Artikel. + @BuiltValueField(wireName: r'articles') + BuiltList
get articles; + + /// Die zu `contactSources` gehörenden Einzel-Kanäle. Join per `sourceId`. + @BuiltValueField(wireName: r'contactChannels') + BuiltList get contactChannels; + + /// Kontaktquellen der Lieferung (Liefer-/Rechnungsadresse, Ansprechpartner …). + @BuiltValueField(wireName: r'contactSources') + BuiltList get contactSources; + + /// Aktuelle Betrags-Gutschrift (`null`, wenn keine aktiv). + @BuiltValueField(wireName: r'credit') + DeliveryCredit? get credit; + + /// Der Kunde der Lieferung (`null`, falls unauffindbar — sollte nicht vorkommen). + @BuiltValueField(wireName: r'customer') + Customer? get customer; + + /// Ansprechpartner des Kunden. + @BuiltValueField(wireName: r'customerContacts') + BuiltList get customerContacts; + + /// Die Lieferung selbst inkl. Positionen (mit Scan-Ständen) und `sortOrder`. + @BuiltValueField(wireName: r'delivery') + DeliveryWithItems get delivery; + + /// Für diese Lieferung gesetzte Service-Werte. + @BuiltValueField(wireName: r'deliveryServices') + BuiltList get deliveryServices; + + /// Notizen der Lieferung, aufsteigend nach `createdAt`. + @BuiltValueField(wireName: r'notes') + BuiltList get notes; + + /// Jüngste protokollierte Zahlungsabwicklung (`None` = keine). + @BuiltValueField(wireName: r'payment') + DeliveryPayment? get payment; + + /// Aktive Service-Definitionen (Stammdaten). + @BuiltValueField(wireName: r'services') + BuiltList get services; + + /// Die Tour, zu der die Lieferung gehört. + @BuiltValueField(wireName: r'tour') + Tour get tour; + + /// Nur die von den Positionen referenzierten Lager. + @BuiltValueField(wireName: r'warehouses') + BuiltList get warehouses; + + DeliveryDetails._(); + + factory DeliveryDetails([void updates(DeliveryDetailsBuilder b)]) = _$DeliveryDetails; + + @BuiltValueHook(initializeBuilder: true) + static void _defaults(DeliveryDetailsBuilder b) => b; + + @BuiltValueSerializer(custom: true) + static Serializer get serializer => _$DeliveryDetailsSerializer(); +} + +class _$DeliveryDetailsSerializer implements PrimitiveSerializer { + @override + final Iterable types = const [DeliveryDetails, _$DeliveryDetails]; + + @override + final String wireName = r'DeliveryDetails'; + + Iterable _serializeProperties( + Serializers serializers, + DeliveryDetails object, { + FullType specifiedType = FullType.unspecified, + }) sync* { + yield r'articles'; + yield serializers.serialize( + object.articles, + specifiedType: const FullType(BuiltList, [FullType(Article)]), + ); + yield r'contactChannels'; + yield serializers.serialize( + object.contactChannels, + specifiedType: const FullType(BuiltList, [FullType(ContactChannel)]), + ); + yield r'contactSources'; + yield serializers.serialize( + object.contactSources, + specifiedType: const FullType(BuiltList, [FullType(ContactSource)]), + ); + if (object.credit != null) { + yield r'credit'; + yield serializers.serialize( + object.credit, + specifiedType: const FullType.nullable(DeliveryCredit), + ); + } + if (object.customer != null) { + yield r'customer'; + yield serializers.serialize( + object.customer, + specifiedType: const FullType.nullable(Customer), + ); + } + yield r'customerContacts'; + yield serializers.serialize( + object.customerContacts, + specifiedType: const FullType(BuiltList, [FullType(CustomerContact)]), + ); + yield r'delivery'; + yield serializers.serialize( + object.delivery, + specifiedType: const FullType(DeliveryWithItems), + ); + yield r'deliveryServices'; + yield serializers.serialize( + object.deliveryServices, + specifiedType: const FullType(BuiltList, [FullType(DeliveryServiceValue)]), + ); + yield r'notes'; + yield serializers.serialize( + object.notes, + specifiedType: const FullType(BuiltList, [FullType(DeliveryNote)]), + ); + if (object.payment != null) { + yield r'payment'; + yield serializers.serialize( + object.payment, + specifiedType: const FullType.nullable(DeliveryPayment), + ); + } + yield r'services'; + yield serializers.serialize( + object.services, + specifiedType: const FullType(BuiltList, [FullType(Service)]), + ); + yield r'tour'; + yield serializers.serialize( + object.tour, + specifiedType: const FullType(Tour), + ); + yield r'warehouses'; + yield serializers.serialize( + object.warehouses, + specifiedType: const FullType(BuiltList, [FullType(Warehouse)]), + ); + } + + @override + Object serialize( + Serializers serializers, + DeliveryDetails object, { + FullType specifiedType = FullType.unspecified, + }) { + return _serializeProperties(serializers, object, specifiedType: specifiedType).toList(); + } + + void _deserializeProperties( + Serializers serializers, + Object serialized, { + FullType specifiedType = FullType.unspecified, + required List serializedList, + required DeliveryDetailsBuilder result, + required List unhandled, + }) { + for (var i = 0; i < serializedList.length; i += 2) { + final key = serializedList[i] as String; + final value = serializedList[i + 1]; + switch (key) { + case r'articles': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType(BuiltList, [FullType(Article)]), + ) as BuiltList
; + result.articles.replace(valueDes); + break; + case r'contactChannels': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType(BuiltList, [FullType(ContactChannel)]), + ) as BuiltList; + result.contactChannels.replace(valueDes); + break; + case r'contactSources': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType(BuiltList, [FullType(ContactSource)]), + ) as BuiltList; + result.contactSources.replace(valueDes); + break; + case r'credit': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType.nullable(DeliveryCredit), + ) as DeliveryCredit?; + if (valueDes == null) continue; + result.credit.replace(valueDes); + break; + case r'customer': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType.nullable(Customer), + ) as Customer?; + if (valueDes == null) continue; + result.customer.replace(valueDes); + break; + case r'customerContacts': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType(BuiltList, [FullType(CustomerContact)]), + ) as BuiltList; + result.customerContacts.replace(valueDes); + break; + case r'delivery': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType(DeliveryWithItems), + ) as DeliveryWithItems; + result.delivery.replace(valueDes); + break; + case r'deliveryServices': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType(BuiltList, [FullType(DeliveryServiceValue)]), + ) as BuiltList; + result.deliveryServices.replace(valueDes); + break; + case r'notes': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType(BuiltList, [FullType(DeliveryNote)]), + ) as BuiltList; + result.notes.replace(valueDes); + break; + case r'payment': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType.nullable(DeliveryPayment), + ) as DeliveryPayment?; + if (valueDes == null) continue; + result.payment.replace(valueDes); + break; + case r'services': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType(BuiltList, [FullType(Service)]), + ) as BuiltList; + result.services.replace(valueDes); + break; + case r'tour': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType(Tour), + ) as Tour; + result.tour.replace(valueDes); + break; + case r'warehouses': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType(BuiltList, [FullType(Warehouse)]), + ) as BuiltList; + result.warehouses.replace(valueDes); + break; + default: + unhandled.add(key); + unhandled.add(value); + break; + } + } + } + + @override + DeliveryDetails deserialize( + Serializers serializers, + Object serialized, { + FullType specifiedType = FullType.unspecified, + }) { + final result = DeliveryDetailsBuilder(); + final serializedList = (serialized as Iterable).toList(); + final unhandled = []; + _deserializeProperties( + serializers, + serialized, + specifiedType: specifiedType, + serializedList: serializedList, + unhandled: unhandled, + result: result, + ); + return result.build(); + } +} + diff --git a/packages/holzleitner_api/lib/src/model/delivery_details.g.dart b/packages/holzleitner_api/lib/src/model/delivery_details.g.dart new file mode 100644 index 0000000..0474d1a --- /dev/null +++ b/packages/holzleitner_api/lib/src/model/delivery_details.g.dart @@ -0,0 +1,291 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'delivery_details.dart'; + +// ************************************************************************** +// BuiltValueGenerator +// ************************************************************************** + +class _$DeliveryDetails extends DeliveryDetails { + @override + final BuiltList
articles; + @override + final BuiltList contactChannels; + @override + final BuiltList contactSources; + @override + final DeliveryCredit? credit; + @override + final Customer? customer; + @override + final BuiltList customerContacts; + @override + final DeliveryWithItems delivery; + @override + final BuiltList deliveryServices; + @override + final BuiltList notes; + @override + final DeliveryPayment? payment; + @override + final BuiltList services; + @override + final Tour tour; + @override + final BuiltList warehouses; + + factory _$DeliveryDetails([void Function(DeliveryDetailsBuilder)? updates]) => + (DeliveryDetailsBuilder()..update(updates))._build(); + + _$DeliveryDetails._( + {required this.articles, + required this.contactChannels, + required this.contactSources, + this.credit, + this.customer, + required this.customerContacts, + required this.delivery, + required this.deliveryServices, + required this.notes, + this.payment, + required this.services, + required this.tour, + required this.warehouses}) + : super._(); + @override + DeliveryDetails rebuild(void Function(DeliveryDetailsBuilder) updates) => + (toBuilder()..update(updates)).build(); + + @override + DeliveryDetailsBuilder toBuilder() => DeliveryDetailsBuilder()..replace(this); + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + return other is DeliveryDetails && + articles == other.articles && + contactChannels == other.contactChannels && + contactSources == other.contactSources && + credit == other.credit && + customer == other.customer && + customerContacts == other.customerContacts && + delivery == other.delivery && + deliveryServices == other.deliveryServices && + notes == other.notes && + payment == other.payment && + services == other.services && + tour == other.tour && + warehouses == other.warehouses; + } + + @override + int get hashCode { + var _$hash = 0; + _$hash = $jc(_$hash, articles.hashCode); + _$hash = $jc(_$hash, contactChannels.hashCode); + _$hash = $jc(_$hash, contactSources.hashCode); + _$hash = $jc(_$hash, credit.hashCode); + _$hash = $jc(_$hash, customer.hashCode); + _$hash = $jc(_$hash, customerContacts.hashCode); + _$hash = $jc(_$hash, delivery.hashCode); + _$hash = $jc(_$hash, deliveryServices.hashCode); + _$hash = $jc(_$hash, notes.hashCode); + _$hash = $jc(_$hash, payment.hashCode); + _$hash = $jc(_$hash, services.hashCode); + _$hash = $jc(_$hash, tour.hashCode); + _$hash = $jc(_$hash, warehouses.hashCode); + _$hash = $jf(_$hash); + return _$hash; + } + + @override + String toString() { + return (newBuiltValueToStringHelper(r'DeliveryDetails') + ..add('articles', articles) + ..add('contactChannels', contactChannels) + ..add('contactSources', contactSources) + ..add('credit', credit) + ..add('customer', customer) + ..add('customerContacts', customerContacts) + ..add('delivery', delivery) + ..add('deliveryServices', deliveryServices) + ..add('notes', notes) + ..add('payment', payment) + ..add('services', services) + ..add('tour', tour) + ..add('warehouses', warehouses)) + .toString(); + } +} + +class DeliveryDetailsBuilder + implements Builder { + _$DeliveryDetails? _$v; + + ListBuilder
? _articles; + ListBuilder
get articles => + _$this._articles ??= ListBuilder
(); + set articles(ListBuilder
? articles) => _$this._articles = articles; + + ListBuilder? _contactChannels; + ListBuilder get contactChannels => + _$this._contactChannels ??= ListBuilder(); + set contactChannels(ListBuilder? contactChannels) => + _$this._contactChannels = contactChannels; + + ListBuilder? _contactSources; + ListBuilder get contactSources => + _$this._contactSources ??= ListBuilder(); + set contactSources(ListBuilder? contactSources) => + _$this._contactSources = contactSources; + + DeliveryCreditBuilder? _credit; + DeliveryCreditBuilder get credit => + _$this._credit ??= DeliveryCreditBuilder(); + set credit(DeliveryCreditBuilder? credit) => _$this._credit = credit; + + CustomerBuilder? _customer; + CustomerBuilder get customer => _$this._customer ??= CustomerBuilder(); + set customer(CustomerBuilder? customer) => _$this._customer = customer; + + ListBuilder? _customerContacts; + ListBuilder get customerContacts => + _$this._customerContacts ??= ListBuilder(); + set customerContacts(ListBuilder? customerContacts) => + _$this._customerContacts = customerContacts; + + DeliveryWithItemsBuilder? _delivery; + DeliveryWithItemsBuilder get delivery => + _$this._delivery ??= DeliveryWithItemsBuilder(); + set delivery(DeliveryWithItemsBuilder? delivery) => + _$this._delivery = delivery; + + ListBuilder? _deliveryServices; + ListBuilder get deliveryServices => + _$this._deliveryServices ??= ListBuilder(); + set deliveryServices(ListBuilder? deliveryServices) => + _$this._deliveryServices = deliveryServices; + + ListBuilder? _notes; + ListBuilder get notes => + _$this._notes ??= ListBuilder(); + set notes(ListBuilder? notes) => _$this._notes = notes; + + DeliveryPaymentBuilder? _payment; + DeliveryPaymentBuilder get payment => + _$this._payment ??= DeliveryPaymentBuilder(); + set payment(DeliveryPaymentBuilder? payment) => _$this._payment = payment; + + ListBuilder? _services; + ListBuilder get services => + _$this._services ??= ListBuilder(); + set services(ListBuilder? services) => _$this._services = services; + + TourBuilder? _tour; + TourBuilder get tour => _$this._tour ??= TourBuilder(); + set tour(TourBuilder? tour) => _$this._tour = tour; + + ListBuilder? _warehouses; + ListBuilder get warehouses => + _$this._warehouses ??= ListBuilder(); + set warehouses(ListBuilder? warehouses) => + _$this._warehouses = warehouses; + + DeliveryDetailsBuilder() { + DeliveryDetails._defaults(this); + } + + DeliveryDetailsBuilder get _$this { + final $v = _$v; + if ($v != null) { + _articles = $v.articles.toBuilder(); + _contactChannels = $v.contactChannels.toBuilder(); + _contactSources = $v.contactSources.toBuilder(); + _credit = $v.credit?.toBuilder(); + _customer = $v.customer?.toBuilder(); + _customerContacts = $v.customerContacts.toBuilder(); + _delivery = $v.delivery.toBuilder(); + _deliveryServices = $v.deliveryServices.toBuilder(); + _notes = $v.notes.toBuilder(); + _payment = $v.payment?.toBuilder(); + _services = $v.services.toBuilder(); + _tour = $v.tour.toBuilder(); + _warehouses = $v.warehouses.toBuilder(); + _$v = null; + } + return this; + } + + @override + void replace(DeliveryDetails other) { + _$v = other as _$DeliveryDetails; + } + + @override + void update(void Function(DeliveryDetailsBuilder)? updates) { + if (updates != null) updates(this); + } + + @override + DeliveryDetails build() => _build(); + + _$DeliveryDetails _build() { + _$DeliveryDetails _$result; + try { + _$result = _$v ?? + _$DeliveryDetails._( + articles: articles.build(), + contactChannels: contactChannels.build(), + contactSources: contactSources.build(), + credit: _credit?.build(), + customer: _customer?.build(), + customerContacts: customerContacts.build(), + delivery: delivery.build(), + deliveryServices: deliveryServices.build(), + notes: notes.build(), + payment: _payment?.build(), + services: services.build(), + tour: tour.build(), + warehouses: warehouses.build(), + ); + } catch (_) { + late String _$failedField; + try { + _$failedField = 'articles'; + articles.build(); + _$failedField = 'contactChannels'; + contactChannels.build(); + _$failedField = 'contactSources'; + contactSources.build(); + _$failedField = 'credit'; + _credit?.build(); + _$failedField = 'customer'; + _customer?.build(); + _$failedField = 'customerContacts'; + customerContacts.build(); + _$failedField = 'delivery'; + delivery.build(); + _$failedField = 'deliveryServices'; + deliveryServices.build(); + _$failedField = 'notes'; + notes.build(); + _$failedField = 'payment'; + _payment?.build(); + _$failedField = 'services'; + services.build(); + _$failedField = 'tour'; + tour.build(); + _$failedField = 'warehouses'; + warehouses.build(); + } catch (e) { + throw BuiltValueNestedFieldError( + r'DeliveryDetails', _$failedField, e.toString()); + } + rethrow; + } + replace(_$result); + return _$result; + } +} + +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/packages/holzleitner_api/lib/src/model/delivery_payment.dart b/packages/holzleitner_api/lib/src/model/delivery_payment.dart new file mode 100644 index 0000000..82ebdbc --- /dev/null +++ b/packages/holzleitner_api/lib/src/model/delivery_payment.dart @@ -0,0 +1,223 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// + +// ignore_for_file: unused_element +import 'package:built_value/built_value.dart'; +import 'package:built_value/serializer.dart'; + +part 'delivery_payment.g.dart'; + +/// Protokollierte Zahlungsabwicklung („Abkassieren\") einer Lieferung. Append-only: jede Bestätigung des Fahrers ist ein eigener Eintrag; im Tour-Aggregat wird pro Lieferung nur der jüngste mitgeliefert. Der Betrag ist der server-seitig berechnete offene Betrag zum Zeitpunkt der Bestätigung. Weicht der aktuelle offene Betrag davon ab (z. B. nach einer neuen Gutschrift), ist der Eintrag veraltet und muss erneuert werden. +/// +/// Properties: +/// * [amountCents] - Abgewickelter Betrag in Cent (offener Betrag bei Bestätigung). +/// * [deliveryId] +/// * [id] +/// * [paymentMethodCode] - Snapshot des Methoden-Codes (`cash`, `ec_card`, `invoice`, …). +/// * [paymentMethodId] +/// * [recordedAt] +/// * [recordedByCarId] +/// * [recordedByPersonalnummer] +@BuiltValue() +abstract class DeliveryPayment implements Built { + /// Abgewickelter Betrag in Cent (offener Betrag bei Bestätigung). + @BuiltValueField(wireName: r'amountCents') + int get amountCents; + + @BuiltValueField(wireName: r'deliveryId') + String get deliveryId; + + @BuiltValueField(wireName: r'id') + String get id; + + /// Snapshot des Methoden-Codes (`cash`, `ec_card`, `invoice`, …). + @BuiltValueField(wireName: r'paymentMethodCode') + String get paymentMethodCode; + + @BuiltValueField(wireName: r'paymentMethodId') + String get paymentMethodId; + + @BuiltValueField(wireName: r'recordedAt') + DateTime get recordedAt; + + @BuiltValueField(wireName: r'recordedByCarId') + String? get recordedByCarId; + + @BuiltValueField(wireName: r'recordedByPersonalnummer') + int get recordedByPersonalnummer; + + DeliveryPayment._(); + + factory DeliveryPayment([void updates(DeliveryPaymentBuilder b)]) = _$DeliveryPayment; + + @BuiltValueHook(initializeBuilder: true) + static void _defaults(DeliveryPaymentBuilder b) => b; + + @BuiltValueSerializer(custom: true) + static Serializer get serializer => _$DeliveryPaymentSerializer(); +} + +class _$DeliveryPaymentSerializer implements PrimitiveSerializer { + @override + final Iterable types = const [DeliveryPayment, _$DeliveryPayment]; + + @override + final String wireName = r'DeliveryPayment'; + + Iterable _serializeProperties( + Serializers serializers, + DeliveryPayment object, { + FullType specifiedType = FullType.unspecified, + }) sync* { + yield r'amountCents'; + yield serializers.serialize( + object.amountCents, + specifiedType: const FullType(int), + ); + yield r'deliveryId'; + yield serializers.serialize( + object.deliveryId, + specifiedType: const FullType(String), + ); + yield r'id'; + yield serializers.serialize( + object.id, + specifiedType: const FullType(String), + ); + yield r'paymentMethodCode'; + yield serializers.serialize( + object.paymentMethodCode, + specifiedType: const FullType(String), + ); + yield r'paymentMethodId'; + yield serializers.serialize( + object.paymentMethodId, + specifiedType: const FullType(String), + ); + yield r'recordedAt'; + yield serializers.serialize( + object.recordedAt, + specifiedType: const FullType(DateTime), + ); + if (object.recordedByCarId != null) { + yield r'recordedByCarId'; + yield serializers.serialize( + object.recordedByCarId, + specifiedType: const FullType.nullable(String), + ); + } + yield r'recordedByPersonalnummer'; + yield serializers.serialize( + object.recordedByPersonalnummer, + specifiedType: const FullType(int), + ); + } + + @override + Object serialize( + Serializers serializers, + DeliveryPayment object, { + FullType specifiedType = FullType.unspecified, + }) { + return _serializeProperties(serializers, object, specifiedType: specifiedType).toList(); + } + + void _deserializeProperties( + Serializers serializers, + Object serialized, { + FullType specifiedType = FullType.unspecified, + required List serializedList, + required DeliveryPaymentBuilder result, + required List unhandled, + }) { + for (var i = 0; i < serializedList.length; i += 2) { + final key = serializedList[i] as String; + final value = serializedList[i + 1]; + switch (key) { + case r'amountCents': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType(int), + ) as int; + result.amountCents = valueDes; + break; + case r'deliveryId': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType(String), + ) as String; + result.deliveryId = valueDes; + break; + case r'id': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType(String), + ) as String; + result.id = valueDes; + break; + case r'paymentMethodCode': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType(String), + ) as String; + result.paymentMethodCode = valueDes; + break; + case r'paymentMethodId': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType(String), + ) as String; + result.paymentMethodId = valueDes; + break; + case r'recordedAt': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType(DateTime), + ) as DateTime; + result.recordedAt = valueDes; + break; + case r'recordedByCarId': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType.nullable(String), + ) as String?; + if (valueDes == null) continue; + result.recordedByCarId = valueDes; + break; + case r'recordedByPersonalnummer': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType(int), + ) as int; + result.recordedByPersonalnummer = valueDes; + break; + default: + unhandled.add(key); + unhandled.add(value); + break; + } + } + } + + @override + DeliveryPayment deserialize( + Serializers serializers, + Object serialized, { + FullType specifiedType = FullType.unspecified, + }) { + final result = DeliveryPaymentBuilder(); + final serializedList = (serialized as Iterable).toList(); + final unhandled = []; + _deserializeProperties( + serializers, + serialized, + specifiedType: specifiedType, + serializedList: serializedList, + unhandled: unhandled, + result: result, + ); + return result.build(); + } +} + diff --git a/packages/holzleitner_api/lib/src/model/delivery_payment.g.dart b/packages/holzleitner_api/lib/src/model/delivery_payment.g.dart new file mode 100644 index 0000000..14e2219 --- /dev/null +++ b/packages/holzleitner_api/lib/src/model/delivery_payment.g.dart @@ -0,0 +1,190 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'delivery_payment.dart'; + +// ************************************************************************** +// BuiltValueGenerator +// ************************************************************************** + +class _$DeliveryPayment extends DeliveryPayment { + @override + final int amountCents; + @override + final String deliveryId; + @override + final String id; + @override + final String paymentMethodCode; + @override + final String paymentMethodId; + @override + final DateTime recordedAt; + @override + final String? recordedByCarId; + @override + final int recordedByPersonalnummer; + + factory _$DeliveryPayment([void Function(DeliveryPaymentBuilder)? updates]) => + (DeliveryPaymentBuilder()..update(updates))._build(); + + _$DeliveryPayment._( + {required this.amountCents, + required this.deliveryId, + required this.id, + required this.paymentMethodCode, + required this.paymentMethodId, + required this.recordedAt, + this.recordedByCarId, + required this.recordedByPersonalnummer}) + : super._(); + @override + DeliveryPayment rebuild(void Function(DeliveryPaymentBuilder) updates) => + (toBuilder()..update(updates)).build(); + + @override + DeliveryPaymentBuilder toBuilder() => DeliveryPaymentBuilder()..replace(this); + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + return other is DeliveryPayment && + amountCents == other.amountCents && + deliveryId == other.deliveryId && + id == other.id && + paymentMethodCode == other.paymentMethodCode && + paymentMethodId == other.paymentMethodId && + recordedAt == other.recordedAt && + recordedByCarId == other.recordedByCarId && + recordedByPersonalnummer == other.recordedByPersonalnummer; + } + + @override + int get hashCode { + var _$hash = 0; + _$hash = $jc(_$hash, amountCents.hashCode); + _$hash = $jc(_$hash, deliveryId.hashCode); + _$hash = $jc(_$hash, id.hashCode); + _$hash = $jc(_$hash, paymentMethodCode.hashCode); + _$hash = $jc(_$hash, paymentMethodId.hashCode); + _$hash = $jc(_$hash, recordedAt.hashCode); + _$hash = $jc(_$hash, recordedByCarId.hashCode); + _$hash = $jc(_$hash, recordedByPersonalnummer.hashCode); + _$hash = $jf(_$hash); + return _$hash; + } + + @override + String toString() { + return (newBuiltValueToStringHelper(r'DeliveryPayment') + ..add('amountCents', amountCents) + ..add('deliveryId', deliveryId) + ..add('id', id) + ..add('paymentMethodCode', paymentMethodCode) + ..add('paymentMethodId', paymentMethodId) + ..add('recordedAt', recordedAt) + ..add('recordedByCarId', recordedByCarId) + ..add('recordedByPersonalnummer', recordedByPersonalnummer)) + .toString(); + } +} + +class DeliveryPaymentBuilder + implements Builder { + _$DeliveryPayment? _$v; + + int? _amountCents; + int? get amountCents => _$this._amountCents; + set amountCents(int? amountCents) => _$this._amountCents = amountCents; + + String? _deliveryId; + String? get deliveryId => _$this._deliveryId; + set deliveryId(String? deliveryId) => _$this._deliveryId = deliveryId; + + String? _id; + String? get id => _$this._id; + set id(String? id) => _$this._id = id; + + String? _paymentMethodCode; + String? get paymentMethodCode => _$this._paymentMethodCode; + set paymentMethodCode(String? paymentMethodCode) => + _$this._paymentMethodCode = paymentMethodCode; + + String? _paymentMethodId; + String? get paymentMethodId => _$this._paymentMethodId; + set paymentMethodId(String? paymentMethodId) => + _$this._paymentMethodId = paymentMethodId; + + DateTime? _recordedAt; + DateTime? get recordedAt => _$this._recordedAt; + set recordedAt(DateTime? recordedAt) => _$this._recordedAt = recordedAt; + + String? _recordedByCarId; + String? get recordedByCarId => _$this._recordedByCarId; + set recordedByCarId(String? recordedByCarId) => + _$this._recordedByCarId = recordedByCarId; + + int? _recordedByPersonalnummer; + int? get recordedByPersonalnummer => _$this._recordedByPersonalnummer; + set recordedByPersonalnummer(int? recordedByPersonalnummer) => + _$this._recordedByPersonalnummer = recordedByPersonalnummer; + + DeliveryPaymentBuilder() { + DeliveryPayment._defaults(this); + } + + DeliveryPaymentBuilder get _$this { + final $v = _$v; + if ($v != null) { + _amountCents = $v.amountCents; + _deliveryId = $v.deliveryId; + _id = $v.id; + _paymentMethodCode = $v.paymentMethodCode; + _paymentMethodId = $v.paymentMethodId; + _recordedAt = $v.recordedAt; + _recordedByCarId = $v.recordedByCarId; + _recordedByPersonalnummer = $v.recordedByPersonalnummer; + _$v = null; + } + return this; + } + + @override + void replace(DeliveryPayment other) { + _$v = other as _$DeliveryPayment; + } + + @override + void update(void Function(DeliveryPaymentBuilder)? updates) { + if (updates != null) updates(this); + } + + @override + DeliveryPayment build() => _build(); + + _$DeliveryPayment _build() { + final _$result = _$v ?? + _$DeliveryPayment._( + amountCents: BuiltValueNullFieldError.checkNotNull( + amountCents, r'DeliveryPayment', 'amountCents'), + deliveryId: BuiltValueNullFieldError.checkNotNull( + deliveryId, r'DeliveryPayment', 'deliveryId'), + id: BuiltValueNullFieldError.checkNotNull( + id, r'DeliveryPayment', 'id'), + paymentMethodCode: BuiltValueNullFieldError.checkNotNull( + paymentMethodCode, r'DeliveryPayment', 'paymentMethodCode'), + paymentMethodId: BuiltValueNullFieldError.checkNotNull( + paymentMethodId, r'DeliveryPayment', 'paymentMethodId'), + recordedAt: BuiltValueNullFieldError.checkNotNull( + recordedAt, r'DeliveryPayment', 'recordedAt'), + recordedByCarId: recordedByCarId, + recordedByPersonalnummer: BuiltValueNullFieldError.checkNotNull( + recordedByPersonalnummer, + r'DeliveryPayment', + 'recordedByPersonalnummer'), + ); + replace(_$result); + return _$result; + } +} + +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/packages/holzleitner_api/lib/src/model/delivery_payment_response.dart b/packages/holzleitner_api/lib/src/model/delivery_payment_response.dart new file mode 100644 index 0000000..fa3500a --- /dev/null +++ b/packages/holzleitner_api/lib/src/model/delivery_payment_response.dart @@ -0,0 +1,107 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// + +// ignore_for_file: unused_element +import 'package:holzleitner_api/src/model/delivery_payment.dart'; +import 'package:built_value/built_value.dart'; +import 'package:built_value/serializer.dart'; + +part 'delivery_payment_response.g.dart'; + +/// DeliveryPaymentResponse +/// +/// Properties: +/// * [payment] +@BuiltValue() +abstract class DeliveryPaymentResponse implements Built { + @BuiltValueField(wireName: r'payment') + DeliveryPayment get payment; + + DeliveryPaymentResponse._(); + + factory DeliveryPaymentResponse([void updates(DeliveryPaymentResponseBuilder b)]) = _$DeliveryPaymentResponse; + + @BuiltValueHook(initializeBuilder: true) + static void _defaults(DeliveryPaymentResponseBuilder b) => b; + + @BuiltValueSerializer(custom: true) + static Serializer get serializer => _$DeliveryPaymentResponseSerializer(); +} + +class _$DeliveryPaymentResponseSerializer implements PrimitiveSerializer { + @override + final Iterable types = const [DeliveryPaymentResponse, _$DeliveryPaymentResponse]; + + @override + final String wireName = r'DeliveryPaymentResponse'; + + Iterable _serializeProperties( + Serializers serializers, + DeliveryPaymentResponse object, { + FullType specifiedType = FullType.unspecified, + }) sync* { + yield r'payment'; + yield serializers.serialize( + object.payment, + specifiedType: const FullType(DeliveryPayment), + ); + } + + @override + Object serialize( + Serializers serializers, + DeliveryPaymentResponse object, { + FullType specifiedType = FullType.unspecified, + }) { + return _serializeProperties(serializers, object, specifiedType: specifiedType).toList(); + } + + void _deserializeProperties( + Serializers serializers, + Object serialized, { + FullType specifiedType = FullType.unspecified, + required List serializedList, + required DeliveryPaymentResponseBuilder result, + required List unhandled, + }) { + for (var i = 0; i < serializedList.length; i += 2) { + final key = serializedList[i] as String; + final value = serializedList[i + 1]; + switch (key) { + case r'payment': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType(DeliveryPayment), + ) as DeliveryPayment; + result.payment.replace(valueDes); + break; + default: + unhandled.add(key); + unhandled.add(value); + break; + } + } + } + + @override + DeliveryPaymentResponse deserialize( + Serializers serializers, + Object serialized, { + FullType specifiedType = FullType.unspecified, + }) { + final result = DeliveryPaymentResponseBuilder(); + final serializedList = (serialized as Iterable).toList(); + final unhandled = []; + _deserializeProperties( + serializers, + serialized, + specifiedType: specifiedType, + serializedList: serializedList, + unhandled: unhandled, + result: result, + ); + return result.build(); + } +} + diff --git a/packages/holzleitner_api/lib/src/model/delivery_payment_response.g.dart b/packages/holzleitner_api/lib/src/model/delivery_payment_response.g.dart new file mode 100644 index 0000000..ca484e4 --- /dev/null +++ b/packages/holzleitner_api/lib/src/model/delivery_payment_response.g.dart @@ -0,0 +1,108 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'delivery_payment_response.dart'; + +// ************************************************************************** +// BuiltValueGenerator +// ************************************************************************** + +class _$DeliveryPaymentResponse extends DeliveryPaymentResponse { + @override + final DeliveryPayment payment; + + factory _$DeliveryPaymentResponse( + [void Function(DeliveryPaymentResponseBuilder)? updates]) => + (DeliveryPaymentResponseBuilder()..update(updates))._build(); + + _$DeliveryPaymentResponse._({required this.payment}) : super._(); + @override + DeliveryPaymentResponse rebuild( + void Function(DeliveryPaymentResponseBuilder) updates) => + (toBuilder()..update(updates)).build(); + + @override + DeliveryPaymentResponseBuilder toBuilder() => + DeliveryPaymentResponseBuilder()..replace(this); + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + return other is DeliveryPaymentResponse && payment == other.payment; + } + + @override + int get hashCode { + var _$hash = 0; + _$hash = $jc(_$hash, payment.hashCode); + _$hash = $jf(_$hash); + return _$hash; + } + + @override + String toString() { + return (newBuiltValueToStringHelper(r'DeliveryPaymentResponse') + ..add('payment', payment)) + .toString(); + } +} + +class DeliveryPaymentResponseBuilder + implements + Builder { + _$DeliveryPaymentResponse? _$v; + + DeliveryPaymentBuilder? _payment; + DeliveryPaymentBuilder get payment => + _$this._payment ??= DeliveryPaymentBuilder(); + set payment(DeliveryPaymentBuilder? payment) => _$this._payment = payment; + + DeliveryPaymentResponseBuilder() { + DeliveryPaymentResponse._defaults(this); + } + + DeliveryPaymentResponseBuilder get _$this { + final $v = _$v; + if ($v != null) { + _payment = $v.payment.toBuilder(); + _$v = null; + } + return this; + } + + @override + void replace(DeliveryPaymentResponse other) { + _$v = other as _$DeliveryPaymentResponse; + } + + @override + void update(void Function(DeliveryPaymentResponseBuilder)? updates) { + if (updates != null) updates(this); + } + + @override + DeliveryPaymentResponse build() => _build(); + + _$DeliveryPaymentResponse _build() { + _$DeliveryPaymentResponse _$result; + try { + _$result = _$v ?? + _$DeliveryPaymentResponse._( + payment: payment.build(), + ); + } catch (_) { + late String _$failedField; + try { + _$failedField = 'payment'; + payment.build(); + } catch (e) { + throw BuiltValueNestedFieldError( + r'DeliveryPaymentResponse', _$failedField, e.toString()); + } + rethrow; + } + replace(_$result); + return _$result; + } +} + +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/packages/holzleitner_api/lib/src/model/pending_review_response.dart b/packages/holzleitner_api/lib/src/model/pending_review_response.dart new file mode 100644 index 0000000..48e40b1 --- /dev/null +++ b/packages/holzleitner_api/lib/src/model/pending_review_response.dart @@ -0,0 +1,243 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// + +// ignore_for_file: unused_element +import 'package:holzleitner_api/src/model/reviewed_item_response.dart'; +import 'package:built_collection/built_collection.dart'; +import 'package:built_value/built_value.dart'; +import 'package:built_value/serializer.dart'; + +part 'pending_review_response.g.dart'; + +/// PendingReviewResponse +/// +/// Properties: +/// * [creditedItems] - Entfernte/teil-gutgeschriebene Positionen. +/// * [customerName] +/// * [deliveryId] +/// * [erpBelegartId] +/// * [erpBelegnummer] +/// * [lastChangeAt] - Zeitpunkt der letzten beleg-ändernden Aktion (RFC 3339). +/// * [moneyCreditCents] - Geld-Gutschrift in Cent (0 = keine). +/// * [moneyCreditReason] +/// * [tourDate] - Tourdatum (ISO `YYYY-MM-DD`). +@BuiltValue() +abstract class PendingReviewResponse implements Built { + /// Entfernte/teil-gutgeschriebene Positionen. + @BuiltValueField(wireName: r'credited_items') + BuiltList get creditedItems; + + @BuiltValueField(wireName: r'customer_name') + String get customerName; + + @BuiltValueField(wireName: r'delivery_id') + String get deliveryId; + + @BuiltValueField(wireName: r'erp_belegart_id') + int get erpBelegartId; + + @BuiltValueField(wireName: r'erp_belegnummer') + String get erpBelegnummer; + + /// Zeitpunkt der letzten beleg-ändernden Aktion (RFC 3339). + @BuiltValueField(wireName: r'last_change_at') + String get lastChangeAt; + + /// Geld-Gutschrift in Cent (0 = keine). + @BuiltValueField(wireName: r'money_credit_cents') + int get moneyCreditCents; + + @BuiltValueField(wireName: r'money_credit_reason') + String? get moneyCreditReason; + + /// Tourdatum (ISO `YYYY-MM-DD`). + @BuiltValueField(wireName: r'tour_date') + String get tourDate; + + PendingReviewResponse._(); + + factory PendingReviewResponse([void updates(PendingReviewResponseBuilder b)]) = _$PendingReviewResponse; + + @BuiltValueHook(initializeBuilder: true) + static void _defaults(PendingReviewResponseBuilder b) => b; + + @BuiltValueSerializer(custom: true) + static Serializer get serializer => _$PendingReviewResponseSerializer(); +} + +class _$PendingReviewResponseSerializer implements PrimitiveSerializer { + @override + final Iterable types = const [PendingReviewResponse, _$PendingReviewResponse]; + + @override + final String wireName = r'PendingReviewResponse'; + + Iterable _serializeProperties( + Serializers serializers, + PendingReviewResponse object, { + FullType specifiedType = FullType.unspecified, + }) sync* { + yield r'credited_items'; + yield serializers.serialize( + object.creditedItems, + specifiedType: const FullType(BuiltList, [FullType(ReviewedItemResponse)]), + ); + yield r'customer_name'; + yield serializers.serialize( + object.customerName, + specifiedType: const FullType(String), + ); + yield r'delivery_id'; + yield serializers.serialize( + object.deliveryId, + specifiedType: const FullType(String), + ); + yield r'erp_belegart_id'; + yield serializers.serialize( + object.erpBelegartId, + specifiedType: const FullType(int), + ); + yield r'erp_belegnummer'; + yield serializers.serialize( + object.erpBelegnummer, + specifiedType: const FullType(String), + ); + yield r'last_change_at'; + yield serializers.serialize( + object.lastChangeAt, + specifiedType: const FullType(String), + ); + yield r'money_credit_cents'; + yield serializers.serialize( + object.moneyCreditCents, + specifiedType: const FullType(int), + ); + if (object.moneyCreditReason != null) { + yield r'money_credit_reason'; + yield serializers.serialize( + object.moneyCreditReason, + specifiedType: const FullType.nullable(String), + ); + } + yield r'tour_date'; + yield serializers.serialize( + object.tourDate, + specifiedType: const FullType(String), + ); + } + + @override + Object serialize( + Serializers serializers, + PendingReviewResponse object, { + FullType specifiedType = FullType.unspecified, + }) { + return _serializeProperties(serializers, object, specifiedType: specifiedType).toList(); + } + + void _deserializeProperties( + Serializers serializers, + Object serialized, { + FullType specifiedType = FullType.unspecified, + required List serializedList, + required PendingReviewResponseBuilder result, + required List unhandled, + }) { + for (var i = 0; i < serializedList.length; i += 2) { + final key = serializedList[i] as String; + final value = serializedList[i + 1]; + switch (key) { + case r'credited_items': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType(BuiltList, [FullType(ReviewedItemResponse)]), + ) as BuiltList; + result.creditedItems.replace(valueDes); + break; + case r'customer_name': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType(String), + ) as String; + result.customerName = valueDes; + break; + case r'delivery_id': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType(String), + ) as String; + result.deliveryId = valueDes; + break; + case r'erp_belegart_id': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType(int), + ) as int; + result.erpBelegartId = valueDes; + break; + case r'erp_belegnummer': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType(String), + ) as String; + result.erpBelegnummer = valueDes; + break; + case r'last_change_at': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType(String), + ) as String; + result.lastChangeAt = valueDes; + break; + case r'money_credit_cents': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType(int), + ) as int; + result.moneyCreditCents = valueDes; + break; + case r'money_credit_reason': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType.nullable(String), + ) as String?; + if (valueDes == null) continue; + result.moneyCreditReason = valueDes; + break; + case r'tour_date': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType(String), + ) as String; + result.tourDate = valueDes; + break; + default: + unhandled.add(key); + unhandled.add(value); + break; + } + } + } + + @override + PendingReviewResponse deserialize( + Serializers serializers, + Object serialized, { + FullType specifiedType = FullType.unspecified, + }) { + final result = PendingReviewResponseBuilder(); + final serializedList = (serialized as Iterable).toList(); + final unhandled = []; + _deserializeProperties( + serializers, + serialized, + specifiedType: specifiedType, + serializedList: serializedList, + unhandled: unhandled, + result: result, + ); + return result.build(); + } +} + diff --git a/packages/holzleitner_api/lib/src/model/pending_review_response.g.dart b/packages/holzleitner_api/lib/src/model/pending_review_response.g.dart new file mode 100644 index 0000000..340a3c3 --- /dev/null +++ b/packages/holzleitner_api/lib/src/model/pending_review_response.g.dart @@ -0,0 +1,218 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'pending_review_response.dart'; + +// ************************************************************************** +// BuiltValueGenerator +// ************************************************************************** + +class _$PendingReviewResponse extends PendingReviewResponse { + @override + final BuiltList creditedItems; + @override + final String customerName; + @override + final String deliveryId; + @override + final int erpBelegartId; + @override + final String erpBelegnummer; + @override + final String lastChangeAt; + @override + final int moneyCreditCents; + @override + final String? moneyCreditReason; + @override + final String tourDate; + + factory _$PendingReviewResponse( + [void Function(PendingReviewResponseBuilder)? updates]) => + (PendingReviewResponseBuilder()..update(updates))._build(); + + _$PendingReviewResponse._( + {required this.creditedItems, + required this.customerName, + required this.deliveryId, + required this.erpBelegartId, + required this.erpBelegnummer, + required this.lastChangeAt, + required this.moneyCreditCents, + this.moneyCreditReason, + required this.tourDate}) + : super._(); + @override + PendingReviewResponse rebuild( + void Function(PendingReviewResponseBuilder) updates) => + (toBuilder()..update(updates)).build(); + + @override + PendingReviewResponseBuilder toBuilder() => + PendingReviewResponseBuilder()..replace(this); + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + return other is PendingReviewResponse && + creditedItems == other.creditedItems && + customerName == other.customerName && + deliveryId == other.deliveryId && + erpBelegartId == other.erpBelegartId && + erpBelegnummer == other.erpBelegnummer && + lastChangeAt == other.lastChangeAt && + moneyCreditCents == other.moneyCreditCents && + moneyCreditReason == other.moneyCreditReason && + tourDate == other.tourDate; + } + + @override + int get hashCode { + var _$hash = 0; + _$hash = $jc(_$hash, creditedItems.hashCode); + _$hash = $jc(_$hash, customerName.hashCode); + _$hash = $jc(_$hash, deliveryId.hashCode); + _$hash = $jc(_$hash, erpBelegartId.hashCode); + _$hash = $jc(_$hash, erpBelegnummer.hashCode); + _$hash = $jc(_$hash, lastChangeAt.hashCode); + _$hash = $jc(_$hash, moneyCreditCents.hashCode); + _$hash = $jc(_$hash, moneyCreditReason.hashCode); + _$hash = $jc(_$hash, tourDate.hashCode); + _$hash = $jf(_$hash); + return _$hash; + } + + @override + String toString() { + return (newBuiltValueToStringHelper(r'PendingReviewResponse') + ..add('creditedItems', creditedItems) + ..add('customerName', customerName) + ..add('deliveryId', deliveryId) + ..add('erpBelegartId', erpBelegartId) + ..add('erpBelegnummer', erpBelegnummer) + ..add('lastChangeAt', lastChangeAt) + ..add('moneyCreditCents', moneyCreditCents) + ..add('moneyCreditReason', moneyCreditReason) + ..add('tourDate', tourDate)) + .toString(); + } +} + +class PendingReviewResponseBuilder + implements Builder { + _$PendingReviewResponse? _$v; + + ListBuilder? _creditedItems; + ListBuilder get creditedItems => + _$this._creditedItems ??= ListBuilder(); + set creditedItems(ListBuilder? creditedItems) => + _$this._creditedItems = creditedItems; + + String? _customerName; + String? get customerName => _$this._customerName; + set customerName(String? customerName) => _$this._customerName = customerName; + + String? _deliveryId; + String? get deliveryId => _$this._deliveryId; + set deliveryId(String? deliveryId) => _$this._deliveryId = deliveryId; + + int? _erpBelegartId; + int? get erpBelegartId => _$this._erpBelegartId; + set erpBelegartId(int? erpBelegartId) => + _$this._erpBelegartId = erpBelegartId; + + String? _erpBelegnummer; + String? get erpBelegnummer => _$this._erpBelegnummer; + set erpBelegnummer(String? erpBelegnummer) => + _$this._erpBelegnummer = erpBelegnummer; + + String? _lastChangeAt; + String? get lastChangeAt => _$this._lastChangeAt; + set lastChangeAt(String? lastChangeAt) => _$this._lastChangeAt = lastChangeAt; + + int? _moneyCreditCents; + int? get moneyCreditCents => _$this._moneyCreditCents; + set moneyCreditCents(int? moneyCreditCents) => + _$this._moneyCreditCents = moneyCreditCents; + + String? _moneyCreditReason; + String? get moneyCreditReason => _$this._moneyCreditReason; + set moneyCreditReason(String? moneyCreditReason) => + _$this._moneyCreditReason = moneyCreditReason; + + String? _tourDate; + String? get tourDate => _$this._tourDate; + set tourDate(String? tourDate) => _$this._tourDate = tourDate; + + PendingReviewResponseBuilder() { + PendingReviewResponse._defaults(this); + } + + PendingReviewResponseBuilder get _$this { + final $v = _$v; + if ($v != null) { + _creditedItems = $v.creditedItems.toBuilder(); + _customerName = $v.customerName; + _deliveryId = $v.deliveryId; + _erpBelegartId = $v.erpBelegartId; + _erpBelegnummer = $v.erpBelegnummer; + _lastChangeAt = $v.lastChangeAt; + _moneyCreditCents = $v.moneyCreditCents; + _moneyCreditReason = $v.moneyCreditReason; + _tourDate = $v.tourDate; + _$v = null; + } + return this; + } + + @override + void replace(PendingReviewResponse other) { + _$v = other as _$PendingReviewResponse; + } + + @override + void update(void Function(PendingReviewResponseBuilder)? updates) { + if (updates != null) updates(this); + } + + @override + PendingReviewResponse build() => _build(); + + _$PendingReviewResponse _build() { + _$PendingReviewResponse _$result; + try { + _$result = _$v ?? + _$PendingReviewResponse._( + creditedItems: creditedItems.build(), + customerName: BuiltValueNullFieldError.checkNotNull( + customerName, r'PendingReviewResponse', 'customerName'), + deliveryId: BuiltValueNullFieldError.checkNotNull( + deliveryId, r'PendingReviewResponse', 'deliveryId'), + erpBelegartId: BuiltValueNullFieldError.checkNotNull( + erpBelegartId, r'PendingReviewResponse', 'erpBelegartId'), + erpBelegnummer: BuiltValueNullFieldError.checkNotNull( + erpBelegnummer, r'PendingReviewResponse', 'erpBelegnummer'), + lastChangeAt: BuiltValueNullFieldError.checkNotNull( + lastChangeAt, r'PendingReviewResponse', 'lastChangeAt'), + moneyCreditCents: BuiltValueNullFieldError.checkNotNull( + moneyCreditCents, r'PendingReviewResponse', 'moneyCreditCents'), + moneyCreditReason: moneyCreditReason, + tourDate: BuiltValueNullFieldError.checkNotNull( + tourDate, r'PendingReviewResponse', 'tourDate'), + ); + } catch (_) { + late String _$failedField; + try { + _$failedField = 'creditedItems'; + creditedItems.build(); + } catch (e) { + throw BuiltValueNestedFieldError( + r'PendingReviewResponse', _$failedField, e.toString()); + } + rethrow; + } + replace(_$result); + return _$result; + } +} + +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/packages/holzleitner_api/lib/src/model/positions_modified_response.dart b/packages/holzleitner_api/lib/src/model/positions_modified_response.dart new file mode 100644 index 0000000..64b6f14 --- /dev/null +++ b/packages/holzleitner_api/lib/src/model/positions_modified_response.dart @@ -0,0 +1,124 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// + +// ignore_for_file: unused_element +import 'package:built_value/built_value.dart'; +import 'package:built_value/serializer.dart'; + +part 'positions_modified_response.g.dart'; + +/// PositionsModifiedResponse +/// +/// Properties: +/// * [belegnummer] - ERP-Belegnummer, nach der gefragt wurde. +/// * [positionsModified] - `true`, wenn an der Lieferung Positionen verändert wurden — eine Zeile wurde entfernt oder in der Menge reduziert (Stück-Gutschrift) **oder** es liegt eine aktive Geld-Gutschrift vor. +@BuiltValue() +abstract class PositionsModifiedResponse implements Built { + /// ERP-Belegnummer, nach der gefragt wurde. + @BuiltValueField(wireName: r'belegnummer') + String get belegnummer; + + /// `true`, wenn an der Lieferung Positionen verändert wurden — eine Zeile wurde entfernt oder in der Menge reduziert (Stück-Gutschrift) **oder** es liegt eine aktive Geld-Gutschrift vor. + @BuiltValueField(wireName: r'positions_modified') + bool get positionsModified; + + PositionsModifiedResponse._(); + + factory PositionsModifiedResponse([void updates(PositionsModifiedResponseBuilder b)]) = _$PositionsModifiedResponse; + + @BuiltValueHook(initializeBuilder: true) + static void _defaults(PositionsModifiedResponseBuilder b) => b; + + @BuiltValueSerializer(custom: true) + static Serializer get serializer => _$PositionsModifiedResponseSerializer(); +} + +class _$PositionsModifiedResponseSerializer implements PrimitiveSerializer { + @override + final Iterable types = const [PositionsModifiedResponse, _$PositionsModifiedResponse]; + + @override + final String wireName = r'PositionsModifiedResponse'; + + Iterable _serializeProperties( + Serializers serializers, + PositionsModifiedResponse object, { + FullType specifiedType = FullType.unspecified, + }) sync* { + yield r'belegnummer'; + yield serializers.serialize( + object.belegnummer, + specifiedType: const FullType(String), + ); + yield r'positions_modified'; + yield serializers.serialize( + object.positionsModified, + specifiedType: const FullType(bool), + ); + } + + @override + Object serialize( + Serializers serializers, + PositionsModifiedResponse object, { + FullType specifiedType = FullType.unspecified, + }) { + return _serializeProperties(serializers, object, specifiedType: specifiedType).toList(); + } + + void _deserializeProperties( + Serializers serializers, + Object serialized, { + FullType specifiedType = FullType.unspecified, + required List serializedList, + required PositionsModifiedResponseBuilder result, + required List unhandled, + }) { + for (var i = 0; i < serializedList.length; i += 2) { + final key = serializedList[i] as String; + final value = serializedList[i + 1]; + switch (key) { + case r'belegnummer': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType(String), + ) as String; + result.belegnummer = valueDes; + break; + case r'positions_modified': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType(bool), + ) as bool; + result.positionsModified = valueDes; + break; + default: + unhandled.add(key); + unhandled.add(value); + break; + } + } + } + + @override + PositionsModifiedResponse deserialize( + Serializers serializers, + Object serialized, { + FullType specifiedType = FullType.unspecified, + }) { + final result = PositionsModifiedResponseBuilder(); + final serializedList = (serialized as Iterable).toList(); + final unhandled = []; + _deserializeProperties( + serializers, + serialized, + specifiedType: specifiedType, + serializedList: serializedList, + unhandled: unhandled, + result: result, + ); + return result.build(); + } +} + diff --git a/packages/holzleitner_api/lib/src/model/positions_modified_response.g.dart b/packages/holzleitner_api/lib/src/model/positions_modified_response.g.dart new file mode 100644 index 0000000..e09b15c --- /dev/null +++ b/packages/holzleitner_api/lib/src/model/positions_modified_response.g.dart @@ -0,0 +1,113 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'positions_modified_response.dart'; + +// ************************************************************************** +// BuiltValueGenerator +// ************************************************************************** + +class _$PositionsModifiedResponse extends PositionsModifiedResponse { + @override + final String belegnummer; + @override + final bool positionsModified; + + factory _$PositionsModifiedResponse( + [void Function(PositionsModifiedResponseBuilder)? updates]) => + (PositionsModifiedResponseBuilder()..update(updates))._build(); + + _$PositionsModifiedResponse._( + {required this.belegnummer, required this.positionsModified}) + : super._(); + @override + PositionsModifiedResponse rebuild( + void Function(PositionsModifiedResponseBuilder) updates) => + (toBuilder()..update(updates)).build(); + + @override + PositionsModifiedResponseBuilder toBuilder() => + PositionsModifiedResponseBuilder()..replace(this); + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + return other is PositionsModifiedResponse && + belegnummer == other.belegnummer && + positionsModified == other.positionsModified; + } + + @override + int get hashCode { + var _$hash = 0; + _$hash = $jc(_$hash, belegnummer.hashCode); + _$hash = $jc(_$hash, positionsModified.hashCode); + _$hash = $jf(_$hash); + return _$hash; + } + + @override + String toString() { + return (newBuiltValueToStringHelper(r'PositionsModifiedResponse') + ..add('belegnummer', belegnummer) + ..add('positionsModified', positionsModified)) + .toString(); + } +} + +class PositionsModifiedResponseBuilder + implements + Builder { + _$PositionsModifiedResponse? _$v; + + String? _belegnummer; + String? get belegnummer => _$this._belegnummer; + set belegnummer(String? belegnummer) => _$this._belegnummer = belegnummer; + + bool? _positionsModified; + bool? get positionsModified => _$this._positionsModified; + set positionsModified(bool? positionsModified) => + _$this._positionsModified = positionsModified; + + PositionsModifiedResponseBuilder() { + PositionsModifiedResponse._defaults(this); + } + + PositionsModifiedResponseBuilder get _$this { + final $v = _$v; + if ($v != null) { + _belegnummer = $v.belegnummer; + _positionsModified = $v.positionsModified; + _$v = null; + } + return this; + } + + @override + void replace(PositionsModifiedResponse other) { + _$v = other as _$PositionsModifiedResponse; + } + + @override + void update(void Function(PositionsModifiedResponseBuilder)? updates) { + if (updates != null) updates(this); + } + + @override + PositionsModifiedResponse build() => _build(); + + _$PositionsModifiedResponse _build() { + final _$result = _$v ?? + _$PositionsModifiedResponse._( + belegnummer: BuiltValueNullFieldError.checkNotNull( + belegnummer, r'PositionsModifiedResponse', 'belegnummer'), + positionsModified: BuiltValueNullFieldError.checkNotNull( + positionsModified, + r'PositionsModifiedResponse', + 'positionsModified'), + ); + replace(_$result); + return _$result; + } +} + +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/packages/holzleitner_api/lib/src/model/record_delivery_payment_request.dart b/packages/holzleitner_api/lib/src/model/record_delivery_payment_request.dart new file mode 100644 index 0000000..4b2db6a --- /dev/null +++ b/packages/holzleitner_api/lib/src/model/record_delivery_payment_request.dart @@ -0,0 +1,161 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// + +// ignore_for_file: unused_element +import 'package:built_value/built_value.dart'; +import 'package:built_value/serializer.dart'; + +part 'record_delivery_payment_request.g.dart'; + +/// RecordDeliveryPaymentRequest +/// +/// Properties: +/// * [authorCarId] - Fahrzeug des Akteurs (Audit-Spur). Muss zum Account gehören. +/// * [clientEventId] - Idempotenz-Schlüssel — pro Bestätigung genau einmal vergeben. +/// * [expectedAmountCents] - Betrag in Cent, den der Fahrer in der App gesehen und bestätigt hat. Der Server berechnet den offenen Betrag selbst und lehnt ab, wenn er davon abweicht — so landet nie ein Betrag im Protokoll, den der Fahrer nicht gesehen hat. +/// * [paymentMethodId] - Gewählte Zahlungsmethode. Muss existieren und aktiv sein. +@BuiltValue() +abstract class RecordDeliveryPaymentRequest implements Built { + /// Fahrzeug des Akteurs (Audit-Spur). Muss zum Account gehören. + @BuiltValueField(wireName: r'authorCarId') + String? get authorCarId; + + /// Idempotenz-Schlüssel — pro Bestätigung genau einmal vergeben. + @BuiltValueField(wireName: r'clientEventId') + String get clientEventId; + + /// Betrag in Cent, den der Fahrer in der App gesehen und bestätigt hat. Der Server berechnet den offenen Betrag selbst und lehnt ab, wenn er davon abweicht — so landet nie ein Betrag im Protokoll, den der Fahrer nicht gesehen hat. + @BuiltValueField(wireName: r'expectedAmountCents') + int get expectedAmountCents; + + /// Gewählte Zahlungsmethode. Muss existieren und aktiv sein. + @BuiltValueField(wireName: r'paymentMethodId') + String get paymentMethodId; + + RecordDeliveryPaymentRequest._(); + + factory RecordDeliveryPaymentRequest([void updates(RecordDeliveryPaymentRequestBuilder b)]) = _$RecordDeliveryPaymentRequest; + + @BuiltValueHook(initializeBuilder: true) + static void _defaults(RecordDeliveryPaymentRequestBuilder b) => b; + + @BuiltValueSerializer(custom: true) + static Serializer get serializer => _$RecordDeliveryPaymentRequestSerializer(); +} + +class _$RecordDeliveryPaymentRequestSerializer implements PrimitiveSerializer { + @override + final Iterable types = const [RecordDeliveryPaymentRequest, _$RecordDeliveryPaymentRequest]; + + @override + final String wireName = r'RecordDeliveryPaymentRequest'; + + Iterable _serializeProperties( + Serializers serializers, + RecordDeliveryPaymentRequest object, { + FullType specifiedType = FullType.unspecified, + }) sync* { + if (object.authorCarId != null) { + yield r'authorCarId'; + yield serializers.serialize( + object.authorCarId, + specifiedType: const FullType.nullable(String), + ); + } + yield r'clientEventId'; + yield serializers.serialize( + object.clientEventId, + specifiedType: const FullType(String), + ); + yield r'expectedAmountCents'; + yield serializers.serialize( + object.expectedAmountCents, + specifiedType: const FullType(int), + ); + yield r'paymentMethodId'; + yield serializers.serialize( + object.paymentMethodId, + specifiedType: const FullType(String), + ); + } + + @override + Object serialize( + Serializers serializers, + RecordDeliveryPaymentRequest object, { + FullType specifiedType = FullType.unspecified, + }) { + return _serializeProperties(serializers, object, specifiedType: specifiedType).toList(); + } + + void _deserializeProperties( + Serializers serializers, + Object serialized, { + FullType specifiedType = FullType.unspecified, + required List serializedList, + required RecordDeliveryPaymentRequestBuilder result, + required List unhandled, + }) { + for (var i = 0; i < serializedList.length; i += 2) { + final key = serializedList[i] as String; + final value = serializedList[i + 1]; + switch (key) { + case r'authorCarId': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType.nullable(String), + ) as String?; + if (valueDes == null) continue; + result.authorCarId = valueDes; + break; + case r'clientEventId': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType(String), + ) as String; + result.clientEventId = valueDes; + break; + case r'expectedAmountCents': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType(int), + ) as int; + result.expectedAmountCents = valueDes; + break; + case r'paymentMethodId': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType(String), + ) as String; + result.paymentMethodId = valueDes; + break; + default: + unhandled.add(key); + unhandled.add(value); + break; + } + } + } + + @override + RecordDeliveryPaymentRequest deserialize( + Serializers serializers, + Object serialized, { + FullType specifiedType = FullType.unspecified, + }) { + final result = RecordDeliveryPaymentRequestBuilder(); + final serializedList = (serialized as Iterable).toList(); + final unhandled = []; + _deserializeProperties( + serializers, + serialized, + specifiedType: specifiedType, + serializedList: serializedList, + unhandled: unhandled, + result: result, + ); + return result.build(); + } +} + diff --git a/packages/holzleitner_api/lib/src/model/record_delivery_payment_request.g.dart b/packages/holzleitner_api/lib/src/model/record_delivery_payment_request.g.dart new file mode 100644 index 0000000..35bbcb2 --- /dev/null +++ b/packages/holzleitner_api/lib/src/model/record_delivery_payment_request.g.dart @@ -0,0 +1,144 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'record_delivery_payment_request.dart'; + +// ************************************************************************** +// BuiltValueGenerator +// ************************************************************************** + +class _$RecordDeliveryPaymentRequest extends RecordDeliveryPaymentRequest { + @override + final String? authorCarId; + @override + final String clientEventId; + @override + final int expectedAmountCents; + @override + final String paymentMethodId; + + factory _$RecordDeliveryPaymentRequest( + [void Function(RecordDeliveryPaymentRequestBuilder)? updates]) => + (RecordDeliveryPaymentRequestBuilder()..update(updates))._build(); + + _$RecordDeliveryPaymentRequest._( + {this.authorCarId, + required this.clientEventId, + required this.expectedAmountCents, + required this.paymentMethodId}) + : super._(); + @override + RecordDeliveryPaymentRequest rebuild( + void Function(RecordDeliveryPaymentRequestBuilder) updates) => + (toBuilder()..update(updates)).build(); + + @override + RecordDeliveryPaymentRequestBuilder toBuilder() => + RecordDeliveryPaymentRequestBuilder()..replace(this); + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + return other is RecordDeliveryPaymentRequest && + authorCarId == other.authorCarId && + clientEventId == other.clientEventId && + expectedAmountCents == other.expectedAmountCents && + paymentMethodId == other.paymentMethodId; + } + + @override + int get hashCode { + var _$hash = 0; + _$hash = $jc(_$hash, authorCarId.hashCode); + _$hash = $jc(_$hash, clientEventId.hashCode); + _$hash = $jc(_$hash, expectedAmountCents.hashCode); + _$hash = $jc(_$hash, paymentMethodId.hashCode); + _$hash = $jf(_$hash); + return _$hash; + } + + @override + String toString() { + return (newBuiltValueToStringHelper(r'RecordDeliveryPaymentRequest') + ..add('authorCarId', authorCarId) + ..add('clientEventId', clientEventId) + ..add('expectedAmountCents', expectedAmountCents) + ..add('paymentMethodId', paymentMethodId)) + .toString(); + } +} + +class RecordDeliveryPaymentRequestBuilder + implements + Builder { + _$RecordDeliveryPaymentRequest? _$v; + + String? _authorCarId; + String? get authorCarId => _$this._authorCarId; + set authorCarId(String? authorCarId) => _$this._authorCarId = authorCarId; + + String? _clientEventId; + String? get clientEventId => _$this._clientEventId; + set clientEventId(String? clientEventId) => + _$this._clientEventId = clientEventId; + + int? _expectedAmountCents; + int? get expectedAmountCents => _$this._expectedAmountCents; + set expectedAmountCents(int? expectedAmountCents) => + _$this._expectedAmountCents = expectedAmountCents; + + String? _paymentMethodId; + String? get paymentMethodId => _$this._paymentMethodId; + set paymentMethodId(String? paymentMethodId) => + _$this._paymentMethodId = paymentMethodId; + + RecordDeliveryPaymentRequestBuilder() { + RecordDeliveryPaymentRequest._defaults(this); + } + + RecordDeliveryPaymentRequestBuilder get _$this { + final $v = _$v; + if ($v != null) { + _authorCarId = $v.authorCarId; + _clientEventId = $v.clientEventId; + _expectedAmountCents = $v.expectedAmountCents; + _paymentMethodId = $v.paymentMethodId; + _$v = null; + } + return this; + } + + @override + void replace(RecordDeliveryPaymentRequest other) { + _$v = other as _$RecordDeliveryPaymentRequest; + } + + @override + void update(void Function(RecordDeliveryPaymentRequestBuilder)? updates) { + if (updates != null) updates(this); + } + + @override + RecordDeliveryPaymentRequest build() => _build(); + + _$RecordDeliveryPaymentRequest _build() { + final _$result = _$v ?? + _$RecordDeliveryPaymentRequest._( + authorCarId: authorCarId, + clientEventId: BuiltValueNullFieldError.checkNotNull( + clientEventId, r'RecordDeliveryPaymentRequest', 'clientEventId'), + expectedAmountCents: BuiltValueNullFieldError.checkNotNull( + expectedAmountCents, + r'RecordDeliveryPaymentRequest', + 'expectedAmountCents'), + paymentMethodId: BuiltValueNullFieldError.checkNotNull( + paymentMethodId, + r'RecordDeliveryPaymentRequest', + 'paymentMethodId'), + ); + replace(_$result); + return _$result; + } +} + +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/packages/holzleitner_api/lib/src/model/resolve_review_request.dart b/packages/holzleitner_api/lib/src/model/resolve_review_request.dart new file mode 100644 index 0000000..843930c --- /dev/null +++ b/packages/holzleitner_api/lib/src/model/resolve_review_request.dart @@ -0,0 +1,127 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// + +// ignore_for_file: unused_element +import 'package:built_value/built_value.dart'; +import 'package:built_value/serializer.dart'; + +part 'resolve_review_request.g.dart'; + +/// ResolveReviewRequest +/// +/// Properties: +/// * [note] - Optionale Notiz zur getroffenen Entscheidung. +/// * [resolvedBy] - Bearbeiter (Name/Kürzel), der die Prüfung bestätigt. +@BuiltValue() +abstract class ResolveReviewRequest implements Built { + /// Optionale Notiz zur getroffenen Entscheidung. + @BuiltValueField(wireName: r'note') + String? get note; + + /// Bearbeiter (Name/Kürzel), der die Prüfung bestätigt. + @BuiltValueField(wireName: r'resolved_by') + String get resolvedBy; + + ResolveReviewRequest._(); + + factory ResolveReviewRequest([void updates(ResolveReviewRequestBuilder b)]) = _$ResolveReviewRequest; + + @BuiltValueHook(initializeBuilder: true) + static void _defaults(ResolveReviewRequestBuilder b) => b; + + @BuiltValueSerializer(custom: true) + static Serializer get serializer => _$ResolveReviewRequestSerializer(); +} + +class _$ResolveReviewRequestSerializer implements PrimitiveSerializer { + @override + final Iterable types = const [ResolveReviewRequest, _$ResolveReviewRequest]; + + @override + final String wireName = r'ResolveReviewRequest'; + + Iterable _serializeProperties( + Serializers serializers, + ResolveReviewRequest object, { + FullType specifiedType = FullType.unspecified, + }) sync* { + if (object.note != null) { + yield r'note'; + yield serializers.serialize( + object.note, + specifiedType: const FullType.nullable(String), + ); + } + yield r'resolved_by'; + yield serializers.serialize( + object.resolvedBy, + specifiedType: const FullType(String), + ); + } + + @override + Object serialize( + Serializers serializers, + ResolveReviewRequest object, { + FullType specifiedType = FullType.unspecified, + }) { + return _serializeProperties(serializers, object, specifiedType: specifiedType).toList(); + } + + void _deserializeProperties( + Serializers serializers, + Object serialized, { + FullType specifiedType = FullType.unspecified, + required List serializedList, + required ResolveReviewRequestBuilder result, + required List unhandled, + }) { + for (var i = 0; i < serializedList.length; i += 2) { + final key = serializedList[i] as String; + final value = serializedList[i + 1]; + switch (key) { + case r'note': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType.nullable(String), + ) as String?; + if (valueDes == null) continue; + result.note = valueDes; + break; + case r'resolved_by': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType(String), + ) as String; + result.resolvedBy = valueDes; + break; + default: + unhandled.add(key); + unhandled.add(value); + break; + } + } + } + + @override + ResolveReviewRequest deserialize( + Serializers serializers, + Object serialized, { + FullType specifiedType = FullType.unspecified, + }) { + final result = ResolveReviewRequestBuilder(); + final serializedList = (serialized as Iterable).toList(); + final unhandled = []; + _deserializeProperties( + serializers, + serialized, + specifiedType: specifiedType, + serializedList: serializedList, + unhandled: unhandled, + result: result, + ); + return result.build(); + } +} + diff --git a/packages/holzleitner_api/lib/src/model/resolve_review_request.g.dart b/packages/holzleitner_api/lib/src/model/resolve_review_request.g.dart new file mode 100644 index 0000000..991fdf9 --- /dev/null +++ b/packages/holzleitner_api/lib/src/model/resolve_review_request.g.dart @@ -0,0 +1,106 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'resolve_review_request.dart'; + +// ************************************************************************** +// BuiltValueGenerator +// ************************************************************************** + +class _$ResolveReviewRequest extends ResolveReviewRequest { + @override + final String? note; + @override + final String resolvedBy; + + factory _$ResolveReviewRequest( + [void Function(ResolveReviewRequestBuilder)? updates]) => + (ResolveReviewRequestBuilder()..update(updates))._build(); + + _$ResolveReviewRequest._({this.note, required this.resolvedBy}) : super._(); + @override + ResolveReviewRequest rebuild( + void Function(ResolveReviewRequestBuilder) updates) => + (toBuilder()..update(updates)).build(); + + @override + ResolveReviewRequestBuilder toBuilder() => + ResolveReviewRequestBuilder()..replace(this); + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + return other is ResolveReviewRequest && + note == other.note && + resolvedBy == other.resolvedBy; + } + + @override + int get hashCode { + var _$hash = 0; + _$hash = $jc(_$hash, note.hashCode); + _$hash = $jc(_$hash, resolvedBy.hashCode); + _$hash = $jf(_$hash); + return _$hash; + } + + @override + String toString() { + return (newBuiltValueToStringHelper(r'ResolveReviewRequest') + ..add('note', note) + ..add('resolvedBy', resolvedBy)) + .toString(); + } +} + +class ResolveReviewRequestBuilder + implements Builder { + _$ResolveReviewRequest? _$v; + + String? _note; + String? get note => _$this._note; + set note(String? note) => _$this._note = note; + + String? _resolvedBy; + String? get resolvedBy => _$this._resolvedBy; + set resolvedBy(String? resolvedBy) => _$this._resolvedBy = resolvedBy; + + ResolveReviewRequestBuilder() { + ResolveReviewRequest._defaults(this); + } + + ResolveReviewRequestBuilder get _$this { + final $v = _$v; + if ($v != null) { + _note = $v.note; + _resolvedBy = $v.resolvedBy; + _$v = null; + } + return this; + } + + @override + void replace(ResolveReviewRequest other) { + _$v = other as _$ResolveReviewRequest; + } + + @override + void update(void Function(ResolveReviewRequestBuilder)? updates) { + if (updates != null) updates(this); + } + + @override + ResolveReviewRequest build() => _build(); + + _$ResolveReviewRequest _build() { + final _$result = _$v ?? + _$ResolveReviewRequest._( + note: note, + resolvedBy: BuiltValueNullFieldError.checkNotNull( + resolvedBy, r'ResolveReviewRequest', 'resolvedBy'), + ); + replace(_$result); + return _$result; + } +} + +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/packages/holzleitner_api/lib/src/model/reviewed_item_response.dart b/packages/holzleitner_api/lib/src/model/reviewed_item_response.dart new file mode 100644 index 0000000..e6ac576 --- /dev/null +++ b/packages/holzleitner_api/lib/src/model/reviewed_item_response.dart @@ -0,0 +1,189 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// + +// ignore_for_file: unused_element +import 'package:built_value/built_value.dart'; +import 'package:built_value/serializer.dart'; + +part 'reviewed_item_response.g.dart'; + +/// ReviewedItemResponse +/// +/// Properties: +/// * [articleName] +/// * [artikelNr] +/// * [belegzeilenNr] +/// * [creditedQuantity] +/// * [reason] +/// * [requiredQuantity] +@BuiltValue() +abstract class ReviewedItemResponse implements Built { + @BuiltValueField(wireName: r'article_name') + String get articleName; + + @BuiltValueField(wireName: r'artikel_nr') + String get artikelNr; + + @BuiltValueField(wireName: r'belegzeilen_nr') + int get belegzeilenNr; + + @BuiltValueField(wireName: r'credited_quantity') + int get creditedQuantity; + + @BuiltValueField(wireName: r'reason') + String? get reason; + + @BuiltValueField(wireName: r'required_quantity') + int get requiredQuantity; + + ReviewedItemResponse._(); + + factory ReviewedItemResponse([void updates(ReviewedItemResponseBuilder b)]) = _$ReviewedItemResponse; + + @BuiltValueHook(initializeBuilder: true) + static void _defaults(ReviewedItemResponseBuilder b) => b; + + @BuiltValueSerializer(custom: true) + static Serializer get serializer => _$ReviewedItemResponseSerializer(); +} + +class _$ReviewedItemResponseSerializer implements PrimitiveSerializer { + @override + final Iterable types = const [ReviewedItemResponse, _$ReviewedItemResponse]; + + @override + final String wireName = r'ReviewedItemResponse'; + + Iterable _serializeProperties( + Serializers serializers, + ReviewedItemResponse object, { + FullType specifiedType = FullType.unspecified, + }) sync* { + yield r'article_name'; + yield serializers.serialize( + object.articleName, + specifiedType: const FullType(String), + ); + yield r'artikel_nr'; + yield serializers.serialize( + object.artikelNr, + specifiedType: const FullType(String), + ); + yield r'belegzeilen_nr'; + yield serializers.serialize( + object.belegzeilenNr, + specifiedType: const FullType(int), + ); + yield r'credited_quantity'; + yield serializers.serialize( + object.creditedQuantity, + specifiedType: const FullType(int), + ); + if (object.reason != null) { + yield r'reason'; + yield serializers.serialize( + object.reason, + specifiedType: const FullType.nullable(String), + ); + } + yield r'required_quantity'; + yield serializers.serialize( + object.requiredQuantity, + specifiedType: const FullType(int), + ); + } + + @override + Object serialize( + Serializers serializers, + ReviewedItemResponse object, { + FullType specifiedType = FullType.unspecified, + }) { + return _serializeProperties(serializers, object, specifiedType: specifiedType).toList(); + } + + void _deserializeProperties( + Serializers serializers, + Object serialized, { + FullType specifiedType = FullType.unspecified, + required List serializedList, + required ReviewedItemResponseBuilder result, + required List unhandled, + }) { + for (var i = 0; i < serializedList.length; i += 2) { + final key = serializedList[i] as String; + final value = serializedList[i + 1]; + switch (key) { + case r'article_name': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType(String), + ) as String; + result.articleName = valueDes; + break; + case r'artikel_nr': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType(String), + ) as String; + result.artikelNr = valueDes; + break; + case r'belegzeilen_nr': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType(int), + ) as int; + result.belegzeilenNr = valueDes; + break; + case r'credited_quantity': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType(int), + ) as int; + result.creditedQuantity = valueDes; + break; + case r'reason': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType.nullable(String), + ) as String?; + if (valueDes == null) continue; + result.reason = valueDes; + break; + case r'required_quantity': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType(int), + ) as int; + result.requiredQuantity = valueDes; + break; + default: + unhandled.add(key); + unhandled.add(value); + break; + } + } + } + + @override + ReviewedItemResponse deserialize( + Serializers serializers, + Object serialized, { + FullType specifiedType = FullType.unspecified, + }) { + final result = ReviewedItemResponseBuilder(); + final serializedList = (serialized as Iterable).toList(); + final unhandled = []; + _deserializeProperties( + serializers, + serialized, + specifiedType: specifiedType, + serializedList: serializedList, + unhandled: unhandled, + result: result, + ); + return result.build(); + } +} + diff --git a/packages/holzleitner_api/lib/src/model/reviewed_item_response.g.dart b/packages/holzleitner_api/lib/src/model/reviewed_item_response.g.dart new file mode 100644 index 0000000..6a42320 --- /dev/null +++ b/packages/holzleitner_api/lib/src/model/reviewed_item_response.g.dart @@ -0,0 +1,164 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'reviewed_item_response.dart'; + +// ************************************************************************** +// BuiltValueGenerator +// ************************************************************************** + +class _$ReviewedItemResponse extends ReviewedItemResponse { + @override + final String articleName; + @override + final String artikelNr; + @override + final int belegzeilenNr; + @override + final int creditedQuantity; + @override + final String? reason; + @override + final int requiredQuantity; + + factory _$ReviewedItemResponse( + [void Function(ReviewedItemResponseBuilder)? updates]) => + (ReviewedItemResponseBuilder()..update(updates))._build(); + + _$ReviewedItemResponse._( + {required this.articleName, + required this.artikelNr, + required this.belegzeilenNr, + required this.creditedQuantity, + this.reason, + required this.requiredQuantity}) + : super._(); + @override + ReviewedItemResponse rebuild( + void Function(ReviewedItemResponseBuilder) updates) => + (toBuilder()..update(updates)).build(); + + @override + ReviewedItemResponseBuilder toBuilder() => + ReviewedItemResponseBuilder()..replace(this); + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + return other is ReviewedItemResponse && + articleName == other.articleName && + artikelNr == other.artikelNr && + belegzeilenNr == other.belegzeilenNr && + creditedQuantity == other.creditedQuantity && + reason == other.reason && + requiredQuantity == other.requiredQuantity; + } + + @override + int get hashCode { + var _$hash = 0; + _$hash = $jc(_$hash, articleName.hashCode); + _$hash = $jc(_$hash, artikelNr.hashCode); + _$hash = $jc(_$hash, belegzeilenNr.hashCode); + _$hash = $jc(_$hash, creditedQuantity.hashCode); + _$hash = $jc(_$hash, reason.hashCode); + _$hash = $jc(_$hash, requiredQuantity.hashCode); + _$hash = $jf(_$hash); + return _$hash; + } + + @override + String toString() { + return (newBuiltValueToStringHelper(r'ReviewedItemResponse') + ..add('articleName', articleName) + ..add('artikelNr', artikelNr) + ..add('belegzeilenNr', belegzeilenNr) + ..add('creditedQuantity', creditedQuantity) + ..add('reason', reason) + ..add('requiredQuantity', requiredQuantity)) + .toString(); + } +} + +class ReviewedItemResponseBuilder + implements Builder { + _$ReviewedItemResponse? _$v; + + String? _articleName; + String? get articleName => _$this._articleName; + set articleName(String? articleName) => _$this._articleName = articleName; + + String? _artikelNr; + String? get artikelNr => _$this._artikelNr; + set artikelNr(String? artikelNr) => _$this._artikelNr = artikelNr; + + int? _belegzeilenNr; + int? get belegzeilenNr => _$this._belegzeilenNr; + set belegzeilenNr(int? belegzeilenNr) => + _$this._belegzeilenNr = belegzeilenNr; + + int? _creditedQuantity; + int? get creditedQuantity => _$this._creditedQuantity; + set creditedQuantity(int? creditedQuantity) => + _$this._creditedQuantity = creditedQuantity; + + String? _reason; + String? get reason => _$this._reason; + set reason(String? reason) => _$this._reason = reason; + + int? _requiredQuantity; + int? get requiredQuantity => _$this._requiredQuantity; + set requiredQuantity(int? requiredQuantity) => + _$this._requiredQuantity = requiredQuantity; + + ReviewedItemResponseBuilder() { + ReviewedItemResponse._defaults(this); + } + + ReviewedItemResponseBuilder get _$this { + final $v = _$v; + if ($v != null) { + _articleName = $v.articleName; + _artikelNr = $v.artikelNr; + _belegzeilenNr = $v.belegzeilenNr; + _creditedQuantity = $v.creditedQuantity; + _reason = $v.reason; + _requiredQuantity = $v.requiredQuantity; + _$v = null; + } + return this; + } + + @override + void replace(ReviewedItemResponse other) { + _$v = other as _$ReviewedItemResponse; + } + + @override + void update(void Function(ReviewedItemResponseBuilder)? updates) { + if (updates != null) updates(this); + } + + @override + ReviewedItemResponse build() => _build(); + + _$ReviewedItemResponse _build() { + final _$result = _$v ?? + _$ReviewedItemResponse._( + articleName: BuiltValueNullFieldError.checkNotNull( + articleName, r'ReviewedItemResponse', 'articleName'), + artikelNr: BuiltValueNullFieldError.checkNotNull( + artikelNr, r'ReviewedItemResponse', 'artikelNr'), + belegzeilenNr: BuiltValueNullFieldError.checkNotNull( + belegzeilenNr, r'ReviewedItemResponse', 'belegzeilenNr'), + creditedQuantity: BuiltValueNullFieldError.checkNotNull( + creditedQuantity, r'ReviewedItemResponse', 'creditedQuantity'), + reason: reason, + requiredQuantity: BuiltValueNullFieldError.checkNotNull( + requiredQuantity, r'ReviewedItemResponse', 'requiredQuantity'), + ); + replace(_$result); + return _$result; + } +} + +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/packages/holzleitner_api/lib/src/model/tour_details.dart b/packages/holzleitner_api/lib/src/model/tour_details.dart index 9378e85..f29ce85 100644 --- a/packages/holzleitner_api/lib/src/model/tour_details.dart +++ b/packages/holzleitner_api/lib/src/model/tour_details.dart @@ -6,6 +6,7 @@ import 'package:holzleitner_api/src/model/delivery_credit.dart'; import 'package:holzleitner_api/src/model/delivery_note.dart'; import 'package:holzleitner_api/src/model/delivery_with_items.dart'; +import 'package:holzleitner_api/src/model/delivery_payment.dart'; import 'package:holzleitner_api/src/model/article.dart'; import 'package:holzleitner_api/src/model/contact_channel.dart'; import 'package:holzleitner_api/src/model/customer.dart'; @@ -33,6 +34,7 @@ part 'tour_details.g.dart'; /// * [deliveries] /// * [deliveryServices] - Pro-Lieferung gesetzte Service-Werte. Join per `delivery_id` + `service_id`. /// * [notes] - Alle Notizen aller Lieferungen dieser Tour, in einer Liste. Die App joint clientseitig per `delivery_id`. Reihenfolge: pro Lieferung aufsteigend nach `created_at`. +/// * [payments] - Jüngste protokollierte Zahlungsabwicklung pro Lieferung (nur Lieferungen mit mindestens einem Eintrag). Join per `delivery_id`. /// * [services] - Aktive Service-Definitionen (Stammdaten) — die App rendert daraus Phase 4. Bewusst hier mitgeliefert, damit die Detailseite alles aus dem Tour-Aggregat hat. /// * [tour] /// * [warehouses] @@ -70,6 +72,10 @@ abstract class TourDetails implements Built { @BuiltValueField(wireName: r'notes') BuiltList get notes; + /// Jüngste protokollierte Zahlungsabwicklung pro Lieferung (nur Lieferungen mit mindestens einem Eintrag). Join per `delivery_id`. + @BuiltValueField(wireName: r'payments') + BuiltList get payments; + /// Aktive Service-Definitionen (Stammdaten) — die App rendert daraus Phase 4. Bewusst hier mitgeliefert, damit die Detailseite alles aus dem Tour-Aggregat hat. @BuiltValueField(wireName: r'services') BuiltList get services; @@ -148,6 +154,11 @@ class _$TourDetailsSerializer implements PrimitiveSerializer { object.notes, specifiedType: const FullType(BuiltList, [FullType(DeliveryNote)]), ); + yield r'payments'; + yield serializers.serialize( + object.payments, + specifiedType: const FullType(BuiltList, [FullType(DeliveryPayment)]), + ); yield r'services'; yield serializers.serialize( object.services, @@ -249,6 +260,13 @@ class _$TourDetailsSerializer implements PrimitiveSerializer { ) as BuiltList; result.notes.replace(valueDes); break; + case r'payments': + final valueDes = serializers.deserialize( + value, + specifiedType: const FullType(BuiltList, [FullType(DeliveryPayment)]), + ) as BuiltList; + result.payments.replace(valueDes); + break; case r'services': final valueDes = serializers.deserialize( value, diff --git a/packages/holzleitner_api/lib/src/model/tour_details.g.dart b/packages/holzleitner_api/lib/src/model/tour_details.g.dart index 7932481..0e41470 100644 --- a/packages/holzleitner_api/lib/src/model/tour_details.g.dart +++ b/packages/holzleitner_api/lib/src/model/tour_details.g.dart @@ -26,6 +26,8 @@ class _$TourDetails extends TourDetails { @override final BuiltList notes; @override + final BuiltList payments; + @override final BuiltList services; @override final Tour tour; @@ -45,6 +47,7 @@ class _$TourDetails extends TourDetails { required this.deliveries, required this.deliveryServices, required this.notes, + required this.payments, required this.services, required this.tour, required this.warehouses}) @@ -69,6 +72,7 @@ class _$TourDetails extends TourDetails { deliveries == other.deliveries && deliveryServices == other.deliveryServices && notes == other.notes && + payments == other.payments && services == other.services && tour == other.tour && warehouses == other.warehouses; @@ -86,6 +90,7 @@ class _$TourDetails extends TourDetails { _$hash = $jc(_$hash, deliveries.hashCode); _$hash = $jc(_$hash, deliveryServices.hashCode); _$hash = $jc(_$hash, notes.hashCode); + _$hash = $jc(_$hash, payments.hashCode); _$hash = $jc(_$hash, services.hashCode); _$hash = $jc(_$hash, tour.hashCode); _$hash = $jc(_$hash, warehouses.hashCode); @@ -105,6 +110,7 @@ class _$TourDetails extends TourDetails { ..add('deliveries', deliveries) ..add('deliveryServices', deliveryServices) ..add('notes', notes) + ..add('payments', payments) ..add('services', services) ..add('tour', tour) ..add('warehouses', warehouses)) @@ -167,6 +173,12 @@ class TourDetailsBuilder implements Builder { _$this._notes ??= ListBuilder(); set notes(ListBuilder? notes) => _$this._notes = notes; + ListBuilder? _payments; + ListBuilder get payments => + _$this._payments ??= ListBuilder(); + set payments(ListBuilder? payments) => + _$this._payments = payments; + ListBuilder? _services; ListBuilder get services => _$this._services ??= ListBuilder(); @@ -198,6 +210,7 @@ class TourDetailsBuilder implements Builder { _deliveries = $v.deliveries.toBuilder(); _deliveryServices = $v.deliveryServices.toBuilder(); _notes = $v.notes.toBuilder(); + _payments = $v.payments.toBuilder(); _services = $v.services.toBuilder(); _tour = $v.tour.toBuilder(); _warehouses = $v.warehouses.toBuilder(); @@ -233,6 +246,7 @@ class TourDetailsBuilder implements Builder { deliveries: deliveries.build(), deliveryServices: deliveryServices.build(), notes: notes.build(), + payments: payments.build(), services: services.build(), tour: tour.build(), warehouses: warehouses.build(), @@ -258,6 +272,8 @@ class TourDetailsBuilder implements Builder { deliveryServices.build(); _$failedField = 'notes'; notes.build(); + _$failedField = 'payments'; + payments.build(); _$failedField = 'services'; services.build(); _$failedField = 'tour'; diff --git a/packages/holzleitner_api/lib/src/serializers.dart b/packages/holzleitner_api/lib/src/serializers.dart index cb6d09a..f0febb6 100644 --- a/packages/holzleitner_api/lib/src/serializers.dart +++ b/packages/holzleitner_api/lib/src/serializers.dart @@ -26,6 +26,8 @@ import 'package:holzleitner_api/src/model/car.dart'; import 'package:holzleitner_api/src/model/car_response.dart'; import 'package:holzleitner_api/src/model/cars_list.dart'; import 'package:holzleitner_api/src/model/complete_delivery_acknowledgements.dart'; +import 'package:holzleitner_api/src/model/completed_deliveries_response.dart'; +import 'package:holzleitner_api/src/model/completed_delivery_item.dart'; import 'package:holzleitner_api/src/model/contact_channel.dart'; import 'package:holzleitner_api/src/model/contact_kind.dart'; import 'package:holzleitner_api/src/model/contact_role.dart'; @@ -42,10 +44,13 @@ import 'package:holzleitner_api/src/model/delivery.dart'; import 'package:holzleitner_api/src/model/delivery_credit.dart'; import 'package:holzleitner_api/src/model/delivery_credit_event_request.dart'; import 'package:holzleitner_api/src/model/delivery_credit_response.dart'; +import 'package:holzleitner_api/src/model/delivery_details.dart'; import 'package:holzleitner_api/src/model/delivery_item.dart'; import 'package:holzleitner_api/src/model/delivery_note.dart'; import 'package:holzleitner_api/src/model/delivery_note_response.dart'; import 'package:holzleitner_api/src/model/delivery_order_entry.dart'; +import 'package:holzleitner_api/src/model/delivery_payment.dart'; +import 'package:holzleitner_api/src/model/delivery_payment_response.dart'; import 'package:holzleitner_api/src/model/delivery_response.dart'; import 'package:holzleitner_api/src/model/delivery_service_response.dart'; import 'package:holzleitner_api/src/model/delivery_service_value.dart'; @@ -58,6 +63,11 @@ import 'package:holzleitner_api/src/model/mark_mail_sent_response.dart'; import 'package:holzleitner_api/src/model/payment_method.dart'; import 'package:holzleitner_api/src/model/payment_method_response.dart'; import 'package:holzleitner_api/src/model/payment_methods_list.dart'; +import 'package:holzleitner_api/src/model/pending_review_response.dart'; +import 'package:holzleitner_api/src/model/positions_modified_response.dart'; +import 'package:holzleitner_api/src/model/record_delivery_payment_request.dart'; +import 'package:holzleitner_api/src/model/resolve_review_request.dart'; +import 'package:holzleitner_api/src/model/reviewed_item_response.dart'; import 'package:holzleitner_api/src/model/scan_event.dart'; import 'package:holzleitner_api/src/model/scan_result.dart'; import 'package:holzleitner_api/src/model/scan_result_status.dart'; @@ -101,6 +111,8 @@ part 'serializers.g.dart'; CarResponse, CarsList, CompleteDeliveryAcknowledgements, + CompletedDeliveriesResponse, + CompletedDeliveryItem, ContactChannel, ContactKind, ContactRole, @@ -117,10 +129,13 @@ part 'serializers.g.dart'; DeliveryCredit, DeliveryCreditEventRequest, DeliveryCreditResponse, + DeliveryDetails, DeliveryItem, DeliveryNote, DeliveryNoteResponse, DeliveryOrderEntry, + DeliveryPayment, + DeliveryPaymentResponse, DeliveryResponse, DeliveryServiceResponse, DeliveryServiceValue, @@ -133,6 +148,11 @@ part 'serializers.g.dart'; PaymentMethod, PaymentMethodResponse, PaymentMethodsList, + PendingReviewResponse, + PositionsModifiedResponse, + RecordDeliveryPaymentRequest, + ResolveReviewRequest, + ReviewedItemResponse, ScanEvent, ScanResult, ScanResultStatus, @@ -162,6 +182,10 @@ part 'serializers.g.dart'; Warehouse, ]) Serializers serializers = (_$serializers.toBuilder() + ..addBuilderFactory( + const FullType(BuiltList, [FullType(PendingReviewResponse)]), + () => ListBuilder(), + ) ..add(Delivery.serializer) ..add(const OneOfSerializer()) ..add(const AnyOfSerializer()) diff --git a/packages/holzleitner_api/lib/src/serializers.g.dart b/packages/holzleitner_api/lib/src/serializers.g.dart index c6ecb24..fe9fde5 100644 --- a/packages/holzleitner_api/lib/src/serializers.g.dart +++ b/packages/holzleitner_api/lib/src/serializers.g.dart @@ -20,6 +20,8 @@ Serializers _$serializers = (Serializers().toBuilder() ..add(CarResponse.serializer) ..add(CarsList.serializer) ..add(CompleteDeliveryAcknowledgements.serializer) + ..add(CompletedDeliveriesResponse.serializer) + ..add(CompletedDeliveryItem.serializer) ..add(ContactChannel.serializer) ..add(ContactKind.serializer) ..add(ContactRole.serializer) @@ -35,10 +37,13 @@ Serializers _$serializers = (Serializers().toBuilder() ..add(DeliveryCredit.serializer) ..add(DeliveryCreditEventRequest.serializer) ..add(DeliveryCreditResponse.serializer) + ..add(DeliveryDetails.serializer) ..add(DeliveryItem.serializer) ..add(DeliveryNote.serializer) ..add(DeliveryNoteResponse.serializer) ..add(DeliveryOrderEntry.serializer) + ..add(DeliveryPayment.serializer) + ..add(DeliveryPaymentResponse.serializer) ..add(DeliveryResponse.serializer) ..add(DeliveryServiceResponse.serializer) ..add(DeliveryServiceValue.serializer) @@ -51,6 +56,11 @@ Serializers _$serializers = (Serializers().toBuilder() ..add(PaymentMethod.serializer) ..add(PaymentMethodResponse.serializer) ..add(PaymentMethodsList.serializer) + ..add(PendingReviewResponse.serializer) + ..add(PositionsModifiedResponse.serializer) + ..add(RecordDeliveryPaymentRequest.serializer) + ..add(ResolveReviewRequest.serializer) + ..add(ReviewedItemResponse.serializer) ..add(ScanEvent.serializer) ..add(ScanResult.serializer) ..add(ScanResultStatus.serializer) @@ -78,6 +88,31 @@ Serializers _$serializers = (Serializers().toBuilder() ..add(UpdatePaymentMethodRequest.serializer) ..add(UpdateServiceRequest.serializer) ..add(Warehouse.serializer) + ..addBuilderFactory( + const FullType(BuiltList, const [const FullType(Article)]), + () => ListBuilder
()) + ..addBuilderFactory( + const FullType(BuiltList, const [const FullType(ContactChannel)]), + () => ListBuilder()) + ..addBuilderFactory( + const FullType(BuiltList, const [const FullType(ContactSource)]), + () => ListBuilder()) + ..addBuilderFactory( + const FullType(BuiltList, const [const FullType(CustomerContact)]), + () => ListBuilder()) + ..addBuilderFactory( + const FullType( + BuiltList, const [const FullType(DeliveryServiceValue)]), + () => ListBuilder()) + ..addBuilderFactory( + const FullType(BuiltList, const [const FullType(DeliveryNote)]), + () => ListBuilder()) + ..addBuilderFactory( + const FullType(BuiltList, const [const FullType(Service)]), + () => ListBuilder()) + ..addBuilderFactory( + const FullType(BuiltList, const [const FullType(Warehouse)]), + () => ListBuilder()) ..addBuilderFactory( const FullType(BuiltList, const [const FullType(Article)]), () => ListBuilder
()) @@ -106,6 +141,9 @@ Serializers _$serializers = (Serializers().toBuilder() ..addBuilderFactory( const FullType(BuiltList, const [const FullType(DeliveryNote)]), () => ListBuilder()) + ..addBuilderFactory( + const FullType(BuiltList, const [const FullType(DeliveryPayment)]), + () => ListBuilder()) ..addBuilderFactory( const FullType(BuiltList, const [const FullType(Service)]), () => ListBuilder()) @@ -115,6 +153,10 @@ Serializers _$serializers = (Serializers().toBuilder() ..addBuilderFactory( const FullType(BuiltList, const [const FullType(Car)]), () => ListBuilder()) + ..addBuilderFactory( + const FullType( + BuiltList, const [const FullType(CompletedDeliveryItem)]), + () => ListBuilder()) ..addBuilderFactory( const FullType(BuiltList, const [const FullType(DeliveryItem)]), () => ListBuilder()) @@ -127,6 +169,10 @@ Serializers _$serializers = (Serializers().toBuilder() ..addBuilderFactory( const FullType(BuiltList, const [const FullType(PaymentMethod)]), () => ListBuilder()) + ..addBuilderFactory( + const FullType( + BuiltList, const [const FullType(ReviewedItemResponse)]), + () => ListBuilder()) ..addBuilderFactory( const FullType(BuiltList, const [const FullType(ScanEvent)]), () => ListBuilder()) diff --git a/packages/holzleitner_api/test/completed_deliveries_response_test.dart b/packages/holzleitner_api/test/completed_deliveries_response_test.dart new file mode 100644 index 0000000..8cde0ae --- /dev/null +++ b/packages/holzleitner_api/test/completed_deliveries_response_test.dart @@ -0,0 +1,35 @@ +import 'package:test/test.dart'; +import 'package:holzleitner_api/holzleitner_api.dart'; + +// tests for CompletedDeliveriesResponse +void main() { + final instance = CompletedDeliveriesResponseBuilder(); + // TODO add properties to the builder and call build() + + group(CompletedDeliveriesResponse, () { + // Anzahl der abgeschlossenen Lieferungen im Bereich. + // int count + test('to test the property `count`', () async { + // TODO + }); + + // Die abgeschlossenen Lieferungen, aufsteigend nach Abschluss-Zeitpunkt. + // BuiltList deliveries + test('to test the property `deliveries`', () async { + // TODO + }); + + // Wirksame untere Grenze (ISO `YYYY-MM-DD`) oder `null` (offen). + // String from + test('to test the property `from`', () async { + // TODO + }); + + // Wirksame obere Grenze (ISO `YYYY-MM-DD`) oder `null` (offen). + // String to + test('to test the property `to`', () async { + // TODO + }); + + }); +} diff --git a/packages/holzleitner_api/test/completed_delivery_item_test.dart b/packages/holzleitner_api/test/completed_delivery_item_test.dart new file mode 100644 index 0000000..a2bd14e --- /dev/null +++ b/packages/holzleitner_api/test/completed_delivery_item_test.dart @@ -0,0 +1,23 @@ +import 'package:test/test.dart'; +import 'package:holzleitner_api/holzleitner_api.dart'; + +// tests for CompletedDeliveryItem +void main() { + final instance = CompletedDeliveryItemBuilder(); + // TODO add properties to the builder and call build() + + group(CompletedDeliveryItem, () { + // ERP-Belegnummer der Lieferung. + // String belegnummer + test('to test the property `belegnummer`', () async { + // TODO + }); + + // `true`, wenn an der Lieferung Positionen verändert wurden — eine Zeile wurde entfernt oder in der Menge reduziert (Stück-Gutschrift) **oder** es liegt eine aktive Geld-Gutschrift vor. + // bool positionsModified + test('to test the property `positionsModified`', () async { + // TODO + }); + + }); +} diff --git a/packages/holzleitner_api/test/delivery_details_test.dart b/packages/holzleitner_api/test/delivery_details_test.dart new file mode 100644 index 0000000..af9a6cc --- /dev/null +++ b/packages/holzleitner_api/test/delivery_details_test.dart @@ -0,0 +1,89 @@ +import 'package:test/test.dart'; +import 'package:holzleitner_api/holzleitner_api.dart'; + +// tests for DeliveryDetails +void main() { + final instance = DeliveryDetailsBuilder(); + // TODO add properties to the builder and call build() + + group(DeliveryDetails, () { + // Nur die von den Positionen referenzierten Artikel. + // BuiltList
articles + test('to test the property `articles`', () async { + // TODO + }); + + // Die zu `contactSources` gehörenden Einzel-Kanäle. Join per `sourceId`. + // BuiltList contactChannels + test('to test the property `contactChannels`', () async { + // TODO + }); + + // Kontaktquellen der Lieferung (Liefer-/Rechnungsadresse, Ansprechpartner …). + // BuiltList contactSources + test('to test the property `contactSources`', () async { + // TODO + }); + + // Aktuelle Betrags-Gutschrift (`null`, wenn keine aktiv). + // DeliveryCredit credit + test('to test the property `credit`', () async { + // TODO + }); + + // Der Kunde der Lieferung (`null`, falls unauffindbar — sollte nicht vorkommen). + // Customer customer + test('to test the property `customer`', () async { + // TODO + }); + + // Ansprechpartner des Kunden. + // BuiltList customerContacts + test('to test the property `customerContacts`', () async { + // TODO + }); + + // Die Lieferung selbst inkl. Positionen (mit Scan-Ständen) und `sortOrder`. + // DeliveryWithItems delivery + test('to test the property `delivery`', () async { + // TODO + }); + + // Für diese Lieferung gesetzte Service-Werte. + // BuiltList deliveryServices + test('to test the property `deliveryServices`', () async { + // TODO + }); + + // Notizen der Lieferung, aufsteigend nach `createdAt`. + // BuiltList notes + test('to test the property `notes`', () async { + // TODO + }); + + // Jüngste protokollierte Zahlungsabwicklung (`None` = keine). + // DeliveryPayment payment + test('to test the property `payment`', () async { + // TODO + }); + + // Aktive Service-Definitionen (Stammdaten). + // BuiltList services + test('to test the property `services`', () async { + // TODO + }); + + // Die Tour, zu der die Lieferung gehört. + // Tour tour + test('to test the property `tour`', () async { + // TODO + }); + + // Nur die von den Positionen referenzierten Lager. + // BuiltList warehouses + test('to test the property `warehouses`', () async { + // TODO + }); + + }); +} diff --git a/packages/holzleitner_api/test/delivery_payment_response_test.dart b/packages/holzleitner_api/test/delivery_payment_response_test.dart new file mode 100644 index 0000000..871353d --- /dev/null +++ b/packages/holzleitner_api/test/delivery_payment_response_test.dart @@ -0,0 +1,16 @@ +import 'package:test/test.dart'; +import 'package:holzleitner_api/holzleitner_api.dart'; + +// tests for DeliveryPaymentResponse +void main() { + final instance = DeliveryPaymentResponseBuilder(); + // TODO add properties to the builder and call build() + + group(DeliveryPaymentResponse, () { + // DeliveryPayment payment + test('to test the property `payment`', () async { + // TODO + }); + + }); +} diff --git a/packages/holzleitner_api/test/delivery_payment_test.dart b/packages/holzleitner_api/test/delivery_payment_test.dart new file mode 100644 index 0000000..3ede518 --- /dev/null +++ b/packages/holzleitner_api/test/delivery_payment_test.dart @@ -0,0 +1,53 @@ +import 'package:test/test.dart'; +import 'package:holzleitner_api/holzleitner_api.dart'; + +// tests for DeliveryPayment +void main() { + final instance = DeliveryPaymentBuilder(); + // TODO add properties to the builder and call build() + + group(DeliveryPayment, () { + // Abgewickelter Betrag in Cent (offener Betrag bei Bestätigung). + // int amountCents + test('to test the property `amountCents`', () async { + // TODO + }); + + // String deliveryId + test('to test the property `deliveryId`', () async { + // TODO + }); + + // String id + test('to test the property `id`', () async { + // TODO + }); + + // Snapshot des Methoden-Codes (`cash`, `ec_card`, `invoice`, …). + // String paymentMethodCode + test('to test the property `paymentMethodCode`', () async { + // TODO + }); + + // String paymentMethodId + test('to test the property `paymentMethodId`', () async { + // TODO + }); + + // DateTime recordedAt + test('to test the property `recordedAt`', () async { + // TODO + }); + + // String recordedByCarId + test('to test the property `recordedByCarId`', () async { + // TODO + }); + + // int recordedByPersonalnummer + test('to test the property `recordedByPersonalnummer`', () async { + // TODO + }); + + }); +} diff --git a/packages/holzleitner_api/test/pending_review_response_test.dart b/packages/holzleitner_api/test/pending_review_response_test.dart new file mode 100644 index 0000000..d80b751 --- /dev/null +++ b/packages/holzleitner_api/test/pending_review_response_test.dart @@ -0,0 +1,60 @@ +import 'package:test/test.dart'; +import 'package:holzleitner_api/holzleitner_api.dart'; + +// tests for PendingReviewResponse +void main() { + final instance = PendingReviewResponseBuilder(); + // TODO add properties to the builder and call build() + + group(PendingReviewResponse, () { + // Entfernte/teil-gutgeschriebene Positionen. + // BuiltList creditedItems + test('to test the property `creditedItems`', () async { + // TODO + }); + + // String customerName + test('to test the property `customerName`', () async { + // TODO + }); + + // String deliveryId + test('to test the property `deliveryId`', () async { + // TODO + }); + + // int erpBelegartId + test('to test the property `erpBelegartId`', () async { + // TODO + }); + + // String erpBelegnummer + test('to test the property `erpBelegnummer`', () async { + // TODO + }); + + // Zeitpunkt der letzten beleg-ändernden Aktion (RFC 3339). + // String lastChangeAt + test('to test the property `lastChangeAt`', () async { + // TODO + }); + + // Geld-Gutschrift in Cent (0 = keine). + // int moneyCreditCents + test('to test the property `moneyCreditCents`', () async { + // TODO + }); + + // String moneyCreditReason + test('to test the property `moneyCreditReason`', () async { + // TODO + }); + + // Tourdatum (ISO `YYYY-MM-DD`). + // String tourDate + test('to test the property `tourDate`', () async { + // TODO + }); + + }); +} diff --git a/packages/holzleitner_api/test/positions_modified_response_test.dart b/packages/holzleitner_api/test/positions_modified_response_test.dart new file mode 100644 index 0000000..2256208 --- /dev/null +++ b/packages/holzleitner_api/test/positions_modified_response_test.dart @@ -0,0 +1,23 @@ +import 'package:test/test.dart'; +import 'package:holzleitner_api/holzleitner_api.dart'; + +// tests for PositionsModifiedResponse +void main() { + final instance = PositionsModifiedResponseBuilder(); + // TODO add properties to the builder and call build() + + group(PositionsModifiedResponse, () { + // ERP-Belegnummer, nach der gefragt wurde. + // String belegnummer + test('to test the property `belegnummer`', () async { + // TODO + }); + + // `true`, wenn an der Lieferung Positionen verändert wurden — eine Zeile wurde entfernt oder in der Menge reduziert (Stück-Gutschrift) **oder** es liegt eine aktive Geld-Gutschrift vor. + // bool positionsModified + test('to test the property `positionsModified`', () async { + // TODO + }); + + }); +} diff --git a/packages/holzleitner_api/test/record_delivery_payment_request_test.dart b/packages/holzleitner_api/test/record_delivery_payment_request_test.dart new file mode 100644 index 0000000..635e7d6 --- /dev/null +++ b/packages/holzleitner_api/test/record_delivery_payment_request_test.dart @@ -0,0 +1,35 @@ +import 'package:test/test.dart'; +import 'package:holzleitner_api/holzleitner_api.dart'; + +// tests for RecordDeliveryPaymentRequest +void main() { + final instance = RecordDeliveryPaymentRequestBuilder(); + // TODO add properties to the builder and call build() + + group(RecordDeliveryPaymentRequest, () { + // Fahrzeug des Akteurs (Audit-Spur). Muss zum Account gehören. + // String authorCarId + test('to test the property `authorCarId`', () async { + // TODO + }); + + // Idempotenz-Schlüssel — pro Bestätigung genau einmal vergeben. + // String clientEventId + test('to test the property `clientEventId`', () async { + // TODO + }); + + // Betrag in Cent, den der Fahrer in der App gesehen und bestätigt hat. Der Server berechnet den offenen Betrag selbst und lehnt ab, wenn er davon abweicht — so landet nie ein Betrag im Protokoll, den der Fahrer nicht gesehen hat. + // int expectedAmountCents + test('to test the property `expectedAmountCents`', () async { + // TODO + }); + + // Gewählte Zahlungsmethode. Muss existieren und aktiv sein. + // String paymentMethodId + test('to test the property `paymentMethodId`', () async { + // TODO + }); + + }); +} diff --git a/packages/holzleitner_api/test/resolve_review_request_test.dart b/packages/holzleitner_api/test/resolve_review_request_test.dart new file mode 100644 index 0000000..2af19a8 --- /dev/null +++ b/packages/holzleitner_api/test/resolve_review_request_test.dart @@ -0,0 +1,23 @@ +import 'package:test/test.dart'; +import 'package:holzleitner_api/holzleitner_api.dart'; + +// tests for ResolveReviewRequest +void main() { + final instance = ResolveReviewRequestBuilder(); + // TODO add properties to the builder and call build() + + group(ResolveReviewRequest, () { + // Optionale Notiz zur getroffenen Entscheidung. + // String note + test('to test the property `note`', () async { + // TODO + }); + + // Bearbeiter (Name/Kürzel), der die Prüfung bestätigt. + // String resolvedBy + test('to test the property `resolvedBy`', () async { + // TODO + }); + + }); +} diff --git a/packages/holzleitner_api/test/reviewed_item_response_test.dart b/packages/holzleitner_api/test/reviewed_item_response_test.dart new file mode 100644 index 0000000..90a18ce --- /dev/null +++ b/packages/holzleitner_api/test/reviewed_item_response_test.dart @@ -0,0 +1,41 @@ +import 'package:test/test.dart'; +import 'package:holzleitner_api/holzleitner_api.dart'; + +// tests for ReviewedItemResponse +void main() { + final instance = ReviewedItemResponseBuilder(); + // TODO add properties to the builder and call build() + + group(ReviewedItemResponse, () { + // String articleName + test('to test the property `articleName`', () async { + // TODO + }); + + // String artikelNr + test('to test the property `artikelNr`', () async { + // TODO + }); + + // int belegzeilenNr + test('to test the property `belegzeilenNr`', () async { + // TODO + }); + + // int creditedQuantity + test('to test the property `creditedQuantity`', () async { + // TODO + }); + + // String reason + test('to test the property `reason`', () async { + // TODO + }); + + // int requiredQuantity + test('to test the property `requiredQuantity`', () async { + // TODO + }); + + }); +} diff --git a/test/payment_step_test.dart b/test/payment_step_test.dart new file mode 100644 index 0000000..78abfd2 --- /dev/null +++ b/test/payment_step_test.dart @@ -0,0 +1,278 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:hl_lieferservice/domain/entity/address.dart'; +import 'package:hl_lieferservice/domain/entity/customer.dart'; +import 'package:hl_lieferservice/domain/entity/delivery.dart'; +import 'package:hl_lieferservice/domain/entity/delivery_credit.dart'; +import 'package:hl_lieferservice/domain/entity/delivery_item.dart'; +import 'package:hl_lieferservice/domain/entity/delivery_payment.dart'; +import 'package:hl_lieferservice/domain/entity/payment_method.dart'; +import 'package:hl_lieferservice/domain/entity/scan_progress.dart'; +import 'package:hl_lieferservice/domain/entity/tour.dart'; +import 'package:hl_lieferservice/domain/entity/tour_details.dart'; +import 'package:hl_lieferservice/domain/repository/payment_methods_repository.dart'; +import 'package:hl_lieferservice/feature/delivery/detail/model/delivery_payment_status.dart'; +import 'package:hl_lieferservice/feature/delivery/detail/presentation/widget/payment_sheet.dart'; +import 'package:hl_lieferservice/feature/payment_methods/bloc/payment_methods_cubit.dart'; + +const _address = Address( + street: 'Konstantinplatz', + houseNumber: '3', + postalCode: '41849', + city: 'Wassenberg', + country: 'DE', +); + +final _methods = [ + PaymentMethod( + id: 'm-cash', + code: 'cash', + name: 'Bar', + active: true, + createdAt: DateTime(2026), + ), + PaymentMethod( + id: 'm-ec', + code: 'ec_card', + name: 'EC-Karte', + active: true, + createdAt: DateTime(2026), + ), + PaymentMethod( + id: 'm-inv', + code: 'invoice', + name: 'Rechnung', + active: true, + createdAt: DateTime(2026), + ), +]; + +/// Lieferung: 2 × 50 € Warenwert, 20 € angezahlt ⇒ 80 € offen. +Delivery _delivery({double prepaid = 20}) => Delivery( + id: 'd1', + tourId: 't1', + customerId: 'c1', + contactPersonIds: const [], + deliveryAddressSnapshot: _address, + erpBelegartId: 24, + erpBelegnummer: 'V-1', + state: DeliveryState.active, + sortOrder: 1, + prepaidAmount: prepaid, + paymentMethodId: 'm-cash', + items: [ + DeliveryItem( + id: 'i1', + deliveryId: 'd1', + articleId: 'a1', + warehouseId: 'w1', + belegzeilenNr: 1, + requiredQuantity: 2, + unitPrice: 50, + scanProgress: ScanProgress( + status: ScanStatus.done, + scannedQuantity: 2, + lastUpdatedAt: DateTime(2026), + ), + ), + ], +); + +TourDetails _details( + Delivery delivery, { + DeliveryPayment? payment, + DeliveryCredit? credit, +}) => TourDetails( + tour: Tour( + id: 't1', + accountId: 414, + date: DateTime(2026, 9, 25), + syncedAt: DateTime(2026), + ), + deliveries: [delivery], + customers: const { + 'c1': Customer( + id: 'c1', + name: 'Cremer', + erpCustomerId: 7843068, + address: _address, + ), + }, + contacts: const {}, + articles: const {}, + warehouses: const {}, + notesByDeliveryId: const {}, + creditsByDeliveryId: {if (credit != null) 'd1': credit}, + paymentsByDeliveryId: {if (payment != null) 'd1': payment}, + services: const [], + serviceValuesByDeliveryId: const {}, + contactSourcesByDeliveryId: const {}, + contactChannelsBySourceId: const {}, +); + +DeliveryPayment _payment(int cents, {String code = 'ec_card'}) => + DeliveryPayment( + id: 'p1', + deliveryId: 'd1', + paymentMethodId: code == 'ec_card' ? 'm-ec' : 'm-cash', + paymentMethodCode: code, + amountCents: cents, + recordedAt: DateTime(2026, 9, 25, 13, 5), + ); + +class _FakePaymentMethods implements PaymentMethodsRepository { + @override + Future> list({bool includeInactive = false}) async => + _methods; + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +void main() { + group('DeliveryPaymentStatus', () { + test('offener Betrag ohne Zahlung ist nicht erledigt', () { + final d = _delivery(); + final s = DeliveryPaymentStatus.of(d, _details(d)); + expect(s.openCents, 8000); + expect(s.isSettled, isFalse); + expect(s.isStale, isFalse); + }); + + test('passende Zahlung erledigt den Step', () { + final d = _delivery(); + final s = DeliveryPaymentStatus.of( + d, + _details(d, payment: _payment(8000)), + ); + expect(s.validPayment?.id, 'p1'); + expect(s.isSettled, isTrue); + }); + + test('Gutschrift nach Zahlung macht sie veraltet', () { + final d = _delivery(); + final s = DeliveryPaymentStatus.of( + d, + _details( + d, + payment: _payment(8000), + credit: const DeliveryCredit( + deliveryId: 'd1', + amountCents: 1000, + reason: 'Kratzer', + ), + ), + ); + expect(s.openCents, 7000); + expect(s.isStale, isTrue); + expect(s.isSettled, isFalse); + }); + + test('bereits bezahlt: nichts offen, Step überspringbar', () { + final d = _delivery(prepaid: 100); + final s = DeliveryPaymentStatus.of(d, _details(d)); + expect(s.hasOpenAmount, isFalse); + expect(s.isSettled, isTrue); + }); + }); + + group('Zahlungs-Modal', () { + late PaymentSheetResult? result; + + Future openSheet(WidgetTester tester) async { + result = null; + final cubit = PaymentMethodsCubit(repository: _FakePaymentMethods()); + await cubit.load(); + await tester.pumpWidget( + BlocProvider.value( + value: cubit, + child: MaterialApp( + home: Builder( + builder: + (context) => Scaffold( + body: TextButton( + onPressed: () async { + result = await showPaymentSheet( + context, + openCents: 8000, + customerNumber: 7843068, + preselectedMethodId: 'm-cash', + ); + }, + child: const Text('open'), + ), + ), + ), + ), + ), + ); + await tester.tap(find.text('open')); + await tester.pumpAndSettle(); + } + + testWidgets('zeigt Betrag, bei EC die Kundennummer, bestätigt Methode', ( + tester, + ) async { + tester.view.physicalSize = const Size(1080, 2400); + tester.view.devicePixelRatio = 2.75; + addTearDown(tester.view.reset); + await openSheet(tester); + + expect(find.textContaining('80,00'), findsOneWidget); + expect(find.text('Bar'), findsOneWidget); + expect(find.text('EC-Karte'), findsOneWidget); + expect(find.text('Rechnung'), findsOneWidget); + expect(find.text('7843068'), findsNothing); + + await tester.tap(find.text('EC-Karte')); + await tester.pumpAndSettle(); + expect(find.text('7843068'), findsOneWidget); + + await tester.tap(find.text('Zahlung erhalten')); + await tester.pumpAndSettle(); + expect(result, isA()); + expect((result as PaymentConfirmed).method.code, 'ec_card'); + }); + + testWidgets('Rechnung ändert den Bestätigungs-Text', (tester) async { + tester.view.physicalSize = const Size(1080, 2400); + tester.view.devicePixelRatio = 2.75; + addTearDown(tester.view.reset); + await openSheet(tester); + + await tester.tap(find.text('Rechnung')); + await tester.pumpAndSettle(); + expect(find.text('Auf Rechnung bestätigen'), findsOneWidget); + }); + + testWidgets('Abbrechen schließt ohne Ergebnis', (tester) async { + tester.view.physicalSize = const Size(1080, 2400); + tester.view.devicePixelRatio = 2.75; + addTearDown(tester.view.reset); + await openSheet(tester); + + await tester.tap(find.text('Abbrechen')); + await tester.pumpAndSettle(); + expect(result, isNull); + expect(find.text('Zahlung abwickeln'), findsNothing); + }); + + testWidgets('Lieferung abbrechen fragt Grund ab', (tester) async { + tester.view.physicalSize = const Size(1080, 2400); + tester.view.devicePixelRatio = 2.75; + addTearDown(tester.view.reset); + await openSheet(tester); + + await tester.tap(find.text('Lieferung abbrechen')); + await tester.pumpAndSettle(); + await tester.enterText(find.byType(TextField), 'Kunde zahlt nicht'); + await tester.tap(find.text('Bestätigen')); + await tester.pumpAndSettle(); + + expect(result, isA()); + expect((result as DeliveryCancelRequested).reason, 'Kunde zahlt nicht'); + }); + }); +}