@hed-hog/operations 0.0.294 → 0.0.296

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 (126) hide show
  1. package/dist/operations.controller.d.ts +415 -0
  2. package/dist/operations.controller.d.ts.map +1 -0
  3. package/dist/operations.controller.js +333 -0
  4. package/dist/operations.controller.js.map +1 -0
  5. package/dist/operations.module.d.ts.map +1 -1
  6. package/dist/operations.module.js +4 -3
  7. package/dist/operations.module.js.map +1 -1
  8. package/dist/operations.service.d.ts +589 -153
  9. package/dist/operations.service.d.ts.map +1 -1
  10. package/dist/operations.service.js +2229 -100
  11. package/dist/operations.service.js.map +1 -1
  12. package/hedhog/data/menu.yaml +198 -251
  13. package/hedhog/data/role.yaml +23 -14
  14. package/hedhog/data/route.yaml +317 -143
  15. package/hedhog/frontend/app/_components/collaborator-details-screen.tsx.ejs +310 -0
  16. package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +631 -0
  17. package/hedhog/frontend/app/_components/contract-details-screen.tsx.ejs +132 -0
  18. package/hedhog/frontend/app/_components/contract-form-screen.tsx.ejs +558 -0
  19. package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +291 -0
  20. package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +689 -0
  21. package/hedhog/frontend/app/_lib/api.ts.ejs +32 -0
  22. package/hedhog/frontend/app/_lib/hooks/use-operations-access.ts.ejs +44 -0
  23. package/hedhog/frontend/app/_lib/types.ts.ejs +360 -0
  24. package/hedhog/frontend/app/_lib/utils/format.ts.ejs +129 -25
  25. package/hedhog/frontend/app/_lib/utils/forms.ts.ejs +14 -0
  26. package/hedhog/frontend/app/approvals/page.tsx.ejs +386 -147
  27. package/hedhog/frontend/app/collaborators/[id]/edit/page.tsx.ejs +11 -0
  28. package/hedhog/frontend/app/collaborators/[id]/page.tsx.ejs +11 -0
  29. package/hedhog/frontend/app/collaborators/new/page.tsx.ejs +5 -0
  30. package/hedhog/frontend/app/collaborators/page.tsx.ejs +261 -0
  31. package/hedhog/frontend/app/contracts/[id]/edit/page.tsx.ejs +11 -0
  32. package/hedhog/frontend/app/contracts/[id]/page.tsx.ejs +11 -108
  33. package/hedhog/frontend/app/contracts/new/page.tsx.ejs +17 -0
  34. package/hedhog/frontend/app/contracts/page.tsx.ejs +262 -181
  35. package/hedhog/frontend/app/page.tsx.ejs +319 -177
  36. package/hedhog/frontend/app/projects/[id]/edit/page.tsx.ejs +11 -0
  37. package/hedhog/frontend/app/projects/[id]/page.tsx.ejs +11 -936
  38. package/hedhog/frontend/app/projects/new/page.tsx.ejs +5 -0
  39. package/hedhog/frontend/app/projects/page.tsx.ejs +236 -1074
  40. package/hedhog/frontend/app/schedule-adjustments/page.tsx.ejs +418 -0
  41. package/hedhog/frontend/app/team/page.tsx.ejs +339 -0
  42. package/hedhog/frontend/app/time-off/page.tsx.ejs +328 -0
  43. package/hedhog/frontend/app/timesheets/page.tsx.ejs +636 -126
  44. package/hedhog/frontend/messages/en.json +648 -454
  45. package/hedhog/frontend/messages/pt.json +647 -454
  46. package/hedhog/table/operations_approval.yaml +49 -0
  47. package/hedhog/table/operations_approval_history.yaml +29 -0
  48. package/hedhog/table/{operations_employee.yaml → operations_collaborator.yaml} +67 -64
  49. package/hedhog/table/operations_collaborator_schedule_day.yaml +34 -0
  50. package/hedhog/table/operations_contract.yaml +100 -48
  51. package/hedhog/table/operations_contract_document.yaml +39 -0
  52. package/hedhog/table/operations_contract_financial_term.yaml +40 -0
  53. package/hedhog/table/operations_contract_history.yaml +27 -0
  54. package/hedhog/table/operations_contract_party.yaml +46 -0
  55. package/hedhog/table/operations_contract_revision.yaml +38 -0
  56. package/hedhog/table/operations_contract_signature.yaml +38 -0
  57. package/hedhog/table/operations_project.yaml +54 -50
  58. package/hedhog/table/{operations_allocation.yaml → operations_project_assignment.yaml} +55 -52
  59. package/hedhog/table/operations_schedule_adjustment_day.yaml +34 -0
  60. package/hedhog/table/operations_schedule_adjustment_request.yaml +53 -0
  61. package/hedhog/table/operations_time_off_request.yaml +57 -0
  62. package/hedhog/table/operations_timesheet.yaml +41 -36
  63. package/hedhog/table/operations_timesheet_entry.yaml +40 -50
  64. package/package.json +7 -6
  65. package/src/operations.controller.ts +182 -0
  66. package/src/operations.module.ts +22 -21
  67. package/src/operations.service.ts +3595 -137
  68. package/hedhog/data/operations_career_level.yaml +0 -102
  69. package/hedhog/data/operations_career_track.yaml +0 -8
  70. package/hedhog/data/operations_certification.yaml +0 -38
  71. package/hedhog/data/operations_evaluation_cycle.yaml +0 -18
  72. package/hedhog/data/operations_performance_criterion.yaml +0 -48
  73. package/hedhog/frontend/app/_components/allocation-calendar.tsx.ejs +0 -56
  74. package/hedhog/frontend/app/_components/kanban-board.tsx.ejs +0 -626
  75. package/hedhog/frontend/app/_components/timesheet-entry-dialog.tsx.ejs +0 -142
  76. package/hedhog/frontend/app/_lib/hooks/use-operations-data.ts.ejs +0 -41
  77. package/hedhog/frontend/app/_lib/hooks/use-operations-growth-data.ts.ejs +0 -63
  78. package/hedhog/frontend/app/_lib/mocks/allocations.mock.ts.ejs +0 -74
  79. package/hedhog/frontend/app/_lib/mocks/contracts.mock.ts.ejs +0 -74
  80. package/hedhog/frontend/app/_lib/mocks/operations-growth.mock.ts.ejs +0 -824
  81. package/hedhog/frontend/app/_lib/mocks/projects.mock.ts.ejs +0 -455
  82. package/hedhog/frontend/app/_lib/mocks/tasks.mock.ts.ejs +0 -117
  83. package/hedhog/frontend/app/_lib/mocks/timesheets.mock.ts.ejs +0 -84
  84. package/hedhog/frontend/app/_lib/mocks/users.mock.ts.ejs +0 -67
  85. package/hedhog/frontend/app/_lib/services/contracts.service.ts.ejs +0 -10
  86. package/hedhog/frontend/app/_lib/services/operations-growth.service.ts.ejs +0 -31
  87. package/hedhog/frontend/app/_lib/services/projects.service.ts.ejs +0 -10
  88. package/hedhog/frontend/app/_lib/services/tasks.service.ts.ejs +0 -10
  89. package/hedhog/frontend/app/_lib/services/timesheets.service.ts.ejs +0 -10
  90. package/hedhog/frontend/app/_lib/types/operations-growth.ts.ejs +0 -209
  91. package/hedhog/frontend/app/_lib/types/operations.ts.ejs +0 -156
  92. package/hedhog/frontend/app/_lib/utils/growth.ts.ejs +0 -62
  93. package/hedhog/frontend/app/_lib/utils/metrics.ts.ejs +0 -103
  94. package/hedhog/frontend/app/_lib/utils/status.ts.ejs +0 -80
  95. package/hedhog/frontend/app/allocations/page.tsx.ejs +0 -155
  96. package/hedhog/frontend/app/career/page.tsx.ejs +0 -143
  97. package/hedhog/frontend/app/certifications/page.tsx.ejs +0 -202
  98. package/hedhog/frontend/app/evaluations/page.tsx.ejs +0 -278
  99. package/hedhog/frontend/app/goals/page.tsx.ejs +0 -171
  100. package/hedhog/frontend/app/growth/page.tsx.ejs +0 -288
  101. package/hedhog/frontend/app/manager/page.tsx.ejs +0 -175
  102. package/hedhog/frontend/app/rewards/page.tsx.ejs +0 -196
  103. package/hedhog/frontend/app/tasks/page.tsx.ejs +0 -999
  104. package/hedhog/table/operations_calibration_item.yaml +0 -61
  105. package/hedhog/table/operations_calibration_session.yaml +0 -25
  106. package/hedhog/table/operations_career_level.yaml +0 -75
  107. package/hedhog/table/operations_career_track.yaml +0 -21
  108. package/hedhog/table/operations_certification.yaml +0 -48
  109. package/hedhog/table/operations_employee_certification.yaml +0 -43
  110. package/hedhog/table/operations_employee_connect.yaml +0 -61
  111. package/hedhog/table/operations_employee_evaluation.yaml +0 -113
  112. package/hedhog/table/operations_employee_evaluation_item.yaml +0 -39
  113. package/hedhog/table/operations_employee_profile.yaml +0 -80
  114. package/hedhog/table/operations_employee_skill_matrix.yaml +0 -30
  115. package/hedhog/table/operations_evaluation_cycle.yaml +0 -31
  116. package/hedhog/table/operations_goal.yaml +0 -67
  117. package/hedhog/table/operations_goal_progress.yaml +0 -31
  118. package/hedhog/table/operations_performance_criterion.yaml +0 -29
  119. package/hedhog/table/operations_promotion_readiness.yaml +0 -49
  120. package/hedhog/table/operations_promotion_recommendation.yaml +0 -63
  121. package/hedhog/table/operations_public_recognition.yaml +0 -46
  122. package/hedhog/table/operations_reward.yaml +0 -100
  123. package/hedhog/table/operations_score_event.yaml +0 -81
  124. package/hedhog/table/operations_task.yaml +0 -60
  125. package/src/operations-data.controller.ts +0 -54
  126. package/src/operations-growth.controller.ts +0 -44
@@ -1,80 +0,0 @@
1
- import type {
2
- ApprovalStatus,
3
- ContractStatus,
4
- ContractType,
5
- ProjectStatus,
6
- TaskStatus,
7
- } from '../types/operations';
8
-
9
- export function getApprovalBadgeClasses(status: ApprovalStatus) {
10
- return {
11
- approved: 'bg-emerald-100 text-emerald-700',
12
- pending: 'bg-amber-100 text-amber-700',
13
- rejected: 'bg-rose-100 text-rose-700',
14
- }[status];
15
- }
16
-
17
- export function getProjectBadgeClasses(status: ProjectStatus) {
18
- return {
19
- planning: 'bg-slate-100 text-slate-700',
20
- active: 'bg-blue-100 text-blue-700',
21
- 'at-risk': 'bg-orange-100 text-orange-700',
22
- paused: 'bg-zinc-100 text-zinc-700',
23
- completed: 'bg-emerald-100 text-emerald-700',
24
- }[status];
25
- }
26
-
27
- export function getTaskBadgeClasses(status: TaskStatus) {
28
- return {
29
- backlog: 'bg-slate-100 text-slate-700',
30
- todo: 'bg-cyan-100 text-cyan-700',
31
- 'in-progress': 'bg-blue-100 text-blue-700',
32
- review: 'bg-violet-100 text-violet-700',
33
- done: 'bg-emerald-100 text-emerald-700',
34
- }[status];
35
- }
36
-
37
- export function getContractBadgeClasses(status: ContractStatus) {
38
- return {
39
- active: 'bg-emerald-100 text-emerald-700',
40
- draft: 'bg-slate-100 text-slate-700',
41
- expired: 'bg-zinc-200 text-zinc-700',
42
- renewal: 'bg-orange-100 text-orange-700',
43
- }[status];
44
- }
45
-
46
- export function getContractTypeLabel(type: ContractType) {
47
- return {
48
- tm: 'T&M',
49
- monthly: 'Monthly',
50
- fixed: 'Fixed',
51
- }[type];
52
- }
53
-
54
- export function getApprovalLabel(status: ApprovalStatus) {
55
- return {
56
- approved: 'Approved',
57
- pending: 'Pending',
58
- rejected: 'Rejected',
59
- }[status];
60
- }
61
-
62
- export function getProjectStatusLabel(status: ProjectStatus) {
63
- return {
64
- planning: 'Planning',
65
- active: 'Active',
66
- 'at-risk': 'At Risk',
67
- paused: 'Paused',
68
- completed: 'Completed',
69
- }[status];
70
- }
71
-
72
- export function getTaskStatusLabel(status: TaskStatus) {
73
- return {
74
- backlog: 'Backlog',
75
- todo: 'Todo',
76
- 'in-progress': 'In Progress',
77
- review: 'Review',
78
- done: 'Done',
79
- }[status];
80
- }
@@ -1,155 +0,0 @@
1
- 'use client';
2
-
3
- import {
4
- EmptyState,
5
- Page,
6
- PaginationFooter,
7
- SearchBar,
8
- } from '@/components/entity-list';
9
- import {
10
- Table,
11
- TableBody,
12
- TableCell,
13
- TableHead,
14
- TableHeader,
15
- TableRow,
16
- } from '@/components/ui/table';
17
- import { useTranslations } from 'next-intl';
18
- import { useMemo, useState } from 'react';
19
- import { Layers } from 'lucide-react';
20
- import { AllocationCalendar } from '../_components/allocation-calendar';
21
- import { OperationsHeader } from '../_components/operations-header';
22
- import { SectionCard } from '../_components/section-card';
23
- import { useOperationsData } from '../_lib/hooks/use-operations-data';
24
- import { formatDate } from '../_lib/utils/format';
25
-
26
- const PAGE_SIZE_OPTIONS = [10, 20, 30, 50];
27
- const DEFAULT_PAGE_SIZE = PAGE_SIZE_OPTIONS[0] ?? 10;
28
-
29
- export default function AllocationsPage() {
30
- const t = useTranslations('operations.AllocationsPage');
31
- const { allocations, users, projects } = useOperationsData();
32
- const [searchInput, setSearchInput] = useState('');
33
- const [search, setSearch] = useState('');
34
- const [currentPage, setCurrentPage] = useState(1);
35
- const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);
36
-
37
- const filteredAllocations = useMemo(
38
- () =>
39
- allocations.filter((allocation) => {
40
- const user = users.find((item) => item.id === allocation.userId);
41
- const project = projects.find((item) => item.id === allocation.projectId);
42
- return `${user?.name} ${project?.name}`
43
- .toLowerCase()
44
- .includes(search.toLowerCase());
45
- }),
46
- [allocations, users, projects, search]
47
- );
48
-
49
- const totalPages = Math.max(
50
- 1,
51
- Math.ceil(filteredAllocations.length / pageSize)
52
- );
53
- const safePage = Math.min(Math.max(currentPage, 1), totalPages);
54
- const paginatedAllocations = useMemo(() => {
55
- const start = (safePage - 1) * pageSize;
56
- return filteredAllocations.slice(start, start + pageSize);
57
- }, [filteredAllocations, safePage, pageSize]);
58
-
59
- return (
60
- <Page>
61
- <OperationsHeader
62
- title={t('title')}
63
- description={t('description')}
64
- current={t('breadcrumb')}
65
- />
66
-
67
- <SearchBar
68
- className="mb-6"
69
- searchQuery={searchInput}
70
- onSearchChange={setSearchInput}
71
- placeholder={t('searchPlaceholder')}
72
- onSearch={() => {
73
- setSearch(searchInput);
74
- setCurrentPage(1);
75
- }}
76
- />
77
-
78
- <SectionCard title={t('tableTitle')} description={t('tableDescription')}>
79
- {filteredAllocations.length === 0 ? (
80
- <EmptyState
81
- icon={<Layers className="h-12 w-12" />}
82
- title={t('emptyState.title')}
83
- description={t('emptyState.description')}
84
- actionLabel={t('emptyState.action')}
85
- onAction={() => {
86
- setSearch('');
87
- setSearchInput('');
88
- setCurrentPage(1);
89
- }}
90
- />
91
- ) : (
92
- <div className="space-y-4">
93
- <Table>
94
- <TableHeader>
95
- <TableRow>
96
- <TableHead>{t('columns.user')}</TableHead>
97
- <TableHead>{t('columns.role')}</TableHead>
98
- <TableHead>{t('columns.project')}</TableHead>
99
- <TableHead>{t('columns.weeklyHours')}</TableHead>
100
- <TableHead>{t('columns.allocation')}</TableHead>
101
- <TableHead>{t('columns.startDate')}</TableHead>
102
- <TableHead>{t('columns.endDate')}</TableHead>
103
- </TableRow>
104
- </TableHeader>
105
- <TableBody>
106
- {paginatedAllocations.map((allocation) => {
107
- const user = users.find(
108
- (item) => item.id === allocation.userId
109
- );
110
- const project = projects.find(
111
- (item) => item.id === allocation.projectId
112
- );
113
-
114
- return (
115
- <TableRow key={allocation.id}>
116
- <TableCell>{user?.name}</TableCell>
117
- <TableCell>{allocation.role}</TableCell>
118
- <TableCell>{project?.name}</TableCell>
119
- <TableCell>{allocation.weeklyHours}h</TableCell>
120
- <TableCell>{allocation.allocationPercent}%</TableCell>
121
- <TableCell>{formatDate(allocation.startDate)}</TableCell>
122
- <TableCell>{formatDate(allocation.endDate)}</TableCell>
123
- </TableRow>
124
- );
125
- })}
126
- </TableBody>
127
- </Table>
128
- <PaginationFooter
129
- currentPage={safePage}
130
- pageSize={pageSize}
131
- totalItems={filteredAllocations.length}
132
- onPageChange={setCurrentPage}
133
- onPageSizeChange={(nextSize) => {
134
- setPageSize(nextSize);
135
- setCurrentPage(1);
136
- }}
137
- pageSizeOptions={PAGE_SIZE_OPTIONS}
138
- />
139
- </div>
140
- )}
141
- </SectionCard>
142
-
143
- <SectionCard
144
- title={t('calendarTitle')}
145
- description={t('calendarDescription')}
146
- >
147
- <AllocationCalendar
148
- allocations={filteredAllocations}
149
- users={users}
150
- projects={projects}
151
- />
152
- </SectionCard>
153
- </Page>
154
- );
155
- }
@@ -1,143 +0,0 @@
1
- 'use client';
2
-
3
- import { Page, SearchBar } from '@/components/entity-list';
4
- import { Badge } from '@/components/ui/badge';
5
- import { Progress } from '@/components/ui/progress';
6
- import { Route } from 'lucide-react';
7
- import { useTranslations } from 'next-intl';
8
- import { useMemo, useState } from 'react';
9
- import { OperationsHeader } from '../_components/operations-header';
10
- import { SectionCard } from '../_components/section-card';
11
- import { useOperationsGrowthData } from '../_lib/hooks/use-operations-growth-data';
12
- import { formatCurrency } from '../_lib/utils/format';
13
-
14
- export default function OperationsCareerPage() {
15
- const t = useTranslations('operations.CareerPage');
16
- const { careerTracks, careerLevels, profiles, users } = useOperationsGrowthData();
17
- const [searchInput, setSearchInput] = useState('');
18
- const [search, setSearch] = useState('');
19
- const [trackFilter, setTrackFilter] = useState('track-delivery');
20
-
21
- const filteredLevels = useMemo(
22
- () =>
23
- careerLevels
24
- .filter((item) => {
25
- const matchesTrack = item.trackId === trackFilter;
26
- const matchesSearch = `${item.name} ${item.summaryCriteria.join(' ')}`
27
- .toLowerCase()
28
- .includes(search.toLowerCase());
29
-
30
- return matchesTrack && matchesSearch;
31
- })
32
- .sort((left, right) => left.order - right.order),
33
- [careerLevels, search, trackFilter]
34
- );
35
-
36
- const selectedTrack = careerTracks.find((item) => item.id === trackFilter);
37
- const peopleOnTrack = profiles.filter((item) => item.trackId === trackFilter);
38
-
39
- return (
40
- <Page>
41
- <OperationsHeader
42
- title={t('title')}
43
- description={t('description')}
44
- current={t('breadcrumb')}
45
- />
46
-
47
- <SearchBar
48
- className="mb-6"
49
- searchQuery={searchInput}
50
- onSearchChange={setSearchInput}
51
- onSearch={() => setSearch(searchInput)}
52
- placeholder={t('searchPlaceholder')}
53
- controls={[
54
- {
55
- id: 'track-filter',
56
- type: 'select',
57
- value: trackFilter,
58
- onChange: setTrackFilter,
59
- placeholder: t('filters.track'),
60
- options: careerTracks.map((item) => ({ value: item.id, label: item.name })),
61
- },
62
- ]}
63
- />
64
-
65
- <SectionCard title={selectedTrack?.name ?? t('title')} description={selectedTrack?.description}>
66
- <div className="grid gap-4 lg:grid-cols-[1.1fr_0.9fr]">
67
- <div className="space-y-4">
68
- {filteredLevels.map((level) => {
69
- const members = peopleOnTrack.filter(
70
- (profile) => profile.currentLevelId === level.id
71
- );
72
-
73
- return (
74
- <div key={level.id} className="rounded-lg border p-4">
75
- <div className="mb-4 flex items-center justify-between gap-3">
76
- <div>
77
- <p className="font-medium">{level.name}</p>
78
- <p className="text-muted-foreground text-sm">{level.code}</p>
79
- </div>
80
- <Badge variant="secondary">
81
- {level.minScore} - {level.maxScore}
82
- </Badge>
83
- </div>
84
- <div className="mb-4 grid gap-4 md:grid-cols-2">
85
- <div>
86
- <p className="text-muted-foreground text-sm">{t('salaryBand')}</p>
87
- <p className="font-medium">
88
- {formatCurrency(level.salaryMin)} - {formatCurrency(level.salaryMax)}
89
- </p>
90
- </div>
91
- <div>
92
- <p className="text-muted-foreground text-sm">{t('peopleInLevel')}</p>
93
- <p className="font-medium">{members.length}</p>
94
- </div>
95
- </div>
96
- <div className="space-y-2">
97
- {level.summaryCriteria.map((criterion) => (
98
- <div key={criterion} className="flex items-center gap-2 text-sm">
99
- <Route className="h-4 w-4 text-blue-700" />
100
- <span>{criterion}</span>
101
- </div>
102
- ))}
103
- </div>
104
- </div>
105
- );
106
- })}
107
- </div>
108
-
109
- <div className="space-y-4">
110
- <SectionCard title={t('whereAmI')} description={t('whereAmIDescription')}>
111
- <div className="space-y-4">
112
- {peopleOnTrack.slice(0, 4).map((profile) => {
113
- const user = users.find((item) => item.id === profile.employeeId);
114
- const currentLevel = careerLevels.find(
115
- (item) => item.id === profile.currentLevelId
116
- );
117
- const nextLevel = careerLevels.find(
118
- (item) => item.id === profile.nextLevelId
119
- );
120
-
121
- return (
122
- <div key={profile.employeeId} className="rounded-lg border p-4">
123
- <div className="mb-2 flex items-center justify-between gap-3">
124
- <span className="font-medium">{user?.name}</span>
125
- <Badge variant="outline">{currentLevel?.name}</Badge>
126
- </div>
127
- <p className="text-muted-foreground mb-3 text-sm">
128
- {nextLevel
129
- ? t('nextStep', { level: nextLevel.name })
130
- : t('topTrack')}
131
- </p>
132
- <Progress value={profile.promotionProgress} />
133
- </div>
134
- );
135
- })}
136
- </div>
137
- </SectionCard>
138
- </div>
139
- </div>
140
- </SectionCard>
141
- </Page>
142
- );
143
- }
@@ -1,202 +0,0 @@
1
- 'use client';
2
-
3
- import { Page, PaginationFooter, SearchBar } from '@/components/entity-list';
4
- import { Badge } from '@/components/ui/badge';
5
- import {
6
- Table,
7
- TableBody,
8
- TableCell,
9
- TableHead,
10
- TableHeader,
11
- TableRow,
12
- } from '@/components/ui/table';
13
- import { useTranslations } from 'next-intl';
14
- import { useMemo, useState } from 'react';
15
- import { OperationsHeader } from '../_components/operations-header';
16
- import { SectionCard } from '../_components/section-card';
17
- import { StatusBadge } from '../_components/status-badge';
18
- import { useOperationsGrowthData } from '../_lib/hooks/use-operations-growth-data';
19
- import { formatDate } from '../_lib/utils/format';
20
- import {
21
- getGrowthCertificationBadgeClasses,
22
- humanizeGrowthStatus,
23
- } from '../_lib/utils/growth';
24
-
25
- const PAGE_SIZE_OPTIONS = [4, 8, 12];
26
- const DEFAULT_PAGE_SIZE = PAGE_SIZE_OPTIONS[0] ?? 4;
27
-
28
- export default function OperationsCertificationsPage() {
29
- const t = useTranslations('operations.CertificationsPage');
30
- const {
31
- users,
32
- careerLevels,
33
- certificationCatalog,
34
- employeeCertifications,
35
- } = useOperationsGrowthData();
36
- const [searchInput, setSearchInput] = useState('');
37
- const [search, setSearch] = useState('');
38
- const [employeeFilter, setEmployeeFilter] = useState('all');
39
- const [categoryFilter, setCategoryFilter] = useState('all');
40
- const [currentPage, setCurrentPage] = useState(1);
41
- const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);
42
-
43
- const categories = Array.from(new Set(certificationCatalog.map((item) => item.category)));
44
-
45
- const filteredEmployeeCertifications = useMemo(
46
- () =>
47
- employeeCertifications.filter((item) => {
48
- const employee = users.find((user) => user.id === item.employeeId);
49
- const certification = certificationCatalog.find(
50
- (catalogItem) => catalogItem.id === item.certificationId
51
- );
52
- const haystack = `${employee?.name ?? ''} ${certification?.name ?? ''} ${certification?.provider ?? ''}`;
53
- const matchesSearch = haystack.toLowerCase().includes(search.toLowerCase());
54
- const matchesEmployee =
55
- employeeFilter === 'all' || item.employeeId === employeeFilter;
56
- const matchesCategory =
57
- categoryFilter === 'all' || certification?.category === categoryFilter;
58
-
59
- return matchesSearch && matchesEmployee && matchesCategory;
60
- }),
61
- [categoryFilter, certificationCatalog, employeeCertifications, employeeFilter, search, users]
62
- );
63
-
64
- const totalPages = Math.max(1, Math.ceil(filteredEmployeeCertifications.length / pageSize));
65
- const safePage = Math.min(Math.max(currentPage, 1), totalPages);
66
- const paginatedEmployeeCertifications = filteredEmployeeCertifications.slice(
67
- (safePage - 1) * pageSize,
68
- safePage * pageSize
69
- );
70
-
71
- return (
72
- <Page>
73
- <OperationsHeader
74
- title={t('title')}
75
- description={t('description')}
76
- current={t('breadcrumb')}
77
- />
78
-
79
- <SearchBar
80
- className="mb-6"
81
- searchQuery={searchInput}
82
- onSearchChange={setSearchInput}
83
- onSearch={() => {
84
- setSearch(searchInput);
85
- setCurrentPage(1);
86
- }}
87
- placeholder={t('searchPlaceholder')}
88
- controls={[
89
- {
90
- id: 'employee-filter',
91
- type: 'select',
92
- value: employeeFilter,
93
- onChange: (value) => {
94
- setEmployeeFilter(value);
95
- setCurrentPage(1);
96
- },
97
- placeholder: t('filters.allEmployees'),
98
- options: [
99
- { value: 'all', label: t('filters.allEmployees') },
100
- ...users.map((item) => ({ value: item.id, label: item.name })),
101
- ],
102
- },
103
- {
104
- id: 'category-filter',
105
- type: 'select',
106
- value: categoryFilter,
107
- onChange: (value) => {
108
- setCategoryFilter(value);
109
- setCurrentPage(1);
110
- },
111
- placeholder: t('filters.allCategories'),
112
- options: [
113
- { value: 'all', label: t('filters.allCategories') },
114
- ...categories.map((item) => ({ value: item, label: item })),
115
- ],
116
- },
117
- ]}
118
- />
119
-
120
- <SectionCard title={t('catalogTitle')} description={t('catalogDescription')}>
121
- <div className="grid gap-4 lg:grid-cols-2">
122
- {certificationCatalog.map((item) => {
123
- const requiredLevel = careerLevels.find(
124
- (level) => level.id === item.requiredLevelId
125
- );
126
-
127
- return (
128
- <div key={item.id} className="rounded-lg border p-4">
129
- <div className="mb-3 flex items-start justify-between gap-3">
130
- <div>
131
- <p className="font-medium">{item.name}</p>
132
- <p className="text-muted-foreground text-sm">{item.provider}</p>
133
- </div>
134
- <Badge variant="secondary">+{item.defaultScore}</Badge>
135
- </div>
136
- <div className="mb-3 flex flex-wrap gap-2">
137
- <Badge variant="outline">{item.category}</Badge>
138
- {requiredLevel ? <Badge variant="outline">{requiredLevel.name}</Badge> : null}
139
- </div>
140
- <p className="text-muted-foreground text-sm">{item.description}</p>
141
- </div>
142
- );
143
- })}
144
- </div>
145
- </SectionCard>
146
-
147
- <SectionCard title={t('employeeTitle')} description={t('employeeDescription')}>
148
- <div className="space-y-4">
149
- <Table>
150
- <TableHeader>
151
- <TableRow>
152
- <TableHead>{t('columns.employee')}</TableHead>
153
- <TableHead>{t('columns.certification')}</TableHead>
154
- <TableHead>{t('columns.category')}</TableHead>
155
- <TableHead>{t('columns.validity')}</TableHead>
156
- <TableHead>{t('columns.score')}</TableHead>
157
- <TableHead>{t('columns.status')}</TableHead>
158
- </TableRow>
159
- </TableHeader>
160
- <TableBody>
161
- {paginatedEmployeeCertifications.map((item) => {
162
- const employee = users.find((user) => user.id === item.employeeId);
163
- const certification = certificationCatalog.find(
164
- (catalogItem) => catalogItem.id === item.certificationId
165
- );
166
-
167
- return (
168
- <TableRow key={item.id}>
169
- <TableCell className="font-medium">{employee?.name}</TableCell>
170
- <TableCell>{certification?.name}</TableCell>
171
- <TableCell>{certification?.category}</TableCell>
172
- <TableCell>
173
- {item.validUntil ? formatDate(item.validUntil) : t('notAvailable')}
174
- </TableCell>
175
- <TableCell>{item.grantedScore}</TableCell>
176
- <TableCell>
177
- <StatusBadge
178
- label={humanizeGrowthStatus(item.status)}
179
- className={getGrowthCertificationBadgeClasses(item.status)}
180
- />
181
- </TableCell>
182
- </TableRow>
183
- );
184
- })}
185
- </TableBody>
186
- </Table>
187
- <PaginationFooter
188
- currentPage={safePage}
189
- pageSize={pageSize}
190
- totalItems={filteredEmployeeCertifications.length}
191
- onPageChange={setCurrentPage}
192
- onPageSizeChange={(value) => {
193
- setPageSize(value);
194
- setCurrentPage(1);
195
- }}
196
- pageSizeOptions={PAGE_SIZE_OPTIONS}
197
- />
198
- </div>
199
- </SectionCard>
200
- </Page>
201
- );
202
- }