Einzelstuecke: Zeile zeigt nur noch, was man im Alltag braucht
Die Tabelle trug neun Spalten, davon sechs als dauernd offene Eingabefelder - bei einem einzigen Stueck schon eine Wand aus Bedienelementen. Jetzt stehen in der Zeile QR, UID, Lagerort, "Verliehen an" und die Aktionen; Kaufdatum, Garantie, Shop, Notiz, Kaufpreis, Belege und der Verleih-Verlauf klappen je Stueck auf. Aenderbar bleibt alles, nur nicht alles gleichzeitig sichtbar. Dazu zwei Fehler von mir aus dem Verleih-Commit: - Die drei Aktionssymbole standen uebereinander statt nebeneinander. Die Aktionszelle benutzte .field-inline, und das bricht in Tabellen bewusst um (styles.css) - mit dem vierten Knopf reichte der Platz nicht mehr. Jetzt .cell-row, das umbricht nicht. - "ueberfaellig (vor 1 Woche)" las sich als Zeitpunkt, gemeint ist eine Dauer. Jetzt "7 Tage ueberfaellig". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
b2dda78a7b
commit
c924dffad6
@@ -48,6 +48,9 @@ export default function Einzelstuecke({ product, locations, shops, isAdmin, onCh
|
||||
const [addForm, setAddForm] = useState(EMPTY_ADD);
|
||||
const [remove, setRemove] = useState(null); // { item, reason, note } | null
|
||||
const [lend, setLend] = useState(null); // Stück, das gerade verliehen wird
|
||||
// Aufgeklappte Stücke. Die Zeile zeigt nur, was man im Alltag braucht
|
||||
// (wo liegt es, ist es verliehen) – Kaufdaten und Belege auf Wunsch.
|
||||
const [offen, setOffen] = useState(() => new Set());
|
||||
const [showAdd, setShowAdd] = useState(false); // Anlege-Maske ein-/ausklappen
|
||||
const [suggestion, setSuggestion] = useState(null); // { itemId, date } aus PDF-Analyse
|
||||
const [docFile, setDocFile] = useState(null); // Beleg, der beim Anlegen mitkommt
|
||||
@@ -254,6 +257,14 @@ export default function Einzelstuecke({ product, locations, shops, isAdmin, onCh
|
||||
|
||||
const ortName = (id) => locationPathById(id, locations);
|
||||
|
||||
function klappe(id) {
|
||||
setOffen((alt) => {
|
||||
const neu = new Set(alt);
|
||||
if (neu.has(id)) neu.delete(id); else neu.add(id);
|
||||
return neu;
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="card">
|
||||
<div className="card-head">
|
||||
@@ -271,9 +282,7 @@ export default function Einzelstuecke({ product, locations, shops, isAdmin, onCh
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>QR</th><th>UID</th><th>Lagerort</th><th>Verliehen an</th>
|
||||
<th>Gekauft am</th><th>Garantie bis</th><th>Gekauft bei</th>
|
||||
<th>Notiz</th><th></th>
|
||||
<th>QR</th><th>UID</th><th>Lagerort</th><th>Verliehen an</th><th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -292,36 +301,10 @@ export default function Einzelstuecke({ product, locations, shops, isAdmin, onCh
|
||||
) : <span>{ortName(it.location_id) || "–"}</span>}
|
||||
</td>
|
||||
<td data-label="Verliehen an"><VerleihText loan={it.loan} /></td>
|
||||
<td data-label="Gekauft am">
|
||||
{isAdmin ? (
|
||||
<input type="date" defaultValue={it.acquired_on || ""} style={{ marginTop: 0 }}
|
||||
onBlur={(e) => { if ((e.target.value || "") !== (it.acquired_on || "")) patchItem(it, { acquired_on: e.target.value || null }); }} />
|
||||
) : <span>{it.acquired_on || "–"}</span>}
|
||||
</td>
|
||||
<td data-label="Garantie bis">
|
||||
{isAdmin ? (
|
||||
<input key={it.warranty_until || "none"} type="date"
|
||||
defaultValue={it.warranty_until || ""} style={{ marginTop: 0 }}
|
||||
onBlur={(e) => { if ((e.target.value || "") !== (it.warranty_until || "")) patchItem(it, { warranty_until: e.target.value || null }); }} />
|
||||
) : <span>{it.warranty_until || "–"}</span>}
|
||||
</td>
|
||||
<td data-label="Gekauft bei">
|
||||
{isAdmin ? (
|
||||
<select value={it.shop_id ?? ""} style={{ marginTop: 0, minWidth: 120 }}
|
||||
onChange={(e) => patchItem(it, { shop_id: e.target.value === "" ? null : Number(e.target.value) })}>
|
||||
<option value="">– unbekannt –</option>
|
||||
{shops.map((s) => <option key={s.id} value={s.id}>{s.name}</option>)}
|
||||
</select>
|
||||
) : <span>{it.shop_name || "–"}</span>}
|
||||
</td>
|
||||
<td data-label="Notiz">
|
||||
{isAdmin ? (
|
||||
<input defaultValue={it.note || ""} placeholder="z.B. Zustand" style={{ marginTop: 0, minWidth: 120 }}
|
||||
onBlur={(e) => { if ((e.target.value || "") !== (it.note || "")) patchItem(it, { note: e.target.value || null }); }} />
|
||||
) : <span>{it.note || "–"}</span>}
|
||||
</td>
|
||||
{/* cell-row statt field-inline: letzteres bricht in Tabellen um
|
||||
(styles.css) und stellte die Symbole uebereinander. */}
|
||||
<td className="num">
|
||||
<div className="field-inline" style={{ justifyContent: "flex-end" }}>
|
||||
<div className="cell-row" style={{ justifyContent: "flex-end" }}>
|
||||
{/* Verleihen und Zurücknehmen ist eine Buchung – das darf jeder. */}
|
||||
<VerleihAktion item={it} onLend={setLend} onChanged={nachAktion}
|
||||
onError={onError} />
|
||||
@@ -337,13 +320,42 @@ export default function Einzelstuecke({ product, locations, shops, isAdmin, onCh
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<button type="button" className="btn-icon"
|
||||
title={offen.has(it.id) ? "Details zuklappen" : "Kaufdaten, Preis, Belege, Verleih-Verlauf"}
|
||||
aria-expanded={offen.has(it.id)}
|
||||
onClick={() => klappe(it.id)}>
|
||||
<Icon name="chevron" size={18} className={offen.has(it.id) ? "" : "rot-90"} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{offen.has(it.id) && (
|
||||
<tr className="einzel-detailrow">
|
||||
<td colSpan={9}>
|
||||
<td colSpan={5}>
|
||||
<div style={{ display: "grid", gap: "var(--sp-4)",
|
||||
gridTemplateColumns: "repeat(auto-fit, minmax(240px, 1fr))" }}>
|
||||
<div className="row" style={{ margin: 0 }}>
|
||||
<label className="grow" style={{ margin: 0 }}>Gekauft am
|
||||
<input type="date" defaultValue={it.acquired_on || ""} disabled={!isAdmin}
|
||||
onBlur={(e) => { if ((e.target.value || "") !== (it.acquired_on || "")) patchItem(it, { acquired_on: e.target.value || null }); }} />
|
||||
</label>
|
||||
<label className="grow" style={{ margin: 0 }}>Garantie bis
|
||||
<input key={it.warranty_until || "none"} type="date" disabled={!isAdmin}
|
||||
defaultValue={it.warranty_until || ""}
|
||||
onBlur={(e) => { if ((e.target.value || "") !== (it.warranty_until || "")) patchItem(it, { warranty_until: e.target.value || null }); }} />
|
||||
</label>
|
||||
</div>
|
||||
<label style={{ margin: 0 }}>Gekauft bei
|
||||
<select value={it.shop_id ?? ""} disabled={!isAdmin}
|
||||
onChange={(e) => patchItem(it, { shop_id: e.target.value === "" ? null : Number(e.target.value) })}>
|
||||
<option value="">– unbekannt –</option>
|
||||
{shops.map((s) => <option key={s.id} value={s.id}>{s.name}</option>)}
|
||||
</select>
|
||||
</label>
|
||||
<label style={{ margin: 0 }}>Notiz
|
||||
<input defaultValue={it.note || ""} placeholder="z.B. Zustand" disabled={!isAdmin}
|
||||
onBlur={(e) => { if ((e.target.value || "") !== (it.note || "")) patchItem(it, { note: e.target.value || null }); }} />
|
||||
</label>
|
||||
<div>
|
||||
<span className="muted small">Verleih</span>
|
||||
<div style={{ marginTop: "var(--sp-2)" }}>
|
||||
@@ -428,9 +440,10 @@ export default function Einzelstuecke({ product, locations, shops, isAdmin, onCh
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
{items.length === 0 && <tr><td colSpan={9} className="empty">Noch keine Einzelstücke.</td></tr>}
|
||||
{items.length === 0 && <tr><td colSpan={5} className="empty">Noch keine Einzelstücke.</td></tr>}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -119,6 +119,8 @@ const PATHS = {
|
||||
</>
|
||||
),
|
||||
plus: <path d="M5 12h14M12 5v14" />,
|
||||
// Aufklappen. Zeigt nach unten; zugeklappt dreht die Zelle es nach rechts.
|
||||
chevron: <path d="m6 9 6 6 6-6" />,
|
||||
trash: (
|
||||
<>
|
||||
<path d="M3 6h18" />
|
||||
|
||||
@@ -18,8 +18,10 @@ export function FristBadge({ loan, warnTage = 7 }) {
|
||||
if (!loan || !loan.due_on) return null;
|
||||
const tage = daysUntil(loan.due_on);
|
||||
const art = loan.days_overdue ? "danger" : (tage != null && tage <= warnTage ? "warn" : null);
|
||||
// Bei Ueberfaelligkeit die Tage ausschreiben: „vor 1 Woche" (relativeExpiry)
|
||||
// liest sich als Zeitpunkt, gemeint ist aber eine Dauer.
|
||||
const text = loan.days_overdue
|
||||
? `überfällig (${relativeExpiry(-loan.days_overdue)})`
|
||||
? `${loan.days_overdue} ${loan.days_overdue === 1 ? "Tag" : "Tage"} überfällig`
|
||||
: `zurück ${relativeExpiry(tage)}`;
|
||||
return art
|
||||
? <span className={`badge ${art}`}>{text}</span>
|
||||
|
||||
@@ -410,6 +410,10 @@ td select { width: auto; min-width: 0; max-width: 100%; }
|
||||
/* Zelle mit Eingabe plus Kennzeichen (z.B. "niedrig"): eine Zeile, damit das
|
||||
Kennzeichen immer an derselben Stelle sitzt statt mal darunter zu rutschen. */
|
||||
.cell-row { display: flex; align-items: center; gap: 6px; }
|
||||
/* Zugeklappt zeigt der Pfeil nach rechts, aufgeklappt nach unten. Eine Drehung
|
||||
statt zweier Symbole – so bleibt die Bewegung sichtbar. */
|
||||
.rot-90 { transform: rotate(-90deg); transition: transform 120ms ease; }
|
||||
.icon { transition: transform 120ms ease; }
|
||||
/* Zwei Zeilen in einer Zelle: Steuerelement oben, Erläuterung darunter. */
|
||||
.cell-col { display: flex; flex-direction: column; gap: 2px; align-items: stretch; }
|
||||
/* Beschriftung mit Erklaerung im Tooltip: die gepunktete Linie zeigt an, dass
|
||||
|
||||
Reference in New Issue
Block a user