Verbrauchsausweis Gewerbe

This commit is contained in:
Moritz Utcke
2025-04-09 13:10:11 -04:00
parent 8daee69576
commit a58c8d466e
8 changed files with 174 additions and 136 deletions

View File

@@ -96,7 +96,7 @@
"postcss-import": "^16.1.0", "postcss-import": "^16.1.0",
"postcss-nesting": "^13.0.1", "postcss-nesting": "^13.0.1",
"prettier": "^2.8.8", "prettier": "^2.8.8",
"prisma": "^6.4.1", "prisma": "6.4.1",
"prisma-dbml-generator": "^0.12.0", "prisma-dbml-generator": "^0.12.0",
"prisma-generator-fake-data": "^0.14.3", "prisma-generator-fake-data": "^0.14.3",
"tsx": "^4.19.3", "tsx": "^4.19.3",

View File

@@ -1,113 +1,89 @@
<script lang="ts"> <script lang="ts">
import { ripple } from "svelte-ripple-action"; import {
import type { RippleOptions } from "svelte-ripple-action/dist/constants.js"; Reader,
import { Reader, Bell, Gear, LockClosed, CaretDown } from "radix-svelte-icons" Bell,
Gear,
LockClosed,
CaretDown,
} from "radix-svelte-icons";
import NotificationProvider from "#components/NotificationProvider/NotificationProvider.svelte"; import NotificationProvider from "#components/NotificationProvider/NotificationProvider.svelte";
import DashboardNotification from "./DashboardNotification.svelte"; import DashboardNotification from "./DashboardNotification.svelte";
import { notifications } from "#components/NotificationProvider/shared.js"; import { notifications } from "#components/NotificationProvider/shared.js";
import ThemeController from "#components/ThemeController.svelte"; import ThemeController from "#components/ThemeController.svelte";
import { BenutzerClient, ObjektKomplettClient } from "#components/Ausweis/types.js"; import {
BenutzerClient,
ObjektKomplettClient,
} from "#components/Ausweis/types.js";
export let lightTheme: boolean; export let lightTheme: boolean;
export let benutzer: BenutzerClient; export let benutzer: BenutzerClient;
const rippleOptions: RippleOptions = {
center: false,
color: lightTheme ? "rgba(233,233,233,0.1)" : "rgba(113, 128, 150, 0.1)",
};
let headerOpen = false;
</script> </script>
<aside class:hidden={!headerOpen} class="fixed left-0 top-16 w-full h-[calc(100%-4rem)] flex md:relative md:h-auto md:w-auto md:top-0 md:flex bg-base-200 border-r border-r-base-300 flex-col py-4"> <aside class="rounded-lg bg-white box px-6 py-5">
<div class="flex flex-row items-center">
<div class="flex flex-row items-center px-4">
<div class="flex flex-row mr-6"> <div class="flex flex-row mr-6">
<a href="/"><img src="/images/header/logo-IBC-big.svg" class="h-16" alt="IBCornelsen - Logo"/></a> <a href="/"
><img
src="/images/header/logo-IBC-big.svg"
class="h-16"
alt="IBCornelsen - Logo"
/></a
>
</div> </div>
<div class="flex-col items-end"> <div class="flex-col items-end">
<div class="text-base-content font-semibold text-left flex" <div class="text-base-content font-semibold text-left flex">
>{benutzer.vorname} {benutzer.name}</div> {benutzer.vorname}
{benutzer.name}
</div>
<div class="text-base-content text-sm flex">{benutzer.email}</div> <div class="text-base-content text-sm flex">{benutzer.email}</div>
<a href="/auth/logout" class="text-xs">Logout</a> <a href="/auth/logout" class="text-xs">Logout</a>
</div> </div>
</div> </div>
<div class="flex flex-col gap-2 mt-0 md:mt-8 px-0"> <div class="flex flex-col gap-2 mt-0 md:mt-8 px-0">
<a use:ripple={rippleOptions} class="button-tab" href="/dashboard"> <a class="button-tab" href="/dashboard">
<Reader width={22} height={22} /> <Reader width={22} height={22} />
Vorgänge Vorgänge
</a> </a>
<a use:ripple={rippleOptions} class="button-tab" href="/dashboard"> <a class="button-tab" href="/dashboard">
<Reader width={22} height={22} /> <Reader width={22} height={22} />
Inbox Inbox
</a> </a>
<hr class="border-gray-600"> <hr class="border-gray-600" />
<!--
<div class="text-base-content text-xl px-4 mt-4">Mitwirkende</div>
<div class="flex flex-col gap-4 px-4">
{#each [
{ name: "Max Mustermann", image: "/images/profile-placeholder.svg", profession: "Architekt" },
{ name: "Erika Musterfrau", image: "/images/profile-placeholder.svg", profession: "Ingenieurin" },
{ name: "Hans Beispiel", image: "/images/profile-placeholder.svg", profession: "Energieberater" },
{ name: "Anna Beispiel", image: "/images/profile-placeholder.svg", profession: "Bauleiterin" }
] as person}
<div class="flex items-center gap-4">
<img src={person.image} alt={person.name} class="w-12 h-12 rounded-full object-cover" />
<div class="flex flex-col">
<span class="text-base-content font-medium">{person.name}</span>
<span class="text-sm text-gray-500">{person.profession}</span>
</div>
<button class="ml-auto btn btn-primary btn-sm">Chat</button>
</div>
{/each}
</div>
-->
<!-- <button use:ripple={rippleOptions} class="button-tab">
<EnvelopeClosed width={22} height={22} />
Kontakt
</button>
<li><details class="[&_.caret]:open:rotate-180">
<summary class="button-tab w-full outline-0 hover:outline-0">
<Cube width={22} height={22} />
Services <CaretDown size={24} class="caret ml-auto transition-transform"></CaretDown></summary>
<ul>
<li>
<button use:ripple={rippleOptions} class="button-tab">
Kontakt
</button>
</li>
<li>
<button use:ripple={rippleOptions} class="button-tab">
Kontakt
</button>
</li>
</ul>
</details></li> -->
{#if benutzer.rolle === "ADMIN"} {#if benutzer.rolle === "ADMIN"}
<li><details class="[&_.caret]:open:rotate-180" open> <li>
<summary class="button-tab w-full outline-0 hover:outline-0 cursor-pointer"> <details class="[&_.caret]:open:rotate-180" open>
<summary
class="button-tab w-full outline-0 hover:outline-0 cursor-pointer"
>
<LockClosed width={22} height={22} /> <LockClosed width={22} height={22} />
Admin <CaretDown size={24} class="caret ml-auto transition-transform"></CaretDown></summary> Admin <CaretDown
size={24}
class="caret ml-auto transition-transform"
></CaretDown></summary
>
<ul> <ul>
<li> <li>
<a use:ripple={rippleOptions} class="button-tab" href="/dashboard/admin/ausweise-pruefen"> <a
class="button-tab"
href="/dashboard/admin/ausweise-pruefen"
>
Ausweise Prüfen Ausweise Prüfen
</a> </a>
</li> </li>
<li> <li>
<a use:ripple={rippleOptions} class="button-tab" href="/dashboard/admin/impersonate-user"> <a
class="button-tab"
href="/dashboard/admin/impersonate-user"
>
Impersonate User Impersonate User
</a> </a>
</li> </li>
</ul> </ul>
</details></li> </details>
</li>
{/if} {/if}
</div> </div>
<div class="mt-10 flex flex-col gap-4 px-8"> <div class="mt-10 flex flex-col gap-4 px-8">
@@ -116,18 +92,26 @@
<div class="dropdown dropdown-top"> <div class="dropdown dropdown-top">
<div class="indicator"> <div class="indicator">
{#if Object.keys($notifications).length > 0} {#if Object.keys($notifications).length > 0}
<span class="indicator-item badge badge-accent text-xs">{Object.keys($notifications).length}</span> <span class="indicator-item badge badge-accent text-xs"
>{Object.keys($notifications).length}</span
>
{/if} {/if}
<button tabindex="0" class="hover:bg-gray-200 p-3 rounded-lg"> <button
tabindex="0"
class="hover:bg-gray-200 p-3 rounded-lg"
>
<Bell size={24} /> <Bell size={24} />
</button> </button>
</div> </div>
<ul class="dropdown-content mb-2 border border-base-300 z-10 menu py-4 px-0 bg-base-200 rounded-box w-80"> <ul
class="dropdown-content mb-2 border border-base-300 z-10 menu py-4 px-0 bg-base-200 rounded-box w-80"
>
<NotificationProvider component={DashboardNotification} /> <NotificationProvider component={DashboardNotification} />
</ul> </ul>
</div> </div>
<a href="/dashboard/einstellungen" <a
href="/dashboard/einstellungen"
class="hover:bg-gray-200 p-3 rounded-lg" class="hover:bg-gray-200 p-3 rounded-lg"
> >
<Gear size={24} /> <Gear size={24} />
@@ -137,10 +121,8 @@
<div class="divider px-8"></div> <div class="divider px-8"></div>
<a <a
href="/dashboard/einstellungen" href="/dashboard/einstellungen"
use:ripple={rippleOptions}
class="hover:bg-gray-200 no-animation focus:shadow-none justify-start py-4 h-auto px-8 rounded-none w-full flex flex-row gap-4" class="hover:bg-gray-200 no-animation focus:shadow-none justify-start py-4 h-auto px-8 rounded-none w-full flex flex-row gap-4"
> >
</a> </a>
</aside> </aside>

View File

@@ -3,22 +3,19 @@
import "../style/global.css"; import "../style/global.css";
import "../../svelte-dialogs.config.js"; import "../../svelte-dialogs.config.js";
import DashboardSidebar from "../components/Dashboard/DashboardSidebar.svelte"; import DashboardSidebar from "../components/Dashboard/DashboardSidebar.svelte";
import { validateAccessTokenServer } from "#server/lib/validateAccessToken"; import { BenutzerClient } from "#components/Ausweis/types";
import { BenutzerClient, ObjektClient } from "#components/Ausweis/types";
const valid = validateAccessTokenServer(Astro)
if (!valid) {
Astro.redirect("/auth/login", 302)
}
export interface Props { export interface Props {
title: string; title: string;
user: BenutzerClient user: BenutzerClient;
} }
const { title, user } = Astro.props; const { title, user } = Astro.props;
if (!user) {
Astro.redirect("/auth/login", 302);
}
const schema = JSON.stringify({ const schema = JSON.stringify({
"@context": "http://schema.org", "@context": "http://schema.org",
"@type": "Corporation", "@type": "Corporation",
@@ -53,22 +50,22 @@ let lightTheme = Astro.cookies.get("theme")?.value === "light";
if (import.meta.env.PROD) { if (import.meta.env.PROD) {
H.init("1jdkoe52", { H.init("1jdkoe52", {
serviceName: "online-energieausweis", serviceName: "online-energieausweis",
backendUrl: "https://highlight-backend.online-energieausweis.org/public", backendUrl:
"https://highlight-backend.online-energieausweis.org/public",
tracingOrigins: true, tracingOrigins: true,
networkRecording: { networkRecording: {
enabled: true, enabled: true,
recordHeadersAndBody: true recordHeadersAndBody: true,
} },
}) });
} }
</script> </script>
<!DOCTYPE html>
<html lang="de"> <html lang="de">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" /> <meta name="viewport" content="width=device-width" />
<script type="application/ld+json" set:html={schema}></script> <script type="application/ld+json" set:html={schema} />
<link rel="icon" type="image/svg+xml" href="/favicon.jpg" /> <link rel="icon" type="image/svg+xml" href="/favicon.jpg" />
<meta <meta
@@ -112,7 +109,6 @@ let lightTheme = Astro.cookies.get("theme")?.value === "light";
</head> </head>
<body> <body>
<main <main
class="p-0 grid max-w-[1920px] class="p-0 grid max-w-[1920px]
xs:grid-cols-[minmax(1fr,1fr)] xs:gap-1 xs:p-0 xs:grid-cols-[minmax(1fr,1fr)] xs:gap-1 xs:p-0
@@ -122,16 +118,15 @@ let lightTheme = Astro.cookies.get("theme")?.value === "light";
xl:grid-cols-[minmax(150px,350px)1fr] xl:gap-4 xl:p-6 xl:grid-cols-[minmax(150px,350px)1fr] xl:gap-4 xl:p-6
2xl:grid-cols-[minmax(150px,300px)1fr] 2xl:gap-5 2xl:p-6" 2xl:grid-cols-[minmax(150px,300px)1fr] 2xl:gap-5 2xl:p-6"
> >
<DashboardSidebar
lightTheme={lightTheme}
benutzer={user}
client:load
/>
<DashboardSidebar lightTheme={lightTheme} benutzer={user} client:load> <article class="box px-6 py-5 h-screen">
</DashboardSidebar>
<article class="box rounded-tl-none
xl:px-10 py-8">
<slot /> <slot />
</article> </article>
</main> </main>
</body> </body>
</html> </html>

View File

@@ -213,15 +213,20 @@ export async function pdfVerbrauchsausweisGewerbe(ausweis: VerbrauchsausweisGewe
const addEnergieverbrauchSkalaPfeile = async (page: PDFPage) => { const addEnergieverbrauchSkalaPfeile = async (page: PDFPage) => {
const pfeilNachUnten = await pdf.embedPng(fs.readFileSync(new URL("../../../public/images/pfeil-nach-unten.png", import.meta.url), "base64")) const pfeilNachUnten = await pdf.embedPng(fs.readFileSync(new URL("../../../public/images/pfeil-nach-unten.png", import.meta.url), "base64"))
const pfeilNachOben = await pdf.embedPng(fs.readFileSync(new URL("../../../public/images/pfeil-nach-oben.png", import.meta.url), "base64"))
// Wir müssen den berechneten Wert zwischen 0 und 1000 als Wert zwischen 0 und 1 festlegen // Wir müssen den berechneten Wert zwischen 0 und 1000 als Wert zwischen 0 und 1 festlegen
const endenergieverbrauchTranslationPercentage = Math.min(1000, Math.max(0, berechnungen?.endEnergieVerbrauchGesamt || 0)) / 1000 const endenergieverbrauchTranslationPercentage = Math.min(1000, Math.max(0, berechnungen?.endEnergieVerbrauchGesamt || 0)) / 1000
const stromVerbrauchTranslationPercentage = Math.min(1000, Math.max(0, berechnungen?.endEnergieVerbrauchStrom || 0)) / 1000 const stromVerbrauchTranslationPercentage = Math.min(1000, Math.max(0, berechnungen?.endEnergieVerbrauchStrom || 0)) / 1000
const vergleichsWertWaermeTranslationPercentage = Math.min(1000, Math.max(0, berechnungen?.vergleichsWertWaerme || 0)) / 1000
const vergleichsWertStromTranslationPercentage = Math.min(1000, Math.max(0, berechnungen?.vergleichsWertStrom || 0)) / 1000
const minTranslation = 78 const minTranslation = 78
const maxTranslation = 512 const maxTranslation = 512
const endenergieverbrauchTranslationX = minTranslation + (maxTranslation - minTranslation) * endenergieverbrauchTranslationPercentage; const endenergieverbrauchTranslationX = minTranslation + (maxTranslation - minTranslation) * endenergieverbrauchTranslationPercentage;
const stromVerbrauchTranslationX = minTranslation + (maxTranslation - minTranslation) * stromVerbrauchTranslationPercentage; const stromVerbrauchTranslationX = minTranslation + (maxTranslation - minTranslation) * stromVerbrauchTranslationPercentage;
const vergleichsWertWaermeTranslationX = minTranslation + (maxTranslation - minTranslation) * vergleichsWertWaermeTranslationPercentage;
const vergleichsWertStromTranslationX = minTranslation + (maxTranslation - minTranslation) * vergleichsWertStromTranslationPercentage;
const pfeilWidth = 20 const pfeilWidth = 20
const margin = 5; const margin = 5;
@@ -233,8 +238,17 @@ export async function pdfVerbrauchsausweisGewerbe(ausweis: VerbrauchsausweisGewe
height: 30 height: 30
}) })
page.drawImage(pfeilNachOben, {
x: vergleichsWertWaermeTranslationX,
y: height - 293,
width: pfeilWidth,
height: 30
})
const endEnergieVerbrauchGesamtText = `${berechnungen?.endEnergieVerbrauchGesamt.toString()}kWh/(m²a)`; const endEnergieVerbrauchGesamtText = `${berechnungen?.endEnergieVerbrauchGesamt.toString()}kWh/(m²a)`;
const stromVerbrauchGesamtText = `${berechnungen?.endEnergieVerbrauchStrom.toString()}kWh/(m²a)`; const stromVerbrauchGesamtText = `${berechnungen?.endEnergieVerbrauchStrom.toString()}kWh/(m²a)`;
const vergleichswertWaermeText = `${berechnungen?.vergleichsWertWaerme.toString()}kWh/(m²a)`
const vergleichswertStromText = `${berechnungen?.vergleichsWertStrom.toString()}kWh/(m²a)`
if (endenergieverbrauchTranslationPercentage > 0.5) { if (endenergieverbrauchTranslationPercentage > 0.5) {
page.drawText("Endenergieverbrauch Wärme", { page.drawText("Endenergieverbrauch Wärme", {
@@ -263,6 +277,33 @@ export async function pdfVerbrauchsausweisGewerbe(ausweis: VerbrauchsausweisGewe
}) })
} }
if (vergleichsWertWaermeTranslationPercentage > 0.5) {
page.drawText("Vergleichswert Wärme", {
x: vergleichsWertWaermeTranslationX - margin - font.widthOfTextAtSize("Vergleichswert", 10),
y: height - 275,
size: 10
})
page.drawText(vergleichswertWaermeText, {
x: vergleichsWertWaermeTranslationX - margin - bold.widthOfTextAtSize(vergleichswertWaermeText, 10),
y: height - 289,
size: 10,
font: bold
})
} else {
page.drawText("Vergleichswert Wärme", {
x: vergleichsWertWaermeTranslationX + pfeilWidth + margin,
y: height - 275,
size: 10
})
page.drawText(vergleichswertWaermeText, {
x: vergleichsWertWaermeTranslationX + pfeilWidth + margin,
y: height - 289,
size: 10,
font: bold
})
}
page.drawImage(pfeilNachUnten, { page.drawImage(pfeilNachUnten, {
x: stromVerbrauchTranslationX, x: stromVerbrauchTranslationX,
y: height - 354, y: height - 354,
@@ -270,6 +311,13 @@ export async function pdfVerbrauchsausweisGewerbe(ausweis: VerbrauchsausweisGewe
height: 30 height: 30
}) })
page.drawImage(pfeilNachOben, {
x: vergleichsWertStromTranslationX,
y: height - 437,
width: pfeilWidth,
height: 30
})
if (endenergieverbrauchTranslationPercentage > 0.5) { if (endenergieverbrauchTranslationPercentage > 0.5) {
page.drawText("Endenergieverbrauch Strom", { page.drawText("Endenergieverbrauch Strom", {
x: stromVerbrauchTranslationX - margin - font.widthOfTextAtSize("Primärenergieverbrauch", 10), x: stromVerbrauchTranslationX - margin - font.widthOfTextAtSize("Primärenergieverbrauch", 10),
@@ -296,6 +344,33 @@ export async function pdfVerbrauchsausweisGewerbe(ausweis: VerbrauchsausweisGewe
font: bold font: bold
}) })
} }
if (vergleichsWertWaermeTranslationPercentage > 0.5) {
page.drawText("Vergleichswert Strom", {
x: vergleichsWertStromTranslationX - margin - font.widthOfTextAtSize("Vergleichswert", 10),
y: height - 420,
size: 10
})
page.drawText(vergleichswertStromText, {
x: vergleichsWertStromTranslationX - margin - bold.widthOfTextAtSize(vergleichswertStromText, 10),
y: height - 434,
size: 10,
font: bold
})
} else {
page.drawText("Vergleichswert Strom", {
x: vergleichsWertStromTranslationX + pfeilWidth + margin,
y: height - 420,
size: 10
})
page.drawText(vergleichswertStromText, {
x: vergleichsWertStromTranslationX + pfeilWidth + margin,
y: height - 434,
size: 10,
font: bold
})
}
} }
addEnergieverbrauchSkalaPfeile(pages[2]) addEnergieverbrauchSkalaPfeile(pages[2])

View File

@@ -2,7 +2,6 @@
import "../../style/formular.css"; import "../../style/formular.css";
import { import {
BenutzerClient, BenutzerClient,
ObjektClient,
ObjektKomplettClient, ObjektKomplettClient,
} from "#components/Ausweis/types.js"; } from "#components/Ausweis/types.js";
import DashboardObjekt from "#components/Dashboard/DashboardObjekt.svelte"; import DashboardObjekt from "#components/Dashboard/DashboardObjekt.svelte";
@@ -14,10 +13,7 @@
import Cookies from "js-cookie"; import Cookies from "js-cookie";
import { API_ACCESS_TOKEN_COOKIE_NAME } from "#lib/constants.js"; import { API_ACCESS_TOKEN_COOKIE_NAME } from "#lib/constants.js";
import Pagination from "#components/Pagination.svelte"; import Pagination from "#components/Pagination.svelte";
import AusweisePruefenFilter from "#components/Dashboard/AusweisePruefenFilter.svelte"; import { Enums, Objekt } from "#lib/client/prisma.js";
import { filterAusweise } from "#lib/filters.js";
import { z, ZodTypeAny } from "zod";
import { Enums } from "#lib/client/prisma.js";
export let user: BenutzerClient; export let user: BenutzerClient;
export let objekte: ObjektKomplettClient[]; export let objekte: ObjektKomplettClient[];
@@ -26,7 +22,7 @@
let objektOverlayHidden = true; let objektOverlayHidden = true;
let objekt: Omit<ObjektClient, "uid"> = { let objekt: Objekt = {
adresse: "", adresse: "",
erstellungsdatum: new Date(), erstellungsdatum: new Date(),
latitude: 0, latitude: 0,
@@ -91,8 +87,6 @@
objekte = objekte objekte = objekte
} }
let filters: { name: keyof z.infer<typeof filterAusweise>, type: ZodTypeAny, value: any }[] = []
export let id: string = ""; export let id: string = "";
</script> </script>

View File

@@ -1,23 +1,15 @@
--- ---
import UserLayout from "#layouts/DashboardLayout.astro"; import UserLayout from "#layouts/DashboardLayout.astro";
import { API_ACCESS_TOKEN_COOKIE_NAME } from "#lib/constants";
import { Enums, prisma } from "#lib/server/prisma"; import { Enums, prisma } from "#lib/server/prisma";
import { createCaller } from "src/astro-typesafe-api-caller";
import DashboardModule from "#modules/Dashboard/DashboardModule.svelte"; import DashboardModule from "#modules/Dashboard/DashboardModule.svelte";
import { getCurrentUser } from "#lib/server/user";
const caller = createCaller(Astro)
const params = Astro.params; const params = Astro.params;
const page = Number(params.page) const page = Number(params.page)
const id = Astro.url.searchParams.get("id") || undefined; const id = Astro.url.searchParams.get("id") || undefined;
const user = await getCurrentUser(Astro)
const user = await caller.user.self.GET.fetch(undefined, {
headers: {
"Authorization": `Bearer ${Astro.cookies.get(API_ACCESS_TOKEN_COOKIE_NAME)?.value}`
}
});
if (!user) { if (!user) {
return Astro.redirect("/auth/login") return Astro.redirect("/auth/login")