diff --git a/src/app/api/projectExpenses/actions.ts b/src/app/api/projectExpenses/actions.ts index d8e3f09..9832824 100644 --- a/src/app/api/projectExpenses/actions.ts +++ b/src/app/api/projectExpenses/actions.ts @@ -20,7 +20,7 @@ export type PostExpenseData = { projectId: number projectCode: string, amount: number - issueDate: string + issueDate?: string receiptDate?: string remarks?: string } @@ -30,6 +30,30 @@ export const saveProjectExpense = async (data: PostExpenseData[]) => { body: JSON.stringify(data), headers: { "Content-Type": "application/json" }, }); + revalidateTag("projectExpenses"); + return response; +} + +export const updateProjectExpense = async (data: any) => { + console.log(data) + const response = await serverFetchJson(`${BASE_API_URL}/project-expense/update`, { + method: "Post", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }); + + revalidateTag("projectExpenses") + return response; +} + +export const deleteProjectExpense = async (rowId: number) => { + const response = await serverFetchWithNoContent(`${BASE_API_URL}/project-expense/${rowId}`, + { + method: "DELETE", + headers: { "Content-Type": "application/json" }, + }, + ) + revalidateTag("projectExpenses"); return response; } \ No newline at end of file diff --git a/src/app/api/projectExpenses/index.ts b/src/app/api/projectExpenses/index.ts index 90baf88..70eaead 100644 --- a/src/app/api/projectExpenses/index.ts +++ b/src/app/api/projectExpenses/index.ts @@ -14,8 +14,9 @@ export type ProjectExpensesResult = { amount: number issueDate: number[] receiptDate: number[] - - } + remarks?: string +} + export type ProjectExpensesResultFormatted = Omit & { issueDate: string; receiptDate: string; diff --git a/src/components/ExpenseSearch/CreateExpenseModal.tsx b/src/components/ExpenseSearch/CreateExpenseModal.tsx index fa555b7..88976a6 100644 --- a/src/components/ExpenseSearch/CreateExpenseModal.tsx +++ b/src/components/ExpenseSearch/CreateExpenseModal.tsx @@ -55,7 +55,7 @@ const CreateExpenseModal: React.FC = ({ isOpen, onClose, projects }) => { const postData: PostExpenseData[] = _data.map((item) => { return { expenseNo: item.expenseNo, - // issueDate: dayjs(item.issueDate).format(INPUT_DATE_FORMAT), + // issueDate: item.issueDate ? dayjs(item.issueDate).format(INPUT_DATE_FORMAT) : null, amount: item.amount, projectId: projects.find((p) => p.code === item.projectCode)!.id, projectCode: item.projectCode, diff --git a/src/components/ExpenseSearch/ExpenseSearch.tsx b/src/components/ExpenseSearch/ExpenseSearch.tsx index 22ee13b..a741e67 100644 --- a/src/components/ExpenseSearch/ExpenseSearch.tsx +++ b/src/components/ExpenseSearch/ExpenseSearch.tsx @@ -10,6 +10,11 @@ import { ButtonGroup, Card, CardContent, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, Divider, Grid, Stack, @@ -21,6 +26,12 @@ import AddIcon from '@mui/icons-material/Add'; import { uniq } from "lodash"; import CreateExpenseModal from "./CreateExpenseModal"; import { ProjectResult } from "@/app/api/projects"; +import StyledDataGrid from "../StyledDataGrid"; +import { GridCellParams, GridColDef, GridRowId, GridRowModes, GridRowModesModel } from "@mui/x-data-grid"; +import { useGridApiRef } from "@mui/x-data-grid"; +import { GridEventListener } from "@mui/x-data-grid"; +import { deleteDialog, successDialog } from "../Swal/CustomAlerts"; +import { deleteProjectExpense, updateProjectExpense } from "@/app/api/projectExpenses/actions"; interface Props { expenses: ProjectExpensesResultFormatted[] @@ -37,6 +48,18 @@ const initState: Modals = { createInvoiceModal: false, } +type expenseRow = Partial< + ProjectExpensesResultFormatted & { + _error: ExpenseRowError; + } +> + +type ExpenseRowError = { + [field in keyof ProjectExpensesResultFormatted]?: string +}& { + message?: string +} + const ExpenseSearch: React.FC = ({ expenses, projects }) => { const router = useRouter(); const { t } = useTranslation("expenses"); @@ -61,17 +84,14 @@ const ExpenseSearch: React.FC = ({ expenses, projects }) => { [] ); - const onExpenseClick = useCallback( - (expenses?: ProjectExpensesResultFormatted) => {}, - [router] - ); - const columns = useMemo[]>( () => [ { name: "id", label: t("Details"), - onClick: onExpenseClick, + onClick: (row: ProjectExpensesResultFormatted) => ( + handleButtonClick(row) + ), buttonIcon: , // disabled: !abilities.includes(MAINTAIN_PROJECT), }, @@ -80,13 +100,148 @@ const ExpenseSearch: React.FC = ({ expenses, projects }) => { { name: "amount", label: t("Amount") }, { name: "teamCode", label: t("Team") }, // { name: "issueDate", label: t("Issue Date") }, + { name: "remarks", label: t("Remarks")} ], - [t, onExpenseClick] + [t] ); const onReset = useCallback(() => { // setFilteredExpenses(); }, []); + /** + * Edit Dialog + */ + const [rowModesModel, setRowModesModel] = useState({}); + const apiRef = useGridApiRef(); + const [selectedRow, setSelectedRow] = useState([]); + const [dialogOpen, setDialogOpen] = useState(false); + + const handleButtonClick = (row: ProjectExpensesResultFormatted) => { + setSelectedRow([row]) + setDialogOpen(true) + setRowModesModel((model) => ({ + ...model, + [row.id]: {mode: GridRowModes.Edit, fieldToFocus: "amount"} + })) + } + + const handleCloseDialog = () => { + setDialogOpen(false); + }; + + const handleDeleteExpense = useCallback(() => { + deleteDialog(async() => { + //console.log(selectedRow[0]) + await deleteProjectExpense(selectedRow[0].id!!) + setDialogOpen(false); + const result = await successDialog("Delete Success", t); + if (result) { + window.location.reload() + } + }, t) + }, [selectedRow]); + + + const handleSaveDialog = async () => { + // setDialogOpen(false); + console.log(selectedRow[0]) + await updateProjectExpense(selectedRow[0]) + setDialogOpen(false); + successDialog(t("Update Success"), t).then(() => { + window.location.reload() + }) + + // console.log(selectedRow[0]) + // setSelectedRow([]); + }; + + const validateExpenseEntry = ( + entry: Partial, + ): ExpenseRowError | undefined => { + // Test for errors + const error: ExpenseRowError = {}; + + // console.log(entry) + if (!entry.amount) { + error.amount = "Please input amount "; + } + + + return Object.keys(error).length > 0 ? error : undefined; + } + + const validateRow = useCallback( + (id: GridRowId) => { + const row = apiRef.current.getRowWithUpdatedValues( + id, + "", + ) + + const error = validateExpenseEntry(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 ProjectExpensesResultFormatted[]) + event.defaultMuiPrevented = true; + } + // console.log(row) + }, + [validateRow], + ); + + const editColumn = useMemo( + () => [ + { + field: "expenseNo", + headerName: t("Expense No"), + editable: true, + flex: 0.5 + }, + { + field: "projectCode", + headerName: t("Project Code"), + editable: false, + flex: 0.3, + }, + { + field: "amount", + headerName: t("Amount (HKD)"), + editable: true, + flex: 0.5, + type: 'number' + }, + { + field: "remarks", + headerName: t("Remarks"), + editable: true, + flex: 1, + }, + ], + [t] + ) + + return ( <> = ({ expenses, projects }) => { { - // 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), - // ), - // ); + setFilteredExpenses( + expenses.filter( + (e) => + e.projectCode.toLowerCase().includes(query.projectCode.toLowerCase()) && + e.projectName.toLowerCase().includes(query.projectName.toLowerCase()) + ), + ); }} onReset={onReset} /> @@ -149,6 +299,75 @@ const ExpenseSearch: React.FC = ({ expenses, projects }) => { columns={columns} /> + + + {t("Edit Expense")} + + + {t("You can edit the expense details here.")} + + .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={editColumn} + getCellClassName={(params: GridCellParams) => { + let classname = ""; + if (params.row._error?.[params.field as keyof ExpenseRowError]) { + classname = "hasError"; + } + return classname; + }} + /> + + + + + + + + + toggleModals("createInvoiceModal")} diff --git a/src/components/InvoiceSearch/CreateInvoiceModal.tsx b/src/components/InvoiceSearch/CreateInvoiceModal.tsx index 305a845..727ac98 100644 --- a/src/components/InvoiceSearch/CreateInvoiceModal.tsx +++ b/src/components/InvoiceSearch/CreateInvoiceModal.tsx @@ -62,10 +62,10 @@ const CreateInvoiceModal: React.FC = ({isOpen, onClose, projects, invoice receiptDate: item.receiptDate ? dayjs(item.receiptDate).format(INPUT_DATE_FORMAT) : null, receivedAmount: item.receivedAmount || null, })) - console.log(postData) + // console.log(postData) submitDialog(async () => { const response = await createInvoices(postData) - console.log(response) + // console.log(response) if (response === "OK") { onClose() successDialog(t("Submit Success"), t).then(() => { diff --git a/src/components/InvoiceSearch/InvoiceSearch.tsx b/src/components/InvoiceSearch/InvoiceSearch.tsx index 8f559e2..1accede 100644 --- a/src/components/InvoiceSearch/InvoiceSearch.tsx +++ b/src/components/InvoiceSearch/InvoiceSearch.tsx @@ -516,10 +516,6 @@ const InvoiceSearch: React.FC = ({ invoices, projects, abilities }) => { {t("You can edit the invoice details here.")} - {/* - items={selectedRow ? [selectedRow] : []} - columns={editCombinedColumns} - /> */}