@hed-hog/operations 0.0.304 → 0.0.306
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-projects.controller.d.ts +15 -0
- package/dist/controllers/operations-projects.controller.d.ts.map +1 -1
- package/dist/controllers/operations-tasks.controller.d.ts +41 -10
- package/dist/controllers/operations-tasks.controller.d.ts.map +1 -1
- package/dist/controllers/operations-tasks.controller.js +11 -0
- package/dist/controllers/operations-tasks.controller.js.map +1 -1
- package/dist/controllers/operations-timesheets.controller.d.ts +21 -0
- package/dist/controllers/operations-timesheets.controller.d.ts.map +1 -1
- package/dist/controllers/operations-timesheets.controller.js +12 -0
- package/dist/controllers/operations-timesheets.controller.js.map +1 -1
- package/dist/dto/create-task.dto.d.ts +7 -1
- package/dist/dto/create-task.dto.d.ts.map +1 -1
- package/dist/dto/create-task.dto.js +38 -5
- package/dist/dto/create-task.dto.js.map +1 -1
- package/dist/dto/list-tasks.dto.d.ts +1 -1
- package/dist/dto/list-tasks.dto.d.ts.map +1 -1
- package/dist/dto/list-tasks.dto.js +2 -2
- package/dist/dto/list-tasks.dto.js.map +1 -1
- package/dist/dto/update-collaborator-type.dto.d.ts +3 -1
- package/dist/dto/update-collaborator-type.dto.d.ts.map +1 -1
- package/dist/dto/update-collaborator-type.dto.js +2 -1
- package/dist/dto/update-collaborator-type.dto.js.map +1 -1
- package/dist/dto/update-task.dto.d.ts +7 -1
- package/dist/dto/update-task.dto.d.ts.map +1 -1
- package/dist/dto/update-task.dto.js +38 -5
- package/dist/dto/update-task.dto.js.map +1 -1
- package/dist/operations.service.d.ts +90 -12
- package/dist/operations.service.d.ts.map +1 -1
- package/dist/operations.service.js +560 -148
- package/dist/operations.service.js.map +1 -1
- package/dist/operations.service.spec.js +73 -0
- package/dist/operations.service.spec.js.map +1 -1
- package/hedhog/data/menu.yaml +26 -26
- package/hedhog/data/operations_collaborator_type.yaml +76 -76
- package/hedhog/data/route.yaml +26 -0
- package/hedhog/frontend/app/_components/async-options-combobox.tsx.ejs +5 -3
- package/hedhog/frontend/app/_components/collaborator-details-screen.tsx.ejs +44 -44
- package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +168 -213
- package/hedhog/frontend/app/_components/collaborator-select-with-create.tsx.ejs +256 -256
- package/hedhog/frontend/app/_components/contract-form-screen.tsx.ejs +7 -7
- package/hedhog/frontend/app/_components/contract-template-form-screen.tsx.ejs +306 -306
- package/hedhog/frontend/app/_components/contract-template-select-with-create.tsx.ejs +247 -247
- package/hedhog/frontend/app/_components/contract-wizard-sheet.tsx.ejs +3520 -3520
- package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +1504 -52
- package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +528 -403
- package/hedhog/frontend/app/_components/section-card.tsx.ejs +25 -18
- package/hedhog/frontend/app/_components/system-user-select-with-create.tsx.ejs +609 -0
- package/hedhog/frontend/app/_components/timesheet-task-create-sheet.tsx.ejs +1 -0
- package/hedhog/frontend/app/_lib/types.ts.ejs +5 -0
- package/hedhog/frontend/app/_lib/utils/format.ts.ejs +7 -7
- package/hedhog/frontend/app/_lib/utils/forms.ts.ejs +48 -1
- package/hedhog/frontend/app/approvals/page.tsx.ejs +2 -2
- package/hedhog/frontend/app/collaborator-types/page.tsx.ejs +513 -502
- package/hedhog/frontend/app/collaborators/page.tsx.ejs +10 -7
- package/hedhog/frontend/app/contracts/page.tsx.ejs +938 -938
- package/hedhog/frontend/app/projects/[id]/edit/page.tsx.ejs +1 -1
- package/hedhog/frontend/app/projects/page.tsx.ejs +360 -133
- package/hedhog/frontend/app/schedule-adjustments/page.tsx.ejs +235 -72
- package/hedhog/frontend/app/timesheets/page.tsx.ejs +344 -134
- package/hedhog/frontend/messages/en.json +32 -4
- package/hedhog/frontend/messages/pt.json +34 -6
- package/hedhog/table/operations_collaborator.yaml +18 -18
- package/hedhog/table/operations_collaborator_equity_participation.yaml +43 -43
- package/hedhog/table/operations_collaborator_type.yaml +33 -33
- package/hedhog/table/operations_contract_document.yaml +33 -33
- package/hedhog/table/operations_project.yaml +9 -0
- package/hedhog/table/operations_task.yaml +43 -4
- package/package.json +6 -6
- package/src/controllers/operations-tasks.controller.ts +11 -0
- package/src/controllers/operations-timesheets.controller.ts +13 -0
- package/src/dto/create-collaborator-type.dto.ts +43 -43
- package/src/dto/create-collaborator.dto.ts +223 -223
- package/src/dto/create-task.dto.ts +47 -7
- package/src/dto/list-collaborator-types.dto.ts +15 -15
- package/src/dto/list-collaborators.dto.ts +30 -30
- package/src/dto/list-tasks.dto.ts +3 -3
- package/src/dto/update-collaborator-type.dto.ts +4 -3
- package/src/dto/update-collaborator.dto.ts +3 -3
- package/src/dto/update-task.dto.ts +47 -7
- package/src/operations.service.spec.ts +96 -0
- package/src/operations.service.ts +813 -135
|
@@ -83,7 +83,11 @@ import type {
|
|
|
83
83
|
OperationsProjectRole,
|
|
84
84
|
} from '../_lib/types';
|
|
85
85
|
import { formatEnumLabel, getStatusBadgeClass } from '../_lib/utils/format';
|
|
86
|
-
import {
|
|
86
|
+
import {
|
|
87
|
+
normalizePercentInput,
|
|
88
|
+
parseNumberInput,
|
|
89
|
+
trimToNull,
|
|
90
|
+
} from '../_lib/utils/forms';
|
|
87
91
|
import { ContractFormScreen } from './contract-form-screen';
|
|
88
92
|
import { ContractTemplateFormScreen } from './contract-template-form-screen';
|
|
89
93
|
import { DepartmentSelectWithCreate } from './department-select-with-create';
|
|
@@ -128,6 +132,32 @@ type ProjectFormState = {
|
|
|
128
132
|
|
|
129
133
|
type ProjectFormValues = ProjectFormState;
|
|
130
134
|
|
|
135
|
+
function normalizeDateInputValue(value?: string | null) {
|
|
136
|
+
if (!value) {
|
|
137
|
+
return '';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const normalizedValue = String(value).trim();
|
|
141
|
+
|
|
142
|
+
if (!normalizedValue) {
|
|
143
|
+
return '';
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const directMatch = normalizedValue.match(/^\d{4}-\d{2}-\d{2}/);
|
|
147
|
+
|
|
148
|
+
if (directMatch?.[0]) {
|
|
149
|
+
return directMatch[0];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const parsedDate = new Date(normalizedValue);
|
|
153
|
+
|
|
154
|
+
if (Number.isNaN(parsedDate.getTime())) {
|
|
155
|
+
return normalizedValue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return parsedDate.toISOString().slice(0, 10);
|
|
159
|
+
}
|
|
160
|
+
|
|
131
161
|
function buildEmptyForm(
|
|
132
162
|
collaborators: OperationsCollaborator[] = []
|
|
133
163
|
): ProjectFormState {
|
|
@@ -168,6 +198,18 @@ function toFormState(
|
|
|
168
198
|
project: OperationsProjectDetails,
|
|
169
199
|
collaborators: OperationsCollaborator[]
|
|
170
200
|
): ProjectFormState {
|
|
201
|
+
const collaboratorOptions =
|
|
202
|
+
collaborators.length > 0
|
|
203
|
+
? collaborators
|
|
204
|
+
: (project.assignments ?? []).map((assignment) => ({
|
|
205
|
+
id: assignment.collaboratorId,
|
|
206
|
+
code: '',
|
|
207
|
+
displayName: assignment.collaboratorName,
|
|
208
|
+
department: null,
|
|
209
|
+
title: null,
|
|
210
|
+
status: 'active',
|
|
211
|
+
}));
|
|
212
|
+
|
|
171
213
|
const assignments = new Map(
|
|
172
214
|
(project.assignments ?? []).map((assignment) => [
|
|
173
215
|
assignment.collaboratorId,
|
|
@@ -176,7 +218,9 @@ function toFormState(
|
|
|
176
218
|
);
|
|
177
219
|
|
|
178
220
|
return {
|
|
179
|
-
contractId: project.
|
|
221
|
+
contractId: project.relatedContract?.id
|
|
222
|
+
? String(project.relatedContract.id)
|
|
223
|
+
: 'none',
|
|
180
224
|
contractTemplateId: 'none',
|
|
181
225
|
managerCollaboratorId: project.managerCollaboratorId
|
|
182
226
|
? String(project.managerCollaboratorId)
|
|
@@ -195,8 +239,8 @@ function toFormState(
|
|
|
195
239
|
project.budgetAmount !== null && project.budgetAmount !== undefined
|
|
196
240
|
? String(project.budgetAmount)
|
|
197
241
|
: '',
|
|
198
|
-
startDate: project.startDate
|
|
199
|
-
endDate: project.endDate
|
|
242
|
+
startDate: normalizeDateInputValue(project.startDate),
|
|
243
|
+
endDate: normalizeDateInputValue(project.endDate),
|
|
200
244
|
billingModel: project.relatedContract?.billingModel ?? 'time_and_material',
|
|
201
245
|
monthlyHourCap:
|
|
202
246
|
project.relatedContract?.monthlyHourCap !== null &&
|
|
@@ -207,7 +251,7 @@ function toFormState(
|
|
|
207
251
|
contractName: project.relatedContract?.name ?? '',
|
|
208
252
|
contractDescription: project.relatedContract?.description ?? '',
|
|
209
253
|
autoGenerateContractDraft: true,
|
|
210
|
-
teamAssignments:
|
|
254
|
+
teamAssignments: collaboratorOptions.map((collaborator) => {
|
|
211
255
|
const assignment = assignments.get(collaborator.id);
|
|
212
256
|
return {
|
|
213
257
|
collaboratorId: collaborator.id,
|
|
@@ -800,6 +844,8 @@ export function ProjectFormScreen({
|
|
|
800
844
|
useQuery<OperationsProjectDetails>({
|
|
801
845
|
queryKey: ['operations-project-form', currentLocaleCode, projectId],
|
|
802
846
|
enabled: Boolean(projectId),
|
|
847
|
+
staleTime: 0,
|
|
848
|
+
refetchOnMount: 'always',
|
|
803
849
|
queryFn: () =>
|
|
804
850
|
fetchOperations<OperationsProjectDetails>(
|
|
805
851
|
request,
|
|
@@ -808,12 +854,12 @@ export function ProjectFormScreen({
|
|
|
808
854
|
});
|
|
809
855
|
|
|
810
856
|
useEffect(() => {
|
|
811
|
-
if (
|
|
857
|
+
if (project) {
|
|
858
|
+
formMethods.reset(toFormState(project, collaborators));
|
|
812
859
|
return;
|
|
813
860
|
}
|
|
814
861
|
|
|
815
|
-
if (
|
|
816
|
-
formMethods.reset(toFormState(project, collaborators));
|
|
862
|
+
if (!collaborators.length) {
|
|
817
863
|
return;
|
|
818
864
|
}
|
|
819
865
|
|
|
@@ -823,11 +869,63 @@ export function ProjectFormScreen({
|
|
|
823
869
|
}
|
|
824
870
|
}, [collaborators, formMethods, project]);
|
|
825
871
|
|
|
872
|
+
const availableCollaborators = useMemo(() => {
|
|
873
|
+
const byId = new Map<number, OperationsCollaborator>();
|
|
874
|
+
|
|
875
|
+
for (const collaborator of collaborators) {
|
|
876
|
+
byId.set(collaborator.id, collaborator);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
if (
|
|
880
|
+
project?.managerCollaboratorId &&
|
|
881
|
+
!byId.has(project.managerCollaboratorId)
|
|
882
|
+
) {
|
|
883
|
+
byId.set(project.managerCollaboratorId, {
|
|
884
|
+
id: project.managerCollaboratorId,
|
|
885
|
+
code: '',
|
|
886
|
+
displayName: project.managerName ?? `#${project.managerCollaboratorId}`,
|
|
887
|
+
department: null,
|
|
888
|
+
title: null,
|
|
889
|
+
status: 'active',
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
for (const assignment of project?.assignments ?? []) {
|
|
894
|
+
if (!byId.has(assignment.collaboratorId)) {
|
|
895
|
+
byId.set(assignment.collaboratorId, {
|
|
896
|
+
id: assignment.collaboratorId,
|
|
897
|
+
code: '',
|
|
898
|
+
displayName: assignment.collaboratorName,
|
|
899
|
+
department: null,
|
|
900
|
+
title: null,
|
|
901
|
+
status: 'active',
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
return Array.from(byId.values());
|
|
907
|
+
}, [collaborators, project]);
|
|
908
|
+
|
|
909
|
+
const availableContracts = useMemo(() => {
|
|
910
|
+
if (!project?.relatedContract) {
|
|
911
|
+
return contracts;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
const hasSelectedContract = contracts.some(
|
|
915
|
+
(contract) => contract.id === project.relatedContract?.id
|
|
916
|
+
);
|
|
917
|
+
|
|
918
|
+
return hasSelectedContract
|
|
919
|
+
? contracts
|
|
920
|
+
: [project.relatedContract, ...contracts];
|
|
921
|
+
}, [contracts, project]);
|
|
922
|
+
|
|
826
923
|
const selectedContract = useMemo(
|
|
827
924
|
() =>
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
925
|
+
availableContracts.find(
|
|
926
|
+
(contract) => String(contract.id) === form.contractId
|
|
927
|
+
) ?? null,
|
|
928
|
+
[availableContracts, form.contractId]
|
|
831
929
|
);
|
|
832
930
|
|
|
833
931
|
const selectedContractTemplate = useMemo(
|
|
@@ -840,14 +938,14 @@ export function ProjectFormScreen({
|
|
|
840
938
|
|
|
841
939
|
const managerOptions = useMemo(
|
|
842
940
|
() =>
|
|
843
|
-
|
|
941
|
+
availableCollaborators.map((collaborator) => ({
|
|
844
942
|
id: collaborator.id,
|
|
845
943
|
title: collaborator.displayName,
|
|
846
944
|
description: [collaborator.department, collaborator.title]
|
|
847
945
|
.filter(Boolean)
|
|
848
946
|
.join(' • '),
|
|
849
947
|
})),
|
|
850
|
-
[
|
|
948
|
+
[availableCollaborators]
|
|
851
949
|
);
|
|
852
950
|
|
|
853
951
|
const projectRoleOptions = useMemo(
|
|
@@ -893,7 +991,7 @@ export function ProjectFormScreen({
|
|
|
893
991
|
const normalizedSearch = assignmentSearch.trim().toLowerCase();
|
|
894
992
|
|
|
895
993
|
return form.teamAssignments.filter((assignment) => {
|
|
896
|
-
const collaborator =
|
|
994
|
+
const collaborator = availableCollaborators.find(
|
|
897
995
|
(item) => item.id === assignment.collaboratorId
|
|
898
996
|
);
|
|
899
997
|
|
|
@@ -916,7 +1014,7 @@ export function ProjectFormScreen({
|
|
|
916
1014
|
String(value).toLowerCase().includes(normalizedSearch)
|
|
917
1015
|
);
|
|
918
1016
|
});
|
|
919
|
-
}, [assignmentSearch,
|
|
1017
|
+
}, [assignmentSearch, availableCollaborators, form.teamAssignments]);
|
|
920
1018
|
|
|
921
1019
|
const updateAssignment = (
|
|
922
1020
|
collaboratorId: number,
|
|
@@ -956,7 +1054,7 @@ export function ProjectFormScreen({
|
|
|
956
1054
|
summary: trimToNull(values.summary),
|
|
957
1055
|
status: values.status,
|
|
958
1056
|
progressPercent: parseNumberInput(values.progressPercent),
|
|
959
|
-
deliveryModel: values.deliveryModel,
|
|
1057
|
+
deliveryModel: trimToNull(values.deliveryModel) ?? 'project_delivery',
|
|
960
1058
|
budgetAmount: parseNumberInput(values.budgetAmount),
|
|
961
1059
|
startDate: trimToNull(values.startDate),
|
|
962
1060
|
endDate: trimToNull(values.endDate),
|
|
@@ -1288,11 +1386,15 @@ export function ProjectFormScreen({
|
|
|
1288
1386
|
<FormControl>
|
|
1289
1387
|
<Input
|
|
1290
1388
|
{...field}
|
|
1291
|
-
type="
|
|
1292
|
-
|
|
1293
|
-
max="100"
|
|
1294
|
-
step="1"
|
|
1389
|
+
type="text"
|
|
1390
|
+
inputMode="decimal"
|
|
1295
1391
|
placeholder={t('placeholders.progressPercent')}
|
|
1392
|
+
value={field.value ?? ''}
|
|
1393
|
+
onChange={(event) =>
|
|
1394
|
+
field.onChange(
|
|
1395
|
+
normalizePercentInput(event.target.value)
|
|
1396
|
+
)
|
|
1397
|
+
}
|
|
1296
1398
|
/>
|
|
1297
1399
|
</FormControl>
|
|
1298
1400
|
<FormMessage />
|
|
@@ -1315,384 +1417,236 @@ export function ProjectFormScreen({
|
|
|
1315
1417
|
{t('sections.financialsDescription')}
|
|
1316
1418
|
</p>
|
|
1317
1419
|
</div>
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
<FieldLabel
|
|
1333
|
-
label={commonT('labels.monthlyHourCap')}
|
|
1334
|
-
hint={t('hints.monthlyHourCap')}
|
|
1335
|
-
/>
|
|
1336
|
-
<Input
|
|
1337
|
-
type="number"
|
|
1338
|
-
step="0.5"
|
|
1339
|
-
placeholder={t('placeholders.monthlyHourCap')}
|
|
1340
|
-
value={form.monthlyHourCap}
|
|
1341
|
-
onChange={(event) =>
|
|
1342
|
-
setForm((current) => ({
|
|
1343
|
-
...current,
|
|
1344
|
-
monthlyHourCap: event.target.value,
|
|
1345
|
-
}))
|
|
1346
|
-
}
|
|
1347
|
-
/>
|
|
1348
|
-
</div>
|
|
1349
|
-
<div className="min-w-0 space-y-2">
|
|
1350
|
-
<FieldLabel label={commonT('labels.billingModel')} />
|
|
1351
|
-
<Select
|
|
1352
|
-
value={form.billingModel}
|
|
1353
|
-
onValueChange={(value) =>
|
|
1354
|
-
setForm((current) => ({ ...current, billingModel: value }))
|
|
1355
|
-
}
|
|
1356
|
-
>
|
|
1357
|
-
<SelectTrigger className="w-full">
|
|
1358
|
-
<SelectValue />
|
|
1359
|
-
</SelectTrigger>
|
|
1360
|
-
<SelectContent>
|
|
1361
|
-
<SelectItem value="time_and_material">
|
|
1362
|
-
{t('options.billingModels.time_and_material')}
|
|
1363
|
-
</SelectItem>
|
|
1364
|
-
<SelectItem value="monthly_retainer">
|
|
1365
|
-
{t('options.billingModels.monthly_retainer')}
|
|
1366
|
-
</SelectItem>
|
|
1367
|
-
<SelectItem value="fixed_price">
|
|
1368
|
-
{t('options.billingModels.fixed_price')}
|
|
1369
|
-
</SelectItem>
|
|
1370
|
-
</SelectContent>
|
|
1371
|
-
</Select>
|
|
1372
|
-
</div>
|
|
1373
|
-
<div className="min-w-0 space-y-2">
|
|
1374
|
-
<FieldLabel
|
|
1375
|
-
label={t('fields.contractTemplate')}
|
|
1376
|
-
hint={t('hints.contractTemplate')}
|
|
1377
|
-
/>
|
|
1378
|
-
<ContractTemplateSelectWithCreate
|
|
1379
|
-
label=""
|
|
1380
|
-
value={form.contractTemplateId}
|
|
1381
|
-
templates={contractTemplates}
|
|
1382
|
-
selectPlaceholder={commonT('labels.notAssigned')}
|
|
1383
|
-
searchPlaceholder={t('placeholders.contractTemplateSearch')}
|
|
1384
|
-
onChange={(value) =>
|
|
1385
|
-
setForm((current) => ({
|
|
1386
|
-
...current,
|
|
1387
|
-
contractTemplateId: value,
|
|
1388
|
-
}))
|
|
1389
|
-
}
|
|
1390
|
-
onCreated={async (template) => {
|
|
1391
|
-
await refetchContractTemplates();
|
|
1392
|
-
setForm((current) => ({
|
|
1393
|
-
...current,
|
|
1394
|
-
contractTemplateId: template?.id
|
|
1395
|
-
? String(template.id)
|
|
1396
|
-
: current.contractTemplateId,
|
|
1397
|
-
}));
|
|
1398
|
-
}}
|
|
1399
|
-
/>
|
|
1400
|
-
</div>
|
|
1401
|
-
<div className="min-w-0 space-y-2">
|
|
1402
|
-
<FieldLabel
|
|
1403
|
-
label={commonT('labels.contract')}
|
|
1404
|
-
hint={t('hints.contract')}
|
|
1405
|
-
/>
|
|
1406
|
-
<ContractSelectWithCreate
|
|
1407
|
-
label=""
|
|
1408
|
-
value={form.contractId}
|
|
1409
|
-
contracts={contracts}
|
|
1410
|
-
selectPlaceholder={commonT('labels.notAssigned')}
|
|
1411
|
-
searchPlaceholder={t('placeholders.contractSearch')}
|
|
1412
|
-
onChange={(value) =>
|
|
1413
|
-
setForm((current) => ({ ...current, contractId: value }))
|
|
1414
|
-
}
|
|
1415
|
-
onCreated={async (contract) => {
|
|
1416
|
-
await refetchContracts();
|
|
1417
|
-
setForm((current) => ({
|
|
1418
|
-
...current,
|
|
1419
|
-
contractId: contract?.id
|
|
1420
|
-
? String(contract.id)
|
|
1421
|
-
: current.contractId,
|
|
1422
|
-
billingModel:
|
|
1423
|
-
contract?.billingModel ?? current.billingModel,
|
|
1424
|
-
monthlyHourCap:
|
|
1425
|
-
contract?.monthlyHourCap !== null &&
|
|
1426
|
-
contract?.monthlyHourCap !== undefined
|
|
1427
|
-
? String(contract.monthlyHourCap)
|
|
1428
|
-
: current.monthlyHourCap,
|
|
1429
|
-
}));
|
|
1430
|
-
}}
|
|
1431
|
-
initialValues={{
|
|
1432
|
-
code: form.code ? `PRJ-${form.code}` : '',
|
|
1433
|
-
name: form.name
|
|
1434
|
-
? `${form.name} Service Agreement`
|
|
1435
|
-
: (selectedContractTemplate?.name ?? ''),
|
|
1436
|
-
clientName: form.clientName,
|
|
1437
|
-
contractTemplateId: form.contractTemplateId,
|
|
1438
|
-
contractCategory:
|
|
1439
|
-
selectedContractTemplate?.contractCategory ?? 'client',
|
|
1440
|
-
contractType:
|
|
1441
|
-
selectedContractTemplate?.contractType ??
|
|
1442
|
-
'service_agreement',
|
|
1443
|
-
signatureStatus:
|
|
1444
|
-
selectedContractTemplate?.signatureStatus ?? 'not_started',
|
|
1445
|
-
billingModel:
|
|
1446
|
-
selectedContractTemplate?.billingModel ?? form.billingModel,
|
|
1447
|
-
budgetAmount: form.budgetAmount,
|
|
1448
|
-
monthlyHourCap: form.monthlyHourCap,
|
|
1449
|
-
startDate: form.startDate,
|
|
1450
|
-
endDate: form.endDate,
|
|
1451
|
-
description:
|
|
1452
|
-
selectedContractTemplate?.description ?? form.summary,
|
|
1453
|
-
contentHtml: selectedContractTemplate?.contentHtml ?? '',
|
|
1454
|
-
}}
|
|
1455
|
-
/>
|
|
1456
|
-
</div>
|
|
1457
|
-
</div>
|
|
1458
|
-
</section>
|
|
1459
|
-
|
|
1460
|
-
<section className="space-y-3">
|
|
1461
|
-
<div className="space-y-0.5">
|
|
1462
|
-
<h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
1463
|
-
{t('sections.contract')}
|
|
1464
|
-
</h3>
|
|
1465
|
-
<p className="text-[11px] text-muted-foreground/80">
|
|
1466
|
-
{t('sections.contractDescription')}
|
|
1467
|
-
</p>
|
|
1468
|
-
</div>
|
|
1469
|
-
|
|
1470
|
-
<div className="grid min-w-0 gap-3 xl:grid-cols-[minmax(0,1.5fr)_minmax(0,1fr)]">
|
|
1471
|
-
<div className="rounded-lg border px-3 py-3">
|
|
1472
|
-
<div className="flex items-start justify-between gap-3">
|
|
1473
|
-
<div className="min-w-0">
|
|
1474
|
-
<div className="text-sm font-medium text-foreground">
|
|
1475
|
-
{selectedContract?.name || commonT('labels.notAssigned')}
|
|
1476
|
-
</div>
|
|
1477
|
-
<div className="mt-1 text-xs text-muted-foreground">
|
|
1478
|
-
{selectedContract
|
|
1479
|
-
? [
|
|
1480
|
-
selectedContract.code,
|
|
1481
|
-
selectedContract.clientName,
|
|
1482
|
-
formatEnumLabel(selectedContract.contractType),
|
|
1483
|
-
]
|
|
1484
|
-
.filter(Boolean)
|
|
1485
|
-
.join(' • ')
|
|
1486
|
-
: t('fields.autoGenerateContractDraftDescription')}
|
|
1487
|
-
</div>
|
|
1420
|
+
<div className="grid min-w-0 gap-3 md:grid-cols-2 xl:grid-cols-5">
|
|
1421
|
+
<div className="min-w-0 space-y-2">
|
|
1422
|
+
<FieldLabel label={commonT('labels.budget')} />
|
|
1423
|
+
<InputMoney
|
|
1424
|
+
value={
|
|
1425
|
+
form.budgetAmount === '' ? '' : Number(form.budgetAmount)
|
|
1426
|
+
}
|
|
1427
|
+
onValueChange={(value) =>
|
|
1428
|
+
setForm((current) => ({
|
|
1429
|
+
...current,
|
|
1430
|
+
budgetAmount: value !== null ? String(value) : '',
|
|
1431
|
+
}))
|
|
1432
|
+
}
|
|
1433
|
+
/>
|
|
1488
1434
|
</div>
|
|
1489
|
-
|
|
1490
|
-
<
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
</div>
|
|
1507
|
-
<div className="mt-1 text-[11px] text-muted-foreground">
|
|
1508
|
-
{[
|
|
1509
|
-
selectedContractTemplate.name,
|
|
1510
|
-
selectedContractTemplate.code,
|
|
1511
|
-
formatEnumLabel(selectedContractTemplate.contractType),
|
|
1512
|
-
]
|
|
1513
|
-
.filter(Boolean)
|
|
1514
|
-
.join(' • ')}
|
|
1515
|
-
</div>
|
|
1435
|
+
<div className="min-w-0 space-y-2">
|
|
1436
|
+
<FieldLabel
|
|
1437
|
+
label={commonT('labels.monthlyHourCap')}
|
|
1438
|
+
hint={t('hints.monthlyHourCap')}
|
|
1439
|
+
/>
|
|
1440
|
+
<Input
|
|
1441
|
+
type="number"
|
|
1442
|
+
step="0.5"
|
|
1443
|
+
placeholder={t('placeholders.monthlyHourCap')}
|
|
1444
|
+
value={form.monthlyHourCap}
|
|
1445
|
+
onChange={(event) =>
|
|
1446
|
+
setForm((current) => ({
|
|
1447
|
+
...current,
|
|
1448
|
+
monthlyHourCap: event.target.value,
|
|
1449
|
+
}))
|
|
1450
|
+
}
|
|
1451
|
+
/>
|
|
1516
1452
|
</div>
|
|
1517
|
-
|
|
1453
|
+
<div className="min-w-0 space-y-2">
|
|
1454
|
+
<FieldLabel label={commonT('labels.billingModel')} />
|
|
1455
|
+
<Select
|
|
1456
|
+
value={form.billingModel}
|
|
1457
|
+
onValueChange={(value) =>
|
|
1458
|
+
setForm((current) => ({
|
|
1459
|
+
...current,
|
|
1460
|
+
billingModel: value,
|
|
1461
|
+
}))
|
|
1462
|
+
}
|
|
1463
|
+
>
|
|
1464
|
+
<SelectTrigger className="w-full">
|
|
1465
|
+
<SelectValue />
|
|
1466
|
+
</SelectTrigger>
|
|
1467
|
+
<SelectContent>
|
|
1468
|
+
<SelectItem value="time_and_material">
|
|
1469
|
+
{t('options.billingModels.time_and_material')}
|
|
1470
|
+
</SelectItem>
|
|
1471
|
+
<SelectItem value="monthly_retainer">
|
|
1472
|
+
{t('options.billingModels.monthly_retainer')}
|
|
1473
|
+
</SelectItem>
|
|
1474
|
+
<SelectItem value="fixed_price">
|
|
1475
|
+
{t('options.billingModels.fixed_price')}
|
|
1476
|
+
</SelectItem>
|
|
1477
|
+
</SelectContent>
|
|
1478
|
+
</Select>
|
|
1479
|
+
</div>
|
|
1480
|
+
<div className="min-w-0 space-y-2">
|
|
1481
|
+
<FieldLabel
|
|
1482
|
+
label={t('fields.contractTemplate')}
|
|
1483
|
+
hint={t('hints.contractTemplate')}
|
|
1484
|
+
/>
|
|
1485
|
+
<ContractTemplateSelectWithCreate
|
|
1486
|
+
label=""
|
|
1487
|
+
value={form.contractTemplateId}
|
|
1488
|
+
templates={contractTemplates}
|
|
1489
|
+
selectPlaceholder={commonT('labels.notAssigned')}
|
|
1490
|
+
searchPlaceholder={t('placeholders.contractTemplateSearch')}
|
|
1491
|
+
onChange={(value) =>
|
|
1492
|
+
setForm((current) => ({
|
|
1493
|
+
...current,
|
|
1494
|
+
contractTemplateId: value,
|
|
1495
|
+
}))
|
|
1496
|
+
}
|
|
1497
|
+
onCreated={async (template) => {
|
|
1498
|
+
await refetchContractTemplates();
|
|
1499
|
+
setForm((current) => ({
|
|
1500
|
+
...current,
|
|
1501
|
+
contractTemplateId: template?.id
|
|
1502
|
+
? String(template.id)
|
|
1503
|
+
: current.contractTemplateId,
|
|
1504
|
+
}));
|
|
1505
|
+
}}
|
|
1506
|
+
/>
|
|
1507
|
+
</div>
|
|
1508
|
+
<div className="min-w-0 space-y-2">
|
|
1509
|
+
<FieldLabel
|
|
1510
|
+
label={commonT('labels.contract')}
|
|
1511
|
+
hint={t('hints.contract')}
|
|
1512
|
+
/>
|
|
1513
|
+
<ContractSelectWithCreate
|
|
1514
|
+
label=""
|
|
1515
|
+
value={form.contractId}
|
|
1516
|
+
contracts={availableContracts}
|
|
1517
|
+
selectPlaceholder={commonT('labels.notAssigned')}
|
|
1518
|
+
searchPlaceholder={t('placeholders.contractSearch')}
|
|
1519
|
+
onChange={(value) =>
|
|
1520
|
+
setForm((current) => ({ ...current, contractId: value }))
|
|
1521
|
+
}
|
|
1522
|
+
onCreated={async (contract) => {
|
|
1523
|
+
await refetchContracts();
|
|
1524
|
+
setForm((current) => ({
|
|
1525
|
+
...current,
|
|
1526
|
+
contractId: contract?.id
|
|
1527
|
+
? String(contract.id)
|
|
1528
|
+
: current.contractId,
|
|
1529
|
+
billingModel:
|
|
1530
|
+
contract?.billingModel ?? current.billingModel,
|
|
1531
|
+
monthlyHourCap:
|
|
1532
|
+
contract?.monthlyHourCap !== null &&
|
|
1533
|
+
contract?.monthlyHourCap !== undefined
|
|
1534
|
+
? String(contract.monthlyHourCap)
|
|
1535
|
+
: current.monthlyHourCap,
|
|
1536
|
+
}));
|
|
1537
|
+
}}
|
|
1538
|
+
initialValues={{
|
|
1539
|
+
code: form.code ? `PRJ-${form.code}` : '',
|
|
1540
|
+
name: form.name
|
|
1541
|
+
? `${form.name} Service Agreement`
|
|
1542
|
+
: (selectedContractTemplate?.name ?? ''),
|
|
1543
|
+
clientName: form.clientName,
|
|
1544
|
+
contractTemplateId: form.contractTemplateId,
|
|
1545
|
+
contractCategory:
|
|
1546
|
+
selectedContractTemplate?.contractCategory ?? 'client',
|
|
1547
|
+
contractType:
|
|
1548
|
+
selectedContractTemplate?.contractType ??
|
|
1549
|
+
'service_agreement',
|
|
1550
|
+
signatureStatus:
|
|
1551
|
+
selectedContractTemplate?.signatureStatus ??
|
|
1552
|
+
'not_started',
|
|
1553
|
+
billingModel:
|
|
1554
|
+
selectedContractTemplate?.billingModel ??
|
|
1555
|
+
form.billingModel,
|
|
1556
|
+
budgetAmount: form.budgetAmount,
|
|
1557
|
+
monthlyHourCap: form.monthlyHourCap,
|
|
1558
|
+
startDate: form.startDate,
|
|
1559
|
+
endDate: form.endDate,
|
|
1560
|
+
description:
|
|
1561
|
+
selectedContractTemplate?.description ?? form.summary,
|
|
1562
|
+
contentHtml: selectedContractTemplate?.contentHtml ?? '',
|
|
1563
|
+
}}
|
|
1564
|
+
/>
|
|
1565
|
+
</div>
|
|
1566
|
+
</div>
|
|
1567
|
+
</section>
|
|
1518
1568
|
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
</Link>
|
|
1528
|
-
</Button>
|
|
1529
|
-
) : null}
|
|
1569
|
+
<section className="space-y-3">
|
|
1570
|
+
<div className="space-y-0.5">
|
|
1571
|
+
<h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
1572
|
+
{t('sections.contract')}
|
|
1573
|
+
</h3>
|
|
1574
|
+
<p className="text-[11px] text-muted-foreground/80">
|
|
1575
|
+
{t('sections.contractDescription')}
|
|
1576
|
+
</p>
|
|
1530
1577
|
</div>
|
|
1531
|
-
</div>
|
|
1532
1578
|
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1579
|
+
<div className="grid min-w-0 gap-3 xl:grid-cols-[minmax(0,1.5fr)_minmax(0,1fr)]">
|
|
1580
|
+
<div className="rounded-lg border px-3 py-3">
|
|
1581
|
+
<div className="flex items-start justify-between gap-3">
|
|
1582
|
+
<div className="min-w-0">
|
|
1583
|
+
<div className="text-sm font-medium text-foreground">
|
|
1584
|
+
{selectedContract?.name ||
|
|
1585
|
+
commonT('labels.notAssigned')}
|
|
1586
|
+
</div>
|
|
1587
|
+
<div className="mt-1 text-xs text-muted-foreground">
|
|
1588
|
+
{selectedContract
|
|
1589
|
+
? [
|
|
1590
|
+
selectedContract.code,
|
|
1591
|
+
selectedContract.clientName,
|
|
1592
|
+
formatEnumLabel(selectedContract.contractType),
|
|
1593
|
+
]
|
|
1594
|
+
.filter(Boolean)
|
|
1595
|
+
.join(' • ')
|
|
1596
|
+
: t('fields.autoGenerateContractDraftDescription')}
|
|
1597
|
+
</div>
|
|
1598
|
+
</div>
|
|
1599
|
+
{selectedContract?.status ? (
|
|
1600
|
+
<span className="shrink-0">
|
|
1601
|
+
<span
|
|
1602
|
+
className={`inline-flex items-center rounded-full px-2.5 py-1 text-xs font-medium ${getStatusBadgeClass(
|
|
1603
|
+
selectedContract.status
|
|
1604
|
+
)}`}
|
|
1605
|
+
>
|
|
1606
|
+
{formatEnumLabel(selectedContract.status)}
|
|
1542
1607
|
</span>
|
|
1543
|
-
</
|
|
1544
|
-
|
|
1545
|
-
<p>
|
|
1546
|
-
{form.contractId === 'none'
|
|
1547
|
-
? t('fields.autoGenerateContractDraftDescription')
|
|
1548
|
-
: t('fields.existingContractSelected')}
|
|
1549
|
-
</p>
|
|
1550
|
-
</TooltipContent>
|
|
1551
|
-
</Tooltip>
|
|
1552
|
-
</div>
|
|
1553
|
-
<div className="text-[11px] text-muted-foreground">
|
|
1554
|
-
{form.contractId === 'none'
|
|
1555
|
-
? t('labels.enabled')
|
|
1556
|
-
: t('labels.disabled')}
|
|
1608
|
+
</span>
|
|
1609
|
+
) : null}
|
|
1557
1610
|
</div>
|
|
1558
|
-
</div>
|
|
1559
|
-
<Switch
|
|
1560
|
-
checked={
|
|
1561
|
-
form.contractId === 'none' && form.autoGenerateContractDraft
|
|
1562
|
-
}
|
|
1563
|
-
disabled={form.contractId !== 'none'}
|
|
1564
|
-
onCheckedChange={(checked) =>
|
|
1565
|
-
setForm((current) => ({
|
|
1566
|
-
...current,
|
|
1567
|
-
autoGenerateContractDraft: checked,
|
|
1568
|
-
}))
|
|
1569
|
-
}
|
|
1570
|
-
/>
|
|
1571
|
-
</div>
|
|
1572
|
-
</div>
|
|
1573
|
-
</div>
|
|
1574
|
-
</section>
|
|
1575
1611
|
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
className="pl-9"
|
|
1592
|
-
value={assignmentSearch}
|
|
1593
|
-
placeholder={t('placeholders.assignmentSearch')}
|
|
1594
|
-
onChange={(event) => setAssignmentSearch(event.target.value)}
|
|
1595
|
-
/>
|
|
1596
|
-
</div>
|
|
1597
|
-
|
|
1598
|
-
<div className="space-y-2">
|
|
1599
|
-
{filteredAssignments.map((assignment) => {
|
|
1600
|
-
const collaborator = collaborators.find(
|
|
1601
|
-
(item) => item.id === assignment.collaboratorId
|
|
1602
|
-
);
|
|
1603
|
-
const assignmentIndex = form.teamAssignments.findIndex(
|
|
1604
|
-
(item) => item.collaboratorId === assignment.collaboratorId
|
|
1605
|
-
);
|
|
1606
|
-
const roleError =
|
|
1607
|
-
assignmentIndex >= 0
|
|
1608
|
-
? formMethods.formState.errors.teamAssignments?.[
|
|
1609
|
-
assignmentIndex
|
|
1610
|
-
]?.roleLabel
|
|
1611
|
-
: undefined;
|
|
1612
|
-
|
|
1613
|
-
if (!collaborator) {
|
|
1614
|
-
return null;
|
|
1615
|
-
}
|
|
1616
|
-
|
|
1617
|
-
return (
|
|
1618
|
-
<div
|
|
1619
|
-
key={assignment.collaboratorId}
|
|
1620
|
-
className="grid min-w-0 gap-2 rounded-lg border px-3 py-2 xl:grid-cols-[minmax(0,1.25fr)_minmax(0,1.2fr)_110px_110px_auto]"
|
|
1621
|
-
>
|
|
1622
|
-
<label className="flex cursor-pointer items-start gap-3 py-1">
|
|
1623
|
-
<Checkbox
|
|
1624
|
-
checked={assignment.selected}
|
|
1625
|
-
onCheckedChange={(checked) =>
|
|
1626
|
-
updateAssignment(assignment.collaboratorId, {
|
|
1627
|
-
selected: checked === true,
|
|
1628
|
-
})
|
|
1629
|
-
}
|
|
1630
|
-
/>
|
|
1631
|
-
<div className="min-w-0">
|
|
1632
|
-
<div className="truncate font-medium">
|
|
1633
|
-
{collaborator.displayName}
|
|
1634
|
-
</div>
|
|
1635
|
-
<div className="truncate text-xs text-muted-foreground">
|
|
1636
|
-
{[
|
|
1637
|
-
collaborator.department,
|
|
1638
|
-
collaborator.title,
|
|
1639
|
-
collaborator.code,
|
|
1640
|
-
]
|
|
1641
|
-
.filter(Boolean)
|
|
1642
|
-
.join(' • ') || commonT('labels.notAvailable')}
|
|
1643
|
-
</div>
|
|
1612
|
+
{selectedContractTemplate ? (
|
|
1613
|
+
<div className="mt-3 rounded-md bg-muted/40 px-2.5 py-2">
|
|
1614
|
+
<div className="text-xs font-medium text-foreground">
|
|
1615
|
+
{t('labels.templateSelected')}
|
|
1616
|
+
</div>
|
|
1617
|
+
<div className="mt-1 text-[11px] text-muted-foreground">
|
|
1618
|
+
{[
|
|
1619
|
+
selectedContractTemplate.name,
|
|
1620
|
+
selectedContractTemplate.code,
|
|
1621
|
+
formatEnumLabel(
|
|
1622
|
+
selectedContractTemplate.contractType
|
|
1623
|
+
),
|
|
1624
|
+
]
|
|
1625
|
+
.filter(Boolean)
|
|
1626
|
+
.join(' • ')}
|
|
1644
1627
|
</div>
|
|
1645
|
-
</label>
|
|
1646
|
-
<div className="space-y-1">
|
|
1647
|
-
<DepartmentSelectWithCreate
|
|
1648
|
-
label=""
|
|
1649
|
-
value={assignment.roleLabel}
|
|
1650
|
-
options={projectRoleOptions}
|
|
1651
|
-
disabled={!assignment.selected}
|
|
1652
|
-
selectPlaceholder={t('placeholders.roleLabel')}
|
|
1653
|
-
createDescription={t('fields.roleLabel')}
|
|
1654
|
-
createPlaceholder={t('placeholders.roleLabelCreate')}
|
|
1655
|
-
onChange={(role) =>
|
|
1656
|
-
updateAssignment(assignment.collaboratorId, {
|
|
1657
|
-
projectRoleId: role.id ? String(role.id) : 'none',
|
|
1658
|
-
roleLabel: role.name,
|
|
1659
|
-
})
|
|
1660
|
-
}
|
|
1661
|
-
onCreate={createProjectRole}
|
|
1662
|
-
/>
|
|
1663
|
-
{roleError?.message ? (
|
|
1664
|
-
<p className="text-sm text-destructive">
|
|
1665
|
-
{String(roleError.message)}
|
|
1666
|
-
</p>
|
|
1667
|
-
) : null}
|
|
1668
1628
|
</div>
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
})
|
|
1691
|
-
}
|
|
1692
|
-
/>
|
|
1693
|
-
<div className="flex items-center justify-between gap-3 px-1 py-2">
|
|
1694
|
-
<div className="flex items-center gap-1.5 text-xs font-medium">
|
|
1695
|
-
<span>{t('fields.isBillable')}</span>
|
|
1629
|
+
) : null}
|
|
1630
|
+
|
|
1631
|
+
<div className="mt-3 flex flex-wrap gap-2">
|
|
1632
|
+
{selectedContract ? (
|
|
1633
|
+
<Button type="button" variant="outline" size="sm" asChild>
|
|
1634
|
+
<Link
|
|
1635
|
+
href={`/operations/contracts?edit=${selectedContract.id}`}
|
|
1636
|
+
>
|
|
1637
|
+
<FileText className="size-4" />
|
|
1638
|
+
{commonT('actions.openContract')}
|
|
1639
|
+
</Link>
|
|
1640
|
+
</Button>
|
|
1641
|
+
) : null}
|
|
1642
|
+
</div>
|
|
1643
|
+
</div>
|
|
1644
|
+
|
|
1645
|
+
<div className="min-w-0 px-1 py-2">
|
|
1646
|
+
<div className="flex items-center justify-between gap-3">
|
|
1647
|
+
<div className="min-w-0">
|
|
1648
|
+
<div className="flex items-center gap-1.5 text-sm font-medium">
|
|
1649
|
+
<span>{t('fields.autoGenerateContractDraft')}</span>
|
|
1696
1650
|
<Tooltip>
|
|
1697
1651
|
<TooltipTrigger asChild>
|
|
1698
1652
|
<span className="inline-flex cursor-help text-muted-foreground">
|
|
@@ -1700,26 +1654,197 @@ export function ProjectFormScreen({
|
|
|
1700
1654
|
</span>
|
|
1701
1655
|
</TooltipTrigger>
|
|
1702
1656
|
<TooltipContent>
|
|
1703
|
-
<p>
|
|
1657
|
+
<p>
|
|
1658
|
+
{form.contractId === 'none'
|
|
1659
|
+
? t(
|
|
1660
|
+
'fields.autoGenerateContractDraftDescription'
|
|
1661
|
+
)
|
|
1662
|
+
: t('fields.existingContractSelected')}
|
|
1663
|
+
</p>
|
|
1704
1664
|
</TooltipContent>
|
|
1705
1665
|
</Tooltip>
|
|
1706
1666
|
</div>
|
|
1707
|
-
<
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
isBillable: checked,
|
|
1713
|
-
})
|
|
1714
|
-
}
|
|
1715
|
-
/>
|
|
1667
|
+
<div className="text-[11px] text-muted-foreground">
|
|
1668
|
+
{form.contractId === 'none'
|
|
1669
|
+
? t('labels.enabled')
|
|
1670
|
+
: t('labels.disabled')}
|
|
1671
|
+
</div>
|
|
1716
1672
|
</div>
|
|
1673
|
+
<Switch
|
|
1674
|
+
checked={
|
|
1675
|
+
form.contractId === 'none' &&
|
|
1676
|
+
form.autoGenerateContractDraft
|
|
1677
|
+
}
|
|
1678
|
+
disabled={form.contractId !== 'none'}
|
|
1679
|
+
onCheckedChange={(checked) =>
|
|
1680
|
+
setForm((current) => ({
|
|
1681
|
+
...current,
|
|
1682
|
+
autoGenerateContractDraft: checked,
|
|
1683
|
+
}))
|
|
1684
|
+
}
|
|
1685
|
+
/>
|
|
1717
1686
|
</div>
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
</
|
|
1721
|
-
|
|
1722
|
-
|
|
1687
|
+
</div>
|
|
1688
|
+
</div>
|
|
1689
|
+
</section>
|
|
1690
|
+
|
|
1691
|
+
<section className="space-y-3">
|
|
1692
|
+
<div className="space-y-0.5">
|
|
1693
|
+
<h3 className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
1694
|
+
{t('sections.team')}
|
|
1695
|
+
</h3>
|
|
1696
|
+
<p className="text-[11px] text-muted-foreground/80">
|
|
1697
|
+
{t('sections.teamDescription', {
|
|
1698
|
+
count: selectedAssignmentsCount,
|
|
1699
|
+
})}
|
|
1700
|
+
</p>
|
|
1701
|
+
</div>
|
|
1702
|
+
<div className="space-y-3">
|
|
1703
|
+
<div className="relative">
|
|
1704
|
+
<Search className="pointer-events-none absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
1705
|
+
<Input
|
|
1706
|
+
className="pl-9"
|
|
1707
|
+
value={assignmentSearch}
|
|
1708
|
+
placeholder={t('placeholders.assignmentSearch')}
|
|
1709
|
+
onChange={(event) =>
|
|
1710
|
+
setAssignmentSearch(event.target.value)
|
|
1711
|
+
}
|
|
1712
|
+
/>
|
|
1713
|
+
</div>
|
|
1714
|
+
|
|
1715
|
+
<div className="space-y-2">
|
|
1716
|
+
{filteredAssignments.map((assignment) => {
|
|
1717
|
+
const collaborator = availableCollaborators.find(
|
|
1718
|
+
(item) => item.id === assignment.collaboratorId
|
|
1719
|
+
);
|
|
1720
|
+
const assignmentIndex = form.teamAssignments.findIndex(
|
|
1721
|
+
(item) =>
|
|
1722
|
+
item.collaboratorId === assignment.collaboratorId
|
|
1723
|
+
);
|
|
1724
|
+
const roleError =
|
|
1725
|
+
assignmentIndex >= 0
|
|
1726
|
+
? formMethods.formState.errors.teamAssignments?.[
|
|
1727
|
+
assignmentIndex
|
|
1728
|
+
]?.roleLabel
|
|
1729
|
+
: undefined;
|
|
1730
|
+
|
|
1731
|
+
if (!collaborator) {
|
|
1732
|
+
return null;
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
return (
|
|
1736
|
+
<div
|
|
1737
|
+
key={assignment.collaboratorId}
|
|
1738
|
+
className="grid min-w-0 gap-2 rounded-lg border px-3 py-2 xl:grid-cols-[minmax(0,1.25fr)_minmax(0,1.2fr)_110px_110px_auto]"
|
|
1739
|
+
>
|
|
1740
|
+
<label className="flex cursor-pointer items-start gap-3 py-1">
|
|
1741
|
+
<Checkbox
|
|
1742
|
+
checked={assignment.selected}
|
|
1743
|
+
onCheckedChange={(checked) =>
|
|
1744
|
+
updateAssignment(assignment.collaboratorId, {
|
|
1745
|
+
selected: checked === true,
|
|
1746
|
+
})
|
|
1747
|
+
}
|
|
1748
|
+
/>
|
|
1749
|
+
<div className="min-w-0">
|
|
1750
|
+
<div className="truncate font-medium">
|
|
1751
|
+
{collaborator.displayName}
|
|
1752
|
+
</div>
|
|
1753
|
+
<div className="truncate text-xs text-muted-foreground">
|
|
1754
|
+
{[
|
|
1755
|
+
collaborator.department,
|
|
1756
|
+
collaborator.title,
|
|
1757
|
+
collaborator.code,
|
|
1758
|
+
]
|
|
1759
|
+
.filter(Boolean)
|
|
1760
|
+
.join(' • ') || commonT('labels.notAvailable')}
|
|
1761
|
+
</div>
|
|
1762
|
+
</div>
|
|
1763
|
+
</label>
|
|
1764
|
+
<div className="space-y-1">
|
|
1765
|
+
<DepartmentSelectWithCreate
|
|
1766
|
+
label=""
|
|
1767
|
+
value={assignment.roleLabel}
|
|
1768
|
+
options={projectRoleOptions}
|
|
1769
|
+
disabled={!assignment.selected}
|
|
1770
|
+
selectPlaceholder={t('placeholders.roleLabel')}
|
|
1771
|
+
createDescription={t('fields.roleLabel')}
|
|
1772
|
+
createPlaceholder={t(
|
|
1773
|
+
'placeholders.roleLabelCreate'
|
|
1774
|
+
)}
|
|
1775
|
+
onChange={(role) =>
|
|
1776
|
+
updateAssignment(assignment.collaboratorId, {
|
|
1777
|
+
projectRoleId: role.id
|
|
1778
|
+
? String(role.id)
|
|
1779
|
+
: 'none',
|
|
1780
|
+
roleLabel: role.name,
|
|
1781
|
+
})
|
|
1782
|
+
}
|
|
1783
|
+
onCreate={createProjectRole}
|
|
1784
|
+
/>
|
|
1785
|
+
{roleError?.message ? (
|
|
1786
|
+
<p className="text-sm text-destructive">
|
|
1787
|
+
{String(roleError.message)}
|
|
1788
|
+
</p>
|
|
1789
|
+
) : null}
|
|
1790
|
+
</div>
|
|
1791
|
+
<Input
|
|
1792
|
+
className="h-9"
|
|
1793
|
+
type="number"
|
|
1794
|
+
placeholder={t('fields.weeklyHours')}
|
|
1795
|
+
value={assignment.weeklyHours}
|
|
1796
|
+
disabled={!assignment.selected}
|
|
1797
|
+
onChange={(event) =>
|
|
1798
|
+
updateAssignment(assignment.collaboratorId, {
|
|
1799
|
+
weeklyHours: event.target.value,
|
|
1800
|
+
})
|
|
1801
|
+
}
|
|
1802
|
+
/>
|
|
1803
|
+
<Input
|
|
1804
|
+
className="h-9"
|
|
1805
|
+
type="text"
|
|
1806
|
+
inputMode="decimal"
|
|
1807
|
+
placeholder={t('fields.allocationPercent')}
|
|
1808
|
+
value={assignment.allocationPercent}
|
|
1809
|
+
disabled={!assignment.selected}
|
|
1810
|
+
onChange={(event) =>
|
|
1811
|
+
updateAssignment(assignment.collaboratorId, {
|
|
1812
|
+
allocationPercent: normalizePercentInput(
|
|
1813
|
+
event.target.value
|
|
1814
|
+
),
|
|
1815
|
+
})
|
|
1816
|
+
}
|
|
1817
|
+
/>
|
|
1818
|
+
<div className="flex items-center justify-between gap-3 px-1 py-2">
|
|
1819
|
+
<div className="flex items-center gap-1.5 text-xs font-medium">
|
|
1820
|
+
<span>{t('fields.isBillable')}</span>
|
|
1821
|
+
<Tooltip>
|
|
1822
|
+
<TooltipTrigger asChild>
|
|
1823
|
+
<span className="inline-flex cursor-help text-muted-foreground">
|
|
1824
|
+
<Info className="h-3.5 w-3.5" />
|
|
1825
|
+
</span>
|
|
1826
|
+
</TooltipTrigger>
|
|
1827
|
+
<TooltipContent>
|
|
1828
|
+
<p>{t('fields.isBillableDescription')}</p>
|
|
1829
|
+
</TooltipContent>
|
|
1830
|
+
</Tooltip>
|
|
1831
|
+
</div>
|
|
1832
|
+
<Switch
|
|
1833
|
+
checked={assignment.isBillable}
|
|
1834
|
+
disabled={!assignment.selected}
|
|
1835
|
+
onCheckedChange={(checked) =>
|
|
1836
|
+
updateAssignment(assignment.collaboratorId, {
|
|
1837
|
+
isBillable: checked,
|
|
1838
|
+
})
|
|
1839
|
+
}
|
|
1840
|
+
/>
|
|
1841
|
+
</div>
|
|
1842
|
+
</div>
|
|
1843
|
+
);
|
|
1844
|
+
})}
|
|
1845
|
+
</div>
|
|
1846
|
+
</div>
|
|
1847
|
+
</section>
|
|
1723
1848
|
</>
|
|
1724
1849
|
) : (
|
|
1725
1850
|
<section className="space-y-3 rounded-lg border border-dashed bg-muted/20 px-4 py-4">
|