@burdenoff/microfe-crewfoundry 2026.731.1 → 2026.801.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/dist/crewfoundry/CrewFoundryRoutes.js +1 -1
  2. package/dist/crewfoundry/CrewFoundryRoutes.js.map +1 -1
  3. package/dist/crewfoundry/pages/CommunicationsAnnouncementsPagePage.js +1 -1
  4. package/dist/crewfoundry/pages/CommunicationsAnnouncementsPagePage.js.map +1 -1
  5. package/dist/crewfoundry/pages/CommunicationsFilesPagePage.js +1 -1
  6. package/dist/crewfoundry/pages/CommunicationsFilesPagePage.js.map +1 -1
  7. package/dist/crewfoundry/pages/CommunicationsTeamsPagePage.js +1 -1
  8. package/dist/crewfoundry/pages/CommunicationsTeamsPagePage.js.map +1 -1
  9. package/dist/crewfoundry/pages/CompanyClientManagementPagePage.js +1 -1
  10. package/dist/crewfoundry/pages/CompanyClientManagementPagePage.js.map +1 -1
  11. package/dist/crewfoundry/pages/CompanyVendorManagementPagePage.js +1 -1
  12. package/dist/crewfoundry/pages/CompanyVendorManagementPagePage.js.map +1 -1
  13. package/dist/crewfoundry/pages/DashboardDashboardPagePage.js +1 -1
  14. package/dist/crewfoundry/pages/DashboardDashboardPagePage.js.map +1 -1
  15. package/dist/crewfoundry/pages/DashboardEnhancedDashboardPagePage.js +1 -1
  16. package/dist/crewfoundry/pages/DashboardEnhancedDashboardPagePage.js.map +1 -1
  17. package/dist/crewfoundry/pages/HiringApplicationsPagePage.js +1 -1
  18. package/dist/crewfoundry/pages/HiringApplicationsPagePage.js.map +1 -1
  19. package/dist/crewfoundry/pages/HiringCreateJobPagePage.js +1 -1
  20. package/dist/crewfoundry/pages/HiringCreateJobPagePage.js.map +1 -1
  21. package/dist/crewfoundry/pages/HiringJobDetailsPagePage.js +1 -1
  22. package/dist/crewfoundry/pages/HiringJobDetailsPagePage.js.map +1 -1
  23. package/dist/crewfoundry/pages/HiringJobsPagePage.js +1 -1
  24. package/dist/crewfoundry/pages/HiringJobsPagePage.js.map +1 -1
  25. package/dist/crewfoundry/pages/HrEmployeeProfilePagePage.js +1 -1
  26. package/dist/crewfoundry/pages/HrEmployeeProfilePagePage.js.map +1 -1
  27. package/dist/crewfoundry/pages/HrEmployeesPagePage.js +1 -1
  28. package/dist/crewfoundry/pages/HrEmployeesPagePage.js.map +1 -1
  29. package/dist/crewfoundry/pages/LearningAssessmentsPagePage.js +1 -1
  30. package/dist/crewfoundry/pages/LearningAssessmentsPagePage.js.map +1 -1
  31. package/dist/crewfoundry/pages/LearningCertificatesPagePage.js +1 -1
  32. package/dist/crewfoundry/pages/LearningCertificatesPagePage.js.map +1 -1
  33. package/dist/crewfoundry/pages/LearningCourseDetailsPagePage.js +1 -1
  34. package/dist/crewfoundry/pages/LearningCourseDetailsPagePage.js.map +1 -1
  35. package/dist/crewfoundry/pages/LearningCourseLearningPagePage.js +1 -1
  36. package/dist/crewfoundry/pages/LearningCourseLearningPagePage.js.map +1 -1
  37. package/dist/crewfoundry/pages/LearningCoursesPagePage.js +1 -1
  38. package/dist/crewfoundry/pages/LearningCoursesPagePage.js.map +1 -1
  39. package/dist/crewfoundry/pages/LearningLearningPagePage.js +1 -1
  40. package/dist/crewfoundry/pages/LearningLearningPagePage.js.map +1 -1
  41. package/dist/crewfoundry/pages/LearningLearningPathDetailPagePage.js +1 -1
  42. package/dist/crewfoundry/pages/LearningLearningPathDetailPagePage.js.map +1 -1
  43. package/dist/crewfoundry/pages/LearningLearningPathsPagePage.js +1 -1
  44. package/dist/crewfoundry/pages/LearningLearningPathsPagePage.js.map +1 -1
  45. package/dist/crewfoundry/pages/LearningMyLearningPagePage.js +1 -1
  46. package/dist/crewfoundry/pages/LearningMyLearningPagePage.js.map +1 -1
  47. package/dist/crewfoundry/pages/MarketplaceFreelancerProfilePageEnhancedPage.js +1 -1
  48. package/dist/crewfoundry/pages/MarketplaceFreelancerProfilePageEnhancedPage.js.map +1 -1
  49. package/dist/crewfoundry/pages/MarketplaceFreelancerProfilePagePage.js +1 -1
  50. package/dist/crewfoundry/pages/MarketplaceFreelancerProfilePagePage.js.map +1 -1
  51. package/dist/crewfoundry/pages/MarketplaceMarketplacePagePage.js +1 -1
  52. package/dist/crewfoundry/pages/MarketplaceMarketplacePagePage.js.map +1 -1
  53. package/dist/crewfoundry/pages/MarketplaceMarketplaceServicesPagePage.js +1 -1
  54. package/dist/crewfoundry/pages/MarketplaceMarketplaceServicesPagePage.js.map +1 -1
  55. package/dist/crewfoundry/pages/MarketplaceMyServicesPagePage.js +1 -1
  56. package/dist/crewfoundry/pages/MarketplaceMyServicesPagePage.js.map +1 -1
  57. package/dist/crewfoundry/pages/MarketplaceProjectDetailsPagePage.js +1 -1
  58. package/dist/crewfoundry/pages/MarketplaceProjectDetailsPagePage.js.map +1 -1
  59. package/dist/crewfoundry/pages/MarketplaceServiceDetailsPagePage.js +1 -1
  60. package/dist/crewfoundry/pages/MarketplaceServiceDetailsPagePage.js.map +1 -1
  61. package/dist/crewfoundry/pages/PaymentsPaymentDashboardPagePage.js +1 -1
  62. package/dist/crewfoundry/pages/PaymentsPaymentDashboardPagePage.js.map +1 -1
  63. package/dist/crewfoundry/pages/PaymentsPaymentMethodsPagePage.js +1 -1
  64. package/dist/crewfoundry/pages/PaymentsPaymentMethodsPagePage.js.map +1 -1
  65. package/dist/crewfoundry/pages/ProjectsCreateProjectPagePage.js +1 -1
  66. package/dist/crewfoundry/pages/ProjectsCreateProjectPagePage.js.map +1 -1
  67. package/dist/crewfoundry/pages/ProjectsProjectDetailsPagePage.js +1 -1
  68. package/dist/crewfoundry/pages/ProjectsProjectDetailsPagePage.js.map +1 -1
  69. package/dist/crewfoundry/pages/ProjectsProjectsPagePage.js +1 -1
  70. package/dist/crewfoundry/pages/ProjectsProjectsPagePage.js.map +1 -1
  71. package/dist/crewfoundry/pages/ProjectsResourcingPagePage.js +1 -1
  72. package/dist/crewfoundry/pages/ProjectsResourcingPagePage.js.map +1 -1
  73. package/dist/crewfoundry/pages/TeamsTeamDetailPagePage.js +1 -1
  74. package/dist/crewfoundry/pages/TeamsTeamDetailPagePage.js.map +1 -1
  75. package/dist/crewfoundry/pages/TeamsTeamDirectoryPagePage.js +1 -1
  76. package/dist/crewfoundry/pages/TeamsTeamDirectoryPagePage.js.map +1 -1
  77. package/dist/crewfoundry/pages/TeamsTeamSettingsPagePage.js +1 -1
  78. package/dist/crewfoundry/pages/TeamsTeamSettingsPagePage.js.map +1 -1
  79. package/dist/crewfoundry/pages/TeamsTeamsPagePage.js +1 -1
  80. package/dist/crewfoundry/pages/TeamsTeamsPagePage.js.map +1 -1
  81. package/dist/crewfoundry/providers/CrewFoundryProvider.js +1 -1
  82. package/dist/crewfoundry/providers/CrewFoundryProvider.js.map +1 -1
  83. package/package.json +6 -6
@@ -1 +1 @@
1
- {"version":3,"file":"HrEmployeesPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/HrEmployeesPagePage.tsx"],"sourcesContent":["import { useState } from 'react';\nimport {\n Badge,\n Button,\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n GlassCard,\n GradientText,\n IllustratedEmptyState,\n Input,\n PagePurpose,\n Pagination,\n ResponsiveTable,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n type ResponsiveTableColumn,\n} from '@burdenoff/fe-libs/ui';\nimport { usePagination } from '@burdenoff/fe-libs/shared/hooks';\nimport { MapPin, Pencil, Plus, Search, Star, Trash2, UserCheck, Users } from 'lucide-react';\nimport { useNavigate } from 'react-router-dom';\nimport { useCrewFoundryStore, type Employee } from '../store/crewfoundryStore';\nimport { AvatarStack, SegmentedTabs } from '../components/viz';\nimport {\n FormModal,\n useFormModal,\n type FieldConfig,\n type FormValues,\n} from '../components/forms/FormModal';\nimport { ConfirmDialog } from '../components/forms/ConfirmDialog';\nimport { getNumber, getTags, getText, newClientId } from '../components/forms/formValues';\n\nconst EMPLOYEE_FIELDS: FieldConfig[] = [\n { name: 'firstName', label: 'First name', type: 'text', required: true },\n { name: 'lastName', label: 'Last name', type: 'text', required: true },\n { name: 'email', label: 'Email', type: 'email', required: true, colSpan: 2 },\n { name: 'position', label: 'Position', type: 'text', required: true },\n { name: 'department', label: 'Department', type: 'text', required: true },\n { name: 'location', label: 'Location', type: 'text', required: true },\n { name: 'phone', label: 'Phone', type: 'text', required: true },\n {\n name: 'type',\n label: 'Employment type',\n type: 'select',\n required: true,\n options: [\n { label: 'Full time', value: 'full_time' },\n { label: 'Part time', value: 'part_time' },\n { label: 'Contract', value: 'contract' },\n { label: 'Intern', value: 'intern' },\n ],\n },\n {\n name: 'status',\n label: 'Status',\n type: 'select',\n required: true,\n options: [\n { label: 'Active', value: 'active' },\n { label: 'Inactive', value: 'inactive' },\n { label: 'On leave', value: 'on_leave' },\n { label: 'Terminated', value: 'terminated' },\n ],\n },\n { name: 'salary', label: 'Annual salary', type: 'number', required: true },\n { name: 'startDate', label: 'Start date', type: 'date', required: true },\n { name: 'performanceScore', label: 'Performance score (0-5)', type: 'number' },\n {\n name: 'skills',\n label: 'Skills',\n type: 'tags',\n colSpan: 2,\n placeholder: 'Add a skill and press Enter',\n },\n];\n\nconst toFormValues = (employee: Employee): FormValues => ({\n firstName: employee.firstName,\n lastName: employee.lastName,\n email: employee.email,\n position: employee.position,\n department: employee.department,\n location: employee.location,\n phone: employee.phone,\n type: employee.type,\n status: employee.status,\n salary: employee.salary,\n startDate: employee.startDate.slice(0, 10),\n performanceScore: employee.performanceScore,\n skills: employee.skills,\n});\n\nexport function HrEmployeesPagePage() {\n const navigate = useNavigate();\n const { employees, addEmployee, updateEmployee, removeEmployee } = useCrewFoundryStore();\n const [search, setSearch] = useState('');\n const [filterDept, setFilterDept] = useState<string>('all');\n const [filterStatus, setFilterStatus] = useState<'all' | 'active' | 'on_leave'>('all');\n const form = useFormModal<Employee>();\n const [deleting, setDeleting] = useState<Employee | null>(null);\n const { page, pageSize, offset, setPage } = usePagination({ initialPageSize: 25 });\n\n const departments = [...new Set(employees.map((e) => e.department))];\n const activeCount = employees.filter((e) => e.status === 'active').length;\n const onLeaveCount = employees.filter((e) => e.status === 'on_leave').length;\n\n const filtered = employees.filter((e) => {\n const matchSearch = `${e.firstName} ${e.lastName} ${e.email} ${e.position}`\n .toLowerCase()\n .includes(search.toLowerCase());\n const matchDept = filterDept === 'all' || e.department === filterDept;\n const matchStatus = filterStatus === 'all' || e.status === filterStatus;\n return matchSearch && matchDept && matchStatus;\n });\n const pageItems = filtered.slice(offset, offset + pageSize);\n\n const handleSearchChange = (value: string) => {\n setSearch(value);\n setPage(1);\n };\n const handleDeptChange = (value: string) => {\n setFilterDept(value);\n setPage(1);\n };\n const handleStatusChange = (value: string) => {\n setFilterStatus(value as 'all' | 'active' | 'on_leave');\n setPage(1);\n };\n const avgRating =\n employees.length > 0\n ? (employees.reduce((s, e) => s + e.performanceScore, 0) / employees.length).toFixed(1)\n : '0.0';\n\n const editing = form.context;\n\n const openProfile = (employee: Employee) =>\n navigate(`../hr/employee-profile-page?id=${encodeURIComponent(employee.id)}`);\n\n const handleSubmit = async (values: FormValues) => {\n const payload: Employee = {\n id: editing?.id ?? newClientId(),\n firstName: getText(values, 'firstName'),\n lastName: getText(values, 'lastName'),\n email: getText(values, 'email'),\n department: getText(values, 'department'),\n position: getText(values, 'position'),\n status: getText(values, 'status') as Employee['status'],\n type: getText(values, 'type') as Employee['type'],\n salary: getNumber(values, 'salary'),\n startDate: getText(values, 'startDate'),\n location: getText(values, 'location'),\n phone: getText(values, 'phone'),\n performanceScore: getNumber(values, 'performanceScore'),\n skills: getTags(values, 'skills'),\n // Preserve the exit date on edit (there is no exit-date input here); new\n // employees have none. The store mapper passes it through when present.\n terminationDate: editing?.terminationDate,\n managerId: editing?.managerId,\n avatar: editing?.avatar,\n };\n if (editing) {\n await updateEmployee(editing.id, payload);\n } else {\n await addEmployee(payload);\n }\n };\n\n const columns: ResponsiveTableColumn<Employee>[] = [\n {\n key: 'name',\n header: 'Employee',\n priority: 'primary',\n cell: (emp) => (\n <div className=\"flex items-center gap-3\">\n <AvatarStack\n people={[{ name: `${emp.firstName} ${emp.lastName}`, avatarUrl: emp.avatar }]}\n size={36}\n />\n <div className=\"min-w-0\">\n <p className=\"truncate font-medium\">\n {emp.firstName} {emp.lastName}\n </p>\n <p className=\"truncate text-xs text-text-secondary\">{emp.email}</p>\n </div>\n </div>\n ),\n },\n {\n key: 'position',\n header: 'Position',\n cell: (emp) => <span className=\"text-text-secondary\">{emp.position}</span>,\n },\n {\n key: 'department',\n header: 'Department',\n cell: (emp) => <span>{emp.department}</span>,\n },\n {\n key: 'location',\n header: 'Location',\n cell: (emp) => (\n <div className=\"flex items-center gap-1 text-text-secondary\">\n <MapPin className=\"h-3 w-3\" />\n <span className=\"truncate text-xs\">{emp.location}</span>\n </div>\n ),\n },\n {\n key: 'salary',\n header: 'Salary',\n headerClassName: 'text-right',\n cellClassName: 'md:text-right',\n cell: (emp) => <span>${emp.salary.toLocaleString()}</span>,\n },\n {\n key: 'rating',\n header: 'Performance',\n cell: (emp) => {\n const tone =\n emp.performanceScore >= 4\n ? 'text-status-success-text'\n : emp.performanceScore >= 3\n ? 'text-status-warning-text'\n : 'text-status-error-text';\n return (\n <div className=\"flex items-center gap-1.5\">\n <span className={`font-semibold tabular-nums ${tone}`}>\n {emp.performanceScore.toFixed(1)}\n </span>\n <Star className={`h-3.5 w-3.5 fill-current ${tone}`} />\n </div>\n );\n },\n },\n {\n key: 'status',\n header: 'Status',\n cell: (emp) => (\n <Badge\n variant={\n emp.status === 'active'\n ? 'default'\n : emp.status === 'on_leave'\n ? 'secondary'\n : 'destructive'\n }\n className=\"text-[10px]\"\n >\n {emp.status.replace('_', ' ')}\n </Badge>\n ),\n },\n ];\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex items-center justify-between\">\n <div>\n <h1 className=\"text-2xl font-semibold\">\n <GradientText>Employee Directory</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">\n {employees.length} employees across {departments.length} departments\n </p>\n <PagePurpose className=\"mt-1\">\n Your complete employee directory — search, filter, and review everyone on the team along\n with their role, department, and status.\n </PagePurpose>\n </div>\n <Button size=\"sm\" onClick={() => form.openModal()}>\n <Plus className=\"h-4 w-4 mr-1\" />\n Add Employee\n </Button>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-4\">\n <GlassCard treatment=\"glass\" glow className=\"border-border-seam\">\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle className=\"text-sm font-medium\">Total</CardTitle>\n <Users className=\"h-4 w-4 text-text-secondary\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">{employees.length}</div>\n </CardContent>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"border-border-seam\">\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle className=\"text-sm font-medium\">Active</CardTitle>\n <UserCheck className=\"h-4 w-4 text-status-success-text\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">{activeCount}</div>\n </CardContent>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"border-border-seam\">\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle className=\"text-sm font-medium\">On Leave</CardTitle>\n <Users className=\"h-4 w-4 text-status-warning-text\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">{onLeaveCount}</div>\n </CardContent>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"border-border-seam\">\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle className=\"text-sm font-medium\">Avg Rating</CardTitle>\n <Star className=\"h-4 w-4 text-status-warning-text\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">{avgRating}</div>\n </CardContent>\n </GlassCard>\n </div>\n\n <div className=\"flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between\">\n <SegmentedTabs\n aria-label=\"Filter by status\"\n value={filterStatus}\n onChange={handleStatusChange}\n tabs={[\n { id: 'all', label: 'All', count: employees.length },\n { id: 'active', label: 'Active', count: activeCount },\n { id: 'on_leave', label: 'On Leave', count: onLeaveCount },\n ]}\n />\n <div className=\"flex flex-col gap-3 sm:flex-row\">\n <div className=\"relative flex-1 sm:max-w-sm\">\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 employees...\"\n className=\"pl-9\"\n value={search}\n onChange={(e) => handleSearchChange(e.target.value)}\n />\n </div>\n <Select value={filterDept} onValueChange={handleDeptChange}>\n <SelectTrigger className=\"w-full sm:w-56\" aria-label=\"Filter by department\">\n <SelectValue placeholder=\"All departments\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"all\">All departments</SelectItem>\n {departments.map((dept) => (\n <SelectItem key={dept} value={dept}>\n {dept}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n </div>\n\n <Card>\n <CardContent className=\"space-y-4 p-4\">\n <ResponsiveTable<Employee>\n data={pageItems}\n rowKey={(emp) => emp.id}\n columns={columns}\n onRowClick={(emp) => openProfile(emp)}\n rowActions={(emp) => (\n <>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-8 w-8 p-0\"\n aria-label={`Edit ${emp.firstName} ${emp.lastName}`}\n onClick={() => form.openModal(emp)}\n >\n <Pencil className=\"h-4 w-4\" />\n </Button>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-8 w-8 p-0\"\n aria-label={`Delete ${emp.firstName} ${emp.lastName}`}\n onClick={() => setDeleting(emp)}\n >\n <Trash2 className=\"h-4 w-4 text-status-error-text\" />\n </Button>\n </>\n )}\n emptyState={\n <div className=\"p-6\">\n <IllustratedEmptyState\n illustration=\"empty-people\"\n title=\"No employees match\"\n description=\"Try a different search term or department filter.\"\n />\n </div>\n }\n />\n {filtered.length > pageSize && (\n <Pagination\n page={page}\n pageSize={pageSize}\n totalItems={filtered.length}\n onPageChange={setPage}\n />\n )}\n </CardContent>\n </Card>\n\n <FormModal\n open={form.open}\n onOpenChange={form.onOpenChange}\n title={editing ? 'Edit employee' : 'Add employee'}\n description={\n editing\n ? 'Update this team member’s profile details.'\n : 'Add a new team member to your directory.'\n }\n fields={EMPLOYEE_FIELDS}\n initialValues={editing ? toFormValues(editing) : undefined}\n submitLabel={editing ? 'Save changes' : 'Add employee'}\n onSubmit={handleSubmit}\n />\n\n <ConfirmDialog\n open={deleting !== null}\n onOpenChange={(open) => {\n if (!open) {\n setDeleting(null);\n }\n }}\n title=\"Delete employee?\"\n description={\n deleting\n ? `${deleting.firstName} ${deleting.lastName} will be removed from the directory. This cannot be undone.`\n : undefined\n }\n confirmLabel=\"Delete\"\n destructive\n onConfirm={async () => {\n if (deleting) {\n await removeEmployee(deleting.id);\n setDeleting(null);\n }\n }}\n />\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"HrEmployeesPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/HrEmployeesPagePage.tsx"],"sourcesContent":["import { useState } from 'react';\nimport {\n Badge,\n Button,\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n GlassCard,\n GradientText,\n IllustratedEmptyState,\n Input,\n PagePurpose,\n Pagination,\n ResponsiveTable,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n type ResponsiveTableColumn,\n} from '@burdenoff/fe-libs/ui';\nimport { usePagination } from '@burdenoff/fe-libs/shared/hooks';\nimport { MapPin, Pencil, Plus, Search, Star, Trash2, UserCheck, Users } from 'lucide-react';\nimport { useNavigate } from 'react-router';\nimport { useCrewFoundryStore, type Employee } from '../store/crewfoundryStore';\nimport { AvatarStack, SegmentedTabs } from '../components/viz';\nimport {\n FormModal,\n useFormModal,\n type FieldConfig,\n type FormValues,\n} from '../components/forms/FormModal';\nimport { ConfirmDialog } from '../components/forms/ConfirmDialog';\nimport { getNumber, getTags, getText, newClientId } from '../components/forms/formValues';\n\nconst EMPLOYEE_FIELDS: FieldConfig[] = [\n { name: 'firstName', label: 'First name', type: 'text', required: true },\n { name: 'lastName', label: 'Last name', type: 'text', required: true },\n { name: 'email', label: 'Email', type: 'email', required: true, colSpan: 2 },\n { name: 'position', label: 'Position', type: 'text', required: true },\n { name: 'department', label: 'Department', type: 'text', required: true },\n { name: 'location', label: 'Location', type: 'text', required: true },\n { name: 'phone', label: 'Phone', type: 'text', required: true },\n {\n name: 'type',\n label: 'Employment type',\n type: 'select',\n required: true,\n options: [\n { label: 'Full time', value: 'full_time' },\n { label: 'Part time', value: 'part_time' },\n { label: 'Contract', value: 'contract' },\n { label: 'Intern', value: 'intern' },\n ],\n },\n {\n name: 'status',\n label: 'Status',\n type: 'select',\n required: true,\n options: [\n { label: 'Active', value: 'active' },\n { label: 'Inactive', value: 'inactive' },\n { label: 'On leave', value: 'on_leave' },\n { label: 'Terminated', value: 'terminated' },\n ],\n },\n { name: 'salary', label: 'Annual salary', type: 'number', required: true },\n { name: 'startDate', label: 'Start date', type: 'date', required: true },\n { name: 'performanceScore', label: 'Performance score (0-5)', type: 'number' },\n {\n name: 'skills',\n label: 'Skills',\n type: 'tags',\n colSpan: 2,\n placeholder: 'Add a skill and press Enter',\n },\n];\n\nconst toFormValues = (employee: Employee): FormValues => ({\n firstName: employee.firstName,\n lastName: employee.lastName,\n email: employee.email,\n position: employee.position,\n department: employee.department,\n location: employee.location,\n phone: employee.phone,\n type: employee.type,\n status: employee.status,\n salary: employee.salary,\n startDate: employee.startDate.slice(0, 10),\n performanceScore: employee.performanceScore,\n skills: employee.skills,\n});\n\nexport function HrEmployeesPagePage() {\n const navigate = useNavigate();\n const { employees, addEmployee, updateEmployee, removeEmployee } = useCrewFoundryStore();\n const [search, setSearch] = useState('');\n const [filterDept, setFilterDept] = useState<string>('all');\n const [filterStatus, setFilterStatus] = useState<'all' | 'active' | 'on_leave'>('all');\n const form = useFormModal<Employee>();\n const [deleting, setDeleting] = useState<Employee | null>(null);\n const { page, pageSize, offset, setPage } = usePagination({ initialPageSize: 25 });\n\n const departments = [...new Set(employees.map((e) => e.department))];\n const activeCount = employees.filter((e) => e.status === 'active').length;\n const onLeaveCount = employees.filter((e) => e.status === 'on_leave').length;\n\n const filtered = employees.filter((e) => {\n const matchSearch = `${e.firstName} ${e.lastName} ${e.email} ${e.position}`\n .toLowerCase()\n .includes(search.toLowerCase());\n const matchDept = filterDept === 'all' || e.department === filterDept;\n const matchStatus = filterStatus === 'all' || e.status === filterStatus;\n return matchSearch && matchDept && matchStatus;\n });\n const pageItems = filtered.slice(offset, offset + pageSize);\n\n const handleSearchChange = (value: string) => {\n setSearch(value);\n setPage(1);\n };\n const handleDeptChange = (value: string) => {\n setFilterDept(value);\n setPage(1);\n };\n const handleStatusChange = (value: string) => {\n setFilterStatus(value as 'all' | 'active' | 'on_leave');\n setPage(1);\n };\n const avgRating =\n employees.length > 0\n ? (employees.reduce((s, e) => s + e.performanceScore, 0) / employees.length).toFixed(1)\n : '0.0';\n\n const editing = form.context;\n\n const openProfile = (employee: Employee) =>\n navigate(`../hr/employee-profile-page?id=${encodeURIComponent(employee.id)}`);\n\n const handleSubmit = async (values: FormValues) => {\n const payload: Employee = {\n id: editing?.id ?? newClientId(),\n firstName: getText(values, 'firstName'),\n lastName: getText(values, 'lastName'),\n email: getText(values, 'email'),\n department: getText(values, 'department'),\n position: getText(values, 'position'),\n status: getText(values, 'status') as Employee['status'],\n type: getText(values, 'type') as Employee['type'],\n salary: getNumber(values, 'salary'),\n startDate: getText(values, 'startDate'),\n location: getText(values, 'location'),\n phone: getText(values, 'phone'),\n performanceScore: getNumber(values, 'performanceScore'),\n skills: getTags(values, 'skills'),\n // Preserve the exit date on edit (there is no exit-date input here); new\n // employees have none. The store mapper passes it through when present.\n terminationDate: editing?.terminationDate,\n managerId: editing?.managerId,\n avatar: editing?.avatar,\n };\n if (editing) {\n await updateEmployee(editing.id, payload);\n } else {\n await addEmployee(payload);\n }\n };\n\n const columns: ResponsiveTableColumn<Employee>[] = [\n {\n key: 'name',\n header: 'Employee',\n priority: 'primary',\n cell: (emp) => (\n <div className=\"flex items-center gap-3\">\n <AvatarStack\n people={[{ name: `${emp.firstName} ${emp.lastName}`, avatarUrl: emp.avatar }]}\n size={36}\n />\n <div className=\"min-w-0\">\n <p className=\"truncate font-medium\">\n {emp.firstName} {emp.lastName}\n </p>\n <p className=\"truncate text-xs text-text-secondary\">{emp.email}</p>\n </div>\n </div>\n ),\n },\n {\n key: 'position',\n header: 'Position',\n cell: (emp) => <span className=\"text-text-secondary\">{emp.position}</span>,\n },\n {\n key: 'department',\n header: 'Department',\n cell: (emp) => <span>{emp.department}</span>,\n },\n {\n key: 'location',\n header: 'Location',\n cell: (emp) => (\n <div className=\"flex items-center gap-1 text-text-secondary\">\n <MapPin className=\"h-3 w-3\" />\n <span className=\"truncate text-xs\">{emp.location}</span>\n </div>\n ),\n },\n {\n key: 'salary',\n header: 'Salary',\n headerClassName: 'text-right',\n cellClassName: 'md:text-right',\n cell: (emp) => <span>${emp.salary.toLocaleString()}</span>,\n },\n {\n key: 'rating',\n header: 'Performance',\n cell: (emp) => {\n const tone =\n emp.performanceScore >= 4\n ? 'text-status-success-text'\n : emp.performanceScore >= 3\n ? 'text-status-warning-text'\n : 'text-status-error-text';\n return (\n <div className=\"flex items-center gap-1.5\">\n <span className={`font-semibold tabular-nums ${tone}`}>\n {emp.performanceScore.toFixed(1)}\n </span>\n <Star className={`h-3.5 w-3.5 fill-current ${tone}`} />\n </div>\n );\n },\n },\n {\n key: 'status',\n header: 'Status',\n cell: (emp) => (\n <Badge\n variant={\n emp.status === 'active'\n ? 'default'\n : emp.status === 'on_leave'\n ? 'secondary'\n : 'destructive'\n }\n className=\"text-[10px]\"\n >\n {emp.status.replace('_', ' ')}\n </Badge>\n ),\n },\n ];\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex items-center justify-between\">\n <div>\n <h1 className=\"text-2xl font-semibold\">\n <GradientText>Employee Directory</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">\n {employees.length} employees across {departments.length} departments\n </p>\n <PagePurpose className=\"mt-1\">\n Your complete employee directory — search, filter, and review everyone on the team along\n with their role, department, and status.\n </PagePurpose>\n </div>\n <Button size=\"sm\" onClick={() => form.openModal()}>\n <Plus className=\"h-4 w-4 mr-1\" />\n Add Employee\n </Button>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-4\">\n <GlassCard treatment=\"glass\" glow className=\"border-border-seam\">\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle className=\"text-sm font-medium\">Total</CardTitle>\n <Users className=\"h-4 w-4 text-text-secondary\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">{employees.length}</div>\n </CardContent>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"border-border-seam\">\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle className=\"text-sm font-medium\">Active</CardTitle>\n <UserCheck className=\"h-4 w-4 text-status-success-text\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">{activeCount}</div>\n </CardContent>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"border-border-seam\">\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle className=\"text-sm font-medium\">On Leave</CardTitle>\n <Users className=\"h-4 w-4 text-status-warning-text\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">{onLeaveCount}</div>\n </CardContent>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"border-border-seam\">\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle className=\"text-sm font-medium\">Avg Rating</CardTitle>\n <Star className=\"h-4 w-4 text-status-warning-text\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">{avgRating}</div>\n </CardContent>\n </GlassCard>\n </div>\n\n <div className=\"flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between\">\n <SegmentedTabs\n aria-label=\"Filter by status\"\n value={filterStatus}\n onChange={handleStatusChange}\n tabs={[\n { id: 'all', label: 'All', count: employees.length },\n { id: 'active', label: 'Active', count: activeCount },\n { id: 'on_leave', label: 'On Leave', count: onLeaveCount },\n ]}\n />\n <div className=\"flex flex-col gap-3 sm:flex-row\">\n <div className=\"relative flex-1 sm:max-w-sm\">\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 employees...\"\n className=\"pl-9\"\n value={search}\n onChange={(e) => handleSearchChange(e.target.value)}\n />\n </div>\n <Select value={filterDept} onValueChange={handleDeptChange}>\n <SelectTrigger className=\"w-full sm:w-56\" aria-label=\"Filter by department\">\n <SelectValue placeholder=\"All departments\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"all\">All departments</SelectItem>\n {departments.map((dept) => (\n <SelectItem key={dept} value={dept}>\n {dept}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n </div>\n\n <Card>\n <CardContent className=\"space-y-4 p-4\">\n <ResponsiveTable<Employee>\n data={pageItems}\n rowKey={(emp) => emp.id}\n columns={columns}\n onRowClick={(emp) => openProfile(emp)}\n rowActions={(emp) => (\n <>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-8 w-8 p-0\"\n aria-label={`Edit ${emp.firstName} ${emp.lastName}`}\n onClick={() => form.openModal(emp)}\n >\n <Pencil className=\"h-4 w-4\" />\n </Button>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-8 w-8 p-0\"\n aria-label={`Delete ${emp.firstName} ${emp.lastName}`}\n onClick={() => setDeleting(emp)}\n >\n <Trash2 className=\"h-4 w-4 text-status-error-text\" />\n </Button>\n </>\n )}\n emptyState={\n <div className=\"p-6\">\n <IllustratedEmptyState\n illustration=\"empty-people\"\n title=\"No employees match\"\n description=\"Try a different search term or department filter.\"\n />\n </div>\n }\n />\n {filtered.length > pageSize && (\n <Pagination\n page={page}\n pageSize={pageSize}\n totalItems={filtered.length}\n onPageChange={setPage}\n />\n )}\n </CardContent>\n </Card>\n\n <FormModal\n open={form.open}\n onOpenChange={form.onOpenChange}\n title={editing ? 'Edit employee' : 'Add employee'}\n description={\n editing\n ? 'Update this team member’s profile details.'\n : 'Add a new team member to your directory.'\n }\n fields={EMPLOYEE_FIELDS}\n initialValues={editing ? toFormValues(editing) : undefined}\n submitLabel={editing ? 'Save changes' : 'Add employee'}\n onSubmit={handleSubmit}\n />\n\n <ConfirmDialog\n open={deleting !== null}\n onOpenChange={(open) => {\n if (!open) {\n setDeleting(null);\n }\n }}\n title=\"Delete employee?\"\n description={\n deleting\n ? `${deleting.firstName} ${deleting.lastName} will be removed from the directory. This cannot be undone.`\n : undefined\n }\n confirmLabel=\"Delete\"\n destructive\n onConfirm={async () => {\n if (deleting) {\n await removeEmployee(deleting.id);\n setDeleting(null);\n }\n }}\n />\n </div>\n );\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { useNavigate as e } from "react-router-dom";
1
+ import { useNavigate as e } from "react-router";
2
2
  import { jsx as t, jsxs as n } from "react/jsx-runtime";
3
3
  import { Button as r, GradientText as i, IllustratedEmptyState as a, PagePurpose as o } from "@burdenoff/fe-libs/ui";
4
4
  //#region src/crewfoundry/pages/LearningAssessmentsPagePage.tsx
@@ -1 +1 @@
1
- {"version":3,"file":"LearningAssessmentsPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/LearningAssessmentsPagePage.tsx"],"sourcesContent":["import { Button, GradientText, IllustratedEmptyState, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { useNavigate } from 'react-router-dom';\n\nexport function LearningAssessmentsPagePage() {\n const navigate = useNavigate();\n\n return (\n <div className=\"space-y-6 p-6\">\n <div>\n <h1 className=\"text-2xl font-semibold\">\n <GradientText>Assessments</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">Quizzes and exams for your enrolled courses</p>\n <PagePurpose className=\"mt-1\">\n Create and review assessments that measure what your team has learned — use them to\n certify skills and identify knowledge gaps.\n </PagePurpose>\n </div>\n\n <IllustratedEmptyState\n illustration=\"empty-generic\"\n title=\"Assessments are coming soon\"\n description=\"Course assessments — question banks, timed attempts, scoring and pass thresholds — are being built. Once available, the quizzes tied to your enrolled courses will appear here.\"\n action={\n <Button variant=\"outline\" onClick={() => navigate('../learning/courses-page')}>\n Browse courses\n </Button>\n }\n />\n </div>\n );\n}\n"],"mappings":";;;;AAGA,SAAgB,IAA8B;CAC5C,IAAM,IAAW,GAAa;AAE9B,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,OAAD,EAAA,UAAA;GACE,kBAAC,MAAD;IAAI,WAAU;cACZ,kBAAC,GAAD,EAAA,UAAc,eAA0B,CAAA;IACrC,CAAA;GACL,kBAAC,KAAD;IAAG,WAAU;cAA8B;IAA+C,CAAA;GAC1F,kBAAC,GAAD;IAAa,WAAU;cAAO;IAGhB,CAAA;GACV,EAAA,CAAA,EAEN,kBAAC,GAAD;GACE,cAAa;GACb,OAAM;GACN,aAAY;GACZ,QACE,kBAAC,GAAD;IAAQ,SAAQ;IAAU,eAAe,EAAS,2BAA2B;cAAE;IAEtE,CAAA;GAEX,CAAA,CACE"}
1
+ {"version":3,"file":"LearningAssessmentsPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/LearningAssessmentsPagePage.tsx"],"sourcesContent":["import { Button, GradientText, IllustratedEmptyState, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { useNavigate } from 'react-router';\n\nexport function LearningAssessmentsPagePage() {\n const navigate = useNavigate();\n\n return (\n <div className=\"space-y-6 p-6\">\n <div>\n <h1 className=\"text-2xl font-semibold\">\n <GradientText>Assessments</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">Quizzes and exams for your enrolled courses</p>\n <PagePurpose className=\"mt-1\">\n Create and review assessments that measure what your team has learned — use them to\n certify skills and identify knowledge gaps.\n </PagePurpose>\n </div>\n\n <IllustratedEmptyState\n illustration=\"empty-generic\"\n title=\"Assessments are coming soon\"\n description=\"Course assessments — question banks, timed attempts, scoring and pass thresholds — are being built. Once available, the quizzes tied to your enrolled courses will appear here.\"\n action={\n <Button variant=\"outline\" onClick={() => navigate('../learning/courses-page')}>\n Browse courses\n </Button>\n }\n />\n </div>\n );\n}\n"],"mappings":";;;;AAGA,SAAgB,IAA8B;CAC5C,IAAM,IAAW,GAAa;AAE9B,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,OAAD,EAAA,UAAA;GACE,kBAAC,MAAD;IAAI,WAAU;cACZ,kBAAC,GAAD,EAAA,UAAc,eAA0B,CAAA;IACrC,CAAA;GACL,kBAAC,KAAD;IAAG,WAAU;cAA8B;IAA+C,CAAA;GAC1F,kBAAC,GAAD;IAAa,WAAU;cAAO;IAGhB,CAAA;GACV,EAAA,CAAA,EAEN,kBAAC,GAAD;GACE,cAAa;GACb,OAAM;GACN,aAAY;GACZ,QACE,kBAAC,GAAD;IAAQ,SAAQ;IAAU,eAAe,EAAS,2BAA2B;cAAE;IAEtE,CAAA;GAEX,CAAA,CACE"}
@@ -4,7 +4,7 @@ import "../components/viz/index.js";
4
4
  import { readAllCourseProgress as n } from "../utils/courseProgress.js";
5
5
  import { useMemo as r } from "react";
6
6
  import { GraduationCap as i } from "lucide-react";
7
- import { useNavigate as a } from "react-router-dom";
7
+ import { useNavigate as a } from "react-router";
8
8
  import { jsx as o, jsxs as s } from "react/jsx-runtime";
9
9
  import { Button as c, GradientText as l, IllustratedEmptyState as u, PagePurpose as d } from "@burdenoff/fe-libs/ui";
10
10
  import { useAuth as f } from "@burdenoff/fe-libs/shared/providers/shell";
@@ -1 +1 @@
1
- {"version":3,"file":"LearningCertificatesPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/LearningCertificatesPagePage.tsx"],"sourcesContent":["import { useMemo } from 'react';\nimport { Button, GradientText, IllustratedEmptyState, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { GraduationCap } from 'lucide-react';\nimport { useNavigate } from 'react-router-dom';\nimport { useAuth } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { useCrewFoundryStore } from '../store/crewfoundryStore';\nimport { CertificateCard } from '../components/viz';\nimport { readAllCourseProgress } from '../utils/courseProgress';\n\nfunction formatDate(iso?: string): string {\n if (!iso) return '—';\n const date = new Date(iso);\n if (Number.isNaN(date.getTime())) return '—';\n return date.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });\n}\n\nfunction downloadCertificate(\n course: string,\n recipient: string,\n dateLabel: string,\n id: string\n): void {\n if (typeof document === 'undefined') return;\n const esc = (s: string) =>\n s.replace(\n /[&<>\"]/g,\n (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;' })[c] ?? c\n );\n const html = `<!doctype html><html><head><meta charset=\"utf-8\"><title>Certificate — ${esc(course)}</title>\n<style>\n body{font-family:Georgia,'Times New Roman',serif;margin:0;padding:48px;text-align:center}\n .frame{border:6px double currentColor;padding:56px 40px;max-width:760px;margin:0 auto}\n .kicker{letter-spacing:.3em;text-transform:uppercase;font-size:12px;opacity:.7}\n h1{font-size:34px;margin:24px 0 8px}\n .recipient{font-size:26px;margin:20px 0}\n .rule{width:200px;border:0;border-top:1px solid currentColor;margin:24px auto;opacity:.4}\n .meta{font-size:13px;opacity:.7;margin-top:32px}\n</style></head><body>\n <div class=\"frame\">\n <div class=\"kicker\">CrewFoundry · Certificate of Completion</div>\n <h1>${esc(course)}</h1>\n <div class=\"kicker\">Awarded to</div>\n <div class=\"recipient\">${esc(recipient)}</div>\n <hr class=\"rule\" />\n <div class=\"meta\">Issued ${esc(dateLabel)} &nbsp;·&nbsp; Credential ID: ${esc(id)}</div>\n </div>\n</body></html>`;\n const blob = new Blob([html], { type: 'text/html' });\n const url = URL.createObjectURL(blob);\n const anchor = document.createElement('a');\n anchor.href = url;\n anchor.download = `crewfoundry-certificate-${id}.html`;\n document.body.appendChild(anchor);\n anchor.click();\n anchor.remove();\n URL.revokeObjectURL(url);\n}\n\nexport function LearningCertificatesPagePage() {\n const navigate = useNavigate();\n const auth = useAuth();\n const { courses } = useCrewFoundryStore();\n const recipient = auth.user?.name?.trim() || 'CrewFoundry Learner';\n\n const earned = useMemo(() => {\n const progressMap = readAllCourseProgress();\n return courses\n .map((course) => {\n const entry = progressMap[course.id];\n const completedModules = entry?.completed.length ?? 0;\n const totalModules = Math.max(course.modules, 1);\n const isComplete = completedModules > 0 && completedModules >= totalModules;\n return { course, isComplete, completedAt: entry?.completedAt };\n })\n .filter((item) => item.isComplete)\n .sort((a, b) => (b.completedAt ?? '').localeCompare(a.completedAt ?? ''));\n }, [courses]);\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div>\n <h1 className=\"text-2xl font-semibold\">\n <GradientText>Certificates</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">\n Proof of completed training and validated skills\n </p>\n <PagePurpose className=\"mt-1\">\n View and download the certificates you&apos;ve earned — proof of completed training and\n validated skills.\n </PagePurpose>\n </div>\n <Button variant=\"outline\" onClick={() => navigate('../learning/my-learning-page')}>\n <GraduationCap className=\"mr-1 h-4 w-4\" />\n My Learning\n </Button>\n </div>\n\n {earned.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-generic\"\n title=\"No certificates yet\"\n description=\"Certificates are issued when you complete a course. Finish a course from the player and every credential you earn will appear here with its issue date and a download link.\"\n action={\n <Button variant=\"outline\" onClick={() => navigate('../learning/courses-page')}>\n Browse courses\n </Button>\n }\n />\n ) : (\n <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-3\">\n {earned.map(({ course, completedAt }) => {\n const credentialId = `CF-${course.id.slice(-6).toUpperCase()}`;\n const dateLabel = formatDate(completedAt);\n return (\n <CertificateCard\n key={course.id}\n title={course.title}\n recipient={recipient}\n date={dateLabel}\n issuer=\"CrewFoundry\"\n credentialId={credentialId}\n onDownload={() =>\n downloadCertificate(course.title, recipient, dateLabel, credentialId)\n }\n />\n );\n })}\n </div>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;AASA,SAAS,EAAW,GAAsB;AACxC,KAAI,CAAC,EAAK,QAAO;CACjB,IAAM,IAAO,IAAI,KAAK,EAAI;AAE1B,QADI,OAAO,MAAM,EAAK,SAAS,CAAC,GAAS,MAClC,EAAK,mBAAmB,KAAA,GAAW;EAAE,MAAM;EAAW,OAAO;EAAS,KAAK;EAAW,CAAC;;AAGhG,SAAS,EACP,GACA,GACA,GACA,GACM;AACN,KAAI,OAAO,WAAa,IAAa;CACrC,IAAM,KAAO,MACX,EAAE,QACA,YACC,OAAO;EAAE,KAAK;EAAS,KAAK;EAAQ,KAAK;EAAQ,MAAK;EAAU,EAAE,MAAM,EAC1E,EACG,IAAO,yEAAyE,EAAI,EAAO,CAAC;;;;;;;;;;;;UAY1F,EAAI,EAAO,CAAC;;6BAEO,EAAI,EAAU,CAAC;;+BAEb,EAAI,EAAU,CAAC,gCAAgC,EAAI,EAAG,CAAC;;iBAG9E,IAAO,IAAI,KAAK,CAAC,EAAK,EAAE,EAAE,MAAM,aAAa,CAAC,EAC9C,IAAM,IAAI,gBAAgB,EAAK,EAC/B,IAAS,SAAS,cAAc,IAAI;AAM1C,CALA,EAAO,OAAO,GACd,EAAO,WAAW,2BAA2B,EAAG,QAChD,SAAS,KAAK,YAAY,EAAO,EACjC,EAAO,OAAO,EACd,EAAO,QAAQ,EACf,IAAI,gBAAgB,EAAI;;AAG1B,SAAgB,IAA+B;CAC7C,IAAM,IAAW,GAAa,EACxB,IAAO,GAAS,EAChB,EAAE,eAAY,GAAqB,EACnC,IAAY,EAAK,MAAM,MAAM,MAAM,IAAI,uBAEvC,IAAS,QAAc;EAC3B,IAAM,IAAc,GAAuB;AAC3C,SAAO,EACJ,KAAK,MAAW;GACf,IAAM,IAAQ,EAAY,EAAO,KAC3B,IAAmB,GAAO,UAAU,UAAU,GAC9C,IAAe,KAAK,IAAI,EAAO,SAAS,EAAE;AAEhD,UAAO;IAAE;IAAQ,YADE,IAAmB,KAAK,KAAoB;IAClC,aAAa,GAAO;IAAa;IAC9D,CACD,QAAQ,MAAS,EAAK,WAAW,CACjC,MAAM,GAAG,OAAO,EAAE,eAAe,IAAI,cAAc,EAAE,eAAe,GAAG,CAAC;IAC1E,CAAC,EAAQ,CAAC;AAEb,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD,EAAA,UAAA;IACE,kBAAC,MAAD;KAAI,WAAU;eACZ,kBAAC,GAAD,EAAA,UAAc,gBAA2B,CAAA;KACtC,CAAA;IACL,kBAAC,KAAD;KAAG,WAAU;eAA8B;KAEvC,CAAA;IACJ,kBAAC,GAAD;KAAa,WAAU;eAAO;KAGhB,CAAA;IACV,EAAA,CAAA,EACN,kBAAC,GAAD;IAAQ,SAAQ;IAAU,eAAe,EAAS,+BAA+B;cAAjF,CACE,kBAAC,GAAD,EAAe,WAAU,gBAAiB,CAAA,EAAA,cAEnC;MACL;MAEL,EAAO,WAAW,IACjB,kBAAC,GAAD;GACE,cAAa;GACb,OAAM;GACN,aAAY;GACZ,QACE,kBAAC,GAAD;IAAQ,SAAQ;IAAU,eAAe,EAAS,2BAA2B;cAAE;IAEtE,CAAA;GAEX,CAAA,GAEF,kBAAC,OAAD;GAAK,WAAU;aACZ,EAAO,KAAK,EAAE,WAAQ,qBAAkB;IACvC,IAAM,IAAe,MAAM,EAAO,GAAG,MAAM,GAAG,CAAC,aAAa,IACtD,IAAY,EAAW,EAAY;AACzC,WACE,kBAAC,GAAD;KAEE,OAAO,EAAO;KACH;KACX,MAAM;KACN,QAAO;KACO;KACd,kBACE,EAAoB,EAAO,OAAO,GAAW,GAAW,EAAa;KAEvE,EATK,EAAO,GASZ;KAEJ;GACE,CAAA,CAEJ"}
1
+ {"version":3,"file":"LearningCertificatesPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/LearningCertificatesPagePage.tsx"],"sourcesContent":["import { useMemo } from 'react';\nimport { Button, GradientText, IllustratedEmptyState, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { GraduationCap } from 'lucide-react';\nimport { useNavigate } from 'react-router';\nimport { useAuth } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { useCrewFoundryStore } from '../store/crewfoundryStore';\nimport { CertificateCard } from '../components/viz';\nimport { readAllCourseProgress } from '../utils/courseProgress';\n\nfunction formatDate(iso?: string): string {\n if (!iso) return '—';\n const date = new Date(iso);\n if (Number.isNaN(date.getTime())) return '—';\n return date.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });\n}\n\nfunction downloadCertificate(\n course: string,\n recipient: string,\n dateLabel: string,\n id: string\n): void {\n if (typeof document === 'undefined') return;\n const esc = (s: string) =>\n s.replace(\n /[&<>\"]/g,\n (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;' })[c] ?? c\n );\n const html = `<!doctype html><html><head><meta charset=\"utf-8\"><title>Certificate — ${esc(course)}</title>\n<style>\n body{font-family:Georgia,'Times New Roman',serif;margin:0;padding:48px;text-align:center}\n .frame{border:6px double currentColor;padding:56px 40px;max-width:760px;margin:0 auto}\n .kicker{letter-spacing:.3em;text-transform:uppercase;font-size:12px;opacity:.7}\n h1{font-size:34px;margin:24px 0 8px}\n .recipient{font-size:26px;margin:20px 0}\n .rule{width:200px;border:0;border-top:1px solid currentColor;margin:24px auto;opacity:.4}\n .meta{font-size:13px;opacity:.7;margin-top:32px}\n</style></head><body>\n <div class=\"frame\">\n <div class=\"kicker\">CrewFoundry · Certificate of Completion</div>\n <h1>${esc(course)}</h1>\n <div class=\"kicker\">Awarded to</div>\n <div class=\"recipient\">${esc(recipient)}</div>\n <hr class=\"rule\" />\n <div class=\"meta\">Issued ${esc(dateLabel)} &nbsp;·&nbsp; Credential ID: ${esc(id)}</div>\n </div>\n</body></html>`;\n const blob = new Blob([html], { type: 'text/html' });\n const url = URL.createObjectURL(blob);\n const anchor = document.createElement('a');\n anchor.href = url;\n anchor.download = `crewfoundry-certificate-${id}.html`;\n document.body.appendChild(anchor);\n anchor.click();\n anchor.remove();\n URL.revokeObjectURL(url);\n}\n\nexport function LearningCertificatesPagePage() {\n const navigate = useNavigate();\n const auth = useAuth();\n const { courses } = useCrewFoundryStore();\n const recipient = auth.user?.name?.trim() || 'CrewFoundry Learner';\n\n const earned = useMemo(() => {\n const progressMap = readAllCourseProgress();\n return courses\n .map((course) => {\n const entry = progressMap[course.id];\n const completedModules = entry?.completed.length ?? 0;\n const totalModules = Math.max(course.modules, 1);\n const isComplete = completedModules > 0 && completedModules >= totalModules;\n return { course, isComplete, completedAt: entry?.completedAt };\n })\n .filter((item) => item.isComplete)\n .sort((a, b) => (b.completedAt ?? '').localeCompare(a.completedAt ?? ''));\n }, [courses]);\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div>\n <h1 className=\"text-2xl font-semibold\">\n <GradientText>Certificates</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">\n Proof of completed training and validated skills\n </p>\n <PagePurpose className=\"mt-1\">\n View and download the certificates you&apos;ve earned — proof of completed training and\n validated skills.\n </PagePurpose>\n </div>\n <Button variant=\"outline\" onClick={() => navigate('../learning/my-learning-page')}>\n <GraduationCap className=\"mr-1 h-4 w-4\" />\n My Learning\n </Button>\n </div>\n\n {earned.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-generic\"\n title=\"No certificates yet\"\n description=\"Certificates are issued when you complete a course. Finish a course from the player and every credential you earn will appear here with its issue date and a download link.\"\n action={\n <Button variant=\"outline\" onClick={() => navigate('../learning/courses-page')}>\n Browse courses\n </Button>\n }\n />\n ) : (\n <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-3\">\n {earned.map(({ course, completedAt }) => {\n const credentialId = `CF-${course.id.slice(-6).toUpperCase()}`;\n const dateLabel = formatDate(completedAt);\n return (\n <CertificateCard\n key={course.id}\n title={course.title}\n recipient={recipient}\n date={dateLabel}\n issuer=\"CrewFoundry\"\n credentialId={credentialId}\n onDownload={() =>\n downloadCertificate(course.title, recipient, dateLabel, credentialId)\n }\n />\n );\n })}\n </div>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;AASA,SAAS,EAAW,GAAsB;AACxC,KAAI,CAAC,EAAK,QAAO;CACjB,IAAM,IAAO,IAAI,KAAK,EAAI;AAE1B,QADI,OAAO,MAAM,EAAK,SAAS,CAAC,GAAS,MAClC,EAAK,mBAAmB,KAAA,GAAW;EAAE,MAAM;EAAW,OAAO;EAAS,KAAK;EAAW,CAAC;;AAGhG,SAAS,EACP,GACA,GACA,GACA,GACM;AACN,KAAI,OAAO,WAAa,IAAa;CACrC,IAAM,KAAO,MACX,EAAE,QACA,YACC,OAAO;EAAE,KAAK;EAAS,KAAK;EAAQ,KAAK;EAAQ,MAAK;EAAU,EAAE,MAAM,EAC1E,EACG,IAAO,yEAAyE,EAAI,EAAO,CAAC;;;;;;;;;;;;UAY1F,EAAI,EAAO,CAAC;;6BAEO,EAAI,EAAU,CAAC;;+BAEb,EAAI,EAAU,CAAC,gCAAgC,EAAI,EAAG,CAAC;;iBAG9E,IAAO,IAAI,KAAK,CAAC,EAAK,EAAE,EAAE,MAAM,aAAa,CAAC,EAC9C,IAAM,IAAI,gBAAgB,EAAK,EAC/B,IAAS,SAAS,cAAc,IAAI;AAM1C,CALA,EAAO,OAAO,GACd,EAAO,WAAW,2BAA2B,EAAG,QAChD,SAAS,KAAK,YAAY,EAAO,EACjC,EAAO,OAAO,EACd,EAAO,QAAQ,EACf,IAAI,gBAAgB,EAAI;;AAG1B,SAAgB,IAA+B;CAC7C,IAAM,IAAW,GAAa,EACxB,IAAO,GAAS,EAChB,EAAE,eAAY,GAAqB,EACnC,IAAY,EAAK,MAAM,MAAM,MAAM,IAAI,uBAEvC,IAAS,QAAc;EAC3B,IAAM,IAAc,GAAuB;AAC3C,SAAO,EACJ,KAAK,MAAW;GACf,IAAM,IAAQ,EAAY,EAAO,KAC3B,IAAmB,GAAO,UAAU,UAAU,GAC9C,IAAe,KAAK,IAAI,EAAO,SAAS,EAAE;AAEhD,UAAO;IAAE;IAAQ,YADE,IAAmB,KAAK,KAAoB;IAClC,aAAa,GAAO;IAAa;IAC9D,CACD,QAAQ,MAAS,EAAK,WAAW,CACjC,MAAM,GAAG,OAAO,EAAE,eAAe,IAAI,cAAc,EAAE,eAAe,GAAG,CAAC;IAC1E,CAAC,EAAQ,CAAC;AAEb,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD,EAAA,UAAA;IACE,kBAAC,MAAD;KAAI,WAAU;eACZ,kBAAC,GAAD,EAAA,UAAc,gBAA2B,CAAA;KACtC,CAAA;IACL,kBAAC,KAAD;KAAG,WAAU;eAA8B;KAEvC,CAAA;IACJ,kBAAC,GAAD;KAAa,WAAU;eAAO;KAGhB,CAAA;IACV,EAAA,CAAA,EACN,kBAAC,GAAD;IAAQ,SAAQ;IAAU,eAAe,EAAS,+BAA+B;cAAjF,CACE,kBAAC,GAAD,EAAe,WAAU,gBAAiB,CAAA,EAAA,cAEnC;MACL;MAEL,EAAO,WAAW,IACjB,kBAAC,GAAD;GACE,cAAa;GACb,OAAM;GACN,aAAY;GACZ,QACE,kBAAC,GAAD;IAAQ,SAAQ;IAAU,eAAe,EAAS,2BAA2B;cAAE;IAEtE,CAAA;GAEX,CAAA,GAEF,kBAAC,OAAD;GAAK,WAAU;aACZ,EAAO,KAAK,EAAE,WAAQ,qBAAkB;IACvC,IAAM,IAAe,MAAM,EAAO,GAAG,MAAM,GAAG,CAAC,aAAa,IACtD,IAAY,EAAW,EAAY;AACzC,WACE,kBAAC,GAAD;KAEE,OAAO,EAAO;KACH;KACX,MAAM;KACN,QAAO;KACO;KACd,kBACE,EAAoB,EAAO,OAAO,GAAW,GAAW,EAAa;KAEvE,EATK,EAAO,GASZ;KAEJ;GACE,CAAA,CAEJ"}
@@ -2,7 +2,7 @@ import { useCrewFoundryStore as e } from "../store/crewfoundryStore.js";
2
2
  import { formatEnumLabel as t } from "../utils/formatEnumLabel.js";
3
3
  import { useState as n } from "react";
4
4
  import { BookOpen as r, Clock as i, GraduationCap as a, Play as o, Star as s, Users as c } from "lucide-react";
5
- import { useNavigate as l, useSearchParams as u } from "react-router-dom";
5
+ import { useNavigate as l, useSearchParams as u } from "react-router";
6
6
  import { jsx as d, jsxs as f } from "react/jsx-runtime";
7
7
  import { Badge as p, Button as m, Card as h, CardContent as g, CardHeader as _, CardTitle as v, EmphasisPanel as y, GradientText as b, IllustratedEmptyState as x, NextSteps as S, PagePurpose as C, Spinner as w } from "@burdenoff/fe-libs/ui";
8
8
  import { useGlobalUi as T } from "@burdenoff/fe-libs/shared/providers/shell";
@@ -1 +1 @@
1
- {"version":3,"file":"LearningCourseDetailsPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/LearningCourseDetailsPagePage.tsx"],"sourcesContent":["import {\n Badge,\n Button,\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n EmphasisPanel,\n GradientText,\n IllustratedEmptyState,\n NextSteps,\n PagePurpose,\n Spinner,\n} from '@burdenoff/fe-libs/ui';\nimport { BookOpen, Clock, GraduationCap, Play, Star, Users } from 'lucide-react';\nimport { useNavigate, useSearchParams } from 'react-router-dom';\nimport { useState } from 'react';\nimport { useGlobalUi } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { useCrewFoundryStore } from '../store/crewfoundryStore';\nimport { formatEnumLabel } from '../utils/formatEnumLabel';\n\nexport function LearningCourseDetailsPagePage() {\n const navigate = useNavigate();\n const [searchParams] = useSearchParams();\n const { courses, enrollCourse, initialized } = useCrewFoundryStore();\n const { showToast } = useGlobalUi();\n const [enrolling, setEnrolling] = useState(false);\n const courseId = searchParams.get('id');\n const course = courseId ? courses.find((c) => c.id === courseId) : courses[0];\n if (!initialized && !course) {\n return (\n <div className=\"flex min-h-[60vh] items-center justify-center p-6\">\n <Spinner size=\"lg\" />\n </div>\n );\n }\n\n if (!course)\n return (\n <div className=\"p-6\">\n <IllustratedEmptyState\n illustration=\"empty-search\"\n title=\"Course not found\"\n description=\"This course is no longer available.\"\n />\n </div>\n );\n const completionRate =\n course.enrolled > 0 ? Math.round((course.completed / course.enrolled) * 100) : 0;\n\n const handleStart = async () => {\n setEnrolling(true);\n try {\n await enrollCourse(course.id);\n showToast('Enrolled — your course is ready', 'success');\n navigate(`../learning/course-learning-page?id=${encodeURIComponent(course.id)}`);\n } catch (error) {\n showToast(error instanceof Error ? error.message : 'Unable to enroll in course', 'error');\n } finally {\n setEnrolling(false);\n }\n };\n\n return (\n <div className=\"space-y-6 p-6 max-w-4xl\">\n <div className=\"flex items-center justify-between\">\n <div>\n <div className=\"flex items-center gap-2 mb-1\">\n <h1 className=\"text-2xl font-semibold\">\n <GradientText>{course.title}</GradientText>\n </h1>\n <Badge\n variant={\n course.level === 'beginner'\n ? 'secondary'\n : course.level === 'intermediate'\n ? 'default'\n : 'destructive'\n }\n >\n {formatEnumLabel(course.level)}\n </Badge>\n </div>\n <p className=\"text-sm text-text-secondary\">\n by {course.instructor} · {course.category}\n </p>\n <PagePurpose className=\"mt-2\">\n Everything you need to decide on and start this course — what it covers, who it&apos;s\n for, and how others have done — so you can enroll with confidence and build the right\n skills.\n </PagePurpose>\n </div>\n <Button onClick={handleStart} disabled={enrolling}>\n <Play className=\"h-4 w-4 mr-1\" />\n {enrolling ? 'Enrolling…' : 'Start Course'}\n </Button>\n </div>\n\n <NextSteps\n storageKey=\"crewfoundry-course-details\"\n steps={[\n {\n id: 'start',\n label: 'Start the first module',\n description: 'Begin learning and track your progress as you go.',\n icon: <Play className=\"h-4 w-4\" />,\n onClick: () =>\n navigate(`../learning/course-learning-page?id=${encodeURIComponent(course.id)}`),\n },\n {\n id: 'my-learning',\n label: 'See all your learning',\n description: 'Review everything you are currently enrolled in.',\n icon: <GraduationCap className=\"h-4 w-4\" />,\n onClick: () => navigate('../learning/my-learning-page'),\n },\n {\n id: 'browse',\n label: 'Browse more courses',\n description: 'Find related training to round out your skills.',\n icon: <BookOpen className=\"h-4 w-4\" />,\n onClick: () => navigate('../learning/courses-page'),\n },\n ]}\n />\n\n <div className=\"grid gap-4 md:grid-cols-4\">\n <Card>\n <CardContent className=\"p-4 text-center\">\n <Clock className=\"h-5 w-5 mx-auto text-text-secondary mb-1\" />\n <p className=\"text-lg font-bold\">{course.duration}</p>\n <p className=\"text-xs text-text-secondary\">Duration</p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"p-4 text-center\">\n <BookOpen className=\"h-5 w-5 mx-auto text-text-secondary mb-1\" />\n <p className=\"text-lg font-bold\">{course.modules}</p>\n <p className=\"text-xs text-text-secondary\">Modules</p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"p-4 text-center\">\n <Users className=\"h-5 w-5 mx-auto text-text-secondary mb-1\" />\n <p className=\"text-lg font-bold\">{course.enrolled}</p>\n <p className=\"text-xs text-text-secondary\">Enrolled</p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"p-4 text-center\">\n <Star className=\"h-5 w-5 mx-auto text-status-warning-text mb-1\" />\n <p className=\"text-lg font-bold\">{course.rating}/5</p>\n <p className=\"text-xs text-text-secondary\">Rating</p>\n </CardContent>\n </Card>\n </div>\n <EmphasisPanel>\n <div className=\"mb-2 text-lg font-semibold\">About this Course</div>\n <p className=\"text-sm text-text-secondary\">{course.description}</p>\n <div className=\"flex flex-wrap gap-1 mt-3\">\n {course.skills.map((s) => (\n <Badge key={s} variant=\"outline\">\n {s}\n </Badge>\n ))}\n </div>\n </EmphasisPanel>\n <Card>\n <CardHeader>\n <CardTitle>Completion Statistics</CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"space-y-2\">\n <div className=\"flex justify-between text-sm\">\n <span>\n {course.completed} of {course.enrolled} students completed\n </span>\n <span className=\"font-medium\">{completionRate}%</span>\n </div>\n <div className=\"h-3 bg-bg-sunken rounded overflow-hidden\">\n <div\n className=\"h-3 bg-status-success-text rounded\"\n style={{ width: `${completionRate}%` }}\n />\n </div>\n </div>\n </CardContent>\n </Card>\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"LearningCourseDetailsPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/LearningCourseDetailsPagePage.tsx"],"sourcesContent":["import {\n Badge,\n Button,\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n EmphasisPanel,\n GradientText,\n IllustratedEmptyState,\n NextSteps,\n PagePurpose,\n Spinner,\n} from '@burdenoff/fe-libs/ui';\nimport { BookOpen, Clock, GraduationCap, Play, Star, Users } from 'lucide-react';\nimport { useNavigate, useSearchParams } from 'react-router';\nimport { useState } from 'react';\nimport { useGlobalUi } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { useCrewFoundryStore } from '../store/crewfoundryStore';\nimport { formatEnumLabel } from '../utils/formatEnumLabel';\n\nexport function LearningCourseDetailsPagePage() {\n const navigate = useNavigate();\n const [searchParams] = useSearchParams();\n const { courses, enrollCourse, initialized } = useCrewFoundryStore();\n const { showToast } = useGlobalUi();\n const [enrolling, setEnrolling] = useState(false);\n const courseId = searchParams.get('id');\n const course = courseId ? courses.find((c) => c.id === courseId) : courses[0];\n if (!initialized && !course) {\n return (\n <div className=\"flex min-h-[60vh] items-center justify-center p-6\">\n <Spinner size=\"lg\" />\n </div>\n );\n }\n\n if (!course)\n return (\n <div className=\"p-6\">\n <IllustratedEmptyState\n illustration=\"empty-search\"\n title=\"Course not found\"\n description=\"This course is no longer available.\"\n />\n </div>\n );\n const completionRate =\n course.enrolled > 0 ? Math.round((course.completed / course.enrolled) * 100) : 0;\n\n const handleStart = async () => {\n setEnrolling(true);\n try {\n await enrollCourse(course.id);\n showToast('Enrolled — your course is ready', 'success');\n navigate(`../learning/course-learning-page?id=${encodeURIComponent(course.id)}`);\n } catch (error) {\n showToast(error instanceof Error ? error.message : 'Unable to enroll in course', 'error');\n } finally {\n setEnrolling(false);\n }\n };\n\n return (\n <div className=\"space-y-6 p-6 max-w-4xl\">\n <div className=\"flex items-center justify-between\">\n <div>\n <div className=\"flex items-center gap-2 mb-1\">\n <h1 className=\"text-2xl font-semibold\">\n <GradientText>{course.title}</GradientText>\n </h1>\n <Badge\n variant={\n course.level === 'beginner'\n ? 'secondary'\n : course.level === 'intermediate'\n ? 'default'\n : 'destructive'\n }\n >\n {formatEnumLabel(course.level)}\n </Badge>\n </div>\n <p className=\"text-sm text-text-secondary\">\n by {course.instructor} · {course.category}\n </p>\n <PagePurpose className=\"mt-2\">\n Everything you need to decide on and start this course — what it covers, who it&apos;s\n for, and how others have done — so you can enroll with confidence and build the right\n skills.\n </PagePurpose>\n </div>\n <Button onClick={handleStart} disabled={enrolling}>\n <Play className=\"h-4 w-4 mr-1\" />\n {enrolling ? 'Enrolling…' : 'Start Course'}\n </Button>\n </div>\n\n <NextSteps\n storageKey=\"crewfoundry-course-details\"\n steps={[\n {\n id: 'start',\n label: 'Start the first module',\n description: 'Begin learning and track your progress as you go.',\n icon: <Play className=\"h-4 w-4\" />,\n onClick: () =>\n navigate(`../learning/course-learning-page?id=${encodeURIComponent(course.id)}`),\n },\n {\n id: 'my-learning',\n label: 'See all your learning',\n description: 'Review everything you are currently enrolled in.',\n icon: <GraduationCap className=\"h-4 w-4\" />,\n onClick: () => navigate('../learning/my-learning-page'),\n },\n {\n id: 'browse',\n label: 'Browse more courses',\n description: 'Find related training to round out your skills.',\n icon: <BookOpen className=\"h-4 w-4\" />,\n onClick: () => navigate('../learning/courses-page'),\n },\n ]}\n />\n\n <div className=\"grid gap-4 md:grid-cols-4\">\n <Card>\n <CardContent className=\"p-4 text-center\">\n <Clock className=\"h-5 w-5 mx-auto text-text-secondary mb-1\" />\n <p className=\"text-lg font-bold\">{course.duration}</p>\n <p className=\"text-xs text-text-secondary\">Duration</p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"p-4 text-center\">\n <BookOpen className=\"h-5 w-5 mx-auto text-text-secondary mb-1\" />\n <p className=\"text-lg font-bold\">{course.modules}</p>\n <p className=\"text-xs text-text-secondary\">Modules</p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"p-4 text-center\">\n <Users className=\"h-5 w-5 mx-auto text-text-secondary mb-1\" />\n <p className=\"text-lg font-bold\">{course.enrolled}</p>\n <p className=\"text-xs text-text-secondary\">Enrolled</p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"p-4 text-center\">\n <Star className=\"h-5 w-5 mx-auto text-status-warning-text mb-1\" />\n <p className=\"text-lg font-bold\">{course.rating}/5</p>\n <p className=\"text-xs text-text-secondary\">Rating</p>\n </CardContent>\n </Card>\n </div>\n <EmphasisPanel>\n <div className=\"mb-2 text-lg font-semibold\">About this Course</div>\n <p className=\"text-sm text-text-secondary\">{course.description}</p>\n <div className=\"flex flex-wrap gap-1 mt-3\">\n {course.skills.map((s) => (\n <Badge key={s} variant=\"outline\">\n {s}\n </Badge>\n ))}\n </div>\n </EmphasisPanel>\n <Card>\n <CardHeader>\n <CardTitle>Completion Statistics</CardTitle>\n </CardHeader>\n <CardContent>\n <div className=\"space-y-2\">\n <div className=\"flex justify-between text-sm\">\n <span>\n {course.completed} of {course.enrolled} students completed\n </span>\n <span className=\"font-medium\">{completionRate}%</span>\n </div>\n <div className=\"h-3 bg-bg-sunken rounded overflow-hidden\">\n <div\n className=\"h-3 bg-status-success-text rounded\"\n style={{ width: `${completionRate}%` }}\n />\n </div>\n </div>\n </CardContent>\n </Card>\n </div>\n );\n}\n"],"mappings":"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"}
@@ -5,7 +5,7 @@ import "../components/viz/index.js";
5
5
  import { progressPercent as r, readCourseProgress as i, writeCourseProgress as a } from "../utils/courseProgress.js";
6
6
  import { useMemo as o, useState as s } from "react";
7
7
  import { Check as c, ChevronLeft as l, ChevronRight as u, CircleDashed as d, FileText as f, Play as p } from "lucide-react";
8
- import { useNavigate as m, useSearchParams as h } from "react-router-dom";
8
+ import { useNavigate as m, useSearchParams as h } from "react-router";
9
9
  import { Fragment as g, jsx as _, jsxs as v } from "react/jsx-runtime";
10
10
  import { Badge as y, Button as b, Card as x, CardContent as S, GlassCard as C, IllustratedEmptyState as w, Spinner as T, Textarea as E } from "@burdenoff/fe-libs/ui";
11
11
  import { useGlobalUi as D } from "@burdenoff/fe-libs/shared/providers/shell";
@@ -1 +1 @@
1
- {"version":3,"file":"LearningCourseLearningPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/LearningCourseLearningPagePage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport {\n Badge,\n Button,\n Card,\n CardContent,\n GlassCard,\n IllustratedEmptyState,\n Spinner,\n Textarea,\n} from '@burdenoff/fe-libs/ui';\nimport { useGlobalUi } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { Check, ChevronLeft, ChevronRight, CircleDashed, FileText, Play } from 'lucide-react';\nimport { useNavigate, useSearchParams } from 'react-router-dom';\nimport { useCrewFoundryStore } from '../store/crewfoundryStore';\nimport { SegmentedTabs } from '../components/viz';\nimport { formatEnumLabel } from '../utils/formatEnumLabel';\nimport { readCourseProgress, writeCourseProgress, progressPercent } from '../utils/courseProgress';\n\nconst ENROLLED_KEY = 'crewfoundry:course-enrolled:v1';\nconst NOTES_KEY = 'crewfoundry:course-notes:v1';\n\nfunction hasEnrolledLocally(courseId: string): boolean {\n if (typeof window === 'undefined') return false;\n try {\n const raw = window.localStorage.getItem(ENROLLED_KEY);\n const ids: unknown = raw ? JSON.parse(raw) : [];\n return Array.isArray(ids) && ids.includes(courseId);\n } catch {\n return false;\n }\n}\n\nfunction markEnrolledLocally(courseId: string): void {\n if (typeof window === 'undefined') return;\n try {\n const raw = window.localStorage.getItem(ENROLLED_KEY);\n const ids: unknown = raw ? JSON.parse(raw) : [];\n const list = Array.isArray(ids) ? ids.filter((x): x is string => typeof x === 'string') : [];\n if (!list.includes(courseId)) {\n list.push(courseId);\n window.localStorage.setItem(ENROLLED_KEY, JSON.stringify(list));\n }\n } catch {\n /* best-effort */\n }\n}\n\nfunction readNotes(courseId: string): string {\n if (typeof window === 'undefined') return '';\n try {\n const raw = window.localStorage.getItem(NOTES_KEY);\n const map: unknown = raw ? JSON.parse(raw) : {};\n if (map && typeof map === 'object') {\n const value = (map as Record<string, unknown>)[courseId];\n return typeof value === 'string' ? value : '';\n }\n return '';\n } catch {\n return '';\n }\n}\n\nfunction writeNotes(courseId: string, notes: string): void {\n if (typeof window === 'undefined') return;\n try {\n const raw = window.localStorage.getItem(NOTES_KEY);\n const parsed: unknown = raw ? JSON.parse(raw) : {};\n const map: Record<string, string> =\n parsed && typeof parsed === 'object' ? (parsed as Record<string, string>) : {};\n map[courseId] = notes;\n window.localStorage.setItem(NOTES_KEY, JSON.stringify(map));\n } catch {\n /* best-effort */\n }\n}\n\ntype PlayerTab = 'overview' | 'resources' | 'notes';\n\nexport function LearningCourseLearningPagePage() {\n const navigate = useNavigate();\n const [searchParams] = useSearchParams();\n const { courses, enrollCourse, initialized } = useCrewFoundryStore();\n const { showToast } = useGlobalUi();\n\n const courseId = searchParams.get('id');\n const course = courseId ? courses.find((c) => c.id === courseId) : courses[0];\n\n const totalModules = course ? Math.max(course.modules, 1) : 1;\n\n const [completed, setCompleted] = useState<number[]>(() =>\n course ? readCourseProgress(course.id).completed : []\n );\n const [current, setCurrent] = useState<number>(() => {\n if (!course) return 0;\n const done = new Set(readCourseProgress(course.id).completed);\n for (let i = 0; i < Math.max(course.modules, 1); i += 1) {\n if (!done.has(i)) return i;\n }\n return 0;\n });\n const [tab, setTab] = useState<PlayerTab>('overview');\n const [notes, setNotes] = useState<string>(() => (course ? readNotes(course.id) : ''));\n\n const completedSet = useMemo(() => new Set(completed), [completed]);\n const pct = progressPercent({ completed }, totalModules);\n\n if (!initialized && !course) {\n return (\n <div className=\"flex min-h-[60vh] items-center justify-center p-6\">\n <Spinner size=\"lg\" />\n </div>\n );\n }\n\n if (!course) {\n return (\n <div className=\"p-6\">\n <IllustratedEmptyState\n illustration=\"empty-search\"\n title=\"Course not found\"\n description=\"This course is no longer available.\"\n action={\n <Button variant=\"outline\" onClick={() => navigate('../learning/courses-page')}>\n Browse courses\n </Button>\n }\n />\n </div>\n );\n }\n\n const persist = (next: number[]) => {\n setCompleted(next);\n writeCourseProgress(course.id, next, totalModules);\n };\n\n const ensureEnrolled = async () => {\n if (hasEnrolledLocally(course.id)) return;\n markEnrolledLocally(course.id);\n try {\n await enrollCourse(course.id);\n } catch {\n /* enrolment is a best-effort side effect of starting the course */\n }\n };\n\n const handleMarkComplete = async () => {\n if (!completedSet.has(current)) {\n const wasEmpty = completed.length === 0;\n const next = [...completed, current];\n persist(next);\n if (wasEmpty) {\n await ensureEnrolled();\n }\n if (next.length >= totalModules) {\n showToast(`You completed ${course.title}`, 'success');\n }\n }\n if (current < totalModules - 1) {\n setCurrent(current + 1);\n setTab('overview');\n }\n };\n\n const goTo = (index: number) => {\n setCurrent(index);\n setTab('overview');\n };\n\n const goPrev = () => current > 0 && goTo(current - 1);\n const goNext = () => current < totalModules - 1 && goTo(current + 1);\n\n const currentComplete = completedSet.has(current);\n const moduleTitle = `Module ${current + 1}`;\n\n return (\n <div className=\"space-y-5 p-6\">\n <div className=\"flex flex-wrap items-center gap-x-2 gap-y-1 text-sm text-text-secondary\">\n <button\n type=\"button\"\n onClick={() => navigate('../learning/courses-page')}\n className=\"hover:text-text-primary\"\n >\n Learning\n </button>\n <ChevronRight className=\"h-3.5 w-3.5\" />\n <button\n type=\"button\"\n onClick={() =>\n navigate(`../learning/course-details-page?id=${encodeURIComponent(course.id)}`)\n }\n className=\"hover:text-text-primary\"\n >\n Courses\n </button>\n <ChevronRight className=\"h-3.5 w-3.5\" />\n <span className=\"font-medium text-text-primary\">{course.title}</span>\n </div>\n\n <div className=\"grid grid-cols-1 gap-5 lg:grid-cols-[minmax(0,20rem)_minmax(0,1fr)]\">\n {/* Course content sidebar */}\n <Card className=\"lg:sticky lg:top-4 lg:max-h-[calc(100vh-2rem)] lg:overflow-y-auto\">\n <CardContent className=\"p-4\">\n <div className=\"mb-3 flex items-baseline justify-between gap-2\">\n <h2 className=\"text-sm font-semibold\">Course content</h2>\n <span className=\"text-xs font-medium text-text-secondary tabular-nums\">\n {pct}% complete\n </span>\n </div>\n <div className=\"mb-4 h-2 overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-action-primary-bg transition-[width] duration-500\"\n style={{ width: `${pct}%` }}\n />\n </div>\n <ol className=\"space-y-1\">\n {Array.from({ length: totalModules }, (_, i) => {\n const done = completedSet.has(i);\n const isCurrent = i === current;\n return (\n <li key={i}>\n <button\n type=\"button\"\n onClick={() => goTo(i)}\n aria-current={isCurrent ? 'step' : undefined}\n className={`flex w-full items-center gap-3 rounded-md px-2.5 py-2 text-left text-sm transition-colors ${\n isCurrent\n ? 'bg-action-primary-bg/10 font-medium text-text-primary'\n : 'text-text-secondary hover:bg-bg-sunken hover:text-text-primary'\n }`}\n >\n <span\n className={`flex h-5 w-5 shrink-0 items-center justify-center rounded-full border ${\n done\n ? 'border-transparent bg-status-success-bg text-status-success-text'\n : isCurrent\n ? 'border-action-primary-bg text-action-primary-bg'\n : 'border-border-default text-transparent'\n }`}\n >\n {done ? (\n <Check className=\"h-3 w-3\" />\n ) : (\n <CircleDashed className=\"h-3 w-3\" />\n )}\n </span>\n <span className=\"truncate\">{`Module ${i + 1}`}</span>\n </button>\n </li>\n );\n })}\n </ol>\n </CardContent>\n </Card>\n\n {/* Main pane */}\n <div className=\"space-y-5\">\n <GlassCard treatment=\"plain\" className=\"overflow-hidden\">\n <button\n type=\"button\"\n onClick={() => void handleMarkComplete()}\n aria-label={currentComplete ? 'Next module' : 'Mark this module complete'}\n className=\"group relative flex aspect-video w-full items-center justify-center outline-none focus-visible:shadow-[var(--shadow-focus)]\"\n style={{\n background:\n 'linear-gradient(135deg, color-mix(in srgb, var(--color-accent-primary) 24%, var(--color-bg-sunken)), var(--color-bg-sunken))',\n }}\n >\n <span className=\"absolute left-4 top-4\">\n <Badge variant=\"secondary\" className=\"text-[10px]\">\n {moduleTitle}\n </Badge>\n </span>\n <span className=\"flex h-16 w-16 items-center justify-center rounded-full bg-action-primary-bg text-action-primary-text shadow-elevation-2 transition-transform group-hover:scale-105\">\n {currentComplete ? (\n <Check className=\"h-7 w-7\" />\n ) : (\n <Play className=\"ml-1 h-7 w-7\" />\n )}\n </span>\n </button>\n <CardContent className=\"space-y-4 p-5\">\n <div>\n <div className=\"flex flex-wrap items-center gap-2\">\n <h1 className=\"text-xl font-semibold\">{moduleTitle}</h1>\n <Badge\n variant={\n course.level === 'beginner'\n ? 'secondary'\n : course.level === 'intermediate'\n ? 'default'\n : 'destructive'\n }\n className=\"text-[10px]\"\n >\n {formatEnumLabel(course.level)}\n </Badge>\n {currentComplete && (\n <span className=\"inline-flex items-center gap-1 text-xs font-medium text-status-success-text\">\n <Check className=\"h-3.5 w-3.5\" /> Completed\n </span>\n )}\n </div>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {course.title} · by {course.instructor}\n </p>\n </div>\n\n <SegmentedTabs\n variant=\"underline\"\n aria-label=\"Lesson detail\"\n value={tab}\n onChange={(id) => setTab(id as PlayerTab)}\n tabs={[\n { id: 'overview', label: 'Overview' },\n { id: 'resources', label: 'Resources' },\n { id: 'notes', label: 'Notes' },\n ]}\n />\n\n {tab === 'overview' && (\n <div className=\"space-y-3\">\n <p className=\"text-sm text-text-secondary\">{course.description}</p>\n {course.skills.length > 0 && (\n <div className=\"flex flex-wrap gap-1\">\n {course.skills.map((s) => (\n <Badge key={s} variant=\"outline\" className=\"text-[10px]\">\n {s}\n </Badge>\n ))}\n </div>\n )}\n </div>\n )}\n\n {tab === 'resources' && (\n <div className=\"rounded-lg border border-dashed border-border-default p-6 text-center\">\n <FileText className=\"mx-auto mb-2 h-6 w-6 text-text-muted\" />\n <p className=\"text-sm font-medium\">No downloadable resources yet</p>\n <p className=\"text-xs text-text-secondary\">\n Slides and attachments for this module will appear here when the instructor adds\n them.\n </p>\n </div>\n )}\n\n {tab === 'notes' && (\n <div className=\"space-y-2\">\n <Textarea\n value={notes}\n onChange={(e) => {\n setNotes(e.target.value);\n writeNotes(course.id, e.target.value);\n }}\n placeholder=\"Jot down your notes for this course. They are saved to this device automatically.\"\n rows={5}\n />\n <p className=\"text-xs text-text-muted\">\n Notes are saved automatically on this device.\n </p>\n </div>\n )}\n </CardContent>\n </GlassCard>\n\n {/* Footer controls */}\n <Card>\n <CardContent className=\"space-y-4 p-4\">\n <div className=\"h-2 overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-status-success-bg transition-[width] duration-500\"\n style={{ width: `${pct}%` }}\n />\n </div>\n <div className=\"flex items-center justify-between gap-2\">\n <Button variant=\"outline\" onClick={goPrev} disabled={current === 0}>\n <ChevronLeft className=\"mr-1 h-4 w-4\" />\n Previous\n </Button>\n <div className=\"flex items-center gap-2\">\n <Button onClick={() => void handleMarkComplete()}>\n {currentComplete ? (\n current < totalModules - 1 ? (\n 'Next module'\n ) : (\n <>\n <Check className=\"mr-1 h-4 w-4\" /> Completed\n </>\n )\n ) : (\n <>\n <Check className=\"mr-1 h-4 w-4\" /> Mark Complete\n </>\n )}\n </Button>\n <Button variant=\"outline\" onClick={goNext} disabled={current >= totalModules - 1}>\n Next\n <ChevronRight className=\"ml-1 h-4 w-4\" />\n </Button>\n </div>\n </div>\n </CardContent>\n </Card>\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"LearningCourseLearningPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/LearningCourseLearningPagePage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport {\n Badge,\n Button,\n Card,\n CardContent,\n GlassCard,\n IllustratedEmptyState,\n Spinner,\n Textarea,\n} from '@burdenoff/fe-libs/ui';\nimport { useGlobalUi } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { Check, ChevronLeft, ChevronRight, CircleDashed, FileText, Play } from 'lucide-react';\nimport { useNavigate, useSearchParams } from 'react-router';\nimport { useCrewFoundryStore } from '../store/crewfoundryStore';\nimport { SegmentedTabs } from '../components/viz';\nimport { formatEnumLabel } from '../utils/formatEnumLabel';\nimport { readCourseProgress, writeCourseProgress, progressPercent } from '../utils/courseProgress';\n\nconst ENROLLED_KEY = 'crewfoundry:course-enrolled:v1';\nconst NOTES_KEY = 'crewfoundry:course-notes:v1';\n\nfunction hasEnrolledLocally(courseId: string): boolean {\n if (typeof window === 'undefined') return false;\n try {\n const raw = window.localStorage.getItem(ENROLLED_KEY);\n const ids: unknown = raw ? JSON.parse(raw) : [];\n return Array.isArray(ids) && ids.includes(courseId);\n } catch {\n return false;\n }\n}\n\nfunction markEnrolledLocally(courseId: string): void {\n if (typeof window === 'undefined') return;\n try {\n const raw = window.localStorage.getItem(ENROLLED_KEY);\n const ids: unknown = raw ? JSON.parse(raw) : [];\n const list = Array.isArray(ids) ? ids.filter((x): x is string => typeof x === 'string') : [];\n if (!list.includes(courseId)) {\n list.push(courseId);\n window.localStorage.setItem(ENROLLED_KEY, JSON.stringify(list));\n }\n } catch {\n /* best-effort */\n }\n}\n\nfunction readNotes(courseId: string): string {\n if (typeof window === 'undefined') return '';\n try {\n const raw = window.localStorage.getItem(NOTES_KEY);\n const map: unknown = raw ? JSON.parse(raw) : {};\n if (map && typeof map === 'object') {\n const value = (map as Record<string, unknown>)[courseId];\n return typeof value === 'string' ? value : '';\n }\n return '';\n } catch {\n return '';\n }\n}\n\nfunction writeNotes(courseId: string, notes: string): void {\n if (typeof window === 'undefined') return;\n try {\n const raw = window.localStorage.getItem(NOTES_KEY);\n const parsed: unknown = raw ? JSON.parse(raw) : {};\n const map: Record<string, string> =\n parsed && typeof parsed === 'object' ? (parsed as Record<string, string>) : {};\n map[courseId] = notes;\n window.localStorage.setItem(NOTES_KEY, JSON.stringify(map));\n } catch {\n /* best-effort */\n }\n}\n\ntype PlayerTab = 'overview' | 'resources' | 'notes';\n\nexport function LearningCourseLearningPagePage() {\n const navigate = useNavigate();\n const [searchParams] = useSearchParams();\n const { courses, enrollCourse, initialized } = useCrewFoundryStore();\n const { showToast } = useGlobalUi();\n\n const courseId = searchParams.get('id');\n const course = courseId ? courses.find((c) => c.id === courseId) : courses[0];\n\n const totalModules = course ? Math.max(course.modules, 1) : 1;\n\n const [completed, setCompleted] = useState<number[]>(() =>\n course ? readCourseProgress(course.id).completed : []\n );\n const [current, setCurrent] = useState<number>(() => {\n if (!course) return 0;\n const done = new Set(readCourseProgress(course.id).completed);\n for (let i = 0; i < Math.max(course.modules, 1); i += 1) {\n if (!done.has(i)) return i;\n }\n return 0;\n });\n const [tab, setTab] = useState<PlayerTab>('overview');\n const [notes, setNotes] = useState<string>(() => (course ? readNotes(course.id) : ''));\n\n const completedSet = useMemo(() => new Set(completed), [completed]);\n const pct = progressPercent({ completed }, totalModules);\n\n if (!initialized && !course) {\n return (\n <div className=\"flex min-h-[60vh] items-center justify-center p-6\">\n <Spinner size=\"lg\" />\n </div>\n );\n }\n\n if (!course) {\n return (\n <div className=\"p-6\">\n <IllustratedEmptyState\n illustration=\"empty-search\"\n title=\"Course not found\"\n description=\"This course is no longer available.\"\n action={\n <Button variant=\"outline\" onClick={() => navigate('../learning/courses-page')}>\n Browse courses\n </Button>\n }\n />\n </div>\n );\n }\n\n const persist = (next: number[]) => {\n setCompleted(next);\n writeCourseProgress(course.id, next, totalModules);\n };\n\n const ensureEnrolled = async () => {\n if (hasEnrolledLocally(course.id)) return;\n markEnrolledLocally(course.id);\n try {\n await enrollCourse(course.id);\n } catch {\n /* enrolment is a best-effort side effect of starting the course */\n }\n };\n\n const handleMarkComplete = async () => {\n if (!completedSet.has(current)) {\n const wasEmpty = completed.length === 0;\n const next = [...completed, current];\n persist(next);\n if (wasEmpty) {\n await ensureEnrolled();\n }\n if (next.length >= totalModules) {\n showToast(`You completed ${course.title}`, 'success');\n }\n }\n if (current < totalModules - 1) {\n setCurrent(current + 1);\n setTab('overview');\n }\n };\n\n const goTo = (index: number) => {\n setCurrent(index);\n setTab('overview');\n };\n\n const goPrev = () => current > 0 && goTo(current - 1);\n const goNext = () => current < totalModules - 1 && goTo(current + 1);\n\n const currentComplete = completedSet.has(current);\n const moduleTitle = `Module ${current + 1}`;\n\n return (\n <div className=\"space-y-5 p-6\">\n <div className=\"flex flex-wrap items-center gap-x-2 gap-y-1 text-sm text-text-secondary\">\n <button\n type=\"button\"\n onClick={() => navigate('../learning/courses-page')}\n className=\"hover:text-text-primary\"\n >\n Learning\n </button>\n <ChevronRight className=\"h-3.5 w-3.5\" />\n <button\n type=\"button\"\n onClick={() =>\n navigate(`../learning/course-details-page?id=${encodeURIComponent(course.id)}`)\n }\n className=\"hover:text-text-primary\"\n >\n Courses\n </button>\n <ChevronRight className=\"h-3.5 w-3.5\" />\n <span className=\"font-medium text-text-primary\">{course.title}</span>\n </div>\n\n <div className=\"grid grid-cols-1 gap-5 lg:grid-cols-[minmax(0,20rem)_minmax(0,1fr)]\">\n {/* Course content sidebar */}\n <Card className=\"lg:sticky lg:top-4 lg:max-h-[calc(100vh-2rem)] lg:overflow-y-auto\">\n <CardContent className=\"p-4\">\n <div className=\"mb-3 flex items-baseline justify-between gap-2\">\n <h2 className=\"text-sm font-semibold\">Course content</h2>\n <span className=\"text-xs font-medium text-text-secondary tabular-nums\">\n {pct}% complete\n </span>\n </div>\n <div className=\"mb-4 h-2 overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-action-primary-bg transition-[width] duration-500\"\n style={{ width: `${pct}%` }}\n />\n </div>\n <ol className=\"space-y-1\">\n {Array.from({ length: totalModules }, (_, i) => {\n const done = completedSet.has(i);\n const isCurrent = i === current;\n return (\n <li key={i}>\n <button\n type=\"button\"\n onClick={() => goTo(i)}\n aria-current={isCurrent ? 'step' : undefined}\n className={`flex w-full items-center gap-3 rounded-md px-2.5 py-2 text-left text-sm transition-colors ${\n isCurrent\n ? 'bg-action-primary-bg/10 font-medium text-text-primary'\n : 'text-text-secondary hover:bg-bg-sunken hover:text-text-primary'\n }`}\n >\n <span\n className={`flex h-5 w-5 shrink-0 items-center justify-center rounded-full border ${\n done\n ? 'border-transparent bg-status-success-bg text-status-success-text'\n : isCurrent\n ? 'border-action-primary-bg text-action-primary-bg'\n : 'border-border-default text-transparent'\n }`}\n >\n {done ? (\n <Check className=\"h-3 w-3\" />\n ) : (\n <CircleDashed className=\"h-3 w-3\" />\n )}\n </span>\n <span className=\"truncate\">{`Module ${i + 1}`}</span>\n </button>\n </li>\n );\n })}\n </ol>\n </CardContent>\n </Card>\n\n {/* Main pane */}\n <div className=\"space-y-5\">\n <GlassCard treatment=\"plain\" className=\"overflow-hidden\">\n <button\n type=\"button\"\n onClick={() => void handleMarkComplete()}\n aria-label={currentComplete ? 'Next module' : 'Mark this module complete'}\n className=\"group relative flex aspect-video w-full items-center justify-center outline-none focus-visible:shadow-[var(--shadow-focus)]\"\n style={{\n background:\n 'linear-gradient(135deg, color-mix(in srgb, var(--color-accent-primary) 24%, var(--color-bg-sunken)), var(--color-bg-sunken))',\n }}\n >\n <span className=\"absolute left-4 top-4\">\n <Badge variant=\"secondary\" className=\"text-[10px]\">\n {moduleTitle}\n </Badge>\n </span>\n <span className=\"flex h-16 w-16 items-center justify-center rounded-full bg-action-primary-bg text-action-primary-text shadow-elevation-2 transition-transform group-hover:scale-105\">\n {currentComplete ? (\n <Check className=\"h-7 w-7\" />\n ) : (\n <Play className=\"ml-1 h-7 w-7\" />\n )}\n </span>\n </button>\n <CardContent className=\"space-y-4 p-5\">\n <div>\n <div className=\"flex flex-wrap items-center gap-2\">\n <h1 className=\"text-xl font-semibold\">{moduleTitle}</h1>\n <Badge\n variant={\n course.level === 'beginner'\n ? 'secondary'\n : course.level === 'intermediate'\n ? 'default'\n : 'destructive'\n }\n className=\"text-[10px]\"\n >\n {formatEnumLabel(course.level)}\n </Badge>\n {currentComplete && (\n <span className=\"inline-flex items-center gap-1 text-xs font-medium text-status-success-text\">\n <Check className=\"h-3.5 w-3.5\" /> Completed\n </span>\n )}\n </div>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {course.title} · by {course.instructor}\n </p>\n </div>\n\n <SegmentedTabs\n variant=\"underline\"\n aria-label=\"Lesson detail\"\n value={tab}\n onChange={(id) => setTab(id as PlayerTab)}\n tabs={[\n { id: 'overview', label: 'Overview' },\n { id: 'resources', label: 'Resources' },\n { id: 'notes', label: 'Notes' },\n ]}\n />\n\n {tab === 'overview' && (\n <div className=\"space-y-3\">\n <p className=\"text-sm text-text-secondary\">{course.description}</p>\n {course.skills.length > 0 && (\n <div className=\"flex flex-wrap gap-1\">\n {course.skills.map((s) => (\n <Badge key={s} variant=\"outline\" className=\"text-[10px]\">\n {s}\n </Badge>\n ))}\n </div>\n )}\n </div>\n )}\n\n {tab === 'resources' && (\n <div className=\"rounded-lg border border-dashed border-border-default p-6 text-center\">\n <FileText className=\"mx-auto mb-2 h-6 w-6 text-text-muted\" />\n <p className=\"text-sm font-medium\">No downloadable resources yet</p>\n <p className=\"text-xs text-text-secondary\">\n Slides and attachments for this module will appear here when the instructor adds\n them.\n </p>\n </div>\n )}\n\n {tab === 'notes' && (\n <div className=\"space-y-2\">\n <Textarea\n value={notes}\n onChange={(e) => {\n setNotes(e.target.value);\n writeNotes(course.id, e.target.value);\n }}\n placeholder=\"Jot down your notes for this course. They are saved to this device automatically.\"\n rows={5}\n />\n <p className=\"text-xs text-text-muted\">\n Notes are saved automatically on this device.\n </p>\n </div>\n )}\n </CardContent>\n </GlassCard>\n\n {/* Footer controls */}\n <Card>\n <CardContent className=\"space-y-4 p-4\">\n <div className=\"h-2 overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-status-success-bg transition-[width] duration-500\"\n style={{ width: `${pct}%` }}\n />\n </div>\n <div className=\"flex items-center justify-between gap-2\">\n <Button variant=\"outline\" onClick={goPrev} disabled={current === 0}>\n <ChevronLeft className=\"mr-1 h-4 w-4\" />\n Previous\n </Button>\n <div className=\"flex items-center gap-2\">\n <Button onClick={() => void handleMarkComplete()}>\n {currentComplete ? (\n current < totalModules - 1 ? (\n 'Next module'\n ) : (\n <>\n <Check className=\"mr-1 h-4 w-4\" /> Completed\n </>\n )\n ) : (\n <>\n <Check className=\"mr-1 h-4 w-4\" /> Mark Complete\n </>\n )}\n </Button>\n <Button variant=\"outline\" onClick={goNext} disabled={current >= totalModules - 1}>\n Next\n <ChevronRight className=\"ml-1 h-4 w-4\" />\n </Button>\n </div>\n </div>\n </CardContent>\n </Card>\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
@@ -7,7 +7,7 @@ import { FormModal as i, useFormModal as a } from "../components/forms/FormModal
7
7
  import { readCourseProgress as o } from "../utils/courseProgress.js";
8
8
  import { useMemo as s, useState as c } from "react";
9
9
  import { BarChart3 as l, Briefcase as u, Code2 as d, GraduationCap as f, Megaphone as p, Palette as m, Play as h, Plus as g, Search as _, ShieldCheck as v, Users as y } from "lucide-react";
10
- import { useNavigate as b } from "react-router-dom";
10
+ import { useNavigate as b } from "react-router";
11
11
  import { jsx as x, jsxs as S } from "react/jsx-runtime";
12
12
  import { Badge as C, Button as w, CardContent as T, CardHeader as E, GlassCard as D, GradientText as O, IllustratedEmptyState as k, Input as A, PagePurpose as j, Pagination as M } from "@burdenoff/fe-libs/ui";
13
13
  import { usePagination as N } from "@burdenoff/fe-libs/shared/hooks";
@@ -1 +1 @@
1
- {"version":3,"file":"LearningCoursesPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/LearningCoursesPagePage.tsx"],"sourcesContent":["import { useMemo, useState, type ComponentType } from 'react';\nimport {\n Badge,\n Button,\n CardContent,\n CardHeader,\n GlassCard,\n GradientText,\n IllustratedEmptyState,\n Input,\n PagePurpose,\n Pagination,\n} from '@burdenoff/fe-libs/ui';\nimport { usePagination } from '@burdenoff/fe-libs/shared/hooks';\nimport { useGlobalUi } from '@burdenoff/fe-libs/shared/providers/shell';\nimport {\n BarChart3,\n Briefcase,\n Code2,\n GraduationCap,\n Megaphone,\n Palette,\n Play,\n Plus,\n Search,\n ShieldCheck,\n Users,\n} from 'lucide-react';\nimport { useNavigate } from 'react-router-dom';\nimport { useCrewFoundryStore, type LearningCourse } from '../store/crewfoundryStore';\nimport {\n FormModal,\n useFormModal,\n type FieldConfig,\n type FormValues,\n} from '../components/forms/FormModal';\nimport { chartPalette, RatingStars } from '../components/viz';\nimport { formatEnumLabel } from '../utils/formatEnumLabel';\nimport { readCourseProgress } from '../utils/courseProgress';\n\nconst COURSE_FIELDS: FieldConfig[] = [\n { name: 'title', label: 'Course Title', type: 'text', required: true, colSpan: 2 },\n { name: 'category', label: 'Category', type: 'text', required: true },\n { name: 'instructor', label: 'Instructor', type: 'text', required: true },\n {\n name: 'duration',\n label: 'Duration',\n type: 'text',\n required: true,\n placeholder: 'e.g. 6 weeks',\n },\n { name: 'modules', label: 'Modules', type: 'number', required: true, placeholder: 'e.g. 8' },\n {\n name: 'level',\n label: 'Level',\n type: 'select',\n required: true,\n options: [\n { label: 'Beginner', value: 'beginner' },\n { label: 'Intermediate', value: 'intermediate' },\n { label: 'Advanced', value: 'advanced' },\n ],\n },\n {\n name: 'status',\n label: 'Status',\n type: 'select',\n required: true,\n options: [\n { label: 'Active', value: 'active' },\n { label: 'Draft', value: 'draft' },\n { label: 'Archived', value: 'archived' },\n ],\n },\n { name: 'description', label: 'Description', type: 'textarea', required: true, colSpan: 2 },\n {\n name: 'skills',\n label: 'Skills',\n type: 'tags',\n placeholder: 'Add a skill and press Enter',\n colSpan: 2,\n },\n];\n\ntype IconType = ComponentType<{ className?: string }>;\n\n/** Deterministic tokenized accent for a category (never a raw color). */\nfunction categoryAccent(category: string): string {\n const palette = chartPalette(8);\n let hash = 0;\n for (let i = 0; i < category.length; i += 1) {\n hash = (hash * 31 + category.charCodeAt(i)) >>> 0;\n }\n return palette[hash % palette.length];\n}\n\nfunction categoryIcon(category: string): IconType {\n const c = category.toLowerCase();\n if (\n c.includes('engineer') ||\n c.includes('develop') ||\n c.includes('program') ||\n c.includes('tech')\n )\n return Code2;\n if (c.includes('design') || c.includes('ux') || c.includes('ui')) return Palette;\n if (c.includes('lead') || c.includes('manage') || c.includes('people') || c.includes('hr'))\n return Users;\n if (c.includes('compliance') || c.includes('security') || c.includes('legal')) return ShieldCheck;\n if (c.includes('data') || c.includes('analyt')) return BarChart3;\n if (c.includes('market') || c.includes('sales')) return Megaphone;\n if (c.includes('business') || c.includes('finance') || c.includes('operations')) return Briefcase;\n return GraduationCap;\n}\n\nfunction levelVariant(level: LearningCourse['level']): 'secondary' | 'default' | 'destructive' {\n return level === 'beginner' ? 'secondary' : level === 'intermediate' ? 'default' : 'destructive';\n}\n\nexport function LearningCoursesPagePage() {\n const { courses, addCourse, enrollCourse } = useCrewFoundryStore();\n const { showToast } = useGlobalUi();\n const navigate = useNavigate();\n const [search, setSearch] = useState('');\n const [category, setCategory] = useState('all');\n const [enrollingId, setEnrollingId] = useState<string | null>(null);\n const createModal = useFormModal();\n const { page, pageSize, offset, setPage } = usePagination({ initialPageSize: 12 });\n\n const categories = useMemo(() => {\n const seen = new Map<string, number>();\n for (const course of courses) {\n const key = course.category.trim() || 'Uncategorized';\n seen.set(key, (seen.get(key) ?? 0) + 1);\n }\n return [...seen.entries()].sort((a, b) => b[1] - a[1]);\n }, [courses]);\n\n const filtered = courses.filter((c) => {\n const matchesCategory =\n category === 'all' || (c.category.trim() || 'Uncategorized') === category;\n const matchesSearch = `${c.title} ${c.category} ${c.instructor} ${c.skills.join(' ')}`\n .toLowerCase()\n .includes(search.toLowerCase());\n return matchesCategory && matchesSearch;\n });\n const paged = filtered.slice(offset, offset + pageSize);\n\n const openCourse = (id: string) =>\n navigate(`../learning/course-details-page?id=${encodeURIComponent(id)}`);\n const openPlayer = (id: string) =>\n navigate(`../learning/course-learning-page?id=${encodeURIComponent(id)}`);\n\n const handleEnroll = async (course: LearningCourse) => {\n setEnrollingId(course.id);\n try {\n await enrollCourse(course.id);\n showToast(`Enrolled in ${course.title}`, 'success');\n openPlayer(course.id);\n } catch (error) {\n showToast(error instanceof Error ? error.message : 'Unable to enroll in course', 'error');\n } finally {\n setEnrollingId(null);\n }\n };\n\n const handleCreate = async (values: FormValues) => {\n await addCourse({\n id: `course-${Date.now()}`,\n title: String(values.title ?? ''),\n category: String(values.category ?? ''),\n instructor: String(values.instructor ?? ''),\n duration: String(values.duration ?? ''),\n level: String(values.level ?? 'beginner') as LearningCourse['level'],\n enrolled: 0,\n completed: 0,\n rating: 0,\n status: String(values.status ?? 'draft') as LearningCourse['status'],\n description: String(values.description ?? ''),\n modules: typeof values.modules === 'number' ? values.modules : Number(values.modules) || 0,\n skills: Array.isArray(values.skills) ? values.skills : [],\n });\n };\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div>\n <h1 className=\"text-2xl font-semibold\">\n <GradientText>Course Catalog</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">\n {courses.length} courses · {categories.length} categories\n </p>\n <PagePurpose className=\"mt-1\">\n Browse the course catalog — find training to grow your team&apos;s skills and enroll in\n the programs that fit your goals.\n </PagePurpose>\n </div>\n <Button onClick={() => createModal.openModal()}>\n <Plus className=\"mr-1 h-4 w-4\" />\n Create Course\n </Button>\n </div>\n\n <div className=\"flex flex-col gap-3\">\n <div className=\"relative max-w-md\">\n <Search className=\"absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-text-secondary\" />\n <Input\n placeholder=\"Search courses, skills, or instructors...\"\n className=\"pl-9\"\n value={search}\n onChange={(e) => {\n setSearch(e.target.value);\n setPage(1);\n }}\n />\n </div>\n <div className=\"flex flex-wrap gap-2\">\n <button\n type=\"button\"\n onClick={() => {\n setCategory('all');\n setPage(1);\n }}\n className={`rounded-full border px-3 py-1 text-xs font-medium transition-colors ${\n category === 'all'\n ? 'border-transparent bg-action-primary-bg text-action-primary-text'\n : 'border-border-subtle bg-bg-surface text-text-secondary hover:text-text-primary'\n }`}\n >\n All\n </button>\n {categories.map(([name, count]) => (\n <button\n key={name}\n type=\"button\"\n onClick={() => {\n setCategory(name);\n setPage(1);\n }}\n className={`inline-flex items-center gap-1.5 rounded-full border px-3 py-1 text-xs font-medium transition-colors ${\n category === name\n ? 'border-transparent bg-action-primary-bg text-action-primary-text'\n : 'border-border-subtle bg-bg-surface text-text-secondary hover:text-text-primary'\n }`}\n >\n {name}\n <span className=\"tabular-nums opacity-70\">{count}</span>\n </button>\n ))}\n </div>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-3\">\n {filtered.length === 0 ? (\n <div className=\"col-span-full\">\n <IllustratedEmptyState\n illustration=\"empty-search\"\n title=\"No courses found\"\n description=\"Try a different search term or category to find training.\"\n />\n </div>\n ) : (\n paged.map((course) => {\n const accent = categoryAccent(course.category);\n const Icon = categoryIcon(course.category);\n const progress = readCourseProgress(course.id);\n const totalModules = Math.max(course.modules, 1);\n const pct =\n progress.completed.length > 0\n ? Math.min(100, Math.round((progress.completed.length / totalModules) * 100))\n : 0;\n const started = pct > 0;\n return (\n <GlassCard\n key={course.id}\n treatment=\"plain\"\n className=\"group flex flex-col overflow-hidden transition-shadow hover:shadow-md\"\n >\n <button\n type=\"button\"\n aria-label={`View ${course.title}`}\n onClick={() => openCourse(course.id)}\n className=\"relative flex h-24 w-full items-center justify-between px-5 text-left outline-none focus-visible:shadow-[var(--shadow-focus)]\"\n style={{\n background: `linear-gradient(135deg, color-mix(in srgb, ${accent} 28%, var(--color-bg-surface)), color-mix(in srgb, ${accent} 8%, var(--color-bg-surface)))`,\n }}\n >\n <span\n className=\"flex h-11 w-11 items-center justify-center rounded-xl bg-bg-surface/70 shadow-elevation-1\"\n style={{ color: accent }}\n >\n <Icon className=\"h-6 w-6\" />\n </span>\n <Badge variant={levelVariant(course.level)} className=\"text-[10px]\">\n {formatEnumLabel(course.level)}\n </Badge>\n </button>\n <CardHeader className=\"pb-2\">\n <Badge variant=\"outline\" className=\"w-fit text-[10px]\">\n {course.category}\n </Badge>\n <button\n type=\"button\"\n onClick={() => openCourse(course.id)}\n className=\"mt-2 text-left text-base font-semibold outline-none hover:text-action-primary-bg focus-visible:text-action-primary-bg\"\n >\n {course.title}\n </button>\n <p className=\"text-xs text-text-secondary\">\n {course.instructor} · {course.duration} · {course.modules} modules\n </p>\n </CardHeader>\n <CardContent className=\"flex flex-1 flex-col gap-3\">\n <p className=\"line-clamp-2 text-sm text-text-secondary\">{course.description}</p>\n {course.skills.length > 0 && (\n <div className=\"flex flex-wrap gap-1\">\n {course.skills.slice(0, 3).map((s) => (\n <Badge key={s} variant=\"outline\" className=\"text-[9px]\">\n {s}\n </Badge>\n ))}\n </div>\n )}\n <div className=\"mt-auto space-y-3\">\n <div className=\"flex items-center justify-between text-xs text-text-secondary\">\n <RatingStars value={course.rating} size={13} showValue />\n <span className=\"flex items-center gap-1\">\n <Users className=\"h-3 w-3\" />\n {course.enrolled} enrolled\n </span>\n </div>\n {started && (\n <div className=\"space-y-1\">\n <div className=\"flex justify-between text-[11px] font-medium\">\n <span className=\"text-text-secondary\">{pct}% complete</span>\n </div>\n <div className=\"h-1.5 overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-action-primary-bg\"\n style={{ width: `${pct}%` }}\n />\n </div>\n </div>\n )}\n <Button\n className=\"w-full\"\n variant={started ? 'outline' : 'default'}\n disabled={enrollingId === course.id}\n onClick={() => (started ? openPlayer(course.id) : void handleEnroll(course))}\n >\n <Play className=\"mr-1 h-4 w-4\" />\n {enrollingId === course.id ? 'Enrolling…' : started ? 'Continue' : 'Enroll'}\n </Button>\n </div>\n </CardContent>\n </GlassCard>\n );\n })\n )}\n </div>\n\n {filtered.length > pageSize && (\n <Pagination\n page={page}\n pageSize={pageSize}\n totalItems={filtered.length}\n onPageChange={setPage}\n />\n )}\n\n <FormModal\n open={createModal.open}\n onOpenChange={createModal.onOpenChange}\n title=\"Create Course\"\n description=\"Add a new course to the catalog.\"\n fields={COURSE_FIELDS}\n submitLabel=\"Create Course\"\n onSubmit={handleCreate}\n />\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"LearningCoursesPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/LearningCoursesPagePage.tsx"],"sourcesContent":["import { useMemo, useState, type ComponentType } from 'react';\nimport {\n Badge,\n Button,\n CardContent,\n CardHeader,\n GlassCard,\n GradientText,\n IllustratedEmptyState,\n Input,\n PagePurpose,\n Pagination,\n} from '@burdenoff/fe-libs/ui';\nimport { usePagination } from '@burdenoff/fe-libs/shared/hooks';\nimport { useGlobalUi } from '@burdenoff/fe-libs/shared/providers/shell';\nimport {\n BarChart3,\n Briefcase,\n Code2,\n GraduationCap,\n Megaphone,\n Palette,\n Play,\n Plus,\n Search,\n ShieldCheck,\n Users,\n} from 'lucide-react';\nimport { useNavigate } from 'react-router';\nimport { useCrewFoundryStore, type LearningCourse } from '../store/crewfoundryStore';\nimport {\n FormModal,\n useFormModal,\n type FieldConfig,\n type FormValues,\n} from '../components/forms/FormModal';\nimport { chartPalette, RatingStars } from '../components/viz';\nimport { formatEnumLabel } from '../utils/formatEnumLabel';\nimport { readCourseProgress } from '../utils/courseProgress';\n\nconst COURSE_FIELDS: FieldConfig[] = [\n { name: 'title', label: 'Course Title', type: 'text', required: true, colSpan: 2 },\n { name: 'category', label: 'Category', type: 'text', required: true },\n { name: 'instructor', label: 'Instructor', type: 'text', required: true },\n {\n name: 'duration',\n label: 'Duration',\n type: 'text',\n required: true,\n placeholder: 'e.g. 6 weeks',\n },\n { name: 'modules', label: 'Modules', type: 'number', required: true, placeholder: 'e.g. 8' },\n {\n name: 'level',\n label: 'Level',\n type: 'select',\n required: true,\n options: [\n { label: 'Beginner', value: 'beginner' },\n { label: 'Intermediate', value: 'intermediate' },\n { label: 'Advanced', value: 'advanced' },\n ],\n },\n {\n name: 'status',\n label: 'Status',\n type: 'select',\n required: true,\n options: [\n { label: 'Active', value: 'active' },\n { label: 'Draft', value: 'draft' },\n { label: 'Archived', value: 'archived' },\n ],\n },\n { name: 'description', label: 'Description', type: 'textarea', required: true, colSpan: 2 },\n {\n name: 'skills',\n label: 'Skills',\n type: 'tags',\n placeholder: 'Add a skill and press Enter',\n colSpan: 2,\n },\n];\n\ntype IconType = ComponentType<{ className?: string }>;\n\n/** Deterministic tokenized accent for a category (never a raw color). */\nfunction categoryAccent(category: string): string {\n const palette = chartPalette(8);\n let hash = 0;\n for (let i = 0; i < category.length; i += 1) {\n hash = (hash * 31 + category.charCodeAt(i)) >>> 0;\n }\n return palette[hash % palette.length];\n}\n\nfunction categoryIcon(category: string): IconType {\n const c = category.toLowerCase();\n if (\n c.includes('engineer') ||\n c.includes('develop') ||\n c.includes('program') ||\n c.includes('tech')\n )\n return Code2;\n if (c.includes('design') || c.includes('ux') || c.includes('ui')) return Palette;\n if (c.includes('lead') || c.includes('manage') || c.includes('people') || c.includes('hr'))\n return Users;\n if (c.includes('compliance') || c.includes('security') || c.includes('legal')) return ShieldCheck;\n if (c.includes('data') || c.includes('analyt')) return BarChart3;\n if (c.includes('market') || c.includes('sales')) return Megaphone;\n if (c.includes('business') || c.includes('finance') || c.includes('operations')) return Briefcase;\n return GraduationCap;\n}\n\nfunction levelVariant(level: LearningCourse['level']): 'secondary' | 'default' | 'destructive' {\n return level === 'beginner' ? 'secondary' : level === 'intermediate' ? 'default' : 'destructive';\n}\n\nexport function LearningCoursesPagePage() {\n const { courses, addCourse, enrollCourse } = useCrewFoundryStore();\n const { showToast } = useGlobalUi();\n const navigate = useNavigate();\n const [search, setSearch] = useState('');\n const [category, setCategory] = useState('all');\n const [enrollingId, setEnrollingId] = useState<string | null>(null);\n const createModal = useFormModal();\n const { page, pageSize, offset, setPage } = usePagination({ initialPageSize: 12 });\n\n const categories = useMemo(() => {\n const seen = new Map<string, number>();\n for (const course of courses) {\n const key = course.category.trim() || 'Uncategorized';\n seen.set(key, (seen.get(key) ?? 0) + 1);\n }\n return [...seen.entries()].sort((a, b) => b[1] - a[1]);\n }, [courses]);\n\n const filtered = courses.filter((c) => {\n const matchesCategory =\n category === 'all' || (c.category.trim() || 'Uncategorized') === category;\n const matchesSearch = `${c.title} ${c.category} ${c.instructor} ${c.skills.join(' ')}`\n .toLowerCase()\n .includes(search.toLowerCase());\n return matchesCategory && matchesSearch;\n });\n const paged = filtered.slice(offset, offset + pageSize);\n\n const openCourse = (id: string) =>\n navigate(`../learning/course-details-page?id=${encodeURIComponent(id)}`);\n const openPlayer = (id: string) =>\n navigate(`../learning/course-learning-page?id=${encodeURIComponent(id)}`);\n\n const handleEnroll = async (course: LearningCourse) => {\n setEnrollingId(course.id);\n try {\n await enrollCourse(course.id);\n showToast(`Enrolled in ${course.title}`, 'success');\n openPlayer(course.id);\n } catch (error) {\n showToast(error instanceof Error ? error.message : 'Unable to enroll in course', 'error');\n } finally {\n setEnrollingId(null);\n }\n };\n\n const handleCreate = async (values: FormValues) => {\n await addCourse({\n id: `course-${Date.now()}`,\n title: String(values.title ?? ''),\n category: String(values.category ?? ''),\n instructor: String(values.instructor ?? ''),\n duration: String(values.duration ?? ''),\n level: String(values.level ?? 'beginner') as LearningCourse['level'],\n enrolled: 0,\n completed: 0,\n rating: 0,\n status: String(values.status ?? 'draft') as LearningCourse['status'],\n description: String(values.description ?? ''),\n modules: typeof values.modules === 'number' ? values.modules : Number(values.modules) || 0,\n skills: Array.isArray(values.skills) ? values.skills : [],\n });\n };\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div>\n <h1 className=\"text-2xl font-semibold\">\n <GradientText>Course Catalog</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">\n {courses.length} courses · {categories.length} categories\n </p>\n <PagePurpose className=\"mt-1\">\n Browse the course catalog — find training to grow your team&apos;s skills and enroll in\n the programs that fit your goals.\n </PagePurpose>\n </div>\n <Button onClick={() => createModal.openModal()}>\n <Plus className=\"mr-1 h-4 w-4\" />\n Create Course\n </Button>\n </div>\n\n <div className=\"flex flex-col gap-3\">\n <div className=\"relative max-w-md\">\n <Search className=\"absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-text-secondary\" />\n <Input\n placeholder=\"Search courses, skills, or instructors...\"\n className=\"pl-9\"\n value={search}\n onChange={(e) => {\n setSearch(e.target.value);\n setPage(1);\n }}\n />\n </div>\n <div className=\"flex flex-wrap gap-2\">\n <button\n type=\"button\"\n onClick={() => {\n setCategory('all');\n setPage(1);\n }}\n className={`rounded-full border px-3 py-1 text-xs font-medium transition-colors ${\n category === 'all'\n ? 'border-transparent bg-action-primary-bg text-action-primary-text'\n : 'border-border-subtle bg-bg-surface text-text-secondary hover:text-text-primary'\n }`}\n >\n All\n </button>\n {categories.map(([name, count]) => (\n <button\n key={name}\n type=\"button\"\n onClick={() => {\n setCategory(name);\n setPage(1);\n }}\n className={`inline-flex items-center gap-1.5 rounded-full border px-3 py-1 text-xs font-medium transition-colors ${\n category === name\n ? 'border-transparent bg-action-primary-bg text-action-primary-text'\n : 'border-border-subtle bg-bg-surface text-text-secondary hover:text-text-primary'\n }`}\n >\n {name}\n <span className=\"tabular-nums opacity-70\">{count}</span>\n </button>\n ))}\n </div>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-3\">\n {filtered.length === 0 ? (\n <div className=\"col-span-full\">\n <IllustratedEmptyState\n illustration=\"empty-search\"\n title=\"No courses found\"\n description=\"Try a different search term or category to find training.\"\n />\n </div>\n ) : (\n paged.map((course) => {\n const accent = categoryAccent(course.category);\n const Icon = categoryIcon(course.category);\n const progress = readCourseProgress(course.id);\n const totalModules = Math.max(course.modules, 1);\n const pct =\n progress.completed.length > 0\n ? Math.min(100, Math.round((progress.completed.length / totalModules) * 100))\n : 0;\n const started = pct > 0;\n return (\n <GlassCard\n key={course.id}\n treatment=\"plain\"\n className=\"group flex flex-col overflow-hidden transition-shadow hover:shadow-md\"\n >\n <button\n type=\"button\"\n aria-label={`View ${course.title}`}\n onClick={() => openCourse(course.id)}\n className=\"relative flex h-24 w-full items-center justify-between px-5 text-left outline-none focus-visible:shadow-[var(--shadow-focus)]\"\n style={{\n background: `linear-gradient(135deg, color-mix(in srgb, ${accent} 28%, var(--color-bg-surface)), color-mix(in srgb, ${accent} 8%, var(--color-bg-surface)))`,\n }}\n >\n <span\n className=\"flex h-11 w-11 items-center justify-center rounded-xl bg-bg-surface/70 shadow-elevation-1\"\n style={{ color: accent }}\n >\n <Icon className=\"h-6 w-6\" />\n </span>\n <Badge variant={levelVariant(course.level)} className=\"text-[10px]\">\n {formatEnumLabel(course.level)}\n </Badge>\n </button>\n <CardHeader className=\"pb-2\">\n <Badge variant=\"outline\" className=\"w-fit text-[10px]\">\n {course.category}\n </Badge>\n <button\n type=\"button\"\n onClick={() => openCourse(course.id)}\n className=\"mt-2 text-left text-base font-semibold outline-none hover:text-action-primary-bg focus-visible:text-action-primary-bg\"\n >\n {course.title}\n </button>\n <p className=\"text-xs text-text-secondary\">\n {course.instructor} · {course.duration} · {course.modules} modules\n </p>\n </CardHeader>\n <CardContent className=\"flex flex-1 flex-col gap-3\">\n <p className=\"line-clamp-2 text-sm text-text-secondary\">{course.description}</p>\n {course.skills.length > 0 && (\n <div className=\"flex flex-wrap gap-1\">\n {course.skills.slice(0, 3).map((s) => (\n <Badge key={s} variant=\"outline\" className=\"text-[9px]\">\n {s}\n </Badge>\n ))}\n </div>\n )}\n <div className=\"mt-auto space-y-3\">\n <div className=\"flex items-center justify-between text-xs text-text-secondary\">\n <RatingStars value={course.rating} size={13} showValue />\n <span className=\"flex items-center gap-1\">\n <Users className=\"h-3 w-3\" />\n {course.enrolled} enrolled\n </span>\n </div>\n {started && (\n <div className=\"space-y-1\">\n <div className=\"flex justify-between text-[11px] font-medium\">\n <span className=\"text-text-secondary\">{pct}% complete</span>\n </div>\n <div className=\"h-1.5 overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-action-primary-bg\"\n style={{ width: `${pct}%` }}\n />\n </div>\n </div>\n )}\n <Button\n className=\"w-full\"\n variant={started ? 'outline' : 'default'}\n disabled={enrollingId === course.id}\n onClick={() => (started ? openPlayer(course.id) : void handleEnroll(course))}\n >\n <Play className=\"mr-1 h-4 w-4\" />\n {enrollingId === course.id ? 'Enrolling…' : started ? 'Continue' : 'Enroll'}\n </Button>\n </div>\n </CardContent>\n </GlassCard>\n );\n })\n )}\n </div>\n\n {filtered.length > pageSize && (\n <Pagination\n page={page}\n pageSize={pageSize}\n totalItems={filtered.length}\n onPageChange={setPage}\n />\n )}\n\n <FormModal\n open={createModal.open}\n onOpenChange={createModal.onOpenChange}\n title=\"Create Course\"\n description=\"Add a new course to the catalog.\"\n fields={COURSE_FIELDS}\n submitLabel=\"Create Course\"\n onSubmit={handleCreate}\n />\n </div>\n );\n}\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import { useCrewFoundryStore as e } from "../store/crewfoundryStore.js";
2
2
  import { formatEnumLabel as t } from "../utils/formatEnumLabel.js";
3
3
  import { BookOpen as n, GraduationCap as r, Star as i, TrendingUp as a, Users as o } from "lucide-react";
4
- import { useNavigate as s } from "react-router-dom";
4
+ import { useNavigate as s } from "react-router";
5
5
  import { jsx as c, jsxs as l } from "react/jsx-runtime";
6
6
  import { Badge as u, Card as d, CardContent as f, CardDescription as p, CardHeader as m, CardTitle as h, GlassCard as g, GradientText as _, PagePurpose as v } from "@burdenoff/fe-libs/ui";
7
7
  //#region src/crewfoundry/pages/LearningLearningPagePage.tsx