Release / release (push) Has been skipped
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).
71 lines
3.1 KiB
TypeScript
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>
|
|
);
|
|
}
|