From 4bb13e0693b5054834679156f05c529e3b959be3 Mon Sep 17 00:00:00 2001 From: "MSI\\derek" Date: Thu, 7 Nov 2024 14:39:51 +0800 Subject: [PATCH] update --- src/app/api/teamCashflow/index.ts | 17 ++ .../CompanyTeamCashFlowV2.tsx | 283 ++++++++++++++++++ .../CompanyTeamCashFlowWrapper.tsx | 22 +- .../ProjectFinancialSummaryV2/gptFn.tsx | 7 +- 4 files changed, 323 insertions(+), 6 deletions(-) create mode 100644 src/components/CompanyTeamCashFlow/CompanyTeamCashFlowV2.tsx diff --git a/src/app/api/teamCashflow/index.ts b/src/app/api/teamCashflow/index.ts index 79e6421..0a3a2cc 100644 --- a/src/app/api/teamCashflow/index.ts +++ b/src/app/api/teamCashflow/index.ts @@ -23,6 +23,14 @@ export interface teamCashFlow { cumulativeExpenditure: number; } +export type CompanyTeamCashFlow = { + month: number, + expenditure: number, + income: number, + cumulativeExpenditure: number, + cumulativeIncome: number, +} + export const fetchTeamCombo = cache(async () => { return serverFetchJson(`${BASE_API_URL}/team/combo`); }); @@ -34,3 +42,12 @@ export const fetchTeamCashFlowChartData = cache(async (year:number,teamId?:numbe return serverFetchJson(`${BASE_API_URL}/dashboard/searchTeamCashFlow?teamId=${teamId}&year=${year}`); } }); + +export const fetchCompanyTeamCashFlow = cache(async (year: string, teamId: number | null,) => { + var endpoint = `${BASE_API_URL}/dashboard/getCompanyTeamCashFlow?year=${year}` + if (teamId) endpoint += `&teamId=${teamId}` + return serverFetchJson(endpoint, { + next: { tags: [year] }, + }); +}); + diff --git a/src/components/CompanyTeamCashFlow/CompanyTeamCashFlowV2.tsx b/src/components/CompanyTeamCashFlow/CompanyTeamCashFlowV2.tsx new file mode 100644 index 0000000..86959b9 --- /dev/null +++ b/src/components/CompanyTeamCashFlow/CompanyTeamCashFlowV2.tsx @@ -0,0 +1,283 @@ +"use client" + +import { TeamResult } from "@/app/api/team"; +import { CompanyTeamCashFlow, fetchCompanyTeamCashFlow } from "@/app/api/teamCashflow"; +import { Autocomplete, Card, CardContent, CardHeader, FormControl, Grid, InputLabel, MenuItem, Select, TextField, Typography } from "@mui/material" +import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers"; +import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; +import { ApexOptions } from "apexcharts"; +import dayjs, { Dayjs } from "dayjs"; +import { useCallback, useEffect, useState } from "react"; +import ReactApexChart from "react-apexcharts"; +import { useTranslation } from "react-i18next"; + +type Props = { + cashflow: CompanyTeamCashFlow[] + teams: TeamResult[] +} + +type Filter = { + year: Dayjs, + teamId: number, +} +const CompanyTeamCashFlowV2: React.FC = ({ + cashflow, + teams +}) => { + const currYear = dayjs() + const { + t, + i18n: { language }, + } = useTranslation(); + const [filter, setFilter] = useState({ + year: currYear, + teamId: 0 + }) + const emptyList = new Array(12).fill(0) + const [needFetch, setNeedFetch] = useState(false); + const [leftMax, setLeftMax] = useState(1000000); + const [rightMax, setRightMax] = useState(1000000); + const [incomeList, setIncomeList] = useState([...emptyList]); + const [cumulativeIncomeList, setCumulativeIncomeList] = useState([...emptyList]); + const [expenditureList, setExpenditureList] = useState([...emptyList]); + const [cumulativeExpenditureList, setCumulativeExpenditureList] = useState([...emptyList]); + + const options: ApexOptions = { + tooltip: { + y: { + formatter: function (val) { + return val.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }); + } + } + }, + chart: { + height: 350, + type: "line", + }, + stroke: { + width: [0, 0, 2, 2], + }, + plotOptions: { + bar: { + horizontal: false, + distributed: false, + }, + }, + dataLabels: { + enabled: false, + }, + xaxis: { + categories: [ + t("JAN"), + t("FEB"), + t("MAR"), + t("APR"), + t("MAY"), + t("JUN"), + t("JUL"), + t("AUG"), + t("SEP"), + t("OCT"), + t("NOV"), + t("DEC"), + ], + }, + yaxis: [ + { + title: { + text:t("Monthly Income and Expenditure (HKD)"), + style: { + fontSize: '15px' + } + }, + min: 0, + max: leftMax, + tickAmount: 5, + labels: { + formatter: function (val) { + return val.toLocaleString() + } + } + }, + { + show: false, + seriesName: "Monthly Expenditure", + title: { + text: t("Monthly Expenditure (HKD)"), + }, + min: 0, + max: leftMax, + tickAmount: 5, + }, + { + seriesName: "Cumulative Income", + opposite: true, + title: { + text: t("Cumulative Income and Expenditure (HKD)"), + style: { + fontSize: '15px' + } + }, + min: 0, + max: rightMax, + tickAmount: 5, + labels: { + formatter: function (val) { + return val.toLocaleString() + } + } + }, + { + show: false, + seriesName: "Cumulative Expenditure", + opposite: true, + title: { + text: t("Cumulative Expenditure (HKD)"), + }, + min: 0, + max: rightMax, + tickAmount: 5, + }, + ], + grid: { + borderColor: "#f1f1f1", + }, + annotations: {}, + series: [ + { + name: t("Monthly Income"), + type: "column", + color: "#ffde91", + data: incomeList, + }, + { + name: t("Monthly Expenditure"), + type: "column", + color: "#82b59a", + data: expenditureList, + }, + { + name: t("Cumulative Income"), + type: "line", + color: "#EE6D7A", + data: cumulativeIncomeList, + }, + { + name: t("Cumulative Expenditure"), + type: "line", + color: "#7cd3f2", + data: cumulativeExpenditureList, + }, + ], + }; + const fetchData = useCallback(async(filter: Filter) => { + const data = await fetchCompanyTeamCashFlow(filter.year.format("YYYY"), filter.teamId) + return data + },[needFetch]) + + useEffect(() => { + console.log(filter) + const fetchAndProcess = async () => { + let income = [...emptyList] + let cumIncome: number[] = [] + let cumExpenditure: number[] = [] + let expenditure = [...emptyList] + let data: CompanyTeamCashFlow[] = [] + if (!needFetch) { + data = cashflow + } else { + data = await fetchData(filter) + } + for (let i = 0; i < 12; i++ ) { + const curr = data[i] + income[i] = curr?.income ?? 0 + expenditure[i] = curr?.expenditure ?? 0 + if (curr) { + cumIncome.push(curr.cumulativeIncome) + cumExpenditure.push(curr.cumulativeExpenditure) + } else { + cumIncome.push(cumIncome[cumIncome.length - 1]) + cumExpenditure.push(cumExpenditure[cumExpenditure.length - 1]) + } + } + setIncomeList(income) + setExpenditureList(expenditure) + setCumulativeIncomeList(cumIncome) + setCumulativeExpenditureList(cumExpenditure) + setLeftMax(Math.max(...income,...expenditure)) + setRightMax(Math.max(...cumIncome,...cumExpenditure)) + } + fetchAndProcess() + }, [filter, needFetch]) + +// useEffect(()=>{ +// console.log(filter) +// // console.log(expenditureList) +// },[filter]) + + return ( + + + + {t("Company / Team Cash Flow")} + + + + + + + + + + { + setNeedFetch(true) + setFilter((prev) => ({...prev, year: newValue!!})) + }} + sx={{ width: 240 }} + slotProps={{ + textField: { + size: 'medium' + } + }} + /> + + {teams.length > 0 && ({id: t.id, label: t.name}))]} + isOptionEqualToValue={(option, value) => option.id === value.id} + getOptionLabel={(option) => option.label} + defaultValue={{id: 0, label: "All Teams"}} + sx={{ width: 240 }} + onChange={(_, value) => { + setNeedFetch(true) + if (value) setFilter((prev) => ({...prev, teamId: value.id})); + }} + renderInput={(params) => } + /> + } + + + + + + + + + +) +} +export default CompanyTeamCashFlowV2 \ No newline at end of file diff --git a/src/components/CompanyTeamCashFlow/CompanyTeamCashFlowWrapper.tsx b/src/components/CompanyTeamCashFlow/CompanyTeamCashFlowWrapper.tsx index d33ad6b..0d4f9a9 100644 --- a/src/components/CompanyTeamCashFlow/CompanyTeamCashFlowWrapper.tsx +++ b/src/components/CompanyTeamCashFlow/CompanyTeamCashFlowWrapper.tsx @@ -1,11 +1,29 @@ import React from "react"; -import CompanyTeamCashFlow from "./CompanyTeamCashFlow"; import { fetchUserAbilities, fetchUserStaff } from "@/app/utils/fetchUtil"; +import { VIEW_DASHBOARD_ALL } from "@/middleware"; +import CompanyTeamCashFlow from "./CompanyTeamCashFlow"; +import dayjs from "dayjs"; +import { fetchCompanyTeamCashFlow } from "@/app/api/teamCashflow"; +import CompanyTeamCashFlowV2 from "./CompanyTeamCashFlowV2"; +import { fetchTeam } from "@/app/api/team"; const CompanyTeamCashFlowWrapper: React.FC = async () => { const [abilities, staff] = await Promise.all([fetchUserAbilities(), fetchUserStaff()]); - + const currYear = dayjs().format("YYYY") + const teamId = abilities.includes(VIEW_DASHBOARD_ALL) ? null : staff.teamId + const cashflow = await fetchCompanyTeamCashFlow(currYear, teamId) + const viewAll = abilities.includes(VIEW_DASHBOARD_ALL) + // own team / all teams depends on abilities return ; + // return + // return }; export default CompanyTeamCashFlowWrapper; \ No newline at end of file diff --git a/src/components/ProjectFinancialSummaryV2/gptFn.tsx b/src/components/ProjectFinancialSummaryV2/gptFn.tsx index a38c8eb..4404029 100644 --- a/src/components/ProjectFinancialSummaryV2/gptFn.tsx +++ b/src/components/ProjectFinancialSummaryV2/gptFn.tsx @@ -1,4 +1,4 @@ -import { FinancialSummaryByProject, FinancialSummaryByClient, FinancialSummaryType, FinancialByProject } from "@/app/api/financialsummary"; +import { FinancialByProject } from "@/app/api/financialsummary"; export type SumOfByTeam = { id: number, @@ -29,8 +29,8 @@ export function sumUpByClient(data: FinancialByProject[]): SumOfByClient[] { if (!acc[item.custId]) { acc[item.custId] = { id: item.custId, - customerCode: item.customerName, - customerName: item.customerCode, + customerCode: item.customerCode, + customerName: item.customerName, totalFee: 0, totalBudget: 0, manhourExpense: 0, @@ -81,6 +81,5 @@ export function sumUpByTeam(data: FinancialByProject[]): SumOfByTeam[] { return acc; }, {}); - // Convert the result object to an array return Object.values(result); }