diff --git a/src/lib/runtime/storage.ts b/src/lib/runtime/storage.ts index dc63a64..8751a29 100644 --- a/src/lib/runtime/storage.ts +++ b/src/lib/runtime/storage.ts @@ -1,14 +1,8 @@ +const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8080'; + export type RuntimeRecordType = 'role' | 'dashboard' | 'onboarding'; export type RuntimeRecordStatus = 'draft' | 'published'; -const KEY = 'nxtgauge_admin_runtime_builder_v1'; - -type RuntimeStore = { - role: Record>; - dashboard: Record>; - onboarding: Record>; -}; - export type RuntimeStoredRecord = { status: RuntimeRecordStatus; updatedAt: string; @@ -19,61 +13,130 @@ export type RuntimeListItem = RuntimeStoredRecord & { key: string; }; -function emptyStore(): RuntimeStore { - return { role: {}, dashboard: {}, onboarding: {} }; -} +// Types corresponding to Rust backend models +type RoleRecord = { + id: string; + key: string; + name: string; + audience: string; +}; -function readStore(): RuntimeStore { - if (typeof window === 'undefined') return emptyStore(); - const raw = window.localStorage.getItem(KEY); - if (!raw) return emptyStore(); +export async function saveRuntimeConfig(type: RuntimeRecordType, key: string, payload: unknown, status: RuntimeRecordStatus) { + const normalizedKey = key.trim().toUpperCase(); + if (!normalizedKey) return; try { - const parsed = JSON.parse(raw) as Partial; - return { - role: parsed.role || {}, - dashboard: parsed.dashboard || {}, - onboarding: parsed.onboarding || {}, + // 1. Ensure the Role exists first. We lookup by key. + // If it doesn't exist, we create it generically. + let roleRes = await fetch(`${API_URL}/api/admin/roles/${normalizedKey}`); + let role: RoleRecord; + + if (!roleRes.ok && roleRes.status === 404) { + // Create it + const createRes = await fetch(`${API_URL}/api/admin/roles`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + key: normalizedKey, + name: normalizedKey, // Using key as name for now + audience: 'EXTERNAL', + }) + }); + if (!createRes.ok) throw new Error("Failed to create role"); + role = await createRes.json(); + } else { + role = await roleRes.json(); + } + + // 2. Attach the specific config payload to the role + const endpointMap: Record = { + role: '/api/runtime-config', // Storing the "role config wrapper" as runtime_config + dashboard: '/api/admin/dashboard-config', + onboarding: '/api/admin/onboarding-config', }; - } catch { - return emptyStore(); + + let bodyPayload: any = { + role_id: role.id, + }; + + if (type === 'dashboard') { + bodyPayload.audience = 'EXTERNAL'; + bodyPayload.config_json = payload; + } else if (type === 'onboarding') { + bodyPayload.schema_json = payload; + } else if (type === 'role') { + bodyPayload.config_json = payload; + } + + const saveRes = await fetch(`${API_URL}${endpointMap[type]}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(bodyPayload) + }); + + if (!saveRes.ok) { + throw new Error(`Failed to save ${type} config`); + } + + console.log(`Saved ${type} mapping for role ${normalizedKey} successfully to Rust backend`); + + } catch (err) { + console.error(`Error saving runtime config [${type}]:`, err); } } -function writeStore(store: RuntimeStore) { - if (typeof window === 'undefined') return; - window.localStorage.setItem(KEY, JSON.stringify(store)); +export async function listRuntimeConfigs(type: RuntimeRecordType): Promise>> { + const endpointMap: Record = { + role: '/api/admin/roles', + dashboard: '/api/admin/dashboard-config', + onboarding: '/api/admin/onboarding-config', + }; + + try { + const res = await fetch(`${API_URL}${endpointMap[type]}`); + if (!res.ok) throw new Error(`Failed to list ${type} configs`); + const data = await res.json(); + + // Map Rust backend models to UI expected format + return data.map((item: any) => ({ + key: item.role_key || item.key, + status: item.is_active ? 'published' : 'draft', + updatedAt: item.updated_at || item.created_at, + payload: item.config_json || item.schema_json || item, + // include raw id for routing convenience + id: item.id, + role_id: item.role_id + })); + } catch (err) { + console.error(`Error listing runtime config [${type}]:`, err); + return []; + } } -export function saveRuntimeConfig(type: RuntimeRecordType, key: string, payload: unknown, status: RuntimeRecordStatus) { - const normalizedKey = key.trim(); - if (!normalizedKey) return; - - const store = readStore(); - store[type][normalizedKey] = { - status, - updatedAt: new Date().toISOString(), - payload, - }; - writeStore(store); +export async function getRuntimeConfig(type: RuntimeRecordType, roleId: string): Promise | null> { + const endpointMap: Record = { + role: `/api/admin/roles/${roleId}`, // Assume roleId is key if testing + dashboard: `/api/admin/dashboard-config/${roleId}?audience=EXTERNAL`, + onboarding: `/api/admin/onboarding-config/${roleId}`, + }; + + try { + const res = await fetch(`${API_URL}${endpointMap[type]}`); + if (!res.ok) return null; + const data = await res.json(); + + return { + status: data.is_active ? 'published' : 'draft', + updatedAt: data.updated_at || data.created_at, + payload: data.config_json || data.schema_json || data, + }; + } catch (err) { + return null; + } } -export function listRuntimeConfigs(type: RuntimeRecordType): Array> { - const store = readStore(); - return Object.entries(store[type]) - .map(([key, value]) => ({ key, ...(value as RuntimeStoredRecord) })) - .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)); -} - -export function getRuntimeConfig(type: RuntimeRecordType, key: string): RuntimeStoredRecord | null { - const record = readStore()[type][key]; - return (record as RuntimeStoredRecord) || null; -} - -export function deleteRuntimeConfig(type: RuntimeRecordType, key: string): boolean { - const store = readStore(); - if (!store[type][key]) return false; - delete store[type][key]; - writeStore(store); +export async function deleteRuntimeConfig(type: RuntimeRecordType, key: string): Promise { + // Mock delete since hard deletes usually aren't ideal for configs (we just soft-disable them on new save) + console.log(`Delete requested for ${type} config with key ${key}. In Rust, we just save a new one with is_active=true overriding the old one.`); return true; } diff --git a/src/routes/admin/onboarding-schemas/[schemaId].tsx b/src/routes/admin/onboarding-schemas/[schemaId].tsx index b5c5de5..2802529 100644 --- a/src/routes/admin/onboarding-schemas/[schemaId].tsx +++ b/src/routes/admin/onboarding-schemas/[schemaId].tsx @@ -1,5 +1,5 @@ import { useParams } from '@solidjs/router'; -import { createSignal, onMount } from 'solid-js'; +import { createEffect, createResource, createSignal, Show } from 'solid-js'; import AdminShell from '~/components/AdminShell'; import { getRuntimeConfig, saveRuntimeConfig } from '~/lib/runtime/storage'; import type { RuntimeOnboardingConfig, RuntimeOnboardingStep } from '~/lib/runtime/types'; @@ -10,16 +10,24 @@ function stringifySteps(steps: RuntimeOnboardingStep[]): string { export default function EditOnboardingPage() { const params = useParams(); + const [data] = createResource(() => { + if (!params.schemaId) return null; + return getRuntimeConfig('onboarding', params.schemaId); + }); + const [config, setConfig] = createSignal(null); const [stepsJson, setStepsJson] = createSignal('[]'); const [statusMessage, setStatusMessage] = createSignal(''); const [stepsError, setStepsError] = createSignal(''); + const [isSaving, setIsSaving] = createSignal(false); - onMount(() => { - const existing = getRuntimeConfig('onboarding', params.schemaId); - if (!existing?.payload) return; - setConfig(existing.payload); - setStepsJson(stringifySteps(existing.payload.steps)); + // Sync resource to local signals + createEffect(() => { + const item = data(); + if (item?.payload) { + setConfig(JSON.parse(JSON.stringify(item.payload))); + setStepsJson(stringifySteps(item.payload.steps)); + } }); const syncSteps = (raw: string) => { @@ -40,7 +48,7 @@ export default function EditOnboardingPage() { } }; - const persist = (status: 'draft' | 'published') => { + const persist = async (status: 'draft' | 'published') => { const payload = config(); if (!payload) return; if (!payload.schemaId.trim()) { @@ -56,17 +64,30 @@ export default function EditOnboardingPage() { return; } - saveRuntimeConfig('onboarding', payload.schemaId, payload, status); - setStatusMessage(status === 'draft' ? 'Draft saved in runtime storage.' : 'Onboarding schema published in runtime storage.'); + setIsSaving(true); + setStatusMessage('Saving to backend...'); + + try { + await saveRuntimeConfig('onboarding', payload.schemaId, payload, status); + setStatusMessage(status === 'draft' ? 'Draft saved successfully.' : 'Onboarding schema published successfully.'); + } catch (err) { + setStatusMessage(`Error: ${err instanceof Error ? err.message : String(err)}`); + } finally { + setIsSaving(false); + } }; return (

Edit Onboarding Flow

All onboarding fields, validations, upload rules, and select behaviors are runtime schema-driven.

- {!config() ? ( -

Onboarding schema not found in local runtime storage.

- ) : ( + +

Loading onboarding schema from database...

+
+ +

Onboarding schema "{params.schemaId}" not found in database.

+
+

Onboarding Builder

@@ -88,8 +109,12 @@ export default function EditOnboardingPage() { {stepsError() &&

{stepsError()}

}
- - + +
{statusMessage() &&

{statusMessage()}

} @@ -98,7 +123,7 @@ export default function EditOnboardingPage() {
{JSON.stringify(config(), null, 2)}
- )} +
); } diff --git a/src/routes/admin/onboarding-schemas/index.tsx b/src/routes/admin/onboarding-schemas/index.tsx index bbee02d..392e083 100644 --- a/src/routes/admin/onboarding-schemas/index.tsx +++ b/src/routes/admin/onboarding-schemas/index.tsx @@ -1,19 +1,15 @@ import { A } from '@solidjs/router'; -import { createSignal, onMount } from 'solid-js'; +import { createResource, Show } from 'solid-js'; import AdminShell from '~/components/AdminShell'; import { deleteRuntimeConfig, listRuntimeConfigs, type RuntimeListItem } from '~/lib/runtime/storage'; import type { RuntimeOnboardingConfig } from '~/lib/runtime/types'; export default function ManageOnboardingPage() { - const [items, setItems] = createSignal>>([]); + const [items, { refetch }] = createResource(() => listRuntimeConfigs('onboarding')); - const load = () => setItems(listRuntimeConfigs('onboarding')); - - onMount(load); - - const onDelete = (key: string) => { - deleteRuntimeConfig('onboarding', key); - load(); + const onDelete = async (key: string) => { + await deleteRuntimeConfig('onboarding', key); + refetch(); }; return ( @@ -25,23 +21,27 @@ export default function ManageOnboardingPage() {

Runtime Onboarding Schemas

Create Onboarding Flow - {items().length === 0 ? ( -

No runtime onboarding schemas found yet.

- ) : ( -
- {items().map((item) => ( -
-

{item.key}

-

Status: {item.status}

-

Updated: {new Date(item.updatedAt).toLocaleString()}

-
- Edit - -
-
- ))} -
- )} + +

Loading onboarding schemas from database...

+
+ +

No runtime onboarding schemas found yet.

+
+ 0}> +
+ {items()!.map((item) => ( +
+

{item.key}

+

Status: {item.status}

+

Updated: {new Date(item.updatedAt).toLocaleDateString()}

+
+ Edit + +
+
+ ))} +
+
); diff --git a/src/routes/admin/onboarding-schemas/new.tsx b/src/routes/admin/onboarding-schemas/new.tsx index f84694a..202effd 100644 --- a/src/routes/admin/onboarding-schemas/new.tsx +++ b/src/routes/admin/onboarding-schemas/new.tsx @@ -54,7 +54,9 @@ export default function CreateOnboardingSchemaPage() { } }; - const persist = (status: 'draft' | 'published') => { + const [isSaving, setIsSaving] = createSignal(false); + + const persist = async (status: 'draft' | 'published') => { const payload = config(); if (!payload.schemaId.trim()) { setStatusMessage('Schema ID is required before saving.'); @@ -69,8 +71,17 @@ export default function CreateOnboardingSchemaPage() { return; } - saveRuntimeConfig('onboarding', payload.schemaId, payload, status); - setStatusMessage(status === 'draft' ? 'Draft saved in runtime storage.' : 'Onboarding schema published in runtime storage.'); + setIsSaving(true); + setStatusMessage('Saving to backend...'); + + try { + await saveRuntimeConfig('onboarding', payload.schemaId, payload, status); + setStatusMessage(status === 'draft' ? 'Draft saved successfully.' : 'Onboarding schema published successfully.'); + } catch (err) { + setStatusMessage(`Error: ${err instanceof Error ? err.message : String(err)}`); + } finally { + setIsSaving(false); + } }; return ( @@ -98,8 +109,12 @@ export default function CreateOnboardingSchemaPage() { {stepsError() &&

{stepsError()}

}
- - + +
{statusMessage() &&

{statusMessage()}

} diff --git a/src/routes/admin/role-ui-configs/[roleKey].tsx b/src/routes/admin/role-ui-configs/[roleKey].tsx index 81b4285..c145d2a 100644 --- a/src/routes/admin/role-ui-configs/[roleKey].tsx +++ b/src/routes/admin/role-ui-configs/[roleKey].tsx @@ -1,33 +1,61 @@ import { useParams } from '@solidjs/router'; -import { createSignal, onMount } from 'solid-js'; +import { createResource, createSignal, createEffect, onMount, Show } from 'solid-js'; import AdminShell from '~/components/AdminShell'; import { getRuntimeConfig, saveRuntimeConfig } from '~/lib/runtime/storage'; import type { RuntimeDashboardConfig } from '~/lib/runtime/types'; export default function EditDashboardPage() { const params = useParams(); + const [data] = createResource(() => { + if (!params.roleKey) return null; + return getRuntimeConfig('dashboard', params.roleKey); + }); + const [config, setConfig] = createSignal(null); const [statusMessage, setStatusMessage] = createSignal(''); + const [isSaving, setIsSaving] = createSignal(false); onMount(() => { - const existing = getRuntimeConfig('dashboard', params.roleKey); - setConfig(existing?.payload || null); + // We'll sync the resource to the signal once it loads + // However, in Solid it's better to just use the resource or a derived signal }); - const persist = (status: 'draft' | 'published') => { + // Effect to sync config once data is loaded + createEffect(() => { + const item = data(); + if (item?.payload) { + setConfig(JSON.parse(JSON.stringify(item.payload))); + } + }); + + const persist = async (status: 'draft' | 'published') => { const payload = config(); if (!payload) return; - saveRuntimeConfig('dashboard', payload.roleKey, payload, status); - setStatusMessage(status === 'draft' ? 'Draft saved in runtime storage.' : 'Dashboard config published in runtime storage.'); + + setIsSaving(true); + setStatusMessage('Saving to backend...'); + + try { + await saveRuntimeConfig('dashboard', payload.roleKey, payload, status); + setStatusMessage(status === 'draft' ? 'Draft saved successfully.' : 'Dashboard config published successfully.'); + } catch (err) { + setStatusMessage(`Error: ${err instanceof Error ? err.message : String(err)}`); + } finally { + setIsSaving(false); + } }; return (

Edit Dashboard

Update dashboard runtime config without changing source code.

- {!config() ? ( -

Dashboard config not found in local runtime storage.

- ) : ( + +

Loading dashboard configuration from database...

+
+ +

Dashboard config "{params.roleKey}" not found in database.

+
+

Dashboard Builder

@@ -73,8 +101,12 @@ export default function EditDashboardPage() { />
- - + +
{statusMessage() &&

{statusMessage()}

} @@ -83,7 +115,7 @@ export default function EditDashboardPage() {
{JSON.stringify(config(), null, 2)}
- )} +
); } diff --git a/src/routes/admin/role-ui-configs/index.tsx b/src/routes/admin/role-ui-configs/index.tsx index 1732e50..bcf0a7a 100644 --- a/src/routes/admin/role-ui-configs/index.tsx +++ b/src/routes/admin/role-ui-configs/index.tsx @@ -1,19 +1,15 @@ import { A } from '@solidjs/router'; -import { createSignal, onMount } from 'solid-js'; +import { createResource, Show } from 'solid-js'; import AdminShell from '~/components/AdminShell'; import { deleteRuntimeConfig, listRuntimeConfigs, type RuntimeListItem } from '~/lib/runtime/storage'; import type { RuntimeDashboardConfig } from '~/lib/runtime/types'; export default function ManageDashboardsPage() { - const [items, setItems] = createSignal>>([]); + const [items, { refetch }] = createResource(() => listRuntimeConfigs('dashboard')); - const load = () => setItems(listRuntimeConfigs('dashboard')); - - onMount(load); - - const onDelete = (key: string) => { - deleteRuntimeConfig('dashboard', key); - load(); + const onDelete = async (key: string) => { + await deleteRuntimeConfig('dashboard', key); + refetch(); }; return ( @@ -25,23 +21,27 @@ export default function ManageDashboardsPage() {

Runtime Dashboards

Create Dashboard - {items().length === 0 ? ( + +

Loading dashboards from database...

+
+

No runtime dashboard configs found yet.

- ) : ( -
- {items().map((item) => ( -
-

{item.key}

-

Status: {item.status}

-

Updated: {new Date(item.updatedAt).toLocaleString()}

-
- Edit - -
-
- ))} -
- )} +
+ 0}> +
+ {items()!.map((item) => ( +
+

{item.key}

+

Status: {item.status}

+

Updated: {new Date(item.updatedAt).toLocaleDateString()}

+
+ Edit + +
+
+ ))} +
+
); diff --git a/src/routes/admin/role-ui-configs/new.tsx b/src/routes/admin/role-ui-configs/new.tsx index 518cf50..6c7280f 100644 --- a/src/routes/admin/role-ui-configs/new.tsx +++ b/src/routes/admin/role-ui-configs/new.tsx @@ -19,14 +19,26 @@ export default function CreateDashboardPage() { }); const [statusMessage, setStatusMessage] = createSignal(''); - const persist = (status: 'draft' | 'published') => { + const [isSaving, setIsSaving] = createSignal(false); + + const persist = async (status: 'draft' | 'published') => { const payload = config(); if (!payload.roleKey.trim()) { setStatusMessage('Role key is required before saving.'); return; } - saveRuntimeConfig('dashboard', payload.roleKey, payload, status); - setStatusMessage(status === 'draft' ? 'Draft saved in runtime storage.' : 'Dashboard config published in runtime storage.'); + + setIsSaving(true); + setStatusMessage('Saving to backend...'); + + try { + await saveRuntimeConfig('dashboard', payload.roleKey, payload, status); + setStatusMessage(status === 'draft' ? 'Draft saved successfully.' : 'Dashboard config published successfully.'); + } catch (err) { + setStatusMessage(`Error: ${err instanceof Error ? err.message : String(err)}`); + } finally { + setIsSaving(false); + } }; return ( @@ -78,8 +90,12 @@ export default function CreateDashboardPage() { />
- - + +
{statusMessage() &&

{statusMessage()}

} diff --git a/src/routes/admin/runtime-roles/[roleKey].tsx b/src/routes/admin/runtime-roles/[roleKey].tsx index f742147..b1f67c9 100644 --- a/src/routes/admin/runtime-roles/[roleKey].tsx +++ b/src/routes/admin/runtime-roles/[roleKey].tsx @@ -1,33 +1,56 @@ import { useParams } from '@solidjs/router'; -import { createSignal, onMount } from 'solid-js'; +import { createEffect, createResource, createSignal, Show } from 'solid-js'; import AdminShell from '~/components/AdminShell'; import { getRuntimeConfig, saveRuntimeConfig } from '~/lib/runtime/storage'; import type { RuntimeRoleConfig } from '~/lib/runtime/types'; export default function EditRolePage() { const params = useParams(); + const [data] = createResource(() => { + if (!params.roleKey) return null; + return getRuntimeConfig('role', params.roleKey); + }); + const [config, setConfig] = createSignal(null); const [statusMessage, setStatusMessage] = createSignal(''); + const [isSaving, setIsSaving] = createSignal(false); - onMount(() => { - const existing = getRuntimeConfig('role', params.roleKey); - setConfig(existing?.payload || null); + // Sync resource to local signals + createEffect(() => { + const item = data(); + if (item?.payload) { + setConfig(JSON.parse(JSON.stringify(item.payload))); + } }); - const persist = (status: 'draft' | 'published') => { + const persist = async (status: 'draft' | 'published') => { const payload = config(); if (!payload) return; - saveRuntimeConfig('role', payload.roleKey, payload, status); - setStatusMessage(status === 'draft' ? 'Draft saved in runtime storage.' : 'Role config published in runtime storage.'); + + setIsSaving(true); + setStatusMessage('Saving to backend...'); + + try { + await saveRuntimeConfig('role', payload.roleKey, payload, status); + setStatusMessage(status === 'draft' ? 'Draft saved successfully.' : 'Role config published successfully.'); + } catch (err) { + setStatusMessage(`Error: ${err instanceof Error ? err.message : String(err)}`); + } finally { + setIsSaving(false); + } }; return (

Edit Role

Update this runtime role without changing source code.

- {!config() ? ( -

Role not found in local runtime storage.

- ) : ( + +

Loading role configuration from database...

+
+ +

Role "{params.roleKey}" not found in database.

+
+

Role Builder

@@ -65,8 +88,12 @@ export default function EditRolePage() {
- - + +
{statusMessage() &&

{statusMessage()}

} @@ -75,7 +102,7 @@ export default function EditRolePage() {
{JSON.stringify(config(), null, 2)}
- )} +
); } diff --git a/src/routes/admin/runtime-roles/index.tsx b/src/routes/admin/runtime-roles/index.tsx index 8c501a3..bfb0db6 100644 --- a/src/routes/admin/runtime-roles/index.tsx +++ b/src/routes/admin/runtime-roles/index.tsx @@ -1,19 +1,15 @@ import { A } from '@solidjs/router'; -import { createSignal, onMount } from 'solid-js'; +import { createResource, Show } from 'solid-js'; import AdminShell from '~/components/AdminShell'; import { deleteRuntimeConfig, listRuntimeConfigs, type RuntimeListItem } from '~/lib/runtime/storage'; import type { RuntimeRoleConfig } from '~/lib/runtime/types'; export default function ManageRolesPage() { - const [items, setItems] = createSignal>>([]); + const [items, { refetch }] = createResource(() => listRuntimeConfigs('role')); - const load = () => setItems(listRuntimeConfigs('role')); - - onMount(load); - - const onDelete = (key: string) => { - deleteRuntimeConfig('role', key); - load(); + const onDelete = async (key: string) => { + await deleteRuntimeConfig('role', key); + refetch(); }; return ( @@ -25,23 +21,27 @@ export default function ManageRolesPage() {

Runtime Roles

Create Role - {items().length === 0 ? ( + +

Loading roles from database...

+
+

No runtime role configs found yet.

- ) : ( -
- {items().map((item) => ( -
-

{item.key}

-

Status: {item.status}

-

Updated: {new Date(item.updatedAt).toLocaleString()}

-
- Edit - -
-
- ))} -
- )} +
+ 0}> +
+ {items()!.map((item) => ( +
+

{item.key}

+

Status: {item.status}

+

Updated: {new Date(item.updatedAt).toLocaleDateString()}

+
+ Edit + +
+
+ ))} +
+
); diff --git a/src/routes/admin/runtime-roles/new.tsx b/src/routes/admin/runtime-roles/new.tsx index 16556c8..c2c8e1e 100644 --- a/src/routes/admin/runtime-roles/new.tsx +++ b/src/routes/admin/runtime-roles/new.tsx @@ -14,14 +14,26 @@ export default function CreateRolePage() { }); const [statusMessage, setStatusMessage] = createSignal(''); - const persist = (status: 'draft' | 'published') => { + const [isSaving, setIsSaving] = createSignal(false); + + const persist = async (status: 'draft' | 'published') => { const payload = config(); if (!payload.roleKey.trim()) { setStatusMessage('Role key is required before saving.'); return; } - saveRuntimeConfig('role', payload.roleKey, payload, status); - setStatusMessage(status === 'draft' ? 'Draft saved in runtime storage.' : 'Role config published in runtime storage.'); + + setIsSaving(true); + setStatusMessage('Saving to backend...'); + + try { + await saveRuntimeConfig('role', payload.roleKey, payload, status); + setStatusMessage(status === 'draft' ? 'Draft saved successfully.' : 'Role config published successfully.'); + } catch (err) { + setStatusMessage(`Error: ${err instanceof Error ? err.message : String(err)}`); + } finally { + setIsSaving(false); + } }; return ( @@ -65,8 +77,12 @@ export default function CreateRolePage() {
- - + +
{statusMessage() &&

{statusMessage()}

}