@hed-hog/operations 0.0.304 → 0.0.305

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 (52) hide show
  1. package/dist/controllers/operations-projects.controller.d.ts +15 -0
  2. package/dist/controllers/operations-projects.controller.d.ts.map +1 -1
  3. package/dist/controllers/operations-tasks.controller.d.ts +41 -10
  4. package/dist/controllers/operations-tasks.controller.d.ts.map +1 -1
  5. package/dist/controllers/operations-tasks.controller.js +11 -0
  6. package/dist/controllers/operations-tasks.controller.js.map +1 -1
  7. package/dist/dto/create-task.dto.d.ts +7 -1
  8. package/dist/dto/create-task.dto.d.ts.map +1 -1
  9. package/dist/dto/create-task.dto.js +38 -5
  10. package/dist/dto/create-task.dto.js.map +1 -1
  11. package/dist/dto/list-tasks.dto.d.ts +1 -1
  12. package/dist/dto/list-tasks.dto.d.ts.map +1 -1
  13. package/dist/dto/list-tasks.dto.js +2 -2
  14. package/dist/dto/list-tasks.dto.js.map +1 -1
  15. package/dist/dto/update-task.dto.d.ts +7 -1
  16. package/dist/dto/update-task.dto.d.ts.map +1 -1
  17. package/dist/dto/update-task.dto.js +38 -5
  18. package/dist/dto/update-task.dto.js.map +1 -1
  19. package/dist/operations.service.d.ts +68 -12
  20. package/dist/operations.service.d.ts.map +1 -1
  21. package/dist/operations.service.js +380 -101
  22. package/dist/operations.service.js.map +1 -1
  23. package/hedhog/data/route.yaml +13 -0
  24. package/hedhog/frontend/app/_components/collaborator-details-screen.tsx.ejs +44 -44
  25. package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +168 -213
  26. package/hedhog/frontend/app/_components/collaborator-select-with-create.tsx.ejs +256 -256
  27. package/hedhog/frontend/app/_components/contract-form-screen.tsx.ejs +7 -7
  28. package/hedhog/frontend/app/_components/contract-template-form-screen.tsx.ejs +306 -306
  29. package/hedhog/frontend/app/_components/contract-template-select-with-create.tsx.ejs +247 -247
  30. package/hedhog/frontend/app/_components/contract-wizard-sheet.tsx.ejs +3520 -3520
  31. package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +1504 -52
  32. package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +528 -403
  33. package/hedhog/frontend/app/_components/section-card.tsx.ejs +25 -18
  34. package/hedhog/frontend/app/_components/system-user-select-with-create.tsx.ejs +609 -0
  35. package/hedhog/frontend/app/_lib/types.ts.ejs +5 -0
  36. package/hedhog/frontend/app/_lib/utils/format.ts.ejs +7 -7
  37. package/hedhog/frontend/app/_lib/utils/forms.ts.ejs +48 -1
  38. package/hedhog/frontend/app/collaborator-types/page.tsx.ejs +502 -502
  39. package/hedhog/frontend/app/collaborators/page.tsx.ejs +10 -7
  40. package/hedhog/frontend/app/contracts/page.tsx.ejs +938 -938
  41. package/hedhog/frontend/app/projects/[id]/edit/page.tsx.ejs +1 -1
  42. package/hedhog/frontend/app/projects/page.tsx.ejs +360 -133
  43. package/hedhog/frontend/messages/en.json +27 -4
  44. package/hedhog/frontend/messages/pt.json +27 -4
  45. package/hedhog/table/operations_project.yaml +9 -0
  46. package/hedhog/table/operations_task.yaml +43 -4
  47. package/package.json +5 -5
  48. package/src/controllers/operations-tasks.controller.ts +11 -0
  49. package/src/dto/create-task.dto.ts +47 -7
  50. package/src/dto/list-tasks.dto.ts +3 -3
  51. package/src/dto/update-task.dto.ts +47 -7
  52. package/src/operations.service.ts +556 -88
@@ -7,5 +7,5 @@ export default async function OperationsProjectEditPage({
7
7
  }) {
8
8
  const { id } = await params;
9
9
 
10
- redirect(`/operations/projects?edit=${id}`);
10
+ redirect(`/operations/projects/${id}?edit=1`);
11
11
  }
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { EmptyState, Page, SearchBar } from '@/components/entity-list';
4
4
  import { Button } from '@/components/ui/button';
5
+ import { Card, CardContent } from '@/components/ui/card';
5
6
  import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
6
7
  import {
7
8
  Sheet,
@@ -18,12 +19,15 @@ import {
18
19
  TableHeader,
19
20
  TableRow,
20
21
  } from '@/components/ui/table';
22
+ import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
21
23
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
22
24
  import {
23
25
  CalendarDays,
24
26
  Eye,
25
27
  FileText,
26
28
  FolderKanban,
29
+ LayoutGrid,
30
+ List,
27
31
  Pencil,
28
32
  PlayCircle,
29
33
  ShieldAlert,
@@ -44,6 +48,10 @@ import {
44
48
  getStatusBadgeClass,
45
49
  } from '../_lib/utils/format';
46
50
 
51
+ const PROJECT_VIEW_STORAGE_KEY = 'operations-projects-view-mode';
52
+
53
+ type ProjectViewMode = 'table' | 'cards';
54
+
47
55
  function parseEditProjectId(value: string | null) {
48
56
  const parsed = Number(value);
49
57
  return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
@@ -52,13 +60,23 @@ function parseEditProjectId(value: string | null) {
52
60
  export default function OperationsProjectsPage() {
53
61
  const t = useTranslations('operations.ProjectsPage');
54
62
  const commonT = useTranslations('operations.Common');
55
- const { request, showToastHandler, currentLocaleCode } = useApp();
63
+ const { request, showToastHandler, currentLocaleCode, getSettingValue } =
64
+ useApp();
56
65
  const access = useOperationsAccess();
57
66
  const router = useRouter();
58
67
  const pathname = usePathname();
59
68
  const searchParams = useSearchParams();
60
69
  const [search, setSearch] = useState('');
61
70
  const [statusFilter, setStatusFilter] = useState('all');
71
+ const [viewMode, setViewMode] = useState<ProjectViewMode>(() => {
72
+ if (typeof window === 'undefined') {
73
+ return 'table';
74
+ }
75
+
76
+ const savedViewMode = window.localStorage.getItem(PROJECT_VIEW_STORAGE_KEY);
77
+
78
+ return savedViewMode === 'cards' ? 'cards' : 'table';
79
+ });
62
80
 
63
81
  const createParam = searchParams.get('create');
64
82
  const editProjectId = parseEditProjectId(searchParams.get('edit'));
@@ -140,6 +158,8 @@ export default function OperationsProjectsPage() {
140
158
  description: t('cards.totalDescription'),
141
159
  value: projects.length,
142
160
  icon: FolderKanban,
161
+ accentClassName: 'from-slate-500/20 via-slate-400/10 to-transparent',
162
+ iconContainerClassName: 'bg-slate-100 text-slate-700',
143
163
  },
144
164
  {
145
165
  key: 'active',
@@ -147,6 +167,8 @@ export default function OperationsProjectsPage() {
147
167
  description: t('cards.activeDescription'),
148
168
  value: projects.filter((item) => item.status === 'active').length,
149
169
  icon: PlayCircle,
170
+ accentClassName: 'from-green-500/20 via-emerald-500/10 to-transparent',
171
+ iconContainerClassName: 'bg-green-50 text-green-600',
150
172
  },
151
173
  {
152
174
  key: 'atRisk',
@@ -154,6 +176,8 @@ export default function OperationsProjectsPage() {
154
176
  description: t('cards.atRiskDescription'),
155
177
  value: projects.filter((item) => item.status === 'at_risk').length,
156
178
  icon: ShieldAlert,
179
+ accentClassName: 'from-amber-500/20 via-orange-500/10 to-transparent',
180
+ iconContainerClassName: 'bg-amber-50 text-amber-600',
157
181
  },
158
182
  {
159
183
  key: 'upcomingDeliveries',
@@ -161,11 +185,25 @@ export default function OperationsProjectsPage() {
161
185
  description: t('cards.upcomingDeliveriesDescription'),
162
186
  value: projects.filter((item) => Boolean(item.endDate)).length,
163
187
  icon: CalendarDays,
188
+ accentClassName: 'from-blue-500/20 via-cyan-500/10 to-transparent',
189
+ iconContainerClassName: 'bg-blue-50 text-blue-600',
164
190
  },
165
191
  ],
166
192
  [projects, t]
167
193
  );
168
194
 
195
+ const handleViewModeChange = (value: string) => {
196
+ if (value !== 'table' && value !== 'cards') {
197
+ return;
198
+ }
199
+
200
+ setViewMode(value);
201
+
202
+ if (typeof window !== 'undefined') {
203
+ window.localStorage.setItem(PROJECT_VIEW_STORAGE_KEY, value);
204
+ }
205
+ };
206
+
169
207
  const toggleArchived = async (project: OperationsProject) => {
170
208
  const nextStatus = project.status === 'archived' ? 'active' : 'archived';
171
209
 
@@ -202,67 +240,81 @@ export default function OperationsProjectsPage() {
202
240
 
203
241
  <KpiCardsGrid items={statsCards} columns={4} />
204
242
 
205
- <SearchBar
206
- searchQuery={search}
207
- onSearchChange={setSearch}
208
- onSearch={() => undefined}
209
- placeholder={t('searchPlaceholder')}
210
- controls={[
211
- {
212
- id: 'status',
213
- type: 'select',
214
- value: statusFilter,
215
- onChange: setStatusFilter,
216
- placeholder: commonT('labels.status'),
217
- options: [
218
- { value: 'all', label: commonT('filters.allStatuses') },
219
- { value: 'planning', label: formatEnumLabel('planning') },
220
- { value: 'active', label: formatEnumLabel('active') },
221
- { value: 'at_risk', label: formatEnumLabel('at_risk') },
222
- { value: 'paused', label: formatEnumLabel('paused') },
223
- { value: 'completed', label: formatEnumLabel('completed') },
224
- { value: 'archived', label: formatEnumLabel('archived') },
225
- ],
226
- },
227
- ]}
228
- />
243
+ <div className="flex min-w-0 flex-col gap-4 xl:flex-row xl:items-center">
244
+ <div className="flex-1">
245
+ <SearchBar
246
+ className="w-full"
247
+ searchQuery={search}
248
+ onSearchChange={setSearch}
249
+ onSearch={() => undefined}
250
+ placeholder={t('searchPlaceholder')}
251
+ controls={[
252
+ {
253
+ id: 'status',
254
+ type: 'select',
255
+ value: statusFilter,
256
+ onChange: setStatusFilter,
257
+ placeholder: commonT('labels.status'),
258
+ options: [
259
+ { value: 'all', label: commonT('filters.allStatuses') },
260
+ { value: 'planning', label: formatEnumLabel('planning') },
261
+ { value: 'active', label: formatEnumLabel('active') },
262
+ { value: 'at_risk', label: formatEnumLabel('at_risk') },
263
+ { value: 'paused', label: formatEnumLabel('paused') },
264
+ { value: 'completed', label: formatEnumLabel('completed') },
265
+ { value: 'archived', label: formatEnumLabel('archived') },
266
+ ],
267
+ },
268
+ ]}
269
+ />
270
+ </div>
271
+
272
+ <div className="flex items-center justify-between gap-3 xl:justify-end">
273
+ <span className="text-xs font-medium text-muted-foreground">
274
+ {t('viewMode')}
275
+ </span>
276
+ <ToggleGroup
277
+ type="single"
278
+ value={viewMode}
279
+ onValueChange={handleViewModeChange}
280
+ variant="outline"
281
+ size="sm"
282
+ aria-label={t('viewMode')}
283
+ >
284
+ <ToggleGroupItem
285
+ value="table"
286
+ className="gap-1.5 px-2.5"
287
+ aria-label={t('viewModeTable')}
288
+ >
289
+ <List className="h-4 w-4" />
290
+ <span className="hidden sm:inline">{t('viewModeTable')}</span>
291
+ </ToggleGroupItem>
292
+ <ToggleGroupItem
293
+ value="cards"
294
+ className="gap-1.5 px-2.5"
295
+ aria-label={t('viewModeCards')}
296
+ >
297
+ <LayoutGrid className="h-4 w-4" />
298
+ <span className="hidden sm:inline">{t('viewModeCards')}</span>
299
+ </ToggleGroupItem>
300
+ </ToggleGroup>
301
+ </div>
302
+ </div>
229
303
 
230
304
  {filteredRows.length > 0 ? (
231
- <div className="overflow-x-auto rounded-md border">
232
- <Table className="table-fixed">
233
- <TableHeader>
234
- <TableRow>
235
- <TableHead className="w-[30%]">
236
- {commonT('labels.project')}
237
- </TableHead>
238
- <TableHead>{commonT('labels.client')}</TableHead>
239
- <TableHead>{commonT('labels.status')}</TableHead>
240
- <TableHead className="hidden lg:table-cell">
241
- {commonT('labels.manager')}
242
- </TableHead>
243
- <TableHead className="hidden md:table-cell">
244
- {commonT('labels.teamSize')}
245
- </TableHead>
246
- <TableHead className="hidden xl:table-cell">
247
- {commonT('labels.startDate')}
248
- </TableHead>
249
- <TableHead className="hidden xl:table-cell">
250
- {commonT('labels.endDate')}
251
- </TableHead>
252
- <TableHead className="hidden 2xl:table-cell">
253
- {commonT('labels.contractStatus')}
254
- </TableHead>
255
- <TableHead className="w-30 text-right sm:w-42.5">
256
- {commonT('labels.actions')}
257
- </TableHead>
258
- </TableRow>
259
- </TableHeader>
260
- <TableBody>
261
- {filteredRows.map((project) => (
262
- <TableRow key={project.id} className="cursor-pointer hover:bg-muted/30">
263
- <TableCell>
305
+ viewMode === 'cards' ? (
306
+ <div className="grid gap-4 md:grid-cols-2 2xl:grid-cols-3">
307
+ {filteredRows.map((project) => (
308
+ <Card
309
+ key={project.id}
310
+ className="cursor-pointer overflow-hidden border-border/60 py-0 shadow-sm transition-all hover:-translate-y-0.5 hover:shadow-md"
311
+ >
312
+ <CardContent className="space-y-4 p-4">
313
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
264
314
  <div className="min-w-0">
265
- <div className="truncate font-medium">{project.name}</div>
315
+ <div className="truncate font-semibold">
316
+ {project.name}
317
+ </div>
266
318
  <div className="truncate text-xs text-muted-foreground">
267
319
  {[
268
320
  project.code,
@@ -273,96 +325,271 @@ export default function OperationsProjectsPage() {
273
325
  .join(' • ') || commonT('labels.notAvailable')}
274
326
  </div>
275
327
  </div>
276
- </TableCell>
277
- <TableCell>
278
- <div className="truncate">
279
- {project.clientName || commonT('labels.notAvailable')}
280
- </div>
281
- </TableCell>
282
- <TableCell>
283
328
  <StatusBadge
284
329
  label={formatEnumLabel(project.status)}
285
330
  className={getStatusBadgeClass(project.status)}
286
331
  />
287
- </TableCell>
288
- <TableCell className="hidden lg:table-cell">
289
- <div className="truncate">
332
+ </div>
333
+
334
+ <div className="grid gap-2 text-sm text-muted-foreground lg:grid-cols-2">
335
+ <div>
336
+ <span className="font-medium text-foreground">
337
+ {commonT('labels.client')}:
338
+ </span>{' '}
339
+ {project.clientName || commonT('labels.notAvailable')}
340
+ </div>
341
+ <div>
342
+ <span className="font-medium text-foreground">
343
+ {commonT('labels.manager')}:
344
+ </span>{' '}
290
345
  {project.managerName || commonT('labels.notAssigned')}
291
346
  </div>
292
- </TableCell>
293
- <TableCell className="hidden md:table-cell">
294
- {project.teamSize ?? 0}
295
- </TableCell>
296
- <TableCell className="hidden xl:table-cell">
297
- {formatDate(project.startDate)}
298
- </TableCell>
299
- <TableCell className="hidden xl:table-cell">
300
- {formatDate(project.endDate)}
301
- </TableCell>
302
- <TableCell className="hidden 2xl:table-cell">
303
- {project.contractStatus ? (
304
- <StatusBadge
305
- label={formatEnumLabel(project.contractStatus)}
306
- className={getStatusBadgeClass(project.contractStatus)}
307
- />
308
- ) : (
309
- commonT('labels.notAssigned')
310
- )}
311
- </TableCell>
312
- <TableCell>
313
- <div className="flex flex-wrap justify-end gap-1.5 sm:gap-2">
314
- <Button variant="outline" size="icon" asChild>
315
- <Link href={`/operations/projects/${project.id}`}>
316
- <Eye className="size-4" />
317
- </Link>
347
+ <div>
348
+ <span className="font-medium text-foreground">
349
+ {commonT('labels.teamSize')}:
350
+ </span>{' '}
351
+ {project.teamSize ?? 0}
352
+ </div>
353
+ <div>
354
+ <span className="font-medium text-foreground">
355
+ {commonT('labels.startDate')}:
356
+ </span>{' '}
357
+ {formatDate(
358
+ project.startDate,
359
+ getSettingValue,
360
+ currentLocaleCode
361
+ )}
362
+ </div>
363
+ <div>
364
+ <span className="font-medium text-foreground">
365
+ {commonT('labels.endDate')}:
366
+ </span>{' '}
367
+ {formatDate(
368
+ project.endDate,
369
+ getSettingValue,
370
+ currentLocaleCode
371
+ )}
372
+ </div>
373
+ <div className="flex items-center gap-2">
374
+ <span className="font-medium text-foreground">
375
+ {commonT('labels.contractStatus')}:
376
+ </span>
377
+ {project.contractStatus ? (
378
+ <StatusBadge
379
+ label={formatEnumLabel(project.contractStatus)}
380
+ className={getStatusBadgeClass(
381
+ project.contractStatus
382
+ )}
383
+ />
384
+ ) : (
385
+ <span>{commonT('labels.notAssigned')}</span>
386
+ )}
387
+ </div>
388
+ </div>
389
+
390
+ <div className="flex flex-wrap justify-end gap-2 border-t border-border/60 pt-3">
391
+ <Button variant="outline" size="icon" asChild>
392
+ <Link href={`/operations/projects/${project.id}`}>
393
+ <Eye className="size-4" />
394
+ </Link>
395
+ </Button>
396
+ {access.isDirector ? (
397
+ <Button
398
+ variant="outline"
399
+ size="icon"
400
+ className="cursor-pointer"
401
+ onClick={() => openEditSheet(project.id)}
402
+ >
403
+ <Pencil className="size-4" />
318
404
  </Button>
319
- {access.isDirector ? (
320
- <Button
321
- variant="outline"
322
- size="icon"
323
- className="cursor-pointer"
324
- onClick={() => openEditSheet(project.id)}
405
+ ) : null}
406
+ <Button
407
+ variant="outline"
408
+ size="icon"
409
+ asChild={Boolean(project.contractId)}
410
+ disabled={!project.contractId}
411
+ >
412
+ {project.contractId ? (
413
+ <Link
414
+ href={`/operations/contracts?edit=${project.contractId}`}
325
415
  >
326
- <Pencil className="size-4" />
327
- </Button>
328
- ) : null}
416
+ <FileText className="size-4" />
417
+ </Link>
418
+ ) : (
419
+ <span>
420
+ <FileText className="size-4" />
421
+ </span>
422
+ )}
423
+ </Button>
424
+ {access.isDirector ? (
329
425
  <Button
330
426
  variant="outline"
331
- size="icon"
332
- asChild={Boolean(project.contractId)}
333
- disabled={!project.contractId}
427
+ size="sm"
428
+ className="cursor-pointer"
429
+ onClick={() => void toggleArchived(project)}
334
430
  >
335
- {project.contractId ? (
336
- <Link
337
- href={`/operations/contracts?edit=${project.contractId}`}
338
- >
339
- <FileText className="size-4" />
340
- </Link>
341
- ) : (
342
- <span>
343
- <FileText className="size-4" />
344
- </span>
345
- )}
431
+ {project.status === 'archived'
432
+ ? commonT('actions.activate')
433
+ : t('actions.archive')}
346
434
  </Button>
347
- {access.isDirector ? (
435
+ ) : null}
436
+ </div>
437
+ </CardContent>
438
+ </Card>
439
+ ))}
440
+ </div>
441
+ ) : (
442
+ <div className="overflow-x-auto rounded-md border">
443
+ <Table className="table-fixed">
444
+ <TableHeader>
445
+ <TableRow>
446
+ <TableHead className="w-[30%]">
447
+ {commonT('labels.project')}
448
+ </TableHead>
449
+ <TableHead>{commonT('labels.client')}</TableHead>
450
+ <TableHead>{commonT('labels.status')}</TableHead>
451
+ <TableHead className="hidden lg:table-cell">
452
+ {commonT('labels.manager')}
453
+ </TableHead>
454
+ <TableHead className="hidden md:table-cell">
455
+ {commonT('labels.teamSize')}
456
+ </TableHead>
457
+ <TableHead className="hidden xl:table-cell">
458
+ {commonT('labels.startDate')}
459
+ </TableHead>
460
+ <TableHead className="hidden xl:table-cell">
461
+ {commonT('labels.endDate')}
462
+ </TableHead>
463
+ <TableHead className="hidden 2xl:table-cell">
464
+ {commonT('labels.contractStatus')}
465
+ </TableHead>
466
+ <TableHead className="w-30 text-right sm:w-42.5">
467
+ {commonT('labels.actions')}
468
+ </TableHead>
469
+ </TableRow>
470
+ </TableHeader>
471
+ <TableBody>
472
+ {filteredRows.map((project) => (
473
+ <TableRow
474
+ key={project.id}
475
+ className="cursor-pointer hover:bg-muted/30"
476
+ >
477
+ <TableCell>
478
+ <div className="min-w-0">
479
+ <div className="truncate font-medium">
480
+ {project.name}
481
+ </div>
482
+ <div className="truncate text-xs text-muted-foreground">
483
+ {[
484
+ project.code,
485
+ project.myRoleLabel,
486
+ project.contractName,
487
+ ]
488
+ .filter(Boolean)
489
+ .join(' • ') || commonT('labels.notAvailable')}
490
+ </div>
491
+ </div>
492
+ </TableCell>
493
+ <TableCell>
494
+ <div className="truncate">
495
+ {project.clientName || commonT('labels.notAvailable')}
496
+ </div>
497
+ </TableCell>
498
+ <TableCell>
499
+ <StatusBadge
500
+ label={formatEnumLabel(project.status)}
501
+ className={getStatusBadgeClass(project.status)}
502
+ />
503
+ </TableCell>
504
+ <TableCell className="hidden lg:table-cell">
505
+ <div className="truncate">
506
+ {project.managerName || commonT('labels.notAssigned')}
507
+ </div>
508
+ </TableCell>
509
+ <TableCell className="hidden md:table-cell">
510
+ {project.teamSize ?? 0}
511
+ </TableCell>
512
+ <TableCell className="hidden xl:table-cell">
513
+ {formatDate(
514
+ project.startDate,
515
+ getSettingValue,
516
+ currentLocaleCode
517
+ )}
518
+ </TableCell>
519
+ <TableCell className="hidden xl:table-cell">
520
+ {formatDate(
521
+ project.endDate,
522
+ getSettingValue,
523
+ currentLocaleCode
524
+ )}
525
+ </TableCell>
526
+ <TableCell className="hidden 2xl:table-cell">
527
+ {project.contractStatus ? (
528
+ <StatusBadge
529
+ label={formatEnumLabel(project.contractStatus)}
530
+ className={getStatusBadgeClass(
531
+ project.contractStatus
532
+ )}
533
+ />
534
+ ) : (
535
+ commonT('labels.notAssigned')
536
+ )}
537
+ </TableCell>
538
+ <TableCell>
539
+ <div className="flex flex-wrap justify-end gap-1.5 sm:gap-2">
540
+ <Button variant="outline" size="icon" asChild>
541
+ <Link href={`/operations/projects/${project.id}`}>
542
+ <Eye className="size-4" />
543
+ </Link>
544
+ </Button>
545
+ {access.isDirector ? (
546
+ <Button
547
+ variant="outline"
548
+ size="icon"
549
+ className="cursor-pointer"
550
+ onClick={() => openEditSheet(project.id)}
551
+ >
552
+ <Pencil className="size-4" />
553
+ </Button>
554
+ ) : null}
348
555
  <Button
349
556
  variant="outline"
350
- size="sm"
351
- className="cursor-pointer"
352
- onClick={() => void toggleArchived(project)}
557
+ size="icon"
558
+ asChild={Boolean(project.contractId)}
559
+ disabled={!project.contractId}
353
560
  >
354
- {project.status === 'archived'
355
- ? commonT('actions.activate')
356
- : t('actions.archive')}
561
+ {project.contractId ? (
562
+ <Link
563
+ href={`/operations/contracts?edit=${project.contractId}`}
564
+ >
565
+ <FileText className="size-4" />
566
+ </Link>
567
+ ) : (
568
+ <span>
569
+ <FileText className="size-4" />
570
+ </span>
571
+ )}
357
572
  </Button>
358
- ) : null}
359
- </div>
360
- </TableCell>
361
- </TableRow>
362
- ))}
363
- </TableBody>
364
- </Table>
365
- </div>
573
+ {access.isDirector ? (
574
+ <Button
575
+ variant="outline"
576
+ size="sm"
577
+ className="cursor-pointer"
578
+ onClick={() => void toggleArchived(project)}
579
+ >
580
+ {project.status === 'archived'
581
+ ? commonT('actions.activate')
582
+ : t('actions.archive')}
583
+ </Button>
584
+ ) : null}
585
+ </div>
586
+ </TableCell>
587
+ </TableRow>
588
+ ))}
589
+ </TableBody>
590
+ </Table>
591
+ </div>
592
+ )
366
593
  ) : (
367
594
  <EmptyState
368
595
  icon={<FolderKanban className="size-12" />}