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