@hed-hog/operations 0.0.332 → 0.0.338

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 (109) hide show
  1. package/dist/controllers/operations-collaborators.controller.d.ts +0 -54
  2. package/dist/controllers/operations-collaborators.controller.d.ts.map +1 -1
  3. package/dist/controllers/operations-collaborators.controller.js +0 -100
  4. package/dist/controllers/operations-collaborators.controller.js.map +1 -1
  5. package/dist/controllers/operations-contracts.controller.d.ts +12 -12
  6. package/dist/operations.service.d.ts +0 -76
  7. package/dist/operations.service.d.ts.map +1 -1
  8. package/dist/operations.service.js +7 -230
  9. package/dist/operations.service.js.map +1 -1
  10. package/dist/operations.service.spec.js +6 -0
  11. package/dist/operations.service.spec.js.map +1 -1
  12. package/hedhog/data/menu.yaml +8 -27
  13. package/hedhog/data/route.yaml +0 -72
  14. package/hedhog/frontend/app/_components/collaborator-details-screen.tsx.ejs +476 -0
  15. package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +3 -39
  16. package/hedhog/frontend/app/_components/collaborator-select-with-create.tsx.ejs +261 -0
  17. package/hedhog/frontend/app/_components/collaborator-tasks-tab.tsx.ejs +358 -358
  18. package/hedhog/frontend/app/_components/collaborator-timesheets-tab.tsx.ejs +6 -6
  19. package/hedhog/frontend/app/_components/contract-content-editor.tsx.ejs +258 -0
  20. package/hedhog/frontend/app/_components/my-project-summary-screen.tsx.ejs +5 -4
  21. package/hedhog/frontend/app/_components/person-select-with-create.tsx.ejs +1 -0
  22. package/hedhog/frontend/app/_components/project-assignments-tab.tsx.ejs +10 -218
  23. package/hedhog/frontend/app/_components/project-cost-report-screen.tsx.ejs +23 -23
  24. package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +24 -708
  25. package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +38 -158
  26. package/hedhog/frontend/app/_components/task-detail-sheet.tsx.ejs +1 -5
  27. package/hedhog/frontend/app/_components/task-form-sheet.tsx.ejs +629 -629
  28. package/hedhog/frontend/app/_lib/api.ts.ejs +0 -151
  29. package/hedhog/frontend/app/_lib/types.ts.ejs +0 -1
  30. package/hedhog/frontend/app/_lib/utils/task-ui.ts.ejs +0 -18
  31. package/hedhog/frontend/app/my-projects/page.tsx.ejs +2 -16
  32. package/hedhog/frontend/app/my-tasks/page.tsx.ejs +86 -24
  33. package/hedhog/frontend/app/projects/page.tsx.ejs +6 -42
  34. package/hedhog/frontend/messages/en.json +2 -96
  35. package/hedhog/frontend/messages/operations/operations/en.json +2100 -0
  36. package/hedhog/frontend/messages/operations/operations/pt.json +2111 -0
  37. package/hedhog/frontend/messages/pt.json +2 -96
  38. package/hedhog/frontend/widgets/capacity-distribution.tsx.ejs +16 -16
  39. package/hedhog/frontend/widgets/effort-by-project.tsx.ejs +16 -16
  40. package/hedhog/frontend/widgets/headcount-by-area.tsx.ejs +16 -16
  41. package/hedhog/frontend/widgets/index.ts.ejs +25 -25
  42. package/hedhog/frontend/widgets/managed-projects-status.tsx.ejs +16 -16
  43. package/hedhog/frontend/widgets/my-hours-period-kpi.tsx.ejs +16 -16
  44. package/hedhog/frontend/widgets/my-open-requests-kpi.tsx.ejs +16 -16
  45. package/hedhog/frontend/widgets/my-pending-requests-list.tsx.ejs +16 -16
  46. package/hedhog/frontend/widgets/my-project-allocations-kpi.tsx.ejs +16 -16
  47. package/hedhog/frontend/widgets/my-quick-actions.tsx.ejs +16 -16
  48. package/hedhog/frontend/widgets/my-relevant-deadlines.tsx.ejs +16 -16
  49. package/hedhog/frontend/widgets/my-timesheet-status-kpi.tsx.ejs +16 -16
  50. package/hedhog/frontend/widgets/my-weekly-journey.tsx.ejs +16 -16
  51. package/hedhog/frontend/widgets/portfolio-costs-kpi.tsx.ejs +16 -16
  52. package/hedhog/frontend/widgets/portfolio-effort-kpi.tsx.ejs +16 -16
  53. package/hedhog/frontend/widgets/portfolio-projects-kpi.tsx.ejs +16 -16
  54. package/hedhog/frontend/widgets/portfolio-risk-kpi.tsx.ejs +16 -16
  55. package/hedhog/frontend/widgets/project-status-overview.tsx.ejs +16 -16
  56. package/hedhog/frontend/widgets/shared-operations-widget.tsx.ejs +169 -169
  57. package/hedhog/frontend/widgets/strategic-deadlines.tsx.ejs +16 -16
  58. package/hedhog/frontend/widgets/team-approval-queue.tsx.ejs +16 -16
  59. package/hedhog/frontend/widgets/team-capacity-kpi.tsx.ejs +16 -16
  60. package/hedhog/frontend/widgets/team-headcount-kpi.tsx.ejs +16 -16
  61. package/hedhog/frontend/widgets/team-hours-kpi.tsx.ejs +16 -16
  62. package/hedhog/frontend/widgets/team-pending-approvals-kpi.tsx.ejs +16 -16
  63. package/hedhog/frontend/widgets/team-utilization-overview.tsx.ejs +16 -16
  64. package/hedhog/frontend/widgets/team-workload-alerts.tsx.ejs +16 -16
  65. package/hedhog/table/operations_collaborator.yaml +8 -8
  66. package/hedhog/table/operations_task.yaml +76 -76
  67. package/hedhog/table/operations_task_activity.yaml +51 -51
  68. package/package.json +6 -6
  69. package/src/controllers/operations-collaborators.controller.ts +8 -117
  70. package/src/controllers/operations-tasks.controller.ts +156 -156
  71. package/src/dashboard/widgets/MyQuickActions.tsx +22 -22
  72. package/src/dto/create-collaborator.dto.ts +4 -4
  73. package/src/operations.service.spec.ts +1006 -988
  74. package/src/operations.service.ts +7 -323
  75. package/dist/dto/create-collaborator-invoice.dto.d.ts +0 -11
  76. package/dist/dto/create-collaborator-invoice.dto.d.ts.map +0 -1
  77. package/dist/dto/create-collaborator-invoice.dto.js +0 -55
  78. package/dist/dto/create-collaborator-invoice.dto.js.map +0 -1
  79. package/dist/dto/create-collaborator-payment.dto.d.ts +0 -10
  80. package/dist/dto/create-collaborator-payment.dto.d.ts.map +0 -1
  81. package/dist/dto/create-collaborator-payment.dto.js +0 -50
  82. package/dist/dto/create-collaborator-payment.dto.js.map +0 -1
  83. package/dist/dto/list-collaborator-invoice.dto.d.ts +0 -4
  84. package/dist/dto/list-collaborator-invoice.dto.d.ts.map +0 -1
  85. package/dist/dto/list-collaborator-invoice.dto.js +0 -8
  86. package/dist/dto/list-collaborator-invoice.dto.js.map +0 -1
  87. package/dist/dto/list-collaborator-payment.dto.d.ts +0 -4
  88. package/dist/dto/list-collaborator-payment.dto.d.ts.map +0 -1
  89. package/dist/dto/list-collaborator-payment.dto.js +0 -8
  90. package/dist/dto/list-collaborator-payment.dto.js.map +0 -1
  91. package/dist/dto/update-collaborator-invoice.dto.d.ts +0 -6
  92. package/dist/dto/update-collaborator-invoice.dto.d.ts.map +0 -1
  93. package/dist/dto/update-collaborator-invoice.dto.js +0 -9
  94. package/dist/dto/update-collaborator-invoice.dto.js.map +0 -1
  95. package/dist/dto/update-collaborator-payment.dto.d.ts +0 -6
  96. package/dist/dto/update-collaborator-payment.dto.d.ts.map +0 -1
  97. package/dist/dto/update-collaborator-payment.dto.js +0 -9
  98. package/dist/dto/update-collaborator-payment.dto.js.map +0 -1
  99. package/hedhog/frontend/app/_components/collaborator-invoices-tab.tsx.ejs +0 -443
  100. package/hedhog/frontend/app/_components/collaborator-payment-history-tab.tsx.ejs +0 -429
  101. package/hedhog/frontend/app/tasks-gantt/page.tsx.ejs +0 -953
  102. package/hedhog/table/operations_collaborator_invoice.yaml +0 -35
  103. package/hedhog/table/operations_collaborator_payment.yaml +0 -32
  104. package/src/dto/create-collaborator-invoice.dto.ts +0 -39
  105. package/src/dto/create-collaborator-payment.dto.ts +0 -35
  106. package/src/dto/list-collaborator-invoice.dto.ts +0 -3
  107. package/src/dto/list-collaborator-payment.dto.ts +0 -3
  108. package/src/dto/update-collaborator-invoice.dto.ts +0 -6
  109. package/src/dto/update-collaborator-payment.dto.ts +0 -6
@@ -18,7 +18,6 @@ import {
18
18
  DialogHeader,
19
19
  DialogTitle,
20
20
  } from '@/components/ui/dialog';
21
- import { EntityPicker } from '@/components/ui/entity-picker';
22
21
  import { Input } from '@/components/ui/input';
23
22
  import { Label } from '@/components/ui/label';
24
23
  import { Progress } from '@/components/ui/progress';
@@ -88,6 +87,7 @@ import {
88
87
  Gauge,
89
88
  GitCommitHorizontal,
90
89
  HeartPulse,
90
+ History,
91
91
  LineChart as LineChartIcon,
92
92
  Loader2,
93
93
  MessageSquare,
@@ -101,7 +101,6 @@ import {
101
101
  Timer,
102
102
  Trash2,
103
103
  TrendingUp,
104
- UserPlus,
105
104
  Users,
106
105
  type LucideIcon,
107
106
  } from 'lucide-react';
@@ -134,11 +133,8 @@ import {
134
133
  useValuesVisibility,
135
134
  } from '../_lib/hooks/use-values-visibility';
136
135
  import type {
137
- OperationsCollaborator,
138
- OperationsCollaboratorDetails,
139
136
  OperationsProjectDetails,
140
137
  OperationsTaskOption,
141
- PaginatedResponse,
142
138
  } from '../_lib/types';
143
139
  import {
144
140
  formatCurrency,
@@ -149,14 +145,17 @@ import {
149
145
  formatPercent,
150
146
  getStatusBadgeClass,
151
147
  } from '../_lib/utils/format';
152
- import { parseNumberInput } from '../_lib/utils/forms';
153
148
  import { OperationsHeader } from './operations-header';
154
149
  import { ProjectCostsSection } from './project-costs-section';
155
- import { ProjectFileAttachments } from './project-file-attachments';
156
150
  import { ProjectFormScreen } from './project-form-screen';
157
151
  import { SectionCard } from './section-card';
158
152
  import { StatusBadge } from './status-badge';
159
- import { TaskDetailSheet, type TaskDetailSheetData } from './task-detail-sheet';
153
+ import {
154
+ TaskDetailSheet,
155
+ type TaskDetailSheetData,
156
+ } from './task-detail-sheet';
157
+ import { ProjectFileAttachments } from './project-file-attachments';
158
+ import { TaskFileAttachments } from './task-file-attachments';
160
159
  import { TaskFormSheet } from './task-form-sheet';
161
160
  import { TimesheetEntryCreateSheet } from './timesheet-entry-create-sheet';
162
161
 
@@ -205,15 +204,6 @@ type TimesheetEntryPrefill = {
205
204
  taskLabel: string;
206
205
  };
207
206
 
208
- function formatAssignmentNumericValue(value: number) {
209
- if (!Number.isFinite(value)) {
210
- return '';
211
- }
212
-
213
- const roundedValue = Math.round(value * 100) / 100;
214
- return String(roundedValue);
215
- }
216
-
217
207
  const KANBAN_COLUMNS: Array<{ id: BoardColumnId; label: string }> = [
218
208
  { id: 'todo', label: 'Backlog' },
219
209
  { id: 'doing', label: 'Em execução' },
@@ -396,7 +386,7 @@ function getInitials(value?: string | null) {
396
386
  function getPersonAvatarUrl(avatarId?: number | null) {
397
387
  return typeof avatarId === 'number' && avatarId > 0
398
388
  ? `${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${avatarId}`
399
- : undefined;
389
+ : '/placeholder.png';
400
390
  }
401
391
 
402
392
  function getUserPhotoUrl(photoId?: number | null) {
@@ -1102,7 +1092,6 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
1102
1092
  const commonT = useTranslations('operations.Common');
1103
1093
  const formT = useTranslations('operations.ProjectFormPage');
1104
1094
  const contractT = useTranslations('operations.ContractFormPage');
1105
- const collaboratorFormT = useTranslations('operations.CollaboratorFormPage');
1106
1095
  const { request, currentLocaleCode, getSettingValue } = useApp();
1107
1096
  const access = useOperationsAccess();
1108
1097
  const isLimitedView = !access.isDirector && !access.isSupervisor;
@@ -1310,26 +1299,6 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
1310
1299
  const [timesheetPrefill, setTimesheetPrefill] =
1311
1300
  useState<TimesheetEntryPrefill | null>(null);
1312
1301
 
1313
- // Assignment management state
1314
- const [assignmentSheetOpen, setAssignmentSheetOpen] = useState(false);
1315
- const [editingAssignment, setEditingAssignment] = useState<
1316
- OperationsProjectDetails['assignments'][0] | null
1317
- >(null);
1318
- const [assignmentFormData, setAssignmentFormData] = useState({
1319
- collaboratorId: '',
1320
- weeklyHours: '',
1321
- allocationPercent: '',
1322
- status: 'active',
1323
- startDate: '',
1324
- endDate: '',
1325
- });
1326
- const [selectedAssignmentCollaborator, setSelectedAssignmentCollaborator] =
1327
- useState<OperationsCollaborator | null>(null);
1328
- const [savingAssignment, setSavingAssignment] = useState(false);
1329
- const [removingAssignmentId, setRemovingAssignmentId] = useState<
1330
- number | null
1331
- >(null);
1332
-
1333
1302
  const apiTasks = useMemo(() => rawTasks.map(apiTaskToBoardTask), [rawTasks]);
1334
1303
  const archivedTasks = useMemo(
1335
1304
  () =>
@@ -1537,11 +1506,7 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
1537
1506
  const handleDeleteProject = useCallback(async () => {
1538
1507
  setDeletingProject(true);
1539
1508
  try {
1540
- await mutateOperations(
1541
- request,
1542
- `/operations/projects/${projectId}`,
1543
- 'DELETE'
1544
- );
1509
+ await mutateOperations(request, `/operations/projects/${projectId}`, 'DELETE');
1545
1510
  router.push('/operations/projects');
1546
1511
  } catch {
1547
1512
  // ignore
@@ -1575,245 +1540,6 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
1575
1540
  [commonT, project, projectId]
1576
1541
  );
1577
1542
 
1578
- const loadCollaboratorOptions = useCallback(
1579
- async ({
1580
- page,
1581
- pageSize,
1582
- search,
1583
- }: {
1584
- page: number;
1585
- pageSize: number;
1586
- search: string;
1587
- }) => {
1588
- const params = new URLSearchParams({
1589
- page: String(page),
1590
- pageSize: String(pageSize),
1591
- sortField: 'displayName',
1592
- sortOrder: 'asc',
1593
- });
1594
- if (search.trim()) params.set('search', search.trim());
1595
- const result = await fetchOperations<
1596
- PaginatedResponse<OperationsCollaborator>
1597
- >(request, `/operations/collaborators?${params.toString()}`);
1598
- const items = result?.data ?? [];
1599
- const total = result?.total ?? 0;
1600
- return { items, hasMore: page * pageSize < total };
1601
- },
1602
- [request]
1603
- );
1604
-
1605
- const loadAssignmentCollaboratorById = useCallback(
1606
- async (collaboratorId: number) => {
1607
- const collaborator = await fetchOperations<OperationsCollaboratorDetails>(
1608
- request,
1609
- `/operations/collaborators/${collaboratorId}`
1610
- );
1611
- setSelectedAssignmentCollaborator(collaborator);
1612
- return collaborator;
1613
- },
1614
- [request]
1615
- );
1616
-
1617
- const createAssignmentCollaborator = useCallback(
1618
- async (values: Record<string, string>) => {
1619
- const displayName = values.displayName?.trim() ?? '';
1620
- const weeklyCapacityHours = parseNumberInput(
1621
- values.weeklyCapacityHours ?? ''
1622
- );
1623
-
1624
- if (!displayName) {
1625
- return null;
1626
- }
1627
-
1628
- return mutateOperations<OperationsCollaborator>(
1629
- request,
1630
- '/operations/collaborators',
1631
- 'POST',
1632
- {
1633
- displayName,
1634
- weeklyCapacityHours,
1635
- status: 'active',
1636
- autoGenerateContractDraft: false,
1637
- }
1638
- );
1639
- },
1640
- [request]
1641
- );
1642
-
1643
- const syncAssignmentFromWeeklyHours = useCallback(
1644
- (
1645
- weeklyHours: string,
1646
- currentFormData: typeof assignmentFormData,
1647
- collaborator?: OperationsCollaborator | null
1648
- ) => {
1649
- const capacity = collaborator?.weeklyCapacityHours;
1650
- if (!capacity || capacity <= 0) {
1651
- return {
1652
- weeklyHours,
1653
- allocationPercent: currentFormData.allocationPercent,
1654
- };
1655
- }
1656
-
1657
- const parsedHours = parseNumberInput(weeklyHours);
1658
- if (parsedHours == null) {
1659
- return { weeklyHours, allocationPercent: '' };
1660
- }
1661
-
1662
- return {
1663
- weeklyHours,
1664
- allocationPercent: formatAssignmentNumericValue(
1665
- (parsedHours / capacity) * 100
1666
- ),
1667
- };
1668
- },
1669
- []
1670
- );
1671
-
1672
- const syncAssignmentFromAllocation = useCallback(
1673
- (
1674
- allocationPercent: string,
1675
- currentFormData: typeof assignmentFormData,
1676
- collaborator?: OperationsCollaborator | null
1677
- ) => {
1678
- const capacity = collaborator?.weeklyCapacityHours;
1679
- if (!capacity || capacity <= 0) {
1680
- return {
1681
- weeklyHours: currentFormData.weeklyHours,
1682
- allocationPercent,
1683
- };
1684
- }
1685
-
1686
- const parsedPercent = parseNumberInput(allocationPercent);
1687
- if (parsedPercent == null) {
1688
- return { weeklyHours: '', allocationPercent };
1689
- }
1690
-
1691
- return {
1692
- weeklyHours: formatAssignmentNumericValue(
1693
- (parsedPercent / 100) * capacity
1694
- ),
1695
- allocationPercent,
1696
- };
1697
- },
1698
- []
1699
- );
1700
-
1701
- const openAddAssignment = useCallback(() => {
1702
- setEditingAssignment(null);
1703
- setSelectedAssignmentCollaborator(null);
1704
- setAssignmentFormData({
1705
- collaboratorId: '',
1706
- weeklyHours: '',
1707
- allocationPercent: '',
1708
- status: 'active',
1709
- startDate: '',
1710
- endDate: '',
1711
- });
1712
- setAssignmentSheetOpen(true);
1713
- }, []);
1714
-
1715
- const openEditAssignment = useCallback(
1716
- (assignment: OperationsProjectDetails['assignments'][0]) => {
1717
- setEditingAssignment(assignment);
1718
- setSelectedAssignmentCollaborator(null);
1719
- setAssignmentFormData({
1720
- collaboratorId: String(assignment.collaboratorId),
1721
- weeklyHours:
1722
- assignment.weeklyHours != null ? String(assignment.weeklyHours) : '',
1723
- allocationPercent:
1724
- assignment.allocationPercent != null
1725
- ? String(assignment.allocationPercent)
1726
- : '',
1727
- status: assignment.status,
1728
- startDate: assignment.startDate?.slice(0, 10) ?? '',
1729
- endDate: assignment.endDate?.slice(0, 10) ?? '',
1730
- });
1731
- void loadAssignmentCollaboratorById(assignment.collaboratorId);
1732
- setAssignmentSheetOpen(true);
1733
- },
1734
- [loadAssignmentCollaboratorById]
1735
- );
1736
-
1737
- const handleSaveAssignment = useCallback(async () => {
1738
- if (!project) return;
1739
- const collabId = Number(assignmentFormData.collaboratorId);
1740
- if (!collabId) return;
1741
- setSavingAssignment(true);
1742
- const newEntry = {
1743
- collaboratorId: collabId,
1744
- weeklyHours: assignmentFormData.weeklyHours
1745
- ? Number(assignmentFormData.weeklyHours)
1746
- : null,
1747
- allocationPercent: assignmentFormData.allocationPercent
1748
- ? Number(assignmentFormData.allocationPercent)
1749
- : null,
1750
- status: assignmentFormData.status,
1751
- startDate: assignmentFormData.startDate || null,
1752
- endDate: assignmentFormData.endDate || null,
1753
- };
1754
- const existingMapped = project.assignments.map((a) => ({
1755
- collaboratorId: a.collaboratorId,
1756
- weeklyHours: a.weeklyHours ?? null,
1757
- allocationPercent: a.allocationPercent ?? null,
1758
- status: a.status,
1759
- startDate: a.startDate ?? null,
1760
- endDate: a.endDate ?? null,
1761
- }));
1762
- const updatedAssignments = editingAssignment
1763
- ? existingMapped.map((a) =>
1764
- a.collaboratorId === editingAssignment.collaboratorId ? newEntry : a
1765
- )
1766
- : [...existingMapped, newEntry];
1767
- try {
1768
- await mutateOperations(
1769
- request,
1770
- `/operations/projects/${projectId}`,
1771
- 'PATCH',
1772
- { teamAssignments: updatedAssignments }
1773
- );
1774
- await refetch();
1775
- setAssignmentSheetOpen(false);
1776
- } catch {
1777
- // ignore
1778
- } finally {
1779
- setSavingAssignment(false);
1780
- }
1781
- }, [
1782
- project,
1783
- assignmentFormData,
1784
- editingAssignment,
1785
- request,
1786
- projectId,
1787
- refetch,
1788
- ]);
1789
-
1790
- const handleConfirmRemoveAssignment = useCallback(async () => {
1791
- if (!project || removingAssignmentId === null) return;
1792
- const updatedAssignments = project.assignments
1793
- .filter((a) => a.collaboratorId !== removingAssignmentId)
1794
- .map((a) => ({
1795
- collaboratorId: a.collaboratorId,
1796
- weeklyHours: a.weeklyHours ?? null,
1797
- allocationPercent: a.allocationPercent ?? null,
1798
- status: a.status,
1799
- startDate: a.startDate ?? null,
1800
- endDate: a.endDate ?? null,
1801
- }));
1802
- try {
1803
- await mutateOperations(
1804
- request,
1805
- `/operations/projects/${projectId}`,
1806
- 'PATCH',
1807
- { teamAssignments: updatedAssignments }
1808
- );
1809
- await refetch();
1810
- } catch {
1811
- // ignore
1812
- } finally {
1813
- setRemovingAssignmentId(null);
1814
- }
1815
- }, [project, removingAssignmentId, request, projectId, refetch]);
1816
-
1817
1543
  const allocationChartData = useMemo(() => {
1818
1544
  if (projectStats?.allocationByCollaborator?.length) {
1819
1545
  return projectStats.allocationByCollaborator;
@@ -2414,13 +2140,10 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
2414
2140
  <TooltipTrigger asChild>
2415
2141
  <div className="flex cursor-default items-center gap-1.5 border-r px-3 py-2 transition hover:bg-muted/30">
2416
2142
  <Avatar className="size-5 shrink-0 border bg-muted">
2417
- <AvatarImage
2418
- src={
2419
- getUserPhotoUrl(project.clientUserPhotoId) ||
2420
- getPersonAvatarUrl(project.clientAvatarId)
2421
- }
2422
- alt={project.clientName || ''}
2423
- />
2143
+ <AvatarImage
2144
+ src={getPersonAvatarUrl(project.clientAvatarId)}
2145
+ alt={project.clientName || ''}
2146
+ />
2424
2147
  <AvatarFallback className="text-[9px]">
2425
2148
  {getInitials(project.clientName)}
2426
2149
  </AvatarFallback>
@@ -2739,10 +2462,7 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
2739
2462
  <div className="flex items-center gap-2">
2740
2463
  <Avatar className="h-8 w-8 border border-border/60 bg-muted">
2741
2464
  <AvatarImage
2742
- src={
2743
- getUserPhotoUrl(project.clientUserPhotoId) ||
2744
- getPersonAvatarUrl(project.clientAvatarId)
2745
- }
2465
+ src={getPersonAvatarUrl(project.clientAvatarId)}
2746
2466
  alt={project.clientName || commonT('labels.client')}
2747
2467
  />
2748
2468
  <AvatarFallback className="bg-muted text-xs font-semibold text-foreground">
@@ -2884,32 +2604,13 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
2884
2604
  <div className="font-medium">
2885
2605
  {project.relatedContract.name}
2886
2606
  </div>
2887
- <div className="flex items-center gap-2 text-sm text-muted-foreground">
2888
- <span className="truncate">
2889
- {project.relatedContract.code || '—'}
2890
- </span>
2891
- {project.relatedContract.clientName ? (
2892
- <>
2893
- <span>•</span>
2894
- <Avatar className="h-4 w-4 shrink-0">
2895
- <AvatarImage
2896
- src={
2897
- getUserPhotoUrl(project.clientUserPhotoId) ||
2898
- getPersonAvatarUrl(project.clientAvatarId)
2899
- }
2900
- alt={project.relatedContract.clientName}
2901
- />
2902
- <AvatarFallback className="text-[8px] font-medium">
2903
- {getInitials(
2904
- project.relatedContract.clientName
2905
- )}
2906
- </AvatarFallback>
2907
- </Avatar>
2908
- <span className="truncate">
2909
- {project.relatedContract.clientName}
2910
- </span>
2911
- </>
2912
- ) : null}
2607
+ <div className="text-sm text-muted-foreground">
2608
+ {[
2609
+ project.relatedContract.code,
2610
+ project.relatedContract.clientName,
2611
+ ]
2612
+ .filter(Boolean)
2613
+ .join(' • ') || commonT('labels.notAvailable')}
2913
2614
  </div>
2914
2615
  </div>
2915
2616
  <div className="flex items-center gap-3">
@@ -3381,7 +3082,7 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
3381
3082
  {(isOver) => (
3382
3083
  <div
3383
3084
  className={[
3384
- 'flex min-h-48 max-h-160 flex-col rounded-3xl border bg-linear-to-b p-3 transition-all',
3085
+ 'flex min-h-128 flex-col overflow-hidden rounded-3xl border bg-linear-to-b p-3 transition-all',
3385
3086
  getColumnClassName(column.id),
3386
3087
  isOver
3387
3088
  ? 'border-primary shadow-lg ring-2 ring-primary/15'
@@ -3424,7 +3125,7 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
3424
3125
  </div>
3425
3126
  </div>
3426
3127
 
3427
- <div className="flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto pb-1 pr-0.5">
3128
+ <div className="flex flex-1 flex-col gap-2">
3428
3129
  <AnimatePresence initial={false}>
3429
3130
  {filteredTaskColumns[column.id].map((task) => {
3430
3131
  const tags = getTaskTags(task);
@@ -4152,19 +3853,6 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
4152
3853
  'rounded-2xl border bg-card p-4 shadow-sm',
4153
3854
  isLimitedView ? 'xl:col-span-12' : 'xl:col-span-8',
4154
3855
  ].join(' ')}
4155
- actions={
4156
- !isLimitedView ? (
4157
- <Button
4158
- size="sm"
4159
- variant="outline"
4160
- className="cursor-pointer"
4161
- onClick={openAddAssignment}
4162
- >
4163
- <UserPlus className="mr-1.5 size-4" />
4164
- {t('teamPanel.addCollaborator')}
4165
- </Button>
4166
- ) : undefined
4167
- }
4168
3856
  >
4169
3857
  {project.assignments.length > 0 ? (
4170
3858
  <div className="space-y-4">
@@ -4272,34 +3960,6 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
4272
3960
  <ToneIcon className="size-3" />
4273
3961
  {t(`teamPanel.status.${tone.labelKey}`)}
4274
3962
  </span>
4275
- {!isLimitedView ? (
4276
- <div className="flex items-center gap-1">
4277
- <Button
4278
- type="button"
4279
- variant="ghost"
4280
- size="icon"
4281
- className="size-7 cursor-pointer"
4282
- onClick={() =>
4283
- openEditAssignment(assignment)
4284
- }
4285
- >
4286
- <Pencil className="size-3.5" />
4287
- </Button>
4288
- <Button
4289
- type="button"
4290
- variant="ghost"
4291
- size="icon"
4292
- className="size-7 cursor-pointer text-destructive hover:text-destructive"
4293
- onClick={() =>
4294
- setRemovingAssignmentId(
4295
- assignment.collaboratorId
4296
- )
4297
- }
4298
- >
4299
- <Trash2 className="size-3.5" />
4300
- </Button>
4301
- </div>
4302
- ) : null}
4303
3963
  </div>
4304
3964
  </div>
4305
3965
  </div>
@@ -4600,347 +4260,6 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
4600
4260
  }
4601
4261
  />
4602
4262
 
4603
- {/* Assignment Add/Edit Sheet */}
4604
- <Sheet
4605
- open={assignmentSheetOpen}
4606
- onOpenChange={(open) => {
4607
- if (!open) {
4608
- setAssignmentSheetOpen(false);
4609
- setSelectedAssignmentCollaborator(null);
4610
- }
4611
- }}
4612
- >
4613
- <SheetContent className="w-full overflow-y-auto sm:max-w-[min(92vw,28rem)]">
4614
- <SheetHeader>
4615
- <SheetTitle>
4616
- {editingAssignment
4617
- ? t('teamPanel.editTitle')
4618
- : t('teamPanel.addTitle')}
4619
- </SheetTitle>
4620
- <SheetDescription>
4621
- {t('teamPanel.formDescription')}
4622
- </SheetDescription>
4623
- </SheetHeader>
4624
- <div className="mt-6 space-y-4 px-1 sm:px-4">
4625
- {!editingAssignment ? (
4626
- <div className="space-y-2">
4627
- <Label className="text-sm font-medium">
4628
- {commonT('labels.collaborator')}
4629
- </Label>
4630
- <EntityPicker<OperationsCollaborator>
4631
- value={
4632
- assignmentFormData.collaboratorId
4633
- ? Number(assignmentFormData.collaboratorId)
4634
- : null
4635
- }
4636
- onChange={(val, option) => {
4637
- const collaborator = option ?? null;
4638
- setSelectedAssignmentCollaborator(collaborator);
4639
- setAssignmentFormData((prev) => {
4640
- const collaboratorId = val ? String(val) : '';
4641
- if (!collaboratorId) {
4642
- return {
4643
- ...prev,
4644
- collaboratorId: '',
4645
- weeklyHours: '',
4646
- allocationPercent: '',
4647
- };
4648
- }
4649
-
4650
- const capacity = collaborator?.weeklyCapacityHours;
4651
- if (!capacity || capacity <= 0) {
4652
- return {
4653
- ...prev,
4654
- collaboratorId,
4655
- };
4656
- }
4657
-
4658
- const parsedHours = parseNumberInput(prev.weeklyHours);
4659
- const parsedPercent = parseNumberInput(
4660
- prev.allocationPercent
4661
- );
4662
-
4663
- if (parsedHours != null) {
4664
- return {
4665
- ...prev,
4666
- collaboratorId,
4667
- ...syncAssignmentFromWeeklyHours(
4668
- prev.weeklyHours,
4669
- prev,
4670
- collaborator
4671
- ),
4672
- };
4673
- }
4674
-
4675
- if (parsedPercent != null) {
4676
- return {
4677
- ...prev,
4678
- collaboratorId,
4679
- ...syncAssignmentFromAllocation(
4680
- prev.allocationPercent,
4681
- prev,
4682
- collaborator
4683
- ),
4684
- };
4685
- }
4686
-
4687
- return {
4688
- ...prev,
4689
- collaboratorId,
4690
- weeklyHours: formatAssignmentNumericValue(capacity),
4691
- allocationPercent: '100',
4692
- };
4693
- });
4694
- }}
4695
- placeholder={commonT('labels.collaborator')}
4696
- searchPlaceholder={commonT('labels.collaborator')}
4697
- loadOptions={loadCollaboratorOptions}
4698
- getOptionValue={(opt) => opt.id}
4699
- getOptionLabel={(opt) => opt.displayName}
4700
- renderOption={({ option }) => (
4701
- <div className="flex min-w-0 items-center gap-2.5">
4702
- <Avatar className="size-6 shrink-0">
4703
- <AvatarImage
4704
- src={
4705
- getUserPhotoUrl(option.userPhotoId) ||
4706
- getPersonAvatarUrl(option.personAvatarId)
4707
- }
4708
- alt={option.displayName}
4709
- />
4710
- <AvatarFallback className="text-[9px]">
4711
- {getInitials(option.displayName)}
4712
- </AvatarFallback>
4713
- </Avatar>
4714
- <div className="min-w-0">
4715
- <div className="truncate text-sm">
4716
- {option.displayName}
4717
- </div>
4718
- {option.department ? (
4719
- <div className="truncate text-xs text-muted-foreground">
4720
- {option.department}
4721
- </div>
4722
- ) : null}
4723
- </div>
4724
- </div>
4725
- )}
4726
- valueType="number"
4727
- clearable
4728
- allowEmptySelection
4729
- showCreateButton
4730
- entityLabel={commonT('labels.collaborator').toLowerCase()}
4731
- createActionLabel={`${commonT('actions.create')} ${commonT(
4732
- 'labels.collaborator'
4733
- ).toLowerCase()}`}
4734
- createTitle={`${commonT('actions.create')} ${commonT(
4735
- 'labels.collaborator'
4736
- ).toLowerCase()}`}
4737
- createDescription={t('teamPanel.formDescription')}
4738
- createFields={[
4739
- {
4740
- name: 'displayName',
4741
- label: collaboratorFormT('fields.displayName'),
4742
- placeholder: collaboratorFormT('fields.displayName'),
4743
- required: true,
4744
- },
4745
- {
4746
- name: 'weeklyCapacityHours',
4747
- label: collaboratorFormT('fields.weeklyCapacityHours'),
4748
- placeholder: '40',
4749
- type: 'number',
4750
- },
4751
- ]}
4752
- mapSearchToCreateValues={(search) => ({
4753
- displayName: search,
4754
- weeklyCapacityHours: '40',
4755
- })}
4756
- onCreate={createAssignmentCollaborator}
4757
- />
4758
- </div>
4759
- ) : (
4760
- <div className="flex items-center gap-3 rounded-xl border bg-muted/30 p-3">
4761
- <Avatar className="size-10 border bg-muted">
4762
- <AvatarImage
4763
- src={
4764
- getUserPhotoUrl(editingAssignment.userPhotoId) ||
4765
- getPersonAvatarUrl(editingAssignment.personAvatarId)
4766
- }
4767
- alt={editingAssignment.collaboratorName}
4768
- />
4769
- <AvatarFallback className="text-xs">
4770
- {getInitials(editingAssignment.collaboratorName)}
4771
- </AvatarFallback>
4772
- </Avatar>
4773
- <div className="min-w-0">
4774
- <div className="truncate text-sm font-semibold">
4775
- {editingAssignment.collaboratorName}
4776
- </div>
4777
- {editingAssignment.roleLabel ? (
4778
- <div className="truncate text-xs text-muted-foreground">
4779
- {editingAssignment.roleLabel}
4780
- </div>
4781
- ) : null}
4782
- </div>
4783
- </div>
4784
- )}
4785
- <div className="space-y-2">
4786
- <Label className="text-sm font-medium">
4787
- {commonT('labels.allocationPercent')}
4788
- </Label>
4789
- <Input
4790
- type="number"
4791
- min="0"
4792
- max="200"
4793
- value={assignmentFormData.allocationPercent}
4794
- onChange={(e) =>
4795
- setAssignmentFormData((prev) => ({
4796
- ...prev,
4797
- ...syncAssignmentFromAllocation(
4798
- e.target.value,
4799
- prev,
4800
- selectedAssignmentCollaborator
4801
- ),
4802
- }))
4803
- }
4804
- placeholder="100"
4805
- />
4806
- </div>
4807
- <div className="space-y-2">
4808
- <Label className="text-sm font-medium">
4809
- {commonT('labels.weeklyCapacity')}
4810
- </Label>
4811
- <Input
4812
- type="number"
4813
- min="0"
4814
- value={assignmentFormData.weeklyHours}
4815
- onChange={(e) =>
4816
- setAssignmentFormData((prev) => ({
4817
- ...prev,
4818
- ...syncAssignmentFromWeeklyHours(
4819
- e.target.value,
4820
- prev,
4821
- selectedAssignmentCollaborator
4822
- ),
4823
- }))
4824
- }
4825
- placeholder="40"
4826
- />
4827
- </div>
4828
- <div className="space-y-2">
4829
- <Label className="text-sm font-medium">
4830
- {commonT('labels.status')}
4831
- </Label>
4832
- <Select
4833
- value={assignmentFormData.status}
4834
- onValueChange={(val) =>
4835
- setAssignmentFormData((prev) => ({ ...prev, status: val }))
4836
- }
4837
- >
4838
- <SelectTrigger className="cursor-pointer">
4839
- <SelectValue />
4840
- </SelectTrigger>
4841
- <SelectContent>
4842
- <SelectItem value="active">
4843
- {formatEnumLabel('active')}
4844
- </SelectItem>
4845
- <SelectItem value="inactive">
4846
- {formatEnumLabel('inactive')}
4847
- </SelectItem>
4848
- <SelectItem value="completed">
4849
- {formatEnumLabel('completed')}
4850
- </SelectItem>
4851
- </SelectContent>
4852
- </Select>
4853
- </div>
4854
- <div className="grid grid-cols-2 gap-3">
4855
- <div className="space-y-2">
4856
- <Label className="text-sm font-medium">
4857
- {commonT('labels.startDate')}
4858
- </Label>
4859
- <Input
4860
- type="date"
4861
- value={assignmentFormData.startDate}
4862
- onChange={(e) =>
4863
- setAssignmentFormData((prev) => ({
4864
- ...prev,
4865
- startDate: e.target.value,
4866
- }))
4867
- }
4868
- />
4869
- </div>
4870
- <div className="space-y-2">
4871
- <Label className="text-sm font-medium">
4872
- {commonT('labels.endDate')}
4873
- </Label>
4874
- <Input
4875
- type="date"
4876
- value={assignmentFormData.endDate}
4877
- onChange={(e) =>
4878
- setAssignmentFormData((prev) => ({
4879
- ...prev,
4880
- endDate: e.target.value,
4881
- }))
4882
- }
4883
- />
4884
- </div>
4885
- </div>
4886
- <div className="flex gap-2 pt-2">
4887
- <Button
4888
- variant="outline"
4889
- className="flex-1 cursor-pointer"
4890
- onClick={() => setAssignmentSheetOpen(false)}
4891
- disabled={savingAssignment}
4892
- >
4893
- {commonT('actions.cancel')}
4894
- </Button>
4895
- <Button
4896
- className="flex-1 cursor-pointer"
4897
- disabled={
4898
- savingAssignment ||
4899
- (!editingAssignment && !assignmentFormData.collaboratorId)
4900
- }
4901
- onClick={() => void handleSaveAssignment()}
4902
- >
4903
- {savingAssignment ? (
4904
- <Loader2 className="mr-2 size-4 animate-spin" />
4905
- ) : null}
4906
- {commonT('actions.save')}
4907
- </Button>
4908
- </div>
4909
- </div>
4910
- </SheetContent>
4911
- </Sheet>
4912
-
4913
- {/* Remove Assignment Confirm Dialog */}
4914
- <Dialog
4915
- open={removingAssignmentId !== null}
4916
- onOpenChange={(open) => {
4917
- if (!open) setRemovingAssignmentId(null);
4918
- }}
4919
- >
4920
- <DialogContent className="sm:max-w-sm">
4921
- <DialogHeader>
4922
- <DialogTitle>{t('teamPanel.removeTitle')}</DialogTitle>
4923
- <DialogDescription>
4924
- {t('teamPanel.removeDescription')}
4925
- </DialogDescription>
4926
- </DialogHeader>
4927
- <DialogFooter className="mt-4">
4928
- <Button
4929
- variant="outline"
4930
- onClick={() => setRemovingAssignmentId(null)}
4931
- >
4932
- {commonT('actions.cancel')}
4933
- </Button>
4934
- <Button
4935
- variant="destructive"
4936
- onClick={() => void handleConfirmRemoveAssignment()}
4937
- >
4938
- {commonT('actions.delete')}
4939
- </Button>
4940
- </DialogFooter>
4941
- </DialogContent>
4942
- </Dialog>
4943
-
4944
4263
  <TimesheetEntryCreateSheet
4945
4264
  open={isTimesheetEntrySheetOpen}
4946
4265
  onOpenChange={(open) => {
@@ -5022,10 +4341,7 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
5022
4341
  )
5023
4342
  : undefined
5024
4343
  }
5025
- onCountChanged={() => {
5026
- setBoardState(null);
5027
- void refetchTasks();
5028
- }}
4344
+ onCountChanged={() => { setBoardState(null); void refetchTasks(); }}
5029
4345
  onSaved={() => {
5030
4346
  setBoardState(null);
5031
4347
  void refetchTasks();