@@ -5,7 +5,7 @@ import Button from "@mui/material/Button"; | |||||
import Stack from "@mui/material/Stack"; | import Stack from "@mui/material/Stack"; | ||||
import Typography from "@mui/material/Typography"; | import Typography from "@mui/material/Typography"; | ||||
import Link from "next/link"; | import Link from "next/link"; | ||||
import CreateInvoice from "@/components/CreateInvoice"; | |||||
import CreateInvoice from "@/components/CreateInvoice_forGen"; | |||||
export const metadata: Metadata = { | export const metadata: Metadata = { | ||||
title: "Create Invoice", | title: "Create Invoice", | ||||
@@ -1,6 +1,6 @@ | |||||
"use server" | "use server" | ||||
import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||||
import { serverFetchJson, serverFetchString } from "@/app/utils/fetchUtil"; | |||||
import { BASE_API_URL } from "@/config/api"; | import { BASE_API_URL } from "@/config/api"; | ||||
import { cache } from "react"; | import { cache } from "react"; | ||||
@@ -64,4 +64,32 @@ export const fetchInvoiceInfoById = cache(async (id: number) => { | |||||
return serverFetchJson<InvoiceInformation[]>(`${BASE_API_URL}/invoices/getInvoiceInfo/${id}`, { | return serverFetchJson<InvoiceInformation[]>(`${BASE_API_URL}/invoices/getInvoiceInfo/${id}`, { | ||||
next: { tags: ["invoiceInfoById"] }, | next: { tags: ["invoiceInfoById"] }, | ||||
}); | }); | ||||
}) | |||||
}) | |||||
export const importIssuedInovice = async (data: FormData) => { | |||||
// console.log("----------------",data) | |||||
const importIssuedInovice = await serverFetchJson<any>( | |||||
`${BASE_API_URL}/invoices/import/issued`, | |||||
{ | |||||
method: "POST", | |||||
body: data, | |||||
// headers: { "Content-Type": "multipart/form-data" }, | |||||
}, | |||||
); | |||||
return importIssuedInovice; | |||||
}; | |||||
export const importReceivedInovice = async (data: FormData) => { | |||||
// console.log("----------------",data) | |||||
const importReceivedInovice = await serverFetchJson<any>( | |||||
`${BASE_API_URL}/invoices/import/received`, | |||||
{ | |||||
method: "POST", | |||||
body: data, | |||||
// headers: { "Content-Type": "multipart/form-data" }, | |||||
}, | |||||
); | |||||
return importReceivedInovice; | |||||
}; |
@@ -15,6 +15,107 @@ export interface InvoiceResult { | |||||
reminder: string; | reminder: string; | ||||
} | } | ||||
export interface issuedInvoiceResult { | |||||
id: number; | |||||
invoiceNo: string; | |||||
projectCode: string; | |||||
projectName: string; | |||||
team: string; | |||||
stage: string; | |||||
paymentMilestone: string; | |||||
paymentMilestoneDate: string; | |||||
client: string; | |||||
address: string; | |||||
attention: string; | |||||
invoiceDate: number[]; | |||||
dueDate: number[]; | |||||
issuedAmount: number; | |||||
} | |||||
export interface receivedInvoiceResult { | |||||
id: number; | |||||
invoiceNo: string; | |||||
projectCode: string; | |||||
projectName: string; | |||||
team: string; | |||||
receiptDate: number[]; | |||||
receivedAmount: number; | |||||
} | |||||
export interface issuedInvoiceList { | |||||
id: number; | |||||
invoiceNo: string; | |||||
projectCode: string; | |||||
projectName: string; | |||||
// team: string; | |||||
stage: string; | |||||
paymentMilestone: string; | |||||
// paymentMilestoneDate: string; | |||||
// client: string; | |||||
// address: string; | |||||
// attention: string; | |||||
invoiceDate: string; | |||||
dueDate: string; | |||||
issuedAmount: string; | |||||
} | |||||
export interface receivedInvoiceList { | |||||
id: number; | |||||
invoiceNo: string; | |||||
projectCode: string; | |||||
projectName: string; | |||||
team: string; | |||||
// stage: string; | |||||
// paymentMilestone: string; | |||||
// paymentMilestoneDate: string; | |||||
// client: string; | |||||
// address: string; | |||||
// attention: string; | |||||
receiptDate: string; | |||||
receivedAmount: string; | |||||
} | |||||
export interface issuedInvoiceSearchForm { | |||||
id: number; | |||||
invoiceNo: string; | |||||
projectCode: string; | |||||
projectName: string; | |||||
// team: string; | |||||
// stage: string; | |||||
// paymentMilestone: string; | |||||
// paymentMilestoneDate: string; | |||||
// client: string; | |||||
// address: string; | |||||
// attention: string; | |||||
invoiceDate: string; | |||||
invoiceDateTo: string; | |||||
dueDate: string; | |||||
dueDateTo: string; | |||||
// issuedAmount: string; | |||||
} | |||||
export interface receivedInvoiceSearchForm { | |||||
id: number; | |||||
invoiceNo: string; | |||||
projectCode: string; | |||||
projectName: string; | |||||
// team: string; | |||||
// stage: string; | |||||
// paymentMilestone: string; | |||||
// paymentMilestoneDate: string; | |||||
// client: string; | |||||
// address: string; | |||||
// attention: string; | |||||
receiptDate: string; | |||||
receiptDateTo: string; | |||||
// dueDate: string; | |||||
// dueDateTo: string; | |||||
// issuedAmount: string; | |||||
} | |||||
export interface InvoiceInformatio{ | export interface InvoiceInformatio{ | ||||
id: number; | id: number; | ||||
address: string; | address: string; | ||||
@@ -32,4 +133,16 @@ export const fetchInvoices = cache(async () => { | |||||
return serverFetchJson<InvoiceResult[]>(`${BASE_API_URL}/invoices`, { | return serverFetchJson<InvoiceResult[]>(`${BASE_API_URL}/invoices`, { | ||||
next: { tags: ["invoices"] }, | next: { tags: ["invoices"] }, | ||||
}); | }); | ||||
}); | |||||
export const fetchIssuedInvoices = cache(async () => { | |||||
return serverFetchJson<issuedInvoiceResult[]>(`${BASE_API_URL}/invoices/v2/allInvoices`, { | |||||
next: { tags: ["invoices"] }, | |||||
}); | |||||
}); | |||||
export const fetchReceivedInvoices = cache(async () => { | |||||
return serverFetchJson<receivedInvoiceResult[]>(`${BASE_API_URL}/invoices/v2/allInvoices/received`, { | |||||
next: { tags: ["invoices"] }, | |||||
}); | |||||
}); | }); |
@@ -212,7 +212,7 @@ const CustomerSave: React.FC<Props> = ({ | |||||
return false | return false | ||||
}) | }) | ||||
} | } | ||||
}, t) | |||||
}, t, {}) | |||||
} catch (e) { | } catch (e) { | ||||
console.log(e) | console.log(e) | ||||
setServerError(t("An error has occurred. Please try again later.")); | setServerError(t("An error has occurred. Please try again later.")); | ||||
@@ -5,98 +5,257 @@ import SearchBox, { Criterion } from "../SearchBox"; | |||||
import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
import SearchResults, { Column } from "../SearchResults"; | import SearchResults, { Column } from "../SearchResults"; | ||||
import EditNote from "@mui/icons-material/EditNote"; | import EditNote from "@mui/icons-material/EditNote"; | ||||
import { InvoiceResult } from "@/app/api/invoices"; | |||||
import { useRouter } from "next/navigation"; | |||||
import { convertLocaleStringToNumber } from "@/app/utils/formatUtil" | |||||
import { Button, ButtonGroup, Stack, Tab, Tabs, TabsProps } from "@mui/material"; | |||||
import FileDownloadIcon from '@mui/icons-material/FileDownload'; | |||||
import FileUploadIcon from '@mui/icons-material/FileUpload'; | |||||
import { dateInRange, downloadFile } from "@/app/utils/commonUtil"; | |||||
import { importIssuedInovice, importReceivedInovice } from "@/app/api/invoices/actions"; | |||||
import { errorDialogWithContent, successDialog } from "../Swal/CustomAlerts"; | |||||
import { issuedInvoiceList, issuedInvoiceResult, issuedInvoiceSearchForm, receivedInvoiceList, receivedInvoiceSearchForm } from "@/app/api/invoices"; | |||||
interface Props { | interface Props { | ||||
invoices: InvoiceResult[]; | |||||
issuedInvoice: issuedInvoiceList[]; | |||||
receivedInvoice: receivedInvoiceList[]; | |||||
} | } | ||||
type SearchQuery = Partial<Omit<InvoiceResult, "id">>; | |||||
type SearchQuery = Partial<Omit<issuedInvoiceSearchForm, "id">>; | |||||
type SearchParamNames = keyof SearchQuery; | type SearchParamNames = keyof SearchQuery; | ||||
const InvoiceSearch: React.FC<Props> = ({ invoices }) => { | |||||
type SearchQuery2 = Partial<Omit<receivedInvoiceSearchForm, "id">>; | |||||
type SearchParamNames2 = keyof SearchQuery2; | |||||
const InvoiceSearch: React.FC<Props> = ({ issuedInvoice, receivedInvoice }) => { | |||||
const { t } = useTranslation("invoices"); | const { t } = useTranslation("invoices"); | ||||
const router = useRouter(); | |||||
const [tabIndex, setTabIndex] = useState(0); | |||||
const [filteredInvoices, setFilteredInvoices] = useState(invoices); | |||||
const [filteredIssuedInvoices, setFilteredIssuedInvoices] = useState(issuedInvoice); | |||||
const [filteredReceivedInvoices, setFilteredReceivedInvoices] = useState(receivedInvoice); | |||||
const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | ||||
() => [ | () => [ | ||||
{ label: t("Project code"), paramName: "projectCode", type: "text" }, | |||||
{ label: t("Project name"), paramName: "projectName", type: "text" }, | |||||
// { label: t("Stage"), paramName: "stage", type: "text" }, | |||||
{ | |||||
label: t("Coming payment milestone from"), | |||||
label2: t("Coming payment milestone to"), | |||||
paramName: "comingPaymentMileStone", | |||||
type: "dateRange" | |||||
}, | |||||
{ | |||||
label: t("Payment date from"), | |||||
label2: t("Payment date to"), | |||||
paramName: "paymentMilestoneDate", | |||||
type: "dateRange" | |||||
}, | |||||
// { label: t("Resource utilization %"), paramName: "resourceUsage", type: "text" }, | |||||
// { label: t("Unbilled hours"), paramName: "unbilledHours", type: "text" }, | |||||
// { label: t("Reminder to issue invoice"), paramName: "reminder", type: "text" }, | |||||
{ label: t("Invoice No"), paramName: "invoiceNo", type: "text" }, | |||||
{ label: t("Project Code"), paramName: "projectCode", type: "text" }, | |||||
{ label: t("Invoice Date"), label2: t("Invoice Date To"), paramName: "invoiceDate", type: "dateRange" }, | |||||
{ label: t("Due Date"), label2: t("Due Date To"), paramName: "dueDate", type: "dateRange" }, | |||||
], | |||||
[t, issuedInvoice], | |||||
); | |||||
const searchCriteria2: Criterion<SearchParamNames2>[] = useMemo( | |||||
() => [ | |||||
{ label: t("Invoice No"), paramName: "invoiceNo", type: "text" }, | |||||
{ label: t("Project Code"), paramName: "projectCode", type: "text" }, | |||||
{ label: t("Recipt Date"), label2: t("Recipt Date To"), paramName: "receiptDate", type: "dateRange" }, | |||||
], | ], | ||||
[t, invoices], | |||||
[t, issuedInvoice], | |||||
); | ); | ||||
const onReset = useCallback(() => { | const onReset = useCallback(() => { | ||||
setFilteredInvoices(invoices); | |||||
}, [invoices]); | |||||
setFilteredIssuedInvoices(issuedInvoice); | |||||
}, [issuedInvoice]); | |||||
const handleImportClick = useCallback(async (event:any) => { | |||||
// console.log(event) | |||||
try { | |||||
const file = event.target.files[0]; | |||||
if (!file) { | |||||
console.log('No file selected'); | |||||
return; | |||||
} | |||||
if (file.type !== 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') { | |||||
console.log('Invalid file format. Only XLSX files are allowed.'); | |||||
return; | |||||
} | |||||
const formData = new FormData(); | |||||
formData.append('multipartFileList', file); | |||||
const response = await importIssuedInovice(formData); | |||||
// response: status, message, projectList, emptyRowList, invoiceList | |||||
console.log(response) | |||||
if (response.status) { | |||||
successDialog(t("Import Success"), t).then(() => { | |||||
window.location.reload() | |||||
}) | |||||
}else{ | |||||
if (response.emptyRowList.length >= 1){ | |||||
errorDialogWithContent(t("Import Fail"), | |||||
t(`Please fill the mandatory field at Row <br> ${response.emptyRowList.join(", ")}`), t) | |||||
.then(() => { | |||||
window.location.reload() | |||||
}) | |||||
} | |||||
else if (response.projectList.length >= 1){ | |||||
errorDialogWithContent(t("Import Fail"), | |||||
t(`Please check the corresponding project code <br> ${response.projectList.join(", ")}`), t) | |||||
.then(() => { | |||||
// window.location.reload() | |||||
}) | |||||
} | |||||
else if (response.invoiceList.length >= 1){ | |||||
errorDialogWithContent(t("Import Fail"), | |||||
t(`Please check the corresponding Invoice No. <br>`)+ `${response.invoiceList.join(", ")}`, t) | |||||
.then(() => { | |||||
window.location.reload() | |||||
}) | |||||
} | |||||
} | |||||
} catch (err) { | |||||
console.log(err) | |||||
return false | |||||
} | |||||
}, []); | |||||
const handleExportClick = useCallback(async (event:any) => { | |||||
try { | |||||
const file = event.target.files[0]; | |||||
const onProjectClick = useCallback((project: InvoiceResult) => { | |||||
console.log(project); | |||||
router.push(`/invoice/new?id=${project.id}`) | |||||
}, [router, t]); | |||||
if (!file) { | |||||
console.log('No file selected'); | |||||
return; | |||||
} | |||||
const columns = useMemo<Column<InvoiceResult>[]>( | |||||
if (file.type !== 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') { | |||||
console.log('Invalid file format. Only XLSX files are allowed.'); | |||||
return; | |||||
} | |||||
const formData = new FormData(); | |||||
formData.append('multipartFileList', file); | |||||
const response = await importReceivedInovice(formData) | |||||
}catch(error){ | |||||
console.log(error) | |||||
} | |||||
}, []); | |||||
const columns = useMemo<Column<issuedInvoiceList>[]>( | |||||
() => [ | () => [ | ||||
{ | |||||
name: "id", | |||||
label: t("Details"), | |||||
onClick: onProjectClick, | |||||
buttonIcon: <EditNote />, | |||||
}, | |||||
{ name: "projectCode", label: t("Project code") }, | |||||
{ name: "projectName", label: t("Project name") }, | |||||
{ name: "invoiceNo", label: t("Invoice No") }, | |||||
{ name: "projectCode", label: t("Project Code") }, | |||||
{ name: "stage", label: t("Stage") }, | { name: "stage", label: t("Stage") }, | ||||
{ name: "comingPaymentMileStone", label: t("Coming payment milestone") }, | |||||
{ name: "paymentMilestoneDate", label: t("Payment date") }, | |||||
{ name: "resourceUsage", label: t("Resource utilization %") }, | |||||
{ name: "unbilledHours", label: t("Unbilled hours") }, | |||||
{ name: "reminder", label: t("Reminder to issue invoice") }, | |||||
{ name: "paymentMilestone", label: t("Payment Milestone") }, | |||||
{ name: "invoiceDate", label: t("Invocie Date") }, | |||||
{ name: "dueDate", label: t("Due Date") }, | |||||
{ name: "issuedAmount", label: t("Amount (HKD") }, | |||||
], | ], | ||||
[t, onProjectClick], | |||||
[t], | |||||
); | |||||
function isDateInRange(dateToCheck: string, startDate: string, endDate: string): boolean { | |||||
if (!startDate || !endDate) { | |||||
return false; | |||||
} | |||||
// console.log(dateToCheck, startDate, endDate) | |||||
const dateToCheckObj = new Date(dateToCheck); | |||||
const startDateObj = new Date(startDate); | |||||
const endDateObj = new Date(endDate); | |||||
// console.log(dateToCheckObj >= startDateObj && dateToCheckObj <= endDateObj) | |||||
return dateToCheckObj >= startDateObj && dateToCheckObj <= endDateObj; | |||||
} | |||||
const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>( | |||||
(_e, newValue) => { | |||||
setTabIndex(newValue); | |||||
}, | |||||
[], | |||||
); | ); | ||||
return ( | return ( | ||||
<> | <> | ||||
<SearchBox | |||||
criteria={searchCriteria} | |||||
<Stack | |||||
direction="row" | |||||
justifyContent="right" | |||||
flexWrap="wrap" | |||||
spacing={2} | |||||
> | |||||
{/* <ButtonGroup variant="contained"> */} | |||||
<Button startIcon={<FileUploadIcon />} variant="contained" component="label"> | |||||
<input | |||||
id='importExcel' | |||||
type='file' | |||||
accept='.xlsx, .csv' | |||||
hidden | |||||
onChange={(event) => {handleImportClick(event)}} | |||||
/> | |||||
{t("Import Invoice Issue Summary")} | |||||
</Button> | |||||
<Button startIcon={<FileUploadIcon />} component="label" variant="contained"> | |||||
<input | |||||
id='importExcel' | |||||
type='file' | |||||
accept='.xlsx, .csv' | |||||
hidden | |||||
onChange={(event) => {handleExportClick(event)}} | |||||
/> | |||||
{t("Import Invoice Amount Receive Summary")} | |||||
</Button> | |||||
{/* </ButtonGroup> */} | |||||
</Stack> | |||||
{ | |||||
tabIndex == 0 && | |||||
<SearchBox | |||||
criteria={searchCriteria} | |||||
onSearch={(query) => { | |||||
console.log(query) | |||||
setFilteredIssuedInvoices( | |||||
issuedInvoice.filter( | |||||
(s) => | |||||
(isDateInRange(s.invoiceDate, query.invoiceDate ?? undefined, query.invoiceDateTo ?? undefined)) || | |||||
(isDateInRange(s.dueDate, query.dueDate ?? undefined, query.dueDateTo ?? undefined)) || | |||||
(s.invoiceNo === query.invoiceNo) || | |||||
(s.projectCode === query.projectCode) | |||||
), | |||||
); | |||||
}} | |||||
onReset={onReset} | |||||
/> | |||||
} | |||||
{ | |||||
tabIndex == 1 && | |||||
<SearchBox | |||||
criteria={searchCriteria2} | |||||
onSearch={(query) => { | onSearch={(query) => { | ||||
console.log(query) | console.log(query) | ||||
setFilteredInvoices( | |||||
invoices.filter( | |||||
(d) => | |||||
d.projectCode.toLowerCase().includes(query.projectCode.toLowerCase()) && | |||||
d.projectName.toLowerCase().includes(query.projectName.toLowerCase()) && | |||||
{/*(query.client === "All" || p.client === query.client) && | |||||
(query.category === "All" || p.category === query.category) && | |||||
(query.team === "All" || p.team === query.team), **/} | |||||
setFilteredIssuedInvoices( | |||||
issuedInvoice.filter( | |||||
(s) => | |||||
(isDateInRange(s.invoiceDate, query.receiptDate ?? undefined, query.receiptDateTo ?? undefined)) || | |||||
(s.invoiceNo === query.invoiceNo) || | |||||
(s.projectCode === query.projectCode) | |||||
), | ), | ||||
); | ); | ||||
}} | }} | ||||
onReset={onReset} | onReset={onReset} | ||||
/> | /> | ||||
<SearchResults<InvoiceResult> | |||||
items={filteredInvoices} | |||||
columns={columns} | |||||
/> | |||||
} | |||||
<Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable"> | |||||
<Tab label={t("Issued Invoices")}/> | |||||
<Tab label={t("Recieved Invoices")}/> | |||||
</Tabs> | |||||
{ | |||||
tabIndex == 0 && | |||||
<SearchResults<issuedInvoiceList> | |||||
items={filteredIssuedInvoices} | |||||
columns={columns} | |||||
/> | |||||
} | |||||
{ | |||||
tabIndex == 1 && | |||||
<p>Todo</p> | |||||
} | |||||
</> | </> | ||||
); | ); | ||||
}; | }; | ||||
@@ -5,7 +5,7 @@ import Stack from "@mui/material/Stack"; | |||||
import React from "react"; | import React from "react"; | ||||
// Can make this nicer | // Can make this nicer | ||||
export const InvoiceSearchLoading: React.FC = () => { | |||||
export const SalarySearchLoading: React.FC = () => { | |||||
return ( | return ( | ||||
<> | <> | ||||
<Card> | <Card> | ||||
@@ -23,7 +23,7 @@ export const InvoiceSearchLoading: React.FC = () => { | |||||
</Stack> | </Stack> | ||||
</CardContent> | </CardContent> | ||||
</Card> | </Card> | ||||
<Card>Invoice | |||||
<Card>Salary | |||||
<CardContent> | <CardContent> | ||||
<Stack spacing={2}> | <Stack spacing={2}> | ||||
<Skeleton variant="rounded" height={40} /> | <Skeleton variant="rounded" height={40} /> | ||||
@@ -37,4 +37,4 @@ export const InvoiceSearchLoading: React.FC = () => { | |||||
); | ); | ||||
}; | }; | ||||
export default InvoiceSearchLoading; | |||||
export default SalarySearchLoading; |
@@ -1,24 +1,44 @@ | |||||
// import { fetchInvoiceCategories, fetchInvoices } from "@/app/api/companys"; | |||||
import React from "react"; | import React from "react"; | ||||
import InvoiceSearch from "./InvoiceSearch"; | import InvoiceSearch from "./InvoiceSearch"; | ||||
import InvoiceSearchLoading from "./InvoiceSearchLoading"; | import InvoiceSearchLoading from "./InvoiceSearchLoading"; | ||||
import { fetchInvoices } from "@/app/api/invoices"; | |||||
import { timestampToDateString } from "@/app/utils/formatUtil"; | |||||
import { fetchIssuedInvoices, fetchReceivedInvoices, issuedInvoiceList, issuedInvoiceResult } from "@/app/api/invoices"; | |||||
import { INPUT_DATE_FORMAT, convertDateArrayToString, moneyFormatter } from "@/app/utils/formatUtil"; | |||||
interface SubComponents { | interface SubComponents { | ||||
Loading: typeof InvoiceSearchLoading; | Loading: typeof InvoiceSearchLoading; | ||||
} | } | ||||
const InvoiceSearchWrapper: React.FC & SubComponents = async () => { | |||||
const Invoices = await fetchInvoices(); | |||||
const temp = Invoices.map((invoice) => ({ | |||||
...invoice, | |||||
paymentMilestoneDate: timestampToDateString(invoice.paymentMilestoneDate) | |||||
})) | |||||
// function calculateHourlyRate(loweLimit: number, upperLimit: number, numOfWorkingDay: number, workingHour: number){ | |||||
// const hourlyRate = (loweLimit + upperLimit)/2/numOfWorkingDay/workingHour | |||||
// return hourlyRate.toLocaleString() | |||||
// } | |||||
return <InvoiceSearch invoices={temp} />; | |||||
const InvoiceSearchWrapper: React.FC & SubComponents = async () => { | |||||
const issuedInvoices = await fetchIssuedInvoices() | |||||
// const receivedInvoices = await fetchReceivedInvoices() | |||||
const convertedIssedInvoices = issuedInvoices.map((invoice)=>{ | |||||
return{ | |||||
id: invoice.id, | |||||
invoiceNo: invoice.invoiceNo, | |||||
projectCode: invoice.projectCode, | |||||
projectName: invoice.projectName, | |||||
stage: invoice.stage, | |||||
paymentMilestone: invoice.paymentMilestone, | |||||
invoiceDate: convertDateArrayToString(invoice.invoiceDate, INPUT_DATE_FORMAT, false)!!, | |||||
dueDate: convertDateArrayToString(invoice.dueDate, INPUT_DATE_FORMAT, false)!!, | |||||
issuedAmount: moneyFormatter.format(invoice.issuedAmount) | |||||
} | |||||
}) | |||||
return <InvoiceSearch | |||||
issuedInvoice={convertedIssedInvoices} | |||||
receivedInvoice={[]} | |||||
/> | |||||
}; | }; | ||||
InvoiceSearchWrapper.Loading = InvoiceSearchLoading; | InvoiceSearchWrapper.Loading = InvoiceSearchLoading; | ||||
@@ -0,0 +1,104 @@ | |||||
"use client"; | |||||
import React, { useCallback, useMemo, useState } 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 { InvoiceResult } from "@/app/api/invoices"; | |||||
import { useRouter } from "next/navigation"; | |||||
interface Props { | |||||
invoices: InvoiceResult[]; | |||||
} | |||||
type SearchQuery = Partial<Omit<InvoiceResult, "id">>; | |||||
type SearchParamNames = keyof SearchQuery; | |||||
const InvoiceSearch: React.FC<Props> = ({ invoices }) => { | |||||
const { t } = useTranslation("invoices"); | |||||
const router = useRouter(); | |||||
const [filteredInvoices, setFilteredInvoices] = useState(invoices); | |||||
const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | |||||
() => [ | |||||
{ label: t("Project code"), paramName: "projectCode", type: "text" }, | |||||
{ label: t("Project name"), paramName: "projectName", type: "text" }, | |||||
// { label: t("Stage"), paramName: "stage", type: "text" }, | |||||
{ | |||||
label: t("Coming payment milestone from"), | |||||
label2: t("Coming payment milestone to"), | |||||
paramName: "comingPaymentMileStone", | |||||
type: "dateRange" | |||||
}, | |||||
{ | |||||
label: t("Payment date from"), | |||||
label2: t("Payment date to"), | |||||
paramName: "paymentMilestoneDate", | |||||
type: "dateRange" | |||||
}, | |||||
// { label: t("Resource utilization %"), paramName: "resourceUsage", type: "text" }, | |||||
// { label: t("Unbilled hours"), paramName: "unbilledHours", type: "text" }, | |||||
// { label: t("Reminder to issue invoice"), paramName: "reminder", type: "text" }, | |||||
], | |||||
[t, invoices], | |||||
); | |||||
const onReset = useCallback(() => { | |||||
setFilteredInvoices(invoices); | |||||
}, [invoices]); | |||||
const onProjectClick = useCallback((project: InvoiceResult) => { | |||||
console.log(project); | |||||
router.push(`/invoice/new?id=${project.id}`) | |||||
}, [router, t]); | |||||
const columns = useMemo<Column<InvoiceResult>[]>( | |||||
() => [ | |||||
{ | |||||
name: "id", | |||||
label: t("Details"), | |||||
onClick: onProjectClick, | |||||
buttonIcon: <EditNote />, | |||||
}, | |||||
{ name: "projectCode", label: t("Project code") }, | |||||
{ name: "projectName", label: t("Project name") }, | |||||
{ name: "stage", label: t("Stage") }, | |||||
{ name: "comingPaymentMileStone", label: t("Coming payment milestone") }, | |||||
{ name: "paymentMilestoneDate", label: t("Payment date") }, | |||||
{ name: "resourceUsage", label: t("Resource utilization %") }, | |||||
{ name: "unbilledHours", label: t("Unbilled hours") }, | |||||
{ name: "reminder", label: t("Reminder to issue invoice") }, | |||||
], | |||||
[t, onProjectClick], | |||||
); | |||||
return ( | |||||
<> | |||||
<SearchBox | |||||
criteria={searchCriteria} | |||||
onSearch={(query) => { | |||||
console.log(query) | |||||
setFilteredInvoices( | |||||
invoices.filter( | |||||
(d) => | |||||
d.projectCode.toLowerCase().includes(query.projectCode.toLowerCase()) && | |||||
d.projectName.toLowerCase().includes(query.projectName.toLowerCase()) && | |||||
{/*(query.client === "All" || p.client === query.client) && | |||||
(query.category === "All" || p.category === query.category) && | |||||
(query.team === "All" || p.team === query.team), **/} | |||||
), | |||||
); | |||||
}} | |||||
onReset={onReset} | |||||
/> | |||||
<SearchResults<InvoiceResult> | |||||
items={filteredInvoices} | |||||
columns={columns} | |||||
/> | |||||
</> | |||||
); | |||||
}; | |||||
export default InvoiceSearch; |
@@ -0,0 +1,40 @@ | |||||
import Card from "@mui/material/Card"; | |||||
import CardContent from "@mui/material/CardContent"; | |||||
import Skeleton from "@mui/material/Skeleton"; | |||||
import Stack from "@mui/material/Stack"; | |||||
import React from "react"; | |||||
// Can make this nicer | |||||
export const InvoiceSearchLoading: React.FC = () => { | |||||
return ( | |||||
<> | |||||
<Card> | |||||
<CardContent> | |||||
<Stack spacing={2}> | |||||
<Skeleton variant="rounded" height={60} /> | |||||
<Skeleton variant="rounded" height={60} /> | |||||
<Skeleton variant="rounded" height={60} /> | |||||
<Skeleton | |||||
variant="rounded" | |||||
height={50} | |||||
width={100} | |||||
sx={{ alignSelf: "flex-end" }} | |||||
/> | |||||
</Stack> | |||||
</CardContent> | |||||
</Card> | |||||
<Card>Invoice | |||||
<CardContent> | |||||
<Stack spacing={2}> | |||||
<Skeleton variant="rounded" height={40} /> | |||||
<Skeleton variant="rounded" height={40} /> | |||||
<Skeleton variant="rounded" height={40} /> | |||||
<Skeleton variant="rounded" height={40} /> | |||||
</Stack> | |||||
</CardContent> | |||||
</Card> | |||||
</> | |||||
); | |||||
}; | |||||
export default InvoiceSearchLoading; |
@@ -0,0 +1,26 @@ | |||||
import React from "react"; | |||||
import InvoiceSearch from "./InvoiceSearch"; | |||||
import InvoiceSearchLoading from "./InvoiceSearchLoading"; | |||||
import { fetchInvoices } from "@/app/api/invoices"; | |||||
import { timestampToDateString } from "@/app/utils/formatUtil"; | |||||
interface SubComponents { | |||||
Loading: typeof InvoiceSearchLoading; | |||||
} | |||||
const InvoiceSearchWrapper: React.FC & SubComponents = async () => { | |||||
const Invoices = await fetchInvoices(); | |||||
const temp = Invoices.map((invoice) => ({ | |||||
...invoice, | |||||
paymentMilestoneDate: timestampToDateString(invoice.paymentMilestoneDate) | |||||
})) | |||||
return <InvoiceSearch invoices={temp} />; | |||||
}; | |||||
InvoiceSearchWrapper.Loading = InvoiceSearchLoading; | |||||
export default InvoiceSearchWrapper; |
@@ -0,0 +1 @@ | |||||
export { default } from "./InvoiceSearchWrapper"; |
@@ -12,6 +12,7 @@ import FileDownloadIcon from '@mui/icons-material/FileDownload'; | |||||
import FileUploadIcon from '@mui/icons-material/FileUpload'; | import FileUploadIcon from '@mui/icons-material/FileUpload'; | ||||
import { exportSalary, importSalarys } from "@/app/api/salarys/actions"; | import { exportSalary, importSalarys } from "@/app/api/salarys/actions"; | ||||
import { downloadFile } from "@/app/utils/commonUtil"; | import { downloadFile } from "@/app/utils/commonUtil"; | ||||
import { errorDialog, successDialog } from "../Swal/CustomAlerts"; | |||||
interface Props { | interface Props { | ||||
salarys: SalaryResult[]; | salarys: SalaryResult[]; | ||||
@@ -63,7 +64,13 @@ const SalarySearch: React.FC<Props> = ({ salarys }) => { | |||||
const response = await importSalarys(formData); | const response = await importSalarys(formData); | ||||
if (response === "OK") { | if (response === "OK") { | ||||
window.location.reload() | |||||
successDialog(t("Import Success"), t).then(()=>{ | |||||
window.location.reload() | |||||
}) | |||||
}else{ | |||||
errorDialog(t("Import Fail")).then(()=>{ | |||||
window.location.reload() | |||||
}) | |||||
} | } | ||||
} catch (err) { | } catch (err) { | ||||
@@ -74,20 +81,24 @@ const SalarySearch: React.FC<Props> = ({ salarys }) => { | |||||
const handleExportClick = useCallback(async (event:any) => { | const handleExportClick = useCallback(async (event:any) => { | ||||
// console.log(event); | // console.log(event); | ||||
const response = await exportSalary() | |||||
if (response) { | |||||
downloadFile(new Uint8Array(response.blobValue), response.filename!!) | |||||
try{ | |||||
const response = await exportSalary() | |||||
if (response) { | |||||
downloadFile(new Uint8Array(response.blobValue), response.filename!!) | |||||
} | |||||
}catch(error){ | |||||
console.log(error) | |||||
} | } | ||||
}, []); | }, []); | ||||
const columns = useMemo<Column<SalaryResult>[]>( | const columns = useMemo<Column<SalaryResult>[]>( | ||||
() => [ | () => [ | ||||
{ | |||||
name: "id", | |||||
label: t("Details"), | |||||
onClick: onSalaryClick, | |||||
buttonIcon: <EditNote />, | |||||
}, | |||||
// { | |||||
// name: "id", | |||||
// label: t("Details"), | |||||
// onClick: onSalaryClick, | |||||
// buttonIcon: <EditNote />, | |||||
// }, | |||||
{ name: "salaryPoint", label: t("Salary Point") }, | { name: "salaryPoint", label: t("Salary Point") }, | ||||
{ name: "lowerLimit", label: t("Lower Limit") }, | { name: "lowerLimit", label: t("Lower Limit") }, | ||||
{ name: "upperLimit", label: t("Upper Limit") }, | { name: "upperLimit", label: t("Upper Limit") }, | ||||
@@ -123,12 +134,19 @@ const SalarySearch: React.FC<Props> = ({ salarys }) => { | |||||
<SearchBox | <SearchBox | ||||
criteria={searchCriteria} | criteria={searchCriteria} | ||||
onSearch={(query) => { | onSearch={(query) => { | ||||
// console.log(Number(query.salaryPoint)) | |||||
setFilteredSalarys( | setFilteredSalarys( | ||||
salarys.filter( | salarys.filter( | ||||
(s) => | (s) => | ||||
((convertLocaleStringToNumber(s.lowerLimit) <= Number(query.salary))&& | |||||
(convertLocaleStringToNumber(s.upperLimit) >= Number(query.salary)))|| | |||||
(s.salaryPoint === Number(query.salaryPoint)) | |||||
{ | |||||
// console.log(s) | |||||
return ( | |||||
((convertLocaleStringToNumber(s.lowerLimit) <= Number(query.salary))&& | |||||
(convertLocaleStringToNumber(s.upperLimit) >= Number(query.salary)))|| | |||||
(s.salaryPoint === Number(query.salaryPoint)) | |||||
) | |||||
} | |||||
), | ), | ||||
); | ); | ||||
}} | }} | ||||
@@ -42,6 +42,16 @@ export const errorDialog = (text, t) => { | |||||
}) | }) | ||||
} | } | ||||
export const errorDialogWithContent = (title, text, t) => { | |||||
return Swal.fire({ | |||||
icon: "error", | |||||
title: title, | |||||
html: text, | |||||
confirmButtonText: t("Confirm"), | |||||
showConfirmButton: true, | |||||
}) | |||||
} | |||||
export const warningDialog = (text, t) => { | export const warningDialog = (text, t) => { | ||||
return Swal.fire({ | return Swal.fire({ | ||||
icon: "warning", | icon: "warning", | ||||