@hed-hog/operations 0.0.2 → 0.0.285

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 (108) hide show
  1. package/README.md +122 -0
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +1 -0
  5. package/dist/index.js.map +1 -1
  6. package/dist/operations-data.controller.d.ts +139 -0
  7. package/dist/operations-data.controller.d.ts.map +1 -0
  8. package/dist/operations-data.controller.js +113 -0
  9. package/dist/operations-data.controller.js.map +1 -0
  10. package/dist/operations-growth.controller.d.ts +48 -0
  11. package/dist/operations-growth.controller.d.ts.map +1 -0
  12. package/dist/operations-growth.controller.js +90 -0
  13. package/dist/operations-growth.controller.js.map +1 -0
  14. package/dist/operations.module.d.ts.map +1 -1
  15. package/dist/operations.module.js +10 -4
  16. package/dist/operations.module.js.map +1 -1
  17. package/dist/operations.service.d.ts +178 -0
  18. package/dist/operations.service.d.ts.map +1 -0
  19. package/dist/operations.service.js +134 -0
  20. package/dist/operations.service.js.map +1 -0
  21. package/hedhog/data/menu.yaml +251 -132
  22. package/hedhog/data/operations_career_level.yaml +102 -0
  23. package/hedhog/data/operations_career_track.yaml +8 -0
  24. package/hedhog/data/operations_certification.yaml +38 -0
  25. package/hedhog/data/operations_evaluation_cycle.yaml +18 -0
  26. package/hedhog/data/operations_performance_criterion.yaml +48 -0
  27. package/hedhog/data/role.yaml +14 -7
  28. package/hedhog/data/route.yaml +143 -80
  29. package/hedhog/frontend/app/_components/allocation-calendar.tsx.ejs +56 -56
  30. package/hedhog/frontend/app/_components/kanban-board.tsx.ejs +83 -83
  31. package/hedhog/frontend/app/_components/operations-header.tsx.ejs +29 -29
  32. package/hedhog/frontend/app/_components/section-card.tsx.ejs +32 -32
  33. package/hedhog/frontend/app/_components/status-badge.tsx.ejs +15 -15
  34. package/hedhog/frontend/app/_components/timesheet-entry-dialog.tsx.ejs +142 -142
  35. package/hedhog/frontend/app/_lib/hooks/use-operations-data.ts.ejs +41 -41
  36. package/hedhog/frontend/app/_lib/hooks/use-operations-growth-data.ts.ejs +63 -0
  37. package/hedhog/frontend/app/_lib/mocks/allocations.mock.ts.ejs +74 -74
  38. package/hedhog/frontend/app/_lib/mocks/contracts.mock.ts.ejs +74 -74
  39. package/hedhog/frontend/app/_lib/mocks/operations-growth.mock.ts.ejs +824 -0
  40. package/hedhog/frontend/app/_lib/mocks/projects.mock.ts.ejs +60 -60
  41. package/hedhog/frontend/app/_lib/mocks/tasks.mock.ts.ejs +88 -88
  42. package/hedhog/frontend/app/_lib/mocks/timesheets.mock.ts.ejs +84 -84
  43. package/hedhog/frontend/app/_lib/mocks/users.mock.ts.ejs +67 -67
  44. package/hedhog/frontend/app/_lib/services/contracts.service.ts.ejs +10 -10
  45. package/hedhog/frontend/app/_lib/services/operations-growth.service.ts.ejs +31 -0
  46. package/hedhog/frontend/app/_lib/services/projects.service.ts.ejs +10 -10
  47. package/hedhog/frontend/app/_lib/services/tasks.service.ts.ejs +10 -10
  48. package/hedhog/frontend/app/_lib/services/timesheets.service.ts.ejs +10 -10
  49. package/hedhog/frontend/app/_lib/types/operations-growth.ts.ejs +209 -0
  50. package/hedhog/frontend/app/_lib/types/operations.ts.ejs +95 -95
  51. package/hedhog/frontend/app/_lib/utils/format.ts.ejs +25 -25
  52. package/hedhog/frontend/app/_lib/utils/growth.ts.ejs +62 -0
  53. package/hedhog/frontend/app/_lib/utils/metrics.ts.ejs +103 -103
  54. package/hedhog/frontend/app/_lib/utils/status.ts.ejs +80 -80
  55. package/hedhog/frontend/app/allocations/page.tsx.ejs +154 -99
  56. package/hedhog/frontend/app/approvals/page.tsx.ejs +147 -147
  57. package/hedhog/frontend/app/career/page.tsx.ejs +143 -0
  58. package/hedhog/frontend/app/certifications/page.tsx.ejs +201 -0
  59. package/hedhog/frontend/app/contracts/[id]/page.tsx.ejs +108 -108
  60. package/hedhog/frontend/app/contracts/page.tsx.ejs +180 -124
  61. package/hedhog/frontend/app/evaluations/page.tsx.ejs +277 -0
  62. package/hedhog/frontend/app/goals/page.tsx.ejs +170 -0
  63. package/hedhog/frontend/app/growth/page.tsx.ejs +288 -0
  64. package/hedhog/frontend/app/layout.tsx.ejs +9 -9
  65. package/hedhog/frontend/app/manager/page.tsx.ejs +175 -0
  66. package/hedhog/frontend/app/page.tsx.ejs +177 -177
  67. package/hedhog/frontend/app/projects/[id]/page.tsx.ejs +186 -186
  68. package/hedhog/frontend/app/projects/page.tsx.ejs +111 -111
  69. package/hedhog/frontend/app/rewards/page.tsx.ejs +195 -0
  70. package/hedhog/frontend/app/tasks/page.tsx.ejs +47 -47
  71. package/hedhog/frontend/app/timesheets/page.tsx.ejs +126 -126
  72. package/hedhog/frontend/messages/en.json +152 -142
  73. package/hedhog/frontend/messages/pt.json +152 -142
  74. package/hedhog/table/operations_allocation.yaml +52 -0
  75. package/hedhog/table/operations_calibration_item.yaml +61 -0
  76. package/hedhog/table/operations_calibration_session.yaml +25 -0
  77. package/hedhog/table/operations_career_level.yaml +75 -0
  78. package/hedhog/table/operations_career_track.yaml +21 -0
  79. package/hedhog/table/operations_certification.yaml +48 -0
  80. package/hedhog/table/operations_contract.yaml +57 -0
  81. package/hedhog/table/operations_employee.yaml +64 -0
  82. package/hedhog/table/operations_employee_certification.yaml +43 -0
  83. package/hedhog/table/operations_employee_connect.yaml +61 -0
  84. package/hedhog/table/operations_employee_evaluation.yaml +113 -0
  85. package/hedhog/table/operations_employee_evaluation_item.yaml +39 -0
  86. package/hedhog/table/operations_employee_profile.yaml +80 -0
  87. package/hedhog/table/operations_employee_skill_matrix.yaml +30 -0
  88. package/hedhog/table/operations_evaluation_cycle.yaml +31 -0
  89. package/hedhog/table/operations_goal.yaml +67 -0
  90. package/hedhog/table/operations_goal_progress.yaml +31 -0
  91. package/hedhog/table/operations_performance_criterion.yaml +29 -0
  92. package/hedhog/table/operations_project.yaml +66 -0
  93. package/hedhog/table/operations_promotion_readiness.yaml +49 -0
  94. package/hedhog/table/operations_promotion_recommendation.yaml +63 -0
  95. package/hedhog/table/operations_public_recognition.yaml +46 -0
  96. package/hedhog/table/operations_reward.yaml +100 -0
  97. package/hedhog/table/operations_score_event.yaml +81 -0
  98. package/hedhog/table/operations_task.yaml +60 -0
  99. package/hedhog/table/operations_timesheet.yaml +49 -0
  100. package/hedhog/table/operations_timesheet_entry.yaml +51 -0
  101. package/package.json +3 -3
  102. package/src/index.ts +2 -1
  103. package/src/language/en.json +8 -8
  104. package/src/language/pt.json +8 -8
  105. package/src/operations-data.controller.ts +54 -0
  106. package/src/operations-growth.controller.ts +44 -0
  107. package/src/operations.module.ts +21 -15
  108. package/src/operations.service.ts +137 -0
@@ -1,147 +1,147 @@
1
- 'use client';
2
-
3
- import { Page } from '@/components/entity-list';
4
- import { Button } from '@/components/ui/button';
5
- import { Input } from '@/components/ui/input';
6
- import {
7
- Table,
8
- TableBody,
9
- TableCell,
10
- TableHead,
11
- TableHeader,
12
- TableRow,
13
- } from '@/components/ui/table';
14
- import { useTranslations } from 'next-intl';
15
- import { useMemo, useState } from 'react';
16
- import { OperationsHeader } from '../_components/operations-header';
17
- import { SectionCard } from '../_components/section-card';
18
- import { StatusBadge } from '../_components/status-badge';
19
- import { useOperationsData } from '../_lib/hooks/use-operations-data';
20
- import { formatDate, formatHours } from '../_lib/utils/format';
21
- import { getApprovalBadgeClasses, getApprovalLabel } from '../_lib/utils/status';
22
-
23
- export default function ApprovalsPage() {
24
- const t = useTranslations('operations.ApprovalsPage');
25
- const { timesheets, users, projects, approvalSummary } = useOperationsData();
26
- const [search, setSearch] = useState('');
27
- const [statuses, setStatuses] = useState<
28
- Record<string, 'approved' | 'pending' | 'rejected'>
29
- >(() =>
30
- timesheets.reduce(
31
- (acc, entry) => {
32
- acc[entry.id] = entry.status;
33
- return acc;
34
- },
35
- {} as Record<string, 'approved' | 'pending' | 'rejected'>
36
- )
37
- );
38
-
39
- const approvalQueue = useMemo(
40
- () =>
41
- timesheets.filter((entry) => {
42
- const user = users.find((item) => item.id === entry.userId);
43
- const project = projects.find((item) => item.id === entry.projectId);
44
-
45
- return `${user?.name} ${project?.name} ${entry.description}`
46
- .toLowerCase()
47
- .includes(search.toLowerCase());
48
- }),
49
- [timesheets, users, projects, search]
50
- );
51
-
52
- return (
53
- <Page>
54
- <OperationsHeader
55
- title={t('title')}
56
- description={t('description')}
57
- current={t('breadcrumb')}
58
- />
59
-
60
- <div className="grid gap-4 md:grid-cols-3">
61
- <SectionCard title={t('cards.pending')}>
62
- <p className="text-3xl font-semibold">{approvalSummary.pending}</p>
63
- </SectionCard>
64
- <SectionCard title={t('cards.approved')}>
65
- <p className="text-3xl font-semibold">{approvalSummary.approved}</p>
66
- </SectionCard>
67
- <SectionCard title={t('cards.rejected')}>
68
- <p className="text-3xl font-semibold">{approvalSummary.rejected}</p>
69
- </SectionCard>
70
- </div>
71
-
72
- <SectionCard title={t('queueTitle')} description={t('queueDescription')}>
73
- <div className="mb-4">
74
- <Input
75
- value={search}
76
- onChange={(event) => setSearch(event.target.value)}
77
- placeholder={t('searchPlaceholder')}
78
- />
79
- </div>
80
-
81
- <Table>
82
- <TableHeader>
83
- <TableRow>
84
- <TableHead>{t('columns.user')}</TableHead>
85
- <TableHead>{t('columns.project')}</TableHead>
86
- <TableHead>{t('columns.date')}</TableHead>
87
- <TableHead>{t('columns.hours')}</TableHead>
88
- <TableHead>{t('columns.description')}</TableHead>
89
- <TableHead>{t('columns.status')}</TableHead>
90
- <TableHead>{t('columns.actions')}</TableHead>
91
- </TableRow>
92
- </TableHeader>
93
- <TableBody>
94
- {approvalQueue.map((entry) => {
95
- const user = users.find((item) => item.id === entry.userId);
96
- const project = projects.find((item) => item.id === entry.projectId);
97
- const currentStatus = statuses[entry.id] ?? 'pending';
98
-
99
- return (
100
- <TableRow key={entry.id}>
101
- <TableCell>{user?.name}</TableCell>
102
- <TableCell>{project?.name}</TableCell>
103
- <TableCell>{formatDate(entry.date)}</TableCell>
104
- <TableCell>{formatHours(entry.hours)}</TableCell>
105
- <TableCell>{entry.description}</TableCell>
106
- <TableCell>
107
- <StatusBadge
108
- label={getApprovalLabel(currentStatus)}
109
- className={getApprovalBadgeClasses(currentStatus)}
110
- />
111
- </TableCell>
112
- <TableCell>
113
- <div className="flex gap-2">
114
- <Button
115
- size="sm"
116
- onClick={() =>
117
- setStatuses((current) => ({
118
- ...current,
119
- [entry.id]: 'approved',
120
- }))
121
- }
122
- >
123
- Approve
124
- </Button>
125
- <Button
126
- size="sm"
127
- variant="outline"
128
- onClick={() =>
129
- setStatuses((current) => ({
130
- ...current,
131
- [entry.id]: 'rejected',
132
- }))
133
- }
134
- >
135
- Reject
136
- </Button>
137
- </div>
138
- </TableCell>
139
- </TableRow>
140
- );
141
- })}
142
- </TableBody>
143
- </Table>
144
- </SectionCard>
145
- </Page>
146
- );
147
- }
1
+ 'use client';
2
+
3
+ import { Page } from '@/components/entity-list';
4
+ import { Button } from '@/components/ui/button';
5
+ import { Input } from '@/components/ui/input';
6
+ import {
7
+ Table,
8
+ TableBody,
9
+ TableCell,
10
+ TableHead,
11
+ TableHeader,
12
+ TableRow,
13
+ } from '@/components/ui/table';
14
+ import { useTranslations } from 'next-intl';
15
+ import { useMemo, useState } from 'react';
16
+ import { OperationsHeader } from '../_components/operations-header';
17
+ import { SectionCard } from '../_components/section-card';
18
+ import { StatusBadge } from '../_components/status-badge';
19
+ import { useOperationsData } from '../_lib/hooks/use-operations-data';
20
+ import { formatDate, formatHours } from '../_lib/utils/format';
21
+ import { getApprovalBadgeClasses, getApprovalLabel } from '../_lib/utils/status';
22
+
23
+ export default function ApprovalsPage() {
24
+ const t = useTranslations('operations.ApprovalsPage');
25
+ const { timesheets, users, projects, approvalSummary } = useOperationsData();
26
+ const [search, setSearch] = useState('');
27
+ const [statuses, setStatuses] = useState<
28
+ Record<string, 'approved' | 'pending' | 'rejected'>
29
+ >(() =>
30
+ timesheets.reduce(
31
+ (acc, entry) => {
32
+ acc[entry.id] = entry.status;
33
+ return acc;
34
+ },
35
+ {} as Record<string, 'approved' | 'pending' | 'rejected'>
36
+ )
37
+ );
38
+
39
+ const approvalQueue = useMemo(
40
+ () =>
41
+ timesheets.filter((entry) => {
42
+ const user = users.find((item) => item.id === entry.userId);
43
+ const project = projects.find((item) => item.id === entry.projectId);
44
+
45
+ return `${user?.name} ${project?.name} ${entry.description}`
46
+ .toLowerCase()
47
+ .includes(search.toLowerCase());
48
+ }),
49
+ [timesheets, users, projects, search]
50
+ );
51
+
52
+ return (
53
+ <Page>
54
+ <OperationsHeader
55
+ title={t('title')}
56
+ description={t('description')}
57
+ current={t('breadcrumb')}
58
+ />
59
+
60
+ <div className="grid gap-4 md:grid-cols-3">
61
+ <SectionCard title={t('cards.pending')}>
62
+ <p className="text-3xl font-semibold">{approvalSummary.pending}</p>
63
+ </SectionCard>
64
+ <SectionCard title={t('cards.approved')}>
65
+ <p className="text-3xl font-semibold">{approvalSummary.approved}</p>
66
+ </SectionCard>
67
+ <SectionCard title={t('cards.rejected')}>
68
+ <p className="text-3xl font-semibold">{approvalSummary.rejected}</p>
69
+ </SectionCard>
70
+ </div>
71
+
72
+ <SectionCard title={t('queueTitle')} description={t('queueDescription')}>
73
+ <div className="mb-4">
74
+ <Input
75
+ value={search}
76
+ onChange={(event) => setSearch(event.target.value)}
77
+ placeholder={t('searchPlaceholder')}
78
+ />
79
+ </div>
80
+
81
+ <Table>
82
+ <TableHeader>
83
+ <TableRow>
84
+ <TableHead>{t('columns.user')}</TableHead>
85
+ <TableHead>{t('columns.project')}</TableHead>
86
+ <TableHead>{t('columns.date')}</TableHead>
87
+ <TableHead>{t('columns.hours')}</TableHead>
88
+ <TableHead>{t('columns.description')}</TableHead>
89
+ <TableHead>{t('columns.status')}</TableHead>
90
+ <TableHead>{t('columns.actions')}</TableHead>
91
+ </TableRow>
92
+ </TableHeader>
93
+ <TableBody>
94
+ {approvalQueue.map((entry) => {
95
+ const user = users.find((item) => item.id === entry.userId);
96
+ const project = projects.find((item) => item.id === entry.projectId);
97
+ const currentStatus = statuses[entry.id] ?? 'pending';
98
+
99
+ return (
100
+ <TableRow key={entry.id}>
101
+ <TableCell>{user?.name}</TableCell>
102
+ <TableCell>{project?.name}</TableCell>
103
+ <TableCell>{formatDate(entry.date)}</TableCell>
104
+ <TableCell>{formatHours(entry.hours)}</TableCell>
105
+ <TableCell>{entry.description}</TableCell>
106
+ <TableCell>
107
+ <StatusBadge
108
+ label={getApprovalLabel(currentStatus)}
109
+ className={getApprovalBadgeClasses(currentStatus)}
110
+ />
111
+ </TableCell>
112
+ <TableCell>
113
+ <div className="flex gap-2">
114
+ <Button
115
+ size="sm"
116
+ onClick={() =>
117
+ setStatuses((current) => ({
118
+ ...current,
119
+ [entry.id]: 'approved',
120
+ }))
121
+ }
122
+ >
123
+ Approve
124
+ </Button>
125
+ <Button
126
+ size="sm"
127
+ variant="outline"
128
+ onClick={() =>
129
+ setStatuses((current) => ({
130
+ ...current,
131
+ [entry.id]: 'rejected',
132
+ }))
133
+ }
134
+ >
135
+ Reject
136
+ </Button>
137
+ </div>
138
+ </TableCell>
139
+ </TableRow>
140
+ );
141
+ })}
142
+ </TableBody>
143
+ </Table>
144
+ </SectionCard>
145
+ </Page>
146
+ );
147
+ }
@@ -0,0 +1,143 @@
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
+ }
@@ -0,0 +1,201 @@
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
+
27
+ export default function OperationsCertificationsPage() {
28
+ const t = useTranslations('operations.CertificationsPage');
29
+ const {
30
+ users,
31
+ careerLevels,
32
+ certificationCatalog,
33
+ employeeCertifications,
34
+ } = useOperationsGrowthData();
35
+ const [searchInput, setSearchInput] = useState('');
36
+ const [search, setSearch] = useState('');
37
+ const [employeeFilter, setEmployeeFilter] = useState('all');
38
+ const [categoryFilter, setCategoryFilter] = useState('all');
39
+ const [currentPage, setCurrentPage] = useState(1);
40
+ const [pageSize, setPageSize] = useState(PAGE_SIZE_OPTIONS[0]);
41
+
42
+ const categories = Array.from(new Set(certificationCatalog.map((item) => item.category)));
43
+
44
+ const filteredEmployeeCertifications = useMemo(
45
+ () =>
46
+ employeeCertifications.filter((item) => {
47
+ const employee = users.find((user) => user.id === item.employeeId);
48
+ const certification = certificationCatalog.find(
49
+ (catalogItem) => catalogItem.id === item.certificationId
50
+ );
51
+ const haystack = `${employee?.name ?? ''} ${certification?.name ?? ''} ${certification?.provider ?? ''}`;
52
+ const matchesSearch = haystack.toLowerCase().includes(search.toLowerCase());
53
+ const matchesEmployee =
54
+ employeeFilter === 'all' || item.employeeId === employeeFilter;
55
+ const matchesCategory =
56
+ categoryFilter === 'all' || certification?.category === categoryFilter;
57
+
58
+ return matchesSearch && matchesEmployee && matchesCategory;
59
+ }),
60
+ [categoryFilter, certificationCatalog, employeeCertifications, employeeFilter, search, users]
61
+ );
62
+
63
+ const totalPages = Math.max(1, Math.ceil(filteredEmployeeCertifications.length / pageSize));
64
+ const safePage = Math.min(Math.max(currentPage, 1), totalPages);
65
+ const paginatedEmployeeCertifications = filteredEmployeeCertifications.slice(
66
+ (safePage - 1) * pageSize,
67
+ safePage * pageSize
68
+ );
69
+
70
+ return (
71
+ <Page>
72
+ <OperationsHeader
73
+ title={t('title')}
74
+ description={t('description')}
75
+ current={t('breadcrumb')}
76
+ />
77
+
78
+ <SearchBar
79
+ className="mb-6"
80
+ searchQuery={searchInput}
81
+ onSearchChange={setSearchInput}
82
+ onSearch={() => {
83
+ setSearch(searchInput);
84
+ setCurrentPage(1);
85
+ }}
86
+ placeholder={t('searchPlaceholder')}
87
+ controls={[
88
+ {
89
+ id: 'employee-filter',
90
+ type: 'select',
91
+ value: employeeFilter,
92
+ onChange: (value) => {
93
+ setEmployeeFilter(value);
94
+ setCurrentPage(1);
95
+ },
96
+ placeholder: t('filters.allEmployees'),
97
+ options: [
98
+ { value: 'all', label: t('filters.allEmployees') },
99
+ ...users.map((item) => ({ value: item.id, label: item.name })),
100
+ ],
101
+ },
102
+ {
103
+ id: 'category-filter',
104
+ type: 'select',
105
+ value: categoryFilter,
106
+ onChange: (value) => {
107
+ setCategoryFilter(value);
108
+ setCurrentPage(1);
109
+ },
110
+ placeholder: t('filters.allCategories'),
111
+ options: [
112
+ { value: 'all', label: t('filters.allCategories') },
113
+ ...categories.map((item) => ({ value: item, label: item })),
114
+ ],
115
+ },
116
+ ]}
117
+ />
118
+
119
+ <SectionCard title={t('catalogTitle')} description={t('catalogDescription')}>
120
+ <div className="grid gap-4 lg:grid-cols-2">
121
+ {certificationCatalog.map((item) => {
122
+ const requiredLevel = careerLevels.find(
123
+ (level) => level.id === item.requiredLevelId
124
+ );
125
+
126
+ return (
127
+ <div key={item.id} className="rounded-lg border p-4">
128
+ <div className="mb-3 flex items-start justify-between gap-3">
129
+ <div>
130
+ <p className="font-medium">{item.name}</p>
131
+ <p className="text-muted-foreground text-sm">{item.provider}</p>
132
+ </div>
133
+ <Badge variant="secondary">+{item.defaultScore}</Badge>
134
+ </div>
135
+ <div className="mb-3 flex flex-wrap gap-2">
136
+ <Badge variant="outline">{item.category}</Badge>
137
+ {requiredLevel ? <Badge variant="outline">{requiredLevel.name}</Badge> : null}
138
+ </div>
139
+ <p className="text-muted-foreground text-sm">{item.description}</p>
140
+ </div>
141
+ );
142
+ })}
143
+ </div>
144
+ </SectionCard>
145
+
146
+ <SectionCard title={t('employeeTitle')} description={t('employeeDescription')}>
147
+ <div className="space-y-4">
148
+ <Table>
149
+ <TableHeader>
150
+ <TableRow>
151
+ <TableHead>{t('columns.employee')}</TableHead>
152
+ <TableHead>{t('columns.certification')}</TableHead>
153
+ <TableHead>{t('columns.category')}</TableHead>
154
+ <TableHead>{t('columns.validity')}</TableHead>
155
+ <TableHead>{t('columns.score')}</TableHead>
156
+ <TableHead>{t('columns.status')}</TableHead>
157
+ </TableRow>
158
+ </TableHeader>
159
+ <TableBody>
160
+ {paginatedEmployeeCertifications.map((item) => {
161
+ const employee = users.find((user) => user.id === item.employeeId);
162
+ const certification = certificationCatalog.find(
163
+ (catalogItem) => catalogItem.id === item.certificationId
164
+ );
165
+
166
+ return (
167
+ <TableRow key={item.id}>
168
+ <TableCell className="font-medium">{employee?.name}</TableCell>
169
+ <TableCell>{certification?.name}</TableCell>
170
+ <TableCell>{certification?.category}</TableCell>
171
+ <TableCell>
172
+ {item.validUntil ? formatDate(item.validUntil) : t('notAvailable')}
173
+ </TableCell>
174
+ <TableCell>{item.grantedScore}</TableCell>
175
+ <TableCell>
176
+ <StatusBadge
177
+ label={humanizeGrowthStatus(item.status)}
178
+ className={getGrowthCertificationBadgeClasses(item.status)}
179
+ />
180
+ </TableCell>
181
+ </TableRow>
182
+ );
183
+ })}
184
+ </TableBody>
185
+ </Table>
186
+ <PaginationFooter
187
+ currentPage={safePage}
188
+ pageSize={pageSize}
189
+ totalItems={filteredEmployeeCertifications.length}
190
+ onPageChange={setCurrentPage}
191
+ onPageSizeChange={(value) => {
192
+ setPageSize(value);
193
+ setCurrentPage(1);
194
+ }}
195
+ pageSizeOptions={PAGE_SIZE_OPTIONS}
196
+ />
197
+ </div>
198
+ </SectionCard>
199
+ </Page>
200
+ );
201
+ }