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