feat(ratecard): implement insertOne functionality for single role creation and update API integration

This commit is contained in:
shancds
2025-05-22 13:03:08 +05:30
parent a879176c24
commit a711d48c9c
6 changed files with 84 additions and 26 deletions

View File

@@ -6,6 +6,24 @@ import HandleExceptions from "../decorators/handle-exceptions";
import WorklenzControllerBase from "./worklenz-controller-base"; 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
@HandleExceptions()
public static async insertOne(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
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]));
}
// Insert multiple roles for a project // Insert multiple roles for a project
@HandleExceptions() @HandleExceptions()
public static async insertMany(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> { public static async insertMany(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
@@ -81,12 +99,11 @@ export default class ProjectRateCardController extends WorklenzControllerBase {
if (!Array.isArray(roles) || !project_id) { if (!Array.isArray(roles) || !project_id) {
return res.status(400).send(new ServerResponse(false, null, "Invalid input")); return res.status(400).send(new ServerResponse(false, null, "Invalid input"));
} }
// Delete existing
await db.query(`DELETE FROM finance_project_rate_card_roles WHERE project_id = $1`, [project_id]);
// Insert new
if (roles.length === 0) { if (roles.length === 0) {
// If no roles provided, do nothing and return empty array
return res.status(200).send(new ServerResponse(true, [])); return res.status(200).send(new ServerResponse(true, []));
} }
// Build upsert query for all roles
const values = roles.map((role: any) => [ const values = roles.map((role: any) => [
project_id, project_id,
role.job_title_id, role.job_title_id,
@@ -95,8 +112,9 @@ export default class ProjectRateCardController extends WorklenzControllerBase {
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 ${values.map((_, i) => `($${i * 3 + 1}, $${i * 3 + 2}, $${i * 3 + 3})`).join(",")} 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 *, 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 flatValues = values.flat(); const flatValues = values.flat();
const result = await db.query(q, flatValues); const result = await db.query(q, flatValues);

View File

@@ -12,6 +12,12 @@ projectRatecardApiRouter.post(
projectManagerValidator, projectManagerValidator,
safeControllerFunction(ProjectRateCardController.insertMany) safeControllerFunction(ProjectRateCardController.insertMany)
); );
// Insert a single role for a project
projectRatecardApiRouter.post(
"/create-project-rate-card-role",
projectManagerValidator,
safeControllerFunction(ProjectRateCardController.insertOne)
);
// Get all roles for a project // Get all roles for a project
projectRatecardApiRouter.get( projectRatecardApiRouter.get(

View File

@@ -21,6 +21,14 @@ export const projectRateCardApiService = {
const response = await apiClient.post<IServerResponse<IProjectRateCardRole[]>>(rootUrl, { project_id, roles }); const response = await apiClient.post<IServerResponse<IProjectRateCardRole[]>>(rootUrl, { project_id, roles });
return response.data; return response.data;
}, },
// Insert a single role for a project
async insertOne({ project_id, job_title_id, rate }: { project_id: string; job_title_id: string; rate: number }): Promise<IServerResponse<IProjectRateCardRole>> {
const response = await apiClient.post<IServerResponse<IProjectRateCardRole>>(
`${rootUrl}/create-project-rate-card-role`,
{ project_id, job_title_id, rate }
);
return response.data;
},
// Get all roles for a project // Get all roles for a project
async getFromProjectId(project_id: string): Promise<IServerResponse<IProjectRateCardRole[]>> { async getFromProjectId(project_id: string): Promise<IServerResponse<IProjectRateCardRole[]>> {

View File

@@ -25,7 +25,6 @@ export const fetchProjectRateCardRoles = createAsyncThunk(
async (project_id: string, { rejectWithValue }) => { async (project_id: string, { rejectWithValue }) => {
try { try {
const response = await projectRateCardApiService.getFromProjectId(project_id); const response = await projectRateCardApiService.getFromProjectId(project_id);
console.log('Project RateCard Roles:', response);
return response.body; return response.body;
} catch (error) { } catch (error) {
logger.error('Fetch Project RateCard Roles', error); logger.error('Fetch Project RateCard Roles', error);
@@ -63,6 +62,23 @@ export const insertProjectRateCardRoles = createAsyncThunk(
} }
); );
export const insertProjectRateCardRole = createAsyncThunk(
'projectFinance/insertOne',
async (
{ project_id, job_title_id, rate }: { project_id: string; job_title_id: string; rate: number },
{ rejectWithValue }
) => {
try {
const response = await projectRateCardApiService.insertOne({ project_id, job_title_id, rate });
return response.body;
} catch (error) {
logger.error('Insert Project RateCard Role', error);
if (error instanceof Error) return rejectWithValue(error.message);
return rejectWithValue('Failed to insert project rate card role');
}
}
);
export const updateProjectRateCardRoleById = createAsyncThunk( export const updateProjectRateCardRoleById = createAsyncThunk(
'projectFinance/updateById', 'projectFinance/updateById',
async ({ id, body }: { id: string; body: { job_title_id: string; rate: string } }, { rejectWithValue }) => { async ({ id, body }: { id: string; body: { job_title_id: string; rate: string } }, { rejectWithValue }) => {

View File

@@ -253,6 +253,7 @@ const RatecardDrawer = ({
render: (text: number, record: any, index: number) => ( render: (text: number, record: any, index: number) => (
<Input <Input
type="number" type="number"
autoFocus={index === addingRowIndex}
value={roles[index]?.rate ?? 0} value={roles[index]?.rate ?? 0}
style={{ style={{
background: 'transparent', background: 'transparent',

View File

@@ -9,6 +9,7 @@ import { JobRoleType, IJobType, RatecardType } from '@/types/project/ratecard.ty
import { import {
deleteProjectRateCardRoleById, deleteProjectRateCardRoleById,
fetchProjectRateCardRoles, fetchProjectRateCardRoles,
insertProjectRateCardRole,
updateProjectRateCardRolesByProjectId, updateProjectRateCardRolesByProjectId,
} from '@/features/finance/project-finance-slice'; } from '@/features/finance/project-finance-slice';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
@@ -40,9 +41,8 @@ const RatecardTable: React.FC = () => {
// Sync local roles with redux roles // Sync local roles with redux roles
useEffect(() => { useEffect(() => {
console.log('Roles Redux:', rolesRedux);
setRoles(rolesRedux); setRoles(rolesRedux);
}, [rolesRedux]); }, [rolesRedux, dispatch]);
// Fetch roles on mount // Fetch roles on mount
useEffect(() => { useEffect(() => {
@@ -72,20 +72,29 @@ const RatecardTable: React.FC = () => {
}; };
// Handle job title select for new row // Handle job title select for new row
const handleSelectJobTitle = (jobTitleId: string) => { const handleSelectJobTitle = async(jobTitleId: string) => {
const jobTitle = jobTitles.find((jt) => jt.id === jobTitleId); const jobTitle = jobTitles.find((jt) => jt.id === jobTitleId);
if (!jobTitle) return; if (!jobTitle || !projectId) return;
// Prevent duplicates // Prevent duplicates
if (roles.some((r) => r.job_title_id === jobTitleId)) return; if (roles.some((r) => r.job_title_id === jobTitleId)) return;
setRoles([ // Dispatch and wait for result
...roles, const resultAction = await dispatch(
{ insertProjectRateCardRole({ project_id: projectId, job_title_id: jobTitleId, rate: 0 })
job_title_id: jobTitleId, );
jobtitle: jobTitle.name || '',
rate: 0, // If fulfilled, update local state with returned id
members: [], if (insertProjectRateCardRole.fulfilled.match(resultAction)) {
}, const newRole = resultAction.payload;
]); setRoles([
...roles,
{
id: newRole.id,
job_title_id: newRole.job_title_id,
jobtitle: newRole.jobtitle,
rate: newRole.rate,
},
]);
}
setAddingRow(false); setAddingRow(false);
}; };
@@ -221,14 +230,14 @@ const RatecardTable: React.FC = () => {
dataSource={ dataSource={
addingRow addingRow
? [ ? [
...roles, ...roles,
{ {
job_title_id: '', job_title_id: '',
jobtitle: '', jobtitle: '',
rate: 0, rate: 0,
members: [], members: [],
}, },
] ]
: roles : roles
} }
columns={columns} columns={columns}