diff --git a/src/app/(main)/expense/page.tsx b/src/app/(main)/expense/page.tsx index 9d24cf1..bde27f2 100644 --- a/src/app/(main)/expense/page.tsx +++ b/src/app/(main)/expense/page.tsx @@ -1,11 +1,11 @@ -import { preloadExpense } from "@/app/api/expenses" +import { preloadProjectExpenses } from "@/app/api/projectExpenses" 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() + preloadProjectExpenses() const { t } = await getServerI18n("expense") return( diff --git a/src/app/api/expenses/index.ts b/src/app/api/expenses/index.ts deleted file mode 100644 index de7f985..0000000 --- a/src/app/api/expenses/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -"use server"; -import { cache } from "react"; -import { serverFetchJson } from "@/app/utils/fetchUtil"; -import { BASE_API_URL } from "@/config/api"; - -export type ExpensesResult = { - id: number - projectCode: string - projectName: string - staffCode: string - staffName: string - description: string - amount: number - approvedAmount: number - verifiedDatetime: number[] - remark: string - } - - -export const preloadExpense = () => { - 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/app/api/projectExpenses/actions.ts b/src/app/api/projectExpenses/actions.ts new file mode 100644 index 0000000..2344b2d --- /dev/null +++ b/src/app/api/projectExpenses/actions.ts @@ -0,0 +1,25 @@ +"use server" + +import { serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; +import { BASE_API_URL } from "@/config/api"; +import { cache } from "react"; +import { revalidateTag } from "next/cache"; + + +export type CreateNewExpense = { + expenseNo: string | undefined, + projectCode: string | undefined, + issueDate: Date + issuedAmount: number, + receiptDate: Date + receivedAmount: number + } + +export type PostExpenseData = { + expenseNo?: string + projectId: number + projectCode: string, + amount: number + issueDate: string + receiptDate?: string + } diff --git a/src/app/api/projectExpenses/index.ts b/src/app/api/projectExpenses/index.ts new file mode 100644 index 0000000..a112773 --- /dev/null +++ b/src/app/api/projectExpenses/index.ts @@ -0,0 +1,31 @@ +"use server"; +import { cache } from "react"; +import { serverFetchJson } from "@/app/utils/fetchUtil"; +import { BASE_API_URL } from "@/config/api"; + +export type ProjectExpensesResult = { + id: number + expenseNo?: string + projectCode: string + projectName: string + teamCode: string + teamName: string + amount: number + issueDate: number[] + receiptDate: number[] + + } +export type ProjectExpensesResultFormatted = Omit & { + issueDate: string; + receiptDate: string; +}; + +export const preloadProjectExpenses = () => { + fetchProjectExpenses() + }; + +export const fetchProjectExpenses = cache(async () => { + return serverFetchJson(`${BASE_API_URL}/project-expense`, { + next: { tags: ["projectExpenses"] }, + }); + }); \ No newline at end of file diff --git a/src/components/ExpenseSearch/CreateExpenseModal.tsx b/src/components/ExpenseSearch/CreateExpenseModal.tsx index d1a296c..cfb4f51 100644 --- a/src/components/ExpenseSearch/CreateExpenseModal.tsx +++ b/src/components/ExpenseSearch/CreateExpenseModal.tsx @@ -12,8 +12,11 @@ import { import { useTranslation } from 'react-i18next'; import { FormProvider, SubmitHandler, useForm } from 'react-hook-form'; import { Check, Close } from "@mui/icons-material"; -import InvoiceTable from './ExpenseTable'; import { ProjectResult } from '@/app/api/projects'; +import { CreateNewExpense, PostExpenseData } from '@/app/api/projectExpenses/actions'; +import ExpenseTable from './ExpenseTable'; +import dayjs from 'dayjs'; +import { INPUT_DATE_FORMAT } from '@/app/utils/formatUtil'; interface Props { isOpen: boolean, @@ -32,13 +35,30 @@ const modalSx: SxProps= { bgcolor: 'background.paper', }; -const CreateInvoiceModal: React.FC = ({isOpen, onClose, projects}) => { +type postData = { + data: PostExpenseData[] +} +const CreateExpenseModal: React.FC = ({isOpen, onClose, projects}) => { const { t } = useTranslation() - const formProps = useForm(); + const formProps = useForm(); - const onSubmit = useCallback>( + const onSubmit = useCallback>( (data) => { - console.log(data) + const _data = data.data + console.log(_data[0]) + console.log(_data[0].issueDate) + console.log(_data[1].issueDate) + const postData: PostExpenseData[] = _data.map(item => { + console.log(item.issueDate) + return ({ + expenseNo: item.expenseNo, + issueDate: dayjs(item.issueDate).format(INPUT_DATE_FORMAT), + amount: item.amount, + projectId: projects.find(p => p.code === item.projectCode)!.id, + projectCode: item.projectCode, + })} + ) + console.log(postData) } , []) @@ -63,7 +83,7 @@ const CreateInvoiceModal: React.FC = ({isOpen, onClose, projects}) => { marginBlock: 2, }} > - + @@ -134,11 +151,17 @@ const ExpenseSearch: React.FC = ({ expenses }) => { - + items={filteredExpenses} columns={columns} /> + toggleModals("createInvoiceModal")} + projects={projects} + /> + ); }; export default ExpenseSearch; diff --git a/src/components/ExpenseSearch/ExpenseSearchWrapper.tsx b/src/components/ExpenseSearch/ExpenseSearchWrapper.tsx index a39b2f7..af36018 100644 --- a/src/components/ExpenseSearch/ExpenseSearchWrapper.tsx +++ b/src/components/ExpenseSearch/ExpenseSearchWrapper.tsx @@ -1,10 +1,12 @@ import React from "react"; import ExpenseSearch from "./ExpenseSearch"; import ExpenseSearchLoading from "./ExpenseSearchLoading"; -import { fetchExpenses } from "@/app/api/expenses"; +import { fetchProjectExpenses } from "@/app/api/projectExpenses"; import dayjs from "dayjs"; import arraySupport from "dayjs/plugin/arraySupport"; import { OUTPUT_DATE_FORMAT, OUTPUT_TIME_FORMAT } from "@/app/utils/formatUtil"; +import { fetchUserStaff } from "@/app/utils/fetchUtil"; +import { fetchProjects } from "@/app/api/projects"; interface SubComponents { @@ -14,20 +16,31 @@ dayjs.extend(arraySupport) const ExpenseSearchWrapper: React.FC & SubComponents = async () => { const [ - Expenses + expenses, + projects ] = await Promise.all([ - fetchExpenses(), + fetchProjectExpenses(), + fetchProjects(), ]); - 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) + // const userStaff = await fetchUserStaff() + // const teamId = userStaff?.teamId + // const _projects = projects + + console.log(expenses) + const _expenses = expenses.map((e) => { + const issueDate = e.issueDate; + const receiptDate = e.receiptDate; + const formattedIssueDate = dayjs([issueDate[0], issueDate[1], issueDate[2]].join()).format(OUTPUT_DATE_FORMAT) + const formattedReceiptDate = dayjs([receiptDate[0], receiptDate[1], receiptDate[2]].join()).format(OUTPUT_DATE_FORMAT) return ({ ...e, - verifiedDatetime: formattedDate + issueDate: formattedIssueDate, + receiptDate: formattedReceiptDate }) }) return }; diff --git a/src/components/ExpenseSearch/ExpenseTable.tsx b/src/components/ExpenseSearch/ExpenseTable.tsx index 92cfae9..e17b32a 100644 --- a/src/components/ExpenseSearch/ExpenseTable.tsx +++ b/src/components/ExpenseSearch/ExpenseTable.tsx @@ -7,8 +7,6 @@ import { moneyFormatter } from "@/app/utils/formatUtil" import { Button, ButtonGroup, Stack, Tab, Tabs, TabsProps, Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions, TextField, CardContent, Typography, Divider, Card, Box, Autocomplete, MenuItem } from "@mui/material"; import FileUploadIcon from '@mui/icons-material/FileUpload'; import { Add, Check, Close, Delete } from "@mui/icons-material"; -import { deleteInvoice, importIssuedInovice, importReceivedInovice, updateInvoice } from "@/app/api/invoices/actions"; -import { deleteDialog, errorDialogWithContent, successDialog } from "../Swal/CustomAlerts"; import { invoiceList, issuedInvoiceList, issuedInvoiceSearchForm, receivedInvoiceList, receivedInvoiceSearchForm } from "@/app/api/invoices"; import EditOutlinedIcon from '@mui/icons-material/EditOutlined'; import { @@ -19,7 +17,8 @@ import { GridRowModel, GridRowModes, GridRowModesModel, - GridRenderEditCellParams, + GridRenderEditCellParams, + useGridApiContext, } from "@mui/x-data-grid"; import { useGridApiRef } from "@mui/x-data-grid"; import StyledDataGrid from "../StyledDataGrid"; @@ -32,15 +31,17 @@ import { th } from "@faker-js/faker"; import { GridRowIdGetter } from "@mui/x-data-grid"; import { useFormContext } from "react-hook-form"; import { ProjectResult } from "@/app/api/projects"; +import { ProjectExpensesResultFormatted } from "@/app/api/projectExpenses"; +import { GridRenderCellParams } from "@mui/x-data-grid"; -type InvoiceListError = { - [field in keyof invoiceList]?: string; +type ExpenseListError = { + [field in keyof ProjectExpensesResultFormatted]?: string; }; -type invoiceListRow = Partial< - invoiceList & { +type ExpenseListRow = Partial< + ProjectExpensesResultFormatted & { _isNew: boolean; - _error: InvoiceListError; + _error: ExpenseListError; } >; @@ -49,12 +50,12 @@ interface Props { } class ProcessRowUpdateError extends Error { - public readonly row: invoiceListRow; - public readonly errors: InvoiceListError | undefined; + public readonly row: ExpenseListRow; + public readonly errors: ExpenseListError | undefined; constructor( - row: invoiceListRow, + row: ExpenseListRow, message?: string, - errors?: InvoiceListError, + errors?: ExpenseListError, ) { super(message); this.row = row; @@ -67,34 +68,25 @@ type project = { label: string; value: number; } -const InvoiceTable: React.FC = ({ projects }) => { +const ExpenseTable: React.FC = ({ projects }) => { console.log(projects) + const projectCombos = projects.map(item => item.code) const { t } = useTranslation() const [rowModesModel, setRowModesModel] = useState({}); - const [selectedRow, setSelectedRow] = useState([]); + const [selectedRow, setSelectedRow] = useState([]); const { getValues, setValue, clearErrors, setError } = useFormContext(); const apiRef = useGridApiRef(); - const [projectCode, setProjectCode] = useState({label: "", value: 0}) - const validateInvoiceEntry = ( - entry: Partial, - ): InvoiceListError | undefined => { + const validateExpenseEntry = ( + entry: Partial, + ): ExpenseListError | undefined => { // Test for errors - const error: any = {}; - - 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){ - - } + const error: ExpenseListError = {}; + if (!entry.issueDate) error.issueDate = "Please input issued date"; + if (!entry.amount) error.amount = "Please input amount"; + if (!entry.projectCode) error.projectCode = "Please input project code"; + console.log(error) return Object.keys(error).length > 0 ? error : undefined; } @@ -105,8 +97,8 @@ const InvoiceTable: React.FC = ({ projects }) => { "", ) - const error = validateInvoiceEntry(row); - // console.log(error) + const error = validateExpenseEntry(row); + console.log(error) // Test for warnings // apiRef.current.updateRows([{ id, _error: error }]); @@ -159,8 +151,8 @@ const InvoiceTable: React.FC = ({ projects }) => { const processRowUpdate = useCallback( ( - newRow: GridRowModel, - originalRow: GridRowModel, + newRow: GridRowModel, + originalRow: GridRowModel, ) => { const errors = validateRow(newRow.id!!); if (errors) { @@ -178,7 +170,7 @@ const InvoiceTable: React.FC = ({ projects }) => { const rowToSave = { ...updatedRow, - } satisfies invoiceListRow; + } satisfies ExpenseListRow; console.log(newRow) @@ -198,7 +190,7 @@ const InvoiceTable: React.FC = ({ projects }) => { (updateError: ProcessRowUpdateError) => { const errors = updateError.errors; const oldRow = updateError.row; - // console.log(errors) + console.log(errors) apiRef.current.updateRows([{ ...oldRow, _error: errors }]); }, [apiRef] @@ -209,38 +201,64 @@ const InvoiceTable: React.FC = ({ projects }) => { setValue("data", selectedRow) }, [selectedRow, setValue]); + function renderAutocomplete(params: GridRenderCellParams) { + return( + + } + /> + + ) + } + + function AutocompleteInput(props: GridRenderCellParams) { + const { id, value, field, hasFocus } = props; + const apiRef = useGridApiContext(); + const ref = React.useRef(null); + + const handleValueChange = useCallback((newValue: any) => { + console.log(newValue) + apiRef.current.setEditCellValue({ id, field, value: newValue }) + }, []); + + return ( + + , value: string | null, ) => handleValueChange(value)} + renderInput={(params) => } + /> + + ); + } + + const renderAutocompleteInput: GridColDef['renderCell'] = (params) => { + return ; + }; const editCombinedColumns = useMemo( () => [ - { field: "invoiceNo", headerName: t("Invoice No"), editable: true, flex: 0.5 }, + { field: "expenseNo", headerName: t("Expense No"), editable: true, flex: 0.5 }, { field: "projectCode", headerName: t("Project Code"), editable: true, flex: 0.3, - renderEditCell(params: GridRenderEditCellParams){ - return( - } - /> - - ) - - } + renderCell: renderAutocomplete, + renderEditCell: renderAutocompleteInput }, - { field: "issuedDate", + { field: "issueDate", headerName: t("Issue Date"), editable: true, flex: 0.4, - // type: 'date', - // valueGetter: (params) => { - // // console.log(params.row.issuedDate) - // return new Date(params.row.issuedDate) - // }, + type: 'date', }, - { field: "issuedAmount", + { field: "amount", headerName: t("Amount (HKD)"), editable: true, flex: 0.5, @@ -251,13 +269,8 @@ const editCombinedColumns = useMemo( headerName: t("Settle Date"), editable: true, flex: 0.4, - }, - { field: "receivedAmount", - headerName: t("Actual Received Amount (HKD)"), - editable: true, - flex: 0.5, - type: 'number' - }, + type: 'date', + }, ], [t] ) @@ -271,7 +284,7 @@ const footer = ( onClick={addRow} size="small" > - {t("Create Invoice")} + {t("Create Expense")} ); @@ -301,9 +314,9 @@ const footer = ( columns={editCombinedColumns} processRowUpdate={processRowUpdate} onProcessRowUpdateError={onProcessRowUpdateError} - getCellClassName={(params: GridCellParams) => { + getCellClassName={(params: GridCellParams) => { let classname = ""; - if (params.row._error?.[params.field as keyof invoiceList]) { + if (params.row._error?.[params.field as keyof ProjectExpensesResultFormatted]) { classname = "hasError"; } return classname; @@ -320,7 +333,7 @@ const footer = ( ) } -export default InvoiceTable +export default ExpenseTable const NoRowsOverlay: React.FC = () => { const { t } = useTranslation("home"); diff --git a/src/components/InvoiceSearch/InvoiceSearchWrapper.tsx b/src/components/InvoiceSearch/InvoiceSearchWrapper.tsx index 8cef526..4f9d0eb 100644 --- a/src/components/InvoiceSearch/InvoiceSearchWrapper.tsx +++ b/src/components/InvoiceSearch/InvoiceSearchWrapper.tsx @@ -19,18 +19,18 @@ interface SubComponents { // } const InvoiceSearchWrapper: React.FC & SubComponents = async () => { - const userStaff = await fetchUserStaff() - const teamId = userStaff?.teamId + // const userStaff = await fetchUserStaff() + // const teamId = userStaff?.teamId const invoices = await fetchInvoicesV3() const projects = await fetchProjects() - const filteredProjects = projects.filter(project => project.teamId === teamId) + // const filteredProjects = projects.filter(project => project.teamId === teamId) - let filteredInvoice = invoices - if (teamId) { - filteredInvoice = invoices.filter(invoice => invoice.teamId === teamId) - } + // let filteredInvoice = invoices + // if (teamId) { + // filteredInvoice = invoices.filter(invoice => invoice.teamId === teamId) + // } - const convertedInvoices = filteredInvoice.map((invoice)=>{ + const convertedInvoices = invoices.map((invoice)=>{ return{ id: invoice.id, invoiceNo: invoice.invoiceNo, @@ -47,7 +47,7 @@ const InvoiceSearchWrapper: React.FC & SubComponents = async () => { return };