Final commit.
This commit is contained in:
@ -150,6 +150,41 @@ Smoke: Login → `loadTourOfToday()` → Tour kommt durch.
|
|||||||
|
|
||||||
Smoke: Tab-Navigation funktioniert, jeder Tab lädt ohne Crash.
|
Smoke: Tab-Navigation funktioniert, jeder Tab lädt ohne Crash.
|
||||||
|
|
||||||
|
### Phase G — Delivery-Lifecycle-Audit-Log (offen)
|
||||||
|
|
||||||
|
**Status:** geplant, noch nicht begonnen. Aus der Diskussion zur
|
||||||
|
Wiederherstellung abgebrochener Lieferungen (Phase C+D-4): heute
|
||||||
|
gehen `state_reason`-Begründungen beim `resume` verloren, weil das
|
||||||
|
Feld direkt an der `deliveries`-Zeile lebt und beim Wiederherstellen
|
||||||
|
genullt wird. Item-Aktionen sind sauber auditierbar (`scan_audit`,
|
||||||
|
append-only) — Delivery-Lifecycle ist es nicht.
|
||||||
|
|
||||||
|
**Scope:**
|
||||||
|
1. Neue Tabelle `delivery_audit` analog zu `scan_audit`:
|
||||||
|
- `id`, `delivery_id`, `client_action_id` (UUID UNIQUE — Idempotenz)
|
||||||
|
- `action` (`hold`|`resume`|`cancel`|`complete`)
|
||||||
|
- `previous_state`, `resulting_state`
|
||||||
|
- `reason` (Pflicht bei `hold` / `cancel`)
|
||||||
|
- `actor_personalnummer`, `actor_car_id?`
|
||||||
|
- `client_acted_at`, `server_recorded_at`
|
||||||
|
- denormalisierter ERP-Bezug: `erp_belegart_id`, `erp_belegnummer`
|
||||||
|
2. Backend: Audit-Insert in jedem `apply_action`-Pfad
|
||||||
|
(`delivery_repository.rs`). Request-DTOs bekommen Pflichtfelder
|
||||||
|
`clientActionId` + `clientActedAt`.
|
||||||
|
3. OpenAPI + Dart-Client neu generieren.
|
||||||
|
4. App-Bloc: UUID + Timestamp pro Lifecycle-Event mitsenden
|
||||||
|
(Helper-Funktion analog zur Scan-Pipeline, dort sitzt das Pattern
|
||||||
|
schon im `TourBloc`).
|
||||||
|
5. **Optional, zweiter Schritt:** `GET /deliveries/{id}/audit`
|
||||||
|
plus UI-Anzeige der Historie. Sinnvollste Stelle: im
|
||||||
|
„Wiederherstellen"-Dialog vom Cancel-Recovery zeigen wir den
|
||||||
|
ursprünglichen Eintrag („Wurde am … durch … abgebrochen mit
|
||||||
|
Grund: …"), damit der Fahrer eine bewusste Entscheidung trifft.
|
||||||
|
|
||||||
|
**Out of Scope dieser Phase:**
|
||||||
|
- `assignCar`-Audit (andere Geste, kein Reason, eigene Sub-Phase).
|
||||||
|
- Notizen-Audit (Notes sind schon append-only — separate Aktivität).
|
||||||
|
|
||||||
### Phase F — Smoke des kompletten Flows
|
### Phase F — Smoke des kompletten Flows
|
||||||
Manuell durchklicken:
|
Manuell durchklicken:
|
||||||
1. Login (Keycloak)
|
1. Login (Keycloak)
|
||||||
|
|||||||
@ -1,43 +1,20 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:hl_lieferservice/bloc/app_events.dart';
|
import 'package:hl_lieferservice/bloc/app_events.dart';
|
||||||
import 'package:hl_lieferservice/bloc/app_states.dart';
|
import 'package:hl_lieferservice/bloc/app_states.dart';
|
||||||
import 'package:hl_lieferservice/main.dart';
|
|
||||||
import 'package:hl_lieferservice/repository/config.dart';
|
|
||||||
|
|
||||||
import '../services/erpframe.dart';
|
|
||||||
|
|
||||||
|
/// App-Bootstrap-Bloc.
|
||||||
|
///
|
||||||
|
/// Vor der Backend-Migration lud dieser Bloc eine `hl_server_config.json` aus
|
||||||
|
/// assets, parste daraus eine `backendUrl` und persistierte sie ins Dateisystem.
|
||||||
|
/// Mit dem Wechsel auf das Rust-Backend kommt die URL über `BackendConfig`
|
||||||
|
/// (compile-time, siehe `data/network/backend_config.dart`); der App-Bloc
|
||||||
|
/// emittiert jetzt nur noch sofort `AppConfigLoaded`, damit die UI ihre
|
||||||
|
/// üblichen Phasen-Übergänge behält.
|
||||||
class AppBloc extends Bloc<AppEvents, AppState> {
|
class AppBloc extends Bloc<AppEvents, AppState> {
|
||||||
AppBloc() : super(AppInitial()) {
|
AppBloc() : super(const AppInitial()) {
|
||||||
on<AppLoadConfig>(_loadConfig);
|
on<AppLoadConfig>((event, emit) {
|
||||||
}
|
emit(const AppConfigLoading());
|
||||||
|
emit(const AppConfigLoaded());
|
||||||
Future<void> _loadConfig(AppLoadConfig event, Emitter<AppState> emit) async {
|
});
|
||||||
emit(AppConfigLoading());
|
|
||||||
try {
|
|
||||||
final repository = ConfigurationRepository(path: event.path);
|
|
||||||
final configuration = LocalDocuFrameConfiguration.fromJson(
|
|
||||||
json.decode(await rootBundle.loadString("assets/${event.path}")),
|
|
||||||
);
|
|
||||||
|
|
||||||
repository.setDocuFrameConfiguration(configuration);
|
|
||||||
|
|
||||||
var config = await repository.getDocuFrameConfiguration();
|
|
||||||
locator.registerSingleton<LocalDocuFrameConfiguration>(config);
|
|
||||||
|
|
||||||
emit(AppConfigLoaded(config: config));
|
|
||||||
} catch (e, st) {
|
|
||||||
debugPrint(e.toString());
|
|
||||||
debugPrint(st.toString());
|
|
||||||
|
|
||||||
emit(
|
|
||||||
AppConfigLoadingFailed(
|
|
||||||
message: "Fehler beim Laden der Konfigurationsdatei.",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,27 @@
|
|||||||
import '../services/erpframe.dart';
|
/// Lifecycle-States des App-Bootstraps.
|
||||||
|
///
|
||||||
|
/// Die alte `LocalDocuFrameConfiguration` mit `backendUrl` ist mit der
|
||||||
|
/// Backend-Migration entfallen — die App-Konfiguration kommt jetzt aus
|
||||||
|
/// `BackendConfig` (compile-time) und nicht mehr aus einer asset-JSON.
|
||||||
|
/// Das `AppConfigLoaded`-Signal bleibt als Marker, dass der App-Bootstrap
|
||||||
|
/// abgeschlossen ist (Networking ist registriert, Token-Provider steht).
|
||||||
|
abstract class AppState {
|
||||||
|
const AppState();
|
||||||
|
}
|
||||||
|
|
||||||
abstract class AppState {}
|
class AppInitial extends AppState {
|
||||||
|
const AppInitial();
|
||||||
|
}
|
||||||
|
|
||||||
|
class AppConfigLoading extends AppState {
|
||||||
|
const AppConfigLoading();
|
||||||
|
}
|
||||||
|
|
||||||
class AppInitial extends AppState {}
|
|
||||||
class AppConfigLoading extends AppState {}
|
|
||||||
class AppConfigLoaded extends AppState {
|
class AppConfigLoaded extends AppState {
|
||||||
LocalDocuFrameConfiguration config;
|
const AppConfigLoaded();
|
||||||
|
|
||||||
AppConfigLoaded({required this.config});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class AppConfigLoadingFailed extends AppState {
|
class AppConfigLoadingFailed extends AppState {
|
||||||
String message;
|
const AppConfigLoadingFailed({required this.message});
|
||||||
|
final String message;
|
||||||
AppConfigLoadingFailed({required this.message});
|
|
||||||
}
|
}
|
||||||
162
lib/data/cache/attachment_cache.dart
vendored
Normal file
162
lib/data/cache/attachment_cache.dart
vendored
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
|
/// Persistenter Datei-Cache für heruntergeladene Attachment-Vorschauen.
|
||||||
|
///
|
||||||
|
/// **Warum überhaupt ein Cache:** Vorschaubilder werden über
|
||||||
|
/// `GET /attachments/{id}` aus DOCUframe gerendert — das kostet Zeit und
|
||||||
|
/// Bandbreite und funktioniert offline gar nicht. Einmal geholte Varianten
|
||||||
|
/// landen deshalb auf der Platte und werden danach lokal bedient.
|
||||||
|
///
|
||||||
|
/// **Warum keine Content-Revalidierung:** Attachments sind unveränderlich.
|
||||||
|
/// Ein hochgeladenes Bild (DOCUframe-Objekt) ändert seinen Inhalt nie. Eine
|
||||||
|
/// einmal gecachte Variante ist daher dauerhaft gültig — kein ETag, kein
|
||||||
|
/// If-None-Match, kein HEAD nötig. Das Einzige, was den Cache betrifft, ist
|
||||||
|
/// das **Löschen** eines Attachments; dafür gibt es [retainOnly], das den
|
||||||
|
/// Cache auf die Menge noch gültiger Attachment-IDs eindampft.
|
||||||
|
///
|
||||||
|
/// **Datei-Layout:** Pro Attachment können mehrere *Varianten* liegen
|
||||||
|
/// (Thumbnail 600×600, Vollbild 2048×2048, …). Der Dateiname kodiert ID und
|
||||||
|
/// Render-Parameter:
|
||||||
|
///
|
||||||
|
/// `{attachmentId}__{w}x{h}_q{q}_{ext}`
|
||||||
|
///
|
||||||
|
/// Die ID steht vorne und ist als UUID frei von `__`, sodass [retainOnly] sie
|
||||||
|
/// zuverlässig wieder herausschneiden kann.
|
||||||
|
///
|
||||||
|
/// Der Cache ist durchweg **best-effort**: Lese-/Schreib-/Lösch-Fehler werden
|
||||||
|
/// geschluckt und führen höchstens zu einem erneuten Download, nie zu einem
|
||||||
|
/// Crash.
|
||||||
|
class AttachmentCache {
|
||||||
|
AttachmentCache();
|
||||||
|
|
||||||
|
static const _subdir = 'attachment_previews';
|
||||||
|
static const _separator = '__';
|
||||||
|
|
||||||
|
/// Einmal aufgelöstes Verzeichnis — `getApplicationCacheDirectory` nicht
|
||||||
|
/// bei jedem Zugriff neu abfragen.
|
||||||
|
Future<Directory>? _dirFuture;
|
||||||
|
|
||||||
|
Future<Directory> _dir() => _dirFuture ??= _resolveDir();
|
||||||
|
|
||||||
|
Future<Directory> _resolveDir() async {
|
||||||
|
final base = await getApplicationCacheDirectory();
|
||||||
|
final dir = Directory('${base.path}/$_subdir');
|
||||||
|
if (!await dir.exists()) {
|
||||||
|
await dir.create(recursive: true);
|
||||||
|
}
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _fileName({
|
||||||
|
required String attachmentId,
|
||||||
|
required int w,
|
||||||
|
required int h,
|
||||||
|
required int q,
|
||||||
|
required String ext,
|
||||||
|
}) =>
|
||||||
|
'$attachmentId$_separator${w}x${h}_q${q}_$ext';
|
||||||
|
|
||||||
|
Future<File> _file({
|
||||||
|
required String attachmentId,
|
||||||
|
required int w,
|
||||||
|
required int h,
|
||||||
|
required int q,
|
||||||
|
required String ext,
|
||||||
|
}) async {
|
||||||
|
final dir = await _dir();
|
||||||
|
return File(
|
||||||
|
'${dir.path}/${_fileName(attachmentId: attachmentId, w: w, h: h, q: q, ext: ext)}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Liest eine gecachte Variante. `null`, wenn nichts da ist oder das Lesen
|
||||||
|
/// scheitert — der Aufrufer lädt dann frisch.
|
||||||
|
Future<Uint8List?> read({
|
||||||
|
required String attachmentId,
|
||||||
|
required int w,
|
||||||
|
required int h,
|
||||||
|
required int q,
|
||||||
|
required String ext,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final f = await _file(
|
||||||
|
attachmentId: attachmentId,
|
||||||
|
w: w,
|
||||||
|
h: h,
|
||||||
|
q: q,
|
||||||
|
ext: ext,
|
||||||
|
);
|
||||||
|
if (!await f.exists()) return null;
|
||||||
|
final bytes = await f.readAsBytes();
|
||||||
|
return bytes.isEmpty ? null : bytes;
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Schreibt eine Variante. Atomar via temp-Datei + rename, damit ein
|
||||||
|
/// paralleler Read nie ein halb geschriebenes File sieht. Leere Bytes
|
||||||
|
/// werden ignoriert (kaputter Download soll keinen leeren Cache-Eintrag
|
||||||
|
/// hinterlassen).
|
||||||
|
Future<void> write({
|
||||||
|
required String attachmentId,
|
||||||
|
required int w,
|
||||||
|
required int h,
|
||||||
|
required int q,
|
||||||
|
required String ext,
|
||||||
|
required Uint8List bytes,
|
||||||
|
}) async {
|
||||||
|
if (bytes.isEmpty) return;
|
||||||
|
try {
|
||||||
|
final f = await _file(
|
||||||
|
attachmentId: attachmentId,
|
||||||
|
w: w,
|
||||||
|
h: h,
|
||||||
|
q: q,
|
||||||
|
ext: ext,
|
||||||
|
);
|
||||||
|
final tmp = File('${f.path}.tmp');
|
||||||
|
await tmp.writeAsBytes(bytes, flush: true);
|
||||||
|
await tmp.rename(f.path);
|
||||||
|
} catch (_) {
|
||||||
|
// best-effort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Entfernt alle gecachten Dateien, deren Attachment-ID **nicht** in
|
||||||
|
/// [validAttachmentIds] vorkommt. So verschwinden die Vorschauen gelöschter
|
||||||
|
/// Foto-Notizen beim nächsten Tour-Load aus dem Cache. Verwaiste
|
||||||
|
/// temp-Dateien (abgebrochene Writes) werden immer mit entfernt.
|
||||||
|
Future<void> retainOnly(Set<String> validAttachmentIds) async {
|
||||||
|
try {
|
||||||
|
final dir = await _dir();
|
||||||
|
if (!await dir.exists()) return;
|
||||||
|
await for (final entity in dir.list()) {
|
||||||
|
if (entity is! File) continue;
|
||||||
|
final name = entity.uri.pathSegments.last;
|
||||||
|
if (name.endsWith('.tmp')) {
|
||||||
|
await _deleteQuietly(entity);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final sepIdx = name.indexOf(_separator);
|
||||||
|
final id = sepIdx == -1 ? name : name.substring(0, sepIdx);
|
||||||
|
if (!validAttachmentIds.contains(id)) {
|
||||||
|
await _deleteQuietly(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// best-effort — Pruning darf nie den Tour-Load stören
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _deleteQuietly(File f) async {
|
||||||
|
try {
|
||||||
|
await f.delete();
|
||||||
|
} catch (_) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
408
lib/data/mapper/tour_mapper.dart
Normal file
408
lib/data/mapper/tour_mapper.dart
Normal file
@ -0,0 +1,408 @@
|
|||||||
|
import 'package:holzleitner_api/holzleitner_api.dart' as api;
|
||||||
|
|
||||||
|
import 'package:hl_lieferservice/domain/entity/address.dart';
|
||||||
|
import 'package:hl_lieferservice/domain/entity/article.dart';
|
||||||
|
import 'package:hl_lieferservice/domain/entity/contact_source.dart';
|
||||||
|
import 'package:hl_lieferservice/domain/entity/customer.dart';
|
||||||
|
import 'package:hl_lieferservice/domain/entity/delivery.dart';
|
||||||
|
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_note.dart';
|
||||||
|
import 'package:hl_lieferservice/domain/entity/delivery_service_value.dart';
|
||||||
|
import 'package:hl_lieferservice/domain/entity/service.dart';
|
||||||
|
import 'package:hl_lieferservice/domain/entity/payment_method.dart';
|
||||||
|
import 'package:hl_lieferservice/domain/entity/scan_intent.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/entity/warehouse.dart';
|
||||||
|
|
||||||
|
/// Eine Schicht, ein Mapper-File: alle Übersetzungen vom generierten
|
||||||
|
/// `built_value`-Client zur Domain. Bewusst pro DTO eine Extension, damit
|
||||||
|
/// Aufrufer sich nicht in benamten Funktionen verlieren.
|
||||||
|
|
||||||
|
// ─── Primitive ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
extension ApiAddressMapper on api.Address {
|
||||||
|
Address toDomain() => Address(
|
||||||
|
street: street,
|
||||||
|
houseNumber: houseNumber,
|
||||||
|
postalCode: postalCode,
|
||||||
|
city: city,
|
||||||
|
country: country,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Stammdaten ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
extension ApiWarehouseMapper on api.Warehouse {
|
||||||
|
Warehouse toDomain() => Warehouse(
|
||||||
|
id: id,
|
||||||
|
name: name,
|
||||||
|
code: code,
|
||||||
|
isStandard: isStandard,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ApiArticleMapper on api.Article {
|
||||||
|
Article toDomain() => Article(
|
||||||
|
id: id,
|
||||||
|
articleNumber: articleNumber,
|
||||||
|
name: name,
|
||||||
|
scannable: scannable,
|
||||||
|
defaultWarehouseId: defaultWarehouseId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ApiCustomerMapper on api.Customer {
|
||||||
|
Customer toDomain() => Customer(
|
||||||
|
id: id,
|
||||||
|
name: name,
|
||||||
|
erpCustomerId: erpCustomerId,
|
||||||
|
address: address.toDomain(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ApiCustomerContactMapper on api.CustomerContact {
|
||||||
|
CustomerContact toDomain() => CustomerContact(
|
||||||
|
id: id,
|
||||||
|
customerId: customerId,
|
||||||
|
name: name,
|
||||||
|
phone: phone,
|
||||||
|
email: email,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Beleg-Kontaktquellen (ContactSource / ContactChannel) ───────────────
|
||||||
|
//
|
||||||
|
// Der OpenAPI-Generator erzeugt für die snake-case-serde-Enums im Backend
|
||||||
|
// `EnumClass`-Wrapper mit camelCase-Identifiern. Verglichen wird wie bei
|
||||||
|
// `ScanStatus` per Identitäts-Check; Fallback ist ein StateError, damit
|
||||||
|
// neue Backend-Werte sofort auffallen statt schweigend zu mappen.
|
||||||
|
|
||||||
|
extension ApiContactRoleMapper on api.ContactRole {
|
||||||
|
ContactRole toDomain() {
|
||||||
|
if (this == api.ContactRole.header) return ContactRole.header;
|
||||||
|
if (this == api.ContactRole.delivery) return ContactRole.delivery;
|
||||||
|
if (this == api.ContactRole.billing) return ContactRole.billing;
|
||||||
|
if (this == api.ContactRole.contactPerson) return ContactRole.contactPerson;
|
||||||
|
if (this == api.ContactRole.customerMaster) {
|
||||||
|
return ContactRole.customerMaster;
|
||||||
|
}
|
||||||
|
throw StateError('Unbekannte ContactRole vom Backend: $this');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ApiContactKindMapper on api.ContactKind {
|
||||||
|
ContactKind toDomain() {
|
||||||
|
if (this == api.ContactKind.phone) return ContactKind.phone;
|
||||||
|
if (this == api.ContactKind.mobile) return ContactKind.mobile;
|
||||||
|
if (this == api.ContactKind.email) return ContactKind.email;
|
||||||
|
if (this == api.ContactKind.web) return ContactKind.web;
|
||||||
|
throw StateError('Unbekannter ContactKind vom Backend: $this');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ApiContactSourceMapper on api.ContactSource {
|
||||||
|
ContactSource toDomain() => ContactSource(
|
||||||
|
id: id,
|
||||||
|
deliveryId: deliveryId,
|
||||||
|
role: role.toDomain(),
|
||||||
|
anrede: anrede,
|
||||||
|
titel: titel,
|
||||||
|
name1: name1,
|
||||||
|
name2: name2,
|
||||||
|
name3: name3,
|
||||||
|
abteilung: abteilung,
|
||||||
|
funktion: funktion,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ApiContactChannelMapper on api.ContactChannel {
|
||||||
|
ContactChannel toDomain() => ContactChannel(
|
||||||
|
id: id,
|
||||||
|
sourceId: sourceId,
|
||||||
|
kind: kind.toDomain(),
|
||||||
|
position: position,
|
||||||
|
value: value,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Scan-Progress & Delivery-Item ───────────────────────────────────────
|
||||||
|
|
||||||
|
extension ApiScanStateMapper on api.ScanState {
|
||||||
|
ScanProgress toDomain() => ScanProgress(
|
||||||
|
status: status.toDomain(),
|
||||||
|
scannedQuantity: scannedQuantity,
|
||||||
|
creditedQuantity: creditedQuantity,
|
||||||
|
lastUpdatedAt: lastUpdatedAt,
|
||||||
|
heldReason: heldReason,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Scan-Apply ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
extension DomainScanActionMapper on ScanAction {
|
||||||
|
api.AuditAction toWire() {
|
||||||
|
switch (this) {
|
||||||
|
case ScanAction.scan:
|
||||||
|
return api.AuditAction.scan;
|
||||||
|
case ScanAction.unscan:
|
||||||
|
return api.AuditAction.unscan;
|
||||||
|
case ScanAction.hold:
|
||||||
|
return api.AuditAction.hold;
|
||||||
|
case ScanAction.unhold:
|
||||||
|
return api.AuditAction.unhold;
|
||||||
|
case ScanAction.remove:
|
||||||
|
return api.AuditAction.remove;
|
||||||
|
case ScanAction.unremove:
|
||||||
|
return api.AuditAction.unremove;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension DomainScanIntentMapper on ScanIntent {
|
||||||
|
api.ScanEvent toWire() => api.ScanEvent((b) {
|
||||||
|
b
|
||||||
|
..clientScanId = clientScanId
|
||||||
|
..clientScannedAt = clientScannedAt.toUtc()
|
||||||
|
..deliveryItemId = deliveryItemId
|
||||||
|
..action = action.toWire()
|
||||||
|
..actorCarId = actorCarId
|
||||||
|
..reason = reason
|
||||||
|
// Nur für remove/unremove relevant; null = ganze Restmenge.
|
||||||
|
..quantity = quantity
|
||||||
|
..manual = manual;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ApiScanResultStatusMapper on api.ScanResultStatus {
|
||||||
|
ScanOutcomeStatus toDomain() {
|
||||||
|
if (this == api.ScanResultStatus.applied) return ScanOutcomeStatus.applied;
|
||||||
|
if (this == api.ScanResultStatus.duplicate) {
|
||||||
|
return ScanOutcomeStatus.duplicate;
|
||||||
|
}
|
||||||
|
if (this == api.ScanResultStatus.rejected) {
|
||||||
|
return ScanOutcomeStatus.rejected;
|
||||||
|
}
|
||||||
|
throw StateError('Unbekannter ScanResultStatus vom Backend: $this');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ApiScanResultMapper on api.ScanResult {
|
||||||
|
ScanOutcome toDomain() => ScanOutcome(
|
||||||
|
clientScanId: clientScanId,
|
||||||
|
status: status.toDomain(),
|
||||||
|
deliveryItemId: deliveryItemId,
|
||||||
|
reason: reason,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ApiScanStatusMapper on api.ScanStatus {
|
||||||
|
ScanStatus toDomain() {
|
||||||
|
// EnumClass kennt keinen `switch`-Exhaustiveness-Check; deshalb explizit.
|
||||||
|
if (this == api.ScanStatus.inProgress) return ScanStatus.inProgress;
|
||||||
|
if (this == api.ScanStatus.done) return ScanStatus.done;
|
||||||
|
if (this == api.ScanStatus.held) return ScanStatus.held;
|
||||||
|
if (this == api.ScanStatus.removed) return ScanStatus.removed;
|
||||||
|
throw StateError('Unbekannter ScanStatus vom Backend: $this');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ApiDeliveryItemMapper on api.DeliveryItem {
|
||||||
|
DeliveryItem toDomain() => DeliveryItem(
|
||||||
|
id: id,
|
||||||
|
deliveryId: deliveryId,
|
||||||
|
articleId: articleId,
|
||||||
|
warehouseId: warehouseId,
|
||||||
|
belegzeilenNr: belegzeilenNr,
|
||||||
|
requiredQuantity: requiredQuantity,
|
||||||
|
scanProgress: scanState.toDomain(),
|
||||||
|
unitPrice: unitPrice,
|
||||||
|
komponentenArtikelNr: komponentenArtikelNr,
|
||||||
|
parentArtikelNr: parentArtikelNr,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Delivery ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
extension ApiDeliveryStateMapper on api.DeliveryState {
|
||||||
|
DeliveryState toDomain() {
|
||||||
|
if (this == api.DeliveryState.active) return DeliveryState.active;
|
||||||
|
if (this == api.DeliveryState.held) return DeliveryState.held;
|
||||||
|
if (this == api.DeliveryState.canceled) return DeliveryState.canceled;
|
||||||
|
if (this == api.DeliveryState.completed) return DeliveryState.completed;
|
||||||
|
throw StateError('Unbekannter DeliveryState vom Backend: $this');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ApiDeliveryWithItemsMapper on api.DeliveryWithItems {
|
||||||
|
Delivery toDomain() => Delivery(
|
||||||
|
id: id,
|
||||||
|
tourId: tourId,
|
||||||
|
customerId: customerId,
|
||||||
|
contactPersonIds: contactPersonIds.toList(growable: false),
|
||||||
|
deliveryAddressSnapshot: deliveryAddressSnapshot.toDomain(),
|
||||||
|
erpBelegartId: erpBelegartId,
|
||||||
|
erpBelegnummer: erpBelegnummer,
|
||||||
|
state: state.toDomain(),
|
||||||
|
stateReason: stateReason,
|
||||||
|
sortOrder: sortOrder,
|
||||||
|
assignedCarId: assignedCarId,
|
||||||
|
desiredTime: desiredTime,
|
||||||
|
specialAgreements: specialAgreements,
|
||||||
|
items: items.map((it) => it.toDomain()).toList(growable: false),
|
||||||
|
prepaidAmount: prepaidAmount,
|
||||||
|
paymentMethodId: paymentMethodId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ApiPaymentMethodMapper on api.PaymentMethod {
|
||||||
|
PaymentMethod toDomain() => PaymentMethod(
|
||||||
|
id: id,
|
||||||
|
code: code,
|
||||||
|
name: name,
|
||||||
|
active: active,
|
||||||
|
createdAt: createdAt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Tour-Notiz ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
extension ApiDeliveryNoteMapper on api.DeliveryNote {
|
||||||
|
DeliveryNote toDomain() => DeliveryNote(
|
||||||
|
id: id,
|
||||||
|
deliveryId: deliveryId,
|
||||||
|
text: text,
|
||||||
|
imageAttachment: imageAttachment,
|
||||||
|
authorPersonalnummer: authorPersonalnummer,
|
||||||
|
authorCarId: authorCarId,
|
||||||
|
creditDeliveryItemId: creditDeliveryItemId,
|
||||||
|
isAmountCreditNote: isAmountCreditNote,
|
||||||
|
imageAttachmentDeleted: imageAttachmentDeleted ?? false,
|
||||||
|
createdAt: createdAt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Tour-Wurzel ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
extension ApiTourMapper on api.Tour {
|
||||||
|
Tour toDomain() => Tour(
|
||||||
|
id: id,
|
||||||
|
accountId: accountId,
|
||||||
|
date: date.toDateTime(),
|
||||||
|
syncedAt: syncedAt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ApiTourSummaryMapper on api.TourSummary {
|
||||||
|
TourSummary toDomain() => TourSummary(
|
||||||
|
tourId: tourId,
|
||||||
|
tourDate: tourDate.toDateTime(),
|
||||||
|
deliveryCount: deliveryCount,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ApiTourDetailsMapper on api.TourDetails {
|
||||||
|
TourDetails toDomain() {
|
||||||
|
final customersMap = <String, Customer>{
|
||||||
|
for (final c in customers) c.id: c.toDomain(),
|
||||||
|
};
|
||||||
|
final contactsMap = <String, CustomerContact>{
|
||||||
|
for (final c in customerContacts) c.id: c.toDomain(),
|
||||||
|
};
|
||||||
|
final articlesMap = <String, Article>{
|
||||||
|
for (final a in articles) a.id: a.toDomain(),
|
||||||
|
};
|
||||||
|
final warehousesMap = <String, Warehouse>{
|
||||||
|
for (final w in warehouses) w.id: w.toDomain(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Notizen sind im Wire flach — pro Lieferung indizieren und aufsteigend
|
||||||
|
// nach createdAt sortieren, damit das UI sich nicht jedes Mal selbst
|
||||||
|
// sortieren muss.
|
||||||
|
final notesGrouped = <String, List<DeliveryNote>>{};
|
||||||
|
for (final n in notes) {
|
||||||
|
final domain = n.toDomain();
|
||||||
|
(notesGrouped[domain.deliveryId] ??= <DeliveryNote>[]).add(domain);
|
||||||
|
}
|
||||||
|
for (final list in notesGrouped.values) {
|
||||||
|
list.sort((a, b) => a.createdAt.compareTo(b.createdAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gutschriften: höchstens eine pro Lieferung (aktueller Stand).
|
||||||
|
final creditsMap = <String, DeliveryCredit>{
|
||||||
|
for (final c in credits) c.deliveryId: c.toDomain(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Service-Definitionen (aktiv, sortiert) + Pro-Lieferung-Werte indizieren.
|
||||||
|
final servicesList =
|
||||||
|
services.map((s) => s.toDomain()).toList(growable: false);
|
||||||
|
final serviceValues = <String, Map<String, DeliveryServiceValue>>{};
|
||||||
|
for (final v in deliveryServices) {
|
||||||
|
(serviceValues[v.deliveryId] ??= <String, DeliveryServiceValue>{})[
|
||||||
|
v.serviceId] = v.toDomain();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kontaktquellen pro Lieferung gruppieren; Kanäle pro Quelle gruppieren.
|
||||||
|
// Backend liefert sie sortiert (Quellen nach Rolle, Kanäle nach kind +
|
||||||
|
// position) — wir behalten die Reihenfolge bei.
|
||||||
|
final sourcesGrouped = <String, List<ContactSource>>{};
|
||||||
|
for (final s in contactSources) {
|
||||||
|
final domain = s.toDomain();
|
||||||
|
(sourcesGrouped[domain.deliveryId] ??= <ContactSource>[]).add(domain);
|
||||||
|
}
|
||||||
|
final channelsGrouped = <String, List<ContactChannel>>{};
|
||||||
|
for (final c in contactChannels) {
|
||||||
|
final domain = c.toDomain();
|
||||||
|
(channelsGrouped[domain.sourceId] ??= <ContactChannel>[]).add(domain);
|
||||||
|
}
|
||||||
|
|
||||||
|
return TourDetails(
|
||||||
|
tour: tour.toDomain(),
|
||||||
|
deliveries: deliveries.map((d) => d.toDomain()).toList(growable: false),
|
||||||
|
customers: customersMap,
|
||||||
|
contacts: contactsMap,
|
||||||
|
articles: articlesMap,
|
||||||
|
warehouses: warehousesMap,
|
||||||
|
notesByDeliveryId: notesGrouped,
|
||||||
|
creditsByDeliveryId: creditsMap,
|
||||||
|
services: servicesList,
|
||||||
|
serviceValuesByDeliveryId: serviceValues,
|
||||||
|
contactSourcesByDeliveryId: sourcesGrouped,
|
||||||
|
contactChannelsBySourceId: channelsGrouped,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ApiServiceMapper on api.Service {
|
||||||
|
Service toDomain() => Service(
|
||||||
|
id: id,
|
||||||
|
key: key,
|
||||||
|
name: name,
|
||||||
|
kind: kind == api.ServiceKind.numeric
|
||||||
|
? ServiceKind.numeric
|
||||||
|
: ServiceKind.boolean,
|
||||||
|
active: active,
|
||||||
|
sortOrder: sortOrder,
|
||||||
|
minValue: minValue,
|
||||||
|
maxValue: maxValue,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ApiDeliveryServiceValueMapper on api.DeliveryServiceValue {
|
||||||
|
DeliveryServiceValue toDomain() => DeliveryServiceValue(
|
||||||
|
deliveryId: deliveryId,
|
||||||
|
serviceId: serviceId,
|
||||||
|
boolValue: boolValue,
|
||||||
|
numericValue: numericValue,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ApiDeliveryCreditMapper on api.DeliveryCredit {
|
||||||
|
DeliveryCredit toDomain() => DeliveryCredit(
|
||||||
|
deliveryId: deliveryId,
|
||||||
|
amountCents: amountCents,
|
||||||
|
reason: reason,
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -53,4 +53,44 @@ class BackendConfig {
|
|||||||
keycloakClientId: 'holzleitner-app',
|
keycloakClientId: 'holzleitner-app',
|
||||||
keycloakRedirectUrl: 'holzleitner://oauth2redirect',
|
keycloakRedirectUrl: 'holzleitner://oauth2redirect',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// Konfiguration für USB-Tunnel via `adb reverse` — gedacht für Tests in
|
||||||
|
/// fremden Netzwerken, in denen das Gerät den Mac nicht über eine LAN-IP
|
||||||
|
/// erreicht. Alles zeigt auf `localhost`; der Traffic wird über den
|
||||||
|
/// USB-Bus zum Host getunnelt.
|
||||||
|
///
|
||||||
|
/// **Setup vor dem Start (Gerät per USB angesteckt):**
|
||||||
|
/// ```
|
||||||
|
/// adb reverse tcp:3000 tcp:3000 # Rust-API
|
||||||
|
/// adb reverse tcp:8080 tcp:8080 # Keycloak
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// **Backend-Voraussetzungen**, damit das OIDC-Login funktioniert:
|
||||||
|
/// * Backend-Env `KEYCLOAK_ISSUER_URL=http://localhost:8080/realms/holzleitner`
|
||||||
|
/// (muss exakt mit [keycloakIssuerUrl] matchen, sonst 401 `invalid issuer`).
|
||||||
|
/// * Keycloak muss den Issuer als `localhost` ausgeben — z. B. via
|
||||||
|
/// `KC_HOSTNAME_URL=http://localhost:8080` (oder Frontend-URL im Realm),
|
||||||
|
/// sonst prägt es den Container-Hostnamen ins `iss`-Claim.
|
||||||
|
/// * Der `holzleitner://oauth2redirect`-Redirect bleibt unverändert (das
|
||||||
|
/// Custom-Scheme ist netzwerk-unabhängig).
|
||||||
|
///
|
||||||
|
/// Aktivieren ohne Code-Edit:
|
||||||
|
/// ```
|
||||||
|
/// flutter run --dart-define=HL_BACKEND=usb
|
||||||
|
/// ```
|
||||||
|
static const BackendConfig usbReverse = BackendConfig(
|
||||||
|
apiBaseUrl: 'http://localhost:3000',
|
||||||
|
keycloakIssuerUrl: 'http://localhost:8080/realms/holzleitner',
|
||||||
|
keycloakClientId: 'holzleitner-app',
|
||||||
|
keycloakRedirectUrl: 'holzleitner://oauth2redirect',
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Wählt die Config anhand des Compile-Time-Flags `HL_BACKEND`:
|
||||||
|
/// * `usb` → [usbReverse] (adb-reverse-Tunnel über localhost)
|
||||||
|
/// * sonst → [localDev] (LAN-IP, Default)
|
||||||
|
///
|
||||||
|
/// So muss für einen Netzwerkwechsel nur das Build-Flag gesetzt werden,
|
||||||
|
/// nicht der Quellcode angefasst.
|
||||||
|
static const BackendConfig fromEnvironment =
|
||||||
|
String.fromEnvironment('HL_BACKEND') == 'usb' ? usbReverse : localDev;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -55,6 +55,13 @@ class KeycloakOidcTokenProvider implements AuthTokenProvider {
|
|||||||
String? _refreshToken;
|
String? _refreshToken;
|
||||||
Map<String, dynamic>? _idTokenClaims;
|
Map<String, dynamic>? _idTokenClaims;
|
||||||
|
|
||||||
|
/// Single-flight-Guard: hält den gerade laufenden Refresh, damit mehrere
|
||||||
|
/// gleichzeitige Aufrufer (Bootstrap: Restore + PaymentMethodsCubit +
|
||||||
|
/// Folge-Requests) sich EINEN Refresh teilen statt parallele
|
||||||
|
/// `flutter_appauth.token()`-Calls auszulösen (die nativ blockieren/haken
|
||||||
|
/// können → App hängt nach Hot-Restart am Splash/Login).
|
||||||
|
Future<String?>? _refreshInFlight;
|
||||||
|
|
||||||
final StreamController<AuthSessionEvent> _events =
|
final StreamController<AuthSessionEvent> _events =
|
||||||
StreamController<AuthSessionEvent>.broadcast();
|
StreamController<AuthSessionEvent>.broadcast();
|
||||||
|
|
||||||
@ -166,6 +173,19 @@ class KeycloakOidcTokenProvider implements AuthTokenProvider {
|
|||||||
final rt = _refreshToken;
|
final rt = _refreshToken;
|
||||||
if (rt == null) return null;
|
if (rt == null) return null;
|
||||||
|
|
||||||
|
// Single-flight: läuft bereits ein Refresh, hängen wir uns dran, statt
|
||||||
|
// einen zweiten `flutter_appauth.token()`-Call zu starten. `??=`
|
||||||
|
// evaluiert die rechte Seite nur, wenn noch kein Refresh läuft.
|
||||||
|
return _refreshInFlight ??= _performRefresh(rt).whenComplete(() {
|
||||||
|
_refreshInFlight = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Führt EINEN Token-Refresh aus. Bei Erfolg werden die Tokens übernommen
|
||||||
|
/// und der neue Access-Token zurückgegeben (ohne Event — stiller Refresh).
|
||||||
|
/// Bei Fehler ist die Session tot: lokal aufräumen, `AuthSessionExpired`
|
||||||
|
/// emittieren, `null` zurück.
|
||||||
|
Future<String?> _performRefresh(String rt) async {
|
||||||
try {
|
try {
|
||||||
final result = await _appAuth.token(
|
final result = await _appAuth.token(
|
||||||
TokenRequest(
|
TokenRequest(
|
||||||
@ -187,11 +207,17 @@ class KeycloakOidcTokenProvider implements AuthTokenProvider {
|
|||||||
return _accessToken;
|
return _accessToken;
|
||||||
} on Exception {
|
} on Exception {
|
||||||
// Refresh hat nicht funktioniert — Session ist tot, nicht
|
// Refresh hat nicht funktioniert — Session ist tot, nicht
|
||||||
// wiederherstellbar. Aufrufer kriegen null zurück, AuthBloc
|
// wiederherstellbar. Reihenfolge bewusst: erst State leeren + Event
|
||||||
// bekommt SessionExpired.
|
// feuern, DANN best-effort den Storage löschen — so kann ein
|
||||||
|
// werfendes `delete` weder das Event verschlucken noch eine Exception
|
||||||
|
// aus `currentAccessToken()` leaken.
|
||||||
_clearSession();
|
_clearSession();
|
||||||
await _storage.delete(key: _refreshTokenStorageKey);
|
|
||||||
_events.add(const AuthSessionExpired());
|
_events.add(const AuthSessionExpired());
|
||||||
|
try {
|
||||||
|
await _storage.delete(key: _refreshTokenStorageKey);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('currentAccessToken: Refresh-Token-Delete fehlgeschlagen: $e');
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,7 +17,7 @@ import 'keycloak_oidc_token_provider.dart';
|
|||||||
/// das reine dart-Smoke-Tool, siehe `tool/smoke_test_api.dart`).
|
/// das reine dart-Smoke-Tool, siehe `tool/smoke_test_api.dart`).
|
||||||
void registerNetworking({
|
void registerNetworking({
|
||||||
required GetIt locator,
|
required GetIt locator,
|
||||||
BackendConfig config = BackendConfig.localDev,
|
BackendConfig config = BackendConfig.fromEnvironment,
|
||||||
}) {
|
}) {
|
||||||
locator.registerSingleton<BackendConfig>(config);
|
locator.registerSingleton<BackendConfig>(config);
|
||||||
|
|
||||||
|
|||||||
125
lib/data/repository/payment_methods_repository_impl.dart
Normal file
125
lib/data/repository/payment_methods_repository_impl.dart
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:holzleitner_api/holzleitner_api.dart' as api;
|
||||||
|
|
||||||
|
import 'package:hl_lieferservice/data/mapper/tour_mapper.dart';
|
||||||
|
import 'package:hl_lieferservice/domain/entity/payment_method.dart';
|
||||||
|
import 'package:hl_lieferservice/domain/repository/payment_methods_repository.dart';
|
||||||
|
|
||||||
|
/// Dio-Impl gegen den generierten `PaymentMethodsApi`.
|
||||||
|
///
|
||||||
|
/// Fehler-Mapping:
|
||||||
|
/// * `409 Conflict` (UNIQUE-Verletzung oder FK-RESTRICT beim Löschen)
|
||||||
|
/// → `PaymentMethodsRepositoryException` mit klarer Meldung.
|
||||||
|
/// * `404` → dito (NotFound-Hinweis im Text).
|
||||||
|
/// * `401` lassen wir ungefangen durchfliegen — globaler Auth-Handler
|
||||||
|
/// übernimmt.
|
||||||
|
class PaymentMethodsRepositoryImpl implements PaymentMethodsRepository {
|
||||||
|
PaymentMethodsRepositoryImpl(this._api);
|
||||||
|
|
||||||
|
final api.HolzleitnerApi _api;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<PaymentMethod>> list({bool includeInactive = false}) async {
|
||||||
|
try {
|
||||||
|
final response = await _api
|
||||||
|
.getPaymentMethodsApi()
|
||||||
|
.listPaymentMethods(includeInactive: includeInactive);
|
||||||
|
final methods = response.data?.methods;
|
||||||
|
if (methods == null) return const [];
|
||||||
|
return methods.map((m) => m.toDomain()).toList(growable: false);
|
||||||
|
} on DioException catch (e) {
|
||||||
|
throw PaymentMethodsRepositoryException(
|
||||||
|
_describe(e, 'Laden der Zahlungsmethoden'),
|
||||||
|
e,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<PaymentMethod> create({
|
||||||
|
required String code,
|
||||||
|
required String name,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final request = api.CreatePaymentMethodRequest((b) {
|
||||||
|
b
|
||||||
|
..code = code
|
||||||
|
..name = name;
|
||||||
|
});
|
||||||
|
final response = await _api
|
||||||
|
.getPaymentMethodsApi()
|
||||||
|
.createPaymentMethod(createPaymentMethodRequest: request);
|
||||||
|
final method = response.data?.method;
|
||||||
|
if (method == null) {
|
||||||
|
throw const PaymentMethodsRepositoryException(
|
||||||
|
'Server lieferte leere Antwort beim Anlegen',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return method.toDomain();
|
||||||
|
} on DioException catch (e) {
|
||||||
|
throw PaymentMethodsRepositoryException(
|
||||||
|
_describe(e, 'Anlegen einer Zahlungsmethode'),
|
||||||
|
e,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<PaymentMethod> update({
|
||||||
|
required String id,
|
||||||
|
String? name,
|
||||||
|
bool? active,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final request = api.UpdatePaymentMethodRequest((b) {
|
||||||
|
if (name != null) b.name = name;
|
||||||
|
if (active != null) b.active = active;
|
||||||
|
});
|
||||||
|
final response =
|
||||||
|
await _api.getPaymentMethodsApi().updatePaymentMethod(
|
||||||
|
id: id,
|
||||||
|
updatePaymentMethodRequest: request,
|
||||||
|
);
|
||||||
|
final method = response.data?.method;
|
||||||
|
if (method == null) {
|
||||||
|
throw const PaymentMethodsRepositoryException(
|
||||||
|
'Server lieferte leere Antwort beim Aktualisieren',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return method.toDomain();
|
||||||
|
} on DioException catch (e) {
|
||||||
|
throw PaymentMethodsRepositoryException(
|
||||||
|
_describe(e, 'Aktualisieren einer Zahlungsmethode'),
|
||||||
|
e,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> delete(String id) async {
|
||||||
|
try {
|
||||||
|
await _api.getPaymentMethodsApi().deletePaymentMethod(id: id);
|
||||||
|
} on DioException catch (e) {
|
||||||
|
throw PaymentMethodsRepositoryException(
|
||||||
|
_describe(e, 'Löschen einer Zahlungsmethode'),
|
||||||
|
e,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _describe(DioException e, String operation) {
|
||||||
|
final status = e.response?.statusCode;
|
||||||
|
final body = e.response?.data;
|
||||||
|
if ((status == 400 || status == 409) &&
|
||||||
|
body is Map &&
|
||||||
|
body['message'] != null) {
|
||||||
|
return body['message'].toString();
|
||||||
|
}
|
||||||
|
if (status == 409) {
|
||||||
|
return 'Zahlungsmethode wird noch von Lieferungen verwendet';
|
||||||
|
}
|
||||||
|
if (status == 404) return 'Zahlungsmethode nicht gefunden';
|
||||||
|
if (status == 401) return 'Sitzung abgelaufen';
|
||||||
|
return '$operation fehlgeschlagen (HTTP ${status ?? 'unbekannt'})';
|
||||||
|
}
|
||||||
|
}
|
||||||
569
lib/data/repository/tour_repository_impl.dart
Normal file
569
lib/data/repository/tour_repository_impl.dart
Normal file
@ -0,0 +1,569 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:holzleitner_api/holzleitner_api.dart' as api;
|
||||||
|
|
||||||
|
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_note.dart';
|
||||||
|
import 'package:hl_lieferservice/domain/entity/delivery_service_value.dart';
|
||||||
|
import 'package:hl_lieferservice/domain/entity/scan_intent.dart';
|
||||||
|
import 'package:hl_lieferservice/domain/entity/tour.dart';
|
||||||
|
import 'package:hl_lieferservice/domain/entity/tour_details.dart';
|
||||||
|
import 'package:hl_lieferservice/domain/repository/tour_repository.dart';
|
||||||
|
|
||||||
|
/// Implementierung gegen den generierten Dio-Client. Übersetzt
|
||||||
|
/// `DioException` in [TourRepositoryException]. 401 lassen wir
|
||||||
|
/// ungefangen durchfliegen — der TokenProvider erkennt 401 separat
|
||||||
|
/// und meldet `AuthSessionExpired`.
|
||||||
|
class TourRepositoryImpl implements TourRepository {
|
||||||
|
TourRepositoryImpl(this._api);
|
||||||
|
|
||||||
|
final api.HolzleitnerApi _api;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<TourSummary?> getMyTourSummaryOfToday() async {
|
||||||
|
try {
|
||||||
|
final response = await _api.getToursApi().listMyToursToday();
|
||||||
|
final tours = response.data?.tours;
|
||||||
|
if (tours == null || tours.isEmpty) return null;
|
||||||
|
// Backend liefert die Liste sortiert; wir nehmen die erste Tour.
|
||||||
|
// Der Fahrer hat aktuell nur eine Tour pro Tag — falls sich das
|
||||||
|
// ändert, wird hier eine Auswahl-UI nötig.
|
||||||
|
return tours.first.toDomain();
|
||||||
|
} on DioException catch (e) {
|
||||||
|
throw TourRepositoryException(_describe(e, 'Laden der Tour-Übersicht'), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<TourDetails> getTourDetails(String tourId) async {
|
||||||
|
try {
|
||||||
|
final response = await _api.getToursApi().getTour(tourId: tourId);
|
||||||
|
final details = response.data;
|
||||||
|
if (details == null) {
|
||||||
|
throw const TourRepositoryException(
|
||||||
|
'Server lieferte leere Tour-Antwort',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return details.toDomain();
|
||||||
|
} on DioException catch (e) {
|
||||||
|
throw TourRepositoryException(_describe(e, 'Laden der Tour'), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<TourDetails?> getMyTourDetailsOfToday() async {
|
||||||
|
final summary = await getMyTourSummaryOfToday();
|
||||||
|
if (summary == null) return null;
|
||||||
|
return getTourDetails(summary.tourId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Map<String, int>> setDeliveryOrder({
|
||||||
|
required String tourId,
|
||||||
|
required List<String> orderedDeliveryIds,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final request = api.SetDeliveryOrderRequest((b) {
|
||||||
|
b.deliveryIds.replace(orderedDeliveryIds);
|
||||||
|
});
|
||||||
|
final response = await _api.getToursApi().setDeliveryOrder(
|
||||||
|
tourId: tourId,
|
||||||
|
setDeliveryOrderRequest: request,
|
||||||
|
);
|
||||||
|
final order = response.data?.order;
|
||||||
|
if (order == null) {
|
||||||
|
throw const TourRepositoryException(
|
||||||
|
'Server lieferte leere Reihenfolge-Antwort',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return {for (final e in order) e.deliveryId: e.sortOrder};
|
||||||
|
} on DioException catch (e) {
|
||||||
|
throw TourRepositoryException(_describe(e, 'Speichern der Reihenfolge'), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Delivery> assignCarToDelivery({
|
||||||
|
required String deliveryId,
|
||||||
|
required String? carId,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final request = api.AssignCarRequest((b) {
|
||||||
|
// `null` ⇒ Backend hebt die Zuweisung auf. built_value lässt das
|
||||||
|
// im Wire als `"carId": null` durchgehen — vom OpenAPI-Schema so
|
||||||
|
// gewollt.
|
||||||
|
b.carId = carId;
|
||||||
|
});
|
||||||
|
final response = await _api.getDeliveriesApi().assignCar(
|
||||||
|
deliveryId: deliveryId,
|
||||||
|
assignCarRequest: request,
|
||||||
|
);
|
||||||
|
final delivery = response.data?.delivery;
|
||||||
|
if (delivery == null) {
|
||||||
|
throw const TourRepositoryException(
|
||||||
|
'Server lieferte leere Delivery-Antwort',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Achtung: Der Endpoint gibt `Delivery` *ohne* Items zurück.
|
||||||
|
// Wir bauen hier eine Domain-Delivery mit leerer Item-Liste —
|
||||||
|
// der Bloc muss die echten Items aus dem lokalen Aggregat mergen.
|
||||||
|
return Delivery(
|
||||||
|
id: delivery.id,
|
||||||
|
tourId: delivery.tourId,
|
||||||
|
customerId: delivery.customerId,
|
||||||
|
contactPersonIds: delivery.contactPersonIds.toList(growable: false),
|
||||||
|
deliveryAddressSnapshot: delivery.deliveryAddressSnapshot.toDomain(),
|
||||||
|
erpBelegartId: delivery.erpBelegartId,
|
||||||
|
erpBelegnummer: delivery.erpBelegnummer,
|
||||||
|
state: delivery.state.toDomain(),
|
||||||
|
stateReason: delivery.stateReason,
|
||||||
|
// Stamm-Endpoint kennt `sortOrder` nicht — Bloc behält den Wert.
|
||||||
|
sortOrder: 0,
|
||||||
|
assignedCarId: delivery.assignedCarId,
|
||||||
|
desiredTime: delivery.desiredTime,
|
||||||
|
specialAgreements: delivery.specialAgreements,
|
||||||
|
items: const [],
|
||||||
|
prepaidAmount: delivery.prepaidAmount,
|
||||||
|
paymentMethodId: delivery.paymentMethodId,
|
||||||
|
);
|
||||||
|
} on DioException catch (e) {
|
||||||
|
throw TourRepositoryException(_describe(e, 'Fahrzeug-Zuweisung'), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Delivery> cancelDelivery({
|
||||||
|
required String deliveryId,
|
||||||
|
required String reason,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final request = api.CancelDeliveryRequest((b) => b..reason = reason);
|
||||||
|
final response = await _api.getDeliveriesApi().cancel(
|
||||||
|
deliveryId: deliveryId,
|
||||||
|
cancelDeliveryRequest: request,
|
||||||
|
);
|
||||||
|
return _liftDeliveryStub(response.data?.delivery, 'Abbruch');
|
||||||
|
} on DioException catch (e) {
|
||||||
|
throw TourRepositoryException(_describe(e, 'Lieferung abbrechen'), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Delivery> holdDelivery({
|
||||||
|
required String deliveryId,
|
||||||
|
required String reason,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final request = api.HoldDeliveryRequest((b) => b..reason = reason);
|
||||||
|
final response = await _api.getDeliveriesApi().hold(
|
||||||
|
deliveryId: deliveryId,
|
||||||
|
holdDeliveryRequest: request,
|
||||||
|
);
|
||||||
|
return _liftDeliveryStub(response.data?.delivery, 'Pausieren');
|
||||||
|
} on DioException catch (e) {
|
||||||
|
throw TourRepositoryException(_describe(e, 'Lieferung pausieren'), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Delivery> resumeDelivery({required String deliveryId}) async {
|
||||||
|
try {
|
||||||
|
final response =
|
||||||
|
await _api.getDeliveriesApi().resume(deliveryId: deliveryId);
|
||||||
|
return _liftDeliveryStub(response.data?.delivery, 'Fortsetzen');
|
||||||
|
} on DioException catch (e) {
|
||||||
|
throw TourRepositoryException(_describe(e, 'Lieferung fortsetzen'), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Delivery> completeDelivery({
|
||||||
|
required String deliveryId,
|
||||||
|
required List<int> customerSignaturePng,
|
||||||
|
required List<int> driverSignaturePng,
|
||||||
|
required bool receiptConfirmed,
|
||||||
|
required bool notesAcknowledged,
|
||||||
|
required List<String> acknowledgedNoteIds,
|
||||||
|
String? paymentMethodId,
|
||||||
|
String? actorCarId,
|
||||||
|
bool paymentCollected = false,
|
||||||
|
}) async {
|
||||||
|
// multipart/form-data: zwei Signatur-PNGs + ein JSON-Feld mit den
|
||||||
|
// Bestätigungen. Direkt über die Dio-Instanz, weil der dart-dio-Generator
|
||||||
|
// für multipart keinen typisierten Body erzeugt (wie beim Bild-Upload).
|
||||||
|
try {
|
||||||
|
final acknowledgements = <String, dynamic>{
|
||||||
|
'receiptConfirmed': receiptConfirmed,
|
||||||
|
'notesAcknowledged': notesAcknowledged,
|
||||||
|
'acknowledgedNoteIds': acknowledgedNoteIds,
|
||||||
|
'paymentCollected': paymentCollected,
|
||||||
|
if (paymentMethodId != null) 'paymentMethodId': paymentMethodId,
|
||||||
|
if (actorCarId != null) 'authorCarId': actorCarId,
|
||||||
|
};
|
||||||
|
final form = FormData.fromMap({
|
||||||
|
'customer_signature': MultipartFile.fromBytes(
|
||||||
|
customerSignaturePng,
|
||||||
|
filename: 'customer_signature.png',
|
||||||
|
contentType: DioMediaType.parse('image/png'),
|
||||||
|
),
|
||||||
|
'driver_signature': MultipartFile.fromBytes(
|
||||||
|
driverSignaturePng,
|
||||||
|
filename: 'driver_signature.png',
|
||||||
|
contentType: DioMediaType.parse('image/png'),
|
||||||
|
),
|
||||||
|
'acknowledgements': jsonEncode(acknowledgements),
|
||||||
|
});
|
||||||
|
final response = await _api.dio.post<Map<String, dynamic>>(
|
||||||
|
'/deliveries/$deliveryId/complete',
|
||||||
|
data: form,
|
||||||
|
);
|
||||||
|
final delivery = response.data?['delivery'] as Map<String, dynamic>?;
|
||||||
|
if (delivery == null) {
|
||||||
|
throw const TourRepositoryException(
|
||||||
|
'Server lieferte leere Delivery-Antwort beim Abschließen',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return _deliveryStubFromJson(delivery);
|
||||||
|
} on DioException catch (e) {
|
||||||
|
throw TourRepositoryException(_describe(e, 'Lieferung abschließen'), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hebt einen Delivery-Stub (Stamm-Endpoint-Response **ohne** Items)
|
||||||
|
/// in die Domain. Aufrufer muss anschließend `copyWith(items: ..., sortOrder: ...)`
|
||||||
|
/// aus dem lokalen Aggregat mergen — der Bloc-Handler kümmert sich darum.
|
||||||
|
Delivery _liftDeliveryStub(api.Delivery? stub, String operation) {
|
||||||
|
if (stub == null) {
|
||||||
|
throw TourRepositoryException(
|
||||||
|
'Server lieferte leere Delivery-Antwort beim $operation',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Delivery(
|
||||||
|
id: stub.id,
|
||||||
|
tourId: stub.tourId,
|
||||||
|
customerId: stub.customerId,
|
||||||
|
contactPersonIds: stub.contactPersonIds.toList(growable: false),
|
||||||
|
deliveryAddressSnapshot: stub.deliveryAddressSnapshot.toDomain(),
|
||||||
|
erpBelegartId: stub.erpBelegartId,
|
||||||
|
erpBelegnummer: stub.erpBelegnummer,
|
||||||
|
state: stub.state.toDomain(),
|
||||||
|
stateReason: stub.stateReason,
|
||||||
|
sortOrder: 0,
|
||||||
|
assignedCarId: stub.assignedCarId,
|
||||||
|
desiredTime: stub.desiredTime,
|
||||||
|
specialAgreements: stub.specialAgreements,
|
||||||
|
items: const [],
|
||||||
|
prepaidAmount: stub.prepaidAmount,
|
||||||
|
paymentMethodId: stub.paymentMethodId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<DeliveryNote> addDeliveryNote({
|
||||||
|
required String deliveryId,
|
||||||
|
String? text,
|
||||||
|
String? imageAttachment,
|
||||||
|
String? creditDeliveryItemId,
|
||||||
|
bool isAmountCreditNote = false,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final request = api.CreateDeliveryNoteRequest((b) {
|
||||||
|
if (text != null) b.text = text;
|
||||||
|
if (imageAttachment != null) b.imageAttachment = imageAttachment;
|
||||||
|
if (creditDeliveryItemId != null) {
|
||||||
|
b.creditDeliveryItemId = creditDeliveryItemId;
|
||||||
|
}
|
||||||
|
b.isAmountCreditNote = isAmountCreditNote;
|
||||||
|
});
|
||||||
|
final response = await _api.getDeliveriesApi().createNote(
|
||||||
|
deliveryId: deliveryId,
|
||||||
|
createDeliveryNoteRequest: request,
|
||||||
|
);
|
||||||
|
final note = response.data?.note;
|
||||||
|
if (note == null) {
|
||||||
|
throw const TourRepositoryException(
|
||||||
|
'Server lieferte leere Notiz-Antwort',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return note.toDomain();
|
||||||
|
} on DioException catch (e) {
|
||||||
|
throw TourRepositoryException(_describe(e, 'Notiz anlegen'), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<DeliveryNote> updateDeliveryNote({
|
||||||
|
required String deliveryId,
|
||||||
|
required String noteId,
|
||||||
|
String? text,
|
||||||
|
String? imageAttachment,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final request = api.UpdateDeliveryNoteRequest((b) {
|
||||||
|
if (text != null) b.text = text;
|
||||||
|
if (imageAttachment != null) b.imageAttachment = imageAttachment;
|
||||||
|
});
|
||||||
|
final response = await _api.getDeliveriesApi().updateNote(
|
||||||
|
deliveryId: deliveryId,
|
||||||
|
noteId: noteId,
|
||||||
|
updateDeliveryNoteRequest: request,
|
||||||
|
);
|
||||||
|
final note = response.data?.note;
|
||||||
|
if (note == null) {
|
||||||
|
throw const TourRepositoryException(
|
||||||
|
'Server lieferte leere Notiz-Antwort',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return note.toDomain();
|
||||||
|
} on DioException catch (e) {
|
||||||
|
throw TourRepositoryException(_describe(e, 'Notiz aktualisieren'), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> deleteDeliveryNote({
|
||||||
|
required String deliveryId,
|
||||||
|
required String noteId,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
await _api.getDeliveriesApi().deleteNote(
|
||||||
|
deliveryId: deliveryId,
|
||||||
|
noteId: noteId,
|
||||||
|
);
|
||||||
|
} on DioException catch (e) {
|
||||||
|
throw TourRepositoryException(_describe(e, 'Notiz löschen'), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<DeliveryNote> uploadDeliveryNoteImage({
|
||||||
|
required String deliveryId,
|
||||||
|
required String filename,
|
||||||
|
required String mime,
|
||||||
|
required List<int> bytes,
|
||||||
|
}) async {
|
||||||
|
// Bewusst direkt über die Dio-Instanz statt über den generierten Client:
|
||||||
|
// der dart-dio-Generator erzeugt für multipart/form-data keinen
|
||||||
|
// typisierten Body-Parameter. Der `HolzleitnerAuthInterceptor` an der
|
||||||
|
// Dio-Instanz hängt den Bearer-Token automatisch an.
|
||||||
|
try {
|
||||||
|
final form = FormData.fromMap({
|
||||||
|
'file': MultipartFile.fromBytes(
|
||||||
|
bytes,
|
||||||
|
filename: filename,
|
||||||
|
contentType: DioMediaType.parse(mime),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
final response = await _api.dio.post<Map<String, dynamic>>(
|
||||||
|
'/deliveries/$deliveryId/notes/image',
|
||||||
|
data: form,
|
||||||
|
);
|
||||||
|
final note = (response.data?['note']) as Map<String, dynamic>?;
|
||||||
|
if (note == null) {
|
||||||
|
throw const TourRepositoryException(
|
||||||
|
'Server lieferte leere Notiz-Antwort beim Bild-Upload',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return _noteFromJson(note);
|
||||||
|
} on DioException catch (e) {
|
||||||
|
throw TourRepositoryException(_describe(e, 'Bild hochladen'), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<DeliveryCredit?> setDeliveryCredit({
|
||||||
|
required String deliveryId,
|
||||||
|
required String clientEventId,
|
||||||
|
required int amountCents,
|
||||||
|
required String reason,
|
||||||
|
String? actorCarId,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final request = api.DeliveryCreditEventRequest((b) {
|
||||||
|
b
|
||||||
|
..clientEventId = clientEventId
|
||||||
|
..action = api.CreditAction.set_
|
||||||
|
..amountCents = amountCents
|
||||||
|
..reason = reason;
|
||||||
|
if (actorCarId != null) b.authorCarId = actorCarId;
|
||||||
|
});
|
||||||
|
final response = await _api.getDeliveriesApi().applyCredit(
|
||||||
|
deliveryId: deliveryId,
|
||||||
|
deliveryCreditEventRequest: request,
|
||||||
|
);
|
||||||
|
return response.data?.credit?.toDomain();
|
||||||
|
} on DioException catch (e) {
|
||||||
|
throw TourRepositoryException(_describe(e, 'Gutschrift setzen'), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<DeliveryCredit?> removeDeliveryCredit({
|
||||||
|
required String deliveryId,
|
||||||
|
required String clientEventId,
|
||||||
|
String? actorCarId,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final request = api.DeliveryCreditEventRequest((b) {
|
||||||
|
b
|
||||||
|
..clientEventId = clientEventId
|
||||||
|
..action = api.CreditAction.remove;
|
||||||
|
if (actorCarId != null) b.authorCarId = actorCarId;
|
||||||
|
});
|
||||||
|
final response = await _api.getDeliveriesApi().applyCredit(
|
||||||
|
deliveryId: deliveryId,
|
||||||
|
deliveryCreditEventRequest: request,
|
||||||
|
);
|
||||||
|
return response.data?.credit?.toDomain();
|
||||||
|
} on DioException catch (e) {
|
||||||
|
throw TourRepositoryException(_describe(e, 'Gutschrift entfernen'), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<DeliveryServiceValue> setDeliveryService({
|
||||||
|
required String deliveryId,
|
||||||
|
required String serviceId,
|
||||||
|
bool? boolValue,
|
||||||
|
int? numericValue,
|
||||||
|
String? actorCarId,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final request = api.SetDeliveryServiceRequest((b) {
|
||||||
|
if (boolValue != null) b.boolValue = boolValue;
|
||||||
|
if (numericValue != null) b.numericValue = numericValue;
|
||||||
|
if (actorCarId != null) b.authorCarId = actorCarId;
|
||||||
|
});
|
||||||
|
final response = await _api.getDeliveriesApi().setService(
|
||||||
|
deliveryId: deliveryId,
|
||||||
|
serviceId: serviceId,
|
||||||
|
setDeliveryServiceRequest: request,
|
||||||
|
);
|
||||||
|
final value = response.data?.value;
|
||||||
|
if (value == null) {
|
||||||
|
throw const TourRepositoryException(
|
||||||
|
'Server lieferte leeren Service-Wert',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return value.toDomain();
|
||||||
|
} on DioException catch (e) {
|
||||||
|
throw TourRepositoryException(_describe(e, 'Service setzen'), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> removeDeliveryService({
|
||||||
|
required String deliveryId,
|
||||||
|
required String serviceId,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
await _api.getDeliveriesApi().deleteServiceValue(
|
||||||
|
deliveryId: deliveryId,
|
||||||
|
serviceId: serviceId,
|
||||||
|
);
|
||||||
|
} on DioException catch (e) {
|
||||||
|
throw TourRepositoryException(_describe(e, 'Service entfernen'), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mappt das rohe Note-JSON (camelCase) der Upload-Antwort in die Domain.
|
||||||
|
/// Eigene Mini-Deserialisierung, weil dieser Pfad nicht über den
|
||||||
|
/// generierten Client (mit built_value) läuft.
|
||||||
|
/// Baut aus der rohen JSON-Map (Stamm-Endpoint-Response **ohne** Items)
|
||||||
|
/// eine Domain-Delivery. Wie [_liftDeliveryStub], aber für die direkten
|
||||||
|
/// Dio-Calls (multipart), die keinen typisierten Body liefern. Aufrufer
|
||||||
|
/// merged Items/sortOrder aus dem lokalen Aggregat.
|
||||||
|
Delivery _deliveryStubFromJson(Map<String, dynamic> j) {
|
||||||
|
final snap = j['deliveryAddressSnapshot'] as Map<String, dynamic>;
|
||||||
|
return Delivery(
|
||||||
|
id: j['id'] as String,
|
||||||
|
tourId: j['tourId'] as String,
|
||||||
|
customerId: j['customerId'] as String,
|
||||||
|
contactPersonIds:
|
||||||
|
(j['contactPersonIds'] as List).cast<String>().toList(growable: false),
|
||||||
|
deliveryAddressSnapshot: Address(
|
||||||
|
street: snap['street'] as String,
|
||||||
|
houseNumber: snap['houseNumber'] as String,
|
||||||
|
postalCode: snap['postalCode'] as String,
|
||||||
|
city: snap['city'] as String,
|
||||||
|
country: snap['country'] as String,
|
||||||
|
),
|
||||||
|
erpBelegartId: (j['erpBelegartId'] as num).toInt(),
|
||||||
|
erpBelegnummer: j['erpBelegnummer'] as String,
|
||||||
|
state: _deliveryStateFromWire(j['state'] as String),
|
||||||
|
stateReason: j['stateReason'] as String?,
|
||||||
|
sortOrder: 0,
|
||||||
|
assignedCarId: j['assignedCarId'] as String?,
|
||||||
|
desiredTime: j['desiredTime'] as String?,
|
||||||
|
specialAgreements: j['specialAgreements'] as String?,
|
||||||
|
items: const [],
|
||||||
|
prepaidAmount: (j['prepaidAmount'] as num).toDouble(),
|
||||||
|
paymentMethodId: j['paymentMethodId'] as String,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
DeliveryState _deliveryStateFromWire(String value) {
|
||||||
|
switch (value) {
|
||||||
|
case 'active':
|
||||||
|
return DeliveryState.active;
|
||||||
|
case 'held':
|
||||||
|
return DeliveryState.held;
|
||||||
|
case 'canceled':
|
||||||
|
return DeliveryState.canceled;
|
||||||
|
case 'completed':
|
||||||
|
return DeliveryState.completed;
|
||||||
|
default:
|
||||||
|
throw TourRepositoryException('Unbekannter DeliveryState: $value');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DeliveryNote _noteFromJson(Map<String, dynamic> j) => DeliveryNote(
|
||||||
|
id: j['id'] as String,
|
||||||
|
deliveryId: j['deliveryId'] as String,
|
||||||
|
text: j['text'] as String?,
|
||||||
|
imageAttachment: j['imageAttachment'] as String?,
|
||||||
|
authorPersonalnummer: (j['authorPersonalnummer'] as num).toInt(),
|
||||||
|
authorCarId: j['authorCarId'] as String?,
|
||||||
|
creditDeliveryItemId: j['creditDeliveryItemId'] as String?,
|
||||||
|
isAmountCreditNote: (j['isAmountCreditNote'] as bool?) ?? false,
|
||||||
|
createdAt: DateTime.parse(j['createdAt'] as String),
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Map<String, ScanOutcome>> applyScans(List<ScanIntent> intents) async {
|
||||||
|
if (intents.isEmpty) return const {};
|
||||||
|
try {
|
||||||
|
final request = api.ApplyScansRequest((b) {
|
||||||
|
b.scans.replace(intents.map((i) => i.toWire()));
|
||||||
|
});
|
||||||
|
final response = await _api.getScansApi().applyScans(
|
||||||
|
applyScansRequest: request,
|
||||||
|
);
|
||||||
|
final results = response.data?.results;
|
||||||
|
if (results == null) {
|
||||||
|
throw const TourRepositoryException(
|
||||||
|
'Server lieferte leere Scan-Antwort',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return {for (final r in results) r.clientScanId: r.toDomain()};
|
||||||
|
} on DioException catch (e) {
|
||||||
|
throw TourRepositoryException(_describe(e, 'Scans anwenden'), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _describe(DioException e, String operation) {
|
||||||
|
final status = e.response?.statusCode;
|
||||||
|
final body = e.response?.data;
|
||||||
|
if (status == 400 && body is Map && body['message'] != null) {
|
||||||
|
return '$operation fehlgeschlagen: ${body['message']}';
|
||||||
|
}
|
||||||
|
if (status == 401) return 'Sitzung abgelaufen';
|
||||||
|
if (status == 403) return 'Keine Berechtigung';
|
||||||
|
if (status == 404) return 'Tour oder Lieferung nicht gefunden';
|
||||||
|
return '$operation fehlgeschlagen (HTTP ${status ?? 'unbekannt'})';
|
||||||
|
}
|
||||||
|
}
|
||||||
55
lib/domain/entity/address.dart
Normal file
55
lib/domain/entity/address.dart
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
/// Postanschrift — Value-Object, identitätslos.
|
||||||
|
///
|
||||||
|
/// Tritt im Domain an drei Stellen auf: am `Customer` (Stamm-Adresse) und
|
||||||
|
/// als `deliveryAddressSnapshot` auf der `Delivery` (eingefrorene Kopie der
|
||||||
|
/// Adresse zum Zeitpunkt der Belegerzeugung, damit nachträgliche Änderungen
|
||||||
|
/// am Stammdatensatz die ausgelieferte Tour nicht „verschieben"). Spiegelt
|
||||||
|
/// das Backend-DTO `Address` 1:1.
|
||||||
|
class Address {
|
||||||
|
const Address({
|
||||||
|
required this.street,
|
||||||
|
required this.houseNumber,
|
||||||
|
required this.postalCode,
|
||||||
|
required this.city,
|
||||||
|
required this.country,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String street;
|
||||||
|
final String houseNumber;
|
||||||
|
final String postalCode;
|
||||||
|
final String city;
|
||||||
|
final String country;
|
||||||
|
|
||||||
|
/// Einzeilige Darstellung für Listen/Header.
|
||||||
|
String get oneLine =>
|
||||||
|
'$street $houseNumber, $postalCode $city';
|
||||||
|
|
||||||
|
Address copyWith({
|
||||||
|
String? street,
|
||||||
|
String? houseNumber,
|
||||||
|
String? postalCode,
|
||||||
|
String? city,
|
||||||
|
String? country,
|
||||||
|
}) {
|
||||||
|
return Address(
|
||||||
|
street: street ?? this.street,
|
||||||
|
houseNumber: houseNumber ?? this.houseNumber,
|
||||||
|
postalCode: postalCode ?? this.postalCode,
|
||||||
|
city: city ?? this.city,
|
||||||
|
country: country ?? this.country,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is Address &&
|
||||||
|
other.street == street &&
|
||||||
|
other.houseNumber == houseNumber &&
|
||||||
|
other.postalCode == postalCode &&
|
||||||
|
other.city == city &&
|
||||||
|
other.country == country;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(street, houseNumber, postalCode, city, country);
|
||||||
|
}
|
||||||
45
lib/domain/entity/article.dart
Normal file
45
lib/domain/entity/article.dart
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
/// Stammdatensatz für einen Artikel (Ware).
|
||||||
|
///
|
||||||
|
/// Der Domain-Artikel kennt — anders als das alte ERPframe-Modell — keine
|
||||||
|
/// Eltern-Kind-Beziehungen mehr. Stücklisten (BOM/Komponenten) werden im
|
||||||
|
/// neuen Backend als gleichrangige `DeliveryItem`s mit gesetztem
|
||||||
|
/// `komponentenArtikelNr` modelliert; der Treiber scannt einfach jedes Item
|
||||||
|
/// separat. Hier deshalb absichtlich kein `components`/`parent`-Feld.
|
||||||
|
class Article {
|
||||||
|
const Article({
|
||||||
|
required this.id,
|
||||||
|
required this.articleNumber,
|
||||||
|
required this.name,
|
||||||
|
required this.scannable,
|
||||||
|
this.defaultWarehouseId,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
final String articleNumber;
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
/// Nicht-scanbar = wird nicht über den Scanner durchgereicht (z. B.
|
||||||
|
/// Dienstleistung, Versandkosten). In der Loading-Phase ausgeblendet.
|
||||||
|
final bool scannable;
|
||||||
|
|
||||||
|
/// Lager-Default für diesen Artikel; das tatsächlich relevante Lager pro
|
||||||
|
/// Lieferung steht aber am `DeliveryItem.warehouseId`. Wird nur als
|
||||||
|
/// UX-Hinweis verwendet.
|
||||||
|
final String? defaultWarehouseId;
|
||||||
|
|
||||||
|
Article copyWith({
|
||||||
|
String? id,
|
||||||
|
String? articleNumber,
|
||||||
|
String? name,
|
||||||
|
bool? scannable,
|
||||||
|
String? defaultWarehouseId,
|
||||||
|
}) {
|
||||||
|
return Article(
|
||||||
|
id: id ?? this.id,
|
||||||
|
articleNumber: articleNumber ?? this.articleNumber,
|
||||||
|
name: name ?? this.name,
|
||||||
|
scannable: scannable ?? this.scannable,
|
||||||
|
defaultWarehouseId: defaultWarehouseId ?? this.defaultWarehouseId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
226
lib/domain/entity/contact_source.dart
Normal file
226
lib/domain/entity/contact_source.dart
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
/// Adress-Rolle eines Beleg-Kontakts. Spiegelt die fünf Adress-FKs am
|
||||||
|
/// ERP-`Belegkopf` (bzw. den Umweg über `Kunden.AdressId`). Die App nutzt
|
||||||
|
/// das primär als Gruppierungs-Label in der Detail-Ansicht.
|
||||||
|
enum ContactRole {
|
||||||
|
/// `Belegkopf.AdressId` — die „eigentliche" Belegadresse.
|
||||||
|
header,
|
||||||
|
|
||||||
|
/// `Belegkopf.LieferAdressId` — kann von der Belegadresse abweichen.
|
||||||
|
delivery,
|
||||||
|
|
||||||
|
/// `Belegkopf.RechnungsAdressId`.
|
||||||
|
billing,
|
||||||
|
|
||||||
|
/// `Belegkopf.AnsprechpartnerId` — verlinkt eine Person, nicht eine Firma.
|
||||||
|
contactPerson,
|
||||||
|
|
||||||
|
/// `Kunden.AdressId` (über `Belegkopf.KundenId`). Die Stammadresse des
|
||||||
|
/// Kunden — dient als Fallback, wenn die belegspezifischen Adressen leer
|
||||||
|
/// sind.
|
||||||
|
customerMaster;
|
||||||
|
|
||||||
|
/// Wire-Repräsentation aus dem Backend (serde `snake_case`).
|
||||||
|
static ContactRole fromWire(String value) {
|
||||||
|
switch (value) {
|
||||||
|
case 'header':
|
||||||
|
return ContactRole.header;
|
||||||
|
case 'delivery':
|
||||||
|
return ContactRole.delivery;
|
||||||
|
case 'billing':
|
||||||
|
return ContactRole.billing;
|
||||||
|
case 'contact_person':
|
||||||
|
return ContactRole.contactPerson;
|
||||||
|
case 'customer_master':
|
||||||
|
return ContactRole.customerMaster;
|
||||||
|
default:
|
||||||
|
throw StateError('Unbekannte ContactRole vom Backend: $value');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deutscher Label-Text für die UI.
|
||||||
|
String get label {
|
||||||
|
switch (this) {
|
||||||
|
case ContactRole.header:
|
||||||
|
return 'Belegadresse';
|
||||||
|
case ContactRole.delivery:
|
||||||
|
return 'Lieferadresse';
|
||||||
|
case ContactRole.billing:
|
||||||
|
return 'Rechnungsadresse';
|
||||||
|
case ContactRole.contactPerson:
|
||||||
|
return 'Ansprechpartner';
|
||||||
|
case ContactRole.customerMaster:
|
||||||
|
return 'Kundenstamm';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Art eines Kommunikationskanals. `fax` wird vom Backend bewusst nicht
|
||||||
|
/// mitgeführt — die App braucht es nicht.
|
||||||
|
enum ContactKind {
|
||||||
|
phone,
|
||||||
|
mobile,
|
||||||
|
email,
|
||||||
|
web;
|
||||||
|
|
||||||
|
static ContactKind fromWire(String value) {
|
||||||
|
switch (value) {
|
||||||
|
case 'phone':
|
||||||
|
return ContactKind.phone;
|
||||||
|
case 'mobile':
|
||||||
|
return ContactKind.mobile;
|
||||||
|
case 'email':
|
||||||
|
return ContactKind.email;
|
||||||
|
case 'web':
|
||||||
|
return ContactKind.web;
|
||||||
|
default:
|
||||||
|
throw StateError('Unbekannter ContactKind vom Backend: $value');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Eine Adress-Quelle, die am Beleg hängt — z. B. Lieferadresse oder
|
||||||
|
/// Ansprechpartner. Der Namensblock kommt direkt aus ERP-`Adressen`
|
||||||
|
/// (`Anrede`/`Titel`/`Name1..3`/`Abteilung`/`Funktion`); die eigentlichen
|
||||||
|
/// Telefonnummern, E-Mails etc. liegen verteilt in zugehörigen
|
||||||
|
/// [ContactChannel]s und werden in [TourDetails.channelsOf] zusammengeführt.
|
||||||
|
class ContactSource {
|
||||||
|
const ContactSource({
|
||||||
|
required this.id,
|
||||||
|
required this.deliveryId,
|
||||||
|
required this.role,
|
||||||
|
this.anrede,
|
||||||
|
this.titel,
|
||||||
|
this.name1,
|
||||||
|
this.name2,
|
||||||
|
this.name3,
|
||||||
|
this.abteilung,
|
||||||
|
this.funktion,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
final String deliveryId;
|
||||||
|
final ContactRole role;
|
||||||
|
|
||||||
|
final String? anrede;
|
||||||
|
final String? titel;
|
||||||
|
final String? name1;
|
||||||
|
final String? name2;
|
||||||
|
final String? name3;
|
||||||
|
final String? abteilung;
|
||||||
|
final String? funktion;
|
||||||
|
|
||||||
|
/// Zusammengesetzte Anzeige des Namens — Anrede + Titel + Name1..3 in
|
||||||
|
/// dieser Reihenfolge, leere Felder werden übersprungen. Gibt `null`
|
||||||
|
/// zurück, wenn die Quelle gar keinen Namen trägt (kann vorkommen, wenn
|
||||||
|
/// nur Telefonnummern hinterlegt sind).
|
||||||
|
String? get displayName {
|
||||||
|
final parts = <String>[
|
||||||
|
if (anrede != null && anrede!.isNotEmpty) anrede!,
|
||||||
|
if (titel != null && titel!.isNotEmpty) titel!,
|
||||||
|
if (name1 != null && name1!.isNotEmpty) name1!,
|
||||||
|
if (name2 != null && name2!.isNotEmpty) name2!,
|
||||||
|
if (name3 != null && name3!.isNotEmpty) name3!,
|
||||||
|
];
|
||||||
|
if (parts.isEmpty) return null;
|
||||||
|
return parts.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Funktionale Zusatzinfo (z. B. „Buchhaltung · Leitung"). Leere
|
||||||
|
/// Komponenten werden ausgeblendet.
|
||||||
|
String? get subtitle {
|
||||||
|
final parts = <String>[
|
||||||
|
if (abteilung != null && abteilung!.isNotEmpty) abteilung!,
|
||||||
|
if (funktion != null && funktion!.isNotEmpty) funktion!,
|
||||||
|
];
|
||||||
|
if (parts.isEmpty) return null;
|
||||||
|
return parts.join(' · ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ein einzelner Kommunikationskanal (Telefon, Mobil, E-Mail, Web). Mehrere
|
||||||
|
/// pro [ContactSource] möglich; die [position] (1-basiert) erhält die
|
||||||
|
/// ERP-Reihenfolge — Position 1 ist der primäre Kanal, Position 2 das
|
||||||
|
/// erste Zusatzfeld usw.
|
||||||
|
class ContactChannel {
|
||||||
|
const ContactChannel({
|
||||||
|
required this.id,
|
||||||
|
required this.sourceId,
|
||||||
|
required this.kind,
|
||||||
|
required this.position,
|
||||||
|
required this.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
final String sourceId;
|
||||||
|
final ContactKind kind;
|
||||||
|
final int position;
|
||||||
|
final String value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Zusammengeführte Sicht auf 1..n [ContactSource]s, die fachlich denselben
|
||||||
|
/// Kontakt darstellen — gleicher Namensblock UND gleiche Channel-Liste
|
||||||
|
/// (also exakt dieselbe Adresse im ERP, nur über verschiedene FKs am
|
||||||
|
/// `Belegkopf` referenziert: typischerweise `AdressId` und `Kunden.AdressId`,
|
||||||
|
/// die in den allermeisten Belegen identisch sind).
|
||||||
|
///
|
||||||
|
/// Die App rendert pro Lieferung eine Karte je Eintrag; `roles` listet
|
||||||
|
/// alle Rollen auf, die zu diesem Eintrag beitragen (z. B. „Belegadresse ·
|
||||||
|
/// Kundenstamm"). Die Channels werden 1:1 von der ersten Quelle übernommen
|
||||||
|
/// — alle Quellen in einer Gruppe haben dieselben.
|
||||||
|
class MergedContactSource {
|
||||||
|
const MergedContactSource({
|
||||||
|
required this.roles,
|
||||||
|
required this.anrede,
|
||||||
|
required this.titel,
|
||||||
|
required this.name1,
|
||||||
|
required this.name2,
|
||||||
|
required this.name3,
|
||||||
|
required this.abteilung,
|
||||||
|
required this.funktion,
|
||||||
|
required this.channels,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Alle Rollen, die diesen zusammengeführten Kontakt liefern.
|
||||||
|
/// Reihenfolge wie in der enum-Definition: header → delivery → billing
|
||||||
|
/// → contactPerson → customerMaster, damit das Label stabil bleibt.
|
||||||
|
final List<ContactRole> roles;
|
||||||
|
|
||||||
|
final String? anrede;
|
||||||
|
final String? titel;
|
||||||
|
final String? name1;
|
||||||
|
final String? name2;
|
||||||
|
final String? name3;
|
||||||
|
final String? abteilung;
|
||||||
|
final String? funktion;
|
||||||
|
|
||||||
|
/// Channels in der gleichen Reihenfolge, wie das Backend sie pro Quelle
|
||||||
|
/// liefert (kind + ERP-Position).
|
||||||
|
final List<ContactChannel> channels;
|
||||||
|
|
||||||
|
/// Zusammengesetzter Anzeigename — identisch zu [ContactSource.displayName].
|
||||||
|
String? get displayName {
|
||||||
|
final parts = <String>[
|
||||||
|
if (anrede != null && anrede!.isNotEmpty) anrede!,
|
||||||
|
if (titel != null && titel!.isNotEmpty) titel!,
|
||||||
|
if (name1 != null && name1!.isNotEmpty) name1!,
|
||||||
|
if (name2 != null && name2!.isNotEmpty) name2!,
|
||||||
|
if (name3 != null && name3!.isNotEmpty) name3!,
|
||||||
|
];
|
||||||
|
if (parts.isEmpty) return null;
|
||||||
|
return parts.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
String? get subtitle {
|
||||||
|
final parts = <String>[
|
||||||
|
if (abteilung != null && abteilung!.isNotEmpty) abteilung!,
|
||||||
|
if (funktion != null && funktion!.isNotEmpty) funktion!,
|
||||||
|
];
|
||||||
|
if (parts.isEmpty) return null;
|
||||||
|
return parts.join(' · ');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Header-Label für die UI — alle Rollen mit `·` getrennt, in
|
||||||
|
/// Enum-Reihenfolge.
|
||||||
|
String get rolesLabel => roles.map((r) => r.label).join(' · ');
|
||||||
|
}
|
||||||
|
|
||||||
53
lib/domain/entity/customer.dart
Normal file
53
lib/domain/entity/customer.dart
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import 'address.dart';
|
||||||
|
|
||||||
|
/// Kunden-Stammdatensatz. Ein Kunde kann mehrere `CustomerContact`s haben
|
||||||
|
/// (Ehepartner, Hausverwalter, …); diese werden separat in der
|
||||||
|
/// `TourDetails.contacts`-Map geführt.
|
||||||
|
class Customer {
|
||||||
|
const Customer({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.erpCustomerId,
|
||||||
|
required this.address,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
final String name;
|
||||||
|
|
||||||
|
/// ERP-Kundennummer (Legacy). Wird in der App nur informativ in der
|
||||||
|
/// Detail-Ansicht angezeigt.
|
||||||
|
final int erpCustomerId;
|
||||||
|
final Address address;
|
||||||
|
|
||||||
|
Customer copyWith({
|
||||||
|
String? id,
|
||||||
|
String? name,
|
||||||
|
int? erpCustomerId,
|
||||||
|
Address? address,
|
||||||
|
}) {
|
||||||
|
return Customer(
|
||||||
|
id: id ?? this.id,
|
||||||
|
name: name ?? this.name,
|
||||||
|
erpCustomerId: erpCustomerId ?? this.erpCustomerId,
|
||||||
|
address: address ?? this.address,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ansprechpartner zu einem Kunden. Optional, daher als eigene Liste in
|
||||||
|
/// `TourDetails` — eine Lieferung referenziert n Kontakte per Id.
|
||||||
|
class CustomerContact {
|
||||||
|
const CustomerContact({
|
||||||
|
required this.id,
|
||||||
|
required this.customerId,
|
||||||
|
required this.name,
|
||||||
|
this.phone,
|
||||||
|
this.email,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
final String customerId;
|
||||||
|
final String name;
|
||||||
|
final String? phone;
|
||||||
|
final String? email;
|
||||||
|
}
|
||||||
152
lib/domain/entity/delivery.dart
Normal file
152
lib/domain/entity/delivery.dart
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
import 'address.dart';
|
||||||
|
import 'delivery_item.dart';
|
||||||
|
|
||||||
|
/// Lebenszyklus einer Lieferung.
|
||||||
|
///
|
||||||
|
/// - `active`: Standard nach Anlage; Fahrer kann scannen/ausliefern.
|
||||||
|
/// - `held`: Pausiert (Kunde nicht da, Termin verschoben) — kein Bearbeitungsfortschritt.
|
||||||
|
/// - `canceled`: Abgebrochen — wird nicht mehr ausgeliefert.
|
||||||
|
/// - `completed`: Abgeschlossen — Signatur und Notizen sind hinterlegt.
|
||||||
|
enum DeliveryState { active, held, canceled, completed }
|
||||||
|
|
||||||
|
/// Eine einzelne Auslieferung an einen Kunden innerhalb einer Tour.
|
||||||
|
///
|
||||||
|
/// Anders als im alten Modell trägt `Delivery` hier ausschließlich
|
||||||
|
/// Logistik-Daten — keine Preise, keine Rabatte, keine Zahlungsoptionen.
|
||||||
|
/// Diese ERP-Themen sind in Phase C+D-2 absichtlich nicht migriert und
|
||||||
|
/// hängen hinter `FeatureFlags`.
|
||||||
|
class Delivery {
|
||||||
|
const Delivery({
|
||||||
|
required this.id,
|
||||||
|
required this.tourId,
|
||||||
|
required this.customerId,
|
||||||
|
required this.contactPersonIds,
|
||||||
|
required this.deliveryAddressSnapshot,
|
||||||
|
required this.erpBelegartId,
|
||||||
|
required this.erpBelegnummer,
|
||||||
|
required this.state,
|
||||||
|
required this.sortOrder,
|
||||||
|
required this.items,
|
||||||
|
required this.prepaidAmount,
|
||||||
|
required this.paymentMethodId,
|
||||||
|
this.assignedCarId,
|
||||||
|
this.desiredTime,
|
||||||
|
this.specialAgreements,
|
||||||
|
this.stateReason,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
final String tourId;
|
||||||
|
final String customerId;
|
||||||
|
|
||||||
|
/// 0..n Kontakte am Kunden, die für diese Lieferung relevant sind.
|
||||||
|
/// Lookup über `TourDetails.contactById`.
|
||||||
|
final List<String> contactPersonIds;
|
||||||
|
|
||||||
|
/// Eingefrorene Lieferadresse zum Zeitpunkt der Belegerzeugung — bleibt
|
||||||
|
/// stabil, auch wenn die Stammadresse am Kunden später geändert wird.
|
||||||
|
final Address deliveryAddressSnapshot;
|
||||||
|
|
||||||
|
/// ERP-Belegart (Lieferschein, Rechnung, …) und -Nummer. Für die App nur
|
||||||
|
/// informativ; in Notizen/Reklamationen ist die Belegnummer der vom
|
||||||
|
/// Kunden verständliche Bezugspunkt.
|
||||||
|
final int erpBelegartId;
|
||||||
|
final String erpBelegnummer;
|
||||||
|
|
||||||
|
final DeliveryState state;
|
||||||
|
|
||||||
|
/// Optionaler Klartext, warum `state` auf `held`/`canceled` steht. Vom
|
||||||
|
/// Backend nicht-leer erzwungen, sobald ein Reason-pflichtiger Zustand
|
||||||
|
/// gesetzt wird.
|
||||||
|
final String? stateReason;
|
||||||
|
|
||||||
|
/// Sortier-Reihenfolge innerhalb der Tour, gesetzt durch
|
||||||
|
/// `PUT /tours/{id}/delivery-order`. Niedriger = früher.
|
||||||
|
final int sortOrder;
|
||||||
|
|
||||||
|
/// UUID des Fahrzeugs, dem diese Lieferung beim Laden zugewiesen wurde.
|
||||||
|
/// `null` = noch nicht zugewiesen.
|
||||||
|
final String? assignedCarId;
|
||||||
|
|
||||||
|
/// Bei Bestellung schon bezahlter Betrag in EUR. `0.0` wenn der Kunde
|
||||||
|
/// alles bei Lieferung zahlt. Wird vom ERP-Sync gesetzt.
|
||||||
|
final double prepaidAmount;
|
||||||
|
|
||||||
|
/// FK auf eine `PaymentMethod` (UUID). Auflösung zu Display-Name und
|
||||||
|
/// Aktiv-Status geht über die Stammdaten-Liste, die die App separat
|
||||||
|
/// lädt — nicht hier embeddet, damit das Tour-Aggregat klein bleibt.
|
||||||
|
final String paymentMethodId;
|
||||||
|
|
||||||
|
final String? desiredTime;
|
||||||
|
final String? specialAgreements;
|
||||||
|
|
||||||
|
final List<DeliveryItem> items;
|
||||||
|
|
||||||
|
// ─── Abgeleitete Sicht-Eigenschaften ──────────────────────────────────
|
||||||
|
|
||||||
|
/// Nur Items, die der Treiber tatsächlich scannen muss. Nicht-scanbare
|
||||||
|
/// Artikel (Dienstleistungen, Versand) sowie bereits entfernte Items
|
||||||
|
/// werden nicht mitgezählt.
|
||||||
|
Iterable<DeliveryItem> scannableItems(
|
||||||
|
bool Function(String articleId) isScannable,
|
||||||
|
) sync* {
|
||||||
|
for (final item in items) {
|
||||||
|
if (item.isRemoved) continue;
|
||||||
|
if (!isScannable(item.articleId)) continue;
|
||||||
|
yield item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `true`, sobald *alle* scanbaren Items dieser Lieferung als `done`
|
||||||
|
/// markiert sind. Wird in der Loading-Übersicht angezeigt und
|
||||||
|
/// kontrolliert in der Detail-Phase den Übergang zur Signatur.
|
||||||
|
bool allScannableItemsDone(bool Function(String articleId) isScannable) {
|
||||||
|
final scannables = scannableItems(isScannable).toList();
|
||||||
|
if (scannables.isEmpty) return false;
|
||||||
|
return scannables.every((item) => item.isDone);
|
||||||
|
}
|
||||||
|
|
||||||
|
Delivery copyWith({
|
||||||
|
String? id,
|
||||||
|
String? tourId,
|
||||||
|
String? customerId,
|
||||||
|
List<String>? contactPersonIds,
|
||||||
|
Address? deliveryAddressSnapshot,
|
||||||
|
int? erpBelegartId,
|
||||||
|
String? erpBelegnummer,
|
||||||
|
DeliveryState? state,
|
||||||
|
String? stateReason,
|
||||||
|
int? sortOrder,
|
||||||
|
String? assignedCarId,
|
||||||
|
Object? desiredTime = _sentinel,
|
||||||
|
Object? specialAgreements = _sentinel,
|
||||||
|
List<DeliveryItem>? items,
|
||||||
|
double? prepaidAmount,
|
||||||
|
String? paymentMethodId,
|
||||||
|
}) {
|
||||||
|
return Delivery(
|
||||||
|
id: id ?? this.id,
|
||||||
|
tourId: tourId ?? this.tourId,
|
||||||
|
customerId: customerId ?? this.customerId,
|
||||||
|
contactPersonIds: contactPersonIds ?? this.contactPersonIds,
|
||||||
|
deliveryAddressSnapshot: deliveryAddressSnapshot ?? this.deliveryAddressSnapshot,
|
||||||
|
erpBelegartId: erpBelegartId ?? this.erpBelegartId,
|
||||||
|
erpBelegnummer: erpBelegnummer ?? this.erpBelegnummer,
|
||||||
|
state: state ?? this.state,
|
||||||
|
stateReason: stateReason ?? this.stateReason,
|
||||||
|
sortOrder: sortOrder ?? this.sortOrder,
|
||||||
|
assignedCarId: assignedCarId ?? this.assignedCarId,
|
||||||
|
desiredTime: identical(desiredTime, _sentinel)
|
||||||
|
? this.desiredTime
|
||||||
|
: desiredTime as String?,
|
||||||
|
specialAgreements: identical(specialAgreements, _sentinel)
|
||||||
|
? this.specialAgreements
|
||||||
|
: specialAgreements as String?,
|
||||||
|
items: items ?? this.items,
|
||||||
|
prepaidAmount: prepaidAmount ?? this.prepaidAmount,
|
||||||
|
paymentMethodId: paymentMethodId ?? this.paymentMethodId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const Object _sentinel = Object();
|
||||||
20
lib/domain/entity/delivery_credit.dart
Normal file
20
lib/domain/entity/delivery_credit.dart
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
/// Aktuelle Betrags-Gutschrift einer Lieferung (Geld-Nachlass, unabhängig von
|
||||||
|
/// Stückzahl). Server-seitig aus dem append-only `delivery_credit_audit`
|
||||||
|
/// abgeleitet (jüngstes Ereignis); existiert nur, solange der letzte Stand
|
||||||
|
/// `set` ist.
|
||||||
|
class DeliveryCredit {
|
||||||
|
const DeliveryCredit({
|
||||||
|
required this.deliveryId,
|
||||||
|
required this.amountCents,
|
||||||
|
required this.reason,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String deliveryId;
|
||||||
|
|
||||||
|
/// Betrag in Cent (> 0, ≤ 15000).
|
||||||
|
final int amountCents;
|
||||||
|
final String reason;
|
||||||
|
|
||||||
|
/// Betrag in ganzen Euro (die Gutschrift läuft in 10-€-Schritten).
|
||||||
|
int get amountEuros => (amountCents / 100).round();
|
||||||
|
}
|
||||||
106
lib/domain/entity/delivery_item.dart
Normal file
106
lib/domain/entity/delivery_item.dart
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import 'scan_progress.dart';
|
||||||
|
|
||||||
|
/// Eine Belegzeile innerhalb einer Lieferung.
|
||||||
|
///
|
||||||
|
/// Verweist über `articleId` auf den Artikel-Stamm (lookup via
|
||||||
|
/// `TourDetails.articleById`) und über `warehouseId` auf das Lager. Die
|
||||||
|
/// Soll-/Ist-Quantitäten leben hier: `requiredQuantity` ist statisch (ERP),
|
||||||
|
/// `scanProgress.scannedQuantity` wandert mit jedem Scan nach oben.
|
||||||
|
///
|
||||||
|
/// `komponentenArtikelNr` markiert Stücklisten-Komponenten. Im neuen
|
||||||
|
/// Backend gibt es **keine** Parent-/Child-Hierarchie mehr — jedes Item ist
|
||||||
|
/// gleichrangig; das Feld dient nur noch der Anzeige ("Teil von X") und
|
||||||
|
/// hat keinerlei Scan-Semantik.
|
||||||
|
class DeliveryItem {
|
||||||
|
const DeliveryItem({
|
||||||
|
required this.id,
|
||||||
|
required this.deliveryId,
|
||||||
|
required this.articleId,
|
||||||
|
required this.warehouseId,
|
||||||
|
required this.belegzeilenNr,
|
||||||
|
required this.requiredQuantity,
|
||||||
|
required this.scanProgress,
|
||||||
|
this.unitPrice = 0,
|
||||||
|
this.komponentenArtikelNr,
|
||||||
|
this.parentArtikelNr,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
final String deliveryId;
|
||||||
|
final String articleId;
|
||||||
|
final String warehouseId;
|
||||||
|
|
||||||
|
/// ERP-Belegzeilen-Nummer. Bestimmt die Reihenfolge der Items in der
|
||||||
|
/// Detail-Ansicht (aufsteigend).
|
||||||
|
final int belegzeilenNr;
|
||||||
|
final int requiredQuantity;
|
||||||
|
final ScanProgress scanProgress;
|
||||||
|
|
||||||
|
/// Stückpreis (brutto, EUR) aus dem ERP-Sync.
|
||||||
|
final double unitPrice;
|
||||||
|
|
||||||
|
final String? komponentenArtikelNr;
|
||||||
|
|
||||||
|
/// Artikelnummer des Oberartikels, zu dem diese Komponente gehört (aus dem
|
||||||
|
/// Sync). `null` bei Oberartikeln/regulären Zeilen. Die Liste rückt
|
||||||
|
/// Komponenten unter ihrem Oberartikel ein.
|
||||||
|
final String? parentArtikelNr;
|
||||||
|
|
||||||
|
/// `true`, wenn dieses Item eine Stücklisten-Komponente ist (gehört unter
|
||||||
|
/// einen Oberartikel).
|
||||||
|
bool get isComponent => parentArtikelNr != null;
|
||||||
|
|
||||||
|
// ─── Abgeleitete Sicht-Eigenschaften ──────────────────────────────────
|
||||||
|
|
||||||
|
/// Tatsächlich auszuliefernde Menge = Soll − Gutschrift. Nie negativ.
|
||||||
|
int get deliveredQuantity {
|
||||||
|
final d = requiredQuantity - scanProgress.creditedQuantity;
|
||||||
|
return d < 0 ? 0 : d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wert der ausgelieferten Menge dieser Position (brutto, EUR).
|
||||||
|
double get lineTotal => unitPrice * deliveredQuantity;
|
||||||
|
|
||||||
|
/// Vollständig gescannt (Status `done` oder Ist ≥ Soll).
|
||||||
|
bool get isDone =>
|
||||||
|
scanProgress.status == ScanStatus.done ||
|
||||||
|
scanProgress.scannedQuantity >= requiredQuantity;
|
||||||
|
|
||||||
|
/// Aktuell pausiert.
|
||||||
|
bool get isHeld => scanProgress.status == ScanStatus.held;
|
||||||
|
|
||||||
|
/// Nach dem Laden wieder entfernt.
|
||||||
|
bool get isRemoved => scanProgress.status == ScanStatus.removed;
|
||||||
|
|
||||||
|
/// Noch offene Restmenge (für Loading-UI). Nicht negativ.
|
||||||
|
int get remainingQuantity {
|
||||||
|
final remaining = requiredQuantity - scanProgress.scannedQuantity;
|
||||||
|
return remaining < 0 ? 0 : remaining;
|
||||||
|
}
|
||||||
|
|
||||||
|
DeliveryItem copyWith({
|
||||||
|
String? id,
|
||||||
|
String? deliveryId,
|
||||||
|
String? articleId,
|
||||||
|
String? warehouseId,
|
||||||
|
int? belegzeilenNr,
|
||||||
|
int? requiredQuantity,
|
||||||
|
ScanProgress? scanProgress,
|
||||||
|
double? unitPrice,
|
||||||
|
String? komponentenArtikelNr,
|
||||||
|
String? parentArtikelNr,
|
||||||
|
}) {
|
||||||
|
return DeliveryItem(
|
||||||
|
id: id ?? this.id,
|
||||||
|
deliveryId: deliveryId ?? this.deliveryId,
|
||||||
|
articleId: articleId ?? this.articleId,
|
||||||
|
warehouseId: warehouseId ?? this.warehouseId,
|
||||||
|
belegzeilenNr: belegzeilenNr ?? this.belegzeilenNr,
|
||||||
|
requiredQuantity: requiredQuantity ?? this.requiredQuantity,
|
||||||
|
scanProgress: scanProgress ?? this.scanProgress,
|
||||||
|
unitPrice: unitPrice ?? this.unitPrice,
|
||||||
|
komponentenArtikelNr: komponentenArtikelNr ?? this.komponentenArtikelNr,
|
||||||
|
parentArtikelNr: parentArtikelNr ?? this.parentArtikelNr,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
83
lib/domain/entity/delivery_note.dart
Normal file
83
lib/domain/entity/delivery_note.dart
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
/// Notiz an einer Lieferung. Text und/oder Bildanhang können gesetzt sein —
|
||||||
|
/// das Backend erzwingt nicht-leer für mindestens einen der beiden.
|
||||||
|
///
|
||||||
|
/// `imageAttachment` ist die UUID des hinterlegten Bildes; das eigentliche
|
||||||
|
/// Binary wird über einen separaten Endpoint geladen (in einer späteren
|
||||||
|
/// Phase modelliert).
|
||||||
|
class DeliveryNote {
|
||||||
|
const DeliveryNote({
|
||||||
|
required this.id,
|
||||||
|
required this.deliveryId,
|
||||||
|
required this.authorPersonalnummer,
|
||||||
|
required this.createdAt,
|
||||||
|
this.text,
|
||||||
|
this.imageAttachment,
|
||||||
|
this.authorCarId,
|
||||||
|
this.creditDeliveryItemId,
|
||||||
|
this.isAmountCreditNote = false,
|
||||||
|
this.imageAttachmentDeleted = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
final String deliveryId;
|
||||||
|
final String? text;
|
||||||
|
final String? imageAttachment;
|
||||||
|
|
||||||
|
/// Personalnummer des Fahrers (aus dem JWT zum Zeitpunkt der Erstellung).
|
||||||
|
/// `int` weil im JWT als numerischer Claim transportiert.
|
||||||
|
final int authorPersonalnummer;
|
||||||
|
|
||||||
|
/// Fahrzeug, mit dem die Notiz erstellt wurde (Audit-Spur, optional).
|
||||||
|
final String? authorCarId;
|
||||||
|
|
||||||
|
/// Gesetzt, wenn die Notiz als Gutschrift-Grund zu einer Belegzeile
|
||||||
|
/// angelegt wurde (deren `DeliveryItem`-Id). Erlaubt es, die Notiz beim
|
||||||
|
/// Zurücknehmen der Gutschrift (Unremove) gezielt wieder zu löschen.
|
||||||
|
final String? creditDeliveryItemId;
|
||||||
|
|
||||||
|
/// `true`, wenn die Notiz den Grund einer Betrags-Gutschrift dokumentiert
|
||||||
|
/// (Lieferungs-Ebene). Wird beim Entfernen der Gutschrift gezielt gelöscht.
|
||||||
|
final bool isAmountCreditNote;
|
||||||
|
|
||||||
|
/// `true`, wenn die lokale Bilddatei nach erfolgreichem Report-Upload
|
||||||
|
/// gelöscht wurde — das Bild steckt dann im Lieferbericht (DOCUframe).
|
||||||
|
/// Die UI zeigt statt der Vorschau einen Hinweis.
|
||||||
|
final bool imageAttachmentDeleted;
|
||||||
|
|
||||||
|
final DateTime createdAt;
|
||||||
|
|
||||||
|
DeliveryNote copyWith({
|
||||||
|
String? id,
|
||||||
|
String? deliveryId,
|
||||||
|
Object? text = _sentinel,
|
||||||
|
Object? imageAttachment = _sentinel,
|
||||||
|
int? authorPersonalnummer,
|
||||||
|
Object? authorCarId = _sentinel,
|
||||||
|
Object? creditDeliveryItemId = _sentinel,
|
||||||
|
bool? isAmountCreditNote,
|
||||||
|
bool? imageAttachmentDeleted,
|
||||||
|
DateTime? createdAt,
|
||||||
|
}) {
|
||||||
|
return DeliveryNote(
|
||||||
|
id: id ?? this.id,
|
||||||
|
deliveryId: deliveryId ?? this.deliveryId,
|
||||||
|
text: identical(text, _sentinel) ? this.text : text as String?,
|
||||||
|
imageAttachment: identical(imageAttachment, _sentinel)
|
||||||
|
? this.imageAttachment
|
||||||
|
: imageAttachment as String?,
|
||||||
|
authorPersonalnummer: authorPersonalnummer ?? this.authorPersonalnummer,
|
||||||
|
authorCarId: identical(authorCarId, _sentinel)
|
||||||
|
? this.authorCarId
|
||||||
|
: authorCarId as String?,
|
||||||
|
creditDeliveryItemId: identical(creditDeliveryItemId, _sentinel)
|
||||||
|
? this.creditDeliveryItemId
|
||||||
|
: creditDeliveryItemId as String?,
|
||||||
|
isAmountCreditNote: isAmountCreditNote ?? this.isAmountCreditNote,
|
||||||
|
imageAttachmentDeleted:
|
||||||
|
imageAttachmentDeleted ?? this.imageAttachmentDeleted,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const Object _sentinel = Object();
|
||||||
15
lib/domain/entity/delivery_service_value.dart
Normal file
15
lib/domain/entity/delivery_service_value.dart
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
/// Pro-Lieferung gesetzter Wert eines Service. Je nach Service-Typ ist genau
|
||||||
|
/// einer der beiden Slots gefüllt.
|
||||||
|
class DeliveryServiceValue {
|
||||||
|
const DeliveryServiceValue({
|
||||||
|
required this.deliveryId,
|
||||||
|
required this.serviceId,
|
||||||
|
this.boolValue,
|
||||||
|
this.numericValue,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String deliveryId;
|
||||||
|
final String serviceId;
|
||||||
|
final bool? boolValue;
|
||||||
|
final int? numericValue;
|
||||||
|
}
|
||||||
38
lib/domain/entity/payment_method.dart
Normal file
38
lib/domain/entity/payment_method.dart
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
/// Zahlungs-Stammdatensatz — spiegelt das Backend-Aggregat `PaymentMethod`.
|
||||||
|
///
|
||||||
|
/// `code` ist der stabile Programm-Identifier (z. B. `"cash"`,
|
||||||
|
/// `"invoice"`); UI-Code kann darüber spezielle Methoden referenzieren,
|
||||||
|
/// ohne die UUID kennen zu müssen. `active = false` ist Soft-Delete —
|
||||||
|
/// die Methode bleibt für historische Lieferungen referenzierbar,
|
||||||
|
/// taucht aber in der Auswahl bei neuen Lieferungen nicht mehr auf.
|
||||||
|
class PaymentMethod {
|
||||||
|
const PaymentMethod({
|
||||||
|
required this.id,
|
||||||
|
required this.code,
|
||||||
|
required this.name,
|
||||||
|
required this.active,
|
||||||
|
required this.createdAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
final String code;
|
||||||
|
final String name;
|
||||||
|
final bool active;
|
||||||
|
final DateTime createdAt;
|
||||||
|
|
||||||
|
PaymentMethod copyWith({
|
||||||
|
String? id,
|
||||||
|
String? code,
|
||||||
|
String? name,
|
||||||
|
bool? active,
|
||||||
|
DateTime? createdAt,
|
||||||
|
}) {
|
||||||
|
return PaymentMethod(
|
||||||
|
id: id ?? this.id,
|
||||||
|
code: code ?? this.code,
|
||||||
|
name: name ?? this.name,
|
||||||
|
active: active ?? this.active,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
93
lib/domain/entity/scan_intent.dart
Normal file
93
lib/domain/entity/scan_intent.dart
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
/// Vom Treiber ausgelöstes Scan-Ereignis, bevor es serverseitig
|
||||||
|
/// angewendet wurde.
|
||||||
|
///
|
||||||
|
/// `clientScanId` ist ein vom Client generierter UUID-Schlüssel und dient
|
||||||
|
/// als **Idempotenz-Anker**: der Server speichert ihn beim ersten Apply
|
||||||
|
/// und antwortet auf jeden weiteren Request mit derselben Id mit
|
||||||
|
/// `duplicate` statt einer zweiten Anwendung. So bleibt Network-Retry
|
||||||
|
/// (z. B. nach Verbindungsabbruch beim ersten POST) bedeutungslos.
|
||||||
|
///
|
||||||
|
/// `clientScannedAt` ist die Wall-Clock-Zeit am Gerät zum Zeitpunkt des
|
||||||
|
/// Scans — der Server nutzt das nur als Audit-Spur, sortiert aber selbst
|
||||||
|
/// nach Server-Empfangszeit, sodass eine schiefe Uhr am Phone die
|
||||||
|
/// Reihenfolge nicht durcheinanderbringt.
|
||||||
|
class ScanIntent {
|
||||||
|
const ScanIntent({
|
||||||
|
required this.clientScanId,
|
||||||
|
required this.clientScannedAt,
|
||||||
|
required this.deliveryItemId,
|
||||||
|
required this.action,
|
||||||
|
this.actorCarId,
|
||||||
|
this.reason,
|
||||||
|
this.quantity,
|
||||||
|
this.manual = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String clientScanId;
|
||||||
|
final DateTime clientScannedAt;
|
||||||
|
final String deliveryItemId;
|
||||||
|
final ScanAction action;
|
||||||
|
|
||||||
|
/// `true`, wenn der Fahrer die Position manuell als geladen bestätigt hat
|
||||||
|
/// (Fallback ohne Barcode). Reine Audit-Information; Default `false`.
|
||||||
|
final bool manual;
|
||||||
|
|
||||||
|
/// Menge für `remove` / `unremove` (Mengen-Gutschrift): wie viele Stück
|
||||||
|
/// der Belegzeile gutgeschrieben bzw. wiederhergestellt werden. `null` =
|
||||||
|
/// ganze Restmenge. Bei `scan`/`unscan`/`hold`/`unhold` ignoriert.
|
||||||
|
final int? quantity;
|
||||||
|
|
||||||
|
/// Fahrzeug, mit dem gescannt wurde — Audit-Spur. Optional, aber die
|
||||||
|
/// App schickt ihn in der Loading-Phase immer mit, weil das Auto zu
|
||||||
|
/// dem Zeitpunkt definitiv gewählt ist.
|
||||||
|
final String? actorCarId;
|
||||||
|
|
||||||
|
/// Klartext-Begründung. Bei `unscan` / `hold` / `remove` vom Backend
|
||||||
|
/// erwartet, bei `scan` / `unhold` ignoriert.
|
||||||
|
final String? reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Auswirkung eines Scan-Ereignisses auf die Pipeline eines Items.
|
||||||
|
/// Spiegel des Backend-Enums `AuditAction`.
|
||||||
|
///
|
||||||
|
/// `unremove` ist die Umkehrung von `remove`: setzt ein `Removed`-Item
|
||||||
|
/// zurück auf `InProgress` (oder `Done`, falls die Soll-Menge schon
|
||||||
|
/// erreicht war). Der ursprüngliche `remove`-Audit-Eintrag bleibt
|
||||||
|
/// erhalten — `unremove` erzeugt einen eigenen Eintrag, sodass die
|
||||||
|
/// Historie der Korrektur vollständig nachvollziehbar bleibt.
|
||||||
|
enum ScanAction { scan, unscan, hold, unhold, remove, unremove }
|
||||||
|
|
||||||
|
/// Ergebnis eines Apply-Versuchs vom Server.
|
||||||
|
class ScanOutcome {
|
||||||
|
const ScanOutcome({
|
||||||
|
required this.clientScanId,
|
||||||
|
required this.status,
|
||||||
|
this.deliveryItemId,
|
||||||
|
this.reason,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String clientScanId;
|
||||||
|
final ScanOutcomeStatus status;
|
||||||
|
|
||||||
|
/// Bei `applied` und `duplicate` immer gesetzt, bei `rejected` häufig
|
||||||
|
/// `null` (z. B. wenn die Id beim Server gar nicht ankam).
|
||||||
|
final String? deliveryItemId;
|
||||||
|
|
||||||
|
/// Bei `rejected` die Server-Begründung — Standard-Text in der UI.
|
||||||
|
final String? reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ScanOutcomeStatus {
|
||||||
|
/// Server hat den Scan angewendet — `scannedQuantity` ist hochgezählt
|
||||||
|
/// oder Status hat sich geändert.
|
||||||
|
applied,
|
||||||
|
|
||||||
|
/// Server hat denselben `clientScanId` schon einmal verarbeitet —
|
||||||
|
/// kein Effekt, aber auch kein Fehler.
|
||||||
|
duplicate,
|
||||||
|
|
||||||
|
/// Server hat den Scan abgelehnt (z. B. Item gehört zu fremder
|
||||||
|
/// Lieferung, Soll-Menge schon voll, Item ist auf `removed`). UI muss
|
||||||
|
/// optimistische Mutation zurückrollen.
|
||||||
|
rejected,
|
||||||
|
}
|
||||||
48
lib/domain/entity/scan_progress.dart
Normal file
48
lib/domain/entity/scan_progress.dart
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
/// Status der Scan-Pipeline eines einzelnen `DeliveryItem`.
|
||||||
|
///
|
||||||
|
/// - `inProgress`: Soll-Menge noch nicht erreicht, Scanner darf weiterzählen.
|
||||||
|
/// - `done`: Soll-Menge erreicht; weitere Scans werden serverseitig abgewiesen.
|
||||||
|
/// - `held`: Pausiert (z. B. „Ware beschädigt, klärt der Fahrer mit dem Lager") —
|
||||||
|
/// `ScanProgress.heldReason` trägt die Begründung.
|
||||||
|
/// - `removed`: Item wurde nach dem Laden wieder abgebucht (Retoure, Falschladung).
|
||||||
|
enum ScanStatus { inProgress, done, held, removed }
|
||||||
|
|
||||||
|
/// Embedded Value-Object am `DeliveryItem`. Beschreibt, wie weit der Fahrer
|
||||||
|
/// mit dem Scannen dieses Items ist — *nicht*, wo das Item logistisch steht.
|
||||||
|
class ScanProgress {
|
||||||
|
const ScanProgress({
|
||||||
|
required this.status,
|
||||||
|
required this.scannedQuantity,
|
||||||
|
required this.lastUpdatedAt,
|
||||||
|
this.creditedQuantity = 0,
|
||||||
|
this.heldReason,
|
||||||
|
});
|
||||||
|
|
||||||
|
final ScanStatus status;
|
||||||
|
final int scannedQuantity;
|
||||||
|
|
||||||
|
/// Als Gutschrift entfernte Menge (0..=requiredQuantity). Eigene Dimension
|
||||||
|
/// neben [scannedQuantity]: „wie viele Stück dieser Zeile hat der Kunde
|
||||||
|
/// nicht angenommen". `status == removed` entspricht voller Gutschrift
|
||||||
|
/// (creditedQuantity == requiredQuantity).
|
||||||
|
final int creditedQuantity;
|
||||||
|
|
||||||
|
final DateTime lastUpdatedAt;
|
||||||
|
final String? heldReason;
|
||||||
|
|
||||||
|
ScanProgress copyWith({
|
||||||
|
ScanStatus? status,
|
||||||
|
int? scannedQuantity,
|
||||||
|
int? creditedQuantity,
|
||||||
|
DateTime? lastUpdatedAt,
|
||||||
|
String? heldReason,
|
||||||
|
}) {
|
||||||
|
return ScanProgress(
|
||||||
|
status: status ?? this.status,
|
||||||
|
scannedQuantity: scannedQuantity ?? this.scannedQuantity,
|
||||||
|
creditedQuantity: creditedQuantity ?? this.creditedQuantity,
|
||||||
|
lastUpdatedAt: lastUpdatedAt ?? this.lastUpdatedAt,
|
||||||
|
heldReason: heldReason ?? this.heldReason,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
29
lib/domain/entity/service.dart
Normal file
29
lib/domain/entity/service.dart
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
/// Eingabetyp eines Service. `boolean` → Checkbox, `numeric` → Zahlenfeld
|
||||||
|
/// mit optionalen Grenzen.
|
||||||
|
enum ServiceKind { boolean, numeric }
|
||||||
|
|
||||||
|
/// Service-Stammdatensatz (früher „Lieferoption") — admin-konfigurierbar.
|
||||||
|
/// In Phase 4 rendert die App aus den aktiven Services die Auswahl.
|
||||||
|
class Service {
|
||||||
|
const Service({
|
||||||
|
required this.id,
|
||||||
|
required this.key,
|
||||||
|
required this.name,
|
||||||
|
required this.kind,
|
||||||
|
required this.active,
|
||||||
|
required this.sortOrder,
|
||||||
|
this.minValue,
|
||||||
|
this.maxValue,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
final String key;
|
||||||
|
final String name;
|
||||||
|
final ServiceKind kind;
|
||||||
|
final bool active;
|
||||||
|
final int sortOrder;
|
||||||
|
|
||||||
|
/// Nur bei [ServiceKind.numeric] relevant.
|
||||||
|
final int? minValue;
|
||||||
|
final int? maxValue;
|
||||||
|
}
|
||||||
53
lib/domain/entity/tour.dart
Normal file
53
lib/domain/entity/tour.dart
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
/// Aggregat-Wurzel eines Tour-Tages.
|
||||||
|
///
|
||||||
|
/// Die `Tour` selbst ist minimal — sie hält nur Identität und Eckdaten;
|
||||||
|
/// die fachlich interessanten Daten (Lieferungen + Stammdaten-Lookups)
|
||||||
|
/// sitzen in `TourDetails`. Diese Trennung erlaubt es, Touren-Listen
|
||||||
|
/// (z. B. `/me/tours/today`) zu rendern, ohne das gesamte Aggregat
|
||||||
|
/// laden zu müssen.
|
||||||
|
class Tour {
|
||||||
|
const Tour({
|
||||||
|
required this.id,
|
||||||
|
required this.accountId,
|
||||||
|
required this.date,
|
||||||
|
required this.syncedAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
final int accountId;
|
||||||
|
final DateTime date;
|
||||||
|
|
||||||
|
/// Zeitpunkt des letzten ERP-Sync. Wird in der Header-Zeile als
|
||||||
|
/// „Stand: …"-Hinweis angezeigt — wenn das ungewöhnlich alt ist, sieht
|
||||||
|
/// der Fahrer das.
|
||||||
|
final DateTime syncedAt;
|
||||||
|
|
||||||
|
Tour copyWith({
|
||||||
|
String? id,
|
||||||
|
int? accountId,
|
||||||
|
DateTime? date,
|
||||||
|
DateTime? syncedAt,
|
||||||
|
}) {
|
||||||
|
return Tour(
|
||||||
|
id: id ?? this.id,
|
||||||
|
accountId: accountId ?? this.accountId,
|
||||||
|
date: date ?? this.date,
|
||||||
|
syncedAt: syncedAt ?? this.syncedAt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tagestour-Übersicht, wie sie `/me/tours/today` liefert. Schlankes Objekt
|
||||||
|
/// für die Initialphase (Tour-Auswahl), ohne das volle Aggregat zu
|
||||||
|
/// transportieren.
|
||||||
|
class TourSummary {
|
||||||
|
const TourSummary({
|
||||||
|
required this.tourId,
|
||||||
|
required this.tourDate,
|
||||||
|
required this.deliveryCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String tourId;
|
||||||
|
final DateTime tourDate;
|
||||||
|
final int deliveryCount;
|
||||||
|
}
|
||||||
428
lib/domain/entity/tour_details.dart
Normal file
428
lib/domain/entity/tour_details.dart
Normal file
@ -0,0 +1,428 @@
|
|||||||
|
import 'article.dart';
|
||||||
|
import 'contact_source.dart';
|
||||||
|
import 'customer.dart';
|
||||||
|
import 'delivery.dart';
|
||||||
|
import 'delivery_credit.dart';
|
||||||
|
import 'delivery_item.dart';
|
||||||
|
import 'delivery_note.dart';
|
||||||
|
import 'delivery_service_value.dart';
|
||||||
|
import 'service.dart';
|
||||||
|
import 'tour.dart';
|
||||||
|
import 'warehouse.dart';
|
||||||
|
|
||||||
|
/// Voll geladenes Tour-Aggregat. Enthält die Tour selbst, alle Lieferungen
|
||||||
|
/// inkl. Items sowie *alle* Stammdaten, die von diesem Schnitt referenziert
|
||||||
|
/// werden. Die Stammdaten kommen als Lookup-Maps statt als List, damit das
|
||||||
|
/// UI ohne O(n)-Suchen auskommt.
|
||||||
|
///
|
||||||
|
/// Die Notizen sind im Backend in einer flachen Liste — wir indizieren sie
|
||||||
|
/// hier einmal per `deliveryId`, weil das UI sie immer „pro Lieferung"
|
||||||
|
/// braucht.
|
||||||
|
class TourDetails {
|
||||||
|
TourDetails({
|
||||||
|
required this.tour,
|
||||||
|
required this.deliveries,
|
||||||
|
required this.customers,
|
||||||
|
required this.contacts,
|
||||||
|
required this.articles,
|
||||||
|
required this.warehouses,
|
||||||
|
required this.notesByDeliveryId,
|
||||||
|
required this.creditsByDeliveryId,
|
||||||
|
required this.services,
|
||||||
|
required this.serviceValuesByDeliveryId,
|
||||||
|
required this.contactSourcesByDeliveryId,
|
||||||
|
required this.contactChannelsBySourceId,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Tour tour;
|
||||||
|
|
||||||
|
/// Alle Lieferungen dieser Tour. Reihenfolge: unsortiert; UI ruft
|
||||||
|
/// `deliveriesSorted` auf, wenn Sortier-Reihenfolge benötigt wird.
|
||||||
|
final List<Delivery> deliveries;
|
||||||
|
|
||||||
|
// ─── Stammdaten-Lookups (Id → Entity) ─────────────────────────────────
|
||||||
|
|
||||||
|
final Map<String, Customer> customers;
|
||||||
|
final Map<String, CustomerContact> contacts;
|
||||||
|
final Map<String, Article> articles;
|
||||||
|
final Map<String, Warehouse> warehouses;
|
||||||
|
|
||||||
|
/// Pro Lieferung: alle Notizen, aufsteigend nach `createdAt`. Wenn eine
|
||||||
|
/// Lieferung keine Notizen hat, liefert der Lookup `null` zurück — das
|
||||||
|
/// UI muss das berücksichtigen.
|
||||||
|
final Map<String, List<DeliveryNote>> notesByDeliveryId;
|
||||||
|
|
||||||
|
/// Pro Lieferung die aktuelle Betrags-Gutschrift (höchstens eine). Fehlt
|
||||||
|
/// der Eintrag, gibt es aktuell keine Gutschrift.
|
||||||
|
final Map<String, DeliveryCredit> creditsByDeliveryId;
|
||||||
|
|
||||||
|
/// Aktive Service-Definitionen (Stammdaten), nach `sortOrder`. Daraus
|
||||||
|
/// rendert Phase 4 die Auswahl.
|
||||||
|
final List<Service> services;
|
||||||
|
|
||||||
|
/// Pro Lieferung die gesetzten Service-Werte, indiziert per `serviceId`.
|
||||||
|
final Map<String, Map<String, DeliveryServiceValue>> serviceValuesByDeliveryId;
|
||||||
|
|
||||||
|
/// Pro Lieferung die Adress-Quellen aus dem ERP (Belegadresse / Liefer-
|
||||||
|
/// adresse / Rechnungsadresse / Ansprechpartner / Kundenstamm). Wird vom
|
||||||
|
/// Sync gefüllt; leere Quellen kommen nicht durch — wer hier 0 Einträge
|
||||||
|
/// sieht, hat im ERP keinen einzigen Kontakt am Beleg hängen.
|
||||||
|
final Map<String, List<ContactSource>> contactSourcesByDeliveryId;
|
||||||
|
|
||||||
|
/// Pro Quelle alle ihre Kommunikationskanäle. Reihenfolge folgt der
|
||||||
|
/// ERP-Position (Telefon 1 → Position 1, Telefon 2 → Position 2, …),
|
||||||
|
/// das UI kann die Liste direkt rendern.
|
||||||
|
final Map<String, List<ContactChannel>> contactChannelsBySourceId;
|
||||||
|
|
||||||
|
// ─── Convenience für UI ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Lieferungen sortiert nach `sortOrder` aufsteigend. Falls zwei
|
||||||
|
/// Lieferungen identische Werte tragen (sollte nicht vorkommen, dient
|
||||||
|
/// nur als Defensive), fällt der Vergleich auf die Belegnummer zurück.
|
||||||
|
List<Delivery> get deliveriesSorted {
|
||||||
|
final copy = List<Delivery>.of(deliveries);
|
||||||
|
copy.sort((a, b) {
|
||||||
|
final byOrder = a.sortOrder.compareTo(b.sortOrder);
|
||||||
|
if (byOrder != 0) return byOrder;
|
||||||
|
return a.erpBelegnummer.compareTo(b.erpBelegnummer);
|
||||||
|
});
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
Customer? customerOf(Delivery delivery) => customers[delivery.customerId];
|
||||||
|
|
||||||
|
Iterable<CustomerContact> contactsOf(Delivery delivery) sync* {
|
||||||
|
for (final id in delivery.contactPersonIds) {
|
||||||
|
final c = contacts[id];
|
||||||
|
if (c != null) yield c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Alle Adress-Quellen einer Lieferung — in der vom Backend gelieferten
|
||||||
|
/// Reihenfolge (nach [ContactRole], anschließend nach Quell-Id für
|
||||||
|
/// stabile UI). Leere Liste, wenn diese Lieferung im ERP keinen Kontakt
|
||||||
|
/// hängen hat.
|
||||||
|
List<ContactSource> contactSourcesOf(Delivery delivery) =>
|
||||||
|
contactSourcesByDeliveryId[delivery.id] ?? const <ContactSource>[];
|
||||||
|
|
||||||
|
/// Alle Kanäle einer einzelnen Quelle. Leere Liste, wenn die Quelle nur
|
||||||
|
/// einen Namensblock trägt (z. B. ein Ansprechpartner ohne Telefonnummer).
|
||||||
|
List<ContactChannel> channelsOf(ContactSource source) =>
|
||||||
|
contactChannelsBySourceId[source.id] ?? const <ContactChannel>[];
|
||||||
|
|
||||||
|
/// Wie [contactSourcesOf], aber Quellen mit identischem Namensblock UND
|
||||||
|
/// identischer Channel-Liste sind zu einem [MergedContactSource] mit
|
||||||
|
/// Multi-Rollen-Header zusammengeführt. Das eliminiert die typische
|
||||||
|
/// Doppelung „Belegadresse + Kundenstamm" bei Belegen, deren
|
||||||
|
/// `Belegkopf.AdressId` ohnehin auf die Kunden-Stammadresse zeigt.
|
||||||
|
///
|
||||||
|
/// Identity-Fingerprint: alle Namensfelder (Anrede / Titel / Name1..3 /
|
||||||
|
/// Abteilung / Funktion) plus die nach (kind, position) sortierten
|
||||||
|
/// (kind, value)-Paare. Zwei Quellen mit identischem Namen, aber
|
||||||
|
/// abweichenden Channels werden NICHT gemerged — das wäre fachlich
|
||||||
|
/// falsch (zwei verschiedene Kontaktdatensätze derselben Person).
|
||||||
|
List<MergedContactSource> mergedContactSourcesOf(Delivery delivery) {
|
||||||
|
final sources = contactSourcesOf(delivery);
|
||||||
|
if (sources.isEmpty) return const <MergedContactSource>[];
|
||||||
|
|
||||||
|
// Reihenfolge der Erstauftritte merken — die Backend-Sortierung
|
||||||
|
// (Quellen nach Rolle aufsteigend) bestimmt damit auch die Reihenfolge
|
||||||
|
// der Merge-Gruppen in der UI.
|
||||||
|
final order = <String>[];
|
||||||
|
final byKey = <String, List<ContactSource>>{};
|
||||||
|
for (final s in sources) {
|
||||||
|
final key = _identityKey(s, channelsOf(s));
|
||||||
|
if (!byKey.containsKey(key)) {
|
||||||
|
order.add(key);
|
||||||
|
byKey[key] = <ContactSource>[];
|
||||||
|
}
|
||||||
|
byKey[key]!.add(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
for (final key in order) _buildMerged(byKey[key]!),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fingerprint einer Quelle: Namensblock + alle (kind, position, value)-
|
||||||
|
/// Tripel. Vorab nach (kind-Index, position) sortiert, damit semantisch
|
||||||
|
/// gleiche Quellen unabhängig von der Speicher-Reihenfolge denselben
|
||||||
|
/// Schlüssel bekommen.
|
||||||
|
String _identityKey(ContactSource s, List<ContactChannel> channels) {
|
||||||
|
final namePart = [
|
||||||
|
s.anrede ?? '',
|
||||||
|
s.titel ?? '',
|
||||||
|
s.name1 ?? '',
|
||||||
|
s.name2 ?? '',
|
||||||
|
s.name3 ?? '',
|
||||||
|
s.abteilung ?? '',
|
||||||
|
s.funktion ?? '',
|
||||||
|
].join('|');
|
||||||
|
final sortedChannels = List<ContactChannel>.of(channels)
|
||||||
|
..sort((a, b) {
|
||||||
|
final byKind = a.kind.index.compareTo(b.kind.index);
|
||||||
|
if (byKind != 0) return byKind;
|
||||||
|
return a.position.compareTo(b.position);
|
||||||
|
});
|
||||||
|
final channelPart = sortedChannels
|
||||||
|
.map((c) => '${c.kind.name}:${c.position}:${c.value}')
|
||||||
|
.join('|');
|
||||||
|
return '$namePart||$channelPart';
|
||||||
|
}
|
||||||
|
|
||||||
|
MergedContactSource _buildMerged(List<ContactSource> group) {
|
||||||
|
// Namensblock + Channels von der ersten Quelle übernehmen — alle Quellen
|
||||||
|
// in der Gruppe sind per Identity-Key garantiert deckungsgleich.
|
||||||
|
final first = group.first;
|
||||||
|
final roles = group.map((s) => s.role).toList()
|
||||||
|
..sort((a, b) => a.index.compareTo(b.index));
|
||||||
|
return MergedContactSource(
|
||||||
|
roles: roles,
|
||||||
|
anrede: first.anrede,
|
||||||
|
titel: first.titel,
|
||||||
|
name1: first.name1,
|
||||||
|
name2: first.name2,
|
||||||
|
name3: first.name3,
|
||||||
|
abteilung: first.abteilung,
|
||||||
|
funktion: first.funktion,
|
||||||
|
channels: channelsOf(first),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Article? articleOf(String articleId) => articles[articleId];
|
||||||
|
|
||||||
|
Warehouse? warehouseOf(String warehouseId) => warehouses[warehouseId];
|
||||||
|
|
||||||
|
List<DeliveryNote> notesOf(String deliveryId) =>
|
||||||
|
notesByDeliveryId[deliveryId] ?? const <DeliveryNote>[];
|
||||||
|
|
||||||
|
/// Aktuelle Betrags-Gutschrift dieser Lieferung, oder `null`.
|
||||||
|
DeliveryCredit? creditOf(String deliveryId) =>
|
||||||
|
creditsByDeliveryId[deliveryId];
|
||||||
|
|
||||||
|
/// Gesetzter Service-Wert dieser Lieferung für einen Service, oder `null`.
|
||||||
|
DeliveryServiceValue? serviceValueOf(String deliveryId, String serviceId) =>
|
||||||
|
serviceValuesByDeliveryId[deliveryId]?[serviceId];
|
||||||
|
|
||||||
|
/// Alle Attachment-IDs, die von Foto-Notizen dieser Tour referenziert
|
||||||
|
/// werden — die Menge der „noch gültigen" Bilder. Dient dem Cache-Pruning
|
||||||
|
/// (`AttachmentCache.retainOnly`): gecachte Vorschauen zu IDs, die hier
|
||||||
|
/// nicht (mehr) vorkommen, gehören zu gelöschten Notizen und dürfen weg.
|
||||||
|
Set<String> get referencedAttachmentIds {
|
||||||
|
final ids = <String>{};
|
||||||
|
for (final notes in notesByDeliveryId.values) {
|
||||||
|
for (final n in notes) {
|
||||||
|
final attachment = n.imageAttachment;
|
||||||
|
if (attachment != null) ids.add(attachment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isArticleScannable(String articleId) =>
|
||||||
|
articles[articleId]?.scannable ?? false;
|
||||||
|
|
||||||
|
/// Nicht-scanbare Positionen einer Lieferung (Dienstleistung / Pauschale /
|
||||||
|
/// Fracht — `article.scannable == false`). Entfernte Zeilen sind hier
|
||||||
|
/// ausgefiltert, weil eine entfernte Dienstleistung den Belade-/Anfahrt-
|
||||||
|
/// Hinweis nicht mehr rechtfertigt.
|
||||||
|
///
|
||||||
|
/// Diese Positionen werden in der Beladen-Phase **nicht gescannt**, sind
|
||||||
|
/// aber fachlich der Grund, warum eine Lieferung ohne scanbare Ware (reine
|
||||||
|
/// Dienstleistung) trotzdem angefahren werden muss.
|
||||||
|
Iterable<DeliveryItem> nonScannableItems(Delivery delivery) sync* {
|
||||||
|
for (final it in delivery.items) {
|
||||||
|
if (it.isRemoved) continue;
|
||||||
|
if (isArticleScannable(it.articleId)) continue;
|
||||||
|
yield it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `true`, wenn die Lieferung mindestens eine nicht-scanbare Position
|
||||||
|
/// (Dienstleistung / Pauschale) trägt — Basis für den Dienstleistungs-
|
||||||
|
/// Hinweis in der Beladen-Ansicht.
|
||||||
|
bool hasServiceItems(Delivery delivery) =>
|
||||||
|
nonScannableItems(delivery).isNotEmpty;
|
||||||
|
|
||||||
|
// ─── Lager-Aufteilung in der Beladen-Phase ───────────────────────────
|
||||||
|
//
|
||||||
|
// Der Fahrer startet standardmäßig im Standardlager (`Warehouse.isStandard`).
|
||||||
|
// Filialen werden separat angefahren — sie blockieren NICHT den Übergang
|
||||||
|
// in die Auslieferungs-Phase. Eine Lieferung gilt deshalb als „fertig
|
||||||
|
// beladen", sobald **alle scanbaren Standardlager-Items** durch sind;
|
||||||
|
// Filial-Items werden in der UI sichtbar gekennzeichnet, damit der
|
||||||
|
// Fahrer weiß, dass er noch eine zweite Station ansteuern muss.
|
||||||
|
|
||||||
|
bool _isStandard(String warehouseId) =>
|
||||||
|
warehouseOf(warehouseId)?.isStandard ?? false;
|
||||||
|
|
||||||
|
bool _isExternal(String warehouseId) {
|
||||||
|
final w = warehouseOf(warehouseId);
|
||||||
|
return w != null && !w.isStandard;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterator über die scanbaren Items einer Lieferung. `includeRemoved`
|
||||||
|
/// kontrolliert, ob entfernte Positionen Teil der Iteration sind:
|
||||||
|
///
|
||||||
|
/// * `false` (default) — für Status-Berechnungen (`standardWarehouseLoadingDone`,
|
||||||
|
/// `hasExternalWarehouseItems`, …). Entfernte Positionen blockieren
|
||||||
|
/// sonst „Fertig"-Marker oder triggern fälschlich Filial-Hinweise.
|
||||||
|
/// * `true` — für die UI-Anzeige (`itemsGroupedByWarehouse`), damit der
|
||||||
|
/// Fahrer entfernte Items als durchgestrichene Zeilen weiterhin sieht
|
||||||
|
/// und sie ggf. wiederherstellen kann.
|
||||||
|
Iterable<DeliveryItem> _activeScannableItems(
|
||||||
|
Delivery delivery, {
|
||||||
|
bool includeRemoved = false,
|
||||||
|
}) sync* {
|
||||||
|
for (final it in delivery.items) {
|
||||||
|
if (!includeRemoved && it.isRemoved) continue;
|
||||||
|
if (!isArticleScannable(it.articleId)) continue;
|
||||||
|
yield it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Standardlager-Beladung dieser Lieferung ist erledigt: jedes scanbare,
|
||||||
|
/// nicht-entfernte Item aus dem Standardlager ist `done`. Lieferungen
|
||||||
|
/// ohne Standardlager-Items (= alles Filiale) sind trivial fertig —
|
||||||
|
/// im Standardlager ist dann nichts zu tun.
|
||||||
|
bool standardWarehouseLoadingDone(Delivery delivery) {
|
||||||
|
return _activeScannableItems(delivery)
|
||||||
|
.where((it) => _isStandard(it.warehouseId))
|
||||||
|
.every((it) => it.isDone);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lieferung enthält mindestens ein noch relevantes Filial-Item.
|
||||||
|
/// „Relevant" = scanbar + nicht entfernt; ob das Item schon gescannt ist
|
||||||
|
/// oder nicht spielt für diese Markierung keine Rolle (entscheidend ist
|
||||||
|
/// nur, dass der Fahrer ein zusätzliches Lager anfahren muss).
|
||||||
|
bool hasExternalWarehouseItems(Delivery delivery) {
|
||||||
|
return _activeScannableItems(delivery).any(
|
||||||
|
(it) => _isExternal(it.warehouseId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filial-Items, die noch nicht beladen wurden — gedacht für die
|
||||||
|
/// Auslieferungs-Übersicht: dort soll der Fahrer auf einen Blick sehen,
|
||||||
|
/// dass er *vor* der Anfahrt zum Kunden noch ein zweites Lager ansteuern
|
||||||
|
/// muss, und welche Artikel ihn dort erwarten.
|
||||||
|
///
|
||||||
|
/// Item-Filter: scanbar + nicht entfernt + Filiale + `!isDone`. Items
|
||||||
|
/// mit Status `held` zählen ebenfalls als „nicht geholt", weil das
|
||||||
|
/// Warenholen noch aussteht.
|
||||||
|
///
|
||||||
|
/// Sortierung: Lager alphabetisch, innerhalb des Lagers nach
|
||||||
|
/// `belegzeilenNr` aufsteigend — stabile Reihenfolge zwischen Builds.
|
||||||
|
List<({Warehouse warehouse, List<DeliveryItem> items})>
|
||||||
|
pendingExternalWarehouseGroups(Delivery delivery) {
|
||||||
|
final byWarehouseId = <String, List<DeliveryItem>>{};
|
||||||
|
for (final it in _activeScannableItems(delivery)) {
|
||||||
|
if (!_isExternal(it.warehouseId)) continue;
|
||||||
|
if (it.isDone) continue;
|
||||||
|
byWarehouseId.putIfAbsent(it.warehouseId, () => []).add(it);
|
||||||
|
}
|
||||||
|
final groups = <({Warehouse warehouse, List<DeliveryItem> items})>[];
|
||||||
|
byWarehouseId.forEach((warehouseId, items) {
|
||||||
|
final w = warehouseOf(warehouseId);
|
||||||
|
if (w == null) return;
|
||||||
|
items.sort((a, b) => a.belegzeilenNr.compareTo(b.belegzeilenNr));
|
||||||
|
groups.add((warehouse: w, items: items));
|
||||||
|
});
|
||||||
|
groups.sort((a, b) => a.warehouse.name.compareTo(b.warehouse.name));
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `true`, wenn die Lieferung noch mindestens einen offenen
|
||||||
|
/// Filial-Artikel hat (= Fahrer muss zuerst in die Filiale).
|
||||||
|
bool hasPendingExternalWarehouseItems(Delivery delivery) {
|
||||||
|
for (final it in _activeScannableItems(delivery)) {
|
||||||
|
if (!_isExternal(it.warehouseId)) continue;
|
||||||
|
if (!it.isDone) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Eindeutige Filial-Namen dieser Lieferung — für Badges /
|
||||||
|
/// Sektions-Header in der UI. Sortiert nach Lager-Name, damit die
|
||||||
|
/// Reihenfolge stabil bleibt zwischen Builds.
|
||||||
|
List<String> externalWarehouseLabels(Delivery delivery) {
|
||||||
|
final names = <String>{};
|
||||||
|
for (final it in _activeScannableItems(delivery)) {
|
||||||
|
if (!_isExternal(it.warehouseId)) continue;
|
||||||
|
final w = warehouseOf(it.warehouseId);
|
||||||
|
if (w != null) names.add(w.name);
|
||||||
|
}
|
||||||
|
final list = names.toList()..sort();
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gruppiert die scanbaren Items einer Lieferung nach Warehouse-Id —
|
||||||
|
/// Standardlager-Eintrag (sofern vorhanden) immer zuerst, danach
|
||||||
|
/// Filiale alphabetisch nach Lager-Name. Items innerhalb einer
|
||||||
|
/// Gruppe sind nach `belegzeilenNr` aufsteigend sortiert.
|
||||||
|
List<({Warehouse warehouse, List<DeliveryItem> items})>
|
||||||
|
itemsGroupedByWarehouse(Delivery delivery) {
|
||||||
|
final byWarehouseId = <String, List<DeliveryItem>>{};
|
||||||
|
// Entfernte Items bleiben in der UI sichtbar (durchgestrichen) und
|
||||||
|
// können dort über das Aktions-Menü wiederhergestellt werden — der
|
||||||
|
// Status-Pfad (`standardWarehouseLoadingDone` etc.) ignoriert sie
|
||||||
|
// trotzdem, weil die jeweiligen Helper ohne `includeRemoved` laufen.
|
||||||
|
for (final it in _activeScannableItems(delivery, includeRemoved: true)) {
|
||||||
|
byWarehouseId.putIfAbsent(it.warehouseId, () => []).add(it);
|
||||||
|
}
|
||||||
|
for (final list in byWarehouseId.values) {
|
||||||
|
list.sort((a, b) => a.belegzeilenNr.compareTo(b.belegzeilenNr));
|
||||||
|
}
|
||||||
|
|
||||||
|
// In zwei Buckets aufteilen, damit der Aufrufer Standard zuerst sieht.
|
||||||
|
final standard = <({Warehouse warehouse, List<DeliveryItem> items})>[];
|
||||||
|
final external = <({Warehouse warehouse, List<DeliveryItem> items})>[];
|
||||||
|
byWarehouseId.forEach((warehouseId, items) {
|
||||||
|
final w = warehouseOf(warehouseId);
|
||||||
|
if (w == null) return; // Defensive: defekte Stammdaten ignorieren
|
||||||
|
final group = (warehouse: w, items: items);
|
||||||
|
if (w.isStandard) {
|
||||||
|
standard.add(group);
|
||||||
|
} else {
|
||||||
|
external.add(group);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
external.sort((a, b) => a.warehouse.name.compareTo(b.warehouse.name));
|
||||||
|
return [...standard, ...external];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Neues Aggregat mit ausgetauschten/erweiterten Listen — gedacht für
|
||||||
|
/// Bloc-Reducer (Reorder, Assign-Car etc.), die das ganze Aggregat
|
||||||
|
/// behalten und nur ein paar Lieferungen austauschen wollen.
|
||||||
|
TourDetails copyWith({
|
||||||
|
Tour? tour,
|
||||||
|
List<Delivery>? deliveries,
|
||||||
|
Map<String, List<DeliveryNote>>? notesByDeliveryId,
|
||||||
|
Map<String, DeliveryCredit>? creditsByDeliveryId,
|
||||||
|
Map<String, Map<String, DeliveryServiceValue>>? serviceValuesByDeliveryId,
|
||||||
|
}) {
|
||||||
|
return TourDetails(
|
||||||
|
tour: tour ?? this.tour,
|
||||||
|
deliveries: deliveries ?? this.deliveries,
|
||||||
|
customers: customers,
|
||||||
|
contacts: contacts,
|
||||||
|
articles: articles,
|
||||||
|
warehouses: warehouses,
|
||||||
|
notesByDeliveryId: notesByDeliveryId ?? this.notesByDeliveryId,
|
||||||
|
creditsByDeliveryId: creditsByDeliveryId ?? this.creditsByDeliveryId,
|
||||||
|
services: services,
|
||||||
|
serviceValuesByDeliveryId:
|
||||||
|
serviceValuesByDeliveryId ?? this.serviceValuesByDeliveryId,
|
||||||
|
contactSourcesByDeliveryId: contactSourcesByDeliveryId,
|
||||||
|
contactChannelsBySourceId: contactChannelsBySourceId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ersetzt eine einzelne Lieferung im Aggregat. Reihenfolge bleibt erhalten.
|
||||||
|
TourDetails replaceDelivery(Delivery updated) {
|
||||||
|
final next = List<Delivery>.of(deliveries);
|
||||||
|
final idx = next.indexWhere((d) => d.id == updated.id);
|
||||||
|
if (idx == -1) return this;
|
||||||
|
next[idx] = updated;
|
||||||
|
return copyWith(deliveries: next);
|
||||||
|
}
|
||||||
|
}
|
||||||
32
lib/domain/entity/warehouse.dart
Normal file
32
lib/domain/entity/warehouse.dart
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
/// Lager-Standort, von dem ein `DeliveryItem` geladen wird.
|
||||||
|
///
|
||||||
|
/// `isStandard` markiert das Hauptlager — die App nutzt das, um in der
|
||||||
|
/// Loading-Übersicht ein „Sonderlager"-Banner zu zeigen, sobald Items aus
|
||||||
|
/// einem nicht-Standard-Lager kommen.
|
||||||
|
class Warehouse {
|
||||||
|
const Warehouse({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.code,
|
||||||
|
required this.isStandard,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
final String name;
|
||||||
|
final String code;
|
||||||
|
final bool isStandard;
|
||||||
|
|
||||||
|
Warehouse copyWith({
|
||||||
|
String? id,
|
||||||
|
String? name,
|
||||||
|
String? code,
|
||||||
|
bool? isStandard,
|
||||||
|
}) {
|
||||||
|
return Warehouse(
|
||||||
|
id: id ?? this.id,
|
||||||
|
name: name ?? this.name,
|
||||||
|
code: code ?? this.code,
|
||||||
|
isStandard: isStandard ?? this.isStandard,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
36
lib/domain/repository/payment_methods_repository.dart
Normal file
36
lib/domain/repository/payment_methods_repository.dart
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import 'package:hl_lieferservice/domain/entity/payment_method.dart';
|
||||||
|
|
||||||
|
/// Port für Zahlungsmethoden — globale Stammdaten.
|
||||||
|
///
|
||||||
|
/// Im Gegensatz zu `CarsRepository` keine Account-Filter: die Methoden
|
||||||
|
/// sind firmenweit, alle Fahrer sehen dieselbe Liste.
|
||||||
|
///
|
||||||
|
/// Lösch-Verhalten: `delete` wirft eine `PaymentMethodsRepositoryException`
|
||||||
|
/// mit konkretem `409`-Fall, wenn die Methode noch von Lieferungen
|
||||||
|
/// referenziert wird (Backend hat dafür den FK-RESTRICT). Für „weiches
|
||||||
|
/// Entfernen" gibt es `update(active: false)`.
|
||||||
|
abstract interface class PaymentMethodsRepository {
|
||||||
|
Future<List<PaymentMethod>> list({bool includeInactive = false});
|
||||||
|
|
||||||
|
Future<PaymentMethod> create({
|
||||||
|
required String code,
|
||||||
|
required String name,
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<PaymentMethod> update({
|
||||||
|
required String id,
|
||||||
|
String? name,
|
||||||
|
bool? active,
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<void> delete(String id);
|
||||||
|
}
|
||||||
|
|
||||||
|
class PaymentMethodsRepositoryException implements Exception {
|
||||||
|
const PaymentMethodsRepositoryException(this.message, [this.cause]);
|
||||||
|
final String message;
|
||||||
|
final Object? cause;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'PaymentMethodsRepositoryException: $message';
|
||||||
|
}
|
||||||
210
lib/domain/repository/tour_repository.dart
Normal file
210
lib/domain/repository/tour_repository.dart
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
import 'package:hl_lieferservice/domain/entity/delivery.dart';
|
||||||
|
import 'package:hl_lieferservice/domain/entity/delivery_credit.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';
|
||||||
|
import 'package:hl_lieferservice/domain/entity/tour.dart';
|
||||||
|
import 'package:hl_lieferservice/domain/entity/tour_details.dart';
|
||||||
|
|
||||||
|
/// Port für das Tour-Aggregat.
|
||||||
|
///
|
||||||
|
/// Der Port deckt in dieser Migrations-Phase nur die Read-Seite + die
|
||||||
|
/// beiden Operationen, die zum Loading-Flow zwingend gebraucht werden:
|
||||||
|
/// Sortierung und Fahrzeug-Zuweisung. Hold/Resume/Cancel/Complete und
|
||||||
|
/// Notizen werden in C+D-4 nachgezogen, damit das hier nicht überladen
|
||||||
|
/// wird und der Bloc fokussiert bleibt.
|
||||||
|
///
|
||||||
|
/// Account-Filter sitzt serverseitig im JWT — der Client schickt nie eine
|
||||||
|
/// `personalnummer`/`accountId` mit.
|
||||||
|
abstract interface class TourRepository {
|
||||||
|
/// Die heutige Tour-Übersicht des angemeldeten Fahrers oder `null`,
|
||||||
|
/// wenn keine Tour für heute angelegt ist (ERP-Sync noch nicht
|
||||||
|
/// gelaufen, Treiber-Urlaub etc.).
|
||||||
|
///
|
||||||
|
/// Liefert nur die schlanke `TourSummary`-Repräsentation;
|
||||||
|
/// [getTourDetails] zieht dann den vollen Aggregat-Snapshot.
|
||||||
|
Future<TourSummary?> getMyTourSummaryOfToday();
|
||||||
|
|
||||||
|
/// Lädt das volle Tour-Aggregat (Tour + Lieferungen + Items +
|
||||||
|
/// Stammdaten + Notizen) für die gegebene Tour-Id.
|
||||||
|
Future<TourDetails> getTourDetails(String tourId);
|
||||||
|
|
||||||
|
/// Convenience: kombiniert [getMyTourSummaryOfToday] + [getTourDetails]
|
||||||
|
/// und gibt `null` zurück, wenn keine Tour existiert. Verwendet die App
|
||||||
|
/// beim Initial-Load.
|
||||||
|
Future<TourDetails?> getMyTourDetailsOfToday();
|
||||||
|
|
||||||
|
/// Schreibt die Sortier-Reihenfolge der Lieferungen einer Tour neu.
|
||||||
|
///
|
||||||
|
/// `orderedDeliveryIds` muss **alle** Lieferungen der Tour enthalten,
|
||||||
|
/// in der gewünschten Reihenfolge — das Backend lehnt unvollständige
|
||||||
|
/// Listen mit `400 validation` ab.
|
||||||
|
///
|
||||||
|
/// Rückgabe: deliveryId → neuer sortOrder (für den Bloc-Reducer).
|
||||||
|
Future<Map<String, int>> setDeliveryOrder({
|
||||||
|
required String tourId,
|
||||||
|
required List<String> orderedDeliveryIds,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Weist einer Lieferung ein Fahrzeug zu. `carId == null` löst die
|
||||||
|
/// bestehende Zuweisung. Der Server gibt die aktualisierte Delivery
|
||||||
|
/// zurück; weil dieser Endpoint nur Stamm-Felder mutiert, ist es Aufgabe
|
||||||
|
/// des Aufrufers, die `items` aus dem lokalen Aggregat zu erhalten.
|
||||||
|
///
|
||||||
|
/// Rückgabe: die Stamm-Delivery **ohne** Items — Aufrufer nutzt
|
||||||
|
/// `copyWith(items: ...)` zum Mergen mit dem lokalen State.
|
||||||
|
Future<Delivery> assignCarToDelivery({
|
||||||
|
required String deliveryId,
|
||||||
|
required String? carId,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Bricht eine Lieferung ab — endgültig (`canceled`). `reason` ist
|
||||||
|
/// vom Backend Pflicht; leere Begründungen werden mit 400 abgelehnt.
|
||||||
|
/// Rückgabe: Server-Snapshot der Delivery **ohne** Items.
|
||||||
|
Future<Delivery> cancelDelivery({
|
||||||
|
required String deliveryId,
|
||||||
|
required String reason,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Pausiert eine Lieferung (`held`). Reversibel über [resumeDelivery].
|
||||||
|
/// `reason` ist Pflicht.
|
||||||
|
Future<Delivery> holdDelivery({
|
||||||
|
required String deliveryId,
|
||||||
|
required String reason,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Setzt eine pausierte Lieferung auf `active` zurück. Kein Reason
|
||||||
|
/// erforderlich.
|
||||||
|
Future<Delivery> resumeDelivery({required String deliveryId});
|
||||||
|
|
||||||
|
/// Schließt eine Lieferung ab (`completed`). Lädt beide Unterschriften
|
||||||
|
/// (Kunde + Fahrer, PNG) per multipart hoch und dokumentiert die
|
||||||
|
/// Bestätigungen des Kunden. Atomar serverseitig — das Backend prüft
|
||||||
|
/// vorher: Lieferung aktiv, alle scanbaren Positionen fertig, Notizen
|
||||||
|
/// bestätigt (falls vorhanden). [paymentMethodId] persistiert die ggf. im
|
||||||
|
/// Summary geänderte Zahlungsmethode (muss existieren + aktiv sein); `null`
|
||||||
|
/// lässt die am Beleg hinterlegte Methode unangetastet. Rückgabe:
|
||||||
|
/// Server-Snapshot der Delivery **ohne** Items (Aufrufer merged Items aus
|
||||||
|
/// dem lokalen Aggregat).
|
||||||
|
Future<Delivery> completeDelivery({
|
||||||
|
required String deliveryId,
|
||||||
|
required List<int> customerSignaturePng,
|
||||||
|
required List<int> driverSignaturePng,
|
||||||
|
required bool receiptConfirmed,
|
||||||
|
required bool notesAcknowledged,
|
||||||
|
required List<String> acknowledgedNoteIds,
|
||||||
|
String? paymentMethodId,
|
||||||
|
String? actorCarId,
|
||||||
|
bool paymentCollected = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Legt eine neue Notiz an einer Lieferung an.
|
||||||
|
///
|
||||||
|
/// Mindestens eines von [text] und [imageAttachment] muss inhaltlich
|
||||||
|
/// gefüllt sein — das Backend erzwingt das. Aktuell unterstützt die App
|
||||||
|
/// nur den Text-Pfad; das `imageAttachment`-Feld bleibt der zukünftigen
|
||||||
|
/// Foto-Upload-Phase vorbehalten.
|
||||||
|
///
|
||||||
|
/// Rückgabe: die neu angelegte Notiz (mit Server-gesetzter `id` und
|
||||||
|
/// `createdAt`) — der Aufrufer hängt sie an das lokale Tour-Aggregat.
|
||||||
|
Future<DeliveryNote> addDeliveryNote({
|
||||||
|
required String deliveryId,
|
||||||
|
String? text,
|
||||||
|
String? imageAttachment,
|
||||||
|
String? creditDeliveryItemId,
|
||||||
|
bool isAmountCreditNote,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Ändert Text/Bild einer bestehenden Notiz. Mindestens eines von [text]
|
||||||
|
/// und [imageAttachment] muss inhaltlich gefüllt sein. Rückgabe: die
|
||||||
|
/// aktualisierte Notiz (Autor/`createdAt` bleiben).
|
||||||
|
Future<DeliveryNote> updateDeliveryNote({
|
||||||
|
required String deliveryId,
|
||||||
|
required String noteId,
|
||||||
|
String? text,
|
||||||
|
String? imageAttachment,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Löscht eine Notiz. Innerhalb des (geteilten) Accounts darf jeder Fahrer
|
||||||
|
/// löschen — keine Autor-Prüfung serverseitig.
|
||||||
|
Future<void> deleteDeliveryNote({
|
||||||
|
required String deliveryId,
|
||||||
|
required String noteId,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Lädt ein Bild zu einer Lieferung hoch (multipart, Feld `file`). Das
|
||||||
|
/// Backend reicht es an DOCUframe weiter und legt eine Bild-Notiz mit der
|
||||||
|
/// zurückgelieferten Referenz (`~ObjectID`) als `imageAttachment` an.
|
||||||
|
/// Rückgabe: die neue Notiz.
|
||||||
|
Future<DeliveryNote> uploadDeliveryNoteImage({
|
||||||
|
required String deliveryId,
|
||||||
|
required String filename,
|
||||||
|
required String mime,
|
||||||
|
required List<int> bytes,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Setzt/ändert die Betrags-Gutschrift einer Lieferung. Append-only +
|
||||||
|
/// idempotent über [clientEventId]. Rückgabe: aktueller Stand (`null`, wenn
|
||||||
|
/// — theoretisch — nichts gesetzt ist).
|
||||||
|
Future<DeliveryCredit?> setDeliveryCredit({
|
||||||
|
required String deliveryId,
|
||||||
|
required String clientEventId,
|
||||||
|
required int amountCents,
|
||||||
|
required String reason,
|
||||||
|
String? actorCarId,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Entfernt die Betrags-Gutschrift einer Lieferung (append-only `remove`).
|
||||||
|
/// Rückgabe: aktueller Stand danach (`null`).
|
||||||
|
Future<DeliveryCredit?> removeDeliveryCredit({
|
||||||
|
required String deliveryId,
|
||||||
|
required String clientEventId,
|
||||||
|
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<DeliveryServiceValue> setDeliveryService({
|
||||||
|
required String deliveryId,
|
||||||
|
required String serviceId,
|
||||||
|
bool? boolValue,
|
||||||
|
int? numericValue,
|
||||||
|
String? actorCarId,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Entfernt den Service-Wert einer Lieferung (Service „nicht gesetzt").
|
||||||
|
Future<void> removeDeliveryService({
|
||||||
|
required String deliveryId,
|
||||||
|
required String serviceId,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Wendet eine Liste Scan-Ereignisse als Batch am Server an.
|
||||||
|
///
|
||||||
|
/// Der Endpoint ist bewusst Bulk: damit kann der Client einen
|
||||||
|
/// Scanner-Burst (z. B. 5 Barcodes in 2 Sekunden) in einem HTTP-Call
|
||||||
|
/// abschicken, **muss** aber nicht — auch ein Aufruf mit nur einem
|
||||||
|
/// `ScanIntent` ist erlaubt.
|
||||||
|
///
|
||||||
|
/// Idempotenz: das Backend speichert pro `clientScanId` einmal. Wer
|
||||||
|
/// retried, bekommt `duplicate` zurück; doppelte Anwendung kann es
|
||||||
|
/// nicht geben.
|
||||||
|
///
|
||||||
|
/// Rückgabe: pro Eingabe-Intent ein [ScanOutcome] (Key =
|
||||||
|
/// `clientScanId`). Die Map enthält **jeden** Intent, auch
|
||||||
|
/// `rejected`-Fälle; bei Netzwerk-/Server-Fehlern wirft das Repository
|
||||||
|
/// stattdessen [TourRepositoryException], die Map ist dann nicht
|
||||||
|
/// teilweise gefüllt.
|
||||||
|
Future<Map<String, ScanOutcome>> applyScans(List<ScanIntent> intents);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Allgemeine Repository-Exception für Tour-Operationen. Konkrete Impls
|
||||||
|
/// dürfen spezifischere Subtypen werfen.
|
||||||
|
class TourRepositoryException implements Exception {
|
||||||
|
const TourRepositoryException(this.message, [this.cause]);
|
||||||
|
|
||||||
|
final String message;
|
||||||
|
final Object? cause;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'TourRepositoryException: $message';
|
||||||
|
}
|
||||||
@ -1,20 +0,0 @@
|
|||||||
import 'package:json_annotation/json_annotation.dart';
|
|
||||||
|
|
||||||
part 'address.g.dart';
|
|
||||||
|
|
||||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
|
||||||
class AddressDTO {
|
|
||||||
AddressDTO(
|
|
||||||
{required this.streetName,
|
|
||||||
required this.postalCode,
|
|
||||||
required this.city});
|
|
||||||
|
|
||||||
String streetName;
|
|
||||||
String postalCode;
|
|
||||||
String city;
|
|
||||||
|
|
||||||
factory AddressDTO.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$AddressDTOFromJson(json);
|
|
||||||
|
|
||||||
Map<dynamic, dynamic> toJson() => _$AddressDTOToJson(this);
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'address.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// JsonSerializableGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
AddressDTO _$AddressDTOFromJson(Map<String, dynamic> json) => AddressDTO(
|
|
||||||
streetName: json['street_name'] as String,
|
|
||||||
postalCode: json['postal_code'] as String,
|
|
||||||
city: json['city'] as String,
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$AddressDTOToJson(AddressDTO instance) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'street_name': instance.streetName,
|
|
||||||
'postal_code': instance.postalCode,
|
|
||||||
'city': instance.city,
|
|
||||||
};
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
import 'package:json_annotation/json_annotation.dart';
|
|
||||||
|
|
||||||
import 'component.dart';
|
|
||||||
|
|
||||||
part 'article.g.dart';
|
|
||||||
|
|
||||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
|
||||||
class ArticleDTO {
|
|
||||||
ArticleDTO({
|
|
||||||
required this.name,
|
|
||||||
required this.articleNr,
|
|
||||||
required this.quantity,
|
|
||||||
required this.price,
|
|
||||||
required this.scannable,
|
|
||||||
required this.internalId,
|
|
||||||
required this.scannedRemovedAmount,
|
|
||||||
required this.scannedAmount,
|
|
||||||
required this.removeNoteId,
|
|
||||||
required this.taxRate,
|
|
||||||
required this.isParent,
|
|
||||||
this.components,
|
|
||||||
this.warehouseNr,
|
|
||||||
this.warehouseName,
|
|
||||||
});
|
|
||||||
|
|
||||||
String name;
|
|
||||||
String articleNr;
|
|
||||||
String quantity;
|
|
||||||
String price;
|
|
||||||
String taxRate;
|
|
||||||
String internalId;
|
|
||||||
String scannedAmount;
|
|
||||||
String scannedRemovedAmount;
|
|
||||||
String? removeNoteId;
|
|
||||||
bool scannable;
|
|
||||||
bool isParent;
|
|
||||||
List<ComponentDTO>? components;
|
|
||||||
String? warehouseNr;
|
|
||||||
String? warehouseName;
|
|
||||||
|
|
||||||
factory ArticleDTO.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$ArticleDTOFromJson(json);
|
|
||||||
|
|
||||||
Map<dynamic, dynamic> toJson() => _$ArticleDTOToJson(this);
|
|
||||||
}
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'article.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// JsonSerializableGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
ArticleDTO _$ArticleDTOFromJson(Map<String, dynamic> json) => ArticleDTO(
|
|
||||||
name: json['name'] as String,
|
|
||||||
articleNr: json['article_nr'] as String,
|
|
||||||
quantity: json['quantity'] as String,
|
|
||||||
price: json['price'] as String,
|
|
||||||
scannable: json['scannable'] as bool,
|
|
||||||
internalId: json['internal_id'] as String,
|
|
||||||
scannedRemovedAmount: json['scanned_removed_amount'] as String,
|
|
||||||
scannedAmount: json['scanned_amount'] as String,
|
|
||||||
removeNoteId: json['remove_note_id'] as String?,
|
|
||||||
taxRate: json['tax_rate'] as String,
|
|
||||||
isParent: json['is_parent'] as bool,
|
|
||||||
components:
|
|
||||||
(json['components'] as List<dynamic>?)
|
|
||||||
?.map((e) => ComponentDTO.fromJson(e as Map<String, dynamic>))
|
|
||||||
.toList(),
|
|
||||||
warehouseNr: json['warehouse_nr'] as String?,
|
|
||||||
warehouseName: json['warehouse_name'] as String?,
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$ArticleDTOToJson(ArticleDTO instance) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'name': instance.name,
|
|
||||||
'article_nr': instance.articleNr,
|
|
||||||
'quantity': instance.quantity,
|
|
||||||
'price': instance.price,
|
|
||||||
'tax_rate': instance.taxRate,
|
|
||||||
'internal_id': instance.internalId,
|
|
||||||
'scanned_amount': instance.scannedAmount,
|
|
||||||
'scanned_removed_amount': instance.scannedRemovedAmount,
|
|
||||||
'remove_note_id': instance.removeNoteId,
|
|
||||||
'scannable': instance.scannable,
|
|
||||||
'is_parent': instance.isParent,
|
|
||||||
'components': instance.components,
|
|
||||||
'warehouse_nr': instance.warehouseNr,
|
|
||||||
'warehouse_name': instance.warehouseName,
|
|
||||||
};
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
import 'package:json_annotation/json_annotation.dart';
|
|
||||||
|
|
||||||
part 'basic_response.g.dart';
|
|
||||||
|
|
||||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
|
||||||
class BasicResponseDTO {
|
|
||||||
BasicResponseDTO(
|
|
||||||
{required this.succeeded,
|
|
||||||
required this.message});
|
|
||||||
|
|
||||||
final bool succeeded;
|
|
||||||
final String message;
|
|
||||||
|
|
||||||
factory BasicResponseDTO.fromJson(Map<String, dynamic> json) => _$BasicResponseDTOFromJson(json);
|
|
||||||
Map<dynamic, dynamic> toJson() => _$BasicResponseDTOToJson(this);
|
|
||||||
}
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'basic_response.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// JsonSerializableGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
BasicResponseDTO _$BasicResponseDTOFromJson(Map<String, dynamic> json) =>
|
|
||||||
BasicResponseDTO(
|
|
||||||
succeeded: json['succeeded'] as bool,
|
|
||||||
message: json['message'] as String,
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$BasicResponseDTOToJson(BasicResponseDTO instance) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'succeeded': instance.succeeded,
|
|
||||||
'message': instance.message,
|
|
||||||
};
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
import 'package:json_annotation/json_annotation.dart';
|
|
||||||
|
|
||||||
part 'car.g.dart';
|
|
||||||
|
|
||||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
|
||||||
class CarDTO {
|
|
||||||
CarDTO(
|
|
||||||
{required this.id,
|
|
||||||
required this.plate});
|
|
||||||
|
|
||||||
final String id;
|
|
||||||
final String plate;
|
|
||||||
|
|
||||||
factory CarDTO.fromJson(Map<String, dynamic> json) => _$CarDTOFromJson(json);
|
|
||||||
Map<dynamic, dynamic> toJson() => _$CarDTOToJson(this);
|
|
||||||
}
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'car.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// JsonSerializableGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
CarDTO _$CarDTOFromJson(Map<String, dynamic> json) =>
|
|
||||||
CarDTO(id: json['id'] as String, plate: json['plate'] as String);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$CarDTOToJson(CarDTO instance) => <String, dynamic>{
|
|
||||||
'id': instance.id,
|
|
||||||
'plate': instance.plate,
|
|
||||||
};
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
import 'package:json_annotation/json_annotation.dart';
|
|
||||||
|
|
||||||
part 'car_add.g.dart';
|
|
||||||
|
|
||||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
|
||||||
class CarAddDTO {
|
|
||||||
CarAddDTO(
|
|
||||||
{required this.teamId,
|
|
||||||
required this.plate});
|
|
||||||
|
|
||||||
final int teamId;
|
|
||||||
final String plate;
|
|
||||||
|
|
||||||
factory CarAddDTO.fromJson(Map<String, dynamic> json) => _$CarAddDTOFromJson(json);
|
|
||||||
factory CarAddDTO.make(int teamID, String plate) {
|
|
||||||
Map<String, dynamic> data = {"team_id": teamID, "plate": plate};
|
|
||||||
return CarAddDTO.fromJson(data);
|
|
||||||
}
|
|
||||||
Map<dynamic, dynamic> toJson() => _$CarAddDTOToJson(this);
|
|
||||||
}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'car_add.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// JsonSerializableGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
CarAddDTO _$CarAddDTOFromJson(Map<String, dynamic> json) => CarAddDTO(
|
|
||||||
teamId: (json['team_id'] as num).toInt(),
|
|
||||||
plate: json['plate'] as String,
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$CarAddDTOToJson(CarAddDTO instance) => <String, dynamic>{
|
|
||||||
'team_id': instance.teamId,
|
|
||||||
'plate': instance.plate,
|
|
||||||
};
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
import 'car.dart';
|
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
|
||||||
|
|
||||||
part 'car_add_response.g.dart';
|
|
||||||
|
|
||||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
|
||||||
class CarAddResponseDTO {
|
|
||||||
CarAddResponseDTO(
|
|
||||||
{required this.succeeded,
|
|
||||||
required this.message,
|
|
||||||
required this.car});
|
|
||||||
|
|
||||||
final bool succeeded;
|
|
||||||
final String message;
|
|
||||||
final CarDTO car;
|
|
||||||
|
|
||||||
factory CarAddResponseDTO.fromJson(Map<String, dynamic> json) => _$CarAddResponseDTOFromJson(json);
|
|
||||||
Map<dynamic, dynamic> toJson() => _$CarAddResponseDTOToJson(this);
|
|
||||||
}
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'car_add_response.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// JsonSerializableGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
CarAddResponseDTO _$CarAddResponseDTOFromJson(Map<String, dynamic> json) =>
|
|
||||||
CarAddResponseDTO(
|
|
||||||
succeeded: json['succeeded'] as bool,
|
|
||||||
message: json['message'] as String,
|
|
||||||
car: CarDTO.fromJson(json['car'] as Map<String, dynamic>),
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$CarAddResponseDTOToJson(CarAddResponseDTO instance) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'succeeded': instance.succeeded,
|
|
||||||
'message': instance.message,
|
|
||||||
'car': instance.car,
|
|
||||||
};
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
import 'car.dart';
|
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
|
||||||
|
|
||||||
part 'car_get_response.g.dart';
|
|
||||||
|
|
||||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
|
||||||
class CarGetResponseDTO {
|
|
||||||
CarGetResponseDTO(
|
|
||||||
{required this.succeeded,
|
|
||||||
required this.message,
|
|
||||||
required this.cars});
|
|
||||||
|
|
||||||
final bool succeeded;
|
|
||||||
final String message;
|
|
||||||
final List<CarDTO>? cars;
|
|
||||||
|
|
||||||
factory CarGetResponseDTO.fromJson(Map<String, dynamic> json) => _$CarGetResponseDTOFromJson(json);
|
|
||||||
Map<dynamic, dynamic> toJson() => _$CarGetResponseDTOToJson(this);
|
|
||||||
}
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'car_get_response.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// JsonSerializableGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
CarGetResponseDTO _$CarGetResponseDTOFromJson(Map<String, dynamic> json) =>
|
|
||||||
CarGetResponseDTO(
|
|
||||||
succeeded: json['succeeded'] as bool,
|
|
||||||
message: json['message'] as String,
|
|
||||||
cars:
|
|
||||||
(json['cars'] as List<dynamic>?)
|
|
||||||
?.map((e) => CarDTO.fromJson(e as Map<String, dynamic>))
|
|
||||||
.toList(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$CarGetResponseDTOToJson(CarGetResponseDTO instance) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'succeeded': instance.succeeded,
|
|
||||||
'message': instance.message,
|
|
||||||
'cars': instance.cars,
|
|
||||||
};
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
import 'package:json_annotation/json_annotation.dart';
|
|
||||||
|
|
||||||
part 'component.g.dart';
|
|
||||||
|
|
||||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
|
||||||
class ComponentDTO {
|
|
||||||
ComponentDTO({
|
|
||||||
required this.articleNr,
|
|
||||||
required this.name,
|
|
||||||
required this.quantity,
|
|
||||||
required this.pos,
|
|
||||||
});
|
|
||||||
|
|
||||||
String articleNr;
|
|
||||||
String name;
|
|
||||||
String quantity;
|
|
||||||
String pos;
|
|
||||||
|
|
||||||
factory ComponentDTO.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$ComponentDTOFromJson(json);
|
|
||||||
|
|
||||||
Map<dynamic, dynamic> toJson() => _$ComponentDTOToJson(this);
|
|
||||||
}
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'component.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// JsonSerializableGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
ComponentDTO _$ComponentDTOFromJson(Map<String, dynamic> json) => ComponentDTO(
|
|
||||||
articleNr: json['article_nr'] as String,
|
|
||||||
name: json['name'] as String,
|
|
||||||
quantity: json['quantity'] as String,
|
|
||||||
pos: json['pos'] as String,
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$ComponentDTOToJson(ComponentDTO instance) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'article_nr': instance.articleNr,
|
|
||||||
'name': instance.name,
|
|
||||||
'quantity': instance.quantity,
|
|
||||||
'pos': instance.pos,
|
|
||||||
};
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
import 'package:json_annotation/json_annotation.dart';
|
|
||||||
|
|
||||||
part 'contact_person.g.dart';
|
|
||||||
|
|
||||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
|
||||||
class ContactPersonDTO {
|
|
||||||
ContactPersonDTO(
|
|
||||||
{required this.name,
|
|
||||||
required this.salutation,
|
|
||||||
required this.phoneNo,
|
|
||||||
required this.mobileNo});
|
|
||||||
|
|
||||||
String name;
|
|
||||||
String salutation;
|
|
||||||
String phoneNo;
|
|
||||||
String mobileNo;
|
|
||||||
|
|
||||||
factory ContactPersonDTO.fromJson(Map<String, dynamic> json) => _$ContactPersonDTOFromJson(json);
|
|
||||||
Map<dynamic, dynamic> toJson() => _$ContactPersonDTOToJson(this);
|
|
||||||
}
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'contact_person.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// JsonSerializableGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
ContactPersonDTO _$ContactPersonDTOFromJson(Map<String, dynamic> json) =>
|
|
||||||
ContactPersonDTO(
|
|
||||||
name: json['name'] as String,
|
|
||||||
salutation: json['salutation'] as String,
|
|
||||||
phoneNo: json['phone_no'] as String,
|
|
||||||
mobileNo: json['mobile_no'] as String,
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$ContactPersonDTOToJson(ContactPersonDTO instance) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'name': instance.name,
|
|
||||||
'salutation': instance.salutation,
|
|
||||||
'phone_no': instance.phoneNo,
|
|
||||||
'mobile_no': instance.mobileNo,
|
|
||||||
};
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
import 'address.dart';
|
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
|
||||||
|
|
||||||
part 'customer.g.dart';
|
|
||||||
|
|
||||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
|
||||||
class CustomerDTO {
|
|
||||||
CustomerDTO({required this.name, required this.address, this.eMail});
|
|
||||||
|
|
||||||
String name;
|
|
||||||
AddressDTO address;
|
|
||||||
String? eMail;
|
|
||||||
|
|
||||||
factory CustomerDTO.fromJson(Map<String, dynamic> json) => _$CustomerDTOFromJson(json);
|
|
||||||
Map<dynamic, dynamic> toJson() => _$CustomerDTOToJson(this);
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'customer.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// JsonSerializableGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
CustomerDTO _$CustomerDTOFromJson(Map<String, dynamic> json) => CustomerDTO(
|
|
||||||
name: json['name'] as String,
|
|
||||||
address: AddressDTO.fromJson(json['address'] as Map<String, dynamic>),
|
|
||||||
eMail: json['e_mail'] as String?,
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$CustomerDTOToJson(CustomerDTO instance) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'name': instance.name,
|
|
||||||
'address': instance.address,
|
|
||||||
'e_mail': instance.eMail,
|
|
||||||
};
|
|
||||||
@ -1,84 +0,0 @@
|
|||||||
import 'package:hl_lieferservice/dto/image_note_response.dart';
|
|
||||||
|
|
||||||
import 'article.dart';
|
|
||||||
import 'contact_person.dart';
|
|
||||||
import 'customer.dart';
|
|
||||||
import 'discount.dart';
|
|
||||||
import 'note.dart';
|
|
||||||
import 'payment.dart';
|
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
|
||||||
|
|
||||||
part 'delivery.g.dart';
|
|
||||||
|
|
||||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
|
||||||
class DeliveryOptionDTO {
|
|
||||||
DeliveryOptionDTO({
|
|
||||||
required this.numerical,
|
|
||||||
required this.value,
|
|
||||||
required this.display,
|
|
||||||
required this.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
bool numerical;
|
|
||||||
String value;
|
|
||||||
String display;
|
|
||||||
String key;
|
|
||||||
|
|
||||||
factory DeliveryOptionDTO.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$DeliveryOptionDTOFromJson(json);
|
|
||||||
|
|
||||||
Map<dynamic, dynamic> toJson() => _$DeliveryOptionDTOToJson(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
|
||||||
class DeliveryDTO {
|
|
||||||
DeliveryDTO({
|
|
||||||
required this.internalReceiptNo,
|
|
||||||
required this.specialAggreements,
|
|
||||||
required this.currency,
|
|
||||||
required this.notes,
|
|
||||||
required this.totalPrice,
|
|
||||||
required this.prepayment,
|
|
||||||
required this.paymentAtDelivery,
|
|
||||||
required this.desiredTime,
|
|
||||||
required this.contactPerson,
|
|
||||||
required this.articles,
|
|
||||||
required this.totalNetValue,
|
|
||||||
required this.totalGrossValue,
|
|
||||||
required this.images,
|
|
||||||
required this.customer,
|
|
||||||
required this.finishedTime,
|
|
||||||
required this.note,
|
|
||||||
required this.state,
|
|
||||||
required this.payment,
|
|
||||||
required this.carId,
|
|
||||||
required this.options,
|
|
||||||
});
|
|
||||||
|
|
||||||
String internalReceiptNo;
|
|
||||||
String? specialAggreements;
|
|
||||||
CustomerDTO customer;
|
|
||||||
String totalPrice;
|
|
||||||
String desiredTime;
|
|
||||||
String totalGrossValue;
|
|
||||||
String totalNetValue;
|
|
||||||
ContactPersonDTO contactPerson;
|
|
||||||
String? currency;
|
|
||||||
List<ArticleDTO> articles;
|
|
||||||
String note;
|
|
||||||
String finishedTime;
|
|
||||||
String carId;
|
|
||||||
String state;
|
|
||||||
String prepayment;
|
|
||||||
String paymentAtDelivery;
|
|
||||||
DiscountDTO? discount;
|
|
||||||
PaymentMethodDTO payment;
|
|
||||||
List<NoteDTO> notes;
|
|
||||||
List<ImageNoteDTO> images;
|
|
||||||
List<DeliveryOptionDTO> options;
|
|
||||||
|
|
||||||
factory DeliveryDTO.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$DeliveryDTOFromJson(json);
|
|
||||||
|
|
||||||
Map<dynamic, dynamic> toJson() => _$DeliveryDTOToJson(this);
|
|
||||||
}
|
|
||||||
@ -1,89 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'delivery.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// JsonSerializableGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
DeliveryOptionDTO _$DeliveryOptionDTOFromJson(Map<String, dynamic> json) =>
|
|
||||||
DeliveryOptionDTO(
|
|
||||||
numerical: json['numerical'] as bool,
|
|
||||||
value: json['value'] as String,
|
|
||||||
display: json['display'] as String,
|
|
||||||
key: json['key'] as String,
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$DeliveryOptionDTOToJson(DeliveryOptionDTO instance) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'numerical': instance.numerical,
|
|
||||||
'value': instance.value,
|
|
||||||
'display': instance.display,
|
|
||||||
'key': instance.key,
|
|
||||||
};
|
|
||||||
|
|
||||||
DeliveryDTO _$DeliveryDTOFromJson(Map<String, dynamic> json) => DeliveryDTO(
|
|
||||||
internalReceiptNo: json['internal_receipt_no'] as String,
|
|
||||||
specialAggreements: json['special_aggreements'] as String?,
|
|
||||||
currency: json['currency'] as String?,
|
|
||||||
notes:
|
|
||||||
(json['notes'] as List<dynamic>)
|
|
||||||
.map((e) => NoteDTO.fromJson(e as Map<String, dynamic>))
|
|
||||||
.toList(),
|
|
||||||
totalPrice: json['total_price'] as String,
|
|
||||||
prepayment: json['prepayment'] as String,
|
|
||||||
paymentAtDelivery: json['payment_at_delivery'] as String,
|
|
||||||
desiredTime: json['desired_time'] as String,
|
|
||||||
contactPerson: ContactPersonDTO.fromJson(
|
|
||||||
json['contact_person'] as Map<String, dynamic>,
|
|
||||||
),
|
|
||||||
articles:
|
|
||||||
(json['articles'] as List<dynamic>)
|
|
||||||
.map((e) => ArticleDTO.fromJson(e as Map<String, dynamic>))
|
|
||||||
.toList(),
|
|
||||||
totalNetValue: json['total_net_value'] as String,
|
|
||||||
totalGrossValue: json['total_gross_value'] as String,
|
|
||||||
images:
|
|
||||||
(json['images'] as List<dynamic>)
|
|
||||||
.map((e) => ImageNoteDTO.fromJson(e as Map<String, dynamic>))
|
|
||||||
.toList(),
|
|
||||||
customer: CustomerDTO.fromJson(json['customer'] as Map<String, dynamic>),
|
|
||||||
finishedTime: json['finished_time'] as String,
|
|
||||||
note: json['note'] as String,
|
|
||||||
state: json['state'] as String,
|
|
||||||
payment: PaymentMethodDTO.fromJson(json['payment'] as Map<String, dynamic>),
|
|
||||||
carId: json['car_id'] as String,
|
|
||||||
options:
|
|
||||||
(json['options'] as List<dynamic>)
|
|
||||||
.map((e) => DeliveryOptionDTO.fromJson(e as Map<String, dynamic>))
|
|
||||||
.toList(),
|
|
||||||
)
|
|
||||||
..discount =
|
|
||||||
json['discount'] == null
|
|
||||||
? null
|
|
||||||
: DiscountDTO.fromJson(json['discount'] as Map<String, dynamic>);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$DeliveryDTOToJson(DeliveryDTO instance) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'internal_receipt_no': instance.internalReceiptNo,
|
|
||||||
'special_aggreements': instance.specialAggreements,
|
|
||||||
'customer': instance.customer,
|
|
||||||
'total_price': instance.totalPrice,
|
|
||||||
'desired_time': instance.desiredTime,
|
|
||||||
'total_gross_value': instance.totalGrossValue,
|
|
||||||
'total_net_value': instance.totalNetValue,
|
|
||||||
'contact_person': instance.contactPerson,
|
|
||||||
'currency': instance.currency,
|
|
||||||
'articles': instance.articles,
|
|
||||||
'note': instance.note,
|
|
||||||
'finished_time': instance.finishedTime,
|
|
||||||
'car_id': instance.carId,
|
|
||||||
'state': instance.state,
|
|
||||||
'prepayment': instance.prepayment,
|
|
||||||
'payment_at_delivery': instance.paymentAtDelivery,
|
|
||||||
'discount': instance.discount,
|
|
||||||
'payment': instance.payment,
|
|
||||||
'notes': instance.notes,
|
|
||||||
'images': instance.images,
|
|
||||||
'options': instance.options,
|
|
||||||
};
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
import 'delivery.dart';
|
|
||||||
import 'driver.dart';
|
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
|
||||||
|
|
||||||
part 'delivery_response.g.dart';
|
|
||||||
|
|
||||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
|
||||||
class DeliveryResponseDTO {
|
|
||||||
DeliveryResponseDTO(
|
|
||||||
{required this.deliveries,
|
|
||||||
required this.driver,
|
|
||||||
required this.discountArticleNumber});
|
|
||||||
|
|
||||||
List<DeliveryDTO> deliveries;
|
|
||||||
DriverDTO driver;
|
|
||||||
String discountArticleNumber;
|
|
||||||
|
|
||||||
factory DeliveryResponseDTO.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$DeliveryResponseDTOFromJson(json);
|
|
||||||
|
|
||||||
Map<dynamic, dynamic> toJson() => _$DeliveryResponseDTOToJson(this);
|
|
||||||
}
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'delivery_response.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// JsonSerializableGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
DeliveryResponseDTO _$DeliveryResponseDTOFromJson(Map<String, dynamic> json) =>
|
|
||||||
DeliveryResponseDTO(
|
|
||||||
deliveries:
|
|
||||||
(json['deliveries'] as List<dynamic>)
|
|
||||||
.map((e) => DeliveryDTO.fromJson(e as Map<String, dynamic>))
|
|
||||||
.toList(),
|
|
||||||
driver: DriverDTO.fromJson(json['driver'] as Map<String, dynamic>),
|
|
||||||
discountArticleNumber: json['discount_article_number'] as String,
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$DeliveryResponseDTOToJson(
|
|
||||||
DeliveryResponseDTO instance,
|
|
||||||
) => <String, dynamic>{
|
|
||||||
'deliveries': instance.deliveries,
|
|
||||||
'driver': instance.driver,
|
|
||||||
'discount_article_number': instance.discountArticleNumber,
|
|
||||||
};
|
|
||||||
@ -1,85 +0,0 @@
|
|||||||
import 'package:hl_lieferservice/model/delivery.dart';
|
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
|
||||||
|
|
||||||
part 'delivery_update.g.dart';
|
|
||||||
|
|
||||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
|
||||||
class DeliveryOptionUpdateDTO {
|
|
||||||
DeliveryOptionUpdateDTO({
|
|
||||||
required this.numerical,
|
|
||||||
required this.value,
|
|
||||||
required this.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
bool numerical;
|
|
||||||
String value;
|
|
||||||
String key;
|
|
||||||
|
|
||||||
factory DeliveryOptionUpdateDTO.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$DeliveryOptionUpdateDTOFromJson(json);
|
|
||||||
|
|
||||||
Map<dynamic, dynamic> toJson() => _$DeliveryOptionUpdateDTOToJson(this);
|
|
||||||
|
|
||||||
factory DeliveryOptionUpdateDTO.fromEntity(DeliveryOption option) {
|
|
||||||
return DeliveryOptionUpdateDTO(
|
|
||||||
numerical: option.numerical,
|
|
||||||
value: option.value,
|
|
||||||
key: option.key,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
|
||||||
class DeliveryUpdateDTO {
|
|
||||||
DeliveryUpdateDTO({
|
|
||||||
required this.deliveryId,
|
|
||||||
this.finishedDate,
|
|
||||||
this.selectedPaymentMethodId,
|
|
||||||
this.options,
|
|
||||||
this.state,
|
|
||||||
this.carId,
|
|
||||||
});
|
|
||||||
|
|
||||||
String deliveryId;
|
|
||||||
String? finishedDate;
|
|
||||||
String? state;
|
|
||||||
String? carId;
|
|
||||||
String? selectedPaymentMethodId;
|
|
||||||
List<DeliveryOptionUpdateDTO>? options;
|
|
||||||
|
|
||||||
factory DeliveryUpdateDTO.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$DeliveryUpdateDTOFromJson(json);
|
|
||||||
|
|
||||||
factory DeliveryUpdateDTO.fromEntity(Delivery delivery) {
|
|
||||||
String state = "";
|
|
||||||
|
|
||||||
switch (delivery.state) {
|
|
||||||
case DeliveryState.finished:
|
|
||||||
state = "geliefert";
|
|
||||||
break;
|
|
||||||
case DeliveryState.ongoing:
|
|
||||||
state = "laufend";
|
|
||||||
break;
|
|
||||||
case DeliveryState.onhold:
|
|
||||||
state = "vertagt";
|
|
||||||
break;
|
|
||||||
case DeliveryState.canceled:
|
|
||||||
state = "abgebrochen";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return DeliveryUpdateDTO(
|
|
||||||
deliveryId: delivery.id,
|
|
||||||
state: state,
|
|
||||||
carId: delivery.carId?.toString() ,
|
|
||||||
selectedPaymentMethodId: delivery.payment.id,
|
|
||||||
options: delivery.options.map(DeliveryOptionUpdateDTO.fromEntity).toList(),
|
|
||||||
finishedDate: delivery.state == DeliveryState.finished
|
|
||||||
? DateFormat('yyyy-MM-dd HH:mm').format(DateTime.now())
|
|
||||||
: null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<dynamic, dynamic> toJson() => _$DeliveryUpdateDTOToJson(this);
|
|
||||||
}
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'delivery_update.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// JsonSerializableGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
DeliveryOptionUpdateDTO _$DeliveryOptionUpdateDTOFromJson(
|
|
||||||
Map<String, dynamic> json,
|
|
||||||
) => DeliveryOptionUpdateDTO(
|
|
||||||
numerical: json['numerical'] as bool,
|
|
||||||
value: json['value'] as String,
|
|
||||||
key: json['key'] as String,
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$DeliveryOptionUpdateDTOToJson(
|
|
||||||
DeliveryOptionUpdateDTO instance,
|
|
||||||
) => <String, dynamic>{
|
|
||||||
'numerical': instance.numerical,
|
|
||||||
'value': instance.value,
|
|
||||||
'key': instance.key,
|
|
||||||
};
|
|
||||||
|
|
||||||
DeliveryUpdateDTO _$DeliveryUpdateDTOFromJson(Map<String, dynamic> json) =>
|
|
||||||
DeliveryUpdateDTO(
|
|
||||||
deliveryId: json['delivery_id'] as String,
|
|
||||||
finishedDate: json['finished_date'] as String?,
|
|
||||||
selectedPaymentMethodId: json['selected_payment_method_id'] as String?,
|
|
||||||
options:
|
|
||||||
(json['options'] as List<dynamic>?)
|
|
||||||
?.map(
|
|
||||||
(e) =>
|
|
||||||
DeliveryOptionUpdateDTO.fromJson(e as Map<String, dynamic>),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
state: json['state'] as String?,
|
|
||||||
carId: json['car_id'] as String?,
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$DeliveryUpdateDTOToJson(DeliveryUpdateDTO instance) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'delivery_id': instance.deliveryId,
|
|
||||||
'finished_date': instance.finishedDate,
|
|
||||||
'state': instance.state,
|
|
||||||
'car_id': instance.carId,
|
|
||||||
'selected_payment_method_id': instance.selectedPaymentMethodId,
|
|
||||||
'options': instance.options,
|
|
||||||
};
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
import 'package:json_annotation/json_annotation.dart';
|
|
||||||
|
|
||||||
part 'delivery_update_response.g.dart';
|
|
||||||
|
|
||||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
|
||||||
class DeliveryUpdateResponseDTO {
|
|
||||||
DeliveryUpdateResponseDTO(
|
|
||||||
{required this.message, required this.code});
|
|
||||||
|
|
||||||
final String code;
|
|
||||||
final String message;
|
|
||||||
|
|
||||||
factory DeliveryUpdateResponseDTO.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$DeliveryUpdateResponseDTOFromJson(json);
|
|
||||||
|
|
||||||
Map<dynamic, dynamic> toJson() => _$DeliveryUpdateResponseDTOToJson(this);
|
|
||||||
}
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'delivery_update_response.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// JsonSerializableGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
DeliveryUpdateResponseDTO _$DeliveryUpdateResponseDTOFromJson(
|
|
||||||
Map<String, dynamic> json,
|
|
||||||
) => DeliveryUpdateResponseDTO(
|
|
||||||
message: json['message'] as String,
|
|
||||||
code: json['code'] as String,
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$DeliveryUpdateResponseDTOToJson(
|
|
||||||
DeliveryUpdateResponseDTO instance,
|
|
||||||
) => <String, dynamic>{'code': instance.code, 'message': instance.message};
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
import 'article.dart';
|
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
|
||||||
|
|
||||||
part 'discount.g.dart';
|
|
||||||
|
|
||||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
|
||||||
class DiscountDTO {
|
|
||||||
DiscountDTO({required this.note, required this.noteId, required this.article});
|
|
||||||
String? note;
|
|
||||||
String? noteId;
|
|
||||||
ArticleDTO article;
|
|
||||||
|
|
||||||
factory DiscountDTO.fromJson(Map<String, dynamic> json) => _$DiscountDTOFromJson(json);
|
|
||||||
Map<dynamic, dynamic> toJson() => _$DiscountDTOToJson(this);
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'discount.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// JsonSerializableGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
DiscountDTO _$DiscountDTOFromJson(Map<String, dynamic> json) => DiscountDTO(
|
|
||||||
note: json['note'] as String?,
|
|
||||||
noteId: json['note_id'] as String?,
|
|
||||||
article: ArticleDTO.fromJson(json['article'] as Map<String, dynamic>),
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$DiscountDTOToJson(DiscountDTO instance) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'note': instance.note,
|
|
||||||
'note_id': instance.noteId,
|
|
||||||
'article': instance.article,
|
|
||||||
};
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
import 'package:json_annotation/json_annotation.dart';
|
|
||||||
|
|
||||||
part 'discount_add.g.dart';
|
|
||||||
|
|
||||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
|
||||||
class DiscountAddDTO {
|
|
||||||
DiscountAddDTO(
|
|
||||||
{required this.note, required this.deliveryId, required this.discount});
|
|
||||||
|
|
||||||
String note;
|
|
||||||
String deliveryId;
|
|
||||||
int discount;
|
|
||||||
|
|
||||||
factory DiscountAddDTO.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$DiscountAddDTOFromJson(json);
|
|
||||||
|
|
||||||
Map<dynamic, dynamic> toJson() => _$DiscountAddDTOToJson(this);
|
|
||||||
}
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'discount_add.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// JsonSerializableGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
DiscountAddDTO _$DiscountAddDTOFromJson(Map<String, dynamic> json) =>
|
|
||||||
DiscountAddDTO(
|
|
||||||
note: json['note'] as String,
|
|
||||||
deliveryId: json['delivery_id'] as String,
|
|
||||||
discount: (json['discount'] as num).toInt(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$DiscountAddDTOToJson(DiscountAddDTO instance) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'note': instance.note,
|
|
||||||
'delivery_id': instance.deliveryId,
|
|
||||||
'discount': instance.discount,
|
|
||||||
};
|
|
||||||
@ -1,64 +0,0 @@
|
|||||||
import 'article.dart';
|
|
||||||
import 'basic_response.dart';
|
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
|
||||||
|
|
||||||
part 'discount_add_response.g.dart';
|
|
||||||
|
|
||||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
|
||||||
class PriceInformation {
|
|
||||||
PriceInformation({required this.net, required this.gross});
|
|
||||||
|
|
||||||
double net;
|
|
||||||
double gross;
|
|
||||||
|
|
||||||
factory PriceInformation.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$PriceInformationFromJson(json);
|
|
||||||
|
|
||||||
Map<dynamic, dynamic> toJson() => _$PriceInformationToJson(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
|
||||||
class NoteInformation {
|
|
||||||
NoteInformation({required this.rowId, required this.noteDescription});
|
|
||||||
|
|
||||||
String rowId;
|
|
||||||
String noteDescription;
|
|
||||||
|
|
||||||
factory NoteInformation.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$NoteInformationFromJson(json);
|
|
||||||
|
|
||||||
Map<dynamic, dynamic> toJson() => _$NoteInformationToJson(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
|
||||||
class UpdatedValues {
|
|
||||||
UpdatedValues(
|
|
||||||
{required this.discount,
|
|
||||||
required this.receipt,
|
|
||||||
required this.article,
|
|
||||||
required this.note});
|
|
||||||
|
|
||||||
PriceInformation discount;
|
|
||||||
PriceInformation receipt;
|
|
||||||
NoteInformation note;
|
|
||||||
ArticleDTO article;
|
|
||||||
|
|
||||||
factory UpdatedValues.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$UpdatedValuesFromJson(json);
|
|
||||||
|
|
||||||
Map<dynamic, dynamic> toJson() => _$UpdatedValuesToJson(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
|
||||||
class DiscountAddResponseDTO extends BasicResponseDTO {
|
|
||||||
DiscountAddResponseDTO(
|
|
||||||
{required this.values, required super.succeeded, required super.message});
|
|
||||||
|
|
||||||
UpdatedValues values;
|
|
||||||
|
|
||||||
factory DiscountAddResponseDTO.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$DiscountAddResponseDTOFromJson(json);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Map<dynamic, dynamic> toJson() => _$DiscountAddResponseDTOToJson(this);
|
|
||||||
}
|
|
||||||
@ -1,61 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'discount_add_response.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// JsonSerializableGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
PriceInformation _$PriceInformationFromJson(Map<String, dynamic> json) =>
|
|
||||||
PriceInformation(
|
|
||||||
net: (json['net'] as num).toDouble(),
|
|
||||||
gross: (json['gross'] as num).toDouble(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$PriceInformationToJson(PriceInformation instance) =>
|
|
||||||
<String, dynamic>{'net': instance.net, 'gross': instance.gross};
|
|
||||||
|
|
||||||
NoteInformation _$NoteInformationFromJson(Map<String, dynamic> json) =>
|
|
||||||
NoteInformation(
|
|
||||||
rowId: json['row_id'] as String,
|
|
||||||
noteDescription: json['note_description'] as String,
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$NoteInformationToJson(NoteInformation instance) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'row_id': instance.rowId,
|
|
||||||
'note_description': instance.noteDescription,
|
|
||||||
};
|
|
||||||
|
|
||||||
UpdatedValues _$UpdatedValuesFromJson(
|
|
||||||
Map<String, dynamic> json,
|
|
||||||
) => UpdatedValues(
|
|
||||||
discount: PriceInformation.fromJson(json['discount'] as Map<String, dynamic>),
|
|
||||||
receipt: PriceInformation.fromJson(json['receipt'] as Map<String, dynamic>),
|
|
||||||
article: ArticleDTO.fromJson(json['article'] as Map<String, dynamic>),
|
|
||||||
note: NoteInformation.fromJson(json['note'] as Map<String, dynamic>),
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$UpdatedValuesToJson(UpdatedValues instance) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'discount': instance.discount,
|
|
||||||
'receipt': instance.receipt,
|
|
||||||
'note': instance.note,
|
|
||||||
'article': instance.article,
|
|
||||||
};
|
|
||||||
|
|
||||||
DiscountAddResponseDTO _$DiscountAddResponseDTOFromJson(
|
|
||||||
Map<String, dynamic> json,
|
|
||||||
) => DiscountAddResponseDTO(
|
|
||||||
values: UpdatedValues.fromJson(json['values'] as Map<String, dynamic>),
|
|
||||||
succeeded: json['succeeded'] as bool,
|
|
||||||
message: json['message'] as String,
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$DiscountAddResponseDTOToJson(
|
|
||||||
DiscountAddResponseDTO instance,
|
|
||||||
) => <String, dynamic>{
|
|
||||||
'succeeded': instance.succeeded,
|
|
||||||
'message': instance.message,
|
|
||||||
'values': instance.values,
|
|
||||||
};
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
import 'package:json_annotation/json_annotation.dart';
|
|
||||||
|
|
||||||
part 'discount_remove.g.dart';
|
|
||||||
|
|
||||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
|
||||||
class DiscountRemoveDTO {
|
|
||||||
DiscountRemoveDTO(
|
|
||||||
{required this.deliveryId});
|
|
||||||
String deliveryId;
|
|
||||||
|
|
||||||
factory DiscountRemoveDTO.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$DiscountRemoveDTOFromJson(json);
|
|
||||||
|
|
||||||
Map<dynamic, dynamic> toJson() => _$DiscountRemoveDTOToJson(this);
|
|
||||||
}
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'discount_remove.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// JsonSerializableGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
DiscountRemoveDTO _$DiscountRemoveDTOFromJson(Map<String, dynamic> json) =>
|
|
||||||
DiscountRemoveDTO(deliveryId: json['delivery_id'] as String);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$DiscountRemoveDTOToJson(DiscountRemoveDTO instance) =>
|
|
||||||
<String, dynamic>{'delivery_id': instance.deliveryId};
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
import 'basic_response.dart';
|
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
|
||||||
|
|
||||||
import 'discount_add_response.dart';
|
|
||||||
|
|
||||||
part 'discount_remove_response.g.dart';
|
|
||||||
|
|
||||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
|
||||||
class DiscountRemoveResponseDTO extends BasicResponseDTO {
|
|
||||||
DiscountRemoveResponseDTO(
|
|
||||||
{
|
|
||||||
required this.receipt,
|
|
||||||
required super.succeeded,
|
|
||||||
required super.message});
|
|
||||||
|
|
||||||
PriceInformation receipt;
|
|
||||||
|
|
||||||
factory DiscountRemoveResponseDTO.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$DiscountRemoveResponseDTOFromJson(json);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Map<dynamic, dynamic> toJson() => _$DiscountRemoveResponseDTOToJson(this);
|
|
||||||
}
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'discount_remove_response.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// JsonSerializableGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
DiscountRemoveResponseDTO _$DiscountRemoveResponseDTOFromJson(
|
|
||||||
Map<String, dynamic> json,
|
|
||||||
) => DiscountRemoveResponseDTO(
|
|
||||||
receipt: PriceInformation.fromJson(json['receipt'] as Map<String, dynamic>),
|
|
||||||
succeeded: json['succeeded'] as bool,
|
|
||||||
message: json['message'] as String,
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$DiscountRemoveResponseDTOToJson(
|
|
||||||
DiscountRemoveResponseDTO instance,
|
|
||||||
) => <String, dynamic>{
|
|
||||||
'succeeded': instance.succeeded,
|
|
||||||
'message': instance.message,
|
|
||||||
'receipt': instance.receipt,
|
|
||||||
};
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
import 'package:json_annotation/json_annotation.dart';
|
|
||||||
|
|
||||||
part 'discount_update.g.dart';
|
|
||||||
|
|
||||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
|
||||||
class DiscountUpdateDTO {
|
|
||||||
DiscountUpdateDTO(
|
|
||||||
{required this.note, required this.deliveryId, required this.discount});
|
|
||||||
|
|
||||||
String? note;
|
|
||||||
String deliveryId;
|
|
||||||
int? discount;
|
|
||||||
|
|
||||||
factory DiscountUpdateDTO.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$DiscountUpdateDTOFromJson(json);
|
|
||||||
|
|
||||||
Map<dynamic, dynamic> toJson() => _$DiscountUpdateDTOToJson(this);
|
|
||||||
}
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'discount_update.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// JsonSerializableGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
DiscountUpdateDTO _$DiscountUpdateDTOFromJson(Map<String, dynamic> json) =>
|
|
||||||
DiscountUpdateDTO(
|
|
||||||
note: json['note'] as String?,
|
|
||||||
deliveryId: json['delivery_id'] as String,
|
|
||||||
discount: (json['discount'] as num?)?.toInt(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$DiscountUpdateDTOToJson(DiscountUpdateDTO instance) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'note': instance.note,
|
|
||||||
'delivery_id': instance.deliveryId,
|
|
||||||
'discount': instance.discount,
|
|
||||||
};
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
|
|
||||||
import 'basic_response.dart';
|
|
||||||
import 'discount_add_response.dart';
|
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
|
||||||
|
|
||||||
part 'discount_update_response.g.dart';
|
|
||||||
|
|
||||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
|
||||||
class DiscountUpdateResponseDTO extends BasicResponseDTO {
|
|
||||||
DiscountUpdateResponseDTO(
|
|
||||||
{required this.values,
|
|
||||||
required super.succeeded,
|
|
||||||
required super.message});
|
|
||||||
|
|
||||||
UpdatedValues? values;
|
|
||||||
|
|
||||||
factory DiscountUpdateResponseDTO.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$DiscountUpdateResponseDTOFromJson(json);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Map<dynamic, dynamic> toJson() => _$DiscountUpdateResponseDTOToJson(this);
|
|
||||||
}
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'discount_update_response.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// JsonSerializableGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
DiscountUpdateResponseDTO _$DiscountUpdateResponseDTOFromJson(
|
|
||||||
Map<String, dynamic> json,
|
|
||||||
) => DiscountUpdateResponseDTO(
|
|
||||||
values:
|
|
||||||
json['values'] == null
|
|
||||||
? null
|
|
||||||
: UpdatedValues.fromJson(json['values'] as Map<String, dynamic>),
|
|
||||||
succeeded: json['succeeded'] as bool,
|
|
||||||
message: json['message'] as String,
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$DiscountUpdateResponseDTOToJson(
|
|
||||||
DiscountUpdateResponseDTO instance,
|
|
||||||
) => <String, dynamic>{
|
|
||||||
'succeeded': instance.succeeded,
|
|
||||||
'message': instance.message,
|
|
||||||
'values': instance.values,
|
|
||||||
};
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
import 'car.dart';
|
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
|
||||||
|
|
||||||
part 'driver.g.dart';
|
|
||||||
|
|
||||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
|
||||||
class DriverDTO {
|
|
||||||
DriverDTO({required this.id, required this.name, required this.salutation, required this.cars});
|
|
||||||
String id;
|
|
||||||
String name;
|
|
||||||
String salutation;
|
|
||||||
List<CarDTO> cars;
|
|
||||||
|
|
||||||
factory DriverDTO.fromJson(Map<String, dynamic> json) => _$DriverDTOFromJson(json);
|
|
||||||
Map<dynamic, dynamic> toJson() => _$DriverDTOToJson(this);
|
|
||||||
}
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'driver.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// JsonSerializableGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
DriverDTO _$DriverDTOFromJson(Map<String, dynamic> json) => DriverDTO(
|
|
||||||
id: json['id'] as String,
|
|
||||||
name: json['name'] as String,
|
|
||||||
salutation: json['salutation'] as String,
|
|
||||||
cars:
|
|
||||||
(json['cars'] as List<dynamic>)
|
|
||||||
.map((e) => CarDTO.fromJson(e as Map<String, dynamic>))
|
|
||||||
.toList(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$DriverDTOToJson(DriverDTO instance) => <String, dynamic>{
|
|
||||||
'id': instance.id,
|
|
||||||
'name': instance.name,
|
|
||||||
'salutation': instance.salutation,
|
|
||||||
'cars': instance.cars,
|
|
||||||
};
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
import 'package:json_annotation/json_annotation.dart';
|
|
||||||
|
|
||||||
part 'image.g.dart';
|
|
||||||
|
|
||||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
|
||||||
class ImageDTO {
|
|
||||||
ImageDTO(
|
|
||||||
{required this.url, required this.name, required this.oid});
|
|
||||||
|
|
||||||
String url;
|
|
||||||
String name;
|
|
||||||
String oid;
|
|
||||||
|
|
||||||
factory ImageDTO.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$ImageDTOFromJson(json);
|
|
||||||
|
|
||||||
Map<dynamic, dynamic> toJson() => _$ImageDTOToJson(this);
|
|
||||||
}
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'image.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// JsonSerializableGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
ImageDTO _$ImageDTOFromJson(Map<String, dynamic> json) => ImageDTO(
|
|
||||||
url: json['url'] as String,
|
|
||||||
name: json['name'] as String,
|
|
||||||
oid: json['oid'] as String,
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$ImageDTOToJson(ImageDTO instance) => <String, dynamic>{
|
|
||||||
'url': instance.url,
|
|
||||||
'name': instance.name,
|
|
||||||
'oid': instance.oid,
|
|
||||||
};
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
import 'package:json_annotation/json_annotation.dart';
|
|
||||||
|
|
||||||
part 'image_note_response.g.dart';
|
|
||||||
|
|
||||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
|
||||||
class ImageNoteDTO {
|
|
||||||
final String url;
|
|
||||||
final String oid;
|
|
||||||
final String name;
|
|
||||||
|
|
||||||
ImageNoteDTO({required this.url, required this.oid, required this.name});
|
|
||||||
|
|
||||||
factory ImageNoteDTO.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$ImageNoteDTOFromJson(json);
|
|
||||||
|
|
||||||
Map<dynamic, dynamic> toJson() => _$ImageNoteDTOToJson(this);
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'image_note_response.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// JsonSerializableGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
ImageNoteDTO _$ImageNoteDTOFromJson(Map<String, dynamic> json) => ImageNoteDTO(
|
|
||||||
url: json['url'] as String,
|
|
||||||
oid: json['oid'] as String,
|
|
||||||
name: json['name'] as String,
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$ImageNoteDTOToJson(ImageNoteDTO instance) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'url': instance.url,
|
|
||||||
'oid': instance.oid,
|
|
||||||
'name': instance.name,
|
|
||||||
};
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
import 'package:json_annotation/json_annotation.dart';
|
|
||||||
|
|
||||||
part 'note.g.dart';
|
|
||||||
|
|
||||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
|
||||||
class NoteDTO {
|
|
||||||
NoteDTO(
|
|
||||||
{required this.id,
|
|
||||||
required this.note});
|
|
||||||
|
|
||||||
final String id;
|
|
||||||
final String note;
|
|
||||||
|
|
||||||
factory NoteDTO.fromJson(Map<String, dynamic> json) => _$NoteDTOFromJson(json);
|
|
||||||
Map<dynamic, dynamic> toJson() => _$NoteDTOToJson(this);
|
|
||||||
}
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'note.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// JsonSerializableGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
NoteDTO _$NoteDTOFromJson(Map<String, dynamic> json) =>
|
|
||||||
NoteDTO(id: json['id'] as String, note: json['note'] as String);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$NoteDTOToJson(NoteDTO instance) => <String, dynamic>{
|
|
||||||
'id': instance.id,
|
|
||||||
'note': instance.note,
|
|
||||||
};
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
import 'package:hl_lieferservice/dto/basic_response.dart';
|
|
||||||
import 'package:hl_lieferservice/dto/note.dart';
|
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
|
||||||
|
|
||||||
part 'note_add_response.g.dart';
|
|
||||||
|
|
||||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
|
||||||
class NoteAddResponseDTO extends BasicResponseDTO {
|
|
||||||
NoteAddResponseDTO(
|
|
||||||
{required this.note, required super.succeeded, required super.message});
|
|
||||||
|
|
||||||
final NoteDTO? note;
|
|
||||||
|
|
||||||
factory NoteAddResponseDTO.fromJson(Map<String, dynamic> json) => _$NoteAddResponseDTOFromJson(json);
|
|
||||||
@override
|
|
||||||
Map<dynamic, dynamic> toJson() => _$NoteAddResponseDTOToJson(this);
|
|
||||||
}
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'note_add_response.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// JsonSerializableGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
NoteAddResponseDTO _$NoteAddResponseDTOFromJson(Map<String, dynamic> json) =>
|
|
||||||
NoteAddResponseDTO(
|
|
||||||
note:
|
|
||||||
json['note'] == null
|
|
||||||
? null
|
|
||||||
: NoteDTO.fromJson(json['note'] as Map<String, dynamic>),
|
|
||||||
succeeded: json['succeeded'] as bool,
|
|
||||||
message: json['message'] as String,
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$NoteAddResponseDTOToJson(NoteAddResponseDTO instance) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'succeeded': instance.succeeded,
|
|
||||||
'message': instance.message,
|
|
||||||
'note': instance.note,
|
|
||||||
};
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
import 'package:hl_lieferservice/dto/image_note_response.dart';
|
|
||||||
|
|
||||||
import 'note.dart';
|
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
|
||||||
|
|
||||||
part 'note_get_response.g.dart';
|
|
||||||
|
|
||||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
|
||||||
class NoteGetResponseDTO {
|
|
||||||
NoteGetResponseDTO(
|
|
||||||
{required this.notes, required this.succeeded, required this.message, required this.images});
|
|
||||||
|
|
||||||
final List<NoteDTO> notes;
|
|
||||||
final List<ImageNoteDTO> images;
|
|
||||||
final bool succeeded;
|
|
||||||
final String message;
|
|
||||||
|
|
||||||
factory NoteGetResponseDTO.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$NoteGetResponseDTOFromJson(json);
|
|
||||||
|
|
||||||
Map<dynamic, dynamic> toJson() => _$NoteGetResponseDTOToJson(this);
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'note_get_response.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// JsonSerializableGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
NoteGetResponseDTO _$NoteGetResponseDTOFromJson(Map<String, dynamic> json) =>
|
|
||||||
NoteGetResponseDTO(
|
|
||||||
notes:
|
|
||||||
(json['notes'] as List<dynamic>)
|
|
||||||
.map((e) => NoteDTO.fromJson(e as Map<String, dynamic>))
|
|
||||||
.toList(),
|
|
||||||
succeeded: json['succeeded'] as bool,
|
|
||||||
message: json['message'] as String,
|
|
||||||
images:
|
|
||||||
(json['images'] as List<dynamic>)
|
|
||||||
.map((e) => ImageNoteDTO.fromJson(e as Map<String, dynamic>))
|
|
||||||
.toList(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$NoteGetResponseDTOToJson(NoteGetResponseDTO instance) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'notes': instance.notes,
|
|
||||||
'images': instance.images,
|
|
||||||
'succeeded': instance.succeeded,
|
|
||||||
'message': instance.message,
|
|
||||||
};
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
import 'package:json_annotation/json_annotation.dart';
|
|
||||||
|
|
||||||
part 'note_template.g.dart';
|
|
||||||
|
|
||||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
|
||||||
class NoteTemplateDTO {
|
|
||||||
NoteTemplateDTO(
|
|
||||||
{required this.language,
|
|
||||||
required this.title,
|
|
||||||
required this.note});
|
|
||||||
|
|
||||||
final String note;
|
|
||||||
final String language;
|
|
||||||
final String title;
|
|
||||||
|
|
||||||
factory NoteTemplateDTO.fromJson(Map<String, dynamic> json) => _$NoteTemplateDTOFromJson(json);
|
|
||||||
Map<dynamic, dynamic> toJson() => _$NoteTemplateDTOToJson(this);
|
|
||||||
}
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'note_template.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// JsonSerializableGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
NoteTemplateDTO _$NoteTemplateDTOFromJson(Map<String, dynamic> json) =>
|
|
||||||
NoteTemplateDTO(
|
|
||||||
language: json['language'] as String,
|
|
||||||
title: json['title'] as String,
|
|
||||||
note: json['note'] as String,
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$NoteTemplateDTOToJson(NoteTemplateDTO instance) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'note': instance.note,
|
|
||||||
'language': instance.language,
|
|
||||||
'title': instance.title,
|
|
||||||
};
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
import 'note_template.dart';
|
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
|
||||||
|
|
||||||
part 'note_template_response.g.dart';
|
|
||||||
|
|
||||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
|
||||||
class NoteTemplateResponseDTO {
|
|
||||||
NoteTemplateResponseDTO(
|
|
||||||
{required this.notes, required this.succeeded, required this.message});
|
|
||||||
|
|
||||||
final List<NoteTemplateDTO> notes;
|
|
||||||
final bool succeeded;
|
|
||||||
final String message;
|
|
||||||
|
|
||||||
factory NoteTemplateResponseDTO.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$NoteTemplateResponseDTOFromJson(json);
|
|
||||||
|
|
||||||
Map<dynamic, dynamic> toJson() => _$NoteTemplateResponseDTOToJson(this);
|
|
||||||
}
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'note_template_response.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// JsonSerializableGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
NoteTemplateResponseDTO _$NoteTemplateResponseDTOFromJson(
|
|
||||||
Map<String, dynamic> json,
|
|
||||||
) => NoteTemplateResponseDTO(
|
|
||||||
notes:
|
|
||||||
(json['notes'] as List<dynamic>)
|
|
||||||
.map((e) => NoteTemplateDTO.fromJson(e as Map<String, dynamic>))
|
|
||||||
.toList(),
|
|
||||||
succeeded: json['succeeded'] as bool,
|
|
||||||
message: json['message'] as String,
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$NoteTemplateResponseDTOToJson(
|
|
||||||
NoteTemplateResponseDTO instance,
|
|
||||||
) => <String, dynamic>{
|
|
||||||
'notes': instance.notes,
|
|
||||||
'succeeded': instance.succeeded,
|
|
||||||
'message': instance.message,
|
|
||||||
};
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
import 'package:json_annotation/json_annotation.dart';
|
|
||||||
|
|
||||||
part 'payment.g.dart';
|
|
||||||
|
|
||||||
@JsonSerializable()
|
|
||||||
class PaymentMethodDTO {
|
|
||||||
PaymentMethodDTO(this.id,
|
|
||||||
{required this.description,
|
|
||||||
required this.shortCode});
|
|
||||||
|
|
||||||
final String id;
|
|
||||||
final String description;
|
|
||||||
final String shortCode;
|
|
||||||
|
|
||||||
factory PaymentMethodDTO.fromJson(Map<String, dynamic> json) => _$PaymentMethodDTOFromJson(json);
|
|
||||||
Map<dynamic, dynamic> toJson() => _$PaymentMethodDTOToJson(this);
|
|
||||||
}
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'payment.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// JsonSerializableGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
PaymentMethodDTO _$PaymentMethodDTOFromJson(Map<String, dynamic> json) =>
|
|
||||||
PaymentMethodDTO(
|
|
||||||
json['id'] as String,
|
|
||||||
description: json['description'] as String,
|
|
||||||
shortCode: json['shortCode'] as String,
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$PaymentMethodDTOToJson(PaymentMethodDTO instance) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'id': instance.id,
|
|
||||||
'description': instance.description,
|
|
||||||
'shortCode': instance.shortCode,
|
|
||||||
};
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
import 'payment.dart';
|
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
|
||||||
|
|
||||||
part 'payments.g.dart';
|
|
||||||
|
|
||||||
@JsonSerializable()
|
|
||||||
class PaymentMethodListDTO {
|
|
||||||
PaymentMethodListDTO({required this.paymentMethods});
|
|
||||||
|
|
||||||
final List<PaymentMethodDTO> paymentMethods;
|
|
||||||
|
|
||||||
factory PaymentMethodListDTO.fromJson(Map<String, dynamic> json) => _$PaymentMethodListDTOFromJson(json);
|
|
||||||
Map<dynamic, dynamic> toJson() => _$PaymentMethodListDTOToJson(this);
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'payments.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// JsonSerializableGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
PaymentMethodListDTO _$PaymentMethodListDTOFromJson(
|
|
||||||
Map<String, dynamic> json,
|
|
||||||
) => PaymentMethodListDTO(
|
|
||||||
paymentMethods:
|
|
||||||
(json['paymentMethods'] as List<dynamic>)
|
|
||||||
.map((e) => PaymentMethodDTO.fromJson(e as Map<String, dynamic>))
|
|
||||||
.toList(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$PaymentMethodListDTOToJson(
|
|
||||||
PaymentMethodListDTO instance,
|
|
||||||
) => <String, dynamic>{'paymentMethods': instance.paymentMethods};
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
import 'package:json_annotation/json_annotation.dart';
|
|
||||||
|
|
||||||
part 'scan.g.dart';
|
|
||||||
|
|
||||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
|
||||||
class ScanDTO {
|
|
||||||
ScanDTO({required this.internalId});
|
|
||||||
|
|
||||||
String internalId;
|
|
||||||
|
|
||||||
factory ScanDTO.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$ScanDTOFromJson(json);
|
|
||||||
|
|
||||||
Map<dynamic, dynamic> toJson() => _$ScanDTOToJson(this);
|
|
||||||
}
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'scan.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// JsonSerializableGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
ScanDTO _$ScanDTOFromJson(Map<String, dynamic> json) =>
|
|
||||||
ScanDTO(internalId: json['internal_id'] as String);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$ScanDTOToJson(ScanDTO instance) => <String, dynamic>{
|
|
||||||
'internal_id': instance.internalId,
|
|
||||||
};
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
import 'package:json_annotation/json_annotation.dart';
|
|
||||||
|
|
||||||
part 'scan_response.g.dart';
|
|
||||||
|
|
||||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
|
||||||
class ScanResponseDTO {
|
|
||||||
ScanResponseDTO(
|
|
||||||
{required this.message, required this.succeeded, required this.noteId});
|
|
||||||
|
|
||||||
final bool succeeded;
|
|
||||||
final String message;
|
|
||||||
final String? noteId;
|
|
||||||
|
|
||||||
factory ScanResponseDTO.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$ScanResponseDTOFromJson(json);
|
|
||||||
|
|
||||||
Map<dynamic, dynamic> toJson() => _$ScanResponseDTOToJson(this);
|
|
||||||
}
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'scan_response.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// JsonSerializableGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
ScanResponseDTO _$ScanResponseDTOFromJson(Map<String, dynamic> json) =>
|
|
||||||
ScanResponseDTO(
|
|
||||||
message: json['message'] as String,
|
|
||||||
succeeded: json['succeeded'] as bool,
|
|
||||||
noteId: json['note_id'] as String?,
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$ScanResponseDTOToJson(ScanResponseDTO instance) =>
|
|
||||||
<String, dynamic>{
|
|
||||||
'succeeded': instance.succeeded,
|
|
||||||
'message': instance.message,
|
|
||||||
'note_id': instance.noteId,
|
|
||||||
};
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
import 'package:json_annotation/json_annotation.dart';
|
|
||||||
|
|
||||||
part 'set_article_amount_request.g.dart';
|
|
||||||
|
|
||||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
|
||||||
class SetArticleAmountRequestDTO {
|
|
||||||
SetArticleAmountRequestDTO({
|
|
||||||
required this.articleId,
|
|
||||||
required this.deliveryId,
|
|
||||||
required this.amount,
|
|
||||||
this.reason
|
|
||||||
});
|
|
||||||
|
|
||||||
String deliveryId;
|
|
||||||
int amount;
|
|
||||||
String articleId;
|
|
||||||
String? reason;
|
|
||||||
|
|
||||||
factory SetArticleAmountRequestDTO.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$SetArticleAmountRequestDTOFromJson(json);
|
|
||||||
|
|
||||||
Map<dynamic, dynamic> toJson() => _$SetArticleAmountRequestDTOToJson(this);
|
|
||||||
}
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'set_article_amount_request.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// JsonSerializableGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
SetArticleAmountRequestDTO _$SetArticleAmountRequestDTOFromJson(
|
|
||||||
Map<String, dynamic> json,
|
|
||||||
) => SetArticleAmountRequestDTO(
|
|
||||||
articleId: json['article_id'] as String,
|
|
||||||
deliveryId: json['delivery_id'] as String,
|
|
||||||
amount: (json['amount'] as num).toInt(),
|
|
||||||
reason: json['reason'] as String?,
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$SetArticleAmountRequestDTOToJson(
|
|
||||||
SetArticleAmountRequestDTO instance,
|
|
||||||
) => <String, dynamic>{
|
|
||||||
'delivery_id': instance.deliveryId,
|
|
||||||
'amount': instance.amount,
|
|
||||||
'article_id': instance.articleId,
|
|
||||||
'reason': instance.reason,
|
|
||||||
};
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
import 'package:hl_lieferservice/dto/basic_response.dart';
|
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
|
||||||
|
|
||||||
part 'set_article_amount_response.g.dart';
|
|
||||||
|
|
||||||
@JsonSerializable(fieldRename: FieldRename.snake)
|
|
||||||
class SetArticleAmountResponseDTO extends BasicResponseDTO {
|
|
||||||
SetArticleAmountResponseDTO({
|
|
||||||
required super.succeeded,
|
|
||||||
required super.message,
|
|
||||||
this.noteId
|
|
||||||
});
|
|
||||||
|
|
||||||
String? noteId;
|
|
||||||
|
|
||||||
factory SetArticleAmountResponseDTO.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$SetArticleAmountResponseDTOFromJson(json);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Map<dynamic, dynamic> toJson() => _$SetArticleAmountResponseDTOToJson(this);
|
|
||||||
}
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'set_article_amount_response.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// JsonSerializableGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
SetArticleAmountResponseDTO _$SetArticleAmountResponseDTOFromJson(
|
|
||||||
Map<String, dynamic> json,
|
|
||||||
) => SetArticleAmountResponseDTO(
|
|
||||||
succeeded: json['succeeded'] as bool,
|
|
||||||
message: json['message'] as String,
|
|
||||||
noteId: json['note_id'] as String?,
|
|
||||||
);
|
|
||||||
|
|
||||||
Map<String, dynamic> _$SetArticleAmountResponseDTOToJson(
|
|
||||||
SetArticleAmountResponseDTO instance,
|
|
||||||
) => <String, dynamic>{
|
|
||||||
'succeeded': instance.succeeded,
|
|
||||||
'message': instance.message,
|
|
||||||
'note_id': instance.noteId,
|
|
||||||
};
|
|
||||||
@ -1 +0,0 @@
|
|||||||
class AppConfigNotFound implements Exception {}
|
|
||||||
@ -81,7 +81,16 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
|||||||
Emitter<AuthState> emit,
|
Emitter<AuthState> emit,
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
final restored = await tokenProvider.restoreSession();
|
// Timeout-Schutz: hängt der Restore (z. B. nativer flutter_appauth-
|
||||||
|
// Token-Call nach Hot-Restart, nicht erreichbarer Issuer), darf der
|
||||||
|
// Bootstrap NICHT ewig im Splash bleiben. Nach dem Timeout fallen wir
|
||||||
|
// sauber auf die LoginPage zurück. Läuft der Restore später doch noch
|
||||||
|
// erfolgreich durch, kommt der Login via Stream-Event (AuthLoggedIn)
|
||||||
|
// nachträglich an und der State wird zu Authenticated.
|
||||||
|
final restored = await tokenProvider.restoreSession().timeout(
|
||||||
|
const Duration(seconds: 15),
|
||||||
|
onTimeout: () => false,
|
||||||
|
);
|
||||||
if (!restored) {
|
if (!restored) {
|
||||||
// Kein gespeicherter Refresh-Token oder Refresh fehlgeschlagen:
|
// Kein gespeicherter Refresh-Token oder Refresh fehlgeschlagen:
|
||||||
// Vom Splash zur LoginPage übergehen. Kein Snackbar — das ist
|
// Vom Splash zur LoginPage übergehen. Kein Snackbar — das ist
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user