Files
frontend/apps/st/src/components/statistics/verification/StatisticsVerificationReport.tsx
T

362 lines
13 KiB
TypeScript

"use client";
import { useEffect, useMemo, useState } from "react";
import { useAlertStore } from "@/shared/lib";
import {
Button,
Label,
LoadingWithAnimation,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@common/components";
import {
statisticsVerificationDetailColumnInfo,
statisticsVerificationSummaryColumnInfo,
} from "../../../entities/statistics/model/statistics-verification-report/statistics-verification-report-columns";
import {
APPROVAL_STATUS_OPTIONS,
DEFAULT_FILTERS,
DEFAULT_SUB_FILTERS,
fetchStatisticsVerificationDetailMock,
fetchStatisticsVerificationSummaryMock,
getSigunguOptions,
getYearOptions,
SIDO_OPTIONS,
STATS_INDICATOR_OPTIONS,
STATUS_OPTIONS,
} from "../../../entities/statistics/model/statistics-verification-report/statistics-verification-report-mock";
import type {
StatisticsVerificationDetailRow,
StatisticsVerificationFilterState,
StatisticsVerificationSubFilterState,
StatisticsVerificationSummaryRow,
} from "../../../entities/statistics/model/statistics-verification-report/statistics-verification-report-types";
import ApproveButton from "@/components/button/ApproveButton";
import ReturnButton from "@/components/button/ReturnButton";
import DataGrid from "@/components/module/DataGrid";
import { toGridRows } from "@/components/util/grid/gridFunction";
function StatisticsVerificationReport() {
const { setMessage: alert } = useAlertStore((state) => state);
const yearOptions = useMemo(() => getYearOptions(), []);
const [filters, setFilters] =
useState<StatisticsVerificationFilterState>(DEFAULT_FILTERS);
const [subFilters, setSubFilters] =
useState<StatisticsVerificationSubFilterState>(DEFAULT_SUB_FILTERS);
const [summaryRows, setSummaryRows] = useState<
StatisticsVerificationSummaryRow[]
>([]);
const [detailRows, setDetailRows] = useState<
StatisticsVerificationDetailRow[]
>([]);
const [summaryGridRows, setSummaryGridRows] = useState<
Record<string, string | number>[]
>([]);
const [detailGridRows, setDetailGridRows] = useState<
Record<string, string | number>[]
>([]);
const [selectedSummaryRow, setSelectedSummaryRow] =
useState<StatisticsVerificationSummaryRow | null>(null);
const [selectedDetailRow, setSelectedDetailRow] =
useState<StatisticsVerificationDetailRow | null>(null);
const [isLoading, setIsLoading] = useState(false);
const sigunguOptions = useMemo(
() => getSigunguOptions(filters.sido),
[filters.sido],
);
useEffect(() => {
void handleSearch(filters, subFilters);
}, []);
async function handleSearch(
nextFilters: StatisticsVerificationFilterState,
nextSubFilters: StatisticsVerificationSubFilterState,
summaryId?: number | null,
) {
try {
setIsLoading(true);
const [summaryData, detailData] = await Promise.all([
fetchStatisticsVerificationSummaryMock(nextFilters),
fetchStatisticsVerificationDetailMock(
nextFilters,
nextSubFilters,
summaryId,
),
]);
setSummaryRows(summaryData);
setDetailRows(detailData);
setSummaryGridRows(toGridRows(summaryData));
setDetailGridRows(toGridRows(detailData));
if (summaryData.length === 0)
alert("조회된 상단 목록 데이터가 없습니다.");
if (detailData.length === 0)
alert("조회된 상세 목록 데이터가 없습니다.");
} catch {
alert("통계검증리포트 조회 중 오류가 발생했습니다.");
} finally {
setIsLoading(false);
}
}
function handleFilterChange<
K extends keyof StatisticsVerificationFilterState,
>(key: K, value: StatisticsVerificationFilterState[K]) {
if (key === "sido") {
setFilters((prev) => ({ ...prev, sido: value, sigungu: "ALL" }));
return;
}
setFilters((prev) => ({ ...prev, [key]: value }));
}
function handleSubFilterChange<
K extends keyof StatisticsVerificationSubFilterState,
>(key: K, value: StatisticsVerificationSubFilterState[K]) {
setSubFilters((prev) => ({ ...prev, [key]: value }));
}
function handleReset() {
setFilters(DEFAULT_FILTERS);
setSubFilters(DEFAULT_SUB_FILTERS);
setSelectedSummaryRow(null);
setSelectedDetailRow(null);
void handleSearch(DEFAULT_FILTERS, DEFAULT_SUB_FILTERS);
}
function handleApprove() {
if (!selectedSummaryRow) {
alert("승인할 상단 목록 행을 먼저 선택해주세요.");
return;
}
alert(
`승인 API TBD: ${selectedSummaryRow.sido} ${selectedSummaryRow.sigungu} (${selectedSummaryRow.reportType})`,
);
}
function handleReject() {
if (!selectedDetailRow) {
alert("반려할 상세 항목 행을 먼저 선택해주세요.");
return;
}
alert(
`반려 API TBD: ${selectedDetailRow.sido} ${selectedDetailRow.sigungu} ${selectedDetailRow.detailItem}`,
);
}
function handleDownload() {
if (detailRows.length === 0) {
alert("다운로드할 상세 목록이 없습니다.");
return;
}
const headers = [
"시도",
"시군구",
"지표",
"세부항목",
"검증날짜",
"오류내역",
"입력값",
"비교값",
"상태",
"예외사유",
"반려의견",
"승인여부",
];
const rows = detailRows.map((row) =>
[
row.sido,
row.sigungu,
row.indicator,
row.detailItem,
row.verifyDate,
row.errorDetail,
String(row.inputValue),
String(row.compareValue),
row.status,
row.exceptionReason,
row.returnOpinion,
row.approvalYn,
].join(","),
);
const csv = [headers.join(","), ...rows].join("\n");
const blob = new Blob([`\uFEFF${csv}`], {
type: "text/csv;charset=utf-8;",
});
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "statistics-verification-report-detail.csv";
link.click();
URL.revokeObjectURL(url);
}
function handleSummaryRowClick(row: Record<string, string | number>) {
const summaryId = Number(row.summaryId);
const selected = summaryRows.find(
(item) => item.summaryId === summaryId,
);
setSelectedSummaryRow(selected ?? null);
if (!summaryId) return;
void handleSearch(filters, subFilters, summaryId);
}
function handleDetailRowClick(row: Record<string, string | number>) {
const detailId = Number(row.detailId);
const selected = detailRows.find((item) => item.detailId === detailId);
setSelectedDetailRow(selected ?? null);
}
return (
<div className="relative">
<div className="card !py-0">
<div className="grid grid-cols-1 gap-[0.8rem] p-[1.2rem] md:grid-cols-[repeat(4,minmax(0,1fr))_auto]">
<FilterSelect
label="시도"
value={filters.sido}
onChange={(value) => handleFilterChange("sido", value)}
options={SIDO_OPTIONS}
/>
<FilterSelect
label="시군구"
value={filters.sigungu}
onChange={(value) =>
handleFilterChange("sigungu", value)
}
options={sigunguOptions}
/>
<FilterSelect
label="년도"
value={filters.year}
onChange={(value) => handleFilterChange("year", value)}
options={yearOptions}
/>
<FilterSelect
label="통계지표"
value={filters.statsIndicator}
onChange={(value) =>
handleFilterChange("statsIndicator", value)
}
options={STATS_INDICATOR_OPTIONS}
/>
<div className="flex items-end justify-end gap-[0.6rem]">
<Button color="green" onClick={handleReset}>
초기화
</Button>
<Button
onClick={() =>
void handleSearch(filters, subFilters)
}
>
조회
</Button>
</div>
</div>
</div>
<div className="card !py-0">
<div className="m-[0.6rem] flex items-center justify-end">
<ApproveButton approveFunc={handleApprove} />
</div>
</div>
<DataGrid
columnInfo={statisticsVerificationSummaryColumnInfo}
fixedColumns={3}
isExcelImport={false}
onRowClick={handleSummaryRowClick}
rowData={summaryGridRows}
rowStartNum={2}
setRowData={setSummaryGridRows}
showHeaderCheckbox={true}
/>
<div className="card !py-0">
<div className="m-[0.6rem] flex flex-wrap items-end justify-between gap-[0.8rem]">
<div className="grid w-full grid-cols-1 gap-[0.8rem] md:w-auto md:grid-cols-2">
<FilterSelect
label="상태"
value={subFilters.status}
onChange={(value) =>
handleSubFilterChange("status", value)
}
options={STATUS_OPTIONS}
/>
<FilterSelect
label="승인상태"
value={subFilters.approvalStatus}
onChange={(value) =>
handleSubFilterChange("approvalStatus", value)
}
options={APPROVAL_STATUS_OPTIONS}
/>
</div>
<div className="flex items-end justify-end gap-[0.6rem]">
<Button color="gray" onClick={handleDownload}>
다운로드
</Button>
<ReturnButton returnFunc={handleReject} />
</div>
</div>
</div>
<DataGrid
columnInfo={statisticsVerificationDetailColumnInfo}
fixedColumns={4}
isExcelImport={false}
onRowClick={handleDetailRowClick}
rowData={detailGridRows}
rowStartNum={2}
setRowData={setDetailGridRows}
showHeaderCheckbox={true}
/>
{isLoading && (
<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-[5rem]">
<LoadingWithAnimation color="primary" />
</div>
</div>
)}
</div>
);
}
interface FilterSelectProps {
label: string;
onChange: (value: string) => void;
options: Array<{ label: string; value: string }>;
value: string;
}
function FilterSelect({ label, onChange, options, value }: FilterSelectProps) {
return (
<div className="flex items-center gap-[0.8rem]">
<Label className="min-w-[4rem] text-[1.3rem]">{label}</Label>
<Select value={value} onValueChange={onChange}>
<SelectTrigger className="h-[3.2rem] min-w-[14rem]">
<SelectValue placeholder="전체" />
</SelectTrigger>
<SelectContent>
{options.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
}
export { StatisticsVerificationReport };
export default StatisticsVerificationReport;