소스 검색

Update edit expense dialog

Update Create expense validation
tags/Baseline_180220205_Frontend
MSI\2Fi 11 달 전
부모
커밋
9b2b9c85fe
6개의 변경된 파일269개의 추가작업 그리고 29개의 파일을 삭제
  1. +25
    -1
      src/app/api/projectExpenses/actions.ts
  2. +3
    -2
      src/app/api/projectExpenses/index.ts
  3. +1
    -1
      src/components/ExpenseSearch/CreateExpenseModal.tsx
  4. +238
    -19
      src/components/ExpenseSearch/ExpenseSearch.tsx
  5. +2
    -2
      src/components/InvoiceSearch/CreateInvoiceModal.tsx
  6. +0
    -4
      src/components/InvoiceSearch/InvoiceSearch.tsx

+ 25
- 1
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<any>(`${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;
}

+ 3
- 2
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<ProjectExpensesResult, 'issueDate' | 'receiptDate'> & {
issueDate: string;
receiptDate: string;


+ 1
- 1
src/components/ExpenseSearch/CreateExpenseModal.tsx 파일 보기

@@ -55,7 +55,7 @@ const CreateExpenseModal: React.FC<Props> = ({ 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,


+ 238
- 19
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<Props> = ({ expenses, projects }) => {
const router = useRouter();
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>[]>(
() => [
{
name: "id",
label: t("Details"),
onClick: onExpenseClick,
onClick: (row: ProjectExpensesResultFormatted) => (
handleButtonClick(row)
),
buttonIcon: <EditNote />,
// disabled: !abilities.includes(MAINTAIN_PROJECT),
},
@@ -80,13 +100,148 @@ const ExpenseSearch: React.FC<Props> = ({ 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<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 (
<>
<Stack
@@ -114,18 +269,13 @@ const ExpenseSearch: React.FC<Props> = ({ expenses, projects }) => {
<SearchBox
criteria={searchCriteria}
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}
/>
@@ -149,6 +299,75 @@ const ExpenseSearch: React.FC<Props> = ({ expenses, projects }) => {
columns={columns}
/>
</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
isOpen={modalsOpen.createInvoiceModal}
onClose={() => toggleModals("createInvoiceModal")}


+ 2
- 2
src/components/InvoiceSearch/CreateInvoiceModal.tsx 파일 보기

@@ -62,10 +62,10 @@ const CreateInvoiceModal: React.FC<Props> = ({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(() => {


+ 0
- 4
src/components/InvoiceSearch/InvoiceSearch.tsx 파일 보기

@@ -516,10 +516,6 @@ const InvoiceSearch: React.FC<Props> = ({ invoices, projects, abilities }) => {
<DialogContentText>
{t("You can edit the invoice details here.")}
</DialogContentText>
{/* <SearchResults<invoiceList>
items={selectedRow ? [selectedRow] : []}
columns={editCombinedColumns}
/> */}
<StyledDataGrid
apiRef={apiRef}
autoHeight


불러오는 중...
취소
저장