Final commit.
This commit is contained in:
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,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user