2026-03-25 22:13:11 +01:00
|
|
|
|
import { createResource, Show, For } from 'solid-js';
|
2026-03-17 20:42:55 +01:00
|
|
|
|
import { A, useSearchParams } from '@solidjs/router';
|
|
|
|
|
|
import ProfileWidget from '~/components/dashboard/ProfileWidget';
|
|
|
|
|
|
|
|
|
|
|
|
async function fetchRuntimeConfig(roleKey: string) {
|
|
|
|
|
|
const RUST_API_URL = import.meta.env.VITE_RUST_API_URL || 'http://localhost:8080';
|
2026-03-25 22:13:11 +01:00
|
|
|
|
|
2026-03-17 20:42:55 +01:00
|
|
|
|
// Try to lookup Role ID first
|
|
|
|
|
|
const roleRes = await fetch(`${RUST_API_URL}/api/admin/roles/${roleKey}`);
|
2026-03-25 22:13:11 +01:00
|
|
|
|
if (!roleRes.ok) throw new Error('Role not found');
|
2026-03-17 20:42:55 +01:00
|
|
|
|
const role = await roleRes.json();
|
|
|
|
|
|
|
|
|
|
|
|
// Then fetch Dashboard config for that role
|
|
|
|
|
|
const configRes = await fetch(`${RUST_API_URL}/api/admin/dashboard-config/${role.id}?audience=EXTERNAL`);
|
2026-03-25 22:13:11 +01:00
|
|
|
|
if (!configRes.ok) throw new Error('Dashboard config not found');
|
2026-03-17 20:42:55 +01:00
|
|
|
|
const dashboardConfig = await configRes.json();
|
2026-03-25 22:13:11 +01:00
|
|
|
|
|
2026-03-17 20:42:55 +01:00
|
|
|
|
return dashboardConfig.config_json; // Returns `{ sidebar: [...], widgets: [...] }`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-25 22:13:11 +01:00
|
|
|
|
function IconSearch() {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
|
|
|
|
<circle cx="11" cy="11" r="7" />
|
|
|
|
|
|
<path d="M20 20l-3.5-3.5" />
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function IconBell() {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
|
|
|
|
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" />
|
|
|
|
|
|
<path d="M13.73 21a2 2 0 0 1-3.46 0" />
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-17 20:42:55 +01:00
|
|
|
|
export default function WorkspaceLayout(props: { children?: any }) {
|
|
|
|
|
|
const [searchParams] = useSearchParams();
|
|
|
|
|
|
const rawRoleKey = searchParams.roleKey;
|
|
|
|
|
|
const roleKey = () => (Array.isArray(rawRoleKey) ? rawRoleKey[0] : rawRoleKey) || 'PHOTOGRAPHER';
|
2026-03-25 22:13:11 +01:00
|
|
|
|
|
2026-03-17 20:42:55 +01:00
|
|
|
|
const [config] = createResource<any, string>(roleKey, fetchRuntimeConfig);
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
2026-03-25 22:13:11 +01:00
|
|
|
|
<div class="min-h-screen bg-slate-100 text-slate-900">
|
|
|
|
|
|
<div class="flex min-h-screen">
|
|
|
|
|
|
<aside class="hidden w-72 shrink-0 border-r border-slate-200 bg-white/95 shadow-sm lg:flex lg:flex-col">
|
|
|
|
|
|
<div class="border-b border-slate-200 px-6 py-6">
|
|
|
|
|
|
<p class="text-[11px] font-semibold uppercase tracking-[0.2em] text-[#fd6116]">Traceworks Jobs</p>
|
|
|
|
|
|
<h1 class="mt-2 text-2xl font-extrabold text-[#100b2f]">Admin Panel</h1>
|
|
|
|
|
|
<p class="mt-2 text-xs font-medium capitalize text-slate-500">{roleKey().replaceAll('_', ' ').toLowerCase()}</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="px-6 pt-5">
|
|
|
|
|
|
<span class="inline-flex rounded-full border border-orange-200 bg-orange-50 px-3 py-1 text-[11px] font-bold uppercase tracking-[0.12em] text-[#fd6116]">
|
|
|
|
|
|
{roleKey().replaceAll('_', ' ')}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<nav class="flex-1 space-y-1 overflow-y-auto px-4 py-6">
|
|
|
|
|
|
<Show when={config.loading}>
|
|
|
|
|
|
<p class="px-3 text-sm text-slate-500">Loading modules...</p>
|
|
|
|
|
|
</Show>
|
|
|
|
|
|
<Show when={config.error}>
|
|
|
|
|
|
<p class="rounded-xl border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">Failed to load shell config.</p>
|
|
|
|
|
|
</Show>
|
|
|
|
|
|
<Show when={config()}>
|
|
|
|
|
|
<For each={config().sidebar}>
|
|
|
|
|
|
{(item: any) => (
|
|
|
|
|
|
<A
|
|
|
|
|
|
href={item.route}
|
|
|
|
|
|
class="group flex items-center gap-3 rounded-xl px-3 py-2.5 text-sm font-semibold text-slate-600 transition hover:bg-slate-50 hover:text-[#100b2f]"
|
|
|
|
|
|
activeClass="bg-[#fd6116]/10 text-[#fd6116] shadow-sm ring-1 ring-[#fd6116]/20"
|
|
|
|
|
|
>
|
|
|
|
|
|
<span class="h-2.5 w-2.5 rounded-full bg-current/35 transition group-[.bg-[#fd6116]/10]:bg-current" />
|
|
|
|
|
|
<span class="flex-1">{item.label}</span>
|
|
|
|
|
|
<span class="text-base leading-none text-slate-400 transition group-hover:text-[#fd6116]">›</span>
|
|
|
|
|
|
</A>
|
|
|
|
|
|
)}
|
2026-03-17 20:42:55 +01:00
|
|
|
|
</For>
|
2026-03-25 22:13:11 +01:00
|
|
|
|
</Show>
|
|
|
|
|
|
</nav>
|
|
|
|
|
|
</aside>
|
|
|
|
|
|
|
|
|
|
|
|
<main class="flex min-w-0 flex-1 flex-col">
|
|
|
|
|
|
<header class="sticky top-0 z-20 border-b border-slate-200 bg-white/90 px-4 py-4 backdrop-blur sm:px-6 lg:px-8">
|
|
|
|
|
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h2 class="text-2xl font-extrabold tracking-tight text-[#100b2f]">Admin Panel</h2>
|
|
|
|
|
|
<p class="mt-1 text-sm text-slate-500">Manage modules and role configuration</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="flex items-center gap-2 sm:gap-3">
|
|
|
|
|
|
<label class="flex h-10 w-44 items-center gap-2 rounded-xl border border-slate-200 bg-slate-50 px-3 text-slate-500 focus-within:border-[#fd6116] focus-within:ring-2 focus-within:ring-[#fd6116]/20 sm:w-64">
|
|
|
|
|
|
<IconSearch />
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="search"
|
|
|
|
|
|
placeholder="Search"
|
|
|
|
|
|
class="w-full border-0 bg-transparent text-sm text-slate-700 outline-none placeholder:text-slate-400"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
aria-label="Notifications"
|
|
|
|
|
|
class="inline-flex h-10 w-10 items-center justify-center rounded-xl border border-slate-200 bg-white text-slate-500 transition hover:border-[#fd6116]/30 hover:text-[#fd6116]"
|
|
|
|
|
|
>
|
|
|
|
|
|
<IconBell />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<span class="inline-flex h-10 w-10 items-center justify-center rounded-full bg-[#fd6116] text-sm font-bold text-white shadow-sm">
|
|
|
|
|
|
AD
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
|
|
<section class="mx-auto w-full max-w-7xl flex-1 space-y-6 px-4 py-6 sm:px-6 lg:px-8">
|
|
|
|
|
|
<div class="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm sm:p-6">
|
|
|
|
|
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h3 class="text-xl font-bold text-[#100b2f]">Role Modules</h3>
|
|
|
|
|
|
<p class="mt-1 text-sm text-slate-500">Dynamic dashboard widgets configured for this role.</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<Show when={config() && config().widgets}>
|
|
|
|
|
|
<div class="mt-5 grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
|
|
|
|
|
|
<For each={config().widgets}>
|
|
|
|
|
|
{(widget: any) => (
|
|
|
|
|
|
<Show when={widget.enabled}>
|
|
|
|
|
|
<article class="rounded-xl border border-slate-200 bg-gradient-to-br from-white to-slate-50 p-4 shadow-sm transition hover:-translate-y-0.5 hover:shadow-md">
|
|
|
|
|
|
<div class="flex items-start justify-between gap-3">
|
|
|
|
|
|
<h4 class="text-base font-bold text-[#100b2f]">{widget.title}</h4>
|
|
|
|
|
|
<span class="inline-flex rounded-full border border-emerald-200 bg-emerald-50 px-2.5 py-0.5 text-[11px] font-semibold text-emerald-700">
|
|
|
|
|
|
Live
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p class="mt-2 text-sm text-slate-500">Dynamic widget module enabled for this role configuration.</p>
|
|
|
|
|
|
</article>
|
|
|
|
|
|
</Show>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</For>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Show>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<ProfileWidget roleKey={roleKey()} />
|
|
|
|
|
|
|
|
|
|
|
|
{props.children}
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</main>
|
|
|
|
|
|
</div>
|
2026-03-17 20:42:55 +01:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|