feat(project-ratecard-member): ratecard member handle backend and frontend and all update fix bug
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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}`);
|
||||||
|
|||||||
@@ -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 }) => {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user