feat(dashboard): add My Portfolio to all professional role dashboards

- Add full single-page portfolio preview in DashboardDesignPreview with
  profile header, about, packages, gallery, experience, testimonials,
  and FAQs sections matching Department Management design language
- Fix sidebar persona resolution to always include My Portfolio for all
  professional subtypes (Photographer, Graphic Designer, Makeup Artist,
  Tutor, Developer, Video Editor, Social Media Manager, Fitness Trainer,
  Catering Services) via a three-layer fallback: roleId lookup → role
  key from roles list → formRoleKey stored directly from dashboard record
- Add personaFromKey() helper so any professional role key maps to
  PROFESSIONAL sidebar even when the roles API returns empty data

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ashwin Kumar 2026-03-30 14:19:00 +02:00
parent 24353ef27a
commit 8a24700e73
2 changed files with 575 additions and 14 deletions

View file

@ -1,21 +1,29 @@
import { For, Show, createEffect, createMemo, createSignal } from 'solid-js'; import { For, Show, createEffect, createMemo, createSignal } from 'solid-js';
import { import {
Award,
Bell, Bell,
BadgeCheck, BadgeCheck,
Bookmark, Bookmark,
BriefcaseBusiness, BriefcaseBusiness,
Camera, Camera,
CheckCircle2,
ChevronDown,
ChevronUp,
Clapperboard, Clapperboard,
Compass, Compass,
Coins, Coins,
Code2, Code2,
Dumbbell, Dumbbell,
Edit2,
Eye,
FileText, FileText,
GraduationCap, GraduationCap,
HandHelping, HandHelping,
HeadphonesIcon, HeadphonesIcon,
Image,
LayoutGrid, LayoutGrid,
LogOut, LogOut,
MapPin,
Megaphone, Megaphone,
PenTool, PenTool,
RefreshCw, RefreshCw,
@ -25,11 +33,13 @@ import {
Settings, Settings,
Settings2, Settings2,
ShieldCheck, ShieldCheck,
Star,
TrendingUp, TrendingUp,
UtensilsCrossed, UtensilsCrossed,
User, User,
UserCircle2, UserCircle2,
Users, Users,
X,
} from 'lucide-solid'; } from 'lucide-solid';
function titleCase(value: string) { function titleCase(value: string) {
@ -52,6 +62,7 @@ function StatusBadge(props: { status: 'ACTIVE' | 'INACTIVE' }) {
function sidebarIcon(label: string) { function sidebarIcon(label: string) {
const key = String(label || '').toLowerCase().trim(); const key = String(label || '').toLowerCase().trim();
if (key.includes('dashboard')) return LayoutGrid; if (key.includes('dashboard')) return LayoutGrid;
if (key.includes('portfolio')) return Image;
if (key.includes('profile')) return UserCircle2; if (key.includes('profile')) return UserCircle2;
if (key.includes('lead')) return HandHelping; if (key.includes('lead')) return HandHelping;
if (key.includes('response')) return FileText; if (key.includes('response')) return FileText;
@ -274,6 +285,244 @@ function profileSpecForRole(roleKey: string): ProfileSpec {
return PROFILE_SPECS[normalized] || PROFILE_SPECS.PROFESSIONAL; return PROFILE_SPECS[normalized] || PROFILE_SPECS.PROFESSIONAL;
} }
type PortfolioSpec = {
roleLabel: string;
tabs: string[];
specialties: string[];
statsLabels: string[];
equipmentLabel: string;
galleryTabLabel: string;
experienceTabLabel: string;
serviceTabLabel: string;
faqItems: Array<{ q: string; a: string }>;
packages: Array<{ name: string; price: string; items: string[] }>;
};
const PORTFOLIO_SPECS: Record<string, PortfolioSpec> = {
PHOTOGRAPHER: {
roleLabel: 'Photographer',
tabs: ['overview', 'about', 'services & pricing', 'portfolio gallery', 'experience & equipment', 'testimonials', 'faqs'],
specialties: ['Wedding', 'Pre-Wedding', 'Candid', 'Event', 'Portrait', 'Lifestyle'],
statsLabels: ['Shoots Done', 'Years Exp', 'Verified Pro', 'Last Delivery', 'Will Travel'],
equipmentLabel: 'Camera & Equipment',
galleryTabLabel: 'portfolio gallery',
experienceTabLabel: 'experience & equipment',
serviceTabLabel: 'services & pricing',
faqItems: [
{ q: 'Do you travel for shoots?', a: 'Yes, I cover Pan-India and select international locations. Travel charges apply beyond 50km.' },
{ q: 'How long before I receive the final photos?', a: 'Edited photos are delivered within 1014 working days after the shoot.' },
{ q: 'What equipment do you use?', a: 'I shoot with Canon EOS R6 and Sony A7IV, with a full range of prime and zoom lenses.' },
{ q: 'Do you provide raw files?', a: 'Raw files are not included by default. They can be added as an optional upgrade.' },
],
packages: [
{ name: 'Essential', price: '₹15,000', items: ['4-hour shoot', '100 edited photos', 'Online gallery', '1 location'] },
{ name: 'Premium', price: '₹28,000', items: ['8-hour shoot', '250 edited photos', 'Online gallery', '2 locations', 'Drone shots'] },
{ name: 'Signature', price: '₹50,000', items: ['Full-day shoot', '500 edited photos', 'USB delivery', '3 locations', 'Drone + Reel'] },
],
},
MAKEUP_ARTIST: {
roleLabel: 'Makeup Artist',
tabs: ['overview', 'about', 'services & pricing', 'gallery', 'experience & certifications', 'testimonials', 'faqs'],
specialties: ['Bridal', 'Party', 'Editorial', 'SFX', 'Airbrush', 'Natural'],
statsLabels: ['Sessions Done', 'Years Exp', 'Verified Pro', 'Last Booking', 'Will Travel'],
equipmentLabel: 'Kit & Products',
galleryTabLabel: 'gallery',
experienceTabLabel: 'experience & certifications',
serviceTabLabel: 'services & pricing',
faqItems: [
{ q: 'Do you do trials before the wedding?', a: 'Yes, a bridal trial is mandatory. It helps finalise the look and ensures comfort on the big day.' },
{ q: 'Which brands do you use?', a: 'I work with MAC, Huda Beauty, Fenty, and NARS for face. Products are selected based on skin type.' },
{ q: 'Do you travel for events?', a: 'Yes. Travel is available with prior notice. Charges apply for locations beyond 30km.' },
{ q: 'How long does bridal makeup take?', a: 'Typically 2.53 hours for full bridal. Party makeup takes around 4560 minutes.' },
],
packages: [
{ name: 'Party', price: '₹3,500', items: ['Full face makeup', 'Hair styling', '45-min session', 'Touch-up kit'] },
{ name: 'Bridal', price: '₹18,000', items: ['Trial session', 'Full bridal look', 'HD & airbrush', 'Touch-up on-site'] },
{ name: 'Bridal Party', price: '₹35,000', items: ['Bride + 4 family', 'Full day', 'Airbrush finish', 'On-site artist'] },
],
},
TUTOR: {
roleLabel: 'Tutor',
tabs: ['overview', 'about', 'subjects & pricing', 'student work', 'qualifications', 'testimonials', 'faqs'],
specialties: ['Mathematics', 'Physics', 'Chemistry', 'Biology', 'English', 'Programming'],
statsLabels: ['Students Taught', 'Years Exp', 'Verified Pro', 'Next Slot', 'Online OK'],
equipmentLabel: 'Teaching Tools',
galleryTabLabel: 'student work',
experienceTabLabel: 'qualifications',
serviceTabLabel: 'subjects & pricing',
faqItems: [
{ q: 'Do you teach online or in-person?', a: 'Both modes available. Online via Zoom or Google Meet. In-person in Chennai and surrounding areas.' },
{ q: 'What boards do you cover?', a: 'CBSE, ICSE, State Board, IB, and A-Levels. I also prepare students for JEE and NEET.' },
{ q: 'How many students per batch?', a: 'Group batches have max 5 students. Individual sessions are 1-on-1 for focused learning.' },
{ q: 'Do you provide study material?', a: 'Yes. Custom notes, practice sheets, and mock tests are included in all plans.' },
],
packages: [
{ name: 'Crash Course', price: '₹5,000/mo', items: ['10 sessions/month', '1 subject', 'Practice tests', 'Online only'] },
{ name: 'Core Plan', price: '₹8,500/mo', items: ['16 sessions/month', '2 subjects', 'Study materials', 'Doubt clearing'] },
{ name: 'Intensive', price: '₹14,000/mo', items: ['24 sessions/month', '3 subjects', 'Mock exams', 'Progress reports'] },
],
},
DEVELOPER: {
roleLabel: 'Developer',
tabs: ['overview', 'about', 'services & pricing', 'projects', 'tech stack & experience', 'testimonials', 'faqs'],
specialties: ['Web Development', 'Mobile Apps', 'API / Backend', 'UI/UX', 'DevOps', 'Consulting'],
statsLabels: ['Projects Done', 'Years Exp', 'Verified Pro', 'Last Delivery', 'Remote OK'],
equipmentLabel: 'Tech Stack',
galleryTabLabel: 'projects',
experienceTabLabel: 'tech stack & experience',
serviceTabLabel: 'services & pricing',
faqItems: [
{ q: 'Do you work on fixed price or hourly?', a: 'Both models available. Small projects are fixed-price; larger engagements use hourly or milestone billing.' },
{ q: 'Do you sign NDAs?', a: 'Yes. NDAs are standard for all client projects involving proprietary data or unreleased products.' },
{ q: 'Can you handle full-stack?', a: 'Yes. I cover React/SolidJS frontend, Rust/Node backend, PostgreSQL databases, and cloud deployment.' },
{ q: 'What is your typical delivery time?', a: 'Landing pages in 35 days. Full apps depend on scope. Detailed estimate provided after briefing.' },
],
packages: [
{ name: 'Starter', price: '₹20,000', items: ['Landing page / MVP', '5-day delivery', '2 revisions', 'Basic SEO'] },
{ name: 'Business', price: '₹65,000', items: ['Full web app', '3-week delivery', 'API integration', 'Admin panel', 'Deployment'] },
{ name: 'Enterprise', price: 'Custom', items: ['Complex systems', 'Dedicated sprint', 'Team collaboration', 'SLA included'] },
],
},
VIDEO_EDITOR: {
roleLabel: 'Video Editor',
tabs: ['overview', 'about', 'services & pricing', 'showreel', 'experience & tools', 'testimonials', 'faqs'],
specialties: ['Wedding Films', 'Corporate', 'Music Videos', 'Reels', 'Documentary', 'Product'],
statsLabels: ['Videos Done', 'Years Exp', 'Verified Pro', 'Last Delivery', 'Remote OK'],
equipmentLabel: 'Editing Suite',
galleryTabLabel: 'showreel',
experienceTabLabel: 'experience & tools',
serviceTabLabel: 'services & pricing',
faqItems: [
{ q: 'Which editing software do you use?', a: 'Adobe Premiere Pro, DaVinci Resolve, and After Effects for motion graphics and colour grading.' },
{ q: 'What is your turnaround time?', a: 'Short reels in 23 days. Full event films take 1015 working days depending on footage length.' },
{ q: 'Do you accept raw footage from other cameras?', a: 'Yes. Any format is accepted — I handle transcoding as part of the workflow.' },
{ q: 'How many revisions are included?', a: '2 revisions are included in all packages. Additional revisions are charged at ₹500 per round.' },
],
packages: [
{ name: 'Reel Edit', price: '₹3,000', items: ['Up to 60s reel', 'Music sync', '2 revisions', '48hr delivery'] },
{ name: 'Event Film', price: '₹12,000', items: ['5-min highlight', 'Colour grade', 'Titles + music', '10-day delivery'] },
{ name: 'Feature Film', price: '₹28,000', items: ['Full-length film', 'Cinematic grade', 'Motion graphics', 'Multi-format export'] },
],
},
GRAPHIC_DESIGNER: {
roleLabel: 'Graphic Designer',
tabs: ['overview', 'about', 'services & pricing', 'portfolio', 'experience & tools', 'testimonials', 'faqs'],
specialties: ['Brand Identity', 'UI/UX Design', 'Print', 'Social Media', 'Motion', 'Packaging'],
statsLabels: ['Projects Done', 'Years Exp', 'Verified Pro', 'Last Delivery', 'Remote OK'],
equipmentLabel: 'Design Tools',
galleryTabLabel: 'portfolio',
experienceTabLabel: 'experience & tools',
serviceTabLabel: 'services & pricing',
faqItems: [
{ q: 'What file formats do you deliver?', a: 'Final files in AI, PDF, PNG, SVG, and any format required. Print-ready and web-optimised variants included.' },
{ q: 'Do you create brand guidelines?', a: 'Yes. Brand identity packages include a full style guide covering colour, typography, and usage rules.' },
{ q: 'How many concepts do you provide initially?', a: '3 initial concepts are presented for branding. UI/UX starts with wireframes before visual designs.' },
{ q: 'Do you handle printing coordination?', a: 'Yes. I work with trusted print vendors and can manage end-to-end print production.' },
],
packages: [
{ name: 'Logo Pack', price: '₹8,000', items: ['3 logo concepts', '2 revisions', 'All file formats', 'Usage rights'] },
{ name: 'Brand Kit', price: '₹22,000', items: ['Logo + palette', 'Typography', 'Brand guidelines', 'Social templates'] },
{ name: 'Full Identity', price: '₹45,000', items: ['Complete brand', 'UI kit', 'Print collateral', 'Motion logo'] },
],
},
SOCIAL_MEDIA_MANAGER: {
roleLabel: 'Social Media Manager',
tabs: ['overview', 'about', 'services & pricing', 'case studies', 'experience & tools', 'testimonials', 'faqs'],
specialties: ['Instagram', 'YouTube', 'LinkedIn', 'Twitter/X', 'Facebook', 'Content Strategy'],
statsLabels: ['Brands Managed', 'Years Exp', 'Verified Pro', 'Last Campaign', 'Remote OK'],
equipmentLabel: 'Platforms & Tools',
galleryTabLabel: 'case studies',
experienceTabLabel: 'experience & tools',
serviceTabLabel: 'services & pricing',
faqItems: [
{ q: 'Do you create the content too?', a: 'Yes. Content creation — including copy, graphics, and reels — is included in all monthly retainer plans.' },
{ q: 'How do you measure results?', a: 'Monthly reports covering reach, engagement rate, follower growth, and link clicks are shared via dashboard.' },
{ q: 'Do you handle paid ads?', a: 'Yes. Meta and Google ad campaigns are available as add-ons with dedicated reporting.' },
{ q: 'What industries do you specialise in?', a: 'Fashion, F&B, real estate, personal brands, and D2C e-commerce. Industry-specific content strategy provided.' },
],
packages: [
{ name: 'Starter', price: '₹12,000/mo', items: ['2 platforms', '12 posts/month', 'Captions + hashtags', 'Monthly report'] },
{ name: 'Growth', price: '₹22,000/mo', items: ['3 platforms', '24 posts/month', 'Reels + Stories', 'Ad management'] },
{ name: 'Premium', price: '₹40,000/mo', items: ['5 platforms', 'Daily posting', 'Full content calendar', 'Influencer outreach'] },
],
},
FITNESS_TRAINER: {
roleLabel: 'Fitness Trainer',
tabs: ['overview', 'about', 'training plans', 'client results', 'certifications', 'testimonials', 'faqs'],
specialties: ['Weight Loss', 'Strength', 'Yoga', 'HIIT', 'Nutrition', 'Rehabilitation'],
statsLabels: ['Clients Trained', 'Years Exp', 'Certified Pro', 'Next Slot', 'Online OK'],
equipmentLabel: 'Certifications & Tools',
galleryTabLabel: 'client results',
experienceTabLabel: 'certifications',
serviceTabLabel: 'training plans',
faqItems: [
{ q: 'Do you offer online training?', a: 'Yes. Online sessions via Zoom with personalised programs tracked through fitness apps.' },
{ q: 'Is nutrition guidance included?', a: 'Basic nutrition guidance is included. A detailed meal plan is available as an add-on.' },
{ q: 'How long until I see results?', a: 'Visible progress typically begins in 46 weeks with consistent training and diet adherence.' },
{ q: 'Do you design custom workout plans?', a: 'Yes. Every client gets a custom plan based on fitness level, goals, and equipment access.' },
],
packages: [
{ name: 'Starter', price: '₹4,000/mo', items: ['8 sessions/month', 'Workout plan', 'WhatsApp check-ins', 'Progress tracking'] },
{ name: 'Transform', price: '₹8,000/mo', items: ['16 sessions/month', 'Custom diet plan', 'Weekly reviews', 'App access'] },
{ name: 'Elite', price: '₹15,000/mo', items: ['Daily check-in', 'Full nutrition plan', 'Body composition tracking', 'Priority access'] },
],
},
CATERING_SERVICES: {
roleLabel: 'Catering Services',
tabs: ['overview', 'about', 'packages & pricing', 'gallery', 'experience & certifications', 'testimonials', 'faqs'],
specialties: ['Weddings', 'Corporate Events', 'Private Parties', 'Buffet', 'Live Counters', 'Theme Catering'],
statsLabels: ['Events Done', 'Years Active', 'Certified', 'Last Event', 'Pan-India'],
equipmentLabel: 'Equipment & Certifications',
galleryTabLabel: 'gallery',
experienceTabLabel: 'experience & certifications',
serviceTabLabel: 'packages & pricing',
faqItems: [
{ q: 'What is your minimum guest count?', a: 'Minimum 50 guests for standard bookings. Intimate private dining available for 20+ guests.' },
{ q: 'Do you handle equipment setup?', a: 'Yes. Full setup including chafing dishes, serving counters, and staff is included in all packages.' },
{ q: 'Are custom menus available?', a: 'Absolutely. Menus are fully customisable — dietary restrictions, regional cuisines, and themed menus accommodated.' },
{ q: 'How far in advance should I book?', a: 'At least 34 weeks for events under 200 guests; 68 weeks for large-scale events.' },
],
packages: [
{ name: 'Essential', price: '₹350/plate', items: ['3-course menu', 'Serving staff', 'Basic setup', 'Buffet style'] },
{ name: 'Premium', price: '₹650/plate', items: ['5-course menu', 'Live counters', 'Themed decor', 'Dedicated manager'] },
{ name: 'Royal', price: '₹1,200/plate', items: ['Custom menu', 'Chef station', 'Premium décor', 'Full-day service'] },
],
},
PROFESSIONAL: {
roleLabel: 'Professional',
tabs: ['overview', 'about', 'services & pricing', 'portfolio', 'experience', 'testimonials', 'faqs'],
specialties: ['Primary Service', 'Secondary Service', 'Consulting', 'Training', 'Events', 'Custom'],
statsLabels: ['Projects Done', 'Years Exp', 'Verified Pro', 'Last Delivery', 'Remote OK'],
equipmentLabel: 'Tools & Equipment',
galleryTabLabel: 'portfolio',
experienceTabLabel: 'experience',
serviceTabLabel: 'services & pricing',
faqItems: [
{ q: 'How do I get started?', a: 'Send a requirement, review the professional\'s profile, and accept the request to receive their contact details.' },
{ q: 'What is your availability?', a: 'Availability is updated regularly on the profile. Contact for specific date confirmation.' },
{ q: 'Do you provide a contract?', a: 'Yes. A service agreement is shared before any project begins for mutual clarity.' },
{ q: 'What is your revision policy?', a: 'Revisions are included as per the selected package. Additional revisions are billed separately.' },
],
packages: [
{ name: 'Basic', price: 'From ₹5,000', items: ['Core deliverables', 'Standard timeline', '1 revision', 'Email support'] },
{ name: 'Standard', price: 'From ₹15,000', items: ['Full scope', 'Priority delivery', '3 revisions', 'Call support'] },
{ name: 'Premium', price: 'Custom', items: ['Custom scope', 'Dedicated support', 'Unlimited revisions', 'SLA'] },
],
},
};
function portfolioSpecForRole(roleKey: string): PortfolioSpec {
const normalized = normalizeRoleKey(roleKey);
return PORTFOLIO_SPECS[normalized] || PORTFOLIO_SPECS.PROFESSIONAL;
}
const PORTFOLIO_TESTIMONIALS = [
{ name: 'Priya S.', rating: 5, text: 'Absolutely brilliant work. The results exceeded every expectation. Highly recommended.', category: 'Verified Booking' },
{ name: 'Rohan M.', rating: 5, text: 'Professional, punctual, and highly creative. Loved the final deliverables.', category: 'Verified Booking' },
{ name: 'Ananya K.', rating: 4, text: 'Great communication throughout the project. Delivered on time with excellent quality.', category: 'Verified Booking' },
{ name: 'Kiran T.', rating: 5, text: 'An absolute pleasure to work with. Will definitely hire again for our next project.', category: 'Verified Booking' },
];
function customerViewFor(sidebar: string, roleKey: string): CustomerView { function customerViewFor(sidebar: string, roleKey: string): CustomerView {
const key = String(sidebar || '').toLowerCase().trim(); const key = String(sidebar || '').toLowerCase().trim();
if (key === 'my dashboard') return { title: 'Customer Dashboard Overview', subtitle: 'Manage your enterprise requirements and track professional responses in real-time.', tabs: ['overview', 'recent requirements', 'quick actions'], cta: 'Post New Requirement' }; if (key === 'my dashboard') return { title: 'Customer Dashboard Overview', subtitle: 'Manage your enterprise requirements and track professional responses in real-time.', tabs: ['overview', 'recent requirements', 'quick actions'], cta: 'Post New Requirement' };
@ -284,6 +533,10 @@ function customerViewFor(sidebar: string, roleKey: string): CustomerView {
const spec = profileSpecForRole(roleKey); const spec = profileSpecForRole(roleKey);
return { title: spec.title, subtitle: spec.subtitle, tabs: spec.tabs, cta: 'Save Changes' }; return { title: spec.title, subtitle: spec.subtitle, tabs: spec.tabs, cta: 'Save Changes' };
} }
if (key === 'my portfolio') {
const spec = portfolioSpecForRole(roleKey);
return { title: `${spec.roleLabel} Portfolio`, subtitle: 'Manage your public portfolio, showcase your work, and control what customers see before they accept your contact request.', tabs: [], cta: 'Edit Portfolio' };
}
if (key === 'credits') return { title: 'Credits & Billing', subtitle: 'View credit balance, top-up packages, and transaction history.', tabs: ['overview', 'buy credits', 'transactions', 'usage history'], cta: 'Buy Credits' }; if (key === 'credits') return { title: 'Credits & Billing', subtitle: 'View credit balance, top-up packages, and transaction history.', tabs: ['overview', 'buy credits', 'transactions', 'usage history'], cta: 'Buy Credits' };
if (key === 'explore nxtgauge') return { title: 'Explore Marketplace', subtitle: 'Discover top-tier professionals and specialized services.', tabs: [] }; if (key === 'explore nxtgauge') return { title: 'Explore Marketplace', subtitle: 'Discover top-tier professionals and specialized services.', tabs: [] };
if (key === 'verification') return { title: 'Verification Portal', subtitle: 'Track verification progress, documents, and updates.', tabs: ['approval status', 'documents', 'activity'] }; if (key === 'verification') return { title: 'Verification Portal', subtitle: 'Track verification progress, documents, and updates.', tabs: ['approval status', 'documents', 'activity'] };
@ -438,6 +691,11 @@ export default function DashboardDesignPreview(props: {
if (isCustomerExternalMode()) return customerView().tabs; if (isCustomerExternalMode()) return customerView().tabs;
return props.tabs.length ? props.tabs : ['overview']; return props.tabs.length ? props.tabs : ['overview'];
}); });
const resolvedTabKey = createMemo(() => {
const tabs = previewTabs();
const key = activeTabKey();
return tabs.some((item) => normalizeTabKey(item) === key) ? key : normalizeTabKey(tabs[0] || '');
});
const previewWidgets = createMemo(() => (props.widgets.length ? props.widgets : ['total_requirements', 'open', 'closed', 'responses', 'saved_pros'])); const previewWidgets = createMemo(() => (props.widgets.length ? props.widgets : ['total_requirements', 'open', 'closed', 'responses', 'saved_pros']));
const previewFields = createMemo(() => (props.fields.length ? props.fields : ['full_name', 'email', 'verification_status', 'approval_status'])); const previewFields = createMemo(() => (props.fields.length ? props.fields : ['full_name', 'email', 'verification_status', 'approval_status']));
const customerKey = createMemo(() => String(props.activeSidebar || '').toLowerCase().trim()); const customerKey = createMemo(() => String(props.activeSidebar || '').toLowerCase().trim());
@ -472,6 +730,7 @@ export default function DashboardDesignPreview(props: {
const [viewTicketFiles, setViewTicketFiles] = createSignal<string[]>([]); const [viewTicketFiles, setViewTicketFiles] = createSignal<string[]>([]);
const [profileSettingsTab, setProfileSettingsTab] = createSignal<'change_password' | 'notifications' | 'privacy'>('change_password'); const [profileSettingsTab, setProfileSettingsTab] = createSignal<'change_password' | 'notifications' | 'privacy'>('change_password');
const [showDeleteAccountModal, setShowDeleteAccountModal] = createSignal(false); const [showDeleteAccountModal, setShowDeleteAccountModal] = createSignal(false);
const [showPortfolioPreview, setShowPortfolioPreview] = createSignal(false);
const [lastSidebarKey, setLastSidebarKey] = createSignal(''); const [lastSidebarKey, setLastSidebarKey] = createSignal('');
const [profileSettingToggles, setProfileSettingToggles] = createSignal<Record<string, boolean>>({ const [profileSettingToggles, setProfileSettingToggles] = createSignal<Record<string, boolean>>({
email_updates: true, email_updates: true,
@ -498,11 +757,6 @@ export default function DashboardDesignPreview(props: {
setViewTicketFiles([]); setViewTicketFiles([]);
}; };
createEffect(() => {
const tabs = previewTabs();
const hasMatch = tabs.some((item) => normalizeTabKey(item) === activeTabKey());
if (!hasMatch && tabs[0]) props.onTabSelect(tabs[0]);
});
createEffect(() => { createEffect(() => {
const key = customerKey(); const key = customerKey();
@ -554,8 +808,199 @@ export default function DashboardDesignPreview(props: {
return { bg: '#F3F4F6', c: '#6B7280' }; return { bg: '#F3F4F6', c: '#6B7280' };
}; };
const renderPortfolioContent = () => {
const spec = portfolioSpecForRole(props.roleKey || '');
return (
<div style="display:flex;flex-direction:column;gap:10px">
{/* Profile Header */}
<div style="border-radius:12px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.06);overflow:hidden">
<div style="padding:16px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;justify-content:space-between;gap:12px">
<div style="display:flex;align-items:center;gap:12px">
<div style="width:52px;height:52px;border-radius:50%;background:#F3F4F6;border:1px solid #E5E7EB;display:flex;align-items:center;justify-content:center;flex-shrink:0">
<User size={22} style="color:#9CA3AF" />
</div>
<div>
<div style="display:flex;align-items:center;gap:8px">
<p style="margin:0;font-size:15px;font-weight:700;color:#111827">Alex Morgan</p>
<span style="font-size:11px;font-weight:600;color:#374151;background:#F3F4F6;border:1px solid #E5E7EB;border-radius:6px;padding:1px 7px">Verified</span>
</div>
<p style="margin:2px 0 0;font-size:12px;color:#6B7280">{spec.roleLabel} · Mumbai, Maharashtra</p>
</div>
</div>
<div style="display:flex;gap:8px;flex-shrink:0">
<button type="button" style="height:34px;border-radius:8px;border:1px solid #E5E7EB;background:white;color:#374151;padding:0 14px;font-size:12px;font-weight:600;cursor:pointer">Edit Portfolio</button>
<button type="button" onClick={() => setShowPortfolioPreview(true)} style="height:34px;border-radius:8px;border:none;background:#0D0D2A;color:white;padding:0 14px;font-size:12px;font-weight:600;cursor:pointer">Preview Portfolio</button>
</div>
</div>
<div style="display:flex">
{spec.statsLabels.map((label, i) => (
<div style={`flex:1;padding:12px 16px;${i < spec.statsLabels.length - 1 ? 'border-right:1px solid #F3F4F6' : ''}`}>
<p style="margin:0;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#9CA3AF">{label}</p>
<p style="margin:4px 0 0;font-size:14px;font-weight:700;color:#111827">{i===0?'248':i===1?'7+':i===2?'Verified':i===3?'2 days':'Yes'}</p>
</div>
))}
</div>
</div>
{/* About + Specialties */}
<div style="display:grid;grid-template-columns:2fr 1fr;gap:10px">
<div style="border-radius:12px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.06);overflow:hidden">
<div style="padding:12px 16px;border-bottom:1px solid #E5E7EB">
<p style="margin:0;font-size:13px;font-weight:700;color:#111827">About</p>
</div>
<div style="padding:14px 16px">
<p style="margin:0;font-size:13px;color:#374151;line-height:1.6">Professional {spec.roleLabel.toLowerCase()} with 7+ years of experience delivering high-quality work across India. Committed to excellence, creativity, and client satisfaction on every project.</p>
</div>
<div style="display:flex;border-top:1px solid #F3F4F6">
{[['7+','Years Exp'],['248','Projects'],['4.9','Rating'],['128','Reviews']].map(([val,lbl], i) => (
<div style={`flex:1;padding:12px 16px;${i<3?'border-right:1px solid #F3F4F6':''}`}>
<p style="margin:0;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#9CA3AF">{lbl}</p>
<p style="margin:4px 0 0;font-size:14px;font-weight:700;color:#111827">{val}</p>
</div>
))}
</div>
</div>
<div style="border-radius:12px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.06);overflow:hidden">
<div style="padding:12px 16px;border-bottom:1px solid #E5E7EB">
<p style="margin:0;font-size:13px;font-weight:700;color:#111827">Specialties</p>
</div>
<div style="padding:14px 16px;display:flex;flex-wrap:wrap;gap:6px">
{spec.specialties.map((s) => (
<span style="height:24px;padding:0 8px;border-radius:6px;border:1px solid #E5E7EB;background:#F9FAFB;font-size:11px;font-weight:600;color:#374151;display:inline-flex;align-items:center">{s}</span>
))}
</div>
<div style="padding:0 16px 14px;border-top:1px solid #F3F4F6;margin-top:4px">
<p style="margin:10px 0 6px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#9CA3AF">Languages</p>
<div style="display:flex;gap:5px">
{['English','Hindi','Marathi'].map((l) => (
<span style="height:22px;padding:0 8px;border-radius:6px;border:1px solid #E5E7EB;background:#F9FAFB;font-size:11px;font-weight:600;color:#374151;display:inline-flex;align-items:center">{l}</span>
))}
</div>
<p style="margin:10px 0 6px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#9CA3AF">Service Areas</p>
<div style="display:flex;flex-wrap:wrap;gap:5px">
{['Mumbai','Pune','Thane','Nashik'].map((c) => (
<span style="height:22px;padding:0 8px;border-radius:6px;border:1px solid #E5E7EB;background:#F9FAFB;font-size:11px;font-weight:600;color:#374151;display:inline-flex;align-items:center">{c}</span>
))}
</div>
</div>
</div>
</div>
{/* Packages */}
<div style="border-radius:12px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.06);overflow:hidden">
<div style="padding:12px 16px;border-bottom:1px solid #E5E7EB">
<p style="margin:0;font-size:13px;font-weight:700;color:#111827">{titleCase(spec.serviceTabLabel)}</p>
</div>
<div style="display:flex">
{spec.packages.map((pkg, i) => (
<div style={`flex:1;padding:16px;${i<2?'border-right:1px solid #F3F4F6':''}`}>
<p style="margin:0;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#9CA3AF">{pkg.name}</p>
<p style="margin:4px 0 0;font-size:16px;font-weight:700;color:#111827">{pkg.price}</p>
<div style="margin-top:8px;display:grid;gap:4px">
{pkg.items.map((item) => (
<div style="display:flex;align-items:center;gap:6px;font-size:12px;color:#374151">
<CheckCircle2 size={11} style="color:#9CA3AF;flex-shrink:0" /> {item}
</div>
))}
</div>
</div>
))}
</div>
</div>
{/* Work Gallery */}
<div style="border-radius:12px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.06);overflow:hidden">
<div style="padding:12px 16px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;justify-content:space-between">
<p style="margin:0;font-size:13px;font-weight:700;color:#111827">{titleCase(spec.galleryTabLabel)}</p>
<button type="button" style="height:30px;border-radius:8px;border:none;background:#0D0D2A;color:white;padding:0 12px;font-size:11px;font-weight:600;cursor:pointer">Upload Work</button>
</div>
<div style="padding:14px 16px;display:grid;grid-template-columns:repeat(4,1fr);gap:8px">
{[0,1,2,3,4,5,6,7].map((i) => (
<div style="height:72px;border-radius:8px;border:1px solid #E5E7EB;background:#F9FAFB;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px">
<Image size={18} style="color:#D1D5DB" />
<span style="font-size:10px;color:#9CA3AF">{spec.specialties[i % spec.specialties.length]}</span>
</div>
))}
</div>
</div>
{/* Experience */}
<div style="border-radius:12px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.06);overflow:hidden">
<div style="padding:12px 16px;border-bottom:1px solid #E5E7EB">
<p style="margin:0;font-size:13px;font-weight:700;color:#111827">{titleCase(spec.experienceTabLabel)}</p>
</div>
<div style="padding:14px 16px">
<p style="margin:0 0 8px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#9CA3AF">{spec.equipmentLabel}</p>
<div style="display:flex;flex-wrap:wrap;gap:6px">
{spec.specialties.map((item) => (
<span style="height:24px;padding:0 8px;border-radius:6px;border:1px solid #E5E7EB;background:#F9FAFB;font-size:11px;font-weight:600;color:#374151;display:inline-flex;align-items:center">{item}</span>
))}
</div>
</div>
<div style="border-top:1px solid #F3F4F6">
{[['2018','Started professional career as '+spec.roleLabel],['2020','Completed 100+ successful projects'],['2022','Won regional industry recognition'],['2024','Joined Nxtgauge marketplace']].map(([yr, desc], i, arr) => (
<div style={`display:flex;gap:16px;align-items:center;padding:10px 16px;${i<arr.length-1?'border-bottom:1px solid #F3F4F6':''}`}>
<p style="margin:0;font-size:11px;font-weight:700;color:#9CA3AF;min-width:32px">{yr}</p>
<p style="margin:0;font-size:13px;color:#374151">{desc}</p>
</div>
))}
</div>
</div>
{/* Testimonials */}
<div style="border-radius:12px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.06);overflow:hidden">
<div style="padding:12px 16px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:10px">
<p style="margin:0;font-size:13px;font-weight:700;color:#111827">Testimonials</p>
<span style="font-size:11px;font-weight:600;color:#374151;background:#F3F4F6;border:1px solid #E5E7EB;border-radius:6px;padding:1px 7px">4.9 · 128 reviews</span>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr">
{PORTFOLIO_TESTIMONIALS.map((t, i) => (
<div style={`padding:14px 16px;${i%2===0?'border-right:1px solid #F3F4F6':''};${i<2?'border-bottom:1px solid #F3F4F6':''}`}>
<div style="display:flex;justify-content:space-between;align-items:center">
<p style="margin:0;font-size:13px;font-weight:700;color:#111827">{t.name}</p>
<div style="display:flex;gap:1px">
{Array.from({length: t.rating}).map(() => <Star size={11} style="color:#F59E0B" fill="#F59E0B" />)}
</div>
</div>
<p style="margin:2px 0 0;font-size:11px;color:#9CA3AF">{t.category}</p>
<p style="margin:8px 0 0;font-size:12px;color:#374151;line-height:1.5">{t.text}</p>
</div>
))}
</div>
</div>
{/* FAQs */}
<div style="border-radius:12px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.06);overflow:hidden">
<div style="padding:12px 16px;border-bottom:1px solid #E5E7EB">
<p style="margin:0;font-size:13px;font-weight:700;color:#111827">Frequently Asked Questions</p>
</div>
<For each={spec.faqItems}>{(faq, i) => (
<div style={`border-bottom:${i() < spec.faqItems.length - 1 ? '1px solid #F3F4F6' : 'none'}`}>
<button
type="button"
onClick={() => setOpenFaqIndex(openFaqIndex()===i() ? null : i())}
style="width:100%;display:flex;justify-content:space-between;align-items:center;padding:12px 16px;background:none;border:none;cursor:pointer;text-align:left"
>
<span style="font-size:13px;font-weight:600;color:#111827">{faq.q}</span>
{openFaqIndex()===i() ? <ChevronUp size={15} style="color:#374151;flex-shrink:0" /> : <ChevronDown size={15} style="color:#9CA3AF;flex-shrink:0" />}
</button>
<Show when={openFaqIndex()===i()}>
<div style="padding:0 16px 12px">
<p style="margin:0;font-size:13px;color:#374151;line-height:1.6">{faq.a}</p>
</div>
</Show>
</div>
)}</For>
</div>
</div>
);
};
const renderCustomerContent = () => { const renderCustomerContent = () => {
const tab = props.activeTab.toLowerCase(); const tab = resolvedTabKey();
if (customerKey() === 'my dashboard') { if (customerKey() === 'my dashboard') {
if (tab === 'recent requirements') { if (tab === 'recent requirements') {
@ -1942,6 +2387,8 @@ export default function DashboardDesignPreview(props: {
); );
} }
if (customerKey() === 'my portfolio') return renderPortfolioContent();
return ( return (
<div style="border:1px solid #E5E7EB;background:white;border-radius:16px;padding:14px;box-shadow:0 1px 4px rgba(0,0,0,0.06)"> <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:13px;font-weight:700;color:#111827">Screen Preview</p> <p style="margin:0;font-size:13px;font-weight:700;color:#111827">Screen Preview</p>
@ -2009,7 +2456,7 @@ export default function DashboardDesignPreview(props: {
<button <button
type="button" type="button"
onClick={() => props.onTabSelect(item)} onClick={() => props.onTabSelect(item)}
style={`padding-bottom:10px;font-size:13px;font-weight:500;background:none;border:none;cursor:pointer;${activeTabKey() === normalizeTabKey(item) ? 'color:#FF5E13;border-bottom:2px solid #FF5E13;margin-bottom:-1px' : 'color:#6B7280'}`} style={`padding-bottom:10px;font-size:13px;font-weight:500;background:none;border:none;cursor:pointer;${resolvedTabKey() === normalizeTabKey(item) ? 'color:#FF5E13;border-bottom:2px solid #FF5E13;margin-bottom:-1px' : 'color:#6B7280'}`}
> >
{titleCase(item)} {titleCase(item)}
</button> </button>
@ -2037,6 +2484,97 @@ export default function DashboardDesignPreview(props: {
</div> </div>
</main> </main>
</div> </div>
<Show when={showPortfolioPreview()}>
<div
style="position:fixed;inset:0;background:rgba(15,23,42,0.5);display:flex;align-items:center;justify-content:center;z-index:50;padding:16px"
onClick={() => setShowPortfolioPreview(false)}
>
<div
style="width:min(540px,100%);max-height:88vh;overflow-y:auto;border:1px solid #E5E7EB;background:white;border-radius:16px;box-shadow:0 20px 50px rgba(0,0,0,0.22)"
onClick={(e) => e.stopPropagation()}
>
<div style="background:#0D0D2A;padding:12px 16px;border-radius:16px 16px 0 0;display:flex;justify-content:space-between;align-items:center">
<div>
<span style="font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:0.08em;color:#FF5E13">Customer View</span>
<p style="margin:2px 0 0;font-size:12px;color:#9CA3AF">What customers see before accepting your contact request</p>
</div>
<button type="button" onClick={() => setShowPortfolioPreview(false)} style="width:28px;height:28px;border-radius:8px;border:1px solid rgba(255,255,255,0.15);background:transparent;display:flex;align-items:center;justify-content:center;cursor:pointer;color:#E5E7EB">
<X size={14} />
</button>
</div>
<div style="padding:16px;display:flex;flex-direction:column;gap:12px">
<div style="display:flex;align-items:center;gap:12px">
<div style="width:60px;height:60px;border-radius:50%;background:#F3F4F6;border:2px solid #E5E7EB;display:flex;align-items:center;justify-content:center;flex-shrink:0">
<User size={24} style="color:#9CA3AF" />
</div>
<div>
<div style="display:flex;align-items:center;gap:7px">
<p style="margin:0;font-size:17px;font-weight:800;color:#111827">Alex Morgan</p>
<span style="display:inline-flex;align-items:center;gap:3px;height:18px;padding:0 7px;border-radius:999px;background:#ECFDF3;border:1px solid #6EE7B7;font-size:10px;font-weight:700;color:#059669">
<BadgeCheck size={9} /> Verified
</span>
</div>
<p style="margin:3px 0 0;font-size:12px;color:#FF5E13;font-weight:600">{portfolioSpecForRole(props.roleKey||'').roleLabel}</p>
<div style="display:flex;align-items:center;gap:3px;margin-top:2px">
<MapPin size={11} style="color:#9CA3AF;flex-shrink:0" />
<span style="font-size:11px;color:#6B7280">Mumbai, Maharashtra</span>
</div>
</div>
</div>
<div style="display:flex;align-items:center;gap:5px">
{[1,2,3,4,5].map(() => <Star size={13} style="color:#F59E0B" fill="#F59E0B" />)}
<span style="font-size:13px;font-weight:700;color:#111827">4.9</span>
<span style="font-size:12px;color:#6B7280">(128 reviews)</span>
</div>
<p style="margin:0;font-size:13px;color:#374151;line-height:1.5">
Passionate {portfolioSpecForRole(props.roleKey||'').roleLabel.toLowerCase()} with 7+ years of experience delivering exceptional results. Specialising in {portfolioSpecForRole(props.roleKey||'').specialties.slice(0,3).join(', ')}.
</p>
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:8px;padding:10px;border:1px solid #E5E7EB;border-radius:10px;background:#F9FAFB">
{[['248','Projects'],['7+','Yrs Exp'],['4.9','Rating'],['128','Reviews']].map(([val,lbl]) => (
<div style="text-align:center">
<p style="margin:0;font-size:15px;font-weight:800;color:#111827">{val}</p>
<p style="margin:1px 0 0;font-size:10px;color:#6B7280;text-transform:uppercase;letter-spacing:0.04em">{lbl}</p>
</div>
))}
</div>
<div>
<p style="margin:0 0 6px;font-size:11px;font-weight:700;color:#6B7280;text-transform:uppercase;letter-spacing:0.04em">Specialties</p>
<div style="display:flex;flex-wrap:wrap;gap:5px">
{portfolioSpecForRole(props.roleKey||'').specialties.map((s) => (
<span style="height:24px;padding:0 8px;border-radius:999px;border:1px solid #FFE0CC;background:#FFF4EE;color:#FF5E13;font-size:11px;font-weight:600;display:inline-flex;align-items:center">{s}</span>
))}
</div>
</div>
<div>
<p style="margin:0 0 6px;font-size:11px;font-weight:700;color:#6B7280;text-transform:uppercase;letter-spacing:0.04em">Featured Package</p>
{(() => {
const pkg = portfolioSpecForRole(props.roleKey||'').packages[1];
return (
<div style="border:2px solid #FF5E13;border-radius:10px;padding:12px;background:#FFF8F4">
<div style="display:flex;justify-content:space-between;align-items:center">
<p style="margin:0;font-size:13px;font-weight:800;color:#111827">{pkg.name}</p>
<span style="font-size:16px;font-weight:800;color:#FF5E13">{pkg.price}</span>
</div>
<div style="display:flex;flex-wrap:wrap;gap:6px;margin-top:8px">
{pkg.items.map((item) => (
<span style="font-size:11px;color:#374151;display:inline-flex;align-items:center;gap:4px">
<CheckCircle2 size={11} style="color:#10B981;flex-shrink:0" /> {item}
</span>
))}
</div>
</div>
);
})()}
</div>
<div style="display:flex;gap:8px;padding-top:4px;border-top:1px solid #E5E7EB">
<button type="button" style="flex:1;height:36px;border-radius:8px;border:none;background:#FF5E13;color:white;font-size:13px;font-weight:700;cursor:pointer">Accept Request</button>
<button type="button" onClick={() => setShowPortfolioPreview(false)} style="flex:1;height:36px;border-radius:8px;border:1px solid #D1D5DB;background:white;color:#374151;font-size:13px;font-weight:700;cursor:pointer">Close Preview</button>
</div>
</div>
</div>
</div>
</Show>
</div> </div>
); );
} }

View file

@ -27,6 +27,7 @@ const ROLE_BASED_SIDEBAR: Record<'PROFESSIONAL' | 'COMPANY' | 'JOB_SEEKER' | 'CU
PROFESSIONAL: [ PROFESSIONAL: [
'My Dashboard', 'My Dashboard',
'My Profile', 'My Profile',
'My Portfolio',
'Leads', 'Leads',
'My Responses', 'My Responses',
'Credits', 'Credits',
@ -182,6 +183,7 @@ export default function ExternalDashboardManagementPage() {
const [name, setName] = createSignal(''); const [name, setName] = createSignal('');
const [code, setCode] = createSignal(''); const [code, setCode] = createSignal('');
const [roleId, setRoleId] = createSignal(''); const [roleId, setRoleId] = createSignal('');
const [formRoleKey, setFormRoleKey] = createSignal('');
const [widgets, setWidgets] = createSignal<string[]>([]); const [widgets, setWidgets] = createSignal<string[]>([]);
const [tabs, setTabs] = createSignal<string[]>([]); const [tabs, setTabs] = createSignal<string[]>([]);
const [sidebarItems, setSidebarItems] = createSignal<string[]>([]); const [sidebarItems, setSidebarItems] = createSignal<string[]>([]);
@ -204,6 +206,15 @@ export default function ExternalDashboardManagementPage() {
return map; return map;
}); });
const personaFromKey = (key: string): 'PROFESSIONAL' | 'COMPANY' | 'JOB_SEEKER' | 'CUSTOMER' | null => {
const k = String(key || '').toUpperCase();
if (!k) return null;
if (k.includes('COMPANY')) return 'COMPANY';
if (k.includes('CUSTOMER')) return 'CUSTOMER';
if (k.includes('JOB_SEEKER') || k.includes('JOBSEEKER')) return 'JOB_SEEKER';
return 'PROFESSIONAL'; // photographer, makeup, tutor, developer, video, graphic, social, fitness, catering, etc.
};
const sidebarLooksCustomer = createMemo(() => { const sidebarLooksCustomer = createMemo(() => {
const joined = sidebarItems().join(' ').toLowerCase(); const joined = sidebarItems().join(' ').toLowerCase();
return joined.includes('my requirements') return joined.includes('my requirements')
@ -211,19 +222,24 @@ export default function ExternalDashboardManagementPage() {
|| joined.includes('shortlisted responses'); || joined.includes('shortlisted responses');
}); });
const selectedRoleKey = createMemo(() => {
const selected = roles().find((r) => r.id === roleId());
return selected?.key || '';
});
const previewSidebarItems = createMemo(() => { const previewSidebarItems = createMemo(() => {
const selectedRoleId = roleId(); const selectedRoleId = roleId();
const persona = rolePersonaById()[selectedRoleId]; // Primary: look up persona by role ID (from loaded roles list)
const personaById = rolePersonaById()[selectedRoleId];
// Fallback: derive persona directly from the stored role key (works when roles API is empty or roleId unmatched)
const personaByKey = personaFromKey(selectedRoleKey() || formRoleKey());
const persona = personaById ?? personaByKey;
if (persona && ROLE_BASED_SIDEBAR[persona]?.length) return ROLE_BASED_SIDEBAR[persona]; if (persona && ROLE_BASED_SIDEBAR[persona]?.length) return ROLE_BASED_SIDEBAR[persona];
if (sidebarLooksCustomer()) return ROLE_BASED_SIDEBAR.CUSTOMER; if (sidebarLooksCustomer()) return ROLE_BASED_SIDEBAR.CUSTOMER;
if (sidebarItems().length) return sidebarItems(); if (sidebarItems().length) return sidebarItems();
return ['My Dashboard', 'My Profile', 'Switch Services', 'Logout']; return ['My Dashboard', 'My Profile', 'Switch Services', 'Logout'];
}); });
const previewTabs = createMemo(() => (tabs().length ? tabs() : ['overview'])); const previewTabs = createMemo(() => (tabs().length ? tabs() : ['overview']));
const selectedRoleKey = createMemo(() => {
const selected = roles().find((r) => r.id === roleId());
return selected?.key || '';
});
const authHeaders = () => { const authHeaders = () => {
const token = typeof sessionStorage !== 'undefined' ? sessionStorage.getItem('nxtgauge_admin_access_token') || '' : ''; const token = typeof sessionStorage !== 'undefined' ? sessionStorage.getItem('nxtgauge_admin_access_token') || '' : '';
@ -326,7 +342,9 @@ export default function ExternalDashboardManagementPage() {
setFormTab('preview'); setFormTab('preview');
setName(''); setName('');
setCode(''); setCode('');
setRoleId(roles()[0]?.id || ''); const firstRole = roles()[0];
setRoleId(firstRole?.id || '');
setFormRoleKey(firstRole?.key || '');
setWidgets([]); setWidgets([]);
setTabs([]); setTabs([]);
setSidebarItems([]); setSidebarItems([]);
@ -345,6 +363,7 @@ export default function ExternalDashboardManagementPage() {
setName(row.name); setName(row.name);
setCode(row.code); setCode(row.code);
setRoleId(row.roleId); setRoleId(row.roleId);
setFormRoleKey(row.roleKey);
setWidgets(row.widgets); setWidgets(row.widgets);
setTabs(row.tabs); setTabs(row.tabs);
setSidebarItems(row.sidebarItems); setSidebarItems(row.sidebarItems);
@ -360,6 +379,9 @@ export default function ExternalDashboardManagementPage() {
createEffect(() => { createEffect(() => {
const selected = roleId(); const selected = roleId();
if (!selected || view() !== 'form' || editingId()) return; if (!selected || view() !== 'form' || editingId()) return;
// Sync formRoleKey whenever roleId changes (so persona fallback stays fresh)
const matchedRole = roles().find((r) => r.id === selected);
if (matchedRole) setFormRoleKey(matchedRole.key);
applySidebarPresetForRole(selected, false); applySidebarPresetForRole(selected, false);
applyPreviewPathForRole(selected, false); applyPreviewPathForRole(selected, false);
}); });
@ -370,7 +392,8 @@ export default function ExternalDashboardManagementPage() {
}); });
createEffect(() => { createEffect(() => {
const list = previewTabs(); const list = tabs(); // use configured tabs only — not the fallback ['overview']
if (!list.length) return; // sidebar-driven tabs (profile, portfolio, etc.) manage validation internally
const active = normalizeToken(activePreviewTab()); const active = normalizeToken(activePreviewTab());
if (!list.some((item) => normalizeToken(item) === active)) setActivePreviewTab(list[0] || ''); if (!list.some((item) => normalizeToken(item) === active)) setActivePreviewTab(list[0] || '');
}); });