@hed-hog/operations 0.0.321 → 0.0.322

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 (31) hide show
  1. package/dist/controllers/operations-contracts.controller.d.ts +9 -9
  2. package/dist/controllers/operations-tasks.controller.d.ts +22 -0
  3. package/dist/controllers/operations-tasks.controller.d.ts.map +1 -1
  4. package/dist/controllers/operations-tasks.controller.js +37 -0
  5. package/dist/controllers/operations-tasks.controller.js.map +1 -1
  6. package/dist/dto/create-task.dto.d.ts.map +1 -1
  7. package/dist/dto/create-task.dto.js +0 -1
  8. package/dist/dto/create-task.dto.js.map +1 -1
  9. package/dist/dto/update-task.dto.d.ts.map +1 -1
  10. package/dist/dto/update-task.dto.js +0 -1
  11. package/dist/dto/update-task.dto.js.map +1 -1
  12. package/dist/operations.service.d.ts +22 -0
  13. package/dist/operations.service.d.ts.map +1 -1
  14. package/dist/operations.service.js +77 -22
  15. package/dist/operations.service.js.map +1 -1
  16. package/hedhog/data/route.yaml +39 -0
  17. package/hedhog/frontend/app/_components/person-select-with-create.tsx.ejs +49 -22
  18. package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +2968 -624
  19. package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +62 -68
  20. package/hedhog/frontend/app/_components/task-file-attachments.tsx.ejs +388 -0
  21. package/hedhog/frontend/app/_lib/types.ts.ejs +1 -0
  22. package/hedhog/frontend/app/my-tasks/page.tsx.ejs +121 -11
  23. package/hedhog/frontend/app/projects/page.tsx.ejs +105 -22
  24. package/hedhog/frontend/messages/en.json +143 -2
  25. package/hedhog/frontend/messages/pt.json +143 -2
  26. package/hedhog/table/operations_task_file.yaml +23 -0
  27. package/package.json +5 -5
  28. package/src/controllers/operations-tasks.controller.ts +43 -9
  29. package/src/dto/create-task.dto.ts +0 -1
  30. package/src/dto/update-task.dto.ts +0 -1
  31. package/src/operations.service.ts +144 -22
@@ -6,6 +6,7 @@ import {
6
6
  PaginationFooter,
7
7
  SearchBar,
8
8
  } from '@/components/entity-list';
9
+ import { RichTextEditor } from '@/components/rich-text-editor';
9
10
  import { Button } from '@/components/ui/button';
10
11
  import { Card, CardContent } from '@/components/ui/card';
11
12
  import {
@@ -33,7 +34,6 @@ import {
33
34
  TableHeader,
34
35
  TableRow,
35
36
  } from '@/components/ui/table';
36
- import { Textarea } from '@/components/ui/textarea';
37
37
  import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
38
38
  import {
39
39
  closestCenter,
@@ -278,6 +278,10 @@ export default function OperationsMyTasksPage() {
278
278
  const [taskFormLoading, setTaskFormLoading] = useState(false);
279
279
  const [taskFormData, setTaskFormData] =
280
280
  useState<TaskFormState>(EMPTY_TASK_FORM);
281
+ const [inlineCreateColumn, setInlineCreateColumn] =
282
+ useState<BoardColumnId | null>(null);
283
+ const [inlineCreateName, setInlineCreateName] = useState('');
284
+ const [inlineCreateLoading, setInlineCreateLoading] = useState(false);
281
285
 
282
286
  // Paginated query for table/cards views
283
287
  const { data: tasksResponse, refetch } = useQuery<
@@ -492,6 +496,32 @@ export default function OperationsMyTasksPage() {
492
496
  [request, refetchAll]
493
497
  );
494
498
 
499
+ const handleInlineCreateTask = useCallback(
500
+ async (column: BoardColumnId) => {
501
+ const trimmed = inlineCreateName.trim();
502
+ if (!trimmed) {
503
+ setInlineCreateColumn(null);
504
+ setInlineCreateName('');
505
+ return;
506
+ }
507
+ setInlineCreateLoading(true);
508
+ try {
509
+ await mutateOperations(request, '/operations/tasks', 'POST', {
510
+ name: trimmed,
511
+ status: column,
512
+ priority: 'medium',
513
+ });
514
+ setBoardOverride(null);
515
+ setInlineCreateColumn(null);
516
+ setInlineCreateName('');
517
+ await refetchBoard();
518
+ } finally {
519
+ setInlineCreateLoading(false);
520
+ }
521
+ },
522
+ [inlineCreateName, request, refetchBoard]
523
+ );
524
+
495
525
  const openCreateTaskForm = useCallback(() => {
496
526
  setEditingTaskId(null);
497
527
  setTaskFormData(EMPTY_TASK_FORM);
@@ -700,9 +730,21 @@ export default function OperationsMyTasksPage() {
700
730
  <Rows3 className="size-4 text-muted-foreground" />
701
731
  {column.label}
702
732
  </div>
703
- <span className="rounded-full bg-background px-2 py-0.5 text-xs text-muted-foreground">
704
- {boardColumns[column.id].length}
705
- </span>
733
+ <div className="flex items-center gap-1">
734
+ <span className="rounded-full bg-background px-2 py-0.5 text-xs text-muted-foreground">
735
+ {boardColumns[column.id].length}
736
+ </span>
737
+ <button
738
+ type="button"
739
+ className="flex size-5 cursor-pointer items-center justify-center rounded-full text-muted-foreground transition hover:bg-muted hover:text-foreground"
740
+ onClick={() => {
741
+ setInlineCreateColumn(column.id);
742
+ setInlineCreateName('');
743
+ }}
744
+ >
745
+ <Plus className="size-3.5" />
746
+ </button>
747
+ </div>
706
748
  </div>
707
749
 
708
750
  <div className="space-y-2">
@@ -829,6 +871,77 @@ export default function OperationsMyTasksPage() {
829
871
  </DraggableTaskCard>
830
872
  ))}
831
873
  </div>
874
+
875
+ {inlineCreateColumn === column.id ? (
876
+ <div className="space-y-1.5 rounded-lg border bg-card p-2">
877
+ <Input
878
+ autoFocus
879
+ placeholder={detailT('taskForm.namePlaceholder')}
880
+ value={inlineCreateName}
881
+ onChange={(e) =>
882
+ setInlineCreateName(e.target.value)
883
+ }
884
+ onKeyDown={(e) => {
885
+ if (e.key === 'Enter') {
886
+ e.preventDefault();
887
+ void handleInlineCreateTask(column.id);
888
+ } else if (e.key === 'Escape') {
889
+ setInlineCreateColumn(null);
890
+ setInlineCreateName('');
891
+ }
892
+ }}
893
+ onBlur={() => {
894
+ if (!inlineCreateName.trim()) {
895
+ setInlineCreateColumn(null);
896
+ setInlineCreateName('');
897
+ }
898
+ }}
899
+ disabled={inlineCreateLoading}
900
+ className="h-8 text-sm"
901
+ />
902
+ <div className="flex gap-1">
903
+ <Button
904
+ type="button"
905
+ size="sm"
906
+ className="h-7 px-2 text-xs"
907
+ disabled={
908
+ !inlineCreateName.trim() || inlineCreateLoading
909
+ }
910
+ onMouseDown={(e) => e.preventDefault()}
911
+ onClick={() =>
912
+ void handleInlineCreateTask(column.id)
913
+ }
914
+ >
915
+ {t('actions.create')}
916
+ </Button>
917
+ <Button
918
+ type="button"
919
+ variant="ghost"
920
+ size="sm"
921
+ className="h-7 px-2 text-xs"
922
+ onMouseDown={(e) => e.preventDefault()}
923
+ onClick={() => {
924
+ setInlineCreateColumn(null);
925
+ setInlineCreateName('');
926
+ }}
927
+ >
928
+ {commonT('actions.cancel')}
929
+ </Button>
930
+ </div>
931
+ </div>
932
+ ) : (
933
+ <button
934
+ type="button"
935
+ className="flex w-full cursor-pointer items-center gap-1 rounded-md px-1 py-1 text-xs text-muted-foreground transition hover:bg-muted hover:text-foreground"
936
+ onClick={() => {
937
+ setInlineCreateColumn(column.id);
938
+ setInlineCreateName('');
939
+ }}
940
+ >
941
+ <Plus className="size-3" />
942
+ {t('actions.create')}
943
+ </button>
944
+ )}
832
945
  </div>
833
946
  )}
834
947
  </DroppableColumn>
@@ -1093,7 +1206,7 @@ export default function OperationsMyTasksPage() {
1093
1206
  }
1094
1207
  }}
1095
1208
  >
1096
- <DialogContent className="sm:max-w-lg">
1209
+ <DialogContent className="sm:max-w-2xl">
1097
1210
  <DialogHeader>
1098
1211
  <DialogTitle>
1099
1212
  {editingTaskId
@@ -1149,17 +1262,14 @@ export default function OperationsMyTasksPage() {
1149
1262
  <Label htmlFor="task-description">
1150
1263
  {detailT('taskForm.descriptionLabel')}
1151
1264
  </Label>
1152
- <Textarea
1153
- id="task-description"
1265
+ <RichTextEditor
1154
1266
  value={taskFormData.description}
1155
- onChange={(event) =>
1267
+ onChange={(val) =>
1156
1268
  setTaskFormData((prev) => ({
1157
1269
  ...prev,
1158
- description: event.target.value,
1270
+ description: val,
1159
1271
  }))
1160
1272
  }
1161
- rows={4}
1162
- placeholder={detailT('taskForm.descriptionPlaceholder')}
1163
1273
  />
1164
1274
  </div>
1165
1275
 
@@ -6,6 +6,7 @@ import {
6
6
  PaginationFooter,
7
7
  SearchBar,
8
8
  } from '@/components/entity-list';
9
+ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
9
10
  import { Button } from '@/components/ui/button';
10
11
  import { Card, CardContent } from '@/components/ui/card';
11
12
  import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
@@ -27,6 +28,8 @@ import {
27
28
  import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
28
29
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
29
30
  import {
31
+ Archive,
32
+ ArchiveRestore,
30
33
  CalendarDays,
31
34
  Eye,
32
35
  FileText,
@@ -53,6 +56,22 @@ const PROJECT_VIEW_STORAGE_KEY = 'operations-projects-view-mode';
53
56
 
54
57
  type ProjectViewMode = 'table' | 'cards';
55
58
 
59
+ function getPersonAvatarUrl(avatarId?: number | null): string {
60
+ return typeof avatarId === 'number' && avatarId > 0
61
+ ? `${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${avatarId}`
62
+ : '/placeholder.png';
63
+ }
64
+
65
+ function getInitials(value?: string | null): string {
66
+ if (!value) return '?';
67
+ return value
68
+ .split(' ')
69
+ .filter(Boolean)
70
+ .slice(0, 2)
71
+ .map((w) => w[0]!.toUpperCase())
72
+ .join('');
73
+ }
74
+
56
75
  function parseEditProjectId(value: string | null) {
57
76
  const parsed = Number(value);
58
77
  return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
@@ -335,6 +354,9 @@ export default function OperationsProjectsPage() {
335
354
  <Card
336
355
  key={project.id}
337
356
  className="cursor-pointer overflow-hidden border-border/60 py-0 shadow-sm transition-all hover:-translate-y-0.5 hover:shadow-md"
357
+ onDoubleClick={() =>
358
+ router.push(`/operations/projects/${project.id}`)
359
+ }
338
360
  >
339
361
  <CardContent className="space-y-4 p-4">
340
362
  <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
@@ -359,17 +381,39 @@ export default function OperationsProjectsPage() {
359
381
  </div>
360
382
 
361
383
  <div className="grid gap-2 text-sm text-muted-foreground lg:grid-cols-2">
362
- <div>
363
- <span className="font-medium text-foreground">
384
+ <div className="flex flex-wrap items-center gap-1.5">
385
+ <span className="shrink-0 font-medium text-foreground">
364
386
  {commonT('labels.client')}:
365
- </span>{' '}
366
- {project.clientName || commonT('labels.notAvailable')}
387
+ </span>
388
+ <Avatar className="h-5 w-5 shrink-0">
389
+ <AvatarImage
390
+ src={getPersonAvatarUrl(project.clientAvatarId)}
391
+ alt={project.clientName ?? ''}
392
+ />
393
+ <AvatarFallback className="text-[9px] font-medium">
394
+ {getInitials(project.clientName)}
395
+ </AvatarFallback>
396
+ </Avatar>
397
+ <span className="truncate">
398
+ {project.clientName || commonT('labels.notAvailable')}
399
+ </span>
367
400
  </div>
368
- <div>
369
- <span className="font-medium text-foreground">
401
+ <div className="flex flex-wrap items-center gap-1.5">
402
+ <span className="shrink-0 font-medium text-foreground">
370
403
  {commonT('labels.manager')}:
371
- </span>{' '}
372
- {project.managerName || commonT('labels.notAssigned')}
404
+ </span>
405
+ <Avatar className="h-5 w-5 shrink-0">
406
+ <AvatarImage
407
+ src={getPersonAvatarUrl(project.managerAvatarId)}
408
+ alt={project.managerName ?? ''}
409
+ />
410
+ <AvatarFallback className="text-[9px] font-medium">
411
+ {getInitials(project.managerName)}
412
+ </AvatarFallback>
413
+ </Avatar>
414
+ <span className="truncate">
415
+ {project.managerName || commonT('labels.notAssigned')}
416
+ </span>
373
417
  </div>
374
418
  <div>
375
419
  <span className="font-medium text-foreground">
@@ -414,7 +458,7 @@ export default function OperationsProjectsPage() {
414
458
  </div>
415
459
  </div>
416
460
 
417
- <div className="flex flex-wrap justify-end gap-2 border-t border-border/60 pt-3">
461
+ <div className="flex items-center justify-end gap-2 border-t border-border/60 pt-3">
418
462
  <Button variant="outline" size="icon" asChild>
419
463
  <Link href={`/operations/projects/${project.id}`}>
420
464
  <Eye className="size-4" />
@@ -451,13 +495,20 @@ export default function OperationsProjectsPage() {
451
495
  {access.isDirector ? (
452
496
  <Button
453
497
  variant="outline"
454
- size="sm"
498
+ size="icon"
455
499
  className="cursor-pointer"
500
+ title={
501
+ project.status === 'archived'
502
+ ? commonT('actions.activate')
503
+ : t('actions.archive')
504
+ }
456
505
  onClick={() => void toggleArchived(project)}
457
506
  >
458
- {project.status === 'archived'
459
- ? commonT('actions.activate')
460
- : t('actions.archive')}
507
+ {project.status === 'archived' ? (
508
+ <ArchiveRestore className="size-4" />
509
+ ) : (
510
+ <Archive className="size-4" />
511
+ )}
461
512
  </Button>
462
513
  ) : null}
463
514
  </div>
@@ -500,6 +551,9 @@ export default function OperationsProjectsPage() {
500
551
  <TableRow
501
552
  key={project.id}
502
553
  className="cursor-pointer hover:bg-muted/30"
554
+ onDoubleClick={() =>
555
+ router.push(`/operations/projects/${project.id}`)
556
+ }
503
557
  >
504
558
  <TableCell>
505
559
  <div className="min-w-0">
@@ -518,8 +572,19 @@ export default function OperationsProjectsPage() {
518
572
  </div>
519
573
  </TableCell>
520
574
  <TableCell>
521
- <div className="truncate">
522
- {project.clientName || commonT('labels.notAvailable')}
575
+ <div className="flex min-w-0 items-center gap-1.5">
576
+ <Avatar className="h-6 w-6 shrink-0">
577
+ <AvatarImage
578
+ src={getPersonAvatarUrl(project.clientAvatarId)}
579
+ alt={project.clientName ?? ''}
580
+ />
581
+ <AvatarFallback className="text-[9px] font-medium">
582
+ {getInitials(project.clientName)}
583
+ </AvatarFallback>
584
+ </Avatar>
585
+ <span className="truncate">
586
+ {project.clientName || commonT('labels.notAvailable')}
587
+ </span>
523
588
  </div>
524
589
  </TableCell>
525
590
  <TableCell>
@@ -529,8 +594,19 @@ export default function OperationsProjectsPage() {
529
594
  />
530
595
  </TableCell>
531
596
  <TableCell className="hidden lg:table-cell">
532
- <div className="truncate">
533
- {project.managerName || commonT('labels.notAssigned')}
597
+ <div className="flex min-w-0 items-center gap-1.5">
598
+ <Avatar className="h-6 w-6 shrink-0">
599
+ <AvatarImage
600
+ src={getPersonAvatarUrl(project.managerAvatarId)}
601
+ alt={project.managerName ?? ''}
602
+ />
603
+ <AvatarFallback className="text-[9px] font-medium">
604
+ {getInitials(project.managerName)}
605
+ </AvatarFallback>
606
+ </Avatar>
607
+ <span className="truncate">
608
+ {project.managerName || commonT('labels.notAssigned')}
609
+ </span>
534
610
  </div>
535
611
  </TableCell>
536
612
  <TableCell className="hidden md:table-cell">
@@ -563,7 +639,7 @@ export default function OperationsProjectsPage() {
563
639
  )}
564
640
  </TableCell>
565
641
  <TableCell>
566
- <div className="flex flex-wrap justify-end gap-1.5 sm:gap-2">
642
+ <div className="flex items-center justify-end gap-1.5">
567
643
  <Button variant="outline" size="icon" asChild>
568
644
  <Link href={`/operations/projects/${project.id}`}>
569
645
  <Eye className="size-4" />
@@ -600,13 +676,20 @@ export default function OperationsProjectsPage() {
600
676
  {access.isDirector ? (
601
677
  <Button
602
678
  variant="outline"
603
- size="sm"
679
+ size="icon"
604
680
  className="cursor-pointer"
681
+ title={
682
+ project.status === 'archived'
683
+ ? commonT('actions.activate')
684
+ : t('actions.archive')
685
+ }
605
686
  onClick={() => void toggleArchived(project)}
606
687
  >
607
- {project.status === 'archived'
608
- ? commonT('actions.activate')
609
- : t('actions.archive')}
688
+ {project.status === 'archived' ? (
689
+ <ArchiveRestore className="size-4" />
690
+ ) : (
691
+ <Archive className="size-4" />
692
+ )}
610
693
  </Button>
611
694
  ) : null}
612
695
  </div>
@@ -723,7 +723,69 @@
723
723
  "loggedHours": "Logged hours",
724
724
  "loggedHoursDescription": "Total hours linked to this project.",
725
725
  "allocation": "Average allocation",
726
- "allocationDescription": "Average assignment allocation across linked collaborators."
726
+ "allocationDescription": "Average assignment allocation across linked collaborators.",
727
+ "lateTasks": "Late tasks",
728
+ "lateTasksDescription": "Open tasks with overdue deadlines.",
729
+ "weeklyVelocity": "Weekly velocity",
730
+ "weeklyVelocityDescription": "Recent or planned hours for the week.",
731
+ "projectHealth": "Project health",
732
+ "pendingTasks": "Pending tasks",
733
+ "pendingTasksDescription": "{overdue} overdue task(s) right now.",
734
+ "activeCollaborators": "Active collaborators",
735
+ "activeCollaboratorsDescription": "People with active allocation on this project."
736
+ },
737
+ "kpi": {
738
+ "indicator": "Indicator",
739
+ "subtitles": {
740
+ "health": "Visual blend of progress, pendencies, and allocation."
741
+ },
742
+ "trends": {
743
+ "hours": "{count} timesheet(s)",
744
+ "health": {
745
+ "good": "Positive",
746
+ "warning": "Warning",
747
+ "danger": "Critical"
748
+ },
749
+ "velocity": {
750
+ "active": "Moving",
751
+ "empty": "No recent pace"
752
+ },
753
+ "allocation": {
754
+ "good": "Balanced",
755
+ "warning": "High",
756
+ "critical": "Overloaded"
757
+ },
758
+ "tasks": {
759
+ "good": "No pendencies",
760
+ "warning": "Active backlog",
761
+ "critical": "{count} late"
762
+ },
763
+ "collaborators": {
764
+ "active": "Active team",
765
+ "empty": "No team"
766
+ }
767
+ }
768
+ },
769
+ "quickActions": {
770
+ "timesheet": "Timesheet",
771
+ "reports": "Reports",
772
+ "more": "More actions"
773
+ },
774
+ "executive": {
775
+ "team": "Team",
776
+ "activeTasks": "Active tasks",
777
+ "completedTasks": "Completed",
778
+ "fallbackDescription": "Track delivery, staffing, contract, and operational signals for this project.",
779
+ "membersCount": "{count} member(s)"
780
+ },
781
+ "breadcrumbTrail": {
782
+ "operations": "Operations",
783
+ "projects": "Projects"
784
+ },
785
+ "health": {
786
+ "good": "Healthy",
787
+ "warning": "Attention",
788
+ "danger": "Critical"
727
789
  },
728
790
  "sections": {
729
791
  "overview": "Overview",
@@ -752,14 +814,92 @@
752
814
  "deliveryHealthDescription": "Visual overview of team allocation and operational pace.",
753
815
  "quickRadar": "Quick radar",
754
816
  "quickRadarDescription": "Short-term decision signals.",
817
+ "timeline": "Operational timeline",
818
+ "timelineDescription": "Recent activity grouped by day, ready for filters and incremental loading.",
755
819
  "taskBoard": "Task board",
756
820
  "taskBoardDescription": "Kanban-style board with drag between columns and task side panel.",
757
821
  "archivedTasks": "Archived tasks",
758
822
  "archivedTasksDescription": "Review archived project tasks, open details, restore them, or delete them permanently."
759
823
  },
760
824
  "charts": {
825
+ "projectProgress": "Progress",
826
+ "burnup": "Burnup / progress",
827
+ "burnupDescription": "Accumulated logged hours against the planned operating pace.",
761
828
  "allocationByCollaborator": "Allocation by collaborator",
762
- "weeklyVelocity": "Weekly velocity"
829
+ "allocationDescription": "Capacity distribution across project collaborators.",
830
+ "weeklyVelocity": "Weekly velocity",
831
+ "weeklyVelocityDescription": "Recent pace of hours logged by the team.",
832
+ "taskDistribution": "Task distribution",
833
+ "taskDistributionDescription": "Task volume by board stage.",
834
+ "operationalHealth": "Operational health",
835
+ "operationalHealthDescription": "Visual score calculated from progress, pendencies, and allocation.",
836
+ "healthScore": "Operational score",
837
+ "start": "Start",
838
+ "current": "Current",
839
+ "emptyTitle": "Not enough data",
840
+ "emptyBurnup": "The burnup will appear once there is progress or logged hours.",
841
+ "emptyVelocity": "No weekly velocity has been returned for this project yet.",
842
+ "emptyAllocation": "Allocation will appear when collaborators are assigned.",
843
+ "emptyTasks": "Tasks will appear once the board has items."
844
+ },
845
+ "kanban": {
846
+ "items": "items",
847
+ "progress": "Progress",
848
+ "searchPlaceholder": "Search task, tag, assignee...",
849
+ "filters": "Filters",
850
+ "allPriorities": "All priorities",
851
+ "groupStatus": "Group by status",
852
+ "noEstimate": "No est.",
853
+ "emptyColumn": "No tasks in this column.",
854
+ "noFilteredTasks": "No tasks match the filters."
855
+ },
856
+ "timeline": {
857
+ "projectStarted": "Project started",
858
+ "projectStartedDescription": "{project} entered execution.",
859
+ "taskCreated": "Task created",
860
+ "taskCompleted": "Task completed",
861
+ "commentAdded": "Comment recorded",
862
+ "timesheetLogged": "Timesheet logged",
863
+ "timesheetLoggedDescription": "{count} timesheet(s), totaling {hours}.",
864
+ "approvalPending": "Approval pending",
865
+ "approvalPendingDescription": "{count} item(s) waiting for review.",
866
+ "completedTasks": "Completed tasks",
867
+ "completedTasksDescription": "{count} task(s) completed",
868
+ "pendingTimesheets": "Pending timesheets",
869
+ "pendingTimesheetsDescription": "{count} timesheet(s) waiting for review",
870
+ "targetDate": "Target date",
871
+ "targetDateDescription": "Operational project deadline.",
872
+ "loadMore": "Load more events",
873
+ "emptyTitle": "No activity to show",
874
+ "empty": "There is not enough operational activity yet to build a timeline.",
875
+ "filters": {
876
+ "all": "All events",
877
+ "task": "Tasks",
878
+ "status": "Status",
879
+ "timesheet": "Timesheets",
880
+ "approval": "Approvals",
881
+ "comment": "Comments"
882
+ },
883
+ "types": {
884
+ "task": "Task",
885
+ "timesheet": "Timesheet",
886
+ "approval": "Approval",
887
+ "comment": "Comment",
888
+ "status": "Status"
889
+ }
890
+ },
891
+ "teamPanel": {
892
+ "available": "Available",
893
+ "highAllocation": "High allocation",
894
+ "overload": "Overload",
895
+ "usedHours": "Used hours",
896
+ "availability": "Availability",
897
+ "overloadWarning": "{value}% above recommended capacity.",
898
+ "status": {
899
+ "available": "Available",
900
+ "high": "High load",
901
+ "overload": "Overload"
902
+ }
763
903
  },
764
904
  "quickRadar": {
765
905
  "activeAssignments": "Active assignments",
@@ -779,6 +919,7 @@
779
919
  "estimateLabel": "Estimate (h)",
780
920
  "tagsLabel": "Tags",
781
921
  "tagsPlaceholder": "planning, client, design (comma-separated)",
922
+ "attachmentsLabel": "Attachments",
782
923
  "saving": "Saving..."
783
924
  },
784
925
  "dialogs": {