473 lines
18 KiB
TypeScript
473 lines
18 KiB
TypeScript
import { useCallback, useMemo, useState } from "react";
|
|
|
|
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
|
import { createColumnHelper, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
|
|
|
import { useAlertStore, useSearchConditionSidoList, useUserStore } from "@/shared/lib";
|
|
import { statisticsManagementApi } from "@/entities/statistics/api/statistics-management-service";
|
|
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
|
|
import type {
|
|
ReviewHistoryResponse,
|
|
StatsInputStatusReviewItem,
|
|
} from "@/entities/statistics/model/statistics-input-review-interface";
|
|
import type {
|
|
ReviewerInfoRequest,
|
|
StatsInputStatusApproveItem,
|
|
} from "@/entities/statistics/model/statistics-interface";
|
|
import { isGugunOkForToolbar } from "@/entities/statistics";
|
|
import {
|
|
Button,
|
|
DataTable,
|
|
Dialog,
|
|
DialogContent,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
LoadingWithAnimation} from "@common/components";
|
|
|
|
import { getUserInfo } from "../../../../../../packages/common/components/src/api";
|
|
|
|
import ApproveButton from "@/components/button/ApproveButton";
|
|
import SubmitButton from "@/components/button/SumitButton";
|
|
import DataGrid from "@/components/module/DataGrid";
|
|
import SearchForm from "@/components/module/SearchForm";
|
|
import { ReviewSubmitDialog } from "@/components/statistics/review/ReviewSubmitDialog";
|
|
import type { DataCellClickCallbackParams } from "@/components/type/DataGrid.type";
|
|
import type { SearchFormValues } from "@/components/type/SearchForm.type";
|
|
import { StatsInputStatusInfoList } from "@/components/util/column/review/StatsInputStatusRvwColumn";
|
|
import { toGridRows } from "@/components/util/grid/gridFunction";
|
|
|
|
type ReviewHistoryRow = ReviewHistoryResponse["data"][number];
|
|
|
|
const checkPrvnStatusCd = (code: string) => {
|
|
return (code === '0' || code === '1') ? '미작성' : (code === '2' ? '제출완료' : (code === '3' ? '승인완료' : '반려'));
|
|
};
|
|
|
|
const checkSubmitStatusCd = (code: string) => {
|
|
return code === '0' ? '미작성' : (code === '1' ? '작성중' : (code === '2' ? '제출완료' : (code === '3' ? '승인완료' : '보완요청')));
|
|
};
|
|
|
|
const isStatsInputStatusRowApprovable = (row: Record<string, string | number>) =>
|
|
String(row.submitStatusCd ?? "") === "3" && (String(row.prvnStatusCd ?? "") === "2" || String(row.prvnStatusCd ?? "") === "3");
|
|
|
|
const mapSewerageListToGridRows = (rawList: StatsInputStatusReviewItem[]) =>
|
|
rawList.map((r) => ({
|
|
...r,
|
|
prvnStatusNm: checkPrvnStatusCd(r.prvnStatusCd || "0"),
|
|
submitStatusNm: checkSubmitStatusCd(r.submitStatusCd || "0"),
|
|
reviewHistory: "검토이력",
|
|
}));
|
|
|
|
// 하수도 통계 검토 00. 통계 입력 현황
|
|
const StatsInputStatusRvw = () => {
|
|
const { sidoList } = useSearchConditionSidoList();
|
|
const { setMessage: alert } = useAlertStore((state) => state);
|
|
const userInfo = useUserStore((state) => state.userInfo);
|
|
const { confirm } = useConfirm();
|
|
const [rowData, setRowData] = useState<Record<string, string | number>[]>([]);
|
|
const [historyRowData, setHistoryRowData] = useState<ReviewHistoryRow[]>([]);
|
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
|
const [isApproveLoading, setIsApproveLoading] = useState(false);
|
|
const [searchedYear, setSearchedYear] = useState<string>("");
|
|
const [lastSearchedValues, setLastSearchedValues] = useState<SearchFormValues | null>(null);
|
|
const [hasCompletedSearch, setHasCompletedSearch] = useState(false);
|
|
const [submitDialogOpen, setSubmitDialogOpen] = useState(false);
|
|
const [reviewHistoryOpen, setReviewHistoryOpen] = useState<boolean>(false);
|
|
const [historySearchInfo, setHistorySearchInfo] = useState<{
|
|
ctpvNm: string;
|
|
sggNm: string;
|
|
tabNm: string;
|
|
}>({
|
|
ctpvNm: '',
|
|
sggNm: '',
|
|
tabNm: '',
|
|
});
|
|
|
|
const columnInfo = useMemo(() => {
|
|
const { columns, columnLayout } = StatsInputStatusInfoList;
|
|
return {
|
|
columnLayout,
|
|
columns: columns.map((col) => ({
|
|
...col,
|
|
editable: false as const,
|
|
})),
|
|
};
|
|
}, []);
|
|
|
|
const reviewHistoryColumns = useMemo(() => {
|
|
const col = createColumnHelper<ReviewHistoryRow>();
|
|
return [
|
|
col.accessor("logDt", {
|
|
header: "등록일",
|
|
cell: (info) => info.getValue(),
|
|
}),
|
|
col.accessor("historyContent", {
|
|
header: "내용",
|
|
cell: (info) => info.getValue(),
|
|
}),
|
|
];
|
|
}, []);
|
|
|
|
const historyTable = useReactTable({
|
|
data: historyRowData,
|
|
columns: reviewHistoryColumns,
|
|
getCoreRowModel: getCoreRowModel(),
|
|
getRowId: (_row, index) => String(index),
|
|
});
|
|
|
|
const handleReset = () => {
|
|
setRowData([]);
|
|
setSearchedYear("");
|
|
setLastSearchedValues(null);
|
|
setHasCompletedSearch(false);
|
|
setIsApproveLoading(false);
|
|
setSubmitDialogOpen(false);
|
|
};
|
|
|
|
const handleSearch = async (values: SearchFormValues) => {
|
|
if (!values.sido || values.sido === 'ALL') {
|
|
alert('시도를 선택해주세요.');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setIsLoading(true);
|
|
setSearchedYear(values.year ?? "");
|
|
setLastSearchedValues(values);
|
|
setHasCompletedSearch(false);
|
|
|
|
const response = await statisticsManagementApi.getStatsInputStatusList({
|
|
ctpvNm: values.sido,
|
|
sggNm: values.gugun === 'ALL' ? '' : values.gugun,
|
|
statsYr: values.year,
|
|
sewerBzmnCd: values.bsnmCd
|
|
});
|
|
|
|
if (response.status === 'success' && response.data) {
|
|
const rawList = response.data.sewerageInputList ?? [];
|
|
setRowData(toGridRows(mapSewerageListToGridRows(rawList)));
|
|
} else {
|
|
setRowData([]);
|
|
setHasCompletedSearch(false);
|
|
alert(response.message || "조회된 데이터가 없습니다.");
|
|
return;
|
|
}
|
|
setHasCompletedSearch(true);
|
|
} catch {
|
|
alert('데이터 조회 중 오류가 발생했습니다.');
|
|
setRowData([]);
|
|
setHasCompletedSearch(false);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
const approveFunc = async () => {
|
|
if (rowData.length === 0) {
|
|
alert("조회된 통계 항목이 없습니다.");
|
|
return;
|
|
}
|
|
if (!lastSearchedValues?.year?.trim() || !lastSearchedValues?.bsnmCd?.trim()) {
|
|
alert("먼저 조회를 진행해주세요.");
|
|
return;
|
|
}
|
|
|
|
if (!rowData.every(isStatsInputStatusRowApprovable)) {
|
|
alert("지자체 진행상태가 승인완료이고 진행상태가 제출완료인 항목만 승인할 수 있습니다.");
|
|
return;
|
|
}
|
|
|
|
if (!(await confirm("조회된 통계 항목을 승인하시겠습니까?"))) {
|
|
return;
|
|
}
|
|
|
|
setIsApproveLoading(true);
|
|
try {
|
|
const userInfo = await getUserInfo();
|
|
const mdfrUuid = (userInfo.userUnqId ?? "").trim();
|
|
if (!mdfrUuid) {
|
|
alert("수정자 ID를 확인할 수 없습니다.");
|
|
return;
|
|
}
|
|
|
|
const statsYr = lastSearchedValues.year.trim();
|
|
const sewerBzmnCdDefault = lastSearchedValues.bsnmCd.trim();
|
|
|
|
const approveItems: StatsInputStatusApproveItem[] = rowData
|
|
.map((row) => {
|
|
const statsCd = String(row.statsCd ?? "").trim();
|
|
const sewerBzmnCd = String(row.sewerBzmnCd ?? sewerBzmnCdDefault).trim();
|
|
return { statsYr, sewerBzmnCd, statsCd, mdfrUuid };
|
|
})
|
|
.filter((item) => item.statsCd.length > 0);
|
|
|
|
if (approveItems.length === 0) {
|
|
alert("통계 코드(statsCd)가 있는 행이 없습니다.");
|
|
return;
|
|
}
|
|
|
|
const response = await statisticsApi.approveStatsInputStatus({ list: approveItems }, "REVIEW");
|
|
if (response.resultCode.toLocaleLowerCase() === "success") {
|
|
try {
|
|
const listResponse = await statisticsManagementApi.getStatsInputStatusList({
|
|
ctpvNm: lastSearchedValues.sido,
|
|
sggNm: lastSearchedValues.gugun === "ALL" ? "" : lastSearchedValues.gugun,
|
|
statsYr: lastSearchedValues.year ?? "",
|
|
sewerBzmnCd: lastSearchedValues.bsnmCd,
|
|
});
|
|
if (listResponse.status === "success" && listResponse.data) {
|
|
const rawList = listResponse.data.sewerageInputList ?? [];
|
|
setRowData(toGridRows(mapSewerageListToGridRows(rawList)));
|
|
}
|
|
} catch {
|
|
/* 목록 갱신 실패는 별도 안내 없이 생략 */
|
|
}
|
|
alert(response.message ?? "승인되었습니다.");
|
|
} else {
|
|
alert(response.message ?? "승인에 실패했습니다.");
|
|
}
|
|
} catch {
|
|
alert("승인 처리 중 오류가 발생했습니다.");
|
|
} finally {
|
|
setIsApproveLoading(false);
|
|
}
|
|
};
|
|
|
|
const gugunOkOrNotDoList = isGugunOkForToolbar(sidoList, lastSearchedValues);
|
|
|
|
const allSubmitStatusApproved = useMemo(
|
|
() =>
|
|
rowData.length > 0 &&
|
|
rowData.every((row) => String(row.submitStatusCd ?? "") === "3"),
|
|
[rowData],
|
|
);
|
|
|
|
/** 진행상태(prvn) 코드가 모두 승인완료(3) */
|
|
const allPrvnStatusApproved = useMemo(
|
|
() =>
|
|
rowData.length > 0 &&
|
|
rowData.every((row) => String(row.prvnStatusCd ?? "") === "3"),
|
|
[rowData],
|
|
);
|
|
|
|
/** 진행상태(prvn) 코드가 모두 제출완료(2) */
|
|
const allPrvnStatusSubmitted = useMemo(
|
|
() =>
|
|
rowData.length > 0 &&
|
|
rowData.every((row) => String(row.prvnStatusCd ?? "") === "2"),
|
|
[rowData],
|
|
);
|
|
|
|
const showSubmitButton =
|
|
hasCompletedSearch &&
|
|
rowData.length > 0 &&
|
|
gugunOkOrNotDoList &&
|
|
!allPrvnStatusApproved &&
|
|
!allPrvnStatusSubmitted;
|
|
|
|
const showApproveButton =
|
|
hasCompletedSearch &&
|
|
rowData.length > 0 &&
|
|
gugunOkOrNotDoList &&
|
|
allSubmitStatusApproved &&
|
|
!allPrvnStatusApproved;
|
|
|
|
const showActionToolbarCard = showSubmitButton || showApproveButton;
|
|
|
|
const submitDialogContext = useMemo<ReviewerInfoRequest | null>(() => {
|
|
if (lastSearchedValues == null) return null;
|
|
|
|
const statsYr = lastSearchedValues.year ?? "";
|
|
const sewerBzmnCd = lastSearchedValues.bsnmCd ?? "";
|
|
const instSeCd = userInfo?.instSeCd ?? "";
|
|
|
|
if (!statsYr || !sewerBzmnCd || !instSeCd) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
statsYr,
|
|
sewerBzmnCd,
|
|
instSeCd,
|
|
};
|
|
}, [lastSearchedValues, userInfo?.instSeCd]);
|
|
|
|
const submitFunc = () => {
|
|
if (submitDialogContext == null) {
|
|
alert("조회 후 제출할 수 있습니다.");
|
|
return;
|
|
}
|
|
setSubmitDialogOpen(true);
|
|
};
|
|
|
|
const refreshListAfterSubmit = async () => {
|
|
if (!lastSearchedValues) return;
|
|
try {
|
|
const listResponse = await statisticsManagementApi.getStatsInputStatusList({
|
|
ctpvNm: lastSearchedValues.sido,
|
|
sggNm: lastSearchedValues.gugun === "ALL" ? "" : lastSearchedValues.gugun,
|
|
statsYr: lastSearchedValues.year ?? "",
|
|
sewerBzmnCd: lastSearchedValues.bsnmCd,
|
|
});
|
|
if (listResponse.status === "success" && listResponse.data) {
|
|
const rawList = listResponse.data.sewerageInputList ?? [];
|
|
setRowData(toGridRows(mapSewerageListToGridRows(rawList)));
|
|
}
|
|
} catch {
|
|
/* 목록 갱신 실패는 별도 안내 없이 생략 */
|
|
}
|
|
};
|
|
|
|
const openReviewHistoryForRow = useCallback(
|
|
async (row: Record<string, string | number>) => {
|
|
if (!searchedYear) {
|
|
alert("먼저 조회를 진행해주세요.");
|
|
return;
|
|
}
|
|
setHistorySearchInfo({
|
|
ctpvNm: String(row.ctpvNm ?? ""),
|
|
sggNm: String(row.sggNm ?? ""),
|
|
tabNm: String(row.tabNm ?? ""),
|
|
});
|
|
const params = {
|
|
statsYr: searchedYear,
|
|
sewerBzmnCd: String(row.sewerBzmnCd ?? ""),
|
|
statsCd: String(row.statsCd ?? ""),
|
|
};
|
|
|
|
try {
|
|
const response = await statisticsManagementApi.getReviewHistory(params);
|
|
|
|
if (response.status === "success") {
|
|
setHistoryRowData(response.data);
|
|
setReviewHistoryOpen(true);
|
|
} else {
|
|
alert(response.message || "검토 이력 조회 중 오류가 발생했습니다.");
|
|
}
|
|
} catch {
|
|
alert("검토 이력 조회 중 오류가 발생했습니다.");
|
|
}
|
|
},
|
|
[alert, searchedYear],
|
|
);
|
|
|
|
const handleReviewHistoryCellClick = useCallback(
|
|
({ fieldName, row }: DataCellClickCallbackParams) => {
|
|
if (fieldName !== "reviewHistory") return;
|
|
void openReviewHistoryForRow(row);
|
|
},
|
|
[openReviewHistoryForRow],
|
|
);
|
|
|
|
return (
|
|
<div className="relative">
|
|
<SearchForm onSubmit={handleSearch} onReset={handleReset} />
|
|
|
|
{showActionToolbarCard && (
|
|
<div className="card !py-0">
|
|
<div className="flex justify-end items-center gap-[0.6rem] m-[0.6rem]">
|
|
{showSubmitButton && (
|
|
<SubmitButton submitFunc={submitFunc} text="일괄 제출" />
|
|
)}
|
|
{showApproveButton && <ApproveButton approveFunc={approveFunc} />}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{(isLoading || isApproveLoading) && (
|
|
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
|
|
<div className="relative size-[5rem]">
|
|
<LoadingWithAnimation color="primary" />
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<DataGrid
|
|
columnInfo={columnInfo}
|
|
isExcelImport={false}
|
|
showHeaderCheckbox={false}
|
|
rowStartNum={2}
|
|
fixedColumns={3}
|
|
rowData={rowData}
|
|
setRowData={setRowData}
|
|
onDataCellClick={handleReviewHistoryCellClick}
|
|
cellStyleCallback={({ row, fieldName }) => {
|
|
if (fieldName === "submitStatusNm") {
|
|
const statusCd = String(row.submitStatusCd ?? "");
|
|
|
|
if (statusCd === "0" || statusCd === "1") {
|
|
return "rg-cell-stats-status-pending";
|
|
}
|
|
if (statusCd === "2") {
|
|
return "rg-cell-stats-status-submitted";
|
|
}
|
|
if (statusCd === "3") {
|
|
return "rg-cell-stats-status-approved";
|
|
}
|
|
if (statusCd === "4") {
|
|
return "rg-cell-stats-status-rejected";
|
|
}
|
|
return undefined;
|
|
} else if (fieldName === "prvnStatusNm") {
|
|
const statusCd = String(row.prvnStatusCd ?? "");
|
|
|
|
if (statusCd === "0" || statusCd === "1") {
|
|
return "rg-cell-stats-status-pending";
|
|
}
|
|
if (statusCd === "2") {
|
|
return "rg-cell-stats-status-submitted";
|
|
}
|
|
if (statusCd === "3") {
|
|
return "rg-cell-stats-status-approved";
|
|
}
|
|
if (statusCd === "4") {
|
|
return "rg-cell-stats-status-rejected";
|
|
}
|
|
return undefined;
|
|
} else {
|
|
return undefined;
|
|
}
|
|
}}
|
|
/>
|
|
<ReviewSubmitDialog
|
|
open={submitDialogOpen}
|
|
onOpenChange={setSubmitDialogOpen}
|
|
context={submitDialogContext}
|
|
hideOpinionFields
|
|
onSubmitted={() => void refreshListAfterSubmit()}
|
|
/>
|
|
<Dialog open={reviewHistoryOpen} onOpenChange={setReviewHistoryOpen}>
|
|
<DialogContent
|
|
className="flex max-h-[85vh] max-w-[48rem] flex-col gap-4"
|
|
aria-describedby={undefined}
|
|
>
|
|
<DialogHeader className="shrink-0">
|
|
<DialogTitle>
|
|
{(historySearchInfo.sggNm ? `${historySearchInfo.ctpvNm} ${historySearchInfo.sggNm} - ${searchedYear}년 ${historySearchInfo.tabNm} 검토이력` : `${historySearchInfo.ctpvNm} - ${searchedYear}년 ${historySearchInfo.tabNm} 검토이력`)}
|
|
</DialogTitle>
|
|
</DialogHeader>
|
|
{historyRowData.length === 0 ?
|
|
<div className="text-2xl h-40 flex items-center justify-center">
|
|
검토 이력이 없습니다.
|
|
</div>
|
|
:
|
|
<div className="min-h-0 flex-1 overflow-y-auto overflow-x-auto">
|
|
<DataTable table={historyTable} />
|
|
</div>
|
|
}
|
|
<DialogFooter className="shrink-0">
|
|
<Button
|
|
type="button"
|
|
color="gray"
|
|
onClick={() => setReviewHistoryOpen(false)}
|
|
>
|
|
닫기
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default StatsInputStatusRvw; |