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
';
}
}
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); }
}
}