From 57824f4b9e5a3d3f9add394fd8afb1aeea90ea2b Mon Sep 17 00:00:00 2001 From: juhanikat Date: Wed, 5 Aug 2026 13:20:56 +0300 Subject: [PATCH] change project update process in backend --- backend/src/controllers/projectsController.ts | 14 ---- backend/src/routes/projects.ts | 26 ++++-- backend/src/services/project.ts | 15 +++- backend/src/services/write/project.ts | 81 ++++++++++++++++++ backend/src/validators/projectsValidator.ts | 10 +-- .../src/components/Project/ProjectDetails.tsx | 83 ++++--------------- frontend/src/redux/projectReducer.ts | 10 +-- frontend/src/shared/validators/project.ts | 19 ++--- 8 files changed, 149 insertions(+), 109 deletions(-) delete mode 100644 backend/src/controllers/projectsController.ts create mode 100644 backend/src/services/write/project.ts diff --git a/backend/src/controllers/projectsController.ts b/backend/src/controllers/projectsController.ts deleted file mode 100644 index e6070677..00000000 --- a/backend/src/controllers/projectsController.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Request, Response } from 'express' -import { createProject, CreateProjectInput, updateProject, UpdateProjectInput } from '../services/projectsService' - -export const createProjectHandler = async (req: Request, res: Response) => { - const createdProject = await createProject(req.body) - - return res.status(201).json(createdProject) -} - -export const updateProjectHandler = async (req: Request<{ id: string }, object, UpdateProjectInput>, res: Response) => { - const updatedProject = await updateProject(Number(req.params.id), req.body) - - return res.status(200).json(updatedProject) -} diff --git a/backend/src/routes/projects.ts b/backend/src/routes/projects.ts index 2d30e407..d575315e 100644 --- a/backend/src/routes/projects.ts +++ b/backend/src/routes/projects.ts @@ -1,11 +1,27 @@ -import { Router } from 'express' -import { createProjectHandler, updateProjectHandler } from '../controllers/projectsController' +import { Request, Router } from 'express' +import { validateEntireProject } from 'src/services/project' +import { writeProject } from 'src/services/write/project' +import { EditDataType, EditMetaData, ProjectDetailsType, Role } from '../../../frontend/src/shared/types' import { requireOneOf } from '../middlewares/authorizer' -import { Role } from '../../../frontend/src/shared/types' const router = Router() -router.post('/', requireOneOf([Role.Admin]), createProjectHandler) -router.put('/:id', requireOneOf([Role.Admin]), updateProjectHandler) +router.put( + '/', + requireOneOf([Role.Admin]), + async (req: Request & EditMetaData }>, res) => { + try { + const { ...editedProject } = req.body.project + const validationErrors = validateEntireProject({ ...editedProject }) + if (validationErrors.length > 0) { + return res.status(403).send(validationErrors) + } + const project = await writeProject(editedProject) + return res.status(200).send({ project }) + } catch (error) { + return res.status(500).send({ message: 'Failed to write project' }) + } + } +) export default router diff --git a/backend/src/services/project.ts b/backend/src/services/project.ts index 6f3172c3..b3057cc2 100644 --- a/backend/src/services/project.ts +++ b/backend/src/services/project.ts @@ -1,4 +1,6 @@ -import { Role, User } from '../../../frontend/src/shared/types' +import { EditDataType, EditMetaData, ProjectDetailsType, Role, User } from '../../../frontend/src/shared/types' +import { validateProject } from '../../../frontend/src/shared/validators/project' +import { ValidationObject } from '../../../frontend/src/shared/validators/validator' import { nowDb } from '../utils/db' export const getAllProjects = async (user?: User) => { @@ -20,7 +22,6 @@ export const getAllProjects = async (user?: User) => { export const getProjectDetails = async (id: number) => { // TODO: Check if user has access - const result = await nowDb.now_proj.findUnique({ where: { pid: id }, include: { @@ -61,3 +62,13 @@ export const deleteProject = async (projectId: number) => { return true } + +export const validateEntireProject = (editedFields: EditDataType & EditMetaData) => { + const keys = Object.keys(editedFields) + const errors: ValidationObject[] = [] + for (const key of keys) { + const error = validateProject(editedFields as EditDataType, key as keyof ProjectDetailsType) + if (error.error) errors.push(error) + } + return errors +} diff --git a/backend/src/services/write/project.ts b/backend/src/services/write/project.ts new file mode 100644 index 00000000..b7c441cd --- /dev/null +++ b/backend/src/services/write/project.ts @@ -0,0 +1,81 @@ +import { ensureValidMemberIds, ValidationError } from 'src/validators/projectsValidator' +import { EditDataType, ProjectDetailsType } from '../../../../frontend/src/shared/types' +import Prisma from '../../../prisma/generated/now_test_client' +import { getFieldsOfTables, nowDb } from '../../utils/db' +import { getProjectDetails } from '../project' +import { filterAllowedKeys, fixRadioSelection } from './writeOperations/utils' + +const loadMembersByIds = async (memberIds: number[]) => { + if (!memberIds.length) return [] as Array<{ initials: string; user_id: number }> + + const members = await nowDb.com_people.findMany({ + select: { initials: true, user_id: true }, + where: { user_id: { in: memberIds } }, + }) + + if (members.length !== memberIds.length) { + throw new ValidationError('One or more project members do not exist') + } + + return members +} + +export const writeProject = async (project: EditDataType) => { + const allowedColumns = getFieldsOfTables(['now_proj']) + const filteredProject = filterAllowedKeys(project, allowedColumns) as Prisma.now_proj + let projectId: number + + const contactPerson = await nowDb.com_people.findFirst({ where: { initials: project.contact } }) + if (!contactPerson) { + throw new ValidationError('Contact does not exist') + } + + const memberUserIds = project.now_proj_people + .filter(person => person.rowState !== 'removed') + .map(person => person.com_people?.user_id) + const uniqueMemberIds = ensureValidMemberIds(memberUserIds) + const members = await loadMembersByIds(uniqueMemberIds) + + const existingProject = await getProjectDetails(filteredProject.pid) + if (!existingProject) { + const newProject = await nowDb.now_proj.create({ + data: { + ...filteredProject, + proj_records: fixRadioSelection(filteredProject.proj_records), + now_proj_people: + members.length > 0 + ? { + create: members.map(member => ({ initials: member.initials })), + } + : undefined, + }, + include: { now_proj_people: true }, + }) + projectId = newProject.pid + } else { + await nowDb.$transaction(async prisma => { + await prisma.now_proj.update({ + where: { pid: filteredProject.pid }, + data: { + ...filteredProject, + proj_records: fixRadioSelection(filteredProject.proj_records), + }, + }) + + if (existingProject) { + // removes all members, then adds back ones that are not marked as removed + if (Array.isArray(members)) { + await prisma.now_proj_people.deleteMany({ where: { pid: filteredProject.pid } }) + if (members.length > 0) { + await prisma.now_proj_people.createMany({ + data: members.map(member => ({ pid: filteredProject.pid, initials: member.initials })), + }) + } + } + } + }) + projectId = filteredProject.pid + } + + return projectId +} diff --git a/backend/src/validators/projectsValidator.ts b/backend/src/validators/projectsValidator.ts index 74fd34f9..012eb141 100644 --- a/backend/src/validators/projectsValidator.ts +++ b/backend/src/validators/projectsValidator.ts @@ -32,19 +32,19 @@ export const ensureNonEmptyString = (value: unknown, fieldName: string, maxLengt return trimmedValue } -export const ensureValidUserId = (value: unknown, fieldName: string) => { - if (typeof value !== 'number' || !Number.isInteger(value)) { +export const ensureValidUserId = (id: unknown, fieldName: string) => { + if (typeof id !== 'number' || !Number.isInteger(id)) { throw new ValidationError(`${fieldName} must be a valid number`) } - return value + return id } export const ensureValidMemberIds = (memberUserIds: unknown) => { - if (memberUserIds === undefined) return [] as number[] + if (memberUserIds === null) return [] as number[] if (!Array.isArray(memberUserIds)) { - throw new ValidationError('Project members must be an array of user IDs') + throw new ValidationError('Project members must be an array of persons') } const numericIds = memberUserIds.filter((id): id is number => typeof id === 'number' && Number.isInteger(id)) diff --git a/frontend/src/components/Project/ProjectDetails.tsx b/frontend/src/components/Project/ProjectDetails.tsx index 121103af..43e86687 100755 --- a/frontend/src/components/Project/ProjectDetails.tsx +++ b/frontend/src/components/Project/ProjectDetails.tsx @@ -2,35 +2,26 @@ import { useMemo } from 'react' import { useNavigate, useParams } from 'react-router-dom' import { useNotify } from '@/hooks/notification' -import { useUsersApi } from '@/hooks/useUsersApi' -import { - UpdateProjectPayload, - useDeleteProjectMutation, - useGetProjectDetailsQuery, - useUpdateProjectMutation, -} from '@/redux/projectReducer' -import { ProjectPeople, RowState } from '@/shared/types' +import { useDeleteProjectMutation, useGetProjectDetailsQuery, useUpdateProjectMutation } from '@/redux/projectReducer' +import { validateProject, validateProjectFields } from '@/shared/validators/project' import { CircularProgress } from '@mui/material' - import { DetailView, TabType } from '../DetailView/DetailView' import { CoordinatorTab } from './Tabs/CoordinatorTab' import type { EditDataType, ProjectDetailsType } from '@/shared/types' -import type { ValidationObject } from '@/shared/validators/validator' import type { FetchBaseQueryError } from '@reduxjs/toolkit/query' export const ProjectDetails = () => { const { id } = useParams() const projectId = useMemo(() => (id ? parseInt(id) : null), [id]) const { isLoading, isError, data } = useGetProjectDetailsQuery(id!, { skip: !projectId }) - const { users, isLoading: isUsersLoading, isError: isUsersError } = useUsersApi() const [deleteProject, { isLoading: isDeleting }] = useDeleteProjectMutation() const [updateProject, { isLoading: isUpdating }] = useUpdateProjectMutation() const { notify } = useNotify() const navigate = useNavigate() - if (isError || isUsersError) return
Error loading data
- if (isLoading || !data || isDeleting || isUsersLoading || isUpdating) return + if (isError) return
Error loading data
+ if (isLoading || !data || isDeleting || isUpdating) return if (data) { document.title = `Project - ${data.proj_name}` } @@ -56,64 +47,11 @@ export const ProjectDetails = () => { }, ] - const validator = ( - _editData: EditDataType, - field: keyof EditDataType - ): ValidationObject => ({ - name: String(field), - error: null, - }) - - const createProjectUpdatePayload = (editData: EditDataType): UpdateProjectPayload | null => { - const userOption = users.find(user => user.initials === editData.contact) - if (!userOption) { - notify('Could not find selected coordinator.', 'error') - return null - } - - const isRemoved = (member: EditDataType) => { - const state = (member as EditDataType & { rowState?: RowState }).rowState - return state === 'removed' || state === 'cancelled' - } - - const memberUserIds = Array.from( - new Set( - (editData.now_proj_people ?? []) - .filter(member => !isRemoved(member)) - .map(member => { - const userIdFromPeople = (member.com_people as { user_id?: number } | undefined)?.user_id - const userIdFromRelation = member.com_people?.user?.user_id - if (typeof userIdFromPeople === 'number') return userIdFromPeople - if (typeof userIdFromRelation === 'number') return userIdFromRelation - return users.find(user => user.initials === member.initials) ?? null - }) - .filter((id): id is number => typeof id === 'number') - ) - ) - return { - pid: editData.pid!, - projectCode: editData.proj_code!, - projectName: editData.proj_name!, - coordinatorUserId: userOption.userId, - projectStatus: editData.proj_status!, - recordStatus: editData.proj_records!, - memberUserIds, - } - } - const onWrite = async (editData: EditDataType) => { if (!projectId) return - const payload = createProjectUpdatePayload(editData) - if (!payload) { - return - } - - console.log(editData) - console.log(payload) - try { - await updateProject(payload).unwrap() + await updateProject(editData).unwrap() notify('Saved project successfully.') } catch (error) { const fetchError = error as FetchBaseQueryError @@ -131,5 +69,14 @@ export const ProjectDetails = () => { } } - return + return ( + + ) } diff --git a/frontend/src/redux/projectReducer.ts b/frontend/src/redux/projectReducer.ts index bf1cbe96..76cd89bb 100755 --- a/frontend/src/redux/projectReducer.ts +++ b/frontend/src/redux/projectReducer.ts @@ -1,5 +1,5 @@ import { api } from './api' -import { Project, ProjectDetailsType } from '@/shared/types' +import { EditDataType, Project, ProjectDetailsType } from '@/shared/types' export type CreateProjectPayload = { projectCode: string @@ -34,11 +34,11 @@ const projectsApi = api.injectEndpoints({ }), invalidatesTags: ['projects'], }), - updateProject: builder.mutation({ - query: ({ pid, ...body }) => ({ - url: `/projects/${pid}`, + updateProject: builder.mutation>({ + query: project => ({ + url: `/projects`, method: 'PUT', - body, + body: { project }, }), invalidatesTags: (result, _error, { pid }) => result ? [{ type: 'project', id: pid }, 'projects'] : ['projects'], diff --git a/frontend/src/shared/validators/project.ts b/frontend/src/shared/validators/project.ts index 2622e0db..a11fdd81 100644 --- a/frontend/src/shared/validators/project.ts +++ b/frontend/src/shared/validators/project.ts @@ -1,4 +1,4 @@ -import { EditDataType, Project } from '../types' +import { EditDataType, ProjectDetailsType } from '../types' import { Validators, validateFields, validator } from './validator' /* @@ -10,7 +10,7 @@ import { Validators, validateFields, validator } from './validator' proj_records: boolean | null; */ -const projectValidators: Validators>> = { +const projectValidators: Validators>> = { pid: { name: 'Project Id', required: true, @@ -39,16 +39,15 @@ const projectValidators: Validators>> = { proj_records: { name: 'Record Status', required: true, - asString: value => { - if (!['public', 'private'].includes(value)) return 'Record Status must be either true or false.' - return null - }, }, } -export const validateProject = (editData: EditDataType, fieldName: keyof EditDataType) => { - return validator>(projectValidators, editData, fieldName) +export const validateProject = ( + editData: EditDataType, + fieldName: keyof EditDataType +) => { + return validator>(projectValidators, editData, fieldName) } -export const validateProjectFields = (editData: Partial>) => - validateFields>(projectValidators, editData) +export const validateProjectFields = (editData: Partial>) => + validateFields>(projectValidators, editData)