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[] | issueDate: number[] | ||||
| receiptDate: number[] | receiptDate: number[] | ||||
| remarks?: string | remarks?: string | ||||
| team: string | |||||
| } | } | ||||
| export type ProjectExpensesResultFormatted = Omit<ProjectExpensesResult, 'issueDate' | 'receiptDate'> & { | export type ProjectExpensesResultFormatted = Omit<ProjectExpensesResult, 'issueDate' | 'receiptDate'> & { | ||||
| @@ -23,6 +23,7 @@ import dayjs from "dayjs"; | |||||
| import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | ||||
| import { submitDialog, successDialog } from "../Swal/CustomAlerts"; | import { submitDialog, successDialog } from "../Swal/CustomAlerts"; | ||||
| import { useRouter } from 'next/navigation'; | import { useRouter } from 'next/navigation'; | ||||
| import { ProjectExpensesResultFormatted } from "@/app/api/projectExpenses"; | |||||
| interface Props { | interface Props { | ||||
| isOpen: boolean; | isOpen: boolean; | ||||
| @@ -41,9 +42,12 @@ const modalSx: SxProps = { | |||||
| bgcolor: "background.paper", | bgcolor: "background.paper", | ||||
| }; | }; | ||||
| type postData = { | type postData = { | ||||
| data: PostExpenseData[]; | |||||
| }; | |||||
| data: (PostExpenseData & { _error: any })[]; | |||||
| } | |||||
| const CreateExpenseModal: React.FC<Props> = ({ isOpen, onClose, projects }) => { | const CreateExpenseModal: React.FC<Props> = ({ isOpen, onClose, projects }) => { | ||||
| const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
| const formProps = useForm<postData>(); | const formProps = useForm<postData>(); | ||||
| @@ -51,6 +55,10 @@ const CreateExpenseModal: React.FC<Props> = ({ isOpen, onClose, projects }) => { | |||||
| const onSubmit = useCallback<SubmitHandler<postData>>((data) => { | const onSubmit = useCallback<SubmitHandler<postData>>((data) => { | ||||
| const _data = data.data; | const _data = data.data; | ||||
| // console.log(_data.some(data => data._error)) | |||||
| if(_data.some(data => data._error)){ | |||||
| return | |||||
| } | |||||
| try { | try { | ||||
| const postData: PostExpenseData[] = _data.map((item) => { | const postData: PostExpenseData[] = _data.map((item) => { | ||||
| return { | return { | ||||
| @@ -76,12 +76,12 @@ const ExpenseSearch: React.FC<Props> = ({ expenses, projects }) => { | |||||
| // { label: t("Expense No"), paramName: "ExpenseNo", type: "text" }, | // { label: t("Expense No"), paramName: "ExpenseNo", type: "text" }, | ||||
| { label: t("Project Code"), paramName: "projectCode", type: "text" }, | { label: t("Project Code"), paramName: "projectCode", type: "text" }, | ||||
| { label: t("Project Name"), paramName: "projectName", 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 />, | buttonIcon: <EditNote />, | ||||
| // disabled: !abilities.includes(MAINTAIN_PROJECT), | // disabled: !abilities.includes(MAINTAIN_PROJECT), | ||||
| }, | }, | ||||
| { name: "expenseNo", label: t("Expense No.")}, | |||||
| { name: "projectCode", label: t("Project Code") }, | { name: "projectCode", label: t("Project Code") }, | ||||
| { name: "projectName", label: t("Project Name") }, | { name: "projectName", label: t("Project Name") }, | ||||
| { name: "amount", label: t("Amount (HKD)"), type: 'money', needTranslation: true}, | { 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: "issueDate", label: t("Issue Date") }, | ||||
| { name: "remarks", label: t("Remarks")} | { name: "remarks", label: t("Remarks")} | ||||
| ], | ], | ||||
| [t] | [t] | ||||
| ); | ); | ||||
| const onReset = useCallback(() => { | const onReset = useCallback(() => { | ||||
| // setFilteredExpenses(); | |||||
| setFilteredExpenses(expenses); | |||||
| }, []); | }, []); | ||||
| /** | /** | ||||
| @@ -287,7 +288,8 @@ const ExpenseSearch: React.FC<Props> = ({ expenses, projects }) => { | |||||
| expenses.filter( | expenses.filter( | ||||
| (e) => | (e) => | ||||
| e.projectCode.toLowerCase().includes(query.projectCode.toLowerCase()) && | 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, | ...e, | ||||
| issuedDate: e.issueDate, | issuedDate: e.issueDate, | ||||
| issueDate: formattedIssueDate, | issueDate: formattedIssueDate, | ||||
| receiptDate: formattedReceiptDate | |||||
| receiptDate: formattedReceiptDate, | |||||
| team: `${e.teamCode} - ${e.teamName}` | |||||
| }) | }) | ||||
| }) | }) | ||||
| return <ExpenseSearch | return <ExpenseSearch | ||||
| @@ -28,6 +28,7 @@ interface Props { | |||||
| TotalFees: number; | TotalFees: number; | ||||
| TotalBudget: number; | TotalBudget: number; | ||||
| TotalCumulative: number; | TotalCumulative: number; | ||||
| TotalProjectExpense: number; | |||||
| TotalInvoicedAmount: number; | TotalInvoicedAmount: number; | ||||
| TotalUnInvoicedAmount: number; | TotalUnInvoicedAmount: number; | ||||
| TotalReceivedAmount: number; | TotalReceivedAmount: number; | ||||
| @@ -50,6 +51,7 @@ const ProjectFinancialCard: React.FC<Props> = ({ | |||||
| TotalFees, | TotalFees, | ||||
| TotalBudget, | TotalBudget, | ||||
| TotalCumulative, | TotalCumulative, | ||||
| TotalProjectExpense, | |||||
| TotalInvoicedAmount, | TotalInvoicedAmount, | ||||
| TotalUnInvoicedAmount, | TotalUnInvoicedAmount, | ||||
| TotalReceivedAmount, | TotalReceivedAmount, | ||||
| @@ -138,10 +140,29 @@ const ProjectFinancialCard: React.FC<Props> = ({ | |||||
| <div className="text-sm font-medium ml-5" style={{ color: "#898d8d" }}> | <div className="text-sm font-medium ml-5" style={{ color: "#898d8d" }}> | ||||
| {"(d) " + t("Total Cumulative Expenditure")} | {"(d) " + t("Total Cumulative Expenditure")} | ||||
| </div> | </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}> | <div className="text-lg font-medium mx-5" style={dataBaseStyle}> | ||||
| {TotalCumulative.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} | {TotalCumulative.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} | ||||
| </div> | </div> | ||||
| <hr /> | <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" }}> | <div className="text-sm font-medium ml-5" style={{ color: "#898d8d" }}> | ||||
| {"(e) " + t("Total Invoiced Amount")} | {"(e) " + t("Total Invoiced Amount")} | ||||
| </div> | </div> | ||||
| @@ -59,6 +59,7 @@ const ProjectFinancialSummary: React.FC = () => { | |||||
| const fetchData = async () => { | const fetchData = async () => { | ||||
| const financialSummaryCard = await fetchFinancialSummaryCard(); | const financialSummaryCard = await fetchFinancialSummaryCard(); | ||||
| console.log(financialSummaryCard) | |||||
| setProjectFinancialData(financialSummaryCard) | setProjectFinancialData(financialSummaryCard) | ||||
| } | } | ||||
| const fetchTableData = async (teamId?:any) => { | 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"), | headerName: t("Total Cumulative Expenditure")+t("HKD"), | ||||
| minWidth:280, | minWidth:280, | ||||
| type: "number", | type: "number", | ||||
| renderCell: (params:any) => { | renderCell: (params:any) => { | ||||
| return ( | 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"), | headerName: t("Total Cumulative Expenditure")+t("HKD"), | ||||
| minWidth:250, | minWidth:250, | ||||
| type: "number", | type: "number", | ||||
| renderCell: (params:any) => { | renderCell: (params:any) => { | ||||
| return ( | 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 fetchProjectTableData = async (teamId?:any,customerId?:any) => { | ||||
| const financialSummaryByProject = await searchFinancialSummaryByProject(teamId); | const financialSummaryByProject = await searchFinancialSummaryByProject(teamId); | ||||
| setProjectFinancialRows(financialSummaryByProject) | setProjectFinancialRows(financialSummaryByProject) | ||||
| console.log(financialSummaryByProject) | |||||
| setFilteredProjectResult(financialSummaryByProject) | setFilteredProjectResult(financialSummaryByProject) | ||||
| } | } | ||||
| @@ -546,7 +596,21 @@ const columns2 = [ | |||||
| <div className="ml-10 mr-10" style={{ display: 'flex', flexWrap: 'wrap', justifyContent: 'start'}}> | <div className="ml-10 mr-10" style={{ display: 'flex', flexWrap: 'wrap', justifyContent: 'start'}}> | ||||
| {projectFinancialData.map((record:any, index:any) => ( | {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)}> | <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> | ||||
| ))} | ))} | ||||
| </div> | </div> | ||||