@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,288 +0,0 @@
1
- 'use client';
2
-
3
- import { EmptyState, Page, SearchBar } from '@/components/entity-list';
4
- import { KpiCard } from '@/components/ui/kpi-card';
5
- import { Badge } from '@/components/ui/badge';
6
- import { Progress } from '@/components/ui/progress';
7
- import {
8
- Line,
9
- LineChart,
10
- ResponsiveContainer,
11
- Tooltip,
12
- XAxis,
13
- YAxis,
14
- } from 'recharts';
15
- import { Award, ChevronRight, Flag, Sparkles, TrendingUp } from 'lucide-react';
16
- import { useTranslations } from 'next-intl';
17
- import { useMemo, useState } from 'react';
18
- import { OperationsHeader } from '../_components/operations-header';
19
- import { SectionCard } from '../_components/section-card';
20
- import { StatusBadge } from '../_components/status-badge';
21
- import { useOperationsGrowthData } from '../_lib/hooks/use-operations-growth-data';
22
- import { formatCurrency, formatDate } from '../_lib/utils/format';
23
- import {
24
- getGrowthPromotionBadgeClasses,
25
- humanizeGrowthStatus,
26
- } from '../_lib/utils/growth';
27
-
28
- export default function OperationsGrowthDashboardPage() {
29
- const t = useTranslations('operations.GrowthDashboardPage');
30
- const {
31
- users,
32
- profiles,
33
- careerTracks,
34
- careerLevels,
35
- goals,
36
- employeeCertifications,
37
- certificationCatalog,
38
- recognitions,
39
- scoreEvents,
40
- } = useOperationsGrowthData();
41
- const [searchInput, setSearchInput] = useState('');
42
- const [search, setSearch] = useState('');
43
- const [trackFilter, setTrackFilter] = useState('all');
44
-
45
- const filteredProfiles = useMemo(
46
- () =>
47
- profiles.filter((profile) => {
48
- const user = users.find((item) => item.id === profile.employeeId);
49
- const track = careerTracks.find((item) => item.id === profile.trackId);
50
- const matchesSearch = `${user?.name ?? ''} ${profile.story}`
51
- .toLowerCase()
52
- .includes(search.toLowerCase());
53
- const matchesTrack =
54
- trackFilter === 'all' || profile.trackId === trackFilter;
55
-
56
- return matchesSearch && matchesTrack;
57
- }),
58
- [careerTracks, profiles, search, trackFilter, users]
59
- );
60
-
61
- const selectedProfile = filteredProfiles[0];
62
-
63
- if (!selectedProfile) {
64
- return (
65
- <Page>
66
- <OperationsHeader
67
- title={t('title')}
68
- description={t('description')}
69
- current={t('breadcrumb')}
70
- />
71
- <EmptyState
72
- icon={<TrendingUp className="h-12 w-12" />}
73
- title={t('emptyState.title')}
74
- description={t('emptyState.description')}
75
- actionLabel={t('emptyState.action')}
76
- onAction={() => {
77
- setSearch('');
78
- setSearchInput('');
79
- setTrackFilter('all');
80
- }}
81
- />
82
- </Page>
83
- );
84
- }
85
-
86
- const employee = users.find((item) => item.id === selectedProfile.employeeId);
87
- const currentLevel = careerLevels.find(
88
- (item) => item.id === selectedProfile.currentLevelId
89
- );
90
- const nextLevel = careerLevels.find(
91
- (item) => item.id === selectedProfile.nextLevelId
92
- );
93
- const track = careerTracks.find((item) => item.id === selectedProfile.trackId);
94
- const employeeGoals = goals.filter(
95
- (item) => item.employeeId === selectedProfile.employeeId
96
- );
97
- const employeeRecognitions = recognitions.filter(
98
- (item) => item.employeeId === selectedProfile.employeeId
99
- );
100
- const employeeCerts = employeeCertifications.filter(
101
- (item) => item.employeeId === selectedProfile.employeeId
102
- );
103
- const scoreHistory = scoreEvents
104
- .filter((item) => item.employeeId === selectedProfile.employeeId)
105
- .map((item) => ({ date: formatDate(item.referenceDate), score: item.scoreDelta }));
106
-
107
- return (
108
- <Page>
109
- <OperationsHeader
110
- title={t('title')}
111
- description={t('description')}
112
- current={t('breadcrumb')}
113
- />
114
-
115
- <SearchBar
116
- className="mb-6"
117
- searchQuery={searchInput}
118
- onSearchChange={setSearchInput}
119
- onSearch={() => setSearch(searchInput)}
120
- placeholder={t('searchPlaceholder')}
121
- controls={[
122
- {
123
- id: 'track-filter',
124
- type: 'select',
125
- value: trackFilter,
126
- onChange: setTrackFilter,
127
- placeholder: t('filters.allTracks'),
128
- options: [
129
- { value: 'all', label: t('filters.allTracks') },
130
- ...careerTracks.map((item) => ({ value: item.id, label: item.name })),
131
- ],
132
- },
133
- ]}
134
- />
135
-
136
- <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
137
- <KpiCard title={t('cards.score')} value={selectedProfile.currentScore} icon={TrendingUp} />
138
- <KpiCard title={t('cards.currentLevel')} value={currentLevel?.name ?? '-'} icon={Award} />
139
- <KpiCard title={t('cards.nextLevel')} value={nextLevel?.name ?? t('cards.none')} icon={ChevronRight} />
140
- <KpiCard
141
- title={t('cards.certifications')}
142
- value={employeeCerts.length}
143
- description={t('cards.certificationsDescription')}
144
- icon={Sparkles}
145
- />
146
- </div>
147
-
148
- <div className="grid gap-4 xl:grid-cols-[1.3fr_0.9fr]">
149
- <SectionCard
150
- title={t('profile.title', { name: employee?.name ?? '-' })}
151
- description={t('profile.description', { track: track?.name ?? '-' })}
152
- >
153
- <div className="space-y-6">
154
- <div className="grid gap-4 md:grid-cols-2">
155
- <div className="space-y-3">
156
- <div className="flex items-center justify-between">
157
- <span className="text-sm font-medium">{t('profile.promotionProgress')}</span>
158
- <StatusBadge
159
- label={humanizeGrowthStatus(selectedProfile.promotionStatus)}
160
- className={getGrowthPromotionBadgeClasses(
161
- selectedProfile.promotionStatus
162
- )}
163
- />
164
- </div>
165
- <Progress value={selectedProfile.promotionProgress} />
166
- <p className="text-muted-foreground text-sm">
167
- {t('profile.progressLabel', {
168
- progress: selectedProfile.promotionProgress,
169
- score: selectedProfile.currentScore,
170
- target: selectedProfile.nextLevelScore,
171
- })}
172
- </p>
173
- </div>
174
- <div className="rounded-lg border border-dashed border-slate-300 p-4">
175
- <p className="text-sm font-medium">{t('profile.salaryBand')}</p>
176
- <p className="mt-2 text-lg font-semibold">
177
- {currentLevel
178
- ? `${formatCurrency(currentLevel.salaryMin)} - ${formatCurrency(
179
- currentLevel.salaryMax
180
- )}`
181
- : '-'}
182
- </p>
183
- <p className="text-muted-foreground mt-2 text-sm">{selectedProfile.story}</p>
184
- </div>
185
- </div>
186
-
187
- <div className="grid gap-4 md:grid-cols-2">
188
- <div>
189
- <p className="mb-3 text-sm font-medium">{t('profile.strengths')}</p>
190
- <div className="flex flex-wrap gap-2">
191
- {selectedProfile.strengths.map((item) => (
192
- <Badge key={item} variant="secondary">
193
- {item}
194
- </Badge>
195
- ))}
196
- </div>
197
- </div>
198
- <div>
199
- <p className="mb-3 text-sm font-medium">{t('profile.gaps')}</p>
200
- <div className="flex flex-wrap gap-2">
201
- {selectedProfile.gaps.map((item) => (
202
- <Badge key={item} variant="outline">
203
- {item}
204
- </Badge>
205
- ))}
206
- </div>
207
- </div>
208
- </div>
209
-
210
- <div className="grid gap-4 md:grid-cols-2">
211
- <div>
212
- <p className="mb-2 text-sm font-medium">{t('profile.goalsInProgress')}</p>
213
- <div className="space-y-3">
214
- {employeeGoals.map((goal) => (
215
- <div key={goal.id} className="rounded-lg border p-3">
216
- <div className="mb-2 flex items-center justify-between gap-3">
217
- <span className="font-medium">{goal.title}</span>
218
- <span className="text-muted-foreground text-sm">
219
- {goal.progressPercent}%
220
- </span>
221
- </div>
222
- <Progress value={goal.progressPercent} />
223
- </div>
224
- ))}
225
- </div>
226
- </div>
227
- <div>
228
- <p className="mb-2 text-sm font-medium">{t('profile.recentRecognition')}</p>
229
- <div className="space-y-3">
230
- {employeeRecognitions.slice(0, 3).map((recognition) => (
231
- <div key={recognition.id} className="rounded-lg border p-3">
232
- <p className="text-sm">{recognition.message}</p>
233
- <p className="text-muted-foreground mt-2 text-xs">
234
- {formatDate(recognition.date)}
235
- </p>
236
- </div>
237
- ))}
238
- </div>
239
- </div>
240
- </div>
241
- </div>
242
- </SectionCard>
243
-
244
- <SectionCard title={t('sidebar.title')} description={t('sidebar.description')}>
245
- <div className="space-y-4">
246
- <div className="rounded-lg border p-4">
247
- <p className="text-muted-foreground text-sm">{t('sidebar.currentTrack')}</p>
248
- <p className="mt-1 text-lg font-semibold">{track?.name ?? '-'}</p>
249
- </div>
250
- <div className="rounded-lg border p-4">
251
- <p className="text-muted-foreground text-sm">{t('sidebar.requiredCerts')}</p>
252
- <div className="mt-3 space-y-3">
253
- {employeeCerts.map((item) => {
254
- const certification = certificationCatalog.find(
255
- (catalogItem) => catalogItem.id === item.certificationId
256
- );
257
-
258
- return (
259
- <div key={item.id} className="flex items-start justify-between gap-3">
260
- <div>
261
- <p className="font-medium">{certification?.name}</p>
262
- <p className="text-muted-foreground text-xs">{certification?.provider}</p>
263
- </div>
264
- <Badge variant={item.status === 'achieved' ? 'default' : 'outline'}>
265
- {humanizeGrowthStatus(item.status)}
266
- </Badge>
267
- </div>
268
- );
269
- })}
270
- </div>
271
- </div>
272
- <div className="rounded-lg border p-4">
273
- <p className="mb-3 text-sm font-medium">{t('sidebar.scoreHistory')}</p>
274
- <ResponsiveContainer width="100%" height={180}>
275
- <LineChart data={scoreHistory}>
276
- <XAxis dataKey="date" tick={{ fontSize: 11 }} />
277
- <YAxis />
278
- <Tooltip />
279
- <Line dataKey="score" type="monotone" stroke="#0f766e" strokeWidth={3} />
280
- </LineChart>
281
- </ResponsiveContainer>
282
- </div>
283
- </div>
284
- </SectionCard>
285
- </div>
286
- </Page>
287
- );
288
- }
@@ -1,175 +0,0 @@
1
- 'use client';
2
-
3
- import { Page } from '@/components/entity-list';
4
- import { KpiCard } from '@/components/ui/kpi-card';
5
- import {
6
- Table,
7
- TableBody,
8
- TableCell,
9
- TableHead,
10
- TableHeader,
11
- TableRow,
12
- } from '@/components/ui/table';
13
- import { AlertTriangle, ArrowUp, Medal, Users } from 'lucide-react';
14
- import { useTranslations } from 'next-intl';
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 {
20
- getGrowthGoalBadgeClasses,
21
- getGrowthPromotionBadgeClasses,
22
- humanizeGrowthStatus,
23
- } from '../_lib/utils/growth';
24
-
25
- export default function OperationsManagerPage() {
26
- const t = useTranslations('operations.ManagerPage');
27
- const {
28
- ranking,
29
- managerSummary,
30
- users,
31
- goals,
32
- employeeCertifications,
33
- promotionRecommendations,
34
- careerLevels,
35
- } = useOperationsGrowthData();
36
-
37
- const atRiskGoals = goals.filter((item) => item.status === 'at_risk');
38
- const pendingCertifications = employeeCertifications.filter(
39
- (item) => item.status !== 'achieved'
40
- );
41
-
42
- return (
43
- <Page>
44
- <OperationsHeader
45
- title={t('title')}
46
- description={t('description')}
47
- current={t('breadcrumb')}
48
- />
49
-
50
- <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
51
- <KpiCard title={t('cards.promotionEligible')} value={managerSummary.promotionEligible} icon={ArrowUp} />
52
- <KpiCard title={t('cards.goalsAtRisk')} value={managerSummary.goalsAtRisk} icon={AlertTriangle} />
53
- <KpiCard title={t('cards.pendingCertifications')} value={managerSummary.pendingCertifications} icon={Medal} />
54
- <KpiCard title={t('cards.totalRecognitions')} value={managerSummary.totalRecognitions} icon={Users} />
55
- </div>
56
-
57
- <div className="grid gap-4 xl:grid-cols-[1.15fr_0.85fr]">
58
- <SectionCard title={t('rankingTitle')} description={t('rankingDescription')}>
59
- <Table>
60
- <TableHeader>
61
- <TableRow>
62
- <TableHead>{t('ranking.employee')}</TableHead>
63
- <TableHead>{t('ranking.score')}</TableHead>
64
- <TableHead>{t('ranking.progress')}</TableHead>
65
- <TableHead>{t('ranking.status')}</TableHead>
66
- </TableRow>
67
- </TableHeader>
68
- <TableBody>
69
- {ranking.map((profile) => {
70
- const employee = users.find((item) => item.id === profile.employeeId);
71
-
72
- return (
73
- <TableRow key={profile.employeeId}>
74
- <TableCell className="font-medium">{employee?.name}</TableCell>
75
- <TableCell>{profile.currentScore}</TableCell>
76
- <TableCell>{profile.promotionProgress}%</TableCell>
77
- <TableCell>
78
- <StatusBadge
79
- label={humanizeGrowthStatus(profile.promotionStatus)}
80
- className={getGrowthPromotionBadgeClasses(profile.promotionStatus)}
81
- />
82
- </TableCell>
83
- </TableRow>
84
- );
85
- })}
86
- </TableBody>
87
- </Table>
88
- </SectionCard>
89
-
90
- <SectionCard title={t('recommendationsTitle')} description={t('recommendationsDescription')}>
91
- <div className="space-y-4">
92
- {promotionRecommendations.map((recommendation) => {
93
- const employee = users.find((item) => item.id === recommendation.employeeId);
94
- const suggestedLevel = careerLevels.find(
95
- (item) => item.id === recommendation.suggestedLevelId
96
- );
97
-
98
- return (
99
- <div key={recommendation.id} className="rounded-lg border p-4">
100
- <div className="mb-2 flex items-center justify-between gap-3">
101
- <span className="font-medium">{employee?.name}</span>
102
- <StatusBadge
103
- label={humanizeGrowthStatus(recommendation.status)}
104
- className={getGrowthPromotionBadgeClasses(
105
- recommendation.eligible ? 'recommended' : 'developing'
106
- )}
107
- />
108
- </div>
109
- <p className="text-muted-foreground text-sm">
110
- {t('recommendations.nextLevel', { level: suggestedLevel?.name ?? '-' })}
111
- </p>
112
- <p className="mt-3 text-sm">{recommendation.justification}</p>
113
- </div>
114
- );
115
- })}
116
- </div>
117
- </SectionCard>
118
- </div>
119
-
120
- <div className="grid gap-4 xl:grid-cols-2">
121
- <SectionCard title={t('riskTitle')} description={t('riskDescription')}>
122
- <div className="space-y-3">
123
- {atRiskGoals.map((goal) => {
124
- const employee = users.find((item) => item.id === goal.employeeId);
125
-
126
- return (
127
- <div key={goal.id} className="rounded-lg border p-4">
128
- <div className="flex items-center justify-between gap-3">
129
- <div>
130
- <p className="font-medium">{goal.title}</p>
131
- <p className="text-muted-foreground text-sm">
132
- {employee?.name ?? goal.teamName}
133
- </p>
134
- </div>
135
- <StatusBadge
136
- label={humanizeGrowthStatus(goal.status)}
137
- className={getGrowthGoalBadgeClasses(goal.status)}
138
- />
139
- </div>
140
- </div>
141
- );
142
- })}
143
- </div>
144
- </SectionCard>
145
-
146
- <SectionCard title={t('certificationTitle')} description={t('certificationDescription')}>
147
- <div className="space-y-3">
148
- {pendingCertifications.map((certification) => {
149
- const employee = users.find((item) => item.id === certification.employeeId);
150
-
151
- return (
152
- <div key={certification.id} className="rounded-lg border p-4">
153
- <div className="flex items-center justify-between gap-3">
154
- <div>
155
- <p className="font-medium">{employee?.name}</p>
156
- <p className="text-muted-foreground text-sm">
157
- {humanizeGrowthStatus(certification.status)}
158
- </p>
159
- </div>
160
- <StatusBadge
161
- label={humanizeGrowthStatus(certification.status)}
162
- className={getGrowthGoalBadgeClasses(
163
- certification.status === 'planned' ? 'draft' : 'at_risk'
164
- )}
165
- />
166
- </div>
167
- </div>
168
- );
169
- })}
170
- </div>
171
- </SectionCard>
172
- </div>
173
- </Page>
174
- );
175
- }
@@ -1,196 +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 { Gift } from 'lucide-react';
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 { useOperationsGrowthData } from '../_lib/hooks/use-operations-growth-data';
20
- import { formatCurrency, formatDate } from '../_lib/utils/format';
21
- import { getGrowthRewardBadgeClasses, humanizeGrowthStatus } from '../_lib/utils/growth';
22
-
23
- const PAGE_SIZE_OPTIONS = [4, 8, 12];
24
- const DEFAULT_PAGE_SIZE = PAGE_SIZE_OPTIONS[0] ?? 4;
25
-
26
- export default function OperationsRewardsPage() {
27
- const t = useTranslations('operations.RewardsPage');
28
- const { rewards, recognitions, users, projects } = useOperationsGrowthData();
29
- const [searchInput, setSearchInput] = useState('');
30
- const [search, setSearch] = useState('');
31
- const [employeeFilter, setEmployeeFilter] = useState('all');
32
- const [currentPage, setCurrentPage] = useState(1);
33
- const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);
34
-
35
- const filteredRewards = useMemo(
36
- () =>
37
- rewards.filter((reward) => {
38
- const employee = users.find((item) => item.id === reward.employeeId);
39
- const haystack = `${employee?.name ?? ''} ${reward.description} ${reward.type} ${reward.origin}`;
40
- const matchesSearch = haystack.toLowerCase().includes(search.toLowerCase());
41
- const matchesEmployee =
42
- employeeFilter === 'all' || reward.employeeId === employeeFilter;
43
-
44
- return matchesSearch && matchesEmployee;
45
- }),
46
- [employeeFilter, rewards, search, users]
47
- );
48
-
49
- const totalPages = Math.max(1, Math.ceil(filteredRewards.length / pageSize));
50
- const safePage = Math.min(Math.max(currentPage, 1), totalPages);
51
- const paginatedRewards = filteredRewards.slice(
52
- (safePage - 1) * pageSize,
53
- safePage * pageSize
54
- );
55
-
56
- return (
57
- <Page>
58
- <OperationsHeader
59
- title={t('title')}
60
- description={t('description')}
61
- current={t('breadcrumb')}
62
- />
63
-
64
- <SearchBar
65
- className="mb-6"
66
- searchQuery={searchInput}
67
- onSearchChange={setSearchInput}
68
- onSearch={() => {
69
- setSearch(searchInput);
70
- setCurrentPage(1);
71
- }}
72
- placeholder={t('searchPlaceholder')}
73
- controls={[
74
- {
75
- id: 'employee-filter',
76
- type: 'select',
77
- value: employeeFilter,
78
- onChange: (value) => {
79
- setEmployeeFilter(value);
80
- setCurrentPage(1);
81
- },
82
- placeholder: t('filters.allEmployees'),
83
- options: [
84
- { value: 'all', label: t('filters.allEmployees') },
85
- ...users.map((item) => ({ value: item.id, label: item.name })),
86
- ],
87
- },
88
- ]}
89
- />
90
-
91
- <SectionCard title={t('rewardsTitle')} description={t('rewardsDescription')}>
92
- <div className="space-y-4">
93
- <Table>
94
- <TableHeader>
95
- <TableRow>
96
- <TableHead>{t('columns.employee')}</TableHead>
97
- <TableHead>{t('columns.type')}</TableHead>
98
- <TableHead>{t('columns.origin')}</TableHead>
99
- <TableHead>{t('columns.amount')}</TableHead>
100
- <TableHead>{t('columns.score')}</TableHead>
101
- <TableHead>{t('columns.status')}</TableHead>
102
- </TableRow>
103
- </TableHeader>
104
- <TableBody>
105
- {paginatedRewards.map((reward) => {
106
- const employee = users.find((item) => item.id === reward.employeeId);
107
-
108
- return (
109
- <TableRow key={reward.id}>
110
- <TableCell className="font-medium">{employee?.name}</TableCell>
111
- <TableCell>{humanizeGrowthStatus(reward.type)}</TableCell>
112
- <TableCell>{humanizeGrowthStatus(reward.origin)}</TableCell>
113
- <TableCell>
114
- {reward.amount && reward.currency
115
- ? formatCurrency(reward.amount)
116
- : t('notApplicable')}
117
- </TableCell>
118
- <TableCell>{reward.relatedScore ?? '-'}</TableCell>
119
- <TableCell>
120
- <StatusBadge
121
- label={humanizeGrowthStatus(reward.status)}
122
- className={getGrowthRewardBadgeClasses(reward.status)}
123
- />
124
- </TableCell>
125
- </TableRow>
126
- );
127
- })}
128
- </TableBody>
129
- </Table>
130
- <PaginationFooter
131
- currentPage={safePage}
132
- pageSize={pageSize}
133
- totalItems={filteredRewards.length}
134
- onPageChange={setCurrentPage}
135
- onPageSizeChange={(value) => {
136
- setPageSize(value);
137
- setCurrentPage(1);
138
- }}
139
- pageSizeOptions={PAGE_SIZE_OPTIONS}
140
- />
141
- </div>
142
- </SectionCard>
143
-
144
- <SectionCard title={t('recognitionTitle')} description={t('recognitionDescription')}>
145
- <div className="grid gap-4 lg:grid-cols-2">
146
- {recognitions.map((recognition) => {
147
- const employee = users.find((item) => item.id === recognition.employeeId);
148
- const author = users.find((item) => item.id === recognition.authorId);
149
- const project = projects.find((item) => item.id === recognition.projectId);
150
-
151
- return (
152
- <div key={recognition.id} className="rounded-lg border p-4">
153
- <div className="mb-3 flex items-center justify-between gap-3">
154
- <div>
155
- <p className="font-medium">{employee?.name}</p>
156
- <p className="text-muted-foreground text-sm">{author?.name}</p>
157
- </div>
158
- <Badge variant="secondary">+{recognition.relatedScore ?? 0}</Badge>
159
- </div>
160
- <p className="text-sm">{recognition.message}</p>
161
- <div className="text-muted-foreground mt-3 flex items-center justify-between text-xs">
162
- <span>{project?.name ?? t('withoutProject')}</span>
163
- <span>{formatDate(recognition.date)}</span>
164
- </div>
165
- </div>
166
- );
167
- })}
168
- </div>
169
- </SectionCard>
170
-
171
- <SectionCard title={t('highlightsTitle')} description={t('highlightsDescription')}>
172
- <div className="grid gap-4 md:grid-cols-3">
173
- <div className="rounded-lg border border-dashed border-slate-300 p-4">
174
- <Gift className="mb-3 h-5 w-5 text-emerald-700" />
175
- <p className="font-medium">{t('highlights.granted')}</p>
176
- <p className="mt-2 text-2xl font-bold">
177
- {rewards.filter((item) => item.status === 'granted').length}
178
- </p>
179
- </div>
180
- <div className="rounded-lg border border-dashed border-slate-300 p-4">
181
- <Gift className="mb-3 h-5 w-5 text-blue-700" />
182
- <p className="font-medium">{t('highlights.publicRecognition')}</p>
183
- <p className="mt-2 text-2xl font-bold">{recognitions.length}</p>
184
- </div>
185
- <div className="rounded-lg border border-dashed border-slate-300 p-4">
186
- <Gift className="mb-3 h-5 w-5 text-orange-700" />
187
- <p className="font-medium">{t('highlights.financial')}</p>
188
- <p className="mt-2 text-2xl font-bold">
189
- {rewards.filter((item) => item.type === 'financial' || item.type === 'bonus').length}
190
- </p>
191
- </div>
192
- </div>
193
- </SectionCard>
194
- </Page>
195
- );
196
- }