From 9bd22dfcb7db35fcb509a2f32caadefc7eef7c03 Mon Sep 17 00:00:00 2001 From: Grovy311 Date: Mon, 3 Aug 2026 17:07:14 +0200 Subject: [PATCH] =?UTF-8?q?Add=20AMEV=20Gleichzeitigkeitsfaktor-Ausw=C3=A4?= =?UTF-8?q?hler=20und=20einklappbare=20Seitenleiste?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Gzf-Picker-Modal mit AMEV-Tabelle 3 (Planungshilfe für elektrische Leistungsbilanzen) im Projektgerät-Modal und im globalen Geräteformular - Docker-Deploy-Tooling (docker-start.sh, run-migrations.js) für den produktiven Container - Seitenleiste ist jetzt einklappbar (Icon-only, Zustand in localStorage) --- .dockerignore | 8 +- Dockerfile | 14 +-- scripts/docker-start.sh | 16 ++++ scripts/run-migrations.js | 16 ++++ src/app/globals.css | 65 ++++++++++++++ src/app/projects/page.tsx | 46 +++++++--- src/frontend/components/AppShell.tsx | 19 +++- src/frontend/components/Sidebar.tsx | 35 ++++++-- src/frontend/components/gzf-picker-modal.tsx | 86 +++++++++++++++++++ .../components/project-device-modal.tsx | 47 ++++++++-- src/shared/constants/amev-gzf.ts | 38 ++++++++ 11 files changed, 349 insertions(+), 41 deletions(-) create mode 100644 scripts/docker-start.sh create mode 100644 scripts/run-migrations.js create mode 100644 src/frontend/components/gzf-picker-modal.tsx create mode 100644 src/shared/constants/amev-gzf.ts diff --git a/.dockerignore b/.dockerignore index 69951f8..80054d0 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,8 +1,8 @@ .git -.agents -.codex .next -data dist node_modules -npm-debug.log* +data +*.db +.env +.env.* diff --git a/Dockerfile b/Dockerfile index 70e7761..41a08fd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,14 @@ -FROM node:22-bookworm-slim - +FROM node:22 WORKDIR /app -ENV NEXT_TELEMETRY_DISABLED=1 - -COPY package.json package-lock.json ./ +COPY package*.json ./ RUN npm ci COPY . . +RUN npm run build:api && npm run build:web -EXPOSE 3000 3001 +RUN mkdir -p data && chmod +x scripts/docker-start.sh + +EXPOSE 3001 + +CMD ["sh", "scripts/docker-start.sh"] diff --git a/scripts/docker-start.sh b/scripts/docker-start.sh new file mode 100644 index 0000000..bf0d856 --- /dev/null +++ b/scripts/docker-start.sh @@ -0,0 +1,16 @@ +#!/bin/sh +set -e + +echo "Running migrations..." +node scripts/run-migrations.js + +echo "Starting API server on :3000..." +node dist/server/index.js & + +echo "Waiting for API..." +until node -e "require('http').get('http://localhost:3000/health', r => process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))" 2>/dev/null; do + sleep 1 +done + +echo "Starting Next.js on :3001..." +exec node_modules/.bin/next start -p 3001 diff --git a/scripts/run-migrations.js b/scripts/run-migrations.js new file mode 100644 index 0000000..ebbed8a --- /dev/null +++ b/scripts/run-migrations.js @@ -0,0 +1,16 @@ +const { migrate } = require('drizzle-orm/better-sqlite3/migrator'); +const { drizzle } = require('drizzle-orm/better-sqlite3'); +const Database = require('better-sqlite3'); +const fs = require('node:fs'); +const path = require('node:path'); + +const dataDir = path.resolve('data'); +if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true }); + +const sqlite = new Database(path.join(dataDir, 'leistungsbilanz.db')); +const db = drizzle(sqlite); + +migrate(db, { migrationsFolder: path.resolve('src/db/migrations') }); +sqlite.close(); + +console.log('Migrations applied'); diff --git a/src/app/globals.css b/src/app/globals.css index 27b2d63..9c07a40 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1219,3 +1219,68 @@ a.kpi:hover { max-height: calc(100vh - 5.5rem); } } + +/* ── Seitenleiste: eingeklappter Zustand ─────────────────────────────── */ + +.app-shell { + transition: grid-template-columns 0.18s ease; +} + +.app-shell-collapsed { + grid-template-columns: 64px 1fr; +} + +.sidebar { + transition: padding 0.18s ease; +} + +.sidebar-brand { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 8px; +} + +.sidebar-toggle { + background: rgba(255, 255, 255, 0.08); + border: 0; + border-radius: 6px; + color: #fff; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + font-size: 0.8rem; + line-height: 1; + width: 26px; + height: 26px; +} + +.sidebar-toggle:hover { + background: rgba(255, 255, 255, 0.18); +} + +.sidebar-collapsed .sidebar-brand { + padding: 16px 10px; + flex-direction: column; + align-items: center; +} + +.sidebar-collapsed .sidebar-brand-text { + display: none; +} + +.sidebar-collapsed .sidebar-nav { + padding: 12px 8px; +} + +.sidebar-collapsed .sidebar-link { + justify-content: center; + padding: 10px; +} + +.sidebar-collapsed .sidebar-section-label, +.sidebar-collapsed .sidebar-label { + display: none; +} diff --git a/src/app/projects/page.tsx b/src/app/projects/page.tsx index 93a2d14..21409fd 100644 --- a/src/app/projects/page.tsx +++ b/src/app/projects/page.tsx @@ -14,6 +14,7 @@ import type { CreateGlobalDeviceInput, GlobalDeviceDto, } from "../../frontend/types"; +import { GzfPickerModal } from "../../frontend/components/gzf-picker-modal"; const PROJECTS_CHANGED_EVENT = "leistungsbilanz:projects-changed"; @@ -67,6 +68,7 @@ export default function ProjectsPage() { id: string; name: string; } | null>(null); + const [isGzfPickerOpen, setIsGzfPickerOpen] = useState(false); async function loadData() { setError(null); @@ -380,18 +382,28 @@ export default function ProjectsPage() { } /> -
- - setGlobalDeviceForm((current) => ({ ...current, demandFactor: event.target.value })) - } - /> +
+
+ + setGlobalDeviceForm((current) => ({ ...current, demandFactor: event.target.value })) + } + /> + +
update("simultaneityFactor", event.target.value)} + step="0.01" + type="number" + value={values.simultaneityFactor} + /> + +
+
+ {isGzfPickerOpen ? ( + setIsGzfPickerOpen(false)} + onSelect={(value) => { + update("simultaneityFactor", String(value)); + setIsGzfPickerOpen(false); + }} + /> + ) : null} + ); } diff --git a/src/shared/constants/amev-gzf.ts b/src/shared/constants/amev-gzf.ts new file mode 100644 index 0000000..0833091 --- /dev/null +++ b/src/shared/constants/amev-gzf.ts @@ -0,0 +1,38 @@ +export interface AmevGzfEntry { + group: string; + range: string; + mean: number; + overallRange?: string; +} + +// Quelle: AMEV „Planungshilfe für elektrische Leistungsbilanzen" (rev. 4), +// Tabelle 3 – Gleichzeitigkeitsfaktoren für Verbrauchergruppen. +export const AMEV_GZF_TABLE: AmevGzfEntry[] = [ + { group: "Absaugung, Digestorien", range: "0,7", mean: 0.7 }, + { group: "Aufzüge/Rolltreppen", range: "0,2 – 0,7", mean: 0.5, overallRange: "0,2" }, + { group: "Beleuchtungsanlagen", range: "0,7 – 0,8", mean: 0.75, overallRange: "0,4" }, + { group: "Beleuchtungsanlagen in innenliegenden Räumen", range: "0,7 – 0,9", mean: 0.8 }, + { group: "IuK-Anlagen", range: "1", mean: 1, overallRange: "1" }, + { group: "Experimentieranlagen", range: "0,2 – 0,4", mean: 0.3 }, + { group: "Heizung", range: "0,7 – 1", mean: 0.85 }, + { group: "Kälteanlagen", range: "0,8 – 1", mean: 0.9, overallRange: "0,4 – 0,5" }, + { group: "Küchen elektrisch ohne Energieoptimierung", range: "0,3 – 0,7", mean: 0.6 }, + { group: "Küchen elektrisch mit Energieoptimierung", range: "0,2 – 0,6", mean: 0.4 }, + { group: "Lastenaufzüge, Krananlagen", range: "0,2", mean: 0.2 }, + { + group: "Lüftungsanlagen, kontrollierte Lüftung", + range: "0,7", + mean: 0.7, + overallRange: "0,4 – 0,5", + }, + { + group: "Steckdosen 230 V allg. Verbraucher (100 W/Steckdose)", + range: "0,1 – 0,3", + mean: 0.2, + }, + { group: "Steckdosen 230 V für IuK (100 W/Steckdose)", range: "0,7 – 0,9", mean: 0.8 }, + { group: "Steckdosen 400 V (1000 W/Steckdose)", range: "0,1 – 0,5", mean: 0.35 }, + { group: "Schmutz-, Warmwasserpumpen", range: "0,2 – 0,4", mean: 0.3 }, + { group: "Umwälzpumpen", range: "0,6 – 1", mean: 0.8 }, + { group: "Werkstätten", range: "0,2 – 0,4", mean: 0.3 }, +];