Files
frontend/apps/st/src/components/statistics/VerificationSelectionReport.tsx
T

321 lines
13 KiB
TypeScript

"use client";
import { useCallback, useState } from "react";
import { useQueries } from "@tanstack/react-query";
import { useTabStore as useGridTabStore } from "@rsone/components";
import { statisticsApi } from "@/entities/statistics/api/statistics-service";
import type { VerificationResultReportItem } from "@/entities/statistics/model/statistics-interface";
import { useAlertStore, useMenuStore } from "@/shared/lib";
import {
findMenuItemByUrlPath,
resolveInputPageNavigationFromStatsCd,
} from "@/shared/lib/stats-cd-input-navigation";
import { useStatisticsInputFocusStore } from "@/store/statistics-input-focus-store";
import { useTabStore } from "@/store/tab-store";
import dayjs from "dayjs";
import {
Badge,
type BadgeProps,
LoadingWithAnimation,
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@common/components";
type VerificationReportTabKey = "NV" | "IMR" | "HB";
const VERIFICATION_REPORT_TABS: { value: VerificationReportTabKey; label: string }[] = [
{ value: "NV", label: "일반" },
{ value: "IMR", label: "I-MR" },
{ value: "HB", label: "HB" },
];
const TYPE_CODE_COLOR_MAP: Record<string, BadgeProps["color"]> = {
NV: "gray",
IMR: "red",
HB: "blue",
};
const TYPE_CODE_NAME_MAP: Record<string, string> = {
NV: "일반",
IMR: "I-MR",
HB: "HB",
};
type VerificationSelectionReportProps = {
onClose?: () => void;
};
const VerificationSelectionReport = ({
onClose,
}: VerificationSelectionReportProps) => {
const menuList = useTabStore((s) => s.menuList);
const addTab = useTabStore((s) => s.addTab);
const setCurrentMenuInfo = useMenuStore((s) => s.setCurrentMenuInfo);
const addTabForGrid = useGridTabStore((s) => s.addTab);
const setPendingInputTab = useStatisticsInputFocusStore(
(s) => s.setPendingInputTab,
);
const alert = useAlertStore((s) => s.setMessage);
const [activeTab, setActiveTab] = useState<VerificationReportTabKey>("NV");
const statsYr = String(dayjs().year() - 1);
const baseRequest = {
statsYr,
pbcpAprvYn: "N" as const,
page: 1,
size: 10000,
prcsYnList: "0",
};
const [nvQuery, imrQuery, hbQuery] = useQueries({
queries: [
{
queryKey: [
"verificationResultList",
statsYr,
"NV",
baseRequest.pbcpAprvYn,
baseRequest.page,
baseRequest.size,
baseRequest.prcsYnList,
],
queryFn: () =>
statisticsApi.getVerificationResultList({
...baseRequest,
vrfcMthdCd: "NV",
}),
},
{
queryKey: [
"verificationResultList",
statsYr,
"IMR",
baseRequest.pbcpAprvYn,
baseRequest.page,
baseRequest.size,
baseRequest.prcsYnList,
],
queryFn: () =>
statisticsApi.getVerificationResultList({
...baseRequest,
vrfcMthdCd: "IMR",
}),
},
{
queryKey: [
"verificationResultList",
statsYr,
"HB",
baseRequest.pbcpAprvYn,
baseRequest.page,
baseRequest.size,
baseRequest.prcsYnList,
],
queryFn: () =>
statisticsApi.getVerificationResultList({
...baseRequest,
vrfcMthdCd: "HB",
}),
},
],
});
const isLoading =
nvQuery.isLoading || imrQuery.isLoading || hbQuery.isLoading;
const error = nvQuery.error ?? imrQuery.error ?? hbQuery.error;
const listsByTab: Record<VerificationReportTabKey, VerificationResultReportItem[]> = {
NV: nvQuery.data?.data?.verifyResultList ?? [],
IMR: imrQuery.data?.data?.verifyResultList ?? [],
HB: hbQuery.data?.data?.verifyResultList ?? [],
};
const openInputFromStatsCd = useCallback(
(item: VerificationResultReportItem) => {
const nav = resolveInputPageNavigationFromStatsCd(item.statsCd);
if (!nav) {
alert(
`입력 화면으로 연결되지 않는 통계코드입니다: ${item.statsCd}`,
);
return;
}
if (!menuList) {
alert(
"메뉴 정보를 불러오는 중입니다. 잠시 후 다시 시도해 주세요.",
);
return;
}
const menu = findMenuItemByUrlPath(menuList, nav.urlPath);
if (!menu) {
alert(
"해당 입력 메뉴를 찾을 수 없습니다. 메뉴 권한을 확인해 주세요.",
);
return;
}
setPendingInputTab(nav.urlPath, nav.tabKey, {
statsYr: item.statsYr,
sewerBzmnCd: item.sewerBzmnCd,
ctpvNm: item.ctpvNm,
sggNm: item.sggNm ?? "",
});
setCurrentMenuInfo(menu);
const ok = addTab(menu);
if (ok) {
addTabForGrid(menu);
} else {
alert(`최대 추가 할 수 있는 화면 수(10개)를 \n초과 하였습니다.
기존 화면을 닫은 후 이용해주세요.`);
}
},
[
menuList,
addTab,
addTabForGrid,
setCurrentMenuInfo,
setPendingInputTab,
alert,
],
);
const renderResultList = (
typeCode: VerificationReportTabKey,
items: VerificationResultReportItem[],
) => (
<div className="min-h-0 flex-1 overflow-y-auto p-2">
{items.length === 0 ? (
<div className="flex h-full min-h-[8rem] items-center justify-center text-lg text-muted-foreground">
항목이 없습니다.
</div>
) : (
<div className="space-y-2 tracking-tight">
{items.map((item) => (
<div
key={`${typeCode}-${item.vrfcSn}`}
role="button"
tabIndex={0}
className="flex flex-col rounded-sm border border-border p-3 cursor-pointer hover:bg-gray-100"
onClick={() => openInputFromStatsCd(item)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
openInputFromStatsCd(item);
}
}}
>
<div className="mb-2 flex items-center justify-between">
<span className="font-medium">
{item.statsNm}
</span>
<Badge
color={
TYPE_CODE_COLOR_MAP[item.vrfcMthdCd] ??
"gray"
}
className="text-sm"
>
{TYPE_CODE_NAME_MAP[item.vrfcMthdCd] ??
item.vrfcMthdNm}
</Badge>
</div>
<p className="text-lg text-muted-foreground">
{item.rsnExpln}
</p>
<div className="flex justify-between text-lg text-muted-foreground">
<span>{item.admdstNm}</span>
<span>
현재: {item.nowVlCn ?? "-"} / 비교:{" "}
{item.compVlCn ?? "-"}
</span>
</div>
</div>
))}
</div>
)}
</div>
);
return (
<div className="w-1/5 bg-container p-4 !pb-2 flex border-r border-border">
<div className="card card-border flex-1 bg-background !px-0">
<div className="border-b border-border flex justify-between">
<h2 className="text-[2.4rem] font-semibold text-title p-4 ">이상치 선별 보고서</h2>
<span
role="button"
tabIndex={0}
aria-label="닫기"
className="mt-2 mr-4 flex flex-shrink-0 cursor-pointer items-start justify-center text-3xl leading-none hover:text-[#2094fa]"
onClick={() => onClose?.()}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
onClose?.();
}
}}
>
<i className="ri-close-line" />
</span>
</div>
<div className="card-body max-h-[calc(100%-3.25rem)] p-2.5">
{isLoading && (
<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>
)}
{error && (
<div className="absolute inset-0 z-50 [data-theme='black']:bg-transparent rounded-lg p-10 flex justify-center items-center">
데이터를 불러올 수 없습니다.
</div>
)}
{!isLoading && !error && (
<section className="flex min-h-0 flex-1 flex-col h-[85rem]">
<Tabs
value={activeTab}
onValueChange={(value) =>
setActiveTab(value as VerificationReportTabKey)
}
className="flex min-h-0 flex-1 flex-col h-full"
variant="default"
>
<TabsList className="grid w-full grid-cols-3 p-1">
{VERIFICATION_REPORT_TABS.map((tab) => (
<TabsTrigger
key={tab.value}
value={tab.value}
className="!px-2 !py-2 text-[1.3rem] whitespace-normal"
>
{tab.label} ({listsByTab[tab.value].length})
</TabsTrigger>
))}
</TabsList>
{VERIFICATION_REPORT_TABS.map((tab) => (
<TabsContent
key={tab.value}
value={tab.value}
className="mt-0 flex min-h-0 flex-1 flex-col p-0 data-[state=inactive]:hidden"
>
{activeTab === tab.value
? renderResultList(
tab.value,
listsByTab[tab.value],
)
: null}
</TabsContent>
))}
</Tabs>
</section>
)}
</div>
</div>
</div>
);
};
export default VerificationSelectionReport;