diff --git a/worklenz-backend/src/controllers/project-finance-controller.ts b/worklenz-backend/src/controllers/project-finance-controller.ts index 01d72960..cb82c432 100644 --- a/worklenz-backend/src/controllers/project-finance-controller.ts +++ b/worklenz-backend/src/controllers/project-finance-controller.ts @@ -8,38 +8,37 @@ import HandleExceptions from "../decorators/handle-exceptions"; import { TASK_STATUS_COLOR_ALPHA } from "../shared/constants"; import { getColor } from "../shared/utils"; import moment from "moment"; - -const Excel = require("exceljs"); +import Excel from "exceljs"; // Utility function to format time in hours, minutes, seconds format const formatTimeToHMS = (totalSeconds: number): string => { if (!totalSeconds || totalSeconds === 0) return "0s"; - + const hours = Math.floor(totalSeconds / 3600); const minutes = Math.floor((totalSeconds % 3600) / 60); const seconds = totalSeconds % 60; - + const parts = []; if (hours > 0) parts.push(`${hours}h`); if (minutes > 0) parts.push(`${minutes}m`); if (seconds > 0 || parts.length === 0) parts.push(`${seconds}s`); - - return parts.join(' '); + + return parts.join(" "); }; // Utility function to parse time string back to seconds for calculations const parseTimeToSeconds = (timeString: string): number => { if (!timeString || timeString === "0s") return 0; - + let totalSeconds = 0; const hourMatch = timeString.match(/(\d+)h/); const minuteMatch = timeString.match(/(\d+)m/); const secondMatch = timeString.match(/(\d+)s/); - + if (hourMatch) totalSeconds += parseInt(hourMatch[1]) * 3600; if (minuteMatch) totalSeconds += parseInt(minuteMatch[1]) * 60; if (secondMatch) totalSeconds += parseInt(secondMatch[1]); - + return totalSeconds; }; @@ -50,7 +49,7 @@ export default class ProjectfinanceController extends WorklenzControllerBase { res: IWorkLenzResponse ): Promise { const projectId = req.params.project_id; - const groupBy = req.query.group || "status"; + const groupBy = req.query.group_by || "status"; // First, get the project rate cards for this project const rateCardQuery = ` @@ -65,7 +64,7 @@ export default class ProjectfinanceController extends WorklenzControllerBase { WHERE fprr.project_id = $1 ORDER BY jt.name; `; - + const rateCardResult = await db.query(rateCardQuery, [projectId]); const projectRateCards = rateCardResult.rows; @@ -205,7 +204,7 @@ export default class ProjectfinanceController extends WorklenzControllerBase { if (Array.isArray(task.assignees)) { for (const assignee of task.assignees) { assignee.color_code = getColor(assignee.name); - + // Get the rate for this assignee using project_members.project_rate_card_role_id const memberRateQuery = ` SELECT @@ -218,12 +217,16 @@ export default class ProjectfinanceController extends WorklenzControllerBase { LEFT JOIN job_titles jt ON fprr.job_title_id = jt.id WHERE pm.team_member_id = $1 AND pm.project_id = $2 `; - + try { - const memberRateResult = await db.query(memberRateQuery, [assignee.team_member_id, projectId]); + const memberRateResult = await db.query(memberRateQuery, [ + assignee.team_member_id, + projectId, + ]); if (memberRateResult.rows.length > 0) { const memberRate = memberRateResult.rows[0]; - assignee.project_rate_card_role_id = memberRate.project_rate_card_role_id; + assignee.project_rate_card_role_id = + memberRate.project_rate_card_role_id; assignee.rate = memberRate.rate ? Number(memberRate.rate) : 0; assignee.job_title_id = memberRate.job_title_id; assignee.job_title_name = memberRate.job_title_name; @@ -235,7 +238,10 @@ export default class ProjectfinanceController extends WorklenzControllerBase { assignee.job_title_name = null; } } catch (error) { - console.error("Error fetching member rate from project_members:", error); + console.error( + "Error fetching member rate from project_members:", + error + ); assignee.project_rate_card_role_id = null; assignee.rate = 0; assignee.job_title_id = null; @@ -246,8 +252,13 @@ export default class ProjectfinanceController extends WorklenzControllerBase { } // Get groups based on groupBy parameter - let groups: Array<{ id: string; group_name: string; color_code: string; color_code_dark: string }> = []; - + let groups: Array<{ + id: string; + group_name: string; + color_code: string; + color_code_dark: string; + }> = []; + if (groupBy === "status") { const q = ` SELECT @@ -284,7 +295,7 @@ export default class ProjectfinanceController extends WorklenzControllerBase { ORDER BY sort_index; `; groups = (await db.query(q, [projectId])).rows; - + // Add TASK_STATUS_COLOR_ALPHA to color codes for (const group of groups) { group.color_code = group.color_code + TASK_STATUS_COLOR_ALPHA; @@ -293,8 +304,8 @@ export default class ProjectfinanceController extends WorklenzControllerBase { } // Group tasks by the selected criteria - const groupedTasks = groups.map(group => { - const groupTasks = tasks.filter(task => { + const groupedTasks = groups.map((group) => { + const groupTasks = tasks.filter((task) => { if (groupBy === "status") return task.status_id === group.id; if (groupBy === "priority") return task.priority_id === group.id; if (groupBy === "phases") return task.phase_id === group.id; @@ -306,13 +317,16 @@ export default class ProjectfinanceController extends WorklenzControllerBase { group_name: group.group_name, color_code: group.color_code, color_code_dark: group.color_code_dark, - tasks: groupTasks.map(task => ({ + tasks: groupTasks.map((task) => ({ id: task.id, name: task.name, estimated_seconds: Number(task.estimated_seconds) || 0, estimated_hours: formatTimeToHMS(Number(task.estimated_seconds) || 0), - total_time_logged_seconds: Number(task.total_time_logged_seconds) || 0, - total_time_logged: formatTimeToHMS(Number(task.total_time_logged_seconds) || 0), + total_time_logged_seconds: + Number(task.total_time_logged_seconds) || 0, + total_time_logged: formatTimeToHMS( + Number(task.total_time_logged_seconds) || 0 + ), estimated_cost: Number(task.estimated_cost) || 0, fixed_cost: Number(task.fixed_cost) || 0, total_budget: Number(task.total_budget) || 0, @@ -320,15 +334,15 @@ export default class ProjectfinanceController extends WorklenzControllerBase { variance: Number(task.variance) || 0, members: task.assignees, billable: task.billable, - sub_tasks_count: Number(task.sub_tasks_count) || 0 - })) + sub_tasks_count: Number(task.sub_tasks_count) || 0, + })), }; }); // Include project rate cards in the response for reference const responseData = { groups: groupedTasks, - project_rate_cards: projectRateCards + project_rate_cards: projectRateCards, }; return res.status(200).send(new ServerResponse(true, responseData)); @@ -343,7 +357,9 @@ export default class ProjectfinanceController extends WorklenzControllerBase { const { fixed_cost } = req.body; if (typeof fixed_cost !== "number" || fixed_cost < 0) { - return res.status(400).send(new ServerResponse(false, null, "Invalid fixed cost value")); + return res + .status(400) + .send(new ServerResponse(false, null, "Invalid fixed cost value")); } const q = ` @@ -354,9 +370,11 @@ export default class ProjectfinanceController extends WorklenzControllerBase { `; const result = await db.query(q, [fixed_cost, taskId]); - + if (result.rows.length === 0) { - return res.status(404).send(new ServerResponse(false, null, "Task not found")); + return res + .status(404) + .send(new ServerResponse(false, null, "Task not found")); } return res.status(200).send(new ServerResponse(true, result.rows[0])); @@ -385,9 +403,11 @@ export default class ProjectfinanceController extends WorklenzControllerBase { `; const taskResult = await db.query(taskQuery, [taskId]); - + if (taskResult.rows.length === 0) { - return res.status(404).send(new ServerResponse(false, null, "Task not found")); + return res + .status(404) + .send(new ServerResponse(false, null, "Task not found")); } const [task] = taskResult.rows; @@ -412,12 +432,15 @@ export default class ProjectfinanceController extends WorklenzControllerBase { LEFT JOIN job_titles jt ON fprr.job_title_id = jt.id WHERE tm.id = $2 `; - + try { - const memberResult = await db.query(memberRateQuery, [task.project_id, assignee.team_member_id]); + const memberResult = await db.query(memberRateQuery, [ + task.project_id, + assignee.team_member_id, + ]); if (memberResult.rows.length > 0) { const [member] = memberResult.rows; - + // Get actual time logged by this member for this task const timeLogQuery = ` SELECT COALESCE(SUM(time_spent), 0) / 3600.0 as logged_hours @@ -426,20 +449,31 @@ export default class ProjectfinanceController extends WorklenzControllerBase { LEFT JOIN team_members tm ON u.id = tm.user_id WHERE twl.task_id = $1 AND tm.id = $2 `; - - const timeLogResult = await db.query(timeLogQuery, [taskId, member.team_member_id]); - const loggedHours = Number(timeLogResult.rows[0]?.logged_hours || 0); - - membersWithRates.push({ + + const timeLogResult = await db.query(timeLogQuery, [ + taskId, + member.team_member_id, + ]); + const loggedHours = Number( + timeLogResult.rows[0]?.logged_hours || 0 + ); + + membersWithRates.push({ team_member_id: member.team_member_id, name: member.name || "Unknown User", avatar_url: member.avatar_url, hourly_rate: Number(member.hourly_rate || 0), job_title_name: member.job_title_name || "Unassigned", - estimated_hours: task.assignees.length > 0 ? Number(task.estimated_hours) / task.assignees.length : 0, + estimated_hours: + task.assignees.length > 0 + ? Number(task.estimated_hours) / task.assignees.length + : 0, logged_hours: loggedHours, - estimated_cost: (task.assignees.length > 0 ? Number(task.estimated_hours) / task.assignees.length : 0) * Number(member.hourly_rate || 0), - actual_cost: loggedHours * Number(member.hourly_rate || 0) + estimated_cost: + (task.assignees.length > 0 + ? Number(task.estimated_hours) / task.assignees.length + : 0) * Number(member.hourly_rate || 0), + actual_cost: loggedHours * Number(member.hourly_rate || 0), }); } } catch (error) { @@ -450,17 +484,17 @@ export default class ProjectfinanceController extends WorklenzControllerBase { // Group members by job title and calculate totals const groupedMembers = membersWithRates.reduce((acc: any, member: any) => { - const jobRole = member.job_title_name || "Unassigned"; - + const jobRole = member.job_title_name || "Unassigned"; + if (!acc[jobRole]) { - acc[jobRole] = { - jobRole, - estimated_hours: 0, - logged_hours: 0, - estimated_cost: 0, - actual_cost: 0, - members: [] - }; + acc[jobRole] = { + jobRole, + estimated_hours: 0, + logged_hours: 0, + estimated_cost: 0, + actual_cost: 0, + members: [], + }; } acc[jobRole].estimated_hours += member.estimated_hours; @@ -475,7 +509,7 @@ export default class ProjectfinanceController extends WorklenzControllerBase { estimated_hours: member.estimated_hours, logged_hours: member.logged_hours, estimated_cost: member.estimated_cost, - actual_cost: member.actual_cost + actual_cost: member.actual_cost, }); return acc; @@ -485,11 +519,23 @@ export default class ProjectfinanceController extends WorklenzControllerBase { const taskTotals = { estimated_hours: Number(task.estimated_hours || 0), logged_hours: Number(task.total_time_logged || 0), - estimated_labor_cost: membersWithRates.reduce((sum, member) => sum + member.estimated_cost, 0), - actual_labor_cost: membersWithRates.reduce((sum, member) => sum + member.actual_cost, 0), + estimated_labor_cost: membersWithRates.reduce( + (sum, member) => sum + member.estimated_cost, + 0 + ), + actual_labor_cost: membersWithRates.reduce( + (sum, member) => sum + member.actual_cost, + 0 + ), fixed_cost: Number(task.fixed_cost || 0), - total_estimated_cost: membersWithRates.reduce((sum, member) => sum + member.estimated_cost, 0) + Number(task.fixed_cost || 0), - total_actual_cost: membersWithRates.reduce((sum, member) => sum + member.actual_cost, 0) + Number(task.fixed_cost || 0) + total_estimated_cost: + membersWithRates.reduce( + (sum, member) => sum + member.estimated_cost, + 0 + ) + Number(task.fixed_cost || 0), + total_actual_cost: + membersWithRates.reduce((sum, member) => sum + member.actual_cost, 0) + + Number(task.fixed_cost || 0), }; const responseData = { @@ -498,10 +544,10 @@ export default class ProjectfinanceController extends WorklenzControllerBase { name: task.name, project_id: task.project_id, billable: task.billable, - ...taskTotals + ...taskTotals, }, grouped_members: Object.values(groupedMembers), - members: membersWithRates + members: membersWithRates, }; return res.status(200).send(new ServerResponse(true, responseData)); @@ -516,7 +562,9 @@ export default class ProjectfinanceController extends WorklenzControllerBase { const parentTaskId = req.params.parent_task_id; if (!parentTaskId) { - return res.status(400).send(new ServerResponse(false, null, "Parent task ID is required")); + return res + .status(400) + .send(new ServerResponse(false, null, "Parent task ID is required")); } // Get subtasks with their financial data @@ -581,7 +629,7 @@ export default class ProjectfinanceController extends WorklenzControllerBase { if (Array.isArray(task.assignees)) { for (const assignee of task.assignees) { assignee.color_code = getColor(assignee.name); - + // Get the rate for this assignee const memberRateQuery = ` SELECT @@ -594,12 +642,16 @@ export default class ProjectfinanceController extends WorklenzControllerBase { LEFT JOIN job_titles jt ON fprr.job_title_id = jt.id WHERE pm.team_member_id = $1 AND pm.project_id = $2 `; - + try { - const memberRateResult = await db.query(memberRateQuery, [assignee.team_member_id, projectId]); + const memberRateResult = await db.query(memberRateQuery, [ + assignee.team_member_id, + projectId, + ]); if (memberRateResult.rows.length > 0) { const memberRate = memberRateResult.rows[0]; - assignee.project_rate_card_role_id = memberRate.project_rate_card_role_id; + assignee.project_rate_card_role_id = + memberRate.project_rate_card_role_id; assignee.rate = memberRate.rate ? Number(memberRate.rate) : 0; assignee.job_title_id = memberRate.job_title_id; assignee.job_title_name = memberRate.job_title_name; @@ -621,13 +673,15 @@ export default class ProjectfinanceController extends WorklenzControllerBase { } // Format the response to match the expected structure - const formattedTasks = tasks.map(task => ({ + const formattedTasks = tasks.map((task) => ({ id: task.id, name: task.name, estimated_seconds: Number(task.estimated_seconds) || 0, estimated_hours: formatTimeToHMS(Number(task.estimated_seconds) || 0), total_time_logged_seconds: Number(task.total_time_logged_seconds) || 0, - total_time_logged: formatTimeToHMS(Number(task.total_time_logged_seconds) || 0), + total_time_logged: formatTimeToHMS( + Number(task.total_time_logged_seconds) || 0 + ), estimated_cost: Number(task.estimated_cost) || 0, fixed_cost: Number(task.fixed_cost) || 0, total_budget: Number(task.total_budget) || 0, @@ -635,7 +689,7 @@ export default class ProjectfinanceController extends WorklenzControllerBase { variance: Number(task.variance) || 0, members: task.assignees, billable: task.billable, - sub_tasks_count: Number(task.sub_tasks_count) || 0 + sub_tasks_count: Number(task.sub_tasks_count) || 0, })); return res.status(200).send(new ServerResponse(true, formattedTasks)); @@ -647,12 +701,12 @@ export default class ProjectfinanceController extends WorklenzControllerBase { res: IWorkLenzResponse ): Promise { const projectId = req.params.project_id; - const groupBy = (req.query.groupBy as string) || 'status'; + const groupBy = (req.query.groupBy as string) || "status"; // Get project name for filename const projectNameQuery = `SELECT name FROM projects WHERE id = $1`; const projectNameResult = await db.query(projectNameQuery, [projectId]); - const projectName = projectNameResult.rows[0]?.name || 'Unknown Project'; + const projectName = projectNameResult.rows[0]?.name || "Unknown Project"; // First, get the project rate cards for this project const rateCardQuery = ` @@ -667,7 +721,7 @@ export default class ProjectfinanceController extends WorklenzControllerBase { WHERE fprr.project_id = $1 ORDER BY jt.name; `; - + const rateCardResult = await db.query(rateCardQuery, [projectId]); const projectRateCards = rateCardResult.rows; @@ -807,7 +861,7 @@ export default class ProjectfinanceController extends WorklenzControllerBase { if (Array.isArray(task.assignees)) { for (const assignee of task.assignees) { assignee.color_code = getColor(assignee.name); - + // Get the rate for this assignee using project_members.project_rate_card_role_id const memberRateQuery = ` SELECT @@ -820,12 +874,16 @@ export default class ProjectfinanceController extends WorklenzControllerBase { LEFT JOIN job_titles jt ON fprr.job_title_id = jt.id WHERE pm.team_member_id = $1 AND pm.project_id = $2 `; - + try { - const memberRateResult = await db.query(memberRateQuery, [assignee.team_member_id, projectId]); + const memberRateResult = await db.query(memberRateQuery, [ + assignee.team_member_id, + projectId, + ]); if (memberRateResult.rows.length > 0) { const memberRate = memberRateResult.rows[0]; - assignee.project_rate_card_role_id = memberRate.project_rate_card_role_id; + assignee.project_rate_card_role_id = + memberRate.project_rate_card_role_id; assignee.rate = memberRate.rate ? Number(memberRate.rate) : 0; assignee.job_title_id = memberRate.job_title_id; assignee.job_title_name = memberRate.job_title_name; @@ -837,7 +895,10 @@ export default class ProjectfinanceController extends WorklenzControllerBase { assignee.job_title_name = null; } } catch (error) { - console.error("Error fetching member rate from project_members:", error); + console.error( + "Error fetching member rate from project_members:", + error + ); assignee.project_rate_card_role_id = null; assignee.rate = 0; assignee.job_title_id = null; @@ -848,8 +909,13 @@ export default class ProjectfinanceController extends WorklenzControllerBase { } // Get groups based on groupBy parameter - let groups: Array<{ id: string; group_name: string; color_code: string; color_code_dark: string }> = []; - + let groups: Array<{ + id: string; + group_name: string; + color_code: string; + color_code_dark: string; + }> = []; + if (groupBy === "status") { const q = ` SELECT @@ -886,7 +952,7 @@ export default class ProjectfinanceController extends WorklenzControllerBase { ORDER BY sort_index; `; groups = (await db.query(q, [projectId])).rows; - + // Add TASK_STATUS_COLOR_ALPHA to color codes for (const group of groups) { group.color_code = group.color_code + TASK_STATUS_COLOR_ALPHA; @@ -895,8 +961,8 @@ export default class ProjectfinanceController extends WorklenzControllerBase { } // Group tasks by the selected criteria - const groupedTasks = groups.map(group => { - const groupTasks = tasks.filter(task => { + const groupedTasks = groups.map((group) => { + const groupTasks = tasks.filter((task) => { if (groupBy === "status") return task.status_id === group.id; if (groupBy === "priority") return task.priority_id === group.id; if (groupBy === "phases") return task.phase_id === group.id; @@ -908,13 +974,16 @@ export default class ProjectfinanceController extends WorklenzControllerBase { group_name: group.group_name, color_code: group.color_code, color_code_dark: group.color_code_dark, - tasks: groupTasks.map(task => ({ + tasks: groupTasks.map((task) => ({ id: task.id, name: task.name, estimated_seconds: Number(task.estimated_seconds) || 0, estimated_hours: formatTimeToHMS(Number(task.estimated_seconds) || 0), - total_time_logged_seconds: Number(task.total_time_logged_seconds) || 0, - total_time_logged: formatTimeToHMS(Number(task.total_time_logged_seconds) || 0), + total_time_logged_seconds: + Number(task.total_time_logged_seconds) || 0, + total_time_logged: formatTimeToHMS( + Number(task.total_time_logged_seconds) || 0 + ), estimated_cost: Number(task.estimated_cost) || 0, fixed_cost: Number(task.fixed_cost) || 0, total_budget: Number(task.total_budget) || 0, @@ -922,15 +991,15 @@ export default class ProjectfinanceController extends WorklenzControllerBase { variance: Number(task.variance) || 0, members: task.assignees, billable: task.billable, - sub_tasks_count: Number(task.sub_tasks_count) || 0 - })) + sub_tasks_count: Number(task.sub_tasks_count) || 0, + })), }; }); // Include project rate cards in the response for reference const responseData = { groups: groupedTasks, - project_rate_cards: projectRateCards + project_rate_cards: projectRateCards, }; // Create Excel workbook and worksheet @@ -950,21 +1019,38 @@ export default class ProjectfinanceController extends WorklenzControllerBase { { header: "Variance", key: "variance", width: 15 }, { header: "Members", key: "members", width: 30 }, { header: "Billable", key: "billable", width: 10 }, - { header: "Sub Tasks Count", key: "sub_tasks_count", width: 15 } + { header: "Sub Tasks Count", key: "sub_tasks_count", width: 15 }, ]; // Add title row - worksheet.getCell("A1").value = `Finance Data Export - ${projectName} - ${moment().format("MMM DD, YYYY")}`; + worksheet.getCell( + "A1" + ).value = `Finance Data Export - ${projectName} - ${moment().format( + "MMM DD, YYYY" + )}`; worksheet.mergeCells("A1:L1"); worksheet.getCell("A1").alignment = { horizontal: "center" }; - worksheet.getCell("A1").style.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "D9D9D9" } }; + worksheet.getCell("A1").style.fill = { + type: "pattern", + pattern: "solid", + fgColor: { argb: "D9D9D9" }, + }; worksheet.getCell("A1").font = { size: 16, bold: true }; // Add headers on row 3 worksheet.getRow(3).values = [ - "Task Name", "Group", "Estimated Hours", "Total Time Logged", - "Estimated Cost", "Fixed Cost", "Total Budget", "Total Actual", - "Variance", "Members", "Billable", "Sub Tasks Count" + "Task Name", + "Group", + "Estimated Hours", + "Total Time Logged", + "Estimated Cost", + "Fixed Cost", + "Total Budget", + "Total Actual", + "Variance", + "Members", + "Billable", + "Sub Tasks Count", ]; worksheet.getRow(3).font = { bold: true }; @@ -984,7 +1070,7 @@ export default class ProjectfinanceController extends WorklenzControllerBase { variance: task.variance.toFixed(2), members: task.members.map((m: any) => m.name).join(", "), billable: task.billable ? "Yes" : "No", - sub_tasks_count: task.sub_tasks_count + sub_tasks_count: task.sub_tasks_count, }); currentRow++; } @@ -994,13 +1080,18 @@ export default class ProjectfinanceController extends WorklenzControllerBase { const buffer = await workbook.xlsx.writeBuffer(); // Create filename with project name, date and time - const sanitizedProjectName = projectName.replace(/[^a-zA-Z0-9\s]/g, '').replace(/\s+/g, '_'); - const dateTime = moment().format('YYYY-MM-DD_HH-mm-ss'); + const sanitizedProjectName = projectName + .replace(/[^a-zA-Z0-9\s]/g, "") + .replace(/\s+/g, "_"); + const dateTime = moment().format("YYYY-MM-DD_HH-mm-ss"); const filename = `${sanitizedProjectName}_Finance_Data_${dateTime}.xlsx`; // Set the response headers for the Excel file res.setHeader("Content-Disposition", `attachment; filename=${filename}`); - res.setHeader("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + res.setHeader( + "Content-Type", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + ); // Send the Excel file as a response res.end(buffer); diff --git a/worklenz-frontend/src/pages/projects/projectView/finance/finance-tab/finance-tab.tsx b/worklenz-frontend/src/pages/projects/projectView/finance/finance-tab/finance-tab.tsx deleted file mode 100644 index d57a50aa..00000000 --- a/worklenz-frontend/src/pages/projects/projectView/finance/finance-tab/finance-tab.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import FinanceTableWrapper from './finance-table/finance-table-wrapper'; -import { IProjectFinanceGroup } from '@/types/project/project-finance.types'; - -interface FinanceTabProps { - groupType: 'status' | 'priority' | 'phases'; - taskGroups: IProjectFinanceGroup[]; - loading: boolean; -} - -const FinanceTab = ({ - groupType, - taskGroups = [], - loading -}: FinanceTabProps) => { - - return ( -
- -
- ); -}; - -export default FinanceTab; diff --git a/worklenz-frontend/src/pages/projects/projectView/finance/project-view-finance-header/group-by-filter-dropdown.tsx b/worklenz-frontend/src/pages/projects/projectView/finance/project-view-finance-header/group-by-filter-dropdown.tsx deleted file mode 100644 index fad9365d..00000000 --- a/worklenz-frontend/src/pages/projects/projectView/finance/project-view-finance-header/group-by-filter-dropdown.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { CaretDownFilled } from '@ant-design/icons'; -import { Flex, Select } from 'antd'; -import React from 'react'; -import { useSelectedProject } from '../../../../../hooks/useSelectedProject'; -import { useAppSelector } from '../../../../../hooks/useAppSelector'; -import { useTranslation } from 'react-i18next'; - -type GroupByFilterDropdownProps = { - activeGroup: 'status' | 'priority' | 'phases'; - setActiveGroup: (group: 'status' | 'priority' | 'phases') => void; -}; - -const GroupByFilterDropdown = ({ - activeGroup, - setActiveGroup, -}: GroupByFilterDropdownProps) => { - // localization - const { t } = useTranslation('project-view-finance'); - - const handleChange = (value: string) => { - setActiveGroup(value as 'status' | 'priority' | 'phases'); - }; - - // get selected project from useSelectedPro - const selectedProject = useSelectedProject(); - - //get phases details from phases slice - const phase = - useAppSelector((state) => state.phaseReducer.phaseList).find( - (phase) => phase?.projectId === selectedProject?.projectId - ) || null; - - const groupDropdownMenuItems = [ - { key: 'status', value: 'status', label: t('statusText') }, - { key: 'priority', value: 'priority', label: t('priorityText') }, - { - key: 'phase', - value: 'phase', - label: phase ? phase?.phase : t('phaseText'), - }, - ]; - - return ( - - {t('groupByText')}: - dispatch(changeCurrency(value))} - /> - - - - )} - - - ); -}; - -export default ProjectViewFinanceHeader; diff --git a/worklenz-frontend/src/pages/projects/projectView/finance/project-view-finance.tsx b/worklenz-frontend/src/pages/projects/projectView/finance/project-view-finance.tsx index 5d2fb08f..ad296f7d 100644 --- a/worklenz-frontend/src/pages/projects/projectView/finance/project-view-finance.tsx +++ b/worklenz-frontend/src/pages/projects/projectView/finance/project-view-finance.tsx @@ -1,20 +1,27 @@ -import { Flex } from 'antd'; -import { useEffect } from 'react'; +import { Button, ConfigProvider, Flex, Select, Typography, message } from 'antd'; +import { useEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { CaretDownFilled, DownOutlined } from '@ant-design/icons'; import { useAppDispatch } from '@/hooks/useAppDispatch'; import { useAppSelector } from '@/hooks/useAppSelector'; -import ProjectViewFinanceHeader from './project-view-finance-header/project-view-finance-header'; -import FinanceTab from './finance-tab/finance-tab'; -import RatecardTab from './ratecard-tab/ratecard-tab'; import { fetchProjectFinances, setActiveTab, setActiveGroup } from '@/features/projects/finance/project-finance.slice'; +import { changeCurrency, toggleImportRatecardsDrawer } from '@/features/finance/finance-slice'; +import { projectFinanceApiService } from '@/api/project-finance-ratecard/project-finance.api.service'; import { RootState } from '@/app/store'; +import FinanceTableWrapper from './finance-tab/finance-table/finance-table-wrapper'; +import RatecardTable from './ratecard-tab/reatecard-table/ratecard-table'; +import ImportRatecardsDrawer from '@/features/finance/ratecard-drawer/import-ratecards-drawer'; const ProjectViewFinance = () => { const { projectId } = useParams<{ projectId: string }>(); const dispatch = useAppDispatch(); + const { t } = useTranslation('project-view-finance'); + const [exporting, setExporting] = useState(false); const { activeTab, activeGroup, loading, taskGroups } = useAppSelector((state: RootState) => state.projectFinances); - const { refreshTimestamp } = useAppSelector((state: RootState) => state.projectReducer); + const { refreshTimestamp, project } = useAppSelector((state: RootState) => state.projectReducer); + const phaseList = useAppSelector((state) => state.phaseReducer.phaseList); useEffect(() => { if (projectId) { @@ -22,23 +29,136 @@ const ProjectViewFinance = () => { } }, [projectId, activeGroup, dispatch, refreshTimestamp]); + const handleExport = async () => { + if (!projectId) { + message.error('Project ID not found'); + return; + } + + try { + setExporting(true); + const blob = await projectFinanceApiService.exportFinanceData(projectId, activeGroup); + + const projectName = project?.name || 'Unknown_Project'; + const sanitizedProjectName = projectName.replace(/[^a-zA-Z0-9\s]/g, '').replace(/\s+/g, '_'); + const dateTime = new Date().toISOString().replace(/[:.]/g, '-').split('T'); + const date = dateTime[0]; + const time = dateTime[1].split('.')[0]; + const filename = `${sanitizedProjectName}_Finance_Data_${date}_${time}.xlsx`; + + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + + message.success('Finance data exported successfully'); + } catch (error) { + console.error('Export failed:', error); + message.error('Failed to export finance data'); + } finally { + setExporting(false); + } + }; + + const groupDropdownMenuItems = [ + { key: 'status', value: 'status', label: t('statusText') }, + { key: 'priority', value: 'priority', label: t('priorityText') }, + { + key: 'phases', + value: 'phases', + label: phaseList.length > 0 ? project?.phase_label || t('phaseText') : t('phaseText'), + }, + ]; + return ( - dispatch(setActiveTab(tab))} - activeGroup={activeGroup} - setActiveGroup={(group) => dispatch(setActiveGroup(group))} - /> + {/* Finance Header */} + + + + + + + + {activeTab === 'finance' && ( + + {t('groupByText')}: + dispatch(changeCurrency(value))} + /> + + + + )} + + + + {/* Tab Content */} {activeTab === 'finance' ? ( - +
+ +
) : ( - + + + + {t('ratecardImportantNotice')} + + + )}
); diff --git a/worklenz-frontend/src/pages/projects/projectView/finance/ratecard-tab/ratecard-tab.tsx b/worklenz-frontend/src/pages/projects/projectView/finance/ratecard-tab/ratecard-tab.tsx deleted file mode 100644 index 4ec0ee07..00000000 --- a/worklenz-frontend/src/pages/projects/projectView/finance/ratecard-tab/ratecard-tab.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; -import RatecardTable from './reatecard-table/ratecard-table'; -import { Button, Flex, Typography } from 'antd'; -import { useTranslation } from 'react-i18next'; -import ImportRatecardsDrawer from '@/features/finance/ratecard-drawer/import-ratecards-drawer'; - -const RatecardTab = () => { - // localization - const { t } = useTranslation('project-view-finance'); - - return ( - - - - - {t('ratecardImportantNotice')} - - {/* */} - - {/* import ratecards drawer */} - - - ); -}; - -export default RatecardTab;