Files
threadcount-community/app/app/settings/page.tsx
T
ThreadCount 0e18f3ba68
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 9776046 on 2026-09-16. Licensed under the Functional Source License (FSL-1.1-ALv2).
2026-09-17 04:35:27 +10:00

71 lines
3.1 KiB
TypeScript

"use client";
/* Settings: seven sections in a side list, each a ?tab= of its own so deep links and the help mark
* land on the right one. Old tab names (general, account, locations, activity…) still resolve. */
import { Suspense, useEffect, useState } from "react";
import Link from "next/link";
import { useSearchParams } from "next/navigation";
import { useSnap } from "@/lib/client";
import { PageHead } from "@/components/ui";
import dynamic from "next/dynamic";
import { CSV_TEMPLATES } from "@/lib/csv";
import { SECTIONS, SettingsStyles, resolveSection, type SectionId } from "@/components/settings/common";
import FacilitySection from "@/components/settings/FacilitySection";
import IssuingRules from "@/components/settings/IssuingRules";
import CatalogueSection from "@/components/settings/CatalogueSection";
import PlacesSection from "@/components/settings/PlacesSection";
import PeopleSignIn from "@/components/settings/PeopleSignIn";
import DataAudit from "@/components/settings/DataAudit";
// Plan pulls in Stripe; load it only when the Plan section is open.
const PlanTab = dynamic(() => import("@/components/PlanTab"), { ssr: false });
// useSearchParams needs a Suspense boundary for static rendering.
export default function SettingsPage() {
return <Suspense fallback={null}><SettingsInner /></Suspense>;
}
function SettingsInner() {
const { s, isAdmin } = useSnap();
const sp = useSearchParams();
const planShown = isAdmin && !!s.plan?.live && !s.demo;
// #hash forms from old links (/app/settings#account).
const [hash, setHash] = useState("");
useEffect(() => {
const read = () => setHash(window.location.hash.replace("#", ""));
read();
window.addEventListener("hashchange", read);
return () => window.removeEventListener("hashchange", read);
}, []);
const tabParam = sp.get("tab");
const importParam = sp.get("import") || "";
const importKind = CSV_TEMPLATES[importParam] ? importParam : undefined;
const resolved = resolveSection(tabParam || hash || (importKind ? "data" : ""));
let section: SectionId = resolved.section;
if (section === "plan" && !planShown) section = "facility";
const sections = SECTIONS.filter((x) => x.id !== "plan" || planShown);
return (
<section>
<SettingsStyles />
<PageHead title="Settings" />
<div className="tc-set">
<nav aria-label="Settings sections" className="tc-set-nav">
{sections.map((x) => (
<Link key={x.id} href={`/app/settings?tab=${x.id}`} scroll={false} aria-current={x.id === section ? "page" : undefined}>{x.label}</Link>
))}
</nav>
<div className="tc-set-body" key={section}>
{section === "facility" && <FacilitySection />}
{section === "issuing" && <IssuingRules />}
{section === "catalogue" && <CatalogueSection />}
{section === "places" && <PlacesSection />}
{section === "people" && <PeopleSignIn />}
{section === "data" && <DataAudit importKind={importKind} scrollAudit={resolved.audit} />}
{section === "plan" && planShown && <PlanTab />}
</div>
</div>
</section>
);
}