type */ public static $acf_schema = [ 'langue' => 'taxonomy', // multi_select - retourne un tableau d'IDs 'jour' => 'select', 'periode' => 'select', 'limite' => 'number', 'limite_par' => 'select', 'actif' => 'true_false', 'ajouter_exception' => 'true_false', 'exceptions_disponibilites' => 'group', ]; public function __construct($data = []) { foreach ($data as $key => $value) { if (property_exists($this, $key)) { $this->$key = $value; } } } /** * Charge une capacité de traduction par ID * @param int $id * @param array $fields Champs spécifiques à charger (optionnel) * @return self|null */ public static function load($id, $fields = []) { $post = \get_post($id); if (!$post || $post->post_type !== 'traduction_langue') { return null; } // Si des champs spécifiques sont demandés if (!empty($fields)) { $data = ['id' => $post->ID]; foreach ($fields as $field) { if (property_exists(self::class, $field)) { $data[$field] = \get_field($field, $post->ID); } } return new self($data); } // Charger tous les champs $langue_field = \get_field('langue', $post->ID); // S'assurer que langue est toujours un tableau if (!is_array($langue_field)) { $langue_field = !empty($langue_field) ? [$langue_field] : []; } return new self([ 'id' => $post->ID, 'langue' => $langue_field, // Tableau d'IDs de langues 'jour' => \get_field('jour', $post->ID), 'periode' => \get_field('periode', $post->ID), 'limite' => \get_field('limite', $post->ID), 'limite_par' => \get_field('limite_par', $post->ID) ?: 'semaine', 'actif' => \get_field('actif', $post->ID) !== false, 'ajouter_exception' => \get_field('ajouter_exception', $post->ID) ?: false, 'exceptions_disponibilites' => \get_field('exceptions_disponibilites', $post->ID) ?: null, ]); } /** * Récupère toutes les capacités actives * @param array $filters Filtres optionnels (langue, jour, periode, type_capacite) * @param bool $use_cache Utiliser le cache (par défaut: true) * @return array Liste des capacités actives */ public static function getActiveCapacites(array $filters = [], bool $use_cache = true): array { // Générer une clé de cache basée sur les filtres $cache_key = 'crvi_capacites_actives_' . md5(serialize($filters)); // Essayer de récupérer depuis le cache si activé et sans filtres complexes if ($use_cache && empty($filters)) { $cached = \get_transient($cache_key); if ($cached !== false) { return $cached; } } $args = [ 'post_type' => 'traduction_langue', 'posts_per_page' => -1, 'post_status' => 'publish', 'meta_query' => [ [ 'key' => 'actif', 'value' => '1', 'compare' => '=', ], ], ]; // Appliquer les filtres if (!empty($filters['langue'])) { $args['tax_query'] = [ [ 'taxonomy' => 'langue', 'field' => 'term_id', 'terms' => (int) $filters['langue'], ], ]; } if (!empty($filters['jour'])) { $args['meta_query'][] = [ 'key' => 'jour', 'value' => $filters['jour'], 'compare' => '=', ]; } if (!empty($filters['periode'])) { $args['meta_query'][] = [ 'key' => 'periode', 'value' => $filters['periode'], 'compare' => '=', ]; } $posts = \get_posts($args); $capacites = []; foreach ($posts as $post) { $capacites[] = self::load($post->ID); } // Mettre en cache si activé et sans filtres if ($use_cache && empty($filters)) { \set_transient($cache_key, $capacites, 1 * \HOUR_IN_SECONDS); } return $capacites; } /** * Compte le nombre de créneaux utilisés pour une capacité donnée * Gère la période (semaine/mois) et exclut les événements annulés/brouillons * * @param int $capacite_id ID de la capacité * @param string|null $date Date de référence (Y-m-d). Si null, utilise la date actuelle * @param self|null $capacite Capacité déjà chargée (évite un appel load() supplémentaire) * @return int Nombre de créneaux utilisés */ public static function countUsed(int $capacite_id, ?string $date = null, ?self $capacite = null): int { // Charger la capacité si non fournie if (!$capacite) { $capacite = self::load($capacite_id); if (!$capacite) { return 0; } } // Date de référence if (!$date) { $date = \current_time('Y-m-d'); } // Générer une clé de cache unique $cache_key = 'crvi_count_used_' . $capacite_id . '_' . $date . '_' . md5(serialize([ $capacite->langue, $capacite->jour, $capacite->periode, $capacite->limite_par, ])); // Essayer de récupérer depuis le cache (cache de 15 minutes) $cached = \get_transient($cache_key); if ($cached !== false) { return (int) $cached; } // Déterminer la plage de dates selon limite_par $date_debut = $date; $date_fin = $date; $date_obj = new \DateTime($date); if ($capacite->limite_par === 'semaine') { // Calculer le début et la fin de la semaine $start_of_week = (int) \get_option('start_of_week', 1); // 0=dimanche, 1=lundi, etc. // Obtenir le jour de la semaine (0=dimanche, 1=lundi, ..., 6=samedi) $day_of_week = (int) $date_obj->format('w'); // Calculer le nombre de jours à soustraire pour arriver au début de la semaine $days_to_subtract = ($day_of_week - $start_of_week + 7) % 7; $date_obj->modify("-{$days_to_subtract} days"); $date_debut = $date_obj->format('Y-m-d'); $date_obj->modify('+6 days'); $date_fin = $date_obj->format('Y-m-d'); } elseif ($capacite->limite_par === 'mois') { // Premier et dernier jour du mois $date_debut = $date_obj->format('Y-m-01'); $date_fin = $date_obj->format('Y-m-t'); } // Récupérer les langues (peut être un tableau) $langue_ids = $capacite->langue; if (empty($langue_ids)) { return 0; } // S'assurer que c'est un tableau if (!is_array($langue_ids)) { $langue_ids = [$langue_ids]; } // Récupérer les slugs de toutes les langues (avec cache) $langue_slugs = []; foreach ($langue_ids as $langue_id) { $slug = self::get_langue_slug($langue_id); if ($slug) { $langue_slugs[] = $slug; } } if (empty($langue_slugs)) { return 0; } // Convertir le jour en format de requête $jour_nom = $capacite->jour; // lundi, mardi, etc. // Requête pour compter les événements global $wpdb; $table_name = $wpdb->prefix . 'crvi_agenda'; // Construire la requête SQL // Pour les langues multiples, utiliser IN au lieu de = $langue_placeholders = implode(',', array_fill(0, count($langue_slugs), '%s')); $where = [ "date_rdv BETWEEN %s AND %s", "langue IN ($langue_placeholders)", "is_deleted = 0", "statut != 'annule'", "statut != 'brouillon'", ]; $values = array_merge([$date_debut, $date_fin], $langue_slugs); // Filtrer par jour de la semaine // DAYOFWEEK() en MySQL retourne: 1=dimanche, 2=lundi, 3=mardi, 4=mercredi, 5=jeudi, 6=vendredi, 7=samedi $jours_map = [ 'dimanche' => 1, 'lundi' => 2, 'mardi' => 3, 'mercredi' => 4, 'jeudi' => 5, 'vendredi' => 6, 'samedi' => 7, ]; if (isset($jours_map[$jour_nom])) { $where[] = "DAYOFWEEK(date_rdv) = %d"; $values[] = $jours_map[$jour_nom]; } // Filtrer par période (matin, après-midi, journée) // Matin: heure_rdv < 12:00 // Après-midi: heure_rdv >= 12:00 // Journée: pas de filtre sur l'heure (tous les événements du jour) if ($capacite->periode === 'matin') { $where[] = "heure_rdv < '12:00:00'"; } elseif ($capacite->periode === 'apres_midi') { $where[] = "heure_rdv >= '12:00:00'"; } // Si période = 'journee', on ne filtre pas par heure $where_sql = 'WHERE ' . implode(' AND ', $where); $sql = "SELECT COUNT(*) FROM {$table_name} {$where_sql}"; $prepared_sql = $wpdb->prepare($sql, $values); $count = (int) $wpdb->get_var($prepared_sql); // Mettre en cache le résultat (15 minutes) \set_transient($cache_key, $count, 15 * \MINUTE_IN_SECONDS); return $count; } /** * Récupère le slug d'une langue avec cache * @param int $langue_term_id Term ID de la langue * @return string|null Slug de la langue ou null si erreur */ private static function get_langue_slug(int $langue_term_id): ?string { $cache_key = 'crvi_langue_slug_' . $langue_term_id; $cached = \get_transient($cache_key); if ($cached !== false) { return $cached; } $langue_term = \get_term($langue_term_id, 'langue'); if (!$langue_term || \is_wp_error($langue_term)) { return null; } $slug = $langue_term->slug; // Mettre en cache (1 heure) \set_transient($cache_key, $slug, 1 * \HOUR_IN_SECONDS); return $slug; } /** * Vérifie si une capacité a des événements associés * Optimisé avec EXISTS au lieu de COUNT pour meilleure performance * Gère les langues multiples (limite partagée) * @param int $capacite_id ID de la capacité * @return bool True si la capacité a des événements */ public static function hasEvents(int $capacite_id): bool { $capacite = self::load($capacite_id); if (!$capacite) { return false; } // Récupérer les langues (peut être un tableau) $langue_ids = $capacite->langue; if (empty($langue_ids)) { return false; } // S'assurer que c'est un tableau if (!is_array($langue_ids)) { $langue_ids = [$langue_ids]; } // Récupérer les slugs de toutes les langues $langue_slugs = []; foreach ($langue_ids as $langue_id) { $slug = self::get_langue_slug($langue_id); if ($slug) { $langue_slugs[] = $slug; } } if (empty($langue_slugs)) { return false; } // Requête optimisée avec EXISTS (plus rapide que COUNT) global $wpdb; $table_name = $wpdb->prefix . 'crvi_agenda'; // Construire les placeholders pour IN $langue_placeholders = implode(',', array_fill(0, count($langue_slugs), '%s')); $sql = $wpdb->prepare( "SELECT EXISTS( SELECT 1 FROM {$table_name} WHERE langue IN ($langue_placeholders) AND is_deleted = 0 AND statut != 'annule' AND statut != 'brouillon' LIMIT 1 )", $langue_slugs ); $exists = (int) $wpdb->get_var($sql); return $exists === 1; } /** * Vérifie si une capacité est disponible (limite non atteinte) * @param int $capacite_id ID de la capacité * @param string|null $date Date de référence (Y-m-d). Si null, utilise la date actuelle * @param self|null $capacite Capacité déjà chargée (évite un appel load() supplémentaire) * @return bool True si la capacité est disponible */ public static function isAvailable(int $capacite_id, ?string $date = null, ?self $capacite = null): bool { // Charger la capacité si non fournie if (!$capacite) { $capacite = self::load($capacite_id); if (!$capacite) { return false; } } // Si la capacité est inactive, elle n'est pas disponible if (!$capacite->actif) { return false; } // Si pas de limite définie, la capacité est toujours disponible if (empty($capacite->limite) || $capacite->limite <= 0) { return true; } // Compter les créneaux utilisés (en passant la capacité pour éviter un load() supplémentaire) $used = self::countUsed($capacite_id, $date, $capacite); // Vérifier si la limite est atteinte return $used < $capacite->limite; } /** * Calcule la limite effective en prenant en compte les exceptions * Exemple : 1 fois/semaine matin (= ~4/mois) + exception 1/mois après-midi = 4 matins + 1 après-midi * * @param self $capacite Capacité * @param string|null $date Date de référence (Y-m-d). Si null, utilise la date actuelle * @return array ['principale' => ['limite' => int, 'periode' => string], 'exception' => ['limite' => int, 'periode' => string] | null] */ public static function getEffectiveLimit(self $capacite, ?string $date = null): array { if (!$date) { $date = \current_time('Y-m-d'); } $result = [ 'principale' => [ 'limite' => (int) $capacite->limite, 'periode' => $capacite->periode, 'limite_par' => $capacite->limite_par, ], 'exception' => null, ]; // Si pas d'exception, retourner seulement la limite principale if (!$capacite->ajouter_exception || empty($capacite->exceptions_disponibilites)) { return $result; } $exceptions = $capacite->exceptions_disponibilites; // Vérifier que l'exception a les champs nécessaires if (empty($exceptions['frequence']) || empty($exceptions['frequence_periode']) || empty($exceptions['periode'])) { return $result; } // Calculer la limite de l'exception $exception_limite = (int) $exceptions['frequence']; $exception_periode = $exceptions['periode']; $exception_frequence_periode = $exceptions['frequence_periode']; // 'semaine' ou 'mois' // Si l'exception est par mois, utiliser directement la fréquence if ($exception_frequence_periode === 'mois') { $result['exception'] = [ 'limite' => $exception_limite, 'periode' => $exception_periode, 'limite_par' => 'mois', ]; } else { // Si l'exception est par semaine, convertir en nombre par mois $date_obj = new \DateTime($date); $nb_semaines = self::getNbSemainesDansMois($date_obj); $result['exception'] = [ 'limite' => $exception_limite * $nb_semaines, 'periode' => $exception_periode, 'limite_par' => 'mois', ]; } return $result; } /** * Calcule le nombre de semaines dans un mois donné * * @param \DateTime $date Date de référence * @return int Nombre de semaines */ private static function getNbSemainesDansMois(\DateTime $date): int { $premier_jour = new \DateTime($date->format('Y-m-01')); $dernier_jour = new \DateTime($date->format('Y-m-t')); $start_of_week = (int) \get_option('start_of_week', 1); // 0=dimanche, 1=lundi // Calculer le nombre de semaines $diff = $premier_jour->diff($dernier_jour); $nb_jours = $diff->days + 1; // Arrondir au nombre de semaines (4 ou 5 selon le mois) return (int) ceil($nb_jours / 7); } /** * Compte les événements utilisés pour la période principale et l'exception * * @param int $capacite_id ID de la capacité * @param string|null $date Date de référence (Y-m-d). Si null, utilise la date actuelle * @param self|null $capacite Capacité déjà chargée * @return array ['principale' => int, 'exception' => int, 'total' => int] */ public static function countUsedWithExceptions(int $capacite_id, ?string $date = null, ?self $capacite = null): array { // Charger la capacité si non fournie if (!$capacite) { $capacite = self::load($capacite_id); if (!$capacite) { return ['principale' => 0, 'exception' => 0, 'total' => 0]; } } // Compter les événements pour la période principale $count_principale = self::countUsed($capacite_id, $date, $capacite); $result = [ 'principale' => $count_principale, 'exception' => 0, 'total' => $count_principale, ]; // Si pas d'exception, retourner if (!$capacite->ajouter_exception || empty($capacite->exceptions_disponibilites)) { return $result; } $exceptions = $capacite->exceptions_disponibilites; // Vérifier que l'exception a les champs nécessaires if (empty($exceptions['periode'])) { return $result; } // Compter les événements pour l'exception (période différente) $exception_periode = $exceptions['periode']; // Si la période de l'exception est différente de la période principale if ($exception_periode !== $capacite->periode) { // Créer une capacité temporaire avec la période de l'exception $temp_capacite = clone $capacite; $temp_capacite->periode = $exception_periode; $count_exception = self::countUsed($capacite_id, $date, $temp_capacite); $result['exception'] = $count_exception; $result['total'] = $count_principale + $count_exception; } return $result; } /** * Récupère les disponibilités pour une langue donnée en agrégeant toutes les capacités * Exemple : Si 2 capacités existent pour l'arabe (1/mois matin + 1/semaine après-midi) * Le résultat sera : 1 matin/mois + ~4 après-midi/mois = 5 créneaux/mois au total * * @param int $langue_term_id ID du terme de la taxonomie langue * @param string|null $date Date de référence (Y-m-d). Si null, utilise la date actuelle * @param bool $use_cache Utiliser le cache transient (par défaut: true) * @return array ['total' => int, 'by_periode' => ['matin' => [...], 'apres_midi' => [...], 'journee' => [...]]] */ public static function getDisponibilitesByLangue(int $langue_term_id, ?string $date = null, bool $use_cache = true): array { if (!$date) { $date = \current_time('Y-m-d'); } // Générer une clé de cache unique basée sur la langue et la date $cache_key = 'crvi_dispos_langue_' . $langue_term_id . '_' . $date; // Essayer de récupérer depuis le cache if ($use_cache) { $cached = \get_transient($cache_key); if ($cached !== false) { return $cached; } } // Initialiser le résultat $result = [ 'total' => 0, 'total_used' => 0, 'total_available' => 0, 'by_periode' => [ 'matin' => [], 'apres_midi' => [], 'journee' => [], ], ]; // Récupérer toutes les capacités actives pour cette langue $capacites = self::getActiveCapacites(['langue' => $langue_term_id], false); if (empty($capacites)) { return $result; } $date_obj = new \DateTime($date); $nb_semaines = self::getNbSemainesDansMois($date_obj); // Parcourir chaque capacité et calculer sa contribution foreach ($capacites as $capacite) { // Vérifier que la langue correspond (peut être un tableau) $capacite_langues = is_array($capacite->langue) ? $capacite->langue : [$capacite->langue]; if (!in_array($langue_term_id, $capacite_langues)) { continue; } // Calculer la limite mensuelle pour la période principale $limite_mensuelle_principale = self::convertToMonthlyLimit( $capacite->limite, $capacite->limite_par, $nb_semaines ); // Compter les événements utilisés pour la période principale $used_principale = self::countUsed($capacite->id, $date, $capacite); // Ajouter la capacité principale $periode_principale = $capacite->periode; $result['by_periode'][$periode_principale][] = [ 'capacite_id' => $capacite->id, 'capacite_title' => \get_the_title($capacite->id), 'jour' => $capacite->jour, 'limite' => $limite_mensuelle_principale, 'used' => $used_principale, 'available' => max(0, $limite_mensuelle_principale - $used_principale), 'type' => 'principale', ]; $result['total'] += $limite_mensuelle_principale; $result['total_used'] += $used_principale; // Si la capacité a une exception if ($capacite->ajouter_exception && !empty($capacite->exceptions_disponibilites)) { $exceptions = $capacite->exceptions_disponibilites; if (!empty($exceptions['frequence']) && !empty($exceptions['frequence_periode']) && !empty($exceptions['periode'])) { // Calculer la limite mensuelle pour l'exception $limite_mensuelle_exception = self::convertToMonthlyLimit( (int) $exceptions['frequence'], $exceptions['frequence_periode'], $nb_semaines ); // Compter les événements utilisés pour l'exception $temp_capacite = clone $capacite; $temp_capacite->periode = $exceptions['periode']; $used_exception = self::countUsed($capacite->id, $date, $temp_capacite); // Ajouter l'exception $periode_exception = $exceptions['periode']; $result['by_periode'][$periode_exception][] = [ 'capacite_id' => $capacite->id, 'capacite_title' => \get_the_title($capacite->id) . ' (Exception)', 'jour' => $capacite->jour, 'limite' => $limite_mensuelle_exception, 'used' => $used_exception, 'available' => max(0, $limite_mensuelle_exception - $used_exception), 'type' => 'exception', ]; $result['total'] += $limite_mensuelle_exception; $result['total_used'] += $used_exception; } } } // Calculer le total disponible $result['total_available'] = max(0, $result['total'] - $result['total_used']); // Calculer les totaux par période foreach ($result['by_periode'] as $periode => &$items) { $periode_total = 0; $periode_used = 0; $periode_available = 0; foreach ($items as $item) { $periode_total += $item['limite']; $periode_used += $item['used']; $periode_available += $item['available']; } // Ajouter un résumé pour cette période $result['by_periode'][$periode . '_summary'] = [ 'total' => $periode_total, 'used' => $periode_used, 'available' => $periode_available, ]; } // Mettre en cache le résultat (2 jours) if ($use_cache) { \set_transient($cache_key, $result, 2 * \DAY_IN_SECONDS); } return $result; } /** * Convertit une limite (semaine ou mois) en limite mensuelle * * @param int $limite Limite originale * @param string $limite_par 'semaine' ou 'mois' * @param int $nb_semaines Nombre de semaines dans le mois * @return int Limite mensuelle */ private static function convertToMonthlyLimit(int $limite, string $limite_par, int $nb_semaines): int { if ($limite_par === 'semaine') { // Convertir hebdomadaire en mensuel return $limite * $nb_semaines; } // Déjà mensuel return $limite; } /** * Récupère un résumé formaté des disponibilités par langue * Version simplifiée pour affichage rapide * * @param int $langue_term_id ID du terme de la taxonomie langue * @param string|null $date Date de référence (Y-m-d). Si null, utilise la date actuelle * @param bool $use_cache Utiliser le cache transient (par défaut: true) * @return array ['matin' => 'X disponibles / Y total', 'apres_midi' => '...', 'journee' => '...', 'total' => '...'] */ public static function getDisponibilitesSummaryByLangue(int $langue_term_id, ?string $date = null, bool $use_cache = true): array { $dispos = self::getDisponibilitesByLangue($langue_term_id, $date, $use_cache); $summary = []; foreach (['matin', 'apres_midi', 'journee'] as $periode) { if (isset($dispos['by_periode'][$periode . '_summary'])) { $data = $dispos['by_periode'][$periode . '_summary']; if ($data['total'] > 0) { $summary[$periode] = sprintf( '%d disponibles / %d total (%d utilisés)', $data['available'], $data['total'], $data['used'] ); } } } $summary['total'] = sprintf( '%d disponibles / %d total (%d utilisés)', $dispos['total_available'], $dispos['total'], $dispos['total_used'] ); return $summary; } /** * Récupère toutes les langues avec leurs disponibilités * Utile pour afficher un tableau récapitulatif * * @param string|null $date Date de référence (Y-m-d). Si null, utilise la date actuelle * @param bool $use_cache Utiliser le cache transient (par défaut: true) * @return array ['langue_slug' => ['name' => '...', 'total' => X, 'available' => Y, ...]] */ public static function getAllLanguesDisponibilites(?string $date = null, bool $use_cache = true): array { if (!$date) { $date = \current_time('Y-m-d'); } // Générer une clé de cache unique $cache_key = 'crvi_all_langues_dispos_' . $date; // Essayer de récupérer depuis le cache if ($use_cache) { $cached = \get_transient($cache_key); if ($cached !== false) { return $cached; } } // Récupérer toutes les langues utilisées dans les capacités actives $capacites = self::getActiveCapacites([], false); $langues_ids = []; foreach ($capacites as $capacite) { $capacite_langues = is_array($capacite->langue) ? $capacite->langue : [$capacite->langue]; $langues_ids = array_merge($langues_ids, $capacite_langues); } $langues_ids = array_unique($langues_ids); $result = []; foreach ($langues_ids as $langue_id) { $langue_term = \get_term($langue_id, 'langue'); if ($langue_term && !\is_wp_error($langue_term)) { // Utiliser le cache pour chaque langue $dispos = self::getDisponibilitesByLangue($langue_id, $date, $use_cache); $result[$langue_term->slug] = [ 'id' => $langue_id, 'name' => $langue_term->name, 'slug' => $langue_term->slug, 'total' => $dispos['total'], 'used' => $dispos['total_used'], 'available' => $dispos['total_available'], 'by_periode' => [ 'matin' => $dispos['by_periode']['matin_summary'] ?? ['total' => 0, 'used' => 0, 'available' => 0], 'apres_midi' => $dispos['by_periode']['apres_midi_summary'] ?? ['total' => 0, 'used' => 0, 'available' => 0], 'journee' => $dispos['by_periode']['journee_summary'] ?? ['total' => 0, 'used' => 0, 'available' => 0], ], 'details' => $dispos['by_periode'], ]; } } // Trier par nom de langue uasort($result, function($a, $b) { return strcmp($a['name'], $b['name']); }); // Mettre en cache le résultat global (2 jours) if ($use_cache) { \set_transient($cache_key, $result, 2 * \DAY_IN_SECONDS); } return $result; } /** * Récupère les disponibilités par langue avec ventilation par jour * Structure : langue -> total, remaining, by_periode (am/pm avec total/remaining), by_jour (lundi->dimanche avec total/remaining) * * @param int $langue_term_id ID du terme de la taxonomie langue * @param string|null $date Date de référence (Y-m-d). Si null, utilise la date actuelle * @param bool $use_cache Utiliser le cache transient (par défaut: true) * @return array ['total' => int, 'remaining' => int, 'by_periode' => [...], 'by_jour' => [...]] */ public static function getDisponibilitesByLangueWithJours(int $langue_term_id, ?string $date = null, bool $use_cache = true): array { if (!$date) { $date = \current_time('Y-m-d'); } // Générer une clé de cache unique $cache_key = 'crvi_dispos_langue_jours_' . $langue_term_id . '_' . $date; // Essayer de récupérer depuis le cache if ($use_cache) { $cached = \get_transient($cache_key); if ($cached !== false) { return $cached; } } // Obtenir les disponibilités globales par langue $dispos = self::getDisponibilitesByLangue($langue_term_id, $date, $use_cache); // Initialiser la structure de résultat $result = [ 'total' => $dispos['total'], 'remaining' => $dispos['total_available'], 'by_periode' => [ 'matin' => [ 'total' => $dispos['by_periode']['matin_summary']['total'] ?? 0, 'remaining' => $dispos['by_periode']['matin_summary']['available'] ?? 0, ], 'apres_midi' => [ 'total' => $dispos['by_periode']['apres_midi_summary']['total'] ?? 0, 'remaining' => $dispos['by_periode']['apres_midi_summary']['available'] ?? 0, ], ], 'by_jour' => [ 'lundi' => ['total' => 0, 'remaining' => 0], 'mardi' => ['total' => 0, 'remaining' => 0], 'mercredi' => ['total' => 0, 'remaining' => 0], 'jeudi' => ['total' => 0, 'remaining' => 0], 'vendredi' => ['total' => 0, 'remaining' => 0], 'samedi' => ['total' => 0, 'remaining' => 0], 'dimanche' => ['total' => 0, 'remaining' => 0], ], ]; // Récupérer toutes les capacités actives pour cette langue $capacites = self::getActiveCapacites(['langue' => $langue_term_id], false); if (empty($capacites)) { return $result; } $date_obj = new \DateTime($date); $nb_semaines = self::getNbSemainesDansMois($date_obj); // Parcourir chaque capacité et ventiler par jour foreach ($capacites as $capacite) { // Vérifier que la langue correspond $capacite_langues = is_array($capacite->langue) ? $capacite->langue : [$capacite->langue]; if (!in_array($langue_term_id, $capacite_langues)) { continue; } // Si un jour est spécifié, ajouter la capacité à ce jour if (!empty($capacite->jour) && isset($result['by_jour'][$capacite->jour])) { // Calculer la limite mensuelle $limite_mensuelle = self::convertToMonthlyLimit( $capacite->limite, $capacite->limite_par, $nb_semaines ); // Compter les événements utilisés $used = self::countUsed($capacite->id, $date, $capacite); // Ajouter au total du jour $result['by_jour'][$capacite->jour]['total'] += $limite_mensuelle; $result['by_jour'][$capacite->jour]['remaining'] += max(0, $limite_mensuelle - $used); // Si la capacité a une exception avec un jour différent, l'ajouter aussi if ($capacite->ajouter_exception && !empty($capacite->exceptions_disponibilites)) { $exceptions = $capacite->exceptions_disponibilites; if (!empty($exceptions['frequence']) && !empty($exceptions['frequence_periode'])) { $limite_exception = self::convertToMonthlyLimit( (int) $exceptions['frequence'], $exceptions['frequence_periode'], $nb_semaines ); // Compter les événements pour l'exception $temp_capacite = clone $capacite; if (!empty($exceptions['periode'])) { $temp_capacite->periode = $exceptions['periode']; } $used_exception = self::countUsed($capacite->id, $date, $temp_capacite); // Ajouter au total du jour $result['by_jour'][$capacite->jour]['total'] += $limite_exception; $result['by_jour'][$capacite->jour]['remaining'] += max(0, $limite_exception - $used_exception); } } } } // Mettre en cache le résultat (2 jours) if ($use_cache) { \set_transient($cache_key, $result, 2 * \DAY_IN_SECONDS); } return $result; } /** * Récupère toutes les langues avec leurs capacités détaillées (périodes et jours) * Format optimisé pour crviACFData JavaScript * * @param string|null $date_debut Date de début (Y-m-d). Si null, utilise le début du mois courant * @param string|null $date_fin Date de fin (Y-m-d). Si null, utilise la fin du mois courant * @param bool $use_cache Utiliser le cache transient (par défaut: true) * @return array ['langue_slug' => ['id' => X, 'name' => '...', 'total' => X, 'remaining' => Y, 'by_periode' => [...], 'by_jour' => [...]]] */ public static function getAllLanguesCapacitesForACF(?string $date_debut = null, ?string $date_fin = null, bool $use_cache = true): array { // Si aucune date n'est fournie, utiliser le mois courant if (!$date_debut || !$date_fin) { $date_actuelle = \current_time('Y-m-d'); $date_obj = new \DateTime($date_actuelle); if (!$date_debut) { $date_debut = $date_obj->format('Y-m-01'); // Premier jour du mois } if (!$date_fin) { $date_fin = $date_obj->format('Y-m-t'); // Dernier jour du mois } } // Générer une clé de cache unique basée sur la plage de dates $cache_key = 'crvi_langues_capacites_acf_' . $date_debut . '_' . $date_fin; // Essayer de récupérer depuis le cache if ($use_cache) { $cached = \get_transient($cache_key); if ($cached !== false) { return $cached; } } // Récupérer uniquement les termes de la taxonomie 'langue' qui sont liés à des posts 'traduction_langue' actifs $langues_terms = \get_terms([ 'taxonomy' => 'langue', 'hide_empty' => true, // Important: ne récupérer que les termes utilisés 'object_ids' => \get_posts([ 'post_type' => 'traduction_langue', 'post_status' => 'publish', 'numberposts' => -1, 'fields' => 'ids' ]) ]); if (\is_wp_error($langues_terms) || empty($langues_terms)) { // Mettre en cache un résultat vide if ($use_cache) { \set_transient($cache_key, [], 1 * \DAY_IN_SECONDS); } return []; } $result = []; foreach ($langues_terms as $langue_term) { // Vérifier qu'il existe au moins une capacité active pour cette langue $capacites = self::getActiveCapacites(['langue' => $langue_term->term_id], false); if (empty($capacites)) { // Pas de capacité active pour cette langue, on saute continue; } // Obtenir les disponibilités avec ventilation par jour // Utiliser la date de début pour le calcul (les méthodes sous-jacentes calculent déjà le mois complet) $dispos = self::getDisponibilitesByLangueWithJours($langue_term->term_id, $date_debut, $use_cache); $result[$langue_term->slug] = [ 'id' => $langue_term->term_id, 'name' => $langue_term->name, 'slug' => $langue_term->slug, 'total' => $dispos['total'], 'remaining' => $dispos['remaining'], 'by_periode' => $dispos['by_periode'], 'by_jour' => $dispos['by_jour'], ]; } // Trier par nom de langue uasort($result, function($a, $b) { return strcmp($a['name'], $b['name']); }); // Mettre en cache le résultat (1 jour) if ($use_cache) { \set_transient($cache_key, $result, 1 * \DAY_IN_SECONDS); } return $result; } /** * Invalide le cache pour une capacité donnée * À appeler lors de la création/modification/suppression d'événements ou de capacités * @param int|null $capacite_id ID de la capacité (optionnel, invalide tout si null) */ public static function invalidate_cache(?int $capacite_id = null): void { global $wpdb; if ($capacite_id) { // Invalider le cache de la capacité spécifique $capacite = self::load($capacite_id); if ($capacite) { // Invalider le cache pour toutes les langues de cette capacité $langue_ids = is_array($capacite->langue) ? $capacite->langue : [$capacite->langue]; foreach ($langue_ids as $langue_id) { // Récupérer toutes les clés de transient pour cette langue // Format: crvi_dispos_langue_{langue_id}_{date} $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s OR option_name LIKE %s OR option_name LIKE %s", '_transient_crvi_dispos_langue_' . $langue_id . '_%', '_transient_timeout_crvi_dispos_langue_' . $langue_id . '_%', '_transient_crvi_dispos_langue_jours_' . $langue_id . '_%', '_transient_timeout_crvi_dispos_langue_jours_' . $langue_id . '_%' ) ); } // Invalider aussi le cache global de toutes les langues $wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_crvi_all_langues_dispos_%' OR option_name LIKE '_transient_timeout_crvi_all_langues_dispos_%' OR option_name LIKE '_transient_crvi_langues_capacites_acf_%' OR option_name LIKE '_transient_timeout_crvi_langues_capacites_acf_%'" ); } // Invalider le cache général des capacités \delete_transient('crvi_capacites_actives_' . md5(serialize([]))); } else { // Invalider tous les caches de disponibilités $wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_crvi_dispos_langue_%' OR option_name LIKE '_transient_timeout_crvi_dispos_langue_%' OR option_name LIKE '_transient_crvi_all_langues_dispos_%' OR option_name LIKE '_transient_timeout_crvi_all_langues_dispos_%' OR option_name LIKE '_transient_crvi_count_used_%' OR option_name LIKE '_transient_timeout_crvi_count_used_%' OR option_name LIKE '_transient_crvi_langues_capacites_acf_%' OR option_name LIKE '_transient_timeout_crvi_langues_capacites_acf_%'" ); // Invalider le cache général des capacités \delete_transient('crvi_capacites_actives_' . md5(serialize([]))); } } }