@hed-hog/operations 0.0.318 → 0.0.321

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 (138) 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-contracts.controller.d.ts +9 -9
  10. package/dist/controllers/operations-projects.controller.d.ts +31 -0
  11. package/dist/controllers/operations-projects.controller.d.ts.map +1 -1
  12. package/dist/controllers/operations-projects.controller.js +23 -0
  13. package/dist/controllers/operations-projects.controller.js.map +1 -1
  14. package/dist/controllers/operations-reports.controller.d.ts +199 -0
  15. package/dist/controllers/operations-reports.controller.d.ts.map +1 -0
  16. package/dist/controllers/operations-reports.controller.js +53 -0
  17. package/dist/controllers/operations-reports.controller.js.map +1 -0
  18. package/dist/controllers/operations-tasks.controller.d.ts +41 -2
  19. package/dist/controllers/operations-tasks.controller.d.ts.map +1 -1
  20. package/dist/controllers/operations-tasks.controller.js +17 -5
  21. package/dist/controllers/operations-tasks.controller.js.map +1 -1
  22. package/dist/dto/create-collaborator-cost.dto.d.ts +16 -0
  23. package/dist/dto/create-collaborator-cost.dto.d.ts.map +1 -0
  24. package/dist/dto/create-collaborator-cost.dto.js +88 -0
  25. package/dist/dto/create-collaborator-cost.dto.js.map +1 -0
  26. package/dist/dto/create-collaborator.dto.d.ts +0 -1
  27. package/dist/dto/create-collaborator.dto.d.ts.map +1 -1
  28. package/dist/dto/create-collaborator.dto.js +0 -6
  29. package/dist/dto/create-collaborator.dto.js.map +1 -1
  30. package/dist/dto/create-cost-type.dto.d.ts +13 -0
  31. package/dist/dto/create-cost-type.dto.d.ts.map +1 -0
  32. package/dist/dto/create-cost-type.dto.js +87 -0
  33. package/dist/dto/create-cost-type.dto.js.map +1 -0
  34. package/dist/dto/list-approvals.dto.d.ts +2 -0
  35. package/dist/dto/list-approvals.dto.d.ts.map +1 -1
  36. package/dist/dto/list-approvals.dto.js +10 -0
  37. package/dist/dto/list-approvals.dto.js.map +1 -1
  38. package/dist/dto/list-collaborator-costs.dto.d.ts +5 -0
  39. package/dist/dto/list-collaborator-costs.dto.d.ts.map +1 -0
  40. package/dist/dto/list-collaborator-costs.dto.js +23 -0
  41. package/dist/dto/list-collaborator-costs.dto.js.map +1 -0
  42. package/dist/dto/list-cost-types.dto.d.ts +6 -0
  43. package/dist/dto/list-cost-types.dto.d.ts.map +1 -0
  44. package/dist/dto/list-cost-types.dto.js +35 -0
  45. package/dist/dto/list-cost-types.dto.js.map +1 -0
  46. package/dist/dto/list-my-projects.dto.d.ts +5 -0
  47. package/dist/dto/list-my-projects.dto.d.ts.map +1 -0
  48. package/dist/dto/list-my-projects.dto.js +23 -0
  49. package/dist/dto/list-my-projects.dto.js.map +1 -0
  50. package/dist/dto/list-my-tasks.dto.d.ts +6 -0
  51. package/dist/dto/list-my-tasks.dto.d.ts.map +1 -0
  52. package/dist/dto/list-my-tasks.dto.js +33 -0
  53. package/dist/dto/list-my-tasks.dto.js.map +1 -0
  54. package/dist/dto/list-projects.dto.d.ts +1 -0
  55. package/dist/dto/list-projects.dto.d.ts.map +1 -1
  56. package/dist/dto/list-projects.dto.js +7 -0
  57. package/dist/dto/list-projects.dto.js.map +1 -1
  58. package/dist/dto/list-reports.dto.d.ts +16 -0
  59. package/dist/dto/list-reports.dto.d.ts.map +1 -0
  60. package/dist/dto/list-reports.dto.js +75 -0
  61. package/dist/dto/list-reports.dto.js.map +1 -0
  62. package/dist/dto/list-tasks.dto.d.ts +2 -0
  63. package/dist/dto/list-tasks.dto.d.ts.map +1 -1
  64. package/dist/dto/list-tasks.dto.js +12 -0
  65. package/dist/dto/list-tasks.dto.js.map +1 -1
  66. package/dist/dto/list-timesheets.dto.d.ts +2 -0
  67. package/dist/dto/list-timesheets.dto.d.ts.map +1 -1
  68. package/dist/dto/list-timesheets.dto.js +10 -0
  69. package/dist/dto/list-timesheets.dto.js.map +1 -1
  70. package/dist/dto/update-collaborator-cost.dto.d.ts +6 -0
  71. package/dist/dto/update-collaborator-cost.dto.d.ts.map +1 -0
  72. package/dist/dto/update-collaborator-cost.dto.js +9 -0
  73. package/dist/dto/update-collaborator-cost.dto.js.map +1 -0
  74. package/dist/dto/update-task.dto.d.ts +1 -0
  75. package/dist/dto/update-task.dto.d.ts.map +1 -1
  76. package/dist/dto/update-task.dto.js +6 -0
  77. package/dist/dto/update-task.dto.js.map +1 -1
  78. package/dist/operations.module.d.ts.map +1 -1
  79. package/dist/operations.module.js +4 -0
  80. package/dist/operations.module.js.map +1 -1
  81. package/dist/operations.service.d.ts +457 -3
  82. package/dist/operations.service.d.ts.map +1 -1
  83. package/dist/operations.service.js +1445 -208
  84. package/dist/operations.service.js.map +1 -1
  85. package/dist/operations.service.spec.js +31 -7
  86. package/dist/operations.service.spec.js.map +1 -1
  87. package/hedhog/data/menu.yaml +112 -7
  88. package/hedhog/data/operations_cost_type.yaml +166 -0
  89. package/hedhog/data/route.yaml +185 -0
  90. package/hedhog/frontend/app/_components/collaborator-costs-section.tsx.ejs +884 -0
  91. package/hedhog/frontend/app/_components/collaborator-details-screen.tsx.ejs +80 -1
  92. package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +219 -94
  93. package/hedhog/frontend/app/_components/contract-details-screen.tsx.ejs +21 -32
  94. package/hedhog/frontend/app/_components/contract-form-screen.tsx.ejs +178 -89
  95. package/hedhog/frontend/app/_components/my-project-summary-screen.tsx.ejs +1185 -0
  96. package/hedhog/frontend/app/_components/operations-calendar-view.tsx.ejs +306 -0
  97. package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +943 -782
  98. package/hedhog/frontend/app/_components/task-detail-sheet.tsx.ejs +223 -0
  99. package/hedhog/frontend/app/_lib/api.ts.ejs +162 -0
  100. package/hedhog/frontend/app/_lib/types.ts.ejs +227 -1
  101. package/hedhog/frontend/app/_lib/utils/format.ts.ejs +11 -3
  102. package/hedhog/frontend/app/approvals/page.tsx.ejs +191 -46
  103. package/hedhog/frontend/app/collaborators/page.tsx.ejs +133 -25
  104. package/hedhog/frontend/app/my-projects/[id]/page.tsx.ejs +11 -0
  105. package/hedhog/frontend/app/my-projects/page.tsx.ejs +440 -0
  106. package/hedhog/frontend/app/my-tasks/page.tsx.ejs +1304 -0
  107. package/hedhog/frontend/app/reports/collaborators/page.tsx.ejs +771 -0
  108. package/hedhog/frontend/app/reports/projects/page.tsx.ejs +809 -0
  109. package/hedhog/frontend/app/timesheets/page.tsx.ejs +322 -58
  110. package/hedhog/frontend/messages/en.json +234 -25
  111. package/hedhog/frontend/messages/pt.json +234 -25
  112. package/hedhog/table/operations_collaborator.yaml +0 -4
  113. package/hedhog/table/operations_collaborator_compensation_history.yaml +28 -0
  114. package/hedhog/table/operations_collaborator_cost.yaml +56 -0
  115. package/hedhog/table/operations_cost_type.yaml +38 -0
  116. package/package.json +7 -7
  117. package/src/controllers/operations-collaborator-costs.controller.ts +147 -0
  118. package/src/controllers/operations-collaborators.controller.ts +19 -8
  119. package/src/controllers/operations-projects.controller.ts +19 -8
  120. package/src/controllers/operations-reports.controller.ts +32 -0
  121. package/src/controllers/operations-tasks.controller.ts +32 -12
  122. package/src/dto/create-collaborator-cost.dto.ts +78 -0
  123. package/src/dto/create-collaborator.dto.ts +9 -14
  124. package/src/dto/create-cost-type.dto.ts +62 -0
  125. package/src/dto/list-approvals.dto.ts +8 -0
  126. package/src/dto/list-collaborator-costs.dto.ts +8 -0
  127. package/src/dto/list-cost-types.dto.ts +19 -0
  128. package/src/dto/list-my-projects.dto.ts +8 -0
  129. package/src/dto/list-my-tasks.dto.ts +17 -0
  130. package/src/dto/list-projects.dto.ts +7 -1
  131. package/src/dto/list-reports.dto.ts +51 -0
  132. package/src/dto/list-tasks.dto.ts +11 -1
  133. package/src/dto/list-timesheets.dto.ts +8 -0
  134. package/src/dto/update-collaborator-cost.dto.ts +4 -0
  135. package/src/dto/update-task.dto.ts +6 -0
  136. package/src/operations.module.ts +4 -0
  137. package/src/operations.service.spec.ts +45 -7
  138. package/src/operations.service.ts +1988 -221
@@ -57,6 +57,8 @@ import { CSS } from '@dnd-kit/utilities';
57
57
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
58
58
  import {
59
59
  AlarmClock,
60
+ Archive,
61
+ ArchiveRestore,
60
62
  BarChart3,
61
63
  FileText,
62
64
  FolderKanban,
@@ -81,7 +83,10 @@ import {
81
83
  } from 'recharts';
82
84
  import { fetchOperations, mutateOperations } from '../_lib/api';
83
85
  import { useOperationsAccess } from '../_lib/hooks/use-operations-access';
84
- import type { OperationsProjectDetails } from '../_lib/types';
86
+ import type {
87
+ OperationsProjectDetails,
88
+ OperationsTaskOption,
89
+ } from '../_lib/types';
85
90
  import {
86
91
  formatCurrency,
87
92
  formatDate,
@@ -95,6 +100,7 @@ import { OperationsHeader } from './operations-header';
95
100
  import { ProjectFormScreen } from './project-form-screen';
96
101
  import { SectionCard } from './section-card';
97
102
  import { StatusBadge } from './status-badge';
103
+ import { TaskDetailSheet, type TaskDetailSheetData } from './task-detail-sheet';
98
104
 
99
105
  type BoardColumnId = 'todo' | 'doing' | 'review' | 'done';
100
106
 
@@ -237,11 +243,17 @@ function DroppableColumn({
237
243
 
238
244
  function DraggableTaskCard({
239
245
  task,
246
+ disabled = false,
240
247
  children,
241
248
  }: {
242
249
  task: BoardTask;
250
+ disabled?: boolean;
243
251
  children: (isDragging: boolean) => React.ReactNode;
244
252
  }) {
253
+ if (disabled) {
254
+ return <div>{children(false)}</div>;
255
+ }
256
+
245
257
  const { attributes, listeners, setNodeRef, transform, isDragging } =
246
258
  useDraggable({ id: taskDragId(task.id) });
247
259
 
@@ -319,6 +331,7 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
319
331
  const contractT = useTranslations('operations.ContractFormPage');
320
332
  const { request, currentLocaleCode, getSettingValue } = useApp();
321
333
  const access = useOperationsAccess();
334
+ const isLimitedView = !access.isDirector && !access.isSupervisor;
322
335
  const router = useRouter();
323
336
  const pathname = usePathname();
324
337
  const searchParams = useSearchParams();
@@ -365,8 +378,10 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
365
378
  };
366
379
 
367
380
  const isEditSheetOpen = useMemo(
368
- () => shouldOpenEditSheet(searchParams.get('edit'), projectId),
369
- [projectId, searchParams]
381
+ () =>
382
+ !isLimitedView &&
383
+ shouldOpenEditSheet(searchParams.get('edit'), projectId),
384
+ [isLimitedView, projectId, searchParams]
370
385
  );
371
386
 
372
387
  const updateSheetQuery = (open: boolean) => {
@@ -443,6 +458,17 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
443
458
  enabled: Boolean(project),
444
459
  });
445
460
 
461
+ const { data: archivedTasksResponse, refetch: refetchArchivedTasks } =
462
+ useQuery<{ data: OperationsTaskOption[] }>({
463
+ queryKey: ['operations-project-archived-tasks', projectId],
464
+ queryFn: () =>
465
+ fetchOperations<{ data: OperationsTaskOption[] }>(
466
+ request,
467
+ `/operations/tasks?projectId=${projectId}&pageSize=100&sortField=createdAt&sortOrder=desc&archived=true`
468
+ ),
469
+ enabled: Boolean(project),
470
+ });
471
+
446
472
  const { data: projectStats } = useQuery<{
447
473
  weeklyVelocity: Array<{ weekLabel: string; loggedHours: number }>;
448
474
  allocationByCollaborator: Array<{ name: string; allocation: number }>;
@@ -455,19 +481,29 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
455
481
  queryKey: ['operations-project-stats', projectId],
456
482
  queryFn: () =>
457
483
  fetchOperations(request, `/operations/projects/${projectId}/stats`),
458
- enabled: Boolean(project),
484
+ enabled: Boolean(project) && !isLimitedView,
459
485
  });
460
486
 
461
487
  const [boardState, setBoardState] = useState<BoardState | null>(null);
462
- const [selectedTaskId, setSelectedTaskId] = useState<number | null>(null);
488
+ const [selectedTask, setSelectedTask] = useState<TaskDetailSheetData | null>(
489
+ null
490
+ );
463
491
  const [taskFormOpen, setTaskFormOpen] = useState(false);
464
492
  const [editingTaskId, setEditingTaskId] = useState<number | null>(null);
465
493
  const [taskFormData, setTaskFormData] =
466
494
  useState<TaskFormState>(EMPTY_TASK_FORM);
467
495
  const [taskFormLoading, setTaskFormLoading] = useState(false);
468
- const [deleteConfirmId, setDeleteConfirmId] = useState<number | null>(null);
496
+ const [deletePromptTask, setDeletePromptTask] =
497
+ useState<TaskDetailSheetData | null>(null);
469
498
 
470
499
  const apiTasks = useMemo(() => rawTasks.map(apiTaskToBoardTask), [rawTasks]);
500
+ const archivedTasks = useMemo(
501
+ () =>
502
+ (archivedTasksResponse?.data ?? []).filter((task) =>
503
+ Boolean(task.deletedAt)
504
+ ),
505
+ [archivedTasksResponse]
506
+ );
471
507
 
472
508
  const taskColumns: BoardColumns = useMemo(() => {
473
509
  if (project && boardState?.projectId === project.id) {
@@ -476,21 +512,6 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
476
512
  return splitTasksByColumn(apiTasks);
477
513
  }, [project, boardState, apiTasks]);
478
514
 
479
- const allTasks = useMemo(
480
- () => [
481
- ...taskColumns.todo,
482
- ...taskColumns.doing,
483
- ...taskColumns.review,
484
- ...taskColumns.done,
485
- ],
486
- [taskColumns]
487
- );
488
-
489
- const selectedTask = useMemo(
490
- () => allTasks.find((task) => task.id === selectedTaskId) ?? null,
491
- [allTasks, selectedTaskId]
492
- );
493
-
494
515
  const taskAssigneeOptions = useMemo(() => {
495
516
  const seen = new Set<number>();
496
517
  return (
@@ -536,7 +557,7 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
536
557
  task.estimateHours != null ? String(task.estimateHours) : '',
537
558
  tags: task.tags ?? '',
538
559
  });
539
- setSelectedTaskId(null);
560
+ setSelectedTask(null);
540
561
  setTaskFormOpen(true);
541
562
  }, []);
542
563
 
@@ -572,31 +593,83 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
572
593
  }
573
594
  setBoardState(null);
574
595
  await refetchTasks();
596
+ await refetchArchivedTasks();
575
597
  setTaskFormOpen(false);
576
598
  setEditingTaskId(null);
577
599
  setTaskFormData(EMPTY_TASK_FORM);
578
600
  } finally {
579
601
  setTaskFormLoading(false);
580
602
  }
581
- }, [taskFormData, editingTaskId, projectId, request, refetchTasks]);
603
+ }, [
604
+ taskFormData,
605
+ editingTaskId,
606
+ projectId,
607
+ request,
608
+ refetchTasks,
609
+ refetchArchivedTasks,
610
+ ]);
611
+
612
+ const handleArchiveTask = useCallback(
613
+ async (taskId: number) => {
614
+ try {
615
+ await mutateOperations(
616
+ request,
617
+ `/operations/tasks/${taskId}`,
618
+ 'PATCH',
619
+ {
620
+ archived: true,
621
+ }
622
+ );
623
+ setBoardState(null);
624
+ setSelectedTask(null);
625
+ await refetchTasks();
626
+ await refetchArchivedTasks();
627
+ } catch {
628
+ // ignore
629
+ }
630
+ },
631
+ [request, refetchTasks, refetchArchivedTasks]
632
+ );
582
633
 
583
- const handleDeleteTask = useCallback(
634
+ const handleRestoreTask = useCallback(
584
635
  async (taskId: number) => {
585
636
  try {
586
637
  await mutateOperations(
587
638
  request,
588
639
  `/operations/tasks/${taskId}`,
640
+ 'PATCH',
641
+ {
642
+ archived: false,
643
+ }
644
+ );
645
+ setSelectedTask(null);
646
+ await refetchTasks();
647
+ await refetchArchivedTasks();
648
+ } catch {
649
+ // ignore
650
+ }
651
+ },
652
+ [request, refetchTasks, refetchArchivedTasks]
653
+ );
654
+
655
+ const handleDeleteTask = useCallback(
656
+ async (taskId: number) => {
657
+ try {
658
+ await mutateOperations(
659
+ request,
660
+ `/operations/tasks/${taskId}?permanent=true`,
589
661
  'DELETE'
590
662
  );
591
663
  setBoardState(null);
592
- setSelectedTaskId(null);
593
- setDeleteConfirmId(null);
664
+ setSelectedTask(null);
665
+ setDeletePromptTask(null);
594
666
  await refetchTasks();
667
+ await refetchArchivedTasks();
595
668
  } catch {
596
669
  // ignore
597
670
  }
598
671
  },
599
- [request, refetchTasks]
672
+ [request, refetchTasks, refetchArchivedTasks]
600
673
  );
601
674
 
602
675
  const allocationChartData = useMemo(() => {
@@ -772,232 +845,91 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
772
845
  }
773
846
  />
774
847
 
775
- <div className="rounded-xl border bg-linear-to-b from-muted/40 to-background p-3 sm:p-4">
776
- <KpiCardsGrid items={cards} />
777
- </div>
848
+ {!isLimitedView ? (
849
+ <>
850
+ <div className="rounded-xl border bg-linear-to-b from-muted/40 to-background p-3 sm:p-4">
851
+ <KpiCardsGrid items={cards} />
852
+ </div>
778
853
 
779
- <div className="grid gap-4 xl:grid-cols-12">
780
- <SectionCard
781
- title={t('sections.overview')}
782
- className="rounded-xl border bg-card p-4 shadow-sm xl:col-span-7"
783
- >
784
- <dl className="grid gap-3 text-sm sm:grid-cols-2 xl:grid-cols-3">
785
- <div>
786
- <dt className="text-muted-foreground">
787
- {commonT('labels.project')}
788
- </dt>
789
- <dd className="font-medium">{project.name}</dd>
790
- </div>
791
- <div>
792
- <dt className="text-muted-foreground">
793
- {commonT('labels.code')}
794
- </dt>
795
- <dd className="font-medium">
796
- {project.code || commonT('labels.notAvailable')}
797
- </dd>
798
- </div>
799
- <div>
800
- <dt className="text-muted-foreground">
801
- {commonT('labels.client')}
802
- </dt>
803
- <dd className="font-medium">
804
- <div className="flex items-center gap-2">
805
- <Avatar className="h-8 w-8 border border-border/60 bg-muted">
806
- <AvatarImage
807
- src={getPersonAvatarUrl(project.clientAvatarId)}
808
- alt={project.clientName || commonT('labels.client')}
809
- />
810
- <AvatarFallback className="bg-muted text-xs font-semibold text-foreground">
811
- {getInitials(
812
- project.clientName || commonT('labels.client')
813
- )}
814
- </AvatarFallback>
815
- </Avatar>
816
- <span>
817
- {project.clientName || commonT('labels.notAvailable')}
818
- </span>
854
+ <div className="grid gap-4 xl:grid-cols-12">
855
+ <SectionCard
856
+ title={t('sections.overview')}
857
+ className="rounded-xl border bg-card p-4 shadow-sm xl:col-span-7"
858
+ >
859
+ <dl className="grid gap-3 text-sm sm:grid-cols-2 xl:grid-cols-3">
860
+ <div>
861
+ <dt className="text-muted-foreground">
862
+ {commonT('labels.project')}
863
+ </dt>
864
+ <dd className="font-medium">{project.name}</dd>
819
865
  </div>
820
- </dd>
821
- </div>
822
- <div>
823
- <dt className="text-muted-foreground">
824
- {commonT('labels.manager')}
825
- </dt>
826
- <dd className="font-medium">
827
- {project.managerName || commonT('labels.notAssigned')}
828
- </dd>
829
- </div>
830
- <div>
831
- <dt className="text-muted-foreground">
832
- {commonT('labels.status')}
833
- </dt>
834
- <dd className="font-medium">
835
- <StatusBadge
836
- label={getProjectStatusLabel(project.status)}
837
- className={getStatusBadgeClass(project.status)}
838
- />
839
- </dd>
840
- </div>
841
- <div>
842
- <dt className="text-muted-foreground">
843
- {commonT('labels.deliveryModel')}
844
- </dt>
845
- <dd className="font-medium">
846
- {project.deliveryModel
847
- ? getDeliveryModelLabel(project.deliveryModel)
848
- : commonT('labels.notAvailable')}
849
- </dd>
850
- </div>
851
- <div>
852
- <dt className="text-muted-foreground">
853
- {commonT('labels.startDate')}
854
- </dt>
855
- <dd className="font-medium">
856
- {formatDate(
857
- project.startDate,
858
- getSettingValue,
859
- currentLocaleCode
860
- )}
861
- </dd>
862
- </div>
863
- <div>
864
- <dt className="text-muted-foreground">
865
- {commonT('labels.endDate')}
866
- </dt>
867
- <dd className="font-medium">
868
- {formatDate(
869
- project.endDate,
870
- getSettingValue,
871
- currentLocaleCode
872
- )}
873
- </dd>
874
- </div>
875
- <div>
876
- <dt className="text-muted-foreground">
877
- {commonT('labels.budget')}
878
- </dt>
879
- <dd className="font-medium">
880
- {project.budgetAmount
881
- ? formatCurrency(
882
- project.budgetAmount,
883
- getSettingValue,
884
- currentLocaleCode
885
- )
886
- : commonT('labels.notAvailable')}
887
- </dd>
888
- </div>
889
- <div>
890
- <dt className="text-muted-foreground">
891
- {commonT('labels.progress')}
892
- </dt>
893
- <dd className="font-medium">
894
- {formatPercent(project.progressPercent)}
895
- </dd>
896
- </div>
897
- <div>
898
- <dt className="text-muted-foreground">
899
- {commonT('labels.timeline')}
900
- </dt>
901
- <dd className="font-medium">
902
- {formatDateRange(
903
- project.startDate,
904
- project.endDate,
905
- getSettingValue,
906
- currentLocaleCode
907
- )}
908
- </dd>
909
- </div>
910
- <div>
911
- <dt className="text-muted-foreground">
912
- {commonT('labels.contractStatus')}
913
- </dt>
914
- <dd className="font-medium">
915
- {project.contractStatus ? (
916
- <StatusBadge
917
- label={getContractStatusLabel(project.contractStatus)}
918
- className={getStatusBadgeClass(project.contractStatus)}
919
- />
920
- ) : (
921
- commonT('labels.notAssigned')
922
- )}
923
- </dd>
924
- </div>
925
- </dl>
926
- {project.summary ? (
927
- <div className="mt-4 rounded-lg border border-border/70 bg-muted/30 p-3 text-sm text-muted-foreground">
928
- {project.summary}
929
- </div>
930
- ) : null}
931
- </SectionCard>
932
-
933
- <SectionCard
934
- title={t('sections.contract')}
935
- className="rounded-xl border bg-card p-4 shadow-sm xl:col-span-5"
936
- >
937
- {project.relatedContract ? (
938
- <div className="space-y-3">
939
- <div className="flex items-center justify-between rounded-lg border bg-muted/20 px-4 py-3">
940
866
  <div>
941
- <div className="font-medium">
942
- {project.relatedContract.name}
943
- </div>
944
- <div className="text-sm text-muted-foreground">
945
- {[
946
- project.relatedContract.code,
947
- project.relatedContract.clientName,
948
- ]
949
- .filter(Boolean)
950
- .join(' • ') || commonT('labels.notAvailable')}
951
- </div>
867
+ <dt className="text-muted-foreground">
868
+ {commonT('labels.code')}
869
+ </dt>
870
+ <dd className="font-medium">
871
+ {project.code || commonT('labels.notAvailable')}
872
+ </dd>
952
873
  </div>
953
- <StatusBadge
954
- label={getContractStatusLabel(project.relatedContract.status)}
955
- className={getStatusBadgeClass(
956
- project.relatedContract.status
957
- )}
958
- />
959
- </div>
960
- <dl className="grid gap-3 text-sm md:grid-cols-2">
961
874
  <div>
962
875
  <dt className="text-muted-foreground">
963
- {commonT('labels.contractCategory')}
876
+ {commonT('labels.client')}
964
877
  </dt>
965
878
  <dd className="font-medium">
966
- {project.relatedContract.contractCategory
967
- ? getContractCategoryLabel(
968
- project.relatedContract.contractCategory
969
- )
970
- : commonT('labels.notAvailable')}
879
+ <div className="flex items-center gap-2">
880
+ <Avatar className="h-8 w-8 border border-border/60 bg-muted">
881
+ <AvatarImage
882
+ src={getPersonAvatarUrl(project.clientAvatarId)}
883
+ alt={project.clientName || commonT('labels.client')}
884
+ />
885
+ <AvatarFallback className="bg-muted text-xs font-semibold text-foreground">
886
+ {getInitials(
887
+ project.clientName || commonT('labels.client')
888
+ )}
889
+ </AvatarFallback>
890
+ </Avatar>
891
+ <span>
892
+ {project.clientName || commonT('labels.notAvailable')}
893
+ </span>
894
+ </div>
971
895
  </dd>
972
896
  </div>
973
897
  <div>
974
898
  <dt className="text-muted-foreground">
975
- {commonT('labels.contractType')}
899
+ {commonT('labels.manager')}
976
900
  </dt>
977
901
  <dd className="font-medium">
978
- {project.relatedContract.contractType
979
- ? getContractTypeLabel(
980
- project.relatedContract.contractType
981
- )
982
- : commonT('labels.notAvailable')}
902
+ {project.managerName || commonT('labels.notAssigned')}
983
903
  </dd>
984
904
  </div>
985
905
  <div>
986
906
  <dt className="text-muted-foreground">
987
- {commonT('labels.billingModel')}
907
+ {commonT('labels.status')}
988
908
  </dt>
989
909
  <dd className="font-medium">
990
- {getBillingModelLabel(project.relatedContract.billingModel)}
910
+ <StatusBadge
911
+ label={getProjectStatusLabel(project.status)}
912
+ className={getStatusBadgeClass(project.status)}
913
+ />
991
914
  </dd>
992
915
  </div>
993
916
  <div>
994
917
  <dt className="text-muted-foreground">
995
- {commonT('labels.timeline')}
918
+ {commonT('labels.deliveryModel')}
996
919
  </dt>
997
920
  <dd className="font-medium">
998
- {formatDateRange(
999
- project.relatedContract.startDate,
1000
- project.relatedContract.endDate,
921
+ {project.deliveryModel
922
+ ? getDeliveryModelLabel(project.deliveryModel)
923
+ : commonT('labels.notAvailable')}
924
+ </dd>
925
+ </div>
926
+ <div>
927
+ <dt className="text-muted-foreground">
928
+ {commonT('labels.startDate')}
929
+ </dt>
930
+ <dd className="font-medium">
931
+ {formatDate(
932
+ project.startDate,
1001
933
  getSettingValue,
1002
934
  currentLocaleCode
1003
935
  )}
@@ -1005,14 +937,14 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
1005
937
  </div>
1006
938
  <div>
1007
939
  <dt className="text-muted-foreground">
1008
- {commonT('labels.signatureStatus')}
940
+ {commonT('labels.endDate')}
1009
941
  </dt>
1010
942
  <dd className="font-medium">
1011
- {project.relatedContract.signatureStatus
1012
- ? getSignatureStatusLabel(
1013
- project.relatedContract.signatureStatus
1014
- )
1015
- : commonT('labels.notAvailable')}
943
+ {formatDate(
944
+ project.endDate,
945
+ getSettingValue,
946
+ currentLocaleCode
947
+ )}
1016
948
  </dd>
1017
949
  </div>
1018
950
  <div>
@@ -1020,148 +952,309 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
1020
952
  {commonT('labels.budget')}
1021
953
  </dt>
1022
954
  <dd className="font-medium">
1023
- {project.relatedContract.budgetAmount
955
+ {project.budgetAmount
1024
956
  ? formatCurrency(
1025
- project.relatedContract.budgetAmount,
957
+ project.budgetAmount,
1026
958
  getSettingValue,
1027
959
  currentLocaleCode
1028
960
  )
1029
961
  : commonT('labels.notAvailable')}
1030
962
  </dd>
1031
963
  </div>
964
+ <div>
965
+ <dt className="text-muted-foreground">
966
+ {commonT('labels.progress')}
967
+ </dt>
968
+ <dd className="font-medium">
969
+ {formatPercent(project.progressPercent)}
970
+ </dd>
971
+ </div>
972
+ <div>
973
+ <dt className="text-muted-foreground">
974
+ {commonT('labels.timeline')}
975
+ </dt>
976
+ <dd className="font-medium">
977
+ {formatDateRange(
978
+ project.startDate,
979
+ project.endDate,
980
+ getSettingValue,
981
+ currentLocaleCode
982
+ )}
983
+ </dd>
984
+ </div>
985
+ <div>
986
+ <dt className="text-muted-foreground">
987
+ {commonT('labels.contractStatus')}
988
+ </dt>
989
+ <dd className="font-medium">
990
+ {project.contractStatus ? (
991
+ <StatusBadge
992
+ label={getContractStatusLabel(project.contractStatus)}
993
+ className={getStatusBadgeClass(project.contractStatus)}
994
+ />
995
+ ) : (
996
+ commonT('labels.notAssigned')
997
+ )}
998
+ </dd>
999
+ </div>
1032
1000
  </dl>
1001
+ {project.summary ? (
1002
+ <div className="mt-4 rounded-lg border border-border/70 bg-muted/30 p-3 text-sm text-muted-foreground">
1003
+ {project.summary}
1004
+ </div>
1005
+ ) : null}
1006
+ </SectionCard>
1033
1007
 
1034
- <Button variant="outline" size="sm" asChild className="w-fit">
1035
- <Link
1036
- href={`/operations/contracts?edit=${project.relatedContract.id}`}
1037
- >
1038
- <FileText className="size-4" />
1039
- {commonT('actions.openContract')}
1040
- </Link>
1041
- </Button>
1042
- </div>
1043
- ) : (
1044
- <p className="text-sm text-muted-foreground">{t('noContract')}</p>
1045
- )}
1046
- </SectionCard>
1047
- </div>
1008
+ <SectionCard
1009
+ title={t('sections.contract')}
1010
+ className="rounded-xl border bg-card p-4 shadow-sm xl:col-span-5"
1011
+ >
1012
+ {project.relatedContract ? (
1013
+ <div className="space-y-3">
1014
+ <div className="flex items-center justify-between rounded-lg border bg-muted/20 px-4 py-3">
1015
+ <div>
1016
+ <div className="font-medium">
1017
+ {project.relatedContract.name}
1018
+ </div>
1019
+ <div className="text-sm text-muted-foreground">
1020
+ {[
1021
+ project.relatedContract.code,
1022
+ project.relatedContract.clientName,
1023
+ ]
1024
+ .filter(Boolean)
1025
+ .join(' • ') || commonT('labels.notAvailable')}
1026
+ </div>
1027
+ </div>
1028
+ <StatusBadge
1029
+ label={getContractStatusLabel(
1030
+ project.relatedContract.status
1031
+ )}
1032
+ className={getStatusBadgeClass(
1033
+ project.relatedContract.status
1034
+ )}
1035
+ />
1036
+ </div>
1037
+ <dl className="grid gap-3 text-sm md:grid-cols-2">
1038
+ <div>
1039
+ <dt className="text-muted-foreground">
1040
+ {commonT('labels.contractCategory')}
1041
+ </dt>
1042
+ <dd className="font-medium">
1043
+ {project.relatedContract.contractCategory
1044
+ ? getContractCategoryLabel(
1045
+ project.relatedContract.contractCategory
1046
+ )
1047
+ : commonT('labels.notAvailable')}
1048
+ </dd>
1049
+ </div>
1050
+ <div>
1051
+ <dt className="text-muted-foreground">
1052
+ {commonT('labels.contractType')}
1053
+ </dt>
1054
+ <dd className="font-medium">
1055
+ {project.relatedContract.contractType
1056
+ ? getContractTypeLabel(
1057
+ project.relatedContract.contractType
1058
+ )
1059
+ : commonT('labels.notAvailable')}
1060
+ </dd>
1061
+ </div>
1062
+ <div>
1063
+ <dt className="text-muted-foreground">
1064
+ {commonT('labels.billingModel')}
1065
+ </dt>
1066
+ <dd className="font-medium">
1067
+ {getBillingModelLabel(
1068
+ project.relatedContract.billingModel
1069
+ )}
1070
+ </dd>
1071
+ </div>
1072
+ <div>
1073
+ <dt className="text-muted-foreground">
1074
+ {commonT('labels.timeline')}
1075
+ </dt>
1076
+ <dd className="font-medium">
1077
+ {formatDateRange(
1078
+ project.relatedContract.startDate,
1079
+ project.relatedContract.endDate,
1080
+ getSettingValue,
1081
+ currentLocaleCode
1082
+ )}
1083
+ </dd>
1084
+ </div>
1085
+ <div>
1086
+ <dt className="text-muted-foreground">
1087
+ {commonT('labels.signatureStatus')}
1088
+ </dt>
1089
+ <dd className="font-medium">
1090
+ {project.relatedContract.signatureStatus
1091
+ ? getSignatureStatusLabel(
1092
+ project.relatedContract.signatureStatus
1093
+ )
1094
+ : commonT('labels.notAvailable')}
1095
+ </dd>
1096
+ </div>
1097
+ <div>
1098
+ <dt className="text-muted-foreground">
1099
+ {commonT('labels.budget')}
1100
+ </dt>
1101
+ <dd className="font-medium">
1102
+ {project.relatedContract.budgetAmount
1103
+ ? formatCurrency(
1104
+ project.relatedContract.budgetAmount,
1105
+ getSettingValue,
1106
+ currentLocaleCode
1107
+ )
1108
+ : commonT('labels.notAvailable')}
1109
+ </dd>
1110
+ </div>
1111
+ </dl>
1112
+
1113
+ <Button variant="outline" size="sm" asChild className="w-fit">
1114
+ <Link
1115
+ href={`/operations/contracts?edit=${project.relatedContract.id}`}
1116
+ >
1117
+ <FileText className="size-4" />
1118
+ {commonT('actions.openContract')}
1119
+ </Link>
1120
+ </Button>
1121
+ </div>
1122
+ ) : (
1123
+ <p className="text-sm text-muted-foreground">
1124
+ {t('noContract')}
1125
+ </p>
1126
+ )}
1127
+ </SectionCard>
1128
+ </div>
1048
1129
 
1049
- <div className="grid gap-4 xl:grid-cols-12">
1050
- <SectionCard
1051
- title={t('sections.deliveryHealth')}
1052
- description={t('sections.deliveryHealthDescription')}
1053
- className="rounded-xl border bg-card p-4 shadow-sm xl:col-span-7"
1054
- >
1055
- <div className="grid gap-4 lg:grid-cols-2">
1056
- <div className="rounded-lg border bg-muted/10 p-3">
1057
- <div className="mb-2 flex items-center gap-2 text-sm font-medium">
1058
- <BarChart3 className="size-4 text-sky-700" />
1059
- {t('charts.allocationByCollaborator')}
1060
- </div>
1061
- <ChartContainer className="h-60 w-full" config={boardChartConfig}>
1062
- <BarChart data={allocationChartData}>
1063
- <CartesianGrid vertical={false} />
1064
- <XAxis dataKey="name" tickLine={false} axisLine={false} />
1065
- <YAxis tickLine={false} axisLine={false} width={28} />
1066
- <ChartTooltip content={<ChartTooltipContent hideLabel />} />
1067
- <Bar
1068
- dataKey="allocation"
1069
- radius={6}
1070
- fill="var(--color-allocation)"
1071
- />
1072
- </BarChart>
1073
- </ChartContainer>
1074
- </div>
1130
+ <div className="grid gap-4 xl:grid-cols-12">
1131
+ <SectionCard
1132
+ title={t('sections.deliveryHealth')}
1133
+ description={t('sections.deliveryHealthDescription')}
1134
+ className="rounded-xl border bg-card p-4 shadow-sm xl:col-span-7"
1135
+ >
1136
+ <div className="grid gap-4 lg:grid-cols-2">
1137
+ <div className="rounded-lg border bg-muted/10 p-3">
1138
+ <div className="mb-2 flex items-center gap-2 text-sm font-medium">
1139
+ <BarChart3 className="size-4 text-sky-700" />
1140
+ {t('charts.allocationByCollaborator')}
1141
+ </div>
1142
+ <ChartContainer
1143
+ className="h-60 w-full"
1144
+ config={boardChartConfig}
1145
+ >
1146
+ <BarChart data={allocationChartData}>
1147
+ <CartesianGrid vertical={false} />
1148
+ <XAxis dataKey="name" tickLine={false} axisLine={false} />
1149
+ <YAxis tickLine={false} axisLine={false} width={28} />
1150
+ <ChartTooltip
1151
+ content={<ChartTooltipContent hideLabel />}
1152
+ />
1153
+ <Bar
1154
+ dataKey="allocation"
1155
+ radius={6}
1156
+ fill="var(--color-allocation)"
1157
+ />
1158
+ </BarChart>
1159
+ </ChartContainer>
1160
+ </div>
1075
1161
 
1076
- <div className="rounded-lg border bg-muted/10 p-3">
1077
- <div className="mb-2 flex items-center gap-2 text-sm font-medium">
1078
- <Rocket className="size-4 text-emerald-700" />
1079
- {t('charts.weeklyVelocity')}
1162
+ <div className="rounded-lg border bg-muted/10 p-3">
1163
+ <div className="mb-2 flex items-center gap-2 text-sm font-medium">
1164
+ <Rocket className="size-4 text-emerald-700" />
1165
+ {t('charts.weeklyVelocity')}
1166
+ </div>
1167
+ <ChartContainer
1168
+ className="h-60 w-full"
1169
+ config={boardChartConfig}
1170
+ >
1171
+ <LineChart data={velocityChartData}>
1172
+ <CartesianGrid vertical={false} />
1173
+ <XAxis dataKey="week" tickLine={false} axisLine={false} />
1174
+ <YAxis tickLine={false} axisLine={false} width={28} />
1175
+ <ChartTooltip content={<ChartTooltipContent />} />
1176
+ <Line
1177
+ type="monotone"
1178
+ dataKey="loggedHours"
1179
+ stroke="var(--color-loggedHours)"
1180
+ strokeWidth={2.5}
1181
+ dot={{ r: 3 }}
1182
+ />
1183
+ <Line
1184
+ type="monotone"
1185
+ dataKey="completedTasks"
1186
+ stroke="var(--color-allocation)"
1187
+ strokeWidth={2}
1188
+ dot={{ r: 3 }}
1189
+ />
1190
+ </LineChart>
1191
+ </ChartContainer>
1192
+ </div>
1080
1193
  </div>
1081
- <ChartContainer className="h-60 w-full" config={boardChartConfig}>
1082
- <LineChart data={velocityChartData}>
1083
- <CartesianGrid vertical={false} />
1084
- <XAxis dataKey="week" tickLine={false} axisLine={false} />
1085
- <YAxis tickLine={false} axisLine={false} width={28} />
1086
- <ChartTooltip content={<ChartTooltipContent />} />
1087
- <Line
1088
- type="monotone"
1089
- dataKey="loggedHours"
1090
- stroke="var(--color-loggedHours)"
1091
- strokeWidth={2.5}
1092
- dot={{ r: 3 }}
1093
- />
1094
- <Line
1095
- type="monotone"
1096
- dataKey="completedTasks"
1097
- stroke="var(--color-allocation)"
1098
- strokeWidth={2}
1099
- dot={{ r: 3 }}
1100
- />
1101
- </LineChart>
1102
- </ChartContainer>
1103
- </div>
1104
- </div>
1105
- </SectionCard>
1194
+ </SectionCard>
1106
1195
 
1107
- <SectionCard
1108
- title={t('sections.quickRadar')}
1109
- description={t('sections.quickRadarDescription')}
1110
- className="rounded-xl border bg-card p-4 shadow-sm xl:col-span-5"
1111
- >
1112
- <div className="space-y-3">
1113
- <div className="rounded-lg border bg-emerald-50/50 p-3">
1114
- <div className="flex items-center justify-between text-sm">
1115
- <span className="text-muted-foreground">
1116
- {t('quickRadar.activeAssignments')}
1117
- </span>
1118
- <span className="font-semibold text-emerald-700">
1119
- {projectStats?.quickRadar?.activeAssignments ??
1120
- project.operationalIndicators.activeAssignments}
1121
- </span>
1122
- </div>
1123
- </div>
1124
- <div className="rounded-lg border bg-amber-50/50 p-3">
1125
- <div className="flex items-center justify-between text-sm">
1126
- <span className="text-muted-foreground">
1127
- {t('quickRadar.timesheetPendencies')}
1128
- </span>
1129
- <span className="font-semibold text-amber-700">
1130
- {projectStats?.quickRadar?.pendingTimesheets ??
1131
- project.timesheetSummary.pendingTimesheets}
1132
- </span>
1133
- </div>
1134
- </div>
1135
- <div className="rounded-lg border bg-sky-50/50 p-3">
1136
- <div className="flex items-center justify-between text-sm">
1137
- <span className="text-muted-foreground">
1138
- {t('quickRadar.plannedWeeklyHours')}
1139
- </span>
1140
- <span className="font-semibold text-sky-700">
1141
- {formatHours(
1142
- projectStats?.quickRadar?.totalWeeklyHours ??
1143
- project.operationalIndicators.totalWeeklyHours
1144
- )}
1145
- </span>
1196
+ <SectionCard
1197
+ title={t('sections.quickRadar')}
1198
+ description={t('sections.quickRadarDescription')}
1199
+ className="rounded-xl border bg-card p-4 shadow-sm xl:col-span-5"
1200
+ >
1201
+ <div className="space-y-3">
1202
+ <div className="rounded-lg border bg-emerald-50/50 p-3">
1203
+ <div className="flex items-center justify-between text-sm">
1204
+ <span className="text-muted-foreground">
1205
+ {t('quickRadar.activeAssignments')}
1206
+ </span>
1207
+ <span className="font-semibold text-emerald-700">
1208
+ {projectStats?.quickRadar?.activeAssignments ??
1209
+ project.operationalIndicators.activeAssignments}
1210
+ </span>
1211
+ </div>
1212
+ </div>
1213
+ <div className="rounded-lg border bg-amber-50/50 p-3">
1214
+ <div className="flex items-center justify-between text-sm">
1215
+ <span className="text-muted-foreground">
1216
+ {t('quickRadar.timesheetPendencies')}
1217
+ </span>
1218
+ <span className="font-semibold text-amber-700">
1219
+ {projectStats?.quickRadar?.pendingTimesheets ??
1220
+ project.timesheetSummary.pendingTimesheets}
1221
+ </span>
1222
+ </div>
1223
+ </div>
1224
+ <div className="rounded-lg border bg-sky-50/50 p-3">
1225
+ <div className="flex items-center justify-between text-sm">
1226
+ <span className="text-muted-foreground">
1227
+ {t('quickRadar.plannedWeeklyHours')}
1228
+ </span>
1229
+ <span className="font-semibold text-sky-700">
1230
+ {formatHours(
1231
+ projectStats?.quickRadar?.totalWeeklyHours ??
1232
+ project.operationalIndicators.totalWeeklyHours
1233
+ )}
1234
+ </span>
1235
+ </div>
1236
+ </div>
1146
1237
  </div>
1147
- </div>
1238
+ </SectionCard>
1148
1239
  </div>
1149
- </SectionCard>
1150
- </div>
1240
+ </>
1241
+ ) : null}
1151
1242
 
1152
1243
  <SectionCard
1153
1244
  title={t('sections.taskBoard')}
1154
1245
  description={t('sections.taskBoardDescription')}
1155
1246
  className="rounded-xl border bg-card p-4 shadow-sm"
1156
1247
  actions={
1157
- <Button
1158
- size="sm"
1159
- variant="outline"
1160
- onClick={() => openCreateTaskForm()}
1161
- >
1162
- <Plus className="size-4" />
1163
- {t('taskForm.titleNew')}
1164
- </Button>
1248
+ !isLimitedView ? (
1249
+ <Button
1250
+ size="sm"
1251
+ variant="outline"
1252
+ onClick={() => openCreateTaskForm()}
1253
+ >
1254
+ <Plus className="size-4" />
1255
+ {t('taskForm.titleNew')}
1256
+ </Button>
1257
+ ) : undefined
1165
1258
  }
1166
1259
  >
1167
1260
  <DndContext
@@ -1193,34 +1286,65 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
1193
1286
 
1194
1287
  <div className="space-y-2">
1195
1288
  {taskColumns[column.id].map((task) => (
1196
- <DraggableTaskCard key={task.id} task={task}>
1289
+ <DraggableTaskCard
1290
+ key={task.id}
1291
+ task={task}
1292
+ disabled={false}
1293
+ >
1197
1294
  {(isDragging) => (
1198
- <button
1199
- type="button"
1295
+ <div
1296
+ role="button"
1297
+ tabIndex={0}
1200
1298
  className={[
1201
1299
  'w-full cursor-pointer rounded-lg border bg-card p-3 text-left shadow-xs transition',
1202
1300
  isDragging
1203
1301
  ? 'border-primary/50 opacity-75'
1204
1302
  : 'hover:border-primary/40 hover:shadow-sm',
1205
1303
  ].join(' ')}
1206
- onClick={() => setSelectedTaskId(task.id)}
1304
+ onClick={() => setSelectedTask(task)}
1305
+ onKeyDown={(event) => {
1306
+ if (
1307
+ event.key === 'Enter' ||
1308
+ event.key === ' '
1309
+ ) {
1310
+ event.preventDefault();
1311
+ setSelectedTask(task);
1312
+ }
1313
+ }}
1207
1314
  >
1208
1315
  <div className="mb-2 flex items-start justify-between gap-2">
1209
1316
  <p className="text-sm font-medium leading-snug">
1210
1317
  {task.name}
1211
1318
  </p>
1212
- <span
1213
- className={[
1214
- 'shrink-0 rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide',
1215
- task.priority === 'high'
1216
- ? 'bg-rose-100 text-rose-700'
1217
- : task.priority === 'medium'
1218
- ? 'bg-amber-100 text-amber-700'
1219
- : 'bg-emerald-100 text-emerald-700',
1220
- ].join(' ')}
1221
- >
1222
- {getTaskPriorityLabel(task.priority)}
1223
- </span>
1319
+ <div className="flex items-start gap-2">
1320
+ <span
1321
+ className={[
1322
+ 'shrink-0 rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide',
1323
+ task.priority === 'high'
1324
+ ? 'bg-rose-100 text-rose-700'
1325
+ : task.priority === 'medium'
1326
+ ? 'bg-amber-100 text-amber-700'
1327
+ : 'bg-emerald-100 text-emerald-700',
1328
+ ].join(' ')}
1329
+ >
1330
+ {getTaskPriorityLabel(task.priority)}
1331
+ </span>
1332
+ <Button
1333
+ type="button"
1334
+ variant="ghost"
1335
+ size="icon"
1336
+ className="size-7 shrink-0 rounded-full"
1337
+ onPointerDown={(event) =>
1338
+ event.stopPropagation()
1339
+ }
1340
+ onClick={(event) => {
1341
+ event.stopPropagation();
1342
+ openEditTaskForm(task);
1343
+ }}
1344
+ >
1345
+ <Pencil className="size-3.5" />
1346
+ </Button>
1347
+ </div>
1224
1348
  </div>
1225
1349
 
1226
1350
  {task.tags ? (
@@ -1283,7 +1407,7 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
1283
1407
  </span>
1284
1408
  </div>
1285
1409
  ) : null}
1286
- </button>
1410
+ </div>
1287
1411
  )}
1288
1412
  </DraggableTaskCard>
1289
1413
  ))}
@@ -1296,11 +1420,106 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
1296
1420
  </DndContext>
1297
1421
  </SectionCard>
1298
1422
 
1423
+ <SectionCard
1424
+ title={t('sections.archivedTasks')}
1425
+ description={t('sections.archivedTasksDescription')}
1426
+ className="rounded-xl border bg-card p-4 shadow-sm"
1427
+ >
1428
+ {archivedTasks.length > 0 ? (
1429
+ <div className="overflow-x-auto rounded-lg border bg-muted/10">
1430
+ <Table>
1431
+ <TableHeader>
1432
+ <TableRow>
1433
+ <TableHead>{commonT('labels.task')}</TableHead>
1434
+ <TableHead>{commonT('labels.status')}</TableHead>
1435
+ <TableHead>{t('taskForm.deadlineLabel')}</TableHead>
1436
+ <TableHead className="text-right">
1437
+ {commonT('labels.actions')}
1438
+ </TableHead>
1439
+ </TableRow>
1440
+ </TableHeader>
1441
+ <TableBody>
1442
+ {archivedTasks.map((task) => (
1443
+ <TableRow
1444
+ key={task.id}
1445
+ className="cursor-pointer hover:bg-muted/30"
1446
+ onClick={() => setSelectedTask(task)}
1447
+ >
1448
+ <TableCell>
1449
+ <div className="min-w-0">
1450
+ <div className="truncate font-medium">{task.name}</div>
1451
+ {task.description ? (
1452
+ <div className="truncate text-xs text-muted-foreground">
1453
+ {task.description}
1454
+ </div>
1455
+ ) : null}
1456
+ </div>
1457
+ </TableCell>
1458
+ <TableCell>
1459
+ <StatusBadge
1460
+ label={
1461
+ KANBAN_COLUMNS.find(
1462
+ (column) => column.id === task.status
1463
+ )?.label ?? formatEnumLabel(task.status)
1464
+ }
1465
+ className={getStatusBadgeClass(task.status)}
1466
+ />
1467
+ </TableCell>
1468
+ <TableCell>
1469
+ {formatDate(
1470
+ task.dueDate,
1471
+ getSettingValue,
1472
+ currentLocaleCode
1473
+ )}
1474
+ </TableCell>
1475
+ <TableCell>
1476
+ <div className="flex justify-end gap-2">
1477
+ <Button
1478
+ variant="outline"
1479
+ size="sm"
1480
+ className="gap-2"
1481
+ onClick={(event) => {
1482
+ event.stopPropagation();
1483
+ void handleRestoreTask(task.id);
1484
+ }}
1485
+ >
1486
+ <ArchiveRestore className="size-4" />
1487
+ {commonT('actions.unarchive')}
1488
+ </Button>
1489
+ <Button
1490
+ variant="destructive"
1491
+ size="sm"
1492
+ className="gap-2"
1493
+ onClick={(event) => {
1494
+ event.stopPropagation();
1495
+ setDeletePromptTask(task);
1496
+ }}
1497
+ >
1498
+ <Trash2 className="size-4" />
1499
+ {commonT('actions.delete')}
1500
+ </Button>
1501
+ </div>
1502
+ </TableCell>
1503
+ </TableRow>
1504
+ ))}
1505
+ </TableBody>
1506
+ </Table>
1507
+ </div>
1508
+ ) : (
1509
+ <p className="text-sm text-muted-foreground">
1510
+ {t('emptyArchivedDescription')}
1511
+ </p>
1512
+ )}
1513
+ </SectionCard>
1514
+
1299
1515
  <div className="grid gap-4 xl:grid-cols-12">
1300
1516
  <SectionCard
1301
1517
  title={t('sections.team')}
1302
1518
  description={t('sections.teamDescription')}
1303
- className="rounded-xl border bg-card p-4 shadow-sm xl:col-span-8"
1519
+ className={[
1520
+ 'rounded-xl border bg-card p-4 shadow-sm',
1521
+ isLimitedView ? 'xl:col-span-12' : 'xl:col-span-8',
1522
+ ].join(' ')}
1304
1523
  >
1305
1524
  {project.assignments.length > 0 ? (
1306
1525
  <div className="overflow-x-auto rounded-lg border bg-muted/10">
@@ -1376,190 +1595,118 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
1376
1595
  )}
1377
1596
  </SectionCard>
1378
1597
 
1379
- <SectionCard
1380
- title={t('sections.indicators')}
1381
- description={t('sections.indicatorsDescription')}
1382
- className="rounded-xl border bg-card p-4 shadow-sm xl:col-span-4"
1383
- >
1384
- <dl className="grid gap-3 text-sm sm:grid-cols-2 xl:grid-cols-1">
1385
- <div>
1386
- <dt className="text-muted-foreground">
1387
- {t('indicators.activeAssignments')}
1388
- </dt>
1389
- <dd className="font-medium">
1390
- {project.operationalIndicators.activeAssignments}
1391
- </dd>
1392
- </div>
1393
- <div>
1394
- <dt className="text-muted-foreground">
1395
- {t('indicators.completedAssignments')}
1396
- </dt>
1397
- <dd className="font-medium">
1398
- {project.operationalIndicators.completedAssignments}
1399
- </dd>
1400
- </div>
1401
- <div>
1402
- <dt className="text-muted-foreground">
1403
- {t('indicators.averageAllocation')}
1404
- </dt>
1405
- <dd className="font-medium">
1406
- {formatPercent(project.operationalIndicators.averageAllocation)}
1407
- </dd>
1408
- </div>
1409
- <div>
1410
- <dt className="text-muted-foreground">
1411
- {t('indicators.totalWeeklyHours')}
1412
- </dt>
1413
- <dd className="font-medium">
1414
- {formatHours(project.operationalIndicators.totalWeeklyHours)}
1415
- </dd>
1416
- </div>
1417
- <div>
1418
- <dt className="text-muted-foreground">{t('cards.timesheets')}</dt>
1419
- <dd className="font-medium">
1420
- {project.timesheetSummary.totalTimesheets}
1421
- </dd>
1422
- </div>
1423
- <div>
1424
- <dt className="text-muted-foreground">
1425
- {commonT('labels.pending')}
1426
- </dt>
1427
- <dd className="font-medium">
1428
- {project.timesheetSummary.pendingTimesheets}
1429
- </dd>
1430
- </div>
1431
- <div>
1432
- <dt className="text-muted-foreground">
1433
- {t('cards.loggedHours')}
1434
- </dt>
1435
- <dd className="font-medium">
1436
- {formatHours(project.timesheetSummary.totalHours)}
1437
- </dd>
1438
- </div>
1439
- </dl>
1440
- </SectionCard>
1598
+ {!isLimitedView ? (
1599
+ <SectionCard
1600
+ title={t('sections.indicators')}
1601
+ description={t('sections.indicatorsDescription')}
1602
+ className="rounded-xl border bg-card p-4 shadow-sm xl:col-span-4"
1603
+ >
1604
+ <dl className="grid gap-3 text-sm sm:grid-cols-2 xl:grid-cols-1">
1605
+ <div>
1606
+ <dt className="text-muted-foreground">
1607
+ {t('indicators.activeAssignments')}
1608
+ </dt>
1609
+ <dd className="font-medium">
1610
+ {project.operationalIndicators.activeAssignments}
1611
+ </dd>
1612
+ </div>
1613
+ <div>
1614
+ <dt className="text-muted-foreground">
1615
+ {t('indicators.completedAssignments')}
1616
+ </dt>
1617
+ <dd className="font-medium">
1618
+ {project.operationalIndicators.completedAssignments}
1619
+ </dd>
1620
+ </div>
1621
+ <div>
1622
+ <dt className="text-muted-foreground">
1623
+ {t('indicators.averageAllocation')}
1624
+ </dt>
1625
+ <dd className="font-medium">
1626
+ {formatPercent(
1627
+ project.operationalIndicators.averageAllocation
1628
+ )}
1629
+ </dd>
1630
+ </div>
1631
+ <div>
1632
+ <dt className="text-muted-foreground">
1633
+ {t('indicators.totalWeeklyHours')}
1634
+ </dt>
1635
+ <dd className="font-medium">
1636
+ {formatHours(project.operationalIndicators.totalWeeklyHours)}
1637
+ </dd>
1638
+ </div>
1639
+ <div>
1640
+ <dt className="text-muted-foreground">
1641
+ {t('cards.timesheets')}
1642
+ </dt>
1643
+ <dd className="font-medium">
1644
+ {project.timesheetSummary.totalTimesheets}
1645
+ </dd>
1646
+ </div>
1647
+ <div>
1648
+ <dt className="text-muted-foreground">
1649
+ {commonT('labels.pending')}
1650
+ </dt>
1651
+ <dd className="font-medium">
1652
+ {project.timesheetSummary.pendingTimesheets}
1653
+ </dd>
1654
+ </div>
1655
+ <div>
1656
+ <dt className="text-muted-foreground">
1657
+ {t('cards.loggedHours')}
1658
+ </dt>
1659
+ <dd className="font-medium">
1660
+ {formatHours(project.timesheetSummary.totalHours)}
1661
+ </dd>
1662
+ </div>
1663
+ </dl>
1664
+ </SectionCard>
1665
+ ) : null}
1441
1666
  </div>
1442
1667
 
1443
- <Sheet
1444
- open={Boolean(selectedTask)}
1668
+ <TaskDetailSheet
1669
+ task={selectedTask}
1670
+ open={selectedTask !== null}
1445
1671
  onOpenChange={(open) => {
1446
1672
  if (!open) {
1447
- setSelectedTaskId(null);
1673
+ setSelectedTask(null);
1448
1674
  }
1449
1675
  }}
1450
- >
1451
- <SheetContent className="w-full overflow-x-hidden overflow-y-auto sm:max-w-lg">
1452
- {selectedTask ? (
1453
- <>
1454
- <SheetHeader>
1455
- <SheetTitle>{selectedTask.name}</SheetTitle>
1456
- <SheetDescription>
1457
- Detalhes da tarefa e contexto de execução.
1458
- </SheetDescription>
1459
- </SheetHeader>
1460
-
1461
- <div className="mt-4 space-y-4">
1462
- {selectedTask.description ? (
1463
- <div className="rounded-lg border bg-muted/10 p-3 text-sm text-muted-foreground">
1464
- {selectedTask.description}
1465
- </div>
1466
- ) : null}
1467
-
1468
- <div className="grid grid-cols-2 gap-3 text-sm">
1469
- <div className="rounded-lg border p-3">
1470
- <div className="mb-1 text-xs text-muted-foreground">
1471
- Prioridade
1472
- </div>
1473
- <div className="font-medium">
1474
- {getTaskPriorityLabel(selectedTask.priority)}
1475
- </div>
1476
- </div>
1477
- <div className="rounded-lg border p-3">
1478
- <div className="mb-1 text-xs text-muted-foreground">
1479
- Estimativa
1480
- </div>
1481
- <div className="font-medium">
1482
- {selectedTask.estimateHours != null
1483
- ? `${selectedTask.estimateHours} horas`
1484
- : '—'}
1485
- </div>
1486
- </div>
1487
- <div className="rounded-lg border p-3">
1488
- <div className="mb-1 text-xs text-muted-foreground">
1489
- Prazo
1490
- </div>
1491
- <div className="font-medium">
1492
- {formatDate(
1493
- selectedTask.dueDate,
1494
- getSettingValue,
1495
- currentLocaleCode
1496
- )}
1497
- </div>
1498
- </div>
1499
- <div className="rounded-lg border p-3">
1500
- <div className="mb-1 text-xs text-muted-foreground">
1501
- Status
1502
- </div>
1503
- <div className="font-medium">
1504
- {
1505
- KANBAN_COLUMNS.find(
1506
- (column) => column.id === selectedTask.status
1507
- )?.label
1508
- }
1509
- </div>
1510
- </div>
1511
- </div>
1512
-
1513
- <div className="rounded-lg border p-3">
1514
- <div className="mb-2 text-xs text-muted-foreground">
1515
- Responsavel
1516
- </div>
1517
- <div className="flex items-center gap-2 text-sm">
1518
- <Avatar className="h-8 w-8 border border-border/60 bg-muted">
1519
- <AvatarImage
1520
- src={
1521
- getUserPhotoUrl(selectedTask.assigneeUserPhotoId) ||
1522
- getPersonAvatarUrl(
1523
- selectedTask.assigneePersonAvatarId
1524
- )
1525
- }
1526
- alt={selectedTask.assigneeName || 'Responsavel'}
1527
- />
1528
- <AvatarFallback className="bg-muted text-xs font-semibold text-foreground">
1529
- {getInitials(selectedTask.assigneeName || 'N/A')}
1530
- </AvatarFallback>
1531
- </Avatar>
1532
- <span>
1533
- {selectedTask.assigneeName ||
1534
- commonT('labels.notAssigned')}
1535
- </span>
1536
- </div>
1537
- </div>
1538
-
1539
- {selectedTask.tags ? (
1540
- <div className="rounded-lg border p-3">
1541
- <div className="mb-2 text-xs text-muted-foreground">
1542
- Etiquetas
1543
- </div>
1544
- <div className="flex flex-wrap gap-1.5">
1545
- {selectedTask.tags.split(',').map((tag) => (
1546
- <span
1547
- key={`${selectedTask.id}-sheet-${tag.trim()}`}
1548
- className="rounded-md bg-muted px-2 py-1 text-xs"
1549
- >
1550
- {tag.trim()}
1551
- </span>
1552
- ))}
1553
- </div>
1554
- </div>
1555
- ) : null}
1556
-
1557
- <div className="grid grid-cols-2 gap-3 border-t pt-5">
1676
+ statusLabel={(status) =>
1677
+ KANBAN_COLUMNS.find((column) => column.id === status)?.label ?? status
1678
+ }
1679
+ footer={
1680
+ selectedTask && !isLimitedView ? (
1681
+ <div className="grid grid-cols-2 gap-3">
1682
+ {archivedTasks.some((task) => task.id === selectedTask.id) ? (
1683
+ <>
1558
1684
  <Button
1559
1685
  variant="outline"
1560
1686
  size="sm"
1561
1687
  className="h-10 gap-2"
1562
- onClick={() => openEditTaskForm(selectedTask)}
1688
+ onClick={() => void handleRestoreTask(selectedTask.id)}
1689
+ >
1690
+ <ArchiveRestore className="size-3.5" />
1691
+ {commonT('actions.unarchive')}
1692
+ </Button>
1693
+ <Button
1694
+ variant="destructive"
1695
+ size="sm"
1696
+ className="h-10 gap-2"
1697
+ onClick={() => setDeletePromptTask(selectedTask)}
1698
+ >
1699
+ <Trash2 className="size-3.5" />
1700
+ {commonT('actions.delete')}
1701
+ </Button>
1702
+ </>
1703
+ ) : (
1704
+ <>
1705
+ <Button
1706
+ variant="outline"
1707
+ size="sm"
1708
+ className="h-10 gap-2"
1709
+ onClick={() => openEditTaskForm(selectedTask as BoardTask)}
1563
1710
  >
1564
1711
  <Pencil className="size-3.5" />
1565
1712
  {commonT('actions.edit')}
@@ -1567,284 +1714,298 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
1567
1714
  <Button
1568
1715
  variant="outline"
1569
1716
  size="sm"
1570
- className="h-10 gap-2 text-destructive hover:bg-destructive/10"
1571
- onClick={() => setDeleteConfirmId(selectedTask.id)}
1717
+ className="h-10 gap-2"
1718
+ onClick={() => void handleArchiveTask(selectedTask.id)}
1572
1719
  >
1573
- <Trash2 className="size-3.5" />
1574
- {commonT('actions.delete')}
1720
+ <Archive className="size-3.5" />
1721
+ {commonT('actions.archive')}
1575
1722
  </Button>
1576
- </div>
1577
- </div>
1578
- </>
1579
- ) : null}
1580
- </SheetContent>
1581
- </Sheet>
1582
-
1583
- <Sheet
1584
- open={isEditSheetOpen}
1585
- onOpenChange={(open) => {
1586
- if (!open) {
1587
- closeEditSheet();
1588
- }
1589
- }}
1590
- >
1591
- <SheetContent className="w-full overflow-x-hidden overflow-y-auto sm:max-w-[min(92vw,64rem)]">
1592
- <SheetHeader>
1593
- <SheetTitle>{formT('editTitle')}</SheetTitle>
1594
- <SheetDescription>{formT('description')}</SheetDescription>
1595
- </SheetHeader>
1596
-
1597
- <ProjectFormScreen
1598
- projectId={projectId}
1599
- onCancel={closeEditSheet}
1600
- onSaved={async () => {
1601
- closeEditSheet();
1602
- await refetch();
1603
- }}
1604
- />
1605
- </SheetContent>
1606
- </Sheet>
1607
-
1608
- {/* Task form dialog */}
1609
- <Dialog
1610
- open={taskFormOpen}
1611
- onOpenChange={(open) => {
1612
- if (!open) {
1613
- setTaskFormOpen(false);
1614
- setEditingTaskId(null);
1615
- setTaskFormData(EMPTY_TASK_FORM);
1616
- }
1617
- }}
1618
- >
1619
- <DialogContent className="sm:max-w-lg">
1620
- <DialogHeader>
1621
- <DialogTitle>
1622
- {editingTaskId ? t('taskForm.titleEdit') : t('taskForm.titleNew')}
1623
- </DialogTitle>
1624
- </DialogHeader>
1625
-
1626
- <div className="space-y-4">
1627
- <div className="space-y-1.5">
1628
- <Label htmlFor="task-name">{t('taskForm.nameLabel')} *</Label>
1629
- <Input
1630
- id="task-name"
1631
- placeholder={t('taskForm.namePlaceholder')}
1632
- value={taskFormData.name}
1633
- onChange={(e) =>
1634
- setTaskFormData((prev) => ({ ...prev, name: e.target.value }))
1635
- }
1636
- />
1723
+ </>
1724
+ )}
1637
1725
  </div>
1726
+ ) : null
1727
+ }
1728
+ />
1638
1729
 
1639
- <div className="space-y-1.5">
1640
- <Label htmlFor="task-description">
1641
- {t('taskForm.descriptionLabel')}
1642
- </Label>
1643
- <Textarea
1644
- id="task-description"
1645
- placeholder={t('taskForm.descriptionPlaceholder')}
1646
- rows={3}
1647
- value={taskFormData.description}
1648
- onChange={(e) =>
1649
- setTaskFormData((prev) => ({
1650
- ...prev,
1651
- description: e.target.value,
1652
- }))
1653
- }
1654
- />
1655
- </div>
1730
+ {!isLimitedView ? (
1731
+ <Sheet
1732
+ open={isEditSheetOpen}
1733
+ onOpenChange={(open) => {
1734
+ if (!open) {
1735
+ closeEditSheet();
1736
+ }
1737
+ }}
1738
+ >
1739
+ <SheetContent className="w-full overflow-x-hidden overflow-y-auto sm:max-w-[min(92vw,64rem)]">
1740
+ <SheetHeader>
1741
+ <SheetTitle>{formT('editTitle')}</SheetTitle>
1742
+ <SheetDescription>{formT('description')}</SheetDescription>
1743
+ </SheetHeader>
1744
+
1745
+ <ProjectFormScreen
1746
+ projectId={projectId}
1747
+ onCancel={closeEditSheet}
1748
+ onSaved={async () => {
1749
+ closeEditSheet();
1750
+ await refetch();
1751
+ }}
1752
+ />
1753
+ </SheetContent>
1754
+ </Sheet>
1755
+ ) : null}
1756
+
1757
+ {!isLimitedView ? (
1758
+ <Dialog
1759
+ open={taskFormOpen}
1760
+ onOpenChange={(open) => {
1761
+ if (!open) {
1762
+ setTaskFormOpen(false);
1763
+ setEditingTaskId(null);
1764
+ setTaskFormData(EMPTY_TASK_FORM);
1765
+ }
1766
+ }}
1767
+ >
1768
+ <DialogContent className="sm:max-w-lg">
1769
+ <DialogHeader>
1770
+ <DialogTitle>
1771
+ {editingTaskId
1772
+ ? t('taskForm.titleEdit')
1773
+ : t('taskForm.titleNew')}
1774
+ </DialogTitle>
1775
+ </DialogHeader>
1776
+
1777
+ <div className="space-y-4">
1778
+ <div className="space-y-1.5">
1779
+ <Label htmlFor="task-name">{t('taskForm.nameLabel')} *</Label>
1780
+ <Input
1781
+ id="task-name"
1782
+ placeholder={t('taskForm.namePlaceholder')}
1783
+ value={taskFormData.name}
1784
+ onChange={(e) =>
1785
+ setTaskFormData((prev) => ({
1786
+ ...prev,
1787
+ name: e.target.value,
1788
+ }))
1789
+ }
1790
+ />
1791
+ </div>
1656
1792
 
1657
- <div className="grid grid-cols-2 gap-3">
1658
1793
  <div className="space-y-1.5">
1659
- <Label>{t('taskForm.priorityLabel')}</Label>
1660
- <Select
1661
- value={taskFormData.priority}
1662
- onValueChange={(v) =>
1794
+ <Label htmlFor="task-description">
1795
+ {t('taskForm.descriptionLabel')}
1796
+ </Label>
1797
+ <Textarea
1798
+ id="task-description"
1799
+ placeholder={t('taskForm.descriptionPlaceholder')}
1800
+ rows={3}
1801
+ value={taskFormData.description}
1802
+ onChange={(e) =>
1663
1803
  setTaskFormData((prev) => ({
1664
1804
  ...prev,
1665
- priority: v as TaskFormState['priority'],
1805
+ description: e.target.value,
1666
1806
  }))
1667
1807
  }
1668
- >
1669
- <SelectTrigger className="w-full">
1670
- <SelectValue />
1671
- </SelectTrigger>
1672
- <SelectContent>
1673
- <SelectItem value="low">
1674
- {getTaskPriorityLabel('low')}
1675
- </SelectItem>
1676
- <SelectItem value="medium">
1677
- {getTaskPriorityLabel('medium')}
1678
- </SelectItem>
1679
- <SelectItem value="high">
1680
- {getTaskPriorityLabel('high')}
1681
- </SelectItem>
1682
- </SelectContent>
1683
- </Select>
1808
+ />
1809
+ </div>
1810
+
1811
+ <div className="grid grid-cols-2 gap-3">
1812
+ <div className="space-y-1.5">
1813
+ <Label>{t('taskForm.priorityLabel')}</Label>
1814
+ <Select
1815
+ value={taskFormData.priority}
1816
+ onValueChange={(v) =>
1817
+ setTaskFormData((prev) => ({
1818
+ ...prev,
1819
+ priority: v as TaskFormState['priority'],
1820
+ }))
1821
+ }
1822
+ >
1823
+ <SelectTrigger className="w-full">
1824
+ <SelectValue />
1825
+ </SelectTrigger>
1826
+ <SelectContent>
1827
+ <SelectItem value="low">
1828
+ {getTaskPriorityLabel('low')}
1829
+ </SelectItem>
1830
+ <SelectItem value="medium">
1831
+ {getTaskPriorityLabel('medium')}
1832
+ </SelectItem>
1833
+ <SelectItem value="high">
1834
+ {getTaskPriorityLabel('high')}
1835
+ </SelectItem>
1836
+ </SelectContent>
1837
+ </Select>
1838
+ </div>
1839
+
1840
+ <div className="space-y-1.5">
1841
+ <Label>{t('taskForm.columnLabel')}</Label>
1842
+ <Select
1843
+ value={taskFormData.status}
1844
+ onValueChange={(v) =>
1845
+ setTaskFormData((prev) => ({
1846
+ ...prev,
1847
+ status: v as BoardColumnId,
1848
+ }))
1849
+ }
1850
+ >
1851
+ <SelectTrigger className="w-full">
1852
+ <SelectValue />
1853
+ </SelectTrigger>
1854
+ <SelectContent>
1855
+ {KANBAN_COLUMNS.map((col) => (
1856
+ <SelectItem key={col.id} value={col.id}>
1857
+ {col.label}
1858
+ </SelectItem>
1859
+ ))}
1860
+ </SelectContent>
1861
+ </Select>
1862
+ </div>
1684
1863
  </div>
1685
1864
 
1686
1865
  <div className="space-y-1.5">
1687
- <Label>{t('taskForm.columnLabel')}</Label>
1866
+ <Label>Responsável</Label>
1688
1867
  <Select
1689
- value={taskFormData.status}
1690
- onValueChange={(v) =>
1868
+ value={taskFormData.assigneeCollaboratorId}
1869
+ onValueChange={(value) =>
1691
1870
  setTaskFormData((prev) => ({
1692
1871
  ...prev,
1693
- status: v as BoardColumnId,
1872
+ assigneeCollaboratorId: value,
1694
1873
  }))
1695
1874
  }
1696
1875
  >
1697
1876
  <SelectTrigger className="w-full">
1698
- <SelectValue />
1877
+ <SelectValue placeholder={commonT('labels.notAssigned')} />
1699
1878
  </SelectTrigger>
1700
1879
  <SelectContent>
1701
- {KANBAN_COLUMNS.map((col) => (
1702
- <SelectItem key={col.id} value={col.id}>
1703
- {col.label}
1880
+ <SelectItem value="none">
1881
+ {commonT('labels.notAssigned')}
1882
+ </SelectItem>
1883
+ {taskAssigneeOptions.map((option) => (
1884
+ <SelectItem key={option.id} value={option.id}>
1885
+ {option.label}
1704
1886
  </SelectItem>
1705
1887
  ))}
1706
1888
  </SelectContent>
1707
1889
  </Select>
1708
1890
  </div>
1709
- </div>
1710
1891
 
1711
- <div className="space-y-1.5">
1712
- <Label>Responsável</Label>
1713
- <Select
1714
- value={taskFormData.assigneeCollaboratorId}
1715
- onValueChange={(value) =>
1716
- setTaskFormData((prev) => ({
1717
- ...prev,
1718
- assigneeCollaboratorId: value,
1719
- }))
1720
- }
1721
- >
1722
- <SelectTrigger className="w-full">
1723
- <SelectValue placeholder={commonT('labels.notAssigned')} />
1724
- </SelectTrigger>
1725
- <SelectContent>
1726
- <SelectItem value="none">
1727
- {commonT('labels.notAssigned')}
1728
- </SelectItem>
1729
- {taskAssigneeOptions.map((option) => (
1730
- <SelectItem key={option.id} value={option.id}>
1731
- {option.label}
1732
- </SelectItem>
1733
- ))}
1734
- </SelectContent>
1735
- </Select>
1736
- </div>
1892
+ <div className="grid grid-cols-2 gap-3">
1893
+ <div className="space-y-1.5">
1894
+ <Label htmlFor="task-due-date">
1895
+ {t('taskForm.deadlineLabel')}
1896
+ </Label>
1897
+ <Input
1898
+ id="task-due-date"
1899
+ type="date"
1900
+ value={taskFormData.dueDate}
1901
+ onChange={(e) =>
1902
+ setTaskFormData((prev) => ({
1903
+ ...prev,
1904
+ dueDate: e.target.value,
1905
+ }))
1906
+ }
1907
+ />
1908
+ </div>
1737
1909
 
1738
- <div className="grid grid-cols-2 gap-3">
1739
- <div className="space-y-1.5">
1740
- <Label htmlFor="task-due-date">
1741
- {t('taskForm.deadlineLabel')}
1742
- </Label>
1743
- <Input
1744
- id="task-due-date"
1745
- type="date"
1746
- value={taskFormData.dueDate}
1747
- onChange={(e) =>
1748
- setTaskFormData((prev) => ({
1749
- ...prev,
1750
- dueDate: e.target.value,
1751
- }))
1752
- }
1753
- />
1910
+ <div className="space-y-1.5">
1911
+ <Label htmlFor="task-estimate">
1912
+ {t('taskForm.estimateLabel')}
1913
+ </Label>
1914
+ <Input
1915
+ id="task-estimate"
1916
+ type="number"
1917
+ min="0"
1918
+ step="0.5"
1919
+ placeholder="0"
1920
+ value={taskFormData.estimateHours}
1921
+ onChange={(e) =>
1922
+ setTaskFormData((prev) => ({
1923
+ ...prev,
1924
+ estimateHours: e.target.value,
1925
+ }))
1926
+ }
1927
+ />
1928
+ </div>
1754
1929
  </div>
1755
1930
 
1756
1931
  <div className="space-y-1.5">
1757
- <Label htmlFor="task-estimate">
1758
- {t('taskForm.estimateLabel')}
1759
- </Label>
1932
+ <Label htmlFor="task-tags">{t('taskForm.tagsLabel')}</Label>
1760
1933
  <Input
1761
- id="task-estimate"
1762
- type="number"
1763
- min="0"
1764
- step="0.5"
1765
- placeholder="0"
1766
- value={taskFormData.estimateHours}
1934
+ id="task-tags"
1935
+ placeholder={t('taskForm.tagsPlaceholder')}
1936
+ value={taskFormData.tags}
1767
1937
  onChange={(e) =>
1768
1938
  setTaskFormData((prev) => ({
1769
1939
  ...prev,
1770
- estimateHours: e.target.value,
1940
+ tags: e.target.value,
1771
1941
  }))
1772
1942
  }
1773
1943
  />
1774
1944
  </div>
1775
1945
  </div>
1776
1946
 
1777
- <div className="space-y-1.5">
1778
- <Label htmlFor="task-tags">{t('taskForm.tagsLabel')}</Label>
1779
- <Input
1780
- id="task-tags"
1781
- placeholder={t('taskForm.tagsPlaceholder')}
1782
- value={taskFormData.tags}
1783
- onChange={(e) =>
1784
- setTaskFormData((prev) => ({ ...prev, tags: e.target.value }))
1785
- }
1786
- />
1787
- </div>
1788
- </div>
1789
-
1790
- <DialogFooter className="mt-4">
1791
- <Button
1792
- variant="outline"
1793
- onClick={() => {
1794
- setTaskFormOpen(false);
1795
- setEditingTaskId(null);
1796
- setTaskFormData(EMPTY_TASK_FORM);
1797
- }}
1798
- disabled={taskFormLoading}
1799
- >
1800
- {commonT('actions.cancel')}
1801
- </Button>
1802
- <Button
1803
- onClick={() => void handleTaskFormSubmit()}
1804
- disabled={taskFormLoading || !taskFormData.name.trim()}
1805
- >
1806
- {taskFormLoading
1807
- ? t('taskForm.saving')
1808
- : editingTaskId
1809
- ? commonT('actions.save')
1810
- : commonT('actions.create')}
1811
- </Button>
1812
- </DialogFooter>
1813
- </DialogContent>
1814
- </Dialog>
1815
-
1816
- {/* Delete confirmation dialog */}
1817
- <Dialog
1818
- open={deleteConfirmId !== null}
1819
- onOpenChange={(open) => {
1820
- if (!open) setDeleteConfirmId(null);
1821
- }}
1822
- >
1823
- <DialogContent className="sm:max-w-sm">
1824
- <DialogHeader>
1825
- <DialogTitle>Excluir tarefa</DialogTitle>
1826
- </DialogHeader>
1827
- <p className="text-sm text-muted-foreground">
1828
- Tem certeza que deseja excluir esta tarefa? Esta acao nao pode ser
1829
- desfeita.
1830
- </p>
1831
- <DialogFooter className="mt-4">
1832
- <Button variant="outline" onClick={() => setDeleteConfirmId(null)}>
1833
- Cancelar
1834
- </Button>
1835
- <Button
1836
- variant="destructive"
1837
- onClick={() => {
1838
- if (deleteConfirmId !== null) {
1839
- void handleDeleteTask(deleteConfirmId);
1840
- }
1841
- }}
1842
- >
1843
- Excluir
1844
- </Button>
1845
- </DialogFooter>
1846
- </DialogContent>
1847
- </Dialog>
1947
+ <DialogFooter className="mt-4">
1948
+ <Button
1949
+ variant="outline"
1950
+ onClick={() => {
1951
+ setTaskFormOpen(false);
1952
+ setEditingTaskId(null);
1953
+ setTaskFormData(EMPTY_TASK_FORM);
1954
+ }}
1955
+ disabled={taskFormLoading}
1956
+ >
1957
+ {commonT('actions.cancel')}
1958
+ </Button>
1959
+ <Button
1960
+ onClick={() => void handleTaskFormSubmit()}
1961
+ disabled={taskFormLoading || !taskFormData.name.trim()}
1962
+ >
1963
+ {taskFormLoading
1964
+ ? t('taskForm.saving')
1965
+ : editingTaskId
1966
+ ? commonT('actions.save')
1967
+ : commonT('actions.create')}
1968
+ </Button>
1969
+ </DialogFooter>
1970
+ </DialogContent>
1971
+ </Dialog>
1972
+ ) : null}
1973
+
1974
+ {!isLimitedView ? (
1975
+ <>
1976
+ <Dialog
1977
+ open={deletePromptTask !== null}
1978
+ onOpenChange={(open) => {
1979
+ if (!open) setDeletePromptTask(null);
1980
+ }}
1981
+ >
1982
+ <DialogContent className="sm:max-w-sm">
1983
+ <DialogHeader>
1984
+ <DialogTitle>{t('dialogs.deleteTitle')}</DialogTitle>
1985
+ </DialogHeader>
1986
+ <p className="text-sm text-muted-foreground">
1987
+ {t('dialogs.deleteDescription')}
1988
+ </p>
1989
+ <DialogFooter className="mt-4">
1990
+ <Button
1991
+ variant="outline"
1992
+ onClick={() => setDeletePromptTask(null)}
1993
+ >
1994
+ {commonT('actions.cancel')}
1995
+ </Button>
1996
+ {deletePromptTask ? (
1997
+ <Button
1998
+ variant="destructive"
1999
+ onClick={() => void handleDeleteTask(deletePromptTask.id)}
2000
+ >
2001
+ {commonT('actions.delete')}
2002
+ </Button>
2003
+ ) : null}
2004
+ </DialogFooter>
2005
+ </DialogContent>
2006
+ </Dialog>
2007
+ </>
2008
+ ) : null}
1848
2009
  </Page>
1849
2010
  );
1850
2011
  }