@burdenoff/microfe-crewfoundry 2026.707.1 → 2026.707.2

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 (45) hide show
  1. package/dist/crewfoundry/components/viz/BarChart.js.map +1 -1
  2. package/dist/crewfoundry/components/viz/CertificateCard.js.map +1 -1
  3. package/dist/crewfoundry/components/viz/DonutChart.js.map +1 -1
  4. package/dist/crewfoundry/components/viz/Heatmap.js.map +1 -1
  5. package/dist/crewfoundry/components/viz/LineChart.js.map +1 -1
  6. package/dist/crewfoundry/components/viz/PathSteps.js.map +1 -1
  7. package/dist/crewfoundry/components/viz/RatingStars.js.map +1 -1
  8. package/dist/crewfoundry/components/viz/SegmentedTabs.js.map +1 -1
  9. package/dist/crewfoundry/components/viz/StatCard.js.map +1 -1
  10. package/dist/crewfoundry/components/viz/lib/Avatar.js.map +1 -1
  11. package/dist/crewfoundry/components/viz/lib/CartesianFrame.js.map +1 -1
  12. package/dist/crewfoundry/components/viz/lib/geometry.js.map +1 -1
  13. package/dist/crewfoundry/lib/analytics.js.map +1 -1
  14. package/dist/crewfoundry/pages/AnalyticsAnalyticsPagePage.js.map +1 -1
  15. package/dist/crewfoundry/pages/AnalyticsEmployeeAnalyticsPagePage.js.map +1 -1
  16. package/dist/crewfoundry/pages/AnalyticsFinancialAnalyticsPagePage.js.map +1 -1
  17. package/dist/crewfoundry/pages/AnalyticsHiringAnalyticsPagePage.js.map +1 -1
  18. package/dist/crewfoundry/pages/CommunicationsMessagesPagePage.js +2 -1
  19. package/dist/crewfoundry/pages/CommunicationsMessagesPagePage.js.map +1 -1
  20. package/dist/crewfoundry/pages/CompanyClientManagementPagePage.js.map +1 -1
  21. package/dist/crewfoundry/pages/DashboardDashboardPagePage.js.map +1 -1
  22. package/dist/crewfoundry/pages/HiringApplicationsPagePage.js.map +1 -1
  23. package/dist/crewfoundry/pages/HiringCandidatesPagePage.js.map +1 -1
  24. package/dist/crewfoundry/pages/HiringCreateJobPagePage.js.map +1 -1
  25. package/dist/crewfoundry/pages/HiringJobDetailsPagePage.js.map +1 -1
  26. package/dist/crewfoundry/pages/HiringJobsPagePage.js.map +1 -1
  27. package/dist/crewfoundry/pages/HrDepartmentsPagePage.js +8 -4
  28. package/dist/crewfoundry/pages/HrDepartmentsPagePage.js.map +1 -1
  29. package/dist/crewfoundry/pages/HrLeaveManagementPagePage.js.map +1 -1
  30. package/dist/crewfoundry/pages/HrPayrollPagePage.js.map +1 -1
  31. package/dist/crewfoundry/pages/HrTimeTrackingPagePage.js.map +1 -1
  32. package/dist/crewfoundry/pages/LearningCertificatesPagePage.js.map +1 -1
  33. package/dist/crewfoundry/pages/LearningCourseLearningPagePage.js.map +1 -1
  34. package/dist/crewfoundry/pages/LearningCoursesPagePage.js.map +1 -1
  35. package/dist/crewfoundry/pages/LearningLearningPathsPagePage.js.map +1 -1
  36. package/dist/crewfoundry/pages/LearningMyLearningPagePage.js.map +1 -1
  37. package/dist/crewfoundry/pages/MarketplaceMarketplacePagePage.js.map +1 -1
  38. package/dist/crewfoundry/pages/MarketplaceServiceDetailsPagePage.js.map +1 -1
  39. package/dist/crewfoundry/pages/PaymentsInvoicesPagePage.js.map +1 -1
  40. package/dist/crewfoundry/pages/ProjectsProjectsPagePage.js.map +1 -1
  41. package/dist/crewfoundry/pages/ProjectsResourcingPagePage.js.map +1 -1
  42. package/dist/crewfoundry/pages/TeamsTeamDetailPagePage.js.map +1 -1
  43. package/dist/crewfoundry/pages/TeamsTeamsPagePage.js.map +1 -1
  44. package/dist/crewfoundry/utils/courseProgress.js.map +1 -1
  45. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"TeamsTeamDetailPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/TeamsTeamDetailPagePage.tsx"],"sourcesContent":["import {\n Badge,\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n EmphasisPanel,\n GradientText,\n IllustratedEmptyState,\n NextSteps,\n PagePurpose,\n ResponsiveTable,\n Spinner,\n type ResponsiveTableColumn,\n} from '@burdenoff/fe-libs/ui';\nimport { BarChart3, Calendar, FolderKanban, Settings, Star, Users } from 'lucide-react';\nimport { useNavigate, useSearchParams } from 'react-router-dom';\nimport { useCrewFoundryStore, type Employee } from '../store/crewfoundryStore';\nimport { formatEnumLabel } from '../utils/formatEnumLabel';\nimport { AvatarStack, getInitials, type Person } from '../components/viz';\n\nfunction InitialsAvatar({ name, src, size = 32 }: { name: string; src?: string; size?: number }) {\n if (src) {\n return (\n <img\n src={src}\n alt={name}\n width={size}\n height={size}\n loading=\"lazy\"\n className=\"shrink-0 rounded-full object-cover\"\n style={{ width: size, height: size }}\n />\n );\n }\n return (\n <span\n aria-hidden=\"true\"\n className=\"inline-flex shrink-0 items-center justify-center rounded-full bg-bg-elevated font-medium text-text-secondary\"\n style={{ width: size, height: size, fontSize: Math.round(size * 0.36) }}\n >\n {getInitials(name)}\n </span>\n );\n}\n\nexport function TeamsTeamDetailPagePage() {\n const navigate = useNavigate();\n const [searchParams] = useSearchParams();\n const { teams, employees, projects, initialized } = useCrewFoundryStore();\n const teamId = searchParams.get('id');\n const team = teams.find((t) => t.id === teamId) ?? teams[0];\n if (!initialized && !team) {\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 (!team)\n return (\n <div className=\"p-6\">\n <IllustratedEmptyState\n illustration=\"empty-search\"\n title=\"Team not found\"\n description=\"This team is no longer available.\"\n />\n </div>\n );\n const teamMembers = employees.filter((e) => team.members.includes(e.id));\n const teamProjects = projects.filter((p) => p.teamId === team.id);\n const activeProjectCount = teamProjects.filter((p) => p.status === 'active').length;\n const lead = employees.find((e) => e.id === team.leadId);\n const memberPeople: Person[] = teamMembers.map((e) => ({\n name: `${e.firstName} ${e.lastName}`.trim(),\n avatarUrl: e.avatar,\n role: e.position,\n }));\n\n const memberColumns: ResponsiveTableColumn<Employee>[] = [\n {\n key: 'name',\n header: 'Name',\n priority: 'primary',\n cell: (emp) => (\n <span className=\"flex items-center gap-2.5\">\n <InitialsAvatar name={`${emp.firstName} ${emp.lastName}`} src={emp.avatar} size={30} />\n <span className=\"font-medium text-text-primary\">\n {emp.firstName} {emp.lastName}\n </span>\n </span>\n ),\n },\n {\n key: 'position',\n header: 'Position',\n priority: 'secondary',\n cell: (emp) => <span className=\"text-text-secondary\">{emp.position}</span>,\n },\n {\n key: 'performance',\n header: 'Performance',\n priority: 'secondary',\n cell: (emp) => (\n <span className=\"flex items-center gap-1 md:justify-center\">\n <Star className=\"h-3 w-3 text-status-warning-text\" />\n {emp.performanceScore}\n </span>\n ),\n },\n {\n key: 'status',\n header: 'Status',\n priority: 'primary',\n cell: (emp) => (\n <Badge variant={emp.status === 'active' ? 'default' : 'secondary'} className=\"text-[10px]\">\n {formatEnumLabel(emp.status)}\n </Badge>\n ),\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 <h1 className=\"text-2xl font-semibold\">\n <GradientText>{team.name}</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">{team.description}</p>\n <PagePurpose className=\"mt-2\">\n A complete view of one team — who&apos;s on it, what they&apos;re working on, and how\n they&apos;re performing — so you can support the group and keep its projects on track.\n </PagePurpose>\n <div className=\"mt-3 flex flex-wrap items-center gap-x-4 gap-y-2\">\n <span className=\"flex items-center gap-2 text-xs text-text-secondary\">\n Led by\n <InitialsAvatar name={lead ? `${lead.firstName} ${lead.lastName}` : team.leadName} src={lead?.avatar} size={22} />\n <span className=\"font-medium text-text-primary\">\n {lead ? `${lead.firstName} ${lead.lastName}` : team.leadName || '—'}\n </span>\n </span>\n {memberPeople.length > 0 ? <AvatarStack people={memberPeople} max={6} size={26} /> : null}\n </div>\n </div>\n <Badge variant={team.status === 'active' ? 'default' : 'secondary'}>\n {formatEnumLabel(team.status)}\n </Badge>\n </div>\n\n <NextSteps\n storageKey=\"crewfoundry-team-details\"\n steps={[\n {\n id: 'analytics',\n label: 'View team analytics',\n description: 'See productivity and workload trends for this team.',\n icon: <BarChart3 className=\"h-4 w-4\" />,\n onClick: () => navigate('../teams/team-analytics-page'),\n },\n {\n id: 'settings',\n label: 'Adjust team settings',\n description: 'Manage membership, permissions, and preferences.',\n icon: <Settings className=\"h-4 w-4\" />,\n onClick: () =>\n navigate(`../teams/team-settings-page?id=${encodeURIComponent(team.id)}`),\n },\n {\n id: 'directory',\n label: 'Browse all teams',\n description: 'Explore how the rest of the organization is structured.',\n icon: <Users className=\"h-4 w-4\" />,\n onClick: () => navigate('../teams/team-directory-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 <Users className=\"h-5 w-5 mx-auto text-text-secondary mb-1\" />\n <p className=\"text-lg font-bold\">{team.memberCount}</p>\n <p className=\"text-xs text-text-secondary\">Members</p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"p-4 text-center\">\n <FolderKanban className=\"h-5 w-5 mx-auto text-text-secondary mb-1\" />\n <p className=\"text-lg font-bold\">\n {activeProjectCount}\n <span className=\"text-sm font-normal text-text-secondary\">/{teamProjects.length}</span>\n </p>\n <p className=\"text-xs text-text-secondary\">Active Projects</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\">{team.leadName}</p>\n <p className=\"text-xs text-text-secondary\">Lead</p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"p-4 text-center\">\n <Calendar className=\"h-5 w-5 mx-auto text-text-secondary mb-1\" />\n <p className=\"text-lg font-bold\">{team.createdAt}</p>\n <p className=\"text-xs text-text-secondary\">Created</p>\n </CardContent>\n </Card>\n </div>\n <EmphasisPanel>\n <div className=\"mb-2 text-lg font-semibold\">Team Members</div>\n {teamMembers.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-people\"\n title=\"No members yet\"\n description=\"Add people to this team to start collaborating.\"\n />\n ) : (\n <ResponsiveTable columns={memberColumns} data={teamMembers} rowKey={(emp) => emp.id} />\n )}\n </EmphasisPanel>\n {teamProjects.length > 0 && (\n <Card>\n <CardHeader>\n <CardTitle>Team Projects</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {teamProjects.map((p) => (\n <div\n key={p.id}\n role=\"button\"\n tabIndex={0}\n aria-label={`Open ${p.name} project detail`}\n onClick={() =>\n navigate(`../projects/project-details-page?id=${encodeURIComponent(p.id)}`)\n }\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n navigate(`../projects/project-details-page?id=${encodeURIComponent(p.id)}`);\n }\n }}\n className=\"flex items-center justify-between rounded border p-3 cursor-pointer hover:bg-bg-sunken/50 focus-visible:outline-none focus-visible:shadow-[var(--shadow-focus)]\"\n >\n <div>\n <p className=\"text-sm font-medium\">{p.name}</p>\n <p className=\"text-xs text-text-secondary\">\n {p.startDate} — {p.endDate}\n </p>\n </div>\n <div className=\"flex items-center gap-2\">\n <div className=\"w-24 h-2 bg-bg-sunken rounded overflow-hidden\">\n <div\n className=\"h-2 bg-action-primary-bg rounded\"\n style={{ width: `${p.progress}%` }}\n />\n </div>\n <span className=\"text-xs\">{p.progress}%</span>\n <Badge\n variant={p.status === 'active' ? 'default' : 'secondary'}\n className=\"text-[10px]\"\n >\n {formatEnumLabel(p.status)}\n </Badge>\n </div>\n </div>\n ))}\n </CardContent>\n </Card>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"TeamsTeamDetailPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/TeamsTeamDetailPagePage.tsx"],"sourcesContent":["import {\n Badge,\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n EmphasisPanel,\n GradientText,\n IllustratedEmptyState,\n NextSteps,\n PagePurpose,\n ResponsiveTable,\n Spinner,\n type ResponsiveTableColumn,\n} from '@burdenoff/fe-libs/ui';\nimport { BarChart3, Calendar, FolderKanban, Settings, Star, Users } from 'lucide-react';\nimport { useNavigate, useSearchParams } from 'react-router-dom';\nimport { useCrewFoundryStore, type Employee } from '../store/crewfoundryStore';\nimport { formatEnumLabel } from '../utils/formatEnumLabel';\nimport { AvatarStack, getInitials, type Person } from '../components/viz';\n\nfunction InitialsAvatar({ name, src, size = 32 }: { name: string; src?: string; size?: number }) {\n if (src) {\n return (\n <img\n src={src}\n alt={name}\n width={size}\n height={size}\n loading=\"lazy\"\n className=\"shrink-0 rounded-full object-cover\"\n style={{ width: size, height: size }}\n />\n );\n }\n return (\n <span\n aria-hidden=\"true\"\n className=\"inline-flex shrink-0 items-center justify-center rounded-full bg-bg-elevated font-medium text-text-secondary\"\n style={{ width: size, height: size, fontSize: Math.round(size * 0.36) }}\n >\n {getInitials(name)}\n </span>\n );\n}\n\nexport function TeamsTeamDetailPagePage() {\n const navigate = useNavigate();\n const [searchParams] = useSearchParams();\n const { teams, employees, projects, initialized } = useCrewFoundryStore();\n const teamId = searchParams.get('id');\n const team = teams.find((t) => t.id === teamId) ?? teams[0];\n if (!initialized && !team) {\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 (!team)\n return (\n <div className=\"p-6\">\n <IllustratedEmptyState\n illustration=\"empty-search\"\n title=\"Team not found\"\n description=\"This team is no longer available.\"\n />\n </div>\n );\n const teamMembers = employees.filter((e) => team.members.includes(e.id));\n const teamProjects = projects.filter((p) => p.teamId === team.id);\n const activeProjectCount = teamProjects.filter((p) => p.status === 'active').length;\n const lead = employees.find((e) => e.id === team.leadId);\n const memberPeople: Person[] = teamMembers.map((e) => ({\n name: `${e.firstName} ${e.lastName}`.trim(),\n avatarUrl: e.avatar,\n role: e.position,\n }));\n\n const memberColumns: ResponsiveTableColumn<Employee>[] = [\n {\n key: 'name',\n header: 'Name',\n priority: 'primary',\n cell: (emp) => (\n <span className=\"flex items-center gap-2.5\">\n <InitialsAvatar name={`${emp.firstName} ${emp.lastName}`} src={emp.avatar} size={30} />\n <span className=\"font-medium text-text-primary\">\n {emp.firstName} {emp.lastName}\n </span>\n </span>\n ),\n },\n {\n key: 'position',\n header: 'Position',\n priority: 'secondary',\n cell: (emp) => <span className=\"text-text-secondary\">{emp.position}</span>,\n },\n {\n key: 'performance',\n header: 'Performance',\n priority: 'secondary',\n cell: (emp) => (\n <span className=\"flex items-center gap-1 md:justify-center\">\n <Star className=\"h-3 w-3 text-status-warning-text\" />\n {emp.performanceScore}\n </span>\n ),\n },\n {\n key: 'status',\n header: 'Status',\n priority: 'primary',\n cell: (emp) => (\n <Badge variant={emp.status === 'active' ? 'default' : 'secondary'} className=\"text-[10px]\">\n {formatEnumLabel(emp.status)}\n </Badge>\n ),\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 <h1 className=\"text-2xl font-semibold\">\n <GradientText>{team.name}</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">{team.description}</p>\n <PagePurpose className=\"mt-2\">\n A complete view of one team — who&apos;s on it, what they&apos;re working on, and how\n they&apos;re performing — so you can support the group and keep its projects on track.\n </PagePurpose>\n <div className=\"mt-3 flex flex-wrap items-center gap-x-4 gap-y-2\">\n <span className=\"flex items-center gap-2 text-xs text-text-secondary\">\n Led by\n <InitialsAvatar\n name={lead ? `${lead.firstName} ${lead.lastName}` : team.leadName}\n src={lead?.avatar}\n size={22}\n />\n <span className=\"font-medium text-text-primary\">\n {lead ? `${lead.firstName} ${lead.lastName}` : team.leadName || '—'}\n </span>\n </span>\n {memberPeople.length > 0 ? (\n <AvatarStack people={memberPeople} max={6} size={26} />\n ) : null}\n </div>\n </div>\n <Badge variant={team.status === 'active' ? 'default' : 'secondary'}>\n {formatEnumLabel(team.status)}\n </Badge>\n </div>\n\n <NextSteps\n storageKey=\"crewfoundry-team-details\"\n steps={[\n {\n id: 'analytics',\n label: 'View team analytics',\n description: 'See productivity and workload trends for this team.',\n icon: <BarChart3 className=\"h-4 w-4\" />,\n onClick: () => navigate('../teams/team-analytics-page'),\n },\n {\n id: 'settings',\n label: 'Adjust team settings',\n description: 'Manage membership, permissions, and preferences.',\n icon: <Settings className=\"h-4 w-4\" />,\n onClick: () =>\n navigate(`../teams/team-settings-page?id=${encodeURIComponent(team.id)}`),\n },\n {\n id: 'directory',\n label: 'Browse all teams',\n description: 'Explore how the rest of the organization is structured.',\n icon: <Users className=\"h-4 w-4\" />,\n onClick: () => navigate('../teams/team-directory-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 <Users className=\"h-5 w-5 mx-auto text-text-secondary mb-1\" />\n <p className=\"text-lg font-bold\">{team.memberCount}</p>\n <p className=\"text-xs text-text-secondary\">Members</p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"p-4 text-center\">\n <FolderKanban className=\"h-5 w-5 mx-auto text-text-secondary mb-1\" />\n <p className=\"text-lg font-bold\">\n {activeProjectCount}\n <span className=\"text-sm font-normal text-text-secondary\">\n /{teamProjects.length}\n </span>\n </p>\n <p className=\"text-xs text-text-secondary\">Active Projects</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\">{team.leadName}</p>\n <p className=\"text-xs text-text-secondary\">Lead</p>\n </CardContent>\n </Card>\n <Card>\n <CardContent className=\"p-4 text-center\">\n <Calendar className=\"h-5 w-5 mx-auto text-text-secondary mb-1\" />\n <p className=\"text-lg font-bold\">{team.createdAt}</p>\n <p className=\"text-xs text-text-secondary\">Created</p>\n </CardContent>\n </Card>\n </div>\n <EmphasisPanel>\n <div className=\"mb-2 text-lg font-semibold\">Team Members</div>\n {teamMembers.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-people\"\n title=\"No members yet\"\n description=\"Add people to this team to start collaborating.\"\n />\n ) : (\n <ResponsiveTable columns={memberColumns} data={teamMembers} rowKey={(emp) => emp.id} />\n )}\n </EmphasisPanel>\n {teamProjects.length > 0 && (\n <Card>\n <CardHeader>\n <CardTitle>Team Projects</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n {teamProjects.map((p) => (\n <div\n key={p.id}\n role=\"button\"\n tabIndex={0}\n aria-label={`Open ${p.name} project detail`}\n onClick={() =>\n navigate(`../projects/project-details-page?id=${encodeURIComponent(p.id)}`)\n }\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n navigate(`../projects/project-details-page?id=${encodeURIComponent(p.id)}`);\n }\n }}\n className=\"flex items-center justify-between rounded border p-3 cursor-pointer hover:bg-bg-sunken/50 focus-visible:outline-none focus-visible:shadow-[var(--shadow-focus)]\"\n >\n <div>\n <p className=\"text-sm font-medium\">{p.name}</p>\n <p className=\"text-xs text-text-secondary\">\n {p.startDate} — {p.endDate}\n </p>\n </div>\n <div className=\"flex items-center gap-2\">\n <div className=\"w-24 h-2 bg-bg-sunken rounded overflow-hidden\">\n <div\n className=\"h-2 bg-action-primary-bg rounded\"\n style={{ width: `${p.progress}%` }}\n />\n </div>\n <span className=\"text-xs\">{p.progress}%</span>\n <Badge\n variant={p.status === 'active' ? 'default' : 'secondary'}\n className=\"text-[10px]\"\n >\n {formatEnumLabel(p.status)}\n </Badge>\n </div>\n </div>\n ))}\n </CardContent>\n </Card>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"TeamsTeamsPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/TeamsTeamsPagePage.tsx"],"sourcesContent":["import {\n Badge,\n Button,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n GlassCard,\n GradientText,\n IllustratedEmptyState,\n PagePurpose,\n Pagination,\n} from '@burdenoff/fe-libs/ui';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { usePagination } from '@burdenoff/fe-libs/shared/hooks';\nimport { ArrowRight, FolderKanban, Plus, Users } from 'lucide-react';\nimport { useNavigate } from 'react-router-dom';\nimport { useMemo, useState } from 'react';\nimport {\n FormModal,\n useFormModal,\n type FieldConfig,\n type FormValues,\n} from '../components/forms/FormModal';\nimport {\n useCrewFoundryStore,\n type Employee,\n type Team,\n} from '../store/crewfoundryStore';\nimport { formatEnumLabel } from '../utils/formatEnumLabel';\nimport { AvatarStack, getInitials, type Person } from '../components/viz';\n\nconst TEAM_FIELDS: FieldConfig[] = [\n { name: 'name', label: 'Team Name', type: 'text', required: true },\n { name: 'department', label: 'Department', type: 'text', placeholder: 'e.g. Engineering' },\n { name: 'leadName', label: 'Team Lead', type: 'text', placeholder: 'Lead full name' },\n {\n name: 'status',\n label: 'Status',\n type: 'select',\n options: [\n { label: 'Active', value: 'active' },\n { label: 'Forming', value: 'forming' },\n ],\n },\n { name: 'description', label: 'Description', type: 'textarea', colSpan: 2 },\n];\n\nfunction InitialsAvatar({ name, src, size = 32 }: { name: string; src?: string; size?: number }) {\n if (src) {\n return (\n <img\n src={src}\n alt={name}\n width={size}\n height={size}\n loading=\"lazy\"\n className=\"shrink-0 rounded-full object-cover\"\n style={{ width: size, height: size }}\n />\n );\n }\n return (\n <span\n aria-hidden=\"true\"\n className=\"inline-flex shrink-0 items-center justify-center rounded-full bg-bg-elevated font-medium text-text-secondary\"\n style={{ width: size, height: size, fontSize: Math.round(size * 0.36) }}\n >\n {getInitials(name)}\n </span>\n );\n}\n\nfunction employeeName(employee: Employee): string {\n return `${employee.firstName} ${employee.lastName}`.trim();\n}\n\nexport function TeamsTeamsPagePage() {\n const { t: translate } = useI18n();\n const navigate = useNavigate();\n const { teams, employees, projects } = useCrewFoundryStore();\n const addTeam = useCrewFoundryStore((state) => state.addTeam);\n const createModal = useFormModal();\n const { page, pageSize, setPage } = usePagination({ initialPageSize: 12 });\n const [selectedId, setSelectedId] = useState<string | null>(null);\n const t = (key: string, fallback: string) => {\n const value = translate(key);\n return value === key ? fallback : value;\n };\n\n const employeeById = useMemo(\n () => new Map(employees.map((employee) => [employee.id, employee])),\n [employees]\n );\n\n const activeProjectsByTeam = useMemo(() => {\n const map = new Map<string, number>();\n for (const project of projects) {\n if (project.status === 'active') {\n map.set(project.teamId, (map.get(project.teamId) ?? 0) + 1);\n }\n }\n return map;\n }, [projects]);\n\n const membersOf = (team: Team): Employee[] =>\n team.members\n .map((memberId) => employeeById.get(memberId))\n .filter((employee): employee is Employee => employee !== undefined);\n\n const peopleOf = (team: Team): Person[] =>\n membersOf(team).map((employee) => ({\n name: employeeName(employee),\n avatarUrl: employee.avatar,\n role: employee.position,\n }));\n\n const pageTeams = teams.slice((page - 1) * pageSize, (page - 1) * pageSize + pageSize);\n const selectedTeam =\n teams.find((team) => team.id === selectedId) ?? pageTeams[0] ?? teams[0] ?? null;\n const selectedMembers = selectedTeam ? membersOf(selectedTeam) : [];\n const selectedActiveProjects = selectedTeam\n ? projects.filter((project) => project.teamId === selectedTeam.id && project.status === 'active')\n : [];\n\n const openTeam = (team: Team) =>\n navigate(`../teams/team-detail-page?id=${encodeURIComponent(team.id)}`);\n\n const handleCreate = async (values: FormValues) => {\n // CreateCrewTeamInput accepts only this field subset; the store spreads the\n // object straight into `input`, so we send exactly the accepted shape and\n // let the server response replace the optimistic row.\n const payload: Partial<Team> = {\n name: String(values.name ?? '').trim(),\n description: String(values.description ?? '').trim(),\n department: String(values.department ?? '').trim() || 'operations',\n leadName: String(values.leadName ?? '').trim(),\n status: (String(values.status ?? 'forming') as Team['status']) || 'forming',\n members: [],\n };\n await addTeam(payload as Team);\n };\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex items-start justify-between gap-4\">\n <div>\n <h1 className=\"text-2xl font-semibold\">\n <GradientText>{t('crewfoundry.teams.title', 'Teams')}</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">\n {teams.length} {t('crewfoundry.teams.summary.count', 'teams across the organization')}\n </p>\n <PagePurpose className=\"mt-1\">\n Manage your teams — create groups, assign members, and organize people around the work\n they do together.\n </PagePurpose>\n </div>\n <Button size=\"sm\" onClick={() => createModal.openModal()}>\n <Plus className=\"h-4 w-4 mr-1\" />\n {t('crewfoundry.teams.actions.createTeam', 'Create Team')}\n </Button>\n </div>\n\n {teams.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-people\"\n title=\"No teams yet\"\n description=\"Create a team to organize people around their work.\"\n />\n ) : (\n <div className=\"grid gap-6 lg:grid-cols-3\">\n {/* Team cards */}\n <div className=\"space-y-4 lg:col-span-2\">\n <div className=\"grid gap-4 sm:grid-cols-2\">\n {pageTeams.map((team) => {\n const lead = team.leadId ? employeeById.get(team.leadId) : undefined;\n const activeCount = activeProjectsByTeam.get(team.id) ?? 0;\n const selected = selectedTeam?.id === team.id;\n return (\n <GlassCard\n key={team.id}\n treatment={selected ? 'glass' : 'plain'}\n glow={selected}\n role=\"button\"\n tabIndex={0}\n aria-pressed={selected}\n aria-label={`Select ${team.name} team`}\n onClick={() => setSelectedId(team.id)}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n setSelectedId(team.id);\n }\n }}\n className=\"cursor-pointer transition-shadow hover:shadow-md focus-visible:shadow-[var(--shadow-focus)] focus-visible:outline-none\"\n >\n <CardHeader className=\"pb-3\">\n <div className=\"flex items-start justify-between gap-2\">\n <CardTitle className=\"text-base\">{team.name}</CardTitle>\n <Badge\n variant={team.status === 'active' ? 'default' : 'secondary'}\n className=\"text-[10px]\"\n >\n {formatEnumLabel(team.status)}\n </Badge>\n </div>\n <div className=\"flex items-center gap-2 text-xs text-text-secondary\">\n <span>{t('crewfoundry.teams.card.ledBy', 'Led by')}</span>\n <InitialsAvatar\n name={lead ? employeeName(lead) : team.leadName}\n src={lead?.avatar}\n size={20}\n />\n <span className=\"font-medium text-text-primary\">\n {lead ? employeeName(lead) : team.leadName || '—'}\n </span>\n </div>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n <AvatarStack people={peopleOf(team)} max={5} size={30} />\n <div className=\"flex items-center gap-4 text-sm text-text-secondary\">\n <span className=\"flex items-center gap-1\">\n <Users className=\"h-3.5 w-3.5\" />\n {team.memberCount} {t('crewfoundry.teams.card.members', 'Members')}\n </span>\n <span className=\"flex items-center gap-1\">\n <FolderKanban className=\"h-3.5 w-3.5\" />\n {activeCount}{' '}\n {t('crewfoundry.teams.card.activeProjects', 'Active Projects')}\n </span>\n </div>\n </CardContent>\n </GlassCard>\n );\n })}\n </div>\n\n {teams.length > pageSize && (\n <Pagination\n page={page}\n pageSize={pageSize}\n totalItems={teams.length}\n onPageChange={setPage}\n />\n )}\n </div>\n\n {/* Selected-team detail panel */}\n <div className=\"lg:col-span-1\">\n {selectedTeam ? (\n <GlassCard\n treatment=\"glass\"\n className=\"border-border-seam lg:sticky lg:top-6\"\n aria-live=\"polite\"\n >\n <CardHeader className=\"pb-3\">\n <div className=\"flex items-start justify-between gap-2\">\n <CardTitle className=\"text-lg\">{selectedTeam.name}</CardTitle>\n <Badge\n variant={selectedTeam.status === 'active' ? 'default' : 'secondary'}\n className=\"text-[10px]\"\n >\n {formatEnumLabel(selectedTeam.status)}\n </Badge>\n </div>\n <CardDescription>{selectedTeam.description}</CardDescription>\n </CardHeader>\n <CardContent className=\"space-y-5\">\n <section>\n <h3 className=\"mb-2 text-sm font-semibold\">\n {t('crewfoundry.teams.panel.members', 'Members')} ({selectedMembers.length})\n </h3>\n {selectedMembers.length === 0 ? (\n <p className=\"text-xs text-text-secondary\">\n No members have been added to this team yet.\n </p>\n ) : (\n <ul className=\"space-y-2 max-h-72 overflow-y-auto pr-1\">\n {selectedMembers.map((member) => (\n <li key={member.id} className=\"flex items-center gap-2.5\">\n <InitialsAvatar\n name={employeeName(member)}\n src={member.avatar}\n size={32}\n />\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {employeeName(member)}\n {member.id === selectedTeam.leadId ? (\n <span className=\"ml-1.5 text-[10px] font-medium text-action-primary-text\">\n · Lead\n </span>\n ) : null}\n </p>\n <p className=\"truncate text-xs text-text-secondary\">\n {member.position}\n </p>\n </div>\n </li>\n ))}\n </ul>\n )}\n </section>\n\n <section>\n <h3 className=\"mb-2 text-sm font-semibold\">\n {t('crewfoundry.teams.panel.activeProjects', 'Active Projects')} (\n {selectedActiveProjects.length})\n </h3>\n {selectedActiveProjects.length === 0 ? (\n <p className=\"text-xs text-text-secondary\">\n No active projects for this team right now.\n </p>\n ) : (\n <ul className=\"space-y-3\">\n {selectedActiveProjects.map((project) => (\n <li key={project.id}>\n <div className=\"mb-1 flex items-center justify-between gap-2 text-xs\">\n <span className=\"truncate font-medium text-text-primary\">\n {project.name}\n </span>\n <span className=\"shrink-0 tabular-nums text-text-secondary\">\n {project.progress}%\n </span>\n </div>\n <div className=\"h-1.5 overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-1.5 rounded-full bg-action-primary-bg\"\n style={{ width: `${project.progress}%` }}\n />\n </div>\n </li>\n ))}\n </ul>\n )}\n </section>\n\n <Button\n variant=\"outline\"\n size=\"sm\"\n className=\"w-full\"\n onClick={() => openTeam(selectedTeam)}\n >\n {t('crewfoundry.teams.panel.openDetail', 'Open full team detail')}\n <ArrowRight className=\"ml-1 h-4 w-4\" />\n </Button>\n </CardContent>\n </GlassCard>\n ) : null}\n </div>\n </div>\n )}\n\n <FormModal\n open={createModal.open}\n onOpenChange={createModal.onOpenChange}\n title={t('crewfoundry.teams.actions.createTeam', 'Create Team')}\n description=\"Organize people around the work they do together.\"\n fields={TEAM_FIELDS}\n submitLabel=\"Create Team\"\n onSubmit={handleCreate}\n />\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"TeamsTeamsPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/TeamsTeamsPagePage.tsx"],"sourcesContent":["import {\n Badge,\n Button,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n GlassCard,\n GradientText,\n IllustratedEmptyState,\n PagePurpose,\n Pagination,\n} from '@burdenoff/fe-libs/ui';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { usePagination } from '@burdenoff/fe-libs/shared/hooks';\nimport { ArrowRight, FolderKanban, Plus, Users } from 'lucide-react';\nimport { useNavigate } from 'react-router-dom';\nimport { useMemo, useState } from 'react';\nimport {\n FormModal,\n useFormModal,\n type FieldConfig,\n type FormValues,\n} from '../components/forms/FormModal';\nimport { useCrewFoundryStore, type Employee, type Team } from '../store/crewfoundryStore';\nimport { formatEnumLabel } from '../utils/formatEnumLabel';\nimport { AvatarStack, getInitials, type Person } from '../components/viz';\n\nconst TEAM_FIELDS: FieldConfig[] = [\n { name: 'name', label: 'Team Name', type: 'text', required: true },\n { name: 'department', label: 'Department', type: 'text', placeholder: 'e.g. Engineering' },\n { name: 'leadName', label: 'Team Lead', type: 'text', placeholder: 'Lead full name' },\n {\n name: 'status',\n label: 'Status',\n type: 'select',\n options: [\n { label: 'Active', value: 'active' },\n { label: 'Forming', value: 'forming' },\n ],\n },\n { name: 'description', label: 'Description', type: 'textarea', colSpan: 2 },\n];\n\nfunction InitialsAvatar({ name, src, size = 32 }: { name: string; src?: string; size?: number }) {\n if (src) {\n return (\n <img\n src={src}\n alt={name}\n width={size}\n height={size}\n loading=\"lazy\"\n className=\"shrink-0 rounded-full object-cover\"\n style={{ width: size, height: size }}\n />\n );\n }\n return (\n <span\n aria-hidden=\"true\"\n className=\"inline-flex shrink-0 items-center justify-center rounded-full bg-bg-elevated font-medium text-text-secondary\"\n style={{ width: size, height: size, fontSize: Math.round(size * 0.36) }}\n >\n {getInitials(name)}\n </span>\n );\n}\n\nfunction employeeName(employee: Employee): string {\n return `${employee.firstName} ${employee.lastName}`.trim();\n}\n\nexport function TeamsTeamsPagePage() {\n const { t: translate } = useI18n();\n const navigate = useNavigate();\n const { teams, employees, projects } = useCrewFoundryStore();\n const addTeam = useCrewFoundryStore((state) => state.addTeam);\n const createModal = useFormModal();\n const { page, pageSize, setPage } = usePagination({ initialPageSize: 12 });\n const [selectedId, setSelectedId] = useState<string | null>(null);\n const t = (key: string, fallback: string) => {\n const value = translate(key);\n return value === key ? fallback : value;\n };\n\n const employeeById = useMemo(\n () => new Map(employees.map((employee) => [employee.id, employee])),\n [employees]\n );\n\n const activeProjectsByTeam = useMemo(() => {\n const map = new Map<string, number>();\n for (const project of projects) {\n if (project.status === 'active') {\n map.set(project.teamId, (map.get(project.teamId) ?? 0) + 1);\n }\n }\n return map;\n }, [projects]);\n\n const membersOf = (team: Team): Employee[] =>\n team.members\n .map((memberId) => employeeById.get(memberId))\n .filter((employee): employee is Employee => employee !== undefined);\n\n const peopleOf = (team: Team): Person[] =>\n membersOf(team).map((employee) => ({\n name: employeeName(employee),\n avatarUrl: employee.avatar,\n role: employee.position,\n }));\n\n const pageTeams = teams.slice((page - 1) * pageSize, (page - 1) * pageSize + pageSize);\n const selectedTeam =\n teams.find((team) => team.id === selectedId) ?? pageTeams[0] ?? teams[0] ?? null;\n const selectedMembers = selectedTeam ? membersOf(selectedTeam) : [];\n const selectedActiveProjects = selectedTeam\n ? projects.filter(\n (project) => project.teamId === selectedTeam.id && project.status === 'active'\n )\n : [];\n\n const openTeam = (team: Team) =>\n navigate(`../teams/team-detail-page?id=${encodeURIComponent(team.id)}`);\n\n const handleCreate = async (values: FormValues) => {\n // CreateCrewTeamInput accepts only this field subset; the store spreads the\n // object straight into `input`, so we send exactly the accepted shape and\n // let the server response replace the optimistic row.\n const payload: Partial<Team> = {\n name: String(values.name ?? '').trim(),\n description: String(values.description ?? '').trim(),\n department: String(values.department ?? '').trim() || 'operations',\n leadName: String(values.leadName ?? '').trim(),\n status: (String(values.status ?? 'forming') as Team['status']) || 'forming',\n members: [],\n };\n await addTeam(payload as Team);\n };\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex items-start justify-between gap-4\">\n <div>\n <h1 className=\"text-2xl font-semibold\">\n <GradientText>{t('crewfoundry.teams.title', 'Teams')}</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">\n {teams.length} {t('crewfoundry.teams.summary.count', 'teams across the organization')}\n </p>\n <PagePurpose className=\"mt-1\">\n Manage your teams — create groups, assign members, and organize people around the work\n they do together.\n </PagePurpose>\n </div>\n <Button size=\"sm\" onClick={() => createModal.openModal()}>\n <Plus className=\"h-4 w-4 mr-1\" />\n {t('crewfoundry.teams.actions.createTeam', 'Create Team')}\n </Button>\n </div>\n\n {teams.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-people\"\n title=\"No teams yet\"\n description=\"Create a team to organize people around their work.\"\n />\n ) : (\n <div className=\"grid gap-6 lg:grid-cols-3\">\n {/* Team cards */}\n <div className=\"space-y-4 lg:col-span-2\">\n <div className=\"grid gap-4 sm:grid-cols-2\">\n {pageTeams.map((team) => {\n const lead = team.leadId ? employeeById.get(team.leadId) : undefined;\n const activeCount = activeProjectsByTeam.get(team.id) ?? 0;\n const selected = selectedTeam?.id === team.id;\n return (\n <GlassCard\n key={team.id}\n treatment={selected ? 'glass' : 'plain'}\n glow={selected}\n role=\"button\"\n tabIndex={0}\n aria-pressed={selected}\n aria-label={`Select ${team.name} team`}\n onClick={() => setSelectedId(team.id)}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n setSelectedId(team.id);\n }\n }}\n className=\"cursor-pointer transition-shadow hover:shadow-md focus-visible:shadow-[var(--shadow-focus)] focus-visible:outline-none\"\n >\n <CardHeader className=\"pb-3\">\n <div className=\"flex items-start justify-between gap-2\">\n <CardTitle className=\"text-base\">{team.name}</CardTitle>\n <Badge\n variant={team.status === 'active' ? 'default' : 'secondary'}\n className=\"text-[10px]\"\n >\n {formatEnumLabel(team.status)}\n </Badge>\n </div>\n <div className=\"flex items-center gap-2 text-xs text-text-secondary\">\n <span>{t('crewfoundry.teams.card.ledBy', 'Led by')}</span>\n <InitialsAvatar\n name={lead ? employeeName(lead) : team.leadName}\n src={lead?.avatar}\n size={20}\n />\n <span className=\"font-medium text-text-primary\">\n {lead ? employeeName(lead) : team.leadName || '—'}\n </span>\n </div>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n <AvatarStack people={peopleOf(team)} max={5} size={30} />\n <div className=\"flex items-center gap-4 text-sm text-text-secondary\">\n <span className=\"flex items-center gap-1\">\n <Users className=\"h-3.5 w-3.5\" />\n {team.memberCount} {t('crewfoundry.teams.card.members', 'Members')}\n </span>\n <span className=\"flex items-center gap-1\">\n <FolderKanban className=\"h-3.5 w-3.5\" />\n {activeCount}{' '}\n {t('crewfoundry.teams.card.activeProjects', 'Active Projects')}\n </span>\n </div>\n </CardContent>\n </GlassCard>\n );\n })}\n </div>\n\n {teams.length > pageSize && (\n <Pagination\n page={page}\n pageSize={pageSize}\n totalItems={teams.length}\n onPageChange={setPage}\n />\n )}\n </div>\n\n {/* Selected-team detail panel */}\n <div className=\"lg:col-span-1\">\n {selectedTeam ? (\n <GlassCard\n treatment=\"glass\"\n className=\"border-border-seam lg:sticky lg:top-6\"\n aria-live=\"polite\"\n >\n <CardHeader className=\"pb-3\">\n <div className=\"flex items-start justify-between gap-2\">\n <CardTitle className=\"text-lg\">{selectedTeam.name}</CardTitle>\n <Badge\n variant={selectedTeam.status === 'active' ? 'default' : 'secondary'}\n className=\"text-[10px]\"\n >\n {formatEnumLabel(selectedTeam.status)}\n </Badge>\n </div>\n <CardDescription>{selectedTeam.description}</CardDescription>\n </CardHeader>\n <CardContent className=\"space-y-5\">\n <section>\n <h3 className=\"mb-2 text-sm font-semibold\">\n {t('crewfoundry.teams.panel.members', 'Members')} ({selectedMembers.length})\n </h3>\n {selectedMembers.length === 0 ? (\n <p className=\"text-xs text-text-secondary\">\n No members have been added to this team yet.\n </p>\n ) : (\n <ul className=\"space-y-2 max-h-72 overflow-y-auto pr-1\">\n {selectedMembers.map((member) => (\n <li key={member.id} className=\"flex items-center gap-2.5\">\n <InitialsAvatar\n name={employeeName(member)}\n src={member.avatar}\n size={32}\n />\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {employeeName(member)}\n {member.id === selectedTeam.leadId ? (\n <span className=\"ml-1.5 text-[10px] font-medium text-action-primary-text\">\n · Lead\n </span>\n ) : null}\n </p>\n <p className=\"truncate text-xs text-text-secondary\">\n {member.position}\n </p>\n </div>\n </li>\n ))}\n </ul>\n )}\n </section>\n\n <section>\n <h3 className=\"mb-2 text-sm font-semibold\">\n {t('crewfoundry.teams.panel.activeProjects', 'Active Projects')} (\n {selectedActiveProjects.length})\n </h3>\n {selectedActiveProjects.length === 0 ? (\n <p className=\"text-xs text-text-secondary\">\n No active projects for this team right now.\n </p>\n ) : (\n <ul className=\"space-y-3\">\n {selectedActiveProjects.map((project) => (\n <li key={project.id}>\n <div className=\"mb-1 flex items-center justify-between gap-2 text-xs\">\n <span className=\"truncate font-medium text-text-primary\">\n {project.name}\n </span>\n <span className=\"shrink-0 tabular-nums text-text-secondary\">\n {project.progress}%\n </span>\n </div>\n <div className=\"h-1.5 overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-1.5 rounded-full bg-action-primary-bg\"\n style={{ width: `${project.progress}%` }}\n />\n </div>\n </li>\n ))}\n </ul>\n )}\n </section>\n\n <Button\n variant=\"outline\"\n size=\"sm\"\n className=\"w-full\"\n onClick={() => openTeam(selectedTeam)}\n >\n {t('crewfoundry.teams.panel.openDetail', 'Open full team detail')}\n <ArrowRight className=\"ml-1 h-4 w-4\" />\n </Button>\n </CardContent>\n </GlassCard>\n ) : null}\n </div>\n </div>\n )}\n\n <FormModal\n open={createModal.open}\n onOpenChange={createModal.onOpenChange}\n title={t('crewfoundry.teams.actions.createTeam', 'Create Team')}\n description=\"Organize people around the work they do together.\"\n fields={TEAM_FIELDS}\n submitLabel=\"Create Team\"\n onSubmit={handleCreate}\n />\n </div>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"courseProgress.js","names":[],"sources":["../../../src/crewfoundry/utils/courseProgress.ts"],"sourcesContent":["/**\n * Per-user course progress, persisted client-side.\n *\n * The CrewFoundry backend models a course only as an aggregate (enrolled /\n * completed counts + a `modules` integer) — there is no per-user, per-module\n * completion record and no mark-complete mutation. Rather than fabricate a\n * progress number, this stores the learner's OWN real interactions (the modules\n * they mark complete in the course player) in localStorage, so progress, hours\n * and certificates reflect genuine user activity and persist across reloads.\n *\n * When a backend progress entity is added, these helpers become the single\n * place to swap the storage layer for the real mutation/query.\n */\n\nconst PROGRESS_KEY = 'crewfoundry:course-progress:v1';\n\nexport interface CourseProgress {\n /** Zero-based indexes of modules the learner has marked complete. */\n completed: number[];\n /** ISO timestamp of when the course first reached 100%. */\n completedAt?: string;\n}\n\ntype ProgressMap = Record<string, CourseProgress>;\n\nfunction readMap(): ProgressMap {\n if (typeof window === 'undefined') {\n return {};\n }\n try {\n const raw = window.localStorage.getItem(PROGRESS_KEY);\n if (!raw) {\n return {};\n }\n const parsed: unknown = JSON.parse(raw);\n if (!parsed || typeof parsed !== 'object') {\n return {};\n }\n return parsed as ProgressMap;\n } catch {\n return {};\n }\n}\n\nfunction writeMap(map: ProgressMap): void {\n if (typeof window === 'undefined') {\n return;\n }\n try {\n window.localStorage.setItem(PROGRESS_KEY, JSON.stringify(map));\n } catch {\n /* storage unavailable / quota exceeded — progress is best-effort */\n }\n}\n\nfunction sanitize(entry: CourseProgress | undefined): CourseProgress {\n if (!entry || !Array.isArray(entry.completed)) {\n return { completed: [] };\n }\n const completed = [...new Set(entry.completed.filter((n) => typeof n === 'number' && n >= 0))].sort(\n (a, b) => a - b\n );\n return {\n completed,\n completedAt: typeof entry.completedAt === 'string' ? entry.completedAt : undefined,\n };\n}\n\n/** Read a single course's progress (never throws; empty when absent). */\nexport function readCourseProgress(courseId: string): CourseProgress {\n return sanitize(readMap()[courseId]);\n}\n\n/** Read every course's progress as a map keyed by course id. */\nexport function readAllCourseProgress(): ProgressMap {\n const map = readMap();\n const out: ProgressMap = {};\n for (const [id, entry] of Object.entries(map)) {\n out[id] = sanitize(entry);\n }\n return out;\n}\n\n/**\n * Persist the set of completed module indexes for a course and return the saved\n * progress. Stamps `completedAt` the first time the course reaches 100%.\n */\nexport function writeCourseProgress(\n courseId: string,\n completed: number[],\n totalModules: number\n): CourseProgress {\n const map = readMap();\n const previous = sanitize(map[courseId]);\n const nextCompleted = [...new Set(completed.filter((n) => typeof n === 'number' && n >= 0))].sort(\n (a, b) => a - b\n );\n const reached100 = totalModules > 0 && nextCompleted.length >= totalModules;\n const entry: CourseProgress = {\n completed: nextCompleted,\n completedAt: reached100 ? (previous.completedAt ?? new Date().toISOString()) : undefined,\n };\n map[courseId] = entry;\n writeMap(map);\n return entry;\n}\n\n/** Completion percentage (0–100) for a course given its module count. */\nexport function progressPercent(progress: CourseProgress, totalModules: number): number {\n if (totalModules <= 0) {\n return 0;\n }\n return Math.min(100, Math.round((progress.completed.length / totalModules) * 100));\n}\n"],"mappings":";AAcA,IAAM,IAAe;AAWrB,SAAS,IAAuB;AAC9B,KAAI,OAAO,SAAW,IACpB,QAAO,EAAE;AAEX,KAAI;EACF,IAAM,IAAM,OAAO,aAAa,QAAQ,EAAa;AACrD,MAAI,CAAC,EACH,QAAO,EAAE;EAEX,IAAM,IAAkB,KAAK,MAAM,EAAI;AAIvC,SAHI,CAAC,KAAU,OAAO,KAAW,WACxB,EAAE,GAEJ;SACD;AACN,SAAO,EAAE;;;AAIb,SAAS,EAAS,GAAwB;AACpC,cAAO,SAAW,KAGtB,KAAI;AACF,SAAO,aAAa,QAAQ,GAAc,KAAK,UAAU,EAAI,CAAC;SACxD;;AAKV,SAAS,EAAS,GAAmD;AAOnE,QANI,CAAC,KAAS,CAAC,MAAM,QAAQ,EAAM,UAAU,GACpC,EAAE,WAAW,EAAE,EAAE,GAKnB;EACL,WAJgB,CAAC,GAAG,IAAI,IAAI,EAAM,UAAU,QAAQ,MAAM,OAAO,KAAM,YAAY,KAAK,EAAE,CAAC,CAAC,CAAC,MAC5F,GAAG,MAAM,IAAI,EACf;EAGC,aAAa,OAAO,EAAM,eAAgB,WAAW,EAAM,cAAc,KAAA;EAC1E;;AAIH,SAAgB,EAAmB,GAAkC;AACnE,QAAO,EAAS,GAAS,CAAC,GAAU;;AAItC,SAAgB,IAAqC;CACnD,IAAM,IAAM,GAAS,EACf,IAAmB,EAAE;AAC3B,MAAK,IAAM,CAAC,GAAI,MAAU,OAAO,QAAQ,EAAI,CAC3C,GAAI,KAAM,EAAS,EAAM;AAE3B,QAAO;;AAOT,SAAgB,EACd,GACA,GACA,GACgB;CAChB,IAAM,IAAM,GAAS,EACf,IAAW,EAAS,EAAI,GAAU,EAClC,IAAgB,CAAC,GAAG,IAAI,IAAI,EAAU,QAAQ,MAAM,OAAO,KAAM,YAAY,KAAK,EAAE,CAAC,CAAC,CAAC,MAC1F,GAAG,MAAM,IAAI,EACf,EAEK,IAAwB;EAC5B,WAAW;EACX,aAHiB,IAAe,KAAK,EAAc,UAAU,IAGlC,EAAS,gCAAe,IAAI,MAAM,EAAC,aAAa,GAAI,KAAA;EAChF;AAGD,QAFA,EAAI,KAAY,GAChB,EAAS,EAAI,EACN;;AAIT,SAAgB,EAAgB,GAA0B,GAA8B;AAItF,QAHI,KAAgB,IACX,IAEF,KAAK,IAAI,KAAK,KAAK,MAAO,EAAS,UAAU,SAAS,IAAgB,IAAI,CAAC"}
1
+ {"version":3,"file":"courseProgress.js","names":[],"sources":["../../../src/crewfoundry/utils/courseProgress.ts"],"sourcesContent":["/**\n * Per-user course progress, persisted client-side.\n *\n * The CrewFoundry backend models a course only as an aggregate (enrolled /\n * completed counts + a `modules` integer) — there is no per-user, per-module\n * completion record and no mark-complete mutation. Rather than fabricate a\n * progress number, this stores the learner's OWN real interactions (the modules\n * they mark complete in the course player) in localStorage, so progress, hours\n * and certificates reflect genuine user activity and persist across reloads.\n *\n * When a backend progress entity is added, these helpers become the single\n * place to swap the storage layer for the real mutation/query.\n */\n\nconst PROGRESS_KEY = 'crewfoundry:course-progress:v1';\n\nexport interface CourseProgress {\n /** Zero-based indexes of modules the learner has marked complete. */\n completed: number[];\n /** ISO timestamp of when the course first reached 100%. */\n completedAt?: string;\n}\n\ntype ProgressMap = Record<string, CourseProgress>;\n\nfunction readMap(): ProgressMap {\n if (typeof window === 'undefined') {\n return {};\n }\n try {\n const raw = window.localStorage.getItem(PROGRESS_KEY);\n if (!raw) {\n return {};\n }\n const parsed: unknown = JSON.parse(raw);\n if (!parsed || typeof parsed !== 'object') {\n return {};\n }\n return parsed as ProgressMap;\n } catch {\n return {};\n }\n}\n\nfunction writeMap(map: ProgressMap): void {\n if (typeof window === 'undefined') {\n return;\n }\n try {\n window.localStorage.setItem(PROGRESS_KEY, JSON.stringify(map));\n } catch {\n /* storage unavailable / quota exceeded — progress is best-effort */\n }\n}\n\nfunction sanitize(entry: CourseProgress | undefined): CourseProgress {\n if (!entry || !Array.isArray(entry.completed)) {\n return { completed: [] };\n }\n const completed = [\n ...new Set(entry.completed.filter((n) => typeof n === 'number' && n >= 0)),\n ].sort((a, b) => a - b);\n return {\n completed,\n completedAt: typeof entry.completedAt === 'string' ? entry.completedAt : undefined,\n };\n}\n\n/** Read a single course's progress (never throws; empty when absent). */\nexport function readCourseProgress(courseId: string): CourseProgress {\n return sanitize(readMap()[courseId]);\n}\n\n/** Read every course's progress as a map keyed by course id. */\nexport function readAllCourseProgress(): ProgressMap {\n const map = readMap();\n const out: ProgressMap = {};\n for (const [id, entry] of Object.entries(map)) {\n out[id] = sanitize(entry);\n }\n return out;\n}\n\n/**\n * Persist the set of completed module indexes for a course and return the saved\n * progress. Stamps `completedAt` the first time the course reaches 100%.\n */\nexport function writeCourseProgress(\n courseId: string,\n completed: number[],\n totalModules: number\n): CourseProgress {\n const map = readMap();\n const previous = sanitize(map[courseId]);\n const nextCompleted = [...new Set(completed.filter((n) => typeof n === 'number' && n >= 0))].sort(\n (a, b) => a - b\n );\n const reached100 = totalModules > 0 && nextCompleted.length >= totalModules;\n const entry: CourseProgress = {\n completed: nextCompleted,\n completedAt: reached100 ? (previous.completedAt ?? new Date().toISOString()) : undefined,\n };\n map[courseId] = entry;\n writeMap(map);\n return entry;\n}\n\n/** Completion percentage (0–100) for a course given its module count. */\nexport function progressPercent(progress: CourseProgress, totalModules: number): number {\n if (totalModules <= 0) {\n return 0;\n }\n return Math.min(100, Math.round((progress.completed.length / totalModules) * 100));\n}\n"],"mappings":";AAcA,IAAM,IAAe;AAWrB,SAAS,IAAuB;AAC9B,KAAI,OAAO,SAAW,IACpB,QAAO,EAAE;AAEX,KAAI;EACF,IAAM,IAAM,OAAO,aAAa,QAAQ,EAAa;AACrD,MAAI,CAAC,EACH,QAAO,EAAE;EAEX,IAAM,IAAkB,KAAK,MAAM,EAAI;AAIvC,SAHI,CAAC,KAAU,OAAO,KAAW,WACxB,EAAE,GAEJ;SACD;AACN,SAAO,EAAE;;;AAIb,SAAS,EAAS,GAAwB;AACpC,cAAO,SAAW,KAGtB,KAAI;AACF,SAAO,aAAa,QAAQ,GAAc,KAAK,UAAU,EAAI,CAAC;SACxD;;AAKV,SAAS,EAAS,GAAmD;AAOnE,QANI,CAAC,KAAS,CAAC,MAAM,QAAQ,EAAM,UAAU,GACpC,EAAE,WAAW,EAAE,EAAE,GAKnB;EACL,WAJgB,CAChB,GAAG,IAAI,IAAI,EAAM,UAAU,QAAQ,MAAM,OAAO,KAAM,YAAY,KAAK,EAAE,CAAC,CAC3E,CAAC,MAAM,GAAG,MAAM,IAAI,EAAE;EAGrB,aAAa,OAAO,EAAM,eAAgB,WAAW,EAAM,cAAc,KAAA;EAC1E;;AAIH,SAAgB,EAAmB,GAAkC;AACnE,QAAO,EAAS,GAAS,CAAC,GAAU;;AAItC,SAAgB,IAAqC;CACnD,IAAM,IAAM,GAAS,EACf,IAAmB,EAAE;AAC3B,MAAK,IAAM,CAAC,GAAI,MAAU,OAAO,QAAQ,EAAI,CAC3C,GAAI,KAAM,EAAS,EAAM;AAE3B,QAAO;;AAOT,SAAgB,EACd,GACA,GACA,GACgB;CAChB,IAAM,IAAM,GAAS,EACf,IAAW,EAAS,EAAI,GAAU,EAClC,IAAgB,CAAC,GAAG,IAAI,IAAI,EAAU,QAAQ,MAAM,OAAO,KAAM,YAAY,KAAK,EAAE,CAAC,CAAC,CAAC,MAC1F,GAAG,MAAM,IAAI,EACf,EAEK,IAAwB;EAC5B,WAAW;EACX,aAHiB,IAAe,KAAK,EAAc,UAAU,IAGlC,EAAS,gCAAe,IAAI,MAAM,EAAC,aAAa,GAAI,KAAA;EAChF;AAGD,QAFA,EAAI,KAAY,GAChB,EAAS,EAAI,EACN;;AAIT,SAAgB,EAAgB,GAA0B,GAA8B;AAItF,QAHI,KAAgB,IACX,IAEF,KAAK,IAAI,KAAK,KAAK,MAAO,EAAS,UAAU,SAAS,IAAgB,IAAI,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-crewfoundry",
3
- "version": "2026.707.1",
3
+ "version": "2026.707.2",
4
4
  "description": "Burdenoff CrewFoundry Microfrontend",
5
5
  "type": "module",
6
6
  "files": [