a3559062db
Build and push image / build (push) Successful in 1m2s
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
170 lines
4.6 KiB
TypeScript
170 lines
4.6 KiB
TypeScript
import { useMemo } from "react";
|
|
import type { Bootstrap, Item } from "../types.js";
|
|
import { helpers, TODAY_STR, enrichItems } from "../types.js";
|
|
import { remainingShort } from "../stats.js";
|
|
import { fmt, TYPE_GLYPHS } from "../format.js";
|
|
import { Btn, Card, Icon } from "../components/primitives/index.js";
|
|
|
|
export function CustodyView({
|
|
data,
|
|
onSelectItem,
|
|
onCheckin,
|
|
onConsume,
|
|
onMarkGone,
|
|
}: {
|
|
data: Bootstrap;
|
|
onSelectItem: (i: Item) => void;
|
|
onCheckin: (i: Item) => void;
|
|
onConsume: (i: Item) => void;
|
|
onMarkGone: (i: Item) => void;
|
|
}) {
|
|
const items = useMemo(() => enrichItems(data), [data]);
|
|
const checkedOut = useMemo(
|
|
() =>
|
|
items
|
|
.filter((i) => i.status === "checked-out")
|
|
.sort(
|
|
(a, b) =>
|
|
+new Date(b.checkoutDate ?? b.purchaseDate) -
|
|
+new Date(a.checkoutDate ?? a.purchaseDate),
|
|
),
|
|
[items],
|
|
);
|
|
|
|
return (
|
|
<div style={{ padding: "40px 48px", maxWidth: 1200 }}>
|
|
<div style={{ marginBottom: 32 }}>
|
|
<h1
|
|
className="serif"
|
|
style={{ fontSize: 44, margin: 0, fontWeight: 500, letterSpacing: "-0.02em" }}
|
|
>
|
|
My Custody
|
|
</h1>
|
|
<div style={{ fontSize: 13, color: "var(--ink-3)", marginTop: 6 }}>
|
|
{checkedOut.length} item{checkedOut.length === 1 ? "" : "s"} checked out
|
|
</div>
|
|
</div>
|
|
|
|
{checkedOut.length === 0 ? (
|
|
<div
|
|
style={{
|
|
textAlign: "center",
|
|
padding: "80px 20px",
|
|
color: "var(--ink-3)",
|
|
}}
|
|
>
|
|
<Icon name="pocket" size={40} color="var(--ink-4)" />
|
|
<div
|
|
style={{
|
|
fontSize: 14,
|
|
fontStyle: "italic",
|
|
marginTop: 16,
|
|
}}
|
|
>
|
|
Nothing checked out right now.
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<Card padded={false}>
|
|
<div
|
|
style={{
|
|
display: "grid",
|
|
gridTemplateColumns: "32px 2fr 1fr 0.8fr 0.8fr 260px",
|
|
padding: "10px 16px",
|
|
fontSize: 11,
|
|
color: "var(--ink-3)",
|
|
textTransform: "uppercase",
|
|
letterSpacing: "0.06em",
|
|
borderBottom: "1px solid var(--line)",
|
|
}}
|
|
>
|
|
<div />
|
|
<div>Item</div>
|
|
<div>Brand</div>
|
|
<div>Remaining</div>
|
|
<div>Checked out</div>
|
|
<div />
|
|
</div>
|
|
|
|
{checkedOut.map((item) => (
|
|
<CustodyRow
|
|
key={item.id}
|
|
item={item}
|
|
data={data}
|
|
onSelect={() => onSelectItem(item)}
|
|
onCheckin={() => onCheckin(item)}
|
|
onConsume={() => onConsume(item)}
|
|
onMarkGone={() => onMarkGone(item)}
|
|
/>
|
|
))}
|
|
</Card>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function CustodyRow({
|
|
item,
|
|
data,
|
|
onSelect,
|
|
onCheckin,
|
|
onConsume,
|
|
onMarkGone,
|
|
}: {
|
|
item: Item;
|
|
data: Bootstrap;
|
|
onSelect: () => void;
|
|
onCheckin: () => void;
|
|
onConsume: () => void;
|
|
onMarkGone: () => void;
|
|
}) {
|
|
const glyph = TYPE_GLYPHS[item.type] ?? "·";
|
|
const pct = helpers.pctRemaining(item, TODAY_STR);
|
|
|
|
return (
|
|
<div
|
|
style={{
|
|
display: "grid",
|
|
gridTemplateColumns: "32px 2fr 1fr 0.8fr 0.8fr 260px",
|
|
padding: "12px 16px",
|
|
alignItems: "center",
|
|
borderBottom: "1px solid var(--line)",
|
|
cursor: "pointer",
|
|
}}
|
|
onClick={onSelect}
|
|
>
|
|
<div style={{ fontSize: 16, textAlign: "center", opacity: 0.6 }}>{glyph}</div>
|
|
<div>
|
|
<div style={{ fontWeight: 500, fontSize: 14 }}>{item.name}</div>
|
|
<div className="mono" style={{ fontSize: 11, color: "var(--ink-3)" }}>
|
|
{item.assetId}
|
|
</div>
|
|
</div>
|
|
<div style={{ fontSize: 13, color: "var(--ink-2)" }}>
|
|
{helpers.brandName(data, item.brandId)}
|
|
</div>
|
|
<div style={{ fontFamily: "var(--mono)", fontSize: 12 }}>
|
|
{remainingShort(item)}
|
|
<span style={{ color: "var(--ink-3)", marginLeft: 6 }}>
|
|
{Math.round(pct * 100)}%
|
|
</span>
|
|
</div>
|
|
<div style={{ fontSize: 12, color: "var(--ink-3)" }}>
|
|
{fmt.daysAgo(item.checkoutDate)}
|
|
</div>
|
|
<div
|
|
style={{ display: "flex", gap: 4, justifyContent: "flex-end" }}
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<Btn variant="sage" icon="check" onClick={onCheckin}>
|
|
Check in
|
|
</Btn>
|
|
<Btn variant="secondary" icon="check" onClick={onConsume}>
|
|
Consume
|
|
</Btn>
|
|
<Btn variant="ghost" icon="bin" onClick={onMarkGone} />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|