Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9bd22dfcb7 |
+4
-4
@@ -1,8 +1,8 @@
|
|||||||
.git
|
.git
|
||||||
.agents
|
|
||||||
.codex
|
|
||||||
.next
|
.next
|
||||||
data
|
|
||||||
dist
|
dist
|
||||||
node_modules
|
node_modules
|
||||||
npm-debug.log*
|
data
|
||||||
|
*.db
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
|||||||
+8
-6
@@ -1,12 +1,14 @@
|
|||||||
FROM node:22-bookworm-slim
|
FROM node:22
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
COPY package*.json ./
|
||||||
|
|
||||||
COPY package.json package-lock.json ./
|
|
||||||
RUN npm ci
|
RUN npm ci
|
||||||
|
|
||||||
COPY . .
|
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"]
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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');
|
||||||
@@ -1219,3 +1219,68 @@ a.kpi:hover {
|
|||||||
max-height: calc(100vh - 5.5rem);
|
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;
|
||||||
|
}
|
||||||
|
|||||||
+34
-12
@@ -14,6 +14,7 @@ import type {
|
|||||||
CreateGlobalDeviceInput,
|
CreateGlobalDeviceInput,
|
||||||
GlobalDeviceDto,
|
GlobalDeviceDto,
|
||||||
} from "../../frontend/types";
|
} from "../../frontend/types";
|
||||||
|
import { GzfPickerModal } from "../../frontend/components/gzf-picker-modal";
|
||||||
|
|
||||||
const PROJECTS_CHANGED_EVENT = "leistungsbilanz:projects-changed";
|
const PROJECTS_CHANGED_EVENT = "leistungsbilanz:projects-changed";
|
||||||
|
|
||||||
@@ -67,6 +68,7 @@ export default function ProjectsPage() {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
const [isGzfPickerOpen, setIsGzfPickerOpen] = useState(false);
|
||||||
|
|
||||||
async function loadData() {
|
async function loadData() {
|
||||||
setError(null);
|
setError(null);
|
||||||
@@ -380,18 +382,28 @@ export default function ProjectsPage() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-6 col-md-1">
|
<div className="col-6 col-md-2">
|
||||||
<input
|
<div className="input-group">
|
||||||
className="form-control"
|
<input
|
||||||
type="number"
|
className="form-control"
|
||||||
step="0.01"
|
type="number"
|
||||||
min="0"
|
step="0.01"
|
||||||
max="1"
|
min="0"
|
||||||
value={globalDeviceForm.demandFactor}
|
max="1"
|
||||||
onChange={(event) =>
|
value={globalDeviceForm.demandFactor}
|
||||||
setGlobalDeviceForm((current) => ({ ...current, demandFactor: event.target.value }))
|
onChange={(event) =>
|
||||||
}
|
setGlobalDeviceForm((current) => ({ ...current, demandFactor: event.target.value }))
|
||||||
/>
|
}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className="btn btn-outline-primary"
|
||||||
|
onClick={() => setIsGzfPickerOpen(true)}
|
||||||
|
title="Gleichzeitigkeitsfaktor nach AMEV wählen"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
AMEV
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-6 col-md-1">
|
<div className="col-6 col-md-1">
|
||||||
<select
|
<select
|
||||||
@@ -505,6 +517,16 @@ export default function ProjectsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isGzfPickerOpen ? (
|
||||||
|
<GzfPickerModal
|
||||||
|
onClose={() => setIsGzfPickerOpen(false)}
|
||||||
|
onSelect={(value) => {
|
||||||
|
setGlobalDeviceForm((current) => ({ ...current, demandFactor: String(value) }));
|
||||||
|
setIsGzfPickerOpen(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,36 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { Sidebar } from "./Sidebar";
|
import { Sidebar } from "./Sidebar";
|
||||||
|
|
||||||
const PROJECT_AREA_PATTERN = /^\/projects(\/[^/]+)?$/;
|
const PROJECT_AREA_PATTERN = /^\/projects(\/[^/]+)?$/;
|
||||||
|
const SIDEBAR_COLLAPSED_STORAGE_KEY = "leistungsbilanz:sidebar-collapsed";
|
||||||
|
|
||||||
export function AppShell({ children }: { children: React.ReactNode }) {
|
export function AppShell({ children }: { children: React.ReactNode }) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const showSidebar = PROJECT_AREA_PATTERN.test(pathname ?? "");
|
const showSidebar = PROJECT_AREA_PATTERN.test(pathname ?? "");
|
||||||
|
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsSidebarCollapsed(window.localStorage.getItem(SIDEBAR_COLLAPSED_STORAGE_KEY) === "1");
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
function toggleSidebarCollapsed() {
|
||||||
|
setIsSidebarCollapsed((current) => {
|
||||||
|
const next = !current;
|
||||||
|
window.localStorage.setItem(SIDEBAR_COLLAPSED_STORAGE_KEY, next ? "1" : "0");
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!showSidebar) {
|
if (!showSidebar) {
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="app-shell">
|
<div className={isSidebarCollapsed ? "app-shell app-shell-collapsed" : "app-shell"}>
|
||||||
<Sidebar />
|
<Sidebar isCollapsed={isSidebarCollapsed} onToggleCollapsed={toggleSidebarCollapsed} />
|
||||||
<div className="app-shell-content">{children}</div>
|
<div className="app-shell-content">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,7 +14,12 @@ const PROJECT_SECTIONS = [
|
|||||||
{ id: "projektgeraete", label: "Projektgeräte", icon: "⚙" },
|
{ id: "projektgeraete", label: "Projektgeräte", icon: "⚙" },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export function Sidebar() {
|
interface SidebarProps {
|
||||||
|
isCollapsed: boolean;
|
||||||
|
onToggleCollapsed: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Sidebar({ isCollapsed, onToggleCollapsed }: SidebarProps) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const projectMatch = pathname?.match(/^\/projects\/([^/]+)$/);
|
const projectMatch = pathname?.match(/^\/projects\/([^/]+)$/);
|
||||||
const activeProjectId = projectMatch ? projectMatch[1] : null;
|
const activeProjectId = projectMatch ? projectMatch[1] : null;
|
||||||
@@ -66,19 +71,31 @@ export function Sidebar() {
|
|||||||
}, [activeProjectId]);
|
}, [activeProjectId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="sidebar">
|
<aside className={isCollapsed ? "sidebar sidebar-collapsed" : "sidebar"}>
|
||||||
<div className="sidebar-brand">
|
<div className="sidebar-brand">
|
||||||
<div className="sidebar-wordmark">Leistungsbilanz</div>
|
<div className="sidebar-brand-text">
|
||||||
<div className="sidebar-bar" />
|
<div className="sidebar-wordmark">Leistungsbilanz</div>
|
||||||
<div className="sidebar-tag">Stromkreis- & Lastberechnung</div>
|
<div className="sidebar-bar" />
|
||||||
|
<div className="sidebar-tag">Stromkreis- & Lastberechnung</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
aria-label={isCollapsed ? "Seitenleiste ausklappen" : "Seitenleiste einklappen"}
|
||||||
|
className="sidebar-toggle"
|
||||||
|
onClick={onToggleCollapsed}
|
||||||
|
title={isCollapsed ? "Seitenleiste ausklappen" : "Seitenleiste einklappen"}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{isCollapsed ? "»" : "«"}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<nav className="sidebar-nav">
|
<nav className="sidebar-nav">
|
||||||
<Link
|
<Link
|
||||||
href="/projects"
|
href="/projects"
|
||||||
className={pathname === "/projects" ? "sidebar-link active" : "sidebar-link"}
|
className={pathname === "/projects" ? "sidebar-link active" : "sidebar-link"}
|
||||||
|
title="Alle Projekte"
|
||||||
>
|
>
|
||||||
<span className="sidebar-icon">▦</span>
|
<span className="sidebar-icon">▦</span>
|
||||||
Alle Projekte
|
<span className="sidebar-label">Alle Projekte</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<div className="sidebar-section-label">Projekte</div>
|
<div className="sidebar-section-label">Projekte</div>
|
||||||
@@ -89,9 +106,10 @@ export function Sidebar() {
|
|||||||
className={
|
className={
|
||||||
project.id === activeProjectId ? "sidebar-link active" : "sidebar-link"
|
project.id === activeProjectId ? "sidebar-link active" : "sidebar-link"
|
||||||
}
|
}
|
||||||
|
title={project.name}
|
||||||
>
|
>
|
||||||
<span className="sidebar-icon">▣</span>
|
<span className="sidebar-icon">▣</span>
|
||||||
{project.name}
|
<span className="sidebar-label">{project.name}</span>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
{!projects.length ? (
|
{!projects.length ? (
|
||||||
@@ -108,9 +126,10 @@ export function Sidebar() {
|
|||||||
key={id}
|
key={id}
|
||||||
href={`#${id}`}
|
href={`#${id}`}
|
||||||
className={activeSection === id ? "sidebar-link active" : "sidebar-link"}
|
className={activeSection === id ? "sidebar-link active" : "sidebar-link"}
|
||||||
|
title={label}
|
||||||
>
|
>
|
||||||
<span className="sidebar-icon">{icon}</span>
|
<span className="sidebar-icon">{icon}</span>
|
||||||
{label}
|
<span className="sidebar-label">{label}</span>
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { AMEV_GZF_TABLE } from "../../shared/constants/amev-gzf";
|
||||||
|
|
||||||
|
interface GzfPickerModalProps {
|
||||||
|
onClose: () => void;
|
||||||
|
onSelect: (value: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatFactor(value: number) {
|
||||||
|
return value.toLocaleString("de-DE", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GzfPickerModal({ onClose, onSelect }: GzfPickerModalProps) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div aria-modal="true" className="modal fade show d-block" role="dialog" tabIndex={-1}>
|
||||||
|
<div className="modal-dialog modal-lg modal-dialog-centered">
|
||||||
|
<div className="modal-content">
|
||||||
|
<div className="modal-header">
|
||||||
|
<div>
|
||||||
|
<h2 className="modal-title fs-5">Gleichzeitigkeitsfaktor nach AMEV</h2>
|
||||||
|
<p className="text-secondary small mb-0">
|
||||||
|
Tabelle 3 – Gleichzeitigkeitsfaktoren für Verbrauchergruppen
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
aria-label="Schließen"
|
||||||
|
className="btn-close"
|
||||||
|
onClick={onClose}
|
||||||
|
type="button"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="modal-body">
|
||||||
|
<div className="table-responsive">
|
||||||
|
<table className="table table-hover align-middle mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Verbrauchergruppe</th>
|
||||||
|
<th>Bereich</th>
|
||||||
|
<th className="text-end">Mittelwert</th>
|
||||||
|
<th className="text-end">Gfges</th>
|
||||||
|
<th className="text-end">Aktion</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{AMEV_GZF_TABLE.map((entry) => (
|
||||||
|
<tr key={entry.group}>
|
||||||
|
<td>{entry.group}</td>
|
||||||
|
<td>{entry.range}</td>
|
||||||
|
<td className="text-end">{formatFactor(entry.mean)}</td>
|
||||||
|
<td className="text-end text-secondary small">
|
||||||
|
{entry.overallRange ?? "–"}
|
||||||
|
</td>
|
||||||
|
<td className="text-end">
|
||||||
|
<button
|
||||||
|
className="btn btn-sm btn-primary"
|
||||||
|
onClick={() => onSelect(entry.mean)}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Übernehmen
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<p className="text-secondary small mt-3 mb-0">
|
||||||
|
Quelle: AMEV „Planungshilfe für elektrische Leistungsbilanzen", Tabelle 3.
|
||||||
|
Übernommen wird jeweils der Mittelwert Gf1 der angegebenen Bandbreite; „Gfges"
|
||||||
|
ist der alternative Gesamtgleichzeitigkeitsfaktor für Verwaltungsgebäude.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="modal-footer">
|
||||||
|
<button className="btn btn-outline-secondary" onClick={onClose} type="button">
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="modal-backdrop fade show" />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
circuitGroupCategoryLabels,
|
circuitGroupCategoryLabels,
|
||||||
type CircuitGroupCategory,
|
type CircuitGroupCategory,
|
||||||
} from "../../shared/constants/circuit-group";
|
} from "../../shared/constants/circuit-group";
|
||||||
|
import { GzfPickerModal } from "./gzf-picker-modal";
|
||||||
|
|
||||||
interface ProjectDeviceModalProps {
|
interface ProjectDeviceModalProps {
|
||||||
globalDevices: GlobalDeviceDto[];
|
globalDevices: GlobalDeviceDto[];
|
||||||
@@ -34,6 +35,7 @@ export function ProjectDeviceModal({
|
|||||||
}: ProjectDeviceModalProps) {
|
}: ProjectDeviceModalProps) {
|
||||||
const [values, setValues] = useState(() => toFormValues(initialDevice, initialValues));
|
const [values, setValues] = useState(() => toFormValues(initialDevice, initialValues));
|
||||||
const [selectedGlobalDeviceId, setSelectedGlobalDeviceId] = useState("");
|
const [selectedGlobalDeviceId, setSelectedGlobalDeviceId] = useState("");
|
||||||
|
const [isGzfPickerOpen, setIsGzfPickerOpen] = useState(false);
|
||||||
|
|
||||||
function update(key: keyof typeof values, value: string) {
|
function update(key: keyof typeof values, value: string) {
|
||||||
setValues((current) => ({ ...current, [key]: value }));
|
setValues((current) => ({ ...current, [key]: value }));
|
||||||
@@ -63,6 +65,7 @@ export function ProjectDeviceModal({
|
|||||||
(!values.cosPhi.trim() || isNumberInRange(values.cosPhi, 0, 1));
|
(!values.cosPhi.trim() || isNumberInRange(values.cosPhi, 0, 1));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<FormModal
|
<FormModal
|
||||||
description={
|
description={
|
||||||
initialDevice
|
initialDevice
|
||||||
@@ -167,15 +170,31 @@ export function ProjectDeviceModal({
|
|||||||
step="0.01"
|
step="0.01"
|
||||||
value={values.powerPerUnit}
|
value={values.powerPerUnit}
|
||||||
/>
|
/>
|
||||||
<NumberField
|
<div className="col-12 col-md-6">
|
||||||
id="device-simultaneity"
|
<label className="form-label" htmlFor="device-simultaneity">
|
||||||
label="Gleichzeitigkeitsfaktor"
|
Gleichzeitigkeitsfaktor
|
||||||
max="1"
|
</label>
|
||||||
min="0"
|
<div className="input-group">
|
||||||
onChange={(value) => update("simultaneityFactor", value)}
|
<input
|
||||||
step="0.01"
|
className="form-control"
|
||||||
value={values.simultaneityFactor}
|
id="device-simultaneity"
|
||||||
/>
|
max="1"
|
||||||
|
min="0"
|
||||||
|
onChange={(event) => update("simultaneityFactor", event.target.value)}
|
||||||
|
step="0.01"
|
||||||
|
type="number"
|
||||||
|
value={values.simultaneityFactor}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className="btn btn-outline-primary"
|
||||||
|
onClick={() => setIsGzfPickerOpen(true)}
|
||||||
|
title="Gleichzeitigkeitsfaktor nach AMEV wählen"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
AMEV
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<NumberField
|
<NumberField
|
||||||
id="device-cos-phi"
|
id="device-cos-phi"
|
||||||
label="cos Phi"
|
label="cos Phi"
|
||||||
@@ -199,6 +218,16 @@ export function ProjectDeviceModal({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</FormModal>
|
</FormModal>
|
||||||
|
{isGzfPickerOpen ? (
|
||||||
|
<GzfPickerModal
|
||||||
|
onClose={() => setIsGzfPickerOpen(false)}
|
||||||
|
onSelect={(value) => {
|
||||||
|
update("simultaneityFactor", String(value));
|
||||||
|
setIsGzfPickerOpen(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
];
|
||||||
Reference in New Issue
Block a user