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