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 [addForm, setAddForm] = useState(EMPTY_ADD);
|
||||||
const [remove, setRemove] = useState(null); // { item, reason, note } | null
|
const [remove, setRemove] = useState(null); // { item, reason, note } | null
|
||||||
const [lend, setLend] = useState(null); // Stück, das gerade verliehen wird
|
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 [showAdd, setShowAdd] = useState(false); // Anlege-Maske ein-/ausklappen
|
||||||
const [suggestion, setSuggestion] = useState(null); // { itemId, date } aus PDF-Analyse
|
const [suggestion, setSuggestion] = useState(null); // { itemId, date } aus PDF-Analyse
|
||||||
const [docFile, setDocFile] = useState(null); // Beleg, der beim Anlegen mitkommt
|
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);
|
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 (
|
return (
|
||||||
<section className="card">
|
<section className="card">
|
||||||
<div className="card-head">
|
<div className="card-head">
|
||||||
@@ -271,9 +282,7 @@ export default function Einzelstuecke({ product, locations, shops, isAdmin, onCh
|
|||||||
<table className="table">
|
<table className="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>QR</th><th>UID</th><th>Lagerort</th><th>Verliehen an</th>
|
<th>QR</th><th>UID</th><th>Lagerort</th><th>Verliehen an</th><th></th>
|
||||||
<th>Gekauft am</th><th>Garantie bis</th><th>Gekauft bei</th>
|
|
||||||
<th>Notiz</th><th></th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -292,36 +301,10 @@ export default function Einzelstuecke({ product, locations, shops, isAdmin, onCh
|
|||||||
) : <span>{ortName(it.location_id) || "–"}</span>}
|
) : <span>{ortName(it.location_id) || "–"}</span>}
|
||||||
</td>
|
</td>
|
||||||
<td data-label="Verliehen an"><VerleihText loan={it.loan} /></td>
|
<td data-label="Verliehen an"><VerleihText loan={it.loan} /></td>
|
||||||
<td data-label="Gekauft am">
|
{/* cell-row statt field-inline: letzteres bricht in Tabellen um
|
||||||
{isAdmin ? (
|
(styles.css) und stellte die Symbole uebereinander. */}
|
||||||
<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>
|
|
||||||
<td className="num">
|
<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. */}
|
{/* Verleihen und Zurücknehmen ist eine Buchung – das darf jeder. */}
|
||||||
<VerleihAktion item={it} onLend={setLend} onChanged={nachAktion}
|
<VerleihAktion item={it} onLend={setLend} onChanged={nachAktion}
|
||||||
onError={onError} />
|
onError={onError} />
|
||||||
@@ -337,13 +320,42 @@ export default function Einzelstuecke({ product, locations, shops, isAdmin, onCh
|
|||||||
</button>
|
</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>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{offen.has(it.id) && (
|
||||||
<tr className="einzel-detailrow">
|
<tr className="einzel-detailrow">
|
||||||
<td colSpan={9}>
|
<td colSpan={5}>
|
||||||
<div style={{ display: "grid", gap: "var(--sp-4)",
|
<div style={{ display: "grid", gap: "var(--sp-4)",
|
||||||
gridTemplateColumns: "repeat(auto-fit, minmax(240px, 1fr))" }}>
|
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>
|
<div>
|
||||||
<span className="muted small">Verleih</span>
|
<span className="muted small">Verleih</span>
|
||||||
<div style={{ marginTop: "var(--sp-2)" }}>
|
<div style={{ marginTop: "var(--sp-2)" }}>
|
||||||
@@ -428,9 +440,10 @@ export default function Einzelstuecke({ product, locations, shops, isAdmin, onCh
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
)}
|
||||||
</Fragment>
|
</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>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -119,6 +119,8 @@ const PATHS = {
|
|||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
plus: <path d="M5 12h14M12 5v14" />,
|
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: (
|
trash: (
|
||||||
<>
|
<>
|
||||||
<path d="M3 6h18" />
|
<path d="M3 6h18" />
|
||||||
|
|||||||
@@ -18,8 +18,10 @@ export function FristBadge({ loan, warnTage = 7 }) {
|
|||||||
if (!loan || !loan.due_on) return null;
|
if (!loan || !loan.due_on) return null;
|
||||||
const tage = daysUntil(loan.due_on);
|
const tage = daysUntil(loan.due_on);
|
||||||
const art = loan.days_overdue ? "danger" : (tage != null && tage <= warnTage ? "warn" : null);
|
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
|
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)}`;
|
: `zurück ${relativeExpiry(tage)}`;
|
||||||
return art
|
return art
|
||||||
? <span className={`badge ${art}`}>{text}</span>
|
? <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
|
/* Zelle mit Eingabe plus Kennzeichen (z.B. "niedrig"): eine Zeile, damit das
|
||||||
Kennzeichen immer an derselben Stelle sitzt statt mal darunter zu rutschen. */
|
Kennzeichen immer an derselben Stelle sitzt statt mal darunter zu rutschen. */
|
||||||
.cell-row { display: flex; align-items: center; gap: 6px; }
|
.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. */
|
/* Zwei Zeilen in einer Zelle: Steuerelement oben, Erläuterung darunter. */
|
||||||
.cell-col { display: flex; flex-direction: column; gap: 2px; align-items: stretch; }
|
.cell-col { display: flex; flex-direction: column; gap: 2px; align-items: stretch; }
|
||||||
/* Beschriftung mit Erklaerung im Tooltip: die gepunktete Linie zeigt an, dass
|
/* Beschriftung mit Erklaerung im Tooltip: die gepunktete Linie zeigt an, dass
|
||||||
|
|||||||
Reference in New Issue
Block a user