feat(ratecard): implement CRUD operations and validation for rate cards

This commit is contained in:
shancds
2025-05-19 17:05:18 +05:30
parent 2b3b0ba635
commit fbfeaceb9c
7 changed files with 365 additions and 118 deletions

View File

@@ -0,0 +1,48 @@
-- Dropping existing finance_rate_cards table
DROP TABLE IF EXISTS finance_rate_cards;
-- Creating table to store rate card details
CREATE TABLE finance_rate_cards
(
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
team_id UUID NOT NULL REFERENCES teams (id) ON DELETE CASCADE,
name VARCHAR NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at timestamp with time zone NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- Dropping existing finance_project_rate_card_roles table
DROP TABLE IF EXISTS finance_project_rate_card_roles;
-- Creating table with single id primary key
CREATE TABLE finance_project_rate_card_roles
(
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
project_id UUID NOT NULL REFERENCES projects (id) ON DELETE CASCADE,
role_id UUID NOT NULL REFERENCES roles (id) ON DELETE CASCADE,
rate DECIMAL(10, 2) NOT NULL CHECK (rate >= 0),
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT unique_project_role UNIQUE (project_id, role_id)
);
-- Dropping existing finance_rate_card_roles table
DROP TABLE IF EXISTS finance_rate_card_roles;
-- Creating table to store role-specific rates for rate cards
CREATE TABLE finance_rate_card_roles
(
rate_card_id UUID NOT NULL REFERENCES finance_rate_cards (id) ON DELETE CASCADE,
role_id UUID REFERENCES roles (id) ON DELETE SET NULL,
rate DECIMAL(10, 2) NOT NULL CHECK (rate >= 0),
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- Adding project_rate_card_role_id column to project_members
ALTER TABLE project_members
ADD COLUMN project_rate_card_role_id UUID REFERENCES finance_project_rate_card_roles(id) ON DELETE SET NULL;
-- Adding rate_card column to projects
ALTER TABLE projects
ADD COLUMN rate_card UUID REFERENCES finance_rate_cards(id) ON DELETE SET NULL;
ALTER TABLE finance_rate_cards
ADD COLUMN currency TEXT NOT NULL DEFAULT 'LKR';

View File

@@ -0,0 +1,84 @@
import { IWorkLenzRequest } from "../interfaces/worklenz-request";
import { IWorkLenzResponse } from "../interfaces/worklenz-response";
import db from "../config/db";
import { ServerResponse } from "../models/server-response";
import WorklenzControllerBase from "./worklenz-controller-base";
import HandleExceptions from "../decorators/handle-exceptions";
export default class RateCardController extends WorklenzControllerBase {
@HandleExceptions()
public static async create(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const q = `
INSERT INTO finance_rate_cards (team_id, name)
VALUES ($1, $2)
RETURNING id, name, team_id, created_at, updated_at;
`;
const result = await db.query(q, [req.user?.team_id || null, req.body.name]);
const [data] = result.rows;
return res.status(200).send(new ServerResponse(true, data));
}
@HandleExceptions()
public static async get(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const { searchQuery, sortField, sortOrder, size, offset } = this.toPaginationOptions(req.query, "name");
const q = `
SELECT ROW_TO_JSON(rec) AS rate_cards
FROM (
SELECT COUNT(*) AS total,
(
SELECT COALESCE(ARRAY_TO_JSON(ARRAY_AGG(ROW_TO_JSON(t))), '[]'::JSON)
FROM (
SELECT id, name, team_id, created_at, updated_at
FROM finance_rate_cards
WHERE team_id = $1 ${searchQuery}
ORDER BY ${sortField} ${sortOrder}
LIMIT $2 OFFSET $3
) t
) AS data
FROM finance_rate_cards
WHERE team_id = $1 ${searchQuery}
) rec;
`;
const result = await db.query(q, [req.user?.team_id || null, size, offset]);
const [data] = result.rows;
return res.status(200).send(new ServerResponse(true, data.rate_cards || this.paginatedDatasetDefaultStruct));
}
@HandleExceptions()
public static async getById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const q = `
SELECT id, name, team_id, created_at, updated_at
FROM finance_rate_cards
WHERE id = $1 AND team_id = $2;
`;
const result = await db.query(q, [req.params.id, req.user?.team_id || null]);
const [data] = result.rows;
return res.status(200).send(new ServerResponse(true, data));
}
@HandleExceptions()
public static async update(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const q = `
UPDATE finance_rate_cards
SET name = $3, updated_at = NOW()
WHERE id = $1 AND team_id = $2
RETURNING id, name, team_id, created_at, updated_at;
`;
const result = await db.query(q, [req.params.id, req.user?.team_id || null, req.body.name]);
const [data] = result.rows;
return res.status(200).send(new ServerResponse(true, data));
}
@HandleExceptions()
public static async deleteById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise<IWorkLenzResponse> {
const q = `
DELETE FROM finance_rate_cards
WHERE id = $1 AND team_id = $2
RETURNING id;
`;
const result = await db.query(q, [req.params.id, req.user?.team_id || null]);
return res.status(200).send(new ServerResponse(true, result.rows.length > 0));
}
}

View File

@@ -0,0 +1,15 @@
import {NextFunction} from "express";
import {IWorkLenzRequest} from "../../interfaces/worklenz-request";
import {IWorkLenzResponse} from "../../interfaces/worklenz-response";
import {ServerResponse} from "../../models/server-response";
export default function (req: IWorkLenzRequest, res: IWorkLenzResponse, next: NextFunction): IWorkLenzResponse | void {
const {name} = req.body;
if (!name || name.trim() === "")
return res.status(200).send(new ServerResponse(false, null, "Name is required"));
req.body.name = req.body.name.trim();
return next();
}

View File

@@ -1,120 +1,123 @@
import express from "express";
import AccessControlsController from "../../controllers/access-controls-controller";
import AuthController from "../../controllers/auth-controller";
import LogsController from "../../controllers/logs-controller";
import OverviewController from "../../controllers/overview-controller";
import TaskPrioritiesController from "../../controllers/task-priorities-controller";
import attachmentsApiRouter from "./attachments-api-router";
import clientsApiRouter from "./clients-api-router";
import jobTitlesApiRouter from "./job-titles-api-router";
import notificationsApiRouter from "./notifications-api-router";
import personalOverviewApiRouter from "./personal-overview-api-router";
import projectMembersApiRouter from "./project-members-api-router";
import projectsApiRouter from "./projects-api-router";
import settingsApiRouter from "./settings-api-router";
import statusesApiRouter from "./statuses-api-router";
import subTasksApiRouter from "./sub-tasks-api-router";
import taskCommentsApiRouter from "./task-comments-api-router";
import taskWorkLogApiRouter from "./task-work-log-api-router";
import tasksApiRouter from "./tasks-api-router";
import teamMembersApiRouter from "./team-members-api-router";
import teamsApiRouter from "./teams-api-router";
import timezonesApiRouter from "./timezones-api-router";
import todoListApiRouter from "./todo-list-api-router";
import projectStatusesApiRouter from "./project-statuses-api-router";
import labelsApiRouter from "./labels-api-router";
import sharedProjectsApiRouter from "./shared-projects-api-router";
import resourceAllocationApiRouter from "./resource-allocation-api-router";
import taskTemplatesApiRouter from "./task-templates-api-router";
import projectInsightsApiRouter from "./project-insights-api-router";
import passwordValidator from "../../middlewares/validators/password-validator";
import adminCenterApiRouter from "./admin-center-api-router";
import reportingApiRouter from "./reporting-api-router";
import activityLogsApiRouter from "./activity-logs-api-router";
import safeControllerFunction from "../../shared/safe-controller-function";
import projectFoldersApiRouter from "./project-folders-api-router";
import taskPhasesApiRouter from "./task-phases-api-router";
import projectCategoriesApiRouter from "./project-categories-api-router";
import homePageApiRouter from "./home-page-api-router";
import ganttApiRouter from "./gantt-api-router";
import projectCommentsApiRouter from "./project-comments-api-router";
import reportingExportApiRouter from "./reporting-export-api-router";
import projectHealthsApiRouter from "./project-healths-api-router";
import ptTasksApiRouter from "./pt-tasks-api-router";
import projectTemplatesApiRouter from "./project-templates-api";
import ptTaskPhasesApiRouter from "./pt_task-phases-api-router";
import ptStatusesApiRouter from "./pt-statuses-api-router";
import workloadApiRouter from "./gannt-apis/workload-api-router";
import roadmapApiRouter from "./gannt-apis/roadmap-api-router";
import scheduleApiRouter from "./gannt-apis/schedule-api-router";
import scheduleApiV2Router from "./gannt-apis/schedule-api-v2-router";
import projectManagerApiRouter from "./project-managers-api-router";
import billingApiRouter from "./billing-api-router";
import taskDependenciesApiRouter from "./task-dependencies-api-router";
import taskRecurringApiRouter from "./task-recurring-api-router";
import express from "express";
import AccessControlsController from "../../controllers/access-controls-controller";
import AuthController from "../../controllers/auth-controller";
import LogsController from "../../controllers/logs-controller";
import OverviewController from "../../controllers/overview-controller";
import TaskPrioritiesController from "../../controllers/task-priorities-controller";
import attachmentsApiRouter from "./attachments-api-router";
import clientsApiRouter from "./clients-api-router";
import jobTitlesApiRouter from "./job-titles-api-router";
import notificationsApiRouter from "./notifications-api-router";
import personalOverviewApiRouter from "./personal-overview-api-router";
import projectMembersApiRouter from "./project-members-api-router";
import projectsApiRouter from "./projects-api-router";
import settingsApiRouter from "./settings-api-router";
import statusesApiRouter from "./statuses-api-router";
import subTasksApiRouter from "./sub-tasks-api-router";
import taskCommentsApiRouter from "./task-comments-api-router";
import taskWorkLogApiRouter from "./task-work-log-api-router";
import tasksApiRouter from "./tasks-api-router";
import teamMembersApiRouter from "./team-members-api-router";
import teamsApiRouter from "./teams-api-router";
import timezonesApiRouter from "./timezones-api-router";
import todoListApiRouter from "./todo-list-api-router";
import projectStatusesApiRouter from "./project-statuses-api-router";
import labelsApiRouter from "./labels-api-router";
import sharedProjectsApiRouter from "./shared-projects-api-router";
import resourceAllocationApiRouter from "./resource-allocation-api-router";
import taskTemplatesApiRouter from "./task-templates-api-router";
import projectInsightsApiRouter from "./project-insights-api-router";
import passwordValidator from "../../middlewares/validators/password-validator";
import adminCenterApiRouter from "./admin-center-api-router";
import reportingApiRouter from "./reporting-api-router";
import activityLogsApiRouter from "./activity-logs-api-router";
import safeControllerFunction from "../../shared/safe-controller-function";
import projectFoldersApiRouter from "./project-folders-api-router";
import taskPhasesApiRouter from "./task-phases-api-router";
import projectCategoriesApiRouter from "./project-categories-api-router";
import homePageApiRouter from "./home-page-api-router";
import ganttApiRouter from "./gantt-api-router";
import projectCommentsApiRouter from "./project-comments-api-router";
import reportingExportApiRouter from "./reporting-export-api-router";
import projectHealthsApiRouter from "./project-healths-api-router";
import ptTasksApiRouter from "./pt-tasks-api-router";
import projectTemplatesApiRouter from "./project-templates-api";
import ptTaskPhasesApiRouter from "./pt_task-phases-api-router";
import ptStatusesApiRouter from "./pt-statuses-api-router";
import workloadApiRouter from "./gannt-apis/workload-api-router";
import roadmapApiRouter from "./gannt-apis/roadmap-api-router";
import scheduleApiRouter from "./gannt-apis/schedule-api-router";
import scheduleApiV2Router from "./gannt-apis/schedule-api-v2-router";
import projectManagerApiRouter from "./project-managers-api-router";
import billingApiRouter from "./billing-api-router";
import taskDependenciesApiRouter from "./task-dependencies-api-router";
import taskRecurringApiRouter from "./task-recurring-api-router";
import customColumnsApiRouter from "./custom-columns-api-router";
import ratecardApiRouter from "./ratecard-api-router";
const api = express.Router();
api.use("/projects", projectsApiRouter);
api.use("/team-members", teamMembersApiRouter);
api.use("/job-titles", jobTitlesApiRouter);
api.use("/clients", clientsApiRouter);
api.use("/rate-cards", ratecardApiRouter);
api.use("/teams", teamsApiRouter);
api.use("/tasks", tasksApiRouter);
api.use("/settings", settingsApiRouter);
api.use("/personal-overview", personalOverviewApiRouter);
api.use("/statuses", statusesApiRouter);
api.use("/todo-list", todoListApiRouter);
api.use("/notifications", notificationsApiRouter);
api.use("/attachments", attachmentsApiRouter);
api.use("/sub-tasks", subTasksApiRouter);
api.use("/project-members", projectMembersApiRouter);
api.use("/task-time-log", taskWorkLogApiRouter);
api.use("/task-comments", taskCommentsApiRouter);
api.use("/timezones", timezonesApiRouter);
api.use("/project-statuses", projectStatusesApiRouter);
api.use("/labels", labelsApiRouter);
api.use("/resource-allocation", resourceAllocationApiRouter);
api.use("/shared/projects", sharedProjectsApiRouter);
api.use("/task-templates", taskTemplatesApiRouter);
api.use("/project-insights", projectInsightsApiRouter);
api.use("/admin-center", adminCenterApiRouter);
api.use("/reporting", reportingApiRouter);
api.use("/activity-logs", activityLogsApiRouter);
api.use("/projects-folders", projectFoldersApiRouter);
api.use("/task-phases", taskPhasesApiRouter);
api.use("/project-categories", projectCategoriesApiRouter);
api.use("/home", homePageApiRouter);
api.use("/gantt", ganttApiRouter);
api.use("/project-comments", projectCommentsApiRouter);
api.use("/reporting-export", reportingExportApiRouter);
api.use("/project-healths", projectHealthsApiRouter);
api.use("/project-templates", projectTemplatesApiRouter);
api.use("/pt-tasks", ptTasksApiRouter);
api.use("/pt-task-phases", ptTaskPhasesApiRouter);
api.use("/pt-statuses", ptStatusesApiRouter);
api.use("/workload-gannt", workloadApiRouter);
api.use("/roadmap-gannt", roadmapApiRouter);
api.use("/schedule-gannt", scheduleApiRouter);
api.use("/schedule-gannt-v2", scheduleApiV2Router);
api.use("/project-managers", projectManagerApiRouter);
api.get("/overview/:id", safeControllerFunction(OverviewController.getById));
api.get("/task-priorities", safeControllerFunction(TaskPrioritiesController.get));
api.post("/change-password", passwordValidator, safeControllerFunction(AuthController.changePassword));
api.get("/access-controls/roles", safeControllerFunction(AccessControlsController.getRoles));
api.get("/logs/my-dashboard", safeControllerFunction(LogsController.getActivityLog));
api.use("/billing", billingApiRouter);
api.use("/task-dependencies", taskDependenciesApiRouter);
api.use("/task-recurring", taskRecurringApiRouter);
const api = express.Router();
api.use("/projects", projectsApiRouter);
api.use("/team-members", teamMembersApiRouter);
api.use("/job-titles", jobTitlesApiRouter);
api.use("/clients", clientsApiRouter);
api.use("/teams", teamsApiRouter);
api.use("/tasks", tasksApiRouter);
api.use("/settings", settingsApiRouter);
api.use("/personal-overview", personalOverviewApiRouter);
api.use("/statuses", statusesApiRouter);
api.use("/todo-list", todoListApiRouter);
api.use("/notifications", notificationsApiRouter);
api.use("/attachments", attachmentsApiRouter);
api.use("/sub-tasks", subTasksApiRouter);
api.use("/project-members", projectMembersApiRouter);
api.use("/task-time-log", taskWorkLogApiRouter);
api.use("/task-comments", taskCommentsApiRouter);
api.use("/timezones", timezonesApiRouter);
api.use("/project-statuses", projectStatusesApiRouter);
api.use("/labels", labelsApiRouter);
api.use("/resource-allocation", resourceAllocationApiRouter);
api.use("/shared/projects", sharedProjectsApiRouter);
api.use("/task-templates", taskTemplatesApiRouter);
api.use("/project-insights", projectInsightsApiRouter);
api.use("/admin-center", adminCenterApiRouter);
api.use("/reporting", reportingApiRouter);
api.use("/activity-logs", activityLogsApiRouter);
api.use("/projects-folders", projectFoldersApiRouter);
api.use("/task-phases", taskPhasesApiRouter);
api.use("/project-categories", projectCategoriesApiRouter);
api.use("/home", homePageApiRouter);
api.use("/gantt", ganttApiRouter);
api.use("/project-comments", projectCommentsApiRouter);
api.use("/reporting-export", reportingExportApiRouter);
api.use("/project-healths", projectHealthsApiRouter);
api.use("/project-templates", projectTemplatesApiRouter);
api.use("/pt-tasks", ptTasksApiRouter);
api.use("/pt-task-phases", ptTaskPhasesApiRouter);
api.use("/pt-statuses", ptStatusesApiRouter);
api.use("/workload-gannt", workloadApiRouter);
api.use("/roadmap-gannt", roadmapApiRouter);
api.use("/schedule-gannt", scheduleApiRouter);
api.use("/schedule-gannt-v2", scheduleApiV2Router);
api.use("/project-managers", projectManagerApiRouter);
api.get("/overview/:id", safeControllerFunction(OverviewController.getById));
api.get("/task-priorities", safeControllerFunction(TaskPrioritiesController.get));
api.post("/change-password", passwordValidator, safeControllerFunction(AuthController.changePassword));
api.get("/access-controls/roles", safeControllerFunction(AccessControlsController.getRoles));
api.get("/logs/my-dashboard", safeControllerFunction(LogsController.getActivityLog));
api.use("/billing", billingApiRouter);
api.use("/task-dependencies", taskDependenciesApiRouter);
api.use("/task-recurring", taskRecurringApiRouter);
api.use("/custom-columns", customColumnsApiRouter);
export default api;

View File

@@ -0,0 +1,48 @@
import express from "express";
import RateCardController from "../../controllers/ratecard-controller";
import idParamValidator from "../../middlewares/validators/id-param-validator";
import teamOwnerOrAdminValidator from "../../middlewares/validators/team-owner-or-admin-validator";
import safeControllerFunction from "../../shared/safe-controller-function";
import projectManagerValidator from "../../middlewares/validators/project-manager-validator";
import ratecardBodyValidator from "../../middlewares/validators/ratecard-body-validator";
const ratecardApiRouter = express.Router();
ratecardApiRouter.post(
"/",
projectManagerValidator,
ratecardBodyValidator,
safeControllerFunction(RateCardController.create)
);
ratecardApiRouter.get(
"/",
safeControllerFunction(RateCardController.get)
);
ratecardApiRouter.get(
"/:id",
teamOwnerOrAdminValidator,
idParamValidator,
safeControllerFunction(RateCardController.getById)
);
ratecardApiRouter.put(
"/:id",
teamOwnerOrAdminValidator,
ratecardBodyValidator,
idParamValidator,
safeControllerFunction(RateCardController.update)
);
ratecardApiRouter.delete(
"/:id",
teamOwnerOrAdminValidator,
idParamValidator,
safeControllerFunction(RateCardController.deleteById)
);
export default ratecardApiRouter;

View File

@@ -0,0 +1,49 @@
import apiClient from '@api/api-client';
import { API_BASE_URL } from '@/shared/constants';
import { IServerResponse } from '@/types/common.types';
import { IJobTitle, IJobTitlesViewModel } from '@/types/job.types';
import { toQueryString } from '@/utils/toQueryString';
type IRatecard = {
id: string;}
type IRatecardViewModel = {
id: string;}
const rootUrl = `${API_BASE_URL}/rate-cards`;
export const rateCardApiService = {
async getRateCards(
index: number,
size: number,
field: string | null,
order: string | null,
search?: string | null
): Promise<IServerResponse<IRatecardViewModel>> {
const s = encodeURIComponent(search || '');
const queryString = toQueryString({ index, size, field, order, search: s });
const response = await apiClient.get<IServerResponse<IRatecardViewModel>>(
`${rootUrl}${queryString}`
);
return response.data;
},
async getRateCardById(id: string): Promise<IServerResponse<IRatecard>> {
const response = await apiClient.get<IServerResponse<IRatecard>>(`${rootUrl}/${id}`);
return response.data;
},
async createRateCard(body: IRatecard): Promise<IServerResponse<IRatecard>> {
const response = await apiClient.post<IServerResponse<IRatecard>>(rootUrl, body);
return response.data;
},
async updateRateCard(id: string, body: IRatecard): Promise<IServerResponse<IRatecard>> {
const response = await apiClient.put<IServerResponse<IRatecard>>(`${rootUrl}/${id}`, body);
return response.data;
},
async deleteRateCard(id: string): Promise<IServerResponse<void>> {
const response = await apiClient.delete<IServerResponse<void>>(`${rootUrl}/${id}`);
return response.data;
},
};

View File

@@ -1,7 +1,7 @@
export type NavRoutesType = {
name: string;
path: string;
adminOnly: boolean;
adminOnly?: boolean;
freePlanFeature?: boolean;
};