344b1701dd
Uniform stock management for healthcare linen rooms. Licensed under the GNU AGPL v3.
111 lines
6.4 KiB
TypeScript
111 lines
6.4 KiB
TypeScript
"use client";
|
|
/* Settings — only what the app itself controls. Everything else about the facility lives on the
|
|
desktop, so there is one place a setting can be wrong rather than two. */
|
|
import { useEffect, useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { useSnap } from "@/lib/client";
|
|
import { locTree } from "@/lib/compute";
|
|
import { clearAllCounts } from "@/lib/opencount";
|
|
import { INK, MBody, MError, MRow, MRule, MSection, MStepper, MTop } from "@/components/m";
|
|
|
|
const BEEP_KEY = "tc.beep";
|
|
|
|
export default function MSettings() {
|
|
const { s, isAdmin, mutate, busy } = useSnap();
|
|
const router = useRouter();
|
|
const [beep, setBeep] = useState(true);
|
|
const [gate, setGate] = useState(s.settings.varianceReason);
|
|
const [err, setErr] = useState("");
|
|
|
|
useEffect(() => { try { setBeep(localStorage.getItem(BEEP_KEY) !== "0"); } catch { /* blocked store */ } }, []);
|
|
const toggleBeep = () => {
|
|
const next = !beep;
|
|
setBeep(next);
|
|
try { localStorage.setItem(BEEP_KEY, next ? "1" : "0"); } catch { /* blocked store */ }
|
|
};
|
|
|
|
const saveGate = async (n: number) => {
|
|
setGate(n);
|
|
const r = await mutate("settings.update", { varianceReason: n });
|
|
if (!r.ok) { setErr(r.error); setGate(s.settings.varianceReason); }
|
|
};
|
|
|
|
const signOut = async () => {
|
|
// Their part-counted shelves go with them. The tally is keyed per person, so what is left
|
|
// behind can never be read by the next signed-in user — but it is theirs, it is on a phone
|
|
// that is passed around a linen room, and nothing would ever clear it again once they have
|
|
// gone. Done before the logout POST so a failed request still leaves the device tidy.
|
|
clearAllCounts(s.session.userId);
|
|
await fetch("/api/auth/logout", { method: "POST" });
|
|
// /m/login, not /auth: /auth is the website's two-pane desktop sign-in, and landing on it
|
|
// inside a phone app is how you make someone think the app is broken. A full navigation
|
|
// rather than router.push, because the session cookie has just been cleared and every page
|
|
// behind it is server-rendered.
|
|
window.location.replace("/m/login");
|
|
};
|
|
|
|
const locs = locTree(s).length;
|
|
|
|
return (
|
|
<>
|
|
<MTop title="Settings" back />
|
|
<MRule />
|
|
<MError msg={err} onDismiss={() => setErr("")} />
|
|
<MBody>
|
|
<div style={{ padding: "20px 16px 22px", borderBottom: "2px solid " + INK }}>
|
|
<div style={{ fontFamily: "var(--font-heading)", fontWeight: 800, fontSize: 26, letterSpacing: "-0.03em" }}>{s.session.name}</div>
|
|
<div style={{ fontSize: 14, color: "var(--color-neutral-700)", marginTop: 6 }}>
|
|
{[s.session.title || s.session.role, s.session.email].filter(Boolean).join(" · ")}
|
|
</div>
|
|
</div>
|
|
|
|
<MSection label="Site" />
|
|
<MRow title={s.settings.facility} sub={s.settings.location} right={<span style={{ fontSize: 13, color: "var(--color-neutral-600)" }}>Desktop</span>} />
|
|
<MRow title="Locations" sub={locs ? `${locs} ${locs === 1 ? "shelf" : "shelves"} and bays set up` : "None set up yet"} right={<span style={{ fontSize: 13, color: "var(--color-neutral-600)" }}>Desktop</span>} />
|
|
|
|
<MSection label="Counting" />
|
|
<MRow title="Beep and buzz on a scan" sub="This device only"
|
|
right={
|
|
<button onClick={toggleBeep} role="switch" aria-checked={beep} aria-label="Beep and buzz on a scan"
|
|
style={{ minWidth: 72, minHeight: 44, border: "2px solid " + INK, background: beep ? INK : "transparent", color: beep ? "var(--color-bg)" : INK, fontFamily: "var(--font-heading)", fontWeight: 800, fontSize: 13, letterSpacing: "0.08em", textTransform: "uppercase", cursor: "pointer" }}>
|
|
{beep ? "On" : "Off"}
|
|
</button>
|
|
} />
|
|
<MRow title="Reason required at" sub={isAdmin ? "A count gap this big has to say why" : "Set by an administrator"}
|
|
right={isAdmin
|
|
? <MStepper n={gate} onChange={saveGate} min={1} max={99} />
|
|
: <span style={{ fontFamily: "var(--font-heading)", fontWeight: 800, fontSize: 19 }}>{gate}</span>} />
|
|
|
|
{/* Deleting an account has to be reachable from inside the app, not only from a web page
|
|
somebody has to know exists — this is the app that created the facility in the first
|
|
place, and it is a Play requirement besides. These rows go to the site's own pages
|
|
rather than a second deletion screen: there is one account-deletion flow, and it is
|
|
the one on threadcount.tech.
|
|
|
|
Absolute, and on the web they open in a new tab. On a phone they cannot: this shell
|
|
registers no browser plugin, so nothing here is able to hand a URL to Chrome, and the
|
|
page loads over the top of the counter with the site's own nav and no tab bar. The row
|
|
says as much before the tap, and the hardware back button comes straight back. The other
|
|
two ways out were worse: a row that does nothing when tapped, or no deletion route in
|
|
the app at all. Somebody who signs in rather than signing up never passes the links on
|
|
the create-account screen, so this is the only place the signed-in counter app names
|
|
them at all. */}
|
|
<MSection label="Your account and your data" />
|
|
<MRow href="https://threadcount.tech/delete-account" external mark="ink" title="Delete your account" sub="How to do it, and exactly what goes with it" />
|
|
<MRow href="https://threadcount.tech/privacy" external mark="ink" title="Privacy policy" sub="What ThreadCount stores, and what it never does" />
|
|
<MRow href="https://threadcount.tech/terms" external mark="ink" title="Terms of use" sub="What you and ThreadCount each agree to" />
|
|
|
|
<div style={{ padding: 16 }}>
|
|
<button onClick={signOut} disabled={busy}
|
|
style={{ width: "100%", minHeight: 64, border: "2px solid var(--color-accent)", background: "transparent", color: "var(--color-accent-700)", fontFamily: "var(--font-heading)", fontWeight: 800, fontSize: 14, letterSpacing: "0.08em", textTransform: "uppercase", textAlign: "left", padding: "0 20px", cursor: "pointer" }}>
|
|
Sign out
|
|
</button>
|
|
</div>
|
|
<p style={{ padding: "0 16px 26px", fontSize: 13.5, color: "var(--color-neutral-600)", lineHeight: 1.6 }}>
|
|
Ordering, reports, the catalogue and the staff register are all on the desktop at threadcount.tech.
|
|
</p>
|
|
</MBody>
|
|
</>
|
|
);
|
|
}
|