feat(ratecard): implement insertOne functionality for single role creation and update API integration
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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[]>> {
|
||||||
|
|||||||
@@ -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 }) => {
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user