762 lines
26 KiB
TypeScript
762 lines
26 KiB
TypeScript
"use client";
|
|
|
|
import type { Dispatch, SetStateAction } from "react";
|
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
|
|
import { useConfirm } from "@frontend-opensource/use-react-hooks";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import dayjs from "dayjs";
|
|
import type { RealGridReact } from "realgrid-react";
|
|
|
|
import { ENDPOINT } from "@/shared/config";
|
|
import { useAlertStore, useMutationWrapper, useUserStore } from "@/shared/lib";
|
|
import { CodeApi, CodeSelect } from "@/entities/code";
|
|
import {
|
|
type ApproveRequest,
|
|
type RejectRequest,
|
|
statisticsApprovalApi,
|
|
type StatisticsVerificationFilterState,
|
|
statisticsVerificationReportColumnInfo,
|
|
statisticsVerificationResultColumnInfo,
|
|
type StatisticsVerificationSubFilterState,
|
|
type StatsInputStatusList,
|
|
useStAnlsSidoCombo,
|
|
useStAnlsSigunguCombo,
|
|
useStatsVrfcRptData,
|
|
useStatsVrfcRsltData,
|
|
type VerifyResultItem,
|
|
} from "@/entities/statistics";
|
|
import {
|
|
Button,
|
|
Combobox,
|
|
ComboboxChip,
|
|
ComboboxChips,
|
|
ComboboxChipsInput,
|
|
ComboboxContent,
|
|
ComboboxEmpty,
|
|
ComboboxItem,
|
|
ComboboxList,
|
|
ComboboxValue,
|
|
Label,
|
|
LoadingWithAnimation,
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
useComboboxAnchor,
|
|
} from "@common/components";
|
|
|
|
import DataGrid from "@/components/module/DataGrid";
|
|
import {
|
|
type ApprovalFormData,
|
|
ApprovalInputDialog,
|
|
} from "@/components/statistics/verification/ApprovalInputDialog";
|
|
import type { CellEditedCallbackParams } from "@/components/type/DataGrid.type";
|
|
import { toGridRows } from "@/components/util/grid/gridFunction";
|
|
|
|
const getYearOptions = () => {
|
|
const currentYear = dayjs().year();
|
|
const yearOptionsRange = Array.from(
|
|
{ length: currentYear - 2017 },
|
|
(_, i) => {
|
|
const year = 2017 + i;
|
|
return { label: String(year), value: String(year) };
|
|
},
|
|
);
|
|
return yearOptionsRange.map((year) => ({
|
|
label: year.label,
|
|
value: year.value,
|
|
}));
|
|
};
|
|
|
|
function isSameStatsInputStatusRow(
|
|
a: StatsInputStatusList,
|
|
b: StatsInputStatusList,
|
|
): boolean {
|
|
return (
|
|
a.ctpvNm === b.ctpvNm && a.sggNm === b.sggNm && a.statsCd === b.statsCd
|
|
);
|
|
}
|
|
|
|
function isSameVerifyResultRow(
|
|
a: VerifyResultItem,
|
|
b: VerifyResultItem,
|
|
): boolean {
|
|
if (a.vrfcSn != null && b.vrfcSn != null) return a.vrfcSn === b.vrfcSn;
|
|
return (
|
|
a.statsYr === b.statsYr &&
|
|
a.statsCd === b.statsCd &&
|
|
a.ctpvNm === b.ctpvNm &&
|
|
a.sggNm === b.sggNm &&
|
|
a.vrfcTrgtNm === b.vrfcTrgtNm &&
|
|
a.vrfcMthdCd === b.vrfcMthdCd
|
|
);
|
|
}
|
|
|
|
function useGridCheckboxSelection<T>(
|
|
sourceRows: T[],
|
|
isSameRow: (a: T, b: T) => boolean,
|
|
setSelected: Dispatch<SetStateAction<T[]>>,
|
|
) {
|
|
const handleItemChecked = useCallback(
|
|
(itemIndex: number, checked: boolean) => {
|
|
const origin = sourceRows[itemIndex];
|
|
if (origin === undefined) return;
|
|
setSelected((prev) => {
|
|
if (checked) {
|
|
if (prev.some((r) => isSameRow(r, origin))) return prev;
|
|
return [...prev, origin];
|
|
}
|
|
return prev.filter((r) => !isSameRow(r, origin));
|
|
});
|
|
},
|
|
[sourceRows, isSameRow, setSelected],
|
|
);
|
|
|
|
const handleAllChecked = useCallback(
|
|
(checked: boolean) => {
|
|
if (checked) setSelected([...sourceRows]);
|
|
else setSelected([]);
|
|
},
|
|
[sourceRows, setSelected],
|
|
);
|
|
|
|
return { handleItemChecked, handleAllChecked };
|
|
}
|
|
|
|
const DEFAULT_FILTERS: StatisticsVerificationFilterState = {
|
|
ctpvNm: null,
|
|
sggNm: null,
|
|
statsYr: String(dayjs().year() - 1),
|
|
sewerBzmnCd: null,
|
|
statsCd: null,
|
|
laeSbmsnSttsCd: "2",
|
|
};
|
|
|
|
const DEFAULT_SUB_FILTERS: StatisticsVerificationSubFilterState = {
|
|
...DEFAULT_FILTERS,
|
|
prcsYnList: "1,2,3",
|
|
pbcpAprvYn: "N",
|
|
};
|
|
|
|
const APPROVAL_STATUS_OPTIONS = [
|
|
{ label: "승인", value: "Y" },
|
|
{ label: "미승인", value: "N" },
|
|
];
|
|
|
|
function StatisticsVerificationReport() {
|
|
const userInfo = useUserStore((state) => state.userInfo);
|
|
const { setMessage: alert } = useAlertStore((state) => state);
|
|
const { confirm } = useConfirm();
|
|
const [filters, setFilters] =
|
|
useState<StatisticsVerificationFilterState>(DEFAULT_FILTERS);
|
|
const [subFilters, setSubFilters] =
|
|
useState<StatisticsVerificationSubFilterState>(DEFAULT_SUB_FILTERS);
|
|
const [selectedReportRows, setSelectedReportRows] = useState<
|
|
StatsInputStatusList[]
|
|
>([]);
|
|
const [selectedResultRows, setSelectedResultRows] = useState<
|
|
VerifyResultItem[]
|
|
>([]);
|
|
const [approvalDialogOpen, setApprovalDialogOpen] = useState(false);
|
|
const editedResultRowMapRef = useRef<Map<number, VerifyResultItem>>(
|
|
new Map(),
|
|
);
|
|
const resultGridRef = useRef<RealGridReact | null>(null);
|
|
const prevLengthRef = useRef(0);
|
|
|
|
const { data: sidoData } = useStAnlsSidoCombo();
|
|
const { data: sigunguData } = useStAnlsSigunguCombo(
|
|
filters.sewerBzmnCd ?? "",
|
|
);
|
|
const detailParams = useMemo(
|
|
() => ({
|
|
statsYr: filters.statsYr,
|
|
ctpvNm:
|
|
subFilters.ctpvNm !== "" ? subFilters.ctpvNm : filters.ctpvNm,
|
|
sggNm: subFilters.sggNm !== "" ? subFilters.sggNm : filters.sggNm,
|
|
sewerBzmnCd:
|
|
subFilters.sewerBzmnCd !== ""
|
|
? subFilters.sewerBzmnCd
|
|
: filters.sewerBzmnCd,
|
|
statsCd:
|
|
subFilters.statsCd !== ""
|
|
? subFilters.statsCd
|
|
: filters.statsCd,
|
|
prcsYnList: subFilters.prcsYnList,
|
|
pbcpAprvYn: subFilters.pbcpAprvYn,
|
|
laeSbmsnSttsCd: subFilters.laeSbmsnSttsCd,
|
|
}),
|
|
[filters, subFilters],
|
|
);
|
|
|
|
const { data: statusOptions } = useQuery({
|
|
queryKey: [ENDPOINT.CMS_SERVICE.CODE, "STINPTEXCT"],
|
|
queryFn: () => CodeApi.getCode("STINPTEXCT"),
|
|
select: (data) =>
|
|
data
|
|
.filter(({ lwpoCd }) => !["0", "4"].includes(lwpoCd))
|
|
.map(({ lwpoCdNm: label, lwpoCd: value }) => ({
|
|
label,
|
|
value,
|
|
})),
|
|
});
|
|
|
|
const statisticsVerificationReportQuery = useStatsVrfcRptData(filters);
|
|
const statisticsVerificationResultQuery =
|
|
useStatsVrfcRsltData(detailParams);
|
|
|
|
const statisticsVerificationReportData =
|
|
statisticsVerificationReportQuery.data ?? [];
|
|
const statisticsVerificationReportGridRows = useMemo(
|
|
() => toGridRows(statisticsVerificationReportData),
|
|
[statisticsVerificationReportData],
|
|
);
|
|
|
|
const statisticsVerificationResultData = useMemo(
|
|
() =>
|
|
statisticsVerificationResultQuery.data?.pages.flatMap(
|
|
(page) => page.verifyResultList ?? [],
|
|
) ?? [],
|
|
[statisticsVerificationResultQuery.data],
|
|
);
|
|
const statisticsVerificationResultGridRows = useMemo(
|
|
() => toGridRows(statisticsVerificationResultData),
|
|
[statisticsVerificationResultData],
|
|
);
|
|
|
|
function mergeWithEditedMap(rows: Record<string, string | number>[]) {
|
|
const editedMap = editedResultRowMapRef.current;
|
|
if (editedMap.size === 0) return rows;
|
|
return rows.map((row) => {
|
|
const vrfcSn = Number(row.vrfcSn);
|
|
const edited = editedMap.get(vrfcSn);
|
|
return edited ? { ...row, ...edited } : row;
|
|
});
|
|
}
|
|
|
|
function captureCheckedVrfcSn(): number[] {
|
|
const gridView = resultGridRef.current?.gridView;
|
|
const dataProvider = resultGridRef.current?.dataProvider;
|
|
|
|
if (!gridView || !dataProvider) return [];
|
|
|
|
const checkedRows = gridView.getCheckedRows?.(true, false, true) ?? [];
|
|
return checkedRows
|
|
.map((row) => Number(dataProvider.getValue?.(row, "vrfcSn")))
|
|
.filter((key) => !Number.isNaN(key));
|
|
}
|
|
|
|
function restoreCheckedVrfcSn(
|
|
keys: number[],
|
|
rows: Record<string, string | number>[],
|
|
) {
|
|
const gridView = resultGridRef.current?.gridView;
|
|
|
|
if (!gridView || keys.length === 0) return;
|
|
|
|
const keySet = new Set(keys);
|
|
const rowsToCheck = rows
|
|
.map((row, index) => ({ index, key: Number(row.vrfcSn) }))
|
|
.filter((item) => !Number.isNaN(item.key) && keySet.has(item.key))
|
|
.map((item) => item.index);
|
|
|
|
if (rowsToCheck.length === 0) return;
|
|
|
|
gridView.checkRows?.(rowsToCheck, true, false);
|
|
}
|
|
|
|
useEffect(() => {
|
|
const dataProvider = resultGridRef.current?.dataProvider;
|
|
const nextRows = statisticsVerificationResultGridRows;
|
|
const prevLength = prevLengthRef.current;
|
|
|
|
if (!dataProvider) {
|
|
prevLengthRef.current = nextRows.length;
|
|
return;
|
|
}
|
|
|
|
if (nextRows.length === 0) {
|
|
dataProvider.setRows?.([]);
|
|
prevLengthRef.current = 0;
|
|
return;
|
|
}
|
|
|
|
if (nextRows.length > prevLength && prevLength > 0) {
|
|
// 무한스크롤 시 데이터 처리
|
|
const newRows = nextRows.slice(prevLength);
|
|
const mergedNewRows = mergeWithEditedMap(newRows);
|
|
dataProvider.addRows?.(mergedNewRows);
|
|
} else {
|
|
// 초기 조회 또는 재조회 시 데이터 처리
|
|
const checkedKeys = captureCheckedVrfcSn();
|
|
const mergedRows = mergeWithEditedMap(nextRows);
|
|
dataProvider.setRows?.(mergedRows);
|
|
restoreCheckedVrfcSn(checkedKeys, mergedRows);
|
|
}
|
|
|
|
prevLengthRef.current = nextRows.length;
|
|
}, [statisticsVerificationResultGridRows]);
|
|
|
|
const isLoading =
|
|
statisticsVerificationReportQuery.isFetching ||
|
|
statisticsVerificationResultQuery.isFetching;
|
|
const isAppendingDetail =
|
|
statisticsVerificationResultQuery.isFetchingNextPage;
|
|
const canLoadMoreDetail = Boolean(
|
|
statisticsVerificationResultQuery.hasNextPage,
|
|
);
|
|
|
|
const {
|
|
handleItemChecked: handleReportItemChecked,
|
|
handleAllChecked: handleReportAllChecked,
|
|
} = useGridCheckboxSelection(
|
|
statisticsVerificationReportData,
|
|
isSameStatsInputStatusRow,
|
|
setSelectedReportRows,
|
|
);
|
|
|
|
const {
|
|
handleItemChecked: handleResultItemChecked,
|
|
handleAllChecked: handleResultAllChecked,
|
|
} = useGridCheckboxSelection(
|
|
statisticsVerificationResultData,
|
|
isSameVerifyResultRow,
|
|
setSelectedResultRows,
|
|
);
|
|
|
|
const { mutateAsync: onApproveStats } = useMutationWrapper({
|
|
mutationFn: (approvalRequests: ApproveRequest[]) =>
|
|
statisticsApprovalApi.approveStats(approvalRequests),
|
|
onSuccess: (data) => {
|
|
alert(data.message ?? "승인이 완료되었습니다.");
|
|
},
|
|
});
|
|
|
|
const { mutateAsync: onRejectStats } = useMutationWrapper({
|
|
mutationFn: (rejectRequests: RejectRequest[]) =>
|
|
statisticsApprovalApi.rejectStats(rejectRequests),
|
|
onSuccess: (data) => {
|
|
alert(data.message ?? "반려가 완료되었습니다.");
|
|
},
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (!statisticsVerificationReportQuery.isError) return;
|
|
alert("통계검증리포트 조회 중 오류가 발생했습니다.");
|
|
setSelectedReportRows([]);
|
|
}, [
|
|
alert,
|
|
statisticsVerificationReportQuery.errorUpdatedAt,
|
|
statisticsVerificationReportQuery.isError,
|
|
]);
|
|
|
|
function resetResultState() {
|
|
setSelectedResultRows([]);
|
|
editedResultRowMapRef.current = new Map();
|
|
prevLengthRef.current = 0;
|
|
resultGridRef.current?.dataProvider?.setRows?.([]);
|
|
}
|
|
|
|
function resetReportState() {
|
|
setSelectedReportRows([]);
|
|
resetResultState();
|
|
}
|
|
|
|
async function handleLoadMoreDetail() {
|
|
if (isLoading || isAppendingDetail || !canLoadMoreDetail) return;
|
|
|
|
try {
|
|
await statisticsVerificationResultQuery.fetchNextPage();
|
|
} catch {
|
|
alert("추가 데이터를 불러오는 중 오류가 발생했습니다.");
|
|
}
|
|
}
|
|
|
|
function handleFilterChange<
|
|
K extends keyof StatisticsVerificationFilterState,
|
|
>(key: K) {
|
|
return (
|
|
value?: StatisticsVerificationFilterState[K],
|
|
optionData?: { label: string; value: string },
|
|
) => {
|
|
resetReportState();
|
|
if (key === "sewerBzmnCd") {
|
|
setFilters((prev) => ({
|
|
...prev,
|
|
ctpvNm: optionData?.label,
|
|
sewerBzmnCd: optionData?.value,
|
|
sggNm: "",
|
|
}));
|
|
setSubFilters((prev) => ({
|
|
...prev,
|
|
ctpvNm: "",
|
|
sggNm: "",
|
|
sewerBzmnCd: "",
|
|
}));
|
|
return;
|
|
}
|
|
if (key === "statsCd") {
|
|
setSubFilters((prev) => ({ ...prev, statsCd: "" }));
|
|
}
|
|
setFilters((prev) => ({ ...prev, [key]: value }));
|
|
};
|
|
}
|
|
|
|
function handleSubFilterChange<
|
|
K extends keyof StatisticsVerificationSubFilterState,
|
|
>(key: K) {
|
|
return (value?: StatisticsVerificationSubFilterState[K]) => {
|
|
resetResultState();
|
|
setSubFilters((prev) => ({ ...prev, [key]: value }));
|
|
};
|
|
}
|
|
|
|
function handleSubFilterReset() {
|
|
setSubFilters(DEFAULT_SUB_FILTERS);
|
|
resetResultState();
|
|
}
|
|
|
|
function handleCellEdited(params: CellEditedCallbackParams) {
|
|
const { itemIndex, fieldName, newValue } = params;
|
|
const dataProvider = resultGridRef.current?.dataProvider as
|
|
| { getValue?: (row: number, field: string) => string | number }
|
|
| undefined;
|
|
const vrfcSn = Number(dataProvider?.getValue?.(itemIndex, "vrfcSn"));
|
|
if (Number.isNaN(vrfcSn)) return;
|
|
|
|
editedResultRowMapRef.current?.set(vrfcSn, {
|
|
...(editedResultRowMapRef.current?.get(vrfcSn) ?? {}),
|
|
[fieldName]: newValue,
|
|
});
|
|
}
|
|
|
|
function handleReset() {
|
|
setFilters(DEFAULT_FILTERS);
|
|
setSubFilters(DEFAULT_SUB_FILTERS);
|
|
resetReportState();
|
|
}
|
|
|
|
function handleApprove() {
|
|
if (selectedReportRows.length === 0) return;
|
|
setApprovalDialogOpen(true);
|
|
}
|
|
|
|
async function handleApprovalConfirm(formData: ApprovalFormData) {
|
|
const commonParam = {
|
|
statsYr: filters.statsYr,
|
|
mdfrId: userInfo?.userUnqId ?? "",
|
|
};
|
|
const approvalRequests = selectedReportRows.map((row) => ({
|
|
...commonParam,
|
|
...formData,
|
|
statsCd: row.statsCd ?? "",
|
|
sewerBzmnCd: row.sewerBzmnCd ?? "",
|
|
}));
|
|
|
|
try {
|
|
if (await confirm("승인하시겠습니까?")) {
|
|
await onApproveStats(approvalRequests);
|
|
setApprovalDialogOpen(false);
|
|
}
|
|
} catch {
|
|
alert("승인 중 오류가 발생했습니다.");
|
|
}
|
|
}
|
|
|
|
async function handleReject() {
|
|
const commonParam = {
|
|
statsYr: filters.statsYr,
|
|
sewerBzmnCd: filters.sewerBzmnCd ?? "",
|
|
statsCd: filters.statsCd ?? "",
|
|
mdfrId: userInfo?.userUnqId ?? "",
|
|
};
|
|
|
|
const requestRows = selectedResultRows.map((row) => {
|
|
const vrfcSn = Number(row.vrfcSn);
|
|
const editedRow = Number.isNaN(vrfcSn)
|
|
? undefined
|
|
: editedResultRowMapRef.current?.get(vrfcSn);
|
|
return { ...row, ...(editedRow ?? {}) };
|
|
});
|
|
|
|
const hasEmptyRjctOpnnCn = requestRows.some(
|
|
(row) => !String(row?.rjctOpnnCn ?? "").trim(),
|
|
);
|
|
if (hasEmptyRjctOpnnCn) {
|
|
alert("선택한 항목의 반려의견을 작성해주세요.");
|
|
return;
|
|
}
|
|
|
|
const rejectRequests = requestRows.map((row) => {
|
|
const vrfcSn = Number(row.vrfcSn);
|
|
|
|
return {
|
|
...commonParam,
|
|
vrfcSn: Number.isNaN(vrfcSn) ? 0 : vrfcSn,
|
|
rjctOpnnCn: String(row.rjctOpnnCn ?? ""),
|
|
};
|
|
});
|
|
|
|
try {
|
|
if (await confirm("반려하시겠습니까?")) {
|
|
await onRejectStats(rejectRequests);
|
|
}
|
|
} catch {
|
|
alert("반려 중 오류가 발생했습니다.");
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="relative">
|
|
<ApprovalInputDialog
|
|
open={approvalDialogOpen}
|
|
onOpenChange={setApprovalDialogOpen}
|
|
onConfirm={handleApprovalConfirm}
|
|
/>
|
|
<div className="flex justify-between p-2">
|
|
<div className="flex items-center gap-6">
|
|
<FilterSelect
|
|
label="시도"
|
|
value={filters.sewerBzmnCd ?? ""}
|
|
onChange={handleFilterChange("sewerBzmnCd")}
|
|
options={
|
|
sidoData?.map((item) => ({
|
|
label: item.adstNm,
|
|
value: item.adstCd,
|
|
})) ?? []
|
|
}
|
|
/>
|
|
<FilterSelect
|
|
label="시군구"
|
|
value={filters.sggNm ?? ""}
|
|
onChange={handleFilterChange("sggNm")}
|
|
options={
|
|
sigunguData?.map((item) => ({
|
|
label: item.adstNm,
|
|
value: item.adstCd,
|
|
})) ?? []
|
|
}
|
|
/>
|
|
<FilterSelect
|
|
label="년도"
|
|
value={filters.statsYr}
|
|
onChange={handleFilterChange("statsYr")}
|
|
options={getYearOptions()}
|
|
/>
|
|
<div className="flex items-center gap-2">
|
|
<Label>통계지표</Label>
|
|
<CodeSelect
|
|
upCd="STITEM"
|
|
value={filters.statsCd ?? ""}
|
|
onValueChange={handleFilterChange("statsCd")}
|
|
placeholder="선택"
|
|
className="w-[32rem]"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-end justify-end gap-2">
|
|
<Button color="white" onClick={handleReset}>
|
|
초기화
|
|
</Button>
|
|
<Button
|
|
color="green"
|
|
onClick={handleApprove}
|
|
disabled={selectedReportRows.length === 0}
|
|
>
|
|
승인
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<DataGrid
|
|
columnInfo={statisticsVerificationReportColumnInfo}
|
|
fixedColumns={3}
|
|
isExcelImport={false}
|
|
isExcelExport={false}
|
|
onItemAllChecked={handleReportAllChecked}
|
|
onItemChecked={handleReportItemChecked}
|
|
rowData={statisticsVerificationReportGridRows}
|
|
rowStartNum={2}
|
|
setRowData={() => {}}
|
|
showHeaderCheckbox={true}
|
|
gridHeight={30}
|
|
onRowClick={(row: StatsInputStatusList) => {
|
|
setSubFilters((prev) => ({
|
|
...prev,
|
|
ctpvNm: row?.ctpvNm ?? "",
|
|
sggNm: row?.sggNm ?? "",
|
|
sewerBzmnCd: row?.sewerBzmnCd ?? "",
|
|
statsCd: row?.statsCd ?? "",
|
|
}));
|
|
}}
|
|
/>
|
|
|
|
<h3 className="mt-10 px-2 text-[1.8rem] font-bold text-title">
|
|
검증 결과
|
|
</h3>
|
|
<div className="mt-2 flex flex-wrap items-end justify-between p-2">
|
|
<div className="flex items-center gap-6">
|
|
<FilterCombobox
|
|
label="상태"
|
|
value={subFilters.prcsYnList}
|
|
onChange={handleSubFilterChange("prcsYnList")}
|
|
options={statusOptions ?? []}
|
|
/>
|
|
<FilterSelect
|
|
label="승인상태"
|
|
value={subFilters.pbcpAprvYn}
|
|
onChange={handleSubFilterChange("pbcpAprvYn")}
|
|
options={APPROVAL_STATUS_OPTIONS}
|
|
/>
|
|
</div>
|
|
<div className="flex items-end justify-end gap-2">
|
|
<Button color="white" onClick={handleSubFilterReset}>
|
|
초기화
|
|
</Button>
|
|
<Button
|
|
color="red"
|
|
onClick={handleReject}
|
|
disabled={
|
|
selectedResultRows.length === 0 ||
|
|
selectedResultRows.some((row) => row.prcsYn !== "2")
|
|
}
|
|
>
|
|
반려
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="relative">
|
|
<DataGrid
|
|
columnInfo={statisticsVerificationResultColumnInfo}
|
|
fixedColumns={4}
|
|
isExcelImport={false}
|
|
onItemAllChecked={handleResultAllChecked}
|
|
onItemChecked={handleResultItemChecked}
|
|
onScrollToBottom={handleLoadMoreDetail}
|
|
rowData={statisticsVerificationResultGridRows}
|
|
rowStartNum={2}
|
|
setRowData={() => {}}
|
|
showHeaderCheckbox={true}
|
|
onCellEdited={handleCellEdited}
|
|
getGridRef={resultGridRef}
|
|
/>
|
|
|
|
{(isLoading || isAppendingDetail) && (
|
|
<div className="[data-theme='black']:bg-transparent absolute inset-0 z-50 flex items-center justify-center rounded-lg p-10">
|
|
<div className="relative size-[6rem]">
|
|
<LoadingWithAnimation color="primary" />
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface FilterSelectProps {
|
|
label: string;
|
|
onChange: (
|
|
value?: string,
|
|
optionData?: { label: string; value: string },
|
|
) => void;
|
|
options: Array<{ label: string; value: string }>;
|
|
value?: string;
|
|
}
|
|
|
|
function FilterCombobox({
|
|
label,
|
|
onChange,
|
|
options,
|
|
value,
|
|
}: FilterSelectProps) {
|
|
const anchor = useComboboxAnchor();
|
|
const items = useMemo(
|
|
() => options.map((option) => option.value),
|
|
[options],
|
|
);
|
|
const selectedValues = useMemo(() => {
|
|
if (!value) return [];
|
|
return value.split(",").filter(Boolean);
|
|
}, [value]);
|
|
|
|
return (
|
|
<div className="flex items-center gap-2">
|
|
<Label className="whitespace-nowrap">{label}</Label>
|
|
<Combobox
|
|
multiple
|
|
autoHighlight
|
|
items={items}
|
|
value={selectedValues}
|
|
onValueChange={(next) => {
|
|
if (next == null) {
|
|
onChange(undefined);
|
|
return;
|
|
}
|
|
const list = Array.isArray(next) ? next : [next];
|
|
onChange(list.length > 0 ? list.join(",") : undefined);
|
|
}}
|
|
>
|
|
<ComboboxChips ref={anchor} className="max-w-[32rem]">
|
|
<ComboboxValue>
|
|
{(values: string[]) => (
|
|
<>
|
|
{values.map((v) => (
|
|
<ComboboxChip key={v}>
|
|
{options.find((o) => o.value === v)
|
|
?.label ?? v}
|
|
</ComboboxChip>
|
|
))}
|
|
<ComboboxChipsInput placeholder="선택" />
|
|
</>
|
|
)}
|
|
</ComboboxValue>
|
|
</ComboboxChips>
|
|
<ComboboxContent anchor={anchor}>
|
|
<ComboboxEmpty>검색 결과가 없습니다.</ComboboxEmpty>
|
|
<ComboboxList>
|
|
{(item: string) => (
|
|
<ComboboxItem key={item} value={item}>
|
|
{options.find((o) => o.value === item)?.label ??
|
|
item}
|
|
</ComboboxItem>
|
|
)}
|
|
</ComboboxList>
|
|
</ComboboxContent>
|
|
</Combobox>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function FilterSelect({ label, onChange, options, value }: FilterSelectProps) {
|
|
return (
|
|
<div className="flex items-center gap-2">
|
|
<Label className="whitespace-nowrap">{label}</Label>
|
|
<Select
|
|
value={value}
|
|
onValueChange={(value) =>
|
|
onChange(
|
|
value,
|
|
options.find((option) => option.value === value),
|
|
)
|
|
}
|
|
>
|
|
<SelectTrigger className="w-[16rem]">
|
|
<SelectValue placeholder="선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{options.map((option) => (
|
|
<SelectItem key={option.value} value={option.value}>
|
|
{option.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default StatisticsVerificationReport;
|