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 { Request, Router } from 'express'
|
||||||
import { createProjectHandler, updateProjectHandler } from '../controllers/projectsController'
|
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 { requireOneOf } from '../middlewares/authorizer'
|
||||||
import { Role } from '../../../frontend/src/shared/types'
|
|
||||||
|
|
||||||
const router = Router()
|
const router = Router()
|
||||||
|
|
||||||
router.post('/', requireOneOf([Role.Admin]), createProjectHandler)
|
router.put(
|
||||||
router.put('/:id', requireOneOf([Role.Admin]), updateProjectHandler)
|
'/',
|
||||||
|
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
|
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'
|
import { nowDb } from '../utils/db'
|
||||||
|
|
||||||
export const getAllProjects = async (user?: User) => {
|
export const getAllProjects = async (user?: User) => {
|
||||||
@@ -20,7 +22,6 @@ export const getAllProjects = async (user?: User) => {
|
|||||||
|
|
||||||
export const getProjectDetails = async (id: number) => {
|
export const getProjectDetails = async (id: number) => {
|
||||||
// TODO: Check if user has access
|
// TODO: Check if user has access
|
||||||
|
|
||||||
const result = await nowDb.now_proj.findUnique({
|
const result = await nowDb.now_proj.findUnique({
|
||||||
where: { pid: id },
|
where: { pid: id },
|
||||||
include: {
|
include: {
|
||||||
@@ -61,3 +62,13 @@ export const deleteProject = async (projectId: number) => {
|
|||||||
|
|
||||||
return true
|
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
|
return trimmedValue
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ensureValidUserId = (value: unknown, fieldName: string) => {
|
export const ensureValidUserId = (id: unknown, fieldName: string) => {
|
||||||
if (typeof value !== 'number' || !Number.isInteger(value)) {
|
if (typeof id !== 'number' || !Number.isInteger(id)) {
|
||||||
throw new ValidationError(`${fieldName} must be a valid number`)
|
throw new ValidationError(`${fieldName} must be a valid number`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return value
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ensureValidMemberIds = (memberUserIds: unknown) => {
|
export const ensureValidMemberIds = (memberUserIds: unknown) => {
|
||||||
if (memberUserIds === undefined) return [] as number[]
|
if (memberUserIds === null) return [] as number[]
|
||||||
|
|
||||||
if (!Array.isArray(memberUserIds)) {
|
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))
|
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 { useNavigate, useParams } from 'react-router-dom'
|
||||||
|
|
||||||
import { useNotify } from '@/hooks/notification'
|
import { useNotify } from '@/hooks/notification'
|
||||||
import { useUsersApi } from '@/hooks/useUsersApi'
|
import { useDeleteProjectMutation, useGetProjectDetailsQuery, useUpdateProjectMutation } from '@/redux/projectReducer'
|
||||||
import {
|
import { validateProject, validateProjectFields } from '@/shared/validators/project'
|
||||||
UpdateProjectPayload,
|
|
||||||
useDeleteProjectMutation,
|
|
||||||
useGetProjectDetailsQuery,
|
|
||||||
useUpdateProjectMutation,
|
|
||||||
} from '@/redux/projectReducer'
|
|
||||||
import { ProjectPeople, RowState } from '@/shared/types'
|
|
||||||
import { CircularProgress } from '@mui/material'
|
import { CircularProgress } from '@mui/material'
|
||||||
|
|
||||||
import { DetailView, TabType } from '../DetailView/DetailView'
|
import { DetailView, TabType } from '../DetailView/DetailView'
|
||||||
import { CoordinatorTab } from './Tabs/CoordinatorTab'
|
import { CoordinatorTab } from './Tabs/CoordinatorTab'
|
||||||
|
|
||||||
import type { EditDataType, ProjectDetailsType } from '@/shared/types'
|
import type { EditDataType, ProjectDetailsType } from '@/shared/types'
|
||||||
import type { ValidationObject } from '@/shared/validators/validator'
|
|
||||||
import type { FetchBaseQueryError } from '@reduxjs/toolkit/query'
|
import type { FetchBaseQueryError } from '@reduxjs/toolkit/query'
|
||||||
|
|
||||||
export const ProjectDetails = () => {
|
export const ProjectDetails = () => {
|
||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
const projectId = useMemo(() => (id ? parseInt(id) : null), [id])
|
const projectId = useMemo(() => (id ? parseInt(id) : null), [id])
|
||||||
const { isLoading, isError, data } = useGetProjectDetailsQuery(id!, { skip: !projectId })
|
const { isLoading, isError, data } = useGetProjectDetailsQuery(id!, { skip: !projectId })
|
||||||
const { users, isLoading: isUsersLoading, isError: isUsersError } = useUsersApi()
|
|
||||||
const [deleteProject, { isLoading: isDeleting }] = useDeleteProjectMutation()
|
const [deleteProject, { isLoading: isDeleting }] = useDeleteProjectMutation()
|
||||||
const [updateProject, { isLoading: isUpdating }] = useUpdateProjectMutation()
|
const [updateProject, { isLoading: isUpdating }] = useUpdateProjectMutation()
|
||||||
const { notify } = useNotify()
|
const { notify } = useNotify()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
if (isError || isUsersError) return <div>Error loading data</div>
|
if (isError) return <div>Error loading data</div>
|
||||||
if (isLoading || !data || isDeleting || isUsersLoading || isUpdating) return <CircularProgress />
|
if (isLoading || !data || isDeleting || isUpdating) return <CircularProgress />
|
||||||
if (data) {
|
if (data) {
|
||||||
document.title = `Project - ${data.proj_name}`
|
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>) => {
|
const onWrite = async (editData: EditDataType<ProjectDetailsType>) => {
|
||||||
if (!projectId) return
|
if (!projectId) return
|
||||||
|
|
||||||
const payload = createProjectUpdatePayload(editData)
|
|
||||||
if (!payload) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(editData)
|
|
||||||
console.log(payload)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await updateProject(payload).unwrap()
|
await updateProject(editData).unwrap()
|
||||||
notify('Saved project successfully.')
|
notify('Saved project successfully.')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const fetchError = error as FetchBaseQueryError
|
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 { api } from './api'
|
||||||
import { Project, ProjectDetailsType } from '@/shared/types'
|
import { EditDataType, Project, ProjectDetailsType } from '@/shared/types'
|
||||||
|
|
||||||
export type CreateProjectPayload = {
|
export type CreateProjectPayload = {
|
||||||
projectCode: string
|
projectCode: string
|
||||||
@@ -34,11 +34,11 @@ const projectsApi = api.injectEndpoints({
|
|||||||
}),
|
}),
|
||||||
invalidatesTags: ['projects'],
|
invalidatesTags: ['projects'],
|
||||||
}),
|
}),
|
||||||
updateProject: builder.mutation<ProjectDetailsType, UpdateProjectPayload>({
|
updateProject: builder.mutation<ProjectDetailsType, EditDataType<ProjectDetailsType>>({
|
||||||
query: ({ pid, ...body }) => ({
|
query: project => ({
|
||||||
url: `/projects/${pid}`,
|
url: `/projects`,
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body,
|
body: { project },
|
||||||
}),
|
}),
|
||||||
invalidatesTags: (result, _error, { pid }) =>
|
invalidatesTags: (result, _error, { pid }) =>
|
||||||
result ? [{ type: 'project', id: pid }, 'projects'] : ['projects'],
|
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'
|
import { Validators, validateFields, validator } from './validator'
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -10,7 +10,7 @@ import { Validators, validateFields, validator } from './validator'
|
|||||||
proj_records: boolean | null;
|
proj_records: boolean | null;
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const projectValidators: Validators<Partial<EditDataType<Project>>> = {
|
const projectValidators: Validators<Partial<EditDataType<ProjectDetailsType>>> = {
|
||||||
pid: {
|
pid: {
|
||||||
name: 'Project Id',
|
name: 'Project Id',
|
||||||
required: true,
|
required: true,
|
||||||
@@ -39,16 +39,15 @@ const projectValidators: Validators<Partial<EditDataType<Project>>> = {
|
|||||||
proj_records: {
|
proj_records: {
|
||||||
name: 'Record Status',
|
name: 'Record Status',
|
||||||
required: true,
|
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>) => {
|
export const validateProject = (
|
||||||
return validator<EditDataType<Project>>(projectValidators, editData, fieldName)
|
editData: EditDataType<ProjectDetailsType>,
|
||||||
|
fieldName: keyof EditDataType<ProjectDetailsType>
|
||||||
|
) => {
|
||||||
|
return validator<EditDataType<ProjectDetailsType>>(projectValidators, editData, fieldName)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const validateProjectFields = (editData: Partial<EditDataType<Project>>) =>
|
export const validateProjectFields = (editData: Partial<EditDataType<ProjectDetailsType>>) =>
|
||||||
validateFields<EditDataType<Project>>(projectValidators, editData)
|
validateFields<EditDataType<ProjectDetailsType>>(projectValidators, editData)
|
||||||
|
|||||||
Reference in New Issue
Block a user