@burdenoff/microfe-crewfoundry 2026.707.1 → 2026.710.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 (58) hide show
  1. package/dist/crewfoundry/components/viz/BarChart.js.map +1 -1
  2. package/dist/crewfoundry/components/viz/CertificateCard.js.map +1 -1
  3. package/dist/crewfoundry/components/viz/DonutChart.js.map +1 -1
  4. package/dist/crewfoundry/components/viz/Heatmap.js.map +1 -1
  5. package/dist/crewfoundry/components/viz/LineChart.js.map +1 -1
  6. package/dist/crewfoundry/components/viz/PathSteps.js.map +1 -1
  7. package/dist/crewfoundry/components/viz/RatingStars.js.map +1 -1
  8. package/dist/crewfoundry/components/viz/SegmentedTabs.js.map +1 -1
  9. package/dist/crewfoundry/components/viz/StatCard.js.map +1 -1
  10. package/dist/crewfoundry/components/viz/lib/Avatar.js.map +1 -1
  11. package/dist/crewfoundry/components/viz/lib/CartesianFrame.js.map +1 -1
  12. package/dist/crewfoundry/components/viz/lib/geometry.js.map +1 -1
  13. package/dist/crewfoundry/index.js +1 -0
  14. package/dist/crewfoundry/lib/analytics.js.map +1 -1
  15. package/dist/crewfoundry/pages/AnalyticsAnalyticsPagePage.js.map +1 -1
  16. package/dist/crewfoundry/pages/AnalyticsEmployeeAnalyticsPagePage.js.map +1 -1
  17. package/dist/crewfoundry/pages/AnalyticsFinancialAnalyticsPagePage.js.map +1 -1
  18. package/dist/crewfoundry/pages/AnalyticsHiringAnalyticsPagePage.js.map +1 -1
  19. package/dist/crewfoundry/pages/CommunicationsMessagesPagePage.js +2 -1
  20. package/dist/crewfoundry/pages/CommunicationsMessagesPagePage.js.map +1 -1
  21. package/dist/crewfoundry/pages/CompanyClientManagementPagePage.js.map +1 -1
  22. package/dist/crewfoundry/pages/DashboardDashboardPagePage.js +4 -4
  23. package/dist/crewfoundry/pages/DashboardDashboardPagePage.js.map +1 -1
  24. package/dist/crewfoundry/pages/HiringApplicationsPagePage.js.map +1 -1
  25. package/dist/crewfoundry/pages/HiringCandidatesPagePage.js +3 -3
  26. package/dist/crewfoundry/pages/HiringCandidatesPagePage.js.map +1 -1
  27. package/dist/crewfoundry/pages/HiringCreateJobPagePage.js.map +1 -1
  28. package/dist/crewfoundry/pages/HiringJobDetailsPagePage.js.map +1 -1
  29. package/dist/crewfoundry/pages/HiringJobsPagePage.js.map +1 -1
  30. package/dist/crewfoundry/pages/HrDepartmentsPagePage.js +8 -4
  31. package/dist/crewfoundry/pages/HrDepartmentsPagePage.js.map +1 -1
  32. package/dist/crewfoundry/pages/HrLeaveManagementPagePage.js +4 -4
  33. package/dist/crewfoundry/pages/HrLeaveManagementPagePage.js.map +1 -1
  34. package/dist/crewfoundry/pages/HrPayrollPagePage.js +3 -3
  35. package/dist/crewfoundry/pages/HrPayrollPagePage.js.map +1 -1
  36. package/dist/crewfoundry/pages/HrTimeTrackingPagePage.js.map +1 -1
  37. package/dist/crewfoundry/pages/LearningCertificatesPagePage.js.map +1 -1
  38. package/dist/crewfoundry/pages/LearningCourseLearningPagePage.js.map +1 -1
  39. package/dist/crewfoundry/pages/LearningCoursesPagePage.js +3 -3
  40. package/dist/crewfoundry/pages/LearningCoursesPagePage.js.map +1 -1
  41. package/dist/crewfoundry/pages/LearningLearningPathsPagePage.js.map +1 -1
  42. package/dist/crewfoundry/pages/LearningMyLearningPagePage.js.map +1 -1
  43. package/dist/crewfoundry/pages/MarketplaceMarketplacePagePage.js.map +1 -1
  44. package/dist/crewfoundry/pages/MarketplaceServiceDetailsPagePage.js.map +1 -1
  45. package/dist/crewfoundry/pages/PaymentsInvoicesPagePage.js.map +1 -1
  46. package/dist/crewfoundry/pages/ProjectsProjectsPagePage.js.map +1 -1
  47. package/dist/crewfoundry/pages/ProjectsResourcingPagePage.js.map +1 -1
  48. package/dist/crewfoundry/pages/TeamsTeamDetailPagePage.js.map +1 -1
  49. package/dist/crewfoundry/pages/TeamsTeamsPagePage.js.map +1 -1
  50. package/dist/crewfoundry/utils/courseProgress.js.map +1 -1
  51. package/dist/index.js +2 -1
  52. package/dist/services/assistantApi.js +273 -0
  53. package/dist/services/assistantApi.js.map +1 -0
  54. package/dist/services/createSandboxAssistantTransport.js +120 -0
  55. package/dist/services/createSandboxAssistantTransport.js.map +1 -0
  56. package/dist/utils/pageContext.js +52 -0
  57. package/dist/utils/pageContext.js.map +1 -0
  58. package/package.json +6 -2
@@ -1 +1 @@
1
- {"version":3,"file":"HrTimeTrackingPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/HrTimeTrackingPagePage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport {\n Badge,\n Button,\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n GradientText,\n PagePurpose,\n Pagination,\n} from '@burdenoff/fe-libs/ui';\nimport { usePagination } from '@burdenoff/fe-libs/shared/hooks';\nimport {\n CheckCircle,\n ChevronLeft,\n ChevronRight,\n Clock,\n LogIn,\n LogOut,\n Timer,\n} from 'lucide-react';\nimport { useCrewFoundryStore, type Employee, type TimeEntry } from '../store/crewfoundryStore';\nimport { AvatarStack, DonutChart, StatCard } from '../components/viz';\nimport { billableSplit } from '../lib/analytics';\nimport {\n FormModal,\n useFormModal,\n type FieldConfig,\n type FormValues,\n} from '../components/forms/FormModal';\nimport { getBool, getNumber, getText, newClientId } from '../components/forms/formValues';\nimport { formatEnumLabel } from '../utils/formatEnumLabel';\n\nconst STATUS_OPTIONS = [\n { label: 'Pending', value: 'pending' },\n { label: 'Approved', value: 'approved' },\n { label: 'Rejected', value: 'rejected' },\n];\n\nconst CLOCK_OUT_FIELDS: FieldConfig[] = [\n {\n name: 'clockOut',\n label: 'Clock-out time',\n type: 'text',\n required: true,\n placeholder: 'e.g. 17:30',\n },\n { name: 'totalHours', label: 'Total hours', type: 'number', required: true },\n { name: 'overtime', label: 'Overtime hours', type: 'number' },\n { name: 'status', label: 'Status', type: 'select', options: STATUS_OPTIONS },\n];\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];\nconst DOW = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];\n\nconst isoLocal = (date: Date): string =>\n `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(\n date.getDate()\n ).padStart(2, '0')}`;\n\n/** Local Monday that opens the week containing `date`. */\nconst startOfWeek = (date: Date): Date => {\n const x = new Date(date.getFullYear(), date.getMonth(), date.getDate());\n const day = (x.getDay() + 6) % 7; // Mon = 0\n x.setDate(x.getDate() - day);\n return x;\n};\n\nconst parseDay = (value: string): Date => {\n const [y, m, d] = value.slice(0, 10).split('-').map(Number);\n return new Date(y, (m || 1) - 1, d || 1);\n};\n\nexport function HrTimeTrackingPagePage() {\n const { timeEntries, employees, addTimeEntry, updateTimeEntry } = useCrewFoundryStore();\n const createForm = useFormModal<void>();\n const clockOutForm = useFormModal<TimeEntry>();\n const [entryPrefill, setEntryPrefill] = useState<{ employeeId: string; date: string } | null>(\n null\n );\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 entryFields = useMemo<FieldConfig[]>(\n () => [\n {\n name: 'employeeId',\n label: 'Employee',\n type: 'select',\n required: true,\n options: employeeOptions,\n colSpan: 2,\n },\n { name: 'date', label: 'Date', type: 'date', required: true },\n { name: 'project', label: 'Project', type: 'text', required: true },\n {\n name: 'clockIn',\n label: 'Clock-in time',\n type: 'text',\n required: true,\n placeholder: 'e.g. 09:00',\n },\n { name: 'clockOut', label: 'Clock-out time', type: 'text', placeholder: 'e.g. 17:30' },\n { name: 'totalHours', label: 'Total hours', type: 'number' },\n { name: 'overtime', label: 'Overtime hours', type: 'number' },\n { name: 'status', label: 'Status', type: 'select', options: STATUS_OPTIONS },\n { name: 'billable', label: 'Billable to client', type: 'switch', colSpan: 2 },\n ],\n [employeeOptions]\n );\n\n // ── Week scoping ───────────────────────────────────────────────────────────\n const [weekOffset, setWeekOffset] = useState(0);\n\n // Anchor the initial week on the most recent entry so the grid opens on data.\n const baseWeekStart = useMemo(() => {\n const latest = timeEntries\n .map((entry) => entry.date.slice(0, 10))\n .filter(Boolean)\n .sort()\n .slice(-1)[0];\n return startOfWeek(latest ? parseDay(latest) : new Date());\n }, [timeEntries]);\n\n const weekStart = useMemo(() => {\n const start = new Date(baseWeekStart);\n start.setDate(start.getDate() + weekOffset * 7);\n return start;\n }, [baseWeekStart, weekOffset]);\n\n const weekDays = useMemo(\n () =>\n Array.from({ length: 7 }, (_, i) => {\n const date = new Date(weekStart);\n date.setDate(weekStart.getDate() + i);\n return { iso: isoLocal(date), label: `${DOW[i]} ${date.getDate()}` };\n }),\n [weekStart]\n );\n const weekDaySet = useMemo(() => new Set(weekDays.map((d) => d.iso)), [weekDays]);\n\n const weekEnd = weekDays[6];\n const weekRangeLabel = `${MONTH_ABBR[weekStart.getMonth()]} ${weekStart.getDate()} – ${\n MONTH_ABBR[parseDay(weekEnd.iso).getMonth()]\n } ${parseDay(weekEnd.iso).getDate()}`;\n\n const weekEntries = useMemo(\n () => timeEntries.filter((entry) => weekDaySet.has(entry.date.slice(0, 10))),\n [timeEntries, weekDaySet]\n );\n\n // KPIs scoped to the selected week.\n const weekTotalHours = weekEntries.reduce((s, t) => s + t.totalHours, 0);\n const weekOvertime = weekEntries.reduce((s, t) => s + t.overtime, 0);\n const weekApproved = weekEntries.filter((t) => t.status === 'approved').length;\n const weekPending = weekEntries.filter((t) => t.status === 'pending').length;\n\n // Weekly timesheet rows: one per employee with entries this week.\n const timesheetRows = useMemo(() => {\n const byEmployee = new Map<\n string,\n { name: string; avatar?: string; perDay: Map<string, number>; total: number; statuses: Set<string> }\n >();\n for (const entry of weekEntries) {\n let row = byEmployee.get(entry.employeeId);\n if (!row) {\n row = {\n name: entry.employeeName || employeeById.get(entry.employeeId)?.firstName || 'Unknown',\n avatar: employeeById.get(entry.employeeId)?.avatar,\n perDay: new Map<string, number>(),\n total: 0,\n statuses: new Set<string>(),\n };\n byEmployee.set(entry.employeeId, row);\n }\n const day = entry.date.slice(0, 10);\n row.perDay.set(day, (row.perDay.get(day) ?? 0) + entry.totalHours);\n row.total += entry.totalHours;\n row.statuses.add(entry.status);\n }\n return [...byEmployee.entries()]\n .map(([employeeId, row]) => ({\n employeeId,\n name: row.name,\n avatar: row.avatar,\n perDay: row.perDay,\n total: row.total,\n status: row.statuses.has('pending')\n ? 'pending'\n : row.statuses.has('rejected')\n ? 'rejected'\n : 'approved',\n }))\n .sort((a, b) => b.total - a.total);\n }, [weekEntries, employeeById]);\n\n // Donut: billable vs non-billable hours for the week, from the real\n // TimeEntry.billable flag. Null (empty state) when no hours are logged —\n // never a fabricated ratio.\n const billableDonut = useMemo(() => {\n const segments = billableSplit(weekEntries);\n if (!segments) {\n return null;\n }\n const total = segments.reduce((sum, seg) => sum + seg.value, 0);\n const billableHours = segments.find((seg) => seg.label === 'Billable')?.value ?? 0;\n const billablePct = total > 0 ? Math.round((billableHours / total) * 100) : 0;\n return { segments, centerValue: `${billablePct}%`, caption: 'Billable' };\n }, [weekEntries]);\n\n // Hours-by-project for the selected week.\n const projectBreakdown = weekEntries.reduce<Record<string, number>>((acc, t) => {\n acc[t.project] = (acc[t.project] || 0) + t.totalHours;\n return acc;\n }, {});\n const maxProjectHours = Math.max(...Object.values(projectBreakdown), 1);\n\n const recentEntries = useMemo(\n () => [...timeEntries].sort((a, b) => b.date.localeCompare(a.date)),\n [timeEntries]\n );\n const { page, pageSize, offset, setPage } = usePagination({ initialPageSize: 8 });\n const activityPageItems = recentEntries.slice(offset, offset + pageSize);\n\n const openCreate = (prefill?: { employeeId: string; date: string }) => {\n setEntryPrefill(prefill ?? null);\n createForm.openModal();\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 payload: TimeEntry = {\n id: newClientId(),\n employeeId,\n employeeName: employee ? `${employee.firstName} ${employee.lastName}` : '',\n date: getText(values, 'date'),\n clockIn: getText(values, 'clockIn'),\n clockOut: getText(values, 'clockOut'),\n totalHours: getNumber(values, 'totalHours'),\n overtime: getNumber(values, 'overtime'),\n project: getText(values, 'project'),\n status: (getText(values, 'status') as TimeEntry['status']) || 'pending',\n billable: getBool(values, 'billable'),\n };\n await addTimeEntry(payload);\n };\n\n const handleClockOut = async (values: FormValues) => {\n const entry = clockOutForm.context;\n if (!entry) {\n return;\n }\n await updateTimeEntry(entry.id, {\n clockOut: getText(values, 'clockOut'),\n totalHours: getNumber(values, 'totalHours'),\n overtime: getNumber(values, 'overtime'),\n status: (getText(values, 'status') as TimeEntry['status']) || entry.status,\n });\n };\n\n // Default `billable` on to mirror the backend default (true) for new entries.\n const createInitial: FormValues = {\n billable: true,\n ...(entryPrefill ? { employeeId: entryPrefill.employeeId, date: entryPrefill.date } : {}),\n };\n\n const clockOutInitial: FormValues | undefined = clockOutForm.context\n ? {\n clockOut: clockOutForm.context.clockOut,\n totalHours: clockOutForm.context.totalHours,\n overtime: clockOutForm.context.overtime,\n status: clockOutForm.context.status,\n }\n : undefined;\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between\">\n <div>\n <h1 className=\"text-2xl font-semibold\">\n <GradientText>Time Tracking</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">Monitor employee work hours and overtime</p>\n <PagePurpose className=\"mt-1\">\n Track hours and attendance across your team — use it to monitor logged time, overtime,\n and timesheet approvals.\n </PagePurpose>\n </div>\n <div className=\"flex items-center gap-2\">\n <div className=\"flex items-center gap-1 rounded-lg border border-border-subtle p-1\">\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-8 w-8 p-0\"\n aria-label=\"Previous week\"\n onClick={() => setWeekOffset((o) => o - 1)}\n >\n <ChevronLeft className=\"h-4 w-4\" />\n </Button>\n <span className=\"min-w-[8.5rem] text-center text-sm font-medium tabular-nums\">\n {weekRangeLabel}\n </span>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-8 w-8 p-0\"\n aria-label=\"Next week\"\n onClick={() => setWeekOffset((o) => o + 1)}\n >\n <ChevronRight className=\"h-4 w-4\" />\n </Button>\n </div>\n <Button size=\"sm\" onClick={() => openCreate()}>\n <Timer className=\"h-4 w-4 mr-1\" />\n Log Time\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 Hours\"\n value={weekTotalHours.toFixed(1)}\n icon={<Clock className=\"h-4 w-4\" />}\n />\n <StatCard\n label=\"Overtime\"\n value={`${weekOvertime.toFixed(1)}h`}\n icon={<Timer className=\"h-4 w-4\" />}\n />\n <StatCard\n label=\"Approved\"\n value={weekApproved}\n icon={<CheckCircle className=\"h-4 w-4\" />}\n />\n <StatCard label=\"Pending\" value={weekPending} icon={<Clock className=\"h-4 w-4\" />} />\n </div>\n\n <div className=\"grid gap-4 xl:grid-cols-[minmax(0,2.2fr)_minmax(0,1fr)]\">\n <Card>\n <CardHeader>\n <CardTitle>Timesheet</CardTitle>\n <CardDescription>\n {weekRangeLabel} · tap a day cell to log time for that person\n </CardDescription>\n </CardHeader>\n <CardContent>\n <div className=\"overflow-x-auto\">\n <table className=\"w-full min-w-[46rem] border-collapse text-sm\">\n <thead>\n <tr className=\"border-b border-border-subtle text-xs text-text-secondary\">\n <th className=\"px-2 py-2 text-left font-medium\">Employee</th>\n {weekDays.map((day) => (\n <th key={day.iso} className=\"px-1 py-2 text-center font-medium\">\n {day.label}\n </th>\n ))}\n <th className=\"px-2 py-2 text-right font-medium\">Total</th>\n <th className=\"px-2 py-2 text-center font-medium\">Status</th>\n </tr>\n </thead>\n <tbody>\n {timesheetRows.map((row) => (\n <tr key={row.employeeId} className=\"border-b border-border-subtle/60\">\n <td className=\"px-2 py-2\">\n <div className=\"flex items-center gap-2\">\n <AvatarStack\n people={[{ name: row.name, avatarUrl: row.avatar }]}\n size={28}\n />\n <span className=\"truncate font-medium\">{row.name}</span>\n </div>\n </td>\n {weekDays.map((day) => {\n const hours = row.perDay.get(day.iso) ?? 0;\n return (\n <td key={day.iso} className=\"px-1 py-2 text-center\">\n <button\n type=\"button\"\n onClick={() =>\n openCreate({ employeeId: row.employeeId, date: day.iso })\n }\n className={`w-full rounded-md border px-1 py-1.5 text-xs tabular-nums transition-colors ${\n hours > 0\n ? 'border-border-subtle text-text-primary hover:bg-bg-sunken'\n : 'border-transparent text-text-muted hover:border-border-subtle hover:bg-bg-sunken'\n }`}\n aria-label={`Log time for ${row.name} on ${day.label}`}\n >\n {hours.toFixed(1)}\n </button>\n </td>\n );\n })}\n <td className=\"px-2 py-2 text-right font-semibold tabular-nums\">\n {row.total.toFixed(1)}\n </td>\n <td className=\"px-2 py-2 text-center\">\n <Badge\n variant={\n row.status === 'approved'\n ? 'default'\n : row.status === 'pending'\n ? 'secondary'\n : 'destructive'\n }\n className=\"text-[10px]\"\n >\n {formatEnumLabel(row.status)}\n </Badge>\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n {timesheetRows.length === 0 && (\n <p className=\"py-8 text-center text-sm text-text-secondary\">\n No time logged for {weekRangeLabel}.\n </p>\n )}\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>Billable split</CardTitle>\n <CardDescription>Billable vs non-billable hours for {weekRangeLabel}</CardDescription>\n </CardHeader>\n <CardContent className=\"flex flex-col items-center\">\n {billableDonut ? (\n <DonutChart\n segments={billableDonut.segments}\n centerLabel={billableDonut.centerValue}\n centerCaption={billableDonut.caption}\n valueFormatter={(v) => `${v.toFixed(1)}h`}\n title=\"Billable vs non-billable hours\"\n />\n ) : (\n <p className=\"py-10 text-center text-sm text-text-secondary\">\n No hours logged this week.\n </p>\n )}\n </CardContent>\n </Card>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-2\">\n <Card>\n <CardHeader>\n <CardTitle>Hours by Project</CardTitle>\n <CardDescription>Time allocation for {weekRangeLabel}</CardDescription>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {Object.entries(projectBreakdown)\n .sort(([, a], [, b]) => b - a)\n .map(([project, hours]) => (\n <div key={project} className=\"flex items-center gap-3\">\n <span className=\"w-40 truncate text-sm\">{project}</span>\n <div className=\"h-4 flex-1 overflow-hidden rounded bg-bg-sunken\">\n <div\n className=\"h-4 rounded bg-action-primary-bg\"\n style={{ width: `${Math.round((hours / maxProjectHours) * 100)}%` }}\n />\n </div>\n <span className=\"w-14 text-right text-sm font-medium tabular-nums\">\n {hours.toFixed(1)}h\n </span>\n </div>\n ))}\n {Object.keys(projectBreakdown).length === 0 && (\n <p className=\"py-6 text-center text-sm text-text-secondary\">\n No project hours this week.\n </p>\n )}\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>Recent Activity</CardTitle>\n <CardDescription>Latest employee clock-in/out entries</CardDescription>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {activityPageItems.map((entry) => (\n <div key={entry.id} className=\"flex items-center justify-between rounded border p-3\">\n <div className=\"flex items-center gap-2.5\">\n <AvatarStack\n people={[\n { name: entry.employeeName, avatarUrl: employeeById.get(entry.employeeId)?.avatar },\n ]}\n size={30}\n />\n <div>\n <p className=\"text-sm font-medium\">{entry.employeeName}</p>\n <p className=\"text-xs text-text-secondary\">{entry.project}</p>\n </div>\n </div>\n <div className=\"flex items-center gap-3\">\n <div className=\"text-right text-xs\">\n <div className=\"flex items-center gap-1\">\n <LogIn className=\"h-3 w-3 text-status-success-text\" />\n {entry.clockIn}\n </div>\n <div className=\"flex items-center gap-1\">\n <LogOut className=\"h-3 w-3 text-status-error-text\" />\n {entry.clockOut}\n </div>\n </div>\n <div className=\"text-right\">\n <p className=\"text-sm font-medium\">{entry.totalHours}h</p>\n {entry.overtime > 0 && (\n <p className=\"text-xs text-status-warning-text\">+{entry.overtime}h OT</p>\n )}\n </div>\n <Badge\n variant={entry.status === 'approved' ? 'default' : 'secondary'}\n className=\"text-[10px]\"\n >\n {formatEnumLabel(entry.status)}\n </Badge>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-8 w-8 p-0\"\n aria-label={`Clock out ${entry.employeeName}`}\n onClick={() => clockOutForm.openModal(entry)}\n >\n <LogOut className=\"h-4 w-4\" />\n </Button>\n </div>\n </div>\n ))}\n {recentEntries.length === 0 && (\n <p className=\"py-6 text-center text-sm text-text-secondary\">\n No time entries logged yet.\n </p>\n )}\n {recentEntries.length > pageSize && (\n <Pagination\n page={page}\n pageSize={pageSize}\n totalItems={recentEntries.length}\n onPageChange={setPage}\n />\n )}\n </CardContent>\n </Card>\n </div>\n\n <FormModal\n open={createForm.open}\n onOpenChange={createForm.onOpenChange}\n title=\"Log time\"\n description=\"Log a work session for a team member.\"\n fields={entryFields}\n initialValues={createInitial}\n submitLabel=\"Add entry\"\n onSubmit={handleCreate}\n />\n\n <FormModal\n open={clockOutForm.open}\n onOpenChange={clockOutForm.onOpenChange}\n title=\"Clock out\"\n description={\n clockOutForm.context\n ? `Record clock-out for ${clockOutForm.context.employeeName}.`\n : undefined\n }\n fields={CLOCK_OUT_FIELDS}\n initialValues={clockOutInitial}\n submitLabel=\"Save\"\n onSubmit={handleClockOut}\n />\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"HrTimeTrackingPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/HrTimeTrackingPagePage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport {\n Badge,\n Button,\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n GradientText,\n PagePurpose,\n Pagination,\n} from '@burdenoff/fe-libs/ui';\nimport { usePagination } from '@burdenoff/fe-libs/shared/hooks';\nimport { CheckCircle, ChevronLeft, ChevronRight, Clock, LogIn, LogOut, Timer } from 'lucide-react';\nimport { useCrewFoundryStore, type Employee, type TimeEntry } from '../store/crewfoundryStore';\nimport { AvatarStack, DonutChart, StatCard } from '../components/viz';\nimport { billableSplit } from '../lib/analytics';\nimport {\n FormModal,\n useFormModal,\n type FieldConfig,\n type FormValues,\n} from '../components/forms/FormModal';\nimport { getBool, getNumber, getText, newClientId } from '../components/forms/formValues';\nimport { formatEnumLabel } from '../utils/formatEnumLabel';\n\nconst STATUS_OPTIONS = [\n { label: 'Pending', value: 'pending' },\n { label: 'Approved', value: 'approved' },\n { label: 'Rejected', value: 'rejected' },\n];\n\nconst CLOCK_OUT_FIELDS: FieldConfig[] = [\n {\n name: 'clockOut',\n label: 'Clock-out time',\n type: 'text',\n required: true,\n placeholder: 'e.g. 17:30',\n },\n { name: 'totalHours', label: 'Total hours', type: 'number', required: true },\n { name: 'overtime', label: 'Overtime hours', type: 'number' },\n { name: 'status', label: 'Status', type: 'select', options: STATUS_OPTIONS },\n];\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];\nconst DOW = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];\n\nconst isoLocal = (date: Date): string =>\n `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(\n date.getDate()\n ).padStart(2, '0')}`;\n\n/** Local Monday that opens the week containing `date`. */\nconst startOfWeek = (date: Date): Date => {\n const x = new Date(date.getFullYear(), date.getMonth(), date.getDate());\n const day = (x.getDay() + 6) % 7; // Mon = 0\n x.setDate(x.getDate() - day);\n return x;\n};\n\nconst parseDay = (value: string): Date => {\n const [y, m, d] = value.slice(0, 10).split('-').map(Number);\n return new Date(y, (m || 1) - 1, d || 1);\n};\n\nexport function HrTimeTrackingPagePage() {\n const { timeEntries, employees, addTimeEntry, updateTimeEntry } = useCrewFoundryStore();\n const createForm = useFormModal<void>();\n const clockOutForm = useFormModal<TimeEntry>();\n const [entryPrefill, setEntryPrefill] = useState<{ employeeId: string; date: string } | null>(\n null\n );\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 entryFields = useMemo<FieldConfig[]>(\n () => [\n {\n name: 'employeeId',\n label: 'Employee',\n type: 'select',\n required: true,\n options: employeeOptions,\n colSpan: 2,\n },\n { name: 'date', label: 'Date', type: 'date', required: true },\n { name: 'project', label: 'Project', type: 'text', required: true },\n {\n name: 'clockIn',\n label: 'Clock-in time',\n type: 'text',\n required: true,\n placeholder: 'e.g. 09:00',\n },\n { name: 'clockOut', label: 'Clock-out time', type: 'text', placeholder: 'e.g. 17:30' },\n { name: 'totalHours', label: 'Total hours', type: 'number' },\n { name: 'overtime', label: 'Overtime hours', type: 'number' },\n { name: 'status', label: 'Status', type: 'select', options: STATUS_OPTIONS },\n { name: 'billable', label: 'Billable to client', type: 'switch', colSpan: 2 },\n ],\n [employeeOptions]\n );\n\n // ── Week scoping ───────────────────────────────────────────────────────────\n const [weekOffset, setWeekOffset] = useState(0);\n\n // Anchor the initial week on the most recent entry so the grid opens on data.\n const baseWeekStart = useMemo(() => {\n const latest = timeEntries\n .map((entry) => entry.date.slice(0, 10))\n .filter(Boolean)\n .sort()\n .slice(-1)[0];\n return startOfWeek(latest ? parseDay(latest) : new Date());\n }, [timeEntries]);\n\n const weekStart = useMemo(() => {\n const start = new Date(baseWeekStart);\n start.setDate(start.getDate() + weekOffset * 7);\n return start;\n }, [baseWeekStart, weekOffset]);\n\n const weekDays = useMemo(\n () =>\n Array.from({ length: 7 }, (_, i) => {\n const date = new Date(weekStart);\n date.setDate(weekStart.getDate() + i);\n return { iso: isoLocal(date), label: `${DOW[i]} ${date.getDate()}` };\n }),\n [weekStart]\n );\n const weekDaySet = useMemo(() => new Set(weekDays.map((d) => d.iso)), [weekDays]);\n\n const weekEnd = weekDays[6];\n const weekRangeLabel = `${MONTH_ABBR[weekStart.getMonth()]} ${weekStart.getDate()} – ${\n MONTH_ABBR[parseDay(weekEnd.iso).getMonth()]\n } ${parseDay(weekEnd.iso).getDate()}`;\n\n const weekEntries = useMemo(\n () => timeEntries.filter((entry) => weekDaySet.has(entry.date.slice(0, 10))),\n [timeEntries, weekDaySet]\n );\n\n // KPIs scoped to the selected week.\n const weekTotalHours = weekEntries.reduce((s, t) => s + t.totalHours, 0);\n const weekOvertime = weekEntries.reduce((s, t) => s + t.overtime, 0);\n const weekApproved = weekEntries.filter((t) => t.status === 'approved').length;\n const weekPending = weekEntries.filter((t) => t.status === 'pending').length;\n\n // Weekly timesheet rows: one per employee with entries this week.\n const timesheetRows = useMemo(() => {\n const byEmployee = new Map<\n string,\n {\n name: string;\n avatar?: string;\n perDay: Map<string, number>;\n total: number;\n statuses: Set<string>;\n }\n >();\n for (const entry of weekEntries) {\n let row = byEmployee.get(entry.employeeId);\n if (!row) {\n row = {\n name: entry.employeeName || employeeById.get(entry.employeeId)?.firstName || 'Unknown',\n avatar: employeeById.get(entry.employeeId)?.avatar,\n perDay: new Map<string, number>(),\n total: 0,\n statuses: new Set<string>(),\n };\n byEmployee.set(entry.employeeId, row);\n }\n const day = entry.date.slice(0, 10);\n row.perDay.set(day, (row.perDay.get(day) ?? 0) + entry.totalHours);\n row.total += entry.totalHours;\n row.statuses.add(entry.status);\n }\n return [...byEmployee.entries()]\n .map(([employeeId, row]) => ({\n employeeId,\n name: row.name,\n avatar: row.avatar,\n perDay: row.perDay,\n total: row.total,\n status: row.statuses.has('pending')\n ? 'pending'\n : row.statuses.has('rejected')\n ? 'rejected'\n : 'approved',\n }))\n .sort((a, b) => b.total - a.total);\n }, [weekEntries, employeeById]);\n\n // Donut: billable vs non-billable hours for the week, from the real\n // TimeEntry.billable flag. Null (empty state) when no hours are logged —\n // never a fabricated ratio.\n const billableDonut = useMemo(() => {\n const segments = billableSplit(weekEntries);\n if (!segments) {\n return null;\n }\n const total = segments.reduce((sum, seg) => sum + seg.value, 0);\n const billableHours = segments.find((seg) => seg.label === 'Billable')?.value ?? 0;\n const billablePct = total > 0 ? Math.round((billableHours / total) * 100) : 0;\n return { segments, centerValue: `${billablePct}%`, caption: 'Billable' };\n }, [weekEntries]);\n\n // Hours-by-project for the selected week.\n const projectBreakdown = weekEntries.reduce<Record<string, number>>((acc, t) => {\n acc[t.project] = (acc[t.project] || 0) + t.totalHours;\n return acc;\n }, {});\n const maxProjectHours = Math.max(...Object.values(projectBreakdown), 1);\n\n const recentEntries = useMemo(\n () => [...timeEntries].sort((a, b) => b.date.localeCompare(a.date)),\n [timeEntries]\n );\n const { page, pageSize, offset, setPage } = usePagination({ initialPageSize: 8 });\n const activityPageItems = recentEntries.slice(offset, offset + pageSize);\n\n const openCreate = (prefill?: { employeeId: string; date: string }) => {\n setEntryPrefill(prefill ?? null);\n createForm.openModal();\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 payload: TimeEntry = {\n id: newClientId(),\n employeeId,\n employeeName: employee ? `${employee.firstName} ${employee.lastName}` : '',\n date: getText(values, 'date'),\n clockIn: getText(values, 'clockIn'),\n clockOut: getText(values, 'clockOut'),\n totalHours: getNumber(values, 'totalHours'),\n overtime: getNumber(values, 'overtime'),\n project: getText(values, 'project'),\n status: (getText(values, 'status') as TimeEntry['status']) || 'pending',\n billable: getBool(values, 'billable'),\n };\n await addTimeEntry(payload);\n };\n\n const handleClockOut = async (values: FormValues) => {\n const entry = clockOutForm.context;\n if (!entry) {\n return;\n }\n await updateTimeEntry(entry.id, {\n clockOut: getText(values, 'clockOut'),\n totalHours: getNumber(values, 'totalHours'),\n overtime: getNumber(values, 'overtime'),\n status: (getText(values, 'status') as TimeEntry['status']) || entry.status,\n });\n };\n\n // Default `billable` on to mirror the backend default (true) for new entries.\n const createInitial: FormValues = {\n billable: true,\n ...(entryPrefill ? { employeeId: entryPrefill.employeeId, date: entryPrefill.date } : {}),\n };\n\n const clockOutInitial: FormValues | undefined = clockOutForm.context\n ? {\n clockOut: clockOutForm.context.clockOut,\n totalHours: clockOutForm.context.totalHours,\n overtime: clockOutForm.context.overtime,\n status: clockOutForm.context.status,\n }\n : undefined;\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between\">\n <div>\n <h1 className=\"text-2xl font-semibold\">\n <GradientText>Time Tracking</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">Monitor employee work hours and overtime</p>\n <PagePurpose className=\"mt-1\">\n Track hours and attendance across your team — use it to monitor logged time, overtime,\n and timesheet approvals.\n </PagePurpose>\n </div>\n <div className=\"flex items-center gap-2\">\n <div className=\"flex items-center gap-1 rounded-lg border border-border-subtle p-1\">\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-8 w-8 p-0\"\n aria-label=\"Previous week\"\n onClick={() => setWeekOffset((o) => o - 1)}\n >\n <ChevronLeft className=\"h-4 w-4\" />\n </Button>\n <span className=\"min-w-[8.5rem] text-center text-sm font-medium tabular-nums\">\n {weekRangeLabel}\n </span>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-8 w-8 p-0\"\n aria-label=\"Next week\"\n onClick={() => setWeekOffset((o) => o + 1)}\n >\n <ChevronRight className=\"h-4 w-4\" />\n </Button>\n </div>\n <Button size=\"sm\" onClick={() => openCreate()}>\n <Timer className=\"h-4 w-4 mr-1\" />\n Log Time\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 Hours\"\n value={weekTotalHours.toFixed(1)}\n icon={<Clock className=\"h-4 w-4\" />}\n />\n <StatCard\n label=\"Overtime\"\n value={`${weekOvertime.toFixed(1)}h`}\n icon={<Timer className=\"h-4 w-4\" />}\n />\n <StatCard\n label=\"Approved\"\n value={weekApproved}\n icon={<CheckCircle className=\"h-4 w-4\" />}\n />\n <StatCard label=\"Pending\" value={weekPending} icon={<Clock className=\"h-4 w-4\" />} />\n </div>\n\n <div className=\"grid gap-4 xl:grid-cols-[minmax(0,2.2fr)_minmax(0,1fr)]\">\n <Card>\n <CardHeader>\n <CardTitle>Timesheet</CardTitle>\n <CardDescription>\n {weekRangeLabel} · tap a day cell to log time for that person\n </CardDescription>\n </CardHeader>\n <CardContent>\n <div className=\"overflow-x-auto\">\n <table className=\"w-full min-w-[46rem] border-collapse text-sm\">\n <thead>\n <tr className=\"border-b border-border-subtle text-xs text-text-secondary\">\n <th className=\"px-2 py-2 text-left font-medium\">Employee</th>\n {weekDays.map((day) => (\n <th key={day.iso} className=\"px-1 py-2 text-center font-medium\">\n {day.label}\n </th>\n ))}\n <th className=\"px-2 py-2 text-right font-medium\">Total</th>\n <th className=\"px-2 py-2 text-center font-medium\">Status</th>\n </tr>\n </thead>\n <tbody>\n {timesheetRows.map((row) => (\n <tr key={row.employeeId} className=\"border-b border-border-subtle/60\">\n <td className=\"px-2 py-2\">\n <div className=\"flex items-center gap-2\">\n <AvatarStack\n people={[{ name: row.name, avatarUrl: row.avatar }]}\n size={28}\n />\n <span className=\"truncate font-medium\">{row.name}</span>\n </div>\n </td>\n {weekDays.map((day) => {\n const hours = row.perDay.get(day.iso) ?? 0;\n return (\n <td key={day.iso} className=\"px-1 py-2 text-center\">\n <button\n type=\"button\"\n onClick={() =>\n openCreate({ employeeId: row.employeeId, date: day.iso })\n }\n className={`w-full rounded-md border px-1 py-1.5 text-xs tabular-nums transition-colors ${\n hours > 0\n ? 'border-border-subtle text-text-primary hover:bg-bg-sunken'\n : 'border-transparent text-text-muted hover:border-border-subtle hover:bg-bg-sunken'\n }`}\n aria-label={`Log time for ${row.name} on ${day.label}`}\n >\n {hours.toFixed(1)}\n </button>\n </td>\n );\n })}\n <td className=\"px-2 py-2 text-right font-semibold tabular-nums\">\n {row.total.toFixed(1)}\n </td>\n <td className=\"px-2 py-2 text-center\">\n <Badge\n variant={\n row.status === 'approved'\n ? 'default'\n : row.status === 'pending'\n ? 'secondary'\n : 'destructive'\n }\n className=\"text-[10px]\"\n >\n {formatEnumLabel(row.status)}\n </Badge>\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n {timesheetRows.length === 0 && (\n <p className=\"py-8 text-center text-sm text-text-secondary\">\n No time logged for {weekRangeLabel}.\n </p>\n )}\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>Billable split</CardTitle>\n <CardDescription>Billable vs non-billable hours for {weekRangeLabel}</CardDescription>\n </CardHeader>\n <CardContent className=\"flex flex-col items-center\">\n {billableDonut ? (\n <DonutChart\n segments={billableDonut.segments}\n centerLabel={billableDonut.centerValue}\n centerCaption={billableDonut.caption}\n valueFormatter={(v) => `${v.toFixed(1)}h`}\n title=\"Billable vs non-billable hours\"\n />\n ) : (\n <p className=\"py-10 text-center text-sm text-text-secondary\">\n No hours logged this week.\n </p>\n )}\n </CardContent>\n </Card>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-2\">\n <Card>\n <CardHeader>\n <CardTitle>Hours by Project</CardTitle>\n <CardDescription>Time allocation for {weekRangeLabel}</CardDescription>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {Object.entries(projectBreakdown)\n .sort(([, a], [, b]) => b - a)\n .map(([project, hours]) => (\n <div key={project} className=\"flex items-center gap-3\">\n <span className=\"w-40 truncate text-sm\">{project}</span>\n <div className=\"h-4 flex-1 overflow-hidden rounded bg-bg-sunken\">\n <div\n className=\"h-4 rounded bg-action-primary-bg\"\n style={{ width: `${Math.round((hours / maxProjectHours) * 100)}%` }}\n />\n </div>\n <span className=\"w-14 text-right text-sm font-medium tabular-nums\">\n {hours.toFixed(1)}h\n </span>\n </div>\n ))}\n {Object.keys(projectBreakdown).length === 0 && (\n <p className=\"py-6 text-center text-sm text-text-secondary\">\n No project hours this week.\n </p>\n )}\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>Recent Activity</CardTitle>\n <CardDescription>Latest employee clock-in/out entries</CardDescription>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {activityPageItems.map((entry) => (\n <div key={entry.id} className=\"flex items-center justify-between rounded border p-3\">\n <div className=\"flex items-center gap-2.5\">\n <AvatarStack\n people={[\n {\n name: entry.employeeName,\n avatarUrl: employeeById.get(entry.employeeId)?.avatar,\n },\n ]}\n size={30}\n />\n <div>\n <p className=\"text-sm font-medium\">{entry.employeeName}</p>\n <p className=\"text-xs text-text-secondary\">{entry.project}</p>\n </div>\n </div>\n <div className=\"flex items-center gap-3\">\n <div className=\"text-right text-xs\">\n <div className=\"flex items-center gap-1\">\n <LogIn className=\"h-3 w-3 text-status-success-text\" />\n {entry.clockIn}\n </div>\n <div className=\"flex items-center gap-1\">\n <LogOut className=\"h-3 w-3 text-status-error-text\" />\n {entry.clockOut}\n </div>\n </div>\n <div className=\"text-right\">\n <p className=\"text-sm font-medium\">{entry.totalHours}h</p>\n {entry.overtime > 0 && (\n <p className=\"text-xs text-status-warning-text\">+{entry.overtime}h OT</p>\n )}\n </div>\n <Badge\n variant={entry.status === 'approved' ? 'default' : 'secondary'}\n className=\"text-[10px]\"\n >\n {formatEnumLabel(entry.status)}\n </Badge>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-8 w-8 p-0\"\n aria-label={`Clock out ${entry.employeeName}`}\n onClick={() => clockOutForm.openModal(entry)}\n >\n <LogOut className=\"h-4 w-4\" />\n </Button>\n </div>\n </div>\n ))}\n {recentEntries.length === 0 && (\n <p className=\"py-6 text-center text-sm text-text-secondary\">\n No time entries logged yet.\n </p>\n )}\n {recentEntries.length > pageSize && (\n <Pagination\n page={page}\n pageSize={pageSize}\n totalItems={recentEntries.length}\n onPageChange={setPage}\n />\n )}\n </CardContent>\n </Card>\n </div>\n\n <FormModal\n open={createForm.open}\n onOpenChange={createForm.onOpenChange}\n title=\"Log time\"\n description=\"Log a work session for a team member.\"\n fields={entryFields}\n initialValues={createInitial}\n submitLabel=\"Add entry\"\n onSubmit={handleCreate}\n />\n\n <FormModal\n open={clockOutForm.open}\n onOpenChange={clockOutForm.onOpenChange}\n title=\"Clock out\"\n description={\n clockOutForm.context\n ? `Record clock-out for ${clockOutForm.context.employeeName}.`\n : undefined\n }\n fields={CLOCK_OUT_FIELDS}\n initialValues={clockOutInitial}\n submitLabel=\"Save\"\n onSubmit={handleClockOut}\n />\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;AA2BA,IAAM,IAAiB;CACrB;EAAE,OAAO;EAAW,OAAO;EAAW;CACtC;EAAE,OAAO;EAAY,OAAO;EAAY;CACxC;EAAE,OAAO;EAAY,OAAO;EAAY;CACzC,EAEK,KAAkC;CACtC;EACE,MAAM;EACN,OAAO;EACP,MAAM;EACN,UAAU;EACV,aAAa;EACd;CACD;EAAE,MAAM;EAAc,OAAO;EAAe,MAAM;EAAU,UAAU;EAAM;CAC5E;EAAE,MAAM;EAAY,OAAO;EAAkB,MAAM;EAAU;CAC7D;EAAE,MAAM;EAAU,OAAO;EAAU,MAAM;EAAU,SAAS;EAAgB;CAC7E,EAEK,IAAa;CACjB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD,EACK,KAAM;CAAC;CAAO;CAAO;CAAO;CAAO;CAAO;CAAO;CAAM,EAEvD,MAAY,MAChB,GAAG,EAAK,aAAa,CAAC,GAAG,OAAO,EAAK,UAAU,GAAG,EAAE,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,OACvE,EAAK,SAAS,CACf,CAAC,SAAS,GAAG,IAAI,IAGd,KAAe,MAAqB;CACxC,IAAM,IAAI,IAAI,KAAK,EAAK,aAAa,EAAE,EAAK,UAAU,EAAE,EAAK,SAAS,CAAC,EACjE,KAAO,EAAE,QAAQ,GAAG,KAAK;AAE/B,QADA,EAAE,QAAQ,EAAE,SAAS,GAAG,EAAI,EACrB;GAGH,KAAY,MAAwB;CACxC,IAAM,CAAC,GAAG,GAAG,KAAK,EAAM,MAAM,GAAG,GAAG,CAAC,MAAM,IAAI,CAAC,IAAI,OAAO;AAC3D,QAAO,IAAI,KAAK,IAAI,KAAK,KAAK,GAAG,KAAK,EAAE;;AAG1C,SAAgB,IAAyB;CACvC,IAAM,EAAE,gBAAa,cAAW,kBAAc,wBAAoB,GAAqB,EACjF,IAAa,GAAoB,EACjC,IAAe,GAAyB,EACxC,CAAC,GAAc,MAAmB,EACtC,KACD,EAEK,IAAe,QACb,IAAI,IAAsB,EAAU,KAAK,MAAa,CAAC,EAAS,IAAI,EAAS,CAAC,CAAC,EACrF,CAAC,EAAU,CACZ,EAEK,IAAkB,QAEpB,EAAU,KAAK,OAAc;EAC3B,OAAO,GAAG,EAAS,UAAU,GAAG,EAAS;EACzC,OAAO,EAAS;EACjB,EAAE,EACL,CAAC,EAAU,CACZ,EAEK,IAAc,QACZ;EACJ;GACE,MAAM;GACN,OAAO;GACP,MAAM;GACN,UAAU;GACV,SAAS;GACT,SAAS;GACV;EACD;GAAE,MAAM;GAAQ,OAAO;GAAQ,MAAM;GAAQ,UAAU;GAAM;EAC7D;GAAE,MAAM;GAAW,OAAO;GAAW,MAAM;GAAQ,UAAU;GAAM;EACnE;GACE,MAAM;GACN,OAAO;GACP,MAAM;GACN,UAAU;GACV,aAAa;GACd;EACD;GAAE,MAAM;GAAY,OAAO;GAAkB,MAAM;GAAQ,aAAa;GAAc;EACtF;GAAE,MAAM;GAAc,OAAO;GAAe,MAAM;GAAU;EAC5D;GAAE,MAAM;GAAY,OAAO;GAAkB,MAAM;GAAU;EAC7D;GAAE,MAAM;GAAU,OAAO;GAAU,MAAM;GAAU,SAAS;GAAgB;EAC5E;GAAE,MAAM;GAAY,OAAO;GAAsB,MAAM;GAAU,SAAS;GAAG;EAC9E,EACD,CAAC,EAAgB,CAClB,EAGK,CAAC,GAAY,KAAiB,EAAS,EAAE,EAGzC,IAAgB,QAAc;EAClC,IAAM,IAAS,EACZ,KAAK,MAAU,EAAM,KAAK,MAAM,GAAG,GAAG,CAAC,CACvC,OAAO,QAAQ,CACf,MAAM,CACN,MAAM,GAAG,CAAC;AACb,SAAO,EAAY,IAAS,EAAS,EAAO,mBAAG,IAAI,MAAM,CAAC;IACzD,CAAC,EAAY,CAAC,EAEX,IAAY,QAAc;EAC9B,IAAM,IAAQ,IAAI,KAAK,EAAc;AAErC,SADA,EAAM,QAAQ,EAAM,SAAS,GAAG,IAAa,EAAE,EACxC;IACN,CAAC,GAAe,EAAW,CAAC,EAEzB,IAAW,QAEb,MAAM,KAAK,EAAE,QAAQ,GAAG,GAAG,GAAG,MAAM;EAClC,IAAM,IAAO,IAAI,KAAK,EAAU;AAEhC,SADA,EAAK,QAAQ,EAAU,SAAS,GAAG,EAAE,EAC9B;GAAE,KAAK,GAAS,EAAK;GAAE,OAAO,GAAG,GAAI,GAAG,GAAG,EAAK,SAAS;GAAI;GACpE,EACJ,CAAC,EAAU,CACZ,EACK,IAAa,QAAc,IAAI,IAAI,EAAS,KAAK,MAAM,EAAE,IAAI,CAAC,EAAE,CAAC,EAAS,CAAC,EAE3E,IAAU,EAAS,IACnB,IAAiB,GAAG,EAAW,EAAU,UAAU,EAAE,GAAG,EAAU,SAAS,CAAC,KAChF,EAAW,EAAS,EAAQ,IAAI,CAAC,UAAU,EAC5C,GAAG,EAAS,EAAQ,IAAI,CAAC,SAAS,IAE7B,IAAc,QACZ,EAAY,QAAQ,MAAU,EAAW,IAAI,EAAM,KAAK,MAAM,GAAG,GAAG,CAAC,CAAC,EAC5E,CAAC,GAAa,EAAW,CAC1B,EAGK,KAAiB,EAAY,QAAQ,GAAG,MAAM,IAAI,EAAE,YAAY,EAAE,EAClE,KAAe,EAAY,QAAQ,GAAG,MAAM,IAAI,EAAE,UAAU,EAAE,EAC9D,KAAe,EAAY,QAAQ,MAAM,EAAE,WAAW,WAAW,CAAC,QAClE,KAAc,EAAY,QAAQ,MAAM,EAAE,WAAW,UAAU,CAAC,QAGhE,IAAgB,QAAc;EAClC,IAAM,oBAAa,IAAI,KASpB;AACH,OAAK,IAAM,KAAS,GAAa;GAC/B,IAAI,IAAM,EAAW,IAAI,EAAM,WAAW;AAC1C,GAAK,MACH,IAAM;IACJ,MAAM,EAAM,gBAAgB,EAAa,IAAI,EAAM,WAAW,EAAE,aAAa;IAC7E,QAAQ,EAAa,IAAI,EAAM,WAAW,EAAE;IAC5C,wBAAQ,IAAI,KAAqB;IACjC,OAAO;IACP,0BAAU,IAAI,KAAa;IAC5B,EACD,EAAW,IAAI,EAAM,YAAY,EAAI;GAEvC,IAAM,IAAM,EAAM,KAAK,MAAM,GAAG,GAAG;AAGnC,GAFA,EAAI,OAAO,IAAI,IAAM,EAAI,OAAO,IAAI,EAAI,IAAI,KAAK,EAAM,WAAW,EAClE,EAAI,SAAS,EAAM,YACnB,EAAI,SAAS,IAAI,EAAM,OAAO;;AAEhC,SAAO,CAAC,GAAG,EAAW,SAAS,CAAC,CAC7B,KAAK,CAAC,GAAY,QAAU;GAC3B;GACA,MAAM,EAAI;GACV,QAAQ,EAAI;GACZ,QAAQ,EAAI;GACZ,OAAO,EAAI;GACX,QAAQ,EAAI,SAAS,IAAI,UAAU,GAC/B,YACA,EAAI,SAAS,IAAI,WAAW,GAC1B,aACA;GACP,EAAE,CACF,MAAM,GAAG,MAAM,EAAE,QAAQ,EAAE,MAAM;IACnC,CAAC,GAAa,EAAa,CAAC,EAKzB,IAAgB,QAAc;EAClC,IAAM,IAAW,GAAc,EAAY;AAC3C,MAAI,CAAC,EACH,QAAO;EAET,IAAM,IAAQ,EAAS,QAAQ,GAAK,MAAQ,IAAM,EAAI,OAAO,EAAE,EACzD,IAAgB,EAAS,MAAM,MAAQ,EAAI,UAAU,WAAW,EAAE,SAAS;AAEjF,SAAO;GAAE;GAAU,aAAa,GADZ,IAAQ,IAAI,KAAK,MAAO,IAAgB,IAAS,IAAI,GAAG,EAC7B;GAAI,SAAS;GAAY;IACvE,CAAC,EAAY,CAAC,EAGX,IAAmB,EAAY,QAAgC,GAAK,OACxE,EAAI,EAAE,YAAY,EAAI,EAAE,YAAY,KAAK,EAAE,YACpC,IACN,EAAE,CAAC,EACA,KAAkB,KAAK,IAAI,GAAG,OAAO,OAAO,EAAiB,EAAE,EAAE,EAEjE,IAAgB,QACd,CAAC,GAAG,EAAY,CAAC,MAAM,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,KAAK,CAAC,EACnE,CAAC,EAAY,CACd,EACK,EAAE,UAAM,aAAU,WAAQ,eAAY,GAAc,EAAE,iBAAiB,GAAG,CAAC,EAC3E,KAAoB,EAAc,MAAM,GAAQ,IAAS,EAAS,EAElE,KAAc,MAAmD;AAErE,EADA,GAAgB,KAAW,KAAK,EAChC,EAAW,WAAW;IAGlB,KAAe,OAAO,MAAuB;EACjD,IAAM,IAAa,EAAQ,GAAQ,aAAa,EAC1C,IAAW,EAAU,MAAM,MAAS,EAAK,OAAO,EAAW;AAcjE,QAAM,GAbqB;GACzB,IAAI,IAAa;GACjB;GACA,cAAc,IAAW,GAAG,EAAS,UAAU,GAAG,EAAS,aAAa;GACxE,MAAM,EAAQ,GAAQ,OAAO;GAC7B,SAAS,EAAQ,GAAQ,UAAU;GACnC,UAAU,EAAQ,GAAQ,WAAW;GACrC,YAAY,EAAU,GAAQ,aAAa;GAC3C,UAAU,EAAU,GAAQ,WAAW;GACvC,SAAS,EAAQ,GAAQ,UAAU;GACnC,QAAS,EAAQ,GAAQ,SAAS,IAA4B;GAC9D,UAAU,GAAQ,GAAQ,WAAW;GACtC,CAC0B;IAGvB,KAAiB,OAAO,MAAuB;EACnD,IAAM,IAAQ,EAAa;AACtB,OAGL,MAAM,GAAgB,EAAM,IAAI;GAC9B,UAAU,EAAQ,GAAQ,WAAW;GACrC,YAAY,EAAU,GAAQ,aAAa;GAC3C,UAAU,EAAU,GAAQ,WAAW;GACvC,QAAS,EAAQ,GAAQ,SAAS,IAA4B,EAAM;GACrE,CAAC;IAIE,KAA4B;EAChC,UAAU;EACV,GAAI,IAAe;GAAE,YAAY,EAAa;GAAY,MAAM,EAAa;GAAM,GAAG,EAAE;EACzF,EAEK,KAA0C,EAAa,UACzD;EACE,UAAU,EAAa,QAAQ;EAC/B,YAAY,EAAa,QAAQ;EACjC,UAAU,EAAa,QAAQ;EAC/B,QAAQ,EAAa,QAAQ;EAC9B,GACD,KAAA;AAEJ,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD,EAAA,UAAA;KACE,kBAAC,MAAD;MAAI,WAAU;gBACZ,kBAAC,IAAD,EAAA,UAAc,iBAA4B,CAAA;MACvC,CAAA;KACL,kBAAC,KAAD;MAAG,WAAU;gBAA8B;MAA4C,CAAA;KACvF,kBAAC,IAAD;MAAa,WAAU;gBAAO;MAGhB,CAAA;KACV,EAAA,CAAA,EACN,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,OAAD;MAAK,WAAU;gBAAf;OACE,kBAAC,GAAD;QACE,SAAQ;QACR,MAAK;QACL,WAAU;QACV,cAAW;QACX,eAAe,GAAe,MAAM,IAAI,EAAE;kBAE1C,kBAAC,IAAD,EAAa,WAAU,WAAY,CAAA;QAC5B,CAAA;OACT,kBAAC,QAAD;QAAM,WAAU;kBACb;QACI,CAAA;OACP,kBAAC,GAAD;QACE,SAAQ;QACR,MAAK;QACL,WAAU;QACV,cAAW;QACX,eAAe,GAAe,MAAM,IAAI,EAAE;kBAE1C,kBAAC,GAAD,EAAc,WAAU,WAAY,CAAA;QAC7B,CAAA;OACL;SACN,kBAAC,GAAD;MAAQ,MAAK;MAAK,eAAe,GAAY;gBAA7C,CACE,kBAAC,GAAD,EAAO,WAAU,gBAAiB,CAAA,EAAA,WAE3B;QACL;OACF;;GAEN,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD;MACE,OAAM;MACN,OAAO,GAAe,QAAQ,EAAE;MAChC,MAAM,kBAAC,GAAD,EAAO,WAAU,WAAY,CAAA;MACnC,CAAA;KACF,kBAAC,GAAD;MACE,OAAM;MACN,OAAO,GAAG,GAAa,QAAQ,EAAE,CAAC;MAClC,MAAM,kBAAC,GAAD,EAAO,WAAU,WAAY,CAAA;MACnC,CAAA;KACF,kBAAC,GAAD;MACE,OAAM;MACN,OAAO;MACP,MAAM,kBAAC,IAAD,EAAa,WAAU,WAAY,CAAA;MACzC,CAAA;KACF,kBAAC,GAAD;MAAU,OAAM;MAAU,OAAO;MAAa,MAAM,kBAAC,GAAD,EAAO,WAAU,WAAY,CAAA;MAAI,CAAA;KACjF;;GAEN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UAAW,aAAqB,CAAA,EAChC,kBAAC,GAAD,EAAA,UAAA,CACG,GAAe,gDACA,EAAA,CAAA,CACP,EAAA,CAAA,EACb,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,SAAD;MAAO,WAAU;gBAAjB,CACE,kBAAC,SAAD,EAAA,UACE,kBAAC,MAAD;OAAI,WAAU;iBAAd;QACE,kBAAC,MAAD;SAAI,WAAU;mBAAkC;SAAa,CAAA;QAC5D,EAAS,KAAK,MACb,kBAAC,MAAD;SAAkB,WAAU;mBACzB,EAAI;SACF,EAFI,EAAI,IAER,CACL;QACF,kBAAC,MAAD;SAAI,WAAU;mBAAmC;SAAU,CAAA;QAC3D,kBAAC,MAAD;SAAI,WAAU;mBAAoC;SAAW,CAAA;QAC1D;UACC,CAAA,EACR,kBAAC,SAAD,EAAA,UACG,EAAc,KAAK,MAClB,kBAAC,MAAD;OAAyB,WAAU;iBAAnC;QACE,kBAAC,MAAD;SAAI,WAAU;mBACZ,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,GAAD;WACE,QAAQ,CAAC;YAAE,MAAM,EAAI;YAAM,WAAW,EAAI;YAAQ,CAAC;WACnD,MAAM;WACN,CAAA,EACF,kBAAC,QAAD;WAAM,WAAU;qBAAwB,EAAI;WAAY,CAAA,CACpD;;SACH,CAAA;QACJ,EAAS,KAAK,MAAQ;SACrB,IAAM,IAAQ,EAAI,OAAO,IAAI,EAAI,IAAI,IAAI;AACzC,gBACE,kBAAC,MAAD;UAAkB,WAAU;oBAC1B,kBAAC,UAAD;WACE,MAAK;WACL,eACE,EAAW;YAAE,YAAY,EAAI;YAAY,MAAM,EAAI;YAAK,CAAC;WAE3D,WAAW,+EACT,IAAQ,IACJ,8DACA;WAEN,cAAY,gBAAgB,EAAI,KAAK,MAAM,EAAI;qBAE9C,EAAM,QAAQ,EAAE;WACV,CAAA;UACN,EAfI,EAAI,IAeR;UAEP;QACF,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAI,MAAM,QAAQ,EAAE;SAClB,CAAA;QACL,kBAAC,MAAD;SAAI,WAAU;mBACZ,kBAAC,GAAD;UACE,SACE,EAAI,WAAW,aACX,YACA,EAAI,WAAW,YACb,cACA;UAER,WAAU;oBAET,EAAgB,EAAI,OAAO;UACtB,CAAA;SACL,CAAA;QACF;SAhDI,EAAI,WAgDR,CACL,EACI,CAAA,CACF;;KACJ,CAAA,EACL,EAAc,WAAW,KACxB,kBAAC,KAAD;KAAG,WAAU;eAAb;MAA4D;MACtC;MAAe;MACjC;OAEM,EAAA,CAAA,CACT,EAAA,CAAA,EAEP,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UAAW,kBAA0B,CAAA,EACrC,kBAAC,GAAD,EAAA,UAAA,CAAiB,uCAAoC,EAAiC,EAAA,CAAA,CAC3E,EAAA,CAAA,EACb,kBAAC,GAAD;KAAa,WAAU;eACpB,IACC,kBAAC,GAAD;MACE,UAAU,EAAc;MACxB,aAAa,EAAc;MAC3B,eAAe,EAAc;MAC7B,iBAAiB,MAAM,GAAG,EAAE,QAAQ,EAAE,CAAC;MACvC,OAAM;MACN,CAAA,GAEF,kBAAC,KAAD;MAAG,WAAU;gBAAgD;MAEzD,CAAA;KAEM,CAAA,CACT,EAAA,CAAA,CACH;;GAEN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UAAW,oBAA4B,CAAA,EACvC,kBAAC,GAAD,EAAA,UAAA,CAAiB,wBAAqB,EAAiC,EAAA,CAAA,CAC5D,EAAA,CAAA,EACb,kBAAC,GAAD;KAAa,WAAU;eAAvB,CACG,OAAO,QAAQ,EAAiB,CAC9B,MAAM,GAAG,IAAI,GAAG,OAAO,IAAI,EAAE,CAC7B,KAAK,CAAC,GAAS,OACd,kBAAC,OAAD;MAAmB,WAAU;gBAA7B;OACE,kBAAC,QAAD;QAAM,WAAU;kBAAyB;QAAe,CAAA;OACxD,kBAAC,OAAD;QAAK,WAAU;kBACb,kBAAC,OAAD;SACE,WAAU;SACV,OAAO,EAAE,OAAO,GAAG,KAAK,MAAO,IAAQ,KAAmB,IAAI,CAAC,IAAI;SACnE,CAAA;QACE,CAAA;OACN,kBAAC,QAAD;QAAM,WAAU;kBAAhB,CACG,EAAM,QAAQ,EAAE,EAAC,IACb;;OACH;QAXI,EAWJ,CACN,EACH,OAAO,KAAK,EAAiB,CAAC,WAAW,KACxC,kBAAC,KAAD;MAAG,WAAU;gBAA+C;MAExD,CAAA,CAEM;OACT,EAAA,CAAA,EAEP,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UAAW,mBAA2B,CAAA,EACtC,kBAAC,GAAD,EAAA,UAAiB,wCAAsD,CAAA,CAC5D,EAAA,CAAA,EACb,kBAAC,GAAD;KAAa,WAAU;eAAvB;MACG,GAAkB,KAAK,MACtB,kBAAC,OAAD;OAAoB,WAAU;iBAA9B,CACE,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,GAAD;SACE,QAAQ,CACN;UACE,MAAM,EAAM;UACZ,WAAW,EAAa,IAAI,EAAM,WAAW,EAAE;UAChD,CACF;SACD,MAAM;SACN,CAAA,EACF,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;SAAG,WAAU;mBAAuB,EAAM;SAAiB,CAAA,EAC3D,kBAAC,KAAD;SAAG,WAAU;mBAA+B,EAAM;SAAY,CAAA,CAC1D,EAAA,CAAA,CACF;WACN,kBAAC,OAAD;QAAK,WAAU;kBAAf;SACE,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,OAAD;WAAK,WAAU;qBAAf,CACE,kBAAC,IAAD,EAAO,WAAU,oCAAqC,CAAA,EACrD,EAAM,QACH;cACN,kBAAC,OAAD;WAAK,WAAU;qBAAf,CACE,kBAAC,GAAD,EAAQ,WAAU,kCAAmC,CAAA,EACpD,EAAM,SACH;aACF;;SACN,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,KAAD;WAAG,WAAU;qBAAb,CAAoC,EAAM,YAAW,IAAK;cACzD,EAAM,WAAW,KAChB,kBAAC,KAAD;WAAG,WAAU;qBAAb;YAAgD;YAAE,EAAM;YAAS;YAAQ;aAEvE;;SACN,kBAAC,GAAD;UACE,SAAS,EAAM,WAAW,aAAa,YAAY;UACnD,WAAU;oBAET,EAAgB,EAAM,OAAO;UACxB,CAAA;SACR,kBAAC,GAAD;UACE,SAAQ;UACR,MAAK;UACL,WAAU;UACV,cAAY,aAAa,EAAM;UAC/B,eAAe,EAAa,UAAU,EAAM;oBAE5C,kBAAC,GAAD,EAAQ,WAAU,WAAY,CAAA;UACvB,CAAA;SACL;UACF;SAjDI,EAAM,GAiDV,CACN;MACD,EAAc,WAAW,KACxB,kBAAC,KAAD;OAAG,WAAU;iBAA+C;OAExD,CAAA;MAEL,EAAc,SAAS,KACtB,kBAAC,GAAD;OACQ;OACI;OACV,YAAY,EAAc;OAC1B,cAAc;OACd,CAAA;MAEQ;OACT,EAAA,CAAA,CACH;;GAEN,kBAAC,GAAD;IACE,MAAM,EAAW;IACjB,cAAc,EAAW;IACzB,OAAM;IACN,aAAY;IACZ,QAAQ;IACR,eAAe;IACf,aAAY;IACZ,UAAU;IACV,CAAA;GAEF,kBAAC,GAAD;IACE,MAAM,EAAa;IACnB,cAAc,EAAa;IAC3B,OAAM;IACN,aACE,EAAa,UACT,wBAAwB,EAAa,QAAQ,aAAa,KAC1D,KAAA;IAEN,QAAQ;IACR,eAAe;IACf,aAAY;IACZ,UAAU;IACV,CAAA;GACE"}
@@ -1 +1 @@
1
- {"version":3,"file":"LearningCertificatesPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/LearningCertificatesPagePage.tsx"],"sourcesContent":["import { useMemo } from 'react';\nimport {\n Button,\n GradientText,\n IllustratedEmptyState,\n PagePurpose,\n} 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(course: string, recipient: string, dateLabel: string, id: string): void {\n if (typeof document === 'undefined') return;\n const esc = (s: string) =>\n s.replace(/[&<>\"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;' })[c] ?? c);\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":";;;;;;;;;;;AAcA,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,EAAoB,GAAgB,GAAmB,GAAmB,GAAkB;AACnG,KAAI,OAAO,WAAa,IAAa;CACrC,IAAM,KAAO,MACX,EAAE,QAAQ,YAAY,OAAO;EAAE,KAAK;EAAS,KAAK;EAAQ,KAAK;EAAQ,MAAK;EAAU,EAAE,MAAM,EAAE,EAC5F,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-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 +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 {\n Check,\n ChevronLeft,\n ChevronRight,\n CircleDashed,\n FileText,\n Play,\n} 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={() => navigate(`../learning/course-details-page?id=${encodeURIComponent(course.id)}`)}\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\">Notes are saved automatically on this device.</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-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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"}
@@ -10,8 +10,8 @@ import { BarChart3 as l, Briefcase as u, Code2 as d, GraduationCap as f, Megapho
10
10
  import { useNavigate as b } from "react-router-dom";
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
- import { usePagination as N } from "@burdenoff/fe-libs/shared/hooks";
14
- import { useGlobalUi as P } from "@burdenoff/fe-libs/shared/providers/shell";
13
+ import { useGlobalUi as N } from "@burdenoff/fe-libs/shared/providers/shell";
14
+ import { usePagination as P } from "@burdenoff/fe-libs/shared/hooks";
15
15
  //#region src/crewfoundry/pages/LearningCoursesPagePage.tsx
16
16
  var F = [
17
17
  {
@@ -115,7 +115,7 @@ function R(e) {
115
115
  return e === "beginner" ? "secondary" : e === "intermediate" ? "default" : "destructive";
116
116
  }
117
117
  function z() {
118
- let { courses: n, addCourse: l, enrollCourse: u } = e(), { showToast: d } = P(), f = b(), [p, m] = c(""), [v, z] = c("all"), [B, V] = c(null), H = a(), { page: U, pageSize: W, offset: G, setPage: K } = N({ initialPageSize: 12 }), q = s(() => {
118
+ let { courses: n, addCourse: l, enrollCourse: u } = e(), { showToast: d } = N(), f = b(), [p, m] = c(""), [v, z] = c("all"), [B, V] = c(null), H = a(), { page: U, pageSize: W, offset: G, setPage: K } = P({ initialPageSize: 12 }), q = s(() => {
119
119
  let e = /* @__PURE__ */ new Map();
120
120
  for (let t of n) {
121
121
  let n = t.category.trim() || "Uncategorized";
@@ -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 (c.includes('engineer') || c.includes('develop') || c.includes('program') || c.includes('tech'))\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 = 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={() =>\n started ? openPlayer(course.id) : void handleEnroll(course)\n }\n >\n <Play className=\"mr-1 h-4 w-4\" />\n {enrollingId === course.id\n ? 'Enrolling…'\n : started\n ? 'Continue'\n : '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;AAUhC,QATI,EAAE,SAAS,WAAW,IAAI,EAAE,SAAS,UAAU,IAAI,EAAE,SAAS,UAAU,IAAI,EAAE,SAAS,OAAO,GACzF,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,IAAkB,MAAa,UAAU,EAAE,SAAS,MAAM,IAAI,qBAAqB,GACnF,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,eACE,IAAU,EAAW,EAAO,GAAG,GAAG,KAAK,EAAa,EAAO;sBAL/D,CAQE,kBAAC,GAAD,EAAM,WAAU,gBAAiB,CAAA,EAChC,MAAgB,EAAO,KACpB,eACA,IACE,aACA,SACC;;WACL;;SACM;;OACJ;QAvFL,EAAO,GAuFF;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,UA3Ne,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;;IA6ME,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-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":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"LearningLearningPathsPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/LearningLearningPathsPagePage.tsx"],"sourcesContent":["import { useMemo } from 'react';\nimport {\n Badge,\n Button,\n Card,\n CardContent,\n GradientText,\n IllustratedEmptyState,\n PagePurpose,\n Pagination,\n} from '@burdenoff/fe-libs/ui';\nimport { usePagination } from '@burdenoff/fe-libs/shared/hooks';\nimport { BookOpen, Clock, Layers, Plus } from 'lucide-react';\nimport { useNavigate } from 'react-router-dom';\nimport { useCrewFoundryStore, type LearningPath } from '../store/crewfoundryStore';\nimport {\n FormModal,\n useFormModal,\n type FieldConfig,\n type FormValues,\n} from '../components/forms/FormModal';\nimport { AvatarStack, PathSteps, type PathStep, type Person } from '../components/viz';\nimport { formatEnumLabel } from '../utils/formatEnumLabel';\n\nconst PATH_FIELDS: FieldConfig[] = [\n { name: 'title', label: 'Path Title', type: 'text', required: true, colSpan: 2 },\n { name: 'description', label: 'Description', type: 'textarea', required: true, colSpan: 2 },\n {\n name: 'duration',\n label: 'Duration',\n type: 'text',\n required: true,\n placeholder: 'e.g. 3 months',\n },\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\nexport function LearningLearningPathsPagePage() {\n const { learningPaths, courses, employees, addLearningPath } = useCrewFoundryStore();\n const navigate = useNavigate();\n const createModal = useFormModal();\n const { page, pageSize, offset, setPage } = usePagination({ initialPageSize: 6 });\n const pagedPaths = learningPaths.slice(offset, offset + pageSize);\n\n const courseTitleById = useMemo(() => {\n const map = new Map<string, string>();\n for (const c of courses) {\n map.set(c.id, c.title);\n }\n return map;\n }, [courses]);\n\n const learnerPeople = useMemo<Person[]>(\n () =>\n employees.slice(0, 5).map((e) => ({\n name: `${e.firstName} ${e.lastName}`.trim(),\n avatarUrl: e.avatar,\n })),\n [employees]\n );\n\n const openPath = (id: string) =>\n navigate(`../learning/learning-path-detail-page?id=${encodeURIComponent(id)}`);\n\n const handleCreate = async (values: FormValues) => {\n const path: LearningPath = {\n id: `path-${Date.now()}`,\n title: String(values.title ?? ''),\n description: String(values.description ?? ''),\n courses: [],\n duration: String(values.duration ?? ''),\n level: String(values.level ?? 'beginner'),\n enrolled: 0,\n completionRate: 0,\n };\n await addLearningPath(path);\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>Learning Paths</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">Structured skill journeys for career growth</p>\n <PagePurpose className=\"mt-1\">\n Structured learning journeys that string courses together — follow a path to build\n toward a role or skill set step by step.\n </PagePurpose>\n </div>\n <Button onClick={() => createModal.openModal()}>\n <Plus className=\"mr-1 h-4 w-4\" />\n New Path\n </Button>\n </div>\n\n {learningPaths.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-generic\"\n title=\"No learning paths yet\"\n description=\"Learning paths will appear here once they are created.\"\n action={\n <Button onClick={() => createModal.openModal()}>\n <Plus className=\"mr-1 h-4 w-4\" />\n Create your first path\n </Button>\n }\n />\n ) : (\n <div className=\"space-y-4\">\n {pagedPaths.map((path) => {\n const courseIds = path.courses;\n const numCourses = courseIds.length;\n const completedCount = Math.round((path.completionRate / 100) * numCourses);\n const steps: PathStep[] = courseIds.map((id, i) => ({\n label: courseTitleById.get(id) ?? 'Course',\n state: i < completedCount ? 'done' : i === completedCount ? 'active' : 'todo',\n }));\n\n return (\n <Card key={path.id} className=\"overflow-hidden\">\n <CardContent className=\"p-5\">\n <div className=\"grid gap-5 lg:grid-cols-[minmax(0,18rem)_minmax(0,1fr)]\">\n {/* Left: identity */}\n <div className=\"space-y-3\">\n <button\n type=\"button\"\n onClick={() => openPath(path.id)}\n className=\"text-left text-lg font-semibold outline-none hover:text-action-primary-bg focus-visible:text-action-primary-bg\"\n >\n {path.title}\n </button>\n <p className=\"line-clamp-2 text-sm text-text-secondary\">{path.description}</p>\n <div className=\"flex flex-wrap gap-2\">\n <Badge variant=\"secondary\" className=\"text-[10px]\">\n {formatEnumLabel(path.level)}\n </Badge>\n <Badge variant=\"outline\" className=\"inline-flex items-center gap-1 text-[10px]\">\n <Clock className=\"h-3 w-3\" />\n {path.duration}\n </Badge>\n <Badge variant=\"outline\" className=\"inline-flex items-center gap-1 text-[10px]\">\n <BookOpen className=\"h-3 w-3\" />\n {numCourses} courses\n </Badge>\n </div>\n {learnerPeople.length > 0 && path.enrolled > 0 && (\n <div>\n <p className=\"mb-1 text-xs text-text-secondary\">Enrolled learners</p>\n <div className=\"flex items-center gap-2\">\n <AvatarStack people={learnerPeople} size={28} max={5} />\n <span className=\"text-xs text-text-secondary tabular-nums\">\n {path.enrolled} enrolled\n </span>\n </div>\n </div>\n )}\n </div>\n\n {/* Right: progress + timeline */}\n <div className=\"space-y-4\">\n <div>\n <div className=\"mb-1 flex items-center justify-between text-sm\">\n <span className=\"text-text-secondary\">Overall progress</span>\n <span className=\"font-semibold tabular-nums\">\n {path.completionRate}% Complete\n </span>\n </div>\n <div className=\"h-2.5 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: `${path.completionRate}%` }}\n />\n </div>\n </div>\n {steps.length > 0 ? (\n <PathSteps steps={steps} />\n ) : (\n <div className=\"flex items-center gap-2 rounded-md border border-dashed border-border-default px-3 py-4 text-sm text-text-secondary\">\n <Layers className=\"h-4 w-4\" />\n No courses in this path yet — open the path to add them.\n </div>\n )}\n <div>\n <Button variant=\"outline\" size=\"sm\" onClick={() => openPath(path.id)}>\n View path\n </Button>\n </div>\n </div>\n </div>\n </CardContent>\n </Card>\n );\n })}\n </div>\n )}\n\n {learningPaths.length > pageSize && (\n <Pagination\n page={page}\n pageSize={pageSize}\n totalItems={learningPaths.length}\n onPageChange={setPage}\n />\n )}\n\n <FormModal\n open={createModal.open}\n onOpenChange={createModal.onOpenChange}\n title=\"Create Learning Path\"\n description=\"Group courses into a structured journey. You can add courses after creating the path.\"\n fields={PATH_FIELDS}\n submitLabel=\"Create Path\"\n onSubmit={handleCreate}\n />\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"LearningLearningPathsPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/LearningLearningPathsPagePage.tsx"],"sourcesContent":["import { useMemo } from 'react';\nimport {\n Badge,\n Button,\n Card,\n CardContent,\n GradientText,\n IllustratedEmptyState,\n PagePurpose,\n Pagination,\n} from '@burdenoff/fe-libs/ui';\nimport { usePagination } from '@burdenoff/fe-libs/shared/hooks';\nimport { BookOpen, Clock, Layers, Plus } from 'lucide-react';\nimport { useNavigate } from 'react-router-dom';\nimport { useCrewFoundryStore, type LearningPath } from '../store/crewfoundryStore';\nimport {\n FormModal,\n useFormModal,\n type FieldConfig,\n type FormValues,\n} from '../components/forms/FormModal';\nimport { AvatarStack, PathSteps, type PathStep, type Person } from '../components/viz';\nimport { formatEnumLabel } from '../utils/formatEnumLabel';\n\nconst PATH_FIELDS: FieldConfig[] = [\n { name: 'title', label: 'Path Title', type: 'text', required: true, colSpan: 2 },\n { name: 'description', label: 'Description', type: 'textarea', required: true, colSpan: 2 },\n {\n name: 'duration',\n label: 'Duration',\n type: 'text',\n required: true,\n placeholder: 'e.g. 3 months',\n },\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\nexport function LearningLearningPathsPagePage() {\n const { learningPaths, courses, employees, addLearningPath } = useCrewFoundryStore();\n const navigate = useNavigate();\n const createModal = useFormModal();\n const { page, pageSize, offset, setPage } = usePagination({ initialPageSize: 6 });\n const pagedPaths = learningPaths.slice(offset, offset + pageSize);\n\n const courseTitleById = useMemo(() => {\n const map = new Map<string, string>();\n for (const c of courses) {\n map.set(c.id, c.title);\n }\n return map;\n }, [courses]);\n\n const learnerPeople = useMemo<Person[]>(\n () =>\n employees.slice(0, 5).map((e) => ({\n name: `${e.firstName} ${e.lastName}`.trim(),\n avatarUrl: e.avatar,\n })),\n [employees]\n );\n\n const openPath = (id: string) =>\n navigate(`../learning/learning-path-detail-page?id=${encodeURIComponent(id)}`);\n\n const handleCreate = async (values: FormValues) => {\n const path: LearningPath = {\n id: `path-${Date.now()}`,\n title: String(values.title ?? ''),\n description: String(values.description ?? ''),\n courses: [],\n duration: String(values.duration ?? ''),\n level: String(values.level ?? 'beginner'),\n enrolled: 0,\n completionRate: 0,\n };\n await addLearningPath(path);\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>Learning Paths</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">Structured skill journeys for career growth</p>\n <PagePurpose className=\"mt-1\">\n Structured learning journeys that string courses together — follow a path to build\n toward a role or skill set step by step.\n </PagePurpose>\n </div>\n <Button onClick={() => createModal.openModal()}>\n <Plus className=\"mr-1 h-4 w-4\" />\n New Path\n </Button>\n </div>\n\n {learningPaths.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-generic\"\n title=\"No learning paths yet\"\n description=\"Learning paths will appear here once they are created.\"\n action={\n <Button onClick={() => createModal.openModal()}>\n <Plus className=\"mr-1 h-4 w-4\" />\n Create your first path\n </Button>\n }\n />\n ) : (\n <div className=\"space-y-4\">\n {pagedPaths.map((path) => {\n const courseIds = path.courses;\n const numCourses = courseIds.length;\n const completedCount = Math.round((path.completionRate / 100) * numCourses);\n const steps: PathStep[] = courseIds.map((id, i) => ({\n label: courseTitleById.get(id) ?? 'Course',\n state: i < completedCount ? 'done' : i === completedCount ? 'active' : 'todo',\n }));\n\n return (\n <Card key={path.id} className=\"overflow-hidden\">\n <CardContent className=\"p-5\">\n <div className=\"grid gap-5 lg:grid-cols-[minmax(0,18rem)_minmax(0,1fr)]\">\n {/* Left: identity */}\n <div className=\"space-y-3\">\n <button\n type=\"button\"\n onClick={() => openPath(path.id)}\n className=\"text-left text-lg font-semibold outline-none hover:text-action-primary-bg focus-visible:text-action-primary-bg\"\n >\n {path.title}\n </button>\n <p className=\"line-clamp-2 text-sm text-text-secondary\">{path.description}</p>\n <div className=\"flex flex-wrap gap-2\">\n <Badge variant=\"secondary\" className=\"text-[10px]\">\n {formatEnumLabel(path.level)}\n </Badge>\n <Badge\n variant=\"outline\"\n className=\"inline-flex items-center gap-1 text-[10px]\"\n >\n <Clock className=\"h-3 w-3\" />\n {path.duration}\n </Badge>\n <Badge\n variant=\"outline\"\n className=\"inline-flex items-center gap-1 text-[10px]\"\n >\n <BookOpen className=\"h-3 w-3\" />\n {numCourses} courses\n </Badge>\n </div>\n {learnerPeople.length > 0 && path.enrolled > 0 && (\n <div>\n <p className=\"mb-1 text-xs text-text-secondary\">Enrolled learners</p>\n <div className=\"flex items-center gap-2\">\n <AvatarStack people={learnerPeople} size={28} max={5} />\n <span className=\"text-xs text-text-secondary tabular-nums\">\n {path.enrolled} enrolled\n </span>\n </div>\n </div>\n )}\n </div>\n\n {/* Right: progress + timeline */}\n <div className=\"space-y-4\">\n <div>\n <div className=\"mb-1 flex items-center justify-between text-sm\">\n <span className=\"text-text-secondary\">Overall progress</span>\n <span className=\"font-semibold tabular-nums\">\n {path.completionRate}% Complete\n </span>\n </div>\n <div className=\"h-2.5 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: `${path.completionRate}%` }}\n />\n </div>\n </div>\n {steps.length > 0 ? (\n <PathSteps steps={steps} />\n ) : (\n <div className=\"flex items-center gap-2 rounded-md border border-dashed border-border-default px-3 py-4 text-sm text-text-secondary\">\n <Layers className=\"h-4 w-4\" />\n No courses in this path yet — open the path to add them.\n </div>\n )}\n <div>\n <Button variant=\"outline\" size=\"sm\" onClick={() => openPath(path.id)}>\n View path\n </Button>\n </div>\n </div>\n </div>\n </CardContent>\n </Card>\n );\n })}\n </div>\n )}\n\n {learningPaths.length > pageSize && (\n <Pagination\n page={page}\n pageSize={pageSize}\n totalItems={learningPaths.length}\n onPageChange={setPage}\n />\n )}\n\n <FormModal\n open={createModal.open}\n onOpenChange={createModal.onOpenChange}\n title=\"Create Learning Path\"\n description=\"Group courses into a structured journey. You can add courses after creating the path.\"\n fields={PATH_FIELDS}\n submitLabel=\"Create Path\"\n onSubmit={handleCreate}\n />\n </div>\n );\n}\n"],"mappings":"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"}