E-Mail-Versand von SMTP auf mailto-Weiterleitung umgestellt
Server-seitiger SMTP-Versand (Zugangsdaten, /api/email/senden, /api/email/test) vollständig entfernt und durch mailto: ersetzt: PDF wird lokal generiert und heruntergeladen, danach öffnet sich das E-Mail-Programm des Nutzers mit vorausgefülltem Empfänger. Vorbereitung für die Docker-Containerisierung, da dort nur ein Admin (der Betreiber) existiert und die Lösung ohne rollenspezifische Ausnahmen für jedes Konto gleichermassen funktionieren muss. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
af43371a5b
commit
6bfb7a07b6
3 files changed
+61
-149
No files matched your search
@@ -31,7 +31,7 @@ Default login after first start: `admin` / `admin` (created automatically by `in
|
||||
|
||||
**Single-file Flask backend, vanilla-JS SPA frontend, zero build tooling.**
|
||||
|
||||
- `app.py` — the entire backend: all `/api/*` routes, SQLite access, auth, SMTP email sending, file upload handling. There's no blueprint/module split — everything lives in this one file.
|
||||
- `app.py` — the entire backend: all `/api/*` routes, SQLite access, auth, file upload handling. There's no blueprint/module split — everything lives in this one file. There is no server-side email sending — see "Email: mailto hand-off, not SMTP" below.
|
||||
- `templates/index.html` — the entire frontend. One Jinja template that is actually a client-rendered SPA: vanilla JS, string-templated HTML, no framework, no bundler. Key conventions in the `<script>` block:
|
||||
- `go(name, extra)` is the client-side router; it maps a page name to a `pg*()` render function (`pgDash`, `pgPatienten`, `pgKatalog`, `pgBackend`, …).
|
||||
- `S(id, html)` sets innerHTML by id; `R(html, title, actions)` sets the main content/title/toolbar in one call.
|
||||
@@ -44,7 +44,7 @@ Default login after first start: `admin` / `admin` (created automatically by `in
|
||||
- **Auth**: session-cookie based. `_get_current_user()` / `_check_admin()` read `session["nutzer_id"]`; `login_required` / `admin_required` decorators guard individual routes, while `global_auth_check()` (a Flask `before_request` hook) blanket-blocks any `/api/*` path not in the small public allowlist (`/api/auth/login`, `/api/auth/me`, `/api/einstellungen/public`). Two roles exist: `admin` and `nutzer`. Passwords are hashed with `werkzeug.security` (PBKDF2).
|
||||
- **Patient isolation**: `patienten.nutzer_id` (FK to `nutzer.id`) records who owns each patient — set automatically to the creating user on `POST /api/patienten`. Two helpers in `app.py` enforce it everywhere: `_get_owned_patient(pid)` (single-patient routes — returns the row if the current user is the owner or an admin, else `None` → caller returns 404) and `_scope_sql(column="id")` (list/aggregate routes — returns a SQL fragment + params to append to a `WHERE` clause; empty for admins, `AND {column} IN (SELECT id FROM patienten WHERE nutzer_id=?)` otherwise). `admin` role is exempt and always sees every patient. Every route touching `patienten` or a child table (`antraege`, `leistungen`, `aufgaben`, `pflegetagebuch`, `dokumente`) must go through one of these two helpers — when adding a new patient-scoped route, follow the existing pattern rather than querying the table directly. `aufgaben.patient_id` is nullable in the schema, but the API now requires a `patient_id` from non-admin callers on creation (admins may still create patient-less tasks) so the isolation model stays consistent between write and read paths.
|
||||
- **File storage**: everything is local under `static/` — `static/fotos/<patient_id>.<ext>` (profile photos), `static/briefe/<patient_id>/…` (generated letter PDFs), `static/dokumente/<patient_id>/…` (uploaded documents), `static/logo/` (company branding). No object storage/CDN involved.
|
||||
- **Settings**: a generic key-value `einstellungen` table (accessed via `get_setting()`) holds SMTP credentials, company branding/address, and an (unused-in-current-flow) Claude API token — surfaced through `/api/admin/einstellungen`.
|
||||
- **Settings**: a generic key-value `einstellungen` table (accessed via `get_setting()`) holds company branding/address and an (unused-in-current-flow) Claude API token — surfaced through `/api/admin/einstellungen`.
|
||||
|
||||
## Planned architecture pivot (see `refactoring.md`)
|
||||
|
||||
@@ -89,12 +89,21 @@ The product owner wants the app to cost **4.99 €/month per account**, provider
|
||||
- Frontend: `curAbo`/`aboBlockiert` globals are set in `initApp()` from `GET /api/abo/status`. `go(name)` redirects any navigation to the `meinabo` page while `aboBlockiert` is true, so a blocked account always lands on its own status/pay screen instead of a broken page. `pgMeinAbo()` is that page (status, price, stub PayPal button) and doubles as the always-available "check my subscription" view when unblocked.
|
||||
- When actually integrating PayPal: replace `/api/abo/aktivieren`'s stub body with real Checkout/Subscription creation, add a webhook endpoint to update `abo_bis`/`abo_typ` on payment events, and only then flip `abo_pflicht` to `"1"`.
|
||||
|
||||
## Email: mailto hand-off, not SMTP
|
||||
|
||||
There is **no server-side email sending anywhere in the app** — no SMTP client, no credentials, no relay. This was a deliberate removal (not a missing feature) done ahead of Docker containerization, because post-Docker the operator (Daniel) is the sole admin with no per-role differentiation to lean on — any email solution had to work identically for every account, not just an admin-configured one. `mailto:` is the "universal command": every OS/browser resolves it to the user's own default mail client (iOS → Mail, Android → Gmail/Outlook/whatever is set as default, desktop → whatever is set as default), with zero server involvement and zero secrets to manage in the container.
|
||||
|
||||
- Previously removed: `smtplib`/`email.mime.*` imports, `_smtp_cfg()`, `_smtp_send()`, `POST /api/email/senden`, `POST /api/email/test`, the "E-Mail (SMTP)" Backend-Einstellungen card, and the `email_smtp_*` settings keys. None of this exists anymore — don't resurrect it as a "fallback"; the user explicitly chose full removal over keeping SMTP as a backup path.
|
||||
- Frontend flow (`sendMailModal(typ)` / `sendMail(typ)` in `templates/index.html`, shared by both Musterbrief and Widerspruch-Generator via the `typ` parameter): the modal collects Empfänger/Betreff/Anschreiben, then on send the existing client-side `html2pdf.js` PDF generation runs exactly as it already did for the plain "PDF herunterladen" button (`dlW()`/`dlM()`) — the blob is downloaded to the user's device **and** persisted to the patient's Akte via `POST /api/patienten/<pid>/briefe` — then `location.href='mailto:'+to+'?subject=...&body=...'` opens the user's mail client with recipient/subject/body pre-filled.
|
||||
- **Hard limitation, not a bug**: `mailto:` cannot carry attachments — this is a browser/OS security restriction, not something this app can work around. The email body includes a bracketed reminder ("Bitte die heruntergeladene Datei „…" anhängen, dann diese Zeile löschen") and the modal's info box explains the same thing before sending, so the manual-attach step is expected UX, not a silent gap.
|
||||
- If you add a third document type that needs "send by email", follow the same pattern: reuse the existing PDF-blob-download code for that document, then call the same `mailto:` construction — don't reintroduce a server-side send path.
|
||||
|
||||
## Demo account
|
||||
|
||||
A permanent `demo`/`demo` account (`nutzer.ist_demo=1`) exists for showing the app to prospects, created automatically in `init_db()` if missing. It's a completely ordinary `nutzer`-role account — no special-casing in the isolation model — with two exceptions layered on top:
|
||||
|
||||
- **Immune to the subscription paywall**: `_hat_aktiven_zugang()` returns `True` for `ist_demo` accounts unconditionally, *before* the `abo_pflicht` check. Don't remove this — a demo visitor must never see "Zahlung erforderlich" regardless of the operator's billing settings. (This was caught by testing, not designed in from the start — worth re-checking if `_hat_aktiven_zugang()` is ever refactored.)
|
||||
- **Cannot send real email**: `POST /api/email/senden` and `/api/email/test` both return 403 for `ist_demo` accounts, so a demo visitor can never trigger outbound mail through the operator's real SMTP relay.
|
||||
- **Email is a non-issue for the demo account**: since email sending was migrated to the client-side `mailto:` hand-off (see "Email: mailto hand-off, not SMTP" below), there is no server-side outbound relay left to abuse — a demo visitor's "send" click only opens *their own* local mail client via a `mailto:` link, never the operator's infrastructure. The earlier 403 block on `/api/email/senden`/`/api/email/test` for `ist_demo` accounts was removed along with those routes; no replacement guard was needed.
|
||||
|
||||
**Auto-reset every 6 hours**, lazily rather than via a background thread/cron (this app has no task scheduler and stays that way — a lazy check fits the existing "one Flask process, no queue" architecture and can't miss a tick across restarts): `_demo_reset_faellig()` compares the `einstellungen` key `demo_reset_at` against `now`; `_demo_reset()` deletes every patient owned by the demo account (cascades to antraege/leistungen/aufgaben/pflegetagebuch/dokumente via the existing `ON DELETE CASCADE` FKs — SQLite enforces this because `get_db()` sets `PRAGMA foreign_keys = ON`), then reseeds one fictional pre-filled patient (`_demo_seed()`: "Grete Musterfrau", Pflegegrad 3, one Antrag, two Leistungen, two Aufgaben, one Pflegetagebuch-Eintrag) and bumps `demo_reset_at`. The check/reset call happens in two places — `api_auth_login()` (fresh state at the start of every demo session) and `global_auth_check()` (catches a session that's still open when the 6h boundary passes mid-visit) — both call the same `_demo_reset_faellig()`/`_demo_reset()` pair, so there's one source of truth for "is a reset due."
|
||||
|
||||
|
||||
@@ -3,11 +3,7 @@ Pflegepilot Alpha – Flask + SQLite Web-App
|
||||
Vollständig mit 82-Leistungen-Katalog integriert
|
||||
"""
|
||||
import sqlite3, os, json, re, base64, urllib.request, urllib.error, time, functools
|
||||
import smtplib, io, zipfile
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.base import MIMEBase
|
||||
from email import encoders
|
||||
import io, zipfile
|
||||
from datetime import datetime, date, timedelta
|
||||
from flask import Flask, render_template, request, jsonify, g, session, Response
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
@@ -1228,88 +1224,6 @@ def api_tagebuch_delete(tid):
|
||||
db.commit()
|
||||
return jsonify({"ok": True})
|
||||
|
||||
# ─── API: E-Mail-Versand ─────────────────────────────────────────────────────
|
||||
def _smtp_cfg():
|
||||
db = get_db()
|
||||
rows = db.execute("SELECT schluessel, wert FROM einstellungen").fetchall()
|
||||
cfg = {r["schluessel"]: r["wert"] for r in rows}
|
||||
return {
|
||||
"server": cfg.get("email_smtp", "").strip(),
|
||||
"port": int(cfg.get("email_port", 587) or 587),
|
||||
"user": cfg.get("email_user", "").strip(),
|
||||
"password": cfg.get("email_pass", ""),
|
||||
"absender": (cfg.get("email_absender") or cfg.get("email_user", "")).strip(),
|
||||
"firma": cfg.get("firmenname", "Pflegepilot"),
|
||||
}
|
||||
|
||||
def _smtp_send(to, subject, body, pdf_base64="", pdf_dateiname="Dokument.pdf"):
|
||||
c = _smtp_cfg()
|
||||
if not c["server"] or not c["user"]:
|
||||
raise ValueError("SMTP nicht konfiguriert – bitte unter Backend → E-Mail einrichten.")
|
||||
msg = MIMEMultipart()
|
||||
msg["From"] = f"{c['firma']} <{c['absender']}>" if c["firma"] else c["absender"]
|
||||
msg["To"] = to
|
||||
msg["Subject"] = subject
|
||||
msg.attach(MIMEText(body + f"\n\n-- \n{c['firma']}", "plain", "utf-8"))
|
||||
if pdf_base64:
|
||||
data = base64.b64decode(pdf_base64)
|
||||
part = MIMEBase("application", "pdf")
|
||||
part.set_payload(data)
|
||||
encoders.encode_base64(part)
|
||||
safe = re.sub(r'[^\w\-.]', '_', pdf_dateiname)
|
||||
part.add_header("Content-Disposition", f'attachment; filename="{safe}"')
|
||||
msg.attach(part)
|
||||
with smtplib.SMTP(c["server"], c["port"], timeout=15) as srv:
|
||||
srv.ehlo()
|
||||
srv.starttls()
|
||||
srv.login(c["user"], c["password"])
|
||||
srv.send_message(msg)
|
||||
|
||||
@app.route("/api/email/senden", methods=["POST"])
|
||||
def api_email_senden():
|
||||
u = _get_current_user()
|
||||
if u and u["ist_demo"]:
|
||||
return jsonify({"error": "E-Mail-Versand ist im Demo-Konto nicht verfügbar."}), 403
|
||||
d = request.json or {}
|
||||
empfaenger = (d.get("empfaenger") or "").strip()
|
||||
betreff = d.get("betreff") or "Nachricht von Pflegepilot"
|
||||
nachricht = d.get("nachricht") or ""
|
||||
pdf_base64 = d.get("pdf_base64") or ""
|
||||
pdf_name = d.get("pdf_dateiname") or "Dokument.pdf"
|
||||
if not empfaenger:
|
||||
return jsonify({"error": "Empfänger-Adresse fehlt"}), 400
|
||||
try:
|
||||
_smtp_send(empfaenger, betreff, nachricht, pdf_base64, pdf_name)
|
||||
return jsonify({"ok": True})
|
||||
except ValueError as e:
|
||||
return jsonify({"error": str(e)}), 400
|
||||
except smtplib.SMTPAuthenticationError:
|
||||
return jsonify({"error": "SMTP-Authentifizierung fehlgeschlagen – Benutzername/Passwort prüfen."}), 500
|
||||
except smtplib.SMTPConnectError as e:
|
||||
return jsonify({"error": f"Verbindung fehlgeschlagen: {e}"}), 500
|
||||
except Exception as e:
|
||||
return jsonify({"error": f"Fehler beim Versand: {e}"}), 500
|
||||
|
||||
@app.route("/api/email/test", methods=["POST"])
|
||||
def api_email_test():
|
||||
u = _get_current_user()
|
||||
if u and u["ist_demo"]:
|
||||
return jsonify({"error": "E-Mail-Versand ist im Demo-Konto nicht verfügbar."}), 403
|
||||
try:
|
||||
c = _smtp_cfg()
|
||||
if not c["server"] or not c["user"]:
|
||||
return jsonify({"error": "SMTP nicht konfiguriert"}), 400
|
||||
_smtp_send(
|
||||
c["absender"],
|
||||
"Pflegepilot – Test-E-Mail",
|
||||
"Die SMTP-Konfiguration funktioniert korrekt.\n\nPflegepilot Alpha"
|
||||
)
|
||||
return jsonify({"ok": True})
|
||||
except ValueError as e:
|
||||
return jsonify({"error": str(e)}), 400
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
# ─── API: Kalender / Fristenüberwachung ──────────────────────────────────────
|
||||
@app.route("/api/kalender", methods=["GET"])
|
||||
def api_kalender():
|
||||
|
||||
@@ -1282,21 +1282,6 @@ async function pgBackend(){
|
||||
|
||||
// ── Zeile 2 ──
|
||||
'<div class="g2 mt3">'+
|
||||
// E-Mail SMTP
|
||||
'<div class="card">'+
|
||||
'<div class="ch"><span class="ct">E-Mail (SMTP)</span><span class="badge b-gr">Beta</span></div>'+
|
||||
'<div class="fg mb4">'+
|
||||
'<div class="fgrp"><label>Absender-Adresse</label><input id="e-email_absender" value="'+(s.email_absender||'')+'" placeholder="pflege@muster.de"></div>'+
|
||||
'<div class="fg c2"><div class="fgrp"><label>SMTP-Server</label><input id="e-email_smtp" value="'+(s.email_smtp||'')+'" placeholder="smtp.gmail.com"></div>'+
|
||||
'<div class="fgrp"><label>Port</label><input id="e-email_port" value="'+(s.email_port||'')+'" placeholder="587"></div></div>'+
|
||||
'<div class="fgrp"><label>SMTP-Benutzer</label><input id="e-email_user" value="'+(s.email_user||'')+'" placeholder="user@muster.de"></div>'+
|
||||
'<div class="fgrp"><label>SMTP-Passwort</label><input id="e-email_pass" type="password" value="'+(s.email_pass||'')+'" placeholder="••••••"></div>'+
|
||||
'</div>'+
|
||||
'<div class="fl g8" style="justify-content:flex-end">'+
|
||||
'<button class="btn" onclick="testEmail()"><i class="ti ti-send"></i>Test-E-Mail</button>'+
|
||||
'<button class="btn btn-p" onclick="saveEinst([\'email_absender\',\'email_smtp\',\'email_port\',\'email_user\',\'email_pass\'])"><i class="ti ti-check"></i>Speichern</button></div>'+
|
||||
'</div>'+
|
||||
|
||||
// Nutzerverwaltung
|
||||
'<div class="card">'+
|
||||
'<div class="ch"><span class="ct">Nutzerverwaltung</span>'+
|
||||
@@ -1313,8 +1298,6 @@ async function pgBackend(){
|
||||
'</div></div>'+
|
||||
'</div>'+
|
||||
|
||||
// ── Zeile 3 ──
|
||||
'<div class="g2 mt3">'+
|
||||
// Abo & Zahlung
|
||||
'<div class="card">'+
|
||||
'<div class="ch"><span class="ct">Abo & Zahlung</span>'+
|
||||
@@ -1332,14 +1315,6 @@ async function pgBackend(){
|
||||
'<span class="badge b-b" style="padding:6px 12px"><i class="ti ti-settings-2" style="margin-right:4px"></i>Admin</span>');
|
||||
}
|
||||
async function adminLogout(){await doLogout();}
|
||||
async function testEmail(){
|
||||
const btn=event?.target;
|
||||
if(btn){btn.disabled=true;btn.innerHTML='<i class="ti ti-loader-2"></i>Sende…';}
|
||||
const r=await api('/api/email/test',{method:'POST'});
|
||||
if(btn){btn.disabled=false;btn.innerHTML='<i class="ti ti-send"></i>Test-E-Mail';}
|
||||
if(r.ok)toast('Test-E-Mail erfolgreich gesendet ✓ (an Absender-Adresse)');
|
||||
else toast(r.error||'Fehler beim Senden',false);
|
||||
}
|
||||
function newNutzerModal(){
|
||||
OM('Neuer Nutzer',
|
||||
'<div class="fg"><div class="fgrp"><label>Benutzername *</label><input id="nu-user" placeholder="max.muster"></div>'+
|
||||
@@ -2357,6 +2332,15 @@ function genW(){
|
||||
}
|
||||
function cpW(){navigator.clipboard.writeText(document.getElementById('wpreview')?.textContent||'');toast('Kopiert');}
|
||||
|
||||
// E-Mail-Versand läuft komplett über mailto: (öffnet das auf dem Gerät
|
||||
// hinterlegte Standard-Mailprogramm - Mail/iCloud auf iPhone, Gmail/Outlook
|
||||
// auf Android, Outlook/Mail/Thunderbird auf dem PC, je nach System-Einstellung
|
||||
// des Nutzers). Bewusst kein serverseitiger Versand mehr: erspart SMTP-
|
||||
// Zugangsdaten im Docker-Setup und funktioniert für jedes Konto identisch,
|
||||
// ohne Sonderrolle für den Admin. Einzige Einschränkung: mailto: kann aus
|
||||
// Sicherheitsgründen (Browser/Betriebssystem) keine Dateianhänge mitschicken
|
||||
// - das PDF wird daher wie gewohnt heruntergeladen, Anhängen bleibt ein
|
||||
// manueller Schritt für den Nutzer im geöffneten Mailprogramm.
|
||||
async function sendMailModal(typ){
|
||||
const pid=typ==='widerspruch'
|
||||
?document.getElementById('wpid')?.value
|
||||
@@ -2385,14 +2369,12 @@ async function sendMailModal(typ){
|
||||
'<div class="fgrp"><label>Betreff</label><input id="ml-subj" value="'+escH(betreff)+'"></div>'+
|
||||
'<div class="fgrp"><label>Anschreiben (E-Mail-Text)</label>'+
|
||||
'<textarea id="ml-body" rows="5" class="inp">Sehr geehrte Damen und Herren,\n\nanbei übersende ich das beigefügte Dokument zu Ihrer Kenntnisnahme.\n\nFür Rückfragen stehe ich Ihnen gerne zur Verfügung.\n\nMit freundlichen Grüßen</textarea></div>'+
|
||||
'<label class="fl ic g8" style="font-size:13px;cursor:pointer;margin-top:6px">'+
|
||||
'<input type="checkbox" id="ml-pdf" checked style="width:auto;cursor:pointer"> Dokument als PDF-Anhang beifügen</label>'+
|
||||
'</div>'+
|
||||
'<div class="alert a-info mt3" style="padding:8px 11px"><i class="ti ti-info-circle"></i>'+
|
||||
'<p class="sm">SMTP-Einstellungen unter <strong>Backend → E-Mail (SMTP)</strong> konfigurieren. Versand erfolgt über Ihren eigenen Mailserver.</p></div>'+
|
||||
'<p class="sm">Das PDF wird jetzt heruntergeladen und Ihr <strong>eigenes E-Mail-Programm</strong> öffnet sich mit vorausgefülltem Empfänger. Bitte hängen Sie die heruntergeladene Datei dort <strong>manuell an</strong>, bevor Sie senden – ein automatischer Anhang ist aus Sicherheitsgründen technisch nicht möglich.</p></div>'+
|
||||
'<div class="fl g8 mt3" style="justify-content:flex-end">'+
|
||||
'<button class="btn" onclick="CM()">Abbrechen</button>'+
|
||||
'<button class="btn btn-p" id="ml-send-btn" onclick="sendMail(\''+typ+'\')"><i class="ti ti-send"></i>Senden</button>'+
|
||||
'<button class="btn btn-p" id="ml-send-btn" onclick="sendMail(\''+typ+'\')"><i class="ti ti-mail"></i>PDF laden & E-Mail öffnen</button>'+
|
||||
'</div>'
|
||||
);
|
||||
}
|
||||
@@ -2401,41 +2383,48 @@ async function sendMail(typ){
|
||||
const to=(document.getElementById('ml-to')?.value||'').trim();
|
||||
const subj=document.getElementById('ml-subj')?.value||'';
|
||||
const body=document.getElementById('ml-body')?.value||'';
|
||||
const withPdf=document.getElementById('ml-pdf')?.checked;
|
||||
if(!to){toast('Empfänger-Adresse eingeben',false);return;}
|
||||
const btn=document.getElementById('ml-send-btn');
|
||||
if(btn){btn.disabled=true;btn.innerHTML='<i class="ti ti-loader-2"></i>Wird gesendet…';}
|
||||
if(btn){btn.disabled=true;btn.innerHTML='<i class="ti ti-loader-2"></i>PDF wird erstellt…';}
|
||||
const previewId=typ==='widerspruch'?'wpreview':'mpreview';
|
||||
const t=document.getElementById(previewId)?.textContent||'';
|
||||
let pdf_base64='',pdf_dateiname='';
|
||||
if(withPdf){
|
||||
const rawName=typ==='widerspruch'
|
||||
?(document.getElementById('wname')?.value||'Widerspruch')
|
||||
:(document.getElementById('mn')?.value||'Brief');
|
||||
const namePart=toFilename(rawName.trim().split(' ').pop()||rawName);
|
||||
pdf_dateiname=(typ==='widerspruch'?'Widerspruch_':'Brief_')+namePart+'_'+isoDate()+'.pdf';
|
||||
const el=document.createElement('div');
|
||||
el.style.cssText='font-family:"Courier New",Courier,monospace;font-size:10.5pt;line-height:1.9;padding:0;white-space:pre-wrap;color:#111;background:#fff;word-break:break-word;';
|
||||
el.textContent=t;
|
||||
try{
|
||||
await new Promise((res,rej)=>{
|
||||
html2pdf().set({margin:[25,20,28,25],filename:pdf_dateiname,html2canvas:{scale:2,useCORS:true},jsPDF:{unit:'mm',format:'a4',orientation:'portrait'}})
|
||||
.from(el).toPdf().get('pdf').then(pdf=>{addPageNums(pdf);pdf_base64=pdf.output('datauristring').split(',')[1];res();}).catch(rej);
|
||||
});
|
||||
}catch(e){
|
||||
toast('PDF-Generierung fehlgeschlagen',false);
|
||||
if(btn){btn.disabled=false;btn.innerHTML='<i class="ti ti-send"></i>Senden';}
|
||||
return;
|
||||
}
|
||||
}
|
||||
const r=await api('/api/email/senden',{method:'POST',body:JSON.stringify({
|
||||
empfaenger:to,betreff:subj,nachricht:body,pdf_base64,pdf_dateiname
|
||||
})});
|
||||
if(r.ok){CM();toast('E-Mail erfolgreich gesendet ✓');}
|
||||
else{
|
||||
toast(r.error||'Fehler beim Senden',false);
|
||||
if(btn){btn.disabled=false;btn.innerHTML='<i class="ti ti-send"></i>Senden';}
|
||||
const pid=typ==='widerspruch'?document.getElementById('wpid')?.value:mPatId;
|
||||
const rawName=typ==='widerspruch'
|
||||
?(document.getElementById('wname')?.value||'Widerspruch')
|
||||
:(document.getElementById('mn')?.value||'Brief');
|
||||
const namePart=toFilename(rawName.trim().split(' ').pop()||rawName);
|
||||
const pdf_dateiname=(typ==='widerspruch'?'Widerspruch_':'Brief_')+namePart+'_'+isoDate()+'.pdf';
|
||||
const el=document.createElement('div');
|
||||
el.style.cssText='font-family:"Courier New",Courier,monospace;font-size:10.5pt;line-height:1.9;padding:0;white-space:pre-wrap;color:#111;background:#fff;word-break:break-word;';
|
||||
el.textContent=t;
|
||||
try{
|
||||
await new Promise((res,rej)=>{
|
||||
html2pdf().set({margin:[25,20,28,25],filename:pdf_dateiname,html2canvas:{scale:2,useCORS:true},jsPDF:{unit:'mm',format:'a4',orientation:'portrait'}})
|
||||
.from(el).toPdf().get('pdf').then(pdf=>{
|
||||
addPageNums(pdf);
|
||||
const blob=pdf.output('blob');
|
||||
const url=URL.createObjectURL(blob);
|
||||
const a=document.createElement('a');a.href=url;a.download=pdf_dateiname;
|
||||
document.body.appendChild(a);a.click();document.body.removeChild(a);
|
||||
setTimeout(()=>URL.revokeObjectURL(url),2000);
|
||||
if(pid){
|
||||
const b64=pdf.output('datauristring').split(',')[1];
|
||||
api('/api/patienten/'+pid+'/briefe',{method:'POST',body:JSON.stringify({
|
||||
typ:typ==='widerspruch'?'widerspruch':'musterbrief',notizen:'PDF: '+pdf_dateiname,brief_text:t,pdf_base64:b64,pdf_dateiname
|
||||
})}).then(rr=>{if(rr.ok)toast('PDF dauerhaft in Patientenakte gespeichert ✓');});
|
||||
}
|
||||
res();
|
||||
}).catch(rej);
|
||||
});
|
||||
}catch(e){
|
||||
toast('PDF-Generierung fehlgeschlagen',false);
|
||||
if(btn){btn.disabled=false;btn.innerHTML='<i class="ti ti-mail"></i>PDF laden & E-Mail öffnen';}
|
||||
return;
|
||||
}
|
||||
const bodyMitHinweis=body+'\n\n\n[Bitte die heruntergeladene Datei „'+pdf_dateiname+'" anhängen, dann diese Zeile löschen]';
|
||||
location.href='mailto:'+to+'?subject='+encodeURIComponent(subj)+'&body='+encodeURIComponent(bodyMitHinweis);
|
||||
toast('PDF heruntergeladen ✓ – E-Mail-Programm öffnet sich, bitte Datei anhängen');
|
||||
CM();
|
||||
}
|
||||
|
||||
function dlW(){
|
||||
|
||||
Reference in new issue
Block a user