| @@ -17,6 +17,12 @@ export interface InvoiceResult { | |||
| reminder: string; | |||
| } | |||
| type InvoiceListError = { | |||
| [field in keyof NewInvoice]?: string; | |||
| } & { | |||
| message?: string; | |||
| }; | |||
| export type NewInvoice = { | |||
| invoiceNo: string | undefined, | |||
| projectCode: string | undefined, | |||
| @@ -24,7 +30,11 @@ export type NewInvoice = { | |||
| issuedAmount: number, | |||
| receiptDate: Date | |||
| receivedAmount: number | |||
| } & { | |||
| _isNew: boolean; | |||
| _error: InvoiceListError | |||
| } | |||
| export type InvoiceType = { | |||
| data: NewInvoice[] | |||
| } | |||
| @@ -151,4 +161,15 @@ export const deleteInvoice = async (id: number) => { | |||
| revalidateTag("invoices"); | |||
| return invoice; | |||
| }; | |||
| }; | |||
| export const createInvoices = async (data: any) => { | |||
| console.log(data) | |||
| const createInvoices = serverFetchString<any>(`${BASE_API_URL}/invoices/create`, { | |||
| method: "Post", | |||
| body: JSON.stringify(data), | |||
| headers: { "Content-Type": "application/json" }, | |||
| }); | |||
| revalidateTag("invoices") | |||
| return createInvoices; | |||
| } | |||
| @@ -10,18 +10,22 @@ import { | |||
| Card | |||
| } from '@mui/material'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { FormProvider, SubmitHandler, useForm } from 'react-hook-form'; | |||
| import { FormProvider, SubmitHandler, useForm, useFormContext } from 'react-hook-form'; | |||
| import { Check, Close } from "@mui/icons-material"; | |||
| import InvoiceTable from './InvoiceTable'; | |||
| import { ProjectResult } from '@/app/api/projects'; | |||
| import { InvoiceType, NewInvoice, PostInvoiceData } from '@/app/api/invoices/actions'; | |||
| import { createInvoices, InvoiceType, NewInvoice, PostInvoiceData } from '@/app/api/invoices/actions'; | |||
| import dayjs from 'dayjs'; | |||
| import { INPUT_DATE_FORMAT } from '@/app/utils/formatUtil'; | |||
| import { invoiceList } from '@/app/api/invoices'; | |||
| import { submitDialog, successDialog } from '../Swal/CustomAlerts'; | |||
| import { useRouter } from 'next/navigation'; | |||
| interface Props { | |||
| isOpen: boolean, | |||
| onClose: () => void | |||
| projects: ProjectResult[] | |||
| onClose: () => void, | |||
| projects: ProjectResult[], | |||
| invoices: invoiceList[] | |||
| } | |||
| const modalSx: SxProps= { | |||
| @@ -35,24 +39,41 @@ const modalSx: SxProps= { | |||
| bgcolor: 'background.paper', | |||
| }; | |||
| const CreateInvoiceModal: React.FC<Props> = ({isOpen, onClose, projects}) => { | |||
| const CreateInvoiceModal: React.FC<Props> = ({isOpen, onClose, projects, invoices}) => { | |||
| const { t } = useTranslation() | |||
| const formProps = useForm<InvoiceType>(); | |||
| const router = useRouter(); | |||
| const onSubmit = useCallback<SubmitHandler<InvoiceType>>( | |||
| async ( data ) => { | |||
| const _data = data.data | |||
| // console.log(_data) | |||
| if(_data.some(data => data._error !== undefined)){ | |||
| // console.log(data) | |||
| return | |||
| } | |||
| // const postData: PostInvoiceData = _data.map(item => ({ | |||
| const postData: any = _data.map(item => ({ | |||
| invoiceNo: item.invoiceNo || '', | |||
| projectId: projects.find(p => p.code === item.projectCode)!.id, | |||
| // projectId: projects.find(p => p.code === item.projectCode)!.id, | |||
| projectCode: item.projectCode || '', | |||
| issuedAmount: item.issuedAmount || 0, | |||
| issueDate: dayjs(item.issuedDate).format(INPUT_DATE_FORMAT), | |||
| receiptDate: item.receiptDate || null, | |||
| issuedDate: dayjs(item.issuedDate).format(INPUT_DATE_FORMAT), | |||
| receiptDate: item.receiptDate ? dayjs(item.receiptDate).format(INPUT_DATE_FORMAT) : null, | |||
| receivedAmount: item.receivedAmount || null, | |||
| })) | |||
| console.log(postData) | |||
| submitDialog(async () => { | |||
| const response = await createInvoices(postData) | |||
| console.log(response) | |||
| if (response === "OK") { | |||
| onClose() | |||
| successDialog(t("Submit Success"), t).then(() => { | |||
| router.replace("/invoice"); | |||
| }) | |||
| } | |||
| }, t) | |||
| } | |||
| , []) | |||
| @@ -77,7 +98,7 @@ const CreateInvoiceModal: React.FC<Props> = ({isOpen, onClose, projects}) => { | |||
| marginBlock: 2, | |||
| }} | |||
| > | |||
| <InvoiceTable projects={projects}/> | |||
| <InvoiceTable projects={projects} invoices={invoices}/> | |||
| </Box> | |||
| <CardActions sx={{ justifyContent: "flex-end" }}> | |||
| <Button | |||
| @@ -20,12 +20,14 @@ import StyledDataGrid from "../StyledDataGrid"; | |||
| import { uniq } from "lodash"; | |||
| import CreateInvoiceModal from "./CreateInvoiceModal"; | |||
| import { ProjectResult } from "@/app/api/projects"; | |||
| import { IMPORT_INVOICE, IMPORT_RECEIPT } from "@/middleware"; | |||
| interface Props { | |||
| invoices: invoiceList[]; | |||
| projects: ProjectResult[]; | |||
| abilities: string[]; | |||
| } | |||
| type InvoiceListError = { | |||
| @@ -45,13 +47,10 @@ type SearchParamNames = keyof SearchQuery; | |||
| type SearchQuery2 = Partial<Omit<receivedInvoiceSearchForm, "id">>; | |||
| type SearchParamNames2 = keyof SearchQuery2; | |||
| const InvoiceSearch: React.FC<Props> = ({ invoices, projects }) => { | |||
| // console.log(invoices) | |||
| const InvoiceSearch: React.FC<Props> = ({ invoices, projects, abilities }) => { | |||
| console.log(abilities) | |||
| const { t } = useTranslation("Invoice"); | |||
| const [tabIndex, setTabIndex] = useState(0); | |||
| // const [filteredIssuedInvoices, setFilteredIssuedInvoices] = useState(issuedInvoice); | |||
| // const [filteredReceivedInvoices, setFilteredReceivedInvoices] = useState(receivedInvoice); | |||
| const [filteredIvoices, setFilterInovices] = useState(invoices); | |||
| const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | |||
| @@ -228,31 +227,6 @@ const InvoiceSearch: React.FC<Props> = ({ invoices, projects }) => { | |||
| }, []); | |||
| const columns = useMemo<Column<issuedInvoiceList>[]>( | |||
| () => [ | |||
| { name: "invoiceNo", label: t("Invoice No") }, | |||
| { name: "projectCode", label: t("Project Code") }, | |||
| { name: "stage", label: t("Stage") }, | |||
| { name: "paymentMilestone", label: t("Payment Milestone") }, | |||
| { name: "invoiceDate", label: t("Invoice Date") }, | |||
| { name: "dueDate", label: t("Due Date") }, | |||
| { name: "issuedAmount", label: t("Amount (HKD)") }, | |||
| ], | |||
| [t], | |||
| ); | |||
| const columns2 = useMemo<Column<receivedInvoiceList>[]>( | |||
| () => [ | |||
| { name: "invoiceNo", label: t("Invoice No") }, | |||
| { name: "projectCode", label: t("Project Code") }, | |||
| { name: "projectName", label: t("Project Name") }, | |||
| { name: "team", label: t("Team") }, | |||
| { name: "receiptDate", label: t("Receipt Date") }, | |||
| { name: "receivedAmount", label: t("Amount (HKD)") }, | |||
| ], | |||
| [t], | |||
| ); | |||
| const [selectedRow, setSelectedRow] = useState<invoiceListRow[] | []>([]); | |||
| const [dialogOpen, setDialogOpen] = useState(false); | |||
| @@ -268,8 +242,6 @@ const InvoiceSearch: React.FC<Props> = ({ invoices, projects }) => { | |||
| const handleCloseDialog = () => { | |||
| setDialogOpen(false); | |||
| // console.log(selectedRow) | |||
| // setSelectedRow([]); | |||
| }; | |||
| const handleDeleteInvoice = useCallback(() => { | |||
| @@ -380,13 +352,6 @@ const InvoiceSearch: React.FC<Props> = ({ invoices, projects }) => { | |||
| return ((!startDate || startDate === "Invalid Date") || dateToCheckObj >= startDateObj) && ((!endDate || endDate === "Invalid Date") || dateToCheckObj <= endDateObj); | |||
| } | |||
| const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>( | |||
| (_e, newValue) => { | |||
| setTabIndex(newValue); | |||
| }, | |||
| [], | |||
| ); | |||
| const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({}); | |||
| const apiRef = useGridApiRef(); | |||
| @@ -453,6 +418,16 @@ const InvoiceSearch: React.FC<Props> = ({ invoices, projects }) => { | |||
| [validateRow], | |||
| ); | |||
| const isAddInvoiceRightExist = () => { | |||
| const importRight = [IMPORT_INVOICE].some((ability) => abilities.includes(ability)) | |||
| return importRight | |||
| } | |||
| const isAddReciptRightExist = () => { | |||
| const importRight = [IMPORT_RECEIPT].some((ability) => abilities.includes(ability)) | |||
| return importRight | |||
| } | |||
| return ( | |||
| <> | |||
| <Stack | |||
| @@ -609,6 +584,7 @@ const InvoiceSearch: React.FC<Props> = ({ invoices, projects }) => { | |||
| isOpen={modelOpen} | |||
| onClose={handleModalClose} | |||
| projects={projects} | |||
| invoices={invoices} | |||
| /> | |||
| </> | |||
| ); | |||
| @@ -5,7 +5,7 @@ import InvoiceSearchLoading from "./InvoiceSearchLoading"; | |||
| import { fetchInvoicesV3, fetchIssuedInvoices, fetchReceivedInvoices, issuedInvoiceList, issuedInvoiceResult } from "@/app/api/invoices"; | |||
| import { INPUT_DATE_FORMAT, convertDateArrayToString, convertDateToString, moneyFormatter, timestampToDateString } from "@/app/utils/formatUtil"; | |||
| import { fetchTeam } from "@/app/api/team"; | |||
| import { fetchUserStaff } from "@/app/utils/fetchUtil"; | |||
| import { fetchUserAbilities, fetchUserStaff } from "@/app/utils/fetchUtil"; | |||
| import { fetchProjects } from "@/app/api/projects"; | |||
| @@ -45,9 +45,12 @@ const InvoiceSearchWrapper: React.FC & SubComponents = async () => { | |||
| } | |||
| }) | |||
| const abilities = await fetchUserAbilities(); | |||
| return <InvoiceSearch | |||
| invoices={convertedInvoices} | |||
| projects={filteredProjects} | |||
| abilities={abilities} | |||
| /> | |||
| }; | |||
| @@ -25,6 +25,8 @@ import useEnhancedEffect from "@mui/material/utils/useEnhancedEffect"; | |||
| type InvoiceListError = { | |||
| [field in keyof invoiceList]?: string; | |||
| } & { | |||
| message?: string; | |||
| }; | |||
| type invoiceListRow = Partial< | |||
| @@ -36,6 +38,7 @@ type invoiceListRow = Partial< | |||
| interface Props { | |||
| projects: ProjectResult[]; | |||
| invoices: invoiceList[]; | |||
| } | |||
| class ProcessRowUpdateError extends Error { | |||
| @@ -57,7 +60,7 @@ type project = { | |||
| label: string; | |||
| value: number; | |||
| } | |||
| const InvoiceTable: React.FC<Props> = ({ projects }) => { | |||
| const InvoiceTable: React.FC<Props> = ({ projects, invoices }) => { | |||
| // if change this to code - name, | |||
| // also change the submit function | |||
| const projectCombos = projects.map(item => item.code) | |||
| @@ -75,20 +78,34 @@ const InvoiceTable: React.FC<Props> = ({ projects }) => { | |||
| console.log(entry) | |||
| if (!entry.issuedAmount) { | |||
| error.issuedAmount = "Please input issued amount "; | |||
| error.issuedAmount = true | |||
| error.message = t("Please input issued amount ") | |||
| } else if (!entry.issuedAmount) { | |||
| error.receivedAmount = "Please input received amount"; | |||
| } else if (entry.invoiceNo === "") { | |||
| error.invoiceNo = "Please input invoice number"; | |||
| error.issuedAmount = true | |||
| error.message = t("Please input received amount") | |||
| } else if (!entry.issuedDate) { | |||
| error.issuedDate = "Please input issue date"; | |||
| } else if (!entry.receiptDate){ | |||
| error.issuedDate = true | |||
| error.message = t("Please input issue date") | |||
| } else if (!entry.projectCode){ | |||
| error.projectCode = true | |||
| error.message = t("Please select project code") | |||
| } else if(!entry.invoiceNo){ | |||
| error.invoiceNo = true | |||
| error.message = t("Please input invoice No") | |||
| }else if(isInvoiceNoExists(entry.invoiceNo)){ | |||
| error.invoiceNo = true | |||
| error.message = t("Duplicate Invoice No") | |||
| } | |||
| return Object.keys(error).length > 0 ? error : undefined; | |||
| } | |||
| const isInvoiceNoExists = (invoiceNo: string): boolean => { | |||
| // console.log(invoices.some(i => i.invoiceNo === invoiceNo)) | |||
| const result = invoices.some(i => i.invoiceNo === invoiceNo) | |||
| return result | |||
| } | |||
| const validateRow = useCallback( | |||
| (id: GridRowId) => { | |||
| const row = apiRef.current.getRowWithUpdatedValues( | |||
| @@ -112,10 +129,10 @@ const InvoiceTable: React.FC<Props> = ({ projects }) => { | |||
| params.id, | |||
| "", | |||
| ) | |||
| console.log(params) | |||
| console.log(apiRef.current) | |||
| console.log(validateRow(params.id) !== undefined) | |||
| console.log(!validateRow(params.id)) | |||
| // console.log(params) | |||
| // console.log(apiRef.current) | |||
| // console.log(validateRow(params.id) !== undefined) | |||
| // console.log(!validateRow(params.id)) | |||
| if (validateRow(params.id) !== undefined && !validateRow(params.id)) { | |||
| setRowModesModel((model) => ({ | |||
| @@ -127,7 +144,7 @@ const InvoiceTable: React.FC<Props> = ({ projects }) => { | |||
| setSelectedRow((row) => [...row] as any[]) | |||
| event.defaultMuiPrevented = true; | |||
| } else { | |||
| console.log(row) | |||
| // console.log(row) | |||
| const error = validateRow(params.id) | |||
| setSelectedRow((row) => { | |||
| const updatedRow = row.map(r => r.id === params.id ? { ...r, _error: error } : r); | |||
| @@ -185,6 +202,8 @@ const InvoiceTable: React.FC<Props> = ({ projects }) => { | |||
| [validateRow], | |||
| ); | |||
| const hasRowErrors = selectedRow.some(row => row._error !== undefined) | |||
| /** | |||
| * Add callback to check error | |||
| */ | |||
| @@ -205,7 +224,7 @@ const InvoiceTable: React.FC<Props> = ({ projects }) => { | |||
| function renderAutocomplete(params: GridRenderCellParams<any, number>) { | |||
| return( | |||
| <Box sx={{ display: 'flex', alignItems: 'center', pr: 2 }}> | |||
| <Box sx={{ display: 'flex', alignItems: 'center', pr: 2, }}> | |||
| <Autocomplete | |||
| readOnly | |||
| sx={{ width: 300 }} | |||
| @@ -283,6 +302,7 @@ const editCombinedColumns = useMemo<GridColDef[]>( | |||
| headerName: t("Settle Date"), | |||
| editable: true, | |||
| flex: 1, | |||
| type: 'date', | |||
| }, | |||
| { field: "receivedAmount", | |||
| headerName: t("Actual Received Amount (HKD)"), | |||
| @@ -305,6 +325,12 @@ const footer = ( | |||
| > | |||
| {t("Create Invoice")} | |||
| </Button> | |||
| { | |||
| hasRowErrors && | |||
| <Typography color="warning.main" variant="body2"> | |||
| {t("There are errors!")} {selectedRow.find(row => row._error !== null)?._error?.message} | |||
| </Typography> | |||
| } | |||
| </Box> | |||
| ); | |||