mirror of
https://github.com/nowcommunity/nowdatabase.git
synced 2026-10-12 19:13:52 +03:00
change project update process in backend
This commit is contained in:
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user