feat(finance): enhance project finance view with export functionality and UI improvements
- Implemented export functionality for finance data, allowing users to download project finance reports in Excel format. - Refactored project finance header to streamline UI components and improve user experience. - Removed deprecated FinanceTab and GroupByFilterDropdown components, consolidating functionality into the main finance view. - Updated the project finance view to utilize new components for better organization and interaction. - Enhanced group selection for finance data, allowing users to filter by status, priority, or phases.
This commit is contained in:
@@ -8,8 +8,7 @@ import HandleExceptions from "../decorators/handle-exceptions";
|
|||||||
import { TASK_STATUS_COLOR_ALPHA } from "../shared/constants";
|
import { TASK_STATUS_COLOR_ALPHA } from "../shared/constants";
|
||||||
import { getColor } from "../shared/utils";
|
import { getColor } from "../shared/utils";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
|
import Excel from "exceljs";
|
||||||
const Excel = require("exceljs");
|
|
||||||
|
|
||||||
// Utility function to format time in hours, minutes, seconds format
|
// Utility function to format time in hours, minutes, seconds format
|
||||||
const formatTimeToHMS = (totalSeconds: number): string => {
|
const formatTimeToHMS = (totalSeconds: number): string => {
|
||||||
@@ -24,7 +23,7 @@ const formatTimeToHMS = (totalSeconds: number): string => {
|
|||||||
if (minutes > 0) parts.push(`${minutes}m`);
|
if (minutes > 0) parts.push(`${minutes}m`);
|
||||||
if (seconds > 0 || parts.length === 0) parts.push(`${seconds}s`);
|
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
|
// Utility function to parse time string back to seconds for calculations
|
||||||
@@ -50,7 +49,7 @@ export default class ProjectfinanceController extends WorklenzControllerBase {
|
|||||||
res: IWorkLenzResponse
|
res: IWorkLenzResponse
|
||||||
): Promise<IWorkLenzResponse> {
|
): Promise<IWorkLenzResponse> {
|
||||||
const projectId = req.params.project_id;
|
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
|
// First, get the project rate cards for this project
|
||||||
const rateCardQuery = `
|
const rateCardQuery = `
|
||||||
@@ -220,10 +219,14 @@ export default class ProjectfinanceController extends WorklenzControllerBase {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
try {
|
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) {
|
if (memberRateResult.rows.length > 0) {
|
||||||
const memberRate = memberRateResult.rows[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.rate = memberRate.rate ? Number(memberRate.rate) : 0;
|
||||||
assignee.job_title_id = memberRate.job_title_id;
|
assignee.job_title_id = memberRate.job_title_id;
|
||||||
assignee.job_title_name = memberRate.job_title_name;
|
assignee.job_title_name = memberRate.job_title_name;
|
||||||
@@ -235,7 +238,10 @@ export default class ProjectfinanceController extends WorklenzControllerBase {
|
|||||||
assignee.job_title_name = null;
|
assignee.job_title_name = null;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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.project_rate_card_role_id = null;
|
||||||
assignee.rate = 0;
|
assignee.rate = 0;
|
||||||
assignee.job_title_id = null;
|
assignee.job_title_id = null;
|
||||||
@@ -246,7 +252,12 @@ export default class ProjectfinanceController extends WorklenzControllerBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get groups based on groupBy parameter
|
// 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") {
|
if (groupBy === "status") {
|
||||||
const q = `
|
const q = `
|
||||||
@@ -293,8 +304,8 @@ export default class ProjectfinanceController extends WorklenzControllerBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Group tasks by the selected criteria
|
// Group tasks by the selected criteria
|
||||||
const groupedTasks = groups.map(group => {
|
const groupedTasks = groups.map((group) => {
|
||||||
const groupTasks = tasks.filter(task => {
|
const groupTasks = tasks.filter((task) => {
|
||||||
if (groupBy === "status") return task.status_id === group.id;
|
if (groupBy === "status") return task.status_id === group.id;
|
||||||
if (groupBy === "priority") return task.priority_id === group.id;
|
if (groupBy === "priority") return task.priority_id === group.id;
|
||||||
if (groupBy === "phases") return task.phase_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,
|
group_name: group.group_name,
|
||||||
color_code: group.color_code,
|
color_code: group.color_code,
|
||||||
color_code_dark: group.color_code_dark,
|
color_code_dark: group.color_code_dark,
|
||||||
tasks: groupTasks.map(task => ({
|
tasks: groupTasks.map((task) => ({
|
||||||
id: task.id,
|
id: task.id,
|
||||||
name: task.name,
|
name: task.name,
|
||||||
estimated_seconds: Number(task.estimated_seconds) || 0,
|
estimated_seconds: Number(task.estimated_seconds) || 0,
|
||||||
estimated_hours: formatTimeToHMS(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_seconds:
|
||||||
total_time_logged: formatTimeToHMS(Number(task.total_time_logged_seconds) || 0),
|
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,
|
estimated_cost: Number(task.estimated_cost) || 0,
|
||||||
fixed_cost: Number(task.fixed_cost) || 0,
|
fixed_cost: Number(task.fixed_cost) || 0,
|
||||||
total_budget: Number(task.total_budget) || 0,
|
total_budget: Number(task.total_budget) || 0,
|
||||||
@@ -320,15 +334,15 @@ export default class ProjectfinanceController extends WorklenzControllerBase {
|
|||||||
variance: Number(task.variance) || 0,
|
variance: Number(task.variance) || 0,
|
||||||
members: task.assignees,
|
members: task.assignees,
|
||||||
billable: task.billable,
|
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
|
// Include project rate cards in the response for reference
|
||||||
const responseData = {
|
const responseData = {
|
||||||
groups: groupedTasks,
|
groups: groupedTasks,
|
||||||
project_rate_cards: projectRateCards
|
project_rate_cards: projectRateCards,
|
||||||
};
|
};
|
||||||
|
|
||||||
return res.status(200).send(new ServerResponse(true, responseData));
|
return res.status(200).send(new ServerResponse(true, responseData));
|
||||||
@@ -343,7 +357,9 @@ export default class ProjectfinanceController extends WorklenzControllerBase {
|
|||||||
const { fixed_cost } = req.body;
|
const { fixed_cost } = req.body;
|
||||||
|
|
||||||
if (typeof fixed_cost !== "number" || fixed_cost < 0) {
|
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 = `
|
const q = `
|
||||||
@@ -356,7 +372,9 @@ export default class ProjectfinanceController extends WorklenzControllerBase {
|
|||||||
const result = await db.query(q, [fixed_cost, taskId]);
|
const result = await db.query(q, [fixed_cost, taskId]);
|
||||||
|
|
||||||
if (result.rows.length === 0) {
|
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]));
|
return res.status(200).send(new ServerResponse(true, result.rows[0]));
|
||||||
@@ -387,7 +405,9 @@ export default class ProjectfinanceController extends WorklenzControllerBase {
|
|||||||
const taskResult = await db.query(taskQuery, [taskId]);
|
const taskResult = await db.query(taskQuery, [taskId]);
|
||||||
|
|
||||||
if (taskResult.rows.length === 0) {
|
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;
|
const [task] = taskResult.rows;
|
||||||
@@ -414,7 +434,10 @@ export default class ProjectfinanceController extends WorklenzControllerBase {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
try {
|
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) {
|
if (memberResult.rows.length > 0) {
|
||||||
const [member] = memberResult.rows;
|
const [member] = memberResult.rows;
|
||||||
|
|
||||||
@@ -427,8 +450,13 @@ export default class ProjectfinanceController extends WorklenzControllerBase {
|
|||||||
WHERE twl.task_id = $1 AND tm.id = $2
|
WHERE twl.task_id = $1 AND tm.id = $2
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const timeLogResult = await db.query(timeLogQuery, [taskId, member.team_member_id]);
|
const timeLogResult = await db.query(timeLogQuery, [
|
||||||
const loggedHours = Number(timeLogResult.rows[0]?.logged_hours || 0);
|
taskId,
|
||||||
|
member.team_member_id,
|
||||||
|
]);
|
||||||
|
const loggedHours = Number(
|
||||||
|
timeLogResult.rows[0]?.logged_hours || 0
|
||||||
|
);
|
||||||
|
|
||||||
membersWithRates.push({
|
membersWithRates.push({
|
||||||
team_member_id: member.team_member_id,
|
team_member_id: member.team_member_id,
|
||||||
@@ -436,10 +464,16 @@ export default class ProjectfinanceController extends WorklenzControllerBase {
|
|||||||
avatar_url: member.avatar_url,
|
avatar_url: member.avatar_url,
|
||||||
hourly_rate: Number(member.hourly_rate || 0),
|
hourly_rate: Number(member.hourly_rate || 0),
|
||||||
job_title_name: member.job_title_name || "Unassigned",
|
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,
|
logged_hours: loggedHours,
|
||||||
estimated_cost: (task.assignees.length > 0 ? Number(task.estimated_hours) / task.assignees.length : 0) * Number(member.hourly_rate || 0),
|
estimated_cost:
|
||||||
actual_cost: loggedHours * Number(member.hourly_rate || 0)
|
(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) {
|
} catch (error) {
|
||||||
@@ -459,7 +493,7 @@ export default class ProjectfinanceController extends WorklenzControllerBase {
|
|||||||
logged_hours: 0,
|
logged_hours: 0,
|
||||||
estimated_cost: 0,
|
estimated_cost: 0,
|
||||||
actual_cost: 0,
|
actual_cost: 0,
|
||||||
members: []
|
members: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -475,7 +509,7 @@ export default class ProjectfinanceController extends WorklenzControllerBase {
|
|||||||
estimated_hours: member.estimated_hours,
|
estimated_hours: member.estimated_hours,
|
||||||
logged_hours: member.logged_hours,
|
logged_hours: member.logged_hours,
|
||||||
estimated_cost: member.estimated_cost,
|
estimated_cost: member.estimated_cost,
|
||||||
actual_cost: member.actual_cost
|
actual_cost: member.actual_cost,
|
||||||
});
|
});
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
@@ -485,11 +519,23 @@ export default class ProjectfinanceController extends WorklenzControllerBase {
|
|||||||
const taskTotals = {
|
const taskTotals = {
|
||||||
estimated_hours: Number(task.estimated_hours || 0),
|
estimated_hours: Number(task.estimated_hours || 0),
|
||||||
logged_hours: Number(task.total_time_logged || 0),
|
logged_hours: Number(task.total_time_logged || 0),
|
||||||
estimated_labor_cost: membersWithRates.reduce((sum, member) => sum + member.estimated_cost, 0),
|
estimated_labor_cost: membersWithRates.reduce(
|
||||||
actual_labor_cost: membersWithRates.reduce((sum, member) => sum + member.actual_cost, 0),
|
(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),
|
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_estimated_cost:
|
||||||
total_actual_cost: membersWithRates.reduce((sum, member) => sum + member.actual_cost, 0) + Number(task.fixed_cost || 0)
|
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 = {
|
const responseData = {
|
||||||
@@ -498,10 +544,10 @@ export default class ProjectfinanceController extends WorklenzControllerBase {
|
|||||||
name: task.name,
|
name: task.name,
|
||||||
project_id: task.project_id,
|
project_id: task.project_id,
|
||||||
billable: task.billable,
|
billable: task.billable,
|
||||||
...taskTotals
|
...taskTotals,
|
||||||
},
|
},
|
||||||
grouped_members: Object.values(groupedMembers),
|
grouped_members: Object.values(groupedMembers),
|
||||||
members: membersWithRates
|
members: membersWithRates,
|
||||||
};
|
};
|
||||||
|
|
||||||
return res.status(200).send(new ServerResponse(true, responseData));
|
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;
|
const parentTaskId = req.params.parent_task_id;
|
||||||
|
|
||||||
if (!parentTaskId) {
|
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
|
// Get subtasks with their financial data
|
||||||
@@ -596,10 +644,14 @@ export default class ProjectfinanceController extends WorklenzControllerBase {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
try {
|
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) {
|
if (memberRateResult.rows.length > 0) {
|
||||||
const memberRate = memberRateResult.rows[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.rate = memberRate.rate ? Number(memberRate.rate) : 0;
|
||||||
assignee.job_title_id = memberRate.job_title_id;
|
assignee.job_title_id = memberRate.job_title_id;
|
||||||
assignee.job_title_name = memberRate.job_title_name;
|
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
|
// Format the response to match the expected structure
|
||||||
const formattedTasks = tasks.map(task => ({
|
const formattedTasks = tasks.map((task) => ({
|
||||||
id: task.id,
|
id: task.id,
|
||||||
name: task.name,
|
name: task.name,
|
||||||
estimated_seconds: Number(task.estimated_seconds) || 0,
|
estimated_seconds: Number(task.estimated_seconds) || 0,
|
||||||
estimated_hours: formatTimeToHMS(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_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,
|
estimated_cost: Number(task.estimated_cost) || 0,
|
||||||
fixed_cost: Number(task.fixed_cost) || 0,
|
fixed_cost: Number(task.fixed_cost) || 0,
|
||||||
total_budget: Number(task.total_budget) || 0,
|
total_budget: Number(task.total_budget) || 0,
|
||||||
@@ -635,7 +689,7 @@ export default class ProjectfinanceController extends WorklenzControllerBase {
|
|||||||
variance: Number(task.variance) || 0,
|
variance: Number(task.variance) || 0,
|
||||||
members: task.assignees,
|
members: task.assignees,
|
||||||
billable: task.billable,
|
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));
|
return res.status(200).send(new ServerResponse(true, formattedTasks));
|
||||||
@@ -647,12 +701,12 @@ export default class ProjectfinanceController extends WorklenzControllerBase {
|
|||||||
res: IWorkLenzResponse
|
res: IWorkLenzResponse
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const projectId = req.params.project_id;
|
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
|
// Get project name for filename
|
||||||
const projectNameQuery = `SELECT name FROM projects WHERE id = $1`;
|
const projectNameQuery = `SELECT name FROM projects WHERE id = $1`;
|
||||||
const projectNameResult = await db.query(projectNameQuery, [projectId]);
|
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
|
// First, get the project rate cards for this project
|
||||||
const rateCardQuery = `
|
const rateCardQuery = `
|
||||||
@@ -822,10 +876,14 @@ export default class ProjectfinanceController extends WorklenzControllerBase {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
try {
|
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) {
|
if (memberRateResult.rows.length > 0) {
|
||||||
const memberRate = memberRateResult.rows[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.rate = memberRate.rate ? Number(memberRate.rate) : 0;
|
||||||
assignee.job_title_id = memberRate.job_title_id;
|
assignee.job_title_id = memberRate.job_title_id;
|
||||||
assignee.job_title_name = memberRate.job_title_name;
|
assignee.job_title_name = memberRate.job_title_name;
|
||||||
@@ -837,7 +895,10 @@ export default class ProjectfinanceController extends WorklenzControllerBase {
|
|||||||
assignee.job_title_name = null;
|
assignee.job_title_name = null;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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.project_rate_card_role_id = null;
|
||||||
assignee.rate = 0;
|
assignee.rate = 0;
|
||||||
assignee.job_title_id = null;
|
assignee.job_title_id = null;
|
||||||
@@ -848,7 +909,12 @@ export default class ProjectfinanceController extends WorklenzControllerBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get groups based on groupBy parameter
|
// 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") {
|
if (groupBy === "status") {
|
||||||
const q = `
|
const q = `
|
||||||
@@ -895,8 +961,8 @@ export default class ProjectfinanceController extends WorklenzControllerBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Group tasks by the selected criteria
|
// Group tasks by the selected criteria
|
||||||
const groupedTasks = groups.map(group => {
|
const groupedTasks = groups.map((group) => {
|
||||||
const groupTasks = tasks.filter(task => {
|
const groupTasks = tasks.filter((task) => {
|
||||||
if (groupBy === "status") return task.status_id === group.id;
|
if (groupBy === "status") return task.status_id === group.id;
|
||||||
if (groupBy === "priority") return task.priority_id === group.id;
|
if (groupBy === "priority") return task.priority_id === group.id;
|
||||||
if (groupBy === "phases") return task.phase_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,
|
group_name: group.group_name,
|
||||||
color_code: group.color_code,
|
color_code: group.color_code,
|
||||||
color_code_dark: group.color_code_dark,
|
color_code_dark: group.color_code_dark,
|
||||||
tasks: groupTasks.map(task => ({
|
tasks: groupTasks.map((task) => ({
|
||||||
id: task.id,
|
id: task.id,
|
||||||
name: task.name,
|
name: task.name,
|
||||||
estimated_seconds: Number(task.estimated_seconds) || 0,
|
estimated_seconds: Number(task.estimated_seconds) || 0,
|
||||||
estimated_hours: formatTimeToHMS(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_seconds:
|
||||||
total_time_logged: formatTimeToHMS(Number(task.total_time_logged_seconds) || 0),
|
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,
|
estimated_cost: Number(task.estimated_cost) || 0,
|
||||||
fixed_cost: Number(task.fixed_cost) || 0,
|
fixed_cost: Number(task.fixed_cost) || 0,
|
||||||
total_budget: Number(task.total_budget) || 0,
|
total_budget: Number(task.total_budget) || 0,
|
||||||
@@ -922,15 +991,15 @@ export default class ProjectfinanceController extends WorklenzControllerBase {
|
|||||||
variance: Number(task.variance) || 0,
|
variance: Number(task.variance) || 0,
|
||||||
members: task.assignees,
|
members: task.assignees,
|
||||||
billable: task.billable,
|
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
|
// Include project rate cards in the response for reference
|
||||||
const responseData = {
|
const responseData = {
|
||||||
groups: groupedTasks,
|
groups: groupedTasks,
|
||||||
project_rate_cards: projectRateCards
|
project_rate_cards: projectRateCards,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create Excel workbook and worksheet
|
// Create Excel workbook and worksheet
|
||||||
@@ -950,21 +1019,38 @@ export default class ProjectfinanceController extends WorklenzControllerBase {
|
|||||||
{ header: "Variance", key: "variance", width: 15 },
|
{ header: "Variance", key: "variance", width: 15 },
|
||||||
{ header: "Members", key: "members", width: 30 },
|
{ header: "Members", key: "members", width: 30 },
|
||||||
{ header: "Billable", key: "billable", width: 10 },
|
{ 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
|
// 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.mergeCells("A1:L1");
|
||||||
worksheet.getCell("A1").alignment = { horizontal: "center" };
|
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 };
|
worksheet.getCell("A1").font = { size: 16, bold: true };
|
||||||
|
|
||||||
// Add headers on row 3
|
// Add headers on row 3
|
||||||
worksheet.getRow(3).values = [
|
worksheet.getRow(3).values = [
|
||||||
"Task Name", "Group", "Estimated Hours", "Total Time Logged",
|
"Task Name",
|
||||||
"Estimated Cost", "Fixed Cost", "Total Budget", "Total Actual",
|
"Group",
|
||||||
"Variance", "Members", "Billable", "Sub Tasks Count"
|
"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 };
|
worksheet.getRow(3).font = { bold: true };
|
||||||
|
|
||||||
@@ -984,7 +1070,7 @@ export default class ProjectfinanceController extends WorklenzControllerBase {
|
|||||||
variance: task.variance.toFixed(2),
|
variance: task.variance.toFixed(2),
|
||||||
members: task.members.map((m: any) => m.name).join(", "),
|
members: task.members.map((m: any) => m.name).join(", "),
|
||||||
billable: task.billable ? "Yes" : "No",
|
billable: task.billable ? "Yes" : "No",
|
||||||
sub_tasks_count: task.sub_tasks_count
|
sub_tasks_count: task.sub_tasks_count,
|
||||||
});
|
});
|
||||||
currentRow++;
|
currentRow++;
|
||||||
}
|
}
|
||||||
@@ -994,13 +1080,18 @@ export default class ProjectfinanceController extends WorklenzControllerBase {
|
|||||||
const buffer = await workbook.xlsx.writeBuffer();
|
const buffer = await workbook.xlsx.writeBuffer();
|
||||||
|
|
||||||
// Create filename with project name, date and time
|
// Create filename with project name, date and time
|
||||||
const sanitizedProjectName = projectName.replace(/[^a-zA-Z0-9\s]/g, '').replace(/\s+/g, '_');
|
const sanitizedProjectName = projectName
|
||||||
const dateTime = moment().format('YYYY-MM-DD_HH-mm-ss');
|
.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`;
|
const filename = `${sanitizedProjectName}_Finance_Data_${dateTime}.xlsx`;
|
||||||
|
|
||||||
// Set the response headers for the Excel file
|
// Set the response headers for the Excel file
|
||||||
res.setHeader("Content-Disposition", `attachment; filename=${filename}`);
|
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
|
// Send the Excel file as a response
|
||||||
res.end(buffer);
|
res.end(buffer);
|
||||||
|
|||||||
@@ -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 (
|
|
||||||
<div>
|
|
||||||
<FinanceTableWrapper activeTablesList={taskGroups} loading={loading} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FinanceTab;
|
|
||||||
@@ -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 (
|
|
||||||
<Flex align="center" gap={4} style={{ marginInlineStart: 12 }}>
|
|
||||||
{t('groupByText')}:
|
|
||||||
<Select
|
|
||||||
defaultValue={'status'}
|
|
||||||
options={groupDropdownMenuItems}
|
|
||||||
onChange={handleChange}
|
|
||||||
suffixIcon={<CaretDownFilled />}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default GroupByFilterDropdown;
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
import { Button, ConfigProvider, Flex, Select, Typography, message } from 'antd';
|
|
||||||
import GroupByFilterDropdown from './group-by-filter-dropdown';
|
|
||||||
import { DownOutlined } from '@ant-design/icons';
|
|
||||||
import { useAppDispatch } from '../../../../../hooks/useAppDispatch';
|
|
||||||
import { useAppSelector } from '../../../../../hooks/useAppSelector';
|
|
||||||
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { changeCurrency, toggleImportRatecardsDrawer } from '@/features/finance/finance-slice';
|
|
||||||
import { projectFinanceApiService } from '@/api/project-finance-ratecard/project-finance.api.service';
|
|
||||||
import { useParams } from 'react-router-dom';
|
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
type ProjectViewFinanceHeaderProps = {
|
|
||||||
activeTab: 'finance' | 'ratecard';
|
|
||||||
setActiveTab: (tab: 'finance' | 'ratecard') => void;
|
|
||||||
activeGroup: 'status' | 'priority' | 'phases';
|
|
||||||
setActiveGroup: (group: 'status' | 'priority' | 'phases') => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ProjectViewFinanceHeader = ({
|
|
||||||
activeTab,
|
|
||||||
setActiveTab,
|
|
||||||
activeGroup,
|
|
||||||
setActiveGroup,
|
|
||||||
}: ProjectViewFinanceHeaderProps) => {
|
|
||||||
// localization
|
|
||||||
const { t } = useTranslation('project-view-finance');
|
|
||||||
const { projectId } = useParams<{ projectId: string }>();
|
|
||||||
const [exporting, setExporting] = useState(false);
|
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const { project } = useAppSelector(state => state.projectReducer);
|
|
||||||
|
|
||||||
const handleExport = async () => {
|
|
||||||
if (!projectId) {
|
|
||||||
message.error('Project ID not found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
setExporting(true);
|
|
||||||
const blob = await projectFinanceApiService.exportFinanceData(projectId, activeGroup);
|
|
||||||
|
|
||||||
// Get project name from Redux state
|
|
||||||
const projectName = project?.name || 'Unknown_Project';
|
|
||||||
|
|
||||||
// Create filename with project name, date and time
|
|
||||||
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`;
|
|
||||||
|
|
||||||
// Create download link
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ConfigProvider wave={{ disabled: true }}>
|
|
||||||
<Flex gap={16} align="center" justify="space-between">
|
|
||||||
<Flex gap={16} align="center">
|
|
||||||
<Flex>
|
|
||||||
<Button
|
|
||||||
className={`${activeTab === 'finance' && 'border-[#1890ff] text-[#1890ff]'} rounded-r-none`}
|
|
||||||
onClick={() => setActiveTab('finance')}
|
|
||||||
>
|
|
||||||
{t('financeText')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
className={`${activeTab === 'ratecard' && 'border-[#1890ff] text-[#1890ff]'} rounded-l-none`}
|
|
||||||
onClick={() => setActiveTab('ratecard')}
|
|
||||||
>
|
|
||||||
{t('ratecardSingularText')}
|
|
||||||
</Button>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
{activeTab === 'finance' && (
|
|
||||||
<GroupByFilterDropdown
|
|
||||||
activeGroup={activeGroup}
|
|
||||||
setActiveGroup={setActiveGroup}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
{activeTab === 'finance' ? (
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
icon={<DownOutlined />}
|
|
||||||
iconPosition="end"
|
|
||||||
loading={exporting}
|
|
||||||
onClick={handleExport}
|
|
||||||
>
|
|
||||||
{t('exportButton')}
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Flex gap={8} align="center">
|
|
||||||
<Flex gap={8} align="center">
|
|
||||||
<Typography.Text>{t('currencyText')}</Typography.Text>
|
|
||||||
<Select
|
|
||||||
defaultValue={'lkr'}
|
|
||||||
options={[
|
|
||||||
{ value: 'lkr', label: 'LKR' },
|
|
||||||
{ value: 'usd', label: 'USD' },
|
|
||||||
{ value: 'inr', label: 'INR' },
|
|
||||||
]}
|
|
||||||
onChange={(value) => dispatch(changeCurrency(value))}
|
|
||||||
/>
|
|
||||||
</Flex>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
onClick={() => dispatch(toggleImportRatecardsDrawer())}
|
|
||||||
>
|
|
||||||
{t('importButton')}
|
|
||||||
</Button>
|
|
||||||
</Flex>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
</ConfigProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ProjectViewFinanceHeader;
|
|
||||||
@@ -1,20 +1,27 @@
|
|||||||
import { Flex } from 'antd';
|
import { Button, ConfigProvider, Flex, Select, Typography, message } from 'antd';
|
||||||
import { useEffect } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { CaretDownFilled, DownOutlined } from '@ant-design/icons';
|
||||||
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
import { useAppDispatch } from '@/hooks/useAppDispatch';
|
||||||
import { useAppSelector } from '@/hooks/useAppSelector';
|
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 { 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 { 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 ProjectViewFinance = () => {
|
||||||
const { projectId } = useParams<{ projectId: string }>();
|
const { projectId } = useParams<{ projectId: string }>();
|
||||||
const dispatch = useAppDispatch();
|
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 { 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(() => {
|
useEffect(() => {
|
||||||
if (projectId) {
|
if (projectId) {
|
||||||
@@ -22,23 +29,136 @@ const ProjectViewFinance = () => {
|
|||||||
}
|
}
|
||||||
}, [projectId, activeGroup, dispatch, refreshTimestamp]);
|
}, [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 (
|
return (
|
||||||
<Flex vertical gap={16} style={{ overflowX: 'hidden' }}>
|
<Flex vertical gap={16} style={{ overflowX: 'hidden' }}>
|
||||||
<ProjectViewFinanceHeader
|
{/* Finance Header */}
|
||||||
activeTab={activeTab}
|
<ConfigProvider wave={{ disabled: true }}>
|
||||||
setActiveTab={(tab) => dispatch(setActiveTab(tab))}
|
<Flex gap={16} align="center" justify="space-between">
|
||||||
activeGroup={activeGroup}
|
<Flex gap={16} align="center">
|
||||||
setActiveGroup={(group) => dispatch(setActiveGroup(group))}
|
<Flex>
|
||||||
|
<Button
|
||||||
|
className={`${activeTab === 'finance' && 'border-[#1890ff] text-[#1890ff]'} rounded-r-none`}
|
||||||
|
onClick={() => dispatch(setActiveTab('finance'))}
|
||||||
|
>
|
||||||
|
{t('financeText')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className={`${activeTab === 'ratecard' && 'border-[#1890ff] text-[#1890ff]'} rounded-l-none`}
|
||||||
|
onClick={() => dispatch(setActiveTab('ratecard'))}
|
||||||
|
>
|
||||||
|
{t('ratecardSingularText')}
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{activeTab === 'finance' && (
|
||||||
|
<Flex align="center" gap={4} style={{ marginInlineStart: 12 }}>
|
||||||
|
{t('groupByText')}:
|
||||||
|
<Select
|
||||||
|
value={activeGroup}
|
||||||
|
options={groupDropdownMenuItems}
|
||||||
|
onChange={(value) => dispatch(setActiveGroup(value as 'status' | 'priority' | 'phases'))}
|
||||||
|
suffixIcon={<CaretDownFilled />}
|
||||||
/>
|
/>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
|
||||||
{activeTab === 'finance' ? (
|
{activeTab === 'finance' ? (
|
||||||
<FinanceTab
|
<Button
|
||||||
groupType={activeGroup}
|
type="primary"
|
||||||
taskGroups={taskGroups}
|
icon={<DownOutlined />}
|
||||||
loading={loading}
|
iconPosition="end"
|
||||||
/>
|
loading={exporting}
|
||||||
|
onClick={handleExport}
|
||||||
|
>
|
||||||
|
{t('exportButton')}
|
||||||
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<RatecardTab />
|
<Flex gap={8} align="center">
|
||||||
|
<Flex gap={8} align="center">
|
||||||
|
<Typography.Text>{t('currencyText')}</Typography.Text>
|
||||||
|
<Select
|
||||||
|
defaultValue={'lkr'}
|
||||||
|
options={[
|
||||||
|
{ value: 'lkr', label: 'LKR' },
|
||||||
|
{ value: 'usd', label: 'USD' },
|
||||||
|
{ value: 'inr', label: 'INR' },
|
||||||
|
]}
|
||||||
|
onChange={(value) => dispatch(changeCurrency(value))}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={() => dispatch(toggleImportRatecardsDrawer())}
|
||||||
|
>
|
||||||
|
{t('importButton')}
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</ConfigProvider>
|
||||||
|
|
||||||
|
{/* Tab Content */}
|
||||||
|
{activeTab === 'finance' ? (
|
||||||
|
<div>
|
||||||
|
<FinanceTableWrapper activeTablesList={taskGroups} loading={loading} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Flex vertical gap={8}>
|
||||||
|
<RatecardTable />
|
||||||
|
<Typography.Text
|
||||||
|
type="danger"
|
||||||
|
style={{ display: 'block', marginTop: '10px' }}
|
||||||
|
>
|
||||||
|
{t('ratecardImportantNotice')}
|
||||||
|
</Typography.Text>
|
||||||
|
<ImportRatecardsDrawer />
|
||||||
|
</Flex>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 (
|
|
||||||
<Flex vertical gap={8}>
|
|
||||||
<RatecardTable />
|
|
||||||
|
|
||||||
<Typography.Text
|
|
||||||
type="danger"
|
|
||||||
style={{ display: 'block', marginTop: '10px' }}
|
|
||||||
>
|
|
||||||
{t('ratecardImportantNotice')}
|
|
||||||
</Typography.Text>
|
|
||||||
{/* <Button
|
|
||||||
type="primary"
|
|
||||||
style={{
|
|
||||||
marginTop: '10px',
|
|
||||||
width: 'fit-content',
|
|
||||||
alignSelf: 'flex-end',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('saveButton')}
|
|
||||||
</Button> */}
|
|
||||||
|
|
||||||
{/* import ratecards drawer */}
|
|
||||||
<ImportRatecardsDrawer />
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RatecardTab;
|
|
||||||
Reference in New Issue
Block a user