feat(project-ratecard-member): ratecard member handle backend and frontend and all update fix bug

This commit is contained in:
shancds
2025-05-23 13:57:53 +05:30
parent 22d0fc7049
commit 659ede7fb5
5 changed files with 159 additions and 37 deletions

View File

@@ -8,22 +8,22 @@ import WorklenzControllerBase from "./worklenz-controller-base";
export default class ProjectRateCardController extends WorklenzControllerBase { export default class ProjectRateCardController extends WorklenzControllerBase {
// Insert a single role for a project // Insert a single role for a project
@HandleExceptions() @HandleExceptions()
public static async createOne(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> { public static async createOne(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const { project_id, job_title_id, rate } = req.body; const { project_id, job_title_id, rate } = req.body;
if (!project_id || !job_title_id || typeof rate !== "number") { if (!project_id || !job_title_id || typeof rate !== "number") {
return res.status(400).send(new ServerResponse(false, null, "Invalid input")); return res.status(400).send(new ServerResponse(false, null, "Invalid input"));
} }
const q = ` const q = `
INSERT INTO finance_project_rate_card_roles (project_id, job_title_id, rate) INSERT INTO finance_project_rate_card_roles (project_id, job_title_id, rate)
VALUES ($1, $2, $3) VALUES ($1, $2, $3)
ON CONFLICT (project_id, job_title_id) DO UPDATE SET rate = EXCLUDED.rate ON CONFLICT (project_id, job_title_id) DO UPDATE SET rate = EXCLUDED.rate
RETURNING *, RETURNING *,
(SELECT name FROM job_titles jt WHERE jt.id = finance_project_rate_card_roles.job_title_id) AS jobtitle; (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]); const result = await db.query(q, [project_id, job_title_id, rate]);
return res.status(200).send(new ServerResponse(true, result.rows[0])); return res.status(200).send(new ServerResponse(true, result.rows[0]));
} }
// Insert multiple roles for a project // Insert multiple roles for a project
@HandleExceptions() @HandleExceptions()
public static async createMany(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> { public static async createMany(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
@@ -106,6 +106,81 @@ public static async createOne(req: IWorkLenzRequest, res: IWorkLenzResponse): Pr
return res.status(200).send(new ServerResponse(true, result.rows[0])); 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<IWorkLenzResponse> {
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) // Update all roles for a project (delete then insert)
@HandleExceptions() @HandleExceptions()
public static async updateByProjectId(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> { public static async updateByProjectId(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
@@ -124,11 +199,27 @@ public static async createOne(req: IWorkLenzRequest, res: IWorkLenzResponse): Pr
role.rate role.rate
]); ]);
const q = ` const q = `
INSERT INTO finance_project_rate_card_roles (project_id, job_title_id, rate) WITH upserted AS (
VALUES ${values.map((_, i) => `($${i * 3 + 1}, $${i * 3 + 2}, $${i * 3 + 3})`).join(",")} INSERT INTO finance_project_rate_card_roles (project_id, job_title_id, rate)
ON CONFLICT (project_id, job_title_id) DO UPDATE SET rate = EXCLUDED.rate, updated_at = NOW() VALUES ${values.map((_, i) => `($${i * 3 + 1}, $${i * 3 + 2}, $${i * 3 + 3})`).join(",")}
RETURNING *, ON CONFLICT (project_id, job_title_id)
(SELECT name FROM job_titles jt WHERE jt.id = finance_project_rate_card_roles.job_title_id) AS jobtitle; 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 flatValues = values.flat();
const result = await db.query(q, flatValues); const result = await db.query(q, flatValues);

View File

@@ -45,6 +45,14 @@ projectRatecardApiRouter.put(
safeControllerFunction(ProjectRateCardController.updateByProjectId) 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 // Delete a single role by id
projectRatecardApiRouter.delete( projectRatecardApiRouter.delete(
"/:id", "/:id",

View File

@@ -1,7 +1,7 @@
import apiClient from '@api/api-client'; import apiClient from '@api/api-client';
import { API_BASE_URL } from '@/shared/constants'; import { API_BASE_URL } from '@/shared/constants';
import { IServerResponse } from '@/types/common.types'; 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`; const rootUrl = `${API_BASE_URL}/project-rate-cards`;
@@ -54,6 +54,19 @@ export const projectRateCardApiService = {
return response.data; return response.data;
}, },
// Update project member rate card role
async updateMemberRateCardRole(
project_id: string,
member_id: string,
project_rate_card_role_id: string
): Promise<IServerResponse<JobRoleType>> {
const response = await apiClient.put<IServerResponse<JobRoleType>>(
`${rootUrl}/project/${project_id}/members/${member_id}/rate-card-role`,
{ project_rate_card_role_id }
);
return response.data;
},
// Delete a single role by id // Delete a single role by id
async deleteFromId(id: string): Promise<IServerResponse<IProjectRateCardRole>> { async deleteFromId(id: string): Promise<IServerResponse<IProjectRateCardRole>> {
const response = await apiClient.delete<IServerResponse<IProjectRateCardRole>>(`${rootUrl}/${id}`); const response = await apiClient.delete<IServerResponse<IProjectRateCardRole>>(`${rootUrl}/${id}`);

View File

@@ -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( export const deleteProjectRateCardRolesByProjectId = createAsyncThunk(
'projectFinance/deleteByProjectId', 'projectFinance/deleteByProjectId',
async (project_id: string, { rejectWithValue }) => { async (project_id: string, { rejectWithValue }) => {

View File

@@ -7,6 +7,7 @@ import { useAppDispatch } from '../../../../../../hooks/useAppDispatch';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { JobRoleType, IJobType, RatecardType } from '@/types/project/ratecard.types'; import { JobRoleType, IJobType, RatecardType } from '@/types/project/ratecard.types';
import { import {
assignMemberToRateCardRole,
deleteProjectRateCardRoleById, deleteProjectRateCardRoleById,
fetchProjectRateCardRoles, fetchProjectRateCardRoles,
insertProjectRateCardRole, insertProjectRateCardRole,
@@ -150,27 +151,28 @@ const RatecardTable: React.FC = () => {
}; };
// Handle member change // Handle member change
const handleMemberChange = (memberId: string, rowIndex: number, record: JobRoleType) => { const handleMemberChange = async (memberId: string, rowIndex: number, record: JobRoleType) => {
if (!projectId && !memberId) return; if (!projectId || !record.id) return; // Ensure required IDs are present
setRoles((prev) => try {
prev.map((role, idx) => { const resultAction = await dispatch(
if (idx !== rowIndex) return role; assignMemberToRateCardRole({
const members = Array.isArray(role.members) ? [...role.members] : []; project_id: projectId,
const memberIdx = members.indexOf(memberId); member_id: memberId,
if (memberIdx > -1) { project_rate_card_role_id: record.id,
members.splice(memberIdx, 1); // Remove if exists })
} else { );
members.push(memberId); // Add if not exists if (assignMemberToRateCardRole.fulfilled.match(resultAction)) {
} const updatedMembers = resultAction.payload; // Array of member IDs
return { ...role, members }; setRoles((prev) =>
}) prev.map((role, idx) => {
); if (idx !== rowIndex) return role;
// Log the required values return { ...role, members: updatedMembers?.members || [] };
console.log({ })
project_id: projectId, );
id: memberId, }
project_rate_card_role_id: record.id, } catch (error) {
}); console.error('Error assigning member:', error);
}
}; };
// Columns // Columns