Schritt 1: Fundament der Lebensmittel-Lagerverwaltung (Pantry)
Backend (FastAPI + PostgreSQL): Chargen mit MHD, FEFO-Auslagern, Einheiten-Umrechnung (Stueck/g/ml + Packungen), Open-Food-Facts-Lookup mit lokalem Fallback, JWT-Auth mit Rollen (Admin/Nutzer), erster Admin beim Setup, Einkaufsliste, Ablaufwarnung, Lagerorte, pytest fuer FEFO. Web-UI (React/Vite): Login, Dashboard, Ein-/Auslagern, Produkte, Lagerorte, Benutzerverwaltung, Einkaufsliste - rollenabhaengig. Deploy: docker-compose + install.sh (Docker-Autoinstall, Secrets), README und Roadmap fuer Schritt 2 (iOS) und Schritt 3. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
105
web/src/api.js
Normal file
105
web/src/api.js
Normal file
@@ -0,0 +1,105 @@
|
||||
// Zentraler API-Client. In Produktion und Dev läuft alles über /api
|
||||
// (nginx bzw. Vite-Proxy leiten an das FastAPI-Backend weiter).
|
||||
|
||||
const API_BASE = "/api";
|
||||
|
||||
const TOKEN_KEY = "pantry_token";
|
||||
|
||||
export function getToken() {
|
||||
return localStorage.getItem(TOKEN_KEY);
|
||||
}
|
||||
export function setToken(token) {
|
||||
if (token) localStorage.setItem(TOKEN_KEY, token);
|
||||
else localStorage.removeItem(TOKEN_KEY);
|
||||
}
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(message, status) {
|
||||
super(message);
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
async function request(path, { method = "GET", body, form } = {}) {
|
||||
const headers = {};
|
||||
const token = getToken();
|
||||
if (token) headers["Authorization"] = `Bearer ${token}`;
|
||||
|
||||
let payload;
|
||||
if (form) {
|
||||
headers["Content-Type"] = "application/x-www-form-urlencoded";
|
||||
payload = new URLSearchParams(form).toString();
|
||||
} else if (body !== undefined) {
|
||||
headers["Content-Type"] = "application/json";
|
||||
payload = JSON.stringify(body);
|
||||
}
|
||||
|
||||
const resp = await fetch(`${API_BASE}${path}`, { method, headers, body: payload });
|
||||
|
||||
if (resp.status === 204) return null;
|
||||
|
||||
let data = null;
|
||||
const text = await resp.text();
|
||||
if (text) {
|
||||
try {
|
||||
data = JSON.parse(text);
|
||||
} catch {
|
||||
data = text;
|
||||
}
|
||||
}
|
||||
|
||||
if (!resp.ok) {
|
||||
const detail =
|
||||
(data && data.detail) ||
|
||||
(typeof data === "string" ? data : null) ||
|
||||
`Fehler ${resp.status}`;
|
||||
throw new ApiError(
|
||||
Array.isArray(detail) ? detail.map((d) => d.msg).join(", ") : detail,
|
||||
resp.status
|
||||
);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
export const api = {
|
||||
login: (username, password) =>
|
||||
request("/auth/login", { method: "POST", form: { username, password } }),
|
||||
me: () => request("/auth/me"),
|
||||
|
||||
// Produkte
|
||||
listProducts: (q) => request(`/products${q ? `?q=${encodeURIComponent(q)}` : ""}`),
|
||||
getProduct: (id) => request(`/products/${id}`),
|
||||
lookup: (barcode) => request(`/products/lookup?barcode=${encodeURIComponent(barcode)}`),
|
||||
createProduct: (body) => request("/products", { method: "POST", body }),
|
||||
updateProduct: (id, body) => request(`/products/${id}`, { method: "PATCH", body }),
|
||||
deleteProduct: (id) => request(`/products/${id}`, { method: "DELETE" }),
|
||||
|
||||
// Bestand
|
||||
checkIn: (body) => request("/stock/checkin", { method: "POST", body }),
|
||||
checkOut: (body) => request("/stock/checkout", { method: "POST", body }),
|
||||
listLots: (productId) =>
|
||||
request(`/lots${productId ? `?product_id=${productId}` : ""}`),
|
||||
|
||||
// Views
|
||||
shoppingList: () => request("/shopping-list"),
|
||||
expiring: (days) => request(`/expiring${days != null ? `?days=${days}` : ""}`),
|
||||
|
||||
// Stammdaten
|
||||
listLocations: () => request("/locations"),
|
||||
createLocation: (body) => request("/locations", { method: "POST", body }),
|
||||
deleteLocation: (id) => request(`/locations/${id}`, { method: "DELETE" }),
|
||||
|
||||
listGroups: () => request("/groups"),
|
||||
createGroup: (body) => request("/groups", { method: "POST", body }),
|
||||
|
||||
// Benutzer
|
||||
listUsers: () => request("/users"),
|
||||
createUser: (body) => request("/users", { method: "POST", body }),
|
||||
updateUser: (id, body) => request(`/users/${id}`, { method: "PATCH", body }),
|
||||
deleteUser: (id) => request(`/users/${id}`, { method: "DELETE" }),
|
||||
|
||||
// Einstellungen
|
||||
listSettings: () => request("/settings"),
|
||||
setSetting: (key, value) =>
|
||||
request(`/settings/${key}?value=${encodeURIComponent(value)}`, { method: "PUT" }),
|
||||
};
|
||||
Reference in New Issue
Block a user