style: Detailansicht Modal dark mode

This commit is contained in:
ISA
2025-09-10 12:25:40 +02:00
parent f2a5f2083a
commit 41910e450e
7 changed files with 88 additions and 58 deletions

View File

@@ -27,6 +27,10 @@ import {
Legend,
Filler,
TimeScale,
type ChartDataset,
type ChartOptions,
type ChartData,
type Chart,
} from "chart.js";
import "chartjs-adapter-date-fns";
@@ -65,7 +69,7 @@ type ReduxDataEntry = {
m?: number; // aktueller Messwert (optional, falls vorhanden)
};
const chartOptions = {
const chartOptions: ChartOptions<"line"> = {
responsive: true,
maintainAspectRatio: false,
plugins: {
@@ -78,9 +82,11 @@ const chartOptions = {
mode: "index" as const,
intersect: false,
callbacks: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
label: function (ctx: any) {
return `Messwert: ${ctx.parsed.y}`;
},
// eslint-disable-next-line @typescript-eslint/no-explicit-any
title: function (items: any[]) {
const date = items[0].parsed.x;
return `Zeitpunkt: ${new Date(date).toLocaleString("de-DE")}`;
@@ -140,13 +146,13 @@ export const DetailModal = ({
}: Props) => {
// Stable empty reference to avoid React-Redux dev warning about selector returning new [] each call
const EMPTY_REDUX_DATA: ReadonlyArray<ReduxDataEntry> = Object.freeze([]);
const chartRef = useRef<any>(null);
const [chartData, setChartData] = useState<any>({
const chartRef = useRef<Chart<"line"> | null>(null);
const [chartData, setChartData] = useState<ChartData<"line">>({
datasets: [],
});
const [isLoading, setIsLoading] = useState(false);
const [shouldUpdateChart, setShouldUpdateChart] = useState(false);
const [forceUpdate, setForceUpdate] = useState(0); // Für periodische UI-Updates
// const [forceUpdate, setForceUpdate] = useState(0); // Für periodische UI-Updates (derzeit nicht benötigt)
const reduxData = useSelector((state: RootState) => {
switch (selectedKey) {
@@ -221,11 +227,8 @@ export const DetailModal = ({
// Periodische UI-Updates alle 2 Sekunden während Wartezeit
useEffect(() => {
if (isOpen && (!chartData.datasets || chartData.datasets.length === 0)) {
const interval = setInterval(() => {
setForceUpdate((prev) => prev + 1); // Force re-render für cursor-wait Update
}, 2000);
return () => clearInterval(interval);
// Optional: periodische Re-Renders wurden deaktiviert, da nicht mehr notwendig
// (kann wieder aktiviert werden falls Cursor-Animation erwünscht ist)
}
}, [isOpen, chartData.datasets]);
@@ -274,7 +277,12 @@ export const DetailModal = ({
useEffect(() => {
if (chartRef.current && selectedKey) {
chartRef.current.options.plugins.title.text = `Verlauf ${selectedKey}`;
const opts = chartRef.current.options as ChartOptions<"line"> & {
plugins?: { title?: { text?: string } };
};
if (opts.plugins?.title) {
opts.plugins.title.text = `Verlauf ${selectedKey}`;
}
chartRef.current.update("none");
}
}, [selectedKey]);
@@ -290,14 +298,13 @@ export const DetailModal = ({
if (chartRef.current && isLoading) {
const chartInstance = chartRef.current;
// Save previous callback to restore later
const prevCallback = chartInstance.options.animation?.onComplete;
chartInstance.options.animation = {
...chartInstance.options.animation,
onComplete: () => {
setIsLoading(false);
if (typeof prevCallback === "function") prevCallback();
},
const animation: any = chartInstance.options.animation || {}; // eslint-disable-line @typescript-eslint/no-explicit-any
const prevCallback = animation.onComplete;
animation.onComplete = () => {
setIsLoading(false);
if (typeof prevCallback === "function") prevCallback();
};
chartInstance.options.animation = animation;
chartInstance.update();
}
}, [chartData, isLoading]);
@@ -338,7 +345,7 @@ export const DetailModal = ({
}
// Create datasets array for multiple lines
const datasets = [];
const datasets: ChartDataset<"line">[] = [];
// Check which data fields are available and create datasets accordingly
const hasMinimum = filtered.some(
@@ -416,7 +423,8 @@ export const DetailModal = ({
});
}
const newChartData = {
const newChartData: ChartData<"line"> = {
labels: [],
datasets: datasets,
};
@@ -428,7 +436,13 @@ export const DetailModal = ({
setChartData({ datasets: [] });
setShouldUpdateChart(false); // Reset flag
}
}, [reduxData, selectedKey, shouldUpdateChart]);
}, [
reduxData,
selectedKey,
shouldUpdateChart,
pickerVonDatum,
pickerBisDatum,
]);
if (!isOpen || !selectedKey) return null;
@@ -442,19 +456,25 @@ export const DetailModal = ({
}`}
>
<div
className={`bg-white p-6 rounded-xl overflow-auto shadow-2xl transition-all duration-300 ${
isFullScreen ? "w-[95vw] h-[90vh]" : "w-[50%] h-[60%]"
role="dialog"
aria-modal="true"
className={`bg-[var(--color-surface)] text-fg border border-base rounded-xl shadow-xl flex flex-col overflow-hidden transition-all duration-300 ${
isFullScreen
? "w-[90vw] h-[90vh]"
: "w-[70rem] max-w-[95vw] h-[40rem]"
} ${!hasChartData ? "cursor-wait" : ""}`}
>
<div className="relative">
<h2 className="text-xl font-semibold">
{/* Header */}
<header className="flex items-center justify-between px-6 py-4 border-b border-base select-none bg-[var(--color-surface)]">
<h2 className="text-base font-bold tracking-wide">
Detailansicht: {selectedKey}
</h2>
<div className="absolute top-0 right-0 flex gap-3">
<div className="flex items-center gap-3 text-lg">
<button
onClick={toggleFullScreen}
className="text-2xl text-[var(--color-fg-muted)] hover:text-[var(--color-fg)] transition"
className="icon-btn text-[1.4rem] hover:text-fg transition"
aria-label={isFullScreen ? "Vollbild verlassen" : "Vollbild"}
type="button"
>
<i
className={
@@ -462,28 +482,35 @@ export const DetailModal = ({
? "bi bi-fullscreen-exit"
: "bi bi-arrows-fullscreen"
}
></i>
/>
</button>
<button
onClick={handleClose}
className="text-2xl text-[var(--color-fg-muted)] hover:text-[var(--color-danger)] transition"
className="icon-btn text-[1.4rem] transition"
aria-label="Modal schließen"
type="button"
>
<i className="bi bi-x-circle-fill"></i>
<i className="bi bi-x-circle-fill" />
</button>
</div>
</div>
</header>
<SystemChartActionBar
zeitraum={zeitraum}
setZeitraum={setZeitraum}
onFetchData={handleFetchData}
isLoading={isLoading}
/>
<div className="h-[85%] rounded shadow border p-2 bg-[var(--color-surface)] border-[var(--color-border)]">
<Line ref={chartRef} data={chartData} options={chartOptions} />
{/* Body */}
<div className="flex-1 min-h-0 flex flex-col px-6 pt-4 pb-5 bg-[var(--color-surface)] overflow-hidden">
<div className="mb-3">
<SystemChartActionBar
zeitraum={zeitraum}
setZeitraum={setZeitraum}
onFetchData={handleFetchData}
isLoading={isLoading}
className="mb-0"
/>
</div>
<div className="flex-1 min-h-0 rounded-lg border border-base bg-[var(--color-surface-alt)] px-3 py-2 shadow-inner">
<Line ref={chartRef} data={chartData} options={chartOptions} />
</div>
</div>
{/* Optional Footer (currently empty, reserved for future) */}
</div>
</div>
);

View File

@@ -30,7 +30,7 @@ const SystemChartActionBar: React.FC<Props> = ({
<label className="font-medium text-sm">Zeitraum:</label>
<Listbox value={zeitraum} onChange={setZeitraum}>
<div className="relative w-48">
<Listbox.Button className="w-full border px-3 py-1 rounded text-left bg-white flex justify-between items-center text-sm">
<Listbox.Button className="w-full border border-base px-3 py-1 rounded text-left bg-[var(--color-surface-alt)] text-fg flex justify-between items-center text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-accent)]/40 transition">
<span>
{
{ DIA0: "Alle Messwerte", DIA1: "Stündlich", DIA2: "Täglich" }[
@@ -39,7 +39,7 @@ const SystemChartActionBar: React.FC<Props> = ({
}
</span>
<svg
className="w-5 h-5 text-gray-400"
className="w-5 h-5 text-[var(--color-fg-muted)]"
viewBox="0 0 20 20"
fill="currentColor"
>
@@ -50,20 +50,18 @@ const SystemChartActionBar: React.FC<Props> = ({
/>
</svg>
</Listbox.Button>
<Listbox.Options className="absolute z-50 mt-1 w-full border rounded bg-white shadow max-h-60 overflow-auto text-sm">
<Listbox.Options className="absolute z-50 mt-1 w-full border border-base rounded bg-[var(--color-surface)] text-fg shadow-lg max-h-60 overflow-auto text-sm focus:outline-none">
{["DIA0", "DIA1", "DIA2"].map((option) => (
<Listbox.Option
key={option}
value={option}
className={({ selected, active }) =>
`px-4 py-1 cursor-pointer ${
selected
? "bg-littwin-blue text-white"
: active
? "bg-gray-200"
: ""
}`
}
className={({ selected, active }) => {
const base = "px-4 py-1 cursor-pointer text-sm";
if (selected) return `${base} bg-littwin-blue text-white`; // selected highlight
if (active)
return `${base} bg-[var(--color-surface-alt)] text-fg`;
return `${base} text-fg`;
}}
>
{
{