|
@@ -10,6 +10,11 @@ import { |
|
|
ButtonGroup, |
|
|
ButtonGroup, |
|
|
Card, |
|
|
Card, |
|
|
CardContent, |
|
|
CardContent, |
|
|
|
|
|
Dialog, |
|
|
|
|
|
DialogActions, |
|
|
|
|
|
DialogContent, |
|
|
|
|
|
DialogContentText, |
|
|
|
|
|
DialogTitle, |
|
|
Divider, |
|
|
Divider, |
|
|
Grid, |
|
|
Grid, |
|
|
Stack, |
|
|
Stack, |
|
@@ -21,6 +26,12 @@ import AddIcon from '@mui/icons-material/Add'; |
|
|
import { uniq } from "lodash"; |
|
|
import { uniq } from "lodash"; |
|
|
import CreateExpenseModal from "./CreateExpenseModal"; |
|
|
import CreateExpenseModal from "./CreateExpenseModal"; |
|
|
import { ProjectResult } from "@/app/api/projects"; |
|
|
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 { |
|
|
interface Props { |
|
|
expenses: ProjectExpensesResultFormatted[] |
|
|
expenses: ProjectExpensesResultFormatted[] |
|
@@ -37,6 +48,18 @@ const initState: Modals = { |
|
|
createInvoiceModal: false, |
|
|
createInvoiceModal: false, |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
type expenseRow = Partial< |
|
|
|
|
|
ProjectExpensesResultFormatted & { |
|
|
|
|
|
_error: ExpenseRowError; |
|
|
|
|
|
} |
|
|
|
|
|
> |
|
|
|
|
|
|
|
|
|
|
|
type ExpenseRowError = { |
|
|
|
|
|
[field in keyof ProjectExpensesResultFormatted]?: string |
|
|
|
|
|
}& { |
|
|
|
|
|
message?: string |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
const ExpenseSearch: React.FC<Props> = ({ expenses, projects }) => { |
|
|
const ExpenseSearch: React.FC<Props> = ({ expenses, projects }) => { |
|
|
const router = useRouter(); |
|
|
const router = useRouter(); |
|
|
const { t } = useTranslation("expenses"); |
|
|
const { t } = useTranslation("expenses"); |
|
@@ -61,17 +84,14 @@ const ExpenseSearch: React.FC<Props> = ({ expenses, projects }) => { |
|
|
[] |
|
|
[] |
|
|
); |
|
|
); |
|
|
|
|
|
|
|
|
const onExpenseClick = useCallback( |
|
|
|
|
|
(expenses?: ProjectExpensesResultFormatted) => {}, |
|
|
|
|
|
[router] |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
const columns = useMemo<Column<ProjectExpensesResultFormatted>[]>( |
|
|
const columns = useMemo<Column<ProjectExpensesResultFormatted>[]>( |
|
|
() => [ |
|
|
() => [ |
|
|
{ |
|
|
{ |
|
|
name: "id", |
|
|
name: "id", |
|
|
label: t("Details"), |
|
|
label: t("Details"), |
|
|
onClick: onExpenseClick, |
|
|
|
|
|
|
|
|
onClick: (row: ProjectExpensesResultFormatted) => ( |
|
|
|
|
|
handleButtonClick(row) |
|
|
|
|
|
), |
|
|
buttonIcon: <EditNote />, |
|
|
buttonIcon: <EditNote />, |
|
|
// disabled: !abilities.includes(MAINTAIN_PROJECT), |
|
|
// disabled: !abilities.includes(MAINTAIN_PROJECT), |
|
|
}, |
|
|
}, |
|
@@ -80,13 +100,148 @@ const ExpenseSearch: React.FC<Props> = ({ expenses, projects }) => { |
|
|
{ name: "amount", label: t("Amount") }, |
|
|
{ name: "amount", label: t("Amount") }, |
|
|
{ name: "teamCode", label: t("Team") }, |
|
|
{ name: "teamCode", label: t("Team") }, |
|
|
// { name: "issueDate", label: t("Issue Date") }, |
|
|
// { name: "issueDate", label: t("Issue Date") }, |
|
|
|
|
|
{ name: "remarks", label: t("Remarks")} |
|
|
], |
|
|
], |
|
|
[t, onExpenseClick] |
|
|
|
|
|
|
|
|
[t] |
|
|
); |
|
|
); |
|
|
const onReset = useCallback(() => { |
|
|
const onReset = useCallback(() => { |
|
|
// setFilteredExpenses(); |
|
|
// setFilteredExpenses(); |
|
|
}, []); |
|
|
}, []); |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* Edit Dialog |
|
|
|
|
|
*/ |
|
|
|
|
|
const [rowModesModel, setRowModesModel] = useState<GridRowModesModel>({}); |
|
|
|
|
|
const apiRef = useGridApiRef(); |
|
|
|
|
|
const [selectedRow, setSelectedRow] = useState<expenseRow[] | []>([]); |
|
|
|
|
|
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<ProjectExpensesResultFormatted>, |
|
|
|
|
|
): 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<GridEventListener<"rowEditStop">>( |
|
|
|
|
|
(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<GridColDef[]>( |
|
|
|
|
|
() => [ |
|
|
|
|
|
{ |
|
|
|
|
|
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 ( |
|
|
return ( |
|
|
<> |
|
|
<> |
|
|
<Stack |
|
|
<Stack |
|
@@ -114,18 +269,13 @@ const ExpenseSearch: React.FC<Props> = ({ expenses, projects }) => { |
|
|
<SearchBox |
|
|
<SearchBox |
|
|
criteria={searchCriteria} |
|
|
criteria={searchCriteria} |
|
|
onSearch={(query) => { |
|
|
onSearch={(query) => { |
|
|
// 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} |
|
|
onReset={onReset} |
|
|
/> |
|
|
/> |
|
@@ -149,6 +299,75 @@ const ExpenseSearch: React.FC<Props> = ({ expenses, projects }) => { |
|
|
columns={columns} |
|
|
columns={columns} |
|
|
/> |
|
|
/> |
|
|
</Stack> |
|
|
</Stack> |
|
|
|
|
|
|
|
|
|
|
|
<Dialog open={dialogOpen} onClose={handleCloseDialog} maxWidth="lg" fullWidth> |
|
|
|
|
|
<DialogTitle>{t("Edit Expense")}</DialogTitle> |
|
|
|
|
|
<DialogContent> |
|
|
|
|
|
<DialogContentText> |
|
|
|
|
|
{t("You can edit the expense details here.")} |
|
|
|
|
|
</DialogContentText> |
|
|
|
|
|
<StyledDataGrid |
|
|
|
|
|
apiRef={apiRef} |
|
|
|
|
|
autoHeight |
|
|
|
|
|
sx={{ |
|
|
|
|
|
"--DataGrid-overlayHeight": "100px", |
|
|
|
|
|
".MuiDataGrid-row .MuiDataGrid-cell.hasError": { |
|
|
|
|
|
border: "1px solid", |
|
|
|
|
|
borderColor: "error.main", |
|
|
|
|
|
}, |
|
|
|
|
|
".MuiDataGrid-row .MuiDataGrid-cell.hasWarning": { |
|
|
|
|
|
border: "1px solid", |
|
|
|
|
|
borderColor: "warning.main", |
|
|
|
|
|
}, |
|
|
|
|
|
'& .MuiDataGrid-columnHeaderTitle': { |
|
|
|
|
|
whiteSpace: 'normal', |
|
|
|
|
|
textWrap: 'pretty', |
|
|
|
|
|
textAlign: 'center', |
|
|
|
|
|
}, |
|
|
|
|
|
'.MuiDataGrid-row:not(.MuiDataGrid-row--dynamicHeight)>.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<expenseRow>) => { |
|
|
|
|
|
let classname = ""; |
|
|
|
|
|
if (params.row._error?.[params.field as keyof ExpenseRowError]) { |
|
|
|
|
|
classname = "hasError"; |
|
|
|
|
|
} |
|
|
|
|
|
return classname; |
|
|
|
|
|
}} |
|
|
|
|
|
/> |
|
|
|
|
|
</DialogContent> |
|
|
|
|
|
<DialogActions> |
|
|
|
|
|
<Button onClick={handleDeleteExpense} color="error"> |
|
|
|
|
|
{t("Delete")} |
|
|
|
|
|
</Button> |
|
|
|
|
|
<Button onClick={handleCloseDialog} color="primary"> |
|
|
|
|
|
{t("Cancel")} |
|
|
|
|
|
</Button> |
|
|
|
|
|
<Button |
|
|
|
|
|
onClick={handleSaveDialog} |
|
|
|
|
|
color="primary" |
|
|
|
|
|
disabled={ |
|
|
|
|
|
Object.values(rowModesModel).some((mode) => mode.mode === GridRowModes.Edit) || |
|
|
|
|
|
selectedRow.some((row) => row._error) |
|
|
|
|
|
} |
|
|
|
|
|
> |
|
|
|
|
|
{t("Save")} |
|
|
|
|
|
</Button> |
|
|
|
|
|
</DialogActions> |
|
|
|
|
|
</Dialog> |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<CreateExpenseModal |
|
|
<CreateExpenseModal |
|
|
isOpen={modalsOpen.createInvoiceModal} |
|
|
isOpen={modalsOpen.createInvoiceModal} |
|
|
onClose={() => toggleModals("createInvoiceModal")} |
|
|
onClose={() => toggleModals("createInvoiceModal")} |
|
|