@burdenoff/microfe-crewfoundry 2026.731.1 → 2026.801.1

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.
Files changed (83) hide show
  1. package/dist/crewfoundry/CrewFoundryRoutes.js +1 -1
  2. package/dist/crewfoundry/CrewFoundryRoutes.js.map +1 -1
  3. package/dist/crewfoundry/pages/CommunicationsAnnouncementsPagePage.js +1 -1
  4. package/dist/crewfoundry/pages/CommunicationsAnnouncementsPagePage.js.map +1 -1
  5. package/dist/crewfoundry/pages/CommunicationsFilesPagePage.js +1 -1
  6. package/dist/crewfoundry/pages/CommunicationsFilesPagePage.js.map +1 -1
  7. package/dist/crewfoundry/pages/CommunicationsTeamsPagePage.js +1 -1
  8. package/dist/crewfoundry/pages/CommunicationsTeamsPagePage.js.map +1 -1
  9. package/dist/crewfoundry/pages/CompanyClientManagementPagePage.js +1 -1
  10. package/dist/crewfoundry/pages/CompanyClientManagementPagePage.js.map +1 -1
  11. package/dist/crewfoundry/pages/CompanyVendorManagementPagePage.js +1 -1
  12. package/dist/crewfoundry/pages/CompanyVendorManagementPagePage.js.map +1 -1
  13. package/dist/crewfoundry/pages/DashboardDashboardPagePage.js +1 -1
  14. package/dist/crewfoundry/pages/DashboardDashboardPagePage.js.map +1 -1
  15. package/dist/crewfoundry/pages/DashboardEnhancedDashboardPagePage.js +1 -1
  16. package/dist/crewfoundry/pages/DashboardEnhancedDashboardPagePage.js.map +1 -1
  17. package/dist/crewfoundry/pages/HiringApplicationsPagePage.js +1 -1
  18. package/dist/crewfoundry/pages/HiringApplicationsPagePage.js.map +1 -1
  19. package/dist/crewfoundry/pages/HiringCreateJobPagePage.js +1 -1
  20. package/dist/crewfoundry/pages/HiringCreateJobPagePage.js.map +1 -1
  21. package/dist/crewfoundry/pages/HiringJobDetailsPagePage.js +1 -1
  22. package/dist/crewfoundry/pages/HiringJobDetailsPagePage.js.map +1 -1
  23. package/dist/crewfoundry/pages/HiringJobsPagePage.js +1 -1
  24. package/dist/crewfoundry/pages/HiringJobsPagePage.js.map +1 -1
  25. package/dist/crewfoundry/pages/HrEmployeeProfilePagePage.js +1 -1
  26. package/dist/crewfoundry/pages/HrEmployeeProfilePagePage.js.map +1 -1
  27. package/dist/crewfoundry/pages/HrEmployeesPagePage.js +1 -1
  28. package/dist/crewfoundry/pages/HrEmployeesPagePage.js.map +1 -1
  29. package/dist/crewfoundry/pages/LearningAssessmentsPagePage.js +1 -1
  30. package/dist/crewfoundry/pages/LearningAssessmentsPagePage.js.map +1 -1
  31. package/dist/crewfoundry/pages/LearningCertificatesPagePage.js +1 -1
  32. package/dist/crewfoundry/pages/LearningCertificatesPagePage.js.map +1 -1
  33. package/dist/crewfoundry/pages/LearningCourseDetailsPagePage.js +1 -1
  34. package/dist/crewfoundry/pages/LearningCourseDetailsPagePage.js.map +1 -1
  35. package/dist/crewfoundry/pages/LearningCourseLearningPagePage.js +1 -1
  36. package/dist/crewfoundry/pages/LearningCourseLearningPagePage.js.map +1 -1
  37. package/dist/crewfoundry/pages/LearningCoursesPagePage.js +1 -1
  38. package/dist/crewfoundry/pages/LearningCoursesPagePage.js.map +1 -1
  39. package/dist/crewfoundry/pages/LearningLearningPagePage.js +1 -1
  40. package/dist/crewfoundry/pages/LearningLearningPagePage.js.map +1 -1
  41. package/dist/crewfoundry/pages/LearningLearningPathDetailPagePage.js +1 -1
  42. package/dist/crewfoundry/pages/LearningLearningPathDetailPagePage.js.map +1 -1
  43. package/dist/crewfoundry/pages/LearningLearningPathsPagePage.js +1 -1
  44. package/dist/crewfoundry/pages/LearningLearningPathsPagePage.js.map +1 -1
  45. package/dist/crewfoundry/pages/LearningMyLearningPagePage.js +1 -1
  46. package/dist/crewfoundry/pages/LearningMyLearningPagePage.js.map +1 -1
  47. package/dist/crewfoundry/pages/MarketplaceFreelancerProfilePageEnhancedPage.js +1 -1
  48. package/dist/crewfoundry/pages/MarketplaceFreelancerProfilePageEnhancedPage.js.map +1 -1
  49. package/dist/crewfoundry/pages/MarketplaceFreelancerProfilePagePage.js +1 -1
  50. package/dist/crewfoundry/pages/MarketplaceFreelancerProfilePagePage.js.map +1 -1
  51. package/dist/crewfoundry/pages/MarketplaceMarketplacePagePage.js +1 -1
  52. package/dist/crewfoundry/pages/MarketplaceMarketplacePagePage.js.map +1 -1
  53. package/dist/crewfoundry/pages/MarketplaceMarketplaceServicesPagePage.js +1 -1
  54. package/dist/crewfoundry/pages/MarketplaceMarketplaceServicesPagePage.js.map +1 -1
  55. package/dist/crewfoundry/pages/MarketplaceMyServicesPagePage.js +1 -1
  56. package/dist/crewfoundry/pages/MarketplaceMyServicesPagePage.js.map +1 -1
  57. package/dist/crewfoundry/pages/MarketplaceProjectDetailsPagePage.js +1 -1
  58. package/dist/crewfoundry/pages/MarketplaceProjectDetailsPagePage.js.map +1 -1
  59. package/dist/crewfoundry/pages/MarketplaceServiceDetailsPagePage.js +1 -1
  60. package/dist/crewfoundry/pages/MarketplaceServiceDetailsPagePage.js.map +1 -1
  61. package/dist/crewfoundry/pages/PaymentsPaymentDashboardPagePage.js +1 -1
  62. package/dist/crewfoundry/pages/PaymentsPaymentDashboardPagePage.js.map +1 -1
  63. package/dist/crewfoundry/pages/PaymentsPaymentMethodsPagePage.js +1 -1
  64. package/dist/crewfoundry/pages/PaymentsPaymentMethodsPagePage.js.map +1 -1
  65. package/dist/crewfoundry/pages/ProjectsCreateProjectPagePage.js +1 -1
  66. package/dist/crewfoundry/pages/ProjectsCreateProjectPagePage.js.map +1 -1
  67. package/dist/crewfoundry/pages/ProjectsProjectDetailsPagePage.js +1 -1
  68. package/dist/crewfoundry/pages/ProjectsProjectDetailsPagePage.js.map +1 -1
  69. package/dist/crewfoundry/pages/ProjectsProjectsPagePage.js +1 -1
  70. package/dist/crewfoundry/pages/ProjectsProjectsPagePage.js.map +1 -1
  71. package/dist/crewfoundry/pages/ProjectsResourcingPagePage.js +1 -1
  72. package/dist/crewfoundry/pages/ProjectsResourcingPagePage.js.map +1 -1
  73. package/dist/crewfoundry/pages/TeamsTeamDetailPagePage.js +1 -1
  74. package/dist/crewfoundry/pages/TeamsTeamDetailPagePage.js.map +1 -1
  75. package/dist/crewfoundry/pages/TeamsTeamDirectoryPagePage.js +1 -1
  76. package/dist/crewfoundry/pages/TeamsTeamDirectoryPagePage.js.map +1 -1
  77. package/dist/crewfoundry/pages/TeamsTeamSettingsPagePage.js +1 -1
  78. package/dist/crewfoundry/pages/TeamsTeamSettingsPagePage.js.map +1 -1
  79. package/dist/crewfoundry/pages/TeamsTeamsPagePage.js +1 -1
  80. package/dist/crewfoundry/pages/TeamsTeamsPagePage.js.map +1 -1
  81. package/dist/crewfoundry/providers/CrewFoundryProvider.js +1 -1
  82. package/dist/crewfoundry/providers/CrewFoundryProvider.js.map +1 -1
  83. package/package.json +6 -6
@@ -1 +1 @@
1
- {"version":3,"file":"HiringCreateJobPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/HiringCreateJobPagePage.tsx"],"sourcesContent":["import { useMemo, useState, type ReactNode } from 'react';\nimport {\n Button,\n GlassCard,\n GradientText,\n Input,\n PagePurpose,\n Textarea,\n} from '@burdenoff/fe-libs/ui';\nimport { useGlobalUi } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { Briefcase, ChevronRight, Plus, Save, X } from 'lucide-react';\nimport { useNavigate } from 'react-router-dom';\nimport { useCrewFoundryStore, type JobPosting } from '../store/crewfoundryStore';\nimport { newClientId } from '../components/forms/formValues';\n\n// Native <select> styled with the same semantic tokens as fe-libs Input.\nconst SELECT_CLASS =\n 'flex h-10 w-full rounded-input border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary transition-colors duration-200 focus-visible:border-border-focus focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50';\n\nconst FIELD_LABEL_CLASS = 'text-sm font-medium text-text-primary';\n\nconst JOB_TYPE_OPTIONS: { label: string; value: JobPosting['type'] }[] = [\n { label: 'Full time', value: 'full_time' },\n { label: 'Part time', value: 'part_time' },\n { label: 'Contract', value: 'contract' },\n { label: 'Remote', value: 'remote' },\n];\n\nconst EXPERIENCE_OPTIONS: { label: string; value: JobPosting['experienceLevel'] }[] = [\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\nfunction FieldLabel({ htmlFor, children }: { htmlFor: string; children: ReactNode }) {\n return (\n <label className={FIELD_LABEL_CLASS} htmlFor={htmlFor}>\n {children}\n </label>\n );\n}\n\nexport function HiringCreateJobPagePage() {\n const navigate = useNavigate();\n const { showToast } = useGlobalUi();\n const addJobPosting = useCrewFoundryStore((state) => state.addJobPosting);\n const departments = useCrewFoundryStore((state) => state.departments);\n\n const [title, setTitle] = useState('');\n const [department, setDepartment] = useState('');\n const [location, setLocation] = useState('');\n const [description, setDescription] = useState('');\n const [type, setType] = useState<JobPosting['type']>('full_time');\n const [experienceLevel, setExperienceLevel] = useState<JobPosting['experienceLevel']>('mid');\n const [salaryMin, setSalaryMin] = useState('');\n const [salaryMax, setSalaryMax] = useState('');\n const [deadline, setDeadline] = useState('');\n const [requirements, setRequirements] = useState<string[]>(['']);\n const [benefits, setBenefits] = useState<string[]>(['']);\n const [submitting, setSubmitting] = useState(false);\n\n const departmentNames = useMemo(\n () => Array.from(new Set(departments.map((d) => d.name).filter(Boolean))).sort(),\n [departments]\n );\n\n const addRequirement = () => setRequirements((prev) => [...prev, '']);\n const addBenefit = () => setBenefits((prev) => [...prev, '']);\n const updateRequirement = (idx: number, value: string) =>\n setRequirements((prev) => prev.map((item, i) => (i === idx ? value : item)));\n const updateBenefit = (idx: number, value: string) =>\n setBenefits((prev) => prev.map((item, i) => (i === idx ? value : item)));\n const removeRequirement = (idx: number) =>\n setRequirements((prev) => prev.filter((_, i) => i !== idx));\n const removeBenefit = (idx: number) => setBenefits((prev) => prev.filter((_, i) => i !== idx));\n\n const submit = async (status: JobPosting['status']) => {\n if (submitting) {\n return;\n }\n if (!title.trim()) {\n showToast('Job Title is required', 'warning');\n return;\n }\n if (!department.trim()) {\n showToast('Department is required', 'warning');\n return;\n }\n const today = new Date().toISOString().slice(0, 10);\n const job: JobPosting = {\n id: newClientId(),\n title: title.trim(),\n department: department.trim(),\n location: location.trim(),\n type,\n experienceLevel,\n salaryMin: salaryMin.trim() === '' ? 0 : Number(salaryMin),\n salaryMax: salaryMax.trim() === '' ? 0 : Number(salaryMax),\n status,\n applicants: 0,\n postedDate: today,\n deadline: deadline.trim() === '' ? today : deadline,\n description: description.trim(),\n requirements: requirements.map((item) => item.trim()).filter(Boolean),\n benefits: benefits.map((item) => item.trim()).filter(Boolean),\n };\n\n setSubmitting(true);\n try {\n await addJobPosting(job);\n showToast(status === 'draft' ? 'Draft saved' : 'Job published', 'success');\n navigate('../hiring/jobs-page');\n } catch (error) {\n showToast(error instanceof Error ? error.message : 'Unable to save job posting', 'error');\n } finally {\n setSubmitting(false);\n }\n };\n\n const renderChipRows = (\n items: string[],\n update: (idx: number, value: string) => void,\n remove: (idx: number) => void,\n add: () => void,\n kind: 'Requirement' | 'Benefit',\n placeholder: string\n ) => (\n <div className=\"space-y-2\">\n {items.map((value, idx) => (\n <div key={idx} className=\"relative\">\n <Input\n className=\"pr-9\"\n placeholder={placeholder}\n value={value}\n onChange={(e) => update(idx, e.target.value)}\n />\n <button\n type=\"button\"\n aria-label={`Remove ${kind.toLowerCase()} ${idx + 1}`}\n onClick={() => remove(idx)}\n className=\"absolute right-2.5 top-1/2 -translate-y-1/2 rounded-sm p-0.5 text-text-secondary transition-colors hover:text-text-primary focus-visible:outline-none focus-visible:text-text-primary\"\n >\n <X className=\"h-4 w-4\" />\n </button>\n </div>\n ))}\n <Button variant=\"link\" size=\"sm\" className=\"h-auto p-0\" onClick={add} type=\"button\">\n <Plus className=\"h-4 w-4 mr-1\" />\n Add {kind}\n </Button>\n </div>\n );\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"space-y-2\">\n <nav\n aria-label=\"Breadcrumb\"\n className=\"flex items-center gap-1 text-sm text-text-secondary\"\n >\n <button\n type=\"button\"\n onClick={() => navigate('../hiring/jobs-page')}\n className=\"rounded-sm transition-colors hover:text-text-primary focus-visible:outline-none focus-visible:text-text-primary\"\n >\n Hiring\n </button>\n <ChevronRight className=\"h-3.5 w-3.5\" />\n <span className=\"text-text-primary\">Post New Job</span>\n </nav>\n <div>\n <h1 className=\"text-2xl font-semibold\">\n <GradientText>Create Job Posting</GradientText>\n </h1>\n <PagePurpose className=\"mt-1\">\n Create a new job posting — define the role, requirements, and compensation here to start\n attracting and collecting applicants.\n </PagePurpose>\n </div>\n </div>\n\n <GlassCard treatment=\"glass\" className=\"max-w-5xl p-6\">\n <div className=\"mb-6 flex items-center gap-2 text-lg font-semibold text-text-primary\">\n <Briefcase className=\"h-5 w-5\" />\n Job details\n </div>\n\n <div className=\"space-y-5\">\n <div className=\"grid grid-cols-1 gap-5 md:grid-cols-2\">\n <div className=\"space-y-2\">\n <FieldLabel htmlFor=\"job-title\">Job Title</FieldLabel>\n <Input\n id=\"job-title\"\n placeholder=\"e.g. Senior Frontend Developer\"\n value={title}\n onChange={(e) => setTitle(e.target.value)}\n />\n </div>\n <div className=\"space-y-2\">\n <FieldLabel htmlFor=\"job-type\">Employment Type</FieldLabel>\n <select\n id=\"job-type\"\n className={SELECT_CLASS}\n value={type}\n onChange={(e) => setType(e.target.value as JobPosting['type'])}\n >\n {JOB_TYPE_OPTIONS.map((option) => (\n <option key={option.value} value={option.value}>\n {option.label}\n </option>\n ))}\n </select>\n </div>\n </div>\n\n <div className=\"grid grid-cols-1 gap-5 md:grid-cols-2\">\n <div className=\"space-y-2\">\n <FieldLabel htmlFor=\"job-department\">Department</FieldLabel>\n <Input\n id=\"job-department\"\n list=\"crewfoundry-department-options\"\n placeholder=\"e.g. Engineering\"\n value={department}\n onChange={(e) => setDepartment(e.target.value)}\n />\n <datalist id=\"crewfoundry-department-options\">\n {departmentNames.map((name) => (\n <option key={name} value={name} />\n ))}\n </datalist>\n </div>\n <div className=\"space-y-2\">\n <FieldLabel htmlFor=\"job-experience\">Experience Level</FieldLabel>\n <select\n id=\"job-experience\"\n className={SELECT_CLASS}\n value={experienceLevel}\n onChange={(e) =>\n setExperienceLevel(e.target.value as JobPosting['experienceLevel'])\n }\n >\n {EXPERIENCE_OPTIONS.map((option) => (\n <option key={option.value} value={option.value}>\n {option.label}\n </option>\n ))}\n </select>\n </div>\n </div>\n\n <div className=\"grid grid-cols-1 gap-5 md:grid-cols-2\">\n <div className=\"space-y-2\">\n <FieldLabel htmlFor=\"job-location\">Location</FieldLabel>\n <Input\n id=\"job-location\"\n placeholder=\"e.g. San Francisco, CA\"\n value={location}\n onChange={(e) => setLocation(e.target.value)}\n />\n </div>\n <div className=\"grid grid-cols-2 gap-4\">\n <div className=\"space-y-2\">\n <FieldLabel htmlFor=\"job-salary-min\">Salary Min</FieldLabel>\n <div className=\"relative\">\n <span className=\"pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-sm text-text-secondary\">\n $\n </span>\n <Input\n id=\"job-salary-min\"\n type=\"number\"\n className=\"pl-7\"\n placeholder=\"120,000\"\n value={salaryMin}\n onChange={(e) => setSalaryMin(e.target.value)}\n />\n </div>\n </div>\n <div className=\"space-y-2\">\n <FieldLabel htmlFor=\"job-salary-max\">Salary Max</FieldLabel>\n <div className=\"relative\">\n <span className=\"pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-sm text-text-secondary\">\n $\n </span>\n <Input\n id=\"job-salary-max\"\n type=\"number\"\n className=\"pl-7\"\n placeholder=\"180,000\"\n value={salaryMax}\n onChange={(e) => setSalaryMax(e.target.value)}\n />\n </div>\n </div>\n </div>\n </div>\n\n <div className=\"grid grid-cols-1 gap-5 md:grid-cols-2\">\n <div className=\"space-y-2\">\n <FieldLabel htmlFor=\"job-deadline\">Application Deadline</FieldLabel>\n <Input\n id=\"job-deadline\"\n type=\"date\"\n value={deadline}\n onChange={(e) => setDeadline(e.target.value)}\n />\n </div>\n </div>\n\n <div className=\"space-y-2\">\n <FieldLabel htmlFor=\"job-description\">Description</FieldLabel>\n <Textarea\n id=\"job-description\"\n placeholder=\"Describe the role, responsibilities, and ideal candidate...\"\n rows={4}\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n />\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 border-t border-border-subtle pt-5 md:grid-cols-2\">\n <div className=\"space-y-3\">\n <div>\n <div className={FIELD_LABEL_CLASS}>Requirements</div>\n <p className=\"text-xs text-text-secondary\">\n List the key requirements for this role\n </p>\n </div>\n {renderChipRows(\n requirements,\n updateRequirement,\n removeRequirement,\n addRequirement,\n 'Requirement',\n 'e.g. 5+ years with React'\n )}\n </div>\n <div className=\"space-y-3\">\n <div>\n <div className={FIELD_LABEL_CLASS}>Benefits</div>\n <p className=\"text-xs text-text-secondary\">What does this position offer?</p>\n </div>\n {renderChipRows(\n benefits,\n updateBenefit,\n removeBenefit,\n addBenefit,\n 'Benefit',\n 'e.g. Flexible remote work'\n )}\n </div>\n </div>\n\n <div className=\"flex flex-col-reverse justify-end gap-2 border-t border-border-subtle pt-5 sm:flex-row\">\n <Button variant=\"outline\" disabled={submitting} onClick={() => submit('draft')}>\n Save as Draft\n </Button>\n <Button disabled={submitting} onClick={() => submit('active')}>\n <Save className=\"h-4 w-4 mr-1\" />\n {submitting ? 'Saving…' : 'Publish Job'}\n </Button>\n </div>\n </div>\n </GlassCard>\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"HiringCreateJobPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/HiringCreateJobPagePage.tsx"],"sourcesContent":["import { useMemo, useState, type ReactNode } from 'react';\nimport {\n Button,\n GlassCard,\n GradientText,\n Input,\n PagePurpose,\n Textarea,\n} from '@burdenoff/fe-libs/ui';\nimport { useGlobalUi } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { Briefcase, ChevronRight, Plus, Save, X } from 'lucide-react';\nimport { useNavigate } from 'react-router';\nimport { useCrewFoundryStore, type JobPosting } from '../store/crewfoundryStore';\nimport { newClientId } from '../components/forms/formValues';\n\n// Native <select> styled with the same semantic tokens as fe-libs Input.\nconst SELECT_CLASS =\n 'flex h-10 w-full rounded-input border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary transition-colors duration-200 focus-visible:border-border-focus focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50';\n\nconst FIELD_LABEL_CLASS = 'text-sm font-medium text-text-primary';\n\nconst JOB_TYPE_OPTIONS: { label: string; value: JobPosting['type'] }[] = [\n { label: 'Full time', value: 'full_time' },\n { label: 'Part time', value: 'part_time' },\n { label: 'Contract', value: 'contract' },\n { label: 'Remote', value: 'remote' },\n];\n\nconst EXPERIENCE_OPTIONS: { label: string; value: JobPosting['experienceLevel'] }[] = [\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\nfunction FieldLabel({ htmlFor, children }: { htmlFor: string; children: ReactNode }) {\n return (\n <label className={FIELD_LABEL_CLASS} htmlFor={htmlFor}>\n {children}\n </label>\n );\n}\n\nexport function HiringCreateJobPagePage() {\n const navigate = useNavigate();\n const { showToast } = useGlobalUi();\n const addJobPosting = useCrewFoundryStore((state) => state.addJobPosting);\n const departments = useCrewFoundryStore((state) => state.departments);\n\n const [title, setTitle] = useState('');\n const [department, setDepartment] = useState('');\n const [location, setLocation] = useState('');\n const [description, setDescription] = useState('');\n const [type, setType] = useState<JobPosting['type']>('full_time');\n const [experienceLevel, setExperienceLevel] = useState<JobPosting['experienceLevel']>('mid');\n const [salaryMin, setSalaryMin] = useState('');\n const [salaryMax, setSalaryMax] = useState('');\n const [deadline, setDeadline] = useState('');\n const [requirements, setRequirements] = useState<string[]>(['']);\n const [benefits, setBenefits] = useState<string[]>(['']);\n const [submitting, setSubmitting] = useState(false);\n\n const departmentNames = useMemo(\n () => Array.from(new Set(departments.map((d) => d.name).filter(Boolean))).sort(),\n [departments]\n );\n\n const addRequirement = () => setRequirements((prev) => [...prev, '']);\n const addBenefit = () => setBenefits((prev) => [...prev, '']);\n const updateRequirement = (idx: number, value: string) =>\n setRequirements((prev) => prev.map((item, i) => (i === idx ? value : item)));\n const updateBenefit = (idx: number, value: string) =>\n setBenefits((prev) => prev.map((item, i) => (i === idx ? value : item)));\n const removeRequirement = (idx: number) =>\n setRequirements((prev) => prev.filter((_, i) => i !== idx));\n const removeBenefit = (idx: number) => setBenefits((prev) => prev.filter((_, i) => i !== idx));\n\n const submit = async (status: JobPosting['status']) => {\n if (submitting) {\n return;\n }\n if (!title.trim()) {\n showToast('Job Title is required', 'warning');\n return;\n }\n if (!department.trim()) {\n showToast('Department is required', 'warning');\n return;\n }\n const today = new Date().toISOString().slice(0, 10);\n const job: JobPosting = {\n id: newClientId(),\n title: title.trim(),\n department: department.trim(),\n location: location.trim(),\n type,\n experienceLevel,\n salaryMin: salaryMin.trim() === '' ? 0 : Number(salaryMin),\n salaryMax: salaryMax.trim() === '' ? 0 : Number(salaryMax),\n status,\n applicants: 0,\n postedDate: today,\n deadline: deadline.trim() === '' ? today : deadline,\n description: description.trim(),\n requirements: requirements.map((item) => item.trim()).filter(Boolean),\n benefits: benefits.map((item) => item.trim()).filter(Boolean),\n };\n\n setSubmitting(true);\n try {\n await addJobPosting(job);\n showToast(status === 'draft' ? 'Draft saved' : 'Job published', 'success');\n navigate('../hiring/jobs-page');\n } catch (error) {\n showToast(error instanceof Error ? error.message : 'Unable to save job posting', 'error');\n } finally {\n setSubmitting(false);\n }\n };\n\n const renderChipRows = (\n items: string[],\n update: (idx: number, value: string) => void,\n remove: (idx: number) => void,\n add: () => void,\n kind: 'Requirement' | 'Benefit',\n placeholder: string\n ) => (\n <div className=\"space-y-2\">\n {items.map((value, idx) => (\n <div key={idx} className=\"relative\">\n <Input\n className=\"pr-9\"\n placeholder={placeholder}\n value={value}\n onChange={(e) => update(idx, e.target.value)}\n />\n <button\n type=\"button\"\n aria-label={`Remove ${kind.toLowerCase()} ${idx + 1}`}\n onClick={() => remove(idx)}\n className=\"absolute right-2.5 top-1/2 -translate-y-1/2 rounded-sm p-0.5 text-text-secondary transition-colors hover:text-text-primary focus-visible:outline-none focus-visible:text-text-primary\"\n >\n <X className=\"h-4 w-4\" />\n </button>\n </div>\n ))}\n <Button variant=\"link\" size=\"sm\" className=\"h-auto p-0\" onClick={add} type=\"button\">\n <Plus className=\"h-4 w-4 mr-1\" />\n Add {kind}\n </Button>\n </div>\n );\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"space-y-2\">\n <nav\n aria-label=\"Breadcrumb\"\n className=\"flex items-center gap-1 text-sm text-text-secondary\"\n >\n <button\n type=\"button\"\n onClick={() => navigate('../hiring/jobs-page')}\n className=\"rounded-sm transition-colors hover:text-text-primary focus-visible:outline-none focus-visible:text-text-primary\"\n >\n Hiring\n </button>\n <ChevronRight className=\"h-3.5 w-3.5\" />\n <span className=\"text-text-primary\">Post New Job</span>\n </nav>\n <div>\n <h1 className=\"text-2xl font-semibold\">\n <GradientText>Create Job Posting</GradientText>\n </h1>\n <PagePurpose className=\"mt-1\">\n Create a new job posting — define the role, requirements, and compensation here to start\n attracting and collecting applicants.\n </PagePurpose>\n </div>\n </div>\n\n <GlassCard treatment=\"glass\" className=\"max-w-5xl p-6\">\n <div className=\"mb-6 flex items-center gap-2 text-lg font-semibold text-text-primary\">\n <Briefcase className=\"h-5 w-5\" />\n Job details\n </div>\n\n <div className=\"space-y-5\">\n <div className=\"grid grid-cols-1 gap-5 md:grid-cols-2\">\n <div className=\"space-y-2\">\n <FieldLabel htmlFor=\"job-title\">Job Title</FieldLabel>\n <Input\n id=\"job-title\"\n placeholder=\"e.g. Senior Frontend Developer\"\n value={title}\n onChange={(e) => setTitle(e.target.value)}\n />\n </div>\n <div className=\"space-y-2\">\n <FieldLabel htmlFor=\"job-type\">Employment Type</FieldLabel>\n <select\n id=\"job-type\"\n className={SELECT_CLASS}\n value={type}\n onChange={(e) => setType(e.target.value as JobPosting['type'])}\n >\n {JOB_TYPE_OPTIONS.map((option) => (\n <option key={option.value} value={option.value}>\n {option.label}\n </option>\n ))}\n </select>\n </div>\n </div>\n\n <div className=\"grid grid-cols-1 gap-5 md:grid-cols-2\">\n <div className=\"space-y-2\">\n <FieldLabel htmlFor=\"job-department\">Department</FieldLabel>\n <Input\n id=\"job-department\"\n list=\"crewfoundry-department-options\"\n placeholder=\"e.g. Engineering\"\n value={department}\n onChange={(e) => setDepartment(e.target.value)}\n />\n <datalist id=\"crewfoundry-department-options\">\n {departmentNames.map((name) => (\n <option key={name} value={name} />\n ))}\n </datalist>\n </div>\n <div className=\"space-y-2\">\n <FieldLabel htmlFor=\"job-experience\">Experience Level</FieldLabel>\n <select\n id=\"job-experience\"\n className={SELECT_CLASS}\n value={experienceLevel}\n onChange={(e) =>\n setExperienceLevel(e.target.value as JobPosting['experienceLevel'])\n }\n >\n {EXPERIENCE_OPTIONS.map((option) => (\n <option key={option.value} value={option.value}>\n {option.label}\n </option>\n ))}\n </select>\n </div>\n </div>\n\n <div className=\"grid grid-cols-1 gap-5 md:grid-cols-2\">\n <div className=\"space-y-2\">\n <FieldLabel htmlFor=\"job-location\">Location</FieldLabel>\n <Input\n id=\"job-location\"\n placeholder=\"e.g. San Francisco, CA\"\n value={location}\n onChange={(e) => setLocation(e.target.value)}\n />\n </div>\n <div className=\"grid grid-cols-2 gap-4\">\n <div className=\"space-y-2\">\n <FieldLabel htmlFor=\"job-salary-min\">Salary Min</FieldLabel>\n <div className=\"relative\">\n <span className=\"pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-sm text-text-secondary\">\n $\n </span>\n <Input\n id=\"job-salary-min\"\n type=\"number\"\n className=\"pl-7\"\n placeholder=\"120,000\"\n value={salaryMin}\n onChange={(e) => setSalaryMin(e.target.value)}\n />\n </div>\n </div>\n <div className=\"space-y-2\">\n <FieldLabel htmlFor=\"job-salary-max\">Salary Max</FieldLabel>\n <div className=\"relative\">\n <span className=\"pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-sm text-text-secondary\">\n $\n </span>\n <Input\n id=\"job-salary-max\"\n type=\"number\"\n className=\"pl-7\"\n placeholder=\"180,000\"\n value={salaryMax}\n onChange={(e) => setSalaryMax(e.target.value)}\n />\n </div>\n </div>\n </div>\n </div>\n\n <div className=\"grid grid-cols-1 gap-5 md:grid-cols-2\">\n <div className=\"space-y-2\">\n <FieldLabel htmlFor=\"job-deadline\">Application Deadline</FieldLabel>\n <Input\n id=\"job-deadline\"\n type=\"date\"\n value={deadline}\n onChange={(e) => setDeadline(e.target.value)}\n />\n </div>\n </div>\n\n <div className=\"space-y-2\">\n <FieldLabel htmlFor=\"job-description\">Description</FieldLabel>\n <Textarea\n id=\"job-description\"\n placeholder=\"Describe the role, responsibilities, and ideal candidate...\"\n rows={4}\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n />\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 border-t border-border-subtle pt-5 md:grid-cols-2\">\n <div className=\"space-y-3\">\n <div>\n <div className={FIELD_LABEL_CLASS}>Requirements</div>\n <p className=\"text-xs text-text-secondary\">\n List the key requirements for this role\n </p>\n </div>\n {renderChipRows(\n requirements,\n updateRequirement,\n removeRequirement,\n addRequirement,\n 'Requirement',\n 'e.g. 5+ years with React'\n )}\n </div>\n <div className=\"space-y-3\">\n <div>\n <div className={FIELD_LABEL_CLASS}>Benefits</div>\n <p className=\"text-xs text-text-secondary\">What does this position offer?</p>\n </div>\n {renderChipRows(\n benefits,\n updateBenefit,\n removeBenefit,\n addBenefit,\n 'Benefit',\n 'e.g. Flexible remote work'\n )}\n </div>\n </div>\n\n <div className=\"flex flex-col-reverse justify-end gap-2 border-t border-border-subtle pt-5 sm:flex-row\">\n <Button variant=\"outline\" disabled={submitting} onClick={() => submit('draft')}>\n Save as Draft\n </Button>\n <Button disabled={submitting} onClick={() => submit('active')}>\n <Save className=\"h-4 w-4 mr-1\" />\n {submitting ? 'Saving…' : 'Publish Job'}\n </Button>\n </div>\n </div>\n </GlassCard>\n </div>\n );\n}\n"],"mappings":"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"}
@@ -11,7 +11,7 @@ import { ConfirmDialog as ne } from "../components/forms/ConfirmDialog.js";
11
11
  import { getNumber as o, getTags as s, getText as c } from "../components/forms/formValues.js";
12
12
  import { useState as l } from "react";
13
13
  import { Briefcase as re, Calendar as ie, CheckCircle as u, MapPin as ae, Pencil as d, Share2 as f, Sparkles as p, Trash2 as m, Users as h, XCircle as g } from "lucide-react";
14
- import { useNavigate as _, useSearchParams as oe } from "react-router-dom";
14
+ import { useNavigate as _, useSearchParams as oe } from "react-router";
15
15
  import { jsx as v, jsxs as y } from "react/jsx-runtime";
16
16
  import { Badge as b, Button as x, Card as S, CardContent as C, CardHeader as w, CardTitle as T, GlassCard as se, GradientText as E, IllustratedEmptyState as D, PagePurpose as O, Spinner as k } from "@burdenoff/fe-libs/ui";
17
17
  import { useGlobalUi as A } from "@burdenoff/fe-libs/shared/providers/shell";
@@ -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-dom';\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 { label: 'Remote', value: 'remote' },\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&apos;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 &quot;Generate matches&quot; to rank the candidate pool against this\n posting&apos;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":"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"}
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 { label: 'Remote', value: 'remote' },\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&apos;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 &quot;Generate matches&quot; to rank the candidate pool against this\n posting&apos;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":"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"}
@@ -5,7 +5,7 @@ import { SegmentedTabs as r } from "../components/viz/SegmentedTabs.js";
5
5
  import "../components/viz/index.js";
6
6
  import { useMemo as i, useState as a } from "react";
7
7
  import { Briefcase as o, DollarSign as s, MapPin as c, Plus as l, Search as u, Users as d } from "lucide-react";
8
- import { useNavigate as f } from "react-router-dom";
8
+ import { useNavigate as f } from "react-router";
9
9
  import { jsx as p, jsxs as m } from "react/jsx-runtime";
10
10
  import { Badge as h, Button as g, CardContent as _, CardHeader as v, CardTitle as y, GlassCard as b, GradientText as x, IllustratedEmptyState as S, Input as C, PagePurpose as w, Pagination as T } from "@burdenoff/fe-libs/ui";
11
11
  import { usePagination as E } from "@burdenoff/fe-libs/shared/hooks";
@@ -1 +1 @@
1
- {"version":3,"file":"HiringJobsPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/HiringJobsPagePage.tsx"],"sourcesContent":["import { useMemo, useState, type ReactNode } from 'react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport {\n Badge,\n Button,\n CardContent,\n CardHeader,\n CardTitle,\n GlassCard,\n GradientText,\n IllustratedEmptyState,\n Input,\n PagePurpose,\n Pagination,\n} from '@burdenoff/fe-libs/ui';\nimport { usePagination } from '@burdenoff/fe-libs/shared/hooks';\nimport { Briefcase, DollarSign, MapPin, Plus, Search, Users } from 'lucide-react';\nimport { useNavigate } from 'react-router-dom';\nimport { useCrewFoundryStore, type JobPosting } from '../store/crewfoundryStore';\nimport { formatEnumLabel } from '../utils/formatEnumLabel';\nimport { AvatarStack, SegmentedTabs, type SegmentedTab } from '../components/viz';\n\n/** Ordered status filter set (only rendered when a status has postings). */\nconst STATUS_ORDER: JobPosting['status'][] = ['active', 'draft', 'paused', 'closed', 'filled'];\n\n/** Relative \"Posted N days ago\" label from an ISO date-only string. */\nfunction relativePostedLabel(dateStr: string): string {\n const posted = new Date(dateStr);\n if (Number.isNaN(posted.getTime())) {\n return dateStr;\n }\n const days = Math.floor((Date.now() - posted.getTime()) / 86_400_000);\n if (days <= 0) {\n return 'Today';\n }\n if (days === 1) {\n return '1 day ago';\n }\n if (days < 7) {\n return `${days} days ago`;\n }\n if (days < 30) {\n const weeks = Math.floor(days / 7);\n return weeks === 1 ? '1 week ago' : `${weeks} weeks ago`;\n }\n const months = Math.floor(days / 30);\n return months === 1 ? '1 month ago' : `${months} months ago`;\n}\n\n/** A label→value row matching the mock's clean scannable card layout. */\nfunction InfoRow({ label, children }: { label: string; children: ReactNode }) {\n return (\n <div className=\"flex items-center justify-between gap-3 text-sm\">\n <span className=\"text-text-secondary\">{label}</span>\n <span className=\"font-medium text-text-primary text-right\">{children}</span>\n </div>\n );\n}\n\nexport function HiringJobsPagePage() {\n const { t: translate } = useI18n();\n const navigate = useNavigate();\n const { jobPostings, candidates } = useCrewFoundryStore();\n const [search, setSearch] = useState('');\n const [statusFilter, setStatusFilter] = useState<string>('all');\n const { page, pageSize, offset, setPage } = usePagination({ initialPageSize: 12 });\n const t = (key: string, fallback: string) => {\n const value = translate(key);\n return value === key ? fallback : value;\n };\n\n const filtered = useMemo(() => {\n const query = search.toLowerCase();\n return jobPostings.filter((j) => {\n const matchesSearch = `${j.title} ${j.department} ${j.location}`\n .toLowerCase()\n .includes(query);\n const matchesStatus = statusFilter === 'all' || j.status === statusFilter;\n return matchesSearch && matchesStatus;\n });\n }, [jobPostings, search, statusFilter]);\n\n const paged = filtered.slice(offset, offset + pageSize);\n const openJob = (id: string) =>\n navigate(`../hiring/job-details-page?id=${encodeURIComponent(id)}`);\n const activeJobs = jobPostings.filter((j) => j.status === 'active').length;\n const totalApplicants = jobPostings.reduce((s, j) => s + j.applicants, 0);\n\n // Live per-status counts drive the filter pills.\n const statusTabs = useMemo<SegmentedTab[]>(() => {\n const tabs: SegmentedTab[] = [{ id: 'all', label: 'All', count: jobPostings.length }];\n for (const status of STATUS_ORDER) {\n const count = jobPostings.filter((j) => j.status === status).length;\n if (count > 0 || status === 'active') {\n tabs.push({ id: status, label: formatEnumLabel(status), count });\n }\n }\n return tabs;\n }, [jobPostings]);\n\n // Real applicant faces per job, matched by the candidate's applied-for position.\n const facesByJob = useMemo(() => {\n const map = new Map<string, { name: string }[]>();\n for (const job of jobPostings) {\n const title = job.title.trim().toLowerCase();\n const people = candidates\n .filter((c) => c.position.trim().toLowerCase() === title)\n .map((c) => ({ name: `${c.firstName} ${c.lastName}`.trim() }));\n map.set(job.id, people);\n }\n return map;\n }, [jobPostings, candidates]);\n\n const setFilter = (id: string) => {\n setStatusFilter(id);\n setPage(1);\n };\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between\">\n <div>\n <h1 className=\"text-2xl font-semibold\">\n <GradientText>{t('crewfoundry.hiringJobs.title', 'Job Postings')}</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">\n {activeJobs} {t('crewfoundry.hiringJobs.summary.activePositions', 'active positions')} ·{' '}\n {totalApplicants}{' '}\n {t('crewfoundry.hiringJobs.summary.totalApplicants', 'total applicants')}\n </p>\n <PagePurpose className=\"mt-1\">\n All your open and past job postings in one place — use it to monitor applicant volume\n and manage what roles you&apos;re actively hiring for.\n </PagePurpose>\n </div>\n <Button size=\"sm\" onClick={() => navigate('../hiring/create-job-page')}>\n <Plus className=\"h-4 w-4 mr-1\" />\n {t('crewfoundry.hiringJobs.actions.postNewJob', 'Post New Job')}\n </Button>\n </div>\n\n <div className=\"flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between\">\n <div className=\"-mx-1 overflow-x-auto px-1\">\n <SegmentedTabs\n tabs={statusTabs}\n value={statusFilter}\n onChange={setFilter}\n aria-label=\"Filter job postings by status\"\n />\n </div>\n <div className=\"relative w-full max-w-sm\">\n <Search className=\"absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-text-secondary\" />\n <Input\n placeholder={t('crewfoundry.hiringJobs.searchPlaceholder', 'Search positions...')}\n className=\"pl-9\"\n value={search}\n onChange={(e) => {\n setSearch(e.target.value);\n setPage(1);\n }}\n />\n </div>\n </div>\n\n {filtered.length === 0 && (\n <IllustratedEmptyState\n illustration=\"empty-search\"\n title=\"No job postings found\"\n description=\"Try adjusting your search or status filter, or create a new job posting.\"\n />\n )}\n\n <div className=\"grid gap-4 md:grid-cols-2 xl:grid-cols-3\">\n {paged.map((job) => {\n const faces = facesByJob.get(job.id) ?? [];\n return (\n <GlassCard\n key={job.id}\n treatment=\"glass\"\n role=\"button\"\n tabIndex={0}\n aria-label={`View ${job.title}`}\n className=\"flex cursor-pointer flex-col transition-shadow hover:shadow-elevation-2 focus-visible:outline-none focus-visible:shadow-[var(--shadow-focus)]\"\n onClick={() => openJob(job.id)}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n openJob(job.id);\n }\n }}\n >\n <CardHeader>\n <div className=\"flex items-start justify-between gap-2\">\n <CardTitle className=\"text-lg leading-snug\">{job.title}</CardTitle>\n <Badge\n variant={\n job.status === 'active'\n ? 'default'\n : job.status === 'draft'\n ? 'secondary'\n : 'outline'\n }\n >\n {formatEnumLabel(job.status)}\n </Badge>\n </div>\n <div className=\"mt-2 flex flex-wrap gap-1.5\">\n <Badge variant=\"outline\" className=\"text-[10px]\">\n {formatEnumLabel(job.type)}\n </Badge>\n <Badge variant=\"outline\" className=\"text-[10px]\">\n {formatEnumLabel(job.experienceLevel)}\n </Badge>\n </div>\n </CardHeader>\n <CardContent className=\"flex flex-1 flex-col gap-2.5\">\n <InfoRow label={t('crewfoundry.hiringJobs.card.department', 'Department')}>\n <span className=\"inline-flex items-center gap-1\">\n <Briefcase className=\"h-3 w-3 text-text-secondary\" />\n {job.department || '—'}\n </span>\n </InfoRow>\n <InfoRow label={t('crewfoundry.hiringJobs.card.location', 'Location')}>\n <span className=\"inline-flex items-center gap-1\">\n <MapPin className=\"h-3 w-3 text-text-secondary\" />\n {job.location || '—'}\n </span>\n </InfoRow>\n <InfoRow label={t('crewfoundry.hiringJobs.card.salary', 'Salary')}>\n <span className=\"inline-flex items-center gap-0.5 tabular-nums\">\n <DollarSign className=\"h-3 w-3 text-text-secondary\" />\n {(job.salaryMin / 1000).toFixed(0)}k – ${(job.salaryMax / 1000).toFixed(0)}k\n </span>\n </InfoRow>\n <InfoRow label={t('crewfoundry.hiringJobs.card.applicantsLabel', 'Applicants')}>\n <span className=\"inline-flex items-center gap-2\">\n {faces.length > 0 && <AvatarStack people={faces} max={4} size={22} />}\n <span className=\"inline-flex items-center gap-1 tabular-nums\">\n <Users className=\"h-3 w-3 text-text-secondary\" />\n {job.applicants}\n </span>\n </span>\n </InfoRow>\n <div className=\"mt-auto pt-1\">\n <InfoRow label={t('crewfoundry.hiringJobs.card.posted', 'Posted')}>\n <span className=\"text-text-secondary font-normal\">\n {relativePostedLabel(job.postedDate)}\n </span>\n </InfoRow>\n </div>\n </CardContent>\n </GlassCard>\n );\n })}\n </div>\n\n {filtered.length > pageSize && (\n <Pagination\n page={page}\n pageSize={pageSize}\n totalItems={filtered.length}\n onPageChange={setPage}\n />\n )}\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"HiringJobsPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/HiringJobsPagePage.tsx"],"sourcesContent":["import { useMemo, useState, type ReactNode } from 'react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport {\n Badge,\n Button,\n CardContent,\n CardHeader,\n CardTitle,\n GlassCard,\n GradientText,\n IllustratedEmptyState,\n Input,\n PagePurpose,\n Pagination,\n} from '@burdenoff/fe-libs/ui';\nimport { usePagination } from '@burdenoff/fe-libs/shared/hooks';\nimport { Briefcase, DollarSign, MapPin, Plus, Search, Users } from 'lucide-react';\nimport { useNavigate } from 'react-router';\nimport { useCrewFoundryStore, type JobPosting } from '../store/crewfoundryStore';\nimport { formatEnumLabel } from '../utils/formatEnumLabel';\nimport { AvatarStack, SegmentedTabs, type SegmentedTab } from '../components/viz';\n\n/** Ordered status filter set (only rendered when a status has postings). */\nconst STATUS_ORDER: JobPosting['status'][] = ['active', 'draft', 'paused', 'closed', 'filled'];\n\n/** Relative \"Posted N days ago\" label from an ISO date-only string. */\nfunction relativePostedLabel(dateStr: string): string {\n const posted = new Date(dateStr);\n if (Number.isNaN(posted.getTime())) {\n return dateStr;\n }\n const days = Math.floor((Date.now() - posted.getTime()) / 86_400_000);\n if (days <= 0) {\n return 'Today';\n }\n if (days === 1) {\n return '1 day ago';\n }\n if (days < 7) {\n return `${days} days ago`;\n }\n if (days < 30) {\n const weeks = Math.floor(days / 7);\n return weeks === 1 ? '1 week ago' : `${weeks} weeks ago`;\n }\n const months = Math.floor(days / 30);\n return months === 1 ? '1 month ago' : `${months} months ago`;\n}\n\n/** A label→value row matching the mock's clean scannable card layout. */\nfunction InfoRow({ label, children }: { label: string; children: ReactNode }) {\n return (\n <div className=\"flex items-center justify-between gap-3 text-sm\">\n <span className=\"text-text-secondary\">{label}</span>\n <span className=\"font-medium text-text-primary text-right\">{children}</span>\n </div>\n );\n}\n\nexport function HiringJobsPagePage() {\n const { t: translate } = useI18n();\n const navigate = useNavigate();\n const { jobPostings, candidates } = useCrewFoundryStore();\n const [search, setSearch] = useState('');\n const [statusFilter, setStatusFilter] = useState<string>('all');\n const { page, pageSize, offset, setPage } = usePagination({ initialPageSize: 12 });\n const t = (key: string, fallback: string) => {\n const value = translate(key);\n return value === key ? fallback : value;\n };\n\n const filtered = useMemo(() => {\n const query = search.toLowerCase();\n return jobPostings.filter((j) => {\n const matchesSearch = `${j.title} ${j.department} ${j.location}`\n .toLowerCase()\n .includes(query);\n const matchesStatus = statusFilter === 'all' || j.status === statusFilter;\n return matchesSearch && matchesStatus;\n });\n }, [jobPostings, search, statusFilter]);\n\n const paged = filtered.slice(offset, offset + pageSize);\n const openJob = (id: string) =>\n navigate(`../hiring/job-details-page?id=${encodeURIComponent(id)}`);\n const activeJobs = jobPostings.filter((j) => j.status === 'active').length;\n const totalApplicants = jobPostings.reduce((s, j) => s + j.applicants, 0);\n\n // Live per-status counts drive the filter pills.\n const statusTabs = useMemo<SegmentedTab[]>(() => {\n const tabs: SegmentedTab[] = [{ id: 'all', label: 'All', count: jobPostings.length }];\n for (const status of STATUS_ORDER) {\n const count = jobPostings.filter((j) => j.status === status).length;\n if (count > 0 || status === 'active') {\n tabs.push({ id: status, label: formatEnumLabel(status), count });\n }\n }\n return tabs;\n }, [jobPostings]);\n\n // Real applicant faces per job, matched by the candidate's applied-for position.\n const facesByJob = useMemo(() => {\n const map = new Map<string, { name: string }[]>();\n for (const job of jobPostings) {\n const title = job.title.trim().toLowerCase();\n const people = candidates\n .filter((c) => c.position.trim().toLowerCase() === title)\n .map((c) => ({ name: `${c.firstName} ${c.lastName}`.trim() }));\n map.set(job.id, people);\n }\n return map;\n }, [jobPostings, candidates]);\n\n const setFilter = (id: string) => {\n setStatusFilter(id);\n setPage(1);\n };\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between\">\n <div>\n <h1 className=\"text-2xl font-semibold\">\n <GradientText>{t('crewfoundry.hiringJobs.title', 'Job Postings')}</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">\n {activeJobs} {t('crewfoundry.hiringJobs.summary.activePositions', 'active positions')} ·{' '}\n {totalApplicants}{' '}\n {t('crewfoundry.hiringJobs.summary.totalApplicants', 'total applicants')}\n </p>\n <PagePurpose className=\"mt-1\">\n All your open and past job postings in one place — use it to monitor applicant volume\n and manage what roles you&apos;re actively hiring for.\n </PagePurpose>\n </div>\n <Button size=\"sm\" onClick={() => navigate('../hiring/create-job-page')}>\n <Plus className=\"h-4 w-4 mr-1\" />\n {t('crewfoundry.hiringJobs.actions.postNewJob', 'Post New Job')}\n </Button>\n </div>\n\n <div className=\"flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between\">\n <div className=\"-mx-1 overflow-x-auto px-1\">\n <SegmentedTabs\n tabs={statusTabs}\n value={statusFilter}\n onChange={setFilter}\n aria-label=\"Filter job postings by status\"\n />\n </div>\n <div className=\"relative w-full max-w-sm\">\n <Search className=\"absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-text-secondary\" />\n <Input\n placeholder={t('crewfoundry.hiringJobs.searchPlaceholder', 'Search positions...')}\n className=\"pl-9\"\n value={search}\n onChange={(e) => {\n setSearch(e.target.value);\n setPage(1);\n }}\n />\n </div>\n </div>\n\n {filtered.length === 0 && (\n <IllustratedEmptyState\n illustration=\"empty-search\"\n title=\"No job postings found\"\n description=\"Try adjusting your search or status filter, or create a new job posting.\"\n />\n )}\n\n <div className=\"grid gap-4 md:grid-cols-2 xl:grid-cols-3\">\n {paged.map((job) => {\n const faces = facesByJob.get(job.id) ?? [];\n return (\n <GlassCard\n key={job.id}\n treatment=\"glass\"\n role=\"button\"\n tabIndex={0}\n aria-label={`View ${job.title}`}\n className=\"flex cursor-pointer flex-col transition-shadow hover:shadow-elevation-2 focus-visible:outline-none focus-visible:shadow-[var(--shadow-focus)]\"\n onClick={() => openJob(job.id)}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n openJob(job.id);\n }\n }}\n >\n <CardHeader>\n <div className=\"flex items-start justify-between gap-2\">\n <CardTitle className=\"text-lg leading-snug\">{job.title}</CardTitle>\n <Badge\n variant={\n job.status === 'active'\n ? 'default'\n : job.status === 'draft'\n ? 'secondary'\n : 'outline'\n }\n >\n {formatEnumLabel(job.status)}\n </Badge>\n </div>\n <div className=\"mt-2 flex flex-wrap gap-1.5\">\n <Badge variant=\"outline\" className=\"text-[10px]\">\n {formatEnumLabel(job.type)}\n </Badge>\n <Badge variant=\"outline\" className=\"text-[10px]\">\n {formatEnumLabel(job.experienceLevel)}\n </Badge>\n </div>\n </CardHeader>\n <CardContent className=\"flex flex-1 flex-col gap-2.5\">\n <InfoRow label={t('crewfoundry.hiringJobs.card.department', 'Department')}>\n <span className=\"inline-flex items-center gap-1\">\n <Briefcase className=\"h-3 w-3 text-text-secondary\" />\n {job.department || '—'}\n </span>\n </InfoRow>\n <InfoRow label={t('crewfoundry.hiringJobs.card.location', 'Location')}>\n <span className=\"inline-flex items-center gap-1\">\n <MapPin className=\"h-3 w-3 text-text-secondary\" />\n {job.location || '—'}\n </span>\n </InfoRow>\n <InfoRow label={t('crewfoundry.hiringJobs.card.salary', 'Salary')}>\n <span className=\"inline-flex items-center gap-0.5 tabular-nums\">\n <DollarSign className=\"h-3 w-3 text-text-secondary\" />\n {(job.salaryMin / 1000).toFixed(0)}k – ${(job.salaryMax / 1000).toFixed(0)}k\n </span>\n </InfoRow>\n <InfoRow label={t('crewfoundry.hiringJobs.card.applicantsLabel', 'Applicants')}>\n <span className=\"inline-flex items-center gap-2\">\n {faces.length > 0 && <AvatarStack people={faces} max={4} size={22} />}\n <span className=\"inline-flex items-center gap-1 tabular-nums\">\n <Users className=\"h-3 w-3 text-text-secondary\" />\n {job.applicants}\n </span>\n </span>\n </InfoRow>\n <div className=\"mt-auto pt-1\">\n <InfoRow label={t('crewfoundry.hiringJobs.card.posted', 'Posted')}>\n <span className=\"text-text-secondary font-normal\">\n {relativePostedLabel(job.postedDate)}\n </span>\n </InfoRow>\n </div>\n </CardContent>\n </GlassCard>\n );\n })}\n </div>\n\n {filtered.length > pageSize && (\n <Pagination\n page={page}\n pageSize={pageSize}\n totalItems={filtered.length}\n onPageChange={setPage}\n />\n )}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -9,7 +9,7 @@ import { ConfirmDialog as o } from "../components/forms/ConfirmDialog.js";
9
9
  import { AttachDocumentDialog as s } from "../components/forms/AttachDocumentDialog.js";
10
10
  import { useEffect as ee, useRef as te, useState as c } from "react";
11
11
  import { ArrowLeft as l, BadgeCheck as ne, CalendarDays as re, FileText as ie, Mail as ae, MapPin as u, Phone as oe, Plus as d, Trash2 as f } from "lucide-react";
12
- import { useNavigate as p, useSearchParams as se } from "react-router-dom";
12
+ import { useNavigate as p, useSearchParams as se } from "react-router";
13
13
  import { jsx as m, jsxs as h } from "react/jsx-runtime";
14
14
  import { Badge as g, Button as _, Card as v, CardContent as y, CardHeader as b, CardTitle as x, GlassCard as S, GradientText as ce, IllustratedEmptyState as C, PagePurpose as w, Spinner as T } from "@burdenoff/fe-libs/ui";
15
15
  import { useGlobalUi as E } from "@burdenoff/fe-libs/shared/providers/shell";
@@ -1 +1 @@
1
- {"version":3,"file":"HrEmployeeProfilePagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/HrEmployeeProfilePagePage.tsx"],"sourcesContent":["import { useEffect, useRef, useState, type ReactNode } from 'react';\nimport {\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 { useGlobalUi } from '@burdenoff/fe-libs/shared/providers/shell';\nimport {\n ArrowLeft,\n BadgeCheck,\n CalendarDays,\n FileText,\n Mail,\n MapPin,\n Phone,\n Plus,\n Trash2,\n} from 'lucide-react';\nimport { useNavigate, useSearchParams } from 'react-router-dom';\nimport { useCrewFoundryStore, type Employee } from '../store/crewfoundryStore';\nimport { formatEnumLabel } from '../utils/formatEnumLabel';\nimport {\n AvatarStack,\n ProgressRing,\n SegmentedTabs,\n Timeline,\n type SegmentedTab,\n type TimelineItem,\n type TimelineTone,\n} from '../components/viz';\nimport { AttachDocumentDialog } from '../components/forms/AttachDocumentDialog';\nimport { ConfirmDialog } from '../components/forms/ConfirmDialog';\n\ntype ProfileTab = 'overview' | 'skills' | 'documents' | 'performance' | 'activity';\n\nconst PROFILE_TABS: SegmentedTab[] = [\n { id: 'overview', label: 'Overview' },\n { id: 'skills', label: 'Skills' },\n { id: 'documents', label: 'Documents' },\n { id: 'performance', label: 'Performance' },\n { id: 'activity', label: 'Activity' },\n];\n\nconst SALARY_BANDS: { min: number; max: number }[] = [\n { min: 0, max: 60_000 },\n { min: 60_000, max: 90_000 },\n { min: 90_000, max: 120_000 },\n { min: 120_000, max: 150_000 },\n { min: 150_000, max: 200_000 },\n { min: 200_000, max: Number.POSITIVE_INFINITY },\n];\n\nfunction salaryBand(salary: number): string {\n const index = SALARY_BANDS.findIndex((band) => salary >= band.min && salary < band.max);\n const safeIndex = index === -1 ? SALARY_BANDS.length - 1 : index;\n const band = SALARY_BANDS[safeIndex];\n const low = `$${Math.round(band.min / 1000)}k`;\n const high = band.max === Number.POSITIVE_INFINITY ? '+' : `–$${Math.round(band.max / 1000)}k`;\n return `Band ${safeIndex + 1} · ${low}${high}`;\n}\n\nfunction tenure(startDate: string): string {\n const start = new Date(startDate);\n if (Number.isNaN(start.getTime())) {\n return '—';\n }\n const now = new Date();\n let months = (now.getFullYear() - start.getFullYear()) * 12 + (now.getMonth() - start.getMonth());\n if (now.getDate() < start.getDate()) {\n months -= 1;\n }\n if (months < 0) {\n months = 0;\n }\n const years = Math.floor(months / 12);\n const remMonths = months % 12;\n const parts: string[] = [];\n if (years > 0) {\n parts.push(`${years} year${years === 1 ? '' : 's'}`);\n }\n parts.push(`${remMonths} month${remMonths === 1 ? '' : 's'}`);\n return parts.join(', ');\n}\n\nfunction performanceLabel(score: number): string {\n if (score >= 4.5) return 'Excellent';\n if (score >= 3.5) return 'Strong';\n if (score >= 2.5) return 'Meets expectations';\n return 'Needs improvement';\n}\n\nfunction statusBadgeVariant(status: Employee['status']): 'default' | 'secondary' | 'destructive' {\n if (status === 'active') return 'default';\n if (status === 'on_leave') return 'secondary';\n return 'destructive';\n}\n\nfunction ContactChip({ icon, children }: { icon: ReactNode; children: ReactNode }) {\n return (\n <span className=\"inline-flex items-center gap-1.5 rounded-full border border-border-subtle bg-bg-surface px-3 py-1 text-xs text-text-secondary\">\n <span className=\"text-text-muted\">{icon}</span>\n <span className=\"truncate\">{children}</span>\n </span>\n );\n}\n\nfunction DetailRow({ label, children }: { label: string; children: ReactNode }) {\n return (\n <div className=\"flex items-start justify-between gap-4 py-2 text-sm\">\n <span className=\"text-text-secondary\">{label}</span>\n <span className=\"text-right font-medium text-text-primary\">{children}</span>\n </div>\n );\n}\n\ninterface ActivitySignal {\n ts: number;\n item: TimelineItem;\n}\n\nfunction buildActivity(\n employee: Employee,\n timeEntries: ReturnType<typeof useCrewFoundryStore.getState>['timeEntries'],\n leaveRequests: ReturnType<typeof useCrewFoundryStore.getState>['leaveRequests'],\n payrollRecords: ReturnType<typeof useCrewFoundryStore.getState>['payrollRecords'],\n projects: ReturnType<typeof useCrewFoundryStore.getState>['projects']\n): TimelineItem[] {\n const signals: ActivitySignal[] = [];\n const toTs = (value: string): number => {\n const parsed = new Date(value).getTime();\n return Number.isNaN(parsed) ? 0 : parsed;\n };\n\n for (const project of projects.filter((p) => p.managerId === employee.id)) {\n signals.push({\n ts: toTs(project.startDate),\n item: {\n id: `project-${project.id}`,\n title: `Leads project “${project.name}”`,\n meta: `${formatEnumLabel(project.status)} · ${project.progress}% complete`,\n time: project.startDate,\n tone: 'info',\n },\n });\n }\n\n for (const leave of leaveRequests.filter((l) => l.employeeId === employee.id)) {\n const tone: TimelineTone =\n leave.status === 'approved'\n ? 'success'\n : leave.status === 'rejected'\n ? 'danger'\n : leave.status === 'pending'\n ? 'warning'\n : 'muted';\n signals.push({\n ts: toTs(leave.startDate),\n item: {\n id: `leave-${leave.id}`,\n title: `${formatEnumLabel(leave.type)} leave — ${formatEnumLabel(leave.status)}`,\n meta: `${leave.startDate} → ${leave.endDate} · ${leave.days} day${leave.days === 1 ? '' : 's'}`,\n time: leave.startDate,\n tone,\n },\n });\n }\n\n for (const record of payrollRecords.filter((r) => r.employeeId === employee.id)) {\n signals.push({\n ts: toTs(record.payDate),\n item: {\n id: `payroll-${record.id}`,\n title: `Payroll · ${record.period}`,\n meta: `Net $${record.netPay.toLocaleString()} (${formatEnumLabel(record.status)})`,\n time: record.payDate,\n tone: record.status === 'paid' ? 'success' : 'muted',\n },\n });\n }\n\n for (const entry of timeEntries.filter((t) => t.employeeId === employee.id)) {\n signals.push({\n ts: toTs(entry.date),\n item: {\n id: `time-${entry.id}`,\n title: `Logged ${entry.totalHours}h on ${entry.project || 'work'}`,\n meta: formatEnumLabel(entry.status),\n time: entry.date,\n tone: 'accent',\n },\n });\n }\n\n return signals\n .sort((a, b) => b.ts - a.ts)\n .slice(0, 14)\n .map((signal) => signal.item);\n}\n\nexport function HrEmployeeProfilePagePage() {\n const navigate = useNavigate();\n const [searchParams] = useSearchParams();\n const employees = useCrewFoundryStore((state) => state.employees);\n const timeEntries = useCrewFoundryStore((state) => state.timeEntries);\n const leaveRequests = useCrewFoundryStore((state) => state.leaveRequests);\n const payrollRecords = useCrewFoundryStore((state) => state.payrollRecords);\n const projects = useCrewFoundryStore((state) => state.projects);\n const initialized = useCrewFoundryStore((state) => state.initialized);\n const employeeDocuments = useCrewFoundryStore((state) => state.employeeDocuments);\n const fetchEmployeeDocuments = useCrewFoundryStore((state) => state.fetchEmployeeDocuments);\n const uploadEmployeeDocument = useCrewFoundryStore((state) => state.uploadEmployeeDocument);\n const detachEmployeeDocument = useCrewFoundryStore((state) => state.detachEmployeeDocument);\n const { showToast } = useGlobalUi();\n const [tab, setTab] = useState<ProfileTab>('overview');\n const [attachDialogOpen, setAttachDialogOpen] = useState(false);\n const [removeDocumentId, setRemoveDocumentId] = useState<string | null>(null);\n // A ref (not state) on purpose: this only gates a fetch side-effect and\n // must never itself trigger a re-render, which is exactly what a `useState`\n // setter called synchronously inside an effect would do.\n const documentsLoadedForRef = useRef<string | null>(null);\n\n const id = searchParams.get('id');\n const employee = employees.find((candidate) => candidate.id === id);\n\n // Lazy-load this employee's documents the first time the Documents tab is\n // opened (documents aren't part of the crewfoundryBootstrap firehose).\n useEffect(() => {\n if (tab === 'documents' && employee && documentsLoadedForRef.current !== employee.id) {\n documentsLoadedForRef.current = employee.id;\n fetchEmployeeDocuments(employee.id).catch((error: unknown) => {\n showToast(error instanceof Error ? error.message : 'Unable to load documents', 'error');\n });\n }\n }, [tab, employee, fetchEmployeeDocuments, showToast]);\n\n if (!initialized && !employee) {\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 (!employee) {\n return (\n <div className=\"space-y-4 p-6\">\n <Button variant=\"ghost\" size=\"sm\" onClick={() => navigate('../hr/employees-page')}>\n <ArrowLeft className=\"mr-1 h-4 w-4\" />\n Back to directory\n </Button>\n <IllustratedEmptyState\n illustration=\"empty-search\"\n title=\"Employee not found\"\n description=\"This profile is no longer available or the link is out of date.\"\n />\n </div>\n );\n }\n\n const manager = employee.managerId\n ? employees.find((candidate) => candidate.id === employee.managerId)\n : undefined;\n const managerName = manager\n ? `${manager.firstName} ${manager.lastName}`\n : employee.managerId\n ? 'Assigned'\n : 'No manager';\n const fullName = `${employee.firstName} ${employee.lastName}`;\n const activity = buildActivity(employee, timeEntries, leaveRequests, payrollRecords, projects);\n\n return (\n <div className=\"space-y-6 p-6\">\n <Button variant=\"ghost\" size=\"sm\" onClick={() => navigate('../hr/employees-page')}>\n <ArrowLeft className=\"mr-1 h-4 w-4\" />\n Back to directory\n </Button>\n\n {/* Profile header */}\n <GlassCard treatment=\"glass\" glow className=\"border-border-seam\">\n <CardContent className=\"p-6\">\n <div className=\"flex flex-col gap-5 sm:flex-row sm:items-start\">\n <AvatarStack\n people={[{ name: fullName, avatarUrl: employee.avatar }]}\n max={1}\n size={72}\n />\n <div className=\"flex-1 space-y-3\">\n <div className=\"flex flex-wrap items-center gap-3\">\n <h1 className=\"text-2xl font-semibold\">\n <GradientText>{fullName}</GradientText>\n </h1>\n <Badge variant={statusBadgeVariant(employee.status)}>\n {formatEnumLabel(employee.status)}\n </Badge>\n </div>\n <p className=\"text-sm text-text-secondary\">\n {employee.position} · {employee.department}\n </p>\n <div className=\"flex flex-wrap gap-2\">\n <ContactChip icon={<Mail className=\"h-3.5 w-3.5\" />}>{employee.email}</ContactChip>\n <ContactChip icon={<Phone className=\"h-3.5 w-3.5\" />}>{employee.phone}</ContactChip>\n <ContactChip icon={<MapPin className=\"h-3.5 w-3.5\" />}>\n {employee.location}\n </ContactChip>\n <ContactChip icon={<CalendarDays className=\"h-3.5 w-3.5\" />}>\n Joined {employee.startDate}\n </ContactChip>\n </div>\n </div>\n </div>\n </CardContent>\n </GlassCard>\n\n <PagePurpose>\n A single view of one team member — their role, tenure, performance, and recent activity — so\n managers and partners have the full context in one place.\n </PagePurpose>\n\n <SegmentedTabs\n tabs={PROFILE_TABS}\n value={tab}\n onChange={(next) => setTab(next as ProfileTab)}\n aria-label=\"Employee profile sections\"\n />\n\n {tab === 'overview' ? (\n <div className=\"grid gap-4 lg:grid-cols-2\">\n <Card>\n <CardHeader>\n <CardTitle className=\"text-base\">Details</CardTitle>\n </CardHeader>\n <CardContent className=\"divide-y divide-border-subtle pt-0\">\n <DetailRow label=\"Manager\">\n <span className=\"inline-flex items-center gap-2\">\n {manager ? (\n <AvatarStack\n people={[{ name: managerName, avatarUrl: manager.avatar }]}\n max={1}\n size={24}\n />\n ) : null}\n {managerName}\n </span>\n </DetailRow>\n <DetailRow label=\"Employment type\">{formatEnumLabel(employee.type)}</DetailRow>\n <DetailRow label=\"Salary band\">{salaryBand(employee.salary)}</DetailRow>\n <DetailRow label=\"Tenure\">{tenure(employee.startDate)}</DetailRow>\n <DetailRow label=\"Employee ID\">\n <span className=\"break-all font-mono text-xs\">{employee.id}</span>\n </DetailRow>\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle className=\"text-base\">Performance</CardTitle>\n </CardHeader>\n <CardContent className=\"flex items-center gap-6 pt-0\">\n <ProgressRing\n value={employee.performanceScore}\n max={5}\n label={employee.performanceScore.toFixed(1)}\n caption=\"/ 5\"\n />\n <div className=\"space-y-1\">\n <p className=\"text-lg font-semibold text-text-primary\">\n {performanceLabel(employee.performanceScore)}\n </p>\n <p className=\"text-sm text-text-secondary\">\n Latest review score of {employee.performanceScore.toFixed(1)} out of 5.\n </p>\n </div>\n </CardContent>\n </Card>\n </div>\n ) : null}\n\n {tab === 'skills' ? (\n <Card>\n <CardHeader>\n <CardTitle className=\"text-base\">Skills</CardTitle>\n </CardHeader>\n <CardContent className=\"pt-0\">\n {employee.skills.length > 0 ? (\n // Real skill tags from the employee record. Rendered as chips (not\n // proficiency bars) because the backend stores tags, not levels —\n // inventing a proficiency % would fabricate data.\n <div className=\"flex flex-wrap gap-2\">\n {employee.skills.map((skill) => (\n <Badge key={skill} variant=\"secondary\" className=\"text-xs\">\n {skill}\n </Badge>\n ))}\n </div>\n ) : (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"No skills recorded\"\n description=\"This team member has no skills captured yet. Add skills from the employee directory to surface them here.\"\n />\n )}\n </CardContent>\n </Card>\n ) : null}\n\n {tab === 'documents' ? (\n <Card>\n <CardHeader className=\"flex flex-row items-center justify-between\">\n <CardTitle className=\"text-base\">Documents</CardTitle>\n <Button size=\"sm\" onClick={() => setAttachDialogOpen(true)}>\n <Plus className=\"mr-1.5 h-3.5 w-3.5\" />\n Attach document\n </Button>\n </CardHeader>\n <CardContent className=\"pt-0\">\n {employeeDocuments.length > 0 ? (\n <div className=\"space-y-2\">\n {employeeDocuments.map((doc) => (\n <div\n key={doc.id}\n className=\"flex items-center justify-between gap-3 rounded-input border border-border-subtle bg-bg-surface px-3 py-2.5\"\n >\n <div className=\"flex min-w-0 items-center gap-3\">\n <FileText className=\"h-4 w-4 shrink-0 text-text-muted\" />\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {doc.title}\n </p>\n <p className=\"text-xs text-text-muted\">\n {doc.category ? `${doc.category} · ` : ''}\n Uploaded {doc.uploadedAt.slice(0, 10)}\n </p>\n </div>\n </div>\n <button\n type=\"button\"\n aria-label={`Remove document \"${doc.title}\"`}\n className=\"shrink-0 text-text-muted hover:text-status-error-fg\"\n onClick={() => setRemoveDocumentId(doc.id)}\n >\n <Trash2 className=\"h-4 w-4\" />\n </button>\n </div>\n ))}\n </div>\n ) : (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"No documents attached\"\n description=\"Offer letters, contracts, and compliance documents attached to this profile will appear here.\"\n action={\n <Button size=\"sm\" onClick={() => setAttachDialogOpen(true)}>\n <Plus className=\"mr-1.5 h-3.5 w-3.5\" />\n Attach document\n </Button>\n }\n />\n )}\n </CardContent>\n </Card>\n ) : null}\n\n {tab === 'performance' ? (\n <Card>\n <CardHeader>\n <CardTitle className=\"text-base\">Performance</CardTitle>\n </CardHeader>\n <CardContent className=\"flex flex-col items-center gap-6 pt-0 sm:flex-row sm:items-center\">\n <ProgressRing\n value={employee.performanceScore}\n max={5}\n size={160}\n label={employee.performanceScore.toFixed(1)}\n caption=\"/ 5\"\n />\n <div className=\"flex-1 space-y-2\">\n <Badge variant=\"secondary\" className=\"gap-1\">\n <BadgeCheck className=\"h-3.5 w-3.5\" />\n {performanceLabel(employee.performanceScore)}\n </Badge>\n <p className=\"text-sm text-text-secondary\">\n {fullName} holds a review score of {employee.performanceScore.toFixed(1)} out of 5.\n </p>\n {/* needs backend fields: goals-met %, peer feedback, manager feedback — only a single\n performanceScore is modelled today, so sub-metrics are intentionally omitted rather\n than invented. */}\n <p className=\"text-xs text-text-muted\">\n Goal, peer, and manager sub-scores will appear here once captured on the review\n record.\n </p>\n </div>\n </CardContent>\n </Card>\n ) : null}\n\n {tab === 'activity' ? (\n <Card>\n <CardHeader>\n <CardTitle className=\"text-base\">Recent activity</CardTitle>\n </CardHeader>\n <CardContent className=\"pt-0\">\n {activity.length > 0 ? (\n <Timeline items={activity} />\n ) : (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"No activity yet\"\n description=\"Time logs, leave, payroll, and project assignments for this employee will show up here.\"\n />\n )}\n </CardContent>\n </Card>\n ) : null}\n\n <AttachDocumentDialog\n open={attachDialogOpen}\n onOpenChange={setAttachDialogOpen}\n onUpload={({ file, title, category }) =>\n uploadEmployeeDocument({ employeeId: employee.id, file, title, category })\n }\n />\n\n <ConfirmDialog\n open={removeDocumentId !== null}\n onOpenChange={(open) => {\n if (!open) setRemoveDocumentId(null);\n }}\n title=\"Remove document?\"\n description=\"This unlinks the document from this employee's profile. The file itself stays in the workspace Files library.\"\n confirmLabel=\"Remove\"\n destructive\n onConfirm={async () => {\n if (removeDocumentId) {\n await detachEmployeeDocument(removeDocumentId);\n setRemoveDocumentId(null);\n }\n }}\n />\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"HrEmployeeProfilePagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/HrEmployeeProfilePagePage.tsx"],"sourcesContent":["import { useEffect, useRef, useState, type ReactNode } from 'react';\nimport {\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 { useGlobalUi } from '@burdenoff/fe-libs/shared/providers/shell';\nimport {\n ArrowLeft,\n BadgeCheck,\n CalendarDays,\n FileText,\n Mail,\n MapPin,\n Phone,\n Plus,\n Trash2,\n} from 'lucide-react';\nimport { useNavigate, useSearchParams } from 'react-router';\nimport { useCrewFoundryStore, type Employee } from '../store/crewfoundryStore';\nimport { formatEnumLabel } from '../utils/formatEnumLabel';\nimport {\n AvatarStack,\n ProgressRing,\n SegmentedTabs,\n Timeline,\n type SegmentedTab,\n type TimelineItem,\n type TimelineTone,\n} from '../components/viz';\nimport { AttachDocumentDialog } from '../components/forms/AttachDocumentDialog';\nimport { ConfirmDialog } from '../components/forms/ConfirmDialog';\n\ntype ProfileTab = 'overview' | 'skills' | 'documents' | 'performance' | 'activity';\n\nconst PROFILE_TABS: SegmentedTab[] = [\n { id: 'overview', label: 'Overview' },\n { id: 'skills', label: 'Skills' },\n { id: 'documents', label: 'Documents' },\n { id: 'performance', label: 'Performance' },\n { id: 'activity', label: 'Activity' },\n];\n\nconst SALARY_BANDS: { min: number; max: number }[] = [\n { min: 0, max: 60_000 },\n { min: 60_000, max: 90_000 },\n { min: 90_000, max: 120_000 },\n { min: 120_000, max: 150_000 },\n { min: 150_000, max: 200_000 },\n { min: 200_000, max: Number.POSITIVE_INFINITY },\n];\n\nfunction salaryBand(salary: number): string {\n const index = SALARY_BANDS.findIndex((band) => salary >= band.min && salary < band.max);\n const safeIndex = index === -1 ? SALARY_BANDS.length - 1 : index;\n const band = SALARY_BANDS[safeIndex];\n const low = `$${Math.round(band.min / 1000)}k`;\n const high = band.max === Number.POSITIVE_INFINITY ? '+' : `–$${Math.round(band.max / 1000)}k`;\n return `Band ${safeIndex + 1} · ${low}${high}`;\n}\n\nfunction tenure(startDate: string): string {\n const start = new Date(startDate);\n if (Number.isNaN(start.getTime())) {\n return '—';\n }\n const now = new Date();\n let months = (now.getFullYear() - start.getFullYear()) * 12 + (now.getMonth() - start.getMonth());\n if (now.getDate() < start.getDate()) {\n months -= 1;\n }\n if (months < 0) {\n months = 0;\n }\n const years = Math.floor(months / 12);\n const remMonths = months % 12;\n const parts: string[] = [];\n if (years > 0) {\n parts.push(`${years} year${years === 1 ? '' : 's'}`);\n }\n parts.push(`${remMonths} month${remMonths === 1 ? '' : 's'}`);\n return parts.join(', ');\n}\n\nfunction performanceLabel(score: number): string {\n if (score >= 4.5) return 'Excellent';\n if (score >= 3.5) return 'Strong';\n if (score >= 2.5) return 'Meets expectations';\n return 'Needs improvement';\n}\n\nfunction statusBadgeVariant(status: Employee['status']): 'default' | 'secondary' | 'destructive' {\n if (status === 'active') return 'default';\n if (status === 'on_leave') return 'secondary';\n return 'destructive';\n}\n\nfunction ContactChip({ icon, children }: { icon: ReactNode; children: ReactNode }) {\n return (\n <span className=\"inline-flex items-center gap-1.5 rounded-full border border-border-subtle bg-bg-surface px-3 py-1 text-xs text-text-secondary\">\n <span className=\"text-text-muted\">{icon}</span>\n <span className=\"truncate\">{children}</span>\n </span>\n );\n}\n\nfunction DetailRow({ label, children }: { label: string; children: ReactNode }) {\n return (\n <div className=\"flex items-start justify-between gap-4 py-2 text-sm\">\n <span className=\"text-text-secondary\">{label}</span>\n <span className=\"text-right font-medium text-text-primary\">{children}</span>\n </div>\n );\n}\n\ninterface ActivitySignal {\n ts: number;\n item: TimelineItem;\n}\n\nfunction buildActivity(\n employee: Employee,\n timeEntries: ReturnType<typeof useCrewFoundryStore.getState>['timeEntries'],\n leaveRequests: ReturnType<typeof useCrewFoundryStore.getState>['leaveRequests'],\n payrollRecords: ReturnType<typeof useCrewFoundryStore.getState>['payrollRecords'],\n projects: ReturnType<typeof useCrewFoundryStore.getState>['projects']\n): TimelineItem[] {\n const signals: ActivitySignal[] = [];\n const toTs = (value: string): number => {\n const parsed = new Date(value).getTime();\n return Number.isNaN(parsed) ? 0 : parsed;\n };\n\n for (const project of projects.filter((p) => p.managerId === employee.id)) {\n signals.push({\n ts: toTs(project.startDate),\n item: {\n id: `project-${project.id}`,\n title: `Leads project “${project.name}”`,\n meta: `${formatEnumLabel(project.status)} · ${project.progress}% complete`,\n time: project.startDate,\n tone: 'info',\n },\n });\n }\n\n for (const leave of leaveRequests.filter((l) => l.employeeId === employee.id)) {\n const tone: TimelineTone =\n leave.status === 'approved'\n ? 'success'\n : leave.status === 'rejected'\n ? 'danger'\n : leave.status === 'pending'\n ? 'warning'\n : 'muted';\n signals.push({\n ts: toTs(leave.startDate),\n item: {\n id: `leave-${leave.id}`,\n title: `${formatEnumLabel(leave.type)} leave — ${formatEnumLabel(leave.status)}`,\n meta: `${leave.startDate} → ${leave.endDate} · ${leave.days} day${leave.days === 1 ? '' : 's'}`,\n time: leave.startDate,\n tone,\n },\n });\n }\n\n for (const record of payrollRecords.filter((r) => r.employeeId === employee.id)) {\n signals.push({\n ts: toTs(record.payDate),\n item: {\n id: `payroll-${record.id}`,\n title: `Payroll · ${record.period}`,\n meta: `Net $${record.netPay.toLocaleString()} (${formatEnumLabel(record.status)})`,\n time: record.payDate,\n tone: record.status === 'paid' ? 'success' : 'muted',\n },\n });\n }\n\n for (const entry of timeEntries.filter((t) => t.employeeId === employee.id)) {\n signals.push({\n ts: toTs(entry.date),\n item: {\n id: `time-${entry.id}`,\n title: `Logged ${entry.totalHours}h on ${entry.project || 'work'}`,\n meta: formatEnumLabel(entry.status),\n time: entry.date,\n tone: 'accent',\n },\n });\n }\n\n return signals\n .sort((a, b) => b.ts - a.ts)\n .slice(0, 14)\n .map((signal) => signal.item);\n}\n\nexport function HrEmployeeProfilePagePage() {\n const navigate = useNavigate();\n const [searchParams] = useSearchParams();\n const employees = useCrewFoundryStore((state) => state.employees);\n const timeEntries = useCrewFoundryStore((state) => state.timeEntries);\n const leaveRequests = useCrewFoundryStore((state) => state.leaveRequests);\n const payrollRecords = useCrewFoundryStore((state) => state.payrollRecords);\n const projects = useCrewFoundryStore((state) => state.projects);\n const initialized = useCrewFoundryStore((state) => state.initialized);\n const employeeDocuments = useCrewFoundryStore((state) => state.employeeDocuments);\n const fetchEmployeeDocuments = useCrewFoundryStore((state) => state.fetchEmployeeDocuments);\n const uploadEmployeeDocument = useCrewFoundryStore((state) => state.uploadEmployeeDocument);\n const detachEmployeeDocument = useCrewFoundryStore((state) => state.detachEmployeeDocument);\n const { showToast } = useGlobalUi();\n const [tab, setTab] = useState<ProfileTab>('overview');\n const [attachDialogOpen, setAttachDialogOpen] = useState(false);\n const [removeDocumentId, setRemoveDocumentId] = useState<string | null>(null);\n // A ref (not state) on purpose: this only gates a fetch side-effect and\n // must never itself trigger a re-render, which is exactly what a `useState`\n // setter called synchronously inside an effect would do.\n const documentsLoadedForRef = useRef<string | null>(null);\n\n const id = searchParams.get('id');\n const employee = employees.find((candidate) => candidate.id === id);\n\n // Lazy-load this employee's documents the first time the Documents tab is\n // opened (documents aren't part of the crewfoundryBootstrap firehose).\n useEffect(() => {\n if (tab === 'documents' && employee && documentsLoadedForRef.current !== employee.id) {\n documentsLoadedForRef.current = employee.id;\n fetchEmployeeDocuments(employee.id).catch((error: unknown) => {\n showToast(error instanceof Error ? error.message : 'Unable to load documents', 'error');\n });\n }\n }, [tab, employee, fetchEmployeeDocuments, showToast]);\n\n if (!initialized && !employee) {\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 (!employee) {\n return (\n <div className=\"space-y-4 p-6\">\n <Button variant=\"ghost\" size=\"sm\" onClick={() => navigate('../hr/employees-page')}>\n <ArrowLeft className=\"mr-1 h-4 w-4\" />\n Back to directory\n </Button>\n <IllustratedEmptyState\n illustration=\"empty-search\"\n title=\"Employee not found\"\n description=\"This profile is no longer available or the link is out of date.\"\n />\n </div>\n );\n }\n\n const manager = employee.managerId\n ? employees.find((candidate) => candidate.id === employee.managerId)\n : undefined;\n const managerName = manager\n ? `${manager.firstName} ${manager.lastName}`\n : employee.managerId\n ? 'Assigned'\n : 'No manager';\n const fullName = `${employee.firstName} ${employee.lastName}`;\n const activity = buildActivity(employee, timeEntries, leaveRequests, payrollRecords, projects);\n\n return (\n <div className=\"space-y-6 p-6\">\n <Button variant=\"ghost\" size=\"sm\" onClick={() => navigate('../hr/employees-page')}>\n <ArrowLeft className=\"mr-1 h-4 w-4\" />\n Back to directory\n </Button>\n\n {/* Profile header */}\n <GlassCard treatment=\"glass\" glow className=\"border-border-seam\">\n <CardContent className=\"p-6\">\n <div className=\"flex flex-col gap-5 sm:flex-row sm:items-start\">\n <AvatarStack\n people={[{ name: fullName, avatarUrl: employee.avatar }]}\n max={1}\n size={72}\n />\n <div className=\"flex-1 space-y-3\">\n <div className=\"flex flex-wrap items-center gap-3\">\n <h1 className=\"text-2xl font-semibold\">\n <GradientText>{fullName}</GradientText>\n </h1>\n <Badge variant={statusBadgeVariant(employee.status)}>\n {formatEnumLabel(employee.status)}\n </Badge>\n </div>\n <p className=\"text-sm text-text-secondary\">\n {employee.position} · {employee.department}\n </p>\n <div className=\"flex flex-wrap gap-2\">\n <ContactChip icon={<Mail className=\"h-3.5 w-3.5\" />}>{employee.email}</ContactChip>\n <ContactChip icon={<Phone className=\"h-3.5 w-3.5\" />}>{employee.phone}</ContactChip>\n <ContactChip icon={<MapPin className=\"h-3.5 w-3.5\" />}>\n {employee.location}\n </ContactChip>\n <ContactChip icon={<CalendarDays className=\"h-3.5 w-3.5\" />}>\n Joined {employee.startDate}\n </ContactChip>\n </div>\n </div>\n </div>\n </CardContent>\n </GlassCard>\n\n <PagePurpose>\n A single view of one team member — their role, tenure, performance, and recent activity — so\n managers and partners have the full context in one place.\n </PagePurpose>\n\n <SegmentedTabs\n tabs={PROFILE_TABS}\n value={tab}\n onChange={(next) => setTab(next as ProfileTab)}\n aria-label=\"Employee profile sections\"\n />\n\n {tab === 'overview' ? (\n <div className=\"grid gap-4 lg:grid-cols-2\">\n <Card>\n <CardHeader>\n <CardTitle className=\"text-base\">Details</CardTitle>\n </CardHeader>\n <CardContent className=\"divide-y divide-border-subtle pt-0\">\n <DetailRow label=\"Manager\">\n <span className=\"inline-flex items-center gap-2\">\n {manager ? (\n <AvatarStack\n people={[{ name: managerName, avatarUrl: manager.avatar }]}\n max={1}\n size={24}\n />\n ) : null}\n {managerName}\n </span>\n </DetailRow>\n <DetailRow label=\"Employment type\">{formatEnumLabel(employee.type)}</DetailRow>\n <DetailRow label=\"Salary band\">{salaryBand(employee.salary)}</DetailRow>\n <DetailRow label=\"Tenure\">{tenure(employee.startDate)}</DetailRow>\n <DetailRow label=\"Employee ID\">\n <span className=\"break-all font-mono text-xs\">{employee.id}</span>\n </DetailRow>\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle className=\"text-base\">Performance</CardTitle>\n </CardHeader>\n <CardContent className=\"flex items-center gap-6 pt-0\">\n <ProgressRing\n value={employee.performanceScore}\n max={5}\n label={employee.performanceScore.toFixed(1)}\n caption=\"/ 5\"\n />\n <div className=\"space-y-1\">\n <p className=\"text-lg font-semibold text-text-primary\">\n {performanceLabel(employee.performanceScore)}\n </p>\n <p className=\"text-sm text-text-secondary\">\n Latest review score of {employee.performanceScore.toFixed(1)} out of 5.\n </p>\n </div>\n </CardContent>\n </Card>\n </div>\n ) : null}\n\n {tab === 'skills' ? (\n <Card>\n <CardHeader>\n <CardTitle className=\"text-base\">Skills</CardTitle>\n </CardHeader>\n <CardContent className=\"pt-0\">\n {employee.skills.length > 0 ? (\n // Real skill tags from the employee record. Rendered as chips (not\n // proficiency bars) because the backend stores tags, not levels —\n // inventing a proficiency % would fabricate data.\n <div className=\"flex flex-wrap gap-2\">\n {employee.skills.map((skill) => (\n <Badge key={skill} variant=\"secondary\" className=\"text-xs\">\n {skill}\n </Badge>\n ))}\n </div>\n ) : (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"No skills recorded\"\n description=\"This team member has no skills captured yet. Add skills from the employee directory to surface them here.\"\n />\n )}\n </CardContent>\n </Card>\n ) : null}\n\n {tab === 'documents' ? (\n <Card>\n <CardHeader className=\"flex flex-row items-center justify-between\">\n <CardTitle className=\"text-base\">Documents</CardTitle>\n <Button size=\"sm\" onClick={() => setAttachDialogOpen(true)}>\n <Plus className=\"mr-1.5 h-3.5 w-3.5\" />\n Attach document\n </Button>\n </CardHeader>\n <CardContent className=\"pt-0\">\n {employeeDocuments.length > 0 ? (\n <div className=\"space-y-2\">\n {employeeDocuments.map((doc) => (\n <div\n key={doc.id}\n className=\"flex items-center justify-between gap-3 rounded-input border border-border-subtle bg-bg-surface px-3 py-2.5\"\n >\n <div className=\"flex min-w-0 items-center gap-3\">\n <FileText className=\"h-4 w-4 shrink-0 text-text-muted\" />\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {doc.title}\n </p>\n <p className=\"text-xs text-text-muted\">\n {doc.category ? `${doc.category} · ` : ''}\n Uploaded {doc.uploadedAt.slice(0, 10)}\n </p>\n </div>\n </div>\n <button\n type=\"button\"\n aria-label={`Remove document \"${doc.title}\"`}\n className=\"shrink-0 text-text-muted hover:text-status-error-fg\"\n onClick={() => setRemoveDocumentId(doc.id)}\n >\n <Trash2 className=\"h-4 w-4\" />\n </button>\n </div>\n ))}\n </div>\n ) : (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"No documents attached\"\n description=\"Offer letters, contracts, and compliance documents attached to this profile will appear here.\"\n action={\n <Button size=\"sm\" onClick={() => setAttachDialogOpen(true)}>\n <Plus className=\"mr-1.5 h-3.5 w-3.5\" />\n Attach document\n </Button>\n }\n />\n )}\n </CardContent>\n </Card>\n ) : null}\n\n {tab === 'performance' ? (\n <Card>\n <CardHeader>\n <CardTitle className=\"text-base\">Performance</CardTitle>\n </CardHeader>\n <CardContent className=\"flex flex-col items-center gap-6 pt-0 sm:flex-row sm:items-center\">\n <ProgressRing\n value={employee.performanceScore}\n max={5}\n size={160}\n label={employee.performanceScore.toFixed(1)}\n caption=\"/ 5\"\n />\n <div className=\"flex-1 space-y-2\">\n <Badge variant=\"secondary\" className=\"gap-1\">\n <BadgeCheck className=\"h-3.5 w-3.5\" />\n {performanceLabel(employee.performanceScore)}\n </Badge>\n <p className=\"text-sm text-text-secondary\">\n {fullName} holds a review score of {employee.performanceScore.toFixed(1)} out of 5.\n </p>\n {/* needs backend fields: goals-met %, peer feedback, manager feedback — only a single\n performanceScore is modelled today, so sub-metrics are intentionally omitted rather\n than invented. */}\n <p className=\"text-xs text-text-muted\">\n Goal, peer, and manager sub-scores will appear here once captured on the review\n record.\n </p>\n </div>\n </CardContent>\n </Card>\n ) : null}\n\n {tab === 'activity' ? (\n <Card>\n <CardHeader>\n <CardTitle className=\"text-base\">Recent activity</CardTitle>\n </CardHeader>\n <CardContent className=\"pt-0\">\n {activity.length > 0 ? (\n <Timeline items={activity} />\n ) : (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"No activity yet\"\n description=\"Time logs, leave, payroll, and project assignments for this employee will show up here.\"\n />\n )}\n </CardContent>\n </Card>\n ) : null}\n\n <AttachDocumentDialog\n open={attachDialogOpen}\n onOpenChange={setAttachDialogOpen}\n onUpload={({ file, title, category }) =>\n uploadEmployeeDocument({ employeeId: employee.id, file, title, category })\n }\n />\n\n <ConfirmDialog\n open={removeDocumentId !== null}\n onOpenChange={(open) => {\n if (!open) setRemoveDocumentId(null);\n }}\n title=\"Remove document?\"\n description=\"This unlinks the document from this employee's profile. The file itself stays in the workspace Files library.\"\n confirmLabel=\"Remove\"\n destructive\n onConfirm={async () => {\n if (removeDocumentId) {\n await detachEmployeeDocument(removeDocumentId);\n setRemoveDocumentId(null);\n }\n }}\n />\n </div>\n );\n}\n"],"mappings":"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"}
@@ -7,7 +7,7 @@ import { ConfirmDialog as a } from "../components/forms/ConfirmDialog.js";
7
7
  import { getNumber as o, getTags as ee, getText as s, newClientId as c } from "../components/forms/formValues.js";
8
8
  import { useState as l } from "react";
9
9
  import { MapPin as te, Pencil as ne, Plus as re, Search as ie, Star as u, Trash2 as d, UserCheck as f, Users as p } from "lucide-react";
10
- import { useNavigate as m } from "react-router-dom";
10
+ import { useNavigate as m } from "react-router";
11
11
  import { Fragment as h, jsx as g, jsxs as _ } from "react/jsx-runtime";
12
12
  import { Badge as ae, Button as v, Card as oe, CardContent as y, CardHeader as b, CardTitle as x, GlassCard as S, GradientText as se, IllustratedEmptyState as ce, Input as le, PagePurpose as ue, Pagination as C, ResponsiveTable as w, Select as T, SelectContent as E, SelectItem as D, SelectTrigger as O, SelectValue as k } from "@burdenoff/fe-libs/ui";
13
13
  import { usePagination as A } from "@burdenoff/fe-libs/shared/hooks";