2. Update project expense, align with invoice (add team search, add expense No, and update team to full name code - name)tags/Baseline_180220205_Frontend
| @@ -15,6 +15,7 @@ export type ProjectExpensesResult = { | |||
| issueDate: number[] | |||
| receiptDate: number[] | |||
| remarks?: string | |||
| team: string | |||
| } | |||
| export type ProjectExpensesResultFormatted = Omit<ProjectExpensesResult, 'issueDate' | 'receiptDate'> & { | |||
| @@ -23,6 +23,7 @@ import dayjs from "dayjs"; | |||
| import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | |||
| import { submitDialog, successDialog } from "../Swal/CustomAlerts"; | |||
| import { useRouter } from 'next/navigation'; | |||
| import { ProjectExpensesResultFormatted } from "@/app/api/projectExpenses"; | |||
| interface Props { | |||
| isOpen: boolean; | |||
| @@ -41,9 +42,12 @@ const modalSx: SxProps = { | |||
| bgcolor: "background.paper", | |||
| }; | |||
| type postData = { | |||
| data: PostExpenseData[]; | |||
| }; | |||
| data: (PostExpenseData & { _error: any })[]; | |||
| } | |||
| const CreateExpenseModal: React.FC<Props> = ({ isOpen, onClose, projects }) => { | |||
| const { t } = useTranslation(); | |||
| const formProps = useForm<postData>(); | |||
| @@ -51,6 +55,10 @@ const CreateExpenseModal: React.FC<Props> = ({ isOpen, onClose, projects }) => { | |||
| const onSubmit = useCallback<SubmitHandler<postData>>((data) => { | |||
| const _data = data.data; | |||
| // console.log(_data.some(data => data._error)) | |||
| if(_data.some(data => data._error)){ | |||
| return | |||
| } | |||
| try { | |||
| const postData: PostExpenseData[] = _data.map((item) => { | |||
| return { | |||
| @@ -76,12 +76,12 @@ const ExpenseSearch: React.FC<Props> = ({ expenses, projects }) => { | |||
| // { label: t("Expense No"), paramName: "ExpenseNo", type: "text" }, | |||
| { label: t("Project Code"), paramName: "projectCode", type: "text" }, | |||
| { label: t("Project Name"), paramName: "projectName", type: "text" }, | |||
| // { | |||
| // label: t("Team"), | |||
| // paramName: "team", | |||
| // type: "select", | |||
| // options: uniq(expenses.map((expenses) => expenses.teamCode)), | |||
| // }, | |||
| { | |||
| label: t("Team"), | |||
| paramName: "team", | |||
| type: "select", | |||
| options: uniq(expenses.map((expenses) => `${expenses.teamCode} - ${expenses.teamName}`)), | |||
| }, | |||
| ], | |||
| [] | |||
| ); | |||
| @@ -97,17 +97,18 @@ const ExpenseSearch: React.FC<Props> = ({ expenses, projects }) => { | |||
| buttonIcon: <EditNote />, | |||
| // disabled: !abilities.includes(MAINTAIN_PROJECT), | |||
| }, | |||
| { name: "expenseNo", label: t("Expense No.")}, | |||
| { name: "projectCode", label: t("Project Code") }, | |||
| { name: "projectName", label: t("Project Name") }, | |||
| { name: "amount", label: t("Amount (HKD)"), type: 'money', needTranslation: true}, | |||
| { name: "teamCode", label: t("Team") }, | |||
| { name: "team", label: t("Team") }, | |||
| { name: "issueDate", label: t("Issue Date") }, | |||
| { name: "remarks", label: t("Remarks")} | |||
| ], | |||
| [t] | |||
| ); | |||
| const onReset = useCallback(() => { | |||
| // setFilteredExpenses(); | |||
| setFilteredExpenses(expenses); | |||
| }, []); | |||
| /** | |||
| @@ -287,7 +288,8 @@ const ExpenseSearch: React.FC<Props> = ({ expenses, projects }) => { | |||
| expenses.filter( | |||
| (e) => | |||
| e.projectCode.toLowerCase().includes(query.projectCode.toLowerCase()) && | |||
| e.projectName.toLowerCase().includes(query.projectName.toLowerCase()) | |||
| e.projectName.toLowerCase().includes(query.projectName.toLowerCase()) && | |||
| (query.team === "All" || query.team.toLowerCase().includes(e.team.toLowerCase())) | |||
| ), | |||
| ); | |||
| }} | |||
| @@ -43,7 +43,8 @@ const ExpenseSearchWrapper: React.FC & SubComponents = async () => { | |||
| ...e, | |||
| issuedDate: e.issueDate, | |||
| issueDate: formattedIssueDate, | |||
| receiptDate: formattedReceiptDate | |||
| receiptDate: formattedReceiptDate, | |||
| team: `${e.teamCode} - ${e.teamName}` | |||
| }) | |||
| }) | |||
| return <ExpenseSearch | |||
| @@ -28,6 +28,7 @@ interface Props { | |||
| TotalFees: number; | |||
| TotalBudget: number; | |||
| TotalCumulative: number; | |||
| TotalProjectExpense: number; | |||
| TotalInvoicedAmount: number; | |||
| TotalUnInvoicedAmount: number; | |||
| TotalReceivedAmount: number; | |||
| @@ -50,6 +51,7 @@ const ProjectFinancialCard: React.FC<Props> = ({ | |||
| TotalFees, | |||
| TotalBudget, | |||
| TotalCumulative, | |||
| TotalProjectExpense, | |||
| TotalInvoicedAmount, | |||
| TotalUnInvoicedAmount, | |||
| TotalReceivedAmount, | |||
| @@ -138,10 +140,29 @@ const ProjectFinancialCard: React.FC<Props> = ({ | |||
| <div className="text-sm font-medium ml-5" style={{ color: "#898d8d" }}> | |||
| {"(d) " + t("Total Cumulative Expenditure")} | |||
| </div> | |||
| <div className="text-lg font-medium mx-5" style={dataBaseStyle}> | |||
| {(TotalCumulative + TotalProjectExpense).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} | |||
| </div> | |||
| <div style={{ overflow: 'hidden' }}> | |||
| <div className="text-sm font-medium ml-5 border-solid w-fit rounded-md mt-2 float-right mr-2" style={{ color: "#888d8f" }}> | |||
| <div className="ml-2 mr-2 ">{"(d) = (d1) + (d2)"}</div> | |||
| </div> | |||
| </div> | |||
| <hr /> | |||
| <div className="text-sm font-medium ml-5" style={{ color: "#898d8d" }}> | |||
| {"(d1) " + t("Manpower Expenses")} | |||
| </div> | |||
| <div className="text-lg font-medium mx-5" style={dataBaseStyle}> | |||
| {TotalCumulative.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} | |||
| </div> | |||
| <hr /> | |||
| <div className="text-sm font-medium ml-5" style={{ color: "#898d8d" }}> | |||
| {"(d2) " + t("Project Expenses")} | |||
| </div> | |||
| <div className="text-lg font-medium mx-5" style={dataBaseStyle}> | |||
| {TotalProjectExpense.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} | |||
| </div> | |||
| <hr /> | |||
| <div className="text-sm font-medium ml-5" style={{ color: "#898d8d" }}> | |||
| {"(e) " + t("Total Invoiced Amount")} | |||
| </div> | |||
| @@ -59,6 +59,7 @@ const ProjectFinancialSummary: React.FC = () => { | |||
| const fetchData = async () => { | |||
| const financialSummaryCard = await fetchFinancialSummaryCard(); | |||
| console.log(financialSummaryCard) | |||
| setProjectFinancialData(financialSummaryCard) | |||
| } | |||
| const fetchTableData = async (teamId?:any) => { | |||
| @@ -231,14 +232,38 @@ const ProjectFinancialSummary: React.FC = () => { | |||
| }, | |||
| }, | |||
| { | |||
| id: 'cumulativeExpenditure', | |||
| field: 'cumulativeExpenditure', | |||
| id: 'totalExpenditure', | |||
| field: 'totalExpenditure', | |||
| headerName: t("Total Cumulative Expenditure")+t("HKD"), | |||
| minWidth:280, | |||
| type: "number", | |||
| renderCell: (params:any) => { | |||
| return ( | |||
| <span>${params.row.cumulativeExpenditure.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</span> | |||
| <span>${(params.row.totalExpenditure).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</span> | |||
| ) | |||
| }, | |||
| }, | |||
| { | |||
| id: 'manhoursExpenditure', | |||
| field: 'manhoursExpenditure', | |||
| headerName: t("Manpower Expenses")+t("HKD"), | |||
| minWidth:280, | |||
| type: "number", | |||
| renderCell: (params:any) => { | |||
| return ( | |||
| <span>${params.row.manhoursExpenditure.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</span> | |||
| ) | |||
| }, | |||
| }, | |||
| { | |||
| id: 'projectExpense', | |||
| field: 'projectExpense', | |||
| headerName: t("Project Expense")+t("HKD"), | |||
| minWidth:280, | |||
| type: "number", | |||
| renderCell: (params:any) => { | |||
| return ( | |||
| <span>${(params.row.projectExpense ?? 0).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</span> | |||
| ) | |||
| }, | |||
| }, | |||
| @@ -453,14 +478,38 @@ const columns2 = [ | |||
| }, | |||
| }, | |||
| { | |||
| id: 'totalCumulativeExpenditure', | |||
| field: 'totalCumulativeExpenditure', | |||
| id: 'totalExpenditure', | |||
| field: 'totalExpenditure', | |||
| headerName: t("Total Cumulative Expenditure")+t("HKD"), | |||
| minWidth:250, | |||
| type: "number", | |||
| renderCell: (params:any) => { | |||
| return ( | |||
| <span>${params.row.cumulativeExpenditure.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</span> | |||
| <span>${(params.row.totalExpenditure).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</span> | |||
| ) | |||
| }, | |||
| }, | |||
| { | |||
| id: 'manhoursExpenditure', | |||
| field: 'manhoursExpenditure', | |||
| headerName: t("Manpower Expenses")+t("HKD"), | |||
| minWidth:280, | |||
| type: "number", | |||
| renderCell: (params:any) => { | |||
| return ( | |||
| <span>${params.row.manhoursExpenditure.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</span> | |||
| ) | |||
| }, | |||
| }, | |||
| { | |||
| id: 'projectExpense', | |||
| field: 'projectExpense', | |||
| headerName: t("Project Expense")+t("HKD"), | |||
| minWidth:280, | |||
| type: "number", | |||
| renderCell: (params:any) => { | |||
| return ( | |||
| <span>${(params.row.projectExpense ?? 0).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</span> | |||
| ) | |||
| }, | |||
| }, | |||
| @@ -512,6 +561,7 @@ const columns2 = [ | |||
| const fetchProjectTableData = async (teamId?:any,customerId?:any) => { | |||
| const financialSummaryByProject = await searchFinancialSummaryByProject(teamId); | |||
| setProjectFinancialRows(financialSummaryByProject) | |||
| console.log(financialSummaryByProject) | |||
| setFilteredProjectResult(financialSummaryByProject) | |||
| } | |||
| @@ -546,7 +596,21 @@ const columns2 = [ | |||
| <div className="ml-10 mr-10" style={{ display: 'flex', flexWrap: 'wrap', justifyContent: 'start'}}> | |||
| {projectFinancialData.map((record:any, index:any) => ( | |||
| <div className="hover:cursor-pointer ml-4 mt-5 mb-4 inline-block" key={index} onClick={(r) => handleCardClick(record,index)}> | |||
| <ProjectFinancialCard Title={record.teamName == "All Team" ? t("All Team") : record.teamName} TeamId={record.teamId} TotalActiveProjectNumber={record.projectNo} TotalFees={record.totalFee} TotalBudget={record.totalBudget} TotalCumulative={record.cumulativeExpenditure ?? 0} TotalInvoicedAmount={record.totalInvoiced ?? 0} TotalUnInvoicedAmount={record.unInvoiced ?? 0} TotalReceivedAmount={record.totalReceived ?? 0} CashFlowStatus={record.cashFlowStatus ?? "Negative"} CostPerformanceIndex={record.cpi ?? 0} ProjectedCashFlowStatus={record.projectedCashFlowStatus ?? "Negative"} ProjectedCPI={record.projectedCpi ?? 0} ClickedIndex={isCardClickedIndex} Index={index}/> | |||
| <ProjectFinancialCard | |||
| Title={record.teamName == "All Team" ? t("All Team") : record.teamName} | |||
| TeamId={record.teamId} TotalActiveProjectNumber={record.projectNo} | |||
| TotalFees={record.totalFee} TotalBudget={record.totalBudget} | |||
| TotalCumulative={record.manhoursExpenditure ?? 0} | |||
| TotalProjectExpense={record.projectExpense ?? 0} | |||
| TotalInvoicedAmount={record.totalInvoiced ?? 0} | |||
| TotalUnInvoicedAmount={record.unInvoiced ?? 0} | |||
| TotalReceivedAmount={record.totalReceived ?? 0} | |||
| CashFlowStatus={record.cashFlowStatus ?? "Negative"} | |||
| CostPerformanceIndex={record.cpi ?? 0} | |||
| ProjectedCashFlowStatus={record.projectedCashFlowStatus ?? "Negative"} | |||
| ProjectedCPI={record.projectedCpi ?? 0} | |||
| ClickedIndex={isCardClickedIndex} | |||
| Index={index}/> | |||
| </div> | |||
| ))} | |||
| </div> | |||