mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 22:32:04 +00:00
feat(admin): module availability matrix
Add an operator-side toggle matrix to the admin assistant section that flips each toggleable module on or off server-wide via PATCH /api/admin/settings/module-availability. Mirrors the assistant-flags matrix: one neutral toggle per module, optimistic flip with a revert on failure, and an authMe invalidation so the resolved module map re-gates the session immediately. New admin.modules.* labels across all six locales; core domains never appear.
This commit is contained in:
1 parent
5d55347dd0
commit
4a1971c0dd
9 files changed
+155
No files matched your search
@@ -0,0 +1,114 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { ToggleLeft } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { MODULE_KEYS, MODULE_REGISTRY } from "@/lib/modules/registry";
|
||||
import type { ModuleKey } from "@/lib/modules/registry";
|
||||
import { SettingsToggle } from "./_shared";
|
||||
import { apiGet, apiPatch } from "@/lib/api/api-fetch";
|
||||
|
||||
/**
|
||||
* v1.18.0 — operator-side panel for server-wide module availability, the
|
||||
* SECOND layer of the two-layer module model. A module turned off here is
|
||||
* off for EVERY account regardless of personal preference — mirroring how
|
||||
* the assistant master flag sits above the per-user coach opt-out.
|
||||
*
|
||||
* Core domains (weight, blood pressure, pulse, medications) are NOT module
|
||||
* keys and never appear here; the gate keeps them always-on structurally.
|
||||
*
|
||||
* UX mirrors the assistant-flags matrix: one toggle per module, calm and
|
||||
* neutral, optimistic flip + server confirm via `useMutation`, with a
|
||||
* failure toast that reverts by re-invalidating the read query.
|
||||
*/
|
||||
|
||||
type AvailabilityMap = Record<ModuleKey, boolean>;
|
||||
|
||||
interface ModuleAvailabilityResponse {
|
||||
availability: AvailabilityMap;
|
||||
}
|
||||
|
||||
function useModuleAvailability() {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.adminModuleAvailability(),
|
||||
queryFn: async () => {
|
||||
return apiGet<ModuleAvailabilityResponse>(
|
||||
"/api/admin/settings/module-availability",
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function useUpdateModuleAvailability() {
|
||||
const client = useQueryClient();
|
||||
const { t } = useTranslations();
|
||||
return useMutation({
|
||||
mutationFn: async (
|
||||
patch: Partial<AvailabilityMap>,
|
||||
): Promise<ModuleAvailabilityResponse> => {
|
||||
return apiPatch<ModuleAvailabilityResponse>(
|
||||
"/api/admin/settings/module-availability",
|
||||
patch,
|
||||
);
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
client.setQueryData(queryKeys.adminModuleAvailability(), data);
|
||||
// The resolved `/api/auth/me` module map depends on this operator
|
||||
// layer; bust it so the operator sees the change within the session.
|
||||
client.invalidateQueries({ queryKey: queryKeys.authMe() });
|
||||
toast.success(t("common.saved"));
|
||||
},
|
||||
onError: (err) => {
|
||||
toast.error(
|
||||
err instanceof Error && err.message
|
||||
? err.message
|
||||
: t("admin.modules.saveError"),
|
||||
);
|
||||
client.invalidateQueries({
|
||||
queryKey: queryKeys.adminModuleAvailability(),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function ModuleAvailabilitySection() {
|
||||
const { t } = useTranslations();
|
||||
const { data } = useModuleAvailability();
|
||||
const mutation = useUpdateModuleAvailability();
|
||||
|
||||
const availability = data?.availability;
|
||||
|
||||
return (
|
||||
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
|
||||
<SettingsCardHeader
|
||||
icon={ToggleLeft}
|
||||
title={t("admin.modules.title")}
|
||||
description={t("admin.modules.description")}
|
||||
/>
|
||||
<div className="mt-4 space-y-4 pl-7">
|
||||
{MODULE_KEYS.map((key) => {
|
||||
const def = MODULE_REGISTRY[key];
|
||||
// Default-available until the read resolves; an explicit `false`
|
||||
// from the operator layer is the only thing that turns it off.
|
||||
const available = availability?.[key] ?? true;
|
||||
return (
|
||||
<SettingsToggle
|
||||
key={key}
|
||||
label={t(def.labelKey)}
|
||||
description={t(def.descriptionKey)}
|
||||
checked={available}
|
||||
onCheckedChange={(checked) =>
|
||||
mutation.mutate({ [key]: checked } as Partial<AvailabilityMap>)
|
||||
}
|
||||
disabled={mutation.isPending}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user