Crvi/app/models/TraductionLangue_Model.php
theShlavuk 036e6cf9ee corr
2026-01-21 23:07:44 +01:00

1247 lines
49 KiB
PHP

<?php
declare(strict_types=1);
namespace ESI_CRVI_AGENDA\models;
use ESI_CRVI_AGENDA\models\CRVI_Event_Model;
/**
* Modèle pour les capacités de traduction
* Gère les règles de capacité par langue, jour et période
*/
class CRVI_TraductionLangue_Model extends Main_Model {
public $id;
public $langue; // Peut être un tableau d'IDs de langues (multi-select)
public $jour;
public $periode;
public $limite;
public $limite_par;
public $actif;
public $ajouter_exception;
public $exceptions_disponibilites;
/**
* Schéma des champs ACF : nom => 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['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 = [];
// Filtrer par langue si demandé (le champ langue est un ACF taxonomy multi-select stocké en meta)
if (!empty($filters['langue'])) {
$langue_id_filter = (int) $filters['langue'];
foreach ($posts as $post) {
$capacite = self::load($post->ID);
if ($capacite && !empty($capacite->langue)) {
// Le champ langue est un tableau d'IDs
$langue_ids = is_array($capacite->langue) ? $capacite->langue : [$capacite->langue];
if (in_array($langue_id_filter, $langue_ids)) {
$capacites[] = $capacite;
}
}
}
} else {
// Pas de filtre par langue, charger toutes les capacités
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(
(int) $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(
(int) $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) {
// Si le cache contient des données, les retourner
if (!empty($cached)) {
error_log('🔍 Cache utilisé pour ' . $cache_key . ' - Résultat: ' . count($cached) . ' langues');
return $cached;
}
// Si le cache est vide, vérifier s'il y a des capacités actives avant de retourner le cache vide
// Cela évite de retourner un cache vide obsolète
$has_active_capacites = \get_posts([
'post_type' => 'traduction_langue',
'post_status' => 'publish',
'posts_per_page' => -1, // On vérifie toutes les capacités actives
'meta_query' => [
[
'key' => 'actif',
'value' => '1',
'compare' => '=',
],
],
]);
error_log('🔍 Capacités actives trouvées: ' . count($has_active_capacites));
if (!empty($has_active_capacites)) {
error_log('⚠️ Cache vide détecté mais des capacités actives existent - invalidation du cache et recalcul');
\delete_transient($cache_key);
// Continuer pour recalculer
} else {
error_log('🔍 Cache utilisé pour ' . $cache_key . ' - Résultat: vide (aucune capacité active)');
return [];
}
}
}
error_log('🔍 Calcul des capacités pour ' . $date_debut . ' à ' . $date_fin);
// Récupérer tous les posts 'traduction_langue' publiés et actifs
$capacites_posts = \get_posts([
'post_type' => 'traduction_langue',
'post_status' => 'publish',
'posts_per_page' => -1,
'meta_query' => [
[
'key' => 'actif',
'value' => '1',
'compare' => '=',
],
],
]);
error_log('🔍 Posts de capacités récupérés: ' . count($capacites_posts));
if (empty($capacites_posts)) {
error_log('⚠️ Aucune capacité active trouvée - résultat vide');
// Ne pas mettre en cache un résultat vide trop longtemps (1 heure au lieu de 1 jour)
if ($use_cache) {
\set_transient($cache_key, [], 1 * \HOUR_IN_SECONDS);
}
return [];
}
// Construire une liste unique de langues à partir des capacités actives
$langues_ids = [];
$capacites_sans_langue = 0;
foreach ($capacites_posts as $post) {
$langue_field = \get_field('langue', $post->ID);
// Le champ langue est un tableau d'IDs (multi-select)
if (is_array($langue_field)) {
if (empty($langue_field)) {
$capacites_sans_langue++;
}
foreach ($langue_field as $langue_id) {
if (!empty($langue_id) && !in_array($langue_id, $langues_ids)) {
$langues_ids[] = (int) $langue_id;
}
}
} elseif (!empty($langue_field)) {
// Cas où c'est un seul ID (ne devrait pas arriver avec multi-select, mais on gère)
$langue_id = (int) $langue_field;
if (!in_array($langue_id, $langues_ids)) {
$langues_ids[] = $langue_id;
}
} else {
$capacites_sans_langue++;
}
}
if ($capacites_sans_langue > 0) {
error_log('⚠️ ' . $capacites_sans_langue . ' capacité(s) active(s) sans langue assignée');
}
error_log('🔍 IDs de langues uniques trouvées: ' . count($langues_ids) . ' - ' . print_r($langues_ids, true));
if (empty($langues_ids)) {
// Mettre en cache un résultat vide
if ($use_cache) {
\set_transient($cache_key, [], 1 * \DAY_IN_SECONDS);
}
return [];
}
// Récupérer les objets termes pour ces langues
$langues_terms = [];
$langues_manquantes = [];
foreach ($langues_ids as $langue_id) {
$term = \get_term($langue_id, 'langue');
if ($term && !\is_wp_error($term)) {
$langues_terms[] = $term;
} else {
$langues_manquantes[] = $langue_id;
error_log('⚠️ Langue ID ' . $langue_id . ' référencée dans une capacité mais non trouvée dans la taxonomie');
}
}
error_log('🔍 Langues récupérées: ' . count($langues_terms) . ' sur ' . count($langues_ids));
if (!empty($langues_manquantes)) {
error_log('⚠️ Langues manquantes (IDs): ' . implode(', ', $langues_manquantes));
}
error_log('🔍 Date de début: ' . $date_debut);
error_log('🔍 Date de fin: ' . $date_fin);
if (empty($langues_terms)) {
error_log('⚠️ Aucune langue valide trouvée - résultat vide');
// Ne pas mettre en cache un résultat vide trop longtemps pour permettre un recalcul rapide
// Si des langues manquantes sont détectées, mettre un cache très court (5 minutes)
$cache_duration = !empty($langues_manquantes) ? 5 * \MINUTE_IN_SECONDS : 1 * \HOUR_IN_SECONDS;
if ($use_cache) {
\set_transient($cache_key, [], $cache_duration);
}
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);
error_log('🔍 Capacités pour ' . $langue_term->name . ' (ID: ' . $langue_term->term_id . '): ' . count($capacites));
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'],
];
}
error_log('🔍 Résultat: ' . print_r($result, true));
// 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 période spécifique
* @param string $date_debut Date de début (Y-m-d)
* @param string $date_fin Date de fin (Y-m-d)
*/
public static function invalidate_cache_periode(string $date_debut, string $date_fin): void {
$cache_key = 'crvi_langues_capacites_acf_' . $date_debut . '_' . $date_fin;
\delete_transient($cache_key);
error_log('🗑️ Cache invalidé pour la période ' . $date_debut . ' à ' . $date_fin);
}
/**
* 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([])));
}
}
}