057da00fd2
Uniform stock management for healthcare linen rooms: the coordinator app, the phone counter and the staff app, for your own server. Built from e2d6d42 on 2026-09-13. Licensed under the Functional Source License (FSL-1.1-ALv2).
87 lines
4.9 KiB
TypeScript
87 lines
4.9 KiB
TypeScript
"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>;
|
|
}
|