- Add AuthProvider context, RequireAuth guard - Create API client with endpoint helpers - Add forgot-password route wired to backend - Remove dummy login button, add forgot-password link - Dashboard: RequireAuth, auth context integration - DashboardDesignPreview: replace Beeceptor with internal payments API - DashboardDesignPreview: add coupon state, validation, apply - DashboardDesignPreview: credit wallet on verify, ledger entry - DashboardDesignPreview: adjust total with discount - misc: getToken import, API constant - fix: token storage (use getToken)
6776 lines
458 KiB
TypeScript
6776 lines
458 KiB
TypeScript
import { For, Show, createEffect, createMemo, createResource, createSignal, onMount } from 'solid-js';
|
||
import {
|
||
Plus, Search, ArrowRight, Clock, User, BookOpen, AlertCircle, Save, X,
|
||
LayoutGrid, List, CheckCircle, Edit2, Trash2, Globe, Lock,
|
||
Rocket, TrendingUp, ShieldCheck, Mail, Phone, MapPin,
|
||
ChevronRight, ChevronLeft, ChevronDown, ChevronUp,
|
||
Download, Filter, MoreVertical, Star, HelpCircle,
|
||
Award, Bell, BadgeCheck, Bookmark, BriefcaseBusiness, Calendar,
|
||
Camera, CheckCircle2, Clapperboard, Compass, Coins, Code2,
|
||
Dumbbell, Eye, FileText, GraduationCap, HandHelping,
|
||
HeadphonesIcon, Image, LogOut, Megaphone, PenTool,
|
||
RefreshCw, Scissors, Settings, Settings2, UtensilsCrossed,
|
||
UserCircle2, Users,
|
||
} from 'lucide-solid';
|
||
import { RuntimeKBConfig, RuntimeKBArticle } from '../../lib/runtime/types';
|
||
import { getToken } from '~/lib/auth';
|
||
|
||
function titleCase(value: string) {
|
||
return String(value || '')
|
||
.replace(/_/g, ' ')
|
||
.replace(/\b\w/g, (c) => c.toUpperCase());
|
||
}
|
||
|
||
const ORANGE_ICON_FILTER = 'invert(51%) sepia(86%) saturate(2445%) hue-rotate(353deg) brightness(101%) contrast(103%)';
|
||
const BLUE_ICON_FILTER = 'invert(11%) sepia(85%) saturate(2462%) hue-rotate(233deg) brightness(91%) contrast(101%)';
|
||
const API = '/api/gateway';
|
||
|
||
function StatusBadge(props: { status: 'ACTIVE' | 'INACTIVE' }) {
|
||
const active = () => props.status === 'ACTIVE';
|
||
return (
|
||
<span style={`display:inline-flex;align-items:center;border-radius:9999px;border:1px solid ${active() ? '#FFD8C2' : '#D1D5DB'};background:${active() ? '#FFF1EB' : '#F3F4F6'};color:${active() ? '#FF5E13' : '#4B5563'};padding:2px 10px;font-size:12px;font-weight:500`}>
|
||
{active() ? 'Active' : 'Inactive'}
|
||
</span>
|
||
);
|
||
}
|
||
|
||
function sidebarIcon(label: string) {
|
||
const key = String(label || '').toLowerCase().trim();
|
||
if (key.includes('dashboard')) return LayoutGrid;
|
||
if (key.includes('portfolio')) return Image;
|
||
if (key.includes('profile')) return UserCircle2;
|
||
if (key.includes('lead')) return HandHelping;
|
||
if (key.includes('response')) return FileText;
|
||
if (key.includes('credit')) return Coins;
|
||
if (key.includes('explore')) return Compass;
|
||
if (key.includes('verification')) return BadgeCheck;
|
||
if (key.includes('support') || key.includes('help center')) return HeadphonesIcon;
|
||
if (key === 'settings') return Settings2;
|
||
if (key.includes('switch role') || key.includes('switch services') || key.includes('switch service')) return RefreshCw;
|
||
if (key.includes('logout')) return LogOut;
|
||
if (key.includes('job')) return BriefcaseBusiness;
|
||
if (key.includes('application')) return FileText;
|
||
if (key.includes('shortlisted')) return Users;
|
||
if (key.includes('saved')) return Bookmark;
|
||
if (key.includes('requirement')) return FileText;
|
||
return LayoutGrid;
|
||
}
|
||
|
||
type CustomerView = {
|
||
title: string;
|
||
subtitle: string;
|
||
tabs: string[];
|
||
cta?: string;
|
||
};
|
||
|
||
type ProfileSpec = {
|
||
title: string;
|
||
subtitle: string;
|
||
tabs: string[];
|
||
tabFields: Record<string, string[]>;
|
||
};
|
||
|
||
function normalizeRoleKey(roleKey: string): string {
|
||
const key = String(roleKey || '').toUpperCase();
|
||
if (key.includes('COMPANY')) return 'COMPANY';
|
||
if (key.includes('CUSTOMER')) return 'CUSTOMER';
|
||
if (key.includes('SERVICE_SEEKER')) return 'CUSTOMER';
|
||
if (key.includes('JOB_SEEKER') || key.includes('JOBSEEKER')) return 'JOB_SEEKER';
|
||
if (key.includes('PHOTOGRAPHER')) return 'PHOTOGRAPHER';
|
||
if (key.includes('MAKEUP')) return 'MAKEUP_ARTIST';
|
||
if (key.includes('DEVELOPER')) return 'DEVELOPER';
|
||
if (key.includes('VIDEO')) return 'VIDEO_EDITOR';
|
||
if (key.includes('UGC') || (key.includes('CONTENT') && key.includes('CREATOR'))) return 'UGC_CONTENT_CREATOR';
|
||
if (key.includes('GRAPHIC')) return 'GRAPHIC_DESIGNER';
|
||
if (key.includes('SOCIAL')) return 'SOCIAL_MEDIA_MANAGER';
|
||
if (key.includes('FITNESS')) return 'FITNESS_TRAINER';
|
||
if (key.includes('TUTOR')) return 'TUTOR';
|
||
if (key.includes('CATER')) return 'CATERING_SERVICES';
|
||
return 'PROFESSIONAL';
|
||
}
|
||
|
||
const PROFILE_SPECS: Record<string, ProfileSpec> = {
|
||
CUSTOMER: {
|
||
title: 'Service Seeker Profile',
|
||
subtitle: 'Manage your personal details, service preferences, documents, and account settings.',
|
||
tabs: ['basic information', 'documents', 'settings'],
|
||
tabFields: {
|
||
'basic information': ['First Name', 'Last Name', 'Email Address', 'Mobile Number', 'Area', 'Place', 'PIN Code', 'Service Category'],
|
||
documents: ['Identity Proof', 'Address Proof'],
|
||
settings: ['Password & Login', 'Notification Preferences', 'Privacy Controls'],
|
||
},
|
||
},
|
||
COMPANY: {
|
||
title: 'Company Profile',
|
||
subtitle: 'Configure organization details, hiring preferences, compliance documents, and settings.',
|
||
tabs: ['basic information', 'documents', 'settings'],
|
||
tabFields: {
|
||
'basic information': ['Company Name', 'Company Email', 'Company Phone', 'City', 'Area', 'Place', 'PIN Code', 'Website URL'],
|
||
documents: ['GST Certificate', 'PAN Card', 'Incorporation Certificate'],
|
||
settings: ['Account Security', 'Team Access', 'Notification Preferences'],
|
||
},
|
||
},
|
||
JOB_SEEKER: {
|
||
title: 'Job Seeker Profile',
|
||
subtitle: 'Maintain your career profile, resume, preferences, and verification docs.',
|
||
tabs: ['basic information', 'documents', 'settings'],
|
||
tabFields: {
|
||
'basic information': ['First Name', 'Last Name', 'Email Address', 'Mobile Number', 'Current Role', 'Total Experience', 'City', 'Area', 'Place'],
|
||
documents: ['Identity Proof', 'Address Proof', 'Education Proof'],
|
||
settings: ['Password & Login', 'Notification Preferences', 'Privacy Controls'],
|
||
},
|
||
},
|
||
PHOTOGRAPHER: {
|
||
title: 'Photographer Profile',
|
||
subtitle: 'Manage your photography details, pricing, portfolio, and documents.',
|
||
tabs: ['basic information', 'documents', 'settings'],
|
||
tabFields: {
|
||
'basic information': ['First Name', 'Last Name', 'Email Address', 'Mobile Number', 'Gender', 'Address Line 1', 'Address Line 2 (Optional)', 'City', 'Area', 'State', 'PIN Code'],
|
||
documents: ['Identity Proof', 'Address Proof', 'Portfolio Ownership Proof'],
|
||
settings: ['Password & Login', 'Notification Preferences', 'Privacy Controls'],
|
||
},
|
||
},
|
||
MAKEUP_ARTIST: {
|
||
title: 'Makeup Artist Profile',
|
||
subtitle: 'Manage makeup specialization, services, portfolio, and compliance documents.',
|
||
tabs: ['basic information', 'documents', 'settings'],
|
||
tabFields: {
|
||
'basic information': ['First Name', 'Last Name', 'Email Address', 'Mobile Number', 'Gender', 'Address Line 1', 'Address Line 2 (Optional)', 'City', 'Area', 'State', 'PIN Code'],
|
||
documents: ['Identity Proof', 'Address Proof', 'Professional Certifications'],
|
||
settings: ['Password & Login', 'Notification Preferences', 'Privacy Controls'],
|
||
},
|
||
},
|
||
DEVELOPER: {
|
||
title: 'Developer Profile',
|
||
subtitle: 'Showcase technical profile, pricing models, portfolio projects, and documents.',
|
||
tabs: ['basic information', 'documents', 'settings'],
|
||
tabFields: {
|
||
'basic information': ['First Name', 'Last Name', 'Email Address', 'Mobile Number', 'Gender', 'Address Line 1', 'Address Line 2 (Optional)', 'City', 'Area', 'State', 'PIN Code'],
|
||
documents: ['Identity Proof', 'Address Proof', 'Tax Document'],
|
||
settings: ['Password & Login', 'Notification Preferences', 'Privacy Controls'],
|
||
},
|
||
},
|
||
VIDEO_EDITOR: {
|
||
title: 'Video Editor Profile',
|
||
subtitle: 'Manage editing profile, services, portfolio, and verification documents.',
|
||
tabs: ['basic information', 'documents', 'settings'],
|
||
tabFields: {
|
||
'basic information': ['First Name', 'Last Name', 'Email Address', 'Mobile Number', 'Gender', 'Address Line 1', 'Address Line 2 (Optional)', 'City', 'Area', 'State', 'PIN Code'],
|
||
documents: ['Identity Proof', 'Address Proof', 'Tax Document'],
|
||
settings: ['Password & Login', 'Notification Preferences', 'Privacy Controls'],
|
||
},
|
||
},
|
||
UGC_CONTENT_CREATOR: {
|
||
title: 'UGC Content Creator Profile',
|
||
subtitle: 'Manage your creator profile, content style, pricing, and portfolio deliverables.',
|
||
tabs: ['basic information', 'documents', 'settings'],
|
||
tabFields: {
|
||
'basic information': ['First Name', 'Last Name', 'Email Address', 'Mobile Number', 'Gender', 'Address Line 1', 'Address Line 2 (Optional)', 'City', 'Area', 'State', 'PIN Code'],
|
||
documents: ['Identity Proof', 'Address Proof', 'Tax Document'],
|
||
settings: ['Password & Login', 'Notification Preferences', 'Privacy Controls'],
|
||
},
|
||
},
|
||
GRAPHIC_DESIGNER: {
|
||
title: 'Graphic Designer Profile',
|
||
subtitle: 'Manage design profile, service pricing, portfolio assets, and documents.',
|
||
tabs: ['basic information', 'documents', 'settings'],
|
||
tabFields: {
|
||
'basic information': ['First Name', 'Last Name', 'Email Address', 'Mobile Number', 'Gender', 'Address Line 1', 'Address Line 2 (Optional)', 'City', 'Area', 'State', 'PIN Code'],
|
||
documents: ['Identity Proof', 'Address Proof', 'Tax Document'],
|
||
settings: ['Password & Login', 'Notification Preferences', 'Privacy Controls'],
|
||
},
|
||
},
|
||
SOCIAL_MEDIA_MANAGER: {
|
||
title: 'Social Media Manager Profile',
|
||
subtitle: 'Manage social profile details, service plans, case studies, and documents.',
|
||
tabs: ['basic information', 'documents', 'settings'],
|
||
tabFields: {
|
||
'basic information': ['First Name', 'Last Name', 'Email Address', 'Mobile Number', 'Gender', 'Address Line 1', 'Address Line 2 (Optional)', 'City', 'Area', 'State', 'PIN Code'],
|
||
documents: ['Identity Proof', 'Address Proof', 'Tax Document'],
|
||
settings: ['Password & Login', 'Notification Preferences', 'Privacy Controls'],
|
||
},
|
||
},
|
||
FITNESS_TRAINER: {
|
||
title: 'Fitness Trainer Profile',
|
||
subtitle: 'Manage training details, plans, certifications, and profile settings.',
|
||
tabs: ['basic information', 'documents', 'settings'],
|
||
tabFields: {
|
||
'basic information': ['First Name', 'Last Name', 'Email Address', 'Mobile Number', 'Gender', 'Address Line 1', 'Address Line 2 (Optional)', 'City', 'Area', 'State', 'PIN Code'],
|
||
documents: ['Identity Proof', 'Address Proof', 'Certification Proof'],
|
||
settings: ['Password & Login', 'Notification Preferences', 'Privacy Controls'],
|
||
},
|
||
},
|
||
TUTOR: {
|
||
title: 'Tutor Profile',
|
||
subtitle: 'Manage teaching details, subjects, pricing, documents, and settings.',
|
||
tabs: ['basic information', 'documents', 'settings'],
|
||
tabFields: {
|
||
'basic information': ['First Name', 'Last Name', 'Email Address', 'Mobile Number', 'Gender', 'Address Line 1', 'Address Line 2 (Optional)', 'City', 'Area', 'State', 'PIN Code'],
|
||
documents: ['Identity Proof', 'Address Proof', 'Educational Proof'],
|
||
settings: ['Password & Login', 'Notification Preferences', 'Privacy Controls'],
|
||
},
|
||
},
|
||
CATERING_SERVICES: {
|
||
title: 'Catering Services Profile',
|
||
subtitle: 'Manage business details, menu packages, gallery, and compliance docs.',
|
||
tabs: ['basic information', 'documents', 'settings'],
|
||
tabFields: {
|
||
'basic information': ['Business Name', 'Contact Person Name', 'Email Address', 'Mobile Number', 'Gender', 'Address Line 1', 'Address Line 2 (Optional)', 'City', 'Area', 'State', 'PIN Code'],
|
||
documents: ['Identity Proof', 'Address Proof', 'Food License'],
|
||
settings: ['Password & Login', 'Notification Preferences', 'Privacy Controls'],
|
||
},
|
||
},
|
||
PROFESSIONAL: {
|
||
title: 'Professional Profile',
|
||
subtitle: 'Manage professional details, pricing, portfolio, and account settings.',
|
||
tabs: ['basic information', 'documents', 'settings'],
|
||
tabFields: {
|
||
'basic information': ['Full Name', 'Email Address', 'Mobile Number', 'Gender', 'Address Line 1', 'Address Line 2 (Optional)', 'City', 'Area', 'State', 'PIN Code'],
|
||
documents: ['Identity Proof', 'Address Proof', 'Tax Document'],
|
||
settings: ['Password & Login', 'Notification Preferences', 'Privacy Controls'],
|
||
},
|
||
},
|
||
};
|
||
|
||
function profileSpecForRole(roleKey: string): ProfileSpec {
|
||
const normalized = normalizeRoleKey(roleKey);
|
||
const base = PROFILE_SPECS[normalized] || PROFILE_SPECS.PROFESSIONAL;
|
||
const existingDocuments = Array.isArray(base.tabFields?.documents) ? base.tabFields.documents : [];
|
||
const documents = Array.from(new Set(['Address', 'Address Proof', ...existingDocuments]));
|
||
return {
|
||
...base,
|
||
tabFields: {
|
||
...base.tabFields,
|
||
documents,
|
||
},
|
||
};
|
||
}
|
||
|
||
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: ['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 10–14 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: ['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.5–3 hours for full bridal. Party makeup takes around 45–60 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: ['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: ['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 3–5 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: ['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 2–3 days. Full event films take 10–15 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'] },
|
||
],
|
||
},
|
||
UGC_CONTENT_CREATOR: {
|
||
roleLabel: 'UGC Content Creator',
|
||
tabs: ['about', 'services & pricing', 'content portfolio', 'experience & tools', 'testimonials', 'faqs'],
|
||
specialties: ['UGC Ads', 'Product Demos', 'Lifestyle UGC', 'Voiceover', 'Scripted Reels', 'Performance Creatives'],
|
||
statsLabels: ['Campaigns Done', 'Years Exp', 'Verified Pro', 'Last Delivery', 'Remote OK'],
|
||
equipmentLabel: 'Creator Setup',
|
||
galleryTabLabel: 'content portfolio',
|
||
experienceTabLabel: 'experience & tools',
|
||
serviceTabLabel: 'services & pricing',
|
||
faqItems: [
|
||
{ q: 'Do you create scripts for UGC ads?', a: 'Yes. Script ideation and hooks are included in campaign packages based on your brand goals.' },
|
||
{ q: 'Can you deliver platform-specific formats?', a: 'Yes. I deliver optimized cuts for Instagram, YouTube Shorts, and TikTok formats.' },
|
||
{ q: 'Are raw files included?', a: 'Raw files are optional and available as an add-on depending on package scope.' },
|
||
{ q: 'What is your turnaround time?', a: 'Most UGC deliverables are completed in 3–5 business days after brief and product receipt.' },
|
||
],
|
||
packages: [
|
||
{ name: 'Starter UGC', price: '₹7,500', items: ['1 video creative', 'Script + hook', 'Vertical format', '2 revisions'] },
|
||
{ name: 'Growth UGC', price: '₹18,000', items: ['3 video creatives', 'Platform variations', 'Usage rights', '4 revisions'] },
|
||
{ name: 'Scale UGC', price: '₹35,000', items: ['6 video creatives', 'A/B hook variants', 'Ad-ready exports', 'Priority delivery'] },
|
||
],
|
||
},
|
||
GRAPHIC_DESIGNER: {
|
||
roleLabel: 'Graphic Designer',
|
||
tabs: ['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: ['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: ['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 4–6 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: ['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 3–4 weeks for events under 200 guests; 6–8 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: ['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;
|
||
}
|
||
|
||
function portfolioMediaConfig(roleKey: string): {
|
||
mode: 'visual' | 'non_visual';
|
||
ctaLabel: string;
|
||
items: string[];
|
||
} {
|
||
const normalized = normalizeRoleKey(roleKey);
|
||
if (
|
||
normalized === 'PHOTOGRAPHER'
|
||
|| normalized === 'MAKEUP_ARTIST'
|
||
|| normalized === 'VIDEO_EDITOR'
|
||
|| normalized === 'UGC_CONTENT_CREATOR'
|
||
|| normalized === 'GRAPHIC_DESIGNER'
|
||
|| normalized === 'SOCIAL_MEDIA_MANAGER'
|
||
|| normalized === 'CATERING_SERVICES'
|
||
|| normalized === 'DEVELOPER'
|
||
|| normalized === 'FITNESS_TRAINER'
|
||
) {
|
||
return {
|
||
mode: 'visual',
|
||
ctaLabel: 'Upload Images',
|
||
items: ['Campaign Shot', 'Result Highlight', 'Before/After', 'Client Output', 'Style Board', 'Final Delivery', 'Portfolio Select', 'Recent Work'],
|
||
};
|
||
}
|
||
return {
|
||
mode: 'non_visual',
|
||
ctaLabel: 'Add Work Entry',
|
||
items: ['Case Study Summary', 'Outcome & Metrics', 'Client Feedback Snippet', 'Delivery Timeline', 'Method & Process', 'Proof of Work'],
|
||
};
|
||
}
|
||
|
||
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 {
|
||
const key = String(sidebar || '').toLowerCase().trim();
|
||
const role = normalizeRoleKey(roleKey);
|
||
const isProfessionalRole = role !== 'COMPANY' && role !== 'JOB_SEEKER' && role !== 'CUSTOMER';
|
||
if (key === 'my dashboard') return { title: 'Service Seeker Dashboard Overview', subtitle: 'Manage your requirements and track professional responses in real-time.', tabs: ['overview', 'recent requirements', 'quick actions'], cta: 'Post New Requirement' };
|
||
if (key === 'leads') return { title: 'Leads', subtitle: 'Browse marketplace requirements and request contact access for current opportunities.', tabs: [], cta: 'Buy Credits' };
|
||
if (key === 'jobs') {
|
||
if (role === 'JOB_SEEKER') return { title: 'Jobs', subtitle: 'Scroll through approved jobs, filter opportunities, and apply with your job seeker profile.', tabs: ['all jobs', 'recommended', 'saved', 'applied', 'expiring soon'], cta: 'Post A Resume' };
|
||
return { title: 'Post Job', subtitle: 'Create a job, submit it for approval, and publish it to attract the right candidates.', tabs: [], cta: 'Post New Job' };
|
||
}
|
||
if (key === 'my requirements') return { title: 'My Requirements Management', subtitle: 'Manage and track active service requests, drafts, and closed items.', tabs: ['all requirements', 'open', 'closed', 'drafts'], cta: 'Post New Requirement' };
|
||
if (key === 'received responses' || key === 'my responses') {
|
||
if (role !== 'CUSTOMER') return { title: 'My Responses', subtitle: 'Track requested and pending leads with current request status.', tabs: ['requested leads', 'pending leads'] };
|
||
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' };
|
||
}
|
||
if (key === 'my portfolio') {
|
||
if (!isProfessionalRole) {
|
||
return { title: 'Portfolio Unavailable', subtitle: 'My Portfolio is available only for professional roles.', tabs: [] };
|
||
}
|
||
const spec = portfolioSpecForRole(roleKey);
|
||
return { title: `${spec.roleLabel} Portfolio`, subtitle: 'Manage your public portfolio, showcase your work, and control what service seekers see before they accept your contact request.', tabs: spec.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 === '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 === 'help center' || key === 'support') return { title: 'Help Center', subtitle: 'Get help, manage tickets, and contact support.', tabs: [] };
|
||
if (key === 'settings') return { title: 'Account & Privacy Settings', subtitle: 'Configure account security and privacy preferences.', tabs: ['account', 'privacy', 'notifications'] };
|
||
if (key === 'switch role' || key === 'switch services' || key === 'switch service') return { title: 'Service Switcher Portal', subtitle: 'Switch to approved services without logging out.', tabs: ['available services', 'pending approvals', 'onboarding'] };
|
||
if (key === 'logout') return { title: 'Logout Confirmation', subtitle: 'Confirm before ending your current session.', tabs: ['confirm logout', 'cancel'] };
|
||
return { title: 'Service Seeker Dashboard', subtitle: 'Preview service seeker dashboard flow.', tabs: ['overview'] };
|
||
}
|
||
|
||
const REQUIREMENT_ROWS = [
|
||
{
|
||
id: '#REQ-9012',
|
||
title: 'Wedding Shoot in Goa',
|
||
summary: 'Traditional & drone coverage',
|
||
category: 'PHOTOGRAPHER',
|
||
amount: '₹50,000',
|
||
budget: '₹50,000 - ₹80,000',
|
||
location: 'Goa (On-site)',
|
||
submission: 'Oct 24, 2023',
|
||
status: 'approved',
|
||
responses: 5,
|
||
responseTag: 'Active (5 Responses)',
|
||
},
|
||
{
|
||
id: '#REQ-8945',
|
||
title: 'E-commerce Portal Redesign',
|
||
summary: 'Next.js + Tailwind CSS specialist',
|
||
category: 'DEVELOPER',
|
||
amount: '₹1,20,000',
|
||
budget: '₹1,20,000 - ₹2,00,000',
|
||
location: 'Remote',
|
||
submission: 'Nov 02, 2023',
|
||
status: 'under review',
|
||
responses: 0,
|
||
responseTag: 'In Review',
|
||
},
|
||
{
|
||
id: '#REQ-8832',
|
||
title: 'Corporate Brand Identity',
|
||
summary: 'Logo, stationery & guidelines',
|
||
category: 'UI DESIGNER',
|
||
amount: '₹35,000',
|
||
budget: '₹35,000 Flat',
|
||
location: 'Mumbai (Hybrid)',
|
||
submission: 'Oct 15, 2023',
|
||
status: 'rejected',
|
||
responses: 0,
|
||
responseTag: 'Closed',
|
||
},
|
||
{
|
||
id: '#REQ-9108',
|
||
title: 'Home Fitness Program',
|
||
summary: '12-week transformation plan',
|
||
category: 'FITNESS TRAINER',
|
||
amount: '₹18,000',
|
||
budget: '₹18,000 - ₹28,000',
|
||
location: 'Chennai (Online + Offline)',
|
||
submission: 'Nov 08, 2023',
|
||
status: 'active',
|
||
responses: 3,
|
||
responseTag: 'New (3 Responses)',
|
||
},
|
||
{
|
||
id: '#REQ-9121',
|
||
title: 'Product Launch Promo Video',
|
||
summary: 'Scripted edit + motion graphics',
|
||
category: 'VIDEO EDITOR',
|
||
amount: '₹40,000',
|
||
budget: '₹40,000 - ₹70,000',
|
||
location: 'Remote',
|
||
submission: 'Nov 10, 2023',
|
||
status: 'draft',
|
||
responses: 0,
|
||
responseTag: 'Draft',
|
||
},
|
||
{
|
||
id: '#REQ-8660',
|
||
title: 'Weekly Maths Coaching',
|
||
summary: 'Class 10 board-focused tutoring',
|
||
category: 'TUTOR',
|
||
amount: '₹8,000',
|
||
budget: '₹8,000 - ₹14,000',
|
||
location: 'Pune (Online)',
|
||
submission: 'Sep 28, 2023',
|
||
status: 'closed',
|
||
responses: 12,
|
||
responseTag: 'Completed',
|
||
},
|
||
];
|
||
|
||
const REQUIREMENT_ROLE_OPTIONS = [
|
||
{ key: 'PHOTOGRAPHER', title: 'Photographer', desc: 'Capture moments with professional event, portrait, or commercial photography.', icon: '/sidebar-icons/photographer.svg' },
|
||
{ key: 'MAKEUP_ARTIST', title: 'Makeup Artist', desc: 'Professional styling for weddings, film, or editorial shoots.', icon: '/sidebar-icons/makeup-artist.svg' },
|
||
{ key: 'TUTOR', title: 'Tutor', desc: 'Expert guidance for academic subjects, languages, or specialized skills.', icon: '/sidebar-icons/tutor.svg' },
|
||
{ key: 'DEVELOPER', title: 'Developer', desc: 'Build websites, mobile apps, or enterprise software solutions.', icon: '/sidebar-icons/developers.svg' },
|
||
{ key: 'VIDEO_EDITOR', title: 'Video Editor', desc: 'Professional post-production for YouTube, film, or social media content.', icon: '/sidebar-icons/report.svg' },
|
||
{ key: 'UGC_CONTENT_CREATOR', title: 'UGC Content Creator', desc: 'Create user-generated content for ads, social campaigns, and product promotions.', icon: '/sidebar-icons/report.svg' },
|
||
{ key: 'FITNESS_TRAINER', title: 'Fitness Trainer', desc: 'Personalized workout plans and health coaching for your goals.', icon: '/sidebar-icons/users.svg' },
|
||
{ key: 'CATERING_SERVICES', title: 'Catering Services', desc: 'High-quality food services for events, corporate parties, and weddings.', icon: '/sidebar-icons/order.svg' },
|
||
{ key: 'GRAPHIC_DESIGNER', title: 'Graphic Designer', desc: 'Creative branding, logo design, and visual marketing assets.', icon: '/sidebar-icons/designation.svg' },
|
||
{ key: 'SOCIAL_MEDIA_MANAGER', title: 'Social Media Manager', desc: 'Strategic growth and content management for your online presence.', icon: '/sidebar-icons/leads.svg' },
|
||
];
|
||
|
||
const REQUIREMENT_ROLE_DETAILS: Record<string, { title: string; subtitle: string; fields: string[]; toggles: string[]; styles: string[] }> = {
|
||
PHOTOGRAPHER: {
|
||
title: 'Photography Details',
|
||
subtitle: 'Tell us more about the shoot to get accurate quotes.',
|
||
fields: ['Event Type', 'Shoot Type', 'Event Date & Time', 'Event Duration (Hours)', 'Venue / Location', 'Number of People', 'Delivery Deadline'],
|
||
toggles: ['Outdoor Shoot', 'Drone Required', 'Physical Album'],
|
||
styles: ['Black & White', 'Vintage', 'Cinematic', 'Fine Art'],
|
||
},
|
||
MAKEUP_ARTIST: {
|
||
title: 'Makeup Service Details',
|
||
subtitle: 'Share look preferences and event details for precise proposals.',
|
||
fields: ['Event Type', 'Makeup Category', 'Event Date & Time', 'Artists Required', 'Venue / Location', 'Skin Tone Preference', 'Ready By Time'],
|
||
toggles: ['Trial Session Required', 'Hair Styling Included', 'Travel Included'],
|
||
styles: ['Natural', 'Glam', 'HD', 'Airbrush'],
|
||
},
|
||
TUTOR: {
|
||
title: 'Tutoring Requirement Details',
|
||
subtitle: 'Specify class details so tutors can send the right plan.',
|
||
fields: ['Subject', 'Class / Grade', 'Mode (Online / Offline)', 'Sessions Per Week', 'Preferred Start Date', 'Student Location', 'Exam Goal'],
|
||
toggles: ['1-on-1 Sessions', 'Homework Support', 'Weekly Progress Reports'],
|
||
styles: ['Concept Focused', 'Exam Focused', 'Crash Course', 'Practice Heavy'],
|
||
},
|
||
DEVELOPER: {
|
||
title: 'Development Requirement Details',
|
||
subtitle: 'Add technical scope details for better estimates.',
|
||
fields: ['Project Type', 'Platform', 'Preferred Stack', 'Project Duration', 'Launch Deadline', 'Team Size Needed', 'Support Duration'],
|
||
toggles: ['UI/UX Included', 'API Development', 'Post-launch Maintenance'],
|
||
styles: ['MVP', 'Scale-ready', 'Enterprise', 'Performance-first'],
|
||
},
|
||
VIDEO_EDITOR: {
|
||
title: 'Video Editing Details',
|
||
subtitle: 'Tell us the output style and timeline for quick delivery.',
|
||
fields: ['Video Category', 'Final Duration', 'Footage Volume', 'Delivery Date', 'Editing Style', 'Platform', 'Revision Rounds'],
|
||
toggles: ['Color Grading', 'Subtitles', 'Motion Graphics'],
|
||
styles: ['Cinematic', 'Social Reels', 'Corporate', 'Documentary'],
|
||
},
|
||
UGC_CONTENT_CREATOR: {
|
||
title: 'UGC Campaign Details',
|
||
subtitle: 'Share campaign objective and deliverables for accurate creator proposals.',
|
||
fields: ['Campaign Goal', 'Platform', 'Deliverables Needed', 'Brand Category', 'Delivery Deadline', 'Target Audience', 'Usage Rights Duration'],
|
||
toggles: ['Script Support', 'Voiceover Needed', 'Product Shipping Required'],
|
||
styles: ['Product Demo', 'Lifestyle UGC', 'Hook-first Ads', 'Testimonial Style'],
|
||
},
|
||
FITNESS_TRAINER: {
|
||
title: 'Fitness Coaching Details',
|
||
subtitle: 'Share your goals and routine to match with the right coach.',
|
||
fields: ['Primary Goal', 'Current Activity Level', 'Preferred Mode', 'Training Days Per Week', 'Preferred Timings', 'Health Conditions', 'Goal Timeline'],
|
||
toggles: ['Nutrition Plan', 'Home Equipment Plan', 'Weekly Tracking'],
|
||
styles: ['Weight Loss', 'Strength', 'Mobility', 'Athletic'],
|
||
},
|
||
CATERING_SERVICES: {
|
||
title: 'Catering Requirement Details',
|
||
subtitle: 'Provide event and menu details to receive accurate quotes.',
|
||
fields: ['Event Type', 'Cuisine Preference', 'Guest Count', 'Service Date', 'Venue / Location', 'Meal Slot', 'Serving Style'],
|
||
toggles: ['Live Counters', 'Dessert Station', 'Serving Staff Included'],
|
||
styles: ['South Indian', 'North Indian', 'Continental', 'Fusion'],
|
||
},
|
||
GRAPHIC_DESIGNER: {
|
||
title: 'Design Requirement Details',
|
||
subtitle: 'Define your creative brief for better design proposals.',
|
||
fields: ['Project Type', 'Brand Industry', 'Deliverables Needed', 'Deadline', 'Target Audience', 'Reference Links', 'Output Formats'],
|
||
toggles: ['Brand Guidelines', 'Source Files', 'Print-ready Assets'],
|
||
styles: ['Minimal', 'Bold', 'Luxury', 'Playful'],
|
||
},
|
||
SOCIAL_MEDIA_MANAGER: {
|
||
title: 'Social Media Requirement Details',
|
||
subtitle: 'Tell us campaign goals and channels for the best fit.',
|
||
fields: ['Primary Goal', 'Platforms', 'Posting Frequency', 'Campaign Duration', 'Start Date', 'Brand Category', 'Monthly Budget'],
|
||
toggles: ['Reels Production', 'Ad Management', 'Community Management'],
|
||
styles: ['Growth Focused', 'Brand Awareness', 'Lead Generation', 'Content-first'],
|
||
},
|
||
};
|
||
|
||
const RESPONSE_ROWS = [
|
||
{ name: 'Julianne Vance', status: 'new', quote: '₹12,86,500' },
|
||
{ name: 'Marcus Holloway', status: 'shortlisted', quote: '₹6,80,600' },
|
||
{ name: 'Sarah Jenkins', status: 'rejected', quote: '₹4,15,000' },
|
||
{ name: 'David Wu', status: 'new', quote: '₹8,30,000' },
|
||
];
|
||
|
||
const COMPANY_JOB_STEPS = ['Job Basics', 'Role & Requirements', 'Compensation & Location', 'Screening', 'Review & Checkout'];
|
||
|
||
const COMPANY_JOB_SKILLS = ['Cloud Architecture', 'Kubernetes', 'Go/Rust', 'PostgreSQL', 'CI/CD', 'System Design'];
|
||
|
||
const LEAD_MARKETPLACE_TABS = ['All Leads', 'Recommended'];
|
||
|
||
type LeadCardStatus = 'open' | 'requested' | 'unlocked' | 'closed';
|
||
|
||
const INITIAL_PRO_LEAD_CARDS = [
|
||
{
|
||
id: 'LD-29831',
|
||
title: 'Luxury Wedding Coverage - ECR',
|
||
category: 'Wedding Photography',
|
||
location: 'Chennai, India',
|
||
area: 'ECR',
|
||
dateRequired: 'Apr 14, 2026',
|
||
urgency: 'High / Immediate',
|
||
budget: '₹2,40,000',
|
||
budgetValue: 240000,
|
||
priceRange: '₹2,00,000 - ₹2,60,000',
|
||
cost: 25,
|
||
status: 'open' as LeadCardStatus,
|
||
match: '98% match',
|
||
contactCount: 7,
|
||
maxContacts: 10,
|
||
},
|
||
{
|
||
id: 'LD-29745',
|
||
title: 'Editorial Fashion Shoot - Studio Series',
|
||
category: 'Fashion / Editorial',
|
||
location: 'Chennai, India',
|
||
area: 'Nungambakkam',
|
||
dateRequired: 'Apr 22, 2026',
|
||
urgency: 'Medium',
|
||
budget: '₹1,10,000',
|
||
budgetValue: 110000,
|
||
priceRange: '₹90,000 - ₹1,30,000',
|
||
cost: 25,
|
||
status: 'requested' as LeadCardStatus,
|
||
match: '92% match',
|
||
contactCount: 9,
|
||
maxContacts: 10,
|
||
},
|
||
{
|
||
id: 'LD-29612',
|
||
title: 'Corporate Branding Shoot - OMR Campus',
|
||
category: 'Commercial Architecture',
|
||
location: 'Chennai, India',
|
||
area: 'Sholinganallur',
|
||
dateRequired: 'May 05, 2026',
|
||
urgency: 'Low / Flexible',
|
||
budget: '₹1,85,000',
|
||
budgetValue: 185000,
|
||
priceRange: '₹1,50,000 - ₹2,10,000',
|
||
cost: 25,
|
||
status: 'unlocked' as LeadCardStatus,
|
||
match: '85% match',
|
||
contactCount: 10,
|
||
maxContacts: 10,
|
||
},
|
||
{
|
||
id: 'LD-29588',
|
||
title: 'Temple Wedding Documentary',
|
||
category: 'Traditional Wedding',
|
||
location: 'Chennai, India',
|
||
area: 'Mylapore',
|
||
dateRequired: 'Apr 30, 2026',
|
||
urgency: 'High / Immediate',
|
||
budget: '₹95,000',
|
||
budgetValue: 95000,
|
||
priceRange: '₹70,000 - ₹1,10,000',
|
||
cost: 25,
|
||
status: 'open' as LeadCardStatus,
|
||
match: '90% match',
|
||
contactCount: 4,
|
||
maxContacts: 10,
|
||
},
|
||
{
|
||
id: 'LD-29477',
|
||
title: 'Product Photography - Electronics',
|
||
category: 'E-commerce',
|
||
location: 'Chennai, India',
|
||
area: 'T Nagar',
|
||
dateRequired: 'May 12, 2026',
|
||
urgency: 'Medium',
|
||
budget: '₹75,000',
|
||
budgetValue: 75000,
|
||
priceRange: '₹50,000 - ₹90,000',
|
||
cost: 25,
|
||
status: 'open' as LeadCardStatus,
|
||
match: '88% match',
|
||
contactCount: 6,
|
||
maxContacts: 10,
|
||
},
|
||
{
|
||
id: 'LD-29340',
|
||
title: 'Corporate Leadership Portraits',
|
||
category: 'Corporate Portrait',
|
||
location: 'Chennai, India',
|
||
area: 'Guindy',
|
||
dateRequired: 'May 22, 2026',
|
||
urgency: 'Low / Flexible',
|
||
budget: '₹1,35,000',
|
||
budgetValue: 135000,
|
||
priceRange: '₹1,00,000 - ₹1,50,000',
|
||
cost: 25,
|
||
status: 'closed' as LeadCardStatus,
|
||
match: '84% match',
|
||
contactCount: 10,
|
||
maxContacts: 10,
|
||
},
|
||
];
|
||
|
||
const JOB_SEEKER_OPEN_JOBS = [
|
||
{ id: 'JOB-1001', title: 'Senior UX/UI Architect', company: 'Design System Global', location: 'San Francisco, CA (Remote)', salary: '$160k - $210k', exp: '5-7 Years Exp.', type: 'Full-time', tags: ['FIGMA', 'REACT', 'TOKEN STUDIO'], match: '98% Match', posted: 'Posted 2 hours ago' },
|
||
{ id: 'JOB-1002', title: 'Engineering Manager (Cloud Infrastructure)', company: 'FinStream Tech', location: 'London, UK (Hybrid)', salary: '£120k - £150k', exp: '8+ Years Exp.', type: 'Hybrid', tags: ['AWS', 'KUBERNETES', 'TERRAFORM'], match: '92% Match', posted: 'Posted 5 hours ago' },
|
||
{ id: 'JOB-1003', title: 'Head of Talent Acquisition', company: 'GreenGrowth HR', location: 'Remote (North America)', salary: '$130k - $180k', exp: '10+ Years Exp.', type: 'Worldwide', tags: ['RECRUITING', 'STRATEGIC HR'], match: '85% Match', posted: 'Posted 1 day ago' },
|
||
{ id: 'JOB-1004', title: 'Senior Data Scientist (LLM Focus)', company: 'Aether Intelligence', location: 'New York, NY', salary: '$200k - $250k', exp: '4+ Years Exp.', type: 'Fast Hire', tags: ['PYTHON', 'PYTORCH', 'TRANSFORMERS'], match: '95% Match', posted: 'Posted 3 hours ago' },
|
||
];
|
||
|
||
type JobBoardCard = (typeof JOB_SEEKER_OPEN_JOBS)[number];
|
||
type CompanyJobSubmissionStatus = 'VERIFICATION_PENDING' | 'VERIFIED' | 'APPROVAL_PENDING' | 'APPROVED_LIVE';
|
||
type CompanyJobSubmission = {
|
||
id: string;
|
||
job: JobBoardCard;
|
||
status: CompanyJobSubmissionStatus;
|
||
};
|
||
type CompanyJobDraft = {
|
||
title: string;
|
||
company: string;
|
||
location: string;
|
||
salary: string;
|
||
exp: string;
|
||
type: string;
|
||
tags: string[];
|
||
match: string;
|
||
posted: string;
|
||
department: string;
|
||
openings: string;
|
||
};
|
||
|
||
type RequirementRow = (typeof REQUIREMENT_ROWS)[number];
|
||
|
||
const DEFAULT_COMPANY_JOB_DRAFT: CompanyJobDraft = {
|
||
title: 'Senior Technical Architect',
|
||
company: 'Your Company',
|
||
location: 'Remote',
|
||
salary: '$80,000 - $120,000',
|
||
exp: '8+ Years Exp.',
|
||
type: 'Full-time, Permanent',
|
||
tags: COMPANY_JOB_SKILLS.slice(0, 3),
|
||
match: 'New',
|
||
posted: 'Posted just now',
|
||
department: 'Engineering & Infrastructure',
|
||
openings: '1',
|
||
};
|
||
|
||
const JOB_SEEKER_APPLIED_ROWS = [
|
||
{ id: 'APP-NX-8293', title: 'Senior Product Designer', company: 'Stripe', location: 'Remote, USA', status: 'Shortlisted', note: 'Recruiter viewed 2h ago' },
|
||
{ id: 'APP-NX-7741', title: 'Staff UX Engineer', company: 'Airbnb', location: 'San Francisco, CA', status: 'Under Review', note: 'In initial screening' },
|
||
{ id: 'APP-NX-9011', title: 'Product Marketing Lead', company: 'Notion', location: 'New York, NY', status: 'Applied', note: 'Successfully submitted' },
|
||
{ id: 'APP-NX-5529', title: 'Growth Specialist', company: 'Meta', location: 'Remote', status: 'Not Selected', note: 'Closed on Oct 10' },
|
||
];
|
||
|
||
const HELP_CENTER_CATEGORIES = [
|
||
{ title: 'Account & Login', description: 'Trouble logging in? Manage your password and account access.', articles: 24, icon: '/sidebar-icons/users.svg' },
|
||
{ title: 'Profile & Verification', description: 'How to get verified and complete your profile faster.', articles: 18, icon: '/sidebar-icons/approval.svg' },
|
||
{ title: 'Jobs & Applications', description: 'Find work and manage your active project applications.', articles: 32, icon: '/sidebar-icons/jobs.svg' },
|
||
{ title: 'Leads & Responses', description: 'Learn how to respond to incoming business leads.', articles: 15, icon: '/sidebar-icons/leads.svg' },
|
||
{ title: 'Credits & Payments', description: 'Invoices, purchasing credits, and billing history.', articles: 21, icon: '/sidebar-icons/credits.svg' },
|
||
{ title: 'Settings & Security', description: 'Manage data, sessions, and notification preferences.', articles: 12, icon: '/sidebar-icons/role.svg' },
|
||
{ title: 'Service Switching', description: 'How to switch between approved service profiles.', articles: 9, icon: '/sidebar-icons/role.svg' },
|
||
{ title: 'Support & Tickets', description: 'Track your support history and ticket updates.', articles: 14, icon: '/sidebar-icons/support.svg' },
|
||
];
|
||
|
||
const HELP_CENTER_ARTICLES = [
|
||
'How to verify your business account',
|
||
'Setting up two-factor authentication',
|
||
'Understanding lead credit balances',
|
||
'Troubleshooting mobile notifications',
|
||
'Connecting your bank account',
|
||
];
|
||
|
||
const HELP_CENTER_FAQS = [
|
||
{
|
||
q: 'What are lead credits?',
|
||
a: 'Lead credits are the internal currency on Nxtgauge. You use them to respond to new inquiries and business opportunities.',
|
||
},
|
||
{
|
||
q: 'How long does verification take?',
|
||
a: 'Most verifications complete within 24 to 72 hours, depending on document quality and review queue.',
|
||
},
|
||
{
|
||
q: 'Can I change my account service?',
|
||
a: 'Yes. Use Switch Services once your additional service registration is approved.',
|
||
},
|
||
{
|
||
q: 'What is the refund policy for credits?',
|
||
a: 'Credits follow platform policy and may be eligible for adjustment when billing issues are validated.',
|
||
},
|
||
];
|
||
|
||
const HELP_TICKET_ROWS = [
|
||
{ id: 'TCK-1042', title: 'Verification clarification required', status: 'Open', updated: '2h ago', priority: 'High', lastMessage: 'Please share GST certificate copy.' },
|
||
{ id: 'TCK-1031', title: 'Unable to see credit invoice', status: 'In Progress', updated: 'Yesterday', priority: 'Medium', lastMessage: 'Invoice regenerated and shared via email.' },
|
||
{ id: 'TCK-1007', title: 'Need onboarding status update', status: 'Resolved', updated: '3 days ago', priority: 'Low', lastMessage: 'Profile approved successfully.' },
|
||
] as const;
|
||
|
||
const HELP_TICKET_DETAILS: Record<
|
||
string,
|
||
{
|
||
userMessage: string;
|
||
adminMessage: string;
|
||
requestedDocuments: string[];
|
||
receivedFiles: Array<{ file: string; state: 'Received' | 'Pending Upload' }>;
|
||
}
|
||
> = {
|
||
'TCK-1042': {
|
||
userMessage: 'Can you confirm if any additional document is required from my side?',
|
||
adminMessage: 'Your verification is active. Please upload the pending documents listed below to avoid delays.',
|
||
requestedDocuments: ['GST Certificate', 'Authorization Letter', 'Business PAN Copy'],
|
||
receivedFiles: [
|
||
{ file: 'gst_certificate.pdf', state: 'Received' },
|
||
{ file: 'authorization_letter.jpg', state: 'Received' },
|
||
{ file: 'business_pan.pdf', state: 'Pending Upload' },
|
||
],
|
||
},
|
||
'TCK-1031': {
|
||
userMessage: 'I cannot find my latest credit invoice in billing history.',
|
||
adminMessage: 'Invoice has been regenerated. Please verify if the attached copy is visible on your billing page now.',
|
||
requestedDocuments: ['Invoice Month Confirmation'],
|
||
receivedFiles: [{ file: 'invoice_month_confirmation.txt', state: 'Received' }],
|
||
},
|
||
'TCK-1007': {
|
||
userMessage: 'Can I get an update on my onboarding review status?',
|
||
adminMessage: 'Your onboarding has been approved. No further action is pending from your side.',
|
||
requestedDocuments: [],
|
||
receivedFiles: [],
|
||
},
|
||
};
|
||
|
||
function roleIcon(roleKey: string) {
|
||
const key = String(roleKey || '').toLowerCase();
|
||
if (key.includes('photo')) return Camera;
|
||
if (key.includes('makeup')) return Scissors;
|
||
if (key.includes('tutor')) return GraduationCap;
|
||
if (key.includes('developer')) return Code2;
|
||
if (key.includes('video')) return Clapperboard;
|
||
if (key.includes('ugc') || (key.includes('content') && key.includes('creator'))) return Clapperboard;
|
||
if (key.includes('graphic')) return PenTool;
|
||
if (key.includes('social')) return Megaphone;
|
||
if (key.includes('cater')) return UtensilsCrossed;
|
||
if (key.includes('fitness')) return Dumbbell;
|
||
return BriefcaseBusiness;
|
||
}
|
||
|
||
function roleIconAsset(roleKey: string) {
|
||
const key = String(roleKey || '').toLowerCase();
|
||
if (key.includes('photo')) return '/sidebar-icons/photographer.svg';
|
||
if (key.includes('makeup')) return '/sidebar-icons/makeup-artist.svg';
|
||
if (key.includes('tutor')) return '/sidebar-icons/tutor.svg';
|
||
if (key.includes('developer')) return '/sidebar-icons/developers.svg';
|
||
if (key.includes('video')) return '';
|
||
if (key.includes('ugc') || (key.includes('content') && key.includes('creator'))) return '/sidebar-icons/report.svg';
|
||
if (key.includes('graphic')) return '/sidebar-icons/designation.svg';
|
||
if (key.includes('social')) return '/sidebar-icons/leads.svg';
|
||
if (key.includes('cater')) return '';
|
||
if (key.includes('fitness')) return '/sidebar-icons/users.svg';
|
||
if (key.includes('company')) return '/sidebar-icons/company.svg';
|
||
return '/sidebar-icons/role.svg';
|
||
}
|
||
|
||
export default function DashboardDesignPreview(props: {
|
||
status: 'ACTIVE' | 'INACTIVE';
|
||
sidebarItems: string[];
|
||
activeSidebar: string;
|
||
onSidebarSelect: (item: string) => void;
|
||
tabs: string[];
|
||
activeTab: string;
|
||
onTabSelect: (item: string) => void;
|
||
widgets: string[];
|
||
fields: string[];
|
||
mode?: 'default' | 'customer_external';
|
||
roleKey?: string;
|
||
exploreRoles?: Array<{ key: string; name: string }>;
|
||
onOpenFullscreen?: () => void;
|
||
hidePreviewHeader?: boolean;
|
||
liveData?: { userName: string; userId: string; rolePrefix: string };
|
||
}) {
|
||
const [isVerified, setIsVerified] = createSignal(false);
|
||
const [verificationPending, setVerificationPending] = createSignal(false);
|
||
|
||
const isProfessionalRoleKey = (roleKey: string) => {
|
||
const role = normalizeRoleKey(roleKey);
|
||
return role !== 'COMPANY' && role !== 'JOB_SEEKER' && role !== 'CUSTOMER';
|
||
};
|
||
const normalizeTabKey = (value: string) => String(value || '').trim().toLowerCase();
|
||
const isCustomerExternalMode = createMemo(() => props.mode === 'customer_external');
|
||
const previewSidebarItems = createMemo(() => (props.sidebarItems.length ? props.sidebarItems : ['My Dashboard', 'My Profile', 'Switch Services', 'Logout']));
|
||
const customerView = createMemo(() => customerViewFor(props.activeSidebar || previewSidebarItems()[0] || 'My Dashboard', props.roleKey || ''));
|
||
const currentProfileSpec = createMemo(() => profileSpecForRole(props.roleKey || ''));
|
||
const isJobSeekerRole = createMemo(() => normalizeRoleKey(props.roleKey || '') === 'JOB_SEEKER');
|
||
const activeTabKey = createMemo(() => normalizeTabKey(props.activeTab));
|
||
const previewTabs = createMemo(() => {
|
||
if (isCustomerExternalMode()) return customerView().tabs;
|
||
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 previewFields = createMemo(() => (props.fields.length ? props.fields : ['full_name', 'email', 'verification_status', 'approval_status']));
|
||
const customerKey = createMemo(() => String(props.activeSidebar || '').toLowerCase().trim());
|
||
const portfolioJobsCompletedPreview = 1;
|
||
const portfolioFeedbackCountPreview = 0;
|
||
const portfolioTestimonialsUnlocked = createMemo(() => portfolioJobsCompletedPreview >= 3 && portfolioFeedbackCountPreview >= 2);
|
||
const exploreRoleCards = createMemo(() => {
|
||
const roles = Array.isArray(props.exploreRoles) ? props.exploreRoles : [];
|
||
const activeRoles = userRoles();
|
||
const professionalOnly = roles
|
||
.filter((r) => String(r?.key || '').trim())
|
||
.filter((r) => {
|
||
const roleKey = String(r?.key || '').toLowerCase();
|
||
return !roleKey.includes('company')
|
||
&& !roleKey.includes('job_seeker')
|
||
&& !roleKey.includes('jobseeker')
|
||
&& !roleKey.includes('customer')
|
||
&& !roleKey.includes('service_seeker');
|
||
})
|
||
.map((role) => {
|
||
const roleKey = String(role.key || '').trim();
|
||
const title = String(role.name || '').trim() || titleCase(roleKey.replace(/_/g, ' ').toLowerCase());
|
||
const isRegistered = activeRoles.some((ar) => normalizeRoleKey(ar.role_key || ar.key) === normalizeRoleKey(roleKey));
|
||
const isCurrent = normalizeRoleKey(props.roleKey || '') === normalizeRoleKey(roleKey);
|
||
return {
|
||
key: roleKey,
|
||
title,
|
||
subtitle: `Apply and onboard for ${title} to unlock more opportunities on Nxtgauge.`,
|
||
status: isCurrent ? 'Active' : isRegistered ? 'Registered' : 'Available',
|
||
action: isCurrent ? 'Active' : isRegistered ? 'Switch' : 'Register',
|
||
iconAsset: roleIconAsset(roleKey),
|
||
Icon: roleIcon(roleKey),
|
||
};
|
||
});
|
||
const defaults = [
|
||
{ key: 'PHOTOGRAPHER', title: 'Photographer', subtitle: 'Capture professional moments and monetize your creative vision.', iconAsset: '/sidebar-icons/photographer.svg', Icon: Camera },
|
||
{ key: 'MAKEUP_ARTIST', title: 'Makeup Artist', subtitle: 'Offer beauty services for events and premium clientele.', iconAsset: '/sidebar-icons/makeup-artist.svg', Icon: Scissors },
|
||
{ key: 'TUTOR', title: 'Tutor', subtitle: 'Share expertise through remote and flexible teaching.', iconAsset: '/sidebar-icons/tutor.svg', Icon: GraduationCap },
|
||
{ key: 'DEVELOPER', title: 'Developer', subtitle: 'Build scalable products for high-growth businesses.', iconAsset: '/sidebar-icons/developers.svg', Icon: Code2 },
|
||
{ key: 'VIDEO_EDITOR', title: 'Video Editor', subtitle: 'Craft compelling stories with world-class editing.', iconAsset: '/sidebar-icons/report.svg', Icon: Clapperboard },
|
||
{ key: 'UGC_CONTENT_CREATOR', title: 'UGC Content Creator', subtitle: 'Create ad-ready user-generated content for brand campaigns.', iconAsset: '/sidebar-icons/report.svg', Icon: Clapperboard },
|
||
{ key: 'GRAPHIC_DESIGNER', title: 'Graphic Designer', subtitle: 'Design visual systems that brands remember.', iconAsset: '/sidebar-icons/designation.svg', Icon: PenTool },
|
||
{ key: 'SOCIAL_MEDIA_MANAGER', title: 'Social Media Manager', subtitle: 'Manage digital presence and audience growth.', iconAsset: '/sidebar-icons/leads.svg', Icon: Megaphone },
|
||
{ key: 'FITNESS_TRAINER', title: 'Fitness Trainer', subtitle: 'Coach clients on health and performance goals.', iconAsset: '/sidebar-icons/users.svg', Icon: Dumbbell },
|
||
{ key: 'CATERING_SERVICES', title: 'Catering Services', subtitle: 'Deliver high-quality catering for events and celebrations.', iconAsset: '/sidebar-icons/order.svg', Icon: UtensilsCrossed },
|
||
];
|
||
const merged = [...professionalOnly];
|
||
defaults.forEach((item) => {
|
||
if (!merged.some((row) => normalizeRoleKey(row.key) === normalizeRoleKey(item.key))) {
|
||
const isRegistered = activeRoles.some((ar) => normalizeRoleKey(ar.role_key || ar.key) === normalizeRoleKey(item.key));
|
||
const isCurrent = normalizeRoleKey(props.roleKey || '') === normalizeRoleKey(item.key);
|
||
merged.push({
|
||
...item,
|
||
status: isCurrent ? 'Active' : isRegistered ? 'Registered' : 'Available',
|
||
action: isCurrent ? 'Active' : isRegistered ? 'Switch' : 'Register'
|
||
});
|
||
}
|
||
});
|
||
return merged.slice(0, 10);
|
||
});
|
||
const [dashboardWidgetOrder, setDashboardWidgetOrder] = createSignal<string[]>([]);
|
||
const [draggingDashboardWidget, setDraggingDashboardWidget] = createSignal<string | null>(null);
|
||
const [helpCenterTab, setHelpCenterTab] = createSignal<'help_center' | 'my_tickets' | 'create_ticket'>('help_center');
|
||
const [myTicketsTab, setMyTicketsTab] = createSignal<'all_tickets' | 'view_ticket'>('all_tickets');
|
||
const [activeTicketId, setActiveTicketId] = createSignal<string>('TCK-1042');
|
||
const [openFaqIndex, setOpenFaqIndex] = createSignal<number | null>(0);
|
||
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);
|
||
const [portfolioTopTab, setPortfolioTopTab] = createSignal<'my_portfolio' | 'preview'>('my_portfolio');
|
||
const [profileApprovalState, setProfileApprovalState] = createSignal<'DRAFT' | 'SUBMITTED' | 'IN_REVIEW' | 'DOCUMENTS_REQUESTED' | 'APPROVED' | 'REJECTED'>('DRAFT');
|
||
const [portfolioApprovalState, setPortfolioApprovalState] = createSignal<'DRAFT' | 'SUBMITTED' | 'IN_REVIEW' | 'DOCUMENTS_REQUESTED' | 'APPROVED' | 'REJECTED'>('DRAFT');
|
||
const [portfolioSpecialties, setPortfolioSpecialties] = createSignal<string[]>([]);
|
||
const [portfolioLanguages, setPortfolioLanguages] = createSignal<string[]>([]);
|
||
const [portfolioServiceAreas, setPortfolioServiceAreas] = createSignal<string[]>([]);
|
||
const [portfolioSpecialtyInput, setPortfolioSpecialtyInput] = createSignal('');
|
||
const [portfolioLanguageInput, setPortfolioLanguageInput] = createSignal('');
|
||
const [portfolioAreaInput, setPortfolioAreaInput] = createSignal('');
|
||
const [profileDocumentType, setProfileDocumentType] = createSignal('Aadhar Card');
|
||
const [portfolioFormValues, setPortfolioFormValues] = createSignal<Record<string, string>>({});
|
||
const [portfolioFormErrors, setPortfolioFormErrors] = createSignal<Record<string, string>>({});
|
||
const [userRoles, setUserRoles] = createSignal<any[]>([]);
|
||
const [portfolioValidationNotice, setPortfolioValidationNotice] = createSignal('');
|
||
const [portfolioServices, setPortfolioServices] = createSignal<Array<{ name: string; model: string; price: string; details: string }>>([]);
|
||
const [portfolioServiceDraft, setPortfolioServiceDraft] = createSignal<{ name: string; model: string; price: string; details: string }>({ name: '', model: 'Flat', price: '', details: '' });
|
||
const [portfolioExperiences, setPortfolioExperiences] = createSignal<Array<{ year: string; title: string; details: string }>>([]);
|
||
const [portfolioExperienceDraft, setPortfolioExperienceDraft] = createSignal<{ year: string; title: string; details: string }>({ year: '', title: '', details: '' });
|
||
const [portfolioDesignTools, setPortfolioDesignTools] = createSignal<string[]>([]);
|
||
const [portfolioToolInput, setPortfolioToolInput] = createSignal('');
|
||
const [portfolioPhotos, setPortfolioPhotos] = createSignal<string[]>([]);
|
||
const [requirementsView, setRequirementsView] = createSignal<'list' | 'new' | 'detail'>('list');
|
||
const [requirementsStep, setRequirementsStep] = createSignal(1);
|
||
const [requirementRows, setRequirementRows] = createSignal<RequirementRow[]>(REQUIREMENT_ROWS);
|
||
const [selectedRequirementRole, setSelectedRequirementRole] = createSignal('PHOTOGRAPHER');
|
||
const [selectedRequirementId, setSelectedRequirementId] = createSignal('#REQ-9012');
|
||
const [jobPostView, setJobPostView] = createSignal<'form' | 'review' | 'success'>('form');
|
||
const [jobPostStep, setJobPostStep] = createSignal(1);
|
||
const [jobBoardJobs, setJobBoardJobs] = createSignal<JobBoardCard[]>(JOB_SEEKER_OPEN_JOBS);
|
||
const [companyJobSubmissions, setCompanyJobSubmissions] = createSignal<CompanyJobSubmission[]>([]);
|
||
const [companyJobDraft] = createSignal<CompanyJobDraft>(DEFAULT_COMPANY_JOB_DRAFT);
|
||
const [jobSeekerScreen, setJobSeekerScreen] = createSignal<'list' | 'detail' | 'apply'>('list');
|
||
const [jobSeekerSelectedId, setJobSeekerSelectedId] = createSignal(JOB_SEEKER_OPEN_JOBS[0]?.id || '');
|
||
const [jobSeekerApplyStep, setJobSeekerApplyStep] = createSignal(2);
|
||
const [lastJobSeekerTabKey, setLastJobSeekerTabKey] = createSignal('');
|
||
const [leadCards, setLeadCards] = createSignal(INITIAL_PRO_LEAD_CARDS);
|
||
const [leadMarketplaceTab, setLeadMarketplaceTab] = createSignal('All Leads');
|
||
const [leadSearch, setLeadSearch] = createSignal('');
|
||
const [leadAreaFilter, setLeadAreaFilter] = createSignal('All Areas');
|
||
const [leadBudgetFilter, setLeadBudgetFilter] = createSignal('All Budgets');
|
||
const [leadDateFilter, setLeadDateFilter] = createSignal('Any Date');
|
||
const [leadSortFilter, setLeadSortFilter] = createSignal('Newest First');
|
||
const [leadFiltersOpen, setLeadFiltersOpen] = createSignal(false);
|
||
const [leadSortOpen, setLeadSortOpen] = createSignal(false);
|
||
const [leadPage, setLeadPage] = createSignal(1);
|
||
const [activeLeadDetailId, setActiveLeadDetailId] = createSignal('');
|
||
const [leadContactConfirmId, setLeadContactConfirmId] = createSignal('');
|
||
const [activeResponseLeadId, setActiveResponseLeadId] = createSignal('');
|
||
const [responsesDetailMode, setResponsesDetailMode] = createSignal(false);
|
||
const [requestedSearch, setRequestedSearch] = createSignal('');
|
||
const [requestedStatusFilter, setRequestedStatusFilter] = createSignal('All Status');
|
||
const [requestedSortFilter, setRequestedSortFilter] = createSignal('Newest First');
|
||
const [requestedSortOpen, setRequestedSortOpen] = createSignal(false);
|
||
const [requestedFilterOpen, setRequestedFilterOpen] = createSignal(false);
|
||
const [requestedPage, setRequestedPage] = createSignal(1);
|
||
const [leadCredits, setLeadCredits] = createSignal(250);
|
||
const [checkoutPackage, setCheckoutPackage] = createSignal<any | null>(null);
|
||
const [paymentStep, setPaymentStep] = createSignal<'idle' | 'processing' | 'verifying' | 'success' | 'error'>('idle');
|
||
const [paymentRef, setPaymentRef] = createSignal<string | null>(null);
|
||
const [paymentResult, setPaymentResult] = createSignal<any>(null);
|
||
// Coupon state
|
||
const [couponCode, setCouponCode] = createSignal('');
|
||
const [appliedCoupon, setAppliedCoupon] = createSignal<{code: string; discount_type: string; discount_value: number; final_price_inr: number} | null>(null);
|
||
const [couponError, setCouponError] = createSignal('');
|
||
const [couponLoading, setCouponLoading] = createSignal(false);
|
||
const [creditManageView, setCreditManageView] = createSignal(false);
|
||
const [txRows, setTxRows] = createSignal<Array<[string, string, string, string, string, string]>>([
|
||
['#INV-2023-089', 'Enterprise Growth', '5,000', '₹1,20,000', 'Completed', 'Oct 24, 2023'],
|
||
['#INV-2023-074', 'Starter Kick', '500', '₹15,000', 'Pending', 'Oct 23, 2023'],
|
||
['#INV-2023-052', 'Pro Pack', '2,500', '₹65,000', 'Completed', 'Oct 21, 2023'],
|
||
['#INV-2023-031', 'Top-up', '1,000', '₹30,000', 'Failed', 'Oct 20, 2023'],
|
||
]);
|
||
const [leadRequestRows, setLeadRequestRows] = createSignal([
|
||
{ id: 'LD-29745', title: 'Editorial Fashion Shoot - Studio Series', city: 'Nungambakkam, Chennai', requestDate: 'Apr 02, 2026', status: 'request_sent', decisionDate: '--' },
|
||
{ id: 'LD-29612', title: 'Corporate Branding Shoot - OMR Campus', city: 'Sholinganallur, Chennai', requestDate: 'Apr 01, 2026', status: 'contact_unlocked', decisionDate: 'Apr 01, 2026' },
|
||
{ id: 'LD-29588', title: 'Temple Wedding Documentary', city: 'Mylapore, Chennai', requestDate: 'Mar 30, 2026', status: 'rejected', decisionDate: 'Mar 30, 2026' },
|
||
] as Array<{ id: string; title: string; city: string; requestDate: string; status: 'request_sent' | 'approved' | 'contact_unlocked' | 'rejected' | 'expired_refunded' | 'cancelled_by_professional'; decisionDate: string }>);
|
||
const [lastSidebarKey, setLastSidebarKey] = createSignal('');
|
||
const [profileSettingToggles, setProfileSettingToggles] = createSignal<Record<string, boolean>>({
|
||
email_updates: true,
|
||
in_app_alerts: true,
|
||
verification_reminders: true,
|
||
profile_visibility: true,
|
||
data_sharing_consent: false,
|
||
});
|
||
const toggleProfileSetting = (key: string) => {
|
||
setProfileSettingToggles((prev) => ({ ...prev, [key]: !prev[key] }));
|
||
};
|
||
const submitCompanyJobForReview = () => {
|
||
const draft = companyJobDraft();
|
||
const id = `JOB-COMP-${Date.now()}`;
|
||
const submittedJob: JobBoardCard = {
|
||
id,
|
||
title: draft.title,
|
||
company: draft.company,
|
||
location: draft.location,
|
||
salary: draft.salary,
|
||
exp: draft.exp,
|
||
type: draft.type,
|
||
tags: draft.tags,
|
||
match: draft.match,
|
||
posted: draft.posted,
|
||
};
|
||
setCompanyJobSubmissions((prev) => [{ id, job: submittedJob, status: 'VERIFICATION_PENDING' }, ...prev]);
|
||
setTimeout(() => {
|
||
setCompanyJobSubmissions((prev) => prev.map((row) => (row.id === id ? { ...row, status: 'VERIFIED' } : row)));
|
||
setTimeout(() => {
|
||
setCompanyJobSubmissions((prev) => prev.map((row) => (row.id === id ? { ...row, status: 'APPROVAL_PENDING' } : row)));
|
||
setTimeout(() => {
|
||
setCompanyJobSubmissions((prev) => prev.map((row) => (row.id === id ? { ...row, status: 'APPROVED_LIVE' } : row)));
|
||
setJobBoardJobs((prev) => [submittedJob, ...prev.filter((job) => job.id !== id)]);
|
||
setJobSeekerSelectedId(id);
|
||
}, 650);
|
||
}, 650);
|
||
}, 650);
|
||
};
|
||
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 = {
|
||
id,
|
||
title: `${roleLabel} Requirement`,
|
||
summary: 'Submitted from customer requirement form',
|
||
category: roleKey,
|
||
amount: '₹1,50,000',
|
||
budget: '₹1,50,000 - ₹2,00,000',
|
||
location: 'Chennai (On-site)',
|
||
submission,
|
||
status: 'under review',
|
||
responses: 0,
|
||
responseTag: 'Verification Pending',
|
||
};
|
||
setRequirementRows((prev) => [newRequirement, ...prev]);
|
||
setSelectedRequirementId(id);
|
||
setRequirementsView('list');
|
||
setRequirementsStep(1);
|
||
setTimeout(() => {
|
||
setRequirementRows((prev) => prev.map((row) => (row.id === id ? { ...row, status: 'approved', responses: 0, responseTag: 'Approval Pending' } : row)));
|
||
setTimeout(() => {
|
||
setRequirementRows((prev) => prev.map((row) => (row.id === id ? { ...row, status: 'active', responses: 0, responseTag: 'Active (0 Responses)' } : row)));
|
||
setLeadCards((prev) => {
|
||
const leadId = id.replace('#REQ', 'LD');
|
||
if (prev.some((card) => card.id === leadId)) return prev;
|
||
return [
|
||
{
|
||
id: leadId,
|
||
title: newRequirement.title,
|
||
category: roleLabel,
|
||
location: 'Chennai, India',
|
||
area: 'Chennai',
|
||
dateRequired: 'May 30, 2026',
|
||
urgency: 'Medium',
|
||
budget: '₹1,50,000',
|
||
budgetValue: 150000,
|
||
priceRange: newRequirement.budget,
|
||
cost: 25,
|
||
status: 'open',
|
||
match: '90% match',
|
||
contactCount: 0,
|
||
maxContacts: 10,
|
||
},
|
||
...prev,
|
||
];
|
||
});
|
||
}, 650);
|
||
}, 650);
|
||
};
|
||
const isProfessionalRole = createMemo(() => isProfessionalRoleKey(props.roleKey || ''));
|
||
const bothApprovalsApproved = createMemo(() => {
|
||
if (!isProfessionalRole()) return profileApprovalState() === 'APPROVED';
|
||
return profileApprovalState() === 'APPROVED' && portfolioApprovalState() === 'APPROVED';
|
||
});
|
||
const approvalTone = (state: string) => {
|
||
if (state === 'APPROVED') return { border: '#E5E7EB', bg: '#F9FAFB', text: '#374151', label: 'Approved' };
|
||
if (state === 'IN_REVIEW' || state === 'SUBMITTED') return { border: '#E5E7EB', bg: '#F9FAFB', text: '#374151', label: 'In Review' };
|
||
if (state === 'DOCUMENTS_REQUESTED') return { border: '#E5E7EB', bg: '#F9FAFB', text: '#374151', label: 'Documents Requested' };
|
||
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`),
|
||
);
|
||
|
||
// Professional services (Packages)
|
||
const [servicesResource, { refetch: refetchServicesLive }] = createResource(
|
||
() => (hasLive() && isProfessionalRole() ? livePrefix() : null),
|
||
(prefix) => apiFetch(`/api/${prefix}/services/me`),
|
||
);
|
||
|
||
// Professional portfolio
|
||
const [portfolioResource, { refetch: refetchPortfolioLive }] = createResource(
|
||
() => (hasLive() && isProfessionalRole() ? livePrefix() : null),
|
||
(prefix) => apiFetch(`/api/${prefix}/portfolio/me`),
|
||
);
|
||
|
||
// Current user roles (for switching and exploration status)
|
||
const [userRolesResource, { refetch: refetchUserRolesLive }] = createResource(
|
||
() => (hasLive() ? 'yes' : null),
|
||
() => apiFetch('/api/users/roles'),
|
||
);
|
||
|
||
// Tracecoin pricing packages
|
||
const [pricingPackagesResource] = createResource(
|
||
() => (hasLive() ? 'yes' : null),
|
||
() => apiFetch('/api/packages'),
|
||
);
|
||
|
||
// Dynamic Knowledge Base Resource
|
||
const [kbResource] = createResource(
|
||
() => (hasLive() ? 'yes' : null),
|
||
async () => {
|
||
const res = await apiFetch('/api/runtime-config/knowledge_base/GLOBAL_KB');
|
||
const data = (res?.payload as RuntimeKBConfig) || { articles: [] };
|
||
return data;
|
||
}
|
||
);
|
||
|
||
const [kbSearch, setKbSearch] = createSignal('');
|
||
const [kbCategory, setKbCategory] = createSignal('All');
|
||
|
||
const filteredKbArticles = createMemo(() => {
|
||
const data = kbResource()?.articles || [];
|
||
return data.filter(a => {
|
||
if (!a.isPublished) return false;
|
||
const matchesSearch = a.title.toLowerCase().includes(kbSearch().toLowerCase()) ||
|
||
a.content.toLowerCase().includes(kbSearch().toLowerCase());
|
||
const matchesCategory = kbCategory() === 'All' || a.category === kbCategory();
|
||
return matchesSearch && matchesCategory;
|
||
});
|
||
});
|
||
|
||
const kbCategoriesWithCounts = createMemo(() => {
|
||
const articles = kbResource()?.articles || [];
|
||
const counts: Record<string, number> = {};
|
||
articles.forEach(a => {
|
||
if (!a.isPublished) return;
|
||
counts[a.category] = (counts[a.category] || 0) + 1;
|
||
});
|
||
return Object.entries(counts).map(([title, articles]) => ({
|
||
title,
|
||
articles,
|
||
icon: '/icons/help-book.png', // Placeholder icon
|
||
description: `Guides and documentation for ${title}.`
|
||
}));
|
||
});
|
||
|
||
const applyCoupon = async () => {
|
||
const code = couponCode().trim();
|
||
if (!code) { setCouponError('Enter a coupon code'); return; }
|
||
setCouponLoading(true);
|
||
setCouponError('');
|
||
try {
|
||
const token = getToken();
|
||
const headers: HeadersInit = {
|
||
'Content-Type': 'application/json',
|
||
...(token ? { 'Authorization': `Bearer ${token}` } : {})
|
||
};
|
||
const res = await fetch(`${API}/coupons/validate`, {
|
||
method: 'POST',
|
||
headers,
|
||
credentials: 'include',
|
||
body: JSON.stringify({
|
||
coupon_code: code,
|
||
role_key: normalizeRoleKey(props.roleKey || ''),
|
||
package_price_inr: Number(checkoutPackage()?.price_paise || 0)
|
||
})
|
||
});
|
||
const data = await res.json();
|
||
if (res.ok && data.valid) {
|
||
setAppliedCoupon({
|
||
code,
|
||
discount_type: data.discount_type,
|
||
discount_value: data.discount_value,
|
||
final_price_inr: data.final_price_inr
|
||
});
|
||
} else {
|
||
setCouponError(data.message || 'Invalid coupon');
|
||
setAppliedCoupon(null);
|
||
}
|
||
} catch (e) {
|
||
setCouponError('Network error');
|
||
setAppliedCoupon(null);
|
||
} finally {
|
||
setCouponLoading(false);
|
||
}
|
||
};
|
||
|
||
const startPayment = async (pkg: any) => {
|
||
setPaymentStep('processing');
|
||
try {
|
||
const token = getToken();
|
||
const headers: HeadersInit = {
|
||
'Content-Type': 'application/json',
|
||
...(token ? { 'Authorization': `Bearer ${token}` } : {})
|
||
};
|
||
const ac = appliedCoupon();
|
||
const body: any = {
|
||
package_id: pkg.id,
|
||
amount: ac ? ac.final_price_inr : pkg.price_paise,
|
||
};
|
||
if (ac) body.coupon_code = ac.code;
|
||
const res = await fetch(`${API}/payments/create-order`, {
|
||
method: 'POST',
|
||
headers,
|
||
credentials: 'include',
|
||
body: JSON.stringify(body),
|
||
});
|
||
const data = await res.json().catch(() => ({}));
|
||
if (!res.ok) {
|
||
setPaymentStep('error');
|
||
setCouponError('Payment initiation failed');
|
||
return;
|
||
}
|
||
setPaymentRef(data.order_id || `ORD-${Date.now()}`);
|
||
setTimeout(() => verifyPayment(pkg), 1200);
|
||
} catch (e) {
|
||
setPaymentRef(`ORD-MOCK-${Date.now()}`);
|
||
setTimeout(() => verifyPayment(pkg), 1200);
|
||
}
|
||
};
|
||
|
||
const verifyPayment = async (pkg: any) => {
|
||
setPaymentStep('verifying');
|
||
try {
|
||
const token = getToken();
|
||
const headers: HeadersInit = {
|
||
'Content-Type': 'application/json',
|
||
...(token ? { 'Authorization': `Bearer ${token}` } : {})
|
||
};
|
||
const res = await fetch(`${API}/payments/verify`, {
|
||
method: 'POST',
|
||
headers,
|
||
credentials: 'include',
|
||
body: JSON.stringify({ order_id: paymentRef(), payment_id: `PAY-${Date.now()}` }),
|
||
});
|
||
if (res.ok) {
|
||
setPaymentStep('success');
|
||
const creditsToAdd = Number(pkg.credits) + Number(pkg.bonus_credits || 0);
|
||
setLeadCredits((prev) => prev + creditsToAdd);
|
||
const ac = appliedCoupon();
|
||
const amountPaid = ac ? ac.final_price_inr : pkg.price_paise;
|
||
const newRow: [string, string, string, string, string, string] = [
|
||
paymentRef() || `#INV-${Date.now()}`,
|
||
pkg.display_name || pkg.name,
|
||
Number(creditsToAdd).toLocaleString(),
|
||
`₹${(amountPaid / 100).toLocaleString('en-IN')}`,
|
||
'Completed',
|
||
new Date().toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' }),
|
||
];
|
||
setTxRows((prev) => [newRow, ...prev]);
|
||
setAppliedCoupon(null);
|
||
setCouponCode('');
|
||
} else {
|
||
setPaymentStep('error');
|
||
}
|
||
} catch (e) {
|
||
setPaymentStep('error');
|
||
}
|
||
};
|
||
|
||
const adjustCreditsManually = (amount: number, reason: string) => {
|
||
setLeadCredits((prev) => prev + amount);
|
||
const newRow: [string, string, string, string, string, string] = [
|
||
`#ADJ-${Date.now().toString().slice(-6)}`,
|
||
`Adjustment: ${reason}`,
|
||
amount > 0 ? `+${amount.toLocaleString()}` : amount.toLocaleString(),
|
||
'₹0',
|
||
'Completed',
|
||
new Date().toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' }),
|
||
];
|
||
setTxRows((prev) => [newRow, ...prev]);
|
||
};
|
||
|
||
// 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 ?? ''),
|
||
amount: item.budget_min != null
|
||
? `₹${Math.round(item.budget_min / 100).toLocaleString('en-IN')}`
|
||
: '₹0',
|
||
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',
|
||
responses: Number(item.response_count ?? 0),
|
||
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 }));
|
||
});
|
||
createEffect(() => {
|
||
const d = userRolesResource();
|
||
if (Array.isArray(d)) setUserRoles(d);
|
||
else if (d && Array.isArray(d.data)) setUserRoles(d.data);
|
||
});
|
||
|
||
createEffect(() => {
|
||
const d = servicesResource();
|
||
if (!d) return;
|
||
const items = Array.isArray(d.items) ? d.items : Array.isArray(d) ? d : [];
|
||
if (items.length > 0) {
|
||
setPortfolioServices(items.map((it: any) => ({
|
||
id: it.id,
|
||
name: it.name || '',
|
||
model: it.model || 'Flat',
|
||
price: it.price || '',
|
||
details: it.details || ''
|
||
})));
|
||
}
|
||
});
|
||
|
||
createEffect(() => {
|
||
const d = portfolioResource();
|
||
if (!d) return;
|
||
if (Array.isArray(d.photos)) setPortfolioPhotos(d.photos);
|
||
if (Array.isArray(d.experience)) setPortfolioExperiences(d.experience);
|
||
if (Array.isArray(d.specialties)) setPortfolioSpecialties(d.specialties);
|
||
if (Array.isArray(d.languages)) setPortfolioLanguages(d.languages);
|
||
if (Array.isArray(d.service_areas)) setPortfolioServiceAreas(d.service_areas);
|
||
});
|
||
|
||
// ─── End live API integration ─────────────────────────────────────────────
|
||
|
||
const registerRole = async (roleKey: string) => {
|
||
const res = await apiPost('/api/users/roles/register', { role_key: roleKey.toUpperCase() });
|
||
if (res && (res.ok || res.status === 200)) {
|
||
window.location.reload();
|
||
} else {
|
||
alert(`Failed to register as ${roleKey}. Please check if you are already registered.`);
|
||
}
|
||
};
|
||
|
||
const switchRole = async (roleKey: string) => {
|
||
const res = await apiPost('/api/users/roles/switch', { role: roleKey.toUpperCase() });
|
||
if (res && (res.ok || res.status === 200)) {
|
||
window.location.reload();
|
||
} else {
|
||
alert(`Failed to switch to ${roleKey}.`);
|
||
}
|
||
};
|
||
|
||
const submitProfileForApproval = async () => {
|
||
if (!hasLive() || !livePrefix()) return;
|
||
try {
|
||
const res = await fetch(`${GW}/api/${livePrefix()}/profile/submit`, {
|
||
method: 'POST',
|
||
credentials: 'include',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
});
|
||
if (res.ok) {
|
||
setProfileApprovalState('SUBMITTED');
|
||
setTimeout(() => setProfileApprovalState('IN_REVIEW'), 250);
|
||
}
|
||
} catch {
|
||
setProfileApprovalState('SUBMITTED');
|
||
}
|
||
};
|
||
const submitPortfolioForApproval = () => {
|
||
setPortfolioApprovalState('SUBMITTED');
|
||
setTimeout(() => setPortfolioApprovalState('IN_REVIEW'), 250);
|
||
};
|
||
createEffect(() => {
|
||
const roleKey = normalizeRoleKey(props.roleKey || '');
|
||
const spec = portfolioSpecForRole(roleKey);
|
||
setPortfolioSpecialties(spec.specialties.slice(0, 6));
|
||
setPortfolioLanguages(['English', 'Hindi', 'Tamil']);
|
||
setPortfolioServiceAreas(['T. Nagar', 'Adyar', 'Velachery', 'Anna Nagar']);
|
||
setPortfolioSpecialtyInput('');
|
||
setPortfolioLanguageInput('');
|
||
setPortfolioAreaInput('');
|
||
setPortfolioFormValues({});
|
||
setPortfolioFormErrors({});
|
||
setPortfolioValidationNotice('');
|
||
setPortfolioServices([{ name: 'Core Service', model: 'Flat', price: '₹15,000', details: 'Includes planning and delivery' }]);
|
||
setPortfolioServiceDraft({ name: '', model: 'Flat', price: '', details: '' });
|
||
setPortfolioExperiences([{ year: '2022', title: 'Started professional practice', details: 'Handled 40+ projects successfully' }]);
|
||
setPortfolioExperienceDraft({ year: '', title: '', details: '' });
|
||
setPortfolioToolInput('');
|
||
setPortfolioDesignTools(['Figma', 'Adobe Photoshop', 'Illustrator']);
|
||
setPortfolioPhotos(['sample-1.jpg', 'sample-2.jpg']);
|
||
});
|
||
const addPortfolioTag = (
|
||
value: string,
|
||
setter: (next: (prev: string[]) => string[]) => void,
|
||
max = 6,
|
||
) => {
|
||
const normalized = String(value || '').trim();
|
||
if (!normalized) return false;
|
||
let added = false;
|
||
setter((prev) => {
|
||
if (prev.some((item) => item.toLowerCase() === normalized.toLowerCase()) || prev.length >= max) return prev;
|
||
added = true;
|
||
return [...prev, normalized];
|
||
});
|
||
return added;
|
||
};
|
||
const removePortfolioTag = (
|
||
value: string,
|
||
setter: (next: (prev: string[]) => string[]) => void,
|
||
) => setter((prev) => prev.filter((item) => item !== value));
|
||
const selectedTicket = createMemo(() => HELP_TICKET_ROWS.find((row) => row.id === activeTicketId()) || HELP_TICKET_ROWS[0]);
|
||
const selectedTicketDetails = createMemo(() => HELP_TICKET_DETAILS[selectedTicket().id] || HELP_TICKET_DETAILS['TCK-1042']);
|
||
const leadCostPerContact = 25;
|
||
const leadsPerPage = 3;
|
||
const requestedPerPage = 5;
|
||
const lockedLeadCredits = createMemo(() => leadCards().filter((card) => card.status === 'requested').length * leadCostPerContact);
|
||
const usableLeadCredits = createMemo(() => Math.max(0, leadCredits() - lockedLeadCredits()));
|
||
const filteredLeadCards = createMemo(() => {
|
||
const query = leadSearch().trim().toLowerCase();
|
||
const area = leadAreaFilter();
|
||
const budget = leadBudgetFilter();
|
||
const date = leadDateFilter();
|
||
const list = leadCards().filter((lead) => {
|
||
if (lead.status !== 'open') return false;
|
||
const matchesQuery = !query
|
||
|| lead.title.toLowerCase().includes(query)
|
||
|| lead.category.toLowerCase().includes(query)
|
||
|| lead.location.toLowerCase().includes(query)
|
||
|| String(lead.area || '').toLowerCase().includes(query);
|
||
if (!matchesQuery) return false;
|
||
if (area !== 'All Areas' && String(lead.area || '') !== area) return false;
|
||
if (date === 'Within 7 Days') {
|
||
const diff = (Date.parse(lead.dateRequired) - Date.now()) / (1000 * 60 * 60 * 24);
|
||
if (!(diff >= 0 && diff <= 7)) return false;
|
||
}
|
||
if (date === 'This Month') {
|
||
const value = new Date(lead.dateRequired);
|
||
const now = new Date();
|
||
if (!(value.getMonth() === now.getMonth() && value.getFullYear() === now.getFullYear())) return false;
|
||
}
|
||
if (budget === 'Under ₹1L' && lead.budgetValue >= 100000) return false;
|
||
if (budget === '₹1L - ₹2L' && !(lead.budgetValue >= 100000 && lead.budgetValue <= 200000)) return false;
|
||
if (budget === 'Above ₹2L' && lead.budgetValue <= 200000) return false;
|
||
return true;
|
||
});
|
||
const sorted = [...list];
|
||
if (leadSortFilter() === 'Budget High-Low') sorted.sort((a, b) => b.budgetValue - a.budgetValue);
|
||
if (leadSortFilter() === 'Budget Low-High') sorted.sort((a, b) => a.budgetValue - b.budgetValue);
|
||
if (leadSortFilter() === 'Newest First') sorted.sort((a, b) => Date.parse(b.dateRequired) - Date.parse(a.dateRequired));
|
||
return sorted;
|
||
});
|
||
const totalLeadPages = createMemo(() => Math.max(1, Math.ceil(filteredLeadCards().length / leadsPerPage)));
|
||
const pagedLeadCards = createMemo(() => {
|
||
const start = (leadPage() - 1) * leadsPerPage;
|
||
return filteredLeadCards().slice(start, start + leadsPerPage);
|
||
});
|
||
const filteredRequestedRows = createMemo(() => {
|
||
const query = requestedSearch().trim().toLowerCase();
|
||
const status = requestedStatusFilter();
|
||
const sorted = leadRequestRows().filter((row) => {
|
||
if (status !== 'All Status' && status !== titleCase(row.status.replace(/_/g, ' '))) return false;
|
||
if (!query) return true;
|
||
return row.id.toLowerCase().includes(query) || row.title.toLowerCase().includes(query) || row.city.toLowerCase().includes(query);
|
||
});
|
||
if (requestedSortFilter() === 'Oldest First') {
|
||
sorted.sort((a, b) => Date.parse(a.requestDate) - Date.parse(b.requestDate));
|
||
} else {
|
||
sorted.sort((a, b) => Date.parse(b.requestDate) - Date.parse(a.requestDate));
|
||
}
|
||
return sorted;
|
||
});
|
||
const totalRequestedPages = createMemo(() => Math.max(1, Math.ceil(filteredRequestedRows().length / requestedPerPage)));
|
||
const pagedRequestedRows = createMemo(() => {
|
||
const start = (requestedPage() - 1) * requestedPerPage;
|
||
return filteredRequestedRows().slice(start, start + requestedPerPage);
|
||
});
|
||
const ticketSummary = createMemo(() => {
|
||
const openCount = HELP_TICKET_ROWS.filter((row) => row.status !== 'Resolved').length;
|
||
const resolvedCount = HELP_TICKET_ROWS.length - openCount;
|
||
return { openCount, resolvedCount, total: HELP_TICKET_ROWS.length };
|
||
});
|
||
const openTicketDetails = (ticketId: string) => {
|
||
setHelpCenterTab('my_tickets');
|
||
setActiveTicketId(ticketId);
|
||
setMyTicketsTab('view_ticket');
|
||
setTicketMessage('');
|
||
setViewTicketFiles([]);
|
||
};
|
||
|
||
|
||
createEffect(() => {
|
||
const key = customerKey();
|
||
if (key !== lastSidebarKey()) {
|
||
setLastSidebarKey(key);
|
||
if (key === 'credits') props.onTabSelect('overview');
|
||
}
|
||
});
|
||
|
||
createEffect(() => {
|
||
setDashboardWidgetOrder(previewWidgets());
|
||
});
|
||
|
||
createEffect(() => {
|
||
if (customerKey() !== 'help center' && customerKey() !== 'support') setHelpCenterTab('help_center');
|
||
});
|
||
|
||
createEffect(() => {
|
||
if (customerKey() !== 'my requirements') {
|
||
setRequirementsView('list');
|
||
setRequirementsStep(1);
|
||
}
|
||
});
|
||
|
||
createEffect(() => {
|
||
if (customerKey() !== 'leads') {
|
||
setLeadMarketplaceTab('All Leads');
|
||
setLeadSearch('');
|
||
setLeadAreaFilter('All Areas');
|
||
setLeadBudgetFilter('All Budgets');
|
||
setLeadDateFilter('Any Date');
|
||
setLeadSortFilter('Newest First');
|
||
setLeadFiltersOpen(false);
|
||
setLeadSortOpen(false);
|
||
setLeadPage(1);
|
||
setActiveLeadDetailId('');
|
||
setLeadContactConfirmId('');
|
||
setActiveResponseLeadId('');
|
||
setResponsesDetailMode(false);
|
||
setRequestedSearch('');
|
||
setRequestedStatusFilter('All Status');
|
||
setRequestedSortFilter('Newest First');
|
||
setRequestedSortOpen(false);
|
||
setRequestedFilterOpen(false);
|
||
setRequestedPage(1);
|
||
}
|
||
});
|
||
|
||
createEffect(() => {
|
||
void leadSearch();
|
||
void leadAreaFilter();
|
||
void leadBudgetFilter();
|
||
void leadDateFilter();
|
||
void leadSortFilter();
|
||
setLeadPage(1);
|
||
});
|
||
|
||
createEffect(() => {
|
||
if (leadPage() > totalLeadPages()) setLeadPage(totalLeadPages());
|
||
});
|
||
|
||
createEffect(() => {
|
||
void requestedSearch();
|
||
void requestedStatusFilter();
|
||
void requestedSortFilter();
|
||
setRequestedPage(1);
|
||
});
|
||
|
||
createEffect(() => {
|
||
if (requestedPage() > totalRequestedPages()) setRequestedPage(totalRequestedPages());
|
||
});
|
||
|
||
createEffect(() => {
|
||
if (customerKey() !== 'jobs') {
|
||
setJobPostView('form');
|
||
setJobPostStep(1);
|
||
setJobSeekerScreen('list');
|
||
setJobSeekerSelectedId(jobBoardJobs()[0]?.id || '');
|
||
setJobSeekerApplyStep(2);
|
||
setLastJobSeekerTabKey('');
|
||
}
|
||
});
|
||
|
||
createEffect(() => {
|
||
if (customerKey() !== 'my portfolio') setPortfolioEditMode(false);
|
||
if (customerKey() !== 'my portfolio') setPortfolioTopTab('my_portfolio');
|
||
});
|
||
|
||
const openPortfolioPreviewInline = () => {
|
||
setPortfolioTopTab('preview');
|
||
props.onSidebarSelect('My Portfolio');
|
||
props.onTabSelect('about');
|
||
};
|
||
|
||
createEffect(() => {
|
||
if (customerKey() !== 'jobs' || !isJobSeekerRole()) return;
|
||
const tabKey = normalizeTabKey(resolvedTabKey());
|
||
if (tabKey === lastJobSeekerTabKey()) return;
|
||
setLastJobSeekerTabKey(tabKey);
|
||
setJobSeekerScreen('list');
|
||
setJobSeekerSelectedId(jobBoardJobs()[0]?.id || '');
|
||
});
|
||
|
||
createEffect(() => {
|
||
const rows = jobBoardJobs();
|
||
const active = jobSeekerSelectedId();
|
||
if (!rows.length) return;
|
||
if (!rows.some((job) => job.id === active)) {
|
||
setJobSeekerSelectedId(rows[0].id);
|
||
}
|
||
});
|
||
|
||
createEffect(() => {
|
||
if (helpCenterTab() !== 'my_tickets') {
|
||
setMyTicketsTab('all_tickets');
|
||
setActiveTicketId('TCK-1042');
|
||
setViewTicketFiles([]);
|
||
}
|
||
});
|
||
|
||
createEffect(() => {
|
||
if (helpCenterTab() !== 'create_ticket') setCreateTicketFiles([]);
|
||
});
|
||
|
||
const moveDashboardWidget = (movingKey: string, targetKey: string) => {
|
||
if (!movingKey || !targetKey || movingKey === targetKey) return;
|
||
setDashboardWidgetOrder((prev) => {
|
||
const from = prev.indexOf(movingKey);
|
||
const to = prev.indexOf(targetKey);
|
||
if (from === -1 || to === -1) return prev;
|
||
const next = [...prev];
|
||
next.splice(from, 1);
|
||
next.splice(to, 0, movingKey);
|
||
return next;
|
||
});
|
||
};
|
||
|
||
const statusChip = (value: string) => {
|
||
const key = value.toLowerCase();
|
||
if (key === 'active' || key === 'new') return { bg: '#FFF1EB', c: '#FF5E13' };
|
||
if (key === 'closed' || key === 'contacted') return { bg: '#ECFDF3', c: '#059669' };
|
||
if (key === 'shortlisted') return { bg: '#EEF2FF', c: '#4338CA' };
|
||
if (key === 'draft') return { bg: '#FEF3C7', c: '#B45309' };
|
||
return { bg: '#F3F4F6', c: '#6B7280' };
|
||
};
|
||
|
||
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;
|
||
changed = true;
|
||
const nextCount = Math.min(card.maxContacts, card.contactCount + 1);
|
||
return {
|
||
...card,
|
||
contactCount: nextCount,
|
||
status: (nextCount >= card.maxContacts ? 'closed' : 'requested') as LeadCardStatus,
|
||
};
|
||
}));
|
||
if (!changed) return;
|
||
setLeadRequestRows((prev) => {
|
||
const idx = prev.findIndex((row) => row.id === leadId);
|
||
if (idx >= 0) {
|
||
const next = [...prev];
|
||
next[idx] = { ...next[idx], status: 'request_sent', decisionDate: '--' };
|
||
return next;
|
||
}
|
||
const selected = leadCards().find((card) => card.id === leadId);
|
||
return [
|
||
{
|
||
id: leadId,
|
||
title: selected?.title || 'Lead Request',
|
||
city: selected ? `${selected.area || 'Central'}, ${selected.location}` : 'Chennai, India',
|
||
requestDate: 'Apr 02, 2026',
|
||
status: 'request_sent' as const,
|
||
decisionDate: '--',
|
||
},
|
||
...prev,
|
||
];
|
||
});
|
||
};
|
||
|
||
const leadDetailsSpec = (lead: { category: string }) => {
|
||
const c = lead.category.toLowerCase();
|
||
if (c.includes('photography') || c.includes('fashion')) {
|
||
return { timeframe: '3-5 shoot days + 7 days edit', scope: 'Pre-shoot planning, full day coverage, edited album delivery', highlights: ['Shot list alignment with customer brief', 'Venue and lighting plan finalization', 'Edited photos + social cuts'] };
|
||
}
|
||
if (c.includes('design') || c.includes('branding')) {
|
||
return { timeframe: '7-14 working days', scope: 'Brand exploration, visual concepts, revision rounds, handoff', highlights: ['Moodboard and style direction', 'Logo/system deliverables', 'Source files + usage formats'] };
|
||
}
|
||
if (c.includes('video')) {
|
||
return { timeframe: '5-10 working days', scope: 'Editing, color correction, sound polish, export variants', highlights: ['Storyboard-based edits', 'Platform-specific outputs', 'Two revision rounds'] };
|
||
}
|
||
return { timeframe: '5-10 working days', scope: 'Requirement discovery, execution plan, and final delivery', highlights: ['Clear milestone tracking', 'Weekly progress updates', 'Final quality checklist'] };
|
||
};
|
||
|
||
const openLeadDetailsInNewTab = (leadId: string) => {
|
||
setActiveLeadDetailId(leadId);
|
||
setLeadMarketplaceTab('View Details');
|
||
};
|
||
const openResponseLeadDetails = (leadId: string) => {
|
||
setActiveResponseLeadId(leadId);
|
||
setResponsesDetailMode(true);
|
||
};
|
||
const openLeadContactConfirm = (leadId: string) => setLeadContactConfirmId(leadId);
|
||
const confirmLeadContactRequest = () => {
|
||
const leadId = leadContactConfirmId();
|
||
if (!leadId) return;
|
||
requestLeadContact(leadId);
|
||
setLeadContactConfirmId('');
|
||
};
|
||
|
||
const approveLeadContact = (leadId: string) => {
|
||
setLeadCards((prev) => prev.map((card) => card.id === leadId && card.status === 'requested'
|
||
? { ...card, status: 'unlocked' as const }
|
||
: card));
|
||
setLeadCredits((prev) => Math.max(0, prev - leadCostPerContact));
|
||
setLeadRequestRows((prev) => prev.map((row) => row.id === leadId
|
||
? { ...row, status: 'contact_unlocked' as const, decisionDate: 'Apr 03, 2026' }
|
||
: row));
|
||
};
|
||
|
||
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;
|
||
return { ...card, contactCount: Math.max(0, card.contactCount - 1), status: 'open' as LeadCardStatus };
|
||
}));
|
||
setLeadCredits((prev) => Math.max(0, prev - leadCostPerContact));
|
||
setLeadRequestRows((prev) => prev.map((row) => row.id === leadId
|
||
? { ...row, status: 'cancelled_by_professional' as const, decisionDate: 'Apr 03, 2026' }
|
||
: row));
|
||
};
|
||
|
||
const refundPendingLead = (leadId: string) => {
|
||
setLeadCards((prev) => prev.map((card) => {
|
||
if (card.id !== leadId) return card;
|
||
if (card.status !== 'requested' && card.status !== 'closed') return card;
|
||
return { ...card, contactCount: Math.max(0, card.contactCount - 1), status: 'open' as LeadCardStatus };
|
||
}));
|
||
setLeadRequestRows((prev) => prev.map((row) => row.id === leadId
|
||
? { ...row, status: 'expired_refunded' as const, decisionDate: 'Auto-refunded' }
|
||
: row));
|
||
};
|
||
|
||
const leadMatchPercent = (lead: { match: string }) => {
|
||
const value = Number.parseInt(String(lead.match || '').replace(/[^0-9]/g, ''), 10);
|
||
return Number.isFinite(value) ? value : 70;
|
||
};
|
||
|
||
const leadProbability = (lead: { match: string; urgency: string; contactCount: number; maxContacts: number }) => {
|
||
const urgencyBoost = lead.urgency.includes('High') ? 8 : lead.urgency.includes('Medium') ? 4 : 0;
|
||
const slotFactor = ((lead.maxContacts - lead.contactCount) / Math.max(1, lead.maxContacts)) * 40;
|
||
const score = Math.round(0.5 * leadMatchPercent(lead) + slotFactor + urgencyBoost);
|
||
return Math.min(95, Math.max(5, score));
|
||
};
|
||
const leadProbabilityColor = (score: number) => {
|
||
if (score >= 75) return '#16A34A';
|
||
if (score >= 50) return '#F59E0B';
|
||
return '#DC2626';
|
||
};
|
||
const leadProbabilityLabel = (score: number) => {
|
||
if (score >= 75) return 'High';
|
||
if (score >= 50) return 'Medium';
|
||
return 'Low';
|
||
};
|
||
const leadGaugeNeedlePoint = (score: number) => {
|
||
const clamped = Math.max(0, Math.min(100, score));
|
||
const angleDeg = 180 - (clamped * 180) / 100;
|
||
const angleRad = (angleDeg * Math.PI) / 180;
|
||
const radius = 34;
|
||
return {
|
||
x: 60 + Math.cos(angleRad) * radius,
|
||
y: 60 - Math.sin(angleRad) * radius,
|
||
};
|
||
};
|
||
const leadGaugeDash = (score: number) => {
|
||
const clamped = Math.max(0, Math.min(100, score));
|
||
const arcLength = Math.PI * 50;
|
||
return `${((clamped / 100) * arcLength).toFixed(1)} ${arcLength.toFixed(1)}`;
|
||
};
|
||
|
||
const renderPortfolioContent = () => {
|
||
const spec = portfolioSpecForRole(props.roleKey || '');
|
||
const mediaConfig = portfolioMediaConfig(props.roleKey || '');
|
||
const submissionTabs = spec.tabs.filter((item) => normalizeTabKey(item) !== normalizeTabKey('testimonials'));
|
||
const selectedPortfolioTab = submissionTabs.find((item) => normalizeTabKey(item) === normalizeTabKey(resolvedTabKey())) || submissionTabs[0] || 'about';
|
||
const selectedPortfolioTabKey = normalizeTabKey(selectedPortfolioTab);
|
||
const serviceTabKey = normalizeTabKey(spec.serviceTabLabel);
|
||
const galleryTabKey = normalizeTabKey(spec.galleryTabLabel);
|
||
const experienceTabKey = normalizeTabKey(spec.experienceTabLabel);
|
||
const testimonialsTabKey = normalizeTabKey('testimonials');
|
||
const faqsTabKey = normalizeTabKey('faqs');
|
||
const portfolioJobsCompleted = portfolioJobsCompletedPreview;
|
||
const portfolioFeedbackCount = portfolioFeedbackCountPreview;
|
||
const testimonialsUnlocked = portfolioTestimonialsUnlocked();
|
||
const isPreviewMode = portfolioTopTab() === 'preview';
|
||
const canEdit = !isPreviewMode;
|
||
const portfolioStepKeys = submissionTabs.map((item) => normalizeTabKey(item));
|
||
const activePortfolioStepIndex = Math.max(0, portfolioStepKeys.findIndex((key) => key === selectedPortfolioTabKey));
|
||
const goToPortfolioStep = (index: number) => {
|
||
const bounded = Math.max(0, Math.min(portfolioStepKeys.length - 1, index));
|
||
props.onTabSelect(submissionTabs[bounded] || submissionTabs[0] || 'about');
|
||
};
|
||
const portfolioTools = (() => {
|
||
const role = normalizeRoleKey(props.roleKey || '');
|
||
if (role === 'GRAPHIC_DESIGNER') return ['Figma', 'Adobe Photoshop', 'Illustrator', 'InDesign', 'After Effects'];
|
||
if (role === 'SOCIAL_MEDIA_MANAGER') return ['Meta Business Suite', 'Canva', 'Buffer', 'Hootsuite', 'Google Analytics'];
|
||
if (role === 'DEVELOPER') return ['React', 'TypeScript', 'Node.js', 'PostgreSQL', 'Docker'];
|
||
if (role === 'VIDEO_EDITOR') return ['Premiere Pro', 'After Effects', 'DaVinci Resolve', 'CapCut', 'Audition'];
|
||
if (role === 'PHOTOGRAPHER') return ['Lightroom', 'Photoshop', 'Capture One', 'Bridge', 'Snapseed'];
|
||
return ['Domain Tool 1', 'Domain Tool 2', 'Domain Tool 3'];
|
||
})();
|
||
const portfolioFormFieldsByTab: Record<string, string[]> = {
|
||
about: ['Professional Headline', 'About You', 'Area', 'Place', 'Travel Preference', 'Response Time'],
|
||
[serviceTabKey]: ['Primary Service', 'Pricing Model', 'Starting Price', 'Delivery Timeline', 'Includes', 'Additional Notes'],
|
||
[galleryTabKey]: ['Portfolio Item Title', 'Portfolio Link', 'Category', 'Project Summary', 'Outcome', 'Asset Upload'],
|
||
[experienceTabKey]: ['Years of Experience', 'Top Tools', 'Milestone 1', 'Milestone 2', 'Certifications', 'Working Style'],
|
||
[testimonialsTabKey]: ['Client Name', 'Client Feedback', 'Rating', 'Project Type', 'Client Location', 'Consent'],
|
||
[faqsTabKey]: ['Question 1', 'Answer 1', 'Question 2', 'Answer 2', 'Question 3', 'Answer 3'],
|
||
};
|
||
const selectedPortfolioFormFields = portfolioFormFieldsByTab[selectedPortfolioTabKey] || portfolioFormFieldsByTab.about;
|
||
const setPortfolioFieldValue = (field: string, value: string) => {
|
||
const fieldKey = `${selectedPortfolioTabKey}::${field}`;
|
||
setPortfolioFormValues((prev) => ({ ...prev, [fieldKey]: value }));
|
||
setPortfolioFormErrors((prev) => ({ ...prev, [fieldKey]: '' }));
|
||
setPortfolioValidationNotice('');
|
||
};
|
||
const renderPortfolioFormField = (field: string) => {
|
||
const key = String(field || '').toLowerCase();
|
||
const isSelect = /model|category|style|rating|consent|response time|travel|type|timeline/i.test(key);
|
||
const isLong = /about|summary|notes|answer|feedback|includes/i.test(key);
|
||
const fieldKey = `${selectedPortfolioTabKey}::${field}`;
|
||
const fieldValue = portfolioFormValues()[fieldKey] || '';
|
||
const fieldError = portfolioFormErrors()[fieldKey] || '';
|
||
const placeholder = (() => {
|
||
if (key.includes('area')) return 'Enter area in Chennai';
|
||
if (key.includes('place')) return 'Enter place in Chennai';
|
||
if (key.includes('price')) return 'Enter amount';
|
||
if (key.includes('link')) return 'Paste URL';
|
||
if (key.includes('upload')) return 'Upload file (PDF/JPG/PNG)';
|
||
return `${isSelect ? 'Select' : 'Enter'} ${field.toLowerCase()}`;
|
||
})();
|
||
return (
|
||
<div style="display:flex;flex-direction:column;gap:6px">
|
||
<p style="margin:0;font-size:11px;font-weight:700;color:#6B7280;letter-spacing:0.01em;text-transform:none">{field}</p>
|
||
<Show
|
||
when={isLong}
|
||
fallback={
|
||
<div style="position:relative">
|
||
<input type="text" value={fieldValue} onInput={(e) => setPortfolioFieldValue(field, e.currentTarget.value)} placeholder={placeholder} style={`width:100%;height:36px;border:1px solid ${fieldError ? '#FFD8C2' : '#E5E7EB'};border-radius:8px;background:white;padding:0 30px 0 10px;font-size:12px;color:#111827;outline:none`} />
|
||
<Show when={isSelect}>
|
||
<span style="position:absolute;right:10px;top:50%;transform:translateY(-50%);color:#9CA3AF;font-size:12px">▾</span>
|
||
</Show>
|
||
</div>
|
||
}
|
||
>
|
||
<textarea value={fieldValue} onInput={(e) => setPortfolioFieldValue(field, e.currentTarget.value)} placeholder={placeholder} style={`width:100%;min-height:74px;border:1px solid ${fieldError ? '#FFD8C2' : '#E5E7EB'};border-radius:8px;background:white;padding:8px 10px;font-size:12px;color:#111827;outline:none;resize:vertical`} />
|
||
</Show>
|
||
<Show when={!!fieldError}>
|
||
<p style="margin:0;font-size:11px;color:#C2410C">{fieldError}</p>
|
||
</Show>
|
||
</div>
|
||
);
|
||
};
|
||
const validatePortfolioStep = (stepKey: string) => {
|
||
const nextErrors: Record<string, string> = {};
|
||
const stepFields = portfolioFormFieldsByTab[stepKey] || [];
|
||
stepFields.forEach((field) => {
|
||
const fKey = `${stepKey}::${field}`;
|
||
const value = (portfolioFormValues()[fKey] || '').trim();
|
||
const optional = /optional/i.test(field);
|
||
const isUpload = /asset upload/i.test(field);
|
||
if (!optional && !isUpload && !value) nextErrors[fKey] = 'This field is required';
|
||
});
|
||
if (stepKey === serviceTabKey && portfolioServices().length < 1) {
|
||
setPortfolioValidationNotice('Add at least one service with pricing.');
|
||
} else if (stepKey === galleryTabKey && (portfolioPhotos().length < 1 || portfolioPhotos().length > 6)) {
|
||
setPortfolioValidationNotice('Add 1 to 6 portfolio photos.');
|
||
} else if (stepKey === experienceTabKey && portfolioExperiences().length < 1) {
|
||
setPortfolioValidationNotice('Add at least one experience entry.');
|
||
} else if (Object.keys(nextErrors).length) {
|
||
setPortfolioValidationNotice('Please fill all required fields in this tab.');
|
||
} else {
|
||
setPortfolioValidationNotice('');
|
||
}
|
||
setPortfolioFormErrors((prev) => ({ ...prev, ...nextErrors }));
|
||
return Object.keys(nextErrors).length === 0
|
||
&& !(stepKey === serviceTabKey && portfolioServices().length < 1)
|
||
&& !(stepKey === galleryTabKey && (portfolioPhotos().length < 1 || portfolioPhotos().length > 6))
|
||
&& !(stepKey === experienceTabKey && portfolioExperiences().length < 1);
|
||
};
|
||
const addServiceDraft = async () => {
|
||
const draft = portfolioServiceDraft();
|
||
if (!draft.name.trim() || !draft.price.trim()) {
|
||
setPortfolioValidationNotice('Add service name and price to continue.');
|
||
return;
|
||
}
|
||
|
||
if (hasLive()) {
|
||
try {
|
||
const res = await apiPost(`/api/${livePrefix()}/services`, {
|
||
name: draft.name.trim(),
|
||
model: draft.model.trim() || 'Flat',
|
||
price: draft.price.trim(),
|
||
details: draft.details.trim(),
|
||
});
|
||
if (res && (res.ok || res.status === 200)) {
|
||
refetchServicesLive();
|
||
setPortfolioServiceDraft({ name: '', model: 'Flat', price: '', details: '' });
|
||
setPortfolioValidationNotice('');
|
||
} else {
|
||
setPortfolioValidationNotice('Failed to save service to backend.');
|
||
}
|
||
} catch (err) {
|
||
console.error(err);
|
||
setPortfolioValidationNotice('Network error while saving service.');
|
||
}
|
||
} else {
|
||
setPortfolioServices((prev) => [...prev, {
|
||
name: draft.name.trim(),
|
||
model: draft.model.trim() || 'Flat',
|
||
price: draft.price.trim(),
|
||
details: draft.details.trim(),
|
||
}]);
|
||
setPortfolioServiceDraft({ name: '', model: 'Flat', price: '', details: '' });
|
||
setPortfolioValidationNotice('');
|
||
}
|
||
};
|
||
const removeServiceItem = async (index: number) => {
|
||
const target = portfolioServices()[index];
|
||
if (hasLive() && target && (target as any).id) {
|
||
try {
|
||
const res = await apiDelete(`/api/${livePrefix()}/services/${(target as any).id}`);
|
||
if (res && (res.ok || res.status === 200)) {
|
||
refetchServicesLive();
|
||
}
|
||
} catch (err) {
|
||
console.error('Failed to delete service:', err);
|
||
}
|
||
} else {
|
||
setPortfolioServices((prev) => prev.filter((_, i) => i !== index));
|
||
}
|
||
};
|
||
const addExperienceDraft = async () => {
|
||
const draft = portfolioExperienceDraft();
|
||
if (!draft.year.trim() || !draft.title.trim()) {
|
||
setPortfolioValidationNotice('Add year and title for each experience entry.');
|
||
return;
|
||
}
|
||
|
||
const nextExp = [...portfolioExperiences(), {
|
||
year: draft.year.trim(),
|
||
title: draft.title.trim(),
|
||
details: draft.details.trim(),
|
||
}];
|
||
|
||
if (hasLive()) {
|
||
try {
|
||
const res = await apiPost(`/api/${livePrefix()}/portfolio/experience`, nextExp);
|
||
if (res && (res.ok || res.status === 200)) {
|
||
refetchPortfolioLive();
|
||
setPortfolioExperienceDraft({ year: '', title: '', details: '' });
|
||
setPortfolioValidationNotice('');
|
||
}
|
||
} catch (err) {
|
||
console.error(err);
|
||
}
|
||
} else {
|
||
setPortfolioExperiences(nextExp);
|
||
setPortfolioExperienceDraft({ year: '', title: '', details: '' });
|
||
setPortfolioValidationNotice('');
|
||
}
|
||
};
|
||
const removeExperienceItem = async (index: number) => {
|
||
const nextExp = portfolioExperiences().filter((_, i) => i !== index);
|
||
if (hasLive()) {
|
||
try {
|
||
const res = await apiPost(`/api/${livePrefix()}/portfolio/experience`, nextExp);
|
||
if (res && (res.ok || res.status === 200)) refetchPortfolioLive();
|
||
} catch (err) {
|
||
console.error(err);
|
||
}
|
||
} else {
|
||
setPortfolioExperiences(nextExp);
|
||
}
|
||
};
|
||
const addPhotoItem = async () => {
|
||
const current = portfolioPhotos();
|
||
if (current.length >= 6) {
|
||
setPortfolioValidationNotice('Portfolio is limited to 6 photos.');
|
||
return;
|
||
}
|
||
const nextLabel = `portfolio-${current.length + 1}.jpg`;
|
||
const nextPhotos = [...current, nextLabel];
|
||
|
||
if (hasLive()) {
|
||
try {
|
||
const res = await apiPost(`/api/${livePrefix()}/portfolio/photos`, nextPhotos);
|
||
if (res && (res.ok || res.status === 200)) refetchPortfolioLive();
|
||
} catch (err) {
|
||
console.error(err);
|
||
}
|
||
} else {
|
||
setPortfolioPhotos(nextPhotos);
|
||
}
|
||
setPortfolioValidationNotice('');
|
||
};
|
||
const removePhotoItem = async (index: number) => {
|
||
const nextPhotos = portfolioPhotos().filter((_, i) => i !== index);
|
||
if (hasLive()) {
|
||
try {
|
||
const res = await apiPost(`/api/${livePrefix()}/portfolio/photos`, nextPhotos);
|
||
if (res && (res.ok || res.status === 200)) refetchPortfolioLive();
|
||
} catch (err) {
|
||
console.error(err);
|
||
}
|
||
} else {
|
||
setPortfolioPhotos(nextPhotos);
|
||
}
|
||
};
|
||
const showSection = (tabKey: string) => (isPreviewMode ? portfolioStepKeys.includes(tabKey) : selectedPortfolioTabKey === tabKey);
|
||
|
||
return (
|
||
<div style="display:flex;flex-direction:column;gap:14px">
|
||
<div style="border-radius:12px;border:1px solid #E5E7EB;background:white;padding:0 12px;box-shadow:0 1px 3px rgba(0,0,0,0.04)">
|
||
<div style="display:flex;align-items:center;gap:20px;border-bottom:1px solid #E5E7EB;padding-top:10px">
|
||
<button
|
||
type="button"
|
||
onClick={() => setPortfolioTopTab('my_portfolio')}
|
||
style={`padding:0 0 10px;font-size:13px;font-weight:500;background:none;border:none;white-space:nowrap;cursor:pointer;color:${portfolioTopTab() === 'my_portfolio' ? '#FF5E13' : '#6B7280'};border-bottom:${portfolioTopTab() === 'my_portfolio' ? '2px solid #FF5E13' : '2px solid transparent'};margin-bottom:-1px`}
|
||
>
|
||
My Portfolio
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => setPortfolioTopTab('preview')}
|
||
style={`padding:0 0 10px;font-size:13px;font-weight:500;background:none;border:none;white-space:nowrap;cursor:pointer;color:${portfolioTopTab() === 'preview' ? '#FF5E13' : '#6B7280'};border-bottom:${portfolioTopTab() === 'preview' ? '2px solid #FF5E13' : '2px solid transparent'};margin-bottom:-1px`}
|
||
>
|
||
Preview
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Profile Header */}
|
||
<div style="border-radius:14px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.06);overflow:hidden">
|
||
<div style="height:3px;background:linear-gradient(90deg,#FF5E13 0%,#FF8A4C 55%,#FFD0B5 100%)" />
|
||
<Show when={isPreviewMode}>
|
||
<div style="padding:16px 20px;border-bottom:1px solid #E5E7EB;display:flex;align-items:flex-start;justify-content:space-between;gap:12px">
|
||
<div style="display:grid;gap:8px">
|
||
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">
|
||
<p style="margin:0;font-size:18px;font-weight:800;color:#111827">Alex Morgan</p>
|
||
<span style="font-size:11px;font-weight:700;color:#374151;background:#F3F4F6;border:1px solid #E5E7EB;border-radius:999px;padding:2px 9px">Verified</span>
|
||
<span style="font-size:11px;font-weight:700;color:#C2410C;background:#FFF1EB;border:1px solid #FFD8C2;border-radius:999px;padding:2px 9px">Top Response Rate</span>
|
||
</div>
|
||
<p style="margin:0;font-size:13px;color:#4B5563">{spec.roleLabel}</p>
|
||
<div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap">
|
||
<span style="height:24px;padding:0 10px;border-radius:999px;background:#FFF1EB;color:#C2410C;font-size:11px;font-weight:700;display:inline-flex;align-items:center">Area: South Chennai</span>
|
||
<span style="height:24px;padding:0 10px;border-radius:999px;background:#FFF1EB;color:#C2410C;font-size:11px;font-weight:700;display:inline-flex;align-items:center">Place: T. Nagar, Chennai</span>
|
||
<span style="height:24px;padding:0 10px;border-radius:999px;border:1px solid #E5E7EB;background:#F9FAFB;color:#374151;font-size:11px;font-weight:700;display:inline-flex;align-items:center">Travel: T. Nagar, Adyar, Velachery, Anna Nagar</span>
|
||
</div>
|
||
</div>
|
||
<div style="display:flex;gap:8px;flex-shrink:0;align-self:center" />
|
||
</div>
|
||
</Show>
|
||
<div style="padding:0 20px;border-bottom:1px solid #E5E7EB;background:#FFFFFF">
|
||
<div style="display:flex;align-items:center;gap:20px;overflow-x:auto;padding-top:10px">
|
||
<For each={[...submissionTabs, 'preview']}>
|
||
{(item) => {
|
||
const itemKey = normalizeTabKey(item);
|
||
const isPreviewStep = itemKey === 'preview';
|
||
const isLockedTestimonialsTab = itemKey === 'testimonials' && !testimonialsUnlocked;
|
||
const isActive = isPreviewStep ? isPreviewMode : (!isPreviewMode && selectedPortfolioTabKey === itemKey);
|
||
return (
|
||
<button
|
||
type="button"
|
||
disabled={isLockedTestimonialsTab}
|
||
onClick={() => {
|
||
if (isLockedTestimonialsTab) return;
|
||
if (isPreviewStep) {
|
||
setPortfolioTopTab('preview');
|
||
return;
|
||
}
|
||
setPortfolioTopTab('my_portfolio');
|
||
props.onTabSelect(item);
|
||
}}
|
||
title={isLockedTestimonialsTab ? 'Unlock after 3 completed jobs and 2 customer feedback entries' : (isPreviewStep ? 'Final preview before submit' : '')}
|
||
style={`padding:0 0 10px;font-size:13px;font-weight:500;background:none;border:none;white-space:nowrap;cursor:${isLockedTestimonialsTab ? 'not-allowed' : 'pointer'};opacity:${isLockedTestimonialsTab ? 0.5 : 1};color:${isActive ? '#FF5E13' : '#6B7280'};border-bottom:${isActive ? '2px solid #FF5E13' : '2px solid transparent'};margin-bottom:-1px;flex-shrink:0`}
|
||
>
|
||
{titleCase(item)}{isLockedTestimonialsTab ? ' • Locked' : ''}
|
||
</button>
|
||
);
|
||
}}
|
||
</For>
|
||
</div>
|
||
</div>
|
||
<Show when={isPreviewMode}>
|
||
<div style="padding:14px 16px;display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px">
|
||
{spec.statsLabels.slice(0, 4).map((label, i) => (
|
||
<div style="border:1px solid #E5E7EB;border-radius:12px;background:#FAFBFD;padding:12px 12px">
|
||
<p style="margin:0;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:0.05em;color:#9CA3AF">{label}</p>
|
||
<p style="margin:5px 0 0;font-size:14px;font-weight:800;color:#111827">{i===0?'248':i===1?'7+':i===2?'Verified':i===3?'2 days':'Yes'}</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</Show>
|
||
</div>
|
||
|
||
<Show when={!isPreviewMode}>
|
||
<div style="display:grid;grid-template-columns:2fr 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)">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;gap:10px">
|
||
<p style="margin:0;font-size:13px;font-weight:700;color:#111827">{titleCase(selectedPortfolioTab)}</p>
|
||
<span style="height:22px;padding:0 8px;border-radius:999px;border:1px solid #DDEBFF;background:#EEF4FF;display:inline-flex;align-items:center;font-size:10px;font-weight:700;color:#03004E">
|
||
{selectedPortfolioFormFields.length} fields
|
||
</span>
|
||
</div>
|
||
<Show when={!!portfolioValidationNotice()}>
|
||
<div style="margin-top:10px;border:1px solid #E5E7EB;background:#F9FAFB;border-radius:8px;padding:8px 10px;font-size:12px;color:#374151">
|
||
{portfolioValidationNotice()}
|
||
</div>
|
||
</Show>
|
||
|
||
<Show when={selectedPortfolioTabKey === serviceTabKey}>
|
||
<div style="display:grid;gap:10px;margin-top:10px">
|
||
<div style="display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px">
|
||
<input type="text" value={portfolioServiceDraft().name} onInput={(e) => setPortfolioServiceDraft((prev) => ({ ...prev, name: e.currentTarget.value }))} placeholder="Service name" style="height:36px;border:1px solid #E5E7EB;border-radius:8px;background:white;padding:0 10px;font-size:12px;color:#111827;outline:none" />
|
||
<input type="text" value={portfolioServiceDraft().price} onInput={(e) => setPortfolioServiceDraft((prev) => ({ ...prev, price: e.currentTarget.value }))} placeholder="Starting price" style="height:36px;border:1px solid #E5E7EB;border-radius:8px;background:white;padding:0 10px;font-size:12px;color:#111827;outline:none" />
|
||
<input type="text" value={portfolioServiceDraft().model} onInput={(e) => setPortfolioServiceDraft((prev) => ({ ...prev, model: e.currentTarget.value }))} placeholder="Pricing model (Flat/Hourly)" style="height:36px;border:1px solid #E5E7EB;border-radius:8px;background:white;padding:0 10px;font-size:12px;color:#111827;outline:none" />
|
||
<input type="text" value={portfolioServiceDraft().details} onInput={(e) => setPortfolioServiceDraft((prev) => ({ ...prev, details: e.currentTarget.value }))} placeholder="Delivery timeline or details" style="height:36px;border:1px solid #E5E7EB;border-radius:8px;background:white;padding:0 10px;font-size:12px;color:#111827;outline:none" />
|
||
</div>
|
||
<div style="display:flex;justify-content:flex-start">
|
||
<button type="button" onClick={addServiceDraft} style="height:32px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:700;color:#374151;cursor:pointer">Add Service</button>
|
||
</div>
|
||
<div style="display:grid;gap:8px">
|
||
<For each={portfolioServices()}>{(service, idx) => (
|
||
<div style="border:1px solid #E5E7EB;border-radius:10px;background:#FAFBFD;padding:10px;display:grid;grid-template-columns:1fr auto;gap:8px;align-items:start">
|
||
<div style="display:grid;gap:4px">
|
||
<p style="margin:0;font-size:12px;font-weight:700;color:#111827">{service.name}</p>
|
||
<p style="margin:0;font-size:11px;color:#6B7280">{service.model} • {service.price}</p>
|
||
<p style="margin:0;font-size:11px;color:#374151">{service.details || 'No additional notes'}</p>
|
||
</div>
|
||
<button type="button" onClick={() => removeServiceItem(idx())} style="height:28px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 10px;font-size:11px;font-weight:700;color:#374151;cursor:pointer">Remove</button>
|
||
</div>
|
||
)}</For>
|
||
</div>
|
||
</div>
|
||
</Show>
|
||
|
||
<Show when={selectedPortfolioTabKey === galleryTabKey}>
|
||
<div style="display:grid;gap:10px;margin-top:10px">
|
||
<div style="display:flex;align-items:center;justify-content:space-between;gap:8px">
|
||
<p style="margin:0;font-size:12px;color:#374151">Portfolio photos ({portfolioPhotos().length}/6)</p>
|
||
<button type="button" onClick={addPhotoItem} disabled={portfolioPhotos().length >= 6} style={`height:32px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:700;color:#374151;cursor:${portfolioPhotos().length >= 6 ? 'not-allowed' : 'pointer'};opacity:${portfolioPhotos().length >= 6 ? 0.6 : 1}`}>Add Photo</button>
|
||
</div>
|
||
<div style="display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px">
|
||
<For each={portfolioPhotos()}>{(photo, idx) => (
|
||
<div style="border:1px solid #E5E7EB;border-radius:10px;background:#FAFBFD;padding:10px;display:flex;align-items:center;justify-content:space-between;gap:8px">
|
||
<span style="font-size:12px;font-weight:600;color:#374151">{photo}</span>
|
||
<button type="button" onClick={() => removePhotoItem(idx())} style="height:26px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 8px;font-size:11px;font-weight:700;color:#374151;cursor:pointer">Remove</button>
|
||
</div>
|
||
)}</For>
|
||
</div>
|
||
</div>
|
||
</Show>
|
||
|
||
<Show when={selectedPortfolioTabKey === experienceTabKey}>
|
||
<div style="display:grid;gap:10px;margin-top:10px">
|
||
<div style="display:grid;grid-template-columns:110px 1fr;gap:10px">
|
||
<input type="text" value={portfolioExperienceDraft().year} onInput={(e) => setPortfolioExperienceDraft((prev) => ({ ...prev, year: e.currentTarget.value }))} placeholder="Year" style="height:36px;border:1px solid #E5E7EB;border-radius:8px;background:white;padding:0 10px;font-size:12px;color:#111827;outline:none" />
|
||
<input type="text" value={portfolioExperienceDraft().title} onInput={(e) => setPortfolioExperienceDraft((prev) => ({ ...prev, title: e.currentTarget.value }))} placeholder="Role or milestone title" style="height:36px;border:1px solid #E5E7EB;border-radius:8px;background:white;padding:0 10px;font-size:12px;color:#111827;outline:none" />
|
||
</div>
|
||
<textarea value={portfolioExperienceDraft().details} onInput={(e) => setPortfolioExperienceDraft((prev) => ({ ...prev, details: e.currentTarget.value }))} placeholder="Brief details" style="min-height:72px;border:1px solid #E5E7EB;border-radius:8px;background:white;padding:8px 10px;font-size:12px;color:#111827;outline:none;resize:vertical" />
|
||
<div style="display:flex;justify-content:flex-start">
|
||
<button type="button" onClick={addExperienceDraft} style="height:32px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:700;color:#374151;cursor:pointer">Add Experience</button>
|
||
</div>
|
||
<div style="border:1px solid #E5E7EB;border-radius:10px;background:#FAFBFD;padding:10px;display:grid;gap:8px">
|
||
<p style="margin:0;font-size:11px;font-weight:700;color:#6B7280;letter-spacing:0.01em;text-transform:none">Design Tools</p>
|
||
<div style="display:flex;flex-wrap:wrap;gap:6px">
|
||
<For each={portfolioDesignTools()}>
|
||
{(tool) => (
|
||
<span style="height:24px;padding:0 8px;border-radius:6px;border:1px solid #E5E7EB;background:white;font-size:11px;font-weight:600;color:#374151;display:inline-flex;align-items:center;gap:6px">
|
||
{tool}
|
||
<button type="button" onClick={() => removePortfolioTag(tool, setPortfolioDesignTools)} style="border:none;background:none;color:#6B7280;cursor:pointer;padding:0;font-size:11px;line-height:1">x</button>
|
||
</span>
|
||
)}
|
||
</For>
|
||
</div>
|
||
<div style="display:grid;grid-template-columns:1fr auto;gap:8px;align-items:center">
|
||
<input type="text" value={portfolioToolInput()} onInput={(e) => setPortfolioToolInput(e.currentTarget.value)} placeholder="Add tools (e.g. Figma, Photoshop, Illustrator)" style="height:34px;border:1px solid #E5E7EB;border-radius:8px;background:white;padding:0 10px;font-size:12px;color:#374151;outline:none" />
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
const added = addPortfolioTag(portfolioToolInput(), setPortfolioDesignTools, 10);
|
||
if (added) setPortfolioToolInput('');
|
||
}}
|
||
style="height:32px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:700;color:#374151;cursor:pointer"
|
||
>
|
||
Add Tool
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div style="display:grid;gap:8px">
|
||
<For each={portfolioExperiences()}>{(entry, idx) => (
|
||
<div style="border:1px solid #E5E7EB;border-radius:10px;background:#FAFBFD;padding:10px;display:grid;grid-template-columns:1fr auto;gap:8px;align-items:start">
|
||
<div style="display:grid;gap:4px">
|
||
<p style="margin:0;font-size:12px;font-weight:700;color:#111827">{entry.year} - {entry.title}</p>
|
||
<p style="margin:0;font-size:11px;color:#374151">{entry.details || 'No additional notes'}</p>
|
||
</div>
|
||
<button type="button" onClick={() => removeExperienceItem(idx())} style="height:28px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 10px;font-size:11px;font-weight:700;color:#374151;cursor:pointer">Remove</button>
|
||
</div>
|
||
)}</For>
|
||
</div>
|
||
</div>
|
||
</Show>
|
||
|
||
<Show when={selectedPortfolioTabKey !== serviceTabKey && selectedPortfolioTabKey !== galleryTabKey && selectedPortfolioTabKey !== experienceTabKey}>
|
||
<div style="display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin-top:10px">
|
||
<For each={selectedPortfolioFormFields}>{(field) => renderPortfolioFormField(field)}</For>
|
||
</div>
|
||
</Show>
|
||
</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">Portfolio Form</p>
|
||
<p style="margin:8px 0 0;font-size:18px;font-weight:800;color:#111827">Simple step-by-step input</p>
|
||
<p style="margin:8px 0 0;font-size:12px;color:#6B7280;line-height:1.5">Fill one tab at a time. Use Next to move through sections, then check final Preview before submitting.</p>
|
||
<Show when={!!portfolioValidationNotice()}>
|
||
<div style="margin-top:10px;border:1px solid #E5E7EB;background:#F9FAFB;border-radius:8px;padding:8px 10px;font-size:12px;color:#374151">
|
||
{portfolioValidationNotice()}
|
||
</div>
|
||
</Show>
|
||
<div style="display:grid;gap:6px;margin-top:10px">
|
||
<For each={submissionTabs}>{(tabName, idx) => (
|
||
<div style={`height:28px;border-radius:8px;border:1px solid #E5E7EB;background:${idx() === activePortfolioStepIndex ? '#FFF8F4' : '#F9FAFB'};padding:0 10px;display:flex;align-items:center;font-size:11px;font-weight:700;color:${idx() === activePortfolioStepIndex ? '#C2410C' : '#374151'}`}>
|
||
{idx() + 1}. {titleCase(tabName)}
|
||
</div>
|
||
)}</For>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Show>
|
||
|
||
<Show when={isPreviewMode}>
|
||
|
||
<Show when={showSection(normalizeTabKey('about'))}>
|
||
{/* About + Specialties */}
|
||
<div style="display:grid;grid-template-columns:2fr 1fr;gap:10px">
|
||
<div style="border-radius:14px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.05);overflow:hidden">
|
||
<div style="padding:12px 16px;border-bottom:1px solid #E5E7EB">
|
||
<p style="margin:0;font-size:13px;font-weight:700;color:#111827;display:flex;align-items:center;gap:6px"><UserCircle2 size={14} style="color:#FF5E13" /> About</p>
|
||
</div>
|
||
<div style="padding:14px 16px">
|
||
<Show
|
||
when={canEdit}
|
||
fallback={<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>}
|
||
>
|
||
<textarea
|
||
value={`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.`}
|
||
style="width:100%;min-height:84px;border:1px solid #FFD8C2;border-radius:10px;background:#FFFCFA;padding:10px;font-size:13px;color:#374151;line-height:1.6;resize:vertical"
|
||
/>
|
||
</Show>
|
||
<div style="margin-top:10px;display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px">
|
||
{['Fast response within 2 hours', 'Clear scope & milestone planning', 'Delivery-first execution'].map((line) => (
|
||
<div style="border:1px solid #F3F4F6;border-radius:8px;background:#FAFAFA;padding:8px;font-size:11px;font-weight:600;color:#374151">{line}</div>
|
||
))}
|
||
</div>
|
||
</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:14px;border:1px solid #E5E7EB;background:white;box-shadow:0 1px 3px rgba(0,0,0,0.05);overflow:hidden">
|
||
<div style="padding:12px 16px;border-bottom:1px solid #E5E7EB">
|
||
<p style="margin:0;font-size:13px;font-weight:700;color:#111827;display:flex;align-items:center;gap:6px"><Rocket size={14} style="color:#FF5E13" /> Specialties</p>
|
||
</div>
|
||
<div style="padding:14px 16px;display:flex;flex-wrap:wrap;gap:6px">
|
||
<For each={portfolioSpecialties()}>
|
||
{(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;gap:6px">
|
||
{s}
|
||
<Show when={canEdit}>
|
||
<button
|
||
type="button"
|
||
onClick={() => removePortfolioTag(s, setPortfolioSpecialties)}
|
||
style="border:none;background:none;color:#6B7280;cursor:pointer;padding:0;font-size:11px;line-height:1"
|
||
aria-label={`Remove ${s}`}
|
||
>
|
||
x
|
||
</button>
|
||
</Show>
|
||
</span>
|
||
)}
|
||
</For>
|
||
</div>
|
||
<Show when={canEdit}>
|
||
<div style="padding:0 16px 12px;display:flex;gap:8px;align-items:center">
|
||
<input
|
||
type="text"
|
||
value={portfolioSpecialtyInput()}
|
||
onInput={(e) => setPortfolioSpecialtyInput(e.currentTarget.value)}
|
||
placeholder="Add specialty"
|
||
style="flex:1;height:32px;border:1px solid #E5E7EB;border-radius:8px;background:white;padding:0 10px;font-size:12px;color:#374151;outline:none"
|
||
/>
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
const added = addPortfolioTag(portfolioSpecialtyInput(), setPortfolioSpecialties, 6);
|
||
if (added) setPortfolioSpecialtyInput('');
|
||
}}
|
||
style="height:32px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 10px;font-size:11px;font-weight:700;color:#374151;cursor:pointer"
|
||
>
|
||
Add
|
||
</button>
|
||
</div>
|
||
</Show>
|
||
<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;flex-wrap:wrap">
|
||
<For each={portfolioLanguages()}>
|
||
{(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;gap:6px">
|
||
{l}
|
||
<Show when={canEdit}>
|
||
<button
|
||
type="button"
|
||
onClick={() => removePortfolioTag(l, setPortfolioLanguages)}
|
||
style="border:none;background:none;color:#6B7280;cursor:pointer;padding:0;font-size:11px;line-height:1"
|
||
aria-label={`Remove ${l}`}
|
||
>
|
||
x
|
||
</button>
|
||
</Show>
|
||
</span>
|
||
)}
|
||
</For>
|
||
</div>
|
||
<Show when={canEdit}>
|
||
<div style="margin-top:6px;display:flex;gap:8px;align-items:center">
|
||
<input
|
||
type="text"
|
||
value={portfolioLanguageInput()}
|
||
onInput={(e) => setPortfolioLanguageInput(e.currentTarget.value)}
|
||
placeholder="Add language"
|
||
style="flex:1;height:30px;border:1px solid #E5E7EB;border-radius:8px;background:white;padding:0 10px;font-size:12px;color:#374151;outline:none"
|
||
/>
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
const added = addPortfolioTag(portfolioLanguageInput(), setPortfolioLanguages, 6);
|
||
if (added) setPortfolioLanguageInput('');
|
||
}}
|
||
style="height:30px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 10px;font-size:11px;font-weight:700;color:#374151;cursor:pointer"
|
||
>
|
||
Add
|
||
</button>
|
||
</div>
|
||
</Show>
|
||
<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">
|
||
<For each={portfolioServiceAreas()}>
|
||
{(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;gap:6px">
|
||
{c}
|
||
<Show when={canEdit}>
|
||
<button
|
||
type="button"
|
||
onClick={() => removePortfolioTag(c, setPortfolioServiceAreas)}
|
||
style="border:none;background:none;color:#6B7280;cursor:pointer;padding:0;font-size:11px;line-height:1"
|
||
aria-label={`Remove ${c}`}
|
||
>
|
||
x
|
||
</button>
|
||
</Show>
|
||
</span>
|
||
)}
|
||
</For>
|
||
</div>
|
||
<Show when={canEdit}>
|
||
<div style="margin-top:6px;display:flex;gap:8px;align-items:center">
|
||
<input
|
||
type="text"
|
||
value={portfolioAreaInput()}
|
||
onInput={(e) => setPortfolioAreaInput(e.currentTarget.value)}
|
||
placeholder="Add service area in Chennai"
|
||
style="flex:1;height:30px;border:1px solid #E5E7EB;border-radius:8px;background:white;padding:0 10px;font-size:12px;color:#374151;outline:none"
|
||
/>
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
const added = addPortfolioTag(portfolioAreaInput(), setPortfolioServiceAreas, 8);
|
||
if (added) setPortfolioAreaInput('');
|
||
}}
|
||
style="height:30px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 10px;font-size:11px;font-weight:700;color:#374151;cursor:pointer"
|
||
>
|
||
Add
|
||
</button>
|
||
</div>
|
||
</Show>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Show>
|
||
|
||
<Show when={showSection(serviceTabKey)}>
|
||
{/* Packages */}
|
||
<div style="border-radius:12px;border:1px solid #E5E7EB;background:linear-gradient(180deg,#FFFFFF 0%,#FCFCFD 100%);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;justify-content:space-between;align-items:center">
|
||
<p style="margin:0;font-size:13px;font-weight:700;color:#111827;display:flex;align-items:center;gap:6px"><Coins size={14} style="color:#FF5E13" /> {titleCase(spec.serviceTabLabel)}</p>
|
||
<span style="height:22px;padding:0 8px;border-radius:999px;background:#FFF1EB;border:1px solid #FFD8C2;color:#C2410C;font-size:10px;font-weight:800;display:inline-flex;align-items:center">Transparent Pricing</span>
|
||
</div>
|
||
<Show when={canEdit}>
|
||
<div style="padding:10px 12px;border-bottom:1px solid #E5E7EB;background:#FFFFFF;display:grid;gap:8px">
|
||
<div style="display:grid;grid-template-columns:1.2fr 0.9fr 1fr 1.3fr auto;gap:8px;padding:8px;border:1px solid #E5E7EB;border-radius:10px;background:#F9FAFB">
|
||
<p style="margin:0;font-size:11px;font-weight:700;color:#6B7280;text-transform:uppercase">Service</p>
|
||
<p style="margin:0;font-size:11px;font-weight:700;color:#6B7280;text-transform:uppercase">Pricing Type</p>
|
||
<p style="margin:0;font-size:11px;font-weight:700;color:#6B7280;text-transform:uppercase">Amount</p>
|
||
<p style="margin:0;font-size:11px;font-weight:700;color:#6B7280;text-transform:uppercase">Details</p>
|
||
<p style="margin:0;font-size:11px;font-weight:700;color:#6B7280;text-transform:uppercase;text-align:center">Action</p>
|
||
</div>
|
||
{[1, 2].map((row) => (
|
||
<div style="display:grid;grid-template-columns:1.2fr 0.9fr 1fr 1.3fr auto;gap:8px;align-items:center;padding:8px;border:1px solid #E5E7EB;border-radius:10px;background:white">
|
||
<input type="text" value={row === 1 ? 'Wedding Coverage' : 'Pre-wedding Shoot'} style="height:34px;border:1px solid #E5E7EB;border-radius:8px;background:white;padding:0 10px;font-size:12px;color:#374151" />
|
||
<select style="height:34px;border:1px solid #E5E7EB;border-radius:8px;background:white;padding:0 10px;font-size:12px;color:#374151">
|
||
<option>Flat</option>
|
||
<option>Hourly</option>
|
||
<option>Per Day</option>
|
||
</select>
|
||
<input type="text" value={row === 1 ? '₹45,000' : '₹18,000'} style="height:34px;border:1px solid #E5E7EB;border-radius:8px;background:white;padding:0 10px;font-size:12px;color:#374151" />
|
||
<input type="text" value={row === 1 ? '8 hours, 300 photos' : '4 hours, 120 photos'} style="height:34px;border:1px solid #E5E7EB;border-radius:8px;background:white;padding:0 10px;font-size:12px;color:#374151" />
|
||
<button type="button" style="height:32px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 10px;font-size:11px;font-weight:700;color:#374151">Remove</button>
|
||
</div>
|
||
))}
|
||
<div style="display:flex;justify-content:flex-start">
|
||
<button type="button" style="height:32px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:700;color:#374151">+ Add Service</button>
|
||
</div>
|
||
</div>
|
||
</Show>
|
||
<div style="display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;padding:12px">
|
||
{spec.packages.map((pkg, i) => (
|
||
<div style={`border:1px solid ${i === 1 ? '#FFD8C2' : '#E5E7EB'};background:${i === 1 ? '#FFF8F4' : '#FFFFFF'};border-radius:10px;padding:12px`}>
|
||
<Show when={i === 1}>
|
||
<span style="height:20px;padding:0 8px;border-radius:999px;background:#FF5E13;color:white;font-size:10px;font-weight:800;display:inline-flex;align-items:center">Most Chosen</span>
|
||
</Show>
|
||
<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>
|
||
</Show>
|
||
|
||
<Show when={showSection(galleryTabKey)}>
|
||
{/* Work Gallery */}
|
||
<div style="border-radius:12px;border:1px solid #E5E7EB;background:linear-gradient(180deg,#FFFFFF 0%,#FCFCFD 100%);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;display:flex;align-items:center;gap:6px"><Image size={14} style="color:#FF5E13" /> {titleCase(spec.galleryTabLabel)}</p>
|
||
<button type="button" disabled={!canEdit} style={`height:30px;border-radius:8px;border:none;background:#0D0D2A;color:white;padding:0 12px;font-size:11px;font-weight:600;cursor:${canEdit ? 'pointer' : 'not-allowed'};opacity:${canEdit ? 1 : 0.6}`}>{mediaConfig.ctaLabel}</button>
|
||
</div>
|
||
<Show when={canEdit}>
|
||
<div style="padding:10px 12px;border-bottom:1px solid #E5E7EB;background:#FFF8F4">
|
||
<div style="height:34px;border:1px dashed #FFD8C2;border-radius:8px;background:#FFFCFA;display:flex;align-items:center;justify-content:center;font-size:12px;color:#9A3412;font-weight:600">
|
||
Drag files here or click upload to add portfolio samples (max 6 photos)
|
||
</div>
|
||
</div>
|
||
</Show>
|
||
<Show
|
||
when={mediaConfig.mode === 'visual'}
|
||
fallback={
|
||
<div style="padding:14px 16px;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px">
|
||
<For each={mediaConfig.items}>
|
||
{(item, idx) => (
|
||
<div style="min-height:84px;border-radius:10px;border:1px solid #E5E7EB;background:#F9FAFB;padding:10px;display:flex;gap:8px;align-items:flex-start">
|
||
<FileText size={16} style="color:#9CA3AF;flex-shrink:0;margin-top:2px" />
|
||
<div>
|
||
<p style="margin:0;font-size:12px;font-weight:700;color:#111827">{item}</p>
|
||
<p style="margin:4px 0 0;font-size:11px;color:#6B7280">Entry #{idx() + 1} • Structured proof without mandatory photos</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</For>
|
||
</div>
|
||
}
|
||
>
|
||
<div style="padding:14px 16px;display:grid;grid-template-columns:repeat(3,1fr);gap:8px">
|
||
{[0,1,2,3,4,5].map((i) => (
|
||
<div style="height:110px;border-radius:10px;border:1px solid #E5E7EB;background:linear-gradient(180deg,#FFFFFF 0%,#F8FAFC 100%);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px">
|
||
<Image size={20} style="color:#C5CCD5" />
|
||
<span style="font-size:10px;color:#9CA3AF;text-align:center;padding:0 6px;line-height:1.35">{mediaConfig.items[i % mediaConfig.items.length]}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</Show>
|
||
</div>
|
||
</Show>
|
||
|
||
<Show when={showSection(experienceTabKey)}>
|
||
{/* Experience */}
|
||
<div style="border-radius:12px;border:1px solid #E5E7EB;background:linear-gradient(180deg,#FFFFFF 0%,#FCFCFD 100%);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;display:flex;align-items:center;gap:6px"><BriefcaseBusiness size={14} style="color:#FF5E13" /> {titleCase(spec.experienceTabLabel)}</p>
|
||
</div>
|
||
<Show when={canEdit}>
|
||
<div style="padding:10px 12px;border-bottom:1px solid #E5E7EB;background:#FFF8F4;display:grid;grid-template-columns:120px 1fr;gap:8px">
|
||
<input type="text" value="Year" style="height:34px;border:1px solid #FFD8C2;border-radius:8px;background:#FFFCFA;padding:0 10px;font-size:12px;color:#374151" />
|
||
<input type="text" value="Milestone description" style="height:34px;border:1px solid #FFD8C2;border-radius:8px;background:#FFFCFA;padding:0 10px;font-size:12px;color:#374151" />
|
||
</div>
|
||
</Show>
|
||
<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 style="margin-top:12px">
|
||
<p style="margin:0 0 8px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:0.05em;color:#9CA3AF">Design Tools</p>
|
||
<Show
|
||
when={canEdit}
|
||
fallback={
|
||
<div style="display:flex;flex-wrap:wrap;gap:6px">
|
||
{(portfolioDesignTools().length ? portfolioDesignTools() : portfolioTools).map((tool) => (
|
||
<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">{tool}</span>
|
||
))}
|
||
</div>
|
||
}
|
||
>
|
||
<div style="display:grid;grid-template-columns:1fr auto;gap:8px;align-items:center">
|
||
<input type="text" placeholder="Add tools (e.g. Figma, Photoshop, Illustrator)" style="height:34px;border:1px solid #E5E7EB;border-radius:8px;background:white;padding:0 10px;font-size:12px;color:#374151" />
|
||
<button type="button" style="height:32px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:700;color:#374151">+ Add Tool</button>
|
||
</div>
|
||
<div style="display:flex;flex-wrap:wrap;gap:6px;margin-top:8px">
|
||
{(portfolioDesignTools().length ? portfolioDesignTools() : portfolioTools).map((tool) => (
|
||
<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;gap:6px">
|
||
{tool}
|
||
<button type="button" style="border:none;background:none;color:#9CA3AF;cursor:pointer;font-size:11px;line-height:1">✕</button>
|
||
</span>
|
||
))}
|
||
</div>
|
||
</Show>
|
||
</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:12px;align-items:flex-start;padding:10px 16px;${i<arr.length-1?'border-bottom:1px solid #F3F4F6':''}`}>
|
||
<span style="margin-top:2px;width:8px;height:8px;border-radius:999px;background:#FF5E13;flex-shrink:0" />
|
||
<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;line-height:1.5">{desc}</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</Show>
|
||
|
||
<Show when={showSection(testimonialsTabKey)}>
|
||
{/* Testimonials */}
|
||
<div style="border-radius:12px;border:1px solid #E5E7EB;background:linear-gradient(180deg,#FFFFFF 0%,#FCFCFD 100%);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;gap:10px">
|
||
<p style="margin:0;font-size:13px;font-weight:700;color:#111827;display:flex;align-items:center;gap:6px"><Star size={14} style="color:#FF5E13" /> Testimonials</p>
|
||
<Show
|
||
when={testimonialsUnlocked}
|
||
fallback={<span style="font-size:11px;font-weight:700;color:#C2410C;background:#FFF1EB;border:1px solid #FFD8C2;border-radius:6px;padding:1px 7px">Locked For New Profiles</span>}
|
||
>
|
||
<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>
|
||
</Show>
|
||
</div>
|
||
<Show
|
||
when={testimonialsUnlocked}
|
||
fallback={
|
||
<div style="padding:16px">
|
||
<div style="border:1px dashed #FFD8C2;background:#FFF8F4;border-radius:10px;padding:14px">
|
||
<p style="margin:0;font-size:13px;font-weight:700;color:#111827">Testimonials will be activated automatically.</p>
|
||
<p style="margin:6px 0 0;font-size:12px;line-height:1.5;color:#6B7280">
|
||
New professionals unlock testimonials after completing at least <strong>3 jobs</strong> and receiving at least <strong>2 customer feedback entries</strong>.
|
||
</p>
|
||
<p style="margin:8px 0 0;font-size:11px;color:#9CA3AF">Current progress: {portfolioJobsCompleted} jobs completed • {portfolioFeedbackCount} feedback received</p>
|
||
</div>
|
||
</div>
|
||
}
|
||
>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;padding:10px">
|
||
{PORTFOLIO_TESTIMONIALS.map((t, i) => (
|
||
<div style="padding:14px 16px;border:1px solid #E5E7EB;border-radius:10px;background:#FFFFFF">
|
||
<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>
|
||
</Show>
|
||
</div>
|
||
</Show>
|
||
|
||
<Show when={showSection(faqsTabKey)}>
|
||
{/* FAQs */}
|
||
<div style="border-radius:12px;border:1px solid #E5E7EB;background:linear-gradient(180deg,#FFFFFF 0%,#FCFCFD 100%);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;display:flex;align-items:center;gap:6px"><HelpCircle size={14} style="color:#FF5E13" /> Frequently Asked Questions</p>
|
||
</div>
|
||
<Show when={canEdit}>
|
||
<div style="padding:10px 12px;border-bottom:1px solid #E5E7EB;background:#FFF8F4;display:grid;gap:8px">
|
||
<input type="text" value="Add question" style="height:34px;border:1px solid #FFD8C2;border-radius:8px;background:#FFFCFA;padding:0 10px;font-size:12px;color:#374151" />
|
||
<textarea style="min-height:66px;border:1px solid #FFD8C2;border-radius:8px;background:#FFFCFA;padding:8px 10px;font-size:12px;color:#374151;resize:vertical">Add answer</textarea>
|
||
</div>
|
||
</Show>
|
||
<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:${openFaqIndex()===i() ? '#FFF8F4' : 'none'};border:none;cursor:pointer;text-align:left`}
|
||
>
|
||
<span style="font-size:13px;font-weight:700;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>
|
||
</Show>
|
||
</Show>
|
||
|
||
<div style="border-radius:12px;border:1px solid #E5E7EB;background:white;padding:10px 12px;display:flex;align-items:center;justify-content:space-between;gap:10px">
|
||
<p style="margin:0;font-size:12px;color:#6B7280">
|
||
<Show when={!isPreviewMode} fallback={<span>Final review mode: check all tabs, then submit for approval.</span>}>
|
||
<span>Step {activePortfolioStepIndex + 1} of {portfolioStepKeys.length}</span>
|
||
</Show>
|
||
</p>
|
||
<div style="display:flex;align-items:center;gap:8px">
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
if (isPreviewMode) {
|
||
setPortfolioTopTab('my_portfolio');
|
||
goToPortfolioStep(portfolioStepKeys.length - 1);
|
||
return;
|
||
}
|
||
goToPortfolioStep(activePortfolioStepIndex - 1);
|
||
}}
|
||
disabled={!isPreviewMode && activePortfolioStepIndex === 0}
|
||
style={`height:32px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:700;color:#374151;cursor:${(!isPreviewMode && activePortfolioStepIndex === 0) ? 'not-allowed' : 'pointer'};opacity:${(!isPreviewMode && activePortfolioStepIndex === 0) ? 0.5 : 1}`}
|
||
>
|
||
Previous
|
||
</button>
|
||
<Show
|
||
when={!isPreviewMode}
|
||
fallback={
|
||
<button type="button" onClick={() => setPortfolioTopTab('my_portfolio')} style="height:32px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:700;color:#374151">
|
||
Back To Edit
|
||
</button>
|
||
}
|
||
>
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
const currentStepKey = portfolioStepKeys[activePortfolioStepIndex];
|
||
if (!validatePortfolioStep(currentStepKey)) return;
|
||
if (activePortfolioStepIndex >= portfolioStepKeys.length - 1) {
|
||
setPortfolioTopTab('preview');
|
||
return;
|
||
}
|
||
goToPortfolioStep(activePortfolioStepIndex + 1);
|
||
}}
|
||
style="height:32px;border-radius:8px;border:none;background:#03004E;color:white;padding:0 12px;font-size:12px;font-weight:700"
|
||
>
|
||
{activePortfolioStepIndex >= portfolioStepKeys.length - 1 ? 'Final Preview' : 'Next'}
|
||
</button>
|
||
</Show>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
);
|
||
};
|
||
|
||
const renderCustomerContent = () => {
|
||
const tab = resolvedTabKey();
|
||
|
||
if (customerKey() === 'my dashboard') {
|
||
if (tab === 'recent requirements') {
|
||
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)">
|
||
<p style="margin:0;font-size:14px;font-weight:700;color:#111827">Recent Requirements</p>
|
||
<div style="margin-top:10px;display:grid;gap:8px">
|
||
<For each={requirementRows()}>{(row) => {
|
||
const chip = statusChip(row.status);
|
||
return (
|
||
<div style="display:grid;grid-template-columns:2fr 1fr 1fr 1fr;gap:8px;padding:8px;border:1px solid #E5E7EB;border-radius:8px;background:#F9FAFB">
|
||
<span style="font-size:12px;font-weight:700;color:#111827">{row.title}</span>
|
||
<span style="font-size:12px;color:#6B7280">{row.amount}</span>
|
||
<span style={`display:inline-flex;align-items:center;justify-content:center;height:22px;border-radius:999px;background:${chip.bg};color:${chip.c};font-size:11px;font-weight:700;text-transform:uppercase`}>{row.status}</span>
|
||
<span style="font-size:12px;color:#374151">{row.responses} responses</span>
|
||
</div>
|
||
);
|
||
}}</For>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
if (tab === 'quick actions') {
|
||
return (
|
||
<div style="display:grid;grid-template-columns: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:14px;font-weight:700;color:#111827">Quick Actions</p>
|
||
<div style="display:grid;gap:8px;margin-top:8px">
|
||
{['Post New Requirement', 'View Responses', 'Buy Credits', 'Explore Professionals'].map((a, i) => (
|
||
<button type="button" style={`height:34px;border-radius:8px;border:${i===0?'none':'1px solid #E5E7EB'};background:${i===0?'#FF5E13':'white'};color:${i===0?'white':'#374151'};font-size:12px;font-weight:700;cursor:pointer;text-align:left;padding:0 10px`}>
|
||
{a}
|
||
</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)">
|
||
<p style="margin:0;font-size:14px;font-weight:700;color:#111827">Recent Activity</p>
|
||
<div style="margin-top:8px;display:grid;gap:8px">
|
||
{['Requirement posted', 'New response received', 'Credit purchase completed'].map((a) => <div style="padding:8px;border:1px solid #E5E7EB;border-radius:8px;background:#F9FAFB;font-size:12px;color:#374151">{a}</div>)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
return (
|
||
<div style="display:flex;flex-direction:column;gap:10px">
|
||
<Show when={!bothApprovalsApproved()}>
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:12px;padding:12px;box-shadow:0 1px 4px rgba(0,0,0,0.04)">
|
||
<p style="margin:0;font-size:14px;font-weight:800;color:#111827">Complete Verification</p>
|
||
<p style="margin:6px 0 0;font-size:12px;color:#6B7280;line-height:1.5">
|
||
To start receiving opportunities, submit your profile and portfolio for admin approval.
|
||
</p>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:10px">
|
||
<div style="border:1px solid #E5E7EB;border-radius:8px;background:#F9FAFB;padding:8px">
|
||
<p style="margin:0;font-size:11px;color:#6B7280">Step 1</p>
|
||
<p style="margin:4px 0 0;font-size:12px;font-weight:800;color:#111827">Profile: {approvalTone(profileApprovalState()).label}</p>
|
||
</div>
|
||
<div style="border:1px solid #E5E7EB;border-radius:8px;background:#F9FAFB;padding:8px">
|
||
<p style="margin:0;font-size:11px;color:#6B7280">Step 2</p>
|
||
<p style="margin:4px 0 0;font-size:12px;font-weight:800;color:#111827">Portfolio: {approvalTone(portfolioApprovalState()).label}</p>
|
||
</div>
|
||
</div>
|
||
<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:32px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:700;color:#374151">Open My Profile</button>
|
||
<Show when={isProfessionalRole()}>
|
||
<button type="button" onClick={() => { props.onSidebarSelect('My Portfolio'); props.onTabSelect('about'); }} style="height:32px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:700;color:#374151">Open My Portfolio</button>
|
||
</Show>
|
||
<button type="button" onClick={() => props.onSidebarSelect('Verification')} style="height:32px;border-radius:8px;border:none;background:#03004E;color:white;padding:0 12px;font-size:12px;font-weight:700">Open Verification</button>
|
||
</div>
|
||
</div>
|
||
</Show>
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:12px;padding:10px 12px;box-shadow:0 1px 4px rgba(0,0,0,0.04)">
|
||
<p style="margin:0;font-size:11px;letter-spacing:0.04em;text-transform:uppercase;color:#6B7280">Widget Customization</p>
|
||
<p style="margin:4px 0 0;font-size:12px;color:#374151">Drag and drop cards below to reorder your dashboard widgets.</p>
|
||
</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, {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>
|
||
<Show when={isProfessionalRole()}>
|
||
<button type="button" onClick={() => { props.onSidebarSelect('My Portfolio'); props.onTabSelect('about'); }} 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 Portfolio</button>
|
||
</Show>
|
||
<button type="button" onClick={() => props.onSidebarSelect('Verification')} style="height:30px;border-radius:8px;border:none;background:#03004E;color:white;padding:0 10px;font-size:11px;font-weight:700">Submit For Approval</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)">
|
||
<p style="margin:0;font-size:11px;letter-spacing:0.04em;color:#6B7280;text-transform:uppercase">Profile Status</p>
|
||
<p style="margin:8px 0 0;font-size:34px;font-weight:800;color:#111827">85%</p>
|
||
</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">{leadCredits().toLocaleString('en-IN')}</p>
|
||
</div>
|
||
</div>
|
||
<div style="display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:10px">
|
||
<For each={dashboardWidgetOrder().slice(0, 5)}>
|
||
{(w) => (
|
||
<div
|
||
draggable
|
||
onDragStart={(e) => {
|
||
setDraggingDashboardWidget(w);
|
||
e.dataTransfer?.setData('text/plain', w);
|
||
}}
|
||
onDragOver={(e) => e.preventDefault()}
|
||
onDrop={(e) => {
|
||
e.preventDefault();
|
||
moveDashboardWidget(draggingDashboardWidget() || '', w);
|
||
setDraggingDashboardWidget(null);
|
||
}}
|
||
style={`border:1px solid #E5E7EB;background:${draggingDashboardWidget() === w ? '#FFF8F4' : 'white'};border-radius:12px;padding:10px;min-height:92px;box-shadow:0 1px 3px rgba(0,0,0,0.05);cursor:grab`}
|
||
>
|
||
<p style="margin:0;font-size:11px;letter-spacing:0.04em;text-transform:uppercase;color:#6B7280">{titleCase(w)}</p>
|
||
<p style="margin:10px 0 0;font-size:22px;font-weight:800;color:#111827">42</p>
|
||
</div>
|
||
)}
|
||
</For>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (customerKey().includes('profile')) {
|
||
const spec = currentProfileSpec();
|
||
const selectedTab = spec.tabs.find((item) => normalizeTabKey(item) === normalizeTabKey(tab)) || spec.tabs[0] || 'basic info';
|
||
const selectedTabKey = normalizeTabKey(selectedTab);
|
||
const defaultFields = ['First Name', 'Last Name', 'Email Address', 'Mobile Number', 'City'];
|
||
const fieldsForTab = spec.tabFields[selectedTab] || spec.tabFields[spec.tabs[0] || ''] || defaultFields;
|
||
const isPreferencesTab = normalizeTabKey(selectedTab) === 'preferences';
|
||
const isDocumentsTab = selectedTabKey === 'documents';
|
||
const isRequiredField = (field: string) => !/\(optional\)|optional/i.test(field);
|
||
const requiredCount = fieldsForTab.filter((f) => isRequiredField(f)).length;
|
||
const profileState = () => approvalTone(profileApprovalState());
|
||
const renderField = (field: string) => {
|
||
const required = isRequiredField(field);
|
||
const isSelect = /country|mode|type|category|level|gender|industry|subjects|platforms|budget|response time/i.test(field);
|
||
const isSpecialitiesField = /specialit|specializ|skills/i.test(field);
|
||
const cleanLabel = String(field || '').replace(/\(optional\)/ig, '').trim();
|
||
const safeLabel = cleanLabel || 'value';
|
||
const key = safeLabel.toLowerCase();
|
||
const labelText = (() => {
|
||
if (key.includes('specialt') || key.includes('specialit') || key.includes('specializ')) return 'Specialities';
|
||
return safeLabel;
|
||
})();
|
||
const isCityField = key === 'city';
|
||
const placeholderText = (() => {
|
||
if (key.includes('gender')) return 'Select gender';
|
||
if (key.includes('address line 1')) return 'Enter address line 1';
|
||
if (key.includes('address line 2')) return 'Enter address line 2';
|
||
if (key === 'city') return 'Chennai';
|
||
if (key === 'state') return 'Enter state';
|
||
if (key === 'address') return 'Enter full address in Chennai';
|
||
if (key.includes('address proof')) return 'Upload address proof (PDF/JPG/PNG)';
|
||
if (key.includes('proof') || key.includes('certificate') || key.includes('document')) return `Upload ${labelText} (PDF/JPG/PNG)`;
|
||
if (key.includes('pin code') || key.includes('pincode')) return 'Enter 6-digit pincode';
|
||
if (key.includes('area')) return 'Enter area in Chennai';
|
||
if (key.includes('place')) return 'Enter place in Chennai';
|
||
if (isSpecialitiesField) return 'e.g. Wedding, Product, Portrait';
|
||
return `${isSelect ? 'Select' : 'Enter'} ${labelText.toLowerCase()}`;
|
||
})();
|
||
return (
|
||
<div style="display:flex;flex-direction:column;gap:6px">
|
||
<p style="margin:0;font-size:11px;font-weight:700;color:#6B7280;letter-spacing:0.01em;text-transform:none">
|
||
{labelText}
|
||
<Show when={required}>
|
||
<span style="color:#FF5E13;margin-left:4px">*</span>
|
||
</Show>
|
||
</p>
|
||
<div style="position:relative">
|
||
<input
|
||
type="text"
|
||
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"
|
||
/>
|
||
<Show when={isSelect}>
|
||
<span style="position:absolute;right:10px;top:50%;transform:translateY(-50%);color:#9CA3AF;font-size:12px">▾</span>
|
||
</Show>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
if (selectedTab === 'settings') {
|
||
const settingsTabs: Array<{ key: 'change_password' | 'notifications' | 'privacy'; label: string }> = [
|
||
{ key: 'change_password', label: 'Change Password' },
|
||
{ key: 'notifications', label: 'Notifications' },
|
||
{ key: 'privacy', label: 'Privacy' },
|
||
];
|
||
const activeSettingsTab = profileSettingsTab();
|
||
return (
|
||
<div style="display:grid;grid-template-columns: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:13px;font-weight:700;color:#111827">Settings</p>
|
||
<div style="display:flex;align-items:center;gap:20px;margin-top:10px;border-bottom:1px solid #E5E7EB">
|
||
<For each={settingsTabs}>
|
||
{(item) => (
|
||
<button
|
||
type="button"
|
||
onClick={() => setProfileSettingsTab(item.key)}
|
||
style={`padding:0 0 10px;font-size:13px;font-weight:500;background:none;border:none;cursor:pointer;${activeSettingsTab === item.key ? 'color:#FF5E13;border-bottom:2px solid #FF5E13;margin-bottom:-1px' : 'color:#6B7280'}`}
|
||
>
|
||
{item.label}
|
||
</button>
|
||
)}
|
||
</For>
|
||
</div>
|
||
<Show when={activeSettingsTab === 'change_password'}>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:10px">
|
||
<div style="display:flex;flex-direction:column;gap:6px">
|
||
<p style="margin:0;font-size:11px;font-weight:700;color:#6B7280;letter-spacing:0.04em;text-transform:uppercase">Current Password<span style="color:#FF5E13;margin-left:4px">*</span></p>
|
||
<input type="password" placeholder="Enter current password" style="height:36px;border:1px solid #E5E7EB;border-radius:8px;background:white;padding:0 10px;font-size:12px;color:#111827;outline:none" />
|
||
</div>
|
||
<div style="display:flex;flex-direction:column;gap:6px">
|
||
<p style="margin:0;font-size:11px;font-weight:700;color:#6B7280;letter-spacing:0.04em;text-transform:uppercase">New Password<span style="color:#FF5E13;margin-left:4px">*</span></p>
|
||
<input type="password" placeholder="Enter new password" style="height:36px;border:1px solid #E5E7EB;border-radius:8px;background:white;padding:0 10px;font-size:12px;color:#111827;outline:none" />
|
||
</div>
|
||
</div>
|
||
<div style="display:flex;flex-direction:column;gap:6px;margin-top:10px">
|
||
<p style="margin:0;font-size:11px;font-weight:700;color:#6B7280;letter-spacing:0.04em;text-transform:uppercase">Confirm Password<span style="color:#FF5E13;margin-left:4px">*</span></p>
|
||
<input type="password" placeholder="Confirm new password" style="height:36px;border:1px solid #E5E7EB;border-radius:8px;background:white;padding:0 10px;font-size:12px;color:#111827;outline:none" />
|
||
</div>
|
||
</Show>
|
||
<Show when={activeSettingsTab === 'notifications'}>
|
||
<div style="display:grid;gap:8px;margin-top:10px">
|
||
{[
|
||
['email_updates', 'Email Updates'],
|
||
['in_app_alerts', 'In-app Alerts'],
|
||
['verification_reminders', 'Verification Reminders'],
|
||
].map(([key, title]) => (
|
||
<div style="display:flex;justify-content:space-between;align-items:center;gap:10px;padding:10px;border:1px solid #E5E7EB;border-radius:10px;background:#F9FAFB">
|
||
<p style="margin:0;font-size:12px;font-weight:700;color:#111827">{title}</p>
|
||
<button
|
||
type="button"
|
||
onClick={() => toggleProfileSetting(String(key))}
|
||
style={`height:30px;border-radius:999px;padding:0 12px;font-size:11px;font-weight:700;cursor:pointer;border:1px solid ${profileSettingToggles()[String(key)] ? '#C7D2FE' : '#D1D5DB'};background:${profileSettingToggles()[String(key)] ? '#EEF2FF' : 'white'};color:${profileSettingToggles()[String(key)] ? '#03004E' : '#374151'}`}
|
||
>
|
||
{profileSettingToggles()[String(key)] ? 'On' : 'Off'}
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</Show>
|
||
<Show when={activeSettingsTab === 'privacy'}>
|
||
<div style="display:grid;gap:8px;margin-top:10px">
|
||
{[
|
||
['profile_visibility', 'Profile Visibility'],
|
||
['data_sharing_consent', 'Data Sharing Consent'],
|
||
].map(([key, title]) => (
|
||
<div style="display:flex;justify-content:space-between;align-items:center;gap:10px;padding:10px;border:1px solid #E5E7EB;border-radius:10px;background:#F9FAFB">
|
||
<p style="margin:0;font-size:12px;font-weight:700;color:#111827">{title}</p>
|
||
<button
|
||
type="button"
|
||
onClick={() => toggleProfileSetting(String(key))}
|
||
style={`height:30px;border-radius:999px;padding:0 12px;font-size:11px;font-weight:700;cursor:pointer;border:1px solid ${profileSettingToggles()[String(key)] ? '#C7D2FE' : '#D1D5DB'};background:${profileSettingToggles()[String(key)] ? '#EEF2FF' : 'white'};color:${profileSettingToggles()[String(key)] ? '#03004E' : '#374151'}`}
|
||
>
|
||
{profileSettingToggles()[String(key)] ? 'On' : 'Off'}
|
||
</button>
|
||
</div>
|
||
))}
|
||
<div style="display:flex;justify-content:space-between;align-items:center;gap:10px;padding:10px;border:1px solid #FFE2D3;border-radius:10px;background:#FFF8F4">
|
||
<div>
|
||
<p style="margin:0;font-size:12px;font-weight:700;color:#111827">Delete Account</p>
|
||
<p style="margin:2px 0 0;font-size:11px;color:#6B7280">Permanently remove your account and data.</p>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
onClick={() => setShowDeleteAccountModal(true)}
|
||
style="height:30px;border-radius:8px;padding:0 12px;font-size:11px;font-weight:700;cursor:pointer;border:1px solid #FFD0BA;background:white;color:#FF5E13"
|
||
>
|
||
Delete
|
||
</button>
|
||
</div>
|
||
</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">
|
||
{activeSettingsTab === 'change_password' ? 'Update Password' : 'Save Settings'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<Show when={showDeleteAccountModal()}>
|
||
<div style="position:fixed;inset:0;background:rgba(15,23,42,0.4);display:flex;align-items:center;justify-content:center;z-index:50;padding:16px">
|
||
<div style="width:min(460px,100%);border:1px solid #E5E7EB;background:white;border-radius:14px;box-shadow:0 10px 30px rgba(0,0,0,0.18);padding:16px">
|
||
<div style="display:flex;align-items:center;gap:8px">
|
||
<span style="width:22px;height:22px;border-radius:999px;background:#FFF1EB;color:#FF5E13;display:inline-flex;align-items:center;justify-content:center;font-size:13px;font-weight:800">!</span>
|
||
<p style="margin:0;font-size:15px;font-weight:800;color:#111827">Delete account?</p>
|
||
</div>
|
||
<p style="margin:10px 0 0;font-size:13px;color:#374151;line-height:1.5">
|
||
This will permanently remove your account. This action cannot be undone.
|
||
</p>
|
||
<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:14px">
|
||
<button
|
||
type="button"
|
||
onClick={() => setShowDeleteAccountModal(false)}
|
||
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={() => setShowDeleteAccountModal(false)}
|
||
style="height:34px;border-radius:8px;border:none;background:#03004E;color:white;padding:0 14px;font-size:12px;font-weight:700"
|
||
>
|
||
Yes, Delete Account
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Show>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div style="display:grid;grid-template-columns:2fr 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)">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;gap:10px">
|
||
<p style="margin:0;font-size:13px;font-weight:700;color:#111827">{isPreferencesTab ? 'Preference Details' : titleCase(selectedTab)}</p>
|
||
<span style="height:22px;padding:0 8px;border-radius:999px;border:1px solid #DDEBFF;background:#EEF4FF;display:inline-flex;align-items:center;font-size:10px;font-weight:700;color:#03004E">
|
||
{fieldsForTab.length} fields
|
||
</span>
|
||
</div>
|
||
<Show
|
||
when={isDocumentsTab}
|
||
fallback={
|
||
<Show
|
||
when={isPreferencesTab}
|
||
fallback={
|
||
<div style="display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin-top:10px">
|
||
<For each={fieldsForTab}>{(field) => renderField(field)}</For>
|
||
</div>
|
||
}
|
||
>
|
||
<div style="display:grid;grid-template-columns:1fr;gap:8px;margin-top:10px;max-width:560px">
|
||
<For each={fieldsForTab}>{(field) => renderField(field)}</For>
|
||
</div>
|
||
</Show>
|
||
}
|
||
>
|
||
<div style="display:grid;grid-template-columns:1fr;gap:10px;margin-top:10px;max-width:560px">
|
||
<div style="display:flex;flex-direction:column;gap:6px">
|
||
<p style="margin:0;font-size:11px;font-weight:700;color:#6B7280;letter-spacing:0.01em;text-transform:none">
|
||
Document type<span style="color:#FF5E13;margin-left:4px">*</span>
|
||
</p>
|
||
<select
|
||
value={profileDocumentType()}
|
||
onChange={(e) => setProfileDocumentType(e.currentTarget.value)}
|
||
style="width:100%;height:36px;border:1px solid #E5E7EB;border-radius:8px;background:white;padding:0 10px;font-size:12px;color:#111827;outline:none"
|
||
>
|
||
<option>Aadhar Card</option>
|
||
<option>PAN Card</option>
|
||
<option>Passport</option>
|
||
<option>Driving License</option>
|
||
</select>
|
||
</div>
|
||
<div style="display:flex;flex-direction:column;gap:6px">
|
||
<p style="margin:0;font-size:11px;font-weight:700;color:#6B7280;letter-spacing:0.01em;text-transform:none">
|
||
Upload document<span style="color:#FF5E13;margin-left:4px">*</span>
|
||
</p>
|
||
<input
|
||
type="text"
|
||
value=""
|
||
placeholder={`Upload ${profileDocumentType().toLowerCase()} (PDF/JPG/PNG)`}
|
||
style="width:100%;height:36px;border:1px solid #E5E7EB;border-radius:8px;background:white;padding:0 10px;font-size:12px;color:#111827;outline:none"
|
||
/>
|
||
</div>
|
||
</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" 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,
|
||
full_name: display || undefined,
|
||
first_name: fn || undefined,
|
||
last_name: ln || undefined,
|
||
business_name: data['Business Name'] || undefined,
|
||
company_name: data['Company Name'] || undefined,
|
||
contact_person: data['Contact Person Name'] || undefined,
|
||
bio: data['Bio'] || undefined,
|
||
location: data['City'] || undefined,
|
||
area: data['Area'] || undefined,
|
||
state: data['State'] || undefined,
|
||
pin_code: data['PIN Code'] || undefined,
|
||
phone: data['Mobile Number'] || undefined,
|
||
email: data['Email Address'] || 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)">
|
||
<p style="margin:0;font-size:11px;letter-spacing:0.04em;text-transform:uppercase;color:#6B7280">What To Do Next</p>
|
||
<p style="margin:8px 0 0;font-size:18px;font-weight:800;color:#111827;line-height:1.3">Complete profile and portfolio to unlock leads</p>
|
||
<p style="margin:8px 0 0;font-size:12px;color:#6B7280;line-height:1.5">Finish My Profile and My Portfolio, then submit both for admin approval. Leads will unlock only after approval.</p>
|
||
<div style="display:grid;gap:8px;margin-top:10px">
|
||
<div style="display:flex;align-items:center;justify-content:space-between;gap:8px;padding:8px;border:1px solid #E5E7EB;border-radius:8px;background:#F9FAFB">
|
||
<span style="font-size:11px;font-weight:700;color:#374151">My Profile</span>
|
||
<span style="font-size:11px;font-weight:800;color:#111827">{approvalTone(profileApprovalState()).label}</span>
|
||
</div>
|
||
<Show when={isProfessionalRole()}>
|
||
<div style="display:flex;align-items:center;justify-content:space-between;gap:8px;padding:8px;border:1px solid #E5E7EB;border-radius:8px;background:#F9FAFB">
|
||
<span style="font-size:11px;font-weight:700;color:#374151">My Portfolio</span>
|
||
<span style="font-size:11px;font-weight:800;color:#111827">{approvalTone(portfolioApprovalState()).label}</span>
|
||
</div>
|
||
</Show>
|
||
</div>
|
||
<div style="display:grid;gap:8px;margin-top:10px">
|
||
<button type="button" onClick={() => { props.onSidebarSelect('My Profile'); props.onTabSelect('basic information'); }} style="height:32px;border:1px solid #D1D5DB;border-radius:8px;background:white;color:#374151;padding:0 12px;font-size:12px;font-weight:700">Fill My Profile</button>
|
||
<Show when={isProfessionalRole()}>
|
||
<button type="button" onClick={() => { props.onSidebarSelect('My Portfolio'); props.onTabSelect('about'); }} style="height:32px;border:1px solid #D1D5DB;border-radius:8px;background:white;color:#374151;padding:0 12px;font-size:12px;font-weight:700">Fill My Portfolio</button>
|
||
</Show>
|
||
<button type="button" onClick={() => props.onSidebarSelect('Verification')} style="height:32px;border:none;border-radius:8px;background:#03004E;color:white;padding:0 12px;font-size:12px;font-weight:700">Submit For Approval</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (customerKey() === 'leads') {
|
||
if (!bothApprovalsApproved()) {
|
||
return (
|
||
<div style="display:grid;gap:12px">
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:14px;padding:14px">
|
||
<p style="margin:0;font-size:22px;font-weight:800;color:#111827">Leads Locked</p>
|
||
<p style="margin:8px 0 0;font-size:13px;color:#6B7280;line-height:1.55">
|
||
Complete verification before accessing leads. You must submit profile and portfolio, then get admin approval.
|
||
</p>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:10px">
|
||
<div style="border:1px solid #E5E7EB;border-radius:10px;background:#F9FAFB;padding:10px">
|
||
<p style="margin:0;font-size:11px;color:#6B7280;text-transform:uppercase;letter-spacing:0.05em">Profile Verification</p>
|
||
<p style="margin:6px 0 0;font-size:13px;font-weight:800;color:#111827">{approvalTone(profileApprovalState()).label}</p>
|
||
</div>
|
||
<div style="border:1px solid #E5E7EB;border-radius:10px;background:#F9FAFB;padding:10px">
|
||
<p style="margin:0;font-size:11px;color:#6B7280;text-transform:uppercase;letter-spacing:0.05em">Portfolio Verification</p>
|
||
<p style="margin:6px 0 0;font-size:13px;font-weight:800;color:#111827">{approvalTone(portfolioApprovalState()).label}</p>
|
||
</div>
|
||
</div>
|
||
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:10px">
|
||
<button type="button" onClick={() => props.onSidebarSelect('Verification')} style="height:32px;border-radius:8px;border:none;background:#03004E;color:white;padding:0 12px;font-size:12px;font-weight:700">Open Verification</button>
|
||
<button type="button" onClick={() => { props.onSidebarSelect('My Profile'); props.onTabSelect('basic information'); }} style="height:32px;border-radius:8px;border:1px solid #E5E7EB;background:white;color:#374151;padding:0 12px;font-size:12px;font-weight:700">Complete Profile</button>
|
||
<button type="button" onClick={() => { props.onSidebarSelect('My Portfolio'); props.onTabSelect('about'); }} style="height:32px;border-radius:8px;border:1px solid #E5E7EB;background:white;color:#374151;padding:0 12px;font-size:12px;font-weight:700">Complete Portfolio</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
const requestStatusPill = (status: string) => {
|
||
if (status === 'request_sent') return { bg: '#DBEAFE', c: '#1D4ED8', text: 'Request Sent' };
|
||
if (status === 'approved') return { bg: '#DCFCE7', c: '#15803D', text: 'Approved' };
|
||
if (status === 'contact_unlocked') return { bg: '#ECFDF3', c: '#047857', text: 'Contact Unlocked' };
|
||
if (status === 'rejected') return { bg: '#FEE2E2', c: '#B91C1C', text: 'Rejected' };
|
||
if (status === 'expired_refunded') return { bg: '#E0E7FF', c: '#4338CA', text: 'Expired · Refunded' };
|
||
if (status === 'cancelled_by_professional') return { bg: '#FFF1EB', c: '#C2410C', text: 'Cancelled by Professional' };
|
||
return { bg: '#F3F4F6', c: '#6B7280', text: titleCase(status) };
|
||
};
|
||
const leadTabs = () => (activeLeadDetailId() ? [...LEAD_MARKETPLACE_TABS, 'View Details'] : LEAD_MARKETPLACE_TABS);
|
||
const selectedLead = () => leadCards().find((item) => item.id === activeLeadDetailId()) || null;
|
||
|
||
if (resolvedTabKey() === 'requested contacts') {
|
||
return (
|
||
<div style="display:flex;flex-direction:column;gap:10px">
|
||
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px">
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:14px;padding:12px">
|
||
<p style="margin:0;font-size:11px;letter-spacing:0.06em;text-transform:uppercase;color:#9CA3AF">Total Spent</p>
|
||
<p style="margin:8px 0 0;font-size:30px;line-height:1;font-weight:800;color:#111827">{250 - leadCredits()}</p>
|
||
<p style="margin:6px 0 0;font-size:12px;color:#6B7280">Tracecoins used this month</p>
|
||
</div>
|
||
<div style="border:1px solid #D7DBFF;background:#03004E;border-radius:14px;padding:12px;color:white">
|
||
<p style="margin:0;font-size:11px;letter-spacing:0.06em;text-transform:uppercase;color:#D7DBFF">Active Requests</p>
|
||
<p style="margin:8px 0 0;font-size:30px;line-height:1;font-weight:800">{leadCards().filter((card) => card.status === 'requested').length}</p>
|
||
<div style="height:4px;border-radius:999px;background:rgba(255,255,255,0.2);overflow:hidden;margin-top:8px">
|
||
<div style={`height:100%;width:${Math.min(100, leadCards().filter((card) => card.status === 'requested').length * 10)}%;background:#FF5E13`} />
|
||
</div>
|
||
</div>
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:14px;padding:12px">
|
||
<p style="margin:0;font-size:11px;letter-spacing:0.06em;text-transform:uppercase;color:#9CA3AF">Locked Credits</p>
|
||
<p style="margin:8px 0 0;font-size:30px;line-height:1;font-weight:800;color:#111827">{lockedLeadCredits()}</p>
|
||
<p style="margin:6px 0 0;font-size:12px;color:#6B7280">Held until service seeker decision (auto-refund in 1 day)</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:16px;overflow:hidden;box-shadow:0 1px 4px rgba(0,0,0,0.06)">
|
||
<div style="padding:10px 12px;border-bottom:1px solid #E5E7EB;display:flex;justify-content:space-between;align-items:center">
|
||
<p style="margin:0;font-size:18px;font-weight:800;color:#111827">Requested Contacts</p>
|
||
<div style="display:flex;gap:8px">
|
||
<div style="position:relative">
|
||
<button type="button" onClick={() => { setRequestedSortOpen((prev) => !prev); setRequestedFilterOpen(false); }} style="display:inline-flex;height:32px;align-items:center;gap:6px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 10px;font-size:12px;color:#374151;font-weight:600">
|
||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#FF5E13" 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={requestedSortOpen()}>
|
||
<div style="position:absolute;right:0;top:36px;z-index:20;min-width:170px;border:1px solid #E5E7EB;border-radius:10px;background:white;padding:6px;box-shadow:0 8px 24px rgba(0,0,0,0.12)">
|
||
{['Newest First', 'Oldest First'].map((item) => (
|
||
<button type="button" onClick={() => { setRequestedSortFilter(item); setRequestedSortOpen(false); }} style={`display:block;width:100%;text-align:left;border:none;background:${requestedSortFilter() === item ? '#FFF1EB' : 'transparent'};color:${requestedSortFilter() === item ? '#FF5E13' : '#374151'};padding:8px 10px;border-radius:8px;font-size:12px;font-weight:600;cursor:pointer`}>
|
||
{item}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</Show>
|
||
</div>
|
||
<div style="position:relative">
|
||
<button type="button" onClick={() => { setRequestedFilterOpen((prev) => !prev); setRequestedSortOpen(false); }} style="display:inline-flex;height:32px;align-items:center;gap:6px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 10px;font-size:12px;color:#374151;font-weight:600">
|
||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#FF5E13" stroke-width="2"><path d="M3 5h18M6 12h12M10 19h4"/></svg>
|
||
Filter
|
||
</button>
|
||
<Show when={requestedFilterOpen()}>
|
||
<div style="position:absolute;right:0;top:36px;z-index:20;min-width:220px;border:1px solid #E5E7EB;border-radius:10px;background:white;padding:6px;box-shadow:0 8px 24px rgba(0,0,0,0.12)">
|
||
{['All Status', 'Request Sent', 'Contact Unlocked', 'Rejected', 'Expired Refunded', 'Cancelled By Professional'].map((item) => (
|
||
<button type="button" onClick={() => { setRequestedStatusFilter(item); setRequestedFilterOpen(false); }} style={`display:block;width:100%;text-align:left;border:none;background:${requestedStatusFilter() === item ? '#FFF1EB' : 'transparent'};color:${requestedStatusFilter() === item ? '#FF5E13' : '#374151'};padding:8px 10px;border-radius:8px;font-size:12px;font-weight:600;cursor:pointer`}>
|
||
{item}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</Show>
|
||
</div>
|
||
<button type="button" style="height:32px;border-radius:8px;border:none;background:#03004E;padding:0 10px;font-size:12px;font-weight:700;color:white">Export</button>
|
||
</div>
|
||
</div>
|
||
<div style="padding:10px 12px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;flex-wrap:wrap">
|
||
<div style="height:32px;min-width:220px;flex:1;border:1px solid #E5E7EB;border-radius:8px;background:#F9FAFB;padding:0 10px;display:flex;align-items:center;gap:8px;font-size:12px;color:#9CA3AF">
|
||
<Search size={14} />
|
||
<input value={requestedSearch()} onInput={(e) => setRequestedSearch(e.currentTarget.value)} placeholder="Search by lead ID / title / area..." style="border:none;background:transparent;outline:none;width:100%;font-size:12px;color:#111827" />
|
||
</div>
|
||
<span style="height:24px;padding:0 10px;border-radius:999px;border:1px solid #E5E7EB;background:#F9FAFB;display:inline-flex;align-items:center;font-size:11px;font-weight:700;color:#6B7280">Sort: {requestedSortFilter()}</span>
|
||
<span style="height:24px;padding:0 10px;border-radius:999px;border:1px solid #E5E7EB;background:#F9FAFB;display:inline-flex;align-items:center;font-size:11px;font-weight:700;color:#6B7280">Status: {requestedStatusFilter()}</span>
|
||
</div>
|
||
<div style="max-height:360px;overflow:auto">
|
||
<table style="width:100%;border-collapse:collapse">
|
||
<thead style="background:#03004E;color:white">
|
||
<tr>
|
||
{['Lead ID', 'Lead Title', 'Request Date', 'Request Status', 'Cost', 'Decision Date', 'Action'].map((h) => (
|
||
<th style="padding:10px;text-align:left;font-size:10px;letter-spacing:0.06em;text-transform:uppercase">{h}</th>
|
||
))}
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<For each={pagedRequestedRows()}>
|
||
{(row) => {
|
||
const badge = requestStatusPill(row.status);
|
||
return (
|
||
<tr style="border-top:1px solid #F3F4F6">
|
||
<td style="padding:10px;font-size:12px;color:#4B5563;font-weight:700">#{row.id}</td>
|
||
<td style="padding:10px">
|
||
<p style="margin:0;font-size:14px;font-weight:700;color:#111827">{row.title}</p>
|
||
<p style="margin:2px 0 0;font-size:12px;color:#9CA3AF">{row.city}</p>
|
||
</td>
|
||
<td style="padding:10px;font-size:12px;color:#374151">{row.requestDate}</td>
|
||
<td style="padding:10px"><span style={`height:22px;padding:0 10px;border-radius:999px;display:inline-flex;align-items:center;font-size:11px;font-weight:700;background:${badge.bg};color:${badge.c}`}>{badge.text}</span></td>
|
||
<td style="padding:10px;font-size:14px;font-weight:700;color:#111827">{leadCostPerContact}</td>
|
||
<td style="padding:10px;font-size:12px;color:#6B7280">{row.decisionDate}</td>
|
||
<td style="padding:10px">
|
||
<div style="display:flex;gap:6px;flex-wrap:wrap">
|
||
<Show when={row.status === 'request_sent'}>
|
||
<button type="button" onClick={() => approveLeadContact(row.id)} style="height:28px;border-radius:8px;border:none;background:#03004E;padding:0 10px;font-size:11px;font-weight:700;color:white">Approve (Demo)</button>
|
||
<button type="button" onClick={() => cancelLeadRequest(row.id)} style="height:28px;border-radius:8px;border:1px solid #FECACA;background:#FEF2F2;padding:0 10px;font-size:11px;font-weight:700;color:#B91C1C">Cancel Request</button>
|
||
<button type="button" onClick={() => refundPendingLead(row.id)} style="height:28px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 10px;font-size:11px;font-weight:700;color:#374151">Refund After 1 Day</button>
|
||
</Show>
|
||
<Show when={row.status === 'contact_unlocked'}>
|
||
<button type="button" style="height:28px;border-radius:8px;border:none;background:#FF5E13;padding:0 10px;font-size:11px;font-weight:700;color:white">View Data</button>
|
||
</Show>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
);
|
||
}}
|
||
</For>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div style="padding:10px 12px;border-top:1px solid #E5E7EB;display:flex;justify-content:space-between;align-items:center">
|
||
<p style="margin:0;font-size:12px;color:#6B7280">
|
||
Showing {pagedRequestedRows().length ? (requestedPage() - 1) * requestedPerPage + 1 : 0} to {(requestedPage() - 1) * requestedPerPage + pagedRequestedRows().length} of {filteredRequestedRows().length} requests
|
||
</p>
|
||
<div style="display:flex;gap:6px">
|
||
<For each={Array.from({ length: totalRequestedPages() }, (_, i) => i + 1)}>
|
||
{(pageNo) => (
|
||
<button
|
||
type="button"
|
||
onClick={() => setRequestedPage(pageNo)}
|
||
style={`width:30px;height:30px;border-radius:8px;border:1px solid #E5E7EB;background:${requestedPage() === pageNo ? '#FF5E13' : 'white'};color:${requestedPage() === pageNo ? 'white' : '#6B7280'};font-size:12px;font-weight:700`}
|
||
>
|
||
{pageNo}
|
||
</button>
|
||
)}
|
||
</For>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (leadMarketplaceTab() === 'View Details' && selectedLead()) {
|
||
const lead = selectedLead()!;
|
||
const spec = leadDetailsSpec(lead);
|
||
return (
|
||
<div style="display:flex;flex-direction:column;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)">
|
||
<div style="border:1px solid #E5E7EB;border-radius:12px;background:linear-gradient(135deg,#FFF8F4 0%, #FFFFFF 70%);padding:14px">
|
||
<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:10px;flex-wrap:wrap">
|
||
<div>
|
||
<p style="margin:0;font-size:11px;letter-spacing:0.06em;text-transform:uppercase;color:#6B7280">View Details</p>
|
||
<p style="margin:4px 0 0;font-size:26px;font-weight:800;color:#111827;line-height:1.2">{lead.title}</p>
|
||
<p style="margin:6px 0 0;font-size:12px;color:#6B7280">{lead.category} • {lead.location} • {lead.area}</p>
|
||
</div>
|
||
<span style="height:24px;padding:0 10px;border-radius:999px;background:#FFF1EB;color:#C2410C;display:inline-flex;align-items:center;font-size:11px;font-weight:700">{lead.match}</span>
|
||
</div>
|
||
<div style="margin-top:10px;display:flex;align-items:center;gap:8px;flex-wrap:wrap">
|
||
<span style="height:24px;padding:0 8px;border-radius:999px;border:1px solid #E5E7EB;background:white;font-size:11px;color:#374151;display:inline-flex;align-items:center;gap:4px"><Calendar size={11} style="color:#6B7280" /><strong style="font-weight:700;color:#6B7280">Time Frame:</strong>{spec.timeframe}</span>
|
||
<span style="height:24px;padding:0 8px;border-radius:999px;border:1px solid #E5E7EB;background:white;font-size:11px;color:#374151;display:inline-flex;align-items:center;gap:4px"><Calendar size={11} style="color:#6B7280" /><strong style="font-weight:700;color:#6B7280">Date:</strong>{lead.dateRequired}</span>
|
||
<span style="height:24px;padding:0 8px;border-radius:999px;border:1px solid #E5E7EB;background:white;font-size:11px;color:#374151;display:inline-flex;align-items:center;gap:4px"><Coins size={11} style="color:#6B7280" /><strong style="font-weight:700;color:#6B7280">Budget:</strong>{lead.budget}</span>
|
||
<span style="height:24px;padding:0 8px;border-radius:999px;border:1px solid #E5E7EB;background:white;font-size:11px;color:#374151;display:inline-flex;align-items:center;gap:4px"><MapPin size={11} style="color:#6B7280" /><strong style="font-weight:700;color:#6B7280">Area:</strong>{String(lead.area || 'Chennai')}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="margin-top:10px;display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px">
|
||
<div style="border:1px solid #E5E7EB;border-radius:10px;background:white;padding:10px;display:flex;align-items:flex-start;gap:8px">
|
||
<span style="width:24px;height:24px;border-radius:999px;background:#FFF1EB;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0"><Calendar size={13} style="color:#FF5E13" /></span>
|
||
<div><p style="margin:0;font-size:11px;color:#6B7280;font-weight:700">Schedule</p><p style="margin:3px 0 0;font-size:13px;font-weight:800;color:#111827;line-height:1.35">{spec.timeframe}</p></div>
|
||
</div>
|
||
<div style="border:1px solid #E5E7EB;border-radius:10px;background:white;padding:10px;display:flex;align-items:flex-start;gap:8px">
|
||
<span style="width:24px;height:24px;border-radius:999px;background:#FFF1EB;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0"><Calendar size={13} style="color:#FF5E13" /></span>
|
||
<div><p style="margin:0;font-size:11px;color:#6B7280;font-weight:700">Date Required</p><p style="margin:3px 0 0;font-size:13px;font-weight:800;color:#111827">{lead.dateRequired}</p></div>
|
||
</div>
|
||
<div style="border:1px solid #E5E7EB;border-radius:10px;background:white;padding:10px;display:flex;align-items:flex-start;gap:8px">
|
||
<span style="width:24px;height:24px;border-radius:999px;background:#FFF1EB;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0"><MapPin size={13} style="color:#FF5E13" /></span>
|
||
<div><p style="margin:0;font-size:11px;color:#6B7280;font-weight:700">Area</p><p style="margin:3px 0 0;font-size:13px;font-weight:800;color:#111827">{String(lead.area || 'Chennai')}</p></div>
|
||
</div>
|
||
<div style="border:1px solid #E5E7EB;border-radius:10px;background:white;padding:10px;display:flex;align-items:flex-start;gap:8px">
|
||
<span style="width:24px;height:24px;border-radius:999px;background:#FFF1EB;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0"><TrendingUp size={13} style="color:#FF5E13" /></span>
|
||
<div style="min-width:0;flex:1">
|
||
<p style="margin:0;font-size:11px;color:#6B7280;font-weight:700">Win Probability</p>
|
||
<p style={`margin:3px 0 0;font-size:13px;font-weight:800;color:${leadProbabilityColor(leadProbability(lead))}`}>{leadProbability(lead)}%</p>
|
||
<div style="margin-top:5px;height:5px;border-radius:999px;background:#E5E7EB;overflow:hidden">
|
||
<div style={`height:100%;width:${leadProbability(lead)}%;background:${leadProbabilityColor(leadProbability(lead))}`} />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div style="margin-top:10px;display:grid;grid-template-columns:2fr 1fr;gap:10px">
|
||
<div style="display:grid;gap:10px">
|
||
<div style="border:1px solid #E5E7EB;border-radius:12px;background:white;padding:12px">
|
||
<p style="margin:0;font-size:11px;letter-spacing:0.05em;text-transform:uppercase;color:#6B7280;font-weight:700">Work Scope</p>
|
||
<p style="margin:8px 0 0;font-size:14px;color:#1F2937;line-height:1.65">{spec.scope}</p>
|
||
</div>
|
||
<div style="border:1px solid #E5E7EB;border-radius:12px;background:#F9FAFB;padding:12px">
|
||
<p style="margin:0 0 8px;font-size:11px;letter-spacing:0.05em;text-transform:uppercase;color:#6B7280;font-weight:700">Lead Specific Highlights</p>
|
||
<div style="display:grid;gap:6px">
|
||
<For each={spec.highlights}>{(item) => <div style="display:flex;align-items:flex-start;gap:6px"><span style="margin-top:3px;width:6px;height:6px;border-radius:999px;background:#FF5E13;flex-shrink:0" /><span style="font-size:14px;color:#1F2937;line-height:1.55">{item}</span></div>}</For>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div style="border:1px solid #E5E7EB;border-radius:12px;background:white;padding:12px;display:grid;gap:8px;align-content:start">
|
||
<div style="display:flex;justify-content:space-between;gap:8px;padding-bottom:6px;border-bottom:1px solid #F3F4F6"><span style="font-size:12px;color:#6B7280">Lead ID</span><span style="font-size:12px;color:#111827;font-weight:700">{lead.id}</span></div>
|
||
<div style="display:flex;justify-content:space-between;gap:8px;padding-bottom:6px;border-bottom:1px solid #F3F4F6">
|
||
<span style="font-size:12px;color:#6B7280">Unlock Cost</span>
|
||
<span style="height:22px;padding:0 8px;border-radius:999px;border:1px solid #FFD8C2;background:#FFF1EB;display:inline-flex;align-items:center;gap:6px"><span style="width:14px;height:14px;border-radius:999px;background:#FF5E13;color:white;display:inline-flex;align-items:center;justify-content:center;font-size:9px;font-weight:800;line-height:1">NG</span><span style="font-size:12px;color:#C2410C;font-weight:800">{lead.cost} Tracecoin</span></span>
|
||
</div>
|
||
<div style="display:flex;justify-content:space-between;gap:8px;padding-bottom:6px;border-bottom:1px solid #F3F4F6"><span style="font-size:12px;color:#6B7280">Contacted</span><span style="font-size:12px;color:#111827;font-weight:700">{lead.contactCount}/{lead.maxContacts}</span></div>
|
||
<div style="display:flex;gap:8px;margin-top:6px;flex-wrap:wrap">
|
||
<button type="button" onClick={() => { setLeadMarketplaceTab('All Leads'); setActiveLeadDetailId(''); }} style="height:32px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:700;color:#374151">Back to Leads</button>
|
||
<button type="button" onClick={() => openLeadContactConfirm(lead.id)} disabled={usableLeadCredits() < leadCostPerContact || lead.contactCount >= lead.maxContacts} style={`height:32px;border-radius:8px;border:none;padding:0 12px;font-size:12px;font-weight:700;color:white;background:${usableLeadCredits() < leadCostPerContact || lead.contactCount >= lead.maxContacts ? '#9CA3AF' : '#03004E'}`}>Request Contact</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div style="display:flex;flex-direction:column;gap:10px">
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:16px;overflow:hidden;box-shadow:0 1px 4px rgba(0,0,0,0.06)">
|
||
<div style="padding:10px 12px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;flex-wrap:wrap">
|
||
<For each={leadTabs()}>
|
||
{(item) => (
|
||
<button
|
||
type="button"
|
||
onClick={() => { setLeadMarketplaceTab(item); if (item !== 'View Details') setActiveLeadDetailId(''); }}
|
||
style={`height:30px;padding:0 8px;border:none;border-bottom:${leadMarketplaceTab() === item ? '2px solid #FF5E13' : '2px solid transparent'};background:none;font-size:12px;font-weight:700;color:${leadMarketplaceTab() === item ? '#FF5E13' : '#6B7280'}`}
|
||
>
|
||
{item}
|
||
</button>
|
||
)}
|
||
</For>
|
||
</div>
|
||
<div style="padding:10px 12px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;flex-wrap:wrap">
|
||
<div style="height:34px;min-width:220px;flex:1;border:1px solid #E5E7EB;border-radius:8px;background:#F9FAFB;padding:0 10px;display:flex;align-items:center;gap:8px;font-size:12px;color:#9CA3AF">
|
||
<Search size={14} />
|
||
<input value={leadSearch()} onInput={(e) => setLeadSearch(e.currentTarget.value)} placeholder="Search by role, area, keyword..." style="border:none;background:transparent;outline:none;width:100%;font-size:12px;color:#111827" />
|
||
</div>
|
||
<div style="position:relative">
|
||
<button
|
||
type="button"
|
||
onClick={() => { setLeadFiltersOpen((prev) => !prev); setLeadSortOpen(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;color:#374151;font-weight:600"
|
||
>
|
||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#FF5E13" stroke-width="2"><path d="M3 5h18M6 12h12M10 19h4"/></svg>
|
||
Filters
|
||
</button>
|
||
<Show when={leadFiltersOpen()}>
|
||
<div style="position:absolute;right:0;top:38px;z-index:20;min-width:280px;border:1px solid #E5E7EB;border-radius:12px;background:white;box-shadow:0 8px 24px rgba(0,0,0,0.12);padding:10px;display:grid;gap:8px">
|
||
<div>
|
||
<p style="margin:0 0 4px;font-size:10px;letter-spacing:0.05em;text-transform:uppercase;color:#9CA3AF;font-weight:700">Area</p>
|
||
<select value={leadAreaFilter()} onChange={(e) => setLeadAreaFilter(e.currentTarget.value)} style="height:34px;width:100%;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 10px;font-size:12px;color:#374151">
|
||
<option>All Areas</option>
|
||
<option>ECR</option>
|
||
<option>Nungambakkam</option>
|
||
<option>Sholinganallur</option>
|
||
<option>Mylapore</option>
|
||
<option>T Nagar</option>
|
||
<option>Guindy</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<p style="margin:0 0 4px;font-size:10px;letter-spacing:0.05em;text-transform:uppercase;color:#9CA3AF;font-weight:700">Budget</p>
|
||
<select value={leadBudgetFilter()} onChange={(e) => setLeadBudgetFilter(e.currentTarget.value)} style="height:34px;width:100%;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 10px;font-size:12px;color:#374151">
|
||
<option>All Budgets</option>
|
||
<option>Under ₹1L</option>
|
||
<option>₹1L - ₹2L</option>
|
||
<option>Above ₹2L</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<p style="margin:0 0 4px;font-size:10px;letter-spacing:0.05em;text-transform:uppercase;color:#9CA3AF;font-weight:700">Date</p>
|
||
<select value={leadDateFilter()} onChange={(e) => setLeadDateFilter(e.currentTarget.value)} style="height:34px;width:100%;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 10px;font-size:12px;color:#374151">
|
||
<option>Any Date</option>
|
||
<option>Within 7 Days</option>
|
||
<option>This Month</option>
|
||
</select>
|
||
</div>
|
||
<div style="display:flex;justify-content:flex-end;gap:8px;padding-top:4px">
|
||
<button type="button" onClick={() => { setLeadAreaFilter('All Areas'); setLeadBudgetFilter('All Budgets'); setLeadDateFilter('Any Date'); }} style="height:30px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 10px;font-size:11px;color:#374151;font-weight:700">Reset</button>
|
||
<button type="button" onClick={() => setLeadFiltersOpen(false)} style="height:30px;border-radius:8px;border:none;background:#03004E;color:white;padding:0 10px;font-size:11px;font-weight:700">Apply</button>
|
||
</div>
|
||
</div>
|
||
</Show>
|
||
</div>
|
||
<div style="position:relative">
|
||
<button
|
||
type="button"
|
||
onClick={() => { setLeadSortOpen((prev) => !prev); setLeadFiltersOpen(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;color:#374151;font-weight:600"
|
||
>
|
||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#FF5E13" 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={leadSortOpen()}>
|
||
<div style="position:absolute;right:0;top:38px;z-index:20;min-width:190px;border:1px solid #E5E7EB;border-radius:10px;background:white;padding:6px;box-shadow:0 8px 24px rgba(0,0,0,0.12)">
|
||
{['Newest First', 'Budget High-Low', 'Budget Low-High'].map((item) => (
|
||
<button type="button" onClick={() => { setLeadSortFilter(item); setLeadSortOpen(false); }} style={`display:block;width:100%;text-align:left;border:none;background:${leadSortFilter() === item ? '#FFF1EB' : 'transparent'};color:${leadSortFilter() === item ? '#FF5E13' : '#374151'};padding:8px 10px;border-radius:8px;font-size:12px;font-weight:600;cursor:pointer`}>
|
||
{item}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</Show>
|
||
</div>
|
||
<span style="height:24px;padding:0 10px;border-radius:999px;border:1px solid #E5E7EB;background:#F9FAFB;display:inline-flex;align-items:center;font-size:11px;font-weight:700;color:#6B7280">Sort: {leadSortFilter()}</span>
|
||
</div>
|
||
|
||
<div style="padding:12px;display:grid;gap:10px;background:#FAFBFF;max-height:520px;overflow:auto">
|
||
<For each={pagedLeadCards()}>
|
||
{(lead) => (
|
||
<div style={`border:1px solid ${lead.status === 'requested' ? '#FFE2D3' : lead.status === 'unlocked' ? '#CFF5E9' : lead.status === 'closed' ? '#F3F4F6' : '#E5E7EB'};border-radius:14px;background:white;padding:14px 16px`}>
|
||
<div style="display:grid;grid-template-columns:minmax(0,1fr) auto;gap:12px;align-items:start">
|
||
<div style="min-width:0">
|
||
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">
|
||
<span style={`height:20px;padding:0 8px;border-radius:999px;font-size:10px;font-weight:700;display:inline-flex;align-items:center;background:${lead.status === 'unlocked' ? '#ECFDF3' : lead.status === 'requested' ? '#FFF1EB' : lead.status === 'closed' ? '#F3F4F6' : '#EEF2FF'};color:${lead.status === 'unlocked' ? '#047857' : lead.status === 'requested' ? '#C2410C' : lead.status === 'closed' ? '#6B7280' : '#3730A3'}`}>
|
||
{lead.status === 'unlocked' ? 'Contact Unlocked' : lead.status === 'requested' ? 'Request Sent' : lead.status === 'closed' ? 'Lead Closed' : 'Open Lead'}
|
||
</span>
|
||
<span style="font-size:12px;color:#6B7280">{lead.category} • {lead.location}</span>
|
||
</div>
|
||
<p style="margin:6px 0 0;font-size:20px;line-height:1.3;font-weight:800;color:#111827">{lead.title}</p>
|
||
<div style="margin-top:10px;display:flex;align-items:center;gap:12px;white-space:nowrap;overflow-x:auto;padding-bottom:2px">
|
||
<span style="font-size:12px;color:#111827;font-weight:700">Area: {lead.area}</span>
|
||
<span style="font-size:12px;color:#D1D5DB">|</span>
|
||
<span style="font-size:12px;color:#111827;font-weight:700">Date: {lead.dateRequired}</span>
|
||
<span style="font-size:12px;color:#D1D5DB">|</span>
|
||
<span style="height:24px;display:inline-flex;align-items:center;gap:6px;padding:0 8px;border:1px solid #FFE2D3;border-radius:999px;background:#FFF7F2">
|
||
<span style="width:14px;height:14px;border-radius:999px;background:#FF5E13;color:white;display:inline-flex;align-items:center;justify-content:center;font-size:10px;font-weight:800;line-height:1">T</span>
|
||
<span style="font-size:12px;color:#C2410C;font-weight:800">{lead.cost}</span>
|
||
</span>
|
||
<span style="font-size:12px;color:#D1D5DB">|</span>
|
||
<span style="display:inline-flex;align-items:center;gap:6px">
|
||
<svg width="62" height="34" viewBox="0 0 120 70" aria-label="Lead probability gauge">
|
||
<path d="M 10 60 A 50 50 0 0 1 110 60" fill="none" stroke="#E5E7EB" stroke-width="8" stroke-linecap="round" />
|
||
<path d="M 10 60 A 50 50 0 0 1 110 60" fill="none" stroke={leadProbabilityColor(leadProbability(lead))} stroke-width="8" stroke-linecap="round" stroke-dasharray={leadGaugeDash(leadProbability(lead))} />
|
||
<line x1="60" y1="60" x2={leadGaugeNeedlePoint(leadProbability(lead)).x} y2={leadGaugeNeedlePoint(leadProbability(lead)).y} stroke="#111827" stroke-width="3" stroke-linecap="round" />
|
||
<circle cx="60" cy="60" r="4.5" fill="#111827" />
|
||
</svg>
|
||
<span style={`font-size:12px;font-weight:800;color:${leadProbabilityColor(leadProbability(lead))}`}>{leadProbability(lead)}%</span>
|
||
</span>
|
||
<span style="font-size:12px;color:#D1D5DB">|</span>
|
||
<span style="height:24px;display:inline-flex;align-items:center;gap:6px;padding:0 8px;border:1px solid #E5E7EB;border-radius:999px;background:#F9FAFB">
|
||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="#6B7280" stroke-width="2"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="8.5" cy="7" r="4"/><path d="M20 8v6"/><path d="M23 11h-6"/></svg>
|
||
<span style="font-size:11px;color:#111827;font-weight:700">{lead.contactCount}/{lead.maxContacts}</span>
|
||
<span style="font-size:10px;color:#6B7280">contacted</span>
|
||
</span>
|
||
<span style="font-size:12px;color:#6B7280;font-weight:700">{Math.max(0, lead.maxContacts - lead.contactCount)} slots left</span>
|
||
</div>
|
||
</div>
|
||
<div style="display:flex;flex-direction:column;align-items:flex-end;gap:8px">
|
||
<p style="margin:0;font-size:11px;letter-spacing:0.06em;text-transform:uppercase;color:#FF5E13;font-weight:700">{lead.match}</p>
|
||
<p style="margin:0;font-size:24px;line-height:1;font-weight:800;color:#111827">{lead.budget}</p>
|
||
<p style="margin:0;font-size:11px;color:#9CA3AF">Est. Budget</p>
|
||
<div style="display:flex;gap:6px;flex-wrap:wrap;justify-content:flex-end;padding-top:2px">
|
||
<button type="button" onClick={() => openLeadDetailsInNewTab(lead.id)} style="height:30px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 10px;font-size:12px;font-weight:700;color:#374151">View Details</button>
|
||
<Show when={lead.status === 'open'}>
|
||
<button
|
||
type="button"
|
||
onClick={() => openLeadContactConfirm(lead.id)}
|
||
disabled={usableLeadCredits() < leadCostPerContact || lead.contactCount >= lead.maxContacts}
|
||
style={`height:30px;border-radius:8px;border:none;padding:0 10px;font-size:12px;font-weight:700;color:white;background:${usableLeadCredits() < leadCostPerContact || lead.contactCount >= lead.maxContacts ? '#9CA3AF' : '#03004E'};cursor:${usableLeadCredits() < leadCostPerContact || lead.contactCount >= lead.maxContacts ? 'not-allowed' : 'pointer'}`}
|
||
>
|
||
Request Contact
|
||
</button>
|
||
</Show>
|
||
<Show when={lead.status === 'requested'}>
|
||
<button type="button" onClick={() => cancelLeadRequest(lead.id)} style="height:30px;border-radius:8px;border:1px solid #FECACA;background:#FEF2F2;padding:0 10px;font-size:12px;font-weight:700;color:#B91C1C">Cancel Request</button>
|
||
</Show>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</For>
|
||
</div>
|
||
<div style="padding:10px 12px;border-top:1px solid #E5E7EB;display:flex;justify-content:space-between;align-items:center">
|
||
<p style="margin:0;font-size:12px;color:#6B7280">
|
||
Showing {pagedLeadCards().length ? (leadPage() - 1) * leadsPerPage + 1 : 0} to {(leadPage() - 1) * leadsPerPage + pagedLeadCards().length} of {filteredLeadCards().length} leads
|
||
</p>
|
||
<div style="display:flex;gap:6px">
|
||
<For each={Array.from({ length: totalLeadPages() }, (_, i) => i + 1)}>
|
||
{(pageNo) => (
|
||
<button
|
||
type="button"
|
||
onClick={() => setLeadPage(pageNo)}
|
||
style={`width:30px;height:30px;border-radius:8px;border:1px solid #E5E7EB;background:${leadPage() === pageNo ? '#FF5E13' : 'white'};color:${leadPage() === pageNo ? 'white' : '#6B7280'};font-size:12px;font-weight:700`}
|
||
>
|
||
{pageNo}
|
||
</button>
|
||
)}
|
||
</For>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<Show when={leadContactConfirmId()}>
|
||
<div style="position:fixed;inset:0;background:rgba(15,23,42,0.45);display:flex;align-items:center;justify-content:center;z-index:60;padding:16px">
|
||
<div style="width:min(460px,100%);border:1px solid #E5E7EB;background:white;border-radius:14px;box-shadow:0 10px 30px rgba(0,0,0,0.18);padding:16px">
|
||
<div style="display:flex;align-items:center;gap:8px">
|
||
<span style="width:22px;height:22px;border-radius:999px;background:#FFF1EB;color:#FF5E13;display:inline-flex;align-items:center;justify-content:center;font-size:13px;font-weight:800">T</span>
|
||
<p style="margin:0;font-size:16px;font-weight:800;color:#111827">Confirm Contact Unlock</p>
|
||
</div>
|
||
<p style="margin:10px 0 0;font-size:13px;color:#374151;line-height:1.5">You are about to spend <strong>25 Tracecoins</strong> to request and view this service seeker contact when approved. Do you want to continue?</p>
|
||
<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:14px">
|
||
<button type="button" onClick={() => setLeadContactConfirmId('')} 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={confirmLeadContactRequest} style="height:34px;border-radius:8px;border:none;background:#03004E;color:white;padding:0 14px;font-size:12px;font-weight:700">Yes, Request Contact</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Show>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (customerKey() === 'jobs') {
|
||
if (isJobSeekerRole()) {
|
||
const selectedJob = () => jobBoardJobs().find((job) => job.id === jobSeekerSelectedId()) || jobBoardJobs()[0];
|
||
const selectedAppliedTab = normalizeTabKey(tab) === 'applied';
|
||
const selectedOpenJobCards = () => {
|
||
const tabKey = normalizeTabKey(tab);
|
||
const rows = jobBoardJobs();
|
||
if (tabKey === 'recommended') return rows.slice(0, 3);
|
||
if (tabKey === 'saved') return rows.slice(1);
|
||
if (tabKey === 'expiring soon') return rows.filter((_, idx) => idx % 2 === 0);
|
||
return rows;
|
||
};
|
||
const stepWidth = () => `${Math.round((jobSeekerApplyStep() / 4) * 100)}%`;
|
||
|
||
if (selectedAppliedTab) {
|
||
return (
|
||
<div style="display:flex;flex-direction:column;gap:10px">
|
||
<div style="display:flex;justify-content:space-between;align-items:center">
|
||
<div style="display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px;flex:1">
|
||
{[
|
||
{ label: 'Total Applications', value: '24', tone: 'dark' },
|
||
{ label: 'Under Review', value: '08', tone: 'blue' },
|
||
{ label: 'Shortlisted', value: '03', tone: 'orange' },
|
||
{ label: 'Interviews', value: '02', tone: 'green' },
|
||
].map((card) => (
|
||
<div style={`border:1px solid #E5E7EB;border-radius:12px;padding:10px;background:${card.tone === 'dark' ? '#03004E' : 'white'};color:${card.tone === 'dark' ? 'white' : '#111827'};box-shadow:0 1px 3px rgba(0,0,0,0.05)`}>
|
||
<p style={`margin:0;font-size:11px;letter-spacing:0.05em;text-transform:uppercase;color:${card.tone === 'dark' ? '#D7DBFF' : '#9CA3AF'}`}>{card.label}</p>
|
||
<p style={`margin:4px 0 0;font-size:32px;line-height:1;font-weight:800;color:${card.tone === 'orange' ? '#C2410C' : card.tone === 'green' ? '#16A34A' : card.tone === 'blue' ? '#2563EB' : card.tone === 'dark' ? 'white' : '#111827'}`}>{card.value}</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
<div style="display:flex;gap:8px;margin-left:10px">
|
||
<button type="button" style="height:34px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 10px;font-size:12px;font-weight:700;color:#374151">Filters</button>
|
||
<button type="button" style="height:34px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 10px;font-size:12px;font-weight:700;color:#374151">Most Recent</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="border:1px solid #E5E7EB;border-radius:14px;background:white;overflow:hidden;box-shadow:0 1px 4px rgba(0,0,0,0.06)">
|
||
<div style="padding:10px 12px;border-bottom:1px solid #E5E7EB;display:flex;justify-content:space-between;align-items:center">
|
||
<p style="margin:0;font-size:14px;font-weight:800;color:#111827;letter-spacing:0.05em;text-transform:uppercase">Latest Updates</p>
|
||
<span style="font-size:11px;color:#9CA3AF">Track your application status and recruiter responses</span>
|
||
</div>
|
||
<div style="display:grid;gap:8px;padding:10px">
|
||
<For each={JOB_SEEKER_APPLIED_ROWS}>
|
||
{(row) => {
|
||
const tone = row.status === 'Shortlisted'
|
||
? { bg: '#FFF1EB', c: '#C2410C' }
|
||
: row.status === 'Under Review'
|
||
? { bg: '#E8F0FF', c: '#2563EB' }
|
||
: row.status === 'Not Selected'
|
||
? { bg: '#FEE2E2', c: '#B91C1C' }
|
||
: { bg: '#F3F4F6', c: '#374151' };
|
||
return (
|
||
<div style={`border:1px solid #E5E7EB;border-left:4px solid ${tone.c};border-radius:10px;background:#FCFCFD;padding:10px;display:grid;grid-template-columns:1fr auto auto;gap:10px;align-items:center`}>
|
||
<div>
|
||
<p style="margin:0;font-size:10px;letter-spacing:0.05em;text-transform:uppercase;color:#6B7280">{row.id} • Applied On Oct 12, 2023</p>
|
||
<p style="margin:4px 0 0;font-size:28px;line-height:1.2;font-weight:800;color:#111827">{row.title}</p>
|
||
<p style="margin:4px 0 0;font-size:13px;color:#4B5563">{row.company} • {row.location}</p>
|
||
</div>
|
||
<div style="text-align:right">
|
||
<span style={`height:24px;padding:0 10px;border-radius:999px;background:${tone.bg};color:${tone.c};font-size:11px;font-weight:700;display:inline-flex;align-items:center`}>{row.status}</span>
|
||
<p style="margin:6px 0 0;font-size:12px;color:#6B7280">{row.note}</p>
|
||
</div>
|
||
<button type="button" style="height:32px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 10px;font-size:12px;font-weight:700;color:#374151">View Details</button>
|
||
</div>
|
||
);
|
||
}}
|
||
</For>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="border:1px solid #FFD8C2;border-radius:14px;background:#FFF8F4;padding:14px;display:flex;justify-content:space-between;align-items:center;gap:10px">
|
||
<div>
|
||
<p style="margin:0;font-size:30px;line-height:1.2;font-weight:800;color:#111827">Boost your response rate</p>
|
||
<p style="margin:6px 0 0;font-size:13px;color:#6B7280">Recruiters are more likely to respond to profiles with updated resume and portfolio links.</p>
|
||
</div>
|
||
<button type="button" onClick={() => setJobSeekerScreen('apply')} style="height:34px;border:none;border-radius:9px;background:#03004E;color:white;padding:0 12px;font-size:12px;font-weight:700;white-space:nowrap">Optimize Profile</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (jobSeekerScreen() === 'apply') {
|
||
return (
|
||
<div style="border:1px solid #E5E7EB;border-radius:16px;background:white;box-shadow:0 1px 4px rgba(0,0,0,0.06);overflow:hidden">
|
||
<div style="display:grid;grid-template-columns:280px 1fr;min-height:520px">
|
||
<aside style="background:#03004E;color:white;padding:14px;display:flex;flex-direction:column;justify-content:space-between">
|
||
<div>
|
||
<p style="margin:0;font-size:36px;line-height:1.1;font-weight:800">Nxtgauge</p>
|
||
<p style="margin:12px 0 0;font-size:10px;letter-spacing:0.08em;text-transform:uppercase;color:#A7B2FF">Applying For</p>
|
||
<p style="margin:4px 0 0;font-size:42px;line-height:1.1;font-weight:800">{selectedJob().title}</p>
|
||
<p style="margin:8px 0 0;font-size:13px;color:#D7DBFF">Full-time • Remote • {selectedJob().salary}</p>
|
||
<div style="margin-top:14px;border:1px solid rgba(255,255,255,0.18);border-radius:10px;padding:10px;background:rgba(255,255,255,0.04)">
|
||
<p style="margin:0;font-size:12px;font-weight:700">{selectedJob().company}</p>
|
||
<p style="margin:3px 0 0;font-size:11px;color:#D7DBFF">{selectedJob().location}</p>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<p style="margin:0;font-size:12px;color:#D7DBFF">Step {jobSeekerApplyStep()} of 4</p>
|
||
<div style="height:4px;border-radius:999px;background:rgba(255,255,255,0.2);margin-top:6px;overflow:hidden"><div style={`height:100%;background:#FF5E13;width:${stepWidth()}`} /></div>
|
||
</div>
|
||
</aside>
|
||
<div style="padding:14px;display:flex;flex-direction:column;gap:12px">
|
||
<div style="display:flex;align-items:center;gap:8px;border-bottom:1px solid #E5E7EB;padding-bottom:10px">
|
||
{[
|
||
{ key: 1, label: 'Profile' },
|
||
{ key: 2, label: 'Materials' },
|
||
{ key: 3, label: 'Details' },
|
||
{ key: 4, label: 'Review' },
|
||
].map((step, idx, arr) => (
|
||
<>
|
||
<button
|
||
type="button"
|
||
onClick={() => setJobSeekerApplyStep(step.key)}
|
||
style={`height:28px;padding:0 10px;border-radius:999px;border:1px solid ${jobSeekerApplyStep() === step.key ? '#FF5E13' : '#E5E7EB'};background:${jobSeekerApplyStep() === step.key ? '#FFF1EB' : '#F9FAFB'};font-size:11px;font-weight:700;color:${jobSeekerApplyStep() === step.key ? '#C2410C' : '#6B7280'}`}
|
||
>
|
||
{step.key}. {step.label}
|
||
</button>
|
||
<Show when={idx < arr.length - 1}><span style="color:#D1D5DB">—</span></Show>
|
||
</>
|
||
))}
|
||
</div>
|
||
<p style="margin:0;font-size:38px;line-height:1.1;font-weight:800;color:#111827">Resume & Portfolio</p>
|
||
<p style="margin:0;font-size:14px;color:#6B7280">Showcase your best work and professional journey to the hiring team.</p>
|
||
|
||
<div>
|
||
<p style="margin:0 0 6px;font-size:11px;letter-spacing:0.06em;text-transform:uppercase;color:#6B7280">Resume (PDF)</p>
|
||
<div style="min-height:120px;border:1px dashed #F4B6A0;border-radius:12px;background:#FFFCFB;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px">
|
||
<FileText size={24} style="color:#C2410C" />
|
||
<p style="margin:0;font-size:14px;color:#111827">Drop your resume here or <span style="color:#C2410C;font-weight:700">browse</span></p>
|
||
<p style="margin:0;font-size:12px;color:#6B7280">Maximum file size: 5MB</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
|
||
<div style="grid-column:1 / -1">
|
||
<p style="margin:0 0 6px;font-size:11px;letter-spacing:0.06em;text-transform:uppercase;color:#6B7280">Portfolio Link</p>
|
||
<div style="height:40px;border:1px solid #E5E7EB;border-radius:10px;background:#F9FAFB;padding:0 10px;display:flex;align-items:center;font-size:13px;color:#6B7280">https://yourportfolio.com</div>
|
||
</div>
|
||
<div style="height:54px;border:1px solid #E5E7EB;border-radius:10px;background:#F9FAFB;padding:0 10px;display:flex;align-items:center;justify-content:space-between">
|
||
<div><p style="margin:0;font-size:12px;font-weight:700;color:#111827">Cover Letter</p><p style="margin:2px 0 0;font-size:11px;color:#6B7280">Optional doc</p></div>
|
||
<button type="button" style="width:20px;height:20px;border-radius:999px;border:none;background:#FFF1EB;color:#C2410C;font-size:14px;font-weight:700;line-height:1">+</button>
|
||
</div>
|
||
<div style="height:54px;border:1px solid #E5E7EB;border-radius:10px;background:#F9FAFB;padding:0 10px;display:flex;align-items:center;justify-content:space-between">
|
||
<div><p style="margin:0;font-size:12px;font-weight:700;color:#111827">GitHub Profile</p><p style="margin:2px 0 0;font-size:11px;color:#6B7280">Optional link</p></div>
|
||
<button type="button" style="width:20px;height:20px;border-radius:999px;border:none;background:#FFF1EB;color:#C2410C;font-size:14px;font-weight:700;line-height:1">+</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="display:flex;align-items:center;justify-content:space-between;border-top:1px solid #E5E7EB;padding-top:10px">
|
||
<button type="button" onClick={() => setJobSeekerScreen('detail')} style="height:34px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:700;color:#374151">Back</button>
|
||
<div style="display:flex;gap:8px">
|
||
<button type="button" style="height:34px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:700;color:#374151">Save Draft</button>
|
||
<button type="button" onClick={() => setJobSeekerApplyStep((prev) => Math.min(4, prev + 1))} style="height:34px;border-radius:8px;border:none;background:#C2410C;padding:0 14px;font-size:12px;font-weight:700;color:white">Continue</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (jobSeekerScreen() === 'detail') {
|
||
return (
|
||
<div style="display:grid;grid-template-columns:1.1fr 1fr;gap:10px;align-items:start">
|
||
<div style="border:1px solid #E5E7EB;border-radius:14px;background:white;padding:12px;display:flex;flex-direction:column;gap:8px;box-shadow:0 1px 4px rgba(0,0,0,0.06);max-height:78vh;overflow:auto">
|
||
<p style="margin:0;font-size:24px;line-height:1.15;font-weight:800;color:#111827">Available Positions</p>
|
||
<p style="margin:0;font-size:13px;color:#6B7280">Found 128 high-match opportunities for your profile.</p>
|
||
<For each={selectedOpenJobCards()}>
|
||
{(job) => (
|
||
<button
|
||
type="button"
|
||
onClick={() => setJobSeekerSelectedId(job.id)}
|
||
style={`text-align:left;border:1px solid ${job.id === selectedJob().id ? '#FF5E13' : '#E5E7EB'};border-radius:12px;background:${job.id === selectedJob().id ? '#FFFCFA' : '#FCFCFD'};padding:10px;display:grid;gap:6px`}
|
||
>
|
||
<div style="display:flex;justify-content:space-between;align-items:center">
|
||
<p style="margin:0;font-size:18px;line-height:1.25;font-weight:800;color:#111827">{job.title}</p>
|
||
<span style="height:20px;padding:0 8px;border-radius:999px;background:#FFF1EB;color:#C2410C;font-size:10px;font-weight:700;display:inline-flex;align-items:center">Top Match</span>
|
||
</div>
|
||
<p style="margin:0;font-size:13px;color:#4B5563">{job.company} • {job.location}</p>
|
||
<p style="margin:0;font-size:13px;color:#6B7280;line-height:1.45">We are looking for a visionary professional to lead core product experiences and collaborate across teams.</p>
|
||
<div style="display:flex;justify-content:space-between;align-items:center;gap:8px">
|
||
<div style="display:flex;flex-wrap:wrap;gap:6px">
|
||
<For each={job.tags.slice(0, 3)}>
|
||
{(tag) => <span style="height:22px;padding:0 8px;border-radius:7px;border:1px solid #E5E7EB;background:#F3F4F6;font-size:10px;font-weight:700;color:#4B5563;display:inline-flex;align-items:center">{tag}</span>}
|
||
</For>
|
||
</div>
|
||
<p style="margin:0;font-size:20px;line-height:1.1;font-weight:800;color:#111827">{job.salary}</p>
|
||
</div>
|
||
</button>
|
||
)}
|
||
</For>
|
||
</div>
|
||
<div style="border:1px solid #E5E7EB;border-radius:14px;background:white;box-shadow:0 1px 4px rgba(0,0,0,0.06);overflow:hidden;max-height:78vh;display:flex;flex-direction:column">
|
||
<div style="height:140px;background:linear-gradient(180deg,#d8e3eb 0%,#f8fafc 100%);position:relative">
|
||
<button type="button" onClick={() => setJobSeekerScreen('list')} style="position:absolute;right:10px;top:10px;width:28px;height:28px;border-radius:999px;border:1px solid rgba(255,255,255,0.7);background:rgba(17,24,39,0.35);color:white;display:flex;align-items:center;justify-content:center">×</button>
|
||
</div>
|
||
<div style="padding:12px;display:grid;gap:10px;overflow:auto">
|
||
<div>
|
||
<p style="margin:0;font-size:26px;line-height:1.2;font-weight:800;color:#111827">{selectedJob().title}</p>
|
||
<p style="margin:6px 0 0;font-size:13px;font-weight:700;color:#C2410C">{selectedJob().company} • {selectedJob().location}</p>
|
||
</div>
|
||
<div style="display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px">
|
||
{[
|
||
['Location', selectedJob().location],
|
||
['Experience', selectedJob().exp],
|
||
['Employment', selectedJob().type],
|
||
['Compensation', selectedJob().salary],
|
||
['Match', selectedJob().match],
|
||
['Posted', selectedJob().posted],
|
||
].map(([label, val]) => (
|
||
<div style="border:1px solid #E5E7EB;border-radius:10px;background:#F9FAFB;padding:10px">
|
||
<p style="margin:0;font-size:10px;letter-spacing:0.06em;text-transform:uppercase;color:#9CA3AF">{label}</p>
|
||
<p style="margin:5px 0 0;font-size:13px;font-weight:700;color:#111827">{val}</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
<div>
|
||
<p style="margin:0;font-size:11px;letter-spacing:0.09em;text-transform:uppercase;color:#6B7280">Skills</p>
|
||
<div style="display:flex;flex-wrap:wrap;gap:6px;margin-top:7px">
|
||
<For each={selectedJob().tags}>
|
||
{(tag) => <span style="height:24px;padding:0 9px;border-radius:8px;border:1px solid #E5E7EB;background:#F3F4F6;font-size:10px;font-weight:700;color:#374151;display:inline-flex;align-items:center">{tag}</span>}
|
||
</For>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<p style="margin:0;font-size:11px;letter-spacing:0.09em;text-transform:uppercase;color:#6B7280">The Opportunity</p>
|
||
<p style="margin:6px 0 0;font-size:14px;line-height:1.6;color:#374151">As a senior role at Nxtgauge, you will shape product experiences used by thousands of professionals and build systems that scale with ambition.</p>
|
||
</div>
|
||
<div>
|
||
<p style="margin:0;font-size:11px;letter-spacing:0.09em;text-transform:uppercase;color:#6B7280">Core Requirements</p>
|
||
<div style="display:grid;gap:6px;margin-top:6px">
|
||
{[
|
||
'Mastery of product and design system governance.',
|
||
'Strong track record of shipping complex B2B products.',
|
||
'Deep understanding of accessibility and UX research.',
|
||
].map((item) => <p style="margin:0;font-size:13px;color:#374151;line-height:1.45">• {item}</p>)}
|
||
</div>
|
||
</div>
|
||
<div style="border:1px solid #191970;border-radius:12px;background:#03004E;color:white;padding:12px">
|
||
<p style="margin:0;font-size:10px;letter-spacing:0.08em;text-transform:uppercase;color:#CDD4FF">Total Compensation</p>
|
||
<p style="margin:6px 0 0;font-size:24px;line-height:1.2;font-weight:800">{selectedJob().salary} <span style="font-size:12px;font-weight:600">/ year</span></p>
|
||
<div style="display:flex;gap:8px;margin-top:10px">
|
||
<button type="button" onClick={() => { setJobSeekerApplyStep(2); setJobSeekerScreen('apply'); }} style="height:36px;flex:1;border:none;border-radius:9px;background:#FF5E13;color:white;padding:0 12px;font-size:12px;font-weight:700">Apply Now</button>
|
||
<button type="button" style="width:36px;height:36px;border-radius:9px;border:1px solid rgba(255,255,255,0.3);background:transparent;color:white;display:flex;align-items:center;justify-content:center"><Bookmark size={14} /></button>
|
||
</div>
|
||
<p style="margin:8px 0 0;font-size:10px;letter-spacing:0.08em;text-transform:uppercase;color:#AAB4FF;text-align:center">Application update: {selectedJob().posted}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div style="display:flex;flex-direction:column;gap:10px">
|
||
<div style="display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:8px">
|
||
{[
|
||
{ label: 'Matching Jobs', value: '124', accent: '#B91C1C', hint: '' },
|
||
{ label: 'Saved Jobs', value: '12', accent: '#4338CA', hint: '' },
|
||
{ label: 'Applied', value: '48', accent: '#6B7280', hint: '' },
|
||
{ label: 'Shortlisted', value: '06', accent: '#0EA5E9', hint: '' },
|
||
{ label: 'New Today', value: '18', accent: 'white', hint: 'dark' },
|
||
].map((card) => (
|
||
<div style={`border:1px solid #E5E7EB;border-radius:12px;padding:10px;background:${card.hint === 'dark' ? '#03004E' : 'white'};box-shadow:0 1px 3px rgba(0,0,0,0.05)`}>
|
||
<p style={`margin:0;font-size:10px;letter-spacing:0.06em;text-transform:uppercase;color:${card.hint === 'dark' ? '#D7DBFF' : '#6B7280'}`}>{card.label}</p>
|
||
<p style={`margin:5px 0 0;font-size:34px;line-height:1;font-weight:800;color:${card.hint === 'dark' ? 'white' : card.accent}`}>{card.value}</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
<div style="border:1px solid #E5E7EB;border-radius:12px;background:white;padding:10px;display:flex;align-items:center;gap:8px;flex-wrap:wrap;box-shadow:0 1px 3px rgba(0,0,0,0.05)">
|
||
<button type="button" style="height:32px;border-radius:8px;border:1px solid #E5E7EB;background:#F9FAFB;padding:0 10px;font-size:12px;font-weight:700;color:#374151">Filters</button>
|
||
{['Role: All Roles', 'Industry: Tech', 'Location: Remote', 'Salary: $100k+', 'Experience: Senior'].map((item) => (
|
||
<button type="button" style="height:32px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 10px;font-size:12px;color:#374151">{item}</button>
|
||
))}
|
||
<button type="button" style="height:32px;border-radius:8px;border:none;background:#EEF2FF;padding:0 10px;font-size:12px;font-weight:700;color:#3730A3">Reset</button>
|
||
</div>
|
||
|
||
<div style="display:grid;gap:8px">
|
||
<For each={selectedOpenJobCards()}>
|
||
{(job) => (
|
||
<div style="border:1px solid #E5E7EB;border-radius:12px;background:white;padding:10px;display:grid;grid-template-columns:1fr auto;gap:10px;box-shadow:0 1px 3px rgba(0,0,0,0.05)">
|
||
<div>
|
||
<p style="margin:0;font-size:20px;line-height:1.25;font-weight:800;color:#111827">{job.title}</p>
|
||
<p style="margin:5px 0 0;font-size:13px;color:#4B5563">{job.company} • {job.location}</p>
|
||
<div style="display:flex;flex-wrap:wrap;gap:6px;margin-top:8px">
|
||
{[job.salary, job.exp, job.type].map((pill, idx) => (
|
||
<span style={`height:22px;padding:0 8px;border-radius:8px;border:1px solid #E5E7EB;background:${idx === 2 ? '#FFF8F4' : '#F9FAFB'};font-size:10px;font-weight:700;color:${idx === 2 ? '#C2410C' : '#374151'};display:inline-flex;align-items:center`}>{pill}</span>
|
||
))}
|
||
</div>
|
||
<div style="display:flex;flex-wrap:wrap;gap:6px;margin-top:8px">
|
||
<For each={job.tags}>{(tag) => <span style="height:22px;padding:0 8px;border-radius:7px;border:1px solid #E5E7EB;background:#F3F4F6;font-size:10px;font-weight:700;color:#4B5563;display:inline-flex;align-items:center">{tag}</span>}</For>
|
||
</div>
|
||
</div>
|
||
<div style="display:flex;flex-direction:column;align-items:flex-end;justify-content:space-between;gap:8px;min-width:200px">
|
||
<div style="text-align:right">
|
||
<span style="height:20px;padding:0 8px;border-radius:999px;background:#FFF1EB;color:#C2410C;font-size:10px;font-weight:700;display:inline-flex;align-items:center">{job.match}</span>
|
||
<p style="margin:8px 0 0;font-size:12px;color:#9CA3AF">{job.posted}</p>
|
||
</div>
|
||
<div style="display:flex;align-items:center;gap:6px">
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
setJobSeekerSelectedId(job.id);
|
||
setJobSeekerScreen('detail');
|
||
}}
|
||
style="height:32px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 10px;font-size:12px;font-weight:700;color:#374151"
|
||
>
|
||
View Details
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
setJobSeekerSelectedId(job.id);
|
||
setJobSeekerApplyStep(2);
|
||
setJobSeekerScreen('apply');
|
||
}}
|
||
style="height:32px;border-radius:8px;border:none;background:#03004E;padding:0 12px;font-size:12px;font-weight:700;color:white"
|
||
>
|
||
Apply Now
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</For>
|
||
</div>
|
||
|
||
<div style="display:flex;justify-content:center">
|
||
<button type="button" style="height:34px;border-radius:10px;border:1px solid #FFD8C2;background:#FFFCFA;padding:0 16px;font-size:12px;font-weight:700;color:#C2410C">Show More Opportunities</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const progressWidth = () => `${Math.round((jobPostStep() / COMPANY_JOB_STEPS.length) * 100)}%`;
|
||
const latestSubmission = () => companyJobSubmissions()[0] || null;
|
||
const latestStatusLabel = () => {
|
||
const status = latestSubmission()?.status;
|
||
if (status === 'VERIFICATION_PENDING') return 'In Verification Management';
|
||
if (status === 'VERIFIED' || status === 'APPROVAL_PENDING') return 'In Approval Management';
|
||
if (status === 'APPROVED_LIVE') return 'Approved and Live';
|
||
return 'Submitted';
|
||
};
|
||
const goNextJobStep = () => {
|
||
if (jobPostStep() >= COMPANY_JOB_STEPS.length) {
|
||
setJobPostView('review');
|
||
return;
|
||
}
|
||
setJobPostStep((prev) => Math.min(COMPANY_JOB_STEPS.length, prev + 1));
|
||
};
|
||
const goPrevJobStep = () => setJobPostStep((prev) => Math.max(1, prev - 1));
|
||
|
||
if (jobPostView() === 'success') {
|
||
return (
|
||
<div style="display:grid;grid-template-columns:2fr 1fr;gap:10px">
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:16px;padding:20px;box-shadow:0 1px 4px rgba(0,0,0,0.06)">
|
||
<div style="display:flex;flex-direction:column;align-items:center;text-align:center;padding:16px 0 8px">
|
||
<div style="width:150px;height:150px;border-radius:999px;border:1px solid #F3E2DA;background:#FFF9F5;display:flex;align-items:center;justify-content:center">
|
||
<div style="width:56px;height:56px;border-radius:999px;background:#C2410C;color:white;display:flex;align-items:center;justify-content:center;font-size:28px;font-weight:800">✓</div>
|
||
</div>
|
||
<p style="margin:14px 0 0;font-size:52px;line-height:1.05;font-weight:800;color:#0D0D2A">Job Submitted Successfully!</p>
|
||
<p style="margin:10px 0 0;font-size:14px;color:#4B5563;max-width:640px;line-height:1.5">Your listing for <strong>{companyJobDraft().title}</strong> has been sent to Verification Management first, then Approval Management. Job seekers can see it only after final approval.</p>
|
||
<div style="margin-top:24px;padding-top:20px;border-top:1px solid #E5E7EB;display:flex;justify-content:flex-end;gap:12px">
|
||
<button type="button" style="height:40px;padding:0 20px;border-radius:10px;border:1px solid #E5E7EB;background:white;font-size:13px;font-weight:600;color:#374151;cursor:pointer">Discard Changes</button>
|
||
<Show
|
||
when={!verificationPending()}
|
||
fallback={
|
||
<button type="button" disabled style="height:40px;padding:0 24px;border-radius:10px;background:#F3F4F6;color:#9CA3AF;border:none;font-size:13px;font-weight:600;cursor:not-allowed">Application Submitted</button>
|
||
}
|
||
>
|
||
<button
|
||
type="button"
|
||
onClick={() => setVerificationPending(true)}
|
||
style="height:40px;padding:0 24px;border-radius:10px;background:#FF5E13;color:white;border:none;font-size:13px;font-weight:600;cursor:pointer;box-shadow:0 2px 4px rgba(255,94,19,0.2)"
|
||
>
|
||
Submit for Verification
|
||
</button>
|
||
</Show>
|
||
</div>
|
||
<p style="margin:8px 0 0;font-size:12px;font-weight:700;color:#C2410C">{latestStatusLabel()}</p>
|
||
<div style="display:flex;gap:8px;margin-top:14px">
|
||
<button type="button" style="height:36px;border-radius:10px;border:1px solid #E5E7EB;background:white;padding:0 14px;font-size:12px;font-weight:700;color:#374151">View Job Details</button>
|
||
<button type="button" onClick={() => { setJobPostView('form'); setJobPostStep(1); }} style="height:36px;border-radius:10px;border:none;background:#C2410C;padding:0 14px;font-size:12px;font-weight:700;color:white">Post Another Job</button>
|
||
</div>
|
||
</div>
|
||
<div style="margin-top:14px;border:1px solid #E5E7EB;border-radius:14px;background:#FCFCFD;padding:14px">
|
||
<div style="display:flex;justify-content:space-between;align-items:center">
|
||
<p style="margin:0;font-size:30px;font-weight:800;color:#111827">Approval Tracking</p>
|
||
<span style="height:24px;padding:0 10px;border-radius:999px;background:#FFF1EB;color:#C2410C;font-size:11px;font-weight:700;display:inline-flex;align-items:center">REF: #JX-9902</span>
|
||
</div>
|
||
<div style="display:grid;gap:10px;margin-top:12px">
|
||
{[
|
||
{ title: 'Job Submitted', desc: 'Form submitted by company.', done: true },
|
||
{ title: 'Verification Management', desc: 'Job content and policy checks run here first.', done: !!latestSubmission() && latestSubmission()!.status !== 'VERIFICATION_PENDING' },
|
||
{ title: 'Approval Management', desc: 'Post-verification approval gate for publishing.', done: !!latestSubmission() && latestSubmission()!.status === 'APPROVED_LIVE' },
|
||
{ title: 'Published To Job Seekers', desc: 'Visible in Jobs list and View Details only after approval.', done: !!latestSubmission() && latestSubmission()!.status === 'APPROVED_LIVE' },
|
||
].map((row, idx) => (
|
||
<div style="display:flex;align-items:flex-start;gap:10px">
|
||
<span style={`width:18px;height:18px;border-radius:999px;display:inline-flex;align-items:center;justify-content:center;font-size:10px;font-weight:800;flex-shrink:0;background:${row.done ? '#FFF1EB' : '#F3F4F6'};color:${row.done ? '#C2410C' : '#9CA3AF'}`}>{row.done ? '✓' : idx + 1}</span>
|
||
<div>
|
||
<p style={`margin:0;font-size:14px;font-weight:700;color:${row.done ? '#111827' : '#94A3B8'}`}>{row.title}</p>
|
||
<p style={`margin:2px 0 0;font-size:12px;line-height:1.45;color:${row.done ? '#6B7280' : '#B8C1D1'}`}>{row.desc}</p>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div style="display:flex;flex-direction:column;gap:10px">
|
||
<div style="border:1px solid #E5E7EB;background:#03004E;border-radius:16px;padding:14px;color:white;box-shadow:0 1px 4px rgba(0,0,0,0.06)">
|
||
<p style="margin:0;font-size:30px;line-height:1.15;font-weight:800">Did you know?</p>
|
||
<p style="margin:8px 0 0;font-size:13px;line-height:1.55;color:#D5D8FF">Jobs with high-quality company descriptions receive <strong>40% more applications</strong>. Take a moment to update your profile.</p>
|
||
<button type="button" style="margin-top:10px;height:30px;border:none;border-radius:8px;background:#C2410C;padding:0 10px;font-size:12px;font-weight:700;color:white">Enhance Profile</button>
|
||
</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:18px;font-weight:800;color:#111827">Back to Dashboard</p>
|
||
<p style="margin:6px 0 0;font-size:12px;color:#6B7280">Monitor all your active listings and candidate inflow.</p>
|
||
<button type="button" style="margin-top:10px;height:34px;width:100%;border:1px solid #E5E7EB;border-radius:10px;background:#F9FAFB;padding:0 12px;font-size:12px;font-weight:700;color:#374151">Go to Dashboard</button>
|
||
</div>
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:12px;padding:10px;display:flex;align-items:center;justify-content:space-between">
|
||
<div>
|
||
<p style="margin:0;font-size:13px;font-weight:700;color:#111827">Need help?</p>
|
||
<p style="margin:2px 0 0;font-size:11px;color:#6B7280">Chat with a hiring specialist</p>
|
||
</div>
|
||
<button type="button" style="height:28px;border:none;border-radius:8px;background:#FFF1EB;color:#C2410C;padding:0 10px;font-size:11px;font-weight:700">Connect</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (jobPostView() === 'review') {
|
||
return (
|
||
<div style="display:grid;grid-template-columns:2fr 1fr;gap:10px">
|
||
<div style="display:flex;flex-direction:column;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:11px;letter-spacing:0.05em;text-transform:uppercase;color:#9CA3AF">Jobs > Review & Checkout</p>
|
||
<p style="margin:6px 0 0;font-size:50px;line-height:1.05;font-weight:800;color:#0D0D2A">Review & Checkout</p>
|
||
<p style="margin:8px 0 0;font-size:14px;color:#6B7280">Step 5 of 5: Finalize your job posting details and approve payment.</p>
|
||
</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)">
|
||
<div style="display:flex;justify-content:space-between;align-items:center">
|
||
<p style="margin:0;font-size:35px;font-weight:800;color:#111827">Job Basics</p>
|
||
<button type="button" style="height:28px;border:none;border-radius:8px;background:#FFF1EB;color:#C2410C;padding:0 10px;font-size:11px;font-weight:700">Edit</button>
|
||
</div>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:10px">
|
||
{[
|
||
['Job Title', companyJobDraft().title],
|
||
['Employment Type', companyJobDraft().type],
|
||
['Company Department', companyJobDraft().department],
|
||
['Openings', companyJobDraft().openings],
|
||
].map(([label, value]) => (
|
||
<div style="border:1px solid #E5E7EB;border-radius:10px;background:#F9FAFB;padding:10px">
|
||
<p style="margin:0;font-size:10px;letter-spacing:0.06em;text-transform:uppercase;color:#9CA3AF">{label}</p>
|
||
<p style="margin:6px 0 0;font-size:20px;font-weight:800;color:#111827;line-height:1.2">{value}</p>
|
||
</div>
|
||
))}
|
||
</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)">
|
||
<div style="display:flex;justify-content:space-between;align-items:center">
|
||
<p style="margin:0;font-size:35px;font-weight:800;color:#111827">Role & Requirements</p>
|
||
<button type="button" style="height:28px;border:none;border-radius:8px;background:#FFF1EB;color:#C2410C;padding:0 10px;font-size:11px;font-weight:700">Edit</button>
|
||
</div>
|
||
<p style="margin:10px 0 0;font-size:11px;letter-spacing:0.05em;text-transform:uppercase;color:#9CA3AF">Technical Skills</p>
|
||
<div style="display:flex;flex-wrap:wrap;gap:6px;margin-top:6px">
|
||
<For each={companyJobDraft().tags}>
|
||
{(skill) => <span style="height:24px;padding:0 8px;border-radius:8px;border:1px solid #E5E7EB;background:#F3F4F6;font-size:11px;font-weight:700;color:#374151;display:inline-flex;align-items:center">{skill}</span>}
|
||
</For>
|
||
</div>
|
||
<p style="margin:10px 0 0;font-size:11px;letter-spacing:0.05em;text-transform:uppercase;color:#9CA3AF">Required Experience</p>
|
||
<p style="margin:4px 0 0;font-size:16px;font-weight:700;color:#111827">{companyJobDraft().exp}</p>
|
||
</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)">
|
||
<div style="display:flex;justify-content:space-between;align-items:center">
|
||
<p style="margin:0;font-size:35px;font-weight:800;color:#111827">Compensation & Location</p>
|
||
<button type="button" style="height:28px;border:none;border-radius:8px;background:#FFF1EB;color:#C2410C;padding:0 10px;font-size:11px;font-weight:700">Edit</button>
|
||
</div>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:10px">
|
||
<div style="border:1px solid #E5E7EB;border-radius:10px;background:#F9FAFB;padding:10px">
|
||
<p style="margin:0;font-size:10px;letter-spacing:0.06em;text-transform:uppercase;color:#9CA3AF">Salary Range</p>
|
||
<p style="margin:6px 0 0;font-size:20px;font-weight:800;color:#111827">{companyJobDraft().salary} / year</p>
|
||
</div>
|
||
<div style="border:1px solid #E5E7EB;border-radius:10px;background:#F9FAFB;padding:10px">
|
||
<p style="margin:0;font-size:10px;letter-spacing:0.06em;text-transform:uppercase;color:#9CA3AF">Location</p>
|
||
<p style="margin:6px 0 0;font-size:20px;font-weight:800;color:#111827">{companyJobDraft().location}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div style="display:flex;flex-direction:column;gap:10px">
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:16px;overflow:hidden;box-shadow:0 1px 4px rgba(0,0,0,0.06)">
|
||
<div style="background:#03004E;padding:12px;color:white">
|
||
<p style="margin:0;font-size:30px;font-weight:800;line-height:1.1">Pricing & Approval</p>
|
||
<p style="margin:6px 0 0;font-size:13px;color:#D7DBFF">Review costs and confirm submission</p>
|
||
</div>
|
||
<div style="padding:12px">
|
||
<div style="border:1px solid #E5E7EB;border-radius:10px;background:#F9FAFB;padding:10px;display:flex;justify-content:space-between;align-items:center">
|
||
<span style="font-size:14px;color:#374151">Post Status</span>
|
||
<span style="font-size:14px;font-weight:700;color:#C2410C">This is a Paid Job Post</span>
|
||
</div>
|
||
<div style="display:grid;gap:8px;margin-top:10px">
|
||
<div style="display:flex;justify-content:space-between;font-size:14px;color:#374151"><span>Credits Required</span><strong style="color:#111827">500 Tracecoins</strong></div>
|
||
<div style="display:flex;justify-content:space-between;font-size:14px;color:#374151"><span>Current Balance</span><strong style="color:#111827">1200 Tracecoins</strong></div>
|
||
<div style="height:1px;background:#E5E7EB" />
|
||
<div style="display:flex;justify-content:space-between;font-size:16px;color:#111827;font-weight:800"><span>Remaining Balance</span><span style="color:#16A34A">700 Tracecoins</span></div>
|
||
</div>
|
||
<div style="margin-top:10px;border:1px solid #FFD8C2;border-radius:10px;background:#FFF8F4;padding:10px">
|
||
<p style="margin:0;font-size:13px;font-weight:700;color:#111827">Approval Required: Yes</p>
|
||
<p style="margin:4px 0 0;font-size:12px;color:#6B7280;line-height:1.45">Your post will be reviewed by our moderation team within 24 hours.</p>
|
||
</div>
|
||
<button type="button" onClick={() => { submitCompanyJobForReview(); setJobPostView('success'); }} style="margin-top:10px;height:38px;width:100%;border:none;border-radius:10px;background:#03004E;color:white;font-size:12px;font-weight:700">Pay 500 Tracecoins & Submit</button>
|
||
</div>
|
||
</div>
|
||
<div style="border:1px solid #FFD8C2;background:linear-gradient(135deg,#3A1E00 0%,#C2410C 90%);border-radius:16px;padding:14px;color:white;box-shadow:0 1px 4px rgba(0,0,0,0.06)">
|
||
<p style="margin:0;font-size:11px;letter-spacing:0.06em;text-transform:uppercase;color:#FFE7DA">Pro Tip</p>
|
||
<p style="margin:8px 0 0;font-size:16px;line-height:1.4;font-weight:700">Boost your visibility by 40% with Sponsored Highlights.</p>
|
||
<button type="button" style="margin-top:10px;height:30px;border:1px solid rgba(255,255,255,0.35);border-radius:8px;background:transparent;color:white;padding:0 10px;font-size:12px;font-weight:700">Learn More</button>
|
||
</div>
|
||
<div style="display:flex;justify-content:space-between;gap:8px">
|
||
<button type="button" onClick={() => { setJobPostView('form'); setJobPostStep(4); }} style="height:34px;flex:1;border-radius:8px;border:1px solid #E5E7EB;background:white;color:#374151;padding:0 12px;font-size:12px;font-weight:700">Back</button>
|
||
<button type="button" onClick={() => { submitCompanyJobForReview(); setJobPostView('success'); }} style="height:34px;flex:1;border-radius:8px;border:none;background:#C2410C;color:white;padding:0 12px;font-size:12px;font-weight:700">Confirm & Submit</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div style="display:flex;flex-direction:column;gap:10px">
|
||
<div style="display:grid;grid-template-columns:2fr 3fr;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:11px;letter-spacing:0.06em;text-transform:uppercase;color:#9CA3AF">Billing Policy</p>
|
||
<p style="margin:6px 0 0;font-size:34px;line-height:1.1;font-weight:800;color:#111827">Job Posting Rule</p>
|
||
<span style="margin-top:8px;height:22px;padding:0 8px;border-radius:999px;background:#ECFDF3;color:#15803D;font-size:11px;font-weight:700;display:inline-flex;align-items:center">Free First Job</span>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:10px">
|
||
<div style="border:1px solid #E5E7EB;border-radius:10px;background:#F9FAFB;padding:10px"><p style="margin:0;font-size:11px;color:#6B7280">Cost Required</p><p style="margin:6px 0 0;font-size:20px;font-weight:800;color:#111827">0 Tracecoins</p></div>
|
||
<div style="border:1px solid #E5E7EB;border-radius:10px;background:#F9FAFB;padding:10px"><p style="margin:0;font-size:11px;color:#6B7280">Usage Count</p><p style="margin:6px 0 0;font-size:20px;font-weight:800;color:#111827">1 Job Posted</p></div>
|
||
</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)">
|
||
<p style="margin:0;font-size:11px;letter-spacing:0.06em;text-transform:uppercase;color:#9CA3AF">Lifecycle</p>
|
||
<p style="margin:6px 0 0;font-size:34px;line-height:1.1;font-weight:800;color:#111827">Approval Flow Status</p>
|
||
<div style="display:flex;align-items:center;gap:6px;margin-top:12px">
|
||
<For each={['Draft', 'Submitted', 'Under Review', 'Approved', 'Live']}>
|
||
{(step, idx) => (
|
||
<>
|
||
<span style={`width:26px;height:26px;border-radius:999px;display:inline-flex;align-items:center;justify-content:center;font-size:11px;font-weight:800;background:${idx() + 1 <= jobPostStep() ? '#C2410C' : '#E5E7EB'};color:${idx() + 1 <= jobPostStep() ? 'white' : '#9CA3AF'}`}>{idx() + 1}</span>
|
||
<Show when={idx() < 4}>
|
||
<span style={`height:2px;flex:1;min-width:18px;background:${idx() + 1 < jobPostStep() ? '#C2410C' : '#E5E7EB'}`} />
|
||
</Show>
|
||
</>
|
||
)}
|
||
</For>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div style="display:grid;grid-template-columns:2fr 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:36px;font-weight:800;color:#111827">Step {jobPostStep()}: {COMPANY_JOB_STEPS[jobPostStep() - 1]}</p>
|
||
<div style="height:6px;border-radius:999px;background:#E5E7EB;overflow:hidden;margin-top:10px"><div style={`height:100%;background:#C2410C;width:${progressWidth()}`} /></div>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:12px">
|
||
<div style="grid-column:1 / -1;display:flex;flex-direction:column;gap:6px"><p style="margin:0;font-size:11px;letter-spacing:0.05em;text-transform:uppercase;color:#6B7280">Job Title</p><div style="height:38px;border:1px solid #E5E7EB;border-radius:10px;background:#F9FAFB;padding:0 10px;display:flex;align-items:center;font-size:12px;color:#374151">{companyJobDraft().title}</div></div>
|
||
<div style="display:flex;flex-direction:column;gap:6px"><p style="margin:0;font-size:11px;letter-spacing:0.05em;text-transform:uppercase;color:#6B7280">Department</p><div style="height:38px;border:1px solid #E5E7EB;border-radius:10px;background:#F9FAFB;padding:0 10px;display:flex;align-items:center;justify-content:space-between;font-size:12px;color:#374151"><span>{companyJobDraft().department}</span><span>▾</span></div></div>
|
||
<div style="display:flex;flex-direction:column;gap:6px"><p style="margin:0;font-size:11px;letter-spacing:0.05em;text-transform:uppercase;color:#6B7280">Job Category</p><div style="height:38px;border:1px solid #E5E7EB;border-radius:10px;background:#F9FAFB;padding:0 10px;display:flex;align-items:center;justify-content:space-between;font-size:12px;color:#374151"><span>Select Category</span><span>▾</span></div></div>
|
||
<div style="grid-column:1 / -1;display:flex;flex-direction:column;gap:6px"><p style="margin:0;font-size:11px;letter-spacing:0.05em;text-transform:uppercase;color:#6B7280">Employment Type</p><div style="display:flex;gap:6px"><span style="height:28px;padding:0 10px;border-radius:999px;border:1px solid #E5E7EB;background:#FFF1EB;color:#C2410C;display:inline-flex;align-items:center;font-size:11px;font-weight:700">{companyJobDraft().type}</span></div></div>
|
||
<div style="display:flex;flex-direction:column;gap:6px"><p style="margin:0;font-size:11px;letter-spacing:0.05em;text-transform:uppercase;color:#6B7280">Seniority</p><div style="height:38px;border:1px solid #E5E7EB;border-radius:10px;background:#F9FAFB;padding:0 10px;display:flex;align-items:center;justify-content:space-between;font-size:12px;color:#374151"><span>Entry Level</span><span>▾</span></div></div>
|
||
<div style="display:flex;flex-direction:column;gap:6px"><p style="margin:0;font-size:11px;letter-spacing:0.05em;text-transform:uppercase;color:#6B7280">Openings</p><div style="height:38px;border:1px solid #E5E7EB;border-radius:10px;background:#F9FAFB;padding:0 10px;display:flex;align-items:center;font-size:12px;color:#374151">{companyJobDraft().openings}</div></div>
|
||
<div style="grid-column:1 / -1;display:flex;flex-direction:column;gap:6px"><p style="margin:0;font-size:11px;letter-spacing:0.05em;text-transform:uppercase;color:#6B7280">Description</p><div style="min-height:120px;border:1px solid #E5E7EB;border-radius:10px;background:#F9FAFB;padding:10px;font-size:12px;color:#9CA3AF">Detailed job responsibilities, skills, and expectations...</div></div>
|
||
</div>
|
||
<div style="display:flex;justify-content:space-between;align-items:center;gap:8px;margin-top:12px;border-top:1px solid #E5E7EB;padding-top:10px">
|
||
<button type="button" onClick={goPrevJobStep} disabled={jobPostStep() === 1} style={`height:34px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:700;color:${jobPostStep() === 1 ? '#9CA3AF' : '#374151'};cursor:${jobPostStep() === 1 ? 'not-allowed' : 'pointer'}`}>Back</button>
|
||
<div style="display:flex;gap:8px">
|
||
<button type="button" style="height:34px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:700;color:#374151">Save as Draft</button>
|
||
<button type="button" onClick={jobPostStep() === COMPANY_JOB_STEPS.length ? () => setJobPostView('review') : goNextJobStep} style="height:34px;border-radius:8px;border:none;background:#03004E;padding:0 12px;font-size:12px;font-weight:700;color:white">
|
||
{jobPostStep() === COMPANY_JOB_STEPS.length ? 'Go To Review' : 'Next: Role & Requirements'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div style="display:flex;flex-direction:column;gap:10px">
|
||
<div style="border:1px solid #D9D8F9;background:#4F4B8A;border-radius:16px;padding:14px;color:white;box-shadow:0 1px 4px rgba(0,0,0,0.06)">
|
||
<div style="display:flex;justify-content:space-between;align-items:center">
|
||
<p style="margin:0;font-size:11px;letter-spacing:0.06em;text-transform:uppercase;color:#DADAFB">Live Preview</p>
|
||
<span style="height:20px;padding:0 8px;border-radius:999px;border:1px solid rgba(255,255,255,0.3);font-size:10px;font-weight:700;display:inline-flex;align-items:center">FREE POST</span>
|
||
</div>
|
||
<p style="margin:10px 0 0;font-size:10px;color:#DADAFB;letter-spacing:0.06em;text-transform:uppercase">Position</p>
|
||
<p style="margin:4px 0 0;font-size:26px;line-height:1.15;font-weight:800;color:white">Product Design Manager</p>
|
||
<div style="display:grid;gap:8px;margin-top:12px">
|
||
{[
|
||
['Location', 'Remote / San Francisco'],
|
||
['Type', 'Full-Time Role'],
|
||
['Posting Fee', '$0.00 (Tracecoins: 0)'],
|
||
].map(([label, val]) => (
|
||
<div style="display:flex;justify-content:space-between;align-items:center;gap:8px">
|
||
<span style="font-size:11px;color:#D7DBFF">{label}</span>
|
||
<span style="font-size:12px;font-weight:700;color:white;text-align:right">{val}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
<div style="border:1px solid #E5E7EB;background:#FFF8F4;border-radius:14px;padding:12px;box-shadow:0 1px 3px rgba(0,0,0,0.05)">
|
||
<p style="margin:0;font-size:12px;font-weight:700;color:#111827">Need help writing?</p>
|
||
<p style="margin:6px 0 0;font-size:12px;color:#6B7280;line-height:1.45">Try our AI Generator to create a compelling description in seconds.</p>
|
||
<button type="button" style="margin-top:8px;height:30px;border:none;border-radius:8px;background:#FFF1EB;color:#C2410C;padding:0 10px;font-size:11px;font-weight:700">Launch AI Assistant</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (customerKey() === 'my requirements') {
|
||
const statusMeta = (value: string) => {
|
||
const key = String(value || '').toLowerCase();
|
||
if (key === 'approved') return { bg: '#DCFCE7', c: '#15803D', label: 'Approved' };
|
||
if (key === 'active') return { bg: '#DBEAFE', c: '#1D4ED8', label: 'Active' };
|
||
if (key === 'under review') return { bg: '#FFEDD5', c: '#C2410C', label: 'Under Review' };
|
||
if (key === 'draft') return { bg: '#F3F4F6', c: '#4B5563', label: 'Draft' };
|
||
if (key === 'closed') return { bg: '#E5E7EB', c: '#4B5563', label: 'Closed' };
|
||
if (key === 'rejected') return { bg: '#FEE2E2', c: '#B91C1C', label: 'Rejected' };
|
||
return { bg: '#F3F4F6', c: '#6B7280', label: titleCase(value) };
|
||
};
|
||
const filteredRows = requirementRows().filter((row) => {
|
||
if (tab === 'open') return row.status === 'active' || row.status === 'under review';
|
||
if (tab === 'closed') return row.status === 'closed';
|
||
if (tab === 'drafts') return row.status === 'draft';
|
||
return true;
|
||
});
|
||
const selectedRoleDetails = () => REQUIREMENT_ROLE_DETAILS[selectedRequirementRole()] || REQUIREMENT_ROLE_DETAILS.PHOTOGRAPHER;
|
||
const steps = ['Choose Type', 'Basic Details', 'Role-Specific', 'Budget & Location', 'Attachments', 'Review'];
|
||
const canGoBack = () => requirementsStep() > 1;
|
||
const nextLabel = () => (requirementsStep() === steps.length ? 'Submit For Approval' : 'Next');
|
||
const goNextStep = () => {
|
||
if (requirementsStep() >= steps.length) {
|
||
submitRequirementForReview();
|
||
return;
|
||
}
|
||
setRequirementsStep((prev) => Math.min(steps.length, prev + 1));
|
||
};
|
||
const goBackStep = () => setRequirementsStep((prev) => Math.max(1, prev - 1));
|
||
|
||
if (requirementsView() === 'new') {
|
||
return (
|
||
<div style="display:flex;flex-direction:column;gap:12px">
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:16px;padding:16px;box-shadow:0 1px 4px rgba(0,0,0,0.06)">
|
||
<div style="display:flex;align-items:center;justify-content:space-between">
|
||
<p style="margin:0;font-size:20px;font-weight:800;color:#111827">Post New Requirement</p>
|
||
<button
|
||
type="button"
|
||
onClick={() => { setRequirementsView('list'); setRequirementsStep(1); }}
|
||
style="height:30px;border-radius:8px;border:1px solid #E5E7EB;background:white;color:#374151;padding:0 10px;font-size:12px;font-weight:700"
|
||
>
|
||
Back To List
|
||
</button>
|
||
</div>
|
||
<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:8px;margin-top:12px">
|
||
<For each={steps}>
|
||
{(step, idx) => (
|
||
<div style="flex:1;display:flex;align-items:center;gap:8px">
|
||
<div style="display:flex;flex-direction:column;align-items:center;gap:6px;min-width:58px">
|
||
<span style={`width:32px;height:32px;border-radius:999px;display:inline-flex;align-items:center;justify-content:center;font-size:12px;font-weight:800;border:1px solid ${requirementsStep() > idx() + 1 ? '#FF5E13' : '#D1D5DB'};background:${requirementsStep() === idx() + 1 ? '#FF5E13' : requirementsStep() > idx() + 1 ? '#FFF1EB' : '#F9FAFB'};color:${requirementsStep() === idx() + 1 ? 'white' : requirementsStep() > idx() + 1 ? '#FF5E13' : '#6B7280'}`}>{idx() + 1}</span>
|
||
<p style={`margin:0;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:0.04em;color:${requirementsStep() === idx() + 1 ? '#111827' : '#9CA3AF'}`}>{step}</p>
|
||
</div>
|
||
<Show when={idx() < steps.length - 1}>
|
||
<div style={`height:2px;flex:1;background:${requirementsStep() > idx() + 1 ? '#FF5E13' : '#E5E7EB'};margin-top:16px`} />
|
||
</Show>
|
||
</div>
|
||
)}
|
||
</For>
|
||
</div>
|
||
</div>
|
||
|
||
<Show when={requirementsStep() === 1}>
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:16px;padding:16px;box-shadow:0 1px 4px rgba(0,0,0,0.06)">
|
||
<p style="margin:0;font-size:30px;font-weight:800;color:#111827;text-align:center">What kind of professional are you looking for?</p>
|
||
<p style="margin:8px 0 0;font-size:14px;color:#6B7280;text-align:center">Select a category to start your requirement. This helps us match you with the right experts.</p>
|
||
<div style="display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;margin-top:14px">
|
||
<For each={REQUIREMENT_ROLE_OPTIONS}>
|
||
{(role) => {
|
||
const active = selectedRequirementRole() === role.key;
|
||
return (
|
||
<button
|
||
type="button"
|
||
onClick={() => setSelectedRequirementRole(role.key)}
|
||
style={`text-align:left;border:1px solid ${active ? '#FF5E13' : '#E5E7EB'};border-radius:12px;background:white;padding:12px;min-height:154px;display:flex;flex-direction:column;gap:10px`}
|
||
>
|
||
<div style="display:flex;align-items:center;justify-content:space-between">
|
||
<span style="width:40px;height:40px;border-radius:10px;background:#F3F4F6;border:1px solid #E5E7EB;display:flex;align-items:center;justify-content:center">
|
||
<img src={role.icon} alt="" style={`width:18px;height:18px;object-fit:contain;filter:${ORANGE_ICON_FILTER}`} />
|
||
</span>
|
||
<Show when={active}>
|
||
<span style="display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;border-radius:999px;background:#FFF1EB;border:1px solid #FFD8C2;color:#FF5E13;font-size:12px;font-weight:800">✓</span>
|
||
</Show>
|
||
</div>
|
||
<p style="margin:0;font-size:16px;font-weight:800;color:#111827">{role.title}</p>
|
||
<p style="margin:0;font-size:12px;line-height:1.5;color:#6B7280">{role.desc}</p>
|
||
<span style={`margin-top:auto;height:30px;border-radius:8px;display:inline-flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;${active ? 'background:#FF5E13;color:white' : 'border:1px solid #E5E7EB;color:#FF5E13'}`}>{active ? 'Continue' : 'Select'}</span>
|
||
</button>
|
||
);
|
||
}}
|
||
</For>
|
||
</div>
|
||
</div>
|
||
</Show>
|
||
|
||
<Show when={requirementsStep() === 2}>
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:16px;padding:16px;box-shadow:0 1px 4px rgba(0,0,0,0.06)">
|
||
<p style="margin:0;font-size:30px;font-weight:800;color:#111827">Basic Requirement Details</p>
|
||
<p style="margin:8px 0 0;font-size:13px;color:#6B7280">Add core details before role-specific inputs.</p>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:12px">
|
||
{['Requirement Title', 'Priority', 'Requirement Description', 'Expected Start Date', 'Service City', 'Contact Number'].map((field) => (
|
||
<div style="display:flex;flex-direction:column;gap:6px">
|
||
<p style="margin:0;font-size:11px;font-weight:700;letter-spacing:0.05em;text-transform:uppercase;color:#6B7280">{field}</p>
|
||
<div style="height:38px;border:1px solid #E5E7EB;border-radius:10px;background:#F9FAFB;padding:0 10px;display:flex;align-items:center;font-size:12px;color:#9CA3AF">
|
||
Enter {field.toLowerCase()}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</Show>
|
||
|
||
<Show when={requirementsStep() === 3}>
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:16px;padding:16px;box-shadow:0 1px 4px rgba(0,0,0,0.06)">
|
||
<p style="margin:0;font-size:30px;font-weight:800;color:#111827">{selectedRoleDetails().title}</p>
|
||
<p style="margin:8px 0 0;font-size:13px;color:#6B7280">{selectedRoleDetails().subtitle}</p>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:12px">
|
||
<For each={selectedRoleDetails().fields}>
|
||
{(field, idx) => (
|
||
<div style={`display:flex;flex-direction:column;gap:6px;${field.includes('Venue') ? 'grid-column:1 / -1' : ''}`}>
|
||
<p style="margin:0;font-size:11px;font-weight:700;letter-spacing:0.05em;text-transform:uppercase;color:#6B7280">{field}</p>
|
||
<div style="height:38px;border:1px solid #E5E7EB;border-radius:10px;background:#F9FAFB;padding:0 10px;display:flex;align-items:center;justify-content:space-between;font-size:12px;color:#9CA3AF">
|
||
<span>{idx() % 2 === 0 ? 'Select' : 'Enter'} {field.toLowerCase()}</span>
|
||
<Show when={idx() % 2 === 0}><span>▾</span></Show>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</For>
|
||
</div>
|
||
<div style="display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;margin-top:12px">
|
||
<For each={selectedRoleDetails().toggles}>
|
||
{(toggleLabel) => (
|
||
<div style="display:flex;align-items:center;gap:10px;padding:10px;border:1px solid #E5E7EB;border-radius:10px;background:#FBFBFB">
|
||
<span style="width:32px;height:18px;border-radius:999px;background:#E5E7EB;position:relative;display:inline-flex;align-items:center;padding:2px">
|
||
<span style="width:14px;height:14px;border-radius:999px;background:white" />
|
||
</span>
|
||
<span style="font-size:12px;font-weight:700;color:#374151">{toggleLabel}</span>
|
||
</div>
|
||
)}
|
||
</For>
|
||
</div>
|
||
<div style="margin-top:12px">
|
||
<p style="margin:0;font-size:11px;font-weight:700;letter-spacing:0.05em;text-transform:uppercase;color:#6B7280">Style Needed</p>
|
||
<div style="display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px;margin-top:8px">
|
||
<For each={selectedRoleDetails().styles}>
|
||
{(styleLabel) => (
|
||
<div style="height:34px;border:1px solid #E5E7EB;border-radius:8px;background:#F9FAFB;padding:0 10px;display:flex;align-items:center;gap:8px;font-size:12px;color:#374151">
|
||
<span style="width:14px;height:14px;border-radius:4px;border:1px solid #F5B197;background:white" />
|
||
{styleLabel}
|
||
</div>
|
||
)}
|
||
</For>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Show>
|
||
|
||
<Show when={requirementsStep() === 4}>
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:16px;padding:16px;box-shadow:0 1px 4px rgba(0,0,0,0.06)">
|
||
<p style="margin:0;font-size:30px;font-weight:800;color:#111827">Budget & Location</p>
|
||
<p style="margin:8px 0 0;font-size:13px;color:#6B7280">Set your target budget and service location to get relevant quotes.</p>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:12px">
|
||
{['Budget Range', 'Urgency Level', 'Service Location', 'Mode (Remote / On-site)', 'Preferred Start Date', 'Flexible Budget'].map((field, idx) => (
|
||
<div style="display:flex;flex-direction:column;gap:6px">
|
||
<p style="margin:0;font-size:11px;font-weight:700;letter-spacing:0.05em;text-transform:uppercase;color:#6B7280">{field}</p>
|
||
<div style="height:38px;border:1px solid #E5E7EB;border-radius:10px;background:#F9FAFB;padding:0 10px;display:flex;align-items:center;justify-content:space-between;font-size:12px;color:#9CA3AF">
|
||
<span>{idx % 2 === 0 ? 'Enter' : 'Select'} {field.toLowerCase()}</span>
|
||
<Show when={idx % 2 === 1}><span>▾</span></Show>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</Show>
|
||
|
||
<Show when={requirementsStep() === 5}>
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:16px;padding:16px;box-shadow:0 1px 4px rgba(0,0,0,0.06)">
|
||
<p style="margin:0;font-size:30px;font-weight:800;color:#111827">Attachments</p>
|
||
<p style="margin:8px 0 0;font-size:13px;color:#6B7280">Upload reference files to help professionals understand your requirement better.</p>
|
||
<div style="display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin-top:12px">
|
||
{[1, 2, 3, 4].map((slot) => (
|
||
<div style="height:110px;border:1px dashed #F5B197;border-radius:12px;background:#FFF9F6;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px">
|
||
<span style="width:26px;height:26px;border-radius:999px;background:white;border:1px solid #FFD8C2;display:flex;align-items:center;justify-content:center;color:#FF5E13;font-weight:800">+</span>
|
||
<p style="margin:0;font-size:11px;color:#6B7280">Attachment {slot}</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</Show>
|
||
|
||
<Show when={requirementsStep() === 6}>
|
||
<div style="display:grid;grid-template-columns:2fr 1fr;gap:10px">
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:16px;padding:16px;box-shadow:0 1px 4px rgba(0,0,0,0.06)">
|
||
<p style="margin:0;font-size:30px;font-weight:800;color:#111827">Review & Submit</p>
|
||
<p style="margin:8px 0 0;font-size:13px;color:#6B7280">Final check before submitting for approval.</p>
|
||
<div style="margin-top:12px;display:grid;gap:8px">
|
||
{[
|
||
['Category', titleCase(selectedRequirementRole().replace(/_/g, ' ').toLowerCase())],
|
||
['Requirement Title', 'Luxury Wedding Shoot'],
|
||
['Budget Range', '₹1,50,000 - ₹2,00,000'],
|
||
['Service Location', 'Chennai, TN'],
|
||
['Expected Start Date', 'Nov 12, 2023'],
|
||
].map(([label, value]) => (
|
||
<div style="display:flex;justify-content:space-between;align-items:center;padding:10px;border:1px solid #E5E7EB;border-radius:10px;background:#F9FAFB">
|
||
<span style="font-size:12px;color:#6B7280">{label}</span>
|
||
<span style="font-size:12px;font-weight:700;color:#111827">{value}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
<div style="border:1px solid #E5E7EB;background:linear-gradient(180deg,#FFFFFF 0%,#FFF6F0 100%);border-radius:16px;padding:16px;box-shadow:0 1px 4px rgba(0,0,0,0.06)">
|
||
<p style="margin:0;font-size:11px;letter-spacing:0.06em;text-transform:uppercase;color:#6B7280">Approval Flow</p>
|
||
<p style="margin:8px 0 0;font-size:20px;font-weight:800;color:#111827">Submit For Approval</p>
|
||
<p style="margin:8px 0 0;font-size:12px;color:#6B7280;line-height:1.5">Once submitted, your requirement goes to Verification Management first, then Approval Management. Only after both stages it goes live in professional leads.</p>
|
||
<div style="display:grid;gap:8px;margin-top:12px">
|
||
{['Draft Created', 'Verification Management', 'Approval Management', 'Live In Leads'].map((state, idx) => (
|
||
<div style="display:flex;align-items:center;gap:8px">
|
||
<span style={`width:18px;height:18px;border-radius:999px;display:inline-flex;align-items:center;justify-content:center;font-size:10px;font-weight:800;background:${idx < 2 ? '#ECFDF3' : '#F3F4F6'};color:${idx < 2 ? '#059669' : '#9CA3AF'}`}>{idx < 2 ? '✓' : idx + 1}</span>
|
||
<span style={`font-size:12px;color:${idx < 2 ? '#111827' : '#6B7280'}`}>{state}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Show>
|
||
|
||
<div style="display:flex;align-items:center;justify-content:space-between;border-top:1px solid #E5E7EB;padding-top:12px">
|
||
<button
|
||
type="button"
|
||
onClick={goBackStep}
|
||
disabled={!canGoBack()}
|
||
style={`height:34px;border-radius:8px;padding:0 12px;font-size:12px;font-weight:700;border:1px solid #E5E7EB;background:white;color:${canGoBack() ? '#374151' : '#9CA3AF'};cursor:${canGoBack() ? 'pointer' : 'not-allowed'}`}
|
||
>
|
||
Back
|
||
</button>
|
||
<div style="display:flex;gap:8px">
|
||
<button type="button" style="height:34px;border-radius:8px;border:1px solid #E5E7EB;background:white;color:#374151;padding:0 12px;font-size:12px;font-weight:700">Save As Draft</button>
|
||
<button type="button" onClick={goNextStep} style="height:34px;border-radius:8px;border:none;background:#03004E;color:white;padding:0 12px;font-size:12px;font-weight:700">{nextLabel()}</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (requirementsView() === 'detail') {
|
||
const selectedRow = requirementRows().find((row) => row.id === selectedRequirementId()) || requirementRows()[0];
|
||
const status = statusMeta(selectedRow.status);
|
||
return (
|
||
<div style="display:grid;grid-template-columns:2fr 1fr;gap:10px">
|
||
<div style="display:flex;flex-direction:column;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)">
|
||
<div style="display:flex;align-items:center;justify-content:space-between;gap:8px">
|
||
<div>
|
||
<p style="margin:0;font-size:11px;letter-spacing:0.05em;text-transform:uppercase;color:#9CA3AF">Requirements > Details</p>
|
||
<p style="margin:6px 0 0;font-size:38px;line-height:1.05;font-weight:800;color:#111827">{selectedRow.title}</p>
|
||
<div style="display:flex;align-items:center;gap:8px;margin-top:8px">
|
||
<span style={`height:22px;padding:0 10px;border-radius:999px;display:inline-flex;align-items:center;font-size:11px;font-weight:700;background:${status.bg};color:${status.c}`}>{status.label}</span>
|
||
<span style="height:22px;padding:0 10px;border-radius:999px;display:inline-flex;align-items:center;font-size:11px;font-weight:700;background:#DBEAFE;color:#1D4ED8">Active</span>
|
||
<span style="font-size:12px;color:#6B7280">Created on {selectedRow.submission}</span>
|
||
</div>
|
||
</div>
|
||
<div style="display:flex;gap:8px">
|
||
<button type="button" style="height:32px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:700;color:#374151">Duplicate</button>
|
||
<button type="button" style="height:32px;border-radius:8px;border:none;background:#FF5E13;padding:0 12px;font-size:12px;font-weight:700;color:white">Boost Requirement</button>
|
||
</div>
|
||
</div>
|
||
<div style="display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;margin-top:10px">
|
||
{[
|
||
['Proposed Budget', selectedRow.budget],
|
||
['Location & Urgency', selectedRow.location],
|
||
['Project Scope', '3 Day Event'],
|
||
].map(([label, val]) => (
|
||
<div style="border:1px solid #E5E7EB;border-radius:12px;background:#F9FAFB;padding:10px">
|
||
<p style="margin:0;font-size:11px;letter-spacing:0.05em;text-transform:uppercase;color:#9CA3AF">{label}</p>
|
||
<p style="margin:7px 0 0;font-size:16px;font-weight:800;color:#111827">{val}</p>
|
||
</div>
|
||
))}
|
||
</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)">
|
||
<p style="margin:0;font-size:34px;font-weight:800;color:#111827">Requirement Overview</p>
|
||
<p style="margin:10px 0 0;font-size:13px;line-height:1.7;color:#374151">We are looking for a premium {selectedRow.category.toLowerCase().replace(/_/g, ' ')} team for a high-end project with a focus on quality, timeline ownership, and clear communication throughout execution.</p>
|
||
<div style="margin-top:10px;padding:10px;border:1px solid #FEC7AA;border-radius:10px;background:#FFF9F5">
|
||
<p style="margin:0;font-size:11px;letter-spacing:0.05em;text-transform:uppercase;color:#C2410C;font-weight:700">Special Instructions</p>
|
||
<ul style="margin:8px 0 0;padding-left:18px;color:#374151;font-size:13px;line-height:1.6">
|
||
<li>Mandatory experience in similar premium projects.</li>
|
||
<li>Must be available for milestone reviews every 72 hours.</li>
|
||
<li>NDA required before onboarding and deliverable sharing.</li>
|
||
</ul>
|
||
</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)">
|
||
<p style="margin:0;font-size:30px;font-weight:800;color:#111827">Reference Material</p>
|
||
<div style="display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px;margin-top:10px">
|
||
{[1, 2, 3, 4].map((slot) => (
|
||
<div style="height:84px;border:1px dashed #F5B197;border-radius:10px;background:#FFF9F6;display:flex;align-items:center;justify-content:center;color:#9CA3AF;font-size:12px;font-weight:700">
|
||
{slot === 4 ? 'Add More' : `Asset ${slot}`}
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div style="display:flex;flex-direction:column;gap:10px">
|
||
<div style="border:1px solid #D9D8F9;background:#4F4B8A;border-radius:16px;padding:14px;color:white;box-shadow:0 1px 4px rgba(0,0,0,0.08)">
|
||
<p style="margin:0;font-size:11px;letter-spacing:0.06em;text-transform:uppercase;color:#DADAFB">Response Metrics</p>
|
||
<p style="margin:8px 0 0;font-size:54px;line-height:1;font-weight:800">08</p>
|
||
<p style="margin:4px 0 0;font-size:12px;color:#E5E7FF">Total Responses</p>
|
||
<div style="height:1px;background:rgba(255,255,255,0.2);margin:10px 0" />
|
||
<div style="display:flex;justify-content:space-between;font-size:12px">
|
||
<span>03 Shortlisted</span>
|
||
<span>03 Rejected</span>
|
||
</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)">
|
||
<p style="margin:0;font-size:11px;letter-spacing:0.06em;text-transform:uppercase;color:#9CA3AF">Approval History</p>
|
||
<div style="display:grid;gap:10px;margin-top:10px">
|
||
{['Requirement Approved', 'Under Review', 'Submitted', 'Draft Created'].map((step) => (
|
||
<div style="display:flex;align-items:center;gap:8px">
|
||
<span style="width:18px;height:18px;border-radius:999px;background:#ECFDF3;color:#059669;display:inline-flex;align-items:center;justify-content:center;font-size:10px;font-weight:800">✓</span>
|
||
<div>
|
||
<p style="margin:0;font-size:12px;font-weight:700;color:#111827">{step}</p>
|
||
<p style="margin:1px 0 0;font-size:11px;color:#9CA3AF">Oct 24, 2023</p>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:16px;padding:12px;display:grid;gap:8px;box-shadow:0 1px 4px rgba(0,0,0,0.06)">
|
||
{['Extend Expiry', 'Edit Details', 'Close Requirement'].map((action, idx) => (
|
||
<button type="button" style={`height:36px;border-radius:8px;border:1px solid ${idx === 2 ? '#FECACA' : '#E5E7EB'};background:${idx === 2 ? '#FEF2F2' : '#F9FAFB'};color:${idx === 2 ? '#B91C1C' : '#374151'};font-size:12px;font-weight:700`}>{action}</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const totalCount = requirementRows().length;
|
||
const draftCount = requirementRows().filter((item) => item.status === 'draft').length;
|
||
const submittedCount = requirementRows().filter((item) => item.status === 'under review').length;
|
||
const approvedCount = requirementRows().filter((item) => item.status === 'approved').length;
|
||
const activeCount = requirementRows().filter((item) => item.status === 'active').length;
|
||
const rejectedCount = requirementRows().filter((item) => item.status === 'rejected').length;
|
||
return (
|
||
<div style="display:flex;flex-direction:column;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)">
|
||
<div style="display:flex;align-items:center;justify-content:space-between;gap:10px">
|
||
<div>
|
||
<p style="margin:0;font-size:47px;line-height:1.05;font-weight:800;color:#111827">My Requirements</p>
|
||
<p style="margin:6px 0 0;font-size:14px;color:#6B7280">Create, submit for approval, and manage your requirements with precision and clarity.</p>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
onClick={() => { setRequirementsView('new'); setRequirementsStep(1); }}
|
||
style="height:38px;border-radius:10px;border:none;background:#03004E;color:white;padding:0 14px;font-size:12px;font-weight:700"
|
||
>
|
||
+ Post New Requirement
|
||
</button>
|
||
</div>
|
||
<div style="display:grid;grid-template-columns:2fr repeat(5,minmax(0,1fr));gap:8px;margin-top:10px">
|
||
{[
|
||
['Total Requirements', String(totalCount)],
|
||
['Drafts', String(draftCount)],
|
||
['Submitted', String(submittedCount)],
|
||
['Approved', String(approvedCount)],
|
||
['Active', String(activeCount)],
|
||
['Rejected', String(rejectedCount)],
|
||
].map(([label, value], idx) => (
|
||
<div style={`border:1px solid #E5E7EB;border-radius:10px;background:${idx === 0 ? '#FFF8F4' : '#F9FAFB'};padding:10px;${idx === 0 ? 'border-left:3px solid #FF5E13' : ''}`}>
|
||
<p style="margin:0;font-size:10px;letter-spacing:0.06em;text-transform:uppercase;color:#9CA3AF;font-weight:700">{label}</p>
|
||
<p style={`margin:6px 0 0;font-size:${idx === 0 ? '42px' : '36px'};line-height:1;font-weight:800;color:#111827`}>{value}</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:16px;overflow:hidden;box-shadow:0 1px 4px rgba(0,0,0,0.06)">
|
||
<div style="padding:10px 12px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px">
|
||
{['All Requirements', 'Drafts', 'Submitted', 'Approved', 'Active', 'Rejected', 'Closed', 'Expired'].map((label, idx) => (
|
||
<button type="button" style={`height:30px;padding:0 8px;border:none;border-bottom:${idx === 0 ? '2px solid #FF5E13' : '2px solid transparent'};background:none;font-size:12px;font-weight:700;color:${idx === 0 ? '#FF5E13' : '#6B7280'}`}>{label}</button>
|
||
))}
|
||
</div>
|
||
<div style="padding:10px 12px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px">
|
||
<div style="height:34px;flex:1;border:1px solid #E5E7EB;border-radius:8px;background:#F9FAFB;padding:0 10px;display:flex;align-items:center;gap:8px;font-size:12px;color:#9CA3AF"><Search size={14} /> Search ID or Title...</div>
|
||
{['Sort By: Newest', 'Category: All', 'Status: All'].map((item) => (
|
||
<button type="button" style="height:34px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 10px;font-size:12px;color:#374151">{item}</button>
|
||
))}
|
||
<button type="button" style="height:34px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 10px;font-size:12px;color:#374151">Export</button>
|
||
</div>
|
||
<div style="overflow-x:auto">
|
||
<table style="width:100%;border-collapse:collapse;min-width:920px">
|
||
<thead style="background:#F9FAFB">
|
||
<tr>
|
||
{['ID', 'Requirement Details', 'Category', 'Budget & Location', 'Submission', 'Status', 'Actions'].map((head) => (
|
||
<th style="padding:10px;text-align:left;font-size:10px;letter-spacing:0.06em;text-transform:uppercase;color:#9CA3AF">{head}</th>
|
||
))}
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<For each={filteredRows}>
|
||
{(row) => {
|
||
const status = statusMeta(row.status);
|
||
return (
|
||
<tr style="border-top:1px solid #F3F4F6">
|
||
<td style="padding:10px;font-size:12px;color:#9CA3AF;font-weight:700">{row.id}</td>
|
||
<td style="padding:10px">
|
||
<p style="margin:0;font-size:14px;font-weight:700;color:#111827">{row.title}</p>
|
||
<p style="margin:3px 0 0;font-size:12px;color:#6B7280">{row.summary}</p>
|
||
</td>
|
||
<td style="padding:10px"><span style="height:20px;padding:0 8px;border-radius:999px;border:1px solid #E5E7EB;background:#F3F4F6;display:inline-flex;align-items:center;font-size:10px;font-weight:700;color:#4B5563">{row.category}</span></td>
|
||
<td style="padding:10px">
|
||
<p style="margin:0;font-size:13px;font-weight:700;color:#111827">{row.budget}</p>
|
||
<p style="margin:2px 0 0;font-size:11px;color:#9CA3AF">{row.location}</p>
|
||
</td>
|
||
<td style="padding:10px;font-size:12px;color:#6B7280">{row.submission}</td>
|
||
<td style="padding:10px">
|
||
<span style={`height:22px;padding:0 10px;border-radius:999px;display:inline-flex;align-items:center;font-size:11px;font-weight:700;background:${status.bg};color:${status.c}`}>{status.label}</span>
|
||
<p style="margin:6px 0 0;font-size:11px;color:#1D4ED8;font-weight:700">{row.responseTag}</p>
|
||
</td>
|
||
<td style="padding:10px">
|
||
<div style="display:flex;gap:6px">
|
||
<button
|
||
type="button"
|
||
onClick={() => { setSelectedRequirementId(row.id); setRequirementsView('detail'); }}
|
||
style="height:28px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 10px;font-size:11px;font-weight:700;color:#374151"
|
||
>
|
||
View
|
||
</button>
|
||
<button type="button" style="height:28px;border-radius:8px;border:none;background:#03004E;padding:0 10px;font-size:11px;font-weight:700;color:white">Edit</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
);
|
||
}}
|
||
</For>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div style="padding:10px 12px;border-top:1px solid #E5E7EB;display:flex;justify-content:space-between;align-items:center">
|
||
<p style="margin:0;font-size:12px;color:#9CA3AF">Showing 1 to {Math.min(filteredRows.length, 6)} of {filteredRows.length} requirements</p>
|
||
<div style="display:flex;gap:6px">
|
||
{[1, 2, 3].map((p) => <button type="button" style={`width:28px;height:28px;border-radius:8px;border:1px solid #E5E7EB;background:${p === 1 ? '#FF5E13' : 'white'};color:${p === 1 ? 'white' : '#6B7280'};font-size:12px;font-weight:700`}>{p}</button>)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (customerKey() === 'received responses' || customerKey() === 'my responses') {
|
||
if (normalizeRoleKey(props.roleKey || '') !== 'CUSTOMER') {
|
||
const leadStatusPill = (status: string) => {
|
||
if (status === 'request_sent') return { c: '#374151', text: 'Request Sent' };
|
||
if (status === 'contact_unlocked') return { c: '#374151', text: 'Contact Unlocked' };
|
||
if (status === 'approved') return { c: '#374151', text: 'Approved' };
|
||
if (status === 'rejected') return { c: '#374151', text: 'Rejected' };
|
||
if (status === 'expired_refunded') return { c: '#374151', text: 'Expired · Refunded' };
|
||
if (status === 'cancelled_by_professional') return { c: '#374151', text: 'Cancelled' };
|
||
return { c: '#374151', text: titleCase(status) };
|
||
};
|
||
const responseSelectedLead = () => leadCards().find((item) => item.id === activeResponseLeadId()) || null;
|
||
const list = filteredRequestedRows().filter((row) => {
|
||
if (resolvedTabKey() === 'pending leads') return row.status === 'request_sent';
|
||
return true;
|
||
});
|
||
const totalPages = Math.max(1, Math.ceil(list.length / requestedPerPage));
|
||
const currentPage = Math.min(requestedPage(), totalPages);
|
||
const pagedList = list.slice((currentPage - 1) * requestedPerPage, (currentPage - 1) * requestedPerPage + requestedPerPage);
|
||
if (responsesDetailMode() && responseSelectedLead()) {
|
||
const lead = responseSelectedLead()!;
|
||
const spec = leadDetailsSpec(lead);
|
||
return (
|
||
<div style="display:flex;flex-direction:column;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)">
|
||
<div style="border:1px solid #E5E7EB;border-radius:12px;background:#FFFFFF;padding:14px">
|
||
<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:10px;flex-wrap:wrap">
|
||
<div>
|
||
<p style="margin:0;font-size:11px;letter-spacing:0.06em;text-transform:uppercase;color:#6B7280">My Responses • View Details</p>
|
||
<p style="margin:4px 0 0;font-size:24px;font-weight:800;color:#111827">{lead.title}</p>
|
||
<p style="margin:4px 0 0;font-size:12px;color:#6B7280">{lead.category} • {lead.location} • {lead.area}</p>
|
||
</div>
|
||
<button type="button" onClick={() => { setResponsesDetailMode(false); setActiveResponseLeadId(''); }} style="height:32px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:700;color:#374151">Back to My Responses</button>
|
||
</div>
|
||
<div style="margin-top:10px;display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px">
|
||
<div style="border:1px solid #E5E7EB;border-radius:10px;background:#F9FAFB;padding:10px;display:flex;gap:8px">
|
||
<span style="width:24px;height:24px;border-radius:999px;background:#FFF1EB;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0"><Calendar size={13} style="color:#FF5E13" /></span>
|
||
<div><p style="margin:0;font-size:11px;color:#6B7280;font-weight:700">Schedule</p><p style="margin:3px 0 0;font-size:13px;font-weight:800;color:#111827">{spec.timeframe}</p></div>
|
||
</div>
|
||
<div style="border:1px solid #E5E7EB;border-radius:10px;background:#F9FAFB;padding:10px;display:flex;gap:8px">
|
||
<span style="width:24px;height:24px;border-radius:999px;background:#FFF1EB;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0"><Calendar size={13} style="color:#FF5E13" /></span>
|
||
<div><p style="margin:0;font-size:11px;color:#6B7280;font-weight:700">Date Required</p><p style="margin:3px 0 0;font-size:13px;font-weight:800;color:#111827">{lead.dateRequired}</p></div>
|
||
</div>
|
||
<div style="border:1px solid #E5E7EB;border-radius:10px;background:#F9FAFB;padding:10px;display:flex;gap:8px">
|
||
<span style="width:24px;height:24px;border-radius:999px;background:#FFF1EB;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0"><MapPin size={13} style="color:#FF5E13" /></span>
|
||
<div><p style="margin:0;font-size:11px;color:#6B7280;font-weight:700">Area</p><p style="margin:3px 0 0;font-size:13px;font-weight:800;color:#111827">{String(lead.area || 'Chennai')}</p></div>
|
||
</div>
|
||
<div style="border:1px solid #E5E7EB;border-radius:10px;background:#F9FAFB;padding:10px;display:flex;gap:8px">
|
||
<span style="width:24px;height:24px;border-radius:999px;background:#FFF1EB;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0"><Coins size={13} style="color:#FF5E13" /></span>
|
||
<div><p style="margin:0;font-size:11px;color:#6B7280;font-weight:700">Unlock Cost</p><p style="margin:3px 0 0;font-size:13px;font-weight:800;color:#C2410C">{lead.cost} Tracecoin</p></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div style="margin-top:10px;display:grid;grid-template-columns:2fr 1fr;gap:10px">
|
||
<div style="display:grid;gap:10px">
|
||
<div style="border:1px solid #E5E7EB;border-radius:12px;background:white;padding:12px">
|
||
<p style="margin:0;font-size:11px;letter-spacing:0.05em;text-transform:uppercase;color:#6B7280;font-weight:700">Work Scope</p>
|
||
<p style="margin:8px 0 0;font-size:14px;color:#1F2937;line-height:1.65">{spec.scope}</p>
|
||
</div>
|
||
<div style="border:1px solid #E5E7EB;border-radius:12px;background:#F9FAFB;padding:12px">
|
||
<p style="margin:0 0 8px;font-size:11px;letter-spacing:0.05em;text-transform:uppercase;color:#6B7280;font-weight:700">Lead Specific Highlights</p>
|
||
<div style="display:grid;gap:6px"><For each={spec.highlights}>{(item) => <div style="display:flex;align-items:flex-start;gap:6px"><span style="margin-top:3px;width:6px;height:6px;border-radius:999px;background:#FF5E13;flex-shrink:0" /><span style="font-size:14px;color:#1F2937;line-height:1.55">{item}</span></div>}</For></div>
|
||
</div>
|
||
</div>
|
||
<div style="border:1px solid #E5E7EB;border-radius:12px;background:white;padding:12px;display:grid;gap:8px;align-content:start">
|
||
<div style="display:flex;justify-content:space-between;gap:8px;padding-bottom:6px;border-bottom:1px solid #F3F4F6"><span style="font-size:12px;color:#6B7280">Lead ID</span><span style="font-size:12px;color:#111827;font-weight:700">{lead.id}</span></div>
|
||
<div style="display:flex;justify-content:space-between;gap:8px;padding-bottom:6px;border-bottom:1px solid #F3F4F6"><span style="font-size:12px;color:#6B7280">Category</span><span style="font-size:12px;color:#111827;font-weight:700">{lead.category}</span></div>
|
||
<div style="display:flex;justify-content:space-between;gap:8px;padding-bottom:6px;border-bottom:1px solid #F3F4F6"><span style="font-size:12px;color:#6B7280">Location</span><span style="font-size:12px;color:#111827;font-weight:700">{lead.location}</span></div>
|
||
<div style="display:flex;justify-content:space-between;gap:8px;padding-bottom:6px;border-bottom:1px solid #F3F4F6"><span style="font-size:12px;color:#6B7280">Status</span><span style="font-size:12px;color:#111827;font-weight:700">From My Responses</span></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
return (
|
||
<div style="display:flex;flex-direction:column;gap:10px">
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:16px;overflow:hidden;box-shadow:0 1px 4px rgba(0,0,0,0.06)">
|
||
<div style="padding:10px 12px;border-bottom:1px solid #E5E7EB;display:flex;justify-content:space-between;align-items:center;gap:10px">
|
||
<p style="margin:0;font-size:18px;font-weight:800;color:#111827">My Responses</p>
|
||
<div style="display:flex;gap:8px">
|
||
<div style="position:relative">
|
||
<button type="button" onClick={() => { setRequestedSortOpen((prev) => !prev); setRequestedFilterOpen(false); }} style="display:inline-flex;height:32px;align-items:center;gap:6px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 10px;font-size:12px;color:#374151;font-weight:600">
|
||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#FF5E13" 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={requestedSortOpen()}>
|
||
<div style="position:absolute;right:0;top:36px;z-index:20;min-width:170px;border:1px solid #E5E7EB;border-radius:10px;background:white;padding:6px;box-shadow:0 8px 24px rgba(0,0,0,0.12)">
|
||
{['Newest First', 'Oldest First'].map((item) => (
|
||
<button type="button" onClick={() => { setRequestedSortFilter(item); setRequestedSortOpen(false); }} style={`display:block;width:100%;text-align:left;border:none;background:${requestedSortFilter() === item ? '#FFF1EB' : 'transparent'};color:${requestedSortFilter() === item ? '#FF5E13' : '#374151'};padding:8px 10px;border-radius:8px;font-size:12px;font-weight:600;cursor:pointer`}>
|
||
{item}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</Show>
|
||
</div>
|
||
<div style="position:relative">
|
||
<button type="button" onClick={() => { setRequestedFilterOpen((prev) => !prev); setRequestedSortOpen(false); }} style="display:inline-flex;height:32px;align-items:center;gap:6px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 10px;font-size:12px;color:#374151;font-weight:600">
|
||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#FF5E13" stroke-width="2"><path d="M3 5h18M6 12h12M10 19h4"/></svg>
|
||
Filter
|
||
</button>
|
||
<Show when={requestedFilterOpen()}>
|
||
<div style="position:absolute;right:0;top:36px;z-index:20;min-width:220px;border:1px solid #E5E7EB;border-radius:10px;background:white;padding:6px;box-shadow:0 8px 24px rgba(0,0,0,0.12)">
|
||
{['All Status', 'Request Sent', 'Contact Unlocked', 'Rejected', 'Expired Refunded', 'Cancelled By Professional'].map((item) => (
|
||
<button type="button" onClick={() => { setRequestedStatusFilter(item); setRequestedFilterOpen(false); }} style={`display:block;width:100%;text-align:left;border:none;background:${requestedStatusFilter() === item ? '#FFF1EB' : 'transparent'};color:${requestedStatusFilter() === item ? '#FF5E13' : '#374151'};padding:8px 10px;border-radius:8px;font-size:12px;font-weight:600;cursor:pointer`}>
|
||
{item}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</Show>
|
||
</div>
|
||
<button type="button" style="display:inline-flex;height:32px;align-items:center;gap:6px;border-radius:8px;border:none;background:#03004E;padding:0 10px;font-size:12px;font-weight:700;color:white"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>Export</button>
|
||
</div>
|
||
</div>
|
||
<div style="padding:10px 12px;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;gap:8px;flex-wrap:wrap">
|
||
<div style="height:32px;min-width:220px;flex:1;border:1px solid #E5E7EB;border-radius:8px;background:#F9FAFB;padding:0 10px;display:flex;align-items:center;gap:8px;font-size:12px;color:#9CA3AF">
|
||
<Search size={14} />
|
||
<input value={requestedSearch()} onInput={(e) => setRequestedSearch(e.currentTarget.value)} placeholder="Search by lead ID / title / area..." style="border:none;background:transparent;outline:none;width:100%;font-size:12px;color:#111827" />
|
||
</div>
|
||
<span style="height:24px;padding:0 10px;border-radius:999px;border:1px solid #E5E7EB;background:#F9FAFB;display:inline-flex;align-items:center;font-size:11px;font-weight:700;color:#6B7280">Sort: {requestedSortFilter()}</span>
|
||
<span style="height:24px;padding:0 10px;border-radius:999px;border:1px solid #E5E7EB;background:#F9FAFB;display:inline-flex;align-items:center;font-size:11px;font-weight:700;color:#6B7280">Status: {requestedStatusFilter()}</span>
|
||
</div>
|
||
<div style="max-height:420px;overflow:auto">
|
||
<table style="width:100%;border-collapse:collapse">
|
||
<thead style="background:#03004E;color:white">
|
||
<tr>
|
||
{['Lead ID', 'Lead Title', 'Request Date', 'Status', 'Cost', 'Decision Date', 'Action'].map((h) => (
|
||
<th style="padding:10px;text-align:left;font-size:10px;letter-spacing:0.06em;text-transform:uppercase">{h}</th>
|
||
))}
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<For each={pagedList}>
|
||
{(row) => {
|
||
const badge = leadStatusPill(row.status);
|
||
return (
|
||
<tr style="border-top:1px solid #F3F4F6">
|
||
<td style="padding:10px;font-size:12px;color:#4B5563;font-weight:700">#{row.id}</td>
|
||
<td style="padding:10px">
|
||
<p style="margin:0;font-size:14px;font-weight:700;color:#111827">{row.title}</p>
|
||
<p style="margin:2px 0 0;font-size:12px;color:#9CA3AF">{row.city}</p>
|
||
</td>
|
||
<td style="padding:10px;font-size:12px;color:#374151">{row.requestDate}</td>
|
||
<td style="padding:10px">
|
||
<span style={`display:inline-flex;align-items:center;font-size:12px;font-weight:600;color:${badge.c}`}>{badge.text}</span>
|
||
</td>
|
||
<td style="padding:10px;font-size:14px;font-weight:700;color:#111827">{leadCostPerContact}</td>
|
||
<td style="padding:10px;font-size:12px;color:#6B7280">{row.decisionDate}</td>
|
||
<td style="padding:10px">
|
||
<div style="display:flex;gap:6px;flex-wrap:wrap">
|
||
<button
|
||
type="button"
|
||
title="View Details"
|
||
aria-label="View Details"
|
||
onClick={() => openResponseLeadDetails(row.id)}
|
||
style="width:30px;height:30px;border-radius:8px;border:1px solid #E5E7EB;background:white;display:inline-flex;align-items:center;justify-content:center;color:#374151"
|
||
>
|
||
<Eye size={14} />
|
||
</button>
|
||
<Show when={row.status === 'request_sent'}>
|
||
<button
|
||
type="button"
|
||
title="Cancel Request"
|
||
aria-label="Cancel Request"
|
||
onClick={() => cancelLeadRequest(row.id)}
|
||
style="width:30px;height:30px;border-radius:8px;border:1px solid #E5E7EB;background:white;display:inline-flex;align-items:center;justify-content:center;color:#374151"
|
||
>
|
||
<X size={14} />
|
||
</button>
|
||
</Show>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
);
|
||
}}
|
||
</For>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div style="padding:10px 12px;border-top:1px solid #E5E7EB;display:flex;justify-content:space-between;align-items:center">
|
||
<p style="margin:0;font-size:12px;color:#6B7280">
|
||
Showing {pagedList.length ? (currentPage - 1) * requestedPerPage + 1 : 0} to {(currentPage - 1) * requestedPerPage + pagedList.length} of {list.length} responses
|
||
</p>
|
||
<div style="display:flex;gap:6px">
|
||
<For each={Array.from({ length: totalPages }, (_, i) => i + 1)}>
|
||
{(pageNo) => (
|
||
<button
|
||
type="button"
|
||
onClick={() => setRequestedPage(pageNo)}
|
||
style={`width:30px;height:30px;border-radius:8px;border:1px solid #E5E7EB;background:${currentPage === pageNo ? '#FF5E13' : 'white'};color:${currentPage === pageNo ? 'white' : '#6B7280'};font-size:12px;font-weight:700`}
|
||
>
|
||
{pageNo}
|
||
</button>
|
||
)}
|
||
</For>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const list = RESPONSE_ROWS.filter((r) => {
|
||
if (tab === 'new') return r.status === 'new';
|
||
if (tab === 'shortlisted') return r.status === 'shortlisted';
|
||
if (tab === 'rejected') return r.status === 'rejected';
|
||
return true;
|
||
});
|
||
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);display:grid;gap:8px">
|
||
<div style="display:flex;align-items:center;justify-content:space-between;gap:8px;padding:10px;border:1px solid #FFE2D3;border-radius:10px;background:#FFF8F4">
|
||
<p style="margin:0;font-size:12px;color:#9A3412;line-height:1.4">Before accepting contact request, review portfolio, experience, and quoted charges.</p>
|
||
<button
|
||
type="button"
|
||
onClick={openPortfolioPreviewInline}
|
||
style="height:30px;border-radius:8px;border:none;background:#03004E;color:white;padding:0 10px;font-size:12px;font-weight:700;white-space:nowrap"
|
||
>
|
||
Preview Portfolio
|
||
</button>
|
||
</div>
|
||
<For each={list}>{(row) => {
|
||
const chip = statusChip(row.status);
|
||
return (
|
||
<div style="display:grid;grid-template-columns:2fr 1fr 1fr;gap:8px;padding:10px;border:1px solid #E5E7EB;border-radius:8px;background:#fff">
|
||
<div>
|
||
<p style="margin:0;font-size:13px;font-weight:700;color:#111827">{row.name}</p>
|
||
<p style="margin:2px 0 0;font-size:12px;color:#6B7280">Applied for active requirement</p>
|
||
<p style="margin:6px 0 0;font-size:11px;font-weight:700;color:#FF5E13">Experience: 7+ years</p>
|
||
</div>
|
||
<div style="font-size:18px;font-weight:800;color:#111827">{row.quote}</div>
|
||
<div style="display:flex;align-items:center;justify-content:flex-end;gap:8px;flex-wrap:wrap">
|
||
<span style={`display:inline-flex;align-items:center;justify-content:center;height:22px;border-radius:999px;padding:0 8px;background:${chip.bg};color:${chip.c};font-size:11px;font-weight:700;text-transform:uppercase`}>{row.status}</span>
|
||
<button
|
||
type="button"
|
||
onClick={openPortfolioPreviewInline}
|
||
style="height:30px;border-radius:8px;border:1px solid #E5E7EB;background:white;color:#374151;padding:0 10px;font-size:12px;font-weight:700"
|
||
>
|
||
View Portfolio
|
||
</button>
|
||
<button type="button" style="height:30px;border-radius:8px;border:none;background:#0D0D2A;color:white;padding:0 10px;font-size:12px;font-weight:700">View Profile</button>
|
||
<button type="button" style="height:30px;border-radius:8px;border:none;background:#FF5E13;color:white;padding:0 10px;font-size:12px;font-weight:700">Accept Contact</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}}</For>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (customerKey() === 'shortlisted responses') {
|
||
const list = RESPONSE_ROWS.filter((r) => {
|
||
if (tab === 'interview scheduled') return r.status === 'shortlisted';
|
||
if (tab === 'finalized') return r.status === 'shortlisted';
|
||
return r.status === 'shortlisted';
|
||
});
|
||
return (
|
||
<div style="display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px">
|
||
<For each={list}>{(p) => (
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:16px;padding:12px;box-shadow:0 1px 4px rgba(0,0,0,0.06)">
|
||
<p style="margin:0;font-size:13px;font-weight:700;color:#111827">{p.name}</p>
|
||
<p style="margin:4px 0 0;font-size:12px;color:#6B7280">High-potential response for active requirement</p>
|
||
<p style="margin:8px 0 0;font-size:12px;font-weight:700;color:#FF5E13">Quoted {p.quote}</p>
|
||
<div style="display:flex;gap:8px;margin-top:10px">
|
||
<button type="button" style="height:30px;border-radius:8px;border:1px solid #E5E7EB;background:#F9FAFB;color:#374151;padding:0 10px;font-size:12px;font-weight:700">View Details</button>
|
||
<button type="button" style="height:30px;border-radius:8px;border:none;background:#FF5E13;color:white;padding:0 10px;font-size:12px;font-weight:700">Finalize</button>
|
||
</div>
|
||
</div>
|
||
)}</For>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (customerKey() === 'credits') {
|
||
const invoiceRows = [
|
||
['#INV-2023-089', 'Oct 14, 2023', 'Enterprise Pack', '₹499.00', 'Paid'],
|
||
['#INV-2023-074', 'Sep 14, 2023', 'Enterprise Pack', '₹499.00', 'Paid'],
|
||
['#INV-2023-052', 'Aug 14, 2023', 'Growth Starter', '₹135.00', 'Paid'],
|
||
['#INV-2023-031', 'Jul 14, 2023', 'Growth Starter', '₹135.00', 'Failed'],
|
||
] as const;
|
||
|
||
const renderCheckout = () => {
|
||
const pkg = checkoutPackage();
|
||
if (!pkg) return null;
|
||
const step = paymentStep();
|
||
|
||
return (
|
||
<div style="display:flex;flex-direction:column;gap:14px">
|
||
<div style="display:flex;align-items:center;gap:10px">
|
||
<button
|
||
type="button"
|
||
onClick={() => { setCheckoutPackage(null); setPaymentStep('idle'); }}
|
||
style="height:32px;border:1px solid #E5E7EB;background:white;border-radius:8px;padding:0 12px;font-size:12px;font-weight:700;color:#374151;cursor:pointer"
|
||
>
|
||
← Back to Packages
|
||
</button>
|
||
<h3 style="margin:0;font-size:18px;font-weight:800;color:#111827">Finalize Purchase</h3>
|
||
</div>
|
||
|
||
<div style="display:grid;grid-template-columns:1fr 1.2fr;gap:14px">
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:16px;padding:16px;box-shadow:0 1px 4px rgba(0,0,0,0.06)">
|
||
<p style="margin:0;font-size:11px;text-transform:uppercase;letter-spacing:0.05em;color:#6B7280;font-weight:700">Selected Package</p>
|
||
<h2 style="margin:8px 0;font-size:24px;font-weight:800;color:#111827">{pkg.display_name || pkg.name}</h2>
|
||
<div style="display:flex;align-items:center;gap:10px;margin-top:12px">
|
||
<div style="width:40px;height:40px;border-radius:12px;background:#FFF1EB;border:1px solid #FFD8C2;display:flex;align-items:center;justify-content:center">
|
||
<img src="/sidebar-icons/credits.svg" alt="" style={`width:20px;height:20px;object-fit:contain;filter:${ORANGE_ICON_FILTER}`} />
|
||
</div>
|
||
<div>
|
||
<p style="margin:0;font-size:18px;font-weight:800;color:#111827">{(Number(pkg.credits) + Number(pkg.bonus_credits || 0)).toLocaleString()} TC</p>
|
||
<p style="margin:0;font-size:12px;color:#6B7280">Total Tracecoins</p>
|
||
</div>
|
||
</div>
|
||
<div style="margin-top:16px;padding-top:16px;border-top:1px solid #F3F4F6">
|
||
<div style="display:flex;justify-content:space-between;margin-bottom:8px">
|
||
<span style="font-size:13px;color:#6B7280">Base Credits</span>
|
||
<span style="font-size:13px;font-weight:700;color:#111827">{Number(pkg.credits).toLocaleString()}</span>
|
||
</div>
|
||
<Show when={pkg.bonus_credits > 0}>
|
||
<div style="display:flex;justify-content:space-between;margin-bottom:8px">
|
||
<span style="font-size:13px;color:#6B7280">Bonus Credits</span>
|
||
<span style="font-size:13px;font-weight:700;color:#FF5E13">+{Number(pkg.bonus_credits).toLocaleString()}</span>
|
||
</div>
|
||
</Show>
|
||
|
||
{/* Coupon section */}
|
||
<Show when={!appliedCoupon()}>
|
||
<div style="margin-top:12px;padding:10px;border:1px dashed #D1D5DB;border-radius:10px;background:#F9FAFB">
|
||
<p style="margin:0 0 6px;font-size:11px;font-weight:700;color:#374151">Coupon Code</p>
|
||
<div style="display:flex;gap:6px">
|
||
<input
|
||
type="text"
|
||
placeholder="Enter code"
|
||
value={couponCode()}
|
||
onInput={(e) => setCouponCode(e.currentTarget.value)}
|
||
style="flex:1;height:32px;border:1px solid #E5E7EB;border-radius:6px;padding:0 8px;font-size:12px"
|
||
disabled={couponLoading()}
|
||
/>
|
||
<button
|
||
type="button"
|
||
onClick={() => void applyCoupon()}
|
||
disabled={couponLoading()}
|
||
style="height:32px;border:none;border-radius:6px;background:#03004E;color:white;padding:0 12px;font-size:12px;font-weight:700;cursor:pointer;opacity:${couponLoading() ? 0.6 : 1}"
|
||
>
|
||
{couponLoading() ? 'Applying...' : 'Apply'}
|
||
</button>
|
||
</div>
|
||
<Show when={couponError()}>
|
||
<p style="margin:4px 0 0;font-size:11px;color:#DC2626">{couponError()}</p>
|
||
</Show>
|
||
</div>
|
||
</Show>
|
||
<Show when={appliedCoupon()}>
|
||
<div style="margin-top:8px;display:flex;justify-content:space-between;font-size:13px;color:#15803D">
|
||
<span>Coupon ({appliedCoupon()!.code})</span>
|
||
<span>-{appliedCoupon()!.discount_type === 'PERCENT' ? `${appliedCoupon()!.discount_value}%` : `₹${appliedCoupon()!.discount_value}`}</span>
|
||
</div>
|
||
</Show>
|
||
|
||
<div style="display:flex;justify-content:space-between;margin-top:12px;padding-top:12px;border-top:2px solid #F3F4F6">
|
||
<span style="font-size:15px;font-weight:800;color:#111827">Total Amount</span>
|
||
<span style="font-size:18px;font-weight:800;color:#FF5E13">₹{((appliedCoupon() ? appliedCoupon().final_price_inr : Number(pkg.price_paise)) / 100).toLocaleString('en-IN')}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="border:1px solid #D9D8F9;background:#F5F5FF;border-radius:16px;padding:20px;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;box-shadow:0 1px 4px rgba(0,0,0,0.06)">
|
||
<Show when={step === 'idle'}>
|
||
<div style="width:56px;height:56px;border-radius:999px;background:white;display:flex;align-items:center;justify-content:center;margin-bottom:14px;box-shadow:0 2px 8px rgba(3,0,78,0.1)">
|
||
<img src="/sidebar-icons/security.svg" alt="" style={`width:28px;height:28px;object-fit:contain;filter:${BLUE_ICON_FILTER}`} />
|
||
</div>
|
||
<h3 style="margin:0;font-size:18px;font-weight:800;color:#03004E">Secure Gateway</h3>
|
||
<p style="margin:8px 0 20px;font-size:13px;color:#4F4B8A;max-width:240px">Continue to our secure partner gateway to complete the transaction.</p>
|
||
<button
|
||
type="button"
|
||
onClick={() => startPayment(pkg)}
|
||
style="height:44px;width:100%;max-width:220px;border:none;border-radius:10px;background:#03004E;color:white;font-size:14px;font-weight:700;cursor:pointer;box-shadow:0 4px 12px rgba(3,0,78,0.2)"
|
||
>
|
||
Confirm & Pay Now
|
||
</button>
|
||
</Show>
|
||
|
||
<Show when={step === 'processing' || step === 'verifying'}>
|
||
<div style="width:48px;height:48px;border:4px solid #E5E7EB;border-top-color:#FF5E13;border-radius:50%;animation:spin 1s linear infinite;margin-bottom:16px" />
|
||
<style>{`@keyframes spin { to { transform: rotate(360deg); } }`}</style>
|
||
<h3 style="margin:0;font-size:18px;font-weight:800;color:#111827">{step === 'processing' ? 'Creating Order...' : 'Verifying Payment...'}</h3>
|
||
<p style="margin:6px 0 0;font-size:13px;color:#6B7280">Please do not refresh or close the page.</p>
|
||
</Show>
|
||
|
||
<Show when={step === 'success'}>
|
||
<div style="width:64px;height:64px;border-radius:999px;background:#DCFCE7;display:flex;align-items:center;justify-content:center;margin-bottom:16px;border:2px solid #22C55E">
|
||
<span style="font-size:32px;color:#15803D">✓</span>
|
||
</div>
|
||
<h3 style="margin:0;font-size:22px;font-weight:800;color:#15803D">Payment Successful!</h3>
|
||
<p style="margin:8px 0 20px;font-size:13px;color:#15803D;max-width:280px">Your account has been credited with {(Number(pkg.credits) + Number(pkg.bonus_credits || 0)).toLocaleString()} Tracecoins.</p>
|
||
<div style="background:white;border:1px solid #DCFCE7;border-radius:12px;padding:12px;width:100%;max-width:260px;margin-bottom:20px">
|
||
<p style="margin:0;font-size:11px;color:#6B7280;text-transform:uppercase">New Balance</p>
|
||
<p style="margin:4px 0 0;font-size:20px;font-weight:800;color:#111827">{leadCredits().toLocaleString()} TC</p>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
onClick={() => { setCheckoutPackage(null); setPaymentStep('idle'); }}
|
||
style="height:40px;width:100%;max-width:220px;border:none;border-radius:10px;background:#03004E;color:white;font-size:13px;font-weight:700;cursor:pointer"
|
||
>
|
||
Back to Dashboard
|
||
</button>
|
||
</Show>
|
||
|
||
<Show when={step === 'error'}>
|
||
<div style="width:64px;height:64px;border-radius:999px;background:#FEE2E2;display:flex;align-items:center;justify-content:center;margin-bottom:16px;border:2px solid #EF4444">
|
||
<span style="font-size:32px;color:#B91C1C">✕</span>
|
||
</div>
|
||
<h3 style="margin:0;font-size:20px;font-weight:800;color:#B91C1C">Payment Failed</h3>
|
||
<p style="margin:8px 0 20px;font-size:13px;color:#6B7280;max-width:240px">Something went wrong with the mock gateway. Please try again.</p>
|
||
<button
|
||
type="button"
|
||
onClick={() => setPaymentStep('idle')}
|
||
style="height:40px;width:100%;max-width:220px;border:none;border-radius:10px;background:#B91C1C;color:white;font-size:13px;font-weight:700;cursor:pointer"
|
||
>
|
||
Try Again
|
||
</button>
|
||
</Show>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
const renderCreditManagement = () => {
|
||
let amtInput: HTMLInputElement | undefined;
|
||
let rsnInput: HTMLInputElement | undefined;
|
||
|
||
return (
|
||
<div style="display:flex;flex-direction:column;gap:14px">
|
||
<div style="display:flex;align-items:center;gap:10px">
|
||
<button
|
||
type="button"
|
||
onClick={() => setCreditManageView(false)}
|
||
style="height:32px;border:1px solid #E5E7EB;background:white;border-radius:8px;padding:0 12px;font-size:12px;font-weight:700;color:#374151;cursor:pointer"
|
||
>
|
||
← Back
|
||
</button>
|
||
<h3 style="margin:0;font-size:18px;font-weight:800;color:#111827">Credit Management Console</h3>
|
||
</div>
|
||
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:16px;padding:20px;box-shadow:0 1px 4px rgba(0,0,0,0.06)">
|
||
<div style="display:flex;align-items:center;gap:12px;margin-bottom:20px;padding-bottom:16px;border-bottom:1px solid #F3F4F6">
|
||
<div style="width:48px;height:48px;border-radius:999px;background:#FFF1EB;display:flex;align-items:center;justify-content:center">
|
||
<img src="/sidebar-icons/credits.svg" alt="" style={`width:24px;height:24px;object-fit:contain;filter:${ORANGE_ICON_FILTER}`} />
|
||
</div>
|
||
<div>
|
||
<p style="margin:0;font-size:12px;color:#6B7280;text-transform:uppercase;font-weight:700">Current User Balance</p>
|
||
<p style="margin:2px 0 0;font-size:24px;font-weight:800;color:#111827">{leadCredits().toLocaleString()} TC</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="display:grid;grid-template-columns:1fr 2fr 1fr;gap:12px;align-items:flex-end">
|
||
<div>
|
||
<label style="display:block;font-size:11px;font-weight:700;color:#6B7280;margin-bottom:6px;text-transform:uppercase">Amount (coins)</label>
|
||
<input
|
||
ref={amtInput}
|
||
type="number"
|
||
placeholder="e.g. 500 or -200"
|
||
style="width:100%;height:38px;border-radius:8px;border:1px solid #E5E7EB;padding:0 12px;font-size:13px;outline:none"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label style="display:block;font-size:11px;font-weight:700;color:#6B7280;margin-bottom:6px;text-transform:uppercase">Reason for adjustment</label>
|
||
<input
|
||
ref={rsnInput}
|
||
type="text"
|
||
placeholder="e.g. Compensation for downtime, Referral bonus..."
|
||
style="width:100%;height:38px;border-radius:8px;border:1px solid #E5E7EB;padding:0 12px;font-size:13px;outline:none"
|
||
/>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
const amount = Number(amtInput?.value || 0);
|
||
const reason = rsnInput?.value || 'Administrative Adjustment';
|
||
if (amount === 0) return;
|
||
adjustCreditsManually(amount, reason);
|
||
if (amtInput) amtInput.value = '';
|
||
if (rsnInput) rsnInput.value = '';
|
||
}}
|
||
style="height:38px;border:none;border-radius:8px;background:#03004E;color:white;font-size:13px;font-weight:700;cursor:pointer"
|
||
>
|
||
Apply Adjustment
|
||
</button>
|
||
</div>
|
||
<p style="margin:10px 0 0;font-size:11px;color:#6B7280">Note: Adjustments are final and logged in transaction history for auditing.</p>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
if (checkoutPackage()) return renderCheckout();
|
||
if (creditManageView()) return renderCreditManagement();
|
||
|
||
if (tab === 'buy credits') {
|
||
return (
|
||
<div style="display:flex;flex-direction:column;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);display:flex;align-items:center;justify-content:space-between;gap:12px">
|
||
<div>
|
||
<p style="margin:0;font-size:11px;text-transform:uppercase;letter-spacing:0.06em;color:#6B7280">Current Balance</p>
|
||
<p style="margin:6px 0 0;font-size:22px;font-weight:800;color:#111827;line-height:1">{leadCredits().toLocaleString()} <span style="font-size:14px;color:#64748B;font-weight:700">Tracecoins</span></p>
|
||
<p style="margin:6px 0 0;font-size:12px;color:#FF5E13;font-weight:700">+12% from last month</p>
|
||
</div>
|
||
<div style="display:flex;gap:10px">
|
||
<button
|
||
type="button"
|
||
onClick={() => setCreditManageView(true)}
|
||
style="height:36px;border:1px solid #E5E7EB;background:white;border-radius:8px;color:#374151;padding:0 14px;font-size:12px;font-weight:700;white-space:nowrap"
|
||
>
|
||
Manage Credits
|
||
</button>
|
||
<button type="button" style="height:36px;border:none;border-radius:8px;background:#03004E;color:white;padding:0 14px;font-size:12px;font-weight:700;white-space:nowrap">Buy Credits</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px">
|
||
<For each={pricingPackagesResource() || []}>
|
||
{(pkg) => (
|
||
<div style={`border:${pkg.is_popular ? '2px solid #FF5E13' : '1px solid #E5E7EB'};background:white;border-radius:12px;padding:14px;box-shadow:0 1px 3px rgba(0,0,0,0.05);position:relative`}>
|
||
<Show when={pkg.is_popular}>
|
||
<span style="position:absolute;top:-10px;right:10px;height:20px;padding:0 10px;border-radius:999px;background:#FF5E13;color:white;font-size:10px;font-weight:700;display:inline-flex;align-items:center">MOST POPULAR</span>
|
||
</Show>
|
||
<div style="width:28px;height:28px;border-radius:999px;background:#FFF1EB;border:1px solid #FFD8C2;display:flex;align-items:center;justify-content:center">
|
||
<img src="/sidebar-icons/credits.svg" alt="" style={`width:14px;height:14px;object-fit:contain;filter:${ORANGE_ICON_FILTER}`} />
|
||
</div>
|
||
<p style="margin:0;font-size:10px;letter-spacing:0.06em;text-transform:uppercase;font-weight:700;color:#9CA3AF">{pkg.name}</p>
|
||
<p style="margin:3px 0 0;font-size:20px;font-weight:800;color:#111827;line-height:1.1">{pkg.display_name || pkg.name}</p>
|
||
<p style="margin:8px 0 0;font-size:20px;font-weight:800;color:#111827;line-height:1">{Number(pkg.credits).toLocaleString()}</p>
|
||
<p style="margin:0;font-size:12px;color:#6B7280">Credits</p>
|
||
<Show when={pkg.bonus_credits > 0}>
|
||
<p style="margin:8px 0 0;font-size:10px;font-weight:700;color:#FF5E13;background:#FFF1EB;border:1px solid #FFD8C2;height:20px;padding:0 8px;border-radius:999px;display:inline-flex;align-items:center">+{pkg.bonus_credits} bonus</p>
|
||
</Show>
|
||
<p style="margin:10px 0 0;font-size:20px;font-weight:800;color:#111827;line-height:1">₹{(Number(pkg.price_paise) / 100).toLocaleString('en-IN')}</p>
|
||
<button
|
||
type="button"
|
||
onClick={() => setCheckoutPackage(pkg)}
|
||
style={`margin-top:10px;height:32px;width:100%;border:none;border-radius:8px;background:${pkg.is_popular ? '#FF5E13' : '#03004E'};color:white;font-size:12px;font-weight:700;cursor:pointer`}
|
||
>
|
||
Buy Package
|
||
</button>
|
||
</div>
|
||
)}
|
||
</For>
|
||
</div>
|
||
|
||
<div style="display:grid;grid-template-columns:2fr 1fr;gap:10px">
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:12px;padding:12px;box-shadow:0 1px 3px rgba(0,0,0,0.05)">
|
||
<p style="margin:0;font-size:13px;font-weight:700;color:#111827">Why buy larger packs?</p>
|
||
<div style="display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;margin-top:8px">
|
||
<div style="border:1px solid #E5E7EB;border-radius:10px;padding:10px;background:#F9FAFB">
|
||
<p style="margin:0;font-size:12px;font-weight:700;color:#111827">More savings</p>
|
||
<p style="margin:4px 0 0;font-size:11px;color:#6B7280">Higher packs include bonus credits.</p>
|
||
</div>
|
||
<div style="border:1px solid #E5E7EB;border-radius:10px;padding:10px;background:#F9FAFB">
|
||
<p style="margin:0;font-size:12px;font-weight:700;color:#111827">No expiry</p>
|
||
<p style="margin:4px 0 0;font-size:11px;color:#6B7280">Credits stay active with your account.</p>
|
||
</div>
|
||
<div style="border:1px solid #E5E7EB;border-radius:10px;padding:10px;background:#F9FAFB">
|
||
<p style="margin:0;font-size:12px;font-weight:700;color:#111827">Instant activation</p>
|
||
<p style="margin:4px 0 0;font-size:11px;color:#6B7280">Credits reflect right after purchase.</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="border:1px solid #E5E7EB;background:#03004E;border-radius:12px;padding:12px;color:white;box-shadow:0 1px 3px rgba(0,0,0,0.05)">
|
||
<p style="margin:0;font-size:11px;letter-spacing:0.06em;text-transform:uppercase;color:#C7D2FE">Recommended</p>
|
||
<p style="margin:6px 0 0;font-size:20px;font-weight:800;line-height:1.2">Start with Standard</p>
|
||
<p style="margin:6px 0 0;font-size:12px;color:#D7DBFF;line-height:1.45">Best value for active buying and response unlocks.</p>
|
||
<button type="button" style="margin-top:10px;height:32px;border:none;border-radius:8px;background:#FF5E13;color:white;padding:0 12px;font-size:12px;font-weight:700;width:100%">Buy Standard</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (tab === 'transactions') {
|
||
return (
|
||
<div style="display:flex;flex-direction:column;gap:10px">
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:16px;box-shadow:0 1px 4px rgba(0,0,0,0.06);overflow:hidden">
|
||
<div style="padding:12px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #E5E7EB;background:#F9FAFB">
|
||
<div style="display:flex;align-items:center;gap:8px;flex:1">
|
||
<input value="" readOnly placeholder="Search transactions..." style="height:34px;min-width:220px;flex:1;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;color:#6B7280;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">
|
||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#FF5E13" 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>
|
||
<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">
|
||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#FF5E13" stroke-width="2"><path d="M3 5h18M6 12h12M10 19h4"/></svg>
|
||
Filters
|
||
</button>
|
||
</div>
|
||
<button type="button" style="display:inline-flex;align-items:center;gap:6px;height:34px;border:none;border-radius:8px;background:#0D0D2A;color:white;padding:0 12px;font-size:12px;font-weight:600">
|
||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#FF5E13" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||
Export
|
||
</button>
|
||
</div>
|
||
<table style="width:100%;border-collapse:collapse">
|
||
<thead style="background:#03004E;color:white">
|
||
<tr>
|
||
{['Invoice No', 'Package', 'Credits', 'Amount Paid', 'Status', 'Date', 'Actions'].map((h) => (
|
||
<th style="padding:10px;font-size:11px;text-align:left;letter-spacing:0.04em;text-transform:uppercase">{h}</th>
|
||
))}
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{txRows().map((row) => (
|
||
<tr style="border-bottom:1px solid #E5E7EB">
|
||
<td style="padding:10px;font-size:12px;font-weight:700;color:#374151">{row[0]}</td>
|
||
<td style="padding:10px;font-size:12px;color:#111827">{row[1]}</td>
|
||
<td style="padding:10px;font-size:12px;color:#111827">{row[2]}</td>
|
||
<td style="padding:10px;font-size:12px;font-weight:700;color:#111827">{row[3]}</td>
|
||
<td style="padding:10px;font-size:12px">
|
||
<span style={`display:inline-flex;height:22px;padding:0 10px;border-radius:999px;font-size:11px;font-weight:700;align-items:center;background:${row[4] === 'Completed' ? '#FFF3EE' : row[4] === 'Pending' ? '#EEF2FF' : '#F3F4F6'};color:${row[4] === 'Completed' ? '#FF5E13' : row[4] === 'Pending' ? '#03004E' : '#6B7280'}`}>{row[4]}</span>
|
||
</td>
|
||
<td style="padding:10px;font-size:12px;color:#64748B">{row[5]}</td>
|
||
<td style="padding:10px">
|
||
<button type="button" style="display:inline-flex;align-items:center;gap:6px;height:28px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 10px;font-size:11px;font-weight:700;color:#374151">
|
||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="#FF5E13" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||
Download
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
<div style="padding:10px;display:flex;align-items:center;justify-content:space-between">
|
||
<p style="margin:0;font-size:12px;color:#64748B">Showing 1 to 4 of 42 transactions</p>
|
||
<div style="display:flex;gap:6px">
|
||
{[1, 2, 3].map((p) => (
|
||
<button type="button" style={`width:30px;height:30px;border-radius:8px;border:1px solid #E5E7EB;background:${p === 1 ? '#FF5E13' : '#fff'};color:${p === 1 ? 'white' : '#6B7280'};font-size:12px;font-weight:700`}>{p}</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (tab === 'usage history') {
|
||
return (
|
||
<div style="display:flex;flex-direction:column;gap:10px">
|
||
<div style="display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px">
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:12px;padding:12px;box-shadow:0 1px 3px rgba(0,0,0,0.05)"><div style="display:flex;align-items:center;gap:6px"><img src="/sidebar-icons/report.svg" alt="" style={`width:14px;height:14px;object-fit:contain;filter:${ORANGE_ICON_FILTER}`} /><p style="margin:0;font-size:11px;color:#6B7280;text-transform:uppercase">Total Used (30d)</p></div><p style="margin:8px 0 0;font-size:20px;font-weight:800;color:#111827">12,480</p></div>
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:12px;padding:12px;box-shadow:0 1px 3px rgba(0,0,0,0.05)"><div style="display:flex;align-items:center;gap:6px"><img src="/sidebar-icons/leads.svg" alt="" style={`width:14px;height:14px;object-fit:contain;filter:${ORANGE_ICON_FILTER}`} /><p style="margin:0;font-size:11px;color:#6B7280;text-transform:uppercase">Most Used Action</p></div><p style="margin:8px 0 0;font-size:18px;font-weight:800;color:#111827">Boost Profile</p></div>
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:12px;padding:12px;box-shadow:0 1px 3px rgba(0,0,0,0.05)"><div style="display:flex;align-items:center;gap:6px"><img src="/sidebar-icons/credits.svg" alt="" style={`width:14px;height:14px;object-fit:contain;filter:${ORANGE_ICON_FILTER}`} /><p style="margin:0;font-size:11px;color:#6B7280;text-transform:uppercase">Current Balance</p></div><p style="margin:8px 0 0;font-size:20px;font-weight:800;color:#111827">12,450 TC</p></div>
|
||
</div>
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:16px;box-shadow:0 1px 4px rgba(0,0,0,0.06);overflow:hidden">
|
||
<div style="padding:12px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #E5E7EB;background:#F9FAFB">
|
||
<div style="display:flex;align-items:center;gap:8px;flex:1">
|
||
<input value="" readOnly placeholder="Search usage history..." style="height:34px;min-width:220px;flex:1;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;color:#6B7280;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">
|
||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#FF5E13" 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>
|
||
<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">
|
||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#FF5E13" stroke-width="2"><path d="M3 5h18M6 12h12M10 19h4"/></svg>
|
||
Filters
|
||
</button>
|
||
</div>
|
||
<button type="button" style="display:inline-flex;align-items:center;gap:6px;height:34px;border:none;border-radius:8px;background:#0D0D2A;color:white;padding:0 12px;font-size:12px;font-weight:600">
|
||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#FF5E13" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||
Export
|
||
</button>
|
||
</div>
|
||
<table style="width:100%;border-collapse:collapse">
|
||
<thead style="background:#03004E;color:white">
|
||
<tr>
|
||
{['Usage ID', 'Action Type', 'Credits Used', 'Related ID', 'Date', 'Remarks'].map((h) => (
|
||
<th style="padding:10px;font-size:11px;text-align:left;letter-spacing:0.04em;text-transform:uppercase">{h}</th>
|
||
))}
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{[
|
||
['#US-99421', 'Unlock Contact', '-5.00', 'CONT-2034', 'Oct 24, 2023', 'Direct profile unlock for enterprise tier'],
|
||
['#US-99418', 'Boost Profile', '-25.00', 'PROF-8812', 'Oct 23, 2023', 'Featured placement (7 days)'],
|
||
['#US-99405', 'Batch Export', '-150.00', 'EXP-0044', 'Oct 22, 2023', 'Export of 300 leads'],
|
||
['#US-99388', 'Unlock Contact', '-5.00', 'CONT-1992', 'Oct 21, 2023', 'Individual unlock action'],
|
||
].map((row) => (
|
||
<tr style="border-bottom:1px solid #E5E7EB">
|
||
<td style="padding:10px;font-size:12px;font-weight:700;color:#64748B">{row[0]}</td>
|
||
<td style="padding:10px;font-size:12px;color:#111827">{row[1]}</td>
|
||
<td style="padding:10px;font-size:12px;font-weight:700;color:#03004E">{row[2]}</td>
|
||
<td style="padding:10px;font-size:12px;color:#64748B">{row[3]}</td>
|
||
<td style="padding:10px;font-size:12px;color:#64748B">{row[4]}</td>
|
||
<td style="padding:10px;font-size:12px;color:#64748B">{row[5]}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
<div style="padding:10px;display:flex;justify-content:space-between;align-items:center;border-top:1px solid #E5E7EB">
|
||
<p style="margin:0;font-size:12px;color:#64748B">Showing 1 to 4 of 142 results</p>
|
||
<div style="display:flex;gap:6px">
|
||
{[1, 2, 3].map((p) => (
|
||
<button type="button" style={`width:30px;height:30px;border-radius:8px;border:1px solid #E5E7EB;background:${p === 1 ? '#03004E' : '#fff'};color:${p === 1 ? 'white' : '#6B7280'};font-size:12px;font-weight:700`}>{p}</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (tab === 'invoices') {
|
||
return (
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:16px;box-shadow:0 1px 4px rgba(0,0,0,0.06);overflow:hidden">
|
||
<div style="padding:12px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #E5E7EB;background:#F9FAFB">
|
||
<div style="display:flex;align-items:center;gap:8px;flex:1">
|
||
<input value="" readOnly placeholder="Search invoices..." style="height:34px;min-width:220px;flex:1;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;color:#6B7280;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">
|
||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#FF5E13" 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>
|
||
<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">
|
||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#FF5E13" stroke-width="2"><path d="M3 5h18M6 12h12M10 19h4"/></svg>
|
||
Filters
|
||
</button>
|
||
</div>
|
||
<button type="button" style="display:inline-flex;align-items:center;gap:6px;height:34px;border:none;border-radius:8px;background:#0D0D2A;color:white;padding:0 12px;font-size:12px;font-weight:600">
|
||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#FF5E13" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||
Export
|
||
</button>
|
||
</div>
|
||
<table style="width:100%;border-collapse:collapse">
|
||
<thead style="background:#03004E;color:white">
|
||
<tr>
|
||
{['Invoice Number', 'Billing Date', 'Package', 'Total', 'Status'].map((h) => (
|
||
<th style="padding:10px;font-size:11px;text-align:left;letter-spacing:0.04em;text-transform:uppercase">{h}</th>
|
||
))}
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{invoiceRows.map((row) => (
|
||
<tr style="border-bottom:1px solid #E5E7EB">
|
||
<td style="padding:10px;font-size:12px;font-weight:700;color:#111827">{row[0]}</td>
|
||
<td style="padding:10px;font-size:12px;color:#6B7280">{row[1]}</td>
|
||
<td style="padding:10px;font-size:12px;color:#111827">{row[2]}</td>
|
||
<td style="padding:10px;font-size:12px;font-weight:700;color:#111827">{row[3]}</td>
|
||
<td style="padding:10px;font-size:12px">
|
||
<span style={`display:inline-flex;height:22px;padding:0 10px;border-radius:999px;font-size:11px;font-weight:700;align-items:center;background:${row[4] === 'Paid' ? '#FFF3EE' : '#F3F4F6'};color:${row[4] === 'Paid' ? '#FF5E13' : '#6B7280'}`}>{row[4]}</span>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
<div style="padding:10px;display:flex;justify-content:space-between;align-items:center;border-top:1px solid #E5E7EB">
|
||
<p style="margin:0;font-size:12px;color:#64748B">Showing 1 to 4 of 24 invoices</p>
|
||
<div style="display:flex;gap:6px">
|
||
{[1, 2, 3].map((p) => (
|
||
<button type="button" style={`width:30px;height:30px;border-radius:8px;border:1px solid #E5E7EB;background:${p === 1 ? '#03004E' : '#fff'};color:${p === 1 ? 'white' : '#6B7280'};font-size:12px;font-weight:700`}>{p}</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div style="display:flex;flex-direction:column;gap:10px">
|
||
<div style="display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px">
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:12px;padding:12px;box-shadow:0 1px 3px rgba(0,0,0,0.05)">
|
||
<div style="display:flex;align-items:center;gap:6px">
|
||
<img src="/sidebar-icons/credits.svg" alt="" style={`width:14px;height:14px;object-fit:contain;filter:${ORANGE_ICON_FILTER}`} />
|
||
<p style="margin:0;font-size:11px;color:#6B7280;text-transform:uppercase">Current Balance</p>
|
||
</div>
|
||
<p style="margin:8px 0 0;font-size:26px;font-weight:800;color:#111827">12,450 TC</p>
|
||
<p style="margin:4px 0 0;font-size:12px;color:#FF5E13;font-weight:700">+12% from last month</p>
|
||
</div>
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:12px;padding:12px;box-shadow:0 1px 3px rgba(0,0,0,0.05)">
|
||
<div style="display:flex;align-items:center;gap:6px">
|
||
<img src="/sidebar-icons/report.svg" alt="" style={`width:14px;height:14px;object-fit:contain;filter:${ORANGE_ICON_FILTER}`} />
|
||
<p style="margin:0;font-size:11px;color:#6B7280;text-transform:uppercase">Monthly Usage</p>
|
||
</div>
|
||
<p style="margin:8px 0 0;font-size:26px;font-weight:800;color:#111827">1,248</p>
|
||
<p style="margin:4px 0 0;font-size:12px;color:#6B7280">Avg 42/day</p>
|
||
</div>
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:12px;padding:12px;box-shadow:0 1px 3px rgba(0,0,0,0.05)">
|
||
<div style="display:flex;align-items:center;gap:6px">
|
||
<img src="/sidebar-icons/approval.svg" alt="" style={`width:14px;height:14px;object-fit:contain;filter:${ORANGE_ICON_FILTER}`} />
|
||
<p style="margin:0;font-size:11px;color:#6B7280;text-transform:uppercase">Pending Invoices</p>
|
||
</div>
|
||
<p style="margin:8px 0 0;font-size:26px;font-weight:800;color:#111827">1</p>
|
||
<button type="button" style="margin-top:8px;height:30px;border:none;border-radius:8px;background:#03004E;color:white;padding:0 10px;font-size:12px;font-weight:700">Buy Credits</button>
|
||
</div>
|
||
</div>
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:16px;box-shadow:0 1px 4px rgba(0,0,0,0.06);overflow:hidden">
|
||
<div style="padding:10px 12px;background:#F9FAFB;border-bottom:1px solid #E5E7EB;display:flex;justify-content:space-between;align-items:center">
|
||
<p style="margin:0;font-size:14px;font-weight:700;color:#111827">Recent Transactions</p>
|
||
<button type="button" style="height:30px;border:none;border-radius:8px;background:#03004E;color:white;padding:0 10px;font-size:12px;font-weight:700">Buy Credits</button>
|
||
</div>
|
||
<div style="padding:12px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #E5E7EB;background:#F9FAFB">
|
||
<div style="display:flex;align-items:center;gap:8px;flex:1">
|
||
<input value="" readOnly placeholder="Search transactions..." style="height:34px;min-width:220px;flex:1;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;color:#6B7280;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">
|
||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#FF5E13" 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>
|
||
<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">
|
||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#FF5E13" stroke-width="2"><path d="M3 5h18M6 12h12M10 19h4"/></svg>
|
||
Filters
|
||
</button>
|
||
</div>
|
||
<button type="button" style="display:inline-flex;align-items:center;gap:6px;height:34px;border:none;border-radius:8px;background:#0D0D2A;color:white;padding:0 12px;font-size:12px;font-weight:600">
|
||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#FF5E13" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||
Export
|
||
</button>
|
||
</div>
|
||
<table style="width:100%;border-collapse:collapse">
|
||
<thead style="background:#03004E;color:white">
|
||
<tr>
|
||
{['Transaction ID', 'Package', 'Credits', 'Amount Paid', 'Status', 'Date'].map((h) => (
|
||
<th style="padding:10px;font-size:11px;text-align:left;letter-spacing:0.04em;text-transform:uppercase">{h}</th>
|
||
))}
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{txRows().slice(0, 3).map((row: any) => (
|
||
<tr style="border-bottom:1px solid #E5E7EB">
|
||
<td style="padding:10px;font-size:12px;font-weight:700;color:#374151">{row[0]}</td>
|
||
<td style="padding:10px;font-size:12px;color:#111827">{row[1]}</td>
|
||
<td style="padding:10px;font-size:12px;color:#111827">{row[2]}</td>
|
||
<td style="padding:10px;font-size:12px;font-weight:700;color:#111827">{row[3]}</td>
|
||
<td style="padding:10px;font-size:12px">
|
||
<span style={`display:inline-flex;height:22px;padding:0 10px;border-radius:999px;font-size:11px;font-weight:700;align-items:center;background:${row[4] === 'Completed' ? '#FFF3EE' : row[4] === 'Pending' ? '#EEF2FF' : '#F3F4F6'};color:${row[4] === 'Completed' ? '#FF5E13' : row[4] === 'Pending' ? '#03004E' : '#6B7280'}`}>{row[4]}</span>
|
||
</td>
|
||
<td style="padding:10px;font-size:12px;color:#64748B">{row[5]}</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (customerKey() === 'explore nxtgauge') {
|
||
return (
|
||
<div style="display:flex;flex-direction:column;gap:12px">
|
||
<div style="border:1px solid #E5E7EB;border-radius:16px;background:white;box-shadow:0 1px 4px rgba(0,0,0,0.06);padding:20px">
|
||
<p style="margin:0;font-size:34px;line-height:1.12;font-weight:800;color:#111827;max-width:760px">Explore opportunities on Nxtgauge</p>
|
||
<p style="margin:10px 0 0;font-size:14px;line-height:1.5;max-width:760px;color:#6B7280">Discover services, connect with verified users, and expand into additional roles using the same dashboard workflow.</p>
|
||
</div>
|
||
|
||
<div style="display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px">
|
||
{[
|
||
{ key: 'COMPANY', title: 'Company', icon: '/sidebar-icons/users.svg', subtitle: 'Hire talent, post jobs, and manage applications in one path.' },
|
||
{ key: 'JOB_SEEKER', title: 'Job Seeker', icon: '/sidebar-icons/company.svg', subtitle: 'Explore opportunities and apply to roles with your profile.' },
|
||
{ key: 'SERVICE_SEEKER', title: 'Service Seeker', icon: '/sidebar-icons/candidate.svg', subtitle: 'Post requirements and connect with verified professionals.' },
|
||
].map((roleCard) => {
|
||
const activeRoles = userRoles();
|
||
const isRegistered = activeRoles.some((ar) => normalizeRoleKey(ar.role_key || ar.key) === normalizeRoleKey(roleCard.key));
|
||
const isCurrentRole = normalizeRoleKey(props.roleKey || '') === normalizeRoleKey(roleCard.key);
|
||
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);display:flex;flex-direction:column;gap:8px">
|
||
<img src={roleCard.icon} alt="" style={`width:20px;height:20px;object-fit:contain;filter:${ORANGE_ICON_FILTER}`} />
|
||
<p style="margin:0;font-size:18px;font-weight:800;color:#111827">{roleCard.title}</p>
|
||
<p style="margin:0;font-size:12px;line-height:1.5;color:#6B7280">{roleCard.subtitle}</p>
|
||
<button
|
||
type="button"
|
||
onClick={() => isCurrentRole ? null : isRegistered ? switchRole(roleCard.key) : registerRole(roleCard.key)}
|
||
style={`margin-top:auto;height:32px;border-radius:8px;border:none;background:${isCurrentRole ? '#E5E7EB' : '#03004E'};color:${isCurrentRole ? '#4B5563' : 'white'};padding:0 10px;font-size:12px;font-weight:700`}
|
||
>
|
||
{isCurrentRole ? 'Current Role' : isRegistered ? 'Switch' : `Register as ${roleCard.title}`}
|
||
</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)">
|
||
<p style="margin:0;font-size:30px;font-weight:800;color:#111827">Professional Services</p>
|
||
<p style="margin:6px 0 0;font-size:13px;color:#6B7280">Choose from 10 professional roles, including UGC Content Creator.</p>
|
||
<div style="display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:10px;margin-top:12px">
|
||
<For each={exploreRoleCards()}>
|
||
{(role) => (
|
||
<div style="border:1px solid #E5E7EB;border-radius:12px;background:white;padding:12px;display:flex;flex-direction:column;min-height:174px">
|
||
<div style="display:flex;align-items:center;justify-content:space-between">
|
||
<Show
|
||
when={role.iconAsset}
|
||
fallback={<role.Icon size={18} color="#FF5E13" strokeWidth={2.2} />}
|
||
>
|
||
<img src={role.iconAsset} alt="" style={`width:18px;height:18px;object-fit:contain;filter:${ORANGE_ICON_FILTER}`} />
|
||
</Show>
|
||
<span style={`display:inline-flex;align-items:center;height:20px;padding:0 8px;border-radius:999px;font-size:10px;font-weight:700;border:1px solid ${role.status === 'Registered' ? '#D1D5DB' : '#FFD8C2'};background:${role.status === 'Registered' ? '#F3F4F6' : '#FFF1EB'};color:${role.status === 'Registered' ? '#4B5563' : '#FF5E13'}`}>{role.status}</span>
|
||
</div>
|
||
<p style="margin:10px 0 0;font-size:18px;font-weight:800;color:#111827;line-height:1.2">{role.title}</p>
|
||
<p style="margin:6px 0 0;font-size:12px;line-height:1.45;color:#6B7280">{role.subtitle}</p>
|
||
<button
|
||
type="button"
|
||
onClick={() => role.action === 'Active' ? null : role.action === 'Switch' ? switchRole(role.key) : registerRole(role.key)}
|
||
style={`margin-top:auto;height:32px;border-radius:8px;border:none;background:${role.action === 'Register' ? '#03004E' : '#E5E7EB'};color:${role.action === 'Register' ? 'white' : '#4B5563'};padding:0 10px;font-size:12px;font-weight:700`}
|
||
>
|
||
{role.action}
|
||
</button>
|
||
</div>
|
||
)}
|
||
</For>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="border:1px solid #E5E7EB;background:linear-gradient(180deg,#FFFFFF 0%,#FFFAF7 100%);border-radius:20px;padding:18px;box-shadow:0 8px 20px rgba(15,23,42,0.06)">
|
||
<p style="margin:0;font-size:12px;letter-spacing:0.08em;text-transform:uppercase;font-weight:700;color:#FF5E13;text-align:center">Growth Advantage</p>
|
||
<p style="margin:4px 0 0;font-size:32px;font-weight:800;color:#111827;text-align:center;line-height:1.1">Why Add More Services?</p>
|
||
<p style="margin:8px auto 0;font-size:13px;line-height:1.5;color:#6B7280;text-align:center;max-width:760px">A multi-service profile helps you acquire more opportunities, improve trust, and scale consistently on a single platform.</p>
|
||
<div style="margin-top:14px;display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px">
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:14px;padding:14px;box-shadow:0 2px 6px rgba(0,0,0,0.05)">
|
||
<div style="width:34px;height:34px;border-radius:999px;background:#FFF3EE;display:flex;align-items:center;justify-content:center"><img src="/sidebar-icons/leads.svg" alt="" style={`width:16px;height:16px;object-fit:contain;filter:${ORANGE_ICON_FILTER}`} /></div>
|
||
<p style="margin:10px 0 0;font-size:15px;font-weight:800;color:#111827">Reach More Buyers</p>
|
||
<p style="margin:6px 0 0;font-size:12px;color:#6B7280;line-height:1.45">Get discovered by customers across multiple demand categories.</p>
|
||
</div>
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:14px;padding:14px;box-shadow:0 2px 6px rgba(0,0,0,0.05)">
|
||
<div style="width:34px;height:34px;border-radius:999px;background:#FFF3EE;display:flex;align-items:center;justify-content:center"><img src="/sidebar-icons/pricing.svg" alt="" style={`width:16px;height:16px;object-fit:contain;filter:${ORANGE_ICON_FILTER}`} /></div>
|
||
<p style="margin:10px 0 0;font-size:15px;font-weight:800;color:#111827">Increase Revenue Paths</p>
|
||
<p style="margin:6px 0 0;font-size:12px;color:#6B7280;line-height:1.45">Offer additional services and create new income streams.</p>
|
||
</div>
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:14px;padding:14px;box-shadow:0 2px 6px rgba(0,0,0,0.05)">
|
||
<div style="width:34px;height:34px;border-radius:999px;background:#FFF3EE;display:flex;align-items:center;justify-content:center"><img src="/sidebar-icons/approval.svg" alt="" style={`width:16px;height:16px;object-fit:contain;filter:${ORANGE_ICON_FILTER}`} /></div>
|
||
<p style="margin:10px 0 0;font-size:15px;font-weight:800;color:#111827">Strengthen Credibility</p>
|
||
<p style="margin:6px 0 0;font-size:12px;color:#6B7280;line-height:1.45">Verified multi-service profiles build confidence and improve conversion.</p>
|
||
</div>
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:14px;padding:14px;box-shadow:0 2px 6px rgba(0,0,0,0.05)">
|
||
<div style="width:34px;height:34px;border-radius:999px;background:#FFF3EE;display:flex;align-items:center;justify-content:center"><img src="/sidebar-icons/report.svg" alt="" style={`width:16px;height:16px;object-fit:contain;filter:${ORANGE_ICON_FILTER}`} /></div>
|
||
<p style="margin:10px 0 0;font-size:15px;font-weight:800;color:#111827">Scale Faster</p>
|
||
<p style="margin:6px 0 0;font-size:12px;color:#6B7280;line-height:1.45">Grow your business from one unified account and workflow.</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (customerKey() === 'verification') {
|
||
if (tab === 'approval status') {
|
||
return (
|
||
<div style="display:flex;flex-direction:column;gap:12px">
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:14px;padding:14px">
|
||
<p style="margin:0;font-size:22px;font-weight:800;color:#111827">Verification Center</p>
|
||
<p style="margin:6px 0 0;font-size:13px;color:#6B7280;line-height:1.5">Complete just 2 steps. Submit profile and portfolio. We review and unlock access.</p>
|
||
<div style="display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;margin-top:10px">
|
||
<div style="border:1px solid #E5E7EB;border-radius:10px;background:#F9FAFB;padding:10px">
|
||
<p style="margin:0;font-size:10px;letter-spacing:0.06em;text-transform:uppercase;color:#9CA3AF">Step 1</p>
|
||
<p style="margin:8px 0 0;font-size:13px;font-weight:800;color:#111827">Profile: {approvalTone(profileApprovalState()).label}</p>
|
||
</div>
|
||
<div style="border:1px solid #E5E7EB;border-radius:10px;background:#F9FAFB;padding:10px">
|
||
<p style="margin:0;font-size:10px;letter-spacing:0.06em;text-transform:uppercase;color:#9CA3AF">Step 2</p>
|
||
<p style="margin:8px 0 0;font-size:13px;font-weight:800;color:#111827">Portfolio: {approvalTone(portfolioApprovalState()).label}</p>
|
||
</div>
|
||
<div style="border:1px solid #E5E7EB;border-radius:10px;background:#F9FAFB;padding:10px">
|
||
<p style="margin:0;font-size:10px;letter-spacing:0.06em;text-transform:uppercase;color:#9CA3AF">Final Access</p>
|
||
<p style="margin:8px 0 0;font-size:13px;font-weight:800;color:#111827">{bothApprovalsApproved() ? 'Unlocked' : 'Blocked'}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:14px;padding:14px">
|
||
<p style="margin:0;font-size:18px;font-weight:800;color:#111827">Profile Verification</p>
|
||
<p style="margin:6px 0 0;font-size:12px;color:#6B7280">Finish your basic information and required documents, then submit.</p>
|
||
<div style="display:flex;gap:8px;justify-content:flex-end;margin-top:10px">
|
||
<button type="button" onClick={() => { props.onSidebarSelect('My Profile'); props.onTabSelect('basic information'); }} style="height:32px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:700;color:#374151">Open My Profile</button>
|
||
<button type="button" onClick={submitProfileForApproval} disabled={profileApprovalState() === 'IN_REVIEW'} style={`height:32px;border-radius:8px;border:none;background:${profileApprovalState() === 'IN_REVIEW' ? '#9CA3AF' : '#03004E'};color:white;padding:0 12px;font-size:12px;font-weight:700`}>Submit Profile</button>
|
||
</div>
|
||
</div>
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:14px;padding:14px">
|
||
<p style="margin:0;font-size:18px;font-weight:800;color:#111827">Portfolio Verification</p>
|
||
<p style="margin:6px 0 0;font-size:12px;color:#6B7280">Add portfolio details and submit separately for review.</p>
|
||
<div style="display:flex;gap:8px;justify-content:flex-end;margin-top:10px">
|
||
<button type="button" onClick={() => { props.onSidebarSelect('My Portfolio'); props.onTabSelect('about'); }} style="height:32px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:700;color:#374151">Open My Portfolio</button>
|
||
<button type="button" onClick={submitPortfolioForApproval} disabled={portfolioApprovalState() === 'IN_REVIEW'} style={`height:32px;border-radius:8px;border:none;background:${portfolioApprovalState() === 'IN_REVIEW' ? '#9CA3AF' : '#03004E'};color:white;padding:0 12px;font-size:12px;font-weight:700`}>Submit Portfolio</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div style="border:1px solid #E5E7EB;background:#F9FAFB;border-radius:12px;padding:12px">
|
||
<p style="margin:0;font-size:12px;color:#374151;line-height:1.5"><strong>Admin Flow:</strong> once submitted, both items appear in Verification Management. Admin can request documents or approve. Leads remain blocked until both are approved.</p>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
if (tab === 'submitted details') {
|
||
return (
|
||
<div style="display:grid;grid-template-columns:1fr 1.5fr;gap:12px">
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:14px;padding:14px">
|
||
<p style="margin:0;font-size:20px;font-weight:800;color:#111827">Application Snapshot</p>
|
||
<div style="display:grid;gap:8px;margin-top:10px">
|
||
{[
|
||
['Service', 'Social Media Manager'],
|
||
['Submission ID', 'VRF-2023-1042'],
|
||
['Submitted On', 'Oct 22, 2023'],
|
||
['Status', 'Under Review'],
|
||
].map(([k, v]) => (
|
||
<div style="padding:10px;border:1px solid #E5E7EB;border-radius:10px;background:#F9FAFB;font-size:12px;color:#374151"><strong>{k}:</strong> {v}</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:14px;padding:14px">
|
||
<p style="margin:0;font-size:20px;font-weight:800;color:#111827">Submitted Details</p>
|
||
<div style="margin-top:10px;padding:10px;border:1px solid #E5E7EB;border-radius:10px;background:#F9FAFB;font-size:12px;line-height:1.6;color:#374151">
|
||
Strategic Social Media Manager with 6+ years of experience in content strategy, campaign planning, and audience growth for D2C brands.
|
||
</div>
|
||
<div style="display:flex;gap:6px;flex-wrap:wrap;margin-top:10px">
|
||
{['Sprout Social', 'Figma', 'Google Analytics'].map((tool) => <span style="height:24px;padding:0 10px;border-radius:999px;border:1px solid #E5E7EB;background:white;display:inline-flex;align-items:center;font-size:11px;color:#374151">{tool}</span>)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
if (tab === 'documents') {
|
||
return (
|
||
<div style="display:grid;gap:12px">
|
||
<div style="display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px">
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:12px;padding:12px">
|
||
<p style="margin:0;font-size:10px;letter-spacing:0.06em;text-transform:uppercase;color:#9CA3AF">Total Documents</p>
|
||
<p style="margin:6px 0 0;font-size:22px;font-weight:800;color:#111827">3</p>
|
||
</div>
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:12px;padding:12px">
|
||
<p style="margin:0;font-size:10px;letter-spacing:0.06em;text-transform:uppercase;color:#9CA3AF">Approved</p>
|
||
<p style="margin:6px 0 0;font-size:22px;font-weight:800;color:#03004E">2</p>
|
||
</div>
|
||
<div style="border:1px solid #FFE2D3;background:#FFF8F4;border-radius:12px;padding:12px">
|
||
<p style="margin:0;font-size:10px;letter-spacing:0.06em;text-transform:uppercase;color:#9CA3AF">Needs Action</p>
|
||
<p style="margin:6px 0 0;font-size:22px;font-weight:800;color:#FF5E13">1</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="border:1px solid #FFE2D3;border-radius:12px;background:#FFF8F4;padding:12px;display:flex;align-items:center;justify-content:space-between;gap:10px">
|
||
<div>
|
||
<p style="margin:0;font-size:12px;font-weight:800;color:#111827">Admin requested missing documents</p>
|
||
<p style="margin:4px 0 0;font-size:12px;color:#374151">Required Missing Documents: Address Proof (clear PDF/JPG/PNG).</p>
|
||
</div>
|
||
<button type="button" style="height:32px;border:none;border-radius:8px;background:#03004E;color:white;padding:0 12px;font-size:12px;font-weight:700;white-space:nowrap">Upload Missing</button>
|
||
</div>
|
||
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:14px;overflow:hidden">
|
||
<div style="padding:12px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #E5E7EB">
|
||
<p style="margin:0;font-size:18px;font-weight:800;color:#111827">Documents</p>
|
||
<button type="button" style="height:32px;border:none;border-radius:8px;background:#03004E;color:white;padding:0 12px;font-size:12px;font-weight:700">Upload New</button>
|
||
</div>
|
||
<table style="width:100%;border-collapse:collapse">
|
||
<thead>
|
||
<tr style="border-bottom:1px solid #E5E7EB;background:#F9FAFB">
|
||
<th style="text-align:left;padding:10px;font-size:11px;color:#6B7280">Document</th>
|
||
<th style="text-align:left;padding:10px;font-size:11px;color:#6B7280">File</th>
|
||
<th style="text-align:left;padding:10px;font-size:11px;color:#6B7280">Status</th>
|
||
<th style="text-align:right;padding:10px;font-size:11px;color:#6B7280">Action</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{[
|
||
['Identity Proof', 'id_scan_v2.pdf', 'Approved'],
|
||
['Address Proof', 'electricity_bill.jpg', 'Rejected'],
|
||
['Tax Document', 'itr_form16_2023.pdf', 'Approved'],
|
||
].map(([doc, file, state]) => (
|
||
<tr style="border-top:1px solid #F3F4F6">
|
||
<td style="padding:10px;font-size:12px;color:#111827;font-weight:600">{doc}</td>
|
||
<td style="padding:10px;font-size:12px;color:#374151">{file}</td>
|
||
<td style="padding:10px">
|
||
<span style={`height:22px;padding:0 8px;border-radius:999px;border:1px solid ${state === 'Rejected' ? '#FFD8C2' : '#DDEBFF'};background:${state === 'Rejected' ? '#FFF1EB' : '#EEF4FF'};display:inline-flex;align-items:center;font-size:10px;font-weight:700;color:${state === 'Rejected' ? '#FF5E13' : '#03004E'}`}>{state}</span>
|
||
</td>
|
||
<td style="padding:10px;text-align:right">
|
||
<Show
|
||
when={state === 'Rejected'}
|
||
fallback={<span style="font-size:11px;color:#9CA3AF">No action</span>}
|
||
>
|
||
<button type="button" style="height:28px;border:1px solid #D1D5DB;border-radius:8px;background:white;color:#374151;padding:0 10px;font-size:11px;font-weight:700">Re-upload</button>
|
||
</Show>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div style="border:1px solid #FFE2D3;border-radius:12px;background:#FFF8F4;padding:10px;font-size:12px;color:#374151">
|
||
Address Proof needs re-upload to continue verification.
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
if (tab === 're-upload') {
|
||
return (
|
||
<div style="display:grid;grid-template-columns:1fr 1.5fr;gap:12px">
|
||
<div style="border:1px solid #FFE2D3;background:#FFF8F4;border-radius:14px;padding:12px">
|
||
<p style="margin:0;font-size:12px;color:#FF5E13;font-weight:700">1 item needs correction</p>
|
||
<p style="margin:8px 0 0;font-size:12px;color:#374151">Identity proof is unclear. Please upload a clear copy.</p>
|
||
</div>
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:14px;padding:12px">
|
||
<p style="margin:0;font-size:20px;font-weight:800;color:#111827">Upload Correction</p>
|
||
<input type="file" style="margin-top:10px;font-size:12px;color:#374151" />
|
||
<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:10px">
|
||
<button type="button" style="height:32px;border-radius:8px;border:1px solid #D1D5DB;background:white;color:#374151;padding:0 12px;font-size:12px;font-weight:700">Cancel</button>
|
||
<button type="button" style="height:32px;border-radius:8px;border:none;background:#03004E;color:white;padding:0 12px;font-size:12px;font-weight:700">Submit</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
if (tab === 'activity') {
|
||
return (
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:14px;overflow:hidden">
|
||
<div style="padding:12px;border-bottom:1px solid #E5E7EB">
|
||
<p style="margin:0;font-size:20px;font-weight:800;color:#111827">Verification Updates</p>
|
||
</div>
|
||
<div style="padding:12px;display:grid;gap:8px">
|
||
{[
|
||
['Today, 11:30 AM', 'Correction submitted', 'Completed'],
|
||
['Today, 10:45 AM', 'Re-upload requested', 'Action Needed'],
|
||
['Yesterday, 04:20 PM', 'Reviewer note added', 'Updated'],
|
||
['Yesterday, 11:10 AM', 'Verification started', 'Started'],
|
||
].map(([time, title, state]) => (
|
||
<div style="display:flex;align-items:center;justify-content:space-between;gap:10px;padding:10px;border:1px solid #E5E7EB;border-radius:10px;background:#F9FAFB">
|
||
<div>
|
||
<p style="margin:0;font-size:12px;font-weight:700;color:#111827">{title}</p>
|
||
<p style="margin:2px 0 0;font-size:11px;color:#6B7280">{time}</p>
|
||
</div>
|
||
<span style={`height:22px;padding:0 8px;border-radius:999px;border:1px solid ${state === 'Action Needed' ? '#FFD8C2' : '#DDEBFF'};background:${state === 'Action Needed' ? '#FFF1EB' : '#EEF4FF'};display:inline-flex;align-items:center;font-size:10px;font-weight:700;color:${state === 'Action Needed' ? '#FF5E13' : '#03004E'}`}>{state}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
return (
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:14px;padding:14px">
|
||
<p style="margin:0;font-size:14px;font-weight:700;color:#111827">Details will appear here</p>
|
||
<p style="margin:6px 0 0;font-size:12px;color:#6B7280">This section is being prepared. Please use the available tabs above.</p>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (customerKey() === 'help center' || customerKey() === 'support') {
|
||
const active = helpCenterTab();
|
||
return (
|
||
<div style="display:flex;flex-direction:column;gap:12px">
|
||
<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:28px;line-height:1.1;font-weight:800;color:#111827">Help Center</p>
|
||
<p style="margin:8px 0 0;font-size:14px;color:#6B7280">Get help, manage tickets, and contact support.</p>
|
||
<div style="display:flex;align-items:center;gap:24px;margin-top:12px;border-bottom:1px solid #E5E7EB;padding-bottom:8px">
|
||
<button type="button" onClick={() => setHelpCenterTab('help_center')} style={`border:none;background:none;padding:0 0 8px;font-size:13px;font-weight:${active === 'help_center' ? 700 : 500};color:${active === 'help_center' ? '#FF5E13' : '#6B7280'};border-bottom:${active === 'help_center' ? '2px solid #FF5E13' : '2px solid transparent'};cursor:pointer`}>Help Center</button>
|
||
<button type="button" onClick={() => setHelpCenterTab('my_tickets')} style={`border:none;background:none;padding:0 0 8px;font-size:13px;font-weight:${active === 'my_tickets' ? 700 : 500};color:${active === 'my_tickets' ? '#FF5E13' : '#6B7280'};border-bottom:${active === 'my_tickets' ? '2px solid #FF5E13' : '2px solid transparent'};cursor:pointer`}>My Tickets</button>
|
||
<button type="button" onClick={() => setHelpCenterTab('create_ticket')} style={`border:none;background:none;padding:0 0 8px;font-size:13px;font-weight:${active === 'create_ticket' ? 700 : 500};color:${active === 'create_ticket' ? '#FF5E13' : '#6B7280'};border-bottom:${active === 'create_ticket' ? '2px solid #FF5E13' : '2px solid transparent'};cursor:pointer`}>Create Ticket</button>
|
||
</div>
|
||
</div>
|
||
|
||
<Show when={active === 'help_center'}>
|
||
<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.08em;text-transform:uppercase;font-weight:700;color:#FF5E13">Knowledge Base</p>
|
||
<div style="display:grid;grid-template-columns:1fr 1.8fr auto;gap:8px;margin-top:10px">
|
||
<select
|
||
value={kbCategory()}
|
||
onChange={(e) => setKbCategory(e.currentTarget.value)}
|
||
style="height:36px;border:1px solid #D1D5DB;border-radius:10px;background:white;padding:0 12px;display:flex;align-items:center;font-size:12px;color:#111827;outline:none"
|
||
>
|
||
<option value="All">All Categories</option>
|
||
<For each={kbCategoriesWithCounts()}>{cat => <option value={cat.title}>{cat.title}</option>}</For>
|
||
</select>
|
||
<div style="position:relative">
|
||
<input
|
||
type="text"
|
||
placeholder="Search help articles, tickets, and guides"
|
||
value={kbSearch()}
|
||
onInput={(e) => setKbSearch(e.currentTarget.value)}
|
||
style="width:100%;height:36px;border:1px solid #D1D5DB;border-radius:10px;background:white;padding:0 12px;font-size:12px;color:#111827;outline:none"
|
||
/>
|
||
</div>
|
||
<button type="button" style="height:36px;width:44px;border:none;border-radius:10px;background:#0D0D2A;color:white;font-size:16px;font-weight:700">⌕</button>
|
||
</div>
|
||
<div style="display:flex;align-items:center;gap:8px;margin-top:10px">
|
||
<span style="font-size:11px;color:#6B7280">Popular:</span>
|
||
{['Verification', 'Credits', 'Lead Response'].map((tag) => <button onClick={() => setKbSearch(tag)} style="height:22px;padding:0 10px;border-radius:999px;border:1px solid #E5E7EB;background:#F9FAFB;color:#374151;display:inline-flex;align-items:center;font-size:11px;cursor:pointer">{tag}</button>)}
|
||
</div>
|
||
</div>
|
||
|
||
<div style="display:flex;justify-content:space-between;align-items:center">
|
||
<p style="margin:0;font-size:24px;font-weight:800;color:#111827;line-height:1.1">Browse by Category</p>
|
||
</div>
|
||
|
||
<div style="display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px">
|
||
<For each={kbCategoriesWithCounts()}>{(cat) => (
|
||
<button
|
||
onClick={() => setKbCategory(cat.title)}
|
||
style="text-align:left;border:1px solid #E5E7EB;background:white;border-radius:14px;padding:12px;box-shadow:0 1px 4px rgba(0,0,0,0.05);cursor:pointer;transition:all 0.2s"
|
||
onMouseEnter={(e) => e.currentTarget.style.borderColor = '#FF5E13'}
|
||
onMouseLeave={(e) => e.currentTarget.style.borderColor = '#E5E7EB'}
|
||
>
|
||
<div style={`width:32px;height:32px;border-radius:8px;background:#FFF3EE;display:flex;align-items:center;justify-content:center;margin-bottom:10px`}>
|
||
<BookOpen size={16} color="#FF5E13" />
|
||
</div>
|
||
<p style="margin:0;font-size:14px;font-weight:800;color:#111827">{cat.title}</p>
|
||
<p style="margin:6px 0 0;font-size:11px;line-height:1.45;color:#6B7280">{cat.description}</p>
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:10px">
|
||
<span style="font-size:10px;font-weight:700;color:#9CA3AF;text-transform:uppercase">{cat.articles} Articles</span>
|
||
</div>
|
||
</button>
|
||
)}</For>
|
||
</div>
|
||
|
||
<div style="display:grid;grid-template-columns:100%;gap:12px">
|
||
<div>
|
||
<div style="display:flex;justify-content:space-between;align-items:end;margin-bottom:10px">
|
||
<p style="margin:0;font-size:24px;font-weight:800;color:#111827;line-height:1.1">
|
||
{kbSearch() || kbCategory() !== 'All' ? 'Search Results' : 'Important Articles'}
|
||
</p>
|
||
<Show when={kbSearch() || kbCategory() !== 'All'}>
|
||
<button onClick={() => { setKbSearch(''); setKbCategory('All'); }} style="font-size:11px;color:#FF5E13;font-weight:700;background:none;border:none;cursor:pointer">Clear Filters</button>
|
||
</Show>
|
||
</div>
|
||
<div style="display:grid;grid-template-columns:repeat(2, 1fr);gap:8px">
|
||
<For each={filteredKbArticles()}>{(a) => (
|
||
<button type="button" style="height:auto;min-height:44px;border:1px solid #E5E7EB;background:white;border-radius:10px;padding:12px;display:flex;justify-content:space-between;align-items:center;cursor:pointer;transition:all 0.15s">
|
||
<div style="display:flex;flex-direction:column;gap:2px;text-align:left">
|
||
<span style="font-[10px];font-weight:700;color:#FF5E13;text-transform:uppercase;letter-spacing:0.02em">{a.category}</span>
|
||
<span style="font-size:13px;font-weight:700;color:#111827">{a.title}</span>
|
||
<span style="font-size:11px;color:#6B7280;line-clamp:1;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical">{a.content}</span>
|
||
</div>
|
||
<span style="font-size:14px;color:#9CA3AF">›</span>
|
||
</button>
|
||
)}</For>
|
||
<Show when={filteredKbArticles().length === 0}>
|
||
<div style="grid-column:1/-1;padding:40px;text-align:center;background:#F9FAFB;border-radius:12px;border:1px dashed #D1D5DB">
|
||
<AlertCircle size={24} color="#9CA3AF" style="margin-bottom:8px" />
|
||
<p style="margin:0;font-size:13px;color:#6B7280">No articles found matching your criteria.</p>
|
||
</div>
|
||
</Show>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px">
|
||
<p style="margin:0;font-size:24px;font-weight:800;color:#111827;line-height:1.1">Quick Guides</p>
|
||
<span style="font-size:11px;color:#9CA3AF">Actionable tutorials to master Nxtgauge</span>
|
||
</div>
|
||
<div style="display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px">
|
||
{[
|
||
{ title: 'The First 24 Hours: Setup', text: 'Complete your core profile requirements and find your first lead fast.', read: '8 min read', level: 'Beginner', Icon: Rocket },
|
||
{ title: 'Maximizing Lead Conversions', text: 'Use response quality and timing strategies to improve closure rate.', read: '15 min read', level: 'Growth', Icon: TrendingUp },
|
||
{ title: 'Data Privacy & Security', text: 'Understand how we protect your business and financial information.', read: '5 min read', level: 'Essential', Icon: ShieldCheck },
|
||
].map((g) => (
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:12px;overflow:hidden;box-shadow:0 1px 4px rgba(0,0,0,0.05)">
|
||
<div style="padding:10px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid #F3F4F6;background:#FAFAFA">
|
||
<span style="height:22px;padding:0 8px;border-radius:999px;background:#FFF1EB;color:#FF5E13;font-size:10px;font-weight:700;display:inline-flex;align-items:center;border:1px solid #FFD8C2">{g.level}</span>
|
||
<span style="width:24px;height:24px;border-radius:999px;background:#FFF1EB;display:inline-flex;align-items:center;justify-content:center;color:#FF5E13;border:1px solid #FFD8C2">
|
||
<g.Icon size={13} />
|
||
</span>
|
||
</div>
|
||
<div style="padding:12px">
|
||
<p style="margin:0;font-size:14px;font-weight:800;color:#111827">{g.title}</p>
|
||
<p style="margin:6px 0 0;font-size:12px;line-height:1.45;color:#6B7280">{g.text}</p>
|
||
<div style="margin-top:10px;display:flex;align-items:center;justify-content:space-between">
|
||
<p style="margin:0;font-size:11px;color:#9CA3AF;text-transform:uppercase">{g.read}</p>
|
||
<button type="button" style="height:26px;border-radius:7px;border:1px solid #D1D5DB;background:white;color:#374151;padding:0 9px;font-size:11px;font-weight:700">Read</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</Show>
|
||
|
||
<Show when={active === 'my_tickets'}>
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:16px;padding:16px;box-shadow:0 1px 4px rgba(0,0,0,0.06)">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;gap:10px">
|
||
<p style="margin:0;font-size:24px;line-height:1.1;font-weight:800;color:#111827">My Tickets</p>
|
||
<div style="display:flex;gap:8px">
|
||
<span style="height:26px;padding:0 10px;border-radius:999px;border:1px solid #E5E7EB;background:#F9FAFB;display:inline-flex;align-items:center;font-size:11px;color:#374151;font-weight:600">Open: {ticketSummary().openCount}</span>
|
||
<span style="height:26px;padding:0 10px;border-radius:999px;border:1px solid #E5E7EB;background:#F9FAFB;display:inline-flex;align-items:center;font-size:11px;color:#374151;font-weight:600">Resolved: {ticketSummary().resolvedCount}</span>
|
||
</div>
|
||
</div>
|
||
<div style="margin-top:10px;border:1px solid #E5E7EB;border-radius:10px;background:#F9FAFB;padding:10px 12px">
|
||
<p style="margin:0;font-size:12px;color:#6B7280">You have {ticketSummary().total} tickets. {ticketSummary().openCount} still need action and {ticketSummary().resolvedCount} are resolved.</p>
|
||
</div>
|
||
<div style="margin-top:14px;display:flex;align-items:center;gap:18px;border-bottom:1px solid #E5E7EB;padding-bottom:8px">
|
||
<button type="button" onClick={() => setMyTicketsTab('all_tickets')} style={`border:none;background:none;padding:0 0 8px;font-size:13px;font-weight:${myTicketsTab() === 'all_tickets' ? 700 : 500};color:${myTicketsTab() === 'all_tickets' ? '#FF5E13' : '#6B7280'};border-bottom:${myTicketsTab() === 'all_tickets' ? '2px solid #FF5E13' : '2px solid transparent'};cursor:pointer`}>All Tickets</button>
|
||
<button type="button" onClick={() => setMyTicketsTab('view_ticket')} style={`border:none;background:none;padding:0 0 8px;font-size:13px;font-weight:${myTicketsTab() === 'view_ticket' ? 700 : 500};color:${myTicketsTab() === 'view_ticket' ? '#FF5E13' : '#6B7280'};border-bottom:${myTicketsTab() === 'view_ticket' ? '2px solid #FF5E13' : '2px solid transparent'};cursor:pointer`}>View Ticket</button>
|
||
</div>
|
||
|
||
<Show when={myTicketsTab() === 'all_tickets'}>
|
||
<div style="margin-top:12px;border:1px solid #E5E7EB;border-radius:12px;overflow:hidden;background:white">
|
||
<table style="width:100%;border-collapse:collapse">
|
||
<thead>
|
||
<tr style="background:#F9FAFB;border-bottom:1px solid #E5E7EB">
|
||
<th style="text-align:left;padding:10px;font-size:11px;color:#6B7280;text-transform:uppercase;letter-spacing:0.04em">Ticket ID</th>
|
||
<th style="text-align:left;padding:10px;font-size:11px;color:#6B7280;text-transform:uppercase;letter-spacing:0.04em">Request</th>
|
||
<th style="text-align:left;padding:10px;font-size:11px;color:#6B7280;text-transform:uppercase;letter-spacing:0.04em">State</th>
|
||
<th style="text-align:left;padding:10px;font-size:11px;color:#6B7280;text-transform:uppercase;letter-spacing:0.04em">Priority</th>
|
||
<th style="text-align:left;padding:10px;font-size:11px;color:#6B7280;text-transform:uppercase;letter-spacing:0.04em">Last Message</th>
|
||
<th style="text-align:left;padding:10px;font-size:11px;color:#6B7280;text-transform:uppercase;letter-spacing:0.04em">Updated</th>
|
||
<th style="text-align:left;padding:10px;font-size:11px;color:#6B7280;text-transform:uppercase;letter-spacing:0.04em">Action</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{HELP_TICKET_ROWS.map((t) => (
|
||
<tr onClick={() => openTicketDetails(t.id)} style={`border-top:1px solid #F3F4F6;background:${activeTicketId() === t.id ? '#F5F7FF' : 'white'};cursor:pointer`}>
|
||
<td style="padding:10px;font-size:12px;font-weight:700;color:#374151">{t.id}</td>
|
||
<td style="padding:10px;font-size:12px;color:#111827;font-weight:600">{t.title}</td>
|
||
<td style="padding:10px">
|
||
<span style={`height:22px;padding:0 8px;border-radius:999px;border:1px solid ${t.status === 'Resolved' ? '#D1FAE5' : '#E5E7EB'};background:${t.status === 'Resolved' ? '#ECFDF3' : '#F9FAFB'};display:inline-flex;align-items:center;font-size:10px;font-weight:700;color:${t.status === 'Resolved' ? '#059669' : '#374151'}`}>{t.status}</span>
|
||
</td>
|
||
<td style="padding:10px;font-size:12px;color:#6B7280">{t.priority}</td>
|
||
<td style="padding:10px;font-size:12px;color:#6B7280">{t.lastMessage}</td>
|
||
<td style="padding:10px;font-size:12px;color:#6B7280">{t.updated}</td>
|
||
<td style="padding:10px">
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
openTicketDetails(t.id);
|
||
}}
|
||
style="height:28px;border-radius:8px;border:none;background:#0D0D2A;color:white;font-size:11px;font-weight:700;padding:0 10px;cursor:pointer"
|
||
>
|
||
Open
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</Show>
|
||
|
||
<Show when={myTicketsTab() === 'view_ticket'}>
|
||
<div style="margin-top:12px;border:1px solid #E5E7EB;border-radius:12px;background:white;padding:14px">
|
||
<div style="display:flex;justify-content:space-between;align-items:center;gap:8px">
|
||
<div>
|
||
<p style="margin:0;font-size:16px;font-weight:800;color:#111827">Ticket Communication</p>
|
||
<p style="margin:3px 0 0;font-size:12px;color:#6B7280">{activeTicketId()} • Share updates and attachments with support team.</p>
|
||
</div>
|
||
<span style="height:24px;padding:0 10px;border-radius:999px;border:1px solid #E5E7EB;background:#F9FAFB;display:inline-flex;align-items:center;font-size:11px;color:#374151">Message Support</span>
|
||
</div>
|
||
<div style="margin-top:12px;display:grid;grid-template-columns:1.4fr 1fr;gap:10px">
|
||
<div style="display:grid;gap:8px">
|
||
<div style="border:1px solid #E5E7EB;border-radius:8px;background:#F9FAFB;padding:10px">
|
||
<p style="margin:0;font-size:11px;color:#6B7280">User message</p>
|
||
<p style="margin:4px 0 0;font-size:12px;color:#111827">{selectedTicketDetails().userMessage}</p>
|
||
</div>
|
||
<div style="border:1px solid #E5E7EB;border-radius:8px;background:#F9FAFB;padding:10px">
|
||
<p style="margin:0;font-size:11px;color:#6B7280">Admin reply</p>
|
||
<p style="margin:4px 0 0;font-size:12px;color:#111827">{selectedTicketDetails().adminMessage}</p>
|
||
</div>
|
||
<div style="margin-top:2px;border:1px solid #E5E7EB;border-radius:10px;background:white;padding:10px">
|
||
<p style="margin:0 0 8px;font-size:12px;font-weight:700;color:#111827">Reply to Support</p>
|
||
<textarea
|
||
value={ticketMessage()}
|
||
onInput={(event) => setTicketMessage(event.currentTarget.value)}
|
||
placeholder="Write your message to support team..."
|
||
style="min-height:82px;width:100%;border:1px solid #E5E7EB;border-radius:10px;background:#F9FAFB;padding:10px;font-size:12px;color:#111827;resize:vertical"
|
||
/>
|
||
<div style="display:flex;justify-content:flex-end;margin-top:8px">
|
||
<button type="button" style="height:36px;border-radius:10px;border:none;background:#0D0D2A;color:white;padding:0 14px;font-size:12px;font-weight:700">Send Message</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div style="display:grid;gap:8px">
|
||
<div style="border:1px solid #E5E7EB;border-radius:10px;background:#F9FAFB;padding:10px">
|
||
<p style="margin:0;font-size:11px;font-weight:700;color:#6B7280;text-transform:uppercase">Requested Documents</p>
|
||
<div style="display:grid;gap:6px;margin-top:8px">
|
||
<For each={selectedTicketDetails().requestedDocuments.length ? selectedTicketDetails().requestedDocuments : ['No documents requested']}>{(d) => (
|
||
<div style="display:flex;justify-content:space-between;align-items:center;background:white;border:1px solid #E5E7EB;border-radius:8px;padding:6px 8px">
|
||
<span style="font-size:12px;color:#111827">{d}</span>
|
||
<span style="font-size:10px;font-weight:700;color:#6B7280">{selectedTicketDetails().requestedDocuments.length ? 'Pending' : 'NA'}</span>
|
||
</div>
|
||
)}</For>
|
||
</div>
|
||
</div>
|
||
<div style="border:1px solid #E5E7EB;border-radius:10px;background:#F9FAFB;padding:10px">
|
||
<p style="margin:0;font-size:11px;font-weight:700;color:#6B7280;text-transform:uppercase">Received From User</p>
|
||
<div style="display:grid;gap:6px;margin-top:8px">
|
||
<For each={selectedTicketDetails().receivedFiles.length ? selectedTicketDetails().receivedFiles : [{ file: 'No files received yet', state: 'Pending Upload' as const }]}>{(f) => (
|
||
<div style="display:flex;justify-content:space-between;align-items:center;background:white;border:1px solid #E5E7EB;border-radius:8px;padding:6px 8px">
|
||
<span style="font-size:12px;color:#111827">{f.file}</span>
|
||
<span style={`font-size:10px;font-weight:700;color:${f.state === 'Received' ? '#059669' : '#6B7280'}`}>{f.state}</span>
|
||
</div>
|
||
)}</For>
|
||
</div>
|
||
</div>
|
||
<div style="border:1px dashed #D1D5DB;border-radius:10px;background:#F9FAFB;padding:10px">
|
||
<p style="margin:0 0 8px;font-size:11px;font-weight:700;color:#6B7280;text-transform:uppercase">Upload Requested Files</p>
|
||
<input
|
||
type="file"
|
||
multiple
|
||
onChange={(event) => {
|
||
const files = event.currentTarget.files;
|
||
setViewTicketFiles(files ? Array.from(files).map((file) => file.name) : []);
|
||
}}
|
||
style="width:100%;font-size:12px;color:#374151"
|
||
/>
|
||
<Show when={viewTicketFiles().length}>
|
||
<div style="display:flex;flex-wrap:wrap;gap:6px;margin-top:8px">
|
||
<For each={viewTicketFiles()}>{(name) => <span style="height:24px;padding:0 8px;border-radius:999px;background:white;border:1px solid #E5E7EB;display:inline-flex;align-items:center;font-size:11px;color:#374151">{name}</span>}</For>
|
||
</div>
|
||
</Show>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Show>
|
||
</div>
|
||
</Show>
|
||
|
||
<Show when={active === 'create_ticket'}>
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:16px;padding:16px;box-shadow:0 1px 4px rgba(0,0,0,0.06)">
|
||
<p style="margin:0;font-size:24px;line-height:1.1;font-weight:800;color:#111827">Create Ticket</p>
|
||
<p style="margin:6px 0 0;font-size:13px;color:#6B7280">Create a support request with complete details for faster resolution.</p>
|
||
<div style="margin-top:12px;display:grid;grid-template-columns:1fr 1fr;gap:12px">
|
||
<div><p style="margin:0 0 6px;font-size:11px;font-weight:700;color:#6B7280;text-transform:uppercase">Subject</p><input type="text" placeholder="Enter ticket subject" style="height:38px;width:100%;border:1px solid #D1D5DB;border-radius:10px;background:white;padding:0 10px;font-size:12px;color:#111827" /></div>
|
||
<div><p style="margin:0 0 6px;font-size:11px;font-weight:700;color:#6B7280;text-transform:uppercase">Category</p><select style="height:38px;width:100%;border:1px solid #D1D5DB;border-radius:10px;background:white;padding:0 10px;font-size:12px;color:#111827"><option>Profile & Verification</option><option>Billing & Credits</option><option>Requirements</option><option>Other</option></select></div>
|
||
<div><p style="margin:0 0 6px;font-size:11px;font-weight:700;color:#6B7280;text-transform:uppercase">Priority</p><select style="height:38px;width:100%;border:1px solid #D1D5DB;border-radius:10px;background:white;padding:0 10px;font-size:12px;color:#111827"><option>Medium</option><option>High</option><option>Low</option></select></div>
|
||
<div><p style="margin:0 0 6px;font-size:11px;font-weight:700;color:#6B7280;text-transform:uppercase">Order / Requirement ID</p><input type="text" placeholder="Optional reference" style="height:38px;width:100%;border:1px solid #D1D5DB;border-radius:10px;background:white;padding:0 10px;font-size:12px;color:#111827" /></div>
|
||
<div style="grid-column:1 / -1"><p style="margin:0 0 6px;font-size:11px;font-weight:700;color:#6B7280;text-transform:uppercase">Description</p><textarea placeholder="Describe the issue in detail..." style="min-height:120px;width:100%;border:1px solid #D1D5DB;border-radius:10px;background:white;padding:10px;font-size:12px;color:#111827;resize:vertical" /></div>
|
||
<div style="grid-column:1 / -1">
|
||
<p style="margin:0 0 6px;font-size:11px;font-weight:700;color:#6B7280;text-transform:uppercase">Attachment</p>
|
||
<div style="border:1px dashed #D1D5DB;border-radius:10px;background:#F9FAFB;padding:12px">
|
||
<input
|
||
type="file"
|
||
multiple
|
||
onChange={(event) => {
|
||
const files = event.currentTarget.files;
|
||
setCreateTicketFiles(files ? Array.from(files).map((file) => file.name) : []);
|
||
}}
|
||
style="width:100%;font-size:12px;color:#374151"
|
||
/>
|
||
<Show when={createTicketFiles().length}>
|
||
<div style="display:flex;flex-wrap:wrap;gap:6px;margin-top:8px">
|
||
<For each={createTicketFiles()}>{(name) => <span style="height:24px;padding:0 8px;border-radius:999px;background:white;border:1px solid #E5E7EB;display:inline-flex;align-items:center;font-size:11px;color:#374151">{name}</span>}</For>
|
||
</div>
|
||
</Show>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div style="display:flex;justify-content:flex-end;gap:8px;margin-top:12px">
|
||
<button type="button" style="height:36px;border-radius:8px;border:1px solid #D1D5DB;background:white;color:#374151;padding:0 12px;font-size:12px;font-weight:700">Cancel</button>
|
||
<button type="button" style="height:36px;border-radius:8px;border:none;background:#0D0D2A;color:white;padding:0 14px;font-size:12px;font-weight:700">Submit Ticket</button>
|
||
</div>
|
||
</div>
|
||
</Show>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (customerKey() === 'settings') {
|
||
if (tab === 'privacy') {
|
||
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)">{['Profile visibility', 'Data sharing consent', 'Session management'].map((s) => <div style="display:flex;justify-content:space-between;align-items:center;padding:8px;border:1px solid #E5E7EB;border-radius:8px;background:#F9FAFB;margin-top:8px"><span style="font-size:12px;color:#374151">{s}</span><span style="font-size:12px;color:#6B7280">Enabled</span></div>)}</div>;
|
||
}
|
||
if (tab === 'notifications') {
|
||
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)">{['Response alerts', 'Billing alerts', 'Security alerts'].map((s) => <div style="display:flex;justify-content:space-between;align-items:center;padding:8px;border:1px solid #E5E7EB;border-radius:8px;background:#F9FAFB;margin-top:8px"><span style="font-size:12px;color:#374151">{s}</span><span style="font-size:12px;color:#6B7280">On</span></div>)}</div>;
|
||
}
|
||
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)">{['Change Password', 'Two-factor authentication', 'Login activity'].map((s) => <div style="padding:8px;border:1px solid #E5E7EB;border-radius:8px;background:#F9FAFB;font-size:12px;color:#374151;margin-top:8px">{s}</div>)}</div>;
|
||
}
|
||
|
||
if (customerKey() === 'switch role' || customerKey() === 'switch services' || customerKey() === 'switch service') {
|
||
const activeRoles = userRoles();
|
||
return (
|
||
<div style="display:flex;flex-direction:column;gap:12px">
|
||
<div style="border:1px solid #E5E7EB;border-radius:16px;background:white;box-shadow:0 1px 4px rgba(0,0,0,0.06);padding:20px">
|
||
<p style="margin:0;font-size:30px;line-height:1.1;font-weight:800;color:#111827">Switch Services</p>
|
||
<p style="margin:6px 0 0;font-size:14px;color:#6B7280">Switch between your active roles to manage different business operations.</p>
|
||
</div>
|
||
|
||
<div style="display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px">
|
||
<For each={activeRoles}>
|
||
{(role) => {
|
||
const roleKey = String(role.role_key || role.key).toUpperCase();
|
||
const isCurrent = normalizeRoleKey(props.roleKey || '') === normalizeRoleKey(roleKey);
|
||
return (
|
||
<div style={`border:1px solid ${isCurrent ? '#FFD8C2' : '#E5E7EB'};background:${isCurrent ? '#FFF9F7' : 'white'};border-radius:16px;padding:16px;box-shadow:0 1px 4px rgba(0,0,0,0.06);display:flex;flex-direction:column;gap:8px`}>
|
||
<div style="display:flex;align-items:center;justify-content:space-between">
|
||
<div style="width:32px;height:32px;border-radius:8px;background:#FFF3EE;display:flex;align-items:center;justify-content:center">
|
||
{(() => {
|
||
const Icon = roleIcon(roleKey);
|
||
return <Icon size={16} color="#FF5E13" />;
|
||
})()}
|
||
</div>
|
||
<span style={`height:20px;padding:0 8px;border-radius:999px;font-size:10px;font-weight:700;background:${role.status === 'APPROVED' ? '#ECFDF5' : '#FEF3C7'};color:${role.status === 'APPROVED' ? '#059669' : '#D97706'}`}>{role.status}</span>
|
||
</div>
|
||
<p style="margin:4px 0 0;font-size:16px;font-weight:800;color:#111827">{titleCase(roleKey.replace(/_/g, ' '))}</p>
|
||
<button
|
||
type="button"
|
||
disabled={isCurrent}
|
||
onClick={() => switchRole(roleKey)}
|
||
style={`margin-top:auto;height:32px;border-radius:8px;border:none;background:${isCurrent ? '#E5E7EB' : '#03004E'};color:${isCurrent ? '#4B5563' : 'white'};padding:0 10px;font-size:12px;font-weight:700;cursor:${isCurrent ? 'default' : 'pointer'}`}
|
||
>
|
||
{isCurrent ? 'Active' : 'Switch Role'}
|
||
</button>
|
||
</div>
|
||
);
|
||
}}
|
||
</For>
|
||
<Show when={activeRoles.length === 0}>
|
||
<div style="grid-column:1 / -1;padding:20px;text-align:center;color:#6B7280;background:#F9FAFB;border-radius:12px;border:1px dashed #D1D5DB">
|
||
No other active roles found. Explore Nxtgauge to add more.
|
||
</div>
|
||
</Show>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (customerKey() === 'logout') {
|
||
if (tab === 'cancel') {
|
||
return <div style="max-width:420px;margin:0 auto;border:1px solid #E5E7EB;background:white;border-radius:16px;padding:14px;text-align:center;box-shadow:0 1px 4px rgba(0,0,0,0.06)"><p style="margin:0;font-size:18px;font-weight:800;color:#111827">Logout Cancelled</p><p style="margin:6px 0 0;font-size:13px;color:#6B7280">Your session is still active.</p></div>;
|
||
}
|
||
return (
|
||
<div style="max-width:420px;margin:0 auto;border:1px solid #E5E7EB;background:white;border-radius:16px;padding:14px;text-align:center;box-shadow:0 1px 4px rgba(0,0,0,0.06)">
|
||
<p style="margin:0;font-size:18px;font-weight:800;color:#111827">Confirm Logout</p>
|
||
<p style="margin:6px 0 0;font-size:13px;color:#6B7280">Are you sure you want to logout from the service seeker portal?</p>
|
||
<div style="display:flex;justify-content:center;gap:8px;margin-top:12px"><button type="button" style="height:32px;border-radius:8px;border:1px solid #E5E7EB;background:white;padding:0 12px;font-size:12px;font-weight:700;color:#374151">Cancel</button><button type="button" style="height:32px;border-radius:8px;border:none;background:#FF5E13;color:white;padding:0 12px;font-size:12px;font-weight:700">Logout</button></div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (customerKey() === 'my portfolio') {
|
||
if (!isProfessionalRoleKey(props.roleKey || '')) {
|
||
return (
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:16px;padding:16px;box-shadow:0 1px 4px rgba(0,0,0,0.06)">
|
||
<p style="margin:0;font-size:16px;font-weight:800;color:#111827">My Portfolio is only for professionals</p>
|
||
<p style="margin:6px 0 0;font-size:13px;color:#6B7280">Switch to a professional role to manage and preview portfolio content.</p>
|
||
</div>
|
||
);
|
||
}
|
||
return renderPortfolioContent();
|
||
}
|
||
|
||
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)">
|
||
<p style="margin:0;font-size:13px;font-weight:700;color:#111827">Screen Preview</p>
|
||
<p style="margin:6px 0 0;font-size:13px;color:#6B7280">Service seeker screen is selected and interactive.</p>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
const portfolioTabIcon = (tabLabel: string) => {
|
||
const key = normalizeTabKey(tabLabel);
|
||
if (key === 'overview') return LayoutGrid;
|
||
if (key === 'about') return UserCircle2;
|
||
if (key.includes('services')) return Coins;
|
||
if (key.includes('portfolio')) return Image;
|
||
if (key.includes('experience')) return BriefcaseBusiness;
|
||
if (key === 'testimonials') return Star;
|
||
if (key === 'faqs') return HelpCircle;
|
||
return FileText;
|
||
};
|
||
|
||
|
||
return (
|
||
<div style="border:1px solid #E5E7EB;border-radius:12px;overflow:hidden;background:#fff">
|
||
<Show when={!props.hidePreviewHeader}>
|
||
<div style="padding:10px 14px;border-bottom:1px solid #E5E7EB;background:#F9FAFB;display:flex;justify-content:space-between;align-items:center">
|
||
<p style="margin:0;font-size:12px;font-weight:700;color:#374151">Actual End-User Dashboard UI Preview</p>
|
||
<div style="display:flex;align-items:center;gap:8px">
|
||
<Show when={props.onOpenFullscreen}>
|
||
<button
|
||
type="button"
|
||
onClick={() => props.onOpenFullscreen?.()}
|
||
title="Open Full Screen Preview"
|
||
aria-label="Open Full Screen Preview"
|
||
style="height:28px;width:28px;border-radius:7px;border:1px solid #E5E7EB;background:white;display:inline-flex;align-items:center;justify-content:center;cursor:pointer;color:#374151"
|
||
>
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||
<polyline points="15 3 21 3 21 9"></polyline>
|
||
<polyline points="9 21 3 21 3 15"></polyline>
|
||
<line x1="21" y1="3" x2="14" y2="10"></line>
|
||
<line x1="3" y1="21" x2="10" y2="14"></line>
|
||
</svg>
|
||
</button>
|
||
</Show>
|
||
<StatusBadge status={props.status} />
|
||
</div>
|
||
</div>
|
||
</Show>
|
||
|
||
<div style="display:grid;grid-template-columns:220px 1fr;min-height:680px;background:#F3F4F6">
|
||
<aside style="display:flex;flex-direction:column;border-right:1px solid #E5E7EB;background:#fff">
|
||
<div style="height:64px;display:flex;align-items:center;border-bottom:1px solid #E5E7EB;padding:0 14px">
|
||
<img src="/nxtgauge-logo.png" alt="Nxtgauge" style="height:40px;object-fit:contain;max-width:170px" />
|
||
</div>
|
||
<div style="padding:10px 8px;display:flex;flex-direction:column;gap:4px;overflow:auto">
|
||
<For each={previewSidebarItems()}>
|
||
{(item) => (
|
||
<button
|
||
type="button"
|
||
onClick={() => props.onSidebarSelect(item)}
|
||
style={`display:flex;align-items:center;gap:9px;text-align:left;height:34px;border-radius:8px;padding:0 10px;border:none;cursor:pointer;font-size:12px;font-weight:600;background:${props.activeSidebar === item ? '#FFF3EE' : 'transparent'};color:${props.activeSidebar === item ? '#FF5E13' : '#6B7280'}`}
|
||
>
|
||
{(() => {
|
||
const Icon = sidebarIcon(item);
|
||
return <Icon size={14} style={`color:${props.activeSidebar === item ? '#FF5E13' : '#9CA3AF'};flex-shrink:0`} strokeWidth={props.activeSidebar === item ? 2.5 : 2} />;
|
||
})()}
|
||
{titleCase(item)}
|
||
</button>
|
||
)}
|
||
</For>
|
||
</div>
|
||
</aside>
|
||
|
||
<main style="display:flex;flex-direction:column">
|
||
<header style="height:64px;background:#fff;border-bottom:1px solid #E5E7EB;display:flex;align-items:center;justify-content:space-between;padding:0 16px">
|
||
<div style="display:flex;align-items:center;gap:10px">
|
||
<div style="position:relative">
|
||
<Search size={16} style="position:absolute;left:10px;top:50%;transform:translateY(-50%);color:#9CA3AF" />
|
||
<input value="" readOnly placeholder="Search resources..." style="height:36px;width:280px;border-radius:10px;border:1px solid #E5E7EB;background:#F9FAFB;padding:0 12px 0 32px;font-size:12px;color:#6B7280" />
|
||
</div>
|
||
</div>
|
||
<div style="display:flex;align-items:center;gap:8px">
|
||
<button type="button" style="width:34px;height:34px;border-radius:8px;border:1px solid #E5E7EB;background:white;display:flex;align-items:center;justify-content:center;cursor:pointer;color:#6B7280"><Bell size={16} /></button>
|
||
<button type="button" style="width:34px;height:34px;border-radius:8px;border:1px solid #E5E7EB;background:white;display:flex;align-items:center;justify-content:center;cursor:pointer;color:#6B7280"><Settings size={16} /></button>
|
||
<button type="button" style="width:34px;height:34px;border-radius:8px;border:1px solid #E5E7EB;background:white;display:flex;align-items:center;justify-content:center;cursor:pointer;color:#6B7280"><User size={16} /></button>
|
||
</div>
|
||
</header>
|
||
<Show when={!isVerified()}>
|
||
<div style="margin:14px 14px 0;background:#FFF7ED;border:1px solid #FFEDD5;border-radius:12px;padding:12px 16px;display:flex;align-items:center;justify-content:space-between;gap:12px;box-shadow:0 1px 2px rgba(0,0,0,0.05)">
|
||
<div style="display:flex;align-items:center;gap:12px">
|
||
<div style="width:36px;height:36px;border-radius:10px;background:#FFEDD5;display:flex;align-items:center;justify-content:center;color:#EA580C">
|
||
<AlertCircle size={20} />
|
||
</div>
|
||
<div>
|
||
<p style="margin:0;font-size:14px;font-weight:700;color:#9A3412">Account Verification Required</p>
|
||
<p style="margin:2px 0 0;font-size:12px;color:#C2410C">{verificationPending() ? 'Your profile is currently under review by our team. This usually takes 24-48 hours.' : 'Complete your profile details to unlock all platform features and start receiving leads.'}</p>
|
||
</div>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
onClick={() => props.onSidebarSelect('My Profile')}
|
||
style="height:32px;padding:0 14px;border-radius:8px;background:#EA580C;color:white;border:none;font-size:12px;font-weight:600;cursor:pointer;display:flex;align-items:center;white-space:nowrap"
|
||
>
|
||
{verificationPending() ? 'View Verification Status' : 'Complete Profile'}
|
||
</button>
|
||
</div>
|
||
</Show>
|
||
|
||
<div style="padding:14px">
|
||
<Show when={!(isCustomerExternalMode() && (customerKey() === 'help center' || customerKey() === 'support'))}>
|
||
<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:12px;color:#6B7280">Current View</p>
|
||
<p style="margin:4px 0 0;font-size:24px;font-weight:800;color:#111827">{isCustomerExternalMode() ? customerView().title : titleCase(props.activeSidebar)}</p>
|
||
<p style="margin:4px 0 0;font-size:13px;color:#6B7280">{isCustomerExternalMode() ? customerView().subtitle : 'Interactive preview for configured dashboard.'}</p>
|
||
<Show when={previewTabs().length > 0 && customerKey() !== 'my portfolio'}>
|
||
<Show
|
||
when={customerKey() === 'my portfolio'}
|
||
fallback={
|
||
<div style="margin-top:12px;display:flex;align-items:center;gap:20px;border-bottom:1px solid #E5E7EB">
|
||
<For each={previewTabs()}>
|
||
{(item) => (
|
||
(() => {
|
||
const itemKey = normalizeTabKey(item);
|
||
const isLockedTestimonialsTab = customerKey() === 'my portfolio' && itemKey === 'testimonials' && !portfolioTestimonialsUnlocked();
|
||
return (
|
||
<button
|
||
type="button"
|
||
disabled={isLockedTestimonialsTab}
|
||
onClick={() => {
|
||
if (isLockedTestimonialsTab) return;
|
||
props.onTabSelect(item);
|
||
}}
|
||
title={isLockedTestimonialsTab ? 'Unlock after 3 completed jobs and 2 customer feedback entries' : ''}
|
||
style={`padding-bottom:10px;font-size:13px;font-weight:500;background:none;border:none;cursor:${isLockedTestimonialsTab ? 'not-allowed' : 'pointer'};opacity:${isLockedTestimonialsTab ? 0.5 : 1};${resolvedTabKey() === itemKey ? 'color:#FF5E13;border-bottom:2px solid #FF5E13;margin-bottom:-1px' : 'color:#6B7280'}`}
|
||
>
|
||
{titleCase(item)} {isLockedTestimonialsTab ? '• Locked' : ''}
|
||
</button>
|
||
);
|
||
})()
|
||
)}
|
||
</For>
|
||
</div>
|
||
}
|
||
>
|
||
<div style="margin-top:12px;display:flex;align-items:center;gap:8px;overflow-x:auto;padding-bottom:2px">
|
||
<For each={previewTabs()}>
|
||
{(item) => {
|
||
const itemKey = normalizeTabKey(item);
|
||
const isLockedTestimonialsTab = itemKey === 'testimonials' && !portfolioTestimonialsUnlocked();
|
||
const isActive = resolvedTabKey() === itemKey;
|
||
const Icon = portfolioTabIcon(item);
|
||
return (
|
||
<button
|
||
type="button"
|
||
disabled={isLockedTestimonialsTab}
|
||
onClick={() => {
|
||
if (isLockedTestimonialsTab) return;
|
||
props.onTabSelect(item);
|
||
}}
|
||
title={isLockedTestimonialsTab ? 'Unlock after 3 completed jobs and 2 customer feedback entries' : ''}
|
||
style={`min-width:148px;height:40px;border-radius:10px;border:1px solid ${isActive ? '#FFD8C2' : '#E5E7EB'};background:${isActive ? '#FFF8F4' : 'white'};padding:0 10px;display:flex;align-items:center;gap:8px;cursor:${isLockedTestimonialsTab ? 'not-allowed' : 'pointer'};opacity:${isLockedTestimonialsTab ? 0.5 : 1};flex-shrink:0`}
|
||
>
|
||
<span style={`width:22px;height:22px;border-radius:7px;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;border:1px solid ${isActive ? '#FFD8C2' : '#E5E7EB'};background:${isActive ? '#FFF1EB' : '#F9FAFB'}`}>
|
||
<Icon size={12} style={`color:${isActive ? '#C2410C' : '#6B7280'}`} />
|
||
</span>
|
||
<span style={`font-size:12px;font-weight:700;line-height:1.2;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:${isActive ? '#111827' : '#374151'}`}>
|
||
{titleCase(item)}{isLockedTestimonialsTab ? ' · Locked' : ''}
|
||
</span>
|
||
</button>
|
||
);
|
||
}}
|
||
</For>
|
||
</div>
|
||
</Show>
|
||
</Show>
|
||
</div>
|
||
</Show>
|
||
|
||
<Show when={isCustomerExternalMode()} fallback={<div style="margin-top:10px;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;color:#374151">Default preview mode.</p></div>}>
|
||
<div style="margin-top:10px">{renderCustomerContent()}</div>
|
||
</Show>
|
||
|
||
<Show when={!isCustomerExternalMode()}>
|
||
<div style="border:1px solid #E5E7EB;background:white;border-radius:16px;padding:14px;margin-top:10px;box-shadow:0 1px 4px rgba(0,0,0,0.06)">
|
||
<p style="margin:0;font-size:11px;letter-spacing:0.05em;text-transform:uppercase;color:#6B7280">Fields View</p>
|
||
<div style="display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px;margin-top:8px">
|
||
<For each={previewFields()}>
|
||
{(f) => <div style="padding:8px;border:1px solid #E5E7EB;border-radius:8px;background:#F9FAFB;font-size:12px;color:#374151">{titleCase(f)}</div>}
|
||
</For>
|
||
</div>
|
||
</div>
|
||
</Show>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
|
||
</div>
|
||
);
|
||
}
|