@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.
- package/dist/controllers/operations-contracts.controller.d.ts +9 -9
- package/dist/controllers/operations-tasks.controller.d.ts +22 -0
- package/dist/controllers/operations-tasks.controller.d.ts.map +1 -1
- package/dist/controllers/operations-tasks.controller.js +37 -0
- package/dist/controllers/operations-tasks.controller.js.map +1 -1
- package/dist/dto/create-task.dto.d.ts.map +1 -1
- package/dist/dto/create-task.dto.js +0 -1
- package/dist/dto/create-task.dto.js.map +1 -1
- package/dist/dto/update-task.dto.d.ts.map +1 -1
- package/dist/dto/update-task.dto.js +0 -1
- package/dist/dto/update-task.dto.js.map +1 -1
- package/dist/operations.service.d.ts +22 -0
- package/dist/operations.service.d.ts.map +1 -1
- package/dist/operations.service.js +77 -22
- package/dist/operations.service.js.map +1 -1
- package/hedhog/data/route.yaml +39 -0
- package/hedhog/frontend/app/_components/person-select-with-create.tsx.ejs +49 -22
- package/hedhog/frontend/app/_components/project-details-screen.tsx.ejs +2968 -624
- package/hedhog/frontend/app/_components/project-form-screen.tsx.ejs +62 -68
- package/hedhog/frontend/app/_components/task-file-attachments.tsx.ejs +388 -0
- package/hedhog/frontend/app/_lib/types.ts.ejs +1 -0
- package/hedhog/frontend/app/my-tasks/page.tsx.ejs +121 -11
- package/hedhog/frontend/app/projects/page.tsx.ejs +105 -22
- package/hedhog/frontend/messages/en.json +143 -2
- package/hedhog/frontend/messages/pt.json +143 -2
- package/hedhog/table/operations_task_file.yaml +23 -0
- package/package.json +5 -5
- package/src/controllers/operations-tasks.controller.ts +43 -9
- package/src/dto/create-task.dto.ts +0 -1
- package/src/dto/update-task.dto.ts +0 -1
- package/src/operations.service.ts +144 -22
|
@@ -721,7 +721,69 @@
|
|
|
721
721
|
"loggedHours": "Horas lançadas",
|
|
722
722
|
"loggedHoursDescription": "Total de horas vinculadas a este projeto.",
|
|
723
723
|
"allocation": "Alocação média",
|
|
724
|
-
"allocationDescription": "Média de alocação das atribuições do projeto."
|
|
724
|
+
"allocationDescription": "Média de alocação das atribuições do projeto.",
|
|
725
|
+
"lateTasks": "Tasks atrasadas",
|
|
726
|
+
"lateTasksDescription": "Tarefas abertas com prazo vencido.",
|
|
727
|
+
"weeklyVelocity": "Velocidade semanal",
|
|
728
|
+
"weeklyVelocityDescription": "Horas recentes ou planejadas na semana.",
|
|
729
|
+
"projectHealth": "Saúde do projeto",
|
|
730
|
+
"pendingTasks": "Tasks pendentes",
|
|
731
|
+
"pendingTasksDescription": "{overdue} tarefa(s) atrasada(s) no momento.",
|
|
732
|
+
"activeCollaborators": "Colaboradores ativos",
|
|
733
|
+
"activeCollaboratorsDescription": "Pessoas com alocação ativa neste projeto."
|
|
734
|
+
},
|
|
735
|
+
"kpi": {
|
|
736
|
+
"indicator": "Indicador",
|
|
737
|
+
"subtitles": {
|
|
738
|
+
"health": "Composição visual de progresso, pendências e alocação."
|
|
739
|
+
},
|
|
740
|
+
"trends": {
|
|
741
|
+
"hours": "{count} timesheet(s)",
|
|
742
|
+
"health": {
|
|
743
|
+
"good": "Positivo",
|
|
744
|
+
"warning": "Warning",
|
|
745
|
+
"danger": "Crítico"
|
|
746
|
+
},
|
|
747
|
+
"velocity": {
|
|
748
|
+
"active": "Em movimento",
|
|
749
|
+
"empty": "Sem ritmo recente"
|
|
750
|
+
},
|
|
751
|
+
"allocation": {
|
|
752
|
+
"good": "Equilibrada",
|
|
753
|
+
"warning": "Alta",
|
|
754
|
+
"critical": "Sobrecarga"
|
|
755
|
+
},
|
|
756
|
+
"tasks": {
|
|
757
|
+
"good": "Sem pendências",
|
|
758
|
+
"warning": "Backlog ativo",
|
|
759
|
+
"critical": "{count} atrasada(s)"
|
|
760
|
+
},
|
|
761
|
+
"collaborators": {
|
|
762
|
+
"active": "Equipe ativa",
|
|
763
|
+
"empty": "Sem equipe"
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
},
|
|
767
|
+
"quickActions": {
|
|
768
|
+
"timesheet": "Timesheet",
|
|
769
|
+
"reports": "Relatórios",
|
|
770
|
+
"more": "Mais ações"
|
|
771
|
+
},
|
|
772
|
+
"executive": {
|
|
773
|
+
"team": "Equipe",
|
|
774
|
+
"activeTasks": "Tasks ativas",
|
|
775
|
+
"completedTasks": "Concluídas",
|
|
776
|
+
"fallbackDescription": "Acompanhe entrega, equipe, contrato e sinais operacionais deste projeto.",
|
|
777
|
+
"membersCount": "{count} membro(s)"
|
|
778
|
+
},
|
|
779
|
+
"breadcrumbTrail": {
|
|
780
|
+
"operations": "Operations",
|
|
781
|
+
"projects": "Projetos"
|
|
782
|
+
},
|
|
783
|
+
"health": {
|
|
784
|
+
"good": "Saudável",
|
|
785
|
+
"warning": "Atenção",
|
|
786
|
+
"danger": "Crítico"
|
|
725
787
|
},
|
|
726
788
|
"sections": {
|
|
727
789
|
"overview": "Visão geral",
|
|
@@ -750,14 +812,92 @@
|
|
|
750
812
|
"deliveryHealthDescription": "Leitura visual de alocação e ritmo operacional da equipe.",
|
|
751
813
|
"quickRadar": "Radar rápido",
|
|
752
814
|
"quickRadarDescription": "Sinais para tomada de decisão no curto prazo.",
|
|
815
|
+
"timeline": "Timeline operacional",
|
|
816
|
+
"timelineDescription": "Atividade recente agrupada por dia, preparada para filtros e carregamento incremental.",
|
|
753
817
|
"taskBoard": "Quadro de tarefas",
|
|
754
818
|
"taskBoardDescription": "Board estilo Kanban com arraste entre colunas e detalhe lateral da tarefa.",
|
|
755
819
|
"archivedTasks": "Tarefas arquivadas",
|
|
756
820
|
"archivedTasksDescription": "Consulte tarefas arquivadas do projeto, veja detalhes, desarquive ou exclua definitivamente."
|
|
757
821
|
},
|
|
758
822
|
"charts": {
|
|
823
|
+
"projectProgress": "Progresso",
|
|
824
|
+
"burnup": "Burnup / progresso",
|
|
825
|
+
"burnupDescription": "Evolução acumulada de horas lançadas contra o ritmo planejado.",
|
|
759
826
|
"allocationByCollaborator": "Alocação por colaborador",
|
|
760
|
-
"
|
|
827
|
+
"allocationDescription": "Distribuição de capacidade entre os colaboradores do projeto.",
|
|
828
|
+
"weeklyVelocity": "Velocidade semanal",
|
|
829
|
+
"weeklyVelocityDescription": "Ritmo recente de horas registradas pela equipe.",
|
|
830
|
+
"taskDistribution": "Distribuição de tasks",
|
|
831
|
+
"taskDistributionDescription": "Volume de tarefas por etapa do quadro.",
|
|
832
|
+
"operationalHealth": "Saúde operacional",
|
|
833
|
+
"operationalHealthDescription": "Score visual calculado a partir de progresso, pendências e alocação.",
|
|
834
|
+
"healthScore": "Score operacional",
|
|
835
|
+
"start": "Início",
|
|
836
|
+
"current": "Atual",
|
|
837
|
+
"emptyTitle": "Sem dados suficientes",
|
|
838
|
+
"emptyBurnup": "O burnup aparecerá quando houver progresso ou horas registradas.",
|
|
839
|
+
"emptyVelocity": "Ainda não há velocidade semanal retornada para este projeto.",
|
|
840
|
+
"emptyAllocation": "A alocação aparecerá quando colaboradores forem vinculados.",
|
|
841
|
+
"emptyTasks": "As tarefas aparecerão quando o quadro tiver itens."
|
|
842
|
+
},
|
|
843
|
+
"kanban": {
|
|
844
|
+
"items": "itens",
|
|
845
|
+
"progress": "Progresso",
|
|
846
|
+
"searchPlaceholder": "Buscar task, tag, responsável...",
|
|
847
|
+
"filters": "Filtros",
|
|
848
|
+
"allPriorities": "Todas prioridades",
|
|
849
|
+
"groupStatus": "Agrupar por status",
|
|
850
|
+
"noEstimate": "Sem estim.",
|
|
851
|
+
"emptyColumn": "Nenhuma task nesta coluna.",
|
|
852
|
+
"noFilteredTasks": "Nenhuma task corresponde aos filtros."
|
|
853
|
+
},
|
|
854
|
+
"timeline": {
|
|
855
|
+
"projectStarted": "Projeto iniciado",
|
|
856
|
+
"projectStartedDescription": "{project} entrou em execução.",
|
|
857
|
+
"taskCreated": "Task criada",
|
|
858
|
+
"taskCompleted": "Task concluída",
|
|
859
|
+
"commentAdded": "Comentário registrado",
|
|
860
|
+
"timesheetLogged": "Timesheet lançado",
|
|
861
|
+
"timesheetLoggedDescription": "{count} timesheet(s), totalizando {hours}.",
|
|
862
|
+
"approvalPending": "Aprovação pendente",
|
|
863
|
+
"approvalPendingDescription": "{count} item(ns) aguardando revisão.",
|
|
864
|
+
"completedTasks": "Tarefas concluídas",
|
|
865
|
+
"completedTasksDescription": "{count} tarefa(s) já concluída(s)",
|
|
866
|
+
"pendingTimesheets": "Timesheets pendentes",
|
|
867
|
+
"pendingTimesheetsDescription": "{count} timesheet(s) aguardando revisão",
|
|
868
|
+
"targetDate": "Data alvo",
|
|
869
|
+
"targetDateDescription": "Deadline operacional do projeto.",
|
|
870
|
+
"loadMore": "Carregar mais eventos",
|
|
871
|
+
"emptyTitle": "Sem atividade para exibir",
|
|
872
|
+
"empty": "Ainda não há eventos operacionais suficientes para montar uma timeline.",
|
|
873
|
+
"filters": {
|
|
874
|
+
"all": "Todos eventos",
|
|
875
|
+
"task": "Tasks",
|
|
876
|
+
"status": "Status",
|
|
877
|
+
"timesheet": "Timesheets",
|
|
878
|
+
"approval": "Aprovações",
|
|
879
|
+
"comment": "Comentários"
|
|
880
|
+
},
|
|
881
|
+
"types": {
|
|
882
|
+
"task": "Task",
|
|
883
|
+
"timesheet": "Timesheet",
|
|
884
|
+
"approval": "Aprovação",
|
|
885
|
+
"comment": "Comentário",
|
|
886
|
+
"status": "Status"
|
|
887
|
+
}
|
|
888
|
+
},
|
|
889
|
+
"teamPanel": {
|
|
890
|
+
"available": "Disponíveis",
|
|
891
|
+
"highAllocation": "Alocação alta",
|
|
892
|
+
"overload": "Sobrecarga",
|
|
893
|
+
"usedHours": "Horas utilizadas",
|
|
894
|
+
"availability": "Disponibilidade",
|
|
895
|
+
"overloadWarning": "{value}% acima da capacidade recomendada.",
|
|
896
|
+
"status": {
|
|
897
|
+
"available": "Disponível",
|
|
898
|
+
"high": "Alta carga",
|
|
899
|
+
"overload": "Sobrecarga"
|
|
900
|
+
}
|
|
761
901
|
},
|
|
762
902
|
"quickRadar": {
|
|
763
903
|
"activeAssignments": "Atribuições ativas",
|
|
@@ -777,6 +917,7 @@
|
|
|
777
917
|
"estimateLabel": "Estimativa (h)",
|
|
778
918
|
"tagsLabel": "Etiquetas",
|
|
779
919
|
"tagsPlaceholder": "planejamento, cliente, design (separadas por vírgula)",
|
|
920
|
+
"attachmentsLabel": "Anexos",
|
|
780
921
|
"saving": "Salvando..."
|
|
781
922
|
},
|
|
782
923
|
"dialogs": {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
columns:
|
|
2
|
+
- type: pk
|
|
3
|
+
- name: operations_task_id
|
|
4
|
+
type: fk
|
|
5
|
+
references:
|
|
6
|
+
table: operations_task
|
|
7
|
+
column: id
|
|
8
|
+
onDelete: CASCADE
|
|
9
|
+
onUpdate: CASCADE
|
|
10
|
+
- name: file_id
|
|
11
|
+
type: fk
|
|
12
|
+
isNullable: true
|
|
13
|
+
references:
|
|
14
|
+
table: file
|
|
15
|
+
column: id
|
|
16
|
+
onDelete: SET NULL
|
|
17
|
+
onUpdate: CASCADE
|
|
18
|
+
- type: created_at
|
|
19
|
+
- type: updated_at
|
|
20
|
+
|
|
21
|
+
indices:
|
|
22
|
+
- columns: [operations_task_id]
|
|
23
|
+
- columns: [file_id]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hed-hog/operations",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.322",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"dependencies": {
|
|
@@ -10,12 +10,12 @@
|
|
|
10
10
|
"@nestjs/jwt": "^11",
|
|
11
11
|
"@nestjs/mapped-types": "*",
|
|
12
12
|
"@hed-hog/api": "0.0.8",
|
|
13
|
-
"@hed-hog/api-prisma": "0.0.6",
|
|
14
|
-
"@hed-hog/api-pagination": "0.0.7",
|
|
15
13
|
"@hed-hog/api-locale": "0.0.14",
|
|
16
|
-
"@hed-hog/
|
|
14
|
+
"@hed-hog/api-prisma": "0.0.6",
|
|
17
15
|
"@hed-hog/api-types": "0.0.1",
|
|
18
|
-
"@hed-hog/
|
|
16
|
+
"@hed-hog/api-pagination": "0.0.7",
|
|
17
|
+
"@hed-hog/core": "0.0.322",
|
|
18
|
+
"@hed-hog/contact": "0.0.322"
|
|
19
19
|
},
|
|
20
20
|
"exports": {
|
|
21
21
|
".": {
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { Role, User } from '@hed-hog/api';
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
3
|
+
Body,
|
|
4
|
+
Controller,
|
|
5
|
+
Delete,
|
|
6
|
+
Get,
|
|
7
|
+
Param,
|
|
8
|
+
ParseIntPipe,
|
|
9
|
+
Patch,
|
|
10
|
+
Post,
|
|
11
|
+
Query,
|
|
12
|
+
UploadedFile,
|
|
13
|
+
UseInterceptors,
|
|
12
14
|
} from '@nestjs/common';
|
|
15
|
+
import { FileInterceptor } from '@nestjs/platform-express';
|
|
13
16
|
import { CreateOperationsTaskDto } from '../dto/create-task.dto';
|
|
14
17
|
import { ListMyTasksDto } from '../dto/list-my-tasks.dto';
|
|
15
18
|
import { ListOperationsTasksDto } from '../dto/list-tasks.dto';
|
|
@@ -80,4 +83,35 @@ export class OperationsTasksController {
|
|
|
80
83
|
permanent === 'true'
|
|
81
84
|
);
|
|
82
85
|
}
|
|
86
|
+
|
|
87
|
+
@Get('tasks/:id/files')
|
|
88
|
+
listTaskFiles(
|
|
89
|
+
@User() user,
|
|
90
|
+
@Param('id', ParseIntPipe) id: number,
|
|
91
|
+
) {
|
|
92
|
+
return this.operationsService.listTaskFiles(Number(user?.id || 0), id);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@Post('tasks/:id/files')
|
|
96
|
+
@UseInterceptors(FileInterceptor('file'))
|
|
97
|
+
addTaskFile(
|
|
98
|
+
@User() user,
|
|
99
|
+
@Param('id', ParseIntPipe) id: number,
|
|
100
|
+
@UploadedFile() file: MulterFile,
|
|
101
|
+
) {
|
|
102
|
+
return this.operationsService.addTaskFile(Number(user?.id || 0), id, file);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
@Delete('tasks/:id/files/:fileRelationId')
|
|
106
|
+
removeTaskFile(
|
|
107
|
+
@User() user,
|
|
108
|
+
@Param('id', ParseIntPipe) id: number,
|
|
109
|
+
@Param('fileRelationId', ParseIntPipe) fileRelationId: number,
|
|
110
|
+
) {
|
|
111
|
+
return this.operationsService.removeTaskFile(
|
|
112
|
+
Number(user?.id || 0),
|
|
113
|
+
id,
|
|
114
|
+
fileRelationId
|
|
115
|
+
);
|
|
116
|
+
}
|
|
83
117
|
}
|
|
@@ -2570,6 +2570,7 @@ export class OperationsService {
|
|
|
2570
2570
|
p.code,
|
|
2571
2571
|
p.name,
|
|
2572
2572
|
p.client_name AS "clientName",
|
|
2573
|
+
cp.avatar_id AS "clientAvatarId",
|
|
2573
2574
|
p.summary,
|
|
2574
2575
|
p.status,
|
|
2575
2576
|
p.progress_percent AS "progressPercent",
|
|
@@ -2580,17 +2581,20 @@ export class OperationsService {
|
|
|
2580
2581
|
c.name AS "contractName",
|
|
2581
2582
|
c.status AS "contractStatus",
|
|
2582
2583
|
m.display_name AS "managerName",
|
|
2584
|
+
mp.avatar_id AS "managerAvatarId",
|
|
2583
2585
|
${ownAssignmentSelect}
|
|
2584
2586
|
COUNT(DISTINCT pa.id)::int AS "teamSize"
|
|
2585
2587
|
FROM operations_project p
|
|
2586
2588
|
LEFT JOIN operations_contract c ON c.id = p.contract_id
|
|
2587
2589
|
LEFT JOIN operations_collaborator m ON m.id = p.manager_collaborator_id
|
|
2590
|
+
LEFT JOIN person cp ON cp.id = p.client_person_id
|
|
2591
|
+
LEFT JOIN person mp ON mp.id = m.person_id
|
|
2588
2592
|
LEFT JOIN operations_project_assignment pa
|
|
2589
2593
|
ON pa.project_id = p.id
|
|
2590
2594
|
AND pa.deleted_at IS NULL
|
|
2591
2595
|
AND pa.status IN ('planned', 'active')
|
|
2592
2596
|
WHERE ${whereClause}
|
|
2593
|
-
GROUP BY p.id, c.id, m.id`;
|
|
2597
|
+
GROUP BY p.id, c.id, m.id, cp.id, mp.id`;
|
|
2594
2598
|
|
|
2595
2599
|
if (!pagination) {
|
|
2596
2600
|
return this.queryRows(`${baseQuery} ORDER BY p.name ASC`, params);
|
|
@@ -2924,29 +2928,39 @@ export class OperationsService {
|
|
|
2924
2928
|
this.requireFields(data as Record<string, unknown>, ['name']);
|
|
2925
2929
|
|
|
2926
2930
|
let assignmentId: number | null = null;
|
|
2927
|
-
let projectId: number | null = null;
|
|
2931
|
+
let projectId: number | null = data.projectId ?? null;
|
|
2928
2932
|
|
|
2929
2933
|
if (data.projectId || data.projectAssignmentId) {
|
|
2930
|
-
|
|
2931
|
-
this.
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2934
|
+
if (actor.isCollaborator && !actor.isDirector && !actor.isSupervisor) {
|
|
2935
|
+
const assignment = await this.resolveProjectAssignmentForActor(
|
|
2936
|
+
this.prisma,
|
|
2937
|
+
actor,
|
|
2938
|
+
{
|
|
2939
|
+
projectId: data.projectId ?? null,
|
|
2940
|
+
projectAssignmentId: data.projectAssignmentId ?? null,
|
|
2941
|
+
}
|
|
2942
|
+
);
|
|
2943
|
+
await this.assertProjectAccess(actor, assignment.projectId);
|
|
2944
|
+
assignmentId = assignment.id;
|
|
2945
|
+
projectId = assignment.projectId;
|
|
2946
|
+
} else {
|
|
2947
|
+
if (data.projectId) {
|
|
2948
|
+
await this.assertProjectAccess(actor, data.projectId);
|
|
2949
|
+
projectId = data.projectId;
|
|
2936
2950
|
}
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2951
|
+
if (data.projectAssignmentId) {
|
|
2952
|
+
const assignment = await this.resolveProjectAssignmentForActor(
|
|
2953
|
+
this.prisma,
|
|
2954
|
+
actor,
|
|
2955
|
+
{
|
|
2956
|
+
projectId: data.projectId ?? null,
|
|
2957
|
+
projectAssignmentId: data.projectAssignmentId,
|
|
2958
|
+
}
|
|
2959
|
+
);
|
|
2960
|
+
assignmentId = assignment.id;
|
|
2961
|
+
projectId = assignment.projectId;
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2950
2964
|
}
|
|
2951
2965
|
|
|
2952
2966
|
const name = this.normalizeOptionalText(data.name);
|
|
@@ -2989,7 +3003,7 @@ export class OperationsService {
|
|
|
2989
3003
|
[
|
|
2990
3004
|
projectId,
|
|
2991
3005
|
assignmentId,
|
|
2992
|
-
data.assigneeCollaboratorId ??
|
|
3006
|
+
data.assigneeCollaboratorId ?? null,
|
|
2993
3007
|
name,
|
|
2994
3008
|
this.normalizeOptionalText(data.description),
|
|
2995
3009
|
data.priority ?? 'medium',
|
|
@@ -3140,6 +3154,114 @@ export class OperationsService {
|
|
|
3140
3154
|
return { success: true };
|
|
3141
3155
|
}
|
|
3142
3156
|
|
|
3157
|
+
async listTaskFiles(userId: number, taskId: number) {
|
|
3158
|
+
const actor = await this.getActorContext(userId);
|
|
3159
|
+
if (!actor.isCollaborator && !actor.isDirector && !actor.isSupervisor) {
|
|
3160
|
+
throw new ForbiddenException(
|
|
3161
|
+
'Operations collaborator access is required.'
|
|
3162
|
+
);
|
|
3163
|
+
}
|
|
3164
|
+
|
|
3165
|
+
const current = await this.getTaskRecordForActor(
|
|
3166
|
+
this.prisma,
|
|
3167
|
+
actor,
|
|
3168
|
+
taskId
|
|
3169
|
+
);
|
|
3170
|
+
await this.assertProjectAccess(actor, current.projectId);
|
|
3171
|
+
|
|
3172
|
+
const rows = (await (this.prisma as any).$queryRawUnsafe(
|
|
3173
|
+
`SELECT tf.id, tf.file_id, f.filename, f.size, m.name AS mimetype, tf.created_at
|
|
3174
|
+
FROM operations_task_file tf
|
|
3175
|
+
JOIN file f ON f.id = tf.file_id
|
|
3176
|
+
JOIN file_mimetype m ON m.id = f.mimetype_id
|
|
3177
|
+
WHERE tf.operations_task_id = $1
|
|
3178
|
+
ORDER BY tf.created_at ASC`,
|
|
3179
|
+
taskId
|
|
3180
|
+
)) as Array<{
|
|
3181
|
+
id: number;
|
|
3182
|
+
file_id: number;
|
|
3183
|
+
filename: string;
|
|
3184
|
+
size: number;
|
|
3185
|
+
mimetype: string;
|
|
3186
|
+
created_at: Date;
|
|
3187
|
+
}>;
|
|
3188
|
+
|
|
3189
|
+
return rows;
|
|
3190
|
+
}
|
|
3191
|
+
|
|
3192
|
+
async addTaskFile(
|
|
3193
|
+
userId: number,
|
|
3194
|
+
taskId: number,
|
|
3195
|
+
file: MulterFile
|
|
3196
|
+
) {
|
|
3197
|
+
const actor = await this.getActorContext(userId);
|
|
3198
|
+
if (!actor.isCollaborator && !actor.isDirector && !actor.isSupervisor) {
|
|
3199
|
+
throw new ForbiddenException(
|
|
3200
|
+
'Operations collaborator access is required.'
|
|
3201
|
+
);
|
|
3202
|
+
}
|
|
3203
|
+
|
|
3204
|
+
const current = await this.getTaskRecordForActor(
|
|
3205
|
+
this.prisma,
|
|
3206
|
+
actor,
|
|
3207
|
+
taskId
|
|
3208
|
+
);
|
|
3209
|
+
await this.assertProjectAccess(actor, current.projectId);
|
|
3210
|
+
|
|
3211
|
+
const uploaded = await this.fileService.upload(
|
|
3212
|
+
`operations/tasks/${taskId}`,
|
|
3213
|
+
file
|
|
3214
|
+
);
|
|
3215
|
+
|
|
3216
|
+
await (this.prisma as any).$executeRawUnsafe(
|
|
3217
|
+
`INSERT INTO operations_task_file (operations_task_id, file_id, created_at, updated_at)
|
|
3218
|
+
VALUES ($1, $2, NOW(), NOW())`,
|
|
3219
|
+
taskId,
|
|
3220
|
+
uploaded.id
|
|
3221
|
+
);
|
|
3222
|
+
|
|
3223
|
+
return uploaded;
|
|
3224
|
+
}
|
|
3225
|
+
|
|
3226
|
+
async removeTaskFile(userId: number, taskId: number, fileRelationId: number) {
|
|
3227
|
+
const actor = await this.getActorContext(userId);
|
|
3228
|
+
if (!actor.isCollaborator && !actor.isDirector && !actor.isSupervisor) {
|
|
3229
|
+
throw new ForbiddenException(
|
|
3230
|
+
'Operations collaborator access is required.'
|
|
3231
|
+
);
|
|
3232
|
+
}
|
|
3233
|
+
|
|
3234
|
+
const current = await this.getTaskRecordForActor(
|
|
3235
|
+
this.prisma,
|
|
3236
|
+
actor,
|
|
3237
|
+
taskId
|
|
3238
|
+
);
|
|
3239
|
+
await this.assertProjectAccess(actor, current.projectId);
|
|
3240
|
+
|
|
3241
|
+
const rows = (await (this.prisma as any).$queryRawUnsafe(
|
|
3242
|
+
`SELECT file_id FROM operations_task_file WHERE id = $1 AND operations_task_id = $2`,
|
|
3243
|
+
fileRelationId,
|
|
3244
|
+
taskId
|
|
3245
|
+
)) as Array<{ file_id: number }>;
|
|
3246
|
+
|
|
3247
|
+
if (!rows.length) {
|
|
3248
|
+
throw new NotFoundException('Task file attachment not found.');
|
|
3249
|
+
}
|
|
3250
|
+
|
|
3251
|
+
const fileId = rows[0].file_id;
|
|
3252
|
+
|
|
3253
|
+
await (this.prisma as any).$executeRawUnsafe(
|
|
3254
|
+
`DELETE FROM operations_task_file WHERE id = $1`,
|
|
3255
|
+
fileRelationId
|
|
3256
|
+
);
|
|
3257
|
+
|
|
3258
|
+
if (fileId) {
|
|
3259
|
+
await this.fileService.delete('en', { ids: [fileId] });
|
|
3260
|
+
}
|
|
3261
|
+
|
|
3262
|
+
return { success: true };
|
|
3263
|
+
}
|
|
3264
|
+
|
|
3143
3265
|
async listTimesheetEntries(
|
|
3144
3266
|
userId: number,
|
|
3145
3267
|
paginationParams: {
|