@@ -5,7 +5,7 @@ import Button from "@mui/material/Button"; | |||
import Stack from "@mui/material/Stack"; | |||
import Typography from "@mui/material/Typography"; | |||
import Link from "next/link"; | |||
import CreateInvoice from "@/components/CreateInvoice"; | |||
import CreateInvoice from "@/components/CreateInvoice_forGen"; | |||
export const metadata: Metadata = { | |||
title: "Create Invoice", | |||
@@ -1,6 +1,6 @@ | |||
"use server" | |||
import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||
import { serverFetchJson, serverFetchString } from "@/app/utils/fetchUtil"; | |||
import { BASE_API_URL } from "@/config/api"; | |||
import { cache } from "react"; | |||
@@ -64,4 +64,32 @@ export const fetchInvoiceInfoById = cache(async (id: number) => { | |||
return serverFetchJson<InvoiceInformation[]>(`${BASE_API_URL}/invoices/getInvoiceInfo/${id}`, { | |||
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; | |||
} | |||
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{ | |||
id: number; | |||
address: string; | |||
@@ -32,4 +133,16 @@ export const fetchInvoices = cache(async () => { | |||
return serverFetchJson<InvoiceResult[]>(`${BASE_API_URL}/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 | |||
}) | |||
} | |||
}, t) | |||
}, t, {}) | |||
} catch (e) { | |||
console.log(e) | |||
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 SearchResults, { Column } from "../SearchResults"; | |||
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 { | |||
invoices: InvoiceResult[]; | |||
issuedInvoice: issuedInvoiceList[]; | |||
receivedInvoice: receivedInvoiceList[]; | |||
} | |||
type SearchQuery = Partial<Omit<InvoiceResult, "id">>; | |||
type SearchQuery = Partial<Omit<issuedInvoiceSearchForm, "id">>; | |||
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 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( | |||
() => [ | |||
{ 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(() => { | |||
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: "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 ( | |||
<> | |||
<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) => { | |||
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} | |||
/> | |||
<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"; | |||
// Can make this nicer | |||
export const InvoiceSearchLoading: React.FC = () => { | |||
export const SalarySearchLoading: React.FC = () => { | |||
return ( | |||
<> | |||
<Card> | |||
@@ -23,7 +23,7 @@ export const InvoiceSearchLoading: React.FC = () => { | |||
</Stack> | |||
</CardContent> | |||
</Card> | |||
<Card>Invoice | |||
<Card>Salary | |||
<CardContent> | |||
<Stack spacing={2}> | |||
<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 InvoiceSearch from "./InvoiceSearch"; | |||
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 { | |||
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; | |||
@@ -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 { exportSalary, importSalarys } from "@/app/api/salarys/actions"; | |||
import { downloadFile } from "@/app/utils/commonUtil"; | |||
import { errorDialog, successDialog } from "../Swal/CustomAlerts"; | |||
interface Props { | |||
salarys: SalaryResult[]; | |||
@@ -63,7 +64,13 @@ const SalarySearch: React.FC<Props> = ({ salarys }) => { | |||
const response = await importSalarys(formData); | |||
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) { | |||
@@ -74,20 +81,24 @@ const SalarySearch: React.FC<Props> = ({ salarys }) => { | |||
const handleExportClick = useCallback(async (event:any) => { | |||
// 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>[]>( | |||
() => [ | |||
{ | |||
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: "lowerLimit", label: t("Lower Limit") }, | |||
{ name: "upperLimit", label: t("Upper Limit") }, | |||
@@ -123,12 +134,19 @@ const SalarySearch: React.FC<Props> = ({ salarys }) => { | |||
<SearchBox | |||
criteria={searchCriteria} | |||
onSearch={(query) => { | |||
// console.log(Number(query.salaryPoint)) | |||
setFilteredSalarys( | |||
salarys.filter( | |||
(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) => { | |||
return Swal.fire({ | |||
icon: "warning", | |||