@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.
Files changed (81) hide show
  1. package/dist/controllers/operations-projects.controller.d.ts +15 -0
  2. package/dist/controllers/operations-projects.controller.d.ts.map +1 -1
  3. package/dist/controllers/operations-tasks.controller.d.ts +41 -10
  4. package/dist/controllers/operations-tasks.controller.d.ts.map +1 -1
  5. package/dist/controllers/operations-tasks.controller.js +11 -0
  6. package/dist/controllers/operations-tasks.controller.js.map +1 -1
  7. package/dist/controllers/operations-timesheets.controller.d.ts +21 -0
  8. package/dist/controllers/operations-timesheets.controller.d.ts.map +1 -1
  9. package/dist/controllers/operations-timesheets.controller.js +12 -0
  10. package/dist/controllers/operations-timesheets.controller.js.map +1 -1
  11. package/dist/dto/create-task.dto.d.ts +7 -1
  12. package/dist/dto/create-task.dto.d.ts.map +1 -1
  13. package/dist/dto/create-task.dto.js +38 -5
  14. package/dist/dto/create-task.dto.js.map +1 -1
  15. package/dist/dto/list-tasks.dto.d.ts +1 -1
  16. package/dist/dto/list-tasks.dto.d.ts.map +1 -1
  17. package/dist/dto/list-tasks.dto.js +2 -2
  18. package/dist/dto/list-tasks.dto.js.map +1 -1
  19. package/dist/dto/update-collaborator-type.dto.d.ts +3 -1
  20. package/dist/dto/update-collaborator-type.dto.d.ts.map +1 -1
  21. package/dist/dto/update-collaborator-type.dto.js +2 -1
  22. package/dist/dto/update-collaborator-type.dto.js.map +1 -1
  23. package/dist/dto/update-task.dto.d.ts +7 -1
  24. package/dist/dto/update-task.dto.d.ts.map +1 -1
  25. package/dist/dto/update-task.dto.js +38 -5
  26. package/dist/dto/update-task.dto.js.map +1 -1
  27. package/dist/operations.service.d.ts +90 -12
  28. package/dist/operations.service.d.ts.map +1 -1
  29. package/dist/operations.service.js +560 -148
  30. package/dist/operations.service.js.map +1 -1
  31. package/dist/operations.service.spec.js +73 -0
  32. package/dist/operations.service.spec.js.map +1 -1
  33. package/hedhog/data/menu.yaml +26 -26
  34. package/hedhog/data/operations_collaborator_type.yaml +76 -76
  35. package/hedhog/data/route.yaml +26 -0
  36. package/hedhog/frontend/app/_components/async-options-combobox.tsx.ejs +5 -3
  37. package/hedhog/frontend/app/_components/collaborator-details-screen.tsx.ejs +44 -44
  38. package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +168 -213
  39. package/hedhog/frontend/app/_components/collaborator-select-with-create.tsx.ejs +256 -256
  40. package/hedhog/frontend/app/_components/contract-form-screen.tsx.ejs +7 -7
  41. package/hedhog/frontend/app/_components/contract-template-form-screen.tsx.ejs +306 -306
  42. package/hedhog/frontend/app/_components/contract-template-select-with-create.tsx.ejs +247 -247
  43. package/hedhog/frontend/app/_components/contract-wizard-sheet.tsx.ejs +3520 -3520
  44. package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +1504 -52
  45. package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +528 -403
  46. package/hedhog/frontend/app/_components/section-card.tsx.ejs +25 -18
  47. package/hedhog/frontend/app/_components/system-user-select-with-create.tsx.ejs +609 -0
  48. package/hedhog/frontend/app/_components/timesheet-task-create-sheet.tsx.ejs +1 -0
  49. package/hedhog/frontend/app/_lib/types.ts.ejs +5 -0
  50. package/hedhog/frontend/app/_lib/utils/format.ts.ejs +7 -7
  51. package/hedhog/frontend/app/_lib/utils/forms.ts.ejs +48 -1
  52. package/hedhog/frontend/app/approvals/page.tsx.ejs +2 -2
  53. package/hedhog/frontend/app/collaborator-types/page.tsx.ejs +513 -502
  54. package/hedhog/frontend/app/collaborators/page.tsx.ejs +10 -7
  55. package/hedhog/frontend/app/contracts/page.tsx.ejs +938 -938
  56. package/hedhog/frontend/app/projects/[id]/edit/page.tsx.ejs +1 -1
  57. package/hedhog/frontend/app/projects/page.tsx.ejs +360 -133
  58. package/hedhog/frontend/app/schedule-adjustments/page.tsx.ejs +235 -72
  59. package/hedhog/frontend/app/timesheets/page.tsx.ejs +344 -134
  60. package/hedhog/frontend/messages/en.json +32 -4
  61. package/hedhog/frontend/messages/pt.json +34 -6
  62. package/hedhog/table/operations_collaborator.yaml +18 -18
  63. package/hedhog/table/operations_collaborator_equity_participation.yaml +43 -43
  64. package/hedhog/table/operations_collaborator_type.yaml +33 -33
  65. package/hedhog/table/operations_contract_document.yaml +33 -33
  66. package/hedhog/table/operations_project.yaml +9 -0
  67. package/hedhog/table/operations_task.yaml +43 -4
  68. package/package.json +6 -6
  69. package/src/controllers/operations-tasks.controller.ts +11 -0
  70. package/src/controllers/operations-timesheets.controller.ts +13 -0
  71. package/src/dto/create-collaborator-type.dto.ts +43 -43
  72. package/src/dto/create-collaborator.dto.ts +223 -223
  73. package/src/dto/create-task.dto.ts +47 -7
  74. package/src/dto/list-collaborator-types.dto.ts +15 -15
  75. package/src/dto/list-collaborators.dto.ts +30 -30
  76. package/src/dto/list-tasks.dto.ts +3 -3
  77. package/src/dto/update-collaborator-type.dto.ts +4 -3
  78. package/src/dto/update-collaborator.dto.ts +3 -3
  79. package/src/dto/update-task.dto.ts +47 -7
  80. package/src/operations.service.spec.ts +96 -0
  81. 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 { 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) =>