if (!defined('ABSPATH')) exit;
final class MVD_Core_Experience_Sheets {
const VERSION = '4.19.0';
const META_SHEET = '_mvd_exp_sheet';
const META_UUID = '_mvd_exp_uuid';
const META_VERSION = '_mvd_exp_version';
const META_LAST_SYNC = '_mvd_exp_last_sync';
const META_SYNC_ERROR = '_mvd_exp_sync_error';
const CAT_SLUG = 'esperienze-mvd';
const REST_NS = 'mvd-experience/v1';
const SIGNATURE_MAX_SKEW = 600;
private static $instance;
private $applying_remote = false;
private $updating_core_meta = false;
public static function instance() {
if (!self::$instance) self::$instance = new self();
return self::$instance;
}
private function __construct() {
add_action('add_meta_boxes', [$this,'add_meta_boxes'], 30);
add_action('save_post_mvd_item', [$this,'save_core_item'], 30, 2);
add_action('mvd_catalog_item_upserted', [$this,'capture_core_payload'], 30, 2);
add_filter('mvd_item_array_payload', [$this,'augment_core_item'], 30, 2);
add_filter('template_include', [$this,'core_template'], 99);
add_action('admin_notices', [$this,'admin_notices']);
add_shortcode('mvd_exp', [$this,'shortcode_field']);
add_shortcode('mvd_exp_badge', [$this,'shortcode_badge']);
add_shortcode('mvd_exp_activity_info', [$this,'shortcode_activity_info']);
add_shortcode('mvd_exp_highlights', [$this,'shortcode_highlights']);
add_shortcode('mvd_exp_itinerary', [$this,'shortcode_itinerary']);
add_shortcode('mvd_exp_included', [$this,'shortcode_included']);
add_shortcode('mvd_exp_meeting_points', [$this,'shortcode_meeting_points']);
add_shortcode('mvd_exp_food', [$this,'shortcode_food']);
add_shortcode('mvd_exp_what_to_bring', [$this,'shortcode_what_to_bring']);
add_shortcode('mvd_exp_pricing', [$this,'shortcode_pricing']);
add_shortcode('mvd_experience_sheet', [$this,'shortcode_full_sheet']);
}
public static function activate() {
if (!term_exists(self::CAT_SLUG, 'category')) wp_insert_term('Esperienze MVD', 'category', ['slug'=>self::CAT_SLUG]);
}
public function ensure_category() {
if (is_admin() && current_user_can('manage_categories') && !term_exists(self::CAT_SLUG,'category')) {
wp_insert_term('Esperienze MVD','category',['slug'=>self::CAT_SLUG]);
}
}
private function is_core() { return class_exists('MVD_Network_Core'); }
private function is_connector() { return class_exists('MVD_Network_Connector') || get_option('mvd_connector_settings', false) !== false; }
private function default_sheet() {
return [
'schema'=>'mvd-experience-sheet/1',
'base'=>[
'code'=>'','commercial_title'=>'','subtitle'=>'','badge'=>'','day_type'=>'',
'short_description'=>'','full_description'=>'','duration_minutes'=>'','start_time'=>'','return_time'=>'',
'languages'=>'','audio_guide'=>'','accessibility_status'=>'','accessibility_notes'=>'',
'municipality'=>'','latitude'=>'','longitude'=>'','seasonality'=>'','availability_notes'=>'',
'cancellation_free_days'=>'','payment_mode'=>'','related_experience_url'=>'','image_url'=>'','gallery_urls'=>'','video_url'=>'',
'min_participants'=>'','max_participants'=>'','age_min'=>'','age_max'=>'','notes'=>'',
],
'activity_info'=>[], 'highlights'=>[], 'itinerary'=>[], 'included'=>[],
'meeting_points'=>[], 'food'=>[], 'what_to_bring'=>[], 'pricing'=>[], 'benefits'=>[], 'rules'=>[],
];
}
private function get_sheet($post_id) {
$sheet = get_post_meta($post_id, self::META_SHEET, true);
if (!is_array($sheet)) $sheet = [];
return $this->normalize_sheet($sheet);
}
private function normalize_sheet($sheet) {
$d=$this->default_sheet();
if (!is_array($sheet)) return $d;
$out=$d;
$out['schema']='mvd-experience-sheet/1';
$base=is_array($sheet['base']??null)?$sheet['base']:[];
foreach($d['base'] as $k=>$v) $out['base'][$k]=isset($base[$k])?(string)$base[$k]:'';
foreach(['activity_info','highlights','itinerary','included','meeting_points','food','what_to_bring','pricing','benefits','rules'] as $group){
$out[$group]=[];
foreach((array)($sheet[$group]??[]) as $row){
if(!is_array($row))continue;
$clean=[];foreach($row as $k=>$v){$clean[sanitize_key($k)]=is_scalar($v)?(string)$v:'';}
if(array_filter($clean,function($v){return trim((string)$v)!=='';}))$out[$group][]=$clean;
}
}
return $out;
}
private function sanitize_sheet($raw) {
$raw=is_array($raw)?wp_unslash($raw):[];
$sheet=$this->normalize_sheet($raw);
foreach($sheet['base'] as $k=>$v){
if(in_array($k,['short_description','full_description','accessibility_notes','availability_notes','notes'],true))$sheet['base'][$k]=wp_kses_post($v);
elseif(in_array($k,['related_experience_url','image_url','video_url'],true))$sheet['base'][$k]=esc_url_raw($v);
elseif($k==='gallery_urls'){
$urls=preg_split('/[\r\n,]+/',(string)$v);$urls=array_values(array_filter(array_map('esc_url_raw',$urls)));$sheet['base'][$k]=implode("\n",$urls);
} else $sheet['base'][$k]=sanitize_text_field($v);
}
$textarea_keys=['text','note','notes','description','value','value_template','condition','services','reference'];
$url_keys=['icon_url','image_url','map_url','url'];
foreach(['activity_info','highlights','itinerary','included','meeting_points','food','what_to_bring','pricing','benefits','rules'] as $group){
foreach($sheet[$group] as &$row){
foreach($row as $k=>$v){
if(in_array($k,$url_keys,true))$row[$k]=esc_url_raw($v);
elseif(in_array($k,$textarea_keys,true))$row[$k]=wp_kses_post($v);
else $row[$k]=sanitize_text_field($v);
}
if(empty($row['row_id']))$row['row_id']='row_'.wp_generate_uuid4();
} unset($row);
}
return $sheet;
}
public function add_meta_boxes() {
if(post_type_exists('mvd_item')) add_meta_box('mvd_exp_sheet_core','Scheda Esperienza · sorgente merchant',[$this,'core_meta_box'],'mvd_item','normal','high');
}
public function merchant_meta_box($post) {
$active=has_category(self::CAT_SLUG,$post);
$sheet=$this->get_sheet($post->ID);$this->editor_sheet_cache=$sheet;$version=max(1,absint(get_post_meta($post->ID,self::META_VERSION,true)));
wp_nonce_field('mvd_exp_sheet_save','mvd_exp_sheet_nonce');
echo '
';
echo '
Stato: '.($active?'Esperienza MVD attiva':'Aggiungi la categoria '.esc_html(self::CAT_SLUG).' per sincronizzarla con MVD.').' Versione: '.esc_html($version).'
';
$this->render_editor($sheet,'mvd_exp_sheet');
echo 'Importa / esporta con ChatGPT, Excel o Google Sheets
Incolla un bundle TSV MVD. Se il campo non è vuoto, al prossimo salvataggio dell’articolo sostituisce la scheda corrente.
Export attuale
';
echo '
';$this->admin_assets();
}
public function core_meta_box($post) {
if(get_post_meta($post->ID,'_mvd_item_type',true)!=='experience'){echo '
Merchant: '.esc_html(get_post_meta($post->ID,'_mvd_merchant_id',true)).' Versione sorgente: '.esc_html(absint(get_post_meta($post->ID,self::META_VERSION,true))).'
';
echo '
Le modifiche salvate qui vengono inviate al merchant. Se il merchant ha già una versione più recente, l’aggiornamento viene bloccato per evitare sovrascritture.
';
foreach($rows as $i=>$row)$this->repeater_row($name,$group,$i,$row,$cols);
echo '
';
echo '';$this->repeater_row($name,$group,'__i__',[],$cols);echo'';
}
private $editor_sheet_cache=null;
private function current_editor_sheet(){return is_array($this->editor_sheet_cache)?$this->editor_sheet_cache:$this->default_sheet();}
private function repeater_row($name,$group,$i,$row,$cols){echo'
';foreach($cols as $key=>$label){$v=$row[$key]??'';$islong=in_array($key,['description','note','notes','text','services','condition','reference','value_template','value'],true);echo'
'.($islong?'':'').'
';}echo'
';}
private function admin_assets(){
echo '';
}
public function save_merchant_post($post_id,$post) {
if($this->applying_remote||wp_is_post_revision($post_id)||defined('DOING_AUTOSAVE')&&DOING_AUTOSAVE)return;
if(!isset($_POST['mvd_exp_sheet_nonce'])||!wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['mvd_exp_sheet_nonce'])),'mvd_exp_sheet_save')||!current_user_can('edit_post',$post_id))return;
$raw=$_POST['mvd_exp_sheet']??[];$import=trim((string)wp_unslash($_POST['mvd_exp_import']??''));
if($import!==''){$parsed=$this->parse_tsv($import);if(!is_wp_error($parsed))$raw=$parsed;else update_post_meta($post_id,self::META_SYNC_ERROR,'Import TSV: '.$parsed->get_error_message());}
$sheet=$this->sanitize_sheet($raw);$this->editor_sheet_cache=$sheet;
if(!get_post_meta($post_id,self::META_UUID,true))update_post_meta($post_id,self::META_UUID,wp_generate_uuid4());
$ver=max(0,absint(get_post_meta($post_id,self::META_VERSION,true)))+1;update_post_meta($post_id,self::META_VERSION,$ver);update_post_meta($post_id,self::META_SHEET,$sheet);
if(has_category(self::CAT_SLUG,$post_id))$this->push_merchant_to_core($post_id,$sheet,$ver);
}
public function save_core_item($post_id,$post) {
if($this->updating_core_meta)return;
if(wp_is_post_revision($post_id)||defined('DOING_AUTOSAVE')&&DOING_AUTOSAVE)return;
if(!isset($_POST['mvd_exp_core_nonce'])||!wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['mvd_exp_core_nonce'])),'mvd_exp_core_save')||!current_user_can('edit_post',$post_id))return;
if(get_post_meta($post_id,'_mvd_item_type',true)!=='experience')return;
$sheet=$this->sanitize_sheet($_POST['mvd_exp_sheet']??[]);$base_version=absint(get_post_meta($post_id,self::META_VERSION,true));
$r=$this->push_core_to_merchant($post_id,$sheet,$base_version);
if(is_wp_error($r)){update_post_meta($post_id,self::META_SYNC_ERROR,$r->get_error_message());update_post_meta($post_id,'_mvd_exp_pending_sheet',$sheet);return;}
$sheet=$this->normalize_sheet($r['sheet']??$sheet);update_post_meta($post_id,self::META_SHEET,$sheet);update_post_meta($post_id,self::META_VERSION,absint($r['version']??($base_version+1)));delete_post_meta($post_id,self::META_SYNC_ERROR);delete_post_meta($post_id,'_mvd_exp_pending_sheet');$this->sync_standard_core_meta($post_id,$sheet);
}
public function admin_notices(){global $post;if(!$post||!current_user_can('edit_post',$post->ID))return;$err=get_post_meta($post->ID,self::META_SYNC_ERROR,true);if($err)echo'
Scheda Esperienza MVD: '.esc_html($err).'
';}
public function register_rest(){register_rest_route(self::REST_NS,'/update',['methods'=>'POST','callback'=>[$this,'rest_update_merchant'],'permission_callback'=>'__return_true']);}
private function connector_settings(){if(class_exists('MVD_Network_Connector')&&method_exists('MVD_Network_Connector','settings'))return MVD_Network_Connector::settings();$s=get_option('mvd_connector_settings',[]);return is_array($s)?$s:[];}
private function verify_merchant_request(WP_REST_Request $request){$s=$this->connector_settings();$raw=$request->get_body();$d=json_decode($raw,true);if(!is_array($d))return new WP_Error('mvd_exp_json','JSON non valido',['status'=>400]);if(sanitize_key($d['merchant_id']??'')!==sanitize_key($s['merchant_id']??''))return new WP_Error('mvd_exp_merchant','Merchant non valido',['status'=>403]);$sig=(string)$request->get_header('x-mvd-signature');$ts=(string)$request->get_header('x-mvd-timestamp');$nonce=(string)$request->get_header('x-mvd-nonce');if(!$sig||!$ts||!$nonce||!ctype_digit($ts)||abs(time()-(int)$ts)>self::SIGNATURE_MAX_SKEW)return new WP_Error('mvd_exp_signature','Firma non valida o scaduta',['status'=>403]);$key='mvd_exp_nonce_'.hash('sha256',$nonce);if(get_transient($key))return new WP_Error('mvd_exp_replay','Richiesta già usata',['status'=>409]);$expected=hash_hmac('sha256',$ts.'.'.$nonce.'.'.$raw,(string)($s['secret']??''));if(!$s['secret']||!hash_equals($expected,$sig))return new WP_Error('mvd_exp_signature','Firma non valida',['status'=>403]);set_transient($key,1,self::SIGNATURE_MAX_SKEW*2);return$d;}
public function rest_update_merchant(WP_REST_Request $request){$d=$this->verify_merchant_request($request);if(is_wp_error($d))return$d;$uuid=sanitize_text_field($d['experience_uuid']??'');$source_id=absint($d['source_post_id']??0);$post_id=0;if($source_id&&get_post_type($source_id)==='post'&&get_post_meta($source_id,self::META_UUID,true)===$uuid)$post_id=$source_id;if(!$post_id&&$uuid){$q=new WP_Query(['post_type'=>'post','post_status'=>['publish','draft','private','pending'],'posts_per_page'=>1,'fields'=>'ids','meta_key'=>self::META_UUID,'meta_value'=>$uuid]);$post_id=absint($q->posts[0]??0);}if(!$post_id)return new WP_Error('mvd_exp_not_found','Esperienza non trovata sul merchant',['status'=>404]);$current=max(1,absint(get_post_meta($post_id,self::META_VERSION,true)));$expected=absint($d['expected_version']??0);if($expected&&$expected!==$current)return new WP_Error('mvd_exp_conflict','Il merchant contiene una versione più recente ('.$current.'). Ricarica prima la scheda.',['status'=>409,'current_version'=>$current]);$sheet=$this->sanitize_sheet($d['sheet']??[]);$newver=$current+1;$this->applying_remote=true;update_post_meta($post_id,self::META_SHEET,$sheet);update_post_meta($post_id,self::META_VERSION,$newver);update_post_meta($post_id,self::META_LAST_SYNC,current_time('mysql'));delete_post_meta($post_id,self::META_SYNC_ERROR);$this->applying_remote=false;return new WP_REST_Response(['success'=>true,'post_id'=>$post_id,'version'=>$newver,'sheet'=>$sheet],200);}
private function push_merchant_to_core($post_id,$sheet,$version){$s=$this->connector_settings();if(empty($s['network_url'])||empty($s['merchant_id'])||empty($s['secret'])){update_post_meta($post_id,self::META_SYNC_ERROR,'Scheda salvata localmente. Connector MVD non configurato: sincronizzazione non eseguita.');return false;}$item=$this->merchant_payload($post_id,$sheet,$version,$s);$payload=['merchant_id'=>sanitize_key($s['merchant_id']),'item'=>$item];$r=$this->signed_post(untrailingslashit($s['network_url']).'/wp-json/mvd/v1/catalog/upsert',$payload,$s['secret']);if(is_wp_error($r)){update_post_meta($post_id,self::META_SYNC_ERROR,$r->get_error_message());return false;}$code=wp_remote_retrieve_response_code($r);$j=json_decode(wp_remote_retrieve_body($r),true);if($code<200||$code>=300){update_post_meta($post_id,self::META_SYNC_ERROR,'Core MVD HTTP '.$code.': '.sanitize_text_field($j['message']??$j['error']??''));return false;}update_post_meta($post_id,self::META_LAST_SYNC,current_time('mysql'));delete_post_meta($post_id,self::META_SYNC_ERROR);return true;}
private function merchant_payload($post_id,$sheet,$version,$s){$b=$sheet['base'];$uuid=get_post_meta($post_id,self::META_UUID,true);if(!$uuid){$uuid=wp_generate_uuid4();update_post_meta($post_id,self::META_UUID,$uuid);} $title=$b['commercial_title']?:get_the_title($post_id);$image=$b['image_url']?:get_the_post_thumbnail_url($post_id,'full');$gallery=array_values(array_filter(array_map('esc_url_raw',preg_split('/[\r\n,]+/',$b['gallery_urls']))));$price=$this->minimum_price($sheet);$meeting=$this->first_meeting_point($sheet);$languages=$this->csv($b['languages']);$item=['resource_id'=>'mvd:'.sanitize_key($s['merchant_id']).':experience:'.$uuid,'remote_product_id'=>(string)$post_id,'sku'=>$b['code'],'title'=>$title,'description'=>$b['full_description']?:$b['short_description'],'short_description'=>$b['short_description'],'item_type'=>'experience','item_category'=>'experience','owner_category'=>sanitize_key($s['merchant_category']??''),'distribution_mode'=>'normal','price'=>$price,'sale_price'=>'','currency'=>'EUR','origin_url'=>get_permalink($post_id),'image_url'=>$image,'gallery'=>$gallery,'modified_gmt'=>get_post_modified_time('c',true,$post_id),'duration_minutes'=>absint($b['duration_minutes']),'meeting_point'=>$meeting,'municipality'=>$b['municipality'],'latitude'=>$b['latitude'],'longitude'=>$b['longitude'],'languages'=>$languages,'min_participants'=>absint($b['min_participants']),'max_participants'=>absint($b['max_participants']),'accessibility'=>trim(wp_strip_all_tags($b['accessibility_status'].' '.$b['accessibility_notes'])),'age_min'=>$b['age_min'],'age_max'=>$b['age_max'],'cancellation_policy'=>$this->cancellation_text($sheet),'seasonality'=>$b['seasonality'].' '.$b['availability_notes'],'experience_enabled'=>true,'experience_sale_mode'=>'booking','experience_availability_type'=>'request','experience_price_type'=>'mixed','experience_included'=>array_values(array_map(function($r){return wp_strip_all_tags($r['text']??'');},array_filter($sheet['included'],function($r){return ($r['status']??'')==='included';}))),'experience_excluded'=>array_values(array_map(function($r){return wp_strip_all_tags($r['text']??'');},array_filter($sheet['included'],function($r){return ($r['status']??'')==='excluded';}))),'experience_what_to_bring'=>array_values(array_map(function($r){return wp_strip_all_tags($r['title']??'');},$sheet['what_to_bring'])),'experience_sheet'=>$sheet,'experience_uuid'=>$uuid,'experience_source_version'=>$version,'source_post_type'=>'post'];$hashItem=$item;unset($hashItem['payload_hash']);$item['payload_hash']=sha1(wp_json_encode($hashItem));return$item;}
private function signed_post($url,$payload,$secret,$timeout=25){$payload['correlation_id']=$payload['correlation_id']??wp_generate_uuid4();$body=wp_json_encode($payload);$ts=(string)time();$nonce=wp_generate_password(24,false,false);$sig=hash_hmac('sha256',$ts.'.'.$nonce.'.'.$body,$secret);return wp_remote_post($url,['timeout'=>$timeout,'headers'=>['Content-Type'=>'application/json','X-MVD-Signature'=>$sig,'X-MVD-Timestamp'=>$ts,'X-MVD-Nonce'=>$nonce,'X-MVD-Correlation-ID'=>$payload['correlation_id']],'body'=>$body]);}
public function capture_core_payload($post_id,$item){if(!is_array($item)||($item['item_type']??'')!=='experience'||empty($item['experience_sheet']))return;$sheet=$this->sanitize_sheet($item['experience_sheet']);update_post_meta($post_id,self::META_SHEET,$sheet);if(!empty($item['experience_uuid']))update_post_meta($post_id,self::META_UUID,sanitize_text_field($item['experience_uuid']));if(isset($item['experience_source_version']))update_post_meta($post_id,self::META_VERSION,absint($item['experience_source_version']));update_post_meta($post_id,self::META_LAST_SYNC,current_time('mysql'));delete_post_meta($post_id,self::META_SYNC_ERROR);$this->sync_standard_core_meta($post_id,$sheet);}
public function augment_core_item($row,$post_id){$sheet=$this->get_sheet($post_id);if($this->sheet_has_data($sheet)){$row['experience_sheet']=$sheet;$row['experience_uuid']=(string)get_post_meta($post_id,self::META_UUID,true);$row['experience_source_version']=absint(get_post_meta($post_id,self::META_VERSION,true));}return$row;}
private function push_core_to_merchant($post_id,$sheet,$base_version){if(!class_exists('MVD_Network_Core'))return new WP_Error('mvd_exp_no_core','MVD Network Core non disponibile.');$merchant_id=sanitize_key(get_post_meta($post_id,'_mvd_merchant_id',true));$merchant=MVD_Network_Core::get_merchant($merchant_id);if(!$merchant||empty($merchant['site_url'])||empty($merchant['secret']))return new WP_Error('mvd_exp_no_merchant','Configurazione merchant incompleta sul Core.');$payload=['merchant_id'=>$merchant_id,'experience_uuid'=>(string)get_post_meta($post_id,self::META_UUID,true),'source_post_id'=>absint(get_post_meta($post_id,'_mvd_remote_product_id',true)),'expected_version'=>$base_version,'sheet'=>$sheet];$url=untrailingslashit($merchant['site_url']).'/wp-json/'.self::REST_NS.'/update';$r=$this->signed_post($url,$payload,$merchant['secret']);if(is_wp_error($r))return$r;$code=wp_remote_retrieve_response_code($r);$j=json_decode(wp_remote_retrieve_body($r),true);if($code<200||$code>=300)return new WP_Error('mvd_exp_remote_'.$code,is_array($j)&&!empty($j['message'])?$j['message']:'Merchant HTTP '.$code,['status'=>$code,'body'=>$j]);return is_array($j)?$j:[];}
private function sync_standard_core_meta($post_id,$sheet){$b=$sheet['base'];update_post_meta($post_id,'_mvd_duration_minutes',absint($b['duration_minutes']));update_post_meta($post_id,'_mvd_meeting_point',$this->first_meeting_point($sheet));update_post_meta($post_id,'_mvd_municipality',sanitize_text_field($b['municipality']));update_post_meta($post_id,'_mvd_latitude',sanitize_text_field($b['latitude']));update_post_meta($post_id,'_mvd_longitude',sanitize_text_field($b['longitude']));update_post_meta($post_id,'_mvd_languages',$this->csv($b['languages']));update_post_meta($post_id,'_mvd_min_participants',absint($b['min_participants']));update_post_meta($post_id,'_mvd_max_participants',absint($b['max_participants']));update_post_meta($post_id,'_mvd_accessibility',wp_strip_all_tags($b['accessibility_status'].' '.$b['accessibility_notes']));update_post_meta($post_id,'_mvd_age_min',absint($b['age_min']));update_post_meta($post_id,'_mvd_age_max',absint($b['age_max']));update_post_meta($post_id,'_mvd_price',$this->minimum_price($sheet));if($b['commercial_title']&&get_the_title($post_id)!==wp_strip_all_tags($b['commercial_title'])){$this->updating_core_meta=true;wp_update_post(['ID'=>$post_id,'post_title'=>sanitize_text_field($b['commercial_title'])]);$this->updating_core_meta=false;}}
public function core_template($template){
if(!is_singular('mvd_item'))return $template;
$id=get_queried_object_id();
if(get_post_meta($id,'_mvd_item_type',true)!=='experience')return $template;
$sheet=$this->get_sheet($id);if(!$this->sheet_has_data($sheet))return $template;
$custom=dirname(__DIR__).'/templates/single-mvd-experience.php';
return file_exists($custom)?$custom:$template;
}
public function render_core_single($content){if(!is_singular('mvd_item')||!in_the_loop()||!is_main_query())return$content;$id=get_the_ID();if(get_post_meta($id,'_mvd_item_type',true)!=='experience')return$content;$sheet=$this->get_sheet($id);if(!$this->sheet_has_data($sheet))return$content;return $this->render_full($sheet,$id);}
private function sheet_has_data($sheet){return !empty($sheet['base']['code'])||!empty($sheet['base']['commercial_title'])||!empty($sheet['highlights'])||!empty($sheet['pricing']);}
private function ctx_post_id($atts){$a=shortcode_atts(['post_id'=>0],$atts);return absint($a['post_id'])?:get_the_ID();}
public function shortcode_field($atts){$a=shortcode_atts(['field'=>'','post_id'=>0],$atts);$s=$this->get_sheet($this->ctx_post_id($a));$key=sanitize_key($a['field']);$v=$s['base'][$key]??'';if($key==='duration_minutes')return esc_html($this->format_duration($v));if($key==='languages')return esc_html($this->format_languages($v));return wp_kses_post($v);}
public function shortcode_badge($atts){$s=$this->get_sheet($this->ctx_post_id($atts));$v=$s['base']['badge'];return $this->frontend_css().($v?''.esc_html($v).'':'');}
public function shortcode_activity_info($atts){$s=$this->get_sheet($this->ctx_post_id($atts));return $this->frontend_css().$this->render_activity($s);}
public function shortcode_highlights($atts){$s=$this->get_sheet($this->ctx_post_id($atts));return $this->frontend_css().$this->render_highlights($s);}
public function shortcode_itinerary($atts){$s=$this->get_sheet($this->ctx_post_id($atts));return $this->frontend_css().$this->render_itinerary($s);}
public function shortcode_included($atts){$s=$this->get_sheet($this->ctx_post_id($atts));return $this->frontend_css().$this->render_included($s);}
public function shortcode_meeting_points($atts){$s=$this->get_sheet($this->ctx_post_id($atts));return $this->frontend_css().$this->render_meetings($s);}
public function shortcode_food($atts){$s=$this->get_sheet($this->ctx_post_id($atts));return $this->frontend_css().$this->render_food($s);}
public function shortcode_what_to_bring($atts){$s=$this->get_sheet($this->ctx_post_id($atts));return $this->frontend_css().$this->render_bring($s);}
public function shortcode_pricing($atts){$s=$this->get_sheet($this->ctx_post_id($atts));return $this->frontend_css().$this->render_pricing($s);}
public function shortcode_full_sheet($atts){$id=$this->ctx_post_id($atts);return $this->render_full($this->get_sheet($id),$id);}
private function token_value($token,$s){$b=$s['base'];switch($token){case'duration':return$this->format_duration($b['duration_minutes']);case'languages':return$this->format_languages($b['languages']);case'start_time':return$b['start_time'];case'return_time':return$b['return_time'];case'cancellation_free_days':return$b['cancellation_free_days'];case'audio_guide':return$b['audio_guide'];case'accessibility_status':return$b['accessibility_status'];case'payment_mode':return$b['payment_mode'];default:return isset($b[$token])?$b[$token]:'';}}
private function tokens($text,$s){return preg_replace_callback('/\{([a-z0-9_]+)\}/i',function($m)use($s){return esc_html($this->token_value(sanitize_key($m[1]),$s));},(string)$text);}
private function icon($url){return$url?'':'';}
private function render_activity($s){if(empty($s['activity_info']))return'';$o='
'.$this->icon($r['icon_url']??'').''.esc_html(trim(($r['distance_km']??'')!==''?'Distanza '.$r['distance_km'].' km ':'').trim(($r['transfer_min']??'')!==''?'('.$r['transfer_min'].' min)':'')).''.(!empty($r['transport'])?'
';return$o;}
private function render_pricing($s){if(empty($s['pricing'])&&empty($s['benefits']))return'';$groups=[];foreach($s['pricing'] as$r){$aud=trim($r['audience']??'')?:'Tariffe';$groups[$aud][]=$r;}$o='
';return$o;}
private function render_full($s,$post_id=0){$b=$s['base'];$o=$this->frontend_css();$o.='';$o.=''.($b['badge']?''.esc_html($b['badge']).'':'').'
':'').'';
if($s['activity_info'])$o.=$this->section('Informazioni sull’attività',$this->render_activity($s));if($s['highlights'])$o.=$this->section('In evidenza',$this->render_highlights($s));if($s['itinerary'])$o.=$this->section('Descrizione completa',$this->render_itinerary($s));if($s['included'])$o.=$this->section('Cosa è incluso',$this->render_included($s));if($s['meeting_points'])$o.=$this->section('Punto di incontro',$this->render_meetings($s));if($s['food'])$o.=$this->section('Dove Mangiare',$this->render_food($s));if($s['what_to_bring'])$o.=$this->section('Come vestire',$this->render_bring($s));if($s['pricing']||$s['benefits'])$o.=$this->section('Tariffe',$this->render_pricing($s));$o.='';return$o;}
private function section($title,$content){return'
'.esc_html($title).'
'.$content.'
';}
private function frontend_css(){static$done=false;if($done)return'';$done=true;return'';}
private function format_duration($min){$min=absint($min);if(!$min)return'';$h=intdiv($min,60);$m=$min%60;if($h&&$m)return$h.' h '.$m.' min';if($h)return$h.' '.($h===1?'ora':'ore');return$m.' min';}
private function format_languages($csv){$map=['it'=>'Italiano','en'=>'Inglese','fr'=>'Francese','de'=>'Tedesco','es'=>'Spagnolo','nl'=>'Olandese'];$a=$this->csv($csv);return implode(', ',array_map(function($v)use($map){return$map[$v]??strtoupper($v);},$a));}
private function csv($v){if(is_array($v))$a=$v;else$a=preg_split('/[,;|]+/',(string)$v);return array_values(array_filter(array_map('sanitize_key',$a)));}
private function range_label($min,$max,$unit){$min=trim((string)$min);$max=trim((string)$max);if($min===''&&$max==='')return'';if($min!==''&&$max!=='')return$min.'–'.$max.' '.$unit;if($min!=='')return$min.'+ '.$unit;return'0–'.$max.' '.$unit;}
private function minimum_price($s){$vals=[];foreach($s['pricing'] as$r){$v=str_replace(',','.',trim($r['price']??''));if($v!==''&&is_numeric($v)&&floatval($v)>0)$vals[]=floatval($v);}return$vals?(string)min($vals):'';}
private function first_meeting_point($s){$r=$s['meeting_points'][0]??[];return trim(($r['location']??'').(($r['address']??'')?', '.$r['address']:''));}
private function cancellation_text($s){$d=absint($s['base']['cancellation_free_days']);return$d?'Cancellazione gratuita fino a '.$d.' giorni prima.':'';}
private function export_tsv($s){$out=["# MVD-EXPERIENCE-TSV 1",'[BASE]',"KEY\tVALUE"];foreach($s['base'] as$k=>$v)$out[]=$k."\t".$this->tsv_cell($v);$defs=$this->import_defs();foreach($defs as$group=>$cols){$out[]='';$out[]='['.strtoupper($group).']';$out[]=implode("\t",array_map('strtoupper',$cols));foreach($s[$group] as$r){$line=[];foreach($cols as$c)$line[]=$this->tsv_cell($r[$c]??'');$out[]=implode("\t",$line);}}return implode("\n",$out);}
private function tsv_cell($v){return str_replace(["\t","\r","\n"],[' ','','\\n'],wp_strip_all_tags((string)$v));}
private function import_defs(){return['activity_info'=>['icon_url','title','value_template','note'],'highlights'=>['text'],'itinerary'=>['kind','icon_url','title','location','address','map_url','duration_min','distance_km','transfer_min','transport','variant','description','image_url'],'included'=>['status','text','icon_url'],'meeting_points'=>['condition','location','address','reference','advance_min','latitude','longitude','parking','parking_free','services','notes'],'food'=>['name','type','location','url','phone','booking_required','condition','notes'],'what_to_bring'=>['icon_url','title','note'],'pricing'=>['audience','age_min','age_max','pax_min','pax_max','price','unit','currency','note'],'benefits'=>['condition','price','discount','note'],'rules'=>['type','from','to','value','text']];}
private function parse_tsv($text){$lines=preg_split('/\r\n|\r|\n/',trim($text));$s=$this->default_sheet();$section='';$headers=[];$defs=$this->import_defs();foreach($lines as$raw){$line=trim($raw);if($line===''||strpos($line,'#')===0)continue;if(preg_match('/^\[([A-Z0-9_]+)\]$/i',$line,$m)){$section=strtolower($m[1]);$headers=[];continue;}$cells=array_map(function($v){return str_replace('\\n',"\n",trim($v));},explode("\t",$raw));if($section==='base'){if(strtoupper($cells[0]??'')==='KEY')continue;$k=sanitize_key($cells[0]??'');if($k&&array_key_exists($k,$s['base']))$s['base'][$k]=$cells[1]??'';continue;}if(!isset($defs[$section]))continue;if(!$headers){$candidate=array_map('sanitize_key',$cells);if(array_intersect($candidate,$defs[$section])){$headers=$candidate;continue;}$headers=$defs[$section];} $row=[];foreach($headers as$i=>$k)if(in_array($k,$defs[$section],true))$row[$k]=$cells[$i]??'';$s[$section][]=$row;}return$this->sanitize_sheet($s);}
}