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

feat(scanner): Mobile Scanner App mit echten Produktionsaufträgen

Scanner-App  Tauri v2 Android App mit
Login, Barcode-Scanner, Consume/Move/Book-Flows und Aufgabenliste.

TasksPage lädt echte RELEASED/IN_PROGRESS Produktionsaufträge via API,
Consume-Flow nutzt den konkreten Auftrag für korrekte Rezept-Skalierung
statt blind den ersten IN_PROGRESS-Auftrag zu nehmen.

Backend: FindStockByBatchId Use Case + REST-Endpoint für Stock-Lookup
per Chargennummer.
This commit is contained in:
Sebastian Frick 2026-03-20 13:58:54 +01:00
parent 72979c9537
commit bf09e3b747
93 changed files with 8977 additions and 60 deletions

View file

@ -0,0 +1,119 @@
import { FlaskConical, ScanLine, Check, PackageSearch } from 'lucide-react';
import { Card, Button } from '@effigenix/ui';
import { PageActions } from '../components/PageActions';
import { PageHeader } from '../components/PageHeader';
import { StepIndicator } from '../components/StepIndicator';
import { useNavigation } from '../navigation';
import { useFlowState } from '../flow-state';
export function ConsumeFlowPage() {
const { navigate, setFlowType } = useNavigation();
const { consumeItems, productionOrder } = useFlowState();
const hasItems = consumeItems.length > 0;
if (!productionOrder) {
navigate('tasks');
return null;
}
function startScan() {
setFlowType('consume');
navigate('scan');
}
return (
<div className="flex-1 flex flex-col animate-fade-in">
<PageHeader
title="Verbrauch erfassen"
subtitle={
<span>
Produktionscharge:{' '}
<span className="font-mono font-medium text-brand-600">{productionOrder.batchNumber ?? ''}</span>
</span>
}
/>
<StepIndicator
steps={[
{ label: 'Prod.-Charge', state: 'done' },
{ label: 'Materialien', state: hasItems ? 'done' : 'active' },
{ label: 'Bestätigen', state: hasItems ? 'active' : 'pending' },
]}
/>
{/* Production batch */}
<div className="px-5 mb-2">
<Card className="p-2.5 flex items-center gap-3 border-brand-200 bg-brand-50">
<div className="w-8 h-8 rounded-lg bg-brand-100 flex items-center justify-center">
<FlaskConical className="w-4 h-4 text-brand-600" />
</div>
<div className="flex-1 min-w-0">
<div className="text-sm font-semibold text-warm-800">
{productionOrder.batchNumber ?? ''} · {productionOrder.articleName}
</div>
<div className="text-xs text-warm-500">
Soll: {productionOrder.plannedQuantity} {productionOrder.plannedQuantityUnit} · In Produktion
</div>
</div>
</Card>
</div>
{/* Scanned materials scrollable area */}
<div className="px-5 flex-1 min-h-0 overflow-y-auto space-y-2">
<h2 className="text-xs font-bold text-warm-500 uppercase tracking-wider mb-2">
Gescannte Materialien ({consumeItems.length})
</h2>
{consumeItems.length > 0 ? (
<div className="space-y-2">
{consumeItems.map((item, i) => (
<Card key={i} className="p-2.5 flex items-center gap-3 animate-fade-in">
<div className="w-2 h-10 rounded-full bg-brand-400 shrink-0" />
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-warm-800">{item.name}</div>
<div className="text-xs text-warm-500">
{item.batch} · {item.location}
</div>
</div>
<div className="text-right shrink-0">
<div className="font-mono font-semibold text-sm text-warm-800">
{item.quantity} {item.unit}
</div>
{item.required != null && (
<div className="text-2xs text-warm-400">
von {item.required} {item.requiredUnit ?? item.unit}
</div>
)}
</div>
</Card>
))}
</div>
) : (
<Card className="p-5 text-center">
<div className="inline-flex items-center justify-center w-11 h-11 rounded-xl bg-warm-100 mb-2">
<PackageSearch className="w-5 h-5 text-warm-400" />
</div>
<p className="text-sm text-warm-500 mb-0.5">Noch keine Materialien erfasst</p>
<p className="text-xs text-warm-400">
Chargen-Etiketten der Eingangsmaterialien scannen.
</p>
</Card>
)}
<Button variant="secondary" size='lg' className="w-full" onClick={startScan}>
<ScanLine className="w-4 h-4" />
{consumeItems.length === 0 ? 'Erste Zutat scannen' : 'Nächste Zutat scannen'}
</Button>
</div>
<PageActions>
{hasItems && (
<Button variant="primary" size='lg' className="w-full" onClick={() => navigate('consume-confirm')}>
<Check className="w-5 h-5" />
{consumeItems.length} Verbrauch{consumeItems.length !== 1 ? 'e' : ''} buchen
</Button>
)}
</PageActions>
</div>
);
}