@hed-hog/operations 0.0.338 → 0.0.349
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 +73 -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/controllers/operations-contracts.controller.d.ts +15 -15
- package/dist/controllers/operations-projects.controller.d.ts +3 -0
- package/dist/controllers/operations-projects.controller.d.ts.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 +98 -0
- package/dist/operations.service.d.ts.map +1 -1
- package/dist/operations.service.js +226 -3
- package/dist/operations.service.js.map +1 -1
- package/hedhog/data/menu.yaml +32 -11
- package/hedhog/data/route.yaml +72 -0
- package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +38 -0
- 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/project-assignments-tab.tsx.ejs +212 -10
- package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +668 -11
- package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +182 -28
- package/hedhog/frontend/app/_components/task-detail-sheet.tsx.ejs +28 -7
- package/hedhog/frontend/app/_lib/api.ts.ejs +151 -0
- package/hedhog/frontend/app/_lib/types.ts.ejs +1 -0
- package/hedhog/frontend/app/_lib/utils/task-ui.ts.ejs +18 -0
- 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 +4 -4
- package/src/controllers/operations-collaborators.controller.ts +109 -0
- 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 +318 -4
|
@@ -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,17 +149,14 @@ 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
|
-
TaskDetailSheet,
|
|
155
|
-
type TaskDetailSheetData,
|
|
156
|
-
} from './task-detail-sheet';
|
|
157
|
-
import { ProjectFileAttachments } from './project-file-attachments';
|
|
158
|
-
import { TaskFileAttachments } from './task-file-attachments';
|
|
159
|
+
import { TaskDetailSheet, type TaskDetailSheetData } from './task-detail-sheet';
|
|
159
160
|
import { TaskFormSheet } from './task-form-sheet';
|
|
160
161
|
import { TimesheetEntryCreateSheet } from './timesheet-entry-create-sheet';
|
|
161
162
|
|
|
@@ -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' },
|
|
@@ -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;
|
|
@@ -1298,6 +1309,24 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
1298
1309
|
useState(false);
|
|
1299
1310
|
const [timesheetPrefill, setTimesheetPrefill] =
|
|
1300
1311
|
useState<TimesheetEntryPrefill | null>(null);
|
|
1312
|
+
const [assignmentSheetOpen, setAssignmentSheetOpen] = useState(false);
|
|
1313
|
+
const [editingAssignment, setEditingAssignment] = useState<
|
|
1314
|
+
OperationsProjectDetails['assignments'][0] | null
|
|
1315
|
+
>(null);
|
|
1316
|
+
const [assignmentFormData, setAssignmentFormData] = useState({
|
|
1317
|
+
collaboratorId: '',
|
|
1318
|
+
weeklyHours: '',
|
|
1319
|
+
allocationPercent: '',
|
|
1320
|
+
status: 'active',
|
|
1321
|
+
startDate: '',
|
|
1322
|
+
endDate: '',
|
|
1323
|
+
});
|
|
1324
|
+
const [selectedAssignmentCollaborator, setSelectedAssignmentCollaborator] =
|
|
1325
|
+
useState<OperationsCollaborator | null>(null);
|
|
1326
|
+
const [savingAssignment, setSavingAssignment] = useState(false);
|
|
1327
|
+
const [removingAssignmentId, setRemovingAssignmentId] = useState<
|
|
1328
|
+
number | null
|
|
1329
|
+
>(null);
|
|
1301
1330
|
|
|
1302
1331
|
const apiTasks = useMemo(() => rawTasks.map(apiTaskToBoardTask), [rawTasks]);
|
|
1303
1332
|
const archivedTasks = useMemo(
|
|
@@ -1506,7 +1535,11 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
1506
1535
|
const handleDeleteProject = useCallback(async () => {
|
|
1507
1536
|
setDeletingProject(true);
|
|
1508
1537
|
try {
|
|
1509
|
-
await mutateOperations(
|
|
1538
|
+
await mutateOperations(
|
|
1539
|
+
request,
|
|
1540
|
+
`/operations/projects/${projectId}`,
|
|
1541
|
+
'DELETE'
|
|
1542
|
+
);
|
|
1510
1543
|
router.push('/operations/projects');
|
|
1511
1544
|
} catch {
|
|
1512
1545
|
// ignore
|
|
@@ -1540,6 +1573,245 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
1540
1573
|
[commonT, project, projectId]
|
|
1541
1574
|
);
|
|
1542
1575
|
|
|
1576
|
+
const loadCollaboratorOptions = useCallback(
|
|
1577
|
+
async ({
|
|
1578
|
+
page,
|
|
1579
|
+
pageSize,
|
|
1580
|
+
search,
|
|
1581
|
+
}: {
|
|
1582
|
+
page: number;
|
|
1583
|
+
pageSize: number;
|
|
1584
|
+
search: string;
|
|
1585
|
+
}) => {
|
|
1586
|
+
const params = new URLSearchParams({
|
|
1587
|
+
page: String(page),
|
|
1588
|
+
pageSize: String(pageSize),
|
|
1589
|
+
sortField: 'displayName',
|
|
1590
|
+
sortOrder: 'asc',
|
|
1591
|
+
});
|
|
1592
|
+
if (search.trim()) params.set('search', search.trim());
|
|
1593
|
+
const result = await fetchOperations<
|
|
1594
|
+
PaginatedResponse<OperationsCollaborator>
|
|
1595
|
+
>(request, `/operations/collaborators?${params.toString()}`);
|
|
1596
|
+
const items = result?.data ?? [];
|
|
1597
|
+
const total = result?.total ?? 0;
|
|
1598
|
+
return { items, hasMore: page * pageSize < total };
|
|
1599
|
+
},
|
|
1600
|
+
[request]
|
|
1601
|
+
);
|
|
1602
|
+
|
|
1603
|
+
const loadAssignmentCollaboratorById = useCallback(
|
|
1604
|
+
async (collaboratorId: number) => {
|
|
1605
|
+
const collaborator = await fetchOperations<OperationsCollaboratorDetails>(
|
|
1606
|
+
request,
|
|
1607
|
+
`/operations/collaborators/${collaboratorId}`
|
|
1608
|
+
);
|
|
1609
|
+
setSelectedAssignmentCollaborator(collaborator);
|
|
1610
|
+
return collaborator;
|
|
1611
|
+
},
|
|
1612
|
+
[request]
|
|
1613
|
+
);
|
|
1614
|
+
|
|
1615
|
+
const createAssignmentCollaborator = useCallback(
|
|
1616
|
+
async (values: Record<string, string>) => {
|
|
1617
|
+
const displayName = values.displayName?.trim() ?? '';
|
|
1618
|
+
const weeklyCapacityHours = parseNumberInput(
|
|
1619
|
+
values.weeklyCapacityHours ?? ''
|
|
1620
|
+
);
|
|
1621
|
+
|
|
1622
|
+
if (!displayName) {
|
|
1623
|
+
return null;
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
return mutateOperations<OperationsCollaborator>(
|
|
1627
|
+
request,
|
|
1628
|
+
'/operations/collaborators',
|
|
1629
|
+
'POST',
|
|
1630
|
+
{
|
|
1631
|
+
displayName,
|
|
1632
|
+
weeklyCapacityHours,
|
|
1633
|
+
status: 'active',
|
|
1634
|
+
autoGenerateContractDraft: false,
|
|
1635
|
+
}
|
|
1636
|
+
);
|
|
1637
|
+
},
|
|
1638
|
+
[request]
|
|
1639
|
+
);
|
|
1640
|
+
|
|
1641
|
+
const syncAssignmentFromWeeklyHours = useCallback(
|
|
1642
|
+
(
|
|
1643
|
+
weeklyHours: string,
|
|
1644
|
+
currentFormData: typeof assignmentFormData,
|
|
1645
|
+
collaborator?: OperationsCollaborator | null
|
|
1646
|
+
) => {
|
|
1647
|
+
const capacity = collaborator?.weeklyCapacityHours;
|
|
1648
|
+
if (!capacity || capacity <= 0) {
|
|
1649
|
+
return {
|
|
1650
|
+
weeklyHours,
|
|
1651
|
+
allocationPercent: currentFormData.allocationPercent,
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
const parsedHours = parseNumberInput(weeklyHours);
|
|
1656
|
+
if (parsedHours == null) {
|
|
1657
|
+
return { weeklyHours, allocationPercent: '' };
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
return {
|
|
1661
|
+
weeklyHours,
|
|
1662
|
+
allocationPercent: formatAssignmentNumericValue(
|
|
1663
|
+
(parsedHours / capacity) * 100
|
|
1664
|
+
),
|
|
1665
|
+
};
|
|
1666
|
+
},
|
|
1667
|
+
[]
|
|
1668
|
+
);
|
|
1669
|
+
|
|
1670
|
+
const syncAssignmentFromAllocation = useCallback(
|
|
1671
|
+
(
|
|
1672
|
+
allocationPercent: string,
|
|
1673
|
+
currentFormData: typeof assignmentFormData,
|
|
1674
|
+
collaborator?: OperationsCollaborator | null
|
|
1675
|
+
) => {
|
|
1676
|
+
const capacity = collaborator?.weeklyCapacityHours;
|
|
1677
|
+
if (!capacity || capacity <= 0) {
|
|
1678
|
+
return {
|
|
1679
|
+
weeklyHours: currentFormData.weeklyHours,
|
|
1680
|
+
allocationPercent,
|
|
1681
|
+
};
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
const parsedPercent = parseNumberInput(allocationPercent);
|
|
1685
|
+
if (parsedPercent == null) {
|
|
1686
|
+
return { weeklyHours: '', allocationPercent };
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
return {
|
|
1690
|
+
weeklyHours: formatAssignmentNumericValue(
|
|
1691
|
+
(parsedPercent / 100) * capacity
|
|
1692
|
+
),
|
|
1693
|
+
allocationPercent,
|
|
1694
|
+
};
|
|
1695
|
+
},
|
|
1696
|
+
[]
|
|
1697
|
+
);
|
|
1698
|
+
|
|
1699
|
+
const openAddAssignment = useCallback(() => {
|
|
1700
|
+
setEditingAssignment(null);
|
|
1701
|
+
setSelectedAssignmentCollaborator(null);
|
|
1702
|
+
setAssignmentFormData({
|
|
1703
|
+
collaboratorId: '',
|
|
1704
|
+
weeklyHours: '',
|
|
1705
|
+
allocationPercent: '',
|
|
1706
|
+
status: 'active',
|
|
1707
|
+
startDate: '',
|
|
1708
|
+
endDate: '',
|
|
1709
|
+
});
|
|
1710
|
+
setAssignmentSheetOpen(true);
|
|
1711
|
+
}, []);
|
|
1712
|
+
|
|
1713
|
+
const openEditAssignment = useCallback(
|
|
1714
|
+
(assignment: OperationsProjectDetails['assignments'][0]) => {
|
|
1715
|
+
setEditingAssignment(assignment);
|
|
1716
|
+
setSelectedAssignmentCollaborator(null);
|
|
1717
|
+
setAssignmentFormData({
|
|
1718
|
+
collaboratorId: String(assignment.collaboratorId),
|
|
1719
|
+
weeklyHours:
|
|
1720
|
+
assignment.weeklyHours != null ? String(assignment.weeklyHours) : '',
|
|
1721
|
+
allocationPercent:
|
|
1722
|
+
assignment.allocationPercent != null
|
|
1723
|
+
? String(assignment.allocationPercent)
|
|
1724
|
+
: '',
|
|
1725
|
+
status: assignment.status,
|
|
1726
|
+
startDate: assignment.startDate?.slice(0, 10) ?? '',
|
|
1727
|
+
endDate: assignment.endDate?.slice(0, 10) ?? '',
|
|
1728
|
+
});
|
|
1729
|
+
void loadAssignmentCollaboratorById(assignment.collaboratorId);
|
|
1730
|
+
setAssignmentSheetOpen(true);
|
|
1731
|
+
},
|
|
1732
|
+
[loadAssignmentCollaboratorById]
|
|
1733
|
+
);
|
|
1734
|
+
|
|
1735
|
+
const handleSaveAssignment = useCallback(async () => {
|
|
1736
|
+
if (!project) return;
|
|
1737
|
+
const collabId = Number(assignmentFormData.collaboratorId);
|
|
1738
|
+
if (!collabId) return;
|
|
1739
|
+
setSavingAssignment(true);
|
|
1740
|
+
const newEntry = {
|
|
1741
|
+
collaboratorId: collabId,
|
|
1742
|
+
weeklyHours: assignmentFormData.weeklyHours
|
|
1743
|
+
? Number(assignmentFormData.weeklyHours)
|
|
1744
|
+
: null,
|
|
1745
|
+
allocationPercent: assignmentFormData.allocationPercent
|
|
1746
|
+
? Number(assignmentFormData.allocationPercent)
|
|
1747
|
+
: null,
|
|
1748
|
+
status: assignmentFormData.status,
|
|
1749
|
+
startDate: assignmentFormData.startDate || null,
|
|
1750
|
+
endDate: assignmentFormData.endDate || null,
|
|
1751
|
+
};
|
|
1752
|
+
const existingMapped = project.assignments.map((a) => ({
|
|
1753
|
+
collaboratorId: a.collaboratorId,
|
|
1754
|
+
weeklyHours: a.weeklyHours ?? null,
|
|
1755
|
+
allocationPercent: a.allocationPercent ?? null,
|
|
1756
|
+
status: a.status,
|
|
1757
|
+
startDate: a.startDate ?? null,
|
|
1758
|
+
endDate: a.endDate ?? null,
|
|
1759
|
+
}));
|
|
1760
|
+
const updatedAssignments = editingAssignment
|
|
1761
|
+
? existingMapped.map((a) =>
|
|
1762
|
+
a.collaboratorId === editingAssignment.collaboratorId ? newEntry : a
|
|
1763
|
+
)
|
|
1764
|
+
: [...existingMapped, newEntry];
|
|
1765
|
+
try {
|
|
1766
|
+
await mutateOperations(
|
|
1767
|
+
request,
|
|
1768
|
+
`/operations/projects/${projectId}`,
|
|
1769
|
+
'PATCH',
|
|
1770
|
+
{ teamAssignments: updatedAssignments }
|
|
1771
|
+
);
|
|
1772
|
+
await refetch();
|
|
1773
|
+
setAssignmentSheetOpen(false);
|
|
1774
|
+
} catch {
|
|
1775
|
+
// ignore
|
|
1776
|
+
} finally {
|
|
1777
|
+
setSavingAssignment(false);
|
|
1778
|
+
}
|
|
1779
|
+
}, [
|
|
1780
|
+
project,
|
|
1781
|
+
assignmentFormData,
|
|
1782
|
+
editingAssignment,
|
|
1783
|
+
request,
|
|
1784
|
+
projectId,
|
|
1785
|
+
refetch,
|
|
1786
|
+
]);
|
|
1787
|
+
|
|
1788
|
+
const handleConfirmRemoveAssignment = useCallback(async () => {
|
|
1789
|
+
if (!project || removingAssignmentId === null) return;
|
|
1790
|
+
const updatedAssignments = project.assignments
|
|
1791
|
+
.filter((a) => a.collaboratorId !== removingAssignmentId)
|
|
1792
|
+
.map((a) => ({
|
|
1793
|
+
collaboratorId: a.collaboratorId,
|
|
1794
|
+
weeklyHours: a.weeklyHours ?? null,
|
|
1795
|
+
allocationPercent: a.allocationPercent ?? null,
|
|
1796
|
+
status: a.status,
|
|
1797
|
+
startDate: a.startDate ?? null,
|
|
1798
|
+
endDate: a.endDate ?? null,
|
|
1799
|
+
}));
|
|
1800
|
+
try {
|
|
1801
|
+
await mutateOperations(
|
|
1802
|
+
request,
|
|
1803
|
+
`/operations/projects/${projectId}`,
|
|
1804
|
+
'PATCH',
|
|
1805
|
+
{ teamAssignments: updatedAssignments }
|
|
1806
|
+
);
|
|
1807
|
+
await refetch();
|
|
1808
|
+
} catch {
|
|
1809
|
+
// ignore
|
|
1810
|
+
} finally {
|
|
1811
|
+
setRemovingAssignmentId(null);
|
|
1812
|
+
}
|
|
1813
|
+
}, [project, removingAssignmentId, request, projectId, refetch]);
|
|
1814
|
+
|
|
1543
1815
|
const allocationChartData = useMemo(() => {
|
|
1544
1816
|
if (projectStats?.allocationByCollaborator?.length) {
|
|
1545
1817
|
return projectStats.allocationByCollaborator;
|
|
@@ -2141,7 +2413,10 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
2141
2413
|
<div className="flex cursor-default items-center gap-1.5 border-r px-3 py-2 transition hover:bg-muted/30">
|
|
2142
2414
|
<Avatar className="size-5 shrink-0 border bg-muted">
|
|
2143
2415
|
<AvatarImage
|
|
2144
|
-
src={
|
|
2416
|
+
src={
|
|
2417
|
+
getUserPhotoUrl(project.clientUserPhotoId) ||
|
|
2418
|
+
getPersonAvatarUrl(project.clientAvatarId)
|
|
2419
|
+
}
|
|
2145
2420
|
alt={project.clientName || ''}
|
|
2146
2421
|
/>
|
|
2147
2422
|
<AvatarFallback className="text-[9px]">
|
|
@@ -2246,7 +2521,6 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
2246
2521
|
<div className="flex items-center gap-1.5">
|
|
2247
2522
|
<div className="h-1.5 w-20 overflow-hidden rounded-full bg-muted">
|
|
2248
2523
|
<motion.div
|
|
2249
|
-
initial={{ width: 0 }}
|
|
2250
2524
|
animate={{ width: `${displayedProgress}%` }}
|
|
2251
2525
|
transition={{ duration: 0.7, ease: 'easeOut' }}
|
|
2252
2526
|
className="h-full rounded-full bg-primary"
|
|
@@ -3853,6 +4127,19 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
3853
4127
|
'rounded-2xl border bg-card p-4 shadow-sm',
|
|
3854
4128
|
isLimitedView ? 'xl:col-span-12' : 'xl:col-span-8',
|
|
3855
4129
|
].join(' ')}
|
|
4130
|
+
actions={
|
|
4131
|
+
!isLimitedView ? (
|
|
4132
|
+
<Button
|
|
4133
|
+
size="sm"
|
|
4134
|
+
variant="outline"
|
|
4135
|
+
className="cursor-pointer"
|
|
4136
|
+
onClick={openAddAssignment}
|
|
4137
|
+
>
|
|
4138
|
+
<UserPlus className="mr-1.5 size-4" />
|
|
4139
|
+
{t('teamPanel.addCollaborator')}
|
|
4140
|
+
</Button>
|
|
4141
|
+
) : undefined
|
|
4142
|
+
}
|
|
3856
4143
|
>
|
|
3857
4144
|
{project.assignments.length > 0 ? (
|
|
3858
4145
|
<div className="space-y-4">
|
|
@@ -3960,6 +4247,34 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
3960
4247
|
<ToneIcon className="size-3" />
|
|
3961
4248
|
{t(`teamPanel.status.${tone.labelKey}`)}
|
|
3962
4249
|
</span>
|
|
4250
|
+
{!isLimitedView ? (
|
|
4251
|
+
<div className="flex items-center gap-1">
|
|
4252
|
+
<Button
|
|
4253
|
+
type="button"
|
|
4254
|
+
variant="ghost"
|
|
4255
|
+
size="icon"
|
|
4256
|
+
className="size-7 cursor-pointer"
|
|
4257
|
+
onClick={() =>
|
|
4258
|
+
openEditAssignment(assignment)
|
|
4259
|
+
}
|
|
4260
|
+
>
|
|
4261
|
+
<Pencil className="size-3.5" />
|
|
4262
|
+
</Button>
|
|
4263
|
+
<Button
|
|
4264
|
+
type="button"
|
|
4265
|
+
variant="ghost"
|
|
4266
|
+
size="icon"
|
|
4267
|
+
className="size-7 cursor-pointer text-destructive hover:text-destructive"
|
|
4268
|
+
onClick={() =>
|
|
4269
|
+
setRemovingAssignmentId(
|
|
4270
|
+
assignment.collaboratorId
|
|
4271
|
+
)
|
|
4272
|
+
}
|
|
4273
|
+
>
|
|
4274
|
+
<Trash2 className="size-3.5" />
|
|
4275
|
+
</Button>
|
|
4276
|
+
</div>
|
|
4277
|
+
) : null}
|
|
3963
4278
|
</div>
|
|
3964
4279
|
</div>
|
|
3965
4280
|
</div>
|
|
@@ -4260,6 +4575,345 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
4260
4575
|
}
|
|
4261
4576
|
/>
|
|
4262
4577
|
|
|
4578
|
+
<Sheet
|
|
4579
|
+
open={assignmentSheetOpen}
|
|
4580
|
+
onOpenChange={(open) => {
|
|
4581
|
+
if (!open) {
|
|
4582
|
+
setAssignmentSheetOpen(false);
|
|
4583
|
+
setSelectedAssignmentCollaborator(null);
|
|
4584
|
+
}
|
|
4585
|
+
}}
|
|
4586
|
+
>
|
|
4587
|
+
<SheetContent className="w-full overflow-y-auto sm:max-w-[min(92vw,28rem)]">
|
|
4588
|
+
<SheetHeader>
|
|
4589
|
+
<SheetTitle>
|
|
4590
|
+
{editingAssignment
|
|
4591
|
+
? t('teamPanel.editTitle')
|
|
4592
|
+
: t('teamPanel.addTitle')}
|
|
4593
|
+
</SheetTitle>
|
|
4594
|
+
<SheetDescription>
|
|
4595
|
+
{t('teamPanel.formDescription')}
|
|
4596
|
+
</SheetDescription>
|
|
4597
|
+
</SheetHeader>
|
|
4598
|
+
<div className="mt-6 space-y-4 px-1 sm:px-4">
|
|
4599
|
+
{!editingAssignment ? (
|
|
4600
|
+
<div className="space-y-2">
|
|
4601
|
+
<Label className="text-sm font-medium">
|
|
4602
|
+
{commonT('labels.collaborator')}
|
|
4603
|
+
</Label>
|
|
4604
|
+
<EntityPicker<OperationsCollaborator>
|
|
4605
|
+
value={
|
|
4606
|
+
assignmentFormData.collaboratorId
|
|
4607
|
+
? Number(assignmentFormData.collaboratorId)
|
|
4608
|
+
: null
|
|
4609
|
+
}
|
|
4610
|
+
onChange={(val, option) => {
|
|
4611
|
+
const collaborator = option ?? null;
|
|
4612
|
+
setSelectedAssignmentCollaborator(collaborator);
|
|
4613
|
+
setAssignmentFormData((prev) => {
|
|
4614
|
+
const collaboratorId = val ? String(val) : '';
|
|
4615
|
+
if (!collaboratorId) {
|
|
4616
|
+
return {
|
|
4617
|
+
...prev,
|
|
4618
|
+
collaboratorId: '',
|
|
4619
|
+
weeklyHours: '',
|
|
4620
|
+
allocationPercent: '',
|
|
4621
|
+
};
|
|
4622
|
+
}
|
|
4623
|
+
|
|
4624
|
+
const capacity = collaborator?.weeklyCapacityHours;
|
|
4625
|
+
if (!capacity || capacity <= 0) {
|
|
4626
|
+
return {
|
|
4627
|
+
...prev,
|
|
4628
|
+
collaboratorId,
|
|
4629
|
+
};
|
|
4630
|
+
}
|
|
4631
|
+
|
|
4632
|
+
const parsedHours = parseNumberInput(prev.weeklyHours);
|
|
4633
|
+
const parsedPercent = parseNumberInput(
|
|
4634
|
+
prev.allocationPercent
|
|
4635
|
+
);
|
|
4636
|
+
|
|
4637
|
+
if (parsedHours != null) {
|
|
4638
|
+
return {
|
|
4639
|
+
...prev,
|
|
4640
|
+
collaboratorId,
|
|
4641
|
+
...syncAssignmentFromWeeklyHours(
|
|
4642
|
+
prev.weeklyHours,
|
|
4643
|
+
prev,
|
|
4644
|
+
collaborator
|
|
4645
|
+
),
|
|
4646
|
+
};
|
|
4647
|
+
}
|
|
4648
|
+
|
|
4649
|
+
if (parsedPercent != null) {
|
|
4650
|
+
return {
|
|
4651
|
+
...prev,
|
|
4652
|
+
collaboratorId,
|
|
4653
|
+
...syncAssignmentFromAllocation(
|
|
4654
|
+
prev.allocationPercent,
|
|
4655
|
+
prev,
|
|
4656
|
+
collaborator
|
|
4657
|
+
),
|
|
4658
|
+
};
|
|
4659
|
+
}
|
|
4660
|
+
|
|
4661
|
+
return {
|
|
4662
|
+
...prev,
|
|
4663
|
+
collaboratorId,
|
|
4664
|
+
weeklyHours: formatAssignmentNumericValue(capacity),
|
|
4665
|
+
allocationPercent: '100',
|
|
4666
|
+
};
|
|
4667
|
+
});
|
|
4668
|
+
}}
|
|
4669
|
+
placeholder={commonT('labels.collaborator')}
|
|
4670
|
+
searchPlaceholder={commonT('labels.collaborator')}
|
|
4671
|
+
loadOptions={loadCollaboratorOptions}
|
|
4672
|
+
getOptionValue={(opt) => opt.id}
|
|
4673
|
+
getOptionLabel={(opt) => opt.displayName}
|
|
4674
|
+
renderOption={({ option }) => (
|
|
4675
|
+
<div className="flex min-w-0 items-center gap-2.5">
|
|
4676
|
+
<Avatar className="size-6 shrink-0">
|
|
4677
|
+
<AvatarImage
|
|
4678
|
+
src={
|
|
4679
|
+
getUserPhotoUrl(option.userPhotoId) ||
|
|
4680
|
+
getPersonAvatarUrl(option.personAvatarId)
|
|
4681
|
+
}
|
|
4682
|
+
alt={option.displayName}
|
|
4683
|
+
/>
|
|
4684
|
+
<AvatarFallback className="text-[9px]">
|
|
4685
|
+
{getInitials(option.displayName)}
|
|
4686
|
+
</AvatarFallback>
|
|
4687
|
+
</Avatar>
|
|
4688
|
+
<div className="min-w-0">
|
|
4689
|
+
<div className="truncate text-sm">
|
|
4690
|
+
{option.displayName}
|
|
4691
|
+
</div>
|
|
4692
|
+
{option.department ? (
|
|
4693
|
+
<div className="truncate text-xs text-muted-foreground">
|
|
4694
|
+
{option.department}
|
|
4695
|
+
</div>
|
|
4696
|
+
) : null}
|
|
4697
|
+
</div>
|
|
4698
|
+
</div>
|
|
4699
|
+
)}
|
|
4700
|
+
valueType="number"
|
|
4701
|
+
clearable
|
|
4702
|
+
allowEmptySelection
|
|
4703
|
+
showCreateButton
|
|
4704
|
+
entityLabel={commonT('labels.collaborator').toLowerCase()}
|
|
4705
|
+
createActionLabel={`${commonT('actions.create')} ${commonT(
|
|
4706
|
+
'labels.collaborator'
|
|
4707
|
+
).toLowerCase()}`}
|
|
4708
|
+
createTitle={`${commonT('actions.create')} ${commonT(
|
|
4709
|
+
'labels.collaborator'
|
|
4710
|
+
).toLowerCase()}`}
|
|
4711
|
+
createDescription={t('teamPanel.formDescription')}
|
|
4712
|
+
createFields={[
|
|
4713
|
+
{
|
|
4714
|
+
name: 'displayName',
|
|
4715
|
+
label: collaboratorFormT('fields.displayName'),
|
|
4716
|
+
placeholder: collaboratorFormT('fields.displayName'),
|
|
4717
|
+
required: true,
|
|
4718
|
+
},
|
|
4719
|
+
{
|
|
4720
|
+
name: 'weeklyCapacityHours',
|
|
4721
|
+
label: collaboratorFormT('fields.weeklyCapacityHours'),
|
|
4722
|
+
placeholder: '40',
|
|
4723
|
+
type: 'number',
|
|
4724
|
+
},
|
|
4725
|
+
]}
|
|
4726
|
+
mapSearchToCreateValues={(search) => ({
|
|
4727
|
+
displayName: search,
|
|
4728
|
+
weeklyCapacityHours: '40',
|
|
4729
|
+
})}
|
|
4730
|
+
onCreate={createAssignmentCollaborator}
|
|
4731
|
+
/>
|
|
4732
|
+
</div>
|
|
4733
|
+
) : (
|
|
4734
|
+
<div className="flex items-center gap-3 rounded-xl border bg-muted/30 p-3">
|
|
4735
|
+
<Avatar className="size-10 border bg-muted">
|
|
4736
|
+
<AvatarImage
|
|
4737
|
+
src={
|
|
4738
|
+
getUserPhotoUrl(editingAssignment.userPhotoId) ||
|
|
4739
|
+
getPersonAvatarUrl(editingAssignment.personAvatarId)
|
|
4740
|
+
}
|
|
4741
|
+
alt={editingAssignment.collaboratorName}
|
|
4742
|
+
/>
|
|
4743
|
+
<AvatarFallback className="text-xs">
|
|
4744
|
+
{getInitials(editingAssignment.collaboratorName)}
|
|
4745
|
+
</AvatarFallback>
|
|
4746
|
+
</Avatar>
|
|
4747
|
+
<div className="min-w-0">
|
|
4748
|
+
<div className="truncate text-sm font-semibold">
|
|
4749
|
+
{editingAssignment.collaboratorName}
|
|
4750
|
+
</div>
|
|
4751
|
+
{editingAssignment.roleLabel ? (
|
|
4752
|
+
<div className="truncate text-xs text-muted-foreground">
|
|
4753
|
+
{editingAssignment.roleLabel}
|
|
4754
|
+
</div>
|
|
4755
|
+
) : null}
|
|
4756
|
+
</div>
|
|
4757
|
+
</div>
|
|
4758
|
+
)}
|
|
4759
|
+
<div className="space-y-2">
|
|
4760
|
+
<Label className="text-sm font-medium">
|
|
4761
|
+
{commonT('labels.allocationPercent')}
|
|
4762
|
+
</Label>
|
|
4763
|
+
<Input
|
|
4764
|
+
type="number"
|
|
4765
|
+
min="0"
|
|
4766
|
+
max="200"
|
|
4767
|
+
value={assignmentFormData.allocationPercent}
|
|
4768
|
+
onChange={(e) =>
|
|
4769
|
+
setAssignmentFormData((prev) => ({
|
|
4770
|
+
...prev,
|
|
4771
|
+
...syncAssignmentFromAllocation(
|
|
4772
|
+
e.target.value,
|
|
4773
|
+
prev,
|
|
4774
|
+
selectedAssignmentCollaborator
|
|
4775
|
+
),
|
|
4776
|
+
}))
|
|
4777
|
+
}
|
|
4778
|
+
placeholder="100"
|
|
4779
|
+
/>
|
|
4780
|
+
</div>
|
|
4781
|
+
<div className="space-y-2">
|
|
4782
|
+
<Label className="text-sm font-medium">
|
|
4783
|
+
{commonT('labels.weeklyCapacity')}
|
|
4784
|
+
</Label>
|
|
4785
|
+
<Input
|
|
4786
|
+
type="number"
|
|
4787
|
+
min="0"
|
|
4788
|
+
value={assignmentFormData.weeklyHours}
|
|
4789
|
+
onChange={(e) =>
|
|
4790
|
+
setAssignmentFormData((prev) => ({
|
|
4791
|
+
...prev,
|
|
4792
|
+
...syncAssignmentFromWeeklyHours(
|
|
4793
|
+
e.target.value,
|
|
4794
|
+
prev,
|
|
4795
|
+
selectedAssignmentCollaborator
|
|
4796
|
+
),
|
|
4797
|
+
}))
|
|
4798
|
+
}
|
|
4799
|
+
placeholder="40"
|
|
4800
|
+
/>
|
|
4801
|
+
</div>
|
|
4802
|
+
<div className="space-y-2">
|
|
4803
|
+
<Label className="text-sm font-medium">
|
|
4804
|
+
{commonT('labels.status')}
|
|
4805
|
+
</Label>
|
|
4806
|
+
<Select
|
|
4807
|
+
value={assignmentFormData.status}
|
|
4808
|
+
onValueChange={(val) =>
|
|
4809
|
+
setAssignmentFormData((prev) => ({ ...prev, status: val }))
|
|
4810
|
+
}
|
|
4811
|
+
>
|
|
4812
|
+
<SelectTrigger className="cursor-pointer">
|
|
4813
|
+
<SelectValue />
|
|
4814
|
+
</SelectTrigger>
|
|
4815
|
+
<SelectContent>
|
|
4816
|
+
<SelectItem value="active">
|
|
4817
|
+
{formatEnumLabel('active')}
|
|
4818
|
+
</SelectItem>
|
|
4819
|
+
<SelectItem value="inactive">
|
|
4820
|
+
{formatEnumLabel('inactive')}
|
|
4821
|
+
</SelectItem>
|
|
4822
|
+
<SelectItem value="completed">
|
|
4823
|
+
{formatEnumLabel('completed')}
|
|
4824
|
+
</SelectItem>
|
|
4825
|
+
</SelectContent>
|
|
4826
|
+
</Select>
|
|
4827
|
+
</div>
|
|
4828
|
+
<div className="grid grid-cols-2 gap-3">
|
|
4829
|
+
<div className="space-y-2">
|
|
4830
|
+
<Label className="text-sm font-medium">
|
|
4831
|
+
{commonT('labels.startDate')}
|
|
4832
|
+
</Label>
|
|
4833
|
+
<Input
|
|
4834
|
+
type="date"
|
|
4835
|
+
value={assignmentFormData.startDate}
|
|
4836
|
+
onChange={(e) =>
|
|
4837
|
+
setAssignmentFormData((prev) => ({
|
|
4838
|
+
...prev,
|
|
4839
|
+
startDate: e.target.value,
|
|
4840
|
+
}))
|
|
4841
|
+
}
|
|
4842
|
+
/>
|
|
4843
|
+
</div>
|
|
4844
|
+
<div className="space-y-2">
|
|
4845
|
+
<Label className="text-sm font-medium">
|
|
4846
|
+
{commonT('labels.endDate')}
|
|
4847
|
+
</Label>
|
|
4848
|
+
<Input
|
|
4849
|
+
type="date"
|
|
4850
|
+
value={assignmentFormData.endDate}
|
|
4851
|
+
onChange={(e) =>
|
|
4852
|
+
setAssignmentFormData((prev) => ({
|
|
4853
|
+
...prev,
|
|
4854
|
+
endDate: e.target.value,
|
|
4855
|
+
}))
|
|
4856
|
+
}
|
|
4857
|
+
/>
|
|
4858
|
+
</div>
|
|
4859
|
+
</div>
|
|
4860
|
+
<div className="flex gap-2 pt-2">
|
|
4861
|
+
<Button
|
|
4862
|
+
variant="outline"
|
|
4863
|
+
className="flex-1 cursor-pointer"
|
|
4864
|
+
onClick={() => setAssignmentSheetOpen(false)}
|
|
4865
|
+
disabled={savingAssignment}
|
|
4866
|
+
>
|
|
4867
|
+
{commonT('actions.cancel')}
|
|
4868
|
+
</Button>
|
|
4869
|
+
<Button
|
|
4870
|
+
className="flex-1 cursor-pointer"
|
|
4871
|
+
disabled={
|
|
4872
|
+
savingAssignment ||
|
|
4873
|
+
(!editingAssignment && !assignmentFormData.collaboratorId)
|
|
4874
|
+
}
|
|
4875
|
+
onClick={() => void handleSaveAssignment()}
|
|
4876
|
+
>
|
|
4877
|
+
{savingAssignment ? (
|
|
4878
|
+
<Loader2 className="mr-2 size-4 animate-spin" />
|
|
4879
|
+
) : null}
|
|
4880
|
+
{commonT('actions.save')}
|
|
4881
|
+
</Button>
|
|
4882
|
+
</div>
|
|
4883
|
+
</div>
|
|
4884
|
+
</SheetContent>
|
|
4885
|
+
</Sheet>
|
|
4886
|
+
|
|
4887
|
+
<Dialog
|
|
4888
|
+
open={removingAssignmentId !== null}
|
|
4889
|
+
onOpenChange={(open) => {
|
|
4890
|
+
if (!open) setRemovingAssignmentId(null);
|
|
4891
|
+
}}
|
|
4892
|
+
>
|
|
4893
|
+
<DialogContent className="sm:max-w-sm">
|
|
4894
|
+
<DialogHeader>
|
|
4895
|
+
<DialogTitle>{t('teamPanel.removeTitle')}</DialogTitle>
|
|
4896
|
+
<DialogDescription>
|
|
4897
|
+
{t('teamPanel.removeDescription')}
|
|
4898
|
+
</DialogDescription>
|
|
4899
|
+
</DialogHeader>
|
|
4900
|
+
<DialogFooter className="mt-4">
|
|
4901
|
+
<Button
|
|
4902
|
+
variant="outline"
|
|
4903
|
+
onClick={() => setRemovingAssignmentId(null)}
|
|
4904
|
+
>
|
|
4905
|
+
{commonT('actions.cancel')}
|
|
4906
|
+
</Button>
|
|
4907
|
+
<Button
|
|
4908
|
+
variant="destructive"
|
|
4909
|
+
onClick={() => void handleConfirmRemoveAssignment()}
|
|
4910
|
+
>
|
|
4911
|
+
{commonT('actions.delete')}
|
|
4912
|
+
</Button>
|
|
4913
|
+
</DialogFooter>
|
|
4914
|
+
</DialogContent>
|
|
4915
|
+
</Dialog>
|
|
4916
|
+
|
|
4263
4917
|
<TimesheetEntryCreateSheet
|
|
4264
4918
|
open={isTimesheetEntrySheetOpen}
|
|
4265
4919
|
onOpenChange={(open) => {
|
|
@@ -4341,7 +4995,10 @@ export function ProjectDetailsScreen({ projectId }: { projectId: number }) {
|
|
|
4341
4995
|
)
|
|
4342
4996
|
: undefined
|
|
4343
4997
|
}
|
|
4344
|
-
onCountChanged={() => {
|
|
4998
|
+
onCountChanged={() => {
|
|
4999
|
+
setBoardState(null);
|
|
5000
|
+
void refetchTasks();
|
|
5001
|
+
}}
|
|
4345
5002
|
onSaved={() => {
|
|
4346
5003
|
setBoardState(null);
|
|
4347
5004
|
void refetchTasks();
|