@@ -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> | ||||
); | ); | ||||