@burdenoff/microfe-crewfoundry 2026.1013.1 → 2026.1013.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/crewfoundry/components/forms/FormModal.d.ts +25 -1
- package/dist/crewfoundry/components/forms/FormModal.d.ts.map +1 -1
- package/dist/crewfoundry/components/forms/FormModal.js +151 -136
- package/dist/crewfoundry/components/forms/FormModal.js.map +1 -1
- package/dist/crewfoundry/pages/AnalyticsHiringAnalyticsPagePage.d.ts.map +1 -1
- package/dist/crewfoundry/pages/AnalyticsHiringAnalyticsPagePage.js +49 -49
- package/dist/crewfoundry/pages/AnalyticsHiringAnalyticsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HiringCreateJobPagePage.d.ts.map +1 -1
- package/dist/crewfoundry/pages/HiringCreateJobPagePage.js +260 -115
- package/dist/crewfoundry/pages/HiringCreateJobPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HiringJobDetailsPagePage.d.ts.map +1 -1
- package/dist/crewfoundry/pages/HiringJobDetailsPagePage.js +199 -166
- package/dist/crewfoundry/pages/HiringJobDetailsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HiringJobsPagePage.d.ts.map +1 -1
- package/dist/crewfoundry/pages/HiringJobsPagePage.js +118 -104
- package/dist/crewfoundry/pages/HiringJobsPagePage.js.map +1 -1
- package/dist/crewfoundry/store/crewfoundryStore.d.ts +1 -1
- package/dist/crewfoundry/store/crewfoundryStore.d.ts.map +1 -1
- package/dist/crewfoundry/store/crewfoundryStore.js.map +1 -1
- package/dist/crewfoundry/utils/dateUtils.d.ts +47 -0
- package/dist/crewfoundry/utils/dateUtils.d.ts.map +1 -0
- package/dist/crewfoundry/utils/dateUtils.js +22 -0
- package/dist/crewfoundry/utils/dateUtils.js.map +1 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HiringJobDetailsPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/HiringJobDetailsPagePage.tsx"],"sourcesContent":["import {\n Badge,\n Button,\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n GlassCard,\n GradientText,\n IllustratedEmptyState,\n PagePurpose,\n Spinner,\n} from '@burdenoff/fe-libs/ui';\nimport { useState } from 'react';\nimport {\n Briefcase,\n Calendar,\n CheckCircle,\n MapPin,\n Pencil,\n Share2,\n Sparkles,\n Trash2,\n Users,\n XCircle,\n} from 'lucide-react';\nimport { useNavigate, useSearchParams } from 'react-router';\nimport { useGlobalUi } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { useCrewFoundryStore, type Candidate, type JobPosting } from '../store/crewfoundryStore';\nimport { FormModal, type FieldConfig, type FormValues } from '../components/forms/FormModal';\nimport { ConfirmDialog } from '../components/forms/ConfirmDialog';\nimport { getNumber, getTags, getText } from '../components/forms/formValues';\nimport { formatEnumLabel } from '../utils/formatEnumLabel';\nimport {\n HBarFunnel,\n RatingStars,\n SkillBar,\n STATUS,\n getInitials,\n type LabeledValue,\n} from '../components/viz';\n\nconst JOB_FIELDS: FieldConfig[] = [\n { name: 'title', label: 'Job Title', type: 'text', required: true, colSpan: 2 },\n { name: 'department', label: 'Department', type: 'text', required: true },\n { name: 'location', label: 'Location', type: 'text', required: true },\n {\n name: 'type',\n label: 'Employment Type',\n type: 'select',\n options: [\n { label: 'Full time', value: 'full_time' },\n { label: 'Part time', value: 'part_time' },\n { label: 'Contract', value: 'contract' },\n ],\n },\n {\n name: 'experienceLevel',\n label: 'Experience Level',\n type: 'select',\n options: [\n { label: 'Entry', value: 'entry' },\n { label: 'Mid', value: 'mid' },\n { label: 'Senior', value: 'senior' },\n { label: 'Lead', value: 'lead' },\n { label: 'Executive', value: 'executive' },\n ],\n },\n {\n name: 'status',\n label: 'Status',\n type: 'select',\n options: [\n { label: 'Draft', value: 'draft' },\n { label: 'Active', value: 'active' },\n { label: 'Paused', value: 'paused' },\n { label: 'Closed', value: 'closed' },\n { label: 'Filled', value: 'filled' },\n ],\n },\n { name: 'salaryMin', label: 'Salary Min ($)', type: 'number' },\n { name: 'salaryMax', label: 'Salary Max ($)', type: 'number' },\n { name: 'deadline', label: 'Application Deadline', type: 'date' },\n { name: 'description', label: 'Description', type: 'textarea', colSpan: 2 },\n { name: 'requirements', label: 'Requirements', type: 'tags' },\n { name: 'benefits', label: 'Benefits', type: 'tags' },\n];\n\nconst STAGE_BADGE: Record<\n Candidate['status'],\n 'default' | 'secondary' | 'destructive' | 'outline'\n> = {\n new: 'outline',\n screening: 'secondary',\n interview: 'secondary',\n assessment: 'secondary',\n offer: 'default',\n hired: 'default',\n rejected: 'destructive',\n};\n\nfunction InitialsAvatar({ name, size = 32 }: { name: string; size?: number }) {\n return (\n <span\n className=\"inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full border border-border-subtle bg-bg-sunken font-medium text-text-secondary\"\n style={{ width: size, height: size, fontSize: Math.round(size * 0.36) }}\n aria-hidden=\"true\"\n >\n {getInitials(name)}\n </span>\n );\n}\n\nexport function HiringJobDetailsPagePage() {\n const navigate = useNavigate();\n const [searchParams] = useSearchParams();\n const { showToast } = useGlobalUi();\n const {\n jobPostings,\n candidates,\n updateJobPosting,\n removeJobPosting,\n initialized,\n jobPostingMatches,\n jobPostingMatchesLoading,\n fetchJobPostingMatches,\n } = useCrewFoundryStore();\n const jobId = searchParams.get('id');\n const job = jobId ? jobPostings.find((j) => j.id === jobId) : jobPostings[0];\n const [editOpen, setEditOpen] = useState(false);\n const [deleteOpen, setDeleteOpen] = useState(false);\n\n if (!initialized && !job) {\n return (\n <div className=\"flex min-h-[60vh] items-center justify-center p-6\">\n <Spinner size=\"lg\" />\n </div>\n );\n }\n\n if (!job) {\n return (\n <div className=\"p-6\">\n <IllustratedEmptyState\n illustration=\"empty-search\"\n title=\"No job posting found\"\n description=\"This job posting is no longer available.\"\n />\n </div>\n );\n }\n\n // Applicants for this role. CrewCandidate has no jobId link, so match on the\n // (normalized) position title — the only real linkage the schema exposes.\n const titleKey = job.title.trim().toLowerCase();\n const jobCandidates = candidates.filter((c) => c.position.trim().toLowerCase() === titleKey);\n const countBy = (status: Candidate['status']) =>\n jobCandidates.filter((c) => c.status === status).length;\n\n const interviewCount = countBy('interview');\n\n // Real-data hiring funnel: \"Applied\" = everyone who applied to this role, then\n // the count currently at each downstream stage (offer folds in hired).\n const funnelStages: LabeledValue[] = [\n { label: 'Applied', value: jobCandidates.length },\n { label: 'Screening', value: countBy('screening') },\n { label: 'Interview', value: interviewCount },\n { label: 'Assessment', value: countBy('assessment') },\n { label: 'Offer', value: countBy('offer') + countBy('hired') },\n ];\n\n const editInitial: FormValues = {\n title: job.title,\n department: job.department,\n location: job.location,\n type: job.type,\n experienceLevel: job.experienceLevel,\n status: job.status,\n salaryMin: job.salaryMin,\n salaryMax: job.salaryMax,\n deadline: job.deadline,\n description: job.description,\n requirements: job.requirements,\n benefits: job.benefits,\n };\n\n const handleEditSubmit = async (values: FormValues) => {\n const updates: Partial<JobPosting> = {\n title: getText(values, 'title'),\n department: getText(values, 'department'),\n location: getText(values, 'location'),\n type: values.type as JobPosting['type'],\n experienceLevel: values.experienceLevel as JobPosting['experienceLevel'],\n status: values.status as JobPosting['status'],\n salaryMin: getNumber(values, 'salaryMin'),\n salaryMax: getNumber(values, 'salaryMax'),\n deadline: getText(values, 'deadline'),\n description: getText(values, 'description'),\n requirements: getTags(values, 'requirements'),\n benefits: getTags(values, 'benefits'),\n };\n await updateJobPosting(job.id, updates);\n showToast('Job posting updated', 'success');\n };\n\n const handleDelete = async () => {\n await removeJobPosting(job.id);\n showToast('Job posting deleted', 'success');\n navigate('../hiring/jobs-page');\n };\n\n const handleShare = async () => {\n try {\n await navigator.clipboard.writeText(window.location.href);\n showToast('Job link copied to clipboard', 'success');\n } catch {\n showToast('Could not copy the job link', 'error');\n }\n };\n\n const viewApplications = () =>\n navigate(`../hiring/applications-page?position=${encodeURIComponent(job.title)}`);\n\n // BOFF-4195 — AI-assisted candidate matching (deterministic skill-overlap +\n // experience-fit scoring; see wspace-crewfoundry-svc matching.service.ts).\n const matches = jobPostingMatches[job.id];\n const matchesLoaded = matches !== undefined;\n const scoreTone = (score: number): string =>\n score >= 75 ? STATUS.success : score >= 45 ? STATUS.warning : STATUS.danger;\n const handleGenerateMatches = async () => {\n try {\n await fetchJobPostingMatches(job.id);\n } catch {\n showToast('Unable to generate candidate matches', 'error');\n }\n };\n\n return (\n <div className=\"space-y-6 p-6\">\n {/* Header */}\n <GlassCard treatment=\"glass\" glow className=\"border-border-seam\">\n <CardContent className=\"flex flex-col gap-4 p-6 lg:flex-row lg:items-start lg:justify-between\">\n <div className=\"min-w-0\">\n <div className=\"mb-1 flex flex-wrap items-center gap-2\">\n <h1 className=\"text-2xl font-semibold\">\n <GradientText>{job.title}</GradientText>\n </h1>\n <Badge variant={job.status === 'active' ? 'default' : 'secondary'}>\n {formatEnumLabel(job.status)}\n </Badge>\n </div>\n <div className=\"flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-text-secondary\">\n <span className=\"flex items-center gap-1\">\n <Briefcase className=\"h-3.5 w-3.5\" />\n {job.department}\n </span>\n <span className=\"flex items-center gap-1\">\n <MapPin className=\"h-3.5 w-3.5\" />\n {job.location}\n </span>\n <span>{formatEnumLabel(job.type)}</span>\n </div>\n <div className=\"mt-2 flex flex-wrap items-center gap-2 text-xs text-text-secondary\">\n <Badge variant=\"secondary\" className=\"text-[10px]\">\n {jobCandidates.length} applicants\n </Badge>\n {interviewCount > 0 && (\n <Badge variant=\"secondary\" className=\"text-[10px]\">\n {interviewCount} in interview\n </Badge>\n )}\n <span className=\"flex items-center gap-1\">\n <Calendar className=\"h-3 w-3\" />\n Posted {job.postedDate}\n </span>\n </div>\n <PagePurpose className=\"mt-3\">\n Everything about this role in one place — review applicants, requirements, and\n compensation to decide who to interview and keep the posting current.\n </PagePurpose>\n </div>\n\n <div className=\"flex shrink-0 flex-col items-start gap-3 lg:items-end\">\n <div className=\"text-2xl font-bold tabular-nums text-text-primary\">\n ${(job.salaryMin / 1000).toFixed(0)}k – ${(job.salaryMax / 1000).toFixed(0)}k\n </div>\n <div className=\"flex flex-wrap gap-2\">\n <Button variant=\"outline\" size=\"sm\" onClick={handleShare}>\n <Share2 className=\"mr-1 h-4 w-4\" />\n Share\n </Button>\n <Button variant=\"outline\" size=\"sm\" onClick={() => setEditOpen(true)}>\n <Pencil className=\"mr-1 h-4 w-4\" />\n Edit\n </Button>\n <Button\n variant=\"destructive\"\n size=\"sm\"\n aria-label=\"Delete job posting\"\n onClick={() => setDeleteOpen(true)}\n >\n <Trash2 className=\"h-4 w-4\" />\n </Button>\n <Button size=\"sm\" onClick={viewApplications}>\n <Users className=\"mr-1 h-4 w-4\" />\n View Applications\n </Button>\n </div>\n </div>\n </CardContent>\n </GlassCard>\n\n {/* Two-column body */}\n <div className=\"grid gap-6 lg:grid-cols-3\">\n {/* Left: description / requirements / benefits */}\n <div className=\"space-y-6 lg:col-span-2\">\n <Card>\n <CardHeader>\n <CardTitle>Description</CardTitle>\n </CardHeader>\n <CardContent>\n <p className=\"text-sm leading-relaxed text-text-secondary\">{job.description}</p>\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>Requirements</CardTitle>\n </CardHeader>\n <CardContent>\n {job.requirements.length === 0 ? (\n <p className=\"text-sm text-text-muted\">No requirements listed.</p>\n ) : (\n <ul className=\"space-y-2\">\n {job.requirements.map((req, idx) => (\n <li key={idx} className=\"flex items-start gap-2 text-sm\">\n <CheckCircle className=\"mt-0.5 h-4 w-4 flex-shrink-0 text-status-success-text\" />\n {req}\n </li>\n ))}\n </ul>\n )}\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>Benefits</CardTitle>\n </CardHeader>\n <CardContent>\n {job.benefits.length === 0 ? (\n <p className=\"text-sm text-text-muted\">No benefits listed.</p>\n ) : (\n <div className=\"flex flex-wrap gap-2\">\n {job.benefits.map((ben, idx) => (\n <Badge key={idx} variant=\"outline\" className=\"rounded-full px-3 py-1 text-xs\">\n {ben}\n </Badge>\n ))}\n </div>\n )}\n </CardContent>\n </Card>\n\n {/* BOFF-4195 — AI-assisted candidate matching (deterministic, explainable) */}\n <Card>\n <CardHeader className=\"flex flex-row items-center justify-between gap-3\">\n <div>\n <CardTitle>Suggested Matches</CardTitle>\n <p className=\"mt-1 text-xs text-text-muted\">\n Rule-based scoring — skill overlap (70%) + experience fit (30%) against this\n role's requirements. Not an AI-generated summary.\n </p>\n </div>\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={handleGenerateMatches}\n disabled={jobPostingMatchesLoading}\n >\n <Sparkles className=\"mr-1 h-4 w-4\" />\n {matchesLoaded ? 'Refresh matches' : 'Generate matches'}\n </Button>\n </CardHeader>\n <CardContent>\n {jobPostingMatchesLoading ? (\n <div className=\"flex items-center justify-center py-6\">\n <Spinner size=\"md\" />\n </div>\n ) : !matchesLoaded ? (\n <p className=\"text-sm text-text-muted\">\n Click "Generate matches" to rank the candidate pool against this\n posting's requirements and experience level.\n </p>\n ) : matches.length === 0 ? (\n <p className=\"text-sm text-text-muted\">\n No candidates in the pool right now (excluding already-hired/rejected).\n </p>\n ) : (\n <ul className=\"space-y-4\">\n {matches.map((match) => (\n <li\n key={match.candidateId}\n className=\"rounded-[var(--radius-card)] border border-border-subtle bg-bg-surface p-3\"\n >\n <div className=\"flex items-center gap-2\">\n <InitialsAvatar\n name={`${match.candidate.firstName} ${match.candidate.lastName}`}\n size={28}\n />\n <span className=\"min-w-0 flex-1 truncate text-sm font-medium text-text-primary\">\n {match.candidate.firstName} {match.candidate.lastName}\n </span>\n <Badge\n variant={STAGE_BADGE[match.candidate.status]}\n className=\"shrink-0 text-[9px]\"\n >\n {formatEnumLabel(match.candidate.status)}\n </Badge>\n </div>\n\n <div className=\"mt-2\">\n <SkillBar\n label=\"Overall match\"\n value={match.score}\n levelLabel={`${match.score}%`}\n color={scoreTone(match.score)}\n height={6}\n />\n </div>\n <div className=\"mt-2 grid grid-cols-2 gap-2 text-xs text-text-secondary\">\n <span>Skills: {match.skillScore}%</span>\n <span>Experience: {match.experienceScore}%</span>\n </div>\n\n {(match.matchedSkills.length > 0 || match.missingSkills.length > 0) && (\n <div className=\"mt-2 flex flex-wrap gap-1\">\n {match.matchedSkills.map((skill) => (\n <Badge\n key={`m-${skill}`}\n variant=\"secondary\"\n className=\"gap-1 text-[10px]\"\n >\n <CheckCircle className=\"h-3 w-3 text-status-success-text\" />\n {skill}\n </Badge>\n ))}\n {match.missingSkills.map((skill) => (\n <Badge\n key={`x-${skill}`}\n variant=\"outline\"\n className=\"gap-1 text-[10px] text-text-muted\"\n >\n <XCircle className=\"h-3 w-3 text-status-error-text\" />\n {skill}\n </Badge>\n ))}\n </div>\n )}\n\n {/* Rationale is always visible — the ranking is never a black box. */}\n <p className=\"mt-2 text-xs leading-relaxed text-text-muted\">\n {match.rationale}\n </p>\n </li>\n ))}\n </ul>\n )}\n </CardContent>\n </Card>\n </div>\n\n {/* Right rail: candidates + funnel */}\n <div className=\"space-y-6\">\n <Card>\n <CardHeader>\n <CardTitle>Candidates</CardTitle>\n </CardHeader>\n <CardContent>\n {jobCandidates.length === 0 ? (\n <p className=\"text-sm text-text-muted\">No applicants matched to this role yet.</p>\n ) : (\n <ul className=\"max-h-96 space-y-1 overflow-y-auto pr-1\">\n {jobCandidates.map((candidate) => (\n <li\n key={candidate.id}\n className=\"flex items-center gap-2 rounded-[var(--radius-input)] px-2 py-2 hover:bg-bg-sunken\"\n >\n <InitialsAvatar\n name={`${candidate.firstName} ${candidate.lastName}`}\n size={30}\n />\n <span className=\"min-w-0 flex-1 truncate text-sm font-medium text-text-primary\">\n {candidate.firstName} {candidate.lastName}\n </span>\n <Badge\n variant={STAGE_BADGE[candidate.status]}\n className=\"shrink-0 text-[9px]\"\n >\n {formatEnumLabel(candidate.status)}\n </Badge>\n <RatingStars value={candidate.rating} size={12} className=\"shrink-0\" />\n </li>\n ))}\n </ul>\n )}\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>Hiring funnel</CardTitle>\n </CardHeader>\n <CardContent>\n {jobCandidates.length === 0 ? (\n <p className=\"text-sm text-text-muted\">\n The funnel appears once candidates apply to this role.\n </p>\n ) : (\n <HBarFunnel\n stages={funnelStages}\n valueFormatter={(value) => String(value)}\n showConversion\n />\n )}\n </CardContent>\n </Card>\n </div>\n </div>\n\n <FormModal\n open={editOpen}\n onOpenChange={setEditOpen}\n title=\"Edit Job Posting\"\n description=\"Update the role details, compensation, and status.\"\n fields={JOB_FIELDS}\n initialValues={editInitial}\n submitLabel=\"Save changes\"\n onSubmit={handleEditSubmit}\n />\n <ConfirmDialog\n open={deleteOpen}\n onOpenChange={setDeleteOpen}\n title=\"Delete job posting\"\n description={`Permanently remove “${job.title}”? This cannot be undone.`}\n confirmLabel=\"Delete\"\n destructive\n onConfirm={handleDelete}\n />\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AA0CA,IAAM,IAA4B;CAChC;EAAE,MAAM;EAAS,OAAO;EAAa,MAAM;EAAQ,UAAU;EAAM,SAAS;EAAG;CAC/E;EAAE,MAAM;EAAc,OAAO;EAAc,MAAM;EAAQ,UAAU;EAAM;CACzE;EAAE,MAAM;EAAY,OAAO;EAAY,MAAM;EAAQ,UAAU;EAAM;CACrE;EACE,MAAM;EACN,OAAO;EACP,MAAM;EACN,SAAS;GACP;IAAE,OAAO;IAAa,OAAO;IAAa;GAC1C;IAAE,OAAO;IAAa,OAAO;IAAa;GAC1C;IAAE,OAAO;IAAY,OAAO;IAAY;GACzC;EACF;CACD;EACE,MAAM;EACN,OAAO;EACP,MAAM;EACN,SAAS;GACP;IAAE,OAAO;IAAS,OAAO;IAAS;GAClC;IAAE,OAAO;IAAO,OAAO;IAAO;GAC9B;IAAE,OAAO;IAAU,OAAO;IAAU;GACpC;IAAE,OAAO;IAAQ,OAAO;IAAQ;GAChC;IAAE,OAAO;IAAa,OAAO;IAAa;GAC3C;EACF;CACD;EACE,MAAM;EACN,OAAO;EACP,MAAM;EACN,SAAS;GACP;IAAE,OAAO;IAAS,OAAO;IAAS;GAClC;IAAE,OAAO;IAAU,OAAO;IAAU;GACpC;IAAE,OAAO;IAAU,OAAO;IAAU;GACpC;IAAE,OAAO;IAAU,OAAO;IAAU;GACpC;IAAE,OAAO;IAAU,OAAO;IAAU;GACrC;EACF;CACD;EAAE,MAAM;EAAa,OAAO;EAAkB,MAAM;EAAU;CAC9D;EAAE,MAAM;EAAa,OAAO;EAAkB,MAAM;EAAU;CAC9D;EAAE,MAAM;EAAY,OAAO;EAAwB,MAAM;EAAQ;CACjE;EAAE,MAAM;EAAe,OAAO;EAAe,MAAM;EAAY,SAAS;EAAG;CAC3E;EAAE,MAAM;EAAgB,OAAO;EAAgB,MAAM;EAAQ;CAC7D;EAAE,MAAM;EAAY,OAAO;EAAY,MAAM;EAAQ;CACtD,EAEK,IAGF;CACF,KAAK;CACL,WAAW;CACX,WAAW;CACX,YAAY;CACZ,OAAO;CACP,OAAO;CACP,UAAU;CACX;AAED,SAAS,EAAe,EAAE,SAAM,UAAO,MAAuC;AAC5E,QACE,kBAAC,QAAD;EACE,WAAU;EACV,OAAO;GAAE,OAAO;GAAM,QAAQ;GAAM,UAAU,KAAK,MAAM,IAAO,IAAK;GAAE;EACvE,eAAY;YAEX,EAAY,EAAK;EACb,CAAA;;AAIX,SAAgB,IAA2B;CACzC,IAAM,IAAW,GAAa,EACxB,CAAC,KAAgB,IAAiB,EAClC,EAAE,iBAAc,GAAa,EAC7B,EACJ,gBACA,eACA,qBACA,qBACA,gBACA,sBACA,6BACA,8BACE,GAAqB,EACnB,IAAQ,EAAa,IAAI,KAAK,EAC9B,IAAM,IAAQ,EAAY,MAAM,MAAM,EAAE,OAAO,EAAM,GAAG,EAAY,IACpE,CAAC,GAAU,KAAe,EAAS,GAAM,EACzC,CAAC,IAAY,KAAiB,EAAS,GAAM;AAEnD,KAAI,CAAC,KAAe,CAAC,EACnB,QACE,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,GAAD,EAAS,MAAK,MAAO,CAAA;EACjB,CAAA;AAIV,KAAI,CAAC,EACH,QACE,kBAAC,OAAD;EAAK,WAAU;YACb,kBAAC,GAAD;GACE,cAAa;GACb,OAAM;GACN,aAAY;GACZ,CAAA;EACE,CAAA;CAMV,IAAM,KAAW,EAAI,MAAM,MAAM,CAAC,aAAa,EACzC,IAAgB,EAAW,QAAQ,MAAM,EAAE,SAAS,MAAM,CAAC,aAAa,KAAK,GAAS,EACtF,KAAW,MACf,EAAc,QAAQ,MAAM,EAAE,WAAW,EAAO,CAAC,QAE7C,IAAiB,EAAQ,YAAY,EAIrC,KAA+B;EACnC;GAAE,OAAO;GAAW,OAAO,EAAc;GAAQ;EACjD;GAAE,OAAO;GAAa,OAAO,EAAQ,YAAY;GAAE;EACnD;GAAE,OAAO;GAAa,OAAO;GAAgB;EAC7C;GAAE,OAAO;GAAc,OAAO,EAAQ,aAAa;GAAE;EACrD;GAAE,OAAO;GAAS,OAAO,EAAQ,QAAQ,GAAG,EAAQ,QAAQ;GAAE;EAC/D,EAEK,KAA0B;EAC9B,OAAO,EAAI;EACX,YAAY,EAAI;EAChB,UAAU,EAAI;EACd,MAAM,EAAI;EACV,iBAAiB,EAAI;EACrB,QAAQ,EAAI;EACZ,WAAW,EAAI;EACf,WAAW,EAAI;EACf,UAAU,EAAI;EACd,aAAa,EAAI;EACjB,cAAc,EAAI;EAClB,UAAU,EAAI;EACf,EAEK,KAAmB,OAAO,MAAuB;EACrD,IAAM,IAA+B;GACnC,OAAO,EAAQ,GAAQ,QAAQ;GAC/B,YAAY,EAAQ,GAAQ,aAAa;GACzC,UAAU,EAAQ,GAAQ,WAAW;GACrC,MAAM,EAAO;GACb,iBAAiB,EAAO;GACxB,QAAQ,EAAO;GACf,WAAW,EAAU,GAAQ,YAAY;GACzC,WAAW,EAAU,GAAQ,YAAY;GACzC,UAAU,EAAQ,GAAQ,WAAW;GACrC,aAAa,EAAQ,GAAQ,cAAc;GAC3C,cAAc,EAAQ,GAAQ,eAAe;GAC7C,UAAU,EAAQ,GAAQ,WAAW;GACtC;AAED,EADA,MAAM,EAAiB,EAAI,IAAI,EAAQ,EACvC,EAAU,uBAAuB,UAAU;IAGvC,KAAe,YAAY;AAG/B,EAFA,MAAM,EAAiB,EAAI,GAAG,EAC9B,EAAU,uBAAuB,UAAU,EAC3C,EAAS,sBAAsB;IAG3B,KAAc,YAAY;AAC9B,MAAI;AAEF,GADA,MAAM,UAAU,UAAU,UAAU,OAAO,SAAS,KAAK,EACzD,EAAU,gCAAgC,UAAU;UAC9C;AACN,KAAU,+BAA+B,QAAQ;;IAI/C,WACJ,EAAS,wCAAwC,mBAAmB,EAAI,MAAM,GAAG,EAI7E,IAAU,EAAkB,EAAI,KAChC,IAAgB,MAAY,KAAA,GAC5B,MAAa,MACjB,KAAS,KAAK,EAAO,UAAU,KAAS,KAAK,EAAO,UAAU,EAAO;AASvE,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GAEE,kBAAC,IAAD;IAAW,WAAU;IAAQ,MAAA;IAAK,WAAU;cAC1C,kBAAC,GAAD;KAAa,WAAU;eAAvB,CACE,kBAAC,OAAD;MAAK,WAAU;gBAAf;OACE,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,MAAD;SAAI,WAAU;mBACZ,kBAAC,GAAD,EAAA,UAAe,EAAI,OAAqB,CAAA;SACrC,CAAA,EACL,kBAAC,GAAD;SAAO,SAAS,EAAI,WAAW,WAAW,YAAY;mBACnD,EAAgB,EAAI,OAAO;SACtB,CAAA,CACJ;;OACN,kBAAC,OAAD;QAAK,WAAU;kBAAf;SACE,kBAAC,QAAD;UAAM,WAAU;oBAAhB,CACE,kBAAC,IAAD,EAAW,WAAU,eAAgB,CAAA,EACpC,EAAI,WACA;;SACP,kBAAC,QAAD;UAAM,WAAU;oBAAhB,CACE,kBAAC,IAAD,EAAQ,WAAU,eAAgB,CAAA,EACjC,EAAI,SACA;;SACP,kBAAC,QAAD,EAAA,UAAO,EAAgB,EAAI,KAAK,EAAQ,CAAA;SACpC;;OACN,kBAAC,OAAD;QAAK,WAAU;kBAAf;SACE,kBAAC,GAAD;UAAO,SAAQ;UAAY,WAAU;oBAArC,CACG,EAAc,QAAO,cAChB;;SACP,IAAiB,KAChB,kBAAC,GAAD;UAAO,SAAQ;UAAY,WAAU;oBAArC,CACG,GAAe,gBACV;;SAEV,kBAAC,QAAD;UAAM,WAAU;oBAAhB;WACE,kBAAC,IAAD,EAAU,WAAU,WAAY,CAAA;;WACxB,EAAI;WACP;;SACH;;OACN,kBAAC,GAAD;QAAa,WAAU;kBAAO;QAGhB,CAAA;OACV;SAEN,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf;QAAmE;SAC9D,EAAI,YAAY,KAAM,QAAQ,EAAE;QAAC;SAAO,EAAI,YAAY,KAAM,QAAQ,EAAE;QAAC;QACxE;UACN,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,GAAD;SAAQ,SAAQ;SAAU,MAAK;SAAK,SAAS;mBAA7C,CACE,kBAAC,GAAD,EAAQ,WAAU,gBAAiB,CAAA,EAAA,QAE5B;;QACT,kBAAC,GAAD;SAAQ,SAAQ;SAAU,MAAK;SAAK,eAAe,EAAY,GAAK;mBAApE,CACE,kBAAC,GAAD,EAAQ,WAAU,gBAAiB,CAAA,EAAA,OAE5B;;QACT,kBAAC,GAAD;SACE,SAAQ;SACR,MAAK;SACL,cAAW;SACX,eAAe,EAAc,GAAK;mBAElC,kBAAC,GAAD,EAAQ,WAAU,WAAY,CAAA;SACvB,CAAA;QACT,kBAAC,GAAD;SAAQ,MAAK;SAAK,SAAS;mBAA3B,CACE,kBAAC,GAAD,EAAO,WAAU,gBAAiB,CAAA,EAAA,oBAE3B;;QACL;SACF;QACM;;IACJ,CAAA;GAGZ,kBAAC,OAAD;IAAK,WAAU;cAAf,CAEE,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD,EAAA,UAAW,eAAuB,CAAA,EACvB,CAAA,EACb,kBAAC,GAAD,EAAA,UACE,kBAAC,KAAD;OAAG,WAAU;iBAA+C,EAAI;OAAgB,CAAA,EACpE,CAAA,CACT,EAAA,CAAA;MAEP,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD,EAAA,UAAW,gBAAwB,CAAA,EACxB,CAAA,EACb,kBAAC,GAAD,EAAA,UACG,EAAI,aAAa,WAAW,IAC3B,kBAAC,KAAD;OAAG,WAAU;iBAA0B;OAA2B,CAAA,GAElE,kBAAC,MAAD;OAAI,WAAU;iBACX,EAAI,aAAa,KAAK,GAAK,MAC1B,kBAAC,MAAD;QAAc,WAAU;kBAAxB,CACE,kBAAC,GAAD,EAAa,WAAU,yDAA0D,CAAA,EAChF,EACE;UAHI,EAGJ,CACL;OACC,CAAA,EAEK,CAAA,CACT,EAAA,CAAA;MAEP,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD,EAAA,UAAW,YAAoB,CAAA,EACpB,CAAA,EACb,kBAAC,GAAD,EAAA,UACG,EAAI,SAAS,WAAW,IACvB,kBAAC,KAAD;OAAG,WAAU;iBAA0B;OAAuB,CAAA,GAE9D,kBAAC,OAAD;OAAK,WAAU;iBACZ,EAAI,SAAS,KAAK,GAAK,MACtB,kBAAC,GAAD;QAAiB,SAAQ;QAAU,WAAU;kBAC1C;QACK,EAFI,EAEJ,CACR;OACE,CAAA,EAEI,CAAA,CACT,EAAA,CAAA;MAGP,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD;OAAY,WAAU;iBAAtB,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UAAW,qBAA6B,CAAA,EACxC,kBAAC,KAAD;QAAG,WAAU;kBAA+B;QAGxC,CAAA,CACA,EAAA,CAAA,EACN,kBAAC,GAAD;QACE,MAAK;QACL,SAAQ;QACR,SApJgB,YAAY;AACxC,aAAI;AACF,gBAAM,EAAuB,EAAI,GAAG;iBAC9B;AACN,YAAU,wCAAwC,QAAQ;;;QAiJhD,UAAU;kBAJZ,CAME,kBAAC,GAAD,EAAU,WAAU,gBAAiB,CAAA,EACpC,IAAgB,oBAAoB,mBAC9B;UACE;UACb,kBAAC,GAAD,EAAA,UACG,IACC,kBAAC,OAAD;OAAK,WAAU;iBACb,kBAAC,GAAD,EAAS,MAAK,MAAO,CAAA;OACjB,CAAA,GACH,IAKD,EAAQ,WAAW,IACrB,kBAAC,KAAD;OAAG,WAAU;iBAA0B;OAEnC,CAAA,GAEJ,kBAAC,MAAD;OAAI,WAAU;iBACX,EAAQ,KAAK,MACZ,kBAAC,MAAD;QAEE,WAAU;kBAFZ;SAIE,kBAAC,OAAD;UAAK,WAAU;oBAAf;WACE,kBAAC,GAAD;YACE,MAAM,GAAG,EAAM,UAAU,UAAU,GAAG,EAAM,UAAU;YACtD,MAAM;YACN,CAAA;WACF,kBAAC,QAAD;YAAM,WAAU;sBAAhB;aACG,EAAM,UAAU;aAAU;aAAE,EAAM,UAAU;aACxC;;WACP,kBAAC,GAAD;YACE,SAAS,EAAY,EAAM,UAAU;YACrC,WAAU;sBAET,EAAgB,EAAM,UAAU,OAAO;YAClC,CAAA;WACJ;;SAEN,kBAAC,OAAD;UAAK,WAAU;oBACb,kBAAC,IAAD;WACE,OAAM;WACN,OAAO,EAAM;WACb,YAAY,GAAG,EAAM,MAAM;WAC3B,OAAO,GAAU,EAAM,MAAM;WAC7B,QAAQ;WACR,CAAA;UACE,CAAA;SACN,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,QAAD,EAAA,UAAA;WAAM;WAAS,EAAM;WAAW;WAAQ,EAAA,CAAA,EACxC,kBAAC,QAAD,EAAA,UAAA;WAAM;WAAa,EAAM;WAAgB;WAAQ,EAAA,CAAA,CAC7C;;UAEJ,EAAM,cAAc,SAAS,KAAK,EAAM,cAAc,SAAS,MAC/D,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACG,EAAM,cAAc,KAAK,MACxB,kBAAC,GAAD;WAEE,SAAQ;WACR,WAAU;qBAHZ,CAKE,kBAAC,GAAD,EAAa,WAAU,oCAAqC,CAAA,EAC3D,EACK;aAND,KAAK,IAMJ,CACR,EACD,EAAM,cAAc,KAAK,MACxB,kBAAC,GAAD;WAEE,SAAQ;WACR,WAAU;qBAHZ,CAKE,kBAAC,GAAD,EAAS,WAAU,kCAAmC,CAAA,EACrD,EACK;aAND,KAAK,IAMJ,CACR,CACE;;SAIR,kBAAC,KAAD;UAAG,WAAU;oBACV,EAAM;UACL,CAAA;SACD;UA9DE,EAAM,YA8DR,CACL;OACC,CAAA,GA5EL,kBAAC,KAAD;OAAG,WAAU;iBAA0B;OAGnC,CAAA,EA2EM,CAAA,CACT,EAAA,CAAA;MACH;QAGN,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD,EAAA,UAAW,cAAsB,CAAA,EACtB,CAAA,EACb,kBAAC,GAAD,EAAA,UACG,EAAc,WAAW,IACxB,kBAAC,KAAD;MAAG,WAAU;gBAA0B;MAA2C,CAAA,GAElF,kBAAC,MAAD;MAAI,WAAU;gBACX,EAAc,KAAK,MAClB,kBAAC,MAAD;OAEE,WAAU;iBAFZ;QAIE,kBAAC,GAAD;SACE,MAAM,GAAG,EAAU,UAAU,GAAG,EAAU;SAC1C,MAAM;SACN,CAAA;QACF,kBAAC,QAAD;SAAM,WAAU;mBAAhB;UACG,EAAU;UAAU;UAAE,EAAU;UAC5B;;QACP,kBAAC,GAAD;SACE,SAAS,EAAY,EAAU;SAC/B,WAAU;mBAET,EAAgB,EAAU,OAAO;SAC5B,CAAA;QACR,kBAAC,GAAD;SAAa,OAAO,EAAU;SAAQ,MAAM;SAAI,WAAU;SAAa,CAAA;QACpE;SAjBE,EAAU,GAiBZ,CACL;MACC,CAAA,EAEK,CAAA,CACT,EAAA,CAAA,EAEP,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD,EAAA,UAAW,iBAAyB,CAAA,EACzB,CAAA,EACb,kBAAC,GAAD,EAAA,UACG,EAAc,WAAW,IACxB,kBAAC,KAAD;MAAG,WAAU;gBAA0B;MAEnC,CAAA,GAEJ,kBAAC,GAAD;MACE,QAAQ;MACR,iBAAiB,MAAU,OAAO,EAAM;MACxC,gBAAA;MACA,CAAA,EAEQ,CAAA,CACT,EAAA,CAAA,CACH;OACF;;GAEN,kBAAC,IAAD;IACE,MAAM;IACN,cAAc;IACd,OAAM;IACN,aAAY;IACZ,QAAQ;IACR,eAAe;IACf,aAAY;IACZ,UAAU;IACV,CAAA;GACF,kBAAC,IAAD;IACE,MAAM;IACN,cAAc;IACd,OAAM;IACN,aAAa,uBAAuB,EAAI,MAAM;IAC9C,cAAa;IACb,aAAA;IACA,WAAW;IACX,CAAA;GACE"}
|
|
1
|
+
{"version":3,"file":"HiringJobDetailsPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/HiringJobDetailsPagePage.tsx"],"sourcesContent":["import {\n Badge,\n Button,\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n GlassCard,\n GradientText,\n IllustratedEmptyState,\n PagePurpose,\n Spinner,\n} from '@burdenoff/fe-libs/ui';\nimport { useState } from 'react';\nimport {\n Briefcase,\n Calendar,\n CheckCircle,\n Clock,\n MapPin,\n Pencil,\n Share2,\n Sparkles,\n Trash2,\n Users,\n XCircle,\n} from 'lucide-react';\nimport { useNavigate, useSearchParams } from 'react-router';\nimport { useGlobalUi } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { useCrewFoundryStore, type Candidate, type JobPosting } from '../store/crewfoundryStore';\nimport { FormModal, type FieldConfig, type FormValues } from '../components/forms/FormModal';\nimport { ConfirmDialog } from '../components/forms/ConfirmDialog';\nimport { getNumber, getTags, getText } from '../components/forms/formValues';\nimport { formatEnumLabel } from '../utils/formatEnumLabel';\nimport { localDayDiff, localTodayString, parseLocalDate } from '../utils/dateUtils';\nimport {\n HBarFunnel,\n RatingStars,\n SkillBar,\n STATUS,\n getInitials,\n type LabeledValue,\n} from '../components/viz';\n\nconst JOB_FIELDS: FieldConfig[] = [\n // Title and Department are always required (drafts and publishes alike).\n // Location, Type, Experience Level, and Description are only required\n // when the job is being actively published — enforced both via the\n // FormModal-level `requiredWhen` predicate (renders the `*` and blocks\n // submit) and via the submit-time guard in handleEditSubmit as a belt-\n // and-braces check. Title/department get the unconditional `required`\n // marker (FormModal's universal check fires regardless of status).\n { name: 'title', label: 'Job Title', type: 'text', required: true, colSpan: 2 },\n { name: 'department', label: 'Department', type: 'text', required: true },\n {\n name: 'location',\n label: 'Location',\n type: 'text',\n requiredWhen: (values) => String(values.status ?? '').toLowerCase() === 'active',\n },\n {\n name: 'type',\n label: 'Employment Type',\n type: 'select',\n requiredWhen: (values) => String(values.status ?? '').toLowerCase() === 'active',\n options: [\n { label: 'Full time', value: 'full_time' },\n { label: 'Part time', value: 'part_time' },\n { label: 'Contract', value: 'contract' },\n ],\n },\n {\n name: 'experienceLevel',\n label: 'Experience Level',\n type: 'select',\n requiredWhen: (values) => String(values.status ?? '').toLowerCase() === 'active',\n options: [\n { label: 'Entry', value: 'entry' },\n { label: 'Mid', value: 'mid' },\n { label: 'Senior', value: 'senior' },\n { label: 'Lead', value: 'lead' },\n { label: 'Executive', value: 'executive' },\n ],\n },\n {\n name: 'status',\n label: 'Status',\n type: 'select',\n options: [\n { label: 'Draft', value: 'draft' },\n { label: 'Active', value: 'active' },\n { label: 'Paused', value: 'paused' },\n { label: 'Closed', value: 'closed' },\n { label: 'Filled', value: 'filled' },\n ],\n },\n { name: 'salaryMin', label: 'Salary Min ($)', type: 'number' },\n { name: 'salaryMax', label: 'Salary Max ($)', type: 'number' },\n { name: 'deadline', label: 'Application Deadline', type: 'date' },\n {\n name: 'description',\n label: 'Description',\n type: 'textarea',\n colSpan: 2,\n requiredWhen: (values) => String(values.status ?? '').toLowerCase() === 'active',\n },\n { name: 'requirements', label: 'Requirements', type: 'tags' },\n { name: 'benefits', label: 'Benefits', type: 'tags' },\n];\n\nconst STAGE_BADGE: Record<\n Candidate['status'],\n 'default' | 'secondary' | 'destructive' | 'outline'\n> = {\n new: 'outline',\n screening: 'secondary',\n interview: 'secondary',\n assessment: 'secondary',\n offer: 'default',\n hired: 'default',\n rejected: 'destructive',\n};\n\nfunction InitialsAvatar({ name, size = 32 }: { name: string; size?: number }) {\n return (\n <span\n className=\"inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full border border-border-subtle bg-bg-sunken font-medium text-text-secondary\"\n style={{ width: size, height: size, fontSize: Math.round(size * 0.36) }}\n aria-hidden=\"true\"\n >\n {getInitials(name)}\n </span>\n );\n}\n\n/**\n * Format a salary value. The previous `(value / 1000).toFixed(0)` truncated\n * any value under 1000 to 0, so an entry-level posting (Min 12, Max 13)\n * displayed as the misleading \"$0k – $0k\". Apply per-value so a range can\n * legitimately span the 1000 threshold — e.g. Min 100, Max 1000 renders\n * as \"$100 – $1k\" rather than grouping them as \"$0k – $1k\". The 0 case is\n * special-cased to \"$0\" so unset salaries don't read as \"—\" or \"0k\".\n */\nfunction formatSalary(value: number): string {\n if (value === 0) return '$0';\n if (value < 1000) return `$${value.toLocaleString()}`;\n return `$${Math.round(value / 1000)}k`;\n}\n\nfunction formatSalaryRange(min: number, max: number): string {\n return `${formatSalary(min)} – ${formatSalary(max)}`;\n}\n\n/**\n * Deadline label from an ISO date-only string or null. Renders relative\n * phrasing when upcoming (\"Closes in 3 days\", \"Closes today\",\n * \"Closed yesterday\") and falls back to the raw date when unparseable.\n * Returns \"No deadline\" for null — matches the card on the\n * Jobs list page so users get a consistent read of a job's deadline.\n *\n * Compares against local calendar days (not raw timestamps) so a deadline\n * picked as \"today\" never reads as \"Closed yesterday\" for users in\n * negative-UTC offsets — `new Date('YYYY-MM-DD')` parses at UTC midnight\n * which would otherwise put it 17-20 hours in the past by local noon.\n */\nfunction formatDeadlineLabel(dateStr: string | null): string {\n if (!dateStr) {\n return 'No deadline';\n }\n const deadline = parseLocalDate(dateStr);\n if (!deadline) {\n return dateStr;\n }\n const days = localDayDiff(new Date(), deadline);\n if (days > 1) {\n return `Closes in ${days} days`;\n }\n if (days === 1) {\n return 'Closes tomorrow';\n }\n if (days === 0) {\n return 'Closes today';\n }\n if (days === -1) {\n return 'Closed yesterday';\n }\n return `Closed ${Math.abs(days)} days ago`;\n}\n\nexport function HiringJobDetailsPagePage() {\n const navigate = useNavigate();\n const [searchParams] = useSearchParams();\n const { showToast } = useGlobalUi();\n const {\n jobPostings,\n candidates,\n updateJobPosting,\n removeJobPosting,\n initialized,\n jobPostingMatches,\n jobPostingMatchesLoading,\n fetchJobPostingMatches,\n } = useCrewFoundryStore();\n const jobId = searchParams.get('id');\n const job = jobId ? jobPostings.find((j) => j.id === jobId) : jobPostings[0];\n const [editOpen, setEditOpen] = useState(false);\n const [deleteOpen, setDeleteOpen] = useState(false);\n // Mirrors the BOFF-7588 create-page validation contract for edits. The\n // showToast path here is currently a no-op (provider has no rendered\n // viewport — see the linear ticket); the modal-level errorMessage banner\n // is what the user actually sees.\n const [editError, setEditError] = useState<string | null>(null);\n // Local calendar date for the deadline `today` guard — see BOFF-7588.\n // Recomputed on every render AND on every submit (via localTodayString())\n // so the value stays current if the modal stays open across midnight.\n const today = localTodayString();\n\n if (!initialized && !job) {\n return (\n <div className=\"flex min-h-[60vh] items-center justify-center p-6\">\n <Spinner size=\"lg\" />\n </div>\n );\n }\n\n if (!job) {\n return (\n <div className=\"p-6\">\n <IllustratedEmptyState\n illustration=\"empty-search\"\n title=\"No job posting found\"\n description=\"This job posting is no longer available.\"\n />\n </div>\n );\n }\n\n // Applicants for this role. CrewCandidate has no jobId link, so match on the\n // (normalized) position title — the only real linkage the schema exposes.\n const titleKey = job.title.trim().toLowerCase();\n const jobCandidates = candidates.filter((c) => c.position.trim().toLowerCase() === titleKey);\n const countBy = (status: Candidate['status']) =>\n jobCandidates.filter((c) => c.status === status).length;\n\n const interviewCount = countBy('interview');\n\n // Real-data hiring funnel: \"Applied\" = everyone who applied to this role, then\n // the count currently at each downstream stage (offer folds in hired).\n const funnelStages: LabeledValue[] = [\n { label: 'Applied', value: jobCandidates.length },\n { label: 'Screening', value: countBy('screening') },\n { label: 'Interview', value: interviewCount },\n { label: 'Assessment', value: countBy('assessment') },\n { label: 'Offer', value: countBy('offer') + countBy('hired') },\n ];\n\n const editInitial: FormValues = {\n title: job.title,\n department: job.department,\n location: job.location,\n type: job.type,\n experienceLevel: job.experienceLevel,\n status: job.status,\n salaryMin: job.salaryMin,\n salaryMax: job.salaryMax,\n deadline: job.deadline ?? '',\n description: job.description,\n requirements: job.requirements,\n benefits: job.benefits,\n };\n\n const handleEditSubmit = async (values: FormValues) => {\n // Build errors map; mirror the create-form rule set (BOFF-7588):\n // title/department/deadline always; location/type/experienceLevel/description\n // when the job is being actively published (status === 'active').\n const errors: string[] = [];\n const title = getText(values, 'title').trim();\n const department = getText(values, 'department').trim();\n const location = getText(values, 'location').trim();\n const description = getText(values, 'description').trim();\n const deadline = getText(values, 'deadline').trim();\n if (!title) errors.push('Job Title is required');\n if (!department) errors.push('Department is required');\n if (deadline && deadline < localTodayString()) {\n errors.push('Application Deadline cannot be a past date');\n }\n if (String(values.status ?? '').toLowerCase() === 'active') {\n if (!location) errors.push('Location is required');\n if (!description) errors.push('Description is required');\n }\n const salaryMinValue = getNumber(values, 'salaryMin');\n const salaryMaxValue = getNumber(values, 'salaryMax');\n // No negatives — neither side of the range can be < 0.\n if (salaryMinValue < 0) {\n errors.push('Salary Min cannot be negative');\n } else if (salaryMaxValue < 0) {\n errors.push('Salary Max cannot be negative');\n } else {\n // Symmetric rule: either both fields are 0 (unset — display as $0),\n // or both must be filled coherently (Max >= Min, no zero-on-one-side\n // asymmetry that produced misleading \"0 to Xk\" displays). Catches\n // both directions: max set without min, and min set without max.\n const bothEmpty = salaryMinValue === 0 && salaryMaxValue === 0;\n if (!bothEmpty && salaryMaxValue === 0 && salaryMinValue > 0) {\n errors.push('Provide Salary Max, or leave both empty');\n } else if (!bothEmpty && salaryMaxValue > 0 && salaryMinValue === 0) {\n errors.push('Provide Salary Min, or leave both empty');\n } else if (!bothEmpty && salaryMaxValue > 0 && salaryMaxValue < salaryMinValue) {\n errors.push('Salary Max cannot be less than Salary Min');\n }\n }\n\n if (errors.length > 0) {\n // Only the banner — FormModal's catch will also call showToast, so we\n // suppress the explicit one to avoid a double-toast when the shared\n // viewport is fixed (BOFF-7584). The banner stays visible because we\n // throw, which skips FormModal's onOpenChange(false) and keeps the\n // modal open with the banner rendered.\n setEditError(errors[0]);\n // Throw so FormModal's onSubmit await rejects and skips its\n // `onOpenChange(false)`, keeping the modal open with the banner visible.\n // Without this, FormModal closes the modal immediately after the\n // Promise resolves, and the user sees \"saved but nothing happens\"\n // because the error banner never gets a frame to render.\n throw new Error(errors[0]);\n }\n setEditError(null);\n\n const updates: Partial<JobPosting> = {\n title,\n department,\n location,\n type: values.type as JobPosting['type'],\n experienceLevel: values.experienceLevel as JobPosting['experienceLevel'],\n status: values.status as JobPosting['status'],\n salaryMin: salaryMinValue,\n salaryMax: salaryMaxValue,\n // null = \"no deadline\", matching the create-page contract (BOFF-7588).\n deadline: deadline === '' ? null : deadline,\n description,\n requirements: getTags(values, 'requirements'),\n benefits: getTags(values, 'benefits'),\n };\n await updateJobPosting(job.id, updates);\n showToast('Job posting updated', 'success');\n setEditOpen(false);\n };\n\n const handleDelete = async () => {\n await removeJobPosting(job.id);\n showToast('Job posting deleted', 'success');\n navigate('../hiring/jobs-page');\n };\n\n const handleShare = async () => {\n try {\n await navigator.clipboard.writeText(window.location.href);\n showToast('Job link copied to clipboard', 'success');\n } catch {\n showToast('Could not copy the job link', 'error');\n }\n };\n\n const viewApplications = () =>\n navigate(`../hiring/applications-page?position=${encodeURIComponent(job.title)}`);\n\n // BOFF-4195 — AI-assisted candidate matching (deterministic skill-overlap +\n // experience-fit scoring; see wspace-crewfoundry-svc matching.service.ts).\n const matches = jobPostingMatches[job.id];\n const matchesLoaded = matches !== undefined;\n const scoreTone = (score: number): string =>\n score >= 75 ? STATUS.success : score >= 45 ? STATUS.warning : STATUS.danger;\n const handleGenerateMatches = async () => {\n try {\n await fetchJobPostingMatches(job.id);\n } catch {\n showToast('Unable to generate candidate matches', 'error');\n }\n };\n\n return (\n <div className=\"space-y-6 p-6\">\n {/* Header */}\n <GlassCard treatment=\"glass\" glow className=\"border-border-seam\">\n <CardContent className=\"flex flex-col gap-4 p-6 lg:flex-row lg:items-start lg:justify-between\">\n <div className=\"min-w-0\">\n <div className=\"mb-1 flex flex-wrap items-center gap-2\">\n <h1 className=\"text-2xl font-semibold\">\n <GradientText>{job.title}</GradientText>\n </h1>\n <Badge variant={job.status === 'active' ? 'default' : 'secondary'}>\n {formatEnumLabel(job.status)}\n </Badge>\n </div>\n <div className=\"flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-text-secondary\">\n <span className=\"flex items-center gap-1\">\n <Briefcase className=\"h-3.5 w-3.5\" />\n {job.department}\n </span>\n <span className=\"flex items-center gap-1\">\n <MapPin className=\"h-3.5 w-3.5\" />\n {job.location}\n </span>\n <span>{formatEnumLabel(job.type)}</span>\n </div>\n <div className=\"mt-2 flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-text-secondary\">\n <Badge variant=\"secondary\" className=\"text-[10px]\">\n {jobCandidates.length} applicants\n </Badge>\n {interviewCount > 0 && (\n <Badge variant=\"secondary\" className=\"text-[10px]\">\n {interviewCount} in interview\n </Badge>\n )}\n <span className=\"flex items-center gap-1\">\n <Calendar className=\"h-3 w-3\" />\n Posted {job.postedDate}\n </span>\n <span className=\"flex items-center gap-1 text-text-secondary\">\n <Clock className=\"h-3 w-3\" />\n {formatDeadlineLabel(job.deadline)}\n </span>\n </div>\n <PagePurpose className=\"mt-3\">\n Everything about this role in one place — review applicants, requirements, and\n compensation to decide who to interview and keep the posting current.\n </PagePurpose>\n </div>\n\n <div className=\"flex shrink-0 flex-col items-start gap-3 lg:items-end\">\n <div className=\"text-2xl font-bold tabular-nums text-text-primary\">\n {formatSalaryRange(job.salaryMin, job.salaryMax)}\n </div>\n <div className=\"flex flex-wrap gap-2\">\n <Button variant=\"outline\" size=\"sm\" onClick={handleShare}>\n <Share2 className=\"mr-1 h-4 w-4\" />\n Share\n </Button>\n <Button variant=\"outline\" size=\"sm\" onClick={() => setEditOpen(true)}>\n <Pencil className=\"mr-1 h-4 w-4\" />\n Edit\n </Button>\n <Button\n variant=\"destructive\"\n size=\"sm\"\n aria-label=\"Delete job posting\"\n onClick={() => setDeleteOpen(true)}\n >\n <Trash2 className=\"h-4 w-4\" />\n </Button>\n <Button size=\"sm\" onClick={viewApplications}>\n <Users className=\"mr-1 h-4 w-4\" />\n View Applications\n </Button>\n </div>\n </div>\n </CardContent>\n </GlassCard>\n\n {/* Two-column body */}\n <div className=\"grid gap-6 lg:grid-cols-3\">\n {/* Left: description / requirements / benefits */}\n <div className=\"space-y-6 lg:col-span-2\">\n <Card>\n <CardHeader>\n <CardTitle>Description</CardTitle>\n </CardHeader>\n <CardContent>\n <p className=\"text-sm leading-relaxed text-text-secondary\">{job.description}</p>\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>Requirements</CardTitle>\n </CardHeader>\n <CardContent>\n {job.requirements.length === 0 ? (\n <p className=\"text-sm text-text-muted\">No requirements listed.</p>\n ) : (\n <ul className=\"space-y-2\">\n {job.requirements.map((req, idx) => (\n <li key={idx} className=\"flex items-start gap-2 text-sm\">\n <CheckCircle className=\"mt-0.5 h-4 w-4 flex-shrink-0 text-status-success-text\" />\n {req}\n </li>\n ))}\n </ul>\n )}\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>Benefits</CardTitle>\n </CardHeader>\n <CardContent>\n {job.benefits.length === 0 ? (\n <p className=\"text-sm text-text-muted\">No benefits listed.</p>\n ) : (\n <div className=\"flex flex-wrap gap-2\">\n {job.benefits.map((ben, idx) => (\n <Badge key={idx} variant=\"outline\" className=\"rounded-full px-3 py-1 text-xs\">\n {ben}\n </Badge>\n ))}\n </div>\n )}\n </CardContent>\n </Card>\n\n {/* BOFF-4195 — AI-assisted candidate matching (deterministic, explainable) */}\n <Card>\n <CardHeader className=\"flex flex-row items-center justify-between gap-3\">\n <div>\n <CardTitle>Suggested Matches</CardTitle>\n <p className=\"mt-1 text-xs text-text-muted\">\n Rule-based scoring — skill overlap (70%) + experience fit (30%) against this\n role's requirements. Not an AI-generated summary.\n </p>\n </div>\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={handleGenerateMatches}\n disabled={jobPostingMatchesLoading}\n >\n <Sparkles className=\"mr-1 h-4 w-4\" />\n {matchesLoaded ? 'Refresh matches' : 'Generate matches'}\n </Button>\n </CardHeader>\n <CardContent>\n {jobPostingMatchesLoading ? (\n <div className=\"flex items-center justify-center py-6\">\n <Spinner size=\"md\" />\n </div>\n ) : !matchesLoaded ? (\n <p className=\"text-sm text-text-muted\">\n Click "Generate matches" to rank the candidate pool against this\n posting's requirements and experience level.\n </p>\n ) : matches.length === 0 ? (\n <p className=\"text-sm text-text-muted\">\n No candidates in the pool right now (excluding already-hired/rejected).\n </p>\n ) : (\n <ul className=\"space-y-4\">\n {matches.map((match) => (\n <li\n key={match.candidateId}\n className=\"rounded-[var(--radius-card)] border border-border-subtle bg-bg-surface p-3\"\n >\n <div className=\"flex items-center gap-2\">\n <InitialsAvatar\n name={`${match.candidate.firstName} ${match.candidate.lastName}`}\n size={28}\n />\n <span className=\"min-w-0 flex-1 truncate text-sm font-medium text-text-primary\">\n {match.candidate.firstName} {match.candidate.lastName}\n </span>\n <Badge\n variant={STAGE_BADGE[match.candidate.status]}\n className=\"shrink-0 text-[9px]\"\n >\n {formatEnumLabel(match.candidate.status)}\n </Badge>\n </div>\n\n <div className=\"mt-2\">\n <SkillBar\n label=\"Overall match\"\n value={match.score}\n levelLabel={`${match.score}%`}\n color={scoreTone(match.score)}\n height={6}\n />\n </div>\n <div className=\"mt-2 grid grid-cols-2 gap-2 text-xs text-text-secondary\">\n <span>Skills: {match.skillScore}%</span>\n <span>Experience: {match.experienceScore}%</span>\n </div>\n\n {(match.matchedSkills.length > 0 || match.missingSkills.length > 0) && (\n <div className=\"mt-2 flex flex-wrap gap-1\">\n {match.matchedSkills.map((skill) => (\n <Badge\n key={`m-${skill}`}\n variant=\"secondary\"\n className=\"gap-1 text-[10px]\"\n >\n <CheckCircle className=\"h-3 w-3 text-status-success-text\" />\n {skill}\n </Badge>\n ))}\n {match.missingSkills.map((skill) => (\n <Badge\n key={`x-${skill}`}\n variant=\"outline\"\n className=\"gap-1 text-[10px] text-text-muted\"\n >\n <XCircle className=\"h-3 w-3 text-status-error-text\" />\n {skill}\n </Badge>\n ))}\n </div>\n )}\n\n {/* Rationale is always visible — the ranking is never a black box. */}\n <p className=\"mt-2 text-xs leading-relaxed text-text-muted\">\n {match.rationale}\n </p>\n </li>\n ))}\n </ul>\n )}\n </CardContent>\n </Card>\n </div>\n\n {/* Right rail: candidates + funnel */}\n <div className=\"space-y-6\">\n <Card>\n <CardHeader>\n <CardTitle>Candidates</CardTitle>\n </CardHeader>\n <CardContent>\n {jobCandidates.length === 0 ? (\n <p className=\"text-sm text-text-muted\">No applicants matched to this role yet.</p>\n ) : (\n <ul className=\"max-h-96 space-y-1 overflow-y-auto pr-1\">\n {jobCandidates.map((candidate) => (\n <li\n key={candidate.id}\n className=\"flex items-center gap-2 rounded-[var(--radius-input)] px-2 py-2 hover:bg-bg-sunken\"\n >\n <InitialsAvatar\n name={`${candidate.firstName} ${candidate.lastName}`}\n size={30}\n />\n <span className=\"min-w-0 flex-1 truncate text-sm font-medium text-text-primary\">\n {candidate.firstName} {candidate.lastName}\n </span>\n <Badge\n variant={STAGE_BADGE[candidate.status]}\n className=\"shrink-0 text-[9px]\"\n >\n {formatEnumLabel(candidate.status)}\n </Badge>\n <RatingStars value={candidate.rating} size={12} className=\"shrink-0\" />\n </li>\n ))}\n </ul>\n )}\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>Hiring funnel</CardTitle>\n </CardHeader>\n <CardContent>\n {jobCandidates.length === 0 ? (\n <p className=\"text-sm text-text-muted\">\n The funnel appears once candidates apply to this role.\n </p>\n ) : (\n <HBarFunnel\n stages={funnelStages}\n valueFormatter={(value) => String(value)}\n showConversion\n />\n )}\n </CardContent>\n </Card>\n </div>\n </div>\n\n <FormModal\n open={editOpen}\n onOpenChange={(open) => {\n setEditOpen(open);\n setEditError(null);\n }}\n title=\"Edit Job Posting\"\n description=\"Update the role details, compensation, and status.\"\n errorMessage={editError ?? undefined}\n onValidationError={(msg) => setEditError(msg)}\n fields={JOB_FIELDS.map((field) =>\n field.name === 'deadline' ? { ...field, min: today } : field\n )}\n initialValues={editInitial}\n submitLabel=\"Save changes\"\n onSubmit={handleEditSubmit}\n />\n <ConfirmDialog\n open={deleteOpen}\n onOpenChange={setDeleteOpen}\n title=\"Delete job posting\"\n description={`Permanently remove “${job.title}”? This cannot be undone.`}\n confirmLabel=\"Delete\"\n destructive\n onConfirm={handleDelete}\n />\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HiringJobsPagePage.d.ts","sourceRoot":"","sources":["../../../src/crewfoundry/pages/HiringJobsPagePage.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"HiringJobsPagePage.d.ts","sourceRoot":"","sources":["../../../src/crewfoundry/pages/HiringJobsPagePage.tsx"],"names":[],"mappings":"AAiHA,wBAAgB,kBAAkB,gCA+MjC"}
|
|
@@ -4,21 +4,22 @@ import { useTr as n } from "../../shared/hooks/useTr.js";
|
|
|
4
4
|
import { AvatarStack as r } from "../components/viz/AvatarStack.js";
|
|
5
5
|
import { SegmentedTabs as i } from "../components/viz/SegmentedTabs.js";
|
|
6
6
|
import "../components/viz/index.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
7
|
+
import { localDayDiff as a, parseLocalDate as o } from "../utils/dateUtils.js";
|
|
8
|
+
import { useMemo as s, useState as c } from "react";
|
|
9
|
+
import { Briefcase as l, DollarSign as u, MapPin as d, Plus as f, Search as p, Users as m } from "lucide-react";
|
|
10
|
+
import { useNavigate as h } from "react-router";
|
|
11
|
+
import { jsx as g, jsxs as _ } from "react/jsx-runtime";
|
|
12
|
+
import { Badge as v, Button as y, CardContent as b, CardHeader as x, CardTitle as S, GlassCard as C, GradientText as w, IllustratedEmptyState as T, Input as E, PagePurpose as D, Pagination as O } from "@burdenoff/fe-libs/ui";
|
|
13
|
+
import { usePagination as k } from "@burdenoff/fe-libs/shared/hooks";
|
|
13
14
|
//#region src/crewfoundry/pages/HiringJobsPagePage.tsx
|
|
14
|
-
var
|
|
15
|
+
var A = [
|
|
15
16
|
"active",
|
|
16
17
|
"draft",
|
|
17
18
|
"paused",
|
|
18
19
|
"closed",
|
|
19
20
|
"filled"
|
|
20
21
|
];
|
|
21
|
-
function
|
|
22
|
+
function j(e) {
|
|
22
23
|
let t = new Date(e);
|
|
23
24
|
if (Number.isNaN(t.getTime())) return e;
|
|
24
25
|
let n = Math.floor((Date.now() - t.getTime()) / 864e5);
|
|
@@ -32,37 +33,50 @@ function k(e) {
|
|
|
32
33
|
let r = Math.floor(n / 30);
|
|
33
34
|
return r === 1 ? "1 month ago" : `${r} months ago`;
|
|
34
35
|
}
|
|
35
|
-
function
|
|
36
|
-
return
|
|
36
|
+
function M(e) {
|
|
37
|
+
return e === 0 ? "$0" : e < 1e3 ? `$${e.toLocaleString()}` : `$${Math.round(e / 1e3)}k`;
|
|
38
|
+
}
|
|
39
|
+
function N(e) {
|
|
40
|
+
if (!e) return "No deadline";
|
|
41
|
+
let t = o(e);
|
|
42
|
+
if (!t) return e;
|
|
43
|
+
let n = a(/* @__PURE__ */ new Date(), t);
|
|
44
|
+
return n > 1 ? `Closes in ${n} days` : n === 1 ? "Closes tomorrow" : n === 0 ? "Closes today" : n === -1 ? "Closed yesterday" : `Closed ${Math.abs(n)} days ago`;
|
|
45
|
+
}
|
|
46
|
+
function P(e, t) {
|
|
47
|
+
return `${M(e)} – ${M(t)}`;
|
|
48
|
+
}
|
|
49
|
+
function F({ label: e, children: t }) {
|
|
50
|
+
return /* @__PURE__ */ _("div", {
|
|
37
51
|
className: "flex items-center justify-between gap-3 text-sm",
|
|
38
|
-
children: [/* @__PURE__ */
|
|
52
|
+
children: [/* @__PURE__ */ g("span", {
|
|
39
53
|
className: "text-text-secondary",
|
|
40
54
|
children: e
|
|
41
|
-
}), /* @__PURE__ */
|
|
55
|
+
}), /* @__PURE__ */ g("span", {
|
|
42
56
|
className: "font-medium text-text-primary text-right",
|
|
43
57
|
children: t
|
|
44
58
|
})]
|
|
45
59
|
});
|
|
46
60
|
}
|
|
47
|
-
function
|
|
48
|
-
let
|
|
49
|
-
let e =
|
|
50
|
-
return
|
|
51
|
-
let n = `${t.title} ${t.department} ${t.location}`.toLowerCase().includes(e), r =
|
|
61
|
+
function I() {
|
|
62
|
+
let a = n(), o = h(), { jobPostings: M, candidates: I } = e(), [L, R] = c(""), [z, B] = c("all"), { page: V, pageSize: H, offset: U, setPage: W } = k({ initialPageSize: 12 }), G = s(() => {
|
|
63
|
+
let e = L.toLowerCase();
|
|
64
|
+
return M.filter((t) => {
|
|
65
|
+
let n = `${t.title} ${t.department} ${t.location}`.toLowerCase().includes(e), r = z === "all" || t.status === z;
|
|
52
66
|
return n && r;
|
|
53
67
|
});
|
|
54
68
|
}, [
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
]),
|
|
69
|
+
M,
|
|
70
|
+
L,
|
|
71
|
+
z
|
|
72
|
+
]), K = G.slice(U, U + H), q = (e) => o(`../hiring/job-details-page?id=${encodeURIComponent(e)}`), J = M.filter((e) => e.status === "active").length, Y = M.reduce((e, t) => e + t.applicants, 0), X = s(() => {
|
|
59
73
|
let e = [{
|
|
60
74
|
id: "all",
|
|
61
75
|
label: "All",
|
|
62
|
-
count:
|
|
76
|
+
count: M.length
|
|
63
77
|
}];
|
|
64
|
-
for (let n of
|
|
65
|
-
let r =
|
|
78
|
+
for (let n of A) {
|
|
79
|
+
let r = M.filter((e) => e.status === n).length;
|
|
66
80
|
(r > 0 || n === "active") && e.push({
|
|
67
81
|
id: n,
|
|
68
82
|
label: t(n),
|
|
@@ -70,179 +84,179 @@ function j() {
|
|
|
70
84
|
});
|
|
71
85
|
}
|
|
72
86
|
return e;
|
|
73
|
-
}, [
|
|
87
|
+
}, [M]), Z = s(() => {
|
|
74
88
|
let e = /* @__PURE__ */ new Map();
|
|
75
|
-
for (let t of
|
|
76
|
-
let n = t.title.trim().toLowerCase(), r =
|
|
89
|
+
for (let t of M) {
|
|
90
|
+
let n = t.title.trim().toLowerCase(), r = I.filter((e) => e.position.trim().toLowerCase() === n).map((e) => ({ name: `${e.firstName} ${e.lastName}`.trim() }));
|
|
77
91
|
e.set(t.id, r);
|
|
78
92
|
}
|
|
79
93
|
return e;
|
|
80
|
-
}, [
|
|
81
|
-
return /* @__PURE__ */
|
|
94
|
+
}, [M, I]);
|
|
95
|
+
return /* @__PURE__ */ _("div", {
|
|
82
96
|
className: "space-y-6 p-6",
|
|
83
97
|
children: [
|
|
84
|
-
/* @__PURE__ */
|
|
98
|
+
/* @__PURE__ */ _("div", {
|
|
85
99
|
className: "flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between",
|
|
86
|
-
children: [/* @__PURE__ */
|
|
87
|
-
/* @__PURE__ */
|
|
100
|
+
children: [/* @__PURE__ */ _("div", { children: [
|
|
101
|
+
/* @__PURE__ */ g("h1", {
|
|
88
102
|
className: "text-2xl font-semibold",
|
|
89
|
-
children: /* @__PURE__ */
|
|
103
|
+
children: /* @__PURE__ */ g(w, { children: a("crewfoundry.hiringJobs.title", "Job Postings") })
|
|
90
104
|
}),
|
|
91
|
-
/* @__PURE__ */
|
|
105
|
+
/* @__PURE__ */ _("p", {
|
|
92
106
|
className: "text-sm text-text-secondary",
|
|
93
107
|
children: [
|
|
94
|
-
|
|
108
|
+
J,
|
|
95
109
|
" ",
|
|
96
|
-
|
|
110
|
+
a("crewfoundry.hiringJobs.summary.activePositions", "active positions"),
|
|
97
111
|
" ",
|
|
98
112
|
"· ",
|
|
99
|
-
|
|
113
|
+
Y,
|
|
100
114
|
" ",
|
|
101
|
-
|
|
115
|
+
a("crewfoundry.hiringJobs.summary.totalApplicants", "total applicants")
|
|
102
116
|
]
|
|
103
117
|
}),
|
|
104
|
-
/* @__PURE__ */
|
|
118
|
+
/* @__PURE__ */ g(D, {
|
|
105
119
|
className: "mt-1",
|
|
106
120
|
children: "All your open and past job postings in one place — use it to monitor applicant volume and manage what roles you're actively hiring for."
|
|
107
121
|
})
|
|
108
|
-
] }), /* @__PURE__ */
|
|
122
|
+
] }), /* @__PURE__ */ _(y, {
|
|
109
123
|
size: "sm",
|
|
110
|
-
onClick: () =>
|
|
111
|
-
children: [/* @__PURE__ */
|
|
124
|
+
onClick: () => o("../hiring/create-job-page"),
|
|
125
|
+
children: [/* @__PURE__ */ g(f, { className: "h-4 w-4 mr-1" }), a("crewfoundry.hiringJobs.actions.postNewJob", "Post New Job")]
|
|
112
126
|
})]
|
|
113
127
|
}),
|
|
114
|
-
/* @__PURE__ */
|
|
128
|
+
/* @__PURE__ */ _("div", {
|
|
115
129
|
className: "flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between",
|
|
116
|
-
children: [/* @__PURE__ */
|
|
130
|
+
children: [/* @__PURE__ */ g("div", {
|
|
117
131
|
className: "-mx-1 overflow-x-auto px-1",
|
|
118
|
-
children: /* @__PURE__ */
|
|
119
|
-
tabs:
|
|
120
|
-
value:
|
|
132
|
+
children: /* @__PURE__ */ g(i, {
|
|
133
|
+
tabs: X,
|
|
134
|
+
value: z,
|
|
121
135
|
onChange: (e) => {
|
|
122
|
-
|
|
136
|
+
B(e), W(1);
|
|
123
137
|
},
|
|
124
138
|
"aria-label": "Filter job postings by status"
|
|
125
139
|
})
|
|
126
|
-
}), /* @__PURE__ */
|
|
140
|
+
}), /* @__PURE__ */ _("div", {
|
|
127
141
|
className: "relative w-full max-w-sm",
|
|
128
|
-
children: [/* @__PURE__ */
|
|
129
|
-
placeholder:
|
|
142
|
+
children: [/* @__PURE__ */ g(p, { className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-text-secondary" }), /* @__PURE__ */ g(E, {
|
|
143
|
+
placeholder: a("crewfoundry.hiringJobs.searchPlaceholder", "Search positions..."),
|
|
130
144
|
className: "pl-9",
|
|
131
|
-
value:
|
|
145
|
+
value: L,
|
|
132
146
|
onChange: (e) => {
|
|
133
|
-
|
|
147
|
+
R(e.target.value), W(1);
|
|
134
148
|
}
|
|
135
149
|
})]
|
|
136
150
|
})]
|
|
137
151
|
}),
|
|
138
|
-
|
|
152
|
+
G.length === 0 && /* @__PURE__ */ g(T, {
|
|
139
153
|
illustration: "empty-search",
|
|
140
154
|
title: "No job postings found",
|
|
141
155
|
description: "Try adjusting your search or status filter, or create a new job posting."
|
|
142
156
|
}),
|
|
143
|
-
/* @__PURE__ */
|
|
157
|
+
/* @__PURE__ */ g("div", {
|
|
144
158
|
className: "grid gap-4 md:grid-cols-2 xl:grid-cols-3",
|
|
145
|
-
children:
|
|
146
|
-
let n =
|
|
147
|
-
return /* @__PURE__ */
|
|
159
|
+
children: K.map((e) => {
|
|
160
|
+
let n = Z.get(e.id) ?? [];
|
|
161
|
+
return /* @__PURE__ */ _(C, {
|
|
148
162
|
treatment: "glass",
|
|
149
163
|
role: "button",
|
|
150
164
|
tabIndex: 0,
|
|
151
165
|
"aria-label": `View ${e.title}`,
|
|
152
166
|
className: "flex cursor-pointer flex-col transition-shadow hover:shadow-elevation-2 focus-visible:outline-none focus-visible:shadow-[var(--shadow-focus)]",
|
|
153
|
-
onClick: () =>
|
|
167
|
+
onClick: () => q(e.id),
|
|
154
168
|
onKeyDown: (t) => {
|
|
155
|
-
(t.key === "Enter" || t.key === " ") && (t.preventDefault(),
|
|
169
|
+
(t.key === "Enter" || t.key === " ") && (t.preventDefault(), q(e.id));
|
|
156
170
|
},
|
|
157
|
-
children: [/* @__PURE__ */
|
|
171
|
+
children: [/* @__PURE__ */ _(x, { children: [/* @__PURE__ */ _("div", {
|
|
158
172
|
className: "flex items-start justify-between gap-2",
|
|
159
|
-
children: [/* @__PURE__ */
|
|
173
|
+
children: [/* @__PURE__ */ g(S, {
|
|
160
174
|
className: "text-lg leading-snug",
|
|
161
175
|
children: e.title
|
|
162
|
-
}), /* @__PURE__ */
|
|
176
|
+
}), /* @__PURE__ */ g(v, {
|
|
163
177
|
variant: e.status === "active" ? "default" : e.status === "draft" ? "secondary" : "outline",
|
|
164
178
|
children: t(e.status)
|
|
165
179
|
})]
|
|
166
|
-
}), /* @__PURE__ */
|
|
180
|
+
}), /* @__PURE__ */ _("div", {
|
|
167
181
|
className: "mt-2 flex flex-wrap gap-1.5",
|
|
168
|
-
children: [/* @__PURE__ */
|
|
182
|
+
children: [/* @__PURE__ */ g(v, {
|
|
169
183
|
variant: "outline",
|
|
170
184
|
className: "text-[10px]",
|
|
171
185
|
children: t(e.type)
|
|
172
|
-
}), /* @__PURE__ */
|
|
186
|
+
}), /* @__PURE__ */ g(v, {
|
|
173
187
|
variant: "outline",
|
|
174
188
|
className: "text-[10px]",
|
|
175
189
|
children: t(e.experienceLevel)
|
|
176
190
|
})]
|
|
177
|
-
})] }), /* @__PURE__ */
|
|
191
|
+
})] }), /* @__PURE__ */ _(b, {
|
|
178
192
|
className: "flex flex-1 flex-col gap-2.5",
|
|
179
193
|
children: [
|
|
180
|
-
/* @__PURE__ */
|
|
181
|
-
label:
|
|
182
|
-
children: /* @__PURE__ */
|
|
194
|
+
/* @__PURE__ */ g(F, {
|
|
195
|
+
label: a("crewfoundry.hiringJobs.card.department", "Department"),
|
|
196
|
+
children: /* @__PURE__ */ _("span", {
|
|
183
197
|
className: "inline-flex items-center gap-1",
|
|
184
|
-
children: [/* @__PURE__ */
|
|
198
|
+
children: [/* @__PURE__ */ g(l, { className: "h-3 w-3 text-text-secondary" }), e.department || "—"]
|
|
185
199
|
})
|
|
186
200
|
}),
|
|
187
|
-
/* @__PURE__ */
|
|
188
|
-
label:
|
|
189
|
-
children: /* @__PURE__ */
|
|
201
|
+
/* @__PURE__ */ g(F, {
|
|
202
|
+
label: a("crewfoundry.hiringJobs.card.location", "Location"),
|
|
203
|
+
children: /* @__PURE__ */ _("span", {
|
|
190
204
|
className: "inline-flex items-center gap-1",
|
|
191
|
-
children: [/* @__PURE__ */
|
|
205
|
+
children: [/* @__PURE__ */ g(d, { className: "h-3 w-3 text-text-secondary" }), e.location || "—"]
|
|
192
206
|
})
|
|
193
207
|
}),
|
|
194
|
-
/* @__PURE__ */
|
|
195
|
-
label:
|
|
196
|
-
children: /* @__PURE__ */
|
|
208
|
+
/* @__PURE__ */ g(F, {
|
|
209
|
+
label: a("crewfoundry.hiringJobs.card.salary", "Salary"),
|
|
210
|
+
children: /* @__PURE__ */ _("span", {
|
|
197
211
|
className: "inline-flex items-center gap-0.5 tabular-nums",
|
|
198
|
-
children: [
|
|
199
|
-
/* @__PURE__ */ m(c, { className: "h-3 w-3 text-text-secondary" }),
|
|
200
|
-
(e.salaryMin / 1e3).toFixed(0),
|
|
201
|
-
"k – $",
|
|
202
|
-
(e.salaryMax / 1e3).toFixed(0),
|
|
203
|
-
"k"
|
|
204
|
-
]
|
|
212
|
+
children: [/* @__PURE__ */ g(u, { className: "h-3 w-3 text-text-secondary" }), P(e.salaryMin, e.salaryMax)]
|
|
205
213
|
})
|
|
206
214
|
}),
|
|
207
|
-
/* @__PURE__ */
|
|
208
|
-
label:
|
|
209
|
-
children: /* @__PURE__ */
|
|
215
|
+
/* @__PURE__ */ g(F, {
|
|
216
|
+
label: a("crewfoundry.hiringJobs.card.applicantsLabel", "Applicants"),
|
|
217
|
+
children: /* @__PURE__ */ _("span", {
|
|
210
218
|
className: "inline-flex items-center gap-2",
|
|
211
|
-
children: [n.length > 0 && /* @__PURE__ */
|
|
219
|
+
children: [n.length > 0 && /* @__PURE__ */ g(r, {
|
|
212
220
|
people: n,
|
|
213
221
|
max: 4,
|
|
214
222
|
size: 22
|
|
215
|
-
}), /* @__PURE__ */
|
|
223
|
+
}), /* @__PURE__ */ _("span", {
|
|
216
224
|
className: "inline-flex items-center gap-1 tabular-nums",
|
|
217
|
-
children: [/* @__PURE__ */ m
|
|
225
|
+
children: [/* @__PURE__ */ g(m, { className: "h-3 w-3 text-text-secondary" }), e.applicants]
|
|
218
226
|
})]
|
|
219
227
|
})
|
|
220
228
|
}),
|
|
221
|
-
/* @__PURE__ */
|
|
222
|
-
className: "mt-auto
|
|
223
|
-
children: /* @__PURE__ */
|
|
224
|
-
label:
|
|
225
|
-
children: /* @__PURE__ */
|
|
229
|
+
/* @__PURE__ */ _("div", {
|
|
230
|
+
className: "mt-auto flex flex-col gap-2.5",
|
|
231
|
+
children: [/* @__PURE__ */ g(F, {
|
|
232
|
+
label: a("crewfoundry.hiringJobs.card.posted", "Posted"),
|
|
233
|
+
children: /* @__PURE__ */ g("span", {
|
|
226
234
|
className: "text-text-secondary font-normal",
|
|
227
|
-
children:
|
|
235
|
+
children: j(e.postedDate)
|
|
228
236
|
})
|
|
229
|
-
})
|
|
237
|
+
}), /* @__PURE__ */ g(F, {
|
|
238
|
+
label: a("crewfoundry.hiringJobs.card.deadline", "Deadline"),
|
|
239
|
+
children: /* @__PURE__ */ g("span", {
|
|
240
|
+
className: "text-text-secondary font-normal",
|
|
241
|
+
children: N(e.deadline)
|
|
242
|
+
})
|
|
243
|
+
})]
|
|
230
244
|
})
|
|
231
245
|
]
|
|
232
246
|
})]
|
|
233
247
|
}, e.id);
|
|
234
248
|
})
|
|
235
249
|
}),
|
|
236
|
-
|
|
237
|
-
page:
|
|
238
|
-
pageSize:
|
|
239
|
-
totalItems:
|
|
240
|
-
onPageChange:
|
|
250
|
+
G.length > H && /* @__PURE__ */ g(O, {
|
|
251
|
+
page: V,
|
|
252
|
+
pageSize: H,
|
|
253
|
+
totalItems: G.length,
|
|
254
|
+
onPageChange: W
|
|
241
255
|
})
|
|
242
256
|
]
|
|
243
257
|
});
|
|
244
258
|
}
|
|
245
259
|
//#endregion
|
|
246
|
-
export {
|
|
260
|
+
export { I as HiringJobsPagePage };
|
|
247
261
|
|
|
248
262
|
//# sourceMappingURL=HiringJobsPagePage.js.map
|