| @@ -1,11 +1,11 @@ | |||||
| import { preloadExpense } from "@/app/api/expenses" | |||||
| import { preloadProjectExpenses } from "@/app/api/projectExpenses" | |||||
| import ExpenseSearch from "@/components/ExpenseSearch" | import ExpenseSearch from "@/components/ExpenseSearch" | ||||
| import { getServerI18n, I18nProvider } from "@/i18n" | import { getServerI18n, I18nProvider } from "@/i18n" | ||||
| import { Stack, Typography } from "@mui/material" | import { Stack, Typography } from "@mui/material" | ||||
| import { Suspense } from "react" | import { Suspense } from "react" | ||||
| const Expense: React.FC = async () => { | const Expense: React.FC = async () => { | ||||
| preloadExpense() | |||||
| preloadProjectExpenses() | |||||
| const { t } = await getServerI18n("expense") | const { t } = await getServerI18n("expense") | ||||
| return( | return( | ||||
| @@ -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<ExpensesResult[]>(`${BASE_API_URL}/expense`, { | |||||
| next: { tags: ["expense"] }, | |||||
| }); | |||||
| }); | |||||
| @@ -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 | |||||
| } | |||||
| @@ -0,0 +1,32 @@ | |||||
| "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 | |||||
| teamId: number | |||||
| teamCode: string | |||||
| teamName: string | |||||
| amount: number | |||||
| issueDate: number[] | |||||
| receiptDate: number[] | |||||
| } | |||||
| export type ProjectExpensesResultFormatted = Omit<ProjectExpensesResult, 'issueDate' | 'receiptDate'> & { | |||||
| issueDate: string; | |||||
| receiptDate: string; | |||||
| }; | |||||
| export const preloadProjectExpenses = () => { | |||||
| fetchProjectExpenses() | |||||
| }; | |||||
| export const fetchProjectExpenses = cache(async () => { | |||||
| return serverFetchJson<ProjectExpensesResult[]>(`${BASE_API_URL}/project-expense`, { | |||||
| next: { tags: ["projectExpenses"] }, | |||||
| }); | |||||
| }); | |||||
| @@ -12,8 +12,11 @@ import { | |||||
| import { useTranslation } from 'react-i18next'; | import { useTranslation } from 'react-i18next'; | ||||
| import { FormProvider, SubmitHandler, useForm } from 'react-hook-form'; | import { FormProvider, SubmitHandler, useForm } from 'react-hook-form'; | ||||
| import { Check, Close } from "@mui/icons-material"; | import { Check, Close } from "@mui/icons-material"; | ||||
| import InvoiceTable from './ExpenseTable'; | |||||
| import { ProjectResult } from '@/app/api/projects'; | 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 { | interface Props { | ||||
| isOpen: boolean, | isOpen: boolean, | ||||
| @@ -32,13 +35,30 @@ const modalSx: SxProps= { | |||||
| bgcolor: 'background.paper', | bgcolor: 'background.paper', | ||||
| }; | }; | ||||
| const CreateInvoiceModal: React.FC<Props> = ({isOpen, onClose, projects}) => { | |||||
| type postData = { | |||||
| data: PostExpenseData[] | |||||
| } | |||||
| const CreateExpenseModal: React.FC<Props> = ({isOpen, onClose, projects}) => { | |||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| const formProps = useForm<any>(); | |||||
| const formProps = useForm<postData>(); | |||||
| const onSubmit = useCallback<SubmitHandler<any>>( | |||||
| const onSubmit = useCallback<SubmitHandler<postData>>( | |||||
| (data) => { | (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<Props> = ({isOpen, onClose, projects}) => { | |||||
| marginBlock: 2, | marginBlock: 2, | ||||
| }} | }} | ||||
| > | > | ||||
| <InvoiceTable projects={projects}/> | |||||
| <ExpenseTable projects={projects}/> | |||||
| </Box> | </Box> | ||||
| <CardActions sx={{ justifyContent: "flex-end" }}> | <CardActions sx={{ justifyContent: "flex-end" }}> | ||||
| <Button | <Button | ||||
| @@ -84,4 +104,4 @@ const CreateInvoiceModal: React.FC<Props> = ({isOpen, onClose, projects}) => { | |||||
| ); | ); | ||||
| }; | }; | ||||
| export default CreateInvoiceModal; | |||||
| export default CreateExpenseModal; | |||||
| @@ -1,6 +1,6 @@ | |||||
| "use client"; | "use client"; | ||||
| import { ExpensesResult } from "@/app/api/expenses"; | |||||
| import { useCallback, useMemo, useState } from "react"; | |||||
| import { ProjectExpensesResult, ProjectExpensesResultFormatted } from "@/app/api/projectExpenses"; | |||||
| import { useCallback, useEffect, useMemo, useState } from "react"; | |||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
| import SearchBox, { Criterion } from "../SearchBox"; | import SearchBox, { Criterion } from "../SearchBox"; | ||||
| import SearchResults, { Column } from "../SearchResults"; | import SearchResults, { Column } from "../SearchResults"; | ||||
| @@ -18,41 +18,55 @@ import { | |||||
| import { moneyFormatter } from "@/app/utils/formatUtil"; | import { moneyFormatter } from "@/app/utils/formatUtil"; | ||||
| import { EditNote } from "@mui/icons-material"; | import { EditNote } from "@mui/icons-material"; | ||||
| import AddIcon from '@mui/icons-material/Add'; | import AddIcon from '@mui/icons-material/Add'; | ||||
| import { uniq } from "lodash"; | |||||
| import CreateExpenseModal from "./CreateExpenseModal"; | |||||
| import { ProjectResult } from "@/app/api/projects"; | |||||
| interface Props { | interface Props { | ||||
| expenses: ExpensesResult[] | |||||
| expenses: ProjectExpensesResultFormatted[] | |||||
| projects: ProjectResult[]; | |||||
| } | } | ||||
| type SearchQuery = Partial<Omit<ExpensesResult, "id">>; | |||||
| type SearchQuery = Partial<Omit<ProjectExpensesResultFormatted, "id">>; | |||||
| type SearchParamNames = keyof SearchQuery; | type SearchParamNames = keyof SearchQuery; | ||||
| const ExpenseSearch: React.FC<Props> = ({ expenses }) => { | |||||
| console.log(expenses) | |||||
| type Modals = { | |||||
| createInvoiceModal: boolean | |||||
| } | |||||
| const initState: Modals = { | |||||
| createInvoiceModal: false, | |||||
| } | |||||
| const ExpenseSearch: React.FC<Props> = ({ expenses, projects }) => { | |||||
| const router = useRouter(); | const router = useRouter(); | ||||
| const { t } = useTranslation("expenses"); | const { t } = useTranslation("expenses"); | ||||
| const [filteredExpenses, setFilteredExpenses] = useState(expenses); | const [filteredExpenses, setFilteredExpenses] = useState(expenses); | ||||
| const [modalsOpen, setModalsOpen] = useState(initState) | |||||
| const toggleModals = useCallback((key: keyof Modals) => { | |||||
| setModalsOpen((prev) => ({...prev, [key]: !prev[key]})) | |||||
| }, [modalsOpen]); | |||||
| const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | ||||
| () => [ | () => [ | ||||
| // { label: t("Expense No"), paramName: "ExpenseNo", type: "text" }, | // { label: t("Expense No"), paramName: "ExpenseNo", type: "text" }, | ||||
| { label: t("Project Code"), paramName: "projectCode", type: "text" }, | { label: t("Project Code"), paramName: "projectCode", type: "text" }, | ||||
| { label: t("Project Name"), paramName: "projectName", type: "text" }, | { label: t("Project Name"), paramName: "projectName", type: "text" }, | ||||
| { | |||||
| label: t("Verified Date"), | |||||
| label2: t("Verified Date To"), | |||||
| paramName: "verifiedDatetime", | |||||
| type: "dateRange", | |||||
| }, | |||||
| // { | |||||
| // label: t("Team"), | |||||
| // paramName: "team", | |||||
| // type: "select", | |||||
| // options: uniq(expenses.map((expenses) => expenses.teamCode)), | |||||
| // }, | |||||
| ], | ], | ||||
| [] | [] | ||||
| ); | ); | ||||
| const onExpenseClick = useCallback( | const onExpenseClick = useCallback( | ||||
| (expenses?: ExpensesResult) => {}, | |||||
| (expenses?: ProjectExpensesResultFormatted) => {}, | |||||
| [router] | [router] | ||||
| ); | ); | ||||
| const columns = useMemo<Column<ExpensesResult>[]>( | |||||
| const columns = useMemo<Column<ProjectExpensesResultFormatted>[]>( | |||||
| () => [ | () => [ | ||||
| { | { | ||||
| name: "id", | name: "id", | ||||
| @@ -63,7 +77,9 @@ const ExpenseSearch: React.FC<Props> = ({ expenses }) => { | |||||
| }, | }, | ||||
| { name: "projectCode", label: t("Project Code") }, | { name: "projectCode", label: t("Project Code") }, | ||||
| { name: "projectName", label: t("Project Name") }, | { name: "projectName", label: t("Project Name") }, | ||||
| { name: "verifiedDatetime", label: t("verifiedDatetime") }, | |||||
| { name: "amount", label: t("Amount") }, | |||||
| { name: "teamCode", label: t("Team") }, | |||||
| { name: "issueDate", label: t("Issue Date") }, | |||||
| ], | ], | ||||
| [t, onExpenseClick] | [t, onExpenseClick] | ||||
| ); | ); | ||||
| @@ -72,6 +88,7 @@ const ExpenseSearch: React.FC<Props> = ({ expenses }) => { | |||||
| }, []); | }, []); | ||||
| return ( | return ( | ||||
| <> | |||||
| <Stack | <Stack | ||||
| spacing={2} | spacing={2} | ||||
| > | > | ||||
| @@ -87,7 +104,7 @@ const ExpenseSearch: React.FC<Props> = ({ expenses }) => { | |||||
| startIcon={<AddIcon />} | startIcon={<AddIcon />} | ||||
| variant="outlined" | variant="outlined" | ||||
| component="label" | component="label" | ||||
| onClick={() => console.log()} | |||||
| onClick={() => toggleModals("createInvoiceModal")} | |||||
| > | > | ||||
| {t("Create expense")} | {t("Create expense")} | ||||
| </Button> | </Button> | ||||
| @@ -134,11 +151,17 @@ const ExpenseSearch: React.FC<Props> = ({ expenses }) => { | |||||
| </CardContent> | </CardContent> | ||||
| </Card> | </Card> | ||||
| <Divider sx={{ paddingBlockEnd: 2 }} /> | <Divider sx={{ paddingBlockEnd: 2 }} /> | ||||
| <SearchResults<ExpensesResult> | |||||
| <SearchResults<ProjectExpensesResultFormatted> | |||||
| items={filteredExpenses} | items={filteredExpenses} | ||||
| columns={columns} | columns={columns} | ||||
| /> | /> | ||||
| </Stack> | </Stack> | ||||
| <CreateExpenseModal | |||||
| isOpen={modalsOpen.createInvoiceModal} | |||||
| onClose={() => toggleModals("createInvoiceModal")} | |||||
| projects={projects} | |||||
| /> | |||||
| </> | |||||
| ); | ); | ||||
| }; | }; | ||||
| export default ExpenseSearch; | export default ExpenseSearch; | ||||
| @@ -1,10 +1,12 @@ | |||||
| import React from "react"; | import React from "react"; | ||||
| import ExpenseSearch from "./ExpenseSearch"; | import ExpenseSearch from "./ExpenseSearch"; | ||||
| import ExpenseSearchLoading from "./ExpenseSearchLoading"; | import ExpenseSearchLoading from "./ExpenseSearchLoading"; | ||||
| import { fetchExpenses } from "@/app/api/expenses"; | |||||
| import { fetchProjectExpenses } from "@/app/api/projectExpenses"; | |||||
| import dayjs from "dayjs"; | import dayjs from "dayjs"; | ||||
| import arraySupport from "dayjs/plugin/arraySupport"; | import arraySupport from "dayjs/plugin/arraySupport"; | ||||
| import { OUTPUT_DATE_FORMAT, OUTPUT_TIME_FORMAT } from "@/app/utils/formatUtil"; | 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 { | interface SubComponents { | ||||
| @@ -14,20 +16,36 @@ dayjs.extend(arraySupport) | |||||
| const ExpenseSearchWrapper: React.FC & SubComponents = async () => { | const ExpenseSearchWrapper: React.FC & SubComponents = async () => { | ||||
| const [ | const [ | ||||
| Expenses | |||||
| expenses, | |||||
| projects | |||||
| ] = await Promise.all([ | ] = 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 | |||||
| let filteredExpenses = expenses | |||||
| if (teamId) { | |||||
| filteredExpenses = expenses.filter(e => e.teamId === teamId) | |||||
| } else { | |||||
| filteredExpenses = [] | |||||
| } | |||||
| const _expenses = filteredExpenses.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 ({ | return ({ | ||||
| ...e, | ...e, | ||||
| verifiedDatetime: formattedDate | |||||
| issueDate: formattedIssueDate, | |||||
| receiptDate: formattedReceiptDate | |||||
| }) | }) | ||||
| }) | }) | ||||
| return <ExpenseSearch | return <ExpenseSearch | ||||
| expenses={_expenses} | expenses={_expenses} | ||||
| projects={projects} | |||||
| /> | /> | ||||
| }; | }; | ||||
| @@ -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 { 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 FileUploadIcon from '@mui/icons-material/FileUpload'; | ||||
| import { Add, Check, Close, Delete } from "@mui/icons-material"; | 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 { invoiceList, issuedInvoiceList, issuedInvoiceSearchForm, receivedInvoiceList, receivedInvoiceSearchForm } from "@/app/api/invoices"; | ||||
| import EditOutlinedIcon from '@mui/icons-material/EditOutlined'; | import EditOutlinedIcon from '@mui/icons-material/EditOutlined'; | ||||
| import { | import { | ||||
| @@ -19,7 +17,8 @@ import { | |||||
| GridRowModel, | GridRowModel, | ||||
| GridRowModes, | GridRowModes, | ||||
| GridRowModesModel, | GridRowModesModel, | ||||
| GridRenderEditCellParams, | |||||
| GridRenderEditCellParams, | |||||
| useGridApiContext, | |||||
| } from "@mui/x-data-grid"; | } from "@mui/x-data-grid"; | ||||
| import { useGridApiRef } from "@mui/x-data-grid"; | import { useGridApiRef } from "@mui/x-data-grid"; | ||||
| import StyledDataGrid from "../StyledDataGrid"; | import StyledDataGrid from "../StyledDataGrid"; | ||||
| @@ -32,15 +31,17 @@ import { th } from "@faker-js/faker"; | |||||
| import { GridRowIdGetter } from "@mui/x-data-grid"; | import { GridRowIdGetter } from "@mui/x-data-grid"; | ||||
| import { useFormContext } from "react-hook-form"; | import { useFormContext } from "react-hook-form"; | ||||
| import { ProjectResult } from "@/app/api/projects"; | 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; | _isNew: boolean; | ||||
| _error: InvoiceListError; | |||||
| _error: ExpenseListError; | |||||
| } | } | ||||
| >; | >; | ||||
| @@ -49,12 +50,12 @@ interface Props { | |||||
| } | } | ||||
| class ProcessRowUpdateError extends Error { | class ProcessRowUpdateError extends Error { | ||||
| public readonly row: invoiceListRow; | |||||
| public readonly errors: InvoiceListError | undefined; | |||||
| public readonly row: ExpenseListRow; | |||||
| public readonly errors: ExpenseListError | undefined; | |||||
| constructor( | constructor( | ||||
| row: invoiceListRow, | |||||
| row: ExpenseListRow, | |||||
| message?: string, | message?: string, | ||||
| errors?: InvoiceListError, | |||||
| errors?: ExpenseListError, | |||||
| ) { | ) { | ||||
| super(message); | super(message); | ||||
| this.row = row; | this.row = row; | ||||
| @@ -67,34 +68,25 @@ type project = { | |||||
| label: string; | label: string; | ||||
| value: number; | value: number; | ||||
| } | } | ||||
| const InvoiceTable: React.FC<Props> = ({ projects }) => { | |||||
| const ExpenseTable: React.FC<Props> = ({ projects }) => { | |||||
| console.log(projects) | console.log(projects) | ||||
| const projectCombos = projects.map(item => item.code) | |||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({}); | const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({}); | ||||
| const [selectedRow, setSelectedRow] = useState<invoiceListRow[] | []>([]); | |||||
| const [selectedRow, setSelectedRow] = useState<ExpenseListRow[] | []>([]); | |||||
| const { getValues, setValue, clearErrors, setError } = | const { getValues, setValue, clearErrors, setError } = | ||||
| useFormContext<any>(); | useFormContext<any>(); | ||||
| const apiRef = useGridApiRef(); | const apiRef = useGridApiRef(); | ||||
| const [projectCode, setProjectCode] = useState<project>({label: "", value: 0}) | |||||
| const validateInvoiceEntry = ( | |||||
| entry: Partial<invoiceList>, | |||||
| ): InvoiceListError | undefined => { | |||||
| const validateExpenseEntry = ( | |||||
| entry: Partial<ProjectExpensesResultFormatted>, | |||||
| ): ExpenseListError | undefined => { | |||||
| // Test for errors | // 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; | return Object.keys(error).length > 0 ? error : undefined; | ||||
| } | } | ||||
| @@ -105,8 +97,8 @@ const InvoiceTable: React.FC<Props> = ({ projects }) => { | |||||
| "", | "", | ||||
| ) | ) | ||||
| const error = validateInvoiceEntry(row); | |||||
| // console.log(error) | |||||
| const error = validateExpenseEntry(row); | |||||
| console.log(error) | |||||
| // Test for warnings | // Test for warnings | ||||
| // apiRef.current.updateRows([{ id, _error: error }]); | // apiRef.current.updateRows([{ id, _error: error }]); | ||||
| @@ -159,8 +151,8 @@ const InvoiceTable: React.FC<Props> = ({ projects }) => { | |||||
| const processRowUpdate = useCallback( | const processRowUpdate = useCallback( | ||||
| ( | ( | ||||
| newRow: GridRowModel<invoiceListRow>, | |||||
| originalRow: GridRowModel<invoiceListRow>, | |||||
| newRow: GridRowModel<ExpenseListRow>, | |||||
| originalRow: GridRowModel<ExpenseListRow>, | |||||
| ) => { | ) => { | ||||
| const errors = validateRow(newRow.id!!); | const errors = validateRow(newRow.id!!); | ||||
| if (errors) { | if (errors) { | ||||
| @@ -178,7 +170,7 @@ const InvoiceTable: React.FC<Props> = ({ projects }) => { | |||||
| const rowToSave = { | const rowToSave = { | ||||
| ...updatedRow, | ...updatedRow, | ||||
| } satisfies invoiceListRow; | |||||
| } satisfies ExpenseListRow; | |||||
| console.log(newRow) | console.log(newRow) | ||||
| @@ -198,7 +190,7 @@ const InvoiceTable: React.FC<Props> = ({ projects }) => { | |||||
| (updateError: ProcessRowUpdateError) => { | (updateError: ProcessRowUpdateError) => { | ||||
| const errors = updateError.errors; | const errors = updateError.errors; | ||||
| const oldRow = updateError.row; | const oldRow = updateError.row; | ||||
| // console.log(errors) | |||||
| console.log(errors) | |||||
| apiRef.current.updateRows([{ ...oldRow, _error: errors }]); | apiRef.current.updateRows([{ ...oldRow, _error: errors }]); | ||||
| }, | }, | ||||
| [apiRef] | [apiRef] | ||||
| @@ -209,38 +201,64 @@ const InvoiceTable: React.FC<Props> = ({ projects }) => { | |||||
| setValue("data", selectedRow) | setValue("data", selectedRow) | ||||
| }, [selectedRow, setValue]); | }, [selectedRow, setValue]); | ||||
| function renderAutocomplete(params: GridRenderCellParams<any, number>) { | |||||
| return( | |||||
| <Box sx={{ display: 'flex', alignItems: 'center', pr: 2 }}> | |||||
| <Autocomplete | |||||
| readOnly | |||||
| sx={{ width: 300 }} | |||||
| value={params.row.projectCode} | |||||
| options={projectCombos} | |||||
| renderInput={(params) => <TextField {...params} />} | |||||
| /> | |||||
| </Box> | |||||
| ) | |||||
| } | |||||
| function AutocompleteInput(props: GridRenderCellParams<any, number>) { | |||||
| const { id, value, field, hasFocus } = props; | |||||
| const apiRef = useGridApiContext(); | |||||
| const ref = React.useRef<HTMLElement>(null); | |||||
| const handleValueChange = useCallback((newValue: any) => { | |||||
| console.log(newValue) | |||||
| apiRef.current.setEditCellValue({ id, field, value: newValue }) | |||||
| }, []); | |||||
| return ( | |||||
| <Box sx={{ display: 'flex', alignItems: 'center', pr: 2 }}> | |||||
| <Autocomplete | |||||
| disablePortal | |||||
| options={projectCombos} | |||||
| sx={{ width: 300 }} | |||||
| onChange={(event: React.SyntheticEvent<Element, Event>, value: string | null, ) => handleValueChange(value)} | |||||
| renderInput={(params) => <TextField {...params} />} | |||||
| /> | |||||
| </Box> | |||||
| ); | |||||
| } | |||||
| const renderAutocompleteInput: GridColDef['renderCell'] = (params) => { | |||||
| return <AutocompleteInput {...params} />; | |||||
| }; | |||||
| const editCombinedColumns = useMemo<GridColDef[]>( | const editCombinedColumns = useMemo<GridColDef[]>( | ||||
| () => [ | () => [ | ||||
| { field: "invoiceNo", headerName: t("Invoice No"), editable: true, flex: 0.5 }, | |||||
| { field: "expenseNo", headerName: t("Expense No"), editable: true, flex: 0.5 }, | |||||
| { field: "projectCode", | { field: "projectCode", | ||||
| headerName: t("Project Code"), | headerName: t("Project Code"), | ||||
| editable: true, | editable: true, | ||||
| flex: 0.3, | flex: 0.3, | ||||
| renderEditCell(params: GridRenderEditCellParams<invoiceListRow, number>){ | |||||
| return( | |||||
| <Autocomplete | |||||
| disablePortal | |||||
| options={[]} | |||||
| sx={{width: '100%'}} | |||||
| renderInput={(params) => <TextField {...params} />} | |||||
| /> | |||||
| ) | |||||
| } | |||||
| renderCell: renderAutocomplete, | |||||
| renderEditCell: renderAutocompleteInput | |||||
| }, | }, | ||||
| { field: "issuedDate", | |||||
| { field: "issueDate", | |||||
| headerName: t("Issue Date"), | headerName: t("Issue Date"), | ||||
| editable: true, | editable: true, | ||||
| flex: 0.4, | 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)"), | headerName: t("Amount (HKD)"), | ||||
| editable: true, | editable: true, | ||||
| flex: 0.5, | flex: 0.5, | ||||
| @@ -251,13 +269,8 @@ const editCombinedColumns = useMemo<GridColDef[]>( | |||||
| headerName: t("Settle Date"), | headerName: t("Settle Date"), | ||||
| editable: true, | editable: true, | ||||
| flex: 0.4, | flex: 0.4, | ||||
| }, | |||||
| { field: "receivedAmount", | |||||
| headerName: t("Actual Received Amount (HKD)"), | |||||
| editable: true, | |||||
| flex: 0.5, | |||||
| type: 'number' | |||||
| }, | |||||
| type: 'date', | |||||
| }, | |||||
| ], | ], | ||||
| [t] | [t] | ||||
| ) | ) | ||||
| @@ -271,7 +284,7 @@ const footer = ( | |||||
| onClick={addRow} | onClick={addRow} | ||||
| size="small" | size="small" | ||||
| > | > | ||||
| {t("Create Invoice")} | |||||
| {t("Create Expense")} | |||||
| </Button> | </Button> | ||||
| </Box> | </Box> | ||||
| ); | ); | ||||
| @@ -301,9 +314,9 @@ const footer = ( | |||||
| columns={editCombinedColumns} | columns={editCombinedColumns} | ||||
| processRowUpdate={processRowUpdate} | processRowUpdate={processRowUpdate} | ||||
| onProcessRowUpdateError={onProcessRowUpdateError} | onProcessRowUpdateError={onProcessRowUpdateError} | ||||
| getCellClassName={(params: GridCellParams<invoiceListRow>) => { | |||||
| getCellClassName={(params: GridCellParams<ExpenseListRow>) => { | |||||
| let classname = ""; | let classname = ""; | ||||
| if (params.row._error?.[params.field as keyof invoiceList]) { | |||||
| if (params.row._error?.[params.field as keyof ProjectExpensesResultFormatted]) { | |||||
| classname = "hasError"; | classname = "hasError"; | ||||
| } | } | ||||
| return classname; | return classname; | ||||
| @@ -320,7 +333,7 @@ const footer = ( | |||||
| ) | ) | ||||
| } | } | ||||
| export default InvoiceTable | |||||
| export default ExpenseTable | |||||
| const NoRowsOverlay: React.FC = () => { | const NoRowsOverlay: React.FC = () => { | ||||
| const { t } = useTranslation("home"); | const { t } = useTranslation("home"); | ||||
| @@ -23,11 +23,12 @@ const InvoiceSearchWrapper: React.FC & SubComponents = async () => { | |||||
| const teamId = userStaff?.teamId | const teamId = userStaff?.teamId | ||||
| const invoices = await fetchInvoicesV3() | const invoices = await fetchInvoicesV3() | ||||
| const projects = await fetchProjects() | const projects = await fetchProjects() | ||||
| const filteredProjects = projects.filter(project => project.teamId === teamId) | |||||
| let filteredInvoice = invoices | let filteredInvoice = invoices | ||||
| if (teamId) { | if (teamId) { | ||||
| filteredInvoice = invoices.filter(invoice => invoice.teamId === teamId) | |||||
| filteredInvoice = invoices.filter(i => i.teamId === teamId) | |||||
| } else { | |||||
| filteredInvoice = [] | |||||
| } | } | ||||
| const convertedInvoices = filteredInvoice.map((invoice)=>{ | const convertedInvoices = filteredInvoice.map((invoice)=>{ | ||||
| @@ -49,7 +50,7 @@ const InvoiceSearchWrapper: React.FC & SubComponents = async () => { | |||||
| return <InvoiceSearch | return <InvoiceSearch | ||||
| invoices={convertedInvoices} | invoices={convertedInvoices} | ||||
| projects={filteredProjects} | |||||
| projects={projects} | |||||
| abilities={abilities} | abilities={abilities} | ||||
| /> | /> | ||||