$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 '
'.intval($_GET['generated']).' séance(s) créée(s), '.intval($_GET['skipped'] ?? 0).' déjà existante(s) ignorée(s).
'.intval($_GET['purged']).' séance(s) supprimée(s). La bibliothèque et les réglages sont conservés.
Choisis les séances dans la grille, puis applique une modification en une seule fois.
'; echo ''; echo '
Clique sur « Modifier » pour ouvrir une séance, ou sélectionne plusieurs lignes pour une action groupée.
'; echo '| Sélection | Horaire | Activité | Public | Équipe | Accessoire | Type | Salle | Notes | État | Actions |
|---|---|---|---|---|---|---|---|---|---|---|
| '.esc_html(self::week_label($date)).''.intval($week_count[$week]).' séances | ||||||||||
| '.esc_html(strtoupper(wp_date('l d/m',$ts))).' | ||||||||||
| '; echo ' | '.esc_html(get_post_meta($id,'mg_hdeb',true).'–'.get_post_meta($id,'mg_hfin',true)).' | '; echo ''.esc_html(self::pretty($act)).' | '; echo ''.esc_html(str_replace('Intermédiaires / Experts','Inter / Expert',self::pretty(get_post_meta($id,'mg_public',true)))).' | '; echo ''.self::team_html($id).' | '; echo ''.esc_html($acc_label).' | '; echo ''.($act==='renforcement' && $renfo_type!==''?esc_html($renfo_type):'—').' | '; echo ''.esc_html(get_post_meta($id,'mg_salle',true) ?: 'Salle rouge').' | '; echo ''.esc_html($note ?: '—').' | '; echo ''.($cancel?'ANNULÉE':'Prévue').' | '; $dup=wp_nonce_url(admin_url('admin-post.php?action=mgpl_duplicate&id='.$id),'mgpl_duplicate_'.$id); echo 'DupliquerModification en cours | '; echo '
| '; 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 ' '; echo 'Équipe'; echo ''; $shadow_value=trim((string)get_post_meta($id,'mg_anim_shadow',true)); echo ''; echo ' Matériel'; echo ''; echo ''; echo ''; echo ' Séance'; echo ''; echo ''; echo ''; echo ' | ||||||||||
| Aucune séance ne correspond aux filtres. | ||||||||||
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 '| Créneau | Progression accessoires (information) |
|---|---|
| '.esc_html($cfg['label']).' | '.esc_html(implode(' → ',$labels)).' |
Chaque créneau possède sa propre progression. Pour conserver un accessoire pendant quinze jours, répète son identifiant deux fois.