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,98 @@
import { createContext, useContext, useMemo, useState, useCallback, type ReactNode } from 'react';
import { createEffigenixClient, type EffigenixClient, type TokenProvider } from '@effigenix/api-client';
import { fetch as tauriFetch } from '@tauri-apps/plugin-http';
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8080';
/**
* Axios adapter that delegates to Tauri's HTTP plugin.
* Bypasses CORS restrictions on mobile webviews.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const tauriAdapter = async (config: any) => {
const url = new URL(config.url || '', config.baseURL);
const headers: Record<string, string> = {};
if (config.headers) {
const raw = typeof config.headers.toJSON === 'function' ? config.headers.toJSON(false) : config.headers;
for (const [key, value] of Object.entries(raw)) {
if (value != null) headers[key] = String(value);
}
}
const response = await tauriFetch(url.toString(), {
method: (config.method || 'GET').toUpperCase(),
headers,
body: config.data != null
? (typeof config.data === 'string' ? config.data : JSON.stringify(config.data))
: undefined,
});
let data: unknown;
const contentType = response.headers.get('content-type') || '';
if (contentType.includes('application/json')) {
data = await response.json();
} else {
data = await response.text();
}
return {
data,
status: response.status,
statusText: response.statusText,
headers: Object.fromEntries(response.headers.entries()),
config,
};
};
interface ApiState {
client: EffigenixClient;
login: (username: string, password: string) => Promise<void>;
isAuthenticated: boolean;
}
const ApiContext = createContext<ApiState | null>(null);
export function ApiProvider({ children }: { children: ReactNode }) {
const [tokens, setTokens] = useState<{ access: string; refresh: string } | null>(null);
const tokenProvider = useMemo<TokenProvider>(() => ({
getAccessToken: async () => tokens?.access ?? null,
getRefreshToken: async () => tokens?.refresh ?? null,
saveTokens: async (accessToken, refreshToken) => {
setTokens({ access: accessToken, refresh: refreshToken });
},
clearTokens: async () => setTokens(null),
}), [tokens]);
const client = useMemo(() => createEffigenixClient(tokenProvider, {
baseUrl: API_BASE,
adapter: tauriAdapter,
}), [tokenProvider]);
const login = useCallback(async (username: string, password: string) => {
const res = await tauriFetch(`${API_BASE}/api/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
});
if (!res.ok) {
const text = await res.text();
throw new Error(`${res.status}: ${text}`);
}
const data = await res.json();
setTokens({ access: data.accessToken, refresh: data.refreshToken });
}, []);
return (
<ApiContext.Provider value={{ client, login, isAuthenticated: tokens !== null }}>
{children}
</ApiContext.Provider>
);
}
export function useApi() {
const ctx = useContext(ApiContext);
if (!ctx) throw new Error('useApi must be used within ApiProvider');
return ctx;
}