1
0
Fork 0
mirror of https://github.com/s-frick/effigenix.git synced 2026-03-28 10:09:35 +01:00
effigenix/frontend/apps/cli/src/components/inventory/StorageLocationDetailScreen.tsx
Sebastian Frick c26d72fbe7 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).
2026-02-19 13:54:29 +01:00

144 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useCallback, useEffect, useState } from 'react';
import { Box, Text, useInput } from 'ink';
import type { StorageLocationDTO, StorageType } from '@effigenix/api-client';
import { STORAGE_TYPE_LABELS } from '@effigenix/api-client';
import { useNavigation } from '../../state/navigation-context.js';
import { useStorageLocations } from '../../hooks/useStorageLocations.js';
import { LoadingSpinner } from '../shared/LoadingSpinner.js';
import { ErrorDisplay } from '../shared/ErrorDisplay.js';
import { SuccessDisplay } from '../shared/SuccessDisplay.js';
import { ConfirmDialog } from '../shared/ConfirmDialog.js';
import { client } from '../../utils/api-client.js';
type MenuAction = 'toggle-status' | 'back';
type Mode = 'menu' | 'confirm-status';
const MENU_ITEMS: { id: MenuAction; label: (loc: StorageLocationDTO) => string }[] = [
{ id: 'toggle-status', label: (loc) => loc.active ? '[Deaktivieren]' : '[Aktivieren]' },
{ id: 'back', label: () => '[Zurück]' },
];
function errorMessage(err: unknown): string {
return err instanceof Error ? err.message : 'Unbekannter Fehler';
}
export function StorageLocationDetailScreen() {
const { params, back } = useNavigation();
const storageLocationId = params['storageLocationId'] ?? '';
const { activateStorageLocation, deactivateStorageLocation } = useStorageLocations();
const [location, setLocation] = useState<StorageLocationDTO | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [selectedAction, setSelectedAction] = useState(0);
const [mode, setMode] = useState<Mode>('menu');
const [actionLoading, setActionLoading] = useState(false);
const [successMessage, setSuccessMessage] = useState<string | null>(null);
const loadLocation = useCallback(() => {
setLoading(true);
setError(null);
client.storageLocations.getById(storageLocationId)
.then((loc) => { setLocation(loc); setLoading(false); })
.catch((err: unknown) => { setError(errorMessage(err)); setLoading(false); });
}, [storageLocationId]);
useEffect(() => { if (storageLocationId) loadLocation(); }, [loadLocation, storageLocationId]);
useInput((_input, key) => {
if (loading || actionLoading) return;
if (mode !== 'menu') return;
if (key.upArrow) setSelectedAction((i) => Math.max(0, i - 1));
if (key.downArrow) setSelectedAction((i) => Math.min(MENU_ITEMS.length - 1, i + 1));
if (key.return) void handleAction();
if (key.backspace || key.escape) back();
});
const handleAction = async () => {
if (!location) return;
const item = MENU_ITEMS[selectedAction];
if (!item) return;
switch (item.id) {
case 'toggle-status':
setMode('confirm-status');
break;
case 'back':
back();
break;
}
};
const handleToggleStatus = useCallback(async () => {
if (!location) return;
setMode('menu');
setActionLoading(true);
const fn = location.active ? deactivateStorageLocation : activateStorageLocation;
const updated = await fn(location.id);
setActionLoading(false);
if (updated) {
setLocation(updated);
setSuccessMessage(location.active ? 'Lagerort deaktiviert.' : 'Lagerort aktiviert.');
}
}, [location, activateStorageLocation, deactivateStorageLocation]);
if (loading) return <LoadingSpinner label="Lade Lagerort..." />;
if (error && !location) return <ErrorDisplay message={error} onDismiss={back} />;
if (!location) return <Text color="red">Lagerort nicht gefunden.</Text>;
const statusColor = location.active ? 'green' : 'red';
const typeName = STORAGE_TYPE_LABELS[location.storageType as StorageType] ?? location.storageType;
return (
<Box flexDirection="column" gap={1}>
<Text color="cyan" bold>Lagerort: {location.name}</Text>
{error && <ErrorDisplay message={error} onDismiss={() => setError(null)} />}
{successMessage && <SuccessDisplay message={successMessage} onDismiss={() => setSuccessMessage(null)} />}
<Box flexDirection="column" borderStyle="round" borderColor="gray" paddingX={2} paddingY={1}>
<Box gap={2}>
<Text color="gray">Status:</Text>
<Text color={statusColor} bold>{location.active ? 'AKTIV' : 'INAKTIV'}</Text>
</Box>
<Box gap={2}>
<Text color="gray">Lagertyp:</Text>
<Text>{typeName}</Text>
</Box>
{location.temperatureRange && (
<Box gap={2}>
<Text color="gray">Temperatur:</Text>
<Text>{location.temperatureRange.minTemperature}°C {location.temperatureRange.maxTemperature}°C</Text>
</Box>
)}
</Box>
{mode === 'confirm-status' && (
<ConfirmDialog
message={location.active ? 'Lagerort deaktivieren?' : 'Lagerort aktivieren?'}
onConfirm={() => void handleToggleStatus()}
onCancel={() => setMode('menu')}
/>
)}
{mode === 'menu' && (
<Box flexDirection="column">
<Text color="gray" bold>Aktionen:</Text>
{actionLoading && <LoadingSpinner label="Aktion wird ausgeführt..." />}
{!actionLoading && MENU_ITEMS.map((item, index) => (
<Box key={item.id}>
<Text color={index === selectedAction ? 'cyan' : 'white'}>
{index === selectedAction ? '▶ ' : ' '}{item.label(location)}
</Text>
</Box>
))}
</Box>
)}
<Box marginTop={1}>
<Text color="gray" dimColor> navigieren · Enter ausführen · Backspace Zurück</Text>
</Box>
</Box>
);
}