Files
threadcount-community/components/screens/NotificationSettings.tsx
T
ThreadCount f72f0626b0
Release / release (push) Has been skipped
ThreadCount Community edition
Uniform stock management for healthcare linen rooms: the coordinator app, the phone counter and the staff app, for your own server. Built from 794bab5 on 2026-09-16. Licensed under the Functional Source License (FSL-1.1-ALv2).
2026-09-17 05:49:16 +10:00

114 lines
5.1 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
/* The Notifications section of Account: one switch per kind of thing this person can be told about.
*
* Two rules shape the whole thing.
*
* The phone is asked for permission when somebody turns the FIRST switch on, never at launch —
* Android 13+ policy, and plain manners. A wearer who never wants to be told is never asked.
*
* "Something waiting on you" is offered to anybody a request can be addressed to, which is not the
* same as "managers": the linen room can re-address one to somebody who manages nobody, and that
* person most needs telling — and, having been told, is entitled to silence it.
*/
import { useEffect, useState } from "react";
import { MError, MSwitchRow } from "@/components/m";
import { N600 } from "@/components/staffui";
import { useStaff } from "@/lib/staffclient";
import { askForPush, pushShell, rememberedToken, type PushShell } from "@/lib/staffpush";
export type NotifyPrefs = { approved: boolean; ready: boolean; round: boolean; kitcheck: boolean; waiting: boolean };
type Key = keyof NotifyPrefs;
const KINDS: { key: Key; title: string }[] = [
{ key: "approved", title: "Approved or declined" },
{ key: "ready", title: "Ready to collect" },
{ key: "round", title: "On the ward round" },
{ key: "kitcheck", title: "Kit check opens" },
];
export default function NotificationSettings({ prefs, configured }: { prefs: NotifyPrefs; configured: boolean }) {
const { me, counts, mutate } = useStaff();
const [on, setOn] = useState<NotifyPrefs>(prefs);
const [err, setErr] = useState("");
const [busy, setBusy] = useState<Key | null>(null);
/* Settled after mount: the server cannot know whether this is the app or a browser, and a line
* about where notifications arrive that changed on hydration would be worse than one that waits
* a beat. "old" is a shell from before the notification bridge existed — see lib/staffpush.ts. */
const [shell, setShell] = useState<PushShell>("none");
useEffect(() => { setShell(pushShell()); }, []);
const bridge = shell === "ready";
/* The waiting switch belongs to whoever a request can be ADDRESSED to, which is what the queue,
* the badge and the Team tab all key on. `counts.team` is the wrong number: it adds the bags on
* a ward desk, so a clerk who manages nobody was offered a switch for a notification the sender
* only ever writes to `r.managerId` — it could never fire, and it vanished off their Account
* screen the moment they signed for the bags. */
const rows = me.isManager || counts.approvals > 0
? [...KINDS, { key: "waiting" as Key, title: "Something waiting on you" }]
: KINDS;
async function toggle(k: Key) {
if (busy) return;
const before = on;
const next = { ...on, [k]: !on[k] };
setOn(next);
setErr("");
setBusy(k);
const r = await mutate("notify.prefs", { [k]: next[k] });
if (!r.ok) {
setOn(before);
setErr(r.error);
setBusy(null);
return;
}
/* Ask the phone after the preference is saved, not before: somebody who says no to Android
* still meant to turn the switch on, and their answer is kept. In a browser there is no bridge
* and nothing is asked — the preference is the server's either way, and it governs the phone
* whenever they next open the app.
*
* The question is "is this phone registered", not "is this the first switch". The old test —
* nothing on before this tap — could never be true: every switch defaults to ON, so a fresh
* account arrives here with all of them set, and for a wearer `waiting` is on with no row to
* turn it off. Android was therefore never asked, no device row was ever written, and every
* send found nobody, on a server with a key installed and every switch saying yes. A remembered
* token is the honest answer to whether this phone has ever handed one back. */
if (next[k] && bridge && configured && !rememberedToken()) {
const ask = await askForPush();
if (ask.ok) await mutate("push.register", { token: ask.token, platform: "android" });
}
setBusy(null);
}
return (
<>
<MError msg={err} onDismiss={() => setErr("")} />
{rows.map((r) => (
<MSwitchRow
key={r.key}
title={r.title}
on={on[r.key]}
disabled={!configured || busy !== null}
onToggle={() => void toggle(r.key)}
/>
))}
{!configured ? (
<div style={{ fontSize: 13, color: N600, padding: "12px 0 0", lineHeight: 1.5 }}>
Notifications arent set up on this server.
</div>
) : shell === "old" ? (
// In the app, on a build from before notifications existed. Saying "they arrive in the app"
// to somebody who is standing in it would be a line that explains nothing.
<div style={{ fontSize: 13, color: N600, padding: "12px 0 0", lineHeight: 1.5 }}>
Update the ThreadCount Staff app to turn these on.
</div>
) : !bridge ? (
<div style={{ fontSize: 13, color: N600, padding: "12px 0 0", lineHeight: 1.5 }}>
Notifications come to the ThreadCount Staff app on your phone.
</div>
) : null}
</>
);
}