This commit is contained in:
Jens Cornelsen
2024-02-23 20:06:11 +01:00
17 changed files with 2150 additions and 63 deletions

View File

@@ -25,6 +25,9 @@
"@ibcornelsen/database": "link:@ibcornelsen/database",
"@ibcornelsen/ui": "^0.0.2",
"@mollie/api-client": "^3.7.0",
"@pdfme/common": "^3.2.3",
"@pdfme/generator": "^3.2.3",
"@pdfme/ui": "^3.2.3",
"@trpc/client": "^10.45.0",
"@trpc/server": "^10.45.0",
"astro": "^2.5.1",
@@ -38,6 +41,7 @@
"esbuild": "^0.18.17",
"express": "^4.18.2",
"flag-icons": "^6.9.2",
"fontkit": "^2.0.2",
"i18next": "^23.4.1",
"i18next-fs-backend": "^2.1.5",
"i18next-http-backend": "^2.2.1",
@@ -65,6 +69,7 @@
"@faker-js/faker": "^8.3.1",
"@tailwindcss/typography": "^0.5.10",
"@types/body-scroll-lock": "^3.1.2",
"@types/fontkit": "^2.0.6",
"@types/js-cookie": "^3.0.6",
"@types/uuid": "^9.0.1",
"@typescript-eslint/eslint-plugin": "^5.59.6",

View File

@@ -1,6 +1,7 @@
import Cookies from "js-cookie";
import { API_ACCESS_TOKEN_COOKIE_NAME, API_REFRESH_TOKEN_COOKIE_NAME } from "../../lib/constants";
import { API_ACCESS_TOKEN_COOKIE_NAME, API_REFRESH_TOKEN_COOKIE_NAME, API_UID_COOKIE_NAME } from "../../lib/constants";
import { client } from "src/trpc";
import moment from "moment";
export async function validateAccessTokenClient() {
@@ -8,7 +9,11 @@ export async function validateAccessTokenClient() {
const refreshToken = Cookies.get(API_REFRESH_TOKEN_COOKIE_NAME);
if (accessToken) {
return true;
const { valid } = await client.v1.benutzer.validateAccessToken.query({accessToken})
if (!valid) {
Cookies.remove(API_ACCESS_TOKEN_COOKIE_NAME);
}
}
// Wir haben keinen Access Token mehr, vielleicht ist dieser ausgelaufen.
@@ -16,6 +21,9 @@ export async function validateAccessTokenClient() {
if (!refreshToken) {
// Wir haben keinen Refresh Token, also müssen wir uns neu anmelden.
Cookies.remove(API_ACCESS_TOKEN_COOKIE_NAME);
Cookies.remove(API_REFRESH_TOKEN_COOKIE_NAME);
Cookies.remove(API_UID_COOKIE_NAME)
return false;
}
@@ -23,21 +31,25 @@ export async function validateAccessTokenClient() {
// Wenn das klappt, dann haben wir auch einen neuen Access Token.
// Wenn das nicht klappt, dann müssen wir uns neu anmelden.
try {
const { accessToken: newAccessToken, exp } = await client.v1.benutzer.getAccessToken.query({
const { accessToken: newAccessToken, accessTokenExpiry, refreshToken: newRefreshToken, refreshTokenExpiry } = await client.v1.benutzer.getAccessToken.query({
refreshToken
})
const options = {
Cookies.set(API_ACCESS_TOKEN_COOKIE_NAME, newAccessToken, {
domain: `.${window.location.hostname}`,
path: "/",
expires: exp
}
Cookies.set(API_ACCESS_TOKEN_COOKIE_NAME, newAccessToken, options);
expires: moment.unix(accessTokenExpiry).toDate()
});
Cookies.set(API_REFRESH_TOKEN_COOKIE_NAME, newRefreshToken, {
domain: `.${window.location.hostname}`,
path: "/",
expires: moment.unix(refreshTokenExpiry).toDate()
})
return true;
} catch (e) {
Cookies.remove(API_ACCESS_TOKEN_COOKIE_NAME);
Cookies.remove(API_REFRESH_TOKEN_COOKIE_NAME);
Cookies.remove(API_UID_COOKIE_NAME)
return false;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { ripple } from "svelte-ripple-action";
import type { RippleOptions } from "svelte-ripple-action/dist/constants";
import { Home, Reader, EnvelopeClosed, Cube, Sun, Moon, Bell, Gear } from "radix-svelte-icons"
import { Home, Reader, EnvelopeClosed, Cube, Sun, Moon, Bell, Gear, LockClosed } from "radix-svelte-icons"
import NotificationProvider from "#components/NotificationProvider/NotificationProvider.svelte";
import DashboardNotification from "./DashboardNotification.svelte";
import { onMount } from "svelte";
@@ -13,7 +13,7 @@
const rippleOptions: RippleOptions = {
center: false,
color: "rgba(113, 128, 150, 0.1)",
color: lightTheme ? "rgba(233,233,233,0.1)" : "rgba(113, 128, 150, 0.1)",
};
onMount(() => {
@@ -66,10 +66,32 @@
</li>
</ul>
</details></li>
<li><details>
<summary class="button-tab w-full outline-0 hover:outline-0">
<LockClosed width={22} height={22} />
Admin</summary>
<ul>
<li>
<a use:ripple={rippleOptions} class="button-tab" href="/dashboard/admin/ausweise-pruefen">
Ausweise Prüfen
</a>
</li>
<li>
<a use:ripple={rippleOptions} class="button-tab" href="/dashboard/admin/pdf-designer">
PDF Designer
</a>
</li>
<li>
<a use:ripple={rippleOptions} class="button-tab" href="/dashboard/admin/pdf-viewer">
PDF Viewer
</a>
</li>
</ul>
</details></li>
</div>
<div class="mt-auto flex flex-col gap-4 px-8">
<div class="flex flex-row justify-between items-center">
<ThemeController {lightTheme}></ThemeController>
<ThemeController bind:lightTheme></ThemeController>
<div class="dropdown dropdown-top">
<div class="indicator">
{#if Object.keys($notifications).length > 0}

View File

@@ -1,10 +1,6 @@
import { VerbrauchsausweisWohnenClient, GebaeudeClient } from "#components/Ausweis/types";
import { getKlimafaktoren } from "#lib/Klimafaktoren";
import { getHeizwertfaktor } from "#lib/server/Heizwertfaktor";
import type {
GebaeudeStammdaten,
VerbrauchsausweisWohnen,
} from "@ibcornelsen/database/client";
export function energetischeNutzflaecheVerbrauchsausweisWohnen_2016(
ausweis: VerbrauchsausweisWohnenClient & {
@@ -36,16 +32,32 @@ export async function endEnergieVerbrauchVerbrauchsausweis_2016(
return null
}
let klimafaktoren: Awaited<ReturnType<typeof getKlimafaktoren>>;
let klimafaktoren: Awaited<ReturnType<typeof getKlimafaktoren>> = [{
month: ausweis.startdatum.getMonth(),
year: ausweis.startdatum.getFullYear(),
klimafaktor: 1
},
{
month: ausweis.startdatum.getMonth(),
year: ausweis.startdatum.getFullYear() + 1,
klimafaktor: 1
},
{
month: ausweis.startdatum.getMonth(),
year: ausweis.startdatum.getFullYear() + 2,
klimafaktor: 1
}];
try {
klimafaktoren = await getKlimafaktoren(ausweis.startdatum, ausweis.gebaeude_stammdaten.plz)
const response = await getKlimafaktoren(ausweis.startdatum, ausweis.gebaeude_stammdaten.plz)
if (response) {
klimafaktoren = response
}
} catch (e) {
return null
}
if (!klimafaktoren) {
return null
// TODO: Entweder setzen wir hier mit falschen Klimafaktoren fort, oder wir geben null zurück.
// Im Moment setzen wir einfach fort, aber das ist nicht unbedingt die beste Lösung.
// return null
}

View File

@@ -0,0 +1,26 @@
import { ALIGNMENT, VERTICAL_ALIGNMENT, DYNAMIC_FONT_SIZE_FIT } from './types';
export const DEFAULT_FONT_SIZE = 13;
export const ALIGN_LEFT = 'left' as ALIGNMENT;
export const ALIGN_CENTER = 'center' as ALIGNMENT;
export const ALIGN_RIGHT = 'right' as ALIGNMENT;
export const DEFAULT_ALIGNMENT = ALIGN_LEFT;
export const VERTICAL_ALIGN_TOP = 'top' as VERTICAL_ALIGNMENT;
export const VERTICAL_ALIGN_MIDDLE = 'middle' as VERTICAL_ALIGNMENT;
export const VERTICAL_ALIGN_BOTTOM = 'bottom' as VERTICAL_ALIGNMENT;
export const DEFAULT_VERTICAL_ALIGNMENT = VERTICAL_ALIGN_TOP;
export const DEFAULT_LINE_HEIGHT = 1;
export const DEFAULT_CHARACTER_SPACING = 0;
export const DEFAULT_FONT_COLOR = '#000000';
export const PLACEHOLDER_FONT_COLOR = '#A0A0A0';
export const DYNAMIC_FIT_VERTICAL = 'vertical' as DYNAMIC_FONT_SIZE_FIT;
export const DYNAMIC_FIT_HORIZONTAL = 'horizontal' as DYNAMIC_FONT_SIZE_FIT;
export const DEFAULT_DYNAMIC_FIT = DYNAMIC_FIT_VERTICAL;
export const DEFAULT_DYNAMIC_MIN_FONT_SIZE = 4;
export const DEFAULT_DYNAMIC_MAX_FONT_SIZE = 72;
export const FONT_SIZE_ADJUSTMENT = 0.25;
export const DEFAULT_OPACITY = 1;
export const HEX_COLOR_PATTERN = '^#(?:[A-Fa-f0-9]{3,4}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$';

View File

@@ -0,0 +1,311 @@
import {
ZOOM,
Plugin,
Schema,
PropPanel,
DEFAULT_FONT_NAME,
getFallbackFontName,
PropPanelSchema,
PropPanelWidgetProps,
} from "@pdfme/common";
import { image, text } from "@pdfme/schemas";
import type { TextSchema } from "@pdfme/schemas/dist/types/src/text/types";
import {
DEFAULT_FONT_SIZE,
DEFAULT_ALIGNMENT,
DEFAULT_VERTICAL_ALIGNMENT,
DEFAULT_CHARACTER_SPACING,
DEFAULT_LINE_HEIGHT,
VERTICAL_ALIGN_TOP,
VERTICAL_ALIGN_MIDDLE,
VERTICAL_ALIGN_BOTTOM,
DEFAULT_FONT_COLOR,
DYNAMIC_FIT_VERTICAL,
DYNAMIC_FIT_HORIZONTAL,
DEFAULT_DYNAMIC_FIT,
DEFAULT_DYNAMIC_MIN_FONT_SIZE,
DEFAULT_DYNAMIC_MAX_FONT_SIZE,
ALIGN_RIGHT,
ALIGN_CENTER,
DEFAULT_OPACITY,
HEX_COLOR_PATTERN,
} from "./constants";
import {
GebaeudeStammdaten,
Rechnungen,
VerbrauchsausweisWohnen,
} from "@ibcornelsen/database/client";
const UseDynamicFontSize = (props: PropPanelWidgetProps) => {
const { rootElement, changeSchemas, activeSchema, i18n } = props;
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.checked = Boolean((activeSchema as any)?.dynamicFontSize);
checkbox.onchange = (e: any) => {
const val = e.target.checked
? {
min: DEFAULT_DYNAMIC_MIN_FONT_SIZE,
max: DEFAULT_DYNAMIC_MAX_FONT_SIZE,
fit: DEFAULT_DYNAMIC_FIT,
}
: undefined;
changeSchemas([
{ key: "dynamicFontSize", value: val, schemaId: activeSchema.id },
]);
};
const label = document.createElement("label");
label.innerText = i18n("schemas.text.dynamicFontSize") || "";
label.style.cssText = "display: flex; width: 100%;";
label.appendChild(checkbox);
rootElement.appendChild(label);
};
type AusweisIndex = keyof VerbrauchsausweisWohnen
| `gebaeude_stammdaten.${keyof GebaeudeStammdaten}`
| `rechnung.${keyof Rechnungen}`;
const sampleData: Partial<Record<AusweisIndex , string>> = {
"gebaeude_stammdaten.adresse": "Musterstraße 123",
"gebaeude_stammdaten.plz": "12345",
"gebaeude_stammdaten.ort": "Musterstadt",
"gebaeude_stammdaten.baujahr_gebaeude": "1990",
"gebaeude_stammdaten.baujahr_heizung": "2000",
}
const variableOptions: {
label: string;
value: AusweisIndex;
}[] = [
{
label: "Gebäude -> Adresse",
value: "gebaeude_stammdaten.adresse",
},
{
label: "Gebäude -> PLZ",
value: "gebaeude_stammdaten.plz",
},
{
label: "Gebaeude -> Ort",
value: "gebaeude_stammdaten.ort",
},
{
label: "Gebäude -> Baujahr Gebäude",
value: "gebaeude_stammdaten.baujahr_gebaeude",
},
{
label: "Gebäude -> Baujahr Heizung",
value: "gebaeude_stammdaten.baujahr_heizung",
},
{
label: "Gebäude -> Fläche",
value: "gebaeude_stammdaten.flaeche"
},
{
label: "Gebäude -> Einheiten",
value: "gebaeude_stammdaten.einheiten"
}
];
interface VariableSchema extends TextSchema {
variable: AusweisIndex | undefined
}
const propPanel: PropPanel<VariableSchema> = {
schema: ({ options, activeSchema, i18n }) => {
const font = options.font || {
[DEFAULT_FONT_NAME]: { data: "", fallback: true },
};
const fontNames = Object.keys(font);
const fallbackFontName = getFallbackFontName(font);
const enableDynamicFont = Boolean(
(activeSchema as any)?.dynamicFontSize
);
const textSchema: Record<string, PropPanelSchema> = {
variable: {
title: "Variable",
type: "string",
widget: "select",
props: { options: variableOptions },
span: 24,
},
fontName: {
title: i18n("schemas.text.fontName"),
type: "string",
widget: "select",
default: fallbackFontName,
props: {
options: fontNames.map((name) => ({
label: name,
value: name,
})),
},
span: 12,
},
fontSize: {
title: i18n("schemas.text.size"),
type: "number",
widget: "inputNumber",
span: 6,
disabled: enableDynamicFont,
},
characterSpacing: {
title: i18n("schemas.text.spacing"),
type: "number",
widget: "inputNumber",
span: 6,
},
alignment: {
title: i18n("schemas.text.textAlign"),
type: "string",
widget: "select",
props: {
options: [
{
label: i18n("schemas.left"),
value: DEFAULT_ALIGNMENT,
},
{ label: i18n("schemas.center"), value: ALIGN_CENTER },
{ label: i18n("schemas.right"), value: ALIGN_RIGHT },
],
},
span: 8,
},
verticalAlignment: {
title: i18n("schemas.text.verticalAlign"),
type: "string",
widget: "select",
props: {
options: [
{
label: i18n("schemas.top"),
value: VERTICAL_ALIGN_TOP,
},
{
label: i18n("schemas.middle"),
value: VERTICAL_ALIGN_MIDDLE,
},
{
label: i18n("schemas.bottom"),
value: VERTICAL_ALIGN_BOTTOM,
},
],
},
span: 8,
},
lineHeight: {
title: i18n("schemas.text.lineHeight"),
type: "number",
widget: "inputNumber",
props: {
step: 0.1,
},
span: 8,
},
useDynamicFontSize: {
type: "boolean",
widget: "UseDynamicFontSize",
bind: false,
span: 16,
},
dynamicFontSize: {
type: "object",
widget: "card",
column: 3,
properties: {
min: {
title: i18n("schemas.text.min"),
type: "number",
widget: "inputNumber",
hidden: !enableDynamicFont,
},
max: {
title: i18n("schemas.text.max"),
type: "number",
widget: "inputNumber",
hidden: !enableDynamicFont,
},
fit: {
title: i18n("schemas.text.fit"),
type: "string",
widget: "select",
hidden: !enableDynamicFont,
props: {
options: [
{
label: i18n("schemas.horizontal"),
value: DYNAMIC_FIT_HORIZONTAL,
},
{
label: i18n("schemas.vertical"),
value: DYNAMIC_FIT_VERTICAL,
},
],
},
},
},
},
fontColor: {
title: i18n("schemas.textColor"),
type: "string",
widget: "color",
rules: [
{
pattern: HEX_COLOR_PATTERN,
message: i18n("hexColorPrompt"),
},
],
},
backgroundColor: {
title: i18n("schemas.bgColor"),
type: "string",
widget: "color",
rules: [
{
pattern: HEX_COLOR_PATTERN,
message: i18n("hexColorPrompt"),
},
],
},
};
return textSchema;
},
widgets: { UseDynamicFontSize },
defaultValue: "Type Something...",
defaultSchema: {
type: "variable",
position: { x: 0, y: 0 },
width: 45,
height: 10,
rotate: 0,
alignment: DEFAULT_ALIGNMENT,
verticalAlignment: DEFAULT_VERTICAL_ALIGNMENT,
fontSize: DEFAULT_FONT_SIZE,
lineHeight: DEFAULT_LINE_HEIGHT,
characterSpacing: DEFAULT_CHARACTER_SPACING,
dynamicFontSize: undefined,
fontColor: DEFAULT_FONT_COLOR,
fontName: undefined,
backgroundColor: "",
opacity: DEFAULT_OPACITY,
variable: undefined
},
};
export const variable: Plugin<VariableSchema> = {
ui: (props) => {
if (props.schema.variable) {
props.value = sampleData[props.schema.variable] as string;
}
return text.ui(props);
},
pdf: (props) => {
props.value = props.schema.variable as string;
text.pdf(props);
},
propPanel,
};

View File

@@ -0,0 +1,29 @@
import type { Schema } from '@pdfme/common';
import type { Font as FontKitFont } from 'fontkit';
export type ALIGNMENT = 'left' | 'center' | 'right';
export type VERTICAL_ALIGNMENT = 'top' | 'middle' | 'bottom';
export type DYNAMIC_FONT_SIZE_FIT = 'horizontal' | 'vertical';
export type FontWidthCalcValues = {
font: FontKitFont;
fontSize: number;
characterSpacing: number;
boxWidthInPt: number;
};
export interface TextSchema extends Schema {
fontName?: string;
alignment: ALIGNMENT;
verticalAlignment: VERTICAL_ALIGNMENT;
fontSize: number;
lineHeight: number;
characterSpacing: number;
dynamicFontSize?: {
min: number;
max: number;
fit: DYNAMIC_FONT_SIZE_FIT;
};
fontColor: string;
backgroundColor: string;
}

View File

@@ -0,0 +1,15 @@
<script lang="ts">
import { GebaeudeClient, VerbrauchsausweisWohnenClient } from "#components/Ausweis/types";
import AusweisPruefenBox from "#components/AusweisPruefenBox.svelte";
import { endEnergieVerbrauchVerbrauchsausweis_2016 } from "#lib/Berechnungen/VerbrauchsausweisWohnen/VerbrauchsausweisWohnen_2016";
export let ausweise: VerbrauchsausweisWohnenClient & { gebaeude_stammdaten: GebaeudeClient }[];
</script>
{#each ausweise as ausweis}
{#await endEnergieVerbrauchVerbrauchsausweis_2016(ausweis)}
<p>Dies ist ein Platzhalter.</p>
{:then calculations}
<AusweisPruefenBox {ausweis} {calculations}></AusweisPruefenBox>
{/await}
{/each}

View File

@@ -0,0 +1,102 @@
<script lang="ts">
import { Designer } from "@pdfme/ui";
import { image, text } from "@pdfme/schemas";
import { variable } from "../../lib/pdf/plugins/variables/index"
import { onMount } from "svelte";
import { Template, BLANK_PDF } from "@pdfme/common";
let template: Template = {
basePdf: BLANK_PDF,
schemas: [
{
},
],
};
let plugins = { Bild: image, Text: text, Variablen: variable }
let container: HTMLDivElement;
let designer: Designer;
onMount(() => {
designer = new Designer({ domContainer: container, template, plugins });
});
function loadBasePDF() {
if (!loadTemplateInput.files) return;
const file = loadTemplateInput.files[0];
const reader = new FileReader();
reader.onload = function (e) {
if (!e.target) return;
const basePdf = e.target.result as string;
const newTemplate = { ...template, basePdf };
designer = new Designer({ domContainer: container, template: newTemplate, plugins });
};
reader.readAsDataURL(file);
}
function addNewField() {
template = designer.getTemplate();
template.schemas[0]["new-field"] = {
type: "text",
position: { x: 0, y: 0 },
width: 10,
height: 10
};
designer.updateTemplate(template);
}
function exportTemplate() {
const template = designer.getTemplate();
const blob = new Blob([JSON.stringify(template)], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "template.json";
a.click();
}
function loadTemplate() {
const input = document.createElement("input");
input.type = "file";
input.accept = ".json";
input.onchange = function (e) {
if (!input.files) return;
const file = input.files[0];
const reader = new FileReader();
reader.onload = function (e) {
if (!e.target) return;
const template = JSON.parse(e.target.result as string) as Template;
designer = new Designer({ domContainer: container, template, plugins });
};
reader.readAsText(file);
};
input.click()
}
let loadTemplateInput: HTMLInputElement;
</script>
<header class="mb-4">
<button class="btn btn-secondary" on:click={() => loadTemplateInput.click()}>Change base PDF</button>
<button class="btn btn-secondary" on:click={addNewField}>Add new Field</button>
<button class="btn btn-secondary" on:click={exportTemplate}>Export</button>
<button class="btn btn-secondary" on:click={loadTemplate}>Load Template</button>
<a class="btn btn-secondary" href="/dashboard/admin/pdf-viewer">Test in Viewer</a>
<input type="file" hidden bind:this={loadTemplateInput} on:change={loadBasePDF}>
</header>
<div bind:this={container}></div>

View File

@@ -0,0 +1,103 @@
<script lang="ts">
import { variable } from "#lib/pdf/plugins/variables";
import { Benutzer, GebaeudeStammdaten, Rechnungen, VerbrauchsausweisWohnen } from "@ibcornelsen/database/client";
import { Template } from "@pdfme/common";
import { Viewer } from "@pdfme/ui";
import { Check } from "radix-svelte-icons";
import { image, text } from "@pdfme/schemas";
type AusweisData = VerbrauchsausweisWohnen & { benutzer: Benutzer, gebaeude_stammdaten: GebaeudeStammdaten, rechnungen: Rechnungen }
export let ausweise: AusweisData[];
let pdfInputs: AusweisData;
let template: Template;
let viewer: Viewer
function convertAusweisData(inputs: AusweisData): Record<string, string> {
return {
"gebaeude_stammdaten.adresse": inputs.gebaeude_stammdaten.adresse || "",
"gebaeude_stammdaten.gebaeudetyp": inputs.gebaeude_stammdaten.gebaeudetyp || "",
"gebaeude_stammdaten.baujahr_gebaeude": inputs.gebaeude_stammdaten.baujahr_gebaeude.join(", ") || "",
"gebaeude_stammdaten.baujahr_heizung": inputs.gebaeude_stammdaten.baujahr_heizung.join(", ") || "",
"gebaeude_stammdaten.plz": inputs.gebaeude_stammdaten.plz || "",
"gebaeude_stammdaten.ort": inputs.gebaeude_stammdaten.ort || "",
}
}
function loadTemplate() {
const input = document.createElement("input");
input.type = "file";
input.accept = ".json";
input.onchange = function (e) {
if (!input.files) return;
const file = input.files[0];
const reader = new FileReader();
reader.onload = async function (e) {
if (!e.target) return;
template = JSON.parse(e.target.result as string) as Template;
if (!pdfInputs) {
alert("Bitte wählen Sie einen Ausweis aus, um diesen als Basis zu verwenden.")
return
};
viewer = new Viewer({
domContainer: pdfViewerContainer,
template,
inputs: [convertAusweisData(pdfInputs)],
plugins: { text, image, variable}
})
};
reader.readAsText(file);
};
input.click();
}
function changeInputs(inputs: AusweisData) {
pdfInputs = inputs;
if (!template) {
alert("Bitte laden Sie zuerst ein Template.")
return
}
if (viewer) viewer.destroy();
viewer = new Viewer({
domContainer: pdfViewerContainer,
template,
inputs: [convertAusweisData(pdfInputs)],
plugins: { text, image, variable}
})
}
let pdfViewerContainer: HTMLDivElement;
</script>
<header>
<div>
<button class="btn btn-secondary mb-4" on:click={loadTemplate}>Load Template</button>
</div>
</header>
<main class="grid grid-cols-[1fr_4fr] gap-4">
<div>
<div class="flex flex-col gap-4">
{#each ausweise as ausweis}
<div class="rounded-lg border p-2 flex flex-row items-center justify-between">
<h2 class="text-black">{ausweis.gebaeude_stammdaten.adresse}</h2>
<button class="btn btn-square btn-ghost p-1.5" on:click={() => {
changeInputs(ausweis)
}}><Check size={20}/></button>
</div>
{/each}
</div>
</div>
<div bind:this={pdfViewerContainer}></div>
</main>

View File

@@ -1,25 +1,26 @@
---
import UserLayout from "../../../layouts/UserLayout.astro";
import { API_UID_COOKIE_NAME } from "../../../lib/constants";
import { validateAccessTokenServer } from "src/server/lib/validateAccessToken";
import DashboardAusweisePruefenModule from "#modules/Dashboard/DashboardAusweisePruefenModule.svelte";
import { prisma } from "@ibcornelsen/database/server";
const uid = Astro.cookies.get(API_UID_COOKIE_NAME).value
const accessTokenValid = await validateAccessTokenServer(Astro);
if (!uid) {
if (!accessTokenValid) {
return Astro.redirect("/auth/login")
}
const user = await prisma.benutzer.findUnique({
where: {
uid
// TODO: Nutzer darf nur auf diese Seite, wenn er die Rolle "admin" hat
const ausweise = await prisma.verbrauchsausweisWohnen.findMany({
take: 10,
include: {
gebaeude_stammdaten: true,
benutzer: true
}
})
if (!user) {
return Astro.redirect("/auth/login")
}
---
<UserLayout title="Ausweise Prüfen">
<UserLayout title="Dashboard">
<DashboardAusweisePruefenModule ausweise={ausweise} client:load></DashboardAusweisePruefenModule>
</UserLayout>

View File

@@ -0,0 +1,10 @@
---
import UserLayout from "../../../layouts/UserLayout.astro";
import DashboardPDFDesignerModule from "../../../modules/Dashboard/DashboardPDFDesignerModule.svelte";
---
<UserLayout title="PDF Designer">
<DashboardPDFDesignerModule client:only></DashboardPDFDesignerModule>
</UserLayout>

View File

@@ -0,0 +1,18 @@
---
import UserLayout from "../../../layouts/UserLayout.astro";
import DashboardPDFViewerModule from "../../../modules/Dashboard/DashboardPDFViewerModule.svelte";
import { prisma } from "@ibcornelsen/database/server";
const ausweise = await prisma.verbrauchsausweisWohnen.findMany({
take: 10,
include: {
benutzer: true,
gebaeude_stammdaten: true,
rechnungen: true
}
})
---
<UserLayout title="PDF Viewer">
<DashboardPDFViewerModule ausweise={ausweise} client:only></DashboardPDFViewerModule>
</UserLayout>

View File

@@ -1,22 +1,10 @@
---
import UserLayout from "../../layouts/UserLayout.astro";
import { API_UID_COOKIE_NAME } from "../../lib/constants";
import UserModule from "#modules/UserModule.svelte";
import { prisma } from "@ibcornelsen/database/server";
import { validateAccessTokenServer } from "src/server/lib/validateAccessToken";
const uid = Astro.cookies.get(API_UID_COOKIE_NAME).value
const accessTokenValid = await validateAccessTokenServer(Astro);
if (!uid) {
return Astro.redirect("/auth/login")
}
const user = await prisma.benutzer.findUnique({
where: {
uid
}
})
if (!user) {
if (!accessTokenValid) {
return Astro.redirect("/auth/login")
}
---

View File

@@ -1,42 +1,57 @@
import { createCaller } from "#lib/caller";
import { API_ACCESS_TOKEN_COOKIE_NAME, API_REFRESH_TOKEN_COOKIE_NAME } from "#lib/constants";
import { API_ACCESS_TOKEN_COOKIE_NAME, API_REFRESH_TOKEN_COOKIE_NAME, API_UID_COOKIE_NAME } from "#lib/constants";
import type { AstroGlobal } from "astro";
import moment from "moment";
export async function validateAccessTokenServer(astro: Readonly<AstroGlobal<Record<string, any>>>) {
export async function validateAccessTokenServer(astro: AstroGlobal) {
const accessToken = astro.cookies.get(API_ACCESS_TOKEN_COOKIE_NAME).value;
const refreshToken = astro.cookies.get(API_REFRESH_TOKEN_COOKIE_NAME).value;
if (accessToken) {
return true;
}
// Wir haben keinen Access Token mehr, vielleicht ist dieser ausgelaufen.
// Schauen wir mal, ob wir einen Refresh Token haben.
if (!refreshToken) {
// Wir haben keinen Refresh Token, also müssen wir uns neu anmelden.
astro.cookies.delete(API_ACCESS_TOKEN_COOKIE_NAME);
astro.cookies.delete(API_REFRESH_TOKEN_COOKIE_NAME);
astro.cookies.delete(API_UID_COOKIE_NAME)
return false;
}
if (accessToken) {
const { valid } = await createCaller(astro).v1.benutzer.validateAccessToken({accessToken})
if (!valid) {
astro.cookies.delete(API_ACCESS_TOKEN_COOKIE_NAME);
} else {
return true
}
}
// Wir haben keinen Access Token mehr, vielleicht ist dieser ausgelaufen.
// Wir haben einen Refresh Token, also versuchen wir uns damit anzumelden.
// Wenn das klappt, dann haben wir auch einen neuen Access Token.
// Wenn das nicht klappt, dann müssen wir uns neu anmelden.
// TODO: Schlägt fehl! Ich habe keine Ahnung warum. Ich habe das Gefühl, dass das an der Astro-Implementierung liegt. Der Refresh Token updated sich nicht richtig....
try {
const { accessToken: newAccessToken, exp } = await createCaller(astro).v1.benutzer.getAccessToken({
const { accessToken: newAccessToken, accessTokenExpiry, refreshToken: newRefreshToken, refreshTokenExpiry } = await createCaller(astro).v1.benutzer.getAccessToken({
refreshToken
})
astro.cookies.set(API_ACCESS_TOKEN_COOKIE_NAME, newAccessToken, {
domain: `.${astro.url.host}`,
path: "/",
expires: moment.unix(exp).toDate()
expires: moment.unix(accessTokenExpiry).toDate()
});
astro.cookies.set(API_REFRESH_TOKEN_COOKIE_NAME, newRefreshToken, {
domain: `.${astro.url.host}`,
path: "/",
expires: moment.unix(refreshTokenExpiry).toDate()
})
return true;
} catch (e) {
astro.cookies.delete(API_ACCESS_TOKEN_COOKIE_NAME);
astro.cookies.delete(API_REFRESH_TOKEN_COOKIE_NAME);
astro.cookies.delete(API_UID_COOKIE_NAME)
return false;
}
}

View File

@@ -3,6 +3,14 @@ module.exports = {
content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}", "./node_modules/@ibcornelsen/ui/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"],
darkMode: "class",
plugins: [require("daisyui"), require("@tailwindcss/typography")],
theme: {
extend: {
colors: {
"pdf-yellow-bright": "#f3cb00",
"pdf-yellow-light": "#fff6ca",
},
},
},
daisyui: {
themes: [{
light: {
@@ -43,9 +51,7 @@ module.exports = {
'--btn-text-case': 'normal',
'--navbar-padding': '.5rem',
'--border-btn': '1px',
"pdf-yellow-bright": "#f3cb00",
"pdf-yellow-light": "#fff6ca",
'--border-btn': '1px'
},
'dark': {
'primary': '#ff7d26',