"use client"; /** * Create Job (2026-11-10) — same form and flow as scheme.qrs.ae "Create Job": * Company · Audit (from Audit Schedule) · Current Stage · Lead Auditor · Templates (Select All) · Audit Date · * expected dates · Document Prepare / Approval Date · Standards · Document Review · No. of Employees · * NACE / EAC · MD · MD Risk · Schedule Slot. * Save creates ONE JOB PER SELECTED TEMPLATE (own job number each). The documents then show in the company's * 📁 Documents tab and in Job Registrar. */ import React, { useEffect, useMemo, useState } from "react"; import { createPortal } from "react-dom"; import toast from "react-hot-toast"; import { FiCheckSquare, FiSquare, FiX } from "react-icons/fi"; import { createCompanyJobs, getJobForm, searchCodebook, type CodebookRow, type JobFormAudit, type JobFormData } from "@/lib/api/company-documents.api"; type Stage = "Stage 1" | "Stage 2"; const EMPTY = { auditId: "" as number | "", stage: "Stage 1" as Stage, leadAuditorId: "" as number | "", templateIds: [] as number[], date: "", expectedSurveillanceDate: "", expectedIADate: "", expectedMRMDate: "", expectedInitialInquiryDate: "", expectedInitial_Stage1_Date: "", prepareDate: "", approvedDate: "", standardIds: [] as number[], docReview: "", numEmployees: "", naceEacCodes: "", md: "", mdRisk: "", scheduleSlot: "", technicalExpert: "", }; const addMonths = (d: string, m: number) => { if (!d) return ""; const x = new Date(d + "T00:00:00Z"); x.setUTCMonth(x.getUTCMonth() + m); return x.toISOString().slice(0, 10); }; export default function CreateJobModal({ company, onClose, onCreated, }: { company: { id: number; name: string } | null; onClose: () => void; onCreated?: () => void; }) { const [data, setData] = useState(null); const [f, setF] = useState({ ...EMPTY }); const [saving, setSaving] = useState(false); const [loading, setLoading] = useState(false); useEffect(() => { if (!company) return; setF({ ...EMPTY }); setData(null); setLoading(true); getJobForm(company.id) .then((d) => { setData(d); // like scheme: preselect the company's latest audit if (d.audits.length) applyAudit(d.audits[0], { ...EMPTY }); }) .catch((e) => toast.error(e?.message || "Could not load the form")) .finally(() => setLoading(false)); // eslint-disable-next-line react-hooks/exhaustive-deps }, [company?.id]); const applyAudit = (a: JobFormAudit, base = f) => { const p = a.prefill || {}; const stage = a.stage_label; const s1 = p.expectedInitial_Stage1_Date || (stage === "Stage 1" ? p.date : "") || ""; setF({ ...base, auditId: a.id, stage, leadAuditorId: (p.leadAuditorId as number) || "", standardIds: p.standardIds || a.standards.map((s) => s.id), date: p.date || "", scheduleSlot: (p.scheduleSlot as string) || "", expectedSurveillanceDate: s1 ? addMonths(s1, 12) : p.expectedSurveillanceDate || "", expectedIADate: s1 ? addMonths(s1, 4) : p.expectedIADate || "", expectedMRMDate: s1 ? addMonths(s1, 3) : p.expectedMRMDate || "", expectedInitialInquiryDate: p.expectedInitialInquiryDate || "", expectedInitial_Stage1_Date: s1, templateIds: a.recommended_template_ids?.[stage] ?? [], }); }; const audit = useMemo(() => data?.audits.find((a) => a.id === f.auditId) ?? null, [data, f.auditId]); const set = (k: keyof typeof EMPTY, v: any) => setF((x) => ({ ...x, [k]: v })); const templates = data?.templates ?? []; const chosen = templates.filter((t) => f.templateIds.includes(t.id)); const needApproval = chosen.filter((t) => t.prints.includes("approvedDate")); const save = async (e: React.FormEvent) => { e.preventDefault(); if (!company || !audit) return toast.error("Select the audit"); if (!f.templateIds.length) return toast.error("Select at least one template"); if (!f.leadAuditorId) return toast.error("Select the lead auditor"); if (!f.date) return toast.error("Audit date is required"); if (!f.standardIds.length) return toast.error("Select the standards"); if (!f.mdRisk) return toast.error("Select MD Risk"); if (f.md && !/^(\d+(\.\d+)?|\.\d+)$/.test(f.md)) return toast.error("MD must be a valid number (e.g. .5, 1, 2)"); if (needApproval.length && !f.approvedDate) return toast.error(`Document Approval Date is needed for: ${needApproval.map((t) => t.name).join(", ")}`); setSaving(true); try { const r = await createCompanyJobs(company.id, { audit_schedule_row_id: audit.id, stage: f.stage, templateIds: f.templateIds, leadAuditorId: Number(f.leadAuditorId), standardIds: f.standardIds, date: f.date, docReview: f.docReview || null, numEmployees: f.numEmployees === "" ? null : Number(f.numEmployees), naceEacCodes: f.naceEacCodes || null, md: f.md || null, mdRisk: f.mdRisk, scheduleSlot: f.scheduleSlot || "FULL", technicalExpert: f.technicalExpert || null, expectedSurveillanceDate: f.expectedSurveillanceDate || null, expectedIADate: f.expectedIADate || null, expectedMRMDate: f.expectedMRMDate || null, expectedInitialInquiryDate: f.expectedInitialInquiryDate || null, expectedInitial_Stage1_Date: f.expectedInitial_Stage1_Date || null, prepareDate: f.prepareDate || null, approvedDate: f.approvedDate || null, }); toast.success(`${r.created} Job(s) created successfully!`); onCreated?.(); onClose(); } catch (err: any) { toast.error(err?.message || "Failed to save job"); } finally { setSaving(false); } }; if (!company || typeof document === "undefined") return null; const byStage = (s: Stage) => templates.filter((t) => t.stage_name === s); const allIds = templates.map((t) => t.id); return createPortal(
e.stopPropagation()} style={{ position: "fixed", inset: 0, zIndex: 1100, background: "rgba(15,23,42,.55)", display: "flex", alignItems: "center", justifyContent: "center", padding: 16 }}>

Create Job

{loading || !data ? (
Loading…
) : (
{/* Templates — Select All, like scheme */}
{audit && }
{(["Stage 1", "Stage 2"] as Stage[]).map((s) => (
{s}
{byStage(s).map((t) => { const on = f.templateIds.includes(t.id); return ( ); })}
))}
set("date", e.target.value)} style={inp} /> set("expectedSurveillanceDate", e.target.value)} style={inp} /> set("expectedIADate", e.target.value)} style={inp} /> set("expectedMRMDate", e.target.value)} style={inp} /> set("expectedInitialInquiryDate", e.target.value)} style={inp} /> set("expectedInitial_Stage1_Date", e.target.value)} style={inp} /> set("prepareDate", e.target.value)} style={inp} /> Document Approval Date {needApproval.length ? REQUIRED : OPTIONAL}} hint={needApproval.length ? `Needed for: ${needApproval.map((t) => t.name).join(", ")}` : "Not needed for the selected templates"} > set("approvedDate", e.target.value)} style={{ ...inp, borderColor: needApproval.length && !f.approvedDate ? "#f87171" : "#d1d5db" }} />
{data.standards.map((s) => { const on = f.standardIds.includes(s.id); return ( ); })}
set("docReview", e.target.value)} style={inp} /> set("numEmployees", e.target.value)} placeholder="No of employee" style={inp} /> { set("naceEacCodes", v); if (risk && !f.mdRisk) set("mdRisk", risk); }} /> set("md", e.target.value)} placeholder="Enter value eg .5,1 etc" style={inp} /> set("technicalExpert", e.target.value)} style={inp} />
)}
, document.body, ); } const inp: React.CSSProperties = { width: "100%", height: 38, padding: "0 10px", borderRadius: 8, border: "1px solid #d1d5db", fontSize: 13, color: "#0f172a", background: "#fff", boxSizing: "border-box" }; const lbl: React.CSSProperties = { display: "flex", alignItems: "center", gap: 6, fontSize: 12.5, fontWeight: 700, color: "#334155", marginBottom: 5 }; const chip: React.CSSProperties = { padding: "4px 10px", borderRadius: 7, border: "1px solid #ddd6fe", background: "#f5f3ff", color: "#5b21b6", fontSize: 11.5, fontWeight: 700, cursor: "pointer" }; function G({ label, hint, children }: { label: React.ReactNode; hint?: string; children: React.ReactNode }) { return (
{children} {hint &&
{hint}
}
); } const Tag = ({ c, b, children }: { c: string; b: string; children: React.ReactNode }) => ( {children} ); function NaceInput({ value, onChange }: { value: string; onChange: (v: string, risk?: string) => void }) { const [q, setQ] = useState(""); const [rows, setRows] = useState([]); const [show, setShow] = useState(false); useEffect(() => { if (!q.trim()) return setRows([]); const t = setTimeout(() => searchCodebook(q).then(setRows).catch(() => setRows([])), 250); return () => clearTimeout(t); }, [q]); const code = (r: CodebookRow) => r.eac_iaf_codes || r.nace_rev1 || r.class || r.group || r.division || ""; const risk = (r: CodebookRow) => { const x = String(r.risk || "").toLowerCase(); return x.startsWith("h") ? "High" : x.startsWith("m") ? "Medium" : x.startsWith("l") ? "Low" : undefined; }; return (
onChange(e.target.value)} placeholder="NACE/EAC" style={inp} /> setShow(true)} onBlur={() => setTimeout(() => setShow(false), 200)} onChange={(e) => setQ(e.target.value)} placeholder="🔍 search code book…" style={{ ...inp, height: 30, marginTop: 5, fontSize: 12, borderStyle: "dashed" }} /> {show && rows.length > 0 && (
{rows.map((r) => ( ))}
)}
); }