7d650e4c10
Uniform stock management for healthcare linen rooms: the coordinator app, the phone counter and the staff app, for your own server. Built from d947f89 on 2026-09-15. Licensed under the Functional Source License (FSL-1.1-ALv2).
61 lines
3.5 KiB
TypeScript
61 lines
3.5 KiB
TypeScript
"use client";
|
||
import Link from "next/link";
|
||
import { useState } from "react";
|
||
import { useSnap } from "@/lib/client";
|
||
|
||
/* The first-run checklist on the dashboard. Six things a new room does once, each read from the
|
||
* records rather than remembered: a tick appears because the staff register has a row in it, not
|
||
* because somebody clicked "done". Every row links to the screen that does it. The panel goes on
|
||
* its own once all six are ticked, or once the room is 60 days old with three or more ticked, and
|
||
* an admin can put it away with Dismiss (Facility.checklistDismissed). Not on the phone app. */
|
||
const DAY = 86_400_000;
|
||
|
||
export default function Checklist({ welcome, onDismissWelcome }: { welcome: boolean; onDismissWelcome: () => void }) {
|
||
const { s, mutate } = useSnap();
|
||
const [hidden, setHidden] = useState(false);
|
||
const admin = s.session.role === "Admin";
|
||
const steps: { label: string; done: boolean; href: string; cta: string }[] = [
|
||
{ label: "Add staff, or import the register", done: s.staff.length > 0, href: "/app/settings?tab=data", cta: "Settings › Data" },
|
||
{ label: "Add garments", done: s.catalog.length > 0, href: "/app/settings?tab=data", cta: "Settings › Data" },
|
||
{ label: "Set reorder levels", done: Object.values(s.stock).some((x) => x.reorder !== null && x.reorder !== undefined), href: "/app/stock", cta: "Inventory" },
|
||
{ label: "Record opening stock", done: s.moves.length > 0 || Object.values(s.stock).some((x) => x.opening > 0), href: "/app/stock", cta: "Inventory" },
|
||
{ label: "Issue a garment", done: s.issues.length > 0, href: "/app/issue", cta: "Issue stock" },
|
||
{ label: "Bind a barcode or print labels", done: Object.keys(s.barcodes).length > 0, href: "/app/stock", cta: "Inventory" },
|
||
];
|
||
const done = steps.filter((x) => x.done).length;
|
||
const ageDays = Math.floor((Date.now() - new Date(s.createdAt).getTime()) / DAY);
|
||
const finished = done === steps.length || (ageDays >= 60 && done >= 3);
|
||
if (hidden || s.settings.checklistDismissed || (finished && !welcome)) return null;
|
||
|
||
async function dismiss() {
|
||
setHidden(true);
|
||
onDismissWelcome();
|
||
if (admin) await mutate("settings.checklist", { dismissed: true });
|
||
}
|
||
|
||
return (
|
||
<div className="tc-panel" style={{ marginBottom: "var(--space-6)" }}>
|
||
<div className="tc-panel-head">
|
||
<div>{welcome ? "Welcome to ThreadCount" : "Getting set up"}</div>
|
||
<div className="tc-panel-aside">{done} of {steps.length}</div>
|
||
</div>
|
||
<div className="tc-panel-list">
|
||
{steps.map((st) => (
|
||
<div key={st.label} className="tc-row" style={{ alignItems: "center" }}>
|
||
<div className="tc-row-fig" style={{ width: 32, flex: "none", fontSize: 16, color: st.done ? "var(--color-accent-700)" : "var(--color-neutral-600)" }} aria-label={st.done ? "done" : "to do"}>{st.done ? "✓" : "○"}</div>
|
||
<div className="tc-row-main">
|
||
<div className="tc-row-name" style={{ textDecoration: st.done ? "line-through" : "none", color: st.done ? "var(--color-neutral-700)" : undefined }}>{st.label}</div>
|
||
</div>
|
||
{!st.done && <Link href={st.href} className="btn btn-secondary">{st.cta}</Link>}
|
||
</div>
|
||
))}
|
||
</div>
|
||
{(admin || welcome) && (
|
||
<div className="tc-panel-body" style={{ display: "flex", justifyContent: "flex-end", paddingTop: 0 }}>
|
||
<button className="btn btn-ghost" onClick={dismiss}>{finished ? "Done" : "Dismiss"}</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|