import { Fragment, useEffect, useState } from "react"; import QRCode from "qrcode"; import { api, authorizedObjectUrl } from "../api"; import { useConfirm } from "../confirm"; import { useToast } from "../toast"; import Icon from "./Icon"; import { REASONS } from "./ObjektBestand"; const reasonLabel = (v) => REASONS.find((r) => r.value === v)?.label || v; /** Kleines QR-Bild für einen Wert (asynchron erzeugt). */ function QrImg({ text, size = 60 }) { const [url, setUrl] = useState(null); useEffect(() => { let ok = true; QRCode.toDataURL(text, { margin: 1, width: size * 2 }) .then((u) => { if (ok) setUrl(u); }) .catch(() => {}); return () => { ok = false; }; }, [text, size]); return url ? QR : ; } const EMPTY_ADD = { count: 1, location_id: "", shop_id: "", acquired_on: "", warranty_until: "", note: "", }; /** * Einzelstücke eines Gegenstands: jedes physische Stück mit eigener UID/QR und * eigenen Angaben (Lagerort, gekauft am, Garantie, gekauft bei, Notiz). */ export default function Einzelstuecke({ product, locations, shops, isAdmin, onChanged, onError }) { const confirm = useConfirm(); const toast = useToast(); const [items, setItems] = useState([]); const [addForm, setAddForm] = useState(EMPTY_ADD); const [remove, setRemove] = useState(null); // { item, reason, note } | null const [showAdd, setShowAdd] = useState(false); // Anlege-Maske ein-/ausklappen const [suggestion, setSuggestion] = useState(null); // { itemId, date } aus PDF-Analyse const origin = window.location.origin; async function load() { try { setItems(await api.listItems(product.id)); } catch (err) { onError(err.message); } } useEffect(() => { load(); /* eslint-disable-next-line */ }, [product.id]); async function nachAktion() { await load(); if (onChanged) await onChanged(); } async function add(e) { e.preventDefault(); try { await api.createItems(product.id, { count: Number(addForm.count) || 1, location_id: addForm.location_id === "" ? null : Number(addForm.location_id), shop_id: addForm.shop_id === "" ? null : Number(addForm.shop_id), acquired_on: addForm.acquired_on || null, warranty_until: addForm.warranty_until || null, note: addForm.note || null, }); setAddForm({ ...EMPTY_ADD, location_id: addForm.location_id, shop_id: addForm.shop_id }); await nachAktion(); toast("Einzelstück(e) angelegt."); } catch (err) { onError(err.message); } } // Kaufpreis in Hauptwährungseinheit eingeben, als Rappen/Cent speichern. function savePrice(item, value) { const roh = value.trim().replace(",", "."); const cents = roh === "" ? null : Math.round(parseFloat(roh) * 100); if (cents !== null && Number.isNaN(cents)) return; if (cents === (item.price_cents ?? null)) return; patchItem(item, { price_cents: cents, currency: cents == null ? null : (item.currency || "CHF") }); } async function uploadDoc(item, file) { try { const res = await api.uploadItemDocument(item.id, file); await load(); if (res && (res.suggested_warranty_until || res.suggested_price_cents != null)) { setSuggestion({ itemId: item.id, date: res.suggested_warranty_until || null, priceCents: res.suggested_price_cents ?? null, }); } toast("Beleg hochgeladen."); } catch (err) { onError(err.message); } } async function deleteDoc(item, doc) { try { await api.deleteItemDocument(item.id, doc.id); await load(); } catch (err) { onError(err.message); } } async function viewDoc(item, doc) { try { const url = await authorizedObjectUrl(`/items/${item.id}/documents/${doc.id}`); if (url) window.open(url, "_blank", "noopener"); } catch (err) { onError(err.message); } } async function patchItem(item, body) { try { await api.updateItem(item.id, body); await load(); } catch (err) { onError(err.message); } } async function doRemove(e) { e.preventDefault(); try { await api.removeItem(remove.item.id, { reason: remove.reason, note: remove.note || null }); setRemove(null); await nachAktion(); toast("Einzelstück entfernt."); } catch (err) { onError(err.message); } } async function loeschen(item) { const ok = await confirm({ title: `Einzelstück ${item.uid} löschen?`, message: "Ohne Grund – nur als Korrektur. Für „kaputt/verloren“ bitte „Entfernen“.", confirmLabel: "Löschen", danger: true, }); if (!ok) return; try { await api.deleteItem(item.id); await nachAktion(); } catch (err) { onError(err.message); } } async function druckeEtiketten() { const labels = await Promise.all(items.map(async (it) => { const url = await QRCode.toDataURL(`${origin}/i/${it.uid}`, { margin: 1, width: 260 }); const name = (product.name || "").replace(/[<>&]/g, ""); return `
${it.uid}
${name}
`; })); const w = window.open("", "_blank"); if (!w) { onError("Bitte Pop-ups für den Druck erlauben."); return; } w.document.write( `Etiketten${labels.join("")} `, ); w.document.close(); } const ortName = (id) => (id == null ? "" : locations.find((l) => l.id === id)?.name || ""); return (

Einzelstücke

{items.length} Stück {items.length > 0 && ( )}
{items.map((it) => ( ))} {items.length === 0 && }
QRUIDLagerortGekauft am Garantie bisGekauft beiNotiz
{it.uid} {isAdmin ? ( ) : {ortName(it.location_id) || "–"}} {isAdmin ? ( { if ((e.target.value || "") !== (it.acquired_on || "")) patchItem(it, { acquired_on: e.target.value || null }); }} /> ) : {it.acquired_on || "–"}} {isAdmin ? ( { if ((e.target.value || "") !== (it.warranty_until || "")) patchItem(it, { warranty_until: e.target.value || null }); }} /> ) : {it.warranty_until || "–"}} {isAdmin ? ( ) : {it.shop_name || "–"}} {isAdmin ? ( { if ((e.target.value || "") !== (it.note || "")) patchItem(it, { note: e.target.value || null }); }} /> ) : {it.note || "–"}} {isAdmin && (
)}
{suggestion && suggestion.itemId === it.id && (suggestion.date || suggestion.priceCents != null) && (
Im Beleg erkannt: {suggestion.date ? ` Garantie bis ${suggestion.date}` : ""} {suggestion.date && suggestion.priceCents != null ? "," : ""} {suggestion.priceCents != null ? ` Kaufpreis ${(suggestion.priceCents / 100).toFixed(2)}` : ""}.
)}
    {(it.documents || []).map((d) => (
  • {isAdmin && ( )}
  • ))} {(it.documents || []).length === 0 && (
  • Noch keine Belege.
  • )}
Noch keine Einzelstücke.
{isAdmin && !showAdd && ( )} {isAdmin && showAdd && (

Einzelstücke anlegen

Gemeinsame Startwerte für alle neuen Stücke – danach je Stück änderbar (z.B. dasselbe Modell 2024 und 2025). Jedes bekommt eine eigene UID + QR.

)} {isAdmin && remove && (

Einzelstück {remove.item.uid} entfernen

)}
); }