| @@ -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", | ||||