@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.
Files changed (39) hide show
  1. package/dist/controllers/operations-timesheets.controller.d.ts +21 -0
  2. package/dist/controllers/operations-timesheets.controller.d.ts.map +1 -1
  3. package/dist/controllers/operations-timesheets.controller.js +12 -0
  4. package/dist/controllers/operations-timesheets.controller.js.map +1 -1
  5. package/dist/dto/update-collaborator-type.dto.d.ts +3 -1
  6. package/dist/dto/update-collaborator-type.dto.d.ts.map +1 -1
  7. package/dist/dto/update-collaborator-type.dto.js +2 -1
  8. package/dist/dto/update-collaborator-type.dto.js.map +1 -1
  9. package/dist/operations.service.d.ts +22 -0
  10. package/dist/operations.service.d.ts.map +1 -1
  11. package/dist/operations.service.js +180 -47
  12. package/dist/operations.service.js.map +1 -1
  13. package/dist/operations.service.spec.js +73 -0
  14. package/dist/operations.service.spec.js.map +1 -1
  15. package/hedhog/data/menu.yaml +26 -26
  16. package/hedhog/data/operations_collaborator_type.yaml +76 -76
  17. package/hedhog/data/route.yaml +13 -0
  18. package/hedhog/frontend/app/_components/async-options-combobox.tsx.ejs +5 -3
  19. package/hedhog/frontend/app/_components/timesheet-task-create-sheet.tsx.ejs +1 -0
  20. package/hedhog/frontend/app/approvals/page.tsx.ejs +2 -2
  21. package/hedhog/frontend/app/collaborator-types/page.tsx.ejs +26 -15
  22. package/hedhog/frontend/app/schedule-adjustments/page.tsx.ejs +235 -72
  23. package/hedhog/frontend/app/timesheets/page.tsx.ejs +344 -134
  24. package/hedhog/frontend/messages/en.json +5 -0
  25. package/hedhog/frontend/messages/pt.json +7 -2
  26. package/hedhog/table/operations_collaborator.yaml +18 -18
  27. package/hedhog/table/operations_collaborator_equity_participation.yaml +43 -43
  28. package/hedhog/table/operations_collaborator_type.yaml +33 -33
  29. package/hedhog/table/operations_contract_document.yaml +33 -33
  30. package/package.json +4 -4
  31. package/src/controllers/operations-timesheets.controller.ts +13 -0
  32. package/src/dto/create-collaborator-type.dto.ts +43 -43
  33. package/src/dto/create-collaborator.dto.ts +223 -223
  34. package/src/dto/list-collaborator-types.dto.ts +15 -15
  35. package/src/dto/list-collaborators.dto.ts +30 -30
  36. package/src/dto/update-collaborator-type.dto.ts +4 -3
  37. package/src/dto/update-collaborator.dto.ts +3 -3
  38. package/src/operations.service.spec.ts +96 -0
  39. 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 { data: projectOptionsResponse, isLoading: isProjectOptionsLoading } =
311
- useQuery<PaginatedResponse<OperationsProjectOption>>({
312
- queryKey: [
313
- 'operations-timesheet-project-options',
314
- currentLocaleCode,
315
- projectQuery.search,
316
- projectQuery.page,
317
- ],
318
- enabled: access.isCollaborator && isSheetOpen,
319
- queryFn: () => {
320
- const params = new URLSearchParams({
321
- page: String(projectQuery.page),
322
- pageSize: '20',
323
- });
324
-
325
- if (projectQuery.search.trim()) {
326
- params.set('search', projectQuery.search.trim());
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
- return fetchOperations<PaginatedResponse<OperationsProjectOption>>(
330
- request,
331
- `/operations/projects/options?${params.toString()}`
332
- );
333
- },
334
- placeholderData: (previous) => previous,
335
- });
395
+ if (projectQuery.search.trim()) {
396
+ params.set('search', projectQuery.search.trim());
397
+ }
336
398
 
337
- const { data: taskOptionsResponse, isLoading: isTaskOptionsLoading } =
338
- useQuery<PaginatedResponse<OperationsTaskOption>>({
339
- queryKey: [
340
- 'operations-timesheet-task-options',
341
- currentLocaleCode,
342
- selectedProjectId,
343
- taskQuery.search,
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
- if (selectedProjectId) {
360
- params.set('projectId', String(selectedProjectId));
361
- }
407
+ const selectedProject =
408
+ projectOptions.find((option) => option.id === selectedProjectId) ?? null;
362
409
 
363
- return fetchOperations<PaginatedResponse<OperationsTaskOption>>(
364
- request,
365
- `/operations/tasks?${params.toString()}`
366
- );
367
- },
368
- placeholderData: (previous) => previous,
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
- ? (projectOptionsResponse.data ?? [])
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
- ? (taskOptionsResponse.data ?? [])
396
- : appendUniqueById(current, taskOptionsResponse.data ?? [])
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
- }, [selectedProjectId, taskOptionsResponse, taskQuery.page]);
533
+ }, [showToastHandler, t, taskOptionsError]);
399
534
 
400
535
  const recentEntries = recentEntriesResponse?.data ?? [];
401
- const selectedProject =
402
- projectOptions.find((option) => option.id === selectedProjectId) ?? null;
536
+ const filteredTaskOptions = selectedProjectId
537
+ ? taskOptions.filter((option) => option.projectId === selectedProjectId)
538
+ : [];
403
539
  const selectedTask =
404
- taskOptions.find((option) => option.id === selectedTaskId) ?? null;
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
- await mutateOperations(request, '/operations/timesheet-entries', 'POST', {
498
- projectId: values.projectId,
499
- taskId: values.taskId,
500
- workDate: values.workDate,
501
- duration: values.duration,
502
- unit: values.unit,
503
- description: trimToNull(values.description),
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?.('success', t('messages.saveSuccess'));
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(error, t('messages.saveError'))
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
- <div
665
- key={entry.id}
666
- className="flex flex-col gap-3 px-6 py-4 sm:flex-row sm:items-center sm:justify-between"
667
- >
668
- <div className="min-w-0 space-y-1">
669
- <div className="flex flex-wrap items-center gap-2">
670
- <span className="font-medium text-foreground">
671
- {[entry.projectCode, entry.projectName]
672
- .filter(Boolean)
673
- .join(' • ') || commonT('labels.unassigned')}
674
- </span>
675
- <span className="text-muted-foreground">•</span>
676
- <span className="text-sm text-muted-foreground">
677
- {entry.taskName ||
678
- entry.activityLabel ||
679
- commonT('labels.noNotes')}
680
- </span>
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 flex-wrap items-center gap-2 text-xs text-muted-foreground">
684
- <span>{formatDateLabel(entry.workDate)}</span>
685
- {entry.description ? (
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
- <span>•</span>
688
- <span className="truncate">
689
- {entry.description}
690
- </span>
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
- <div className="flex items-center gap-2 self-start sm:self-center">
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>{t('sheet.createTitle')}</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={taskOptions}
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": "ão foi possível salvar o lançamento.",
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": "ão foi possível excluir o lançamento.",
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.",