@hed-hog/operations 0.0.295 → 0.0.296

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/dist/operations.controller.d.ts +415 -0
  2. package/dist/operations.controller.d.ts.map +1 -0
  3. package/dist/operations.controller.js +333 -0
  4. package/dist/operations.controller.js.map +1 -0
  5. package/dist/operations.module.d.ts.map +1 -1
  6. package/dist/operations.module.js +4 -3
  7. package/dist/operations.module.js.map +1 -1
  8. package/dist/operations.service.d.ts +589 -153
  9. package/dist/operations.service.d.ts.map +1 -1
  10. package/dist/operations.service.js +2229 -100
  11. package/dist/operations.service.js.map +1 -1
  12. package/hedhog/data/menu.yaml +198 -251
  13. package/hedhog/data/role.yaml +23 -14
  14. package/hedhog/data/route.yaml +317 -143
  15. package/hedhog/frontend/app/_components/collaborator-details-screen.tsx.ejs +310 -0
  16. package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +631 -0
  17. package/hedhog/frontend/app/_components/contract-details-screen.tsx.ejs +132 -0
  18. package/hedhog/frontend/app/_components/contract-form-screen.tsx.ejs +558 -0
  19. package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +291 -0
  20. package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +689 -0
  21. package/hedhog/frontend/app/_lib/api.ts.ejs +32 -0
  22. package/hedhog/frontend/app/_lib/hooks/use-operations-access.ts.ejs +44 -0
  23. package/hedhog/frontend/app/_lib/types.ts.ejs +360 -0
  24. package/hedhog/frontend/app/_lib/utils/format.ts.ejs +129 -25
  25. package/hedhog/frontend/app/_lib/utils/forms.ts.ejs +14 -0
  26. package/hedhog/frontend/app/approvals/page.tsx.ejs +386 -147
  27. package/hedhog/frontend/app/collaborators/[id]/edit/page.tsx.ejs +11 -0
  28. package/hedhog/frontend/app/collaborators/[id]/page.tsx.ejs +11 -0
  29. package/hedhog/frontend/app/collaborators/new/page.tsx.ejs +5 -0
  30. package/hedhog/frontend/app/collaborators/page.tsx.ejs +261 -0
  31. package/hedhog/frontend/app/contracts/[id]/edit/page.tsx.ejs +11 -0
  32. package/hedhog/frontend/app/contracts/[id]/page.tsx.ejs +11 -108
  33. package/hedhog/frontend/app/contracts/new/page.tsx.ejs +17 -0
  34. package/hedhog/frontend/app/contracts/page.tsx.ejs +262 -181
  35. package/hedhog/frontend/app/page.tsx.ejs +319 -177
  36. package/hedhog/frontend/app/projects/[id]/edit/page.tsx.ejs +11 -0
  37. package/hedhog/frontend/app/projects/[id]/page.tsx.ejs +11 -936
  38. package/hedhog/frontend/app/projects/new/page.tsx.ejs +5 -0
  39. package/hedhog/frontend/app/projects/page.tsx.ejs +236 -1074
  40. package/hedhog/frontend/app/schedule-adjustments/page.tsx.ejs +418 -0
  41. package/hedhog/frontend/app/team/page.tsx.ejs +339 -0
  42. package/hedhog/frontend/app/time-off/page.tsx.ejs +328 -0
  43. package/hedhog/frontend/app/timesheets/page.tsx.ejs +636 -126
  44. package/hedhog/frontend/messages/en.json +648 -454
  45. package/hedhog/frontend/messages/pt.json +647 -454
  46. package/hedhog/table/operations_approval.yaml +49 -0
  47. package/hedhog/table/operations_approval_history.yaml +29 -0
  48. package/hedhog/table/{operations_employee.yaml → operations_collaborator.yaml} +67 -64
  49. package/hedhog/table/operations_collaborator_schedule_day.yaml +34 -0
  50. package/hedhog/table/operations_contract.yaml +100 -48
  51. package/hedhog/table/operations_contract_document.yaml +39 -0
  52. package/hedhog/table/operations_contract_financial_term.yaml +40 -0
  53. package/hedhog/table/operations_contract_history.yaml +27 -0
  54. package/hedhog/table/operations_contract_party.yaml +46 -0
  55. package/hedhog/table/operations_contract_revision.yaml +38 -0
  56. package/hedhog/table/operations_contract_signature.yaml +38 -0
  57. package/hedhog/table/operations_project.yaml +54 -50
  58. package/hedhog/table/{operations_allocation.yaml → operations_project_assignment.yaml} +55 -52
  59. package/hedhog/table/operations_schedule_adjustment_day.yaml +34 -0
  60. package/hedhog/table/operations_schedule_adjustment_request.yaml +53 -0
  61. package/hedhog/table/operations_time_off_request.yaml +57 -0
  62. package/hedhog/table/operations_timesheet.yaml +41 -36
  63. package/hedhog/table/operations_timesheet_entry.yaml +40 -50
  64. package/package.json +8 -7
  65. package/src/operations.controller.ts +182 -0
  66. package/src/operations.module.ts +22 -21
  67. package/src/operations.service.ts +3595 -137
  68. package/hedhog/data/operations_career_level.yaml +0 -102
  69. package/hedhog/data/operations_career_track.yaml +0 -8
  70. package/hedhog/data/operations_certification.yaml +0 -38
  71. package/hedhog/data/operations_evaluation_cycle.yaml +0 -18
  72. package/hedhog/data/operations_performance_criterion.yaml +0 -48
  73. package/hedhog/frontend/app/_components/allocation-calendar.tsx.ejs +0 -56
  74. package/hedhog/frontend/app/_components/kanban-board.tsx.ejs +0 -626
  75. package/hedhog/frontend/app/_components/timesheet-entry-dialog.tsx.ejs +0 -142
  76. package/hedhog/frontend/app/_lib/hooks/use-operations-data.ts.ejs +0 -41
  77. package/hedhog/frontend/app/_lib/hooks/use-operations-growth-data.ts.ejs +0 -63
  78. package/hedhog/frontend/app/_lib/mocks/allocations.mock.ts.ejs +0 -74
  79. package/hedhog/frontend/app/_lib/mocks/contracts.mock.ts.ejs +0 -74
  80. package/hedhog/frontend/app/_lib/mocks/operations-growth.mock.ts.ejs +0 -824
  81. package/hedhog/frontend/app/_lib/mocks/projects.mock.ts.ejs +0 -455
  82. package/hedhog/frontend/app/_lib/mocks/tasks.mock.ts.ejs +0 -117
  83. package/hedhog/frontend/app/_lib/mocks/timesheets.mock.ts.ejs +0 -84
  84. package/hedhog/frontend/app/_lib/mocks/users.mock.ts.ejs +0 -67
  85. package/hedhog/frontend/app/_lib/services/contracts.service.ts.ejs +0 -10
  86. package/hedhog/frontend/app/_lib/services/operations-growth.service.ts.ejs +0 -31
  87. package/hedhog/frontend/app/_lib/services/projects.service.ts.ejs +0 -10
  88. package/hedhog/frontend/app/_lib/services/tasks.service.ts.ejs +0 -10
  89. package/hedhog/frontend/app/_lib/services/timesheets.service.ts.ejs +0 -10
  90. package/hedhog/frontend/app/_lib/types/operations-growth.ts.ejs +0 -209
  91. package/hedhog/frontend/app/_lib/types/operations.ts.ejs +0 -156
  92. package/hedhog/frontend/app/_lib/utils/growth.ts.ejs +0 -62
  93. package/hedhog/frontend/app/_lib/utils/metrics.ts.ejs +0 -103
  94. package/hedhog/frontend/app/_lib/utils/status.ts.ejs +0 -80
  95. package/hedhog/frontend/app/allocations/page.tsx.ejs +0 -155
  96. package/hedhog/frontend/app/career/page.tsx.ejs +0 -143
  97. package/hedhog/frontend/app/certifications/page.tsx.ejs +0 -202
  98. package/hedhog/frontend/app/evaluations/page.tsx.ejs +0 -278
  99. package/hedhog/frontend/app/goals/page.tsx.ejs +0 -171
  100. package/hedhog/frontend/app/growth/page.tsx.ejs +0 -288
  101. package/hedhog/frontend/app/manager/page.tsx.ejs +0 -175
  102. package/hedhog/frontend/app/rewards/page.tsx.ejs +0 -196
  103. package/hedhog/frontend/app/tasks/page.tsx.ejs +0 -999
  104. package/hedhog/table/operations_calibration_item.yaml +0 -61
  105. package/hedhog/table/operations_calibration_session.yaml +0 -25
  106. package/hedhog/table/operations_career_level.yaml +0 -75
  107. package/hedhog/table/operations_career_track.yaml +0 -21
  108. package/hedhog/table/operations_certification.yaml +0 -48
  109. package/hedhog/table/operations_employee_certification.yaml +0 -43
  110. package/hedhog/table/operations_employee_connect.yaml +0 -61
  111. package/hedhog/table/operations_employee_evaluation.yaml +0 -113
  112. package/hedhog/table/operations_employee_evaluation_item.yaml +0 -39
  113. package/hedhog/table/operations_employee_profile.yaml +0 -80
  114. package/hedhog/table/operations_employee_skill_matrix.yaml +0 -30
  115. package/hedhog/table/operations_evaluation_cycle.yaml +0 -31
  116. package/hedhog/table/operations_goal.yaml +0 -67
  117. package/hedhog/table/operations_goal_progress.yaml +0 -31
  118. package/hedhog/table/operations_performance_criterion.yaml +0 -29
  119. package/hedhog/table/operations_promotion_readiness.yaml +0 -49
  120. package/hedhog/table/operations_promotion_recommendation.yaml +0 -63
  121. package/hedhog/table/operations_public_recognition.yaml +0 -46
  122. package/hedhog/table/operations_reward.yaml +0 -100
  123. package/hedhog/table/operations_score_event.yaml +0 -81
  124. package/hedhog/table/operations_task.yaml +0 -60
  125. package/src/operations-data.controller.ts +0 -54
  126. package/src/operations-growth.controller.ts +0 -44
@@ -0,0 +1,689 @@
1
+ 'use client';
2
+
3
+ import { EmptyState, Page } from '@/components/entity-list';
4
+ import { Button } from '@/components/ui/button';
5
+ import { Checkbox } from '@/components/ui/checkbox';
6
+ import { Input } from '@/components/ui/input';
7
+ import {
8
+ Select,
9
+ SelectContent,
10
+ SelectItem,
11
+ SelectTrigger,
12
+ SelectValue,
13
+ } from '@/components/ui/select';
14
+ import { Switch } from '@/components/ui/switch';
15
+ import { Textarea } from '@/components/ui/textarea';
16
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
17
+ import { ArrowLeft, FolderKanban, Save } from 'lucide-react';
18
+ import Link from 'next/link';
19
+ import { useRouter } from 'next/navigation';
20
+ import { useEffect, useState } from 'react';
21
+ import { useTranslations } from 'next-intl';
22
+ import { OperationsHeader } from './operations-header';
23
+ import { SectionCard } from './section-card';
24
+ import { fetchOperations, mutateOperations } from '../_lib/api';
25
+ import { useOperationsAccess } from '../_lib/hooks/use-operations-access';
26
+ import type {
27
+ OperationsCollaborator,
28
+ OperationsContract,
29
+ OperationsProjectDetails,
30
+ } from '../_lib/types';
31
+ import { parseNumberInput, trimToNull } from '../_lib/utils/forms';
32
+ import { formatEnumLabel } from '../_lib/utils/format';
33
+
34
+ type TeamAssignmentState = {
35
+ collaboratorId: number;
36
+ selected: boolean;
37
+ roleLabel: string;
38
+ weeklyHours: string;
39
+ allocationPercent: string;
40
+ isBillable: boolean;
41
+ status: string;
42
+ };
43
+
44
+ type ProjectFormState = {
45
+ contractId: string;
46
+ managerCollaboratorId: string;
47
+ code: string;
48
+ name: string;
49
+ clientName: string;
50
+ summary: string;
51
+ status: string;
52
+ progressPercent: string;
53
+ deliveryModel: string;
54
+ budgetAmount: string;
55
+ startDate: string;
56
+ endDate: string;
57
+ billingModel: string;
58
+ monthlyHourCap: string;
59
+ contractCode: string;
60
+ contractName: string;
61
+ contractDescription: string;
62
+ autoGenerateContractDraft: boolean;
63
+ teamAssignments: TeamAssignmentState[];
64
+ };
65
+
66
+ function buildEmptyForm(
67
+ collaborators: OperationsCollaborator[] = []
68
+ ): ProjectFormState {
69
+ return {
70
+ contractId: 'none',
71
+ managerCollaboratorId: 'none',
72
+ code: '',
73
+ name: '',
74
+ clientName: '',
75
+ summary: '',
76
+ status: 'planning',
77
+ progressPercent: '',
78
+ deliveryModel: 'project_delivery',
79
+ budgetAmount: '',
80
+ startDate: '',
81
+ endDate: '',
82
+ billingModel: 'time_and_material',
83
+ monthlyHourCap: '',
84
+ contractCode: '',
85
+ contractName: '',
86
+ contractDescription: '',
87
+ autoGenerateContractDraft: true,
88
+ teamAssignments: collaborators.map((collaborator) => ({
89
+ collaboratorId: collaborator.id,
90
+ selected: false,
91
+ roleLabel: '',
92
+ weeklyHours: '',
93
+ allocationPercent: '',
94
+ isBillable: true,
95
+ status: 'active',
96
+ })),
97
+ };
98
+ }
99
+
100
+ function toFormState(
101
+ project: OperationsProjectDetails,
102
+ collaborators: OperationsCollaborator[]
103
+ ): ProjectFormState {
104
+ const assignments = new Map(
105
+ (project.assignments ?? []).map((assignment) => [
106
+ assignment.collaboratorId,
107
+ assignment,
108
+ ])
109
+ );
110
+
111
+ return {
112
+ contractId: project.contractId ? String(project.contractId) : 'none',
113
+ managerCollaboratorId: project.managerCollaboratorId
114
+ ? String(project.managerCollaboratorId)
115
+ : 'none',
116
+ code: project.code ?? '',
117
+ name: project.name ?? '',
118
+ clientName: project.clientName ?? '',
119
+ summary: project.summary ?? '',
120
+ status: project.status ?? 'planning',
121
+ progressPercent:
122
+ project.progressPercent !== null && project.progressPercent !== undefined
123
+ ? String(project.progressPercent)
124
+ : '',
125
+ deliveryModel: project.deliveryModel ?? 'project_delivery',
126
+ budgetAmount:
127
+ project.budgetAmount !== null && project.budgetAmount !== undefined
128
+ ? String(project.budgetAmount)
129
+ : '',
130
+ startDate: project.startDate ?? '',
131
+ endDate: project.endDate ?? '',
132
+ billingModel:
133
+ project.relatedContract?.billingModel ?? 'time_and_material',
134
+ monthlyHourCap:
135
+ project.relatedContract?.monthlyHourCap !== null &&
136
+ project.relatedContract?.monthlyHourCap !== undefined
137
+ ? String(project.relatedContract.monthlyHourCap)
138
+ : '',
139
+ contractCode: project.relatedContract?.code ?? '',
140
+ contractName: project.relatedContract?.name ?? '',
141
+ contractDescription: project.relatedContract?.description ?? '',
142
+ autoGenerateContractDraft: true,
143
+ teamAssignments: collaborators.map((collaborator) => {
144
+ const assignment = assignments.get(collaborator.id);
145
+ return {
146
+ collaboratorId: collaborator.id,
147
+ selected: Boolean(assignment),
148
+ roleLabel: assignment?.roleLabel ?? '',
149
+ weeklyHours:
150
+ assignment?.weeklyHours !== null && assignment?.weeklyHours !== undefined
151
+ ? String(assignment.weeklyHours)
152
+ : '',
153
+ allocationPercent:
154
+ assignment?.allocationPercent !== null &&
155
+ assignment?.allocationPercent !== undefined
156
+ ? String(assignment.allocationPercent)
157
+ : '',
158
+ isBillable: assignment?.isBillable ?? true,
159
+ status: assignment?.status ?? 'active',
160
+ };
161
+ }),
162
+ };
163
+ }
164
+
165
+ export function ProjectFormScreen({ projectId }: { projectId?: number }) {
166
+ const t = useTranslations('operations.ProjectFormPage');
167
+ const commonT = useTranslations('operations.Common');
168
+ const { request, showToastHandler, currentLocaleCode } = useApp();
169
+ const access = useOperationsAccess();
170
+ const router = useRouter();
171
+ const [form, setForm] = useState<ProjectFormState>(buildEmptyForm());
172
+
173
+ const { data: collaborators = [] } = useQuery<OperationsCollaborator[]>({
174
+ queryKey: ['operations-project-form-collaborators', currentLocaleCode],
175
+ enabled: access.isDirector,
176
+ queryFn: () =>
177
+ fetchOperations<OperationsCollaborator[]>(request, '/operations/collaborators'),
178
+ });
179
+
180
+ const { data: contracts = [] } = useQuery<OperationsContract[]>({
181
+ queryKey: ['operations-project-form-contracts', currentLocaleCode],
182
+ enabled: access.isDirector,
183
+ queryFn: () =>
184
+ fetchOperations<OperationsContract[]>(request, '/operations/contracts'),
185
+ });
186
+
187
+ const { data: project, isLoading: isLoadingProject } =
188
+ useQuery<OperationsProjectDetails>({
189
+ queryKey: ['operations-project-form', currentLocaleCode, projectId],
190
+ enabled: Boolean(projectId),
191
+ queryFn: () =>
192
+ fetchOperations<OperationsProjectDetails>(
193
+ request,
194
+ `/operations/projects/${projectId}`
195
+ ),
196
+ });
197
+
198
+ useEffect(() => {
199
+ if (!collaborators.length) {
200
+ return;
201
+ }
202
+
203
+ if (project) {
204
+ setForm(toFormState(project, collaborators));
205
+ return;
206
+ }
207
+
208
+ setForm((current) =>
209
+ current.teamAssignments.length ? current : buildEmptyForm(collaborators)
210
+ );
211
+ }, [collaborators, project]);
212
+
213
+ const updateAssignment = (
214
+ collaboratorId: number,
215
+ patch: Partial<TeamAssignmentState>
216
+ ) => {
217
+ setForm((current) => ({
218
+ ...current,
219
+ teamAssignments: current.teamAssignments.map((assignment) =>
220
+ assignment.collaboratorId === collaboratorId
221
+ ? { ...assignment, ...patch }
222
+ : assignment
223
+ ),
224
+ }));
225
+ };
226
+
227
+ const onSubmit = async () => {
228
+ if (!form.code.trim() || !form.name.trim() || !form.clientName.trim()) {
229
+ showToastHandler?.('error', t('messages.requiredFields'));
230
+ return;
231
+ }
232
+
233
+ const payload = {
234
+ contractId: form.contractId === 'none' ? null : parseNumberInput(form.contractId),
235
+ managerCollaboratorId:
236
+ form.managerCollaboratorId === 'none'
237
+ ? null
238
+ : parseNumberInput(form.managerCollaboratorId),
239
+ code: form.code.trim(),
240
+ name: form.name.trim(),
241
+ clientName: trimToNull(form.clientName),
242
+ summary: trimToNull(form.summary),
243
+ status: form.status,
244
+ progressPercent: parseNumberInput(form.progressPercent),
245
+ deliveryModel: form.deliveryModel,
246
+ budgetAmount: parseNumberInput(form.budgetAmount),
247
+ startDate: trimToNull(form.startDate),
248
+ endDate: trimToNull(form.endDate),
249
+ billingModel: form.billingModel,
250
+ monthlyHourCap: parseNumberInput(form.monthlyHourCap),
251
+ contractCode: trimToNull(form.contractCode),
252
+ contractName: trimToNull(form.contractName),
253
+ contractDescription: trimToNull(form.contractDescription),
254
+ autoGenerateContractDraft: form.contractId === 'none'
255
+ ? form.autoGenerateContractDraft
256
+ : false,
257
+ teamAssignments: form.teamAssignments
258
+ .filter((assignment) => assignment.selected)
259
+ .map((assignment) => ({
260
+ collaboratorId: assignment.collaboratorId,
261
+ roleLabel: trimToNull(assignment.roleLabel),
262
+ weeklyHours: parseNumberInput(assignment.weeklyHours),
263
+ allocationPercent: parseNumberInput(assignment.allocationPercent),
264
+ isBillable: assignment.isBillable,
265
+ status: assignment.status,
266
+ startDate: trimToNull(form.startDate),
267
+ endDate: trimToNull(form.endDate),
268
+ })),
269
+ };
270
+
271
+ try {
272
+ const response = projectId
273
+ ? await mutateOperations<OperationsProjectDetails>(
274
+ request,
275
+ `/operations/projects/${projectId}`,
276
+ 'PATCH',
277
+ payload
278
+ )
279
+ : await mutateOperations<OperationsProjectDetails>(
280
+ request,
281
+ '/operations/projects',
282
+ 'POST',
283
+ payload
284
+ );
285
+
286
+ showToastHandler?.(
287
+ 'success',
288
+ projectId ? t('messages.updateSuccess') : t('messages.createSuccess')
289
+ );
290
+ router.push(`/operations/projects/${response.id}`);
291
+ } catch {
292
+ showToastHandler?.(
293
+ 'error',
294
+ projectId ? t('messages.updateError') : t('messages.createError')
295
+ );
296
+ }
297
+ };
298
+
299
+ if (!access.isDirector && !access.isLoading) {
300
+ return (
301
+ <Page>
302
+ <OperationsHeader
303
+ title={t(projectId ? 'editTitle' : 'newTitle')}
304
+ description={t('description')}
305
+ current={t('breadcrumb')}
306
+ />
307
+ <EmptyState
308
+ icon={<FolderKanban className="size-12" />}
309
+ title={commonT('states.noAccessTitle')}
310
+ description={t('noAccessDescription')}
311
+ actionLabel={commonT('actions.refresh')}
312
+ onAction={() => router.refresh()}
313
+ />
314
+ </Page>
315
+ );
316
+ }
317
+
318
+ return (
319
+ <Page>
320
+ <OperationsHeader
321
+ title={t(projectId ? 'editTitle' : 'newTitle')}
322
+ description={t('description')}
323
+ current={t('breadcrumb')}
324
+ actions={
325
+ <div className="flex gap-2">
326
+ <Button variant="outline" size="sm" asChild>
327
+ <Link
328
+ href={projectId ? `/operations/projects/${projectId}` : '/operations/projects'}
329
+ >
330
+ <ArrowLeft className="size-4" />
331
+ {commonT('actions.back')}
332
+ </Link>
333
+ </Button>
334
+ <Button size="sm" onClick={() => void onSubmit()}>
335
+ <Save className="size-4" />
336
+ {commonT('actions.save')}
337
+ </Button>
338
+ </div>
339
+ }
340
+ />
341
+
342
+ <div className="grid gap-4 xl:grid-cols-2">
343
+ <SectionCard title={t('sections.basicInfo')} description={t('sections.basicInfoDescription')}>
344
+ <div className="grid gap-4 md:grid-cols-2">
345
+ <div className="space-y-2">
346
+ <label className="text-sm font-medium">{t('fields.code')}</label>
347
+ <Input
348
+ value={form.code}
349
+ onChange={(event) =>
350
+ setForm((current) => ({ ...current, code: event.target.value }))
351
+ }
352
+ />
353
+ </div>
354
+ <div className="space-y-2">
355
+ <label className="text-sm font-medium">{t('fields.name')}</label>
356
+ <Input
357
+ value={form.name}
358
+ onChange={(event) =>
359
+ setForm((current) => ({ ...current, name: event.target.value }))
360
+ }
361
+ />
362
+ </div>
363
+ <div className="space-y-2">
364
+ <label className="text-sm font-medium">{t('fields.clientName')}</label>
365
+ <Input
366
+ value={form.clientName}
367
+ onChange={(event) =>
368
+ setForm((current) => ({
369
+ ...current,
370
+ clientName: event.target.value,
371
+ }))
372
+ }
373
+ />
374
+ </div>
375
+ <div className="space-y-2">
376
+ <label className="text-sm font-medium">{t('fields.deliveryModel')}</label>
377
+ <Select
378
+ value={form.deliveryModel}
379
+ onValueChange={(value) =>
380
+ setForm((current) => ({ ...current, deliveryModel: value }))
381
+ }
382
+ >
383
+ <SelectTrigger>
384
+ <SelectValue />
385
+ </SelectTrigger>
386
+ <SelectContent>
387
+ <SelectItem value="project_delivery">Project Delivery</SelectItem>
388
+ <SelectItem value="dedicated_team">Dedicated Team</SelectItem>
389
+ <SelectItem value="shared_team">Shared Team</SelectItem>
390
+ <SelectItem value="support">Support</SelectItem>
391
+ </SelectContent>
392
+ </Select>
393
+ </div>
394
+ <div className="space-y-2 md:col-span-2">
395
+ <label className="text-sm font-medium">{t('fields.summary')}</label>
396
+ <Textarea
397
+ rows={4}
398
+ value={form.summary}
399
+ onChange={(event) =>
400
+ setForm((current) => ({ ...current, summary: event.target.value }))
401
+ }
402
+ />
403
+ </div>
404
+ </div>
405
+ </SectionCard>
406
+
407
+ <SectionCard title={t('sections.governance')} description={t('sections.governanceDescription')}>
408
+ <div className="grid gap-4 md:grid-cols-2">
409
+ <div className="space-y-2">
410
+ <label className="text-sm font-medium">{commonT('labels.manager')}</label>
411
+ <Select
412
+ value={form.managerCollaboratorId}
413
+ onValueChange={(value) =>
414
+ setForm((current) => ({
415
+ ...current,
416
+ managerCollaboratorId: value,
417
+ }))
418
+ }
419
+ >
420
+ <SelectTrigger>
421
+ <SelectValue />
422
+ </SelectTrigger>
423
+ <SelectContent>
424
+ <SelectItem value="none">{commonT('labels.notAssigned')}</SelectItem>
425
+ {collaborators.map((collaborator) => (
426
+ <SelectItem key={collaborator.id} value={String(collaborator.id)}>
427
+ {collaborator.displayName}
428
+ </SelectItem>
429
+ ))}
430
+ </SelectContent>
431
+ </Select>
432
+ </div>
433
+ <div className="space-y-2">
434
+ <label className="text-sm font-medium">{commonT('labels.status')}</label>
435
+ <Select
436
+ value={form.status}
437
+ onValueChange={(value) =>
438
+ setForm((current) => ({ ...current, status: value }))
439
+ }
440
+ >
441
+ <SelectTrigger>
442
+ <SelectValue />
443
+ </SelectTrigger>
444
+ <SelectContent>
445
+ <SelectItem value="planning">Planning</SelectItem>
446
+ <SelectItem value="active">Active</SelectItem>
447
+ <SelectItem value="at_risk">At Risk</SelectItem>
448
+ <SelectItem value="paused">Paused</SelectItem>
449
+ <SelectItem value="completed">Completed</SelectItem>
450
+ <SelectItem value="archived">Archived</SelectItem>
451
+ </SelectContent>
452
+ </Select>
453
+ </div>
454
+ <div className="space-y-2">
455
+ <label className="text-sm font-medium">{commonT('labels.startDate')}</label>
456
+ <Input
457
+ type="date"
458
+ value={form.startDate}
459
+ onChange={(event) =>
460
+ setForm((current) => ({ ...current, startDate: event.target.value }))
461
+ }
462
+ />
463
+ </div>
464
+ <div className="space-y-2">
465
+ <label className="text-sm font-medium">{commonT('labels.endDate')}</label>
466
+ <Input
467
+ type="date"
468
+ value={form.endDate}
469
+ onChange={(event) =>
470
+ setForm((current) => ({ ...current, endDate: event.target.value }))
471
+ }
472
+ />
473
+ </div>
474
+ </div>
475
+ </SectionCard>
476
+ </div>
477
+
478
+ <SectionCard title={t('sections.team')} description={t('sections.teamDescription')}>
479
+ <div className="space-y-3">
480
+ {form.teamAssignments.map((assignment) => {
481
+ const collaborator = collaborators.find(
482
+ (item) => item.id === assignment.collaboratorId
483
+ );
484
+ if (!collaborator) {
485
+ return null;
486
+ }
487
+
488
+ return (
489
+ <div
490
+ key={assignment.collaboratorId}
491
+ className="grid gap-3 rounded-lg border p-4 lg:grid-cols-[minmax(220px,1fr)_minmax(180px,1fr)_120px_120px]"
492
+ >
493
+ <div className="flex items-start gap-3">
494
+ <Checkbox
495
+ checked={assignment.selected}
496
+ onCheckedChange={(checked) =>
497
+ updateAssignment(assignment.collaboratorId, {
498
+ selected: checked === true,
499
+ })
500
+ }
501
+ />
502
+ <div>
503
+ <div className="font-medium">{collaborator.displayName}</div>
504
+ <div className="text-xs text-muted-foreground">
505
+ {[collaborator.department, collaborator.title]
506
+ .filter(Boolean)
507
+ .join(' • ') || commonT('labels.notAvailable')}
508
+ </div>
509
+ </div>
510
+ </div>
511
+ <Input
512
+ placeholder={t('fields.roleLabel')}
513
+ value={assignment.roleLabel}
514
+ disabled={!assignment.selected}
515
+ onChange={(event) =>
516
+ updateAssignment(assignment.collaboratorId, {
517
+ roleLabel: event.target.value,
518
+ })
519
+ }
520
+ />
521
+ <Input
522
+ type="number"
523
+ placeholder={t('fields.weeklyHours')}
524
+ value={assignment.weeklyHours}
525
+ disabled={!assignment.selected}
526
+ onChange={(event) =>
527
+ updateAssignment(assignment.collaboratorId, {
528
+ weeklyHours: event.target.value,
529
+ })
530
+ }
531
+ />
532
+ <Input
533
+ type="number"
534
+ placeholder={t('fields.allocationPercent')}
535
+ value={assignment.allocationPercent}
536
+ disabled={!assignment.selected}
537
+ onChange={(event) =>
538
+ updateAssignment(assignment.collaboratorId, {
539
+ allocationPercent: event.target.value,
540
+ })
541
+ }
542
+ />
543
+ </div>
544
+ );
545
+ })}
546
+ </div>
547
+ </SectionCard>
548
+
549
+ <div className="grid gap-4 xl:grid-cols-2">
550
+ <SectionCard title={t('sections.financials')} description={t('sections.financialsDescription')}>
551
+ <div className="grid gap-4 md:grid-cols-2">
552
+ <div className="space-y-2">
553
+ <label className="text-sm font-medium">{commonT('labels.budget')}</label>
554
+ <Input
555
+ type="number"
556
+ step="0.01"
557
+ value={form.budgetAmount}
558
+ onChange={(event) =>
559
+ setForm((current) => ({ ...current, budgetAmount: event.target.value }))
560
+ }
561
+ />
562
+ </div>
563
+ <div className="space-y-2">
564
+ <label className="text-sm font-medium">{commonT('labels.monthlyHourCap')}</label>
565
+ <Input
566
+ type="number"
567
+ step="0.5"
568
+ value={form.monthlyHourCap}
569
+ onChange={(event) =>
570
+ setForm((current) => ({
571
+ ...current,
572
+ monthlyHourCap: event.target.value,
573
+ }))
574
+ }
575
+ />
576
+ </div>
577
+ <div className="space-y-2">
578
+ <label className="text-sm font-medium">{commonT('labels.billingModel')}</label>
579
+ <Select
580
+ value={form.billingModel}
581
+ onValueChange={(value) =>
582
+ setForm((current) => ({ ...current, billingModel: value }))
583
+ }
584
+ >
585
+ <SelectTrigger>
586
+ <SelectValue />
587
+ </SelectTrigger>
588
+ <SelectContent>
589
+ <SelectItem value="time_and_material">Time And Material</SelectItem>
590
+ <SelectItem value="monthly_retainer">Monthly Retainer</SelectItem>
591
+ <SelectItem value="fixed_price">Fixed Price</SelectItem>
592
+ </SelectContent>
593
+ </Select>
594
+ </div>
595
+ <div className="space-y-2">
596
+ <label className="text-sm font-medium">{commonT('labels.contract')}</label>
597
+ <Select
598
+ value={form.contractId}
599
+ onValueChange={(value) =>
600
+ setForm((current) => ({ ...current, contractId: value }))
601
+ }
602
+ >
603
+ <SelectTrigger>
604
+ <SelectValue />
605
+ </SelectTrigger>
606
+ <SelectContent>
607
+ <SelectItem value="none">{commonT('labels.notAssigned')}</SelectItem>
608
+ {contracts.map((contract) => (
609
+ <SelectItem key={contract.id} value={String(contract.id)}>
610
+ {contract.name}
611
+ </SelectItem>
612
+ ))}
613
+ </SelectContent>
614
+ </Select>
615
+ </div>
616
+ </div>
617
+ </SectionCard>
618
+
619
+ <SectionCard title={t('sections.contract')} description={t('sections.contractDescription')}>
620
+ <div className="grid gap-4 md:grid-cols-2">
621
+ <div className="space-y-2">
622
+ <label className="text-sm font-medium">{t('fields.contractCode')}</label>
623
+ <Input
624
+ value={form.contractCode}
625
+ onChange={(event) =>
626
+ setForm((current) => ({ ...current, contractCode: event.target.value }))
627
+ }
628
+ />
629
+ </div>
630
+ <div className="space-y-2">
631
+ <label className="text-sm font-medium">{t('fields.contractName')}</label>
632
+ <Input
633
+ value={form.contractName}
634
+ onChange={(event) =>
635
+ setForm((current) => ({ ...current, contractName: event.target.value }))
636
+ }
637
+ />
638
+ </div>
639
+ <div className="space-y-2 md:col-span-2">
640
+ <label className="text-sm font-medium">{t('fields.contractDescription')}</label>
641
+ <Textarea
642
+ rows={4}
643
+ value={form.contractDescription}
644
+ onChange={(event) =>
645
+ setForm((current) => ({
646
+ ...current,
647
+ contractDescription: event.target.value,
648
+ }))
649
+ }
650
+ />
651
+ </div>
652
+ <div className="flex items-center justify-between rounded-lg border px-4 py-3 md:col-span-2">
653
+ <div>
654
+ <div className="font-medium">{t('fields.autoGenerateContractDraft')}</div>
655
+ <div className="text-sm text-muted-foreground">
656
+ {form.contractId === 'none'
657
+ ? t('fields.autoGenerateContractDraftDescription')
658
+ : t('fields.existingContractSelected')}
659
+ </div>
660
+ </div>
661
+ <Switch
662
+ checked={form.contractId === 'none' && form.autoGenerateContractDraft}
663
+ disabled={form.contractId !== 'none'}
664
+ onCheckedChange={(checked) =>
665
+ setForm((current) => ({
666
+ ...current,
667
+ autoGenerateContractDraft: checked,
668
+ }))
669
+ }
670
+ />
671
+ </div>
672
+ </div>
673
+ </SectionCard>
674
+ </div>
675
+
676
+ {projectId && isLoadingProject ? (
677
+ <div className="text-sm text-muted-foreground">{t('loading')}</div>
678
+ ) : null}
679
+
680
+ {project ? (
681
+ <div className="text-xs text-muted-foreground">
682
+ {t('messages.currentContractStatus', {
683
+ status: formatEnumLabel(project.contractStatus),
684
+ })}
685
+ </div>
686
+ ) : null}
687
+ </Page>
688
+ );
689
+ }