change project update process in backend

This commit is contained in:
juhanikat
2026-08-05 16:36:54 +03:00
parent 57eec7c149
commit 57824f4b9e
8 changed files with 149 additions and 109 deletions
@@ -1,14 +0,0 @@
import { Request, Response } from 'express'
import { createProject, CreateProjectInput, updateProject, UpdateProjectInput } from '../services/projectsService'
export const createProjectHandler = async (req: Request<object, object, CreateProjectInput>, 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)
}
+21 -5
View File
@@ -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<object, object, { project: EditDataType<ProjectDetailsType> & 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
+13 -2
View File
@@ -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<ProjectDetailsType> & EditMetaData) => {
const keys = Object.keys(editedFields)
const errors: ValidationObject[] = []
for (const key of keys) {
const error = validateProject(editedFields as EditDataType<ProjectDetailsType>, key as keyof ProjectDetailsType)
if (error.error) errors.push(error)
}
return errors
}
+81
View File
@@ -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<ProjectDetailsType>) => {
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
}
+5 -5
View File
@@ -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))
@@ -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 <div>Error loading data</div>
if (isLoading || !data || isDeleting || isUsersLoading || isUpdating) return <CircularProgress />
if (isError) return <div>Error loading data</div>
if (isLoading || !data || isDeleting || isUpdating) return <CircularProgress />
if (data) {
document.title = `Project - ${data.proj_name}`
}
@@ -56,64 +47,11 @@ export const ProjectDetails = () => {
},
]
const validator = (
_editData: EditDataType<ProjectDetailsType>,
field: keyof EditDataType<ProjectDetailsType>
): ValidationObject => ({
name: String(field),
error: null,
})
const createProjectUpdatePayload = (editData: EditDataType<ProjectDetailsType>): 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<ProjectPeople>) => {
const state = (member as EditDataType<ProjectPeople> & { 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<ProjectDetailsType>) => {
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 <DetailView tabs={tabs} data={data} validator={validator} deleteFunction={deleteFunction} onWrite={onWrite} />
return (
<DetailView
tabs={tabs}
data={data}
validator={validateProject}
validateFields={validateProjectFields}
deleteFunction={deleteFunction}
onWrite={onWrite}
/>
)
}
+5 -5
View File
@@ -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<ProjectDetailsType, UpdateProjectPayload>({
query: ({ pid, ...body }) => ({
url: `/projects/${pid}`,
updateProject: builder.mutation<ProjectDetailsType, EditDataType<ProjectDetailsType>>({
query: project => ({
url: `/projects`,
method: 'PUT',
body,
body: { project },
}),
invalidatesTags: (result, _error, { pid }) =>
result ? [{ type: 'project', id: pid }, 'projects'] : ['projects'],
+9 -10
View File
@@ -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<Partial<EditDataType<Project>>> = {
const projectValidators: Validators<Partial<EditDataType<ProjectDetailsType>>> = {
pid: {
name: 'Project Id',
required: true,
@@ -39,16 +39,15 @@ const projectValidators: Validators<Partial<EditDataType<Project>>> = {
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<Project>, fieldName: keyof EditDataType<Project>) => {
return validator<EditDataType<Project>>(projectValidators, editData, fieldName)
export const validateProject = (
editData: EditDataType<ProjectDetailsType>,
fieldName: keyof EditDataType<ProjectDetailsType>
) => {
return validator<EditDataType<ProjectDetailsType>>(projectValidators, editData, fieldName)
}
export const validateProjectFields = (editData: Partial<EditDataType<Project>>) =>
validateFields<EditDataType<Project>>(projectValidators, editData)
export const validateProjectFields = (editData: Partial<EditDataType<ProjectDetailsType>>) =>
validateFields<EditDataType<ProjectDetailsType>>(projectValidators, editData)