diff --git a/worklenz-backend/src/controllers/project-ratecard-controller.ts b/worklenz-backend/src/controllers/project-ratecard-controller.ts index 99543712..5d203bc0 100644 --- a/worklenz-backend/src/controllers/project-ratecard-controller.ts +++ b/worklenz-backend/src/controllers/project-ratecard-controller.ts @@ -9,7 +9,7 @@ export default class ProjectRateCardController extends WorklenzControllerBase { // Insert a single role for a project @HandleExceptions() -public static async insertOne(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { +public static async createOne(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { 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")); @@ -26,7 +26,7 @@ public static async insertOne(req: IWorkLenzRequest, res: IWorkLenzResponse): Pr } // Insert multiple roles for a project @HandleExceptions() - public static async insertMany(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + public static async createMany(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { const { project_id, roles } = req.body; if (!Array.isArray(roles) || !project_id) { return res.status(400).send(new ServerResponse(false, null, "Invalid input")); @@ -50,10 +50,17 @@ public static async insertOne(req: IWorkLenzRequest, res: IWorkLenzResponse): Pr // Get all roles for a project @HandleExceptions() - public static async getFromProjectId(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + public static async getByProjectId(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { const { project_id } = req.params; const q = ` - SELECT fprr.*, jt.name as jobtitle + SELECT + fprr.*, + jt.name as jobtitle, + ( + SELECT COALESCE(json_agg(pm.id), '[]'::json) + FROM project_members pm + WHERE pm.project_rate_card_role_id = fprr.id + ) AS members FROM finance_project_rate_card_roles fprr LEFT JOIN job_titles jt ON fprr.job_title_id = jt.id WHERE fprr.project_id = $1 @@ -65,10 +72,17 @@ public static async insertOne(req: IWorkLenzRequest, res: IWorkLenzResponse): Pr // Get a single role by id @HandleExceptions() - public static async getFromId(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + public static async getById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { const { id } = req.params; const q = ` - SELECT fprr.*, jt.name as jobtitle + SELECT + fprr.*, + jt.name as jobtitle, + ( + SELECT COALESCE(json_agg(pm.id), '[]'::json) + FROM project_members pm + WHERE pm.project_rate_card_role_id = fprr.id + ) AS members FROM finance_project_rate_card_roles fprr LEFT JOIN job_titles jt ON fprr.job_title_id = jt.id WHERE fprr.id = $1; @@ -79,7 +93,7 @@ public static async insertOne(req: IWorkLenzRequest, res: IWorkLenzResponse): Pr // Update a single role by id @HandleExceptions() - public static async updateFromId(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + public static async updateById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { const { id } = req.params; const { job_title_id, rate } = req.body; const q = ` @@ -94,7 +108,7 @@ public static async insertOne(req: IWorkLenzRequest, res: IWorkLenzResponse): Pr // Update all roles for a project (delete then insert) @HandleExceptions() - public static async updateFromProjectId(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + public static async updateByProjectId(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { const { project_id, roles } = req.body; if (!Array.isArray(roles) || !project_id) { return res.status(400).send(new ServerResponse(false, null, "Invalid input")); @@ -123,7 +137,7 @@ public static async insertOne(req: IWorkLenzRequest, res: IWorkLenzResponse): Pr // Delete a single role by id @HandleExceptions() - public static async deleteFromId(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + public static async deleteById(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { const { id } = req.params; const q = `DELETE FROM finance_project_rate_card_roles WHERE id = $1 RETURNING *;`; const result = await db.query(q, [id]); @@ -132,7 +146,7 @@ public static async insertOne(req: IWorkLenzRequest, res: IWorkLenzResponse): Pr // Delete all roles for a project @HandleExceptions() - public static async deleteFromProjectId(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { + public static async deleteByProjectId(req: IWorkLenzRequest, res: IWorkLenzResponse): Promise { const { project_id } = req.params; const q = `DELETE FROM finance_project_rate_card_roles WHERE project_id = $1 RETURNING *;`; const result = await db.query(q, [project_id]); diff --git a/worklenz-backend/src/routes/apis/project-ratecard-api-router.ts b/worklenz-backend/src/routes/apis/project-ratecard-api-router.ts index 2ed31118..d1e17fc2 100644 --- a/worklenz-backend/src/routes/apis/project-ratecard-api-router.ts +++ b/worklenz-backend/src/routes/apis/project-ratecard-api-router.ts @@ -10,52 +10,52 @@ const projectRatecardApiRouter = express.Router(); projectRatecardApiRouter.post( "/", projectManagerValidator, - safeControllerFunction(ProjectRateCardController.insertMany) + safeControllerFunction(ProjectRateCardController.createMany) ); // Insert a single role for a project projectRatecardApiRouter.post( "/create-project-rate-card-role", projectManagerValidator, - safeControllerFunction(ProjectRateCardController.insertOne) + safeControllerFunction(ProjectRateCardController.createOne) ); // Get all roles for a project projectRatecardApiRouter.get( "/project/:project_id", - safeControllerFunction(ProjectRateCardController.getFromProjectId) + safeControllerFunction(ProjectRateCardController.getByProjectId) ); // Get a single role by id projectRatecardApiRouter.get( "/:id", idParamValidator, - safeControllerFunction(ProjectRateCardController.getFromId) + safeControllerFunction(ProjectRateCardController.getById) ); // Update a single role by id projectRatecardApiRouter.put( "/:id", idParamValidator, - safeControllerFunction(ProjectRateCardController.updateFromId) + safeControllerFunction(ProjectRateCardController.updateById) ); // Update all roles for a project (delete then insert) projectRatecardApiRouter.put( "/project/:project_id", - safeControllerFunction(ProjectRateCardController.updateFromProjectId) + safeControllerFunction(ProjectRateCardController.updateByProjectId) ); // Delete a single role by id projectRatecardApiRouter.delete( "/:id", idParamValidator, - safeControllerFunction(ProjectRateCardController.deleteFromId) + safeControllerFunction(ProjectRateCardController.deleteById) ); // Delete all roles for a project projectRatecardApiRouter.delete( "/project/:project_id", - safeControllerFunction(ProjectRateCardController.deleteFromProjectId) + safeControllerFunction(ProjectRateCardController.deleteByProjectId) ); export default projectRatecardApiRouter; diff --git a/worklenz-frontend/src/components/project-ratecard/ratecard-assignee-selector.tsx b/worklenz-frontend/src/components/project-ratecard/ratecard-assignee-selector.tsx new file mode 100644 index 00000000..96cf53e8 --- /dev/null +++ b/worklenz-frontend/src/components/project-ratecard/ratecard-assignee-selector.tsx @@ -0,0 +1,104 @@ +import { useEffect, useRef, useState } from 'react'; +import { InputRef } from 'antd/es/input'; +import Dropdown from 'antd/es/dropdown'; +import Card from 'antd/es/card'; +import List from 'antd/es/list'; +import Input from 'antd/es/input'; +import Checkbox from 'antd/es/checkbox'; +import Button from 'antd/es/button'; +import Empty from 'antd/es/empty'; +import { PlusOutlined } from '@ant-design/icons'; +import SingleAvatar from '../common/single-avatar/single-avatar'; +import { JobRoleType } from '@/types/project/ratecard.types'; +import { IProjectMembersViewModel, IProjectMemberViewModel } from '@/types/projectMember.types'; + +interface RateCardAssigneeSelectorProps { + projectId: string; + onChange?: (memberId: string) => void; + selectedMemberIds?: string[]; + memberlist?: IProjectMemberViewModel[]; +} + +const RateCardAssigneeSelector = ({ + projectId, + onChange, + selectedMemberIds = [], + memberlist, +}: RateCardAssigneeSelectorProps) => { + const membersInputRef = useRef(null); + const [searchQuery, setSearchQuery] = useState(''); + const [members, setMembers] = useState(memberlist || []); + const [isLoading, setIsLoading] = useState(false); + const filteredMembers = members.filter(member => + member.name?.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + const dropdownContent = ( + + setSearchQuery(e.currentTarget.value)} + placeholder="Search members" + /> + + {filteredMembers.length ? ( + filteredMembers.map(member => ( + !member.pending_invitation && onChange?.(member.id || '')} + > + onChange?.(member.id || '')} + /> +
+ +
+ {member.name} + {/* {member.email} */} +
+ )) + ) : ( + + )} +
+
+ ); + + return ( + dropdownContent} + onOpenChange={open => { + if (open) setTimeout(() => membersInputRef.current?.focus(), 0); + }} + > +