| @@ -17,6 +17,12 @@ export interface InvoiceResult { | |||||
| reminder: string; | reminder: string; | ||||
| } | } | ||||
| type InvoiceListError = { | |||||
| [field in keyof NewInvoice]?: string; | |||||
| } & { | |||||
| message?: string; | |||||
| }; | |||||
| export type NewInvoice = { | export type NewInvoice = { | ||||
| invoiceNo: string | undefined, | invoiceNo: string | undefined, | ||||
| projectCode: string | undefined, | projectCode: string | undefined, | ||||
| @@ -24,7 +30,11 @@ export type NewInvoice = { | |||||
| issuedAmount: number, | issuedAmount: number, | ||||
| receiptDate: Date | receiptDate: Date | ||||
| receivedAmount: number | receivedAmount: number | ||||
| } & { | |||||
| _isNew: boolean; | |||||
| _error: InvoiceListError | |||||
| } | } | ||||
| export type InvoiceType = { | export type InvoiceType = { | ||||
| data: NewInvoice[] | data: NewInvoice[] | ||||
| } | } | ||||
| @@ -151,4 +161,15 @@ export const deleteInvoice = async (id: number) => { | |||||
| revalidateTag("invoices"); | revalidateTag("invoices"); | ||||
| return invoice; | 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 | Card | ||||
| } from '@mui/material'; | } from '@mui/material'; | ||||
| import { useTranslation } from 'react-i18next'; | 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 { Check, Close } from "@mui/icons-material"; | ||||
| import InvoiceTable from './InvoiceTable'; | import InvoiceTable from './InvoiceTable'; | ||||
| import { ProjectResult } from '@/app/api/projects'; | 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 dayjs from 'dayjs'; | ||||
| import { INPUT_DATE_FORMAT } from '@/app/utils/formatUtil'; | 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 { | interface Props { | ||||
| isOpen: boolean, | isOpen: boolean, | ||||
| onClose: () => void | |||||
| projects: ProjectResult[] | |||||
| onClose: () => void, | |||||
| projects: ProjectResult[], | |||||
| invoices: invoiceList[] | |||||
| } | } | ||||
| const modalSx: SxProps= { | const modalSx: SxProps= { | ||||
| @@ -35,24 +39,41 @@ const modalSx: SxProps= { | |||||
| bgcolor: 'background.paper', | bgcolor: 'background.paper', | ||||
| }; | }; | ||||
| const CreateInvoiceModal: React.FC<Props> = ({isOpen, onClose, projects}) => { | |||||
| const CreateInvoiceModal: React.FC<Props> = ({isOpen, onClose, projects, invoices}) => { | |||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| const formProps = useForm<InvoiceType>(); | const formProps = useForm<InvoiceType>(); | ||||
| const router = useRouter(); | |||||
| const onSubmit = useCallback<SubmitHandler<InvoiceType>>( | const onSubmit = useCallback<SubmitHandler<InvoiceType>>( | ||||
| async ( data ) => { | async ( data ) => { | ||||
| const _data = data.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: PostInvoiceData = _data.map(item => ({ | ||||
| const postData: any = _data.map(item => ({ | const postData: any = _data.map(item => ({ | ||||
| invoiceNo: item.invoiceNo || '', | invoiceNo: item.invoiceNo || '', | ||||
| projectId: projects.find(p => p.code === item.projectCode)!.id, | |||||
| // projectId: projects.find(p => p.code === item.projectCode)!.id, | |||||
| projectCode: item.projectCode || '', | projectCode: item.projectCode || '', | ||||
| issuedAmount: item.issuedAmount || 0, | 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, | receivedAmount: item.receivedAmount || null, | ||||
| })) | })) | ||||
| console.log(postData) | 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, | marginBlock: 2, | ||||
| }} | }} | ||||
| > | > | ||||
| <InvoiceTable projects={projects}/> | |||||
| <InvoiceTable projects={projects} invoices={invoices}/> | |||||
| </Box> | </Box> | ||||
| <CardActions sx={{ justifyContent: "flex-end" }}> | <CardActions sx={{ justifyContent: "flex-end" }}> | ||||
| <Button | <Button | ||||
| @@ -20,12 +20,14 @@ import StyledDataGrid from "../StyledDataGrid"; | |||||
| import { uniq } from "lodash"; | import { uniq } from "lodash"; | ||||
| import CreateInvoiceModal from "./CreateInvoiceModal"; | import CreateInvoiceModal from "./CreateInvoiceModal"; | ||||
| import { ProjectResult } from "@/app/api/projects"; | import { ProjectResult } from "@/app/api/projects"; | ||||
| import { IMPORT_INVOICE, IMPORT_RECEIPT } from "@/middleware"; | |||||
| interface Props { | interface Props { | ||||
| invoices: invoiceList[]; | invoices: invoiceList[]; | ||||
| projects: ProjectResult[]; | projects: ProjectResult[]; | ||||
| abilities: string[]; | |||||
| } | } | ||||
| type InvoiceListError = { | type InvoiceListError = { | ||||
| @@ -45,13 +47,10 @@ type SearchParamNames = keyof SearchQuery; | |||||
| type SearchQuery2 = Partial<Omit<receivedInvoiceSearchForm, "id">>; | type SearchQuery2 = Partial<Omit<receivedInvoiceSearchForm, "id">>; | ||||
| type SearchParamNames2 = keyof SearchQuery2; | 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 { t } = useTranslation("Invoice"); | ||||
| const [tabIndex, setTabIndex] = useState(0); | |||||
| // const [filteredIssuedInvoices, setFilteredIssuedInvoices] = useState(issuedInvoice); | |||||
| // const [filteredReceivedInvoices, setFilteredReceivedInvoices] = useState(receivedInvoice); | |||||
| const [filteredIvoices, setFilterInovices] = useState(invoices); | const [filteredIvoices, setFilterInovices] = useState(invoices); | ||||
| const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | 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 [selectedRow, setSelectedRow] = useState<invoiceListRow[] | []>([]); | ||||
| const [dialogOpen, setDialogOpen] = useState(false); | const [dialogOpen, setDialogOpen] = useState(false); | ||||
| @@ -268,8 +242,6 @@ const InvoiceSearch: React.FC<Props> = ({ invoices, projects }) => { | |||||
| const handleCloseDialog = () => { | const handleCloseDialog = () => { | ||||
| setDialogOpen(false); | setDialogOpen(false); | ||||
| // console.log(selectedRow) | |||||
| // setSelectedRow([]); | |||||
| }; | }; | ||||
| const handleDeleteInvoice = useCallback(() => { | 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); | 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 [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({}); | ||||
| const apiRef = useGridApiRef(); | const apiRef = useGridApiRef(); | ||||
| @@ -453,6 +418,16 @@ const InvoiceSearch: React.FC<Props> = ({ invoices, projects }) => { | |||||
| [validateRow], | [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 ( | return ( | ||||
| <> | <> | ||||
| <Stack | <Stack | ||||
| @@ -609,6 +584,7 @@ const InvoiceSearch: React.FC<Props> = ({ invoices, projects }) => { | |||||
| isOpen={modelOpen} | isOpen={modelOpen} | ||||
| onClose={handleModalClose} | onClose={handleModalClose} | ||||
| projects={projects} | projects={projects} | ||||
| invoices={invoices} | |||||
| /> | /> | ||||
| </> | </> | ||||
| ); | ); | ||||
| @@ -5,7 +5,7 @@ import InvoiceSearchLoading from "./InvoiceSearchLoading"; | |||||
| import { fetchInvoicesV3, fetchIssuedInvoices, fetchReceivedInvoices, issuedInvoiceList, issuedInvoiceResult } from "@/app/api/invoices"; | import { fetchInvoicesV3, fetchIssuedInvoices, fetchReceivedInvoices, issuedInvoiceList, issuedInvoiceResult } from "@/app/api/invoices"; | ||||
| import { INPUT_DATE_FORMAT, convertDateArrayToString, convertDateToString, moneyFormatter, timestampToDateString } from "@/app/utils/formatUtil"; | import { INPUT_DATE_FORMAT, convertDateArrayToString, convertDateToString, moneyFormatter, timestampToDateString } from "@/app/utils/formatUtil"; | ||||
| import { fetchTeam } from "@/app/api/team"; | 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"; | import { fetchProjects } from "@/app/api/projects"; | ||||
| @@ -45,9 +45,12 @@ const InvoiceSearchWrapper: React.FC & SubComponents = async () => { | |||||
| } | } | ||||
| }) | }) | ||||
| const abilities = await fetchUserAbilities(); | |||||
| return <InvoiceSearch | return <InvoiceSearch | ||||
| invoices={convertedInvoices} | invoices={convertedInvoices} | ||||
| projects={filteredProjects} | projects={filteredProjects} | ||||
| abilities={abilities} | |||||
| /> | /> | ||||
| }; | }; | ||||
| @@ -25,6 +25,8 @@ import useEnhancedEffect from "@mui/material/utils/useEnhancedEffect"; | |||||
| type InvoiceListError = { | type InvoiceListError = { | ||||
| [field in keyof invoiceList]?: string; | [field in keyof invoiceList]?: string; | ||||
| } & { | |||||
| message?: string; | |||||
| }; | }; | ||||
| type invoiceListRow = Partial< | type invoiceListRow = Partial< | ||||
| @@ -36,6 +38,7 @@ type invoiceListRow = Partial< | |||||
| interface Props { | interface Props { | ||||
| projects: ProjectResult[]; | projects: ProjectResult[]; | ||||
| invoices: invoiceList[]; | |||||
| } | } | ||||
| class ProcessRowUpdateError extends Error { | class ProcessRowUpdateError extends Error { | ||||
| @@ -57,7 +60,7 @@ type project = { | |||||
| label: string; | label: string; | ||||
| value: number; | value: number; | ||||
| } | } | ||||
| const InvoiceTable: React.FC<Props> = ({ projects }) => { | |||||
| const InvoiceTable: React.FC<Props> = ({ projects, invoices }) => { | |||||
| // if change this to code - name, | // if change this to code - name, | ||||
| // also change the submit function | // also change the submit function | ||||
| const projectCombos = projects.map(item => item.code) | const projectCombos = projects.map(item => item.code) | ||||
| @@ -75,20 +78,34 @@ const InvoiceTable: React.FC<Props> = ({ projects }) => { | |||||
| console.log(entry) | console.log(entry) | ||||
| if (!entry.issuedAmount) { | if (!entry.issuedAmount) { | ||||
| error.issuedAmount = "Please input issued amount "; | |||||
| error.issuedAmount = true | |||||
| error.message = t("Please input issued amount ") | |||||
| } else if (!entry.issuedAmount) { | } 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) { | } 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; | 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( | const validateRow = useCallback( | ||||
| (id: GridRowId) => { | (id: GridRowId) => { | ||||
| const row = apiRef.current.getRowWithUpdatedValues( | const row = apiRef.current.getRowWithUpdatedValues( | ||||
| @@ -112,10 +129,10 @@ const InvoiceTable: React.FC<Props> = ({ projects }) => { | |||||
| params.id, | 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)) { | if (validateRow(params.id) !== undefined && !validateRow(params.id)) { | ||||
| setRowModesModel((model) => ({ | setRowModesModel((model) => ({ | ||||
| @@ -127,7 +144,7 @@ const InvoiceTable: React.FC<Props> = ({ projects }) => { | |||||
| setSelectedRow((row) => [...row] as any[]) | setSelectedRow((row) => [...row] as any[]) | ||||
| event.defaultMuiPrevented = true; | event.defaultMuiPrevented = true; | ||||
| } else { | } else { | ||||
| console.log(row) | |||||
| // console.log(row) | |||||
| const error = validateRow(params.id) | const error = validateRow(params.id) | ||||
| setSelectedRow((row) => { | setSelectedRow((row) => { | ||||
| const updatedRow = row.map(r => r.id === params.id ? { ...r, _error: error } : r); | const updatedRow = row.map(r => r.id === params.id ? { ...r, _error: error } : r); | ||||
| @@ -185,6 +202,8 @@ const InvoiceTable: React.FC<Props> = ({ projects }) => { | |||||
| [validateRow], | [validateRow], | ||||
| ); | ); | ||||
| const hasRowErrors = selectedRow.some(row => row._error !== undefined) | |||||
| /** | /** | ||||
| * Add callback to check error | * Add callback to check error | ||||
| */ | */ | ||||
| @@ -205,7 +224,7 @@ const InvoiceTable: React.FC<Props> = ({ projects }) => { | |||||
| function renderAutocomplete(params: GridRenderCellParams<any, number>) { | function renderAutocomplete(params: GridRenderCellParams<any, number>) { | ||||
| return( | return( | ||||
| <Box sx={{ display: 'flex', alignItems: 'center', pr: 2 }}> | |||||
| <Box sx={{ display: 'flex', alignItems: 'center', pr: 2, }}> | |||||
| <Autocomplete | <Autocomplete | ||||
| readOnly | readOnly | ||||
| sx={{ width: 300 }} | sx={{ width: 300 }} | ||||
| @@ -283,6 +302,7 @@ const editCombinedColumns = useMemo<GridColDef[]>( | |||||
| headerName: t("Settle Date"), | headerName: t("Settle Date"), | ||||
| editable: true, | editable: true, | ||||
| flex: 1, | flex: 1, | ||||
| type: 'date', | |||||
| }, | }, | ||||
| { field: "receivedAmount", | { field: "receivedAmount", | ||||
| headerName: t("Actual Received Amount (HKD)"), | headerName: t("Actual Received Amount (HKD)"), | ||||
| @@ -305,6 +325,12 @@ const footer = ( | |||||
| > | > | ||||
| {t("Create Invoice")} | {t("Create Invoice")} | ||||
| </Button> | </Button> | ||||
| { | |||||
| hasRowErrors && | |||||
| <Typography color="warning.main" variant="body2"> | |||||
| {t("There are errors!")} {selectedRow.find(row => row._error !== null)?._error?.message} | |||||
| </Typography> | |||||
| } | |||||
| </Box> | </Box> | ||||
| ); | ); | ||||