// Create Project (search-first dedup → form) + Upload Artifact metadata modal const CreateProjectModal = ({ open, onClose, onCreated, tier = "execution" }) => { const [stage, setStage] = React.useState("search"); // search | match | form | success const [client, setClient] = React.useState(""); const [matched, setMatched] = React.useState(null); const [suggestions, setSuggestions] = React.useState([]); const [form, setForm] = React.useState({ client: "", clientType: "", vertical: "", engagement: "", months: "", budget: "", client_details: "" }); const [errors, setErrors] = React.useState({}); const [loading, setLoading] = React.useState(false); React.useEffect(() => { if (open) { setStage("search"); setClient(""); setMatched(null); setSuggestions([]); setForm({ client: "", clientType: "", vertical: "", engagement: "", months: "", budget: "", client_details: "" }); setErrors({}); } }, [open]); // Live search suggestions from backend React.useEffect(() => { if (!client.trim() || client.length < 1) { setSuggestions([]); return; } const t = setTimeout(() => { PrimaAPI.listProjects(client.trim()) .then(results => setSuggestions((results || []).slice(0, 4))) .catch(() => setSuggestions([])); }, 250); return () => clearTimeout(t); }, [client]); const checkMatch = async () => { const q = client.trim().toLowerCase(); if (!q) return; setLoading(true); try { const results = await PrimaAPI.listProjects(q); const exact = (results || []).find(p => p.client_name.toLowerCase() === q); if (exact) { setMatched(exact); setStage("match"); } else { setForm(f => ({ ...f, client })); setStage("form"); } } catch (e) { // If backend unavailable, proceed to form setForm(f => ({ ...f, client })); setStage("form"); } finally { setLoading(false); } }; const validate = () => { const e = {}; if (!form.client.trim()) e.client = "Required."; if (!form.clientType) e.clientType = "Required."; if (!form.vertical) e.vertical = "Required."; if (!form.engagement) e.engagement = "Required."; if (!form.months || isNaN(+form.months) || +form.months < 1 || +form.months > 60) e.months = "Months must be between 1 and 60."; setErrors(e); return Object.keys(e).length === 0; }; const submit = async () => { if (!validate()) return; setLoading(true); setErrors({}); try { const resp = await PrimaAPI.createProject({ client_name: form.client, client_type: form.clientType, vertical: form.vertical, engagement: form.engagement, months: parseInt(form.months, 10), budget: form.budget || undefined, client_details: form.client_details || "", }); if (resp.message && resp.message.includes("already exists")) { // Leadership auto-joined case setMatched(resp); setStage("success"); setTimeout(() => { onCreated(resp); onClose(); }, 1500); } else { setStage("success"); setTimeout(() => { onCreated(resp); onClose(); }, 1100); } } catch (e) { if (e.status === 409) { // Management join request sent case setJoinSent(true); setStage("match"); // Go back to match stage to show the "Request Sent" message setErrors({ api: e.message }); } else { setErrors({ api: e.message || "Failed to create project." }); } } finally { setLoading(false); } }; const [joinSent, setJoinSent] = React.useState(false); const joinExisting = async () => { if (!matched) return; setLoading(true); try { const resp = await PrimaAPI.joinProject(matched.id); if (resp && resp.status === "approved") { // Leadership auto-approve: enter project immediately onCreated(matched); onClose(); } else { setJoinSent(true); } } catch (e) { setErrors({ api: e.message || "Failed to request access." }); } finally { setLoading(false); } }; return ( ) : stage === "match" ? ( <> {joinSent ? ( ) : ( tier !== "execution" ? ( ) : (
Ask the project owner to invite you.
) )} ) : stage === "form" ? ( <> ) : null }> {stage === "search" && (
setClient(e.target.value)} placeholder="Start typing the project name…" autoFocus onKeyDown={(e) => { if (e.key === "Enter" && client.trim()) checkMatch(); }} /> {suggestions.length > 0 && (
{suggestions.map(p => ( ))}
)}
Already exists? You'll be able to request access to contribute.
)} {stage === "match" && matched && (
{errors.api &&
{errors.api}
}
This project already exists
{matched.client_name}
{matched.client_type} · {matched.vertical}
{matched.description || "—"}
{matched.code} {matched.engagement} {matched.status}

Duplicate projects are blocked. Request to join — the project Owner or any Tier 1 (VP+) user can approve. Once approved, this project appears in your workspace with upload rights.

)} {stage === "form" && (
{errors.api &&
{errors.api}
}
setForm({ ...form, client: e.target.value })} error={errors.client} />