diff --git a/src/app/(main)/expense/page.tsx b/src/app/(main)/expense/page.tsx index 6cf1645..9d24cf1 100644 --- a/src/app/(main)/expense/page.tsx +++ b/src/app/(main)/expense/page.tsx @@ -1,15 +1,16 @@ +import { preloadExpense } from "@/app/api/expenses" import ExpenseSearch from "@/components/ExpenseSearch" import { getServerI18n, I18nProvider } from "@/i18n" import { Stack, Typography } from "@mui/material" import { Suspense } from "react" const Expense: React.FC = async () => { + preloadExpense() const { t } = await getServerI18n("expense") return( { + fetchExpenses() + }; + +export const fetchExpenses = cache(async () => { + return serverFetchJson(`${BASE_API_URL}/expense`, { + next: { tags: ["expense"] }, + }); + }); \ No newline at end of file diff --git a/src/components/ExpenseSearch/ExpenseSearch.tsx b/src/components/ExpenseSearch/ExpenseSearch.tsx index 8a0dc1d..d1ce72a 100644 --- a/src/components/ExpenseSearch/ExpenseSearch.tsx +++ b/src/components/ExpenseSearch/ExpenseSearch.tsx @@ -1,443 +1,144 @@ "use client"; - -import React, { useCallback, useMemo, useState } from "react"; -import SearchBox, { Criterion } from "../SearchBox"; +import { ExpensesResult } from "@/app/api/expenses"; +import { useCallback, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; +import SearchBox, { Criterion } from "../SearchBox"; import SearchResults, { Column } from "../SearchResults"; -import { moneyFormatter } from "@/app/utils/formatUtil" -import { - Button, ButtonGroup, Stack, - Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions, - CardContent, Typography, Divider, Card +import { useRouter } from "next/navigation"; +import { + Button, + ButtonGroup, + Card, + CardContent, + Divider, + Grid, + Stack, + Typography, } from "@mui/material"; +import { moneyFormatter } from "@/app/utils/formatUtil"; +import { EditNote } from "@mui/icons-material"; import AddIcon from '@mui/icons-material/Add'; -import { deleteInvoice, updateInvoice } from "@/app/api/invoices/actions"; -import { deleteDialog, errorDialogWithContent, successDialog } from "../Swal/CustomAlerts"; -import { invoiceList, issuedInvoiceList, issuedInvoiceSearchForm, receivedInvoiceList } from "@/app/api/invoices"; -import EditOutlinedIcon from '@mui/icons-material/EditOutlined'; -import { GridCellParams, GridColDef, GridEventListener, GridRowId, GridRowModes, GridRowModesModel } from "@mui/x-data-grid"; -import { useGridApiRef } from "@mui/x-data-grid"; -import StyledDataGrid from "../StyledDataGrid"; - -import { uniq } from "lodash"; -import CreateInvoiceModal from "./CreateExpenseModal"; -import { ProjectResult } from "@/app/api/projects"; - - interface Props { - invoices: invoiceList[]; - projects: ProjectResult[]; + expenses: ExpensesResult[] } -type InvoiceListError = { - [field in keyof invoiceList]?: string; -}; - -type invoiceListRow = Partial< - invoiceList & { - _isNew: boolean; - _error: InvoiceListError; - } ->; - -type SearchQuery = Partial>; +type SearchQuery = Partial>; type SearchParamNames = keyof SearchQuery; - -const ExpenseSearch: React.FC = ({ invoices, projects }) => { - // console.log(invoices) - const { t } = useTranslation("expense"); - - - const [filteredIvoices, setFilterInovices] = useState(invoices); +const ExpenseSearch: React.FC = ({ expenses }) => { + console.log(expenses) + const router = useRouter(); + const { t } = useTranslation("expenses"); + const [filteredExpenses, setFilteredExpenses] = useState(expenses); const searchCriteria: Criterion[] = useMemo( () => [ - { label: t("Invoice No"), paramName: "invoiceNo", type: "text" }, + // { label: t("Expense No"), paramName: "ExpenseNo", type: "text" }, { label: t("Project Code"), paramName: "projectCode", type: "text" }, + { label: t("Project Name"), paramName: "projectName", type: "text" }, { - label: t("Team"), - paramName: "team", - type: "select", - options: uniq(invoices.map((invoice) => invoice.teamCodeName)), + label: t("Verified Date"), + label2: t("Verified Date To"), + paramName: "verifiedDatetime", + type: "dateRange", }, - { label: t("Issue Date"), label2: t("Issue Date To"), paramName: "invoiceDate", type: "dateRange" }, - { label: t("Settle Date"), label2: t("Settle Date To"), paramName: "dueDate", type: "dateRange" }, - ], - [t, invoices], - ); - - const onReset = useCallback(() => { - setFilterInovices(invoices) - }, [invoices]); - - const [modelOpen, setModelOpen] = useState(false); - - const handleAddInvoiceClick = useCallback(() => { - setModelOpen(true) - },[]) - - const handleModalClose = useCallback(() => { - setModelOpen(false) - },[]) - - const showErrorDialog = (title: string, content: string) => { - errorDialogWithContent(title, content, t).then(() => { - window.location.reload(); - }); - }; - - const columns = useMemo[]>( - () => [ - { name: "invoiceNo", label: t("Invoice No") }, - { name: "projectCode", label: t("Project Code") }, - { name: "stage", label: t("Stage") }, - { name: "paymentMilestone", label: t("Payment Milestone") }, - { name: "invoiceDate", label: t("Invoice Date") }, - { name: "dueDate", label: t("Due Date") }, - { name: "issuedAmount", label: t("Amount (HKD)") }, ], - [t], + [] ); - const columns2 = useMemo[]>( - () => [ - { name: "invoiceNo", label: t("Invoice No") }, - { name: "projectCode", label: t("Project Code") }, - { name: "projectName", label: t("Project Name") }, - { name: "team", label: t("Team") }, - { name: "receiptDate", label: t("Receipt Date") }, - { name: "receivedAmount", label: t("Amount (HKD)") }, - ], - [t], + const onExpenseClick = useCallback( + (expenses?: ExpensesResult) => {}, + [router] ); - const [selectedRow, setSelectedRow] = useState([]); - const [dialogOpen, setDialogOpen] = useState(false); - - const handleButtonClick = (row: invoiceList) => { - console.log(row) - setSelectedRow([row]); - setDialogOpen(true); - setRowModesModel((model) => ({ - ...model, - [row.id]: { mode: GridRowModes.Edit, fieldToFocus: "issuedAmount" }, - })); - }; - - const handleCloseDialog = () => { - setDialogOpen(false); - }; - - const handleDeleteInvoice = useCallback(() => { - deleteDialog(async() => { - //console.log(selectedRow[0]) - await deleteInvoice(selectedRow[0].id!!) - setDialogOpen(false); - const result = await successDialog("Delete Success", t); - if (result) { - window.location.reload() - } - }, t) - }, [selectedRow]); - - - const handleSaveDialog = async () => { - await updateInvoice(selectedRow[0]) - setDialogOpen(false); - successDialog(t("Update Success"), t).then(() => { - window.location.reload() - }) - }; - - const combinedColumns = useMemo[]>( + const columns = useMemo[]>( () => [ - { - name: "invoiceNo", - label: t("Edit"), - onClick: (row: invoiceList) => ( - handleButtonClick(row) - ), - buttonIcon: + { + name: "id", + label: t("Details"), + onClick: onExpenseClick, + buttonIcon: , + // disabled: !abilities.includes(MAINTAIN_PROJECT), }, - { name: "invoiceNo", label: t("Invoice No") }, { name: "projectCode", label: t("Project Code") }, { name: "projectName", label: t("Project Name") }, - { name: "team", label: t("Team") }, - { name: "issuedDate", label: t("Issue Date") }, - { name: "issuedAmount", label: t("Amount (HKD)"), type: 'money', needTranslation: true }, - { name: "receiptDate", label: t("Settle Date") }, - { name: "receivedAmount", label: t("Actual Received Amount (HKD)"), type: 'money', needTranslation: true }, - ], - [t] - ) - - const editCombinedColumns = useMemo( - () => [ - { field: "invoiceNo", headerName: t("Invoice No"), editable: true, flex: 0.5 }, - { field: "projectCode", headerName: t("Project Code"), editable: false, flex: 0.3 }, - { field: "projectName", headerName: t("Project Name"), flex: 1 }, - { field: "team", headerName: t("Team"), flex: 0.2 }, - { field: "issuedDate", - headerName: t("Issue Date"), - editable: true, - flex: 0.4, - // type: 'date', - // valueGetter: (params) => { - // // console.log(params.row.issuedDate) - // return new Date(params.row.issuedDate) - // }, - }, - { field: "issuedAmount", - headerName: t("Amount (HKD)"), - editable: true, - flex: 0.5, - type: 'number' - }, - { - field: "receiptDate", - headerName: t("Settle Date"), - editable: true, - flex: 0.4, - // renderCell: (params) => { - // console.log(params) - // return ( - // - // - // - // ); - // } - }, - { field: "receivedAmount", - headerName: t("Actual Received Amount (HKD)"), - editable: true, - flex: 0.5, - type: 'number' - }, + { name: "verifiedDatetime", label: t("verifiedDatetime") }, ], - [t] - ) - - function isDateInRange(dateToCheck: string, startDate: string, endDate: string): boolean { - - if ((!startDate || startDate === "Invalid Date") && (!endDate || endDate === "Invalid Date")) { - return true; - } - - const dateToCheckObj = new Date(dateToCheck); - const startDateObj = new Date(startDate); - const endDateObj = new Date(endDate); - - return ((!startDate || startDate === "Invalid Date") || dateToCheckObj >= startDateObj) && ((!endDate || endDate === "Invalid Date") || dateToCheckObj <= endDateObj); - } - - const [rowModesModel, setRowModesModel] = useState({}); - const apiRef = useGridApiRef(); - - const validateInvoiceEntry = ( - entry: Partial, - ): InvoiceListError | undefined => { - // Test for errors - const error: InvoiceListError = {}; - - console.log(entry) - if (!entry.issuedAmount) { - error.issuedAmount = "Please input issued amount "; - } else if (!entry.issuedAmount) { - error.receivedAmount = "Please input received amount"; - } else if (entry.invoiceNo === "") { - error.invoiceNo = "Please input invoice number"; - } else if (!entry.issuedDate) { - error.issuedDate = "Please input issue date"; - } else if (!entry.receiptDate){ - error.receiptDate = "Please input receipt date"; - } - - - return Object.keys(error).length > 0 ? error : undefined; - } - - const validateRow = useCallback( - (id: GridRowId) => { - const row = apiRef.current.getRowWithUpdatedValues( - id, - "", - ) - - const error = validateInvoiceEntry(row); - console.log(error) - // Test for warnings - - apiRef.current.updateRows([{ id, _error: error }]); - return !error; - }, - [apiRef], - ); - - const handleEditStop = useCallback>( - (params, event) => { - // console.log(params.id) - if (validateRow(params.id) !== undefined || !validateRow(params.id)) { - - setRowModesModel((model) => ({ - ...model, - [params.id]: { mode: GridRowModes.View}, - })); - - const row = apiRef.current.getRowWithUpdatedValues( - params.id, - "", - ) - console.log(row) - setSelectedRow([{...row}] as invoiceList[]) - event.defaultMuiPrevented = true; - } - // console.log(row) - }, - [validateRow], + [t, onExpenseClick] ); + const onReset = useCallback(() => { + // setFilteredExpenses(); + }, []); return ( - <> - - - - - { - // tabIndex == 0 && - { - // console.log(query) - setFilterInovices( - invoices.filter( - (s) => (s.invoiceNo.toLowerCase().includes(query.invoiceNo.toLowerCase())) - && (s.projectCode.toLowerCase().includes(query.projectCode.toLowerCase())) - && (query.team === "All" || query.team.toLowerCase().includes(s.team.toLowerCase())) - && (isDateInRange(s.issuedDate, query.invoiceDate ?? undefined, query.invoiceDateTo ?? undefined)) - && (isDateInRange(s.receiptDate, query.dueDate ?? undefined, query.dueDateTo ?? undefined)) - ), - ); - }} - onReset={onReset} - /> - } - + + + + + + + + { + // setFilteredExpenses( + // projects.filter( + // (p) => + // p.code.toLowerCase().includes(query.code.toLowerCase()) && + // p.name.toLowerCase().includes(query.name.toLowerCase()) && + // (query.client === "All" || p.client === query.client) && + // (query.category === "All" || p.category === query.category) && + // // (query.team === "All" || p.team === query.team) && + // (query.team === "All" || query.team.toLowerCase().includes(p.team.toLowerCase())) && + // (query.status === "All" || p.status === query.status), + // ), + // ); + }} + onReset={onReset} + /> - - {t('Total Issued Amount (HKD)')}: - {moneyFormatter.format(filteredIvoices.reduce((acc, current) => (acc + current.issuedAmount), 0))} - - - {t('Total Received Amount (HKD)')}: - {moneyFormatter.format(filteredIvoices.reduce((acc, current) => (acc + current.receivedAmount), 0))} - + + + {t("Total Issued Amount (HKD)")}: + + + {/* {moneyFormatter.format(filteredExpenses.reduce((acc, curr) => (acc + curr.issuedAmount), 0))} */} + + + + + {t("Total Received Amount (HKD)")}: + + + {/* {moneyFormatter.format(filteredExpenses.reduce((acc, curr) => (acc + curr.receivedAmount), 0))} */} + + - - - - items={filteredIvoices} - columns={combinedColumns} - autoRedirectToFirstPage - /> - - - {t("Edit Invoice")} - - - {t("You can edit the invoice details here.")} - - {/* - items={selectedRow ? [selectedRow] : []} - columns={editCombinedColumns} - /> */} - .MuiDataGrid-cell': { - overflow: 'auto', - whiteSpace: 'nowrap', - textWrap: 'pretty', - }, - width: "100%", // Make the DataGrid wider - }} - disableColumnMenu - editMode="row" - rows={selectedRow} - rowModesModel={rowModesModel} - onRowModesModelChange={setRowModesModel} - onRowEditStop={handleEditStop} - columns={editCombinedColumns} - getCellClassName={(params: GridCellParams) => { - let classname = ""; - if (params.row._error?.[params.field as keyof invoiceList]) { - classname = "hasError"; - } - return classname; - }} + + items={filteredExpenses} + columns={columns} /> - - - - - - - - - + ); }; - export default ExpenseSearch; diff --git a/src/components/ExpenseSearch/ExpenseSearchWrapper.tsx b/src/components/ExpenseSearch/ExpenseSearchWrapper.tsx index 1cc2282..a39b2f7 100644 --- a/src/components/ExpenseSearch/ExpenseSearchWrapper.tsx +++ b/src/components/ExpenseSearch/ExpenseSearchWrapper.tsx @@ -1,18 +1,33 @@ import React from "react"; import ExpenseSearch from "./ExpenseSearch"; import ExpenseSearchLoading from "./ExpenseSearchLoading"; +import { fetchExpenses } from "@/app/api/expenses"; +import dayjs from "dayjs"; +import arraySupport from "dayjs/plugin/arraySupport"; +import { OUTPUT_DATE_FORMAT, OUTPUT_TIME_FORMAT } from "@/app/utils/formatUtil"; interface SubComponents { Loading: typeof ExpenseSearchLoading; } +dayjs.extend(arraySupport) const ExpenseSearchWrapper: React.FC & SubComponents = async () => { - - + const [ + Expenses + ] = await Promise.all([ + fetchExpenses(), + ]); + const _expenses = Expenses.map((e) => { + const date: number[] = e.verifiedDatetime as number[]; + const formattedDate = dayjs([date[0], date[1], date[2]].join()).format(OUTPUT_DATE_FORMAT) + return ({ + ...e, + verifiedDatetime: formattedDate + }) + }) return }; diff --git a/src/components/InvoiceSearch/InvoiceTable.tsx b/src/components/InvoiceSearch/InvoiceTable.tsx index 3f33f44..42a7b2b 100644 --- a/src/components/InvoiceSearch/InvoiceTable.tsx +++ b/src/components/InvoiceSearch/InvoiceTable.tsx @@ -58,11 +58,8 @@ type project = { value: number; } const InvoiceTable: React.FC = ({ projects }) => { - console.log(projects) - // const projectCombos: project[] = projects.map(item => ({ - // value: item.id, - // label: item.code - // })) + // if change this to code - name, + // also change the submit function const projectCombos = projects.map(item => item.code) const { t } = useTranslation() const [rowModesModel, setRowModesModel] = useState({});