$monday, 'lun2'=>$monday, 'mar1'=>$other, 'mar2'=>'Gérard', 'mer1'=>$monday, 'mer2'=>'Gérard', ]; } public static function next_week_index() { global $wpdb; $value=$wpdb->get_var( "SELECT MAX(CAST(meta_value AS UNSIGNED)) FROM {$wpdb->postmeta} WHERE meta_key='mg_week_index'" ); return $value===null ? 0 : ((int)$value+1); } private static function existing_week_index(DateTimeImmutable $monday) { $start=$monday->format('Y-m-d'); $end=$monday->modify('+6 days')->format('Y-m-d'); $ids=get_posts([ 'post_type'=>'mg_seance', 'post_status'=>'any', 'posts_per_page'=>-1, 'fields'=>'ids', 'meta_query'=>[ 'relation'=>'AND', [ 'key'=>'mg_date', 'value'=>$start, 'compare'=>'>=', 'type'=>'DATE', ], [ 'key'=>'mg_date', 'value'=>$end, 'compare'=>'<=', 'type'=>'DATE', ], ], ]); foreach($ids as $id) { $index=get_post_meta($id,'mg_week_index',true); if ($index!=='' && is_numeric($index)) { return (int)$index; } } return null; } public static function accessory_for($slot,$absolute_week_index) { $seqs=MGPL_Settings::sequences(); $items=array_values( array_filter( array_map( 'trim', explode(',',$seqs[$slot] ?? 'sans') ) ) ); if (!$items) $items=['sans']; return $items[$absolute_week_index % count($items)]; } public static function generate() { if (!current_user_can('manage_options')) { wp_die('Accès refusé'); } check_admin_referer('mgpl_generate'); $start=sanitize_text_field( wp_unslash($_POST['start_date'] ?? '') ); $weeks=max( 1, min( 60, (int)($_POST['weeks'] ?? 4) ) ); $starter=sanitize_text_field( wp_unslash($_POST['starter'] ?? 'Gérard') ); if (!in_array($starter,['Gérard','Noëlle'],true)) { $starter='Gérard'; } $dt=DateTimeImmutable::createFromFormat( '!Y-m-d', $start, wp_timezone() ); if (!$dt) { wp_die('Date invalide'); } $dt=$dt->modify( '-'.((int)$dt->format('N')-1).' days' ); $slots=MGPL_Settings::slots(); $next_new_index=self::next_week_index(); $created=0; $skipped=0; for($w=0;$w<$weeks;$w++) { $monday_animator= ($w%2===0) ? $starter : ($starter==='Gérard' ? 'Noëlle' : 'Gérard'); $anim=self::animators_for_week($monday_animator); $week_monday=$dt->modify('+'.($w*7).' days'); $existing_index=self::existing_week_index($week_monday); if ($existing_index!==null) { $absolute_index=$existing_index; } else { $absolute_index=$next_new_index; $next_new_index++; } foreach($slots as $slot=>$cfg) { $date=$week_monday ->modify('+'.($cfg['day']-1).' days') ->format('Y-m-d'); $existing=get_posts([ 'post_type'=>'mg_seance', 'post_status'=>'any', 'numberposts'=>1, 'fields'=>'ids', 'meta_query'=>[ [ 'key'=>'mg_date', 'value'=>$date, ], [ 'key'=>'mg_creneau', 'value'=>$slot, ], ], ]); if ($existing) { $skipped++; continue; } $id=wp_insert_post([ 'post_type'=>'mg_seance', 'post_status'=>'publish', 'post_title'=>$date.' — '.$cfg['label'], ],true); if (is_wp_error($id)) { continue; } $meta=[ 'mg_date'=>$date, 'mg_creneau'=>$slot, 'mg_activite'=>$cfg['activity'], 'mg_public'=>$cfg['public'], 'mg_hdeb'=>$cfg['start'], 'mg_hfin'=>$cfg['end'], 'mg_anim'=>$anim[$slot], 'mg_acc'=>self::accessory_for( $slot, $absolute_index ), 'mg_salle'=>'Salle rouge', 'mg_annule'=>'0', 'mg_week_index'=>(string)$absolute_index, 'mg_generated'=>'1', ]; foreach($meta as $key=>$value) { update_post_meta($id,$key,$value); } $created++; } } wp_safe_redirect( add_query_arg( [ 'page'=>'mg-planning', 'generated'=>$created, 'skipped'=>$skipped, ], admin_url('admin.php') ) ); exit; }}admin_url('admin-ajax.php'),'nonce'=>wp_create_nonce('mgpl_inline'), 'error'=>'Erreur lors de l’enregistrement', 'cancelConfirm'=>'Annuler cette séance ? Elle restera visible et pourra être réactivée.', ]); } private static function pretty($key) { $map=array_merge(MGPL_Settings::accessories(),[ 'pilates-classic'=>'Classic','power-pilates'=>'Power', 'renforcement'=>'Renfo','debutants'=>'Débutants', 'inter-expert'=>'Intermédiaires / Experts','tout-public'=>'Tout public', ]); return $map[$key] ?? $key; } private static function normalize_date($raw) { $raw=trim((string)$raw); if (preg_match('/^\d{8}$/',$raw)) return substr($raw,0,4).'-'.substr($raw,4,2).'-'.substr($raw,6,2); return $raw; } /** * Liste des animateurs proposés dans la "combo" du back-office. * Gérard et Noëlle restent les valeurs de base. * Tout nom saisi manuellement dans une séance devient ensuite * automatiquement une suggestion du datalist. */ private static function animator_names() { $names = ['Gérard', 'Noëlle']; $ids = get_posts([ 'post_type' => 'mg_seance', 'post_status' => 'any', 'posts_per_page' => -1, 'fields' => 'ids', ]); foreach ($ids as $id) { foreach (['mg_anim', 'mg_anim_other'] as $key) { $name = trim((string) get_post_meta($id, $key, true)); if ( $name !== '' && $name !== 'autre' && !in_array($name, $names, true) ) { $names[] = $name; } } } return $names; } private static function filters() { return [ 'from'=>sanitize_text_field($_GET['from'] ?? ''), 'to'=>sanitize_text_field($_GET['to'] ?? ''), 'activity'=>sanitize_key($_GET['activity'] ?? ''), 'anim'=>sanitize_text_field($_GET['anim'] ?? ''), 'state'=>sanitize_key($_GET['state'] ?? ''), 'accessory'=>sanitize_key($_GET['accessory'] ?? ''), ]; } private static function anim_display($id) { $main=trim((string)get_post_meta($id,'mg_anim',true)); $shadow=trim((string)get_post_meta($id,'mg_anim_shadow',true)); $other=trim((string)get_post_meta($id,'mg_anim_other',true)); $parts=[]; if ($main!=='' && $main!=='autre') $parts[]=$main; if ($shadow!=='') $parts[]=$shadow.' (shadow)'; if ($other!=='') $parts[]=$other; return implode(' + ',$parts); } private static function team_html($id) { $main=trim((string)get_post_meta($id,'mg_anim',true)); $shadow=trim((string)get_post_meta($id,'mg_anim_shadow',true)); $other=trim((string)get_post_meta($id,'mg_anim_other',true)); $html=''; if ($main!=='' && $main!=='autre') $html.=''.esc_html($main).''; if ($shadow!=='') $html.='SHADOW '.esc_html($shadow).''; if ($other!=='') $html.='+ '.esc_html($other).''; return $html!=='' ? $html : '—'; } private static function week_label($date) { $dt=new DateTimeImmutable($date,wp_timezone()); $monday=$dt->modify('-'.((int)$dt->format('N')-1).' days'); $sunday=$monday->modify('+6 days'); return 'Du '.wp_date('d/m/Y',$monday->getTimestamp()).' au '.wp_date('d/m/Y',$sunday->getTimestamp()); } public static function dashboard() { if (!current_user_can('edit_posts')) return; $f=self::filters(); $meta=['relation'=>'AND']; if ($f['activity']) $meta[]=['key'=>'mg_activite','value'=>$f['activity']]; if ($f['accessory']) $meta[]=['key'=>'mg_acc','value'=>$f['accessory']]; if ($f['anim']) { $meta[]=['relation'=>'OR', ['key'=>'mg_anim','value'=>$f['anim'],'compare'=>'LIKE'], ['key'=>'mg_anim_shadow','value'=>$f['anim'],'compare'=>'LIKE'], ['key'=>'mg_anim_other','value'=>$f['anim'],'compare'=>'LIKE'], ]; } if ($f['state']==='cancelled') $meta[]=['key'=>'mg_annule','value'=>'1']; if ($f['state']==='active') $meta[]=['key'=>'mg_annule','value'=>'0']; $q=new WP_Query([ 'post_type'=>'mg_seance','post_status'=>'publish','posts_per_page'=>1000, 'meta_key'=>'mg_date','orderby'=>['meta_value'=>'ASC','ID'=>'ASC'],'meta_query'=>$meta, ]); $accessories=MGPL_Settings::accessories(); $animators=self::animator_names(); $sessions=[]; while($q->have_posts()) { $q->the_post(); $id=get_the_ID(); $date=self::normalize_date(get_post_meta($id,'mg_date',true)); if (!$date || !preg_match('/^\d{4}-\d{2}-\d{2}$/',$date)) continue; if ($f['from'] && $date<$f['from']) continue; if ($f['to'] && $date>$f['to']) continue; $sessions[]=['id'=>$id,'date'=>$date]; } wp_reset_postdata(); $total=count($sessions); $cancelled=0; $weeks=[]; foreach($sessions as $session) { if (get_post_meta($session['id'],'mg_annule',true)==='1') $cancelled++; $weeks[wp_date('o-W',strtotime($session['date']))]=true; } $active=$total-$cancelled; echo '

MG Planning

Saison 2026–2027
'; echo '
'.$total.' séances'.count($weeks).' semaines'.$active.' prévues'.$cancelled.' annulées
'; if (isset($_GET['generated'])) echo '

'.intval($_GET['generated']).' séance(s) créée(s), '.intval($_GET['skipped'] ?? 0).' déjà existante(s) ignorée(s).

'; if (isset($_GET['purged'])) echo '

'.intval($_GET['purged']).' séance(s) supprimée(s). La bibliothèque et les réglages sont conservés.

'; echo '
'; echo '
Générer une période Ajouter une séance '; if (current_user_can('manage_options')) { $purge=wp_nonce_url(admin_url('admin-post.php?action=mgpl_purge'),'mgpl_purge'); echo ' Purger toutes les séances'; } echo '
'; echo '
'; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo 'Réinitialiser
'; echo '
'; echo ''; echo ''; echo '0 séance sélectionnée'; echo '
'; echo ''; echo '

Clique sur « Modifier » pour ouvrir une séance, ou sélectionne plusieurs lignes pour une action groupée.

'; echo '
'; echo ''; foreach($animators as $animator) { echo ''; } echo ''; echo '
'; $last_week=''; $last_day=''; $week_count=[]; foreach($sessions as $session) $week_count[wp_date('o-W',strtotime($session['date']))]=($week_count[wp_date('o-W',strtotime($session['date']))] ?? 0)+1; foreach($sessions as $session) { $id=$session['id']; $date=$session['date']; $ts=strtotime($date); $week=wp_date('o-W',$ts); $cancel=get_post_meta($id,'mg_annule',true)==='1'; if ($week!==$last_week) { $last_week=$week; $last_day=''; $week_dt=new DateTimeImmutable($date,wp_timezone()); $week_start=$week_dt->modify('-'.((int)$week_dt->format('N')-1).' days')->format('Y-m-d'); $week_end=(new DateTimeImmutable($week_start,wp_timezone()))->modify('+6 days')->format('Y-m-d'); echo ''; } if ($date!==$last_day) { $last_day=$date; echo ''; } $act=get_post_meta($id,'mg_activite',true); $acc=get_post_meta($id,'mg_acc',true); $acc_other=trim((string)get_post_meta($id,'mg_acc_autre',true)); $renfo_type=trim((string)get_post_meta($id,'mg_renfo_type',true)); $note=trim((string)get_post_meta($id,'mg_note',true)); $acc_label=$acc_other ?: self::pretty($acc); if ($acc==='' || $acc==='sans') $acc_label='Aucun'; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; echo ''; $dup=wp_nonce_url(admin_url('admin-post.php?action=mgpl_duplicate&id='.$id),'mgpl_duplicate_'.$id); echo ''; echo ''; echo ''; } if (!$total) echo ''; echo '
SélectionHoraireActivitéPublicÉquipeAccessoireTypeSalleNotesÉtatActions
'.esc_html(self::week_label($date)).''.intval($week_count[$week]).' séances
'.esc_html(strtoupper(wp_date('l d/m',$ts))).'
'.esc_html(get_post_meta($id,'mg_hdeb',true).'–'.get_post_meta($id,'mg_hfin',true)).''.esc_html(self::pretty($act)).''.esc_html(str_replace('Intermédiaires / Experts','Inter / Expert',self::pretty(get_post_meta($id,'mg_public',true)))).''.self::team_html($id).''.esc_html($acc_label).''.($act==='renforcement' && $renfo_type!==''?esc_html($renfo_type):'—').''.esc_html(get_post_meta($id,'mg_salle',true) ?: 'Salle rouge').''.esc_html($note ?: '—').''.($cancel?'ANNULÉE':'Prévue').' DupliquerModification en cours
'; echo '
Modifier la séance'.esc_html(strtoupper(wp_date('l d/m/Y',$ts))).' · '.esc_html(get_post_meta($id,'mg_hdeb',true).'–'.get_post_meta($id,'mg_hfin',true)).' · '.esc_html(self::pretty($act)).'
'; echo '
'; echo '

Équipe

'; echo ''; $shadow_value=trim((string)get_post_meta($id,'mg_anim_shadow',true)); echo ''; echo '
'; echo '

Matériel

'; echo ''; echo ''; echo ''; echo '
'; echo '

Séance

'; echo ''; echo ''; echo ''; echo '
'; echo '
Aucune séance ne correspond aux filtres.
'; } public static function generator_page() { if (!current_user_can('manage_options')) return; $suggest='Gérard'; $last=get_posts(['post_type'=>'mg_seance','post_status'=>'publish','numberposts'=>1,'meta_key'=>'mg_date','orderby'=>'meta_value','order'=>'DESC','meta_query'=>[['key'=>'mg_creneau','value'=>'lun1']]]); if($last) { $a=get_post_meta($last[0]->ID,'mg_anim',true); $suggest=$a==='Gérard'?'Noëlle':'Gérard'; } echo '

Générer une période

'; echo '

Le générateur reconstruit toujours le planning à partir du masque standard des six séances et de l’alternance Gérard/Noëlle. Les remplacements ponctuels effectués dans le planning ne deviennent jamais le nouveau masque.

'; echo '

Accessoires : ils restent pilotés par leur progression propre, indépendante des animateurs. Les séquences ci-dessous sont affichées uniquement pour information.

'; $seqs=MGPL_Settings::sequences(); $acc_labels=MGPL_Settings::accessories(); echo ''; foreach(MGPL_Settings::slots() as $slot=>$cfg) { $items=array_values(array_filter(array_map('trim',explode(',',$seqs[$slot] ?? 'sans')))); if (!$items) $items=['sans']; $labels=[]; foreach($items as $item) { if ($item==='sans' || $item==='') { $labels[]='Sans accessoire'; } else { $labels[]=$acc_labels[$item] ?? self::pretty($item); } } echo ''; } echo '
CréneauProgression accessoires (information)
'.esc_html($cfg['label']).''.esc_html(implode(' → ',$labels)).'
'; echo '
'; wp_nonce_field('mgpl_generate'); echo ''; echo ''; echo ''; echo ''; echo '
Date de la première séance

Choisis normalement le lundi de début de période.

Durée semaines
Animateur du premier lundi

Ce choix sert uniquement de point de départ au masque standard Gérard/Noëlle. Les remplaçants saisis dans le planning ne modifient pas ce masque.

'; submit_button('Générer le planning'); echo '
'; } public static function settings_page() { if (!current_user_can('manage_options')) return; $seqs=MGPL_Settings::sequences(); $imgs=MGPL_Settings::images(); echo '

Accessoires et progression

Chaque créneau possède sa propre progression. Pour conserver un accessoire pendant quinze jours, répète son identifiant deux fois.

'; settings_fields('mgpl_settings'); echo ''; foreach(MGPL_Settings::slots() as $slot=>$cfg) { echo ''; } echo '
'.esc_html($cfg['label']).'

Exemple : sans,sans,petite-balle-club,petite-balle-club,ring-club,ring-club

Images par défaut

'; foreach(MGPL_Settings::accessories() as $key=>$label) echo ''; echo '
'.esc_html($label).'
'; submit_button('Enregistrer'); echo '
'; } public static function save_session() { check_ajax_referer('mgpl_inline','nonce'); $id=(int)($_POST['id'] ?? 0); if (!$id || !current_user_can('edit_post',$id)) wp_send_json_error(['message'=>'Accès refusé']); $fields=$_POST['fields'] ?? []; if (!is_array($fields)) wp_send_json_error(['message'=>'Données invalides']); $allowed=['mg_anim','mg_anim_shadow','mg_anim_other','mg_acc','mg_acc_autre','mg_photo_url','mg_salle','mg_renfo_type','mg_note']; foreach($fields as $key=>$raw) { $key=sanitize_key($key); if (!in_array($key,$allowed,true)) continue; $value=$key==='mg_photo_url' ? esc_url_raw(wp_unslash($raw)) : sanitize_textarea_field(wp_unslash($raw)); update_post_meta($id,$key,$value); } wp_send_json_success(['message'=>'Séance enregistrée']); } public static function bulk_update() { check_ajax_referer('mgpl_inline','nonce'); if (!current_user_can('edit_posts')) wp_send_json_error(['message'=>'Accès refusé']); $ids=array_values(array_unique(array_filter(array_map('intval',(array)($_POST['ids'] ?? []))))); $action=sanitize_key($_POST['bulk_action'] ?? ''); $value=sanitize_text_field(wp_unslash($_POST['value'] ?? '')); $accessory=sanitize_key($_POST['accessory'] ?? ''); $custom_accessory=sanitize_text_field(wp_unslash($_POST['custom_accessory'] ?? '')); if (!$ids) wp_send_json_error(['message'=>'Aucune séance sélectionnée']); $allowed=['shadow_set','shadow_clear','main_set','other_set','other_clear','accessory_set','room_set','renfo_set','cancel','reactivate']; if (!in_array($action,$allowed,true)) wp_send_json_error(['message'=>'Action invalide']); $updated=0; foreach($ids as $id) { if (get_post_type($id)!=='mg_seance' || !current_user_can('edit_post',$id)) continue; switch($action) { case 'shadow_set': update_post_meta($id,'mg_anim_shadow',$value); break; case 'shadow_clear': update_post_meta($id,'mg_anim_shadow',''); break; case 'main_set': update_post_meta($id,'mg_anim',$value); break; case 'other_set': update_post_meta($id,'mg_anim_other',$value); break; case 'other_clear': update_post_meta($id,'mg_anim_other',''); break; case 'accessory_set': update_post_meta($id,'mg_acc',$accessory); update_post_meta($id,'mg_acc_autre',$accessory==='autre' ? $custom_accessory : ''); break; case 'room_set': update_post_meta($id,'mg_salle',$value); break; case 'renfo_set': update_post_meta($id,'mg_renfo_type',$value); break; case 'cancel': update_post_meta($id,'mg_annule','1'); break; case 'reactivate': update_post_meta($id,'mg_annule','0'); break; } $updated++; } wp_send_json_success(['updated'=>$updated]); } public static function inline_update() { check_ajax_referer('mgpl_inline','nonce'); $id=(int)($_POST['id'] ?? 0); if (!$id || !current_user_can('edit_post',$id)) wp_send_json_error(['message'=>'Accès refusé']); $key=sanitize_key($_POST['key'] ?? ''); $allowed=['mg_annule']; if (!in_array($key,$allowed,true)) wp_send_json_error(['message'=>'Champ refusé']); $value=$key==='mg_photo_url' ? esc_url_raw(wp_unslash($_POST['value'] ?? '')) : sanitize_text_field(wp_unslash($_POST['value'] ?? '')); update_post_meta($id,$key,$value); wp_send_json_success(['value'=>$value]); } public static function duplicate() { $id=(int)($_GET['id'] ?? 0); if (!$id || !current_user_can('edit_post',$id)) wp_die('Accès refusé'); check_admin_referer('mgpl_duplicate_'.$id); $post=get_post($id); if (!$post || $post->post_type!=='mg_seance') wp_die('Séance introuvable'); $new=wp_insert_post(['post_type'=>'mg_seance','post_status'=>'draft','post_title'=>$post->post_title.' (copie)'],true); if (!is_wp_error($new)) { foreach(get_post_meta($id) as $key=>$values) foreach($values as $value) add_post_meta($new,$key,maybe_unserialize($value)); wp_safe_redirect(get_edit_post_link($new,'url')); exit; } wp_die('Duplication impossible'); } public static function purge() { if (!current_user_can('manage_options')) wp_die('Accès refusé'); check_admin_referer('mgpl_purge'); $ids=get_posts(['post_type'=>'mg_seance','post_status'=>'any','posts_per_page'=>-1,'fields'=>'ids']); $count=0; foreach($ids as $id) if (wp_delete_post($id,true)) $count++; wp_safe_redirect(add_query_arg(['page'=>'mg-planning','purged'=>$count],admin_url('admin.php'))); exit; }}