Files
Holzleitner-Lieferservice-App/lib/feature/delivery/bloc/tour_event.dart
Dennis Nemec dd7fe9a8eb 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 <noreply@anthropic.com>
2026-09-25 14:04:36 +02:00

376 lines
12 KiB
Dart

/// Events für den [`TourBloc`].
///
/// Bewusst minimaler Scope für Phase C+D-2: Lese-Pfad, Reorder,
/// Car-Assign. Scan-, Hold-, Cancel-, Complete-, Discount- und
/// Notiz-Events kommen in C+D-3 / C+D-4 zurück.
sealed class TourEvent {
const TourEvent();
}
/// Lädt die Tour des angemeldeten Fahrers. Account-Filter sitzt im JWT.
///
/// Zwei Spielarten:
/// * [LoadTour] (ohne Datum): **Reload/Retry** — lädt den aktuell im Bloc
/// gemerkten Tag erneut und ändert die Datumswahl NICHT. Für Retry-Buttons
/// und Fehler-Seiten, die sonst versehentlich auf „heute" zurückspringen.
/// * [LoadTour.forDate]: setzt gezielt ein Datum (`null` = heute) und merkt
/// es sich als aktuelles Datum für nachfolgende Reloads/Refreshes. Für den
/// Kopf-Kalender und den Initial-Load (aus dem [TourDateCubit]).
class LoadTour extends TourEvent {
const LoadTour()
: date = null,
hasDate = false;
const LoadTour.forDate(this.date) : hasDate = true;
/// Ziel-Datum, nur relevant wenn [hasDate] `true` ist. `null` = heute.
final DateTime? date;
/// `true` = [date] wurde bewusst gesetzt; `false` = „aktuelles Datum
/// behalten" (reiner Reload).
final bool hasDate;
}
/// Pull-to-refresh / manueller Reload aus der Übersicht. Vorhandener
/// `TourLoaded`-State bleibt sichtbar, bis der neue Snapshot da ist;
/// `TourLoading` wird nur emittiert, wenn vorher kein State existierte.
class RefreshTour extends TourEvent {
const RefreshTour();
}
/// Schreibt die Sortier-Reihenfolge aller Lieferungen einer Tour an das
/// Backend. Der Bloc fordert die UI nicht auf, die vollständige Reihenfolge
/// zu kennen — sie wird aus der Liste übergeben.
class ReorderDeliveries extends TourEvent {
const ReorderDeliveries({required this.orderedDeliveryIds});
final List<String> orderedDeliveryIds;
}
/// Weist einer Lieferung ein Fahrzeug zu — oder hebt die Zuweisung auf
/// (`carId == null`).
class AssignCarToDelivery extends TourEvent {
const AssignCarToDelivery({required this.deliveryId, required this.carId});
final String deliveryId;
final String? carId;
}
/// Bulk-Variante: weist mehreren Lieferungen in einer atomaren Aktion das
/// gleiche Fahrzeug zu. Notwendig, weil flutter_bloc Events standardmäßig
/// **concurrent** verarbeitet — N parallel laufende `AssignCarToDelivery`-
/// Handler lesen alle den Initial-State und überschreiben sich gegenseitig
/// beim `emit`, sodass nur die letzte Zuweisung sichtbar bleibt. Der
/// Bulk-Handler verarbeitet die Liste sequenziell und emittiert genau
/// einen Zwischen-State am Ende.
///
/// Backend-seitig macht der Handler trotzdem N einzelne HTTP-Calls, weil
/// der `PATCH /deliveries/{id}/assigned-car`-Endpoint keine Liste kennt.
class AssignCarToDeliveries extends TourEvent {
const AssignCarToDeliveries({
required this.deliveryIds,
required this.carId,
});
final List<String> deliveryIds;
final String? carId;
}
/// Scan-Trigger aus der Loading-Phase. Der Bloc inkrementiert lokal
/// **optimistisch** die Scan-Quantität, ruft anschließend `POST /scans`
/// und rollt im `rejected`-Fall lokal zurück. `duplicate` ist still
/// (lokal schon hochgezählt, Server bestätigt nicht erneut).
///
/// `actorCarId` ist die UUID des aktuell gewählten Fahrzeugs — das
/// Backend nutzt das nur als Audit-Spur; an der Lieferung selbst
/// passiert dadurch nichts.
class ScanItem extends TourEvent {
const ScanItem({
required this.deliveryItemId,
required this.actorCarId,
this.manual = false,
});
final String deliveryItemId;
final String actorCarId;
/// `true` = manuelle Zeilen-Bestätigung (Fallback ohne Barcode): die
/// **gesamte Restmenge** wird auf einmal als geladen verbucht und im
/// Backend als `manual` protokolliert. `false` = regulärer Einzel-Scan (+1).
final bool manual;
}
/// Umkehrung eines Scans (z. B. „falsch gescannt"). `reason` ist vom
/// Backend für `unscan` erforderlich.
class UnscanItem extends TourEvent {
const UnscanItem({
required this.deliveryItemId,
required this.actorCarId,
required this.reason,
});
final String deliveryItemId;
final String actorCarId;
final String reason;
}
/// Pausiert ein Item (`scan_status=held`). Reversibel über [UnholdItem].
/// `reason` Pflicht.
class HoldItem extends TourEvent {
const HoldItem({
required this.deliveryItemId,
required this.actorCarId,
required this.reason,
});
final String deliveryItemId;
final String actorCarId;
final String reason;
}
/// Setzt ein pausiertes Item zurück auf `in_progress`. Kein Reason
/// nötig — der Server löscht beim Unhold den `held_reason`.
class UnholdItem extends TourEvent {
const UnholdItem({
required this.deliveryItemId,
required this.actorCarId,
});
final String deliveryItemId;
final String actorCarId;
}
/// Entfernt ein Item aus der Lieferung (`scan_status=removed`).
/// `reason` Pflicht. Reversibel über [UnremoveItem] — die Historie
/// bleibt im Audit-Log vollständig erhalten.
class RemoveItem extends TourEvent {
const RemoveItem({
required this.deliveryItemId,
required this.actorCarId,
required this.reason,
this.quantity,
this.saveReasonAsNote = false,
});
final String deliveryItemId;
final String actorCarId;
final String reason;
/// Mengen-Gutschrift: wie viele Stück gutgeschrieben werden. `null` =
/// ganze Restmenge (= klassisches „ganze Zeile entfernen").
final int? quantity;
/// Wenn `true`, wird der `reason` nach erfolgreichem Entfernen zusätzlich
/// als Lieferungs-Notiz gespeichert. Gesetzt aus dem Gutschrift-Flow
/// (Step „Artikel & Gutschriften"); im Beladen-Flow `false`, dort wäre
/// eine Notiz pro Abbuchung nur Rauschen.
final bool saveReasonAsNote;
}
/// Stellt ein entferntes Item wieder her — geht nur, wenn der aktuelle
/// Status `removed` ist. Kein Reason nötig (Backend-Konvention wie bei
/// `unscan` und `unhold`). Audit-Log behält den ursprünglichen
/// `remove`-Eintrag und fügt einen neuen `unremove`-Eintrag hinzu.
class UnremoveItem extends TourEvent {
const UnremoveItem({
required this.deliveryItemId,
required this.actorCarId,
this.quantity,
});
final String deliveryItemId;
final String actorCarId;
/// Mengen-Gutschrift zurücknehmen: wie viele Stück wiederhergestellt
/// werden. `null` = gesamte Gutschrift zurück.
final int? quantity;
}
// ─── Delivery-Lifecycle ───────────────────────────────────────────────
/// Bricht eine Lieferung endgültig ab. `reason` ist Pflicht.
class CancelDelivery extends TourEvent {
const CancelDelivery({required this.deliveryId, required this.reason});
final String deliveryId;
final String reason;
}
/// Pausiert eine Lieferung. `reason` ist Pflicht. Reversibel über
/// [ResumeDelivery].
class HoldDelivery extends TourEvent {
const HoldDelivery({required this.deliveryId, required this.reason});
final String deliveryId;
final String reason;
}
/// Setzt eine pausierte Lieferung zurück auf `active`.
class ResumeDelivery extends TourEvent {
const ResumeDelivery({required this.deliveryId});
final String deliveryId;
}
/// Schließt eine Lieferung ab: lädt beide Unterschriften hoch, dokumentiert
/// die Bestätigungen des Kunden und setzt die Lieferung auf `completed`.
class CompleteDelivery extends TourEvent {
const CompleteDelivery({
required this.deliveryId,
required this.customerSignaturePng,
required this.driverSignaturePng,
required this.receiptConfirmed,
required this.notesAcknowledged,
required this.acknowledgedNoteIds,
this.paymentMethodId,
this.actorCarId,
this.paymentCollected = false,
});
final String deliveryId;
final List<int> customerSignaturePng;
final List<int> driverSignaturePng;
final bool receiptConfirmed;
final bool notesAcknowledged;
final List<String> acknowledgedNoteIds;
/// Vom Fahrer im Summary gewählte Zahlungsmethode (Override). `null` =
/// die am Beleg hinterlegte Methode bleibt. Wird beim Abschluss persistiert.
final String? paymentMethodId;
final String? actorCarId;
/// Fahrer hat das Vor-Ort-Inkasso (Bar/EC) des offenen Betrags bestätigt.
/// `false`, wenn kein Inkasso anfiel (offen == 0 oder „Auf Rechnung").
final bool paymentCollected;
}
/// Legt eine neue (Text- oder Bild-)Notiz an einer Lieferung an. Aktuell
/// wird nur der Text-Pfad von der UI getriggert; `imageAttachment` ist als
/// Storage-Key (z. B. Pre-Signed-URL-Key) gedacht und wartet auf die
/// zukünftige Foto-Upload-Phase.
/// Setzt/ändert die Betrags-Gutschrift einer Lieferung (Geld-Nachlass).
class SetDeliveryCredit extends TourEvent {
const SetDeliveryCredit({
required this.deliveryId,
required this.amountCents,
required this.reason,
required this.actorCarId,
});
final String deliveryId;
final int amountCents;
final String reason;
final String actorCarId;
}
/// Entfernt die Betrags-Gutschrift einer Lieferung.
class RemoveDeliveryCredit extends TourEvent {
const RemoveDeliveryCredit({
required this.deliveryId,
required this.actorCarId,
});
final String deliveryId;
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({
required this.deliveryId,
required this.serviceId,
required this.actorCarId,
this.boolValue,
this.numericValue,
});
final String deliveryId;
final String serviceId;
final String actorCarId;
final bool? boolValue;
final int? numericValue;
}
/// Entfernt den Service-Wert einer Lieferung (Service „nicht gesetzt").
class RemoveDeliveryServiceValue extends TourEvent {
const RemoveDeliveryServiceValue({
required this.deliveryId,
required this.serviceId,
});
final String deliveryId;
final String serviceId;
}
class AddDeliveryNote extends TourEvent {
const AddDeliveryNote({
required this.deliveryId,
this.text,
this.imageAttachment,
this.creditDeliveryItemId,
});
final String deliveryId;
final String? text;
final String? imageAttachment;
/// Optionaler Gutschrift-Bezug (DeliveryItem-Id). Gesetzt, wenn die Notiz
/// einen Gutschrift-Grund dokumentiert — ermöglicht das Löschen beim
/// Unremove.
final String? creditDeliveryItemId;
}
/// Ändert Text/Bild einer bestehenden Notiz.
class UpdateDeliveryNote extends TourEvent {
const UpdateDeliveryNote({
required this.deliveryId,
required this.noteId,
this.text,
this.imageAttachment,
});
final String deliveryId;
final String noteId;
final String? text;
final String? imageAttachment;
}
/// Löscht eine Notiz.
class DeleteDeliveryNote extends TourEvent {
const DeleteDeliveryNote({required this.deliveryId, required this.noteId});
final String deliveryId;
final String noteId;
}
/// Lädt ein Bild als Notiz hoch (geht über DOCUframe).
class UploadDeliveryNoteImage extends TourEvent {
const UploadDeliveryNoteImage({
required this.deliveryId,
required this.filename,
required this.mime,
required this.bytes,
});
final String deliveryId;
final String filename;
final String mime;
final List<int> bytes;
}