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

@@ -6,6 +6,6 @@ NEXT_PUBLIC_USE_MOCK_BACKEND_LOOP_START=false
NEXT_PUBLIC_EXPORT_STATIC=false NEXT_PUBLIC_EXPORT_STATIC=false
NEXT_PUBLIC_USE_CGI=false NEXT_PUBLIC_USE_CGI=false
# App-Versionsnummer # App-Versionsnummer
NEXT_PUBLIC_APP_VERSION=1.6.898 NEXT_PUBLIC_APP_VERSION=1.6.899
NEXT_PUBLIC_CPL_MODE=json # json (Entwicklungsumgebung) oder jsSimulatedProd (CPL ->CGI-Interface-Simulator) oder production (CPL-> CGI-Interface Platzhalter) NEXT_PUBLIC_CPL_MODE=json # json (Entwicklungsumgebung) oder jsSimulatedProd (CPL ->CGI-Interface-Simulator) oder production (CPL-> CGI-Interface Platzhalter)

View File

@@ -5,5 +5,5 @@ NEXT_PUBLIC_CPL_API_PATH=/CPL
NEXT_PUBLIC_EXPORT_STATIC=true NEXT_PUBLIC_EXPORT_STATIC=true
NEXT_PUBLIC_USE_CGI=true NEXT_PUBLIC_USE_CGI=true
# App-Versionsnummer # App-Versionsnummer
NEXT_PUBLIC_APP_VERSION=1.6.898 NEXT_PUBLIC_APP_VERSION=1.6.899
NEXT_PUBLIC_CPL_MODE=production NEXT_PUBLIC_CPL_MODE=production

View File

@@ -1,3 +1,8 @@
## [1.6.899] 2025-09-10
- style: AnalogInputsChartModal dark mode
---
## [1.6.898] 2025-09-10 ## [1.6.898] 2025-09-10
- style: AnalogInputsSettingsModal dark mode - style: AnalogInputsSettingsModal dark mode

View File

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

View File

@@ -30,7 +30,7 @@ const SystemChartActionBar: React.FC<Props> = ({
<label className="font-medium text-sm">Zeitraum:</label> <label className="font-medium text-sm">Zeitraum:</label>
<Listbox value={zeitraum} onChange={setZeitraum}> <Listbox value={zeitraum} onChange={setZeitraum}>
<div className="relative w-48"> <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> <span>
{ {
{ DIA0: "Alle Messwerte", DIA1: "Stündlich", DIA2: "Täglich" }[ { DIA0: "Alle Messwerte", DIA1: "Stündlich", DIA2: "Täglich" }[
@@ -39,7 +39,7 @@ const SystemChartActionBar: React.FC<Props> = ({
} }
</span> </span>
<svg <svg
className="w-5 h-5 text-gray-400" className="w-5 h-5 text-[var(--color-fg-muted)]"
viewBox="0 0 20 20" viewBox="0 0 20 20"
fill="currentColor" fill="currentColor"
> >
@@ -50,20 +50,18 @@ const SystemChartActionBar: React.FC<Props> = ({
/> />
</svg> </svg>
</Listbox.Button> </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) => ( {["DIA0", "DIA1", "DIA2"].map((option) => (
<Listbox.Option <Listbox.Option
key={option} key={option}
value={option} value={option}
className={({ selected, active }) => className={({ selected, active }) => {
`px-4 py-1 cursor-pointer ${ const base = "px-4 py-1 cursor-pointer text-sm";
selected if (selected) return `${base} bg-littwin-blue text-white`; // selected highlight
? "bg-littwin-blue text-white" if (active)
: active return `${base} bg-[var(--color-surface-alt)] text-fg`;
? "bg-gray-200" return `${base} text-fg`;
: "" }}
}`
}
> >
{ {
{ {

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "cpl-v4", "name": "cpl-v4",
"version": "1.6.898", "version": "1.6.899",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "cpl-v4", "name": "cpl-v4",
"version": "1.6.898", "version": "1.6.899",
"dependencies": { "dependencies": {
"@emotion/react": "^11.13.0", "@emotion/react": "^11.13.0",
"@emotion/styled": "^11.13.0", "@emotion/styled": "^11.13.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "cpl-v4", "name": "cpl-v4",
"version": "1.6.898", "version": "1.6.899",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev -p 3000", "dev": "next dev -p 3000",