@burdenoff/microfe-crewfoundry 2026.730.1 → 2026.801.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/dist/crewfoundry/CrewFoundryRoutes.js +1 -1
  2. package/dist/crewfoundry/CrewFoundryRoutes.js.map +1 -1
  3. package/dist/crewfoundry/components/payroll/GlobalPayrollSection.d.ts +2 -0
  4. package/dist/crewfoundry/components/payroll/GlobalPayrollSection.d.ts.map +1 -0
  5. package/dist/crewfoundry/components/payroll/GlobalPayrollSection.js +369 -0
  6. package/dist/crewfoundry/components/payroll/GlobalPayrollSection.js.map +1 -0
  7. package/dist/crewfoundry/pages/CommunicationsAnnouncementsPagePage.js +1 -1
  8. package/dist/crewfoundry/pages/CommunicationsAnnouncementsPagePage.js.map +1 -1
  9. package/dist/crewfoundry/pages/CommunicationsFilesPagePage.js +1 -1
  10. package/dist/crewfoundry/pages/CommunicationsFilesPagePage.js.map +1 -1
  11. package/dist/crewfoundry/pages/CommunicationsTeamsPagePage.js +1 -1
  12. package/dist/crewfoundry/pages/CommunicationsTeamsPagePage.js.map +1 -1
  13. package/dist/crewfoundry/pages/CompanyClientManagementPagePage.js +1 -1
  14. package/dist/crewfoundry/pages/CompanyClientManagementPagePage.js.map +1 -1
  15. package/dist/crewfoundry/pages/CompanyVendorManagementPagePage.js +1 -1
  16. package/dist/crewfoundry/pages/CompanyVendorManagementPagePage.js.map +1 -1
  17. package/dist/crewfoundry/pages/DashboardDashboardPagePage.js +1 -1
  18. package/dist/crewfoundry/pages/DashboardDashboardPagePage.js.map +1 -1
  19. package/dist/crewfoundry/pages/DashboardEnhancedDashboardPagePage.js +1 -1
  20. package/dist/crewfoundry/pages/DashboardEnhancedDashboardPagePage.js.map +1 -1
  21. package/dist/crewfoundry/pages/HiringApplicationsPagePage.js +1 -1
  22. package/dist/crewfoundry/pages/HiringApplicationsPagePage.js.map +1 -1
  23. package/dist/crewfoundry/pages/HiringCreateJobPagePage.js +1 -1
  24. package/dist/crewfoundry/pages/HiringCreateJobPagePage.js.map +1 -1
  25. package/dist/crewfoundry/pages/HiringJobDetailsPagePage.js +1 -1
  26. package/dist/crewfoundry/pages/HiringJobDetailsPagePage.js.map +1 -1
  27. package/dist/crewfoundry/pages/HiringJobsPagePage.js +1 -1
  28. package/dist/crewfoundry/pages/HiringJobsPagePage.js.map +1 -1
  29. package/dist/crewfoundry/pages/HrEmployeeProfilePagePage.js +1 -1
  30. package/dist/crewfoundry/pages/HrEmployeeProfilePagePage.js.map +1 -1
  31. package/dist/crewfoundry/pages/HrEmployeesPagePage.js +1 -1
  32. package/dist/crewfoundry/pages/HrEmployeesPagePage.js.map +1 -1
  33. package/dist/crewfoundry/pages/HrPayrollPagePage.d.ts.map +1 -1
  34. package/dist/crewfoundry/pages/HrPayrollPagePage.js +136 -134
  35. package/dist/crewfoundry/pages/HrPayrollPagePage.js.map +1 -1
  36. package/dist/crewfoundry/pages/LearningAssessmentsPagePage.js +1 -1
  37. package/dist/crewfoundry/pages/LearningAssessmentsPagePage.js.map +1 -1
  38. package/dist/crewfoundry/pages/LearningCertificatesPagePage.js +1 -1
  39. package/dist/crewfoundry/pages/LearningCertificatesPagePage.js.map +1 -1
  40. package/dist/crewfoundry/pages/LearningCourseDetailsPagePage.js +1 -1
  41. package/dist/crewfoundry/pages/LearningCourseDetailsPagePage.js.map +1 -1
  42. package/dist/crewfoundry/pages/LearningCourseLearningPagePage.js +1 -1
  43. package/dist/crewfoundry/pages/LearningCourseLearningPagePage.js.map +1 -1
  44. package/dist/crewfoundry/pages/LearningCoursesPagePage.js +1 -1
  45. package/dist/crewfoundry/pages/LearningCoursesPagePage.js.map +1 -1
  46. package/dist/crewfoundry/pages/LearningLearningPagePage.js +1 -1
  47. package/dist/crewfoundry/pages/LearningLearningPagePage.js.map +1 -1
  48. package/dist/crewfoundry/pages/LearningLearningPathDetailPagePage.js +1 -1
  49. package/dist/crewfoundry/pages/LearningLearningPathDetailPagePage.js.map +1 -1
  50. package/dist/crewfoundry/pages/LearningLearningPathsPagePage.js +1 -1
  51. package/dist/crewfoundry/pages/LearningLearningPathsPagePage.js.map +1 -1
  52. package/dist/crewfoundry/pages/LearningMyLearningPagePage.js +1 -1
  53. package/dist/crewfoundry/pages/LearningMyLearningPagePage.js.map +1 -1
  54. package/dist/crewfoundry/pages/MarketplaceFreelancerProfilePageEnhancedPage.js +1 -1
  55. package/dist/crewfoundry/pages/MarketplaceFreelancerProfilePageEnhancedPage.js.map +1 -1
  56. package/dist/crewfoundry/pages/MarketplaceFreelancerProfilePagePage.js +1 -1
  57. package/dist/crewfoundry/pages/MarketplaceFreelancerProfilePagePage.js.map +1 -1
  58. package/dist/crewfoundry/pages/MarketplaceMarketplacePagePage.js +1 -1
  59. package/dist/crewfoundry/pages/MarketplaceMarketplacePagePage.js.map +1 -1
  60. package/dist/crewfoundry/pages/MarketplaceMarketplaceServicesPagePage.js +1 -1
  61. package/dist/crewfoundry/pages/MarketplaceMarketplaceServicesPagePage.js.map +1 -1
  62. package/dist/crewfoundry/pages/MarketplaceMyServicesPagePage.js +1 -1
  63. package/dist/crewfoundry/pages/MarketplaceMyServicesPagePage.js.map +1 -1
  64. package/dist/crewfoundry/pages/MarketplaceProjectDetailsPagePage.js +1 -1
  65. package/dist/crewfoundry/pages/MarketplaceProjectDetailsPagePage.js.map +1 -1
  66. package/dist/crewfoundry/pages/MarketplaceServiceDetailsPagePage.js +1 -1
  67. package/dist/crewfoundry/pages/MarketplaceServiceDetailsPagePage.js.map +1 -1
  68. package/dist/crewfoundry/pages/PaymentsPaymentDashboardPagePage.js +1 -1
  69. package/dist/crewfoundry/pages/PaymentsPaymentDashboardPagePage.js.map +1 -1
  70. package/dist/crewfoundry/pages/PaymentsPaymentMethodsPagePage.js +1 -1
  71. package/dist/crewfoundry/pages/PaymentsPaymentMethodsPagePage.js.map +1 -1
  72. package/dist/crewfoundry/pages/ProjectsCreateProjectPagePage.js +1 -1
  73. package/dist/crewfoundry/pages/ProjectsCreateProjectPagePage.js.map +1 -1
  74. package/dist/crewfoundry/pages/ProjectsProjectDetailsPagePage.js +1 -1
  75. package/dist/crewfoundry/pages/ProjectsProjectDetailsPagePage.js.map +1 -1
  76. package/dist/crewfoundry/pages/ProjectsProjectsPagePage.js +1 -1
  77. package/dist/crewfoundry/pages/ProjectsProjectsPagePage.js.map +1 -1
  78. package/dist/crewfoundry/pages/ProjectsResourcingPagePage.js +1 -1
  79. package/dist/crewfoundry/pages/ProjectsResourcingPagePage.js.map +1 -1
  80. package/dist/crewfoundry/pages/TeamsTeamDetailPagePage.js +1 -1
  81. package/dist/crewfoundry/pages/TeamsTeamDetailPagePage.js.map +1 -1
  82. package/dist/crewfoundry/pages/TeamsTeamDirectoryPagePage.js +1 -1
  83. package/dist/crewfoundry/pages/TeamsTeamDirectoryPagePage.js.map +1 -1
  84. package/dist/crewfoundry/pages/TeamsTeamSettingsPagePage.js +1 -1
  85. package/dist/crewfoundry/pages/TeamsTeamSettingsPagePage.js.map +1 -1
  86. package/dist/crewfoundry/pages/TeamsTeamsPagePage.js +1 -1
  87. package/dist/crewfoundry/pages/TeamsTeamsPagePage.js.map +1 -1
  88. package/dist/crewfoundry/providers/CrewFoundryProvider.js +1 -1
  89. package/dist/crewfoundry/providers/CrewFoundryProvider.js.map +1 -1
  90. package/dist/crewfoundry/store/crewfoundryStore.d.ts +73 -0
  91. package/dist/crewfoundry/store/crewfoundryStore.d.ts.map +1 -1
  92. package/dist/crewfoundry/store/crewfoundryStore.js +104 -48
  93. package/dist/crewfoundry/store/crewfoundryStore.js.map +1 -1
  94. package/package.json +6 -6
@@ -1 +1 @@
1
- {"version":3,"file":"HrPayrollPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/HrPayrollPagePage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport {\n Badge,\n Button,\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n GlassCard,\n GradientText,\n PagePurpose,\n Pagination,\n ResponsiveTable,\n Separator,\n type ResponsiveTableColumn,\n} from '@burdenoff/fe-libs/ui';\nimport { usePagination } from '@burdenoff/fe-libs/shared/hooks';\nimport { useGlobalUi } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { CalendarClock, CheckCircle, DollarSign, Download, Play, Plus, Users } from 'lucide-react';\nimport { useCrewFoundryStore, type Employee, type PayrollRecord } from '../store/crewfoundryStore';\nimport { AvatarStack, StatCard } from '../components/viz';\nimport {\n FormModal,\n useFormModal,\n type FieldConfig,\n type FormValues,\n} from '../components/forms/FormModal';\nimport { ConfirmDialog } from '../components/forms/ConfirmDialog';\nimport { getNumber, getText, newClientId } from '../components/forms/formValues';\nimport { formatEnumLabel } from '../utils/formatEnumLabel';\n\nconst STATUS_OPTIONS = [\n { label: 'Draft', value: 'draft' },\n { label: 'Processing', value: 'processing' },\n { label: 'Approved', value: 'approved' },\n { label: 'Paid', value: 'paid' },\n { label: 'Failed', value: 'failed' },\n];\n\nconst MONTH_NAMES = [\n 'January',\n 'February',\n 'March',\n 'April',\n 'May',\n 'June',\n 'July',\n 'August',\n 'September',\n 'October',\n 'November',\n 'December',\n];\nconst MONTH_ABBR = [\n 'Jan',\n 'Feb',\n 'Mar',\n 'Apr',\n 'May',\n 'Jun',\n 'Jul',\n 'Aug',\n 'Sep',\n 'Oct',\n 'Nov',\n 'Dec',\n];\n\n// Derive the next pay-run label from a \"Month YYYY\" period; \"—\" when unparseable.\nconst nextRunLabel = (period: string): string => {\n const [monthName, yearStr] = period.split(' ');\n const monthIndex = MONTH_NAMES.findIndex((m) => m.toLowerCase() === monthName?.toLowerCase());\n const year = Number(yearStr);\n if (monthIndex === -1 || Number.isNaN(year)) {\n return '—';\n }\n const nextMonth = (monthIndex + 1) % 12;\n const nextYear = monthIndex === 11 ? year + 1 : year;\n return `${MONTH_ABBR[nextMonth]} 1, ${nextYear}`;\n};\n\nexport function HrPayrollPagePage() {\n const { payrollRecords, employees, addPayrollRecord, updatePayrollRecord } =\n useCrewFoundryStore();\n const { showToast } = useGlobalUi();\n const periods = useMemo(\n () => [...new Set(payrollRecords.map((r) => r.period))],\n [payrollRecords]\n );\n const [selectedPeriod, setSelectedPeriod] = useState('');\n const createForm = useFormModal<void>();\n const [runOpen, setRunOpen] = useState(false);\n const [running, setRunning] = useState(false);\n const { page, pageSize, offset, setPage } = usePagination({ initialPageSize: 10 });\n\n // Default the active period to the first real one once data loads.\n const activePeriod = selectedPeriod || periods[0] || '';\n\n const employeeById = useMemo(\n () => new Map<string, Employee>(employees.map((employee) => [employee.id, employee])),\n [employees]\n );\n\n const employeeOptions = useMemo(\n () =>\n employees.map((employee) => ({\n label: `${employee.firstName} ${employee.lastName}`,\n value: employee.id,\n })),\n [employees]\n );\n\n const payrollFields = useMemo<FieldConfig[]>(\n () => [\n {\n name: 'employeeId',\n label: 'Employee',\n type: 'select',\n required: true,\n options: employeeOptions,\n colSpan: 2,\n },\n {\n name: 'period',\n label: 'Pay period',\n type: 'text',\n required: true,\n placeholder: 'e.g. February 2026',\n },\n {\n name: 'paymentMethod',\n label: 'Payment method',\n type: 'text',\n required: true,\n placeholder: 'e.g. Bank Transfer',\n },\n { name: 'grossPay', label: 'Gross pay', type: 'number', required: true },\n { name: 'deductions', label: 'Deductions', type: 'number', required: true },\n { name: 'netPay', label: 'Net pay (auto if blank)', type: 'number' },\n { name: 'payDate', label: 'Pay date', type: 'date' },\n { name: 'status', label: 'Status', type: 'select', options: STATUS_OPTIONS },\n ],\n [employeeOptions]\n );\n\n const periodRecords = payrollRecords.filter((r) => r.period === activePeriod);\n const totalGross = periodRecords.reduce((s, r) => s + r.grossPay, 0);\n const totalNet = periodRecords.reduce((s, r) => s + r.netPay, 0);\n const totalDeductions = periodRecords.reduce((s, r) => s + r.deductions, 0);\n const paidCount = periodRecords.filter((r) => r.status === 'paid').length;\n const pendingCount = periodRecords.length - paidCount;\n const runPct =\n periodRecords.length > 0 ? Math.round((paidCount / periodRecords.length) * 100) : 0;\n const runComplete = periodRecords.length > 0 && pendingCount === 0;\n const recordPageItems = periodRecords.slice(offset, offset + pageSize);\n\n const handlePeriodChange = (period: string) => {\n setSelectedPeriod(period);\n setPage(1);\n };\n\n const handleRunPayroll = async () => {\n const toAdvance = periodRecords.filter((r) => r.status !== 'paid');\n setRunning(true);\n let done = 0;\n try {\n // Sequential to avoid bursting the gateway (no PgBouncer in front of the svc).\n for (const record of toAdvance) {\n await updatePayrollRecord(record.id, {\n status: 'paid',\n payDate: new Date().toISOString().slice(0, 10),\n });\n done += 1;\n }\n showToast(`Pay run complete — ${done} record${done === 1 ? '' : 's'} paid`, 'success');\n } catch (error) {\n showToast(\n error instanceof Error ? error.message : 'Payroll run failed partway through',\n 'error'\n );\n } finally {\n setRunning(false);\n setRunOpen(false);\n }\n };\n\n const handleExportCsv = () => {\n if (periodRecords.length === 0) {\n showToast('No records to export for this period', 'info');\n return;\n }\n const header = ['Employee', 'Department', 'Period', 'Gross', 'Deductions', 'Net', 'Status'];\n const escape = (value: string) => `\"${value.replace(/\"/g, '\"\"')}\"`;\n const rows = periodRecords.map((r) =>\n [\n r.employeeName,\n r.department,\n r.period,\n String(r.grossPay),\n String(r.deductions),\n String(r.netPay),\n r.status,\n ]\n .map(escape)\n .join(',')\n );\n const csv = [header.map(escape).join(','), ...rows].join('\\n');\n const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });\n const url = URL.createObjectURL(blob);\n const link = document.createElement('a');\n link.href = url;\n link.download = `payroll-${activePeriod.replace(/\\s+/g, '-').toLowerCase()}.csv`;\n document.body.appendChild(link);\n link.click();\n document.body.removeChild(link);\n URL.revokeObjectURL(url);\n showToast('Payroll CSV exported', 'success');\n };\n\n const recordColumns: ResponsiveTableColumn<PayrollRecord>[] = [\n {\n key: 'employee',\n header: 'Employee',\n priority: 'primary',\n cell: (record) => (\n <div className=\"flex items-center gap-2.5\">\n <AvatarStack\n people={[\n { name: record.employeeName, avatarUrl: employeeById.get(record.employeeId)?.avatar },\n ]}\n size={32}\n />\n <span className=\"font-medium\">{record.employeeName}</span>\n </div>\n ),\n },\n {\n key: 'department',\n header: 'Department',\n cell: (record) => <span className=\"text-text-secondary\">{record.department}</span>,\n },\n {\n key: 'gross',\n header: 'Gross Pay',\n headerClassName: 'text-right',\n cellClassName: 'md:text-right',\n cell: (record) => <span>${record.grossPay.toLocaleString()}</span>,\n },\n {\n key: 'deductions',\n header: 'Deductions',\n headerClassName: 'text-right',\n cellClassName: 'md:text-right',\n cell: (record) => (\n <span className=\"text-status-error-text\">-${record.deductions.toLocaleString()}</span>\n ),\n },\n {\n key: 'net',\n header: 'Net Pay',\n headerClassName: 'text-right',\n cellClassName: 'md:text-right',\n cell: (record) => <span className=\"font-medium\">${record.netPay.toLocaleString()}</span>,\n },\n {\n key: 'method',\n header: 'Payment Method',\n cell: (record) => <span className=\"text-text-secondary\">{record.paymentMethod}</span>,\n },\n {\n key: 'status',\n header: 'Status',\n cell: (record) => (\n <div className=\"flex items-center gap-2\">\n <Badge\n variant={\n record.status === 'paid'\n ? 'default'\n : record.status === 'processing'\n ? 'secondary'\n : 'outline'\n }\n className=\"text-[10px]\"\n >\n {formatEnumLabel(record.status)}\n </Badge>\n {record.status !== 'paid' && (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-6 px-2 text-[10px]\"\n onClick={() => handleMarkPaid(record)}\n >\n Mark paid\n </Button>\n )}\n </div>\n ),\n },\n ];\n\n const handleCreate = async (values: FormValues) => {\n const employeeId = getText(values, 'employeeId');\n const employee = employees.find((item) => item.id === employeeId);\n const grossPay = getNumber(values, 'grossPay');\n const deductions = getNumber(values, 'deductions');\n const netPayInput = getNumber(values, 'netPay');\n const payload: PayrollRecord = {\n id: newClientId(),\n employeeId,\n employeeName: employee ? `${employee.firstName} ${employee.lastName}` : '',\n department: employee?.department ?? '',\n period: getText(values, 'period'),\n grossPay,\n deductions,\n netPay: netPayInput > 0 ? netPayInput : Math.max(grossPay - deductions, 0),\n status: (getText(values, 'status') as PayrollRecord['status']) || 'draft',\n payDate: getText(values, 'payDate'),\n paymentMethod: getText(values, 'paymentMethod'),\n };\n await addPayrollRecord(payload);\n };\n\n const handleMarkPaid = async (record: PayrollRecord) => {\n try {\n await updatePayrollRecord(record.id, {\n status: 'paid',\n payDate: new Date().toISOString().slice(0, 10),\n });\n } catch (error) {\n showToast(\n error instanceof Error ? error.message : 'Unable to update payroll record',\n 'error'\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>Payroll</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">\n {activePeriod ? `${activePeriod} pay run` : 'Process and manage employee payroll'}\n </p>\n <PagePurpose className=\"mt-1\">\n Run and review payroll — track salaries, deductions, and pay runs so your team gets paid\n accurately and on time.\n </PagePurpose>\n </div>\n <div className=\"flex gap-2\">\n <Button variant=\"outline\" size=\"sm\" onClick={handleExportCsv}>\n <Download className=\"h-4 w-4 mr-1\" />\n Export\n </Button>\n <Button variant=\"outline\" size=\"sm\" onClick={() => createForm.openModal()}>\n <Plus className=\"h-4 w-4 mr-1\" />\n Add Record\n </Button>\n <Button\n size=\"sm\"\n onClick={() => setRunOpen(true)}\n disabled={runComplete || periodRecords.length === 0}\n title={\n runComplete ? 'All records in this period are paid' : 'Advance this period to paid'\n }\n >\n <Play className=\"h-4 w-4 mr-1\" />\n Run Payroll\n </Button>\n </div>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n label=\"Total Payroll\"\n value={`$${totalGross.toLocaleString()}`}\n icon={<DollarSign className=\"h-4 w-4\" />}\n />\n <StatCard\n label=\"Employees Paid\"\n value={paidCount}\n icon={<CheckCircle className=\"h-4 w-4\" />}\n />\n <StatCard label=\"Pending\" value={pendingCount} icon={<Users className=\"h-4 w-4\" />} />\n <StatCard\n label=\"Next Run\"\n value={activePeriod ? nextRunLabel(activePeriod) : '—'}\n icon={<CalendarClock className=\"h-4 w-4\" />}\n />\n </div>\n\n {periodRecords.length > 0 && (\n <GlassCard treatment=\"glass\" glow className=\"border-border-seam\">\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0\">\n <CardTitle className=\"text-base\">Current pay run</CardTitle>\n <Badge\n variant={runComplete ? 'default' : 'secondary'}\n className=\"flex items-center gap-1\"\n >\n <CheckCircle className=\"h-3 w-3\" />\n {runComplete ? 'Completed' : 'Processing'}\n </Badge>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div className=\"grid gap-4 sm:grid-cols-4\">\n <div>\n <p className=\"text-xs text-text-secondary\">Period</p>\n <p className=\"text-sm font-semibold\">{activePeriod}</p>\n </div>\n <div>\n <p className=\"text-xs text-text-secondary\">Gross Total</p>\n <p className=\"text-sm font-semibold tabular-nums\">${totalGross.toLocaleString()}</p>\n </div>\n <div>\n <p className=\"text-xs text-text-secondary\">Deductions</p>\n <p className=\"text-sm font-semibold tabular-nums text-status-error-text\">\n -${totalDeductions.toLocaleString()}\n </p>\n </div>\n <div>\n <p className=\"text-xs text-text-secondary\">Net Total</p>\n <p className=\"text-sm font-semibold tabular-nums\">${totalNet.toLocaleString()}</p>\n </div>\n </div>\n <div className=\"space-y-1\">\n <div className=\"flex items-center justify-between text-xs text-text-secondary\">\n <span>\n {paidCount} of {periodRecords.length} paid\n </span>\n <span className=\"font-medium text-text-primary tabular-nums\">{runPct}%</span>\n </div>\n <div className=\"h-2 overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-2 rounded-full bg-status-success-text transition-all duration-500\"\n style={{ width: `${runPct}%` }}\n />\n </div>\n </div>\n </CardContent>\n </GlassCard>\n )}\n\n {periods.length > 1 && (\n <div className=\"flex flex-wrap gap-2\">\n {periods.map((period) => (\n <Button\n key={period}\n variant={activePeriod === period ? 'default' : 'outline'}\n size=\"sm\"\n onClick={() => handlePeriodChange(period)}\n >\n {period}\n </Button>\n ))}\n </div>\n )}\n\n <Card>\n <CardHeader>\n <CardTitle>Payroll Records</CardTitle>\n <CardDescription>Individual employee payroll details for {activePeriod}</CardDescription>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <ResponsiveTable<PayrollRecord>\n data={recordPageItems}\n rowKey={(record) => record.id}\n columns={recordColumns}\n emptyState={\n <p className=\"py-6 text-center text-sm text-text-secondary\">\n No payroll records for {activePeriod || 'this period'}.\n </p>\n }\n />\n {periodRecords.length > pageSize && (\n <Pagination\n page={page}\n pageSize={pageSize}\n totalItems={periodRecords.length}\n onPageChange={setPage}\n />\n )}\n {periodRecords.length > 0 && (\n <>\n <Separator />\n <div className=\"flex flex-wrap items-center justify-between gap-4 text-sm font-semibold\">\n <span>Total for {activePeriod}</span>\n <div className=\"flex flex-wrap gap-x-6 gap-y-1\">\n <span>\n <span className=\"text-text-secondary\">Gross </span>$\n {totalGross.toLocaleString()}\n </span>\n <span className=\"text-status-error-text\">\n <span className=\"text-text-secondary\">Deductions </span>-$\n {totalDeductions.toLocaleString()}\n </span>\n <span>\n <span className=\"text-text-secondary\">Net </span>${totalNet.toLocaleString()}\n </span>\n </div>\n </div>\n </>\n )}\n </CardContent>\n </Card>\n\n <FormModal\n open={createForm.open}\n onOpenChange={createForm.onOpenChange}\n title=\"Add payroll record\"\n description=\"Create a payroll entry for a team member.\"\n fields={payrollFields}\n submitLabel=\"Add record\"\n onSubmit={handleCreate}\n />\n\n <ConfirmDialog\n open={runOpen}\n onOpenChange={(open) => {\n if (!open && !running) {\n setRunOpen(false);\n }\n }}\n title={`Run payroll for ${activePeriod}?`}\n description={`This marks the ${pendingCount} remaining record${\n pendingCount === 1 ? '' : 's'\n } in ${activePeriod} as paid. This cannot be undone.`}\n confirmLabel={running ? 'Processing…' : 'Run payroll'}\n onConfirm={handleRunPayroll}\n />\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"HrPayrollPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/HrPayrollPagePage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport {\n Badge,\n Button,\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n GlassCard,\n GradientText,\n PagePurpose,\n Pagination,\n ResponsiveTable,\n Separator,\n type ResponsiveTableColumn,\n} from '@burdenoff/fe-libs/ui';\nimport { usePagination } from '@burdenoff/fe-libs/shared/hooks';\nimport { useGlobalUi } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { CalendarClock, CheckCircle, DollarSign, Download, Play, Plus, Users } from 'lucide-react';\nimport { useCrewFoundryStore, type Employee, type PayrollRecord } from '../store/crewfoundryStore';\nimport { AvatarStack, StatCard } from '../components/viz';\nimport {\n FormModal,\n useFormModal,\n type FieldConfig,\n type FormValues,\n} from '../components/forms/FormModal';\nimport { ConfirmDialog } from '../components/forms/ConfirmDialog';\nimport { getNumber, getText, newClientId } from '../components/forms/formValues';\nimport { formatEnumLabel } from '../utils/formatEnumLabel';\nimport { GlobalPayrollSection } from '../components/payroll/GlobalPayrollSection';\n\nconst STATUS_OPTIONS = [\n { label: 'Draft', value: 'draft' },\n { label: 'Processing', value: 'processing' },\n { label: 'Approved', value: 'approved' },\n { label: 'Paid', value: 'paid' },\n { label: 'Failed', value: 'failed' },\n];\n\nconst MONTH_NAMES = [\n 'January',\n 'February',\n 'March',\n 'April',\n 'May',\n 'June',\n 'July',\n 'August',\n 'September',\n 'October',\n 'November',\n 'December',\n];\nconst MONTH_ABBR = [\n 'Jan',\n 'Feb',\n 'Mar',\n 'Apr',\n 'May',\n 'Jun',\n 'Jul',\n 'Aug',\n 'Sep',\n 'Oct',\n 'Nov',\n 'Dec',\n];\n\n// Derive the next pay-run label from a \"Month YYYY\" period; \"—\" when unparseable.\nconst nextRunLabel = (period: string): string => {\n const [monthName, yearStr] = period.split(' ');\n const monthIndex = MONTH_NAMES.findIndex((m) => m.toLowerCase() === monthName?.toLowerCase());\n const year = Number(yearStr);\n if (monthIndex === -1 || Number.isNaN(year)) {\n return '—';\n }\n const nextMonth = (monthIndex + 1) % 12;\n const nextYear = monthIndex === 11 ? year + 1 : year;\n return `${MONTH_ABBR[nextMonth]} 1, ${nextYear}`;\n};\n\nexport function HrPayrollPagePage() {\n const { payrollRecords, employees, addPayrollRecord, updatePayrollRecord } =\n useCrewFoundryStore();\n const { showToast } = useGlobalUi();\n const periods = useMemo(\n () => [...new Set(payrollRecords.map((r) => r.period))],\n [payrollRecords]\n );\n const [selectedPeriod, setSelectedPeriod] = useState('');\n const createForm = useFormModal<void>();\n const [runOpen, setRunOpen] = useState(false);\n const [running, setRunning] = useState(false);\n const { page, pageSize, offset, setPage } = usePagination({ initialPageSize: 10 });\n\n // Default the active period to the first real one once data loads.\n const activePeriod = selectedPeriod || periods[0] || '';\n\n const employeeById = useMemo(\n () => new Map<string, Employee>(employees.map((employee) => [employee.id, employee])),\n [employees]\n );\n\n const employeeOptions = useMemo(\n () =>\n employees.map((employee) => ({\n label: `${employee.firstName} ${employee.lastName}`,\n value: employee.id,\n })),\n [employees]\n );\n\n const payrollFields = useMemo<FieldConfig[]>(\n () => [\n {\n name: 'employeeId',\n label: 'Employee',\n type: 'select',\n required: true,\n options: employeeOptions,\n colSpan: 2,\n },\n {\n name: 'period',\n label: 'Pay period',\n type: 'text',\n required: true,\n placeholder: 'e.g. February 2026',\n },\n {\n name: 'paymentMethod',\n label: 'Payment method',\n type: 'text',\n required: true,\n placeholder: 'e.g. Bank Transfer',\n },\n { name: 'grossPay', label: 'Gross pay', type: 'number', required: true },\n { name: 'deductions', label: 'Deductions', type: 'number', required: true },\n { name: 'netPay', label: 'Net pay (auto if blank)', type: 'number' },\n { name: 'payDate', label: 'Pay date', type: 'date' },\n { name: 'status', label: 'Status', type: 'select', options: STATUS_OPTIONS },\n ],\n [employeeOptions]\n );\n\n const periodRecords = payrollRecords.filter((r) => r.period === activePeriod);\n const totalGross = periodRecords.reduce((s, r) => s + r.grossPay, 0);\n const totalNet = periodRecords.reduce((s, r) => s + r.netPay, 0);\n const totalDeductions = periodRecords.reduce((s, r) => s + r.deductions, 0);\n const paidCount = periodRecords.filter((r) => r.status === 'paid').length;\n const pendingCount = periodRecords.length - paidCount;\n const runPct =\n periodRecords.length > 0 ? Math.round((paidCount / periodRecords.length) * 100) : 0;\n const runComplete = periodRecords.length > 0 && pendingCount === 0;\n const recordPageItems = periodRecords.slice(offset, offset + pageSize);\n\n const handlePeriodChange = (period: string) => {\n setSelectedPeriod(period);\n setPage(1);\n };\n\n const handleRunPayroll = async () => {\n const toAdvance = periodRecords.filter((r) => r.status !== 'paid');\n setRunning(true);\n let done = 0;\n try {\n // Sequential to avoid bursting the gateway (no PgBouncer in front of the svc).\n for (const record of toAdvance) {\n await updatePayrollRecord(record.id, {\n status: 'paid',\n payDate: new Date().toISOString().slice(0, 10),\n });\n done += 1;\n }\n showToast(`Pay run complete — ${done} record${done === 1 ? '' : 's'} paid`, 'success');\n } catch (error) {\n showToast(\n error instanceof Error ? error.message : 'Payroll run failed partway through',\n 'error'\n );\n } finally {\n setRunning(false);\n setRunOpen(false);\n }\n };\n\n const handleExportCsv = () => {\n if (periodRecords.length === 0) {\n showToast('No records to export for this period', 'info');\n return;\n }\n const header = ['Employee', 'Department', 'Period', 'Gross', 'Deductions', 'Net', 'Status'];\n const escape = (value: string) => `\"${value.replace(/\"/g, '\"\"')}\"`;\n const rows = periodRecords.map((r) =>\n [\n r.employeeName,\n r.department,\n r.period,\n String(r.grossPay),\n String(r.deductions),\n String(r.netPay),\n r.status,\n ]\n .map(escape)\n .join(',')\n );\n const csv = [header.map(escape).join(','), ...rows].join('\\n');\n const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });\n const url = URL.createObjectURL(blob);\n const link = document.createElement('a');\n link.href = url;\n link.download = `payroll-${activePeriod.replace(/\\s+/g, '-').toLowerCase()}.csv`;\n document.body.appendChild(link);\n link.click();\n document.body.removeChild(link);\n URL.revokeObjectURL(url);\n showToast('Payroll CSV exported', 'success');\n };\n\n const recordColumns: ResponsiveTableColumn<PayrollRecord>[] = [\n {\n key: 'employee',\n header: 'Employee',\n priority: 'primary',\n cell: (record) => (\n <div className=\"flex items-center gap-2.5\">\n <AvatarStack\n people={[\n { name: record.employeeName, avatarUrl: employeeById.get(record.employeeId)?.avatar },\n ]}\n size={32}\n />\n <span className=\"font-medium\">{record.employeeName}</span>\n </div>\n ),\n },\n {\n key: 'department',\n header: 'Department',\n cell: (record) => <span className=\"text-text-secondary\">{record.department}</span>,\n },\n {\n key: 'gross',\n header: 'Gross Pay',\n headerClassName: 'text-right',\n cellClassName: 'md:text-right',\n cell: (record) => <span>${record.grossPay.toLocaleString()}</span>,\n },\n {\n key: 'deductions',\n header: 'Deductions',\n headerClassName: 'text-right',\n cellClassName: 'md:text-right',\n cell: (record) => (\n <span className=\"text-status-error-text\">-${record.deductions.toLocaleString()}</span>\n ),\n },\n {\n key: 'net',\n header: 'Net Pay',\n headerClassName: 'text-right',\n cellClassName: 'md:text-right',\n cell: (record) => <span className=\"font-medium\">${record.netPay.toLocaleString()}</span>,\n },\n {\n key: 'method',\n header: 'Payment Method',\n cell: (record) => <span className=\"text-text-secondary\">{record.paymentMethod}</span>,\n },\n {\n key: 'status',\n header: 'Status',\n cell: (record) => (\n <div className=\"flex items-center gap-2\">\n <Badge\n variant={\n record.status === 'paid'\n ? 'default'\n : record.status === 'processing'\n ? 'secondary'\n : 'outline'\n }\n className=\"text-[10px]\"\n >\n {formatEnumLabel(record.status)}\n </Badge>\n {record.status !== 'paid' && (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-6 px-2 text-[10px]\"\n onClick={() => handleMarkPaid(record)}\n >\n Mark paid\n </Button>\n )}\n </div>\n ),\n },\n ];\n\n const handleCreate = async (values: FormValues) => {\n const employeeId = getText(values, 'employeeId');\n const employee = employees.find((item) => item.id === employeeId);\n const grossPay = getNumber(values, 'grossPay');\n const deductions = getNumber(values, 'deductions');\n const netPayInput = getNumber(values, 'netPay');\n const payload: PayrollRecord = {\n id: newClientId(),\n employeeId,\n employeeName: employee ? `${employee.firstName} ${employee.lastName}` : '',\n department: employee?.department ?? '',\n period: getText(values, 'period'),\n grossPay,\n deductions,\n netPay: netPayInput > 0 ? netPayInput : Math.max(grossPay - deductions, 0),\n status: (getText(values, 'status') as PayrollRecord['status']) || 'draft',\n payDate: getText(values, 'payDate'),\n paymentMethod: getText(values, 'paymentMethod'),\n };\n await addPayrollRecord(payload);\n };\n\n const handleMarkPaid = async (record: PayrollRecord) => {\n try {\n await updatePayrollRecord(record.id, {\n status: 'paid',\n payDate: new Date().toISOString().slice(0, 10),\n });\n } catch (error) {\n showToast(\n error instanceof Error ? error.message : 'Unable to update payroll record',\n 'error'\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>Payroll</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">\n {activePeriod ? `${activePeriod} pay run` : 'Process and manage employee payroll'}\n </p>\n <PagePurpose className=\"mt-1\">\n Run and review payroll — track salaries, deductions, and pay runs so your team gets paid\n accurately and on time.\n </PagePurpose>\n </div>\n <div className=\"flex gap-2\">\n <Button variant=\"outline\" size=\"sm\" onClick={handleExportCsv}>\n <Download className=\"h-4 w-4 mr-1\" />\n Export\n </Button>\n <Button variant=\"outline\" size=\"sm\" onClick={() => createForm.openModal()}>\n <Plus className=\"h-4 w-4 mr-1\" />\n Add Record\n </Button>\n <Button\n size=\"sm\"\n onClick={() => setRunOpen(true)}\n disabled={runComplete || periodRecords.length === 0}\n title={\n runComplete ? 'All records in this period are paid' : 'Advance this period to paid'\n }\n >\n <Play className=\"h-4 w-4 mr-1\" />\n Run Payroll\n </Button>\n </div>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n label=\"Total Payroll\"\n value={`$${totalGross.toLocaleString()}`}\n icon={<DollarSign className=\"h-4 w-4\" />}\n />\n <StatCard\n label=\"Employees Paid\"\n value={paidCount}\n icon={<CheckCircle className=\"h-4 w-4\" />}\n />\n <StatCard label=\"Pending\" value={pendingCount} icon={<Users className=\"h-4 w-4\" />} />\n <StatCard\n label=\"Next Run\"\n value={activePeriod ? nextRunLabel(activePeriod) : '—'}\n icon={<CalendarClock className=\"h-4 w-4\" />}\n />\n </div>\n\n {periodRecords.length > 0 && (\n <GlassCard treatment=\"glass\" glow className=\"border-border-seam\">\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0\">\n <CardTitle className=\"text-base\">Current pay run</CardTitle>\n <Badge\n variant={runComplete ? 'default' : 'secondary'}\n className=\"flex items-center gap-1\"\n >\n <CheckCircle className=\"h-3 w-3\" />\n {runComplete ? 'Completed' : 'Processing'}\n </Badge>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <div className=\"grid gap-4 sm:grid-cols-4\">\n <div>\n <p className=\"text-xs text-text-secondary\">Period</p>\n <p className=\"text-sm font-semibold\">{activePeriod}</p>\n </div>\n <div>\n <p className=\"text-xs text-text-secondary\">Gross Total</p>\n <p className=\"text-sm font-semibold tabular-nums\">${totalGross.toLocaleString()}</p>\n </div>\n <div>\n <p className=\"text-xs text-text-secondary\">Deductions</p>\n <p className=\"text-sm font-semibold tabular-nums text-status-error-text\">\n -${totalDeductions.toLocaleString()}\n </p>\n </div>\n <div>\n <p className=\"text-xs text-text-secondary\">Net Total</p>\n <p className=\"text-sm font-semibold tabular-nums\">${totalNet.toLocaleString()}</p>\n </div>\n </div>\n <div className=\"space-y-1\">\n <div className=\"flex items-center justify-between text-xs text-text-secondary\">\n <span>\n {paidCount} of {periodRecords.length} paid\n </span>\n <span className=\"font-medium text-text-primary tabular-nums\">{runPct}%</span>\n </div>\n <div className=\"h-2 overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-2 rounded-full bg-status-success-text transition-all duration-500\"\n style={{ width: `${runPct}%` }}\n />\n </div>\n </div>\n </CardContent>\n </GlassCard>\n )}\n\n {periods.length > 1 && (\n <div className=\"flex flex-wrap gap-2\">\n {periods.map((period) => (\n <Button\n key={period}\n variant={activePeriod === period ? 'default' : 'outline'}\n size=\"sm\"\n onClick={() => handlePeriodChange(period)}\n >\n {period}\n </Button>\n ))}\n </div>\n )}\n\n <Card>\n <CardHeader>\n <CardTitle>Payroll Records</CardTitle>\n <CardDescription>Individual employee payroll details for {activePeriod}</CardDescription>\n </CardHeader>\n <CardContent className=\"space-y-4\">\n <ResponsiveTable<PayrollRecord>\n data={recordPageItems}\n rowKey={(record) => record.id}\n columns={recordColumns}\n emptyState={\n <p className=\"py-6 text-center text-sm text-text-secondary\">\n No payroll records for {activePeriod || 'this period'}.\n </p>\n }\n />\n {periodRecords.length > pageSize && (\n <Pagination\n page={page}\n pageSize={pageSize}\n totalItems={periodRecords.length}\n onPageChange={setPage}\n />\n )}\n {periodRecords.length > 0 && (\n <>\n <Separator />\n <div className=\"flex flex-wrap items-center justify-between gap-4 text-sm font-semibold\">\n <span>Total for {activePeriod}</span>\n <div className=\"flex flex-wrap gap-x-6 gap-y-1\">\n <span>\n <span className=\"text-text-secondary\">Gross </span>$\n {totalGross.toLocaleString()}\n </span>\n <span className=\"text-status-error-text\">\n <span className=\"text-text-secondary\">Deductions </span>-$\n {totalDeductions.toLocaleString()}\n </span>\n <span>\n <span className=\"text-text-secondary\">Net </span>${totalNet.toLocaleString()}\n </span>\n </div>\n </div>\n </>\n )}\n </CardContent>\n </Card>\n\n <GlobalPayrollSection />\n\n <FormModal\n open={createForm.open}\n onOpenChange={createForm.onOpenChange}\n title=\"Add payroll record\"\n description=\"Create a payroll entry for a team member.\"\n fields={payrollFields}\n submitLabel=\"Add record\"\n onSubmit={handleCreate}\n />\n\n <ConfirmDialog\n open={runOpen}\n onOpenChange={(open) => {\n if (!open && !running) {\n setRunOpen(false);\n }\n }}\n title={`Run payroll for ${activePeriod}?`}\n description={`This marks the ${pendingCount} remaining record${\n pendingCount === 1 ? '' : 's'\n } in ${activePeriod} as paid. This cannot be undone.`}\n confirmLabel={running ? 'Processing…' : 'Run payroll'}\n onConfirm={handleRunPayroll}\n />\n </div>\n );\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { useNavigate as e } from "react-router-dom";
1
+ import { useNavigate as e } from "react-router";
2
2
  import { jsx as t, jsxs as n } from "react/jsx-runtime";
3
3
  import { Button as r, GradientText as i, IllustratedEmptyState as a, PagePurpose as o } from "@burdenoff/fe-libs/ui";
4
4
  //#region src/crewfoundry/pages/LearningAssessmentsPagePage.tsx
@@ -1 +1 @@
1
- {"version":3,"file":"LearningAssessmentsPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/LearningAssessmentsPagePage.tsx"],"sourcesContent":["import { Button, GradientText, IllustratedEmptyState, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { useNavigate } from 'react-router-dom';\n\nexport function LearningAssessmentsPagePage() {\n const navigate = useNavigate();\n\n return (\n <div className=\"space-y-6 p-6\">\n <div>\n <h1 className=\"text-2xl font-semibold\">\n <GradientText>Assessments</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">Quizzes and exams for your enrolled courses</p>\n <PagePurpose className=\"mt-1\">\n Create and review assessments that measure what your team has learned — use them to\n certify skills and identify knowledge gaps.\n </PagePurpose>\n </div>\n\n <IllustratedEmptyState\n illustration=\"empty-generic\"\n title=\"Assessments are coming soon\"\n description=\"Course assessments — question banks, timed attempts, scoring and pass thresholds — are being built. Once available, the quizzes tied to your enrolled courses will appear here.\"\n action={\n <Button variant=\"outline\" onClick={() => navigate('../learning/courses-page')}>\n Browse courses\n </Button>\n }\n />\n </div>\n );\n}\n"],"mappings":";;;;AAGA,SAAgB,IAA8B;CAC5C,IAAM,IAAW,GAAa;AAE9B,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,OAAD,EAAA,UAAA;GACE,kBAAC,MAAD;IAAI,WAAU;cACZ,kBAAC,GAAD,EAAA,UAAc,eAA0B,CAAA;IACrC,CAAA;GACL,kBAAC,KAAD;IAAG,WAAU;cAA8B;IAA+C,CAAA;GAC1F,kBAAC,GAAD;IAAa,WAAU;cAAO;IAGhB,CAAA;GACV,EAAA,CAAA,EAEN,kBAAC,GAAD;GACE,cAAa;GACb,OAAM;GACN,aAAY;GACZ,QACE,kBAAC,GAAD;IAAQ,SAAQ;IAAU,eAAe,EAAS,2BAA2B;cAAE;IAEtE,CAAA;GAEX,CAAA,CACE"}
1
+ {"version":3,"file":"LearningAssessmentsPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/LearningAssessmentsPagePage.tsx"],"sourcesContent":["import { Button, GradientText, IllustratedEmptyState, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { useNavigate } from 'react-router';\n\nexport function LearningAssessmentsPagePage() {\n const navigate = useNavigate();\n\n return (\n <div className=\"space-y-6 p-6\">\n <div>\n <h1 className=\"text-2xl font-semibold\">\n <GradientText>Assessments</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">Quizzes and exams for your enrolled courses</p>\n <PagePurpose className=\"mt-1\">\n Create and review assessments that measure what your team has learned — use them to\n certify skills and identify knowledge gaps.\n </PagePurpose>\n </div>\n\n <IllustratedEmptyState\n illustration=\"empty-generic\"\n title=\"Assessments are coming soon\"\n description=\"Course assessments — question banks, timed attempts, scoring and pass thresholds — are being built. Once available, the quizzes tied to your enrolled courses will appear here.\"\n action={\n <Button variant=\"outline\" onClick={() => navigate('../learning/courses-page')}>\n Browse courses\n </Button>\n }\n />\n </div>\n );\n}\n"],"mappings":";;;;AAGA,SAAgB,IAA8B;CAC5C,IAAM,IAAW,GAAa;AAE9B,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,OAAD,EAAA,UAAA;GACE,kBAAC,MAAD;IAAI,WAAU;cACZ,kBAAC,GAAD,EAAA,UAAc,eAA0B,CAAA;IACrC,CAAA;GACL,kBAAC,KAAD;IAAG,WAAU;cAA8B;IAA+C,CAAA;GAC1F,kBAAC,GAAD;IAAa,WAAU;cAAO;IAGhB,CAAA;GACV,EAAA,CAAA,EAEN,kBAAC,GAAD;GACE,cAAa;GACb,OAAM;GACN,aAAY;GACZ,QACE,kBAAC,GAAD;IAAQ,SAAQ;IAAU,eAAe,EAAS,2BAA2B;cAAE;IAEtE,CAAA;GAEX,CAAA,CACE"}
@@ -4,7 +4,7 @@ import "../components/viz/index.js";
4
4
  import { readAllCourseProgress as n } from "../utils/courseProgress.js";
5
5
  import { useMemo as r } from "react";
6
6
  import { GraduationCap as i } from "lucide-react";
7
- import { useNavigate as a } from "react-router-dom";
7
+ import { useNavigate as a } from "react-router";
8
8
  import { jsx as o, jsxs as s } from "react/jsx-runtime";
9
9
  import { Button as c, GradientText as l, IllustratedEmptyState as u, PagePurpose as d } from "@burdenoff/fe-libs/ui";
10
10
  import { useAuth as f } from "@burdenoff/fe-libs/shared/providers/shell";
@@ -1 +1 @@
1
- {"version":3,"file":"LearningCertificatesPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/LearningCertificatesPagePage.tsx"],"sourcesContent":["import { useMemo } from 'react';\nimport { Button, GradientText, IllustratedEmptyState, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { GraduationCap } from 'lucide-react';\nimport { useNavigate } from 'react-router-dom';\nimport { useAuth } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { useCrewFoundryStore } from '../store/crewfoundryStore';\nimport { CertificateCard } from '../components/viz';\nimport { readAllCourseProgress } from '../utils/courseProgress';\n\nfunction formatDate(iso?: string): string {\n if (!iso) return '—';\n const date = new Date(iso);\n if (Number.isNaN(date.getTime())) return '—';\n return date.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });\n}\n\nfunction downloadCertificate(\n course: string,\n recipient: string,\n dateLabel: string,\n id: string\n): void {\n if (typeof document === 'undefined') return;\n const esc = (s: string) =>\n s.replace(\n /[&<>\"]/g,\n (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;' })[c] ?? c\n );\n const html = `<!doctype html><html><head><meta charset=\"utf-8\"><title>Certificate — ${esc(course)}</title>\n<style>\n body{font-family:Georgia,'Times New Roman',serif;margin:0;padding:48px;text-align:center}\n .frame{border:6px double currentColor;padding:56px 40px;max-width:760px;margin:0 auto}\n .kicker{letter-spacing:.3em;text-transform:uppercase;font-size:12px;opacity:.7}\n h1{font-size:34px;margin:24px 0 8px}\n .recipient{font-size:26px;margin:20px 0}\n .rule{width:200px;border:0;border-top:1px solid currentColor;margin:24px auto;opacity:.4}\n .meta{font-size:13px;opacity:.7;margin-top:32px}\n</style></head><body>\n <div class=\"frame\">\n <div class=\"kicker\">CrewFoundry · Certificate of Completion</div>\n <h1>${esc(course)}</h1>\n <div class=\"kicker\">Awarded to</div>\n <div class=\"recipient\">${esc(recipient)}</div>\n <hr class=\"rule\" />\n <div class=\"meta\">Issued ${esc(dateLabel)} &nbsp;·&nbsp; Credential ID: ${esc(id)}</div>\n </div>\n</body></html>`;\n const blob = new Blob([html], { type: 'text/html' });\n const url = URL.createObjectURL(blob);\n const anchor = document.createElement('a');\n anchor.href = url;\n anchor.download = `crewfoundry-certificate-${id}.html`;\n document.body.appendChild(anchor);\n anchor.click();\n anchor.remove();\n URL.revokeObjectURL(url);\n}\n\nexport function LearningCertificatesPagePage() {\n const navigate = useNavigate();\n const auth = useAuth();\n const { courses } = useCrewFoundryStore();\n const recipient = auth.user?.name?.trim() || 'CrewFoundry Learner';\n\n const earned = useMemo(() => {\n const progressMap = readAllCourseProgress();\n return courses\n .map((course) => {\n const entry = progressMap[course.id];\n const completedModules = entry?.completed.length ?? 0;\n const totalModules = Math.max(course.modules, 1);\n const isComplete = completedModules > 0 && completedModules >= totalModules;\n return { course, isComplete, completedAt: entry?.completedAt };\n })\n .filter((item) => item.isComplete)\n .sort((a, b) => (b.completedAt ?? '').localeCompare(a.completedAt ?? ''));\n }, [courses]);\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div>\n <h1 className=\"text-2xl font-semibold\">\n <GradientText>Certificates</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">\n Proof of completed training and validated skills\n </p>\n <PagePurpose className=\"mt-1\">\n View and download the certificates you&apos;ve earned — proof of completed training and\n validated skills.\n </PagePurpose>\n </div>\n <Button variant=\"outline\" onClick={() => navigate('../learning/my-learning-page')}>\n <GraduationCap className=\"mr-1 h-4 w-4\" />\n My Learning\n </Button>\n </div>\n\n {earned.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-generic\"\n title=\"No certificates yet\"\n description=\"Certificates are issued when you complete a course. Finish a course from the player and every credential you earn will appear here with its issue date and a download link.\"\n action={\n <Button variant=\"outline\" onClick={() => navigate('../learning/courses-page')}>\n Browse courses\n </Button>\n }\n />\n ) : (\n <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-3\">\n {earned.map(({ course, completedAt }) => {\n const credentialId = `CF-${course.id.slice(-6).toUpperCase()}`;\n const dateLabel = formatDate(completedAt);\n return (\n <CertificateCard\n key={course.id}\n title={course.title}\n recipient={recipient}\n date={dateLabel}\n issuer=\"CrewFoundry\"\n credentialId={credentialId}\n onDownload={() =>\n downloadCertificate(course.title, recipient, dateLabel, credentialId)\n }\n />\n );\n })}\n </div>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;AASA,SAAS,EAAW,GAAsB;AACxC,KAAI,CAAC,EAAK,QAAO;CACjB,IAAM,IAAO,IAAI,KAAK,EAAI;AAE1B,QADI,OAAO,MAAM,EAAK,SAAS,CAAC,GAAS,MAClC,EAAK,mBAAmB,KAAA,GAAW;EAAE,MAAM;EAAW,OAAO;EAAS,KAAK;EAAW,CAAC;;AAGhG,SAAS,EACP,GACA,GACA,GACA,GACM;AACN,KAAI,OAAO,WAAa,IAAa;CACrC,IAAM,KAAO,MACX,EAAE,QACA,YACC,OAAO;EAAE,KAAK;EAAS,KAAK;EAAQ,KAAK;EAAQ,MAAK;EAAU,EAAE,MAAM,EAC1E,EACG,IAAO,yEAAyE,EAAI,EAAO,CAAC;;;;;;;;;;;;UAY1F,EAAI,EAAO,CAAC;;6BAEO,EAAI,EAAU,CAAC;;+BAEb,EAAI,EAAU,CAAC,gCAAgC,EAAI,EAAG,CAAC;;iBAG9E,IAAO,IAAI,KAAK,CAAC,EAAK,EAAE,EAAE,MAAM,aAAa,CAAC,EAC9C,IAAM,IAAI,gBAAgB,EAAK,EAC/B,IAAS,SAAS,cAAc,IAAI;AAM1C,CALA,EAAO,OAAO,GACd,EAAO,WAAW,2BAA2B,EAAG,QAChD,SAAS,KAAK,YAAY,EAAO,EACjC,EAAO,OAAO,EACd,EAAO,QAAQ,EACf,IAAI,gBAAgB,EAAI;;AAG1B,SAAgB,IAA+B;CAC7C,IAAM,IAAW,GAAa,EACxB,IAAO,GAAS,EAChB,EAAE,eAAY,GAAqB,EACnC,IAAY,EAAK,MAAM,MAAM,MAAM,IAAI,uBAEvC,IAAS,QAAc;EAC3B,IAAM,IAAc,GAAuB;AAC3C,SAAO,EACJ,KAAK,MAAW;GACf,IAAM,IAAQ,EAAY,EAAO,KAC3B,IAAmB,GAAO,UAAU,UAAU,GAC9C,IAAe,KAAK,IAAI,EAAO,SAAS,EAAE;AAEhD,UAAO;IAAE;IAAQ,YADE,IAAmB,KAAK,KAAoB;IAClC,aAAa,GAAO;IAAa;IAC9D,CACD,QAAQ,MAAS,EAAK,WAAW,CACjC,MAAM,GAAG,OAAO,EAAE,eAAe,IAAI,cAAc,EAAE,eAAe,GAAG,CAAC;IAC1E,CAAC,EAAQ,CAAC;AAEb,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD,EAAA,UAAA;IACE,kBAAC,MAAD;KAAI,WAAU;eACZ,kBAAC,GAAD,EAAA,UAAc,gBAA2B,CAAA;KACtC,CAAA;IACL,kBAAC,KAAD;KAAG,WAAU;eAA8B;KAEvC,CAAA;IACJ,kBAAC,GAAD;KAAa,WAAU;eAAO;KAGhB,CAAA;IACV,EAAA,CAAA,EACN,kBAAC,GAAD;IAAQ,SAAQ;IAAU,eAAe,EAAS,+BAA+B;cAAjF,CACE,kBAAC,GAAD,EAAe,WAAU,gBAAiB,CAAA,EAAA,cAEnC;MACL;MAEL,EAAO,WAAW,IACjB,kBAAC,GAAD;GACE,cAAa;GACb,OAAM;GACN,aAAY;GACZ,QACE,kBAAC,GAAD;IAAQ,SAAQ;IAAU,eAAe,EAAS,2BAA2B;cAAE;IAEtE,CAAA;GAEX,CAAA,GAEF,kBAAC,OAAD;GAAK,WAAU;aACZ,EAAO,KAAK,EAAE,WAAQ,qBAAkB;IACvC,IAAM,IAAe,MAAM,EAAO,GAAG,MAAM,GAAG,CAAC,aAAa,IACtD,IAAY,EAAW,EAAY;AACzC,WACE,kBAAC,GAAD;KAEE,OAAO,EAAO;KACH;KACX,MAAM;KACN,QAAO;KACO;KACd,kBACE,EAAoB,EAAO,OAAO,GAAW,GAAW,EAAa;KAEvE,EATK,EAAO,GASZ;KAEJ;GACE,CAAA,CAEJ"}
1
+ {"version":3,"file":"LearningCertificatesPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/LearningCertificatesPagePage.tsx"],"sourcesContent":["import { useMemo } from 'react';\nimport { Button, GradientText, IllustratedEmptyState, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { GraduationCap } from 'lucide-react';\nimport { useNavigate } from 'react-router';\nimport { useAuth } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { useCrewFoundryStore } from '../store/crewfoundryStore';\nimport { CertificateCard } from '../components/viz';\nimport { readAllCourseProgress } from '../utils/courseProgress';\n\nfunction formatDate(iso?: string): string {\n if (!iso) return '—';\n const date = new Date(iso);\n if (Number.isNaN(date.getTime())) return '—';\n return date.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });\n}\n\nfunction downloadCertificate(\n course: string,\n recipient: string,\n dateLabel: string,\n id: string\n): void {\n if (typeof document === 'undefined') return;\n const esc = (s: string) =>\n s.replace(\n /[&<>\"]/g,\n (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;' })[c] ?? c\n );\n const html = `<!doctype html><html><head><meta charset=\"utf-8\"><title>Certificate — ${esc(course)}</title>\n<style>\n body{font-family:Georgia,'Times New Roman',serif;margin:0;padding:48px;text-align:center}\n .frame{border:6px double currentColor;padding:56px 40px;max-width:760px;margin:0 auto}\n .kicker{letter-spacing:.3em;text-transform:uppercase;font-size:12px;opacity:.7}\n h1{font-size:34px;margin:24px 0 8px}\n .recipient{font-size:26px;margin:20px 0}\n .rule{width:200px;border:0;border-top:1px solid currentColor;margin:24px auto;opacity:.4}\n .meta{font-size:13px;opacity:.7;margin-top:32px}\n</style></head><body>\n <div class=\"frame\">\n <div class=\"kicker\">CrewFoundry · Certificate of Completion</div>\n <h1>${esc(course)}</h1>\n <div class=\"kicker\">Awarded to</div>\n <div class=\"recipient\">${esc(recipient)}</div>\n <hr class=\"rule\" />\n <div class=\"meta\">Issued ${esc(dateLabel)} &nbsp;·&nbsp; Credential ID: ${esc(id)}</div>\n </div>\n</body></html>`;\n const blob = new Blob([html], { type: 'text/html' });\n const url = URL.createObjectURL(blob);\n const anchor = document.createElement('a');\n anchor.href = url;\n anchor.download = `crewfoundry-certificate-${id}.html`;\n document.body.appendChild(anchor);\n anchor.click();\n anchor.remove();\n URL.revokeObjectURL(url);\n}\n\nexport function LearningCertificatesPagePage() {\n const navigate = useNavigate();\n const auth = useAuth();\n const { courses } = useCrewFoundryStore();\n const recipient = auth.user?.name?.trim() || 'CrewFoundry Learner';\n\n const earned = useMemo(() => {\n const progressMap = readAllCourseProgress();\n return courses\n .map((course) => {\n const entry = progressMap[course.id];\n const completedModules = entry?.completed.length ?? 0;\n const totalModules = Math.max(course.modules, 1);\n const isComplete = completedModules > 0 && completedModules >= totalModules;\n return { course, isComplete, completedAt: entry?.completedAt };\n })\n .filter((item) => item.isComplete)\n .sort((a, b) => (b.completedAt ?? '').localeCompare(a.completedAt ?? ''));\n }, [courses]);\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div>\n <h1 className=\"text-2xl font-semibold\">\n <GradientText>Certificates</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">\n Proof of completed training and validated skills\n </p>\n <PagePurpose className=\"mt-1\">\n View and download the certificates you&apos;ve earned — proof of completed training and\n validated skills.\n </PagePurpose>\n </div>\n <Button variant=\"outline\" onClick={() => navigate('../learning/my-learning-page')}>\n <GraduationCap className=\"mr-1 h-4 w-4\" />\n My Learning\n </Button>\n </div>\n\n {earned.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-generic\"\n title=\"No certificates yet\"\n description=\"Certificates are issued when you complete a course. Finish a course from the player and every credential you earn will appear here with its issue date and a download link.\"\n action={\n <Button variant=\"outline\" onClick={() => navigate('../learning/courses-page')}>\n Browse courses\n </Button>\n }\n />\n ) : (\n <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-3\">\n {earned.map(({ course, completedAt }) => {\n const credentialId = `CF-${course.id.slice(-6).toUpperCase()}`;\n const dateLabel = formatDate(completedAt);\n return (\n <CertificateCard\n key={course.id}\n title={course.title}\n recipient={recipient}\n date={dateLabel}\n issuer=\"CrewFoundry\"\n credentialId={credentialId}\n onDownload={() =>\n downloadCertificate(course.title, recipient, dateLabel, credentialId)\n }\n />\n );\n })}\n </div>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;AASA,SAAS,EAAW,GAAsB;AACxC,KAAI,CAAC,EAAK,QAAO;CACjB,IAAM,IAAO,IAAI,KAAK,EAAI;AAE1B,QADI,OAAO,MAAM,EAAK,SAAS,CAAC,GAAS,MAClC,EAAK,mBAAmB,KAAA,GAAW;EAAE,MAAM;EAAW,OAAO;EAAS,KAAK;EAAW,CAAC;;AAGhG,SAAS,EACP,GACA,GACA,GACA,GACM;AACN,KAAI,OAAO,WAAa,IAAa;CACrC,IAAM,KAAO,MACX,EAAE,QACA,YACC,OAAO;EAAE,KAAK;EAAS,KAAK;EAAQ,KAAK;EAAQ,MAAK;EAAU,EAAE,MAAM,EAC1E,EACG,IAAO,yEAAyE,EAAI,EAAO,CAAC;;;;;;;;;;;;UAY1F,EAAI,EAAO,CAAC;;6BAEO,EAAI,EAAU,CAAC;;+BAEb,EAAI,EAAU,CAAC,gCAAgC,EAAI,EAAG,CAAC;;iBAG9E,IAAO,IAAI,KAAK,CAAC,EAAK,EAAE,EAAE,MAAM,aAAa,CAAC,EAC9C,IAAM,IAAI,gBAAgB,EAAK,EAC/B,IAAS,SAAS,cAAc,IAAI;AAM1C,CALA,EAAO,OAAO,GACd,EAAO,WAAW,2BAA2B,EAAG,QAChD,SAAS,KAAK,YAAY,EAAO,EACjC,EAAO,OAAO,EACd,EAAO,QAAQ,EACf,IAAI,gBAAgB,EAAI;;AAG1B,SAAgB,IAA+B;CAC7C,IAAM,IAAW,GAAa,EACxB,IAAO,GAAS,EAChB,EAAE,eAAY,GAAqB,EACnC,IAAY,EAAK,MAAM,MAAM,MAAM,IAAI,uBAEvC,IAAS,QAAc;EAC3B,IAAM,IAAc,GAAuB;AAC3C,SAAO,EACJ,KAAK,MAAW;GACf,IAAM,IAAQ,EAAY,EAAO,KAC3B,IAAmB,GAAO,UAAU,UAAU,GAC9C,IAAe,KAAK,IAAI,EAAO,SAAS,EAAE;AAEhD,UAAO;IAAE;IAAQ,YADE,IAAmB,KAAK,KAAoB;IAClC,aAAa,GAAO;IAAa;IAC9D,CACD,QAAQ,MAAS,EAAK,WAAW,CACjC,MAAM,GAAG,OAAO,EAAE,eAAe,IAAI,cAAc,EAAE,eAAe,GAAG,CAAC;IAC1E,CAAC,EAAQ,CAAC;AAEb,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD,EAAA,UAAA;IACE,kBAAC,MAAD;KAAI,WAAU;eACZ,kBAAC,GAAD,EAAA,UAAc,gBAA2B,CAAA;KACtC,CAAA;IACL,kBAAC,KAAD;KAAG,WAAU;eAA8B;KAEvC,CAAA;IACJ,kBAAC,GAAD;KAAa,WAAU;eAAO;KAGhB,CAAA;IACV,EAAA,CAAA,EACN,kBAAC,GAAD;IAAQ,SAAQ;IAAU,eAAe,EAAS,+BAA+B;cAAjF,CACE,kBAAC,GAAD,EAAe,WAAU,gBAAiB,CAAA,EAAA,cAEnC;MACL;MAEL,EAAO,WAAW,IACjB,kBAAC,GAAD;GACE,cAAa;GACb,OAAM;GACN,aAAY;GACZ,QACE,kBAAC,GAAD;IAAQ,SAAQ;IAAU,eAAe,EAAS,2BAA2B;cAAE;IAEtE,CAAA;GAEX,CAAA,GAEF,kBAAC,OAAD;GAAK,WAAU;aACZ,EAAO,KAAK,EAAE,WAAQ,qBAAkB;IACvC,IAAM,IAAe,MAAM,EAAO,GAAG,MAAM,GAAG,CAAC,aAAa,IACtD,IAAY,EAAW,EAAY;AACzC,WACE,kBAAC,GAAD;KAEE,OAAO,EAAO;KACH;KACX,MAAM;KACN,QAAO;KACO;KACd,kBACE,EAAoB,EAAO,OAAO,GAAW,GAAW,EAAa;KAEvE,EATK,EAAO,GASZ;KAEJ;GACE,CAAA,CAEJ"}
@@ -2,7 +2,7 @@ import { useCrewFoundryStore as e } from "../store/crewfoundryStore.js";
2
2
  import { formatEnumLabel as t } from "../utils/formatEnumLabel.js";
3
3
  import { useState as n } from "react";
4
4
  import { BookOpen as r, Clock as i, GraduationCap as a, Play as o, Star as s, Users as c } from "lucide-react";
5
- import { useNavigate as l, useSearchParams as u } from "react-router-dom";
5
+ import { useNavigate as l, useSearchParams as u } from "react-router";
6
6
  import { jsx as d, jsxs as f } from "react/jsx-runtime";
7
7
  import { Badge as p, Button as m, Card as h, CardContent as g, CardHeader as _, CardTitle as v, EmphasisPanel as y, GradientText as b, IllustratedEmptyState as x, NextSteps as S, PagePurpose as C, Spinner as w } from "@burdenoff/fe-libs/ui";
8
8
  import { useGlobalUi as T } from "@burdenoff/fe-libs/shared/providers/shell";
@@ -1 +1 @@
1
- {"version":3,"file":"LearningCourseDetailsPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/LearningCourseDetailsPagePage.tsx"],"sourcesContent":["import {\n Badge,\n Button,\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n EmphasisPanel,\n GradientText,\n IllustratedEmptyState,\n NextSteps,\n PagePurpose,\n Spinner,\n} from '@burdenoff/fe-libs/ui';\nimport { BookOpen, Clock, GraduationCap, Play, Star, Users } from 'lucide-react';\nimport { useNavigate, useSearchParams } from 'react-router-dom';\nimport { useState } from 'react';\nimport { useGlobalUi } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { useCrewFoundryStore } from '../store/crewfoundryStore';\nimport { formatEnumLabel } from '../utils/formatEnumLabel';\n\nexport function LearningCourseDetailsPagePage() {\n const navigate = useNavigate();\n const [searchParams] = useSearchParams();\n const { courses, enrollCourse, initialized } = useCrewFoundryStore();\n const { showToast } = useGlobalUi();\n const [enrolling, setEnrolling] = useState(false);\n const courseId = searchParams.get('id');\n const course = courseId ? courses.find((c) => c.id === courseId) : courses[0];\n if (!initialized && !course) {\n return (\n <div className=\"flex min-h-[60vh] items-center justify-center p-6\">\n <Spinner size=\"lg\" />\n </div>\n );\n }\n\n if (!course)\n return (\n <div className=\"p-6\">\n <IllustratedEmptyState\n illustration=\"empty-search\"\n title=\"Course not found\"\n description=\"This course is no longer available.\"\n />\n </div>\n );\n const completionRate =\n course.enrolled > 0 ? Math.round((course.completed / course.enrolled) * 100) : 0;\n\n const handleStart = async () => {\n setEnrolling(true);\n try {\n await enrollCourse(course.id);\n showToast('Enrolled — your course is ready', 'success');\n navigate(`../learning/course-learning-page?id=${encodeURIComponent(course.id)}`);\n } catch (error) {\n showToast(error instanceof Error ? error.message : 'Unable to enroll in course', 'error');\n } finally {\n setEnrolling(false);\n }\n };\n\n return (\n <div className=\"space-y-6 p-6 max-w-4xl\">\n <div className=\"flex items-center justify-between\">\n <div>\n <div className=\"flex items-center gap-2 mb-1\">\n <h1 className=\"text-2xl font-semibold\">\n <GradientText>{course.title}</GradientText>\n </h1>\n <Badge\n variant={\n course.level === 'beginner'\n ? 'secondary'\n : course.level === 'intermediate'\n ? 'default'\n : 'destructive'\n }\n >\n {formatEnumLabel(course.level)}\n </Badge>\n </div>\n <p className=\"text-sm text-text-secondary\">\n by {course.instructor} · {course.category}\n </p>\n <PagePurpose className=\"mt-2\">\n Everything you need to decide on and start this course — what it covers, who it&apos;s\n for, and how others have done — so you can enroll with confidence and build the right\n skills.\n </PagePurpose>\n </div>\n <Button onClick={handleStart} disabled={enrolling}>\n <Play className=\"h-4 w-4 mr-1\" />\n {enrolling ? 'Enrolling…' : 'Start Course'}\n </Button>\n </div>\n\n <NextSteps\n storageKey=\"crewfoundry-course-details\"\n steps={[\n {\n id: 'start',\n label: 'Start the first module',\n description: 'Begin learning and track your progress as you go.',\n icon: <Play className=\"h-4 w-4\" />,\n onClick: () =>\n navigate(`../learning/course-learning-page?id=${encodeURIComponent(course.id)}`),\n },\n {\n id: 'my-learning',\n label: 'See all your learning',\n description: 'Review everything you are currently enrolled in.',\n icon: <GraduationCap className=\"h-4 w-4\" />,\n onClick: () => navigate('../learning/my-learning-page'),\n },\n {\n id: 'browse',\n label: 'Browse more courses',\n description: 'Find related training to round out your skills.',\n icon: <BookOpen className=\"h-4 w-4\" />,\n onClick: () => navigate('../learning/courses-page'),\n },\n ]}\n />\n\n <div className=\"grid gap-4 md:grid-cols-4\">\n <Card>\n <CardContent className=\"p-4 text-center\">\n <Clock className=\"h-5 w-5 mx-auto text-text-secondary mb-1\" />\n <p className=\"text-lg font-bold\">{course.duration}</p>\n <p className=\"text-xs text-text-secondary\">Duration</p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"p-4 text-center\">\n <BookOpen className=\"h-5 w-5 mx-auto text-text-secondary mb-1\" />\n <p className=\"text-lg font-bold\">{course.modules}</p>\n <p className=\"text-xs text-text-secondary\">Modules</p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"p-4 text-center\">\n <Users className=\"h-5 w-5 mx-auto text-text-secondary mb-1\" />\n <p className=\"text-lg font-bold\">{course.enrolled}</p>\n <p className=\"text-xs text-text-secondary\">Enrolled</p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"p-4 text-center\">\n <Star className=\"h-5 w-5 mx-auto text-status-warning-text mb-1\" />\n <p className=\"text-lg font-bold\">{course.rating}/5</p>\n <p className=\"text-xs text-text-secondary\">Rating</p>\n </CardContent>\n </Card>\n </div>\n <EmphasisPanel>\n <div className=\"mb-2 text-lg font-semibold\">About this Course</div>\n <p className=\"text-sm text-text-secondary\">{course.description}</p>\n <div className=\"flex flex-wrap gap-1 mt-3\">\n {course.skills.map((s) => (\n <Badge key={s} variant=\"outline\">\n {s}\n </Badge>\n ))}\n </div>\n </EmphasisPanel>\n <Card>\n <CardHeader>\n <CardTitle>Completion Statistics</CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"space-y-2\">\n <div className=\"flex justify-between text-sm\">\n <span>\n {course.completed} of {course.enrolled} students completed\n </span>\n <span className=\"font-medium\">{completionRate}%</span>\n </div>\n <div className=\"h-3 bg-bg-sunken rounded overflow-hidden\">\n <div\n className=\"h-3 bg-status-success-text rounded\"\n style={{ width: `${completionRate}%` }}\n />\n </div>\n </div>\n </CardContent>\n </Card>\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"LearningCourseDetailsPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/LearningCourseDetailsPagePage.tsx"],"sourcesContent":["import {\n Badge,\n Button,\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n EmphasisPanel,\n GradientText,\n IllustratedEmptyState,\n NextSteps,\n PagePurpose,\n Spinner,\n} from '@burdenoff/fe-libs/ui';\nimport { BookOpen, Clock, GraduationCap, Play, Star, Users } from 'lucide-react';\nimport { useNavigate, useSearchParams } from 'react-router';\nimport { useState } from 'react';\nimport { useGlobalUi } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { useCrewFoundryStore } from '../store/crewfoundryStore';\nimport { formatEnumLabel } from '../utils/formatEnumLabel';\n\nexport function LearningCourseDetailsPagePage() {\n const navigate = useNavigate();\n const [searchParams] = useSearchParams();\n const { courses, enrollCourse, initialized } = useCrewFoundryStore();\n const { showToast } = useGlobalUi();\n const [enrolling, setEnrolling] = useState(false);\n const courseId = searchParams.get('id');\n const course = courseId ? courses.find((c) => c.id === courseId) : courses[0];\n if (!initialized && !course) {\n return (\n <div className=\"flex min-h-[60vh] items-center justify-center p-6\">\n <Spinner size=\"lg\" />\n </div>\n );\n }\n\n if (!course)\n return (\n <div className=\"p-6\">\n <IllustratedEmptyState\n illustration=\"empty-search\"\n title=\"Course not found\"\n description=\"This course is no longer available.\"\n />\n </div>\n );\n const completionRate =\n course.enrolled > 0 ? Math.round((course.completed / course.enrolled) * 100) : 0;\n\n const handleStart = async () => {\n setEnrolling(true);\n try {\n await enrollCourse(course.id);\n showToast('Enrolled — your course is ready', 'success');\n navigate(`../learning/course-learning-page?id=${encodeURIComponent(course.id)}`);\n } catch (error) {\n showToast(error instanceof Error ? error.message : 'Unable to enroll in course', 'error');\n } finally {\n setEnrolling(false);\n }\n };\n\n return (\n <div className=\"space-y-6 p-6 max-w-4xl\">\n <div className=\"flex items-center justify-between\">\n <div>\n <div className=\"flex items-center gap-2 mb-1\">\n <h1 className=\"text-2xl font-semibold\">\n <GradientText>{course.title}</GradientText>\n </h1>\n <Badge\n variant={\n course.level === 'beginner'\n ? 'secondary'\n : course.level === 'intermediate'\n ? 'default'\n : 'destructive'\n }\n >\n {formatEnumLabel(course.level)}\n </Badge>\n </div>\n <p className=\"text-sm text-text-secondary\">\n by {course.instructor} · {course.category}\n </p>\n <PagePurpose className=\"mt-2\">\n Everything you need to decide on and start this course — what it covers, who it&apos;s\n for, and how others have done — so you can enroll with confidence and build the right\n skills.\n </PagePurpose>\n </div>\n <Button onClick={handleStart} disabled={enrolling}>\n <Play className=\"h-4 w-4 mr-1\" />\n {enrolling ? 'Enrolling…' : 'Start Course'}\n </Button>\n </div>\n\n <NextSteps\n storageKey=\"crewfoundry-course-details\"\n steps={[\n {\n id: 'start',\n label: 'Start the first module',\n description: 'Begin learning and track your progress as you go.',\n icon: <Play className=\"h-4 w-4\" />,\n onClick: () =>\n navigate(`../learning/course-learning-page?id=${encodeURIComponent(course.id)}`),\n },\n {\n id: 'my-learning',\n label: 'See all your learning',\n description: 'Review everything you are currently enrolled in.',\n icon: <GraduationCap className=\"h-4 w-4\" />,\n onClick: () => navigate('../learning/my-learning-page'),\n },\n {\n id: 'browse',\n label: 'Browse more courses',\n description: 'Find related training to round out your skills.',\n icon: <BookOpen className=\"h-4 w-4\" />,\n onClick: () => navigate('../learning/courses-page'),\n },\n ]}\n />\n\n <div className=\"grid gap-4 md:grid-cols-4\">\n <Card>\n <CardContent className=\"p-4 text-center\">\n <Clock className=\"h-5 w-5 mx-auto text-text-secondary mb-1\" />\n <p className=\"text-lg font-bold\">{course.duration}</p>\n <p className=\"text-xs text-text-secondary\">Duration</p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"p-4 text-center\">\n <BookOpen className=\"h-5 w-5 mx-auto text-text-secondary mb-1\" />\n <p className=\"text-lg font-bold\">{course.modules}</p>\n <p className=\"text-xs text-text-secondary\">Modules</p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"p-4 text-center\">\n <Users className=\"h-5 w-5 mx-auto text-text-secondary mb-1\" />\n <p className=\"text-lg font-bold\">{course.enrolled}</p>\n <p className=\"text-xs text-text-secondary\">Enrolled</p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"p-4 text-center\">\n <Star className=\"h-5 w-5 mx-auto text-status-warning-text mb-1\" />\n <p className=\"text-lg font-bold\">{course.rating}/5</p>\n <p className=\"text-xs text-text-secondary\">Rating</p>\n </CardContent>\n </Card>\n </div>\n <EmphasisPanel>\n <div className=\"mb-2 text-lg font-semibold\">About this Course</div>\n <p className=\"text-sm text-text-secondary\">{course.description}</p>\n <div className=\"flex flex-wrap gap-1 mt-3\">\n {course.skills.map((s) => (\n <Badge key={s} variant=\"outline\">\n {s}\n </Badge>\n ))}\n </div>\n </EmphasisPanel>\n <Card>\n <CardHeader>\n <CardTitle>Completion Statistics</CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"space-y-2\">\n <div className=\"flex justify-between text-sm\">\n <span>\n {course.completed} of {course.enrolled} students completed\n </span>\n <span className=\"font-medium\">{completionRate}%</span>\n </div>\n <div className=\"h-3 bg-bg-sunken rounded overflow-hidden\">\n <div\n className=\"h-3 bg-status-success-text rounded\"\n style={{ width: `${completionRate}%` }}\n />\n </div>\n </div>\n </CardContent>\n </Card>\n </div>\n );\n}\n"],"mappings":"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"}
@@ -5,7 +5,7 @@ import "../components/viz/index.js";
5
5
  import { progressPercent as r, readCourseProgress as i, writeCourseProgress as a } from "../utils/courseProgress.js";
6
6
  import { useMemo as o, useState as s } from "react";
7
7
  import { Check as c, ChevronLeft as l, ChevronRight as u, CircleDashed as d, FileText as f, Play as p } from "lucide-react";
8
- import { useNavigate as m, useSearchParams as h } from "react-router-dom";
8
+ import { useNavigate as m, useSearchParams as h } from "react-router";
9
9
  import { Fragment as g, jsx as _, jsxs as v } from "react/jsx-runtime";
10
10
  import { Badge as y, Button as b, Card as x, CardContent as S, GlassCard as C, IllustratedEmptyState as w, Spinner as T, Textarea as E } from "@burdenoff/fe-libs/ui";
11
11
  import { useGlobalUi as D } from "@burdenoff/fe-libs/shared/providers/shell";
@@ -1 +1 @@
1
- {"version":3,"file":"LearningCourseLearningPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/LearningCourseLearningPagePage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport {\n Badge,\n Button,\n Card,\n CardContent,\n GlassCard,\n IllustratedEmptyState,\n Spinner,\n Textarea,\n} from '@burdenoff/fe-libs/ui';\nimport { useGlobalUi } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { Check, ChevronLeft, ChevronRight, CircleDashed, FileText, Play } from 'lucide-react';\nimport { useNavigate, useSearchParams } from 'react-router-dom';\nimport { useCrewFoundryStore } from '../store/crewfoundryStore';\nimport { SegmentedTabs } from '../components/viz';\nimport { formatEnumLabel } from '../utils/formatEnumLabel';\nimport { readCourseProgress, writeCourseProgress, progressPercent } from '../utils/courseProgress';\n\nconst ENROLLED_KEY = 'crewfoundry:course-enrolled:v1';\nconst NOTES_KEY = 'crewfoundry:course-notes:v1';\n\nfunction hasEnrolledLocally(courseId: string): boolean {\n if (typeof window === 'undefined') return false;\n try {\n const raw = window.localStorage.getItem(ENROLLED_KEY);\n const ids: unknown = raw ? JSON.parse(raw) : [];\n return Array.isArray(ids) && ids.includes(courseId);\n } catch {\n return false;\n }\n}\n\nfunction markEnrolledLocally(courseId: string): void {\n if (typeof window === 'undefined') return;\n try {\n const raw = window.localStorage.getItem(ENROLLED_KEY);\n const ids: unknown = raw ? JSON.parse(raw) : [];\n const list = Array.isArray(ids) ? ids.filter((x): x is string => typeof x === 'string') : [];\n if (!list.includes(courseId)) {\n list.push(courseId);\n window.localStorage.setItem(ENROLLED_KEY, JSON.stringify(list));\n }\n } catch {\n /* best-effort */\n }\n}\n\nfunction readNotes(courseId: string): string {\n if (typeof window === 'undefined') return '';\n try {\n const raw = window.localStorage.getItem(NOTES_KEY);\n const map: unknown = raw ? JSON.parse(raw) : {};\n if (map && typeof map === 'object') {\n const value = (map as Record<string, unknown>)[courseId];\n return typeof value === 'string' ? value : '';\n }\n return '';\n } catch {\n return '';\n }\n}\n\nfunction writeNotes(courseId: string, notes: string): void {\n if (typeof window === 'undefined') return;\n try {\n const raw = window.localStorage.getItem(NOTES_KEY);\n const parsed: unknown = raw ? JSON.parse(raw) : {};\n const map: Record<string, string> =\n parsed && typeof parsed === 'object' ? (parsed as Record<string, string>) : {};\n map[courseId] = notes;\n window.localStorage.setItem(NOTES_KEY, JSON.stringify(map));\n } catch {\n /* best-effort */\n }\n}\n\ntype PlayerTab = 'overview' | 'resources' | 'notes';\n\nexport function LearningCourseLearningPagePage() {\n const navigate = useNavigate();\n const [searchParams] = useSearchParams();\n const { courses, enrollCourse, initialized } = useCrewFoundryStore();\n const { showToast } = useGlobalUi();\n\n const courseId = searchParams.get('id');\n const course = courseId ? courses.find((c) => c.id === courseId) : courses[0];\n\n const totalModules = course ? Math.max(course.modules, 1) : 1;\n\n const [completed, setCompleted] = useState<number[]>(() =>\n course ? readCourseProgress(course.id).completed : []\n );\n const [current, setCurrent] = useState<number>(() => {\n if (!course) return 0;\n const done = new Set(readCourseProgress(course.id).completed);\n for (let i = 0; i < Math.max(course.modules, 1); i += 1) {\n if (!done.has(i)) return i;\n }\n return 0;\n });\n const [tab, setTab] = useState<PlayerTab>('overview');\n const [notes, setNotes] = useState<string>(() => (course ? readNotes(course.id) : ''));\n\n const completedSet = useMemo(() => new Set(completed), [completed]);\n const pct = progressPercent({ completed }, totalModules);\n\n if (!initialized && !course) {\n return (\n <div className=\"flex min-h-[60vh] items-center justify-center p-6\">\n <Spinner size=\"lg\" />\n </div>\n );\n }\n\n if (!course) {\n return (\n <div className=\"p-6\">\n <IllustratedEmptyState\n illustration=\"empty-search\"\n title=\"Course not found\"\n description=\"This course is no longer available.\"\n action={\n <Button variant=\"outline\" onClick={() => navigate('../learning/courses-page')}>\n Browse courses\n </Button>\n }\n />\n </div>\n );\n }\n\n const persist = (next: number[]) => {\n setCompleted(next);\n writeCourseProgress(course.id, next, totalModules);\n };\n\n const ensureEnrolled = async () => {\n if (hasEnrolledLocally(course.id)) return;\n markEnrolledLocally(course.id);\n try {\n await enrollCourse(course.id);\n } catch {\n /* enrolment is a best-effort side effect of starting the course */\n }\n };\n\n const handleMarkComplete = async () => {\n if (!completedSet.has(current)) {\n const wasEmpty = completed.length === 0;\n const next = [...completed, current];\n persist(next);\n if (wasEmpty) {\n await ensureEnrolled();\n }\n if (next.length >= totalModules) {\n showToast(`You completed ${course.title}`, 'success');\n }\n }\n if (current < totalModules - 1) {\n setCurrent(current + 1);\n setTab('overview');\n }\n };\n\n const goTo = (index: number) => {\n setCurrent(index);\n setTab('overview');\n };\n\n const goPrev = () => current > 0 && goTo(current - 1);\n const goNext = () => current < totalModules - 1 && goTo(current + 1);\n\n const currentComplete = completedSet.has(current);\n const moduleTitle = `Module ${current + 1}`;\n\n return (\n <div className=\"space-y-5 p-6\">\n <div className=\"flex flex-wrap items-center gap-x-2 gap-y-1 text-sm text-text-secondary\">\n <button\n type=\"button\"\n onClick={() => navigate('../learning/courses-page')}\n className=\"hover:text-text-primary\"\n >\n Learning\n </button>\n <ChevronRight className=\"h-3.5 w-3.5\" />\n <button\n type=\"button\"\n onClick={() =>\n navigate(`../learning/course-details-page?id=${encodeURIComponent(course.id)}`)\n }\n className=\"hover:text-text-primary\"\n >\n Courses\n </button>\n <ChevronRight className=\"h-3.5 w-3.5\" />\n <span className=\"font-medium text-text-primary\">{course.title}</span>\n </div>\n\n <div className=\"grid grid-cols-1 gap-5 lg:grid-cols-[minmax(0,20rem)_minmax(0,1fr)]\">\n {/* Course content sidebar */}\n <Card className=\"lg:sticky lg:top-4 lg:max-h-[calc(100vh-2rem)] lg:overflow-y-auto\">\n <CardContent className=\"p-4\">\n <div className=\"mb-3 flex items-baseline justify-between gap-2\">\n <h2 className=\"text-sm font-semibold\">Course content</h2>\n <span className=\"text-xs font-medium text-text-secondary tabular-nums\">\n {pct}% complete\n </span>\n </div>\n <div className=\"mb-4 h-2 overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-action-primary-bg transition-[width] duration-500\"\n style={{ width: `${pct}%` }}\n />\n </div>\n <ol className=\"space-y-1\">\n {Array.from({ length: totalModules }, (_, i) => {\n const done = completedSet.has(i);\n const isCurrent = i === current;\n return (\n <li key={i}>\n <button\n type=\"button\"\n onClick={() => goTo(i)}\n aria-current={isCurrent ? 'step' : undefined}\n className={`flex w-full items-center gap-3 rounded-md px-2.5 py-2 text-left text-sm transition-colors ${\n isCurrent\n ? 'bg-action-primary-bg/10 font-medium text-text-primary'\n : 'text-text-secondary hover:bg-bg-sunken hover:text-text-primary'\n }`}\n >\n <span\n className={`flex h-5 w-5 shrink-0 items-center justify-center rounded-full border ${\n done\n ? 'border-transparent bg-status-success-bg text-status-success-text'\n : isCurrent\n ? 'border-action-primary-bg text-action-primary-bg'\n : 'border-border-default text-transparent'\n }`}\n >\n {done ? (\n <Check className=\"h-3 w-3\" />\n ) : (\n <CircleDashed className=\"h-3 w-3\" />\n )}\n </span>\n <span className=\"truncate\">{`Module ${i + 1}`}</span>\n </button>\n </li>\n );\n })}\n </ol>\n </CardContent>\n </Card>\n\n {/* Main pane */}\n <div className=\"space-y-5\">\n <GlassCard treatment=\"plain\" className=\"overflow-hidden\">\n <button\n type=\"button\"\n onClick={() => void handleMarkComplete()}\n aria-label={currentComplete ? 'Next module' : 'Mark this module complete'}\n className=\"group relative flex aspect-video w-full items-center justify-center outline-none focus-visible:shadow-[var(--shadow-focus)]\"\n style={{\n background:\n 'linear-gradient(135deg, color-mix(in srgb, var(--color-accent-primary) 24%, var(--color-bg-sunken)), var(--color-bg-sunken))',\n }}\n >\n <span className=\"absolute left-4 top-4\">\n <Badge variant=\"secondary\" className=\"text-[10px]\">\n {moduleTitle}\n </Badge>\n </span>\n <span className=\"flex h-16 w-16 items-center justify-center rounded-full bg-action-primary-bg text-action-primary-text shadow-elevation-2 transition-transform group-hover:scale-105\">\n {currentComplete ? (\n <Check className=\"h-7 w-7\" />\n ) : (\n <Play className=\"ml-1 h-7 w-7\" />\n )}\n </span>\n </button>\n <CardContent className=\"space-y-4 p-5\">\n <div>\n <div className=\"flex flex-wrap items-center gap-2\">\n <h1 className=\"text-xl font-semibold\">{moduleTitle}</h1>\n <Badge\n variant={\n course.level === 'beginner'\n ? 'secondary'\n : course.level === 'intermediate'\n ? 'default'\n : 'destructive'\n }\n className=\"text-[10px]\"\n >\n {formatEnumLabel(course.level)}\n </Badge>\n {currentComplete && (\n <span className=\"inline-flex items-center gap-1 text-xs font-medium text-status-success-text\">\n <Check className=\"h-3.5 w-3.5\" /> Completed\n </span>\n )}\n </div>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {course.title} · by {course.instructor}\n </p>\n </div>\n\n <SegmentedTabs\n variant=\"underline\"\n aria-label=\"Lesson detail\"\n value={tab}\n onChange={(id) => setTab(id as PlayerTab)}\n tabs={[\n { id: 'overview', label: 'Overview' },\n { id: 'resources', label: 'Resources' },\n { id: 'notes', label: 'Notes' },\n ]}\n />\n\n {tab === 'overview' && (\n <div className=\"space-y-3\">\n <p className=\"text-sm text-text-secondary\">{course.description}</p>\n {course.skills.length > 0 && (\n <div className=\"flex flex-wrap gap-1\">\n {course.skills.map((s) => (\n <Badge key={s} variant=\"outline\" className=\"text-[10px]\">\n {s}\n </Badge>\n ))}\n </div>\n )}\n </div>\n )}\n\n {tab === 'resources' && (\n <div className=\"rounded-lg border border-dashed border-border-default p-6 text-center\">\n <FileText className=\"mx-auto mb-2 h-6 w-6 text-text-muted\" />\n <p className=\"text-sm font-medium\">No downloadable resources yet</p>\n <p className=\"text-xs text-text-secondary\">\n Slides and attachments for this module will appear here when the instructor adds\n them.\n </p>\n </div>\n )}\n\n {tab === 'notes' && (\n <div className=\"space-y-2\">\n <Textarea\n value={notes}\n onChange={(e) => {\n setNotes(e.target.value);\n writeNotes(course.id, e.target.value);\n }}\n placeholder=\"Jot down your notes for this course. They are saved to this device automatically.\"\n rows={5}\n />\n <p className=\"text-xs text-text-muted\">\n Notes are saved automatically on this device.\n </p>\n </div>\n )}\n </CardContent>\n </GlassCard>\n\n {/* Footer controls */}\n <Card>\n <CardContent className=\"space-y-4 p-4\">\n <div className=\"h-2 overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-status-success-bg transition-[width] duration-500\"\n style={{ width: `${pct}%` }}\n />\n </div>\n <div className=\"flex items-center justify-between gap-2\">\n <Button variant=\"outline\" onClick={goPrev} disabled={current === 0}>\n <ChevronLeft className=\"mr-1 h-4 w-4\" />\n Previous\n </Button>\n <div className=\"flex items-center gap-2\">\n <Button onClick={() => void handleMarkComplete()}>\n {currentComplete ? (\n current < totalModules - 1 ? (\n 'Next module'\n ) : (\n <>\n <Check className=\"mr-1 h-4 w-4\" /> Completed\n </>\n )\n ) : (\n <>\n <Check className=\"mr-1 h-4 w-4\" /> Mark Complete\n </>\n )}\n </Button>\n <Button variant=\"outline\" onClick={goNext} disabled={current >= totalModules - 1}>\n Next\n <ChevronRight className=\"ml-1 h-4 w-4\" />\n </Button>\n </div>\n </div>\n </CardContent>\n </Card>\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"LearningCourseLearningPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/LearningCourseLearningPagePage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport {\n Badge,\n Button,\n Card,\n CardContent,\n GlassCard,\n IllustratedEmptyState,\n Spinner,\n Textarea,\n} from '@burdenoff/fe-libs/ui';\nimport { useGlobalUi } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { Check, ChevronLeft, ChevronRight, CircleDashed, FileText, Play } from 'lucide-react';\nimport { useNavigate, useSearchParams } from 'react-router';\nimport { useCrewFoundryStore } from '../store/crewfoundryStore';\nimport { SegmentedTabs } from '../components/viz';\nimport { formatEnumLabel } from '../utils/formatEnumLabel';\nimport { readCourseProgress, writeCourseProgress, progressPercent } from '../utils/courseProgress';\n\nconst ENROLLED_KEY = 'crewfoundry:course-enrolled:v1';\nconst NOTES_KEY = 'crewfoundry:course-notes:v1';\n\nfunction hasEnrolledLocally(courseId: string): boolean {\n if (typeof window === 'undefined') return false;\n try {\n const raw = window.localStorage.getItem(ENROLLED_KEY);\n const ids: unknown = raw ? JSON.parse(raw) : [];\n return Array.isArray(ids) && ids.includes(courseId);\n } catch {\n return false;\n }\n}\n\nfunction markEnrolledLocally(courseId: string): void {\n if (typeof window === 'undefined') return;\n try {\n const raw = window.localStorage.getItem(ENROLLED_KEY);\n const ids: unknown = raw ? JSON.parse(raw) : [];\n const list = Array.isArray(ids) ? ids.filter((x): x is string => typeof x === 'string') : [];\n if (!list.includes(courseId)) {\n list.push(courseId);\n window.localStorage.setItem(ENROLLED_KEY, JSON.stringify(list));\n }\n } catch {\n /* best-effort */\n }\n}\n\nfunction readNotes(courseId: string): string {\n if (typeof window === 'undefined') return '';\n try {\n const raw = window.localStorage.getItem(NOTES_KEY);\n const map: unknown = raw ? JSON.parse(raw) : {};\n if (map && typeof map === 'object') {\n const value = (map as Record<string, unknown>)[courseId];\n return typeof value === 'string' ? value : '';\n }\n return '';\n } catch {\n return '';\n }\n}\n\nfunction writeNotes(courseId: string, notes: string): void {\n if (typeof window === 'undefined') return;\n try {\n const raw = window.localStorage.getItem(NOTES_KEY);\n const parsed: unknown = raw ? JSON.parse(raw) : {};\n const map: Record<string, string> =\n parsed && typeof parsed === 'object' ? (parsed as Record<string, string>) : {};\n map[courseId] = notes;\n window.localStorage.setItem(NOTES_KEY, JSON.stringify(map));\n } catch {\n /* best-effort */\n }\n}\n\ntype PlayerTab = 'overview' | 'resources' | 'notes';\n\nexport function LearningCourseLearningPagePage() {\n const navigate = useNavigate();\n const [searchParams] = useSearchParams();\n const { courses, enrollCourse, initialized } = useCrewFoundryStore();\n const { showToast } = useGlobalUi();\n\n const courseId = searchParams.get('id');\n const course = courseId ? courses.find((c) => c.id === courseId) : courses[0];\n\n const totalModules = course ? Math.max(course.modules, 1) : 1;\n\n const [completed, setCompleted] = useState<number[]>(() =>\n course ? readCourseProgress(course.id).completed : []\n );\n const [current, setCurrent] = useState<number>(() => {\n if (!course) return 0;\n const done = new Set(readCourseProgress(course.id).completed);\n for (let i = 0; i < Math.max(course.modules, 1); i += 1) {\n if (!done.has(i)) return i;\n }\n return 0;\n });\n const [tab, setTab] = useState<PlayerTab>('overview');\n const [notes, setNotes] = useState<string>(() => (course ? readNotes(course.id) : ''));\n\n const completedSet = useMemo(() => new Set(completed), [completed]);\n const pct = progressPercent({ completed }, totalModules);\n\n if (!initialized && !course) {\n return (\n <div className=\"flex min-h-[60vh] items-center justify-center p-6\">\n <Spinner size=\"lg\" />\n </div>\n );\n }\n\n if (!course) {\n return (\n <div className=\"p-6\">\n <IllustratedEmptyState\n illustration=\"empty-search\"\n title=\"Course not found\"\n description=\"This course is no longer available.\"\n action={\n <Button variant=\"outline\" onClick={() => navigate('../learning/courses-page')}>\n Browse courses\n </Button>\n }\n />\n </div>\n );\n }\n\n const persist = (next: number[]) => {\n setCompleted(next);\n writeCourseProgress(course.id, next, totalModules);\n };\n\n const ensureEnrolled = async () => {\n if (hasEnrolledLocally(course.id)) return;\n markEnrolledLocally(course.id);\n try {\n await enrollCourse(course.id);\n } catch {\n /* enrolment is a best-effort side effect of starting the course */\n }\n };\n\n const handleMarkComplete = async () => {\n if (!completedSet.has(current)) {\n const wasEmpty = completed.length === 0;\n const next = [...completed, current];\n persist(next);\n if (wasEmpty) {\n await ensureEnrolled();\n }\n if (next.length >= totalModules) {\n showToast(`You completed ${course.title}`, 'success');\n }\n }\n if (current < totalModules - 1) {\n setCurrent(current + 1);\n setTab('overview');\n }\n };\n\n const goTo = (index: number) => {\n setCurrent(index);\n setTab('overview');\n };\n\n const goPrev = () => current > 0 && goTo(current - 1);\n const goNext = () => current < totalModules - 1 && goTo(current + 1);\n\n const currentComplete = completedSet.has(current);\n const moduleTitle = `Module ${current + 1}`;\n\n return (\n <div className=\"space-y-5 p-6\">\n <div className=\"flex flex-wrap items-center gap-x-2 gap-y-1 text-sm text-text-secondary\">\n <button\n type=\"button\"\n onClick={() => navigate('../learning/courses-page')}\n className=\"hover:text-text-primary\"\n >\n Learning\n </button>\n <ChevronRight className=\"h-3.5 w-3.5\" />\n <button\n type=\"button\"\n onClick={() =>\n navigate(`../learning/course-details-page?id=${encodeURIComponent(course.id)}`)\n }\n className=\"hover:text-text-primary\"\n >\n Courses\n </button>\n <ChevronRight className=\"h-3.5 w-3.5\" />\n <span className=\"font-medium text-text-primary\">{course.title}</span>\n </div>\n\n <div className=\"grid grid-cols-1 gap-5 lg:grid-cols-[minmax(0,20rem)_minmax(0,1fr)]\">\n {/* Course content sidebar */}\n <Card className=\"lg:sticky lg:top-4 lg:max-h-[calc(100vh-2rem)] lg:overflow-y-auto\">\n <CardContent className=\"p-4\">\n <div className=\"mb-3 flex items-baseline justify-between gap-2\">\n <h2 className=\"text-sm font-semibold\">Course content</h2>\n <span className=\"text-xs font-medium text-text-secondary tabular-nums\">\n {pct}% complete\n </span>\n </div>\n <div className=\"mb-4 h-2 overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-action-primary-bg transition-[width] duration-500\"\n style={{ width: `${pct}%` }}\n />\n </div>\n <ol className=\"space-y-1\">\n {Array.from({ length: totalModules }, (_, i) => {\n const done = completedSet.has(i);\n const isCurrent = i === current;\n return (\n <li key={i}>\n <button\n type=\"button\"\n onClick={() => goTo(i)}\n aria-current={isCurrent ? 'step' : undefined}\n className={`flex w-full items-center gap-3 rounded-md px-2.5 py-2 text-left text-sm transition-colors ${\n isCurrent\n ? 'bg-action-primary-bg/10 font-medium text-text-primary'\n : 'text-text-secondary hover:bg-bg-sunken hover:text-text-primary'\n }`}\n >\n <span\n className={`flex h-5 w-5 shrink-0 items-center justify-center rounded-full border ${\n done\n ? 'border-transparent bg-status-success-bg text-status-success-text'\n : isCurrent\n ? 'border-action-primary-bg text-action-primary-bg'\n : 'border-border-default text-transparent'\n }`}\n >\n {done ? (\n <Check className=\"h-3 w-3\" />\n ) : (\n <CircleDashed className=\"h-3 w-3\" />\n )}\n </span>\n <span className=\"truncate\">{`Module ${i + 1}`}</span>\n </button>\n </li>\n );\n })}\n </ol>\n </CardContent>\n </Card>\n\n {/* Main pane */}\n <div className=\"space-y-5\">\n <GlassCard treatment=\"plain\" className=\"overflow-hidden\">\n <button\n type=\"button\"\n onClick={() => void handleMarkComplete()}\n aria-label={currentComplete ? 'Next module' : 'Mark this module complete'}\n className=\"group relative flex aspect-video w-full items-center justify-center outline-none focus-visible:shadow-[var(--shadow-focus)]\"\n style={{\n background:\n 'linear-gradient(135deg, color-mix(in srgb, var(--color-accent-primary) 24%, var(--color-bg-sunken)), var(--color-bg-sunken))',\n }}\n >\n <span className=\"absolute left-4 top-4\">\n <Badge variant=\"secondary\" className=\"text-[10px]\">\n {moduleTitle}\n </Badge>\n </span>\n <span className=\"flex h-16 w-16 items-center justify-center rounded-full bg-action-primary-bg text-action-primary-text shadow-elevation-2 transition-transform group-hover:scale-105\">\n {currentComplete ? (\n <Check className=\"h-7 w-7\" />\n ) : (\n <Play className=\"ml-1 h-7 w-7\" />\n )}\n </span>\n </button>\n <CardContent className=\"space-y-4 p-5\">\n <div>\n <div className=\"flex flex-wrap items-center gap-2\">\n <h1 className=\"text-xl font-semibold\">{moduleTitle}</h1>\n <Badge\n variant={\n course.level === 'beginner'\n ? 'secondary'\n : course.level === 'intermediate'\n ? 'default'\n : 'destructive'\n }\n className=\"text-[10px]\"\n >\n {formatEnumLabel(course.level)}\n </Badge>\n {currentComplete && (\n <span className=\"inline-flex items-center gap-1 text-xs font-medium text-status-success-text\">\n <Check className=\"h-3.5 w-3.5\" /> Completed\n </span>\n )}\n </div>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {course.title} · by {course.instructor}\n </p>\n </div>\n\n <SegmentedTabs\n variant=\"underline\"\n aria-label=\"Lesson detail\"\n value={tab}\n onChange={(id) => setTab(id as PlayerTab)}\n tabs={[\n { id: 'overview', label: 'Overview' },\n { id: 'resources', label: 'Resources' },\n { id: 'notes', label: 'Notes' },\n ]}\n />\n\n {tab === 'overview' && (\n <div className=\"space-y-3\">\n <p className=\"text-sm text-text-secondary\">{course.description}</p>\n {course.skills.length > 0 && (\n <div className=\"flex flex-wrap gap-1\">\n {course.skills.map((s) => (\n <Badge key={s} variant=\"outline\" className=\"text-[10px]\">\n {s}\n </Badge>\n ))}\n </div>\n )}\n </div>\n )}\n\n {tab === 'resources' && (\n <div className=\"rounded-lg border border-dashed border-border-default p-6 text-center\">\n <FileText className=\"mx-auto mb-2 h-6 w-6 text-text-muted\" />\n <p className=\"text-sm font-medium\">No downloadable resources yet</p>\n <p className=\"text-xs text-text-secondary\">\n Slides and attachments for this module will appear here when the instructor adds\n them.\n </p>\n </div>\n )}\n\n {tab === 'notes' && (\n <div className=\"space-y-2\">\n <Textarea\n value={notes}\n onChange={(e) => {\n setNotes(e.target.value);\n writeNotes(course.id, e.target.value);\n }}\n placeholder=\"Jot down your notes for this course. They are saved to this device automatically.\"\n rows={5}\n />\n <p className=\"text-xs text-text-muted\">\n Notes are saved automatically on this device.\n </p>\n </div>\n )}\n </CardContent>\n </GlassCard>\n\n {/* Footer controls */}\n <Card>\n <CardContent className=\"space-y-4 p-4\">\n <div className=\"h-2 overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-status-success-bg transition-[width] duration-500\"\n style={{ width: `${pct}%` }}\n />\n </div>\n <div className=\"flex items-center justify-between gap-2\">\n <Button variant=\"outline\" onClick={goPrev} disabled={current === 0}>\n <ChevronLeft className=\"mr-1 h-4 w-4\" />\n Previous\n </Button>\n <div className=\"flex items-center gap-2\">\n <Button onClick={() => void handleMarkComplete()}>\n {currentComplete ? (\n current < totalModules - 1 ? (\n 'Next module'\n ) : (\n <>\n <Check className=\"mr-1 h-4 w-4\" /> Completed\n </>\n )\n ) : (\n <>\n <Check className=\"mr-1 h-4 w-4\" /> Mark Complete\n </>\n )}\n </Button>\n <Button variant=\"outline\" onClick={goNext} disabled={current >= totalModules - 1}>\n Next\n <ChevronRight className=\"ml-1 h-4 w-4\" />\n </Button>\n </div>\n </div>\n </CardContent>\n </Card>\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
@@ -7,7 +7,7 @@ import { FormModal as i, useFormModal as a } from "../components/forms/FormModal
7
7
  import { readCourseProgress as o } from "../utils/courseProgress.js";
8
8
  import { useMemo as s, useState as c } from "react";
9
9
  import { BarChart3 as l, Briefcase as u, Code2 as d, GraduationCap as f, Megaphone as p, Palette as m, Play as h, Plus as g, Search as _, ShieldCheck as v, Users as y } from "lucide-react";
10
- import { useNavigate as b } from "react-router-dom";
10
+ import { useNavigate as b } from "react-router";
11
11
  import { jsx as x, jsxs as S } from "react/jsx-runtime";
12
12
  import { Badge as C, Button as w, CardContent as T, CardHeader as E, GlassCard as D, GradientText as O, IllustratedEmptyState as k, Input as A, PagePurpose as j, Pagination as M } from "@burdenoff/fe-libs/ui";
13
13
  import { usePagination as N } from "@burdenoff/fe-libs/shared/hooks";
@@ -1 +1 @@
1
- {"version":3,"file":"LearningCoursesPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/LearningCoursesPagePage.tsx"],"sourcesContent":["import { useMemo, useState, type ComponentType } from 'react';\nimport {\n Badge,\n Button,\n CardContent,\n CardHeader,\n GlassCard,\n GradientText,\n IllustratedEmptyState,\n Input,\n PagePurpose,\n Pagination,\n} from '@burdenoff/fe-libs/ui';\nimport { usePagination } from '@burdenoff/fe-libs/shared/hooks';\nimport { useGlobalUi } from '@burdenoff/fe-libs/shared/providers/shell';\nimport {\n BarChart3,\n Briefcase,\n Code2,\n GraduationCap,\n Megaphone,\n Palette,\n Play,\n Plus,\n Search,\n ShieldCheck,\n Users,\n} from 'lucide-react';\nimport { useNavigate } from 'react-router-dom';\nimport { useCrewFoundryStore, type LearningCourse } from '../store/crewfoundryStore';\nimport {\n FormModal,\n useFormModal,\n type FieldConfig,\n type FormValues,\n} from '../components/forms/FormModal';\nimport { chartPalette, RatingStars } from '../components/viz';\nimport { formatEnumLabel } from '../utils/formatEnumLabel';\nimport { readCourseProgress } from '../utils/courseProgress';\n\nconst COURSE_FIELDS: FieldConfig[] = [\n { name: 'title', label: 'Course Title', type: 'text', required: true, colSpan: 2 },\n { name: 'category', label: 'Category', type: 'text', required: true },\n { name: 'instructor', label: 'Instructor', type: 'text', required: true },\n {\n name: 'duration',\n label: 'Duration',\n type: 'text',\n required: true,\n placeholder: 'e.g. 6 weeks',\n },\n { name: 'modules', label: 'Modules', type: 'number', required: true, placeholder: 'e.g. 8' },\n {\n name: 'level',\n label: 'Level',\n type: 'select',\n required: true,\n options: [\n { label: 'Beginner', value: 'beginner' },\n { label: 'Intermediate', value: 'intermediate' },\n { label: 'Advanced', value: 'advanced' },\n ],\n },\n {\n name: 'status',\n label: 'Status',\n type: 'select',\n required: true,\n options: [\n { label: 'Active', value: 'active' },\n { label: 'Draft', value: 'draft' },\n { label: 'Archived', value: 'archived' },\n ],\n },\n { name: 'description', label: 'Description', type: 'textarea', required: true, colSpan: 2 },\n {\n name: 'skills',\n label: 'Skills',\n type: 'tags',\n placeholder: 'Add a skill and press Enter',\n colSpan: 2,\n },\n];\n\ntype IconType = ComponentType<{ className?: string }>;\n\n/** Deterministic tokenized accent for a category (never a raw color). */\nfunction categoryAccent(category: string): string {\n const palette = chartPalette(8);\n let hash = 0;\n for (let i = 0; i < category.length; i += 1) {\n hash = (hash * 31 + category.charCodeAt(i)) >>> 0;\n }\n return palette[hash % palette.length];\n}\n\nfunction categoryIcon(category: string): IconType {\n const c = category.toLowerCase();\n if (\n c.includes('engineer') ||\n c.includes('develop') ||\n c.includes('program') ||\n c.includes('tech')\n )\n return Code2;\n if (c.includes('design') || c.includes('ux') || c.includes('ui')) return Palette;\n if (c.includes('lead') || c.includes('manage') || c.includes('people') || c.includes('hr'))\n return Users;\n if (c.includes('compliance') || c.includes('security') || c.includes('legal')) return ShieldCheck;\n if (c.includes('data') || c.includes('analyt')) return BarChart3;\n if (c.includes('market') || c.includes('sales')) return Megaphone;\n if (c.includes('business') || c.includes('finance') || c.includes('operations')) return Briefcase;\n return GraduationCap;\n}\n\nfunction levelVariant(level: LearningCourse['level']): 'secondary' | 'default' | 'destructive' {\n return level === 'beginner' ? 'secondary' : level === 'intermediate' ? 'default' : 'destructive';\n}\n\nexport function LearningCoursesPagePage() {\n const { courses, addCourse, enrollCourse } = useCrewFoundryStore();\n const { showToast } = useGlobalUi();\n const navigate = useNavigate();\n const [search, setSearch] = useState('');\n const [category, setCategory] = useState('all');\n const [enrollingId, setEnrollingId] = useState<string | null>(null);\n const createModal = useFormModal();\n const { page, pageSize, offset, setPage } = usePagination({ initialPageSize: 12 });\n\n const categories = useMemo(() => {\n const seen = new Map<string, number>();\n for (const course of courses) {\n const key = course.category.trim() || 'Uncategorized';\n seen.set(key, (seen.get(key) ?? 0) + 1);\n }\n return [...seen.entries()].sort((a, b) => b[1] - a[1]);\n }, [courses]);\n\n const filtered = courses.filter((c) => {\n const matchesCategory =\n category === 'all' || (c.category.trim() || 'Uncategorized') === category;\n const matchesSearch = `${c.title} ${c.category} ${c.instructor} ${c.skills.join(' ')}`\n .toLowerCase()\n .includes(search.toLowerCase());\n return matchesCategory && matchesSearch;\n });\n const paged = filtered.slice(offset, offset + pageSize);\n\n const openCourse = (id: string) =>\n navigate(`../learning/course-details-page?id=${encodeURIComponent(id)}`);\n const openPlayer = (id: string) =>\n navigate(`../learning/course-learning-page?id=${encodeURIComponent(id)}`);\n\n const handleEnroll = async (course: LearningCourse) => {\n setEnrollingId(course.id);\n try {\n await enrollCourse(course.id);\n showToast(`Enrolled in ${course.title}`, 'success');\n openPlayer(course.id);\n } catch (error) {\n showToast(error instanceof Error ? error.message : 'Unable to enroll in course', 'error');\n } finally {\n setEnrollingId(null);\n }\n };\n\n const handleCreate = async (values: FormValues) => {\n await addCourse({\n id: `course-${Date.now()}`,\n title: String(values.title ?? ''),\n category: String(values.category ?? ''),\n instructor: String(values.instructor ?? ''),\n duration: String(values.duration ?? ''),\n level: String(values.level ?? 'beginner') as LearningCourse['level'],\n enrolled: 0,\n completed: 0,\n rating: 0,\n status: String(values.status ?? 'draft') as LearningCourse['status'],\n description: String(values.description ?? ''),\n modules: typeof values.modules === 'number' ? values.modules : Number(values.modules) || 0,\n skills: Array.isArray(values.skills) ? values.skills : [],\n });\n };\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div>\n <h1 className=\"text-2xl font-semibold\">\n <GradientText>Course Catalog</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">\n {courses.length} courses · {categories.length} categories\n </p>\n <PagePurpose className=\"mt-1\">\n Browse the course catalog — find training to grow your team&apos;s skills and enroll in\n the programs that fit your goals.\n </PagePurpose>\n </div>\n <Button onClick={() => createModal.openModal()}>\n <Plus className=\"mr-1 h-4 w-4\" />\n Create Course\n </Button>\n </div>\n\n <div className=\"flex flex-col gap-3\">\n <div className=\"relative max-w-md\">\n <Search className=\"absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-text-secondary\" />\n <Input\n placeholder=\"Search courses, skills, or instructors...\"\n className=\"pl-9\"\n value={search}\n onChange={(e) => {\n setSearch(e.target.value);\n setPage(1);\n }}\n />\n </div>\n <div className=\"flex flex-wrap gap-2\">\n <button\n type=\"button\"\n onClick={() => {\n setCategory('all');\n setPage(1);\n }}\n className={`rounded-full border px-3 py-1 text-xs font-medium transition-colors ${\n category === 'all'\n ? 'border-transparent bg-action-primary-bg text-action-primary-text'\n : 'border-border-subtle bg-bg-surface text-text-secondary hover:text-text-primary'\n }`}\n >\n All\n </button>\n {categories.map(([name, count]) => (\n <button\n key={name}\n type=\"button\"\n onClick={() => {\n setCategory(name);\n setPage(1);\n }}\n className={`inline-flex items-center gap-1.5 rounded-full border px-3 py-1 text-xs font-medium transition-colors ${\n category === name\n ? 'border-transparent bg-action-primary-bg text-action-primary-text'\n : 'border-border-subtle bg-bg-surface text-text-secondary hover:text-text-primary'\n }`}\n >\n {name}\n <span className=\"tabular-nums opacity-70\">{count}</span>\n </button>\n ))}\n </div>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-3\">\n {filtered.length === 0 ? (\n <div className=\"col-span-full\">\n <IllustratedEmptyState\n illustration=\"empty-search\"\n title=\"No courses found\"\n description=\"Try a different search term or category to find training.\"\n />\n </div>\n ) : (\n paged.map((course) => {\n const accent = categoryAccent(course.category);\n const Icon = categoryIcon(course.category);\n const progress = readCourseProgress(course.id);\n const totalModules = Math.max(course.modules, 1);\n const pct =\n progress.completed.length > 0\n ? Math.min(100, Math.round((progress.completed.length / totalModules) * 100))\n : 0;\n const started = pct > 0;\n return (\n <GlassCard\n key={course.id}\n treatment=\"plain\"\n className=\"group flex flex-col overflow-hidden transition-shadow hover:shadow-md\"\n >\n <button\n type=\"button\"\n aria-label={`View ${course.title}`}\n onClick={() => openCourse(course.id)}\n className=\"relative flex h-24 w-full items-center justify-between px-5 text-left outline-none focus-visible:shadow-[var(--shadow-focus)]\"\n style={{\n background: `linear-gradient(135deg, color-mix(in srgb, ${accent} 28%, var(--color-bg-surface)), color-mix(in srgb, ${accent} 8%, var(--color-bg-surface)))`,\n }}\n >\n <span\n className=\"flex h-11 w-11 items-center justify-center rounded-xl bg-bg-surface/70 shadow-elevation-1\"\n style={{ color: accent }}\n >\n <Icon className=\"h-6 w-6\" />\n </span>\n <Badge variant={levelVariant(course.level)} className=\"text-[10px]\">\n {formatEnumLabel(course.level)}\n </Badge>\n </button>\n <CardHeader className=\"pb-2\">\n <Badge variant=\"outline\" className=\"w-fit text-[10px]\">\n {course.category}\n </Badge>\n <button\n type=\"button\"\n onClick={() => openCourse(course.id)}\n className=\"mt-2 text-left text-base font-semibold outline-none hover:text-action-primary-bg focus-visible:text-action-primary-bg\"\n >\n {course.title}\n </button>\n <p className=\"text-xs text-text-secondary\">\n {course.instructor} · {course.duration} · {course.modules} modules\n </p>\n </CardHeader>\n <CardContent className=\"flex flex-1 flex-col gap-3\">\n <p className=\"line-clamp-2 text-sm text-text-secondary\">{course.description}</p>\n {course.skills.length > 0 && (\n <div className=\"flex flex-wrap gap-1\">\n {course.skills.slice(0, 3).map((s) => (\n <Badge key={s} variant=\"outline\" className=\"text-[9px]\">\n {s}\n </Badge>\n ))}\n </div>\n )}\n <div className=\"mt-auto space-y-3\">\n <div className=\"flex items-center justify-between text-xs text-text-secondary\">\n <RatingStars value={course.rating} size={13} showValue />\n <span className=\"flex items-center gap-1\">\n <Users className=\"h-3 w-3\" />\n {course.enrolled} enrolled\n </span>\n </div>\n {started && (\n <div className=\"space-y-1\">\n <div className=\"flex justify-between text-[11px] font-medium\">\n <span className=\"text-text-secondary\">{pct}% complete</span>\n </div>\n <div className=\"h-1.5 overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-action-primary-bg\"\n style={{ width: `${pct}%` }}\n />\n </div>\n </div>\n )}\n <Button\n className=\"w-full\"\n variant={started ? 'outline' : 'default'}\n disabled={enrollingId === course.id}\n onClick={() => (started ? openPlayer(course.id) : void handleEnroll(course))}\n >\n <Play className=\"mr-1 h-4 w-4\" />\n {enrollingId === course.id ? 'Enrolling…' : started ? 'Continue' : 'Enroll'}\n </Button>\n </div>\n </CardContent>\n </GlassCard>\n );\n })\n )}\n </div>\n\n {filtered.length > pageSize && (\n <Pagination\n page={page}\n pageSize={pageSize}\n totalItems={filtered.length}\n onPageChange={setPage}\n />\n )}\n\n <FormModal\n open={createModal.open}\n onOpenChange={createModal.onOpenChange}\n title=\"Create Course\"\n description=\"Add a new course to the catalog.\"\n fields={COURSE_FIELDS}\n submitLabel=\"Create Course\"\n onSubmit={handleCreate}\n />\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;AAwCA,IAAM,IAA+B;CACnC;EAAE,MAAM;EAAS,OAAO;EAAgB,MAAM;EAAQ,UAAU;EAAM,SAAS;EAAG;CAClF;EAAE,MAAM;EAAY,OAAO;EAAY,MAAM;EAAQ,UAAU;EAAM;CACrE;EAAE,MAAM;EAAc,OAAO;EAAc,MAAM;EAAQ,UAAU;EAAM;CACzE;EACE,MAAM;EACN,OAAO;EACP,MAAM;EACN,UAAU;EACV,aAAa;EACd;CACD;EAAE,MAAM;EAAW,OAAO;EAAW,MAAM;EAAU,UAAU;EAAM,aAAa;EAAU;CAC5F;EACE,MAAM;EACN,OAAO;EACP,MAAM;EACN,UAAU;EACV,SAAS;GACP;IAAE,OAAO;IAAY,OAAO;IAAY;GACxC;IAAE,OAAO;IAAgB,OAAO;IAAgB;GAChD;IAAE,OAAO;IAAY,OAAO;IAAY;GACzC;EACF;CACD;EACE,MAAM;EACN,OAAO;EACP,MAAM;EACN,UAAU;EACV,SAAS;GACP;IAAE,OAAO;IAAU,OAAO;IAAU;GACpC;IAAE,OAAO;IAAS,OAAO;IAAS;GAClC;IAAE,OAAO;IAAY,OAAO;IAAY;GACzC;EACF;CACD;EAAE,MAAM;EAAe,OAAO;EAAe,MAAM;EAAY,UAAU;EAAM,SAAS;EAAG;CAC3F;EACE,MAAM;EACN,OAAO;EACP,MAAM;EACN,aAAa;EACb,SAAS;EACV;CACF;AAKD,SAAS,EAAe,GAA0B;CAChD,IAAM,IAAU,EAAa,EAAE,EAC3B,IAAO;AACX,MAAK,IAAI,IAAI,GAAG,IAAI,EAAS,QAAQ,KAAK,EACxC,KAAQ,IAAO,KAAK,EAAS,WAAW,EAAE,KAAM;AAElD,QAAO,EAAQ,IAAO,EAAQ;;AAGhC,SAAS,EAAa,GAA4B;CAChD,IAAM,IAAI,EAAS,aAAa;AAehC,QAbE,EAAE,SAAS,WAAW,IACtB,EAAE,SAAS,UAAU,IACrB,EAAE,SAAS,UAAU,IACrB,EAAE,SAAS,OAAO,GAEX,IACL,EAAE,SAAS,SAAS,IAAI,EAAE,SAAS,KAAK,IAAI,EAAE,SAAS,KAAK,GAAS,IACrE,EAAE,SAAS,OAAO,IAAI,EAAE,SAAS,SAAS,IAAI,EAAE,SAAS,SAAS,IAAI,EAAE,SAAS,KAAK,GACjF,IACL,EAAE,SAAS,aAAa,IAAI,EAAE,SAAS,WAAW,IAAI,EAAE,SAAS,QAAQ,GAAS,IAClF,EAAE,SAAS,OAAO,IAAI,EAAE,SAAS,SAAS,GAAS,IACnD,EAAE,SAAS,SAAS,IAAI,EAAE,SAAS,QAAQ,GAAS,IACpD,EAAE,SAAS,WAAW,IAAI,EAAE,SAAS,UAAU,IAAI,EAAE,SAAS,aAAa,GAAS,IACjF;;AAGT,SAAS,EAAa,GAAyE;AAC7F,QAAO,MAAU,aAAa,cAAc,MAAU,iBAAiB,YAAY;;AAGrF,SAAgB,IAA0B;CACxC,IAAM,EAAE,YAAS,cAAW,oBAAiB,GAAqB,EAC5D,EAAE,iBAAc,GAAa,EAC7B,IAAW,GAAa,EACxB,CAAC,GAAQ,KAAa,EAAS,GAAG,EAClC,CAAC,GAAU,KAAe,EAAS,MAAM,EACzC,CAAC,GAAa,KAAkB,EAAwB,KAAK,EAC7D,IAAc,GAAc,EAC5B,EAAE,SAAM,aAAU,WAAQ,eAAY,EAAc,EAAE,iBAAiB,IAAI,CAAC,EAE5E,IAAa,QAAc;EAC/B,IAAM,oBAAO,IAAI,KAAqB;AACtC,OAAK,IAAM,KAAU,GAAS;GAC5B,IAAM,IAAM,EAAO,SAAS,MAAM,IAAI;AACtC,KAAK,IAAI,IAAM,EAAK,IAAI,EAAI,IAAI,KAAK,EAAE;;AAEzC,SAAO,CAAC,GAAG,EAAK,SAAS,CAAC,CAAC,MAAM,GAAG,MAAM,EAAE,KAAK,EAAE,GAAG;IACrD,CAAC,EAAQ,CAAC,EAEP,IAAW,EAAQ,QAAQ,MAAM;EACrC,IAAM,IACJ,MAAa,UAAU,EAAE,SAAS,MAAM,IAAI,qBAAqB,GAC7D,IAAgB,GAAG,EAAE,MAAM,GAAG,EAAE,SAAS,GAAG,EAAE,WAAW,GAAG,EAAE,OAAO,KAAK,IAAI,GACjF,aAAa,CACb,SAAS,EAAO,aAAa,CAAC;AACjC,SAAO,KAAmB;GAC1B,EACI,IAAQ,EAAS,MAAM,GAAQ,IAAS,EAAS,EAEjD,KAAc,MAClB,EAAS,sCAAsC,mBAAmB,EAAG,GAAG,EACpE,KAAc,MAClB,EAAS,uCAAuC,mBAAmB,EAAG,GAAG,EAErE,IAAe,OAAO,MAA2B;AACrD,IAAe,EAAO,GAAG;AACzB,MAAI;AAGF,GAFA,MAAM,EAAa,EAAO,GAAG,EAC7B,EAAU,eAAe,EAAO,SAAS,UAAU,EACnD,EAAW,EAAO,GAAG;WACd,GAAO;AACd,KAAU,aAAiB,QAAQ,EAAM,UAAU,8BAA8B,QAAQ;YACjF;AACR,KAAe,KAAK;;;AAsBxB,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,kBAA6B,CAAA;MACxC,CAAA;KACL,kBAAC,KAAD;MAAG,WAAU;gBAAb;OACG,EAAQ;OAAO;OAAY,EAAW;OAAO;OAC5C;;KACJ,kBAAC,GAAD;MAAa,WAAU;gBAAO;MAGhB,CAAA;KACV,EAAA,CAAA,EACN,kBAAC,GAAD;KAAQ,eAAe,EAAY,WAAW;eAA9C,CACE,kBAAC,GAAD,EAAM,WAAU,gBAAiB,CAAA,EAAA,gBAE1B;OACL;;GAEN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD,EAAQ,WAAU,wEAAyE,CAAA,EAC3F,kBAAC,GAAD;MACE,aAAY;MACZ,WAAU;MACV,OAAO;MACP,WAAW,MAAM;AAEf,OADA,EAAU,EAAE,OAAO,MAAM,EACzB,EAAQ,EAAE;;MAEZ,CAAA,CACE;QACN,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,UAAD;MACE,MAAK;MACL,eAAe;AAEb,OADA,EAAY,MAAM,EAClB,EAAQ,EAAE;;MAEZ,WAAW,uEACT,MAAa,QACT,qEACA;gBAEP;MAEQ,CAAA,EACR,EAAW,KAAK,CAAC,GAAM,OACtB,kBAAC,UAAD;MAEE,MAAK;MACL,eAAe;AAEb,OADA,EAAY,EAAK,EACjB,EAAQ,EAAE;;MAEZ,WAAW,wGACT,MAAa,IACT,qEACA;gBAVR,CAaG,GACD,kBAAC,QAAD;OAAM,WAAU;iBAA2B;OAAa,CAAA,CACjD;QAdF,EAcE,CACT,CACE;OACF;;GAEN,kBAAC,OAAD;IAAK,WAAU;cACZ,EAAS,WAAW,IACnB,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,GAAD;MACE,cAAa;MACb,OAAM;MACN,aAAY;MACZ,CAAA;KACE,CAAA,GAEN,EAAM,KAAK,MAAW;KACpB,IAAM,IAAS,EAAe,EAAO,SAAS,EACxC,IAAO,EAAa,EAAO,SAAS,EACpC,IAAW,EAAmB,EAAO,GAAG,EACxC,IAAe,KAAK,IAAI,EAAO,SAAS,EAAE,EAC1C,IACJ,EAAS,UAAU,SAAS,IACxB,KAAK,IAAI,KAAK,KAAK,MAAO,EAAS,UAAU,SAAS,IAAgB,IAAI,CAAC,GAC3E,GACA,IAAU,IAAM;AACtB,YACE,kBAAC,GAAD;MAEE,WAAU;MACV,WAAU;gBAHZ;OAKE,kBAAC,UAAD;QACE,MAAK;QACL,cAAY,QAAQ,EAAO;QAC3B,eAAe,EAAW,EAAO,GAAG;QACpC,WAAU;QACV,OAAO,EACL,YAAY,8CAA8C,EAAO,qDAAqD,EAAO,iCAC9H;kBAPH,CASE,kBAAC,QAAD;SACE,WAAU;SACV,OAAO,EAAE,OAAO,GAAQ;mBAExB,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA;SACvB,CAAA,EACP,kBAAC,GAAD;SAAO,SAAS,EAAa,EAAO,MAAM;SAAE,WAAU;mBACnD,EAAgB,EAAO,MAAM;SACxB,CAAA,CACD;;OACT,kBAAC,GAAD;QAAY,WAAU;kBAAtB;SACE,kBAAC,GAAD;UAAO,SAAQ;UAAU,WAAU;oBAChC,EAAO;UACF,CAAA;SACR,kBAAC,UAAD;UACE,MAAK;UACL,eAAe,EAAW,EAAO,GAAG;UACpC,WAAU;oBAET,EAAO;UACD,CAAA;SACT,kBAAC,KAAD;UAAG,WAAU;oBAAb;WACG,EAAO;WAAW;WAAI,EAAO;WAAS;WAAI,EAAO;WAAQ;WACxD;;SACO;;OACb,kBAAC,GAAD;QAAa,WAAU;kBAAvB;SACE,kBAAC,KAAD;UAAG,WAAU;oBAA4C,EAAO;UAAgB,CAAA;SAC/E,EAAO,OAAO,SAAS,KACtB,kBAAC,OAAD;UAAK,WAAU;oBACZ,EAAO,OAAO,MAAM,GAAG,EAAE,CAAC,KAAK,MAC9B,kBAAC,GAAD;WAAe,SAAQ;WAAU,WAAU;qBACxC;WACK,EAFI,EAEJ,CACR;UACE,CAAA;SAER,kBAAC,OAAD;UAAK,WAAU;oBAAf;WACE,kBAAC,OAAD;YAAK,WAAU;sBAAf,CACE,kBAAC,GAAD;aAAa,OAAO,EAAO;aAAQ,MAAM;aAAI,WAAA;aAAY,CAAA,EACzD,kBAAC,QAAD;aAAM,WAAU;uBAAhB;cACE,kBAAC,GAAD,EAAO,WAAU,WAAY,CAAA;cAC5B,EAAO;cAAS;cACZ;eACH;;WACL,KACC,kBAAC,OAAD;YAAK,WAAU;sBAAf,CACE,kBAAC,OAAD;aAAK,WAAU;uBACb,kBAAC,QAAD;cAAM,WAAU;wBAAhB,CAAuC,GAAI,aAAiB;;aACxD,CAAA,EACN,kBAAC,OAAD;aAAK,WAAU;uBACb,kBAAC,OAAD;cACE,WAAU;cACV,OAAO,EAAE,OAAO,GAAG,EAAI,IAAI;cAC3B,CAAA;aACE,CAAA,CACF;;WAER,kBAAC,GAAD;YACE,WAAU;YACV,SAAS,IAAU,YAAY;YAC/B,UAAU,MAAgB,EAAO;YACjC,eAAgB,IAAU,EAAW,EAAO,GAAG,GAAG,KAAK,EAAa,EAAO;sBAJ7E,CAME,kBAAC,GAAD,EAAM,WAAU,gBAAiB,CAAA,EAChC,MAAgB,EAAO,KAAK,eAAe,IAAU,aAAa,SAC5D;;WACL;;SACM;;OACJ;QAjFL,EAAO,GAiFF;MAEd;IAEA,CAAA;GAEL,EAAS,SAAS,KACjB,kBAAC,GAAD;IACQ;IACI;IACV,YAAY,EAAS;IACrB,cAAc;IACd,CAAA;GAGJ,kBAAC,GAAD;IACE,MAAM,EAAY;IAClB,cAAc,EAAY;IAC1B,OAAM;IACN,aAAY;IACZ,QAAQ;IACR,aAAY;IACZ,UArNe,OAAO,MAAuB;AACjD,WAAM,EAAU;MACd,IAAI,UAAU,KAAK,KAAK;MACxB,OAAO,OAAO,EAAO,SAAS,GAAG;MACjC,UAAU,OAAO,EAAO,YAAY,GAAG;MACvC,YAAY,OAAO,EAAO,cAAc,GAAG;MAC3C,UAAU,OAAO,EAAO,YAAY,GAAG;MACvC,OAAO,OAAO,EAAO,SAAS,WAAW;MACzC,UAAU;MACV,WAAW;MACX,QAAQ;MACR,QAAQ,OAAO,EAAO,UAAU,QAAQ;MACxC,aAAa,OAAO,EAAO,eAAe,GAAG;MAC7C,SAAS,OAAO,EAAO,WAAY,WAAW,EAAO,UAAU,OAAO,EAAO,QAAQ,IAAI;MACzF,QAAQ,MAAM,QAAQ,EAAO,OAAO,GAAG,EAAO,SAAS,EAAE;MAC1D,CAAC;;IAuME,CAAA;GACE"}
1
+ {"version":3,"file":"LearningCoursesPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/LearningCoursesPagePage.tsx"],"sourcesContent":["import { useMemo, useState, type ComponentType } from 'react';\nimport {\n Badge,\n Button,\n CardContent,\n CardHeader,\n GlassCard,\n GradientText,\n IllustratedEmptyState,\n Input,\n PagePurpose,\n Pagination,\n} from '@burdenoff/fe-libs/ui';\nimport { usePagination } from '@burdenoff/fe-libs/shared/hooks';\nimport { useGlobalUi } from '@burdenoff/fe-libs/shared/providers/shell';\nimport {\n BarChart3,\n Briefcase,\n Code2,\n GraduationCap,\n Megaphone,\n Palette,\n Play,\n Plus,\n Search,\n ShieldCheck,\n Users,\n} from 'lucide-react';\nimport { useNavigate } from 'react-router';\nimport { useCrewFoundryStore, type LearningCourse } from '../store/crewfoundryStore';\nimport {\n FormModal,\n useFormModal,\n type FieldConfig,\n type FormValues,\n} from '../components/forms/FormModal';\nimport { chartPalette, RatingStars } from '../components/viz';\nimport { formatEnumLabel } from '../utils/formatEnumLabel';\nimport { readCourseProgress } from '../utils/courseProgress';\n\nconst COURSE_FIELDS: FieldConfig[] = [\n { name: 'title', label: 'Course Title', type: 'text', required: true, colSpan: 2 },\n { name: 'category', label: 'Category', type: 'text', required: true },\n { name: 'instructor', label: 'Instructor', type: 'text', required: true },\n {\n name: 'duration',\n label: 'Duration',\n type: 'text',\n required: true,\n placeholder: 'e.g. 6 weeks',\n },\n { name: 'modules', label: 'Modules', type: 'number', required: true, placeholder: 'e.g. 8' },\n {\n name: 'level',\n label: 'Level',\n type: 'select',\n required: true,\n options: [\n { label: 'Beginner', value: 'beginner' },\n { label: 'Intermediate', value: 'intermediate' },\n { label: 'Advanced', value: 'advanced' },\n ],\n },\n {\n name: 'status',\n label: 'Status',\n type: 'select',\n required: true,\n options: [\n { label: 'Active', value: 'active' },\n { label: 'Draft', value: 'draft' },\n { label: 'Archived', value: 'archived' },\n ],\n },\n { name: 'description', label: 'Description', type: 'textarea', required: true, colSpan: 2 },\n {\n name: 'skills',\n label: 'Skills',\n type: 'tags',\n placeholder: 'Add a skill and press Enter',\n colSpan: 2,\n },\n];\n\ntype IconType = ComponentType<{ className?: string }>;\n\n/** Deterministic tokenized accent for a category (never a raw color). */\nfunction categoryAccent(category: string): string {\n const palette = chartPalette(8);\n let hash = 0;\n for (let i = 0; i < category.length; i += 1) {\n hash = (hash * 31 + category.charCodeAt(i)) >>> 0;\n }\n return palette[hash % palette.length];\n}\n\nfunction categoryIcon(category: string): IconType {\n const c = category.toLowerCase();\n if (\n c.includes('engineer') ||\n c.includes('develop') ||\n c.includes('program') ||\n c.includes('tech')\n )\n return Code2;\n if (c.includes('design') || c.includes('ux') || c.includes('ui')) return Palette;\n if (c.includes('lead') || c.includes('manage') || c.includes('people') || c.includes('hr'))\n return Users;\n if (c.includes('compliance') || c.includes('security') || c.includes('legal')) return ShieldCheck;\n if (c.includes('data') || c.includes('analyt')) return BarChart3;\n if (c.includes('market') || c.includes('sales')) return Megaphone;\n if (c.includes('business') || c.includes('finance') || c.includes('operations')) return Briefcase;\n return GraduationCap;\n}\n\nfunction levelVariant(level: LearningCourse['level']): 'secondary' | 'default' | 'destructive' {\n return level === 'beginner' ? 'secondary' : level === 'intermediate' ? 'default' : 'destructive';\n}\n\nexport function LearningCoursesPagePage() {\n const { courses, addCourse, enrollCourse } = useCrewFoundryStore();\n const { showToast } = useGlobalUi();\n const navigate = useNavigate();\n const [search, setSearch] = useState('');\n const [category, setCategory] = useState('all');\n const [enrollingId, setEnrollingId] = useState<string | null>(null);\n const createModal = useFormModal();\n const { page, pageSize, offset, setPage } = usePagination({ initialPageSize: 12 });\n\n const categories = useMemo(() => {\n const seen = new Map<string, number>();\n for (const course of courses) {\n const key = course.category.trim() || 'Uncategorized';\n seen.set(key, (seen.get(key) ?? 0) + 1);\n }\n return [...seen.entries()].sort((a, b) => b[1] - a[1]);\n }, [courses]);\n\n const filtered = courses.filter((c) => {\n const matchesCategory =\n category === 'all' || (c.category.trim() || 'Uncategorized') === category;\n const matchesSearch = `${c.title} ${c.category} ${c.instructor} ${c.skills.join(' ')}`\n .toLowerCase()\n .includes(search.toLowerCase());\n return matchesCategory && matchesSearch;\n });\n const paged = filtered.slice(offset, offset + pageSize);\n\n const openCourse = (id: string) =>\n navigate(`../learning/course-details-page?id=${encodeURIComponent(id)}`);\n const openPlayer = (id: string) =>\n navigate(`../learning/course-learning-page?id=${encodeURIComponent(id)}`);\n\n const handleEnroll = async (course: LearningCourse) => {\n setEnrollingId(course.id);\n try {\n await enrollCourse(course.id);\n showToast(`Enrolled in ${course.title}`, 'success');\n openPlayer(course.id);\n } catch (error) {\n showToast(error instanceof Error ? error.message : 'Unable to enroll in course', 'error');\n } finally {\n setEnrollingId(null);\n }\n };\n\n const handleCreate = async (values: FormValues) => {\n await addCourse({\n id: `course-${Date.now()}`,\n title: String(values.title ?? ''),\n category: String(values.category ?? ''),\n instructor: String(values.instructor ?? ''),\n duration: String(values.duration ?? ''),\n level: String(values.level ?? 'beginner') as LearningCourse['level'],\n enrolled: 0,\n completed: 0,\n rating: 0,\n status: String(values.status ?? 'draft') as LearningCourse['status'],\n description: String(values.description ?? ''),\n modules: typeof values.modules === 'number' ? values.modules : Number(values.modules) || 0,\n skills: Array.isArray(values.skills) ? values.skills : [],\n });\n };\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div>\n <h1 className=\"text-2xl font-semibold\">\n <GradientText>Course Catalog</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">\n {courses.length} courses · {categories.length} categories\n </p>\n <PagePurpose className=\"mt-1\">\n Browse the course catalog — find training to grow your team&apos;s skills and enroll in\n the programs that fit your goals.\n </PagePurpose>\n </div>\n <Button onClick={() => createModal.openModal()}>\n <Plus className=\"mr-1 h-4 w-4\" />\n Create Course\n </Button>\n </div>\n\n <div className=\"flex flex-col gap-3\">\n <div className=\"relative max-w-md\">\n <Search className=\"absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-text-secondary\" />\n <Input\n placeholder=\"Search courses, skills, or instructors...\"\n className=\"pl-9\"\n value={search}\n onChange={(e) => {\n setSearch(e.target.value);\n setPage(1);\n }}\n />\n </div>\n <div className=\"flex flex-wrap gap-2\">\n <button\n type=\"button\"\n onClick={() => {\n setCategory('all');\n setPage(1);\n }}\n className={`rounded-full border px-3 py-1 text-xs font-medium transition-colors ${\n category === 'all'\n ? 'border-transparent bg-action-primary-bg text-action-primary-text'\n : 'border-border-subtle bg-bg-surface text-text-secondary hover:text-text-primary'\n }`}\n >\n All\n </button>\n {categories.map(([name, count]) => (\n <button\n key={name}\n type=\"button\"\n onClick={() => {\n setCategory(name);\n setPage(1);\n }}\n className={`inline-flex items-center gap-1.5 rounded-full border px-3 py-1 text-xs font-medium transition-colors ${\n category === name\n ? 'border-transparent bg-action-primary-bg text-action-primary-text'\n : 'border-border-subtle bg-bg-surface text-text-secondary hover:text-text-primary'\n }`}\n >\n {name}\n <span className=\"tabular-nums opacity-70\">{count}</span>\n </button>\n ))}\n </div>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-3\">\n {filtered.length === 0 ? (\n <div className=\"col-span-full\">\n <IllustratedEmptyState\n illustration=\"empty-search\"\n title=\"No courses found\"\n description=\"Try a different search term or category to find training.\"\n />\n </div>\n ) : (\n paged.map((course) => {\n const accent = categoryAccent(course.category);\n const Icon = categoryIcon(course.category);\n const progress = readCourseProgress(course.id);\n const totalModules = Math.max(course.modules, 1);\n const pct =\n progress.completed.length > 0\n ? Math.min(100, Math.round((progress.completed.length / totalModules) * 100))\n : 0;\n const started = pct > 0;\n return (\n <GlassCard\n key={course.id}\n treatment=\"plain\"\n className=\"group flex flex-col overflow-hidden transition-shadow hover:shadow-md\"\n >\n <button\n type=\"button\"\n aria-label={`View ${course.title}`}\n onClick={() => openCourse(course.id)}\n className=\"relative flex h-24 w-full items-center justify-between px-5 text-left outline-none focus-visible:shadow-[var(--shadow-focus)]\"\n style={{\n background: `linear-gradient(135deg, color-mix(in srgb, ${accent} 28%, var(--color-bg-surface)), color-mix(in srgb, ${accent} 8%, var(--color-bg-surface)))`,\n }}\n >\n <span\n className=\"flex h-11 w-11 items-center justify-center rounded-xl bg-bg-surface/70 shadow-elevation-1\"\n style={{ color: accent }}\n >\n <Icon className=\"h-6 w-6\" />\n </span>\n <Badge variant={levelVariant(course.level)} className=\"text-[10px]\">\n {formatEnumLabel(course.level)}\n </Badge>\n </button>\n <CardHeader className=\"pb-2\">\n <Badge variant=\"outline\" className=\"w-fit text-[10px]\">\n {course.category}\n </Badge>\n <button\n type=\"button\"\n onClick={() => openCourse(course.id)}\n className=\"mt-2 text-left text-base font-semibold outline-none hover:text-action-primary-bg focus-visible:text-action-primary-bg\"\n >\n {course.title}\n </button>\n <p className=\"text-xs text-text-secondary\">\n {course.instructor} · {course.duration} · {course.modules} modules\n </p>\n </CardHeader>\n <CardContent className=\"flex flex-1 flex-col gap-3\">\n <p className=\"line-clamp-2 text-sm text-text-secondary\">{course.description}</p>\n {course.skills.length > 0 && (\n <div className=\"flex flex-wrap gap-1\">\n {course.skills.slice(0, 3).map((s) => (\n <Badge key={s} variant=\"outline\" className=\"text-[9px]\">\n {s}\n </Badge>\n ))}\n </div>\n )}\n <div className=\"mt-auto space-y-3\">\n <div className=\"flex items-center justify-between text-xs text-text-secondary\">\n <RatingStars value={course.rating} size={13} showValue />\n <span className=\"flex items-center gap-1\">\n <Users className=\"h-3 w-3\" />\n {course.enrolled} enrolled\n </span>\n </div>\n {started && (\n <div className=\"space-y-1\">\n <div className=\"flex justify-between text-[11px] font-medium\">\n <span className=\"text-text-secondary\">{pct}% complete</span>\n </div>\n <div className=\"h-1.5 overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-action-primary-bg\"\n style={{ width: `${pct}%` }}\n />\n </div>\n </div>\n )}\n <Button\n className=\"w-full\"\n variant={started ? 'outline' : 'default'}\n disabled={enrollingId === course.id}\n onClick={() => (started ? openPlayer(course.id) : void handleEnroll(course))}\n >\n <Play className=\"mr-1 h-4 w-4\" />\n {enrollingId === course.id ? 'Enrolling…' : started ? 'Continue' : 'Enroll'}\n </Button>\n </div>\n </CardContent>\n </GlassCard>\n );\n })\n )}\n </div>\n\n {filtered.length > pageSize && (\n <Pagination\n page={page}\n pageSize={pageSize}\n totalItems={filtered.length}\n onPageChange={setPage}\n />\n )}\n\n <FormModal\n open={createModal.open}\n onOpenChange={createModal.onOpenChange}\n title=\"Create Course\"\n description=\"Add a new course to the catalog.\"\n fields={COURSE_FIELDS}\n submitLabel=\"Create Course\"\n onSubmit={handleCreate}\n />\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 { BookOpen as n, GraduationCap as r, Star as i, TrendingUp as a, Users as o } from "lucide-react";
4
- import { useNavigate as s } from "react-router-dom";
4
+ import { useNavigate as s } from "react-router";
5
5
  import { jsx as c, jsxs as l } from "react/jsx-runtime";
6
6
  import { Badge as u, Card as d, CardContent as f, CardDescription as p, CardHeader as m, CardTitle as h, GlassCard as g, GradientText as _, PagePurpose as v } from "@burdenoff/fe-libs/ui";
7
7
  //#region src/crewfoundry/pages/LearningLearningPagePage.tsx