Merge branch 'main' of https://github.com/IBCornelsen/online-energieausweis
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
1412
src/components/AusweisPruefenBox.svelte
Normal file
1412
src/components/AusweisPruefenBox.svelte
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
|
||||
26
src/lib/pdf/plugins/variables/constants.ts
Normal file
26
src/lib/pdf/plugins/variables/constants.ts
Normal 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})$';
|
||||
311
src/lib/pdf/plugins/variables/index.ts
Normal file
311
src/lib/pdf/plugins/variables/index.ts
Normal 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,
|
||||
};
|
||||
29
src/lib/pdf/plugins/variables/types.ts
Normal file
29
src/lib/pdf/plugins/variables/types.ts
Normal 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;
|
||||
}
|
||||
15
src/modules/Dashboard/DashboardAusweisePruefenModule.svelte
Normal file
15
src/modules/Dashboard/DashboardAusweisePruefenModule.svelte
Normal 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}
|
||||
102
src/modules/Dashboard/DashboardPDFDesignerModule.svelte
Normal file
102
src/modules/Dashboard/DashboardPDFDesignerModule.svelte
Normal 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>
|
||||
103
src/modules/Dashboard/DashboardPDFViewerModule.svelte
Normal file
103
src/modules/Dashboard/DashboardPDFViewerModule.svelte
Normal 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>
|
||||
@@ -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>
|
||||
10
src/pages/dashboard/admin/pdf-designer.astro
Normal file
10
src/pages/dashboard/admin/pdf-designer.astro
Normal 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>
|
||||
18
src/pages/dashboard/admin/pdf-viewer.astro
Normal file
18
src/pages/dashboard/admin/pdf-viewer.astro
Normal 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>
|
||||
@@ -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")
|
||||
}
|
||||
---
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user