1
0
Fork 0
mirror of https://github.com/s-frick/effigenix.git synced 2026-03-29 01:13:40 +01:00

feat(cli): Stammdaten-TUI mit Master Data API-Anbindung

- Neue Screens: Kategorien, Lieferanten, Artikel, Kunden (jeweils
  Liste, Detail, Anlegen + Detailaktionen wie Bewertung, Zertifikate,
  Verkaufseinheiten, Lieferadressen, Präferenzen)
- API-Client: Resources für alle 4 Stammdaten-Aggregate implementiert
  (categories, suppliers, articles, customers) mit Mapping von
  verschachtelten Domain-VOs auf flache DTOs
- Lieferant, Artikel, Kategorie: echte HTTP-Calls gegen Backend
  (/api/suppliers, /api/articles, /api/categories, /api/customers)
- 204-No-Content-Endpoints (removeSalesUnit, removeSupplier,
  removeCertificate, removeDeliveryAddress, removeFrameContract)
  lösen Re-Fetch des Aggregats aus
- MasterdataMenu, Navigation-Erweiterung, App.tsx-Routing
This commit is contained in:
Sebastian Frick 2026-02-18 13:35:20 +01:00
parent 797f435a49
commit d27dbaa843
30 changed files with 3882 additions and 1 deletions

View file

@ -0,0 +1,297 @@
/**
* Customers resource Real HTTP implementation.
* Endpoints: GET/POST /api/customers, GET/PUT /api/customers/{id},
* POST /api/customers/{id}/activate|deactivate,
* POST /api/customers/{id}/delivery-addresses,
* DELETE /api/customers/{id}/delivery-addresses/{label},
* PUT /api/customers/{id}/frame-contract,
* DELETE /api/customers/{id}/frame-contract,
* PUT /api/customers/{id}/preferences
*
* NOTE: Backend returns domain objects with nested VOs:
* { "id": {"value":"uuid"}, "name": {"value":"string"},
* "billingAddress": {street, houseNumber, postalCode, city, country},
* "contactInfo": {phone, email, contactPerson},
* "paymentTerms": {paymentDueDays, description},
* "deliveryAddresses": [{label, address: {...}, contactPerson, deliveryNotes}],
* "frameContract": {"id": {"value":"uuid"}, validFrom, validUntil, deliveryRhythm, lineItems},
* "preferences": ["BIO", ...], "status": "ACTIVE", ... }
* DELETE delivery-addresses/{label} and DELETE frame-contract return 204 re-fetch.
*/
import type { AxiosInstance } from 'axios';
import type { AddressDTO, ContactInfoDTO, PaymentTermsDTO } from './suppliers.js';
export type CustomerType = 'B2B' | 'B2C';
export type CustomerStatus = 'ACTIVE' | 'INACTIVE';
export type CustomerPreference =
| 'BIO'
| 'REGIONAL'
| 'TIERWOHL'
| 'HALAL'
| 'KOSHER'
| 'GLUTENFREI'
| 'LAKTOSEFREI';
export type DeliveryRhythm = 'DAILY' | 'WEEKLY' | 'BIWEEKLY' | 'MONTHLY' | 'ON_DEMAND';
export const CUSTOMER_PREFERENCE_LABELS: Record<CustomerPreference, string> = {
BIO: 'Bio',
REGIONAL: 'Regional',
TIERWOHL: 'Tierwohl',
HALAL: 'Halal',
KOSHER: 'Koscher',
GLUTENFREI: 'Glutenfrei',
LAKTOSEFREI: 'Laktosefrei',
};
export const DELIVERY_RHYTHM_LABELS: Record<DeliveryRhythm, string> = {
DAILY: 'Täglich',
WEEKLY: 'Wöchentlich',
BIWEEKLY: 'Zweiwöchentlich',
MONTHLY: 'Monatlich',
ON_DEMAND: 'Nach Bedarf',
};
export interface DeliveryAddressDTO {
label: string;
address: AddressDTO;
contactPerson: string | null;
deliveryNotes: string | null;
}
export interface ContractLineItemDTO {
articleId: string;
agreedPrice: number;
agreedQuantity: number | null;
unit: string | null;
}
export interface FrameContractDTO {
id: string;
validFrom: string | null;
validUntil: string | null;
deliveryRhythm: DeliveryRhythm;
lineItems: ContractLineItemDTO[];
}
export interface CustomerDTO {
id: string;
name: string;
type: CustomerType;
status: CustomerStatus;
billingAddress: AddressDTO;
contactInfo: ContactInfoDTO;
paymentTerms: PaymentTermsDTO | null;
deliveryAddresses: DeliveryAddressDTO[];
frameContract: FrameContractDTO | null;
preferences: CustomerPreference[];
createdAt: string;
updatedAt: string;
}
export interface CreateCustomerRequest {
name: string;
type: CustomerType;
phone: string;
street: string;
houseNumber: string;
postalCode: string;
city: string;
country: string;
email?: string;
contactPerson?: string;
paymentDueDays?: number;
paymentDescription?: string;
}
export interface UpdateCustomerRequest {
name?: string;
phone?: string;
email?: string | null;
contactPerson?: string | null;
street?: string;
houseNumber?: string;
postalCode?: string;
city?: string;
country?: string;
paymentDueDays?: number | null;
paymentDescription?: string | null;
}
export interface AddDeliveryAddressRequest {
label: string;
street: string;
houseNumber: string;
postalCode: string;
city: string;
country: string;
contactPerson?: string;
deliveryNotes?: string;
}
export interface SetFrameContractLineItem {
articleId: string;
agreedPrice: number;
agreedQuantity?: number;
unit?: string;
}
export interface SetFrameContractRequest {
validFrom?: string;
validUntil?: string;
rhythm: DeliveryRhythm;
lineItems: SetFrameContractLineItem[];
}
// ── Backend response shapes (domain objects with nested VOs) ─────────────────
interface BackendPaymentTerms {
paymentDueDays: number;
description: string | null; // Note: backend field is "description", not "paymentDescription"
}
interface BackendContractLineItem {
articleId: { value: string };
agreedPrice: { amount: number; currency: string };
agreedQuantity: number | null;
unit: string | null;
}
interface BackendFrameContract {
id: { value: string };
validFrom: string | null;
validUntil: string | null;
deliveryRhythm: DeliveryRhythm;
lineItems: BackendContractLineItem[];
}
interface BackendCustomer {
id: { value: string };
name: { value: string };
type: CustomerType;
status: CustomerStatus;
billingAddress: AddressDTO;
contactInfo: ContactInfoDTO;
paymentTerms: BackendPaymentTerms | null;
deliveryAddresses: DeliveryAddressDTO[]; // DeliveryAddress is a record → matches DTO shape
frameContract: BackendFrameContract | null;
preferences: CustomerPreference[];
createdAt: string;
updatedAt: string;
}
function mapLineItem(bli: BackendContractLineItem): ContractLineItemDTO {
return {
articleId: bli.articleId.value,
agreedPrice: bli.agreedPrice.amount,
agreedQuantity: bli.agreedQuantity,
unit: bli.unit,
};
}
function mapFrameContract(bfc: BackendFrameContract): FrameContractDTO {
return {
id: bfc.id.value,
validFrom: bfc.validFrom,
validUntil: bfc.validUntil,
deliveryRhythm: bfc.deliveryRhythm,
lineItems: bfc.lineItems.map(mapLineItem),
};
}
function mapCustomer(bc: BackendCustomer): CustomerDTO {
return {
id: bc.id.value,
name: bc.name.value,
type: bc.type,
status: bc.status,
billingAddress: bc.billingAddress,
contactInfo: bc.contactInfo,
paymentTerms: bc.paymentTerms
? {
paymentDueDays: bc.paymentTerms.paymentDueDays,
paymentDescription: bc.paymentTerms.description,
}
: null,
deliveryAddresses: bc.deliveryAddresses,
frameContract: bc.frameContract ? mapFrameContract(bc.frameContract) : null,
preferences: bc.preferences,
createdAt: bc.createdAt,
updatedAt: bc.updatedAt,
};
}
// ── Resource factory ─────────────────────────────────────────────────────────
export function createCustomersResource(client: AxiosInstance) {
return {
async list(): Promise<CustomerDTO[]> {
const res = await client.get<BackendCustomer[]>('/api/customers');
return res.data.map(mapCustomer);
},
async getById(id: string): Promise<CustomerDTO> {
const res = await client.get<BackendCustomer>(`/api/customers/${id}`);
return mapCustomer(res.data);
},
async create(request: CreateCustomerRequest): Promise<CustomerDTO> {
const res = await client.post<BackendCustomer>('/api/customers', request);
return mapCustomer(res.data);
},
async update(id: string, request: UpdateCustomerRequest): Promise<CustomerDTO> {
const res = await client.put<BackendCustomer>(`/api/customers/${id}`, request);
return mapCustomer(res.data);
},
async activate(id: string): Promise<CustomerDTO> {
const res = await client.post<BackendCustomer>(`/api/customers/${id}/activate`);
return mapCustomer(res.data);
},
async deactivate(id: string): Promise<CustomerDTO> {
const res = await client.post<BackendCustomer>(`/api/customers/${id}/deactivate`);
return mapCustomer(res.data);
},
async addDeliveryAddress(id: string, request: AddDeliveryAddressRequest): Promise<CustomerDTO> {
const res = await client.post<BackendCustomer>(
`/api/customers/${id}/delivery-addresses`,
request,
);
return mapCustomer(res.data);
},
// Returns 204 No Content → re-fetch customer
async removeDeliveryAddress(id: string, label: string): Promise<CustomerDTO> {
await client.delete(`/api/customers/${id}/delivery-addresses/${encodeURIComponent(label)}`);
const res = await client.get<BackendCustomer>(`/api/customers/${id}`);
return mapCustomer(res.data);
},
async setFrameContract(id: string, request: SetFrameContractRequest): Promise<CustomerDTO> {
const res = await client.put<BackendCustomer>(
`/api/customers/${id}/frame-contract`,
request,
);
return mapCustomer(res.data);
},
// Returns 204 No Content → re-fetch customer
async removeFrameContract(id: string): Promise<CustomerDTO> {
await client.delete(`/api/customers/${id}/frame-contract`);
const res = await client.get<BackendCustomer>(`/api/customers/${id}`);
return mapCustomer(res.data);
},
async setPreferences(id: string, preferences: CustomerPreference[]): Promise<CustomerDTO> {
const res = await client.put<BackendCustomer>(`/api/customers/${id}/preferences`, {
preferences,
});
return mapCustomer(res.data);
},
};
}
export type CustomersResource = ReturnType<typeof createCustomersResource>;