diff --git a/src/components/admin/DashboardDesignPreview.tsx b/src/components/admin/DashboardDesignPreview.tsx index bbad7d6..cead7b2 100644 --- a/src/components/admin/DashboardDesignPreview.tsx +++ b/src/components/admin/DashboardDesignPreview.tsx @@ -5,6 +5,7 @@ import { BadgeCheck, Bookmark, BriefcaseBusiness, + Calendar, Camera, CheckCircle2, ChevronDown, @@ -20,6 +21,7 @@ import { GraduationCap, HandHelping, HeadphonesIcon, + HelpCircle, Image, LayoutGrid, LogOut, @@ -99,11 +101,13 @@ 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'; @@ -114,23 +118,21 @@ function normalizeRoleKey(roleKey: string): string { const PROFILE_SPECS: Record = { CUSTOMER: { - title: 'Customer Profile', + title: 'Service Seeker Profile', subtitle: 'Manage your personal details, service preferences, documents, and account settings.', - tabs: ['basic info', 'preferences', 'settings'], + tabs: ['basic information', 'documents', 'settings'], tabFields: { - 'basic info': ['Full Name', 'Email Address', 'Mobile Number', 'Area', 'PIN Code', 'Company Name (Optional)'], - preferences: ['Service Mode', 'Preferred Budget Range', 'Preferred Response Time', 'Preferred Area', 'Preferred PIN Code'], + '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: ['company info', 'business details', 'hiring preferences', 'documents', 'settings'], + tabs: ['basic information', 'documents', 'settings'], tabFields: { - 'company info': ['Company Name', 'Company Email', 'Company Phone', 'City', 'State', 'Country', 'PIN Code', 'Website URL'], - 'business details': ['Industry', 'Business Type', 'GST Number', 'Company Size', 'Founded Year', 'Headquarter Address'], - 'hiring preferences': ['Hiring Roles', 'Work Mode', 'Preferred Experience', 'Budget Range', 'Joining Timeline'], + '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'], }, @@ -138,12 +140,9 @@ const PROFILE_SPECS: Record = { JOB_SEEKER: { title: 'Job Seeker Profile', subtitle: 'Maintain your career profile, resume, preferences, and verification docs.', - tabs: ['basic info', 'career profile', 'resume & portfolio', 'preferences', 'documents', 'settings'], + tabs: ['basic information', 'documents', 'settings'], tabFields: { - 'basic info': ['Full Name', 'Email Address', 'Mobile Number', 'Date of Birth', 'City', 'State', 'Country'], - 'career profile': ['Current Role', 'Total Experience', 'Highest Qualification', 'Skills', 'Current CTC', 'Expected CTC'], - 'resume & portfolio': ['Resume Upload', 'Portfolio URL', 'LinkedIn URL', 'Certifications'], - preferences: ['Preferred Job Type', 'Preferred Work Mode', 'Preferred City', 'Notice Period'], + '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'], }, @@ -151,12 +150,9 @@ const PROFILE_SPECS: Record = { PHOTOGRAPHER: { title: 'Photographer Profile', subtitle: 'Manage your photography details, pricing, portfolio, and documents.', - tabs: ['basic info', 'photography details', 'services & pricing', 'portfolio', 'documents', 'settings'], + tabs: ['basic information', 'documents', 'settings'], tabFields: { - 'basic info': ['Full Name', 'Brand Name', 'Email Address', 'Mobile Number', 'City', 'State', 'Country', 'PIN Code'], - 'photography details': ['Specializations', 'Experience Years', 'Equipment Owned', 'Service Area', 'Languages'], - 'services & pricing': ['Service Types', 'Starting Price', 'Hourly Rate', 'Package Details'], - portfolio: ['Portfolio Links', 'Sample Photos', 'Client Highlights'], + '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'], }, @@ -164,12 +160,9 @@ const PROFILE_SPECS: Record = { MAKEUP_ARTIST: { title: 'Makeup Artist Profile', subtitle: 'Manage makeup specialization, services, portfolio, and compliance documents.', - tabs: ['basic info', 'makeup details', 'services & pricing', 'portfolio', 'documents', 'settings'], + tabs: ['basic information', 'documents', 'settings'], tabFields: { - 'basic info': ['Full Name', 'Brand Name', 'Email Address', 'Mobile Number', 'City', 'State', 'Country', 'PIN Code'], - 'makeup details': ['Specializations', 'Experience Years', 'Products Used', 'Service Area', 'Languages'], - 'services & pricing': ['Bridal Packages', 'Party Makeup', 'Trial Pricing', 'Travel Charges'], - portfolio: ['Portfolio Links', 'Before/After Gallery', 'Client Testimonials'], + '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'], }, @@ -177,12 +170,9 @@ const PROFILE_SPECS: Record = { DEVELOPER: { title: 'Developer Profile', subtitle: 'Showcase technical profile, pricing models, portfolio projects, and documents.', - tabs: ['basic info', 'technical profile', 'services & pricing', 'portfolio / projects', 'documents', 'settings'], + tabs: ['basic information', 'documents', 'settings'], tabFields: { - 'basic info': ['Full Name', 'Brand Name', 'Email Address', 'Mobile Number', 'City', 'State', 'Country', 'PIN Code'], - 'technical profile': ['Primary Stack', 'Frameworks', 'Databases', 'Cloud Skills', 'Years of Experience', 'GitHub URL'], - 'services & pricing': ['Service Categories', 'Project-Based Pricing', 'Hourly Pricing', 'Minimum Project Size'], - 'portfolio / projects': ['Featured Projects', 'Case Studies', 'Demo Links', 'Client Testimonials'], + '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'], }, @@ -190,12 +180,19 @@ const PROFILE_SPECS: Record = { VIDEO_EDITOR: { title: 'Video Editor Profile', subtitle: 'Manage editing profile, services, portfolio, and verification documents.', - tabs: ['basic info', 'editing details', 'services & pricing', 'portfolio', 'documents', 'settings'], + tabs: ['basic information', 'documents', 'settings'], tabFields: { - 'basic info': ['Full Name', 'Brand Name', 'Email Address', 'Mobile Number', 'City', 'State', 'Country', 'PIN Code'], - 'editing details': ['Specialization', 'Software Expertise', 'Delivery Timeline', 'Languages'], - 'services & pricing': ['Editing Packages', 'Per-Minute Price', 'Revision Policy', 'Express Delivery Charges'], - portfolio: ['Showreel Links', 'Sample Projects', 'Client Reviews'], + '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'], }, @@ -203,12 +200,9 @@ const PROFILE_SPECS: Record = { GRAPHIC_DESIGNER: { title: 'Graphic Designer Profile', subtitle: 'Manage design profile, service pricing, portfolio assets, and documents.', - tabs: ['basic info', 'design profile', 'services & pricing', 'portfolio', 'documents', 'settings'], + tabs: ['basic information', 'documents', 'settings'], tabFields: { - 'basic info': ['Full Name', 'Brand Name', 'Email Address', 'Mobile Number', 'City', 'State', 'Country', 'PIN Code'], - 'design profile': ['Design Specialization', 'Tools Expertise', 'Years of Experience', 'Service Area'], - 'services & pricing': ['Design Packages', 'Hourly Rate', 'Revision Policy', 'Delivery Timeline'], - portfolio: ['Behance/Dribbble Links', 'Case Studies', 'Brand Projects'], + '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'], }, @@ -216,12 +210,9 @@ const PROFILE_SPECS: Record = { SOCIAL_MEDIA_MANAGER: { title: 'Social Media Manager Profile', subtitle: 'Manage social profile details, service plans, case studies, and documents.', - tabs: ['basic info', 'social media profile', 'services & pricing', 'case studies / portfolio', 'documents', 'settings'], + tabs: ['basic information', 'documents', 'settings'], tabFields: { - 'basic info': ['Full Name', 'Brand Name', 'Email Address', 'Mobile Number', 'City', 'State', 'Country', 'PIN Code'], - 'social media profile': ['Specializations', 'Platforms Managed', 'Tools Used', 'Years of Experience'], - 'services & pricing': ['Monthly Retainer', 'Campaign Pricing', 'Platform-Wise Services'], - 'case studies / portfolio': ['Case Study Links', 'Growth Metrics', 'Client Accounts Handled'], + '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'], }, @@ -229,12 +220,9 @@ const PROFILE_SPECS: Record = { FITNESS_TRAINER: { title: 'Fitness Trainer Profile', subtitle: 'Manage training details, plans, certifications, and profile settings.', - tabs: ['basic info', 'training details', 'services & pricing', 'certifications', 'documents', 'settings'], + tabs: ['basic information', 'documents', 'settings'], tabFields: { - 'basic info': ['Full Name', 'Brand Name', 'Email Address', 'Mobile Number', 'City', 'State', 'Country', 'PIN Code'], - 'training details': ['Training Specializations', 'Experience Years', 'Session Mode', 'Available Timings'], - 'services & pricing': ['Personal Training Plans', 'Group Session Plans', 'Monthly Pricing'], - certifications: ['Trainer Certifications', 'Workshop Credentials', 'First Aid Certificate'], + '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'], }, @@ -242,12 +230,9 @@ const PROFILE_SPECS: Record = { TUTOR: { title: 'Tutor Profile', subtitle: 'Manage teaching details, subjects, pricing, documents, and settings.', - tabs: ['basic info', 'teaching details', 'subjects & levels', 'pricing', 'documents', 'settings'], + tabs: ['basic information', 'documents', 'settings'], tabFields: { - 'basic info': ['Full Name', 'Email Address', 'Mobile Number', 'City', 'State', 'Country', 'PIN Code'], - 'teaching details': ['Teaching Mode', 'Years of Experience', 'Board Expertise', 'Languages'], - 'subjects & levels': ['Subjects', 'Grade Levels', 'Exam Preparation Focus'], - pricing: ['Hourly Fee', 'Monthly Plan', 'Group Session Pricing'], + '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'], }, @@ -255,12 +240,9 @@ const PROFILE_SPECS: Record = { CATERING_SERVICES: { title: 'Catering Services Profile', subtitle: 'Manage business details, menu packages, gallery, and compliance docs.', - tabs: ['business info', 'service details', 'packages & pricing', 'portfolio / gallery', 'documents', 'settings'], + tabs: ['basic information', 'documents', 'settings'], tabFields: { - 'business info': ['Business Name', 'Contact Person Name', 'Email Address', 'Mobile Number', 'City', 'State', 'Country', 'PIN Code'], - 'service details': ['Cuisine Types', 'Service Area', 'Serving Capacity', 'Event Types'], - 'packages & pricing': ['Starter Package', 'Premium Package', 'Custom Package', 'Travel Charges'], - 'portfolio / gallery': ['Food Gallery', 'Event Photos', 'Client Testimonials'], + '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'], }, @@ -268,12 +250,9 @@ const PROFILE_SPECS: Record = { PROFESSIONAL: { title: 'Professional Profile', subtitle: 'Manage professional details, pricing, portfolio, and account settings.', - tabs: ['basic info', 'service details', 'services & pricing', 'portfolio', 'documents', 'settings'], + tabs: ['basic information', 'documents', 'settings'], tabFields: { - 'basic info': ['Full Name', 'Email Address', 'Mobile Number', 'City', 'State', 'Country'], - 'service details': ['Primary Service', 'Experience', 'Specializations', 'Languages'], - 'services & pricing': ['Service List', 'Base Pricing', 'Custom Quotes'], - portfolio: ['Showcase Links', 'Sample Work', 'Client Testimonials'], + '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'], }, @@ -282,7 +261,16 @@ const PROFILE_SPECS: Record = { function profileSpecForRole(roleKey: string): ProfileSpec { const normalized = normalizeRoleKey(roleKey); - return PROFILE_SPECS[normalized] || PROFILE_SPECS.PROFESSIONAL; + 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 = { @@ -301,7 +289,7 @@ type PortfolioSpec = { const PORTFOLIO_SPECS: Record = { PHOTOGRAPHER: { roleLabel: 'Photographer', - tabs: ['overview', 'about', 'services & pricing', 'portfolio gallery', 'experience & equipment', 'testimonials', 'faqs'], + 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', @@ -322,7 +310,7 @@ const PORTFOLIO_SPECS: Record = { }, MAKEUP_ARTIST: { roleLabel: 'Makeup Artist', - tabs: ['overview', 'about', 'services & pricing', 'gallery', 'experience & certifications', 'testimonials', 'faqs'], + 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', @@ -343,7 +331,7 @@ const PORTFOLIO_SPECS: Record = { }, TUTOR: { roleLabel: 'Tutor', - tabs: ['overview', 'about', 'subjects & pricing', 'student work', 'qualifications', 'testimonials', 'faqs'], + 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', @@ -364,7 +352,7 @@ const PORTFOLIO_SPECS: Record = { }, DEVELOPER: { roleLabel: 'Developer', - tabs: ['overview', 'about', 'services & pricing', 'projects', 'tech stack & experience', 'testimonials', 'faqs'], + 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', @@ -385,7 +373,7 @@ const PORTFOLIO_SPECS: Record = { }, VIDEO_EDITOR: { roleLabel: 'Video Editor', - tabs: ['overview', 'about', 'services & pricing', 'showreel', 'experience & tools', 'testimonials', 'faqs'], + 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', @@ -404,9 +392,30 @@ const PORTFOLIO_SPECS: Record = { { 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: ['overview', 'about', 'services & pricing', 'portfolio', 'experience & tools', 'testimonials', 'faqs'], + 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', @@ -427,7 +436,7 @@ const PORTFOLIO_SPECS: Record = { }, SOCIAL_MEDIA_MANAGER: { roleLabel: 'Social Media Manager', - tabs: ['overview', 'about', 'services & pricing', 'case studies', 'experience & tools', 'testimonials', 'faqs'], + 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', @@ -448,7 +457,7 @@ const PORTFOLIO_SPECS: Record = { }, FITNESS_TRAINER: { roleLabel: 'Fitness Trainer', - tabs: ['overview', 'about', 'training plans', 'client results', 'certifications', 'testimonials', 'faqs'], + 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', @@ -469,7 +478,7 @@ const PORTFOLIO_SPECS: Record = { }, CATERING_SERVICES: { roleLabel: 'Catering Services', - tabs: ['overview', 'about', 'packages & pricing', 'gallery', 'experience & certifications', 'testimonials', 'faqs'], + 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', @@ -490,7 +499,7 @@ const PORTFOLIO_SPECS: Record = { }, PROFESSIONAL: { roleLabel: 'Professional', - tabs: ['overview', 'about', 'services & pricing', 'portfolio', 'experience', 'testimonials', 'faqs'], + 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', @@ -516,6 +525,34 @@ function portfolioSpecForRole(roleKey: string): PortfolioSpec { 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' + ) { + 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' }, @@ -526,22 +563,29 @@ const PORTFOLIO_TESTIMONIALS = [ function customerViewFor(sidebar: string, roleKey: string): CustomerView { const key = String(sidebar || '').toLowerCase().trim(); const role = normalizeRoleKey(roleKey); - if (key === 'my dashboard') return { title: 'Customer Dashboard Overview', subtitle: 'Manage your enterprise requirements and track professional responses in real-time.', tabs: ['overview', 'recent requirements', 'quick actions'], cta: 'Post New Requirement' }; - if (key === 'leads') return { title: 'Leads', subtitle: 'Browse marketplace requirements, request contact access, and track conversion status.', tabs: ['marketplace', 'requested contacts'], cta: 'Buy Credits' }; + 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') return { title: 'Received Professional Responses', subtitle: 'Review and manage professional applications for active requirements.', tabs: ['all responses', 'new', 'shortlisted', 'rejected'] }; + 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.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 customers see before they accept your contact request.', tabs: spec.tabs, cta: 'Edit Portfolio' }; + 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: [] }; @@ -550,7 +594,7 @@ function customerViewFor(sidebar: string, roleKey: string): CustomerView { 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: 'Customer Dashboard', subtitle: 'Preview customer dashboard flow.', tabs: ['overview'] }; + return { title: 'Service Seeker Dashboard', subtitle: 'Preview service seeker dashboard flow.', tabs: ['overview'] }; } const REQUIREMENT_ROWS = [ @@ -628,6 +672,7 @@ const REQUIREMENT_ROLE_OPTIONS = [ { 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' }, @@ -670,6 +715,13 @@ const REQUIREMENT_ROLE_DETAILS: Record; + onOpenFullscreen?: () => void; }) { + 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'])); @@ -910,13 +1037,20 @@ export default function DashboardDesignPreview(props: { const portfolioTestimonialsUnlocked = createMemo(() => portfolioJobsCompletedPreview >= 3 && portfolioFeedbackCountPreview >= 2); const exploreRoleCards = createMemo(() => { const roles = Array.isArray(props.exploreRoles) ? props.exploreRoles : []; - if (!roles.length) return []; - return roles + 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 isCurrent = roleKey.toLowerCase().includes('customer'); + const isCurrent = normalizeRoleKey(props.roleKey || '') === normalizeRoleKey(roleKey); return { key: roleKey, title, @@ -927,6 +1061,26 @@ export default function DashboardDesignPreview(props: { 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 isCurrent = normalizeRoleKey(props.roleKey || '') === normalizeRoleKey(item.key); + merged.push({ ...item, status: isCurrent ? 'Registered' : 'Available', action: isCurrent ? 'Active' : 'Register' }); + } + }); + return merged.slice(0, 10); }); const [dashboardWidgetOrder, setDashboardWidgetOrder] = createSignal([]); const [draggingDashboardWidget, setDraggingDashboardWidget] = createSignal(null); @@ -939,8 +1093,27 @@ export default function DashboardDesignPreview(props: { const [viewTicketFiles, setViewTicketFiles] = createSignal([]); const [profileSettingsTab, setProfileSettingsTab] = createSignal<'change_password' | 'notifications' | 'privacy'>('change_password'); const [showDeleteAccountModal, setShowDeleteAccountModal] = createSignal(false); - const [showPortfolioPreview, setShowPortfolioPreview] = 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([]); + const [portfolioLanguages, setPortfolioLanguages] = createSignal([]); + const [portfolioServiceAreas, setPortfolioServiceAreas] = createSignal([]); + const [portfolioSpecialtyInput, setPortfolioSpecialtyInput] = createSignal(''); + const [portfolioLanguageInput, setPortfolioLanguageInput] = createSignal(''); + const [portfolioAreaInput, setPortfolioAreaInput] = createSignal(''); + const [profileDocumentType, setProfileDocumentType] = createSignal('Aadhar Card'); + const [portfolioFormValues, setPortfolioFormValues] = createSignal>({}); + const [portfolioFormErrors, setPortfolioFormErrors] = createSignal>({}); + const [portfolioValidationNotice, setPortfolioValidationNotice] = createSignal(''); + const [portfolioServices, setPortfolioServices] = createSignal>([]); + const [portfolioServiceDraft, setPortfolioServiceDraft] = createSignal<{ name: string; model: string; price: string; details: string }>({ name: '', model: 'Flat', price: '', details: '' }); + const [portfolioExperiences, setPortfolioExperiences] = createSignal>([]); + const [portfolioExperienceDraft, setPortfolioExperienceDraft] = createSignal<{ year: string; title: string; details: string }>({ year: '', title: '', details: '' }); + const [portfolioDesignTools, setPortfolioDesignTools] = createSignal([]); + const [portfolioToolInput, setPortfolioToolInput] = createSignal(''); + const [portfolioPhotos, setPortfolioPhotos] = createSignal([]); const [requirementsView, setRequirementsView] = createSignal<'list' | 'new' | 'detail'>('list'); const [requirementsStep, setRequirementsStep] = createSignal(1); const [selectedRequirementRole, setSelectedRequirementRole] = createSignal('PHOTOGRAPHER'); @@ -953,11 +1126,29 @@ export default function DashboardDesignPreview(props: { 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 [leadRequestRows, setLeadRequestRows] = createSignal([ - { id: 'LD-29745', title: 'Editorial Fashion Shoot - Avant-Garde Series', city: 'London, UK', requestDate: 'Oct 24, 2023', status: 'request_sent', decisionDate: '--' }, - { id: 'LD-29612', title: 'Corporate Branding - Tech HQ Reimagined', city: 'San Francisco, CA', requestDate: 'Oct 23, 2023', status: 'contact_unlocked', decisionDate: 'Oct 23, 2023' }, - { id: 'LD-29588', title: 'Family Outdoor Session', city: 'Rome, Italy', requestDate: 'Oct 19, 2023', status: 'rejected', decisionDate: 'Oct 19, 2023' }, + { 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>({ @@ -970,12 +1161,131 @@ export default function DashboardDesignPreview(props: { const toggleProfileSetting = (key: string) => { setProfileSettingToggles((prev) => ({ ...prev, [key]: !prev[key] })); }; + 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' }; + }; + const submitProfileForApproval = () => { + setProfileApprovalState('SUBMITTED'); + setTimeout(() => setProfileApprovalState('IN_REVIEW'), 250); + }; + 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 maxLeadRequests = createMemo(() => Math.floor(usableLeadCredits() / leadCostPerContact)); + 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; @@ -1016,9 +1326,51 @@ export default function DashboardDesignPreview(props: { 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'); @@ -1032,8 +1384,15 @@ export default function DashboardDesignPreview(props: { 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()); @@ -1081,9 +1440,14 @@ export default function DashboardDesignPreview(props: { if (usableLeadCredits() < leadCostPerContact) return; let changed = false; setLeadCards((prev) => prev.map((card) => { - if (card.id !== leadId || card.status !== 'open') return card; + if (card.id !== leadId || card.status !== 'open' || card.contactCount >= card.maxContacts) return card; changed = true; - return { ...card, status: 'requested' as const }; + 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) => { @@ -1098,8 +1462,8 @@ export default function DashboardDesignPreview(props: { { id: leadId, title: selected?.title || 'Lead Request', - city: selected?.location || 'Remote', - requestDate: 'Oct 24, 2023', + city: selected ? `${selected.area || 'Central'}, ${selected.location}` : 'Chennai, India', + requestDate: 'Apr 02, 2026', status: 'request_sent' as const, decisionDate: '--', }, @@ -1108,38 +1472,111 @@ export default function DashboardDesignPreview(props: { }); }; + 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: 'Oct 25, 2023' } + ? { ...row, status: 'contact_unlocked' as const, decisionDate: 'Apr 03, 2026' } : row)); }; const cancelLeadRequest = (leadId: string) => { - setLeadCards((prev) => prev.map((card) => card.id === leadId && card.status === 'requested' - ? { ...card, status: 'open' as const } - : card)); + 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: 'Oct 25, 2023' } + ? { ...row, status: 'cancelled_by_professional' as const, decisionDate: 'Apr 03, 2026' } : row)); }; const refundPendingLead = (leadId: string) => { - setLeadCards((prev) => prev.map((card) => card.id === leadId && card.status === 'requested' - ? { ...card, status: 'open' as const } - : card)); + 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 selectedPortfolioTab = spec.tabs.find((item) => normalizeTabKey(item) === normalizeTabKey(resolvedTabKey())) || spec.tabs[0] || 'overview'; + 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); @@ -1149,63 +1586,383 @@ export default function DashboardDesignPreview(props: { const portfolioJobsCompleted = portfolioJobsCompletedPreview; const portfolioFeedbackCount = portfolioFeedbackCountPreview; const testimonialsUnlocked = portfolioTestimonialsUnlocked(); - const showSection = (tabKey: string) => selectedPortfolioTabKey === normalizeTabKey('overview') || selectedPortfolioTabKey === tabKey; + 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 = { + 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 ( +
+

{field}

+ + 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`} /> + + + +
+ } + > + + + {(faq, i) => (
@@ -1386,6 +2349,56 @@ export default function DashboardDesignPreview(props: { )}
+ + +
+

+ Final review mode: check all tabs, then submit for approval.}> + Step {activePortfolioStepIndex + 1} of {portfolioStepKeys.length} + +

+
+ + 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 + + } + > + + +
+
); @@ -1439,6 +2452,31 @@ export default function DashboardDesignPreview(props: { } return (
+ +
+

Complete Verification

+

+ To start receiving opportunities, submit your profile and portfolio for admin approval. +

+
+
+

Step 1

+

Profile: {approvalTone(profileApprovalState()).label}

+
+
+

Step 2

+

Portfolio: {approvalTone(portfolioApprovalState()).label}

+
+
+
+ + + + + +
+
+

Widget Customization

Drag and drop cards below to reorder your dashboard widgets.

@@ -1446,7 +2484,14 @@ export default function DashboardDesignPreview(props: {

Welcome back, Alex

-

Manage your enterprise requirements and track professional responses in real-time.

+

To start receiving opportunities, complete My Profile and My Portfolio, then submit both for approval.

+
+ + + + + +

Profile Status

@@ -1487,38 +2532,59 @@ export default function DashboardDesignPreview(props: { if (customerKey().includes('profile')) { const spec = currentProfileSpec(); const selectedTab = spec.tabs.find((item) => normalizeTabKey(item) === normalizeTabKey(tab)) || spec.tabs[0] || 'basic info'; - const defaultFields = ['Full Name', 'Email Address', 'Mobile Number', 'City']; + 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 leftColumn = fieldsForTab.slice(0, Math.max(1, Math.ceil(fieldsForTab.length / 2))); - const rightColumn = fieldsForTab.slice(Math.max(1, Math.ceil(fieldsForTab.length / 2))); + 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|specialization|industry|subjects|platforms|budget|response time/i.test(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 = (() => { - const key = safeLabel.toLowerCase(); + 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'; - return `${isSelect ? 'Select' : 'Enter'} ${safeLabel.toLowerCase()}`; + if (key.includes('place')) return 'Enter place in Chennai'; + if (isSpecialitiesField) return 'e.g. Wedding, Product, Portrait'; + return `${isSelect ? 'Select' : 'Enter'} ${labelText.toLowerCase()}`; })(); return (
-

- {field} +

+ {labelText} *

-
- {placeholderText} +
+ - +
@@ -1533,7 +2599,7 @@ export default function DashboardDesignPreview(props: { ]; const activeSettingsTab = profileSettingsTab(); return ( -
+

Settings

@@ -1624,18 +2690,6 @@ export default function DashboardDesignPreview(props: {
-
-

Settings Summary

-

3 Sections

-

Simple account controls only.

-
- {['Change Password', 'Email Notifications', 'Profile Visibility'].map((item) => ( -
- {item} -
- ))} -
-
@@ -1679,20 +2733,49 @@ export default function DashboardDesignPreview(props: {
-
- {(field) => renderField(field)} + + {(field) => renderField(field)} +
+ } + > +
+ {(field) => renderField(field)}
-
- {(field) => renderField(field)} -
-
+
} > -
- {(field) => renderField(field)} +
+
+

+ Document type* +

+ +
+
+

+ Upload document* +

+ +
@@ -1701,20 +2784,27 @@ export default function DashboardDesignPreview(props: {
-

Profile Completeness

-

87%

-
-
+

What To Do Next

+

Complete profile and portfolio to unlock leads

+

Finish My Profile and My Portfolio, then submit both for admin approval. Leads will unlock only after approval.

+
+
+ My Profile + {approvalTone(profileApprovalState()).label} +
+ +
+ My Portfolio + {approvalTone(portfolioApprovalState()).label} +
+
-

Active Section

-

{titleCase(selectedTab)}

-

Mandatory Fields

-

{requiredCount}

-

Marked with *

-

{spec.subtitle}

-
- - +
+ + + + +
@@ -1722,6 +2812,33 @@ export default function DashboardDesignPreview(props: { } if (customerKey() === 'leads') { + if (!bothApprovalsApproved()) { + return ( +
+
+

Leads Locked

+

+ Complete verification before accessing leads. You must submit profile and portfolio, then get admin approval. +

+
+
+

Profile Verification

+

{approvalTone(profileApprovalState()).label}

+
+
+

Portfolio Verification

+

{approvalTone(portfolioApprovalState()).label}

+
+
+
+ + + +
+
+
+ ); + } 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' }; @@ -1731,6 +2848,8 @@ export default function DashboardDesignPreview(props: { 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 ( @@ -1738,31 +2857,69 @@ export default function DashboardDesignPreview(props: {

Total Spent

-

{250 - leadCredits()}

+

{250 - leadCredits()}

Tracecoins used this month

Active Requests

-

{leadCards().filter((card) => card.status === 'requested').length}

+

{leadCards().filter((card) => card.status === 'requested').length}

card.status === 'requested').length * 10)}%;background:#FF5E13`} />

Locked Credits

-

{lockedLeadCredits()}

-

Held until customer decision (auto-refund in 1 day)

+

{lockedLeadCredits()}

+

Held until service seeker decision (auto-refund in 1 day)

-

Requested Contacts

+

Requested Contacts

- +
+ + +
+ {['Newest First', 'Oldest First'].map((item) => ( + + ))} +
+
+
+
+ + +
+ {['All Status', 'Request Sent', 'Contact Unlocked', 'Rejected', 'Expired Refunded', 'Cancelled By Professional'].map((item) => ( + + ))} +
+
+
+
+
+ + 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" /> +
+ Sort: {requestedSortFilter()} + Status: {requestedStatusFilter()} +
+
@@ -1772,7 +2929,7 @@ export default function DashboardDesignPreview(props: { - + {(row) => { const badge = requestStatusPill(row.status); return ( @@ -1804,6 +2961,103 @@ export default function DashboardDesignPreview(props: {
+
+
+

+ Showing {pagedRequestedRows().length ? (requestedPage() - 1) * requestedPerPage + 1 : 0} to {(requestedPage() - 1) * requestedPerPage + pagedRequestedRows().length} of {filteredRequestedRows().length} requests +

+
+ i + 1)}> + {(pageNo) => ( + + )} + +
+
+
+
+ ); + } + + if (leadMarketplaceTab() === 'View Details' && selectedLead()) { + const lead = selectedLead()!; + const spec = leadDetailsSpec(lead); + return ( +
+
+
+
+
+

View Details

+

{lead.title}

+

{lead.category} • {lead.location} • {lead.area}

+
+ {lead.match} +
+
+ Time Frame:{spec.timeframe} + Date:{lead.dateRequired} + Budget:{lead.budget} + Area:{String(lead.area || 'Chennai')} +
+
+ +
+
+ +

Schedule

{spec.timeframe}

+
+
+ +

Date Required

{lead.dateRequired}

+
+
+ +

Area

{String(lead.area || 'Chennai')}

+
+
+ +
+

Win Probability

+

{leadProbability(lead)}%

+
+
+
+
+
+
+
+
+
+

Work Scope

+

{spec.scope}

+
+
+

Lead Specific Highlights

+
+ {(item) =>
{item}
}
+
+
+
+
+
Lead ID{lead.id}
+
+ Unlock Cost + NG{lead.cost} Tracecoin +
+
Contacted{lead.contactCount}/{lead.maxContacts}
+
+ + +
+
+
); @@ -1811,36 +3065,13 @@ export default function DashboardDesignPreview(props: { return (
-
- {[ - ['New Leads', '124'], - ['Matching Leads', '42'], - ['Saved Leads', '18'], - ['Requests Sent', String(leadRequestRows().length)], - ['Expiring Soon', '09'], - ['Tracecoins', String(leadCredits())], - ].map(([label, value], idx) => ( -
-

{label}

-

{value}

-
- ))} -
- -
-

- Contact request rule: 25 credits are locked per request (not deducted) until customer accepts. Pending requests auto-refund in 1 day. - If professional cancels request, 25 credits are deducted. With {leadCredits()} credits, max active requests now: {maxLeadRequests()}. -

-
-
- + {(item) => (
-
Search by role, area, keyword...
- {['London, UK +50mi', 'Photography', 'Budget Range', 'Client Timeline', 'Newest First'].map((item) => ( - - ))} +
+ + setLeadSearch(e.currentTarget.value)} placeholder="Search by role, area, keyword..." style="border:none;background:transparent;outline:none;width:100%;font-size:12px;color:#111827" /> +
+
+ + +
+
+

Area

+ +
+
+

Budget

+ +
+
+

Date

+ +
+
+ + +
+
+
+
+
+ + +
+ {['Newest First', 'Budget High-Low', 'Budget Low-High'].map((item) => ( + + ))} +
+
+
+ Sort: {leadSortFilter()}
-
- +
+ {(lead) => ( -
-
-
- +
+
+
+
+ + {lead.status === 'unlocked' ? 'Contact Unlocked' : lead.status === 'requested' ? 'Request Sent' : lead.status === 'closed' ? 'Lead Closed' : 'Open Lead'} + + {lead.category} • {lead.location} +
+

{lead.title}

+
+ Area: {lead.area} + | + Date: {lead.dateRequired} + | + + T + {lead.cost} + + | + + + + + + + + {leadProbability(lead)}% + + | + + + {lead.contactCount}/{lead.maxContacts} + contacted + + {Math.max(0, lead.maxContacts - lead.contactCount)} slots left +
-
-

Investment

-

{lead.cost}

-

Tracecoins

-
-
-
-
- - {lead.status === 'unlocked' ? 'Contact Unlocked' : lead.status === 'requested' ? 'Request Sent' : 'High Urgency'} - - {lead.category} • {lead.location} -
-

{lead.title}

-
-

Date Required

{lead.dateRequired}

-

Urgency

{lead.urgency}

-

Cost

{lead.cost} Tracecoins

-
-
-
-
+

{lead.match}

-

{lead.budget}

-

Est. Budget

-
-
- - +

{lead.budget}

+

Est. Budget

+
+ + - - - - - - - + Request Contact + + + + + +
)}
+
+

+ Showing {pagedLeadCards().length ? (leadPage() - 1) * leadsPerPage + 1 : 0} to {(leadPage() - 1) * leadsPerPage + pagedLeadCards().length} of {filteredLeadCards().length} leads +

+
+ i + 1)}> + {(pageNo) => ( + + )} + +
+
+ +
+
+
+ T +

Confirm Contact Unlock

+
+

You are about to spend 25 Tracecoins to request and view this service seeker contact when approved. Do you want to continue?

+
+ + +
+
+
+
); } @@ -2944,7 +4284,208 @@ export default function DashboardDesignPreview(props: { ); } - if (customerKey() === 'received responses') { + 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 ( +
+
+
+
+
+

My Responses • View Details

+

{lead.title}

+

{lead.category} • {lead.location} • {lead.area}

+
+ +
+
+
+ +

Schedule

{spec.timeframe}

+
+
+ +

Date Required

{lead.dateRequired}

+
+
+ +

Area

{String(lead.area || 'Chennai')}

+
+
+ +

Unlock Cost

{lead.cost} Tracecoin

+
+
+
+
+
+
+

Work Scope

+

{spec.scope}

+
+
+

Lead Specific Highlights

+
{(item) =>
{item}
}
+
+
+
+
Lead ID{lead.id}
+
Category{lead.category}
+
Location{lead.location}
+
StatusFrom My Responses
+
+
+
+
+ ); + } + return ( +
+
+
+

My Responses

+
+
+ + +
+ {['Newest First', 'Oldest First'].map((item) => ( + + ))} +
+
+
+
+ + +
+ {['All Status', 'Request Sent', 'Contact Unlocked', 'Rejected', 'Expired Refunded', 'Cancelled By Professional'].map((item) => ( + + ))} +
+
+
+ +
+
+
+
+ + 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" /> +
+ Sort: {requestedSortFilter()} + Status: {requestedStatusFilter()} +
+
+ + + + {['Lead ID', 'Lead Title', 'Request Date', 'Status', 'Cost', 'Decision Date', 'Action'].map((h) => ( + + ))} + + + + + {(row) => { + const badge = leadStatusPill(row.status); + return ( + + + + + + + + + + ); + }} + + +
{h}
#{row.id} +

{row.title}

+

{row.city}

+
{row.requestDate} + {badge.text} + {leadCostPerContact}{row.decisionDate} +
+ + + + +
+
+
+
+

+ Showing {pagedList.length ? (currentPage - 1) * requestedPerPage + 1 : 0} to {(currentPage - 1) * requestedPerPage + pagedList.length} of {list.length} responses +

+
+ i + 1)}> + {(pageNo) => ( + + )} + +
+
+
+
+ ); + } + const list = RESPONSE_ROWS.filter((r) => { if (tab === 'new') return r.status === 'new'; if (tab === 'shortlisted') return r.status === 'shortlisted'; @@ -2957,7 +4498,7 @@ export default function DashboardDesignPreview(props: {

Before accepting contact request, review portfolio, experience, and quoted charges.

@@ -3058,7 +4599,7 @@ export default function DashboardDesignPreview(props: {

{pkg.subtitle}

{pkg.name}

-

{pkg.credits}

+

{pkg.credits}

Credits

{pkg.bonus}

{pkg.price}

@@ -3134,7 +4675,7 @@ export default function DashboardDesignPreview(props: { {row[2]} {row[3]} - {row[4]} + {row[4]} {row[5]} @@ -3169,7 +4710,23 @@ export default function DashboardDesignPreview(props: {

Current Balance

12,450 TC

-
Usage Logs
+
+
+ + + +
+ +
@@ -3188,7 +4745,7 @@ export default function DashboardDesignPreview(props: { - + @@ -3212,9 +4769,22 @@ export default function DashboardDesignPreview(props: { if (tab === 'invoices') { return (
-
-

Invoices

- +
+
+ + + +
+
{row[0]} {row[1]}{row[2]}{row[2]} {row[3]} {row[4]} {row[5]}
@@ -3232,7 +4802,7 @@ export default function DashboardDesignPreview(props: { ))} @@ -3259,7 +4829,7 @@ export default function DashboardDesignPreview(props: {

Current Balance

12,450 TC

-

+12% from last month

+

+12% from last month

@@ -3283,6 +4853,23 @@ export default function DashboardDesignPreview(props: {

Recent Transactions

+
+
+ + + +
+ +
{row[2]} {row[3]} - {row[4]} + {row[4]}
@@ -3299,7 +4886,7 @@ export default function DashboardDesignPreview(props: { @@ -3314,53 +4901,41 @@ export default function DashboardDesignPreview(props: { if (customerKey() === 'explore nxtgauge') { return (
-
-
-

Welcome back, explore more opportunities on Nxtgauge

-

Nxtgauge helps you discover, connect, and grow across multiple services. Start a new journey or expand your professional horizon today.

-
-
-
-

120% Expansion Rate

-
-
-
+
+

Explore opportunities on Nxtgauge

+

Discover services, connect with verified users, and expand into additional roles using the same dashboard workflow.

-
- -

Service Professionals

-

Expand from one niche to multiple categories and unlock more demand.

-
-
- -

Growing Companies

-

Hire, list services, and create new revenue channels from one place.

-
-
- -

Individual Customers

-

Access multiple verified services through one unified profile.

-
+ {[ + { 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 isCurrentRole = normalizeRoleKey(props.roleKey || '') === normalizeRoleKey(roleCard.key); + return ( +
+ +

{roleCard.title}

+

{roleCard.subtitle}

+ +
+ ); + })}
-

Discover New Services

-

Expand your horizon by registering for additional services.

-
- +

Professional Services

+

Choose from 10 professional roles, including UGC Content Creator.

+
+ {(role) => ( -
+
-
- {[ - ['Verification Status', 'In Progress'], - ['Profile Approval', 'Pending'], - ['Submitted Service', 'Social Media Manager'], - ['Submission Date', 'Oct 24, 2024'], - ['Review Completion', '65%'], - ].map(([k, v], i) => ( -
-

{k}

-

{v}

- -
-
-
- +
+

Verification Center

+

Complete just 2 steps. Submit profile and portfolio. We review and unlock access.

+
+
+

Step 1

+

Profile: {approvalTone(profileApprovalState()).label}

- ))} +
+

Step 2

+

Portfolio: {approvalTone(portfolioApprovalState()).label}

+
+
+

Final Access

+

{bothApprovalsApproved() ? 'Unlocked' : 'Blocked'}

+
+
- -
-
-
-

Application Journey

-
-

Current step: Under Review (2 of 3)

- 67% Complete -
- -
-
-
- -
- {[ - ['Submitted', 'Completed'], - ['Under Review', 'In Progress'], - ['Approved', 'Pending'], - ].map(([s, state], i) => ( -
-
- {i < 2 ? '✓' : '3'} - {state} -
-

{s}

-
- ))} -
- -

Next step: Approval will be completed after reviewer checks are done.

-
- -
-
-

Verification Checklist

- Live Verification -
-
- {[ - ['Identity Verification', 'Government ID & Biometrics', 'Verified'], - ['Contact Verification', 'Phone & Corporate Email', 'Verified'], - ['Profile Details', 'Basic Info & Avatar', 'Checked'], - ['Role Details', 'Experience & Previous Services', 'Under Review'], - ].map(([title, sub, state]) => ( -
-
-

{title}

-

{sub}

-
- {state} -
- ))} -
+
+
+

Profile Verification

+

Finish your basic information and required documents, then submit.

+
+ +
- -
-
-
- - - -

Action Required

-
-

Please upload the missing document to continue.

-
-

What to upload

- Address Proof -
-

Purpose: address check.

-

Updated today

-
-
-

Next Step

-

Upload missing document

-

Once uploaded, your verification will move to final approval.

- +
+

Portfolio Verification

+

Add portfolio details and submit separately for review.

+
+ +
+
+

Admin Flow: once submitted, both items appear in Verification Management. Admin can request documents or approve. Leads remain blocked until both are approved.

+
); } @@ -3973,7 +5487,7 @@ export default function DashboardDesignPreview(props: { } return (
- {['Customer (Active)', 'Professional', 'Company'].map((r, i) =>

{r}

)} + {['Service Seeker (Active)', 'Professional', 'Company'].map((r, i) =>

{r}

)}
); } @@ -3985,27 +5499,68 @@ export default function DashboardDesignPreview(props: { return (

Confirm Logout

-

Are you sure you want to logout from the customer portal?

+

Are you sure you want to logout from the service seeker portal?

); } - if (customerKey() === 'my portfolio') return renderPortfolioContent(); + if (customerKey() === 'my portfolio') { + if (!isProfessionalRoleKey(props.roleKey || '')) { + return ( +
+

My Portfolio is only for professionals

+

Switch to a professional role to manage and preview portfolio content.

+
+ ); + } + return renderPortfolioContent(); + } return (

Screen Preview

-

Customer screen is selected and interactive.

+

Service seeker screen is selected and interactive.

); }; + 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 (

Actual End-User Dashboard UI Preview

- +
+ + + + +
@@ -4053,13 +5608,43 @@ export default function DashboardDesignPreview(props: {

Current View

{isCustomerExternalMode() ? customerView().title : titleCase(props.activeSidebar)}

{isCustomerExternalMode() ? customerView().subtitle : 'Interactive preview for configured dashboard.'}

- 0}> -
- - {(item) => ( - (() => { + 0 && customerKey() !== 'my portfolio'}> + + + {(item) => ( + (() => { + const itemKey = normalizeTabKey(item); + const isLockedTestimonialsTab = customerKey() === 'my portfolio' && itemKey === 'testimonials' && !portfolioTestimonialsUnlocked(); + return ( + + ); + })() + )} + +
+ } + > +
+ + {(item) => { const itemKey = normalizeTabKey(item); - const isLockedTestimonialsTab = customerKey() === 'my portfolio' && itemKey === 'testimonials' && !portfolioTestimonialsUnlocked(); + const isLockedTestimonialsTab = itemKey === 'testimonials' && !portfolioTestimonialsUnlocked(); + const isActive = resolvedTabKey() === itemKey; + const Icon = portfolioTabIcon(item); return ( ); - })() - )} - -
+ }} + +
+
@@ -4100,92 +5690,6 @@ export default function DashboardDesignPreview(props: {
- -
setShowPortfolioPreview(false)} - > -
e.stopPropagation()} - > -
-
- Customer View -

What customers see before accepting your contact request

-
- -
-
-
-
-

Alex Morgan

- - Verified - -
-

{portfolioSpecForRole(props.roleKey||'').roleLabel}

-
- Area: Mumbai Region - Place: Andheri East, Mumbai - Travel: Mumbai, Navi Mumbai, Thane, Pune -
-
-
- {[1,2,3,4,5].map(() => )} - 4.9 - (128 reviews) -
-

- Passionate {portfolioSpecForRole(props.roleKey||'').roleLabel.toLowerCase()} with 7+ years of experience delivering exceptional results. Specialising in {portfolioSpecForRole(props.roleKey||'').specialties.slice(0,3).join(', ')}. -

-
- {[['248','Projects'],['7+','Yrs Exp'],['4.9','Rating'],['128','Reviews']].map(([val,lbl]) => ( -
-

{val}

-

{lbl}

-
- ))} -
-
-

Specialties

-
- {portfolioSpecForRole(props.roleKey||'').specialties.map((s) => ( - {s} - ))} -
-
-
-

Featured Package

- {(() => { - const pkg = portfolioSpecForRole(props.roleKey||'').packages[1]; - return ( -
-
-

{pkg.name}

- {pkg.price} -
-
- {pkg.items.map((item) => ( - - {item} - - ))} -
-
- ); - })()} -
-
- - -
-
-
-
-
); } diff --git a/src/components/admin/ExternalRoleForm.tsx b/src/components/admin/ExternalRoleForm.tsx index cb4bea2..589beec 100644 --- a/src/components/admin/ExternalRoleForm.tsx +++ b/src/components/admin/ExternalRoleForm.tsx @@ -57,6 +57,7 @@ const ONBOARDING_SCHEMA_OPTIONS = [ 'social_media_manager_onboarding_v1', 'fitness_trainer_onboarding_v1', 'catering_service_onboarding_v1', + 'ugc_content_creator_onboarding_v1', 'default_onboarding_v1', ]; @@ -142,7 +143,7 @@ const DEFAULT_PRESETS: Record = { }, customer: { roleKey: 'customer', - displayName: 'Customer', + displayName: 'Service Seeker', vertical: 'marketplace', roleCategory: 'consumer', enabledModules: ['dashboard', 'profile', 'requirements', 'marketplace', 'wallet', 'notifications', 'settings'], @@ -163,6 +164,21 @@ const DEFAULT_PRESETS: Record = { runtimeConfigVersion: 1, isActive: true, }, + ugc_content_creator: { + roleKey: 'ugc_content_creator', + displayName: 'UGC Content Creator', + vertical: 'marketplace', + roleCategory: 'provider', + enabledModules: MARKETPLACE_PROVIDER_MODULES, + permissions: MARKETPLACE_PROVIDER_PERMISSIONS, + onboardingSchemaId: 'ugc_content_creator_onboarding_v1', + requiresOnboardingApproval: true, + requiresLeadApproval: true, + requiresJobApproval: false, + featureLimits: { maxOpenLeads: 10 }, + runtimeConfigVersion: 1, + isActive: true, + }, photographer: { roleKey: 'photographer', displayName: 'Photographer', @@ -476,7 +492,7 @@ export default function ExternalRoleForm(props: ExternalRoleFormProps) { - + @@ -484,6 +500,7 @@ export default function ExternalRoleForm(props: ExternalRoleFormProps) { + diff --git a/src/components/admin/OnboardingFlowBuilder.tsx b/src/components/admin/OnboardingFlowBuilder.tsx index d5136f7..9444c30 100644 --- a/src/components/admin/OnboardingFlowBuilder.tsx +++ b/src/components/admin/OnboardingFlowBuilder.tsx @@ -58,6 +58,7 @@ const ROLE_OPTIONS = [ 'job_seeker', 'jobseeker', 'customer', + 'service_seeker', 'professional', 'photographer', 'video_editor', @@ -68,6 +69,7 @@ const ROLE_OPTIONS = [ 'makeup_artist', 'tutor', 'developer', + 'ugc_content_creator', ]; function fallbackRoleOptions(): { value: string; label: string }[] { @@ -203,7 +205,7 @@ export function createDefaultFields(roleKey: string): OnboardingField[] { 'Professional role', 'select', true, - ['photographer', 'makeup_artist', 'tutor', 'developer', 'video_editor', 'graphic_designer', 'social_media_manager', 'fitness_trainer', 'catering_services'], + ['photographer', 'makeup_artist', 'tutor', 'developer', 'video_editor', 'ugc_content_creator', 'graphic_designer', 'social_media_manager', 'fitness_trainer', 'catering_services'], ), createField('full_name', 'Full name', 'text', true), createField('experience', 'Experience', 'text', true), @@ -225,7 +227,7 @@ export function createDefaultFields(roleKey: string): OnboardingField[] { 'Service category', 'select', true, - ['photographer', 'makeup_artist', 'tutor', 'developer', 'video_editor', 'graphic_designer', 'social_media_manager', 'fitness_trainer', 'catering_services'], + ['photographer', 'makeup_artist', 'tutor', 'developer', 'video_editor', 'ugc_content_creator', 'graphic_designer', 'social_media_manager', 'fitness_trainer', 'catering_services'], ), createField('event_type', 'Event type', 'text', false), createField('coverage_hours', 'Coverage hours', 'number', false), @@ -325,6 +327,19 @@ export function createDefaultFields(roleKey: string): OnboardingField[] { createField('availability', 'Availability', 'select', true, ['weekdays', 'weekends', 'all_days']), createField('identity_document', 'Identity Document', 'file', true), ], + ugc_content_creator: [ + createField('full_name', 'Full name', 'text', true), + createField('email', 'Email', 'email', true), + createField('phone', 'Phone', 'tel', true), + createField('city', 'City', 'text', true), + createField('service_area', 'Area / Place', 'text', true), + createField('experience_years', 'Experience (Years)', 'number', false), + createField('specialization', 'UGC specialization', 'text', true), + createField('portfolio_url', 'Portfolio URL', 'url', false), + createField('price_range', 'Price range', 'text', true), + createField('availability', 'Availability', 'select', true, ['weekdays', 'weekends', 'all_days']), + createField('identity_document', 'Identity Document', 'file', true), + ], graphic_designer: [ createField('full_name', 'Full name', 'text', true), createField('email', 'Email', 'email', true), @@ -377,7 +392,7 @@ export function createDefaultFields(roleKey: string): OnboardingField[] { if (role === 'company') return company; if (role === 'job_seeker' || role === 'jobseeker') return jobseeker; - if (role === 'customer') return customer; + if (role === 'customer' || role === 'service_seeker') return customer; if (role === 'professional') return professionalBase; if (specialistOverrides[role]) return specialistOverrides[role]; diff --git a/src/routes/admin/[...module].tsx b/src/routes/admin/[...module].tsx index bdac2e6..54fd0c3 100644 --- a/src/routes/admin/[...module].tsx +++ b/src/routes/admin/[...module].tsx @@ -3,6 +3,7 @@ import { createMemo } from 'solid-js'; import AdminShell from '~/components/AdminShell'; import ApprovalManagementPage from './approval'; import VerificationManagementPage from './verification'; +import UsersManagementPage from './users'; function toTitle(value: string): string { return value @@ -46,6 +47,10 @@ export default function LegacyModuleShellPage() { return ; } + if (modulePath === 'users' || modulePath === 'users-management' || modulePath === 'user-management') { + return ; + } + const moduleName = createMemo(() => toTitle(modulePath || 'Management')); const legacyPath = createMemo(() => resolveLegacyPath(modulePath)); const legacyUrl = createMemo(() => `${LEGACY_ADMIN_ORIGIN}${legacyPath()}`); diff --git a/src/routes/admin/approval.tsx b/src/routes/admin/approval.tsx index 53703af..d5f14be 100644 --- a/src/routes/admin/approval.tsx +++ b/src/routes/admin/approval.tsx @@ -3,10 +3,20 @@ import AdminShell from '~/components/AdminShell'; import type { CrudRecord } from '~/lib/admin/types'; const API = '/api/gateway'; +const APPROVAL_QUEUE_STORAGE_KEY = 'nxtgauge_admin_approval_queue'; + +type ApprovalSubmittedField = { label: string; value: string }; +type ApprovalDocument = { + id: string; + title: string; + type: 'IMAGE' | 'PDF'; + url: string; + status: 'SUBMITTED' | 'MISSING' | 'INVALID'; +}; type ApprovalRecord = CrudRecord & { applicantName?: string; - approvalType: 'PROFILE' | 'BUSINESS' | 'JOB' | 'ORDER' | 'INVOICE' | 'COUPON' | 'DISCOUNT' | 'TAX' | 'ROLE' | 'REQUIREMENT'; + approvalType: 'PROFILE' | 'BUSINESS' | 'JOB' | 'ORDER' | 'INVOICE' | 'COUPON' | 'DISCOUNT' | 'TAX' | 'ROLE' | 'REQUIREMENT' | 'PORTFOLIO'; userType: 'CUSTOMER' | 'PROFESSIONAL' | 'COMPANY' | 'JOBSEEKER'; roleTags?: string[]; primaryService?: string; @@ -16,12 +26,44 @@ type ApprovalRecord = CrudRecord & { assignedApprover?: string; priority: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL'; status: 'PENDING' | 'IN_REVIEW' | 'APPROVED' | 'REJECTED' | 'ON_HOLD' | 'ESCALATED'; + sourceKey?: string; + submittedFields?: ApprovalSubmittedField[]; + documents?: ApprovalDocument[]; + payload?: any; +}; + +type ApprovalQueueItem = { + id: string; + requestType: + | 'Profile Approval' + | 'Portfolio Approval' + | 'Company Approval' + | 'Job Seeker Approval' + | 'Service Seeker Profile Approval' + | 'Service Seeker Requirement' + | 'Job Approval'; + applicantName: string; + roleLabel: string; + userType: 'PROFESSIONAL' | 'COMPANY' | 'JOBSEEKER' | 'CUSTOMER'; + roleKey: string; + area: string; + submittedOn: string; + documents: ApprovalDocument[]; + submittedFields: ApprovalSubmittedField[]; }; const toTitle = (value: string) => String(value || '') .replace(/_/g, ' ') .replace(/\b\w/g, (c) => c.toUpperCase()); +const normalizeUserType = (value: unknown): ApprovalRecord['userType'] => { + const key = String(value || '').toUpperCase(); + if (key.includes('COMPANY')) return 'COMPANY'; + if (key.includes('JOB_SEEKER') || key.includes('JOBSEEKER')) return 'JOBSEEKER'; + if (key.includes('CUSTOMER') || key.includes('SERVICE_SEEKER')) return 'CUSTOMER'; + return 'PROFESSIONAL'; +}; + function extractRoleTags(source: any): string[] { const values: string[] = []; const pushValue = (value: unknown) => { @@ -48,6 +90,225 @@ function extractRoleTags(source: any): string[] { return Array.from(new Set(values.map((v) => toTitle(v)))).slice(0, 4); } +function extractSubmittedFields(source: any): ApprovalSubmittedField[] { + const payload = source || {}; + const fullName = String( + payload.full_name + || payload.fullName + || [payload.first_name || payload.firstName, payload.last_name || payload.lastName].filter(Boolean).join(' ') + || payload.company_name + || payload.title + || '—', + ).trim(); + + const candidates: ApprovalSubmittedField[] = [ + { label: 'Name / Title', value: fullName || '—' }, + { label: 'Email', value: String(payload.email || payload.email_address || payload.emailAddress || '—') }, + { label: 'Mobile', value: String(payload.mobile || payload.mobile_number || payload.phone || payload.contact_number || '—') }, + { label: 'Role / Category', value: String(payload.role_key || payload.role || payload.category || payload.profession || payload.service_category || '—') }, + { label: 'Area', value: String(payload.area || payload.location || payload.city || 'Chennai') }, + { label: 'Place', value: String(payload.place || payload.locality || payload.city || 'Chennai') }, + { label: 'Description', value: String(payload.description || payload.about || payload.bio || '—') }, + ]; + + return candidates.filter((item) => item.value && item.value !== '—'); +} + +function extractDocuments(source: any): ApprovalDocument[] { + const raw = Array.isArray(source?.documents) ? source.documents : []; + if (!raw.length) { + const portfolioImages = Array.isArray(source?.portfolio_images) + ? source.portfolio_images + : Array.isArray(source?.images) + ? source.images + : Array.isArray(source?.gallery) + ? source.gallery + : []; + if (!portfolioImages.length) return []; + return portfolioImages.slice(0, 6).map((asset: any, idx: number) => ({ + id: String(asset.id || `portfolio-${idx + 1}`), + title: String(asset.title || asset.name || `Portfolio Image ${idx + 1}`), + type: 'IMAGE', + url: String(asset.url || '/nxtgauge-logo.png'), + status: 'SUBMITTED', + })); + } + return raw.slice(0, 12).map((doc: any, idx: number) => { + const statusRaw = String(doc.status || '').toUpperCase(); + return { + id: String(doc.id || `doc-${idx + 1}`), + title: String(doc.title || doc.name || `Document ${idx + 1}`), + type: String(doc.type || '').toUpperCase().includes('PDF') ? 'PDF' : 'IMAGE', + url: String(doc.url || '/nxtgauge-logo.png'), + status: statusRaw === 'MISSING' + ? 'MISSING' + : statusRaw === 'INVALID' + ? 'INVALID' + : 'SUBMITTED', + }; + }); +} + +const FALLBACK_APPROVAL_ROWS: ApprovalRecord[] = [ + { + id: 'AP-P-92841', + name: 'Profile Approval - Sarah Jenkins (Demo)', + applicantName: 'Sarah Jenkins (Demo)', + approvalType: 'PROFILE', + userType: 'PROFESSIONAL', + roleTags: ['Photographer'], + primaryService: 'Photography', + area: 'T. Nagar, Chennai', + submittedDate: '2026-04-01', + verificationStatus: 'VERIFIED', + assignedApprover: 'Unassigned', + priority: 'MEDIUM', + status: 'PENDING', + updatedAt: '2026-04-01', + sourceKey: 'fallback:profile:92841', + submittedFields: [ + { label: 'Name / Title', value: 'Sarah Jenkins' }, + { label: 'Email', value: 'sarah.jenkins@nxtgauge.com' }, + { label: 'Mobile', value: '+91 90000 00001' }, + { label: 'Area', value: 'T. Nagar' }, + { label: 'Place', value: 'Chennai' }, + ], + documents: [ + { id: 'identity-proof', title: 'Identity Proof', type: 'IMAGE', url: '/nxtgauge-logo.png', status: 'SUBMITTED' }, + { id: 'address-proof', title: 'Address Proof', type: 'PDF', url: '/nxtgauge-icon.png', status: 'SUBMITTED' }, + ], + }, + { + id: 'AP-F-92835', + name: 'Portfolio Approval - Marcus Davis (Demo)', + applicantName: 'Marcus Davis (Demo)', + approvalType: 'PORTFOLIO', + userType: 'PROFESSIONAL', + roleTags: ['Graphic Designer'], + primaryService: 'Design Portfolio', + area: 'Velachery, Chennai', + submittedDate: '2026-04-01', + verificationStatus: 'VERIFIED', + assignedApprover: 'Unassigned', + priority: 'HIGH', + status: 'PENDING', + updatedAt: '2026-04-01', + sourceKey: 'fallback:portfolio:92835', + submittedFields: [ + { label: 'Name / Title', value: 'Marcus Davis' }, + { label: 'Role / Category', value: 'Graphic Designer' }, + { label: 'Area', value: 'Velachery' }, + { label: 'Description', value: 'Portfolio submitted for verification.' }, + ], + documents: [ + { id: 'portfolio-1', title: 'Portfolio Image 1', type: 'IMAGE', url: '/nxtgauge-logo.png', status: 'SUBMITTED' }, + { id: 'portfolio-2', title: 'Portfolio Image 2', type: 'IMAGE', url: '/nxtgauge-icon.png', status: 'SUBMITTED' }, + ], + }, + { + id: 'AP-B-92839', + name: 'Business Approval - Zenith Tech Hub (Demo)', + applicantName: 'Zenith Tech Hub (Demo)', + approvalType: 'BUSINESS', + userType: 'COMPANY', + roleTags: ['Company'], + primaryService: 'Hiring Company', + area: 'Guindy, Chennai', + submittedDate: '2026-03-31', + verificationStatus: 'VERIFIED', + assignedApprover: 'Unassigned', + priority: 'HIGH', + status: 'PENDING', + updatedAt: '2026-03-31', + sourceKey: 'fallback:company:92839', + submittedFields: [ + { label: 'Name / Title', value: 'Zenith Tech Hub' }, + { label: 'Email', value: 'admin@zenithtechhub.com' }, + { label: 'Area', value: 'Guindy' }, + { label: 'Place', value: 'Chennai' }, + ], + documents: [ + { id: 'gst-certificate', title: 'GST Certificate', type: 'PDF', url: '/nxtgauge-logo.png', status: 'SUBMITTED' }, + { id: 'incorporation', title: 'Incorporation Certificate', type: 'PDF', url: '/nxtgauge-icon.png', status: 'SUBMITTED' }, + ], + }, + { + id: 'AP-R-92812', + name: 'Requirement Approval - Luxury Wedding Shoot (Demo)', + applicantName: 'Luxury Wedding Shoot (Demo)', + approvalType: 'REQUIREMENT', + userType: 'CUSTOMER', + roleTags: ['Service Seeker', 'Photographer'], + primaryService: 'Service Requirement', + area: 'Adyar, Chennai', + submittedDate: '2026-03-30', + verificationStatus: 'FLAGGED', + assignedApprover: 'Unassigned', + priority: 'CRITICAL', + status: 'ESCALATED', + updatedAt: '2026-03-30', + sourceKey: 'fallback:requirement:92812', + submittedFields: [ + { label: 'Name / Title', value: 'Luxury Wedding Shoot' }, + { label: 'Role / Category', value: 'Photographer' }, + { label: 'Area', value: 'Adyar' }, + { label: 'Description', value: 'Need urgent premium wedding photography team.' }, + ], + documents: [ + { id: 'requirement-brief', title: 'Requirement Brief', type: 'PDF', url: '/nxtgauge-logo.png', status: 'SUBMITTED' }, + { id: 'reference-moodboard', title: 'Reference Moodboard', type: 'IMAGE', url: '/nxtgauge-icon.png', status: 'SUBMITTED' }, + ], + }, +]; + +function mergeWithFallbackRows(sourceRows: ApprovalRecord[]): ApprovalRecord[] { + const merged = [...sourceRows]; + for (const demo of FALLBACK_APPROVAL_ROWS) { + const duplicate = merged.some((row) => { + const rowKey = String(row.sourceKey || '').toLowerCase(); + const rowName = String(row.applicantName || '').toLowerCase(); + const rowType = String(row.approvalType || '').toLowerCase(); + const demoName = String(demo.applicantName || '').toLowerCase(); + const demoType = String(demo.approvalType || '').toLowerCase(); + if (rowKey.startsWith('fallback:')) return rowKey === String(demo.sourceKey || '').toLowerCase(); + return rowName === demoName && rowType === demoType; + }); + if (!duplicate) merged.push(demo); + } + return merged; +} + +function verificationToApprovalType(requestType: ApprovalQueueItem['requestType']): ApprovalRecord['approvalType'] { + if (requestType === 'Portfolio Approval') return 'PORTFOLIO'; + if (requestType === 'Service Seeker Requirement') return 'REQUIREMENT'; + if (requestType === 'Job Approval') return 'JOB'; + if (requestType === 'Company Approval') return 'BUSINESS'; + return 'PROFILE'; +} + +function mapQueueItemsToApprovals(items: ApprovalQueueItem[]): ApprovalRecord[] { + return items.map((item) => ({ + id: String(item.id), + name: `${item.requestType} - ${item.applicantName}`, + applicantName: item.applicantName, + approvalType: verificationToApprovalType(item.requestType), + userType: normalizeUserType(item.userType), + roleTags: [toTitle(item.roleLabel), toTitle(item.roleKey)].filter((v, i, arr) => v && arr.indexOf(v) === i), + primaryService: toTitle(item.roleLabel || item.roleKey || item.requestType), + area: item.area || 'Chennai', + submittedDate: item.submittedOn || '', + verificationStatus: 'VERIFIED', + assignedApprover: 'Unassigned', + priority: item.requestType === 'Portfolio Approval' || item.requestType === 'Job Approval' ? 'HIGH' : 'MEDIUM', + status: 'PENDING', + updatedAt: item.submittedOn || '', + sourceKey: `verification:${item.id}`, + submittedFields: Array.isArray(item.submittedFields) ? item.submittedFields : [], + documents: Array.isArray(item.documents) ? item.documents : [], + payload: { queueRequestType: item.requestType }, + })); +} + function StatusBadge(props: { status: string }) { const getColors = () => { switch (props.status) { @@ -112,6 +373,20 @@ export default function ApprovalManagementPage() { const [error, setError] = createSignal(''); const [isActing, setIsActing] = createSignal(false); + const selectedDocuments = createMemo(() => { + const row = viewingCase(); + if (!row) return []; + if (Array.isArray(row.documents) && row.documents.length) return row.documents; + return extractDocuments(row.payload || {}); + }); + + const selectedFields = createMemo(() => { + const row = viewingCase(); + if (!row) return []; + if (Array.isArray(row.submittedFields) && row.submittedFields.length) return row.submittedFields; + return extractSubmittedFields(row.payload || row); + }); + const load = async () => { setError(''); try { @@ -127,47 +402,112 @@ export default function ApprovalManagementPage() { }); if (!res.ok) throw new Error(`Request failed (${res.status})`); const payload = await res.json().catch(() => ({} as any)); + const jobs = Array.isArray(payload?.jobs) ? payload.jobs : []; const requirements = Array.isArray(payload?.requirements) ? payload.requirements : []; + const profiles = Array.isArray(payload?.profiles) ? payload.profiles : []; + const portfolios = Array.isArray(payload?.portfolios) ? payload.portfolios : []; const mappedJobs: ApprovalRecord[] = jobs.map((job: any) => ({ - id: String(job.id), + id: `AP-J-${String(job.id || Math.random()).slice(0, 8)}`, name: `Job Approval - ${String(job.title || 'Untitled Job')}`, - applicantName: String(job.title || 'Untitled Job'), + applicantName: String(job.company_name || job.created_by_name || job.title || 'Company Applicant'), approvalType: 'JOB', userType: 'COMPANY', roleTags: extractRoleTags(job), primaryService: String(job.category || job.department || job.role || 'Job Posting'), - area: String(job.location || job.city || job.work_mode || '—'), + area: String(job.location || job.city || job.work_mode || 'Chennai'), submittedDate: String(job.created_at || ''), verificationStatus: 'VERIFIED', assignedApprover: 'Unassigned', priority: 'HIGH', status: 'PENDING', updatedAt: String(job.updated_at || job.created_at || ''), + sourceKey: `jobs:${String(job.id || '')}`, + submittedFields: extractSubmittedFields(job), + documents: extractDocuments(job), + payload: job, })); const mappedReqs: ApprovalRecord[] = requirements.map((req: any) => ({ - id: String(req.id), + id: `AP-R-${String(req.id || Math.random()).slice(0, 8)}`, name: `Requirement Approval - ${String(req.title || 'Untitled Requirement')}`, - applicantName: String(req.title || 'Untitled Requirement'), + applicantName: String(req.created_by_name || req.customer_name || req.title || 'Service Seeker Applicant'), approvalType: 'REQUIREMENT', userType: 'CUSTOMER', roleTags: extractRoleTags(req), primaryService: String(req.category || req.profession || req.service_type || 'Requirement'), - area: String(req.location || req.city || req.area || '—'), + area: String(req.location || req.city || req.area || 'Chennai'), submittedDate: String(req.created_at || ''), verificationStatus: 'VERIFIED', assignedApprover: 'Unassigned', priority: 'MEDIUM', status: 'PENDING', updatedAt: String(req.updated_at || req.created_at || ''), + sourceKey: `requirements:${String(req.id || '')}`, + submittedFields: extractSubmittedFields(req), + documents: extractDocuments(req), + payload: req, })); - setRows([...mappedJobs, ...mappedReqs]); + const mappedProfiles: ApprovalRecord[] = profiles.map((profile: any) => ({ + id: `AP-P-${String(profile.id || profile.user_id || Math.random()).slice(0, 8)}`, + name: `Profile Approval - ${String(profile.full_name || profile.name || 'Applicant')}`, + applicantName: String(profile.full_name || profile.name || profile.user_name || 'Applicant'), + approvalType: profile.role_key === 'COMPANY' ? 'BUSINESS' : 'PROFILE', + userType: normalizeUserType(profile.role_key || profile.roleKey || profile.user_type), + roleTags: extractRoleTags(profile), + primaryService: String(profile.role_key || profile.role || profile.category || 'Profile'), + area: String(profile.area || profile.place || profile.city || 'Chennai'), + submittedDate: String(profile.submitted_at || profile.created_at || ''), + verificationStatus: 'VERIFIED', + assignedApprover: 'Unassigned', + priority: 'MEDIUM', + status: 'PENDING', + updatedAt: String(profile.updated_at || profile.created_at || ''), + sourceKey: `profiles:${String(profile.id || profile.user_id || '')}`, + submittedFields: extractSubmittedFields(profile), + documents: extractDocuments(profile), + payload: profile, + })); + + const mappedPortfolios: ApprovalRecord[] = portfolios.map((portfolio: any) => ({ + id: `AP-F-${String(portfolio.id || portfolio.user_id || Math.random()).slice(0, 8)}`, + name: `Portfolio Approval - ${String(portfolio.full_name || portfolio.name || 'Professional')}`, + applicantName: String(portfolio.full_name || portfolio.name || 'Professional'), + approvalType: 'PORTFOLIO', + userType: 'PROFESSIONAL', + roleTags: extractRoleTags(portfolio), + primaryService: String(portfolio.role_key || portfolio.role || 'Professional Portfolio'), + area: String(portfolio.area || portfolio.place || portfolio.city || 'Chennai'), + submittedDate: String(portfolio.submitted_at || portfolio.created_at || ''), + verificationStatus: 'VERIFIED', + assignedApprover: 'Unassigned', + priority: 'HIGH', + status: 'PENDING', + updatedAt: String(portfolio.updated_at || portfolio.created_at || ''), + sourceKey: `portfolios:${String(portfolio.id || portfolio.user_id || '')}`, + submittedFields: extractSubmittedFields(portfolio), + documents: extractDocuments(portfolio), + payload: portfolio, + })); + + const queueRaw = typeof window !== 'undefined' ? window.localStorage.getItem(APPROVAL_QUEUE_STORAGE_KEY) : null; + const queueParsed = queueRaw ? JSON.parse(queueRaw) : []; + const queueItems = Array.isArray(queueParsed) ? queueParsed as ApprovalQueueItem[] : []; + const mappedQueue = mapQueueItemsToApprovals(queueItems); + + const merged = [...mappedQueue, ...mappedJobs, ...mappedReqs, ...mappedProfiles, ...mappedPortfolios]; + const deduped = merged.filter((row, index, arr) => { + const key = row.sourceKey || row.id; + return arr.findIndex((candidate) => (candidate.sourceKey || candidate.id) === key) === index; + }); + + const withDemo = mergeWithFallbackRows(deduped); + setRows(withDemo.length ? withDemo : FALLBACK_APPROVAL_ROWS); } catch (e: any) { - setRows([]); - setError(e?.message || 'Could not reach approvals API.'); + setRows(FALLBACK_APPROVAL_ROWS); + setError(e?.message || 'Could not reach approvals API. Showing demo approval queue data.'); } }; @@ -242,12 +582,23 @@ export default function ApprovalManagementPage() { setOpenMenuId(null); }; + const setLocalStatus = (row: ApprovalRecord, nextStatus: ApprovalRecord['status']) => { + setRows((prev) => prev.map((item) => ((item.sourceKey || item.id) === (row.sourceKey || row.id) ? { ...item, status: nextStatus } : item))); + setViewingCase((current) => { + if (!current) return current; + return (current.sourceKey || current.id) === (row.sourceKey || row.id) ? { ...current, status: nextStatus } : current; + }); + }; + const runApprovalAction = async (row: ApprovalRecord, action: 'approve' | 'reject') => { const type = row.approvalType; + const nextStatus: ApprovalRecord['status'] = action === 'approve' ? 'APPROVED' : 'REJECTED'; + if (type !== 'JOB' && type !== 'REQUIREMENT') { - setError(`Action is not supported for approval type "${type}".`); + setLocalStatus(row, nextStatus); return; } + setIsActing(true); setError(''); try { @@ -255,8 +606,8 @@ export default function ApprovalManagementPage() { ? sessionStorage.getItem('nxtgauge_admin_access_token') || '' : ''; const endpoint = type === 'JOB' - ? `${API}/api/admin/approvals/jobs/${row.id}/${action}` - : `${API}/api/admin/approvals/requirements/${row.id}/${action}`; + ? `${API}/api/admin/approvals/jobs/${String(row.id).replace(/^AP-J-/, '')}/${action}` + : `${API}/api/admin/approvals/requirements/${String(row.id).replace(/^AP-R-/, '')}/${action}`; const res = await fetch(endpoint, { method: 'POST', headers: { @@ -286,8 +637,7 @@ export default function ApprovalManagementPage() { return (
- - {/* Page header */} +

Approval Management

Manage final approval decisions for all platform entities and requests

@@ -298,9 +648,8 @@ export default function ApprovalManagementPage() {
- {/* ── LIST VIEW ── */} -
+
{([ { key: 'all', label: 'All Approvals', action: () => { setListTab('all'); setStatusFilter('all'); } }, { key: 'escalated', label: `Escalated (${escalatedCount()})`, action: () => { setListTab('escalated'); setStatusFilter('escalated'); } }, @@ -309,7 +658,7 @@ export default function ApprovalManagementPage() { @@ -340,7 +689,7 @@ export default function ApprovalManagementPage() {
-
+
{(['overview', 'verification', 'checklist', 'logs'] as const).map((tab, i) => { const labels = ['Overview', 'Verification Summary', 'Approval Checklist', 'Activity Logs']; const active = () => detailTab() === tab; @@ -380,25 +729,24 @@ export default function ApprovalManagementPage() {
-
-

Approval Decision Path

-
-
-
- {[ - { l: 'Submitted', active: true }, - { l: 'Verified', active: true }, - { l: 'Review', active: true }, - { l: 'Decision', active: false }, - ].map((step) => ( -
-
-

{step.l}

-
- ))} + + 0}> +
+

Submitted Details

+
+ + {(field) => ( +
+

{field.label}

+

{field.value || '—'}

+
+ )} +
+
-
+
+

Decision Notes

{row[2]} {row[3]} - {row[4]} + {row[4]} {row[5]}