@hed-hog/operations 0.0.317 → 0.0.319

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 (137) hide show
  1. package/dist/controllers/operations-collaborator-costs.controller.d.ts +144 -0
  2. package/dist/controllers/operations-collaborator-costs.controller.d.ts.map +1 -0
  3. package/dist/controllers/operations-collaborator-costs.controller.js +162 -0
  4. package/dist/controllers/operations-collaborator-costs.controller.js.map +1 -0
  5. package/dist/controllers/operations-collaborators.controller.d.ts +14 -0
  6. package/dist/controllers/operations-collaborators.controller.d.ts.map +1 -1
  7. package/dist/controllers/operations-collaborators.controller.js +11 -0
  8. package/dist/controllers/operations-collaborators.controller.js.map +1 -1
  9. package/dist/controllers/operations-projects.controller.d.ts +31 -0
  10. package/dist/controllers/operations-projects.controller.d.ts.map +1 -1
  11. package/dist/controllers/operations-projects.controller.js +23 -0
  12. package/dist/controllers/operations-projects.controller.js.map +1 -1
  13. package/dist/controllers/operations-reports.controller.d.ts +199 -0
  14. package/dist/controllers/operations-reports.controller.d.ts.map +1 -0
  15. package/dist/controllers/operations-reports.controller.js +53 -0
  16. package/dist/controllers/operations-reports.controller.js.map +1 -0
  17. package/dist/controllers/operations-tasks.controller.d.ts +41 -2
  18. package/dist/controllers/operations-tasks.controller.d.ts.map +1 -1
  19. package/dist/controllers/operations-tasks.controller.js +17 -5
  20. package/dist/controllers/operations-tasks.controller.js.map +1 -1
  21. package/dist/dto/create-collaborator-cost.dto.d.ts +16 -0
  22. package/dist/dto/create-collaborator-cost.dto.d.ts.map +1 -0
  23. package/dist/dto/create-collaborator-cost.dto.js +88 -0
  24. package/dist/dto/create-collaborator-cost.dto.js.map +1 -0
  25. package/dist/dto/create-collaborator.dto.d.ts +0 -1
  26. package/dist/dto/create-collaborator.dto.d.ts.map +1 -1
  27. package/dist/dto/create-collaborator.dto.js +0 -6
  28. package/dist/dto/create-collaborator.dto.js.map +1 -1
  29. package/dist/dto/create-cost-type.dto.d.ts +13 -0
  30. package/dist/dto/create-cost-type.dto.d.ts.map +1 -0
  31. package/dist/dto/create-cost-type.dto.js +87 -0
  32. package/dist/dto/create-cost-type.dto.js.map +1 -0
  33. package/dist/dto/list-approvals.dto.d.ts +2 -0
  34. package/dist/dto/list-approvals.dto.d.ts.map +1 -1
  35. package/dist/dto/list-approvals.dto.js +10 -0
  36. package/dist/dto/list-approvals.dto.js.map +1 -1
  37. package/dist/dto/list-collaborator-costs.dto.d.ts +5 -0
  38. package/dist/dto/list-collaborator-costs.dto.d.ts.map +1 -0
  39. package/dist/dto/list-collaborator-costs.dto.js +23 -0
  40. package/dist/dto/list-collaborator-costs.dto.js.map +1 -0
  41. package/dist/dto/list-cost-types.dto.d.ts +6 -0
  42. package/dist/dto/list-cost-types.dto.d.ts.map +1 -0
  43. package/dist/dto/list-cost-types.dto.js +35 -0
  44. package/dist/dto/list-cost-types.dto.js.map +1 -0
  45. package/dist/dto/list-my-projects.dto.d.ts +5 -0
  46. package/dist/dto/list-my-projects.dto.d.ts.map +1 -0
  47. package/dist/dto/list-my-projects.dto.js +23 -0
  48. package/dist/dto/list-my-projects.dto.js.map +1 -0
  49. package/dist/dto/list-my-tasks.dto.d.ts +6 -0
  50. package/dist/dto/list-my-tasks.dto.d.ts.map +1 -0
  51. package/dist/dto/list-my-tasks.dto.js +33 -0
  52. package/dist/dto/list-my-tasks.dto.js.map +1 -0
  53. package/dist/dto/list-projects.dto.d.ts +1 -0
  54. package/dist/dto/list-projects.dto.d.ts.map +1 -1
  55. package/dist/dto/list-projects.dto.js +7 -0
  56. package/dist/dto/list-projects.dto.js.map +1 -1
  57. package/dist/dto/list-reports.dto.d.ts +16 -0
  58. package/dist/dto/list-reports.dto.d.ts.map +1 -0
  59. package/dist/dto/list-reports.dto.js +75 -0
  60. package/dist/dto/list-reports.dto.js.map +1 -0
  61. package/dist/dto/list-tasks.dto.d.ts +2 -0
  62. package/dist/dto/list-tasks.dto.d.ts.map +1 -1
  63. package/dist/dto/list-tasks.dto.js +12 -0
  64. package/dist/dto/list-tasks.dto.js.map +1 -1
  65. package/dist/dto/list-timesheets.dto.d.ts +2 -0
  66. package/dist/dto/list-timesheets.dto.d.ts.map +1 -1
  67. package/dist/dto/list-timesheets.dto.js +10 -0
  68. package/dist/dto/list-timesheets.dto.js.map +1 -1
  69. package/dist/dto/update-collaborator-cost.dto.d.ts +6 -0
  70. package/dist/dto/update-collaborator-cost.dto.d.ts.map +1 -0
  71. package/dist/dto/update-collaborator-cost.dto.js +9 -0
  72. package/dist/dto/update-collaborator-cost.dto.js.map +1 -0
  73. package/dist/dto/update-task.dto.d.ts +1 -0
  74. package/dist/dto/update-task.dto.d.ts.map +1 -1
  75. package/dist/dto/update-task.dto.js +6 -0
  76. package/dist/dto/update-task.dto.js.map +1 -1
  77. package/dist/operations.module.d.ts.map +1 -1
  78. package/dist/operations.module.js +4 -0
  79. package/dist/operations.module.js.map +1 -1
  80. package/dist/operations.service.d.ts +457 -3
  81. package/dist/operations.service.d.ts.map +1 -1
  82. package/dist/operations.service.js +1445 -208
  83. package/dist/operations.service.js.map +1 -1
  84. package/dist/operations.service.spec.js +31 -7
  85. package/dist/operations.service.spec.js.map +1 -1
  86. package/hedhog/data/menu.yaml +112 -7
  87. package/hedhog/data/operations_cost_type.yaml +166 -0
  88. package/hedhog/data/route.yaml +185 -0
  89. package/hedhog/frontend/app/_components/collaborator-costs-section.tsx.ejs +884 -0
  90. package/hedhog/frontend/app/_components/collaborator-details-screen.tsx.ejs +94 -15
  91. package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +219 -94
  92. package/hedhog/frontend/app/_components/contract-details-screen.tsx.ejs +21 -32
  93. package/hedhog/frontend/app/_components/contract-form-screen.tsx.ejs +178 -89
  94. package/hedhog/frontend/app/_components/my-project-summary-screen.tsx.ejs +1185 -0
  95. package/hedhog/frontend/app/_components/operations-calendar-view.tsx.ejs +306 -0
  96. package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +943 -782
  97. package/hedhog/frontend/app/_components/task-detail-sheet.tsx.ejs +223 -0
  98. package/hedhog/frontend/app/_lib/api.ts.ejs +162 -0
  99. package/hedhog/frontend/app/_lib/types.ts.ejs +229 -3
  100. package/hedhog/frontend/app/_lib/utils/format.ts.ejs +11 -3
  101. package/hedhog/frontend/app/approvals/page.tsx.ejs +191 -46
  102. package/hedhog/frontend/app/collaborators/page.tsx.ejs +133 -25
  103. package/hedhog/frontend/app/my-projects/[id]/page.tsx.ejs +11 -0
  104. package/hedhog/frontend/app/my-projects/page.tsx.ejs +440 -0
  105. package/hedhog/frontend/app/my-tasks/page.tsx.ejs +1304 -0
  106. package/hedhog/frontend/app/reports/collaborators/page.tsx.ejs +771 -0
  107. package/hedhog/frontend/app/reports/projects/page.tsx.ejs +809 -0
  108. package/hedhog/frontend/app/timesheets/page.tsx.ejs +322 -58
  109. package/hedhog/frontend/messages/en.json +234 -25
  110. package/hedhog/frontend/messages/pt.json +234 -25
  111. package/hedhog/table/operations_collaborator.yaml +0 -4
  112. package/hedhog/table/operations_collaborator_compensation_history.yaml +28 -0
  113. package/hedhog/table/operations_collaborator_cost.yaml +56 -0
  114. package/hedhog/table/operations_cost_type.yaml +38 -0
  115. package/package.json +6 -6
  116. package/src/controllers/operations-collaborator-costs.controller.ts +147 -0
  117. package/src/controllers/operations-collaborators.controller.ts +19 -8
  118. package/src/controllers/operations-projects.controller.ts +19 -8
  119. package/src/controllers/operations-reports.controller.ts +32 -0
  120. package/src/controllers/operations-tasks.controller.ts +32 -12
  121. package/src/dto/create-collaborator-cost.dto.ts +78 -0
  122. package/src/dto/create-collaborator.dto.ts +9 -14
  123. package/src/dto/create-cost-type.dto.ts +62 -0
  124. package/src/dto/list-approvals.dto.ts +8 -0
  125. package/src/dto/list-collaborator-costs.dto.ts +8 -0
  126. package/src/dto/list-cost-types.dto.ts +19 -0
  127. package/src/dto/list-my-projects.dto.ts +8 -0
  128. package/src/dto/list-my-tasks.dto.ts +17 -0
  129. package/src/dto/list-projects.dto.ts +7 -1
  130. package/src/dto/list-reports.dto.ts +51 -0
  131. package/src/dto/list-tasks.dto.ts +11 -1
  132. package/src/dto/list-timesheets.dto.ts +8 -0
  133. package/src/dto/update-collaborator-cost.dto.ts +4 -0
  134. package/src/dto/update-task.dto.ts +6 -0
  135. package/src/operations.module.ts +7 -3
  136. package/src/operations.service.spec.ts +45 -7
  137. package/src/operations.service.ts +1992 -225
@@ -0,0 +1,440 @@
1
+ 'use client';
2
+
3
+ import {
4
+ EmptyState,
5
+ Page,
6
+ PaginationFooter,
7
+ SearchBar,
8
+ } from '@/components/entity-list';
9
+ import { Button } from '@/components/ui/button';
10
+ import { Card, CardContent } from '@/components/ui/card';
11
+ import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
12
+ import {
13
+ Table,
14
+ TableBody,
15
+ TableCell,
16
+ TableHead,
17
+ TableHeader,
18
+ TableRow,
19
+ } from '@/components/ui/table';
20
+ import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
21
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
22
+ import {
23
+ CalendarDays,
24
+ Eye,
25
+ FolderKanban,
26
+ LayoutGrid,
27
+ List,
28
+ PlayCircle,
29
+ ShieldAlert,
30
+ } from 'lucide-react';
31
+ import { useTranslations } from 'next-intl';
32
+ import Link from 'next/link';
33
+ import { useMemo, useState } from 'react';
34
+ import { OperationsHeader } from '../_components/operations-header';
35
+ import { StatusBadge } from '../_components/status-badge';
36
+ import { fetchOperations } from '../_lib/api';
37
+ import type { OperationsProject, PaginatedResponse } from '../_lib/types';
38
+ import { formatDate, getStatusBadgeClass } from '../_lib/utils/format';
39
+
40
+ const MY_PROJECTS_VIEW_STORAGE_KEY = 'operations-my-projects-view-mode';
41
+
42
+ type ProjectViewMode = 'table' | 'cards';
43
+
44
+ export default function OperationsMyProjectsPage() {
45
+ const t = useTranslations('operations.MyProjectsPage');
46
+ const commonT = useTranslations('operations.Common');
47
+ const projectFormT = useTranslations('operations.ProjectFormPage');
48
+
49
+ const tProjectStatus = (status: string | null | undefined) =>
50
+ status
51
+ ? (projectFormT as unknown as (k: string) => string)(
52
+ `options.statuses.${status}`
53
+ )
54
+ : '-';
55
+ const { request, currentLocaleCode, getSettingValue } = useApp();
56
+ const [search, setSearch] = useState('');
57
+ const [statusFilter, setStatusFilter] = useState('all');
58
+ const [page, setPage] = useState(1);
59
+ const [pageSize, setPageSize] = useState(12);
60
+ const [viewMode, setViewMode] = useState<ProjectViewMode>(() => {
61
+ if (typeof window === 'undefined') {
62
+ return 'table';
63
+ }
64
+
65
+ const savedViewMode = window.localStorage.getItem(
66
+ MY_PROJECTS_VIEW_STORAGE_KEY
67
+ );
68
+
69
+ return savedViewMode === 'cards' ? 'cards' : 'table';
70
+ });
71
+
72
+ const { data: projectsResponse, refetch } = useQuery<
73
+ PaginatedResponse<OperationsProject>
74
+ >({
75
+ queryKey: [
76
+ 'operations-my-projects-list',
77
+ currentLocaleCode,
78
+ search,
79
+ statusFilter,
80
+ page,
81
+ pageSize,
82
+ ],
83
+ queryFn: () => {
84
+ const params = new URLSearchParams({
85
+ page: String(page),
86
+ pageSize: String(pageSize),
87
+ });
88
+
89
+ if (search.trim()) {
90
+ params.set('search', search.trim());
91
+ }
92
+
93
+ if (statusFilter !== 'all') {
94
+ params.set('status', statusFilter);
95
+ }
96
+
97
+ return fetchOperations<PaginatedResponse<OperationsProject>>(
98
+ request,
99
+ `/operations/my-projects?${params.toString()}`
100
+ );
101
+ },
102
+ placeholderData: (previous) => previous,
103
+ });
104
+ const projects = projectsResponse?.data ?? [];
105
+
106
+ const statsCards = useMemo(
107
+ () => [
108
+ {
109
+ key: 'total',
110
+ title: t('cards.total'),
111
+ description: t('cards.totalDescription'),
112
+ value: projects.length,
113
+ icon: FolderKanban,
114
+ accentClassName: 'from-slate-500/20 via-slate-400/10 to-transparent',
115
+ iconContainerClassName: 'bg-slate-100 text-slate-700',
116
+ },
117
+ {
118
+ key: 'active',
119
+ title: t('cards.active'),
120
+ description: t('cards.activeDescription'),
121
+ value: projects.filter((item) => item.status === 'active').length,
122
+ icon: PlayCircle,
123
+ accentClassName: 'from-green-500/20 via-emerald-500/10 to-transparent',
124
+ iconContainerClassName: 'bg-green-50 text-green-600',
125
+ },
126
+ {
127
+ key: 'atRisk',
128
+ title: t('cards.atRisk'),
129
+ description: t('cards.atRiskDescription'),
130
+ value: projects.filter((item) => item.status === 'at_risk').length,
131
+ icon: ShieldAlert,
132
+ accentClassName: 'from-amber-500/20 via-orange-500/10 to-transparent',
133
+ iconContainerClassName: 'bg-amber-50 text-amber-600',
134
+ },
135
+ {
136
+ key: 'upcomingDeliveries',
137
+ title: t('cards.upcomingDeliveries'),
138
+ description: t('cards.upcomingDeliveriesDescription'),
139
+ value: projects.filter((item) => Boolean(item.endDate)).length,
140
+ icon: CalendarDays,
141
+ accentClassName: 'from-blue-500/20 via-cyan-500/10 to-transparent',
142
+ iconContainerClassName: 'bg-blue-50 text-blue-600',
143
+ },
144
+ ],
145
+ [projects, t]
146
+ );
147
+
148
+ const handleViewModeChange = (value: string) => {
149
+ if (value !== 'table' && value !== 'cards') {
150
+ return;
151
+ }
152
+
153
+ setViewMode(value);
154
+
155
+ if (typeof window !== 'undefined') {
156
+ window.localStorage.setItem(MY_PROJECTS_VIEW_STORAGE_KEY, value);
157
+ }
158
+ };
159
+
160
+ return (
161
+ <Page>
162
+ <OperationsHeader
163
+ title={t('title')}
164
+ description={t('description')}
165
+ current={t('breadcrumb')}
166
+ />
167
+
168
+ <KpiCardsGrid items={statsCards} columns={4} />
169
+
170
+ <div className="flex min-w-0 flex-col gap-4 xl:flex-row xl:items-center">
171
+ <div className="flex-1">
172
+ <SearchBar
173
+ className="w-full"
174
+ searchQuery={search}
175
+ onSearchChange={(value) => {
176
+ setSearch(value);
177
+ setPage(1);
178
+ }}
179
+ showSearchButton={false}
180
+ debounceMs={500}
181
+ placeholder={t('searchPlaceholder')}
182
+ controls={[
183
+ {
184
+ id: 'status',
185
+ type: 'select',
186
+ value: statusFilter,
187
+ onChange: (value) => {
188
+ setStatusFilter(value);
189
+ setPage(1);
190
+ },
191
+ placeholder: commonT('labels.status'),
192
+ options: [
193
+ { value: 'all', label: commonT('filters.allStatuses') },
194
+ { value: 'planning', label: tProjectStatus('planning') },
195
+ { value: 'active', label: tProjectStatus('active') },
196
+ { value: 'at_risk', label: tProjectStatus('at_risk') },
197
+ { value: 'paused', label: tProjectStatus('paused') },
198
+ { value: 'completed', label: tProjectStatus('completed') },
199
+ { value: 'archived', label: tProjectStatus('archived') },
200
+ ],
201
+ },
202
+ ]}
203
+ />
204
+ </div>
205
+
206
+ <div className="flex items-center justify-between gap-3 xl:justify-end">
207
+ <span className="text-xs font-medium text-muted-foreground">
208
+ {t('viewMode')}
209
+ </span>
210
+ <ToggleGroup
211
+ type="single"
212
+ value={viewMode}
213
+ onValueChange={handleViewModeChange}
214
+ variant="outline"
215
+ size="sm"
216
+ aria-label={t('viewMode')}
217
+ >
218
+ <ToggleGroupItem
219
+ value="table"
220
+ className="gap-1.5 px-2.5"
221
+ aria-label={t('viewModeTable')}
222
+ >
223
+ <List className="h-4 w-4" />
224
+ <span className="hidden sm:inline">{t('viewModeTable')}</span>
225
+ </ToggleGroupItem>
226
+ <ToggleGroupItem
227
+ value="cards"
228
+ className="gap-1.5 px-2.5"
229
+ aria-label={t('viewModeCards')}
230
+ >
231
+ <LayoutGrid className="h-4 w-4" />
232
+ <span className="hidden sm:inline">{t('viewModeCards')}</span>
233
+ </ToggleGroupItem>
234
+ </ToggleGroup>
235
+ </div>
236
+ </div>
237
+
238
+ {projects.length > 0 ? (
239
+ viewMode === 'cards' ? (
240
+ <div className="grid gap-4 md:grid-cols-2 2xl:grid-cols-3">
241
+ {projects.map((project) => (
242
+ <Card
243
+ key={project.id}
244
+ className="cursor-pointer overflow-hidden border-border/60 py-0 shadow-sm transition-all hover:-translate-y-0.5 hover:shadow-md"
245
+ >
246
+ <CardContent className="space-y-4 p-4">
247
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
248
+ <div className="min-w-0">
249
+ <div className="truncate font-semibold">
250
+ {project.name}
251
+ </div>
252
+ <div className="truncate text-xs text-muted-foreground">
253
+ {[
254
+ project.code,
255
+ project.myRoleLabel,
256
+ project.contractName,
257
+ ]
258
+ .filter(Boolean)
259
+ .join(' • ') || commonT('labels.notAvailable')}
260
+ </div>
261
+ </div>
262
+ <StatusBadge
263
+ label={tProjectStatus(project.status)}
264
+ className={getStatusBadgeClass(project.status)}
265
+ />
266
+ </div>
267
+
268
+ <div className="grid gap-2 text-sm text-muted-foreground lg:grid-cols-2">
269
+ <div>
270
+ <span className="font-medium text-foreground">
271
+ {commonT('labels.client')}:
272
+ </span>{' '}
273
+ {project.clientName || commonT('labels.notAvailable')}
274
+ </div>
275
+ <div>
276
+ <span className="font-medium text-foreground">
277
+ {commonT('labels.manager')}:
278
+ </span>{' '}
279
+ {project.managerName || commonT('labels.notAssigned')}
280
+ </div>
281
+ <div>
282
+ <span className="font-medium text-foreground">
283
+ {commonT('labels.teamSize')}:
284
+ </span>{' '}
285
+ {project.teamSize ?? 0}
286
+ </div>
287
+ <div>
288
+ <span className="font-medium text-foreground">
289
+ {commonT('labels.startDate')}:
290
+ </span>{' '}
291
+ {formatDate(
292
+ project.startDate,
293
+ getSettingValue,
294
+ currentLocaleCode
295
+ )}
296
+ </div>
297
+ <div>
298
+ <span className="font-medium text-foreground">
299
+ {commonT('labels.endDate')}:
300
+ </span>{' '}
301
+ {formatDate(
302
+ project.endDate,
303
+ getSettingValue,
304
+ currentLocaleCode
305
+ )}
306
+ </div>
307
+ </div>
308
+
309
+ <div className="flex justify-end border-t border-border/60 pt-3">
310
+ <Button variant="outline" size="icon" asChild>
311
+ <Link href={`/operations/my-projects/${project.id}`}>
312
+ <Eye className="size-4" />
313
+ </Link>
314
+ </Button>
315
+ </div>
316
+ </CardContent>
317
+ </Card>
318
+ ))}
319
+ </div>
320
+ ) : (
321
+ <div className="overflow-x-auto rounded-md border">
322
+ <Table className="table-fixed">
323
+ <TableHeader>
324
+ <TableRow>
325
+ <TableHead className="w-[30%]">
326
+ {commonT('labels.project')}
327
+ </TableHead>
328
+ <TableHead>{commonT('labels.client')}</TableHead>
329
+ <TableHead>{commonT('labels.status')}</TableHead>
330
+ <TableHead className="hidden lg:table-cell">
331
+ {commonT('labels.manager')}
332
+ </TableHead>
333
+ <TableHead className="hidden md:table-cell">
334
+ {commonT('labels.teamSize')}
335
+ </TableHead>
336
+ <TableHead className="hidden xl:table-cell">
337
+ {commonT('labels.startDate')}
338
+ </TableHead>
339
+ <TableHead className="hidden xl:table-cell">
340
+ {commonT('labels.endDate')}
341
+ </TableHead>
342
+ <TableHead className="w-20 text-right">
343
+ {commonT('labels.actions')}
344
+ </TableHead>
345
+ </TableRow>
346
+ </TableHeader>
347
+ <TableBody>
348
+ {projects.map((project) => (
349
+ <TableRow
350
+ key={project.id}
351
+ className="cursor-pointer hover:bg-muted/30"
352
+ >
353
+ <TableCell>
354
+ <div className="min-w-0">
355
+ <div className="truncate font-medium">
356
+ {project.name}
357
+ </div>
358
+ <div className="truncate text-xs text-muted-foreground">
359
+ {[
360
+ project.code,
361
+ project.myRoleLabel,
362
+ project.contractName,
363
+ ]
364
+ .filter(Boolean)
365
+ .join(' • ') || commonT('labels.notAvailable')}
366
+ </div>
367
+ </div>
368
+ </TableCell>
369
+ <TableCell>
370
+ <div className="truncate">
371
+ {project.clientName || commonT('labels.notAvailable')}
372
+ </div>
373
+ </TableCell>
374
+ <TableCell>
375
+ <StatusBadge
376
+ label={tProjectStatus(project.status)}
377
+ className={getStatusBadgeClass(project.status)}
378
+ />
379
+ </TableCell>
380
+ <TableCell className="hidden lg:table-cell">
381
+ <div className="truncate">
382
+ {project.managerName || commonT('labels.notAssigned')}
383
+ </div>
384
+ </TableCell>
385
+ <TableCell className="hidden md:table-cell">
386
+ {project.teamSize ?? 0}
387
+ </TableCell>
388
+ <TableCell className="hidden xl:table-cell">
389
+ {formatDate(
390
+ project.startDate,
391
+ getSettingValue,
392
+ currentLocaleCode
393
+ )}
394
+ </TableCell>
395
+ <TableCell className="hidden xl:table-cell">
396
+ {formatDate(
397
+ project.endDate,
398
+ getSettingValue,
399
+ currentLocaleCode
400
+ )}
401
+ </TableCell>
402
+ <TableCell>
403
+ <div className="flex justify-end">
404
+ <Button variant="outline" size="icon" asChild>
405
+ <Link href={`/operations/my-projects/${project.id}`}>
406
+ <Eye className="size-4" />
407
+ </Link>
408
+ </Button>
409
+ </div>
410
+ </TableCell>
411
+ </TableRow>
412
+ ))}
413
+ </TableBody>
414
+ </Table>
415
+ </div>
416
+ )
417
+ ) : (
418
+ <EmptyState
419
+ icon={<FolderKanban className="size-12" />}
420
+ title={commonT('states.emptyTitle')}
421
+ description={t('emptyDescription')}
422
+ actionLabel={commonT('actions.refresh')}
423
+ onAction={() => void refetch()}
424
+ />
425
+ )}
426
+
427
+ <PaginationFooter
428
+ currentPage={projectsResponse?.page ?? page}
429
+ pageSize={projectsResponse?.pageSize ?? pageSize}
430
+ totalItems={projectsResponse?.total ?? 0}
431
+ onPageChange={setPage}
432
+ onPageSizeChange={(value) => {
433
+ setPageSize(value);
434
+ setPage(1);
435
+ }}
436
+ pageSizeOptions={[12, 24, 48]}
437
+ />
438
+ </Page>
439
+ );
440
+ }