@hed-hog/operations 0.0.305 → 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-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/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/operations.service.d.ts +22 -0
- package/dist/operations.service.d.ts.map +1 -1
- package/dist/operations.service.js +180 -47
- 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 +13 -0
- package/hedhog/frontend/app/_components/async-options-combobox.tsx.ejs +5 -3
- package/hedhog/frontend/app/_components/timesheet-task-create-sheet.tsx.ejs +1 -0
- package/hedhog/frontend/app/approvals/page.tsx.ejs +2 -2
- package/hedhog/frontend/app/collaborator-types/page.tsx.ejs +26 -15
- 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 +5 -0
- package/hedhog/frontend/messages/pt.json +7 -2
- 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/package.json +4 -4
- 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/list-collaborator-types.dto.ts +15 -15
- package/src/dto/list-collaborators.dto.ts +30 -30
- package/src/dto/update-collaborator-type.dto.ts +4 -3
- package/src/dto/update-collaborator.dto.ts +3 -3
- package/src/operations.service.spec.ts +96 -0
- package/src/operations.service.ts +257 -47
|
@@ -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) =>
|
|
@@ -1111,12 +1111,17 @@
|
|
|
1111
1111
|
},
|
|
1112
1112
|
"messages": {
|
|
1113
1113
|
"requiredFields": "Project, task, date, and duration are required.",
|
|
1114
|
+
"collaboratorContextRequired": "Your user must be linked to a collaborator profile to report time.",
|
|
1114
1115
|
"entryValidation": "Enter a valid duration greater than zero.",
|
|
1115
1116
|
"selectProjectRequired": "Select a project assignment first.",
|
|
1116
1117
|
"selectTaskRequired": "Select or create a task first.",
|
|
1117
1118
|
"selectProjectFirst": "Select a project before creating a task.",
|
|
1119
|
+
"projectLoadError": "Unable to load the available projects.",
|
|
1120
|
+
"taskLoadError": "Unable to load the available tasks.",
|
|
1118
1121
|
"saveSuccess": "Entry saved successfully.",
|
|
1119
1122
|
"saveError": "Unable to save the entry.",
|
|
1123
|
+
"updateSuccess": "Entry updated successfully.",
|
|
1124
|
+
"updateError": "Unable to update the entry.",
|
|
1120
1125
|
"deleteSuccess": "Entry deleted successfully.",
|
|
1121
1126
|
"deleteError": "Unable to delete the entry.",
|
|
1122
1127
|
"confirmDeleteTitle": "Delete entry?",
|
|
@@ -1109,14 +1109,19 @@
|
|
|
1109
1109
|
},
|
|
1110
1110
|
"messages": {
|
|
1111
1111
|
"requiredFields": "Projeto, tarefa, data e duração são obrigatórios.",
|
|
1112
|
+
"collaboratorContextRequired": "Seu usuário precisa estar vinculado a um colaborador para lançar horas.",
|
|
1112
1113
|
"entryValidation": "Informe uma duração válida maior que zero.",
|
|
1113
1114
|
"selectProjectRequired": "Selecione primeiro uma alocação de projeto.",
|
|
1114
1115
|
"selectTaskRequired": "Selecione ou crie uma tarefa primeiro.",
|
|
1115
1116
|
"selectProjectFirst": "Selecione um projeto antes de criar uma tarefa.",
|
|
1117
|
+
"projectLoadError": "Não foi possível carregar os projetos disponíveis.",
|
|
1118
|
+
"taskLoadError": "Não foi possível carregar as tarefas disponíveis.",
|
|
1116
1119
|
"saveSuccess": "Lançamento salvo com sucesso.",
|
|
1117
|
-
"saveError": "
|
|
1120
|
+
"saveError": "Não foi possível salvar o lançamento.",
|
|
1121
|
+
"updateSuccess": "Lançamento atualizado com sucesso.",
|
|
1122
|
+
"updateError": "Não foi possível atualizar o lançamento.",
|
|
1118
1123
|
"deleteSuccess": "Lançamento excluído com sucesso.",
|
|
1119
|
-
"deleteError": "
|
|
1124
|
+
"deleteError": "Não foi possível excluir o lançamento.",
|
|
1120
1125
|
"confirmDeleteTitle": "Excluir lançamento?",
|
|
1121
1126
|
"confirmDeleteDescription": "Isso removerá o lançamento de \"{name}\" do rascunho semanal atual.",
|
|
1122
1127
|
"submitSuccess": "Timesheet enviada com sucesso.",
|