From 659ede7fb51a201d7f18c4b34e4e9782d9070742 Mon Sep 17 00:00:00 2001 From: shancds Date: Fri, 23 May 2025 13:57:53 +0530 Subject: [PATCH] feat(project-ratecard-member): ratecard member handle backend and frontend and all update fix bug --- .../project-ratecard-controller.ts | 121 +++++++++++++++--- .../apis/project-ratecard-api-router.ts | 8 ++ .../project-finance-rate-cards.api.service.ts | 15 ++- .../features/finance/project-finance-slice.ts | 8 ++ .../reatecard-table/ratecard-table.tsx | 44 ++++--- 5 files changed, 159 insertions(+), 37 deletions(-) diff --git a/worklenz-backend/src/controllers/project-ratecard-controller.ts b/worklenz-backend/src/controllers/project-ratecard-controller.ts index 5d203bc0..42c0edf4 100644 --- a/worklenz-backend/src/controllers/project-ratecard-controller.ts +++ b/worklenz-backend/src/controllers/project-ratecard-controller.ts @@ -8,22 +8,22 @@ import WorklenzControllerBase from "./worklenz-controller-base"; export default class ProjectRateCardController extends WorklenzControllerBase { // Insert a single role for a project -@HandleExceptions() -public static async createOne(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { - const { project_id, job_title_id, rate } = req.body; - if (!project_id || !job_title_id || typeof rate !== "number") { - return res.status(400).send(new ServerResponse(false, null, "Invalid input")); - } - const q = ` + @HandleExceptions() + public static async createOne(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + const { project_id, job_title_id, rate } = req.body; + if (!project_id || !job_title_id || typeof rate !== "number") { + return res.status(400).send(new ServerResponse(false, null, "Invalid input")); + } + const q = ` INSERT INTO finance_project_rate_card_roles (project_id, job_title_id, rate) VALUES ($1, $2, $3) ON CONFLICT (project_id, job_title_id) DO UPDATE SET rate = EXCLUDED.rate RETURNING *, (SELECT name FROM job_titles jt WHERE jt.id = finance_project_rate_card_roles.job_title_id) AS jobtitle; `; - const result = await db.query(q, [project_id, job_title_id, rate]); - return res.status(200).send(new ServerResponse(true, result.rows[0])); -} + const result = await db.query(q, [project_id, job_title_id, rate]); + return res.status(200).send(new ServerResponse(true, result.rows[0])); + } // Insert multiple roles for a project @HandleExceptions() public static async createMany(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { @@ -106,6 +106,81 @@ public static async createOne(req: IWorkLenzRequest, res: IWorkLenzResponse): Pr return res.status(200).send(new ServerResponse(true, result.rows[0])); } + // update project member rate for a project with members + @HandleExceptions() + public static async updateProjectMemberByProjectIdAndMemberId( + req: IWorkLenzRequest, + res: IWorkLenzResponse + ): Promise { + const { project_id, id } = req.params; + const { project_rate_card_role_id } = req.body; + + if (!project_id || !id || !project_rate_card_role_id) { + return res.status(400).send(new ServerResponse(false, null, "Missing values")); + } + + try { + // Step 1: Check current role assignment + const checkQuery = ` + SELECT project_rate_card_role_id + FROM project_members + WHERE id = $1 AND project_id = $2; + `; + const { rows: checkRows } = await db.query(checkQuery, [id, project_id]); + + const currentRoleId = checkRows[0]?.project_rate_card_role_id; + + if (currentRoleId !== null && currentRoleId !== project_rate_card_role_id) { + // Step 2: Fetch members with the requested role + const membersQuery = ` + SELECT COALESCE(json_agg(id), '[]'::json) AS members + FROM project_members + WHERE project_id = $1 AND project_rate_card_role_id = $2; + `; + const { rows: memberRows } = await db.query(membersQuery, [project_id, project_rate_card_role_id]); + + return res.status(200).send( + new ServerResponse(false, memberRows[0], "Already Assigned !") + ); + } + + // Step 3: Perform the update + const updateQuery = ` + UPDATE project_members + SET project_rate_card_role_id = CASE + WHEN project_rate_card_role_id = $1 THEN NULL + ELSE $1 + END + WHERE id = $2 + AND project_id = $3 + AND EXISTS ( + SELECT 1 + FROM finance_project_rate_card_roles + WHERE id = $1 AND project_id = $3 + ) + RETURNING project_rate_card_role_id; + `; + const { rows: updateRows } = await db.query(updateQuery, [project_rate_card_role_id, id, project_id]); + + if (updateRows.length === 0) { + return res.status(200).send(new ServerResponse(true, [], "Project member not found or invalid project_rate_card_role_id")); + } + + const updatedRoleId = updateRows[0].project_rate_card_role_id || project_rate_card_role_id; + + // Step 4: Fetch updated members list + const membersQuery = ` + SELECT COALESCE(json_agg(id), '[]'::json) AS members + FROM project_members + WHERE project_id = $1 AND project_rate_card_role_id = $2; + `; + const { rows: finalMembers } = await db.query(membersQuery, [project_id, updatedRoleId]); + + return res.status(200).send(new ServerResponse(true, finalMembers[0])); + } catch (error) { + return res.status(500).send(new ServerResponse(false, null, "Internal server error")); + } + } // Update all roles for a project (delete then insert) @HandleExceptions() public static async updateByProjectId(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { @@ -124,11 +199,27 @@ public static async createOne(req: IWorkLenzRequest, res: IWorkLenzResponse): Pr role.rate ]); const q = ` - INSERT INTO finance_project_rate_card_roles (project_id, job_title_id, rate) - VALUES ${values.map((_, i) => `($${i * 3 + 1}, $${i * 3 + 2}, $${i * 3 + 3})`).join(",")} - ON CONFLICT (project_id, job_title_id) DO UPDATE SET rate = EXCLUDED.rate, updated_at = NOW() - RETURNING *, - (SELECT name FROM job_titles jt WHERE jt.id = finance_project_rate_card_roles.job_title_id) AS jobtitle; + WITH upserted AS ( + INSERT INTO finance_project_rate_card_roles (project_id, job_title_id, rate) + VALUES ${values.map((_, i) => `($${i * 3 + 1}, $${i * 3 + 2}, $${i * 3 + 3})`).join(",")} + ON CONFLICT (project_id, job_title_id) + DO UPDATE SET rate = EXCLUDED.rate, updated_at = NOW() + RETURNING * + ), + jobtitles AS ( + SELECT upr.*, jt.name AS jobtitle + FROM upserted upr + JOIN job_titles jt ON jt.id = upr.job_title_id + ), + members AS ( + SELECT json_agg(pm.id) AS members, pm.project_rate_card_role_id + FROM project_members pm + WHERE pm.project_rate_card_role_id IN (SELECT id FROM jobtitles) + GROUP BY pm.project_rate_card_role_id + ) + SELECT jt.*, m.members + FROM jobtitles jt + LEFT JOIN members m ON m.project_rate_card_role_id = jt.id; `; const flatValues = values.flat(); const result = await db.query(q, flatValues); diff --git a/worklenz-backend/src/routes/apis/project-ratecard-api-router.ts b/worklenz-backend/src/routes/apis/project-ratecard-api-router.ts index d1e17fc2..09462c4d 100644 --- a/worklenz-backend/src/routes/apis/project-ratecard-api-router.ts +++ b/worklenz-backend/src/routes/apis/project-ratecard-api-router.ts @@ -45,6 +45,14 @@ projectRatecardApiRouter.put( safeControllerFunction(ProjectRateCardController.updateByProjectId) ); +// Update project member rate card role +projectRatecardApiRouter.put( + "/project/:project_id/members/:id/rate-card-role", + idParamValidator, + projectManagerValidator, + safeControllerFunction(ProjectRateCardController.updateProjectMemberByProjectIdAndMemberId) +); + // Delete a single role by id projectRatecardApiRouter.delete( "/:id", diff --git a/worklenz-frontend/src/api/project-finance-ratecard/project-finance-rate-cards.api.service.ts b/worklenz-frontend/src/api/project-finance-ratecard/project-finance-rate-cards.api.service.ts index df2072cb..6546dbf9 100644 --- a/worklenz-frontend/src/api/project-finance-ratecard/project-finance-rate-cards.api.service.ts +++ b/worklenz-frontend/src/api/project-finance-ratecard/project-finance-rate-cards.api.service.ts @@ -1,7 +1,7 @@ import apiClient from '@api/api-client'; import { API_BASE_URL } from '@/shared/constants'; import { IServerResponse } from '@/types/common.types'; -import { IJobType } from '@/types/project/ratecard.types'; +import { IJobType, JobRoleType } from '@/types/project/ratecard.types'; const rootUrl = `${API_BASE_URL}/project-rate-cards`; @@ -54,6 +54,19 @@ export const projectRateCardApiService = { return response.data; }, + // Update project member rate card role + async updateMemberRateCardRole( + project_id: string, + member_id: string, + project_rate_card_role_id: string + ): Promise> { + const response = await apiClient.put>( + `${rootUrl}/project/${project_id}/members/${member_id}/rate-card-role`, + { project_rate_card_role_id } + ); + return response.data; + }, + // Delete a single role by id async deleteFromId(id: string): Promise> { const response = await apiClient.delete>(`${rootUrl}/${id}`); diff --git a/worklenz-frontend/src/features/finance/project-finance-slice.ts b/worklenz-frontend/src/features/finance/project-finance-slice.ts index d0f897dd..c9549cfc 100644 --- a/worklenz-frontend/src/features/finance/project-finance-slice.ts +++ b/worklenz-frontend/src/features/finance/project-finance-slice.ts @@ -121,6 +121,14 @@ export const deleteProjectRateCardRoleById = createAsyncThunk( } ); +export const assignMemberToRateCardRole = createAsyncThunk( + 'projectFinance/assignMemberToRateCardRole', + async ({ project_id, member_id, project_rate_card_role_id }: { project_id: string; member_id: string; project_rate_card_role_id: string }) => { + const response = await projectRateCardApiService.updateMemberRateCardRole(project_id, member_id, project_rate_card_role_id); + return response.body; + } +); + export const deleteProjectRateCardRolesByProjectId = createAsyncThunk( 'projectFinance/deleteByProjectId', async (project_id: string, { rejectWithValue }) => { diff --git a/worklenz-frontend/src/pages/projects/projectView/finance/ratecard-tab/reatecard-table/ratecard-table.tsx b/worklenz-frontend/src/pages/projects/projectView/finance/ratecard-tab/reatecard-table/ratecard-table.tsx index 2e5bacca..6641c80d 100644 --- a/worklenz-frontend/src/pages/projects/projectView/finance/ratecard-tab/reatecard-table/ratecard-table.tsx +++ b/worklenz-frontend/src/pages/projects/projectView/finance/ratecard-tab/reatecard-table/ratecard-table.tsx @@ -7,6 +7,7 @@ import { useAppDispatch } from '../../../../../../hooks/useAppDispatch'; import { useTranslation } from 'react-i18next'; import { JobRoleType, IJobType, RatecardType } from '@/types/project/ratecard.types'; import { + assignMemberToRateCardRole, deleteProjectRateCardRoleById, fetchProjectRateCardRoles, insertProjectRateCardRole, @@ -150,27 +151,28 @@ const RatecardTable: React.FC = () => { }; // Handle member change - const handleMemberChange = (memberId: string, rowIndex: number, record: JobRoleType) => { - if (!projectId && !memberId) return; - setRoles((prev) => - prev.map((role, idx) => { - if (idx !== rowIndex) return role; - const members = Array.isArray(role.members) ? [...role.members] : []; - const memberIdx = members.indexOf(memberId); - if (memberIdx > -1) { - members.splice(memberIdx, 1); // Remove if exists - } else { - members.push(memberId); // Add if not exists - } - return { ...role, members }; - }) - ); - // Log the required values - console.log({ - project_id: projectId, - id: memberId, - project_rate_card_role_id: record.id, - }); + const handleMemberChange = async (memberId: string, rowIndex: number, record: JobRoleType) => { + if (!projectId || !record.id) return; // Ensure required IDs are present + try { + const resultAction = await dispatch( + assignMemberToRateCardRole({ + project_id: projectId, + member_id: memberId, + project_rate_card_role_id: record.id, + }) + ); + if (assignMemberToRateCardRole.fulfilled.match(resultAction)) { + const updatedMembers = resultAction.payload; // Array of member IDs + setRoles((prev) => + prev.map((role, idx) => { + if (idx !== rowIndex) return role; + return { ...role, members: updatedMembers?.members || [] }; + }) + ); + } + } catch (error) { + console.error('Error assigning member:', error); + } }; // Columns