ajout liaison capacité traduction - events
This commit is contained in:
parent
62fa707291
commit
f7d1bb29a5
@ -661,11 +661,26 @@ class CRVI_Event_Controller {
|
||||
}
|
||||
public static function create_event($request) {
|
||||
$data = $request->get_json_params();
|
||||
|
||||
// Valider les capacités de traduction si l'événement a une langue
|
||||
if (!empty($data['langue']) && !empty($data['date_rdv']) && !empty($data['heure_rdv'])) {
|
||||
$validation = self::validate_traduction_capacite($data['langue'], $data['date_rdv'], $data['heure_rdv']);
|
||||
if (!$validation['valid']) {
|
||||
return Api_Helper::json_error($validation['message'], 400);
|
||||
}
|
||||
}
|
||||
|
||||
$model = new CRVI_Event_Model();
|
||||
$result = $model->create_event($data);
|
||||
if (is_wp_error($result)) {
|
||||
return Api_Helper::json_error($result->get_error_message(), 400);
|
||||
}
|
||||
|
||||
// Invalider le cache des capacités de traduction si l'événement a une langue
|
||||
if (!empty($data['langue'])) {
|
||||
\ESI_CRVI_AGENDA\models\CRVI_TraductionLangue_Model::invalidate_cache();
|
||||
}
|
||||
|
||||
return Api_Helper::json_success(['id' => $result, 'message' => 'Événement créé avec succès']);
|
||||
}
|
||||
public static function update_event($request) {
|
||||
@ -689,7 +704,33 @@ class CRVI_Event_Controller {
|
||||
$data = [];
|
||||
}
|
||||
|
||||
// Récupérer l'événement existant pour obtenir les valeurs actuelles
|
||||
$model = new CRVI_Event_Model();
|
||||
$existing_event = $model->get_event_enriched($id);
|
||||
|
||||
// Fusionner les données existantes avec les nouvelles pour la validation
|
||||
$merged_data = array_merge(
|
||||
[
|
||||
'langue' => $existing_event['langue'] ?? null,
|
||||
'date_rdv' => $existing_event['date_rdv'] ?? null,
|
||||
'heure_rdv' => $existing_event['heure_rdv'] ?? null,
|
||||
],
|
||||
$data
|
||||
);
|
||||
|
||||
// Valider les capacités de traduction si l'événement a une langue
|
||||
if (!empty($merged_data['langue']) && !empty($merged_data['date_rdv']) && !empty($merged_data['heure_rdv'])) {
|
||||
$validation = self::validate_traduction_capacite(
|
||||
$merged_data['langue'],
|
||||
$merged_data['date_rdv'],
|
||||
$merged_data['heure_rdv'],
|
||||
$id // Exclure l'événement actuel du comptage
|
||||
);
|
||||
if (!$validation['valid']) {
|
||||
return Api_Helper::json_error($validation['message'], 400);
|
||||
}
|
||||
}
|
||||
|
||||
$result = $model->update_event($id, $data);
|
||||
if (is_wp_error($result)) {
|
||||
$error_code = $result->get_error_code();
|
||||
@ -697,6 +738,10 @@ class CRVI_Event_Controller {
|
||||
$status_code = isset($error_data['status']) ? $error_data['status'] : 400;
|
||||
return Api_Helper::json_error($result->get_error_message(), $status_code);
|
||||
}
|
||||
|
||||
// Invalider le cache des capacités de traduction après modification
|
||||
\ESI_CRVI_AGENDA\models\CRVI_TraductionLangue_Model::invalidate_cache();
|
||||
|
||||
return Api_Helper::json_success(['id' => $id, 'message' => 'Événement modifié avec succès']);
|
||||
}
|
||||
public static function delete_event($request) {
|
||||
@ -706,9 +751,156 @@ class CRVI_Event_Controller {
|
||||
if (is_wp_error($result)) {
|
||||
return Api_Helper::json_error($result->get_error_message(), 400);
|
||||
}
|
||||
|
||||
// Invalider le cache des capacités de traduction après suppression
|
||||
\ESI_CRVI_AGENDA\models\CRVI_TraductionLangue_Model::invalidate_cache();
|
||||
|
||||
return Api_Helper::json_success(['id' => $id, 'message' => 'Événement supprimé avec succès']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Valide la disponibilité des capacités de traduction pour un événement
|
||||
*
|
||||
* @param string $langue_slug Slug de la langue (ex: 'arabe', 'tigrinya')
|
||||
* @param string $date_rdv Date du RDV (format: Y-m-d)
|
||||
* @param string $heure_rdv Heure du RDV (format: H:i:s)
|
||||
* @param int|null $exclude_event_id ID de l'événement à exclure du comptage (pour les modifications)
|
||||
* @return array ['valid' => bool, 'message' => string, 'details' => array]
|
||||
*/
|
||||
private static function validate_traduction_capacite($langue_slug, $date_rdv, $heure_rdv, $exclude_event_id = null) {
|
||||
// Récupérer le terme de la langue depuis le slug
|
||||
$langue_term = get_term_by('slug', $langue_slug, 'langue');
|
||||
if (!$langue_term || is_wp_error($langue_term)) {
|
||||
return [
|
||||
'valid' => true, // Si la langue n'existe pas, on ne bloque pas
|
||||
'message' => '',
|
||||
'details' => []
|
||||
];
|
||||
}
|
||||
|
||||
// Extraire le jour de la semaine depuis la date
|
||||
$date_obj = new \DateTime($date_rdv);
|
||||
$jours_map = [
|
||||
0 => 'dimanche',
|
||||
1 => 'lundi',
|
||||
2 => 'mardi',
|
||||
3 => 'mercredi',
|
||||
4 => 'jeudi',
|
||||
5 => 'vendredi',
|
||||
6 => 'samedi'
|
||||
];
|
||||
$jour = $jours_map[(int) $date_obj->format('w')];
|
||||
|
||||
// Extraire la période depuis l'heure
|
||||
$heure_obj = new \DateTime($heure_rdv);
|
||||
$heure_int = (int) $heure_obj->format('H');
|
||||
$periode = $heure_int < 12 ? 'matin' : 'apres_midi';
|
||||
|
||||
// Récupérer les capacités disponibles pour cette langue
|
||||
$capacites = \ESI_CRVI_AGENDA\models\CRVI_TraductionLangue_Model::getActiveCapacites([
|
||||
'langue' => $langue_term->term_id
|
||||
], false);
|
||||
|
||||
if (empty($capacites)) {
|
||||
return [
|
||||
'valid' => false,
|
||||
'message' => sprintf(
|
||||
'Aucune capacité de traduction configurée pour la langue "%s".',
|
||||
$langue_term->name
|
||||
),
|
||||
'details' => [
|
||||
'langue' => $langue_term->name,
|
||||
'jour' => $jour,
|
||||
'periode' => $periode
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
// Vérifier si une capacité correspond au jour et à la période
|
||||
$capacite_trouvee = null;
|
||||
foreach ($capacites as $capacite) {
|
||||
// Vérifier le jour (si défini dans la capacité)
|
||||
$jour_match = empty($capacite->jour) || $capacite->jour === $jour;
|
||||
|
||||
// Vérifier la période (si définie dans la capacité)
|
||||
$periode_match = empty($capacite->periode) ||
|
||||
$capacite->periode === 'journee' ||
|
||||
$capacite->periode === $periode;
|
||||
|
||||
if ($jour_match && $periode_match) {
|
||||
$capacite_trouvee = $capacite;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$capacite_trouvee) {
|
||||
return [
|
||||
'valid' => false,
|
||||
'message' => sprintf(
|
||||
'Aucune capacité de traduction disponible pour la langue "%s" le %s en %s.',
|
||||
$langue_term->name,
|
||||
$jour,
|
||||
$periode === 'matin' ? 'matinée' : 'après-midi'
|
||||
),
|
||||
'details' => [
|
||||
'langue' => $langue_term->name,
|
||||
'jour' => $jour,
|
||||
'periode' => $periode
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
// Vérifier si la capacité a encore des créneaux disponibles
|
||||
$capacite_disponible = \ESI_CRVI_AGENDA\models\CRVI_TraductionLangue_Model::isAvailable(
|
||||
$capacite_trouvee->id,
|
||||
$date_rdv,
|
||||
$capacite_trouvee
|
||||
);
|
||||
|
||||
if (!$capacite_disponible) {
|
||||
// Récupérer les statistiques pour un message détaillé
|
||||
$used = \ESI_CRVI_AGENDA\models\CRVI_TraductionLangue_Model::countUsed(
|
||||
$capacite_trouvee->id,
|
||||
$date_rdv,
|
||||
$capacite_trouvee
|
||||
);
|
||||
|
||||
return [
|
||||
'valid' => false,
|
||||
'message' => sprintf(
|
||||
'Capacité de traduction atteinte pour la langue "%s" le %s en %s. ' .
|
||||
'Créneaux utilisés : %d/%d pour cette %s.',
|
||||
$langue_term->name,
|
||||
$jour,
|
||||
$periode === 'matin' ? 'matinée' : 'après-midi',
|
||||
$used,
|
||||
$capacite_trouvee->limite,
|
||||
$capacite_trouvee->limite_par === 'semaine' ? 'semaine' : 'mois'
|
||||
),
|
||||
'details' => [
|
||||
'langue' => $langue_term->name,
|
||||
'jour' => $jour,
|
||||
'periode' => $periode,
|
||||
'used' => $used,
|
||||
'limite' => $capacite_trouvee->limite,
|
||||
'limite_par' => $capacite_trouvee->limite_par
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
// Tout est OK
|
||||
return [
|
||||
'valid' => true,
|
||||
'message' => 'Capacité de traduction disponible',
|
||||
'details' => [
|
||||
'langue' => $langue_term->name,
|
||||
'jour' => $jour,
|
||||
'periode' => $periode,
|
||||
'capacite_id' => $capacite_trouvee->id
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée des permanences pour l'intervenant connecté
|
||||
* POST /wp-json/crvi/v1/intervenant/permanences
|
||||
@ -1418,6 +1610,10 @@ class CRVI_Event_Controller {
|
||||
if (is_wp_error($result)) {
|
||||
return Api_Helper::json_error($result->get_error_message(), 400);
|
||||
}
|
||||
|
||||
// Invalider le cache des capacités de traduction après restauration
|
||||
\ESI_CRVI_AGENDA\models\CRVI_TraductionLangue_Model::invalidate_cache();
|
||||
|
||||
return Api_Helper::json_success(['id' => $id, 'message' => 'Événement restauré avec succès']);
|
||||
}
|
||||
|
||||
@ -1428,6 +1624,10 @@ class CRVI_Event_Controller {
|
||||
if (is_wp_error($result)) {
|
||||
return Api_Helper::json_error($result->get_error_message(), 400);
|
||||
}
|
||||
|
||||
// Invalider le cache des capacités de traduction après suppression définitive
|
||||
\ESI_CRVI_AGENDA\models\CRVI_TraductionLangue_Model::invalidate_cache();
|
||||
|
||||
return Api_Helper::json_success(['id' => $id, 'message' => 'Événement supprimé définitivement']);
|
||||
}
|
||||
|
||||
|
||||
@ -328,14 +328,13 @@ class CRVI_Plugin {
|
||||
// CSS des librairies (Bootstrap, Toastr, Select2) - compilé par Vite
|
||||
wp_enqueue_style('crvi_libraries_css', ESI_CRVI_AGENDA_URL . 'assets/js/dist/crvi_libraries.min.css', [], '1.0.0');
|
||||
|
||||
// CSS principal du plugin - compilé par Vite
|
||||
// CSS principal du plugin - compilé par Vite (inclut déjà agenda-visual-filters.css)
|
||||
wp_enqueue_style('crvi_main_css', ESI_CRVI_AGENDA_URL . 'assets/js/dist/crvi_main.min.css', ['crvi_libraries_css'], '1.0.0');
|
||||
|
||||
|
||||
// Librairies externes (Bootstrap, Toastr, FullCalendar) - chargées en premier
|
||||
wp_enqueue_script('crvi_libraries', ESI_CRVI_AGENDA_URL . 'assets/js/dist/crvi_libraries.min.js', ['jquery', 'select2'], '1.0.0', true);
|
||||
|
||||
// Script principal du plugin (modules locaux) - dépend des librairies
|
||||
// Script principal du plugin (modules locaux, inclut déjà agenda-visual-filters.js) - dépend des librairies
|
||||
wp_enqueue_script('crvi_main_agenda', ESI_CRVI_AGENDA_URL . 'assets/js/dist/crvi_main.min.js', ['crvi_libraries'], '1.0.0', true);
|
||||
|
||||
self::localize_acf_data('crvi_main_agenda');
|
||||
@ -468,7 +467,8 @@ class CRVI_Plugin {
|
||||
'couleurs_rdv' => self::get_couleurs_rdv_acf_data(),
|
||||
'couleurs_permanence' => self::get_couleurs_permanence_acf_data(),
|
||||
'indisponibilites_intervenants' => self::get_intervenants_disponibilites(),
|
||||
'beneficiaires' => self::get_beneficiaires_acf_data()
|
||||
'beneficiaires' => self::get_beneficiaires_acf_data(),
|
||||
'traductions_capacites' => self::get_traductions_capacites_data()
|
||||
];
|
||||
|
||||
wp_localize_script($script_handle, 'crviACFData', $acf_data);
|
||||
@ -880,6 +880,27 @@ class CRVI_Plugin {
|
||||
return $beneficiaires_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les capacités de traduction par langue avec ventilation par période et par jour
|
||||
* Format : langue_slug => {id, name, slug, total, remaining, by_periode: {matin/apres_midi: {total, remaining}}, by_jour: {lundi->dimanche: {total, remaining}}}
|
||||
*
|
||||
* @return array Capacités de traduction par langue
|
||||
*/
|
||||
private static function get_traductions_capacites_data() {
|
||||
// Vérifier que la classe du modèle existe
|
||||
if (!class_exists('\ESI_CRVI_AGENDA\models\CRVI_TraductionLangue_Model')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Récupérer la date actuelle
|
||||
$date = current_time('Y-m-d');
|
||||
|
||||
// Utiliser la méthode du modèle qui gère le cache
|
||||
$capacites = \ESI_CRVI_AGENDA\models\CRVI_TraductionLangue_Model::getAllLanguesCapacitesForACF($date, true);
|
||||
|
||||
return $capacites;
|
||||
}
|
||||
|
||||
/**
|
||||
* crvi+script loader
|
||||
*/
|
||||
|
||||
@ -840,6 +840,195 @@ class CRVI_TraductionLangue_Model extends Main_Model {
|
||||
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 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' => ['id' => X, 'name' => '...', 'total' => X, 'remaining' => Y, 'by_periode' => [...], 'by_jour' => [...]]]
|
||||
*/
|
||||
public static function getAllLanguesCapacitesForACF(?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_langues_capacites_acf_' . $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 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)) {
|
||||
// Obtenir les disponibilités avec ventilation par jour
|
||||
$dispos = self::getDisponibilitesByLangueWithJours($langue_id, $date, $use_cache);
|
||||
|
||||
$result[$langue_term->slug] = [
|
||||
'id' => $langue_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 (2 jours)
|
||||
if ($use_cache) {
|
||||
\set_transient($cache_key, $result, 2 * \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
|
||||
@ -862,9 +1051,13 @@ class CRVI_TraductionLangue_Model extends Main_Model {
|
||||
$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_timeout_crvi_dispos_langue_' . $langue_id . '_%',
|
||||
'_transient_crvi_dispos_langue_jours_' . $langue_id . '_%',
|
||||
'_transient_timeout_crvi_dispos_langue_jours_' . $langue_id . '_%'
|
||||
)
|
||||
);
|
||||
}
|
||||
@ -873,7 +1066,9 @@ class CRVI_TraductionLangue_Model extends Main_Model {
|
||||
$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_timeout_crvi_all_langues_dispos_%'
|
||||
OR option_name LIKE '_transient_crvi_langues_capacites_acf_%'
|
||||
OR option_name LIKE '_transient_timeout_crvi_langues_capacites_acf_%'"
|
||||
);
|
||||
}
|
||||
|
||||
@ -888,7 +1083,9 @@ class CRVI_TraductionLangue_Model extends Main_Model {
|
||||
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_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
|
||||
|
||||
276
assets/css/agenda-visual-filters.css
Normal file
276
assets/css/agenda-visual-filters.css
Normal file
@ -0,0 +1,276 @@
|
||||
/**
|
||||
* Styles pour les filtres visuels de l'agenda admin
|
||||
* Boutons de départements et capacités de traduction
|
||||
*/
|
||||
|
||||
/* Conteneur principal */
|
||||
.visual-filters-container {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.visual-filters-container .card {
|
||||
border: 1px solid #dee2e6;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.visual-filters-container .card-header {
|
||||
background-color: #f8f9fa;
|
||||
border-bottom: 2px solid #007bff;
|
||||
padding: 12px 20px;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.visual-filters-container .card-header:hover {
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
|
||||
.visual-filters-container .card-header h5 {
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.visual-filters-container .card-header .btn-link {
|
||||
color: #333;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.visual-filters-container .card-header .btn-link:hover {
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
#visual-filters-toggle-icon {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.visual-filters-container .card-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* Groupes de filtres */
|
||||
.filter-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.filter-group:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.filter-group-title {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
/* Conteneur des boutons */
|
||||
.button-filters {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* Bouton de filtre commun */
|
||||
.filter-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
background-color: #fff;
|
||||
border: 2px solid #dee2e6;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 0.9rem;
|
||||
color: #495057;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.filter-btn i {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.filter-btn:hover:not(.disabled) {
|
||||
border-color: #007bff;
|
||||
background-color: #f8f9fa;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0,123,255,0.2);
|
||||
}
|
||||
|
||||
.filter-btn.active {
|
||||
background-color: #007bff;
|
||||
border-color: #007bff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.filter-btn.active i {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.filter-btn.disabled {
|
||||
background-color: #e9ecef;
|
||||
border-color: #dee2e6;
|
||||
color: #6c757d;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* Bouton de capacité de traduction */
|
||||
.trad-btn {
|
||||
min-width: 200px;
|
||||
padding: 12px 16px;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.trad-btn-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.trad-btn-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.trad-btn-label {
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.trad-btn-stats {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.trad-stat {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.trad-btn.active .trad-stat {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Barre de progression */
|
||||
.trad-progress {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background-color: #e9ecef;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.trad-progress-bar {
|
||||
height: 100%;
|
||||
transition: width 0.3s ease;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Détails (matin/après-midi) */
|
||||
.trad-btn-details {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.trad-detail {
|
||||
font-size: 0.75rem;
|
||||
color: #6c757d;
|
||||
background-color: #f8f9fa;
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.trad-btn.active .trad-detail {
|
||||
background-color: rgba(255,255,255,0.2);
|
||||
color: rgba(255,255,255,0.9);
|
||||
}
|
||||
|
||||
.trad-btn.disabled .trad-detail {
|
||||
background-color: #e9ecef;
|
||||
color: #adb5bd;
|
||||
}
|
||||
|
||||
/* Animation de chargement */
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-btn.loading {
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.button-filters {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.filter-btn {
|
||||
font-size: 0.85rem;
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
.trad-btn {
|
||||
min-width: 150px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.trad-btn-label {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.trad-stat {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.trad-detail {
|
||||
font-size: 0.7rem;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
}
|
||||
|
||||
/* États spéciaux */
|
||||
.filter-btn.warning {
|
||||
border-color: #ffc107;
|
||||
}
|
||||
|
||||
.filter-btn.danger {
|
||||
border-color: #dc3545;
|
||||
}
|
||||
|
||||
/* Tooltip pour informations supplémentaires */
|
||||
.filter-btn[data-bs-toggle="tooltip"] {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Badge de notification */
|
||||
.filter-btn .badge {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
background-color: #dc3545;
|
||||
color: #fff;
|
||||
font-size: 0.7rem;
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
font-weight: 600;
|
||||
}
|
||||
@ -9,4 +9,6 @@
|
||||
@import './agenda-events.css';
|
||||
@import './crvi-agenda.css';
|
||||
/* Overrides admin calendrier (sera minifié avec le reste via Vite) */
|
||||
@import './admin-calendar-overrides.css';
|
||||
@import './admin-calendar-overrides.css';
|
||||
/* Filtres visuels agenda admin */
|
||||
@import './agenda-visual-filters.css';
|
||||
@ -11,6 +11,7 @@ import { initializeProfile } from './modules/agenda-intervenant-profile.js';
|
||||
import { initializePermanences } from './modules/agenda-intervenant-permanences.js';
|
||||
import { initializeAdminPermanences } from './modules/agenda-admin-permanences.js';
|
||||
import { initStatsTable } from './modules/agenda-stats-table.js';
|
||||
import AgendaVisualFilters from './modules/agenda-visual-filters.js';
|
||||
|
||||
/**
|
||||
* FIX GLOBAL : Corriger le stacking context de TOUTES les modales Bootstrap
|
||||
@ -126,6 +127,13 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
console.warn('⚠️ Échec de l\'initialisation du calendrier');
|
||||
}
|
||||
|
||||
// Initialiser les filtres visuels si présents
|
||||
const visualFiltersEl = document.getElementById('departements-filter-buttons');
|
||||
if (visualFiltersEl) {
|
||||
console.log('🔍 Initialisation des filtres visuels...');
|
||||
window.agendaVisualFilters = new AgendaVisualFilters();
|
||||
}
|
||||
|
||||
const modal = document.getElementById('eventModal');
|
||||
if (modal) {
|
||||
// Vérifier que bootstrap est disponible
|
||||
|
||||
290
assets/js/modules/agenda-visual-filters.js
Normal file
290
assets/js/modules/agenda-visual-filters.js
Normal file
@ -0,0 +1,290 @@
|
||||
/**
|
||||
* Gestion des filtres visuels de l'agenda admin
|
||||
* Filtres par département et capacités de traduction
|
||||
*/
|
||||
|
||||
class AgendaVisualFilters {
|
||||
constructor() {
|
||||
this.departements = window.crviACFData?.departements || {};
|
||||
this.traductions = window.crviACFData?.traductions_capacites || {};
|
||||
this.selectedDepartement = null;
|
||||
this.selectedTraduction = null;
|
||||
|
||||
this.departementsContainer = document.getElementById('departements-filter-buttons');
|
||||
this.traductionsContainer = document.getElementById('traductions-filter-buttons');
|
||||
|
||||
if (this.departementsContainer && this.traductionsContainer) {
|
||||
this.init();
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
this.renderDepartementsButtons();
|
||||
this.renderTraductionsButtons();
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère les boutons pour les départements
|
||||
*/
|
||||
renderDepartementsButtons() {
|
||||
const container = this.departementsContainer;
|
||||
container.innerHTML = '';
|
||||
|
||||
// Bouton "Tous"
|
||||
const btnTous = this.createButton({
|
||||
id: 'dept-all',
|
||||
label: 'Tous les départements',
|
||||
icon: 'fas fa-th',
|
||||
color: '#6c757d',
|
||||
active: this.selectedDepartement === null
|
||||
});
|
||||
btnTous.addEventListener('click', () => this.selectDepartement(null));
|
||||
container.appendChild(btnTous);
|
||||
|
||||
// Boutons pour chaque département
|
||||
Object.entries(this.departements).forEach(([slug, dept]) => {
|
||||
const btn = this.createButton({
|
||||
id: `dept-${slug}`,
|
||||
label: dept.nom,
|
||||
icon: dept.icone || 'fas fa-building',
|
||||
color: dept.couleur || '#6c757d',
|
||||
active: this.selectedDepartement === slug
|
||||
});
|
||||
btn.addEventListener('click', () => this.selectDepartement(slug));
|
||||
container.appendChild(btn);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère les boutons pour les capacités de traduction
|
||||
*/
|
||||
renderTraductionsButtons() {
|
||||
const container = this.traductionsContainer;
|
||||
container.innerHTML = '';
|
||||
|
||||
// Bouton "Tous"
|
||||
const btnTous = this.createButton({
|
||||
id: 'trad-all',
|
||||
label: 'Toutes les langues',
|
||||
icon: 'fas fa-language',
|
||||
color: '#6c757d',
|
||||
active: this.selectedTraduction === null
|
||||
});
|
||||
btnTous.addEventListener('click', () => this.selectTraduction(null));
|
||||
container.appendChild(btnTous);
|
||||
|
||||
// Boutons pour chaque langue avec barre de progression
|
||||
Object.entries(this.traductions).forEach(([slug, langue]) => {
|
||||
const btn = this.createTraductionButton(slug, langue);
|
||||
container.appendChild(btn);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un bouton de filtre simple
|
||||
*/
|
||||
createButton({ id, label, icon, color, active = false }) {
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = `filter-btn ${active ? 'active' : ''}`;
|
||||
btn.id = id;
|
||||
btn.innerHTML = `
|
||||
<i class="${icon}" style="color: ${color};"></i>
|
||||
<span>${label}</span>
|
||||
`;
|
||||
return btn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un bouton de capacité de traduction avec barre de progression
|
||||
*/
|
||||
createTraductionButton(slug, langue) {
|
||||
const isDisabled = langue.remaining === 0;
|
||||
const pourcentage = langue.total > 0
|
||||
? Math.round((langue.remaining / langue.total) * 100)
|
||||
: 0;
|
||||
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = `filter-btn trad-btn ${this.selectedTraduction === slug ? 'active' : ''} ${isDisabled ? 'disabled' : ''}`;
|
||||
btn.id = `trad-${slug}`;
|
||||
btn.disabled = isDisabled;
|
||||
|
||||
// Couleur de la barre selon le pourcentage
|
||||
let barColor = '#28a745'; // Vert
|
||||
if (pourcentage < 50) barColor = '#ffc107'; // Jaune
|
||||
if (pourcentage < 25) barColor = '#dc3545'; // Rouge
|
||||
if (pourcentage === 0) barColor = '#6c757d'; // Gris
|
||||
|
||||
btn.innerHTML = `
|
||||
<div class="trad-btn-content">
|
||||
<div class="trad-btn-header">
|
||||
<i class="fas fa-language"></i>
|
||||
<span class="trad-btn-label">${langue.name}</span>
|
||||
</div>
|
||||
<div class="trad-btn-stats">
|
||||
<span class="trad-stat">${langue.remaining}/${langue.total}</span>
|
||||
</div>
|
||||
<div class="trad-progress">
|
||||
<div class="trad-progress-bar" style="width: ${pourcentage}%; background-color: ${barColor};"></div>
|
||||
</div>
|
||||
<div class="trad-btn-details">
|
||||
<span class="trad-detail">Matin: ${langue.by_periode.matin.remaining}/${langue.by_periode.matin.total}</span>
|
||||
<span class="trad-detail">AM: ${langue.by_periode.apres_midi.remaining}/${langue.by_periode.apres_midi.total}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (!isDisabled) {
|
||||
btn.addEventListener('click', () => this.selectTraduction(slug));
|
||||
}
|
||||
|
||||
return btn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sélectionne un département
|
||||
*/
|
||||
selectDepartement(slug) {
|
||||
this.selectedDepartement = slug;
|
||||
|
||||
// Mettre à jour l'UI
|
||||
this.departementsContainer.querySelectorAll('.filter-btn').forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
});
|
||||
|
||||
const btnId = slug ? `dept-${slug}` : 'dept-all';
|
||||
const activeBtn = document.getElementById(btnId);
|
||||
if (activeBtn) {
|
||||
activeBtn.classList.add('active');
|
||||
}
|
||||
|
||||
// Déclencher le filtrage
|
||||
this.applyFilters();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sélectionne une capacité de traduction
|
||||
*/
|
||||
selectTraduction(slug) {
|
||||
this.selectedTraduction = slug;
|
||||
|
||||
// Mettre à jour l'UI
|
||||
this.traductionsContainer.querySelectorAll('.filter-btn').forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
});
|
||||
|
||||
const btnId = slug ? `trad-${slug}` : 'trad-all';
|
||||
const activeBtn = document.getElementById(btnId);
|
||||
if (activeBtn) {
|
||||
activeBtn.classList.add('active');
|
||||
}
|
||||
|
||||
// Déclencher le filtrage
|
||||
this.applyFilters();
|
||||
}
|
||||
|
||||
/**
|
||||
* Applique les filtres sélectionnés
|
||||
*/
|
||||
applyFilters() {
|
||||
const filters = {
|
||||
departement: this.selectedDepartement,
|
||||
traduction: this.selectedTraduction
|
||||
};
|
||||
|
||||
console.log('Filtres appliqués:', filters);
|
||||
|
||||
// Mettre à jour les filtres dans le formulaire principal si nécessaire
|
||||
if (filters.departement) {
|
||||
const deptSelect = document.getElementById('departement');
|
||||
if (deptSelect) {
|
||||
// Trouver l'option correspondante par le nom
|
||||
const deptData = this.departements[filters.departement];
|
||||
if (deptData) {
|
||||
Array.from(deptSelect.options).forEach(option => {
|
||||
if (option.textContent === deptData.nom) {
|
||||
deptSelect.value = option.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const deptSelect = document.getElementById('departement');
|
||||
if (deptSelect) {
|
||||
deptSelect.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
if (filters.traduction) {
|
||||
const langueSelect = document.getElementById('langue');
|
||||
if (langueSelect) {
|
||||
// La valeur doit correspondre au slug de la langue
|
||||
langueSelect.value = filters.traduction;
|
||||
}
|
||||
} else {
|
||||
const langueSelect = document.getElementById('langue');
|
||||
if (langueSelect) {
|
||||
langueSelect.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Déclencher un événement personnalisé pour que le calendrier se mette à jour
|
||||
document.dispatchEvent(new CustomEvent('visualFiltersChanged', {
|
||||
detail: filters
|
||||
}));
|
||||
|
||||
// Déclencher le bouton de filtrage du formulaire principal
|
||||
const filterBtn = document.getElementById('filterBtn');
|
||||
if (filterBtn) {
|
||||
filterBtn.click();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure les écouteurs d'événements globaux
|
||||
*/
|
||||
setupEventListeners() {
|
||||
// Écouter les changements de date pour mettre à jour les capacités
|
||||
const dateInput = document.getElementById('date');
|
||||
if (dateInput) {
|
||||
dateInput.addEventListener('change', () => {
|
||||
// Recharger les capacités de traduction pour cette date
|
||||
// (à implémenter si nécessaire)
|
||||
});
|
||||
}
|
||||
|
||||
// Réinitialiser les filtres visuels quand le bouton reset est cliqué
|
||||
const resetBtn = document.getElementById('resetFiltersBtn');
|
||||
if (resetBtn) {
|
||||
resetBtn.addEventListener('click', () => {
|
||||
this.selectedDepartement = null;
|
||||
this.selectedTraduction = null;
|
||||
this.renderDepartementsButtons();
|
||||
this.renderTraductionsButtons();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rafraîchit l'affichage des boutons
|
||||
*/
|
||||
refresh() {
|
||||
this.renderDepartementsButtons();
|
||||
this.renderTraductionsButtons();
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtient les filtres actifs
|
||||
*/
|
||||
getActiveFilters() {
|
||||
return {
|
||||
departement: this.selectedDepartement,
|
||||
traduction: this.selectedTraduction
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Exporter pour utilisation dans d'autres modules
|
||||
export default AgendaVisualFilters;
|
||||
@ -7,6 +7,7 @@
|
||||
|
||||
|
||||
<section class="agenda-container">
|
||||
|
||||
<!--filters-->
|
||||
<section class="filters-container">
|
||||
<form class="filters" method="get" onsubmit="return false;">
|
||||
@ -122,6 +123,43 @@
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<!-- Filtres visuels -->
|
||||
<section class="visual-filters-container mb-4">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center" style="cursor: pointer;" data-bs-toggle="collapse" data-bs-target="#visualFiltersCollapse" aria-expanded="true" aria-controls="visualFiltersCollapse">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-filter me-2"></i>Filtres rapides
|
||||
</h5>
|
||||
<button class="btn btn-link text-decoration-none p-0" type="button">
|
||||
<i class="fas fa-chevron-down" id="visual-filters-toggle-icon"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div id="visualFiltersCollapse" class="collapse show">
|
||||
<div class="card-body">
|
||||
<!-- Filtres par département -->
|
||||
<div class="filter-group mb-3">
|
||||
<h6 class="filter-group-title">
|
||||
<i class="fas fa-building me-2"></i>Départements
|
||||
</h6>
|
||||
<div id="departements-filter-buttons" class="button-filters">
|
||||
<!-- Boutons générés dynamiquement -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filtres par capacité de traduction -->
|
||||
<div class="filter-group">
|
||||
<h6 class="filter-group-title">
|
||||
<i class="fas fa-language me-2"></i>Capacités de traduction
|
||||
</h6>
|
||||
<div id="traductions-filter-buttons" class="button-filters">
|
||||
<!-- Boutons générés dynamiquement -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!--agenda-->
|
||||
<section class="agenda-inner-container" id="agenda-calendar">
|
||||
<div id="loading-indicator" style="text-align: center; padding: 20px;">
|
||||
@ -136,4 +174,24 @@
|
||||
require_once dirname(__DIR__, 1) . '/modules/agenda-modal.php';
|
||||
?>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
// Animer l'icône du collapse des filtres visuels
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const collapseElement = document.getElementById('visualFiltersCollapse');
|
||||
const toggleIcon = document.getElementById('visual-filters-toggle-icon');
|
||||
|
||||
if (collapseElement && toggleIcon) {
|
||||
collapseElement.addEventListener('show.bs.collapse', function() {
|
||||
toggleIcon.style.transform = 'rotate(180deg)';
|
||||
toggleIcon.style.transition = 'transform 0.3s ease';
|
||||
});
|
||||
|
||||
collapseElement.addEventListener('hide.bs.collapse', function() {
|
||||
toggleIcon.style.transform = 'rotate(0deg)';
|
||||
toggleIcon.style.transition = 'transform 0.3s ease';
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
Loading…
Reference in New Issue
Block a user