import React, { useState } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { Sparkles, ChevronDown, ChevronUp, Sliders, Loader2, Star } from 'lucide-react'; import { useCreateJobPosting, useUpdateJobPosting, useGenerateJobDescription, useRoleCategories, useCreateRoleCategory } from '@/lib/krowHooks'; import { toast } from 'react-hot-toast'; import { ROLE_CATEGORIES } from '@/lib/roleCategories'; import { CERT_OPTIONS, CRITERIA_LABELS, ENGLISH_LEVELS, defaultPosition } from '@/lib/positionModel'; import { SkillRequirementsField } from '@/components/krow/SkillRequirementsField'; /** * Create a Position. * * The manual form, reachable at `/admin/positions/new`. `embedded` drops the * page header for a host that supplies its own, and `onDone` lets that host * close it instead of navigating away. * * Owliver does not open this form. Asking Owliver for a position is a * conversation that ends in the same record (see `lib/skills/positionFlow.js`), * and both paths share one field definition — `lib/positionModel.js` — so * neither can drift into a different shape of position. */ export default function CreatePosition({ prefill: prefillProp, embedded = false, onDone }) { const navigate = useNavigate(); /* Owliver opens this page with the fields it could infer from the request. It is a starting point, not a submission: every field stays editable and the position is still created by the button at the bottom. */ const { state } = useLocation(); const prefill = prefillProp || state?.prefill || null; const createJob = useCreateJobPosting(); const updateJob = useUpdateJobPosting(); const generateDesc = useGenerateJobDescription(); const [form, setForm] = useState({ ...defaultPosition(), ...prefill }); const { data: customCategories = [] } = useRoleCategories(); const createRoleCategory = useCreateRoleCategory(); const [newCategory, setNewCategory] = useState(''); const [showWeights, setShowWeights] = useState(false); const allCategories = [...new Set([...ROLE_CATEGORIES, ...customCategories.map(c => c.name)])]; const [draftId, setDraftId] = useState(null); const [aiResult, setAiResult] = useState(null); const update = (field, value) => setForm({ ...form, [field]: value }); const toggleCert = (cert) => { update('certifications_required', form.certifications_required.includes(cert) ? form.certifications_required.filter(c => c !== cert) : [...form.certifications_required, cert]); }; const updateWeight = (key, value) => { update('vetting_criteria', { ...form.vetting_criteria, [key]: parseInt(value) || 0 }); }; const totalWeight = Object.values(form.vetting_criteria).reduce((a, b) => a + b, 0); const handleGenerateDescription = async () => { if (!form.title) { toast.error('Please enter a job title first'); return; } try { // Create draft if not exists let id = draftId; if (!id) { const draft = await createJob.mutateAsync({ ...form, status: 'draft', pay_range_min: Number(form.pay_range_min) || 0, pay_range_max: Number(form.pay_range_max) || 0 }); id = draft.id; setDraftId(id); } const result = await generateDesc.mutateAsync({ data: { ...form, pay_range_min: Number(form.pay_range_min) || 0, pay_range_max: Number(form.pay_range_max) || 0 }, draftId: id }); setAiResult(result); toast.success('Job description generated!'); } catch (e) { toast.error('AI generation failed. Please try again.'); } }; const handleSave = async (status) => { if (!form.title) { toast.error('Job title is required'); return; } if (totalWeight !== 100) { toast.error('Vetting weights must total 100%'); return; } try { const payload = { ...form, status, company: String(form.company || '').trim(), pay_range_min: Number(form.pay_range_min) || 0, pay_range_max: Number(form.pay_range_max) || 0, ...(aiResult ? { description: aiResult.description, responsibilities: aiResult.responsibilities, qualifications: aiResult.qualifications, nice_to_haves: aiResult.nice_to_haves, ai_generated: true } : {}), }; let createdId = draftId; if (draftId) { await updateJob.mutateAsync({ id: draftId, data: payload }); } else { const created = await createJob.mutateAsync(payload); createdId = created?.id; } toast.success(status === 'active' ? 'Job posting published!' : 'Saved as draft'); /* Inline, the list behind the form is already the destination — closing returns to it with the new position in place. */ if (onDone) { onDone(createdId); } else if (status === 'active' && createdId) { navigate(`/admin/positions/${createdId}`); } else { navigate('/admin/positions'); } } catch (e) { toast.error('Failed to save'); } }; return (
{/* Header */} {!embedded && (

Create a Position

Set hiring criteria once — KROW interviews and ranks every applicant automatically.

)} {/* Card */}
{/* Section Header */}

Create Job Posting

{/* Row 1 */}
update('title', e.target.value)} placeholder="e.g. Experienced Server" className="w-full mt-1.5 h-10 rounded-lg border border-[#E5E7EB] bg-white px-3 text-[13px] focus:border-[#0838E0] focus:outline-none" />
{/* The client this role is being staffed for. Stored on the position record (`company`), which is what every surface reads to show who the hire is actually for — the field existed in the model and had no way in from this form until now. */}
update('company', e.target.value)} placeholder="e.g. Fairmont San Jose" className="w-full mt-1.5 h-10 rounded-lg border border-[#E5E7EB] bg-white px-3 text-[13px] focus:border-[#0838E0] focus:outline-none" />
{/* Add new category */}
setNewCategory(e.target.value)} placeholder="Create a new role category (saved for reuse)..." className="flex-1 h-10 rounded-lg border border-[#E5E7EB] px-3 text-[13px] focus:border-[#0838E0] focus:outline-none" />
{/* Requirements */}