From 50c72fa8d81c91753005e3eabfe1367fb41f7a66 Mon Sep 17 00:00:00 2001 From: Thomas Date: Wed, 26 Aug 2026 11:15:53 +0200 Subject: [PATCH] Lizenz-Vertrieb Feinschliff: Zugabe +10%, sichere Datei-Einloesung, Mail-Config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Umsetzung des Thomas-Feedbacks: - Reihenfolge: freie Lizenzen beim Lehrer jetzt ORDER BY id (= Reihenfolge der reinkopierten Liste); stornierte ausgeblendet. - Gratis-Zugabe: Checkbox (Default an) "+10 % aufgerundet" -> geliefert = ceil(bestellt*1.1) (14 -> 16). Batch speichert qty_ordered (verrechnet) und qty (geliefert); Live-Vorschau im Formular. - Einloeselink/QR ENTFERNT (Sicherheitsrisiko: falscher eingeloggter Account). einloesen.html + pages/einloesen.php geloescht, Sammellink aus UI/Mail raus. - CSV: Gueltig-bis + Metadaten nur in der KOPFZEILE, danach reine Code-Zeilen (per Copy-Paste einloesbar). Dateiname: _geograsim_. - Interne Bestellnummer = batch.id (laufend, nicht editierbar, storno-stabil). - Lehrer-Einloesen: sichere .txt/.csv-Ablage (mehrstufig: Endung+MIME, Groessen- limit, nur Text lesen, Whitelist-Regex extrahiert nur Code-Tokens, Server validiert) — kein Upload, keine Ausfuehrung/Rendering. - Mailsystem: zentrale Config in neuer app_settings (Provider server|aws, Absender-Name/-Adresse, oeffentliche URL), pflegbar in Admin + Test-Versand. Mailer liest Absender aus Config (SMTP-Zugang bleibt in .env). Mailtext nutzt kanonische geograsim.at (nicht v3/localhost). Migrationen: -2 (qty_ordered), -3 (app_settings). Beide additiv, auf v3 angewandt. Co-Authored-By: Claude Opus 4.8 --- .../2026-08-26-lizenz-vertrieb-2.sql | 8 ++ ...6-08-26-lizenz-vertrieb-3-mailsettings.sql | 19 +++ App/admin-licenses.html | 124 +++++++++++++++--- App/einloesen.html | 80 ----------- App/pages/einloesen.php | 1 - App/php/api/licenses.php | 76 +++++++++-- App/php/lib/Mailer.php | 25 +++- App/teacher.html | 37 +++++- 8 files changed, 253 insertions(+), 117 deletions(-) create mode 100644 App/Don_t_Deploy/2026-08-26-lizenz-vertrieb-2.sql create mode 100644 App/Don_t_Deploy/2026-08-26-lizenz-vertrieb-3-mailsettings.sql delete mode 100644 App/einloesen.html delete mode 100644 App/pages/einloesen.php diff --git a/App/Don_t_Deploy/2026-08-26-lizenz-vertrieb-2.sql b/App/Don_t_Deploy/2026-08-26-lizenz-vertrieb-2.sql new file mode 100644 index 0000000..5671e26 --- /dev/null +++ b/App/Don_t_Deploy/2026-08-26-lizenz-vertrieb-2.sql @@ -0,0 +1,8 @@ +-- ===================================================================== +-- Lizenz-Vertrieb — Nachtrag 2 (2026-08-26) +-- * bestellte vs. gelieferte Menge (Zugabe +10 %) +-- Additiv & non-breaking. (Mail-Config: siehe Nachtrag 3, app_settings.) +-- ===================================================================== + +ALTER TABLE license_batches + ADD COLUMN qty_ordered INT UNSIGNED NULL AFTER qty; -- bestellt (verrechnet); qty = geliefert inkl. Zugabe diff --git a/App/Don_t_Deploy/2026-08-26-lizenz-vertrieb-3-mailsettings.sql b/App/Don_t_Deploy/2026-08-26-lizenz-vertrieb-3-mailsettings.sql new file mode 100644 index 0000000..c6a4e4a --- /dev/null +++ b/App/Don_t_Deploy/2026-08-26-lizenz-vertrieb-3-mailsettings.sql @@ -0,0 +1,19 @@ +-- ===================================================================== +-- Lizenz-Vertrieb — Nachtrag 3 (2026-08-26): portable App-Settings +-- Eigene Key/Value-Tabelle fuer die Mail-Konfiguration (Provider/Absender/URL). +-- Ersetzt die Nutzung der lokalen Orphan-Tabelle platform_config_v2 (existiert +-- auf v3/Prod nicht). Additiv & non-breaking. MariaDB + MySQL 8 kompatibel. +-- ===================================================================== + +CREATE TABLE IF NOT EXISTS app_settings ( + skey VARCHAR(64) NOT NULL PRIMARY KEY, + sval TEXT NULL, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + updated_by INT UNSIGNED NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +INSERT IGNORE INTO app_settings (skey, sval) VALUES + ('mail_provider', 'server'), + ('mail_from_email', 'support@geograsim.at'), + ('mail_from_name', 'GeoGraSim'), + ('mail_public_url', 'https://geograsim.at'); diff --git a/App/admin-licenses.html b/App/admin-licenses.html index c4d5704..9db94fa 100644 --- a/App/admin-licenses.html +++ b/App/admin-licenses.html @@ -128,8 +128,8 @@
📦 Lizenzen ausgeben (Vertrieb)
- - + +
@@ -168,6 +168,12 @@
+
+ + +
@@ -192,6 +198,41 @@
+ +
+
✉️ Mail-Einstellungen
+
+
+ + +
AWS wird aktiv, sobald konfiguriert.
+
+
+ + +
+
+ + +
SMTP-Zugang bleibt geheim in der .env.
+
+
+ + +
Für Links in Mails (nicht v3/localhost).
+
+
+
+ + + + +
+
+
Neue Lizenzen generieren
@@ -409,16 +450,18 @@ async function issueBatch(){ var name = document.getElementById('iss-name').value.trim(); var vf = document.getElementById('iss-from').value; var vu = document.getElementById('iss-until').value; + var bonus = document.getElementById('iss-bonus').checked; var msg = document.getElementById('iss-msg'); if(count<1||count>5000){ msg.textContent='Anzahl 1–5000.'; return; } + var delivered = calcDelivered(count, bonus); var who = name||email||custno||'(kein Kunde)'; - if(count>=100 && !confirm(count+' Codes für „'+who+'"'+(order?' (Best.-Nr. '+order+')':'')+', gültig bis '+fmtDate(vu)+', Typ '+type+'.\n\nWirklich erzeugen?')) return; + if(count>=100 && !confirm(count+' bestellt → '+delivered+' Codes im Paket für „'+who+'"'+(order?' (Best.-Nr. '+order+')':'')+', gültig bis '+fmtDate(vu)+', Typ '+type+'.\n\nWirklich erzeugen?')) return; msg.textContent = 'Erzeuge …'; - var r = await api('licenses', {action:'issue_batch', count:count, sales_type:type, order_no:order, + var r = await api('licenses', {action:'issue_batch', count:count, bonus:bonus, sales_type:type, order_no:order, customer:{email:email, customer_no:custno, name:name}, valid_from:vf, valid_until:vu}); if(r.error){ msg.innerHTML=''+esc(r.error)+''; return; } - msg.innerHTML='✓ '+r.count+' Codes erzeugt (Charge #'+r.batch_id+').'; - lastIssue = {batch_id:r.batch_id, codes:r.codes, count:r.count, valid_until:r.valid_until, token:r.redeem_token, order_no:order, email:email}; + msg.innerHTML='✓ '+r.count+' Codes erzeugt (Bestellung #'+r.internal_no+', bestellt '+r.ordered+').'; + lastIssue = {batch_id:r.batch_id, internal_no:r.internal_no, ordered:r.ordered, codes:r.codes, count:r.count, valid_until:r.valid_until, token:r.redeem_token, order_no:order, email:email}; renderIssueResult(); loadBatches(); loadLicenses(); @@ -429,17 +472,15 @@ function renderIssueResult(){ var codesText = m.codes.join('\n'); var mailText = buildMailText(m); var h = '
' + + '
Bestellung #'+m.internal_no+' · bestellt '+m.ordered+' · geliefert '+m.count+''+(m.count>m.ordered?' (+'+(m.count-m.ordered)+' gratis)':'')+' · gültig bis '+fmtDate(m.valid_until)+'
' + '

1 · Codes (' + m.count + ') — kopieren

' + '' + '
' + '' + '' - + '' + + '' + '
' - + '

2 · Sammel-Einlöse-Link (alle auf einmal)

' - + '
' - + '
Pro Code funktioniert auch: '+esc(absUrl('/einloesen?code=CODE'))+'
' - + '

3 · Per Mail senden

' + + '

2 · Per Mail senden

' + '
' + '
' + '
' @@ -457,20 +498,65 @@ function buildMailText(m){ var order = m.order_no ? ('\nBestellnummer: '+m.order_no) : ''; return 'Guten Tag,\n\nvielen Dank für Ihre Bestellung. Anbei Ihre '+m.count+' GeoGraSim-Lizenzcodes, gültig bis '+fmtDate(m.valid_until)+'.'+order+'\n\n' + codesPart + '\n\n' - + 'Einlösen: Als Lehrperson unter '+absUrl('/')+' anmelden und die Codes unter „Lizenzen" eingeben.\n' - + 'Oder alle auf einmal über diesen Link: '+absUrl('/einloesen?charge='+m.token)+'\n\n' + + 'Einlösen: Als Lehrperson unter '+PUBLIC_URL+' anmelden und die Codes unter „Lizenzen" eingeben.\n\n' + 'Freundliche Grüße\nGeoGraSim'; } function copyEl(id){ var el=document.getElementById(id); el.select(); navigator.clipboard.writeText(el.value).catch(function(){document.execCommand('copy');}); } +// CSV: Metadaten (inkl. Gültig-bis) in die KOPFZEILE, danach reine Code-Zeilen +// -> der Code-Block bleibt per Copy-Paste einlösbar (kein Datum je Zeile). function downloadCodes(fmt){ var m = lastIssue; if(!m) return; var content, mime; - if(fmt==='csv'){ content='Code;Gueltig_bis\n'+m.codes.map(function(c){return c+';'+m.valid_until;}).join('\n')+'\n'; mime='text/csv'; } - else { content=m.codes.join('\r\n')+'\r\n'; mime='text/plain'; } - var base = m.order_no ? m.order_no.replace(/[^a-zA-Z0-9_\-]/g,'_') : (new Date().toISOString().slice(0,16).replace(/[:T]/g,'-')); - var a=document.createElement('a'); a.href=URL.createObjectURL(new Blob([content],{type:mime})); a.download=base+'.'+fmt; a.click(); + if(fmt==='csv'){ + var head='Bestellung;#'+(m.internal_no||'')+'\nExterne Bestellnummer;'+(m.order_no||'')+'\nGültig bis;'+fmtDate(m.valid_until)+'\nAnzahl;'+m.count+'\n\n'; + content=head+m.codes.join('\r\n')+'\r\n'; mime='text/csv'; + } else { content=m.codes.join('\r\n')+'\r\n'; mime='text/plain'; } + var ext = m.order_no ? m.order_no.replace(/[^a-zA-Z0-9_\-]/g,'_') : (new Date().toISOString().slice(0,16).replace(/[:T]/g,'-')); + var fname = ext+'_geograsim_'+(m.internal_no||'')+'.'+fmt; + var a=document.createElement('a'); a.href=URL.createObjectURL(new Blob([content],{type:mime})); a.download=fname; a.click(); +} + +function calcDelivered(ordered, bonus){ return bonus ? Math.ceil(ordered*1.1) : ordered; } +function updatePreview(){ + var o=parseInt(document.getElementById('iss-count').value)||0; + var b=document.getElementById('iss-bonus').checked; + var el=document.getElementById('iss-preview'); if(!el) return; + if(o<1){ el.textContent=''; return; } + var d=calcDelivered(o,b); + el.innerHTML = b ? ('→ im Paket '+d+' Codes (+'+(d-o)+' gratis)') : ('→ '+d+' Codes (keine Zugabe)'); +} + +// ── Mail-Einstellungen ── +var PUBLIC_URL='https://geograsim.at'; +async function loadMailConfig(){ + var r=await api('licenses?admin=1&mail_config=1'); + if(!r || r.error) return; + PUBLIC_URL=r.public_url||PUBLIC_URL; + document.getElementById('mc-provider').value=r.provider||'server'; + document.getElementById('mc-fromname').value=r.from_name||''; + document.getElementById('mc-fromemail').value=r.from_email||''; + document.getElementById('mc-publicurl').value=r.public_url||''; +} +async function saveMailConfig(){ + var mm=document.getElementById('mc-msg'); mm.textContent='Speichere …'; + var r=await api('licenses',{action:'set_mail_config', + provider:document.getElementById('mc-provider').value, + from_name:document.getElementById('mc-fromname').value.trim(), + from_email:document.getElementById('mc-fromemail').value.trim(), + public_url:document.getElementById('mc-publicurl').value.trim()}); + if(r.error){ mm.innerHTML=''+esc(r.error)+''; return; } + mm.innerHTML='✓ Gespeichert.'; loadMailConfig(); +} +async function testMail(){ + var to=document.getElementById('mc-testto').value.trim(); + var mm=document.getElementById('mc-msg'); + if(!to){ mm.innerHTML='Test-Adresse fehlt.'; return; } + mm.textContent='Sende Test …'; + var r=await api('licenses',{action:'test_mail', to_email:to}); + if(r.error){ mm.innerHTML=''+esc(r.error)+''; return; } + mm.innerHTML='✓ Test gesendet.'; } async function sendMail(batchId){ @@ -542,8 +628,6 @@ async function openBatch(id){ + ' · gültig '+fmtDate(b.default_valid_from)+'–'+fmtDate(b.default_valid_until) + (b.canceled_at?' · Charge storniert':'')+'
'; h+='
' - + '' - + '' + '' + '
'; h+='
'; @@ -608,6 +692,8 @@ fetch(BASE + '/api/admin', {method:'POST', headers:{'Content-Type':'application/ loadLicenses(); prefillDefaults(); loadBatches(); + loadMailConfig(); + updatePreview(); }); diff --git a/App/einloesen.html b/App/einloesen.html deleted file mode 100644 index bdc7d08..0000000 --- a/App/einloesen.html +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - Lizenz einlösen — GeoGraSim - - - - -
-
GeoGraSim
-
Lizenz einlösen
-
- -
- - - - diff --git a/App/pages/einloesen.php b/App/pages/einloesen.php deleted file mode 100644 index 21a0704..0000000 --- a/App/pages/einloesen.php +++ /dev/null @@ -1 +0,0 @@ -fetchOne("SELECT sval FROM app_settings WHERE skey = ?", [$key]); + return ($r && $r['sval'] !== null && $r['sval'] !== '') ? (string)$r['sval'] : $default; + } catch (\Throwable $e) { return $default; } +} + // === GET === if ($method === 'GET') { // Super-Admin: alle Lizenzen @@ -105,6 +113,16 @@ if ($method === 'GET') { ]); } + // Mail-System-Konfiguration + if (isset($_GET['mail_config'])) { + Response::ok([ + 'provider' => lic_cfg($db, 'mail_provider', 'server'), + 'from_email' => lic_cfg($db, 'mail_from_email', 'support@geograsim.at'), + 'from_name' => lic_cfg($db, 'mail_from_name', 'GeoGraSim'), + 'public_url' => lic_cfg($db, 'mail_public_url', 'https://geograsim.at'), + ]); + } + // Kunden-Autocomplete if (isset($_GET['customers'])) { $like = '%' . trim($_GET['customers']) . '%'; @@ -214,9 +232,10 @@ if ($method === 'GET') { [$classId] ); - // Auch freie Lizenzen des Lehrers (noch nicht zugewiesen) + // Auch freie Lizenzen des Lehrers (noch nicht zugewiesen), storniert ausgeschlossen. + // Reihenfolge = id (= Reihenfolge, in der die Codes ausgegeben/reinkopiert wurden). $freeLicenses = $db->fetchAll( - 'SELECT id, code, school_year FROM licenses WHERE teacher_id = ? AND student_id IS NULL ORDER BY code', + 'SELECT id, code, school_year FROM licenses WHERE teacher_id = ? AND student_id IS NULL AND canceled_at IS NULL ORDER BY id', [$teacherId] ); @@ -348,8 +367,11 @@ if ($method === 'POST') { $adminId = Session::adminId(); if (!$adminId) Response::error('Nur Admin', 403); - $count = (int)($body['count'] ?? 0); - if ($count < 1 || $count > 5000) Response::error('Anzahl muss zwischen 1 und 5000 liegen'); + $ordered = (int)($body['count'] ?? 0); + if ($ordered < 1 || $ordered > 5000) Response::error('Anzahl muss zwischen 1 und 5000 liegen'); + // Gratis-Zugabe: +10 %, aufgerundet (14 → 16). Standard aktiv. + $bonus = !array_key_exists('bonus', $body) || !empty($body['bonus']); + $count = $bonus ? (int)ceil($ordered * 1.1) : $ordered; $type = in_array($body['sales_type'] ?? '', LIC_TYPES, true) ? $body['sales_type'] : 'privat'; $orderNo = trim((string)($body['order_no'] ?? '')) ?: null; $note = trim((string)($body['note'] ?? '')) ?: null; @@ -366,9 +388,9 @@ if ($method === 'POST') { $db->begin(); try { $db->execute( - "INSERT INTO license_batches (customer_id, order_no, sales_type, qty, default_valid_from, default_valid_until, note, redeem_token, created_by) - VALUES (?,?,?,?,?,?,?,?,?)", - [$customerId, $orderNo, $type, $count, $validFrom, $validUntil, $note, $token, $adminId]); + "INSERT INTO license_batches (customer_id, order_no, sales_type, qty, qty_ordered, default_valid_from, default_valid_until, note, redeem_token, created_by) + VALUES (?,?,?,?,?,?,?,?,?,?)", + [$customerId, $orderNo, $type, $count, $ordered, $validFrom, $validUntil, $note, $token, $adminId]); $batchId = (int)$db->lastInsertId(); $codes = []; $made = 0; $tries = 0; $maxTries = $count * 8 + 50; @@ -383,16 +405,16 @@ if ($method === 'POST') { } catch (\Throwable $e) { /* Code-Kollision (extrem selten) → neuer Versuch */ } } if ($made < $count) throw new \RuntimeException("Nur $made von $count Codes erzeugt"); - lic_event($db, $batchId, null, 'issued', "$count Codes, gültig $validFrom…$validUntil, Typ $type" . ($orderNo ? ", Best.-Nr. $orderNo" : ''), $adminId); + lic_event($db, $batchId, null, 'issued', "bestellt $ordered, geliefert $made (Zugabe " . ($bonus ? 'ja' : 'nein') . "), gültig $validFrom…$validUntil, Typ $type" . ($orderNo ? ", Best.-Nr. $orderNo" : ''), $adminId); $db->commit(); } catch (\Throwable $e) { $db->rollBack(); Response::error('Fehler beim Erzeugen: ' . $e->getMessage(), 500); } Response::ok([ - 'batch_id' => $batchId, 'redeem_token' => $token, + 'batch_id' => $batchId, 'internal_no' => $batchId, 'redeem_token' => $token, 'valid_from' => $validFrom, 'valid_until' => $validUntil, - 'count' => $made, 'codes' => $codes, + 'ordered' => $ordered, 'count' => $made, 'codes' => $codes, ]); } @@ -444,6 +466,8 @@ if ($method === 'POST') { if ($action === 'send_batch_mail') { $adminId = Session::adminId(); if (!$adminId) Response::error('Nur Admin', 403); $bid = (int)($body['batch_id'] ?? 0); if (!$bid) Response::error('batch_id erforderlich'); + $provider = lic_cfg($db, 'mail_provider', 'server'); + if ($provider !== 'server') Response::error('Aktiver Mail-Treiber „' . $provider . '" ist noch nicht verfügbar. In den Mail-Einstellungen auf „server" stellen.'); $to = trim((string)($body['to_email'] ?? '')); $subject = trim((string)($body['subject'] ?? '')) ?: 'Ihre GeoGraSim-Lizenzcodes'; $textBody = (string)($body['body_text'] ?? ''); @@ -459,7 +483,7 @@ if ($method === 'POST') { } $db->execute( "INSERT INTO license_mails (batch_id, to_email, subject, body_text, body_html, provider, status, created_by) VALUES (?,?,?,?,?,?,?,?)", - [$bid, $to, $subject, $textBody, $html, 'server', 'queued', $adminId]); + [$bid, $to, $subject, $textBody, $html, $provider, 'queued', $adminId]); $mailId = (int)$db->lastInsertId(); try { Mailer::sendWithAttachments($to, $subject, $html, $textBody, $attach); @@ -489,6 +513,36 @@ if ($method === 'POST') { Response::ok(['redeemed' => $n]); } + // ═══════════════ Mail-System-Konfiguration ═══════════════ + if ($action === 'set_mail_config') { + $adminId = Session::adminId(); if (!$adminId) Response::error('Nur Admin', 403); + $prov = in_array($body['provider'] ?? '', ['server', 'aws'], true) ? $body['provider'] : 'server'; + $femail = trim((string)($body['from_email'] ?? '')); + $fname = trim((string)($body['from_name'] ?? '')); + $purl = rtrim(trim((string)($body['public_url'] ?? '')), '/'); + if ($femail !== '' && !filter_var($femail, FILTER_VALIDATE_EMAIL)) Response::error('Absender-Adresse ungültig'); + $set = ['mail_provider' => $prov, 'mail_from_email' => $femail, 'mail_from_name' => $fname, 'mail_public_url' => $purl]; + foreach ($set as $k => $v) { + if ($v === '') continue; + $db->execute("INSERT INTO app_settings (skey, sval, updated_by) VALUES (?,?,?) ON DUPLICATE KEY UPDATE sval = VALUES(sval), updated_by = VALUES(updated_by)", [$k, $v, $adminId]); + } + Response::ok(); + } + + if ($action === 'test_mail') { + $adminId = Session::adminId(); if (!$adminId) Response::error('Nur Admin', 403); + $to = trim((string)($body['to_email'] ?? '')); + if (!filter_var($to, FILTER_VALIDATE_EMAIL)) Response::error('Gültige Test-Adresse erforderlich'); + $provider = lic_cfg($db, 'mail_provider', 'server'); + if ($provider !== 'server') Response::error('Aktiver Treiber „' . $provider . '" ist noch nicht verfügbar. Auf „server" stellen.'); + try { + Mailer::sendWithAttachments($to, 'GeoGraSim — Test-Mail', + Mailer::renderPlainBody("Test-Mail aus der Lizenz-Verwaltung.\nAbsender und Versand funktionieren."), + 'Test-Mail — Versand funktioniert.'); + Response::ok(['status' => 'sent']); + } catch (\Throwable $e) { Response::error('Versand fehlgeschlagen: ' . $e->getMessage(), 500); } + } + Response::error('Unbekannte Aktion'); } diff --git a/App/php/lib/Mailer.php b/App/php/lib/Mailer.php index d8a0a79..350a031 100644 --- a/App/php/lib/Mailer.php +++ b/App/php/lib/Mailer.php @@ -16,13 +16,30 @@ class Mailer { $mail->SMTPSecure = \PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_STARTTLS; $mail->Port = defined('SMTP_PORT') ? (int)SMTP_PORT : 587; $mail->CharSet = 'UTF-8'; - $mail->setFrom( - defined('SMTP_FROM') ? SMTP_FROM : 'support@geograsim.at', - defined('SMTP_FROM_NAME') ? SMTP_FROM_NAME : 'GeoGraSim' - ); + [$fromEmail, $fromName] = self::configuredFrom(); + $mail->setFrom($fromEmail, $fromName); return $mail; } + /** Absender aus Admin-Config (platform_config_v2), Fallback auf .env-Konstanten. + * SMTP-Zugangsdaten bleiben bewusst in .env (geheim) — nur die sichtbare + * Absenderadresse/-name ist zentral in der Admin-Oberflaeche pflegbar. */ + private static function configuredFrom(): array { + $email = defined('SMTP_FROM') ? SMTP_FROM : 'support@geograsim.at'; + $name = defined('SMTP_FROM_NAME') ? SMTP_FROM_NAME : 'GeoGraSim'; + try { + if (class_exists('Database')) { + $rows = Database::get()->fetchAll( + "SELECT skey, sval FROM app_settings WHERE skey IN ('mail_from_email','mail_from_name')"); + foreach ($rows as $row) { + if ($row['skey'] === 'mail_from_email' && !empty($row['sval'])) $email = $row['sval']; + if ($row['skey'] === 'mail_from_name' && !empty($row['sval'])) $name = $row['sval']; + } + } + } catch (\Throwable $e) { /* Config nicht verfuegbar -> Defaults */ } + return [$email, $name]; + } + public static function send(string $to, string $subject, string $htmlBody, string $textBody = ''): bool { try { $mail = self::create(); diff --git a/App/teacher.html b/App/teacher.html index b4e882d..289ddc2 100644 --- a/App/teacher.html +++ b/App/teacher.html @@ -431,8 +431,12 @@ schueler01,Max M.,,,,,
CodeStatusGültig bisLehrperson