class RPB_Admin { const OPT = 'rpb_options'; static function init() { add_action('admin_menu', [__CLASS__,'menu']); add_action('admin_init', [__CLASS__,'register']); } static function menu(){ add_options_page('RailPrice Bridge','RailPrice Bridge','manage_options','rpb', [__CLASS__,'render']); } static function register(){ register_setting(self::OPT, self::OPT); add_settings_section('rpb_main','Einstellungen', '__return_false','rpb'); // Für Tempo: Ein großes JSON-Feld add_settings_field('config','Konfiguration (JSON)',[__CLASS__,'field'],'rpb','rpb_main'); } static function field(){ $val = get_option(self::OPT, ['config'=>'']); echo ''; echo '

Beispiel siehe Doku: terminals, matrix, tariffs, provider

'; } static function render(){ echo '

RailPrice Bridge

'; settings_fields(self::OPT); do_settings_sections('rpb'); submit_button(); echo '
'; } } class RPB_Distance { static function init(){} static function km_between($a,$b){ // $a=['lat'=>..,'lng'=>..], $b=['lat'=>..,'lng'=>..] $R=6371; $dLat=deg2rad($b['lat']-$a['lat']); $dLng=deg2rad($b['lng']-$a['lng']); $lat1=deg2rad($a['lat']); $lat2=deg2rad($b['lat']); $h = sin($dLat/2)**2 + cos($lat1)*cos($lat2)*sin($dLng/2)**2; return 2*$R*asin(min(1,sqrt($h))); } // TODO: add google/mapbox directions if provider configured. } class RPB_Calculator { static function init(){ add_action('rest_api_init', function(){ register_rest_route('railprice/v1','/quote',[ 'methods'=>'POST', 'permission_callback'=>'__return_true', 'callback'=>[__CLASS__,'quote'] ]); }); add_action('wp_enqueue_scripts',[__CLASS__,'enqueue']); } static function enqueue(){ // Nur auf Seiten mit Buchungsformular (notfalls überall) wp_enqueue_script('rpb-bridge', RPB_URL.'assets/bridge.js', ['jquery'], RPB_VER, true); wp_localize_script('rpb-bridge','RPB',{ 'rest': esc_url_raw( rest_url('railprice/v1/quote') ) }); wp_enqueue_style('rpb-bridge', RPB_URL.'assets/bridge.css', [], RPB_VER); } static function cfg(){ $opt = get_option(RPB_Admin::OPT,[]); $cfg = json_decode($opt['config'] ?? '{}', true); return $cfg ?: []; } static function nearest($points, $ref){ $best=null; $min=INF; foreach($points as $p){ $d = RPB_Distance::km_between(['lat'=>$p['lat'],'lng'=>$p['lng']], $ref); if($d < $min){ $min=$d; $best=$p; } } return $best; } static function find_matrix_entry($cfg, $de_id, $ua_id){ foreach($cfg['matrix'] as $m){ if($m['de_id']===$de_id && $m['ua_id']===$ua_id) return $m; } return null; } static function quote($req){ $cfg = self::cfg(); $in = $req->get_json_params(); $pickup = ['lat'=>floatval($in['pickup_lat']??0),'lng'=>floatval($in['pickup_lng']??0)]; $dropoff = ['lat'=>floatval($in['dropoff_lat']??0),'lng'=>floatval($in['dropoff_lng']??0)]; $adults = max(0, intval($in['passenger_adult']??0)); $kids = max(0, intval($in['passenger_children']??0)); // 1) DE-Terminal if(($cfg['routing']['de_terminal_mode']??'fixed')==='fixed'){ $de = array_values(array_filter($cfg['terminals'], fn($t)=>$t['id']===$cfg['routing']['fixed_de_id']))[0] ?? null; } else { $de = self::nearest(array_filter($cfg['terminals'], fn($t)=>$t['side']==='DE'), $pickup); } // 2) UA-Terminal if(!empty($in['ua_terminal_id'])){ $ua = array_values(array_filter($cfg['terminals'], fn($t)=>$t['id']===$in['ua_terminal_id']))[0] ?? null; } else { $ua = self::nearest(array_filter($cfg['terminals'], fn($t)=>$t['side']==='UA'), $dropoff); } if(!$de || !$ua) return new WP_Error('rpb_terminal','Terminal nicht gefunden', ['status'=>400]); // 3) Distanzen $km_vorlauf = RPB_Distance::km_between($pickup, ['lat'=>$de['lat'],'lng'=>$de['lng']]); $km_nachlauf= RPB_Distance::km_between(['lat'=>$ua['lat'],'lng'=>$ua['lng']], $dropoff); // 4) Mainrun $mx = self::find_matrix_entry($cfg, $de['id'], $ua['id']); if(!$mx) return new WP_Error('rpb_matrix','Keine Mainrun-Preise für dieses Paar', ['status'=>400]); $main_adult = floatval($mx['price_adult_40'] ?? 0); $main_child = floatval($mx['price_child_20'] ?? 0); $main_eur = ($adults * $main_adult) + ($kids * $main_child); // 5) Tarife $eur_km_de = floatval($cfg['tariffs']['eur_per_km_de'] ?? 0); $eur_km_ua = floatval($cfg['tariffs']['eur_per_km_ua'] ?? 0); $vorlauf_eur = $km_vorlauf * $eur_km_de; $nachlauf_eur = $km_nachlauf * $eur_km_ua; $total = round($vorlauf_eur + $main_eur + $nachlauf_eur, 2); return [ 'currency' => $mx['currency'] ?? 'EUR', 'total' => $total, 'breakdown' => [ 'vorlauf_km' => round($km_vorlauf,1), 'vorlauf_eur'=> round($vorlauf_eur,2), 'mainrun_eur'=> round($main_eur,2), 'nachlauf_km'=> round($km_nachlauf,1), 'nachlauf_eur'=>round($nachlauf_eur,2), ], 'used_de_id'=>$de['id'], 'used_ua_id'=>$ua['id'] ]; } } class RPB_Woo { static function init(){ // Beim Add-to-Cart: unsere Hidden Inputs mitnehmen add_filter('woocommerce_add_cart_item_data',[__CLASS__,'attach_cart_data'],10,3); // Preis setzen add_action('woocommerce_before_calculate_totals',[__CLASS__,'override_price'],20); // Fees (Breakdown) anhängen add_action('woocommerce_cart_calculate_fees',[__CLASS__,'add_fees'],20); // In Order übernehmen add_action('woocommerce_checkout_create_order_line_item',[__CLASS__,'save_item_meta'],10,4); } static function attach_cart_data($item_data, $product_id, $variation_id){ $keys=['rpb_total','rpb_currency','rpb_breakdown','rpb_de_id','rpb_ua_id']; foreach($keys as $k){ if(isset($_POST[$k])) $item_data[$k] = sanitize_text_field($_POST[$k]); } return $item_data; } static function override_price($cart){ if(is_admin() && !defined('DOING_AJAX')) return; foreach($cart->get_cart() as $cart_item){ if(isset($cart_item['rpb_total'])){ $cart_item['data']->set_price( floatval($cart_item['rpb_total']) ); } } } static function add_fees($cart){ foreach($cart->get_cart() as $cart_item){ if(empty($cart_item['rpb_breakdown'])) continue; $c = json_decode($cart_item['rpb_breakdown'], true); if(!$c) continue; $cur = $cart_item['rpb_currency'] ?? get_woocommerce_currency(); // Option: nur 1 Fee (Gesamt) oder 3 Fees (Breakdown). Hier 3: $cart->add_fee('Vorlauf', floatval($c['vorlauf_eur'] ?? 0)); $cart->add_fee('Mainrun', floatval($c['mainrun_eur'] ?? 0)); $cart->add_fee('Nachlauf', floatval($c['nachlauf_eur'] ?? 0)); // Summe der Fees = Gesamtpreis; Da wir oben den Artikelpreis schon auf total gesetzt haben, // kannst du alternativ: Artikel auf 0 setzen und nur Fees nutzen. Entscheide nach Darstellung. break; // nur einmal, wenn 1 Item } } static function save_item_meta($item, $cart_item_key, $values, $order){ $keys=['rpb_total','rpb_currency','rpb_breakdown','rpb_de_id','rpb_ua_id']; foreach($keys as $k){ if(isset($values[$k])) $item->add_meta_data($k, $values[$k], true); } } }