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:45:19 +10:00
commit 1bc2de655a
505 changed files with 56223 additions and 0 deletions
+120
View File
@@ -0,0 +1,120 @@
import { notFound } from "next/navigation";
import { requireOperator } from "@/lib/ops/session";
import { facility } from "@/lib/ops/projections";
import { revealedContacts, REVEAL_MINUTES } from "@/lib/ops/reveal";
import Reveal from "./Reveal";
import Plan from "./Plan";
import Danger from "./Danger";
/* One facility: configuration, counts, activity. Contacts are masked — the ops_ro role cannot read
* them, and the projection never asks. They appear only while this operator holds an unexpired
* reveal grant, read through the separate reveal role (lib/ops/reveal.ts).
* Nothing about a wearer is reachable from here at any level. */
export const dynamic = "force-dynamic";
const dl: React.CSSProperties = { display: "grid", gridTemplateColumns: "auto 1fr", gap: "8px 16px", fontSize: 13, margin: 0 };
const dt: React.CSSProperties = { color: "var(--color-neutral-600)" };
const dd: React.CSSProperties = { margin: 0, fontWeight: 600, fontVariantNumeric: "tabular-nums" };
const masked: React.CSSProperties = { fontFamily: "ui-monospace, Menlo, monospace", letterSpacing: "0.02em", color: "var(--color-neutral-700)" };
export default async function OpsFacility({ params }: { params: Promise<{ id: string }> }) {
const op = await requireOperator();
const { id } = await params;
const f = await facility(id);
if (!f) notFound();
const rv = f.isDemo ? null : await revealedContacts(op.id, id);
const fmt = (d: Date) => d.toLocaleDateString("en-AU", { day: "numeric", month: "short", year: "numeric" });
const hm = (d: Date) => d.toLocaleTimeString("en-AU", { timeZone: "Australia/Brisbane", hour: "2-digit", minute: "2-digit" });
const n = (x: number) => x.toLocaleString();
const c = f.counts;
return (
<>
<div className="page-head">
<div>
<div className="sec" style={{ border: 0, padding: 0 }}>Facility{f.isDemo ? " · demo" : ""}</div>
<h1 className="h1">{f.name}</h1>
<div style={{ fontSize: 13, color: "var(--color-neutral-700)", marginTop: 4 }}>
Created {fmt(f.createdAt)} · {f.timezone} · rev {n(f.rev)} · {f.backupAgeDays === null ? "never exported" : `last export ${f.backupAgeDays} days ago`}
</div>
</div>
<a className="btn" href="/ops/facilities">All facilities</a>
</div>
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(280px, 1fr))", gap: 24, marginTop: 20 }}>
<div className="tc-panel">
<div className="tc-panel-head"><span>Contacts</span><span className="tc-panel-aside">{rv ? `revealed until ${hm(rv.expiresAt)}` : "masked"}</span></div>
<div className="tc-panel-body" style={{ fontSize: 13 }}>
{rv ? (
<>
<div style={{ padding: "6px 0", borderBottom: "1px solid var(--color-divider)" }}><div className="sec" style={{ border: 0, padding: 0, fontSize: 11 }}>Coordinator</div><span style={{ fontWeight: 600 }}>{rv.coordinator || "—"}</span></div>
<div style={{ padding: "6px 0", borderBottom: "1px solid var(--color-divider)" }}><div className="sec" style={{ border: 0, padding: 0, fontSize: 11 }}>Email</div><span style={{ fontWeight: 600 }}>{rv.coordinatorEmail || "—"}</span></div>
<div style={{ padding: "6px 0" }}><div className="sec" style={{ border: 0, padding: 0, fontSize: 11 }}>Phone</div><span style={{ fontWeight: 600 }}>{rv.coordinatorPhone || "—"}</span></div>
<div style={{ borderTop: "2px solid var(--color-text)", marginTop: 12, paddingTop: 10, fontSize: 12.5, color: "var(--color-neutral-700)", display: "flex", gap: 8 }}>
<span style={{ color: "var(--color-accent)", fontWeight: 800 }}></span>
<span>{`Revealed ${hm(rv.at)} for ${REVEAL_MINUTES} minutes, in the trail and emailed. Reason: ${rv.reason}`}</span>
</div>
</>
) : (
<>
<div style={{ padding: "6px 0", borderBottom: "1px solid var(--color-divider)" }}><div className="sec" style={{ border: 0, padding: 0, fontSize: 11 }}>Coordinator</div><span style={masked}></span></div>
<div style={{ padding: "6px 0", borderBottom: "1px solid var(--color-divider)" }}><div className="sec" style={{ border: 0, padding: 0, fontSize: 11 }}>Email</div><span style={masked}>@</span></div>
<div style={{ padding: "6px 0" }}><div className="sec" style={{ border: 0, padding: 0, fontSize: 11 }}>Phone</div><span style={masked}> </span></div>
<div style={{ borderTop: "2px solid var(--color-text)", marginTop: 12, paddingTop: 10, fontSize: 12.5, color: "var(--color-neutral-700)", display: "flex", gap: 8 }}>
<span style={{ color: "var(--color-accent)", fontWeight: 800 }}></span>
<span>The console&rsquo;s database role cannot read these. Revealing them is a separate act: it asks for a reason, lasts thirty minutes, is written to the operator trail and emails you a copy. Nothing about a wearer can be revealed at any level.</span>
</div>
{!f.isDemo && <Reveal facilityId={f.id} minutes={REVEAL_MINUTES} />}
</>
)}
</div>
</div>
<div className="tc-panel">
<div className="tc-panel-head"><span>Configuration</span></div>
<dl className="tc-panel-body" style={dl}>
<dt style={dt}>Ceiling</dt><dd style={dd}>{f.config.capSets} sets held at any time</dd>
<dt style={dt}>Starting kit</dt><dd style={dd}>{f.config.initialSets} sets</dd>
<dt style={dt}>Staff groups</dt><dd style={dd}>{f.config.groups} named · {f.config.fteGroups} on the FTE table · {f.config.kitGroups} on the starting kit</dd>
<dt style={dt}>Reorder default</dt><dd style={dd}>{f.config.defaultReorder}</dd>
<dt style={dt}>GL account</dt><dd style={dd}>{f.config.glAccountSet ? "set" : "not set"}</dd>
<dt style={dt}>Journal line</dt><dd style={dd}>{f.config.journalDesc}</dd>
<dt style={dt}>Barcode lookup</dt><dd style={dd}>{f.config.barcodeLookup ? "on" : "off"}</dd>
<dt style={dt}>Slip organisation</dt><dd style={dd}>{f.config.slipOrgSet ? "set" : "not set"}</dd>
<dt style={dt}>Single sign-on</dt><dd style={dd}>{`${f.config.sso}${f.config.ssoStaff ? " · staff too" : ""}`}</dd>
</dl>
</div>
</div>
<div className="tc-panel" style={{ marginTop: 24 }}>
<div className="tc-panel-head"><span>Counts</span><span className="tc-panel-aside">no records readable</span></div>
<dl className="tc-panel-body" style={{ ...dl, gridTemplateColumns: "repeat(auto-fit, minmax(200px, 1fr))" }}>
{/* Whole phrases, not `{value} word`: React separates adjacent text and expressions with
comment nodes, so "1 admin" would render as "1<!-- --> admin" and never match a grep. */}
<div><dt style={dt}>Coordinators</dt><dd style={dd}>{`${n(c.users)} · ${c.admins} admin · ${c.usersWith2fa} with 2FA${c.inactiveUsers ? ` · ${c.inactiveUsers} inactive` : ""}`}</dd></div>
<div><dt style={dt}>Staff register</dt><dd style={dd}>{`${n(c.staff)} · ${n(c.activeStaff)} active`}</dd></div>
<div><dt style={dt}>Staff accounts</dt><dd style={dd}>{`${n(c.staffAccounts)} · ${n(c.accountsSeen7d)} seen this week`}</dd></div>
<div><dt style={dt}>Catalogue</dt><dd style={dd}>{n(c.items)}</dd></div>
<div><dt style={dt}>Issues</dt><dd style={dd}>{n(c.issues)}</dd></div>
<div><dt style={dt}>Orders</dt><dd style={dd}>{n(c.orders)}</dd></div>
<div><dt style={dt}>Requests</dt><dd style={dd}>{n(c.requests)}</dd></div>
<div><dt style={dt}>Events, 24h</dt><dd style={dd}>{n(c.events24h)}</dd></div>
</dl>
</div>
{!f.isDemo && (
<>
<div className="tc-panel" style={{ marginTop: 24 }}>
<div className="tc-panel-head"><span>Plan</span><span className="tc-panel-aside">the billing desk · every act in the trail</span></div>
<Plan facilityId={f.id} view={f.planView} owner={op.role === "OWNER"} />
</div>
<div className="tc-panel" style={{ marginTop: 24, borderColor: "var(--color-accent)" }}>
<div className="tc-panel-head"><span>Danger zone</span><span className="tc-panel-aside">cannot be undone</span></div>
<Danger facilityId={f.id} name={f.name} owner={op.role === "OWNER"} totpEnabled={op.totpEnabled} />
</div>
</>
)}
</>
);
}