浏览代码

Add Edit button for editing invoice detail

tags/Baseline_30082024_FRONTEND_UAT
MSI\2Fi 1年前
父节点
当前提交
9f1867ab64
共有 5 个文件被更改,包括 320 次插入63 次删除
  1. +14
    -1
      src/app/api/invoices/actions.ts
  2. +14
    -0
      src/app/api/invoices/index.ts
  3. +6
    -0
      src/app/utils/formatUtil.ts
  4. +283
    -59
      src/components/InvoiceSearch/InvoiceSearch.tsx
  5. +3
    -3
      src/components/InvoiceSearch/InvoiceSearchWrapper.tsx

+ 14
- 1
src/app/api/invoices/actions.ts 查看文件

@@ -2,6 +2,7 @@

import { serverFetchJson, serverFetchString } from "@/app/utils/fetchUtil";
import { BASE_API_URL } from "@/config/api";
import { revalidateTag } from "next/cache";
import { cache } from "react";

export interface InvoiceResult {
@@ -104,4 +105,16 @@ export const importInvoices = async (data: FormData) => {
);

return importInvoices;
};
};

export const updateInvoice = async (data: any) => {
console.log(data)
const updateInvoice = await serverFetchJson<any>(`${BASE_API_URL}/invoices/update`, {
method: "Post",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
});
revalidateTag("invoices")
return updateInvoice;
}

+ 14
- 0
src/app/api/invoices/index.ts 查看文件

@@ -69,6 +69,20 @@ export interface invoiceList {
// stage: string;
issuedDate: string;
receiptDate: string;
issuedAmount: number;
receivedAmount: number;
team: string;
}

export interface invoiceColum {
id: number;
action: string;
invoiceNo: string;
projectCode: string;
projectName: string;
// stage: string;
issuedDate: string;
receiptDate: string;
issuedAmount: string;
receivedAmount: string;
team: string;


+ 6
- 0
src/app/utils/formatUtil.ts 查看文件

@@ -33,6 +33,9 @@ export const convertDateArrayToString = (
format: string = OUTPUT_DATE_FORMAT,
needTime: boolean = false,
) => {
if (dateArray === null){
return "-"
}
if (dateArray.length === 6) {
if (!needTime) {
const dateString = `${dateArray[0]}-${dateArray[1]}-${dateArray[2]}`;
@@ -131,6 +134,9 @@ export function convertLocaleStringToNumber(numberString: string): number {
}

export function timestampToDateString(timestamp: string): string {
if (timestamp === "0" || timestamp === null) {
return "-";
}
const date = new Date(timestamp);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");


+ 283
- 59
src/components/InvoiceSearch/InvoiceSearch.tsx 查看文件

@@ -6,13 +6,17 @@ import { useTranslation } from "react-i18next";
import SearchResults, { Column } from "../SearchResults";
import EditNote from "@mui/icons-material/EditNote";
import { convertLocaleStringToNumber } from "@/app/utils/formatUtil"
import { Button, ButtonGroup, Stack, Tab, Tabs, TabsProps } from "@mui/material";
import FileDownloadIcon from '@mui/icons-material/FileDownload';
import { Button, ButtonGroup, Stack, Tab, Tabs, TabsProps, Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions } from "@mui/material";
import FileUploadIcon from '@mui/icons-material/FileUpload';
import { dateInRange, downloadFile } from "@/app/utils/commonUtil";
import { importInvoices, importIssuedInovice, importReceivedInovice } from "@/app/api/invoices/actions";
import { importInvoices, importIssuedInovice, importReceivedInovice, updateInvoice } from "@/app/api/invoices/actions";
import { errorDialogWithContent, successDialog } from "../Swal/CustomAlerts";
import { invoiceList, issuedInvoiceList, issuedInvoiceResult, issuedInvoiceSearchForm, receivedInvoiceList, receivedInvoiceSearchForm } from "@/app/api/invoices";
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
import { GridCellEditStartReasons, GridCellParams, GridColDef, GridEventListener, GridRowId, GridRowModel, GridRowModes, GridRowModesModel } from "@mui/x-data-grid";
import { useGridApiRef } from "@mui/x-data-grid";
import StyledDataGrid from "../StyledDataGrid";
import { QrCodeScannerOutlined } from "@mui/icons-material";

interface Props {
issuedInvoice: issuedInvoiceList[];
@@ -20,6 +24,17 @@ interface Props {
invoices: invoiceList[];
}

type InvoiceListError = {
[field in keyof invoiceList]?: string;
};

type invoiceListRow = Partial<
invoiceList & {
_isNew: boolean;
_error: InvoiceListError;
}
>;

type SearchQuery = Partial<Omit<issuedInvoiceSearchForm, "id">>;
type SearchParamNames = keyof SearchQuery;

@@ -67,10 +82,8 @@ const InvoiceSearch: React.FC<Props> = ({ issuedInvoice, receivedInvoice, invoic
return obj.map(obj => `"${obj.projectCode}" does not match with ${obj.invoicesNo}`).join(", ")
}

const handleImportClick = useCallback(async (event:any) => {
// console.log(event)
const handleImportClick = useCallback(async (event: any) => {
try {

const file = event.target.files[0];

if (!file) {
@@ -86,57 +99,57 @@ const InvoiceSearch: React.FC<Props> = ({ issuedInvoice, receivedInvoice, invoic
const formData = new FormData();
formData.append('multipartFileList', file);

const response = await importInvoices(formData);
// response: status, message, projectList, emptyRowList, invoiceList
const response = await importIssuedInovice(formData);
console.log(response);

console.log(response)
if (response.status) {
successDialog(t("Import Success"), t).then(() => {
window.location.reload()
})
}else{
if (response.emptyRowList.length >= 1){
errorDialogWithContent(t("Import Fail"),
t(`Please fill the mandatory field at Row <br> ${response.emptyRowList.join(", ")}`), t)
.then(() => {
window.location.reload()
})
}
else if (response.projectList.length >= 1){
errorDialogWithContent(t("Import Fail"),
t(`Please check the corresponding project code <br> ${response.projectList.join(", ")}`), t)
.then(() => {
// window.location.reload()
})
}
else if (response.invoiceList.length >= 1){
errorDialogWithContent(t("Import Fail"),
t(`Please check the corresponding Invoice No. The invoice is imported. <br>`)+ `${response.invoiceList.join(", ")}`, t)
.then(() => {
window.location.reload()
})
}
else if (response.duplicateItem.length >= 1){
errorDialogWithContent(t("Import Fail"),
t(`Please check the corresponding Invoice No. The below invoice has duplicated number. <br>`)+ `${response.duplicateItem.join(", ")}`, t)
.then(() => {
window.location.reload()
})
}else if (response.paymentMilestones.length >= 1){
errorDialogWithContent(t("Import Fail"),
t(`The payment milestone does not match with records. Please check the corresponding Invoice No. <br>`)+ `${concatListOfObject(response.paymentMilestones)}`, t)
.then(() => {
window.location.reload()
})
}
window.location.reload();
});
} else {
handleImportError(response);
}

} catch (err) {
console.log(err)
return false
}
} catch (err) {
console.log(err);
return false;
}
}, []);

const handleImportError = (response: any) => {
if (response.emptyRowList.length >= 1) {
showErrorDialog(
t("Import Fail"),
t(`Please fill the mandatory field at Row <br> ${response.emptyRowList.join(", ")}`)
);
} else if (response.projectList.length >= 1) {
showErrorDialog(
t("Import Fail"),
t(`Please check the corresponding project code <br> ${response.projectList.join(", ")}`)
);
} else if (response.invoiceList.length >= 1) {
showErrorDialog(
t("Import Fail"),
t(`Please check the corresponding Invoice No. The invoice is imported. <br>`) + `${response.invoiceList.join(", ")}`
);
} else if (response.duplicateItem.length >= 1) {
showErrorDialog(
t("Import Fail"),
t(`Please check the corresponding Invoice No. The below invoice has duplicated number. <br>`)+ `${response.duplicateItem.join(", ")}`
);
} else if (response.paymentMilestones.length >= 1) {
showErrorDialog(
t("Import Fail"),
t(`The payment milestone does not match with records. Please check the corresponding Invoice No. <br>`) + `${concatListOfObject(response.paymentMilestones)}`
);
}
};

const showErrorDialog = (title: string, content: string) => {
errorDialogWithContent(title, content, t).then(() => {
window.location.reload();
});
};

const handleRecImportClick = useCallback(async (event:any) => {
try {

@@ -229,16 +242,93 @@ const InvoiceSearch: React.FC<Props> = ({ issuedInvoice, receivedInvoice, invoic
[t],
);

const [selectedRow, setSelectedRow] = useState<invoiceListRow[] | []>([]);
const [dialogOpen, setDialogOpen] = useState(false);

const handleButtonClick = (row: invoiceList) => {
console.log(row)
setSelectedRow([row]);
setDialogOpen(true);
setRowModesModel((model) => ({
...model,
[row.id]: { mode: GridRowModes.Edit, fieldToFocus: "issuedAmount" },
}));
};

const handleCloseDialog = () => {
setDialogOpen(false);
// console.log(selectedRow)
// setSelectedRow([]);
};

const handleSaveDialog = async () => {
// setDialogOpen(false);
const response = await updateInvoice(selectedRow[0])
setDialogOpen(false);
successDialog(t("Update Success"), t).then(() => {
// window.location.reload()
})

// console.log(selectedRow[0])
// setSelectedRow([]);
};

const combinedColumns = useMemo<Column<invoiceList>[]>(
() => [
{
name: "invoiceNo",
label: t("Edit"),
onClick: (row: invoiceList) => (
handleButtonClick(row)
),
buttonIcon: <EditOutlinedIcon />
},
{ name: "invoiceNo", label: t("Invoice No") },
{ name: "projectCode", label: t("Project Code") },
{ name: "projectName", label: t("Project Name") },
{ name: "team", label: t("Team") },
{ name: "issuedDate", label: t("Issue Date") },
{ name: "receivedAmount", label: t("Amount (HKD)") },
{ name: "issuedAmount", label: t("Amount (HKD)"), type: 'money', needTranslation: true },
{ name: "receiptDate", label: t("Settle Date") },
{ name: "receivedAmount", label: t("Actual Received Amount (HKD)") },
{ name: "receivedAmount", label: t("Actual Received Amount (HKD)"), type: 'money', needTranslation: true },
],
[t]
)

const editCombinedColumns = useMemo<GridColDef[]>(
() => [
{ field: "invoiceNo", headerName: t("Invoice No"), editable: true, flex: 0.5 },
{ field: "projectCode", headerName: t("Project Code"), editable: false, flex: 0.3 },
{ field: "projectName", headerName: t("Project Name"), flex: 1 },
{ field: "team", headerName: t("Team"), flex: 0.2 },
{ field: "issuedDate",
headerName: t("Issue Date"),
editable: true,
flex: 0.4,
// type: 'date',
// valueGetter: (params) => {
// // console.log(params.row.issuedDate)
// return new Date(params.row.issuedDate)
// },
},
{ field: "issuedAmount",
headerName: t("Amount (HKD)"),
editable: true,
flex: 0.5,
type: 'number'
},
{
field: "receiptDate",
headerName: t("Settle Date"),
editable: true,
flex: 0.4,
},
{ field: "receivedAmount",
headerName: t("Actual Received Amount (HKD)"),
editable: true,
flex: 0.5,
type: 'number'
},
],
[t]
)
@@ -263,6 +353,72 @@ const InvoiceSearch: React.FC<Props> = ({ issuedInvoice, receivedInvoice, invoic
[],
);

const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({});
const apiRef = useGridApiRef();

const validateInvoiceEntry = (
entry: Partial<invoiceList>,
): InvoiceListError | undefined => {
// Test for errors
const error: InvoiceListError = {};
console.log(entry)
if (!entry.issuedAmount) {
error.issuedAmount = "Please input issued amount ";
} else if (!entry.issuedAmount) {
error.receivedAmount = "Please input received amount";
} else if (entry.invoiceNo === "") {
error.invoiceNo = "Please input invoice number";
} else if (!entry.issuedDate) {
error.issuedDate = "Please input issue date";
} else if (!entry.receiptDate){
error.receiptDate = "Please input receipt date";
}

return Object.keys(error).length > 0 ? error : undefined;
}

const validateRow = useCallback(
(id: GridRowId) => {
const row = apiRef.current.getRowWithUpdatedValues(
id,
"",
)

const error = validateInvoiceEntry(row);
console.log(error)
// Test for warnings

apiRef.current.updateRows([{ id, _error: error }]);
return !error;
},
[apiRef],
);

const handleEditStop = useCallback<GridEventListener<"rowEditStop">>(
(params, event) => {
// console.log(params.id)
if (validateRow(params.id) !== undefined || !validateRow(params.id)) {
setRowModesModel((model) => ({
...model,
[params.id]: { mode: GridRowModes.View},
}));
const row = apiRef.current.getRowWithUpdatedValues(
params.id,
"",
)
console.log(row)
setSelectedRow([{...row}] as invoiceList[])
event.defaultMuiPrevented = true;
}
// console.log(row)
},
[validateRow],
);

return (
<>
<Stack
@@ -271,8 +427,8 @@ const InvoiceSearch: React.FC<Props> = ({ issuedInvoice, receivedInvoice, invoic
flexWrap="wrap"
spacing={2}
>
{/* <ButtonGroup variant="contained"> */}
{/* <Button startIcon={<FileUploadIcon />} variant="contained" component="label">
<ButtonGroup variant="contained">
<Button startIcon={<FileUploadIcon />} variant="contained" component="label">
<input
id='importExcel'
type='file'
@@ -291,9 +447,9 @@ const InvoiceSearch: React.FC<Props> = ({ issuedInvoice, receivedInvoice, invoic
onChange={(event) => {handleRecImportClick(event)}}
/>
{t("Import Invoice Amount Receive Summary")}
</Button> */}
{/* </ButtonGroup> */}
<Button startIcon={<FileUploadIcon />} variant="contained" component="label">
</Button>
</ButtonGroup>
{/* <Button startIcon={<FileUploadIcon />} variant="contained" component="label">
<input
id='importExcel'
type='file'
@@ -302,7 +458,7 @@ const InvoiceSearch: React.FC<Props> = ({ issuedInvoice, receivedInvoice, invoic
onChange={(event) => {handleImportClick(event)}}
/>
{t("Import Invoice Summary")}
</Button>
</Button> */}
</Stack>
{
// tabIndex == 0 &&
@@ -360,6 +516,74 @@ const InvoiceSearch: React.FC<Props> = ({ issuedInvoice, receivedInvoice, invoic
columns={columns2}
/>
} */}

<Dialog open={dialogOpen} onClose={handleCloseDialog} maxWidth="lg" fullWidth>
<DialogTitle>{t("Edit Invoice")}</DialogTitle>
<DialogContent>
<DialogContentText>
{t("You can edit the invoice details here.")}
</DialogContentText>
{/* <SearchResults<invoiceList>
items={selectedRow ? [selectedRow] : []}
columns={editCombinedColumns}
/> */}
<StyledDataGrid
apiRef={apiRef}
autoHeight
sx={{
"--DataGrid-overlayHeight": "100px",
".MuiDataGrid-row .MuiDataGrid-cell.hasError": {
border: "1px solid",
borderColor: "error.main",
},
".MuiDataGrid-row .MuiDataGrid-cell.hasWarning": {
border: "1px solid",
borderColor: "warning.main",
},
'& .MuiDataGrid-columnHeaderTitle': {
whiteSpace: 'normal',
textWrap: 'pretty',
textAlign: 'center',
},
'.MuiDataGrid-row:not(.MuiDataGrid-row--dynamicHeight)>.MuiDataGrid-cell': {
overflow: 'auto',
whiteSpace: 'nowrap',
textWrap: 'pretty',
},
width: "100%", // Make the DataGrid wider
}}
disableColumnMenu
editMode="row"
rows={selectedRow}
rowModesModel={rowModesModel}
onRowModesModelChange={setRowModesModel}
onRowEditStop={handleEditStop}
columns={editCombinedColumns}
getCellClassName={(params: GridCellParams<invoiceListRow>) => {
let classname = "";
if (params.row._error?.[params.field as keyof invoiceList]) {
classname = "hasError";
}
return classname;
}}
/>
</DialogContent>
<DialogActions>
<Button onClick={handleCloseDialog} color="primary">
{t("Cancel")}
</Button>
<Button
onClick={handleSaveDialog}
color="primary"
disabled={
Object.values(rowModesModel).some((mode) => mode.mode === GridRowModes.Edit) ||
selectedRow.some((row) => row._error)
}
>
{t("Save")}
</Button>
</DialogActions>
</Dialog>
</>
);


+ 3
- 3
src/components/InvoiceSearch/InvoiceSearchWrapper.tsx 查看文件

@@ -56,9 +56,9 @@ const InvoiceSearchWrapper: React.FC & SubComponents = async () => {
projectName: invoice.projectName,
team: invoice.team,
issuedDate: timestampToDateString(invoice.invoiceDate)!!,
receiptDate: timestampToDateString(invoice.receiptDate??0)!!,
issuedAmount: moneyFormatter.format(invoice.issueAmount),
receivedAmount: moneyFormatter.format(invoice.paidAmount)
receiptDate: timestampToDateString(invoice.receiptDate)!!,
issuedAmount: invoice.issueAmount,
receivedAmount: invoice.paidAmount
}
})



正在加载...
取消
保存