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 = { for (final c in customers) c.id: c.toDomain(), }; final contactsMap = { for (final c in customerContacts) c.id: c.toDomain(), }; final articlesMap = { for (final a in articles) a.id: a.toDomain(), }; final warehousesMap = { 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 = >{}; for (final n in notes) { final domain = n.toDomain(); (notesGrouped[domain.deliveryId] ??= []).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 = { 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 = >{}; for (final v in deliveryServices) { (serviceValues[v.deliveryId] ??= {})[ 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 = >{}; for (final s in contactSources) { final domain = s.toDomain(); (sourcesGrouped[domain.deliveryId] ??= []).add(domain); } final channelsGrouped = >{}; for (final c in contactChannels) { final domain = c.toDomain(); (channelsGrouped[domain.sourceId] ??= []).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, ); }