feat(project-finance): implement project finance API and frontend integration for task retrieval
This commit is contained in:
@@ -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 { fetchData } from '../../../../../utils/fetchData';
|
||||
import { IProjectFinanceGroup } from '@/types/project/project-finance.types';
|
||||
|
||||
interface FinanceTabProps {
|
||||
groupType: 'status' | 'priority' | 'phases';
|
||||
taskGroups: IProjectFinanceGroup[];
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
const FinanceTab = ({
|
||||
groupType,
|
||||
}: {
|
||||
groupType: 'status' | 'priority' | 'phases';
|
||||
}) => {
|
||||
// Save each table's list according to the groups
|
||||
const [statusTables, setStatusTables] = useState<any[]>([]);
|
||||
const [priorityTables, setPriorityTables] = useState<any[]>([]);
|
||||
const [activeTablesList, setActiveTablesList] = useState<any[]>([]);
|
||||
|
||||
// Fetch data for status tables
|
||||
useMemo(() => {
|
||||
fetchData('/finance-mock-data/finance-task-status.json', setStatusTables);
|
||||
}, []);
|
||||
|
||||
// Fetch data for priority tables
|
||||
useMemo(() => {
|
||||
fetchData(
|
||||
'/finance-mock-data/finance-task-priority.json',
|
||||
setPriorityTables
|
||||
);
|
||||
}, []);
|
||||
|
||||
// Update activeTablesList based on groupType and fetched data
|
||||
useEffect(() => {
|
||||
if (groupType === 'status') {
|
||||
setActiveTablesList(statusTables);
|
||||
} else if (groupType === 'priority') {
|
||||
setActiveTablesList(priorityTables);
|
||||
}
|
||||
}, [groupType, priorityTables, statusTables]);
|
||||
taskGroups,
|
||||
loading
|
||||
}: FinanceTabProps) => {
|
||||
// Transform taskGroups into the format expected by FinanceTableWrapper
|
||||
const activeTablesList = taskGroups.map(group => ({
|
||||
id: group.group_id,
|
||||
name: group.group_name,
|
||||
color_code: group.color_code,
|
||||
color_code_dark: group.color_code_dark,
|
||||
tasks: group.tasks.map(task => ({
|
||||
taskId: task.id,
|
||||
task: task.name,
|
||||
hours: task.estimated_hours || 0,
|
||||
cost: 0, // TODO: Calculate based on rate and hours
|
||||
fixedCost: 0, // TODO: Add fixed cost field
|
||||
totalBudget: 0, // TODO: Calculate total budget
|
||||
totalActual: task.actual_hours || 0,
|
||||
variance: 0, // TODO: Calculate variance
|
||||
members: task.members || [],
|
||||
isbBillable: task.billable
|
||||
}))
|
||||
}));
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FinanceTableWrapper activeTablesList={activeTablesList} />
|
||||
<FinanceTableWrapper activeTablesList={activeTablesList} loading={loading} />
|
||||
</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 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: any;
|
||||
loading
|
||||
}) => {
|
||||
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);
|
||||
|
||||
// localization
|
||||
const { t } = useTranslation('project-view-finance');
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
// function on task click
|
||||
const onTaskClick = (task: any) => {
|
||||
setSelectedTask(task);
|
||||
dispatch(toggleFinanceDrawer());
|
||||
};
|
||||
|
||||
// trigger the table scrolling
|
||||
useEffect(() => {
|
||||
const tableContainer = document.querySelector('.tasklist-container');
|
||||
const handleScroll = () => {
|
||||
@@ -39,22 +54,15 @@ const FinanceTableWrapper = ({
|
||||
}
|
||||
};
|
||||
|
||||
// add the scroll event listener
|
||||
tableContainer?.addEventListener('scroll', handleScroll);
|
||||
|
||||
// cleanup on unmount
|
||||
return () => {
|
||||
tableContainer?.removeEventListener('scroll', handleScroll);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// get theme data from theme reducer
|
||||
const themeMode = useAppSelector((state) => state.themeReducer.mode);
|
||||
|
||||
// get tasklist and currently using currency from finance reducer
|
||||
const { currency } = useAppSelector((state) => state.financeReducer);
|
||||
|
||||
// totals of all the tasks
|
||||
const totals = activeTablesList.reduce(
|
||||
(
|
||||
acc: {
|
||||
@@ -135,7 +143,6 @@ const FinanceTableWrapper = ({
|
||||
}
|
||||
};
|
||||
|
||||
// layout styles for table and the columns
|
||||
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]'}`;
|
||||
|
||||
@@ -233,7 +240,7 @@ const FinanceTableWrapper = ({
|
||||
)}
|
||||
</tr>
|
||||
|
||||
{activeTablesList.map((table: any, index: number) => (
|
||||
{activeTablesList.map((table, index) => (
|
||||
<FinanceTable
|
||||
key={index}
|
||||
table={table}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import { themeWiseColor } from '../../../../../../utils/themeWiseColor';
|
||||
import { colors } from '../../../../../../styles/colors';
|
||||
import { financeTableColumns } from '@/lib/project/project-view-finance-table-columns';
|
||||
import Avatars from '@/components/avatars/avatars';
|
||||
|
||||
type FinanceTableProps = {
|
||||
table: any;
|
||||
@@ -132,11 +133,7 @@ const FinanceTable = ({
|
||||
);
|
||||
case 'members':
|
||||
return (
|
||||
<Avatar.Group>
|
||||
{task.members.map((member: any) => (
|
||||
<CustomAvatar avatarName={member.name} size={26} />
|
||||
))}
|
||||
</Avatar.Group>
|
||||
task?.assignees && <Avatars members={task.assignees} />
|
||||
);
|
||||
case 'hours':
|
||||
return <Typography.Text>{task.hours}</Typography.Text>;
|
||||
|
||||
@@ -1,15 +1,53 @@
|
||||
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 FinanceTab from './finance-tab/finance-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 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 { projectId } = useParams<{ projectId: string }>();
|
||||
const [activeTab, setActiveTab] = useState<FinanceTabType>('finance');
|
||||
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 (
|
||||
<Flex vertical gap={16} style={{ overflowX: 'hidden' }}>
|
||||
@@ -21,7 +59,11 @@ const ProjectViewFinance = () => {
|
||||
/>
|
||||
|
||||
{activeTab === 'finance' ? (
|
||||
<FinanceTab groupType={activeGroup} />
|
||||
<FinanceTab
|
||||
groupType={activeGroup}
|
||||
taskGroups={taskGroups}
|
||||
loading={loading}
|
||||
/>
|
||||
) : (
|
||||
<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