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> | ||||