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,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<IWorkLenzResponse> {
|
||||
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<void> {
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user