@hed-hog/operations 0.0.331 → 0.0.332
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.
- package/dist/controllers/operations-collaborators.controller.d.ts +54 -0
- package/dist/controllers/operations-collaborators.controller.d.ts.map +1 -1
- package/dist/controllers/operations-collaborators.controller.js +100 -0
- package/dist/controllers/operations-collaborators.controller.js.map +1 -1
- package/dist/dto/create-collaborator-invoice.dto.d.ts +11 -0
- package/dist/dto/create-collaborator-invoice.dto.d.ts.map +1 -0
- package/dist/dto/create-collaborator-invoice.dto.js +55 -0
- package/dist/dto/create-collaborator-invoice.dto.js.map +1 -0
- package/dist/dto/create-collaborator-payment.dto.d.ts +10 -0
- package/dist/dto/create-collaborator-payment.dto.d.ts.map +1 -0
- package/dist/dto/create-collaborator-payment.dto.js +50 -0
- package/dist/dto/create-collaborator-payment.dto.js.map +1 -0
- package/dist/dto/list-collaborator-invoice.dto.d.ts +4 -0
- package/dist/dto/list-collaborator-invoice.dto.d.ts.map +1 -0
- package/dist/dto/list-collaborator-invoice.dto.js +8 -0
- package/dist/dto/list-collaborator-invoice.dto.js.map +1 -0
- package/dist/dto/list-collaborator-payment.dto.d.ts +4 -0
- package/dist/dto/list-collaborator-payment.dto.d.ts.map +1 -0
- package/dist/dto/list-collaborator-payment.dto.js +8 -0
- package/dist/dto/list-collaborator-payment.dto.js.map +1 -0
- package/dist/dto/update-collaborator-invoice.dto.d.ts +6 -0
- package/dist/dto/update-collaborator-invoice.dto.d.ts.map +1 -0
- package/dist/dto/update-collaborator-invoice.dto.js +9 -0
- package/dist/dto/update-collaborator-invoice.dto.js.map +1 -0
- package/dist/dto/update-collaborator-payment.dto.d.ts +6 -0
- package/dist/dto/update-collaborator-payment.dto.d.ts.map +1 -0
- package/dist/dto/update-collaborator-payment.dto.js +9 -0
- package/dist/dto/update-collaborator-payment.dto.js.map +1 -0
- package/dist/operations.service.d.ts +76 -0
- package/dist/operations.service.d.ts.map +1 -1
- package/dist/operations.service.js +235 -5
- package/dist/operations.service.js.map +1 -1
- package/hedhog/data/menu.yaml +27 -8
- package/hedhog/data/route.yaml +72 -0
- package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +39 -3
- package/hedhog/frontend/app/_components/collaborator-invoices-tab.tsx.ejs +443 -0
- package/hedhog/frontend/app/_components/collaborator-payment-history-tab.tsx.ejs +429 -0
- package/hedhog/frontend/app/_components/my-project-summary-screen.tsx.ejs +86 -87
- package/hedhog/frontend/app/_components/project-assignments-tab.tsx.ejs +218 -10
- package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +710 -26
- package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +158 -38
- package/hedhog/frontend/app/_components/task-detail-sheet.tsx.ejs +807 -803
- package/hedhog/frontend/app/_lib/api.ts.ejs +631 -480
- package/hedhog/frontend/app/_lib/types.ts.ejs +6 -5
- package/hedhog/frontend/app/_lib/utils/task-ui.ts.ejs +18 -0
- package/hedhog/frontend/app/my-projects/page.tsx.ejs +16 -2
- package/hedhog/frontend/app/my-tasks/page.tsx.ejs +95 -157
- package/hedhog/frontend/app/projects/page.tsx.ejs +42 -6
- package/hedhog/frontend/app/tasks-gantt/page.tsx.ejs +953 -0
- package/hedhog/frontend/messages/en.json +96 -2
- package/hedhog/frontend/messages/pt.json +96 -2
- package/hedhog/table/operations_collaborator_invoice.yaml +35 -0
- package/hedhog/table/operations_collaborator_payment.yaml +32 -0
- package/package.json +5 -5
- package/src/controllers/operations-collaborators.controller.ts +117 -8
- package/src/dto/create-collaborator-invoice.dto.ts +39 -0
- package/src/dto/create-collaborator-payment.dto.ts +35 -0
- package/src/dto/list-collaborator-invoice.dto.ts +3 -0
- package/src/dto/list-collaborator-payment.dto.ts +3 -0
- package/src/dto/update-collaborator-invoice.dto.ts +6 -0
- package/src/dto/update-collaborator-payment.dto.ts +6 -0
- package/src/operations.service.ts +328 -5
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
DialogHeader,
|
|
19
19
|
DialogTitle,
|
|
20
20
|
} from '@/components/ui/dialog';
|
|
21
|
+
import { EntityPicker } from '@/components/ui/entity-picker';
|
|
21
22
|
import { Input } from '@/components/ui/input';
|
|
22
23
|
import { Label } from '@/components/ui/label';
|
|
23
24
|
import { Progress } from '@/components/ui/progress';
|
|
@@ -87,7 +88,6 @@ import {
|
|
|
87
88
|
Gauge,
|
|
88
89
|
GitCommitHorizontal,
|
|
89
90
|
HeartPulse,
|
|
90
|
-
History,
|
|
91
91
|
LineChart as LineChartIcon,
|
|
92
92
|
Loader2,
|
|
93
93
|
MessageSquare,
|
|
@@ -101,6 +101,7 @@ import {
|
|
|
101
101
|
Timer,
|
|
102
102
|
Trash2,
|
|
103
103
|
TrendingUp,
|
|
104
|
+
UserPlus,
|
|
104
105
|
Users,
|
|
105
106
|
type LucideIcon,
|
|
106
107
|
} from 'lucide-react';
|
|
@@ -133,8 +134,11 @@ import {
|
|
|
133
134
|
useValuesVisibility,
|
|
134
135
|
} from '../_lib/hooks/use-values-visibility';
|
|
135
136
|
import type {
|
|
137
|
+
OperationsCollaborator,
|
|
138
|
+
OperationsCollaboratorDetails,
|
|
136
139
|
OperationsProjectDetails,
|
|
137
140
|
OperationsTaskOption,
|
|
141
|
+
PaginatedResponse,
|
|
138
142
|
} from '../_lib/types';
|
|
139
143
|
import {
|
|
140
144
|
formatCurrency,
|
|
@@ -145,19 +149,16 @@ import {
|
|
|
145
149
|
formatPercent,
|
|
146
150
|
getStatusBadgeClass,
|
|
147
151
|
} from '../_lib/utils/format';
|
|
152
|
+
import { parseNumberInput } from '../_lib/utils/forms';
|
|
148
153
|
import { OperationsHeader } from './operations-header';
|
|
149
154
|
import { ProjectCostsSection } from './project-costs-section';
|
|
155
|
+
import { ProjectFileAttachments } from './project-file-attachments';
|
|
150
156
|
import { ProjectFormScreen } from './project-form-screen';
|
|
151
157
|
import { SectionCard } from './section-card';
|
|
152
158
|
import { StatusBadge } from './status-badge';
|
|
153
|
-
import {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
} from './task-detail-sheet';
|
|
157
|
-
import { ProjectFileAttachments } from './project-file-attachments';
|
|
158
|
-
import { TaskFileAttachments } from './task-file-attachments';
|
|
159
|
-
import { TaskFormSheet } from './task-form-sheet';
|
|
160
|
-
import { TimesheetEntryCreateSheet } from './timesheet-entry-create-sheet';
|
|
159
|
+
import { TaskDetailSheet, type TaskDetailSheetData } from './task-detail-sheet';
|
|
160
|
+
import { TaskFormSheet } from './task-form-sheet';
|
|
161
|
+
import { TimesheetEntryCreateSheet } from './timesheet-entry-create-sheet';
|
|
161
162
|
|
|
162
163
|
type BoardColumnId = 'todo' | 'doing' | 'review' | 'done';
|
|
163
164
|
|
|
@@ -204,6 +205,15 @@ type TimesheetEntryPrefill = {
|
|
|
204
205
|
taskLabel: string;
|
|
205
206
|
};
|
|
206
207
|
|
|
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
|
+
|
|
207
217
|
const KANBAN_COLUMNS: Array<{ id: BoardColumnId; label: string }> = [
|
|
208
218
|
{ id: 'todo', label: 'Backlog' },
|
|
209
219
|
{ id: 'doing', label: 'Em execução' },
|
|
@@ -386,7 +396,7 @@ function getInitials(value?: string | null) {
|
|
|
386
396
|
function getPersonAvatarUrl(avatarId?: number | null) {
|
|
387
397
|
return typeof avatarId === 'number' && avatarId > 0
|
|
388
398
|
? `${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${avatarId}`
|
|
389
|
-
:
|
|
399
|
+
: undefined;
|
|
390
400
|
}
|
|
391
401
|
|
|
392
402
|
function getUserPhotoUrl(photoId?: number | null) {
|
|
@@ -1092,6 +1102,7 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
1092
1102
|
const commonT = useTranslations('operations.Common');
|
|
1093
1103
|
const formT = useTranslations('operations.ProjectFormPage');
|
|
1094
1104
|
const contractT = useTranslations('operations.ContractFormPage');
|
|
1105
|
+
const collaboratorFormT = useTranslations('operations.CollaboratorFormPage');
|
|
1095
1106
|
const { request, currentLocaleCode, getSettingValue } = useApp();
|
|
1096
1107
|
const access = useOperationsAccess();
|
|
1097
1108
|
const isLimitedView = !access.isDirector && !access.isSupervisor;
|
|
@@ -1299,6 +1310,26 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
1299
1310
|
const [timesheetPrefill, setTimesheetPrefill] =
|
|
1300
1311
|
useState<TimesheetEntryPrefill | null>(null);
|
|
1301
1312
|
|
|
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
|
+
|
|
1302
1333
|
const apiTasks = useMemo(() => rawTasks.map(apiTaskToBoardTask), [rawTasks]);
|
|
1303
1334
|
const archivedTasks = useMemo(
|
|
1304
1335
|
() =>
|
|
@@ -1506,7 +1537,11 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
1506
1537
|
const handleDeleteProject = useCallback(async () => {
|
|
1507
1538
|
setDeletingProject(true);
|
|
1508
1539
|
try {
|
|
1509
|
-
await mutateOperations(
|
|
1540
|
+
await mutateOperations(
|
|
1541
|
+
request,
|
|
1542
|
+
`/operations/projects/${projectId}`,
|
|
1543
|
+
'DELETE'
|
|
1544
|
+
);
|
|
1510
1545
|
router.push('/operations/projects');
|
|
1511
1546
|
} catch {
|
|
1512
1547
|
// ignore
|
|
@@ -1540,6 +1575,245 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
1540
1575
|
[commonT, project, projectId]
|
|
1541
1576
|
);
|
|
1542
1577
|
|
|
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
|
+
|
|
1543
1817
|
const allocationChartData = useMemo(() => {
|
|
1544
1818
|
if (projectStats?.allocationByCollaborator?.length) {
|
|
1545
1819
|
return projectStats.allocationByCollaborator;
|
|
@@ -2140,10 +2414,13 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
2140
2414
|
<TooltipTrigger asChild>
|
|
2141
2415
|
<div className="flex cursor-default items-center gap-1.5 border-r px-3 py-2 transition hover:bg-muted/30">
|
|
2142
2416
|
<Avatar className="size-5 shrink-0 border bg-muted">
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2417
|
+
<AvatarImage
|
|
2418
|
+
src={
|
|
2419
|
+
getUserPhotoUrl(project.clientUserPhotoId) ||
|
|
2420
|
+
getPersonAvatarUrl(project.clientAvatarId)
|
|
2421
|
+
}
|
|
2422
|
+
alt={project.clientName || ''}
|
|
2423
|
+
/>
|
|
2147
2424
|
<AvatarFallback className="text-[9px]">
|
|
2148
2425
|
{getInitials(project.clientName)}
|
|
2149
2426
|
</AvatarFallback>
|
|
@@ -2462,7 +2739,10 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
2462
2739
|
<div className="flex items-center gap-2">
|
|
2463
2740
|
<Avatar className="h-8 w-8 border border-border/60 bg-muted">
|
|
2464
2741
|
<AvatarImage
|
|
2465
|
-
src={
|
|
2742
|
+
src={
|
|
2743
|
+
getUserPhotoUrl(project.clientUserPhotoId) ||
|
|
2744
|
+
getPersonAvatarUrl(project.clientAvatarId)
|
|
2745
|
+
}
|
|
2466
2746
|
alt={project.clientName || commonT('labels.client')}
|
|
2467
2747
|
/>
|
|
2468
2748
|
<AvatarFallback className="bg-muted text-xs font-semibold text-foreground">
|
|
@@ -2604,13 +2884,32 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
2604
2884
|
<div className="font-medium">
|
|
2605
2885
|
{project.relatedContract.name}
|
|
2606
2886
|
</div>
|
|
2607
|
-
<div className="text-sm text-muted-foreground">
|
|
2608
|
-
|
|
2609
|
-
project.relatedContract.code
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
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}
|
|
2614
2913
|
</div>
|
|
2615
2914
|
</div>
|
|
2616
2915
|
<div className="flex items-center gap-3">
|
|
@@ -3082,7 +3381,7 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
3082
3381
|
{(isOver) => (
|
|
3083
3382
|
<div
|
|
3084
3383
|
className={[
|
|
3085
|
-
'flex min-h-
|
|
3384
|
+
'flex min-h-48 max-h-160 flex-col rounded-3xl border bg-linear-to-b p-3 transition-all',
|
|
3086
3385
|
getColumnClassName(column.id),
|
|
3087
3386
|
isOver
|
|
3088
3387
|
? 'border-primary shadow-lg ring-2 ring-primary/15'
|
|
@@ -3125,7 +3424,7 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
3125
3424
|
</div>
|
|
3126
3425
|
</div>
|
|
3127
3426
|
|
|
3128
|
-
<div className="flex flex-1 flex-col gap-2">
|
|
3427
|
+
<div className="flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto pb-1 pr-0.5">
|
|
3129
3428
|
<AnimatePresence initial={false}>
|
|
3130
3429
|
{filteredTaskColumns[column.id].map((task) => {
|
|
3131
3430
|
const tags = getTaskTags(task);
|
|
@@ -3853,6 +4152,19 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
3853
4152
|
'rounded-2xl border bg-card p-4 shadow-sm',
|
|
3854
4153
|
isLimitedView ? 'xl:col-span-12' : 'xl:col-span-8',
|
|
3855
4154
|
].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
|
+
}
|
|
3856
4168
|
>
|
|
3857
4169
|
{project.assignments.length > 0 ? (
|
|
3858
4170
|
<div className="space-y-4">
|
|
@@ -3960,6 +4272,34 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
3960
4272
|
<ToneIcon className="size-3" />
|
|
3961
4273
|
{t(`teamPanel.status.${tone.labelKey}`)}
|
|
3962
4274
|
</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}
|
|
3963
4303
|
</div>
|
|
3964
4304
|
</div>
|
|
3965
4305
|
</div>
|
|
@@ -4260,6 +4600,347 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
4260
4600
|
}
|
|
4261
4601
|
/>
|
|
4262
4602
|
|
|
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
|
+
|
|
4263
4944
|
<TimesheetEntryCreateSheet
|
|
4264
4945
|
open={isTimesheetEntrySheetOpen}
|
|
4265
4946
|
onOpenChange={(open) => {
|
|
@@ -4341,7 +5022,10 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
4341
5022
|
)
|
|
4342
5023
|
: undefined
|
|
4343
5024
|
}
|
|
4344
|
-
onCountChanged={() => {
|
|
5025
|
+
onCountChanged={() => {
|
|
5026
|
+
setBoardState(null);
|
|
5027
|
+
void refetchTasks();
|
|
5028
|
+
}}
|
|
4345
5029
|
onSaved={() => {
|
|
4346
5030
|
setBoardState(null);
|
|
4347
5031
|
void refetchTasks();
|