commit 8dad67b4c1edb668f78c98666499f28f926a7d39 Author: Lucas Orth Date: Mon Aug 24 12:14:00 2026 +0200 Import aus Monorepo (frischer Start) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8930253 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.zip +*.log diff --git a/formel.php b/formel.php new file mode 100644 index 0000000..cbf92da --- /dev/null +++ b/formel.php @@ -0,0 +1,155 @@ += <= > < == !=), 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; + } +} diff --git a/skrift-preisrechner.php b/skrift-preisrechner.php new file mode 100644 index 0000000..a1a7791 --- /dev/null +++ b/skrift-preisrechner.php @@ -0,0 +1,422 @@ + 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 '

Preisrechner nicht verfügbar.
' + . esc_html($daten['fehler']) . '

'; + } + $bekannt = array(); + foreach ($daten['produkte'] as $pp) { if (isset($pp['key'])) $bekannt[] = $pp['key']; } + return '

Preisrechner: Produkt „' . esc_html($a['produkt']) . '" nicht gefunden.' + . (count($bekannt) ? '
Verfügbar: ' + . esc_html(implode(', ', $bekannt)) . '' : '') . '

'; + } + + $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(); ?> +
+ + + +
+ Preis pro Stück + € +
+
+
+ +