@@ -6,13 +6,17 @@ import { useTranslation } from "react-i18next";
import SearchResults, { Column } from "../SearchResults";
import SearchResults, { Column } from "../SearchResults";
import EditNote from "@mui/icons-material/EditNote";
import EditNote from "@mui/icons-material/EditNote";
import { convertLocaleStringToNumber } from "@/app/utils/formatUtil"
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 FileUploadIcon from '@mui/icons-material/FileUpload';
import { dateInRange, downloadFile } from "@/app/utils/commonUtil";
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 { errorDialogWithContent, successDialog } from "../Swal/CustomAlerts";
import { invoiceList, issuedInvoiceList, issuedInvoiceResult, issuedInvoiceSearchForm, receivedInvoiceList, receivedInvoiceSearchForm } from "@/app/api/invoices";
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 {
interface Props {
issuedInvoice: issuedInvoiceList[];
issuedInvoice: issuedInvoiceList[];
@@ -20,6 +24,17 @@ interface Props {
invoices: invoiceList[];
invoices: invoiceList[];
}
}
type InvoiceListError = {
[field in keyof invoiceList]?: string;
};
type invoiceListRow = Partial<
invoiceList & {
_isNew: boolean;
_error: InvoiceListError;
}
>;
type SearchQuery = Partial<Omit<issuedInvoiceSearchForm, "id">>;
type SearchQuery = Partial<Omit<issuedInvoiceSearchForm, "id">>;
type SearchParamNames = keyof SearchQuery;
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(", ")
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 {
try {
const file = event.target.files[0];
const file = event.target.files[0];
if (!file) {
if (!file) {
@@ -86,57 +99,57 @@ const InvoiceSearch: React.FC<Props> = ({ issuedInvoice, receivedInvoice, invoic
const formData = new FormData();
const formData = new FormData();
formData.append('multipartFileList', file);
formData.append('multipartFileList', file);
const response = await importInv oices (formData);
// response: status, message, projectList, emptyRowList, invoiceList
const response = await importIssuedI nov ice(formData);
console.log(response);
console.log(response)
if (response.status) {
if (response.status) {
successDialog(t("Import Success"), t).then(() => {
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) => {
const handleRecImportClick = useCallback(async (event:any) => {
try {
try {
@@ -229,16 +242,93 @@ const InvoiceSearch: React.FC<Props> = ({ issuedInvoice, receivedInvoice, invoic
[t],
[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>[]>(
const combinedColumns = useMemo<Column<invoiceList>[]>(
() => [
() => [
{
name: "invoiceNo",
label: t("Edit"),
onClick: (row: invoiceList) => (
handleButtonClick(row)
),
buttonIcon: <EditOutlinedIcon />
},
{ name: "invoiceNo", label: t("Invoice No") },
{ name: "invoiceNo", label: t("Invoice 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: "team", label: t("Team") },
{ name: "team", label: t("Team") },
{ name: "issuedDate", label: t("Issue Date") },
{ 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: "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]
[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 (
return (
<>
<>
<Stack
<Stack
@@ -271,8 +427,8 @@ const InvoiceSearch: React.FC<Props> = ({ issuedInvoice, receivedInvoice, invoic
flexWrap="wrap"
flexWrap="wrap"
spacing={2}
spacing={2}
>
>
{/* <ButtonGroup variant="contained"> */}
{/* <Button startIcon={<FileUploadIcon />} variant="contained" component="label">
<ButtonGroup variant="contained">
<Button startIcon={<FileUploadIcon />} variant="contained" component="label">
<input
<input
id='importExcel'
id='importExcel'
type='file'
type='file'
@@ -291,9 +447,9 @@ const InvoiceSearch: React.FC<Props> = ({ issuedInvoice, receivedInvoice, invoic
onChange={(event) => {handleRecImportClick(event)}}
onChange={(event) => {handleRecImportClick(event)}}
/>
/>
{t("Import Invoice Amount Receive Summary")}
{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
<input
id='importExcel'
id='importExcel'
type='file'
type='file'
@@ -302,7 +458,7 @@ const InvoiceSearch: React.FC<Props> = ({ issuedInvoice, receivedInvoice, invoic
onChange={(event) => {handleImportClick(event)}}
onChange={(event) => {handleImportClick(event)}}
/>
/>
{t("Import Invoice Summary")}
{t("Import Invoice Summary")}
</Button>
</Button> */}
</Stack>
</Stack>
{
{
// tabIndex == 0 &&
// tabIndex == 0 &&
@@ -360,6 +516,74 @@ const InvoiceSearch: React.FC<Props> = ({ issuedInvoice, receivedInvoice, invoic
columns={columns2}
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>
</>
</>
);
);