// Primus IQ — Auth: Login + Onboarding (v1)
// Self-contained: no dependency on other components. All styling via CSS tokens.
// ── TAXONOMY ──────────────────────────────────────────────────────────────────
const TAXONOMY = {
designations: [
{ value: "analyst", label: "Analyst" },
{ value: "consultant", label: "Consultant" },
{ value: "sr. consultant", label: "Sr. Consultant" },
{ value: "software engineer", label: "Software Engineer" },
{ value: "manager", label: "Manager" },
{ value: "avp", label: "AVP" },
{ value: "senior software engineer", label: "Senior Software Engineer" },
{ value: "vp", label: "VP" },
{ value: "ed", label: "ED" },
{ value: "md/ceo", label: "MD/CEO" },
{ value: "system manager", label: "System Manager" },
{ value: "sr. system manager", label: "Sr. System Manager" },
],
domains: ["Strategy", "Public Policy", "IT/Digital", "Finance", "Operations", "ESG"],
skills: [
"Financial Modeling", "Market Research", "Stakeholder Engagement", "Policy Analysis",
"Data Analytics", "Project Management", "Business Development", "Process Optimization",
],
tools: ["PowerPoint", "Excel", "Power BI", "Tableau", "Python", "SQL", "QGIS"],
methodologies: [
"Theory of Change", "Value Chain Analysis", "Porter's Five Forces", "Lean", "Agile",
],
industries: [
"Industrial Development", "Smart Cities", "Rural Growth", "Digital Government",
"Climate & Energy", "Health Systems", "Financial Inclusion",
],
regions: [
"Pan-India", "North India", "South India", "East India", "West India",
"APAC", "MENA", "Africa", "Europe",
],
languages: ["English", "Hindi", "Tamil", "Telugu", "Marathi", "Bengali", "Gujarati"],
practices: [
"Public Policy Realization", "Sector Potential Realization", "Impact Realization",
"Economic Potential Realization", "Data & Digital Strategy", "Transaction Realization",
],
};
window.TAXONOMY = TAXONOMY;
// ── LoginVideo ────────────────────────────────────────────────────────────────
const LoginVideo = () => (
<>
{/* Maroon tint so white text at the bottom stays readable */}
>
);
// ── Login ─────────────────────────────────────────────────────────────────────
const Login = ({ onLogin }) => {
const [email, setEmail] = React.useState("");
const [pw, setPw] = React.useState("");
const [step, setStep] = React.useState(0); // 0=email, 1=password
const [err, setErr] = React.useState({});
const [loading, setLoading] = React.useState(false);
const isSso = email.toLowerCase().endsWith("@primuspartners.in");
const onNext = (e) => {
e && e.preventDefault();
if (!email.match(/.+@.+\..+/)) { setErr({ email: "Enter a valid work email." }); return; }
setErr({});
if (isSso) { setLoading(true); window.location.href = `/auth/sso/login?email=${encodeURIComponent(email)}`; }
else setStep(1);
};
const onManualLogin = async (e) => {
e.preventDefault();
if (pw.length < 6) { setErr({ pw: "Password must be at least 6 characters." }); return; }
setLoading(true);
try {
const data = await PrimusAPI.login(email, pw);
onLogin(data);
} catch (ex) {
setErr({ pw: ex.message || "Invalid credentials." });
} finally { setLoading(false); }
};
const inputStyle = (hasErr) => ({
width: "100%", height: 44, padding: "0 14px",
border: `1px solid ${hasErr ? "var(--maroon)" : "var(--line-2)"}`,
borderRadius: "var(--r-md)", fontSize: 14, fontFamily: "inherit",
background: "var(--bg-card)", color: "var(--ink)", boxSizing: "border-box",
outline: "none", transition: "border-color 0.15s",
});
return (
{/* Left brand panel */}
Primus Partners
Primus IQ
Every deck, dataset and decision — woven into one firm-wide intelligence layer.
{/* Right form panel */}
Sign in to Primus IQ
{step === 0 ? "Welcome back." : "Enter password."}
{step === 0 ? "Sign in with your Primus work email." : "Enter your credentials to continue."}
🔐
Secure by default
{" "}· Microsoft Entra ID enforced · @primuspartners.in SSO required
Trouble signing in?{" "}
Contact IT
);
};
// ── Onboarding helpers ─────────────────────────────────────────────────────────
const InputField = ({ label, hint, error, required, children }) => (
{children}
{error &&
{error}
}
);
const textInputStyle = (hasErr) => ({
width: "100%", height: 40, padding: "0 12px",
border: `1px solid ${hasErr ? "var(--maroon)" : "var(--line-2)"}`,
borderRadius: "var(--r-md)", fontSize: 14, fontFamily: "inherit",
background: "var(--bg-card)", color: "var(--ink)", boxSizing: "border-box",
outline: "none", transition: "border-color 0.15s",
});
const TextInput = ({ error, ...props }) => (
);
const SelectInput = ({ value, onChange, options, placeholder, error, disabled }) => (
);
const MultiChipInput = ({ values, onChange, options, allowCustom }) => {
const [inputVal, setInputVal] = React.useState("");
const add = (v) => {
const trimmed = v.trim();
if (trimmed && !values.includes(trimmed)) onChange([...values, trimmed]);
setInputVal("");
};
const remove = (v) => onChange(values.filter(x => x !== v));
const onKeyDown = (e) => {
if (e.key === "Enter") { e.preventDefault(); if (inputVal.trim()) add(inputVal); }
if (e.key === "Backspace" && !inputVal && values.length > 0) remove(values[values.length - 1]);
};
const filteredOptions = options
? options.filter(o => !values.includes(o) && (inputVal === "" || o.toLowerCase().includes(inputVal.toLowerCase())))
: [];
return (
{values.map(v => (
{v}
))}
setInputVal(e.target.value)} onKeyDown={onKeyDown}
placeholder={values.length === 0 ? "Type and press Enter…" : ""}
style={{ border: "none", outline: "none", fontSize: 13, fontFamily: "inherit", background: "transparent", flex: 1, minWidth: 80, color: "var(--ink)" }}
/>
{filteredOptions.length > 0 && inputVal && (
{filteredOptions.slice(0, 8).map(opt => (
add(opt)}
style={{ padding: "8px 12px", fontSize: 13, cursor: "pointer", color: "var(--ink)" }}
onMouseEnter={e => e.currentTarget.style.background = "var(--bg-soft)"}
onMouseLeave={e => e.currentTarget.style.background = "transparent"}
>{opt}
))}
)}
);
};
// ── Onboarding step components ─────────────────────────────────────────────────
const BasicStep = ({ form, set, errors }) => {
const handleFileChange = (e) => {
const file = e.target.files[0];
if (!file) return;
const isValid = file.type === "application/pdf" || file.name.endsWith(".docx") || file.type === "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
if (!isValid) { alert("Please upload a PDF or DOCX file."); return; }
if (file.size > 10 * 1024 * 1024) { alert("File must be under 10 MB."); return; }
set("resumeFile", file);
};
return (
set("fullName", e.target.value)} placeholder="As on records" error={errors.fullName} />
set("empCode", e.target.value.toUpperCase())} placeholder="e.g. PP-04129" error={errors.empCode} />
);
};
const ExpertiseStep = ({ form, set, errors }) => (
set("primary", v)}
options={TAXONOMY.domains} placeholder="Select domain…" error={errors.primary}
/>
set("secondary", v)}
options={TAXONOMY.domains.filter(d => d !== form.primary)} placeholder="Optional…"
/>
set("skills", v)} options={TAXONOMY.skills} allowCustom />
set("tools", v)} options={TAXONOMY.tools} allowCustom />
set("methodologies", v)} options={TAXONOMY.methodologies} />
);
const IndustryStep = ({ form, set, errors }) => (
set("industries", v)} options={TAXONOMY.industries} />
set("regions", v)} options={TAXONOMY.regions} />
set("languages", v)} options={TAXONOMY.languages} allowCustom />
);
const ReviewStep = ({ form }) => {
const designationLabel = TAXONOMY.designations.find(d => d.value === form.designation)?.label || form.designation || "—";
const Row = ({ k, v }) => {
let display;
if (Array.isArray(v)) {
display = v.length > 0
? v.map(x => (
{x}
))
: —;
} else {
display = v || —;
}
return (
);
};
return (
Review your Consultant DNA
{designationLabel}
);
};
// ── Onboarding ─────────────────────────────────────────────────────────────────
const ONBOARDING_STEPS = ["Basic", "Expertise", "Industry", "Review"];
const Onboarding = ({ user, onComplete }) => {
const [step, setStep] = React.useState(0);
const [form, setForm] = React.useState({
email: user?.primus_email || user?.email || "",
fullName: user?.full_name || "",
empCode: user?.emp_code || "",
designation: user?.designation || "",
yoe: String(user?.yoe ?? ""),
resumeFile: null,
primary: user?.primary_domain || "",
secondary: user?.secondary_domain || "",
skills: user?.skills || [],
tools: user?.tools || [],
methodologies: user?.methodologies || [],
industries: user?.industries || [],
regions: user?.regions || [],
languages: user?.languages || [],
});
const [errors, setErrors] = React.useState({});
const [loading, setLoading] = React.useState(false);
const set = (k, v) => setForm(f => ({ ...f, [k]: v }));
const validateStep = () => {
const e = {};
if (step === 0) {
if (!form.fullName.trim()) e.fullName = "Full name is required.";
if (!form.empCode.trim()) e.empCode = "Employee code is required.";
if (form.yoe === "" || isNaN(+form.yoe) || +form.yoe < 0) e.yoe = "Enter a number ≥ 0.";
} else if (step === 1) {
if (!form.primary) e.primary = "Pick a primary domain.";
if (form.skills.length === 0) e.skills = "Add at least one skill.";
} else if (step === 2) {
if (form.industries.length === 0) e.industries = "Select at least one industry.";
}
setErrors(e);
return Object.keys(e).length === 0;
};
const next = async () => {
if (!validateStep()) return;
setLoading(true);
try {
if (step === 0) {
await PrimusAPI.profileStep1({ full_name: form.fullName, emp_code: form.empCode, yoe: parseInt(form.yoe, 10), designation: form.designation || undefined });
if (form.resumeFile) {
try { await PrimusAPI.uploadResume(form.resumeFile); } catch (ex) { console.warn("Resume upload failed:", ex); }
}
} else if (step === 1) {
await PrimusAPI.profileStep2({ primary_domain: form.primary, secondary_domain: form.secondary || "", skills: form.skills, tools: form.tools, methodologies: form.methodologies });
} else if (step === 2) {
await PrimusAPI.profileStep3({ industries: form.industries, regions: form.regions, languages: form.languages });
}
setStep(s => s + 1);
} catch (ex) { setErrors({ api: ex.message }); }
finally { setLoading(false); }
};
const handleSubmit = async () => {
setLoading(true);
try { await PrimusAPI.profileFinalize(); onComplete(); }
catch (ex) { setErrors({ api: ex.message }); }
finally { setLoading(false); }
};
const stepTitles = ["Identity", "Expertise", "Background", "Review"];
const stepDescs = [
"Your basic profile information.",
"Your domain expertise and skills.",
"Industries and regions you've worked in.",
"Review your profile before submitting.",
];
return (
{/* Left aside */}
Primus IQ
Build your
Consultant DNA.
A few minutes to map your expertise — so the firm's intelligence layer can route work to you and surface your past contributions.
{["Capture once. Used everywhere.", "Edit any time from your profile.", "Your data. Restricted views by role."].map((t, i) => (
{i + 1}
{t}
))}
{/* Right form */}
{/* Step indicator */}
{ONBOARDING_STEPS.map((s, i) => (
{i < step ? "✓" : i + 1}
{s}
{i < ONBOARDING_STEPS.length - 1 && (
)}
))}
{stepTitles[step]}
{stepDescs[step]}
{errors.api && (
{errors.api}
)}
{step === 0 &&
}
{step === 1 &&
}
{step === 2 &&
}
{step === 3 &&
}
{step < ONBOARDING_STEPS.length - 1 ? (
) : (
)}
);
};
// ── Expose on window ────────────────────────────────────────────────────────────
Object.assign(window, { Login, Onboarding });