From 1155ceee2e20e4268bddc4d376c19efde728857e Mon Sep 17 00:00:00 2001 From: Nicolas Date: Mon, 30 Dec 2024 17:43:51 +0100 Subject: [PATCH] project month wip --- backend/src/controllers/projectsMonth.ts | 12 ++++++-- .../src/controllers/utils/entity-events.ts | 4 +-- backend/src/server.ts | 4 +-- frontend/src/actions/configActions.ts | 2 +- frontend/src/actions/projectActions.ts | 29 +++++++++++++++++++ .../hooks/useEntityChangedToast.tsx | 8 ++--- .../components/project/EditProjectMonths.tsx | 3 ++ .../project-month-list/ProjectMonthsLists.tsx | 3 ++ .../components/socketio/EntityEventPayload.ts | 2 +- .../src/components/socketio/SocketService.ts | 14 ++++++--- frontend/src/trans.nl.ts | 3 +- 11 files changed, 66 insertions(+), 18 deletions(-) diff --git a/backend/src/controllers/projectsMonth.ts b/backend/src/controllers/projectsMonth.ts index d5e51fd3..92a6ff5b 100644 --- a/backend/src/controllers/projectsMonth.ts +++ b/backend/src/controllers/projectsMonth.ts @@ -2,9 +2,10 @@ import {Request, Response} from 'express'; import {ObjectID} from 'mongodb'; import moment from 'moment'; import {IProjectMonth, IProjectMonthOverview, TimesheetCheckAttachmentType} from '../models/projectsMonth'; -import {CollectionNames, createAudit, updateAudit} from '../models/common'; +import {CollectionNames, createAudit, SocketEventTypes, updateAudit} from '../models/common'; import {ConfacRequest} from '../models/technical'; import {saveAudit} from './utils/audit-logs'; +import { emitEntityEvent } from './utils/entity-events'; export const getProjectsPerMonthController = async (req: Request, res: Response) => { @@ -55,6 +56,8 @@ export const createProjectsMonthController = async (req: ConfacRequest, res: Res return createdProjectMonth; })); + emitEntityEvent(req, SocketEventTypes.EntityCreated, CollectionNames.PROJECTS_MONTH, null, createdProjectsMonth); + return res.send(createdProjectsMonth); }; @@ -68,7 +71,9 @@ export const patchProjectsMonthController = async (req: ConfacRequest, res: Resp const projMonthCollection = req.db.collection(CollectionNames.PROJECTS_MONTH); const {value: originalProjectMonth} = await projMonthCollection.findOneAndUpdate({_id: new ObjectID(_id)}, {$set: projectMonth}, {returnOriginal: true}); await saveAudit(req, 'projectMonth', originalProjectMonth, projectMonth); - return res.send({_id, ...projectMonth}); + const projectMonthResponse = {_id, ...projectMonth}; + emitEntityEvent(req, SocketEventTypes.EntityUpdated, CollectionNames.PROJECTS_MONTH, projectMonthResponse._id, projectMonthResponse); + return res.send(projectMonthResponse); } const inserted = await req.db.collection(CollectionNames.PROJECTS_MONTH).insertOne({ @@ -76,7 +81,7 @@ export const patchProjectsMonthController = async (req: ConfacRequest, res: Resp audit: createAudit(req.user), }); const [createdProjectMonth] = inserted.ops; - + emitEntityEvent(req, SocketEventTypes.EntityCreated, CollectionNames.PROJECTS_MONTH, createdProjectMonth._id, createdProjectMonth); return res.send(createdProjectMonth); }; @@ -86,5 +91,6 @@ export const deleteProjectsMonthController = async (req: ConfacRequest, res: Res const id = req.body.id; await req.db.collection(CollectionNames.PROJECTS_MONTH).findOneAndDelete({ _id: new ObjectID(id) }); await req.db.collection(CollectionNames.ATTACHMENTS_PROJECT_MONTH).findOneAndDelete({ _id: new ObjectID(id) }); + emitEntityEvent(req, SocketEventTypes.EntityDeleted, CollectionNames.PROJECTS_MONTH, id, null); return res.send(id); }; diff --git a/backend/src/controllers/utils/entity-events.ts b/backend/src/controllers/utils/entity-events.ts index 27f06b25..52638ceb 100644 --- a/backend/src/controllers/utils/entity-events.ts +++ b/backend/src/controllers/utils/entity-events.ts @@ -2,7 +2,7 @@ import { ObjectID } from 'bson'; import {CollectionNames, SocketEventTypes} from '../../models/common'; import {ConfacRequest} from '../../models/technical'; -export function emitEntityEvent(req: ConfacRequest, eventType: SocketEventTypes, entityType: CollectionNames, entityId: ObjectID, entity:any) { +export function emitEntityEvent(req: ConfacRequest, eventType: SocketEventTypes, entityType: CollectionNames, entityId: ObjectID | null, entity: any|null) { const sourceSocketId = req.headers['x-socket-id']; const sourceUserEmail = req.user?.data?.email; req.io.emit(eventType, { @@ -17,7 +17,7 @@ export function emitEntityEvent(req: ConfacRequest, eventType: SocketEventTypes, interface EntityEventPayload{ entityType: string; entity: any; - entityId: ObjectID; + entityId: ObjectID | null; sourceSocketId: string | undefined; sourceUserEmail: string | undefined; } \ No newline at end of file diff --git a/backend/src/server.ts b/backend/src/server.ts index 91949459..c8f390dd 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -19,7 +19,7 @@ const server = http.createServer(app); const io = new Server(server, { cors: { origin: '*', // Allow all origins - methods: ['GET', 'POST'], // Allowed HTTP methods + methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'], // Allowed HTTP methods allowedHeaders: ['x-socket-id'], // Optional: specify allowed headers credentials: true, // Allow credentials (e.g., cookies) }, @@ -31,7 +31,7 @@ sgMail.setApiKey(appConfig.SENDGRID_API_KEY); // Allow only specific origins (e.g., your frontend's URL) const corsOptions = { origin: 'http://localhost:3000', // Replace with your frontend URL - methods: ['GET', 'POST', 'PUT', 'DELETE'], // Allowed HTTP methods + methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'], // Allowed HTTP methods allowedHeaders: ['Content-Type', 'Authorization', 'x-socket-id'], // Allowed headers credentials: true, // Allow cookies and credentials }; diff --git a/frontend/src/actions/configActions.ts b/frontend/src/actions/configActions.ts index 7e38edf4..76fde829 100644 --- a/frontend/src/actions/configActions.ts +++ b/frontend/src/actions/configActions.ts @@ -28,7 +28,7 @@ export function updateConfig(newConfig: ConfigModel) { }; } -export function handleconfigSocketEvents(eventType: string, eventPayload: EntityEventPayload){ +export function handleConfigSocketEvents(eventType: string, eventPayload: EntityEventPayload){ return (dispatch: Dispatch) => { dispatch(busyToggle()); switch(eventType){ diff --git a/frontend/src/actions/projectActions.ts b/frontend/src/actions/projectActions.ts index 9cce4171..3f1d04cc 100644 --- a/frontend/src/actions/projectActions.ts +++ b/frontend/src/actions/projectActions.ts @@ -263,4 +263,33 @@ export function handleProjectSocketEvents(eventType: string, eventPayload: Entit } dispatch(busyToggle.off()); } +} + +export function handleProjectMonthSocketEvents(eventType: string, eventPayload: EntityEventPayload){ + return (dispatch: Dispatch) => { + dispatch(busyToggle()); + switch(eventType){ + case SocketEventTypes.EntityUpdated: + case SocketEventTypes.EntityCreated: + if(Array.isArray(eventPayload.entity)){ + dispatch({ + type: ACTION_TYPES.PROJECTS_MONTH_FETCHED, + projectsMonth: eventPayload.entity, + }); + }else{ + dispatch({ + type: ACTION_TYPES.PROJECTS_MONTH_UPDATE, + projectMonth: eventPayload.entity, + }); + } + break; + case SocketEventTypes.EntityDeleted: + dispatch({ + type: ACTION_TYPES.PROJECTS_MONTH_DELETE, + id: eventPayload.entityId, + }); break; + default: throw new Error(`${eventType} not supported for project month.`); + } + dispatch(busyToggle.off()); +} } \ No newline at end of file diff --git a/frontend/src/components/hooks/useEntityChangedToast.tsx b/frontend/src/components/hooks/useEntityChangedToast.tsx index 59269bf1..13608c7e 100644 --- a/frontend/src/components/hooks/useEntityChangedToast.tsx +++ b/frontend/src/components/hooks/useEntityChangedToast.tsx @@ -1,17 +1,17 @@ import { useEffect } from "react"; import { socketService } from "../socketio/SocketService"; -function useEntityChangedToast(entityId: string|null|undefined) { +function useEntityChangedToast(entityId: string|null|undefined, entityType: string|null|undefined = null) { useEffect(()=>{ var subs: undefined| (()=>void); - if(entityId){ - subs = socketService.enableToastsForEntity(entityId); + if(entityId || entityType ){ + subs = socketService.enableToastsForEntity(entityId, entityType); } return subs; - }, [entityId]); + }, [entityId, entityType]); } export default useEntityChangedToast; \ No newline at end of file diff --git a/frontend/src/components/project/EditProjectMonths.tsx b/frontend/src/components/project/EditProjectMonths.tsx index 73882226..ce9b539f 100644 --- a/frontend/src/components/project/EditProjectMonths.tsx +++ b/frontend/src/components/project/EditProjectMonths.tsx @@ -17,6 +17,7 @@ import {useProjectsMonth} from '../hooks/useProjects'; import {EnhanceWithConfirmation} from '../enhancers/EnhanceWithConfirmation'; import {Button} from '../controls/form-controls/Button'; import {useParams} from 'react-router-dom'; +import useEntityChangedToast from '../hooks/useEntityChangedToast'; const ConfirmationButton = EnhanceWithConfirmation(Button); @@ -27,6 +28,8 @@ export const EditProjectMonths = () => { const model = useProjectsMonth(params.projectMonthId); const [projectMonth, setProjectMonth] = useState((model && model.details) || getNewProjectMonth()); + useEntityChangedToast(model?._id); + const docTitle = projectMonth._id ? 'projectMonthEdit' : 'projectMonthNew'; const consultantName = (model && model.consultantName) || ''; const clientName = (model && model.client.name) || ''; diff --git a/frontend/src/components/project/project-month-list/ProjectMonthsLists.tsx b/frontend/src/components/project/project-month-list/ProjectMonthsLists.tsx index 53af9933..a85857ed 100644 --- a/frontend/src/components/project/project-month-list/ProjectMonthsLists.tsx +++ b/frontend/src/components/project/project-month-list/ProjectMonthsLists.tsx @@ -9,6 +9,7 @@ import { ProjectMonthsListToolbar } from './ProjectMonthsListToolbar'; import './project-month-list.scss'; +import useEntityChangedToast from '../../hooks/useEntityChangedToast'; /** The monthly invoicing tables including the top searchbar */ @@ -21,6 +22,8 @@ export const ProjectMonthsLists = () => { .filter((month, index, arr) => arr.indexOf(month) === index) .sort((a, b) => b.localeCompare(a)); + useEntityChangedToast(null, 'projects_month'); + return ( diff --git a/frontend/src/components/socketio/EntityEventPayload.ts b/frontend/src/components/socketio/EntityEventPayload.ts index f3f68843..25c7846d 100644 --- a/frontend/src/components/socketio/EntityEventPayload.ts +++ b/frontend/src/components/socketio/EntityEventPayload.ts @@ -1,7 +1,7 @@ export interface EntityEventPayload { entityType: string; entity: any; - entityId: string; + entityId: string | null; sourceSocketId: string | undefined; sourceUserEmail: string | undefined; } diff --git a/frontend/src/components/socketio/SocketService.ts b/frontend/src/components/socketio/SocketService.ts index c7d9576c..f61db5aa 100644 --- a/frontend/src/components/socketio/SocketService.ts +++ b/frontend/src/components/socketio/SocketService.ts @@ -1,7 +1,7 @@ import { Dispatch } from "redux"; import { io } from "socket.io-client"; -import { handleClientSocketEvents, handleconfigSocketEvents, handleConsultantSocketEvents, handleProjectSocketEvents } from "../../actions"; +import { handleClientSocketEvents, handleConfigSocketEvents, handleConsultantSocketEvents, handleProjectMonthSocketEvents, handleProjectSocketEvents } from "../../actions"; import { SocketEventTypes } from "./SocketEventTypes"; import { EntityEventPayload } from "./EntityEventPayload"; import { t } from "../utils"; @@ -44,7 +44,8 @@ function createSocketService () { case 'clients': dispatch(handleClientSocketEvents(eventType, eventPayload)); break; case 'users': dispatch(handleUserSocketEvents(eventType, eventPayload)); break; case 'roles': dispatch(handleRoleSocketEvents(eventType, eventPayload)); break; - case 'config': dispatch(handleconfigSocketEvents(eventType, eventPayload)); break; + case 'config': dispatch(handleConfigSocketEvents(eventType, eventPayload)); break; + case 'projects_month': dispatch(handleProjectMonthSocketEvents(eventType, eventPayload)); break; default: throw new Error(`${eventPayload.entityType} event for entity type not supported.`); }; }); @@ -70,6 +71,7 @@ function createSocketService () { default: throw new Error(`${eventType} not supported.`); } + // TODO nicolas debounce toasts toast.info( t(`socketio.operation.${operation}`, { entityType: t(`socketio.entities.${eventPayload.entityType}`), @@ -79,7 +81,7 @@ function createSocketService () { ); } - function enableToastsForEntity(entityId: string) { + function enableToastsForEntity(entityId: string|null|undefined, entityType: string|null|undefined) { var unsubscriptions: (()=>void)[] = []; function registerHandlerForEventType(eventType: SocketEventTypes){ @@ -89,10 +91,14 @@ function createSocketService () { console.log("Event ignored for entityId subscription => source socket id is self"); return; } - if(msg.entityId !== entityId){ + if(!!entityId && msg.entityId !== entityId){ console.log("Event ignored for entityId subscription => entity id not match"); return; } + if(!!entityType && msg.entityType !== entityType){ + console.log("Event ignored for entityType subscription => entity type not match"); + return; + } toastEntityChanged(eventType, msg); }; diff --git a/frontend/src/trans.nl.ts b/frontend/src/trans.nl.ts index fe033c9c..61f93d4c 100644 --- a/frontend/src/trans.nl.ts +++ b/frontend/src/trans.nl.ts @@ -684,7 +684,8 @@ export const trans = { clients: 'Klant', users: 'Gebruiker', roles: 'Rol', - config: 'Configuratie' + config: 'Configuratie', + projects_month: 'Project maand' }, operation: { entityUpdated: '{entityType} werd aangepast door {user}',