2026-03-09 10:36:03 +08:00
"use client" ;
2026-03-18 20:38:43 +08:00
import type { ChartConfiguration } from "chart.js" ;
2026-03-09 10:36:03 +08:00
import { useMemo } from "react" ;
import { useChart } from "@/hooks/useChart" ;
import { useDashboardStore , useHistoryData } from "@/hooks/useDashboardStore" ;
2026-03-10 04:45:40 +08:00
import { useI18n } from "@/hooks/useI18n" ;
2026-03-13 06:41:33 +08:00
import { ProFeaturePaywall } from "@/components/dashboard/ProFeaturePaywall" ;
2026-03-09 10:36:03 +08:00
import { getHistorySummary } from "@/lib/dashboard-utils" ;
function HistoryChart() {
const store = useDashboardStore ();
2026-03-10 04:45:40 +08:00
const { locale } = useI18n ();
2026-03-09 10:36:03 +08:00
const { data } = useHistoryData ();
2026-03-27 20:58:38 +08:00
const isNoaaSettlement =
store . selectedDetail ? . current ? . settlement_source === "noaa" ||
store . selectedDetail ? . current ? . settlement_source_label === "NOAA" ;
const noaaStationCode = String (
store . selectedDetail ? . current ? . station_code ||
store . selectedDetail ? . risk ? . icao ||
"NOAA" ,
)
. trim ()
. toUpperCase ();
2026-03-09 10:36:03 +08:00
const summary = useMemo (
() => getHistorySummary ( data , store . selectedDetail ? . local_date ),
[ data , store . selectedDetail ? . local_date ],
);
const hasMgm =
store . selectedCity === "ankara" &&
2026-03-29 21:09:16 +08:00
summary . mgmSeriesComplete &&
2026-03-09 10:36:03 +08:00
summary . mgms . some (( value ) => value != null );
2026-03-17 23:15:13 +08:00
const hasBestBaseline =
Boolean ( summary . bestModelName ) &&
summary . bestModelName !== "MGM" &&
summary . bestModelSeries . some (( value ) => value != null );
2026-03-09 10:36:03 +08:00
2026-03-13 07:56:20 +08:00
const canvasRef = useChart (() => {
const datasets : NonNullable <
ChartConfiguration < "line" > [ "data" ]
> [ "datasets" ] = [
{
backgroundColor : "rgba(248, 113, 113, 0.1)" ,
borderColor : "#f87171" ,
borderWidth : 2 ,
data : summary.actuals ,
2026-03-27 20:58:38 +08:00
label : isNoaaSettlement
2026-03-23 14:39:41 +08:00
? locale === "en-US"
2026-03-27 20:58:38 +08:00
? `NOAA Settled High ( ${ noaaStationCode } )`
: `NOAA 结算最高温 ( ${ noaaStationCode } )`
2026-03-23 14:39:41 +08:00
: locale === "en-US"
? "Observed High"
: "实测最高温" ,
2026-03-13 07:56:20 +08:00
pointBackgroundColor : "#f87171" ,
pointBorderColor : "#fff" ,
pointHoverRadius : 7 ,
pointRadius : 5 ,
tension : 0.2 ,
},
{
backgroundColor : "transparent" ,
borderColor : "#34d399" ,
borderDash : [ 5 , 4 ],
borderWidth : 2 ,
data : summary.debs ,
label : locale === "en-US" ? "DEB Fusion" : "DEB 融合" ,
pointHoverRadius : 6 ,
pointRadius : 4 ,
tension : 0.2 ,
},
];
2026-03-09 10:36:03 +08:00
2026-03-13 07:56:20 +08:00
if ( hasMgm ) {
datasets . push ({
backgroundColor : "transparent" ,
borderColor : "#fb923c" ,
borderWidth : 2 ,
data : summary.mgms ,
label : locale === "en-US" ? "MGM Official Forecast" : "MGM 官方预报" ,
pointHoverRadius : 6 ,
pointRadius : 4 ,
tension : 0.2 ,
});
}
2026-03-09 10:36:03 +08:00
2026-03-17 23:15:13 +08:00
if ( hasBestBaseline ) {
datasets . push ({
backgroundColor : "transparent" ,
borderColor : "#60a5fa" ,
borderDash : [ 4 , 3 ],
borderWidth : 2 ,
data : summary.bestModelSeries ,
label :
locale === "en-US"
? `Best Baseline ( ${ summary . bestModelName } )`
: `最佳单模型 ( ${ summary . bestModelName } )` ,
pointHoverRadius : 6 ,
pointRadius : 4 ,
tension : 0.2 ,
});
}
2026-03-13 07:56:20 +08:00
return {
data : {
datasets ,
labels : summary.dates ,
},
options : {
interaction : { intersect : false , mode : "index" },
maintainAspectRatio : false ,
plugins : {
legend : {
labels : {
boxHeight : 12 ,
boxWidth : 34 ,
color : "#94a3b8" ,
font : { family : "Inter" , size : 14 },
padding : 18 ,
2026-03-09 10:36:03 +08:00
},
2026-03-13 07:56:20 +08:00
},
tooltip : {
backgroundColor : "rgba(15, 23, 42, 0.9)" ,
borderColor : "rgba(255, 255, 255, 0.1)" ,
borderWidth : 1 ,
bodyFont : { family : "Inter" , size : 13 },
titleFont : { family : "Inter" , size : 13 , weight : 600 },
callbacks : {
label : ( ctx ) =>
` ${ ctx . dataset . label } : ${ ctx . parsed . y ? . toFixed ( 1 ) } °` ,
2026-03-09 10:36:03 +08:00
},
},
2026-03-13 07:56:20 +08:00
},
responsive : true ,
scales : {
x : {
grid : { color : "rgba(255,255,255,0.04)" },
ticks : {
color : "#64748b" ,
font : { family : "Inter" , size : 12 },
padding : 8 ,
2026-03-09 10:36:03 +08:00
},
2026-03-13 07:56:20 +08:00
},
y : {
grid : { color : "rgba(255,255,255,0.04)" },
ticks : {
color : "#64748b" ,
font : { family : "Inter" , size : 12 },
padding : 8 ,
2026-03-09 10:36:03 +08:00
},
},
},
2026-03-13 07:56:20 +08:00
},
type : "line" ,
} satisfies ChartConfiguration < "line" > ;
2026-03-27 20:58:38 +08:00
}, [ hasBestBaseline , hasMgm , isNoaaSettlement , noaaStationCode , summary , locale ]);
2026-03-09 10:36:03 +08:00
if ( ! summary . recentData . length ) return null ;
return (
< div className = "history-chart-wrapper" >
< canvas ref = { canvasRef } />
</ div >
);
}
export function HistoryModal() {
const store = useDashboardStore ();
2026-03-23 23:17:48 +08:00
const { t , locale } = useI18n ();
2026-04-13 18:42:27 +08:00
const { data , error , isLoading , isOpen , isRecordsLoading , meta } = useHistoryData ();
2026-03-13 06:41:33 +08:00
const isPro = store . proAccess . subscriptionActive ;
const isProLoading = store . proAccess . loading ;
2026-03-27 20:58:38 +08:00
const isNoaaSettlement =
store . selectedDetail ? . current ? . settlement_source === "noaa" ||
store . selectedDetail ? . current ? . settlement_source_label === "NOAA" ;
const noaaStationCode = String (
store . selectedDetail ? . current ? . station_code ||
store . selectedDetail ? . risk ? . icao ||
"NOAA" ,
)
. trim ()
. toUpperCase ();
const noaaStationName =
String ( store . selectedDetail ? . current ? . station_name || "" ). trim () ||
String ( store . selectedDetail ? . risk ? . airport || "" ). trim () ||
noaaStationCode ;
2026-03-09 10:36:03 +08:00
const summary = useMemo (
() => getHistorySummary ( data , store . selectedDetail ? . local_date ),
[ data , store . selectedDetail ? . local_date ],
);
2026-03-23 23:17:48 +08:00
const settledPeakRows = useMemo (
() =>
summary . recentData
. filter (
( row ) =>
row . actual != null &&
row . actual_peak_time &&
row . deb_at_peak_minus_12h != null ,
)
. reverse (),
[ summary . recentData ],
);
2026-04-17 19:49:01 +08:00
const modelReferenceRows = useMemo (
() =>
summary . recentData
. filter (
( row ) =>
row . actual != null &&
row . model_reference ? . available &&
( row . model_reference . models ? . length || 0 ) > 0 ,
)
. slice ( - 5 )
. reverse (),
[ summary . recentData ],
);
2026-03-09 10:36:03 +08:00
if ( ! isOpen ) return null ;
return (
< div
className = "modal-overlay"
role = "dialog"
aria-modal = "true"
aria-labelledby = "history-modal-title"
onClick = {( event ) => {
if ( event . target === event . currentTarget ) {
store . closeHistory ();
}
}}
>
2026-03-13 07:56:20 +08:00
{ isProLoading ? (
< div
className = "modal-content"
style = {{ padding : "40px" , textAlign : "center" }}
>
< div style = {{ color : "var(--text-muted)" }}>
{ t ( "dashboard.loading" )}
</ div >
2026-03-09 10:36:03 +08:00
</ div >
2026-03-13 07:56:20 +08:00
) : ! isPro ? (
< ProFeaturePaywall feature = "history" onClose = { store . closeHistory } />
) : (
< div className = "modal-content history-modal" >
< div className = "modal-header" >
2026-04-15 23:51:54 +08:00
< div className = "modal-title-stack" >
< div className = "modal-overline" >
< span >{ locale === "en-US" ? "Audit workspace" : "对账工作台" }</ span >
< span className = "modal-overline-sep" > • </ span >
< span >{ store . selectedCity ? . toUpperCase () || "" }</ span >
2026-04-13 18:42:27 +08:00
</ div >
2026-04-15 23:51:54 +08:00
< h2 id = "history-modal-title" >
{ t ( "history.title" , {
city : store.selectedCity?.toUpperCase () || "" ,
})}
</ h2 >
< div className = "modal-subtitle" >
{ locale === "en-US"
? "Observed highs, DEB path, and historical baseline consistency."
: "查看实测最高温、DEB 路径与历史基线是否一致。" }
</ div >
{ meta ? . mode === "preview" ? (
< div className = "modal-header-meta" >
< span className = "modal-meta-pill" >
{ isRecordsLoading
? locale === "en-US"
? "Full records syncing"
: "完整记录补齐中"
: meta . hasMore
? locale === "en-US"
? `Preview ${ meta . previewCount } / ${ meta . fullCount } `
: `预览 ${ meta . previewCount } / ${ meta . fullCount } `
: locale === "en-US"
? "Full set loaded"
: "完整记录已到齐" }
</ span >
</ div >
) : null }
</ div >
2026-03-13 07:56:20 +08:00
< button
type = "button"
className = "modal-close"
aria-label = { t ( "history.closeAria" )}
onClick = { store . closeHistory }
2026-03-13 06:41:33 +08:00
>
2026-03-13 07:56:20 +08:00
×
</ button >
</ div >
< div className = "modal-body" >
2026-03-27 20:58:38 +08:00
{ isNoaaSettlement && (
2026-04-15 23:51:54 +08:00
< div className = "modal-callout modal-callout-info" >
2026-03-23 14:39:41 +08:00
{ t ( "lang" ) === "en-US"
2026-03-27 20:58:38 +08:00
? ` ${ store . selectedDetail ? . display_name || store . selectedCity || "This city" } historical actuals are aligned to NOAA ${ noaaStationCode } ( ${ noaaStationName } ) settlement rules: use the highest rounded whole-degree Celsius reading after the date is finalized.`
: ` ${ store . selectedDetail ? . display_name || store . selectedCity || "该城市" } 历史对账已按 NOAA ${ noaaStationCode } ( ${ noaaStationName } )结算口径对齐:采用该日最终完成质控后的最高整度摄氏值。` }
2026-03-23 14:39:41 +08:00
</ div >
)}
2026-03-29 21:09:16 +08:00
{ isLoading ? (
< div className = "history-modal-loading" >
2026-03-29 21:21:52 +08:00
< div className = "history-fetch-loading" >
< div className = "history-fetch-scan" aria-hidden = "true" >
< span className = "history-fetch-ring history-fetch-ring-1" />
< span className = "history-fetch-ring history-fetch-ring-2" />
< span className = "history-fetch-sweep" />
< span className = "history-fetch-core" />
2026-03-29 21:09:16 +08:00
</ div >
2026-03-29 21:21:52 +08:00
< div className = "history-fetch-bars" aria-hidden = "true" >
< span className = "history-fetch-bar history-fetch-bar-1" />
< span className = "history-fetch-bar history-fetch-bar-2" />
< span className = "history-fetch-bar history-fetch-bar-3" />
< span className = "history-fetch-bar history-fetch-bar-4" />
2026-03-29 21:09:16 +08:00
</ div >
2026-03-29 21:21:52 +08:00
< div className = "history-fetch-lines" aria-hidden = "true" >
< span className = "history-fetch-line history-fetch-line-1" />
< span className = "history-fetch-line history-fetch-line-2" />
< span className = "history-fetch-line history-fetch-line-3" />
2026-03-29 21:09:16 +08:00
</ div >
2026-03-29 21:21:52 +08:00
< div className = "history-fetch-copy" >
2026-03-29 21:09:16 +08:00
< strong >
{ locale === "en-US"
? "Scanning archived settlement history"
: "正在扫描历史结算档案" }
</ strong >
< span >
{ locale === "en-US"
? "Reconciling settled highs, DEB traces, and baseline forecasts..."
: "正在对齐实测高温、DEB 轨迹与基线预报..." }
</ span >
</ div >
</ div >
</ div >
) : (
<>
2026-04-15 23:51:54 +08:00
< div className = "modal-section-heading" >
< div className = "modal-section-kicker" >
{ locale === "en-US" ? "Performance snapshot" : "表现快照" }
</ div >
< h3 >
{ locale === "en-US"
? "Recent settlement performance"
: "近期结算表现" }
</ h3 >
< div className = "modal-section-note" >
{ locale === "en-US"
? "Hit rate, MAE, and baseline comparison across recent settled days."
: "查看近期已结算样本里的命中率、误差与基线对照。" }
</ div >
</ div >
2026-03-29 21:09:16 +08:00
< div className = "history-stats" >
{ error ? (
2026-03-13 07:56:20 +08:00
< span style = {{ color : "var(--accent-red)" }}>
{ t ( "history.error" )}
</ span >
2026-03-29 21:09:16 +08:00
) : ! summary . recentData . length ? (
2026-03-13 07:56:20 +08:00
< span style = {{ color : "var(--text-muted)" }}>
{ t ( "history.empty" )}
</ span >
2026-03-29 21:09:16 +08:00
) : (
2026-03-13 07:56:20 +08:00
<>
< div className = "h-stat-card" >
2026-03-17 23:15:13 +08:00
< span className = "label" >{ t ( "history.debHitRate" )}</ span >
2026-03-13 07:56:20 +08:00
< span className = "val" >
{ summary . hitRate != null ? ` ${ summary . hitRate } %` : "--" }
</ span >
</ div >
< div className = "h-stat-card" >
2026-03-17 23:15:13 +08:00
< span className = "label" >{ t ( "history.debMae" )}</ span >
2026-03-13 07:56:20 +08:00
< span className = "val" >
{ summary . debMae != null ? ` ${ summary . debMae } °` : "--" }
</ span >
</ div >
2026-03-17 23:15:13 +08:00
< div className = "h-stat-card" >
< span className = "label" >{ t ( "history.bestModelMae" )}</ span >
< span className = "val" >
{ summary . bestModelMae != null
? ` ${ summary . bestModelMae } ° ${
summary . bestModelName
? ` ( ${ summary . bestModelName } )`
: ""
} `
: "--" }
</ span >
</ div >
< div className = "h-stat-card" >
< span className = "label" >{ t ( "history.debVsBest" )}</ span >
< span className = "val" >
{ summary . debWinRateVsBest != null
? ` ${ summary . debWinRateVsBest } % ( ${ summary . debWinDaysVsBest } / ${ summary . debVsBestComparableDays } )`
: "--" }
</ span >
</ div >
2026-03-13 07:56:20 +08:00
< div className = "h-stat-card" >
< span className = "label" >{ t ( "history.sample" )}</ span >
< span className = "val" >
{ t ( "history.sampleDays" , { count : summary.settledCount })}
</ span >
</ div >
</>
2026-03-29 21:09:16 +08:00
)}
</ div >
{ ! error && < HistoryChart />}
2026-04-17 19:49:01 +08:00
{ ! error && modelReferenceRows . length > 0 && (
< div className = "history-model-reference" >
< div className = "modal-section-heading" >
< div className = "modal-section-kicker" >
{ locale === "en-US" ? "Reference layer" : "参考层" }
</ div >
< div className = "history-peak-reference-title" >
{ locale === "en-US"
? "Model Reference at Cutoff"
: "当时模型参考" }
</ div >
< div className = "modal-section-note" >
{ locale === "en-US"
? "These are archived model snapshots used for audit context. Settlement truth still comes only from the finalized observation source."
: "这里展示当时归档的模型快照,仅用于解释判断背景;结算真值仍只来自最终实况来源。" }
</ div >
</ div >
< div className = "history-model-reference-scroll" >
{ modelReferenceRows . map (( row ) => {
const models = ( row . model_reference ? . models || []). slice ( 0 , 6 );
return (
< div key = { row . date } className = "history-model-reference-row" >
< div className = "history-peak-reference-date" >
{ row . date }
</ div >
< div className = "history-model-reference-body" >
< div className = "history-model-reference-summary" >
< span >
{ locale === "en-US" ? "Actual" : "最终实测" }{ " " }
< strong >
{ row . actual }
{ store . selectedDetail ? . temp_symbol || "°C" }
</ strong >
</ span >
< span >
DEB { " " }
< strong >
{ row . model_reference ? . deb ? . value ?? row . deb ?? "--" }
{ store . selectedDetail ? . temp_symbol || "°C" }
</ strong >
{ row . model_reference ? . deb ? . error != null
? ` / ${ locale === "en-US" ? "err" : "误差" } ${ row . model_reference . deb . error }${ store . selectedDetail ? . temp_symbol || "°C" } `
: "" }
</ span >
</ div >
< div className = "history-model-reference-models" >
{ models . map (( model ) => (
< div
key = { ` ${ row . date } - ${ model . model } ` }
className = "history-model-reference-model"
>
< span className = "history-model-name" >
{ model . model }
</ span >
< span >
{ model . value }
{ store . selectedDetail ? . temp_symbol || "°C" }
</ span >
< span className = "history-model-error" >
{ model . error != null
? ` ${ locale === "en-US" ? "err" : "误差" } ${ model . error }${ store . selectedDetail ? . temp_symbol || "°C" } `
: locale === "en-US"
? "pending"
: "待结算" }
</ span >
</ div >
))}
</ div >
</ div >
</ div >
);
})}
</ div >
</ div >
)}
2026-03-29 21:09:16 +08:00
{ ! error && settledPeakRows . length > 0 && (
2026-03-29 20:53:48 +08:00
< div className = "history-peak-reference" >
2026-04-15 23:51:54 +08:00
< div className = "modal-section-heading" >
< div className = "modal-section-kicker" >
{ locale === "en-US" ? "Reference table" : "参考表" }
</ div >
< div className = "history-peak-reference-title" >
{ locale === "en-US"
? "Peak-12h DEB Reference (Approx.)"
: "峰值前 12 小时 DEB 参考(近似)" }
</ div >
< div className = "modal-section-note" >
{ locale === "en-US"
? "Use peak-minus-12h DEB as a fast sanity check for settled highs."
: "用峰值前 12 小时的 DEB 作为历史结算高温的快速校验。" }
</ div >
2026-03-23 23:17:48 +08:00
</ div >
2026-03-29 20:53:48 +08:00
< div className = "history-peak-reference-scroll" >
2026-03-23 23:17:48 +08:00
{ settledPeakRows . map (( row ) => (
2026-03-29 20:53:48 +08:00
< div key = { row . date } className = "history-peak-reference-row" >
< div className = "history-peak-reference-date" >
2026-03-23 23:17:48 +08:00
{ row . date }
</ div >
2026-03-29 20:53:48 +08:00
< div className = "history-peak-reference-meta" >
2026-03-23 23:17:48 +08:00
< div >
{ locale === "en-US" ? "Peak ref" : "峰值参考" } : { " " }
< span style = {{ color : "var(--text-primary)" }}>
{ row . actual }
{ store . selectedDetail ? . temp_symbol || "°C" } @ { " " }
{ row . actual_peak_time }
</ span >
</ div >
< div >
{ locale === "en-US" ? "DEB@-12h" : "峰值前12小时 DEB" } : { " " }
< span style = {{ color : "var(--text-primary)" }}>
{ row . deb_at_peak_minus_12h }
{ store . selectedDetail ? . temp_symbol || "°C" } @ { " " }
{ row . deb_at_peak_minus_12h_time }
</ span >
</ div >
< div >
{ locale === "en-US" ? "Actual" : "最终实测" } : { " " }
< span style = {{ color : "var(--text-primary)" }}>
{ row . actual }
{ store . selectedDetail ? . temp_symbol || "°C" }
</ span >
</ div >
< div >
{ locale === "en-US" ? "Error" : "误差" } : { " " }
< span
style = {{
color :
( row . deb_at_peak_minus_12h_error ?? 0 ) > 0
? "#f59e0b"
: "#34d399" ,
}}
>
{ row . deb_at_peak_minus_12h_error != null
? ` ${ row . deb_at_peak_minus_12h_error > 0 ? "+" : "" }${ row . deb_at_peak_minus_12h_error }${ store . selectedDetail ? . temp_symbol || "°C" } `
: "--" }
</ span >
</ div >
</ div >
</ div >
))}
</ div >
</ div >
2026-03-29 21:09:16 +08:00
)}
</>
2026-03-23 23:17:48 +08:00
)}
2026-03-13 07:56:20 +08:00
</ div >
2026-03-09 10:36:03 +08:00
</ div >
2026-03-13 07:56:20 +08:00
)}
2026-03-09 10:36:03 +08:00
</ div >
);
}