diff --git a/apps/backoffice-v2/src/pages/Entity/Entity.page.tsx b/apps/backoffice-v2/src/pages/Entity/Entity.page.tsx index 526e81e7fc..edb9fbb61d 100644 --- a/apps/backoffice-v2/src/pages/Entity/Entity.page.tsx +++ b/apps/backoffice-v2/src/pages/Entity/Entity.page.tsx @@ -8,7 +8,7 @@ import { BlocksVariant } from '@/lib/blocks/variants/BlocksVariant/BlocksVariant import { useEntityLogic } from '@/pages/Entity/hooks/useEntityLogic/useEntityLogic'; export const Entity = () => { - const { workflow, selectedEntity } = useEntityLogic(); + const { workflow, notes, selectedEntity } = useEntityLogic(); const { isNotesOpen } = useNotes(); if (!workflow) { @@ -21,6 +21,7 @@ export const Entity = () => { {/* Reject and approve header */} { {isNotesOpen && ( = ({ id, fullName, + numberOfNotes, showResolutionButtons, }) => { const { @@ -85,7 +86,16 @@ export const Actions: FunctionComponent = ({ )}
Notes - +
+ + {numberOfNotes > 0 && ( +
+ {numberOfNotes} +
+ )} +
diff --git a/apps/backoffice-v2/src/pages/Entity/components/Case/interfaces.ts b/apps/backoffice-v2/src/pages/Entity/components/Case/interfaces.ts index 1ac14e0be9..258067ddb3 100644 --- a/apps/backoffice-v2/src/pages/Entity/components/Case/interfaces.ts +++ b/apps/backoffice-v2/src/pages/Entity/components/Case/interfaces.ts @@ -28,6 +28,7 @@ export interface IActionsProps { id: string; fullName: string; avatarUrl: string; + numberOfNotes: number; showResolutionButtons?: boolean; workflow: TWorkflowById; } diff --git a/apps/backoffice-v2/src/pages/Entity/components/Notes/Notes.tsx b/apps/backoffice-v2/src/pages/Entity/components/Notes/Notes.tsx index 80321c2514..a177ca0bc8 100644 --- a/apps/backoffice-v2/src/pages/Entity/components/Notes/Notes.tsx +++ b/apps/backoffice-v2/src/pages/Entity/components/Notes/Notes.tsx @@ -3,23 +3,26 @@ import { motion } from 'framer-motion'; import { TextArea } from '@ballerine/ui'; import { Loader2, X } from 'lucide-react'; +import { NoteableType } from './types'; import { ctw } from '@/common/utils/ctw/ctw'; import { useNotes } from '@/domains/notes/hooks/useNotes'; import { Button } from '@/common/components/atoms/Button/Button'; import { useNotesLogic } from '@/pages/Entity/components/Notes/hooks/useNotesLogic'; export const Notes = ({ + notes, displayName, entityId, entityType, noteableId, noteableType, }: { + notes: any[]; displayName: string; entityId: string; entityType: 'Business' | 'EndUser'; noteableId: string; - noteableType: 'Report' | 'Alert' | 'Workflow'; + noteableType: NoteableType; }) => { const { toggleNotes } = useNotes(); const { note, onNoteChange, onSubmit } = useNotesLogic(); diff --git a/apps/backoffice-v2/src/pages/Entity/components/Notes/hooks/fetchers.ts b/apps/backoffice-v2/src/pages/Entity/components/Notes/hooks/fetchers.ts index 69cf8882fb..91b6af0489 100644 --- a/apps/backoffice-v2/src/pages/Entity/components/Notes/hooks/fetchers.ts +++ b/apps/backoffice-v2/src/pages/Entity/components/Notes/hooks/fetchers.ts @@ -1,7 +1,10 @@ import { z } from 'zod'; -import { apiClient } from '@/common/api-client/api-client'; + import { Method } from '@/common/enums'; +import { apiClient } from '@/common/api-client/api-client'; +import { NoteableType } from '@/pages/Entity/components/Notes/types'; import { handleZodError } from '@/common/utils/handle-zod-error/handle-zod-error'; +import { NoteSchema } from '@/pages/Entity/components/Notes/hooks/schemas/note-schema'; export const createNote = async ({ entityId, @@ -14,7 +17,7 @@ export const createNote = async ({ entityId: string; entityType: 'Business' | 'EndUser'; noteableId: string; - noteableType: 'Report' | 'Alert' | 'Workflow'; + noteableType: NoteableType; content: string; parentNoteId: string | null; }) => { @@ -34,3 +37,19 @@ export const createNote = async ({ return handleZodError(error, note); }; + +export const getNotesByNotable = async ({ + noteableId, + noteableType, +}: { + noteableId: string; + noteableType: NoteableType; +}) => { + const [note, error] = await apiClient({ + endpoint: `../external/notes/${noteableType}/${noteableId}`, + method: Method.GET, + schema: z.array(NoteSchema), + }); + + return handleZodError(error, note); +}; diff --git a/apps/backoffice-v2/src/pages/Entity/components/Notes/hooks/mutations/useCreateNoteMutation/useCreateNoteMutation.tsx b/apps/backoffice-v2/src/pages/Entity/components/Notes/hooks/mutations/useCreateNoteMutation/useCreateNoteMutation.tsx index 999e4256f2..8ea9368307 100644 --- a/apps/backoffice-v2/src/pages/Entity/components/Notes/hooks/mutations/useCreateNoteMutation/useCreateNoteMutation.tsx +++ b/apps/backoffice-v2/src/pages/Entity/components/Notes/hooks/mutations/useCreateNoteMutation/useCreateNoteMutation.tsx @@ -5,12 +5,9 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { HttpError } from '@/common/errors/http-error'; import { createNote } from '@/pages/Entity/components/Notes/hooks/fetchers'; +import { NoteableType } from '@/pages/Entity/components/Notes/types'; -export const useCreateNoteMutation = ({ - onSuccess, -}: { - onSuccess?: (data: TData) => void; -}) => { +export const useCreateNoteMutation = (onSuccess?: (data: TData) => void) => { const queryClient = useQueryClient(); return useMutation({ @@ -25,7 +22,7 @@ export const useCreateNoteMutation = ({ entityId: string; entityType: 'Business' | 'EndUser'; noteableId: string; - noteableType: 'Report' | 'Alert' | 'Workflow'; + noteableType: NoteableType; content: string; parentNoteId: string | null; }) => diff --git a/apps/backoffice-v2/src/pages/Entity/components/Notes/hooks/queries/useNotesByNoteable/useNotesByNoteable.tsx b/apps/backoffice-v2/src/pages/Entity/components/Notes/hooks/queries/useNotesByNoteable/useNotesByNoteable.tsx new file mode 100644 index 0000000000..eac1431e5c --- /dev/null +++ b/apps/backoffice-v2/src/pages/Entity/components/Notes/hooks/queries/useNotesByNoteable/useNotesByNoteable.tsx @@ -0,0 +1,22 @@ +import { useIsAuthenticated } from '@/domains/auth/context/AuthProvider/hooks/useIsAuthenticated/useIsAuthenticated'; +import { useQuery } from '@tanstack/react-query'; +import { isString } from '@/common/utils/is-string/is-string'; +import { notesQueryKey } from '@/pages/Entity/components/Notes/hooks/query-keys'; +import { NoteableType } from '@/pages/Entity/components/Notes/types'; + +export const useNotesByNoteable = ({ + noteableType, + noteableId = '', +}: { + noteableType: NoteableType; + noteableId?: string; +}) => { + const isAuthenticated = useIsAuthenticated(); + + return useQuery({ + ...notesQueryKey.byNoteable({ noteableType, noteableId }), + enabled: isAuthenticated && isString(noteableId) && !!noteableId.length, + staleTime: 100_000, + refetchInterval: 1_000_000, + }); +}; diff --git a/apps/backoffice-v2/src/pages/Entity/components/Notes/hooks/query-keys.ts b/apps/backoffice-v2/src/pages/Entity/components/Notes/hooks/query-keys.ts new file mode 100644 index 0000000000..78c8e35e04 --- /dev/null +++ b/apps/backoffice-v2/src/pages/Entity/components/Notes/hooks/query-keys.ts @@ -0,0 +1,17 @@ +import { createQueryKeys } from '@lukemorales/query-key-factory'; + +import { NoteableType } from '@/pages/Entity/components/Notes/types'; +import { getNotesByNotable } from '@/pages/Entity/components/Notes/hooks/fetchers'; + +export const notesQueryKey = createQueryKeys('notes', { + byNoteable: ({ + noteableType, + noteableId, + }: { + noteableType: NoteableType; + noteableId: string; + }) => ({ + queryKey: [{ noteableType, noteableId }], + queryFn: () => getNotesByNotable({ noteableType, noteableId }), + }), +}); diff --git a/apps/backoffice-v2/src/pages/Entity/components/Notes/hooks/create-note-schema.ts b/apps/backoffice-v2/src/pages/Entity/components/Notes/hooks/schemas/create-note-schema.ts similarity index 100% rename from apps/backoffice-v2/src/pages/Entity/components/Notes/hooks/create-note-schema.ts rename to apps/backoffice-v2/src/pages/Entity/components/Notes/hooks/schemas/create-note-schema.ts diff --git a/apps/backoffice-v2/src/pages/Entity/components/Notes/hooks/schemas/note-schema.ts b/apps/backoffice-v2/src/pages/Entity/components/Notes/hooks/schemas/note-schema.ts new file mode 100644 index 0000000000..c7facc38c4 --- /dev/null +++ b/apps/backoffice-v2/src/pages/Entity/components/Notes/hooks/schemas/note-schema.ts @@ -0,0 +1,18 @@ +import { z } from 'zod'; + +const BasicNoteSchema = z.object({ + id: z.string(), + entityId: z.string(), + entityType: z.enum(['Business', 'EndUser']), + noteableId: z.string(), + noteableType: z.enum(['Report', 'Alert', 'Workflow']), + content: z.string(), + fileIds: z.array(z.string()), + createdAt: z.string().datetime(), + updatedAt: z.string().datetime(), +}); + +export const NoteSchema = BasicNoteSchema.extend({ + parentNote: z.union([BasicNoteSchema, z.null()]), + childrenNotes: z.array(BasicNoteSchema), +}); diff --git a/apps/backoffice-v2/src/pages/Entity/components/Notes/hooks/useNotesLogic.tsx b/apps/backoffice-v2/src/pages/Entity/components/Notes/hooks/useNotesLogic.tsx index 448be1ceff..c3859fe319 100644 --- a/apps/backoffice-v2/src/pages/Entity/components/Notes/hooks/useNotesLogic.tsx +++ b/apps/backoffice-v2/src/pages/Entity/components/Notes/hooks/useNotesLogic.tsx @@ -3,7 +3,7 @@ import { useCallback, useState } from 'react'; import { SubmitHandler } from 'react-hook-form'; import { useCreateNoteMutation } from '@/pages/Entity/components/Notes/hooks/mutations/useCreateNoteMutation/useCreateNoteMutation'; -import { CreateNoteSchema } from '@/pages/Entity/components/Notes/hooks/create-note-schema'; +import { CreateNoteSchema } from '@/pages/Entity/components/Notes/hooks/schemas/create-note-schema'; export const useNotesLogic = () => { const [note, setNote] = useState(''); @@ -12,7 +12,7 @@ export const useNotesLogic = () => { setNote(event.target.value); }, []); - const { mutate: mutateCreateNote, isLoading: isSubmitting } = useCreateNoteMutation({}); + const { mutate: mutateCreateNote, isLoading: isSubmitting } = useCreateNoteMutation(); const onSubmit: SubmitHandler> = data => { mutateCreateNote(data); diff --git a/apps/backoffice-v2/src/pages/Entity/components/Notes/types.ts b/apps/backoffice-v2/src/pages/Entity/components/Notes/types.ts new file mode 100644 index 0000000000..85fc19e0fa --- /dev/null +++ b/apps/backoffice-v2/src/pages/Entity/components/Notes/types.ts @@ -0,0 +1 @@ +export type NoteableType = 'Report' | 'Alert' | 'Workflow'; diff --git a/apps/backoffice-v2/src/pages/Entity/hooks/useEntityLogic/useEntityLogic.ts b/apps/backoffice-v2/src/pages/Entity/hooks/useEntityLogic/useEntityLogic.ts index a9c2897cb4..b58c0befe5 100644 --- a/apps/backoffice-v2/src/pages/Entity/hooks/useEntityLogic/useEntityLogic.ts +++ b/apps/backoffice-v2/src/pages/Entity/hooks/useEntityLogic/useEntityLogic.ts @@ -1,11 +1,17 @@ import { useCurrentCaseQuery } from '@/pages/Entity/hooks/useCurrentCaseQuery/useCurrentCaseQuery'; +import { useParams } from 'react-router-dom'; +import { useNotesByNoteable } from '@/pages/Entity/components/Notes/hooks/queries/useNotesByNoteable/useNotesByNoteable'; export const useEntityLogic = () => { + const { entityId } = useParams(); + const { data: notes } = useNotesByNoteable({ noteableId: entityId, noteableType: 'Workflow' }); + const { data: workflow } = useCurrentCaseQuery(); const selectedEntity = workflow?.entity; return { selectedEntity, workflow, + notes, }; }; diff --git a/services/workflows-service/src/note/dtos/get-by-noteable.dto.ts b/services/workflows-service/src/note/dtos/get-by-noteable.dto.ts new file mode 100644 index 0000000000..ec7f019838 --- /dev/null +++ b/services/workflows-service/src/note/dtos/get-by-noteable.dto.ts @@ -0,0 +1,20 @@ +import { IsNotEmpty, IsString } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; +import { Noteable } from '@prisma/client'; + +export class GetByNoteableDto { + @ApiProperty({ + required: true, + type: String, + }) + @IsString() + @IsNotEmpty() + noteableId!: string; + + @ApiProperty({ + required: true, + enum: ['Workflow', 'Report', 'Alert'], + }) + @IsString() + noteableType!: Noteable; +} diff --git a/services/workflows-service/src/note/note.controller.external.ts b/services/workflows-service/src/note/note.controller.external.ts index 49c7903169..57f87f39c2 100644 --- a/services/workflows-service/src/note/note.controller.external.ts +++ b/services/workflows-service/src/note/note.controller.external.ts @@ -1,11 +1,13 @@ +import { Param } from '@nestjs/common'; import * as common from '@nestjs/common'; import * as swagger from '@nestjs/swagger'; import type { TProjectId } from '@/types'; import { NoteModel } from '@/note/note.model'; import { NoteService } from '@/note/note.service'; -import { CurrentProject } from '@/common/decorators/current-project.decorator'; import { CreateNoteDto } from './dtos/create-note.dto'; +import { CurrentProject } from '@/common/decorators/current-project.decorator'; +import { GetByNoteableDto } from '@/note/dtos/get-by-noteable.dto'; @swagger.ApiTags('Notes') @swagger.ApiBearerAuth() @@ -20,6 +22,22 @@ export class NoteControllerExternal { return this.noteService.list(currentProjectId); } + @common.Get('/:noteableType/:noteableId') + @swagger.ApiForbiddenResponse() + @swagger.ApiOkResponse({ type: Array }) + async getByNoteable( + @Param('noteableType') noteableType: GetByNoteableDto['noteableType'], + @Param('noteableId') noteableId: GetByNoteableDto['noteableId'], + @CurrentProject() currentProjectId: TProjectId, + ) { + return this.noteService.list(currentProjectId, { + where: { + noteableId, + noteableType, + }, + }); + } + @common.Post() @swagger.ApiForbiddenResponse() @swagger.ApiCreatedResponse({ type: NoteModel }) diff --git a/services/workflows-service/src/note/note.repository.ts b/services/workflows-service/src/note/note.repository.ts index ca76f49094..a1207607df 100644 --- a/services/workflows-service/src/note/note.repository.ts +++ b/services/workflows-service/src/note/note.repository.ts @@ -43,7 +43,7 @@ export class NoteRepository { transaction: PrismaTransaction | PrismaClient = this.prismaService, ) { return transaction.note.findMany({ - ...((args || defaultArgs) as Prisma.NoteFindManyArgs), + select: { ...(args?.select || defaultArgs.select) } as Prisma.NoteFindManyArgs['select'], where: { ...(args?.where || {}), deletedAt: null, projectId }, }); } @@ -55,7 +55,9 @@ export class NoteRepository { transaction: PrismaTransaction | PrismaClient = this.prismaService, ) { return transaction.note.findFirstOrThrow({ - ...((args || defaultArgs) as Prisma.NoteFindFirstOrThrowArgs), + select: { + ...(args?.select || defaultArgs.select), + } as Prisma.NoteFindFirstOrThrowArgs['select'], where: { id, deletedAt: null, projectId }, }); } @@ -65,7 +67,7 @@ export class NoteRepository { args?: Prisma.SelectSubset>, ) { return this.prismaService.note.findMany({ - ...((args || defaultArgs) as Prisma.NoteFindManyArgs), + select: { ...(args?.select || defaultArgs.select) } as Prisma.NoteFindManyArgs['select'], where: { deletedAt: null, projectId }, }); }