import { ArrowDown, ArrowUp, RotateCcw, Save } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; import { Button, DismissibleAlert, FormSection, IconButton, i18nMessage, LoadingFrame, SegmentedControl, ToggleSwitch, usePlatformLanguage, useUnsavedDraftGuard, type ApiSettings, type AuthInfo } from "@govoplan/core-webui"; import { loadEffectiveQuickAccess, loadQuickAccessCatalogue, loadQuickAccessProfile, saveQuickAccessProfile, type EffectiveQuickAccess, type QuickAccessCatalogue, type QuickAccessPreference, type QuickAccessProfile } from "../../api/quickAccess"; type ProfileScope = "system" | "tenant" | "me"; type Draft = { categories: Record; tools: Record; }; type AvailabilityMode = "inherit" | "available" | "blocked" | "forced"; type EffectiveProvenance = { state: "available" | "forced" | "blocked"; availabilitySource: "module" | "system" | "tenant" | "user"; orderSource: "module" | "system" | "tenant" | "user"; }; const EMPTY_DRAFT: Draft = { categories: {}, tools: {} }; export default function QuickAccessSettingsPanel({ settings, auth, scope, canWrite }: { settings: ApiSettings; auth: AuthInfo; scope: ProfileScope; canWrite: boolean; }) { const [catalogue, setCatalogue] = useState(null); const [effective, setEffective] = useState(null); const [profile, setProfile] = useState(null); const [draft, setDraft] = useState(EMPTY_DRAFT); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [message, setMessage] = useState(""); const [messageTone, setMessageTone] = useState<"success" | "warning">("success"); const { translateText } = usePlatformLanguage(); const isPersonal = scope === "me"; const dirty = useMemo( () => profile !== null && JSON.stringify(draft) !== JSON.stringify({ categories: profile.category_preferences, tools: profile.tool_preferences }), [draft, profile] ); useEffect(() => { void load(); }, [auth.user.account_id, scope, settings.accessToken, settings.apiBaseUrl, settings.apiKey]); useUnsavedDraftGuard({ dirty, onSave: save, onDiscard: reset }); async function load() { setLoading(true); setMessage(""); const includeAll = scope !== "me"; try { const [nextCatalogue, nextProfile, nextEffective] = await Promise.all([ loadQuickAccessCatalogue(settings, includeAll), loadQuickAccessProfile(settings, scope), loadEffectiveQuickAccess(settings, includeAll) ]); setCatalogue(nextCatalogue); setProfile(nextProfile); setEffective(nextEffective); setDraft({ categories: nextProfile.category_preferences, tools: nextProfile.tool_preferences }); } catch (reason) { setMessageTone("warning"); setMessage(reason instanceof Error ? reason.message : "i18n:govoplan-quick-access.load_failed"); } finally { setLoading(false); } } async function save(): Promise { if (!profile || !canWrite) return false; setSaving(true); setMessage(""); try { const next = await saveQuickAccessProfile( settings, scope, profile, draft.categories, draft.tools ); setProfile(next); setDraft({ categories: next.category_preferences, tools: next.tool_preferences }); setMessageTone("success"); setMessage("i18n:govoplan-quick-access.saved"); window.dispatchEvent(new CustomEvent("govoplan:quick-access-changed")); return true; } catch (reason) { setMessageTone("warning"); setMessage(reason instanceof Error ? reason.message : "i18n:govoplan-quick-access.save_failed"); return false; } finally { setSaving(false); } } function reset() { if (!profile) return; setDraft({ categories: profile.category_preferences, tools: profile.tool_preferences }); } function setPreference( group: "categories" | "tools", id: string, preference: QuickAccessPreference | null ) { setDraft((current) => { const nextGroup = { ...current[group] }; if (preference === null) delete nextGroup[id]; else nextGroup[id] = preference; return { ...current, [group]: nextGroup }; }); } function move( group: "categories" | "tools", ids: string[], id: string, direction: -1 | 1 ) { const currentIndex = ids.indexOf(id); const targetIndex = currentIndex + direction; if (currentIndex < 0 || targetIndex < 0 || targetIndex >= ids.length) return; const reordered = [...ids]; [reordered[currentIndex], reordered[targetIndex]] = [reordered[targetIndex], reordered[currentIndex]]; setDraft((current) => ({ ...current, [group]: Object.fromEntries(reordered.map((itemId, index) => [ itemId, { ...(current[group][itemId] ?? {}), order: (index + 1) * 10 } ])) })); } const categories = useMemo( () => [...(catalogue?.categories ?? [])].sort((left, right) => (draft.categories[left.id]?.order ?? left.order) - (draft.categories[right.id]?.order ?? right.order) ), [catalogue, draft.categories] ); const tools = useMemo( () => [...(catalogue?.tools ?? [])].sort((left, right) => { if (left.category_id !== right.category_id) return left.category_id.localeCompare(right.category_id); return (draft.tools[left.id]?.order ?? left.order) - (draft.tools[right.id]?.order ?? right.order); }), [catalogue, draft.tools] ); const lockedCategories = new Map( (effective?.categories ?? []).filter((item) => item.locked_by).map((item) => [item.id, item.locked_by]) ); const lockedTools = new Map( (effective?.categories ?? []).flatMap((category) => category.tools).filter((item) => item.locked_by).map((item) => [item.id, item.locked_by]) ); const categoryProvenance = new Map( (effective?.categories ?? []).map((item) => [item.id, effectiveProvenance(item)]) ); const toolProvenance = new Map( (effective?.categories ?? []).flatMap((category) => category.tools.map((item) => [item.id, effectiveProvenance(item)] as const) ) ); const categoryIds = categories.map((item) => item.id); return (

i18n:govoplan-quick-access.settings_title

{isPersonal ? "i18n:govoplan-quick-access.personal_help" : "i18n:govoplan-quick-access.admin_help"}

{message ? {message} : null} {profile && (profile.stale_category_ids.length || profile.stale_tool_ids.length) ? ( i18n:govoplan-quick-access.stale_preferences_retained ) : null} {categories.map((category, index) => ( setPreference("categories", category.id, value)} onMoveUp={() => move("categories", categoryIds, category.id, -1)} onMoveDown={() => move("categories", categoryIds, category.id, 1)} first={index === 0} last={index === categories.length - 1} /> ))} {categories.flatMap((category) => { const categoryTools = tools.filter((tool) => tool.category_id === category.id); const toolIds = categoryTools.map((tool) => tool.id); return categoryTools.map((tool, index) => ( setPreference("tools", tool.id, value)} onMoveUp={() => move("tools", toolIds, tool.id, -1)} onMoveDown={() => move("tools", toolIds, tool.id, 1)} first={index === 0} last={index === categoryTools.length - 1} /> )); })} {!tools.length ?

i18n:govoplan-quick-access.no_registered_tools

: null}
); } function PreferenceRow({ id, label, description, preference, defaultEnabled = true, isPersonal, lockedBy, provenance, disabled, onChange, onMoveUp, onMoveDown, first, last }: { id: string; label: string; description: string; preference?: QuickAccessPreference; defaultEnabled?: boolean; isPersonal: boolean; lockedBy?: string | null; provenance?: EffectiveProvenance; disabled: boolean; onChange: (value: QuickAccessPreference | null) => void; onMoveUp: () => void; onMoveDown: () => void; first: boolean; last: boolean; }) { const mode = preferenceMode(preference); const rowDisabled = disabled || Boolean(lockedBy); return (
{label} {description} {lockedBy ? ( {i18nMessage("i18n:govoplan-quick-access.locked_by_value", { value0: lockedBy })} ) : null} {provenance ? ( {i18nMessage("i18n:govoplan-quick-access.effective_provenance", { value0: provenance.state, value1: provenance.availabilitySource, value2: provenance.orderSource })} ) : null}
{isPersonal ? ( onChange({ ...preference, enabled })} /> ) : ( onChange(preferenceForMode(value, preference))} options={[ { id: "inherit", label: "i18n:govoplan-quick-access.inherit" }, { id: "available", label: "i18n:govoplan-quick-access.available" }, { id: "blocked", label: "i18n:govoplan-quick-access.blocked" }, { id: "forced", label: "i18n:govoplan-quick-access.forced" } ]} /> )}
} variant="ghost" disabled={disabled || first} onClick={onMoveUp} /> } variant="ghost" disabled={disabled || last} onClick={onMoveDown} />
); } function effectiveProvenance(item: { availability_state: "available" | "forced" | "blocked"; availability_source: "module" | "system" | "tenant" | "user"; order_source: "module" | "system" | "tenant" | "user"; }): EffectiveProvenance { return { state: item.availability_state, availabilitySource: item.availability_source, orderSource: item.order_source }; } function preferenceMode(preference?: QuickAccessPreference): AvailabilityMode { if (!preference || preference.enabled === null || preference.enabled === undefined) return "inherit"; if (preference.enabled === false) return "blocked"; return preference.forced ? "forced" : "available"; } function editableConstraintSource( scope: ProfileScope, source: string | null | undefined ): string | null { if (!source || scope === "system") return null; if (scope === "tenant") return source === "system" ? source : null; return source; } function preferenceForMode( mode: AvailabilityMode, current?: QuickAccessPreference ): QuickAccessPreference | null { if (mode === "inherit") return current?.order === undefined ? null : { order: current.order }; if (mode === "blocked") return { ...current, enabled: false, forced: false }; if (mode === "forced") return { ...current, enabled: true, forced: true }; return { ...current, enabled: true, forced: false }; }