1
0
Fork 0
mirror of https://github.com/s-frick/effigenix.git synced 2026-03-28 11:59:35 +01:00

feat: TUI-Screens für Inventar und Produktion + API-Client Typ-Migration

Neue TUI-Features:
- Inventar: Lageorte auflisten, anlegen, bearbeiten, (de-)aktivieren
- Produktion: Rezepte auflisten, anlegen, Detail-Ansicht
- Navigation erweitert (Hauptmenü, Routing)

API-Client auf generierte OpenAPI-Typen umgestellt:
- 6 neue Alias-Dateien in @effigenix/types (supplier, category, article,
  customer, inventory, production)
- api-client Re-Exports direkt von @effigenix/types statt via Resources
- Backend: @Schema(requiredProperties) auf 16 Response-Records
- Backend: OpenApiCustomizer für application-layer DTOs (UserDTO, RoleDTO)

Hinweis: Backend-Endpoints für GET /api/recipes und
GET /api/inventory/storage-locations/{id} fehlen noch (separate Issues).
This commit is contained in:
Sebastian Frick 2026-02-19 13:45:35 +01:00
parent bee3f28b5f
commit c26d72fbe7
48 changed files with 2090 additions and 474 deletions

View file

@ -22,6 +22,8 @@ export { createCategoriesResource } from './resources/categories.js';
export { createSuppliersResource } from './resources/suppliers.js';
export { createArticlesResource } from './resources/articles.js';
export { createCustomersResource } from './resources/customers.js';
export { createStorageLocationsResource } from './resources/storage-locations.js';
export { createRecipesResource } from './resources/recipes.js';
export {
ApiError,
AuthenticationError,
@ -29,12 +31,15 @@ export {
RefreshTokenExpiredError,
} from './errors.js';
export type { ValidationErrorDetail } from './errors.js';
// Auth types (no generated alias, stay in resource)
export type {
LoginRequest,
LoginResponse,
RefreshTokenRequest,
AuthResource,
} from './resources/auth.js';
// Types from @effigenix/types (generated OpenAPI aliases)
export type {
UserDTO,
RoleDTO,
@ -42,18 +47,7 @@ export type {
UpdateUserRequest,
ChangePasswordRequest,
AssignRoleRequest,
UsersResource,
} from './resources/users.js';
export type { RolesResource } from './resources/roles.js';
export type {
ProductCategoryDTO,
CreateCategoryRequest,
UpdateCategoryRequest,
CategoriesResource,
} from './resources/categories.js';
export type {
SupplierDTO,
SupplierStatus,
AddressDTO,
ContactInfoDTO,
PaymentTermsDTO,
@ -64,36 +58,57 @@ export type {
RateSupplierRequest,
AddCertificateRequest,
RemoveCertificateRequest,
SuppliersResource,
} from './resources/suppliers.js';
export type {
ProductCategoryDTO,
CreateCategoryRequest,
UpdateCategoryRequest,
ArticleDTO,
ArticleStatus,
SalesUnitDTO,
Unit,
PriceModel,
CreateArticleRequest,
UpdateArticleRequest,
AddSalesUnitRequest,
UpdateSalesUnitPriceRequest,
ArticlesResource,
} from './resources/articles.js';
export { UNIT_LABELS, PRICE_MODEL_LABELS } from './resources/articles.js';
export type {
CustomerDTO,
CustomerType,
CustomerStatus,
CustomerPreference,
DeliveryRhythm,
DeliveryAddressDTO,
FrameContractDTO,
ContractLineItemDTO,
CreateCustomerRequest,
UpdateCustomerRequest,
AddDeliveryAddressRequest,
SetFrameContractLineItem,
SetFrameContractRequest,
StorageLocationDTO,
TemperatureRangeDTO,
CreateStorageLocationRequest,
UpdateStorageLocationRequest,
RecipeDTO,
IngredientDTO,
CreateRecipeRequest,
AddRecipeIngredientRequest,
} from '@effigenix/types';
// Resource types (runtime, stay in resource files)
export type { UsersResource } from './resources/users.js';
export type { RolesResource } from './resources/roles.js';
export type { CategoriesResource } from './resources/categories.js';
export type { SuppliersResource, SupplierStatus } from './resources/suppliers.js';
export type { ArticlesResource, ArticleStatus, Unit, PriceModel } from './resources/articles.js';
export { UNIT_LABELS, PRICE_MODEL_LABELS } from './resources/articles.js';
export type {
CustomersResource,
CustomerType,
CustomerStatus,
CustomerPreference,
DeliveryRhythm,
} from './resources/customers.js';
export { CUSTOMER_PREFERENCE_LABELS, DELIVERY_RHYTHM_LABELS } from './resources/customers.js';
export type {
StorageLocationsResource,
StorageType,
StorageLocationFilter,
} from './resources/storage-locations.js';
export { STORAGE_TYPE_LABELS } from './resources/storage-locations.js';
export type { RecipesResource, RecipeType, RecipeStatus } from './resources/recipes.js';
export { RECIPE_TYPE_LABELS } from './resources/recipes.js';
import { createApiClient } from './client.js';
import { createAuthResource } from './resources/auth.js';
@ -103,6 +118,8 @@ import { createCategoriesResource } from './resources/categories.js';
import { createSuppliersResource } from './resources/suppliers.js';
import { createArticlesResource } from './resources/articles.js';
import { createCustomersResource } from './resources/customers.js';
import { createStorageLocationsResource } from './resources/storage-locations.js';
import { createRecipesResource } from './resources/recipes.js';
import type { TokenProvider } from './token-provider.js';
import type { ApiConfig } from '@effigenix/config';
@ -124,6 +141,8 @@ export function createEffigenixClient(
suppliers: createSuppliersResource(axiosClient),
articles: createArticlesResource(axiosClient),
customers: createCustomersResource(axiosClient),
storageLocations: createStorageLocationsResource(axiosClient),
recipes: createRecipesResource(axiosClient),
};
}

View file

@ -10,6 +10,14 @@
*/
import type { AxiosInstance } from 'axios';
import type {
ArticleDTO,
SalesUnitDTO,
CreateArticleRequest,
UpdateArticleRequest,
AddSalesUnitRequest,
UpdateSalesUnitPriceRequest,
} from '@effigenix/types';
export type Unit = 'PIECE_FIXED' | 'KG' | 'HUNDRED_GRAM' | 'PIECE_VARIABLE';
export type PriceModel = 'FIXED' | 'WEIGHT_BASED';
@ -27,48 +35,14 @@ export const PRICE_MODEL_LABELS: Record<PriceModel, string> = {
WEIGHT_BASED: 'Gewichtsbasiert',
};
export interface SalesUnitDTO {
id: string;
unit: Unit;
priceModel: PriceModel;
price: number;
}
export interface ArticleDTO {
id: string;
name: string;
articleNumber: string;
categoryId: string;
salesUnits: SalesUnitDTO[];
status: ArticleStatus;
supplierIds: string[];
createdAt: string;
updatedAt: string;
}
export interface CreateArticleRequest {
name: string;
articleNumber: string;
categoryId: string;
unit: Unit;
priceModel: PriceModel;
price: number;
}
export interface UpdateArticleRequest {
name?: string;
categoryId?: string;
}
export interface AddSalesUnitRequest {
unit: Unit;
priceModel: PriceModel;
price: number;
}
export interface UpdateSalesUnitPriceRequest {
price: number;
}
export type {
ArticleDTO,
SalesUnitDTO,
CreateArticleRequest,
UpdateArticleRequest,
AddSalesUnitRequest,
UpdateSalesUnitPriceRequest,
};
// ── Resource factory ─────────────────────────────────────────────────────────

View file

@ -7,22 +7,17 @@
*/
import type { AxiosInstance } from 'axios';
import type {
ProductCategoryDTO,
CreateCategoryRequest,
UpdateCategoryRequest,
} from '@effigenix/types';
export interface ProductCategoryDTO {
id: string;
name: string;
description: string | null;
}
export interface CreateCategoryRequest {
name: string;
description?: string;
}
export interface UpdateCategoryRequest {
name?: string;
description?: string | null;
}
export type {
ProductCategoryDTO,
CreateCategoryRequest,
UpdateCategoryRequest,
};
// ── Resource factory ─────────────────────────────────────────────────────────

View file

@ -12,7 +12,16 @@
*/
import type { AxiosInstance } from 'axios';
import type { AddressDTO, ContactInfoDTO, PaymentTermsDTO } from './suppliers.js';
import type {
CustomerDTO,
DeliveryAddressDTO,
FrameContractDTO,
ContractLineItemDTO,
CreateCustomerRequest,
UpdateCustomerRequest,
AddDeliveryAddressRequest,
SetFrameContractRequest,
} from '@effigenix/types';
export type CustomerType = 'B2B' | 'B2C';
export type CustomerStatus = 'ACTIVE' | 'INACTIVE';
@ -44,96 +53,16 @@ export const DELIVERY_RHYTHM_LABELS: Record<DeliveryRhythm, string> = {
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[];
}
export type {
CustomerDTO,
DeliveryAddressDTO,
FrameContractDTO,
ContractLineItemDTO,
CreateCustomerRequest,
UpdateCustomerRequest,
AddDeliveryAddressRequest,
SetFrameContractRequest,
};
// ── Resource factory ─────────────────────────────────────────────────────────

View file

@ -0,0 +1,66 @@
/**
* Recipes resource Production BC.
* Endpoints: POST /api/recipes,
* POST /api/recipes/{id}/ingredients,
* DELETE /api/recipes/{id}/ingredients/{ingredientId}
*/
import type { AxiosInstance } from 'axios';
import type {
RecipeDTO,
IngredientDTO,
CreateRecipeRequest,
AddRecipeIngredientRequest,
} from '@effigenix/types';
export type RecipeType = 'RAW_MATERIAL' | 'INTERMEDIATE' | 'FINISHED_PRODUCT';
export type RecipeStatus = 'DRAFT' | 'ACTIVE' | 'ARCHIVED';
export const RECIPE_TYPE_LABELS: Record<RecipeType, string> = {
RAW_MATERIAL: 'Rohstoff',
INTERMEDIATE: 'Halbfabrikat',
FINISHED_PRODUCT: 'Fertigprodukt',
};
export type {
RecipeDTO,
IngredientDTO,
CreateRecipeRequest,
AddRecipeIngredientRequest,
};
// ── Resource factory ─────────────────────────────────────────────────────────
const BASE = '/api/recipes';
export function createRecipesResource(client: AxiosInstance) {
return {
async list(): Promise<RecipeDTO[]> {
const res = await client.get<RecipeDTO[]>(BASE);
return res.data;
},
async getById(id: string): Promise<RecipeDTO> {
const res = await client.get<RecipeDTO>(`${BASE}/${id}`);
return res.data;
},
async create(request: CreateRecipeRequest): Promise<RecipeDTO> {
const res = await client.post<RecipeDTO>(BASE, request);
return res.data;
},
async addIngredient(id: string, request: AddRecipeIngredientRequest): Promise<RecipeDTO> {
const res = await client.post<RecipeDTO>(`${BASE}/${id}/ingredients`, request);
return res.data;
},
async removeIngredient(recipeId: string, ingredientId: string): Promise<RecipeDTO> {
await client.delete(`${BASE}/${recipeId}/ingredients/${ingredientId}`);
const res = await client.get<RecipeDTO>(`${BASE}/${recipeId}`);
return res.data;
},
};
}
export type RecipesResource = ReturnType<typeof createRecipesResource>;

View file

@ -0,0 +1,79 @@
/**
* StorageLocations resource Inventory BC.
* Endpoints: GET/POST /api/inventory/storage-locations,
* PUT /api/inventory/storage-locations/{id},
* PATCH /api/inventory/storage-locations/{id}/activate|deactivate
*/
import type { AxiosInstance } from 'axios';
import type {
StorageLocationDTO,
TemperatureRangeDTO,
CreateStorageLocationRequest,
UpdateStorageLocationRequest,
} from '@effigenix/types';
export type StorageType = 'COLD_ROOM' | 'FREEZER' | 'DRY_STORAGE' | 'DISPLAY_COUNTER' | 'PRODUCTION_AREA';
export const STORAGE_TYPE_LABELS: Record<StorageType, string> = {
COLD_ROOM: 'Kühlraum',
FREEZER: 'Tiefkühler',
DRY_STORAGE: 'Trockenlager',
DISPLAY_COUNTER: 'Vitrine',
PRODUCTION_AREA: 'Produktionsbereich',
};
export type {
StorageLocationDTO,
TemperatureRangeDTO,
CreateStorageLocationRequest,
UpdateStorageLocationRequest,
};
export interface StorageLocationFilter {
storageType?: string;
active?: boolean;
}
// ── Resource factory ─────────────────────────────────────────────────────────
const BASE = '/api/inventory/storage-locations';
export function createStorageLocationsResource(client: AxiosInstance) {
return {
async list(filter?: StorageLocationFilter): Promise<StorageLocationDTO[]> {
const params: Record<string, string> = {};
if (filter?.storageType) params['storageType'] = filter.storageType;
if (filter?.active !== undefined) params['active'] = String(filter.active);
const res = await client.get<StorageLocationDTO[]>(BASE, { params });
return res.data;
},
async getById(id: string): Promise<StorageLocationDTO> {
const res = await client.get<StorageLocationDTO>(`${BASE}/${id}`);
return res.data;
},
async create(request: CreateStorageLocationRequest): Promise<StorageLocationDTO> {
const res = await client.post<StorageLocationDTO>(BASE, request);
return res.data;
},
async update(id: string, request: UpdateStorageLocationRequest): Promise<StorageLocationDTO> {
const res = await client.put<StorageLocationDTO>(`${BASE}/${id}`, request);
return res.data;
},
async activate(id: string): Promise<StorageLocationDTO> {
const res = await client.patch<StorageLocationDTO>(`${BASE}/${id}/activate`);
return res.data;
},
async deactivate(id: string): Promise<StorageLocationDTO> {
const res = await client.patch<StorageLocationDTO>(`${BASE}/${id}/deactivate`);
return res.data;
},
};
}
export type StorageLocationsResource = ReturnType<typeof createStorageLocationsResource>;

View file

@ -10,100 +10,35 @@
*/
import type { AxiosInstance } from 'axios';
export interface AddressDTO {
street: string;
houseNumber: string | null;
postalCode: string;
city: string;
country: string;
}
export interface ContactInfoDTO {
phone: string;
email: string | null;
contactPerson: string | null;
}
export interface PaymentTermsDTO {
paymentDueDays: number;
paymentDescription: string | null;
}
export interface QualityCertificateDTO {
certificateType: string;
issuer: string;
validFrom: string;
validUntil: string;
}
export interface SupplierRatingDTO {
qualityScore: number;
deliveryScore: number;
priceScore: number;
}
import type {
SupplierDTO,
AddressDTO,
ContactInfoDTO,
PaymentTermsDTO,
QualityCertificateDTO,
SupplierRatingDTO,
CreateSupplierRequest,
UpdateSupplierRequest,
RateSupplierRequest,
AddCertificateRequest,
RemoveCertificateRequest,
} from '@effigenix/types';
export type SupplierStatus = 'ACTIVE' | 'INACTIVE';
export interface SupplierDTO {
id: string;
name: string;
status: SupplierStatus;
address: AddressDTO | null;
contactInfo: ContactInfoDTO;
paymentTerms: PaymentTermsDTO | null;
certificates: QualityCertificateDTO[];
rating: SupplierRatingDTO | null;
createdAt: string;
updatedAt: string;
}
export interface CreateSupplierRequest {
name: string;
phone: string;
email?: string;
contactPerson?: string;
street?: string;
houseNumber?: string;
postalCode?: string;
city?: string;
country?: string;
paymentDueDays?: number;
paymentDescription?: string;
}
export interface UpdateSupplierRequest {
name?: string;
phone?: string;
email?: string | null;
contactPerson?: string | null;
street?: string | null;
houseNumber?: string | null;
postalCode?: string | null;
city?: string | null;
country?: string | null;
paymentDueDays?: number | null;
paymentDescription?: string | null;
}
export interface RateSupplierRequest {
qualityScore: number;
deliveryScore: number;
priceScore: number;
}
export interface AddCertificateRequest {
certificateType: string;
issuer: string;
validFrom: string;
validUntil: string;
}
export interface RemoveCertificateRequest {
certificateType: string;
issuer: string;
validFrom: string;
}
export type {
SupplierDTO,
AddressDTO,
ContactInfoDTO,
PaymentTermsDTO,
QualityCertificateDTO,
SupplierRatingDTO,
CreateSupplierRequest,
UpdateSupplierRequest,
RateSupplierRequest,
AddCertificateRequest,
RemoveCertificateRequest,
};
// ── Resource factory ─────────────────────────────────────────────────────────

View file

@ -4,45 +4,23 @@
import type { AxiosInstance } from 'axios';
import { API_PATHS } from '@effigenix/config';
import type {
UserDTO,
RoleDTO,
CreateUserRequest,
UpdateUserRequest,
ChangePasswordRequest,
AssignRoleRequest,
} from '@effigenix/types';
export interface UserDTO {
id: string;
username: string;
email: string;
roles: RoleDTO[];
branchId?: string;
status: 'ACTIVE' | 'LOCKED';
createdAt: string;
lastLogin?: string;
}
export interface RoleDTO {
id: string;
name: string;
permissions: string[];
}
export interface CreateUserRequest {
username: string;
email: string;
password: string;
roleNames: string[];
branchId?: string;
}
export interface UpdateUserRequest {
email?: string;
branchId?: string;
}
export interface ChangePasswordRequest {
currentPassword: string;
newPassword: string;
}
export interface AssignRoleRequest {
roleName: string;
}
export type {
UserDTO,
RoleDTO,
CreateUserRequest,
UpdateUserRequest,
ChangePasswordRequest,
AssignRoleRequest,
};
export function createUsersResource(client: AxiosInstance) {
return {