nxtgauge-frontend-solid/src/routes/workspace.tsx

157 lines
7.3 KiB
TypeScript
Raw Normal View History

import { createResource, Show, For } from 'solid-js';
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';
// Try to lookup Role ID first
const roleRes = await fetch(`${RUST_API_URL}/api/admin/roles/${roleKey}`);
if (!roleRes.ok) throw new Error('Role not found');
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`);
if (!configRes.ok) throw new Error('Dashboard config not found');
const dashboardConfig = await configRes.json();
return dashboardConfig.config_json; // Returns `{ sidebar: [...], widgets: [...] }`
}
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>
);
}
export default function WorkspaceLayout(props: { children?: any }) {
const [searchParams] = useSearchParams();
const rawRoleKey = searchParams.roleKey;
const roleKey = () => (Array.isArray(rawRoleKey) ? rawRoleKey[0] : rawRoleKey) || 'PHOTOGRAPHER';
const [config] = createResource<any, string>(roleKey, fetchRuntimeConfig);
return (
<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>
)}
</For>
</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>
</div>
);
}