@hed-hog/operations 0.0.3 → 0.0.286

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 +626 -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 +455 -60
  41. package/hedhog/frontend/app/_lib/mocks/tasks.mock.ts.ejs +117 -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 +156 -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 +155 -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 +202 -0
  59. package/hedhog/frontend/app/contracts/[id]/page.tsx.ejs +108 -108
  60. package/hedhog/frontend/app/contracts/page.tsx.ejs +181 -124
  61. package/hedhog/frontend/app/evaluations/page.tsx.ejs +278 -0
  62. package/hedhog/frontend/app/goals/page.tsx.ejs +171 -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 +936 -186
  68. package/hedhog/frontend/app/projects/page.tsx.ejs +1074 -111
  69. package/hedhog/frontend/app/rewards/page.tsx.ejs +196 -0
  70. package/hedhog/frontend/app/tasks/page.tsx.ejs +999 -47
  71. package/hedhog/frontend/app/timesheets/page.tsx.ejs +126 -126
  72. package/hedhog/frontend/messages/en.json +454 -142
  73. package/hedhog/frontend/messages/pt.json +454 -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 +4 -4
  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
@@ -0,0 +1,278 @@
1
+ 'use client';
2
+
3
+ import { Page, PaginationFooter, SearchBar } from '@/components/entity-list';
4
+ import { Button } from '@/components/ui/button';
5
+ import { Progress } from '@/components/ui/progress';
6
+ import {
7
+ Table,
8
+ TableBody,
9
+ TableCell,
10
+ TableHead,
11
+ TableHeader,
12
+ TableRow,
13
+ } from '@/components/ui/table';
14
+ import { FilePlus2 } from 'lucide-react';
15
+ import { useTranslations } from 'next-intl';
16
+ import { useMemo, useState } from 'react';
17
+ import { OperationsHeader } from '../_components/operations-header';
18
+ import { SectionCard } from '../_components/section-card';
19
+ import { StatusBadge } from '../_components/status-badge';
20
+ import { useOperationsGrowthData } from '../_lib/hooks/use-operations-growth-data';
21
+ import { formatDate } from '../_lib/utils/format';
22
+ import {
23
+ getGrowthEvaluationBadgeClasses,
24
+ humanizeGrowthStatus,
25
+ } from '../_lib/utils/growth';
26
+
27
+ const PAGE_SIZE_OPTIONS = [4, 8, 12];
28
+ const DEFAULT_PAGE_SIZE = PAGE_SIZE_OPTIONS[0] ?? 4;
29
+
30
+ export default function OperationsEvaluationsPage() {
31
+ const t = useTranslations('operations.EvaluationsPage');
32
+ const { evaluations, users, projects, tasks, evaluationCycles, performanceCriteria } =
33
+ useOperationsGrowthData();
34
+ const [searchInput, setSearchInput] = useState('');
35
+ const [search, setSearch] = useState('');
36
+ const [employeeFilter, setEmployeeFilter] = useState('all');
37
+ const [cycleFilter, setCycleFilter] = useState('all');
38
+ const [projectFilter, setProjectFilter] = useState('all');
39
+ const [currentPage, setCurrentPage] = useState(1);
40
+ const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);
41
+ const [selectedEvaluationId, setSelectedEvaluationId] = useState(evaluations[0]?.id ?? '');
42
+
43
+ const filteredEvaluations = useMemo(
44
+ () =>
45
+ evaluations.filter((evaluation) => {
46
+ const employee = users.find((item) => item.id === evaluation.employeeId);
47
+ const evaluator = users.find((item) => item.id === evaluation.evaluatorId);
48
+ const project = projects.find((item) => item.id === evaluation.projectId);
49
+ const cycle = evaluationCycles.find((item) => item.id === evaluation.cycleId);
50
+ const haystack = `${employee?.name ?? ''} ${evaluator?.name ?? ''} ${project?.name ?? ''} ${cycle?.name ?? ''}`;
51
+
52
+ const matchesSearch = haystack.toLowerCase().includes(search.toLowerCase());
53
+ const matchesEmployee =
54
+ employeeFilter === 'all' || evaluation.employeeId === employeeFilter;
55
+ const matchesCycle = cycleFilter === 'all' || evaluation.cycleId === cycleFilter;
56
+ const matchesProject =
57
+ projectFilter === 'all' || evaluation.projectId === projectFilter;
58
+
59
+ return matchesSearch && matchesEmployee && matchesCycle && matchesProject;
60
+ }),
61
+ [cycleFilter, employeeFilter, evaluationCycles, evaluations, projectFilter, projects, search, users]
62
+ );
63
+
64
+ const totalPages = Math.max(1, Math.ceil(filteredEvaluations.length / pageSize));
65
+ const safePage = Math.min(Math.max(currentPage, 1), totalPages);
66
+ const paginatedEvaluations = filteredEvaluations.slice(
67
+ (safePage - 1) * pageSize,
68
+ safePage * pageSize
69
+ );
70
+ const selectedEvaluation =
71
+ filteredEvaluations.find((item) => item.id === selectedEvaluationId) ??
72
+ paginatedEvaluations[0] ??
73
+ null;
74
+
75
+ const selectedEmployee = users.find(
76
+ (item) => item.id === selectedEvaluation?.employeeId
77
+ );
78
+ const selectedEvaluator = users.find(
79
+ (item) => item.id === selectedEvaluation?.evaluatorId
80
+ );
81
+ const selectedProject = projects.find(
82
+ (item) => item.id === selectedEvaluation?.projectId
83
+ );
84
+ const selectedTask = tasks.find((item) => item.id === selectedEvaluation?.taskId);
85
+
86
+ return (
87
+ <Page>
88
+ <OperationsHeader
89
+ title={t('title')}
90
+ description={t('description')}
91
+ current={t('breadcrumb')}
92
+ actions={
93
+ <Button className="gap-2" size="sm">
94
+ <FilePlus2 className="h-4 w-4" />
95
+ {t('newAction')}
96
+ </Button>
97
+ }
98
+ />
99
+
100
+ <SearchBar
101
+ className="mb-6"
102
+ searchQuery={searchInput}
103
+ onSearchChange={setSearchInput}
104
+ onSearch={() => {
105
+ setSearch(searchInput);
106
+ setCurrentPage(1);
107
+ }}
108
+ placeholder={t('searchPlaceholder')}
109
+ controls={[
110
+ {
111
+ id: 'employee-filter',
112
+ type: 'select',
113
+ value: employeeFilter,
114
+ onChange: (value) => {
115
+ setEmployeeFilter(value);
116
+ setCurrentPage(1);
117
+ },
118
+ placeholder: t('filters.allEmployees'),
119
+ options: [
120
+ { value: 'all', label: t('filters.allEmployees') },
121
+ ...users.map((item) => ({ value: item.id, label: item.name })),
122
+ ],
123
+ },
124
+ {
125
+ id: 'cycle-filter',
126
+ type: 'select',
127
+ value: cycleFilter,
128
+ onChange: (value) => {
129
+ setCycleFilter(value);
130
+ setCurrentPage(1);
131
+ },
132
+ placeholder: t('filters.allCycles'),
133
+ options: [
134
+ { value: 'all', label: t('filters.allCycles') },
135
+ ...evaluationCycles.map((item) => ({ value: item.id, label: item.name })),
136
+ ],
137
+ },
138
+ {
139
+ id: 'project-filter',
140
+ type: 'select',
141
+ value: projectFilter,
142
+ onChange: (value) => {
143
+ setProjectFilter(value);
144
+ setCurrentPage(1);
145
+ },
146
+ placeholder: t('filters.allProjects'),
147
+ options: [
148
+ { value: 'all', label: t('filters.allProjects') },
149
+ ...projects.map((item) => ({ value: item.id, label: item.name })),
150
+ ],
151
+ },
152
+ ]}
153
+ />
154
+
155
+ <div className="grid gap-4 xl:grid-cols-[1.25fr_0.95fr]">
156
+ <SectionCard title={t('tableTitle')} description={t('tableDescription')}>
157
+ <div className="space-y-4">
158
+ <Table>
159
+ <TableHeader>
160
+ <TableRow>
161
+ <TableHead>{t('columns.employee')}</TableHead>
162
+ <TableHead>{t('columns.evaluator')}</TableHead>
163
+ <TableHead>{t('columns.project')}</TableHead>
164
+ <TableHead>{t('columns.cycle')}</TableHead>
165
+ <TableHead>{t('columns.score')}</TableHead>
166
+ <TableHead>{t('columns.status')}</TableHead>
167
+ </TableRow>
168
+ </TableHeader>
169
+ <TableBody>
170
+ {paginatedEvaluations.map((evaluation) => {
171
+ const employee = users.find((item) => item.id === evaluation.employeeId);
172
+ const evaluator = users.find((item) => item.id === evaluation.evaluatorId);
173
+ const project = projects.find((item) => item.id === evaluation.projectId);
174
+ const cycle = evaluationCycles.find((item) => item.id === evaluation.cycleId);
175
+
176
+ return (
177
+ <TableRow
178
+ key={evaluation.id}
179
+ className="cursor-pointer"
180
+ onClick={() => setSelectedEvaluationId(evaluation.id)}
181
+ >
182
+ <TableCell className="font-medium">{employee?.name}</TableCell>
183
+ <TableCell>{evaluator?.name}</TableCell>
184
+ <TableCell>{project?.name ?? '-'}</TableCell>
185
+ <TableCell>{cycle?.name}</TableCell>
186
+ <TableCell>{evaluation.generatedScore}</TableCell>
187
+ <TableCell>
188
+ <StatusBadge
189
+ label={humanizeGrowthStatus(evaluation.status)}
190
+ className={getGrowthEvaluationBadgeClasses(evaluation.status)}
191
+ />
192
+ </TableCell>
193
+ </TableRow>
194
+ );
195
+ })}
196
+ </TableBody>
197
+ </Table>
198
+ <PaginationFooter
199
+ currentPage={safePage}
200
+ pageSize={pageSize}
201
+ totalItems={filteredEvaluations.length}
202
+ onPageChange={setCurrentPage}
203
+ onPageSizeChange={(value) => {
204
+ setPageSize(value);
205
+ setCurrentPage(1);
206
+ }}
207
+ pageSizeOptions={PAGE_SIZE_OPTIONS}
208
+ />
209
+ </div>
210
+ </SectionCard>
211
+
212
+ <SectionCard title={t('detailTitle')} description={t('detailDescription')}>
213
+ {selectedEvaluation ? (
214
+ <div className="space-y-5">
215
+ <div className="grid gap-3 rounded-lg border p-4 text-sm">
216
+ <div className="flex items-center justify-between">
217
+ <span className="text-muted-foreground">{t('detail.employee')}</span>
218
+ <span className="font-medium">{selectedEmployee?.name}</span>
219
+ </div>
220
+ <div className="flex items-center justify-between">
221
+ <span className="text-muted-foreground">{t('detail.evaluator')}</span>
222
+ <span className="font-medium">{selectedEvaluator?.name}</span>
223
+ </div>
224
+ <div className="flex items-center justify-between">
225
+ <span className="text-muted-foreground">{t('detail.project')}</span>
226
+ <span className="font-medium">{selectedProject?.name ?? '-'}</span>
227
+ </div>
228
+ <div className="flex items-center justify-between">
229
+ <span className="text-muted-foreground">{t('detail.task')}</span>
230
+ <span className="font-medium">{selectedTask?.title ?? '-'}</span>
231
+ </div>
232
+ <div className="flex items-center justify-between">
233
+ <span className="text-muted-foreground">{t('detail.date')}</span>
234
+ <span className="font-medium">{formatDate(selectedEvaluation.evaluationDate)}</span>
235
+ </div>
236
+ </div>
237
+
238
+ <div>
239
+ <p className="mb-3 text-sm font-medium">{t('detail.criteria')}</p>
240
+ <div className="space-y-4">
241
+ {selectedEvaluation.criteria.map((criterion) => {
242
+ const item = performanceCriteria.find(
243
+ (entry) => entry.id === criterion.criterionId
244
+ );
245
+
246
+ return (
247
+ <div key={criterion.criterionId} className="rounded-lg border p-3">
248
+ <div className="mb-2 flex items-center justify-between gap-3 text-sm">
249
+ <span className="font-medium">{item?.name}</span>
250
+ <span>{criterion.generatedScore} pts</span>
251
+ </div>
252
+ <Progress value={criterion.rating * 20} />
253
+ </div>
254
+ );
255
+ })}
256
+ </div>
257
+ </div>
258
+
259
+ <div className="rounded-lg border p-4">
260
+ <p className="text-sm font-medium">{t('detail.publicComment')}</p>
261
+ <p className="text-muted-foreground mt-2 text-sm">
262
+ {selectedEvaluation.publicComment}
263
+ </p>
264
+ </div>
265
+
266
+ <div className="rounded-lg border p-4">
267
+ <p className="text-sm font-medium">{t('detail.privateComment')}</p>
268
+ <p className="text-muted-foreground mt-2 text-sm">
269
+ {selectedEvaluation.privateComment}
270
+ </p>
271
+ </div>
272
+ </div>
273
+ ) : null}
274
+ </SectionCard>
275
+ </div>
276
+ </Page>
277
+ );
278
+ }
@@ -0,0 +1,171 @@
1
+ 'use client';
2
+
3
+ import { Page, PaginationFooter, SearchBar } from '@/components/entity-list';
4
+ import { Badge } from '@/components/ui/badge';
5
+ import { Progress } from '@/components/ui/progress';
6
+ import { Target } 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 { StatusBadge } from '../_components/status-badge';
12
+ import { useOperationsGrowthData } from '../_lib/hooks/use-operations-growth-data';
13
+ import { getGrowthGoalBadgeClasses, humanizeGrowthStatus } from '../_lib/utils/growth';
14
+
15
+ const PAGE_SIZE_OPTIONS = [4, 8, 12];
16
+ const DEFAULT_PAGE_SIZE = PAGE_SIZE_OPTIONS[0] ?? 4;
17
+
18
+ export default function OperationsGoalsPage() {
19
+ const t = useTranslations('operations.GoalsPage');
20
+ const { goals, users, projects } = useOperationsGrowthData();
21
+ const [searchInput, setSearchInput] = useState('');
22
+ const [search, setSearch] = useState('');
23
+ const [statusFilter, setStatusFilter] = useState('all');
24
+ const [currentPage, setCurrentPage] = useState(1);
25
+ const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE);
26
+
27
+ const filteredGoals = useMemo(
28
+ () =>
29
+ goals.filter((goal) => {
30
+ const owner = users.find((item) => item.id === goal.employeeId)?.name;
31
+ const project = projects.find((item) => item.id === goal.projectId)?.name;
32
+ const haystack = `${goal.title} ${goal.description} ${owner ?? ''} ${project ?? ''} ${goal.teamName ?? ''}`;
33
+ const matchesSearch = haystack.toLowerCase().includes(search.toLowerCase());
34
+ const matchesStatus = statusFilter === 'all' || goal.status === statusFilter;
35
+
36
+ return matchesSearch && matchesStatus;
37
+ }),
38
+ [goals, projects, search, statusFilter, users]
39
+ );
40
+
41
+ const totalPages = Math.max(1, Math.ceil(filteredGoals.length / pageSize));
42
+ const safePage = Math.min(Math.max(currentPage, 1), totalPages);
43
+ const paginatedGoals = filteredGoals.slice(
44
+ (safePage - 1) * pageSize,
45
+ safePage * pageSize
46
+ );
47
+
48
+ return (
49
+ <Page>
50
+ <OperationsHeader
51
+ title={t('title')}
52
+ description={t('description')}
53
+ current={t('breadcrumb')}
54
+ />
55
+
56
+ <SearchBar
57
+ className="mb-6"
58
+ searchQuery={searchInput}
59
+ onSearchChange={setSearchInput}
60
+ onSearch={() => {
61
+ setSearch(searchInput);
62
+ setCurrentPage(1);
63
+ }}
64
+ placeholder={t('searchPlaceholder')}
65
+ controls={[
66
+ {
67
+ id: 'status-filter',
68
+ type: 'select',
69
+ value: statusFilter,
70
+ onChange: (value) => {
71
+ setStatusFilter(value);
72
+ setCurrentPage(1);
73
+ },
74
+ placeholder: t('filters.allStatuses'),
75
+ options: [
76
+ { value: 'all', label: t('filters.allStatuses') },
77
+ { value: 'active', label: t('filters.active') },
78
+ { value: 'at_risk', label: t('filters.atRisk') },
79
+ { value: 'completed', label: t('filters.completed') },
80
+ { value: 'draft', label: t('filters.draft') },
81
+ ],
82
+ },
83
+ ]}
84
+ />
85
+
86
+ <SectionCard title={t('sectionTitle')} description={t('sectionDescription')}>
87
+ <div className="space-y-4">
88
+ <div className="grid gap-4 lg:grid-cols-2">
89
+ {paginatedGoals.map((goal) => {
90
+ const owner = users.find((item) => item.id === goal.employeeId)?.name;
91
+ const project = projects.find((item) => item.id === goal.projectId)?.name;
92
+
93
+ return (
94
+ <div key={goal.id} className="rounded-lg border p-4">
95
+ <div className="mb-3 flex items-start justify-between gap-3">
96
+ <div>
97
+ <p className="font-medium">{goal.title}</p>
98
+ <p className="text-muted-foreground text-sm">{goal.description}</p>
99
+ </div>
100
+ <StatusBadge
101
+ label={humanizeGrowthStatus(goal.status)}
102
+ className={getGrowthGoalBadgeClasses(goal.status)}
103
+ />
104
+ </div>
105
+ <div className="mb-3 flex flex-wrap gap-2">
106
+ <Badge variant="outline">{goal.periodLabel}</Badge>
107
+ <Badge variant="secondary">{owner ?? goal.teamName ?? t('sharedTeam')}</Badge>
108
+ {project ? <Badge variant="outline">{project}</Badge> : null}
109
+ </div>
110
+ <div className="space-y-2">
111
+ <div className="flex items-center justify-between text-sm">
112
+ <span>{t('progress')}</span>
113
+ <span>{goal.progressPercent}%</span>
114
+ </div>
115
+ <Progress value={goal.progressPercent} />
116
+ <div className="flex items-center justify-between text-sm">
117
+ <span className="text-muted-foreground">{t('scoreGenerated')}</span>
118
+ <span>
119
+ {goal.currentScore} / {goal.targetScore}
120
+ </span>
121
+ </div>
122
+ </div>
123
+ <div className="mt-4 rounded-md bg-slate-50 p-3 text-sm">
124
+ <span className="font-medium">{t('completionRule')}:</span> {goal.completionRule}
125
+ </div>
126
+ </div>
127
+ );
128
+ })}
129
+ </div>
130
+ <PaginationFooter
131
+ currentPage={safePage}
132
+ pageSize={pageSize}
133
+ totalItems={filteredGoals.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('highlightsTitle')} description={t('highlightsDescription')}>
145
+ <div className="grid gap-4 md:grid-cols-3">
146
+ <div className="rounded-lg border border-dashed border-slate-300 p-4">
147
+ <Target className="mb-3 h-5 w-5 text-blue-700" />
148
+ <p className="font-medium">{t('highlights.activeGoals')}</p>
149
+ <p className="mt-2 text-2xl font-bold">
150
+ {goals.filter((item) => item.status === 'active').length}
151
+ </p>
152
+ </div>
153
+ <div className="rounded-lg border border-dashed border-slate-300 p-4">
154
+ <Target className="mb-3 h-5 w-5 text-orange-700" />
155
+ <p className="font-medium">{t('highlights.atRiskGoals')}</p>
156
+ <p className="mt-2 text-2xl font-bold">
157
+ {goals.filter((item) => item.status === 'at_risk').length}
158
+ </p>
159
+ </div>
160
+ <div className="rounded-lg border border-dashed border-slate-300 p-4">
161
+ <Target className="mb-3 h-5 w-5 text-emerald-700" />
162
+ <p className="font-medium">{t('highlights.completedGoals')}</p>
163
+ <p className="mt-2 text-2xl font-bold">
164
+ {goals.filter((item) => item.status === 'completed').length}
165
+ </p>
166
+ </div>
167
+ </div>
168
+ </SectionCard>
169
+ </Page>
170
+ );
171
+ }