diff --git a/src/components/admin/DashboardDesignPreview.tsx b/src/components/admin/DashboardDesignPreview.tsx index a022530..2832ce1 100644 --- a/src/components/admin/DashboardDesignPreview.tsx +++ b/src/components/admin/DashboardDesignPreview.tsx @@ -1,4 +1,4 @@ -import { For, Show, createEffect, createMemo, createSignal } from 'solid-js'; +import { For, Show, createEffect, createMemo, createResource, createSignal } from 'solid-js'; import { Award, Bell, @@ -539,6 +539,8 @@ function portfolioMediaConfig(roleKey: string): { || normalized === 'GRAPHIC_DESIGNER' || normalized === 'SOCIAL_MEDIA_MANAGER' || normalized === 'CATERING_SERVICES' + || normalized === 'DEVELOPER' + || normalized === 'FITNESS_TRAINER' ) { return { mode: 'visual', @@ -576,6 +578,10 @@ function customerViewFor(sidebar: string, roleKey: string): CustomerView { return { title: 'Received Professional Responses', subtitle: 'Review and manage professional applications for active requirements.', tabs: ['all responses', 'new', 'shortlisted', 'rejected'] }; } if (key === 'shortlisted responses') return { title: 'Shortlisted Responses', subtitle: 'Focus on high-intent responses and convert them to confirmed engagements.', tabs: ['all shortlisted', 'interview scheduled', 'finalized'] }; + if (key === 'applications') return { title: 'Applications', subtitle: 'Review all candidate applications received for your active job postings.', tabs: ['all applications', 'shortlisted', 'under review', 'rejected'], cta: 'View Job Postings' }; + if (key === 'shortlisted candidates') return { title: 'Shortlisted Candidates', subtitle: 'Manage candidates you have shortlisted across all job postings.', tabs: ['shortlisted', 'interview scheduled', 'offer extended'] }; + if (key === 'my applications') return { title: 'My Applications', subtitle: 'Track the status of all jobs you have applied to.', tabs: ['all', 'under review', 'shortlisted', 'rejected'], cta: 'Browse Jobs' }; + if (key === 'saved jobs') return { title: 'Saved Jobs', subtitle: 'Jobs you have bookmarked. Apply before they expire.', tabs: ['saved', 'expiring soon'], cta: 'Browse Jobs' }; if (key.includes('profile')) { const spec = profileSpecForRole(roleKey); return { title: spec.title, subtitle: spec.subtitle, tabs: spec.tabs, cta: 'Save Changes' }; @@ -1046,6 +1052,7 @@ export default function DashboardDesignPreview(props: { exploreRoles?: Array<{ key: string; name: string }>; onOpenFullscreen?: () => void; hidePreviewHeader?: boolean; + liveData?: { userName: string; userId: string; rolePrefix: string }; }) { const isProfessionalRoleKey = (roleKey: string) => { const role = normalizeRoleKey(roleKey); @@ -1129,6 +1136,9 @@ export default function DashboardDesignPreview(props: { const [ticketMessage, setTicketMessage] = createSignal(''); const [createTicketFiles, setCreateTicketFiles] = createSignal([]); const [viewTicketFiles, setViewTicketFiles] = createSignal([]); + const [profileFormData, setProfileFormData] = createSignal>({}); + const [profileSaving, setProfileSaving] = createSignal(false); + const [profileSaveStatus, setProfileSaveStatus] = createSignal<'idle' | 'saved' | 'error'>('idle'); const [profileSettingsTab, setProfileSettingsTab] = createSignal<'change_password' | 'notifications' | 'privacy'>('change_password'); const [showDeleteAccountModal, setShowDeleteAccountModal] = createSignal(false); const [portfolioEditMode, setPortfolioEditMode] = createSignal(false); @@ -1234,6 +1244,19 @@ export default function DashboardDesignPreview(props: { const submitRequirementForReview = () => { const roleKey = selectedRequirementRole(); const roleLabel = titleCase(roleKey.replace(/_/g, ' ').toLowerCase()); + if (hasLive()) { + apiPost('/api/customers/requirements', { + profession_key: roleKey, + title: `${roleLabel} Requirement`, + description: 'Submitted via dashboard', + location: 'Chennai', + budget_min: 10000000, + budget_max: 20000000, + }).then((res: any) => res?.json?.().then((r: any) => { + // After creating, submit for approval + if (r?.id) apiPost(`/api/customers/requirements/${r.id}/submit`, {}); + })).then(() => refetchRequirementsLive()); + } const id = `#REQ-${Math.floor(9200 + Math.random() * 899)}`; const submission = new Date().toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' }); const newRequirement: RequirementRow = { @@ -1294,6 +1317,189 @@ export default function DashboardDesignPreview(props: { if (state === 'REJECTED') return { border: '#E5E7EB', bg: '#F9FAFB', text: '#374151', label: 'Rejected' }; return { border: '#E5E7EB', bg: '#F9FAFB', text: '#374151', label: 'Draft' }; }; + + // ─── Live API integration (customer_external mode with liveData) ────────── + const hasLive = () => isCustomerExternalMode() && !!props.liveData; + const livePrefix = () => props.liveData?.rolePrefix ?? ''; + const GW = '/api/gateway'; + const apiFetch = (path: string) => + fetch(`${GW}${path}`, { credentials: 'include' }) + .then((r) => (r.ok ? r.json() : null)) + .catch(() => null); + const apiPost = (path: string, body: unknown) => + fetch(`${GW}${path}`, { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }).catch(() => null); + const apiDelete = (path: string) => + fetch(`${GW}${path}`, { method: 'DELETE', credentials: 'include' }).catch(() => null); + + // Credits balance + const [creditsResource] = createResource( + () => (hasLive() ? livePrefix() : null), + (prefix) => apiFetch(`/api/${prefix}/wallet/balance`), + ); + // Marketplace requirements (professionals) + const [marketplaceResource] = createResource( + () => (hasLive() && isProfessionalRole() ? livePrefix() : null), + (prefix) => apiFetch(`/api/${prefix}/marketplace?limit=50`), + ); + // My lead requests (professionals) + const [leadRequestsResource, { refetch: refetchLeadRequestsLive }] = createResource( + () => (hasLive() && isProfessionalRole() ? livePrefix() : null), + (prefix) => apiFetch(`/api/${prefix}/leads/requests/me?limit=50`), + ); + // Customer requirements + const [requirementsResource, { refetch: refetchRequirementsLive }] = createResource( + () => (hasLive() && normalizeRoleKey(props.roleKey ?? '') === 'CUSTOMER' ? 'yes' : null), + () => apiFetch('/api/customers/requirements?limit=50'), + ); + // Jobs board + const [jobsResource] = createResource( + () => { + const r = normalizeRoleKey(props.roleKey ?? ''); + return hasLive() && (r === 'JOB_SEEKER' || r === 'COMPANY') ? r : null; + }, + () => apiFetch('/api/jobs?limit=50'), + ); + // User profile (all roles) + const [profileResource] = createResource( + () => (hasLive() ? livePrefix() : null), + (prefix) => apiFetch(`/api/${prefix}/profile/me`), + ); + + // Sync resources → local signals + createEffect(() => { + const d = creditsResource(); + if (d != null && typeof d.balance === 'number') setLeadCredits(d.balance); + }); + createEffect(() => { + const d = marketplaceResource(); + if (!d) return; + const items: any[] = Array.isArray(d.items) ? d.items : Array.isArray(d) ? d : []; + if (!items.length) return; + setLeadCards(items.map((item: any) => ({ + id: String(item.id ?? item.requirement_id ?? ''), + title: String(item.title ?? item.description ?? 'Requirement'), + category: String(item.category ?? item.profession_key ?? props.roleKey ?? ''), + location: String(item.location ?? item.city ?? 'India'), + area: String(item.area ?? item.locality ?? ''), + dateRequired: item.required_by + ? new Date(item.required_by).toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' }) + : 'TBD', + urgency: item.urgency === 'HIGH' ? 'High' : item.urgency === 'MEDIUM' ? 'Medium' : 'Low', + budget: item.budget_min != null + ? `₹${Math.round(item.budget_min / 100).toLocaleString('en-IN')} - ₹${Math.round((item.budget_max ?? item.budget_min) / 100).toLocaleString('en-IN')}` + : '₹0', + budgetValue: Number(item.budget_max ?? item.budget_min ?? 0) / 100, + priceRange: item.budget_min != null + ? `₹${Math.round(item.budget_min / 100).toLocaleString('en-IN')} - ₹${Math.round((item.budget_max ?? item.budget_min) / 100).toLocaleString('en-IN')}` + : '₹0', + cost: 25, + status: 'open' as const, + match: '80% match', + contactCount: Number(item.contact_count ?? 0), + maxContacts: Number(item.max_contacts ?? 10), + }))); + }); + createEffect(() => { + const d = leadRequestsResource(); + if (!d) return; + const items: any[] = Array.isArray(d.items) ? d.items : Array.isArray(d) ? d : []; + if (!items.length) return; + const statusMap: Record = { + PENDING: 'request_sent', CONTACT_UNLOCKED: 'contact_unlocked', + REJECTED: 'rejected', CANCELLED: 'cancelled_by_professional', EXPIRED: 'expired_refunded', + }; + setLeadRequestRows(items.map((item: any) => ({ + id: String(item.id ?? ''), + title: String(item.title ?? item.requirement_title ?? 'Lead Request'), + city: String(item.location ?? item.city ?? 'India'), + requestDate: item.created_at + ? new Date(item.created_at).toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' }) + : '--', + status: statusMap[String(item.status ?? '')] ?? 'request_sent', + decisionDate: item.decision_date + ? new Date(item.decision_date).toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' }) + : '--', + }))); + }); + createEffect(() => { + const d = requirementsResource(); + if (!d) return; + const items: any[] = Array.isArray(d.items) ? d.items : Array.isArray(d) ? d : []; + if (!items.length) return; + const statusMap: Record = { + PENDING: 'under review', APPROVED: 'approved', ACTIVE: 'active', + REJECTED: 'closed', CLOSED: 'closed', + }; + setRequirementRows(items.map((item: any) => ({ + id: String(item.id ?? ''), + title: String(item.title ?? item.description ?? 'Requirement'), + summary: String(item.description ?? ''), + category: String(item.category ?? item.profession_key ?? ''), + budget: item.budget_min != null + ? `₹${Math.round(item.budget_min / 100).toLocaleString('en-IN')} - ₹${Math.round((item.budget_max ?? item.budget_min) / 100).toLocaleString('en-IN')}` + : '₹0', + location: String(item.location ?? 'India'), + submission: item.created_at + ? new Date(item.created_at).toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' }) + : '--', + status: statusMap[String(item.status ?? '')] ?? 'under review', + responseTag: `Active (${Number(item.response_count ?? 0)} Responses)`, + }))); + }); + createEffect(() => { + const d = jobsResource(); + if (!d) return; + const items: any[] = Array.isArray(d.items) ? d.items : Array.isArray(d) ? d : []; + if (!items.length) return; + setJobBoardJobs(items.map((item: any) => ({ + id: String(item.id ?? ''), + title: String(item.title ?? 'Position'), + company: String(item.company_name ?? item.company ?? 'Company'), + location: String(item.location ?? 'India'), + salary: item.salary_min != null + ? `₹${Math.round(item.salary_min / 100).toLocaleString('en-IN')}+` + : 'Negotiable', + exp: String(item.experience_required ?? item.experience ?? '0-2 yrs'), + type: String(item.employment_type ?? item.type ?? 'Full-Time'), + tags: Array.isArray(item.tags) ? item.tags : [], + match: '', + posted: item.created_at + ? new Date(item.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) + : '--', + }))); + }); + createEffect(() => { + const d = profileResource(); + if (!d) return; + const parts = String(d.display_name || d.full_name || d.business_name || '').split(' '); + const map: Record = {}; + map['First Name'] = d.first_name || parts[0] || ''; + map['Last Name'] = d.last_name || parts.slice(1).join(' ') || ''; + map['Business Name'] = d.business_name || d.display_name || ''; + map['Contact Person Name'] = d.contact_person || d.display_name || ''; + map['Company Name'] = d.company_name || d.display_name || ''; + map['Email Address'] = d.email || ''; + map['Mobile Number'] = d.phone || d.mobile || ''; + if (d.location) map['City'] = d.location; + if (d.area) map['Area'] = d.area; + if (d.state) map['State'] = d.state; + if (d.pin_code) map['PIN Code'] = d.pin_code; + if (d.bio) map['Bio'] = d.bio; + if (d.status) setProfileApprovalState( + d.status === 'APPROVED' ? 'APPROVED' + : d.status === 'REJECTED' ? 'REJECTED' + : d.status === 'PENDING' ? 'IN_REVIEW' + : 'DRAFT', + ); + setProfileFormData((prev) => ({ ...prev, ...map })); + }); + // ─── End live API integration ───────────────────────────────────────────── + const submitProfileForApproval = () => { setProfileApprovalState('SUBMITTED'); setTimeout(() => setProfileApprovalState('IN_REVIEW'), 250); @@ -1568,6 +1774,10 @@ export default function DashboardDesignPreview(props: { const requestLeadContact = (leadId: string) => { if (usableLeadCredits() < leadCostPerContact) return; + if (hasLive()) { + apiPost(`/api/${livePrefix()}/leads/request`, { requirement_id: leadId }) + .then(() => refetchLeadRequestsLive()); + } let changed = false; setLeadCards((prev) => prev.map((card) => { if (card.id !== leadId || card.status !== 'open' || card.contactCount >= card.maxContacts) return card; @@ -1643,6 +1853,10 @@ export default function DashboardDesignPreview(props: { }; const cancelLeadRequest = (leadId: string) => { + if (hasLive()) { + apiDelete(`/api/${livePrefix()}/leads/requests/${leadId}`) + .then(() => refetchLeadRequestsLive()); + } setLeadCards((prev) => prev.map((card) => { if (card.id !== leadId) return card; if (card.status !== 'requested' && card.status !== 'closed') return card; @@ -2613,7 +2827,7 @@ export default function DashboardDesignPreview(props: {
-

Welcome back, Alex

+

Welcome back, {props.liveData?.userName ?? 'Alex'}

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

@@ -2629,7 +2843,7 @@ export default function DashboardDesignPreview(props: {

Credits Balance

-

2,500

+

{leadCredits().toLocaleString('en-IN')}

@@ -2708,8 +2922,9 @@ export default function DashboardDesignPreview(props: {
!isCityField && setProfileFormData((prev) => ({ ...prev, [field]: e.currentTarget.value }))} placeholder={placeholderText} style="width:100%;height:36px;border:1px solid #E5E7EB;border-radius:8px;background:white;padding:0 30px 0 10px;font-size:12px;color:#111827;outline:none" /> @@ -2909,8 +3124,38 @@ export default function DashboardDesignPreview(props: {
- - + +
diff --git a/src/routes/dashboard.tsx b/src/routes/dashboard.tsx index adcd677..242ce0d 100644 --- a/src/routes/dashboard.tsx +++ b/src/routes/dashboard.tsx @@ -58,9 +58,43 @@ const ROLE_BASED_SIDEBAR: Record = { CATERING_SERVICES: ['My Dashboard', 'My Profile', 'My Portfolio', 'Leads', 'My Responses', 'Credits', 'Explore Nxtgauge', 'Verification', 'Help Center', 'Settings', 'Switch Services', 'Logout'], COMPANY: ['My Dashboard', 'My Profile', 'Jobs', 'Applications', 'Shortlisted Candidates', 'Credits', 'Explore Nxtgauge', 'Verification', 'Help Center', 'Settings', 'Switch Services', 'Logout'], JOB_SEEKER: ['My Dashboard', 'My Profile', 'Jobs', 'My Applications', 'Saved Jobs', 'Explore Nxtgauge', 'Verification', 'Help Center', 'Settings', 'Switch Services', 'Logout'], - CUSTOMER: ['My Dashboard', 'My Profile', 'My Requirements', 'Received Responses', 'Shortlisted Responses', 'Credits', 'Explore Nxtgauge', 'Verification', 'Help Center', 'Switch Services', 'Logout'], + CUSTOMER: ['My Dashboard', 'My Profile', 'My Requirements', 'Received Responses', 'Shortlisted Responses', 'Credits', 'Explore Nxtgauge', 'Verification', 'Help Center', 'Settings', 'Switch Services', 'Logout'], }; +const ROLE_PREFIXES: Record = { + PHOTOGRAPHER: 'photographers', + MAKEUP_ARTIST: 'makeup-artists', + TUTOR: 'tutors', + DEVELOPER: 'developers', + VIDEO_EDITOR: 'video-editors', + GRAPHIC_DESIGNER: 'graphic-designers', + SOCIAL_MEDIA_MANAGER: 'social-media-managers', + FITNESS_TRAINER: 'fitness-trainers', + CATERING_SERVICES: 'catering-services', + UGC_CONTENT_CREATOR: 'ugc-content-creators', + CUSTOMER: 'customers', + COMPANY: 'companies', + JOB_SEEKER: 'jobseeker', +}; + +function getNameFromStorage(): string { + if (typeof window === 'undefined') return 'User'; + const keys = ['nxtgauge_signup_profile_v1', 'nxtgauge_auth_user', 'nxtgauge_user']; + for (const key of keys) { + try { + const raw = window.localStorage.getItem(key) || window.sessionStorage.getItem(key); + if (!raw) continue; + const parsed = JSON.parse(raw); + const name = parsed?.name + || parsed?.first_name + || parsed?.user?.name + || parsed?.user?.first_name; + if (name) return String(name); + } catch { /* ignore */ } + } + return 'User'; +} + const EXPLORE_ROLES = [ { key: 'PHOTOGRAPHER', name: 'Photographer' }, { key: 'MAKEUP_ARTIST', name: 'Makeup Artist' }, @@ -182,10 +216,25 @@ export default function RuntimeDashboardPage() { const [role, setRole] = createSignal('JOB_SEEKER'); const [activeSidebar, setActiveSidebar] = createSignal('My Dashboard'); const [activeTab, setActiveTab] = createSignal('overview'); + const [userName, setUserName] = createSignal('User'); + const [userId, setUserId] = createSignal(''); onMount(() => { setHydrated(true); setRole(getInitialRoleFromStorage()); + setUserName(getNameFromStorage()); + // Fetch fresh session data + fetchJson('/api/auth/session').then((data) => { + if (!data) return; + const name = data.full_name + || data.name + || (data.first_name ? `${data.first_name} ${data.last_name ?? ''}`.trim() : '') + || data.email?.split('@')[0] + || ''; + if (name) setUserName(name); + if (data.id || data.user_id) setUserId(String(data.id || data.user_id)); + if (data.active_role) setRole(normalizeRole(data.active_role)); + }); }); const [bundle] = createResource( @@ -216,6 +265,12 @@ export default function RuntimeDashboardPage() { const loading = createMemo(() => !hydrated() || bundle.loading); const ready = createMemo(() => hydrated() && !bundle.loading); + const liveData = createMemo(() => { + const prefix = ROLE_PREFIXES[role()]; + if (!prefix) return undefined; + return { userName: userName(), userId: userId(), rolePrefix: prefix }; + }); + return (
@@ -224,18 +279,9 @@ export default function RuntimeDashboardPage() {
Loading dashboard...
- -
-

Dashboard Unavailable

-

- Dashboard configuration was not found for your role. Please contact support. -

-
-
- - +