ThreadCount Community edition

Uniform stock management for healthcare linen rooms. Licensed under the GNU AGPL v3.
This commit is contained in:
ThreadCount
2026-09-13 08:54:35 +10:00
commit 344b1701dd
505 changed files with 56231 additions and 0 deletions
+86
View File
@@ -0,0 +1,86 @@
"use client";
/* Signed in — onboarding screen 05. A beat of confirmation before the app: which linen room you
are now in, and the two figures that decide what the morning looks like. */
import { Suspense, useMemo } from "react";
import Link from "next/link";
import { useSearchParams } from "next/navigation";
import { useDerived, useSnap } from "@/lib/client";
import { daysBetween, locTree, onhand, reorderAt, touched } from "@/lib/compute";
function SignedInInner() {
const { s } = useSnap();
const { L, variants } = useDerived();
const isNew = useSearchParams().get("new") === "1";
const d = useMemo(() => {
const counted = variants.filter((v) => touched(s, L, v.key));
const low = counted.filter((v) => onhand(s, L, v.key) <= reorderAt(s, v.key)).length;
const lastCount = s.stocktakes.find((t) => t.mode !== "preloved");
return {
lines: counted.length,
low,
since: lastCount ? daysBetween(lastCount.date, s.today) : null,
locations: locTree(s).length,
};
}, [s, L, variants]);
const row: React.CSSProperties = { display: "flex", alignItems: "baseline", gap: 12, padding: "14px 0", borderBottom: "1px solid var(--color-divider)" };
const lab: React.CSSProperties = { flex: 1, fontSize: 14.5, color: "var(--color-neutral-800)" };
// Figures get the big numeral; "Never counted" is a sentence and was being set at the same size,
// where it ran nearly the width of the row and read as the loudest thing on the screen.
const val = (hot?: boolean, text?: boolean): React.CSSProperties => ({ fontFamily: "var(--font-heading)", fontWeight: 800, fontSize: text ? 14.5 : 19, fontVariantNumeric: "tabular-nums", color: hot ? "var(--color-accent-700)" : "var(--color-text)" });
const overdue = d.since !== null && d.since > 30;
return (
<>
<section style={{ background: "var(--color-accent)", color: "#fff", padding: "calc(34px + env(safe-area-inset-top, 0px)) 24px 34px" }}>
<div style={{ fontSize: 11, fontWeight: 600, letterSpacing: "0.14em", textTransform: "uppercase", color: "rgba(255,255,255,0.88)" }}>
{isNew ? "Facility created" : "Signed in"}
</div>
<h1 style={{ fontFamily: "var(--font-heading)", fontWeight: 800, fontSize: 36, lineHeight: 1.02, letterSpacing: "-0.03em", marginTop: 10 }}>
{s.settings.facility}
</h1>
</section>
<div style={{ flex: 1, overflowY: "auto", padding: "24px 24px 30px", background: "var(--color-bg)" }}>
{isNew ? (
<p style={{ fontSize: 15, lineHeight: 1.6, color: "var(--color-neutral-800)" }}>
Your linen room is set up and you are its first administrator. There is nothing in it
yet the catalogue, staff register and cost centres come in from CSV on the desktop at
threadcount.tech, and take about an afternoon.
</p>
) : (
<>
<div style={{ fontSize: 11, fontWeight: 600, letterSpacing: "0.12em", textTransform: "uppercase", color: "var(--color-neutral-600)" }}>
{s.settings.location || "Linen Room"}
</div>
<div style={{ marginTop: 14, borderTop: "2px solid var(--color-text)" }}>
<div style={row}><span style={lab}>Lines on the shelf</span><span style={val()}>{d.lines}</span></div>
<div style={row}><span style={lab}>Below par</span><span style={val(d.low > 0)}>{d.low}</span></div>
<div style={row}>
<span style={lab}>Since the last count</span>
<span style={val(overdue, d.since === null)}>{d.since === null ? "Never counted" : `${d.since} day${d.since === 1 ? "" : "s"}`}</span>
</div>
</div>
{d.locations === 0 && (
<p style={{ fontSize: 13.5, lineHeight: 1.6, color: "var(--color-neutral-800)", background: "#fff", borderLeft: "6px solid var(--color-accent)", padding: 14, marginTop: 20 }}>
No shelves set up yet, so there is nothing to count against. Add them in Settings on
the desktop, then place each size on one from Inventory.
</p>
)}
</>
)}
</div>
<Link href="/m" replace
style={{ flex: "0 0 auto", height: 66, background: "var(--color-text)", color: "var(--color-bg)", display: "flex", alignItems: "center", gap: 12, padding: "0 24px calc(0px + env(safe-area-inset-bottom, 0px))", textDecoration: "none", fontFamily: "var(--font-heading)", fontWeight: 800, fontSize: 14, letterSpacing: "0.08em", textTransform: "uppercase" }}>
<span style={{ flex: 1 }}>Start the day</span>
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2.2} strokeLinecap="square" aria-hidden="true"><path d="M5 12h14" /><path d="m13 6 6 6-6 6" /></svg>
</Link>
</>
);
}
export default function MSignedIn() {
return <Suspense fallback={null}><SignedInInner /></Suspense>;
}