@burdenoff/microfe-crewfoundry 2026.1029.1 → 2026.1030.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.
- package/dist/crewfoundry/lib/useFocusedRecordParam.d.ts +51 -0
- package/dist/crewfoundry/lib/useFocusedRecordParam.d.ts.map +1 -0
- package/dist/crewfoundry/lib/useFocusedRecordParam.js +38 -0
- package/dist/crewfoundry/lib/useFocusedRecordParam.js.map +1 -0
- package/dist/crewfoundry/pages/CommunicationsAnnouncementsPagePage.d.ts.map +1 -1
- package/dist/crewfoundry/pages/CommunicationsAnnouncementsPagePage.js +119 -112
- package/dist/crewfoundry/pages/CommunicationsAnnouncementsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/CompanyClientManagementPagePage.d.ts.map +1 -1
- package/dist/crewfoundry/pages/CompanyClientManagementPagePage.js +78 -71
- package/dist/crewfoundry/pages/CompanyClientManagementPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/CompanyVendorManagementPagePage.d.ts.map +1 -1
- package/dist/crewfoundry/pages/CompanyVendorManagementPagePage.js +88 -81
- package/dist/crewfoundry/pages/CompanyVendorManagementPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HiringCandidatesPagePage.d.ts.map +1 -1
- package/dist/crewfoundry/pages/HiringCandidatesPagePage.js +133 -126
- package/dist/crewfoundry/pages/HiringCandidatesPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HiringInterviewsPagePage.d.ts.map +1 -1
- package/dist/crewfoundry/pages/HiringInterviewsPagePage.js +150 -143
- package/dist/crewfoundry/pages/HiringInterviewsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HrDepartmentsPagePage.d.ts.map +1 -1
- package/dist/crewfoundry/pages/HrDepartmentsPagePage.js +110 -103
- package/dist/crewfoundry/pages/HrDepartmentsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HrLeaveManagementPagePage.d.ts.map +1 -1
- package/dist/crewfoundry/pages/HrLeaveManagementPagePage.js +173 -162
- package/dist/crewfoundry/pages/HrLeaveManagementPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HrPayrollPagePage.d.ts.map +1 -1
- package/dist/crewfoundry/pages/HrPayrollPagePage.js +169 -158
- package/dist/crewfoundry/pages/HrPayrollPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HrTimeTrackingPagePage.d.ts.map +1 -1
- package/dist/crewfoundry/pages/HrTimeTrackingPagePage.js +179 -163
- package/dist/crewfoundry/pages/HrTimeTrackingPagePage.js.map +1 -1
- package/dist/crewfoundry/store/crewfoundryStore.d.ts.map +1 -1
- package/dist/crewfoundry/store/crewfoundryStore.js +218 -203
- package/dist/crewfoundry/store/crewfoundryStore.js.map +1 -1
- package/dist/utils/pageContext.d.ts +43 -4
- package/dist/utils/pageContext.d.ts.map +1 -1
- package/dist/utils/pageContext.js +10 -3
- package/dist/utils/pageContext.js.map +1 -1
- package/package.json +3 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HiringInterviewsPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/HiringInterviewsPagePage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport {\n Badge,\n Button,\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n GlassCard,\n GradientText,\n IllustratedEmptyState,\n PagePurpose,\n Textarea,\n} from '@burdenoff/fe-libs/ui';\nimport { useGlobalUi } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { Calendar, Clock, Plus, Star, Video } from 'lucide-react';\nimport { useCrewFoundryStore, type Interview } from '../store/crewfoundryStore';\nimport { FormModal, type FieldConfig, type FormValues } from '../components/forms/FormModal';\nimport { getText, newClientId } from '../components/forms/formValues';\nimport { formatEnumLabel } from '../utils/formatEnumLabel';\nimport { RatingStars, getInitials } from '../components/viz';\n\nconst INTERVIEW_TYPE_OPTIONS: FieldConfig['options'] = [\n { label: 'Phone', value: 'phone' },\n { label: 'Video', value: 'video' },\n { label: 'Onsite', value: 'onsite' },\n { label: 'Panel', value: 'panel' },\n { label: 'Technical', value: 'technical' },\n];\n\n// Per-competency scorecard. CrewInterview has only a single `feedback` (String)\n// + `rating` (Float), and the gateway strips `rating` from update inputs — so the\n// four competency scores are persisted by encoding them into `feedback` behind a\n// compact machine marker, then parsed back out on load. The written notes follow\n// the marker line; the overall rating is derived from the competency scores.\nconst COMPETENCIES: { key: string; label: string }[] = [\n { key: 'technical', label: 'Technical Skills' },\n { key: 'problem', label: 'Problem Solving' },\n { key: 'communication', label: 'Communication' },\n { key: 'culture', label: 'Culture Fit' },\n];\n\nconst SCORECARD_MARKER = '@@scorecard';\n\ninterface ParsedScorecard {\n scores: Record<string, number>;\n notes: string;\n}\n\nfunction parseScorecard(feedback?: string | null): ParsedScorecard {\n if (!feedback) {\n return { scores: {}, notes: '' };\n }\n if (!feedback.startsWith(`${SCORECARD_MARKER}:`)) {\n return { scores: {}, notes: feedback };\n }\n const newline = feedback.indexOf('\\n');\n const header = newline === -1 ? feedback : feedback.slice(0, newline);\n const notes = newline === -1 ? '' : feedback.slice(newline + 1);\n const scores: Record<string, number> = {};\n const body = header.slice(SCORECARD_MARKER.length + 1);\n for (const pair of body.split(',')) {\n const [key, raw] = pair.split('=');\n const value = Number(raw);\n if (key && !Number.isNaN(value)) {\n scores[key] = value;\n }\n }\n return { scores, notes };\n}\n\nfunction serializeScorecard(scores: Record<string, number>, notes: string): string {\n const entries = COMPETENCIES.filter((c) => (scores[c.key] ?? 0) > 0).map(\n (c) => `${c.key}=${scores[c.key]}`\n );\n if (entries.length === 0) {\n return notes;\n }\n return `${SCORECARD_MARKER}:${entries.join(',')}\\n${notes}`;\n}\n\nfunction overallScore(scores: Record<string, number>): number {\n const values = COMPETENCIES.map((c) => scores[c.key]).filter(\n (v): v is number => typeof v === 'number' && v > 0\n );\n if (values.length === 0) {\n return 0;\n }\n return Math.round((values.reduce((a, b) => a + b, 0) / values.length) * 10) / 10;\n}\n\nfunction InitialsAvatar({ name, size = 40 }: { name: string; size?: number }) {\n return (\n <span\n className=\"inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full border border-border-subtle bg-bg-sunken font-medium text-text-secondary\"\n style={{ width: size, height: size, fontSize: Math.round(size * 0.36) }}\n aria-hidden=\"true\"\n >\n {getInitials(name)}\n </span>\n );\n}\n\n/** Interactive 1–max star input for a single competency. */\nfunction StarInput({\n value,\n onChange,\n label,\n max = 5,\n}: {\n value: number;\n onChange: (value: number) => void;\n label: string;\n max?: number;\n}) {\n return (\n <div className=\"flex items-center gap-0.5\" role=\"radiogroup\" aria-label={label}>\n {Array.from({ length: max }, (_, i) => i + 1).map((n) => (\n <button\n key={n}\n type=\"button\"\n role=\"radio\"\n aria-checked={value === n}\n aria-label={`${n} out of ${max}`}\n onClick={() => onChange(value === n ? 0 : n)}\n className=\"rounded p-0.5 outline-none transition-transform hover:scale-110 focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <Star\n className=\"h-5 w-5\"\n strokeWidth={1.5}\n style={{\n color: n <= value ? 'var(--color-status-warning-text)' : 'var(--color-border-strong)',\n }}\n fill={n <= value ? 'currentColor' : 'none'}\n />\n </button>\n ))}\n </div>\n );\n}\n\nconst TYPE_BADGE: Record<Interview['type'], 'default' | 'secondary' | 'outline'> = {\n technical: 'default',\n panel: 'secondary',\n onsite: 'secondary',\n video: 'outline',\n phone: 'outline',\n};\n\nfunction formatDay(dateStr: string): string {\n const date = new Date(dateStr);\n if (Number.isNaN(date.getTime())) {\n return dateStr || 'Unscheduled';\n }\n return date.toLocaleDateString(undefined, {\n weekday: 'short',\n month: 'short',\n day: 'numeric',\n });\n}\n\nexport function HiringInterviewsPagePage() {\n const { showToast } = useGlobalUi();\n const { interviews, candidates, addInterview, updateInterview } = useCrewFoundryStore();\n const [scheduleOpen, setScheduleOpen] = useState(false);\n const [selectedId, setSelectedId] = useState<string | null>(null);\n const [saving, setSaving] = useState(false);\n\n const scheduled = interviews.filter((i) => i.status === 'scheduled');\n const completed = interviews.filter((i) => i.status === 'completed');\n\n // Interviews sorted chronologically, grouped by day for the schedule list.\n const dayGroups = useMemo(() => {\n const sorted = [...interviews].sort((a, b) => {\n const da = new Date(a.date).getTime();\n const db = new Date(b.date).getTime();\n if (Number.isNaN(da) && Number.isNaN(db)) return 0;\n if (Number.isNaN(da)) return 1;\n if (Number.isNaN(db)) return -1;\n return da - db;\n });\n const groups: { label: string; items: Interview[] }[] = [];\n for (const interview of sorted) {\n const label = formatDay(interview.date);\n const last = groups[groups.length - 1];\n if (last && last.label === label) {\n last.items.push(interview);\n } else {\n groups.push({ label, items: [interview] });\n }\n }\n return groups;\n }, [interviews]);\n\n // Effective selection is derived (no effect): honour an explicit click, else\n // default to the first scheduled interview, else the first interview overall.\n const activeId = selectedId ?? scheduled[0]?.id ?? interviews[0]?.id ?? null;\n const selected = activeId ? (interviews.find((i) => i.id === activeId) ?? null) : null;\n\n const thisWeekCount = useMemo(() => {\n const now = new Date();\n const startOfWeek = new Date(now);\n startOfWeek.setDate(now.getDate() - ((now.getDay() + 6) % 7));\n startOfWeek.setHours(0, 0, 0, 0);\n const endOfWeek = new Date(startOfWeek);\n endOfWeek.setDate(startOfWeek.getDate() + 7);\n return interviews.filter((i) => {\n const date = new Date(i.date);\n return !Number.isNaN(date.getTime()) && date >= startOfWeek && date < endOfWeek;\n }).length;\n }, [interviews]);\n\n const scheduleFields = useMemo<FieldConfig[]>(\n () => [\n {\n name: 'candidateId',\n label: 'Candidate',\n type: 'select',\n required: true,\n placeholder: candidates.length ? 'Select a candidate…' : 'No candidates available',\n options: candidates.map((c) => ({\n label: `${c.firstName} ${c.lastName} — ${c.position}`,\n value: c.id,\n })),\n },\n { name: 'interviewerName', label: 'Interviewer', type: 'text', required: true },\n { name: 'date', label: 'Date', type: 'date', required: true },\n { name: 'time', label: 'Time', type: 'text', required: true, placeholder: 'e.g. 10:30 AM' },\n {\n name: 'type',\n label: 'Type',\n type: 'select',\n required: true,\n options: INTERVIEW_TYPE_OPTIONS,\n },\n ],\n [candidates]\n );\n\n const handleSchedule = async (values: FormValues) => {\n const candidate = candidates.find((c) => c.id === getText(values, 'candidateId'));\n if (!candidate) {\n throw new Error('Please select a candidate');\n }\n await addInterview({\n id: newClientId(),\n candidateId: candidate.id,\n candidateName: `${candidate.firstName} ${candidate.lastName}`,\n position: candidate.position,\n interviewerName: getText(values, 'interviewerName'),\n date: getText(values, 'date'),\n time: getText(values, 'time'),\n type: values.type as Interview['type'],\n status: 'scheduled',\n });\n showToast('Interview scheduled', 'success');\n };\n\n const handleSaveScorecard = async (submit: boolean, feedback: string) => {\n if (!selected) {\n return;\n }\n setSaving(true);\n try {\n await updateInterview(selected.id, {\n status: submit ? 'completed' : selected.status,\n feedback,\n });\n showToast(submit ? 'Scorecard submitted' : 'Draft saved', 'success');\n } catch {\n showToast('Could not save the scorecard', 'error');\n } finally {\n setSaving(false);\n }\n };\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div>\n <h1 className=\"text-2xl font-semibold\">\n <GradientText>Interviews</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">\n {scheduled.length} upcoming · {completed.length} completed\n </p>\n <PagePurpose className=\"mt-1\">\n Schedule interviews and score candidates on a live scorecard — capture competency\n ratings and feedback the moment the conversation ends.\n </PagePurpose>\n </div>\n <Button size=\"sm\" onClick={() => setScheduleOpen(true)}>\n <Plus className=\"mr-1 h-4 w-4\" />\n Schedule Interview\n </Button>\n </div>\n\n <div className=\"grid gap-4 sm:grid-cols-3\">\n <GlassCard treatment=\"glass\" glow className=\"border-border-seam\">\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle className=\"text-sm font-medium\">Scheduled</CardTitle>\n <Calendar className=\"h-4 w-4 text-status-info-text\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">{scheduled.length}</div>\n </CardContent>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"border-border-seam\">\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle className=\"text-sm font-medium\">Completed</CardTitle>\n <Clock className=\"h-4 w-4 text-status-success-text\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">{completed.length}</div>\n </CardContent>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"border-border-seam\">\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle className=\"text-sm font-medium\">This Week</CardTitle>\n <Video className=\"h-4 w-4 text-text-secondary\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">{thisWeekCount}</div>\n </CardContent>\n </GlassCard>\n </div>\n\n <div className=\"grid gap-6 lg:grid-cols-3\">\n {/* Scheduled / all interviews list */}\n <Card className=\"lg:col-span-2\">\n <CardHeader>\n <CardTitle>Scheduled Interviews</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n {interviews.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-search\"\n title=\"No interviews scheduled\"\n description=\"Schedule an interview to start tracking your pipeline conversations.\"\n />\n ) : (\n dayGroups.map((group) => (\n <div key={group.label} className=\"space-y-2\">\n <div className=\"sticky top-0 rounded bg-bg-sunken px-3 py-1 text-xs font-medium text-text-secondary\">\n {group.label}\n </div>\n {group.items.map((interview) => {\n const isSelected = interview.id === activeId;\n const parsed = parseScorecard(interview.feedback);\n const overall = overallScore(parsed.scores) || interview.rating || 0;\n return (\n <button\n key={interview.id}\n type=\"button\"\n onClick={() => setSelectedId(interview.id)}\n aria-pressed={isSelected}\n className={`flex w-full flex-wrap items-center justify-between gap-3 rounded-[var(--radius-input)] border p-4 text-left transition-colors ${\n isSelected\n ? 'border-action-primary-bg bg-action-primary-bg/10'\n : 'border-border-subtle hover:bg-bg-sunken'\n }`}\n >\n <div className=\"flex min-w-0 items-center gap-3\">\n <InitialsAvatar name={interview.candidateName} />\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {interview.candidateName}\n </p>\n <p className=\"truncate text-xs text-text-secondary\">\n {interview.position}\n </p>\n <p className=\"truncate text-xs text-text-secondary\">\n Interviewer: {interview.interviewerName}\n </p>\n </div>\n </div>\n <div className=\"flex flex-wrap items-center gap-2\">\n <div className=\"text-right\">\n <Badge variant={TYPE_BADGE[interview.type]} className=\"text-[10px]\">\n {formatEnumLabel(interview.type)}\n </Badge>\n <p className=\"mt-1 text-xs text-text-secondary\">{interview.time}</p>\n </div>\n {interview.status === 'completed' && overall > 0 && (\n <RatingStars value={overall} size={12} />\n )}\n <Badge\n variant={interview.status === 'completed' ? 'default' : 'secondary'}\n className=\"text-[10px]\"\n >\n {formatEnumLabel(interview.status)}\n </Badge>\n </div>\n </button>\n );\n })}\n </div>\n ))\n )}\n </CardContent>\n </Card>\n\n {/* Persistent scorecard panel */}\n <div className=\"lg:sticky lg:top-4 lg:self-start\">\n <Card>\n <CardHeader>\n <CardTitle>Interview Scorecard</CardTitle>\n {selected && (\n <p className=\"text-sm text-text-secondary\">\n {formatEnumLabel(selected.type)} interview for {selected.candidateName}\n </p>\n )}\n </CardHeader>\n <CardContent className=\"space-y-4\">\n {!selected ? (\n <p className=\"text-sm text-text-muted\">\n Select an interview to open its scorecard.\n </p>\n ) : (\n <ScorecardEditor\n key={selected.id}\n interview={selected}\n saving={saving}\n onSave={handleSaveScorecard}\n />\n )}\n </CardContent>\n </Card>\n </div>\n </div>\n\n <FormModal\n open={scheduleOpen}\n onOpenChange={setScheduleOpen}\n title=\"Schedule Interview\"\n description=\"Set up a conversation with a candidate in your pipeline.\"\n fields={scheduleFields}\n submitLabel=\"Schedule\"\n onSubmit={handleSchedule}\n />\n </div>\n );\n}\n\ninterface ScorecardEditorProps {\n interview: Interview;\n saving: boolean;\n onSave: (submit: boolean, feedback: string) => void;\n}\n\n/**\n * Editable scorecard for one interview. Re-mounted (via `key={interview.id}`)\n * whenever the selection changes, so its local state initializes from the\n * interview's persisted feedback without a synchronizing effect.\n */\nfunction ScorecardEditor({ interview, saving, onSave }: ScorecardEditorProps) {\n const initial = parseScorecard(interview.feedback);\n const [scores, setScores] = useState<Record<string, number>>(initial.scores);\n const [notes, setNotes] = useState(initial.notes);\n\n const overall = overallScore(scores);\n\n return (\n <>\n <div className=\"grid grid-cols-2 gap-3 rounded-[var(--radius-input)] bg-bg-sunken p-3 text-xs\">\n <div>\n <p className=\"text-text-muted\">Candidate</p>\n <p className=\"font-medium text-text-primary\">{interview.candidateName}</p>\n <p className=\"mt-1 text-text-muted\">Position</p>\n <p className=\"font-medium text-text-primary\">{interview.position}</p>\n </div>\n <div>\n <p className=\"text-text-muted\">Date</p>\n <p className=\"font-medium text-text-primary\">{interview.date}</p>\n <p className=\"mt-1 text-text-muted\">Time</p>\n <p className=\"font-medium text-text-primary\">{interview.time}</p>\n </div>\n </div>\n\n <div className=\"space-y-3\">\n {COMPETENCIES.map((competency) => (\n <div key={competency.key} className=\"flex items-center justify-between gap-2\">\n <span className=\"text-sm font-medium text-text-primary\">{competency.label}</span>\n <StarInput\n label={competency.label}\n value={scores[competency.key] ?? 0}\n onChange={(value) => setScores((prev) => ({ ...prev, [competency.key]: value }))}\n />\n </div>\n ))}\n </div>\n\n <div className=\"flex items-center justify-between border-t border-border-subtle pt-3 text-sm\">\n <span className=\"font-medium text-text-primary\">Overall</span>\n {overall > 0 ? (\n <RatingStars value={overall} size={16} showValue />\n ) : (\n <span className=\"text-xs text-text-muted\">Not scored</span>\n )}\n </div>\n\n <div className=\"space-y-1.5\">\n <label htmlFor=\"scorecard-feedback\" className=\"text-sm font-medium\">\n Feedback\n </label>\n <Textarea\n id=\"scorecard-feedback\"\n rows={5}\n value={notes}\n onChange={(e) => setNotes(e.target.value)}\n placeholder=\"Summarize strengths, concerns, and a recommendation…\"\n />\n </div>\n\n <div className=\"flex gap-2\">\n <Button\n className=\"flex-1\"\n disabled={saving}\n onClick={() => onSave(true, serializeScorecard(scores, notes))}\n >\n Submit\n </Button>\n <Button\n variant=\"outline\"\n className=\"flex-1\"\n disabled={saving}\n onClick={() => onSave(false, serializeScorecard(scores, notes))}\n >\n Save Draft\n </Button>\n </div>\n </>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"HiringInterviewsPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/HiringInterviewsPagePage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport {\n Badge,\n Button,\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n GlassCard,\n GradientText,\n IllustratedEmptyState,\n PagePurpose,\n Textarea,\n} from '@burdenoff/fe-libs/ui';\nimport { useGlobalUi } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { Calendar, Clock, Plus, Star, Video } from 'lucide-react';\nimport { useCrewFoundryStore, type Interview } from '../store/crewfoundryStore';\nimport { FormModal, type FieldConfig, type FormValues } from '../components/forms/FormModal';\nimport { getText, newClientId } from '../components/forms/formValues';\nimport { formatEnumLabel } from '../utils/formatEnumLabel';\nimport { useFocusedRecordParam } from '../lib/useFocusedRecordParam';\nimport { RatingStars, getInitials } from '../components/viz';\n\nconst INTERVIEW_TYPE_OPTIONS: FieldConfig['options'] = [\n { label: 'Phone', value: 'phone' },\n { label: 'Video', value: 'video' },\n { label: 'Onsite', value: 'onsite' },\n { label: 'Panel', value: 'panel' },\n { label: 'Technical', value: 'technical' },\n];\n\n// Per-competency scorecard. CrewInterview has only a single `feedback` (String)\n// + `rating` (Float), and the gateway strips `rating` from update inputs — so the\n// four competency scores are persisted by encoding them into `feedback` behind a\n// compact machine marker, then parsed back out on load. The written notes follow\n// the marker line; the overall rating is derived from the competency scores.\nconst COMPETENCIES: { key: string; label: string }[] = [\n { key: 'technical', label: 'Technical Skills' },\n { key: 'problem', label: 'Problem Solving' },\n { key: 'communication', label: 'Communication' },\n { key: 'culture', label: 'Culture Fit' },\n];\n\nconst SCORECARD_MARKER = '@@scorecard';\n\ninterface ParsedScorecard {\n scores: Record<string, number>;\n notes: string;\n}\n\nfunction parseScorecard(feedback?: string | null): ParsedScorecard {\n if (!feedback) {\n return { scores: {}, notes: '' };\n }\n if (!feedback.startsWith(`${SCORECARD_MARKER}:`)) {\n return { scores: {}, notes: feedback };\n }\n const newline = feedback.indexOf('\\n');\n const header = newline === -1 ? feedback : feedback.slice(0, newline);\n const notes = newline === -1 ? '' : feedback.slice(newline + 1);\n const scores: Record<string, number> = {};\n const body = header.slice(SCORECARD_MARKER.length + 1);\n for (const pair of body.split(',')) {\n const [key, raw] = pair.split('=');\n const value = Number(raw);\n if (key && !Number.isNaN(value)) {\n scores[key] = value;\n }\n }\n return { scores, notes };\n}\n\nfunction serializeScorecard(scores: Record<string, number>, notes: string): string {\n const entries = COMPETENCIES.filter((c) => (scores[c.key] ?? 0) > 0).map(\n (c) => `${c.key}=${scores[c.key]}`\n );\n if (entries.length === 0) {\n return notes;\n }\n return `${SCORECARD_MARKER}:${entries.join(',')}\\n${notes}`;\n}\n\nfunction overallScore(scores: Record<string, number>): number {\n const values = COMPETENCIES.map((c) => scores[c.key]).filter(\n (v): v is number => typeof v === 'number' && v > 0\n );\n if (values.length === 0) {\n return 0;\n }\n return Math.round((values.reduce((a, b) => a + b, 0) / values.length) * 10) / 10;\n}\n\nfunction InitialsAvatar({ name, size = 40 }: { name: string; size?: number }) {\n return (\n <span\n className=\"inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full border border-border-subtle bg-bg-sunken font-medium text-text-secondary\"\n style={{ width: size, height: size, fontSize: Math.round(size * 0.36) }}\n aria-hidden=\"true\"\n >\n {getInitials(name)}\n </span>\n );\n}\n\n/** Interactive 1–max star input for a single competency. */\nfunction StarInput({\n value,\n onChange,\n label,\n max = 5,\n}: {\n value: number;\n onChange: (value: number) => void;\n label: string;\n max?: number;\n}) {\n return (\n <div className=\"flex items-center gap-0.5\" role=\"radiogroup\" aria-label={label}>\n {Array.from({ length: max }, (_, i) => i + 1).map((n) => (\n <button\n key={n}\n type=\"button\"\n role=\"radio\"\n aria-checked={value === n}\n aria-label={`${n} out of ${max}`}\n onClick={() => onChange(value === n ? 0 : n)}\n className=\"rounded p-0.5 outline-none transition-transform hover:scale-110 focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <Star\n className=\"h-5 w-5\"\n strokeWidth={1.5}\n style={{\n color: n <= value ? 'var(--color-status-warning-text)' : 'var(--color-border-strong)',\n }}\n fill={n <= value ? 'currentColor' : 'none'}\n />\n </button>\n ))}\n </div>\n );\n}\n\nconst TYPE_BADGE: Record<Interview['type'], 'default' | 'secondary' | 'outline'> = {\n technical: 'default',\n panel: 'secondary',\n onsite: 'secondary',\n video: 'outline',\n phone: 'outline',\n};\n\nfunction formatDay(dateStr: string): string {\n const date = new Date(dateStr);\n if (Number.isNaN(date.getTime())) {\n return dateStr || 'Unscheduled';\n }\n return date.toLocaleDateString(undefined, {\n weekday: 'short',\n month: 'short',\n day: 'numeric',\n });\n}\n\nexport function HiringInterviewsPagePage() {\n const { showToast } = useGlobalUi();\n const { interviews, candidates, addInterview, updateInterview } = useCrewFoundryStore();\n const [scheduleOpen, setScheduleOpen] = useState(false);\n const [selectedId, setSelectedId] = useState<string | null>(null);\n const [saving, setSaving] = useState(false);\n\n const scheduled = interviews.filter((i) => i.status === 'scheduled');\n const completed = interviews.filter((i) => i.status === 'completed');\n\n // Interviews sorted chronologically, grouped by day for the schedule list.\n const dayGroups = useMemo(() => {\n const sorted = [...interviews].sort((a, b) => {\n const da = new Date(a.date).getTime();\n const db = new Date(b.date).getTime();\n if (Number.isNaN(da) && Number.isNaN(db)) return 0;\n if (Number.isNaN(da)) return 1;\n if (Number.isNaN(db)) return -1;\n return da - db;\n });\n const groups: { label: string; items: Interview[] }[] = [];\n for (const interview of sorted) {\n const label = formatDay(interview.date);\n const last = groups[groups.length - 1];\n if (last && last.label === label) {\n last.items.push(interview);\n } else {\n groups.push({ label, items: [interview] });\n }\n }\n return groups;\n }, [interviews]);\n\n // Effective selection is derived (no effect): honour an explicit click, else\n // default to the first scheduled interview, else the first interview overall.\n const activeId = selectedId ?? scheduled[0]?.id ?? interviews[0]?.id ?? null;\n const selected = activeId ? (interviews.find((i) => i.id === activeId) ?? null) : null;\n\n // The interview in the detail pane is the one \"this interview\" refers to:\n // keep it in `?id=` for the assistant's page context and for links.\n useFocusedRecordParam({\n records: interviews,\n activeId: selected?.id ?? null,\n onRestore: (interview) => setSelectedId(interview.id),\n });\n\n const thisWeekCount = useMemo(() => {\n const now = new Date();\n const startOfWeek = new Date(now);\n startOfWeek.setDate(now.getDate() - ((now.getDay() + 6) % 7));\n startOfWeek.setHours(0, 0, 0, 0);\n const endOfWeek = new Date(startOfWeek);\n endOfWeek.setDate(startOfWeek.getDate() + 7);\n return interviews.filter((i) => {\n const date = new Date(i.date);\n return !Number.isNaN(date.getTime()) && date >= startOfWeek && date < endOfWeek;\n }).length;\n }, [interviews]);\n\n const scheduleFields = useMemo<FieldConfig[]>(\n () => [\n {\n name: 'candidateId',\n label: 'Candidate',\n type: 'select',\n required: true,\n placeholder: candidates.length ? 'Select a candidate…' : 'No candidates available',\n options: candidates.map((c) => ({\n label: `${c.firstName} ${c.lastName} — ${c.position}`,\n value: c.id,\n })),\n },\n { name: 'interviewerName', label: 'Interviewer', type: 'text', required: true },\n { name: 'date', label: 'Date', type: 'date', required: true },\n { name: 'time', label: 'Time', type: 'text', required: true, placeholder: 'e.g. 10:30 AM' },\n {\n name: 'type',\n label: 'Type',\n type: 'select',\n required: true,\n options: INTERVIEW_TYPE_OPTIONS,\n },\n ],\n [candidates]\n );\n\n const handleSchedule = async (values: FormValues) => {\n const candidate = candidates.find((c) => c.id === getText(values, 'candidateId'));\n if (!candidate) {\n throw new Error('Please select a candidate');\n }\n await addInterview({\n id: newClientId(),\n candidateId: candidate.id,\n candidateName: `${candidate.firstName} ${candidate.lastName}`,\n position: candidate.position,\n interviewerName: getText(values, 'interviewerName'),\n date: getText(values, 'date'),\n time: getText(values, 'time'),\n type: values.type as Interview['type'],\n status: 'scheduled',\n });\n showToast('Interview scheduled', 'success');\n };\n\n const handleSaveScorecard = async (submit: boolean, feedback: string) => {\n if (!selected) {\n return;\n }\n setSaving(true);\n try {\n await updateInterview(selected.id, {\n status: submit ? 'completed' : selected.status,\n feedback,\n });\n showToast(submit ? 'Scorecard submitted' : 'Draft saved', 'success');\n } catch {\n showToast('Could not save the scorecard', 'error');\n } finally {\n setSaving(false);\n }\n };\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div>\n <h1 className=\"text-2xl font-semibold\">\n <GradientText>Interviews</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">\n {scheduled.length} upcoming · {completed.length} completed\n </p>\n <PagePurpose className=\"mt-1\">\n Schedule interviews and score candidates on a live scorecard — capture competency\n ratings and feedback the moment the conversation ends.\n </PagePurpose>\n </div>\n <Button size=\"sm\" onClick={() => setScheduleOpen(true)}>\n <Plus className=\"mr-1 h-4 w-4\" />\n Schedule Interview\n </Button>\n </div>\n\n <div className=\"grid gap-4 sm:grid-cols-3\">\n <GlassCard treatment=\"glass\" glow className=\"border-border-seam\">\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle className=\"text-sm font-medium\">Scheduled</CardTitle>\n <Calendar className=\"h-4 w-4 text-status-info-text\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">{scheduled.length}</div>\n </CardContent>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"border-border-seam\">\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle className=\"text-sm font-medium\">Completed</CardTitle>\n <Clock className=\"h-4 w-4 text-status-success-text\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">{completed.length}</div>\n </CardContent>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"border-border-seam\">\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle className=\"text-sm font-medium\">This Week</CardTitle>\n <Video className=\"h-4 w-4 text-text-secondary\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">{thisWeekCount}</div>\n </CardContent>\n </GlassCard>\n </div>\n\n <div className=\"grid gap-6 lg:grid-cols-3\">\n {/* Scheduled / all interviews list */}\n <Card className=\"lg:col-span-2\">\n <CardHeader>\n <CardTitle>Scheduled Interviews</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n {interviews.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-search\"\n title=\"No interviews scheduled\"\n description=\"Schedule an interview to start tracking your pipeline conversations.\"\n />\n ) : (\n dayGroups.map((group) => (\n <div key={group.label} className=\"space-y-2\">\n <div className=\"sticky top-0 rounded bg-bg-sunken px-3 py-1 text-xs font-medium text-text-secondary\">\n {group.label}\n </div>\n {group.items.map((interview) => {\n const isSelected = interview.id === activeId;\n const parsed = parseScorecard(interview.feedback);\n const overall = overallScore(parsed.scores) || interview.rating || 0;\n return (\n <button\n key={interview.id}\n type=\"button\"\n onClick={() => setSelectedId(interview.id)}\n aria-pressed={isSelected}\n className={`flex w-full flex-wrap items-center justify-between gap-3 rounded-[var(--radius-input)] border p-4 text-left transition-colors ${\n isSelected\n ? 'border-action-primary-bg bg-action-primary-bg/10'\n : 'border-border-subtle hover:bg-bg-sunken'\n }`}\n >\n <div className=\"flex min-w-0 items-center gap-3\">\n <InitialsAvatar name={interview.candidateName} />\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {interview.candidateName}\n </p>\n <p className=\"truncate text-xs text-text-secondary\">\n {interview.position}\n </p>\n <p className=\"truncate text-xs text-text-secondary\">\n Interviewer: {interview.interviewerName}\n </p>\n </div>\n </div>\n <div className=\"flex flex-wrap items-center gap-2\">\n <div className=\"text-right\">\n <Badge variant={TYPE_BADGE[interview.type]} className=\"text-[10px]\">\n {formatEnumLabel(interview.type)}\n </Badge>\n <p className=\"mt-1 text-xs text-text-secondary\">{interview.time}</p>\n </div>\n {interview.status === 'completed' && overall > 0 && (\n <RatingStars value={overall} size={12} />\n )}\n <Badge\n variant={interview.status === 'completed' ? 'default' : 'secondary'}\n className=\"text-[10px]\"\n >\n {formatEnumLabel(interview.status)}\n </Badge>\n </div>\n </button>\n );\n })}\n </div>\n ))\n )}\n </CardContent>\n </Card>\n\n {/* Persistent scorecard panel */}\n <div className=\"lg:sticky lg:top-4 lg:self-start\">\n <Card>\n <CardHeader>\n <CardTitle>Interview Scorecard</CardTitle>\n {selected && (\n <p className=\"text-sm text-text-secondary\">\n {formatEnumLabel(selected.type)} interview for {selected.candidateName}\n </p>\n )}\n </CardHeader>\n <CardContent className=\"space-y-4\">\n {!selected ? (\n <p className=\"text-sm text-text-muted\">\n Select an interview to open its scorecard.\n </p>\n ) : (\n <ScorecardEditor\n key={selected.id}\n interview={selected}\n saving={saving}\n onSave={handleSaveScorecard}\n />\n )}\n </CardContent>\n </Card>\n </div>\n </div>\n\n <FormModal\n open={scheduleOpen}\n onOpenChange={setScheduleOpen}\n title=\"Schedule Interview\"\n description=\"Set up a conversation with a candidate in your pipeline.\"\n fields={scheduleFields}\n submitLabel=\"Schedule\"\n onSubmit={handleSchedule}\n />\n </div>\n );\n}\n\ninterface ScorecardEditorProps {\n interview: Interview;\n saving: boolean;\n onSave: (submit: boolean, feedback: string) => void;\n}\n\n/**\n * Editable scorecard for one interview. Re-mounted (via `key={interview.id}`)\n * whenever the selection changes, so its local state initializes from the\n * interview's persisted feedback without a synchronizing effect.\n */\nfunction ScorecardEditor({ interview, saving, onSave }: ScorecardEditorProps) {\n const initial = parseScorecard(interview.feedback);\n const [scores, setScores] = useState<Record<string, number>>(initial.scores);\n const [notes, setNotes] = useState(initial.notes);\n\n const overall = overallScore(scores);\n\n return (\n <>\n <div className=\"grid grid-cols-2 gap-3 rounded-[var(--radius-input)] bg-bg-sunken p-3 text-xs\">\n <div>\n <p className=\"text-text-muted\">Candidate</p>\n <p className=\"font-medium text-text-primary\">{interview.candidateName}</p>\n <p className=\"mt-1 text-text-muted\">Position</p>\n <p className=\"font-medium text-text-primary\">{interview.position}</p>\n </div>\n <div>\n <p className=\"text-text-muted\">Date</p>\n <p className=\"font-medium text-text-primary\">{interview.date}</p>\n <p className=\"mt-1 text-text-muted\">Time</p>\n <p className=\"font-medium text-text-primary\">{interview.time}</p>\n </div>\n </div>\n\n <div className=\"space-y-3\">\n {COMPETENCIES.map((competency) => (\n <div key={competency.key} className=\"flex items-center justify-between gap-2\">\n <span className=\"text-sm font-medium text-text-primary\">{competency.label}</span>\n <StarInput\n label={competency.label}\n value={scores[competency.key] ?? 0}\n onChange={(value) => setScores((prev) => ({ ...prev, [competency.key]: value }))}\n />\n </div>\n ))}\n </div>\n\n <div className=\"flex items-center justify-between border-t border-border-subtle pt-3 text-sm\">\n <span className=\"font-medium text-text-primary\">Overall</span>\n {overall > 0 ? (\n <RatingStars value={overall} size={16} showValue />\n ) : (\n <span className=\"text-xs text-text-muted\">Not scored</span>\n )}\n </div>\n\n <div className=\"space-y-1.5\">\n <label htmlFor=\"scorecard-feedback\" className=\"text-sm font-medium\">\n Feedback\n </label>\n <Textarea\n id=\"scorecard-feedback\"\n rows={5}\n value={notes}\n onChange={(e) => setNotes(e.target.value)}\n placeholder=\"Summarize strengths, concerns, and a recommendation…\"\n />\n </div>\n\n <div className=\"flex gap-2\">\n <Button\n className=\"flex-1\"\n disabled={saving}\n onClick={() => onSave(true, serializeScorecard(scores, notes))}\n >\n Submit\n </Button>\n <Button\n variant=\"outline\"\n className=\"flex-1\"\n disabled={saving}\n onClick={() => onSave(false, serializeScorecard(scores, notes))}\n >\n Save Draft\n </Button>\n </div>\n </>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;AAuBA,IAAM,IAAiD;CACrD;EAAE,OAAO;EAAS,OAAO;EAAS;CAClC;EAAE,OAAO;EAAS,OAAO;EAAS;CAClC;EAAE,OAAO;EAAU,OAAO;EAAU;CACpC;EAAE,OAAO;EAAS,OAAO;EAAS;CAClC;EAAE,OAAO;EAAa,OAAO;EAAa;CAC3C,EAOK,IAAiD;CACrD;EAAE,KAAK;EAAa,OAAO;EAAoB;CAC/C;EAAE,KAAK;EAAW,OAAO;EAAmB;CAC5C;EAAE,KAAK;EAAiB,OAAO;EAAiB;CAChD;EAAE,KAAK;EAAW,OAAO;EAAe;CACzC,EAEK,IAAmB;AAOzB,SAAS,EAAe,GAA2C;AACjE,KAAI,CAAC,EACH,QAAO;EAAE,QAAQ,EAAE;EAAE,OAAO;EAAI;AAElC,KAAI,CAAC,EAAS,WAAW,GAAG,EAAiB,GAAG,CAC9C,QAAO;EAAE,QAAQ,EAAE;EAAE,OAAO;EAAU;CAExC,IAAM,IAAU,EAAS,QAAQ,KAAK,EAChC,IAAS,MAAY,KAAK,IAAW,EAAS,MAAM,GAAG,EAAQ,EAC/D,IAAQ,MAAY,KAAK,KAAK,EAAS,MAAM,IAAU,EAAE,EACzD,IAAiC,EAAE,EACnC,IAAO,EAAO,MAAM,GAA4B;AACtD,MAAK,IAAM,KAAQ,EAAK,MAAM,IAAI,EAAE;EAClC,IAAM,CAAC,GAAK,KAAO,EAAK,MAAM,IAAI,EAC5B,IAAQ,OAAO,EAAI;AACzB,EAAI,KAAO,CAAC,OAAO,MAAM,EAAM,KAC7B,EAAO,KAAO;;AAGlB,QAAO;EAAE;EAAQ;EAAO;;AAG1B,SAAS,EAAmB,GAAgC,GAAuB;CACjF,IAAM,IAAU,EAAa,QAAQ,OAAO,EAAO,EAAE,QAAQ,KAAK,EAAE,CAAC,KAClE,MAAM,GAAG,EAAE,IAAI,GAAG,EAAO,EAAE,OAC7B;AAID,QAHI,EAAQ,WAAW,IACd,IAEF,GAAG,EAAiB,GAAG,EAAQ,KAAK,IAAI,CAAC,IAAI;;AAGtD,SAAS,EAAa,GAAwC;CAC5D,IAAM,IAAS,EAAa,KAAK,MAAM,EAAO,EAAE,KAAK,CAAC,QACnD,MAAmB,OAAO,KAAM,YAAY,IAAI,EAClD;AAID,QAHI,EAAO,WAAW,IACb,IAEF,KAAK,MAAO,EAAO,QAAQ,GAAG,MAAM,IAAI,GAAG,EAAE,GAAG,EAAO,SAAU,GAAG,GAAG;;AAGhF,SAAS,EAAe,EAAE,SAAM,UAAO,MAAuC;AAC5E,QACE,kBAAC,QAAD;EACE,WAAU;EACV,OAAO;GAAE,OAAO;GAAM,QAAQ;GAAM,UAAU,KAAK,MAAM,IAAO,IAAK;GAAE;EACvE,eAAY;YAEX,EAAY,EAAK;EACb,CAAA;;AAKX,SAAS,EAAU,EACjB,UACA,aACA,UACA,SAAM,KAML;AACD,QACE,kBAAC,OAAD;EAAK,WAAU;EAA4B,MAAK;EAAa,cAAY;YACtE,MAAM,KAAK,EAAE,QAAQ,GAAK,GAAG,GAAG,MAAM,IAAI,EAAE,CAAC,KAAK,MACjD,kBAAC,UAAD;GAEE,MAAK;GACL,MAAK;GACL,gBAAc,MAAU;GACxB,cAAY,GAAG,EAAE,UAAU;GAC3B,eAAe,EAAS,MAAU,IAAI,IAAI,EAAE;GAC5C,WAAU;aAEV,kBAAC,GAAD;IACE,WAAU;IACV,aAAa;IACb,OAAO,EACL,OAAO,KAAK,IAAQ,qCAAqC,8BAC1D;IACD,MAAM,KAAK,IAAQ,iBAAiB;IACpC,CAAA;GACK,EAhBF,EAgBE,CACT;EACE,CAAA;;AAIV,IAAM,IAA6E;CACjF,WAAW;CACX,OAAO;CACP,QAAQ;CACR,OAAO;CACP,OAAO;CACR;AAED,SAAS,EAAU,GAAyB;CAC1C,IAAM,IAAO,IAAI,KAAK,EAAQ;AAI9B,QAHI,OAAO,MAAM,EAAK,SAAS,CAAC,GACvB,KAAW,gBAEb,EAAK,mBAAmB,KAAA,GAAW;EACxC,SAAS;EACT,OAAO;EACP,KAAK;EACN,CAAC;;AAGJ,SAAgB,IAA2B;CACzC,IAAM,EAAE,iBAAc,GAAa,EAC7B,EAAE,eAAY,eAAY,iBAAc,uBAAoB,GAAqB,EACjF,CAAC,GAAc,KAAmB,EAAS,GAAM,EACjD,CAAC,GAAY,KAAiB,EAAwB,KAAK,EAC3D,CAAC,GAAQ,KAAa,EAAS,GAAM,EAErC,IAAY,EAAW,QAAQ,MAAM,EAAE,WAAW,YAAY,EAC9D,IAAY,EAAW,QAAQ,MAAM,EAAE,WAAW,YAAY,EAG9D,IAAY,QAAc;EAC9B,IAAM,IAAS,CAAC,GAAG,EAAW,CAAC,MAAM,GAAG,MAAM;GAC5C,IAAM,IAAK,IAAI,KAAK,EAAE,KAAK,CAAC,SAAS,EAC/B,IAAK,IAAI,KAAK,EAAE,KAAK,CAAC,SAAS;AAIrC,UAHI,OAAO,MAAM,EAAG,IAAI,OAAO,MAAM,EAAG,GAAS,IAC7C,OAAO,MAAM,EAAG,GAAS,IACzB,OAAO,MAAM,EAAG,GAAS,KACtB,IAAK;IACZ,EACI,IAAkD,EAAE;AAC1D,OAAK,IAAM,KAAa,GAAQ;GAC9B,IAAM,IAAQ,EAAU,EAAU,KAAK,EACjC,IAAO,EAAO,EAAO,SAAS;AACpC,GAAI,KAAQ,EAAK,UAAU,IACzB,EAAK,MAAM,KAAK,EAAU,GAE1B,EAAO,KAAK;IAAE;IAAO,OAAO,CAAC,EAAU;IAAE,CAAC;;AAG9C,SAAO;IACN,CAAC,EAAW,CAAC,EAIV,IAAW,KAAc,EAAU,IAAI,MAAM,EAAW,IAAI,MAAM,MAClE,IAAW,IAAY,EAAW,MAAM,MAAM,EAAE,OAAO,EAAS,IAAI,OAAQ;AAIlF,GAAsB;EACpB,SAAS;EACT,UAAU,GAAU,MAAM;EAC1B,YAAY,MAAc,EAAc,EAAU,GAAG;EACtD,CAAC;CAEF,IAAM,IAAgB,QAAc;EAClC,IAAM,oBAAM,IAAI,MAAM,EAChB,IAAc,IAAI,KAAK,EAAI;AAEjC,EADA,EAAY,QAAQ,EAAI,SAAS,IAAK,EAAI,QAAQ,GAAG,KAAK,EAAG,EAC7D,EAAY,SAAS,GAAG,GAAG,GAAG,EAAE;EAChC,IAAM,IAAY,IAAI,KAAK,EAAY;AAEvC,SADA,EAAU,QAAQ,EAAY,SAAS,GAAG,EAAE,EACrC,EAAW,QAAQ,MAAM;GAC9B,IAAM,IAAO,IAAI,KAAK,EAAE,KAAK;AAC7B,UAAO,CAAC,OAAO,MAAM,EAAK,SAAS,CAAC,IAAI,KAAQ,KAAe,IAAO;IACtE,CAAC;IACF,CAAC,EAAW,CAAC,EAEV,IAAiB,QACf;EACJ;GACE,MAAM;GACN,OAAO;GACP,MAAM;GACN,UAAU;GACV,aAAa,EAAW,SAAS,wBAAwB;GACzD,SAAS,EAAW,KAAK,OAAO;IAC9B,OAAO,GAAG,EAAE,UAAU,GAAG,EAAE,SAAS,KAAK,EAAE;IAC3C,OAAO,EAAE;IACV,EAAE;GACJ;EACD;GAAE,MAAM;GAAmB,OAAO;GAAe,MAAM;GAAQ,UAAU;GAAM;EAC/E;GAAE,MAAM;GAAQ,OAAO;GAAQ,MAAM;GAAQ,UAAU;GAAM;EAC7D;GAAE,MAAM;GAAQ,OAAO;GAAQ,MAAM;GAAQ,UAAU;GAAM,aAAa;GAAiB;EAC3F;GACE,MAAM;GACN,OAAO;GACP,MAAM;GACN,UAAU;GACV,SAAS;GACV;EACF,EACD,CAAC,EAAW,CACb;AAuCD,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD,EAAA,UAAA;KACE,kBAAC,MAAD;MAAI,WAAU;gBACZ,kBAAC,GAAD,EAAA,UAAc,cAAyB,CAAA;MACpC,CAAA;KACL,kBAAC,KAAD;MAAG,WAAU;gBAAb;OACG,EAAU;OAAO;OAAa,EAAU;OAAO;OAC9C;;KACJ,kBAAC,GAAD;MAAa,WAAU;gBAAO;MAGhB,CAAA;KACV,EAAA,CAAA,EACN,kBAAC,GAAD;KAAQ,MAAK;KAAK,eAAe,EAAgB,GAAK;eAAtD,CACE,kBAAC,GAAD,EAAM,WAAU,gBAAiB,CAAA,EAAA,qBAE1B;OACL;;GAEN,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD;MAAW,WAAU;MAAQ,MAAA;MAAK,WAAU;gBAA5C,CACE,kBAAC,GAAD;OAAY,WAAU;iBAAtB,CACE,kBAAC,GAAD;QAAW,WAAU;kBAAsB;QAAqB,CAAA,EAChE,kBAAC,GAAD,EAAU,WAAU,iCAAkC,CAAA,CAC3C;UACb,kBAAC,GAAD,EAAA,UACE,kBAAC,OAAD;OAAK,WAAU;iBAAmC,EAAU;OAAa,CAAA,EAC7D,CAAA,CACJ;;KACZ,kBAAC,GAAD;MAAW,WAAU;MAAS,WAAU;gBAAxC,CACE,kBAAC,GAAD;OAAY,WAAU;iBAAtB,CACE,kBAAC,GAAD;QAAW,WAAU;kBAAsB;QAAqB,CAAA,EAChE,kBAAC,GAAD,EAAO,WAAU,oCAAqC,CAAA,CAC3C;UACb,kBAAC,GAAD,EAAA,UACE,kBAAC,OAAD;OAAK,WAAU;iBAAmC,EAAU;OAAa,CAAA,EAC7D,CAAA,CACJ;;KACZ,kBAAC,GAAD;MAAW,WAAU;MAAS,WAAU;gBAAxC,CACE,kBAAC,GAAD;OAAY,WAAU;iBAAtB,CACE,kBAAC,GAAD;QAAW,WAAU;kBAAsB;QAAqB,CAAA,EAChE,kBAAC,GAAD,EAAO,WAAU,+BAAgC,CAAA,CACtC;UACb,kBAAC,GAAD,EAAA,UACE,kBAAC,OAAD;OAAK,WAAU;iBAAmC;OAAoB,CAAA,EAC1D,CAAA,CACJ;;KACR;;GAEN,kBAAC,OAAD;IAAK,WAAU;cAAf,CAEE,kBAAC,GAAD;KAAM,WAAU;eAAhB,CACE,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD,EAAA,UAAW,wBAAgC,CAAA,EAChC,CAAA,EACb,kBAAC,GAAD;MAAa,WAAU;gBACpB,EAAW,WAAW,IACrB,kBAAC,GAAD;OACE,cAAa;OACb,OAAM;OACN,aAAY;OACZ,CAAA,GAEF,EAAU,KAAK,MACb,kBAAC,OAAD;OAAuB,WAAU;iBAAjC,CACE,kBAAC,OAAD;QAAK,WAAU;kBACZ,EAAM;QACH,CAAA,EACL,EAAM,MAAM,KAAK,MAAc;QAC9B,IAAM,IAAa,EAAU,OAAO,GAE9B,IAAU,EADD,EAAe,EAAU,SAAS,CACb,OAAO,IAAI,EAAU,UAAU;AACnE,eACE,kBAAC,UAAD;SAEE,MAAK;SACL,eAAe,EAAc,EAAU,GAAG;SAC1C,gBAAc;SACd,WAAW,iIACT,IACI,qDACA;mBARR,CAWE,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,GAAD,EAAgB,MAAM,EAAU,eAAiB,CAAA,EACjD,kBAAC,OAAD;WAAK,WAAU;qBAAf;YACE,kBAAC,KAAD;aAAG,WAAU;uBACV,EAAU;aACT,CAAA;YACJ,kBAAC,KAAD;aAAG,WAAU;uBACV,EAAU;aACT,CAAA;YACJ,kBAAC,KAAD;aAAG,WAAU;uBAAb,CAAoD,iBACpC,EAAU,gBACtB;;YACA;aACF;aACN,kBAAC,OAAD;UAAK,WAAU;oBAAf;WACE,kBAAC,OAAD;YAAK,WAAU;sBAAf,CACE,kBAAC,GAAD;aAAO,SAAS,EAAW,EAAU;aAAO,WAAU;uBACnD,EAAgB,EAAU,KAAK;aAC1B,CAAA,EACR,kBAAC,KAAD;aAAG,WAAU;uBAAoC,EAAU;aAAS,CAAA,CAChE;;WACL,EAAU,WAAW,eAAe,IAAU,KAC7C,kBAAC,GAAD;YAAa,OAAO;YAAS,MAAM;YAAM,CAAA;WAE3C,kBAAC,GAAD;YACE,SAAS,EAAU,WAAW,cAAc,YAAY;YACxD,WAAU;sBAET,EAAgB,EAAU,OAAO;YAC5B,CAAA;WACJ;YACC;WAzCF,EAAU,GAyCR;SAEX,CACE;SAtDI,EAAM,MAsDV,CACN;MAEQ,CAAA,CACT;QAGP,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UAAW,uBAA+B,CAAA,EACzC,KACC,kBAAC,KAAD;MAAG,WAAU;gBAAb;OACG,EAAgB,EAAS,KAAK;OAAC;OAAgB,EAAS;OACvD;QAEK,EAAA,CAAA,EACb,kBAAC,GAAD;MAAa,WAAU;gBACnB,IAKA,kBAAC,GAAD;OAEE,WAAW;OACH;OACR,QArKY,OAAO,GAAiB,MAAqB;AAClE,eAGL;WAAU,GAAK;AACf,aAAI;AAKF,UAJA,MAAM,EAAgB,EAAS,IAAI;WACjC,QAAQ,IAAS,cAAc,EAAS;WACxC;WACD,CAAC,EACF,EAAU,IAAS,wBAAwB,eAAe,UAAU;iBAC9D;AACN,YAAU,gCAAgC,QAAQ;mBAC1C;AACR,YAAU,GAAM;;;;OAwJJ,EAJK,EAAS,GAId,GATF,kBAAC,KAAD;OAAG,WAAU;iBAA0B;OAEnC,CAAA;MASM,CAAA,CACT,EAAA,CAAA;KACH,CAAA,CACF;;GAEN,kBAAC,GAAD;IACE,MAAM;IACN,cAAc;IACd,OAAM;IACN,aAAY;IACZ,QAAQ;IACR,aAAY;IACZ,UAvMiB,OAAO,MAAuB;KACnD,IAAM,IAAY,EAAW,MAAM,MAAM,EAAE,OAAO,EAAQ,GAAQ,cAAc,CAAC;AACjF,SAAI,CAAC,EACH,OAAU,MAAM,4BAA4B;AAa9C,KAXA,MAAM,EAAa;MACjB,IAAI,GAAa;MACjB,aAAa,EAAU;MACvB,eAAe,GAAG,EAAU,UAAU,GAAG,EAAU;MACnD,UAAU,EAAU;MACpB,iBAAiB,EAAQ,GAAQ,kBAAkB;MACnD,MAAM,EAAQ,GAAQ,OAAO;MAC7B,MAAM,EAAQ,GAAQ,OAAO;MAC7B,MAAM,EAAO;MACb,QAAQ;MACT,CAAC,EACF,EAAU,uBAAuB,UAAU;;IAwLvC,CAAA;GACE;;;AAeV,SAAS,EAAgB,EAAE,cAAW,WAAQ,aAAgC;CAC5E,IAAM,IAAU,EAAe,EAAU,SAAS,EAC5C,CAAC,GAAQ,KAAa,EAAiC,EAAQ,OAAO,EACtE,CAAC,GAAO,KAAY,EAAS,EAAQ,MAAM,EAE3C,IAAU,EAAa,EAAO;AAEpC,QACE,kBAAA,GAAA,EAAA,UAAA;EACE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD,EAAA,UAAA;IACE,kBAAC,KAAD;KAAG,WAAU;eAAkB;KAAa,CAAA;IAC5C,kBAAC,KAAD;KAAG,WAAU;eAAiC,EAAU;KAAkB,CAAA;IAC1E,kBAAC,KAAD;KAAG,WAAU;eAAuB;KAAY,CAAA;IAChD,kBAAC,KAAD;KAAG,WAAU;eAAiC,EAAU;KAAa,CAAA;IACjE,EAAA,CAAA,EACN,kBAAC,OAAD,EAAA,UAAA;IACE,kBAAC,KAAD;KAAG,WAAU;eAAkB;KAAQ,CAAA;IACvC,kBAAC,KAAD;KAAG,WAAU;eAAiC,EAAU;KAAS,CAAA;IACjE,kBAAC,KAAD;KAAG,WAAU;eAAuB;KAAQ,CAAA;IAC5C,kBAAC,KAAD;KAAG,WAAU;eAAiC,EAAU;KAAS,CAAA;IAC7D,EAAA,CAAA,CACF;;EAEN,kBAAC,OAAD;GAAK,WAAU;aACZ,EAAa,KAAK,MACjB,kBAAC,OAAD;IAA0B,WAAU;cAApC,CACE,kBAAC,QAAD;KAAM,WAAU;eAAyC,EAAW;KAAa,CAAA,EACjF,kBAAC,GAAD;KACE,OAAO,EAAW;KAClB,OAAO,EAAO,EAAW,QAAQ;KACjC,WAAW,MAAU,GAAW,OAAU;MAAE,GAAG;OAAO,EAAW,MAAM;MAAO,EAAE;KAChF,CAAA,CACE;MAPI,EAAW,IAOf,CACN;GACE,CAAA;EAEN,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,QAAD;IAAM,WAAU;cAAgC;IAAc,CAAA,EAC7D,IAAU,IACT,kBAAC,GAAD;IAAa,OAAO;IAAS,MAAM;IAAI,WAAA;IAAY,CAAA,GAEnD,kBAAC,QAAD;IAAM,WAAU;cAA0B;IAAiB,CAAA,CAEzD;;EAEN,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,SAAD;IAAO,SAAQ;IAAqB,WAAU;cAAsB;IAE5D,CAAA,EACR,kBAAC,GAAD;IACE,IAAG;IACH,MAAM;IACN,OAAO;IACP,WAAW,MAAM,EAAS,EAAE,OAAO,MAAM;IACzC,aAAY;IACZ,CAAA,CACE;;EAEN,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,GAAD;IACE,WAAU;IACV,UAAU;IACV,eAAe,EAAO,IAAM,EAAmB,GAAQ,EAAM,CAAC;cAC/D;IAEQ,CAAA,EACT,kBAAC,GAAD;IACE,SAAQ;IACR,WAAU;IACV,UAAU;IACV,eAAe,EAAO,IAAO,EAAmB,GAAQ,EAAM,CAAC;cAChE;IAEQ,CAAA,CACL;;EACL,EAAA,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HrDepartmentsPagePage.d.ts","sourceRoot":"","sources":["../../../src/crewfoundry/pages/HrDepartmentsPagePage.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"HrDepartmentsPagePage.d.ts","sourceRoot":"","sources":["../../../src/crewfoundry/pages/HrDepartmentsPagePage.tsx"],"names":[],"mappings":"AAsDA,wBAAgB,qBAAqB,gCA6SpC"}
|
|
@@ -5,14 +5,15 @@ import { OrgTree as r } from "../components/viz/OrgTree.js";
|
|
|
5
5
|
import { AvatarStack as i } from "../components/viz/AvatarStack.js";
|
|
6
6
|
import "../components/viz/index.js";
|
|
7
7
|
import { FormModal as a, useFormModal as o } from "../components/forms/FormModal.js";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
8
|
+
import { useFocusedRecordParam as s } from "../lib/useFocusedRecordParam.js";
|
|
9
|
+
import { ConfirmDialog as c } from "../components/forms/ConfirmDialog.js";
|
|
10
|
+
import { getNumber as l, getText as u, newClientId as d } from "../components/forms/formValues.js";
|
|
11
|
+
import { useCallback as f, useMemo as p, useState as m } from "react";
|
|
12
|
+
import { Building2 as h, DollarSign as g, MapPin as _, Network as v, Pencil as y, Plus as b, Trash2 as x, Users as S } from "lucide-react";
|
|
13
|
+
import { jsx as C, jsxs as w } from "react/jsx-runtime";
|
|
14
|
+
import { Badge as T, Button as E, Card as D, CardContent as O, CardHeader as k, CardTitle as A, GlassCard as j, GradientText as M, PagePurpose as N } from "@burdenoff/fe-libs/ui";
|
|
14
15
|
//#region src/crewfoundry/pages/HrDepartmentsPagePage.tsx
|
|
15
|
-
var
|
|
16
|
+
var P = (e) => `${e.firstName} ${e.lastName}`.trim(), F = [
|
|
16
17
|
{
|
|
17
18
|
name: "name",
|
|
18
19
|
label: "Department name",
|
|
@@ -51,25 +52,31 @@ var N = (e) => `${e.firstName} ${e.lastName}`.trim(), P = [
|
|
|
51
52
|
value: "restructuring"
|
|
52
53
|
}]
|
|
53
54
|
}
|
|
54
|
-
],
|
|
55
|
+
], I = (e) => ({
|
|
55
56
|
name: e.name,
|
|
56
57
|
headName: e.headName,
|
|
57
58
|
location: e.location,
|
|
58
59
|
budget: e.budget,
|
|
59
60
|
status: e.status
|
|
60
61
|
});
|
|
61
|
-
function
|
|
62
|
-
let { departments:
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
function L() {
|
|
63
|
+
let { departments: L, employees: R, teams: z, addDepartment: B, updateDepartment: V, removeDepartment: H } = e(), U = o();
|
|
64
|
+
s({
|
|
65
|
+
records: L,
|
|
66
|
+
activeId: U.open ? U.context?.id ?? null : null,
|
|
67
|
+
onRestore: U.openModal
|
|
68
|
+
});
|
|
69
|
+
let [W, G] = m(null), K = L.reduce((e, t) => e + t.budget, 0), q = L.reduce((e, t) => e + t.employeeCount, 0), J = U.context, Y = f((e) => R.find((t) => t.id === e.headId) || R.find((t) => P(t) === e.headName), [R]), X = p(() => {
|
|
70
|
+
if (L.length === 0) return null;
|
|
71
|
+
let e = R.find((e) => !e.managerId && /chief|ceo|founder|president|head of company/i.test(e.position)), t = L.map((e) => {
|
|
72
|
+
let t = Y(e), n = z.filter((t) => t.department === e.name), r;
|
|
66
73
|
return r = n.length > 0 ? n.slice(0, 4).map((e) => ({
|
|
67
74
|
id: e.id,
|
|
68
75
|
name: e.name,
|
|
69
76
|
role: `${e.memberCount} member${e.memberCount === 1 ? "" : "s"}`
|
|
70
|
-
})) :
|
|
77
|
+
})) : R.filter((n) => n.department === e.name && n.id !== t?.id).slice(0, 3).map((e) => ({
|
|
71
78
|
id: e.id,
|
|
72
|
-
name:
|
|
79
|
+
name: P(e),
|
|
73
80
|
role: e.position,
|
|
74
81
|
avatarUrl: e.avatar
|
|
75
82
|
})), {
|
|
@@ -82,175 +89,175 @@ function I() {
|
|
|
82
89
|
});
|
|
83
90
|
return {
|
|
84
91
|
id: e?.id ?? "org-root",
|
|
85
|
-
name: e ?
|
|
92
|
+
name: e ? P(e) : "Organization",
|
|
86
93
|
role: e?.position ?? "Company",
|
|
87
94
|
avatarUrl: e?.avatar,
|
|
88
95
|
children: t
|
|
89
96
|
};
|
|
90
97
|
}, [
|
|
91
|
-
I,
|
|
92
98
|
L,
|
|
93
99
|
R,
|
|
94
|
-
|
|
100
|
+
z,
|
|
101
|
+
Y
|
|
95
102
|
]);
|
|
96
|
-
return /* @__PURE__ */
|
|
103
|
+
return /* @__PURE__ */ w("div", {
|
|
97
104
|
className: "space-y-6 p-6",
|
|
98
105
|
children: [
|
|
99
|
-
/* @__PURE__ */
|
|
106
|
+
/* @__PURE__ */ w("div", {
|
|
100
107
|
className: "flex items-center justify-between",
|
|
101
|
-
children: [/* @__PURE__ */
|
|
102
|
-
/* @__PURE__ */
|
|
108
|
+
children: [/* @__PURE__ */ w("div", { children: [
|
|
109
|
+
/* @__PURE__ */ C("h1", {
|
|
103
110
|
className: "text-2xl font-semibold",
|
|
104
|
-
children: /* @__PURE__ */
|
|
111
|
+
children: /* @__PURE__ */ C(M, { children: "Departments" })
|
|
105
112
|
}),
|
|
106
|
-
/* @__PURE__ */
|
|
113
|
+
/* @__PURE__ */ C("p", {
|
|
107
114
|
className: "text-sm text-text-secondary",
|
|
108
115
|
children: "Organizational structure and department management"
|
|
109
116
|
}),
|
|
110
|
-
/* @__PURE__ */
|
|
117
|
+
/* @__PURE__ */ C(N, {
|
|
111
118
|
className: "mt-1",
|
|
112
119
|
children: "Organize your company into departments — set up structure, assign heads, and see how headcount is distributed across the org."
|
|
113
120
|
})
|
|
114
|
-
] }), /* @__PURE__ */
|
|
121
|
+
] }), /* @__PURE__ */ w(E, {
|
|
115
122
|
size: "sm",
|
|
116
|
-
onClick: () =>
|
|
117
|
-
children: [/* @__PURE__ */
|
|
123
|
+
onClick: () => U.openModal(),
|
|
124
|
+
children: [/* @__PURE__ */ C(b, { className: "h-4 w-4 mr-1" }), "Add Department"]
|
|
118
125
|
})]
|
|
119
126
|
}),
|
|
120
|
-
/* @__PURE__ */
|
|
127
|
+
/* @__PURE__ */ w("div", {
|
|
121
128
|
className: "grid gap-4 md:grid-cols-3",
|
|
122
129
|
children: [
|
|
123
|
-
/* @__PURE__ */
|
|
130
|
+
/* @__PURE__ */ C(n, {
|
|
124
131
|
label: "Total Departments",
|
|
125
|
-
value:
|
|
126
|
-
icon: /* @__PURE__ */
|
|
132
|
+
value: L.length,
|
|
133
|
+
icon: /* @__PURE__ */ C(h, { className: "h-4 w-4" })
|
|
127
134
|
}),
|
|
128
|
-
/* @__PURE__ */
|
|
135
|
+
/* @__PURE__ */ C(n, {
|
|
129
136
|
label: "Total Headcount",
|
|
130
|
-
value:
|
|
131
|
-
icon: /* @__PURE__ */ S
|
|
137
|
+
value: q,
|
|
138
|
+
icon: /* @__PURE__ */ C(S, { className: "h-4 w-4" })
|
|
132
139
|
}),
|
|
133
|
-
/* @__PURE__ */
|
|
140
|
+
/* @__PURE__ */ C(n, {
|
|
134
141
|
label: "Total Budget",
|
|
135
|
-
value: `$${(
|
|
136
|
-
icon: /* @__PURE__ */
|
|
142
|
+
value: `$${(K / 1e6).toFixed(1)}M`,
|
|
143
|
+
icon: /* @__PURE__ */ C(g, { className: "h-4 w-4" })
|
|
137
144
|
})
|
|
138
145
|
]
|
|
139
146
|
}),
|
|
140
|
-
/* @__PURE__ */
|
|
147
|
+
/* @__PURE__ */ C("div", {
|
|
141
148
|
className: "grid gap-4 md:grid-cols-2 xl:grid-cols-3",
|
|
142
|
-
children:
|
|
143
|
-
let n =
|
|
144
|
-
return /* @__PURE__ */
|
|
149
|
+
children: L.map((e) => {
|
|
150
|
+
let n = R.filter((t) => t.department === e.name), r = e.employeeCount || n.length, a = n.length > 0 ? Math.round(n.reduce((e, t) => e + t.salary, 0) / n.length) : 0, o = n.length > 0 ? n.reduce((e, t) => e + t.performanceScore, 0) / n.length : 0, s = Math.round(o / 5 * 100), c = Y(e), l = s >= 80 ? "bg-status-success-text" : s >= 60 ? "bg-status-warning-text" : "bg-status-error-text";
|
|
151
|
+
return /* @__PURE__ */ w(j, {
|
|
145
152
|
treatment: "glass",
|
|
146
153
|
className: "border-border-seam",
|
|
147
|
-
children: [/* @__PURE__ */
|
|
154
|
+
children: [/* @__PURE__ */ C(k, { children: /* @__PURE__ */ w("div", {
|
|
148
155
|
className: "flex items-start justify-between gap-2",
|
|
149
|
-
children: [/* @__PURE__ */
|
|
156
|
+
children: [/* @__PURE__ */ w("div", {
|
|
150
157
|
className: "min-w-0",
|
|
151
|
-
children: [/* @__PURE__ */
|
|
158
|
+
children: [/* @__PURE__ */ C(A, {
|
|
152
159
|
className: "truncate text-lg",
|
|
153
160
|
children: e.name
|
|
154
|
-
}), /* @__PURE__ */
|
|
161
|
+
}), /* @__PURE__ */ w("div", {
|
|
155
162
|
className: "mt-2 flex items-center gap-2",
|
|
156
|
-
children: [/* @__PURE__ */
|
|
163
|
+
children: [/* @__PURE__ */ C(i, {
|
|
157
164
|
people: [{
|
|
158
165
|
name: e.headName || "Unassigned",
|
|
159
166
|
avatarUrl: c?.avatar
|
|
160
167
|
}],
|
|
161
168
|
size: 28
|
|
162
|
-
}), /* @__PURE__ */
|
|
169
|
+
}), /* @__PURE__ */ w("span", {
|
|
163
170
|
className: "truncate text-sm text-text-secondary",
|
|
164
|
-
children: [/* @__PURE__ */
|
|
171
|
+
children: [/* @__PURE__ */ C("span", {
|
|
165
172
|
className: "font-medium text-text-primary",
|
|
166
173
|
children: e.headName
|
|
167
174
|
}), e.headName ? ", Head" : "No head assigned"]
|
|
168
175
|
})]
|
|
169
176
|
})]
|
|
170
|
-
}), /* @__PURE__ */
|
|
177
|
+
}), /* @__PURE__ */ w("div", {
|
|
171
178
|
className: "flex items-center gap-1",
|
|
172
179
|
children: [
|
|
173
|
-
/* @__PURE__ */
|
|
180
|
+
/* @__PURE__ */ C(T, {
|
|
174
181
|
variant: e.status === "active" ? "default" : "secondary",
|
|
175
182
|
children: t(e.status)
|
|
176
183
|
}),
|
|
177
|
-
/* @__PURE__ */
|
|
184
|
+
/* @__PURE__ */ C(E, {
|
|
178
185
|
variant: "ghost",
|
|
179
186
|
size: "sm",
|
|
180
187
|
className: "h-8 w-8 p-0",
|
|
181
188
|
"aria-label": `Edit ${e.name}`,
|
|
182
|
-
onClick: () =>
|
|
183
|
-
children: /* @__PURE__ */
|
|
189
|
+
onClick: () => U.openModal(e),
|
|
190
|
+
children: /* @__PURE__ */ C(y, { className: "h-4 w-4" })
|
|
184
191
|
}),
|
|
185
|
-
/* @__PURE__ */
|
|
192
|
+
/* @__PURE__ */ C(E, {
|
|
186
193
|
variant: "ghost",
|
|
187
194
|
size: "sm",
|
|
188
195
|
className: "h-8 w-8 p-0",
|
|
189
196
|
"aria-label": `Delete ${e.name}`,
|
|
190
|
-
onClick: () =>
|
|
191
|
-
children: /* @__PURE__ */
|
|
197
|
+
onClick: () => G(e),
|
|
198
|
+
children: /* @__PURE__ */ C(x, { className: "h-4 w-4 text-status-error-text" })
|
|
192
199
|
})
|
|
193
200
|
]
|
|
194
201
|
})]
|
|
195
|
-
}) }), /* @__PURE__ */
|
|
202
|
+
}) }), /* @__PURE__ */ w(O, {
|
|
196
203
|
className: "space-y-4",
|
|
197
204
|
children: [
|
|
198
|
-
/* @__PURE__ */
|
|
205
|
+
/* @__PURE__ */ w("div", {
|
|
199
206
|
className: "flex items-end justify-between gap-3",
|
|
200
|
-
children: [/* @__PURE__ */
|
|
207
|
+
children: [/* @__PURE__ */ w("div", { children: [/* @__PURE__ */ C("p", {
|
|
201
208
|
className: "text-3xl font-bold leading-none tabular-nums",
|
|
202
209
|
children: r
|
|
203
|
-
}), /* @__PURE__ */
|
|
210
|
+
}), /* @__PURE__ */ C("p", {
|
|
204
211
|
className: "mt-1 text-xs text-text-secondary",
|
|
205
212
|
children: "Employees"
|
|
206
|
-
})] }), /* @__PURE__ */
|
|
213
|
+
})] }), /* @__PURE__ */ w("div", {
|
|
207
214
|
className: "text-right",
|
|
208
|
-
children: [/* @__PURE__ */
|
|
215
|
+
children: [/* @__PURE__ */ w("p", {
|
|
209
216
|
className: "text-lg font-semibold tabular-nums",
|
|
210
217
|
children: [
|
|
211
218
|
"$",
|
|
212
219
|
(e.budget / 1e6).toFixed(1),
|
|
213
220
|
"M"
|
|
214
221
|
]
|
|
215
|
-
}), /* @__PURE__ */
|
|
222
|
+
}), /* @__PURE__ */ C("p", {
|
|
216
223
|
className: "text-xs text-text-secondary",
|
|
217
224
|
children: "Annual budget"
|
|
218
225
|
})]
|
|
219
226
|
})]
|
|
220
227
|
}),
|
|
221
|
-
/* @__PURE__ */
|
|
228
|
+
/* @__PURE__ */ w("div", {
|
|
222
229
|
className: "space-y-1",
|
|
223
|
-
children: [/* @__PURE__ */
|
|
230
|
+
children: [/* @__PURE__ */ w("div", {
|
|
224
231
|
className: "flex items-center justify-between text-xs text-text-secondary",
|
|
225
|
-
children: [/* @__PURE__ */
|
|
232
|
+
children: [/* @__PURE__ */ C("span", { children: "Avg performance" }), /* @__PURE__ */ w("span", {
|
|
226
233
|
className: "font-medium text-text-primary tabular-nums",
|
|
227
234
|
children: [s, "%"]
|
|
228
235
|
})]
|
|
229
|
-
}), /* @__PURE__ */
|
|
236
|
+
}), /* @__PURE__ */ C("div", {
|
|
230
237
|
className: "h-2 overflow-hidden rounded-full bg-bg-sunken",
|
|
231
|
-
children: /* @__PURE__ */
|
|
238
|
+
children: /* @__PURE__ */ C("div", {
|
|
232
239
|
className: `h-2 rounded-full ${l}`,
|
|
233
240
|
style: { width: `${s}%` }
|
|
234
241
|
})
|
|
235
242
|
})]
|
|
236
243
|
}),
|
|
237
|
-
/* @__PURE__ */
|
|
244
|
+
/* @__PURE__ */ w("div", {
|
|
238
245
|
className: "flex items-center justify-between text-xs text-text-secondary",
|
|
239
|
-
children: [/* @__PURE__ */
|
|
246
|
+
children: [/* @__PURE__ */ w("span", {
|
|
240
247
|
className: "flex items-center gap-1",
|
|
241
|
-
children: [/* @__PURE__ */
|
|
242
|
-
}), /* @__PURE__ */
|
|
248
|
+
children: [/* @__PURE__ */ C(_, { className: "h-3 w-3" }), e.location]
|
|
249
|
+
}), /* @__PURE__ */ w("span", { children: [
|
|
243
250
|
"Cost/Head:",
|
|
244
251
|
" ",
|
|
245
252
|
r > 0 ? `$${Math.round(e.budget / r / 1e3)}K` : "—"
|
|
246
253
|
] })]
|
|
247
254
|
}),
|
|
248
|
-
a > 0 && /* @__PURE__ */
|
|
255
|
+
a > 0 && /* @__PURE__ */ w("p", {
|
|
249
256
|
className: "text-xs text-text-secondary",
|
|
250
257
|
children: [
|
|
251
258
|
"Avg salary",
|
|
252
259
|
" ",
|
|
253
|
-
/* @__PURE__ */
|
|
260
|
+
/* @__PURE__ */ w("span", {
|
|
254
261
|
className: "font-medium text-text-primary",
|
|
255
262
|
children: ["$", a.toLocaleString()]
|
|
256
263
|
})
|
|
@@ -261,55 +268,55 @@ function I() {
|
|
|
261
268
|
}, e.id);
|
|
262
269
|
})
|
|
263
270
|
}),
|
|
264
|
-
|
|
271
|
+
X && /* @__PURE__ */ w(D, { children: [/* @__PURE__ */ C(k, { children: /* @__PURE__ */ w(A, {
|
|
265
272
|
className: "flex items-center gap-2 text-lg",
|
|
266
|
-
children: [/* @__PURE__ */
|
|
267
|
-
}) }), /* @__PURE__ */
|
|
273
|
+
children: [/* @__PURE__ */ C(v, { className: "h-4 w-4 text-text-secondary" }), "Org structure"]
|
|
274
|
+
}) }), /* @__PURE__ */ C(O, { children: /* @__PURE__ */ C("div", {
|
|
268
275
|
className: "overflow-x-auto pb-2",
|
|
269
|
-
children: /* @__PURE__ */
|
|
276
|
+
children: /* @__PURE__ */ C("div", {
|
|
270
277
|
className: "min-w-fit",
|
|
271
|
-
children: /* @__PURE__ */
|
|
278
|
+
children: /* @__PURE__ */ C(r, { root: X })
|
|
272
279
|
})
|
|
273
280
|
}) })] }),
|
|
274
|
-
/* @__PURE__ */
|
|
275
|
-
open:
|
|
276
|
-
onOpenChange:
|
|
277
|
-
title:
|
|
278
|
-
description:
|
|
279
|
-
fields:
|
|
280
|
-
initialValues:
|
|
281
|
-
submitLabel:
|
|
281
|
+
/* @__PURE__ */ C(a, {
|
|
282
|
+
open: U.open,
|
|
283
|
+
onOpenChange: U.onOpenChange,
|
|
284
|
+
title: J ? "Edit department" : "Add department",
|
|
285
|
+
description: J ? "Update this department’s details." : "Create a new department in your organization.",
|
|
286
|
+
fields: F,
|
|
287
|
+
initialValues: J ? I(J) : void 0,
|
|
288
|
+
submitLabel: J ? "Save changes" : "Add department",
|
|
282
289
|
onSubmit: async (e) => {
|
|
283
290
|
let t = {
|
|
284
|
-
id:
|
|
285
|
-
name:
|
|
286
|
-
headId:
|
|
287
|
-
headName:
|
|
288
|
-
employeeCount:
|
|
289
|
-
budget:
|
|
290
|
-
location:
|
|
291
|
-
status:
|
|
291
|
+
id: J?.id ?? d(),
|
|
292
|
+
name: u(e, "name"),
|
|
293
|
+
headId: J?.headId ?? "",
|
|
294
|
+
headName: u(e, "headName"),
|
|
295
|
+
employeeCount: J?.employeeCount ?? 0,
|
|
296
|
+
budget: l(e, "budget"),
|
|
297
|
+
location: u(e, "location"),
|
|
298
|
+
status: u(e, "status")
|
|
292
299
|
};
|
|
293
|
-
|
|
300
|
+
J ? await V(J.id, t) : await B(t);
|
|
294
301
|
}
|
|
295
302
|
}),
|
|
296
|
-
/* @__PURE__ */
|
|
297
|
-
open:
|
|
303
|
+
/* @__PURE__ */ C(c, {
|
|
304
|
+
open: W !== null,
|
|
298
305
|
onOpenChange: (e) => {
|
|
299
|
-
e ||
|
|
306
|
+
e || G(null);
|
|
300
307
|
},
|
|
301
308
|
title: "Delete department?",
|
|
302
|
-
description:
|
|
309
|
+
description: W ? `${W.name} will be removed. This cannot be undone.` : void 0,
|
|
303
310
|
confirmLabel: "Delete",
|
|
304
311
|
destructive: !0,
|
|
305
312
|
onConfirm: async () => {
|
|
306
|
-
|
|
313
|
+
W && (await H(W.id), G(null));
|
|
307
314
|
}
|
|
308
315
|
})
|
|
309
316
|
]
|
|
310
317
|
});
|
|
311
318
|
}
|
|
312
319
|
//#endregion
|
|
313
|
-
export {
|
|
320
|
+
export { L as HrDepartmentsPagePage };
|
|
314
321
|
|
|
315
322
|
//# sourceMappingURL=HrDepartmentsPagePage.js.map
|