1 Commits

Author SHA1 Message Date
Moritz Utcke
dc0509cac2 Implementiert Nutzer Verifizierung
Fügt einen Mechanismus zur Nutzerverifizierung per E-Mail ein.

Nach der Registrierung wird eine E-Mail mit einem zeitbasierten Verifizierungscode versandt. Der Nutzer muss diesen Code eingeben, um sein Konto zu aktivieren.

Die Methode zur Erstellung des Codes ist zeitbasiert und ändert sich alle 15 Minuten.
2025-07-30 09:39:30 -05:00
91 changed files with 1932 additions and 1497 deletions

View File

@@ -0,0 +1,72 @@
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
outputs:
reason: ${{ steps.check.outputs.reason }}
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
COUNT=$(git rev-list --count origin/staging..origin/main)
if [ "$COUNT" -gt 0 ]; then
echo "reason=ok" >> $GITHUB_OUTPUT
echo "❌ Staging is behind main and requires manual merging."
exit 1
elif [ "$COUNT" -eq 0 ]; then
echo "reason=identical" >> $GITHUB_OUTPUT
echo "✅ Staging and main are identical. Nothing to do."
exit 42
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() && needs.merge.outputs.reason != 'identical'
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 }}

View File

@@ -28,4 +28,4 @@ jobs:
git clean -f -d
git pull origin dev
git status
make prod-no-backup
make prod

View File

@@ -13,15 +13,18 @@ jobs:
steps:
- name: Prevent dev merges
run: |
if [[ "${{ github.head_ref }}" == "dev" && "${{ github.base_ref }}" == "main" ]]; then
echo "ERROR: Merging 'dev' into 'main' is forbidden!"
echo "${{ github.head_ref }}";
echo "${{ github.base_ref }}";
if [[ "${{ github.head_ref }}" == "dev" ]]; then
echo "ERROR: Merging 'dev' into '${{ github.base_ref }}' is forbidden!"
exit 1
fi
- name: Allow only staging into main
if: github.base_ref == 'main'
run: |
echo "${{ github.head_ref }}";
echo "${{ github.base_ref }}";
if [[ "${{ github.head_ref }}" != "staging" ]]; then
echo "ERROR: Only 'staging' branch is allowed to merge into 'main'. Current: '${{ github.head_ref }}'"
exit 1

View File

@@ -30,9 +30,6 @@ 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) \
@@ -62,11 +59,9 @@ 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 start bun --name "update-dwd-klimafaktoren-cron" --no-autorestart --cron "0 12 28 * *" -- src/cronjobs/update-dwd-klimafaktoren.ts
prod: prod-no-backup backup-database-cronjob
prod-no-backup: install-dependencies prisma-studio update-dwd-klimafaktoren-cron
prod: install-dependencies prisma-studio backup-database-cronjob update-dwd-klimafaktoren-cron
bun run build
mkdir -p ~/logs
mkdir -p ~/persistent/online-energieausweis

View File

@@ -2,7 +2,6 @@
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
@@ -12,15 +11,19 @@ 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 $DATABASE_NAME pg_dump --data-only -U main main | brotli --quality=3 > $FILE_NAME
docker exec -t online-energieausweis-database-1 pg_dump --data-only -U main main | brotli --best > $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 $DATABASE_NAME pg_dumpall -c -U main | brotli --quality=3 > $FILE_NAME_COMPLETE
docker exec -t online-energieausweis-database-1 pg_dumpall -c -U main | brotli --best > $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

@@ -15,7 +15,6 @@
"@pdfme/common": "^5.2.16",
"@pdfme/generator": "^5.2.16",
"@pdfme/ui": "^5.2.16",
"@svelte-plugins/datepicker": "^1.0.11",
"@trpc/client": "^10.45.2",
"@trpc/server": "^10.45.2",
"astro": "^4.16.17",
@@ -27,8 +26,6 @@
"express": "^4.21.2",
"flag-icons": "^6.15.0",
"fontkit": "^2.0.4",
"handlebars": "^4.7.8",
"heic2any": "^0.0.4",
"highlight.run": "^9.14.0",
"is-base64": "^1.1.0",
"js-cookie": "^3.0.5",
@@ -37,11 +34,11 @@
"jwt-decode": "^4.0.0",
"mime": "^4.0.6",
"moment": "^2.30.1",
"moment-timezone": "^0.6.0",
"moment-timezone": "^0.5.46",
"nodemailer": "^6.10.0",
"pdf-lib": "^1.17.1",
"postcss-nested": "^7.0.2",
"puppeteer": "^24.15.0",
"puppeteer": "^24.7.2",
"radix-svelte-icons": "^1.0.0",
"sass": "^1.83.4",
"sharp": "^0.33.5",
@@ -526,7 +523,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.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=="],
"@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=="],
"@rc-component/async-validator": ["@rc-component/async-validator@5.0.4", "", { "dependencies": { "@babel/runtime": "^7.24.4" } }, "sha512-qgGdcVIF604M9EqjNF0hbUTz42bz/RDtxWdWuU5EQe3hi7M8ob54B6B35rOsvX5eSvIHIzT9iH1R3n+hk3CGfg=="],
@@ -714,8 +711,6 @@
"@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=="],
"@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=="],
"@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@1.0.4", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^2.2.0", "svelte": "^3.54.0 || ^4.0.0", "vite": "^4.0.0" } }, "sha512-zjiuZ3yydBtwpF3bj0kQNV0YXe+iKE545QGZVTaylW3eAzFr+pJ/cwK8lZEaRp4JtaJXhD5DyWAV4AxLh6DgaQ=="],
@@ -1062,7 +1057,7 @@
"chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="],
"chromium-bidi": ["chromium-bidi@7.2.0", "", { "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" }, "peerDependencies": { "devtools-protocol": "*" } }, "sha512-gREyhyBstermK+0RbcJLbFhcQctg92AGgDe/h/taMJEOLRdtSswBAO9KmvltFSQWgM2LrwWu5SIuEUbdm3JsyQ=="],
"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=="],
"ci-info": ["ci-info@4.1.0", "", {}, "sha512-HutrvTNsF48wnxkzERIXOe5/mlcfFcbfCmwcg6CJnizbSue78AbDt+1cgl26zwn61WFxhcPykPfZrbqjGmBb4A=="],
@@ -1246,7 +1241,7 @@
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
"devtools-protocol": ["devtools-protocol@0.0.1464554", "", {}, "sha512-CAoP3lYfwAGQTaAXYvA6JZR0fjGUb7qec1qf4mToyoH2TZgUFeIqYcjh6f9jNuhHfuZiEdH+PONHYrLhRQX6aw=="],
"devtools-protocol": ["devtools-protocol@0.0.1425554", "", {}, "sha512-uRfxR6Nlzdzt0ihVIkV+sLztKgs7rgquY/Mhcv1YNCWDh5IZgl5mnn2aeEnW5stYTE0wwiF4RYVz8eMEpV1SEw=="],
"dezalgo": ["dezalgo@1.0.4", "", { "dependencies": { "asap": "^2.0.0", "wrappy": "1" } }, "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig=="],
@@ -1520,8 +1515,6 @@
"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=="],
@@ -1556,8 +1549,6 @@
"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=="],
"heic2any": ["heic2any@0.0.4", "", {}, "sha512-3lLnZiDELfabVH87htnRolZ2iehX9zwpRyGNz22GKXIu0fznlblf0/ftppXKNqS26dqFSeqfIBhAmAj/uSp0cA=="],
"hexoid": ["hexoid@2.0.0", "", {}, "sha512-qlspKUK7IlSQv2o+5I7yhUd7TxlOG2Vr5LTa3ve2XSNVKAL/n/u/7KLvKmFNimomDIKvZFXWHv0T12mv7rT8Aw=="],
"highlight.run": ["highlight.run@9.14.0", "", {}, "sha512-ZR+ZLHlVU8lXqsuto0ZEMAOuvptaTBBf1jradnKDIn9OfAXupcYFbkASDlbsZtyBh2SYJSK50xwrucXujhksRg=="],
@@ -1968,7 +1959,7 @@
"moment": ["moment@2.30.1", "", {}, "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how=="],
"moment-timezone": ["moment-timezone@0.6.0", "", { "dependencies": { "moment": "^2.29.4" } }, "sha512-ldA5lRNm3iJCWZcBCab4pnNL3HSZYXVb/3TYr75/1WCTWYuTqYUb5f/S384pncYjJ88lbO8Z4uPDvmoluHJc8Q=="],
"moment-timezone": ["moment-timezone@0.5.47", "", { "dependencies": { "moment": "^2.29.4" } }, "sha512-UbNt/JAWS0m/NJOebR0QMRHBk0hu03r5dx9GK8Cs0AS3I81yDcOc9k+DytPItgVvBP7J6Mf6U2n3BPAacAV9oA=="],
"mrmime": ["mrmime@2.0.0", "", {}, "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw=="],
@@ -1986,8 +1977,6 @@
"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=="],
@@ -2198,9 +2187,9 @@
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"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": ["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-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=="],
"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=="],
"qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="],
@@ -2574,7 +2563,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.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-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-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=="],
@@ -2674,8 +2663,6 @@
"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=="],
@@ -2774,15 +2761,13 @@
"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.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
"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=="],
"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=="],
@@ -2882,9 +2867,7 @@
"@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/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=="],
"@puppeteer/browsers/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
@@ -2982,8 +2965,6 @@
"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=="],
@@ -3082,8 +3063,6 @@
"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=="],

View File

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

View File

@@ -29,7 +29,6 @@
"@pdfme/common": "^5.2.16",
"@pdfme/generator": "^5.2.16",
"@pdfme/ui": "^5.2.16",
"@svelte-plugins/datepicker": "^1.0.11",
"@trpc/client": "^10.45.2",
"@trpc/server": "^10.45.2",
"astro": "^4.16.17",
@@ -41,8 +40,6 @@
"express": "^4.21.2",
"flag-icons": "^6.15.0",
"fontkit": "^2.0.4",
"handlebars": "^4.7.8",
"heic2any": "^0.0.4",
"highlight.run": "^9.14.0",
"is-base64": "^1.1.0",
"js-cookie": "^3.0.5",
@@ -51,11 +48,11 @@
"jwt-decode": "^4.0.0",
"mime": "^4.0.6",
"moment": "^2.30.1",
"moment-timezone": "^0.6.0",
"moment-timezone": "^0.5.46",
"nodemailer": "^6.10.0",
"pdf-lib": "^1.17.1",
"postcss-nested": "^7.0.2",
"puppeteer": "^24.15.0",
"puppeteer": "^24.7.2",
"radix-svelte-icons": "^1.0.0",
"sass": "^1.83.4",
"sharp": "^0.33.5",

View File

@@ -1,49 +0,0 @@
/*
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

@@ -1,15 +0,0 @@
/*
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

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

View File

@@ -1,12 +0,0 @@
/*
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

@@ -1,5 +0,0 @@
-- 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

@@ -1,10 +0,0 @@
/*
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

@@ -2,7 +2,6 @@
enum BenutzerRolle {
USER
ADMIN
RESELLER
}
model Benutzer {
@@ -21,7 +20,6 @@ model Benutzer {
rolle BenutzerRolle @default(USER)
firma String?
lex_office_id String?
partner_code String?
verified Boolean @default(false)
@@ -52,8 +50,6 @@ model Benutzer {
events Event[]
@@map("benutzer")
Provisionen Provisionen[]
bilder Bild[]
}

View File

@@ -15,8 +15,6 @@ 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

@@ -1,11 +0,0 @@
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

@@ -3,43 +3,17 @@
# === Configuration ===
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
LOCAL_DOWNLOAD_DIR="./"
# === 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 file exists locally
if [ ! -f "$CUSTOM_FILE" ]; then
# 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")
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
# === Use filename from argument if provided ===
if [ -n "$1" ]; then
LATEST_FILE="$1"
else
echo "🔍 No custom file provided, searching for latest .sql.br file in S3"
echo "📡 No filename provided, fetching latest..."
# === Get latest file from IONOS S3 bucket ===
LATEST_FILE=$(aws s3api list-objects-v2 \
LATEST_FILE=$(aws --profile ionos s3api list-objects-v2 \
--bucket "$BUCKET_NAME" \
--prefix "data-dump" \
--prefix "full-dump" \
--endpoint-url "$ENDPOINT_URL" \
--query 'Contents | sort_by(@, &LastModified) | [-1].Key' \
--output text)
@@ -49,30 +23,25 @@ else
echo "❌ No matching .sql.br file found."
exit 1
fi
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
FILENAME=$(basename "$LATEST_FILE")
SQL_FILE="${FILENAME%.br}" # Remove .br suffix
echo "📥 Downloading $LATEST_FILE"
aws --profile ionos s3 cp "s3://$BUCKET_NAME/$LATEST_FILE" "$LOCAL_DOWNLOAD_DIR" \
--endpoint-url "$ENDPOINT_URL"
# === Decompress with Brotli ===
echo "🗜️ Decompressing $FILENAME -> $SQL_FILE"
brotli -d "$FILENAME"
# === Import into Postgres inside Docker ===
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 "🐘 Importing into PostgreSQL (database:main)"
docker exec -i "database" env PGPASSWORD="hHMP8cd^N3SnzGRR" \
psql -U "main" -d "main" < "$SQL_FILE"
echo "✅ Import complete."
# === Optional: Clean up
# If custom file was provided, do not delete it
if [ -z "$CUSTOM_FILE" ]; then
echo "🧹 Cleaning up downloaded files..."
rm "$FILENAME" "$SQL_FILE"
fi
rm "$FILENAME" "$SQL_FILE"

View File

@@ -5,12 +5,7 @@ export const createCaller = createCallerFactory({
"klimafaktoren": await import("../src/pages/api/klimafaktoren.ts"),
"postleitzahlen": await import("../src/pages/api/postleitzahlen.ts"),
"unterlage": await import("../src/pages/api/unterlage.ts"),
"ausweise": await import("../src/pages/api/ausweise/index.ts"),
"bedarfsausweis-gewerbe/[id]": await import("../src/pages/api/bedarfsausweis-gewerbe/[id].ts"),
"bedarfsausweis-gewerbe": await import("../src/pages/api/bedarfsausweis-gewerbe/index.ts"),
"aufnahme": await import("../src/pages/api/aufnahme/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"),
"admin/ausstellen": await import("../src/pages/api/admin/ausstellen.ts"),
"admin/bedarfsausweis-ausstellen": await import("../src/pages/api/admin/bedarfsausweis-ausstellen.ts"),
"admin/bestellbestaetigung": await import("../src/pages/api/admin/bestellbestaetigung.ts"),
@@ -18,29 +13,35 @@ 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"),
"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"),
"objekt": await import("../src/pages/api/objekt/index.ts"),
"ausweise": await import("../src/pages/api/ausweise/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"),
"auth/verification-code": await import("../src/pages/api/auth/verification-code.ts"),
"aufnahme": await import("../src/pages/api/aufnahme/index.ts"),
"bedarfsausweis-gewerbe/[id]": await import("../src/pages/api/bedarfsausweis-gewerbe/[id].ts"),
"bedarfsausweis-gewerbe": await import("../src/pages/api/bedarfsausweis-gewerbe/index.ts"),
"ausweise": await import("../src/pages/api/ausweise/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"),
"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"),
"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"),
"user": await import("../src/pages/api/user/index.ts"),
"user/self": await import("../src/pages/api/user/self.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"),
"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"),
"webhooks/mollie": await import("../src/pages/api/webhooks/mollie.ts"),
"objekt/[id]": await import("../src/pages/api/objekt/[id]/index.ts"),
"aufnahme/[id]/bilder": await import("../src/pages/api/aufnahme/[id]/bilder.ts"),
"aufnahme/[id]": await import("../src/pages/api/aufnahme/[id]/index.ts"),
"aufnahme/[id]/unterlagen": await import("../src/pages/api/aufnahme/[id]/unterlagen.ts"),
"objekt/[id]": await import("../src/pages/api/objekt/[id]/index.ts"),
})

View File

@@ -0,0 +1,49 @@
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

@@ -0,0 +1,67 @@
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

@@ -1,133 +0,0 @@
<script lang="ts">
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: 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) {
const monat = moment(bestellung.created_at).format("Y-MM");
if (monat in bestellungenNachMonat) {
bestellungenNachMonat[monat].push(bestellung)
} else {
bestellungenNachMonat[monat] = [bestellung]
}
}
// Wir brauchen alle Monate zwischen dem ersten Mal, dass der partner_code benutzt wurde bis zum heutigen Zeitpunkt.
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();
while (current.isBefore(end)) {
monthsArray.push(current.clone());
current.add(1, 'month');
}
return monthsArray.reverse(); // Most recent month first
}
const periods = getMonthlyPeriods(startdatum, enddatum)
let isOpen = false;
$: formatiertesStartDatum = moment(startdatum).format("DD.MM.YYYY");
$: formatiertesEndDatum = moment(enddatum).format("DD.MM.YYYY");
function toggleDatePicker() {
isOpen = !isOpen;
}
const onChange = ({ startDate, endDate }: { startDate: number, endDate: number }) => {
window.location.href = `/dashboard/abrechnung?start=${moment(startDate).format("YYYY-MM-DD")}&end=${moment(endDate).format("YYYY-MM-DD")}`;
};
</script>
<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={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 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}
{#each periods as dt}
{@const jahrMonat = dt.format("Y-MM")}
{#if jahrMonat in bestellungenNachMonat && bestellungenNachMonat[jahrMonat].length > 0}
<!-- Echo dropdown foreach month. -->
{@const provisionMonat = bestellungenNachMonat[jahrMonat].reduce((acc, bestellung) => {
const { provision_prozent, provision_betrag } = getProvision(bestellung.ausweis.ausweisart, bestellung.ausweis.ausweistyp, provisionen);
return acc + provision_betrag;
}, 0)}
<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

@@ -0,0 +1,108 @@
<script lang="ts">
import { BedarfsausweisWohnen, Enums, Rechnung, VerbrauchsausweisGewerbe, VerbrauchsausweisWohnen } from "#lib/server/prisma.js";
import moment from "moment";
export let bestellungen: (Rechnung & {
verbrauchsausweis_wohnen: VerbrauchsausweisWohnen | null,
verbrauchsausweis_gewerbe: VerbrauchsausweisGewerbe | null,
bedarfsausweis_wohnen: BedarfsausweisWohnen | null,
})[];
export let provisionen: Record<Enums.Ausweisart, number>;
export let partnerCodeErstesMal: Date;
const bestellungenNachMonat: Record<string, (typeof bestellungen)> = {};
for (const bestellung of bestellungen) {
const monat = moment(bestellung.created_at).format("Y-m");
if (monat in bestellungenNachMonat) {
bestellungenNachMonat[monat].push(bestellung)
} else {
bestellungenNachMonat[monat] = [bestellung]
}
}
// 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');
const monthsArray: moment.Moment[] = [];
const current = start.clone();
while (current.isBefore(end)) {
monthsArray.push(current.clone());
current.add(1, 'month');
}
return monthsArray.reverse(); // Most recent month first
}
const periods = getMonthlyPeriods(partnerCodeErstesMal)
</script>
{#each periods as dt}
{@const jahrMonat = dt.format("Y-m")}
{#if jahrMonat in bestellungenNachMonat && bestellungenNachMonat[jahrMonat].length > 0}
<!-- Echo dropdown foreach month. -->
{@const provisionMonat = bestellungenNachMonat[jahrMonat].reduce((acc, bestellung) => {
if (bestellung.verbrauchsausweis_wohnen) {
return acc + provisionen[Enums.Ausweisart.VerbrauchsausweisWohnen];
}
if (bestellung.bedarfsausweis_wohnen) {
return acc + provisionen[Enums.Ausweisart.BedarfsausweisWohnen];
}
if (bestellung.verbrauchsausweis_gewerbe) {
return acc + provisionen[Enums.Ausweisart.VerbrauchsausweisGewerbe];
}
return acc;
}) * 1.19}
<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 id='QTT' style='margin-top: 0 !important; display:none;'>
<thead>
<tr>
<td style='text-align:center;'>ID</td>
<td style='text-align:center;'>DATUM</td>
<td style='width:11em;text-align:center;'>GEBÄUDEADRESSE </td>
<td style='width:11em;text-align:center;'>PLZ </td>
<td style='width:11em;text-align:center;'>ORT </td>
<td style='text-align:center;'>AUSWEIS</td>
<td style='width:5em;text-align:center;'>BETRAG NETTO</td>
</tr>
</thead>
<tbody>
{#each bestellungenNachMonat[jahrMonat] as bestellung}
{@const provisionBestellung = bestellung.verbrauchsausweis_wohnen ? provisionen[Enums.Ausweisart.VerbrauchsausweisWohnen] : bestellung.verbrauchsausweis_gewerbe ? provisionen[Enums.Ausweisart.VerbrauchsausweisGewerbe] : provisionen[Enums.Ausweisart.BedarfsausweisWohnen]}
<tr>
<td style='width:1em;text-align:center;'>{bestellung.id}</td>
<td style='width:9em;text-align:center;font-weight:bold;'>{moment(bestellung.created_at).format("Y/m/d")}</td>
<td style='width:8em;text-align:left;'>{bestellung["objekt_strasse"]}</td>
<td style='width:5em;text-align:center;'>{bestellung["objekt_plz"]}</td>
<td style='width:6em;text-align:left;'>{bestellung["objekt_ort"]}</td>
<td style='width:3em;text-align:center;'>{bestellung['ausweisart']}</td>
<td style='width:8em;text-align:right;'>{provisionBestellung}</td>
</tr>
{/each}
</table>
{/if}
{/each}
<!-- foreach ($period as $dt) {
$year_month = $dt->format("Y-m");
$month_name = $months[$dt->format("m")];
if ((new DateTime(date("m/d/Y", strtotime($EEtimestamp))))->format("d") - (new DateTime(date("m/d/Y", strtotime($SStimestamp))))->format("d") == 1) {
$Pall = $dt->format("d/m/Y") . ' bis ' . (new DateTime($today))->format("d/m/Y");
} -->
<!-- } -->

View File

@@ -214,8 +214,8 @@ grid-cols-1 gap-x-2 gap-y-4
/>
<div class="text-center xs:text-left justify-self-stretch">
<b>Selbsteingabe online</b><br>inkl. ausführlicher&nbsp;telefonischer
Beratung!
<b>Verbrauchsausweis online</b><br>inkl. ausführlicher&nbsp;telefonischer
Beratung
</div>
<div class="text-center xs:text-right">
@@ -234,7 +234,7 @@ grid-cols-1 gap-x-2 gap-y-4
/>
<div class="text-center xs:text-left justify-self-stretch">
<b>Wir übernehmen die Eingabe</b><br>Sie übermitteln die nötigen Unterlagen per Upload oder E-Mail.
<b>Verbrauchsausweis offline</b><br>Sie schicken uns 3&nbsp;Verbrauchsabrechnungen&nbsp;zu)
</div>
<div class="text-center xs:text-right">

View File

@@ -11,22 +11,6 @@
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();
}
}
</script>
<div
@@ -113,11 +97,10 @@ xl:grid-cols-3 xl:gap-x-8 xl:gap-y-8
data-test="flaeche"
maxlength="4"
type="number"
step="1"
required
autocomplete="off"
on:keypress={onlyAllowIntegerInput}
on:paste={onlyAllowPastingIntegers}
data-rule-minlength="2"
data-msg-minlength="min. 2 Zeichen"
bind:value={aufnahme.flaeche}
/>
@@ -141,10 +124,7 @@ 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

@@ -60,8 +60,6 @@
return centeredValue;
}
let translation_1 = 0;
let translation_2 = 0;
$: {
@@ -76,7 +74,6 @@
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, 1.8, 0.56],
["Strommix", "kWh", 1.0, 2.4, 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,9 +42,6 @@ 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

@@ -5,7 +5,7 @@
CaretDown,
MagnifyingGlass,
} from "radix-svelte-icons";
import { Benutzer, Enums } from "#lib/client/prisma.js";
import { Benutzer } from "#lib/server/prisma.js";
export let lightTheme: boolean;
export let benutzer: Benutzer;
@@ -38,12 +38,6 @@
{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>
@@ -76,9 +70,7 @@
</div>
</div>
{/if}
{#if benutzer.rolle === Enums.BenutzerRolle.RESELLER}
<a href="/dashboard/abrechnung" class="button ">Monatliche Abrechnung</a>
{/if}
<a href="/dashboard/abrechnung" class="button ">Conversions</a>
</div>
<hr class="border-gray-600" />

View File

@@ -2,7 +2,6 @@
export let hidden: boolean = true;
export let closeable: boolean = true;
import { disableBodyScroll, enableBodyScroll } from 'body-scroll-lock';
import { Cross1 } from 'radix-svelte-icons';
$: if (globalThis.window) {
if (hidden) {
@@ -13,11 +12,13 @@
}
</script>
<div class="fixed top-0 left-0 w-[100vw] h-[100vh] flex items-center justify-center bg-[rgba(0,0,0,0.8)] z-50" class:hidden={hidden}>
<div class="fixed top-0 left-0 w-[100vw] h-[100vh] flex items-center justify-center bg-[rgba(0,0,0,0.8)] z-50" class:hidden={hidden} on:click|self={() => {
hidden = closeable ? true : hidden;
}}>
{#if closeable}
<button class="absolute top-4 right-4 text-white bg-gray-50 bg-opacity-25 px-4 py-2 rounded-lg" type="button" on:click={() => {
<button class="absolute top-4 left-4 text-white" on:click={() => {
hidden = true;
}}><Cross1 size={20}></Cross1></button>
}}>Schließen</button>
{/if}
<slot></slot>
</div>

View File

@@ -2,10 +2,9 @@
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, { email: userEmail });
const success = await dialogs.modal(TicketPopup);
console.log(success);

View File

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

View File

@@ -1,9 +1,7 @@
<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;
@@ -19,7 +17,6 @@
} 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 ausweis:
@@ -39,10 +36,8 @@
for (let i = 0; i < files.length; i++) {
const file = files[i];
console.log(file);
if (file.type !== "image/jpeg" && file.type !== "image/png" && file.type !== "image/webp" && file.type !== "image/heif" && file.type !== "image/heic") {
if (file.type !== "image/jpeg" && file.type !== "image/png") {
continue;
}
@@ -52,7 +47,7 @@
const reader = new FileReader();
reader.onload = async () => {
reader.onload = () => {
if (reader.readyState != reader.DONE) {
return;
}
@@ -62,18 +57,6 @@
}
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 () => {
@@ -102,12 +85,6 @@
data: dataURL,
kategorie,
name: file.name
}, {
headers: {
"Authorization": `Bearer ${Cookies.get(
API_ACCESS_TOKEN_COOKIE_NAME
)}`
}
}))
if (error) {
@@ -151,7 +128,6 @@
<div class="input-standard">
<input
type="file"
accept="image/*"
class="file-input file-input-ghost h-[38px]"
bind:this={fileUpload}
{name}
@@ -168,7 +144,6 @@
<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 as Date, objekt.plz as string);
const response = await getKlimafaktoren(ausweis.startdatum, objekt.plz);
// Alle Klimfaktoren konnten abgefragt werden.
const eevva = await endEnergieVerbrauchVerbrauchsausweis_2016(ausweis, aufnahme, objekt);
if (eevva){

View File

@@ -39,38 +39,63 @@ const isNET = pathname.includes("immonet");
</div>
<div>
<!-- Navigation als Liste (nur ab sm sichtbar) -->
<ul class="navlist hidden xl:flex">
<li><a href={`/${partner}/energieausweis-erstellen/verbrauchsausweis-wohngebaeude/`}><button class={tab === 0 ? "glow" : ""}>Verbrauchsausweis</button></a></li>
<li><a href={`/${partner}/energieausweis-erstellen/verbrauchsausweis-gewerbe/`}><button class={tab === 1 ? "glow" : ""}>Verbrauchsausweis Gewerbe</button></a></li>
<li><a href={`/${partner}/energieausweis-erstellen/bedarfsausweis-wohngebaeude/`}><button class={tab === 2 ? "glow" : ""}>Bedarfsausweis</button></a></li>
<li><a href={`/${partner}/angebot-anfragen/bedarfsausweis-gewerbe-anfragen/`}><button class={tab === 3 ? "glow" : ""}>Bedarfsausweis Gewerbe</button></a></li>
<li><a href={`/${partner}/angebot-anfragen/geg-nachweis-wohnen-anfragen/`}><button class={tab === 4 ? "glow" : ""}>GEG Nachweis Wohngebäude</button></a></li>
<li><a href={`/${partner}/angebot-anfragen/geg-nachweis-gewerbe-anfragen/`}><button class={tab === 5 ? "glow" : ""}>GEG Nachweis Gewerbe</button></a></li>
<li><a href={`/${partner}/welcher-ausweis/${partner}`}><button class={tab === 6 ? "glow" : ""}>Welcher Ausweis</button></a></li>
</ul>
<!-- Navigation als Dropdown (nur bis sm sichtbar) -->
<select
class="xl:hidden border rounded p-2 w-[calc(100%-2rem)]"
onchange="if (this.value) window.location.href=this.value"
>
<option value="">Auswahl treffen…</option>
<option value={`/${partner}/energieausweis-erstellen/verbrauchsausweis-wohngebaeude/`} selected={tab === 0}>Verbrauchsausweis</option>
<option value={`/${partner}/energieausweis-erstellen/verbrauchsausweis-gewerbe/`} selected={tab === 1}>Verbrauchsausweis Gewerbe</option>
<option value={`/${partner}/energieausweis-erstellen/bedarfsausweis-wohngebaeude/`} selected={tab === 2}>Bedarfsausweis</option>
<option value={`/${partner}/angebot-anfragen/bedarfsausweis-gewerbe-anfragen/`} selected={tab === 3}>Bedarfsausweis Gewerbe</option>
<option value={`/${partner}/angebot-anfragen/geg-nachweis-wohnen-anfragen/`} selected={tab === 4}>GEG Nachweis Wohngebäude</option>
<option value={`/${partner}/angebot-anfragen/geg-nachweis-gewerbe-anfragen/`} selected={tab === 5}>GEG Nachweis Gewerbe</option>
<option value={`/${partner}/welcher-ausweis/${partner}`} selected={tab === 6}>Welcher Ausweis</option>
</select>
<ul class="navlist">
<li>
<a
href={`/${partner}/energieausweis-erstellen/verbrauchsausweis-wohngebaeude/`}
><button class={tab === 0 ? "glow" : ""}
>Verbrauchsausweis</button
></a>
</li>
<li>
<a
href={`/${partner}/energieausweis-erstellen/verbrauchsausweis-gewerbe/`}
><button class={tab === 1 ? "glow" : ""}
>Verbrauchsausweis Gewerbe</button
></a>
</li>
<li>
<a
href={`/${partner}/energieausweis-erstellen/bedarfsausweis-wohngebaeude/`}
><button class={tab === 2 ? "glow" : ""}
>Bedarfsausweis</button
></a>
</li>
<li>
<a
href={`/${partner}/angebot-anfragen/bedarfsausweis-gewerbe-anfragen/`}
><button class={tab === 3 ? "glow" : ""}
>Bedarfsausweis Gewerbe</button
></a>
</li>
<li>
<a
href={`/${partner}/angebot-anfragen/geg-nachweis-wohnen-anfragen/`}
><button class={tab === 4 ? "glow" : ""}
>GEG Nachweis Wohngebäude</button
></a>
</li>
<li>
<a
href={`/${partner}/angebot-anfragen/geg-nachweis-gewerbe-anfragen/`}
><button class={tab === 5 ? "glow" : ""}
>GEG Nachweis Gewerbe</button
></a>
</li>
<li>
<a href={`/${partner}/welcher-ausweis/${partner}`}
><button class={tab === 6 ? "glow" : ""}
>Welcher Ausweis</button
></a>
</li>
</ul>
</div>
</div>
</header>
<div
id="titel"
class="block w-full lg:h-[270px] bg-cover !px-8 md:px-32 !py-8 md:py-16"
class="block w-full 2xl:h-[270px] lg:h-[148px] bg-cover px-24 py-20"
style={`background-image: url('/images/partner/${partner}/hero-energieausweis.jpg');
background-repeat:no-repeat; background-position:right;`}
>
@@ -161,7 +186,7 @@ background-repeat:no-repeat; background-position:right;`}
font-family: "immo Sans";
font-weight:400;
div{@apply w-fit bg-white/75 py-6 px-4 md:px-16 rounded-lg ring-2 ring-black/15 text-[1.45rem];box-shadow:8px 8px 16px rgba(0,0,0,0.5);}
div{@apply w-fit bg-white/75 py-6 px-16 rounded-lg ring-2 ring-black/15 text-[1.45rem];box-shadow:8px 8px 16px rgba(0,0,0,0.5);}
}
.header-button {

View File

@@ -1,9 +1,7 @@
<script lang="ts">
import { fade } from "svelte/transition";
import WidgetCardTemplate from "#components/widgets/immowelt/WidgetCardTemplate.svelte";
import { PRICES } from "#lib/constants.js";
import { Enums } from "#lib/client/prisma";
import { Enums } from "#lib/client/prisma.js";
let ausnahme: boolean = false;
let oneBOX: boolean = false;
@@ -17,56 +15,54 @@
let heizungsAlter: string = "bitte auswählen";
let leerStand: string = "bitte auswählen";
const partner:string = "immowelt";
const partner: string = "immowelt";
const twoBoxReason = ["Vermietung/Verkauf", "Aushangpflicht", "Sonstiges"];
const gewerbeHouse = ["Gewerbegebäude", "Mischgebäude"];
$: ausnahme =
$: ausnahme =
leerStand === "mehr als 30" ||
heizungsAlter === "< 3" ||
(baujahr === "vor 1978" && einheiten === "bis 4 Wohneinheiten" && sanierungsstatus === "unsaniert");
(baujahr === "vor 1978" &&
einheiten === "bis 4 Wohneinheiten" &&
sanierungsstatus === "unsaniert");
$: isTwoBoxReason = twoBoxReason.includes(anlass);
$: isTwoBoxReason = twoBoxReason.includes(anlass);
$: isGewerbe = gewerbeHouse.includes(gebaeudetyp);
$: isGewerbe = gewerbeHouse.includes(gebaeudetyp);
$: oneBOX =
$: oneBOX =
(ausnahme && !isGewerbe) ||
(!isTwoBoxReason && gebaeudetyp !== "Mischgebäude") ||
(gebaeudetyp === "Gewerbegebäude" && leerStand === "mehr als 30");
$: threeBOX =
(ausnahme && gebaeudetyp === "Mischgebäude" && isTwoBoxReason && leerStand !== "mehr als 30");
$: standardXL =
(einheiten === "mehr als 4 Wohneinheiten")
$: threeBOX =
ausnahme &&
gebaeudetyp === "Mischgebäude" &&
isTwoBoxReason &&
leerStand !== "mehr als 30";
$: standardXL = einheiten === "mehr als 4 Wohneinheiten";
</script>
<div id="IBC_app">
<input id="recode" type="hidden" value="widgetvorlage" />
<div id="OEA_input">
<div id="firstrow" class="firstrow"
<div
id="firstrow"
class="firstrow"
class:sm:grid-cols-3={isTwoBoxReason}
class:sm:grid-cols-2={!isTwoBoxReason}>
class:sm:grid-cols-2={!isTwoBoxReason}
>
<div class="auswahl">
<div class="titel">Anlass</div>
<select
id="anlass"
class="selectfeld"
bind:value={anlass}>
<select id="anlass" class="selectfeld" bind:value={anlass}>
>
<option selected disabled>bitte auswählen</option>
<option value="Vermietung/Verkauf">Vermietung/Verkauf</option>
<option value="Vermietung/Verkauf"
>Vermietung/Verkauf</option
>
<option value="Modernisierung">Modernisierung</option>
<option value="Neubau">Neubau</option>
<option value="Erweiterung">Erweiterung</option>
@@ -77,9 +73,7 @@ $: standardXL =
<div class="auswahl">
<div class="titel">Gebäudetyp</div>
<select
class="selectfeld"
bind:value={gebaeudetyp}>
<select class="selectfeld" bind:value={gebaeudetyp}>
>
<option selected disabled>bitte auswählen</option>
<option value="Einfamilienhaus">Einfamilienhaus</option>
@@ -93,10 +87,7 @@ $: standardXL =
{#if isTwoBoxReason}
<div class="auswahl">
<div class="titel">Sanierungsstand</div>
<select
class="selectfeld"
bind:value={sanierungsstatus}>
<select class="selectfeld" bind:value={sanierungsstatus}>
>
<option selected disabled>bitte auswählen</option>
<option value="saniert">saniert</option>
@@ -104,7 +95,6 @@ $: standardXL =
</select>
</div>
{/if}
</div>
{#if isTwoBoxReason}
@@ -115,7 +105,6 @@ $: standardXL =
id="baujahr"
class="selectfeld"
bind:value={baujahr}
>
<option selected disabled>bitte auswählen</option>
<option value="vor 1978">vor 1978</option>
@@ -125,10 +114,7 @@ $: standardXL =
<div class="auswahl">
<div class="titel">Heizungsalter</div>
<select
class="selectfeld"
bind:value={heizungsAlter}
>
<select class="selectfeld" bind:value={heizungsAlter}>
<option selected disabled>bitte auswählen</option>
<option value="< 3">jünger als 3 Jahre</option>
<option value=">= 3">3 Jahre oder älter</option>
@@ -137,10 +123,7 @@ $: standardXL =
<div class="auswahl">
<div class="titel">Wohneinheiten</div>
<select
class="selectfeld"
bind:value={einheiten}
>
<select class="selectfeld" bind:value={einheiten}>
<option selected disabled>bitte auswählen</option>
<option value="bis 4 Wohneinheiten"
>bis 4 Wohneinheiten</option
@@ -153,10 +136,7 @@ $: standardXL =
<div class="OEA_item4">
<div class="titel">Leerstand</div>
<select
class="selectfeld ausnahmen"
bind:value={leerStand}
>
<select class="selectfeld ausnahmen" bind:value={leerStand}>
<option selected disabled>bitte auswählen</option>
<option value="bis 30">bis 30%</option>
<option value="mehr als 30">mehr als 30%</option>
@@ -165,23 +145,34 @@ $: standardXL =
</div>
{/if}
<div id="thirdrow" class="thirdrow"
<div
id="thirdrow"
class="thirdrow"
class:grid-cols-1={oneBOX}
class:md:grid-cols-6={threeBOX}
class:md:grid-cols-4={!oneBOX && !threeBOX}
>
{#if isTwoBoxReason && (gebaeudetyp != "Gewerbegebäude") && (ausnahme === false)}
{#if isTwoBoxReason && gebaeudetyp != "Gewerbegebäude" && ausnahme === false}
<WidgetCardTemplate
name="Verbrauchsausweis Wohngebäude"
price = {PRICES.VerbrauchsausweisWohnen[standardXL ? Enums.AusweisTyp.standardXL : Enums.AusweisTyp.Standard]}
price1 = {PRICES.VerbrauchsausweisWohnen[standardXL ? Enums.AusweisTyp.BeratungXL : Enums.AusweisTyp.Beratung]}
price2 = {PRICES.VerbrauchsausweisWohnen[standardXL ? Enums.AusweisTyp.OfflineXL : Enums.AusweisTyp.Offline]}
src={'https://online-energieausweis.org/images/partner/'+partner+'/wohngebaeude.svg'}
price={PRICES.VerbrauchsausweisWohnen[
standardXL
? Enums.AusweisTyp.standardXL
: Enums.AusweisTyp.Standard
]}
price1={PRICES.VerbrauchsausweisWohnen[
standardXL
? Enums.AusweisTyp.BeratungXL
: Enums.AusweisTyp.Beratung
]}
price2={PRICES.VerbrauchsausweisWohnen[
standardXL
? Enums.AusweisTyp.OfflineXL
: Enums.AusweisTyp.Offline
]}
src={"https://online-energieausweis.org/images/partner/" +
partner +
"/wohngebaeude.svg"}
alt="Wohnhaus Verbrauchsausweis"
variant="einfach"
empfehlung="nein"
@@ -189,27 +180,40 @@ $: standardXL =
services={[
["3&nbsp;Jahresverbräuche der Heizung benötigt.", true],
["Zulässig bei Vermietung oder Verkauf.", true],
["Unzulässig bei unsanierten Gebäuden vor 1978.", false],
[
"Unzulässig bei unsanierten Gebäuden vor 1978.",
false,
],
["Ungenau durch individuelles Heizverhalten.", false],
["Wird nicht immer bei den Banken akzeptiert.", false]
["Wird nicht immer bei den Banken akzeptiert.", false],
]}
href_buy1={`https://online-energieausweis.org/${partner}/energieausweis-erstellen/verbrauchsausweis-wohngebaeude/${standardXL ? '?ausweistyp=standardXL' : ''}`}
href_buy2={`https://online-energieausweis.org/${partner}/energieausweis-erstellen/verbrauchsausweis-wohngebaeude/${standardXL ? '?ausweistyp=BeratungXL' : '?ausweistyp=Beratung'}`}
href_buy3={`https://online-energieausweis.org/${partner}/energieausweis-erstellen/verbrauchsausweis-wohngebaeude/${standardXL ? '?ausweistyp=OfflineXL' : '?ausweistyp=Offline'}`}
href_buy1={`https://online-energieausweis.org/${partner}/energieausweis-erstellen/verbrauchsausweis-wohngebaeude/${standardXL ? "?ausweistyp=standardXL" : ""}`}
href_buy2={`https://online-energieausweis.org/${partner}/energieausweis-erstellen/verbrauchsausweis-wohngebaeude/${standardXL ? "?ausweistyp=BeratungXL" : "?ausweistyp=Beratung"}`}
href_buy3={`https://online-energieausweis.org/${partner}/energieausweis-erstellen/verbrauchsausweis-wohngebaeude/${standardXL ? "?ausweistyp=OfflineXL" : "?ausweistyp=Offline"}`}
></WidgetCardTemplate>
{/if}
{#if isTwoBoxReason && (gebaeudetyp != "Gewerbegebäude")}
{#if isTwoBoxReason && gebaeudetyp != "Gewerbegebäude"}
<WidgetCardTemplate
name="Bedarfsausweis Wohngebäude"
price = {PRICES.BedarfsausweisWohnen[standardXL ? Enums.AusweisTyp.standardXL : Enums.AusweisTyp.Standard]}
price1 = {PRICES.BedarfsausweisWohnen[standardXL ? Enums.AusweisTyp.BeratungXL : Enums.AusweisTyp.Beratung]}
price2 = {PRICES.BedarfsausweisWohnen[standardXL ? Enums.AusweisTyp.OfflineXL : Enums.AusweisTyp.Offline]}
src={'https://online-energieausweis.org/images/partner/'+partner+'/wohngebaeude.svg'}
price={PRICES.BedarfsausweisWohnen[
standardXL
? Enums.AusweisTyp.standardXL
: Enums.AusweisTyp.Standard
]}
price1={PRICES.BedarfsausweisWohnen[
standardXL
? Enums.AusweisTyp.BeratungXL
: Enums.AusweisTyp.Beratung
]}
price2={PRICES.BedarfsausweisWohnen[
standardXL
? Enums.AusweisTyp.OfflineXL
: Enums.AusweisTyp.Offline
]}
src={"https://online-energieausweis.org/images/partner/" +
partner +
"/wohngebaeude.svg"}
alt="Wohnhaus Bedarfsausweis"
variant="fundiert"
empfehlung="ja"
@@ -219,200 +223,283 @@ $: standardXL =
["Für Vermietung, Verkauf und Finanzierung.", true],
["Zulässig auch für unsanierte Objekte.", true],
["Kann als Grundlage für den ISFP dienen.", true],
["Objektivere Berechnungsmethode nach DIN 18599.", true],
[
"Objektivere Berechnungsmethode nach DIN 18599.",
true,
],
]}
href_buy1={`https://online-energieausweis.org/${partner}/energieausweis-erstellen/bedarfsausweis-wohngebaeude/${standardXL ? '?ausweistyp=standardXL' : ''}`}
href_buy2={`https://online-energieausweis.org/${partner}/energieausweis-erstellen/bedarfsausweis-wohngebaeude/${standardXL ? '?ausweistyp=BeratungXL' : '?ausweistyp=Beratung'}`}
href_buy3={`https://online-energieausweis.org/${partner}/energieausweis-erstellen/bedarfsausweis-wohngebaeude/${standardXL ? '?ausweistyp=OfflineXL' : '?ausweistyp=Offline'}`}
href_buy1={`https://online-energieausweis.org/${partner}/energieausweis-erstellen/bedarfsausweis-wohngebaeude/${standardXL ? "?ausweistyp=standardXL" : ""}`}
href_buy2={`https://online-energieausweis.org/${partner}/energieausweis-erstellen/bedarfsausweis-wohngebaeude/${standardXL ? "?ausweistyp=BeratungXL" : "?ausweistyp=Beratung"}`}
href_buy3={`https://online-energieausweis.org/${partner}/energieausweis-erstellen/bedarfsausweis-wohngebaeude/${standardXL ? "?ausweistyp=OfflineXL" : "?ausweistyp=Offline"}`}
></WidgetCardTemplate>
{/if}
{#if isTwoBoxReason && isGewerbe && (leerStand != "mehr als 30")}
{#if isTwoBoxReason && isGewerbe && leerStand != "mehr als 30"}
<WidgetCardTemplate
name="Verbrauchsausweis Gewerbegebäude"
price = {PRICES.VerbrauchsausweisGewerbe[standardXL ? Enums.AusweisTyp.standardXL : Enums.AusweisTyp.Standard]}
price1 = {PRICES.VerbrauchsausweisGewerbe[standardXL ? Enums.AusweisTyp.BeratungXL : Enums.AusweisTyp.Beratung]}
price2 = {PRICES.VerbrauchsausweisGewerbe[standardXL ? Enums.AusweisTyp.OfflineXL : Enums.AusweisTyp.Offline]}
src={'https://online-energieausweis.org/images/partner/'+partner+'/gewerbegebaeude.svg'}
price={PRICES.VerbrauchsausweisGewerbe[
standardXL
? Enums.AusweisTyp.standardXL
: Enums.AusweisTyp.Standard
]}
price1={PRICES.VerbrauchsausweisGewerbe[
standardXL
? Enums.AusweisTyp.BeratungXL
: Enums.AusweisTyp.Beratung
]}
price2={PRICES.VerbrauchsausweisGewerbe[
standardXL
? Enums.AusweisTyp.OfflineXL
: Enums.AusweisTyp.Offline
]}
src={"https://online-energieausweis.org/images/partner/" +
partner +
"/gewerbegebaeude.svg"}
alt="Gewerbe Verbrauchsausweis"
variant="einfach"
empfehlung="nein"
cta="jetzt&nbsp;online erstellen"
services={[
["3&nbsp;Jahresverbräuche von Heizung Gebäudestrom&nbsp;nötig.", true],
[
"3&nbsp;Jahresverbräuche von Heizung Gebäudestrom&nbsp;nötig.",
true,
],
["Zulässig bei Vermietung oder Verkauf.", true],
["Für bauliche und energetische Maßnahmen ungeeignet.", false],
[
"Für bauliche und energetische Maßnahmen ungeeignet.",
false,
],
["Wird nicht immer bei den Banken akzeptiert.", false],
["Ungenau durch individuelles Heizverhalten", false],
]}
href_buy1={`https://online-energieausweis.org/${partner}/energieausweis-erstellen/verbrauchsausweis-gewerbe/${standardXL ? '?ausweistyp=standardXL' : ''}`}
href_buy2={`https://online-energieausweis.org/${partner}/energieausweis-erstellen/verbrauchsausweis-gewerbe/${standardXL ? '?ausweistyp=BeratungXL' : '?ausweistyp=Beratung'}`}
href_buy3={`https://online-energieausweis.org/${partner}/energieausweis-erstellen/verbrauchsausweis-gewerbe/${standardXL ? '?ausweistyp=OfflineXL' : '?ausweistyp=Offline'}`}
href_buy1={`https://online-energieausweis.org/${partner}/energieausweis-erstellen/verbrauchsausweis-gewerbe/${standardXL ? "?ausweistyp=standardXL" : ""}`}
href_buy2={`https://online-energieausweis.org/${partner}/energieausweis-erstellen/verbrauchsausweis-gewerbe/${standardXL ? "?ausweistyp=BeratungXL" : "?ausweistyp=Beratung"}`}
href_buy3={`https://online-energieausweis.org/${partner}/energieausweis-erstellen/verbrauchsausweis-gewerbe/${standardXL ? "?ausweistyp=OfflineXL" : "?ausweistyp=Offline"}`}
></WidgetCardTemplate>
{/if}
{#if isTwoBoxReason && isGewerbe}
<WidgetCardTemplate
name="Bedarfsausweis Gewerbegebäude"
price = {PRICES.BedarfsausweisGewerbe[standardXL ? Enums.AusweisTyp.standardXL : Enums.AusweisTyp.Standard]}
price1 = {PRICES.BedarfsausweisGewerbe[standardXL ? Enums.AusweisTyp.BeratungXL : Enums.AusweisTyp.Beratung]}
price2 = {PRICES.BedarfsausweisGewerbe[standardXL ? Enums.AusweisTyp.OfflineXL : Enums.AusweisTyp.Offline]}
src={'https://online-energieausweis.org/images/partner/'+partner+'/gewerbegebaeude.svg'}
price={PRICES.BedarfsausweisGewerbe[
standardXL
? Enums.AusweisTyp.standardXL
: Enums.AusweisTyp.Standard
]}
price1={PRICES.BedarfsausweisGewerbe[
standardXL
? Enums.AusweisTyp.BeratungXL
: Enums.AusweisTyp.Beratung
]}
price2={PRICES.BedarfsausweisGewerbe[
standardXL
? Enums.AusweisTyp.OfflineXL
: Enums.AusweisTyp.Offline
]}
src={"https://online-energieausweis.org/images/partner/" +
partner +
"/gewerbegebaeude.svg"}
alt="Gewerbe Bedarfsausweis"
variant="fundiert"
empfehlung="ja"
cta="Angebot anfragen"
services={[
["Mehrzonenmodell nach DIN 18599.", true],
["Zulässig bei Vermietung oder Verkauf.", true],
["Grundlage für Sanierung-Varianten.", true],
["Objektiveres, besser vergleichbares Ergebnis.", true],
["Zulässig bei Leerstand oder fehlenden Verbräuchen", true],
[
"Zulässig bei Leerstand oder fehlenden Verbräuchen",
true,
],
]}
href_buy1={`https://online-energieausweis.org/${partner}/angebot-anfragen/bedarfsausweis-gewerbe-anfragen/${standardXL ? '?ausweistyp=standardXL' : ''}`}
href_buy2={`https://online-energieausweis.org/${partner}/angebot-anfragen/bedarfsausweis-gewerbe-anfragen/${standardXL ? '?ausweistyp=BeratungXL' : '?ausweistyp=Beratung'}`}
href_buy1={`https://online-energieausweis.org/${partner}/angebot-anfragen/bedarfsausweis-gewerbe-anfragen/${standardXL ? "?ausweistyp=standardXL" : ""}`}
href_buy2={`https://online-energieausweis.org/${partner}/angebot-anfragen/bedarfsausweis-gewerbe-anfragen/${standardXL ? "?ausweistyp=BeratungXL" : "?ausweistyp=Beratung"}`}
></WidgetCardTemplate>
{/if}
{#if (anlass != "bitte auswählen") && !isTwoBoxReason && (gebaeudetyp != "Gewerbegebäude")}
{#if anlass != "bitte auswählen" && !isTwoBoxReason && gebaeudetyp != "Gewerbegebäude"}
<WidgetCardTemplate
name="GEG-Nachweis Wohngebäude"
price = {PRICES.GEGNachweisWohnen[standardXL ? Enums.AusweisTyp.standardXL : Enums.AusweisTyp.Standard]}
price1 = {PRICES.GEGNachweisWohnen[standardXL ? Enums.AusweisTyp.BeratungXL : Enums.AusweisTyp.Beratung]}
price2 = {PRICES.GEGNachweisWohnen[standardXL ? Enums.AusweisTyp.OfflineXL : Enums.AusweisTyp.Offline]}
src={'https://online-energieausweis.org/images/partner/'+partner+'/wohngebaeude.svg'}
price={PRICES.GEGNachweisWohnen[
standardXL
? Enums.AusweisTyp.standardXL
: Enums.AusweisTyp.Standard
]}
price1={PRICES.GEGNachweisWohnen[
standardXL
? Enums.AusweisTyp.BeratungXL
: Enums.AusweisTyp.Beratung
]}
price2={PRICES.GEGNachweisWohnen[
standardXL
? Enums.AusweisTyp.OfflineXL
: Enums.AusweisTyp.Offline
]}
src={"https://online-energieausweis.org/images/partner/" +
partner +
"/wohngebaeude.svg"}
alt="GEG-Nachweis-Wohnen"
variant="Bauvorlage"
empfehlung="ja"
cta="Angebot anfragen"
services={[
["Nachweis fürs Bauamt bei Neubau oder Modernisierung.", true],
["Beinhaltet die Effizienz der Bauteile und Anlagentechnik.", true],
["Erstellung des Energieausweises nach Abschluss inklusive.", true],
["Berechnung und Bilanzierung nach aktueller DIN 18599.", true],
["Zonierung und Erstellung eines 3D Gebäudemodells.", true],
[
"Nachweis fürs Bauamt bei Neubau oder Modernisierung.",
true,
],
[
"Beinhaltet die Effizienz der Bauteile und Anlagentechnik.",
true,
],
[
"Erstellung des Energieausweises nach Abschluss inklusive.",
true,
],
[
"Berechnung und Bilanzierung nach aktueller DIN 18599.",
true,
],
[
"Zonierung und Erstellung eines 3D Gebäudemodells.",
true,
],
]}
href_buy1={`https://online-energieausweis.org/${partner}/angebot-anfragen/geg-nachweis-wohnen-anfragen/${standardXL ? '?ausweistyp=standardXL' : ''}`}
href_buy2={`https://online-energieausweis.org/${partner}/angebot-anfragen/geg-nachweis-wohnen-anfragen/${standardXL ? '?ausweistyp=BeratungXL' : '?ausweistyp=Beratung'}`}
href_buy1={`https://online-energieausweis.org/${partner}/angebot-anfragen/geg-nachweis-wohnen-anfragen/${standardXL ? "?ausweistyp=standardXL" : ""}`}
href_buy2={`https://online-energieausweis.org/${partner}/angebot-anfragen/geg-nachweis-wohnen-anfragen/${standardXL ? "?ausweistyp=BeratungXL" : "?ausweistyp=Beratung"}`}
></WidgetCardTemplate>
{/if}
{#if (anlass != "bitte auswählen") && !isTwoBoxReason && isGewerbe}
{#if anlass != "bitte auswählen" && !isTwoBoxReason && isGewerbe}
<WidgetCardTemplate
name="GEG-Nachweis Gewerbegebäude"
price = {PRICES.GEGNachweisGewerbe[standardXL ? Enums.AusweisTyp.standardXL : Enums.AusweisTyp.Standard]}
price1 = {PRICES.GEGNachweisGewerbe[standardXL ? Enums.AusweisTyp.BeratungXL : Enums.AusweisTyp.Beratung]}
price2 = {PRICES.GEGNachweisGewerbe[standardXL ? Enums.AusweisTyp.OfflineXL : Enums.AusweisTyp.Offline]}
src={'https://online-energieausweis.org/images/partner/'+partner+'/gewerbegebaeude.svg'}
price={PRICES.GEGNachweisGewerbe[
standardXL
? Enums.AusweisTyp.standardXL
: Enums.AusweisTyp.Standard
]}
price1={PRICES.GEGNachweisGewerbe[
standardXL
? Enums.AusweisTyp.BeratungXL
: Enums.AusweisTyp.Beratung
]}
price2={PRICES.GEGNachweisGewerbe[
standardXL
? Enums.AusweisTyp.OfflineXL
: Enums.AusweisTyp.Offline
]}
src={"https://online-energieausweis.org/images/partner/" +
partner +
"/gewerbegebaeude.svg"}
alt="GEG-Nachweis-Gewerbe"
variant="Bauvorlage"
empfehlung="ja"
cta="Angebot anfragen"
services={[
["Nachweis fürs Bauamt bei Neubau oder Modernisierung.", true],
["Beinhaltet die Effizienz der Bauteile und Anlagentechnik.", true],
["Erstellung des Energieausweises nach Abschluss inklusive.", true],
["Berechnung und Bilanzierung nach aktueller DIN 18599.", true],
["Mehrzonenmodell inkl. Erstellung eines 3D Gebäudemodells.", true],
[
"Nachweis fürs Bauamt bei Neubau oder Modernisierung.",
true,
],
[
"Beinhaltet die Effizienz der Bauteile und Anlagentechnik.",
true,
],
[
"Erstellung des Energieausweises nach Abschluss inklusive.",
true,
],
[
"Berechnung und Bilanzierung nach aktueller DIN 18599.",
true,
],
[
"Mehrzonenmodell inkl. Erstellung eines 3D Gebäudemodells.",
true,
],
]}
href_buy1={`https://online-energieausweis.org/${partner}/angebot-anfragen/geg-nachweis-gewerbe-anfragen/${standardXL ? '?ausweistyp=standardXL' : ''}`}
href_buy2={`https://online-energieausweis.org/${partner}/angebot-anfragen/geg-nachweis-gewerbe-anfragen/${standardXL ? '?ausweistyp=BeratungXL' : '?ausweistyp=Beratung'}`}
href_buy1={`https://online-energieausweis.org/${partner}/angebot-anfragen/geg-nachweis-gewerbe-anfragen/${standardXL ? "?ausweistyp=standardXL" : ""}`}
href_buy2={`https://online-energieausweis.org/${partner}/angebot-anfragen/geg-nachweis-gewerbe-anfragen/${standardXL ? "?ausweistyp=BeratungXL" : "?ausweistyp=Beratung"}`}
></WidgetCardTemplate>
{/if}
</div>
</div>
</div>
<style lang="postcss">
#IBC_app {
#IBC_app {
@font-face {
font-family: "immo Sans";
src: url('/fonts/Immo-Sans/immoSans-Regular.eot');
src: url('/fonts/Immo-Sans/immoSans-Regular.eot?#iefix') format('embedded-opentype'),
url('/fonts/Immo-Sans/immoSans-Regular.woff2') format('woff2'),
url('/fonts/Immo-Sans/immoSans-Regular.woff') format('woff');
src: url("/fonts/Immo-Sans/immoSans-Regular.eot");
src:
url("/fonts/Immo-Sans/immoSans-Regular.eot?#iefix")
format("embedded-opentype"),
url("/fonts/Immo-Sans/immoSans-Regular.woff2") format("woff2"),
url("/fonts/Immo-Sans/immoSans-Regular.woff") format("woff");
font-style: normal;
font-weight: 400;
}
@font-face {
font-family: "immo Sans Bold";
src: url('/fonts/Immo-Sans/immoSans-Bold.eot');
src: url('/fonts/Immo-Sans/immoSans-Bold.eot?#iefix') format('embedded-opentype'), url('../../Fonts/Immo-Sans/immoSans-Bold.woff2') format('woff2'), url('../../Fonts/Immo-Sans/immoSans-Bold.woff') format('woff');
font-style: normal;
font-weight: 700;
}
}
@font-face {
font-family: 'Antique Olive Compact bold';
font-family: "immo Sans Bold";
src: url("/fonts/Immo-Sans/immoSans-Bold.eot");
src:
url("/fonts/Immo-Sans/immoSans-Bold.eot?#iefix")
format("embedded-opentype"),
url("../../Fonts/Immo-Sans/immoSans-Bold.woff2") format("woff2"),
url("../../Fonts/Immo-Sans/immoSans-Bold.woff") format("woff");
font-style: normal;
font-weight: 700;
}
@font-face {
font-family: "Antique Olive Compact bold";
font-weight: 700;
font-style: normal;
font-display:swap;
src: url("/fonts/Antique Olive Std Compact.woff2") format('woff2');
font-display: swap;
src: url("/fonts/Antique Olive Std Compact.woff2") format("woff2");
}
@apply min-w-[320px] max-w-[1920px] p-[4px]
sm:p-[10px];
font-family: "immo Sans";
select option{
font-family: "immo Sans",sans-serif;}
select option {
font-family: "immo Sans", sans-serif;
}
.firstrow{@apply grid grid-cols-1 gap-x-4 gap-y-2
.firstrow {
@apply grid grid-cols-1 gap-x-4 gap-y-2
sm:grid-cols-2 sm:gap-x-4 sm:gap-y-2;
.titel{@apply text-black font-bold bg-[#ffcc00] px-2 py-1 rounded-[0.25rem];}
.titel {
@apply text-black font-bold bg-[#ffcc00] px-2 py-1 rounded-[0.25rem];
}
.selectfeld{@apply w-full px-2 py-1 min-h-[38px] ring-1 ring-black/15}
.selectfeld {
@apply w-full px-2 py-1 min-h-[38px] ring-1 ring-black/15;
}
}
}
.secondrow{@apply grid grid-cols-2 gap-x-4 gap-y-2 mt-4
.secondrow {
@apply grid grid-cols-2 gap-x-4 gap-y-2 mt-4
sm:grid-cols-4 sm:gap-x-4 sm:gap-y-2;
.titel{@apply text-black font-bold bg-[#cccccc] px-2 py-1 rounded-[0.25rem];}
.titel {
@apply text-black font-bold bg-[#cccccc] px-2 py-1 rounded-[0.25rem];
}
.selectfeld{@apply w-full px-2 py-1 min-h-[38px] ring-1 ring-black/15}
.selectfeld {
@apply w-full px-2 py-1 min-h-[38px] ring-1 ring-black/15;
}
}
}
#OEA_input {
@apply grid;
}
#OEA_input{@apply grid}
.thirdrow{@apply grid grid-cols-1 gap-x-4 gap-y-2 col-start-1
.thirdrow {
@apply grid grid-cols-1 gap-x-4 gap-y-2 col-start-1
md:grid-cols-4 md:gap-x-4 md:gap-y-2;
}
}
}
}
</style>

View File

@@ -34,7 +34,6 @@
ab {price}
</p>
</div>
</div>
<hr class="col-span-2 w-full md:w-[50%] md:m-auto bg-[#ffcc00] h-[2px]" />
@@ -52,9 +51,11 @@
<hr class="col-span-2 w-full md:w-[50%] md:m-auto bg-[#ffcc00] h-[2px]" />
<div class="sumCent buttoncols"
<div
class="sumCent buttoncols"
class:md:grid-cols-3={href_buy3}
class:md:grid-cols-2={!href_buy3}>
class:md:grid-cols-2={!href_buy3}
>
<a
href={href_buy1}
class="buttoncol"

View File

@@ -19,7 +19,6 @@ 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

@@ -47,10 +47,10 @@ export const BedarfsausweisWohnenSchema = z.object({
haustuer_art: z.number().nullish(),
dach_bauart: z.string().nullish(),
decke_bauart: z.string().nullish(),
dach_daemmung: z.number().nullish(),
decke_daemmung: z.number().nullish(),
aussenwand_daemmung: z.number().nullish(),
boden_daemmung: z.number().nullish(),
dach_daemmung: z.string().nullish(),
decke_daemmung: z.string().nullish(),
aussenwand_daemmung: z.string().nullish(),
boden_daemmung: z.string().nullish(),
aussenwand_bauart: z.string().nullish(),
boden_bauart: z.string().nullish(),
warmwasser_verteilung: z.string().nullish(),

View File

@@ -17,7 +17,6 @@ export const BenutzerSchema = z.object({
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,6 +7,5 @@ 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,7 +12,6 @@ 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

@@ -1,13 +0,0 @@
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

@@ -150,7 +150,7 @@ height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
<Footer />
<NotificationWrapper client:load />
<TicketButton client:load userEmail={user?.email ?? ""}></TicketButton>
<TicketButton client:load></TicketButton>
</body>
</html>

View File

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

View File

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

View File

@@ -7,6 +7,7 @@ export const getKlimafaktoren = memoize(async (date: Date, plz: string) => {
return null;
}
try {
const response = await api.klimafaktoren.GET.fetch({
plz,
genauigkeit: "years",
@@ -16,4 +17,7 @@ export const getKlimafaktoren = memoize(async (date: Date, plz: string) => {
enddatum: moment(date).add(2, "years").utc(true).toString(),
});
return response;
} catch (e) {
return null;
}
});

View File

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

View File

@@ -0,0 +1,29 @@
import { uptime } from "os"
import crypto from "crypto";
/**
* Generiert einen zeitbasierten Hash der sich alle 15 Minuten ändert und an die Uptime des servers gekoppelt ist.
* @param email - Die E-Mail-Adresse des Benutzers, die als Teil des Hashes verwendet wird.
* @param time - Die Zeit in Millisekunden, die seit dem Start des Servers vergangen ist (Standard ist die Uptime des Servers).
* @param length - Die Länge des zurückgegebenen Hashes (Standard ist 6 Zeichen).
* @returns Ein zeitbasierter Hash, der sich alle 15 Minuten ändert und auf der E-Mail-Adresse basiert.
*/
export function createTimeBasedHash(email: string, time: number = uptime(), length: number = 6): string {
const now = Date.now();
const elapsed = now - time;
const window = Math.floor(elapsed / (15 * 60 * 1000)); // 15 minute windows
const data = `${email}:${window}`;
// Use a cryptographic hash (you can also use HMAC with a secret if you want)
const hash = crypto.createHash('sha256').update(data).digest();
// Convert part of the hash to an integer
const int = hash.readUInt32BE(0); // take first 4 bytes
// Modulo to get 6 digits
const pin = (int % 1000000).toString().padStart(6, '0');
return pin;
}

View File

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

View File

@@ -1,14 +0,0 @@
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

@@ -1,27 +1,11 @@
import {
AufnahmeClient,
BedarfsausweisWohnenClient,
BenutzerClient,
BildClient,
getAusweisartFromId,
ObjektClient,
RechnungClient,
VerbrauchsausweisGewerbeClient,
VerbrauchsausweisWohnenClient,
} from "#components/Ausweis/types.js";
import { AufnahmeClient, BedarfsausweisWohnenClient, BenutzerClient, BildClient, getAusweisartFromId, ObjektClient, RechnungClient, VerbrauchsausweisGewerbeClient, VerbrauchsausweisWohnenClient } from "#components/Ausweis/types.js";
import { pdfDatenblattVerbrauchsausweisGewerbe } from "#lib/pdf/pdfDatenblattVerbrauchsausweisGewerbe.js";
import { pdfDatenblattVerbrauchsausweisWohnen } from "#lib/pdf/pdfDatenblattVerbrauchsausweisWohnen.js";
import { pdfVerbrauchsausweisGewerbe } from "#lib/pdf/pdfVerbrauchsausweisGewerbe.js";
import { pdfVerbrauchsausweisWohnen } from "#lib/pdf/pdfVerbrauchsausweisWohnen.js";
import { pdfAushangVerbrauchsausweisGewerbe } from "#lib/pdf/pdfAushangVerbrauchsausweisGewerbe.js";
import {
BedarfsausweisWohnen,
Enums,
prisma,
Rechnung,
VerbrauchsausweisGewerbe,
VerbrauchsausweisWohnen,
} from "#lib/server/prisma.js";
import { Enums, prisma, Rechnung } from "#lib/server/prisma.js";
/**
* Gibt den richtigen Prisma Adapter für die Ausweisart basierend auf der UID zurück, oder null bei einer falschen UID.
@@ -31,17 +15,17 @@ export function getPrismaAusweisAdapter(id: string) {
const ausweisart = getAusweisartFromId(id);
if (ausweisart === Enums.Ausweisart.VerbrauchsausweisWohnen) {
return prisma.verbrauchsausweisWohnen;
return prisma.verbrauchsausweisWohnen
} else if (ausweisart === Enums.Ausweisart.VerbrauchsausweisGewerbe) {
return prisma.verbrauchsausweisGewerbe;
return prisma.verbrauchsausweisGewerbe
} else if (ausweisart === Enums.Ausweisart.BedarfsausweisWohnen) {
return prisma.bedarfsausweisWohnen;
return prisma.bedarfsausweisWohnen
} else if (ausweisart === Enums.Ausweisart.GEGNachweisWohnen) {
return prisma.gEGNachweisWohnen;
return prisma.gEGNachweisWohnen
} else if (ausweisart === Enums.Ausweisart.GEGNachweisGewerbe) {
return prisma.gEGNachweisGewerbe;
return prisma.gEGNachweisGewerbe
} else if (ausweisart === Enums.Ausweisart.BedarfsausweisGewerbe) {
return prisma.bedarfsausweisGewerbe;
return prisma.bedarfsausweisGewerbe
}
}
@@ -49,166 +33,50 @@ export function getPrismaAusweisAdapter(id: string) {
* Gibt den richtigen Ansichtsausweis basierend auf der Ausweisart zurück.
* @param ausweis
*/
export async function getAnsichtsausweis(
ausweis:
| VerbrauchsausweisWohnenClient
| VerbrauchsausweisGewerbeClient
| BedarfsausweisWohnenClient,
aufnahme: AufnahmeClient,
objekt: ObjektClient,
bilder: BildClient[],
user: BenutzerClient,
vorschau: boolean = true,
ausweisart = getAusweisartFromId(ausweis.id)
) {
export async function getAnsichtsausweis(ausweis: VerbrauchsausweisWohnenClient | VerbrauchsausweisGewerbeClient | BedarfsausweisWohnenClient, aufnahme: AufnahmeClient, objekt: ObjektClient, bilder: BildClient[], user: BenutzerClient, vorschau: boolean = true, ausweisart = getAusweisartFromId(ausweis.id)) {
if (!ausweisart) {
return null;
return null
}
if (ausweisart === Enums.Ausweisart.VerbrauchsausweisWohnen) {
return await pdfVerbrauchsausweisWohnen(
ausweis as VerbrauchsausweisWohnenClient,
aufnahme,
objekt,
bilder,
user,
vorschau
);
return await pdfVerbrauchsausweisWohnen(ausweis as VerbrauchsausweisWohnenClient, aufnahme, objekt, bilder, user, vorschau)
} else if (ausweisart === Enums.Ausweisart.VerbrauchsausweisGewerbe) {
return await pdfVerbrauchsausweisGewerbe(
ausweis as VerbrauchsausweisGewerbeClient,
aufnahme,
objekt,
bilder,
user,
vorschau
);
return await pdfVerbrauchsausweisGewerbe(ausweis as VerbrauchsausweisGewerbeClient, aufnahme, objekt, bilder, user, vorschau)
}
return null;
return null
}
/**
* Gibt das richtige Datenblatt basierend auf der Ausweisart zurück.
* @param ausweis
*/
export async function getDatenblatt(
ausweis:
| VerbrauchsausweisWohnenClient
| VerbrauchsausweisGewerbeClient
| BedarfsausweisWohnenClient,
aufnahme: AufnahmeClient,
objekt: ObjektClient,
bilder: BildClient[],
user: BenutzerClient,
rechnung: Rechnung,
ausweisart = getAusweisartFromId(ausweis.id)
) {
export async function getDatenblatt(ausweis: VerbrauchsausweisWohnenClient | VerbrauchsausweisGewerbeClient | BedarfsausweisWohnenClient, aufnahme: AufnahmeClient, objekt: ObjektClient, bilder: BildClient[], user: BenutzerClient, rechnung: Rechnung, ausweisart = getAusweisartFromId(ausweis.id)) {
if (!ausweisart) {
return null;
return null
}
if (ausweisart === Enums.Ausweisart.VerbrauchsausweisWohnen) {
return await pdfDatenblattVerbrauchsausweisWohnen(
ausweis as VerbrauchsausweisWohnenClient,
aufnahme,
objekt,
rechnung,
bilder
);
return await pdfDatenblattVerbrauchsausweisWohnen(ausweis as VerbrauchsausweisWohnenClient, aufnahme, objekt, rechnung, bilder)
} else if (ausweisart === Enums.Ausweisart.VerbrauchsausweisGewerbe) {
return await pdfDatenblattVerbrauchsausweisGewerbe(
ausweis as VerbrauchsausweisGewerbeClient,
aufnahme,
objekt,
rechnung,
bilder
);
return await pdfDatenblattVerbrauchsausweisGewerbe(ausweis as VerbrauchsausweisGewerbeClient, aufnahme, objekt, rechnung, bilder)
}
return null;
return null
}
/**
* Gibt den richtigen Aushang basierend auf der Ausweisart zurück.
* @param ausweis
*/
export async function getAushang(
ausweis:
| VerbrauchsausweisWohnenClient
| VerbrauchsausweisGewerbeClient
| BedarfsausweisWohnenClient,
aufnahme: AufnahmeClient,
objekt: ObjektClient,
bilder: BildClient[],
user: BenutzerClient,
vorschau: boolean = true,
rechnung: Rechnung,
ausweisart = getAusweisartFromId(ausweis.id)
) {
export async function getAushang(ausweis: VerbrauchsausweisWohnenClient | VerbrauchsausweisGewerbeClient | BedarfsausweisWohnenClient, aufnahme: AufnahmeClient, objekt: ObjektClient, bilder: BildClient[], user: BenutzerClient, vorschau: boolean = true, rechnung: Rechnung, ausweisart = getAusweisartFromId(ausweis.id)) {
if (!ausweisart || !rechnung.services.includes(Enums.Service.Aushang)) {
return null;
return null
}
if (ausweisart === Enums.Ausweisart.VerbrauchsausweisGewerbe) {
return await pdfAushangVerbrauchsausweisGewerbe(
ausweis as VerbrauchsausweisGewerbeClient,
aufnahme,
objekt,
bilder,
user,
vorschau
);
return await pdfAushangVerbrauchsausweisGewerbe(ausweis as VerbrauchsausweisGewerbeClient, aufnahme, objekt, bilder, user, vorschau)
}
return null;
}
/**
* Extrahiert die Ausweisfelder aus einem Objekt, das mehrere Ausweisarten enthält, und fasst sie in einem gemeinsamen Feld `ausweis` zusammen.
* @param row Ein Objekt, das die Ausweisfelder enthält.
* @returns Ein neues Objekt, das die Ausweisfelder extrahiert und in einem gemeinsamen Feld `ausweis` zusammenfasst.
*/
export function extrahiereAusweisAusFeldMitMehrerenAusweisen<T>(
row: T & {
bedarfsausweis_wohnen?: BedarfsausweisWohnen;
verbrauchsausweis_wohnen?: VerbrauchsausweisWohnen;
verbrauchsausweis_gewerbe?: VerbrauchsausweisGewerbe;
}
) {
const {
bedarfsausweis_wohnen,
verbrauchsausweis_wohnen,
verbrauchsausweis_gewerbe,
...rest
} = row;
return {
...rest,
ausweis: {
...(bedarfsausweis_wohnen ??
verbrauchsausweis_wohnen ??
verbrauchsausweis_gewerbe),
ausweisart: bedarfsausweis_wohnen
? Enums.Ausweisart.BedarfsausweisWohnen
: 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: (
| BedarfsausweisWohnen
| VerbrauchsausweisWohnen
| VerbrauchsausweisGewerbe
) & { ausweisart: Enums.Ausweisart };
} & Omit<
T,
| "bedarfsausweis_wohnen"
| "verbrauchsausweis_wohnen"
| "verbrauchsausweis_gewerbe"
>;
return null
}

View File

@@ -0,0 +1,41 @@
import { transport } from "#lib/mail.js";
import {
Benutzer,
} from "#lib/client/prisma.js";
import { createTimeBasedHash } from "#lib/auth/time-based-hash.js";
export async function sendVerificationCodeMail(
user: Benutzer
) {
const code = createTimeBasedHash(user.email)
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>
<p>Um Ihre Registrierung abzuschließen, geben Sie folgenden Bestätigungscode auf der Website ein, um Ihre E-Mail-Adresse zu bestätigen:<br><br>
<b>${code}</b>
</p>
<p>
Mit freundlichen Grüßen,
<br>
Dipl.-Ing. Jens Cornelsen
<br>
<br>
<strong>IB Cornelsen</strong>
<br>
Katendeich 5A
<br>
21035 Hamburg
<br>
www.online-energieausweis.org
<br>
<br>
fon 040 · 209339850
<br>
fax 040 · 209339859
</p>`
});
}

View File

@@ -5,6 +5,7 @@ import {
} from "#lib/client/prisma.js";
import { encodeToken } from "#lib/auth/token.js";
import { TokenType } from "#lib/auth/types.js";
import { createTimeBasedHash } from "#lib/auth/time-based-hash.js";
export async function sendRegisterMail(
user: Benutzer
@@ -15,6 +16,8 @@ export async function sendRegisterMail(
id: user.id
})
const code = createTimeBasedHash(user.email)
await transport.sendMail({
from: `"IBCornelsen" <info@online-energieausweis.org>`,
to: user.email,
@@ -22,10 +25,11 @@ export async function sendRegisterMail(
bcc: "info@online-energieausweis.org",
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>
<a href="${BASE_URI}/auth/verify?t=${verificationJwt}">E-Mail-Adresse bestätigen</a><br></p>
<a href="${BASE_URI}/auth/verify?t=${verificationJwt}">E-Mail-Adresse bestätigen</a><br><br>
Oder geben Sie folgenden Bestätigungscode auf der Website ein:<br>
<b>${code}</b>
</p>
<p>
Mit freundlichen Grüßen,
<br>

View File

@@ -6,10 +6,11 @@
export let onLogin: (response: Awaited<ReturnType<typeof loginClient>>) => any;
export let email: string = "";
export let password: string = "";
export let route: "login" | "signup" = "login"
const navigate = (target: string) => {
route = target as typeof route;
let route: "login" | "signup" = "login"
const navigate = (target: typeof route) => {
route = target
}
const loginData = {

View File

@@ -72,7 +72,6 @@
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>

View File

@@ -0,0 +1,384 @@
<script lang="ts">
import ZipSearch from "../components/PlzSuche.svelte";
import Label from "../components/Label.svelte";
import type {
Bezahlmethoden,
} from "#lib/client/prisma";
import { Enums } from "#lib/client/prisma";
import PaymentOption from "#components/PaymentOption.svelte";
import CheckoutItem from "#components/CheckoutItem.svelte";
import { BenutzerClient, VerbrauchsausweisWohnenClient } from "#components/Ausweis/types.js";
import { PRICES } from "#lib/constants.js";
import { RechnungClient } from "#components/Ausweis/types.js";
import { api } from "astro-typesafe-api/client";
export let user: BenutzerClient;
export let ausweis:
| VerbrauchsausweisWohnenClient;
// TODO: überarbeiten und zu inferProcedureOutput machen
let rechnung: Partial<RechnungClient> = {
email: user.email,
empfaenger: user.vorname + " " + user.name,
strasse: user.adresse,
plz: user.plz,
ort: user.ort,
versand_empfaenger: user.vorname + " " + user.name,
versand_strasse: user.adresse,
versand_plz: user.plz,
versand_ort: user.ort,
telefon: user.telefon,
};
let services = [
{
name: "Qualitätsdruck per Post (zusätzlich zur PDF Version) für 9€ inkl. MwSt.",
id: Enums.Service.Qualitaetsdruck,
price: 9,
selected: false,
},
{
name: "Aushang (für öffentliche Gebäude gesetzlich vorgeschrieben) für 10€ inkl. MwSt.",
id: Enums.Service.Aushang,
price: 10,
selected: false,
},
{
name: "Same Day Service (Bestellung Werktags vor 12:00 Uhr - Ausstellung bis 18:00 Uhr am gleichen Tag) für 29€ inkl. MwSt.",
id: Enums.Service.SameDay,
price: 29,
selected: false,
},
{
name: "Telefonische Energieeffizienzberatung für 75€ inkl. MwSt.",
id: Enums.Service.Telefonberatung,
price: 75,
selected: false,
},
];
export let aktiveBezahlmethode: Bezahlmethoden =
Enums.Bezahlmethoden.paypal;
async function createPayment(e: SubmitEvent) {
e.preventDefault();
// TODO
const response = await api.rechnung.PUT.fetch({
...rechnung,
ausweisart: Enums.Ausweisart.VerbrauchsausweisWohnen,
ausweis_uid: ausweis.uid,
bezahlmethode: aktiveBezahlmethode,
services: services
.filter((service) => service.selected)
.map((service) => service.id),
});
if (aktiveBezahlmethode === Enums.Bezahlmethoden.rechnung) {
window.location.href = `/payment/success?r=${response.uid}&a=${ausweis.uid}`
} else {
window.location.href = response.checkout_url as string;
}
}
const priceTotal = services.reduce((acc, service) => {
if (service.selected) {
return acc + service.price;
}
return acc;
}, 0) + PRICES[Enums.Ausweisart.VerbrauchsausweisWohnen][0];
</script>
<form class="grid grid-cols-[2fr_1fr] gap-4 h-full" on:submit={createPayment}>
<div>
<h3 class="font-semibold">Ansprechpartner</h3>
<div class="rounded-lg border p-4 border-base-300 bg-base-100">
<div class="grid grid-cols-3 gap-4">
<!-- Anrede -->
<div>
<Label>Anrede *</Label>
<div>
<select name="anrede" bind:value={user.anrede}>
<option>bitte auswählen</option>
<option value="Herr">Herr</option>
<option value="Frau">Frau</option>
</select>
</div>
</div>
<!-- Vorname -->
<div>
<Label>Vorname *</Label>
<input
name="vorname"
type="text"
bind:value={user.vorname}
required
/>
</div>
<!-- Nachname -->
<div>
<Label>Nachname *</Label>
<input
name="name"
type="text"
bind:value={user.name}
required
/>
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<!-- Telefon -->
<div>
<Label>Telefon</Label>
<input
name="telefon"
bind:value={user.telefon}
type="text"
/>
</div>
<!-- Email -->
<div>
<Label>E-Mail *</Label>
<input
name="email"
type="email"
bind:value={user.email}
required
/>
</div>
</div>
</div>
<h3 class="mt-8 font-semibold">Rechnungsadresse</h3>
<div class="rounded-lg border p-4 border-base-300 bg-base-100">
<div class="grid grid-cols-2 gap-4">
<div>
<Label>Empfänger *</Label>
<input
name="rechnung_empfaenger"
type="text"
bind:value={rechnung.empfaenger}
required
data-rule-maxlength="100"
data-msg-maxlength="max. 100 Zeichen"
/>
</div>
<!-- Zusatzzeile -->
<div>
<Label>Zusatzzeile</Label>
<input
name="rechnung_zusatzzeile"
bind:value={rechnung.zusatzzeile}
type="text"
data-rule-maxlength="80"
data-msg-maxlength="max. 80 Zeichen"
/>
</div>
</div>
<div class="grid grid-cols-3 gap-4">
<!-- Strasse -->
<div>
<Label>Straße, Hausnummer *</Label>
<input
name="rechnung_strasse"
bind:value={rechnung.strasse}
type="text"
required
data-rule-maxlength="40"
data-msg-maxlength="max. 40 Zeichen"
/>
</div>
<!-- PLZ -->
<ZipSearch
name="rechnung_plz"
bind:zip={rechnung.plz}
bind:city={rechnung.ort}
/>
<!-- Ort -->
<div>
<Label>Ort *</Label>
<input
name="rechnung_ort"
readonly
type="text"
required
value={rechnung.ort}
/>
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<!-- Telefon -->
<div>
<Label>Telefon</Label>
<input
name="rechnung_telefon"
bind:value={rechnung.telefon}
type="text"
/>
</div>
<!-- Email -->
<div>
<Label>E-Mail</Label>
<input
name="rechnung_email"
bind:value={rechnung.email}
type="email"
/>
</div>
</div>
</div>
<h3 class="mt-8 font-semibold">Versandadresse</h3>
<div class="rounded-lg border p-4 border-base-300 bg-base-100">
<div class="flex flex-row gap-2 items-center">
<input
class="w-[15px] h-[15px]"
type="checkbox"
name="abweichende_versand_adresse"
bind:checked={rechnung.abweichende_versand_adresse}
/>
<Label>Abweichende Versandadresse</Label>
</div>
<div class="grid grid-cols-2 gap-4">
<!-- Empfänger -->
<div>
<Label>Empfänger *</Label>
<input
name="versand_empfaenger"
type="text"
readonly={!rechnung.abweichende_versand_adresse}
bind:value={rechnung.versand_empfaenger}
required
data-rule-maxlength="100"
data-msg-maxlength="max. 100 Zeichen"
/>
</div>
<!-- Zusatzzeile -->
<div>
<Label>Zusatzzeile</Label>
<input
name="versand_zusatzzeile"
type="text"
readonly={!rechnung.abweichende_versand_adresse}
bind:value={rechnung.versand_zusatzzeile}
data-rule-maxlength="80"
data-msg-maxlength="max. 80 Zeichen"
/>
</div>
</div>
<div class="grid grid-cols-3 gap-4">
<!-- Strasse -->
<div>
<Label>Straße, Hausnummer *</Label>
<input
name="versand_strasse"
type="text"
readonly={!rechnung.abweichende_versand_adresse}
bind:value={rechnung.versand_strasse}
required
data-rule-maxlength="40"
data-msg-maxlength="max. 40 Zeichen"
/>
</div>
<!-- PLZ -->
<ZipSearch
name="versand_plz"
readonly={!rechnung.abweichende_versand_adresse}
bind:zip={rechnung.versand_plz}
bind:city={rechnung.versand_ort}
/>
<!-- Ort -->
<div>
<Label>Ort *</Label>
<input
name="versand_ort"
type="text"
readonly
required
bind:value={rechnung.versand_ort}
/>
</div>
</div>
</div>
<h3 class="mt-8 font-semibold">Bezahlmethode</h3>
<div
class="rounded-lg border p-4 border-base-300 bg-base-100 flex flex-row gap-4 justify-between"
>
<PaymentOption
bezahlmethode={Enums.Bezahlmethoden.paypal}
bind:aktiveBezahlmethode
name={"PayPal"}
icon={"/images/paypal.png"}
></PaymentOption>
<PaymentOption
bezahlmethode={Enums.Bezahlmethoden.sofort}
bind:aktiveBezahlmethode
name={"Sofort"}
icon={"/images/sofort.png"}
></PaymentOption>
<PaymentOption
bezahlmethode={Enums.Bezahlmethoden.giropay}
bind:aktiveBezahlmethode
name={"Giropay"}
icon={"/images/giropay.png"}
></PaymentOption>
<PaymentOption
bezahlmethode={Enums.Bezahlmethoden.creditcard}
bind:aktiveBezahlmethode
name={"Kreditkarte"}
icon={"/images/mastercard.png"}
></PaymentOption>
<PaymentOption
bezahlmethode={Enums.Bezahlmethoden.rechnung}
bind:aktiveBezahlmethode
name={"Rechnung"}
icon={"/images/rechnung.png"}
></PaymentOption>
</div>
</div>
<div>
<h3 class="font-semibold">Zusammenfassung</h3>
<div class="rounded-lg border p-4 border-base-300 bg-base-100">
<CheckoutItem
image={"https://www.gih.de/wp-content/uploads/2015/11/EnergieausweisW-E.jpg"}
name={"Energieausweis"}
description={"Verbrauchsausweis Wohnen"}
price={45}
quantity={1}
removable={false}
maxQuantity={1}
/>
<div class="mt-auto">
<hr />
<div class="flex flex-row items-center justify-between">
<span class="opacity-75 text-sm">Netto</span>
<span class="font-semibold text-sm">{Math.round(priceTotal * 0.81 * 100) / 100}</span>
</div>
<div class="flex flex-row items-center justify-between">
<span class="opacity-75 text-sm">19% MwSt</span>
<span class="font-semibold text-sm">{Math.round(priceTotal * 0.19 * 100) / 100}}€</span>
</div>
<hr />
<div class="flex flex-row items-center justify-between">
<span class="opacity-75 text-sm">Gesamt</span>
<span class="font-semibold text-sm">{Math.round(priceTotal)}</span>
</div>
<p class="mt-8">Mit dem Klick auf "Bestellung Bestätigen" akzeptieren sie unsere <a href="/agb">AGB</a> und <a href="/impressum">Datenschutzbestimmungen</a>. Sie werden zu ihrem ausgewählten Bezahlprovider weitergeleitet, nach Bezahlung werden sie automatisch zu unserem Portal zurückgeleitet.</p>
<button class="btn btn-secondary w-full mt-4"
>Bestellung Bestätigen</button
>
</div>
</div>
</div>
</form>

View File

@@ -1156,7 +1156,7 @@ grid-cols-3 sm:grid-cols-5 justify-around justify-items-center items-center"
{/if}
<!-- Für alle -->
<div class="pruefpunkt">
<input type="checkbox" required/>
<input type="checkbox"/>
<div class="text-left">
Ich habe die AGB und DSGVO im <a href="/impressum#agb" target="_blank" rel="noopener noreferrer">Impressum</a> gelesen und akzeptiert.
</div>
@@ -1215,7 +1215,7 @@ sm:grid-cols-[min-content_min-content_min-content] sm:justify-self-end sm:mt-8"
<Overlay bind:hidden={loginOverlayHidden}>
<div class="bg-white w-full max-w-screen-sm py-8">
<EmbeddedAuthFlowModule onLogin={loginAction} email={email} route="signup"></EmbeddedAuthFlowModule>
<EmbeddedAuthFlowModule onLogin={loginAction} email={email}></EmbeddedAuthFlowModule>
</div>
</Overlay>

View File

@@ -3,6 +3,7 @@
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";
let passwort: string;
@@ -12,6 +13,10 @@
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) {
@@ -25,7 +30,7 @@
}
try {
const { id } = await api.user.PUT.fetch({
await api.user.PUT.fetch({
email,
passwort,
vorname,
@@ -37,7 +42,7 @@
return
}
window.location.href = "/auth/login";
window.location.href = `/auth/code?email=${email}`;
} catch (e) {
errorHidden = false;
}
@@ -82,7 +87,7 @@
name="email"
class="input input-bordered text-base text-base-content font-medium"
bind:value={email}
on:keyup={() => (email = email.toLowerCase())}
on:input={handleInput}
required
/>
</div>

View File

@@ -0,0 +1,89 @@
<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 NotificationWrapper from "#components/Notifications/NotificationWrapper.svelte";
export let redirect: string | null = null;
export let email: string;
function verify(e: SubmitEvent) {
e.preventDefault();
const code = numbers.join("");
if (code.length !== 6) {
addNotification({
message: "Bitte geben Sie einen gültigen Verifizierungscode ein.",
dismissable: true,
timeout: 3000,
type: "error"
});
return;
}
api.auth["verification-code"].POST.fetch({ code, email }).then(() => {
if (redirect) {
window.location.href = redirect;
} else {
window.location.href = "/";
}
}).catch(() => {
errorHidden = false;
});
}
// TODO
function codeErneutAnfordern() {
api.auth["verification-code"].GET.fetch(null, {
headers: {
"Authorization": "Bearer"
}
})
}
let numbers = new Array(6).fill("");
let errorHidden = true;
</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">Verifizierung</h1>
<p>Wir haben ihnen einen Verifizierungscode per Email geschickt, bitte geben sie diesen ein um ihre Registrierung fertigzustellen.</p>
<form on:submit={verify}>
<div class="flex flex-row gap-4 w-full justify-center my-12">
{#each { length: 6 } as _, i}
<input
type="text"
class="input input-bordered text-4xl text-base-content font-medium w-12 text-center"
bind:value={numbers[i]}
maxlength="1"
on:input={function(e) {
if (i !== 5) {
e.target.nextSibling.focus()
}
}}
required
/>
{/each}
</div>
<div class="flex justify-between">
<button type="submit" class="button"
>Abschicken</button
>
<!-- <button type="button" on:click={codeErneutAnfordern} class="button"
>Code erneut anfordern</button
> -->
</div>
{#if !errorHidden}
<div class="flex flex-row gap-4 mt-8" in:fade out:fade={{delay: 400}}>
<CrossCircled size={24} />
<span class="font-semibold"> Da ist wohl etwas schiefgelaufen. Der eingegebene Verifizierungscode ist ungültig.</span>
</div>
{/if}
</form>
<NotificationWrapper></NotificationWrapper>
</div>

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.ADMIN ? {
where: user.rolle === Enums.BenutzerRolle.USER ? {
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.ADMIN ? {
where: user.rolle === Enums.BenutzerRolle.USER ? {
id,
benutzer_id: user.id
} : { id },

View File

@@ -0,0 +1,70 @@
import { z } from "zod";
import { prisma } from "#lib/server/prisma.js";
import { APIError, defineApiRoute } from "astro-typesafe-api/server";
import { sendVerificationCodeMail } from "#lib/server/mail/code.js";
import { authorizationMiddleware } from "#lib/middleware/authorization.js";
import { createTimeBasedHash } from "#lib/auth/time-based-hash.js";
export const GET = defineApiRoute({
meta: {
description:
"Fragt einen neuen Verifizierungscode per Mail an.",
tags: ["Benutzer"],
summary: "Verifizierungscode anfragen.",
},
middleware: authorizationMiddleware,
output: z.void(),
async fetch(input, ctx) {
// Falls der Nutzer nicht existiert, wird eine Fehlermeldung zurückgegeben.
const user = await prisma.benutzer.findUnique({
where: {
email: input.email.toLowerCase(),
},
});
if (!user) {
throw new APIError({
code: "BAD_REQUEST",
message: "Benutzer konnte nicht gefunden werden.",
});
}
await sendVerificationCodeMail(user);
},
});
export const POST = defineApiRoute({
meta: {
description:
"Versucht den Nutzer mithilfe des abgeschickten Codes zu verifizieren.",
tags: ["Benutzer"],
summary: "Verifizieren.",
},
input: z.object({
code: z.string(),
email: z.string().email().toLowerCase(),
}),
output: z.void(),
async fetch({ code, email }, ctx) {
const generatedCode = createTimeBasedHash(email);
console.log(generatedCode, code);
if (code !== generatedCode) {
throw new APIError({
code: "BAD_REQUEST",
message: "Der eingegebene Verifizierungscode ist ungültig.",
});
}
await prisma.benutzer.update({
where: {
email: email.toLowerCase(),
},
data: {
verified: true,
},
});
},
});

View File

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

View File

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

View File

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

View File

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

View File

@@ -23,8 +23,7 @@ export const PUT = defineApiRoute({
output: z.object({
id: z.string({ description: "Die id des Bildes." }),
}),
middleware: maybeAuthorizationMiddleware,
async fetch(input, context, user) {
async fetch(input) {
const data = input.data;
if (!isBase64(data, { mimeRequired: true })) {
@@ -44,7 +43,6 @@ export const PUT = defineApiRoute({
id,
kategorie: input.kategorie,
name: input.name,
benutzer_id: user ? user.id : null,
},
});
@@ -93,11 +91,11 @@ export const DELETE = defineApiRoute({
await prisma.bild.delete({
where: {
id: input.id,
OR: [{
benutzer_id: user.id
}, {
benutzer_id: null
}]
aufnahme: {
benutzer: {
id: user.id,
},
},
},
});
} else {
@@ -109,8 +107,6 @@ export const DELETE = defineApiRoute({
});
}
} catch (e) {
console.log(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.ADMIN) {
if (user.rolle === Enums.BenutzerRolle.USER) {
await sendAusweisGespeichertMail(user, ctx.params.id as string)
}
},

View File

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

View File

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

View File

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

View File

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

View File

@@ -51,10 +51,7 @@ export const PUT = defineApiRoute({
// Wir erstellen eine Mollie Payment Referenz und eine neue Rechnung in unserer Datenbank, daraufhin geben
// wir eine Checkout URL zurück auf die der Nutzer weitergeleitet werden kann.
let { ausweis_id, ausweisart, bezahlmethode, services, partner_code } = input;
// Manchmal startet der partner_code mit Gänsefüßchen, das entfernen wir hier.
partner_code = partner_code?.replace(/"/g, "") ?? null;
const { ausweis_id, ausweisart, bezahlmethode, services, partner_code } = input;
const adapter = getPrismaAusweisAdapter(ausweis_id);

View File

@@ -50,7 +50,7 @@ export const PUT = defineApiRoute({
const url = new URL("https://api.trello.com/1/cards")
url.searchParams.append("name", input.titel)
url.searchParams.append("desc", `User: ${input.email}\n\nDescription: ${input.beschreibung}\n\nKategorie: ${category}\n\nTelefon: ${(input.metadata as Record<string, string>).telefon || "Nicht angegeben"}`)
url.searchParams.append("desc", `User: ${input.email}\n\nDescription: ${input.beschreibung}\n\nCategory: ${category}`)
url.searchParams.append("pos", "top")
url.searchParams.append("idLabels",
(category &&
@@ -58,7 +58,7 @@ export const PUT = defineApiRoute({
"650e909fdc09629a4d6d495d")
url.searchParams.append("key", "e057eb39018368ea96e456c753ac41b4")
url.searchParams.append("idList", "67d75ca7403fd22c49bc7447")
url.searchParams.append("token", "ATTA6f1774d98472db1897a2373ee7b55ab15f218c2445b6609dfef3071fe5203a90DB15678A")
url.searchParams.append("token", "ATTA8b65b3587ab627167038cc32a3460650973eb181cde01dabb208ca1e90ed5467AC06A4F2")
// Wir laden das Ticket zu Trello hoch.
const result = await fetch(url, {

View File

@@ -65,10 +65,10 @@ export const GET = defineApiRoute({
})),
output: z.array(BenutzerSchema),
middleware: authorizationMiddleware,
async fetch(input, context, benutzer) {
async fetch(input, context, admin) {
if ("id" in input) {
// Nur Admins oder der Benutzer selbst kann einen einzelnen Benutzer lesen
if (benutzer.rolle != Enums.BenutzerRolle.ADMIN && input.id != benutzer.id) {
//Only Admin can read other users
if (admin.rolle != Enums.BenutzerRolle.ADMIN && input.id != admin.id) {
return;
}
@@ -84,8 +84,8 @@ export const GET = defineApiRoute({
return [user];
} else {
// Nur Admins können nach mehreren Benutzern suchen
if (benutzer.rolle != Enums.BenutzerRolle.ADMIN) {
//Only admin can read many users
if (admin.rolle != Enums.BenutzerRolle.ADMIN ) {
return;
}
@@ -114,12 +114,9 @@ export const PUT = defineApiRoute({
id: IDWithPrefix
}),
async fetch(input) {
let { email, passwort, vorname, name } = input;
email = email.toLowerCase();
const existingUser = await prisma.benutzer.findUnique({
where: {
email
email: input.email
}
})
@@ -134,10 +131,10 @@ export const PUT = defineApiRoute({
const user = await prisma.benutzer.create({
data: {
email,
passwort: hashPassword(passwort),
vorname,
name,
email: input.email,
passwort: hashPassword(input.passwort),
vorname: input.vorname,
name: input.name,
id
}
})

View File

@@ -46,7 +46,7 @@ export const PATCH = defineApiRoute({
data: input
})
if (user.rolle !== Enums.BenutzerRolle.ADMIN) {
if (user.rolle === Enums.BenutzerRolle.USER) {
await sendAusweisGespeichertMail(user, ctx.params.id as string)
}
},
@@ -173,7 +173,7 @@ export const GET = defineApiRoute({
}
const ausweis = await prisma.verbrauchsausweisGewerbe.findUnique({
where: user.rolle !== Enums.BenutzerRolle.ADMIN ? {
where: user.rolle === Enums.BenutzerRolle.USER ? {
id,
benutzer_id: user.id
} : { id },

View File

@@ -47,7 +47,7 @@ export const PATCH = defineApiRoute({
data: input
})
if (user.rolle !== Enums.BenutzerRolle.ADMIN) {
if (user.rolle === Enums.BenutzerRolle.USER) {
await sendAusweisGespeichertMail(user, ctx.params.id as string)
}
},
@@ -174,7 +174,7 @@ export const GET = defineApiRoute({
}
const ausweis = await prisma.verbrauchsausweisWohnen.findUnique({
where: user.rolle !== Enums.BenutzerRolle.ADMIN ? {
where: user.rolle === Enums.BenutzerRolle.USER ? {
id,
benutzer_id: user.id
} : { id },

View File

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

24
src/pages/auth/code.astro Normal file
View File

@@ -0,0 +1,24 @@
---
import CodeModule from "../../modules/auth/CodeModule.svelte";
import MinimalLayout from "#layouts/MinimalLayout.astro";
import { validateAccessTokenServer } from "#server/lib/validateAccessToken";
const valid = await validateAccessTokenServer(Astro)
if (valid) {
return Astro.redirect("/dashboard")
}
const redirect = Astro.url.searchParams.get("redirect");
const email = Astro.url.searchParams.get("email");
if (!email) {
return Astro.redirect("/");
}
---
<MinimalLayout title="Verifizierung - IBCornelsen">
<CodeModule client:load {redirect} {email}></CodeModule>
</MinimalLayout>

View File

@@ -1,31 +1,26 @@
---
import AbrechnungTable from "#components/Abrechnung/AbrechnungTable.svelte";
import BlankLayout from "#layouts/BlankLayout.astro";
import { getProvision } from "#lib/provision";
import { extrahiereAusweisAusFeldMitMehrerenAusweisen } from "#lib/server/ausweis";
import AbrechungTable from "#components/Abrechnung/AbrechungTable.svelte";
import { Enums, prisma } from "#lib/server/prisma";
import { getCurrentUser } from "#lib/server/user";
import moment from "moment-timezone";
import moment from "moment";
moment.locale("de");
moment.tz.setDefault("Europe/Berlin");
const start = moment(Astro.url.searchParams.get("start"));
const end = moment(Astro.url.searchParams.get("end"));
const start = moment(Astro.url.searchParams.get("start"))
const end = moment(Astro.url.searchParams.get("end"))
let startdatum = start.isValid() ? start.startOf("day").toDate() : moment().startOf("month").toDate();
let enddatum = end.isValid() ? end.endOf("day").toDate() : moment().endOf("month").toDate();
let startdatum = start.toDate();
let enddatum = end.toDate();
// Wir dürfen die Abrechnung erst ab Juni starten lassen.
if (startdatum < moment().set("year", 2025).set("month", 5).set("date", 1).toDate()) {
startdatum = moment().set("year", 2025).set("month", 5).set("date", 1).set("hour", 0).set("minute", 0).set("second", 0).toDate();
enddatum = moment().set("year", 2025).set("month", 5).set("date", 1).set("hour", 0).set("minute", 0).set("second", 0).endOf("month").toDate();
}
const benutzer = await getCurrentUser(Astro);
const benutzer = await getCurrentUser(Astro)
if (!benutzer) {
return Astro.redirect("/404");
return Astro.redirect("/404")
}
const provisionen={
[Enums.Ausweisart.VerbrauchsausweisWohnen]: 10,
[Enums.Ausweisart.BedarfsausweisWohnen]: 10,
[Enums.Ausweisart.VerbrauchsausweisGewerbe]: 10,
}
// $kommission = db()->one("SELECT abr_va, abr_ba, abr_vanw FROM users WHERE resellercode = :resellercode", ["resellercode" => $resellercode]);
@@ -34,206 +29,306 @@ let bestellungen;
if (start.isValid() && end.isValid()) {
bestellungen = await prisma.rechnung.findMany({
where: {
partner_code: benutzer.partner_code,
OR: [
{
partner_code: "immowelt",
OR: [{
verbrauchsausweis_gewerbe: {
ausgestellt: true,
},
ausgestellt: true
}
},
{
bedarfsausweis_wohnen: {
ausgestellt: true,
},
ausgestellt: true
}
},
{
verbrauchsausweis_wohnen: {
ausgestellt: true,
ausgestellt: true
}
}],
AND: [{
created_at: {
gte: startdatum
},
}, {
created_at: {
lte: enddatum
},
],
AND: [
{
erstellt_am: {
gte: startdatum,
},
},
{
erstellt_am: {
lte: enddatum,
},
},
],
}]
},
orderBy: {
erstellt_am: "desc",
created_at: "desc"
},
include: {
bedarfsausweis_wohnen: {
include: {
aufnahme: {
include: {
objekt: true,
},
},
},
},
verbrauchsausweis_gewerbe: {
include: {
aufnahme: {
include: {
objekt: true,
},
},
},
},
verbrauchsausweis_wohnen: {
include: {
aufnahme: {
include: {
objekt: true,
},
},
},
},
},
bedarfsausweis_wohnen: true,
verbrauchsausweis_gewerbe: true,
verbrauchsausweis_wohnen: true
}
});
} else {
bestellungen = await prisma.rechnung.findMany({
where: {
partner_code: benutzer.partner_code,
OR: [
{
partner_code: "immowelt",
OR: [{
verbrauchsausweis_gewerbe: {
ausgestellt: true,
},
ausgestellt: true
}
},
{
bedarfsausweis_wohnen: {
ausgestellt: true,
},
ausgestellt: true
}
},
{
verbrauchsausweis_wohnen: {
ausgestellt: true,
},
},
],
ausgestellt: true
}
}]
},
orderBy: {
erstellt_am: "desc",
created_at: "desc"
},
include: {
bedarfsausweis_wohnen: {
include: {
aufnahme: {
include: {
objekt: true,
},
},
},
},
verbrauchsausweis_gewerbe: {
include: {
aufnahme: {
include: {
objekt: true,
},
},
},
},
verbrauchsausweis_wohnen: {
include: {
aufnahme: {
include: {
objekt: true,
},
},
},
},
},
bedarfsausweis_wohnen: true,
verbrauchsausweis_gewerbe: true,
verbrauchsausweis_wohnen: true
}
});
}
// Wann wurde der partner_code zum ersten mal benutzt?
if (!startdatum) {
startdatum = (
await prisma.rechnung.findFirst({
const partnerCodeErstesMal = (await prisma.rechnung.findFirst({
select: {
erstellt_am: true,
created_at: true
},
where: {
partner_code: benutzer.partner_code,
OR: [
{
partner_code: "immowelt",
OR: [{
verbrauchsausweis_gewerbe: {
ausgestellt: true,
},
ausgestellt: true
}
},
{
bedarfsausweis_wohnen: {
ausgestellt: true,
},
ausgestellt: true
}
},
{
verbrauchsausweis_wohnen: {
ausgestellt: true,
},
},
],
erstellt_am: {
gte: moment().set("year", 2020).set("dayOfYear", 1).toDate(),
},
ausgestellt: true
}
}],
created_at: {
gte: moment().set("year", 2020).set("dayOfYear", 1).toDate()
}
},
orderBy: {
erstellt_am: "asc",
},
})
)?.erstellt_am || moment().startOf("month").toDate();
}
const provisionen = await prisma.provisionen.findMany({
where: {
benutzer_id: benutzer.id
created_at: "asc"
}
})
bestellungen = bestellungen.map((bestellung) =>
extrahiereAusweisAusFeldMitMehrerenAusweisen(bestellung)
);
}))?.created_at
let provision = 0;
const ausweisarten: string[] = [];
for (const bestellung of bestellungen) {
const { provision_betrag, provision_prozent } = getProvision(bestellung.ausweis.ausweisart, bestellung.ausweis.ausweistyp, provisionen);
provision += provision_betrag;
if (bestellung.verbrauchsausweis_wohnen) {
ausweisarten.push(Enums.Ausweisart.VerbrauchsausweisWohnen)
provision += provisionen[Enums.Ausweisart.VerbrauchsausweisWohnen]
}
if (bestellung.bedarfsausweis_wohnen) {
ausweisarten.push(Enums.Ausweisart.BedarfsausweisWohnen)
provision += provisionen[Enums.Ausweisart.BedarfsausweisWohnen]
}
if (bestellung.verbrauchsausweis_gewerbe) {
ausweisarten.push(Enums.Ausweisart.VerbrauchsausweisGewerbe)
provision += provisionen[Enums.Ausweisart.VerbrauchsausweisGewerbe]
}
}
---
<BlankLayout title="Monatliche Abrechnung">
<AbrechnungTable
bestellungen={bestellungen}
{provisionen}
startdatum={startdatum}
enddatum={enddatum}
email={benutzer.email}
client:load
/>
<div class="fixed bottom-0 left-0 right-0 bg-white p-4 shadow">
<div class="flex justify-between items-center">
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>Reporting | online-energieausweis.org</title>
<meta charset="utf-8">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<script type="text/javascript" src="https://cdn.jsdelivr.net/jquery/latest/jquery.min.js"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/momentjs/latest/moment.min.js"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/daterangepicker/daterangepicker.min.js"></script>
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/daterangepicker/daterangepicker.css" />
<link rel="stylesheet" href="./main.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
</head>
<style>
body {
font-family: arial;
}
#inputwrap {
position: fixed;
top: 0;
left: 10%;
width: 70%;
padding-top: 10px;
padding-bottom: 10px;
background: #fff;
}
#cal {
margin-right: 10px !important;
}
#demo {
width: 76%;
display: inline-block;
}
table tr,
td {
border: 0.1em solid #000;
padding: 0;
margin: 0;
}
#QTT {
border-collapse: collapse;
width: 70%;
margin-top: 8em;
margin-left: 10%;
table-layout: auto;
}
#QTT thead td {
background: #ff7d26;
font-weight: bold;
}
#QTT tr:nth-child(even) {
background-color: #f2f2f2;
}
#QTT td {
padding: 0.4em 0.4em 0.4em 0.4em;
text-align: right;
}
#logo1 {
display: inline-block;
margin-right: 1em;
margin-top: -3px;
}
#logo2 {
width: 18%;
margin-bottom: 0.5em;
float: right;
padding-top: -1px;
margin-right: -5px;
}
</style>
<div id='inputwrap' class='form-group' >
<div style='display:flex; justify-content: space-between; align-items:center;'>
<img id='logo1' src='https://widget.ib-cornelsen.de/OEA_WIDGETS/img/IBC-logo.png' alt='IBCornelsen' />
<h5 style='margin-top: 10px;'><b>Erziehlte Conversions von {benutzer.email}</b></h5>
</div>
<input type='text' id='demo' class='form-control' name='demo' value='' placeholder='Bitte Zeitraum auswählen' />
</div>
<AbrechungTable bestellungen={bestellungen} {provisionen} {partnerCodeErstesMal}></AbrechungTable>
<div id="total" class="footer">
<div class="inner">
<div>
<p>
Abrechnungsbetrag gesamt: <b>{provision.toFixed(2)} €</b>
</p>
<p id="betrag_gesamt">Abrechnungsbetrag gesamt: <b>{provision} €</b></p>
</div>
<a
target="_blank"
rel="noreferrer noopener"
class="bg-secondary text-white px-4 py-2 rounded-lg hover:bg-secondary-focus"
href=`/dashboard/abrechnung/monatlich.pdf?d=${moment().subtract(1, "month").format("YYYY-MM")}`
>PDF für letzten Monat generieren.</a
>
<a target='_blank' rel='noreferrer noopener' href=`/user/abrechnung/pdf.php?month=${moment().subtract(1, "month").get("month")}&year=${moment().subtract(1, "month").get("year")}`>PDF für letzten Monat generieren.</a>
</div>
</div>
</BlankLayout>
</div>
<script type="text/javascript">
$('#demo').daterangepicker({
"showDropdowns": true,
"minYear": 2019,
ranges: {
'Heute': [moment(), moment()],
'Gestern': [moment().subtract(1, 'days'), moment().subtract(1, 'days')],
'letzte 7 Tage': [moment().subtract(6, 'days'), moment()],
'letzte 30 Tage': [moment().subtract(29, 'days'), moment()],
'dieser Monat': [moment().startOf('month'), moment().endOf('month')],
'letzter Monat': [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')]
},
"locale": {
"format": "DD/MM/YYYY",
"separator": " - ",
"applyLabel": "Übernehmen",
"cancelLabel": "Abrechen",
"fromLabel": "Von",
"toLabel": "Bis",
"customRangeLabel": "Benutzerdefiniert",
"weekLabel": "W",
"daysOfWeek": [
"So",
"Mo",
"Di",
"Mi",
"Do",
"Fr",
"Sa"
],
"monthNames": [
"Januar",
"Februar",
"März",
"April",
"Mai",
"Juni",
"Juli",
"August",
"September",
"Oktober",
"November",
"Dezember"
],
"firstDay": 1
},
"autoUpdateInput": false,
"alwaysShowCalendars": true,
"startDate": "<?php echo $day_start_display; ?>",
"endDate": "<?php echo $day_end_display; ?>",
"minDate": "01/10/2019"
}, function(start, end, label) {
var Pstart = start.format('MM/DD/YYYY');
var Pend = end.format('MM/DD/YYYY');
$("#start").val(Pstart);
$("#end").val(Pend);
$('#FO').submit();
});
// $("#demo").val(`Conversions im Zeitraum: <?php echo $Pall; ?>`);
</script>
<body>
<form id='FO' method='GET' action='index.php'>
<input type="hidden" id="start" name="start" value='' />
<input type="hidden" id="end" name="end" value='' />
</form>
<script>
</script>
</body>
</html>

View File

@@ -1,146 +0,0 @@
---
import abrechnungTemplateHTML from "../../../templates/pdf/abrechnung.handlebars?raw";
import puppeteer from "puppeteer";
import Handlebars from "handlebars";
import moment from "moment";
import { getCurrentUser } from "#lib/server/user";
import { prisma } from "#lib/server/prisma";
import { extrahiereAusweisAusFeldMitMehrerenAusweisen } from "#lib/server/ausweis";
import { getProvision } from "#lib/provision";
moment.locale("de");
moment.tz.setDefault("Europe/Berlin");
const datum = moment(Astro.url.searchParams.get("d")).set("date", 1).set("hour", 0).set("minute", 0).set("second", 0);
const benutzer = await getCurrentUser(Astro);
// Wir dürfen die Abrechnung erst ab Juni starten lassen.
if (datum.isBefore(moment().set("year", 2025).set("month", 4).endOf("month"))) {
return Astro.redirect("/404")
}
if (!benutzer) {
return Astro.redirect("/404");
}
let bestellungen = await prisma.rechnung.findMany({
where: {
partner_code: benutzer.partner_code,
OR: [
{
verbrauchsausweis_gewerbe: {
ausgestellt: true,
},
},
{
bedarfsausweis_wohnen: {
ausgestellt: true,
},
},
{
verbrauchsausweis_wohnen: {
ausgestellt: true,
},
},
],
AND: [
{
erstellt_am: {
gte: datum.startOf("month").toDate(),
},
},
{
erstellt_am: {
lte: datum.endOf("month").toDate(),
},
},
],
},
orderBy: {
erstellt_am: "desc",
},
include: {
bedarfsausweis_wohnen: {
include: {
aufnahme: {
include: {
objekt: true,
},
},
},
},
verbrauchsausweis_gewerbe: {
include: {
aufnahme: {
include: {
objekt: true,
},
},
},
},
verbrauchsausweis_wohnen: {
include: {
aufnahme: {
include: {
objekt: true,
},
},
},
},
},
});
const provisionen = await prisma.provisionen.findMany({
where: {
benutzer_id: benutzer.id
}
})
const ausweisBestellungen = bestellungen.map(bestellung => extrahiereAusweisAusFeldMitMehrerenAusweisen(bestellung));
const browser = await puppeteer.launch({
headless: true,
args: ["--no-sandbox", "--disable-setuid-sandbox"],
});
const page = await browser.newPage();
// Wir splitten die Daten in Blöcke auf, der erste Block hat 11 Einträge, die folgenden Blöcke haben 15 Einträge.
const blocks = [];
const firstBlock = ausweisBestellungen.slice(0, 16);
const remainingBlocks = ausweisBestellungen.slice(16);
blocks.push(firstBlock);
for (let i = 0; i < remainingBlocks.length; i += 20) {
blocks.push(remainingBlocks.slice(i, i + 20));
}
Handlebars.registerHelper("get-provision-prozent", function (ausweisart, ausweistyp) {
const { provision_prozent } = getProvision(ausweisart, ausweistyp, provisionen);
return provision_prozent || 0;
});
Handlebars.registerHelper("get-provision-betrag", function (ausweisart, ausweistyp) {
const { provision_betrag } = getProvision(ausweisart, ausweistyp, provisionen);
return provision_betrag ? provision_betrag.toFixed(2) : "0.00";
});
const gesamt = ausweisBestellungen.reduce((acc, bestellung) => {
const { provision_betrag } = getProvision(bestellung.ausweis.ausweisart, bestellung.ausweis.ausweistyp, provisionen);
return acc + (provision_betrag || 0);
}, 0).toFixed(2);
const template = Handlebars.compile(abrechnungTemplateHTML);
const html = template({ monat: datum.format("MMMM YYYY"), bestellungen: blocks, heute: moment().format("DD.MM.YYYY"), plz: benutzer.plz, ort: benutzer.ort, adresse: benutzer.adresse, firma: benutzer.firma, email: benutzer.email, gesamt });
await page.goto(`data:text/html;charset=UTF-8,${encodeURIComponent(html)}`, {
waitUntil: "networkidle0",
});
const pdf = await page.pdf({ format: "A4" });
await browser.close();
return new Response(pdf, {
headers: {
"Content-Type": "application/pdf",
"Content-Disposition": `attachment; filename="Abrechnung_${datum.format("YYYY_MM")}.pdf"`,
},
});
---

View File

@@ -61,5 +61,5 @@ Astro.cookies.set(API_ACCESS_TOKEN_COOKIE_NAME, accessToken, {
expires: moment().add(30, "minutes").toDate()
})
return Astro.redirect("/dashboard");
return Astro.redirect("/dashboard")
---

View File

@@ -28,7 +28,7 @@ if (!user) {
const aufnahme = await prisma.aufnahme.findUnique({
where: user.rolle !== Enums.BenutzerRolle.ADMIN ? {
where: user.rolle === Enums.BenutzerRolle.USER ? {
benutzer: {
id: user.id
},

View File

@@ -15,7 +15,7 @@ if (!user) {
const totalPageCount = await prisma.aufnahme.count({
where:
user.rolle !== Enums.BenutzerRolle.ADMIN
user.rolle === Enums.BenutzerRolle.USER
? {
benutzer: {
id: user.id,
@@ -27,7 +27,7 @@ const totalPageCount = await prisma.aufnahme.count({
let ausweis;
// Wir fragen den neuesten Ausweis ab
// Falls der Nutzer ein Admin ist dann kommt der ganz neueste ansonsten der neueste des eingeloggten Benutzers.
if (user.rolle !== Enums.BenutzerRolle.ADMIN) {
if (user.rolle === Enums.BenutzerRolle.USER) {
const adapter = getPrismaAusweisAdapter(id);
ausweis = await adapter?.findUnique({
where: {

View File

@@ -1,6 +1,8 @@
---
import { Enums, prisma } from "#lib/server/prisma";
import UserLayout from "#layouts/DashboardLayout.astro";
import { getCurrentUser } from "#lib/server/user";
import moment from "moment";
const page = Number(Astro.url.searchParams.get("p"));
@@ -12,7 +14,7 @@ if (!user) {
const totalPageCount = await prisma.aufnahme.count({
where:
(user.rolle !== Enums.BenutzerRolle.ADMIN)
user.rolle === Enums.BenutzerRolle.USER
? {
benutzer: {
id: user.id,
@@ -21,17 +23,14 @@ const totalPageCount = await prisma.aufnahme.count({
: {},
});
if ((page < 1 || page > totalPageCount) && totalPageCount > 0) {
if (page < 1 || page > totalPageCount) {
return Astro.redirect("/dashboard/objekte?p=1");
} else if (totalPageCount === 0) {
return Astro.redirect("/dashboard/objekte/leer");
}
let result: { id: string; updated_at: Date }[] = [];
// Wir fragen den neuesten Ausweis ab
// Falls der Nutzer ein Admin ist dann kommt der ganz neueste ansonsten der neueste des eingeloggten Benutzers.
if (user.rolle !== Enums.BenutzerRolle.ADMIN) {
if (user.rolle === Enums.BenutzerRolle.USER) {
result =
await prisma.$queryRaw`SELECT id, updated_at FROM "VerbrauchsausweisWohnen" WHERE benutzer_id = ${user.id} UNION ALL
SELECT id, updated_at FROM "VerbrauchsausweisGewerbe" WHERE benutzer_id = ${user.id} UNION ALL
@@ -41,6 +40,15 @@ if (user.rolle !== Enums.BenutzerRolle.ADMIN) {
SELECT id, updated_at FROM "GEGNachweisGewerbe" WHERE benutzer_id = ${user.id}
ORDER BY updated_at DESC LIMIT 1 OFFSET ${page - 1}`;
} else {
const date = moment().subtract(2, "hours").toDate()
// SELECT id, updated_at FROM "VerbrauchsausweisWohnen" WHERE created_at >= ${date} AND bestellt = ${true} UNION ALL
// SELECT id, updated_at FROM "VerbrauchsausweisGewerbe" WHERE created_at >= ${date} AND bestellt = ${true} UNION ALL
// SELECT id, updated_at FROM "BedarfsausweisWohnen" WHERE created_at >= ${date} AND bestellt = ${true} UNION ALL
// SELECT id, updated_at FROM "BedarfsausweisGewerbe" WHERE created_at >= ${date} AND bestellt = ${true} UNION ALL
// SELECT id, updated_at FROM "GEGNachweisWohnen" WHERE created_at >= ${date} AND bestellt = ${true} UNION ALL
// SELECT id, updated_at FROM "GEGNachweisGewerbe" WHERE created_at >= ${date} AND bestellt = ${true}
result =
await prisma.$queryRaw`SELECT id, updated_at FROM "VerbrauchsausweisWohnen" WHERE ausgestellt = ${false} AND bestellt = ${true} UNION ALL
SELECT id, updated_at FROM "VerbrauchsausweisGewerbe" WHERE ausgestellt = ${false} AND bestellt = ${true} UNION ALL
@@ -52,6 +60,10 @@ if (user.rolle !== Enums.BenutzerRolle.ADMIN) {
}
if (result.length > 0) {
return Astro.redirect(`/dashboard/objekte/${result[0].id}?p=${page}`);
return Astro.redirect(`/dashboard/objekte/${result[0].id}?p=${page}`)
}
---
<UserLayout title="Objekte" {user} besteller={null}>
<p>Keine Ausweise konnten gefunden werden.</p>
</UserLayout>

View File

@@ -1,15 +0,0 @@
---
import DashboardLayout from "#layouts/DashboardLayout.astro";
import { getCurrentUser } from "#lib/server/user";
const user = await getCurrentUser(Astro);
if (!user) {
return Astro.redirect("/auth/login");
}
---
<DashboardLayout title="Objekte" user={user} besteller={null}>
<p>Sie haben bisher keine Ausweise erstellt. Klicken sie <a href="/energieausweis-erstellen/verbrauchsausweis-wohngebaeude/">hier</a>, um einen neuen Ausweis zu erstellen.</p>
</DashboardLayout>

View File

@@ -0,0 +1,31 @@
---
import KaufabschlussModule from "#modules/KaufabschlussModule.svelte";
import AusweisLayout from "#layouts/AusweisLayoutPruefung.astro";
import { Enums } from "#lib/client/prisma";
import { createCaller } from "src/astro-typesafe-api-caller";
// Man sollte nur auf diese Seite kommen, wenn ein Ausweis bereits vorliegt und in der Datenbank abgespeichert wurde.
const uid = Astro.url.searchParams.get("uid");
if (!uid) {
return Astro.redirect("/404");
}
const caller = createCaller(Astro);
const ausweis = await caller.v1.verbrauchsausweisWohnen.get({
uid
})
const user = await caller.v1.benutzer.self();
if (!ausweis || !user) {
return Astro.redirect("/404");
}
---
<AusweisLayout title="Kundendaten Aufnehmen - IBCornelsen">
<KaufabschlussModule user={user} ausweis={ausweis} aktiveBezahlmethode={Enums.Bezahlmethoden.paypal} client:load></KaufabschlussModule>
</AusweisLayout>

View File

@@ -1,87 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Abrechnung</title>
<link href="https://unpkg.com/tailwindcss@2.0.1/dist/tailwind.min.css" rel="stylesheet">
</head>
<body>
{{#each bestellungen}}
<header class="p-12 flex flex-row items-center justify-between" style="border-bottom: 12px #f37e3c solid;">
<img src="https://online-energieausweis.org/images/header/logo-big.png" alt="" class="h-24">
<div class="flex flex-col">
<p>fon 040 · 209339850</p>
<p>fax 040 · 209339859</p>
<p class="font-semibold">online-energieausweis.org</p>
</div>
</header>
<article class="py-8 px-16">
{{#if @first}}
<div class="flex flex-col gap-4">
<div class="flex flex-row">
<p class="text-sm">IB Cornelsen · Katendeich 5A · 21035 Hamburg</p>
</div>
<div class="flex flex-col">
<p>{{ @root.firma }}</p>
<p>{{ @root.adresse }}</p>
<p>{{ @root.plz }} {{ @root.ort }}</p>
</div>
</div>
{{/if}}
<div class="flex flex-col gap-4 mt-12">
{{#if @first}}
<div class="flex flex-row justify-between items-center">
<div class="flex flex-col">
<p class="font-semibold">Erzielte Conversions {{ @root.monat }}</p>
<p>Erstellt am {{ @root.heute }}</p>
</div>
<p class="font-semibold">Gesamt {{ @root.gesamt }} €</p>
</div>
{{/if}}
<table class="table border-collapse border border-black">
<thead>
<tr class="h-16">
<th class="text-center text-sm border-black border" style="background-color: #f37e3c;">ID - Datum</th>
<th class="text-center text-sm border-black border" style="background-color: #f37e3c;">Produkt</th>
<th class="text-center text-sm border-black border" style="background-color: #f37e3c;">Produktpreis</th>
<th class="text-center text-sm border-black border" style="background-color: #f37e3c;">Provision in %</th>
<th class="text-center text-sm border-black border" style="background-color: #f37e3c;">Betrag Netto</th>
</tr>
</thead>
<tbody>
{{#each this}}
<tr>
{{#with ausweis}}
<td class="border-black border p-1">
<p class="text-sm">{{ id }}</p>
<p class="text-sm">{{ createdAt }}</p>
</td>
<td class=" border-black border p-1 text-sm text-center">
{{ ausweisart }}
</td>
{{/with}}
<td class=" border-black border p-1 font-semibold text-sm text-center">
{{ betrag }}
</td>
{{#with ausweis}}
<td class=" border-black border p-1 text-sm text-center">
{{get-provision-prozent ausweisart ausweistyp}} %
</td>
<td class=" border-black border p-1 text-sm text-center">
{{get-provision-betrag ausweisart ausweistyp}}
</td>
{{/with}}
</tr>
{{/each}}
</tbody>
</table>
</div>
</article>
<footer class="px-16 py-6 flex flex-row justify-between items-center fixed bottom-0 left-0 w-full" style="border-top: 12px #f37e3c solid;">
<p class="font-semibold">Copyright © 2018 · IB Cornelsen</p>
<p class="font-semibold">info@online-energieausweis.org</p>
</footer>
{{/each}}
</body>
</html>

View File

@@ -39,16 +39,40 @@ fi
if [[ "$SKIP_BACKUP" == false ]]; then
echo "📦 Backup wird erstellt..."
docker exec -t "$CONTAINER_NAME" pg_dumpall -c -U "$DB_USER" | brotli --quality=1 > "$FILE_NAME"
docker exec -t "$CONTAINER_NAME" pg_dumpall -c -U "$DB_USER" | brotli > "$FILE_NAME"
echo "✅ Backup abgeschlossen: $FILE_NAME"
fi
echo "🧨 Alle Daten aus allen Tabellen werden gelöscht..."
# Generate and run TRUNCATE statements for all tables in the public schema
docker exec -i "$CONTAINER_NAME" psql -U "$DB_USER" "postgres" <<'EOSQL'
DROP DATABASE IF EXISTS main;
CREATE DATABASE main WITH OWNER main ENCODING 'UTF8';
docker exec -i "$CONTAINER_NAME" psql -U "$DB_USER" "$DB_NAME" <<'EOSQL'
DO $$
DECLARE
r RECORD;
sql TEXT := '';
BEGIN
-- Truncate all tables
FOR r IN
SELECT tablename
FROM pg_tables
WHERE schemaname = 'public'
LOOP
sql := sql || FORMAT('DROP TABLE public.%I CASCADE;', r.tablename);
END LOOP;
-- Drop all sequences
FOR r IN
SELECT sequence_name
FROM information_schema.sequences
WHERE sequence_schema = 'public'
LOOP
sql := sql || FORMAT('DROP SEQUENCE IF EXISTS public.%I CASCADE;', r.sequence_name);
END LOOP;
EXECUTE sql;
END
$$;
EOSQL
echo "✅ Alle Tabellen gelöscht und Schema zurückgesetzt."