feat(admin): runtime-gated sidebar, wire roles/employees/departments/designations
This commit is contained in:
parent
bed0942d12
commit
93e38018dc
8 changed files with 440 additions and 75 deletions
|
|
@ -56,6 +56,47 @@ const PAGE_TITLES: Array<{ prefix: string; label: string; exact?: boolean }> = [
|
|||
];
|
||||
|
||||
const TAB_SETS: Array<{ prefixes: string[]; tabs: Tab[] }> = [];
|
||||
const ROUTE_MODULE_KEYS: Array<{ prefix: string; keys: string[] }> = [
|
||||
{ prefix: '/admin/department', keys: ['DEPARTMENT_MANAGEMENT', 'DEPARTMENTS'] },
|
||||
{ prefix: '/admin/designation', keys: ['DESIGNATION_MANAGEMENT', 'DESIGNATIONS'] },
|
||||
{ prefix: '/admin/roles', keys: ['INTERNAL_ROLE_MANAGEMENT', 'ROLES'] },
|
||||
{ prefix: '/admin/employees', keys: ['EMPLOYEE_MANAGEMENT', 'EMPLOYEES'] },
|
||||
{ prefix: '/admin/external-roles', keys: ['EXTERNAL_ROLE_MANAGEMENT', 'EXTERNAL_ROLES'] },
|
||||
{ prefix: '/admin/onboarding-management', keys: ['ONBOARDING_MANAGEMENT', 'ONBOARDING_SCHEMAS', 'ONBOARDING'] },
|
||||
{ prefix: '/admin/onboarding-schemas', keys: ['ONBOARDING_MANAGEMENT', 'ONBOARDING_SCHEMAS', 'ONBOARDING'] },
|
||||
{ prefix: '/admin/internal-dashboard-management', keys: ['INTERNAL_DASHBOARD_MANAGEMENT', 'INTERNAL_DASHBOARDS'] },
|
||||
{ prefix: '/admin/external-dashboard-management', keys: ['DASHBOARD_CONFIG_MANAGEMENT', 'EXTERNAL_DASHBOARD_MANAGEMENT', 'EXTERNAL_DASHBOARDS', 'RUNTIME_ROLES'] },
|
||||
{ prefix: '/admin/role-ui-configs', keys: ['DASHBOARD_CONFIG_MANAGEMENT', 'EXTERNAL_DASHBOARD_MANAGEMENT', 'EXTERNAL_DASHBOARDS', 'RUNTIME_ROLES'] },
|
||||
{ prefix: '/admin/verification', keys: ['VERIFICATION_MANAGEMENT', 'VERIFICATIONS'] },
|
||||
{ prefix: '/admin/verification-status', keys: ['VERIFICATION_MANAGEMENT', 'VERIFICATIONS'] },
|
||||
{ prefix: '/admin/approval', keys: ['APPROVAL_MANAGEMENT', 'APPROVALS'] },
|
||||
{ prefix: '/admin/users', keys: ['USER_MANAGEMENT', 'USERS'] },
|
||||
{ prefix: '/admin/company', keys: ['COMPANY_MANAGEMENT', 'COMPANIES'] },
|
||||
{ prefix: '/admin/candidate', keys: ['CANDIDATE_MANAGEMENT', 'CANDIDATES'] },
|
||||
{ prefix: '/admin/customer', keys: ['CUSTOMER_MANAGEMENT', 'CUSTOMERS'] },
|
||||
{ prefix: '/admin/photographer', keys: ['PHOTOGRAPHER_MANAGEMENT', 'PHOTOGRAPHERS'] },
|
||||
{ prefix: '/admin/makeup-artist', keys: ['MAKEUP_ARTIST_MANAGEMENT', 'MAKEUP_ARTISTS'] },
|
||||
{ prefix: '/admin/tutors', keys: ['TUTOR_MANAGEMENT', 'TUTORS'] },
|
||||
{ prefix: '/admin/developers', keys: ['DEVELOPER_MANAGEMENT', 'DEVELOPERS'] },
|
||||
{ prefix: '/admin/video-editors', keys: ['VIDEO_EDITOR_MANAGEMENT', 'VIDEO_EDITORS'] },
|
||||
{ prefix: '/admin/fitness-trainers', keys: ['FITNESS_TRAINER_MANAGEMENT', 'FITNESS_TRAINERS'] },
|
||||
{ prefix: '/admin/catering-services', keys: ['CATERING_SERVICES_MANAGEMENT', 'CATERING_SERVICES'] },
|
||||
{ prefix: '/admin/graphic-designers', keys: ['GRAPHIC_DESIGNER_MANAGEMENT', 'GRAPHIC_DESIGNERS'] },
|
||||
{ prefix: '/admin/social-media-managers', keys: ['SOCIAL_MEDIA_MANAGEMENT', 'SOCIAL_MEDIA_MANAGER_MANAGEMENT', 'SOCIAL_MEDIA_MANAGERS'] },
|
||||
{ prefix: '/admin/jobs', keys: ['JOBS_MANAGEMENT', 'JOBS'] },
|
||||
{ prefix: '/admin/leads', keys: ['LEADS_MANAGEMENT', 'LEADS', 'REQUIREMENTS_MANAGEMENT', 'REQUIREMENTS'] },
|
||||
{ prefix: '/admin/pricing', keys: ['PRICING_MANAGEMENT', 'PRICING'] },
|
||||
{ prefix: '/admin/credit', keys: ['CREDIT_MANAGEMENT', 'CREDITS'] },
|
||||
{ prefix: '/admin/coupon', keys: ['COUPON_MANAGEMENT', 'COUPONS'] },
|
||||
{ prefix: '/admin/discount', keys: ['DISCOUNT_MANAGEMENT', 'DISCOUNTS'] },
|
||||
{ prefix: '/admin/tax', keys: ['TAX_MANAGEMENT', 'TAXES'] },
|
||||
{ prefix: '/admin/order', keys: ['ORDER_MANAGEMENT', 'ORDERS'] },
|
||||
{ prefix: '/admin/invoice', keys: ['INVOICE_MANAGEMENT', 'INVOICES'] },
|
||||
{ prefix: '/admin/review', keys: ['REVIEW_MANAGEMENT', 'REVIEWS'] },
|
||||
{ prefix: '/admin/support', keys: ['SUPPORT_MANAGEMENT', 'SUPPORT'] },
|
||||
{ prefix: '/admin/report', keys: ['REPORT_MANAGEMENT', 'REPORTS'] },
|
||||
{ prefix: '/admin/ledger', keys: ['LEDGER', 'LEDGER_MANAGEMENT'] },
|
||||
];
|
||||
|
||||
const SEARCH_MODULES = [
|
||||
{
|
||||
|
|
@ -257,6 +298,8 @@ export default function AdminShell(props: { children: JSX.Element }) {
|
|||
|
||||
const [checkedSession, setCheckedSession] = createSignal(false);
|
||||
const [adminName, setAdminName] = createSignal('Admin User');
|
||||
const [allowedModules, setAllowedModules] = createSignal<string[] | null>(null);
|
||||
const [isSuperAdmin, setIsSuperAdmin] = createSignal(false);
|
||||
const [sidebarOpen, setSidebarOpen] = createSignal(false);
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = createSignal(false);
|
||||
const [notifCount] = createSignal(0);
|
||||
|
|
@ -318,7 +361,7 @@ export default function AdminShell(props: { children: JSX.Element }) {
|
|||
const accessToken = typeof sessionStorage !== 'undefined'
|
||||
? sessionStorage.getItem('nxtgauge_admin_access_token') || ''
|
||||
: '';
|
||||
const response = await fetch('/api/gateway/users/auth/me', {
|
||||
const response = await fetch('/api/gateway/api/auth/session', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
|
|
@ -330,6 +373,52 @@ export default function AdminShell(props: { children: JSX.Element }) {
|
|||
const payload = await response.json().catch(() => ({}));
|
||||
if (!response.ok || isExternalIdentity(payload)) throw new Error('Unauthorized');
|
||||
if (payload?.full_name) setAdminName(payload.full_name);
|
||||
|
||||
const roleKey = String(
|
||||
payload?.active_role
|
||||
|| payload?.role
|
||||
|| payload?.user?.active_role
|
||||
|| payload?.user?.active_role_key
|
||||
|| payload?.user?.role
|
||||
|| payload?.user?.role_key
|
||||
|| '',
|
||||
).toUpperCase();
|
||||
setIsSuperAdmin(roleKey === 'SUPER_ADMIN');
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/gateway/api/runtime-config', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'x-portal-target': 'admin',
|
||||
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
|
||||
},
|
||||
credentials: 'include',
|
||||
});
|
||||
const runtime = await res.json().catch(() => ({}));
|
||||
if (res.ok) {
|
||||
const mods = (
|
||||
runtime?.enabled_modules
|
||||
|| runtime?.enabledModules
|
||||
|| runtime?.modules
|
||||
|| runtime?.config_json?.modules
|
||||
|| runtime?.configJson?.modules
|
||||
|| []
|
||||
) as unknown;
|
||||
if (Array.isArray(mods)) {
|
||||
setAllowedModules(mods.map((m) => String(m || '').trim().toUpperCase()).filter(Boolean));
|
||||
} else {
|
||||
setAllowedModules(null);
|
||||
}
|
||||
const activeRole = String(runtime?.active_role || runtime?.user?.active_role || roleKey || '').toUpperCase();
|
||||
if (activeRole) setIsSuperAdmin(activeRole === 'SUPER_ADMIN');
|
||||
} else {
|
||||
setAllowedModules(null);
|
||||
}
|
||||
} catch {
|
||||
setAllowedModules(null);
|
||||
}
|
||||
|
||||
setCheckedSession(true);
|
||||
} catch {
|
||||
clearAdminSession();
|
||||
|
|
@ -358,6 +447,26 @@ export default function AdminShell(props: { children: JSX.Element }) {
|
|||
return `${parts[0][0]}${parts[1][0]}`.toUpperCase();
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
if (!checkedSession()) return;
|
||||
if (isSuperAdmin()) return;
|
||||
|
||||
const modules = allowedModules();
|
||||
if (!modules || modules.length === 0) return;
|
||||
|
||||
const path = location.pathname;
|
||||
if (path === '/admin') return;
|
||||
|
||||
const guard = ROUTE_MODULE_KEYS.find((entry) => path === entry.prefix || path.startsWith(`${entry.prefix}/`));
|
||||
if (!guard) return;
|
||||
|
||||
const allowed = new Set(modules.map((m) => String(m || '').trim().toUpperCase()).filter(Boolean));
|
||||
const ok = guard.keys.some((k) => allowed.has(String(k).toUpperCase()));
|
||||
if (ok) return;
|
||||
|
||||
navigate(`/admin?denied=${encodeURIComponent(guard.keys[0])}&from=${encodeURIComponent(path)}`, { replace: true });
|
||||
});
|
||||
|
||||
return (
|
||||
<div class="min-h-screen bg-[#F9FAFB] text-[#0D0D2A]">
|
||||
<Show
|
||||
|
|
@ -374,6 +483,8 @@ export default function AdminShell(props: { children: JSX.Element }) {
|
|||
onNavigate={() => setSidebarOpen(false)}
|
||||
adminName={adminName()}
|
||||
adminInitials={adminInitials()}
|
||||
allowedModules={allowedModules()}
|
||||
isSuperAdmin={isSuperAdmin()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { A, useLocation } from '@solidjs/router';
|
||||
import { For, Show } from 'solid-js';
|
||||
import { For, Show, createMemo } from 'solid-js';
|
||||
import {
|
||||
LayoutGrid, Building2, Briefcase, Users, ShieldCheck, FileText,
|
||||
LayoutDashboard, ClipboardList, UserRoundSearch, UserCircle,
|
||||
|
|
@ -15,63 +15,64 @@ type NavItem = {
|
|||
label: string;
|
||||
icon: any;
|
||||
aliasPrefix?: string;
|
||||
moduleKeys?: string[];
|
||||
};
|
||||
|
||||
const GROUPS: NavItem[][] = [
|
||||
[
|
||||
{ href: '/admin', label: 'Dashboard', icon: LayoutGrid },
|
||||
{ href: '/admin', label: 'Dashboard', icon: LayoutGrid, moduleKeys: ['ADMIN_DASHBOARD'] },
|
||||
],
|
||||
[
|
||||
{ href: '/admin/department', label: 'Department Management', icon: Building2 },
|
||||
{ href: '/admin/designation', label: 'Designation Management', icon: Briefcase },
|
||||
{ href: '/admin/roles', label: 'Internal Role Management', icon: ShieldCheck },
|
||||
{ href: '/admin/employees', label: 'Employee Management', icon: Users },
|
||||
{ href: '/admin/department', label: 'Department Management', icon: Building2, moduleKeys: ['DEPARTMENT_MANAGEMENT', 'DEPARTMENTS'] },
|
||||
{ href: '/admin/designation', label: 'Designation Management', icon: Briefcase, moduleKeys: ['DESIGNATION_MANAGEMENT', 'DESIGNATIONS'] },
|
||||
{ href: '/admin/roles', label: 'Internal Role Management', icon: ShieldCheck, moduleKeys: ['INTERNAL_ROLE_MANAGEMENT', 'ROLES'] },
|
||||
{ href: '/admin/employees', label: 'Employee Management', icon: Users, moduleKeys: ['EMPLOYEE_MANAGEMENT', 'EMPLOYEES'] },
|
||||
],
|
||||
[
|
||||
{ href: '/admin/external-roles', label: 'External Role Management', icon: ShieldCheck },
|
||||
{ href: '/admin/onboarding-management', label: 'External Onboarding Management', icon: FileText, aliasPrefix: '/admin/onboarding-schemas' },
|
||||
{ href: '/admin/internal-dashboard-management', label: 'Internal Dashboard Management', icon: LayoutDashboard },
|
||||
{ href: '/admin/external-dashboard-management', label: 'External Dashboard Management', icon: LayoutDashboard, aliasPrefix: '/admin/role-ui-configs' },
|
||||
{ href: '/admin/external-roles', label: 'External Role Management', icon: ShieldCheck, moduleKeys: ['EXTERNAL_ROLE_MANAGEMENT', 'EXTERNAL_ROLES'] },
|
||||
{ href: '/admin/onboarding-management', label: 'External Onboarding Management', icon: FileText, aliasPrefix: '/admin/onboarding-schemas', moduleKeys: ['ONBOARDING_MANAGEMENT', 'ONBOARDING_SCHEMAS', 'ONBOARDING'] },
|
||||
{ href: '/admin/internal-dashboard-management', label: 'Internal Dashboard Management', icon: LayoutDashboard, moduleKeys: ['INTERNAL_DASHBOARD_MANAGEMENT', 'INTERNAL_DASHBOARDS'] },
|
||||
{ href: '/admin/external-dashboard-management', label: 'External Dashboard Management', icon: LayoutDashboard, aliasPrefix: '/admin/role-ui-configs', moduleKeys: ['DASHBOARD_CONFIG_MANAGEMENT', 'EXTERNAL_DASHBOARD_MANAGEMENT', 'EXTERNAL_DASHBOARDS', 'RUNTIME_ROLES'] },
|
||||
],
|
||||
[
|
||||
{ href: '/admin/verification', label: 'Verification Management', icon: BadgeCheck },
|
||||
{ href: '/admin/approval', label: 'Approval Management', icon: ClipboardList },
|
||||
{ href: '/admin/verification', label: 'Verification Management', icon: BadgeCheck, moduleKeys: ['VERIFICATION_MANAGEMENT', 'VERIFICATIONS'] },
|
||||
{ href: '/admin/approval', label: 'Approval Management', icon: ClipboardList, moduleKeys: ['APPROVAL_MANAGEMENT', 'APPROVALS'] },
|
||||
],
|
||||
[
|
||||
{ href: '/admin/users', label: 'Users Management', icon: UserRoundSearch },
|
||||
{ href: '/admin/company', label: 'Company Management', icon: Building2 },
|
||||
{ href: '/admin/candidate', label: 'Candidate Management', icon: UserCircle },
|
||||
{ href: '/admin/customer', label: 'Customer Management', icon: UserCircle },
|
||||
{ href: '/admin/users', label: 'Users Management', icon: UserRoundSearch, moduleKeys: ['USER_MANAGEMENT', 'USERS'] },
|
||||
{ href: '/admin/company', label: 'Company Management', icon: Building2, moduleKeys: ['COMPANY_MANAGEMENT', 'COMPANIES'] },
|
||||
{ href: '/admin/candidate', label: 'Candidate Management', icon: UserCircle, moduleKeys: ['CANDIDATE_MANAGEMENT', 'CANDIDATES'] },
|
||||
{ href: '/admin/customer', label: 'Customer Management', icon: UserCircle, moduleKeys: ['CUSTOMER_MANAGEMENT', 'CUSTOMERS'] },
|
||||
],
|
||||
[
|
||||
{ href: '/admin/photographer', label: 'Photographer Management', icon: Camera },
|
||||
{ href: '/admin/makeup-artist', label: 'Makeup Artist Management', icon: Palette },
|
||||
{ href: '/admin/tutors', label: 'Tutors Management', icon: BookOpen },
|
||||
{ href: '/admin/developers', label: 'Developers Management', icon: Code2 },
|
||||
{ href: '/admin/video-editors', label: 'Video Editor Management', icon: Film },
|
||||
{ href: '/admin/fitness-trainers', label: 'Fitness Trainer Management', icon: Activity },
|
||||
{ href: '/admin/catering-services', label: 'Catering Services Management', icon: Utensils },
|
||||
{ href: '/admin/graphic-designers', label: 'Graphics Designer Management', icon: PenTool },
|
||||
{ href: '/admin/social-media-managers', label: 'Social Media Manager Management', icon: Megaphone },
|
||||
{ href: '/admin/photographer', label: 'Photographer Management', icon: Camera, moduleKeys: ['PHOTOGRAPHER_MANAGEMENT', 'PHOTOGRAPHERS'] },
|
||||
{ href: '/admin/makeup-artist', label: 'Makeup Artist Management', icon: Palette, moduleKeys: ['MAKEUP_ARTIST_MANAGEMENT', 'MAKEUP_ARTISTS'] },
|
||||
{ href: '/admin/tutors', label: 'Tutors Management', icon: BookOpen, moduleKeys: ['TUTOR_MANAGEMENT', 'TUTORS'] },
|
||||
{ href: '/admin/developers', label: 'Developers Management', icon: Code2, moduleKeys: ['DEVELOPER_MANAGEMENT', 'DEVELOPERS'] },
|
||||
{ href: '/admin/video-editors', label: 'Video Editor Management', icon: Film, moduleKeys: ['VIDEO_EDITOR_MANAGEMENT', 'VIDEO_EDITORS'] },
|
||||
{ href: '/admin/fitness-trainers', label: 'Fitness Trainer Management', icon: Activity, moduleKeys: ['FITNESS_TRAINER_MANAGEMENT', 'FITNESS_TRAINERS'] },
|
||||
{ href: '/admin/catering-services', label: 'Catering Services Management', icon: Utensils, moduleKeys: ['CATERING_SERVICES_MANAGEMENT', 'CATERING_SERVICES'] },
|
||||
{ href: '/admin/graphic-designers', label: 'Graphics Designer Management', icon: PenTool, moduleKeys: ['GRAPHIC_DESIGNER_MANAGEMENT', 'GRAPHIC_DESIGNERS'] },
|
||||
{ href: '/admin/social-media-managers', label: 'Social Media Manager Management', icon: Megaphone, moduleKeys: ['SOCIAL_MEDIA_MANAGEMENT', 'SOCIAL_MEDIA_MANAGER_MANAGEMENT', 'SOCIAL_MEDIA_MANAGERS'] },
|
||||
],
|
||||
[
|
||||
{ href: '/admin/jobs', label: 'Jobs Management', icon: BriefcaseBusiness },
|
||||
{ href: '/admin/leads', label: 'Leads Management', icon: HandHelping },
|
||||
{ href: '/admin/jobs', label: 'Jobs Management', icon: BriefcaseBusiness, moduleKeys: ['JOBS_MANAGEMENT', 'JOBS'] },
|
||||
{ href: '/admin/leads', label: 'Leads Management', icon: HandHelping, moduleKeys: ['LEADS_MANAGEMENT', 'LEADS', 'REQUIREMENTS_MANAGEMENT', 'REQUIREMENTS'] },
|
||||
],
|
||||
[
|
||||
{ href: '/admin/pricing', label: 'Pricing Management', icon: WalletCards },
|
||||
{ href: '/admin/credit', label: 'Credit Management', icon: CreditCard },
|
||||
{ href: '/admin/coupon', label: 'Coupon Management', icon: Tag },
|
||||
{ href: '/admin/discount', label: 'Discount Management', icon: Percent },
|
||||
{ href: '/admin/tax', label: 'Tax Management', icon: Receipt },
|
||||
{ href: '/admin/order', label: 'Order Management', icon: ShoppingCart },
|
||||
{ href: '/admin/invoice', label: 'Invoice Management', icon: FileCheck },
|
||||
{ href: '/admin/pricing', label: 'Pricing Management', icon: WalletCards, moduleKeys: ['PRICING_MANAGEMENT', 'PRICING'] },
|
||||
{ href: '/admin/credit', label: 'Credit Management', icon: CreditCard, moduleKeys: ['CREDIT_MANAGEMENT', 'CREDITS'] },
|
||||
{ href: '/admin/coupon', label: 'Coupon Management', icon: Tag, moduleKeys: ['COUPON_MANAGEMENT', 'COUPONS'] },
|
||||
{ href: '/admin/discount', label: 'Discount Management', icon: Percent, moduleKeys: ['DISCOUNT_MANAGEMENT', 'DISCOUNTS'] },
|
||||
{ href: '/admin/tax', label: 'Tax Management', icon: Receipt, moduleKeys: ['TAX_MANAGEMENT', 'TAXES'] },
|
||||
{ href: '/admin/order', label: 'Order Management', icon: ShoppingCart, moduleKeys: ['ORDER_MANAGEMENT', 'ORDERS'] },
|
||||
{ href: '/admin/invoice', label: 'Invoice Management', icon: FileCheck, moduleKeys: ['INVOICE_MANAGEMENT', 'INVOICES'] },
|
||||
],
|
||||
[
|
||||
{ href: '/admin/review', label: 'Review Management', icon: Star },
|
||||
{ href: '/admin/support', label: 'Support Management', icon: HeadphonesIcon },
|
||||
{ href: '/admin/report', label: 'Report Management', icon: BarChart3 },
|
||||
{ href: '/admin/ledger', label: 'Ledger Management', icon: Receipt },
|
||||
{ href: '/admin/review', label: 'Review Management', icon: Star, moduleKeys: ['REVIEW_MANAGEMENT', 'REVIEWS'] },
|
||||
{ href: '/admin/support', label: 'Support Management', icon: HeadphonesIcon, moduleKeys: ['SUPPORT_MANAGEMENT', 'SUPPORT'] },
|
||||
{ href: '/admin/report', label: 'Report Management', icon: BarChart3, moduleKeys: ['REPORT_MANAGEMENT', 'REPORTS'] },
|
||||
{ href: '/admin/ledger', label: 'Ledger Management', icon: Receipt, moduleKeys: ['LEDGER', 'LEDGER_MANAGEMENT'] },
|
||||
],
|
||||
];
|
||||
|
||||
|
|
@ -81,9 +82,23 @@ export default function AdminSidebar(props: {
|
|||
onNavigate?: () => void;
|
||||
adminName: string;
|
||||
adminInitials: string;
|
||||
allowedModules?: string[] | null;
|
||||
isSuperAdmin?: boolean;
|
||||
}) {
|
||||
const location = useLocation();
|
||||
|
||||
const allowed = createMemo(() => new Set((props.allowedModules || []).map((m) => String(m || '').trim().toUpperCase()).filter(Boolean)));
|
||||
const canShowItem = (item: NavItem) => {
|
||||
if (props.isSuperAdmin) return true;
|
||||
if (!props.allowedModules || props.allowedModules.length === 0) return true;
|
||||
if (item.href === '/admin') return true;
|
||||
const keys = item.moduleKeys || [];
|
||||
for (const k of keys) if (allowed().has(String(k).toUpperCase())) return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
const visibleGroups = createMemo(() => GROUPS.map((group) => group.filter((item) => canShowItem(item))).filter((group) => group.length > 0));
|
||||
|
||||
const isActive = (item: NavItem) => {
|
||||
if (location.pathname === '/admin') return item.href === '/admin';
|
||||
if (item.href === '/admin') return false;
|
||||
|
|
@ -129,7 +144,7 @@ export default function AdminSidebar(props: {
|
|||
|
||||
{/* Navigation */}
|
||||
<nav style="flex:1;min-height:0;overflow-y:auto;padding:10px 8px">
|
||||
<For each={GROUPS}>
|
||||
<For each={visibleGroups()}>
|
||||
{(group, gi) => (
|
||||
<>
|
||||
<Show when={gi() > 0}>
|
||||
|
|
|
|||
|
|
@ -113,6 +113,9 @@ export default function DepartmentManagementPage() {
|
|||
setIsLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
const accessToken = typeof sessionStorage !== 'undefined'
|
||||
? sessionStorage.getItem('nxtgauge_admin_access_token') || ''
|
||||
: '';
|
||||
const params = new URLSearchParams({
|
||||
page: '1',
|
||||
per_page: '100',
|
||||
|
|
@ -121,7 +124,13 @@ export default function DepartmentManagementPage() {
|
|||
if (statusFilter() !== 'all') {
|
||||
params.set('status', statusFilter());
|
||||
}
|
||||
const res = await fetch(`${API}/api/admin/departments?${params.toString()}`);
|
||||
const res = await fetch(`${API}/api/admin/departments?${params.toString()}`, {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
|
||||
},
|
||||
credentials: 'include',
|
||||
});
|
||||
if (!res.ok) throw new Error(`Request failed (${res.status})`);
|
||||
const payload = (await res.json().catch(() => null)) as DepartmentListResponse | null;
|
||||
const list = Array.isArray(payload)
|
||||
|
|
@ -208,13 +217,21 @@ export default function DepartmentManagementPage() {
|
|||
};
|
||||
|
||||
try {
|
||||
const accessToken = typeof sessionStorage !== 'undefined'
|
||||
? sessionStorage.getItem('nxtgauge_admin_access_token') || ''
|
||||
: '';
|
||||
const endpoint = editingId()
|
||||
? `${API}/api/admin/departments/${editingId()}`
|
||||
: `${API}/api/admin/departments`;
|
||||
const method = editingId() ? 'PATCH' : 'POST';
|
||||
const res = await fetch(endpoint, {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!res.ok) {
|
||||
|
|
@ -329,7 +346,7 @@ export default function DepartmentManagementPage() {
|
|||
</Show>
|
||||
|
||||
{/* Table card */}
|
||||
<div style:display={listTab() === 'view' ? 'none' : 'block'}>
|
||||
<div style={{ display: listTab() === 'view' ? 'none' : 'block' }}>
|
||||
<div style="margin-top:1.5rem;margin-left:-24px;margin-right:-24px;border-radius:0;border-left:none;border-right:none;overflow:hidden;border-top:1px solid #E5E7EB;border-bottom:1px solid #E5E7EB;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.06)">
|
||||
|
||||
{/* Filter bar */}
|
||||
|
|
|
|||
|
|
@ -137,6 +137,9 @@ export default function DesignationManagementPage() {
|
|||
setIsLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
const accessToken = typeof sessionStorage !== 'undefined'
|
||||
? sessionStorage.getItem('nxtgauge_admin_access_token') || ''
|
||||
: '';
|
||||
const params = new URLSearchParams({
|
||||
page: '1',
|
||||
per_page: '100',
|
||||
|
|
@ -148,7 +151,13 @@ export default function DesignationManagementPage() {
|
|||
if (deptFilter() !== 'all') {
|
||||
params.set('department_id', deptFilter());
|
||||
}
|
||||
const res = await fetch(`${API}/api/admin/designations?${params.toString()}`);
|
||||
const res = await fetch(`${API}/api/admin/designations?${params.toString()}`, {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
|
||||
},
|
||||
credentials: 'include',
|
||||
});
|
||||
if (!res.ok) throw new Error(`Request failed (${res.status})`);
|
||||
const payload = (await res.json().catch(() => null)) as DesignationListResponse | null;
|
||||
const list = Array.isArray(payload)
|
||||
|
|
@ -171,7 +180,16 @@ export default function DesignationManagementPage() {
|
|||
|
||||
const loadDepartments = async () => {
|
||||
try {
|
||||
const res = await fetch(`${API}/api/admin/departments?per_page=100`);
|
||||
const accessToken = typeof sessionStorage !== 'undefined'
|
||||
? sessionStorage.getItem('nxtgauge_admin_access_token') || ''
|
||||
: '';
|
||||
const res = await fetch(`${API}/api/admin/departments?per_page=100`, {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
|
||||
},
|
||||
credentials: 'include',
|
||||
});
|
||||
if (!res.ok) return;
|
||||
const payload = await res.json().catch(() => null);
|
||||
const list: any[] = Array.isArray(payload)
|
||||
|
|
@ -252,13 +270,21 @@ export default function DesignationManagementPage() {
|
|||
}
|
||||
|
||||
try {
|
||||
const accessToken = typeof sessionStorage !== 'undefined'
|
||||
? sessionStorage.getItem('nxtgauge_admin_access_token') || ''
|
||||
: '';
|
||||
const endpoint = editingId()
|
||||
? `${API}/api/admin/designations/${editingId()}`
|
||||
: `${API}/api/admin/designations`;
|
||||
const method = editingId() ? 'PATCH' : 'POST';
|
||||
const res = await fetch(endpoint, {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!res.ok) {
|
||||
|
|
@ -373,7 +399,7 @@ export default function DesignationManagementPage() {
|
|||
</Show>
|
||||
|
||||
{/* Table card */}
|
||||
<div style:display={listTab() === 'view' ? 'none' : 'block'}>
|
||||
<div style={{ display: listTab() === 'view' ? 'none' : 'block' }}>
|
||||
<div style="margin-top:1.5rem;margin-left:-24px;margin-right:-24px;border-radius:0;border-left:none;border-right:none;overflow:hidden;border-top:1px solid #E5E7EB;border-bottom:1px solid #E5E7EB;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.06)">
|
||||
|
||||
{/* Filter bar */}
|
||||
|
|
|
|||
|
|
@ -103,6 +103,10 @@ export default function EmployeeManagementPage() {
|
|||
const [openMenuId, setOpenMenuId] = createSignal<string | null>(null);
|
||||
const [editingId, setEditingId] = createSignal<string | null>(null);
|
||||
const [viewingEmp, setViewingEmp] = createSignal<EmployeeRecord | null>(null);
|
||||
const [statusFilter, setStatusFilter] = createSignal<'all' | EmployeeRecord['status']>('all');
|
||||
const [sortBy, setSortBy] = createSignal<'name_asc' | 'name_desc' | 'joined_desc' | 'joined_asc'>('joined_desc');
|
||||
const [sortMenuOpen, setSortMenuOpen] = createSignal(false);
|
||||
const [filterMenuOpen, setFilterMenuOpen] = createSignal(false);
|
||||
|
||||
// Form Signals
|
||||
const [name, setName] = createSignal('');
|
||||
|
|
@ -123,7 +127,23 @@ export default function EmployeeManagementPage() {
|
|||
const load = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const res = await fetch(`${API}/api/admin/employees`);
|
||||
const accessToken = typeof sessionStorage !== 'undefined'
|
||||
? sessionStorage.getItem('nxtgauge_admin_access_token') || ''
|
||||
: '';
|
||||
const params = new URLSearchParams();
|
||||
const q = search().trim();
|
||||
if (q) params.set('q', q);
|
||||
if (statusFilter() !== 'all') params.set('status', statusFilter() as string);
|
||||
params.set('sort', sortBy());
|
||||
params.set('page', '1');
|
||||
params.set('per_page', '100');
|
||||
const res = await fetch(`${API}/api/admin/employees?${params.toString()}`, {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
|
||||
},
|
||||
credentials: 'include',
|
||||
});
|
||||
if (!res.ok) throw new Error();
|
||||
const data = await res.json();
|
||||
const list = Array.isArray(data) ? data : (data.employees ?? data.users ?? []);
|
||||
|
|
@ -139,13 +159,29 @@ export default function EmployeeManagementPage() {
|
|||
onMount(() => void load());
|
||||
|
||||
const filteredRows = createMemo(() => {
|
||||
const q = search().toLowerCase();
|
||||
if (!q) return rows();
|
||||
return rows().filter(r =>
|
||||
r.name.toLowerCase().includes(q) ||
|
||||
r.email.toLowerCase().includes(q) ||
|
||||
String(r.employeeId || '').toLowerCase().includes(q)
|
||||
);
|
||||
let list = rows();
|
||||
const f = statusFilter();
|
||||
if (f !== 'all') list = list.filter((r) => r.status === f);
|
||||
const q = search().trim().toLowerCase();
|
||||
if (q) {
|
||||
list = list.filter(r =>
|
||||
r.name.toLowerCase().includes(q) ||
|
||||
r.email.toLowerCase().includes(q) ||
|
||||
String(r.employeeId || '').toLowerCase().includes(q) ||
|
||||
String(r.department || '').toLowerCase().includes(q) ||
|
||||
String(r.designation || '').toLowerCase().includes(q)
|
||||
);
|
||||
}
|
||||
const sorted = [...list];
|
||||
const mode = sortBy();
|
||||
const jd = (v?: string) => Date.parse(String(v || '')) || 0;
|
||||
sorted.sort((a, b) => {
|
||||
if (mode === 'name_asc') return a.name.localeCompare(b.name);
|
||||
if (mode === 'name_desc') return b.name.localeCompare(a.name);
|
||||
if (mode === 'joined_asc') return jd(a.joiningDate) - jd(b.joiningDate);
|
||||
return jd(b.joiningDate) - jd(a.joiningDate);
|
||||
});
|
||||
return sorted;
|
||||
});
|
||||
|
||||
const resetForm = () => {
|
||||
|
|
@ -179,12 +215,54 @@ export default function EmployeeManagementPage() {
|
|||
return;
|
||||
}
|
||||
setIsSaving(true);
|
||||
// In a real app, we'd call the API here. For now, we'll just simulate success.
|
||||
setTimeout(() => {
|
||||
setIsSaving(false);
|
||||
try {
|
||||
const accessToken = typeof sessionStorage !== 'undefined'
|
||||
? sessionStorage.getItem('nxtgauge_admin_access_token') || ''
|
||||
: '';
|
||||
const isEdit = Boolean(editingId());
|
||||
const body: Record<string, unknown> = {};
|
||||
if (empId().trim()) body.employee_code = empId().trim();
|
||||
// These fields expect IDs; leave blank if not provided
|
||||
if (dept().trim()) body.department_id = dept().trim();
|
||||
if (desig().trim()) body.designation_id = desig().trim();
|
||||
if (role().trim()) body.role_id = role().trim();
|
||||
const endpoint = isEdit
|
||||
? `${API}/api/admin/employees/${editingId()}`
|
||||
: `${API}/api/admin/employees`;
|
||||
if (!isEdit) {
|
||||
if (!role().trim()) throw new Error('Role ID is required to create employee');
|
||||
if (!email().trim()) throw new Error('Email required to resolve user');
|
||||
// For initial version, require an existing user_id; resolve by email
|
||||
const resUser = await fetch(`${API}/api/admin/users?email=${encodeURIComponent(email().trim())}`, {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
|
||||
},
|
||||
credentials: 'include',
|
||||
}).catch(() => null);
|
||||
const payload = await resUser?.json().catch(() => null);
|
||||
const userId = payload?.user?.id || payload?.id || payload?.users?.[0]?.id;
|
||||
if (!userId) throw new Error('User not found for the provided email');
|
||||
body.user_id = userId;
|
||||
}
|
||||
const res = await fetch(endpoint, {
|
||||
method: isEdit ? 'PATCH' : 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!res.ok) throw new Error(`Request failed (${res.status})`);
|
||||
setView('list');
|
||||
load();
|
||||
}, 500);
|
||||
await load();
|
||||
} catch (e: any) {
|
||||
setError(e?.message || 'Failed to save employee');
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -225,10 +303,44 @@ export default function EmployeeManagementPage() {
|
|||
placeholder="Search employees..."
|
||||
style="height:34px;flex:1;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:13px;color:#111827;outline:none"
|
||||
/>
|
||||
<button type="button" style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:500;color:#374151;cursor:pointer">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 5h18M6 12h12M10 19h4"/></svg>
|
||||
Filters
|
||||
</button>
|
||||
<div style="position:relative">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setSortMenuOpen((v) => !v); setFilterMenuOpen(false); }}
|
||||
style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:500;color:#374151;cursor:pointer"
|
||||
>
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M7 4v13"/><path d="m3 13 4 4 4-4"/><path d="M17 20V7"/><path d="m21 11-4-4-4 4"/></svg>
|
||||
Sort
|
||||
</button>
|
||||
<Show when={sortMenuOpen()}>
|
||||
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:220px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
|
||||
{(['joined_desc','joined_asc','name_asc','name_desc'] as const).map((s, i) => (
|
||||
<button type="button" onClick={() => { setSortBy(s); setSortMenuOpen(false); }} style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${sortBy() === s ? '#FF5E13' : '#374151'};background:${sortBy() === s ? '#FFF1EB' : 'transparent'}`}>
|
||||
{['Joining (Newest)','Joining (Oldest)','Name (A→Z)','Name (Z→A)'][i]}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
<div style="position:relative">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setFilterMenuOpen((v) => !v); setSortMenuOpen(false); }}
|
||||
style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:500;color:#374151;cursor:pointer"
|
||||
>
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 5h18M6 12h12M10 19h4"/></svg>
|
||||
Filters
|
||||
</button>
|
||||
<Show when={filterMenuOpen()}>
|
||||
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:220px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
|
||||
{(['all','ACTIVE','PROBATION','ON_LEAVE','SUSPENDED','INACTIVE'] as const).map((s) => (
|
||||
<button type="button" onClick={() => { setStatusFilter(s as any); setFilterMenuOpen(false); }} style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${statusFilter() === s ? '#FF5E13' : '#374151'};background:${statusFilter() === s ? '#FFF1EB' : 'transparent'}`}>
|
||||
{s === 'all' ? 'All Statuses' : s.replace('_',' ').toUpperCase()}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
<button type="button" style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;background:#0D0D2A;padding:0 12px;font-size:12px;font-weight:600;color:white;border:none;cursor:pointer">
|
||||
Export
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -132,9 +132,12 @@ export default function ExternalRoleManagementPage() {
|
|||
|
||||
const [search, setSearch] = createSignal('');
|
||||
const [statusFilter, setStatusFilter] = createSignal('all');
|
||||
const [verticalFilter, setVerticalFilter] = createSignal<'all' | 'jobs' | 'marketplace'>('all');
|
||||
const [categoryFilter, setCategoryFilter] = createSignal<'all' | 'provider' | 'employer' | 'consumer' | 'specialist'>('all');
|
||||
const [sortBy, setSortBy] = createSignal<'name_asc' | 'name_desc' | 'users_desc' | 'users_asc'>('name_asc');
|
||||
const [sortMenuOpen, setSortMenuOpen] = createSignal(false);
|
||||
const [filterMenuOpen, setFilterMenuOpen] = createSignal(false);
|
||||
const [filterMenu2Open, setFilterMenu2Open] = createSignal(false);
|
||||
const [rows, setRows] = createSignal<ExternalRoleRecord[]>([]);
|
||||
const [viewingRole, setViewingRole] = createSignal<ExternalRoleRecord | null>(null);
|
||||
const [editingId, setEditingId] = createSignal<string | null>(null);
|
||||
|
|
@ -171,6 +174,8 @@ export default function ExternalRoleManagementPage() {
|
|||
const filteredRows = createMemo(() => {
|
||||
let r = rows();
|
||||
if (statusFilter() !== 'all') r = r.filter((d) => d.status === statusFilter().toUpperCase());
|
||||
if (verticalFilter() !== 'all') r = r.filter((d) => d.vertical === verticalFilter());
|
||||
if (categoryFilter() !== 'all') r = r.filter((d) => d.category === categoryFilter());
|
||||
const q = search().toLowerCase();
|
||||
if (q) {
|
||||
r = r.filter(r => r.name.toLowerCase().includes(q) || r.code.toLowerCase().includes(q));
|
||||
|
|
@ -448,11 +453,11 @@ export default function ExternalRoleManagementPage() {
|
|||
<div style="position:relative">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setFilterMenuOpen((v) => !v); setSortMenuOpen(false); }}
|
||||
onClick={() => { setFilterMenuOpen((v) => !v); setFilterMenu2Open(false); setSortMenuOpen(false); }}
|
||||
style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:500;color:#374151;cursor:pointer"
|
||||
>
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 5h18M6 12h12M10 19h4"/></svg>
|
||||
Filters
|
||||
Status
|
||||
</button>
|
||||
<Show when={filterMenuOpen()}>
|
||||
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:160px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
|
||||
|
|
@ -464,6 +469,33 @@ export default function ExternalRoleManagementPage() {
|
|||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
<div style="position:relative">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setFilterMenu2Open((v) => !v); setFilterMenuOpen(false); setSortMenuOpen(false); }}
|
||||
style="display:inline-flex;height:34px;align-items:center;gap:6px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:500;color:#374151;cursor:pointer"
|
||||
>
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 5h18M6 12h12M10 19h4"/></svg>
|
||||
Vertical / Category
|
||||
</button>
|
||||
<Show when={filterMenu2Open()}>
|
||||
<div style="position:absolute;left:0;top:38px;z-index:30;min-width:220px;border-radius:12px;border:1px solid #E5E7EB;background:white;padding:6px;box-shadow:0 4px 16px rgba(0,0,0,0.1)">
|
||||
<div style="padding:6px 8px;font-size:11px;font-weight:700;color:#9CA3AF;text-transform:uppercase">Vertical</div>
|
||||
{(['all','jobs','marketplace'] as const).map((s) => (
|
||||
<button type="button" onClick={() => { setVerticalFilter(s); }} style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${verticalFilter() === s ? '#FF5E13' : '#374151'};background:${verticalFilter() === s ? '#FFF1EB' : 'transparent'}`}>
|
||||
{s === 'all' ? 'All Verticals' : s[0].toUpperCase() + s.slice(1)}
|
||||
</button>
|
||||
))}
|
||||
<div style="height:1px;background:#F3F4F6;margin:6px 4px" />
|
||||
<div style="padding:6px 8px;font-size:11px;font-weight:700;color:#9CA3AF;text-transform:uppercase">Category</div>
|
||||
{(['all','provider','employer','consumer','specialist'] as const).map((s) => (
|
||||
<button type="button" onClick={() => { setCategoryFilter(s); setFilterMenu2Open(false); }} style={`display:block;width:100%;border-radius:8px;padding:8px 12px;text-align:left;font-size:13px;background:none;border:none;cursor:pointer;color:${categoryFilter() === s ? '#FF5E13' : '#374151'};background:${categoryFilter() === s ? '#FFF1EB' : 'transparent'}`}>
|
||||
{s === 'all' ? 'All Categories' : s[0].toUpperCase() + s.slice(1)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { For, Show, createEffect, createMemo, createSignal, onCleanup, onMount } from 'solid-js';
|
||||
import { useSearchParams } from '@solidjs/router';
|
||||
import {
|
||||
BarChart3,
|
||||
Building2,
|
||||
|
|
@ -200,6 +201,7 @@ function LivePreview() {
|
|||
}
|
||||
|
||||
export default function AdminHomePage() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const [layout, setLayout] = createSignal<RuntimeDashboardLayout>(sanitizeLayout(DEFAULT_LAYOUT));
|
||||
const [settingsOpen, setSettingsOpen] = createSignal(false);
|
||||
const [isHydrating, setIsHydrating] = createSignal(true);
|
||||
|
|
@ -323,6 +325,11 @@ export default function AdminHomePage() {
|
|||
return (
|
||||
<AdminShell>
|
||||
<div class="w-full">
|
||||
<Show when={Boolean(searchParams.denied)}>
|
||||
<div class="rounded-2xl border border-[#E5E7EB] bg-white shadow-sm" style="margin-bottom: 18px">
|
||||
<p class="notice">You don’t have access to {String(searchParams.denied || '').replace(/_/g, ' ')}.</p>
|
||||
</div>
|
||||
</Show>
|
||||
<div class="rounded-2xl border border-[#E5E7EB] bg-white px-6 py-5 shadow-sm md:px-8" style="margin-bottom: 28px">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -113,7 +113,16 @@ export default function RoleManagementPage() {
|
|||
setError('');
|
||||
try {
|
||||
const params = new URLSearchParams({ audience: 'INTERNAL', per_page: '100', q: search().trim() });
|
||||
const res = await fetch(`${API}/api/admin/roles?${params}`);
|
||||
const accessToken = typeof sessionStorage !== 'undefined'
|
||||
? sessionStorage.getItem('nxtgauge_admin_access_token') || ''
|
||||
: '';
|
||||
const res = await fetch(`${API}/api/admin/roles?${params}`, {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
|
||||
},
|
||||
credentials: 'include',
|
||||
});
|
||||
if (!res.ok) throw new Error(`Request failed (${res.status})`);
|
||||
const payload = await res.json().catch(() => null);
|
||||
const list: any[] = Array.isArray(payload)
|
||||
|
|
@ -132,7 +141,16 @@ export default function RoleManagementPage() {
|
|||
|
||||
const loadDepartments = async () => {
|
||||
try {
|
||||
const res = await fetch(`${API}/api/admin/departments?per_page=100`);
|
||||
const accessToken = typeof sessionStorage !== 'undefined'
|
||||
? sessionStorage.getItem('nxtgauge_admin_access_token') || ''
|
||||
: '';
|
||||
const res = await fetch(`${API}/api/admin/departments?per_page=100`, {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
|
||||
},
|
||||
credentials: 'include',
|
||||
});
|
||||
if (!res.ok) return;
|
||||
const payload = await res.json().catch(() => null);
|
||||
const list: any[] = Array.isArray(payload?.departments) ? payload.departments : [];
|
||||
|
|
@ -179,9 +197,10 @@ export default function RoleManagementPage() {
|
|||
setSelectedPermissions(new Set());
|
||||
setFormTab('general'); setView('form'); setOpenMenuId(null);
|
||||
// Fetch permission_keys for this role
|
||||
fetch(`${API}/api/admin/roles/${row.id}`).then(r => r.json()).then(detail => {
|
||||
fetch(`${API}/api/admin/roles/${row.id}`).then(r => r.json()).then((detail) => {
|
||||
if (Array.isArray(detail?.permission_keys)) {
|
||||
setSelectedPermissions(new Set(detail.permission_keys));
|
||||
const keys = (detail.permission_keys as any[]).map((k) => String(k));
|
||||
setSelectedPermissions(new Set<string>(keys));
|
||||
}
|
||||
}).catch(() => {});
|
||||
};
|
||||
|
|
@ -193,7 +212,7 @@ export default function RoleManagementPage() {
|
|||
const res = await fetch(`${API}/api/admin/roles/${row.id}`);
|
||||
if (res.ok) {
|
||||
const detail = await res.json();
|
||||
setViewingPermissions(Array.isArray(detail?.permission_keys) ? detail.permission_keys : []);
|
||||
setViewingPermissions(Array.isArray(detail?.permission_keys) ? (detail.permission_keys as any[]).map((k: any) => String(k)) : []);
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
};
|
||||
|
|
@ -207,6 +226,9 @@ export default function RoleManagementPage() {
|
|||
setIsSaving(true);
|
||||
setFormError('');
|
||||
try {
|
||||
const accessToken = typeof sessionStorage !== 'undefined'
|
||||
? sessionStorage.getItem('nxtgauge_admin_access_token') || ''
|
||||
: '';
|
||||
const isCreate = !editingId();
|
||||
const endpoint = isCreate
|
||||
? `${API}/api/admin/roles`
|
||||
|
|
@ -218,7 +240,7 @@ export default function RoleManagementPage() {
|
|||
is_active: status() === 'ACTIVE',
|
||||
can_approve_requests: canApproveRequests(),
|
||||
can_manage_system_settings: canManageSystemSettings(),
|
||||
permission_keys: Array.from(selectedPermissions()),
|
||||
permission_keys: Array.from(selectedPermissions()) as string[],
|
||||
};
|
||||
if (departmentId().trim()) body.department_id = departmentId().trim();
|
||||
if (isCreate) {
|
||||
|
|
@ -227,7 +249,12 @@ export default function RoleManagementPage() {
|
|||
}
|
||||
const res = await fetch(endpoint, {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!res.ok) {
|
||||
|
|
@ -248,7 +275,17 @@ export default function RoleManagementPage() {
|
|||
if (!window.confirm(`Delete role "${roleName}"?`)) return;
|
||||
setOpenMenuId(null);
|
||||
try {
|
||||
const res = await fetch(`${API}/api/admin/roles/${id}`, { method: 'DELETE' });
|
||||
const accessToken = typeof sessionStorage !== 'undefined'
|
||||
? sessionStorage.getItem('nxtgauge_admin_access_token') || ''
|
||||
: '';
|
||||
const res = await fetch(`${API}/api/admin/roles/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
|
||||
},
|
||||
credentials: 'include',
|
||||
});
|
||||
if (!res.ok) throw new Error(`Request failed (${res.status})`);
|
||||
await load();
|
||||
} catch (err: any) {
|
||||
|
|
@ -259,9 +296,17 @@ export default function RoleManagementPage() {
|
|||
const toggleStatus = async (row: RoleRecord) => {
|
||||
setOpenMenuId(null);
|
||||
try {
|
||||
const accessToken = typeof sessionStorage !== 'undefined'
|
||||
? sessionStorage.getItem('nxtgauge_admin_access_token') || ''
|
||||
: '';
|
||||
const res = await fetch(`${API}/api/admin/roles/${row.id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ is_active: row.status !== 'ACTIVE' }),
|
||||
});
|
||||
if (!res.ok) throw new Error(`Request failed (${res.status})`);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue