@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
@@ -1,186 +1,186 @@
1
- 'use client';
2
-
3
- import { Page } from '@/components/entity-list';
4
- import {
5
- Table,
6
- TableBody,
7
- TableCell,
8
- TableHead,
9
- TableHeader,
10
- TableRow,
11
- } from '@/components/ui/table';
12
- import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
13
- import { useParams } from 'next/navigation';
14
- import { KanbanBoard } from '../../_components/kanban-board';
15
- import { OperationsHeader } from '../../_components/operations-header';
16
- import { SectionCard } from '../../_components/section-card';
17
- import { StatusBadge } from '../../_components/status-badge';
18
- import { useOperationsData } from '../../_lib/hooks/use-operations-data';
19
- import {
20
- formatCurrency,
21
- formatDate,
22
- formatHours,
23
- } from '../../_lib/utils/format';
24
- import { getProjectTeam } from '../../_lib/utils/metrics';
25
- import {
26
- getApprovalBadgeClasses,
27
- getApprovalLabel,
28
- getProjectBadgeClasses,
29
- getProjectStatusLabel,
30
- } from '../../_lib/utils/status';
31
-
32
- export default function ProjectDetailsPage() {
33
- const params = useParams<{ id: string }>();
34
- const { projects, contracts, users, tasks, timesheets } = useOperationsData();
35
- const project = projects.find((item) => item.id === params.id);
36
-
37
- if (!project) {
38
- return <Page>Project not found.</Page>;
39
- }
40
-
41
- const contract = contracts.find((item) => item.id === project.contractId);
42
- const team = getProjectTeam(project.id);
43
- const projectTasks = tasks.filter((task) => task.projectId === project.id);
44
- const projectTimesheets = timesheets.filter(
45
- (entry) => entry.projectId === project.id
46
- );
47
-
48
- return (
49
- <Page>
50
- <OperationsHeader
51
- title={project.name}
52
- description={project.description}
53
- current="Project Details"
54
- />
55
-
56
- <Tabs defaultValue="overview" className="space-y-4">
57
- <TabsList>
58
- <TabsTrigger value="overview">Overview</TabsTrigger>
59
- <TabsTrigger value="team">Team</TabsTrigger>
60
- <TabsTrigger value="tasks">Tasks</TabsTrigger>
61
- <TabsTrigger value="timesheets">Timesheets</TabsTrigger>
62
- <TabsTrigger value="costs">Costs</TabsTrigger>
63
- </TabsList>
64
-
65
- <TabsContent value="overview">
66
- <SectionCard title="Project Overview">
67
- <div className="grid gap-4 text-sm md:grid-cols-2">
68
- <p>
69
- <span className="font-medium">Status:</span>{' '}
70
- <StatusBadge
71
- label={getProjectStatusLabel(project.status)}
72
- className={getProjectBadgeClasses(project.status)}
73
- />
74
- </p>
75
- <p>
76
- <span className="font-medium">Progress:</span> {project.progress}%
77
- </p>
78
- <p>
79
- <span className="font-medium">Client:</span> {project.client}
80
- </p>
81
- <p>
82
- <span className="font-medium">Contract:</span> {contract?.name}
83
- </p>
84
- <p className="md:col-span-2">
85
- <span className="font-medium">Description:</span>{' '}
86
- {project.description}
87
- </p>
88
- </div>
89
- </SectionCard>
90
- </TabsContent>
91
-
92
- <TabsContent value="team">
93
- <SectionCard title="Team Allocation">
94
- <Table>
95
- <TableHeader>
96
- <TableRow>
97
- <TableHead>Member</TableHead>
98
- <TableHead>Role</TableHead>
99
- <TableHead>Hour Rate</TableHead>
100
- <TableHead>Allocation %</TableHead>
101
- </TableRow>
102
- </TableHeader>
103
- <TableBody>
104
- {team.map((member) => (
105
- <TableRow key={member.id}>
106
- <TableCell>{member.name}</TableCell>
107
- <TableCell>{member.role}</TableCell>
108
- <TableCell>{formatCurrency(member.hourlyRate)}</TableCell>
109
- <TableCell>{member.utilizationTarget}%</TableCell>
110
- </TableRow>
111
- ))}
112
- </TableBody>
113
- </Table>
114
- </SectionCard>
115
- </TabsContent>
116
-
117
- <TabsContent value="tasks">
118
- <SectionCard title="Kanban Preview">
119
- <KanbanBoard tasks={projectTasks} users={users} />
120
- </SectionCard>
121
- </TabsContent>
122
-
123
- <TabsContent value="timesheets">
124
- <SectionCard title="Project Timesheets">
125
- <Table>
126
- <TableHeader>
127
- <TableRow>
128
- <TableHead>Date</TableHead>
129
- <TableHead>User</TableHead>
130
- <TableHead>Hours</TableHead>
131
- <TableHead>Description</TableHead>
132
- <TableHead>Status</TableHead>
133
- </TableRow>
134
- </TableHeader>
135
- <TableBody>
136
- {projectTimesheets.map((entry) => {
137
- const user = users.find((item) => item.id === entry.userId);
138
-
139
- return (
140
- <TableRow key={entry.id}>
141
- <TableCell>{formatDate(entry.date)}</TableCell>
142
- <TableCell>{user?.name}</TableCell>
143
- <TableCell>{formatHours(entry.hours)}</TableCell>
144
- <TableCell>{entry.description}</TableCell>
145
- <TableCell>
146
- <StatusBadge
147
- label={getApprovalLabel(entry.status)}
148
- className={getApprovalBadgeClasses(entry.status)}
149
- />
150
- </TableCell>
151
- </TableRow>
152
- );
153
- })}
154
- </TableBody>
155
- </Table>
156
- </SectionCard>
157
- </TabsContent>
158
-
159
- <TabsContent value="costs">
160
- <SectionCard title="Costs (Mock)">
161
- <div className="grid gap-3 md:grid-cols-3">
162
- <div className="rounded-lg border p-4">
163
- <p className="text-sm text-muted-foreground">Budget</p>
164
- <p className="text-xl font-semibold">
165
- {formatCurrency(project.budget)}
166
- </p>
167
- </div>
168
- <div className="rounded-lg border p-4">
169
- <p className="text-sm text-muted-foreground">Logged Hours</p>
170
- <p className="text-xl font-semibold">
171
- {formatHours(project.hoursLogged)}
172
- </p>
173
- </div>
174
- <div className="rounded-lg border p-4">
175
- <p className="text-sm text-muted-foreground">Projected End</p>
176
- <p className="text-xl font-semibold">
177
- {formatDate(project.endDate)}
178
- </p>
179
- </div>
180
- </div>
181
- </SectionCard>
182
- </TabsContent>
183
- </Tabs>
184
- </Page>
185
- );
186
- }
1
+ 'use client';
2
+
3
+ import { Page } from '@/components/entity-list';
4
+ import {
5
+ Table,
6
+ TableBody,
7
+ TableCell,
8
+ TableHead,
9
+ TableHeader,
10
+ TableRow,
11
+ } from '@/components/ui/table';
12
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
13
+ import { useParams } from 'next/navigation';
14
+ import { KanbanBoard } from '../../_components/kanban-board';
15
+ import { OperationsHeader } from '../../_components/operations-header';
16
+ import { SectionCard } from '../../_components/section-card';
17
+ import { StatusBadge } from '../../_components/status-badge';
18
+ import { useOperationsData } from '../../_lib/hooks/use-operations-data';
19
+ import {
20
+ formatCurrency,
21
+ formatDate,
22
+ formatHours,
23
+ } from '../../_lib/utils/format';
24
+ import { getProjectTeam } from '../../_lib/utils/metrics';
25
+ import {
26
+ getApprovalBadgeClasses,
27
+ getApprovalLabel,
28
+ getProjectBadgeClasses,
29
+ getProjectStatusLabel,
30
+ } from '../../_lib/utils/status';
31
+
32
+ export default function ProjectDetailsPage() {
33
+ const params = useParams<{ id: string }>();
34
+ const { projects, contracts, users, tasks, timesheets } = useOperationsData();
35
+ const project = projects.find((item) => item.id === params.id);
36
+
37
+ if (!project) {
38
+ return <Page>Project not found.</Page>;
39
+ }
40
+
41
+ const contract = contracts.find((item) => item.id === project.contractId);
42
+ const team = getProjectTeam(project.id);
43
+ const projectTasks = tasks.filter((task) => task.projectId === project.id);
44
+ const projectTimesheets = timesheets.filter(
45
+ (entry) => entry.projectId === project.id
46
+ );
47
+
48
+ return (
49
+ <Page>
50
+ <OperationsHeader
51
+ title={project.name}
52
+ description={project.description}
53
+ current="Project Details"
54
+ />
55
+
56
+ <Tabs defaultValue="overview" className="space-y-4">
57
+ <TabsList>
58
+ <TabsTrigger value="overview">Overview</TabsTrigger>
59
+ <TabsTrigger value="team">Team</TabsTrigger>
60
+ <TabsTrigger value="tasks">Tasks</TabsTrigger>
61
+ <TabsTrigger value="timesheets">Timesheets</TabsTrigger>
62
+ <TabsTrigger value="costs">Costs</TabsTrigger>
63
+ </TabsList>
64
+
65
+ <TabsContent value="overview">
66
+ <SectionCard title="Project Overview">
67
+ <div className="grid gap-4 text-sm md:grid-cols-2">
68
+ <p>
69
+ <span className="font-medium">Status:</span>{' '}
70
+ <StatusBadge
71
+ label={getProjectStatusLabel(project.status)}
72
+ className={getProjectBadgeClasses(project.status)}
73
+ />
74
+ </p>
75
+ <p>
76
+ <span className="font-medium">Progress:</span> {project.progress}%
77
+ </p>
78
+ <p>
79
+ <span className="font-medium">Client:</span> {project.client}
80
+ </p>
81
+ <p>
82
+ <span className="font-medium">Contract:</span> {contract?.name}
83
+ </p>
84
+ <p className="md:col-span-2">
85
+ <span className="font-medium">Description:</span>{' '}
86
+ {project.description}
87
+ </p>
88
+ </div>
89
+ </SectionCard>
90
+ </TabsContent>
91
+
92
+ <TabsContent value="team">
93
+ <SectionCard title="Team Allocation">
94
+ <Table>
95
+ <TableHeader>
96
+ <TableRow>
97
+ <TableHead>Member</TableHead>
98
+ <TableHead>Role</TableHead>
99
+ <TableHead>Hour Rate</TableHead>
100
+ <TableHead>Allocation %</TableHead>
101
+ </TableRow>
102
+ </TableHeader>
103
+ <TableBody>
104
+ {team.map((member) => (
105
+ <TableRow key={member.id}>
106
+ <TableCell>{member.name}</TableCell>
107
+ <TableCell>{member.role}</TableCell>
108
+ <TableCell>{formatCurrency(member.hourlyRate)}</TableCell>
109
+ <TableCell>{member.utilizationTarget}%</TableCell>
110
+ </TableRow>
111
+ ))}
112
+ </TableBody>
113
+ </Table>
114
+ </SectionCard>
115
+ </TabsContent>
116
+
117
+ <TabsContent value="tasks">
118
+ <SectionCard title="Kanban Preview">
119
+ <KanbanBoard tasks={projectTasks} users={users} />
120
+ </SectionCard>
121
+ </TabsContent>
122
+
123
+ <TabsContent value="timesheets">
124
+ <SectionCard title="Project Timesheets">
125
+ <Table>
126
+ <TableHeader>
127
+ <TableRow>
128
+ <TableHead>Date</TableHead>
129
+ <TableHead>User</TableHead>
130
+ <TableHead>Hours</TableHead>
131
+ <TableHead>Description</TableHead>
132
+ <TableHead>Status</TableHead>
133
+ </TableRow>
134
+ </TableHeader>
135
+ <TableBody>
136
+ {projectTimesheets.map((entry) => {
137
+ const user = users.find((item) => item.id === entry.userId);
138
+
139
+ return (
140
+ <TableRow key={entry.id}>
141
+ <TableCell>{formatDate(entry.date)}</TableCell>
142
+ <TableCell>{user?.name}</TableCell>
143
+ <TableCell>{formatHours(entry.hours)}</TableCell>
144
+ <TableCell>{entry.description}</TableCell>
145
+ <TableCell>
146
+ <StatusBadge
147
+ label={getApprovalLabel(entry.status)}
148
+ className={getApprovalBadgeClasses(entry.status)}
149
+ />
150
+ </TableCell>
151
+ </TableRow>
152
+ );
153
+ })}
154
+ </TableBody>
155
+ </Table>
156
+ </SectionCard>
157
+ </TabsContent>
158
+
159
+ <TabsContent value="costs">
160
+ <SectionCard title="Costs (Mock)">
161
+ <div className="grid gap-3 md:grid-cols-3">
162
+ <div className="rounded-lg border p-4">
163
+ <p className="text-sm text-muted-foreground">Budget</p>
164
+ <p className="text-xl font-semibold">
165
+ {formatCurrency(project.budget)}
166
+ </p>
167
+ </div>
168
+ <div className="rounded-lg border p-4">
169
+ <p className="text-sm text-muted-foreground">Logged Hours</p>
170
+ <p className="text-xl font-semibold">
171
+ {formatHours(project.hoursLogged)}
172
+ </p>
173
+ </div>
174
+ <div className="rounded-lg border p-4">
175
+ <p className="text-sm text-muted-foreground">Projected End</p>
176
+ <p className="text-xl font-semibold">
177
+ {formatDate(project.endDate)}
178
+ </p>
179
+ </div>
180
+ </div>
181
+ </SectionCard>
182
+ </TabsContent>
183
+ </Tabs>
184
+ </Page>
185
+ );
186
+ }
@@ -1,111 +1,111 @@
1
- 'use client';
2
-
3
- import { Page } from '@/components/entity-list';
4
- import { Input } from '@/components/ui/input';
5
- import { Progress } from '@/components/ui/progress';
6
- import { useTranslations } from 'next-intl';
7
- import Link from 'next/link';
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 { useOperationsData } from '../_lib/hooks/use-operations-data';
13
- import { formatDate, formatHours } from '../_lib/utils/format';
14
- import {
15
- getProjectBadgeClasses,
16
- getProjectStatusLabel,
17
- } from '../_lib/utils/status';
18
-
19
- export default function ProjectsPage() {
20
- const t = useTranslations('operations.ProjectsPage');
21
- const { projects, users } = useOperationsData();
22
- const [search, setSearch] = useState('');
23
-
24
- const filteredProjects = useMemo(
25
- () =>
26
- projects.filter((project) =>
27
- `${project.name} ${project.client}`
28
- .toLowerCase()
29
- .includes(search.toLowerCase())
30
- ),
31
- [projects, search]
32
- );
33
-
34
- return (
35
- <Page>
36
- <OperationsHeader
37
- title={t('title')}
38
- description={t('description')}
39
- current={t('breadcrumb')}
40
- />
41
-
42
- <SectionCard title={t('gridTitle')} description={t('gridDescription')}>
43
- <div className="mb-4">
44
- <Input
45
- value={search}
46
- onChange={(event) => setSearch(event.target.value)}
47
- placeholder={t('searchPlaceholder')}
48
- />
49
- </div>
50
- <div className="grid gap-4 xl:grid-cols-2">
51
- {filteredProjects.map((project) => {
52
- const memberNames = users
53
- .filter((user) => project.teamMemberIds.includes(user.id))
54
- .map((user) => user.name)
55
- .join(', ');
56
-
57
- return (
58
- <Link
59
- key={project.id}
60
- href={`/operations/projects/${project.id}`}
61
- className="rounded-xl border p-5 transition hover:border-primary/40 hover:shadow-md"
62
- >
63
- <div className="space-y-4">
64
- <div className="flex items-start justify-between gap-4">
65
- <div>
66
- <p className="text-lg font-semibold">{project.name}</p>
67
- <p className="text-sm text-muted-foreground">
68
- {project.client}
69
- </p>
70
- </div>
71
- <StatusBadge
72
- label={getProjectStatusLabel(project.status)}
73
- className={getProjectBadgeClasses(project.status)}
74
- />
75
- </div>
76
-
77
- <div>
78
- <div className="mb-2 flex items-center justify-between text-sm">
79
- <span>{t('progress')}</span>
80
- <span>{project.progress}%</span>
81
- </div>
82
- <Progress value={project.progress} />
83
- </div>
84
-
85
- <div className="grid gap-3 text-sm sm:grid-cols-2">
86
- <p>
87
- <span className="font-medium">{t('teamMembers')}:</span>{' '}
88
- {memberNames}
89
- </p>
90
- <p>
91
- <span className="font-medium">{t('hoursLogged')}:</span>{' '}
92
- {formatHours(project.hoursLogged)}
93
- </p>
94
- <p>
95
- <span className="font-medium">{t('startDate')}:</span>{' '}
96
- {formatDate(project.startDate)}
97
- </p>
98
- <p>
99
- <span className="font-medium">{t('endDate')}:</span>{' '}
100
- {formatDate(project.endDate)}
101
- </p>
102
- </div>
103
- </div>
104
- </Link>
105
- );
106
- })}
107
- </div>
108
- </SectionCard>
109
- </Page>
110
- );
111
- }
1
+ 'use client';
2
+
3
+ import { Page } from '@/components/entity-list';
4
+ import { Input } from '@/components/ui/input';
5
+ import { Progress } from '@/components/ui/progress';
6
+ import { useTranslations } from 'next-intl';
7
+ import Link from 'next/link';
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 { useOperationsData } from '../_lib/hooks/use-operations-data';
13
+ import { formatDate, formatHours } from '../_lib/utils/format';
14
+ import {
15
+ getProjectBadgeClasses,
16
+ getProjectStatusLabel,
17
+ } from '../_lib/utils/status';
18
+
19
+ export default function ProjectsPage() {
20
+ const t = useTranslations('operations.ProjectsPage');
21
+ const { projects, users } = useOperationsData();
22
+ const [search, setSearch] = useState('');
23
+
24
+ const filteredProjects = useMemo(
25
+ () =>
26
+ projects.filter((project) =>
27
+ `${project.name} ${project.client}`
28
+ .toLowerCase()
29
+ .includes(search.toLowerCase())
30
+ ),
31
+ [projects, search]
32
+ );
33
+
34
+ return (
35
+ <Page>
36
+ <OperationsHeader
37
+ title={t('title')}
38
+ description={t('description')}
39
+ current={t('breadcrumb')}
40
+ />
41
+
42
+ <SectionCard title={t('gridTitle')} description={t('gridDescription')}>
43
+ <div className="mb-4">
44
+ <Input
45
+ value={search}
46
+ onChange={(event) => setSearch(event.target.value)}
47
+ placeholder={t('searchPlaceholder')}
48
+ />
49
+ </div>
50
+ <div className="grid gap-4 xl:grid-cols-2">
51
+ {filteredProjects.map((project) => {
52
+ const memberNames = users
53
+ .filter((user) => project.teamMemberIds.includes(user.id))
54
+ .map((user) => user.name)
55
+ .join(', ');
56
+
57
+ return (
58
+ <Link
59
+ key={project.id}
60
+ href={`/operations/projects/${project.id}`}
61
+ className="rounded-xl border p-5 transition hover:border-primary/40 hover:shadow-md"
62
+ >
63
+ <div className="space-y-4">
64
+ <div className="flex items-start justify-between gap-4">
65
+ <div>
66
+ <p className="text-lg font-semibold">{project.name}</p>
67
+ <p className="text-sm text-muted-foreground">
68
+ {project.client}
69
+ </p>
70
+ </div>
71
+ <StatusBadge
72
+ label={getProjectStatusLabel(project.status)}
73
+ className={getProjectBadgeClasses(project.status)}
74
+ />
75
+ </div>
76
+
77
+ <div>
78
+ <div className="mb-2 flex items-center justify-between text-sm">
79
+ <span>{t('progress')}</span>
80
+ <span>{project.progress}%</span>
81
+ </div>
82
+ <Progress value={project.progress} />
83
+ </div>
84
+
85
+ <div className="grid gap-3 text-sm sm:grid-cols-2">
86
+ <p>
87
+ <span className="font-medium">{t('teamMembers')}:</span>{' '}
88
+ {memberNames}
89
+ </p>
90
+ <p>
91
+ <span className="font-medium">{t('hoursLogged')}:</span>{' '}
92
+ {formatHours(project.hoursLogged)}
93
+ </p>
94
+ <p>
95
+ <span className="font-medium">{t('startDate')}:</span>{' '}
96
+ {formatDate(project.startDate)}
97
+ </p>
98
+ <p>
99
+ <span className="font-medium">{t('endDate')}:</span>{' '}
100
+ {formatDate(project.endDate)}
101
+ </p>
102
+ </div>
103
+ </div>
104
+ </Link>
105
+ );
106
+ })}
107
+ </div>
108
+ </SectionCard>
109
+ </Page>
110
+ );
111
+ }