@burdenoff/microfe-crewfoundry 2026.731.1 → 2026.801.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/crewfoundry/CrewFoundryRoutes.js +1 -1
- package/dist/crewfoundry/CrewFoundryRoutes.js.map +1 -1
- package/dist/crewfoundry/pages/CommunicationsAnnouncementsPagePage.js +1 -1
- package/dist/crewfoundry/pages/CommunicationsAnnouncementsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/CommunicationsFilesPagePage.js +1 -1
- package/dist/crewfoundry/pages/CommunicationsFilesPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/CommunicationsTeamsPagePage.js +1 -1
- package/dist/crewfoundry/pages/CommunicationsTeamsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/CompanyClientManagementPagePage.js +1 -1
- package/dist/crewfoundry/pages/CompanyClientManagementPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/CompanyVendorManagementPagePage.js +1 -1
- package/dist/crewfoundry/pages/CompanyVendorManagementPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/DashboardDashboardPagePage.js +1 -1
- package/dist/crewfoundry/pages/DashboardDashboardPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/DashboardEnhancedDashboardPagePage.js +1 -1
- package/dist/crewfoundry/pages/DashboardEnhancedDashboardPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HiringApplicationsPagePage.js +1 -1
- package/dist/crewfoundry/pages/HiringApplicationsPagePage.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/HiringJobDetailsPagePage.js +1 -1
- package/dist/crewfoundry/pages/HiringJobDetailsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HiringJobsPagePage.js +1 -1
- package/dist/crewfoundry/pages/HiringJobsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HrEmployeeProfilePagePage.js +1 -1
- package/dist/crewfoundry/pages/HrEmployeeProfilePagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HrEmployeesPagePage.js +1 -1
- package/dist/crewfoundry/pages/HrEmployeesPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/LearningAssessmentsPagePage.js +1 -1
- package/dist/crewfoundry/pages/LearningAssessmentsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/LearningCertificatesPagePage.js +1 -1
- package/dist/crewfoundry/pages/LearningCertificatesPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/LearningCourseDetailsPagePage.js +1 -1
- package/dist/crewfoundry/pages/LearningCourseDetailsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/LearningCourseLearningPagePage.js +1 -1
- package/dist/crewfoundry/pages/LearningCourseLearningPagePage.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 +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 +1 -1
- package/dist/crewfoundry/pages/LearningMyLearningPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/MarketplaceFreelancerProfilePageEnhancedPage.js +1 -1
- package/dist/crewfoundry/pages/MarketplaceFreelancerProfilePageEnhancedPage.js.map +1 -1
- package/dist/crewfoundry/pages/MarketplaceFreelancerProfilePagePage.js +1 -1
- package/dist/crewfoundry/pages/MarketplaceFreelancerProfilePagePage.js.map +1 -1
- package/dist/crewfoundry/pages/MarketplaceMarketplacePagePage.js +1 -1
- package/dist/crewfoundry/pages/MarketplaceMarketplacePagePage.js.map +1 -1
- package/dist/crewfoundry/pages/MarketplaceMarketplaceServicesPagePage.js +1 -1
- package/dist/crewfoundry/pages/MarketplaceMarketplaceServicesPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/MarketplaceMyServicesPagePage.js +1 -1
- package/dist/crewfoundry/pages/MarketplaceMyServicesPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/MarketplaceProjectDetailsPagePage.js +1 -1
- package/dist/crewfoundry/pages/MarketplaceProjectDetailsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/MarketplaceServiceDetailsPagePage.js +1 -1
- package/dist/crewfoundry/pages/MarketplaceServiceDetailsPagePage.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 +1 -1
- package/dist/crewfoundry/pages/PaymentsPaymentMethodsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/ProjectsCreateProjectPagePage.js +1 -1
- package/dist/crewfoundry/pages/ProjectsCreateProjectPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/ProjectsProjectDetailsPagePage.js +1 -1
- package/dist/crewfoundry/pages/ProjectsProjectDetailsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/ProjectsProjectsPagePage.js +1 -1
- package/dist/crewfoundry/pages/ProjectsProjectsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/ProjectsResourcingPagePage.js +1 -1
- package/dist/crewfoundry/pages/ProjectsResourcingPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/TeamsTeamDetailPagePage.js +1 -1
- package/dist/crewfoundry/pages/TeamsTeamDetailPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/TeamsTeamDirectoryPagePage.js +1 -1
- package/dist/crewfoundry/pages/TeamsTeamDirectoryPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/TeamsTeamSettingsPagePage.js +1 -1
- package/dist/crewfoundry/pages/TeamsTeamSettingsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/TeamsTeamsPagePage.js +1 -1
- package/dist/crewfoundry/pages/TeamsTeamsPagePage.js.map +1 -1
- package/dist/crewfoundry/providers/CrewFoundryProvider.js +1 -1
- package/dist/crewfoundry/providers/CrewFoundryProvider.js.map +1 -1
- package/package.json +6 -6
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CompanyVendorManagementPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/CompanyVendorManagementPagePage.tsx"],"sourcesContent":["import { useState } from 'react';\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n Badge,\n Button,\n CardContent,\n CardHeader,\n CardTitle,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n GlassCard,\n GradientText,\n IllustratedEmptyState,\n Input,\n PagePurpose,\n Pagination,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@burdenoff/fe-libs/ui';\nimport { usePagination } from '@burdenoff/fe-libs/shared/hooks';\nimport { Mail, MoreHorizontal, Pencil, Plus, Search, Star, Trash2 } from 'lucide-react';\nimport { useNavigate } from 'react-router-dom';\nimport {\n FormModal,\n useFormModal,\n type FieldConfig,\n type FormValues,\n} from '../components/forms/FormModal';\nimport { useCrewFoundryStore, type Vendor } from '../store/crewfoundryStore';\nimport { formatEnumLabel } from '../utils/formatEnumLabel';\nimport { AvatarStack, SegmentedTabs, StatCard } from '../components/viz';\nimport { totalContractValue, thisQuarterRevenue } from '../lib/companyFinance';\n\nconst VENDOR_FIELDS: FieldConfig[] = [\n { name: 'name', label: 'Vendor name', type: 'text', required: true },\n { name: 'category', label: 'Category', type: 'text', required: true },\n { name: 'contactPerson', label: 'Contact person', type: 'text' },\n { name: 'email', label: 'Contact email', type: 'email' },\n { name: 'phone', label: 'Phone', type: 'text' },\n {\n name: 'status',\n label: 'Status',\n type: 'select',\n options: [\n { label: 'Active', value: 'active' },\n { label: 'Inactive', value: 'inactive' },\n { label: 'Pending', value: 'pending' },\n ],\n },\n { name: 'contractValue', label: 'Contract value (USD)', type: 'number' },\n { name: 'rating', label: 'Rating (0-5)', type: 'number' },\n];\n\nconst toInitialValues = (vendor: Vendor): FormValues => ({\n name: vendor.name,\n category: vendor.category,\n contactPerson: vendor.contactPerson,\n email: vendor.email,\n phone: vendor.phone,\n status: vendor.status,\n contractValue: vendor.contractValue,\n rating: vendor.rating,\n});\n\nconst valuesToVendor = (values: FormValues, base?: Vendor): Vendor => ({\n id: base?.id ?? crypto.randomUUID(),\n name: String(values.name ?? ''),\n category: String(values.category ?? ''),\n contactPerson: String(values.contactPerson ?? ''),\n email: String(values.email ?? ''),\n phone: String(values.phone ?? ''),\n status: (values.status as Vendor['status']) || 'pending',\n contractValue:\n typeof values.contractValue === 'number'\n ? values.contractValue\n : Number(values.contractValue) || 0,\n rating: typeof values.rating === 'number' ? values.rating : Number(values.rating) || 0,\n});\n\nconst compactMoney = (value: number): string => {\n if (value >= 1_000_000) {\n return `$${(value / 1_000_000).toFixed(1)}M`;\n }\n if (value >= 1_000) {\n return `$${Math.round(value / 1_000)}k`;\n }\n return `$${Math.round(value)}`;\n};\n\nexport function CompanyVendorManagementPagePage() {\n const { vendors, clients, invoices, addVendor, updateVendor, removeVendor } =\n useCrewFoundryStore();\n const navigate = useNavigate();\n const modal = useFormModal<Vendor>();\n const [search, setSearch] = useState('');\n const [filterStatus, setFilterStatus] = useState<string>('all');\n const [deleteTarget, setDeleteTarget] = useState<Vendor | null>(null);\n const { page, pageSize, offset, setPage } = usePagination({ initialPageSize: 12 });\n\n const activeClients = clients.filter((c) => c.status === 'active');\n const contractValue = totalContractValue(clients, vendors);\n const quarterRevenue = thisQuarterRevenue(invoices);\n\n const filtered = vendors.filter((v) => {\n const matchSearch = `${v.name} ${v.category} ${v.contactPerson} ${v.email}`\n .toLowerCase()\n .includes(search.toLowerCase());\n const matchStatus = filterStatus === 'all' || v.status === filterStatus;\n return matchSearch && matchStatus;\n });\n const paged = filtered.slice(offset, offset + pageSize);\n\n const handleSubmit = async (values: FormValues) => {\n if (modal.context) {\n await updateVendor(modal.context.id, valuesToVendor(values, modal.context));\n } else {\n await addVendor(valuesToVendor(values));\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>Clients & Vendors</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">\n {clients.length} clients · {vendors.length} vendors\n </p>\n <PagePurpose className=\"mt-1\">\n Track the vendors and suppliers you depend on — contracts, contacts, and status — to\n manage external relationships in one place.\n </PagePurpose>\n </div>\n <Button size=\"sm\" onClick={() => modal.openModal()}>\n <Plus className=\"h-4 w-4 mr-1\" />\n Add Vendor\n </Button>\n </div>\n\n <SegmentedTabs\n aria-label=\"Clients and vendors\"\n variant=\"underline\"\n value=\"vendors\"\n onChange={(id) => {\n if (id === 'clients') {\n navigate('../company/client-management-page');\n }\n }}\n tabs={[\n { id: 'clients', label: 'Clients', count: clients.length },\n { id: 'vendors', label: 'Vendors', count: vendors.length },\n ]}\n />\n\n <div className=\"grid gap-4 sm:grid-cols-2 xl:grid-cols-4\">\n <StatCard label=\"Active Clients\" value={activeClients.length} />\n <StatCard label=\"Vendors\" value={vendors.length} />\n <StatCard label=\"Total Contract Value\" value={compactMoney(contractValue)} />\n <StatCard label=\"This Quarter\" value={compactMoney(quarterRevenue)} />\n </div>\n\n <div className=\"flex flex-col gap-2 sm:flex-row sm:items-center\">\n <div className=\"relative flex-1\">\n <Search className=\"absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-text-secondary\" />\n <Input\n placeholder=\"Search vendors...\"\n className=\"pl-9\"\n value={search}\n onChange={(event) => {\n setSearch(event.target.value);\n setPage(1);\n }}\n />\n </div>\n <Select\n value={filterStatus}\n onValueChange={(value) => {\n setFilterStatus(value);\n setPage(1);\n }}\n >\n <SelectTrigger className=\"w-full sm:w-40\" aria-label=\"Filter by status\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"all\">All statuses</SelectItem>\n <SelectItem value=\"active\">Active</SelectItem>\n <SelectItem value=\"inactive\">Inactive</SelectItem>\n <SelectItem value=\"pending\">Pending</SelectItem>\n </SelectContent>\n </Select>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-3\">\n {vendors.length === 0 ? (\n <div className=\"col-span-full\">\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"No vendors yet\"\n description=\"Add a vendor to start tracking external relationships.\"\n />\n </div>\n ) : filtered.length === 0 ? (\n <div className=\"col-span-full\">\n <IllustratedEmptyState\n illustration=\"empty-search\"\n title=\"No vendors match\"\n description=\"Try a different search term or status filter.\"\n />\n </div>\n ) : (\n paged.map((vendor) => (\n <GlassCard key={vendor.id} treatment=\"glass\">\n <CardHeader>\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"flex min-w-0 items-center gap-3\">\n <AvatarStack people={[{ name: vendor.name }]} size={36} />\n <CardTitle className=\"truncate text-base\">{vendor.name}</CardTitle>\n </div>\n <div className=\"flex items-center gap-1\">\n <Badge\n variant={vendor.status === 'active' ? 'default' : 'secondary'}\n className=\"text-[10px]\"\n >\n {formatEnumLabel(vendor.status)}\n </Badge>\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"h-8 w-8\"\n aria-label={`More actions for ${vendor.name}`}\n >\n <MoreHorizontal className=\"h-4 w-4\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n <DropdownMenuItem onSelect={() => modal.openModal(vendor)}>\n <Pencil className=\"mr-2 h-4 w-4\" />\n Edit\n </DropdownMenuItem>\n <DropdownMenuSeparator />\n <DropdownMenuItem\n className=\"text-status-error-text\"\n onSelect={() => setDeleteTarget(vendor)}\n >\n <Trash2 className=\"mr-2 h-4 w-4\" />\n Delete\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n </div>\n </div>\n </CardHeader>\n <CardContent>\n <div className=\"space-y-2\">\n <Badge variant=\"outline\" className=\"text-[10px]\">\n {vendor.category}\n </Badge>\n <div className=\"grid grid-cols-2 gap-2 mt-2\">\n <div>\n <p className=\"text-xs text-text-secondary\">Contact</p>\n <p className=\"text-sm\">{vendor.contactPerson || '—'}</p>\n </div>\n <div>\n <p className=\"text-xs text-text-secondary\">Contract Value</p>\n <p className=\"text-sm font-medium tabular-nums\">\n ${vendor.contractValue.toLocaleString()}\n </p>\n </div>\n </div>\n <div className=\"flex items-center justify-between pt-2 border-t mt-2\">\n <div className=\"flex items-center gap-1 text-xs text-text-secondary\">\n <Mail className=\"h-3 w-3\" />\n {vendor.email || '—'}\n </div>\n <div className=\"flex items-center gap-1\">\n <Star className=\"h-3 w-3 text-status-warning-text\" />\n <span className=\"text-sm tabular-nums\">{vendor.rating}</span>\n </div>\n </div>\n </div>\n </CardContent>\n </GlassCard>\n ))\n )}\n </div>\n {filtered.length > pageSize && (\n <Pagination\n page={page}\n pageSize={pageSize}\n totalItems={filtered.length}\n onPageChange={setPage}\n />\n )}\n\n <FormModal\n open={modal.open}\n onOpenChange={modal.onOpenChange}\n title={modal.context ? 'Edit vendor' : 'Add vendor'}\n description=\"Track the vendor, its contract value, and primary contact.\"\n fields={VENDOR_FIELDS}\n initialValues={modal.context ? toInitialValues(modal.context) : undefined}\n submitLabel={modal.context ? 'Save changes' : 'Add vendor'}\n onSubmit={handleSubmit}\n />\n\n <AlertDialog\n open={deleteTarget !== null}\n onOpenChange={(open) => {\n if (!open) {\n setDeleteTarget(null);\n }\n }}\n >\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>Delete vendor?</AlertDialogTitle>\n <AlertDialogDescription>\n {deleteTarget\n ? `This removes “${deleteTarget.name}” from your vendor directory. This action cannot be undone.`\n : ''}\n </AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel>Cancel</AlertDialogCancel>\n <AlertDialogAction\n onClick={(event) => {\n event.preventDefault();\n if (deleteTarget) {\n void removeVendor(deleteTarget.id);\n }\n setDeleteTarget(null);\n }}\n >\n Delete\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CompanyVendorManagementPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/CompanyVendorManagementPagePage.tsx"],"sourcesContent":["import { useState } from 'react';\nimport {\n AlertDialog,\n AlertDialogAction,\n AlertDialogCancel,\n AlertDialogContent,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogHeader,\n AlertDialogTitle,\n Badge,\n Button,\n CardContent,\n CardHeader,\n CardTitle,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n GlassCard,\n GradientText,\n IllustratedEmptyState,\n Input,\n PagePurpose,\n Pagination,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@burdenoff/fe-libs/ui';\nimport { usePagination } from '@burdenoff/fe-libs/shared/hooks';\nimport { Mail, MoreHorizontal, Pencil, Plus, Search, Star, Trash2 } from 'lucide-react';\nimport { useNavigate } from 'react-router';\nimport {\n FormModal,\n useFormModal,\n type FieldConfig,\n type FormValues,\n} from '../components/forms/FormModal';\nimport { useCrewFoundryStore, type Vendor } from '../store/crewfoundryStore';\nimport { formatEnumLabel } from '../utils/formatEnumLabel';\nimport { AvatarStack, SegmentedTabs, StatCard } from '../components/viz';\nimport { totalContractValue, thisQuarterRevenue } from '../lib/companyFinance';\n\nconst VENDOR_FIELDS: FieldConfig[] = [\n { name: 'name', label: 'Vendor name', type: 'text', required: true },\n { name: 'category', label: 'Category', type: 'text', required: true },\n { name: 'contactPerson', label: 'Contact person', type: 'text' },\n { name: 'email', label: 'Contact email', type: 'email' },\n { name: 'phone', label: 'Phone', type: 'text' },\n {\n name: 'status',\n label: 'Status',\n type: 'select',\n options: [\n { label: 'Active', value: 'active' },\n { label: 'Inactive', value: 'inactive' },\n { label: 'Pending', value: 'pending' },\n ],\n },\n { name: 'contractValue', label: 'Contract value (USD)', type: 'number' },\n { name: 'rating', label: 'Rating (0-5)', type: 'number' },\n];\n\nconst toInitialValues = (vendor: Vendor): FormValues => ({\n name: vendor.name,\n category: vendor.category,\n contactPerson: vendor.contactPerson,\n email: vendor.email,\n phone: vendor.phone,\n status: vendor.status,\n contractValue: vendor.contractValue,\n rating: vendor.rating,\n});\n\nconst valuesToVendor = (values: FormValues, base?: Vendor): Vendor => ({\n id: base?.id ?? crypto.randomUUID(),\n name: String(values.name ?? ''),\n category: String(values.category ?? ''),\n contactPerson: String(values.contactPerson ?? ''),\n email: String(values.email ?? ''),\n phone: String(values.phone ?? ''),\n status: (values.status as Vendor['status']) || 'pending',\n contractValue:\n typeof values.contractValue === 'number'\n ? values.contractValue\n : Number(values.contractValue) || 0,\n rating: typeof values.rating === 'number' ? values.rating : Number(values.rating) || 0,\n});\n\nconst compactMoney = (value: number): string => {\n if (value >= 1_000_000) {\n return `$${(value / 1_000_000).toFixed(1)}M`;\n }\n if (value >= 1_000) {\n return `$${Math.round(value / 1_000)}k`;\n }\n return `$${Math.round(value)}`;\n};\n\nexport function CompanyVendorManagementPagePage() {\n const { vendors, clients, invoices, addVendor, updateVendor, removeVendor } =\n useCrewFoundryStore();\n const navigate = useNavigate();\n const modal = useFormModal<Vendor>();\n const [search, setSearch] = useState('');\n const [filterStatus, setFilterStatus] = useState<string>('all');\n const [deleteTarget, setDeleteTarget] = useState<Vendor | null>(null);\n const { page, pageSize, offset, setPage } = usePagination({ initialPageSize: 12 });\n\n const activeClients = clients.filter((c) => c.status === 'active');\n const contractValue = totalContractValue(clients, vendors);\n const quarterRevenue = thisQuarterRevenue(invoices);\n\n const filtered = vendors.filter((v) => {\n const matchSearch = `${v.name} ${v.category} ${v.contactPerson} ${v.email}`\n .toLowerCase()\n .includes(search.toLowerCase());\n const matchStatus = filterStatus === 'all' || v.status === filterStatus;\n return matchSearch && matchStatus;\n });\n const paged = filtered.slice(offset, offset + pageSize);\n\n const handleSubmit = async (values: FormValues) => {\n if (modal.context) {\n await updateVendor(modal.context.id, valuesToVendor(values, modal.context));\n } else {\n await addVendor(valuesToVendor(values));\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>Clients & Vendors</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">\n {clients.length} clients · {vendors.length} vendors\n </p>\n <PagePurpose className=\"mt-1\">\n Track the vendors and suppliers you depend on — contracts, contacts, and status — to\n manage external relationships in one place.\n </PagePurpose>\n </div>\n <Button size=\"sm\" onClick={() => modal.openModal()}>\n <Plus className=\"h-4 w-4 mr-1\" />\n Add Vendor\n </Button>\n </div>\n\n <SegmentedTabs\n aria-label=\"Clients and vendors\"\n variant=\"underline\"\n value=\"vendors\"\n onChange={(id) => {\n if (id === 'clients') {\n navigate('../company/client-management-page');\n }\n }}\n tabs={[\n { id: 'clients', label: 'Clients', count: clients.length },\n { id: 'vendors', label: 'Vendors', count: vendors.length },\n ]}\n />\n\n <div className=\"grid gap-4 sm:grid-cols-2 xl:grid-cols-4\">\n <StatCard label=\"Active Clients\" value={activeClients.length} />\n <StatCard label=\"Vendors\" value={vendors.length} />\n <StatCard label=\"Total Contract Value\" value={compactMoney(contractValue)} />\n <StatCard label=\"This Quarter\" value={compactMoney(quarterRevenue)} />\n </div>\n\n <div className=\"flex flex-col gap-2 sm:flex-row sm:items-center\">\n <div className=\"relative flex-1\">\n <Search className=\"absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-text-secondary\" />\n <Input\n placeholder=\"Search vendors...\"\n className=\"pl-9\"\n value={search}\n onChange={(event) => {\n setSearch(event.target.value);\n setPage(1);\n }}\n />\n </div>\n <Select\n value={filterStatus}\n onValueChange={(value) => {\n setFilterStatus(value);\n setPage(1);\n }}\n >\n <SelectTrigger className=\"w-full sm:w-40\" aria-label=\"Filter by status\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"all\">All statuses</SelectItem>\n <SelectItem value=\"active\">Active</SelectItem>\n <SelectItem value=\"inactive\">Inactive</SelectItem>\n <SelectItem value=\"pending\">Pending</SelectItem>\n </SelectContent>\n </Select>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-3\">\n {vendors.length === 0 ? (\n <div className=\"col-span-full\">\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title=\"No vendors yet\"\n description=\"Add a vendor to start tracking external relationships.\"\n />\n </div>\n ) : filtered.length === 0 ? (\n <div className=\"col-span-full\">\n <IllustratedEmptyState\n illustration=\"empty-search\"\n title=\"No vendors match\"\n description=\"Try a different search term or status filter.\"\n />\n </div>\n ) : (\n paged.map((vendor) => (\n <GlassCard key={vendor.id} treatment=\"glass\">\n <CardHeader>\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"flex min-w-0 items-center gap-3\">\n <AvatarStack people={[{ name: vendor.name }]} size={36} />\n <CardTitle className=\"truncate text-base\">{vendor.name}</CardTitle>\n </div>\n <div className=\"flex items-center gap-1\">\n <Badge\n variant={vendor.status === 'active' ? 'default' : 'secondary'}\n className=\"text-[10px]\"\n >\n {formatEnumLabel(vendor.status)}\n </Badge>\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"h-8 w-8\"\n aria-label={`More actions for ${vendor.name}`}\n >\n <MoreHorizontal className=\"h-4 w-4\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n <DropdownMenuItem onSelect={() => modal.openModal(vendor)}>\n <Pencil className=\"mr-2 h-4 w-4\" />\n Edit\n </DropdownMenuItem>\n <DropdownMenuSeparator />\n <DropdownMenuItem\n className=\"text-status-error-text\"\n onSelect={() => setDeleteTarget(vendor)}\n >\n <Trash2 className=\"mr-2 h-4 w-4\" />\n Delete\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n </div>\n </div>\n </CardHeader>\n <CardContent>\n <div className=\"space-y-2\">\n <Badge variant=\"outline\" className=\"text-[10px]\">\n {vendor.category}\n </Badge>\n <div className=\"grid grid-cols-2 gap-2 mt-2\">\n <div>\n <p className=\"text-xs text-text-secondary\">Contact</p>\n <p className=\"text-sm\">{vendor.contactPerson || '—'}</p>\n </div>\n <div>\n <p className=\"text-xs text-text-secondary\">Contract Value</p>\n <p className=\"text-sm font-medium tabular-nums\">\n ${vendor.contractValue.toLocaleString()}\n </p>\n </div>\n </div>\n <div className=\"flex items-center justify-between pt-2 border-t mt-2\">\n <div className=\"flex items-center gap-1 text-xs text-text-secondary\">\n <Mail className=\"h-3 w-3\" />\n {vendor.email || '—'}\n </div>\n <div className=\"flex items-center gap-1\">\n <Star className=\"h-3 w-3 text-status-warning-text\" />\n <span className=\"text-sm tabular-nums\">{vendor.rating}</span>\n </div>\n </div>\n </div>\n </CardContent>\n </GlassCard>\n ))\n )}\n </div>\n {filtered.length > pageSize && (\n <Pagination\n page={page}\n pageSize={pageSize}\n totalItems={filtered.length}\n onPageChange={setPage}\n />\n )}\n\n <FormModal\n open={modal.open}\n onOpenChange={modal.onOpenChange}\n title={modal.context ? 'Edit vendor' : 'Add vendor'}\n description=\"Track the vendor, its contract value, and primary contact.\"\n fields={VENDOR_FIELDS}\n initialValues={modal.context ? toInitialValues(modal.context) : undefined}\n submitLabel={modal.context ? 'Save changes' : 'Add vendor'}\n onSubmit={handleSubmit}\n />\n\n <AlertDialog\n open={deleteTarget !== null}\n onOpenChange={(open) => {\n if (!open) {\n setDeleteTarget(null);\n }\n }}\n >\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>Delete vendor?</AlertDialogTitle>\n <AlertDialogDescription>\n {deleteTarget\n ? `This removes “${deleteTarget.name}” from your vendor directory. This action cannot be undone.`\n : ''}\n </AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel>Cancel</AlertDialogCancel>\n <AlertDialogAction\n onClick={(event) => {\n event.preventDefault();\n if (deleteTarget) {\n void removeVendor(deleteTarget.id);\n }\n setDeleteTarget(null);\n }}\n >\n Delete\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -9,7 +9,7 @@ import "../components/viz/index.js";
|
|
|
9
9
|
import { headcountByMonth as te, momDelta as ne } from "../lib/analytics.js";
|
|
10
10
|
import { useMemo as o, useState as s } from "react";
|
|
11
11
|
import { Activity as c, Briefcase as l, Calendar as u, FolderPlus as re, Plus as ie, TrendingUp as ae, UserPlus as d, Users as f } from "lucide-react";
|
|
12
|
-
import { useNavigate as oe } from "react-router
|
|
12
|
+
import { useNavigate as oe } from "react-router";
|
|
13
13
|
import { jsx as p, jsxs as m } from "react/jsx-runtime";
|
|
14
14
|
import { AuroraBackground as se, Badge as h, Button as g, Card as _, CardContent as v, CardHeader as y, CardTitle as b, DropdownMenu as ce, DropdownMenuContent as le, DropdownMenuItem as x, DropdownMenuLabel as ue, DropdownMenuSeparator as S, DropdownMenuTrigger as C, GradientText as de, IllustratedEmptyState as w, NextSteps as fe, PagePurpose as pe, Pagination as me } from "@burdenoff/fe-libs/ui";
|
|
15
15
|
import { usePagination as he } from "@burdenoff/fe-libs/shared/hooks";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DashboardDashboardPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/DashboardDashboardPagePage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport {\n Badge,\n Button,\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n AuroraBackground,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n GradientText,\n IllustratedEmptyState,\n NextSteps,\n PagePurpose,\n Pagination,\n} from '@burdenoff/fe-libs/ui';\nimport { usePagination } from '@burdenoff/fe-libs/shared/hooks';\nimport {\n Activity,\n Briefcase,\n Calendar,\n FolderPlus,\n Plus,\n TrendingUp,\n UserPlus,\n Users,\n} from 'lucide-react';\nimport { useNavigate } from 'react-router-dom';\nimport { useGlobalUi } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { useCrewFoundryStore } from '../store/crewfoundryStore';\nimport { useCrewFoundry } from '../providers/CrewFoundryProvider';\nimport { formatEnumLabel } from '../utils/formatEnumLabel';\nimport { AreaChart, HBarFunnel, StatCard, getInitials } from '../components/viz';\nimport { headcountByMonth, momDelta } from '../lib/analytics';\n\n/** Cumulative count of entities per absolute month index, from their date field. */\nfunction cumulativeSeries(dates: (string | undefined | null)[]): number[] {\n const indices = dates\n .map((value) => (value ? new Date(value) : null))\n .filter((date): date is Date => date !== null && !Number.isNaN(date.getTime()))\n .map((date) => date.getUTCFullYear() * 12 + date.getUTCMonth());\n if (indices.length === 0) {\n return [];\n }\n const min = Math.min(...indices);\n const max = Math.max(...indices);\n const series: number[] = [];\n for (let index = min; index <= max; index += 1) {\n series.push(indices.filter((start) => start <= index).length);\n }\n return series;\n}\n\n/** Last-vs-prior percentage change of a raw series, for KPI trend arrows. */\nfunction seriesDeltaPercent(values: number[]): number | undefined {\n if (values.length < 2) {\n return undefined;\n }\n const current = values[values.length - 1];\n const previous = values[values.length - 2];\n if (previous <= 0) {\n return undefined;\n }\n return Math.round(((current - previous) / previous) * 100);\n}\n\n/** Circular initials avatar (backend has no interview-candidate photos). */\nfunction InitialsAvatar({ name, size = 38 }: { name: string; size?: number }) {\n return (\n <span\n aria-hidden=\"true\"\n className=\"inline-flex shrink-0 items-center justify-center rounded-full bg-bg-elevated font-medium text-text-secondary\"\n style={{ width: size, height: size, fontSize: Math.round(size * 0.36) }}\n >\n {getInitials(name)}\n </span>\n );\n}\n\nconst STAGE_ORDER = ['new', 'screening', 'interview', 'assessment', 'offer', 'hired'] as const;\n\nexport function DashboardDashboardPagePage() {\n const { t: translate } = useI18n();\n const navigate = useNavigate();\n const { showToast } = useGlobalUi();\n const { currentUser } = useCrewFoundry();\n const {\n employees,\n jobPostings,\n interviews,\n leaveRequests,\n projects,\n candidates,\n announcements,\n updateLeaveRequest,\n } = useCrewFoundryStore();\n const [processingLeaveId, setProcessingLeaveId] = useState<string | null>(null);\n const [expandedAnnouncements, setExpandedAnnouncements] = useState<Record<string, boolean>>({});\n\n const approverName =\n currentUser?.name ||\n [currentUser?.firstName, currentUser?.lastName].filter(Boolean).join(' ') ||\n currentUser?.email ||\n 'Manager';\n\n const decideLeave = async (id: string, status: 'approved' | 'rejected') => {\n setProcessingLeaveId(id);\n try {\n await updateLeaveRequest(id, { status, approverName });\n showToast(status === 'approved' ? 'Leave approved' : 'Leave rejected', 'success');\n } catch (error) {\n showToast(error instanceof Error ? error.message : 'Unable to update leave request', 'error');\n } finally {\n setProcessingLeaveId(null);\n }\n };\n const t = (key: string, fallback: string) => {\n const value = translate(key);\n return value === key ? fallback : value;\n };\n\n const activeEmployees = employees.filter((e) => e.status === 'active').length;\n const onLeave = employees.filter((e) => e.status === 'on_leave').length;\n const openPositions = jobPostings.filter((j) => j.status === 'active').length;\n const totalApplicants = jobPostings.reduce((sum, j) => sum + j.applicants, 0);\n const pendingLeaves = leaveRequests.filter((l) => l.status === 'pending').length;\n const activeProjects = projects.filter((p) => p.status === 'active').length;\n const avgPerformance = (\n employees.reduce((sum, e) => sum + e.performanceScore, 0) / Math.max(employees.length, 1)\n ).toFixed(1);\n\n // Real cumulative headcount time-series from employee start dates.\n const headcountSeries = useMemo(() => headcountByMonth(employees), [employees]);\n const headcountValues = headcountSeries.map((point) => point.value);\n const headcountDelta = momDelta(headcountSeries).deltaPercent;\n\n const openingsSeries = useMemo(\n () =>\n cumulativeSeries(\n jobPostings.filter((job) => job.status === 'active').map((j) => j.postedDate)\n ),\n [jobPostings]\n );\n const projectsSeries = useMemo(\n () => cumulativeSeries(projects.filter((p) => p.status === 'active').map((p) => p.startDate)),\n [projects]\n );\n\n // Descending hiring funnel: candidates who have reached at least each stage.\n const pipelineCounts = useMemo(() => {\n const rankOf = (status: string) => STAGE_ORDER.indexOf(status as (typeof STAGE_ORDER)[number]);\n const reached = (minRank: number) =>\n candidates.filter((candidate) => rankOf(candidate.status) >= minRank).length;\n return {\n applied: candidates.length,\n screening: reached(1),\n interview: reached(2),\n offer: reached(4),\n hired: reached(5),\n };\n }, [candidates]);\n const pipeline = [\n {\n label: t('crewfoundry.dashboard.pipeline.applied', 'Applied'),\n value: pipelineCounts.applied,\n },\n {\n label: t('crewfoundry.dashboard.pipeline.screening', 'Screening'),\n value: pipelineCounts.screening,\n },\n {\n label: t('crewfoundry.dashboard.pipeline.interview', 'Interview'),\n value: pipelineCounts.interview,\n },\n { label: t('crewfoundry.dashboard.pipeline.offer', 'Offer'), value: pipelineCounts.offer },\n { label: t('crewfoundry.dashboard.pipeline.hired', 'Hired'), value: pipelineCounts.hired },\n ];\n\n const employeeByName = useMemo(() => {\n const map = new Map<string, string | undefined>();\n for (const employee of employees) {\n map.set(`${employee.firstName} ${employee.lastName}`.trim().toLowerCase(), employee.avatar);\n }\n return map;\n }, [employees]);\n\n const scheduledInterviews = [...interviews]\n .filter((i) => i.status === 'scheduled')\n .sort((a, b) => `${a.date} ${a.time}`.localeCompare(`${b.date} ${b.time}`));\n const upcomingInterviews = scheduledInterviews.slice(0, 5);\n\n const sortedAnnouncements = useMemo(\n () =>\n [...announcements].sort((a, b) => {\n if (a.isPinned !== b.isPinned) {\n return a.isPinned ? -1 : 1;\n }\n return b.createdAt.localeCompare(a.createdAt);\n }),\n [announcements]\n );\n const topAnnouncements = sortedAnnouncements.slice(0, 4);\n\n const recentLeaveRequests = [...leaveRequests].sort((a, b) => {\n if (a.status === 'pending' && b.status !== 'pending') return -1;\n if (b.status === 'pending' && a.status !== 'pending') return 1;\n return b.startDate.localeCompare(a.startDate);\n });\n const {\n page: leavePage,\n pageSize: leavePageSize,\n offset: leaveOffset,\n setPage: setLeavePage,\n } = usePagination({ initialPageSize: 6 });\n const leavePageItems = recentLeaveRequests.slice(leaveOffset, leaveOffset + leavePageSize);\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"relative overflow-hidden rounded-xl border border-border-seam bg-gloss-card p-6\">\n <AuroraBackground intensity=\"subtle\" />\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>{t('crewfoundry.dashboard.title', 'Command Center')}</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">\n {t('crewfoundry.dashboard.subtitle', 'Your workforce at a glance')}\n </p>\n <PagePurpose className=\"mt-2\">\n {t(\n 'crewfoundry.dashboard.purpose',\n 'Your workforce command center — see headcount, hiring, performance, and projects at a glance, then jump straight to the area that needs your attention.'\n )}\n </PagePurpose>\n </div>\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button className=\"self-start\">\n <Plus className=\"mr-1 h-4 w-4\" />\n {t('crewfoundry.dashboard.new', 'New')}\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\" className=\"w-52\">\n <DropdownMenuLabel>{t('crewfoundry.dashboard.newMenu', 'Create')}</DropdownMenuLabel>\n <DropdownMenuSeparator />\n <DropdownMenuItem onClick={() => navigate('../hr/employees-page')}>\n <UserPlus className=\"mr-2 h-4 w-4\" />\n {t('crewfoundry.dashboard.newEmployee', 'Add employee')}\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => navigate('../hiring/create-job-page')}>\n <Briefcase className=\"mr-2 h-4 w-4\" />\n {t('crewfoundry.dashboard.newJob', 'Post a job')}\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => navigate('../projects/projects-page')}>\n <FolderPlus className=\"mr-2 h-4 w-4\" />\n {t('crewfoundry.dashboard.newProject', 'New project')}\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n </div>\n </div>\n\n <NextSteps\n storageKey=\"crewfoundry-dashboard\"\n steps={[\n {\n id: 'employees',\n label: t('crewfoundry.dashboard.next.employees', 'Review your team'),\n description: t(\n 'crewfoundry.dashboard.next.employeesDesc',\n 'Open the employee directory to see who is active and on leave.'\n ),\n icon: <Users className=\"h-4 w-4\" />,\n onClick: () => navigate('../hr/employees-page'),\n },\n {\n id: 'hiring',\n label: t('crewfoundry.dashboard.next.hiring', 'Post a new job'),\n description: t(\n 'crewfoundry.dashboard.next.hiringDesc',\n 'Create an opening to start collecting applicants.'\n ),\n icon: <Briefcase className=\"h-4 w-4\" />,\n onClick: () => navigate('../hiring/create-job-page'),\n },\n {\n id: 'projects',\n label: t('crewfoundry.dashboard.next.projects', 'Check active projects'),\n description: t(\n 'crewfoundry.dashboard.next.projectsDesc',\n 'Track progress and budgets across in-flight work.'\n ),\n icon: <Activity className=\"h-4 w-4\" />,\n onClick: () => navigate('../projects/projects-page'),\n },\n ]}\n />\n\n {/* Bento: KPI grid + headcount growth + upcoming interviews */}\n <div className=\"grid gap-4 lg:grid-cols-12\">\n <div className=\"grid gap-4 sm:grid-cols-2 lg:col-span-5\">\n <StatCard\n label={t('crewfoundry.dashboard.stats.totalEmployees', 'Total Employees')}\n value={employees.length}\n delta={headcountDelta || undefined}\n deltaLabel={t('crewfoundry.dashboard.stats.fromLastMonth', 'from last month')}\n icon={<Users className=\"h-4 w-4\" />}\n sparkline={headcountValues}\n sparklineVariant=\"area\"\n />\n <StatCard\n label={t('crewfoundry.dashboard.stats.openPositions', 'Open Positions')}\n value={openPositions}\n delta={seriesDeltaPercent(openingsSeries)}\n deltaLabel={t('crewfoundry.dashboard.stats.fromLastMonth', 'from last month')}\n icon={<Briefcase className=\"h-4 w-4\" />}\n sparkline={openingsSeries}\n sparklineVariant=\"area\"\n />\n <StatCard\n label={t('crewfoundry.dashboard.stats.activeProjects', 'Active Projects')}\n value={activeProjects}\n delta={seriesDeltaPercent(projectsSeries)}\n deltaLabel={t('crewfoundry.dashboard.stats.fromLastMonth', 'from last month')}\n icon={<Activity className=\"h-4 w-4\" />}\n sparkline={projectsSeries}\n sparklineVariant=\"area\"\n />\n <StatCard\n label={t('crewfoundry.dashboard.stats.avgPerformance', 'Avg Performance')}\n value={`${avgPerformance}`}\n icon={<TrendingUp className=\"h-4 w-4\" />}\n />\n </div>\n\n <Card className=\"lg:col-span-4\">\n <CardHeader>\n <CardTitle className=\"text-base\">\n {t('crewfoundry.dashboard.headcountGrowth.title', 'Headcount growth')}\n </CardTitle>\n </CardHeader>\n <CardContent>\n {headcountSeries.length > 1 ? (\n <AreaChart\n series={headcountSeries}\n height={210}\n title=\"Cumulative headcount by month\"\n />\n ) : (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={t('crewfoundry.dashboard.headcountGrowth.emptyTitle', 'Not enough history')}\n description={t(\n 'crewfoundry.dashboard.headcountGrowth.emptyDescription',\n 'Headcount growth appears once employees span more than one hiring month.'\n )}\n />\n )}\n </CardContent>\n </Card>\n\n <Card className=\"lg:col-span-3\">\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0\">\n <CardTitle className=\"text-base\">\n {t('crewfoundry.dashboard.upcomingInterviews.title', 'Upcoming interviews')}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {upcomingInterviews.length === 0 && (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={t(\n 'crewfoundry.dashboard.upcomingInterviews.emptyTitle',\n 'No interviews scheduled'\n )}\n description={t(\n 'crewfoundry.dashboard.upcomingInterviews.emptyDescription',\n 'Scheduled interviews will appear here.'\n )}\n />\n )}\n {upcomingInterviews.map((interview) => {\n const avatar = employeeByName.get(interview.candidateName.trim().toLowerCase());\n return (\n <div key={interview.id} className=\"flex items-center gap-3\">\n {avatar ? (\n <img\n src={avatar}\n alt=\"\"\n className=\"h-9 w-9 shrink-0 rounded-full object-cover\"\n />\n ) : (\n <InitialsAvatar name={interview.candidateName} />\n )}\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium\">{interview.candidateName}</p>\n <p className=\"truncate text-xs text-text-secondary\">{interview.position}</p>\n </div>\n <div className=\"flex shrink-0 items-center gap-1.5 text-right\">\n <div>\n <p className=\"text-xs font-medium\">{interview.time}</p>\n <p className=\"text-[10px] text-text-secondary\">{interview.date}</p>\n </div>\n <Calendar className=\"h-3.5 w-3.5 text-text-secondary\" />\n </div>\n </div>\n );\n })}\n {scheduledInterviews.length > upcomingInterviews.length && (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"w-full\"\n onClick={() => navigate('../hiring/interviews-page')}\n >\n {t('crewfoundry.dashboard.upcomingInterviews.viewAll', 'View All')}\n </Button>\n )}\n </CardContent>\n </Card>\n </div>\n\n {/* Hiring pipeline funnel + announcements rail */}\n <div className=\"grid gap-4 lg:grid-cols-12\">\n <Card className=\"lg:col-span-8\">\n <CardHeader>\n <CardTitle className=\"text-base\">\n {t('crewfoundry.dashboard.pipeline.title', 'Hiring pipeline')}\n </CardTitle>\n </CardHeader>\n <CardContent>\n {candidates.length > 0 ? (\n <HBarFunnel stages={pipeline} valueFormatter={(value) => `${value}`} />\n ) : (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={t('crewfoundry.dashboard.pipeline.emptyTitle', 'No candidates yet')}\n description={t(\n 'crewfoundry.dashboard.pipeline.emptyDescription',\n 'Your hiring funnel appears here once candidates enter the pipeline.'\n )}\n />\n )}\n </CardContent>\n </Card>\n\n <Card className=\"lg:col-span-4\">\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0\">\n <CardTitle className=\"text-base\">\n {t('crewfoundry.dashboard.announcements.title', 'Announcements')}\n </CardTitle>\n {sortedAnnouncements.length > topAnnouncements.length && (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => navigate('../communications/announcements-page')}\n >\n {t('crewfoundry.dashboard.announcements.viewAll', 'View All')}\n </Button>\n )}\n </CardHeader>\n <CardContent className=\"space-y-4\">\n {topAnnouncements.length === 0 && (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={t('crewfoundry.dashboard.announcements.emptyTitle', 'No announcements')}\n description={t(\n 'crewfoundry.dashboard.announcements.emptyDescription',\n 'Company announcements will appear here.'\n )}\n />\n )}\n {topAnnouncements.map((announcement) => {\n const expanded = expandedAnnouncements[announcement.id];\n const isLong = announcement.content.length > 120;\n return (\n <div key={announcement.id} className=\"space-y-1\">\n <div className=\"flex items-center gap-2\">\n <p className=\"text-sm font-medium\">{announcement.title}</p>\n {announcement.isPinned && (\n <Badge variant=\"outline\" className=\"text-[10px]\">\n {t('crewfoundry.dashboard.announcements.pinned', 'Pinned')}\n </Badge>\n )}\n </div>\n <p className=\"text-xs text-text-secondary\">\n {announcement.createdAt} · {announcement.author}\n </p>\n <p className=\"text-sm text-text-secondary\">\n {expanded || !isLong\n ? announcement.content\n : `${announcement.content.slice(0, 120)}…`}\n </p>\n {isLong && (\n <button\n type=\"button\"\n className=\"text-xs font-medium text-action-primary-bg hover:underline\"\n onClick={() =>\n setExpandedAnnouncements((prev) => ({\n ...prev,\n [announcement.id]: !prev[announcement.id],\n }))\n }\n >\n {expanded\n ? t('crewfoundry.dashboard.announcements.readLess', 'Read Less')\n : t('crewfoundry.dashboard.announcements.readMore', 'Read More')}\n </button>\n )}\n </div>\n );\n })}\n </CardContent>\n </Card>\n </div>\n\n {/* Leave approvals (real approve / reject) */}\n <Card>\n <CardHeader>\n <CardTitle className=\"text-base\">\n {t('crewfoundry.dashboard.leaveRequests.title', 'Recent Leave Requests')}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {leaveRequests.length === 0 && (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={t('crewfoundry.dashboard.leaveRequests.emptyTitle', 'No leave requests')}\n description={t(\n 'crewfoundry.dashboard.leaveRequests.emptyDescription',\n 'Pending leave requests will appear here.'\n )}\n />\n )}\n {leavePageItems.map((leave) => (\n <div key={leave.id} className=\"flex items-center justify-between rounded border p-3\">\n <div className=\"flex items-center gap-3\">\n <InitialsAvatar name={leave.employeeName} size={34} />\n <div>\n <p className=\"text-sm font-medium\">{leave.employeeName}</p>\n <div className=\"flex items-center gap-2 text-xs text-text-secondary\">\n <Calendar className=\"h-3 w-3\" />\n <span>\n {leave.startDate} - {leave.endDate}\n </span>\n </div>\n </div>\n </div>\n <div className=\"flex items-center gap-2\">\n {leave.status === 'pending' && (\n <div className=\"flex gap-1\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n className=\"h-7 text-xs\"\n disabled={processingLeaveId === leave.id}\n onClick={() => void decideLeave(leave.id, 'rejected')}\n >\n {t('crewfoundry.dashboard.leaveRequests.reject', 'Reject')}\n </Button>\n <Button\n size=\"sm\"\n className=\"h-7 text-xs\"\n disabled={processingLeaveId === leave.id}\n onClick={() => void decideLeave(leave.id, 'approved')}\n >\n {t('crewfoundry.dashboard.leaveRequests.approve', 'Approve')}\n </Button>\n </div>\n )}\n <Badge\n variant={\n leave.status === 'approved'\n ? 'default'\n : leave.status === 'pending'\n ? 'secondary'\n : 'destructive'\n }\n >\n {formatEnumLabel(leave.status)}\n </Badge>\n <Badge variant=\"outline\">{formatEnumLabel(leave.type)}</Badge>\n </div>\n </div>\n ))}\n {recentLeaveRequests.length > leavePageSize && (\n <Pagination\n page={leavePage}\n pageSize={leavePageSize}\n totalItems={recentLeaveRequests.length}\n onPageChange={setLeavePage}\n />\n )}\n </CardContent>\n </Card>\n\n <p className=\"text-center text-xs text-text-muted\">\n {activeEmployees} {t('crewfoundry.dashboard.stats.active', 'active')} · {onLeave}{' '}\n {t('crewfoundry.dashboard.stats.onLeave', 'on leave')} · {totalApplicants}{' '}\n {t('crewfoundry.dashboard.stats.totalApplicants', 'total applicants')} · {pendingLeaves}{' '}\n {t('crewfoundry.dashboard.stats.pendingLeaveRequests', 'pending leave requests')}\n </p>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"DashboardDashboardPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/DashboardDashboardPagePage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport {\n Badge,\n Button,\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n AuroraBackground,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n GradientText,\n IllustratedEmptyState,\n NextSteps,\n PagePurpose,\n Pagination,\n} from '@burdenoff/fe-libs/ui';\nimport { usePagination } from '@burdenoff/fe-libs/shared/hooks';\nimport {\n Activity,\n Briefcase,\n Calendar,\n FolderPlus,\n Plus,\n TrendingUp,\n UserPlus,\n Users,\n} from 'lucide-react';\nimport { useNavigate } from 'react-router';\nimport { useGlobalUi } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { useCrewFoundryStore } from '../store/crewfoundryStore';\nimport { useCrewFoundry } from '../providers/CrewFoundryProvider';\nimport { formatEnumLabel } from '../utils/formatEnumLabel';\nimport { AreaChart, HBarFunnel, StatCard, getInitials } from '../components/viz';\nimport { headcountByMonth, momDelta } from '../lib/analytics';\n\n/** Cumulative count of entities per absolute month index, from their date field. */\nfunction cumulativeSeries(dates: (string | undefined | null)[]): number[] {\n const indices = dates\n .map((value) => (value ? new Date(value) : null))\n .filter((date): date is Date => date !== null && !Number.isNaN(date.getTime()))\n .map((date) => date.getUTCFullYear() * 12 + date.getUTCMonth());\n if (indices.length === 0) {\n return [];\n }\n const min = Math.min(...indices);\n const max = Math.max(...indices);\n const series: number[] = [];\n for (let index = min; index <= max; index += 1) {\n series.push(indices.filter((start) => start <= index).length);\n }\n return series;\n}\n\n/** Last-vs-prior percentage change of a raw series, for KPI trend arrows. */\nfunction seriesDeltaPercent(values: number[]): number | undefined {\n if (values.length < 2) {\n return undefined;\n }\n const current = values[values.length - 1];\n const previous = values[values.length - 2];\n if (previous <= 0) {\n return undefined;\n }\n return Math.round(((current - previous) / previous) * 100);\n}\n\n/** Circular initials avatar (backend has no interview-candidate photos). */\nfunction InitialsAvatar({ name, size = 38 }: { name: string; size?: number }) {\n return (\n <span\n aria-hidden=\"true\"\n className=\"inline-flex shrink-0 items-center justify-center rounded-full bg-bg-elevated font-medium text-text-secondary\"\n style={{ width: size, height: size, fontSize: Math.round(size * 0.36) }}\n >\n {getInitials(name)}\n </span>\n );\n}\n\nconst STAGE_ORDER = ['new', 'screening', 'interview', 'assessment', 'offer', 'hired'] as const;\n\nexport function DashboardDashboardPagePage() {\n const { t: translate } = useI18n();\n const navigate = useNavigate();\n const { showToast } = useGlobalUi();\n const { currentUser } = useCrewFoundry();\n const {\n employees,\n jobPostings,\n interviews,\n leaveRequests,\n projects,\n candidates,\n announcements,\n updateLeaveRequest,\n } = useCrewFoundryStore();\n const [processingLeaveId, setProcessingLeaveId] = useState<string | null>(null);\n const [expandedAnnouncements, setExpandedAnnouncements] = useState<Record<string, boolean>>({});\n\n const approverName =\n currentUser?.name ||\n [currentUser?.firstName, currentUser?.lastName].filter(Boolean).join(' ') ||\n currentUser?.email ||\n 'Manager';\n\n const decideLeave = async (id: string, status: 'approved' | 'rejected') => {\n setProcessingLeaveId(id);\n try {\n await updateLeaveRequest(id, { status, approverName });\n showToast(status === 'approved' ? 'Leave approved' : 'Leave rejected', 'success');\n } catch (error) {\n showToast(error instanceof Error ? error.message : 'Unable to update leave request', 'error');\n } finally {\n setProcessingLeaveId(null);\n }\n };\n const t = (key: string, fallback: string) => {\n const value = translate(key);\n return value === key ? fallback : value;\n };\n\n const activeEmployees = employees.filter((e) => e.status === 'active').length;\n const onLeave = employees.filter((e) => e.status === 'on_leave').length;\n const openPositions = jobPostings.filter((j) => j.status === 'active').length;\n const totalApplicants = jobPostings.reduce((sum, j) => sum + j.applicants, 0);\n const pendingLeaves = leaveRequests.filter((l) => l.status === 'pending').length;\n const activeProjects = projects.filter((p) => p.status === 'active').length;\n const avgPerformance = (\n employees.reduce((sum, e) => sum + e.performanceScore, 0) / Math.max(employees.length, 1)\n ).toFixed(1);\n\n // Real cumulative headcount time-series from employee start dates.\n const headcountSeries = useMemo(() => headcountByMonth(employees), [employees]);\n const headcountValues = headcountSeries.map((point) => point.value);\n const headcountDelta = momDelta(headcountSeries).deltaPercent;\n\n const openingsSeries = useMemo(\n () =>\n cumulativeSeries(\n jobPostings.filter((job) => job.status === 'active').map((j) => j.postedDate)\n ),\n [jobPostings]\n );\n const projectsSeries = useMemo(\n () => cumulativeSeries(projects.filter((p) => p.status === 'active').map((p) => p.startDate)),\n [projects]\n );\n\n // Descending hiring funnel: candidates who have reached at least each stage.\n const pipelineCounts = useMemo(() => {\n const rankOf = (status: string) => STAGE_ORDER.indexOf(status as (typeof STAGE_ORDER)[number]);\n const reached = (minRank: number) =>\n candidates.filter((candidate) => rankOf(candidate.status) >= minRank).length;\n return {\n applied: candidates.length,\n screening: reached(1),\n interview: reached(2),\n offer: reached(4),\n hired: reached(5),\n };\n }, [candidates]);\n const pipeline = [\n {\n label: t('crewfoundry.dashboard.pipeline.applied', 'Applied'),\n value: pipelineCounts.applied,\n },\n {\n label: t('crewfoundry.dashboard.pipeline.screening', 'Screening'),\n value: pipelineCounts.screening,\n },\n {\n label: t('crewfoundry.dashboard.pipeline.interview', 'Interview'),\n value: pipelineCounts.interview,\n },\n { label: t('crewfoundry.dashboard.pipeline.offer', 'Offer'), value: pipelineCounts.offer },\n { label: t('crewfoundry.dashboard.pipeline.hired', 'Hired'), value: pipelineCounts.hired },\n ];\n\n const employeeByName = useMemo(() => {\n const map = new Map<string, string | undefined>();\n for (const employee of employees) {\n map.set(`${employee.firstName} ${employee.lastName}`.trim().toLowerCase(), employee.avatar);\n }\n return map;\n }, [employees]);\n\n const scheduledInterviews = [...interviews]\n .filter((i) => i.status === 'scheduled')\n .sort((a, b) => `${a.date} ${a.time}`.localeCompare(`${b.date} ${b.time}`));\n const upcomingInterviews = scheduledInterviews.slice(0, 5);\n\n const sortedAnnouncements = useMemo(\n () =>\n [...announcements].sort((a, b) => {\n if (a.isPinned !== b.isPinned) {\n return a.isPinned ? -1 : 1;\n }\n return b.createdAt.localeCompare(a.createdAt);\n }),\n [announcements]\n );\n const topAnnouncements = sortedAnnouncements.slice(0, 4);\n\n const recentLeaveRequests = [...leaveRequests].sort((a, b) => {\n if (a.status === 'pending' && b.status !== 'pending') return -1;\n if (b.status === 'pending' && a.status !== 'pending') return 1;\n return b.startDate.localeCompare(a.startDate);\n });\n const {\n page: leavePage,\n pageSize: leavePageSize,\n offset: leaveOffset,\n setPage: setLeavePage,\n } = usePagination({ initialPageSize: 6 });\n const leavePageItems = recentLeaveRequests.slice(leaveOffset, leaveOffset + leavePageSize);\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"relative overflow-hidden rounded-xl border border-border-seam bg-gloss-card p-6\">\n <AuroraBackground intensity=\"subtle\" />\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>{t('crewfoundry.dashboard.title', 'Command Center')}</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">\n {t('crewfoundry.dashboard.subtitle', 'Your workforce at a glance')}\n </p>\n <PagePurpose className=\"mt-2\">\n {t(\n 'crewfoundry.dashboard.purpose',\n 'Your workforce command center — see headcount, hiring, performance, and projects at a glance, then jump straight to the area that needs your attention.'\n )}\n </PagePurpose>\n </div>\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button className=\"self-start\">\n <Plus className=\"mr-1 h-4 w-4\" />\n {t('crewfoundry.dashboard.new', 'New')}\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\" className=\"w-52\">\n <DropdownMenuLabel>{t('crewfoundry.dashboard.newMenu', 'Create')}</DropdownMenuLabel>\n <DropdownMenuSeparator />\n <DropdownMenuItem onClick={() => navigate('../hr/employees-page')}>\n <UserPlus className=\"mr-2 h-4 w-4\" />\n {t('crewfoundry.dashboard.newEmployee', 'Add employee')}\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => navigate('../hiring/create-job-page')}>\n <Briefcase className=\"mr-2 h-4 w-4\" />\n {t('crewfoundry.dashboard.newJob', 'Post a job')}\n </DropdownMenuItem>\n <DropdownMenuItem onClick={() => navigate('../projects/projects-page')}>\n <FolderPlus className=\"mr-2 h-4 w-4\" />\n {t('crewfoundry.dashboard.newProject', 'New project')}\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n </div>\n </div>\n\n <NextSteps\n storageKey=\"crewfoundry-dashboard\"\n steps={[\n {\n id: 'employees',\n label: t('crewfoundry.dashboard.next.employees', 'Review your team'),\n description: t(\n 'crewfoundry.dashboard.next.employeesDesc',\n 'Open the employee directory to see who is active and on leave.'\n ),\n icon: <Users className=\"h-4 w-4\" />,\n onClick: () => navigate('../hr/employees-page'),\n },\n {\n id: 'hiring',\n label: t('crewfoundry.dashboard.next.hiring', 'Post a new job'),\n description: t(\n 'crewfoundry.dashboard.next.hiringDesc',\n 'Create an opening to start collecting applicants.'\n ),\n icon: <Briefcase className=\"h-4 w-4\" />,\n onClick: () => navigate('../hiring/create-job-page'),\n },\n {\n id: 'projects',\n label: t('crewfoundry.dashboard.next.projects', 'Check active projects'),\n description: t(\n 'crewfoundry.dashboard.next.projectsDesc',\n 'Track progress and budgets across in-flight work.'\n ),\n icon: <Activity className=\"h-4 w-4\" />,\n onClick: () => navigate('../projects/projects-page'),\n },\n ]}\n />\n\n {/* Bento: KPI grid + headcount growth + upcoming interviews */}\n <div className=\"grid gap-4 lg:grid-cols-12\">\n <div className=\"grid gap-4 sm:grid-cols-2 lg:col-span-5\">\n <StatCard\n label={t('crewfoundry.dashboard.stats.totalEmployees', 'Total Employees')}\n value={employees.length}\n delta={headcountDelta || undefined}\n deltaLabel={t('crewfoundry.dashboard.stats.fromLastMonth', 'from last month')}\n icon={<Users className=\"h-4 w-4\" />}\n sparkline={headcountValues}\n sparklineVariant=\"area\"\n />\n <StatCard\n label={t('crewfoundry.dashboard.stats.openPositions', 'Open Positions')}\n value={openPositions}\n delta={seriesDeltaPercent(openingsSeries)}\n deltaLabel={t('crewfoundry.dashboard.stats.fromLastMonth', 'from last month')}\n icon={<Briefcase className=\"h-4 w-4\" />}\n sparkline={openingsSeries}\n sparklineVariant=\"area\"\n />\n <StatCard\n label={t('crewfoundry.dashboard.stats.activeProjects', 'Active Projects')}\n value={activeProjects}\n delta={seriesDeltaPercent(projectsSeries)}\n deltaLabel={t('crewfoundry.dashboard.stats.fromLastMonth', 'from last month')}\n icon={<Activity className=\"h-4 w-4\" />}\n sparkline={projectsSeries}\n sparklineVariant=\"area\"\n />\n <StatCard\n label={t('crewfoundry.dashboard.stats.avgPerformance', 'Avg Performance')}\n value={`${avgPerformance}`}\n icon={<TrendingUp className=\"h-4 w-4\" />}\n />\n </div>\n\n <Card className=\"lg:col-span-4\">\n <CardHeader>\n <CardTitle className=\"text-base\">\n {t('crewfoundry.dashboard.headcountGrowth.title', 'Headcount growth')}\n </CardTitle>\n </CardHeader>\n <CardContent>\n {headcountSeries.length > 1 ? (\n <AreaChart\n series={headcountSeries}\n height={210}\n title=\"Cumulative headcount by month\"\n />\n ) : (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={t('crewfoundry.dashboard.headcountGrowth.emptyTitle', 'Not enough history')}\n description={t(\n 'crewfoundry.dashboard.headcountGrowth.emptyDescription',\n 'Headcount growth appears once employees span more than one hiring month.'\n )}\n />\n )}\n </CardContent>\n </Card>\n\n <Card className=\"lg:col-span-3\">\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0\">\n <CardTitle className=\"text-base\">\n {t('crewfoundry.dashboard.upcomingInterviews.title', 'Upcoming interviews')}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {upcomingInterviews.length === 0 && (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={t(\n 'crewfoundry.dashboard.upcomingInterviews.emptyTitle',\n 'No interviews scheduled'\n )}\n description={t(\n 'crewfoundry.dashboard.upcomingInterviews.emptyDescription',\n 'Scheduled interviews will appear here.'\n )}\n />\n )}\n {upcomingInterviews.map((interview) => {\n const avatar = employeeByName.get(interview.candidateName.trim().toLowerCase());\n return (\n <div key={interview.id} className=\"flex items-center gap-3\">\n {avatar ? (\n <img\n src={avatar}\n alt=\"\"\n className=\"h-9 w-9 shrink-0 rounded-full object-cover\"\n />\n ) : (\n <InitialsAvatar name={interview.candidateName} />\n )}\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium\">{interview.candidateName}</p>\n <p className=\"truncate text-xs text-text-secondary\">{interview.position}</p>\n </div>\n <div className=\"flex shrink-0 items-center gap-1.5 text-right\">\n <div>\n <p className=\"text-xs font-medium\">{interview.time}</p>\n <p className=\"text-[10px] text-text-secondary\">{interview.date}</p>\n </div>\n <Calendar className=\"h-3.5 w-3.5 text-text-secondary\" />\n </div>\n </div>\n );\n })}\n {scheduledInterviews.length > upcomingInterviews.length && (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"w-full\"\n onClick={() => navigate('../hiring/interviews-page')}\n >\n {t('crewfoundry.dashboard.upcomingInterviews.viewAll', 'View All')}\n </Button>\n )}\n </CardContent>\n </Card>\n </div>\n\n {/* Hiring pipeline funnel + announcements rail */}\n <div className=\"grid gap-4 lg:grid-cols-12\">\n <Card className=\"lg:col-span-8\">\n <CardHeader>\n <CardTitle className=\"text-base\">\n {t('crewfoundry.dashboard.pipeline.title', 'Hiring pipeline')}\n </CardTitle>\n </CardHeader>\n <CardContent>\n {candidates.length > 0 ? (\n <HBarFunnel stages={pipeline} valueFormatter={(value) => `${value}`} />\n ) : (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={t('crewfoundry.dashboard.pipeline.emptyTitle', 'No candidates yet')}\n description={t(\n 'crewfoundry.dashboard.pipeline.emptyDescription',\n 'Your hiring funnel appears here once candidates enter the pipeline.'\n )}\n />\n )}\n </CardContent>\n </Card>\n\n <Card className=\"lg:col-span-4\">\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0\">\n <CardTitle className=\"text-base\">\n {t('crewfoundry.dashboard.announcements.title', 'Announcements')}\n </CardTitle>\n {sortedAnnouncements.length > topAnnouncements.length && (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => navigate('../communications/announcements-page')}\n >\n {t('crewfoundry.dashboard.announcements.viewAll', 'View All')}\n </Button>\n )}\n </CardHeader>\n <CardContent className=\"space-y-4\">\n {topAnnouncements.length === 0 && (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={t('crewfoundry.dashboard.announcements.emptyTitle', 'No announcements')}\n description={t(\n 'crewfoundry.dashboard.announcements.emptyDescription',\n 'Company announcements will appear here.'\n )}\n />\n )}\n {topAnnouncements.map((announcement) => {\n const expanded = expandedAnnouncements[announcement.id];\n const isLong = announcement.content.length > 120;\n return (\n <div key={announcement.id} className=\"space-y-1\">\n <div className=\"flex items-center gap-2\">\n <p className=\"text-sm font-medium\">{announcement.title}</p>\n {announcement.isPinned && (\n <Badge variant=\"outline\" className=\"text-[10px]\">\n {t('crewfoundry.dashboard.announcements.pinned', 'Pinned')}\n </Badge>\n )}\n </div>\n <p className=\"text-xs text-text-secondary\">\n {announcement.createdAt} · {announcement.author}\n </p>\n <p className=\"text-sm text-text-secondary\">\n {expanded || !isLong\n ? announcement.content\n : `${announcement.content.slice(0, 120)}…`}\n </p>\n {isLong && (\n <button\n type=\"button\"\n className=\"text-xs font-medium text-action-primary-bg hover:underline\"\n onClick={() =>\n setExpandedAnnouncements((prev) => ({\n ...prev,\n [announcement.id]: !prev[announcement.id],\n }))\n }\n >\n {expanded\n ? t('crewfoundry.dashboard.announcements.readLess', 'Read Less')\n : t('crewfoundry.dashboard.announcements.readMore', 'Read More')}\n </button>\n )}\n </div>\n );\n })}\n </CardContent>\n </Card>\n </div>\n\n {/* Leave approvals (real approve / reject) */}\n <Card>\n <CardHeader>\n <CardTitle className=\"text-base\">\n {t('crewfoundry.dashboard.leaveRequests.title', 'Recent Leave Requests')}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {leaveRequests.length === 0 && (\n <IllustratedEmptyState\n illustration=\"empty-data\"\n title={t('crewfoundry.dashboard.leaveRequests.emptyTitle', 'No leave requests')}\n description={t(\n 'crewfoundry.dashboard.leaveRequests.emptyDescription',\n 'Pending leave requests will appear here.'\n )}\n />\n )}\n {leavePageItems.map((leave) => (\n <div key={leave.id} className=\"flex items-center justify-between rounded border p-3\">\n <div className=\"flex items-center gap-3\">\n <InitialsAvatar name={leave.employeeName} size={34} />\n <div>\n <p className=\"text-sm font-medium\">{leave.employeeName}</p>\n <div className=\"flex items-center gap-2 text-xs text-text-secondary\">\n <Calendar className=\"h-3 w-3\" />\n <span>\n {leave.startDate} - {leave.endDate}\n </span>\n </div>\n </div>\n </div>\n <div className=\"flex items-center gap-2\">\n {leave.status === 'pending' && (\n <div className=\"flex gap-1\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n className=\"h-7 text-xs\"\n disabled={processingLeaveId === leave.id}\n onClick={() => void decideLeave(leave.id, 'rejected')}\n >\n {t('crewfoundry.dashboard.leaveRequests.reject', 'Reject')}\n </Button>\n <Button\n size=\"sm\"\n className=\"h-7 text-xs\"\n disabled={processingLeaveId === leave.id}\n onClick={() => void decideLeave(leave.id, 'approved')}\n >\n {t('crewfoundry.dashboard.leaveRequests.approve', 'Approve')}\n </Button>\n </div>\n )}\n <Badge\n variant={\n leave.status === 'approved'\n ? 'default'\n : leave.status === 'pending'\n ? 'secondary'\n : 'destructive'\n }\n >\n {formatEnumLabel(leave.status)}\n </Badge>\n <Badge variant=\"outline\">{formatEnumLabel(leave.type)}</Badge>\n </div>\n </div>\n ))}\n {recentLeaveRequests.length > leavePageSize && (\n <Pagination\n page={leavePage}\n pageSize={leavePageSize}\n totalItems={recentLeaveRequests.length}\n onPageChange={setLeavePage}\n />\n )}\n </CardContent>\n </Card>\n\n <p className=\"text-center text-xs text-text-muted\">\n {activeEmployees} {t('crewfoundry.dashboard.stats.active', 'active')} · {onLeave}{' '}\n {t('crewfoundry.dashboard.stats.onLeave', 'on leave')} · {totalApplicants}{' '}\n {t('crewfoundry.dashboard.stats.totalApplicants', 'total applicants')} · {pendingLeaves}{' '}\n {t('crewfoundry.dashboard.stats.pendingLeaveRequests', 'pending leave requests')}\n </p>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -3,7 +3,7 @@ import { useCrewFoundryStore as t } from "../store/crewfoundryStore.js";
|
|
|
3
3
|
import { formatEnumLabel as n } from "../utils/formatEnumLabel.js";
|
|
4
4
|
import { useMemo as r, useState as i } from "react";
|
|
5
5
|
import { AlertTriangle as a, ArrowUpRight as o, Award as s, Bell as c, Briefcase as l, CheckCircle as u, MapPin as ee, Star as te, Users as d, Zap as f } from "lucide-react";
|
|
6
|
-
import { useNavigate as p } from "react-router
|
|
6
|
+
import { useNavigate as p } from "react-router";
|
|
7
7
|
import { jsx as m, jsxs as h } from "react/jsx-runtime";
|
|
8
8
|
import { Badge as g, Button as _, Card as v, CardContent as y, CardDescription as b, CardHeader as x, CardTitle as S, EmphasisPanel as C, GradientText as w, NextSteps as T, PagePurpose as E, ResponsiveTable as D, Separator as O } from "@burdenoff/fe-libs/ui";
|
|
9
9
|
import { useGlobalUi as ne } from "@burdenoff/fe-libs/shared/providers/shell";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DashboardEnhancedDashboardPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/DashboardEnhancedDashboardPagePage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport {\n Badge,\n Button,\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n EmphasisPanel,\n GradientText,\n NextSteps,\n PagePurpose,\n ResponsiveTable,\n type ResponsiveTableColumn,\n Separator,\n} from '@burdenoff/fe-libs/ui';\nimport {\n AlertTriangle,\n ArrowUpRight,\n Award,\n Bell,\n Briefcase,\n CheckCircle,\n MapPin,\n Star,\n Users,\n Zap,\n} from 'lucide-react';\nimport { useNavigate } from 'react-router-dom';\nimport { useGlobalUi } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { useCrewFoundryStore, type Employee } from '../store/crewfoundryStore';\nimport { useCrewFoundry } from '../providers/CrewFoundryProvider';\nimport { formatEnumLabel } from '../utils/formatEnumLabel';\n\nexport function DashboardEnhancedDashboardPagePage() {\n const navigate = useNavigate();\n const { showToast } = useGlobalUi();\n const { currentUser } = useCrewFoundry();\n const {\n employees,\n jobPostings,\n candidates,\n interviews,\n leaveRequests,\n notifications,\n projects,\n announcements,\n updateLeaveRequest,\n markAllNotificationsRead,\n } = useCrewFoundryStore();\n const [processingLeaveId, setProcessingLeaveId] = useState<string | null>(null);\n const [markingRead, setMarkingRead] = useState(false);\n\n const handleMarkAllRead = async () => {\n setMarkingRead(true);\n try {\n await markAllNotificationsRead();\n showToast('All notifications marked as read', 'success');\n } catch (error) {\n showToast(error instanceof Error ? error.message : 'Unable to update notifications', 'error');\n } finally {\n setMarkingRead(false);\n }\n };\n\n const approverName =\n currentUser?.name ||\n [currentUser?.firstName, currentUser?.lastName].filter(Boolean).join(' ') ||\n currentUser?.email ||\n 'Manager';\n\n const decideLeave = async (id: string, status: 'approved' | 'rejected') => {\n setProcessingLeaveId(id);\n try {\n await updateLeaveRequest(id, { status, approverName });\n showToast(status === 'approved' ? 'Leave approved' : 'Leave rejected', 'success');\n } catch (error) {\n showToast(error instanceof Error ? error.message : 'Unable to update leave request', 'error');\n } finally {\n setProcessingLeaveId(null);\n }\n };\n\n const quickActions = useMemo(\n () => [\n {\n label: `Post New Job (${jobPostings.filter((job) => job.status === 'active').length} active)`,\n icon: Briefcase,\n color: 'text-status-info-text',\n to: '../../hiring/create-job-page',\n },\n {\n label: `Review Candidates (${candidates.filter((candidate) => candidate.status === 'new').length})`,\n icon: Users,\n color: 'text-action-primary-bg',\n to: '../../hiring/candidates-page',\n },\n {\n label: `Approve Leaves (${leaveRequests.filter((leave) => leave.status === 'pending').length})`,\n icon: CheckCircle,\n color: 'text-status-success-text',\n to: '../../hr/leave-management-page',\n },\n {\n label: `Run Payroll (${projects.filter((project) => project.status === 'active').length} projects)`,\n icon: Zap,\n color: 'text-status-warning-text',\n to: '../../hr/payroll-page',\n },\n ],\n [candidates, jobPostings, leaveRequests, projects]\n );\n\n // Only goals whose current value is derived from real store data.\n const companyGoals = useMemo(() => {\n const hired = candidates.filter((candidate) => candidate.status === 'hired').length;\n const avgPerformance =\n employees.reduce((sum, employee) => sum + employee.performanceScore, 0) /\n Math.max(employees.length, 1);\n const activeShare = Math.round(\n (employees.filter((employee) => employee.status === 'active').length /\n Math.max(employees.length, 1)) *\n 100\n );\n return [\n {\n goal: 'Fill 30 open positions',\n current: hired,\n target: 30,\n unit: 'positions',\n },\n {\n goal: 'Reach 90% average performance',\n current: Math.round(avgPerformance * 20),\n target: 90,\n unit: '%',\n },\n {\n goal: 'Keep 95% of workforce active',\n current: activeShare,\n target: 95,\n unit: '%',\n },\n ];\n }, [candidates, employees]);\n\n const upcomingInterviews = interviews.filter((i) => i.status === 'scheduled');\n const pendingLeaves = leaveRequests.filter((l) => l.status === 'pending');\n const unreadNotifs = notifications.filter((n) => !n.read);\n const pinnedAnnouncements = announcements.filter((a) => a.isPinned);\n const topPerformers = [...employees]\n .sort((a, b) => b.performanceScore - a.performanceScore)\n .slice(0, 5);\n const recentHires = [...employees]\n .sort((a, b) => b.startDate.localeCompare(a.startDate))\n .slice(0, 5);\n\n const locationMap = employees.reduce<Record<string, number>>((acc, e) => {\n acc[e.location] = (acc[e.location] || 0) + 1;\n return acc;\n }, {});\n\n const recentHireColumns: ResponsiveTableColumn<Employee>[] = [\n {\n key: 'name',\n header: 'Name',\n priority: 'primary',\n cell: (emp) => (\n <span className=\"font-medium\">\n {emp.firstName} {emp.lastName}\n </span>\n ),\n },\n {\n key: 'position',\n header: 'Position',\n priority: 'secondary',\n cell: (emp) => <span className=\"text-text-secondary\">{emp.position}</span>,\n },\n {\n key: 'department',\n header: 'Department',\n priority: 'secondary',\n cell: (emp) => <span>{emp.department}</span>,\n },\n {\n key: 'location',\n header: 'Location',\n priority: 'hidden-on-mobile',\n cell: (emp) => <span className=\"text-text-secondary\">{emp.location}</span>,\n },\n {\n key: 'startDate',\n header: 'Start Date',\n priority: 'secondary',\n cell: (emp) => <span>{emp.startDate}</span>,\n },\n {\n key: 'type',\n header: 'Type',\n priority: 'primary',\n cell: (emp) => (\n <Badge variant=\"outline\" className=\"w-fit text-[10px]\">\n {emp.type.replace('_', ' ')}\n </Badge>\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>Enhanced Dashboard</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">Complete workforce command center</p>\n <PagePurpose className=\"mt-1\">\n A richer, customizable command center for your workforce — see live metrics, activity,\n and trends tailored to how you run your organization.\n </PagePurpose>\n </div>\n <div className=\"flex items-center gap-3\">\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"relative\"\n disabled={unreadNotifs.length === 0 || markingRead}\n aria-label={\n unreadNotifs.length > 0\n ? `${unreadNotifs.length} unread notifications, mark all as read`\n : 'No unread notifications'\n }\n title={\n unreadNotifs.length > 0 ? 'Mark all notifications as read' : 'No unread notifications'\n }\n onClick={() => void handleMarkAllRead()}\n >\n <Bell className=\"h-5 w-5 text-text-secondary\" />\n {unreadNotifs.length > 0 && (\n <span className=\"absolute -top-1 -right-1 h-4 w-4 rounded-full bg-status-error-text text-[10px] text-text-inverse flex items-center justify-center\">\n {unreadNotifs.length}\n </span>\n )}\n </Button>\n <Button size=\"sm\" onClick={() => navigate('../hr/leave-management-page')}>\n <Zap className=\"h-4 w-4 mr-1\" />\n Quick Actions\n </Button>\n </div>\n </div>\n\n <NextSteps\n storageKey=\"crewfoundry-enhanced-dashboard\"\n steps={[\n {\n id: 'hiring',\n label: 'Move hiring forward',\n description: 'Review open roles and applicants in your pipeline.',\n icon: <Briefcase className=\"h-4 w-4\" />,\n onClick: () => navigate('../hiring/jobs-page'),\n },\n {\n id: 'people',\n label: 'Check on your people',\n description: 'See who is active, on leave, or needs attention.',\n icon: <Users className=\"h-4 w-4\" />,\n onClick: () => navigate('../hr/employees-page'),\n },\n {\n id: 'analytics',\n label: 'Dig into analytics',\n description: 'Explore deeper workforce and hiring trends.',\n icon: <ArrowUpRight className=\"h-4 w-4\" />,\n onClick: () => navigate('../analytics/analytics-page'),\n },\n ]}\n />\n\n <div className=\"grid gap-3 md:grid-cols-4\">\n {quickActions.map((action) => (\n <Card\n key={action.label}\n role=\"button\"\n tabIndex={0}\n aria-label={action.label}\n className=\"cursor-pointer hover:shadow-md transition-shadow\"\n onClick={() => navigate(action.to)}\n onKeyDown={(event) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n navigate(action.to);\n }\n }}\n >\n <CardContent className=\"flex items-center gap-3 p-4\">\n <action.icon className={`h-5 w-5 ${action.color}`} />\n <span className=\"text-sm font-medium\">{action.label}</span>\n <ArrowUpRight className=\"h-4 w-4 ml-auto text-text-secondary\" />\n </CardContent>\n </Card>\n ))}\n </div>\n\n {pinnedAnnouncements.length > 0 && (\n <Card className=\"border-status-warning-text/30 bg-status-warning-bg/50\">\n <CardContent className=\"p-4\">\n {pinnedAnnouncements.map((ann) => (\n <div key={ann.id} className=\"flex items-start gap-3\">\n <AlertTriangle className=\"h-4 w-4 text-status-warning-text mt-0.5\" />\n <div>\n <p className=\"text-sm font-medium\">{ann.title}</p>\n <p className=\"text-xs text-text-secondary mt-1\">{ann.content.slice(0, 120)}...</p>\n <p className=\"text-xs text-text-secondary mt-1\">\n — {ann.author}, {ann.authorRole}\n </p>\n </div>\n <Badge variant=\"outline\" className=\"ml-auto flex-shrink-0\">\n {formatEnumLabel(ann.priority)}\n </Badge>\n </div>\n ))}\n </CardContent>\n </Card>\n )}\n\n <div className=\"grid gap-4 md:grid-cols-3\">\n <EmphasisPanel>\n <div className=\"text-base font-semibold\">Company Goals</div>\n <p className=\"mb-4 text-sm text-text-secondary\">Q1 2026 targets</p>\n <div className=\"space-y-4\">\n {companyGoals.map((g) => {\n const pct = Math.round((g.current / g.target) * 100);\n return (\n <div key={g.goal} className=\"space-y-1\">\n <div className=\"flex justify-between text-xs\">\n <span className=\"text-text-secondary\">{g.goal}</span>\n <span className=\"font-medium\">\n {g.current}/{g.target} {g.unit}\n </span>\n </div>\n <div className=\"h-2 bg-bg-sunken rounded overflow-hidden\">\n <div\n className={`h-2 rounded ${pct >= 90 ? 'bg-status-success-text' : pct >= 70 ? 'bg-status-warning-text' : 'bg-status-error-text'}`}\n style={{ width: `${Math.min(pct, 100)}%` }}\n />\n </div>\n </div>\n );\n })}\n </div>\n </EmphasisPanel>\n\n <Card>\n <CardHeader>\n <CardTitle className=\"text-base\">Upcoming Interviews</CardTitle>\n <CardDescription>{upcomingInterviews.length} scheduled</CardDescription>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {upcomingInterviews.map((interview) => (\n <div key={interview.id} className=\"flex items-center gap-3 rounded border p-2\">\n <div className=\"flex-1 min-w-0\">\n <p className=\"text-sm font-medium truncate\">{interview.candidateName}</p>\n <p className=\"text-xs text-text-secondary\">{interview.position}</p>\n </div>\n <div className=\"text-right flex-shrink-0\">\n <p className=\"text-xs font-medium\">{interview.date}</p>\n <p className=\"text-xs text-text-secondary\">{interview.time}</p>\n </div>\n <Badge variant=\"outline\" className=\"text-[10px]\">\n {formatEnumLabel(interview.type)}\n </Badge>\n </div>\n ))}\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle className=\"text-base\">Pending Actions</CardTitle>\n <CardDescription>Items requiring your attention</CardDescription>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {pendingLeaves.map((leave) => (\n <div key={leave.id} className=\"flex items-center justify-between rounded border p-2\">\n <div>\n <p className=\"text-sm font-medium\">{leave.employeeName}</p>\n <p className=\"text-xs text-text-secondary\">\n {formatEnumLabel(leave.type)} leave · {leave.days} days\n </p>\n </div>\n <div className=\"flex gap-1\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n className=\"h-7 text-xs\"\n disabled={processingLeaveId === leave.id}\n onClick={() => void decideLeave(leave.id, 'rejected')}\n >\n Deny\n </Button>\n <Button\n size=\"sm\"\n className=\"h-7 text-xs\"\n disabled={processingLeaveId === leave.id}\n onClick={() => void decideLeave(leave.id, 'approved')}\n >\n Approve\n </Button>\n </div>\n </div>\n ))}\n <Separator />\n <div className=\"text-xs text-text-secondary text-center\">\n {candidates.filter((c) => c.status === 'new').length} new applications to review\n </div>\n </CardContent>\n </Card>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-2\">\n <Card>\n <CardHeader>\n <CardTitle className=\"text-base flex items-center gap-2\">\n <Award className=\"h-4 w-4 text-status-warning-text\" />\n Top Performers\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {topPerformers.map((emp, idx) => (\n <div key={emp.id} className=\"flex items-center gap-3\">\n <span className=\"text-sm font-bold text-text-secondary w-5\">#{idx + 1}</span>\n <div className=\"flex-1\">\n <p className=\"text-sm font-medium\">\n {emp.firstName} {emp.lastName}\n </p>\n <p className=\"text-xs text-text-secondary\">\n {emp.position} · {emp.department}\n </p>\n </div>\n <div className=\"flex items-center gap-1\">\n <Star className=\"h-3 w-3 text-status-warning-text\" />\n <span className=\"text-sm font-medium\">{emp.performanceScore}</span>\n </div>\n </div>\n ))}\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle className=\"text-base flex items-center gap-2\">\n <MapPin className=\"h-4 w-4\" />\n Workforce Locations\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {Object.entries(locationMap)\n .sort(([, a], [, b]) => b - a)\n .map(([location, count]) => (\n <div key={location} className=\"flex items-center justify-between\">\n <span className=\"text-sm\">{location}</span>\n <div className=\"flex items-center gap-2\">\n <span className=\"text-sm font-medium\">{count}</span>\n <span className=\"text-xs text-text-secondary\">\n ({Math.round((count / Math.max(employees.length, 1)) * 100)}%)\n </span>\n </div>\n </div>\n ))}\n </CardContent>\n </Card>\n </div>\n\n <Card>\n <CardHeader>\n <CardTitle>Recent Hires</CardTitle>\n <CardDescription>Newest team members</CardDescription>\n </CardHeader>\n <CardContent>\n <ResponsiveTable\n columns={recentHireColumns}\n data={recentHires}\n rowKey={(emp) => emp.id}\n />\n </CardContent>\n </Card>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"DashboardEnhancedDashboardPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/DashboardEnhancedDashboardPagePage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport {\n Badge,\n Button,\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n EmphasisPanel,\n GradientText,\n NextSteps,\n PagePurpose,\n ResponsiveTable,\n type ResponsiveTableColumn,\n Separator,\n} from '@burdenoff/fe-libs/ui';\nimport {\n AlertTriangle,\n ArrowUpRight,\n Award,\n Bell,\n Briefcase,\n CheckCircle,\n MapPin,\n Star,\n Users,\n Zap,\n} from 'lucide-react';\nimport { useNavigate } from 'react-router';\nimport { useGlobalUi } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { useCrewFoundryStore, type Employee } from '../store/crewfoundryStore';\nimport { useCrewFoundry } from '../providers/CrewFoundryProvider';\nimport { formatEnumLabel } from '../utils/formatEnumLabel';\n\nexport function DashboardEnhancedDashboardPagePage() {\n const navigate = useNavigate();\n const { showToast } = useGlobalUi();\n const { currentUser } = useCrewFoundry();\n const {\n employees,\n jobPostings,\n candidates,\n interviews,\n leaveRequests,\n notifications,\n projects,\n announcements,\n updateLeaveRequest,\n markAllNotificationsRead,\n } = useCrewFoundryStore();\n const [processingLeaveId, setProcessingLeaveId] = useState<string | null>(null);\n const [markingRead, setMarkingRead] = useState(false);\n\n const handleMarkAllRead = async () => {\n setMarkingRead(true);\n try {\n await markAllNotificationsRead();\n showToast('All notifications marked as read', 'success');\n } catch (error) {\n showToast(error instanceof Error ? error.message : 'Unable to update notifications', 'error');\n } finally {\n setMarkingRead(false);\n }\n };\n\n const approverName =\n currentUser?.name ||\n [currentUser?.firstName, currentUser?.lastName].filter(Boolean).join(' ') ||\n currentUser?.email ||\n 'Manager';\n\n const decideLeave = async (id: string, status: 'approved' | 'rejected') => {\n setProcessingLeaveId(id);\n try {\n await updateLeaveRequest(id, { status, approverName });\n showToast(status === 'approved' ? 'Leave approved' : 'Leave rejected', 'success');\n } catch (error) {\n showToast(error instanceof Error ? error.message : 'Unable to update leave request', 'error');\n } finally {\n setProcessingLeaveId(null);\n }\n };\n\n const quickActions = useMemo(\n () => [\n {\n label: `Post New Job (${jobPostings.filter((job) => job.status === 'active').length} active)`,\n icon: Briefcase,\n color: 'text-status-info-text',\n to: '../../hiring/create-job-page',\n },\n {\n label: `Review Candidates (${candidates.filter((candidate) => candidate.status === 'new').length})`,\n icon: Users,\n color: 'text-action-primary-bg',\n to: '../../hiring/candidates-page',\n },\n {\n label: `Approve Leaves (${leaveRequests.filter((leave) => leave.status === 'pending').length})`,\n icon: CheckCircle,\n color: 'text-status-success-text',\n to: '../../hr/leave-management-page',\n },\n {\n label: `Run Payroll (${projects.filter((project) => project.status === 'active').length} projects)`,\n icon: Zap,\n color: 'text-status-warning-text',\n to: '../../hr/payroll-page',\n },\n ],\n [candidates, jobPostings, leaveRequests, projects]\n );\n\n // Only goals whose current value is derived from real store data.\n const companyGoals = useMemo(() => {\n const hired = candidates.filter((candidate) => candidate.status === 'hired').length;\n const avgPerformance =\n employees.reduce((sum, employee) => sum + employee.performanceScore, 0) /\n Math.max(employees.length, 1);\n const activeShare = Math.round(\n (employees.filter((employee) => employee.status === 'active').length /\n Math.max(employees.length, 1)) *\n 100\n );\n return [\n {\n goal: 'Fill 30 open positions',\n current: hired,\n target: 30,\n unit: 'positions',\n },\n {\n goal: 'Reach 90% average performance',\n current: Math.round(avgPerformance * 20),\n target: 90,\n unit: '%',\n },\n {\n goal: 'Keep 95% of workforce active',\n current: activeShare,\n target: 95,\n unit: '%',\n },\n ];\n }, [candidates, employees]);\n\n const upcomingInterviews = interviews.filter((i) => i.status === 'scheduled');\n const pendingLeaves = leaveRequests.filter((l) => l.status === 'pending');\n const unreadNotifs = notifications.filter((n) => !n.read);\n const pinnedAnnouncements = announcements.filter((a) => a.isPinned);\n const topPerformers = [...employees]\n .sort((a, b) => b.performanceScore - a.performanceScore)\n .slice(0, 5);\n const recentHires = [...employees]\n .sort((a, b) => b.startDate.localeCompare(a.startDate))\n .slice(0, 5);\n\n const locationMap = employees.reduce<Record<string, number>>((acc, e) => {\n acc[e.location] = (acc[e.location] || 0) + 1;\n return acc;\n }, {});\n\n const recentHireColumns: ResponsiveTableColumn<Employee>[] = [\n {\n key: 'name',\n header: 'Name',\n priority: 'primary',\n cell: (emp) => (\n <span className=\"font-medium\">\n {emp.firstName} {emp.lastName}\n </span>\n ),\n },\n {\n key: 'position',\n header: 'Position',\n priority: 'secondary',\n cell: (emp) => <span className=\"text-text-secondary\">{emp.position}</span>,\n },\n {\n key: 'department',\n header: 'Department',\n priority: 'secondary',\n cell: (emp) => <span>{emp.department}</span>,\n },\n {\n key: 'location',\n header: 'Location',\n priority: 'hidden-on-mobile',\n cell: (emp) => <span className=\"text-text-secondary\">{emp.location}</span>,\n },\n {\n key: 'startDate',\n header: 'Start Date',\n priority: 'secondary',\n cell: (emp) => <span>{emp.startDate}</span>,\n },\n {\n key: 'type',\n header: 'Type',\n priority: 'primary',\n cell: (emp) => (\n <Badge variant=\"outline\" className=\"w-fit text-[10px]\">\n {emp.type.replace('_', ' ')}\n </Badge>\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>Enhanced Dashboard</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">Complete workforce command center</p>\n <PagePurpose className=\"mt-1\">\n A richer, customizable command center for your workforce — see live metrics, activity,\n and trends tailored to how you run your organization.\n </PagePurpose>\n </div>\n <div className=\"flex items-center gap-3\">\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"relative\"\n disabled={unreadNotifs.length === 0 || markingRead}\n aria-label={\n unreadNotifs.length > 0\n ? `${unreadNotifs.length} unread notifications, mark all as read`\n : 'No unread notifications'\n }\n title={\n unreadNotifs.length > 0 ? 'Mark all notifications as read' : 'No unread notifications'\n }\n onClick={() => void handleMarkAllRead()}\n >\n <Bell className=\"h-5 w-5 text-text-secondary\" />\n {unreadNotifs.length > 0 && (\n <span className=\"absolute -top-1 -right-1 h-4 w-4 rounded-full bg-status-error-text text-[10px] text-text-inverse flex items-center justify-center\">\n {unreadNotifs.length}\n </span>\n )}\n </Button>\n <Button size=\"sm\" onClick={() => navigate('../hr/leave-management-page')}>\n <Zap className=\"h-4 w-4 mr-1\" />\n Quick Actions\n </Button>\n </div>\n </div>\n\n <NextSteps\n storageKey=\"crewfoundry-enhanced-dashboard\"\n steps={[\n {\n id: 'hiring',\n label: 'Move hiring forward',\n description: 'Review open roles and applicants in your pipeline.',\n icon: <Briefcase className=\"h-4 w-4\" />,\n onClick: () => navigate('../hiring/jobs-page'),\n },\n {\n id: 'people',\n label: 'Check on your people',\n description: 'See who is active, on leave, or needs attention.',\n icon: <Users className=\"h-4 w-4\" />,\n onClick: () => navigate('../hr/employees-page'),\n },\n {\n id: 'analytics',\n label: 'Dig into analytics',\n description: 'Explore deeper workforce and hiring trends.',\n icon: <ArrowUpRight className=\"h-4 w-4\" />,\n onClick: () => navigate('../analytics/analytics-page'),\n },\n ]}\n />\n\n <div className=\"grid gap-3 md:grid-cols-4\">\n {quickActions.map((action) => (\n <Card\n key={action.label}\n role=\"button\"\n tabIndex={0}\n aria-label={action.label}\n className=\"cursor-pointer hover:shadow-md transition-shadow\"\n onClick={() => navigate(action.to)}\n onKeyDown={(event) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n navigate(action.to);\n }\n }}\n >\n <CardContent className=\"flex items-center gap-3 p-4\">\n <action.icon className={`h-5 w-5 ${action.color}`} />\n <span className=\"text-sm font-medium\">{action.label}</span>\n <ArrowUpRight className=\"h-4 w-4 ml-auto text-text-secondary\" />\n </CardContent>\n </Card>\n ))}\n </div>\n\n {pinnedAnnouncements.length > 0 && (\n <Card className=\"border-status-warning-text/30 bg-status-warning-bg/50\">\n <CardContent className=\"p-4\">\n {pinnedAnnouncements.map((ann) => (\n <div key={ann.id} className=\"flex items-start gap-3\">\n <AlertTriangle className=\"h-4 w-4 text-status-warning-text mt-0.5\" />\n <div>\n <p className=\"text-sm font-medium\">{ann.title}</p>\n <p className=\"text-xs text-text-secondary mt-1\">{ann.content.slice(0, 120)}...</p>\n <p className=\"text-xs text-text-secondary mt-1\">\n — {ann.author}, {ann.authorRole}\n </p>\n </div>\n <Badge variant=\"outline\" className=\"ml-auto flex-shrink-0\">\n {formatEnumLabel(ann.priority)}\n </Badge>\n </div>\n ))}\n </CardContent>\n </Card>\n )}\n\n <div className=\"grid gap-4 md:grid-cols-3\">\n <EmphasisPanel>\n <div className=\"text-base font-semibold\">Company Goals</div>\n <p className=\"mb-4 text-sm text-text-secondary\">Q1 2026 targets</p>\n <div className=\"space-y-4\">\n {companyGoals.map((g) => {\n const pct = Math.round((g.current / g.target) * 100);\n return (\n <div key={g.goal} className=\"space-y-1\">\n <div className=\"flex justify-between text-xs\">\n <span className=\"text-text-secondary\">{g.goal}</span>\n <span className=\"font-medium\">\n {g.current}/{g.target} {g.unit}\n </span>\n </div>\n <div className=\"h-2 bg-bg-sunken rounded overflow-hidden\">\n <div\n className={`h-2 rounded ${pct >= 90 ? 'bg-status-success-text' : pct >= 70 ? 'bg-status-warning-text' : 'bg-status-error-text'}`}\n style={{ width: `${Math.min(pct, 100)}%` }}\n />\n </div>\n </div>\n );\n })}\n </div>\n </EmphasisPanel>\n\n <Card>\n <CardHeader>\n <CardTitle className=\"text-base\">Upcoming Interviews</CardTitle>\n <CardDescription>{upcomingInterviews.length} scheduled</CardDescription>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {upcomingInterviews.map((interview) => (\n <div key={interview.id} className=\"flex items-center gap-3 rounded border p-2\">\n <div className=\"flex-1 min-w-0\">\n <p className=\"text-sm font-medium truncate\">{interview.candidateName}</p>\n <p className=\"text-xs text-text-secondary\">{interview.position}</p>\n </div>\n <div className=\"text-right flex-shrink-0\">\n <p className=\"text-xs font-medium\">{interview.date}</p>\n <p className=\"text-xs text-text-secondary\">{interview.time}</p>\n </div>\n <Badge variant=\"outline\" className=\"text-[10px]\">\n {formatEnumLabel(interview.type)}\n </Badge>\n </div>\n ))}\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle className=\"text-base\">Pending Actions</CardTitle>\n <CardDescription>Items requiring your attention</CardDescription>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {pendingLeaves.map((leave) => (\n <div key={leave.id} className=\"flex items-center justify-between rounded border p-2\">\n <div>\n <p className=\"text-sm font-medium\">{leave.employeeName}</p>\n <p className=\"text-xs text-text-secondary\">\n {formatEnumLabel(leave.type)} leave · {leave.days} days\n </p>\n </div>\n <div className=\"flex gap-1\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n className=\"h-7 text-xs\"\n disabled={processingLeaveId === leave.id}\n onClick={() => void decideLeave(leave.id, 'rejected')}\n >\n Deny\n </Button>\n <Button\n size=\"sm\"\n className=\"h-7 text-xs\"\n disabled={processingLeaveId === leave.id}\n onClick={() => void decideLeave(leave.id, 'approved')}\n >\n Approve\n </Button>\n </div>\n </div>\n ))}\n <Separator />\n <div className=\"text-xs text-text-secondary text-center\">\n {candidates.filter((c) => c.status === 'new').length} new applications to review\n </div>\n </CardContent>\n </Card>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-2\">\n <Card>\n <CardHeader>\n <CardTitle className=\"text-base flex items-center gap-2\">\n <Award className=\"h-4 w-4 text-status-warning-text\" />\n Top Performers\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {topPerformers.map((emp, idx) => (\n <div key={emp.id} className=\"flex items-center gap-3\">\n <span className=\"text-sm font-bold text-text-secondary w-5\">#{idx + 1}</span>\n <div className=\"flex-1\">\n <p className=\"text-sm font-medium\">\n {emp.firstName} {emp.lastName}\n </p>\n <p className=\"text-xs text-text-secondary\">\n {emp.position} · {emp.department}\n </p>\n </div>\n <div className=\"flex items-center gap-1\">\n <Star className=\"h-3 w-3 text-status-warning-text\" />\n <span className=\"text-sm font-medium\">{emp.performanceScore}</span>\n </div>\n </div>\n ))}\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle className=\"text-base flex items-center gap-2\">\n <MapPin className=\"h-4 w-4\" />\n Workforce Locations\n </CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {Object.entries(locationMap)\n .sort(([, a], [, b]) => b - a)\n .map(([location, count]) => (\n <div key={location} className=\"flex items-center justify-between\">\n <span className=\"text-sm\">{location}</span>\n <div className=\"flex items-center gap-2\">\n <span className=\"text-sm font-medium\">{count}</span>\n <span className=\"text-xs text-text-secondary\">\n ({Math.round((count / Math.max(employees.length, 1)) * 100)}%)\n </span>\n </div>\n </div>\n ))}\n </CardContent>\n </Card>\n </div>\n\n <Card>\n <CardHeader>\n <CardTitle>Recent Hires</CardTitle>\n <CardDescription>Newest team members</CardDescription>\n </CardHeader>\n <CardContent>\n <ResponsiveTable\n columns={recentHireColumns}\n data={recentHires}\n rowKey={(emp) => emp.id}\n />\n </CardContent>\n </Card>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useCrewFoundryStore as e } from "../store/crewfoundryStore.js";
|
|
2
2
|
import { formatEnumLabel as t } from "../utils/formatEnumLabel.js";
|
|
3
3
|
import { Clock as n, FileText as r, Star as i, UserCheck as a, Users as o, X as s } from "lucide-react";
|
|
4
|
-
import { useSearchParams as c } from "react-router
|
|
4
|
+
import { useSearchParams as c } from "react-router";
|
|
5
5
|
import { jsx as l, jsxs as u } from "react/jsx-runtime";
|
|
6
6
|
import { Badge as d, Button as f, Card as p, CardContent as m, CardDescription as h, CardHeader as g, CardTitle as _, GlassCard as v, GradientText as y, IllustratedEmptyState as b, PagePurpose as x, Pagination as S, ResponsiveTable as C } from "@burdenoff/fe-libs/ui";
|
|
7
7
|
import { usePagination as w } from "@burdenoff/fe-libs/shared/hooks";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HiringApplicationsPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/HiringApplicationsPagePage.tsx"],"sourcesContent":["import {\n Badge,\n Button,\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} from '@burdenoff/fe-libs/ui';\nimport { usePagination } from '@burdenoff/fe-libs/shared/hooks';\nimport { Clock, FileText, Star, UserCheck, Users, X } from 'lucide-react';\nimport { useSearchParams } from 'react-router-dom';\nimport { useCrewFoundryStore, type Candidate } from '../store/crewfoundryStore';\nimport { formatEnumLabel } from '../utils/formatEnumLabel';\n\nconst CANDIDATE_COLUMNS: ResponsiveTableColumn<Candidate>[] = [\n {\n key: 'candidate',\n header: 'Candidate',\n priority: 'primary',\n cell: (c) => (\n <div>\n <p className=\"font-medium\">\n {c.firstName} {c.lastName}\n </p>\n <p className=\"text-xs text-text-secondary\">{c.email}</p>\n </div>\n ),\n },\n {\n key: 'position',\n header: 'Position',\n priority: 'secondary',\n cell: (c) => <span className=\"text-text-secondary\">{c.position}</span>,\n },\n {\n key: 'company',\n header: 'Company',\n priority: 'secondary',\n cell: (c) => <span className=\"text-text-secondary\">{c.currentCompany}</span>,\n },\n {\n key: 'experience',\n header: 'Experience',\n priority: 'hidden-on-mobile',\n cell: (c) => <span>{c.experience} yrs</span>,\n },\n {\n key: 'rating',\n header: 'Rating',\n priority: 'hidden-on-mobile',\n cell: (c) => (\n <span className=\"flex items-center gap-1\">\n <Star className=\"h-3 w-3 text-status-warning-text\" />\n {c.rating}\n </span>\n ),\n },\n {\n key: 'skills',\n header: 'Skills',\n priority: 'secondary',\n cell: (c) => (\n <div className=\"flex flex-wrap gap-1\">\n {c.skills.slice(0, 2).map((s) => (\n <Badge key={s} variant=\"outline\" className=\"text-[9px]\">\n {s}\n </Badge>\n ))}\n </div>\n ),\n },\n {\n key: 'status',\n header: 'Status',\n priority: 'secondary',\n cell: (c) => (\n <Badge\n variant={\n c.status === 'hired'\n ? 'default'\n : c.status === 'offer'\n ? 'default'\n : c.status === 'rejected'\n ? 'destructive'\n : 'secondary'\n }\n className=\"text-[10px]\"\n >\n {formatEnumLabel(c.status)}\n </Badge>\n ),\n },\n];\n\nexport function HiringApplicationsPagePage() {\n const { candidates, jobPostings } = useCrewFoundryStore();\n const [searchParams, setSearchParams] = useSearchParams();\n const { page, pageSize, offset, setPage } = usePagination({ initialPageSize: 25 });\n\n // Optional role scope from the Job Detail \"View Applications\" deep-link.\n const positionFilter = searchParams.get('position');\n const positionKey = positionFilter?.trim().toLowerCase() ?? null;\n const scoped = positionKey\n ? candidates.filter((c) => c.position.trim().toLowerCase() === positionKey)\n : candidates;\n\n const clearPositionFilter = () => {\n const next = new URLSearchParams(searchParams);\n next.delete('position');\n setSearchParams(next, { replace: true });\n setPage(1);\n };\n\n const newApps = scoped.filter((c) => c.status === 'new');\n const screeningApps = scoped.filter((c) => c.status === 'screening');\n const interviewApps = scoped.filter((c) => c.status === 'interview');\n const hiredCount = scoped.filter((c) => c.status === 'hired').length;\n const totalApps = positionKey ? scoped.length : jobPostings.reduce((s, j) => s + j.applicants, 0);\n const conversionRate = totalApps > 0 ? (hiredCount / totalApps) * 100 : 0;\n const maxApplicants = Math.max(1, ...jobPostings.map((j) => j.applicants));\n const rankedPositions = [...jobPostings].sort((a, b) => b.applicants - a.applicants);\n const pagedCandidates = scoped.slice(offset, offset + pageSize);\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>Applications</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">\n {positionFilter\n ? `${scoped.length} applications for ${positionFilter}`\n : `${totalApps} total applications across all positions`}\n </p>\n <PagePurpose className=\"mt-1\">\n Review and triage incoming job applications — move candidates through your pipeline and\n decide who to interview next.\n </PagePurpose>\n </div>\n {positionFilter && (\n <Button variant=\"outline\" size=\"sm\" onClick={clearPositionFilter}>\n <X className=\"mr-1 h-4 w-4\" />\n {positionFilter}\n </Button>\n )}\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\">New</CardTitle>\n <FileText className=\"h-4 w-4 text-status-info-text\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">{newApps.length}</div>\n <p className=\"text-xs text-text-secondary\">Unreviewed applications</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\">Screening</CardTitle>\n <Clock className=\"h-4 w-4 text-status-warning-text\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">{screeningApps.length}</div>\n <p className=\"text-xs text-text-secondary\">Under review</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\">Interview Stage</CardTitle>\n <Users className=\"h-4 w-4 text-action-primary-bg\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">{interviewApps.length}</div>\n <p className=\"text-xs text-text-secondary\">Moving forward</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\">Conversion Rate</CardTitle>\n <UserCheck className=\"h-4 w-4 text-status-success-text\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">{conversionRate.toFixed(1)}%</div>\n <p className=\"text-xs text-text-secondary\">Hired vs. total applications</p>\n </CardContent>\n </GlassCard>\n </div>\n\n <Card>\n <CardHeader>\n <CardTitle>Applications by Position</CardTitle>\n <CardDescription>Applicant count per open role</CardDescription>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {rankedPositions.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-search\"\n title=\"No positions yet\"\n description=\"Create a job posting to start collecting applications.\"\n />\n ) : (\n rankedPositions.map((job) => (\n <div key={job.id} className=\"flex items-center gap-3\">\n <span className=\"w-56 text-sm font-medium truncate\">{job.title}</span>\n <div className=\"flex-1 h-4 bg-bg-sunken rounded overflow-hidden\">\n <div\n className=\"h-4 bg-action-primary-bg rounded\"\n style={{ width: `${Math.round((job.applicants / maxApplicants) * 100)}%` }}\n />\n </div>\n <span className=\"text-sm font-medium w-10 text-right\">{job.applicants}</span>\n <Badge\n variant={job.status === 'active' ? 'default' : 'secondary'}\n className=\"text-[10px]\"\n >\n {formatEnumLabel(job.status)}\n </Badge>\n </div>\n ))\n )}\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>\n {positionFilter ? `Applicants — ${positionFilter}` : 'All Candidates'}\n </CardTitle>\n <CardDescription>\n {positionFilter ? 'Candidates who applied to this role' : 'Complete candidate pipeline'}\n </CardDescription>\n </CardHeader>\n <CardContent>\n <ResponsiveTable\n data={pagedCandidates}\n rowKey={(c) => c.id}\n columns={CANDIDATE_COLUMNS}\n emptyState={\n <IllustratedEmptyState\n illustration=\"empty-search\"\n title=\"No candidates yet\"\n description=\"Candidates you add to your pipeline will appear here.\"\n />\n }\n />\n {scoped.length > pageSize && (\n <div className=\"mt-4\">\n <Pagination\n page={page}\n pageSize={pageSize}\n totalItems={scoped.length}\n onPageChange={setPage}\n />\n </div>\n )}\n </CardContent>\n </Card>\n </div>\n );\n}\n"],"mappings":";;;;;;;;AAsBA,IAAM,IAAwD;CAC5D;EACE,KAAK;EACL,QAAQ;EACR,UAAU;EACV,OAAO,MACL,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;GAAG,WAAU;aAAb;IACG,EAAE;IAAU;IAAE,EAAE;IACf;MACJ,kBAAC,KAAD;GAAG,WAAU;aAA+B,EAAE;GAAU,CAAA,CACpD,EAAA,CAAA;EAET;CACD;EACE,KAAK;EACL,QAAQ;EACR,UAAU;EACV,OAAO,MAAM,kBAAC,QAAD;GAAM,WAAU;aAAuB,EAAE;GAAgB,CAAA;EACvE;CACD;EACE,KAAK;EACL,QAAQ;EACR,UAAU;EACV,OAAO,MAAM,kBAAC,QAAD;GAAM,WAAU;aAAuB,EAAE;GAAsB,CAAA;EAC7E;CACD;EACE,KAAK;EACL,QAAQ;EACR,UAAU;EACV,OAAO,MAAM,kBAAC,QAAD,EAAA,UAAA,CAAO,EAAE,YAAW,OAAW,EAAA,CAAA;EAC7C;CACD;EACE,KAAK;EACL,QAAQ;EACR,UAAU;EACV,OAAO,MACL,kBAAC,QAAD;GAAM,WAAU;aAAhB,CACE,kBAAC,GAAD,EAAM,WAAU,oCAAqC,CAAA,EACpD,EAAE,OACE;;EAEV;CACD;EACE,KAAK;EACL,QAAQ;EACR,UAAU;EACV,OAAO,MACL,kBAAC,OAAD;GAAK,WAAU;aACZ,EAAE,OAAO,MAAM,GAAG,EAAE,CAAC,KAAK,MACzB,kBAAC,GAAD;IAAe,SAAQ;IAAU,WAAU;cACxC;IACK,EAFI,EAEJ,CACR;GACE,CAAA;EAET;CACD;EACE,KAAK;EACL,QAAQ;EACR,UAAU;EACV,OAAO,MACL,kBAAC,GAAD;GACE,SACE,EAAE,WAAW,WAET,EAAE,WAAW,UADb,YAGE,EAAE,WAAW,aACX,gBACA;GAEV,WAAU;aAET,EAAgB,EAAE,OAAO;GACpB,CAAA;EAEX;CACF;AAED,SAAgB,IAA6B;CAC3C,IAAM,EAAE,eAAY,mBAAgB,GAAqB,EACnD,CAAC,GAAc,KAAmB,GAAiB,EACnD,EAAE,SAAM,aAAU,WAAQ,eAAY,EAAc,EAAE,iBAAiB,IAAI,CAAC,EAG5E,IAAiB,EAAa,IAAI,WAAW,EAC7C,IAAc,GAAgB,MAAM,CAAC,aAAa,IAAI,MACtD,IAAS,IACX,EAAW,QAAQ,MAAM,EAAE,SAAS,MAAM,CAAC,aAAa,KAAK,EAAY,GACzE,GAEE,UAA4B;EAChC,IAAM,IAAO,IAAI,gBAAgB,EAAa;AAG9C,EAFA,EAAK,OAAO,WAAW,EACvB,EAAgB,GAAM,EAAE,SAAS,IAAM,CAAC,EACxC,EAAQ,EAAE;IAGN,IAAU,EAAO,QAAQ,MAAM,EAAE,WAAW,MAAM,EAClD,IAAgB,EAAO,QAAQ,MAAM,EAAE,WAAW,YAAY,EAC9D,IAAgB,EAAO,QAAQ,MAAM,EAAE,WAAW,YAAY,EAC9D,IAAa,EAAO,QAAQ,MAAM,EAAE,WAAW,QAAQ,CAAC,QACxD,IAAY,IAAc,EAAO,SAAS,EAAY,QAAQ,GAAG,MAAM,IAAI,EAAE,YAAY,EAAE,EAC3F,IAAiB,IAAY,IAAK,IAAa,IAAa,MAAM,GAClE,IAAgB,KAAK,IAAI,GAAG,GAAG,EAAY,KAAK,MAAM,EAAE,WAAW,CAAC,EACpE,IAAkB,CAAC,GAAG,EAAY,CAAC,MAAM,GAAG,MAAM,EAAE,aAAa,EAAE,WAAW,EAC9E,IAAkB,EAAO,MAAM,GAAQ,IAAS,EAAS;AAE/D,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD,EAAA,UAAA;KACE,kBAAC,MAAD;MAAI,WAAU;gBACZ,kBAAC,GAAD,EAAA,UAAc,gBAA2B,CAAA;MACtC,CAAA;KACL,kBAAC,KAAD;MAAG,WAAU;gBACV,IACG,GAAG,EAAO,OAAO,oBAAoB,MACrC,GAAG,EAAU;MACf,CAAA;KACJ,kBAAC,GAAD;MAAa,WAAU;gBAAO;MAGhB,CAAA;KACV,EAAA,CAAA,EACL,KACC,kBAAC,GAAD;KAAQ,SAAQ;KAAU,MAAK;KAAK,SAAS;eAA7C,CACE,kBAAC,GAAD,EAAG,WAAU,gBAAiB,CAAA,EAC7B,EACM;OAEP;;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;QAAe,CAAA,EAC1D,kBAAC,GAAD,EAAU,WAAU,iCAAkC,CAAA,CAC3C;UACb,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAmC,EAAQ;OAAa,CAAA,EACvE,kBAAC,KAAD;OAAG,WAAU;iBAA8B;OAA2B,CAAA,CAC1D,EAAA,CAAA,CACJ;;KACZ,kBAAC,GAAD;MAAW,WAAU;MAAS,WAAU;gBAAxC,CACE,kBAAC,GAAD;OAAY,WAAU;iBAAtB,CACE,kBAAC,GAAD;QAAW,WAAU;kBAAsB;QAAqB,CAAA,EAChE,kBAAC,GAAD,EAAO,WAAU,oCAAqC,CAAA,CAC3C;UACb,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAmC,EAAc;OAAa,CAAA,EAC7E,kBAAC,KAAD;OAAG,WAAU;iBAA8B;OAAgB,CAAA,CAC/C,EAAA,CAAA,CACJ;;KACZ,kBAAC,GAAD;MAAW,WAAU;MAAS,WAAU;gBAAxC,CACE,kBAAC,GAAD;OAAY,WAAU;iBAAtB,CACE,kBAAC,GAAD;QAAW,WAAU;kBAAsB;QAA2B,CAAA,EACtE,kBAAC,GAAD,EAAO,WAAU,kCAAmC,CAAA,CACzC;UACb,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAmC,EAAc;OAAa,CAAA,EAC7E,kBAAC,KAAD;OAAG,WAAU;iBAA8B;OAAkB,CAAA,CACjD,EAAA,CAAA,CACJ;;KACZ,kBAAC,GAAD;MAAW,WAAU;MAAS,WAAU;gBAAxC,CACE,kBAAC,GAAD;OAAY,WAAU;iBAAtB,CACE,kBAAC,GAAD;QAAW,WAAU;kBAAsB;QAA2B,CAAA,EACtE,kBAAC,GAAD,EAAW,WAAU,oCAAqC,CAAA,CAC/C;UACb,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CAAkD,EAAe,QAAQ,EAAE,EAAC,IAAO;UACnF,kBAAC,KAAD;OAAG,WAAU;iBAA8B;OAAgC,CAAA,CAC/D,EAAA,CAAA,CACJ;;KACR;;GAEN,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UAAW,4BAAoC,CAAA,EAC/C,kBAAC,GAAD,EAAA,UAAiB,iCAA+C,CAAA,CACrD,EAAA,CAAA,EACb,kBAAC,GAAD;IAAa,WAAU;cACpB,EAAgB,WAAW,IAC1B,kBAAC,GAAD;KACE,cAAa;KACb,OAAM;KACN,aAAY;KACZ,CAAA,GAEF,EAAgB,KAAK,MACnB,kBAAC,OAAD;KAAkB,WAAU;eAA5B;MACE,kBAAC,QAAD;OAAM,WAAU;iBAAqC,EAAI;OAAa,CAAA;MACtE,kBAAC,OAAD;OAAK,WAAU;iBACb,kBAAC,OAAD;QACE,WAAU;QACV,OAAO,EAAE,OAAO,GAAG,KAAK,MAAO,EAAI,aAAa,IAAiB,IAAI,CAAC,IAAI;QAC1E,CAAA;OACE,CAAA;MACN,kBAAC,QAAD;OAAM,WAAU;iBAAuC,EAAI;OAAkB,CAAA;MAC7E,kBAAC,GAAD;OACE,SAAS,EAAI,WAAW,WAAW,YAAY;OAC/C,WAAU;iBAET,EAAgB,EAAI,OAAO;OACtB,CAAA;MACJ;OAfI,EAAI,GAeR,CACN;IAEQ,CAAA,CACT,EAAA,CAAA;GAEP,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UACG,IAAiB,gBAAgB,MAAmB,kBAC3C,CAAA,EACZ,kBAAC,GAAD,EAAA,UACG,IAAiB,wCAAwC,+BAC1C,CAAA,CACP,EAAA,CAAA,EACb,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD;IACE,MAAM;IACN,SAAS,MAAM,EAAE;IACjB,SAAS;IACT,YACE,kBAAC,GAAD;KACE,cAAa;KACb,OAAM;KACN,aAAY;KACZ,CAAA;IAEJ,CAAA,EACD,EAAO,SAAS,KACf,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,GAAD;KACQ;KACI;KACV,YAAY,EAAO;KACnB,cAAc;KACd,CAAA;IACE,CAAA,CAEI,EAAA,CAAA,CACT,EAAA,CAAA;GACH"}
|
|
1
|
+
{"version":3,"file":"HiringApplicationsPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/HiringApplicationsPagePage.tsx"],"sourcesContent":["import {\n Badge,\n Button,\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} from '@burdenoff/fe-libs/ui';\nimport { usePagination } from '@burdenoff/fe-libs/shared/hooks';\nimport { Clock, FileText, Star, UserCheck, Users, X } from 'lucide-react';\nimport { useSearchParams } from 'react-router';\nimport { useCrewFoundryStore, type Candidate } from '../store/crewfoundryStore';\nimport { formatEnumLabel } from '../utils/formatEnumLabel';\n\nconst CANDIDATE_COLUMNS: ResponsiveTableColumn<Candidate>[] = [\n {\n key: 'candidate',\n header: 'Candidate',\n priority: 'primary',\n cell: (c) => (\n <div>\n <p className=\"font-medium\">\n {c.firstName} {c.lastName}\n </p>\n <p className=\"text-xs text-text-secondary\">{c.email}</p>\n </div>\n ),\n },\n {\n key: 'position',\n header: 'Position',\n priority: 'secondary',\n cell: (c) => <span className=\"text-text-secondary\">{c.position}</span>,\n },\n {\n key: 'company',\n header: 'Company',\n priority: 'secondary',\n cell: (c) => <span className=\"text-text-secondary\">{c.currentCompany}</span>,\n },\n {\n key: 'experience',\n header: 'Experience',\n priority: 'hidden-on-mobile',\n cell: (c) => <span>{c.experience} yrs</span>,\n },\n {\n key: 'rating',\n header: 'Rating',\n priority: 'hidden-on-mobile',\n cell: (c) => (\n <span className=\"flex items-center gap-1\">\n <Star className=\"h-3 w-3 text-status-warning-text\" />\n {c.rating}\n </span>\n ),\n },\n {\n key: 'skills',\n header: 'Skills',\n priority: 'secondary',\n cell: (c) => (\n <div className=\"flex flex-wrap gap-1\">\n {c.skills.slice(0, 2).map((s) => (\n <Badge key={s} variant=\"outline\" className=\"text-[9px]\">\n {s}\n </Badge>\n ))}\n </div>\n ),\n },\n {\n key: 'status',\n header: 'Status',\n priority: 'secondary',\n cell: (c) => (\n <Badge\n variant={\n c.status === 'hired'\n ? 'default'\n : c.status === 'offer'\n ? 'default'\n : c.status === 'rejected'\n ? 'destructive'\n : 'secondary'\n }\n className=\"text-[10px]\"\n >\n {formatEnumLabel(c.status)}\n </Badge>\n ),\n },\n];\n\nexport function HiringApplicationsPagePage() {\n const { candidates, jobPostings } = useCrewFoundryStore();\n const [searchParams, setSearchParams] = useSearchParams();\n const { page, pageSize, offset, setPage } = usePagination({ initialPageSize: 25 });\n\n // Optional role scope from the Job Detail \"View Applications\" deep-link.\n const positionFilter = searchParams.get('position');\n const positionKey = positionFilter?.trim().toLowerCase() ?? null;\n const scoped = positionKey\n ? candidates.filter((c) => c.position.trim().toLowerCase() === positionKey)\n : candidates;\n\n const clearPositionFilter = () => {\n const next = new URLSearchParams(searchParams);\n next.delete('position');\n setSearchParams(next, { replace: true });\n setPage(1);\n };\n\n const newApps = scoped.filter((c) => c.status === 'new');\n const screeningApps = scoped.filter((c) => c.status === 'screening');\n const interviewApps = scoped.filter((c) => c.status === 'interview');\n const hiredCount = scoped.filter((c) => c.status === 'hired').length;\n const totalApps = positionKey ? scoped.length : jobPostings.reduce((s, j) => s + j.applicants, 0);\n const conversionRate = totalApps > 0 ? (hiredCount / totalApps) * 100 : 0;\n const maxApplicants = Math.max(1, ...jobPostings.map((j) => j.applicants));\n const rankedPositions = [...jobPostings].sort((a, b) => b.applicants - a.applicants);\n const pagedCandidates = scoped.slice(offset, offset + pageSize);\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>Applications</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">\n {positionFilter\n ? `${scoped.length} applications for ${positionFilter}`\n : `${totalApps} total applications across all positions`}\n </p>\n <PagePurpose className=\"mt-1\">\n Review and triage incoming job applications — move candidates through your pipeline and\n decide who to interview next.\n </PagePurpose>\n </div>\n {positionFilter && (\n <Button variant=\"outline\" size=\"sm\" onClick={clearPositionFilter}>\n <X className=\"mr-1 h-4 w-4\" />\n {positionFilter}\n </Button>\n )}\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\">New</CardTitle>\n <FileText className=\"h-4 w-4 text-status-info-text\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">{newApps.length}</div>\n <p className=\"text-xs text-text-secondary\">Unreviewed applications</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\">Screening</CardTitle>\n <Clock className=\"h-4 w-4 text-status-warning-text\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">{screeningApps.length}</div>\n <p className=\"text-xs text-text-secondary\">Under review</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\">Interview Stage</CardTitle>\n <Users className=\"h-4 w-4 text-action-primary-bg\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">{interviewApps.length}</div>\n <p className=\"text-xs text-text-secondary\">Moving forward</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\">Conversion Rate</CardTitle>\n <UserCheck className=\"h-4 w-4 text-status-success-text\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">{conversionRate.toFixed(1)}%</div>\n <p className=\"text-xs text-text-secondary\">Hired vs. total applications</p>\n </CardContent>\n </GlassCard>\n </div>\n\n <Card>\n <CardHeader>\n <CardTitle>Applications by Position</CardTitle>\n <CardDescription>Applicant count per open role</CardDescription>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {rankedPositions.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-search\"\n title=\"No positions yet\"\n description=\"Create a job posting to start collecting applications.\"\n />\n ) : (\n rankedPositions.map((job) => (\n <div key={job.id} className=\"flex items-center gap-3\">\n <span className=\"w-56 text-sm font-medium truncate\">{job.title}</span>\n <div className=\"flex-1 h-4 bg-bg-sunken rounded overflow-hidden\">\n <div\n className=\"h-4 bg-action-primary-bg rounded\"\n style={{ width: `${Math.round((job.applicants / maxApplicants) * 100)}%` }}\n />\n </div>\n <span className=\"text-sm font-medium w-10 text-right\">{job.applicants}</span>\n <Badge\n variant={job.status === 'active' ? 'default' : 'secondary'}\n className=\"text-[10px]\"\n >\n {formatEnumLabel(job.status)}\n </Badge>\n </div>\n ))\n )}\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>\n {positionFilter ? `Applicants — ${positionFilter}` : 'All Candidates'}\n </CardTitle>\n <CardDescription>\n {positionFilter ? 'Candidates who applied to this role' : 'Complete candidate pipeline'}\n </CardDescription>\n </CardHeader>\n <CardContent>\n <ResponsiveTable\n data={pagedCandidates}\n rowKey={(c) => c.id}\n columns={CANDIDATE_COLUMNS}\n emptyState={\n <IllustratedEmptyState\n illustration=\"empty-search\"\n title=\"No candidates yet\"\n description=\"Candidates you add to your pipeline will appear here.\"\n />\n }\n />\n {scoped.length > pageSize && (\n <div className=\"mt-4\">\n <Pagination\n page={page}\n pageSize={pageSize}\n totalItems={scoped.length}\n onPageChange={setPage}\n />\n </div>\n )}\n </CardContent>\n </Card>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -2,7 +2,7 @@ import { useCrewFoundryStore as e } from "../store/crewfoundryStore.js";
|
|
|
2
2
|
import { newClientId as t } from "../components/forms/formValues.js";
|
|
3
3
|
import { useMemo as n, useState as r } from "react";
|
|
4
4
|
import { Briefcase as i, ChevronRight as a, Plus as o, Save as s, X as c } from "lucide-react";
|
|
5
|
-
import { useNavigate as l } from "react-router
|
|
5
|
+
import { useNavigate as l } from "react-router";
|
|
6
6
|
import { jsx as u, jsxs as d } from "react/jsx-runtime";
|
|
7
7
|
import { Button as f, GlassCard as ee, GradientText as p, Input as m, PagePurpose as h, Textarea as g } from "@burdenoff/fe-libs/ui";
|
|
8
8
|
import { useGlobalUi as _ } from "@burdenoff/fe-libs/shared/providers/shell";
|