@burdenoff/microfe-crewfoundry 2026.704.1 → 2026.704.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/crewfoundry/components/forms/FormModal.js.map +1 -1
- package/dist/crewfoundry/pages/AdminAdminPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/AnalyticsAnalyticsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/AnalyticsEmployeeAnalyticsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/AnalyticsFinancialAnalyticsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/AnalyticsHiringAnalyticsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/CommunicationsAnnouncementsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/CommunicationsCallsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/CommunicationsDirectoryPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/CommunicationsFilesPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/CommunicationsMessagesPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/CommunicationsTeamsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/CommunicationsVideoCallsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/CompanyClientManagementPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/CompanyCompanyDashboardPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/CompanyCompanyProfilePagePage.js.map +1 -1
- package/dist/crewfoundry/pages/CompanyVendorManagementPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/DashboardAnalyticsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/DashboardDashboardPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/DashboardEnhancedDashboardPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/DashboardExecutiveDashboardPage.js.map +1 -1
- package/dist/crewfoundry/pages/HelpApiDocumentationPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HiringApplicationsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HiringCandidatesPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HiringCreateJobPagePage.js +1 -1
- package/dist/crewfoundry/pages/HiringCreateJobPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HiringInterviewsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HiringJobDetailsPagePage.js +1 -1
- package/dist/crewfoundry/pages/HiringJobDetailsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HiringJobsPagePage.js +2 -2
- package/dist/crewfoundry/pages/HiringJobsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HrDepartmentsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HrEmployeesPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HrLeaveManagementPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HrPayrollPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HrTimeTrackingPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/LearningAssessmentsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/LearningCertificatesPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/LearningCourseDetailsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/LearningCoursesPagePage.js +1 -1
- package/dist/crewfoundry/pages/LearningCoursesPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/LearningLearningPagePage.js +1 -1
- package/dist/crewfoundry/pages/LearningLearningPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/LearningLearningPathDetailPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/LearningLearningPathsPagePage.js +1 -1
- package/dist/crewfoundry/pages/LearningLearningPathsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/LearningMyLearningPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/MarketplaceBookingManagementPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/MarketplaceEarningsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/MarketplaceFreelancerProfilePageEnhancedPage.js.map +1 -1
- package/dist/crewfoundry/pages/MarketplaceFreelancerProfilePagePage.js.map +1 -1
- package/dist/crewfoundry/pages/MarketplaceMarketplacePagePage.js.map +1 -1
- package/dist/crewfoundry/pages/MarketplaceMarketplaceServicesPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/MarketplaceMessagingPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/MarketplaceMyServicesPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/MarketplaceProjectDetailsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/MarketplaceServiceDetailsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/PaymentsEscrowPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/PaymentsInvoicesPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/PaymentsPaymentDashboardPagePage.js +1 -1
- package/dist/crewfoundry/pages/PaymentsPaymentDashboardPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/PaymentsPaymentMethodsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/PaymentsSecurityPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/ProjectsCreateProjectPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/ProjectsProjectsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/TeamsTeamAnalyticsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/TeamsTeamDetailPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/TeamsTeamDirectoryPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/TeamsTeamSettingsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/TeamsTeamsPagePage.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FormModal.js","names":[],"sources":["../../../../src/crewfoundry/components/forms/FormModal.tsx"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\nimport {\n Button,\n Input,\n Label,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n Switch,\n Textarea,\n} from '@burdenoff/fe-libs/ui';\nimport { useGlobalUi } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { X } from 'lucide-react';\n\n// ── Public API ──────────────────────────────────────────────────\nexport type FormFieldType =\n | 'text'\n | 'number'\n | 'date'\n | 'email'\n | 'textarea'\n | 'select'\n | 'tags'\n | 'switch';\n\nexport type FormValue = string | number | boolean | string[];\nexport type FormValues = Record<string, FormValue>;\n\nexport interface FieldOption {\n label: string;\n value: string;\n}\n\nexport interface FieldConfig {\n name: string;\n label: string;\n type: FormFieldType;\n required?: boolean;\n placeholder?: string;\n options?: FieldOption[];\n /** Column span in the 2-col grid on md+ (defaults to 1; textarea/tags default to 2). */\n colSpan?: 1 | 2;\n}\n\nexport interface FormModalProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n title: string;\n description?: string;\n fields: FieldConfig[];\n initialValues?: FormValues;\n submitLabel?: string;\n onSubmit: (values: FormValues) => Promise<void>;\n}\n\n// ── Internal state helpers ──────────────────────────────────────\n// Text-like inputs are held as strings for controlled inputs; switches as\n// booleans; tags as string[]. Values are coerced back to their public shapes\n// on submit.\ntype InternalValue = string | boolean | string[];\ntype InternalValues = Record<string, InternalValue>;\n\n\nconst buildInitialState = (fields: FieldConfig[], initialValues?: FormValues): InternalValues => {\n const state: InternalValues = {};\n for (const field of fields) {\n const raw = initialValues?.[field.name];\n if (field.type === 'switch') {\n state[field.name] = Boolean(raw);\n } else if (field.type === 'tags') {\n if (Array.isArray(raw)) {\n state[field.name] = raw.map((item) => String(item));\n } else if (typeof raw === 'string' && raw.length > 0) {\n state[field.name] = raw\n .split(',')\n .map((item) => item.trim())\n .filter(Boolean);\n } else {\n state[field.name] = [];\n }\n } else {\n state[field.name] = raw === undefined || raw === null ? '' : String(raw);\n }\n }\n return state;\n};\n\nconst isEmptyValue = (field: FieldConfig, value: InternalValue): boolean => {\n if (field.type === 'switch') {\n return false;\n }\n if (field.type === 'tags') {\n return !Array.isArray(value) || value.length === 0;\n }\n return typeof value !== 'string' || value.trim() === '';\n};\n\nconst toOutput = (fields: FieldConfig[], state: InternalValues): FormValues => {\n const output: FormValues = {};\n for (const field of fields) {\n const value = state[field.name];\n if (field.type === 'switch') {\n output[field.name] = Boolean(value);\n } else if (field.type === 'tags') {\n output[field.name] = Array.isArray(value) ? value : [];\n } else if (field.type === 'number') {\n const raw = typeof value === 'string' ? value.trim() : '';\n output[field.name] = raw === '' ? '' : Number(raw);\n } else {\n output[field.name] = typeof value === 'string' ? value : '';\n }\n }\n return output;\n};\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 focus-visible:shadow-[var(--shadow-focus)] focus-visible:ring-1 focus-visible:ring-[var(--color-focus-ring)] disabled:cursor-not-allowed disabled:opacity-50';\n\n// ── Component ───────────────────────────────────────────────────\nexport function FormModal({\n open,\n onOpenChange,\n title,\n description,\n fields,\n initialValues,\n submitLabel = 'Save',\n onSubmit,\n}: FormModalProps) {\n const { showToast } = useGlobalUi();\n const [values, setValues] = useState<InternalValues>(() =>\n buildInitialState(fields, initialValues)\n );\n const [tagDrafts, setTagDrafts] = useState<Record<string, string>>({});\n const [submitting, setSubmitting] = useState(false);\n\n // Keep the latest field config / initial values reachable from the reset\n // effect without making them effect dependencies (page callers rarely\n // memoize these, and depending on them would loop or clobber edits).\n const fieldsRef = useRef(fields);\n const initialValuesRef = useRef(initialValues);\n const wasOpenRef = useRef(false);\n\n // Sync the latest field config / initial values into refs after each commit\n // (never during render). Declared before the reset effect so that on a rising\n // edge of `open` the refs are refreshed first, then read by the reset below.\n useEffect(() => {\n fieldsRef.current = fields;\n initialValuesRef.current = initialValues;\n }, [fields, initialValues]);\n\n // Reset the working copy on each rising edge of `open` so edit vs create\n // contexts never leak into each other.\n useEffect(() => {\n if (open && !wasOpenRef.current) {\n setValues(buildInitialState(fieldsRef.current, initialValuesRef.current));\n setTagDrafts({});\n setSubmitting(false);\n }\n wasOpenRef.current = open;\n }, [open]);\n\n const setFieldValue = useCallback((name: string, value: InternalValue) => {\n setValues((prev) => ({ ...prev, [name]: value }));\n }, []);\n\n const commitTag = useCallback(\n (name: string) => {\n setTagDrafts((prevDrafts) => {\n const draft = (prevDrafts[name] ?? '').trim();\n if (draft) {\n setValues((prev) => {\n const current = Array.isArray(prev[name]) ? (prev[name] as string[]) : [];\n if (current.includes(draft)) {\n return prev;\n }\n return { ...prev, [name]: [...current, draft] };\n });\n }\n return { ...prevDrafts, [name]: '' };\n });\n },\n []\n );\n\n const removeTag = useCallback((name: string, tag: string) => {\n setValues((prev) => {\n const current = Array.isArray(prev[name]) ? (prev[name] as string[]) : [];\n return { ...prev, [name]: current.filter((item) => item !== tag) };\n });\n }, []);\n\n const handleSubmit = useCallback(\n async (event: React.FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n if (submitting) {\n return;\n }\n\n // Fold any un-committed tag draft into its field before validating.\n const staged: InternalValues = { ...values };\n for (const field of fields) {\n if (field.type === 'tags') {\n const draft = (tagDrafts[field.name] ?? '').trim();\n if (draft) {\n const current = Array.isArray(staged[field.name])\n ? (staged[field.name] as string[])\n : [];\n if (!current.includes(draft)) {\n staged[field.name] = [...current, draft];\n }\n }\n }\n }\n\n const missing = fields.find((field) => field.required && isEmptyValue(field, staged[field.name]));\n if (missing) {\n showToast(`${missing.label} is required`, 'warning');\n return;\n }\n\n setSubmitting(true);\n try {\n await onSubmit(toOutput(fields, staged));\n onOpenChange(false);\n } catch (error) {\n showToast(error instanceof Error ? error.message : 'Something went wrong', 'error');\n } finally {\n setSubmitting(false);\n }\n },\n [fields, onOpenChange, onSubmit, showToast, submitting, tagDrafts, values]\n );\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent className=\"max-h-[85vh] overflow-y-auto sm:max-w-[560px]\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>{title}</ResponsiveDialogTitle>\n {description ? (\n <ResponsiveDialogDescription>{description}</ResponsiveDialogDescription>\n ) : null}\n </ResponsiveDialogHeader>\n\n <form onSubmit={handleSubmit} className=\"space-y-4\">\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2\">\n {fields.map((field) => {\n const fieldId = `formmodal-${field.name}`;\n const spanTwo =\n field.colSpan === 2 ||\n (field.colSpan === undefined && (field.type === 'textarea' || field.type === 'tags'));\n const value = values[field.name];\n\n return (\n <div\n key={field.name}\n className={`flex flex-col gap-1.5 ${spanTwo ? 'md:col-span-2' : ''}`}\n >\n {field.type === 'switch' ? (\n <div className=\"flex items-center justify-between gap-3 rounded-input border border-border-default bg-bg-surface px-3 py-2\">\n <Label htmlFor={fieldId}>\n {field.label}\n {field.required ? <span className=\"text-status-error-fg\"> *</span> : null}\n </Label>\n <Switch\n id={fieldId}\n checked={Boolean(value)}\n onCheckedChange={(checked) => setFieldValue(field.name, checked)}\n disabled={submitting}\n />\n </div>\n ) : (\n <>\n <Label htmlFor={fieldId}>\n {field.label}\n {field.required ? <span className=\"text-status-error-fg\"> *</span> : null}\n </Label>\n\n {field.type === 'textarea' ? (\n <Textarea\n id={fieldId}\n value={typeof value === 'string' ? value : ''}\n placeholder={field.placeholder}\n disabled={submitting}\n onChange={(event) => setFieldValue(field.name, event.target.value)}\n />\n ) : field.type === 'select' ? (\n <select\n id={fieldId}\n className={SELECT_CLASS}\n value={typeof value === 'string' ? value : ''}\n disabled={submitting}\n onChange={(event) => setFieldValue(field.name, event.target.value)}\n >\n <option value=\"\" disabled>\n {field.placeholder ?? 'Select…'}\n </option>\n {(field.options ?? []).map((option) => (\n <option key={option.value} value={option.value}>\n {option.label}\n </option>\n ))}\n </select>\n ) : field.type === 'tags' ? (\n <div className=\"flex min-h-10 flex-wrap items-center gap-1.5 rounded-input border border-border-default bg-bg-surface px-2 py-1.5\">\n {(Array.isArray(value) ? value : []).map((tag) => (\n <span\n key={tag}\n className=\"inline-flex items-center gap-1 rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-primary\"\n >\n {tag}\n <button\n type=\"button\"\n aria-label={`Remove ${tag}`}\n className=\"text-text-secondary hover:text-text-primary\"\n onClick={() => removeTag(field.name, tag)}\n disabled={submitting}\n >\n <X className=\"h-3 w-3\" />\n </button>\n </span>\n ))}\n <input\n id={fieldId}\n className=\"min-w-[6rem] flex-1 bg-transparent text-sm text-text-primary outline-none placeholder:text-text-placeholder\"\n value={tagDrafts[field.name] ?? ''}\n placeholder={field.placeholder ?? 'Add and press Enter'}\n disabled={submitting}\n onChange={(event) =>\n setTagDrafts((prev) => ({ ...prev, [field.name]: event.target.value }))\n }\n onKeyDown={(event) => {\n if (event.key === 'Enter' || event.key === ',') {\n event.preventDefault();\n commitTag(field.name);\n } else if (\n event.key === 'Backspace' &&\n (tagDrafts[field.name] ?? '') === ''\n ) {\n const current = Array.isArray(value) ? value : [];\n if (current.length > 0) {\n removeTag(field.name, current[current.length - 1]);\n }\n }\n }}\n onBlur={() => commitTag(field.name)}\n />\n </div>\n ) : (\n <Input\n id={fieldId}\n type={\n field.type === 'number'\n ? 'number'\n : field.type === 'date'\n ? 'date'\n : field.type === 'email'\n ? 'email'\n : 'text'\n }\n value={typeof value === 'string' ? value : ''}\n placeholder={field.placeholder}\n disabled={submitting}\n onChange={(event) => setFieldValue(field.name, event.target.value)}\n />\n )}\n </>\n )}\n </div>\n );\n })}\n </div>\n\n <ResponsiveDialogFooter>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => onOpenChange(false)}\n disabled={submitting}\n >\n Cancel\n </Button>\n <Button type=\"submit\" disabled={submitting}>\n {submitting ? 'Saving…' : submitLabel}\n </Button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\n// ── Optional open/close helper ──────────────────────────────────\nexport interface UseFormModalResult<TContext> {\n open: boolean;\n /** The row/context passed to openModal (e.g. the entity being edited). */\n context: TContext | null;\n openModal: (context?: TContext) => void;\n closeModal: () => void;\n onOpenChange: (open: boolean) => void;\n}\n\n/**\n * Tiny helper for pages that toggle a single FormModal and want to remember\n * which row is being edited. `context` is null for a create flow.\n */\nexport function useFormModal<TContext = void>(): UseFormModalResult<TContext> {\n const [open, setOpen] = useState(false);\n const [context, setContext] = useState<TContext | null>(null);\n\n const openModal = useCallback((next?: TContext) => {\n setContext(next ?? null);\n setOpen(true);\n }, []);\n\n const closeModal = useCallback(() => setOpen(false), []);\n\n const onOpenChange = useCallback((nextOpen: boolean) => {\n setOpen(nextOpen);\n if (!nextOpen) {\n setContext(null);\n }\n }, []);\n\n return { open, context, openModal, closeModal, onOpenChange };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"FormModal.js","names":[],"sources":["../../../../src/crewfoundry/components/forms/FormModal.tsx"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\nimport {\n Button,\n Input,\n Label,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n Switch,\n Textarea,\n} from '@burdenoff/fe-libs/ui';\nimport { useGlobalUi } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { X } from 'lucide-react';\n\n// ── Public API ──────────────────────────────────────────────────\nexport type FormFieldType =\n | 'text'\n | 'number'\n | 'date'\n | 'email'\n | 'textarea'\n | 'select'\n | 'tags'\n | 'switch';\n\nexport type FormValue = string | number | boolean | string[];\nexport type FormValues = Record<string, FormValue>;\n\nexport interface FieldOption {\n label: string;\n value: string;\n}\n\nexport interface FieldConfig {\n name: string;\n label: string;\n type: FormFieldType;\n required?: boolean;\n placeholder?: string;\n options?: FieldOption[];\n /** Column span in the 2-col grid on md+ (defaults to 1; textarea/tags default to 2). */\n colSpan?: 1 | 2;\n}\n\nexport interface FormModalProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n title: string;\n description?: string;\n fields: FieldConfig[];\n initialValues?: FormValues;\n submitLabel?: string;\n onSubmit: (values: FormValues) => Promise<void>;\n}\n\n// ── Internal state helpers ──────────────────────────────────────\n// Text-like inputs are held as strings for controlled inputs; switches as\n// booleans; tags as string[]. Values are coerced back to their public shapes\n// on submit.\ntype InternalValue = string | boolean | string[];\ntype InternalValues = Record<string, InternalValue>;\n\nconst buildInitialState = (fields: FieldConfig[], initialValues?: FormValues): InternalValues => {\n const state: InternalValues = {};\n for (const field of fields) {\n const raw = initialValues?.[field.name];\n if (field.type === 'switch') {\n state[field.name] = Boolean(raw);\n } else if (field.type === 'tags') {\n if (Array.isArray(raw)) {\n state[field.name] = raw.map((item) => String(item));\n } else if (typeof raw === 'string' && raw.length > 0) {\n state[field.name] = raw\n .split(',')\n .map((item) => item.trim())\n .filter(Boolean);\n } else {\n state[field.name] = [];\n }\n } else {\n state[field.name] = raw === undefined || raw === null ? '' : String(raw);\n }\n }\n return state;\n};\n\nconst isEmptyValue = (field: FieldConfig, value: InternalValue): boolean => {\n if (field.type === 'switch') {\n return false;\n }\n if (field.type === 'tags') {\n return !Array.isArray(value) || value.length === 0;\n }\n return typeof value !== 'string' || value.trim() === '';\n};\n\nconst toOutput = (fields: FieldConfig[], state: InternalValues): FormValues => {\n const output: FormValues = {};\n for (const field of fields) {\n const value = state[field.name];\n if (field.type === 'switch') {\n output[field.name] = Boolean(value);\n } else if (field.type === 'tags') {\n output[field.name] = Array.isArray(value) ? value : [];\n } else if (field.type === 'number') {\n const raw = typeof value === 'string' ? value.trim() : '';\n output[field.name] = raw === '' ? '' : Number(raw);\n } else {\n output[field.name] = typeof value === 'string' ? value : '';\n }\n }\n return output;\n};\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 focus-visible:shadow-[var(--shadow-focus)] focus-visible:ring-1 focus-visible:ring-[var(--color-focus-ring)] disabled:cursor-not-allowed disabled:opacity-50';\n\n// ── Component ───────────────────────────────────────────────────\nexport function FormModal({\n open,\n onOpenChange,\n title,\n description,\n fields,\n initialValues,\n submitLabel = 'Save',\n onSubmit,\n}: FormModalProps) {\n const { showToast } = useGlobalUi();\n const [values, setValues] = useState<InternalValues>(() =>\n buildInitialState(fields, initialValues)\n );\n const [tagDrafts, setTagDrafts] = useState<Record<string, string>>({});\n const [submitting, setSubmitting] = useState(false);\n\n // Keep the latest field config / initial values reachable from the reset\n // effect without making them effect dependencies (page callers rarely\n // memoize these, and depending on them would loop or clobber edits).\n const fieldsRef = useRef(fields);\n const initialValuesRef = useRef(initialValues);\n const wasOpenRef = useRef(false);\n\n // Sync the latest field config / initial values into refs after each commit\n // (never during render). Declared before the reset effect so that on a rising\n // edge of `open` the refs are refreshed first, then read by the reset below.\n useEffect(() => {\n fieldsRef.current = fields;\n initialValuesRef.current = initialValues;\n }, [fields, initialValues]);\n\n // Reset the working copy on each rising edge of `open` so edit vs create\n // contexts never leak into each other.\n useEffect(() => {\n if (open && !wasOpenRef.current) {\n setValues(buildInitialState(fieldsRef.current, initialValuesRef.current));\n setTagDrafts({});\n setSubmitting(false);\n }\n wasOpenRef.current = open;\n }, [open]);\n\n const setFieldValue = useCallback((name: string, value: InternalValue) => {\n setValues((prev) => ({ ...prev, [name]: value }));\n }, []);\n\n const commitTag = useCallback((name: string) => {\n setTagDrafts((prevDrafts) => {\n const draft = (prevDrafts[name] ?? '').trim();\n if (draft) {\n setValues((prev) => {\n const current = Array.isArray(prev[name]) ? (prev[name] as string[]) : [];\n if (current.includes(draft)) {\n return prev;\n }\n return { ...prev, [name]: [...current, draft] };\n });\n }\n return { ...prevDrafts, [name]: '' };\n });\n }, []);\n\n const removeTag = useCallback((name: string, tag: string) => {\n setValues((prev) => {\n const current = Array.isArray(prev[name]) ? (prev[name] as string[]) : [];\n return { ...prev, [name]: current.filter((item) => item !== tag) };\n });\n }, []);\n\n const handleSubmit = useCallback(\n async (event: React.FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n if (submitting) {\n return;\n }\n\n // Fold any un-committed tag draft into its field before validating.\n const staged: InternalValues = { ...values };\n for (const field of fields) {\n if (field.type === 'tags') {\n const draft = (tagDrafts[field.name] ?? '').trim();\n if (draft) {\n const current = Array.isArray(staged[field.name])\n ? (staged[field.name] as string[])\n : [];\n if (!current.includes(draft)) {\n staged[field.name] = [...current, draft];\n }\n }\n }\n }\n\n const missing = fields.find(\n (field) => field.required && isEmptyValue(field, staged[field.name])\n );\n if (missing) {\n showToast(`${missing.label} is required`, 'warning');\n return;\n }\n\n setSubmitting(true);\n try {\n await onSubmit(toOutput(fields, staged));\n onOpenChange(false);\n } catch (error) {\n showToast(error instanceof Error ? error.message : 'Something went wrong', 'error');\n } finally {\n setSubmitting(false);\n }\n },\n [fields, onOpenChange, onSubmit, showToast, submitting, tagDrafts, values]\n );\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent className=\"max-h-[85vh] overflow-y-auto sm:max-w-[560px]\">\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>{title}</ResponsiveDialogTitle>\n {description ? (\n <ResponsiveDialogDescription>{description}</ResponsiveDialogDescription>\n ) : null}\n </ResponsiveDialogHeader>\n\n <form onSubmit={handleSubmit} className=\"space-y-4\">\n <div className=\"grid grid-cols-1 gap-4 md:grid-cols-2\">\n {fields.map((field) => {\n const fieldId = `formmodal-${field.name}`;\n const spanTwo =\n field.colSpan === 2 ||\n (field.colSpan === undefined &&\n (field.type === 'textarea' || field.type === 'tags'));\n const value = values[field.name];\n\n return (\n <div\n key={field.name}\n className={`flex flex-col gap-1.5 ${spanTwo ? 'md:col-span-2' : ''}`}\n >\n {field.type === 'switch' ? (\n <div className=\"flex items-center justify-between gap-3 rounded-input border border-border-default bg-bg-surface px-3 py-2\">\n <Label htmlFor={fieldId}>\n {field.label}\n {field.required ? <span className=\"text-status-error-fg\"> *</span> : null}\n </Label>\n <Switch\n id={fieldId}\n checked={Boolean(value)}\n onCheckedChange={(checked) => setFieldValue(field.name, checked)}\n disabled={submitting}\n />\n </div>\n ) : (\n <>\n <Label htmlFor={fieldId}>\n {field.label}\n {field.required ? <span className=\"text-status-error-fg\"> *</span> : null}\n </Label>\n\n {field.type === 'textarea' ? (\n <Textarea\n id={fieldId}\n value={typeof value === 'string' ? value : ''}\n placeholder={field.placeholder}\n disabled={submitting}\n onChange={(event) => setFieldValue(field.name, event.target.value)}\n />\n ) : field.type === 'select' ? (\n <select\n id={fieldId}\n className={SELECT_CLASS}\n value={typeof value === 'string' ? value : ''}\n disabled={submitting}\n onChange={(event) => setFieldValue(field.name, event.target.value)}\n >\n <option value=\"\" disabled>\n {field.placeholder ?? 'Select…'}\n </option>\n {(field.options ?? []).map((option) => (\n <option key={option.value} value={option.value}>\n {option.label}\n </option>\n ))}\n </select>\n ) : field.type === 'tags' ? (\n <div className=\"flex min-h-10 flex-wrap items-center gap-1.5 rounded-input border border-border-default bg-bg-surface px-2 py-1.5\">\n {(Array.isArray(value) ? value : []).map((tag) => (\n <span\n key={tag}\n className=\"inline-flex items-center gap-1 rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-primary\"\n >\n {tag}\n <button\n type=\"button\"\n aria-label={`Remove ${tag}`}\n className=\"text-text-secondary hover:text-text-primary\"\n onClick={() => removeTag(field.name, tag)}\n disabled={submitting}\n >\n <X className=\"h-3 w-3\" />\n </button>\n </span>\n ))}\n <input\n id={fieldId}\n className=\"min-w-[6rem] flex-1 bg-transparent text-sm text-text-primary outline-none placeholder:text-text-placeholder\"\n value={tagDrafts[field.name] ?? ''}\n placeholder={field.placeholder ?? 'Add and press Enter'}\n disabled={submitting}\n onChange={(event) =>\n setTagDrafts((prev) => ({\n ...prev,\n [field.name]: event.target.value,\n }))\n }\n onKeyDown={(event) => {\n if (event.key === 'Enter' || event.key === ',') {\n event.preventDefault();\n commitTag(field.name);\n } else if (\n event.key === 'Backspace' &&\n (tagDrafts[field.name] ?? '') === ''\n ) {\n const current = Array.isArray(value) ? value : [];\n if (current.length > 0) {\n removeTag(field.name, current[current.length - 1]);\n }\n }\n }}\n onBlur={() => commitTag(field.name)}\n />\n </div>\n ) : (\n <Input\n id={fieldId}\n type={\n field.type === 'number'\n ? 'number'\n : field.type === 'date'\n ? 'date'\n : field.type === 'email'\n ? 'email'\n : 'text'\n }\n value={typeof value === 'string' ? value : ''}\n placeholder={field.placeholder}\n disabled={submitting}\n onChange={(event) => setFieldValue(field.name, event.target.value)}\n />\n )}\n </>\n )}\n </div>\n );\n })}\n </div>\n\n <ResponsiveDialogFooter>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => onOpenChange(false)}\n disabled={submitting}\n >\n Cancel\n </Button>\n <Button type=\"submit\" disabled={submitting}>\n {submitting ? 'Saving…' : submitLabel}\n </Button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\n// ── Optional open/close helper ──────────────────────────────────\nexport interface UseFormModalResult<TContext> {\n open: boolean;\n /** The row/context passed to openModal (e.g. the entity being edited). */\n context: TContext | null;\n openModal: (context?: TContext) => void;\n closeModal: () => void;\n onOpenChange: (open: boolean) => void;\n}\n\n/**\n * Tiny helper for pages that toggle a single FormModal and want to remember\n * which row is being edited. `context` is null for a create flow.\n */\nexport function useFormModal<TContext = void>(): UseFormModalResult<TContext> {\n const [open, setOpen] = useState(false);\n const [context, setContext] = useState<TContext | null>(null);\n\n const openModal = useCallback((next?: TContext) => {\n setContext(next ?? null);\n setOpen(true);\n }, []);\n\n const closeModal = useCallback(() => setOpen(false), []);\n\n const onOpenChange = useCallback((nextOpen: boolean) => {\n setOpen(nextOpen);\n if (!nextOpen) {\n setContext(null);\n }\n }, []);\n\n return { open, context, openModal, closeModal, onOpenChange };\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AdminAdminPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/AdminAdminPagePage.tsx"],"sourcesContent":["import { useMemo } from 'react';\nimport {\n Activity,\n Database,\n Globe,\n Lock,\n Monitor,\n Server,\n Shield,\n UserCheck,\n Users,\n Wrench,\n} from 'lucide-react';\nimport {\n Badge,\n Button,\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n GlassCard,\n GradientText,\n IllustratedEmptyState,\n PagePurpose,\n ResponsiveTable,\n type ResponsiveTableColumn,\n} from '@burdenoff/fe-libs/ui';\nimport { useCrewFoundryStore } from '../store/crewfoundryStore';\n\ntype SystemModule = {\n name: string;\n status: 'operational' | 'degraded';\n icon: typeof Lock;\n};\n\ntype UserRole = {\n role: string;\n count: number;\n permissions: string;\n};\n\nexport function AdminAdminPagePage() {\n const { employees, departments, notifications, payrollRecords, jobPostings } =\n useCrewFoundryStore();\n\n const systemModules = useMemo<SystemModule[]>(() => {\n const hasWarning = notifications.some((notification) => notification.type === 'warning');\n const hasError = notifications.some((notification) => notification.type === 'error');\n\n return [\n { name: 'Authentication', status: 'operational', icon: Lock },\n { name: 'Employee Directory', status: 'operational', icon: Users },\n {\n name: 'Payroll Engine',\n status: payrollRecords.some((record) => record.status === 'failed')\n ? 'degraded'\n : 'operational',\n icon: Database,\n },\n { name: 'Recruitment ATS', status: 'operational', icon: Globe },\n {\n name: 'Analytics Pipeline',\n status: hasWarning || hasError ? 'degraded' : 'operational',\n icon: Activity,\n },\n {\n name: 'Notification Service',\n status: hasError ? 'degraded' : 'operational',\n icon: Monitor,\n },\n ];\n }, [notifications, payrollRecords]);\n\n // Role distribution derived from real employee positions and active postings.\n const userRoles = useMemo<UserRole[]>(() => {\n const managers = employees.filter((employee) =>\n employee.position.toLowerCase().includes('manager')\n ).length;\n const leads = employees.filter((employee) =>\n employee.position.toLowerCase().includes('lead')\n ).length;\n const recruiters = jobPostings.filter((posting) => posting.status === 'active').length;\n return [\n {\n role: 'HR Manager',\n count: Math.max(1, managers),\n permissions: 'Employee, payroll, leave management',\n },\n {\n role: 'Hiring Manager',\n count: Math.max(1, recruiters),\n permissions: 'Job postings, candidates, interviews',\n },\n {\n role: 'Department Lead',\n count: Math.max(1, leads),\n permissions: 'Team management, reports',\n },\n {\n role: 'Employee',\n count: Math.max(0, employees.length - managers - leads),\n permissions: 'Self-service, directory, learning',\n },\n ];\n }, [employees, jobPostings]);\n\n const operationalModules = systemModules.filter(\n (module) => module.status === 'operational'\n ).length;\n const activeUsers = employees.filter((employee) => employee.status === 'active').length;\n const securityScore = Math.round(\n (operationalModules / Math.max(systemModules.length, 1)) * 100\n );\n\n const roleColumns: ResponsiveTableColumn<UserRole>[] = [\n {\n key: 'role',\n header: 'Role',\n priority: 'primary',\n cell: (role) => <span className=\"font-medium\">{role.role}</span>,\n },\n {\n key: 'count',\n header: 'Users',\n priority: 'primary',\n cell: (role) => <span className=\"md:block md:text-right\">{role.count}</span>,\n },\n {\n key: 'permissions',\n header: 'Permissions',\n priority: 'secondary',\n cell: (role) => <span className=\"text-xs text-text-secondary\">{role.permissions}</span>,\n },\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-center sm:justify-between\">\n <div>\n <h1 className=\"text-2xl font-semibold\">\n <GradientText>Administration</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">System administration and access management</p>\n <PagePurpose className=\"mt-1\">Manage organization-wide settings, roles, and system controls from one place — use it to onboard admins, adjust permissions, and keep your CrewFoundry workspace secure and compliant.</PagePurpose>\n </div>\n <div className=\"flex gap-2\">\n <Button\n variant=\"outline\"\n size=\"sm\"\n disabled\n aria-label=\"System settings (coming soon)\"\n title=\"System settings are coming soon\"\n >\n <Wrench className=\"mr-1 h-4 w-4\" />\n System Settings\n </Button>\n <Button\n size=\"sm\"\n disabled\n aria-label=\"Security controls (coming soon)\"\n title=\"Security controls are coming soon\"\n >\n <Shield className=\"mr-1 h-4 w-4\" />\n Security\n </Button>\n </div>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-4\">\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\">Total Users</CardTitle>\n <Users className=\"h-4 w-4 text-text-secondary\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">{employees.length}</div>\n <p className=\"text-xs text-text-secondary\">{departments.length} departments</p>\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\">Active Users</CardTitle>\n <UserCheck className=\"h-4 w-4 text-text-secondary\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">{activeUsers}</div>\n <p className=\"text-xs text-text-secondary\">Currently active employees</p>\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\">Operational Modules</CardTitle>\n <Server className=\"h-4 w-4 text-text-secondary\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">\n {operationalModules}/{systemModules.length}\n </div>\n <p className=\"text-xs text-text-secondary\">Modules reporting healthy</p>\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\">Security Score</CardTitle>\n <Shield className=\"h-4 w-4 text-text-secondary\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">{securityScore}/100</div>\n <p className=\"text-xs text-status-success-text\">Derived from module health checks</p>\n </CardContent>\n </GlassCard>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-2\">\n <Card>\n <CardHeader>\n <CardTitle>System Status</CardTitle>\n <CardDescription>Module health derived from live workspace signals</CardDescription>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {systemModules.map((module) => (\n <div\n key={module.name}\n className=\"flex items-center justify-between rounded border p-3\"\n >\n <div className=\"flex items-center gap-3\">\n <module.icon className=\"h-4 w-4 text-text-secondary\" />\n <span className=\"text-sm font-medium\">{module.name}</span>\n </div>\n <Badge\n variant={module.status === 'operational' ? 'default' : 'destructive'}\n className=\"text-[10px]\"\n >\n {module.status}\n </Badge>\n </div>\n ))}\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>User Roles</CardTitle>\n <CardDescription>Role distribution and permissions</CardDescription>\n </CardHeader>\n <CardContent>\n <ResponsiveTable columns={roleColumns} data={userRoles} rowKey={(role) => role.role} />\n </CardContent>\n </Card>\n </div>\n\n <Card>\n <CardHeader>\n <CardTitle>Recent Audit Log</CardTitle>\n <CardDescription>System activity and security events</CardDescription>\n </CardHeader>\n <CardContent>\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"Audit log coming soon\"\n description=\"A verified trail of real system and security events will appear here once audit-log integration is connected.\"\n />\n </CardContent>\n </Card>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AdminAdminPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/AdminAdminPagePage.tsx"],"sourcesContent":["import { useMemo } from 'react';\nimport {\n Activity,\n Database,\n Globe,\n Lock,\n Monitor,\n Server,\n Shield,\n UserCheck,\n Users,\n Wrench,\n} from 'lucide-react';\nimport {\n Badge,\n Button,\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n GlassCard,\n GradientText,\n IllustratedEmptyState,\n PagePurpose,\n ResponsiveTable,\n type ResponsiveTableColumn,\n} from '@burdenoff/fe-libs/ui';\nimport { useCrewFoundryStore } from '../store/crewfoundryStore';\n\ntype SystemModule = {\n name: string;\n status: 'operational' | 'degraded';\n icon: typeof Lock;\n};\n\ntype UserRole = {\n role: string;\n count: number;\n permissions: string;\n};\n\nexport function AdminAdminPagePage() {\n const { employees, departments, notifications, payrollRecords, jobPostings } =\n useCrewFoundryStore();\n\n const systemModules = useMemo<SystemModule[]>(() => {\n const hasWarning = notifications.some((notification) => notification.type === 'warning');\n const hasError = notifications.some((notification) => notification.type === 'error');\n\n return [\n { name: 'Authentication', status: 'operational', icon: Lock },\n { name: 'Employee Directory', status: 'operational', icon: Users },\n {\n name: 'Payroll Engine',\n status: payrollRecords.some((record) => record.status === 'failed')\n ? 'degraded'\n : 'operational',\n icon: Database,\n },\n { name: 'Recruitment ATS', status: 'operational', icon: Globe },\n {\n name: 'Analytics Pipeline',\n status: hasWarning || hasError ? 'degraded' : 'operational',\n icon: Activity,\n },\n {\n name: 'Notification Service',\n status: hasError ? 'degraded' : 'operational',\n icon: Monitor,\n },\n ];\n }, [notifications, payrollRecords]);\n\n // Role distribution derived from real employee positions and active postings.\n const userRoles = useMemo<UserRole[]>(() => {\n const managers = employees.filter((employee) =>\n employee.position.toLowerCase().includes('manager')\n ).length;\n const leads = employees.filter((employee) =>\n employee.position.toLowerCase().includes('lead')\n ).length;\n const recruiters = jobPostings.filter((posting) => posting.status === 'active').length;\n return [\n {\n role: 'HR Manager',\n count: Math.max(1, managers),\n permissions: 'Employee, payroll, leave management',\n },\n {\n role: 'Hiring Manager',\n count: Math.max(1, recruiters),\n permissions: 'Job postings, candidates, interviews',\n },\n {\n role: 'Department Lead',\n count: Math.max(1, leads),\n permissions: 'Team management, reports',\n },\n {\n role: 'Employee',\n count: Math.max(0, employees.length - managers - leads),\n permissions: 'Self-service, directory, learning',\n },\n ];\n }, [employees, jobPostings]);\n\n const operationalModules = systemModules.filter(\n (module) => module.status === 'operational'\n ).length;\n const activeUsers = employees.filter((employee) => employee.status === 'active').length;\n const securityScore = Math.round((operationalModules / Math.max(systemModules.length, 1)) * 100);\n\n const roleColumns: ResponsiveTableColumn<UserRole>[] = [\n {\n key: 'role',\n header: 'Role',\n priority: 'primary',\n cell: (role) => <span className=\"font-medium\">{role.role}</span>,\n },\n {\n key: 'count',\n header: 'Users',\n priority: 'primary',\n cell: (role) => <span className=\"md:block md:text-right\">{role.count}</span>,\n },\n {\n key: 'permissions',\n header: 'Permissions',\n priority: 'secondary',\n cell: (role) => <span className=\"text-xs text-text-secondary\">{role.permissions}</span>,\n },\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-center sm:justify-between\">\n <div>\n <h1 className=\"text-2xl font-semibold\">\n <GradientText>Administration</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">System administration and access management</p>\n <PagePurpose className=\"mt-1\">\n Manage organization-wide settings, roles, and system controls from one place — use it to\n onboard admins, adjust permissions, and keep your CrewFoundry workspace secure and\n compliant.\n </PagePurpose>\n </div>\n <div className=\"flex gap-2\">\n <Button\n variant=\"outline\"\n size=\"sm\"\n disabled\n aria-label=\"System settings (coming soon)\"\n title=\"System settings are coming soon\"\n >\n <Wrench className=\"mr-1 h-4 w-4\" />\n System Settings\n </Button>\n <Button\n size=\"sm\"\n disabled\n aria-label=\"Security controls (coming soon)\"\n title=\"Security controls are coming soon\"\n >\n <Shield className=\"mr-1 h-4 w-4\" />\n Security\n </Button>\n </div>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-4\">\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\">Total Users</CardTitle>\n <Users className=\"h-4 w-4 text-text-secondary\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">{employees.length}</div>\n <p className=\"text-xs text-text-secondary\">{departments.length} departments</p>\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\">Active Users</CardTitle>\n <UserCheck className=\"h-4 w-4 text-text-secondary\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">{activeUsers}</div>\n <p className=\"text-xs text-text-secondary\">Currently active employees</p>\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\">Operational Modules</CardTitle>\n <Server className=\"h-4 w-4 text-text-secondary\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">\n {operationalModules}/{systemModules.length}\n </div>\n <p className=\"text-xs text-text-secondary\">Modules reporting healthy</p>\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\">Security Score</CardTitle>\n <Shield className=\"h-4 w-4 text-text-secondary\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">{securityScore}/100</div>\n <p className=\"text-xs text-status-success-text\">Derived from module health checks</p>\n </CardContent>\n </GlassCard>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-2\">\n <Card>\n <CardHeader>\n <CardTitle>System Status</CardTitle>\n <CardDescription>Module health derived from live workspace signals</CardDescription>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {systemModules.map((module) => (\n <div\n key={module.name}\n className=\"flex items-center justify-between rounded border p-3\"\n >\n <div className=\"flex items-center gap-3\">\n <module.icon className=\"h-4 w-4 text-text-secondary\" />\n <span className=\"text-sm font-medium\">{module.name}</span>\n </div>\n <Badge\n variant={module.status === 'operational' ? 'default' : 'destructive'}\n className=\"text-[10px]\"\n >\n {module.status}\n </Badge>\n </div>\n ))}\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>User Roles</CardTitle>\n <CardDescription>Role distribution and permissions</CardDescription>\n </CardHeader>\n <CardContent>\n <ResponsiveTable columns={roleColumns} data={userRoles} rowKey={(role) => role.role} />\n </CardContent>\n </Card>\n </div>\n\n <Card>\n <CardHeader>\n <CardTitle>Recent Audit Log</CardTitle>\n <CardDescription>System activity and security events</CardDescription>\n </CardHeader>\n <CardContent>\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"Audit log coming soon\"\n description=\"A verified trail of real system and security events will appear here once audit-log integration is connected.\"\n />\n </CardContent>\n </Card>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AnalyticsAnalyticsPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/AnalyticsAnalyticsPagePage.tsx"],"sourcesContent":["import { useMemo } from 'react';\nimport { Activity, BarChart3, Calendar, PieChart, TrendingUp, Users } from 'lucide-react';\nimport {\n Badge,\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n GlassCard,\n GradientText,\n PagePurpose,\n ResponsiveTable,\n type ResponsiveTableColumn,\n Tabs,\n TabsContent,\n TabsList,\n TabsTrigger,\n} from '@burdenoff/fe-libs/ui';\nimport { useCrewFoundryStore } from '../store/crewfoundryStore';\n\ntype MonthlyMetric = {\n month: string;\n headcount: number;\n hires: number;\n exits: number;\n};\n\ntype DeptRow = {\n name: string;\n count: number;\n budget: number;\n};\n\ntype FunnelStage = {\n stage: string;\n count: number;\n color: string;\n};\n\nexport function AnalyticsAnalyticsPagePage() {\n const { employees, departments, jobPostings, candidates, projects } = useCrewFoundryStore();\n\n const monthlyMetrics = useMemo<MonthlyMetric[]>(() => {\n if (employees.length === 0) {\n return [];\n }\n const monthKey = (value: string) => {\n const date = new Date(value);\n return `${date.getFullYear()}-${String(date.getMonth()).padStart(2, '0')}`;\n };\n const distinctMonths = Array.from(new Set(employees.map((item) => monthKey(item.startDate))))\n .sort()\n .slice(-6);\n return distinctMonths.map((key) => {\n const [year, month] = key.split('-').map(Number);\n const monthEnd = new Date(year, month + 1, 0).getTime();\n const label = new Date(year, month, 1).toLocaleString('en-US', {\n month: 'short',\n year: '2-digit',\n });\n // Real cumulative headcount as of month end, and real hires/exits in month.\n const headcount = employees.filter(\n (item) => new Date(item.startDate).getTime() <= monthEnd && item.status !== 'terminated'\n ).length;\n const hires = employees.filter((item) => monthKey(item.startDate) === key).length;\n const exits = employees.filter(\n (item) => item.status === 'terminated' && monthKey(item.startDate) === key\n ).length;\n return { month: label, headcount, hires, exits };\n });\n }, [employees]);\n\n const deptDistribution = departments.map((department) => ({\n name: department.name,\n count: department.employeeCount,\n budget: department.budget,\n }));\n\n const maxDeptCount = Math.max(1, ...deptDistribution.map((department) => department.count));\n const maxMetric = Math.max(1, ...monthlyMetrics.map((metric) => metric.headcount));\n\n const sortedDeptDistribution = [...deptDistribution].sort((a, b) => b.count - a.count);\n\n const deptColumns: ResponsiveTableColumn<DeptRow>[] = [\n {\n key: 'name',\n header: 'Department',\n priority: 'primary',\n cell: (department) => <span className=\"font-medium\">{department.name}</span>,\n },\n {\n key: 'count',\n header: 'Headcount',\n priority: 'secondary',\n cell: (department) => <span className=\"md:block md:text-right\">{department.count}</span>,\n },\n {\n key: 'budget',\n header: 'Budget',\n priority: 'secondary',\n cell: (department) => (\n <span className=\"md:block md:text-right\">\n ${(department.budget / 1000000).toFixed(1)}M\n </span>\n ),\n },\n {\n key: 'distribution',\n header: 'Distribution',\n priority: 'primary',\n cell: (department) => (\n <div className=\"h-3 w-full min-w-24 overflow-hidden rounded bg-bg-sunken\">\n <div\n className=\"h-3 rounded bg-action-primary-bg/70\"\n style={{ width: `${Math.round((department.count / maxDeptCount) * 100)}%` }}\n />\n </div>\n ),\n },\n ];\n\n const hiringFunnel = useMemo<FunnelStage[]>(\n () => [\n {\n stage: 'Applications',\n count: jobPostings.reduce((sum, posting) => sum + posting.applicants, 0),\n color: 'bg-status-info-text',\n },\n {\n stage: 'Screening',\n count: candidates.filter((candidate) => candidate.status === 'screening').length,\n color: 'bg-status-info-text',\n },\n {\n stage: 'Interview',\n count: candidates.filter((candidate) => candidate.status === 'interview').length,\n color: 'bg-action-primary-bg',\n },\n {\n stage: 'Assessment',\n count: candidates.filter((candidate) => candidate.status === 'assessment').length,\n color: 'bg-action-primary-bg',\n },\n {\n stage: 'Offer',\n count: candidates.filter((candidate) => candidate.status === 'offer').length,\n color: 'bg-status-warning-text',\n },\n {\n stage: 'Hired',\n count: candidates.filter((candidate) => candidate.status === 'hired').length,\n color: 'bg-status-success-text',\n },\n ],\n [candidates, jobPostings]\n );\n\n const maxFunnel = Math.max(1, ...hiringFunnel.map((stage) => stage.count));\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex items-center justify-between\">\n <div>\n <h1 className=\"text-2xl font-semibold\">\n <GradientText>Workforce Analytics</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">\n Comprehensive analytics across all HR domains\n </p>\n <PagePurpose className=\"mt-1\">Your workforce analytics hub — track headcount, performance, and engagement trends here to spot what's working and where your teams need attention.</PagePurpose>\n </div>\n <Badge variant=\"outline\" className=\"flex items-center gap-1 self-start\">\n <Calendar className=\"h-3.5 w-3.5\" />\n Rolling 6 months\n </Badge>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-4\">\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\">Total Workforce</CardTitle>\n <Users className=\"h-4 w-4 text-text-secondary\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">{employees.length}</div>\n <div className=\"flex items-center text-xs text-status-success-text\">\n <TrendingUp className=\"mr-1 h-3 w-3\" />\n Live headcount data\n </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\">Active Departments</CardTitle>\n <PieChart className=\"h-4 w-4 text-text-secondary\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">{departments.length}</div>\n <p className=\"text-xs text-text-secondary\">All departments tracked</p>\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\">Active Projects</CardTitle>\n <Activity className=\"h-4 w-4 text-text-secondary\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">\n {projects.filter((project) => project.status === 'active').length}\n </div>\n <p className=\"text-xs text-text-secondary\">\n {projects.filter((project) => project.status === 'completed').length} completed\n </p>\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\">Hiring Pipeline</CardTitle>\n <BarChart3 className=\"h-4 w-4 text-text-secondary\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">{candidates.length}</div>\n <p className=\"text-xs text-text-secondary\">candidates in pipeline</p>\n </CardContent>\n </GlassCard>\n </div>\n\n <Tabs defaultValue=\"overview\">\n <TabsList>\n <TabsTrigger value=\"overview\">Overview</TabsTrigger>\n <TabsTrigger value=\"hiring\">Hiring Funnel</TabsTrigger>\n <TabsTrigger value=\"departments\">Departments</TabsTrigger>\n </TabsList>\n\n <TabsContent value=\"overview\" className=\"space-y-4\">\n <Card>\n <CardHeader>\n <CardTitle>Workforce Growth Trend</CardTitle>\n <CardDescription>Monthly headcount and hiring activity</CardDescription>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {monthlyMetrics.map((metric) => (\n <div\n key={`${metric.month}-${metric.headcount}`}\n className=\"flex items-center gap-3\"\n >\n <span className=\"w-10 text-sm text-text-secondary\">{metric.month}</span>\n <div className=\"h-5 flex-1 overflow-hidden rounded bg-bg-sunken\">\n <div\n className=\"h-5 rounded bg-action-primary-bg\"\n style={{ width: `${Math.round((metric.headcount / maxMetric) * 100)}%` }}\n />\n </div>\n <span className=\"w-10 text-right text-sm font-medium\">{metric.headcount}</span>\n <div className=\"flex w-32 justify-end gap-1\">\n <Badge variant=\"default\" className=\"text-[10px]\">\n +{metric.hires} hired\n </Badge>\n {metric.exits > 0 && (\n <Badge variant=\"destructive\" className=\"text-[10px]\">\n -{metric.exits} exits\n </Badge>\n )}\n </div>\n </div>\n ))}\n </CardContent>\n </Card>\n </TabsContent>\n\n <TabsContent value=\"hiring\" className=\"space-y-4\">\n <Card>\n <CardHeader>\n <CardTitle>Hiring Funnel</CardTitle>\n <CardDescription>Candidate pipeline stages</CardDescription>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n {hiringFunnel.map((stage) => (\n <div key={stage.stage} className=\"flex items-center gap-3\">\n <span className=\"w-28 text-sm font-medium\">{stage.stage}</span>\n <div className=\"h-6 flex-1 overflow-hidden rounded bg-bg-sunken\">\n <div\n className={`h-6 rounded ${stage.color}`}\n style={{\n width: `${Math.max(Math.round((stage.count / maxFunnel) * 100), 5)}%`,\n }}\n />\n </div>\n <span className=\"w-10 text-right text-sm font-bold\">{stage.count}</span>\n </div>\n ))}\n </CardContent>\n </Card>\n </TabsContent>\n\n <TabsContent value=\"departments\" className=\"space-y-4\">\n <Card>\n <CardHeader>\n <CardTitle>Department Breakdown</CardTitle>\n <CardDescription>Headcount and budget by department</CardDescription>\n </CardHeader>\n <CardContent>\n <ResponsiveTable\n columns={deptColumns}\n data={sortedDeptDistribution}\n rowKey={(department) => department.name}\n />\n </CardContent>\n </Card>\n </TabsContent>\n </Tabs>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AnalyticsAnalyticsPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/AnalyticsAnalyticsPagePage.tsx"],"sourcesContent":["import { useMemo } from 'react';\nimport { Activity, BarChart3, Calendar, PieChart, TrendingUp, Users } from 'lucide-react';\nimport {\n Badge,\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n GlassCard,\n GradientText,\n PagePurpose,\n ResponsiveTable,\n type ResponsiveTableColumn,\n Tabs,\n TabsContent,\n TabsList,\n TabsTrigger,\n} from '@burdenoff/fe-libs/ui';\nimport { useCrewFoundryStore } from '../store/crewfoundryStore';\n\ntype MonthlyMetric = {\n month: string;\n headcount: number;\n hires: number;\n exits: number;\n};\n\ntype DeptRow = {\n name: string;\n count: number;\n budget: number;\n};\n\ntype FunnelStage = {\n stage: string;\n count: number;\n color: string;\n};\n\nexport function AnalyticsAnalyticsPagePage() {\n const { employees, departments, jobPostings, candidates, projects } = useCrewFoundryStore();\n\n const monthlyMetrics = useMemo<MonthlyMetric[]>(() => {\n if (employees.length === 0) {\n return [];\n }\n const monthKey = (value: string) => {\n const date = new Date(value);\n return `${date.getFullYear()}-${String(date.getMonth()).padStart(2, '0')}`;\n };\n const distinctMonths = Array.from(new Set(employees.map((item) => monthKey(item.startDate))))\n .sort()\n .slice(-6);\n return distinctMonths.map((key) => {\n const [year, month] = key.split('-').map(Number);\n const monthEnd = new Date(year, month + 1, 0).getTime();\n const label = new Date(year, month, 1).toLocaleString('en-US', {\n month: 'short',\n year: '2-digit',\n });\n // Real cumulative headcount as of month end, and real hires/exits in month.\n const headcount = employees.filter(\n (item) => new Date(item.startDate).getTime() <= monthEnd && item.status !== 'terminated'\n ).length;\n const hires = employees.filter((item) => monthKey(item.startDate) === key).length;\n const exits = employees.filter(\n (item) => item.status === 'terminated' && monthKey(item.startDate) === key\n ).length;\n return { month: label, headcount, hires, exits };\n });\n }, [employees]);\n\n const deptDistribution = departments.map((department) => ({\n name: department.name,\n count: department.employeeCount,\n budget: department.budget,\n }));\n\n const maxDeptCount = Math.max(1, ...deptDistribution.map((department) => department.count));\n const maxMetric = Math.max(1, ...monthlyMetrics.map((metric) => metric.headcount));\n\n const sortedDeptDistribution = [...deptDistribution].sort((a, b) => b.count - a.count);\n\n const deptColumns: ResponsiveTableColumn<DeptRow>[] = [\n {\n key: 'name',\n header: 'Department',\n priority: 'primary',\n cell: (department) => <span className=\"font-medium\">{department.name}</span>,\n },\n {\n key: 'count',\n header: 'Headcount',\n priority: 'secondary',\n cell: (department) => <span className=\"md:block md:text-right\">{department.count}</span>,\n },\n {\n key: 'budget',\n header: 'Budget',\n priority: 'secondary',\n cell: (department) => (\n <span className=\"md:block md:text-right\">${(department.budget / 1000000).toFixed(1)}M</span>\n ),\n },\n {\n key: 'distribution',\n header: 'Distribution',\n priority: 'primary',\n cell: (department) => (\n <div className=\"h-3 w-full min-w-24 overflow-hidden rounded bg-bg-sunken\">\n <div\n className=\"h-3 rounded bg-action-primary-bg/70\"\n style={{ width: `${Math.round((department.count / maxDeptCount) * 100)}%` }}\n />\n </div>\n ),\n },\n ];\n\n const hiringFunnel = useMemo<FunnelStage[]>(\n () => [\n {\n stage: 'Applications',\n count: jobPostings.reduce((sum, posting) => sum + posting.applicants, 0),\n color: 'bg-status-info-text',\n },\n {\n stage: 'Screening',\n count: candidates.filter((candidate) => candidate.status === 'screening').length,\n color: 'bg-status-info-text',\n },\n {\n stage: 'Interview',\n count: candidates.filter((candidate) => candidate.status === 'interview').length,\n color: 'bg-action-primary-bg',\n },\n {\n stage: 'Assessment',\n count: candidates.filter((candidate) => candidate.status === 'assessment').length,\n color: 'bg-action-primary-bg',\n },\n {\n stage: 'Offer',\n count: candidates.filter((candidate) => candidate.status === 'offer').length,\n color: 'bg-status-warning-text',\n },\n {\n stage: 'Hired',\n count: candidates.filter((candidate) => candidate.status === 'hired').length,\n color: 'bg-status-success-text',\n },\n ],\n [candidates, jobPostings]\n );\n\n const maxFunnel = Math.max(1, ...hiringFunnel.map((stage) => stage.count));\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex items-center justify-between\">\n <div>\n <h1 className=\"text-2xl font-semibold\">\n <GradientText>Workforce Analytics</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">\n Comprehensive analytics across all HR domains\n </p>\n <PagePurpose className=\"mt-1\">\n Your workforce analytics hub — track headcount, performance, and engagement trends here\n to spot what's working and where your teams need attention.\n </PagePurpose>\n </div>\n <Badge variant=\"outline\" className=\"flex items-center gap-1 self-start\">\n <Calendar className=\"h-3.5 w-3.5\" />\n Rolling 6 months\n </Badge>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-4\">\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\">Total Workforce</CardTitle>\n <Users className=\"h-4 w-4 text-text-secondary\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">{employees.length}</div>\n <div className=\"flex items-center text-xs text-status-success-text\">\n <TrendingUp className=\"mr-1 h-3 w-3\" />\n Live headcount data\n </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\">Active Departments</CardTitle>\n <PieChart className=\"h-4 w-4 text-text-secondary\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">{departments.length}</div>\n <p className=\"text-xs text-text-secondary\">All departments tracked</p>\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\">Active Projects</CardTitle>\n <Activity className=\"h-4 w-4 text-text-secondary\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">\n {projects.filter((project) => project.status === 'active').length}\n </div>\n <p className=\"text-xs text-text-secondary\">\n {projects.filter((project) => project.status === 'completed').length} completed\n </p>\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\">Hiring Pipeline</CardTitle>\n <BarChart3 className=\"h-4 w-4 text-text-secondary\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">{candidates.length}</div>\n <p className=\"text-xs text-text-secondary\">candidates in pipeline</p>\n </CardContent>\n </GlassCard>\n </div>\n\n <Tabs defaultValue=\"overview\">\n <TabsList>\n <TabsTrigger value=\"overview\">Overview</TabsTrigger>\n <TabsTrigger value=\"hiring\">Hiring Funnel</TabsTrigger>\n <TabsTrigger value=\"departments\">Departments</TabsTrigger>\n </TabsList>\n\n <TabsContent value=\"overview\" className=\"space-y-4\">\n <Card>\n <CardHeader>\n <CardTitle>Workforce Growth Trend</CardTitle>\n <CardDescription>Monthly headcount and hiring activity</CardDescription>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {monthlyMetrics.map((metric) => (\n <div\n key={`${metric.month}-${metric.headcount}`}\n className=\"flex items-center gap-3\"\n >\n <span className=\"w-10 text-sm text-text-secondary\">{metric.month}</span>\n <div className=\"h-5 flex-1 overflow-hidden rounded bg-bg-sunken\">\n <div\n className=\"h-5 rounded bg-action-primary-bg\"\n style={{ width: `${Math.round((metric.headcount / maxMetric) * 100)}%` }}\n />\n </div>\n <span className=\"w-10 text-right text-sm font-medium\">{metric.headcount}</span>\n <div className=\"flex w-32 justify-end gap-1\">\n <Badge variant=\"default\" className=\"text-[10px]\">\n +{metric.hires} hired\n </Badge>\n {metric.exits > 0 && (\n <Badge variant=\"destructive\" className=\"text-[10px]\">\n -{metric.exits} exits\n </Badge>\n )}\n </div>\n </div>\n ))}\n </CardContent>\n </Card>\n </TabsContent>\n\n <TabsContent value=\"hiring\" className=\"space-y-4\">\n <Card>\n <CardHeader>\n <CardTitle>Hiring Funnel</CardTitle>\n <CardDescription>Candidate pipeline stages</CardDescription>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n {hiringFunnel.map((stage) => (\n <div key={stage.stage} className=\"flex items-center gap-3\">\n <span className=\"w-28 text-sm font-medium\">{stage.stage}</span>\n <div className=\"h-6 flex-1 overflow-hidden rounded bg-bg-sunken\">\n <div\n className={`h-6 rounded ${stage.color}`}\n style={{\n width: `${Math.max(Math.round((stage.count / maxFunnel) * 100), 5)}%`,\n }}\n />\n </div>\n <span className=\"w-10 text-right text-sm font-bold\">{stage.count}</span>\n </div>\n ))}\n </CardContent>\n </Card>\n </TabsContent>\n\n <TabsContent value=\"departments\" className=\"space-y-4\">\n <Card>\n <CardHeader>\n <CardTitle>Department Breakdown</CardTitle>\n <CardDescription>Headcount and budget by department</CardDescription>\n </CardHeader>\n <CardContent>\n <ResponsiveTable\n columns={deptColumns}\n data={sortedDeptDistribution}\n rowKey={(department) => department.name}\n />\n </CardContent>\n </Card>\n </TabsContent>\n </Tabs>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AnalyticsEmployeeAnalyticsPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/AnalyticsEmployeeAnalyticsPagePage.tsx"],"sourcesContent":["import {\n Badge,\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n GlassCard,\n GradientText,\n IllustratedEmptyState,\n PagePurpose,\n ResponsiveTable,\n type ResponsiveTableColumn,\n} from '@burdenoff/fe-libs/ui';\nimport { Award, BarChart3, Clock, MapPin, Star, UserCheck } from 'lucide-react';\nimport { useMemo } from 'react';\nimport { useCrewFoundryStore } from '../store/crewfoundryStore';\n\ntype DeptPerformanceRow = {\n name: string;\n headcount: number;\n avgScore: string;\n avgSalary: number;\n};\n\nexport function AnalyticsEmployeeAnalyticsPagePage() {\n const { employees, departments } = useCrewFoundryStore();\n\n const tenureDistribution = useMemo(() => {\n const total = Math.max(employees.length, 1);\n const getYears = (startDate: string) =>\n Math.max(\n 0,\n (new Date('2026-03-01').getTime() - new Date(startDate).getTime()) /\n (1000 * 60 * 60 * 24 * 365)\n );\n const buckets = [\n { range: '< 1 year', min: 0, max: 1 },\n { range: '1-2 years', min: 1, max: 2 },\n { range: '2-3 years', min: 2, max: 3 },\n { range: '3-4 years', min: 3, max: 4 },\n { range: '4+ years', min: 4, max: Infinity },\n ];\n return buckets.map((bucket) => {\n const count = employees.filter((employee) => {\n const years = getYears(employee.startDate);\n return years >= bucket.min && years < bucket.max;\n }).length;\n return { range: bucket.range, count, percentage: Math.round((count / total) * 100) };\n });\n }, [employees]);\n\n // Real average and median tenure derived from employee start dates.\n const { avgTenure, medianTenure } = useMemo(() => {\n if (employees.length === 0) {\n return { avgTenure: 0, medianTenure: 0 };\n }\n const getYears = (startDate: string) =>\n Math.max(\n 0,\n (new Date('2026-03-01').getTime() - new Date(startDate).getTime()) /\n (1000 * 60 * 60 * 24 * 365)\n );\n const years = employees.map((employee) => getYears(employee.startDate)).sort((a, b) => a - b);\n const avg = years.reduce((sum, value) => sum + value, 0) / years.length;\n const mid = Math.floor(years.length / 2);\n const median =\n years.length % 2 === 1 ? years[mid] : (years[mid - 1] + years[mid]) / 2;\n return { avgTenure: avg, medianTenure: median };\n }, [employees]);\n\n const avgPerformance = (\n employees.reduce((sum, e) => sum + e.performanceScore, 0) / Math.max(employees.length, 1)\n ).toFixed(1);\n const avgSalary = Math.round(\n employees.reduce((sum, e) => sum + e.salary, 0) / Math.max(employees.length, 1)\n );\n const activeCount = employees.filter((e) => e.status === 'active').length;\n const onLeaveCount = employees.filter((e) => e.status === 'on_leave').length;\n\n const deptPerformance: DeptPerformanceRow[] = departments.map((dept) => {\n const deptEmps = employees.filter((e) => e.department === dept.name);\n const avgScore =\n deptEmps.length > 0\n ? (deptEmps.reduce((s, e) => s + e.performanceScore, 0) / deptEmps.length).toFixed(1)\n : '0';\n const avgSal =\n deptEmps.length > 0\n ? Math.round(deptEmps.reduce((s, e) => s + e.salary, 0) / deptEmps.length)\n : 0;\n return { name: dept.name, headcount: deptEmps.length, avgScore, avgSalary: avgSal };\n });\n\n const sortedDeptPerformance = [...deptPerformance].sort(\n (a, b) => Number(b.avgScore) - Number(a.avgScore)\n );\n\n // Real key-insight figures.\n const topDept = sortedDeptPerformance[0];\n const flightRiskCount = employees.filter((e) => e.performanceScore < 3.5).length;\n const deptsWithStaff = deptPerformance.filter((d) => d.headcount > 0);\n const sortedSalaries = [...deptsWithStaff].map((d) => d.avgSalary).sort((a, b) => a - b);\n const medianDeptSalary =\n sortedSalaries.length > 0 ? sortedSalaries[Math.floor(sortedSalaries.length / 2)] : 0;\n const belowMedianDeptCount = deptsWithStaff.filter(\n (d) => d.avgSalary < medianDeptSalary\n ).length;\n\n const deptPerformanceColumns: ResponsiveTableColumn<DeptPerformanceRow>[] = [\n {\n key: 'name',\n header: 'Department',\n priority: 'primary',\n cell: (dept) => <span className=\"font-medium\">{dept.name}</span>,\n },\n {\n key: 'headcount',\n header: 'Headcount',\n priority: 'secondary',\n cell: (dept) => <span className=\"md:block md:text-right\">{dept.headcount}</span>,\n },\n {\n key: 'avgScore',\n header: 'Avg Score',\n priority: 'primary',\n cell: (dept) => (\n <span className=\"md:block md:text-right\">\n <Badge variant={Number(dept.avgScore) >= 4.5 ? 'default' : 'secondary'}>\n {dept.avgScore}\n </Badge>\n </span>\n ),\n },\n {\n key: 'avgSalary',\n header: 'Avg Salary',\n priority: 'secondary',\n cell: (dept) => (\n <span className=\"md:block md:text-right\">${dept.avgSalary.toLocaleString()}</span>\n ),\n },\n ];\n\n const locationBreakdown = employees.reduce<Record<string, number>>((acc, e) => {\n acc[e.location] = (acc[e.location] || 0) + 1;\n return acc;\n }, {});\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex items-center justify-between\">\n <div>\n <h1 className=\"text-2xl font-semibold\">\n <GradientText>Employee Analytics</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">\n Deep-dive into employee performance, engagement, and demographics\n </p>\n <PagePurpose className=\"mt-1\">See how individual employees and teams are performing over time — use these insights for reviews, recognition, and targeted coaching.</PagePurpose>\n </div>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-4\">\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\">Active Employees</CardTitle>\n <UserCheck className=\"h-4 w-4 text-text-secondary\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">{activeCount}</div>\n <p className=\"text-xs text-text-secondary\">{onLeaveCount} currently on leave</p>\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\">Avg Performance</CardTitle>\n <Star className=\"h-4 w-4 text-text-secondary\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">{avgPerformance}/5.0</div>\n <p className=\"text-xs text-text-secondary\">Across {employees.length} employees</p>\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\">Avg Compensation</CardTitle>\n <BarChart3 className=\"h-4 w-4 text-text-secondary\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">${avgSalary.toLocaleString()}</div>\n <p className=\"text-xs text-text-secondary\">Across all departments</p>\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\">Avg Tenure</CardTitle>\n <Clock className=\"h-4 w-4 text-text-secondary\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">{avgTenure.toFixed(1)} yrs</div>\n <p className=\"text-xs text-text-secondary\">Median: {medianTenure.toFixed(1)} years</p>\n </CardContent>\n </GlassCard>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-2\">\n <Card>\n <CardHeader>\n <CardTitle>Department Performance</CardTitle>\n <CardDescription>\n Average performance score and compensation by department\n </CardDescription>\n </CardHeader>\n <CardContent>\n <ResponsiveTable\n columns={deptPerformanceColumns}\n data={sortedDeptPerformance}\n rowKey={(dept) => dept.name}\n />\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>Engagement Drivers</CardTitle>\n <CardDescription>Employee satisfaction by category</CardDescription>\n </CardHeader>\n <CardContent>\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"Engagement surveys coming soon\"\n description=\"Satisfaction and engagement drivers will appear here once the employee survey integration is connected — we don't show estimated scores.\"\n />\n </CardContent>\n </Card>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-2\">\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <Clock className=\"h-4 w-4\" />\n Tenure Distribution\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {tenureDistribution.map((t) => (\n <div key={t.range} className=\"flex items-center gap-3\">\n <span className=\"w-20 text-sm text-text-secondary\">{t.range}</span>\n <div className=\"flex-1 h-4 bg-bg-sunken rounded overflow-hidden\">\n <div\n className=\"h-4 bg-action-primary-bg/70 rounded\"\n style={{ width: `${t.percentage}%` }}\n />\n </div>\n <span className=\"text-sm font-medium w-10 text-right\">{t.count}</span>\n <span className=\"text-xs text-text-secondary w-10 text-right\">{t.percentage}%</span>\n </div>\n ))}\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <MapPin className=\"h-4 w-4\" />\n Location Distribution\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {Object.entries(locationBreakdown)\n .sort(([, a], [, b]) => b - a)\n .map(([loc, count]) => (\n <div key={loc} className=\"flex items-center justify-between py-1\">\n <span className=\"text-sm\">{loc}</span>\n <div className=\"flex items-center gap-2\">\n <span className=\"text-sm font-medium\">{count}</span>\n <Badge variant=\"outline\" className=\"text-[10px]\">\n {Math.round((count / Math.max(employees.length, 1)) * 100)}%\n </Badge>\n </div>\n </div>\n ))}\n </CardContent>\n </Card>\n </div>\n\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <Award className=\"h-4 w-4 text-status-warning-text\" />\n Key Insights\n </CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"grid gap-3 md:grid-cols-3\">\n <div className=\"rounded border p-3\">\n <p className=\"text-sm font-medium\">Highest Performance</p>\n <p className=\"text-xs text-text-secondary mt-1\">\n {topDept\n ? `${topDept.name} leads with an average score of ${topDept.avgScore}`\n : 'No department performance data yet'}\n </p>\n </div>\n <div className=\"rounded border p-3\">\n <p className=\"text-sm font-medium\">Retention Risk</p>\n <p className=\"text-xs text-text-secondary mt-1\">\n {flightRiskCount} employee{flightRiskCount === 1 ? '' : 's'} below a 3.5 performance\n score — a potential flight risk\n </p>\n </div>\n <div className=\"rounded border p-3\">\n <p className=\"text-sm font-medium\">Compensation Gap</p>\n <p className=\"text-xs text-text-secondary mt-1\">\n {belowMedianDeptCount} department{belowMedianDeptCount === 1 ? '' : 's'} below the\n median department salary\n </p>\n </div>\n </div>\n </CardContent>\n </Card>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AnalyticsEmployeeAnalyticsPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/AnalyticsEmployeeAnalyticsPagePage.tsx"],"sourcesContent":["import {\n Badge,\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n GlassCard,\n GradientText,\n IllustratedEmptyState,\n PagePurpose,\n ResponsiveTable,\n type ResponsiveTableColumn,\n} from '@burdenoff/fe-libs/ui';\nimport { Award, BarChart3, Clock, MapPin, Star, UserCheck } from 'lucide-react';\nimport { useMemo } from 'react';\nimport { useCrewFoundryStore } from '../store/crewfoundryStore';\n\ntype DeptPerformanceRow = {\n name: string;\n headcount: number;\n avgScore: string;\n avgSalary: number;\n};\n\nexport function AnalyticsEmployeeAnalyticsPagePage() {\n const { employees, departments } = useCrewFoundryStore();\n\n const tenureDistribution = useMemo(() => {\n const total = Math.max(employees.length, 1);\n const getYears = (startDate: string) =>\n Math.max(\n 0,\n (new Date('2026-03-01').getTime() - new Date(startDate).getTime()) /\n (1000 * 60 * 60 * 24 * 365)\n );\n const buckets = [\n { range: '< 1 year', min: 0, max: 1 },\n { range: '1-2 years', min: 1, max: 2 },\n { range: '2-3 years', min: 2, max: 3 },\n { range: '3-4 years', min: 3, max: 4 },\n { range: '4+ years', min: 4, max: Infinity },\n ];\n return buckets.map((bucket) => {\n const count = employees.filter((employee) => {\n const years = getYears(employee.startDate);\n return years >= bucket.min && years < bucket.max;\n }).length;\n return { range: bucket.range, count, percentage: Math.round((count / total) * 100) };\n });\n }, [employees]);\n\n // Real average and median tenure derived from employee start dates.\n const { avgTenure, medianTenure } = useMemo(() => {\n if (employees.length === 0) {\n return { avgTenure: 0, medianTenure: 0 };\n }\n const getYears = (startDate: string) =>\n Math.max(\n 0,\n (new Date('2026-03-01').getTime() - new Date(startDate).getTime()) /\n (1000 * 60 * 60 * 24 * 365)\n );\n const years = employees.map((employee) => getYears(employee.startDate)).sort((a, b) => a - b);\n const avg = years.reduce((sum, value) => sum + value, 0) / years.length;\n const mid = Math.floor(years.length / 2);\n const median = years.length % 2 === 1 ? years[mid] : (years[mid - 1] + years[mid]) / 2;\n return { avgTenure: avg, medianTenure: median };\n }, [employees]);\n\n const avgPerformance = (\n employees.reduce((sum, e) => sum + e.performanceScore, 0) / Math.max(employees.length, 1)\n ).toFixed(1);\n const avgSalary = Math.round(\n employees.reduce((sum, e) => sum + e.salary, 0) / Math.max(employees.length, 1)\n );\n const activeCount = employees.filter((e) => e.status === 'active').length;\n const onLeaveCount = employees.filter((e) => e.status === 'on_leave').length;\n\n const deptPerformance: DeptPerformanceRow[] = departments.map((dept) => {\n const deptEmps = employees.filter((e) => e.department === dept.name);\n const avgScore =\n deptEmps.length > 0\n ? (deptEmps.reduce((s, e) => s + e.performanceScore, 0) / deptEmps.length).toFixed(1)\n : '0';\n const avgSal =\n deptEmps.length > 0\n ? Math.round(deptEmps.reduce((s, e) => s + e.salary, 0) / deptEmps.length)\n : 0;\n return { name: dept.name, headcount: deptEmps.length, avgScore, avgSalary: avgSal };\n });\n\n const sortedDeptPerformance = [...deptPerformance].sort(\n (a, b) => Number(b.avgScore) - Number(a.avgScore)\n );\n\n // Real key-insight figures.\n const topDept = sortedDeptPerformance[0];\n const flightRiskCount = employees.filter((e) => e.performanceScore < 3.5).length;\n const deptsWithStaff = deptPerformance.filter((d) => d.headcount > 0);\n const sortedSalaries = [...deptsWithStaff].map((d) => d.avgSalary).sort((a, b) => a - b);\n const medianDeptSalary =\n sortedSalaries.length > 0 ? sortedSalaries[Math.floor(sortedSalaries.length / 2)] : 0;\n const belowMedianDeptCount = deptsWithStaff.filter((d) => d.avgSalary < medianDeptSalary).length;\n\n const deptPerformanceColumns: ResponsiveTableColumn<DeptPerformanceRow>[] = [\n {\n key: 'name',\n header: 'Department',\n priority: 'primary',\n cell: (dept) => <span className=\"font-medium\">{dept.name}</span>,\n },\n {\n key: 'headcount',\n header: 'Headcount',\n priority: 'secondary',\n cell: (dept) => <span className=\"md:block md:text-right\">{dept.headcount}</span>,\n },\n {\n key: 'avgScore',\n header: 'Avg Score',\n priority: 'primary',\n cell: (dept) => (\n <span className=\"md:block md:text-right\">\n <Badge variant={Number(dept.avgScore) >= 4.5 ? 'default' : 'secondary'}>\n {dept.avgScore}\n </Badge>\n </span>\n ),\n },\n {\n key: 'avgSalary',\n header: 'Avg Salary',\n priority: 'secondary',\n cell: (dept) => (\n <span className=\"md:block md:text-right\">${dept.avgSalary.toLocaleString()}</span>\n ),\n },\n ];\n\n const locationBreakdown = employees.reduce<Record<string, number>>((acc, e) => {\n acc[e.location] = (acc[e.location] || 0) + 1;\n return acc;\n }, {});\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex items-center justify-between\">\n <div>\n <h1 className=\"text-2xl font-semibold\">\n <GradientText>Employee Analytics</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">\n Deep-dive into employee performance, engagement, and demographics\n </p>\n <PagePurpose className=\"mt-1\">\n See how individual employees and teams are performing over time — use these insights for\n reviews, recognition, and targeted coaching.\n </PagePurpose>\n </div>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-4\">\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\">Active Employees</CardTitle>\n <UserCheck className=\"h-4 w-4 text-text-secondary\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">{activeCount}</div>\n <p className=\"text-xs text-text-secondary\">{onLeaveCount} currently on leave</p>\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\">Avg Performance</CardTitle>\n <Star className=\"h-4 w-4 text-text-secondary\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">{avgPerformance}/5.0</div>\n <p className=\"text-xs text-text-secondary\">Across {employees.length} employees</p>\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\">Avg Compensation</CardTitle>\n <BarChart3 className=\"h-4 w-4 text-text-secondary\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">${avgSalary.toLocaleString()}</div>\n <p className=\"text-xs text-text-secondary\">Across all departments</p>\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\">Avg Tenure</CardTitle>\n <Clock className=\"h-4 w-4 text-text-secondary\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">{avgTenure.toFixed(1)} yrs</div>\n <p className=\"text-xs text-text-secondary\">Median: {medianTenure.toFixed(1)} years</p>\n </CardContent>\n </GlassCard>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-2\">\n <Card>\n <CardHeader>\n <CardTitle>Department Performance</CardTitle>\n <CardDescription>\n Average performance score and compensation by department\n </CardDescription>\n </CardHeader>\n <CardContent>\n <ResponsiveTable\n columns={deptPerformanceColumns}\n data={sortedDeptPerformance}\n rowKey={(dept) => dept.name}\n />\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>Engagement Drivers</CardTitle>\n <CardDescription>Employee satisfaction by category</CardDescription>\n </CardHeader>\n <CardContent>\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"Engagement surveys coming soon\"\n description=\"Satisfaction and engagement drivers will appear here once the employee survey integration is connected — we don't show estimated scores.\"\n />\n </CardContent>\n </Card>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-2\">\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <Clock className=\"h-4 w-4\" />\n Tenure Distribution\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {tenureDistribution.map((t) => (\n <div key={t.range} className=\"flex items-center gap-3\">\n <span className=\"w-20 text-sm text-text-secondary\">{t.range}</span>\n <div className=\"flex-1 h-4 bg-bg-sunken rounded overflow-hidden\">\n <div\n className=\"h-4 bg-action-primary-bg/70 rounded\"\n style={{ width: `${t.percentage}%` }}\n />\n </div>\n <span className=\"text-sm font-medium w-10 text-right\">{t.count}</span>\n <span className=\"text-xs text-text-secondary w-10 text-right\">{t.percentage}%</span>\n </div>\n ))}\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <MapPin className=\"h-4 w-4\" />\n Location Distribution\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {Object.entries(locationBreakdown)\n .sort(([, a], [, b]) => b - a)\n .map(([loc, count]) => (\n <div key={loc} className=\"flex items-center justify-between py-1\">\n <span className=\"text-sm\">{loc}</span>\n <div className=\"flex items-center gap-2\">\n <span className=\"text-sm font-medium\">{count}</span>\n <Badge variant=\"outline\" className=\"text-[10px]\">\n {Math.round((count / Math.max(employees.length, 1)) * 100)}%\n </Badge>\n </div>\n </div>\n ))}\n </CardContent>\n </Card>\n </div>\n\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <Award className=\"h-4 w-4 text-status-warning-text\" />\n Key Insights\n </CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"grid gap-3 md:grid-cols-3\">\n <div className=\"rounded border p-3\">\n <p className=\"text-sm font-medium\">Highest Performance</p>\n <p className=\"text-xs text-text-secondary mt-1\">\n {topDept\n ? `${topDept.name} leads with an average score of ${topDept.avgScore}`\n : 'No department performance data yet'}\n </p>\n </div>\n <div className=\"rounded border p-3\">\n <p className=\"text-sm font-medium\">Retention Risk</p>\n <p className=\"text-xs text-text-secondary mt-1\">\n {flightRiskCount} employee{flightRiskCount === 1 ? '' : 's'} below a 3.5 performance\n score — a potential flight risk\n </p>\n </div>\n <div className=\"rounded border p-3\">\n <p className=\"text-sm font-medium\">Compensation Gap</p>\n <p className=\"text-xs text-text-secondary mt-1\">\n {belowMedianDeptCount} department{belowMedianDeptCount === 1 ? '' : 's'} below the\n median department salary\n </p>\n </div>\n </div>\n </CardContent>\n </Card>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AnalyticsFinancialAnalyticsPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/AnalyticsFinancialAnalyticsPagePage.tsx"],"sourcesContent":["import {\n Badge,\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n GlassCard,\n GradientText,\n IllustratedEmptyState,\n PagePurpose,\n Pagination,\n ResponsiveTable,\n type ResponsiveTableColumn,\n Separator,\n} from '@burdenoff/fe-libs/ui';\nimport { usePagination } from '@burdenoff/fe-libs/shared/hooks';\nimport { DollarSign, FileText, Receipt, TrendingUp, Wallet } from 'lucide-react';\nimport { useMemo } from 'react';\nimport { useCrewFoundryStore } from '../store/crewfoundryStore';\n\ntype DeptCostRow = {\n name: string;\n salary: number;\n budget: number;\n headcount: number;\n};\n\nexport function AnalyticsFinancialAnalyticsPagePage() {\n const { employees, departments, payrollRecords, invoices, projects } = useCrewFoundryStore();\n\n const monthlyPayrollTrend = useMemo(() => {\n const byPeriod = payrollRecords.reduce<\n Record<string, { gross: number; net: number; deductions: number }>\n >((acc, record) => {\n const month = new Date(record.payDate).toLocaleString('en-US', { month: 'short' });\n const current = acc[month] || { gross: 0, net: 0, deductions: 0 };\n current.gross += record.grossPay;\n current.net += record.netPay;\n current.deductions += record.deductions;\n acc[month] = current;\n return acc;\n }, {});\n return Object.entries(byPeriod)\n .map(([month, value]) => ({ month, ...value }))\n .slice(-6);\n }, [payrollRecords]);\n\n // Real spend lines only (no invented budget coefficients).\n const spendByCategory = useMemo(() => {\n const items = [\n {\n category: 'Gross Payroll',\n amount: payrollRecords.reduce((sum, record) => sum + record.grossPay, 0),\n icon: Wallet,\n },\n {\n category: 'Deductions',\n amount: payrollRecords.reduce((sum, record) => sum + record.deductions, 0),\n icon: Receipt,\n },\n {\n category: 'Project Spend',\n amount: projects.reduce((sum, project) => sum + project.spent, 0),\n icon: TrendingUp,\n },\n {\n category: 'Collected Invoices',\n amount: invoices\n .filter((invoice) => invoice.status === 'paid')\n .reduce((sum, invoice) => sum + invoice.amount, 0),\n icon: FileText,\n },\n ];\n const total = items.reduce((sum, item) => sum + item.amount, 0) || 1;\n return items.map((item) => ({ ...item, percentage: Math.round((item.amount / total) * 100) }));\n }, [invoices, payrollRecords, projects]);\n\n const totalPayroll = employees.reduce((sum, e) => sum + e.salary, 0);\n const monthlyPayroll = Math.round(totalPayroll / 12);\n const totalInvoiced = invoices.reduce((sum, inv) => sum + inv.amount, 0);\n const paidInvoices = invoices\n .filter((i) => i.status === 'paid')\n .reduce((s, i) => s + i.amount, 0);\n const totalProjectBudget = projects.reduce((s, p) => s + p.budget, 0);\n const totalProjectSpent = projects.reduce((s, p) => s + p.spent, 0);\n\n const maxPayroll = Math.max(1, ...monthlyPayrollTrend.map((m) => m.gross));\n const projectUtilization =\n totalProjectBudget > 0 ? Math.round((totalProjectSpent / totalProjectBudget) * 100) : 0;\n\n const deptCost: DeptCostRow[] = departments.map((d) => {\n const deptEmps = employees.filter((e) => e.department === d.name);\n const totalSal = deptEmps.reduce((s, e) => s + e.salary, 0);\n return { name: d.name, salary: totalSal, budget: d.budget, headcount: deptEmps.length };\n });\n const sortedDeptCost = [...deptCost].sort((a, b) => b.salary - a.salary);\n const totalDeptBudget = departments.reduce((s, d) => s + d.budget, 0);\n\n // Outstanding invoices = anything not fully paid, matching the card title.\n const outstandingInvoices = [...invoices]\n .filter((invoice) => invoice.status !== 'paid')\n .sort((a, b) => a.dueDate.localeCompare(b.dueDate));\n const {\n page: invoicePage,\n pageSize: invoicePageSize,\n offset: invoiceOffset,\n setPage: setInvoicePage,\n } = usePagination({ initialPageSize: 6 });\n const invoicePageItems = outstandingInvoices.slice(\n invoiceOffset,\n invoiceOffset + invoicePageSize\n );\n\n const deptCostColumns: ResponsiveTableColumn<DeptCostRow>[] = [\n {\n key: 'name',\n header: 'Department',\n priority: 'primary',\n cell: (d) => <span className=\"font-medium\">{d.name}</span>,\n },\n {\n key: 'headcount',\n header: 'Headcount',\n priority: 'secondary',\n cell: (d) => <span className=\"md:block md:text-right\">{d.headcount}</span>,\n },\n {\n key: 'salary',\n header: 'Total Salary',\n priority: 'secondary',\n cell: (d) => <span className=\"md:block md:text-right\">${(d.salary / 1000).toFixed(0)}K</span>,\n },\n {\n key: 'budget',\n header: 'Dept Budget',\n priority: 'secondary',\n cell: (d) => (\n <span className=\"md:block md:text-right\">${(d.budget / 1000000).toFixed(1)}M</span>\n ),\n },\n {\n key: 'ratio',\n header: 'Salary/Budget %',\n priority: 'primary',\n cell: (d) => {\n const ratio = d.budget > 0 ? Math.round((d.salary / d.budget) * 100) : 0;\n return (\n <span className=\"md:block md:text-right\">\n <Badge variant={ratio > 50 ? 'destructive' : 'default'} className=\"text-[10px]\">\n {ratio}%\n </Badge>\n </span>\n );\n },\n },\n ];\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex items-center justify-between\">\n <div>\n <h1 className=\"text-2xl font-semibold\">\n <GradientText>Financial Analytics</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">\n Workforce financial metrics and cost analysis\n </p>\n <PagePurpose className=\"mt-1\">Understand the money side of your workforce — payroll, project budgets, and spend trends — so you can plan headcount and control costs with confidence.</PagePurpose>\n </div>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-4\">\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\">Monthly Payroll</CardTitle>\n <DollarSign className=\"h-4 w-4 text-text-secondary\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">\n ${(monthlyPayroll / 1000).toFixed(0)}K\n </div>\n <p className=\"text-xs text-text-secondary\">Annual salary ÷ 12 months</p>\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\">Annual Salary Cost</CardTitle>\n <Wallet className=\"h-4 w-4 text-text-secondary\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">\n ${(totalPayroll / 1000000).toFixed(1)}M\n </div>\n <p className=\"text-xs text-text-secondary\">{employees.length} employees</p>\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\">Total Invoiced</CardTitle>\n <FileText className=\"h-4 w-4 text-text-secondary\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">\n ${(totalInvoiced / 1000).toFixed(1)}K\n </div>\n <p className=\"text-xs text-text-secondary\">\n ${(paidInvoices / 1000).toFixed(1)}K collected\n </p>\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\">Project Budget</CardTitle>\n <TrendingUp className=\"h-4 w-4 text-text-secondary\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">\n ${(totalProjectBudget / 1000000).toFixed(1)}M\n </div>\n <p className=\"text-xs text-text-secondary\">{projectUtilization}% utilized</p>\n </CardContent>\n </GlassCard>\n </div>\n\n <Card>\n <CardHeader>\n <CardTitle>Payroll Trend</CardTitle>\n <CardDescription>Monthly gross payroll over the last 6 months</CardDescription>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {monthlyPayrollTrend.map((m) => (\n <div key={m.month} className=\"flex items-center gap-3\">\n <span className=\"w-10 text-sm text-text-secondary\">{m.month}</span>\n <div className=\"flex-1 h-5 bg-bg-sunken rounded overflow-hidden flex\">\n <div\n className=\"h-5 bg-action-primary-bg\"\n style={{ width: `${Math.round((m.net / maxPayroll) * 100)}%` }}\n />\n <div\n className=\"h-5 bg-action-primary-bg/30\"\n style={{ width: `${Math.round((m.deductions / maxPayroll) * 100)}%` }}\n />\n </div>\n <span className=\"text-sm font-medium w-20 text-right\">\n ${(m.gross / 1000000).toFixed(2)}M\n </span>\n </div>\n ))}\n <div className=\"flex items-center gap-4 mt-2 text-xs text-text-secondary\">\n <div className=\"flex items-center gap-1\">\n <div className=\"h-3 w-3 bg-action-primary-bg rounded\" />\n <span>Net Pay</span>\n </div>\n <div className=\"flex items-center gap-1\">\n <div className=\"h-3 w-3 bg-action-primary-bg/30 rounded\" />\n <span>Deductions</span>\n </div>\n </div>\n </CardContent>\n </Card>\n\n <div className=\"grid gap-4 md:grid-cols-2\">\n <Card>\n <CardHeader>\n <CardTitle>Spend by Category</CardTitle>\n <CardDescription>Share of tracked workforce spend</CardDescription>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n {spendByCategory.map((cat) => (\n <div key={cat.category} className=\"space-y-1\">\n <div className=\"flex justify-between text-sm\">\n <span className=\"flex items-center gap-2 font-medium\">\n <cat.icon className=\"h-4 w-4 text-text-secondary\" />\n {cat.category}\n </span>\n <span>${(cat.amount / 1000).toFixed(0)}K</span>\n </div>\n <div className=\"h-3 bg-bg-sunken rounded overflow-hidden\">\n <div\n className=\"h-3 rounded bg-action-primary-bg\"\n style={{ width: `${cat.percentage}%` }}\n />\n </div>\n <p className=\"text-xs text-text-secondary text-right\">{cat.percentage}% of spend</p>\n </div>\n ))}\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>Outstanding Invoices</CardTitle>\n <CardDescription>Unpaid invoices requiring attention</CardDescription>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {outstandingInvoices.length === 0 && (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"No outstanding invoices\"\n description=\"Every invoice has been paid. New unpaid invoices will appear here.\"\n />\n )}\n {invoicePageItems.map((inv) => (\n <div key={inv.id} className=\"flex items-center justify-between rounded border p-3\">\n <div>\n <p className=\"text-sm font-medium\">{inv.number}</p>\n <p className=\"text-xs text-text-secondary\">{inv.clientName}</p>\n </div>\n <div className=\"text-right\">\n <p className=\"text-sm font-medium\">${inv.amount.toLocaleString()}</p>\n <Badge\n variant={inv.status === 'overdue' ? 'destructive' : 'secondary'}\n className=\"text-[10px]\"\n >\n {inv.status}\n </Badge>\n </div>\n </div>\n ))}\n {outstandingInvoices.length > invoicePageSize && (\n <Pagination\n page={invoicePage}\n pageSize={invoicePageSize}\n totalItems={outstandingInvoices.length}\n onPageChange={setInvoicePage}\n />\n )}\n </CardContent>\n </Card>\n </div>\n\n <Card>\n <CardHeader>\n <CardTitle>Department Cost Analysis</CardTitle>\n <CardDescription>Total salary cost and budget allocation by department</CardDescription>\n </CardHeader>\n <CardContent>\n <ResponsiveTable\n columns={deptCostColumns}\n data={sortedDeptCost}\n rowKey={(d) => d.name}\n />\n <Separator className=\"my-3\" />\n <div className=\"flex flex-wrap items-center justify-between gap-2 text-sm font-bold\">\n <span>Total</span>\n <span>{employees.length} employees</span>\n <span>${(totalPayroll / 1000000).toFixed(1)}M salary</span>\n <span>${(totalDeptBudget / 1000000).toFixed(1)}M budget</span>\n </div>\n </CardContent>\n </Card>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AnalyticsFinancialAnalyticsPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/AnalyticsFinancialAnalyticsPagePage.tsx"],"sourcesContent":["import {\n Badge,\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n GlassCard,\n GradientText,\n IllustratedEmptyState,\n PagePurpose,\n Pagination,\n ResponsiveTable,\n type ResponsiveTableColumn,\n Separator,\n} from '@burdenoff/fe-libs/ui';\nimport { usePagination } from '@burdenoff/fe-libs/shared/hooks';\nimport { DollarSign, FileText, Receipt, TrendingUp, Wallet } from 'lucide-react';\nimport { useMemo } from 'react';\nimport { useCrewFoundryStore } from '../store/crewfoundryStore';\n\ntype DeptCostRow = {\n name: string;\n salary: number;\n budget: number;\n headcount: number;\n};\n\nexport function AnalyticsFinancialAnalyticsPagePage() {\n const { employees, departments, payrollRecords, invoices, projects } = useCrewFoundryStore();\n\n const monthlyPayrollTrend = useMemo(() => {\n const byPeriod = payrollRecords.reduce<\n Record<string, { gross: number; net: number; deductions: number }>\n >((acc, record) => {\n const month = new Date(record.payDate).toLocaleString('en-US', { month: 'short' });\n const current = acc[month] || { gross: 0, net: 0, deductions: 0 };\n current.gross += record.grossPay;\n current.net += record.netPay;\n current.deductions += record.deductions;\n acc[month] = current;\n return acc;\n }, {});\n return Object.entries(byPeriod)\n .map(([month, value]) => ({ month, ...value }))\n .slice(-6);\n }, [payrollRecords]);\n\n // Real spend lines only (no invented budget coefficients).\n const spendByCategory = useMemo(() => {\n const items = [\n {\n category: 'Gross Payroll',\n amount: payrollRecords.reduce((sum, record) => sum + record.grossPay, 0),\n icon: Wallet,\n },\n {\n category: 'Deductions',\n amount: payrollRecords.reduce((sum, record) => sum + record.deductions, 0),\n icon: Receipt,\n },\n {\n category: 'Project Spend',\n amount: projects.reduce((sum, project) => sum + project.spent, 0),\n icon: TrendingUp,\n },\n {\n category: 'Collected Invoices',\n amount: invoices\n .filter((invoice) => invoice.status === 'paid')\n .reduce((sum, invoice) => sum + invoice.amount, 0),\n icon: FileText,\n },\n ];\n const total = items.reduce((sum, item) => sum + item.amount, 0) || 1;\n return items.map((item) => ({ ...item, percentage: Math.round((item.amount / total) * 100) }));\n }, [invoices, payrollRecords, projects]);\n\n const totalPayroll = employees.reduce((sum, e) => sum + e.salary, 0);\n const monthlyPayroll = Math.round(totalPayroll / 12);\n const totalInvoiced = invoices.reduce((sum, inv) => sum + inv.amount, 0);\n const paidInvoices = invoices\n .filter((i) => i.status === 'paid')\n .reduce((s, i) => s + i.amount, 0);\n const totalProjectBudget = projects.reduce((s, p) => s + p.budget, 0);\n const totalProjectSpent = projects.reduce((s, p) => s + p.spent, 0);\n\n const maxPayroll = Math.max(1, ...monthlyPayrollTrend.map((m) => m.gross));\n const projectUtilization =\n totalProjectBudget > 0 ? Math.round((totalProjectSpent / totalProjectBudget) * 100) : 0;\n\n const deptCost: DeptCostRow[] = departments.map((d) => {\n const deptEmps = employees.filter((e) => e.department === d.name);\n const totalSal = deptEmps.reduce((s, e) => s + e.salary, 0);\n return { name: d.name, salary: totalSal, budget: d.budget, headcount: deptEmps.length };\n });\n const sortedDeptCost = [...deptCost].sort((a, b) => b.salary - a.salary);\n const totalDeptBudget = departments.reduce((s, d) => s + d.budget, 0);\n\n // Outstanding invoices = anything not fully paid, matching the card title.\n const outstandingInvoices = [...invoices]\n .filter((invoice) => invoice.status !== 'paid')\n .sort((a, b) => a.dueDate.localeCompare(b.dueDate));\n const {\n page: invoicePage,\n pageSize: invoicePageSize,\n offset: invoiceOffset,\n setPage: setInvoicePage,\n } = usePagination({ initialPageSize: 6 });\n const invoicePageItems = outstandingInvoices.slice(\n invoiceOffset,\n invoiceOffset + invoicePageSize\n );\n\n const deptCostColumns: ResponsiveTableColumn<DeptCostRow>[] = [\n {\n key: 'name',\n header: 'Department',\n priority: 'primary',\n cell: (d) => <span className=\"font-medium\">{d.name}</span>,\n },\n {\n key: 'headcount',\n header: 'Headcount',\n priority: 'secondary',\n cell: (d) => <span className=\"md:block md:text-right\">{d.headcount}</span>,\n },\n {\n key: 'salary',\n header: 'Total Salary',\n priority: 'secondary',\n cell: (d) => <span className=\"md:block md:text-right\">${(d.salary / 1000).toFixed(0)}K</span>,\n },\n {\n key: 'budget',\n header: 'Dept Budget',\n priority: 'secondary',\n cell: (d) => (\n <span className=\"md:block md:text-right\">${(d.budget / 1000000).toFixed(1)}M</span>\n ),\n },\n {\n key: 'ratio',\n header: 'Salary/Budget %',\n priority: 'primary',\n cell: (d) => {\n const ratio = d.budget > 0 ? Math.round((d.salary / d.budget) * 100) : 0;\n return (\n <span className=\"md:block md:text-right\">\n <Badge variant={ratio > 50 ? 'destructive' : 'default'} className=\"text-[10px]\">\n {ratio}%\n </Badge>\n </span>\n );\n },\n },\n ];\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex items-center justify-between\">\n <div>\n <h1 className=\"text-2xl font-semibold\">\n <GradientText>Financial Analytics</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">\n Workforce financial metrics and cost analysis\n </p>\n <PagePurpose className=\"mt-1\">\n Understand the money side of your workforce — payroll, project budgets, and spend trends\n — so you can plan headcount and control costs with confidence.\n </PagePurpose>\n </div>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-4\">\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\">Monthly Payroll</CardTitle>\n <DollarSign className=\"h-4 w-4 text-text-secondary\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">\n ${(monthlyPayroll / 1000).toFixed(0)}K\n </div>\n <p className=\"text-xs text-text-secondary\">Annual salary ÷ 12 months</p>\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\">Annual Salary Cost</CardTitle>\n <Wallet className=\"h-4 w-4 text-text-secondary\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">\n ${(totalPayroll / 1000000).toFixed(1)}M\n </div>\n <p className=\"text-xs text-text-secondary\">{employees.length} employees</p>\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\">Total Invoiced</CardTitle>\n <FileText className=\"h-4 w-4 text-text-secondary\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">\n ${(totalInvoiced / 1000).toFixed(1)}K\n </div>\n <p className=\"text-xs text-text-secondary\">\n ${(paidInvoices / 1000).toFixed(1)}K collected\n </p>\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\">Project Budget</CardTitle>\n <TrendingUp className=\"h-4 w-4 text-text-secondary\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">\n ${(totalProjectBudget / 1000000).toFixed(1)}M\n </div>\n <p className=\"text-xs text-text-secondary\">{projectUtilization}% utilized</p>\n </CardContent>\n </GlassCard>\n </div>\n\n <Card>\n <CardHeader>\n <CardTitle>Payroll Trend</CardTitle>\n <CardDescription>Monthly gross payroll over the last 6 months</CardDescription>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {monthlyPayrollTrend.map((m) => (\n <div key={m.month} className=\"flex items-center gap-3\">\n <span className=\"w-10 text-sm text-text-secondary\">{m.month}</span>\n <div className=\"flex-1 h-5 bg-bg-sunken rounded overflow-hidden flex\">\n <div\n className=\"h-5 bg-action-primary-bg\"\n style={{ width: `${Math.round((m.net / maxPayroll) * 100)}%` }}\n />\n <div\n className=\"h-5 bg-action-primary-bg/30\"\n style={{ width: `${Math.round((m.deductions / maxPayroll) * 100)}%` }}\n />\n </div>\n <span className=\"text-sm font-medium w-20 text-right\">\n ${(m.gross / 1000000).toFixed(2)}M\n </span>\n </div>\n ))}\n <div className=\"flex items-center gap-4 mt-2 text-xs text-text-secondary\">\n <div className=\"flex items-center gap-1\">\n <div className=\"h-3 w-3 bg-action-primary-bg rounded\" />\n <span>Net Pay</span>\n </div>\n <div className=\"flex items-center gap-1\">\n <div className=\"h-3 w-3 bg-action-primary-bg/30 rounded\" />\n <span>Deductions</span>\n </div>\n </div>\n </CardContent>\n </Card>\n\n <div className=\"grid gap-4 md:grid-cols-2\">\n <Card>\n <CardHeader>\n <CardTitle>Spend by Category</CardTitle>\n <CardDescription>Share of tracked workforce spend</CardDescription>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n {spendByCategory.map((cat) => (\n <div key={cat.category} className=\"space-y-1\">\n <div className=\"flex justify-between text-sm\">\n <span className=\"flex items-center gap-2 font-medium\">\n <cat.icon className=\"h-4 w-4 text-text-secondary\" />\n {cat.category}\n </span>\n <span>${(cat.amount / 1000).toFixed(0)}K</span>\n </div>\n <div className=\"h-3 bg-bg-sunken rounded overflow-hidden\">\n <div\n className=\"h-3 rounded bg-action-primary-bg\"\n style={{ width: `${cat.percentage}%` }}\n />\n </div>\n <p className=\"text-xs text-text-secondary text-right\">{cat.percentage}% of spend</p>\n </div>\n ))}\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>Outstanding Invoices</CardTitle>\n <CardDescription>Unpaid invoices requiring attention</CardDescription>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {outstandingInvoices.length === 0 && (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"No outstanding invoices\"\n description=\"Every invoice has been paid. New unpaid invoices will appear here.\"\n />\n )}\n {invoicePageItems.map((inv) => (\n <div key={inv.id} className=\"flex items-center justify-between rounded border p-3\">\n <div>\n <p className=\"text-sm font-medium\">{inv.number}</p>\n <p className=\"text-xs text-text-secondary\">{inv.clientName}</p>\n </div>\n <div className=\"text-right\">\n <p className=\"text-sm font-medium\">${inv.amount.toLocaleString()}</p>\n <Badge\n variant={inv.status === 'overdue' ? 'destructive' : 'secondary'}\n className=\"text-[10px]\"\n >\n {inv.status}\n </Badge>\n </div>\n </div>\n ))}\n {outstandingInvoices.length > invoicePageSize && (\n <Pagination\n page={invoicePage}\n pageSize={invoicePageSize}\n totalItems={outstandingInvoices.length}\n onPageChange={setInvoicePage}\n />\n )}\n </CardContent>\n </Card>\n </div>\n\n <Card>\n <CardHeader>\n <CardTitle>Department Cost Analysis</CardTitle>\n <CardDescription>Total salary cost and budget allocation by department</CardDescription>\n </CardHeader>\n <CardContent>\n <ResponsiveTable columns={deptCostColumns} data={sortedDeptCost} rowKey={(d) => d.name} />\n <Separator className=\"my-3\" />\n <div className=\"flex flex-wrap items-center justify-between gap-2 text-sm font-bold\">\n <span>Total</span>\n <span>{employees.length} employees</span>\n <span>${(totalPayroll / 1000000).toFixed(1)}M salary</span>\n <span>${(totalDeptBudget / 1000000).toFixed(1)}M budget</span>\n </div>\n </CardContent>\n </Card>\n </div>\n );\n}\n"],"mappings":";;;;;;;AA4BA,SAAgB,IAAsC;CACpD,IAAM,EAAE,cAAW,gBAAa,mBAAgB,aAAU,gBAAa,GAAqB,EAEtF,IAAsB,QAAc;EACxC,IAAM,IAAW,EAAe,QAE7B,GAAK,MAAW;GACjB,IAAM,IAAQ,IAAI,KAAK,EAAO,QAAQ,CAAC,eAAe,SAAS,EAAE,OAAO,SAAS,CAAC,EAC5E,IAAU,EAAI,MAAU;IAAE,OAAO;IAAG,KAAK;IAAG,YAAY;IAAG;AAKjE,UAJA,EAAQ,SAAS,EAAO,UACxB,EAAQ,OAAO,EAAO,QACtB,EAAQ,cAAc,EAAO,YAC7B,EAAI,KAAS,GACN;KACN,EAAE,CAAC;AACN,SAAO,OAAO,QAAQ,EAAS,CAC5B,KAAK,CAAC,GAAO,QAAY;GAAE;GAAO,GAAG;GAAO,EAAE,CAC9C,MAAM,GAAG;IACX,CAAC,EAAe,CAAC,EAGd,IAAkB,QAAc;EACpC,IAAM,IAAQ;GACZ;IACE,UAAU;IACV,QAAQ,EAAe,QAAQ,GAAK,MAAW,IAAM,EAAO,UAAU,EAAE;IACxE,MAAM;IACP;GACD;IACE,UAAU;IACV,QAAQ,EAAe,QAAQ,GAAK,MAAW,IAAM,EAAO,YAAY,EAAE;IAC1E,MAAM;IACP;GACD;IACE,UAAU;IACV,QAAQ,EAAS,QAAQ,GAAK,MAAY,IAAM,EAAQ,OAAO,EAAE;IACjE,MAAM;IACP;GACD;IACE,UAAU;IACV,QAAQ,EACL,QAAQ,MAAY,EAAQ,WAAW,OAAO,CAC9C,QAAQ,GAAK,MAAY,IAAM,EAAQ,QAAQ,EAAE;IACpD,MAAM;IACP;GACF,EACK,IAAQ,EAAM,QAAQ,GAAK,MAAS,IAAM,EAAK,QAAQ,EAAE,IAAI;AACnE,SAAO,EAAM,KAAK,OAAU;GAAE,GAAG;GAAM,YAAY,KAAK,MAAO,EAAK,SAAS,IAAS,IAAI;GAAE,EAAE;IAC7F;EAAC;EAAU;EAAgB;EAAS,CAAC,EAElC,IAAe,EAAU,QAAQ,GAAK,MAAM,IAAM,EAAE,QAAQ,EAAE,EAC9D,IAAiB,KAAK,MAAM,IAAe,GAAG,EAC9C,IAAgB,EAAS,QAAQ,GAAK,MAAQ,IAAM,EAAI,QAAQ,EAAE,EAClE,IAAe,EAClB,QAAQ,MAAM,EAAE,WAAW,OAAO,CAClC,QAAQ,GAAG,MAAM,IAAI,EAAE,QAAQ,EAAE,EAC9B,IAAqB,EAAS,QAAQ,GAAG,MAAM,IAAI,EAAE,QAAQ,EAAE,EAC/D,IAAoB,EAAS,QAAQ,GAAG,MAAM,IAAI,EAAE,OAAO,EAAE,EAE7D,IAAa,KAAK,IAAI,GAAG,GAAG,EAAoB,KAAK,MAAM,EAAE,MAAM,CAAC,EACpE,IACJ,IAAqB,IAAI,KAAK,MAAO,IAAoB,IAAsB,IAAI,GAAG,GAOlF,IAAiB,CAAC,GALQ,EAAY,KAAK,MAAM;EACrD,IAAM,IAAW,EAAU,QAAQ,MAAM,EAAE,eAAe,EAAE,KAAK,EAC3D,IAAW,EAAS,QAAQ,GAAG,MAAM,IAAI,EAAE,QAAQ,EAAE;AAC3D,SAAO;GAAE,MAAM,EAAE;GAAM,QAAQ;GAAU,QAAQ,EAAE;GAAQ,WAAW,EAAS;GAAQ;GACvF,CACkC,CAAC,MAAM,GAAG,MAAM,EAAE,SAAS,EAAE,OAAO,EAClE,IAAkB,EAAY,QAAQ,GAAG,MAAM,IAAI,EAAE,QAAQ,EAAE,EAG/D,IAAsB,CAAC,GAAG,EAAS,CACtC,QAAQ,MAAY,EAAQ,WAAW,OAAO,CAC9C,MAAM,GAAG,MAAM,EAAE,QAAQ,cAAc,EAAE,QAAQ,CAAC,EAC/C,EACJ,MAAM,GACN,UAAU,GACV,QAAQ,GACR,SAAS,MACP,EAAc,EAAE,iBAAiB,GAAG,CAAC,EACnC,IAAmB,EAAoB,MAC3C,GACA,IAAgB,EACjB;AA8CD,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,OAAD,EAAA,UAAA;KACE,kBAAC,MAAD;MAAI,WAAU;gBACZ,kBAAC,GAAD,EAAA,UAAc,uBAAkC,CAAA;MAC7C,CAAA;KACL,kBAAC,KAAD;MAAG,WAAU;gBAA8B;MAEvC,CAAA;KACJ,kBAAC,GAAD;MAAa,WAAU;gBAAO;MAGhB,CAAA;KACV,EAAA,CAAA;IACF,CAAA;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;QAA2B,CAAA,EACtE,kBAAC,GAAD,EAAY,WAAU,+BAAgC,CAAA,CAC3C;UACb,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf;QAAiD;SAC5C,IAAiB,KAAM,QAAQ,EAAE;QAAC;QACjC;UACN,kBAAC,KAAD;OAAG,WAAU;iBAA8B;OAA6B,CAAA,CAC5D,EAAA,CAAA,CACJ;;KACZ,kBAAC,GAAD;MAAW,WAAU;MAAS,WAAU;gBAAxC,CACE,kBAAC,GAAD;OAAY,WAAU;iBAAtB,CACE,kBAAC,GAAD;QAAW,WAAU;kBAAsB;QAA8B,CAAA,EACzE,kBAAC,GAAD,EAAQ,WAAU,+BAAgC,CAAA,CACvC;UACb,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf;QAAiD;SAC5C,IAAe,KAAS,QAAQ,EAAE;QAAC;QAClC;UACN,kBAAC,KAAD;OAAG,WAAU;iBAAb,CAA4C,EAAU,QAAO,aAAc;SAC/D,EAAA,CAAA,CACJ;;KACZ,kBAAC,GAAD;MAAW,WAAU;MAAS,WAAU;gBAAxC,CACE,kBAAC,GAAD;OAAY,WAAU;iBAAtB,CACE,kBAAC,GAAD;QAAW,WAAU;kBAAsB;QAA0B,CAAA,EACrE,kBAAC,GAAD,EAAU,WAAU,+BAAgC,CAAA,CACzC;UACb,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf;QAAiD;SAC5C,IAAgB,KAAM,QAAQ,EAAE;QAAC;QAChC;UACN,kBAAC,KAAD;OAAG,WAAU;iBAAb;QAA2C;SACtC,IAAe,KAAM,QAAQ,EAAE;QAAC;QACjC;SACQ,EAAA,CAAA,CACJ;;KACZ,kBAAC,GAAD;MAAW,WAAU;MAAS,WAAU;gBAAxC,CACE,kBAAC,GAAD;OAAY,WAAU;iBAAtB,CACE,kBAAC,GAAD;QAAW,WAAU;kBAAsB;QAA0B,CAAA,EACrE,kBAAC,GAAD,EAAY,WAAU,+BAAgC,CAAA,CAC3C;UACb,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf;QAAiD;SAC5C,IAAqB,KAAS,QAAQ,EAAE;QAAC;QACxC;UACN,kBAAC,KAAD;OAAG,WAAU;iBAAb,CAA4C,GAAmB,aAAc;SACjE,EAAA,CAAA,CACJ;;KACR;;GAEN,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UAAW,iBAAyB,CAAA,EACpC,kBAAC,GAAD,EAAA,UAAiB,gDAA8D,CAAA,CACpE,EAAA,CAAA,EACb,kBAAC,GAAD;IAAa,WAAU;cAAvB,CACG,EAAoB,KAAK,MACxB,kBAAC,OAAD;KAAmB,WAAU;eAA7B;MACE,kBAAC,QAAD;OAAM,WAAU;iBAAoC,EAAE;OAAa,CAAA;MACnE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,OAAD;QACE,WAAU;QACV,OAAO,EAAE,OAAO,GAAG,KAAK,MAAO,EAAE,MAAM,IAAc,IAAI,CAAC,IAAI;QAC9D,CAAA,EACF,kBAAC,OAAD;QACE,WAAU;QACV,OAAO,EAAE,OAAO,GAAG,KAAK,MAAO,EAAE,aAAa,IAAc,IAAI,CAAC,IAAI;QACrE,CAAA,CACE;;MACN,kBAAC,QAAD;OAAM,WAAU;iBAAhB;QAAsD;SACjD,EAAE,QAAQ,KAAS,QAAQ,EAAE;QAAC;QAC5B;;MACH;OAfI,EAAE,MAeN,CACN,EACF,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,OAAD,EAAK,WAAU,wCAAyC,CAAA,EACxD,kBAAC,QAAD,EAAA,UAAM,WAAc,CAAA,CAChB;SACN,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,OAAD,EAAK,WAAU,2CAA4C,CAAA,EAC3D,kBAAC,QAAD,EAAA,UAAM,cAAiB,CAAA,CACnB;QACF;OACM;MACT,EAAA,CAAA;GAEP,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UAAW,qBAA6B,CAAA,EACxC,kBAAC,GAAD,EAAA,UAAiB,oCAAkD,CAAA,CACxD,EAAA,CAAA,EACb,kBAAC,GAAD;KAAa,WAAU;eACpB,EAAgB,KAAK,MACpB,kBAAC,OAAD;MAAwB,WAAU;gBAAlC;OACE,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,QAAD;SAAM,WAAU;mBAAhB,CACE,kBAAC,EAAI,MAAL,EAAU,WAAU,+BAAgC,CAAA,EACnD,EAAI,SACA;YACP,kBAAC,QAAD,EAAA,UAAA;SAAM;UAAG,EAAI,SAAS,KAAM,QAAQ,EAAE;SAAC;SAAQ,EAAA,CAAA,CAC3C;;OACN,kBAAC,OAAD;QAAK,WAAU;kBACb,kBAAC,OAAD;SACE,WAAU;SACV,OAAO,EAAE,OAAO,GAAG,EAAI,WAAW,IAAI;SACtC,CAAA;QACE,CAAA;OACN,kBAAC,KAAD;QAAG,WAAU;kBAAb,CAAuD,EAAI,YAAW,aAAc;;OAChF;QAfI,EAAI,SAeR,CACN;KACU,CAAA,CACT,EAAA,CAAA,EAEP,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UAAW,wBAAgC,CAAA,EAC3C,kBAAC,GAAD,EAAA,UAAiB,uCAAqD,CAAA,CAC3D,EAAA,CAAA,EACb,kBAAC,GAAD;KAAa,WAAU;eAAvB;MACG,EAAoB,WAAW,KAC9B,kBAAC,GAAD;OACE,cAAa;OACb,OAAM;OACN,aAAY;OACZ,CAAA;MAEH,EAAiB,KAAK,MACrB,kBAAC,OAAD;OAAkB,WAAU;iBAA5B,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;QAAG,WAAU;kBAAuB,EAAI;QAAW,CAAA,EACnD,kBAAC,KAAD;QAAG,WAAU;kBAA+B,EAAI;QAAe,CAAA,CAC3D,EAAA,CAAA,EACN,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,KAAD;SAAG,WAAU;mBAAb,CAAmC,KAAE,EAAI,OAAO,gBAAgB,CAAK;YACrE,kBAAC,GAAD;SACE,SAAS,EAAI,WAAW,YAAY,gBAAgB;SACpD,WAAU;mBAET,EAAI;SACC,CAAA,CACJ;UACF;SAdI,EAAI,GAcR,CACN;MACD,EAAoB,SAAS,KAC5B,kBAAC,GAAD;OACE,MAAM;OACN,UAAU;OACV,YAAY,EAAoB;OAChC,cAAc;OACd,CAAA;MAEQ;OACT,EAAA,CAAA,CACH;;GAEN,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UAAW,4BAAoC,CAAA,EAC/C,kBAAC,GAAD,EAAA,UAAiB,yDAAuE,CAAA,CAC7E,EAAA,CAAA,EACb,kBAAC,GAAD,EAAA,UAAA;IACE,kBAAC,GAAD;KAAiB,SAnOqC;MAC5D;OACE,KAAK;OACL,QAAQ;OACR,UAAU;OACV,OAAO,MAAM,kBAAC,QAAD;QAAM,WAAU;kBAAe,EAAE;QAAY,CAAA;OAC3D;MACD;OACE,KAAK;OACL,QAAQ;OACR,UAAU;OACV,OAAO,MAAM,kBAAC,QAAD;QAAM,WAAU;kBAA0B,EAAE;QAAiB,CAAA;OAC3E;MACD;OACE,KAAK;OACL,QAAQ;OACR,UAAU;OACV,OAAO,MAAM,kBAAC,QAAD;QAAM,WAAU;kBAAhB;SAAyC;UAAG,EAAE,SAAS,KAAM,QAAQ,EAAE;SAAC;SAAQ;;OAC9F;MACD;OACE,KAAK;OACL,QAAQ;OACR,UAAU;OACV,OAAO,MACL,kBAAC,QAAD;QAAM,WAAU;kBAAhB;SAAyC;UAAG,EAAE,SAAS,KAAS,QAAQ,EAAE;SAAC;SAAQ;;OAEtF;MACD;OACE,KAAK;OACL,QAAQ;OACR,UAAU;OACV,OAAO,MAAM;QACX,IAAM,IAAQ,EAAE,SAAS,IAAI,KAAK,MAAO,EAAE,SAAS,EAAE,SAAU,IAAI,GAAG;AACvE,eACE,kBAAC,QAAD;SAAM,WAAU;mBACd,kBAAC,GAAD;UAAO,SAAS,IAAQ,KAAK,gBAAgB;UAAW,WAAU;oBAAlE,CACG,GAAM,IACD;;SACH,CAAA;;OAGZ;MACF;KAyLkD,MAAM;KAAgB,SAAS,MAAM,EAAE;KAAQ,CAAA;IAC1F,kBAAC,GAAD,EAAW,WAAU,QAAS,CAAA;IAC9B,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,QAAD,EAAA,UAAM,SAAY,CAAA;MAClB,kBAAC,QAAD,EAAA,UAAA,CAAO,EAAU,QAAO,aAAiB,EAAA,CAAA;MACzC,kBAAC,QAAD,EAAA,UAAA;OAAM;QAAG,IAAe,KAAS,QAAQ,EAAE;OAAC;OAAe,EAAA,CAAA;MAC3D,kBAAC,QAAD,EAAA,UAAA;OAAM;QAAG,IAAkB,KAAS,QAAQ,EAAE;OAAC;OAAe,EAAA,CAAA;MAC1D;;IACM,EAAA,CAAA,CACT,EAAA,CAAA;GACH"}
|