feat: dashboard session timer and verification UI improvements
- Add SessionTimer component with 13min warning / 15min idle auto-logout - Move VerificationSubmissionGuide from ProfilePage to MyDashboardPage - Remove duplicate VerificationSubmissionGuide from ProfilePage - Fix 'Go to My Portfolio' button to navigate properly - Change error messages to 'Service unavailable' for failed widget loads - Brand color updates for VerificationSubmissionGuide
This commit is contained in:
parent
a3cc407207
commit
eb61206810
6 changed files with 410 additions and 227 deletions
106
src/components/SessionTimer.tsx
Normal file
106
src/components/SessionTimer.tsx
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
import { Show, createSignal, onCleanup, onMount } from 'solid-js';
|
||||||
|
import { useAuth } from '~/lib/auth';
|
||||||
|
|
||||||
|
const WARNING_AT = 13 * 60 * 1000;
|
||||||
|
const LOGOUT_AT = 15 * 60 * 1000;
|
||||||
|
|
||||||
|
export default function SessionTimer() {
|
||||||
|
const auth = useAuth();
|
||||||
|
const [showWarning, setShowWarning] = createSignal(false);
|
||||||
|
let warningTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
let logoutTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
let lastActivity = Date.now();
|
||||||
|
|
||||||
|
const resetTimers = () => {
|
||||||
|
lastActivity = Date.now();
|
||||||
|
setShowWarning(false);
|
||||||
|
if (warningTimer) clearTimeout(warningTimer);
|
||||||
|
if (logoutTimer) clearTimeout(logoutTimer);
|
||||||
|
warningTimer = setTimeout(() => setShowWarning(true), WARNING_AT);
|
||||||
|
logoutTimer = setTimeout(() => auth.logout(), LOGOUT_AT);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onActivity = () => {
|
||||||
|
if (auth.isAuthenticated()) resetTimers();
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (!auth.isAuthenticated()) return;
|
||||||
|
resetTimers();
|
||||||
|
|
||||||
|
const events = ['mousedown', 'keydown', 'touchstart', 'scroll'];
|
||||||
|
for (const ev of events) {
|
||||||
|
document.addEventListener(ev, onActivity, { passive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
if (warningTimer) clearTimeout(warningTimer);
|
||||||
|
if (logoutTimer) clearTimeout(logoutTimer);
|
||||||
|
for (const ev of events) {
|
||||||
|
document.removeEventListener(ev, onActivity);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Show when={showWarning()}>
|
||||||
|
<div style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: '0',
|
||||||
|
left: '0',
|
||||||
|
right: '0',
|
||||||
|
bottom: '0',
|
||||||
|
background: 'rgba(0,0,0,0.5)',
|
||||||
|
display: 'flex',
|
||||||
|
'align-items': 'center',
|
||||||
|
'justify-content': 'center',
|
||||||
|
'z-index': '9999',
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
background: '#FFF',
|
||||||
|
'border-radius': '16px',
|
||||||
|
padding: '32px',
|
||||||
|
'max-width': '420px',
|
||||||
|
width: '90%',
|
||||||
|
'text-align': 'center',
|
||||||
|
'box-shadow': '0 20px 60px rgba(0,0,0,0.3)',
|
||||||
|
}}>
|
||||||
|
<p style={{
|
||||||
|
margin: '0 0 8px',
|
||||||
|
'font-size': '20px',
|
||||||
|
'font-weight': '800',
|
||||||
|
color: '#0D0D2A',
|
||||||
|
}}>
|
||||||
|
Session Expiring
|
||||||
|
</p>
|
||||||
|
<p style={{
|
||||||
|
margin: '0 0 24px',
|
||||||
|
'font-size': '14px',
|
||||||
|
color: '#6B7280',
|
||||||
|
'line-height': '1.6',
|
||||||
|
}}>
|
||||||
|
Your session will expire soon due to inactivity.
|
||||||
|
Click continue to stay signed in.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={resetTimers}
|
||||||
|
style={{
|
||||||
|
background: '#FF5E13',
|
||||||
|
color: '#FFF',
|
||||||
|
border: 'none',
|
||||||
|
'border-radius': '10px',
|
||||||
|
padding: '12px 32px',
|
||||||
|
'font-size': '15px',
|
||||||
|
'font-weight': '700',
|
||||||
|
cursor: 'pointer',
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Continue Session
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { For, Show, createMemo, createSignal, onMount, Switch, Match } from 'solid-js';
|
import { For, Show, createMemo, createSignal, onMount, Switch, Match } from 'solid-js';
|
||||||
import { LayoutDashboard } from 'lucide-solid';
|
import { LayoutDashboard } from 'lucide-solid';
|
||||||
import { BTN_GHOST, BTN_PRIMARY, CARD } from '~/components/DashboardShell';
|
import { BTN_GHOST, BTN_PRIMARY, CARD } from '~/components/DashboardShell';
|
||||||
import { normalizeRole, PROFESSIONAL_ROLE_SET, type RoleKey } from './RoleDashboardShared';
|
import { normalizeRole, PROFESSIONAL_ROLE_SET, ROLE_PREFIXES, type RoleKey } from './RoleDashboardShared';
|
||||||
import WalletWidget from './widgets/WalletWidget';
|
import WalletWidget from './widgets/WalletWidget';
|
||||||
import LeadsWidget from './widgets/LeadsWidget';
|
import LeadsWidget from './widgets/LeadsWidget';
|
||||||
import JobsWidget from './widgets/JobsWidget';
|
import JobsWidget from './widgets/JobsWidget';
|
||||||
|
|
@ -11,6 +11,8 @@ import ShortlistedWidget from './widgets/ShortlistedWidget';
|
||||||
import PortfolioWidget from './widgets/PortfolioWidget';
|
import PortfolioWidget from './widgets/PortfolioWidget';
|
||||||
import ProfileCompletionWidget from './widgets/ProfileCompletionWidget';
|
import ProfileCompletionWidget from './widgets/ProfileCompletionWidget';
|
||||||
import VerificationWidget from './widgets/VerificationWidget';
|
import VerificationWidget from './widgets/VerificationWidget';
|
||||||
|
import VerificationSubmissionGuide from './VerificationSubmissionGuide';
|
||||||
|
import { fetchProfile } from '~/lib/api';
|
||||||
|
|
||||||
const NAVY = '#0D0D2A';
|
const NAVY = '#0D0D2A';
|
||||||
const ORANGE = '#FF5E13';
|
const ORANGE = '#FF5E13';
|
||||||
|
|
@ -67,6 +69,7 @@ export default function MyDashboardPage(props: Props) {
|
||||||
const [widgetOrder, setWidgetOrder] = createSignal<string[]>([]);
|
const [widgetOrder, setWidgetOrder] = createSignal<string[]>([]);
|
||||||
const [draggingIdx, setDraggingIdx] = createSignal<number | null>(null);
|
const [draggingIdx, setDraggingIdx] = createSignal<number | null>(null);
|
||||||
const [visibleWidgets, setVisibleWidgets] = createSignal<Set<string>>(new Set());
|
const [visibleWidgets, setVisibleWidgets] = createSignal<Set<string>>(new Set());
|
||||||
|
const [profileData, setProfileData] = createSignal<Record<string, any>>({});
|
||||||
|
|
||||||
const getRoleType = (): string => {
|
const getRoleType = (): string => {
|
||||||
if (PROFESSIONAL_ROLE_SET.has(props.roleKey)) return 'PROFESSIONAL';
|
if (PROFESSIONAL_ROLE_SET.has(props.roleKey)) return 'PROFESSIONAL';
|
||||||
|
|
@ -90,6 +93,56 @@ export default function MyDashboardPage(props: Props) {
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
setTimeout(() => loadData(), 0);
|
setTimeout(() => loadData(), 0);
|
||||||
initWidgetOrder();
|
initWidgetOrder();
|
||||||
|
loadProfileData();
|
||||||
|
});
|
||||||
|
|
||||||
|
const loadProfileData = async () => {
|
||||||
|
const prefix = ROLE_PREFIXES[props.roleKey];
|
||||||
|
if (!prefix) return;
|
||||||
|
try {
|
||||||
|
const data = await fetchProfile(prefix);
|
||||||
|
if (data) setProfileData(data);
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
};
|
||||||
|
|
||||||
|
const missingBasicLabels = createMemo(() => {
|
||||||
|
const data = profileData();
|
||||||
|
const missing: string[] = [];
|
||||||
|
if (!data) return missing;
|
||||||
|
const p = data.profile || data;
|
||||||
|
if (!String(p?.first_name || '').trim()) missing.push('First Name');
|
||||||
|
if (!String(p?.last_name || '').trim()) missing.push('Last Name');
|
||||||
|
if (!String(p?.email || '').trim()) missing.push('Email Address');
|
||||||
|
if (!String(p?.phone || '').trim()) missing.push('Mobile Number');
|
||||||
|
if (!String(p?.address_line_1 || p?.address || '').trim()) missing.push('Address Line 1');
|
||||||
|
if (!String(p?.city || '').trim()) missing.push('City');
|
||||||
|
if (!String(p?.area || '').trim()) missing.push('Area');
|
||||||
|
if (!String(p?.state || '').trim()) missing.push('State');
|
||||||
|
return missing;
|
||||||
|
});
|
||||||
|
|
||||||
|
const missingDocLabels = createMemo(() => {
|
||||||
|
const data = profileData();
|
||||||
|
if (!data) return [];
|
||||||
|
const docs = data.documents || data.documents_data || [];
|
||||||
|
const missing: string[] = [];
|
||||||
|
if (!docs.some((d: any) => d?.doc_type === 'identity')) missing.push('Identity Proof');
|
||||||
|
if (!docs.some((d: any) => d?.doc_type === 'address')) missing.push('Address Proof');
|
||||||
|
if (!docs.some((d: any) => d?.doc_type === 'portfolio')) missing.push('Portfolio Ownership Proof');
|
||||||
|
return missing;
|
||||||
|
});
|
||||||
|
|
||||||
|
const missingPortfolioLabels = createMemo(() => {
|
||||||
|
const data = profileData();
|
||||||
|
if (!data) return ['About', 'Services & pricing', 'Experience / tools', 'FAQs', 'Showcase items'];
|
||||||
|
const p = data.portfolio || data.custom_data || {};
|
||||||
|
const missing: string[] = [];
|
||||||
|
if (!String(p?.about || p?.bio || '').trim()) missing.push('About');
|
||||||
|
if (!String(p?.services || p?.pricing || '').trim()) missing.push('Services & pricing');
|
||||||
|
if (!String(p?.experience || p?.tools || '').trim()) missing.push('Experience / tools');
|
||||||
|
if (!String(p?.faqs || '').trim()) missing.push('FAQs');
|
||||||
|
if (!String(p?.showcase || p?.portfolio_items || '').trim()) missing.push('Showcase items');
|
||||||
|
return missing.length > 0 ? missing : [];
|
||||||
});
|
});
|
||||||
|
|
||||||
const moveWidget = (fromIdx: number, toIdx: number) => {
|
const moveWidget = (fromIdx: number, toIdx: number) => {
|
||||||
|
|
@ -377,64 +430,21 @@ export default function MyDashboardPage(props: Props) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={showVerificationPrompt()}>
|
<Show when={showVerificationPrompt()}>
|
||||||
<div
|
<VerificationSubmissionGuide
|
||||||
style={{
|
statusLabel={verificationStatus().replace(/_/g, ' ')}
|
||||||
...CARD,
|
statusColor="#B7791F"
|
||||||
border: '1px solid #E5E7EB',
|
locked={verificationStatus() === 'UNDER_REVIEW' || verificationStatus() === 'PENDING'}
|
||||||
background: 'white',
|
approved={verificationStatus() === 'APPROVED'}
|
||||||
display: 'grid',
|
missingBasicLabels={missingBasicLabels()}
|
||||||
gap: '10px',
|
missingDocLabels={missingDocLabels()}
|
||||||
}}
|
missingPortfolioLabels={missingPortfolioLabels()}
|
||||||
>
|
canSubmit={missingBasicLabels().length === 0 && missingDocLabels().length === 0 && missingPortfolioLabels().length === 0}
|
||||||
<div style={{ display: 'grid', gap: '8px' }}>
|
submitting={false}
|
||||||
<span
|
onSubmit={() => {}}
|
||||||
style={{
|
onGoBasic={() => props.onNavigate?.('My Profile')}
|
||||||
display: 'inline-flex',
|
onGoDocuments={() => props.onNavigate?.('My Profile')}
|
||||||
'align-items': 'center',
|
onGoPortfolio={() => props.onNavigate?.('My Portfolio')}
|
||||||
width: 'fit-content',
|
/>
|
||||||
height: '24px',
|
|
||||||
padding: '0 10px',
|
|
||||||
'border-radius': '999px',
|
|
||||||
border: `1px solid ${verificationTone().badgeBorder}`,
|
|
||||||
background: verificationTone().badgeBackground,
|
|
||||||
color: verificationTone().badgeColor,
|
|
||||||
'font-size': '11px',
|
|
||||||
'font-weight': '700',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{verificationStatus().replace(/_/g, ' ')}
|
|
||||||
</span>
|
|
||||||
<p style={{ margin: '0', 'font-size': '15px', 'font-weight': '800', color: '#111827' }}>{verificationTone().title}</p>
|
|
||||||
<p style={{ margin: '0', 'font-size': '13px', color: '#6B7280' }}>
|
|
||||||
{verificationTone().description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div style={{ display: 'flex', gap: '8px', 'flex-wrap': 'wrap' }}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => props.onNavigate?.('My Profile')}
|
|
||||||
style={{ ...BTN_PRIMARY, height: '32px', 'font-size': '12px', padding: '0 12px' }}
|
|
||||||
>
|
|
||||||
Go to My Profile
|
|
||||||
</button>
|
|
||||||
<Show when={showPortfolioCta()}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => props.onNavigate?.('My Portfolio')}
|
|
||||||
style={{ ...BTN_GHOST, height: '32px', 'font-size': '12px', padding: '0 12px' }}
|
|
||||||
>
|
|
||||||
Go to My Portfolio
|
|
||||||
</button>
|
|
||||||
</Show>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => props.onNavigate?.('Verification')}
|
|
||||||
style={{ ...BTN_GHOST, height: '32px', 'font-size': '12px', padding: '0 12px' }}
|
|
||||||
>
|
|
||||||
Open Verification
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<div style={CARD}>
|
<div style={CARD}>
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ import {
|
||||||
LABEL,
|
LABEL,
|
||||||
BTN_PRIMARY,
|
BTN_PRIMARY,
|
||||||
} from "~/components/DashboardShell";
|
} from "~/components/DashboardShell";
|
||||||
import VerificationSubmissionGuide from "~/components/dashboard/VerificationSubmissionGuide";
|
|
||||||
|
|
||||||
const API = "/api/gateway";
|
const API = "/api/gateway";
|
||||||
|
|
||||||
|
|
@ -397,6 +396,7 @@ interface Props {
|
||||||
roleKey: string;
|
roleKey: string;
|
||||||
runtimeFields?: string[];
|
runtimeFields?: string[];
|
||||||
onVerificationStatusChange?: (status: string) => void;
|
onVerificationStatusChange?: (status: string) => void;
|
||||||
|
onNavigate?: (key: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Tab = "basic" | "documents";
|
type Tab = "basic" | "documents";
|
||||||
|
|
@ -627,22 +627,6 @@ export default function ProfilePage(props: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ "max-width": "760px" }}>
|
<div style={{ "max-width": "760px" }}>
|
||||||
<VerificationSubmissionGuide
|
|
||||||
statusLabel={statusLabel[verificationStatus()] ?? verificationStatus()}
|
|
||||||
statusColor={statusColor[verificationStatus()] ?? "#9CA3AF"}
|
|
||||||
locked={isLocked()}
|
|
||||||
approved={verificationStatus() === "APPROVED"}
|
|
||||||
docRequest={docRequest()}
|
|
||||||
missingBasicLabels={missingBasicLabels()}
|
|
||||||
missingDocLabels={missingDocLabels()}
|
|
||||||
missingPortfolioLabels={missingPortfolioLabels()}
|
|
||||||
canSubmit={canSubmitVerification()}
|
|
||||||
submitting={submitting()}
|
|
||||||
onSubmit={handleSubmitForVerification}
|
|
||||||
onGoBasic={() => setTab("basic")}
|
|
||||||
onGoDocuments={() => setTab("documents")}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Show when={submitMsg()}>
|
<Show when={submitMsg()}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { For, Show } from "solid-js";
|
import { For, Show } from "solid-js";
|
||||||
import { BTN_GHOST, BTN_PRIMARY, CARD } from "../DashboardShell";
|
import { BTN_GHOST, BTN_PRIMARY, CARD, ORANGE, NAVY } from "../DashboardShell";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
statusLabel: string;
|
statusLabel: string;
|
||||||
|
|
@ -15,176 +15,256 @@ type Props = {
|
||||||
onSubmit: () => void;
|
onSubmit: () => void;
|
||||||
onGoBasic: () => void;
|
onGoBasic: () => void;
|
||||||
onGoDocuments: () => void;
|
onGoDocuments: () => void;
|
||||||
|
onGoPortfolio: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function VerificationSubmissionGuide(props: Props) {
|
export default function VerificationSubmissionGuide(props: Props) {
|
||||||
const portfolioMissing = () => props.missingPortfolioLabels ?? [];
|
const portfolioMissing = () => props.missingPortfolioLabels ?? [];
|
||||||
const totalMissing = () => props.missingBasicLabels.length + props.missingDocLabels.length + portfolioMissing().length;
|
const totalMissing = () => props.missingBasicLabels.length + props.missingDocLabels.length + portfolioMissing().length;
|
||||||
|
|
||||||
|
const allDone = () => totalMissing() === 0;
|
||||||
|
const progress = () => {
|
||||||
|
const total = 3;
|
||||||
|
const done = [
|
||||||
|
props.missingBasicLabels.length === 0,
|
||||||
|
props.missingDocLabels.length === 0,
|
||||||
|
portfolioMissing().length === 0,
|
||||||
|
].filter(Boolean).length;
|
||||||
|
return Math.round((done / total) * 100);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ ...CARD, "margin-bottom": "16px", display: "grid", gap: "12px" }}>
|
<div style={{ ...CARD, "margin-bottom": "16px", padding: "0", overflow: "hidden" }}>
|
||||||
<div
|
<div style={{
|
||||||
style={{
|
padding: "20px 24px",
|
||||||
|
"border-bottom": "1px solid #E5E7EB",
|
||||||
|
background: "#FFFFFF",
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
"align-items": "center",
|
"align-items": "center",
|
||||||
"justify-content": "space-between",
|
"justify-content": "space-between",
|
||||||
gap: "10px",
|
gap: "16px",
|
||||||
"flex-wrap": "wrap",
|
"flex-wrap": "wrap",
|
||||||
}}
|
}}>
|
||||||
>
|
<div style={{ display: "flex", "align-items": "center", gap: "14px" }}>
|
||||||
<div style={{ display: "flex", "align-items": "center", gap: "10px" }}>
|
<div style={{
|
||||||
<span
|
position: "relative",
|
||||||
style={{
|
width: "44px",
|
||||||
display: "inline-flex",
|
height: "44px",
|
||||||
"align-items": "center",
|
}}>
|
||||||
height: "24px",
|
<svg viewBox="0 0 36 36" style={{ width: "44px", height: "44px", transform: "rotate(-90deg)" }}>
|
||||||
padding: "0 10px",
|
<circle cx="18" cy="18" r="15" fill="none" stroke="#E5E7EB" stroke-width="2.5" />
|
||||||
"border-radius": "999px",
|
<circle
|
||||||
border: "1px solid #E5E7EB",
|
cx="18" cy="18" r="15" fill="none"
|
||||||
background: "#F9FAFB",
|
stroke={NAVY}
|
||||||
color: "#4B5563",
|
stroke-width="2.5"
|
||||||
"font-size": "11px",
|
stroke-dasharray={`${progress()} 100`}
|
||||||
"font-weight": "700",
|
stroke-linecap="round"
|
||||||
}}
|
/>
|
||||||
>
|
</svg>
|
||||||
{props.statusLabel}
|
<span style={{
|
||||||
</span>
|
position: "absolute",
|
||||||
<span style={{ "font-size": "13px", color: "#6B7280" }}>
|
inset: "0",
|
||||||
{props.approved
|
display: "flex",
|
||||||
? "Your profile is approved."
|
"align-items": "center",
|
||||||
: props.locked
|
"justify-content": "center",
|
||||||
? "Verification in progress."
|
"font-size": "12px",
|
||||||
: totalMissing() === 0
|
"font-weight": "700",
|
||||||
? "Ready to submit for verification."
|
color: NAVY,
|
||||||
: `${totalMissing()} item${totalMissing() > 1 ? "s" : ""} left before submission.`}
|
}}>
|
||||||
</span>
|
{progress()}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p style={{
|
||||||
|
margin: "0 0 2px",
|
||||||
|
"font-size": "14px",
|
||||||
|
"font-weight": "600",
|
||||||
|
color: NAVY,
|
||||||
|
}}>
|
||||||
|
{props.approved
|
||||||
|
? "Profile Approved"
|
||||||
|
: props.locked
|
||||||
|
? "Verification In Progress"
|
||||||
|
: allDone()
|
||||||
|
? "Ready to Submit"
|
||||||
|
: `${totalMissing()} Item${totalMissing() > 1 ? "s" : ""} Left`}
|
||||||
|
</p>
|
||||||
|
<p style={{
|
||||||
|
margin: "0",
|
||||||
|
"font-size": "12px",
|
||||||
|
color: "#6B7280",
|
||||||
|
}}>
|
||||||
|
{props.approved
|
||||||
|
? "Your profile is approved"
|
||||||
|
: props.locked
|
||||||
|
? "Verification in progress"
|
||||||
|
: allDone()
|
||||||
|
? "Submit for verification"
|
||||||
|
: "Complete the steps below to submit"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Show when={!props.locked && !props.approved}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={props.onSubmit}
|
||||||
|
disabled={!props.canSubmit || props.submitting}
|
||||||
|
style={{
|
||||||
|
...BTN_PRIMARY,
|
||||||
|
background: ORANGE,
|
||||||
|
color: "#FFFFFF",
|
||||||
|
border: "none",
|
||||||
|
opacity: !props.canSubmit || props.submitting ? "0.5" : "1",
|
||||||
|
cursor: !props.canSubmit || props.submitting ? "not-allowed" : "pointer",
|
||||||
|
padding: "10px 20px",
|
||||||
|
"font-weight": "600",
|
||||||
|
"border-radius": "8px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.submitting ? "Submitting..." : "Submit for Verification"}
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<Show when={!props.locked && !props.approved}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={props.onSubmit}
|
|
||||||
disabled={!props.canSubmit || props.submitting}
|
|
||||||
style={{
|
|
||||||
...BTN_PRIMARY,
|
|
||||||
background: "#0D0D2A",
|
|
||||||
color: "#FFFFFF",
|
|
||||||
border: "none",
|
|
||||||
opacity: "1",
|
|
||||||
cursor: !props.canSubmit || props.submitting ? "not-allowed" : "pointer",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{props.submitting ? "Submitting..." : "Submit for Verification"}
|
|
||||||
</button>
|
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={props.docRequest}>
|
<Show when={props.docRequest}>
|
||||||
<div
|
<div style={{
|
||||||
style={{
|
margin: "16px 24px",
|
||||||
border: "1px solid #FED7AA",
|
background: "#FEF3C7",
|
||||||
background: "#FFF7ED",
|
padding: "12px 16px",
|
||||||
padding: "10px 12px",
|
"border-radius": "8px",
|
||||||
"border-radius": "10px",
|
"font-size": "13px",
|
||||||
"font-size": "12px",
|
color: "#92400E",
|
||||||
color: "#9A3412",
|
}}>
|
||||||
}}
|
|
||||||
>
|
|
||||||
<strong>Admin request:</strong> {props.docRequest}
|
<strong>Admin request:</strong> {props.docRequest}
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<div style={{ display: "grid", gap: "8px" }}>
|
<div style={{ padding: "16px 24px 20px", display: "grid", gap: "10px" }}>
|
||||||
<div
|
<ChecklistItem
|
||||||
style={{
|
number={1}
|
||||||
border: "1px solid #E5E7EB",
|
title="Complete required basic profile fields"
|
||||||
"border-radius": "10px",
|
done={props.missingBasicLabels.length === 0}
|
||||||
padding: "10px 12px",
|
missingLabels={props.missingBasicLabels}
|
||||||
background: "#FFFFFF",
|
onGo={props.onGoBasic}
|
||||||
}}
|
buttonText="Go to Basic Information"
|
||||||
>
|
/>
|
||||||
<p style={{ margin: "0", "font-size": "12px", "font-weight": "700", color: "#111827" }}>
|
<ChecklistItem
|
||||||
1. Complete required basic profile fields
|
number={2}
|
||||||
</p>
|
title="Upload clear required documents"
|
||||||
<Show
|
done={props.missingDocLabels.length === 0}
|
||||||
when={props.missingBasicLabels.length > 0}
|
missingLabels={props.missingDocLabels}
|
||||||
fallback={<p style={{ margin: "4px 0 0", "font-size": "12px", color: "#6B7280" }}>Done.</p>}
|
onGo={props.onGoDocuments}
|
||||||
>
|
buttonText="Go to Documents"
|
||||||
<p style={{ margin: "4px 0 0", "font-size": "12px", color: "#6B7280" }}>
|
/>
|
||||||
Missing: {props.missingBasicLabels.join(", ")}
|
<ChecklistItem
|
||||||
</p>
|
number={3}
|
||||||
<button type="button" onClick={props.onGoBasic} style={{ ...BTN_GHOST, height: "30px", "margin-top": "8px", "font-size": "12px" }}>
|
title="Add portfolio showcase items"
|
||||||
Go to Basic Information
|
done={portfolioMissing().length === 0}
|
||||||
</button>
|
missingLabels={portfolioMissing()}
|
||||||
</Show>
|
onGo={props.onGoPortfolio}
|
||||||
</div>
|
buttonText="Go to My Portfolio"
|
||||||
|
/>
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
border: "1px solid #E5E7EB",
|
|
||||||
"border-radius": "10px",
|
|
||||||
padding: "10px 12px",
|
|
||||||
background: "#FFFFFF",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<p style={{ margin: "0", "font-size": "12px", "font-weight": "700", color: "#111827" }}>
|
|
||||||
2. Upload clear required documents
|
|
||||||
</p>
|
|
||||||
<Show
|
|
||||||
when={props.missingDocLabels.length > 0}
|
|
||||||
fallback={<p style={{ margin: "4px 0 0", "font-size": "12px", color: "#6B7280" }}>Done.</p>}
|
|
||||||
>
|
|
||||||
<div style={{ display: "flex", gap: "6px", "flex-wrap": "wrap", "margin-top": "6px" }}>
|
|
||||||
<For each={props.missingDocLabels}>
|
|
||||||
{(item) => (
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
height: "22px",
|
|
||||||
display: "inline-flex",
|
|
||||||
"align-items": "center",
|
|
||||||
padding: "0 8px",
|
|
||||||
"border-radius": "999px",
|
|
||||||
border: "1px solid #E5E7EB",
|
|
||||||
background: "#F9FAFB",
|
|
||||||
"font-size": "11px",
|
|
||||||
color: "#6B7280",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{item}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
<button type="button" onClick={props.onGoDocuments} style={{ ...BTN_GHOST, height: "30px", "margin-top": "8px", "font-size": "12px" }}>
|
|
||||||
Go to Documents
|
|
||||||
</button>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
border: "1px solid #E5E7EB",
|
|
||||||
"border-radius": "10px",
|
|
||||||
padding: "10px 12px",
|
|
||||||
background: "#FFFFFF",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<p style={{ margin: "0", "font-size": "12px", "font-weight": "700", color: "#111827" }}>
|
|
||||||
3. Add portfolio showcase items
|
|
||||||
</p>
|
|
||||||
<Show
|
|
||||||
when={portfolioMissing().length > 0}
|
|
||||||
fallback={<p style={{ margin: "4px 0 0", "font-size": "12px", color: "#6B7280" }}>Done.</p>}
|
|
||||||
>
|
|
||||||
<p style={{ margin: "4px 0 0", "font-size": "12px", color: "#6B7280" }}>
|
|
||||||
Missing: {portfolioMissing().join(", ")}. Go to <strong>My Portfolio</strong> and complete these sections.
|
|
||||||
</p>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p style={{ margin: "0", "font-size": "12px", color: "#6B7280" }}>
|
<div style={{
|
||||||
Document tips: use full-page, readable scans, and match name/address exactly with your profile details.
|
padding: "12px 24px",
|
||||||
</p>
|
background: "#F9FAFB",
|
||||||
|
"border-top": "1px solid #E5E7EB",
|
||||||
|
}}>
|
||||||
|
<p style={{ margin: "0", "font-size": "12px", color: "#6B7280" }}>
|
||||||
|
💡 Use full-page, readable scans. Match name and address exactly with your profile details.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ChecklistItem(props: {
|
||||||
|
number: number;
|
||||||
|
title: string;
|
||||||
|
done: boolean;
|
||||||
|
missingLabels: string[];
|
||||||
|
onGo?: () => void;
|
||||||
|
buttonText?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
display: "flex",
|
||||||
|
gap: "12px",
|
||||||
|
padding: "12px 14px",
|
||||||
|
background: "#FFFFFF",
|
||||||
|
border: "1px solid #E5E7EB",
|
||||||
|
"border-radius": "8px",
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
width: "24px",
|
||||||
|
height: "24px",
|
||||||
|
"border-radius": "50%",
|
||||||
|
background: props.done ? NAVY : "#E5E7EB",
|
||||||
|
color: props.done ? "#fff" : "#9CA3AF",
|
||||||
|
display: "flex",
|
||||||
|
"align-items": "center",
|
||||||
|
"justify-content": "center",
|
||||||
|
"font-size": "12px",
|
||||||
|
"font-weight": "700",
|
||||||
|
"flex-shrink": "0",
|
||||||
|
}}>
|
||||||
|
<Show when={props.done} fallback={props.number}>✓</Show>
|
||||||
|
</div>
|
||||||
|
<div style={{ flex: "1", "min-width": "0" }}>
|
||||||
|
<p style={{ margin: "0", "font-size": "13px", "font-weight": "500", color: "#111827" }}>
|
||||||
|
{props.title}
|
||||||
|
</p>
|
||||||
|
<Show
|
||||||
|
when={props.missingLabels.length > 0}
|
||||||
|
fallback={
|
||||||
|
<p style={{ margin: "2px 0 0", "font-size": "12px", color: NAVY, "font-weight": "500" }}>
|
||||||
|
✓ Complete
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div style={{ display: "flex", "flex-wrap": "wrap", gap: "6px", "margin-top": "6px" }}>
|
||||||
|
<For each={props.missingLabels}>
|
||||||
|
{(label) => (
|
||||||
|
<span style={{
|
||||||
|
display: "inline-flex",
|
||||||
|
"align-items": "center",
|
||||||
|
padding: "2px 8px",
|
||||||
|
"border-radius": "4px",
|
||||||
|
background: "#F3F4F6",
|
||||||
|
color: "#374151",
|
||||||
|
"font-size": "11px",
|
||||||
|
}}>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
<Show when={props.onGo}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={props.onGo}
|
||||||
|
style={{
|
||||||
|
...BTN_GHOST,
|
||||||
|
height: "28px",
|
||||||
|
"font-size": "11px",
|
||||||
|
"font-weight": "500",
|
||||||
|
color: ORANGE,
|
||||||
|
background: "#FFF7ED",
|
||||||
|
border: "none",
|
||||||
|
padding: "0 10px",
|
||||||
|
"border-radius": "4px",
|
||||||
|
"margin-top": "8px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.buttonText} →
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -109,7 +109,7 @@ export default function LeadsWidget(props: Props) {
|
||||||
<DashboardWidget
|
<DashboardWidget
|
||||||
title={isProfessional() ? 'My Lead Requests' : 'My Requirements'}
|
title={isProfessional() ? 'My Lead Requests' : 'My Requirements'}
|
||||||
loading={data.loading}
|
loading={data.loading}
|
||||||
error={data.error ? 'Failed to load' : undefined}
|
error={data.error ? 'Service unavailable' : undefined}
|
||||||
icon={<MapPin size={16} />}
|
icon={<MapPin size={16} />}
|
||||||
>
|
>
|
||||||
<Show when={stats()}>
|
<Show when={stats()}>
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ import ExploreServicesPage from "~/components/dashboard/ExploreServicesPage";
|
||||||
import HelpCenterDashboardPage from "~/components/dashboard/HelpCenterDashboardPage";
|
import HelpCenterDashboardPage from "~/components/dashboard/HelpCenterDashboardPage";
|
||||||
import SwitchServicesPage from "~/components/dashboard/SwitchServicesPage";
|
import SwitchServicesPage from "~/components/dashboard/SwitchServicesPage";
|
||||||
import LogoutPage from "~/components/dashboard/LogoutPage";
|
import LogoutPage from "~/components/dashboard/LogoutPage";
|
||||||
|
import SessionTimer from "~/components/SessionTimer";
|
||||||
import { PROFESSIONAL_ROLE_SET } from "~/components/dashboard/RoleDashboardShared";
|
import { PROFESSIONAL_ROLE_SET } from "~/components/dashboard/RoleDashboardShared";
|
||||||
|
|
||||||
// Sidebar items that have real data implementations (wired to backend APIs)
|
// Sidebar items that have real data implementations (wired to backend APIs)
|
||||||
|
|
@ -788,6 +789,7 @@ export default function RuntimeDashboardPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RequireAuth>
|
<RequireAuth>
|
||||||
|
<SessionTimer />
|
||||||
<main style={{ "min-height": "100vh", background: "#F3F4F6" }}>
|
<main style={{ "min-height": "100vh", background: "#F3F4F6" }}>
|
||||||
<Show when={loading()}>
|
<Show when={loading()}>
|
||||||
<div style={cardStyle}>Loading dashboard…</div>
|
<div style={cardStyle}>Loading dashboard…</div>
|
||||||
|
|
@ -822,6 +824,7 @@ export default function RuntimeDashboardPage() {
|
||||||
roleKey={role()}
|
roleKey={role()}
|
||||||
runtimeFields={bundle()?.fields || []}
|
runtimeFields={bundle()?.fields || []}
|
||||||
onVerificationStatusChange={(status) => setVerificationStatusOverride(String(status || "").toUpperCase())}
|
onVerificationStatusChange={(status) => setVerificationStatusOverride(String(status || "").toUpperCase())}
|
||||||
|
onNavigate={setActiveSidebar}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={activeSidebarKey() === "my portfolio"}>
|
<Match when={activeSidebarKey() === "my portfolio"}>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue