Import aus Monorepo (frischer Start)

This commit is contained in:
Lucas Orth
2026-08-24 12:14:00 +02:00
commit 8dad67b4c1
3 changed files with 579 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*.zip
*.log

155
formel.php Normal file
View File

@@ -0,0 +1,155 @@
<?php
/**
* Kleiner, sicherer Formel-Interpreter (kein eval).
* Unterstützt: Zahlen, Variablen, + - * /, Klammern, unäres Minus,
* Vergleiche (>= <= > < == !=), Ternär (a ? b : c) und die Funktionen
* sqrt, pow, abs, min, max, round, floor, ceil.
*
* Damit rechnet das Plugin mit GENAU DER Formel, die in Directus hinterlegt
* ist – Änderungen an der Formel wirken automatisch mit.
*/
if (!defined('ABSPATH')) exit;
/** Zerlegt den Ausdruck in Tokens. */
function skrift_formel_tokens($ausdruck) {
$s = (string) $ausdruck;
$s = str_replace('Math.', '', $s); // Math.sqrt(...) → sqrt(...)
$s = preg_replace('/%(\w+)%/', '$1', $s); // %qty% → qty
$tokens = array();
$pos = 0; $len = strlen($s);
while ($pos < $len) {
$c = $s[$pos];
if (ctype_space($c)) { $pos++; continue; }
if (ctype_digit($c) || ($c === '.' && $pos + 1 < $len && ctype_digit($s[$pos + 1]))) {
$start = $pos;
while ($pos < $len && (ctype_digit($s[$pos]) || $s[$pos] === '.')) $pos++;
$tokens[] = array('num', (float) substr($s, $start, $pos - $start));
continue;
}
if (ctype_alpha($c) || $c === '_') {
$start = $pos;
while ($pos < $len && (ctype_alnum($s[$pos]) || $s[$pos] === '_')) $pos++;
$tokens[] = array('id', substr($s, $start, $pos - $start));
continue;
}
$zwei = substr($s, $pos, 2);
if (in_array($zwei, array('>=', '<=', '==', '!='), true)) { $tokens[] = array('op', $zwei); $pos += 2; continue; }
$tokens[] = array('op', $c); $pos++;
}
return $tokens;
}
function skrift_formel_ist($t, $i, $wert) {
return $i < count($t) && $t[$i][0] === 'op' && $t[$i][1] === $wert;
}
function skrift_formel_fn($name, $args) {
$a0 = isset($args[0]) ? (float) $args[0] : 0.0;
switch ($name) {
case 'sqrt': return $a0 < 0 ? 0.0 : sqrt($a0);
case 'pow': return pow($a0, isset($args[1]) ? (float) $args[1] : 0.0);
case 'abs': return abs($a0);
case 'min': return empty($args) ? 0.0 : min($args);
case 'max': return empty($args) ? 0.0 : max($args);
case 'round': return round($a0);
case 'floor': return floor($a0);
case 'ceil': return ceil($a0);
}
return 0.0;
}
function skrift_formel_primary(&$i, $t, $v) {
if ($i >= count($t)) return 0.0;
$tk = $t[$i];
if ($tk[0] === 'num') { $i++; return $tk[1]; }
if ($tk[0] === 'id') {
$name = $tk[1]; $i++;
if (skrift_formel_ist($t, $i, '(')) {
$i++; $args = array();
if (!skrift_formel_ist($t, $i, ')')) {
$args[] = skrift_formel_ternary($i, $t, $v);
while (skrift_formel_ist($t, $i, ',')) { $i++; $args[] = skrift_formel_ternary($i, $t, $v); }
}
if (skrift_formel_ist($t, $i, ')')) $i++;
return skrift_formel_fn($name, $args);
}
return isset($v[$name]) ? (float) $v[$name] : 0.0;
}
if ($tk[0] === 'op' && $tk[1] === '(') {
$i++; $r = skrift_formel_ternary($i, $t, $v);
if (skrift_formel_ist($t, $i, ')')) $i++;
return $r;
}
$i++; return 0.0;
}
function skrift_formel_unary(&$i, $t, $v) {
if (skrift_formel_ist($t, $i, '-')) { $i++; return -skrift_formel_unary($i, $t, $v); }
if (skrift_formel_ist($t, $i, '+')) { $i++; return skrift_formel_unary($i, $t, $v); }
return skrift_formel_primary($i, $t, $v);
}
function skrift_formel_mul(&$i, $t, $v) {
$l = skrift_formel_unary($i, $t, $v);
while (skrift_formel_ist($t, $i, '*') || skrift_formel_ist($t, $i, '/')) {
$op = $t[$i][1]; $i++;
$r = skrift_formel_unary($i, $t, $v);
$l = $op === '*' ? $l * $r : ($r == 0 ? 0.0 : $l / $r);
}
return $l;
}
function skrift_formel_add(&$i, $t, $v) {
$l = skrift_formel_mul($i, $t, $v);
while (skrift_formel_ist($t, $i, '+') || skrift_formel_ist($t, $i, '-')) {
$op = $t[$i][1]; $i++;
$r = skrift_formel_mul($i, $t, $v);
$l = $op === '+' ? $l + $r : $l - $r;
}
return $l;
}
function skrift_formel_compare(&$i, $t, $v) {
$l = skrift_formel_add($i, $t, $v);
while ($i < count($t) && $t[$i][0] === 'op'
&& in_array($t[$i][1], array('>=', '<=', '==', '!=', '>', '<'), true)) {
$op = $t[$i][1]; $i++;
$r = skrift_formel_add($i, $t, $v);
switch ($op) {
case '>=': $l = $l >= $r; break;
case '<=': $l = $l <= $r; break;
case '>': $l = $l > $r; break;
case '<': $l = $l < $r; break;
case '==': $l = $l == $r; break;
case '!=': $l = $l != $r; break;
}
}
return $l;
}
function skrift_formel_ternary(&$i, $t, $v) {
$c = skrift_formel_compare($i, $t, $v);
if (skrift_formel_ist($t, $i, '?')) {
$i++;
$a = skrift_formel_ternary($i, $t, $v);
if (skrift_formel_ist($t, $i, ':')) $i++;
$b = skrift_formel_ternary($i, $t, $v);
return $c ? $a : $b;
}
return $c;
}
/** Wertet den Ausdruck aus. Bei Problemen wird $fallback zurückgegeben. */
function skrift_formel_eval($ausdruck, $vars, $fallback = 1.0) {
if (!is_string($ausdruck) || trim($ausdruck) === '') return $fallback;
try {
$t = skrift_formel_tokens($ausdruck);
$i = 0;
$r = (float) skrift_formel_ternary($i, $t, $vars);
if (!is_finite($r)) return $fallback;
return $r;
} catch (Throwable $e) {
return $fallback;
}
}

422
skrift-preisrechner.php Normal file
View File

@@ -0,0 +1,422 @@
<?php
/**
* Plugin Name: Skrift Preisrechner
* Description: Preisrechner für Briefe/Postkarten per Shortcode. Holt Preisdaten stündlich per WP-Cron aus dem Skrift-Backend (öffentliche Read-API, ohne Token) und speichert sie in WordPress. Die Anzeige rechnet ohne Netzwerkzugriff – dadurch ohne Verzögerung. Shortcodes: [skrift_preis_inkl] und [skrift_preis_exkl].
* Version: 2.0.0
* Author: Skrift
*
* Sicherheit: Es werden ausschließlich ÖFFENTLICHE Werte gelesen (Basispreis,
* Formel-Parameter, MwSt, Porto) – keine Secrets, kein API-Token.
*/
if (!defined('ABSPATH')) exit;
require_once __DIR__ . '/formel.php';
const SKRIFT_PREIS_OPTION = 'skrift_preis_daten';
const SKRIFT_PREIS_CRON = 'skrift_preis_sync';
/** Nach dieser Zeit gelten gespeicherte Daten als veraltet (Cron-Ausfall). */
const SKRIFT_PREIS_MAX_ALTER = 7200; // 2 Stunden
/** Basis-URL des Skrift-Backends (Directus). Per Filter überschreibbar. */
function skrift_preis_backend_url() {
return rtrim(apply_filters('skrift_preis_backend_url', 'https://admin.skrift.de'), '/');
}
/**
* Holt die Preisdaten frisch aus Directus und legt sie in einer WP-Option ab.
* Wird stündlich per Cron aufgerufen – nicht im normalen Seitenaufruf.
* Bei Fehlern bleiben die zuletzt gespeicherten Daten erhalten.
*/
function skrift_preis_sync() {
$base = skrift_preis_backend_url();
$fehler = null;
$hole = function ($pfad) use ($base, &$fehler) {
$res = wp_remote_get($base . $pfad, array('timeout' => 10));
if (is_wp_error($res)) {
if ($fehler === null) $fehler = 'Backend nicht erreichbar (' . $res->get_error_message() . ')';
return null;
}
$code = wp_remote_retrieve_response_code($res);
if ($code !== 200) {
if ($fehler === null) $fehler = 'Backend antwortete mit HTTP ' . $code . ' auf ' . $pfad;
return null;
}
$json = json_decode(wp_remote_retrieve_body($res), true);
if (!is_array($json) || !array_key_exists('data', $json)) {
if ($fehler === null) $fehler = 'Unerwartete Antwort auf ' . $pfad;
return null;
}
return $json['data'];
};
$produkte = $hole('/items/products?fields=key,name,base_price,norm_qty,price_factor_a,price_factor_e,pricing_mode,type&filter[active][_eq]=true&limit=-1');
$settings = $hole('/items/pricing_settings?fields=*');
$items = $hole('/items/price_items?fields=key,price,unit,category&filter[active][_eq]=true&limit=-1');
// Nur bei Erfolg überschreiben – sonst bleiben die alten Werte stehen.
if ($fehler === null && is_array($produkte) && !empty($produkte)) {
update_option(SKRIFT_PREIS_OPTION, array(
'produkte' => $produkte,
'settings' => is_array($settings) ? $settings : array(),
'items' => is_array($items) ? $items : array(),
'zeit' => time(),
'fehler' => null,
), false); // kein autoload – wird nur bei Bedarf geladen
return true;
}
// Fehler am vorhandenen Datensatz vermerken (für die Diagnose in der Ausgabe).
$alt = get_option(SKRIFT_PREIS_OPTION);
if (is_array($alt)) {
$alt['fehler'] = $fehler;
update_option(SKRIFT_PREIS_OPTION, $alt, false);
} else {
update_option(SKRIFT_PREIS_OPTION, array(
'produkte' => array(), 'settings' => array(), 'items' => array(),
'zeit' => 0, 'fehler' => $fehler,
), false);
}
return false;
}
add_action(SKRIFT_PREIS_CRON, 'skrift_preis_sync');
/** Stündlichen Cron beim Aktivieren einplanen und sofort einmal synchronisieren. */
register_activation_hook(__FILE__, function () {
if (!wp_next_scheduled(SKRIFT_PREIS_CRON)) {
wp_schedule_event(time() + 60, 'hourly', SKRIFT_PREIS_CRON);
}
skrift_preis_sync();
});
register_deactivation_hook(__FILE__, function () {
$ts = wp_next_scheduled(SKRIFT_PREIS_CRON);
if ($ts) wp_unschedule_event($ts, SKRIFT_PREIS_CRON);
});
/** Sicherheitsnetz: fehlt der Cron-Eintrag (z. B. nach Migration), neu planen. */
add_action('init', function () {
if (!wp_next_scheduled(SKRIFT_PREIS_CRON)) {
wp_schedule_event(time() + 60, 'hourly', SKRIFT_PREIS_CRON);
}
// Manuelles Aktualisieren als Admin: ?skrift_preis_refresh=1
if (isset($_GET['skrift_preis_refresh']) && current_user_can('manage_options')) {
skrift_preis_sync();
}
});
/**
* Liefert die gespeicherten Preisdaten. Kein HTTP im Normalfall.
* Nur wenn noch nie synchronisiert wurde (oder die Daten deutlich veraltet
* sind), wird einmalig nachgeholt.
*/
function skrift_preis_daten() {
$d = get_option(SKRIFT_PREIS_OPTION);
$leer = !is_array($d) || empty($d['produkte']);
$alt = is_array($d) && isset($d['zeit']) && (time() - (int) $d['zeit']) > SKRIFT_PREIS_MAX_ALTER;
if ($leer || $alt) {
skrift_preis_sync();
$d = get_option(SKRIFT_PREIS_OPTION);
}
if (!is_array($d)) {
$d = array('produkte' => array(), 'settings' => array(), 'items' => array(), 'zeit' => 0, 'fehler' => 'Keine Daten.');
}
return $d;
}
/** Einen Preis-Positionswert (z. B. porto_inland) aus dem Katalog lesen. */
function skrift_preis_item($items, $key) {
foreach ($items as $it) {
if (isset($it['key']) && $it['key'] === $key) return (float) $it['price'];
}
return 0.0;
}
/** Ein Produkt per Schlüssel finden. */
function skrift_preis_produkt($produkte, $key) {
foreach ($produkte as $p) {
if (isset($p['key']) && $p['key'] === $key) return $p;
}
return null;
}
/**
* Die für die Rechnung nötigen Parameter eines Produkts – Grundlage sowohl für
* die PHP-Rechnung als auch für die identische Rechnung im Browser.
*/
function skrift_preis_parameter($daten, $produktKey, $typ = 'privat') {
$s = $daten['settings'];
$p = skrift_preis_produkt($daten['produkte'], $produktKey);
if (!$p) return null;
// Normalpreis-Menge kommt vom Produkt; ohne Wert kein Kleinmengen-Aufschlag.
$norm = isset($p['norm_qty']) ? (float) $p['norm_qty'] : 0.0;
// Kleinmengen-Aufschlag je Produkt (a = Deckel, e = Exponent). Leer → 1 / 3.
$a = (isset($p['price_factor_a']) && $p['price_factor_a'] !== null && $p['price_factor_a'] !== '') ? (float) $p['price_factor_a'] : 1.0;
$e = (isset($p['price_factor_e']) && $p['price_factor_e'] !== null && $p['price_factor_e'] !== '') ? (float) $p['price_factor_e'] : 3.0;
// Eine gemeinsame Formel.
$formel = !empty($s['multiplier_formula']) ? $s['multiplier_formula'] : '';
$tiers = array();
if (isset($s['quantity_discounts']) && is_array($s['quantity_discounts'])) {
foreach ($s['quantity_discounts'] as $t) {
$tiers[] = array(
'min_qty' => isset($t['min_qty']) ? (int) $t['min_qty'] : 0,
'percent' => isset($t['percent']) ? (float) $t['percent'] : 0.0,
);
}
}
return array(
'base' => isset($p['base_price']) ? (float) $p['base_price'] : 0.0,
'norm' => $norm,
'a' => $a,
'e' => $e,
'formel' => $formel,
'mwst' => isset($s['mwst_percent']) ? (float) $s['mwst_percent'] : 19.0,
'tiers' => $tiers,
'porto' => skrift_preis_item($daten['items'], 'porto_inland'),
'name' => isset($p['name']) ? $p['name'] : $produktKey,
);
}
/**
* Kernrechnung in PHP (spiegelt die Standard-Formel des Backends):
* grund = base * menge * multiplikator
* multiplik. = menge >= norm ? 1 : 2 - sqrt(menge / norm)
* danach Mengenrabatt-Staffel, dann / menge = Preis pro Stück (netto).
*/
function skrift_preis_rechne_param($par, $menge, $mitKuvert) {
if (!$par) return null;
$menge = max(1, (int) $menge);
// Multiplikator exakt mit der in Directus hinterlegten Formel berechnen.
// Ohne Normalpreis-Menge am Produkt greift kein Kleinmengen-Aufschlag.
$mult = 1.0;
if ($par['norm'] > 0) {
$mult = skrift_formel_eval($par['formel'], array(
'qty' => $menge, 'menge' => $menge,
'norm' => $par['norm'], 'base_price' => $par['base'],
'a' => $par['a'], 'e' => $par['e'],
), 1.0);
}
if (!is_finite($mult) || $mult <= 0) $mult = 1.0;
$grund = $par['base'] * $menge * $mult;
$besteMin = -1; $bestePercent = 0.0;
foreach ($par['tiers'] as $t) {
if ($menge >= $t['min_qty'] && $t['percent'] > 0 && $t['min_qty'] > $besteMin) {
$besteMin = $t['min_qty']; $bestePercent = $t['percent'];
}
}
if ($bestePercent > 0) $grund *= (1 - $bestePercent / 100);
$netto = $grund / $menge;
if ($mitKuvert) $netto += $par['porto'];
return array(
'netto' => round($netto, 2),
'brutto' => round($netto * (1 + $par['mwst'] / 100), 2),
'mwst' => $par['mwst'],
);
}
/** REST-Endpunkt bleibt bestehen (PHP-Rechnung), z. B. für eigene Integrationen. */
add_action('rest_api_init', function () {
register_rest_route('skrift/v1', '/preis', array(
'methods' => 'GET',
'permission_callback' => '__return_true',
'callback' => function (WP_REST_Request $req) {
$daten = skrift_preis_daten();
$par = skrift_preis_parameter(
$daten,
sanitize_text_field($req->get_param('produkt')),
$req->get_param('typ') === 'unternehmen' ? 'unternehmen' : 'privat'
);
$r = skrift_preis_rechne_param(
$par,
(int) $req->get_param('menge'),
$req->get_param('kuvert') === '1' || $req->get_param('kuvert') === 'true'
);
if (!$r) {
$grund = !empty($daten['fehler']) ? $daten['fehler'] : 'Produkt nicht gefunden.';
return new WP_Error('skrift_preis', $grund, array('status' => 404));
}
return rest_ensure_response($r);
},
));
});
/**
* Darstellung. $mitMwst bestimmt, WELCHER Preis gezeigt wird – der Besucher
* wählt das bewusst nicht selbst.
*
* Die Parameter stehen im Seitenquelltext; die Neuberechnung passiert dadurch
* sofort im Browser (kein Netzwerkzugriff, keine Latenz) mit exakt derselben
* Formel wie in PHP.
*/
function skrift_preis_render($atts, $mitMwst) {
$a = shortcode_atts(array('produkt' => 'briefe', 'typ' => 'privat', 'menge' => 100), $atts);
$daten = skrift_preis_daten();
$par = skrift_preis_parameter($daten, $a['produkt'], $a['typ']);
if (!$par) {
if (!empty($daten['fehler'])) {
return '<p><strong>Preisrechner nicht verfügbar.</strong><br><span style="color:#64748b;font-size:13px;">'
. esc_html($daten['fehler']) . '</span></p>';
}
$bekannt = array();
foreach ($daten['produkte'] as $pp) { if (isset($pp['key'])) $bekannt[] = $pp['key']; }
return '<p><strong>Preisrechner:</strong> Produkt „' . esc_html($a['produkt']) . '" nicht gefunden.'
. (count($bekannt) ? '<br><span style="color:#64748b;font-size:13px;">Verfügbar: '
. esc_html(implode(', ', $bekannt)) . '</span>' : '') . '</p>';
}
$startMenge = max(1, (int) $a['menge']);
$start = skrift_preis_rechne_param($par, $startMenge, false);
$startWert = $mitMwst ? $start['brutto'] : $start['netto'];
$hinweis = $mitMwst
? 'inkl. ' . rtrim(rtrim(number_format($par['mwst'], 1, ',', '.'), '0'), ',') . ' % MwSt.'
: 'zzgl. MwSt.';
$id = 'skpreis_' . wp_rand(1000, 9999);
ob_start(); ?>
<div class="sk-preisrechner" id="<?php echo esc_attr($id); ?>" style="width:100%;box-sizing:border-box;color:#1A1A1A;font-family:var(--e-global-typography-text-font-family, inherit);font-weight:var(--e-global-typography-text-font-weight, inherit);">
<label style="display:block;font-size:0.875em;margin-bottom:8px;color:#1A1A1A;">Anzahl</label>
<input type="number" min="1" value="<?php echo esc_attr($startMenge); ?>" class="skp-menge"
style="width:100%;box-sizing:border-box;padding:12px 14px;border:1px solid #d8dde5;border-radius:8px;font-family:inherit;font-size:1em;color:#1A1A1A;margin-bottom:18px;">
<label style="display:flex;align-items:flex-start;gap:10px;font-size:0.95em;color:#1A1A1A;margin-bottom:20px;cursor:pointer;">
<input type="checkbox" class="skp-kuvert" style="margin-top:4px;flex:0 0 auto;">
<span>Mit handbeschriebenem Kuvert und Direktversand an die Empfänger</span>
</label>
<div style="border-top:1px solid #e6e6e6;padding-top:16px;display:flex;justify-content:space-between;align-items:baseline;gap:16px;">
<span style="font-size:1em;">Preis pro Stück</span>
<strong class="skp-wert" style="font-size:1.5em;font-family:var(--e-global-typography-primary-font-family, inherit);white-space:nowrap;"><?php echo esc_html(number_format($startWert, 2, ',', '.')); ?> €</strong>
</div>
<div style="text-align:right;font-size:0.8em;opacity:.65;margin-top:4px;"><?php echo esc_html($hinweis); ?></div>
</div>
<script>
(function () {
var wrap = document.getElementById(<?php echo wp_json_encode($id); ?>);
if (!wrap) return;
var par = <?php echo wp_json_encode($par); ?>;
var mitMwst = <?php echo $mitMwst ? 'true' : 'false'; ?>;
var menge = wrap.querySelector('.skp-menge');
var kuvert = wrap.querySelector('.skp-kuvert');
var wert = wrap.querySelector('.skp-wert');
// Formel-Interpreter (Spiegel der PHP-Fassung) – wertet die in Directus
// hinterlegte Formel aus, damit Änderungen automatisch mitwirken.
function formelEval(ausdruck, vars) {
if (!ausdruck) return 1;
var s = String(ausdruck).replace(/Math\./g, '').replace(/%(\w+)%/g, '$1');
var T = [], i = 0;
while (i < s.length) {
var c = s[i];
if (/\s/.test(c)) { i++; continue; }
if (/[0-9]/.test(c) || (c === '.' && /[0-9]/.test(s[i + 1] || ''))) {
var st = i; while (i < s.length && /[0-9.]/.test(s[i])) i++;
T.push(['num', parseFloat(s.slice(st, i))]); continue;
}
if (/[A-Za-z_]/.test(c)) {
var sa = i; while (i < s.length && /[A-Za-z0-9_]/.test(s[i])) i++;
T.push(['id', s.slice(sa, i)]); continue;
}
var zwei = s.substr(i, 2);
if (['>=', '<=', '==', '!='].indexOf(zwei) >= 0) { T.push(['op', zwei]); i += 2; continue; }
T.push(['op', c]); i++;
}
var k = 0;
function ist(w) { return k < T.length && T[k][0] === 'op' && T[k][1] === w; }
function fn(n, a) {
var x = a[0] || 0;
switch (n) {
case 'sqrt': return x < 0 ? 0 : Math.sqrt(x);
case 'pow': return Math.pow(x, a[1] || 0);
case 'abs': return Math.abs(x);
case 'min': return Math.min.apply(null, a);
case 'max': return Math.max.apply(null, a);
case 'round': return Math.round(x);
case 'floor': return Math.floor(x);
case 'ceil': return Math.ceil(x);
}
return 0;
}
function primary() {
if (k >= T.length) return 0;
var t = T[k];
if (t[0] === 'num') { k++; return t[1]; }
if (t[0] === 'id') {
var n = t[1]; k++;
if (ist('(')) {
k++; var a = [];
if (!ist(')')) { a.push(ternary()); while (ist(',')) { k++; a.push(ternary()); } }
if (ist(')')) k++;
return fn(n, a);
}
return vars[n] !== undefined ? Number(vars[n]) : 0;
}
if (ist('(')) { k++; var r = ternary(); if (ist(')')) k++; return r; }
k++; return 0;
}
function unary() { if (ist('-')) { k++; return -unary(); } if (ist('+')) { k++; return unary(); } return primary(); }
function mul() { var l = unary(); while (ist('*') || ist('/')) { var o = T[k][1]; k++; var r = unary(); l = o === '*' ? l * r : (r === 0 ? 0 : l / r); } return l; }
function add() { var l = mul(); while (ist('+') || ist('-')) { var o = T[k][1]; k++; var r = mul(); l = o === '+' ? l + r : l - r; } return l; }
function cmp() {
var l = add();
while (k < T.length && T[k][0] === 'op' && ['>=', '<=', '==', '!=', '>', '<'].indexOf(T[k][1]) >= 0) {
var o = T[k][1]; k++; var r = add();
l = o === '>=' ? l >= r : o === '<=' ? l <= r : o === '>' ? l > r : o === '<' ? l < r : o === '==' ? l == r : l != r;
}
return l;
}
function ternary() {
var c = cmp();
if (ist('?')) { k++; var a = ternary(); if (ist(':')) k++; var b = ternary(); return c ? a : b; }
return c;
}
var out = Number(ternary());
return isFinite(out) ? out : 1;
}
function berechne(m, mitKuvert) {
m = Math.max(1, parseInt(m, 10) || 1);
var mult = par.norm > 0
? formelEval(par.formel, { qty: m, menge: m, norm: par.norm, base_price: par.base, a: par.a, e: par.e })
: 1;
if (!isFinite(mult) || mult <= 0) mult = 1;
var grund = par.base * m * mult;
var besteMin = -1, bestePercent = 0;
(par.tiers || []).forEach(function (t) {
if (m >= t.min_qty && t.percent > 0 && t.min_qty > besteMin) { besteMin = t.min_qty; bestePercent = t.percent; }
});
if (bestePercent > 0) grund *= (1 - bestePercent / 100);
var netto = grund / m;
if (mitKuvert) netto += par.porto;
return mitMwst ? netto * (1 + par.mwst / 100) : netto;
}
function fmt(v) { return v.toLocaleString('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + ' €'; }
function aktualisiere() { wert.textContent = fmt(berechne(menge.value, kuvert.checked)); }
menge.addEventListener('input', aktualisiere);
kuvert.addEventListener('change', aktualisiere);
// Erst beim Verlassen des Feldes auf 1 zurücksetzen – währenddessen darf es leer sein.
menge.addEventListener('blur', function () {
var n = parseInt(menge.value, 10);
if (!(n >= 1)) { menge.value = '1'; aktualisiere(); }
});
})();
</script>
<?php
return ob_get_clean();
}
/** [skrift_preis_inkl produkt="briefe"] – Preis inkl. MwSt. */
add_shortcode('skrift_preis_inkl', function ($atts) { return skrift_preis_render($atts, true); });
/** [skrift_preis_exkl produkt="briefe"] – Preis exkl. MwSt. */
add_shortcode('skrift_preis_exkl', function ($atts) { return skrift_preis_render($atts, false); });
/** Alt-Shortcode bleibt gültig und zeigt den Bruttopreis. */
add_shortcode('skrift_preis', function ($atts) { return skrift_preis_render($atts, true); });