@burdenoff/microfe-crewfoundry 2026.704.5 → 2026.704.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/crewfoundry/CrewFoundryRoutes.js +51 -43
- package/dist/crewfoundry/CrewFoundryRoutes.js.map +1 -1
- package/dist/crewfoundry/components/forms/formValues.js +5 -2
- package/dist/crewfoundry/components/forms/formValues.js.map +1 -1
- package/dist/crewfoundry/components/viz/AllocationBar.js +68 -0
- package/dist/crewfoundry/components/viz/AllocationBar.js.map +1 -0
- package/dist/crewfoundry/components/viz/AreaChart.js +102 -0
- package/dist/crewfoundry/components/viz/AreaChart.js.map +1 -0
- package/dist/crewfoundry/components/viz/AvatarStack.js +36 -0
- package/dist/crewfoundry/components/viz/AvatarStack.js.map +1 -0
- package/dist/crewfoundry/components/viz/BarChart.js +83 -0
- package/dist/crewfoundry/components/viz/BarChart.js.map +1 -0
- package/dist/crewfoundry/components/viz/CertificateCard.js +118 -0
- package/dist/crewfoundry/components/viz/CertificateCard.js.map +1 -0
- package/dist/crewfoundry/components/viz/DonutChart.js +109 -0
- package/dist/crewfoundry/components/viz/DonutChart.js.map +1 -0
- package/dist/crewfoundry/components/viz/HBarFunnel.js +54 -0
- package/dist/crewfoundry/components/viz/HBarFunnel.js.map +1 -0
- package/dist/crewfoundry/components/viz/Heatmap.js +91 -0
- package/dist/crewfoundry/components/viz/Heatmap.js.map +1 -0
- package/dist/crewfoundry/components/viz/KanbanBoard.js +50 -0
- package/dist/crewfoundry/components/viz/KanbanBoard.js.map +1 -0
- package/dist/crewfoundry/components/viz/LineChart.js +123 -0
- package/dist/crewfoundry/components/viz/LineChart.js.map +1 -0
- package/dist/crewfoundry/components/viz/OrgTree.js +83 -0
- package/dist/crewfoundry/components/viz/OrgTree.js.map +1 -0
- package/dist/crewfoundry/components/viz/PathSteps.js +68 -0
- package/dist/crewfoundry/components/viz/PathSteps.js.map +1 -0
- package/dist/crewfoundry/components/viz/ProgressRing.js +68 -0
- package/dist/crewfoundry/components/viz/ProgressRing.js.map +1 -0
- package/dist/crewfoundry/components/viz/RatingStars.js +51 -0
- package/dist/crewfoundry/components/viz/RatingStars.js.map +1 -0
- package/dist/crewfoundry/components/viz/SegmentedTabs.js +69 -0
- package/dist/crewfoundry/components/viz/SegmentedTabs.js.map +1 -0
- package/dist/crewfoundry/components/viz/SkillBar.js +3 -0
- package/dist/crewfoundry/components/viz/Sparkline.js +74 -0
- package/dist/crewfoundry/components/viz/Sparkline.js.map +1 -0
- package/dist/crewfoundry/components/viz/StatCard.js +60 -0
- package/dist/crewfoundry/components/viz/StatCard.js.map +1 -0
- package/dist/crewfoundry/components/viz/StepProgress.js +3 -0
- package/dist/crewfoundry/components/viz/Timeline.js +66 -0
- package/dist/crewfoundry/components/viz/Timeline.js.map +1 -0
- package/dist/crewfoundry/components/viz/index.js +23 -0
- package/dist/crewfoundry/components/viz/lib/Avatar.js +30 -0
- package/dist/crewfoundry/components/viz/lib/Avatar.js.map +1 -0
- package/dist/crewfoundry/components/viz/lib/CartesianFrame.js +119 -0
- package/dist/crewfoundry/components/viz/lib/CartesianFrame.js.map +1 -0
- package/dist/crewfoundry/components/viz/lib/ResponsiveSvg.js +35 -0
- package/dist/crewfoundry/components/viz/lib/ResponsiveSvg.js.map +1 -0
- package/dist/crewfoundry/components/viz/lib/format.js +21 -0
- package/dist/crewfoundry/components/viz/lib/format.js.map +1 -0
- package/dist/crewfoundry/components/viz/lib/geometry.js +69 -0
- package/dist/crewfoundry/components/viz/lib/geometry.js.map +1 -0
- package/dist/crewfoundry/components/viz/lib/hooks.js +39 -0
- package/dist/crewfoundry/components/viz/lib/hooks.js.map +1 -0
- package/dist/crewfoundry/components/viz/lib/palette.js +27 -0
- package/dist/crewfoundry/components/viz/lib/palette.js.map +1 -0
- package/dist/crewfoundry/lib/analytics.js +323 -0
- package/dist/crewfoundry/lib/analytics.js.map +1 -0
- package/dist/crewfoundry/lib/companyFinance.js +16 -0
- package/dist/crewfoundry/lib/companyFinance.js.map +1 -0
- package/dist/crewfoundry/pages/AnalyticsAnalyticsPagePage.js +226 -283
- package/dist/crewfoundry/pages/AnalyticsAnalyticsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/AnalyticsEmployeeAnalyticsPagePage.js +107 -120
- package/dist/crewfoundry/pages/AnalyticsEmployeeAnalyticsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/AnalyticsFinancialAnalyticsPagePage.js +122 -162
- package/dist/crewfoundry/pages/AnalyticsFinancialAnalyticsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/AnalyticsHiringAnalyticsPagePage.js +121 -158
- package/dist/crewfoundry/pages/AnalyticsHiringAnalyticsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/CommunicationsAnnouncementsPagePage.js +316 -211
- package/dist/crewfoundry/pages/CommunicationsAnnouncementsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/CommunicationsMessagesPagePage.js +75 -27
- package/dist/crewfoundry/pages/CommunicationsMessagesPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/CompanyClientManagementPagePage.js +167 -165
- package/dist/crewfoundry/pages/CompanyClientManagementPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/CompanyVendorManagementPagePage.js +168 -109
- package/dist/crewfoundry/pages/CompanyVendorManagementPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/DashboardDashboardPagePage.js +401 -290
- package/dist/crewfoundry/pages/DashboardDashboardPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HiringApplicationsPagePage.js +84 -75
- package/dist/crewfoundry/pages/HiringApplicationsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HiringCandidatesPagePage.js +368 -174
- package/dist/crewfoundry/pages/HiringCandidatesPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HiringCreateJobPagePage.js +236 -201
- package/dist/crewfoundry/pages/HiringCreateJobPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HiringInterviewsPagePage.js +369 -196
- package/dist/crewfoundry/pages/HiringInterviewsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HiringJobDetailsPagePage.js +252 -249
- package/dist/crewfoundry/pages/HiringJobDetailsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HiringJobsPagePage.js +210 -110
- package/dist/crewfoundry/pages/HiringJobsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HrDepartmentsPagePage.js +216 -178
- package/dist/crewfoundry/pages/HrDepartmentsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HrEmployeeProfilePagePage.js +385 -0
- package/dist/crewfoundry/pages/HrEmployeeProfilePagePage.js.map +1 -0
- package/dist/crewfoundry/pages/HrEmployeesPagePage.js +189 -130
- package/dist/crewfoundry/pages/HrEmployeesPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HrLeaveManagementPagePage.js +397 -241
- package/dist/crewfoundry/pages/HrLeaveManagementPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HrPayrollPagePage.js +262 -147
- package/dist/crewfoundry/pages/HrPayrollPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HrTimeTrackingPagePage.js +345 -167
- package/dist/crewfoundry/pages/HrTimeTrackingPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/LearningCertificatesPagePage.js +98 -24
- package/dist/crewfoundry/pages/LearningCertificatesPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/LearningCourseDetailsPagePage.js +35 -31
- package/dist/crewfoundry/pages/LearningCourseDetailsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/LearningCourseLearningPagePage.js +289 -108
- package/dist/crewfoundry/pages/LearningCourseLearningPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/LearningCoursesPagePage.js +221 -114
- package/dist/crewfoundry/pages/LearningCoursesPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/LearningLearningPathDetailPagePage.js +136 -133
- package/dist/crewfoundry/pages/LearningLearningPathDetailPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/LearningLearningPathsPagePage.js +148 -97
- package/dist/crewfoundry/pages/LearningLearningPathsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/LearningMyLearningPagePage.js +264 -157
- package/dist/crewfoundry/pages/LearningMyLearningPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/MarketplaceFreelancerProfilePageEnhancedPage.js.map +1 -1
- package/dist/crewfoundry/pages/MarketplaceFreelancerProfilePagePage.js +85 -93
- package/dist/crewfoundry/pages/MarketplaceFreelancerProfilePagePage.js.map +1 -1
- package/dist/crewfoundry/pages/MarketplaceMarketplacePagePage.js +211 -122
- package/dist/crewfoundry/pages/MarketplaceMarketplacePagePage.js.map +1 -1
- package/dist/crewfoundry/pages/MarketplaceMarketplaceServicesPagePage.js +94 -54
- package/dist/crewfoundry/pages/MarketplaceMarketplaceServicesPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/MarketplaceProjectDetailsPagePage.js +28 -25
- package/dist/crewfoundry/pages/MarketplaceProjectDetailsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/MarketplaceServiceDetailsPagePage.js +290 -138
- package/dist/crewfoundry/pages/MarketplaceServiceDetailsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/PaymentsInvoicesPagePage.js +169 -79
- package/dist/crewfoundry/pages/PaymentsInvoicesPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/PaymentsPaymentDashboardPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/ProjectsProjectDetailsPagePage.js +65 -61
- package/dist/crewfoundry/pages/ProjectsProjectDetailsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/ProjectsProjectsPagePage.js +296 -125
- package/dist/crewfoundry/pages/ProjectsProjectsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/ProjectsResourcingPagePage.js +187 -0
- package/dist/crewfoundry/pages/ProjectsResourcingPagePage.js.map +1 -0
- package/dist/crewfoundry/pages/TeamsTeamDetailPagePage.js +142 -76
- package/dist/crewfoundry/pages/TeamsTeamDetailPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/TeamsTeamDirectoryPagePage.js +93 -43
- package/dist/crewfoundry/pages/TeamsTeamDirectoryPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/TeamsTeamSettingsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/TeamsTeamsPagePage.js +250 -98
- package/dist/crewfoundry/pages/TeamsTeamsPagePage.js.map +1 -1
- package/dist/crewfoundry/store/crewfoundryStore.js +1 -1
- package/dist/crewfoundry/store/crewfoundryStore.js.map +1 -1
- package/dist/crewfoundry/utils/cn.js +10 -0
- package/dist/crewfoundry/utils/cn.js.map +1 -0
- package/dist/crewfoundry/utils/courseProgress.js +46 -0
- package/dist/crewfoundry/utils/courseProgress.js.map +1 -0
- package/dist/node_modules/clsx/dist/clsx.js +18 -0
- package/dist/node_modules/clsx/dist/clsx.js.map +1 -0
- package/dist/node_modules/tailwind-merge/dist/bundle-mjs.js +1716 -0
- package/dist/node_modules/tailwind-merge/dist/bundle-mjs.js.map +1 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HrTimeTrackingPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/HrTimeTrackingPagePage.tsx"],"sourcesContent":["import { useMemo } from 'react';\nimport {\n Badge,\n Button,\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n GlassCard,\n GradientText,\n PagePurpose,\n Pagination,\n} from '@burdenoff/fe-libs/ui';\nimport { usePagination } from '@burdenoff/fe-libs/shared/hooks';\nimport { CheckCircle, Clock, LogIn, LogOut, Timer } from 'lucide-react';\nimport { useCrewFoundryStore, type TimeEntry } from '../store/crewfoundryStore';\nimport {\n FormModal,\n useFormModal,\n type FieldConfig,\n type FormValues,\n} from '../components/forms/FormModal';\nimport { getNumber, getText, newClientId } from '../components/forms/formValues';\nimport { formatEnumLabel } from '../utils/formatEnumLabel';\n\nconst STATUS_OPTIONS = [\n { label: 'Pending', value: 'pending' },\n { label: 'Approved', value: 'approved' },\n { label: 'Rejected', value: 'rejected' },\n];\n\nconst CLOCK_OUT_FIELDS: FieldConfig[] = [\n {\n name: 'clockOut',\n label: 'Clock-out time',\n type: 'text',\n required: true,\n placeholder: 'e.g. 17:30',\n },\n { name: 'totalHours', label: 'Total hours', type: 'number', required: true },\n { name: 'overtime', label: 'Overtime hours', type: 'number' },\n { name: 'status', label: 'Status', type: 'select', options: STATUS_OPTIONS },\n];\n\nexport function HrTimeTrackingPagePage() {\n const { timeEntries, employees, addTimeEntry, updateTimeEntry } = useCrewFoundryStore();\n const createForm = useFormModal<void>();\n const clockOutForm = useFormModal<TimeEntry>();\n\n const employeeOptions = useMemo(\n () =>\n employees.map((employee) => ({\n label: `${employee.firstName} ${employee.lastName}`,\n value: employee.id,\n })),\n [employees]\n );\n\n const entryFields = useMemo<FieldConfig[]>(\n () => [\n {\n name: 'employeeId',\n label: 'Employee',\n type: 'select',\n required: true,\n options: employeeOptions,\n colSpan: 2,\n },\n { name: 'date', label: 'Date', type: 'date', required: true },\n { name: 'project', label: 'Project', type: 'text', required: true },\n {\n name: 'clockIn',\n label: 'Clock-in time',\n type: 'text',\n required: true,\n placeholder: 'e.g. 09:00',\n },\n { name: 'clockOut', label: 'Clock-out time', type: 'text', placeholder: 'e.g. 17:30' },\n { name: 'totalHours', label: 'Total hours', type: 'number' },\n { name: 'overtime', label: 'Overtime hours', type: 'number' },\n { name: 'status', label: 'Status', type: 'select', options: STATUS_OPTIONS },\n ],\n [employeeOptions]\n );\n\n const totalHours = timeEntries.reduce((s, t) => s + t.totalHours, 0);\n const totalOvertime = timeEntries.reduce((s, t) => s + t.overtime, 0);\n const approvedEntries = timeEntries.filter((t) => t.status === 'approved').length;\n const pendingEntries = timeEntries.filter((t) => t.status === 'pending').length;\n\n const projectBreakdown = timeEntries.reduce<Record<string, number>>((acc, t) => {\n acc[t.project] = (acc[t.project] || 0) + t.totalHours;\n return acc;\n }, {});\n const maxProjectHours = Math.max(...Object.values(projectBreakdown), 1);\n\n const recentEntries = useMemo(\n () => [...timeEntries].sort((a, b) => b.date.localeCompare(a.date)),\n [timeEntries]\n );\n const { page, pageSize, offset, setPage } = usePagination({ initialPageSize: 8 });\n const activityPageItems = recentEntries.slice(offset, offset + pageSize);\n\n const handleCreate = async (values: FormValues) => {\n const employeeId = getText(values, 'employeeId');\n const employee = employees.find((item) => item.id === employeeId);\n const payload: TimeEntry = {\n id: newClientId(),\n employeeId,\n employeeName: employee ? `${employee.firstName} ${employee.lastName}` : '',\n date: getText(values, 'date'),\n clockIn: getText(values, 'clockIn'),\n clockOut: getText(values, 'clockOut'),\n totalHours: getNumber(values, 'totalHours'),\n overtime: getNumber(values, 'overtime'),\n project: getText(values, 'project'),\n status: (getText(values, 'status') as TimeEntry['status']) || 'pending',\n };\n await addTimeEntry(payload);\n };\n\n const handleClockOut = async (values: FormValues) => {\n const entry = clockOutForm.context;\n if (!entry) {\n return;\n }\n await updateTimeEntry(entry.id, {\n clockOut: getText(values, 'clockOut'),\n totalHours: getNumber(values, 'totalHours'),\n overtime: getNumber(values, 'overtime'),\n status: (getText(values, 'status') as TimeEntry['status']) || entry.status,\n });\n };\n\n const clockOutInitial: FormValues | undefined = clockOutForm.context\n ? {\n clockOut: clockOutForm.context.clockOut,\n totalHours: clockOutForm.context.totalHours,\n overtime: clockOutForm.context.overtime,\n status: clockOutForm.context.status,\n }\n : undefined;\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex items-center justify-between\">\n <div>\n <h1 className=\"text-2xl font-semibold\">\n <GradientText>Time Tracking</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">Monitor employee work hours and overtime</p>\n <PagePurpose className=\"mt-1\">\n Track hours and attendance across your team — use it to monitor logged time, overtime,\n and timesheet approvals.\n </PagePurpose>\n </div>\n <Button size=\"sm\" onClick={() => createForm.openModal()}>\n <Timer className=\"h-4 w-4 mr-1\" />\n Add Entry\n </Button>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-4\">\n <GlassCard treatment=\"glass\" glow className=\"border-border-seam\">\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle className=\"text-sm font-medium\">Total Hours</CardTitle>\n <Clock className=\"h-4 w-4 text-text-secondary\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">{totalHours.toFixed(1)}</div>\n <p className=\"text-xs text-text-secondary\">All logged hours</p>\n </CardContent>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"border-border-seam\">\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle className=\"text-sm font-medium\">Overtime</CardTitle>\n <Timer className=\"h-4 w-4 text-status-warning-text\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">{totalOvertime.toFixed(1)}h</div>\n <p className=\"text-xs text-text-secondary\">Extra hours logged</p>\n </CardContent>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"border-border-seam\">\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle className=\"text-sm font-medium\">Approved</CardTitle>\n <CheckCircle className=\"h-4 w-4 text-status-success-text\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">{approvedEntries}</div>\n <p className=\"text-xs text-text-secondary\">time entries</p>\n </CardContent>\n </GlassCard>\n <GlassCard treatment=\"metric\" className=\"border-border-seam\">\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle className=\"text-sm font-medium\">Pending</CardTitle>\n <Clock className=\"h-4 w-4 text-status-warning-text\" />\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold tabular-nums\">{pendingEntries}</div>\n <p className=\"text-xs text-text-secondary\">awaiting approval</p>\n </CardContent>\n </GlassCard>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-2\">\n <Card>\n <CardHeader>\n <CardTitle>Hours by Project</CardTitle>\n <CardDescription>Time allocation across active projects</CardDescription>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {Object.entries(projectBreakdown)\n .sort(([, a], [, b]) => b - a)\n .map(([project, hours]) => (\n <div key={project} className=\"flex items-center gap-3\">\n <span className=\"w-48 text-sm truncate\">{project}</span>\n <div className=\"flex-1 h-4 bg-bg-sunken rounded overflow-hidden\">\n <div\n className=\"h-4 bg-action-primary-bg rounded\"\n style={{ width: `${Math.round((hours / maxProjectHours) * 100)}%` }}\n />\n </div>\n <span className=\"text-sm font-medium w-14 text-right\">{hours.toFixed(1)}h</span>\n </div>\n ))}\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>Recent Activity</CardTitle>\n <CardDescription>Latest employee clock-in/out entries</CardDescription>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {activityPageItems.map((entry) => (\n <div key={entry.id} className=\"flex items-center justify-between rounded border p-3\">\n <div>\n <p className=\"text-sm font-medium\">{entry.employeeName}</p>\n <p className=\"text-xs text-text-secondary\">{entry.project}</p>\n </div>\n <div className=\"flex items-center gap-3\">\n <div className=\"text-right text-xs\">\n <div className=\"flex items-center gap-1\">\n <LogIn className=\"h-3 w-3 text-status-success-text\" />\n {entry.clockIn}\n </div>\n <div className=\"flex items-center gap-1\">\n <LogOut className=\"h-3 w-3 text-status-error-text\" />\n {entry.clockOut}\n </div>\n </div>\n <div className=\"text-right\">\n <p className=\"text-sm font-medium\">{entry.totalHours}h</p>\n {entry.overtime > 0 && (\n <p className=\"text-xs text-status-warning-text\">+{entry.overtime}h OT</p>\n )}\n </div>\n <Badge\n variant={entry.status === 'approved' ? 'default' : 'secondary'}\n className=\"text-[10px]\"\n >\n {formatEnumLabel(entry.status)}\n </Badge>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-8 w-8 p-0\"\n aria-label={`Clock out ${entry.employeeName}`}\n onClick={() => clockOutForm.openModal(entry)}\n >\n <LogOut className=\"h-4 w-4\" />\n </Button>\n </div>\n </div>\n ))}\n {recentEntries.length === 0 && (\n <p className=\"py-6 text-center text-sm text-text-secondary\">\n No time entries logged yet.\n </p>\n )}\n {recentEntries.length > pageSize && (\n <Pagination\n page={page}\n pageSize={pageSize}\n totalItems={recentEntries.length}\n onPageChange={setPage}\n />\n )}\n </CardContent>\n </Card>\n </div>\n\n <FormModal\n open={createForm.open}\n onOpenChange={createForm.onOpenChange}\n title=\"Add time entry\"\n description=\"Log a work session for a team member.\"\n fields={entryFields}\n submitLabel=\"Add entry\"\n onSubmit={handleCreate}\n />\n\n <FormModal\n open={clockOutForm.open}\n onOpenChange={clockOutForm.onOpenChange}\n title=\"Clock out\"\n description={\n clockOutForm.context\n ? `Record clock-out for ${clockOutForm.context.employeeName}.`\n : undefined\n }\n fields={CLOCK_OUT_FIELDS}\n initialValues={clockOutInitial}\n submitLabel=\"Save\"\n onSubmit={handleClockOut}\n />\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;AA0BA,IAAM,IAAiB;CACrB;EAAE,OAAO;EAAW,OAAO;EAAW;CACtC;EAAE,OAAO;EAAY,OAAO;EAAY;CACxC;EAAE,OAAO;EAAY,OAAO;EAAY;CACzC,EAEK,IAAkC;CACtC;EACE,MAAM;EACN,OAAO;EACP,MAAM;EACN,UAAU;EACV,aAAa;EACd;CACD;EAAE,MAAM;EAAc,OAAO;EAAe,MAAM;EAAU,UAAU;EAAM;CAC5E;EAAE,MAAM;EAAY,OAAO;EAAkB,MAAM;EAAU;CAC7D;EAAE,MAAM;EAAU,OAAO;EAAU,MAAM;EAAU,SAAS;EAAgB;CAC7E;AAED,SAAgB,IAAyB;CACvC,IAAM,EAAE,gBAAa,cAAW,iBAAc,uBAAoB,GAAqB,EACjF,IAAa,GAAoB,EACjC,IAAe,GAAyB,EAExC,IAAkB,QAEpB,EAAU,KAAK,OAAc;EAC3B,OAAO,GAAG,EAAS,UAAU,GAAG,EAAS;EACzC,OAAO,EAAS;EACjB,EAAE,EACL,CAAC,EAAU,CACZ,EAEK,IAAc,QACZ;EACJ;GACE,MAAM;GACN,OAAO;GACP,MAAM;GACN,UAAU;GACV,SAAS;GACT,SAAS;GACV;EACD;GAAE,MAAM;GAAQ,OAAO;GAAQ,MAAM;GAAQ,UAAU;GAAM;EAC7D;GAAE,MAAM;GAAW,OAAO;GAAW,MAAM;GAAQ,UAAU;GAAM;EACnE;GACE,MAAM;GACN,OAAO;GACP,MAAM;GACN,UAAU;GACV,aAAa;GACd;EACD;GAAE,MAAM;GAAY,OAAO;GAAkB,MAAM;GAAQ,aAAa;GAAc;EACtF;GAAE,MAAM;GAAc,OAAO;GAAe,MAAM;GAAU;EAC5D;GAAE,MAAM;GAAY,OAAO;GAAkB,MAAM;GAAU;EAC7D;GAAE,MAAM;GAAU,OAAO;GAAU,MAAM;GAAU,SAAS;GAAgB;EAC7E,EACD,CAAC,EAAgB,CAClB,EAEK,IAAa,EAAY,QAAQ,GAAG,MAAM,IAAI,EAAE,YAAY,EAAE,EAC9D,IAAgB,EAAY,QAAQ,GAAG,MAAM,IAAI,EAAE,UAAU,EAAE,EAC/D,IAAkB,EAAY,QAAQ,MAAM,EAAE,WAAW,WAAW,CAAC,QACrE,IAAiB,EAAY,QAAQ,MAAM,EAAE,WAAW,UAAU,CAAC,QAEnE,IAAmB,EAAY,QAAgC,GAAK,OACxE,EAAI,EAAE,YAAY,EAAI,EAAE,YAAY,KAAK,EAAE,YACpC,IACN,EAAE,CAAC,EACA,IAAkB,KAAK,IAAI,GAAG,OAAO,OAAO,EAAiB,EAAE,EAAE,EAEjE,IAAgB,QACd,CAAC,GAAG,EAAY,CAAC,MAAM,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,KAAK,CAAC,EACnE,CAAC,EAAY,CACd,EACK,EAAE,SAAM,aAAU,WAAQ,eAAY,EAAc,EAAE,iBAAiB,GAAG,CAAC,EAC3E,IAAoB,EAAc,MAAM,GAAQ,IAAS,EAAS,EAElE,IAAe,OAAO,MAAuB;EACjD,IAAM,IAAa,EAAQ,GAAQ,aAAa,EAC1C,IAAW,EAAU,MAAM,MAAS,EAAK,OAAO,EAAW;AAajE,QAAM,EAZqB;GACzB,IAAI,GAAa;GACjB;GACA,cAAc,IAAW,GAAG,EAAS,UAAU,GAAG,EAAS,aAAa;GACxE,MAAM,EAAQ,GAAQ,OAAO;GAC7B,SAAS,EAAQ,GAAQ,UAAU;GACnC,UAAU,EAAQ,GAAQ,WAAW;GACrC,YAAY,EAAU,GAAQ,aAAa;GAC3C,UAAU,EAAU,GAAQ,WAAW;GACvC,SAAS,EAAQ,GAAQ,UAAU;GACnC,QAAS,EAAQ,GAAQ,SAAS,IAA4B;GAC/D,CAC0B;IAGvB,IAAiB,OAAO,MAAuB;EACnD,IAAM,IAAQ,EAAa;AACtB,OAGL,MAAM,EAAgB,EAAM,IAAI;GAC9B,UAAU,EAAQ,GAAQ,WAAW;GACrC,YAAY,EAAU,GAAQ,aAAa;GAC3C,UAAU,EAAU,GAAQ,WAAW;GACvC,QAAS,EAAQ,GAAQ,SAAS,IAA4B,EAAM;GACrE,CAAC;IAGE,IAA0C,EAAa,UACzD;EACE,UAAU,EAAa,QAAQ;EAC/B,YAAY,EAAa,QAAQ;EACjC,UAAU,EAAa,QAAQ;EAC/B,QAAQ,EAAa,QAAQ;EAC9B,GACD,KAAA;AAEJ,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD,EAAA,UAAA;KACE,kBAAC,MAAD;MAAI,WAAU;gBACZ,kBAAC,GAAD,EAAA,UAAc,iBAA4B,CAAA;MACvC,CAAA;KACL,kBAAC,KAAD;MAAG,WAAU;gBAA8B;MAA4C,CAAA;KACvF,kBAAC,GAAD;MAAa,WAAU;gBAAO;MAGhB,CAAA;KACV,EAAA,CAAA,EACN,kBAAC,GAAD;KAAQ,MAAK;KAAK,eAAe,EAAW,WAAW;eAAvD,CACE,kBAAC,GAAD,EAAO,WAAU,gBAAiB,CAAA,EAAA,YAE3B;OACL;;GAEN,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD;MAAW,WAAU;MAAQ,MAAA;MAAK,WAAU;gBAA5C,CACE,kBAAC,GAAD;OAAY,WAAU;iBAAtB,CACE,kBAAC,GAAD;QAAW,WAAU;kBAAsB;QAAuB,CAAA,EAClE,kBAAC,GAAD,EAAO,WAAU,+BAAgC,CAAA,CACtC;UACb,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAmC,EAAW,QAAQ,EAAE;OAAO,CAAA,EAC9E,kBAAC,KAAD;OAAG,WAAU;iBAA8B;OAAoB,CAAA,CACnD,EAAA,CAAA,CACJ;;KACZ,kBAAC,GAAD;MAAW,WAAU;MAAS,WAAU;gBAAxC,CACE,kBAAC,GAAD;OAAY,WAAU;iBAAtB,CACE,kBAAC,GAAD;QAAW,WAAU;kBAAsB;QAAoB,CAAA,EAC/D,kBAAC,GAAD,EAAO,WAAU,oCAAqC,CAAA,CAC3C;UACb,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CAAkD,EAAc,QAAQ,EAAE,EAAC,IAAO;UAClF,kBAAC,KAAD;OAAG,WAAU;iBAA8B;OAAsB,CAAA,CACrD,EAAA,CAAA,CACJ;;KACZ,kBAAC,GAAD;MAAW,WAAU;MAAS,WAAU;gBAAxC,CACE,kBAAC,GAAD;OAAY,WAAU;iBAAtB,CACE,kBAAC,GAAD;QAAW,WAAU;kBAAsB;QAAoB,CAAA,EAC/D,kBAAC,GAAD,EAAa,WAAU,oCAAqC,CAAA,CACjD;UACb,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAmC;OAAsB,CAAA,EACxE,kBAAC,KAAD;OAAG,WAAU;iBAA8B;OAAgB,CAAA,CAC/C,EAAA,CAAA,CACJ;;KACZ,kBAAC,GAAD;MAAW,WAAU;MAAS,WAAU;gBAAxC,CACE,kBAAC,GAAD;OAAY,WAAU;iBAAtB,CACE,kBAAC,GAAD;QAAW,WAAU;kBAAsB;QAAmB,CAAA,EAC9D,kBAAC,GAAD,EAAO,WAAU,oCAAqC,CAAA,CAC3C;UACb,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAmC;OAAqB,CAAA,EACvE,kBAAC,KAAD;OAAG,WAAU;iBAA8B;OAAqB,CAAA,CACpD,EAAA,CAAA,CACJ;;KACR;;GAEN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UAAW,oBAA4B,CAAA,EACvC,kBAAC,GAAD,EAAA,UAAiB,0CAAwD,CAAA,CAC9D,EAAA,CAAA,EACb,kBAAC,GAAD;KAAa,WAAU;eACpB,OAAO,QAAQ,EAAiB,CAC9B,MAAM,GAAG,IAAI,GAAG,OAAO,IAAI,EAAE,CAC7B,KAAK,CAAC,GAAS,OACd,kBAAC,OAAD;MAAmB,WAAU;gBAA7B;OACE,kBAAC,QAAD;QAAM,WAAU;kBAAyB;QAAe,CAAA;OACxD,kBAAC,OAAD;QAAK,WAAU;kBACb,kBAAC,OAAD;SACE,WAAU;SACV,OAAO,EAAE,OAAO,GAAG,KAAK,MAAO,IAAQ,IAAmB,IAAI,CAAC,IAAI;SACnE,CAAA;QACE,CAAA;OACN,kBAAC,QAAD;QAAM,WAAU;kBAAhB,CAAuD,EAAM,QAAQ,EAAE,EAAC,IAAQ;;OAC5E;QATI,EASJ,CACN;KACQ,CAAA,CACT,EAAA,CAAA,EAEP,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UAAW,mBAA2B,CAAA,EACtC,kBAAC,GAAD,EAAA,UAAiB,wCAAsD,CAAA,CAC5D,EAAA,CAAA,EACb,kBAAC,GAAD;KAAa,WAAU;eAAvB;MACG,EAAkB,KAAK,MACtB,kBAAC,OAAD;OAAoB,WAAU;iBAA9B,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;QAAG,WAAU;kBAAuB,EAAM;QAAiB,CAAA,EAC3D,kBAAC,KAAD;QAAG,WAAU;kBAA+B,EAAM;QAAY,CAAA,CAC1D,EAAA,CAAA,EACN,kBAAC,OAAD;QAAK,WAAU;kBAAf;SACE,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,OAAD;WAAK,WAAU;qBAAf,CACE,kBAAC,GAAD,EAAO,WAAU,oCAAqC,CAAA,EACrD,EAAM,QACH;cACN,kBAAC,OAAD;WAAK,WAAU;qBAAf,CACE,kBAAC,GAAD,EAAQ,WAAU,kCAAmC,CAAA,EACpD,EAAM,SACH;aACF;;SACN,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,KAAD;WAAG,WAAU;qBAAb,CAAoC,EAAM,YAAW,IAAK;cACzD,EAAM,WAAW,KAChB,kBAAC,KAAD;WAAG,WAAU;qBAAb;YAAgD;YAAE,EAAM;YAAS;YAAQ;aAEvE;;SACN,kBAAC,GAAD;UACE,SAAS,EAAM,WAAW,aAAa,YAAY;UACnD,WAAU;oBAET,EAAgB,EAAM,OAAO;UACxB,CAAA;SACR,kBAAC,GAAD;UACE,SAAQ;UACR,MAAK;UACL,WAAU;UACV,cAAY,aAAa,EAAM;UAC/B,eAAe,EAAa,UAAU,EAAM;oBAE5C,kBAAC,GAAD,EAAQ,WAAU,WAAY,CAAA;UACvB,CAAA;SACL;UACF;SAtCI,EAAM,GAsCV,CACN;MACD,EAAc,WAAW,KACxB,kBAAC,KAAD;OAAG,WAAU;iBAA+C;OAExD,CAAA;MAEL,EAAc,SAAS,KACtB,kBAAC,GAAD;OACQ;OACI;OACV,YAAY,EAAc;OAC1B,cAAc;OACd,CAAA;MAEQ;OACT,EAAA,CAAA,CACH;;GAEN,kBAAC,GAAD;IACE,MAAM,EAAW;IACjB,cAAc,EAAW;IACzB,OAAM;IACN,aAAY;IACZ,QAAQ;IACR,aAAY;IACZ,UAAU;IACV,CAAA;GAEF,kBAAC,GAAD;IACE,MAAM,EAAa;IACnB,cAAc,EAAa;IAC3B,OAAM;IACN,aACE,EAAa,UACT,wBAAwB,EAAa,QAAQ,aAAa,KAC1D,KAAA;IAEN,QAAQ;IACR,eAAe;IACf,aAAY;IACZ,UAAU;IACV,CAAA;GACE"}
|
|
1
|
+
{"version":3,"file":"HrTimeTrackingPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/HrTimeTrackingPagePage.tsx"],"sourcesContent":["import { useMemo, useState } from 'react';\nimport {\n Badge,\n Button,\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n GradientText,\n PagePurpose,\n Pagination,\n} from '@burdenoff/fe-libs/ui';\nimport { usePagination } from '@burdenoff/fe-libs/shared/hooks';\nimport {\n CheckCircle,\n ChevronLeft,\n ChevronRight,\n Clock,\n LogIn,\n LogOut,\n Timer,\n} from 'lucide-react';\nimport { useCrewFoundryStore, type Employee, type TimeEntry } from '../store/crewfoundryStore';\nimport { AvatarStack, DonutChart, StatCard } from '../components/viz';\nimport { billableSplit } from '../lib/analytics';\nimport {\n FormModal,\n useFormModal,\n type FieldConfig,\n type FormValues,\n} from '../components/forms/FormModal';\nimport { getBool, getNumber, getText, newClientId } from '../components/forms/formValues';\nimport { formatEnumLabel } from '../utils/formatEnumLabel';\n\nconst STATUS_OPTIONS = [\n { label: 'Pending', value: 'pending' },\n { label: 'Approved', value: 'approved' },\n { label: 'Rejected', value: 'rejected' },\n];\n\nconst CLOCK_OUT_FIELDS: FieldConfig[] = [\n {\n name: 'clockOut',\n label: 'Clock-out time',\n type: 'text',\n required: true,\n placeholder: 'e.g. 17:30',\n },\n { name: 'totalHours', label: 'Total hours', type: 'number', required: true },\n { name: 'overtime', label: 'Overtime hours', type: 'number' },\n { name: 'status', label: 'Status', type: 'select', options: STATUS_OPTIONS },\n];\n\nconst MONTH_ABBR = [\n 'Jan',\n 'Feb',\n 'Mar',\n 'Apr',\n 'May',\n 'Jun',\n 'Jul',\n 'Aug',\n 'Sep',\n 'Oct',\n 'Nov',\n 'Dec',\n];\nconst DOW = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];\n\nconst isoLocal = (date: Date): string =>\n `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(\n date.getDate()\n ).padStart(2, '0')}`;\n\n/** Local Monday that opens the week containing `date`. */\nconst startOfWeek = (date: Date): Date => {\n const x = new Date(date.getFullYear(), date.getMonth(), date.getDate());\n const day = (x.getDay() + 6) % 7; // Mon = 0\n x.setDate(x.getDate() - day);\n return x;\n};\n\nconst parseDay = (value: string): Date => {\n const [y, m, d] = value.slice(0, 10).split('-').map(Number);\n return new Date(y, (m || 1) - 1, d || 1);\n};\n\nexport function HrTimeTrackingPagePage() {\n const { timeEntries, employees, addTimeEntry, updateTimeEntry } = useCrewFoundryStore();\n const createForm = useFormModal<void>();\n const clockOutForm = useFormModal<TimeEntry>();\n const [entryPrefill, setEntryPrefill] = useState<{ employeeId: string; date: string } | null>(\n null\n );\n\n const employeeById = useMemo(\n () => new Map<string, Employee>(employees.map((employee) => [employee.id, employee])),\n [employees]\n );\n\n const employeeOptions = useMemo(\n () =>\n employees.map((employee) => ({\n label: `${employee.firstName} ${employee.lastName}`,\n value: employee.id,\n })),\n [employees]\n );\n\n const entryFields = useMemo<FieldConfig[]>(\n () => [\n {\n name: 'employeeId',\n label: 'Employee',\n type: 'select',\n required: true,\n options: employeeOptions,\n colSpan: 2,\n },\n { name: 'date', label: 'Date', type: 'date', required: true },\n { name: 'project', label: 'Project', type: 'text', required: true },\n {\n name: 'clockIn',\n label: 'Clock-in time',\n type: 'text',\n required: true,\n placeholder: 'e.g. 09:00',\n },\n { name: 'clockOut', label: 'Clock-out time', type: 'text', placeholder: 'e.g. 17:30' },\n { name: 'totalHours', label: 'Total hours', type: 'number' },\n { name: 'overtime', label: 'Overtime hours', type: 'number' },\n { name: 'status', label: 'Status', type: 'select', options: STATUS_OPTIONS },\n { name: 'billable', label: 'Billable to client', type: 'switch', colSpan: 2 },\n ],\n [employeeOptions]\n );\n\n // ── Week scoping ───────────────────────────────────────────────────────────\n const [weekOffset, setWeekOffset] = useState(0);\n\n // Anchor the initial week on the most recent entry so the grid opens on data.\n const baseWeekStart = useMemo(() => {\n const latest = timeEntries\n .map((entry) => entry.date.slice(0, 10))\n .filter(Boolean)\n .sort()\n .slice(-1)[0];\n return startOfWeek(latest ? parseDay(latest) : new Date());\n }, [timeEntries]);\n\n const weekStart = useMemo(() => {\n const start = new Date(baseWeekStart);\n start.setDate(start.getDate() + weekOffset * 7);\n return start;\n }, [baseWeekStart, weekOffset]);\n\n const weekDays = useMemo(\n () =>\n Array.from({ length: 7 }, (_, i) => {\n const date = new Date(weekStart);\n date.setDate(weekStart.getDate() + i);\n return { iso: isoLocal(date), label: `${DOW[i]} ${date.getDate()}` };\n }),\n [weekStart]\n );\n const weekDaySet = useMemo(() => new Set(weekDays.map((d) => d.iso)), [weekDays]);\n\n const weekEnd = weekDays[6];\n const weekRangeLabel = `${MONTH_ABBR[weekStart.getMonth()]} ${weekStart.getDate()} – ${\n MONTH_ABBR[parseDay(weekEnd.iso).getMonth()]\n } ${parseDay(weekEnd.iso).getDate()}`;\n\n const weekEntries = useMemo(\n () => timeEntries.filter((entry) => weekDaySet.has(entry.date.slice(0, 10))),\n [timeEntries, weekDaySet]\n );\n\n // KPIs scoped to the selected week.\n const weekTotalHours = weekEntries.reduce((s, t) => s + t.totalHours, 0);\n const weekOvertime = weekEntries.reduce((s, t) => s + t.overtime, 0);\n const weekApproved = weekEntries.filter((t) => t.status === 'approved').length;\n const weekPending = weekEntries.filter((t) => t.status === 'pending').length;\n\n // Weekly timesheet rows: one per employee with entries this week.\n const timesheetRows = useMemo(() => {\n const byEmployee = new Map<\n string,\n { name: string; avatar?: string; perDay: Map<string, number>; total: number; statuses: Set<string> }\n >();\n for (const entry of weekEntries) {\n let row = byEmployee.get(entry.employeeId);\n if (!row) {\n row = {\n name: entry.employeeName || employeeById.get(entry.employeeId)?.firstName || 'Unknown',\n avatar: employeeById.get(entry.employeeId)?.avatar,\n perDay: new Map<string, number>(),\n total: 0,\n statuses: new Set<string>(),\n };\n byEmployee.set(entry.employeeId, row);\n }\n const day = entry.date.slice(0, 10);\n row.perDay.set(day, (row.perDay.get(day) ?? 0) + entry.totalHours);\n row.total += entry.totalHours;\n row.statuses.add(entry.status);\n }\n return [...byEmployee.entries()]\n .map(([employeeId, row]) => ({\n employeeId,\n name: row.name,\n avatar: row.avatar,\n perDay: row.perDay,\n total: row.total,\n status: row.statuses.has('pending')\n ? 'pending'\n : row.statuses.has('rejected')\n ? 'rejected'\n : 'approved',\n }))\n .sort((a, b) => b.total - a.total);\n }, [weekEntries, employeeById]);\n\n // Donut: billable vs non-billable hours for the week, from the real\n // TimeEntry.billable flag. Null (empty state) when no hours are logged —\n // never a fabricated ratio.\n const billableDonut = useMemo(() => {\n const segments = billableSplit(weekEntries);\n if (!segments) {\n return null;\n }\n const total = segments.reduce((sum, seg) => sum + seg.value, 0);\n const billableHours = segments.find((seg) => seg.label === 'Billable')?.value ?? 0;\n const billablePct = total > 0 ? Math.round((billableHours / total) * 100) : 0;\n return { segments, centerValue: `${billablePct}%`, caption: 'Billable' };\n }, [weekEntries]);\n\n // Hours-by-project for the selected week.\n const projectBreakdown = weekEntries.reduce<Record<string, number>>((acc, t) => {\n acc[t.project] = (acc[t.project] || 0) + t.totalHours;\n return acc;\n }, {});\n const maxProjectHours = Math.max(...Object.values(projectBreakdown), 1);\n\n const recentEntries = useMemo(\n () => [...timeEntries].sort((a, b) => b.date.localeCompare(a.date)),\n [timeEntries]\n );\n const { page, pageSize, offset, setPage } = usePagination({ initialPageSize: 8 });\n const activityPageItems = recentEntries.slice(offset, offset + pageSize);\n\n const openCreate = (prefill?: { employeeId: string; date: string }) => {\n setEntryPrefill(prefill ?? null);\n createForm.openModal();\n };\n\n const handleCreate = async (values: FormValues) => {\n const employeeId = getText(values, 'employeeId');\n const employee = employees.find((item) => item.id === employeeId);\n const payload: TimeEntry = {\n id: newClientId(),\n employeeId,\n employeeName: employee ? `${employee.firstName} ${employee.lastName}` : '',\n date: getText(values, 'date'),\n clockIn: getText(values, 'clockIn'),\n clockOut: getText(values, 'clockOut'),\n totalHours: getNumber(values, 'totalHours'),\n overtime: getNumber(values, 'overtime'),\n project: getText(values, 'project'),\n status: (getText(values, 'status') as TimeEntry['status']) || 'pending',\n billable: getBool(values, 'billable'),\n };\n await addTimeEntry(payload);\n };\n\n const handleClockOut = async (values: FormValues) => {\n const entry = clockOutForm.context;\n if (!entry) {\n return;\n }\n await updateTimeEntry(entry.id, {\n clockOut: getText(values, 'clockOut'),\n totalHours: getNumber(values, 'totalHours'),\n overtime: getNumber(values, 'overtime'),\n status: (getText(values, 'status') as TimeEntry['status']) || entry.status,\n });\n };\n\n // Default `billable` on to mirror the backend default (true) for new entries.\n const createInitial: FormValues = {\n billable: true,\n ...(entryPrefill ? { employeeId: entryPrefill.employeeId, date: entryPrefill.date } : {}),\n };\n\n const clockOutInitial: FormValues | undefined = clockOutForm.context\n ? {\n clockOut: clockOutForm.context.clockOut,\n totalHours: clockOutForm.context.totalHours,\n overtime: clockOutForm.context.overtime,\n status: clockOutForm.context.status,\n }\n : undefined;\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between\">\n <div>\n <h1 className=\"text-2xl font-semibold\">\n <GradientText>Time Tracking</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">Monitor employee work hours and overtime</p>\n <PagePurpose className=\"mt-1\">\n Track hours and attendance across your team — use it to monitor logged time, overtime,\n and timesheet approvals.\n </PagePurpose>\n </div>\n <div className=\"flex items-center gap-2\">\n <div className=\"flex items-center gap-1 rounded-lg border border-border-subtle p-1\">\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-8 w-8 p-0\"\n aria-label=\"Previous week\"\n onClick={() => setWeekOffset((o) => o - 1)}\n >\n <ChevronLeft className=\"h-4 w-4\" />\n </Button>\n <span className=\"min-w-[8.5rem] text-center text-sm font-medium tabular-nums\">\n {weekRangeLabel}\n </span>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-8 w-8 p-0\"\n aria-label=\"Next week\"\n onClick={() => setWeekOffset((o) => o + 1)}\n >\n <ChevronRight className=\"h-4 w-4\" />\n </Button>\n </div>\n <Button size=\"sm\" onClick={() => openCreate()}>\n <Timer className=\"h-4 w-4 mr-1\" />\n Log Time\n </Button>\n </div>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n label=\"Total Hours\"\n value={weekTotalHours.toFixed(1)}\n icon={<Clock className=\"h-4 w-4\" />}\n />\n <StatCard\n label=\"Overtime\"\n value={`${weekOvertime.toFixed(1)}h`}\n icon={<Timer className=\"h-4 w-4\" />}\n />\n <StatCard\n label=\"Approved\"\n value={weekApproved}\n icon={<CheckCircle className=\"h-4 w-4\" />}\n />\n <StatCard label=\"Pending\" value={weekPending} icon={<Clock className=\"h-4 w-4\" />} />\n </div>\n\n <div className=\"grid gap-4 xl:grid-cols-[minmax(0,2.2fr)_minmax(0,1fr)]\">\n <Card>\n <CardHeader>\n <CardTitle>Timesheet</CardTitle>\n <CardDescription>\n {weekRangeLabel} · tap a day cell to log time for that person\n </CardDescription>\n </CardHeader>\n <CardContent>\n <div className=\"overflow-x-auto\">\n <table className=\"w-full min-w-[46rem] border-collapse text-sm\">\n <thead>\n <tr className=\"border-b border-border-subtle text-xs text-text-secondary\">\n <th className=\"px-2 py-2 text-left font-medium\">Employee</th>\n {weekDays.map((day) => (\n <th key={day.iso} className=\"px-1 py-2 text-center font-medium\">\n {day.label}\n </th>\n ))}\n <th className=\"px-2 py-2 text-right font-medium\">Total</th>\n <th className=\"px-2 py-2 text-center font-medium\">Status</th>\n </tr>\n </thead>\n <tbody>\n {timesheetRows.map((row) => (\n <tr key={row.employeeId} className=\"border-b border-border-subtle/60\">\n <td className=\"px-2 py-2\">\n <div className=\"flex items-center gap-2\">\n <AvatarStack\n people={[{ name: row.name, avatarUrl: row.avatar }]}\n size={28}\n />\n <span className=\"truncate font-medium\">{row.name}</span>\n </div>\n </td>\n {weekDays.map((day) => {\n const hours = row.perDay.get(day.iso) ?? 0;\n return (\n <td key={day.iso} className=\"px-1 py-2 text-center\">\n <button\n type=\"button\"\n onClick={() =>\n openCreate({ employeeId: row.employeeId, date: day.iso })\n }\n className={`w-full rounded-md border px-1 py-1.5 text-xs tabular-nums transition-colors ${\n hours > 0\n ? 'border-border-subtle text-text-primary hover:bg-bg-sunken'\n : 'border-transparent text-text-muted hover:border-border-subtle hover:bg-bg-sunken'\n }`}\n aria-label={`Log time for ${row.name} on ${day.label}`}\n >\n {hours.toFixed(1)}\n </button>\n </td>\n );\n })}\n <td className=\"px-2 py-2 text-right font-semibold tabular-nums\">\n {row.total.toFixed(1)}\n </td>\n <td className=\"px-2 py-2 text-center\">\n <Badge\n variant={\n row.status === 'approved'\n ? 'default'\n : row.status === 'pending'\n ? 'secondary'\n : 'destructive'\n }\n className=\"text-[10px]\"\n >\n {formatEnumLabel(row.status)}\n </Badge>\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n {timesheetRows.length === 0 && (\n <p className=\"py-8 text-center text-sm text-text-secondary\">\n No time logged for {weekRangeLabel}.\n </p>\n )}\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>Billable split</CardTitle>\n <CardDescription>Billable vs non-billable hours for {weekRangeLabel}</CardDescription>\n </CardHeader>\n <CardContent className=\"flex flex-col items-center\">\n {billableDonut ? (\n <DonutChart\n segments={billableDonut.segments}\n centerLabel={billableDonut.centerValue}\n centerCaption={billableDonut.caption}\n valueFormatter={(v) => `${v.toFixed(1)}h`}\n title=\"Billable vs non-billable hours\"\n />\n ) : (\n <p className=\"py-10 text-center text-sm text-text-secondary\">\n No hours logged this week.\n </p>\n )}\n </CardContent>\n </Card>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-2\">\n <Card>\n <CardHeader>\n <CardTitle>Hours by Project</CardTitle>\n <CardDescription>Time allocation for {weekRangeLabel}</CardDescription>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {Object.entries(projectBreakdown)\n .sort(([, a], [, b]) => b - a)\n .map(([project, hours]) => (\n <div key={project} className=\"flex items-center gap-3\">\n <span className=\"w-40 truncate text-sm\">{project}</span>\n <div className=\"h-4 flex-1 overflow-hidden rounded bg-bg-sunken\">\n <div\n className=\"h-4 rounded bg-action-primary-bg\"\n style={{ width: `${Math.round((hours / maxProjectHours) * 100)}%` }}\n />\n </div>\n <span className=\"w-14 text-right text-sm font-medium tabular-nums\">\n {hours.toFixed(1)}h\n </span>\n </div>\n ))}\n {Object.keys(projectBreakdown).length === 0 && (\n <p className=\"py-6 text-center text-sm text-text-secondary\">\n No project hours this week.\n </p>\n )}\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>Recent Activity</CardTitle>\n <CardDescription>Latest employee clock-in/out entries</CardDescription>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {activityPageItems.map((entry) => (\n <div key={entry.id} className=\"flex items-center justify-between rounded border p-3\">\n <div className=\"flex items-center gap-2.5\">\n <AvatarStack\n people={[\n { name: entry.employeeName, avatarUrl: employeeById.get(entry.employeeId)?.avatar },\n ]}\n size={30}\n />\n <div>\n <p className=\"text-sm font-medium\">{entry.employeeName}</p>\n <p className=\"text-xs text-text-secondary\">{entry.project}</p>\n </div>\n </div>\n <div className=\"flex items-center gap-3\">\n <div className=\"text-right text-xs\">\n <div className=\"flex items-center gap-1\">\n <LogIn className=\"h-3 w-3 text-status-success-text\" />\n {entry.clockIn}\n </div>\n <div className=\"flex items-center gap-1\">\n <LogOut className=\"h-3 w-3 text-status-error-text\" />\n {entry.clockOut}\n </div>\n </div>\n <div className=\"text-right\">\n <p className=\"text-sm font-medium\">{entry.totalHours}h</p>\n {entry.overtime > 0 && (\n <p className=\"text-xs text-status-warning-text\">+{entry.overtime}h OT</p>\n )}\n </div>\n <Badge\n variant={entry.status === 'approved' ? 'default' : 'secondary'}\n className=\"text-[10px]\"\n >\n {formatEnumLabel(entry.status)}\n </Badge>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-8 w-8 p-0\"\n aria-label={`Clock out ${entry.employeeName}`}\n onClick={() => clockOutForm.openModal(entry)}\n >\n <LogOut className=\"h-4 w-4\" />\n </Button>\n </div>\n </div>\n ))}\n {recentEntries.length === 0 && (\n <p className=\"py-6 text-center text-sm text-text-secondary\">\n No time entries logged yet.\n </p>\n )}\n {recentEntries.length > pageSize && (\n <Pagination\n page={page}\n pageSize={pageSize}\n totalItems={recentEntries.length}\n onPageChange={setPage}\n />\n )}\n </CardContent>\n </Card>\n </div>\n\n <FormModal\n open={createForm.open}\n onOpenChange={createForm.onOpenChange}\n title=\"Log time\"\n description=\"Log a work session for a team member.\"\n fields={entryFields}\n initialValues={createInitial}\n submitLabel=\"Add entry\"\n onSubmit={handleCreate}\n />\n\n <FormModal\n open={clockOutForm.open}\n onOpenChange={clockOutForm.onOpenChange}\n title=\"Clock out\"\n description={\n clockOutForm.context\n ? `Record clock-out for ${clockOutForm.context.employeeName}.`\n : undefined\n }\n fields={CLOCK_OUT_FIELDS}\n initialValues={clockOutInitial}\n submitLabel=\"Save\"\n onSubmit={handleClockOut}\n />\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,37 +1,111 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
1
|
+
import { useCrewFoundryStore as e } from "../store/crewfoundryStore.js";
|
|
2
|
+
import { CertificateCard as t } from "../components/viz/CertificateCard.js";
|
|
3
|
+
import "../components/viz/index.js";
|
|
4
|
+
import { readAllCourseProgress as n } from "../utils/courseProgress.js";
|
|
5
|
+
import { useMemo as r } from "react";
|
|
6
|
+
import { GraduationCap as i } from "lucide-react";
|
|
7
|
+
import { useNavigate as a } from "react-router-dom";
|
|
8
|
+
import { jsx as o, jsxs as s } from "react/jsx-runtime";
|
|
9
|
+
import { Button as c, GradientText as l, IllustratedEmptyState as u, PagePurpose as d } from "@burdenoff/fe-libs/ui";
|
|
10
|
+
import { useAuth as f } from "@burdenoff/fe-libs/shared/providers/shell";
|
|
4
11
|
//#region src/crewfoundry/pages/LearningCertificatesPagePage.tsx
|
|
5
|
-
function
|
|
6
|
-
|
|
7
|
-
|
|
12
|
+
function p(e) {
|
|
13
|
+
if (!e) return "—";
|
|
14
|
+
let t = new Date(e);
|
|
15
|
+
return Number.isNaN(t.getTime()) ? "—" : t.toLocaleDateString(void 0, {
|
|
16
|
+
year: "numeric",
|
|
17
|
+
month: "short",
|
|
18
|
+
day: "numeric"
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function m(e, t, n, r) {
|
|
22
|
+
if (typeof document > "u") return;
|
|
23
|
+
let i = (e) => e.replace(/[&<>"]/g, (e) => ({
|
|
24
|
+
"&": "&",
|
|
25
|
+
"<": "<",
|
|
26
|
+
">": ">",
|
|
27
|
+
"\"": """
|
|
28
|
+
})[e] ?? e), a = `<!doctype html><html><head><meta charset="utf-8"><title>Certificate — ${i(e)}</title>
|
|
29
|
+
<style>
|
|
30
|
+
body{font-family:Georgia,'Times New Roman',serif;margin:0;padding:48px;text-align:center}
|
|
31
|
+
.frame{border:6px double currentColor;padding:56px 40px;max-width:760px;margin:0 auto}
|
|
32
|
+
.kicker{letter-spacing:.3em;text-transform:uppercase;font-size:12px;opacity:.7}
|
|
33
|
+
h1{font-size:34px;margin:24px 0 8px}
|
|
34
|
+
.recipient{font-size:26px;margin:20px 0}
|
|
35
|
+
.rule{width:200px;border:0;border-top:1px solid currentColor;margin:24px auto;opacity:.4}
|
|
36
|
+
.meta{font-size:13px;opacity:.7;margin-top:32px}
|
|
37
|
+
</style></head><body>
|
|
38
|
+
<div class="frame">
|
|
39
|
+
<div class="kicker">CrewFoundry · Certificate of Completion</div>
|
|
40
|
+
<h1>${i(e)}</h1>
|
|
41
|
+
<div class="kicker">Awarded to</div>
|
|
42
|
+
<div class="recipient">${i(t)}</div>
|
|
43
|
+
<hr class="rule" />
|
|
44
|
+
<div class="meta">Issued ${i(n)} · Credential ID: ${i(r)}</div>
|
|
45
|
+
</div>
|
|
46
|
+
</body></html>`, o = new Blob([a], { type: "text/html" }), s = URL.createObjectURL(o), c = document.createElement("a");
|
|
47
|
+
c.href = s, c.download = `crewfoundry-certificate-${r}.html`, document.body.appendChild(c), c.click(), c.remove(), URL.revokeObjectURL(s);
|
|
48
|
+
}
|
|
49
|
+
function h() {
|
|
50
|
+
let h = a(), g = f(), { courses: _ } = e(), v = g.user?.name?.trim() || "CrewFoundry Learner", y = r(() => {
|
|
51
|
+
let e = n();
|
|
52
|
+
return _.map((t) => {
|
|
53
|
+
let n = e[t.id], r = n?.completed.length ?? 0, i = Math.max(t.modules, 1);
|
|
54
|
+
return {
|
|
55
|
+
course: t,
|
|
56
|
+
isComplete: r > 0 && r >= i,
|
|
57
|
+
completedAt: n?.completedAt
|
|
58
|
+
};
|
|
59
|
+
}).filter((e) => e.isComplete).sort((e, t) => (t.completedAt ?? "").localeCompare(e.completedAt ?? ""));
|
|
60
|
+
}, [_]);
|
|
61
|
+
return /* @__PURE__ */ s("div", {
|
|
8
62
|
className: "space-y-6 p-6",
|
|
9
|
-
children: [/* @__PURE__ */
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
63
|
+
children: [/* @__PURE__ */ s("div", {
|
|
64
|
+
className: "flex flex-wrap items-start justify-between gap-3",
|
|
65
|
+
children: [/* @__PURE__ */ s("div", { children: [
|
|
66
|
+
/* @__PURE__ */ o("h1", {
|
|
67
|
+
className: "text-2xl font-semibold",
|
|
68
|
+
children: /* @__PURE__ */ o(l, { children: "Certificates" })
|
|
69
|
+
}),
|
|
70
|
+
/* @__PURE__ */ o("p", {
|
|
71
|
+
className: "text-sm text-text-secondary",
|
|
72
|
+
children: "Proof of completed training and validated skills"
|
|
73
|
+
}),
|
|
74
|
+
/* @__PURE__ */ o(d, {
|
|
75
|
+
className: "mt-1",
|
|
76
|
+
children: "View and download the certificates you've earned — proof of completed training and validated skills."
|
|
77
|
+
})
|
|
78
|
+
] }), /* @__PURE__ */ s(c, {
|
|
79
|
+
variant: "outline",
|
|
80
|
+
onClick: () => h("../learning/my-learning-page"),
|
|
81
|
+
children: [/* @__PURE__ */ o(i, { className: "mr-1 h-4 w-4" }), "My Learning"]
|
|
82
|
+
})]
|
|
83
|
+
}), y.length === 0 ? /* @__PURE__ */ o(u, {
|
|
23
84
|
illustration: "empty-generic",
|
|
24
85
|
title: "No certificates yet",
|
|
25
|
-
description: "Certificates are issued when a course
|
|
26
|
-
action: /* @__PURE__ */
|
|
86
|
+
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.",
|
|
87
|
+
action: /* @__PURE__ */ o(c, {
|
|
27
88
|
variant: "outline",
|
|
28
|
-
onClick: () =>
|
|
89
|
+
onClick: () => h("../learning/courses-page"),
|
|
29
90
|
children: "Browse courses"
|
|
30
91
|
})
|
|
92
|
+
}) : /* @__PURE__ */ o("div", {
|
|
93
|
+
className: "grid gap-4 md:grid-cols-2 lg:grid-cols-3",
|
|
94
|
+
children: y.map(({ course: e, completedAt: n }) => {
|
|
95
|
+
let r = `CF-${e.id.slice(-6).toUpperCase()}`, i = p(n);
|
|
96
|
+
return /* @__PURE__ */ o(t, {
|
|
97
|
+
title: e.title,
|
|
98
|
+
recipient: v,
|
|
99
|
+
date: i,
|
|
100
|
+
issuer: "CrewFoundry",
|
|
101
|
+
credentialId: r,
|
|
102
|
+
onDownload: () => m(e.title, v, i, r)
|
|
103
|
+
}, e.id);
|
|
104
|
+
})
|
|
31
105
|
})]
|
|
32
106
|
});
|
|
33
107
|
}
|
|
34
108
|
//#endregion
|
|
35
|
-
export {
|
|
109
|
+
export { h as LearningCertificatesPagePage };
|
|
36
110
|
|
|
37
111
|
//# sourceMappingURL=LearningCertificatesPagePage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LearningCertificatesPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/LearningCertificatesPagePage.tsx"],"sourcesContent":["import { Button
|
|
1
|
+
{"version":3,"file":"LearningCertificatesPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/LearningCertificatesPagePage.tsx"],"sourcesContent":["import { useMemo } from 'react';\nimport {\n Button,\n GradientText,\n IllustratedEmptyState,\n PagePurpose,\n} from '@burdenoff/fe-libs/ui';\nimport { GraduationCap } from 'lucide-react';\nimport { useNavigate } from 'react-router-dom';\nimport { useAuth } from '@burdenoff/fe-libs/shared/providers/shell';\nimport { useCrewFoundryStore } from '../store/crewfoundryStore';\nimport { CertificateCard } from '../components/viz';\nimport { readAllCourseProgress } from '../utils/courseProgress';\n\nfunction formatDate(iso?: string): string {\n if (!iso) return '—';\n const date = new Date(iso);\n if (Number.isNaN(date.getTime())) return '—';\n return date.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });\n}\n\nfunction downloadCertificate(course: string, recipient: string, dateLabel: string, id: string): void {\n if (typeof document === 'undefined') return;\n const esc = (s: string) =>\n s.replace(/[&<>\"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '\"': '"' })[c] ?? c);\n const html = `<!doctype html><html><head><meta charset=\"utf-8\"><title>Certificate — ${esc(course)}</title>\n<style>\n body{font-family:Georgia,'Times New Roman',serif;margin:0;padding:48px;text-align:center}\n .frame{border:6px double currentColor;padding:56px 40px;max-width:760px;margin:0 auto}\n .kicker{letter-spacing:.3em;text-transform:uppercase;font-size:12px;opacity:.7}\n h1{font-size:34px;margin:24px 0 8px}\n .recipient{font-size:26px;margin:20px 0}\n .rule{width:200px;border:0;border-top:1px solid currentColor;margin:24px auto;opacity:.4}\n .meta{font-size:13px;opacity:.7;margin-top:32px}\n</style></head><body>\n <div class=\"frame\">\n <div class=\"kicker\">CrewFoundry · Certificate of Completion</div>\n <h1>${esc(course)}</h1>\n <div class=\"kicker\">Awarded to</div>\n <div class=\"recipient\">${esc(recipient)}</div>\n <hr class=\"rule\" />\n <div class=\"meta\">Issued ${esc(dateLabel)} · 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've earned — proof of completed training and\n validated skills.\n </PagePurpose>\n </div>\n <Button variant=\"outline\" onClick={() => navigate('../learning/my-learning-page')}>\n <GraduationCap className=\"mr-1 h-4 w-4\" />\n My Learning\n </Button>\n </div>\n\n {earned.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-generic\"\n title=\"No certificates yet\"\n description=\"Certificates are issued when you complete a course. Finish a course from the player and every credential you earn will appear here with its issue date and a download link.\"\n action={\n <Button variant=\"outline\" onClick={() => navigate('../learning/courses-page')}>\n Browse courses\n </Button>\n }\n />\n ) : (\n <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-3\">\n {earned.map(({ course, completedAt }) => {\n const credentialId = `CF-${course.id.slice(-6).toUpperCase()}`;\n const dateLabel = formatDate(completedAt);\n return (\n <CertificateCard\n key={course.id}\n title={course.title}\n recipient={recipient}\n date={dateLabel}\n issuer=\"CrewFoundry\"\n credentialId={credentialId}\n onDownload={() =>\n downloadCertificate(course.title, recipient, dateLabel, credentialId)\n }\n />\n );\n })}\n </div>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;AAcA,SAAS,EAAW,GAAsB;AACxC,KAAI,CAAC,EAAK,QAAO;CACjB,IAAM,IAAO,IAAI,KAAK,EAAI;AAE1B,QADI,OAAO,MAAM,EAAK,SAAS,CAAC,GAAS,MAClC,EAAK,mBAAmB,KAAA,GAAW;EAAE,MAAM;EAAW,OAAO;EAAS,KAAK;EAAW,CAAC;;AAGhG,SAAS,EAAoB,GAAgB,GAAmB,GAAmB,GAAkB;AACnG,KAAI,OAAO,WAAa,IAAa;CACrC,IAAM,KAAO,MACX,EAAE,QAAQ,YAAY,OAAO;EAAE,KAAK;EAAS,KAAK;EAAQ,KAAK;EAAQ,MAAK;EAAU,EAAE,MAAM,EAAE,EAC5F,IAAO,yEAAyE,EAAI,EAAO,CAAC;;;;;;;;;;;;UAY1F,EAAI,EAAO,CAAC;;6BAEO,EAAI,EAAU,CAAC;;+BAEb,EAAI,EAAU,CAAC,gCAAgC,EAAI,EAAG,CAAC;;iBAG9E,IAAO,IAAI,KAAK,CAAC,EAAK,EAAE,EAAE,MAAM,aAAa,CAAC,EAC9C,IAAM,IAAI,gBAAgB,EAAK,EAC/B,IAAS,SAAS,cAAc,IAAI;AAM1C,CALA,EAAO,OAAO,GACd,EAAO,WAAW,2BAA2B,EAAG,QAChD,SAAS,KAAK,YAAY,EAAO,EACjC,EAAO,OAAO,EACd,EAAO,QAAQ,EACf,IAAI,gBAAgB,EAAI;;AAG1B,SAAgB,IAA+B;CAC7C,IAAM,IAAW,GAAa,EACxB,IAAO,GAAS,EAChB,EAAE,eAAY,GAAqB,EACnC,IAAY,EAAK,MAAM,MAAM,MAAM,IAAI,uBAEvC,IAAS,QAAc;EAC3B,IAAM,IAAc,GAAuB;AAC3C,SAAO,EACJ,KAAK,MAAW;GACf,IAAM,IAAQ,EAAY,EAAO,KAC3B,IAAmB,GAAO,UAAU,UAAU,GAC9C,IAAe,KAAK,IAAI,EAAO,SAAS,EAAE;AAEhD,UAAO;IAAE;IAAQ,YADE,IAAmB,KAAK,KAAoB;IAClC,aAAa,GAAO;IAAa;IAC9D,CACD,QAAQ,MAAS,EAAK,WAAW,CACjC,MAAM,GAAG,OAAO,EAAE,eAAe,IAAI,cAAc,EAAE,eAAe,GAAG,CAAC;IAC1E,CAAC,EAAQ,CAAC;AAEb,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD,EAAA,UAAA;IACE,kBAAC,MAAD;KAAI,WAAU;eACZ,kBAAC,GAAD,EAAA,UAAc,gBAA2B,CAAA;KACtC,CAAA;IACL,kBAAC,KAAD;KAAG,WAAU;eAA8B;KAEvC,CAAA;IACJ,kBAAC,GAAD;KAAa,WAAU;eAAO;KAGhB,CAAA;IACV,EAAA,CAAA,EACN,kBAAC,GAAD;IAAQ,SAAQ;IAAU,eAAe,EAAS,+BAA+B;cAAjF,CACE,kBAAC,GAAD,EAAe,WAAU,gBAAiB,CAAA,EAAA,cAEnC;MACL;MAEL,EAAO,WAAW,IACjB,kBAAC,GAAD;GACE,cAAa;GACb,OAAM;GACN,aAAY;GACZ,QACE,kBAAC,GAAD;IAAQ,SAAQ;IAAU,eAAe,EAAS,2BAA2B;cAAE;IAEtE,CAAA;GAEX,CAAA,GAEF,kBAAC,OAAD;GAAK,WAAU;aACZ,EAAO,KAAK,EAAE,WAAQ,qBAAkB;IACvC,IAAM,IAAe,MAAM,EAAO,GAAG,MAAM,GAAG,CAAC,aAAa,IACtD,IAAY,EAAW,EAAY;AACzC,WACE,kBAAC,GAAD;KAEE,OAAO,EAAO;KACH;KACX,MAAM;KACN,QAAO;KACO;KACd,kBACE,EAAoB,EAAO,OAAO,GAAW,GAAW,EAAa;KAEvE,EATK,EAAO,GASZ;KAEJ;GACE,CAAA,CAEJ"}
|
|
@@ -4,12 +4,16 @@ 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
5
|
import { useNavigate as l, useSearchParams as u } from "react-router-dom";
|
|
6
6
|
import { jsx as d, jsxs as f } from "react/jsx-runtime";
|
|
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 } from "@burdenoff/fe-libs/ui";
|
|
8
|
-
import { useGlobalUi as
|
|
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
|
+
import { useGlobalUi as T } from "@burdenoff/fe-libs/shared/providers/shell";
|
|
9
9
|
//#region src/crewfoundry/pages/LearningCourseDetailsPagePage.tsx
|
|
10
|
-
function
|
|
11
|
-
let
|
|
12
|
-
if (!
|
|
10
|
+
function E() {
|
|
11
|
+
let E = l(), [D] = u(), { courses: O, enrollCourse: k, initialized: A } = e(), { showToast: j } = T(), [M, N] = n(!1), P = D.get("id"), F = P ? O.find((e) => e.id === P) : O[0];
|
|
12
|
+
if (!A && !F) return /* @__PURE__ */ d("div", {
|
|
13
|
+
className: "flex min-h-[60vh] items-center justify-center p-6",
|
|
14
|
+
children: /* @__PURE__ */ d(w, { size: "lg" })
|
|
15
|
+
});
|
|
16
|
+
if (!F) return /* @__PURE__ */ d("div", {
|
|
13
17
|
className: "p-6",
|
|
14
18
|
children: /* @__PURE__ */ d(x, {
|
|
15
19
|
illustration: "empty-search",
|
|
@@ -17,7 +21,7 @@ function T() {
|
|
|
17
21
|
description: "This course is no longer available."
|
|
18
22
|
})
|
|
19
23
|
});
|
|
20
|
-
let
|
|
24
|
+
let I = F.enrolled > 0 ? Math.round(F.completed / F.enrolled * 100) : 0;
|
|
21
25
|
return /* @__PURE__ */ f("div", {
|
|
22
26
|
className: "space-y-6 p-6 max-w-4xl",
|
|
23
27
|
children: [
|
|
@@ -28,19 +32,19 @@ function T() {
|
|
|
28
32
|
className: "flex items-center gap-2 mb-1",
|
|
29
33
|
children: [/* @__PURE__ */ d("h1", {
|
|
30
34
|
className: "text-2xl font-semibold",
|
|
31
|
-
children: /* @__PURE__ */ d(b, { children:
|
|
35
|
+
children: /* @__PURE__ */ d(b, { children: F.title })
|
|
32
36
|
}), /* @__PURE__ */ d(p, {
|
|
33
|
-
variant:
|
|
34
|
-
children: t(
|
|
37
|
+
variant: F.level === "beginner" ? "secondary" : F.level === "intermediate" ? "default" : "destructive",
|
|
38
|
+
children: t(F.level)
|
|
35
39
|
})]
|
|
36
40
|
}),
|
|
37
41
|
/* @__PURE__ */ f("p", {
|
|
38
42
|
className: "text-sm text-text-secondary",
|
|
39
43
|
children: [
|
|
40
44
|
"by ",
|
|
41
|
-
|
|
45
|
+
F.instructor,
|
|
42
46
|
" · ",
|
|
43
|
-
|
|
47
|
+
F.category
|
|
44
48
|
]
|
|
45
49
|
}),
|
|
46
50
|
/* @__PURE__ */ d(C, {
|
|
@@ -49,17 +53,17 @@ function T() {
|
|
|
49
53
|
})
|
|
50
54
|
] }), /* @__PURE__ */ f(m, {
|
|
51
55
|
onClick: async () => {
|
|
52
|
-
|
|
56
|
+
N(!0);
|
|
53
57
|
try {
|
|
54
|
-
await
|
|
58
|
+
await k(F.id), j("Enrolled — your course is ready", "success"), E(`../learning/course-learning-page?id=${encodeURIComponent(F.id)}`);
|
|
55
59
|
} catch (e) {
|
|
56
|
-
|
|
60
|
+
j(e instanceof Error ? e.message : "Unable to enroll in course", "error");
|
|
57
61
|
} finally {
|
|
58
|
-
|
|
62
|
+
N(!1);
|
|
59
63
|
}
|
|
60
64
|
},
|
|
61
|
-
disabled:
|
|
62
|
-
children: [/* @__PURE__ */ d(o, { className: "h-4 w-4 mr-1" }),
|
|
65
|
+
disabled: M,
|
|
66
|
+
children: [/* @__PURE__ */ d(o, { className: "h-4 w-4 mr-1" }), M ? "Enrolling…" : "Start Course"]
|
|
63
67
|
})]
|
|
64
68
|
}),
|
|
65
69
|
/* @__PURE__ */ d(S, {
|
|
@@ -70,21 +74,21 @@ function T() {
|
|
|
70
74
|
label: "Start the first module",
|
|
71
75
|
description: "Begin learning and track your progress as you go.",
|
|
72
76
|
icon: /* @__PURE__ */ d(o, { className: "h-4 w-4" }),
|
|
73
|
-
onClick: () =>
|
|
77
|
+
onClick: () => E(`../learning/course-learning-page?id=${encodeURIComponent(F.id)}`)
|
|
74
78
|
},
|
|
75
79
|
{
|
|
76
80
|
id: "my-learning",
|
|
77
81
|
label: "See all your learning",
|
|
78
82
|
description: "Review everything you are currently enrolled in.",
|
|
79
83
|
icon: /* @__PURE__ */ d(a, { className: "h-4 w-4" }),
|
|
80
|
-
onClick: () =>
|
|
84
|
+
onClick: () => E("../learning/my-learning-page")
|
|
81
85
|
},
|
|
82
86
|
{
|
|
83
87
|
id: "browse",
|
|
84
88
|
label: "Browse more courses",
|
|
85
89
|
description: "Find related training to round out your skills.",
|
|
86
90
|
icon: /* @__PURE__ */ d(r, { className: "h-4 w-4" }),
|
|
87
|
-
onClick: () =>
|
|
91
|
+
onClick: () => E("../learning/courses-page")
|
|
88
92
|
}
|
|
89
93
|
]
|
|
90
94
|
}),
|
|
@@ -97,7 +101,7 @@ function T() {
|
|
|
97
101
|
/* @__PURE__ */ d(i, { className: "h-5 w-5 mx-auto text-text-secondary mb-1" }),
|
|
98
102
|
/* @__PURE__ */ d("p", {
|
|
99
103
|
className: "text-lg font-bold",
|
|
100
|
-
children:
|
|
104
|
+
children: F.duration
|
|
101
105
|
}),
|
|
102
106
|
/* @__PURE__ */ d("p", {
|
|
103
107
|
className: "text-xs text-text-secondary",
|
|
@@ -111,7 +115,7 @@ function T() {
|
|
|
111
115
|
/* @__PURE__ */ d(r, { className: "h-5 w-5 mx-auto text-text-secondary mb-1" }),
|
|
112
116
|
/* @__PURE__ */ d("p", {
|
|
113
117
|
className: "text-lg font-bold",
|
|
114
|
-
children:
|
|
118
|
+
children: F.modules
|
|
115
119
|
}),
|
|
116
120
|
/* @__PURE__ */ d("p", {
|
|
117
121
|
className: "text-xs text-text-secondary",
|
|
@@ -125,7 +129,7 @@ function T() {
|
|
|
125
129
|
/* @__PURE__ */ d(c, { className: "h-5 w-5 mx-auto text-text-secondary mb-1" }),
|
|
126
130
|
/* @__PURE__ */ d("p", {
|
|
127
131
|
className: "text-lg font-bold",
|
|
128
|
-
children:
|
|
132
|
+
children: F.enrolled
|
|
129
133
|
}),
|
|
130
134
|
/* @__PURE__ */ d("p", {
|
|
131
135
|
className: "text-xs text-text-secondary",
|
|
@@ -139,7 +143,7 @@ function T() {
|
|
|
139
143
|
/* @__PURE__ */ d(s, { className: "h-5 w-5 mx-auto text-status-warning-text mb-1" }),
|
|
140
144
|
/* @__PURE__ */ f("p", {
|
|
141
145
|
className: "text-lg font-bold",
|
|
142
|
-
children: [
|
|
146
|
+
children: [F.rating, "/5"]
|
|
143
147
|
}),
|
|
144
148
|
/* @__PURE__ */ d("p", {
|
|
145
149
|
className: "text-xs text-text-secondary",
|
|
@@ -156,11 +160,11 @@ function T() {
|
|
|
156
160
|
}),
|
|
157
161
|
/* @__PURE__ */ d("p", {
|
|
158
162
|
className: "text-sm text-text-secondary",
|
|
159
|
-
children:
|
|
163
|
+
children: F.description
|
|
160
164
|
}),
|
|
161
165
|
/* @__PURE__ */ d("div", {
|
|
162
166
|
className: "flex flex-wrap gap-1 mt-3",
|
|
163
|
-
children:
|
|
167
|
+
children: F.skills.map((e) => /* @__PURE__ */ d(p, {
|
|
164
168
|
variant: "outline",
|
|
165
169
|
children: e
|
|
166
170
|
}, e))
|
|
@@ -171,19 +175,19 @@ function T() {
|
|
|
171
175
|
children: [/* @__PURE__ */ f("div", {
|
|
172
176
|
className: "flex justify-between text-sm",
|
|
173
177
|
children: [/* @__PURE__ */ f("span", { children: [
|
|
174
|
-
|
|
178
|
+
F.completed,
|
|
175
179
|
" of ",
|
|
176
|
-
|
|
180
|
+
F.enrolled,
|
|
177
181
|
" students completed"
|
|
178
182
|
] }), /* @__PURE__ */ f("span", {
|
|
179
183
|
className: "font-medium",
|
|
180
|
-
children: [
|
|
184
|
+
children: [I, "%"]
|
|
181
185
|
})]
|
|
182
186
|
}), /* @__PURE__ */ d("div", {
|
|
183
187
|
className: "h-3 bg-bg-sunken rounded overflow-hidden",
|
|
184
188
|
children: /* @__PURE__ */ d("div", {
|
|
185
189
|
className: "h-3 bg-status-success-text rounded",
|
|
186
|
-
style: { width: `${
|
|
190
|
+
style: { width: `${I}%` }
|
|
187
191
|
})
|
|
188
192
|
})]
|
|
189
193
|
}) })] })
|
|
@@ -191,6 +195,6 @@ function T() {
|
|
|
191
195
|
});
|
|
192
196
|
}
|
|
193
197
|
//#endregion
|
|
194
|
-
export {
|
|
198
|
+
export { E as LearningCourseDetailsPagePage };
|
|
195
199
|
|
|
196
200
|
//# sourceMappingURL=LearningCourseDetailsPagePage.js.map
|
|
@@ -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} 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 } = 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 (!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 } 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'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-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'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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"}
|