@hed-hog/operations 0.0.319 → 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 (38) hide show
  1. package/dist/controllers/operations-tasks.controller.d.ts +22 -0
  2. package/dist/controllers/operations-tasks.controller.d.ts.map +1 -1
  3. package/dist/controllers/operations-tasks.controller.js +37 -0
  4. package/dist/controllers/operations-tasks.controller.js.map +1 -1
  5. package/dist/dto/create-task.dto.d.ts.map +1 -1
  6. package/dist/dto/create-task.dto.js +0 -1
  7. package/dist/dto/create-task.dto.js.map +1 -1
  8. package/dist/dto/update-task.dto.d.ts.map +1 -1
  9. package/dist/dto/update-task.dto.js +0 -1
  10. package/dist/dto/update-task.dto.js.map +1 -1
  11. package/dist/operations.service.d.ts +22 -0
  12. package/dist/operations.service.d.ts.map +1 -1
  13. package/dist/operations.service.js +187 -132
  14. package/dist/operations.service.js.map +1 -1
  15. package/hedhog/data/operations_cost_type.yaml +95 -95
  16. package/hedhog/data/route.yaml +39 -0
  17. package/hedhog/frontend/app/_components/collaborator-costs-section.tsx.ejs +884 -884
  18. package/hedhog/frontend/app/_components/collaborator-details-screen.tsx.ejs +23 -23
  19. package/hedhog/frontend/app/_components/person-select-with-create.tsx.ejs +49 -22
  20. package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +2968 -624
  21. package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +62 -68
  22. package/hedhog/frontend/app/_components/task-file-attachments.tsx.ejs +388 -0
  23. package/hedhog/frontend/app/_lib/types.ts.ejs +179 -178
  24. package/hedhog/frontend/app/my-tasks/page.tsx.ejs +121 -11
  25. package/hedhog/frontend/app/projects/page.tsx.ejs +105 -22
  26. package/hedhog/frontend/app/reports/collaborators/page.tsx.ejs +771 -771
  27. package/hedhog/frontend/app/reports/projects/page.tsx.ejs +809 -809
  28. package/hedhog/frontend/messages/en.json +143 -2
  29. package/hedhog/frontend/messages/pt.json +143 -2
  30. package/hedhog/table/operations_task_file.yaml +23 -0
  31. package/package.json +5 -5
  32. package/src/controllers/operations-reports.controller.ts +32 -32
  33. package/src/controllers/operations-tasks.controller.ts +43 -9
  34. package/src/dto/create-task.dto.ts +0 -1
  35. package/src/dto/list-reports.dto.ts +51 -51
  36. package/src/dto/update-task.dto.ts +0 -1
  37. package/src/operations.module.ts +5 -5
  38. package/src/operations.service.ts +754 -632
@@ -8,7 +8,7 @@ export type PaginatedResponse<T> = {
8
8
  data: T[];
9
9
  };
10
10
 
11
- export type OperationsDashboard = {
11
+ export type OperationsDashboard = {
12
12
  actor: {
13
13
  roleScope: 'self' | 'team' | 'full';
14
14
  collaboratorId: number | null;
@@ -32,183 +32,183 @@ export type OperationsDashboard = {
32
32
  totalHours: number | null;
33
33
  status: string;
34
34
  }>;
35
- };
36
-
37
- export type OperationsReportScenario = 'base' | 'growth' | 'conservative';
38
-
39
- export type OperationsReportPlanningCard = {
40
- title: string;
41
- value: string;
42
- description: string;
43
- };
44
-
45
- export type OperationsProjectsReport = {
46
- filters: {
47
- from: string;
48
- to: string;
49
- status: string;
50
- client: string;
51
- scenario: OperationsReportScenario;
52
- clients: string[];
53
- };
54
- summary: {
55
- contractedRevenue: number;
56
- recognizedRevenue: number;
57
- realizedCost: number;
58
- forecastCost: number;
59
- profit: number;
60
- margin: number;
61
- plannedHours: number;
62
- actualHours: number;
63
- billableHours: number;
64
- reworkHours: number;
65
- backlogValue: number;
66
- avgDeadline: number;
67
- avgAllocation: number;
68
- atRisk: number;
69
- burnRate: number;
70
- };
71
- forecast: Array<{
72
- month: string;
73
- revenue: number;
74
- cost: number;
75
- profit: number;
76
- backlog: number;
77
- planned: number;
78
- actual: number;
79
- }>;
80
- costComposition: Array<{ name: string; value: number }>;
81
- hoursByProject: Array<{
82
- project: string;
83
- Faturável: number;
84
- Interno: number;
85
- Retrabalho: number;
86
- Livre: number;
87
- }>;
88
- health: Array<{
89
- project: string;
90
- margem: number;
91
- prazo: number;
92
- alocacao: number;
93
- saude: number;
94
- }>;
95
- ranking: Array<{ name: string; Lucro: number; Custo: number }>;
96
- progress: Array<{ month: string; Planejado: number; Realizado: number }>;
97
- planningCards: OperationsReportPlanningCard[];
98
- rows: Array<{
99
- id: number;
100
- name: string;
101
- client: string;
102
- manager: string;
103
- squad: string;
104
- status: 'on_track' | 'attention' | 'late' | 'paused';
105
- contractType: 'fixed_price' | 'time_materials' | 'retainer';
106
- priority: string;
107
- startDate: string | null;
108
- endDate: string | null;
109
- contractedRevenue: number;
110
- recognizedRevenue: number;
111
- realizedCost: number;
112
- forecastCost: number;
113
- teamCost: number;
114
- infraCost: number;
115
- licenseCost: number;
116
- thirdPartyCost: number;
117
- reworkCost: number;
118
- plannedHours: number;
119
- actualHours: number;
120
- billableHours: number;
121
- reworkHours: number;
122
- internalHours: number;
123
- allocatedCapacity: number;
124
- physicalProgress: number;
125
- financialProgress: number;
126
- backlogValue: number;
127
- futureDeliveries: number;
128
- risk: 'baixo' | 'médio' | 'alto';
129
- recommendation: string;
130
- }>;
131
- };
132
-
133
- export type OperationsCollaboratorsReport = {
134
- filters: {
135
- from: string;
136
- to: string;
137
- department: string;
138
- contractType: string;
139
- scenario: OperationsReportScenario;
140
- departments: string[];
141
- contractTypes: string[];
142
- };
143
- summary: {
144
- cost: number;
145
- salary: number;
146
- benefits: number;
147
- taxes: number;
148
- tools: number;
149
- billableValue: number;
150
- profit: number;
151
- margin: number;
152
- availableHours: number;
153
- allocatedHours: number;
154
- billableHours: number;
155
- internalHours: number;
156
- overtimeHours: number;
157
- freeHours: number;
158
- allocation: number;
159
- utilization: number;
160
- overloadCount: number;
161
- hourlyCost: number;
162
- };
163
- forecast: Array<{
164
- month: string;
165
- revenue: number;
166
- cost: number;
167
- profit: number;
168
- margin: number;
169
- capacity: number;
170
- }>;
171
- costComposition: Array<{ name: string; value: number }>;
172
- capacityByDepartment: Array<{
173
- department: string;
174
- Faturável: number;
175
- Interno: number;
176
- Livre: number;
177
- Sobrecarga: number;
178
- }>;
179
- health: Array<{
180
- department: string;
181
- margem: number;
182
- alocacao: number;
183
- utilizacao: number;
184
- saude: number;
185
- }>;
186
- ranking: Array<{ name: string; Custo: number; Lucro: number }>;
187
- planningCards: OperationsReportPlanningCard[];
188
- rows: Array<{
189
- id: number;
190
- name: string;
191
- role: string;
192
- seniority: string;
193
- department: string;
194
- contractType: string;
195
- startDate: string | null;
196
- endDate: string | null;
197
- salaryCost: number;
198
- benefitsCost: number;
199
- taxesCost: number;
200
- toolsCost: number;
201
- billableValue: number;
202
- availableHours: number;
203
- allocatedHours: number;
204
- billableHours: number;
205
- internalHours: number;
206
- overtimeHours: number;
207
- projects: number;
208
- risk: 'baixo' | 'médio' | 'alto';
209
- recommendation: string;
210
- }>;
211
- };
35
+ };
36
+
37
+ export type OperationsReportScenario = 'base' | 'growth' | 'conservative';
38
+
39
+ export type OperationsReportPlanningCard = {
40
+ title: string;
41
+ value: string;
42
+ description: string;
43
+ };
44
+
45
+ export type OperationsProjectsReport = {
46
+ filters: {
47
+ from: string;
48
+ to: string;
49
+ status: string;
50
+ client: string;
51
+ scenario: OperationsReportScenario;
52
+ clients: string[];
53
+ };
54
+ summary: {
55
+ contractedRevenue: number;
56
+ recognizedRevenue: number;
57
+ realizedCost: number;
58
+ forecastCost: number;
59
+ profit: number;
60
+ margin: number;
61
+ plannedHours: number;
62
+ actualHours: number;
63
+ billableHours: number;
64
+ reworkHours: number;
65
+ backlogValue: number;
66
+ avgDeadline: number;
67
+ avgAllocation: number;
68
+ atRisk: number;
69
+ burnRate: number;
70
+ };
71
+ forecast: Array<{
72
+ month: string;
73
+ revenue: number;
74
+ cost: number;
75
+ profit: number;
76
+ backlog: number;
77
+ planned: number;
78
+ actual: number;
79
+ }>;
80
+ costComposition: Array<{ name: string; value: number }>;
81
+ hoursByProject: Array<{
82
+ project: string;
83
+ Faturável: number;
84
+ Interno: number;
85
+ Retrabalho: number;
86
+ Livre: number;
87
+ }>;
88
+ health: Array<{
89
+ project: string;
90
+ margem: number;
91
+ prazo: number;
92
+ alocacao: number;
93
+ saude: number;
94
+ }>;
95
+ ranking: Array<{ name: string; Lucro: number; Custo: number }>;
96
+ progress: Array<{ month: string; Planejado: number; Realizado: number }>;
97
+ planningCards: OperationsReportPlanningCard[];
98
+ rows: Array<{
99
+ id: number;
100
+ name: string;
101
+ client: string;
102
+ manager: string;
103
+ squad: string;
104
+ status: 'on_track' | 'attention' | 'late' | 'paused';
105
+ contractType: 'fixed_price' | 'time_materials' | 'retainer';
106
+ priority: string;
107
+ startDate: string | null;
108
+ endDate: string | null;
109
+ contractedRevenue: number;
110
+ recognizedRevenue: number;
111
+ realizedCost: number;
112
+ forecastCost: number;
113
+ teamCost: number;
114
+ infraCost: number;
115
+ licenseCost: number;
116
+ thirdPartyCost: number;
117
+ reworkCost: number;
118
+ plannedHours: number;
119
+ actualHours: number;
120
+ billableHours: number;
121
+ reworkHours: number;
122
+ internalHours: number;
123
+ allocatedCapacity: number;
124
+ physicalProgress: number;
125
+ financialProgress: number;
126
+ backlogValue: number;
127
+ futureDeliveries: number;
128
+ risk: 'baixo' | 'médio' | 'alto';
129
+ recommendation: string;
130
+ }>;
131
+ };
132
+
133
+ export type OperationsCollaboratorsReport = {
134
+ filters: {
135
+ from: string;
136
+ to: string;
137
+ department: string;
138
+ contractType: string;
139
+ scenario: OperationsReportScenario;
140
+ departments: string[];
141
+ contractTypes: string[];
142
+ };
143
+ summary: {
144
+ cost: number;
145
+ salary: number;
146
+ benefits: number;
147
+ taxes: number;
148
+ tools: number;
149
+ billableValue: number;
150
+ profit: number;
151
+ margin: number;
152
+ availableHours: number;
153
+ allocatedHours: number;
154
+ billableHours: number;
155
+ internalHours: number;
156
+ overtimeHours: number;
157
+ freeHours: number;
158
+ allocation: number;
159
+ utilization: number;
160
+ overloadCount: number;
161
+ hourlyCost: number;
162
+ };
163
+ forecast: Array<{
164
+ month: string;
165
+ revenue: number;
166
+ cost: number;
167
+ profit: number;
168
+ margin: number;
169
+ capacity: number;
170
+ }>;
171
+ costComposition: Array<{ name: string; value: number }>;
172
+ capacityByDepartment: Array<{
173
+ department: string;
174
+ Faturável: number;
175
+ Interno: number;
176
+ Livre: number;
177
+ Sobrecarga: number;
178
+ }>;
179
+ health: Array<{
180
+ department: string;
181
+ margem: number;
182
+ alocacao: number;
183
+ utilizacao: number;
184
+ saude: number;
185
+ }>;
186
+ ranking: Array<{ name: string; Custo: number; Lucro: number }>;
187
+ planningCards: OperationsReportPlanningCard[];
188
+ rows: Array<{
189
+ id: number;
190
+ name: string;
191
+ role: string;
192
+ seniority: string;
193
+ department: string;
194
+ contractType: string;
195
+ startDate: string | null;
196
+ endDate: string | null;
197
+ salaryCost: number;
198
+ benefitsCost: number;
199
+ taxesCost: number;
200
+ toolsCost: number;
201
+ billableValue: number;
202
+ availableHours: number;
203
+ allocatedHours: number;
204
+ billableHours: number;
205
+ internalHours: number;
206
+ overtimeHours: number;
207
+ projects: number;
208
+ risk: 'baixo' | 'médio' | 'alto';
209
+ recommendation: string;
210
+ }>;
211
+ };
212
212
 
213
213
  export type OperationsCollaborator = {
214
214
  id: number;
@@ -365,6 +365,7 @@ export type OperationsProject = {
365
365
  contractStatus?: string | null;
366
366
  contractCategory?: string | null;
367
367
  managerName?: string | null;
368
+ managerAvatarId?: number | null;
368
369
  myAssignmentId?: number | null;
369
370
  myRoleLabel?: string | null;
370
371
  teamSize?: number;
@@ -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