@hed-hog/operations 0.0.332 → 0.0.347

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 (70) hide show
  1. package/dist/controllers/operations-collaborators.controller.d.ts +55 -36
  2. package/dist/controllers/operations-collaborators.controller.d.ts.map +1 -1
  3. package/dist/controllers/operations-projects.controller.d.ts +3 -0
  4. package/dist/controllers/operations-projects.controller.d.ts.map +1 -1
  5. package/dist/operations.service.d.ts +58 -36
  6. package/dist/operations.service.d.ts.map +1 -1
  7. package/dist/operations.service.js +34 -34
  8. package/dist/operations.service.js.map +1 -1
  9. package/dist/operations.service.spec.js +6 -0
  10. package/dist/operations.service.spec.js.map +1 -1
  11. package/hedhog/data/menu.yaml +5 -3
  12. package/hedhog/data/route.yaml +7 -7
  13. package/hedhog/frontend/app/_components/collaborator-details-screen.tsx.ejs +476 -0
  14. package/hedhog/frontend/app/_components/collaborator-form-screen.tsx.ejs +3 -1
  15. package/hedhog/frontend/app/_components/collaborator-select-with-create.tsx.ejs +261 -0
  16. package/hedhog/frontend/app/_components/collaborator-tasks-tab.tsx.ejs +358 -358
  17. package/hedhog/frontend/app/_components/collaborator-timesheets-tab.tsx.ejs +6 -6
  18. package/hedhog/frontend/app/_components/contract-content-editor.tsx.ejs +258 -0
  19. package/hedhog/frontend/app/_components/my-project-summary-screen.tsx.ejs +5 -4
  20. package/hedhog/frontend/app/_components/person-select-with-create.tsx.ejs +1 -0
  21. package/hedhog/frontend/app/_components/project-assignments-tab.tsx.ejs +0 -6
  22. package/hedhog/frontend/app/_components/project-cost-report-screen.tsx.ejs +23 -23
  23. package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +23 -50
  24. package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +62 -28
  25. package/hedhog/frontend/app/_components/task-detail-sheet.tsx.ejs +23 -6
  26. package/hedhog/frontend/app/_components/task-form-sheet.tsx.ejs +629 -629
  27. package/hedhog/frontend/app/_lib/api.ts.ejs +2 -2
  28. package/hedhog/frontend/app/_lib/utils/task-ui.ts.ejs +1 -1
  29. package/hedhog/frontend/app/my-projects/page.tsx.ejs +2 -16
  30. package/hedhog/frontend/app/my-tasks/page.tsx.ejs +86 -24
  31. package/hedhog/frontend/app/projects/page.tsx.ejs +6 -42
  32. package/hedhog/frontend/messages/operations/operations/en.json +2100 -0
  33. package/hedhog/frontend/messages/operations/operations/pt.json +2111 -0
  34. package/hedhog/frontend/widgets/capacity-distribution.tsx.ejs +16 -16
  35. package/hedhog/frontend/widgets/effort-by-project.tsx.ejs +16 -16
  36. package/hedhog/frontend/widgets/headcount-by-area.tsx.ejs +16 -16
  37. package/hedhog/frontend/widgets/index.ts.ejs +25 -25
  38. package/hedhog/frontend/widgets/managed-projects-status.tsx.ejs +16 -16
  39. package/hedhog/frontend/widgets/my-hours-period-kpi.tsx.ejs +16 -16
  40. package/hedhog/frontend/widgets/my-open-requests-kpi.tsx.ejs +16 -16
  41. package/hedhog/frontend/widgets/my-pending-requests-list.tsx.ejs +16 -16
  42. package/hedhog/frontend/widgets/my-project-allocations-kpi.tsx.ejs +16 -16
  43. package/hedhog/frontend/widgets/my-quick-actions.tsx.ejs +16 -16
  44. package/hedhog/frontend/widgets/my-relevant-deadlines.tsx.ejs +16 -16
  45. package/hedhog/frontend/widgets/my-timesheet-status-kpi.tsx.ejs +16 -16
  46. package/hedhog/frontend/widgets/my-weekly-journey.tsx.ejs +16 -16
  47. package/hedhog/frontend/widgets/portfolio-costs-kpi.tsx.ejs +16 -16
  48. package/hedhog/frontend/widgets/portfolio-effort-kpi.tsx.ejs +16 -16
  49. package/hedhog/frontend/widgets/portfolio-projects-kpi.tsx.ejs +16 -16
  50. package/hedhog/frontend/widgets/portfolio-risk-kpi.tsx.ejs +16 -16
  51. package/hedhog/frontend/widgets/project-status-overview.tsx.ejs +16 -16
  52. package/hedhog/frontend/widgets/shared-operations-widget.tsx.ejs +169 -169
  53. package/hedhog/frontend/widgets/strategic-deadlines.tsx.ejs +16 -16
  54. package/hedhog/frontend/widgets/team-approval-queue.tsx.ejs +16 -16
  55. package/hedhog/frontend/widgets/team-capacity-kpi.tsx.ejs +16 -16
  56. package/hedhog/frontend/widgets/team-headcount-kpi.tsx.ejs +16 -16
  57. package/hedhog/frontend/widgets/team-hours-kpi.tsx.ejs +16 -16
  58. package/hedhog/frontend/widgets/team-pending-approvals-kpi.tsx.ejs +16 -16
  59. package/hedhog/frontend/widgets/team-utilization-overview.tsx.ejs +16 -16
  60. package/hedhog/frontend/widgets/team-workload-alerts.tsx.ejs +16 -16
  61. package/hedhog/table/operations_collaborator.yaml +8 -8
  62. package/hedhog/table/operations_task.yaml +76 -76
  63. package/hedhog/table/operations_task_activity.yaml +51 -51
  64. package/package.json +6 -6
  65. package/src/controllers/operations-collaborators.controller.ts +9 -9
  66. package/src/controllers/operations-tasks.controller.ts +156 -156
  67. package/src/dashboard/widgets/MyQuickActions.tsx +22 -22
  68. package/src/dto/create-collaborator.dto.ts +4 -4
  69. package/src/operations.service.spec.ts +1006 -988
  70. package/src/operations.service.ts +40 -42
@@ -479,7 +479,7 @@ export function deleteTaskComment(
479
479
  );
480
480
  }
481
481
 
482
- // ─── Collaborator Payments ────────────────────────────────────────────────────
482
+ // Collaborator Payments
483
483
 
484
484
  export type CollaboratorPayment = {
485
485
  id: number;
@@ -553,7 +553,7 @@ export function deleteCollaboratorPayment(
553
553
  );
554
554
  }
555
555
 
556
- // ─── Collaborator Invoices ────────────────────────────────────────────────────
556
+ // Collaborator Invoices
557
557
 
558
558
  export type CollaboratorInvoice = {
559
559
  id: number;
@@ -46,7 +46,7 @@ export function getTaskDescriptionPreview(value?: string | null) {
46
46
  if (!value) return '';
47
47
 
48
48
  return String(value)
49
- .replace(/<br\s*\/?>/gi, ' ')
49
+ .replace(/<br\s*\/?>(?=)/gi, ' ')
50
50
  .replace(/<\/(p|div|li|ul|ol|h1|h2|h3|h4|h5|h6)>/gi, ' ')
51
51
  .replace(/<li[^>]*>/gi, ' ')
52
52
  .replace(/<[^>]*>/g, '')
@@ -49,12 +49,6 @@ function getPersonAvatarUrl(avatarId?: number | null): string {
49
49
  : '/placeholder.png';
50
50
  }
51
51
 
52
- function getUserPhotoUrl(photoId?: number | null): string {
53
- return typeof photoId === 'number' && photoId > 0
54
- ? `${process.env.NEXT_PUBLIC_API_BASE_URL}/user/avatar/${photoId}`
55
- : '/placeholder.png';
56
- }
57
-
58
52
  function getInitials(value?: string | null): string {
59
53
  if (!value) return '?';
60
54
  return value
@@ -299,11 +293,7 @@ export default function OperationsMyProjectsPage() {
299
293
  </span>
300
294
  <Avatar className="h-5 w-5 shrink-0">
301
295
  <AvatarImage
302
- src={
303
- project.clientUserPhotoId
304
- ? getUserPhotoUrl(project.clientUserPhotoId)
305
- : getPersonAvatarUrl(project.clientAvatarId)
306
- }
296
+ src={getPersonAvatarUrl(project.clientAvatarId)}
307
297
  alt={project.clientName ?? ''}
308
298
  />
309
299
  <AvatarFallback className="text-[9px] font-medium">
@@ -412,11 +402,7 @@ export default function OperationsMyProjectsPage() {
412
402
  <div className="flex min-w-0 items-center gap-1.5">
413
403
  <Avatar className="h-6 w-6 shrink-0">
414
404
  <AvatarImage
415
- src={
416
- project.clientUserPhotoId
417
- ? getUserPhotoUrl(project.clientUserPhotoId)
418
- : getPersonAvatarUrl(project.clientAvatarId)
419
- }
405
+ src={getPersonAvatarUrl(project.clientAvatarId)}
420
406
  alt={project.clientName ?? ''}
421
407
  />
422
408
  <AvatarFallback className="text-[9px] font-medium">
@@ -6,7 +6,6 @@ import {
6
6
  PaginationFooter,
7
7
  SearchBar,
8
8
  } from '@/components/entity-list';
9
- import { RichTextEditor } from '@/components/rich-text-editor';
10
9
  import { Button } from '@/components/ui/button';
11
10
  import { Card, CardContent } from '@/components/ui/card';
12
11
  import {
@@ -44,7 +43,7 @@ import {
44
43
  } from '@/components/ui/table';
45
44
  import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
46
45
  import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
47
- import { usePersistedPageSize } from '@/hooks/use-persisted-page-size';
46
+ import { RichTextEditor } from '@/components/rich-text-editor';
48
47
  import {
49
48
  closestCenter,
50
49
  DndContext,
@@ -87,14 +86,15 @@ import { useCallback, useMemo, useState } from 'react';
87
86
  import { OperationsHeader } from '../_components/operations-header';
88
87
  import { StatusBadge } from '../_components/status-badge';
89
88
  import {
90
- TaskCommentsSection,
91
89
  TaskDetailSheet,
90
+ TaskCommentsSection,
92
91
  type TaskDetailSheetData,
93
92
  } from '../_components/task-detail-sheet';
94
93
  import { TaskFileAttachments } from '../_components/task-file-attachments';
95
94
  import { TimesheetEntryCreateSheet } from '../_components/timesheet-entry-create-sheet';
96
95
  import { fetchOperations, mutateOperations } from '../_lib/api';
97
96
  import { useMentionItems } from '../_lib/hooks/use-mention-items';
97
+ import { usePersistedPageSize } from '@/hooks/use-persisted-page-size';
98
98
  import type {
99
99
  OperationsCollaborator,
100
100
  OperationsProjectOption,
@@ -102,7 +102,6 @@ import type {
102
102
  PaginatedResponse,
103
103
  } from '../_lib/types';
104
104
  import { formatDate, getStatusBadgeClass } from '../_lib/utils/format';
105
- import { getTaskDescriptionPreview } from '../_lib/utils/task-ui';
106
105
 
107
106
  type TaskViewMode = 'table' | 'cards' | 'board';
108
107
 
@@ -706,13 +705,9 @@ export default function OperationsMyTasksPage() {
706
705
  [inlineCreateName, request, refetchBoard]
707
706
  );
708
707
 
709
- const openCreateTaskForm = useCallback((status?: BoardColumnId) => {
708
+ const openCreateTaskForm = useCallback(() => {
710
709
  setEditingTaskId(null);
711
710
  setSelectedTask(null);
712
- setTaskFormData((prev) => ({
713
- ...EMPTY_TASK_FORM,
714
- ...(status ? { status } : { status: prev.status }),
715
- }));
716
711
  setTaskFormOpen(true);
717
712
  }, []);
718
713
 
@@ -938,7 +933,7 @@ export default function OperationsMyTasksPage() {
938
933
  {(isOver) => (
939
934
  <div
940
935
  className={[
941
- 'flex min-h-48 max-h-160 flex-col rounded-3xl border bg-linear-to-b p-3 transition-all',
936
+ 'flex min-h-128 flex-col overflow-hidden rounded-3xl border bg-linear-to-b p-3 transition-all',
942
937
  getColumnClassName(column.id),
943
938
  isOver
944
939
  ? 'border-primary shadow-lg ring-2 ring-primary/15'
@@ -968,14 +963,17 @@ export default function OperationsMyTasksPage() {
968
963
  <button
969
964
  type="button"
970
965
  className="flex size-5 cursor-pointer items-center justify-center rounded-full text-muted-foreground transition hover:bg-muted hover:text-foreground"
971
- onClick={() => openCreateTaskForm(column.id)}
966
+ onClick={() => {
967
+ setInlineCreateColumn(column.id);
968
+ setInlineCreateName('');
969
+ }}
972
970
  >
973
971
  <Plus className="size-3.5" />
974
972
  </button>
975
973
  </div>
976
974
  </div>
977
975
 
978
- <div className="flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto pb-1 pr-0.5">
976
+ <div className="flex flex-1 flex-col gap-2">
979
977
  <AnimatePresence initial={false}>
980
978
  {boardColumns[column.id].map((task) => {
981
979
  const taskTags = task.tags
@@ -1027,8 +1025,9 @@ export default function OperationsMyTasksPage() {
1027
1025
  </p>
1028
1026
  {task.description ? (
1029
1027
  <p className="line-clamp-2 text-xs leading-5 text-muted-foreground">
1030
- {getTaskDescriptionPreview(
1031
- task.description
1028
+ {task.description.replace(
1029
+ /<[^>]*>/g,
1030
+ ''
1032
1031
  )}
1033
1032
  </p>
1034
1033
  ) : null}
@@ -1152,14 +1151,76 @@ export default function OperationsMyTasksPage() {
1152
1151
  </AnimatePresence>
1153
1152
  </div>
1154
1153
 
1155
- <button
1156
- type="button"
1157
- className="mt-auto flex w-full cursor-pointer items-center justify-center gap-1 rounded-2xl border border-dashed bg-background/70 px-3 py-2 text-xs text-muted-foreground transition hover:border-primary/40 hover:bg-primary/5 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
1158
- onClick={() => openCreateTaskForm(column.id)}
1159
- >
1160
- <Plus className="size-3" />
1161
- {t('actions.create')}
1162
- </button>
1154
+ {inlineCreateColumn === column.id ? (
1155
+ <div className="space-y-1.5 rounded-lg border bg-card p-2">
1156
+ <Input
1157
+ autoFocus
1158
+ placeholder={detailT('taskForm.namePlaceholder')}
1159
+ value={inlineCreateName}
1160
+ onChange={(e) =>
1161
+ setInlineCreateName(e.target.value)
1162
+ }
1163
+ onKeyDown={(e) => {
1164
+ if (e.key === 'Enter') {
1165
+ e.preventDefault();
1166
+ void handleInlineCreateTask(column.id);
1167
+ } else if (e.key === 'Escape') {
1168
+ setInlineCreateColumn(null);
1169
+ setInlineCreateName('');
1170
+ }
1171
+ }}
1172
+ onBlur={() => {
1173
+ if (!inlineCreateName.trim()) {
1174
+ setInlineCreateColumn(null);
1175
+ setInlineCreateName('');
1176
+ }
1177
+ }}
1178
+ disabled={inlineCreateLoading}
1179
+ className="h-8 text-sm"
1180
+ />
1181
+ <div className="flex gap-1">
1182
+ <Button
1183
+ type="button"
1184
+ size="sm"
1185
+ className="h-7 px-2 text-xs"
1186
+ disabled={
1187
+ !inlineCreateName.trim() || inlineCreateLoading
1188
+ }
1189
+ onMouseDown={(e) => e.preventDefault()}
1190
+ onClick={() =>
1191
+ void handleInlineCreateTask(column.id)
1192
+ }
1193
+ >
1194
+ {t('actions.create')}
1195
+ </Button>
1196
+ <Button
1197
+ type="button"
1198
+ variant="ghost"
1199
+ size="sm"
1200
+ className="h-7 px-2 text-xs"
1201
+ onMouseDown={(e) => e.preventDefault()}
1202
+ onClick={() => {
1203
+ setInlineCreateColumn(null);
1204
+ setInlineCreateName('');
1205
+ }}
1206
+ >
1207
+ {commonT('actions.cancel')}
1208
+ </Button>
1209
+ </div>
1210
+ </div>
1211
+ ) : (
1212
+ <button
1213
+ type="button"
1214
+ 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"
1215
+ onClick={() => {
1216
+ setInlineCreateColumn(column.id);
1217
+ setInlineCreateName('');
1218
+ }}
1219
+ >
1220
+ <Plus className="size-3" />
1221
+ {t('actions.create')}
1222
+ </button>
1223
+ )}
1163
1224
  </div>
1164
1225
  )}
1165
1226
  </DroppableColumn>
@@ -1215,7 +1276,7 @@ export default function OperationsMyTasksPage() {
1215
1276
 
1216
1277
  {task.description ? (
1217
1278
  <p className="line-clamp-3 text-sm text-muted-foreground">
1218
- {getTaskDescriptionPreview(task.description)}
1279
+ {task.description}
1219
1280
  </p>
1220
1281
  ) : null}
1221
1282
 
@@ -1264,7 +1325,7 @@ export default function OperationsMyTasksPage() {
1264
1325
  <div className="truncate font-medium">{task.name}</div>
1265
1326
  {task.description ? (
1266
1327
  <div className="truncate text-xs text-muted-foreground">
1267
- {getTaskDescriptionPreview(task.description)}
1328
+ {task.description}
1268
1329
  </div>
1269
1330
  ) : null}
1270
1331
  </div>
@@ -1465,6 +1526,7 @@ export default function OperationsMyTasksPage() {
1465
1526
  </DialogContent>
1466
1527
  </Dialog>
1467
1528
 
1529
+
1468
1530
  <Sheet
1469
1531
  open={taskFormOpen}
1470
1532
  onOpenChange={(open) => {
@@ -194,15 +194,9 @@ function DroppableProjectColumn({
194
194
  return <div ref={setNodeRef}>{children(isOver)}</div>;
195
195
  }
196
196
 
197
- function getPersonAvatarUrl(avatarId?: number | null) {
197
+ function getPersonAvatarUrl(avatarId?: number | null): string {
198
198
  return typeof avatarId === 'number' && avatarId > 0
199
199
  ? `${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${avatarId}`
200
- : undefined;
201
- }
202
-
203
- function getUserPhotoUrl(photoId?: number | null) {
204
- return typeof photoId === 'number' && photoId > 0
205
- ? `${process.env.NEXT_PUBLIC_API_BASE_URL}/user/avatar/${photoId}`
206
200
  : '/placeholder.png';
207
201
  }
208
202
 
@@ -512,31 +506,9 @@ export default function OperationsProjectsPage() {
512
506
  >
513
507
  <CardContent className="space-y-2 p-3">
514
508
  <div className="truncate text-sm font-semibold">{project.name}</div>
515
- <div className="flex items-center gap-2 text-xs text-muted-foreground">
516
- {project.clientName ? (
517
- <>
518
- <span className="truncate">{project.code || '—'}</span>
519
- <span>•</span>
520
- <Avatar className="h-4 w-4 shrink-0">
521
- <AvatarImage
522
- src={
523
- project.clientUserPhotoId
524
- ? getUserPhotoUrl(project.clientUserPhotoId)
525
- : getPersonAvatarUrl(project.clientAvatarId)
526
- }
527
- alt={project.clientName}
528
- />
529
- <AvatarFallback className="text-[8px] font-medium">
530
- {getInitials(project.clientName)}
531
- </AvatarFallback>
532
- </Avatar>
533
- <span className="truncate">{project.clientName}</span>
534
- </>
535
- ) : (
536
- <span className="truncate">
537
- {project.code || commonT('labels.notAvailable')}
538
- </span>
539
- )}
509
+ <div className="truncate text-xs text-muted-foreground">
510
+ {[project.code, project.clientName].filter(Boolean).join(' ') ||
511
+ commonT('labels.notAvailable')}
540
512
  </div>
541
513
  <div className="flex items-center justify-between gap-2 pt-1">
542
514
  <StatusBadge
@@ -786,11 +758,7 @@ export default function OperationsProjectsPage() {
786
758
  </span>
787
759
  <Avatar className="h-5 w-5 shrink-0">
788
760
  <AvatarImage
789
- src={
790
- project.clientUserPhotoId
791
- ? getUserPhotoUrl(project.clientUserPhotoId)
792
- : getPersonAvatarUrl(project.clientAvatarId)
793
- }
761
+ src={getPersonAvatarUrl(project.clientAvatarId)}
794
762
  alt={project.clientName ?? ''}
795
763
  />
796
764
  <AvatarFallback className="text-[9px] font-medium">
@@ -995,11 +963,7 @@ export default function OperationsProjectsPage() {
995
963
  <div className="flex min-w-0 items-center gap-1.5">
996
964
  <Avatar className="h-6 w-6 shrink-0">
997
965
  <AvatarImage
998
- src={
999
- project.clientUserPhotoId
1000
- ? getUserPhotoUrl(project.clientUserPhotoId)
1001
- : getPersonAvatarUrl(project.clientAvatarId)
1002
- }
966
+ src={getPersonAvatarUrl(project.clientAvatarId)}
1003
967
  alt={project.clientName ?? ''}
1004
968
  />
1005
969
  <AvatarFallback className="text-[9px] font-medium">