ソースを参照

Create Invoice

tags/Baseline_30082024_FRONTEND_UAT
MSI\2Fi 1年前
コミット
99dfcd03d1
2個のファイルの変更379行の追加0行の削除
  1. +84
    -0
      src/components/InvoiceSearch/CreateInvoiceModal.tsx
  2. +295
    -0
      src/components/InvoiceSearch/InvoiceTable.tsx

+ 84
- 0
src/components/InvoiceSearch/CreateInvoiceModal.tsx ファイルの表示

@@ -0,0 +1,84 @@
import React, { useCallback, useState } from 'react';
import {
Modal,
Box,
Typography,
Button,
SxProps,
CardContent,
CardActions,
Card
} from '@mui/material';
import { useTranslation } from 'react-i18next';
import { FormProvider, SubmitHandler, useForm } from 'react-hook-form';
import { Check, Close } from "@mui/icons-material";
import InvoiceTable from './InvoiceTable';

interface Props {
isOpen: boolean,
onClose: () => void;
}

const modalSx: SxProps= {
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: { xs: "calc(100% - 2rem)", sm: "90%" },
maxHeight: "90%",
maxWidth: 1400,
bgcolor: 'background.paper',
};

const CreateInvoiceModal: React.FC<Props> = ({isOpen, onClose}) => {
const { t } = useTranslation()
const formProps = useForm<any>();

const onSubmit = useCallback<SubmitHandler<any>>(
(data) => {
console.log(data)
}
, [])

return (
<FormProvider {...formProps}>
<Modal
open={isOpen}
onClose={onClose}
>
<Card sx={modalSx}>
<CardContent
component="form"
onSubmit={formProps.handleSubmit(onSubmit)}
>
<Typography variant="overline" display="block" marginBlockEnd={1}>
{t("Invoice Creation")}
</Typography>
<Box
sx={{
marginInline: -4,
marginBlock: 4,
}}
>
<InvoiceTable/>
</Box>
<CardActions sx={{ justifyContent: "flex-end" }}>
<Button
variant="outlined"
startIcon={<Close />}
onClick={onClose}
>
{t("Cancel")}
</Button>
<Button variant="contained" startIcon={<Check />} type="submit">
{t("Save")}
</Button>
</CardActions>
</CardContent>
</Card>
</Modal>
</FormProvider>
);
};

export default CreateInvoiceModal;

+ 295
- 0
src/components/InvoiceSearch/InvoiceTable.tsx ファイルの表示

@@ -0,0 +1,295 @@
import React, { useCallback, useMemo, useState, useEffect } from "react";
import SearchBox, { Criterion } from "../SearchBox";
import { useTranslation } from "react-i18next";
import SearchResults, { Column } from "../SearchResults";
import EditNote from "@mui/icons-material/EditNote";
import { moneyFormatter } from "@/app/utils/formatUtil"
import { Button, ButtonGroup, Stack, Tab, Tabs, TabsProps, Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions, TextField, CardContent, Typography, Divider, Card, Box } from "@mui/material";
import FileUploadIcon from '@mui/icons-material/FileUpload';
import { Add, Check, Close, Delete } from "@mui/icons-material";
import { deleteInvoice, importIssuedInovice, importReceivedInovice, updateInvoice } from "@/app/api/invoices/actions";
import { deleteDialog, errorDialogWithContent, successDialog } from "../Swal/CustomAlerts";
import { invoiceList, issuedInvoiceList, issuedInvoiceSearchForm, receivedInvoiceList, receivedInvoiceSearchForm } from "@/app/api/invoices";
import EditOutlinedIcon from '@mui/icons-material/EditOutlined';
import { GridCellParams, GridColDef, GridEventListener, GridRowId, GridRowModel, GridRowModes, GridRowModesModel } from "@mui/x-data-grid";
import { useGridApiRef } from "@mui/x-data-grid";
import StyledDataGrid from "../StyledDataGrid";

import { uniq } from "lodash";
import CreateInvoiceModal from "./CreateInvoiceModal";
import { GridToolbarContainer } from "@mui/x-data-grid";
import { FooterPropsOverrides } from "@mui/x-data-grid";
import { th } from "@faker-js/faker";
import { GridRowIdGetter } from "@mui/x-data-grid";

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

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

class ProcessRowUpdateError extends Error {
public readonly row: invoiceListRow;
public readonly errors: InvoiceListError | undefined;
constructor(
row: invoiceListRow,
message?: string,
errors?: InvoiceListError,
) {
super(message);
this.row = row;
this.errors = errors;

Object.setPrototypeOf(this, ProcessRowUpdateError.prototype);
}
}

const InvoiceTable: React.FC = () => {
const { t } = useTranslation()
const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({});
const [selectedRow, setSelectedRow] = useState<invoiceListRow[] | []>([]);
const apiRef = useGridApiRef();

const validateInvoiceEntry = (
entry: Partial<invoiceList>,
): InvoiceListError | undefined => {
// Test for errors
const error: any = {};
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){
}

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) => [...row] as any[])
event.defaultMuiPrevented = true;
}
// console.log(row)
},
[validateRow],
);



const addRow = useCallback(() => {
const id = Date.now();
setSelectedRow((e) => [...e, { id, _isNew: true }]);
setRowModesModel((model) => ({
...model,
[id]: { mode: GridRowModes.Edit },
}));
}, []);

const processRowUpdate = useCallback(
(
newRow: GridRowModel<invoiceListRow>,
originalRow: GridRowModel<invoiceListRow>,
) => {
const errors = validateRow(newRow.id!!);
if (errors) {
// console.log(errors)
// throw new error for error checking
throw new ProcessRowUpdateError(
originalRow,
"validation error",
errors,
)
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { _isNew, _error, ...updatedRow } = newRow;
const rowToSave = {
...updatedRow,
} satisfies invoiceListRow;

console.log(newRow)

setSelectedRow((es) =>
es.map((e) => (e.id === rowToSave.id ? rowToSave : e))
);
return rowToSave;
},
[validateRow],
);

/**
* Add callback to check error
*/
const onProcessRowUpdateError = useCallback(
(updateError: ProcessRowUpdateError) => {
const errors = updateError.errors;
const oldRow = updateError.row;

apiRef.current.updateRows([{ ...oldRow, _error: errors }]);
},
[apiRef]
)

useEffect(() => {
console.log(selectedRow)
}, [selectedRow]);


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

const footer = (
<Box display="flex" gap={2} alignItems="center">
<Button
disableRipple
variant="outlined"
startIcon={<Add />}
onClick={addRow}
size="small"
>
{t("Create Invoice")}
</Button>
</Box>
);

return (
<>
<StyledDataGrid
apiRef={apiRef}
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",
},
}}
disableColumnMenu
editMode="row"
rows={selectedRow}
rowModesModel={rowModesModel}
onRowModesModelChange={setRowModesModel}
onRowEditStop={handleEditStop}
columns={editCombinedColumns}
processRowUpdate={processRowUpdate}
onProcessRowUpdateError={onProcessRowUpdateError}
getCellClassName={(params: GridCellParams<invoiceListRow>) => {
let classname = "";
if (params.row._error?.[params.field as keyof invoiceList]) {
classname = "hasError";
}
return classname;
}}
slots={{
footer: FooterToolbar,
noRowsOverlay: NoRowsOverlay,
}}
slotProps={{
footer: { child: footer },
}}
/>
</>
)
}

export default InvoiceTable

const NoRowsOverlay: React.FC = () => {
const { t } = useTranslation("home");
return (
<Box
display="flex"
justifyContent="center"
alignItems="center"
height="100%"
>
<Typography variant="caption">{t("Add some time entries!")}</Typography>
</Box>
);
};
const FooterToolbar: React.FC<FooterPropsOverrides> = ({ child }) => {
return <GridToolbarContainer sx={{ p: 2 }}>{child}</GridToolbarContainer>;
};

読み込み中…
キャンセル
保存