86 Commits
UMBE ... dev

Author SHA1 Message Date
Moritz Utcke
f95e87b450 Empfaenger 2025-10-18 11:53:39 -04:00
Moritz Utcke
1de949a08e Update production pipeline 2025-10-17 18:19:11 -04:00
Moritz Utcke
af4bc5b282 Empfänger und Stammdaten speichern 2025-10-17 18:05:53 -04:00
Moritz Utcke
77a71d8f6c Adressdaten werden bei Bestellung nicht direkt übernommen 2025-10-17 13:32:17 -04:00
Moritz Utcke
946a991176 Verbesserungen
1. Info Box direkt beim Login anzeigen anstatt kleine Box unten in der Ecke

2. Pflichtfelder bei Schritt 2 noch nicht in rot markiert
2025-10-17 13:13:43 -04:00
Moritz Utcke
38386ed830 Registriermail 2025-10-17 10:06:40 -04:00
Moritz Utcke
0b01a76953 Verbesserungen Registrierung 2025-10-17 10:05:07 -04:00
Moritz Utcke
462fa79592 Automatische Registrierung 2025-10-16 18:09:18 -04:00
Moritz Utcke
5df588e8e9 Registrierungsprozess verschlankt 2025-10-16 17:34:26 -04:00
Moritz Utcke
5cca857358 Vereinfachter Registrierungsprozess
Nicht-eingeloggte Nutzer sollen sich einfacher registrieren/authentifizieren können.

Speichern oder Bestellen nicht-eingeloggte Nutzer auf der Kundendaten-Seite, so soll ein Popup aufgehen, wo sie einen Bestätigungscode eingeben können. Dieser Code wird umgehend an ihre eingegene Email gesendet.

Wird der Code efolgreich eingegeben gibt es zwei Fälle:

Unter der Email existiert bereits ein Account → Der Ausweis wird dann diesem User zugewiesen

Unter der Email existiet noch kein Account → Ein neuer Account wird angelegt und der Ausweise dem neuen Account zugewiesen

Wird ein neuner Account automatisch angelegt, so benötigen wir auch noch einen Prozess, wie der Nutzer dann sein Passwort vergeben kann. Idealerweise erhält er in seiner Willkommensmail einen Link zur Passwort setzung. Alternativ nutzt er einfach die bestehende Passwortrücksetzen-Funktion auf der Webseite.

Um bei der Erstregistrierung soll ein Zahlencode an die eingegebene E-Mail verschickt werden. Dieser muss dann vom User eingegeben werden um die Registrierung bzw. meistens ja dann die Erstbestellung abzuschließen. Der Zahlencode kann ja dann das Passwort sein. Wir weisen den Kunden darauf hin sich ein eigenes Passwort zu vergeben.
2025-10-16 10:24:35 -04:00
Moritz Utcke
882d2d2f60 Rahmen bleiben rot wenn man Feld ausgefüllt hat 2025-10-14 18:25:07 -04:00
Moritz Utcke
1ef0b5e15a Rechnungsdaten Updaten 2025-10-14 12:18:48 -04:00
Moritz Utcke
5f97e1f37e Daten weg im Bestellptozess nach Passwort anfordern. Joachim Weggler
Nachdem ein neues Passwort angefragt wurde wird der Kunde nun automatisch zurück zum Formular geleitet.
2025-10-14 11:59:15 -04:00
Moritz Utcke
4aa0d125e7 Besseres Error Logging 2025-10-14 11:44:18 -04:00
Moritz Utcke
ecedaaa716 Merge remote-tracking branch 'origin/dev' into dev 2025-10-14 11:43:05 -04:00
Moritz Utcke
817f0075d1 Anzeige nicht ausgefüllte Pflichtfelder bei weiter oft nicht eindeutig. 2025-10-14 11:42:58 -04:00
Jens Cornelsen
944a632495 Fernwärme Pforzheim hinzugefuegt 2025-10-14 17:32:57 +02:00
Moritz Utcke
950bd95c2a Bildupload 2025-10-13 13:33:23 -04:00
Moritz Utcke
4381578205 Bugfixes & Features
1. Wenn pdf hochgeladen wird und abgespeichert wird erscheint es nicht im Formular aber im Dashboard.

2. Loading Spinner beim Hochladen von Dateien damit man sieht das was passiert.

3. Bei Bedarfsausweis Gewerbe anfragen sind in der Liste bei Gebäudetyp nur Wohngebäude!!
2025-10-13 11:33:45 -04:00
Moritz Utcke
d4793af2a4 Autostart bei Datenbank Backup rausgenommen 2025-10-12 19:24:31 -04:00
Moritz Utcke
6f192e816d Update Makefile 2025-10-12 19:04:32 -04:00
Moritz Utcke
4198669b94 DWD Klimafaktoren 2025-10-12 18:55:23 -04:00
Moritz Utcke
2a6e02f395 Fix: Geschosshöhe wird nicht gefetched 2025-10-12 17:43:50 -04:00
Moritz Utcke
fec28de07f Merge remote-tracking branch 'origin/dev' into dev 2025-10-12 17:00:07 -04:00
Moritz Utcke
c41cfe43f0 Email zweimal eingeben 2025-10-12 16:03:47 -04:00
Jens Cornelsen
762c0aeb97 Primaerenergiefasktor Strom angepasst 2025-10-01 16:11:44 +02:00
Moritz Utcke
c010bbaff0 Fläche darf nur noch Integer sein 2025-09-25 11:41:28 -04:00
Moritz Utcke
6e3f749b07 Merge branch 'main' into dev 2025-09-25 10:55:30 -04:00
Moritz Utcke
e4ef1230dd Registrierung Email Lowercase
Bei der Registrierung wird die Email nun immer zu Kleinbuchstaben umgewandelt
2025-09-25 10:54:56 -04:00
Moritz Utcke
a7bdd02652 HEIC und HEIF zu JPEG Konvertierung
Damit wird nun auch das bescheuerte Bildformat von Apple unterstützt
2025-09-25 10:43:49 -04:00
Moritz Utcke
aa60e63d1b Merge branch 'staging' 2025-09-25 10:20:38 -04:00
Moritz Utcke
6b5ae04e36 Merge branch 'dev' into staging 2025-09-25 10:20:31 -04:00
Moritz Utcke
634bef971d Float zu Int und Bildupload 2025-09-25 10:20:18 -04:00
Moritz Utcke
2b76953e68 Merge branch 'staging' [skip ci] 2025-09-25 08:09:22 -04:00
Moritz Utcke
192396933f Merge branch 'dev' into staging 2025-09-25 08:04:54 -04:00
Moritz Utcke
82f8cfc0ff Int zu float umgestellt 2025-09-24 14:26:08 -04:00
Moritz Utcke
c2e96aad74 Merge pull request #573 from IBCornelsen/staging
Resller kann sich nicht einloggen
2025-09-17 10:27:52 -04:00
Moritz Utcke
2d7ea57f57 Merge branch 'dev' into staging 2025-09-17 10:25:28 -04:00
Moritz Utcke
9e6e893c28 Reseller kann sich nicht einloggen 2025-09-17 10:25:15 -04:00
Moritz Utcke
f71c4b9ef9 Merge pull request #572 from IBCornelsen/staging
Klimafaktoren Bug Gefixt
2025-09-14 15:04:19 -03:00
Moritz Utcke
ce8d4bde18 Merge branch 'staging' into dev 2025-09-14 14:03:42 -04:00
Moritz Utcke
4e54511fe0 Klimafaktoren Bug Gefixt
In dem Client Skript was sich die Klimfaktoren abholen sollte war noch eine alte Definition der getKlimafaktoren Funktion genutzt worden.
2025-09-14 14:03:16 -04:00
Moritz Utcke
a1a286b67f Merge pull request #571 from IBCornelsen/staging
Abrechnung Fix
2025-09-12 13:12:35 -03:00
Moritz Utcke
fe4b4fb53a Merge branch 'dev' into staging 2025-09-12 12:11:25 -04:00
Moritz Utcke
b4f84d7ecd Abrechnung Fix 2025-09-12 12:11:05 -04:00
Jens Cornelsen
9157df7a1c Merge pull request #570 from IBCornelsen/staging
Update main
2025-09-12 17:58:20 +02:00
Jens Cornelsen
ef7086a08a Merge pull request #569 from IBCornelsen/dev
Partner Code aufgeräumt
2025-09-12 17:55:34 +02:00
Moritz Utcke
15d57f0a2c Partner Code aufgeräumt 2025-09-12 12:27:57 -03:00
Jens Cornelsen
82993ce74d Merge pull request #568 from IBCornelsen/staging
Update main
2025-09-12 17:14:50 +02:00
Jens Cornelsen
76fa34bf64 Merge pull request #567 from IBCornelsen/dev
Update staging
2025-09-12 17:13:25 +02:00
Jens Cornelsen
d31c8c2fb2 Fernwärme Neumünster 2025-09-08 14:07:41 +02:00
Jens Cornelsen
4f5b94cbd7 Fernwärme Neumünster 2025-09-08 14:05:27 +02:00
Jens Cornelsen
db4325ad2c Fernwärme Erfurt hinzugefügt 2025-09-07 14:00:25 +02:00
Jens Cornelsen
1d2f0a7ce8 Fernwärme Erfurt hinzugefügt 2025-09-07 13:46:11 +02:00
Moritz Utcke
3fc7196652 Docker Datenbank Log Limit 2025-08-30 18:27:00 -04:00
Carl Mahnke
a7bd398f39 Support Popup: Email vorbelegen wenn eingeloggt 2025-08-19 17:07:14 +02:00
Moritz Utcke
cd4e021332 Email von Nutzern klein machen 2025-08-06 10:53:15 -04:00
Moritz Utcke
9c897bd1e3 Merge branch 'main' into dev 2025-08-05 19:49:58 -04:00
Moritz Utcke
addb647618 Layout 2025-08-05 12:13:18 -04:00
Moritz Utcke
8d07486bed Logs entfernt 2025-08-05 11:57:30 -04:00
Moritz Utcke
208ade8219 Fix 2025-08-05 11:48:19 -04:00
Moritz Utcke
2cc44d2c09 Gesamtpreis 2025-08-05 11:36:31 -04:00
Moritz Utcke
366e813f29 Adresse 2025-08-05 11:16:14 -04:00
Moritz Utcke
5683596e09 Zeit Fix 2025-08-05 11:06:27 -04:00
Moritz Utcke
811d574576 Filter 2025-08-05 10:56:55 -04:00
Moritz Utcke
4df30004de Ausweistyp 2025-08-05 10:35:15 -04:00
Moritz Utcke
55a0248de4 Netto Fix 2025-08-05 09:53:45 -04:00
Moritz Utcke
b3c84e1cb6 Sortierung 2025-08-05 09:45:03 -04:00
Moritz Utcke
7b7a1f77a0 Zeitzonen 2025-08-05 09:41:00 -04:00
Moritz Utcke
24da37aa87 Merge pull request #565 from IBCornelsen/dev
Abrechnung PDF
2025-08-05 09:26:47 -04:00
Moritz Utcke
2fdd71f6d6 Merge remote-tracking branch 'origin/dev' into dev 2025-08-05 09:25:43 -04:00
Moritz Utcke
648ecc5901 PDF Abrechnung 2025-08-05 09:25:37 -04:00
Jens Cornelsen
f0e2bd8096 Merge pull request #564 from IBCornelsen/staging
Update main
2025-08-05 14:41:02 +02:00
Jens Cornelsen
10591444a8 Merge pull request #563 from IBCornelsen/dev
Update staging
2025-08-05 14:38:24 +02:00
Jens Cornelsen
6c706573b0 Merge branch 'staging' into dev 2025-08-05 14:33:53 +02:00
Jens Cornelsen
0106d380fc Merge pull request #562 from IBCornelsen/main
Update staging
2025-08-05 14:24:43 +02:00
Moritz Utcke
a05c32167e Benutzer Rolle Fix 2025-08-04 20:16:04 -04:00
Moritz Utcke
cbfa49b7bc Fix? 2025-08-04 19:33:17 -04:00
Moritz Utcke
f56684a6e8 Abrechnung Generierung 2025-08-04 17:23:04 -04:00
Moritz Utcke
2bee3f55c6 Ticket Telefon Hotfix 2025-08-04 11:09:36 -04:00
Moritz Utcke
eb04f5b0b5 Trello ID Hotfix 2025-08-04 11:04:41 -04:00
UMBENOMENA
0866a6e4a1 Merge pull request #561 from IBCornelsen/staging
Landingpage Menu Titel
2025-08-04 00:08:31 +02:00
Robert Jagtiani
0dc4289232 Titel PY- 2025-08-04 00:06:48 +02:00
Robert Jagtiani
6d8d8cb39e landingpage partner header menu und Titel fix 2025-08-04 00:06:24 +02:00
UMBENOMENA
3096f05f67 Merge pull request #560 from IBCornelsen/UMBE
Titel PY-
2025-08-03 22:47:44 +02:00
UMBENOMENA
ea695e3234 Merge pull request #559 from IBCornelsen/UMBE
Landing Page Menu Titel Fix
2025-08-03 22:36:30 +02:00
139 changed files with 1604 additions and 959 deletions

2
.env
View File

@@ -4,6 +4,8 @@
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
SECRET="jg57cya4mrNkGlnb2R85X1gI8LdY7iNZ9MF0Jbn0K5zQBshOxv"
POSTGRES_DB=main
POSTGRES_HOST=localhost
POSTGRES_PORT=5432

View File

@@ -1,76 +0,0 @@
name: Auto Merge Staging into Main
on:
schedule:
- cron: '0 2 * * *' # 2:00 UTC = 4:00 Europäische Zeit
workflow_dispatch:
jobs:
merge:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set Git user
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Fetch all branches
run: |
git fetch origin main
git fetch origin staging
- name: Check if main has commits not in staging
id: check
run: |
git fetch origin
if [ $(git rev-list --count origin/staging..origin/main) -gt 0 ]; then
echo "❌ Staging is behind main and requires manual merging."
exit 1
fi
- name: Create PR from staging to main
id: create_pr
run: |
PR_URL=$(gh pr create --base main --head staging --title "Auto-merge staging into main" --body "This PR was created automatically by GitHub Actions. It merges the latest \`staging\` into \`main\`.")
echo "PR_URL=$PR_URL" >> $GITHUB_OUTPUT
PR_NUMBER=$(echo $PR_URL | awk -F'/' '{print $NF}')
echo "PR_NUMBER=$PR_NUMBER" >> $GITHUB_OUTPUT
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Enable auto-merge on PR
if: steps.create_pr.outputs.PR_NUMBER != ''
run: |
gh pr merge ${{ steps.create_pr.outputs.PR_NUMBER }} --merge --auto
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
notify_failure:
needs: merge
if: failure()
runs-on: ubuntu-latest
steps:
- name: Send Discord notification on failure
run: |
curl -H "Content-Type: application/json" \
-X POST \
-d "{\"content\": \"🚨 Auto-Merge fehlgeschlagen! Bitte manuell prüfen: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}\"}" \
${{ secrets.DISCORD_WEBHOOK_URL }}
notify_success:
needs: merge
if: success()
runs-on: ubuntu-latest
steps:
- name: Send Discord notification on success
run: |
curl -H "Content-Type: application/json" \
-X POST \
-d "{\"content\": \"✅ Auto-Merge ausgeführt! Ergebnis jetzt auf [GitHub](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) und [online-energieausweis.org](https://online-energieausweis.org) einsehen.\"}" \
${{ secrets.DISCORD_WEBHOOK_URL }}

View File

@@ -5,8 +5,33 @@ on:
branches: [main]
jobs:
deploy:
check-migrations:
name: Check for new Prisma migrations
runs-on: ubuntu-latest
outputs:
has_new_migrations: ${{ steps.diff.outputs.has_new_migrations }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # get full history so git diff works properly
- name: Detect new migration files
id: diff
run: |
# Compare the last two commits on main
if git diff --quiet HEAD~1 -- prisma/migrations/; then
echo "✅ No new Prisma migrations detected."
echo "has_new_migrations=false" >> $GITHUB_OUTPUT
else
echo "⚠️ New Prisma migrations detected! Blocking deployment."
echo "has_new_migrations=true" >> $GITHUB_OUTPUT
fi
deploy:
name: Deploy to production
runs-on: ubuntu-latest
needs: check-migrations
if: needs.check-migrations.outputs.has_new_migrations == 'false'
steps:
- uses: actions/checkout@v2
- name: Install Bun
@@ -29,3 +54,15 @@ jobs:
git pull origin main
git status
make prod
block-deploy:
name: Block deployment (new migrations detected)
runs-on: ubuntu-latest
needs: check-migrations
if: needs.check-migrations.outputs.has_new_migrations == 'true'
steps:
- name: Stop deploy
run: |
echo "🚫 Deployment blocked because new Prisma migrations were detected."
echo "Please apply migrations on staging and verify before deploying to production."
exit 1

2
.gitignore vendored
View File

@@ -15,6 +15,8 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
error.log*
combined.log*
# lockfile
pnpm-lock.yaml

View File

@@ -30,6 +30,9 @@ run-database: stop-database
docker volume create $(DB_VOLUME)
docker build -t $(DB_CONTAINER_NAME) .
docker run -d --name $(DB_CONTAINER_NAME) \
--log-driver=json-file \
--log-opt max-size=50m \
--log-opt max-file=3 \
--restart=always \
-e POSTGRES_USER=$(DB_USER) \
-e POSTGRES_PASSWORD=$(DB_PASSWORD) \
@@ -59,7 +62,8 @@ all:
bun run dev 2>&1 | tee ~/logs/`date '+%d-%m-%Y_%H:%M:%S'`.log
update-dwd-klimafaktoren-cron:
pm2 start bun --name "update-dwd-klimafaktoren-cron" --cron "0 12 28 * *" -- src/cronjobs/update-dwd-klimafaktoren.ts
- pm2 delete update-dwd-klimafaktoren-cron
pm2 start --no-autorestart bun --name "update-dwd-klimafaktoren-cron" --cron "0 12 28 * *" -- src/cronjobs/update-dwd-klimafaktoren.ts
prod: prod-no-backup backup-database-cronjob

View File

@@ -5,6 +5,9 @@ import tailwind from "@astrojs/tailwind";
import node from "@astrojs/node";
import mdx from "@astrojs/mdx";
import astroTypesafeAPI from "astro-typesafe-api"
import { logger } from "./src/lib/logger";
logger.info("Astro config loaded");
// https://astro.build/config
export default defineConfig({

View File

@@ -1,7 +1,13 @@
#!/bin/bash
if [[ -z "$prev_restart_delay" && -n "$cron_restart" ]]; then
echo "skipping initial launch...."
exit 0
fi
FILE_NAME=data-dump_`date +%Y-%m-%d"_"%H_%M_%S`.sql.br
FILE_NAME_COMPLETE=full-dump_`date +%Y-%m-%d"_"%H_%M_%S`.sql.br
DATABASE_NAME=database
# Das wird benötigt für AWS Ionos Kompatibilität.
export AWS_REQUEST_CHECKSUM_CALCULATION=when_required
@@ -11,19 +17,15 @@ export AWS_RESPONSE_CHECKSUM_VALIDATION=when_required
# IMPORTANT: Dieser Befehl benötigt das `ionos` Profil, sonst wird er nicht funktionieren.
# Das Profil kann mit `aws configure --profile ionos` erstellt werden.
# Den Key dafür findet man auf https://dcd.ionos.com/latest/?lang=en#/key-management
docker exec -t online-energieausweis-database-1 pg_dump --data-only -U main main | brotli --best > $FILE_NAME
docker exec -t $DATABASE_NAME pg_dump --data-only -U main main | brotli --quality=3 > $FILE_NAME
aws s3 cp $FILE_NAME s3://ibc-db-backup/ --profile ionos --endpoint-url https://s3.eu-central-3.ionoscloud.com --storage-class STANDARD
echo "Uploaded $FILE_NAME"
docker exec -t online-energieausweis-database-1 pg_dumpall -c -U main | brotli --best > $FILE_NAME_COMPLETE
docker exec -t $DATABASE_NAME pg_dumpall -c -U main | brotli --quality=3 > $FILE_NAME_COMPLETE
<<<<<<< HEAD
aws s3 cp $FILE_NAME_COMPLETE s3://ibc-db-backup/ --profile ionos --endpoint-url https://s3-eu-central-3.ionoscloud.com --storage-class STANDARD
=======
aws s3 cp $FILE_NAME_COMPLETE s3://ibc-db-backup/ --profile ionos --endpoint-url https://s3.eu-central-3.ionoscloud.com --storage-class STANDARD
>>>>>>> dev
echo "Uploaded $FILE_NAME_COMPLETE"

View File

@@ -27,7 +27,8 @@
"express": "^4.21.2",
"flag-icons": "^6.15.0",
"fontkit": "^2.0.4",
"highlight.run": "^9.14.0",
"handlebars": "^4.7.8",
"heic2any": "^0.0.4",
"is-base64": "^1.1.0",
"js-cookie": "^3.0.5",
"js-interpolate": "^1.3.2",
@@ -35,11 +36,11 @@
"jwt-decode": "^4.0.0",
"mime": "^4.0.6",
"moment": "^2.30.1",
"moment-timezone": "^0.5.46",
"moment-timezone": "^0.6.0",
"nodemailer": "^6.10.0",
"pdf-lib": "^1.17.1",
"postcss-nested": "^7.0.2",
"puppeteer": "^24.7.2",
"puppeteer": "^24.15.0",
"radix-svelte-icons": "^1.0.0",
"sass": "^1.83.4",
"sharp": "^0.33.5",
@@ -54,7 +55,7 @@
"tailwindcss": "^3.4.17",
"trpc-openapi": "^1.2.0",
"uuid": "^9.0.1",
"winston": "^3.17.0",
"winston": "^3.18.3",
"zod": "^3.24.1",
},
"devDependencies": {
@@ -268,7 +269,7 @@
"@cypress/xvfb": ["@cypress/xvfb@1.2.4", "", { "dependencies": { "debug": "^3.1.0", "lodash.once": "^4.1.1" } }, "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q=="],
"@dabh/diagnostics": ["@dabh/diagnostics@2.0.3", "", { "dependencies": { "colorspace": "1.1.x", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA=="],
"@dabh/diagnostics": ["@dabh/diagnostics@2.0.8", "", { "dependencies": { "@so-ric/colorspace": "^1.1.6", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q=="],
"@daybrush/utils": ["@daybrush/utils@1.13.0", "", {}, "sha512-ALK12C6SQNNHw1enXK+UO8bdyQ+jaWNQ1Af7Z3FNxeAwjYhQT7do+TRE4RASAJ3ObaS2+TJ7TXR3oz2Gzbw0PQ=="],
@@ -524,7 +525,7 @@
"@proload/core": ["@proload/core@0.3.3", "", { "dependencies": { "deepmerge": "^4.2.2", "escalade": "^3.1.1" } }, "sha512-7dAFWsIK84C90AMl24+N/ProHKm4iw0akcnoKjRvbfHifJZBLhaDsDus1QJmhG12lXj4e/uB/8mB/0aduCW+NQ=="],
"@puppeteer/browsers": ["@puppeteer/browsers@2.10.2", "", { "dependencies": { "debug": "^4.4.0", "extract-zip": "^2.0.1", "progress": "^2.0.3", "proxy-agent": "^6.5.0", "semver": "^7.7.1", "tar-fs": "^3.0.8", "yargs": "^17.7.2" }, "bin": { "browsers": "lib/cjs/main-cli.js" } }, "sha512-i4Ez+s9oRWQbNjtI/3+jxr7OH508mjAKvza0ekPJem0ZtmsYHP3B5dq62+IaBHKaGCOuqJxXzvFLUhJvQ6jtsQ=="],
"@puppeteer/browsers": ["@puppeteer/browsers@2.10.6", "", { "dependencies": { "debug": "^4.4.1", "extract-zip": "^2.0.1", "progress": "^2.0.3", "proxy-agent": "^6.5.0", "semver": "^7.7.2", "tar-fs": "^3.1.0", "yargs": "^17.7.2" }, "bin": { "browsers": "lib/cjs/main-cli.js" } }, "sha512-pHUn6ZRt39bP3698HFQlu2ZHCkS/lPcpv7fVQcGBSzNNygw171UXAKrCUhy+TEMw4lEttOKDgNpb04hwUAJeiQ=="],
"@rc-component/async-validator": ["@rc-component/async-validator@5.0.4", "", { "dependencies": { "@babel/runtime": "^7.24.4" } }, "sha512-qgGdcVIF604M9EqjNF0hbUTz42bz/RDtxWdWuU5EQe3hi7M8ob54B6B35rOsvX5eSvIHIzT9iH1R3n+hk3CGfg=="],
@@ -712,6 +713,8 @@
"@smithy/util-waiter": ["@smithy/util-waiter@4.0.2", "", { "dependencies": { "@smithy/abort-controller": "^4.0.1", "@smithy/types": "^4.1.0", "tslib": "^2.6.2" } }, "sha512-piUTHyp2Axx3p/kc2CIJkYSv0BAaheBQmbACZgQSSfWUumWNW+R1lL+H9PDBxKJkvOeEX+hKYEFiwO8xagL8AQ=="],
"@so-ric/colorspace": ["@so-ric/colorspace@1.1.6", "", { "dependencies": { "color": "^5.0.2", "text-hex": "1.0.x" } }, "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw=="],
"@svelte-plugins/datepicker": ["@svelte-plugins/datepicker@1.0.11", "", {}, "sha512-Tqc07QLyRkCpc3Glg6oRLTUApLtCrOh52d6vJ7L32QI17HrwvcDDjaH3LF3X1SBm3CWdMrnqfJp3xjUZmB4wzw=="],
"@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@2.5.3", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^1.0.4", "debug": "^4.3.4", "deepmerge": "^4.3.1", "kleur": "^4.1.5", "magic-string": "^0.30.3", "svelte-hmr": "^0.15.3", "vitefu": "^0.2.4" }, "peerDependencies": { "svelte": "^3.54.0 || ^4.0.0 || ^5.0.0-next.0", "vite": "^4.0.0" } }, "sha512-erhNtXxE5/6xGZz/M9eXsmI7Pxa6MS7jyTy06zN3Ck++ldrppOnOlJwHHTsMC7DHDQdgUp4NAc4cDNQ9eGdB/w=="],
@@ -1060,7 +1063,7 @@
"chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="],
"chromium-bidi": ["chromium-bidi@4.1.1", "", { "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" }, "peerDependencies": { "devtools-protocol": "*" } }, "sha512-biR7t4vF3YluE6RlMSk9IWk+b9U+WWyzHp+N2pL9vRTk+UXHYRTVp7jTK58ZNzMLBgoLMHY4QyJMbeuw3eKxqg=="],
"chromium-bidi": ["chromium-bidi@7.2.0", "", { "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" }, "peerDependencies": { "devtools-protocol": "*" } }, "sha512-gREyhyBstermK+0RbcJLbFhcQctg92AGgDe/h/taMJEOLRdtSswBAO9KmvltFSQWgM2LrwWu5SIuEUbdm3JsyQ=="],
"ci-info": ["ci-info@4.1.0", "", {}, "sha512-HutrvTNsF48wnxkzERIXOe5/mlcfFcbfCmwcg6CJnizbSue78AbDt+1cgl26zwn61WFxhcPykPfZrbqjGmBb4A=="],
@@ -1104,8 +1107,6 @@
"colorette": ["colorette@2.0.19", "", {}, "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ=="],
"colorspace": ["colorspace@1.1.4", "", { "dependencies": { "color": "^3.1.3", "text-hex": "1.0.x" } }, "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w=="],
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
@@ -1244,7 +1245,7 @@
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
"devtools-protocol": ["devtools-protocol@0.0.1425554", "", {}, "sha512-uRfxR6Nlzdzt0ihVIkV+sLztKgs7rgquY/Mhcv1YNCWDh5IZgl5mnn2aeEnW5stYTE0wwiF4RYVz8eMEpV1SEw=="],
"devtools-protocol": ["devtools-protocol@0.0.1464554", "", {}, "sha512-CAoP3lYfwAGQTaAXYvA6JZR0fjGUb7qec1qf4mToyoH2TZgUFeIqYcjh6f9jNuhHfuZiEdH+PONHYrLhRQX6aw=="],
"dezalgo": ["dezalgo@1.0.4", "", { "dependencies": { "asap": "^2.0.0", "wrappy": "1" } }, "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig=="],
@@ -1518,6 +1519,8 @@
"h3": ["h3@1.14.0", "", { "dependencies": { "cookie-es": "^1.2.2", "crossws": "^0.3.2", "defu": "^6.1.4", "destr": "^2.0.3", "iron-webcrypto": "^1.2.1", "ohash": "^1.1.4", "radix3": "^1.1.2", "ufo": "^1.5.4", "uncrypto": "^0.1.3", "unenv": "^1.10.0" } }, "sha512-ao22eiONdgelqcnknw0iD645qW0s9NnrJHr5OBz4WOMdBdycfSas1EQf1wXRsm+PcB2Yoj43pjBPwqIpJQTeWg=="],
"handlebars": ["handlebars@4.7.8", "", { "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0" }, "optionalDependencies": { "uglify-js": "^3.1.4" }, "bin": { "handlebars": "bin/handlebars" } }, "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ=="],
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
@@ -1552,9 +1555,9 @@
"hastscript": ["hastscript@9.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^6.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-jzaLBGavEDKHrc5EfFImKN7nZKKBdSLIdGvCwDZ9TfzbF2ffXiov8CKE445L2Z1Ek2t/m4SKQ2j6Ipv7NyUolw=="],
"hexoid": ["hexoid@2.0.0", "", {}, "sha512-qlspKUK7IlSQv2o+5I7yhUd7TxlOG2Vr5LTa3ve2XSNVKAL/n/u/7KLvKmFNimomDIKvZFXWHv0T12mv7rT8Aw=="],
"heic2any": ["heic2any@0.0.4", "", {}, "sha512-3lLnZiDELfabVH87htnRolZ2iehX9zwpRyGNz22GKXIu0fznlblf0/ftppXKNqS26dqFSeqfIBhAmAj/uSp0cA=="],
"highlight.run": ["highlight.run@9.14.0", "", {}, "sha512-ZR+ZLHlVU8lXqsuto0ZEMAOuvptaTBBf1jradnKDIn9OfAXupcYFbkASDlbsZtyBh2SYJSK50xwrucXujhksRg=="],
"hexoid": ["hexoid@2.0.0", "", {}, "sha512-qlspKUK7IlSQv2o+5I7yhUd7TxlOG2Vr5LTa3ve2XSNVKAL/n/u/7KLvKmFNimomDIKvZFXWHv0T12mv7rT8Aw=="],
"hosted-git-info": ["hosted-git-info@2.8.9", "", {}, "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw=="],
@@ -1962,7 +1965,7 @@
"moment": ["moment@2.30.1", "", {}, "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how=="],
"moment-timezone": ["moment-timezone@0.5.47", "", { "dependencies": { "moment": "^2.29.4" } }, "sha512-UbNt/JAWS0m/NJOebR0QMRHBk0hu03r5dx9GK8Cs0AS3I81yDcOc9k+DytPItgVvBP7J6Mf6U2n3BPAacAV9oA=="],
"moment-timezone": ["moment-timezone@0.6.0", "", { "dependencies": { "moment": "^2.29.4" } }, "sha512-ldA5lRNm3iJCWZcBCab4pnNL3HSZYXVb/3TYr75/1WCTWYuTqYUb5f/S384pncYjJ88lbO8Z4uPDvmoluHJc8Q=="],
"mrmime": ["mrmime@2.0.0", "", {}, "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw=="],
@@ -1980,6 +1983,8 @@
"negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
"neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="],
"neotraverse": ["neotraverse@0.6.18", "", {}, "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA=="],
"netmask": ["netmask@2.0.2", "", {}, "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg=="],
@@ -2190,9 +2195,9 @@
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"puppeteer": ["puppeteer@24.7.2", "", { "dependencies": { "@puppeteer/browsers": "2.10.2", "chromium-bidi": "4.1.1", "cosmiconfig": "^9.0.0", "devtools-protocol": "0.0.1425554", "puppeteer-core": "24.7.2", "typed-query-selector": "^2.12.0" }, "bin": { "puppeteer": "lib/cjs/puppeteer/node/cli.js" } }, "sha512-ifYqoY6wGs0yZeFuFPn8BE9FhuveXkarF+eO18I2e/axdoCh4Qh1AE+qXdJBhdaeoPt6eRNTY4Dih29Jbq8wow=="],
"puppeteer": ["puppeteer@24.15.0", "", { "dependencies": { "@puppeteer/browsers": "2.10.6", "chromium-bidi": "7.2.0", "cosmiconfig": "^9.0.0", "devtools-protocol": "0.0.1464554", "puppeteer-core": "24.15.0", "typed-query-selector": "^2.12.0" }, "bin": { "puppeteer": "lib/cjs/puppeteer/node/cli.js" } }, "sha512-HPSOTw+DFsU/5s2TUUWEum9WjFbyjmvFDuGHtj2X4YUz2AzOzvKMkT3+A3FR+E+ZefiX/h3kyLyXzWJWx/eMLQ=="],
"puppeteer-core": ["puppeteer-core@24.7.2", "", { "dependencies": { "@puppeteer/browsers": "2.10.2", "chromium-bidi": "4.1.1", "debug": "^4.4.0", "devtools-protocol": "0.0.1425554", "typed-query-selector": "^2.12.0", "ws": "^8.18.1" } }, "sha512-P9pZyTmJqKODFCnkZgemCpoFA4LbAa8+NumHVQKyP5X9IgdNS1ZnAnIh1sMAwhF8/xEUGf7jt+qmNLlKieFw1Q=="],
"puppeteer-core": ["puppeteer-core@24.15.0", "", { "dependencies": { "@puppeteer/browsers": "2.10.6", "chromium-bidi": "7.2.0", "debug": "^4.4.1", "devtools-protocol": "0.0.1464554", "typed-query-selector": "^2.12.0", "ws": "^8.18.3" } }, "sha512-2iy0iBeWbNyhgiCGd/wvGrDSo73emNFjSxYOcyAqYiagkYt5q4cPfVXaVDKBsukgc2fIIfLAalBZlaxldxdDYg=="],
"qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="],
@@ -2566,7 +2571,7 @@
"tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="],
"tar-fs": ["tar-fs@3.0.8", "", { "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { "bare-fs": "^4.0.1", "bare-path": "^3.0.0" } }, "sha512-ZoROL70jptorGAlgAYiLoBLItEKw/fUxg9BSYK/dF/GAGYFJOJJJMvjPAKDJraCXFwadD456FCuvLWgfhMsPwg=="],
"tar-fs": ["tar-fs@3.1.0", "", { "dependencies": { "pump": "^3.0.0", "tar-stream": "^3.1.5" }, "optionalDependencies": { "bare-fs": "^4.0.1", "bare-path": "^3.0.0" } }, "sha512-5Mty5y/sOF1YWj1J6GiBodjlDc05CUR8PKXrsnFAiSG0xA+GHeWLovaZPYUDXkH/1iKRf2+M5+OrRgzC7O9b7w=="],
"tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="],
@@ -2666,6 +2671,8 @@
"ufo": ["ufo@1.5.4", "", {}, "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ=="],
"uglify-js": ["uglify-js@3.19.3", "", { "bin": { "uglifyjs": "bin/uglifyjs" } }, "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ=="],
"uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="],
"undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
@@ -2758,19 +2765,21 @@
"widest-line": ["widest-line@5.0.0", "", { "dependencies": { "string-width": "^7.0.0" } }, "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA=="],
"winston": ["winston@3.17.0", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw=="],
"winston": ["winston@3.18.3", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.8", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, "sha512-NoBZauFNNWENgsnC9YpgyYwOVrl2m58PpQ8lNHjV3kosGs7KJ7Npk9pCUE+WJlawVSe8mykWDKWFSVfs3QO9ww=="],
"winston-transport": ["winston-transport@4.9.0", "", { "dependencies": { "logform": "^2.7.0", "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" } }, "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A=="],
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
"wordwrap": ["wordwrap@1.0.0", "", {}, "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="],
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
"wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"ws": ["ws@8.18.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w=="],
"ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
"xml-crypto": ["xml-crypto@6.0.0", "", { "dependencies": { "@xmldom/is-dom-node": "^1.0.1", "@xmldom/xmldom": "^0.8.10", "xpath": "^0.0.33" } }, "sha512-L3RgnkaDrHaYcCnoENv4Idzt1ZRj5U1z1BDH98QdDTQfssScx8adgxhd9qwyYo+E3fXbQZjEQH7aiXHLVgxGvw=="],
@@ -2870,10 +2879,14 @@
"@prisma/schema-files-loader/fs-extra": ["fs-extra@11.1.1", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ=="],
"@puppeteer/browsers/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
"@puppeteer/browsers/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
"@puppeteer/browsers/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"@so-ric/colorspace/color": ["color@5.0.2", "", { "dependencies": { "color-convert": "^3.0.1", "color-string": "^2.0.0" } }, "sha512-e2hz5BzbUPcYlIRHo8ieAhYgoajrJr+hWoceg6E345TPsATMUKqDgzt8fSXZJJbxfpiPzkWyphz8yn8At7q3fA=="],
"@sveltejs/vite-plugin-svelte/vitefu": ["vitefu@0.2.5", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["vite"] }, "sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q=="],
"@types/ssh2/@types/node": ["@types/node@18.19.86", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-fifKayi175wLyKyc5qUfyENhQ1dCNI1UNjp653d8kuYcPQN5JhX3dGuP/XmvPTg/xRBn1VTLpbmi+H/Mr7tLfQ=="],
@@ -2920,8 +2933,6 @@
"co-body/qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="],
"colorspace/color": ["color@3.2.1", "", { "dependencies": { "color-convert": "^1.9.3", "color-string": "^1.6.0" } }, "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA=="],
"csvtojson/strip-bom": ["strip-bom@2.0.0", "", { "dependencies": { "is-utf8": "^0.2.0" } }, "sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g=="],
"cypress/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
@@ -2968,6 +2979,8 @@
"gray-matter/js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="],
"handlebars/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"hasha/type-fest": ["type-fest@0.8.1", "", {}, "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA=="],
"ignore-walk/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="],
@@ -3066,6 +3079,8 @@
"proxy-agent/proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
"puppeteer-core/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
"rc-align/rc-util": ["rc-util@4.21.1", "", { "dependencies": { "add-dom-event-listener": "^1.1.0", "prop-types": "^15.5.10", "react-is": "^16.12.0", "react-lifecycles-compat": "^3.0.4", "shallowequal": "^1.1.0" } }, "sha512-Z+vlkSQVc1l8O2UjR3WQ+XdWlhj5q9BMQNLk2iOBch75CqPfrJyGtcWMcnhRlNuDu0Ndtt4kLVO8JI8BrABobg=="],
"rc-animate/rc-util": ["rc-util@4.21.1", "", { "dependencies": { "add-dom-event-listener": "^1.1.0", "prop-types": "^15.5.10", "react-is": "^16.12.0", "react-lifecycles-compat": "^3.0.4", "shallowequal": "^1.1.0" } }, "sha512-Z+vlkSQVc1l8O2UjR3WQ+XdWlhj5q9BMQNLk2iOBch75CqPfrJyGtcWMcnhRlNuDu0Ndtt4kLVO8JI8BrABobg=="],
@@ -3162,6 +3177,10 @@
"@prisma/internals/globby/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
"@so-ric/colorspace/color/color-convert": ["color-convert@3.1.2", "", { "dependencies": { "color-name": "^2.0.0" } }, "sha512-UNqkvCDXstVck3kdowtOTWROIJQwafjOfXSmddoDrXo4cewMKmusCeF22Q24zvjR8nwWib/3S/dfyzPItPEiJg=="],
"@so-ric/colorspace/color/color-string": ["color-string@2.1.2", "", { "dependencies": { "color-name": "^2.0.0" } }, "sha512-RxmjYxbWemV9gKu4zPgiZagUxbH3RQpEIO77XoSSX0ivgABDZ+h8Zuash/EMFLTI4N9QgFPOJ6JQpPZKFxa+dA=="],
"@types/ssh2/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
"@typescript-eslint/typescript-estree/globby/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
@@ -3182,8 +3201,6 @@
"boxen/wrap-ansi/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
"colorspace/color/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="],
"express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"express/send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="],
@@ -3330,12 +3347,14 @@
"@prisma/internals/@prisma/debug/debug/ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="],
"@so-ric/colorspace/color/color-convert/color-name": ["color-name@2.0.2", "", {}, "sha512-9vEt7gE16EW7Eu7pvZnR0abW9z6ufzhXxGXZEVU9IqPdlsUiMwJeJfRtq0zePUmnbHGT9zajca7mX8zgoayo4A=="],
"@so-ric/colorspace/color/color-string/color-name": ["color-name@2.0.2", "", {}, "sha512-9vEt7gE16EW7Eu7pvZnR0abW9z6ufzhXxGXZEVU9IqPdlsUiMwJeJfRtq0zePUmnbHGT9zajca7mX8zgoayo4A=="],
"boxen/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
"boxen/wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
"colorspace/color/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="],
"form-render/color/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="],
"npm-packlist/glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],

View File

@@ -1,6 +1,9 @@
version: '3'
services:
database:
container_name: database
image: postgres:17.5
build: ./
restart: always
env_file:

View File

@@ -41,7 +41,8 @@
"express": "^4.21.2",
"flag-icons": "^6.15.0",
"fontkit": "^2.0.4",
"highlight.run": "^9.14.0",
"handlebars": "^4.7.8",
"heic2any": "^0.0.4",
"is-base64": "^1.1.0",
"js-cookie": "^3.0.5",
"js-interpolate": "^1.3.2",
@@ -49,11 +50,11 @@
"jwt-decode": "^4.0.0",
"mime": "^4.0.6",
"moment": "^2.30.1",
"moment-timezone": "^0.5.46",
"moment-timezone": "^0.6.0",
"nodemailer": "^6.10.0",
"pdf-lib": "^1.17.1",
"postcss-nested": "^7.0.2",
"puppeteer": "^24.7.2",
"puppeteer": "^24.15.0",
"radix-svelte-icons": "^1.0.0",
"sass": "^1.83.4",
"sharp": "^0.33.5",
@@ -68,7 +69,7 @@
"tailwindcss": "^3.4.17",
"trpc-openapi": "^1.2.0",
"uuid": "^9.0.1",
"winston": "^3.17.0",
"winston": "^3.18.3",
"zod": "^3.24.1"
},
"devDependencies": {

View File

@@ -0,0 +1,49 @@
/*
Warnings:
- The `fenster_art_1` column on the `BedarfsausweisWohnen` table would be dropped and recreated. This will lead to data loss if there is data in the column.
- The `fenster_art_2` column on the `BedarfsausweisWohnen` table would be dropped and recreated. This will lead to data loss if there is data in the column.
- The `dachfenster_art` column on the `BedarfsausweisWohnen` table would be dropped and recreated. This will lead to data loss if there is data in the column.
- The `haustuer_art` column on the `BedarfsausweisWohnen` table would be dropped and recreated. This will lead to data loss if there is data in the column.
- The `dach_daemmung` column on the `BedarfsausweisWohnen` table would be dropped and recreated. This will lead to data loss if there is data in the column.
- The `decke_daemmung` column on the `BedarfsausweisWohnen` table would be dropped and recreated. This will lead to data loss if there is data in the column.
- The `aussenwand_daemmung` column on the `BedarfsausweisWohnen` table would be dropped and recreated. This will lead to data loss if there is data in the column.
- The `boden_daemmung` column on the `BedarfsausweisWohnen` table would be dropped and recreated. This will lead to data loss if there is data in the column.
*/
-- AlterEnum
ALTER TYPE "BenutzerRolle" ADD VALUE 'RESELLER';
-- AlterTable
ALTER TABLE "BedarfsausweisWohnen" DROP COLUMN "fenster_art_1",
ADD COLUMN "fenster_art_1" DOUBLE PRECISION,
DROP COLUMN "fenster_art_2",
ADD COLUMN "fenster_art_2" DOUBLE PRECISION,
DROP COLUMN "dachfenster_art",
ADD COLUMN "dachfenster_art" DOUBLE PRECISION,
DROP COLUMN "haustuer_art",
ADD COLUMN "haustuer_art" DOUBLE PRECISION,
DROP COLUMN "dach_daemmung",
ADD COLUMN "dach_daemmung" DOUBLE PRECISION,
DROP COLUMN "decke_daemmung",
ADD COLUMN "decke_daemmung" DOUBLE PRECISION,
DROP COLUMN "aussenwand_daemmung",
ADD COLUMN "aussenwand_daemmung" DOUBLE PRECISION,
DROP COLUMN "boden_daemmung",
ADD COLUMN "boden_daemmung" DOUBLE PRECISION;
-- CreateTable
CREATE TABLE "Provisionen" (
"id" TEXT NOT NULL,
"ausweisart" TEXT NOT NULL,
"provision_prozent" DOUBLE PRECISION NOT NULL,
"provision_betrag" DOUBLE PRECISION NOT NULL,
"benutzer_id" VARCHAR(11),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Provisionen_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "Provisionen" ADD CONSTRAINT "Provisionen_benutzer_id_fkey" FOREIGN KEY ("benutzer_id") REFERENCES "benutzer"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@@ -0,0 +1,15 @@
/*
Warnings:
- The primary key for the `Provisionen` table will be changed. If it partially fails, the table could be left without primary key constraint.
- The `id` column on the `Provisionen` table would be dropped and recreated. This will lead to data loss if there is data in the column.
- Changed the type of `ausweisart` on the `Provisionen` table. No cast exists, the column would be dropped and recreated, which cannot be done if there is data, since the column is required.
*/
-- AlterTable
ALTER TABLE "Provisionen" DROP CONSTRAINT "Provisionen_pkey",
DROP COLUMN "id",
ADD COLUMN "id" SERIAL NOT NULL,
DROP COLUMN "ausweisart",
ADD COLUMN "ausweisart" "Ausweisart" NOT NULL,
ADD CONSTRAINT "Provisionen_pkey" PRIMARY KEY ("id");

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "benutzer" ADD COLUMN "partner_code" TEXT;

View File

@@ -0,0 +1,12 @@
/*
Warnings:
- Added the required column `ausweistyp` to the `Provisionen` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "Aufnahme" ALTER COLUMN "flaeche" SET DATA TYPE DOUBLE PRECISION,
ALTER COLUMN "nutzflaeche" SET DATA TYPE DOUBLE PRECISION;
-- AlterTable
ALTER TABLE "Provisionen" ADD COLUMN "ausweistyp" "AusweisTyp" NOT NULL;

View File

@@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "Bild" ADD COLUMN "benutzer_id" TEXT;
-- AddForeignKey
ALTER TABLE "Bild" ADD CONSTRAINT "Bild_benutzer_id_fkey" FOREIGN KEY ("benutzer_id") REFERENCES "benutzer"("id") ON DELETE NO ACTION ON UPDATE NO ACTION;

View File

@@ -0,0 +1,10 @@
/*
Warnings:
- You are about to alter the column `flaeche` on the `Aufnahme` table. The data in that column could be lost. The data in that column will be cast from `DoublePrecision` to `Integer`.
- You are about to alter the column `nutzflaeche` on the `Aufnahme` table. The data in that column could be lost. The data in that column will be cast from `DoublePrecision` to `Integer`.
*/
-- AlterTable
ALTER TABLE "Aufnahme" ALTER COLUMN "flaeche" SET DATA TYPE INTEGER,
ALTER COLUMN "nutzflaeche" SET DATA TYPE INTEGER;

View File

@@ -0,0 +1,9 @@
/*
Warnings:
- You are about to drop the column `anrede` on the `benutzer` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "benutzer" DROP COLUMN "anrede",
ADD COLUMN "empfaenger" VARCHAR(100);

View File

@@ -2,6 +2,7 @@
enum BenutzerRolle {
USER
ADMIN
RESELLER
}
model Benutzer {
@@ -16,10 +17,11 @@ model Benutzer {
ort String? @db.VarChar(50)
adresse String? @db.VarChar(150)
telefon String? @db.VarChar(50)
anrede String? @db.VarChar(50)
empfaenger String? @db.VarChar(100)
rolle BenutzerRolle @default(USER)
firma String?
lex_office_id String?
partner_code String?
verified Boolean @default(false)
@@ -50,6 +52,8 @@ model Benutzer {
events Event[]
@@map("benutzer")
Provisionen Provisionen[]
bilder Bild[]
}

View File

@@ -15,6 +15,8 @@ model Bild {
created_at DateTime @default(now())
updated_at DateTime @updatedAt @default(now())
benutzer_id String?
benutzer Benutzer? @relation(fields: [benutzer_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
aufnahme_id String?
aufnahme Aufnahme? @relation(fields: [aufnahme_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
}

View File

@@ -0,0 +1,11 @@
model Provisionen {
id Int @id @default(autoincrement())
ausweisart Ausweisart
ausweistyp AusweisTyp
provision_prozent Float
provision_betrag Float
benutzer_id String? @db.VarChar(11)
benutzer Benutzer? @relation(fields: [benutzer_id], references: [id])
created_at DateTime @default(now())
updated_at DateTime @updatedAt
}

View File

@@ -4,19 +4,35 @@
BUCKET_NAME="ibc-db-backup"
ENDPOINT_URL="https://s3.eu-central-3.ionoscloud.com"
LOCAL_DOWNLOAD_DIR="./" # Where to save the file
DATABASE_NAME=database
# === Check if a custom file is given as a command line argument ===
if [ $# -eq 1 ]; then
CUSTOM_FILE="$1"
echo "🔍 Using custom file: $CUSTOM_FILE"
# Check if the file exists
# Check if file exists locally
if [ ! -f "$CUSTOM_FILE" ]; then
echo "❌ Custom file does not exist: $CUSTOM_FILE"
exit 1
# Check if the file exists on the remote
if ! aws s3api head-object --bucket "$BUCKET_NAME" --key "$CUSTOM_FILE" --endpoint-url "$ENDPOINT_URL" > /dev/null 2>&1; then
echo "❌ Custom file does not exist in S3 bucket or locally."
exit 1
else
echo "📥 Downloading $CUSTOM_FILE from S3"
aws s3 cp "s3://$BUCKET_NAME/$CUSTOM_FILE" "$LOCAL_DOWNLOAD_DIR" \
--endpoint-url "$ENDPOINT_URL"
fi
fi
LATEST_FILE="$CUSTOM_FILE"
FILENAME=$(basename "$LATEST_FILE")
SQL_FILE="${FILENAME%.br}" # Remove .br suffix
if [[ "$FILENAME" == *.br ]]; then
echo "🗜️ Detected compressed file: $FILENAME"
# Remove the .br suffix for the SQL file
SQL_FILE="${FILENAME%.br}" # Remove .br suffix
brotli -d "$FILENAME"
else
SQL_FILE=$FILENAME
fi
else
echo "🔍 No custom file provided, searching for latest .sql.br file in S3"
@@ -36,22 +52,27 @@ else
echo "🔍 Latest file found: $LATEST_FILE"
FILENAME=$(basename "$LATEST_FILE")
SQL_FILE="${FILENAME%.br}" # Remove .br suffix
echo "📥 Downloading $LATEST_FILE"
aws s3 cp "s3://$BUCKET_NAME/$LATEST_FILE" "$LOCAL_DOWNLOAD_DIR" \
--endpoint-url "$ENDPOINT_URL"
brotli -d "$FILENAME"
echo "🗜️ Decompressed to $SQL_FILE"
fi
# === Decompress with Brotli ===
echo "🗜️ Decompressing $FILENAME -> $SQL_FILE"
brotli -d "$FILENAME"
# === Import into Postgres inside Docker ===
echo "🐘 Importing into PostgreSQL (online-energieausweis-database-1:main)"
docker exec -i "online-energieausweis-database-1" env PGPASSWORD="hHMP8cd^N3SnzGRR" \
psql -U "main" -d "main" < "$SQL_FILE"
echo "🐘 Importing into PostgreSQL ($DATABASE_NAME:main)"
docker exec -i "$DATABASE_NAME" env PGPASSWORD="hHMP8cd^N3SnzGRR" \
psql -v ON_ERROR_STOP=0 -U main -d main < "$SQL_FILE"
echo "✅ Import complete."
# === Optional: Clean up
rm "$FILENAME" "$SQL_FILE"
# If custom file was provided, do not delete it
if [ -z "$CUSTOM_FILE" ]; then
echo "🧹 Cleaning up downloaded files..."
rm "$FILENAME" "$SQL_FILE"
fi

View File

@@ -12,31 +12,32 @@ export const createCaller = createCallerFactory({
"admin/nicht-ausstellen": await import("../src/pages/api/admin/nicht-ausstellen.ts"),
"admin/registriernummer": await import("../src/pages/api/admin/registriernummer.ts"),
"admin/stornieren": await import("../src/pages/api/admin/stornieren.ts"),
"aufnahme": await import("../src/pages/api/aufnahme/index.ts"),
"ausweise": await import("../src/pages/api/ausweise/index.ts"),
"aufnahme": await import("../src/pages/api/aufnahme/index.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/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"),
"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"),
"bilder/[id]": await import("../src/pages/api/bilder/[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"),
"bedarfsausweis-wohnen/[id]": await import("../src/pages/api/bedarfsausweis-wohnen/[id].ts"),
"bedarfsausweis-wohnen": await import("../src/pages/api/bedarfsausweis-wohnen/index.ts"),
"objekt": await import("../src/pages/api/objekt/index.ts"),
"rechnung/[id]": await import("../src/pages/api/rechnung/[id].ts"),
"rechnung/anfordern": await import("../src/pages/api/rechnung/anfordern.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"),
"user/autocreate": await import("../src/pages/api/user/autocreate.ts"),
"user": await import("../src/pages/api/user/index.ts"),
"user/self": await import("../src/pages/api/user/self.ts"),
"verbrauchsausweis-gewerbe/[id]": await import("../src/pages/api/verbrauchsausweis-gewerbe/[id].ts"),
"verbrauchsausweis-gewerbe": await import("../src/pages/api/verbrauchsausweis-gewerbe/index.ts"),
"ticket": await import("../src/pages/api/ticket/index.ts"),
"verbrauchsausweis-wohnen/[id]": await import("../src/pages/api/verbrauchsausweis-wohnen/[id].ts"),
"verbrauchsausweis-wohnen": await import("../src/pages/api/verbrauchsausweis-wohnen/index.ts"),
"verbrauchsausweis-gewerbe/[id]": await import("../src/pages/api/verbrauchsausweis-gewerbe/[id].ts"),
"verbrauchsausweis-gewerbe": await import("../src/pages/api/verbrauchsausweis-gewerbe/index.ts"),
"webhooks/mollie": await import("../src/pages/api/webhooks/mollie.ts"),
"aufnahme/[id]/bilder": await import("../src/pages/api/aufnahme/[id]/bilder.ts"),
"aufnahme/[id]": await import("../src/pages/api/aufnahme/[id]/index.ts"),

View File

@@ -1,14 +1,6 @@
import { api } from "astro-typesafe-api/client";
import Cookies from "js-cookie";
import { API_ACCESS_TOKEN_COOKIE_NAME } from "#lib/constants.js";
import {
AufnahmeClient,
BedarfsausweisWohnenClient,
BildClient,
ObjektClient,
VerbrauchsausweisGewerbeClient,
VerbrauchsausweisWohnenClient,
} from "#components/Ausweis/types.js";
import {
Aufnahme,
BedarfsausweisWohnen,

View File

@@ -1,49 +0,0 @@
import { dialogs } from "../../../svelte-dialogs.config";
import { loginClient } from "#lib/login";
import { addNotification } from "#components/Notifications/shared";
export async function spawnLoginPrompt() {
const result = await dialogs.prompt(
[
{
label: "Email",
type: "email",
required: true,
placeholder: "Email",
name: "email"
},
{
label: "Passwort",
type: "password",
name: "passwort",
required: true,
placeholder: "********",
},
],
{
title: "Login",
submitButtonText: "Einloggen",
cancelButtonText: "Abbrechen",
}
);
if (!result) return false;
const [email, passwort] = result;
const loginResult = await loginClient(email, passwort);
if (loginResult === null) {
addNotification({
type: "error",
message: "Einloggen fehlgeschlagen",
dismissable: true,
subtext: "Bitte überprüfen Sie ihre Eingaben und versuchen es erneut.",
timeout: 5000,
})
return false
}
return true
}

View File

@@ -1,67 +0,0 @@
import { dialogs } from "../../../svelte-dialogs.config.js";
import { addNotification } from "#components/Notifications/shared.js";
import { api } from "astro-typesafe-api/client";
export async function spawnSignupPrompt() {
const result = await dialogs.prompt(
[
{
label: "Vorname",
type: "text",
required: true,
placeholder: "Vorname",
name: "vorname"
},
{
label: "Name",
type: "text",
required: true,
placeholder: "Name",
name: "name"
},
{
label: "Email",
type: "email",
required: true,
placeholder: "Email",
name: "email"
},
{
label: "Passwort",
type: "password",
name: "passwort",
required: true,
placeholder: "********",
},
],
{
title: "Registrieren",
submitButtonText: "Registrieren",
cancelButtonText: "Abbrechen",
}
);
if (!result) return false;
const [vorname, name, email, passwort] = result;
try {
const response = await api.user.PUT.fetch({
email,
passwort,
vorname,
name,
});
return true;
} catch(e) {
addNotification({
type: "error",
message: "Registrieren fehlgeschlagen",
dismissable: true,
subtext: "Ein Fehler ist aufgetreten, vielleicht wird die angegebene Email bereits verwendet.",
timeout: 5000,
})
return false;
}
}

View File

@@ -170,7 +170,6 @@ export async function benutzerSpeichern(benutzer: Partial<Benutzer>): Promise<st
email: benutzer.email,
passwort: "",
adresse: benutzer.adresse ?? null,
anrede: benutzer.anrede ?? null,
firma: benutzer.firma ?? null,
vorname: benutzer.vorname ?? null,
ort: benutzer.ort ?? null,

View File

@@ -1,13 +1,17 @@
<script lang="ts">
import { Aufnahme, BedarfsausweisWohnen, Enums, Objekt, Rechnung, VerbrauchsausweisGewerbe, VerbrauchsausweisWohnen } from "#lib/server/prisma.js";
import moment from "moment";
import { Aufnahme, BedarfsausweisWohnen, Enums, Objekt, Provisionen, Rechnung, VerbrauchsausweisGewerbe, VerbrauchsausweisWohnen } from "#lib/server/prisma.js";
import moment from "moment-timezone"
import { DatePicker } from "@svelte-plugins/datepicker"
import { getProvision } from "#lib/provision.js";
export let bestellungen: (Rechnung & {
ausweis: (VerbrauchsausweisWohnen | BedarfsausweisWohnen | VerbrauchsausweisGewerbe) & { aufnahme: Aufnahme & { objekt: Objekt }}
})[];
export let provisionen: Record<Enums.Ausweisart, number>;
export let partnerCodeErstesMal: Date;
export let provisionen: Provisionen[];
export let email: string;
export let startdatum: Date;
export let enddatum: Date;
moment.locale("de");
moment.tz.setDefault("Europe/Berlin");
const bestellungenNachMonat: Record<string, (typeof bestellungen)> = {};
for (const bestellung of bestellungen) {
@@ -20,17 +24,9 @@
}
// Wir brauchen alle Monate zwischen dem ersten Mal, dass der partner_code benutzt wurde bis zum heutigen Zeitpunkt.
const months: Record<string, string> = {
"01": "Januar", "02": "Februar", "03": "März", "04": "April",
"05": "Mai", "06": "Juni", "07": "Juli", "08": "August",
"09": "September", "10": "Oktober", "11": "November", "12": "Dezember"
};
function getMonthlyPeriods(minTime?: Date): moment.Moment[] {
const min = minTime ? moment(minTime) : moment();
const start = min.clone().startOf('month');
const end = moment().add(1, 'month').startOf('month');
function getMonthlyPeriods(from: Date, to: Date): moment.Moment[] {
const start = moment(from).startOf('month');
const end = moment(to).endOf('month');
const monthsArray: moment.Moment[] = [];
const current = start.clone();
@@ -43,14 +39,12 @@
return monthsArray.reverse(); // Most recent month first
}
const periods = getMonthlyPeriods(partnerCodeErstesMal)
const periods = getMonthlyPeriods(startdatum, enddatum)
let isOpen = false;
export let startDate = moment(partnerCodeErstesMal).startOf('month').toDate();
export let endDate = moment().endOf('month').toDate();
$: formattedStartDate = moment(startDate).format("DD.MM.YYYY");
$: formattedEndDate = moment(endDate).format("DD.MM.YYYY");
$: formatiertesStartDatum = moment(startdatum).format("DD.MM.YYYY");
$: formatiertesEndDatum = moment(enddatum).format("DD.MM.YYYY");
function toggleDatePicker() {
isOpen = !isOpen;
}
@@ -62,14 +56,14 @@
<div class="fixed top-0 left-0 right-0 bg-white p-4 shadow z-10">
<div class="flex justify-between items-center">
<DatePicker bind:isOpen bind:startDate bind:endDate isRange={true} onDateChange={onChange} isMultipane={true}>
<input type="text" class="w-min" readonly value={`${formattedStartDate} - ${formattedEndDate}`} on:click={toggleDatePicker} />
<DatePicker bind:isOpen bind:startDate={startdatum} bind:endDate={enddatum} enableFutureDates={false} isRange={true} onDateChange={onChange} isMultipane={true}>
<input type="text" class="w-min" readonly value={`${formatiertesStartDatum} - ${formatiertesEndDatum}`} on:click={toggleDatePicker} />
</DatePicker>
<p>Abrechnungsübersicht für <strong>{email}</strong></p>
</div>
</div>
<main class="my-24 flex justify-center max-w-6xl mx-auto px-4">
<main class="my-24 flex flex-col justify-center max-w-6xl mx-auto px-4">
{#if !bestellungen || bestellungen.length === 0}
<p class="text-center text-gray-500">Keine Bestellungen gefunden.</p>
{/if}
@@ -78,44 +72,62 @@
{#if jahrMonat in bestellungenNachMonat && bestellungenNachMonat[jahrMonat].length > 0}
<!-- Echo dropdown foreach month. -->
{@const provisionMonat = bestellungenNachMonat[jahrMonat].reduce((acc, bestellung) => {
return acc + provisionen[bestellung.ausweis.ausweisart] || 0;
}, 0) * 1.19}
const { provision_prozent, provision_betrag } = getProvision(bestellung.ausweis.ausweisart, bestellung.ausweis.ausweistyp, provisionen);
return acc + provision_betrag;
}, 0)}
<!-- <div onclick="$(this).nextUntil('.dropdown_month').filter('table').toggle(); $('#betrag_gesamt').html('Abrechnungsbetrag $month_name: <b>$provision_month €</b>')" class='dropdown_month'>
<p>$month_name $year_name - Klicke, um Tabelle anzuzeigen</p>
<a target='_blank' rel='noreferrer noopener' href='/user/abrechnung/pdf.php?month={dt.format("m")}&year={dt.format("Y")}'>PDF Ansehen</a>
</div> -->
<table class="w-full mb-4 border-collapse border border-gray-300">
<thead>
<tr class="bg-primary text-white">
<td class="text-center font-bold">ID</td>
<td class="text-center font-bold">DATUM</td>
<td class="text-center font-bold">GEBÄUDEADRESSE </td>
<td class="text-center font-bold">PLZ </td>
<td class="text-center font-bold">ORT </td>
<td class="text-center font-bold">AUSWEIS</td>
<td class="text-center font-bold w-48">BETRAG NETTO</td>
</tr>
</thead>
<tbody class="text-sm">
<tr class="bg-secondary text-white">
<td class="text-center font-bold" colspan="6">{months[dt.format("MM")]} {dt.format("YYYY")}</td>
<td class="text-right font-bold w-48" style="font-family: monospace;">{provisionMonat.toFixed(2)}</td>
</tr>
</tbody>
{#each bestellungenNachMonat[jahrMonat] as bestellung}
{@const provisionBestellung = provisionen[bestellung.ausweis.ausweisart] || 0}
<tr>
<td class="text-center px-4 w-24" style="font-family: monospace;">{bestellung.id}</td>
<td class="text-center font-bold w-32">{moment(bestellung.created_at).format("DD.MM.YYYY")}</td>
<td class="text-left w-64">{bestellung.ausweis.aufnahme.objekt.adresse}</td>
<td class="text-center w-16">{bestellung.ausweis.aufnahme.objekt.plz}</td>
<td class="text-left w-64">{bestellung.ausweis.aufnahme.objekt.ort}</td>
<td class="text-center w-32">{bestellung.ausweis.ausweisart}</td>
<td class="text-right w-48" style="font-family: monospace;">{provisionBestellung.toFixed(2)}</td>
</tr>
{/each}
</table>
<details class="group" open>
<summary class="flex justify-between items-center cursor-pointer p-4 bg-gray-100 hover:bg-gray-200">
<span class="font-semibold">{moment(dt).format("MMMM YYYY")}</span>
<div class="flex flex-row gap-4 items-center">
<a href={`/dashboard/abrechnung/monatlich.pdf?d=${moment(dt).format("YYYY-MM")}`} target="_blank" rel="noreferrer noopener">PDF generieren</a>
<span class="text-gray-500">{provisionMonat.toFixed(2)}</span>
<svg class="w-4 h-4 transition-transform duration-300 group-open:rotate-180" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</div>
</summary>
<div class="p-4">
<table class="w-full mb-4 border-collapse border border-gray-300">
<thead>
<tr class="bg-primary text-white w-full">
<td class="text-center p-2 font-bold">ID</td>
<td class="text-center p-2 font-bold">Datum</td>
<td class="text-center p-2 font-bold">Ausweis</td>
<td class="text-center p-2 font-bold">Provision in %</td>
<td class="text-center p-2 font-bold">Betrag Netto</td>
</tr>
</thead>
<tbody class="text-sm">
{#each bestellungenNachMonat[jahrMonat] as bestellung}
{@const provisionBestellung = getProvision(bestellung.ausweis.ausweisart, bestellung.ausweis.ausweistyp, provisionen)}
<tr class="border-b border-gray-300 hover:bg-gray-100">
<td class="text-center py-2 px-4 w-24" style="font-family: monospace;">{bestellung.ausweis.id}</td>
<td class="text-center py-2 font-bold w-32">{moment(bestellung.created_at).format("DD.MM.YYYY HH:mm")}</td>
<td class="text-center py-2 w-32">{bestellung.ausweis.ausweisart} {bestellung.ausweis.ausweistyp}</td>
<td class="text-center py-2 w-32">{provisionBestellung?.provision_prozent || 0} %</td>
<td class="text-right py-2 w-24" style="font-family: monospace;">{provisionBestellung?.provision_betrag.toFixed(2)}</td>
</tr>
{/each}
</tbody>
</table>
</div>
</details>
{:else if !bestellungenNachMonat[jahrMonat] || bestellungenNachMonat[jahrMonat].length === 0}
<details class="group">
<summary class="flex justify-between items-center cursor-pointer p-4 bg-gray-100 hover:bg-gray-200">
<span class="font-semibold">{moment(dt).format("MMMM YYYY")}</span>
<div class="flex flex-row gap-4 items-center">
<span class="text-gray-500">Keine Bestellungen gefunden</span>
<svg class="w-4 h-4 transition-transform duration-300 group-open:rotate-180" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</div>
</summary>
<div class="p-4 border-t border-gray-200">
<p class="text-gray-500">Für diesen Monat liegen uns keine Bestellungen über ihren Resellercode vor.</p>
</div>
</details>
{/if}
{/each}
</main>

View File

@@ -75,7 +75,7 @@ xl:grid-cols-3 xl:gap-x-8 xl:gap-y-8
>
<option disabled selected value={null}>Bitte auswählen</option>
{#if ausweisart==Enums.Ausweisart.VerbrauchsausweisWohnen || ausweisart === Enums.Ausweisart.GEGNachweisWohnen || ausweisart === Enums.Ausweisart.BedarfsausweisWohnen || ausweisart === Enums.Ausweisart.BedarfsausweisGewerbe}
{#if ausweisart==Enums.Ausweisart.VerbrauchsausweisWohnen || ausweisart === Enums.Ausweisart.GEGNachweisWohnen || ausweisart === Enums.Ausweisart.BedarfsausweisWohnen}
<option value="Einfamilienhaus">Einfamilienhaus</option>
<option value="Freistehendes Einfamilienhaus">Freistehendes Einfamilienhaus</option>
<option value="Freistehendes Zweifamilienhaus">Freistehendes Zweifamilienhaus</option>
@@ -87,7 +87,7 @@ xl:grid-cols-3 xl:gap-x-8 xl:gap-y-8
<option value="Atrium-Bungalow">Atrium-Bungalow</option>
<option value="Winkelbungalow">Winkelbungalow</option>
{:else if ausweisart==Enums.Ausweisart.VerbrauchsausweisGewerbe || ausweisart=== Enums.Ausweisart.GEGNachweisGewerbe}
{:else if ausweisart==Enums.Ausweisart.VerbrauchsausweisGewerbe || ausweisart=== Enums.Ausweisart.GEGNachweisGewerbe || ausweisart === Enums.Ausweisart.BedarfsausweisGewerbe}
<option value="Verwaltungsgebäude (allgemein)">Verwaltungsgebäude (allgemein)</option>
<option value="Parlaments- und Gerichtsgebäude">Parlaments- und Gerichtsgebäude</option>
<option value="Ministerien u. Ämter u. Behörden">Ministerien u. Ämter u. Behörden</option>
@@ -186,6 +186,7 @@ xl:grid-cols-3 xl:gap-x-8 xl:gap-y-8
onlyUnique={true}
minlength={4}
maxlength={4}
required={true}
onFocusIn={() => {
addNotification({
message: "Info",
@@ -249,6 +250,7 @@ xl:grid-cols-3 xl:gap-x-8 xl:gap-y-8
minlength={4}
maxlength={4}
onlyUnique={true}
required={true}
onFocusIn={() => {
addNotification({
message: "Info",
@@ -287,6 +289,7 @@ xl:grid-cols-3 xl:gap-x-8 xl:gap-y-8
onlyUnique={true}
minlength={4}
maxlength={4}
required={true}
onFocusIn={() => {
addNotification({
message: "Info",

View File

@@ -44,11 +44,17 @@
// Wir holen uns die Daten aus dem Formular
const data = new FormData(form);
// Und gleichen diese mit allen Feldern ab die "required" sind, damit stellen wir sicher, dass alles richtig ausgefüllt wurde.
(form.querySelectorAll("select[name][required],input[name][required]") as NodeListOf<HTMLInputElement | HTMLSelectElement>).forEach((element) => {
(form.querySelectorAll("select[name][required], input[name][required]") as NodeListOf<HTMLInputElement | HTMLSelectElement>).forEach((element) => {
const value = data.get(element.getAttribute("name") as string)
if (!value && element.required) {
element.setCustomValidity("Eine Auswahl ist verpflichtend.")
element.dataset["isinvalid"] = "true"
element.addEventListener("change", () => {
element.setCustomValidity("")
element.dataset["isinvalid"] = "false"
})
} else {
element.setCustomValidity("")
}
@@ -178,7 +184,15 @@ sm:grid-cols-[1fr_min-content_min-content_min-content] sm:justify-self-end">
<Overlay bind:hidden={loginOverlayHidden}>
<div class="bg-white w-full max-w-screen-sm py-8">
<EmbeddedAuthFlowModule onLogin={loginAction} email={""}></EmbeddedAuthFlowModule>
<EmbeddedAuthFlowModule onLogin={loginAction} email={""} route="signup" title={
{
login: "Ausweisdaten speichern",
signup: "Ausweisdaten speichern"
}
} buttonText={{
login: "Speichern",
signup: "Speichern"
}}></EmbeddedAuthFlowModule>
</div>
</Overlay>

View File

@@ -1,9 +1,8 @@
<script lang="ts">
import HelpLabel from "#components/labels/HelpLabel.svelte";
import Inputlabel from "#components/labels/InputLabel.svelte";
import PlzSuche from "#components/PlzSuche.svelte";
import ZipSearch from "#components/PlzSuche.svelte";
import { Enums } from "#lib/client/prisma.js";
import { AufnahmeClient, ObjektClient } from "./types.js";
@@ -11,6 +10,24 @@
export let objekt: ObjektClient;
export let ausweisart: Enums.Ausweisart;
function onlyAllowIntegerInput(event: KeyboardEvent) {
const charCode = event.which || event.keyCode;
// Allow only numbers (0-9) and control keys (e.g., backspace)
if (charCode < 48 || charCode > 57) {
event.preventDefault();
}
}
function onlyAllowPastingIntegers(event: ClipboardEvent) {
const clipboardData = event.clipboardData || (window as any).clipboardData;
const pastedData = clipboardData.getData("Text");
if (!/^\d+$/.test(pastedData)) {
event.preventDefault();
}
}
let ortInput: HTMLInputElement;
</script>
<div
@@ -53,9 +70,12 @@ xl:grid-cols-3 xl:gap-x-8 xl:gap-y-8
<div class="input-noHelp">
<Inputlabel title="PLZ *"></Inputlabel>
<ZipSearch
<PlzSuche
bind:zip={objekt.plz}
bind:city={objekt.ort}
onchange={(e) => {
ortInput.dispatchEvent(new Event('change'));
}}
name="plz"
/>
</div>
@@ -69,6 +89,7 @@ xl:grid-cols-3 xl:gap-x-8 xl:gap-y-8
readonly={false}
required
bind:value={objekt.ort}
bind:this={ortInput}
type="text"
/>
@@ -97,10 +118,11 @@ xl:grid-cols-3 xl:gap-x-8 xl:gap-y-8
data-test="flaeche"
maxlength="4"
type="number"
step="1"
required
autocomplete="off"
data-rule-minlength="2"
data-msg-minlength="min. 2 Zeichen"
on:keypress={onlyAllowIntegerInput}
on:paste={onlyAllowPastingIntegers}
bind:value={aufnahme.flaeche}
/>
@@ -124,7 +146,10 @@ xl:grid-cols-3 xl:gap-x-8 xl:gap-y-8
data-test="nutzflaeche"
maxlength="4"
type="number"
step="1"
required
on:keypress={onlyAllowIntegerInput}
on:paste={onlyAllowPastingIntegers}
bind:value={aufnahme.nutzflaeche}
/>

View File

@@ -186,7 +186,7 @@ xl:grid-cols-2 xl:gap-x-8 xl:gap-y-8
>
<option>Bitte auswählen</option>
{#each arrayRange(2.1, 4.5, 0.1) as step}
<option value={step}>{step.toFixed(2)} m</option>
<option value={step} selected={ausweis.geschosshoehe === step}>{step.toFixed(2)} m</option>
{/each}
</select>

View File

@@ -60,6 +60,8 @@
return centeredValue;
}
let translation_1 = 0;
let translation_2 = 0;
$: {
@@ -74,6 +76,7 @@
if (!result) {
return;
}
translation_1 = Math.max(
0,
Math.min(

View File

@@ -16,7 +16,7 @@ const brennstoffe: [
["Flüssiggas", "kg", 13.0, 1.1, 0.27],
["Braunkohle", "kg", 5.5, 1.2, 0.43],
["Holzhackschnitzel", "SRm", 650.0, 0.2, 0.02],
["Strommix", "kWh", 1.0, 2.4, 0.56],
["Strommix", "kWh", 1.0, 1.8, 0.56],
["Fernwärme KWK FB", "kWh", 1.0, 0.7, 0.3],
["Nahwärme KWK FB", "kWh", 1.0, 0.7, 0.3],
["Heizöl EL", "kWh", 1.0, 1.1, 0.31],
@@ -42,6 +42,9 @@ const brennstoffe: [
["Fernwärme HKW FB", "kWh", 1.0, 1.3, 0.4],
["Fernwärme HKW EB", "kWh", 1.0, 0.1, 0.06],
["Fernwärme Hamburg", "kWh", 1.0, 0.33, 0.064],
["Fernwärme Erfurt", "kWh", 1.0, 0.3, 0],
["Fernwärme Neumünster", "kWh", 1.0, 0.28, 0.0133],
["Fernwärme Pforzheim", "kWh", 1.0, 0.25, 0],
["Erdgas", "kWh", 1.0, 1.1, 0.24],
["Heizöl", "kWh", 1.0, 1.1, 0.31],
["Heizöl", "l", 10.0, 1.1, 0.31],

View File

@@ -746,24 +746,29 @@
<div class="card-body">
<div class="flex flex-col flex-wrap items-left gap-2">
<h3 class="font-semibold text-lg">Bilder</h3>
<div class="grid grid-cols-[1fr] md:grid-cols-[1fr,1fr,1fr] lg:grid-cols-[1fr,1fr,1fr] justify-start items-center gap-2">
{#each aufnahme.bilder as bild, i (i)}
<img src="/bilder/{bild.id}.jpg" alt={bild.kategorie} loading="lazy" class="max-h-[10vh] h-full w-full object-contain">
{/each}
</div>
<hr>
<h3 class="font-semibold text-lg">Unterlagen</h3>
<div class="text-sm">
{#if aufnahme.unterlagen.length > 0}
{#each aufnahme.unterlagen as unterlage}
<a href="/unterlagen/{unterlage.id}.{mime.getExtension(unterlage.mime)}" target="_blank" class="text-black flex flex-row items-center gap-2 bg-base-200 p-2 rounded-lg"><FileText size={32}></FileText> {unterlage.name}</a>
{#if aufnahme.bilder.length > 0}
<h3 class="font-semibold text-lg">Bilder</h3>
<div class="grid grid-cols-[1fr] md:grid-cols-[1fr,1fr,1fr] lg:grid-cols-[1fr,1fr,1fr] justify-start items-center gap-2">
{#each aufnahme.bilder as bild, i (i)}
<img src="/bilder/{bild.id}.jpg" alt={bild.kategorie} loading="lazy" class="max-h-[10vh] h-full w-full object-contain">
{/each}
{/if}
</div>
</div>
<hr>
{/if}
{#if aufnahme.unterlagen.length > 0}
<h3 class="font-semibold text-lg">Unterlagen</h3>
<div class="text-sm">
{#if aufnahme.unterlagen.length > 0}
{#each aufnahme.unterlagen as unterlage}
<a href="/unterlagen/{unterlage.id}.{mime.getExtension(unterlage.mime)}" target="_blank" class="text-black flex flex-row items-center gap-2 bg-base-200 p-2 rounded-lg"><FileText size={32}></FileText> {unterlage.name}</a>
{/each}
{/if}
</div>
{/if}
</div>
<div class="dropdown dropdown-top items-end absolute bottom-4 right-4 z-50">
<!-- Benachrichtigungen -->
<!-- <div class="dropdown dropdown-top items-end absolute bottom-4 right-4 z-50">
<div class="indicator">
{#if Object.keys($notifications).length > 0}
<span class="indicator-item badge badge-accent text-xs"
@@ -782,7 +787,7 @@
>
<NotificationProvider component={DashboardNotification} />
</ul>
</div>
</div> -->
</div>
</div>

View File

@@ -5,7 +5,7 @@
CaretDown,
MagnifyingGlass,
} from "radix-svelte-icons";
import { Benutzer } from "#lib/server/prisma.js";
import { Benutzer, Enums } from "#lib/client/prisma.js";
export let lightTheme: boolean;
export let benutzer: Benutzer;
@@ -38,6 +38,12 @@
{benutzer.name}
</div>
<div class="text-base-content text-sm flex">{benutzer.email}</div>
{#if benutzer.rolle === Enums.BenutzerRolle.RESELLER}
<div class="text-xs text-gray-500 flex">Reseller</div>
{/if}
{#if benutzer.rolle === Enums.BenutzerRolle.ADMIN}
<div class="text-xs text-gray-500 flex">Admin</div>
{/if}
<a href="/auth/logout" class="text-xs">Logout</a>
</div>
</div>
@@ -70,7 +76,9 @@
</div>
</div>
{/if}
<!-- <a href="/dashboard/abrechnung" class="button ">Monatliche Abrechnung</a> -->
{#if benutzer.rolle === Enums.BenutzerRolle.RESELLER}
<a href="/dashboard/abrechnung" class="button ">Monatliche Abrechnung</a>
{/if}
</div>
<hr class="border-gray-600" />

View File

@@ -1,10 +1,10 @@
<script lang="ts">
import { BedarfsausweisWohnenClient, GEGNachweisWohnenClient, ObjektClient, UnterlageClient, VerbrauchsausweisGewerbeClient, VerbrauchsausweisWohnenClient } from "./Ausweis/types.js";
import { BedarfsausweisWohnenClient, ObjektClient, VerbrauchsausweisGewerbeClient, VerbrauchsausweisWohnenClient } from "./Ausweis/types.js";
import { Trash, Upload } from "radix-svelte-icons";
import HelpLabel from "#components/labels/HelpLabel.svelte";
export let kategorie: string = "";
export let files: Unterlage[] = [];
export let files: (Unterlage & { preview?: boolean })[] = [];
export let max: number = Infinity;
export let min: number = 1;
export let name: string = "";
@@ -26,6 +26,8 @@
for (let i = 0; i < fileArray.length; i++) {
const file = fileArray[i];
files.push({ preview: true, kategorie, name: file.name, mime: file.type, aufnahme_id: null, id: "" });
files = files;
if (i == max) {
break;
@@ -62,7 +64,13 @@
name: file.name
})
files.push({ id, kategorie, name: file.name, mime: mimeType, aufnahme_id: null });
const placeholder = files.find((f) => f.name === fileArray[i].name && f.preview === true && f.kategorie === kategorie);
if (!placeholder) {
return;
}
placeholder!.preview = false;
placeholder!.id = id;
placeholder!.aufnahme_id = ausweis.id;
files = files;
@@ -110,23 +118,37 @@
<div class="grid grid-cols-2 gap-2">
{#each files as file, i}
{#if file.kategorie == kategorie}
<div class="relative group">
<div
class="h-full max-h-96 w-full rounded-lg border-2 group-hover:contrast-50 object-cover transition-all text-center items-center flex p-4 text-opacity-75 text-black"
>{file.name}</div>
<div class="invisible group-hover:visible absolute left-[50%] top-[50%] translate-x-[-50%] translate-y-[-50%] flex flex-row gap-2">
<button
type="button"
class="rounded-full w-[30px] h-[30px] flex items-center justify-center p-0 bg-[rgba(0,0,0,0.4)]"
on:click={() => {
delete files[i];
files = files.filter((x) => x);
}}
>
<Trash size={20} color="#fff"></Trash>
</button>
{#if file.preview === true}
<!-- Show loading spinner -->
<div class="relative group">
<div
class="h-full max-h-96 w-full rounded-lg border-2 group-hover:contrast-50 object-cover transition-all text-center items-center justify-center flex p-4 text-opacity-75 text-black"
>
<div class="flex flex-row gap-4">
<span>{file.name}</span>
<span class="loader"></span>
</div>
</div>
</div>
</div>
{:else}
<div class="relative group">
<div
class="h-full max-h-96 w-full rounded-lg border-2 group-hover:contrast-50 object-cover transition-all text-center items-center flex p-4 text-opacity-75 text-black"
>{file.name}</div>
<div class="invisible group-hover:visible absolute left-[50%] top-[50%] translate-x-[-50%] translate-y-[-50%] flex flex-row gap-2">
<button
type="button"
class="rounded-full w-[30px] h-[30px] flex items-center justify-center p-0 bg-[rgba(0,0,0,0.4)]"
on:click={() => {
delete files[i];
files = files.filter((x) => x);
}}
>
<Trash size={20} color="#fff"></Trash>
</button>
</div>
</div>
{/if}
{/if}
{/each}

View File

@@ -7,7 +7,7 @@
import Cookies from "js-cookie";
import { API_ACCESS_TOKEN_COOKIE_NAME } from "#lib/constants.js";
export let images: BildClient[] = [];
export let images: (BildClient & { preview?: boolean })[] = [];
export let max: number = Infinity;
export let min: number = 1;
export let name: string = "";
@@ -35,40 +35,43 @@
<div class="grid grid-cols-2 gap-2">
{#each images as image, i}
{#if image.kategorie == kategorie}
<div class="relative group">
<img
src="/bilder/{image.id}.jpg"
alt={kategorie}
class="h-full max-h-96 w-full rounded-lg border-2 group-hover:contrast-50 object-cover transition-all"
/>
<div class="invisible group-hover:visible absolute left-[50%] top-[50%] translate-x-[-50%] translate-y-[-50%] flex flex-row gap-2">
<button
type="button"
class="rounded-full w-[30px] h-[30px] flex items-center justify-center p-0 bg-[rgba(0,0,0,0.4)]"
on:click={() => {
deleteImage(images[i])
}}
>
<Trash size={20} color="#fff"></Trash>
</button>
<!-- <button
type="button"
class="rounded-full w-[30px] h-[30px] flex items-center justify-center p-0 bg-[rgba(0,0,0,0.4)]"
on:click={async () => {
let image = await rotateImage(images[i]);
images[i] = image;
images = images
}}
>
<RotateCounterClockwise size={20} color="#fff"></RotateCounterClockwise>
</button> -->
{#if image.preview === true}
<!-- Show loading spinner -->
<div class="relative group">
<div
class="h-full max-h-96 w-full rounded-lg border-2 group-hover:contrast-50 object-cover transition-all text-center items-center justify-center flex p-4 text-opacity-75 text-black"
>
<div class="flex flex-row gap-4">
<span class="loader"></span>
</div>
</div>
</div>
{:else}
<div class="relative group">
<img
src="/bilder/{image.id}.jpg"
alt={kategorie}
class="h-full max-h-96 w-full rounded-lg border-2 group-hover:contrast-50 object-cover transition-all"
/>
<div class="invisible group-hover:visible absolute left-[50%] top-[50%] translate-x-[-50%] translate-y-[-50%] flex flex-row gap-2">
<button
type="button"
class="rounded-full w-[30px] h-[30px] flex items-center justify-center p-0 bg-[rgba(0,0,0,0.4)]"
on:click={() => {
deleteImage(images[i])
}}
>
<Trash size={20} color="#fff"></Trash>
</button>
</div>
</div>
</div>
{/if}
{/if}
{/each}
<!-- Wir zeigen Platzhalter an, damit der Nutzer sieht wie viele Bilder er hochladen soll -->
{#each { length: Math.max(0, Math.min(max, 4) - images.filter(image => image.kategorie === kategorie).length) } as _, i}
<div class="relative group">
<img
src="/placeholder.png"

View File

@@ -5,7 +5,7 @@
export let readonly: boolean = false;
export let city: string | null | undefined;
export let zip: string | null = "";
export let onchange: (event: Event) => void = () => {};
let hideZipDropdown: boolean = true;
let zipCodes: inferOutput<API["postleitzahlen"]["GET"]> = [];

View File

@@ -19,6 +19,7 @@
export let onFocusIn: () => any = () => {};
export let onFocusOut: () => any = () => {};
export let className: string = "";
export let required: boolean = false;
function addTag(tag: string) {
if ((onlyUnique && tags.indexOf(tag) > -1) || maxTags == tags.length) {
@@ -48,6 +49,8 @@
tag = ""
}
}
$: required = tags.length > 0 ? false : required;
</script>
<div
@@ -83,6 +86,7 @@
class="input input-bordered h-10 px-2 py-1.5 {className}"
{minlength}
{maxlength}
{required}
disabled={disable}
readonly={readonly}
autocomplete="off"

View File

@@ -2,9 +2,10 @@
import { dialogs } from "svelte-dialogs";
import TicketPopup from "./TicketPopup.svelte";
import { addNotification } from "@ibcornelsen/ui";
export let userEmail: string = "";
async function showTicketPopup() {
const success = await dialogs.modal(TicketPopup);
const success = await dialogs.modal(TicketPopup, { email: userEmail });
console.log(success);

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { api } from "astro-typesafe-api/client";
import { getClose } from "svelte-dialogs";
export let email: string = "";
const close = getClose();
@@ -12,7 +13,7 @@
email: email,
metadata: {
category: category,
phone: phone,
telefon: telefon,
},
titel: title,
})
@@ -27,8 +28,8 @@
let category = "";
let title = "";
let description = "";
let email = "";
let phone = "";
//let email = "";
let telefon = "";
</script>
<form class="max-w-lg" on:submit={createTicket}>
@@ -89,9 +90,9 @@
<input
class="input input-bordered"
type="tel"
placeholder="Ihre Telefonnumer"
name="phone"
bind:value={phone}
placeholder="Ihre Telefonnummer"
name="telefon"
bind:value={telefon}
required
/>
</div>

View File

@@ -1,7 +1,9 @@
<script lang="ts">
import HelpLabel from "#components/labels/HelpLabel.svelte";
import type { Enums } from "#lib/client/prisma.js";
import Cookies from "js-cookie";
import { tryCatch } from "#lib/tryCatch.js";
import heic2any from "heic2any";
export let max: number = 2;
export let min: number = 1;
@@ -17,8 +19,9 @@
} from "./Ausweis/types.js";
import { api } from "astro-typesafe-api/client";
import { addNotification } from "./Notifications/shared.js";
import { API_ACCESS_TOKEN_COOKIE_NAME } from "#lib/constants.js";
export let images: BildClient[] = [];
export let images: (BildClient & { preview?: boolean })[] = [];
export let ausweis:
| VerbrauchsausweisWohnenClient
| VerbrauchsausweisGewerbeClient
@@ -37,7 +40,7 @@
for (let i = 0; i < files.length; i++) {
const file = files[i];
if (file.type !== "image/jpeg" && file.type !== "image/png") {
if (file.type !== "image/jpeg" && file.type !== "image/png" && file.type !== "image/webp" && file.type !== "image/heif" && file.type !== "image/heic") {
continue;
}
@@ -45,9 +48,12 @@
break;
}
images.push({ preview: true, benutzer_id: null, kategorie, created_at: new Date(), updated_at: new Date(), id: "" });
images = images;
const reader = new FileReader();
reader.onload = () => {
reader.onload = async () => {
if (reader.readyState != reader.DONE) {
return;
}
@@ -57,6 +63,18 @@
}
let blob = new Blob([reader.result as ArrayBuffer]);
if (file.type === "image/heif" || file.type === "image/heic") {
// Heic files are not supported by canvas, so we convert them to jpeg first
// using an external library.
// This is a workaround until all browsers support heic natively.
// For more information see: https://caniuse.com/?search=heic
// and https://developer.apple.com/documentation/imageio/reading_heif_and_heic_images_on_ios
// and https://stackoverflow.com/questions/65887402/how-to-convert-heic-to-jpeg-in-javascript
// and https://github.com/mbitsnbites/heic2any
blob = await heic2any({ blob, toType: "image/jpeg", quality: 0.8 });
}
let url = URL.createObjectURL(blob);
let image = new Image();
image.onload = async () => {
@@ -85,6 +103,12 @@
data: dataURL,
kategorie,
name: file.name
}, {
headers: {
"Authorization": `Bearer ${Cookies.get(
API_ACCESS_TOKEN_COOKIE_NAME
)}`
}
}))
if (error) {
@@ -96,8 +120,19 @@
dismissable: true
})
} else {
images.push({ id: result.id, kategorie });
images = images;
const index = images.findIndex((img) => img.preview === true);
if (index !== -1) {
delete images[index];
images = images.filter((img) => img);
}
images = [...images, {
benutzer_id: null,
created_at: new Date(),
updated_at: new Date(),
id: result.id,
kategorie
}];
if (i == Math.min(files.length, max) - 1) {
this.value = "";
@@ -128,6 +163,7 @@
<div class="input-standard">
<input
type="file"
accept="image/*"
class="file-input file-input-ghost h-[38px]"
bind:this={fileUpload}
{name}
@@ -144,6 +180,7 @@
<div class="input-standard">
<input
type="file"
accept="image/*"
class="file-input file-input-ghost h-[38px]"
bind:this={fileUpload}
{name}

View File

@@ -11,7 +11,7 @@ export async function auditEndEnergie(ausweis: VerbrauchsausweisWohnenClient | V
if (aufnahme){
if (aufnahme.flaeche && ausweis.verbrauch_1 && ausweis.verbrauch_2 && ausweis.verbrauch_3) {
try {
const response = await getKlimafaktoren(ausweis.startdatum, objekt.plz);
const response = await getKlimafaktoren(ausweis.startdatum as Date, objekt.plz as string);
// Alle Klimfaktoren konnten abgefragt werden.
const eevva = await endEnergieVerbrauchVerbrauchsausweis_2016(ausweis, aufnahme, objekt);
if (eevva){

View File

@@ -2,6 +2,11 @@ import { prisma } from "#lib/server/prisma.js";
import moment from "moment";
import csv from "csvtojson"
if (!process.env.prev_restart_delay && process.env.cron_restart) {
console.log('skipping initial launch....');
process.exit(0);
}
// Als erstes schauen wir, welches das letzte Jahr ist, für das wir einen Verbrauchsausweis haben.
// Das machen wir, indem wir die Ausweise nach Jahr und Monat sortieren und dann den letzten Eintrag nehmen.
const newestDate = await prisma.klimafaktoren.findFirst({

View File

@@ -19,6 +19,7 @@ export type Lueftungskonzept = (typeof Lueftungskonzept)[keyof typeof Lueftungsk
export const BenutzerRolle = {
USER: "USER",
ADMIN: "ADMIN",
RESELLER: "RESELLER",
} as const;
export type BenutzerRolle = (typeof BenutzerRolle)[keyof typeof BenutzerRolle];

View File

@@ -38,19 +38,19 @@ export const BedarfsausweisWohnenSchema = z.object({
volumen: z.number().nullish(),
dicht: z.boolean().nullish(),
fenster_flaeche_1: z.number().nullish(),
fenster_art_1: z.string().nullish(),
fenster_art_1: z.number().nullish(),
fenster_flaeche_2: z.number().nullish(),
fenster_art_2: z.string().nullish(),
fenster_art_2: z.number().nullish(),
dachfenster_flaeche: z.number().nullish(),
dachfenster_art: z.string().nullish(),
dachfenster_art: z.number().nullish(),
haustuer_flaeche: z.number().nullish(),
haustuer_art: z.string().nullish(),
haustuer_art: z.number().nullish(),
dach_bauart: z.string().nullish(),
decke_bauart: z.string().nullish(),
dach_daemmung: z.string().nullish(),
decke_daemmung: z.string().nullish(),
aussenwand_daemmung: z.string().nullish(),
boden_daemmung: z.string().nullish(),
dach_daemmung: z.number().nullish(),
decke_daemmung: z.number().nullish(),
aussenwand_daemmung: z.number().nullish(),
boden_daemmung: z.number().nullish(),
aussenwand_bauart: z.string().nullish(),
boden_bauart: z.string().nullish(),
warmwasser_verteilung: z.string().nullish(),

View File

@@ -13,10 +13,11 @@ export const BenutzerSchema = z.object({
ort: z.string().nullish(),
adresse: z.string().nullish(),
telefon: z.string().nullish(),
anrede: z.string().nullish(),
empfaenger: z.string().nullish(),
rolle: z.nativeEnum(BenutzerRolle),
firma: z.string().nullish(),
lex_office_id: z.string().nullish(),
partner_code: z.string().nullish(),
verified: z.boolean(),
created_at: z.date(),
updated_at: z.date(),

View File

@@ -7,5 +7,6 @@ export const BildSchema = z.object({
name: z.string(),
created_at: z.date(),
updated_at: z.date(),
benutzer_id: z.string().nullish(),
aufnahme_id: z.string().nullish(),
})

View File

@@ -12,6 +12,7 @@ export * from "./gegnachweiswohnen"
export * from "./klimafaktoren"
export * from "./objekt"
export * from "./postleitzahlen"
export * from "./provisionen"
export * from "./rechnung"
export * from "./refreshtokens"
export * from "./tickets"

View File

@@ -0,0 +1,13 @@
import * as z from "zod"
import { Ausweisart, AusweisTyp } from "@prisma/client"
export const ProvisionenSchema = z.object({
id: z.number().int(),
ausweisart: z.nativeEnum(Ausweisart),
ausweistyp: z.nativeEnum(AusweisTyp),
provision_prozent: z.number(),
provision_betrag: z.number(),
benutzer_id: z.string().nullish(),
created_at: z.date(),
updated_at: z.date(),
})

View File

@@ -18,20 +18,6 @@ const { title } = Astro.props;
---
<script>
// import { H } from "highlight.run";
// if (import.meta.env.PROD) {
// H.init("1jdkoe52", {
// serviceName: "online-energieausweis",
// backendUrl: "https://highlight-backend.online-energieausweis.org/public",
// tracingOrigins: true,
// networkRecording: {
// enabled: true,
// recordHeadersAndBody: true
// }
// });
// }
window.addEventListener("scroll", () => {
const skala = document.getElementById("skala");

View File

@@ -17,22 +17,6 @@ export interface Props {
const { title } = Astro.props;
---
<script>
// import { H } from "highlight.run";
// if (import.meta.env.PROD) {
// H.init("1jdkoe52", {
// serviceName: "online-energieausweis",
// backendUrl: "https://highlight-backend.online-energieausweis.org/public",
// tracingOrigins: true,
// networkRecording: {
// enabled: true,
// recordHeadersAndBody: true
// }
// })
// }
</script>
<!DOCTYPE html>
<html lang="de">

View File

@@ -34,22 +34,6 @@ const schema = JSON.stringify({
});
---
<script>
// import { H } from "highlight.run";
// if (import.meta.env.PROD) {
// H.init("1jdkoe52", {
// serviceName: "online-energieausweis",
// backendUrl: "https://highlight-backend.online-energieausweis.org/public",
// tracingOrigins: true,
// networkRecording: {
// enabled: true,
// recordHeadersAndBody: true
// }
// })
// }
</script>
<!DOCTYPE html>
<html lang="de">
<head>

View File

@@ -45,31 +45,6 @@ const schema = JSON.stringify({
let lightTheme = Astro.cookies.get("theme")?.value === "light";
---
<script >
// import { H } from "highlight.run";
// const user = JSON.parse(document.body.dataset.user);
// if (import.meta.env.PROD) {
// H.init("1jdkoe52", {
// serviceName: "online-energieausweis",
// backendUrl:
// "https://highlight-backend.online-energieausweis.org/public",
// tracingOrigins: true,
// networkRecording: {
// enabled: true,
// recordHeadersAndBody: true,
// }
// });
// if (user) {
// H.identify(user.email, {
// id: user.id
// })
// }
// }
</script>
<html lang="de">
<head>
<meta charset="UTF-8" />

View File

@@ -19,19 +19,6 @@ const { title } = Astro.props;
---
<script>
// import { H } from "highlight.run";
// if (import.meta.env.PROD) {
// H.init("1jdkoe52", {
// serviceName: "online-energieausweis",
// backendUrl: "https://highlight-backend.online-energieausweis.org/public",
// tracingOrigins: true,
// networkRecording: {
// enabled: true,
// recordHeadersAndBody: true
// }
// })
// }
/*
window.addEventListener("scroll", (event) => {
@@ -150,7 +137,7 @@ height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
<Footer />
<NotificationWrapper client:load />
<TicketButton client:load></TicketButton>
<TicketButton client:load userEmail={user?.email ?? ""}></TicketButton>
</body>
</html>

View File

@@ -10,9 +10,8 @@ export async function endEnergieVerbrauchVerbrauchsausweisGewerbe_2016_Client(
) {
const startdatum = moment(ausweis.startdatum);
let klimafaktoren = await getKlimafaktoren(
objekt.plz as string,
startdatum.toDate(),
startdatum.add(2, "years").toDate()
objekt.plz as string,
);
if (!klimafaktoren || klimafaktoren.length === 0) {

View File

@@ -13,12 +13,14 @@ export async function endEnergieVerbrauchVerbrauchsausweis_2016_Client(
objekt: ObjektClient,
) {
const startdatum = moment(ausweis.startdatum);
let klimafaktoren = await getKlimafaktoren(
objekt.plz as string,
startdatum.toDate(),
startdatum.add(2, "years").toDate()
objekt.plz as string
);
console.log("Klimafaktoren", klimafaktoren);
if (!klimafaktoren || klimafaktoren.length === 0) {
klimafaktoren = [
{

View File

@@ -3,21 +3,17 @@ import { memoize } from "./Memoization.js";
import { api } from "astro-typesafe-api/client"
export const getKlimafaktoren = memoize(async (date: Date, plz: string) => {
if (!plz || !date) {
if (!plz || plz.length < 5 || !date) {
return null;
}
try {
const response = await api.klimafaktoren.GET.fetch({
plz,
genauigkeit: "years",
// @ts-ignore Der Adapter nimmt z.coerce.date()
startdatum: moment(date).utc(true).toString(),
// @ts-ignore Der Adapter nimmt z.coerce.date()
enddatum: moment(date).add(2, "years").utc(true).toString(),
});
return response;
} catch (e) {
return null;
}
const response = await api.klimafaktoren.GET.fetch({
plz,
genauigkeit: "years",
// @ts-ignore Der Adapter nimmt z.coerce.date()
startdatum: moment(date).utc(true).toString(),
// @ts-ignore Der Adapter nimmt z.coerce.date()
enddatum: moment(date).add(2, "years").utc(true).toString(),
});
return response;
});

View File

@@ -1,18 +1,21 @@
type MemoizedFunction<T> = (...args: any[]) => T;
type MemoizedFunction<T extends (...args: any[]) => Promise<any> | any> = (...args: Parameters<T>) => Promise<ReturnType<T>> | ReturnType<T>;
export function memoize<T>(func: (...args: any[]) => T): MemoizedFunction<T> {
const cache = new Map<string, T>();
export function memoize<T extends (...args: any[]) => Promise<any> | any>(func: T): MemoizedFunction<T> {
const cache = new Map<string, ReturnType<T>>();
return (...args: any[]): T => {
return (...args: Parameters<T>): Promise<ReturnType<T>> | ReturnType<T> => {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key)!;
}
const result = func(...args);
cache.set(key, result);
return result;
if (result instanceof Promise) {
return result.then(resolved => {
return resolved;
});
} else {
cache.set(key, result);
return result;
}
};
}

25
src/lib/logger.ts Normal file
View File

@@ -0,0 +1,25 @@
import winston, { format} from "winston";
const { combine, timestamp } = format;
const loggingFormat = format.printf(({ level, message, timestamp }) => {
return `${timestamp} [${level}]: ${message}`;
});
export const logger = winston.createLogger({
level: "info",
format: combine(
timestamp(),
loggingFormat
),
transports: [
new winston.transports.File({ filename: "error.log", level: "error" }),
new winston.transports.File({ filename: "combined.log" }),
],
});
if (process.env.NODE_ENV !== "production") {
logger.add(new winston.transports.Console({
format: combine(timestamp(), loggingFormat),
}));
}

View File

@@ -92,11 +92,14 @@ export async function authorizationMiddleware(input: any, ctx: TypesafeAPIContex
}
export async function maybeAuthorizationMiddleware(input: any, ctx: TypesafeAPIContextWithRequest<any>) {
let user = null;
try {
return authorizationMiddleware(input, ctx)
user = await authorizationMiddleware(input, ctx)
} catch(e) {
return null;
console.log(e);
}
return user;
}
export const authorizationHeaders = {

14
src/lib/provision.ts Normal file
View File

@@ -0,0 +1,14 @@
import { Enums, Provisionen } from "./client/prisma.js";
export function getProvision(ausweisart: Enums.Ausweisart, ausweistyp: Enums.AusweisTyp, provisionen: Provisionen[]): { provision_prozent: number, provision_betrag: number } {
const provision = provisionen.find(p => p.ausweisart === ausweisart && p.ausweistyp === ausweistyp);
return {
provision_prozent: provision?.provision_prozent || 0,
provision_betrag: provision?.provision_betrag || 0
};
}
export function getProductName(ausweisart: Enums.Ausweisart, ausweistyp: Enums.AusweisTyp): string {
return `${Enums.Ausweisart[ausweisart]} ${Enums.AusweisTyp[ausweistyp]}`;
}

View File

@@ -193,6 +193,11 @@ export function extrahiereAusweisAusFeldMitMehrerenAusweisen<T>(
: verbrauchsausweis_wohnen
? Enums.Ausweisart.VerbrauchsausweisWohnen
: Enums.Ausweisart.VerbrauchsausweisGewerbe,
ausweistyp: bedarfsausweis_wohnen
? bedarfsausweis_wohnen.ausweistyp
: verbrauchsausweis_wohnen
? verbrauchsausweis_wohnen.ausweistyp
: verbrauchsausweis_gewerbe?.ausweistyp || Enums.AusweisTyp.Standard,
},
} as {
ausweis: (

View File

@@ -2,6 +2,7 @@ import os from "os"
import fs from "fs"
export const PERSISTENT_DIR = `${os.homedir()}/persistent/online-energieausweis`
export const SECRET = process.env.SECRET as string
if (!fs.existsSync(PERSISTENT_DIR)) {
fs.mkdirSync(PERSISTENT_DIR, {recursive: true})

View File

@@ -0,0 +1,45 @@
import { transport } from "#lib/mail.js";
import {
Benutzer,
} from "#lib/client/prisma.js";
export async function sendAutoRegisterMail(
user: Benutzer,
password: string
) {
await transport.sendMail({
from: `"IBCornelsen" <info@online-energieausweis.org>`,
to: user.email,
subject: `Ihre Registrierung bei IBCornelsen`,
bcc: "info@online-energieausweis.org",
html: `<p>Sehr geehrte/r Kund/in,</p>
<p>vielen Dank für Ihre Registrierung bei IBCornelsen. Ihr Benutzerkonto wurde erfolgreich erstellt.<br><br>
Nachfolgend finden Sie Ihre Zugangsdaten:<br><br>
E-Mail: ${user.email}<br>
Passwort: ${password}<br><br>
Sollten Sie diese Registrierung nicht vorgenommen haben, können Sie diese E-Mail einfach ignorieren. Ihr Benutzerkonto wird in diesem Fall nicht aktiviert.<br><br>
Falls Sie Fragen haben oder Unterstützung benötigen, stehen wir Ihnen gerne zur Verfügung. Kontaktieren Sie uns einfach unter <a href="mailto:support@online-energieausweis.org">support@online-energieausweis.org</a>.
<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>`
});
}

View File

@@ -75,6 +75,6 @@ export async function sendInvoiceMail(
name: rechnung.empfaenger || "",
},
bcc: "info@online-energieausweis.org",
html: `<p>Sehr geehrte*r ${user.vorname} ${user.name},</p>` + getPaymentInvoiceBody(ausweis, rechnung, ausweisart),
html: `<p>Sehr geehrte/r ${user.vorname} ${user.name},</p>` + getPaymentInvoiceBody(ausweis, rechnung, ausweisart),
});
}

View File

@@ -80,6 +80,6 @@ export async function sendPaymentSuccessMail(
name: rechnung.empfaenger || "",
},
bcc: "info@online-energieausweis.org",
html: `<p>Sehr geehrte*r ${user.vorname} ${user.name},</p>` + getPaymentSuccessBody(ausweis, rechnung, ausweisart),
html: `<p>Sehr geehrte/r ${user.vorname} ${user.name},</p>` + getPaymentSuccessBody(ausweis, rechnung, ausweisart),
});
}

View File

@@ -1,31 +1,30 @@
import { BASE_URI } from "#lib/constants.js";
import { transport } from "#lib/mail.js";
import {
Benutzer,
} from "#lib/client/prisma.js";
import { encodeToken } from "#lib/auth/token.js";
import { TokenType } from "#lib/auth/types.js";
export async function sendRegisterMail(
user: Benutzer
user: Benutzer,
passwort: string
) {
const verificationJwt = encodeToken({
typ: TokenType.Verify,
exp: Date.now() + (15 * 60 * 1000),
uid: user.uid
})
await transport.sendMail({
from: `"IBCornelsen" <info@online-energieausweis.org>`,
to: user.email,
subject: `Ihre Registrierung bei IBCornelsen`,
bcc: "info@online-energieausweis.org",
html: `<p>Sehr geehrte*r ${user.vorname} ${user.name},</p>
html: `<p>Sehr geehrte/r ${user.vorname} ${user.name},</p>
<p>vielen Dank für Ihre Registrierung bei IBCornelsen. Ihr Benutzerkonto wurde erfolgreich erstellt.<br><br>
Um Ihre Registrierung abzuschließen, klicken Sie bitte auf den folgenden Link, um Ihre E-Mail-Adresse zu bestätigen:<br><br>
Nachfolgend finden Sie Ihre Zugangsdaten:<br><br>
E-Mail: ${user.email}<br>
Passwort: ${passwort}<br><br>
<a href="${BASE_URI}/auth/verify?t=${verificationJwt}">E-Mail-Adresse bestätigen</a><br></p>
Aus Sicherheitsgründen empfehlen wir Ihnen, Ihr Passwort nach dem ersten Login zu ändern.<br><br>
Um sich anzumelden, besuchen Sie bitte unsere Website unter <a href="https://online-energieausweis.org/auth/login">online-energieausweis.org/auth/login</a>.<br><br>
Sollten Sie diese Registrierung nicht vorgenommen haben, können Sie diese E-Mail einfach ignorieren. Ihr Benutzerkonto wird in diesem Fall nicht aktiviert.<br><br>
Falls Sie Fragen haben oder Unterstützung benötigen, stehen wir Ihnen gerne zur Verfügung. Kontaktieren Sie uns einfach unter <a href="mailto:support@online-energieausweis.org">support@online-energieausweis.org</a>.
<p>
Mit freundlichen Grüßen,
<br>

View File

@@ -7,7 +7,7 @@ export async function sendAusweisGespeichertMail(user: Benutzer, ausweis_id: str
from: `"IBCornelsen" <info@online-energieausweis.org>`,
to: user.email,
subject: `Ihr Ausweis wurde gespeichert - IBCornelsen - (ID: ${ausweis_id})`,
html: `<p>Sehr geehrte*r ${user.vorname} ${user.name},</p>
html: `<p>Sehr geehrte/r ${user.vorname} ${user.name},</p>
<p>Ihr Energieausweis wurde erfolgreich in Ihrem Konto gespeichert. Sie können ihn jederzeit in Ihrem Kundenbereich abrufen.<br><br>
Ihre Vorgänge und Ausweise können Sie in Ihrem Kundenkonto einsehen und bearbeiten:<br><br>

View File

@@ -13,6 +13,7 @@
try {
sent = true
const response = await api.auth["passwort-vergessen"].GET.fetch({
redirect,
email
})
@@ -57,7 +58,7 @@
{#if showEmailSuccess}
<div class="flex-row justify-between" style="margin-top: 10px">
<a class="link link-hover"
href="/auth/login{redirect ? `?redirect=${redirect}` : ""}">Einloggen</a
href="/auth/login{redirect ? `?r=${redirect}` : ""}">Einloggen</a
>
</div>
{/if}

View File

@@ -7,6 +7,7 @@
let passwortWiederholen: string;
export let token: string;
export let redirect: string | null = null;
let disabled = false;
@@ -28,7 +29,7 @@
})
setTimeout(() => {
window.location.href = "/auth/login"
window.location.href = "/auth/login?r=" + encodeURIComponent(redirect ?? "/dashboard")
}, 5000)
} catch (e) {
disabled = false

View File

@@ -1,8 +1,5 @@
<script lang="ts">
import {
Reader,
EnvelopeClosed,
Cube,
Person,
} from "radix-svelte-icons";
import { Tabs, Tab, TabList, TabPanel } from "../../components/Tabs/index.js";

View File

@@ -1,4 +1,5 @@
<script lang="ts">
import { addNotification } from "#components/Notifications/shared.js";
import { loginClient } from "#lib/login.js";
import EmbeddedLoginModule from "./EmbeddedLoginModule.svelte"
import EmbeddedRegisterModule from "./EmbeddedRegisterModule.svelte"
@@ -7,22 +8,31 @@
export let email: string = "";
export let password: string = "";
export let route: "login" | "signup" = "login"
export let title: Record<typeof route, string> = {
login: "Einloggen",
signup: "Registrieren"
}
export let buttonText: Record<typeof route, string> = {
login: "Einloggen",
signup: "Registrieren"
}
const navigate = (target: string) => {
route = target as typeof route;
}
const loginData = {
email,
passwort: "",
}
</script>
{#if route == "login"}
<EmbeddedLoginModule onLogin={onLogin} bind:email bind:password {navigate} />
{:else}
<EmbeddedRegisterModule bind:email bind:password onRegister={(response) => {
email = response.email
navigate("login")
}} {navigate} />
<EmbeddedLoginModule onLogin={onLogin} bind:email bind:password {navigate} title={title.login} buttonText={buttonText.login} />
{:else if route == "signup"}
<EmbeddedRegisterModule bind:email onRegister={(response) => {
addNotification({
message: "Registrierung erfolgreich",
subtext: "Ein Passwort wurde an ihre Email Adresse gesendet, sie werden nun automatisch weitergeleitet..",
type: "success",
timeout: 6000,
dismissable: true
})
onLogin(response)
}} {navigate} title={title.signup} buttonText={buttonText.signup} />
{/if}

View File

@@ -1,10 +1,11 @@
<script lang="ts">
import { addNotification } from "@ibcornelsen/ui";
import { loginClient } from "#lib/login.js";
export let navigate: (target: string) => void;
export let email: string;
export let password: string;
export let title: string = "Einloggen";
export let buttonText: string = "Einloggen";
export let onLogin: (response: Awaited<ReturnType<typeof loginClient>>) => any;
@@ -13,21 +14,19 @@
const response = await loginClient(email, password)
if (response === null) {
addNotification({
message: "Ups...",
subtext: "Das hat leider nicht geklappt, haben sie ihr Passwort und ihre Email Adresse richtig eingegeben?",
type: "error",
timeout: 6000,
dismissable: true
})
error = true;
errorMessage = "Das hat leider nicht geklappt, haben sie ihr Passwort und ihre Email Adresse richtig eingegeben?"
} else {
onLogin(response);
}
}
let error = false;
let errorMessage = "";
</script>
<form style="width:50%;margin: 0 auto" on:submit={login} name="login">
<h1 class="text-2xl font-semibold mb-6">Einloggen</h1>
<form class="max-w-md mx-auto" on:submit={login} name="login">
<h1 class="text-3xl mb-4 p-0">{title}</h1>
<div class="flex flex-col gap-4">
<div>
<h4>Email</h4>
@@ -37,6 +36,7 @@
placeholder="Email"
name="email"
bind:value={email}
on:focus={() => (error = false)}
required
/>
</div>
@@ -48,13 +48,19 @@
placeholder="********"
name="passwort"
bind:value={password}
on:focus={() => (error = false)}
required
/>
</div>
<button class="button" type="submit">Einloggen</button>
{#if error}
<div class="bg-red-200 p-4 rounded-lg w-full">
<p class="text-red-800">{errorMessage}</p>
</div>
{/if}
<button class="button" type="submit">{buttonText}</button>
<div class="flex flex-row justify-between" style="margin-top: 10px">
<a on:click={() => navigate("signup")} class="cursor-pointer" data-cy="registrieren">Registrieren</a>
<a href="/user/passwort_vergessen">Passwort Vergessen?</a>
<a href="/auth/passwort-vergessen?r={window.location.href}">Passwort Vergessen?</a>
</div>
</div>
</form>

View File

@@ -1,95 +1,79 @@
<script lang="ts">
import { addNotification } from "@ibcornelsen/ui";
import { loginClient } from "#lib/login.js";
import { api } from "astro-typesafe-api/client";
export let navigate: (target: string) => void;
export let onRegister: (response: { email: string, name: string, vorname: string }) => void;
export let password: string;
export let onRegister: (response: Awaited<ReturnType<typeof loginClient>>) => void;
export let email: string;
let vorname: string;
let name: string;
export let title: string = "Registrieren";
export let buttonText: string = "Registrieren";
let repeatEmail: string;
async function signUp(e: SubmitEvent) {
async function signup(e: SubmitEvent) {
e.preventDefault()
if (email !== repeatEmail) {
error = true;
errorMessage = "Die eingegebenen Email Adressen stimmen nicht überein.";
return;
}
try {
const response = await api.user.PUT.fetch({
const { id, passwort } = await api.user.autocreate.PUT.fetch({
email,
passwort: password,
vorname,
name,
});
onRegister({
email,
name,
vorname
})
const response = await loginClient(email, passwort)
onRegister(response);
} catch (e) {
addNotification({
message: "Ups...",
subtext:
"Da ist wohl etwas schiefgelaufen. Diese Email Adresse ist bereits in Benutzung, haben sie vielleicht bereits ein Konto bei uns?",
type: "error",
timeout: 0,
dismissable: true,
});
error = true;
errorMessage = "Sie besitzen bereits ein Konto bei IBC. Bitte loggen Sie sich mit Ihrem Passwort ein oder vergeben sich über “Passwort vergessen” ein neues."
navigate("login");
}
}
let error: boolean = false;
let errorMessage: string = "";
</script>
<form style="width:50%;margin: 0 auto" name="signup" on:submit={signUp}>
<h1>Registrieren</h1>
<form class="max-w-md mx-auto" name="signup" on:submit={signup}>
<h1 class="text-3xl mb-4 p-0">{title}</h1>
<p class="p-0 text-base">Ihre Ausweisdaten werden bei uns gespeichert und Ihnen wird ein vorläufiges Passwort erstellt. Sollte Ihre E-Mail bereits bei uns registriert sein, dann loggen Sie sich bitte mit Ihrem vorhandenen Passwort ein oder vergeben sich ein neues über “Passwort vergessen”.</p>
<hr>
<div class="flex flex-col gap-4">
<div class="flex flex-row gap-4 w-full">
<div class="w-1/2">
<h4>Vorname</h4>
<input
type="text"
placeholder="Vorname"
name="vorname"
class="px-2.5 py-1.5 rounded-lg border bg-gray-50"
bind:value={vorname}
required
/>
</div>
<div class="w-1/2">
<h4>Nachname</h4>
<input
type="text"
placeholder="Nachname"
name="nachname"
class="px-2.5 py-1.5 rounded-lg border bg-gray-50"
bind:value={name}
required
/>
</div>
</div>
<div>
<h4>Email</h4>
<input
type="email"
placeholder="Email"
placeholder="max.mustermann@email.de"
name="email"
class="px-2.5 py-1.5 rounded-lg border bg-gray-50"
bind:value={email}
on:keyup={() => (email = email.toLowerCase())}
required
/>
</div>
<div>
<h4>Passwort</h4>
<div class="flex flex-col gap-2">
<h4>Email erneut eingeben</h4>
<input
type="password"
placeholder="********"
name="passwort"
class="px-2.5 py-1.5 rounded-lg border bg-gray-50"
bind:value={password}
type="text"
placeholder="max.mustermann@email.de"
name="email"
class="input input-bordered text-base text-base-content font-medium"
bind:value={repeatEmail}
on:keyup={() => (repeatEmail = repeatEmail.toLowerCase())}
required
/>
</div>
<button class="button" type="submit">Registrieren</button>
<div class="flex-row justify-between" style="margin-top: 10px">
{#if error}
<div class="bg-red-200 p-4 rounded-lg w-full">
<p class="text-red-800">{errorMessage}</p>
</div>
{/if}
<button class="button" type="submit">{buttonText}</button>
<div class="flex flex-row justify-between" style="margin-top: 10px">
<button on:click={() => navigate("login")}>Einloggen</button>
<a href="/user/passwort_vergessen">Passwort Vergessen?</a>
<a href="/auth/passwort-vergessen?r={window.location.href}">Passwort Vergessen?</a>
</div>
</div>
</form>

View File

@@ -2,7 +2,7 @@
import PerformanceScore from "#components/Ausweis/PerformanceScore.svelte";
import Progressbar from "#components/Ausweis/Progressbar.svelte";
import Bereich from "#components/labels/Bereich.svelte";
import type { BedarfsausweisGewerbe, BedarfsausweisWohnen, Bezahlmethoden, GEGNachweisGewerbe, GEGNachweisWohnen, Unterlage, VerbrauchsausweisGewerbe, VerbrauchsausweisWohnen } from "#lib/client/prisma.js";
import type { BedarfsausweisGewerbe, BedarfsausweisWohnen, Benutzer, Bezahlmethoden, GEGNachweisGewerbe, GEGNachweisWohnen, Unterlage, VerbrauchsausweisGewerbe, VerbrauchsausweisWohnen } from "#lib/client/prisma.js";
import { Enums } from "#lib/client/prisma.js";
import {
API_ACCESS_TOKEN_COOKIE_NAME,
@@ -34,7 +34,6 @@
import { endEnergieVerbrauchVerbrauchsausweisGewerbe_2016_Client } from "#lib/Berechnungen/VerbrauchsausweisGewerbe/VerbrauchsausweisGewerbe_2016_Client.js";
import { benutzerSpeichern } from "#client/lib/speichern.js";
import { benutzerLesen } from "#client/lib/lesen.js";
import { exclude } from "#lib/exclude.js";
export let user: Partial<BenutzerClient>;
export let impersonatedUser: Partial<BenutzerClient> | null = null;
@@ -56,7 +55,7 @@
email = rechnung?.email || localStorage.getItem("kundendaten.email") || user.email || "";
vorname = localStorage.getItem("kundendaten.vorname") || user.vorname || "";
name = localStorage.getItem("kundendaten.name") || user.name || "";
empfaenger = rechnung?.empfaenger || localStorage.getItem("kundendaten.empfaenger") || (user.vorname && user.name ? `${user.vorname} ${user.name}` : "")
empfaenger = rechnung?.empfaenger || localStorage.getItem("kundendaten.empfaenger") || user.empfaenger
strasse = rechnung?.strasse || localStorage.getItem("kundendaten.strasse") || user.adresse || "";
plz = rechnung?.plz || localStorage.getItem("kundendaten.plz") || user.plz || "";
ort = rechnung?.ort || localStorage.getItem("kundendaten.ort") || user.ort || "";
@@ -171,6 +170,7 @@
async function anfordern() {
if (!form.checkValidity()) {
displayFormValidity()
addNotification({
dismissable: true,
message: "Fehlende Daten.",
@@ -345,8 +345,30 @@
}
}
async function bestellen(authuser = null) {
function displayFormValidity() {
(form.querySelectorAll("select[name][required], input[name][required]") as NodeListOf<HTMLInputElement | HTMLSelectElement>).forEach((element) => {
if (element.willValidate && !element.checkValidity()) {
element.dataset["isinvalid"] = "true"
const onChange = () => {
console.log(element, element.value, element.checkValidity());
if (!element.checkValidity()) {
return;
}
element.dataset["isinvalid"] = "false"
element.removeEventListener("change", onChange)
}
element.addEventListener("change", onChange)
}
})
}
async function bestellen(authuser: Benutzer | null = null) {
if (!form.checkValidity()) {
displayFormValidity()
addNotification({
dismissable: true,
message: "Fehlende Daten.",
@@ -418,6 +440,7 @@
const merged_versand_ort = versand_ort || ort;
const merged_versand_zusatzzeile = versand_zusatzzeile || zusatzzeile;
if (rechnung) {
const result = await api.rechnung._id.PATCH.fetch({
bezahlmethode: aktiveBezahlmethode,
@@ -504,6 +527,14 @@
let loginOverlayHidden = true;
let loginAction = () => {};
let form: HTMLFormElement;
let ortInput: HTMLInputElement;
$: {
if (ort && ortInput) {
ortInput.value = ort
ortInput.dispatchEvent(new Event("change"))
}
}
</script>
{#if !nurRechnungsadresseUpdate}
@@ -662,6 +693,9 @@ xl:grid-cols-3 xl:gap-x-8 xl:gap-y-8
name="rechnung_plz"
bind:zip={plz}
bind:city={ort}
onchange={(e) => {
ortInput.dispatchEvent(new Event('change'));
}}
/>
</div>
@@ -673,6 +707,7 @@ xl:grid-cols-3 xl:gap-x-8 xl:gap-y-8
type="text"
required
bind:value={ort}
bind:this={ortInput}
/>
<div class="help-label">
@@ -1213,14 +1248,24 @@ sm:grid-cols-[min-content_min-content_min-content] sm:justify-self-end sm:mt-8"
</div>
</div>
<Overlay bind:hidden={loginOverlayHidden}>
<div class="bg-white w-full max-w-screen-sm py-8">
<EmbeddedAuthFlowModule onLogin={loginAction} email={email} route="signup"></EmbeddedAuthFlowModule>
</div>
</Overlay>
</form>
<Overlay bind:hidden={loginOverlayHidden}>
<div class="bg-white w-full max-w-screen-sm py-8">
<EmbeddedAuthFlowModule onLogin={loginAction} email={email} route="signup" title={
{
login: "Ausweis bestellen",
signup: "Ausweis bestellen"
}
} buttonText={{
login: "Bestellen",
signup: "Bestellen"
}}></EmbeddedAuthFlowModule>
</div>
</Overlay>
<NotificationWrapper></NotificationWrapper>

View File

@@ -1,7 +1,5 @@
<script lang="ts">
import { loginClient } from "#lib/login.js";
import CrossCircled from "radix-svelte-icons/src/lib/icons/CrossCircled.svelte";
import { fade } from "svelte/transition";
let email: string;
let passwort: string;
@@ -13,7 +11,8 @@
const response = await loginClient(email, passwort);
if (response === null) {
errorHidden = false;
error = true
errorMessage = "Das hat leider nicht geklappt, haben sie ihr Passwort und ihre Email Adresse richtig eingegeben?"
} else {
if (redirect) {
window.location.href = redirect;
@@ -24,7 +23,8 @@
}
}
let errorHidden = true;
let error = false;
let errorMessage = "";
</script>
<div class="mx-auto w-1/3 bg-base-200 p-8 border border-base-300 rounded-lg">
@@ -38,7 +38,7 @@
placeholder="nutzer@email.com"
name="email"
bind:value={email}
on:focus={() => (errorHidden = true)}
on:focus={() => (error = false)}
required
/>
</div>
@@ -51,20 +51,19 @@
placeholder="********"
name="passwort"
bind:value={passwort}
on:focus={() => (errorHidden = true)}
on:focus={() => (error = false)}
required
/>
</div>
{#if !errorHidden}
<div role="alert" class="alert alert-error" in:fade out:fade={{delay: 400}}>
<CrossCircled size={24} />
<span class="font-semibold">Das hat leider nicht geklappt, haben sie ihr Passwort und ihre Email Adresse richtig eingegeben?</span>
</div>
{#if error}
<div class="bg-red-200 p-4 rounded-lg w-full">
<p class="text-red-800">{errorMessage}</p>
</div>
{/if}
<button class="button" type="submit">Einloggen</button>
<div class="flex-row justify-between" style="margin-top: 10px">
<a class="link link-hover" href="/auth/signup{redirect ? `?redirect=${redirect}` : ""}">Registrieren</a>
<a class="link link-hover" href="/auth/passwort-vergessen{redirect ? `?redirect=${redirect}` : ""}"
<div class="flex flex-row justify-between" style="margin-top: 10px">
<a class="link link-hover" href="/auth/signup{redirect ? `?r=${redirect}` : ""}">Registrieren</a>
<a class="link link-hover" href="/auth/passwort-vergessen{redirect ? `?r=${redirect}` : ""}"
>Passwort Vergessen?</a
>
</div>

View File

@@ -1,60 +1,58 @@
<script lang="ts">
import { addNotification } from "#components/Notifications/shared.js";
import { CrossCircled } from "radix-svelte-icons";
import { fade } from "svelte/transition";
import { api } from "astro-typesafe-api/client";
import NotificationProvider from "#components/NotificationProvider/NotificationProvider.svelte";
import NotificationWrapper from "#components/Notifications/NotificationWrapper.svelte";
import { loginClient } from "#lib/login.js";
import PlzSuche from "#components/PlzSuche.svelte";
let passwort: string;
let email: string;
let vorname: string;
let name: string;
let repeatEmail: string;
let adresse: string;
let plz: string;
let ort: string;
let empfaenger: string;
export let redirect: string | null = null;
function handleInput(event) {
email = event.target.value.toLowerCase();
}
async function login(e: SubmitEvent) {
e.preventDefault()
if (passwort.length < 8) {
addNotification({
message: "Passwort muss mindestens 6 Zeichen enthalten.",
dismissable: true,
timeout: 3000,
type: "error"
})
if (email !== repeatEmail) {
error = true;
errorMessage = "Die eingegebenen Email Adressen stimmen nicht überein.";
return;
}
try {
const { uid } = await api.user.PUT.fetch({
email,
passwort,
const { id, passwort } = await api.user.PUT.fetch({
vorname,
name
name, email,
adresse, plz, ort, empfaenger
})
await loginClient(email, passwort)
if (redirect) {
window.location.href = redirect
return
}
window.location.href = "/auth/login";
window.location.href = "/dashboard";
} catch (e) {
errorHidden = false;
error = true
errorMessage = "Sie besitzen bereits ein Konto bei IBC. Bitte loggen Sie sich mit Ihrem Passwort ein oder vergeben sich über “Passwort vergessen” ein neues."
}
}
let errorHidden = true;
let error = false;
let errorMessage = "";
</script>
<div class="mx-auto w-1/3 bg-base-200 p-8 border border-base-300 rounded-lg">
<h1 class="text-3xl mb-4">Registrieren</h1>
<form class="flex flex-col gap-4" on:submit={login}>
<p>Bitte geben sie Email, Ansprechpartner und Adressdaten ein, um einen Account beim IBC zu erstellen, ein Passwort wird ihnen per Email zugesendet, dieses können sie im Nachhinein jederzeit ändern.</p>
<form class="flex flex-col gap-4 mt-8" on:submit={login}>
<div class="flex flex-row gap-4 w-full">
<div class="w-1/2 flex flex-col gap-2">
<h4>Vorname</h4>
@@ -79,44 +77,82 @@
/>
</div>
</div>
<div class="flex flex-row gap-4 w-full">
<div class="w-1/2 flex flex-col gap-2">
<h4>Straße</h4>
<input
type="text"
placeholder="Straße"
name="strasse"
class="input input-bordered text-base text-base-content font-medium"
bind:value={adresse}
required
/>
</div>
<div class="w-1/2 flex flex-col gap-2">
<h4>PLZ</h4>
<PlzSuche bind:zip={plz} bind:city={ort} name="plz" readonly={false} />
</div>
<div class="w-1/2 flex flex-col gap-2">
<h4>Ort</h4>
<input
type="text"
placeholder="Ort"
name="ort"
class="input input-bordered text-base text-base-content font-medium"
bind:value={ort}
required
/>
</div>
</div>
<div class="flex flex-col gap-2">
<h4>Empfänger</h4>
<input
type="text"
placeholder="Max Mustermann"
name="empfaenger"
class="input input-bordered text-base text-base-content font-medium"
bind:value={empfaenger}
required
/>
</div>
<div class="flex flex-col gap-2">
<h4>Email</h4>
<input
type="text"
placeholder="Email"
placeholder="max.mustermann@email.de"
name="email"
class="input input-bordered text-base text-base-content font-medium"
bind:value={email}
on:input={handleInput}
on:keyup={() => (email = email.toLowerCase())}
required
/>
</div>
<div class="flex flex-col gap-2">
<h4>Passwort</h4>
<h4>Email erneut eingeben</h4>
<input
type="password"
placeholder="********"
minlength="8"
name="passwort"
type="text"
placeholder="max.mustermann@email.de"
name="email"
class="input input-bordered text-base text-base-content font-medium"
bind:value={passwort}
bind:value={repeatEmail}
on:keyup={() => (repeatEmail = repeatEmail.toLowerCase())}
required
/>
</div>
{#if !errorHidden}
<div role="alert" class="alert alert-error" in:fade out:fade={{delay: 400}}>
<CrossCircled size={24} />
<span class="font-semibold">Da ist wohl etwas schiefgelaufen. Diese Email Adresse ist bereits in Benutzung, haben sie vielleicht bereits ein Konto bei uns?</span>
</div>
{#if error}
<div class="bg-red-200 p-4 rounded-lg w-full">
<p class="text-red-800">{errorMessage}</p>
</div>
{/if}
<button type="submit" class="button"
>Registrieren</button
>
<div class="flex-row justify-between" style="margin-top: 10px">
>Registrieren</button>
<div class="flex flex-row justify-between" style="margin-top: 10px">
<a class="link link-hover"
href="/auth/login{redirect ? `?redirect=${redirect}` : ""}">Einloggen</a
href="/auth/login{redirect ? `?r=${redirect}` : ""}">Einloggen</a
>
<a class="link link-hover" href="/auth/passwort-vergessen{redirect ? `?redirect=${redirect}` : ""}">Passwort Vergessen?</a>
<a class="link link-hover" href="/auth/passwort-vergessen{redirect ? `?r=${redirect}` : ""}">Passwort Vergessen?</a>
</div>
</form>
<NotificationWrapper></NotificationWrapper>

View File

@@ -20,7 +20,7 @@ const user = await getCurrentUser(Astro)
if (id) {
if (!user) {
return Astro.redirect(
`/auth/login?redirect=${Astro.url.toString()}`
`/auth/login?r=${Astro.url.toString()}`
);
}
@@ -57,7 +57,7 @@ if (id) {
} else if (aufnahme_id) {
if (!user) {
return Astro.redirect(
`/auth/login?redirect=${Astro.url.toString()}`
`/auth/login?r=${Astro.url.toString()}`
);
}

View File

@@ -20,7 +20,7 @@ const user = await getCurrentUser(Astro)
if (id) {
if (!user) {
return Astro.redirect(
`/auth/login?redirect=${Astro.url.toString()}`
`/auth/login?r=${Astro.url.toString()}`
);
}
@@ -57,7 +57,7 @@ if (id) {
} else if (aufnahme_id) {
if (!user) {
return Astro.redirect(
`/auth/login?redirect=${Astro.url.toString()}`
`/auth/login?r=${Astro.url.toString()}`
);
}

View File

@@ -21,7 +21,7 @@ const user = await getCurrentUser(Astro)
if (id) {
if (!user) {
return Astro.redirect(
`/auth/login?redirect=${Astro.url.toString()}`
`/auth/login?r=${Astro.url.toString()}`
);
}
@@ -58,7 +58,7 @@ if (id) {
} else if (aufnahme_id) {
if (!user) {
return Astro.redirect(
`/auth/login?redirect=${Astro.url.toString()}`
`/auth/login?r=${Astro.url.toString()}`
);
}

View File

@@ -57,7 +57,7 @@ let loadFromDatabase = false;
if (typ === AusstellungsTyp.Neuausstellung) {
if (!user) {
return Astro.redirect(`/auth/login?redirect=${Astro.url.toString()}`);
return Astro.redirect(`/auth/login?r=${Astro.url.toString()}`);
}
if (!ausweis_id) {
@@ -115,7 +115,7 @@ if (typ === AusstellungsTyp.Neuausstellung) {
loadFromDatabase = true;
} else if (typ === AusstellungsTyp.Speichern) {
if (!user) {
return Astro.redirect(`/auth/login?redirect=${Astro.url.toString()}`);
return Astro.redirect(`/auth/login?r=${Astro.url.toString()}`);
}
if (!ausweis_id) {
@@ -151,7 +151,7 @@ if (typ === AusstellungsTyp.Neuausstellung) {
loadFromDatabase = true;
} else if (typ === AusstellungsTyp.Alternativdokument) {
if (!user) {
return Astro.redirect(`/auth/login?redirect=${Astro.url.toString()}`);
return Astro.redirect(`/auth/login?r=${Astro.url.toString()}`);
}
if (!ausweis_id) {

View File

@@ -20,7 +20,7 @@ const user = await getCurrentUser(Astro)
if (id) {
if (!user) {
return Astro.redirect(
`/auth/login?redirect=${Astro.url.toString()}`
`/auth/login?r=${Astro.url.toString()}`
);
}
@@ -58,7 +58,7 @@ if (id) {
} else if (aufnahme_id) {
if (!user) {
return Astro.redirect(
`/auth/login?redirect=${Astro.url.toString()}`
`/auth/login?r=${Astro.url.toString()}`
);
}

View File

@@ -20,7 +20,7 @@ const user = await getCurrentUser(Astro)
if (id) {
if (!user) {
return Astro.redirect(
`/auth/login?redirect=${Astro.url.toString()}`
`/auth/login?r=${Astro.url.toString()}`
);
}
@@ -58,7 +58,7 @@ if (id) {
} else if (aufnahme_id) {
if (!user) {
return Astro.redirect(
`/auth/login?redirect=${Astro.url.toString()}`
`/auth/login?r=${Astro.url.toString()}`
);
}

View File

@@ -21,7 +21,7 @@ const user = await getCurrentUser(Astro)
if (id) {
if (!user) {
return Astro.redirect(
`/auth/login?redirect=${Astro.url.toString()}`
`/auth/login?r=${Astro.url.toString()}`
);
}
@@ -59,7 +59,7 @@ if (id) {
} else if (aufnahme_id) {
if (!user) {
return Astro.redirect(
`/auth/login?redirect=${Astro.url.toString()}`
`/auth/login?r=${Astro.url.toString()}`
);
}

View File

@@ -294,7 +294,7 @@ export const GET = defineApiRoute({
if (rechnung.status === Enums.Rechnungsstatus.paid) {
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" : ""
@@ -323,7 +323,7 @@ export const GET = defineApiRoute({
</p>`;
} else {
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" : ""

View File

@@ -303,7 +303,7 @@ export const POST = defineApiRoute({
if (rechnung.status === Enums.Rechnungsstatus.paid) {
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" : ""
@@ -332,7 +332,7 @@ export const POST = defineApiRoute({
</p>`;
} else {
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" : ""

View File

@@ -120,7 +120,7 @@ export const PUT = defineApiRoute({
to: rechnung.email || user.email,
bcc: "info@online-energieausweis.org",
subject: `Stornierung des Energieausweises vom Ingenieurbüro Cornelsen (ID: ${ausweis.id})`,
html: `<p>Sehr geehrte*r ${user.vorname} ${user.name},</p>
html: `<p>Sehr geehrte/r ${user.vorname} ${user.name},</p>
<p>Ihr Energieausweis wurde soeben storniert.
<br>

View File

@@ -64,7 +64,7 @@ export const GET = defineApiRoute({
const { id } = ctx.params;
const aufnahme = await prisma.aufnahme.findUnique({
where: user.rolle === Enums.BenutzerRolle.USER ? {
where: user.rolle !== Enums.BenutzerRolle.ADMIN ? {
id,
benutzer_id: user.id
} : { id },

View File

@@ -63,7 +63,7 @@ export const GET = defineApiRoute({
const { id } = context.params;
const aufnahme = await prisma.aufnahme.findUnique({
where: user.rolle === Enums.BenutzerRolle.USER ? {
where: user.rolle !== Enums.BenutzerRolle.ADMIN ? {
id,
benutzer_id: user.id
} : { id },

View File

@@ -9,7 +9,8 @@ import { transport } from "#lib/mail.js";
export const GET = defineApiRoute({
input: z.object({
email: z.string().email()
email: z.string().email(),
redirect: z.string().optional()
}),
output: z.void(),
async fetch(input, context, transfer) {
@@ -42,7 +43,7 @@ export const GET = defineApiRoute({
sie haben eine Anfrage zum Zurücksetzen ihres Passworts gestellt. Klicken sie auf den folgenden Link, um ein neues Passwort festzulegen:
https://online-energieausweis.org/auth/passwort-zuruecksetzen?t=${resetToken}
https://online-energieausweis.org/auth/passwort-zuruecksetzen?t=${resetToken}${input.redirect ? `&r=${input.redirect}` : ""}
Dieser Link ist für die nächsten 15 Minuten gültig. Falls du diese Anfrage nicht gestellt hast, kannst du diese E-Mail ignorieren - dein Passwort bleibt unverändert.

View File

@@ -49,7 +49,7 @@ export const PATCH = defineApiRoute({
data: input
})
if (user.rolle === Enums.BenutzerRolle.USER) {
if (user.rolle !== Enums.BenutzerRolle.ADMIN) {
await sendAusweisGespeichertMail(user, ctx.params.id as string)
}
},

View File

@@ -66,7 +66,7 @@ export const PUT = defineApiRoute({
}
}
});
if (user.rolle === Enums.BenutzerRolle.USER) {
if (user.rolle !== Enums.BenutzerRolle.ADMIN) {
await sendAusweisGespeichertMail(user, id)
}
return nachweis.id

View File

@@ -48,7 +48,7 @@ export const PATCH = defineApiRoute({
},
data: input
})
if (user.rolle === Enums.BenutzerRolle.USER) {
if (user.rolle !== Enums.BenutzerRolle.ADMIN) {
await sendAusweisGespeichertMail(user, ctx.params.id as string)
}
},

View File

@@ -73,7 +73,7 @@ export const PUT = defineApiRoute({
},
},
});
if (user.rolle === Enums.BenutzerRolle.USER) {
if (user.rolle !== Enums.BenutzerRolle.ADMIN) {
await sendAusweisGespeichertMail(user, id)
}
return id;

View File

@@ -10,6 +10,7 @@ import { PutObjectCommand } from "@aws-sdk/client-s3";
import { s3Client } from "#lib/s3.js";
import { generateIDWithPrefix } from "#lib/db.js";
import { VALID_UUID_PREFIXES } from "#lib/constants.js";
import { logger } from "#lib/logger.js";
export const PUT = defineApiRoute({
input: BildSchema.pick({
@@ -23,7 +24,8 @@ export const PUT = defineApiRoute({
output: z.object({
id: z.string({ description: "Die id des Bildes." }),
}),
async fetch(input) {
middleware: maybeAuthorizationMiddleware,
async fetch(input, context, user) {
const data = input.data;
if (!isBase64(data, { mimeRequired: true })) {
@@ -43,6 +45,7 @@ export const PUT = defineApiRoute({
id,
kategorie: input.kategorie,
name: input.name,
benutzer_id: user ? user.id : null,
},
});
@@ -67,6 +70,7 @@ export const PUT = defineApiRoute({
id,
},
});
logger.error("Fehler beim Speichern des Bildes in S3: " + e);
// Und geben einen Fehler zurück
throw new APIError({
code: "INTERNAL_SERVER_ERROR",
@@ -91,11 +95,11 @@ export const DELETE = defineApiRoute({
await prisma.bild.delete({
where: {
id: input.id,
aufnahme: {
benutzer: {
id: user.id,
},
},
OR: [{
benutzer_id: user.id
}, {
benutzer_id: null
}]
},
});
} else {
@@ -107,6 +111,7 @@ export const DELETE = defineApiRoute({
});
}
} catch (e) {
logger.error("Fehler beim Löschen des Bildes: " + e);
throw new APIError({
code: "INTERNAL_SERVER_ERROR",
message: "Bild konnte nicht gelöscht werden.",

View File

@@ -48,7 +48,7 @@ export const PATCH = defineApiRoute({
},
data: input
})
if (user.rolle === Enums.BenutzerRolle.USER) {
if (user.rolle !== Enums.BenutzerRolle.ADMIN) {
await sendAusweisGespeichertMail(user, ctx.params.id as string)
}
},

View File

@@ -83,7 +83,7 @@ export const PUT = defineApiRoute({
},
},
});
if (user.rolle === Enums.BenutzerRolle.USER) {
if (user.rolle !== Enums.BenutzerRolle.ADMIN) {
await sendAusweisGespeichertMail(user, id)
}
return {

View File

@@ -48,7 +48,7 @@ export const PATCH = defineApiRoute({
},
data: input
})
if (user.rolle === Enums.BenutzerRolle.USER) {
if (user.rolle !== Enums.BenutzerRolle.ADMIN) {
await sendAusweisGespeichertMail(user, ctx.params.id as string)
}
},

View File

@@ -83,7 +83,7 @@ export const PUT = defineApiRoute({
},
},
});
if (user.rolle === Enums.BenutzerRolle.USER) {
if (user.rolle !== Enums.BenutzerRolle.ADMIN) {
await sendAusweisGespeichertMail(user, id)
}
return {

Some files were not shown because too many files have changed in this diff Show More