@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
|
@@ -54,13 +54,14 @@ import {
|
|
|
54
54
|
ClipboardList,
|
|
55
55
|
Clock3,
|
|
56
56
|
Loader2,
|
|
57
|
+
Pencil,
|
|
57
58
|
Plus,
|
|
58
59
|
Send,
|
|
59
60
|
Sparkles,
|
|
60
61
|
Trash2,
|
|
61
62
|
} from 'lucide-react';
|
|
62
63
|
import { useTranslations } from 'next-intl';
|
|
63
|
-
import { useEffect, useMemo, useState } from 'react';
|
|
64
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
64
65
|
import { useForm, useWatch } from 'react-hook-form';
|
|
65
66
|
import { z } from 'zod';
|
|
66
67
|
|
|
@@ -195,6 +196,69 @@ function buildFollowUpFormValues(
|
|
|
195
196
|
};
|
|
196
197
|
}
|
|
197
198
|
|
|
199
|
+
function buildProjectOptionFromEntry(
|
|
200
|
+
entry: OperationsTimesheetEntry
|
|
201
|
+
): OperationsProjectOption | null {
|
|
202
|
+
if (!entry.projectId) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
id: entry.projectId,
|
|
208
|
+
label:
|
|
209
|
+
[entry.projectCode, entry.projectName, entry.roleLabel]
|
|
210
|
+
.filter(Boolean)
|
|
211
|
+
.join(' • ') || String(entry.projectId),
|
|
212
|
+
name: entry.projectName || String(entry.projectId),
|
|
213
|
+
code: entry.projectCode,
|
|
214
|
+
projectAssignmentId: entry.projectAssignmentId,
|
|
215
|
+
roleLabel: entry.roleLabel,
|
|
216
|
+
status: 'active',
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function buildTaskOptionFromEntry(
|
|
221
|
+
entry: OperationsTimesheetEntry
|
|
222
|
+
): OperationsTaskOption | null {
|
|
223
|
+
if (!entry.taskId || !entry.projectId || !entry.projectAssignmentId) {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
id: entry.taskId,
|
|
229
|
+
label:
|
|
230
|
+
[entry.taskName || entry.activityLabel, entry.projectName]
|
|
231
|
+
.filter(Boolean)
|
|
232
|
+
.join(' • ') || String(entry.taskId),
|
|
233
|
+
name: entry.taskName || entry.activityLabel || String(entry.taskId),
|
|
234
|
+
status: 'todo',
|
|
235
|
+
projectId: entry.projectId,
|
|
236
|
+
projectAssignmentId: entry.projectAssignmentId,
|
|
237
|
+
projectName: entry.projectName || '',
|
|
238
|
+
projectCode: entry.projectCode,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function buildEditFormValues(
|
|
243
|
+
entry: OperationsTimesheetEntry
|
|
244
|
+
): QuickEntryFormValues {
|
|
245
|
+
const durationMinutes =
|
|
246
|
+
typeof entry.durationMinutes === 'number' &&
|
|
247
|
+
Number.isFinite(entry.durationMinutes)
|
|
248
|
+
? entry.durationMinutes
|
|
249
|
+
: Math.round(Number(entry.hours ?? 0) * 60);
|
|
250
|
+
const useHours = durationMinutes > 0 && durationMinutes % 60 === 0;
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
projectId: entry.projectId ?? undefined,
|
|
254
|
+
taskId: entry.taskId ?? undefined,
|
|
255
|
+
workDate: entry.workDate ? entry.workDate.slice(0, 10) : '',
|
|
256
|
+
duration: useHours ? durationMinutes / 60 : durationMinutes,
|
|
257
|
+
unit: useHours ? 'hours' : 'minutes',
|
|
258
|
+
description: entry.description ?? '',
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
198
262
|
export default function OperationsTimesheetsPage() {
|
|
199
263
|
const t = useTranslations('operations.TimesheetsPage');
|
|
200
264
|
const commonT = useTranslations('operations.Common');
|
|
@@ -205,6 +269,8 @@ export default function OperationsTimesheetsPage() {
|
|
|
205
269
|
const [isSheetOpen, setIsSheetOpen] = useState(false);
|
|
206
270
|
const [isTaskCreateSheetOpen, setIsTaskCreateSheetOpen] = useState(false);
|
|
207
271
|
const [keepContextOnSave, setKeepContextOnSave] = useState(true);
|
|
272
|
+
const [editingEntry, setEditingEntry] =
|
|
273
|
+
useState<OperationsTimesheetEntry | null>(null);
|
|
208
274
|
const [entryToDelete, setEntryToDelete] =
|
|
209
275
|
useState<OperationsTimesheetEntry | null>(null);
|
|
210
276
|
const [isDeletingEntry, setIsDeletingEntry] = useState(false);
|
|
@@ -215,6 +281,7 @@ export default function OperationsTimesheetsPage() {
|
|
|
215
281
|
const [taskQuery, setTaskQuery] = useState({ search: '', page: 1 });
|
|
216
282
|
const [taskRefreshKey, setTaskRefreshKey] = useState(0);
|
|
217
283
|
const [taskOptions, setTaskOptions] = useState<OperationsTaskOption[]>([]);
|
|
284
|
+
const previousProjectIdRef = useRef<number | undefined>(undefined);
|
|
218
285
|
|
|
219
286
|
const quickEntrySchema = useMemo(
|
|
220
287
|
() =>
|
|
@@ -307,66 +374,73 @@ export default function OperationsTimesheetsPage() {
|
|
|
307
374
|
placeholderData: (previous) => previous,
|
|
308
375
|
});
|
|
309
376
|
|
|
310
|
-
const {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
}
|
|
377
|
+
const {
|
|
378
|
+
data: projectOptionsResponse,
|
|
379
|
+
isLoading: isProjectOptionsLoading,
|
|
380
|
+
error: projectOptionsError,
|
|
381
|
+
} = useQuery<PaginatedResponse<OperationsProjectOption>>({
|
|
382
|
+
queryKey: [
|
|
383
|
+
'operations-timesheet-project-options',
|
|
384
|
+
currentLocaleCode,
|
|
385
|
+
projectQuery.search,
|
|
386
|
+
projectQuery.page,
|
|
387
|
+
],
|
|
388
|
+
enabled: access.isCollaborator && isSheetOpen,
|
|
389
|
+
queryFn: () => {
|
|
390
|
+
const params = new URLSearchParams({
|
|
391
|
+
page: String(projectQuery.page),
|
|
392
|
+
pageSize: '20',
|
|
393
|
+
});
|
|
328
394
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
);
|
|
333
|
-
},
|
|
334
|
-
placeholderData: (previous) => previous,
|
|
335
|
-
});
|
|
395
|
+
if (projectQuery.search.trim()) {
|
|
396
|
+
params.set('search', projectQuery.search.trim());
|
|
397
|
+
}
|
|
336
398
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
taskQuery.page,
|
|
345
|
-
taskRefreshKey,
|
|
346
|
-
],
|
|
347
|
-
enabled:
|
|
348
|
-
access.isCollaborator && isSheetOpen && Boolean(selectedProjectId),
|
|
349
|
-
queryFn: () => {
|
|
350
|
-
const params = new URLSearchParams({
|
|
351
|
-
page: String(taskQuery.page),
|
|
352
|
-
pageSize: '20',
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
if (taskQuery.search.trim()) {
|
|
356
|
-
params.set('search', taskQuery.search.trim());
|
|
357
|
-
}
|
|
399
|
+
return fetchOperations<PaginatedResponse<OperationsProjectOption>>(
|
|
400
|
+
request,
|
|
401
|
+
`/operations/projects/options?${params.toString()}`
|
|
402
|
+
);
|
|
403
|
+
},
|
|
404
|
+
placeholderData: (previous) => previous,
|
|
405
|
+
});
|
|
358
406
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
}
|
|
407
|
+
const selectedProject =
|
|
408
|
+
projectOptions.find((option) => option.id === selectedProjectId) ?? null;
|
|
362
409
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
410
|
+
const {
|
|
411
|
+
data: taskOptionsResponse,
|
|
412
|
+
isLoading: isTaskOptionsLoading,
|
|
413
|
+
error: taskOptionsError,
|
|
414
|
+
} = useQuery<PaginatedResponse<OperationsTaskOption>>({
|
|
415
|
+
queryKey: [
|
|
416
|
+
'operations-timesheet-task-options',
|
|
417
|
+
currentLocaleCode,
|
|
418
|
+
selectedProjectId,
|
|
419
|
+
taskQuery.search,
|
|
420
|
+
taskQuery.page,
|
|
421
|
+
taskRefreshKey,
|
|
422
|
+
],
|
|
423
|
+
enabled: access.isCollaborator && isSheetOpen && Boolean(selectedProjectId),
|
|
424
|
+
queryFn: () => {
|
|
425
|
+
const params = new URLSearchParams({
|
|
426
|
+
page: String(taskQuery.page),
|
|
427
|
+
pageSize: '20',
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
if (taskQuery.search.trim()) {
|
|
431
|
+
params.set('search', taskQuery.search.trim());
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (selectedProjectId) {
|
|
435
|
+
params.set('projectId', String(selectedProjectId));
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return fetchOperations<PaginatedResponse<OperationsTaskOption>>(
|
|
439
|
+
request,
|
|
440
|
+
`/operations/tasks?${params.toString()}`
|
|
441
|
+
);
|
|
442
|
+
},
|
|
443
|
+
});
|
|
370
444
|
|
|
371
445
|
useEffect(() => {
|
|
372
446
|
if (!projectOptionsResponse) {
|
|
@@ -375,10 +449,12 @@ export default function OperationsTimesheetsPage() {
|
|
|
375
449
|
|
|
376
450
|
setProjectOptions((current) =>
|
|
377
451
|
projectQuery.page === 1
|
|
378
|
-
?
|
|
452
|
+
? editingEntry
|
|
453
|
+
? appendUniqueById(current, projectOptionsResponse.data ?? [])
|
|
454
|
+
: (projectOptionsResponse.data ?? [])
|
|
379
455
|
: appendUniqueById(current, projectOptionsResponse.data ?? [])
|
|
380
456
|
);
|
|
381
|
-
}, [projectOptionsResponse, projectQuery.page]);
|
|
457
|
+
}, [editingEntry, projectOptionsResponse, projectQuery.page]);
|
|
382
458
|
|
|
383
459
|
useEffect(() => {
|
|
384
460
|
if (!selectedProjectId) {
|
|
@@ -392,16 +468,77 @@ export default function OperationsTimesheetsPage() {
|
|
|
392
468
|
|
|
393
469
|
setTaskOptions((current) =>
|
|
394
470
|
taskQuery.page === 1
|
|
395
|
-
?
|
|
396
|
-
|
|
471
|
+
? editingEntry
|
|
472
|
+
? appendUniqueById(
|
|
473
|
+
current,
|
|
474
|
+
(taskOptionsResponse.data ?? []).filter(
|
|
475
|
+
(option) => option.projectId === selectedProjectId
|
|
476
|
+
)
|
|
477
|
+
)
|
|
478
|
+
: (taskOptionsResponse.data ?? []).filter(
|
|
479
|
+
(option) => option.projectId === selectedProjectId
|
|
480
|
+
)
|
|
481
|
+
: appendUniqueById(
|
|
482
|
+
current,
|
|
483
|
+
(taskOptionsResponse.data ?? []).filter(
|
|
484
|
+
(option) => option.projectId === selectedProjectId
|
|
485
|
+
)
|
|
486
|
+
)
|
|
487
|
+
);
|
|
488
|
+
}, [editingEntry, selectedProjectId, taskOptionsResponse, taskQuery.page]);
|
|
489
|
+
|
|
490
|
+
useEffect(() => {
|
|
491
|
+
const previousProjectId = previousProjectIdRef.current;
|
|
492
|
+
previousProjectIdRef.current = selectedProjectId;
|
|
493
|
+
|
|
494
|
+
if (
|
|
495
|
+
previousProjectId === undefined ||
|
|
496
|
+
previousProjectId === selectedProjectId
|
|
497
|
+
) {
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
setTaskOptions([]);
|
|
502
|
+
setTaskQuery({ search: '', page: 1 });
|
|
503
|
+
form.setValue('taskId', undefined, {
|
|
504
|
+
shouldDirty: true,
|
|
505
|
+
shouldTouch: true,
|
|
506
|
+
shouldValidate: false,
|
|
507
|
+
});
|
|
508
|
+
}, [form, selectedProjectId]);
|
|
509
|
+
|
|
510
|
+
useEffect(() => {
|
|
511
|
+
if (!projectOptionsError) {
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
showToastHandler?.(
|
|
516
|
+
'error',
|
|
517
|
+
getOperationsErrorMessage(
|
|
518
|
+
projectOptionsError,
|
|
519
|
+
t('messages.projectLoadError')
|
|
520
|
+
)
|
|
521
|
+
);
|
|
522
|
+
}, [projectOptionsError, showToastHandler, t]);
|
|
523
|
+
|
|
524
|
+
useEffect(() => {
|
|
525
|
+
if (!taskOptionsError) {
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
showToastHandler?.(
|
|
530
|
+
'error',
|
|
531
|
+
getOperationsErrorMessage(taskOptionsError, t('messages.taskLoadError'))
|
|
397
532
|
);
|
|
398
|
-
}, [
|
|
533
|
+
}, [showToastHandler, t, taskOptionsError]);
|
|
399
534
|
|
|
400
535
|
const recentEntries = recentEntriesResponse?.data ?? [];
|
|
401
|
-
const
|
|
402
|
-
|
|
536
|
+
const filteredTaskOptions = selectedProjectId
|
|
537
|
+
? taskOptions.filter((option) => option.projectId === selectedProjectId)
|
|
538
|
+
: [];
|
|
403
539
|
const selectedTask =
|
|
404
|
-
|
|
540
|
+
filteredTaskOptions.find((option) => option.id === selectedTaskId) ?? null;
|
|
541
|
+
const isEditingEntry = Boolean(editingEntry);
|
|
405
542
|
|
|
406
543
|
const filteredRows = useMemo(
|
|
407
544
|
() =>
|
|
@@ -459,20 +596,38 @@ export default function OperationsTimesheetsPage() {
|
|
|
459
596
|
];
|
|
460
597
|
|
|
461
598
|
const openCreate = () => {
|
|
599
|
+
setEditingEntry(null);
|
|
462
600
|
form.reset(buildDefaultFormValues());
|
|
463
601
|
setProjectQuery({ search: '', page: 1 });
|
|
464
602
|
setTaskQuery({ search: '', page: 1 });
|
|
603
|
+
setProjectOptions([]);
|
|
465
604
|
setTaskOptions([]);
|
|
466
605
|
setIsTaskCreateSheetOpen(false);
|
|
467
606
|
setIsSheetOpen(true);
|
|
468
607
|
};
|
|
469
608
|
|
|
609
|
+
const openEditEntry = (entry: OperationsTimesheetEntry) => {
|
|
610
|
+
const projectOption = buildProjectOptionFromEntry(entry);
|
|
611
|
+
const taskOption = buildTaskOptionFromEntry(entry);
|
|
612
|
+
|
|
613
|
+
setEditingEntry(entry);
|
|
614
|
+
form.reset(buildEditFormValues(entry));
|
|
615
|
+
setProjectQuery({ search: '', page: 1 });
|
|
616
|
+
setTaskQuery({ search: '', page: 1 });
|
|
617
|
+
setProjectOptions(projectOption ? [projectOption] : []);
|
|
618
|
+
setTaskOptions(taskOption ? [taskOption] : []);
|
|
619
|
+
setIsTaskCreateSheetOpen(false);
|
|
620
|
+
setIsSheetOpen(true);
|
|
621
|
+
};
|
|
622
|
+
|
|
470
623
|
const closeCreateSheet = () => {
|
|
471
624
|
setIsSheetOpen(false);
|
|
625
|
+
setEditingEntry(null);
|
|
472
626
|
setIsTaskCreateSheetOpen(false);
|
|
473
627
|
form.reset(buildDefaultFormValues());
|
|
474
628
|
setProjectQuery({ search: '', page: 1 });
|
|
475
629
|
setTaskQuery({ search: '', page: 1 });
|
|
630
|
+
setProjectOptions([]);
|
|
476
631
|
setTaskOptions([]);
|
|
477
632
|
};
|
|
478
633
|
|
|
@@ -493,19 +648,38 @@ export default function OperationsTimesheetsPage() {
|
|
|
493
648
|
};
|
|
494
649
|
|
|
495
650
|
const handleQuickEntrySubmit = async (values: QuickEntryFormValues) => {
|
|
651
|
+
const payload = {
|
|
652
|
+
projectId: values.projectId,
|
|
653
|
+
taskId: values.taskId,
|
|
654
|
+
workDate: values.workDate,
|
|
655
|
+
duration: values.duration,
|
|
656
|
+
unit: values.unit,
|
|
657
|
+
description: trimToNull(values.description),
|
|
658
|
+
};
|
|
659
|
+
|
|
496
660
|
try {
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
}
|
|
661
|
+
if (editingEntry?.id) {
|
|
662
|
+
await mutateOperations(
|
|
663
|
+
request,
|
|
664
|
+
`/operations/timesheet-entries/${editingEntry.id}`,
|
|
665
|
+
'PATCH',
|
|
666
|
+
payload
|
|
667
|
+
);
|
|
668
|
+
} else {
|
|
669
|
+
await mutateOperations(
|
|
670
|
+
request,
|
|
671
|
+
'/operations/timesheet-entries',
|
|
672
|
+
'POST',
|
|
673
|
+
payload
|
|
674
|
+
);
|
|
675
|
+
}
|
|
505
676
|
|
|
506
|
-
showToastHandler?.(
|
|
677
|
+
showToastHandler?.(
|
|
678
|
+
'success',
|
|
679
|
+
editingEntry ? t('messages.updateSuccess') : t('messages.saveSuccess')
|
|
680
|
+
);
|
|
507
681
|
|
|
508
|
-
if (keepContextOnSave) {
|
|
682
|
+
if (!editingEntry && keepContextOnSave) {
|
|
509
683
|
form.reset(buildFollowUpFormValues(values, true));
|
|
510
684
|
setProjectQuery({ search: '', page: 1 });
|
|
511
685
|
setTaskQuery({ search: '', page: 1 });
|
|
@@ -519,7 +693,10 @@ export default function OperationsTimesheetsPage() {
|
|
|
519
693
|
} catch (error) {
|
|
520
694
|
showToastHandler?.(
|
|
521
695
|
'error',
|
|
522
|
-
getOperationsErrorMessage(
|
|
696
|
+
getOperationsErrorMessage(
|
|
697
|
+
error,
|
|
698
|
+
editingEntry ? t('messages.updateError') : t('messages.saveError')
|
|
699
|
+
)
|
|
523
700
|
);
|
|
524
701
|
}
|
|
525
702
|
};
|
|
@@ -660,69 +837,98 @@ export default function OperationsTimesheetsPage() {
|
|
|
660
837
|
</div>
|
|
661
838
|
) : recentEntries.length > 0 ? (
|
|
662
839
|
<div className="divide-y">
|
|
663
|
-
{recentEntries.map((entry) =>
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
840
|
+
{recentEntries.map((entry) => {
|
|
841
|
+
const isEditableEntry = canManageEntry(entry);
|
|
842
|
+
|
|
843
|
+
return (
|
|
844
|
+
<div
|
|
845
|
+
key={entry.id}
|
|
846
|
+
className={`flex flex-col gap-3 px-6 py-4 sm:flex-row sm:items-center sm:justify-between ${
|
|
847
|
+
isEditableEntry
|
|
848
|
+
? 'cursor-pointer hover:bg-muted/30'
|
|
849
|
+
: ''
|
|
850
|
+
}`}
|
|
851
|
+
onClick={
|
|
852
|
+
isEditableEntry ? () => openEditEntry(entry) : undefined
|
|
853
|
+
}
|
|
854
|
+
>
|
|
855
|
+
<div className="min-w-0 space-y-1">
|
|
856
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
857
|
+
<span className="font-medium text-foreground">
|
|
858
|
+
{[entry.projectCode, entry.projectName]
|
|
859
|
+
.filter(Boolean)
|
|
860
|
+
.join(' • ') || commonT('labels.unassigned')}
|
|
861
|
+
</span>
|
|
862
|
+
<span className="text-muted-foreground">•</span>
|
|
863
|
+
<span className="text-sm text-muted-foreground">
|
|
864
|
+
{entry.taskName ||
|
|
865
|
+
entry.activityLabel ||
|
|
866
|
+
commonT('labels.noNotes')}
|
|
867
|
+
</span>
|
|
868
|
+
</div>
|
|
869
|
+
|
|
870
|
+
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
|
871
|
+
<span>{formatDateLabel(entry.workDate)}</span>
|
|
872
|
+
{entry.description ? (
|
|
873
|
+
<>
|
|
874
|
+
<span>•</span>
|
|
875
|
+
<span className="truncate">
|
|
876
|
+
{entry.description}
|
|
877
|
+
</span>
|
|
878
|
+
</>
|
|
879
|
+
) : null}
|
|
880
|
+
</div>
|
|
681
881
|
</div>
|
|
682
882
|
|
|
683
|
-
<div className="flex
|
|
684
|
-
|
|
685
|
-
|
|
883
|
+
<div className="flex items-center gap-2 self-start sm:self-center">
|
|
884
|
+
{entry.status ? (
|
|
885
|
+
<StatusBadge
|
|
886
|
+
label={formatEnumLabel(entry.status)}
|
|
887
|
+
className={getStatusBadgeClass(entry.status)}
|
|
888
|
+
/>
|
|
889
|
+
) : null}
|
|
890
|
+
|
|
891
|
+
<span className="inline-flex items-center rounded-md bg-muted px-2.5 py-1 text-xs font-medium text-foreground">
|
|
892
|
+
{formatEntryDuration(
|
|
893
|
+
entry.durationMinutes,
|
|
894
|
+
entry.hours
|
|
895
|
+
)}
|
|
896
|
+
</span>
|
|
897
|
+
|
|
898
|
+
{isEditableEntry ? (
|
|
686
899
|
<>
|
|
687
|
-
<
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
900
|
+
<Button
|
|
901
|
+
type="button"
|
|
902
|
+
variant="ghost"
|
|
903
|
+
size="icon"
|
|
904
|
+
className="text-muted-foreground"
|
|
905
|
+
onClick={(event) => {
|
|
906
|
+
event.stopPropagation();
|
|
907
|
+
openEditEntry(entry);
|
|
908
|
+
}}
|
|
909
|
+
aria-label={t('sheet.editTitle')}
|
|
910
|
+
>
|
|
911
|
+
<Pencil className="size-4" />
|
|
912
|
+
</Button>
|
|
913
|
+
<Button
|
|
914
|
+
type="button"
|
|
915
|
+
variant="ghost"
|
|
916
|
+
size="icon"
|
|
917
|
+
className="text-muted-foreground hover:text-destructive"
|
|
918
|
+
onClick={(event) => {
|
|
919
|
+
event.stopPropagation();
|
|
920
|
+
setEntryToDelete(entry);
|
|
921
|
+
}}
|
|
922
|
+
aria-label={t('messages.confirmDeleteTitle')}
|
|
923
|
+
>
|
|
924
|
+
<Trash2 className="size-4" />
|
|
925
|
+
</Button>
|
|
691
926
|
</>
|
|
692
927
|
) : null}
|
|
693
928
|
</div>
|
|
694
929
|
</div>
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
{entry.status ? (
|
|
698
|
-
<StatusBadge
|
|
699
|
-
label={formatEnumLabel(entry.status)}
|
|
700
|
-
className={getStatusBadgeClass(entry.status)}
|
|
701
|
-
/>
|
|
702
|
-
) : null}
|
|
703
|
-
|
|
704
|
-
<span className="inline-flex items-center rounded-md bg-muted px-2.5 py-1 text-xs font-medium text-foreground">
|
|
705
|
-
{formatEntryDuration(
|
|
706
|
-
entry.durationMinutes,
|
|
707
|
-
entry.hours
|
|
708
|
-
)}
|
|
709
|
-
</span>
|
|
710
|
-
|
|
711
|
-
{canManageEntry(entry) ? (
|
|
712
|
-
<Button
|
|
713
|
-
type="button"
|
|
714
|
-
variant="ghost"
|
|
715
|
-
size="icon"
|
|
716
|
-
className="text-muted-foreground hover:text-destructive"
|
|
717
|
-
onClick={() => setEntryToDelete(entry)}
|
|
718
|
-
aria-label={t('messages.confirmDeleteTitle')}
|
|
719
|
-
>
|
|
720
|
-
<Trash2 className="size-4" />
|
|
721
|
-
</Button>
|
|
722
|
-
) : null}
|
|
723
|
-
</div>
|
|
724
|
-
</div>
|
|
725
|
-
))}
|
|
930
|
+
);
|
|
931
|
+
})}
|
|
726
932
|
</div>
|
|
727
933
|
) : (
|
|
728
934
|
<div className="px-6 py-6 text-sm text-muted-foreground">
|
|
@@ -857,7 +1063,11 @@ export default function OperationsTimesheetsPage() {
|
|
|
857
1063
|
</div>
|
|
858
1064
|
|
|
859
1065
|
<div className="space-y-1">
|
|
860
|
-
<SheetTitle>
|
|
1066
|
+
<SheetTitle>
|
|
1067
|
+
{isEditingEntry
|
|
1068
|
+
? t('sheet.editTitle')
|
|
1069
|
+
: t('sheet.createTitle')}
|
|
1070
|
+
</SheetTitle>
|
|
861
1071
|
<SheetDescription>{t('sheet.description')}</SheetDescription>
|
|
862
1072
|
</div>
|
|
863
1073
|
</div>
|
|
@@ -926,7 +1136,7 @@ export default function OperationsTimesheetsPage() {
|
|
|
926
1136
|
<AsyncOptionsCombobox
|
|
927
1137
|
value={field.value}
|
|
928
1138
|
selectedOption={selectedTask}
|
|
929
|
-
options={
|
|
1139
|
+
options={filteredTaskOptions}
|
|
930
1140
|
onSelect={(option) => field.onChange(option?.id)}
|
|
931
1141
|
searchValue={taskQuery.search}
|
|
932
1142
|
onSearchValueChange={(value) =>
|