diff --git a/src/app/api/cashflow/index.ts b/src/app/api/cashflow/index.ts index 4de997c..b597a5c 100644 --- a/src/app/api/cashflow/index.ts +++ b/src/app/api/cashflow/index.ts @@ -56,6 +56,14 @@ export interface CashFlowAnticipatedChartResult { AverageManhours: number; teamLead: number; totalManhour: number; + aniticipateExpenditure: number; +} +export interface CashFlowLedgerResult { + date: string; + income: number; + expenditure: number; + balance: number; + remarks: string; } export const preloadProjects = () => { @@ -98,3 +106,14 @@ export const fetchProjectsCashFlowAnticipate = cache(async (projectIdList: numbe } }); + +export const fetchProjectsCashFlowLedger = cache(async (projectIdList: number[]) => { + if (projectIdList.length !== 0) { + const queryParams = new URLSearchParams(); + queryParams.append('projectIdList', projectIdList.join(',')); + return serverFetchJson(`${BASE_API_URL}/dashboard/searchCashFlowLedger?${queryParams.toString()}`); + } else { + return []; + } + +}); diff --git a/src/app/api/teamCashflow/index.ts b/src/app/api/teamCashflow/index.ts new file mode 100644 index 0000000..79e6421 --- /dev/null +++ b/src/app/api/teamCashflow/index.ts @@ -0,0 +1,36 @@ +"use server"; +import { cache } from "react"; +import { serverFetchJson } from "@/app/utils/fetchUtil"; +import { BASE_API_URL } from "@/config/api"; + +export interface comboProp { + id: any; + label: string; +} + +export interface teamCombo { + records: comboProp[]; +} + +export interface teamCashFlow { + monthInvoice: string; + invoiceMonth: string; + income: number; + cumulativeIncome: number; + monthExpenditure: string; + recordMonth: string; + expenditure: number; + cumulativeExpenditure: number; +} + +export const fetchTeamCombo = cache(async () => { + return serverFetchJson(`${BASE_API_URL}/team/combo`); +}); + +export const fetchTeamCashFlowChartData = cache(async (year:number,teamId?:number,) => { + if (teamId === null) { + return serverFetchJson(`${BASE_API_URL}/dashboard/searchTeamCashFlow?year=${year}`); + } else { + return serverFetchJson(`${BASE_API_URL}/dashboard/searchTeamCashFlow?teamId=${teamId}&year=${year}`); + } +}); diff --git a/src/components/Breadcrumb/Breadcrumb.tsx b/src/components/Breadcrumb/Breadcrumb.tsx index 77a4eda..b69b582 100644 --- a/src/components/Breadcrumb/Breadcrumb.tsx +++ b/src/components/Breadcrumb/Breadcrumb.tsx @@ -14,6 +14,13 @@ const pathToLabelMap: { [path: string]: string } = { "": "Overview", "/home": "User Workspace", "/dashboard": "Dashboard", + "/dashboard/CompanyTeamCashFlow": "Company Team Cash Flow", + "/dashboard/ProjectCashFlow": "Project Cash Flow", + "/dashboard/ProjectFinancialSummary": "Project Finanical Summary", + "/dashboard/ProjectResourceSummary": "Project Resource Summary", + "/dashboard/ProjectStatusByClient": "Project Status by Client", + "/dashboard/ProjectStatusByTeam": "Project Status by Team", + "/dashboard/StaffUtilization": "Staff Utilization", "/projects": "Projects", "/projects/create": "Create Project", "/projects/createSub": "Sub Project", diff --git a/src/components/CompanyTeamCashFlow/CompanyTeamCashFlow.tsx b/src/components/CompanyTeamCashFlow/CompanyTeamCashFlow.tsx index aeeeaf2..6ae4150 100644 --- a/src/components/CompanyTeamCashFlow/CompanyTeamCashFlow.tsx +++ b/src/components/CompanyTeamCashFlow/CompanyTeamCashFlow.tsx @@ -21,19 +21,77 @@ import { Suspense } from "react"; import ProgressCashFlowSearch from "@/components/ProgressCashFlowSearch"; import { Input, Label } from "reactstrap"; import Select, { components } from "react-select"; +import {fetchTeamCombo,fetchTeamCashFlowChartData} from "@/app/api/teamCashflow"; const CompanyTeamCashFlow: React.FC = () => { const todayDate = new Date(); const [selectionModel, setSelectionModel]: any[] = React.useState([]); + const [teamOptions, setTeamOptions]: any[] = React.useState([]); + const [teamId, setTeamId]:any = React.useState(null); + const [monthlyIncomeList, setMonthlyIncomeList]: any[] = React.useState([]); + const [monthlyCumulativeIncomeList, setMonthlyCumulativeIncomeList]: any[] = React.useState([]); + const [monthlyExpenditureList, setMonthlyExpenditureList]: any[] = React.useState([]); + const [monthlyCumulativeExpenditureList, setMonthlyCumulativeExpenditureList]: any[] = React.useState([]); + const [monthlyChartLeftMax, setMonthlyChartLeftMax]: any[] = React.useState(0); + const [monthlyChartRightMax, setMonthlyChartRightMax]: any[] = React.useState(0); const [cashFlowYear, setCashFlowYear]: any[] = React.useState( todayDate.getFullYear(), ); - const teamOptions = [ - { value: 1, label: "XXX Team" }, - { value: 2, label: "YYY Team" }, - { value: 3, label: "ZZZ Team" }, - ]; + const fetchChartData = async () => { + const cashFlowMonthlyChartData:any = await fetchTeamCashFlowChartData(cashFlowYear,teamId); + console.log(cashFlowMonthlyChartData[0]) + const monthlyIncome = [] + const cumulativeIncome = [] + const monthlyExpenditure = [] + const cumulativeExpenditure = [] + var leftMax = 0 + var rightMax = 0 + // if (cashFlowMonthlyChartData.length !== 0) { + for (var i = 0; i < cashFlowMonthlyChartData[0].teamCashFlowIncome.length; i++) { + if (leftMax < cashFlowMonthlyChartData[0].teamCashFlowIncome[i].income || leftMax < cashFlowMonthlyChartData[0].teamCashFlowExpenditure[i].expenditure){ + leftMax = Math.max(cashFlowMonthlyChartData[0].teamCashFlowIncome[i].income,cashFlowMonthlyChartData[0].teamCashFlowExpenditure[i].expenditure) + } + monthlyIncome.push(cashFlowMonthlyChartData[0].teamCashFlowIncome[i].income) + cumulativeIncome.push(cashFlowMonthlyChartData[0].teamCashFlowIncome[i].cumulativeIncome) + } + for (var i = 0; i < cashFlowMonthlyChartData[0].teamCashFlowExpenditure.length; i++) { + if (rightMax < cashFlowMonthlyChartData[0].teamCashFlowIncome[i].income || rightMax < cashFlowMonthlyChartData[0].teamCashFlowExpenditure[i].expenditure){ + rightMax = Math.max(cashFlowMonthlyChartData[0].teamCashFlowIncome[i].income,cashFlowMonthlyChartData[0].teamCashFlowExpenditure[i].expenditure) + } + monthlyExpenditure.push(cashFlowMonthlyChartData[0].teamCashFlowExpenditure[i].expenditure) + cumulativeExpenditure.push(cashFlowMonthlyChartData[0].teamCashFlowExpenditure[i].cumulativeExpenditure) + } + setMonthlyIncomeList(monthlyIncome) + setMonthlyCumulativeIncomeList(cumulativeIncome) + setMonthlyExpenditureList(monthlyExpenditure) + setMonthlyCumulativeExpenditureList(cumulativeExpenditure) + setMonthlyChartLeftMax(leftMax) + setMonthlyChartRightMax(rightMax) + // } else { + // setMonthlyIncomeList([0,0,0,0,0,0,0,0,0,0,0,0]) + // setMonthlyCumulativeIncomeList([0,0,0,0,0,0,0,0,0,0,0,0]) + // setMonthlyExpenditureList([0,0,0,0,0,0,0,0,0,0,0,0]) + // setMonthlyCumulativeExpenditureList([0,0,0,0,0,0,0,0,0,0,0,0]) + // } + } + + const fetchComboData = async () => { + const teamComboList = [] + const teamCombo = await fetchTeamCombo(); + for (var i = 0; i < teamCombo.records.length; i++) { + teamComboList.push({value: teamCombo.records[i].id, label: teamCombo.records[i].label}) + } + setTeamOptions(teamComboList) + } + + useEffect(() => { + fetchComboData() + }, []); + + useEffect(() => { + fetchChartData() + }, [cashFlowYear,teamId]); const columns = [ { @@ -158,7 +216,7 @@ const CompanyTeamCashFlow: React.FC = () => { text: "Monthly Income and Expenditure(HKD)", }, min: 0, - max: 3700000, + max: monthlyChartLeftMax, tickAmount: 5, labels: { formatter: function (val) { @@ -173,7 +231,7 @@ const CompanyTeamCashFlow: React.FC = () => { text: "Monthly Expenditure (HKD)", }, min: 0, - max: 3700000, + max: monthlyChartLeftMax, tickAmount: 5, }, { @@ -183,7 +241,7 @@ const CompanyTeamCashFlow: React.FC = () => { text: "Cumulative Income and Expenditure(HKD)", }, min: 0, - max: 21000000, + max: monthlyChartRightMax, tickAmount: 5, labels: { formatter: function (val) { @@ -199,7 +257,7 @@ const CompanyTeamCashFlow: React.FC = () => { text: "Cumulative Expenditure (HKD)", }, min: 0, - max: 21000000, + max: monthlyChartRightMax, tickAmount: 5, }, ], @@ -212,37 +270,25 @@ const CompanyTeamCashFlow: React.FC = () => { name: "Monthly_Income", type: "column", color: "#ffde91", - data: [ - 1280000, 170000, 3600000, 2400000, 1000000, 1800000, 1800000, 1200000, - 1250000, 1200000, 600000, 2400000, - ], + data: monthlyIncomeList, }, { name: "Monthly_Expenditure", type: "column", color: "#82b59a", - data: [ - 1200000, 1400000, 2000000, 1400000, 1450000, 1800000, 1200000, - 1400000, 1200000, 1600000, 2000000, 1600000, - ], + data: monthlyExpenditureList, }, { name: "Cumulative_Income", type: "line", color: "#EE6D7A", - data: [ - 500000, 3000000, 7000000, 9000000, 10000000, 13000000, 14000000, - 16000000, 17000000, 17500000, 18000000, 20000000, - ], + data: monthlyCumulativeIncomeList, }, { name: "Cumulative_Expenditure", type: "line", color: "#7cd3f2", - data: [ - 400000, 2800000, 4000000, 5200000, 7100000, 8000000, 10000000, - 11000000, 12100000, 14000000, 15400000, 17200000, - ], + data: monthlyCumulativeExpenditureList, }, ], }; @@ -296,6 +342,13 @@ const CompanyTeamCashFlow: React.FC = () => { placeholder="All Team" options={teamOptions} isClearable={true} + onChange={(selectedOption:any) => { + if (selectedOption === null) { + setTeamId(null); + } else { + setTeamId(selectedOption.value); + } + }} /> { const [monthlyCumulativeExpenditureList, setMonthlyCumulativeExpenditureList]: any[] = React.useState([]); const [monthlyChartLeftMax, setMonthlyChartLeftMax]: any[] = React.useState(0); const [monthlyChartRightMax, setMonthlyChartRightMax]: any[] = React.useState(0); + const [monthlyAnticipateLeftMax, setMonthlyAnticipateLeftMax]: any[] = React.useState(0); const [receivedPercentage,setReceivedPercentage]: any[] = React.useState(0); const [totalBudget,setTotalBudget]: any[] = React.useState(0); const [totalInvoiced,setTotalInvoiced]: any[] = React.useState(0); @@ -51,6 +52,9 @@ const ProjectCashFlow: React.FC = () => { const [totalExpenditure,setTotalExpenditure]: any[] = React.useState(0); const [expenditureReceivable,setExpenditureReceivable]: any[] = React.useState(0); const [expenditurePercentage,setExpenditurePercentage]: any[] = React.useState(0); + const [monthlyAnticipateIncomeList, setMonthlyAnticipateIncomeList]: any[] = React.useState([0,0,0,0,0,0,0,0,0,0,0,0]); + const [monthlyAnticipateExpenditureList, setMonthlyAnticipateExpenditureList]: any[] = React.useState([0,0,0,0,0,0,0,0,0,0,0,0]); + const [ledgerData, setLedgerData]: any[] = React.useState([]); const [cashFlowYear, setCashFlowYear]: any[] = React.useState( todayDate.getFullYear(), ); @@ -105,6 +109,11 @@ const ProjectCashFlow: React.FC = () => { setMonthlyCumulativeExpenditureList(cumulativeExpenditure) setMonthlyChartLeftMax(leftMax) setMonthlyChartRightMax(rightMax) + } else { + setMonthlyIncomeList([0,0,0,0,0,0,0,0,0,0,0,0]) + setMonthlyCumulativeIncomeList([0,0,0,0,0,0,0,0,0,0,0,0]) + setMonthlyExpenditureList([0,0,0,0,0,0,0,0,0,0,0,0]) + setMonthlyCumulativeExpenditureList([0,0,0,0,0,0,0,0,0,0,0,0]) } } @@ -122,7 +131,7 @@ const ProjectCashFlow: React.FC = () => { } } const fetchAnticipateData = async () => { - const cashFlowAnticipateData = await fetchProjectsCashFlowAnticipate(selectedProjectIdList,cashFlowYear); + const cashFlowAnticipateData = await fetchProjectsCashFlowAnticipate(selectedProjectIdList,anticipateCashFlowYear); const monthlyAnticipateIncome = [] var anticipateLeftMax = 0 if(cashFlowAnticipateData.length !== 0){ @@ -132,8 +141,51 @@ const ProjectCashFlow: React.FC = () => { } monthlyAnticipateIncome.push(cashFlowAnticipateData[0].anticipateIncomeList[i].anticipateIncome) } + } else { + setMonthlyAnticipateIncomeList([0,0,0,0,0,0,0,0,0,0,0,0]) + setMonthlyAnticipateExpenditureList([0,0,0,0,0,0,0,0,0,0,0,0]) } - console.log(monthlyAnticipateIncome) + setMonthlyAnticipateIncomeList(monthlyAnticipateIncome) + if(cashFlowAnticipateData.length !== 0){ + if (cashFlowAnticipateData[0].anticipateExpenditureList.length !== 0) { + const anticipateExpenditureList = [] + for (var i = 0; i < cashFlowAnticipateData[0].anticipateExpenditureList.length; i++) { + const subAnticipateExpenditure = [] + var duration = cashFlowAnticipateData[0].anticipateExpenditureList[i].Duration + var month = cashFlowAnticipateData[0].anticipateExpenditureList[i].startMonth + const anticipateExpenditure = cashFlowAnticipateData[0].anticipateExpenditureList[i].aniticipateExpenditure + for (var j = 1; j < 13; j++){ + if (month === j && duration > 0) { + subAnticipateExpenditure.push(anticipateExpenditure) + duration = duration - 1 + } else { + subAnticipateExpenditure.push(0) + } + } + anticipateExpenditureList.push(subAnticipateExpenditure) + } + const result = new Array(anticipateExpenditureList[0].length).fill(0); + for (const arr of anticipateExpenditureList) { + for (let i = 0; i < arr.length; i++) { + result[i] += arr[i]; + } + } + setMonthlyAnticipateExpenditureList(result) + for (var i = 0; i < monthlyAnticipateIncome.length; i++) { + if (anticipateLeftMax < monthlyAnticipateIncome[i] || result[i]){ + anticipateLeftMax = Math.max(monthlyAnticipateIncome[i],result[i]) + } + setMonthlyAnticipateLeftMax(anticipateLeftMax) + } + } else { + setMonthlyAnticipateExpenditureList([0,0,0,0,0,0,0,0,0,0,0,0]) + } + + } + } + const fetchProjectCashFlowLedger = async () => { + const cashFlowLedgerData = await fetchProjectsCashFlowLedger(selectedProjectIdList); + setLedgerData(cashFlowLedgerData) } useEffect(() => { fetchData() @@ -143,7 +195,11 @@ const ProjectCashFlow: React.FC = () => { fetchChartData() fetchReceivableAndExpenditureData() fetchAnticipateData() + fetchProjectCashFlowLedger() }, [cashFlowYear,selectedProjectIdList]); + useEffect(() => { + fetchAnticipateData() + }, [anticipateCashFlowYear,selectedProjectIdList]); const columns = [ { id: "projectCode", @@ -207,18 +263,39 @@ const ProjectCashFlow: React.FC = () => { field: "expenditure", headerName: "Expenditure (HKD)", flex: 0.6, + renderCell: (params:any) => { + return ( + ${params.row.expenditure.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} + ) + } }, { id: "income", field: "income", headerName: "Income (HKD)", flex: 0.6, + renderCell: (params:any) => { + return ( + ${params.row.income.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} + ) + } }, { - id: "cashFlowBalance", - field: "cashFlowBalance", + id: "balance", + field: "balance", headerName: "Cash Flow Balance (HKD)", flex: 0.6, + renderCell: (params:any) => { + if (params.row.balance < 0) { + return ( + (${Math.abs(params.row.balance).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}) + ) + } else { + return ( + ${params.row.balance.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} + ) + } + }, }, { id: "remarks", @@ -383,50 +460,50 @@ const ProjectCashFlow: React.FC = () => { text: "Anticipate Monthly Income and Expenditure(HKD)", }, min: 0, - max: monthlyChartLeftMax, - tickAmount: 5, - labels: { - formatter: function (val) { - return val.toLocaleString() - } - } - }, - { - show: false, - seriesName: "Monthly_Expenditure", - title: { - text: "Monthly Expenditure (HKD)", - }, - min: 0, - max: monthlyChartLeftMax, - tickAmount: 5, - }, - { - seriesName: "Cumulative_Income", - opposite: true, - title: { - text: "Cumulative Income and Expenditure(HKD)", - }, - min: 0, - max: monthlyChartRightMax, - tickAmount: 5, - labels: { - formatter: function (val) { - return val.toLocaleString() - } - } - }, - { - show: false, - seriesName: "Cumulative_Expenditure", - opposite: true, - title: { - text: "Cumulative Expenditure (HKD)", - }, - min: 0, - max: monthlyChartRightMax, + max: monthlyAnticipateLeftMax, tickAmount: 5, + // labels: { + // formatter: function (val) { + // return val.toLocaleString() + // } + // } }, + // { + // show: false, + // seriesName: "Monthly_Expenditure", + // title: { + // text: "Monthly Expenditure (HKD)", + // }, + // min: 0, + // max: monthlyAnticipateLeftMax, + // tickAmount: 5, + // }, + // { + // seriesName: "Cumulative_Income", + // opposite: true, + // title: { + // text: "Cumulative Income and Expenditure(HKD)", + // }, + // min: 0, + // max: MonthlyAnticipateLeftMax, + // tickAmount: 5, + // labels: { + // formatter: function (val) { + // return val.toLocaleString() + // } + // } + // }, + // { + // show: false, + // seriesName: "Cumulative_Expenditure", + // opposite: true, + // title: { + // text: "Cumulative Expenditure (HKD)", + // }, + // min: 0, + // max: monthlyChartRightMax, + // tickAmount: 5, + // }, ], grid: { borderColor: "#f1f1f1", @@ -437,16 +514,13 @@ const ProjectCashFlow: React.FC = () => { name: "Monthly_Income", type: "column", color: "#f1c48a", - data: [0, 110000, 0, 0, 185000, 0, 0, 189000, 0, 0, 300000, 0], + data: monthlyAnticipateIncomeList, }, { name: "Monthly_Expenditure", type: "column", color: "#89d7f3", - data: [ - 60000, 60000, 60000, 60000, 60000, 60000, 60000, 60000, 60000, 60000, - 60000, 60000, - ], + data: monthlyAnticipateExpenditureList, } ], }; @@ -635,7 +709,6 @@ const ProjectCashFlow: React.FC = () => { remarks: "Monthly Manpower Expenditure", }, ]; - const [ledgerData, setLedgerData]: any[] = React.useState(ledgerRows); const searchCriteria: Criterion[] = useMemo( () => [ @@ -771,7 +844,7 @@ const ProjectCashFlow: React.FC = () => { className="text-lg font-medium ml-5" style={{ color: "#6b87cf" }} > - {totalInvoiced.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} + ${totalInvoiced.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
{ className="text-lg font-medium ml-5" style={{ color: "#6b87cf" }} > - {totalReceived.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} + ${totalReceived.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}

{ className="text-lg font-medium ml-5 mb-2" style={{ color: "#6b87cf" }} > - {receivable.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} + ${receivable.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
@@ -833,7 +906,7 @@ const ProjectCashFlow: React.FC = () => { className="text-lg font-medium ml-5" style={{ color: "#6b87cf" }} > - {totalBudget.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} + ${totalBudget.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
{ className="text-lg font-medium ml-5" style={{ color: "#6b87cf" }} > - {totalExpenditure.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} + ${totalExpenditure.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}

{ className="text-lg font-medium ml-5 mb-2" style={{ color: "#6b87cf" }} > - {expenditureReceivable.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} + ${expenditureReceivable.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
@@ -894,7 +967,7 @@ const ProjectCashFlow: React.FC = () => {