Wire user dashboard to real APIs — profile, credits, leads, requirements, jobs

- dashboard.tsx: fetch session for real user name/ID/role, fallback to
  localStorage; show dashboard with role defaults when runtime config unavailable
- DashboardDesignPreview: add liveData prop; createResource for credits,
  marketplace, lead requests, customer requirements, jobs, and profile
- Profile form: inputs now track state via profileFormData signal; pre-filled
  from GET /api/${prefix}/profile/me; Save Changes PATCHes real endpoint
- Lead actions: Send Request POSTs to /api/${prefix}/leads/request; Cancel
  DELETEs /api/${prefix}/leads/requests/{id}; both refetch after completion
- Requirement submit: POSTs to /api/customers/requirements then submits for approval
- Replace hardcoded "Alex" with real session name; credits from wallet balance API
- Fix launch.json PATH so npm is found in sh shell

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ashwin Kumar 2026-04-05 21:13:54 +02:00
parent a8ad2b0620
commit 564a383a10
2 changed files with 310 additions and 18 deletions

View file

@ -1,4 +1,4 @@
import { For, Show, createEffect, createMemo, createSignal } from 'solid-js';
import { For, Show, createEffect, createMemo, createResource, createSignal } from 'solid-js';
import {
Award,
Bell,
@ -539,6 +539,8 @@ function portfolioMediaConfig(roleKey: string): {
|| normalized === 'GRAPHIC_DESIGNER'
|| normalized === 'SOCIAL_MEDIA_MANAGER'
|| normalized === 'CATERING_SERVICES'
|| normalized === 'DEVELOPER'
|| normalized === 'FITNESS_TRAINER'
) {
return {
mode: 'visual',
@ -576,6 +578,10 @@ function customerViewFor(sidebar: string, roleKey: string): CustomerView {
return { title: 'Received Professional Responses', subtitle: 'Review and manage professional applications for active requirements.', tabs: ['all responses', 'new', 'shortlisted', 'rejected'] };
}
if (key === 'shortlisted responses') return { title: 'Shortlisted Responses', subtitle: 'Focus on high-intent responses and convert them to confirmed engagements.', tabs: ['all shortlisted', 'interview scheduled', 'finalized'] };
if (key === 'applications') return { title: 'Applications', subtitle: 'Review all candidate applications received for your active job postings.', tabs: ['all applications', 'shortlisted', 'under review', 'rejected'], cta: 'View Job Postings' };
if (key === 'shortlisted candidates') return { title: 'Shortlisted Candidates', subtitle: 'Manage candidates you have shortlisted across all job postings.', tabs: ['shortlisted', 'interview scheduled', 'offer extended'] };
if (key === 'my applications') return { title: 'My Applications', subtitle: 'Track the status of all jobs you have applied to.', tabs: ['all', 'under review', 'shortlisted', 'rejected'], cta: 'Browse Jobs' };
if (key === 'saved jobs') return { title: 'Saved Jobs', subtitle: 'Jobs you have bookmarked. Apply before they expire.', tabs: ['saved', 'expiring soon'], cta: 'Browse Jobs' };
if (key.includes('profile')) {
const spec = profileSpecForRole(roleKey);
return { title: spec.title, subtitle: spec.subtitle, tabs: spec.tabs, cta: 'Save Changes' };
@ -1046,6 +1052,7 @@ export default function DashboardDesignPreview(props: {
exploreRoles?: Array<{ key: string; name: string }>;
onOpenFullscreen?: () => void;
hidePreviewHeader?: boolean;
liveData?: { userName: string; userId: string; rolePrefix: string };
}) {
const isProfessionalRoleKey = (roleKey: string) => {
const role = normalizeRoleKey(roleKey);
@ -1129,6 +1136,9 @@ export default function DashboardDesignPreview(props: {
const [ticketMessage, setTicketMessage] = createSignal('');
const [createTicketFiles, setCreateTicketFiles] = createSignal<string[]>([]);
const [viewTicketFiles, setViewTicketFiles] = createSignal<string[]>([]);
const [profileFormData, setProfileFormData] = createSignal<Record<string, string>>({});
const [profileSaving, setProfileSaving] = createSignal(false);
const [profileSaveStatus, setProfileSaveStatus] = createSignal<'idle' | 'saved' | 'error'>('idle');
const [profileSettingsTab, setProfileSettingsTab] = createSignal<'change_password' | 'notifications' | 'privacy'>('change_password');
const [showDeleteAccountModal, setShowDeleteAccountModal] = createSignal(false);
const [portfolioEditMode, setPortfolioEditMode] = createSignal(false);
@ -1234,6 +1244,19 @@ export default function DashboardDesignPreview(props: {
const submitRequirementForReview = () => {
const roleKey = selectedRequirementRole();
const roleLabel = titleCase(roleKey.replace(/_/g, ' ').toLowerCase());
if (hasLive()) {
apiPost('/api/customers/requirements', {
profession_key: roleKey,
title: `${roleLabel} Requirement`,
description: 'Submitted via dashboard',
location: 'Chennai',
budget_min: 10000000,
budget_max: 20000000,
}).then((res: any) => res?.json?.().then((r: any) => {
// After creating, submit for approval
if (r?.id) apiPost(`/api/customers/requirements/${r.id}/submit`, {});
})).then(() => refetchRequirementsLive());
}
const id = `#REQ-${Math.floor(9200 + Math.random() * 899)}`;
const submission = new Date().toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' });
const newRequirement: RequirementRow = {
@ -1294,6 +1317,189 @@ export default function DashboardDesignPreview(props: {
if (state === 'REJECTED') return { border: '#E5E7EB', bg: '#F9FAFB', text: '#374151', label: 'Rejected' };
return { border: '#E5E7EB', bg: '#F9FAFB', text: '#374151', label: 'Draft' };
};
// ─── Live API integration (customer_external mode with liveData) ──────────
const hasLive = () => isCustomerExternalMode() && !!props.liveData;
const livePrefix = () => props.liveData?.rolePrefix ?? '';
const GW = '/api/gateway';
const apiFetch = (path: string) =>
fetch(`${GW}${path}`, { credentials: 'include' })
.then((r) => (r.ok ? r.json() : null))
.catch(() => null);
const apiPost = (path: string, body: unknown) =>
fetch(`${GW}${path}`, {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
}).catch(() => null);
const apiDelete = (path: string) =>
fetch(`${GW}${path}`, { method: 'DELETE', credentials: 'include' }).catch(() => null);
// Credits balance
const [creditsResource] = createResource(
() => (hasLive() ? livePrefix() : null),
(prefix) => apiFetch(`/api/${prefix}/wallet/balance`),
);
// Marketplace requirements (professionals)
const [marketplaceResource] = createResource(
() => (hasLive() && isProfessionalRole() ? livePrefix() : null),
(prefix) => apiFetch(`/api/${prefix}/marketplace?limit=50`),
);
// My lead requests (professionals)
const [leadRequestsResource, { refetch: refetchLeadRequestsLive }] = createResource(
() => (hasLive() && isProfessionalRole() ? livePrefix() : null),
(prefix) => apiFetch(`/api/${prefix}/leads/requests/me?limit=50`),
);
// Customer requirements
const [requirementsResource, { refetch: refetchRequirementsLive }] = createResource(
() => (hasLive() && normalizeRoleKey(props.roleKey ?? '') === 'CUSTOMER' ? 'yes' : null),
() => apiFetch('/api/customers/requirements?limit=50'),
);
// Jobs board
const [jobsResource] = createResource(
() => {
const r = normalizeRoleKey(props.roleKey ?? '');
return hasLive() && (r === 'JOB_SEEKER' || r === 'COMPANY') ? r : null;
},
() => apiFetch('/api/jobs?limit=50'),
);
// User profile (all roles)
const [profileResource] = createResource(
() => (hasLive() ? livePrefix() : null),
(prefix) => apiFetch(`/api/${prefix}/profile/me`),
);
// Sync resources → local signals
createEffect(() => {
const d = creditsResource();
if (d != null && typeof d.balance === 'number') setLeadCredits(d.balance);
});
createEffect(() => {
const d = marketplaceResource();
if (!d) return;
const items: any[] = Array.isArray(d.items) ? d.items : Array.isArray(d) ? d : [];
if (!items.length) return;
setLeadCards(items.map((item: any) => ({
id: String(item.id ?? item.requirement_id ?? ''),
title: String(item.title ?? item.description ?? 'Requirement'),
category: String(item.category ?? item.profession_key ?? props.roleKey ?? ''),
location: String(item.location ?? item.city ?? 'India'),
area: String(item.area ?? item.locality ?? ''),
dateRequired: item.required_by
? new Date(item.required_by).toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' })
: 'TBD',
urgency: item.urgency === 'HIGH' ? 'High' : item.urgency === 'MEDIUM' ? 'Medium' : 'Low',
budget: item.budget_min != null
? `${Math.round(item.budget_min / 100).toLocaleString('en-IN')} - ₹${Math.round((item.budget_max ?? item.budget_min) / 100).toLocaleString('en-IN')}`
: '₹0',
budgetValue: Number(item.budget_max ?? item.budget_min ?? 0) / 100,
priceRange: item.budget_min != null
? `${Math.round(item.budget_min / 100).toLocaleString('en-IN')} - ₹${Math.round((item.budget_max ?? item.budget_min) / 100).toLocaleString('en-IN')}`
: '₹0',
cost: 25,
status: 'open' as const,
match: '80% match',
contactCount: Number(item.contact_count ?? 0),
maxContacts: Number(item.max_contacts ?? 10),
})));
});
createEffect(() => {
const d = leadRequestsResource();
if (!d) return;
const items: any[] = Array.isArray(d.items) ? d.items : Array.isArray(d) ? d : [];
if (!items.length) return;
const statusMap: Record<string, any> = {
PENDING: 'request_sent', CONTACT_UNLOCKED: 'contact_unlocked',
REJECTED: 'rejected', CANCELLED: 'cancelled_by_professional', EXPIRED: 'expired_refunded',
};
setLeadRequestRows(items.map((item: any) => ({
id: String(item.id ?? ''),
title: String(item.title ?? item.requirement_title ?? 'Lead Request'),
city: String(item.location ?? item.city ?? 'India'),
requestDate: item.created_at
? new Date(item.created_at).toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' })
: '--',
status: statusMap[String(item.status ?? '')] ?? 'request_sent',
decisionDate: item.decision_date
? new Date(item.decision_date).toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' })
: '--',
})));
});
createEffect(() => {
const d = requirementsResource();
if (!d) return;
const items: any[] = Array.isArray(d.items) ? d.items : Array.isArray(d) ? d : [];
if (!items.length) return;
const statusMap: Record<string, string> = {
PENDING: 'under review', APPROVED: 'approved', ACTIVE: 'active',
REJECTED: 'closed', CLOSED: 'closed',
};
setRequirementRows(items.map((item: any) => ({
id: String(item.id ?? ''),
title: String(item.title ?? item.description ?? 'Requirement'),
summary: String(item.description ?? ''),
category: String(item.category ?? item.profession_key ?? ''),
budget: item.budget_min != null
? `${Math.round(item.budget_min / 100).toLocaleString('en-IN')} - ₹${Math.round((item.budget_max ?? item.budget_min) / 100).toLocaleString('en-IN')}`
: '₹0',
location: String(item.location ?? 'India'),
submission: item.created_at
? new Date(item.created_at).toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' })
: '--',
status: statusMap[String(item.status ?? '')] ?? 'under review',
responseTag: `Active (${Number(item.response_count ?? 0)} Responses)`,
})));
});
createEffect(() => {
const d = jobsResource();
if (!d) return;
const items: any[] = Array.isArray(d.items) ? d.items : Array.isArray(d) ? d : [];
if (!items.length) return;
setJobBoardJobs(items.map((item: any) => ({
id: String(item.id ?? ''),
title: String(item.title ?? 'Position'),
company: String(item.company_name ?? item.company ?? 'Company'),
location: String(item.location ?? 'India'),
salary: item.salary_min != null
? `${Math.round(item.salary_min / 100).toLocaleString('en-IN')}+`
: 'Negotiable',
exp: String(item.experience_required ?? item.experience ?? '0-2 yrs'),
type: String(item.employment_type ?? item.type ?? 'Full-Time'),
tags: Array.isArray(item.tags) ? item.tags : [],
match: '',
posted: item.created_at
? new Date(item.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
: '--',
})));
});
createEffect(() => {
const d = profileResource();
if (!d) return;
const parts = String(d.display_name || d.full_name || d.business_name || '').split(' ');
const map: Record<string, string> = {};
map['First Name'] = d.first_name || parts[0] || '';
map['Last Name'] = d.last_name || parts.slice(1).join(' ') || '';
map['Business Name'] = d.business_name || d.display_name || '';
map['Contact Person Name'] = d.contact_person || d.display_name || '';
map['Company Name'] = d.company_name || d.display_name || '';
map['Email Address'] = d.email || '';
map['Mobile Number'] = d.phone || d.mobile || '';
if (d.location) map['City'] = d.location;
if (d.area) map['Area'] = d.area;
if (d.state) map['State'] = d.state;
if (d.pin_code) map['PIN Code'] = d.pin_code;
if (d.bio) map['Bio'] = d.bio;
if (d.status) setProfileApprovalState(
d.status === 'APPROVED' ? 'APPROVED'
: d.status === 'REJECTED' ? 'REJECTED'
: d.status === 'PENDING' ? 'IN_REVIEW'
: 'DRAFT',
);
setProfileFormData((prev) => ({ ...prev, ...map }));
});
// ─── End live API integration ─────────────────────────────────────────────
const submitProfileForApproval = () => {
setProfileApprovalState('SUBMITTED');
setTimeout(() => setProfileApprovalState('IN_REVIEW'), 250);
@ -1568,6 +1774,10 @@ export default function DashboardDesignPreview(props: {
const requestLeadContact = (leadId: string) => {
if (usableLeadCredits() < leadCostPerContact) return;
if (hasLive()) {
apiPost(`/api/${livePrefix()}/leads/request`, { requirement_id: leadId })
.then(() => refetchLeadRequestsLive());
}
let changed = false;
setLeadCards((prev) => prev.map((card) => {
if (card.id !== leadId || card.status !== 'open' || card.contactCount >= card.maxContacts) return card;
@ -1643,6 +1853,10 @@ export default function DashboardDesignPreview(props: {
};
const cancelLeadRequest = (leadId: string) => {
if (hasLive()) {
apiDelete(`/api/${livePrefix()}/leads/requests/${leadId}`)
.then(() => refetchLeadRequestsLive());
}
setLeadCards((prev) => prev.map((card) => {
if (card.id !== leadId) return card;
if (card.status !== 'requested' && card.status !== 'closed') return card;
@ -2613,7 +2827,7 @@ export default function DashboardDesignPreview(props: {
</div>
<div style="display:grid;grid-template-columns:2fr 1fr 1fr;gap:10px">
<div style="border:1px solid #E5E7EB;background:white;border-radius:16px;padding:14px;box-shadow:0 1px 4px rgba(0,0,0,0.06)">
<p style="margin:0;font-size:32px;font-weight:800;color:#111827;line-height:1.1">Welcome back, Alex</p>
<p style="margin:0;font-size:32px;font-weight:800;color:#111827;line-height:1.1">Welcome back, {props.liveData?.userName ?? 'Alex'}</p>
<p style="margin:6px 0 0;font-size:13px;color:#6B7280">To start receiving opportunities, complete My Profile and My Portfolio, then submit both for approval.</p>
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:10px">
<button type="button" onClick={() => { props.onSidebarSelect('My Profile'); props.onTabSelect('basic information'); }} style="height:30px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 10px;font-size:11px;font-weight:700;color:#374151">Fill My Profile</button>
@ -2629,7 +2843,7 @@ export default function DashboardDesignPreview(props: {
</div>
<div style="border:1px solid #E5E7EB;background:white;border-radius:16px;padding:14px;box-shadow:0 1px 4px rgba(0,0,0,0.06)">
<p style="margin:0;font-size:11px;letter-spacing:0.04em;text-transform:uppercase;color:#6B7280">Credits Balance</p>
<p style="margin:8px 0 0;font-size:34px;font-weight:800;line-height:1;color:#111827">2,500</p>
<p style="margin:8px 0 0;font-size:34px;font-weight:800;line-height:1;color:#111827">{leadCredits().toLocaleString('en-IN')}</p>
</div>
</div>
<div style="display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:10px">
@ -2708,8 +2922,9 @@ export default function DashboardDesignPreview(props: {
<div style="position:relative">
<input
type="text"
value={isCityField ? 'Chennai' : ''}
value={profileFormData()[field] ?? (isCityField ? 'Chennai' : '')}
readOnly={isCityField}
onInput={(e) => !isCityField && setProfileFormData((prev) => ({ ...prev, [field]: e.currentTarget.value }))}
placeholder={placeholderText}
style="width:100%;height:36px;border:1px solid #E5E7EB;border-radius:8px;background:white;padding:0 30px 0 10px;font-size:12px;color:#111827;outline:none"
/>
@ -2909,8 +3124,38 @@ export default function DashboardDesignPreview(props: {
</div>
</Show>
<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:12px;padding-top:12px;border-top:1px solid #E5E7EB">
<button type="button" style="height:34px;border-radius:8px;border:1px solid #D1D5DB;background:white;color:#374151;padding:0 14px;font-size:12px;font-weight:700">Cancel</button>
<button type="button" style="height:34px;border-radius:8px;border:none;background:#03004E;color:white;padding:0 14px;font-size:12px;font-weight:700">Save Changes</button>
<button type="button" onClick={() => setProfileFormData({})} style="height:34px;border-radius:8px;border:1px solid #D1D5DB;background:white;color:#374151;padding:0 14px;font-size:12px;font-weight:700">Cancel</button>
<button
type="button"
onClick={() => {
if (!hasLive()) return;
const data = profileFormData();
setProfileSaving(true);
const fn = data['First Name'] || '';
const ln = data['Last Name'] || '';
const display = data['Business Name'] || data['Company Name'] || data['Contact Person Name'] || [fn, ln].filter(Boolean).join(' ');
fetch(`${GW}/api/${livePrefix()}/profile/me`, {
method: 'PATCH',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
display_name: display || undefined,
bio: data['Bio'] || undefined,
location: data['City'] || undefined,
area: data['Area'] || undefined,
state: data['State'] || undefined,
pin_code: data['PIN Code'] || undefined,
}),
}).then((r) => {
setProfileSaving(false);
setProfileSaveStatus(r.ok ? 'saved' : 'error');
setTimeout(() => setProfileSaveStatus('idle'), 2500);
}).catch(() => { setProfileSaving(false); setProfileSaveStatus('error'); });
}}
style={`height:34px;border-radius:8px;border:none;background:${profileSaveStatus() === 'error' ? '#DC2626' : profileSaveStatus() === 'saved' ? '#16A34A' : '#03004E'};color:white;padding:0 14px;font-size:12px;font-weight:700`}
>
{profileSaving() ? 'Saving…' : profileSaveStatus() === 'saved' ? 'Saved ✓' : profileSaveStatus() === 'error' ? 'Error — Retry' : 'Save Changes'}
</button>
</div>
</div>
<div style="border:1px solid #E5E7EB;background:white;border-radius:16px;padding:14px;box-shadow:0 1px 4px rgba(0,0,0,0.06)">

View file

@ -58,9 +58,43 @@ const ROLE_BASED_SIDEBAR: Record<RoleKey, string[]> = {
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', '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' },
@ -182,10 +216,25 @@ export default function RuntimeDashboardPage() {
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);
setRole(getInitialRoleFromStorage());
setUserName(getNameFromStorage());
// Fetch fresh session data
fetchJson('/api/auth/session').then((data) => {
if (!data) return;
const name = data.full_name
|| data.name
|| (data.first_name ? `${data.first_name} ${data.last_name ?? ''}`.trim() : '')
|| data.email?.split('@')[0]
|| '';
if (name) setUserName(name);
if (data.id || data.user_id) setUserId(String(data.id || data.user_id));
if (data.active_role) setRole(normalizeRole(data.active_role));
});
});
const [bundle] = createResource(
@ -216,6 +265,12 @@ export default function RuntimeDashboardPage() {
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 };
});
return (
<main style={{ 'min-height': '100vh', background: '#f3f4f6' }}>
<div>
@ -224,18 +279,9 @@ export default function RuntimeDashboardPage() {
<div style={cardStyle}>Loading dashboard...</div>
</Show>
<Show when={ready() && !bundle()}>
<div style={cardStyle}>
<h2 style={{ margin: 0, 'font-size': '18px' }}>Dashboard Unavailable</h2>
<p style={{ margin: '8px 0 0', color: '#6B7280' }}>
Dashboard configuration was not found for your role. Please contact support.
</p>
</div>
</Show>
<Show when={ready() && bundle()}>
<Show when={ready()}>
<DashboardDesignPreview
status={bundle()!.status}
status={bundle()?.status ?? 'ACTIVE'}
sidebarItems={sidebarItems()}
activeSidebar={activeSidebar()}
onSidebarSelect={setActiveSidebar}
@ -248,6 +294,7 @@ export default function RuntimeDashboardPage() {
roleKey={role()}
exploreRoles={EXPLORE_ROLES}
hidePreviewHeader
liveData={liveData()}
/>
</Show>
</div>