Bedarfsausweis Wohnen ausstellen

This commit is contained in:
Moritz Utcke
2025-04-23 10:34:30 -03:00
parent cfad65878e
commit 076c49c054
8 changed files with 616 additions and 128 deletions

View File

@@ -5,30 +5,31 @@ export const createCaller = createCallerFactory({
"klimafaktoren": await import("../src/pages/api/klimafaktoren.ts"), "klimafaktoren": await import("../src/pages/api/klimafaktoren.ts"),
"postleitzahlen": await import("../src/pages/api/postleitzahlen.ts"), "postleitzahlen": await import("../src/pages/api/postleitzahlen.ts"),
"unterlage": await import("../src/pages/api/unterlage.ts"), "unterlage": await import("../src/pages/api/unterlage.ts"),
"aufnahme": await import("../src/pages/api/aufnahme/index.ts"),
"ausweise": await import("../src/pages/api/ausweise/index.ts"),
"bedarfsausweis-gewerbe/[id]": await import("../src/pages/api/bedarfsausweis-gewerbe/[id].ts"),
"bedarfsausweis-gewerbe": await import("../src/pages/api/bedarfsausweis-gewerbe/index.ts"),
"admin/ausstellen": await import("../src/pages/api/admin/ausstellen.ts"), "admin/ausstellen": await import("../src/pages/api/admin/ausstellen.ts"),
"admin/bedarfsausweis-ausstellen": await import("../src/pages/api/admin/bedarfsausweis-ausstellen.ts"),
"admin/bestellbestaetigung": await import("../src/pages/api/admin/bestellbestaetigung.ts"), "admin/bestellbestaetigung": await import("../src/pages/api/admin/bestellbestaetigung.ts"),
"admin/erinnern": await import("../src/pages/api/admin/erinnern.ts"), "admin/erinnern": await import("../src/pages/api/admin/erinnern.ts"),
"admin/nicht-ausstellen": await import("../src/pages/api/admin/nicht-ausstellen.ts"), "admin/nicht-ausstellen": await import("../src/pages/api/admin/nicht-ausstellen.ts"),
"admin/registriernummer": await import("../src/pages/api/admin/registriernummer.ts"), "admin/registriernummer": await import("../src/pages/api/admin/registriernummer.ts"),
"admin/stornieren": await import("../src/pages/api/admin/stornieren.ts"), "admin/stornieren": await import("../src/pages/api/admin/stornieren.ts"),
"aufnahme": await import("../src/pages/api/aufnahme/index.ts"), "bedarfsausweis-wohnen/[id]": await import("../src/pages/api/bedarfsausweis-wohnen/[id].ts"),
"ausweise": await import("../src/pages/api/ausweise/index.ts"), "bedarfsausweis-wohnen": await import("../src/pages/api/bedarfsausweis-wohnen/index.ts"),
"auth/access-token": await import("../src/pages/api/auth/access-token.ts"), "auth/access-token": await import("../src/pages/api/auth/access-token.ts"),
"auth/passwort-vergessen": await import("../src/pages/api/auth/passwort-vergessen.ts"), "auth/passwort-vergessen": await import("../src/pages/api/auth/passwort-vergessen.ts"),
"auth/refresh-token": await import("../src/pages/api/auth/refresh-token.ts"), "auth/refresh-token": await import("../src/pages/api/auth/refresh-token.ts"),
"bedarfsausweis-gewerbe/[id]": await import("../src/pages/api/bedarfsausweis-gewerbe/[id].ts"),
"bedarfsausweis-gewerbe": await import("../src/pages/api/bedarfsausweis-gewerbe/index.ts"),
"bedarfsausweis-wohnen/[id]": await import("../src/pages/api/bedarfsausweis-wohnen/[id].ts"),
"bedarfsausweis-wohnen": await import("../src/pages/api/bedarfsausweis-wohnen/index.ts"),
"bilder/[id]": await import("../src/pages/api/bilder/[id].ts"), "bilder/[id]": await import("../src/pages/api/bilder/[id].ts"),
"geg-nachweis-gewerbe/[id]": await import("../src/pages/api/geg-nachweis-gewerbe/[id].ts"), "geg-nachweis-gewerbe/[id]": await import("../src/pages/api/geg-nachweis-gewerbe/[id].ts"),
"geg-nachweis-gewerbe": await import("../src/pages/api/geg-nachweis-gewerbe/index.ts"), "geg-nachweis-gewerbe": await import("../src/pages/api/geg-nachweis-gewerbe/index.ts"),
"geg-nachweis-wohnen/[id]": await import("../src/pages/api/geg-nachweis-wohnen/[id].ts"), "geg-nachweis-wohnen/[id]": await import("../src/pages/api/geg-nachweis-wohnen/[id].ts"),
"geg-nachweis-wohnen": await import("../src/pages/api/geg-nachweis-wohnen/index.ts"), "geg-nachweis-wohnen": await import("../src/pages/api/geg-nachweis-wohnen/index.ts"),
"objekt": await import("../src/pages/api/objekt/index.ts"),
"rechnung/[id]": await import("../src/pages/api/rechnung/[id].ts"), "rechnung/[id]": await import("../src/pages/api/rechnung/[id].ts"),
"rechnung/anfordern": await import("../src/pages/api/rechnung/anfordern.ts"), "rechnung/anfordern": await import("../src/pages/api/rechnung/anfordern.ts"),
"rechnung": await import("../src/pages/api/rechnung/index.ts"), "rechnung": await import("../src/pages/api/rechnung/index.ts"),
"objekt": await import("../src/pages/api/objekt/index.ts"),
"ticket": await import("../src/pages/api/ticket/index.ts"), "ticket": await import("../src/pages/api/ticket/index.ts"),
"user": await import("../src/pages/api/user/index.ts"), "user": await import("../src/pages/api/user/index.ts"),
"user/self": await import("../src/pages/api/user/self.ts"), "user/self": await import("../src/pages/api/user/self.ts"),

View File

@@ -5,7 +5,6 @@
import { import {
CrossCircled, CrossCircled,
DotsVertical, DotsVertical,
Download,
Pencil2, Pencil2,
QuestionMarkCircled, QuestionMarkCircled,
} from "radix-svelte-icons"; } from "radix-svelte-icons";
@@ -28,10 +27,8 @@
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 { Bell } from "radix-svelte-icons"; import { Bell } from "radix-svelte-icons";
import { A13BerechnungRechnerischeLaufzeitHeizung } from "#lib/Berechnungen/BedarfsausweisWohnen/A13BerechnungRechnerischeLaufzeitHeizung.js"; import mime from "mime"
import mime from "mime"
import { getGEGNachweisGewerbe } from "#lib/server/db.js";
const progress = ausweis.ausgestellt ? 100 : ausweis.bestellt ? 66 : 33; const progress = ausweis.ausgestellt ? 100 : ausweis.bestellt ? 66 : 33;
const ausweisart = getAusweisartFromId(ausweis.id); const ausweisart = getAusweisartFromId(ausweis.id);
@@ -151,6 +148,8 @@
} }
} }
async function ausstellen(post = false) { async function ausstellen(post = false) {
const notification = addNotification({ const notification = addNotification({
message: "Ausweis wird ausgestellt.", message: "Ausweis wird ausgestellt.",
@@ -158,33 +157,124 @@
type: "info", type: "info",
timeout: 0, timeout: 0,
}) })
try {
await api.admin.ausstellen.GET.fetch({ if (ausweisart === Enums.Ausweisart.BedarfsausweisWohnen) {
id_ausweis: ausweis.id, const results: {data: string, name: string, type: "Sonstiges" | "Ausweis"}[] = []
post for (const file of bedarfsausweisAdditionalInput.files || []) {
}, { const reader = new FileReader();
headers: {
"Authorization": `Bearer ${Cookies.get(API_ACCESS_TOKEN_COOKIE_NAME)}` reader.onload = (ev) => {
const result = reader.result;
if (!result) {
addNotification({
message: `Die Datei ${file.name} konnte nicht verarbeitet werden.`,
timeout: 3000,
type: "error",
dismissable: true
})
}
results.push({
data: result as string,
name: file.name,
type: "Sonstiges"
})
} }
}) reader.readAsDataURL(file)
}
ausweis.ausgestellt = true; // Jetzt holen wir uns noch den Ausweis
const files = bedarfsausweisFileInput.files
const reader = new FileReader()
updateNotification(notification, { if (files === null || (files.length === 0)) {
message: "Ausweis ausgestellt.", addNotification({
subtext: "Der Ausweis wurde erfolgreich ausgestellt.", message: "Bitte laden sie vor dem Ausstellen einen Ausweis hoch.",
timeout: 3000, timeout: 3000,
type: "success", type: "error",
}) dismissable: true
} catch(e) { })
updateNotification(notification, { return;
message: "Das hat nicht geklappt.", }
subtext: e as string,
timeout: 3000, reader.onload = (ev) => {
type: "error", const result = reader.result;
})
if (!result) {
addNotification({
message: `Die Datei ${files[0].name} konnte nicht verarbeitet werden.`,
timeout: 3000,
type: "error",
dismissable: true
})
}
results.push({
data: result as string,
name: files[0].name,
type: "Ausweis"
})
}
reader.readAsDataURL(files[0])
try {
await api.admin["bedarfsausweis-ausstellen"].POST.fetch({
id_ausweis: ausweis.id,
post,
files: results
}, {
headers: {
"Authorization": `Bearer ${Cookies.get(API_ACCESS_TOKEN_COOKIE_NAME)}`
}
})
updateNotification(notification, {
message: "Ausweis ausgestellt.",
subtext: "Der Ausweis wurde erfolgreich ausgestellt.",
timeout: 3000,
type: "success",
})
} catch(e) {
updateNotification(notification, {
message: "Das hat nicht geklappt.",
subtext: e as string,
timeout: 3000,
type: "error",
})
}
} else {
try {
await api.admin.ausstellen.GET.fetch({
id_ausweis: ausweis.id,
post
}, {
headers: {
"Authorization": `Bearer ${Cookies.get(API_ACCESS_TOKEN_COOKIE_NAME)}`
}
})
ausweis.ausgestellt = true;
updateNotification(notification, {
message: "Ausweis ausgestellt.",
subtext: "Der Ausweis wurde erfolgreich ausgestellt.",
timeout: 3000,
type: "success",
})
} catch(e) {
updateNotification(notification, {
message: "Das hat nicht geklappt.",
subtext: e as string,
timeout: 3000,
type: "error",
})
}
} }
} }
let bedarfsausweisFileInput: HTMLInputElement;
let bedarfsausweisAdditionalInput: HTMLInputElement;
</script> </script>
<div class="relative bg-base-200 border border-base-300 rounded-lg p-4 m-2"> <div class="relative bg-base-200 border border-base-300 rounded-lg p-4 m-2">
@@ -310,6 +400,12 @@
</div> </div>
{/await} {/await}
{#if benutzer.rolle === Enums.BenutzerRolle.ADMIN && ausweisart === Enums.Ausweisart.BedarfsausweisWohnen}
<span>Laden sie hier den Ausweis hoch</span>
<input type="file" bind:this={bedarfsausweisFileInput}>
<span>Laden sie hier zusätzliche Dokumente hoch</span>
<input type="file" bind:this={bedarfsausweisAdditionalInput} multiple>
{/if}
<div class="flex flex-row justify-start items-center mb-4"> <div class="flex flex-row justify-start items-center mb-4">
<a <a
@@ -406,6 +502,7 @@
{/if} {/if}
{#if benutzer.rolle === Enums.BenutzerRolle.ADMIN} {#if benutzer.rolle === Enums.BenutzerRolle.ADMIN}
<button class="button text-sm" title="Ausstellen" on:click={() => ausstellen(false)}>A</button> <button class="button text-sm" title="Ausstellen" on:click={() => ausstellen(false)}>A</button>
<button class="button text-sm" title="Ausstellen mit Postversand" on:click={() => ausstellen(true)}>P</button> <button class="button text-sm" title="Ausstellen mit Postversand" on:click={() => ausstellen(true)}>P</button>
<button class="button text-sm" title="Stornieren" on:click={stornieren}>S</button> <button class="button text-sm" title="Stornieren" on:click={stornieren}>S</button>

View File

@@ -19,7 +19,7 @@
export let lightTheme: boolean; export let lightTheme: boolean;
export let benutzer: Benutzer; export let benutzer: Benutzer;
export let besteller: Benutzer; export let besteller: Benutzer | null;
let id: string; let id: string;
</script> </script>
@@ -63,7 +63,8 @@
<hr class="border-gray-600" /> <hr class="border-gray-600" />
Besteller {#if besteller}
Besteller
<div class="flex flex-row mb-4"> <div class="flex flex-row mb-4">
<div class="item-center mr-4"> <div class="item-center mr-4">
@@ -82,8 +83,9 @@
</div> </div>
</div> </div>
</div> </div>
<hr class="border-gray-600" /> <hr class="border-gray-600" />
{/if}
<!-- <!--
Mitwirkende Mitwirkende

View File

@@ -8,7 +8,7 @@ import { Benutzer } from "#lib/server/prisma";
export interface Props { export interface Props {
title: string; title: string;
user: Benutzer; user: Benutzer;
besteller: Benutzer; besteller: Benutzer | null;
} }
const { title, user, besteller } = Astro.props; const { title, user, besteller } = Astro.props;

View File

@@ -12,7 +12,7 @@ import {
VerbrauchsausweisGewerbe, VerbrauchsausweisGewerbe,
VerbrauchsausweisWohnen, VerbrauchsausweisWohnen,
} from "#lib/server/prisma.js"; } from "#lib/server/prisma.js";
import { join } from "path" import { join } from "path";
import { APIError, defineApiRoute } from "astro-typesafe-api/server"; import { APIError, defineApiRoute } from "astro-typesafe-api/server";
import { z } from "astro:content"; import { z } from "astro:content";
import { transport } from "#lib/mail.js"; import { transport } from "#lib/mail.js";
@@ -20,9 +20,13 @@ import { BASE_URI } from "#lib/constants.js";
import { getAnsichtsausweis, getDatenblatt } from "#lib/server/ausweis.js"; import { getAnsichtsausweis, getDatenblatt } from "#lib/server/ausweis.js";
import { PutObjectCommand } from "@aws-sdk/client-s3"; import { PutObjectCommand } from "@aws-sdk/client-s3";
import { s3Client } from "#lib/s3.js"; import { s3Client } from "#lib/s3.js";
import { createInvoice, getLexOfficeRechnung, getLexOfficeVoucherNumber } from "#lib/server/invoice.js"; import {
createInvoice,
getLexOfficeRechnung,
getLexOfficeVoucherNumber,
} from "#lib/server/invoice.js";
import { tryCatch } from "#lib/tryCatch.js"; import { tryCatch } from "#lib/tryCatch.js";
import SFTPClient from 'ssh2-sftp-client'; import SFTPClient from "ssh2-sftp-client";
import { import {
getBedarfsausweisWohnenKomplett, getBedarfsausweisWohnenKomplett,
getVerbrauchsausweisGewerbeKomplett, getVerbrauchsausweisGewerbeKomplett,
@@ -33,7 +37,11 @@ import { PDFDocument } from "pdf-lib";
export const GET = defineApiRoute({ export const GET = defineApiRoute({
input: z.object({ input: z.object({
id_ausweis: z.string(), id_ausweis: z.string(),
post: z.boolean().describe("Ob der Ausweis auch per Post ausgestellt werden soll.").optional().default(false) post: z
.boolean()
.describe("Ob der Ausweis auch per Post ausgestellt werden soll.")
.optional()
.default(false),
}), }),
output: z.void(), output: z.void(),
middleware: adminMiddleware, middleware: adminMiddleware,
@@ -51,8 +59,8 @@ export const GET = defineApiRoute({
objekt: Objekt & { objekt: Objekt & {
benutzer: Benutzer | null; benutzer: Benutzer | null;
}; };
}, };
rechnung: Rechnung rechnung: Rechnung;
}) })
| null = null; | null = null;
@@ -76,15 +84,15 @@ export const GET = defineApiRoute({
OR: [ OR: [
{ bedarfsausweis_wohnen: { id: id_ausweis } }, { bedarfsausweis_wohnen: { id: id_ausweis } },
{ verbrauchsausweis_wohnen: { id: id_ausweis } }, { verbrauchsausweis_wohnen: { id: id_ausweis } },
{ verbrauchsausweis_gewerbe: { id: id_ausweis } } { verbrauchsausweis_gewerbe: { id: id_ausweis } },
] ],
}, },
orderBy: { orderBy: {
erstellt_am: "desc", erstellt_am: "desc",
}, },
include: { include: {
benutzer: true benutzer: true,
} },
}); });
if (!rechnung) { if (!rechnung) {
@@ -106,7 +114,7 @@ export const GET = defineApiRoute({
code: "BAD_REQUEST", code: "BAD_REQUEST",
message: message:
"Die Rechnung konnte bei LexOffice nicht angelegt werden..", "Die Rechnung konnte bei LexOffice nicht angelegt werden..",
cause: error cause: error,
}); });
} }
@@ -123,7 +131,7 @@ export const GET = defineApiRoute({
}, },
}); });
} else { } else {
voucherNumber = await getLexOfficeVoucherNumber(rechnung) voucherNumber = await getLexOfficeVoucherNumber(rechnung);
} }
const pdfAusweis = await getAnsichtsausweis( const pdfAusweis = await getAnsichtsausweis(
@@ -146,73 +154,78 @@ export const GET = defineApiRoute({
// TODO: Das ist immer noch scheiße, LexOffice ist doof // TODO: Das ist immer noch scheiße, LexOffice ist doof
// Hier müssen wir warten, damit wir sichergehen können, dass die Rechnung bei LexOffice existiert. // Hier müssen wir warten, damit wir sichergehen können, dass die Rechnung bei LexOffice existiert.
setTimeout(async () => { setTimeout(async () => {
const [pdfRechnung, pdfRechnungError] = await tryCatch(getLexOfficeRechnung(rechnung)); const [pdfRechnung, pdfRechnungError] = await tryCatch(
getLexOfficeRechnung(rechnung)
);
if (pdfRechnungError) { if (pdfRechnungError) {
throw new APIError({ throw new APIError({
code: "INTERNAL_SERVER_ERROR", code: "INTERNAL_SERVER_ERROR",
message: "Rechnungs PDF konnte nicht generiert werden." message: "Rechnungs PDF konnte nicht generiert werden.",
}) });
} }
if (!pdfAusweis) { if (!pdfAusweis) {
throw new APIError({ throw new APIError({
code: "INTERNAL_SERVER_ERROR", code: "INTERNAL_SERVER_ERROR",
message: "Ausweis PDF konnte nicht generiert werden." message: "Ausweis PDF konnte nicht generiert werden.",
}) });
} }
if (!pdfDatenblatt) { if (!pdfDatenblatt) {
throw new APIError({ throw new APIError({
code: "INTERNAL_SERVER_ERROR", code: "INTERNAL_SERVER_ERROR",
message: "Datenblatt PDF konnte nicht generiert werden." message: "Datenblatt PDF konnte nicht generiert werden.",
}) });
} }
const ausweisCommand = new PutObjectCommand({ const ausweisCommand = new PutObjectCommand({
Bucket: "ibc-pdfs", Bucket: "ibc-pdfs",
Key: `ID_${ausweis.id}_Energieausweis.pdf`, Key: `ID_${ausweis.id}_Energieausweis.pdf`,
Body: pdfAusweis, Body: pdfAusweis,
ACL: "private", ACL: "private",
}); });
await s3Client.send(ausweisCommand); await s3Client.send(ausweisCommand);
const datenblattCommand = new PutObjectCommand({ const datenblattCommand = new PutObjectCommand({
Bucket: "ibc-pdfs", Bucket: "ibc-pdfs",
Key: `ID_${ausweis.id}_Datenblatt.pdf`, Key: `ID_${ausweis.id}_Datenblatt.pdf`,
Body: pdfDatenblatt, Body: pdfDatenblatt,
ACL: "private", ACL: "private",
}); });
await s3Client.send(datenblattCommand); await s3Client.send(datenblattCommand);
const rechnungsCommand = new PutObjectCommand({ const rechnungsCommand = new PutObjectCommand({
Bucket: "ibc-pdfs", Bucket: "ibc-pdfs",
Key: `ID_${ausweis.id}_Rechnung.pdf`, Key: `ID_${ausweis.id}_Rechnung.pdf`,
Body: pdfDatenblatt, Body: Buffer.from(pdfRechnung),
ACL: "private", ACL: "private",
}); });
await s3Client.send(rechnungsCommand); await s3Client.send(rechnungsCommand);
// Falls Postversand angefragt wurde müssen wir die Dateien auf den Postserver hochladen // Falls Postversand angefragt wurde müssen wir die Dateien auf den Postserver hochladen
if (post) { if (post) {
const dateiNameDruck = `1011000000000-AW_ID_${ausweis.id}.pdf`; const dateiNameDruck = `1011000000000-AW_ID_${ausweis.id}.pdf`;
const outputPdf = await PDFDocument.create(); const outputPdf = await PDFDocument.create();
async function appendPdf(buffer: Uint8Array<ArrayBufferLike>) { async function appendPdf(buffer: Uint8Array<ArrayBufferLike>) {
const doc = await PDFDocument.load(buffer); const doc = await PDFDocument.load(buffer);
const copiedPages = await outputPdf.copyPages(doc, doc.getPageIndices()); const copiedPages = await outputPdf.copyPages(
doc,
doc.getPageIndices()
);
for (const page of copiedPages) { for (const page of copiedPages) {
outputPdf.addPage(page); outputPdf.addPage(page);
} }
} }
await appendPdf(pdfDatenblatt); await appendPdf(pdfDatenblatt);
await appendPdf(pdfAusweis); await appendPdf(pdfAusweis);
const pdfBytes = await outputPdf.save(); const pdfBytes = await outputPdf.save();
const pdfCommand = new PutObjectCommand({ const pdfCommand = new PutObjectCommand({
@@ -221,38 +234,44 @@ export const GET = defineApiRoute({
Body: pdfBytes, Body: pdfBytes,
ACL: "private", ACL: "private",
}); });
await s3Client.send(pdfCommand); await s3Client.send(pdfCommand);
const sftp = new SFTPClient(); const sftp = new SFTPClient();
try { try {
await sftp.connect({ await sftp.connect({
host: 'api.ppost.de', host: "api.ppost.de",
username: 'jens.cornelsen@ib-cornelsen.de', username: "jens.cornelsen@ib-cornelsen.de",
password: 'ANTQesWYjd', password: "ANTQesWYjd",
}); });
const cwd = await sftp.cwd(); const cwd = await sftp.cwd();
await sftp.put(Buffer.from(pdfBytes), join(cwd, "upload/api", dateiNameDruck)); await sftp.put(
Buffer.from(pdfBytes),
join(cwd, "upload/api", dateiNameDruck)
);
} catch (err) { } catch (err) {
console.error('SFTP Upload failed:', err); console.error("SFTP Upload failed:", err);
throw new APIError({ throw new APIError({
code: "INTERNAL_SERVER_ERROR", code: "INTERNAL_SERVER_ERROR",
message: "Login zum Postversand Server war nicht erfolgreich." message:
"Login zum Postversand Server war nicht erfolgreich.",
}); });
} finally { } finally {
sftp.end(); sftp.end();
} }
} }
let html: string; let html: string;
if (rechnung.status === Enums.Rechnungsstatus.paid) { if (rechnung.status === Enums.Rechnungsstatus.paid) {
html = ` html = `
<p>Sehr geehrte*r ${rechnung.empfaenger},</p> <p>Sehr geehrte*r ${rechnung.empfaenger},</p>
<p>im Anhang finden Sie Ihren geprüften Energieusweis inkl. Rechnung als PDF-Datei. ${post ? "Zusätzlich haben wir Ihren Ausweis per Post verschickt" : ""} Den Rechnungsbetrag haben Sie bereits bezahlt. Vielen Dank.</p> <p>im Anhang finden Sie Ihren geprüften Energieusweis inkl. Rechnung als PDF-Datei. ${
post ? "Zusätzlich haben wir Ihren Ausweis per Post verschickt" : ""
} Den Rechnungsbetrag haben Sie bereits bezahlt. Vielen Dank.</p>
<p> <p>
Mit freundlichen Grüßen, Mit freundlichen Grüßen,
@@ -279,7 +298,9 @@ export const GET = defineApiRoute({
html = ` html = `
<p>Sehr geehrte*r ${rechnung.empfaenger},</p> <p>Sehr geehrte*r ${rechnung.empfaenger},</p>
<p>im Anhang finden Sie Ihren geprüften Energieusweis inkl. Rechnung als PDF-Datei. ${post ? "Zusätzlich haben wir Ihren Ausweis per Post verschickt" : ""} Nachfolgend finden Sie unsere Bankverbindung. Bitte geben Sie als Verwendungszweck die Rechnungsnummer an (siehe unten). Vielen Dank.</p> <p>im Anhang finden Sie Ihren geprüften Energieusweis inkl. Rechnung als PDF-Datei. ${
post ? "Zusätzlich haben wir Ihren Ausweis per Post verschickt" : ""
} Nachfolgend finden Sie unsere Bankverbindung. Bitte geben Sie als Verwendungszweck die Rechnungsnummer an (siehe unten). Vielen Dank.</p>
<br> <br>
<table> <table>
@@ -316,34 +337,38 @@ export const GET = defineApiRoute({
fax 040 · 209339859 fax 040 · 209339859
</p>`; </p>`;
} }
await transport.sendMail({ await transport.sendMail({
from: `"IBCornelsen" <info@online-energieausweis.org>`, from: `"IBCornelsen" <info@online-energieausweis.org>`,
to: rechnung.email || rechnung.benutzer.email, to: rechnung.email || rechnung.benutzer.email,
bcc: "info@online-energieausweis.org", bcc: "info@online-energieausweis.org",
subject: `Ihr Originalausweis vom Ingenieurbüro Cornelsen (ID: ${ausweis.id})`, subject: `Ihr Originalausweis vom Ingenieurbüro Cornelsen (ID: ${ausweis.id})`,
html, html,
attachments: [{ attachments: [
filename: `ID_${ausweis.id}_Energieausweis.pdf`, {
encoding: "binary", filename: `ID_${ausweis.id}_Energieausweis.pdf`,
content: Buffer.from(pdfAusweis), encoding: "binary",
contentType: "application/pdf", content: Buffer.from(pdfAusweis),
contentDisposition: "attachment", contentType: "application/pdf",
}, { contentDisposition: "attachment",
filename: `ID_${ausweis.id}_Datenblatt.pdf`, },
encoding: "binary", {
content: Buffer.from(pdfDatenblatt), filename: `ID_${ausweis.id}_Datenblatt.pdf`,
contentType: "application/pdf", encoding: "binary",
contentDisposition: "attachment", content: Buffer.from(pdfDatenblatt),
}, { contentType: "application/pdf",
filename: `ID_${ausweis.id}_Rechnung.pdf`, contentDisposition: "attachment",
encoding: "binary", },
content: Buffer.from(pdfRechnung), {
contentType: "application/pdf", filename: `ID_${ausweis.id}_Rechnung.pdf`,
contentDisposition: "attachment", encoding: "binary",
}] content: Buffer.from(pdfRechnung),
contentType: "application/pdf",
contentDisposition: "attachment",
},
],
}); });
if (ausweisart === Enums.Ausweisart.VerbrauchsausweisWohnen) { if (ausweisart === Enums.Ausweisart.VerbrauchsausweisWohnen) {
await prisma.verbrauchsausweisWohnen.update({ await prisma.verbrauchsausweisWohnen.update({
where: { where: {
@@ -353,7 +378,9 @@ export const GET = defineApiRoute({
ausgestellt: true, ausgestellt: true,
}, },
}); });
} else if (ausweisart === Enums.Ausweisart.VerbrauchsausweisGewerbe) { } else if (
ausweisart === Enums.Ausweisart.VerbrauchsausweisGewerbe
) {
await prisma.verbrauchsausweisGewerbe.update({ await prisma.verbrauchsausweisGewerbe.update({
where: { where: {
id: ausweis.id, id: ausweis.id,
@@ -372,6 +399,6 @@ export const GET = defineApiRoute({
}, },
}); });
} }
}, 3000) }, 3000);
}, },
}); });

View File

@@ -0,0 +1,361 @@
import { getAusweisartFromId } from "#components/Ausweis/types.js";
import { adminMiddleware } from "#lib/middleware/authorization.js";
import {
Aufnahme,
BedarfsausweisWohnen,
Benutzer,
Bild,
Enums,
Objekt,
prisma,
Rechnung,
} from "#lib/server/prisma.js";
import { join } from "path";
import { APIError, defineApiRoute } from "astro-typesafe-api/server";
import { z } from "astro:content";
import { transport } from "#lib/mail.js";
import { PutObjectCommand } from "@aws-sdk/client-s3";
import { s3Client } from "#lib/s3.js";
import {
createInvoice,
getLexOfficeRechnung,
getLexOfficeVoucherNumber,
} from "#lib/server/invoice.js";
import { tryCatch } from "#lib/tryCatch.js";
import SFTPClient from "ssh2-sftp-client";
import {
getBedarfsausweisWohnenKomplett,
} from "#lib/server/db.js";
import { PDFDocument } from "pdf-lib";
import isBase64 from "is-base64";
import Mail from "nodemailer/lib/mailer/index.js";
import mime from "mime"
export const POST = defineApiRoute({
input: z.object({
id_ausweis: z.string(),
post: z
.boolean()
.describe("Ob der Ausweis auch per Post ausgestellt werden soll.")
.optional()
.default(false),
files: z.object({
data: z.string(),
name: z.string(),
type: z.enum(["Ausweis", "Sonstiges"])
}).array()
}),
output: z.void(),
middleware: adminMiddleware,
async fetch({ id_ausweis, post, files }, context) {
const ausweisart = getAusweisartFromId(id_ausweis);
let ausweis:
| ((BedarfsausweisWohnen) & {
aufnahme: Aufnahme & {
bilder: Bild[];
objekt: Objekt & {
benutzer: Benutzer | null;
};
};
rechnung: Rechnung;
})
| null = null;
if (ausweisart === Enums.Ausweisart.BedarfsausweisWohnen) {
ausweis = await getBedarfsausweisWohnenKomplett(id_ausweis);
} else {
throw new APIError({
code: "BAD_REQUEST",
message: "Nur Bedarfsausweise werden von dieser Route unterstützt."
})
}
if (!ausweis) {
throw new APIError({
code: "BAD_REQUEST",
message: "Ausweis existiert nicht.",
});
}
const rechnung = await prisma.rechnung.findFirst({
where: {
bedarfsausweis_wohnen: { id: id_ausweis }
},
orderBy: {
erstellt_am: "desc",
},
include: {
benutzer: true,
},
});
if (!rechnung) {
throw new APIError({
code: "BAD_REQUEST",
message:
"Die Rechnung wurde noch nicht erstellt, wir können nicht fortfahren.",
});
}
let voucherNumber: string = "";
if (!rechnung.lex_office_id) {
const [result, error] = await tryCatch(
createInvoice(ausweis, rechnung)
);
if (error) {
throw new APIError({
code: "BAD_REQUEST",
message:
"Die Rechnung konnte bei LexOffice nicht angelegt werden..",
cause: error,
});
}
const { id, voucherNumber: lexOfficeVoucherNumber } = result;
voucherNumber = lexOfficeVoucherNumber;
await prisma.rechnung.update({
where: {
id: rechnung.id,
},
data: {
lex_office_id: id,
},
});
} else {
voucherNumber = await getLexOfficeVoucherNumber(rechnung);
}
// Hier müssen wir warten, damit wir sichergehen können, dass die Rechnung bei LexOffice existiert.
setTimeout(async () => {
const [pdfRechnung, pdfRechnungError] = await tryCatch(
getLexOfficeRechnung(rechnung)
);
if (pdfRechnungError) {
throw new APIError({
code: "INTERNAL_SERVER_ERROR",
message: "Rechnungs PDF konnte nicht generiert werden.",
});
}
const processedFiles: Mail.Attachment[] = []
for (const file of files) {
const { data, name, type } = file;
if (!isBase64(data, { mimeRequired: true })) {
throw new APIError({
code: "BAD_REQUEST",
message: `Das Attachment '${name}' kann nicht gespeichert werden.`,
});
}
const dataWithoutPrefix = data.replace(
/^data:image\/\w+;base64,/,
""
);
const buffer = Buffer.from(dataWithoutPrefix, "base64");
const mimeType = mime.getType(name.split(".").pop() as string)
if (!mimeType) {
throw new APIError({
code: "BAD_REQUEST",
message: `Die Datei ${name} hat einen ungültigen MimeType`
})
}
let filename: string;
if (type === "Ausweis") {
filename = `ID_${ausweis.id}_Ausweis.pdf`
} else {
filename = `ID_${ausweis.id}_${name}`;
}
processedFiles.push({
content: buffer,
contentDisposition: "attachment",
contentType: mimeType as string,
encoding: "binary",
filename
})
const command = new PutObjectCommand({
Bucket: "ibc-pdfs",
Key: name,
Body: buffer,
ACL: "private",
});
await s3Client.send(command);
}
const rechnungsCommand = new PutObjectCommand({
Bucket: "ibc-pdfs",
Key: `ID_${ausweis.id}_Rechnung.pdf`,
Body: Buffer.from(pdfRechnung),
ACL: "private",
});
await s3Client.send(rechnungsCommand);
// Falls Postversand angefragt wurde müssen wir die Dateien auf den Postserver hochladen
if (post) {
const dateiNameDruck = `1011000000000-AW_ID_${ausweis.id}.pdf`;
const outputPdf = await PDFDocument.create();
async function appendPdf(buffer: Uint8Array<ArrayBufferLike>) {
const doc = await PDFDocument.load(buffer);
const copiedPages = await outputPdf.copyPages(
doc,
doc.getPageIndices()
);
for (const page of copiedPages) {
outputPdf.addPage(page);
}
}
for (const file of processedFiles) {
await appendPdf(file.content as Buffer);
}
const pdfBytes = await outputPdf.save();
const pdfCommand = new PutObjectCommand({
Bucket: "ibc-pdfs",
Key: dateiNameDruck,
Body: pdfBytes,
ACL: "private",
});
await s3Client.send(pdfCommand);
const sftp = new SFTPClient();
try {
await sftp.connect({
host: "api.ppost.de",
username: "jens.cornelsen@ib-cornelsen.de",
password: "ANTQesWYjd",
});
const cwd = await sftp.cwd();
await sftp.put(
Buffer.from(pdfBytes),
join(cwd, "upload/api", dateiNameDruck)
);
} catch (err) {
console.error("SFTP Upload failed:", err);
throw new APIError({
code: "INTERNAL_SERVER_ERROR",
message:
"Login zum Postversand Server war nicht erfolgreich.",
});
} finally {
sftp.end();
}
}
let html: string;
if (rechnung.status === Enums.Rechnungsstatus.paid) {
html = `
<p>Sehr geehrte*r ${rechnung.empfaenger},</p>
<p>im Anhang finden Sie Ihren geprüften Energieusweis inkl. Rechnung als PDF-Datei. ${
post ? "Zusätzlich haben wir Ihren Ausweis per Post verschickt" : ""
} Den Rechnungsbetrag haben Sie bereits bezahlt. Vielen Dank.</p>
<p>
Mit freundlichen Grüßen,
<br>
Dipl.-Ing. Jens Cornelsen
<br>
<br>
<strong>IB Cornelsen</strong>
<br>
Katendeich 5A
<br>
21035 Hamburg
<br>
www.online-energieausweis.org
<br>
<br>
fon 040 · 209339850
<br>
fax 040 · 209339859
</p>`;
} else {
html = `
<p>Sehr geehrte*r ${rechnung.empfaenger},</p>
<p>im Anhang finden Sie Ihren geprüften Energieusweis inkl. Rechnung als PDF-Datei. ${
post ? "Zusätzlich haben wir Ihren Ausweis per Post verschickt" : ""
} Nachfolgend finden Sie unsere Bankverbindung. Bitte geben Sie als Verwendungszweck die Rechnungsnummer an (siehe unten). Vielen Dank.</p>
<br>
<table>
<tr><td>Kreditinstitut</td><td>:</td><td>\t Commerzbank AG</td>
<tr><td>Empfänger</td><td>:</td><td>\t IB Cornelsen</td>
<tr><td>IBAN</td><td>:<td>\t DE81 2004 0000 0348 6008 00</td>
<tr><td>BIC</td><td>:</td><td>\t COBADEFFXXX</td>
<tr><td>Betrag</td><td>:</td><td>\t <b>${rechnung.betrag}€</b></td>
<tr><td>Verwendungszweck</td><td>:</td><td>\t <b>${voucherNumber}</b></td>
</table>
<br>
<br>
<p>
Mit freundlichen Grüßen,
<br>
Dipl.-Ing. Jens Cornelsen
<br>
<br>
<strong>IB Cornelsen</strong>
<br>
Katendeich 5A
<br>
21035 Hamburg
<br>
www.online-energieausweis.org
<br>
<br>
fon 040 · 209339850
<br>
fax 040 · 209339859
</p>`;
}
await transport.sendMail({
from: `"IBCornelsen" <info@online-energieausweis.org>`,
to: rechnung.email || rechnung.benutzer.email,
bcc: "info@online-energieausweis.org",
subject: `Ihr Originalausweis vom Ingenieurbüro Cornelsen (ID: ${ausweis.id})`,
html,
attachments: processedFiles,
});
if (ausweisart === Enums.Ausweisart.BedarfsausweisWohnen) {
await prisma.bedarfsausweisWohnen.update({
where: {
id: ausweis.id,
},
data: {
ausgestellt: true,
},
});
}
}, 3000);
},
});

View File

@@ -1,21 +1,10 @@
--- ---
import DashboardLayout from "#layouts/DashboardLayout.astro"; import DashboardLayout from "#layouts/DashboardLayout.astro";
import UserLayout from "#layouts/DashboardLayout.astro"; import { Enums } from "#lib/server/prisma";
import { API_ACCESS_TOKEN_COOKIE_NAME } from "#lib/constants";
import { Enums, prisma } from "#lib/server/prisma";
import { createCaller } from "src/astro-typesafe-api-caller";
import DashboardModule from "#modules/Dashboard/DashboardModule.svelte";
import ImpersonateUserModule from "#modules/ImpersonateUserModule.svelte"; import ImpersonateUserModule from "#modules/ImpersonateUserModule.svelte";
import { getCurrentUser } from "#lib/server/user";
const caller = createCaller(Astro) const user = await getCurrentUser(Astro)
const params = Astro.params;
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")
@@ -27,6 +16,6 @@ if (user.rolle !== Enums.BenutzerRolle.ADMIN) {
--- ---
<DashboardLayout title="Impersonate User" {user}> <DashboardLayout title="Impersonate User" {user} besteller={null}>
<ImpersonateUserModule client:load></ImpersonateUserModule> <ImpersonateUserModule client:load></ImpersonateUserModule>
</DashboardLayout> </DashboardLayout>

View File

@@ -1,6 +1,8 @@
--- ---
import { Enums, prisma } from "#lib/server/prisma"; import { Enums, prisma } from "#lib/server/prisma";
import UserLayout from "#layouts/DashboardLayout.astro";
import { getCurrentUser } from "#lib/server/user"; import { getCurrentUser } from "#lib/server/user";
import moment from "moment";
const page = Number(Astro.url.searchParams.get("p")); const page = Number(Astro.url.searchParams.get("p"));
@@ -38,6 +40,15 @@ if (user.rolle === Enums.BenutzerRolle.USER) {
SELECT id, updated_at FROM "GEGNachweisGewerbe" WHERE benutzer_id = ${user.id} SELECT id, updated_at FROM "GEGNachweisGewerbe" WHERE benutzer_id = ${user.id}
ORDER BY updated_at DESC LIMIT 1 OFFSET ${page - 1}`; ORDER BY updated_at DESC LIMIT 1 OFFSET ${page - 1}`;
} else { } else {
const date = moment().subtract(2, "hours").toDate()
// SELECT id, updated_at FROM "VerbrauchsausweisWohnen" WHERE created_at >= ${date} AND bestellt = ${true} UNION ALL
// SELECT id, updated_at FROM "VerbrauchsausweisGewerbe" WHERE created_at >= ${date} AND bestellt = ${true} UNION ALL
// SELECT id, updated_at FROM "BedarfsausweisWohnen" WHERE created_at >= ${date} AND bestellt = ${true} UNION ALL
// SELECT id, updated_at FROM "BedarfsausweisGewerbe" WHERE created_at >= ${date} AND bestellt = ${true} UNION ALL
// SELECT id, updated_at FROM "GEGNachweisWohnen" WHERE created_at >= ${date} AND bestellt = ${true} UNION ALL
// SELECT id, updated_at FROM "GEGNachweisGewerbe" WHERE created_at >= ${date} AND bestellt = ${true}
result = result =
await prisma.$queryRaw`SELECT id, updated_at FROM "VerbrauchsausweisWohnen" UNION ALL await prisma.$queryRaw`SELECT id, updated_at FROM "VerbrauchsausweisWohnen" UNION ALL
SELECT id, updated_at FROM "VerbrauchsausweisGewerbe" UNION ALL SELECT id, updated_at FROM "VerbrauchsausweisGewerbe" UNION ALL
@@ -48,11 +59,11 @@ if (user.rolle === Enums.BenutzerRolle.USER) {
ORDER BY updated_at DESC LIMIT 1 OFFSET ${page - 1}`; ORDER BY updated_at DESC LIMIT 1 OFFSET ${page - 1}`;
} }
if (!result) { if (result.length > 0) {
return Astro.redirect("/dashboard/objekte?p=1"); return Astro.redirect(`/dashboard/objekte/${result[0].id}?p=${page}`)
} }
return Astro.redirect(`/dashboard/objekte/${result[0].id}?p=${page}`)
--- ---
<script></script> <UserLayout title="Objekte" {user} besteller={null}>
<p>Keine Ausweise konnten gefunden werden.</p>
</UserLayout>