"use client";
import { useEffect } from "react";
import { useTheme } from "@/components/theme/ThemeProvider";
import { FilterButton } from "@/components/ui/FilterButton";
import { GradeBadge } from "@/components/ui/GradeBadge";
import { ProgressBar } from "@/components/ui/ProgressBar";
import type { AreaFilter } from "@/lib/clean-logs/areas";
import { CLEAN_RESULT_PENDING_LABEL } from "@/lib/format/clean-result";
import { displayRate, isProblemGrade } from "@/lib/judge";
import { THEME_TOKENS } from "@/lib/theme";
import type { AreaCompletionsResponse, Grade } from "@/lib/types";
import styles from "./AreaModal.module.css";
interface AreaModalProps {
facilityName: string;
dateLabel: string;
/** ★サーバ側で絞るので、切り替えたら親が再取得する★(指示書Z 決定8) */
filter: AreaFilter;
onChangeFilter: (filter: AreaFilter) => void;
/** 取得できていないあいだは null(`loading` / `error` を見て出し分ける) */
data: AreaCompletionsResponse | null;
loading: boolean;
error: string | null;
onClose: () => void;
}
/** バーの色(モックと同じ対応)。★判定そのものはAPIが返した `grade` を使う★ */
function barTone(grade: Grade): "ok" | "wn" | "ng" {
if (grade === "◎" || grade === "○") return "ok";
if (grade === "△") return "wn";
return "ng";
}
/**
* エリア別清掃完了率のモーダル(F-02 の「エリア別詳細を見る →」から開く)。
*
* ★★並び順・判定・件数はすべてAPIが返したものをそのまま使う★★(`CLAUDE.md` §9.6)—
* ★フロントで並べ替え・再判定をしないこと★。`filter` の絞り込みもサーバ側。
*
* ★行の完了率は1回ごとの完了率★(`runCompletionRate()`。`min(clean_area ÷ task_area × 100, 100)` を
* 小数第1位まで切り捨て。D-155)。
* ★全体率(ヘッダの「全体」)は行の完了率の単純平均(清掃1回=1票)★なので、
* ★★行の完了率を足し合わせて件数で割れば全体率と一致する★★(未実施=完了率が `null` の行は
* 分子にも分母にも入れない)。計算は APIルート側の `buildAreaCompletions()`
* (`lib/clean-logs/areas.ts`)。★以前は全体だけ面積ベースの「意図した非対称」だった★
* (指示書Z 決定7)が、2026-09-30 に単純平均へ揃えた(D-151。指示書AQ §5-5)。
* ★2026-10-02 に、平均する材料を `clean_ratio` 列から1回ごとの完了率へ変えた(D-155。指示書AS)★。
*
* ★未実施のエリアは `completion_rate` が `null`★ → `displayRate()` が `-` を返し、
* バーは 0 幅、判定は `×`。★`0%` と表示しないこと★(D-107 落とし穴1)。
*/
export function AreaModal({
facilityName,
dateLabel,
filter,
onChangeFilter,
data,
loading,
error,
onClose,
}: AreaModalProps) {
const { t, theme } = useTheme();
const c = THEME_TOKENS[theme];
// ★Esc で閉じる★(指示書BC 5-4。フォーカスの移動・背景のスクロールの停止は本書ではやらない — §7-8)
useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
};
window.addEventListener("keydown", onKeyDown);
return () => window.removeEventListener("keydown", onKeyDown);
}, [onClose]);
return (
<div className={styles.overlay} role="dialog" aria-modal="true">
<div className={styles.panel}>
<div className={styles.header}>
<div className={styles.headerRow}>
<div>
<div className={styles.title}>{t("エリア別清掃完了率")}</div>
<div className={styles.meta}>
{facilityName} | {dateLabel}
{data && (
<>
{" "}
| {t("全体")} {displayRate(data.overall_completion_rate)}{" "}
{data.overall_grade}
</>
)}
</div>
</div>
<button type="button" className={styles.close} onClick={onClose} aria-label={t("閉じる")}>
✕
</button>
</div>
<div className={styles.filters}>
<FilterButton
active={filter === "problem"}
color={c.pr}
onClick={() => onChangeFilter("problem")}
>
{t("問題エリアのみ")}
</FilterButton>
<FilterButton
active={filter === "all"}
color={c.pr}
onClick={() => onChangeFilter("all")}
>
{t("全件")}
</FilterButton>
</div>
</div>
<div className={styles.body}>
{loading && <div className={styles.message}>{t("読み込み中…")}</div>}
{!loading && error && <div className={styles.message}>{error}</div>}
{!loading && !error && data && (
<>
<div className={styles.countLine}>
{filter === "problem"
? t("問題エリア({n} / {m}エリア)")
.replace("{n}", String(data.problem_count))
.replace("{m}", String(data.total_count))
: t("全{n}エリア(完了率昇順)").replace(
"{n}",
String(data.total_count)
)}
</div>
{data.items.length === 0 && (
<div className={styles.message}>
{/* ★「全部◎○だった」と「そもそもエリアが1つも無い」を区別する★ —
同じ文言にすると、データが取れていないことに気づけない */}
{data.total_count === 0
? t("エリアのデータがありません")
: t("問題エリアはありません(全エリア◎○)")}
</div>
)}
{data.items.map((item) => {
// ★結果待ち(item.result_pending。指示書BB A3 / D-173)★ — レポートがまだ届いていないだけかもしれない。
// ★判定(grade は "×" のまま)を見せない・問題エリアの赤字にしない★(清掃詳細ログの結果待ちと同じ見せ方:
// 完了率「-」・判定の札の代わりに「結果待ち」。`cleanLogRowView()` と同じ考え方)。
// ★APIが返した `grade` / `result_pending` を見るだけ(閾値でここで判定し直さない)★
const pending = item.result_pending;
const problem = !pending && isProblemGrade(item.grade);
return (
<div key={item.section_id} className={styles.row}>
<div
className={`${styles.areaName} ${problem ? styles.areaNameProblem : ""}`}
title={item.section_name}
>
{item.section_name}
</div>
<div className={styles.bar}>
{/* ★未実施・結果待ち(null)は 0 幅★ */}
<ProgressBar
percent={item.completion_rate ?? 0}
tone={barTone(item.grade)}
/>
</div>
{/* ★丸めるのは表示のときだけ(`CLAUDE.md` §4.3)。未実施・結果待ちは `-`★。結果待ちは色を付けない */}
<div className={styles.rate} data-tone={pending ? undefined : barTone(item.grade)}>
{displayRate(item.completion_rate)}
</div>
{/* ★札と「結果待ち」は同じ固定幅の枠に入れる★ — 幅が行ごとに変わると完了率の列がずれる */}
<div className={styles.gradeSlot}>
{pending ? (
<span className={styles.pending}>{t(CLEAN_RESULT_PENDING_LABEL)}</span>
) : (
<GradeBadge grade={item.grade} />
)}
</div>
</div>
);
})}
</>
)}
</div>
<div className={styles.footer}>
<button type="button" className={styles.closeBtn} onClick={onClose}>
{t("閉じる")}
</button>
</div>
</div>
</div>
);
}