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

362 lines
14 KiB
TypeScript
Raw Normal View History

import { Match, Show, Switch, createEffect, createMemo, createResource, createSignal, onMount } from 'solid-js';
import { useNavigate } from '@solidjs/router';
import { useAuth, RequireAuth } from '~/lib/auth';
import DashboardDesignPreview from '~/components/admin/DashboardDesignPreview';
import DashboardShell from '~/components/DashboardShell';
import ProfilePage from '~/components/dashboard/ProfilePage';
import PortfolioPage from '~/components/dashboard/PortfolioPage';
import VerificationStatusPage from '~/components/dashboard/VerificationStatusPage';
// Sidebar items that have real implementations (not the preview mock)
const REAL_PAGES = ['my profile', 'my portfolio', 'verification'];
type RoleKey =
| 'COMPANY'
| 'CUSTOMER'
| 'JOB_SEEKER'
| 'PHOTOGRAPHER'
| 'MAKEUP_ARTIST'
| 'TUTOR'
| 'DEVELOPER'
| 'VIDEO_EDITOR'
| 'UGC_CONTENT_CREATOR'
| 'GRAPHIC_DESIGNER'
| 'SOCIAL_MEDIA_MANAGER'
| 'FITNESS_TRAINER'
| 'CATERING_SERVICES';
type RuntimeBundle = {
role: RoleKey;
status: 'ACTIVE' | 'INACTIVE';
sidebarItems: string[];
tabs: string[];
widgets: string[];
fields: string[];
source: 'dashboard-config';
};
const API_GATEWAY = '/api/gateway';
const SERVER_API_BASE = (process.env.PUBLIC_API_URL || 'http://localhost:8080/api').replace(/\/+$/, '');
const ROLE_OPTIONS: RoleKey[] = [
'COMPANY',
'CUSTOMER',
'JOB_SEEKER',
'PHOTOGRAPHER',
'MAKEUP_ARTIST',
'TUTOR',
'DEVELOPER',
'VIDEO_EDITOR',
'UGC_CONTENT_CREATOR',
'GRAPHIC_DESIGNER',
'SOCIAL_MEDIA_MANAGER',
'FITNESS_TRAINER',
'CATERING_SERVICES',
];
const ROLE_BASED_SIDEBAR: Record<RoleKey, string[]> = {
PHOTOGRAPHER: ['My Dashboard', 'My Profile', 'My Portfolio', 'Leads', 'My Responses', 'Credits', 'Explore Nxtgauge', 'Verification', 'Help Center', 'Settings', 'Switch Services', 'Logout'],
MAKEUP_ARTIST: ['My Dashboard', 'My Profile', 'My Portfolio', 'Leads', 'My Responses', 'Credits', 'Explore Nxtgauge', 'Verification', 'Help Center', 'Settings', 'Switch Services', 'Logout'],
TUTOR: ['My Dashboard', 'My Profile', 'My Portfolio', 'Leads', 'My Responses', 'Credits', 'Explore Nxtgauge', 'Verification', 'Help Center', 'Settings', 'Switch Services', 'Logout'],
DEVELOPER: ['My Dashboard', 'My Profile', 'My Portfolio', 'Leads', 'My Responses', 'Credits', 'Explore Nxtgauge', 'Verification', 'Help Center', 'Settings', 'Switch Services', 'Logout'],
VIDEO_EDITOR: ['My Dashboard', 'My Profile', 'My Portfolio', 'Leads', 'My Responses', 'Credits', 'Explore Nxtgauge', 'Verification', 'Help Center', 'Settings', 'Switch Services', 'Logout'],
UGC_CONTENT_CREATOR: ['My Dashboard', 'My Profile', 'My Portfolio', 'Leads', 'My Responses', 'Credits', 'Explore Nxtgauge', 'Verification', 'Help Center', 'Settings', 'Switch Services', 'Logout'],
GRAPHIC_DESIGNER: ['My Dashboard', 'My Profile', 'My Portfolio', 'Leads', 'My Responses', 'Credits', 'Explore Nxtgauge', 'Verification', 'Help Center', 'Settings', 'Switch Services', 'Logout'],
SOCIAL_MEDIA_MANAGER: ['My Dashboard', 'My Profile', 'My Portfolio', 'Leads', 'My Responses', 'Credits', 'Explore Nxtgauge', 'Verification', 'Help Center', 'Settings', 'Switch Services', 'Logout'],
FITNESS_TRAINER: ['My Dashboard', 'My Profile', 'My Portfolio', 'Leads', 'My Responses', 'Credits', 'Explore Nxtgauge', 'Verification', 'Help Center', 'Settings', 'Switch Services', 'Logout'],
CATERING_SERVICES: ['My Dashboard', 'My Profile', 'My Portfolio', 'Leads', 'My Responses', 'Credits', 'Explore Nxtgauge', 'Verification', 'Help Center', 'Settings', 'Switch Services', 'Logout'],
COMPANY: ['My Dashboard', 'My Profile', 'Jobs', 'Applications', 'Shortlisted Candidates', 'Credits', 'Explore Nxtgauge', 'Verification', 'Help Center', 'Settings', 'Switch Services', 'Logout'],
JOB_SEEKER: ['My Dashboard', 'My Profile', 'Jobs', 'My Applications', 'Saved Jobs', 'Explore Nxtgauge', 'Verification', 'Help Center', 'Settings', 'Switch Services', 'Logout'],
CUSTOMER: ['My Dashboard', 'My Profile', 'My Requirements', 'Received Responses', 'Shortlisted Responses', 'Credits', 'Explore Nxtgauge', 'Verification', 'Help Center', 'Settings', 'Switch Services', 'Logout'],
};
const ROLE_PREFIXES: Record<string, string> = {
PHOTOGRAPHER: 'photographers',
MAKEUP_ARTIST: 'makeup-artists',
TUTOR: 'tutors',
DEVELOPER: 'developers',
VIDEO_EDITOR: 'video-editors',
GRAPHIC_DESIGNER: 'graphic-designers',
SOCIAL_MEDIA_MANAGER: 'social-media-managers',
FITNESS_TRAINER: 'fitness-trainers',
CATERING_SERVICES: 'catering-services',
UGC_CONTENT_CREATOR: 'ugc-content-creators',
CUSTOMER: 'customers',
COMPANY: 'companies',
JOB_SEEKER: 'jobseeker',
};
function getNameFromStorage(): string {
if (typeof window === 'undefined') return 'User';
const keys = ['nxtgauge_signup_profile_v1', 'nxtgauge_auth_user', 'nxtgauge_user'];
for (const key of keys) {
try {
const raw = window.localStorage.getItem(key) || window.sessionStorage.getItem(key);
if (!raw) continue;
const parsed = JSON.parse(raw);
const name = parsed?.name
|| parsed?.first_name
|| parsed?.user?.name
|| parsed?.user?.first_name;
if (name) return String(name);
} catch { /* ignore */ }
}
return 'User';
}
const EXPLORE_ROLES = [
{ key: 'PHOTOGRAPHER', name: 'Photographer' },
{ key: 'MAKEUP_ARTIST', name: 'Makeup Artist' },
{ key: 'TUTOR', name: 'Tutor' },
{ key: 'DEVELOPER', name: 'Developer' },
{ key: 'VIDEO_EDITOR', name: 'Video Editor' },
{ key: 'UGC_CONTENT_CREATOR', name: 'UGC Content Creator' },
{ key: 'GRAPHIC_DESIGNER', name: 'Graphic Designer' },
{ key: 'SOCIAL_MEDIA_MANAGER', name: 'Social Media Manager' },
{ key: 'FITNESS_TRAINER', name: 'Fitness Trainer' },
{ key: 'CATERING_SERVICES', name: 'Catering Services' },
];
function normalizeRole(value: string): RoleKey {
const up = String(value || '').trim().toUpperCase().replace(/\s+/g, '_');
return (ROLE_OPTIONS.find((r) => r === up) || 'JOB_SEEKER') as RoleKey;
}
function asStringArray(value: unknown): string[] {
if (!Array.isArray(value)) return [];
return value
.map((item) => String(item || '').trim())
.filter(Boolean);
}
function getInitialRoleFromStorage(): RoleKey {
if (typeof window === 'undefined') return 'JOB_SEEKER';
const keys = ['nxtgauge_signup_profile_v1', 'nxtgauge_auth_user', 'nxtgauge_user'];
for (const key of keys) {
try {
const raw = window.localStorage.getItem(key) || window.sessionStorage.getItem(key);
if (!raw) continue;
const parsed = JSON.parse(raw);
const found = normalizeRole(
String(
parsed?.roleKey
|| parsed?.role
|| parsed?.active_role
|| parsed?.user?.active_role
|| parsed?.user?.roles?.[0]
|| '',
),
);
if (ROLE_OPTIONS.includes(found)) return found;
} catch {
// ignore invalid payload
}
}
return 'JOB_SEEKER';
}
async function fetchJson(path: string): Promise<any | null> {
try {
const isServer = typeof window === 'undefined';
const target = isServer
? `${SERVER_API_BASE}${path}`
: `${API_GATEWAY}${path}`;
const res = await fetch(target, { credentials: 'include' });
if (!res.ok) return null;
return await res.json();
} catch {
return null;
}
}
async function loadRoleBundle(role: RoleKey): Promise<RuntimeBundle | null> {
if (typeof window === 'undefined') {
return null;
}
let payload = await fetchJson(`/api/config/dashboard/by-key/${encodeURIComponent(role)}?audience=EXTERNAL`);
if (!payload) {
const listPayload = await fetchJson('/api/admin/dashboard-config?audience=EXTERNAL');
const rows = Array.isArray(listPayload) ? listPayload : (Array.isArray(listPayload?.items) ? listPayload.items : []);
const matched = rows.find((row: any) => String(row?.role_key || row?.config_json?.role_key || '').toUpperCase() === role);
if (matched) payload = matched;
}
const config = (payload?.config_json || payload || null) as Record<string, unknown> | null;
if (!config) return null;
const sidebarItems = asStringArray((config as any)?.sidebar_items ?? (config as any)?.sidebarItems);
const tabs = asStringArray((config as any)?.tabs);
const widgetsRaw = Array.isArray((config as any)?.widgets) ? (config as any).widgets : [];
const widgets = widgetsRaw
.map((item: any) => String(typeof item === 'string' ? item : (item?.key || item?.id || '')).trim())
.filter(Boolean);
const fields = asStringArray((config as any)?.fields);
return {
role,
status: payload?.is_active === false ? 'INACTIVE' : 'ACTIVE',
sidebarItems,
tabs,
widgets,
fields,
source: 'dashboard-config',
};
}
function mergeSidebar(role: RoleKey, runtimeSidebar: string[]): string[] {
const base = ROLE_BASED_SIDEBAR[role] || ['My Dashboard', 'My Profile', 'Switch Services', 'Logout'];
const fromRuntime = runtimeSidebar.filter(Boolean);
const map = new Map<string, string>();
for (const item of [...fromRuntime, ...base]) {
const key = item.trim().toLowerCase();
if (!map.has(key)) map.set(key, item);
}
let merged = Array.from(map.values());
if (role === 'JOB_SEEKER') {
merged = merged.filter((item) => item.trim().toLowerCase() !== 'credits');
}
if (!merged.some((item) => item.trim().toLowerCase() === 'explore nxtgauge')) {
const insertBefore = merged.findIndex((item) => item.trim().toLowerCase() === 'verification');
if (insertBefore >= 0) merged.splice(insertBefore, 0, 'Explore Nxtgauge');
else merged.push('Explore Nxtgauge');
}
return merged;
}
export default function RuntimeDashboardPage() {
const navigate = useNavigate();
const auth = useAuth();
const [hydrated, setHydrated] = createSignal(false);
const [role, setRole] = createSignal<RoleKey>('JOB_SEEKER');
const [activeSidebar, setActiveSidebar] = createSignal('My Dashboard');
const [activeTab, setActiveTab] = createSignal('overview');
const [userName, setUserName] = createSignal('User');
const [userId, setUserId] = createSignal('');
onMount(() => {
setHydrated(true);
const storedRole = getInitialRoleFromStorage();
setRole(storedRole);
setUserName(getNameFromStorage());
if (auth.user()) {
const u = auth.user()!;
if (u.full_name) setUserName(u.full_name);
if (u.id) setUserId(u.id);
if (u.active_role) setRole(normalizeRole(u.active_role));
}
});
createEffect(() => {
const u = auth.user();
if (u) {
if (u.full_name && userName() === 'User') setUserName(u.full_name);
if (u.id && !userId()) setUserId(u.id);
if (u.active_role) setRole(normalizeRole(u.active_role));
}
});
const [bundle] = createResource(
() => role(),
loadRoleBundle,
);
const sidebarItems = createMemo(() => mergeSidebar(role(), bundle()?.sidebarItems || []));
createEffect(() => {
const first = sidebarItems()[0] || 'My Dashboard';
const current = activeSidebar();
const exists = sidebarItems().some((item) => item.toLowerCase() === current.toLowerCase());
if (!exists) setActiveSidebar(first);
});
const tabs = createMemo(() => {
const fromRuntime = bundle()?.tabs || [];
return fromRuntime.length > 0 ? fromRuntime : ['overview'];
});
createEffect(() => {
role();
const firstTab = tabs()[0] || 'overview';
setActiveTab((prev) => (prev ? prev : firstTab));
});
const loading = createMemo(() => !hydrated() || bundle.loading);
const ready = createMemo(() => hydrated() && !bundle.loading);
const liveData = createMemo(() => {
const prefix = ROLE_PREFIXES[role()];
if (!prefix) return undefined;
return { userName: userName(), userId: userId(), rolePrefix: prefix };
});
const isRealPage = createMemo(() =>
REAL_PAGES.includes(activeSidebar().toLowerCase()),
);
return (
<RequireAuth>
<main style={{ 'min-height': '100vh', background: '#F3F4F6' }}>
<Show when={loading()}>
<div style={cardStyle}>Loading dashboard</div>
</Show>
<Show when={ready()}>
{/* ── Real pages: DashboardShell + actual components ── */}
<Show when={isRealPage()}>
<DashboardShell
sidebarItems={sidebarItems()}
activeSidebar={activeSidebar()}
onSidebarSelect={setActiveSidebar}
roleKey={role()}
userName={userName()}
>
<Switch>
<Match when={activeSidebar().toLowerCase() === 'my profile'}>
<ProfilePage roleKey={role()} />
</Match>
<Match when={activeSidebar().toLowerCase() === 'my portfolio'}>
<PortfolioPage roleKey={role()} />
</Match>
<Match when={activeSidebar().toLowerCase() === 'verification'}>
<VerificationStatusPage
roleKey={role()}
onNavigate={setActiveSidebar}
/>
</Match>
</Switch>
</DashboardShell>
</Show>
{/* ── All other views: DashboardDesignPreview mock ── */}
<Show when={!isRealPage()}>
<DashboardDesignPreview
status={bundle()?.status ?? 'ACTIVE'}
sidebarItems={sidebarItems()}
activeSidebar={activeSidebar()}
onSidebarSelect={setActiveSidebar}
tabs={tabs()}
activeTab={activeTab()}
onTabSelect={setActiveTab}
widgets={bundle()?.widgets || []}
fields={bundle()?.fields || []}
mode="customer_external"
roleKey={role()}
exploreRoles={EXPLORE_ROLES}
hidePreviewHeader
liveData={liveData()}
/>
</Show>
</Show>
</main>
</RequireAuth>
);
}
const cardStyle = {
border: '1px solid #E5E7EB',
'border-radius': '12px',
padding: '16px',
background: '#fff',
color: '#111827',
} as const;