feat(project-finance): implement project finance API and frontend integration for task retrieval
This commit is contained in:
135
worklenz-backend/src/controllers/project-finance-controller.ts
Normal file
135
worklenz-backend/src/controllers/project-finance-controller.ts
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import { IWorkLenzRequest } from "../interfaces/worklenz-request";
|
||||||
|
import { IWorkLenzResponse } from "../interfaces/worklenz-response";
|
||||||
|
|
||||||
|
import db from "../config/db";
|
||||||
|
import { ServerResponse } from "../models/server-response";
|
||||||
|
import WorklenzControllerBase from "./worklenz-controller-base";
|
||||||
|
import HandleExceptions from "../decorators/handle-exceptions";
|
||||||
|
|
||||||
|
export default class ProjectfinanceController extends WorklenzControllerBase {
|
||||||
|
@HandleExceptions()
|
||||||
|
public static async getTasks(
|
||||||
|
req: IWorkLenzRequest,
|
||||||
|
res: IWorkLenzResponse
|
||||||
|
): Promise<IWorkLenzResponse> {
|
||||||
|
const { project_id } = req.params;
|
||||||
|
const { group_by = "status" } = req.query;
|
||||||
|
|
||||||
|
const q = `
|
||||||
|
WITH task_data AS (
|
||||||
|
SELECT
|
||||||
|
t.id,
|
||||||
|
t.name,
|
||||||
|
t.status_id,
|
||||||
|
t.priority_id,
|
||||||
|
tp.phase_id,
|
||||||
|
(t.total_minutes / 3600.0) as estimated_hours,
|
||||||
|
(COALESCE((SELECT SUM(time_spent) FROM task_work_log WHERE task_id = t.id), 0) / 3600.0) as actual_hours,
|
||||||
|
t.completed_at,
|
||||||
|
t.created_at,
|
||||||
|
t.updated_at,
|
||||||
|
t.billable,
|
||||||
|
s.name as status_name,
|
||||||
|
p.name as priority_name,
|
||||||
|
ph.name as phase_name,
|
||||||
|
(SELECT color_code FROM sys_task_status_categories WHERE id = s.category_id) as status_color,
|
||||||
|
(SELECT color_code_dark FROM sys_task_status_categories WHERE id = s.category_id) as status_color_dark,
|
||||||
|
(SELECT color_code FROM task_priorities WHERE id = t.priority_id) as priority_color,
|
||||||
|
(SELECT color_code FROM project_phases WHERE id = tp.phase_id) as phase_color,
|
||||||
|
(SELECT get_task_assignees(t.id)) as assignees,
|
||||||
|
json_agg(
|
||||||
|
json_build_object(
|
||||||
|
'name', u.name,
|
||||||
|
'avatar_url', u.avatar_url,
|
||||||
|
'team_member_id', tm.id,
|
||||||
|
'color_code', '#1890ff'
|
||||||
|
)
|
||||||
|
) FILTER (WHERE u.id IS NOT NULL) as members
|
||||||
|
FROM tasks t
|
||||||
|
LEFT JOIN task_statuses s ON t.status_id = s.id
|
||||||
|
LEFT JOIN task_priorities p ON t.priority_id = p.id
|
||||||
|
LEFT JOIN task_phase tp ON t.id = tp.task_id
|
||||||
|
LEFT JOIN project_phases ph ON tp.phase_id = ph.id
|
||||||
|
LEFT JOIN tasks_assignees ta ON t.id = ta.task_id
|
||||||
|
LEFT JOIN project_members pm ON ta.project_member_id = pm.id
|
||||||
|
LEFT JOIN team_members tm ON pm.team_member_id = tm.id
|
||||||
|
LEFT JOIN finance_project_rate_card_roles pmr ON pm.project_rate_card_role_id = pmr.id
|
||||||
|
LEFT JOIN users u ON tm.user_id = u.id
|
||||||
|
LEFT JOIN job_titles jt ON tm.job_title_id = jt.id
|
||||||
|
WHERE t.project_id = $1
|
||||||
|
GROUP BY
|
||||||
|
t.id,
|
||||||
|
s.name,
|
||||||
|
p.name,
|
||||||
|
ph.name,
|
||||||
|
tp.phase_id,
|
||||||
|
s.category_id,
|
||||||
|
t.priority_id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
CASE
|
||||||
|
WHEN $2 = 'status' THEN status_id
|
||||||
|
WHEN $2 = 'priority' THEN priority_id
|
||||||
|
WHEN $2 = 'phases' THEN phase_id
|
||||||
|
END as group_id,
|
||||||
|
CASE
|
||||||
|
WHEN $2 = 'status' THEN status_name
|
||||||
|
WHEN $2 = 'priority' THEN priority_name
|
||||||
|
WHEN $2 = 'phases' THEN phase_name
|
||||||
|
END as group_name,
|
||||||
|
CASE
|
||||||
|
WHEN $2 = 'status' THEN status_color
|
||||||
|
WHEN $2 = 'priority' THEN priority_color
|
||||||
|
WHEN $2 = 'phases' THEN phase_color
|
||||||
|
END as color_code,
|
||||||
|
CASE
|
||||||
|
WHEN $2 = 'status' THEN status_color_dark
|
||||||
|
WHEN $2 = 'priority' THEN priority_color
|
||||||
|
WHEN $2 = 'phases' THEN phase_color
|
||||||
|
END as color_code_dark,
|
||||||
|
json_agg(
|
||||||
|
json_build_object(
|
||||||
|
'id', id,
|
||||||
|
'name', name,
|
||||||
|
'status_id', status_id,
|
||||||
|
'priority_id', priority_id,
|
||||||
|
'phase_id', phase_id,
|
||||||
|
'estimated_hours', estimated_hours,
|
||||||
|
'actual_hours', actual_hours,
|
||||||
|
'completed_at', completed_at,
|
||||||
|
'created_at', created_at,
|
||||||
|
'updated_at', updated_at,
|
||||||
|
'billable', billable,
|
||||||
|
'assignees', assignees,
|
||||||
|
'members', members
|
||||||
|
)
|
||||||
|
) as tasks
|
||||||
|
FROM task_data
|
||||||
|
GROUP BY
|
||||||
|
CASE
|
||||||
|
WHEN $2 = 'status' THEN status_id
|
||||||
|
WHEN $2 = 'priority' THEN priority_id
|
||||||
|
WHEN $2 = 'phases' THEN phase_id
|
||||||
|
END,
|
||||||
|
CASE
|
||||||
|
WHEN $2 = 'status' THEN status_name
|
||||||
|
WHEN $2 = 'priority' THEN priority_name
|
||||||
|
WHEN $2 = 'phases' THEN phase_name
|
||||||
|
END,
|
||||||
|
CASE
|
||||||
|
WHEN $2 = 'status' THEN status_color
|
||||||
|
WHEN $2 = 'priority' THEN priority_color
|
||||||
|
WHEN $2 = 'phases' THEN phase_color
|
||||||
|
END,
|
||||||
|
CASE
|
||||||
|
WHEN $2 = 'status' THEN status_color_dark
|
||||||
|
WHEN $2 = 'priority' THEN priority_color
|
||||||
|
WHEN $2 = 'phases' THEN phase_color
|
||||||
|
END
|
||||||
|
ORDER BY group_name;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const result = await db.query(q, [project_id, group_by]);
|
||||||
|
return res.status(200).send(new ServerResponse(true, result.rows));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -60,6 +60,7 @@ import taskRecurringApiRouter from "./task-recurring-api-router";
|
|||||||
import customColumnsApiRouter from "./custom-columns-api-router";
|
import customColumnsApiRouter from "./custom-columns-api-router";
|
||||||
import ratecardApiRouter from "./ratecard-api-router";
|
import ratecardApiRouter from "./ratecard-api-router";
|
||||||
import projectRatecardApiRouter from "./project-ratecard-api-router";
|
import projectRatecardApiRouter from "./project-ratecard-api-router";
|
||||||
|
import projectFinanceApiRouter from "./project-finance-api-router";
|
||||||
|
|
||||||
const api = express.Router();
|
const api = express.Router();
|
||||||
|
|
||||||
@@ -122,4 +123,6 @@ api.use("/task-recurring", taskRecurringApiRouter);
|
|||||||
|
|
||||||
api.use("/custom-columns", customColumnsApiRouter);
|
api.use("/custom-columns", customColumnsApiRouter);
|
||||||
|
|
||||||
|
api.use("/project-finance", projectFinanceApiRouter);
|
||||||
|
|
||||||
export default api;
|
export default api;
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import express from "express";
|
||||||
|
|
||||||
|
import ProjectfinanceController from "../../controllers/project-finance-controller";
|
||||||
|
|
||||||
|
const projectFinanceApiRouter = express.Router();
|
||||||
|
|
||||||
|
projectFinanceApiRouter.get("/project/:project_id/tasks", ProjectfinanceController.getTasks);
|
||||||
|
|
||||||
|
export default projectFinanceApiRouter;
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { API_BASE_URL } from "@/shared/constants";
|
||||||
|
import { IServerResponse } from "@/types/common.types";
|
||||||
|
import apiClient from "../api-client";
|
||||||
|
import { IProjectFinanceGroup } from "@/types/project/project-finance.types";
|
||||||
|
|
||||||
|
const rootUrl = `${API_BASE_URL}/project-finance`;
|
||||||
|
|
||||||
|
export const projectFinanceApiService = {
|
||||||
|
getProjectTasks: async (
|
||||||
|
projectId: string,
|
||||||
|
groupBy: 'status' | 'priority' | 'phases' = 'status'
|
||||||
|
): Promise<IServerResponse<IProjectFinanceGroup[]>> => {
|
||||||
|
const response = await apiClient.get<IServerResponse<IProjectFinanceGroup[]>>(
|
||||||
|
`${rootUrl}/project/${projectId}/tasks`,
|
||||||
|
{
|
||||||
|
params: { group_by: groupBy }
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -1,42 +1,41 @@
|
|||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React from 'react';
|
||||||
import FinanceTableWrapper from './finance-table/finance-table-wrapper';
|
import FinanceTableWrapper from './finance-table/finance-table-wrapper';
|
||||||
import { fetchData } from '../../../../../utils/fetchData';
|
import { IProjectFinanceGroup } from '@/types/project/project-finance.types';
|
||||||
|
|
||||||
|
interface FinanceTabProps {
|
||||||
|
groupType: 'status' | 'priority' | 'phases';
|
||||||
|
taskGroups: IProjectFinanceGroup[];
|
||||||
|
loading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
const FinanceTab = ({
|
const FinanceTab = ({
|
||||||
groupType,
|
groupType,
|
||||||
}: {
|
taskGroups,
|
||||||
groupType: 'status' | 'priority' | 'phases';
|
loading
|
||||||
}) => {
|
}: FinanceTabProps) => {
|
||||||
// Save each table's list according to the groups
|
// Transform taskGroups into the format expected by FinanceTableWrapper
|
||||||
const [statusTables, setStatusTables] = useState<any[]>([]);
|
const activeTablesList = taskGroups.map(group => ({
|
||||||
const [priorityTables, setPriorityTables] = useState<any[]>([]);
|
id: group.group_id,
|
||||||
const [activeTablesList, setActiveTablesList] = useState<any[]>([]);
|
name: group.group_name,
|
||||||
|
color_code: group.color_code,
|
||||||
// Fetch data for status tables
|
color_code_dark: group.color_code_dark,
|
||||||
useMemo(() => {
|
tasks: group.tasks.map(task => ({
|
||||||
fetchData('/finance-mock-data/finance-task-status.json', setStatusTables);
|
taskId: task.id,
|
||||||
}, []);
|
task: task.name,
|
||||||
|
hours: task.estimated_hours || 0,
|
||||||
// Fetch data for priority tables
|
cost: 0, // TODO: Calculate based on rate and hours
|
||||||
useMemo(() => {
|
fixedCost: 0, // TODO: Add fixed cost field
|
||||||
fetchData(
|
totalBudget: 0, // TODO: Calculate total budget
|
||||||
'/finance-mock-data/finance-task-priority.json',
|
totalActual: task.actual_hours || 0,
|
||||||
setPriorityTables
|
variance: 0, // TODO: Calculate variance
|
||||||
);
|
members: task.members || [],
|
||||||
}, []);
|
isbBillable: task.billable
|
||||||
|
}))
|
||||||
// Update activeTablesList based on groupType and fetched data
|
}));
|
||||||
useEffect(() => {
|
|
||||||
if (groupType === 'status') {
|
|
||||||
setActiveTablesList(statusTables);
|
|
||||||
} else if (groupType === 'priority') {
|
|
||||||
setActiveTablesList(priorityTables);
|
|
||||||
}
|
|
||||||
}, [groupType, priorityTables, statusTables]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<FinanceTableWrapper activeTablesList={activeTablesList} />
|
<FinanceTableWrapper activeTablesList={activeTablesList} loading={loading} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Card, Col, Row, Spin } from "antd";
|
||||||
|
import { useThemeContext } from "../../../../../context/theme-context";
|
||||||
|
import { FinanceTable } from "./finance-table";
|
||||||
|
import { IFinanceTable } from "./finance-table.interface";
|
||||||
|
import { IProjectFinanceGroup } from "../../../../../types/project/project-finance.types";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
activeTablesList: IProjectFinanceGroup[];
|
||||||
|
loading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FinanceTableWrapper: React.FC<Props> = ({ activeTablesList, loading }) => {
|
||||||
|
const { isDarkMode } = useThemeContext();
|
||||||
|
|
||||||
|
const getTableColor = (table: IProjectFinanceGroup) => {
|
||||||
|
return isDarkMode ? table.color_code_dark : table.color_code;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="finance-table-wrapper">
|
||||||
|
<Row gutter={[16, 16]}>
|
||||||
|
{activeTablesList.map((table) => (
|
||||||
|
<Col key={table.group_id} xs={24} sm={24} md={24} lg={24} xl={24}>
|
||||||
|
<Card
|
||||||
|
className="finance-table-card"
|
||||||
|
style={{
|
||||||
|
borderTop: `3px solid ${getTableColor(table)}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="finance-table-header">
|
||||||
|
<h3>{table.group_name}</h3>
|
||||||
|
</div>
|
||||||
|
<FinanceTable
|
||||||
|
table={table as unknown as IFinanceTable}
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -9,28 +9,43 @@ import { financeTableColumns } from '@/lib/project/project-view-finance-table-co
|
|||||||
import FinanceTable from './finance-table';
|
import FinanceTable from './finance-table';
|
||||||
import FinanceDrawer from '@/features/finance/finance-drawer/finance-drawer';
|
import FinanceDrawer from '@/features/finance/finance-drawer/finance-drawer';
|
||||||
|
|
||||||
const FinanceTableWrapper = ({
|
interface FinanceTableWrapperProps {
|
||||||
|
activeTablesList: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
color_code: string;
|
||||||
|
color_code_dark: string;
|
||||||
|
tasks: {
|
||||||
|
taskId: string;
|
||||||
|
task: string;
|
||||||
|
hours: number;
|
||||||
|
cost: number;
|
||||||
|
fixedCost: number;
|
||||||
|
totalBudget: number;
|
||||||
|
totalActual: number;
|
||||||
|
variance: number;
|
||||||
|
members: any[];
|
||||||
|
isbBillable: boolean;
|
||||||
|
}[];
|
||||||
|
}[];
|
||||||
|
loading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FinanceTableWrapper: React.FC<FinanceTableWrapperProps> = ({
|
||||||
activeTablesList,
|
activeTablesList,
|
||||||
}: {
|
loading
|
||||||
activeTablesList: any;
|
|
||||||
}) => {
|
}) => {
|
||||||
const [isScrolling, setIsScrolling] = useState(false);
|
const [isScrolling, setIsScrolling] = useState(false);
|
||||||
|
|
||||||
//? this state for inside this state individualy in finance table only display the data of the last table's task when a task is clicked The selectedTask state does not synchronize across tables so thats why move the selectedTask state to a parent component
|
|
||||||
const [selectedTask, setSelectedTask] = useState(null);
|
const [selectedTask, setSelectedTask] = useState(null);
|
||||||
|
|
||||||
// localization
|
|
||||||
const { t } = useTranslation('project-view-finance');
|
const { t } = useTranslation('project-view-finance');
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
// function on task click
|
|
||||||
const onTaskClick = (task: any) => {
|
const onTaskClick = (task: any) => {
|
||||||
setSelectedTask(task);
|
setSelectedTask(task);
|
||||||
dispatch(toggleFinanceDrawer());
|
dispatch(toggleFinanceDrawer());
|
||||||
};
|
};
|
||||||
|
|
||||||
// trigger the table scrolling
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const tableContainer = document.querySelector('.tasklist-container');
|
const tableContainer = document.querySelector('.tasklist-container');
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
@@ -39,22 +54,15 @@ const FinanceTableWrapper = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// add the scroll event listener
|
|
||||||
tableContainer?.addEventListener('scroll', handleScroll);
|
tableContainer?.addEventListener('scroll', handleScroll);
|
||||||
|
|
||||||
// cleanup on unmount
|
|
||||||
return () => {
|
return () => {
|
||||||
tableContainer?.removeEventListener('scroll', handleScroll);
|
tableContainer?.removeEventListener('scroll', handleScroll);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// get theme data from theme reducer
|
|
||||||
const themeMode = useAppSelector((state) => state.themeReducer.mode);
|
const themeMode = useAppSelector((state) => state.themeReducer.mode);
|
||||||
|
|
||||||
// get tasklist and currently using currency from finance reducer
|
|
||||||
const { currency } = useAppSelector((state) => state.financeReducer);
|
const { currency } = useAppSelector((state) => state.financeReducer);
|
||||||
|
|
||||||
// totals of all the tasks
|
|
||||||
const totals = activeTablesList.reduce(
|
const totals = activeTablesList.reduce(
|
||||||
(
|
(
|
||||||
acc: {
|
acc: {
|
||||||
@@ -135,7 +143,6 @@ const FinanceTableWrapper = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// layout styles for table and the columns
|
|
||||||
const customColumnHeaderStyles = (key: string) =>
|
const customColumnHeaderStyles = (key: string) =>
|
||||||
`px-2 text-left ${key === 'selector' && 'sticky left-0 z-10'} ${key === 'task' && 'sticky left-[48px] z-10'} ${key === 'members' && `sticky left-[288px] z-10 ${isScrolling ? 'after:content after:absolute after:top-0 after:-right-1 after:-z-10 after:h-[68px] after:w-1.5 after:bg-transparent after:bg-gradient-to-r after:from-[rgba(0,0,0,0.12)] after:to-transparent' : ''}`} ${themeMode === 'dark' ? 'bg-[#1d1d1d] border-[#303030]' : 'bg-[#fafafa]'}`;
|
`px-2 text-left ${key === 'selector' && 'sticky left-0 z-10'} ${key === 'task' && 'sticky left-[48px] z-10'} ${key === 'members' && `sticky left-[288px] z-10 ${isScrolling ? 'after:content after:absolute after:top-0 after:-right-1 after:-z-10 after:h-[68px] after:w-1.5 after:bg-transparent after:bg-gradient-to-r after:from-[rgba(0,0,0,0.12)] after:to-transparent' : ''}`} ${themeMode === 'dark' ? 'bg-[#1d1d1d] border-[#303030]' : 'bg-[#fafafa]'}`;
|
||||||
|
|
||||||
@@ -233,7 +240,7 @@ const FinanceTableWrapper = ({
|
|||||||
)}
|
)}
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
{activeTablesList.map((table: any, index: number) => (
|
{activeTablesList.map((table, index) => (
|
||||||
<FinanceTable
|
<FinanceTable
|
||||||
key={index}
|
key={index}
|
||||||
table={table}
|
table={table}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
import { themeWiseColor } from '../../../../../../utils/themeWiseColor';
|
import { themeWiseColor } from '../../../../../../utils/themeWiseColor';
|
||||||
import { colors } from '../../../../../../styles/colors';
|
import { colors } from '../../../../../../styles/colors';
|
||||||
import { financeTableColumns } from '@/lib/project/project-view-finance-table-columns';
|
import { financeTableColumns } from '@/lib/project/project-view-finance-table-columns';
|
||||||
|
import Avatars from '@/components/avatars/avatars';
|
||||||
|
|
||||||
type FinanceTableProps = {
|
type FinanceTableProps = {
|
||||||
table: any;
|
table: any;
|
||||||
@@ -132,11 +133,7 @@ const FinanceTable = ({
|
|||||||
);
|
);
|
||||||
case 'members':
|
case 'members':
|
||||||
return (
|
return (
|
||||||
<Avatar.Group>
|
task?.assignees && <Avatars members={task.assignees} />
|
||||||
{task.members.map((member: any) => (
|
|
||||||
<CustomAvatar avatarName={member.name} size={26} />
|
|
||||||
))}
|
|
||||||
</Avatar.Group>
|
|
||||||
);
|
);
|
||||||
case 'hours':
|
case 'hours':
|
||||||
return <Typography.Text>{task.hours}</Typography.Text>;
|
return <Typography.Text>{task.hours}</Typography.Text>;
|
||||||
|
|||||||
@@ -1,15 +1,53 @@
|
|||||||
import { Flex } from 'antd';
|
import { Flex } from 'antd';
|
||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
import ProjectViewFinanceHeader from './project-view-finance-header/project-view-finance-header';
|
import ProjectViewFinanceHeader from './project-view-finance-header/project-view-finance-header';
|
||||||
import FinanceTab from './finance-tab/finance-tab';
|
import FinanceTab from './finance-tab/finance-tab';
|
||||||
import RatecardTab from './ratecard-tab/ratecard-tab';
|
import RatecardTab from './ratecard-tab/ratecard-tab';
|
||||||
|
import { projectFinanceApiService } from '@/api/project-finance-ratecard/project-finance.api.service';
|
||||||
|
import { IProjectFinanceGroup } from '@/types/project/project-finance.types';
|
||||||
|
|
||||||
type FinanceTabType = 'finance' | 'ratecard';
|
type FinanceTabType = 'finance' | 'ratecard';
|
||||||
type GroupTypes = 'status' | 'priority' | 'phases';
|
type GroupTypes = 'status' | 'priority' | 'phases';
|
||||||
|
|
||||||
|
interface TaskGroup {
|
||||||
|
group_id: string;
|
||||||
|
group_name: string;
|
||||||
|
tasks: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FinanceTabProps {
|
||||||
|
groupType: GroupTypes;
|
||||||
|
taskGroups: TaskGroup[];
|
||||||
|
loading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
const ProjectViewFinance = () => {
|
const ProjectViewFinance = () => {
|
||||||
|
const { projectId } = useParams<{ projectId: string }>();
|
||||||
const [activeTab, setActiveTab] = useState<FinanceTabType>('finance');
|
const [activeTab, setActiveTab] = useState<FinanceTabType>('finance');
|
||||||
const [activeGroup, setActiveGroup] = useState<GroupTypes>('status');
|
const [activeGroup, setActiveGroup] = useState<GroupTypes>('status');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [taskGroups, setTaskGroups] = useState<IProjectFinanceGroup[]>([]);
|
||||||
|
|
||||||
|
const fetchTasks = async () => {
|
||||||
|
if (!projectId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await projectFinanceApiService.getProjectTasks(projectId, activeGroup);
|
||||||
|
if (response.done) {
|
||||||
|
setTaskGroups(response.body);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching tasks:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchTasks();
|
||||||
|
}, [projectId, activeGroup]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex vertical gap={16} style={{ overflowX: 'hidden' }}>
|
<Flex vertical gap={16} style={{ overflowX: 'hidden' }}>
|
||||||
@@ -21,7 +59,11 @@ const ProjectViewFinance = () => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{activeTab === 'finance' ? (
|
{activeTab === 'finance' ? (
|
||||||
<FinanceTab groupType={activeGroup} />
|
<FinanceTab
|
||||||
|
groupType={activeGroup}
|
||||||
|
taskGroups={taskGroups}
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<RatecardTab />
|
<RatecardTab />
|
||||||
)}
|
)}
|
||||||
|
|||||||
45
worklenz-frontend/src/types/project/project-finance.types.ts
Normal file
45
worklenz-frontend/src/types/project/project-finance.types.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
export interface IProjectFinanceUser {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
avatar_url: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IProjectFinanceJobTitle {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IProjectFinanceMember {
|
||||||
|
id: string;
|
||||||
|
team_member_id: string;
|
||||||
|
job_title_id: string;
|
||||||
|
rate: number | null;
|
||||||
|
user: IProjectFinanceUser;
|
||||||
|
job_title: IProjectFinanceJobTitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IProjectFinanceTask {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
status_id: string;
|
||||||
|
priority_id: string;
|
||||||
|
phase_id: string;
|
||||||
|
estimated_hours: number;
|
||||||
|
actual_hours: number;
|
||||||
|
completed_at: string | null;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
billable: boolean;
|
||||||
|
assignees: any[]; // Using any[] since we don't have the assignee structure yet
|
||||||
|
members: IProjectFinanceMember[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IProjectFinanceGroup {
|
||||||
|
group_id: string;
|
||||||
|
group_name: string;
|
||||||
|
color_code: string;
|
||||||
|
color_code_dark: string;
|
||||||
|
tasks: IProjectFinanceTask[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ProjectFinanceGroupType = 'status' | 'priority' | 'phases';
|
||||||
Reference in New Issue
Block a user