Compare commits

...

69 Commits
master ... main

Author SHA1 Message Date
theShlavuk
282958a13d ajout options 2026-01-22 21:06:17 +01:00
jps
24e8a54ff9 Ajout departements 2026-01-22 09:38:29 +01:00
theShlavuk
b123b75b27 ajout fonctions utiliraires 2026-01-22 08:05:14 +01:00
theShlavuk
760e4d4f78 amélioration capacité traductions 2026-01-21 23:24:07 +01:00
theShlavuk
a815a21d1b corr 2026-01-21 23:10:17 +01:00
theShlavuk
036e6cf9ee corr 2026-01-21 23:07:44 +01:00
theShlavuk
79dd7564bc jk 2026-01-21 23:05:54 +01:00
theShlavuk
e58d3eaf4e modif 2026-01-21 23:04:15 +01:00
theShlavuk
975426b01c kk 2026-01-21 22:56:46 +01:00
theShlavuk
31bc09759a k 2026-01-21 22:55:14 +01:00
theShlavuk
200f6aab15 retest 2026-01-21 22:52:55 +01:00
theShlavuk
9369ee17ce test 2026-01-21 22:48:42 +01:00
theShlavuk
444ab59ff7 ajout logs bis 2026-01-21 22:44:40 +01:00
theShlavuk
06a7848540 ajout log 2026-01-21 22:42:08 +01:00
theShlavuk
1f988d8064 amelioration recup capacité traduction 2026-01-21 22:35:25 +01:00
theShlavuk
842f41c860 ciorrection double id langue 2026-01-21 22:29:06 +01:00
theShlavuk
3349b8b1f7 corrections 2026-01-21 22:20:02 +01:00
theShlavuk
040a1075e4 amélioration select langue 2026-01-21 21:59:24 +01:00
theShlavuk
90bb68af53 langue select 2026-01-21 21:50:15 +01:00
theShlavuk
62cf615ed4 simplification des langues 2026-01-21 21:45:10 +01:00
theShlavuk
c95ec9d09a test 2026-01-21 21:36:03 +01:00
theShlavuk
d6dad2a7a9 correction et simplification 2026-01-21 21:33:37 +01:00
theShlavuk
c647a5082c correction affichage 2026-01-21 21:29:28 +01:00
theShlavuk
587646aca8 ajout gestion langues disponibles 2026-01-21 21:25:43 +01:00
theShlavuk
f31fa748b1 corrections envoi langues perma 2026-01-21 21:19:24 +01:00
theShlavuk
80618e0b96 corections 2026-01-21 21:11:35 +01:00
jps
9f1cad89be correction autocomplete 2026-01-21 11:29:49 +01:00
jps
75bff25eb0 correctifs presences 2026-01-21 11:24:44 +01:00
jps
7c426d892d corrections 2026-01-21 11:12:45 +01:00
jps
884be3761d Correction presences 2026-01-21 11:08:10 +01:00
jps
b0b5a2eb53 correction 2026-01-21 10:40:35 +01:00
jps
d577ed90f1 ajout 2026-01-21 10:37:43 +01:00
jps
f54f1060e8 Ajout clear transient pour debug 2026-01-21 10:36:19 +01:00
jps
14a7c216c5 correction filtre rapides 2026-01-21 10:34:01 +01:00
jps
23dd434546 correction visuelle filtre rapide 2026-01-21 10:11:31 +01:00
theShlavuk
f7d1bb29a5 ajout liaison capacité traduction - events 2026-01-20 23:00:56 +01:00
theShlavuk
62fa707291 ajout permanences 2026-01-20 22:35:52 +01:00
theShlavuk
3e2d5743ff ajout notif conflit indispo - events 2026-01-20 22:13:15 +01:00
theShlavuk
227f39c50f correction pagination 2026-01-20 22:00:20 +01:00
theShlavuk
b95ed600e9 ajouts stats 2026-01-20 21:57:58 +01:00
theShlavuk
683b160804 correction bouton presences 2026-01-20 21:37:26 +01:00
theShlavuk
3c8254cb86 changements departements 2026-01-20 21:28:59 +01:00
theShlavuk
fa36f16291 ajout message alert + liste rouge 2026-01-20 21:17:45 +01:00
jps
cdbb2f344f correction import css 2026-01-20 16:43:33 +01:00
jps
2120db133e timeline bis 2026-01-20 16:40:22 +01:00
jps
e545165450 timeline histo 2026-01-20 16:36:13 +01:00
jps
dde9503e07 comparaison string/number 2026-01-20 16:20:50 +01:00
jps
0b07a03925 log debug 2026-01-20 16:15:24 +01:00
jps
bfdd9b10f1 correctif couleur 2026-01-20 16:06:08 +01:00
jps
f5997d89ad adaptation couleur 2026-01-20 15:55:10 +01:00
jps
c07395efcf tset bis 2026-01-20 15:47:14 +01:00
jps
3dffdc5649 modifier popover 2026-01-20 15:45:03 +01:00
jps
af6953bcb8 456 2026-01-20 15:37:15 +01:00
jps
b18f5a874b ,m,lm,lmebis 2026-01-20 15:16:41 +01:00
jps
9a007e5b63 bfjhdbhjf2 2026-01-20 15:16:16 +01:00
jps
58f9c86f9c bjkbhj 2026-01-20 15:13:38 +01:00
jps
1d8b536e32 test 2026-01-20 15:08:54 +01:00
jps
e7873ecd1e ajout select2 langue 2026-01-20 15:00:38 +01:00
jps
524e4bf12c Ajout logs 2 2026-01-20 14:57:40 +01:00
jps
ee0f3e8ddb ajout logs 2026-01-20 14:50:45 +01:00
jps
b4e2ed7f0e ajout route traducteur controller 2026-01-20 14:17:23 +01:00
jps
4e0f28d354 ajout debug langue 2026-01-20 14:11:21 +01:00
jps
f6ae2c7a59 corrections bouton 2026-01-20 13:50:14 +01:00
jps
2236e7f773 correction edit block 2026-01-20 13:38:01 +01:00
jps
4fa1747e07 correction boutoun 2026-01-20 13:34:09 +01:00
jps
88367d0530 modularisation agenda modal 2026-01-20 13:24:39 +01:00
jps
ec0d7da5f9 correction module 2026-01-20 12:29:31 +01:00
jps
26e35e6c68 modularisation utilitaire agenda events 2026-01-20 12:26:26 +01:00
jps
0401b8a1aa correction vite 2026-01-20 12:12:07 +01:00
42 changed files with 7060 additions and 4891 deletions

View File

@ -79,6 +79,15 @@ class CRVI_Beneficiaire_Controller {
],
]);
// Endpoint pour rechercher des bénéficiaires par nom/prénom
register_rest_route('crvi/v1', '/beneficiaires/search', [
[
'methods' => 'GET',
'callback' => [self::class, 'search_beneficiaires'],
'permission_callback' => '__return_true',
],
]);
register_rest_route('crvi/v1', '/beneficiaires/(?P<id>\\d+)', [
[
'methods' => 'GET',
@ -122,6 +131,61 @@ class CRVI_Beneficiaire_Controller {
return Api_Helper::json_success($items);
}
/**
* Recherche de bénéficiaires par nom ou prénom
* GET /wp-json/crvi/v1/beneficiaires/search?search=durand
*/
public static function search_beneficiaires($request) {
$search = $request->get_param('search');
if (empty($search) || strlen($search) < 3) {
return Api_Helper::json_success([]);
}
// Rechercher les bénéficiaires par nom ou prénom
$args = [
'post_type' => 'beneficiaire',
'posts_per_page' => 20,
'post_status' => 'publish',
'meta_query' => [
'relation' => 'OR',
[
'key' => 'nom',
'value' => $search,
'compare' => 'LIKE'
],
[
'key' => 'prenom',
'value' => $search,
'compare' => 'LIKE'
]
]
];
$query = new \WP_Query($args);
$results = [];
if ($query->have_posts()) {
while ($query->have_posts()) {
$query->the_post();
$post_id = get_the_ID();
$nom = get_post_meta($post_id, 'nom', true);
$prenom = get_post_meta($post_id, 'prenom', true);
$results[] = [
'id' => $post_id,
'nom' => $nom,
'prenom' => $prenom,
'nom_complet' => trim($prenom . ' ' . $nom)
];
}
wp_reset_postdata();
}
return Api_Helper::json_success($results);
}
public static function get_item($request) {
$id = (int) $request['id'];
$item = CRVI_Beneficiaire_Model::load($id);

View File

@ -10,6 +10,7 @@ use ESI_CRVI_AGENDA\models\CRVI_Event_Model;
use ESI_CRVI_AGENDA\models\CRVI_Traducteur_Model;
use ESI_CRVI_AGENDA\models\CRVI_Intervenant_Model;
use ESI_CRVI_AGENDA\models\CRVI_Presence_Model;
use ESI_CRVI_AGENDA\models\CRVI_Departement_Model;
use ESI_CRVI_AGENDA\controllers\Intervenant_Controller;
class CRVI_Event_Controller {
@ -92,6 +93,11 @@ class CRVI_Event_Controller {
'callback' => [self::class, 'get_statuts'],
'permission_callback' => '__return_true',
]);
\register_rest_route('crvi/v1', '/filters/traductions-capacites', [
'methods' => 'GET',
'callback' => [self::class, 'get_traductions_capacites'],
'permission_callback' => '__return_true',
]);
// --- Historique bénéficiaire ---
\register_rest_route('crvi/v1', '/beneficiaires/(?P<id>\\d+)/historique', [
@ -460,35 +466,11 @@ class CRVI_Event_Controller {
}
}
// Récupérer tous les départements disponibles depuis les CPT
$departements_posts = get_posts([
'post_type' => 'departement',
'numberposts' => -1,
'post_status' => 'publish',
]);
// Récupérer tous les départements actifs
$departements = CRVI_Departement_Model::all(true, true);
$departements = [];
foreach ($departements_posts as $post) {
$departements[] = [
'id' => $post->ID,
'nom' => $post->post_title,
];
}
// Récupérer tous les types d'intervention disponibles depuis les CPT
$types_intervention_posts = get_posts([
'post_type' => 'type_intervention',
'numberposts' => -1,
'post_status' => 'publish',
]);
$types_intervention = [];
foreach ($types_intervention_posts as $post) {
$types_intervention[] = [
'id' => $post->ID,
'nom' => $post->post_title,
];
}
// Récupérer les types d'intervention groupés par département
$types_intervention_groupes = self::get_types_intervention_by_departement();
$response = [
'intervenants' => $intervenants_formatted,
@ -496,7 +478,7 @@ class CRVI_Event_Controller {
'beneficiaires' => $beneficiaires,
'langues' => $langues,
'departements' => $departements,
'types_intervention' => $types_intervention,
'types_intervention_groupes' => $types_intervention_groupes,
];
// Inclure les traducteurs seulement si la vérification a été faite
@ -507,6 +489,52 @@ class CRVI_Event_Controller {
return Api_Helper::json_success($response);
}
/**
* Récupère les types d'intervention groupés par département
* @return array Structure: [departement_id => ['nom' => string, 'types' => [['id' => int, 'nom' => string]]]]
*/
private static function get_types_intervention_by_departement() {
$departements = CRVI_Departement_Model::all(true, true);
$result = [];
foreach ($departements as $departement) {
$departement_id = $departement['id'];
$departement_nom = $departement['nom'];
// Récupérer le repeater type_dinterventions
if (function_exists('get_field')) {
$type_dinterventions = get_field('type_dinterventions', $departement_id);
if (is_array($type_dinterventions) && !empty($type_dinterventions)) {
$types = [];
foreach ($type_dinterventions as $item) {
// Le repeater contient 'type' (texte) et 'article_intervention' (ID du CPT)
$article_intervention_id = $item['article_intervention'] ?? null;
if ($article_intervention_id) {
$type_intervention_post = get_post($article_intervention_id);
if ($type_intervention_post && $type_intervention_post->post_type === 'type_intervention') {
$types[] = [
'id' => $type_intervention_post->ID,
'nom' => $type_intervention_post->post_title,
];
}
}
}
if (!empty($types)) {
$result[$departement_id] = [
'nom' => $departement_nom,
'types' => $types,
];
}
}
}
}
return $result;
}
/**
* Endpoint pour récupérer les permissions de l'utilisateur courant.
* @param WP_REST_Request $request
@ -567,12 +595,21 @@ class CRVI_Event_Controller {
$langue_term = get_term_by('slug', $langue_original, 'langue');
if ($langue_term && !is_wp_error($langue_term)) {
$langue_label = $langue_term->name;
} else {
// Debug: comprendre pourquoi on ne trouve pas le terme
error_log('CRVI Debug - Langue non trouvée: ID=' . $langue_original);
if (is_wp_error($langue_term)) {
error_log('CRVI Debug - Erreur WP: ' . $langue_term->get_error_message());
}
}
}
// Ajouter le label de la langue pour l'affichage dans la modal
// Ajouter le label de la langue pour l'affichage dans la modal (seulement si on a trouvé le nom)
if (!empty($langue_label)) {
$event['langue_label'] = $langue_label;
} else {
// Ne pas assigner l'ID comme fallback - laisser le champ vide
$event['langue_label'] = '';
}
}
@ -626,12 +663,21 @@ class CRVI_Event_Controller {
$langue_term = get_term_by('slug', $langue_original, 'langue');
if ($langue_term && !is_wp_error($langue_term)) {
$langue_label = $langue_term->name;
} else {
// Debug: comprendre pourquoi on ne trouve pas le terme
error_log('CRVI Debug get_event - Langue non trouvée: ID=' . $langue_original);
if (is_wp_error($langue_term)) {
error_log('CRVI Debug get_event - Erreur WP: ' . $langue_term->get_error_message());
}
}
}
// Ajouter le label de la langue pour l'affichage dans la modal
// Ajouter le label de la langue pour l'affichage dans la modal (seulement si on a trouvé le nom)
if (!empty($langue_label)) {
$event['langue_label'] = $langue_label;
} else {
// Ne pas assigner l'ID comme fallback - laisser le champ vide
$event['langue_label'] = '';
}
}
@ -655,11 +701,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) {
@ -683,7 +744,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();
@ -691,6 +778,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) {
@ -700,9 +791,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
@ -954,13 +1192,34 @@ class CRVI_Event_Controller {
}
// Nettoyer et valider les langues (slugs de la taxonomie)
// Accepter soit des slugs soit des IDs (convertir les IDs en slugs)
$langues_valides = [];
if (!empty($langues)) {
foreach ($langues as $langue_slug) {
$langue_slug = sanitize_text_field($langue_slug);
// Vérifier que la langue existe dans la taxonomie
$term = get_term_by('slug', $langue_slug, 'langue');
if ($term && !is_wp_error($term)) {
foreach ($langues as $langue_value) {
$langue_value = sanitize_text_field($langue_value);
if (empty($langue_value)) {
continue;
}
$term = null;
$langue_slug = null;
// Si c'est un nombre, essayer de récupérer par ID
if (is_numeric($langue_value)) {
$term = get_term((int)$langue_value, 'langue');
if ($term && !is_wp_error($term)) {
$langue_slug = $term->slug;
}
} else {
// Sinon, essayer par slug
$term = get_term_by('slug', $langue_value, 'langue');
if ($term && !is_wp_error($term)) {
$langue_slug = $term->slug;
}
}
// Ajouter le slug si la langue existe
if ($langue_slug) {
$langues_valides[] = $langue_slug;
}
}
@ -1412,6 +1671,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']);
}
@ -1422,6 +1685,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']);
}
@ -1517,6 +1784,12 @@ class CRVI_Event_Controller {
$langue_term = get_term_by('slug', $langue_original, 'langue');
if ($langue_term && !is_wp_error($langue_term)) {
$langue_label = $langue_term->name;
} else {
// Debug: comprendre pourquoi on ne trouve pas le terme
error_log('CRVI Debug FullCalendar - Langue non trouvée: ID=' . $langue_original);
if (is_wp_error($langue_term)) {
error_log('CRVI Debug FullCalendar - Erreur WP: ' . $langue_term->get_error_message());
}
}
}
}
@ -1686,6 +1959,26 @@ class CRVI_Event_Controller {
return Api_Helper::json_success($statuts);
}
/**
* Récupère les capacités de traduction avec une plage de dates
* GET /crvi/v1/filters/traductions-capacites?date_debut=2025-01-01&date_fin=2025-01-31
*/
public static function get_traductions_capacites($request) {
$params = $request->get_params();
$date_debut = $params['date_debut'] ?? null;
$date_fin = $params['date_fin'] ?? null;
// Vérifier que la classe du modèle existe
if (!class_exists('\ESI_CRVI_AGENDA\models\CRVI_TraductionLangue_Model')) {
return Api_Helper::json_error('Modèle de traduction non disponible', 500);
}
// Utiliser la méthode du modèle qui gère le cache avec la plage de dates
$capacites = \ESI_CRVI_AGENDA\models\CRVI_TraductionLangue_Model::getAllLanguesCapacitesForACF($date_debut, $date_fin, true);
return Api_Helper::json_success($capacites);
}
/**
* Vérifie si l'utilisateur peut modifier un événement
* @param \WP_REST_Request $request

View File

@ -558,4 +558,156 @@ class CRVI_Notifications_Controller {
return $results;
}
/**
* Enrichit les données d'un événement avec les informations complètes
*
* @param object $event L'événement de base
* @return array|null L'événement enrichi avec département, type, bénéficiaire, etc.
*/
public static function enrich_event_data($event) {
global $wpdb;
$table_name = $wpdb->prefix . 'crvi_agenda';
// Récupérer les informations complètes de l'événement
$full_event = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$table_name} WHERE id = %d",
$event->id
));
if (!$full_event) {
return null;
}
$enriched = [
'id' => $full_event->id,
'date_rdv' => $full_event->date_rdv,
'heure_rdv' => $full_event->heure_rdv,
'type' => $full_event->type,
'statut' => $full_event->statut,
'commentaire' => $full_event->commentaire,
];
// Récupérer le département
if (!empty($full_event->id_departement)) {
$departement = get_term($full_event->id_departement, 'departement');
$enriched['departement'] = $departement && !is_wp_error($departement) ? $departement->name : '-';
} else {
$enriched['departement'] = '-';
}
// Récupérer le type d'intervention
if (!empty($full_event->id_type_intervention)) {
$type_intervention = get_term($full_event->id_type_intervention, 'type_intervention');
$enriched['type_intervention'] = $type_intervention && !is_wp_error($type_intervention) ? $type_intervention->name : '-';
} else {
$enriched['type_intervention'] = '-';
}
// Déterminer si c'est un rendez-vous de groupe ou individuel
$enriched['type_rdv'] = $full_event->type === 'groupe' ? 'Groupe' : 'Individuel';
// Récupérer le(s) bénéficiaire(s)
$enriched['beneficiaires'] = [];
if ($full_event->type === 'groupe') {
// Pour les groupes, récupérer tous les bénéficiaires du groupe
$beneficiaires_ids = $wpdb->get_col($wpdb->prepare(
"SELECT id_beneficiaire FROM {$wpdb->prefix}crvi_agenda_groupe_beneficiaires WHERE id_event = %d",
$full_event->id
));
foreach ($beneficiaires_ids as $benef_id) {
$benef = get_post($benef_id);
if ($benef) {
$enriched['beneficiaires'][] = get_the_title($benef_id);
}
}
} else {
// Pour les RDV individuels
if (!empty($full_event->id_beneficiaire)) {
$benef = get_post($full_event->id_beneficiaire);
if ($benef) {
$enriched['beneficiaires'][] = get_the_title($full_event->id_beneficiaire);
}
}
}
return $enriched;
}
/**
* Envoie un email de notification aux administrateurs en cas de conflit de disponibilité
*
* @param int $user_id L'ID de l'utilisateur (intervenant)
* @param array $conflicts Les conflits détectés
* @return bool True si l'email a été envoyé, false sinon
*/
public static function send_conflict_notification($user_id, $conflicts) {
// Récupérer l'email admin depuis les options ACF
$admin_email = get_field('email_admin', 'option');
// Fallback sur l'email WordPress si l'option ACF n'existe pas
if (empty($admin_email)) {
$admin_email = get_option('admin_email');
}
if (empty($admin_email)) {
error_log('CRVI: Aucun email admin trouvé pour l\'envoi de notification de conflit');
return false;
}
// Récupérer les informations de l'intervenant
$user = get_user_by('id', $user_id);
if (!$user) {
error_log('CRVI: Utilisateur introuvable (ID: ' . $user_id . ')');
return false;
}
// Enrichir les données de tous les événements en conflit
foreach ($conflicts as $key => &$conflict) {
if (isset($conflict['events'])) {
foreach ($conflict['events'] as &$event) {
$event['enriched'] = self::enrich_event_data($event);
}
}
}
unset($conflict, $event); // Libérer les références
// Créer le sujet de l'email
$subject = sprintf(
'[Agenda CRVI] Alerte : Conflit de disponibilité - %s',
$user->display_name
);
// Charger le template email
$template_path = plugin_dir_path(__FILE__) . '../../templates/email/conflict-notification.php';
if (!file_exists($template_path)) {
error_log('CRVI: Template email introuvable : ' . $template_path);
return false;
}
// Exécuter le template PHP avec output buffering pour capturer le HTML généré
ob_start();
include $template_path;
$message = ob_get_clean();
// Configuration des en-têtes pour l'email HTML
$headers = [
'Content-Type: text/html; charset=UTF-8',
'From: Agenda CRVI <' . get_option('admin_email') . '>'
];
// Envoyer l'email
$sent = wp_mail($admin_email, $subject, $message, $headers);
if ($sent) {
error_log('CRVI: Email de notification de conflit envoyé avec succès à ' . $admin_email);
} else {
error_log('CRVI: Échec de l\'envoi de l\'email de notification de conflit à ' . $admin_email);
}
return $sent;
}
}

View File

@ -171,6 +171,10 @@ class CRVI_Plugin {
add_action('acf/save_post', [self::class, 'check_intervenant_availability_on_save'], 20);
// Hook pour afficher les messages de conflit d'indisponibilités
add_action('admin_notices', [self::class, 'display_intervenant_conflicts_notice']);
// Hook pour vider le cache via paramètre URL
add_action('admin_init', [self::class, 'handle_clear_cache_request']);
// Redirection des utilisateurs non connectés depuis les pages intervenant
add_action('template_redirect', [self::class, 'redirect_non_logged_from_intervenant_pages']);
}
/**
@ -181,7 +185,7 @@ class CRVI_Plugin {
add_menu_page(
__('Agenda CRVI', 'esi_crvi_agenda'),
__('Agenda CRVI', 'esi_crvi_agenda'),
'manage_options',
'edit_posts',
'crvi_agenda',
[CRVI_Main_View::class, 'render_hub_admin_page'],
'dashicons-calendar-alt',
@ -193,7 +197,7 @@ class CRVI_Plugin {
'crvi_agenda',
__('Hub', 'esi_crvi_agenda'),
__('Hub', 'esi_crvi_agenda'),
'manage_options',
'edit_posts',
'crvi_agenda',
[CRVI_Main_View::class, 'render_hub_admin_page']
);
@ -203,7 +207,7 @@ class CRVI_Plugin {
'crvi_agenda',
__('Stats', 'esi_crvi_agenda'),
__('Stats', 'esi_crvi_agenda'),
'manage_options',
'edit_posts',
'crvi_agenda_stats',
[self::class, 'render_stats_page']
);
@ -213,7 +217,7 @@ class CRVI_Plugin {
'crvi_agenda',
__('Agenda', 'esi_crvi_agenda'),
__('Agenda', 'esi_crvi_agenda'),
'manage_options',
'edit_posts',
'crvi_agenda_hub',
[CRVI_Agenda_View::class, 'render_agenda_page']
);
@ -223,7 +227,7 @@ class CRVI_Plugin {
'crvi_agenda',
__('Permanences', 'esi_crvi_agenda'),
__('Permanences', 'esi_crvi_agenda'),
'manage_options',
'edit_posts',
'crvi_agenda_permanences',
[self::class, 'render_permanences_page']
);
@ -280,6 +284,85 @@ class CRVI_Plugin {
public static function load_filters() {
/* add_filter('rest_endpoints', [self::class, 'register_routes']); */
add_filter('wp_script_attributes', [self::class, 'custom_script_tag'], 10, 2);
// Redirection des intervenants après connexion vers leur espace
add_filter('login_redirect', [self::class, 'redirect_intervenant_after_login'], 10, 3);
}
/**
* Redirige les intervenants vers leur espace après connexion
*
* @param string $redirect_to URL de redirection par défaut
* @param string $requested_redirect_to URL de redirection demandée
* @param WP_User|WP_Error $user Utilisateur connecté ou erreur
* @return string URL de redirection
*/
public static function redirect_intervenant_after_login($redirect_to, $requested_redirect_to, $user) {
// Vérifier si l'utilisateur est valide et n'est pas une erreur
if (is_wp_error($user) || !is_a($user, 'WP_User')) {
return $redirect_to;
}
// Si une redirection spécifique a été demandée, la respecter
if (!empty($requested_redirect_to)) {
return $requested_redirect_to;
}
// Vérifier si l'utilisateur a le rôle 'intervenant'
if (in_array('intervenant', $user->roles, true)) {
// Rediriger vers l'espace intervenant
return home_url('/espace-intervenant');
}
// Pour les autres utilisateurs, utiliser la redirection par défaut
return $redirect_to;
}
/**
* Redirige les utilisateurs non connectés depuis les pages intervenant vers la home
* Vérifie la page 'espace-intervenant' (ID 3307) et ses pages enfants
*/
public static function redirect_non_logged_from_intervenant_pages() {
// Vérifier si l'utilisateur est connecté
if (is_user_logged_in()) {
return;
}
global $post;
// ID de la page espace-intervenant
$intervenant_page_id = 3307;
// Vérifier si on est sur la page espace-intervenant ou une de ses pages enfants
$is_intervenant_page = false;
if ($post && $post->post_type === 'page') {
// Vérifier si c'est la page espace-intervenant elle-même
if ($post->ID == $intervenant_page_id) {
$is_intervenant_page = true;
}
// Vérifier si c'est une page enfant de espace-intervenant
if (!$is_intervenant_page && $post->post_parent) {
$current_post = $post;
// Remonter la hiérarchie pour vérifier si un parent est la page 3307
while ($current_post->post_parent) {
$current_post = get_post($current_post->post_parent);
if (!$current_post) {
break;
}
if ($current_post->ID == $intervenant_page_id) {
$is_intervenant_page = true;
break;
}
}
}
}
// Si on est sur une page intervenant et que l'utilisateur n'est pas connecté, rediriger vers la home
if ($is_intervenant_page) {
wp_redirect(home_url('/'));
exit;
}
}
public function load_shortcodes() {
@ -328,14 +411,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');
@ -467,7 +549,9 @@ class CRVI_Plugin {
'permissions' => self::get_user_permissions(),
'couleurs_rdv' => self::get_couleurs_rdv_acf_data(),
'couleurs_permanence' => self::get_couleurs_permanence_acf_data(),
'indisponibilites_intervenants' => self::get_intervenants_disponibilites()
'indisponibilites_intervenants' => self::get_intervenants_disponibilites(),
'beneficiaires' => self::get_beneficiaires_acf_data(),
'traductions_capacites' => self::get_traductions_capacites_data()
];
wp_localize_script($script_handle, 'crviACFData', $acf_data);
@ -840,6 +924,70 @@ class CRVI_Plugin {
return $intervenants_data;
}
/**
* Récupère les données des bénéficiaires (notamment leur statut liste rouge)
*/
private static function get_beneficiaires_acf_data() {
$transient_key = 'crvi_beneficiaires_liste_rouge';
$beneficiaires_data = get_transient($transient_key);
if (false === $beneficiaires_data) {
$beneficiaires_data = [];
// Vérifier que ACF est actif
if (!function_exists('get_field')) {
return $beneficiaires_data;
}
// Récupérer tous les bénéficiaires
$beneficiaires = get_posts([
'post_type' => 'beneficiaire',
'numberposts' => -1,
'post_status' => 'publish'
]);
foreach ($beneficiaires as $beneficiaire) {
// Récupérer le champ ACF personne_en_liste_rouge
$personne_en_liste_rouge = get_field('field_69495826ac495', $beneficiaire->ID);
$beneficiaires_data[$beneficiaire->ID] = [
'id' => $beneficiaire->ID,
'personne_en_liste_rouge' => (bool) $personne_en_liste_rouge
];
}
// Mettre en cache pour 3 heures
set_transient($transient_key, $beneficiaires_data, 3 * HOUR_IN_SECONDS);
}
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 [];
}
// Calculer le début et la fin du mois courant
$date_actuelle = current_time('Y-m-d');
$date_obj = new \DateTime($date_actuelle);
$date_debut = $date_obj->format('Y-m-01'); // Premier jour du mois
$date_fin = $date_obj->format('Y-m-t'); // Dernier jour du mois
// Utiliser la méthode du modèle qui gère le cache avec la plage de dates
$capacites = \ESI_CRVI_AGENDA\models\CRVI_TraductionLangue_Model::getAllLanguesCapacitesForACF($date_debut, $date_fin, true);
error_log('🔍 Capacités de traduction récupérées pour le mois de ' . $date_debut . ' à ' . $date_fin . ': ' . print_r($capacites, true));
return $capacites;
}
/**
* crvi+script loader
*/
@ -948,7 +1096,7 @@ class CRVI_Plugin {
delete_transient('crvi_intervenants_disponibilites');
global $wpdb;
$table_name = $wpdb->prefix . 'crvi_agenda_events';
$table_name = $wpdb->prefix . 'crvi_agenda';
$conflicts = [];
// 1. Vérifier les jours de disponibilité
@ -1123,6 +1271,9 @@ class CRVI_Plugin {
// Stocker le message dans un transient pour l'afficher après la redirection
set_transient('crvi_intervenant_conflicts_' . $user_id, $message, 30);
// Envoyer un email de notification aux administrateurs via le contrôleur de notifications
CRVI_Notifications_Controller::send_conflict_notification($user_id, $conflicts);
}
}
@ -1159,6 +1310,9 @@ class CRVI_Plugin {
* Affiche les messages de conflit d'indisponibilités stockés dans les transients (admin)
*/
public static function display_intervenant_conflicts_notice() {
// DEBUG: Vérifier que l'action est bien appelée
// error_log('CRVI DEBUG: display_intervenant_conflicts_notice appelé');
// Récupérer l'ID utilisateur courant si on est sur une page de profil
$user_id = 0;
@ -1217,4 +1371,58 @@ class CRVI_Plugin {
delete_transient('crvi_intervenant_conflicts_' . $user_id);
}
}
/**
* Gère la demande de vidage du cache via paramètre URL
* Usage: ?crvi_clear_cache=1 ou ?crvi_clear_cache=traductions
*/
public static function handle_clear_cache_request() {
// Vérifier si le paramètre existe
if (!isset($_GET['crvi_clear_cache'])) {
return;
}
// Vérifier les permissions (seulement pour les admins)
if (!current_user_can('administrator')) {
wp_die('Vous n\'avez pas les permissions nécessaires pour vider le cache.');
}
$cache_type = sanitize_text_field($_GET['crvi_clear_cache']);
global $wpdb;
switch ($cache_type) {
case 'traductions':
case 'capacites':
// Vider uniquement le cache des capacités de traduction
\ESI_CRVI_AGENDA\models\CRVI_TraductionLangue_Model::invalidate_cache();
$message = 'Cache des capacités de traduction vidé avec succès.';
break;
case 'all':
case '1':
case 'true':
// Vider tous les transients du plugin CRVI
$wpdb->query(
"DELETE FROM {$wpdb->options}
WHERE option_name LIKE '_transient_crvi_%'
OR option_name LIKE '_transient_timeout_crvi_%'"
);
$message = 'Tous les caches CRVI vidés avec succès.';
break;
default:
$message = 'Type de cache non reconnu. Utilisez: traductions, all, ou 1';
break;
}
// Afficher un message et rediriger
add_action('admin_notices', function() use ($message) {
echo '<div class="notice notice-success is-dismissible"><p>' . esc_html($message) . '</p></div>';
});
// Rediriger pour retirer le paramètre de l'URL
$redirect_url = remove_query_arg('crvi_clear_cache');
wp_safe_redirect($redirect_url);
exit;
}
}

View File

@ -493,4 +493,12 @@ class CRVI_TraductionLangue_Controller {
// Afficher le template
include dirname(__DIR__, 2) . '/templates/admin/traduction-langue-list.php';
}
/**
* Enregistre les routes REST API pour les capacités de traduction
*/
public static function register_routes() {
// Pas de routes API nécessaires pour le moment
// Cette méthode est requise par le système de Plugin::register_routes()
}
}

View File

@ -14,6 +14,7 @@ class CRVI_Departement_Model extends Main_Model
public static $acf_schema = [
'nom' => 'text',
'type_dinterventions' => 'repeater',
'actif' => 'true_false',
];
/**
@ -37,8 +38,11 @@ class CRVI_Departement_Model extends Main_Model
$data['id'] = $post->ID;
} elseif ($field === 'nom') {
$data['nom'] = $post->post_title;
} elseif (property_exists(self::class, $field)) {
$data[$field] = get_field($field, $post->ID);
} elseif ($field === 'actif') {
// Charger le champ ACF 'actif'
$data['actif'] = function_exists('get_field') ? get_field('actif', $post->ID) : true;
} elseif (isset(self::$acf_schema[$field])) {
$data[$field] = function_exists('get_field') ? get_field($field, $post->ID) : null;
}
}
return new self($data);
@ -50,8 +54,10 @@ class CRVI_Departement_Model extends Main_Model
/**
* Retourne la liste de tous les départements (CPT)
* @param bool $simple_list Si true, retourne uniquement id et nom
* @param bool $only_active Si true (défaut), ne retourne que les départements actifs
*/
public static function all($simple_list = false)
public static function all($simple_list = false, $only_active = true)
{
$posts = get_posts([
'post_type' => 'departement',
@ -59,6 +65,16 @@ class CRVI_Departement_Model extends Main_Model
'post_status' => 'publish',
]);
// Filtrer par statut actif si demandé
if ($only_active && function_exists('get_field')) {
$posts = array_filter($posts, function($post) {
$actif = get_field('actif', $post->ID);
// N'afficher que les départements où le champ 'actif' est explicitement 'oui' (true, 1, '1', ou 'oui')
// Exclure ceux où le champ est null, false, 0, ou inexistant
return ($actif === true || $actif === 1 || $actif === '1' || $actif === 'oui');
});
}
if ($simple_list) {
$posts = array_map(function($post) {
return [

View File

@ -594,8 +594,15 @@ class CRVI_Event_Model extends Main_Model {
* get departement nom
*/
public function get_departement_nom($id_departement) {
if (empty($id_departement)) {
return '';
}
$departement = get_term_by('id', $id_departement, 'departement');
if (!$departement || is_wp_error($departement)) {
return '';
}
return $departement->name;
}
@ -603,7 +610,15 @@ class CRVI_Event_Model extends Main_Model {
* get type_intervention nom
*/
public function get_type_intervention_nom($id_type_intervention) {
if (empty($id_type_intervention)) {
return '';
}
$type_intervention = get_term_by('id', $id_type_intervention, 'type_intervention');
if (!$type_intervention || is_wp_error($type_intervention)) {
return '';
}
return $type_intervention->name;
}
@ -845,6 +860,7 @@ class CRVI_Event_Model extends Main_Model {
'id_intervenant' => $existing_event->id_intervenant ?? null,
'id_traducteur' => $existing_event->id_traducteur ?? null,
'id_local' => $existing_event->id_local ?? null,
'langues_disponibles' => $existing_event->langues_disponibles ?? null,
], $data);
// 5. Valider les données fusionnées (aligné avec create_event)
@ -949,6 +965,38 @@ class CRVI_Event_Model extends Main_Model {
// 7. Calculer automatiquement le flag assign
$update_data['assign'] = self::calculate_assign_flag($update_data);
// 7.5. Traiter langues_disponibles UNIQUEMENT pour les permanences
// Ne pas traiter pour les autres types d'événements pour ne pas casser la mise à jour classique
if (($update_data['type'] ?? '') === 'permanence') {
$langues_disponibles_value = '';
// Si c'est un tableau (langues), le convertir en chaîne séparée par virgules
if (isset($data['langues']) && is_array($data['langues']) && !empty($data['langues'])) {
// Nettoyer et valider les slugs de langues
$langues_valides = [];
foreach ($data['langues'] as $langue_slug) {
$langue_slug = sanitize_text_field($langue_slug);
if (!empty($langue_slug)) {
$langues_valides[] = $langue_slug;
}
}
// Joindre les langues avec le séparateur virgule (,)
if (!empty($langues_valides)) {
$langues_disponibles_value = implode(',', $langues_valides);
}
}
// Si c'est déjà une chaîne (langues_disponibles) - pour mise à jour directe
elseif (isset($data['langues_disponibles'])) {
$langues_disponibles_value = sanitize_text_field($data['langues_disponibles']);
}
// Si aucune langue n'est envoyée, conserver la valeur existante (déjà dans $update_data via array_merge)
// Mettre à jour seulement si une valeur a été fournie
if (isset($data['langues']) || isset($data['langues_disponibles'])) {
$update_data['langues_disponibles'] = $langues_disponibles_value;
}
}
// 8. Ajouter les métadonnées de modification
$update_data['modifie_par'] = get_current_user_id();
$update_data['date_modification'] = current_time('mysql');
@ -959,7 +1007,7 @@ class CRVI_Event_Model extends Main_Model {
'date_rdv', 'heure_rdv', 'date_fin', 'heure_fin', 'type', 'statut',
'motif_annulation', 'commentaire', 'id_departement', 'id_type_intervention',
'langue', 'id_beneficiaire', 'id_intervenant', 'id_traducteur', 'id_local',
'assign', 'modifie_par', 'date_modification'
'langues_disponibles', 'assign', 'modifie_par', 'date_modification'
];
$filtered_data = [];
@ -1124,6 +1172,25 @@ class CRVI_Event_Model extends Main_Model {
$where = [];
$values = [];
// Traitement spécial pour le filtre langue (inclure les permanences avec langues_disponibles)
$langue_filter_value = null;
$langue_slug = null;
if (!empty($params['langue']) || (isset($params['langue']) && $params['langue'] === '0')) {
$langue_filter_value = $params['langue'];
// Essayer d'obtenir le slug de la langue à partir de l'ID
$langue_term = get_term((int)$langue_filter_value, 'langue');
if ($langue_term && !is_wp_error($langue_term)) {
$langue_slug = $langue_term->slug;
} else {
// Si ce n'est pas un ID, essayer comme slug directement
$langue_term = get_term_by('slug', $langue_filter_value, 'langue');
if ($langue_term && !is_wp_error($langue_term)) {
$langue_slug = $langue_term->slug;
}
}
}
// Traitement des filtres
foreach ($map as $api => $col) {
if (!empty($params[$api]) || (isset($params[$api]) && $params[$api] === '0')) {
@ -1137,6 +1204,19 @@ class CRVI_Event_Model extends Main_Model {
// Support du filtre assign (0 ou 1)
$where[] = "$col = %d";
$values[] = (int)$params[$api];
} elseif ($api === 'langue') {
// Filtre langue spécial : inclure les rendez-vous avec cette langue
// ET les permanences avec cette langue dans langues_disponibles
if ($langue_slug) {
// Inclure les événements avec langue = ID OU les permanences avec langues_disponibles contenant le slug
$where[] = "($col = %s OR (type = 'permanence' AND langues_disponibles LIKE %s))";
$values[] = $langue_filter_value;
$values[] = '%' . $wpdb->esc_like($langue_slug) . '%';
} else {
// Fallback : comportement normal si on ne trouve pas le slug
$where[] = "$col = %s";
$values[] = $langue_filter_value;
}
} else {
$where[] = "$col = %s";
$values[] = $params[$api];
@ -1423,6 +1503,25 @@ class CRVI_Event_Model extends Main_Model {
$where = [];
$values = [];
// Traitement spécial pour le filtre langue (inclure les permanences avec langues_disponibles)
$langue_filter_value = null;
$langue_slug = null;
if (!empty($params['langue']) || (isset($params['langue']) && $params['langue'] === '0')) {
$langue_filter_value = $params['langue'];
// Essayer d'obtenir le slug de la langue à partir de l'ID
$langue_term = get_term((int)$langue_filter_value, 'langue');
if ($langue_term && !is_wp_error($langue_term)) {
$langue_slug = $langue_term->slug;
} else {
// Si ce n'est pas un ID, essayer comme slug directement
$langue_term = get_term_by('slug', $langue_filter_value, 'langue');
if ($langue_term && !is_wp_error($langue_term)) {
$langue_slug = $langue_term->slug;
}
}
}
// Traitement des filtres
foreach ($map as $api => $col) {
if (!empty($params[$api]) || (isset($params[$api]) && $params[$api] === '0')) {
@ -1435,6 +1534,19 @@ class CRVI_Event_Model extends Main_Model {
} elseif ($api === 'assign') {
$where[] = "$col = %d";
$values[] = (int)$params[$api];
} elseif ($api === 'langue') {
// Filtre langue spécial : inclure les rendez-vous avec cette langue
// ET les permanences avec cette langue dans langues_disponibles
if ($langue_slug) {
// Inclure les événements avec langue = ID OU les permanences avec langues_disponibles contenant le slug
$where[] = "($col = %s OR (type = 'permanence' AND langues_disponibles LIKE %s))";
$values[] = $langue_filter_value;
$values[] = '%' . $wpdb->esc_like($langue_slug) . '%';
} else {
// Fallback : comportement normal si on ne trouve pas le slug
$where[] = "$col = %s";
$values[] = $langue_filter_value;
}
} else {
$where[] = "$col = %s";
$values[] = $params[$api];
@ -1673,8 +1785,20 @@ class CRVI_Event_Model extends Main_Model {
'id' => (int) $event['id'],
'date_rdv' => $event['date_rdv'],
'heure_rdv' => $event['heure_rdv'],
'type' => $event['type'] ?? '',
'statut' => $event['statut'] ?? '',
'commentaire' => $event['commentaire'] ?? '',
];
// Charger le bénéficiaire et son statut liste rouge
if (!empty($event['id_beneficiaire'])) {
$personne_en_liste_rouge = false;
if (function_exists('get_field')) {
$personne_en_liste_rouge = get_field('field_69495826ac495', $event['id_beneficiaire']);
}
$enriched_event['personne_en_liste_rouge'] = (bool) $personne_en_liste_rouge;
}
// Charger l'intervenant
if (!empty($event['id_intervenant'])) {
$intervenant = CRVI_Intervenant_Model::load($event['id_intervenant'], ['id', 'nom', 'prenom']);

View File

@ -118,16 +118,6 @@ class CRVI_TraductionLangue_Model extends Main_Model {
];
// 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',
@ -147,8 +137,24 @@ class CRVI_TraductionLangue_Model extends Main_Model {
$posts = \get_posts($args);
$capacites = [];
foreach ($posts as $post) {
$capacites[] = self::load($post->ID);
// 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
@ -621,7 +627,7 @@ class CRVI_TraductionLangue_Model extends Main_Model {
// Calculer la limite mensuelle pour la période principale
$limite_mensuelle_principale = self::convertToMonthlyLimit(
$capacite->limite,
(int) $capacite->limite,
$capacite->limite_par,
$nb_semaines
);
@ -683,15 +689,29 @@ class CRVI_TraductionLangue_Model extends Main_Model {
$result['total_available'] = max(0, $result['total'] - $result['total_used']);
// Calculer les totaux par période
foreach ($result['by_periode'] as $periode => &$items) {
// Créer une copie des clés pour éviter de modifier le tableau pendant l'itération
$periodes_keys = array_keys($result['by_periode']);
foreach ($periodes_keys as $periode) {
// Ignorer les clés qui sont déjà des summaries
if (strpos($periode, '_summary') !== false) {
continue;
}
$items = $result['by_periode'][$periode];
$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'];
// Vérifier que $items est un tableau
if (is_array($items)) {
foreach ($items as $item) {
// Vérifier que $item est un tableau et non un int
if (is_array($item) && isset($item['limite']) && isset($item['used']) && isset($item['available'])) {
$periode_total += $item['limite'];
$periode_used += $item['used'];
$periode_available += $item['available'];
}
}
}
// Ajouter un résumé pour cette période
@ -840,6 +860,340 @@ 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(
(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
@ -862,9 +1216,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 +1231,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 +1248,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

View File

@ -15,7 +15,10 @@ class CRVI_Agenda_View {
$locals = CRVI_Local_Model::get_locals([],true);
$intervenants = CRVI_Intervenant_Model::get_intervenants([],true);
$departements = CRVI_Departement_Model::all(true);
$types_intervention = CRVI_Type_Intervention_Model::all(true);
// Récupérer les types d'intervention groupés par département
$types_intervention_groupes = self::get_types_intervention_by_departement();
$traducteurs = CRVI_Traducteur_Model::get_traducteurs([],true);
$langues_beneficiaire = Api_Helper::get_languages(true);
$genres = Api_Helper::get_acf_field_options('field_685e466352755');
@ -45,4 +48,50 @@ class CRVI_Agenda_View {
echo '<p style="color:red">Template agenda-page.php introuvable.</p>';
}
}
/**
* Récupère les types d'intervention groupés par département
* @return array Structure: [departement_id => ['nom' => string, 'types' => [['id' => int, 'nom' => string]]]]
*/
private static function get_types_intervention_by_departement() {
$departements = CRVI_Departement_Model::all(true);
$result = [];
foreach ($departements as $departement) {
$departement_id = $departement['id'];
$departement_nom = $departement['nom'];
// Récupérer le repeater type_dinterventions
if (function_exists('get_field')) {
$type_dinterventions = get_field('type_dinterventions', $departement_id);
if (is_array($type_dinterventions) && !empty($type_dinterventions)) {
$types = [];
foreach ($type_dinterventions as $item) {
// Le repeater contient 'type' (texte) et 'article_intervention' (ID du CPT)
$article_intervention_id = $item['article_intervention'] ?? null;
if ($article_intervention_id) {
$type_intervention_post = get_post($article_intervention_id);
if ($type_intervention_post && $type_intervention_post->post_type === 'type_intervention') {
$types[] = [
'id' => $type_intervention_post->ID,
'nom' => $type_intervention_post->post_title,
];
}
}
}
if (!empty($types)) {
$result[$departement_id] = [
'nom' => $departement_nom,
'types' => $types,
];
}
}
}
}
return $result;
}
}

View 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;
}

View File

@ -143,3 +143,270 @@
max-width: 300px !important;
}
}
/* ========================================
Timeline pour historique bénéficiaire
======================================== */
.crvi-timeline {
display: flex;
flex-direction: column;
margin: 30px auto;
padding-left: 100px;
position: relative;
}
.crvi-timeline__event {
background: #fff;
margin-bottom: 20px;
position: relative;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
padding: 25px;
transition: all 0.3s ease;
}
.crvi-timeline__event:hover {
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
}
/* Ligne verticale qui connecte les événements */
.crvi-timeline__event:after {
content: "";
width: 4px;
height: calc(100% + 20px);
position: absolute;
top: 50%;
left: -74px;
z-index: 0;
}
/* Cercle numéroté sur la ligne */
.crvi-timeline__event:before {
content: attr(data-number);
width: 50px;
height: 50px;
position: absolute;
background: #fff;
border-radius: 50%;
left: -98px;
top: 50%;
transform: translateY(-50%);
border: 4px solid;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
font-weight: 700;
z-index: 2;
color: #fff;
}
/* Masquer la ligne après le dernier élément */
.crvi-timeline__event--last:after {
content: none;
}
/* Container pour date/heure dans l'événement */
.crvi-timeline__event__header {
display: flex;
align-items: center;
margin-bottom: 15px;
padding-bottom: 15px;
border-bottom: 2px solid #f0f0f0;
}
.crvi-timeline__event__date-wrapper {
display: flex;
align-items: center;
gap: 15px;
}
.crvi-timeline__event__date-icon {
width: 50px;
height: 50px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
}
.crvi-timeline__event__date-info {
display: flex;
flex-direction: column;
}
.crvi-timeline__event__date {
font-size: 1rem;
font-weight: 700;
margin-bottom: 3px;
}
.crvi-timeline__event__time {
font-size: 0.85rem;
opacity: 0.8;
}
/* Titre de l'événement */
.crvi-timeline__event__title {
font-size: 1.1rem;
font-weight: 700;
margin-bottom: 15px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* Description/contenu */
.crvi-timeline__event__content {
font-size: 0.95rem;
line-height: 1.6;
color: #555;
}
.crvi-timeline__event__content .mb-2 {
margin-bottom: 10px;
}
.crvi-timeline__event__content strong {
color: #333;
font-weight: 600;
}
/* Type d'événement : Par défaut (gris) */
.crvi-timeline__event--default {
background: #f8f9fb;
border-left: 4px solid #9ca3af;
}
.crvi-timeline__event--default:before {
background: #e5e7eb;
border-color: #9ca3af;
color: #6b7280;
}
.crvi-timeline__event--default:after {
background: #d1d5db;
}
.crvi-timeline__event--default .crvi-timeline__event__date-icon {
background: #e5e7eb;
color: #6b7280;
}
.crvi-timeline__event--default .crvi-timeline__event__date,
.crvi-timeline__event--default .crvi-timeline__event__title {
color: #6b7280;
}
/* Type d'événement : Prévu (bleu) */
.crvi-timeline__event--prevu {
background: #eff6ff;
border-left: 4px solid #3b82f6;
}
.crvi-timeline__event--prevu:before {
background: #3b82f6;
border-color: #2563eb;
color: #fff;
}
.crvi-timeline__event--prevu:after {
background: #3b82f6;
}
.crvi-timeline__event--prevu .crvi-timeline__event__date-icon {
background: #3b82f6;
color: #fff;
}
.crvi-timeline__event--prevu .crvi-timeline__event__date,
.crvi-timeline__event--prevu .crvi-timeline__event__title {
color: #2563eb;
}
/* Type d'événement : Présent (vert) */
.crvi-timeline__event--present {
background: #f0fdf4;
border-left: 4px solid #22c55e;
}
.crvi-timeline__event--present:before {
background: #22c55e;
border-color: #16a34a;
color: #fff;
}
.crvi-timeline__event--present:after {
background: #22c55e;
}
.crvi-timeline__event--present .crvi-timeline__event__date-icon {
background: #22c55e;
color: #fff;
}
.crvi-timeline__event--present .crvi-timeline__event__date,
.crvi-timeline__event--present .crvi-timeline__event__title {
color: #16a34a;
}
/* Type d'événement : Incident/Absence (rouge) */
.crvi-timeline__event--incident {
background: #fef2f2;
border-left: 4px solid #ef4444;
}
.crvi-timeline__event--incident:before {
background: #ef4444;
border-color: #dc2626;
color: #fff;
}
.crvi-timeline__event--incident:after {
background: #ef4444;
}
.crvi-timeline__event--incident .crvi-timeline__event__date-icon {
background: #ef4444;
color: #fff;
}
.crvi-timeline__event--incident .crvi-timeline__event__date,
.crvi-timeline__event--incident .crvi-timeline__event__title {
color: #dc2626;
}
.crvi-quick-filters {
max-width: 100%;
}
.crvi-quick-filters .filter-group {
float: none;
width: 100%;
margin-bottom: 10px;
}
/* Responsive */
@media (max-width: 768px) {
.crvi-timeline {
padding-left: 50px;
}
.crvi-timeline__event:before {
width: 35px;
height: 35px;
left: -68px;
font-size: 0.9rem;
}
.crvi-timeline__event:after {
left: -53px;
width: 3px;
}
.crvi-timeline__event__header {
flex-direction: column;
align-items: flex-start;
}
}

View File

@ -7,5 +7,8 @@
/* CSS personnalisé du plugin */
@import './crvi.css';
@import './agenda-events.css';
@import './crvi-agenda.css';
/* Overrides admin calendrier (sera minifié avec le reste via Vite) */
@import './admin-calendar-overrides.css';
/* Filtres visuels agenda admin */
@import './agenda-visual-filters.css';

View File

@ -1,12 +1,17 @@
/* Styles pour l'autocomplétion des présences dans la modal de validation des présences */
/* Le conteneur td doit être en position relative pour le positionnement absolu des suggestions */
#presence_rows td {
position: relative;
}
.presence-nom-input {
position: relative;
}
.autocomplete-suggestions {
position: absolute;
z-index: 1050;
z-index: 1060;
background: white;
border: 1px solid #ccc;
border-radius: 4px;
@ -14,7 +19,6 @@
overflow-y: auto;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
margin-top: 2px;
width: 100%;
min-width: 200px;
}

View File

@ -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
@ -151,7 +159,8 @@ document.addEventListener('DOMContentLoaded', function() {
}
// Initialiser Select2 sur les <select> (commun à tous les contextes)
jQuery('.select2').select2();
// Exclure les selects avec skip-select2 (comme le select langue dans le modal)
jQuery('.select2:not(.skip-select2)').select2();
});

View File

@ -31,7 +31,13 @@ export function initializeAdminPermanences() {
}
// Initialiser Select2 pour le champ langues
initializeSelect2();
// Utiliser un petit délai pour s'assurer que le DOM est complètement prêt
setTimeout(() => {
initializeSelect2();
}, 100);
// Réinitialiser Select2 quand l'onglet devient visible (problème avec éléments cachés)
setupSelect2TabListener();
// Écouter les changements pour mettre à jour l'aperçu
setupPreviewListeners();
@ -46,13 +52,109 @@ export function initializeAdminPermanences() {
initializeCsvImport();
}
// Variable de garde pour éviter les initialisations multiples simultanées
let isInitializingSelect2 = false;
let select2Initialized = false; // Garde globale pour éviter toute réinitialisation
/**
* Initialise Select2 pour le champ de sélection des langues
*/
function initializeSelect2() {
console.log('🔍 initializeSelect2() appelée - Stack:', new Error().stack);
// Éviter les appels multiples simultanés
if (isInitializingSelect2) {
console.log('⏳ Initialisation Select2 déjà en cours, attente...');
return;
}
// Si Select2 est déjà initialisé et fonctionne, ne pas réinitialiser
const languesSelect = document.getElementById('langues-permanences');
if (languesSelect && typeof jQuery !== 'undefined' && jQuery.fn.select2) {
jQuery(languesSelect).select2({
if (languesSelect && select2Initialized) {
const $select = jQuery(languesSelect);
if ($select.data('select2') && $select.hasClass('select2-hidden-accessible')) {
console.log('✅ Select2 déjà initialisé (flag=true), pas de réinitialisation');
return;
}
}
if (!languesSelect) {
console.warn('Select langues-permanences non trouvé');
return;
}
// Vérifier que jQuery et Select2 sont disponibles
if (typeof jQuery === 'undefined' || !jQuery.fn.select2) {
console.warn('jQuery ou Select2 non disponible, réessai dans 100ms...');
setTimeout(initializeSelect2, 100);
return;
}
const $select = jQuery(languesSelect);
// DESTRUCTION AGRESSIVE : Détruire tout Select2 au début, sans pitié !
try {
// Détruire l'instance Select2 si elle existe
if ($select.data('select2')) {
$select.select2('destroy');
}
// Supprimer TOUS les conteneurs Select2 dans le parent
const parentContainer = languesSelect.closest('.card-body') || languesSelect.parentElement;
if (parentContainer) {
// Supprimer tous les conteneurs Select2
const select2Containers = parentContainer.querySelectorAll('span.select2-container');
select2Containers.forEach(container => container.remove());
}
// Nettoyer tous les attributs Select2 du select
$select.removeClass('select2-hidden-accessible');
$select.removeAttr('data-select2-id');
$select.removeAttr('aria-hidden');
$select.removeAttr('tabindex');
console.log('💥 Select2 complètement détruit (méchamment)');
} catch (e) {
console.warn('Erreur lors de la destruction agressive:', e);
}
// Marquer qu'on est en train d'initialiser
isInitializingSelect2 = true;
select2Initialized = false; // Réinitialiser le flag
// Vérifier que le select a des options
const options = languesSelect.querySelectorAll('option');
if (options.length === 0) {
console.warn('Le select langues-permanences n\'a pas d\'options');
isInitializingSelect2 = false;
return;
}
// Initialiser Select2
try {
// Vérifier que les options sont valides
const validOptions = Array.from(options).filter(opt => {
return opt.value && opt.value.trim() !== '' && opt.textContent && opt.textContent.trim() !== '';
});
console.log('📋 Options valides trouvées:', validOptions.length, 'sur', options.length);
if (validOptions.length === 0) {
console.error('Aucune option valide trouvée dans le select');
return;
}
// S'assurer que le select est visible avant l'initialisation
const isVisible = languesSelect.offsetParent !== null;
if (!isVisible) {
console.warn('Le select langues-permanences n\'est pas visible, réessai dans 200ms...');
setTimeout(initializeSelect2, 200);
return;
}
// Initialiser Select2 - laisser Select2 lire automatiquement depuis le select natif
// Ne pas passer data pour éviter les conflits avec le select natif
$select.select2({
placeholder: 'Sélectionnez une ou plusieurs langues',
allowClear: true,
width: '100%',
@ -62,9 +164,58 @@ function initializeSelect2() {
}
}
});
// Vérifier que Select2 a bien chargé les options
setTimeout(() => {
const select2Instance = $select.data('select2');
if (select2Instance) {
// Forcer Select2 à recharger les options depuis le select natif
$select.trigger('change.select2');
console.log('✅ Select2 initialisé pour langues-permanences avec', validOptions.length, 'options');
// Marquer comme initialisé
select2Initialized = true;
} else {
console.warn('⚠️ Select2 n\'a pas été correctement initialisé');
select2Initialized = false;
}
// Libérer la garde
isInitializingSelect2 = false;
}, 50);
} catch (e) {
console.error('Erreur lors de l\'initialisation de Select2:', e);
isInitializingSelect2 = false;
select2Initialized = false;
}
}
/**
* Configure un listener pour réinitialiser Select2 quand l'onglet devient visible
* (uniquement si Select2 n'est pas déjà correctement initialisé)
*/
function setupSelect2TabListener() {
const tabButtons = document.querySelectorAll('#permanences-tabs button[data-bs-toggle="tab"]');
tabButtons.forEach(button => {
button.addEventListener('shown.bs.tab', function(e) {
// Vérifier si l'onglet qui devient visible contient le select langues-permanences
const targetTabId = e.target.getAttribute('data-bs-target');
const targetTab = document.querySelector(targetTabId);
if (targetTab && targetTab.querySelector('#langues-permanences')) {
// Ne PAS réinitialiser si Select2 est déjà initialisé
// La garde globale select2Initialized empêche toute réinitialisation
if (!select2Initialized) {
console.log('🔄 Onglet changé, vérification Select2...');
setTimeout(() => {
initializeSelect2();
}, 100);
} else {
console.log('✅ Select2 déjà initialisé, pas de réinitialisation');
}
}
});
});
}
/**
* Configure la préselection des inputs jours[] et heures[]
* à partir des attributs data-days et data-time-slots de l'option sélectionnée
@ -187,6 +338,18 @@ function setupPreviewListeners() {
heuresInputs.forEach(input => {
input.addEventListener('change', updatePreview);
});
// Écouter les changements de durée de permanence (1h ou 15min)
const dureeInputs = document.querySelectorAll('input[name="duree_permanence"]');
dureeInputs.forEach(input => {
input.addEventListener('change', updatePreview);
});
// Écouter les changements du nombre de tranches (si 15min)
const nbTranchesSelect = document.getElementById('nb-tranches');
if (nbTranchesSelect) {
nbTranchesSelect.addEventListener('change', updatePreview);
}
}
/**
@ -200,6 +363,8 @@ function updatePreview() {
.map(input => input.value)
.sort(); // Trier les heures pour un affichage cohérent
const moisDebut = document.getElementById('mois-debut')?.value;
const dureePermanence = document.querySelector('input[name="duree_permanence"]:checked')?.value || '1h';
const nbTranches = dureePermanence === '15min' ? parseInt(document.getElementById('nb-tranches')?.value || '1') : 1;
if (heuresChecked.length === 0) {
clearPreview();
@ -217,15 +382,37 @@ function updatePreview() {
}
// Calculer les tranches horaires à partir des heures sélectionnées
// Chaque heure sélectionnée = 1 tranche d'1 heure
const tranches = heuresChecked.map(heureDebut => {
const [h, m] = heureDebut.split(':').map(Number);
const heureFin = `${String(h + 1).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
return {
debut: heureDebut,
fin: heureFin
};
});
let tranches = [];
if (dureePermanence === '15min') {
// Mode 15 minutes : créer des tranches de 15 minutes
heuresChecked.forEach(heureDebut => {
const [h, m] = heureDebut.split(':').map(Number);
for (let i = 0; i < nbTranches; i++) {
const minutesDebut = m + (i * 15);
const minutesFin = m + ((i + 1) * 15);
const trancheDebut = `${String(h).padStart(2, '0')}:${String(minutesDebut).padStart(2, '0')}`;
const trancheFin = `${String(h).padStart(2, '0')}:${String(minutesFin).padStart(2, '0')}`;
tranches.push({
debut: trancheDebut,
fin: trancheFin
});
}
});
} else {
// Mode 1 heure : chaque heure sélectionnée = 1 tranche d'1 heure
tranches = heuresChecked.map(heureDebut => {
const [h, m] = heureDebut.split(':').map(Number);
const heureFin = `${String(h + 1).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
return {
debut: heureDebut,
fin: heureFin
};
});
}
// Calculer les dates à partir du mois de début sélectionné
const [year, month] = moisDebut.split('-').map(Number);

View File

@ -98,3 +98,12 @@ export async function getFilters(type, params = {}) {
}
return apiFetch(`filters/${type}`);
}
export async function getTraductionsCapacites(date_debut, date_fin) {
const params = {};
if (date_debut) params.date_debut = date_debut;
if (date_fin) params.date_fin = date_fin;
const query = new URLSearchParams(params).toString();
const endpoint = `filters/traductions-capacites${query ? '?' + query : ''}`;
return apiFetch(endpoint);
}

View File

@ -1,7 +1,8 @@
// Module ES6 pour la création d'entités depuis le modal d'événement
import { apiFetch } from './agenda-api.js';
import { notifyError, notifySuccess } from './agenda-notifications.js';
import { populateSelects, preserveModalData } from './agenda-modal.js';
import { openSubModal } from './agenda-modal.js';
import { populateSelects } from './agenda-modal-select.js';
// Configuration des entités
const ENTITY_CONFIG = {
@ -97,49 +98,18 @@ function openCreateEntityModal(entityType) {
return;
}
const modal = document.getElementById(config.modalId);
if (!modal) {
console.error(`Modal non trouvé: ${config.modalId}`);
return;
}
// Réinitialiser le formulaire
const form = document.getElementById(config.formId);
if (form) {
form.reset();
}
// Les selects sont maintenant initialisés automatiquement par jQuery('.select2').select2()
// Préserver les données de la modale principale avant de la fermer
preserveModalData();
// Fermer le modal principal d'événement s'il est ouvert
const eventModal = document.getElementById('eventModal');
if (eventModal) {
const eventBsModal = bootstrap.Modal.getInstance(eventModal);
if (eventBsModal) {
eventBsModal.hide();
// Utiliser la fonction générique pour ouvrir la sous-modale
openSubModal(
config.modalId,
// Callback avant ouverture : réinitialiser le formulaire
(subModal) => {
const form = document.getElementById(config.formId);
if (form) {
form.reset();
}
// Les selects sont maintenant initialisés automatiquement par jQuery('.select2').select2()
}
}
// Attendre un peu que le modal principal se ferme avant d'ouvrir le nouveau
setTimeout(() => {
// Ouvrir le modal de création
if (window.bootstrap && window.bootstrap.Modal) {
const bsModal = new window.bootstrap.Modal(modal);
bsModal.show();
// Ajouter un event listener pour rouvrir le modal principal quand on ferme
modal.addEventListener('hidden.bs.modal', function() {
// Rouvrir le modal principal d'événement avec les données préservées
if (eventModal && window.bootstrap && window.bootstrap.Modal) {
const newEventModal = new window.bootstrap.Modal(eventModal);
newEventModal.show();
}
}, { once: true }); // Une seule fois
}
}, 300); // Délai pour la transition
);
}
// Les selects sont maintenant initialisés automatiquement par jQuery('.select2').select2()

View File

@ -0,0 +1,623 @@
// Module de mapping des événements pour FullCalendar
// Contient toute la logique de transformation des données API vers le format FullCalendar
/**
* Calcule la luminosité d'une couleur hexadécimale
* @param {string} hexColor - Couleur au format #RRGGBB
* @returns {number} - Luminosité entre 0 et 1
*/
export function getLuminance(hexColor) {
// Vérifier que hexColor n'est pas null ou undefined
if (!hexColor || typeof hexColor !== 'string') {
console.warn('⚠️ [getLuminance] Valeur hexColor invalide:', hexColor);
return 0.5; // Retourner une valeur moyenne par défaut
}
// Convertir hex en RGB
const hex = hexColor.replace('#', '');
const r = parseInt(hex.substr(0, 2), 16);
const g = parseInt(hex.substr(2, 2), 16);
const b = parseInt(hex.substr(4, 2), 16);
// Calculer la luminosité relative selon WCAG
return (0.299 * r + 0.587 * g + 0.114 * b) / 255;
}
/**
* Détermine la couleur de texte optimale selon le contraste
* @param {string} backgroundColor - Couleur de fond au format #RRGGBB
* @returns {string} - Couleur de texte (#000000 ou #ffffff)
*/
export function getTextColor(backgroundColor) {
// Vérifier que backgroundColor n'est pas null ou undefined
if (!backgroundColor || typeof backgroundColor !== 'string') {
console.warn('⚠️ [getTextColor] Valeur backgroundColor invalide:', backgroundColor);
return '#000000'; // Retourner noir par défaut
}
const luminance = getLuminance(backgroundColor);
return luminance > 0.5 ? '#000000' : '#ffffff';
}
/**
* Mappe un événement API vers le format FullCalendar
* @param {Object} ev - Événement depuis l'API
* @returns {Object} - Événement au format FullCalendar
*/
export function mapEventToFullCalendar(ev) {
// Hiérarchie de détermination des couleurs :
// 1) Pour RDV assignés (individuel/groupe) avec type d'intervention : couleur du type d'intervention
// 2) Pour RDV assignés (individuel/groupe) sans type d'intervention : orange (par défaut)
// 3) Pour permanences : couleur selon le type (assignée, non attribuée, non disponible)
let backgroundColor = null;
let textColor = null;
// Vérifier si l'événement est assigné (a un intervenant et un local/beneficiaire)
const isEventAssigned = ev.id_intervenant && (ev.id_local || ev.id_beneficiaire);
// Pour les RDV (individuel/groupe) assignés
if (ev.type && ev.type !== 'permanence' && isEventAssigned) {
// Ordre de priorité : département > type d'intervention > local > défaut
// Priorité 1 : Couleur du département
const departementId = ev.id_departement ? parseInt(ev.id_departement) : null;
// 🔍 DEBUG pour événement 410
if (ev.id === 410 || ev.id === '410') {
console.log('🔍 [MAPPER DEBUG 410 - RDV] Début analyse couleur département:', {
eventId: ev.id,
type: ev.type,
id_departement_brut: ev.id_departement,
departementId_parsed: departementId,
crviACFData_existe: !!window.crviACFData,
departements_existe: !!(window.crviACFData && window.crviACFData.departements)
});
}
if (departementId && !isNaN(departementId) && window.crviACFData && window.crviACFData.departements) {
// 🔍 DEBUG pour événement 410 - liste des départements
if (ev.id === 410 || ev.id === '410') {
console.log('🔍 [MAPPER DEBUG 410 - RDV] Recherche département ID:', departementId);
console.log('🔍 [MAPPER DEBUG 410 - RDV] Départements disponibles:',
Object.keys(window.crviACFData.departements).map(key => ({
key: key,
id: window.crviACFData.departements[key].id,
nom: window.crviACFData.departements[key].nom,
couleur: window.crviACFData.departements[key].couleur
}))
);
}
// Chercher le département par ID
for (const key in window.crviACFData.departements) {
const dept = window.crviACFData.departements[key];
// 🔍 DEBUG pour événement 410 - comparaison
if (ev.id === 410 || ev.id === '410') {
console.log('🔍 [MAPPER DEBUG 410 - RDV] Comparaison:', {
key: key,
dept_id: dept.id,
dept_id_type: typeof dept.id,
recherche_id: departementId,
recherche_id_type: typeof departementId,
sont_egaux: dept.id === departementId,
dept_couleur: dept.couleur
});
}
if (dept.id === departementId && dept.couleur) {
backgroundColor = dept.couleur;
textColor = getTextColor(backgroundColor);
console.log('🎨 [COULEUR] RDV assigné - département:', {
eventId: ev.id,
type: ev.type,
departementId: departementId,
departementNom: dept.nom,
backgroundColor: backgroundColor,
source: 'departement'
});
// 🔍 DEBUG pour événement 410 - succès
if (ev.id === 410 || ev.id === '410') {
console.log('✅ [MAPPER DEBUG 410 - RDV] Couleur département appliquée:', {
departementNom: dept.nom,
couleurAppliquee: backgroundColor
});
}
break;
}
}
// 🔍 DEBUG pour événement 410 - échec de recherche
if ((ev.id === 410 || ev.id === '410') && !backgroundColor) {
console.warn('⚠️ [MAPPER DEBUG 410 - RDV] Aucun département correspondant trouvé!');
}
}
// Priorité 2 : Type d'intervention (si pas de département)
if (!backgroundColor) {
let typeInterventionId = null;
if (ev.id_type_intervention) {
typeInterventionId = parseInt(ev.id_type_intervention);
} else if (ev.type_intervention && ev.type_intervention.id) {
typeInterventionId = parseInt(ev.type_intervention.id);
}
// Si l'événement a un type d'intervention, utiliser sa couleur depuis crviAjax
if (typeInterventionId && !isNaN(typeInterventionId) && window.crviAjax && window.crviAjax.couleurs_types_intervention) {
const couleurTypeIntervention = window.crviAjax.couleurs_types_intervention[typeInterventionId];
if (couleurTypeIntervention) {
backgroundColor = couleurTypeIntervention;
textColor = getTextColor(couleurTypeIntervention);
console.log('🎨 [COULEUR] RDV assigné avec type d\'intervention:', {
eventId: ev.id,
type: ev.type,
typeInterventionId: typeInterventionId,
backgroundColor: backgroundColor,
textColor: textColor,
source: 'type_intervention'
});
}
}
}
// Priorité 3 : Couleur du local (type de local)
if (!backgroundColor && ev.local) {
const localType = ev.local.type || ev.local_type;
if (localType && window.crviACFData && window.crviACFData.types_local) {
const typeLocalConfig = window.crviACFData.types_local[localType];
if (typeLocalConfig && typeLocalConfig.couleur) {
backgroundColor = typeLocalConfig.couleur;
textColor = getTextColor(backgroundColor);
console.log('🎨 [COULEUR] RDV assigné - type de local:', {
eventId: ev.id,
type: ev.type,
localType: localType,
backgroundColor: backgroundColor,
source: 'type_local'
});
}
}
}
// Fallback : orange par défaut
if (!backgroundColor) {
backgroundColor = '#ff9800'; // Orange pour les événements assignés sans type d'intervention
textColor = getTextColor(backgroundColor);
console.log('🎨 [COULEUR] RDV assigné sans type d\'intervention (défaut orange):', {
eventId: ev.id,
type: ev.type,
backgroundColor: backgroundColor,
textColor: textColor,
source: 'defaut_assigné'
});
}
}
// Pour les RDV non assignés : couleur selon type_de_local (bureau ou salle)
if (!backgroundColor && ev.type && ev.type !== 'permanence' && !isEventAssigned) {
// Vérifier si l'événement a un local avec un type_de_local
if (ev.local && ev.local.type_de_local) {
const typeLocal = ev.local.type_de_local.toLowerCase();
if (window.crviACFData && window.crviACFData.couleurs_rdv) {
const couleurRdv = window.crviACFData.couleurs_rdv[typeLocal];
if (couleurRdv) {
backgroundColor = couleurRdv;
textColor = getTextColor(couleurRdv);
console.log('🎨 [COULEUR] RDV non assigné selon type de local:', {
eventId: ev.id,
type: ev.type,
typeLocal: typeLocal,
backgroundColor: backgroundColor,
textColor: textColor,
source: 'type_local'
});
}
}
}
}
// Pour permanences (événements de type 'permanence') : logique simplifiée
// 1) Non attribuée : dispo ? couleur dispo : couleur indispo
// 2) Attribuée : a type intervention ? couleur type : couleur défaut
let isPermanenceDisabled = false;
if (!backgroundColor && ev.type === 'permanence') {
// Une permanence est assignée si elle a un bénéficiaire ET un local (assign = 1)
const isPermanenceAssigned = ev.assign === 1 || (ev.id_beneficiaire && ev.id_local);
// Une permanence est non attribuée si elle n'est pas assignée (assign = 0)
const isPermanenceNonAttribuee = ev.assign === 0 && !ev.id_beneficiaire && !ev.id_local;
// Vérifier si le bénéficiaire est en congé (indisponibilitee_ponctuelle)
let isPermanenceNonDisponible = false;
if (ev.beneficiaire && ev.beneficiaire.indisponibilitee_ponctuelle && Array.isArray(ev.beneficiaire.indisponibilitee_ponctuelle)) {
const eventDate = ev.date_rdv; // Format YYYY-MM-DD
const eventDateObj = new Date(eventDate + 'T00:00:00');
// Vérifier si la date de l'événement est dans une période d'indisponibilité
for (const indispo of ev.beneficiaire.indisponibilitee_ponctuelle) {
if (indispo.debut && indispo.fin) {
let debutDate, finDate;
// Gérer le format d/m/Y (format ACF)
if (typeof indispo.debut === 'string' && indispo.debut.includes('/')) {
const debutParts = indispo.debut.split('/');
const finParts = indispo.fin.split('/');
if (debutParts.length === 3 && finParts.length === 3) {
// Format d/m/Y
debutDate = new Date(parseInt(debutParts[2]), parseInt(debutParts[1]) - 1, parseInt(debutParts[0]));
finDate = new Date(parseInt(finParts[2]), parseInt(finParts[1]) - 1, parseInt(finParts[0]));
}
} else {
// Format YYYY-MM-DD ou timestamp
debutDate = new Date(indispo.debut);
finDate = new Date(indispo.fin);
}
if (debutDate && finDate && !isNaN(debutDate.getTime()) && !isNaN(finDate.getTime())) {
// Ajuster pour inclure toute la journée
debutDate.setHours(0, 0, 0, 0);
finDate.setHours(23, 59, 59, 999);
if (eventDateObj >= debutDate && eventDateObj <= finDate) {
isPermanenceNonDisponible = true;
isPermanenceDisabled = true;
break;
}
}
}
}
}
// Vérifier si l'intervenant est indisponible (congés ou jour non disponible)
if (!isPermanenceNonDisponible && ev.id_intervenant && window.crviACFData && window.crviACFData.indisponibilites_intervenants) {
const intervenantId = parseInt(ev.id_intervenant);
const intervenantDispo = window.crviACFData.indisponibilites_intervenants[intervenantId];
if (intervenantDispo) {
const eventDate = ev.date_rdv; // Format YYYY-MM-DD
const eventDateObj = new Date(eventDate + 'T00:00:00');
// Vérifier les jours de disponibilité (0 = dimanche, 1 = lundi, etc.)
const dayOfWeek = eventDateObj.getDay();
const dayNames = ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'];
const dayName = dayNames[dayOfWeek];
// Si l'intervenant a des jours de disponibilité définis et que ce jour n'en fait pas partie
if (intervenantDispo.jours_dispo && Array.isArray(intervenantDispo.jours_dispo) &&
intervenantDispo.jours_dispo.length > 0 &&
!intervenantDispo.jours_dispo.includes(dayName)) {
console.log('🚫 [PERMANENCE] Intervenant non disponible ce jour:', {
intervenantId,
date: eventDate,
dayName,
joursDisponibles: intervenantDispo.jours_dispo
});
isPermanenceNonDisponible = true;
isPermanenceDisabled = true;
}
// Vérifier les congés/indisponibilités ponctuelles
if (!isPermanenceNonDisponible && intervenantDispo.conges && Array.isArray(intervenantDispo.conges)) {
for (const conge of intervenantDispo.conges) {
if (conge.debut && conge.fin) {
let debutDate, finDate;
// Gérer le format d/m/Y (format ACF)
if (typeof conge.debut === 'string' && conge.debut.includes('/')) {
const debutParts = conge.debut.split('/');
const finParts = conge.fin.split('/');
if (debutParts.length === 3 && finParts.length === 3) {
debutDate = new Date(parseInt(debutParts[2]), parseInt(debutParts[1]) - 1, parseInt(debutParts[0]));
finDate = new Date(parseInt(finParts[2]), parseInt(finParts[1]) - 1, parseInt(finParts[0]));
}
} else {
// Format YYYY-MM-DD ou timestamp
debutDate = new Date(conge.debut);
finDate = new Date(conge.fin);
}
if (debutDate && finDate && !isNaN(debutDate.getTime()) && !isNaN(finDate.getTime())) {
debutDate.setHours(0, 0, 0, 0);
finDate.setHours(23, 59, 59, 999);
if (eventDateObj >= debutDate && eventDateObj <= finDate) {
console.log('🚫 [PERMANENCE] Intervenant en congé:', {
intervenantId,
date: eventDate,
congeDebut: conge.debut,
congeFin: conge.fin,
congeType: conge.type
});
isPermanenceNonDisponible = true;
isPermanenceDisabled = true;
break;
}
}
}
}
}
}
}
// Vérifier aussi le statut si pas déjà déterminé
if (!isPermanenceNonDisponible && ev.statut && ['annule', 'non_tenu', 'absence'].includes(ev.statut)) {
isPermanenceNonDisponible = true;
}
// LOGIQUE SIMPLIFIÉE
if (isPermanenceNonAttribuee) {
// 1) Non attribuée : dispo ? couleur dispo : couleur indispo
if (isPermanenceNonDisponible && window.crviAjax && window.crviAjax.couleur_permanence_non_disponible) {
backgroundColor = window.crviAjax.couleur_permanence_non_disponible;
console.log('🎨 [COULEUR] Permanence non attribuée - indisponible:', {
eventId: ev.id,
backgroundColor: backgroundColor,
source: 'permanence_non_attribuee_indispo'
});
} else if (window.crviAjax && window.crviAjax.couleur_permanence_non_attribuee) {
backgroundColor = window.crviAjax.couleur_permanence_non_attribuee;
console.log('🎨 [COULEUR] Permanence non attribuée - disponible:', {
eventId: ev.id,
backgroundColor: backgroundColor,
source: 'permanence_non_attribuee_dispo'
});
}
} else if (isPermanenceAssigned) {
// 2) Attribuée : ordre de priorité - département puis local
// Priorité 1 : Couleur du département
const departementId = ev.id_departement ? parseInt(ev.id_departement) : null;
// 🔍 DEBUG pour événement 410
if (ev.id === 410 || ev.id === '410') {
console.log('🔍 [MAPPER DEBUG 410] Début analyse couleur département:', {
eventId: ev.id,
id_departement_brut: ev.id_departement,
departementId_parsed: departementId,
crviACFData_existe: !!window.crviACFData,
departements_existe: !!(window.crviACFData && window.crviACFData.departements),
departements_data: window.crviACFData ? window.crviACFData.departements : 'N/A'
});
}
if (departementId && !isNaN(departementId) && window.crviACFData && window.crviACFData.departements) {
// 🔍 DEBUG pour événement 410 - liste des départements
if (ev.id === 410 || ev.id === '410') {
console.log('🔍 [MAPPER DEBUG 410] Recherche département ID:', departementId);
console.log('🔍 [MAPPER DEBUG 410] Départements disponibles:',
Object.keys(window.crviACFData.departements).map(key => ({
key: key,
id: window.crviACFData.departements[key].id,
nom: window.crviACFData.departements[key].nom,
couleur: window.crviACFData.departements[key].couleur
}))
);
}
// Chercher le département par ID
for (const key in window.crviACFData.departements) {
const dept = window.crviACFData.departements[key];
// 🔍 DEBUG pour événement 410 - comparaison
if (ev.id === 410 || ev.id === '410') {
console.log('🔍 [MAPPER DEBUG 410] Comparaison:', {
key: key,
dept_id: dept.id,
dept_id_type: typeof dept.id,
recherche_id: departementId,
recherche_id_type: typeof departementId,
sont_egaux: dept.id === departementId,
dept_couleur: dept.couleur
});
}
if (dept.id === departementId && dept.couleur) {
backgroundColor = dept.couleur;
console.log('🎨 [COULEUR] Permanence assignée - département:', {
eventId: ev.id,
departementId: departementId,
departementNom: dept.nom,
backgroundColor: backgroundColor,
source: 'departement'
});
// 🔍 DEBUG pour événement 410 - succès
if (ev.id === 410 || ev.id === '410') {
console.log('✅ [MAPPER DEBUG 410] Couleur département appliquée:', {
departementNom: dept.nom,
couleurAppliquee: backgroundColor
});
}
break;
}
}
// 🔍 DEBUG pour événement 410 - échec de recherche
if ((ev.id === 410 || ev.id === '410') && !backgroundColor) {
console.warn('⚠️ [MAPPER DEBUG 410] Aucun département correspondant trouvé!');
}
}
// Priorité 2 : Couleur du local (type de local)
if (!backgroundColor && ev.local) {
const localType = ev.local.type || ev.local_type;
if (localType && window.crviACFData && window.crviACFData.types_local) {
const typeLocalConfig = window.crviACFData.types_local[localType];
if (typeLocalConfig && typeLocalConfig.couleur) {
backgroundColor = typeLocalConfig.couleur;
console.log('🎨 [COULEUR] Permanence assignée - type de local:', {
eventId: ev.id,
localType: localType,
backgroundColor: backgroundColor,
source: 'type_local'
});
}
}
}
// Fallback : couleur par défaut des permanences
if (!backgroundColor) {
backgroundColor = (window.crviACFData && window.crviACFData.couleurs_permanence && window.crviACFData.couleurs_permanence.permanence)
? window.crviACFData.couleurs_permanence.permanence
: '#9e9e9e';
console.log('🎨 [COULEUR] Permanence assignée - défaut:', {
eventId: ev.id,
backgroundColor: backgroundColor,
source: 'permanence_assignee_defaut'
});
}
}
// S'assurer que backgroundColor n'est pas null avant d'appeler getTextColor
if (backgroundColor) {
textColor = getTextColor(backgroundColor);
} else {
textColor = '#000000'; // Par défaut
}
}
// Couleur par défaut si aucune condition n'est remplie
if (!backgroundColor) {
backgroundColor = '#6c757d'; // Gris par défaut
textColor = getTextColor(backgroundColor);
console.log('🎨 [COULEUR] Couleur par défaut (gris):', {
eventId: ev.id,
type: ev.type,
backgroundColor: backgroundColor,
textColor: textColor,
source: 'defaut_general'
});
}
// 🔍 DEBUG FINAL pour événement 410
if (ev.id === 410 || ev.id === '410') {
console.log('🔍 [MAPPER DEBUG 410] COULEUR FINALE avant return:', {
eventId: ev.id,
backgroundColor: backgroundColor,
textColor: textColor,
type: ev.type,
id_departement: ev.id_departement,
isPermanenceAssigned: ev.assign === 1 || (ev.id_beneficiaire && ev.id_local),
ev_complet: ev
});
}
// Log final de la couleur appliquée
console.log('🎨 [COULEUR FINALE] Événement:', {
eventId: ev.id,
title: ev.type || 'Sans type',
backgroundColor: backgroundColor,
borderColor: backgroundColor,
textColor: textColor,
isAssigned: isEventAssigned,
type: ev.type
});
// Générer un titre plus explicite
let title = ev.type || 'Sans type';
// Gestion spéciale pour les permanences
if (ev.type === 'permanence') {
if (ev.intervenant) {
const intervenant = ev.intervenant;
const nomComplet = `${intervenant.prenom || ''} ${intervenant.nom || ''}`.trim();
title = 'p. ' + (nomComplet || 'Intervenant');
} else {
title = 'p. Intervenant';
}
} else {
// Utiliser le nom de l'intervenant comme titre principal pour les autres événements
if (ev.intervenant) {
const intervenant = ev.intervenant;
const nomComplet = `${intervenant.prenom || ''} ${intervenant.nom || ''}`.trim();
if (nomComplet) {
title = nomComplet;
}
}
// Ajouter le type d'intervention principal si disponible
if (ev.intervenant && ev.intervenant.types_intervention_noms && ev.intervenant.types_intervention_noms.length > 0) {
const primaryType = ev.intervenant.types_intervention_noms[0]; // Premier type d'intervention
title += ` - ${primaryType}`;
}
// Ajouter le commentaire si disponible
if (ev.commentaire) {
title += ' - ' + ev.commentaire;
}
}
// S'assurer que les couleurs sont toujours définies
if (!backgroundColor || typeof backgroundColor !== 'string') {
console.warn('⚠️ [Mapping] backgroundColor invalide pour l\'événement:', ev.id, backgroundColor);
backgroundColor = '#6c757d'; // Gris par défaut
}
if (!textColor || typeof textColor !== 'string') {
textColor = getTextColor(backgroundColor);
}
// Vérifier si l'événement est passé
const eventStartDate = new Date(ev.date_rdv + 'T' + ev.heure_rdv);
const now = new Date();
const isEventPast = eventStartDate < now;
// Un événement est éditable seulement si :
// 1. Il n'est pas désactivé (permanence)
// 2. Il n'est pas passé
const isEditable = !isPermanenceDisabled && !isEventPast;
return {
id: ev.id,
title: title,
start: ev.date_rdv + 'T' + ev.heure_rdv,
end: ev.date_fin + 'T' + ev.heure_fin,
backgroundColor: backgroundColor,
borderColor: backgroundColor,
textColor: textColor,
editable: isEditable, // Désactiver l'édition si la permanence est désactivée OU si l'événement est passé
durationEditable: isEditable, // Désactiver le redimensionnement si la permanence est désactivée OU si l'événement est passé
startEditable: isEditable, // Désactiver le déplacement si la permanence est désactivée OU si l'événement est passé
classNames: isPermanenceDisabled ? ['permanence-disabled'] : (isEventPast ? ['event-past'] : []),
extendedProps: {
// Données de base
type: ev.type,
commentaire: ev.commentaire,
date_rdv: ev.date_rdv,
heure_rdv: ev.heure_rdv,
date_fin: ev.date_fin,
heure_fin: ev.heure_fin,
// Relations avec les entités
id_beneficiaire: ev.id_beneficiaire,
id_intervenant: ev.id_intervenant,
id_traducteur: ev.id_traducteur,
id_local: ev.id_local,
id_departement: ev.id_departement,
id_type_intervention: ev.id_type_intervention || (ev.type_intervention && ev.type_intervention.id ? ev.type_intervention.id : null),
langue: ev.langue,
langues_disponibles: ev.langues_disponibles || null,
// Données des entités (si disponibles)
beneficiaire: ev.beneficiaire,
intervenant: ev.intervenant,
traducteur: ev.traducteur,
local: ev.local,
// Données spécifiques aux groupes
nb_participants: ev.nb_participants,
nb_hommes: ev.nb_hommes,
nb_femmes: ev.nb_femmes,
// Autres données
statut: ev.statut,
assign: ev.assign || 0,
isDisabled: isPermanenceDisabled,
created_at: ev.created_at,
updated_at: ev.updated_at
}
};
}

View File

@ -1,46 +1,10 @@
// Module de gestion des filtres dynamiques pour l'agenda
import { getEvents } from './agenda-api.js';
import { mapEventToFullCalendar } from './agenda-event-mapper.js';
let currentFilters = {};
let calendarInstance = null;
/**
* Génère le titre d'un événement en utilisant les données enrichies
* @param {Object} event - Données de l'événement
* @returns {string} Titre de l'événement
*/
function getEventTitle(event) {
let title = '';
// Utiliser le nom du bénéficiaire si disponible
if (event.beneficiaire && event.beneficiaire.nom_complet) {
title = event.beneficiaire.nom_complet;
} else if (event.type === 'groupe') {
title = 'Groupe';
} else {
title = event.type || 'Sans type';
}
// Ajouter le type d'intervention principal si disponible
if (event.intervenant && event.intervenant.types_intervention_noms && event.intervenant.types_intervention_noms.length > 0) {
const primaryType = event.intervenant.types_intervention_noms[0]; // Premier type d'intervention
title += ` - ${primaryType}`;
}
// Ajouter le département si disponible (optionnel - décommenter si souhaité)
// if (event.intervenant && event.intervenant.departements_noms && event.intervenant.departements_noms.length > 0) {
// const departements = event.intervenant.departements_noms.join(', ');
// title += ` [${departements}]`;
// }
// Ajouter le commentaire si disponible
if (event.commentaire) {
title += ' - ' + event.commentaire;
}
return title;
}
/**
* Initialise les filtres dynamiques
* @param {Object} calendar - Instance FullCalendar
@ -67,37 +31,7 @@ export function initializeFilters(calendar) {
}
const apiEvents = await getEvents(params);
const events = apiEvents.map(ev => ({
id: ev.id,
title: getEventTitle(ev),
start: ev.date_rdv + 'T' + ev.heure_rdv,
end: ev.date_fin + 'T' + ev.heure_fin,
extendedProps: {
type: ev.type,
commentaire: ev.commentaire,
date_rdv: ev.date_rdv,
heure_rdv: ev.heure_rdv,
date_fin: ev.date_fin,
heure_fin: ev.heure_fin,
// Garder les IDs pour compatibilité
id_beneficiaire: ev.id_beneficiaire,
id_intervenant: ev.id_intervenant,
id_traducteur: ev.id_traducteur,
id_local: ev.id_local,
langue: ev.langue,
// Nouvelles données enrichies
beneficiaire: ev.beneficiaire,
intervenant: ev.intervenant,
traducteur: ev.traducteur,
local: ev.local,
nb_participants: ev.nb_participants,
nb_hommes: ev.nb_hommes,
nb_femmes: ev.nb_femmes,
statut: ev.statut,
created_at: ev.created_at,
updated_at: ev.updated_at
}
}));
const events = apiEvents.map(ev => mapEventToFullCalendar(ev));
successCallback(events);
} catch (e) {
console.error('Erreur lors du chargement des événements:', e);
@ -215,10 +149,16 @@ function collectFilters() {
filters.intervenant = personne;
}
// Département
const departement = document.getElementById('departement');
if (departement && departement.value && departement.value.trim() !== '') {
filters.departement = departement.value;
}
// Type d'intervention
const typeIntervention = document.getElementById('type_intervention').value;
if (typeIntervention && typeIntervention.trim() !== '') {
filters.type_intervention = typeIntervention;
const typeIntervention = document.getElementById('type_intervention');
if (typeIntervention && typeIntervention.value && typeIntervention.value.trim() !== '') {
filters.type_intervention = typeIntervention.value;
}
// Bénéficiaire
@ -228,7 +168,7 @@ function collectFilters() {
}
// Langue
const langue = document.getElementById('langue').value;
const langue = document.getElementById('langue_filtre')?.value;
if (langue && langue.trim() !== '') {
filters.langue = langue;
}

View File

@ -3,6 +3,7 @@ import { openModal } from './agenda-modal.js';
import { getEvents, updateEvent, getEvent } from './agenda-api.js';
import { notifyError } from './agenda-notifications.js';
import { initializeFilters } from './agenda-filters.js';
import { mapEventToFullCalendar, getTextColor, getLuminance } from './agenda-event-mapper.js';
import toastr from 'toastr';
import { Calendar } from '@fullcalendar/core';
import dayGridPlugin from '@fullcalendar/daygrid';
@ -12,66 +13,6 @@ import interactionPlugin from '@fullcalendar/interaction';
import { apiFetch } from './agenda-api.js';
// Ajoutez d'autres plugins si besoin
/**
* Calcule la luminosité d'une couleur hexadécimale
* @param {string} hexColor - Couleur au format #RRGGBB
* @returns {number} - Luminosité entre 0 et 1
*/
function getLuminance(hexColor) {
// Vérifier que hexColor n'est pas null ou undefined
if (!hexColor || typeof hexColor !== 'string') {
console.warn('⚠️ [getLuminance] Valeur hexColor invalide:', hexColor);
return 0.5; // Retourner une valeur moyenne par défaut
}
// Convertir hex en RGB
const hex = hexColor.replace('#', '');
const r = parseInt(hex.substr(0, 2), 16);
const g = parseInt(hex.substr(2, 2), 16);
const b = parseInt(hex.substr(4, 2), 16);
// Calculer la luminosité relative selon WCAG
return (0.299 * r + 0.587 * g + 0.114 * b) / 255;
}
/**
* Détermine la couleur de texte optimale selon le contraste
* @param {string} backgroundColor - Couleur de fond au format #RRGGBB
* @returns {string} - Couleur de texte (#000000 ou #ffffff)
*/
function getTextColor(backgroundColor) {
// Vérifier que backgroundColor n'est pas null ou undefined
if (!backgroundColor || typeof backgroundColor !== 'string') {
console.warn('⚠️ [getTextColor] Valeur backgroundColor invalide:', backgroundColor);
return '#000000'; // Retourner noir par défaut
}
const luminance = getLuminance(backgroundColor);
return luminance > 0.5 ? '#000000' : '#ffffff';
}
/**
* Convertit une couleur hexadécimale en RGBA avec opacité
* @param {string} hex - Couleur au format #RRGGBB
* @param {number} alpha - Opacité entre 0 et 1
* @returns {string} - Couleur au format rgba(r, g, b, a)
*/
function hexToRgba(hex, alpha) {
// Vérifier que hex n'est pas null ou undefined
if (!hex || typeof hex !== 'string') {
console.warn('⚠️ [hexToRgba] Valeur hex invalide:', hex);
return `rgba(108, 117, 125, ${alpha || 1})`; // Retourner gris par défaut
}
const hexClean = hex.replace('#', '');
const r = parseInt(hexClean.substr(0, 2), 16);
const g = parseInt(hexClean.substr(2, 2), 16);
const b = parseInt(hexClean.substr(4, 2), 16);
const rgbaValue = `rgba(${r}, ${g}, ${b}, ${alpha || 1})`;
console.log('🔧 [hexToRgba] Conversion:', { hex, alpha, result: rgbaValue });
return rgbaValue;
}
export function initializeCalendar() {
console.log('🚀 Initialisation de FullCalendar...');
const calendarEl = document.getElementById('agenda-calendar');
@ -139,451 +80,7 @@ export function initializeCalendar() {
};
const apiEvents = await getEvents(params);
// Mapping des objets API vers le format FullCalendar
const events = apiEvents.map(ev => {
// Fonction utilitaire pour trouver la configuration d'un intervenant
function findIntervenantConfig(intervenant) {
if (!intervenant || !crviACFData || !crviACFData.intervenants) {
// console.log('❌ Données manquantes - intervenant:', !!intervenant, 'crviACFData:', !!crviACFData, 'intervenants:', !!crviACFData?.intervenants);
return null;
}
// console.log('Intervenants disponibles:', crviACFData.intervenants);
// console.log('Intervenant recherché:', intervenant);
// Vérifier que l'intervenant a un ID
if (!intervenant.id) {
// console.log('❌ ID d\'intervenant manquant:', intervenant);
return null;
}
const intervenantId = parseInt(intervenant.id);
if (isNaN(intervenantId)) {
// console.log('❌ ID d\'intervenant invalide:', intervenant.id);
return null;
}
// Chercher directement par ID
let intervenantConfig = null;
for (const key in crviACFData.intervenants) {
const config = crviACFData.intervenants[key];
// Vérifier que la config a un ID
if (!config || !config.id) {
// console.log('⚠️ Configuration invalide pour la clé:', key, 'config:', config);
continue;
}
// console.log('Comparaison - ID recherché:', intervenantId, 'ID config:', config.id, 'Type config.id:', typeof config.id);
if (config.id == intervenantId) {
intervenantConfig = config;
// console.log('✅ Configuration trouvée pour ID:', intervenantId);
break;
}
}
if (!intervenantConfig) {
// console.log('❌ Aucune configuration trouvée pour l\'ID:', intervenantId);
// console.log('IDs disponibles:', Object.values(crviACFData.intervenants).map(c => c.id));
}
return intervenantConfig;
}
// Hiérarchie de détermination des couleurs :
// 1) Pour RDV assignés (individuel/groupe) avec type d'intervention : couleur du type d'intervention
// 2) Pour RDV assignés (individuel/groupe) sans type d'intervention : orange (par défaut)
// 3) Pour permanences : couleur selon le type (assignée, non attribuée, non disponible)
let backgroundColor = null;
let textColor = null;
// Vérifier si l'événement est assigné (a un intervenant et un local/beneficiaire)
const isEventAssigned = ev.id_intervenant && (ev.id_local || ev.id_beneficiaire);
// Pour les RDV (individuel/groupe) assignés
if (ev.type && ev.type !== 'permanence' && isEventAssigned) {
// Vérifier si l'événement a un type d'intervention défini
let typeInterventionId = null;
if (ev.id_type_intervention) {
typeInterventionId = parseInt(ev.id_type_intervention);
} else if (ev.type_intervention && ev.type_intervention.id) {
typeInterventionId = parseInt(ev.type_intervention.id);
}
// Si l'événement a un type d'intervention, utiliser sa couleur depuis crviAjax
if (typeInterventionId && !isNaN(typeInterventionId) && window.crviAjax && window.crviAjax.couleurs_types_intervention) {
const couleurTypeIntervention = window.crviAjax.couleurs_types_intervention[typeInterventionId];
if (couleurTypeIntervention) {
backgroundColor = couleurTypeIntervention;
textColor = getTextColor(couleurTypeIntervention);
console.log('🎨 [COULEUR] RDV assigné avec type d\'intervention:', {
eventId: ev.id,
type: ev.type,
typeInterventionId: typeInterventionId,
backgroundColor: backgroundColor,
textColor: textColor,
source: 'type_intervention'
});
}
}
// Si pas de couleur définie (pas de type d'intervention), garder orange par défaut
if (!backgroundColor) {
backgroundColor = '#ff9800'; // Orange pour les événements assignés sans type d'intervention
textColor = getTextColor(backgroundColor);
console.log('🎨 [COULEUR] RDV assigné sans type d\'intervention (défaut orange):', {
eventId: ev.id,
type: ev.type,
backgroundColor: backgroundColor,
textColor: textColor,
source: 'defaut_assigné'
});
}
}
// Pour les RDV non assignés : couleur selon type_de_local (bureau ou salle)
if (!backgroundColor && ev.type && ev.type !== 'permanence' && !isEventAssigned) {
// Vérifier si l'événement a un local avec un type_de_local
if (ev.local && ev.local.type_de_local) {
const typeLocal = ev.local.type_de_local.toLowerCase();
if (crviACFData && crviACFData.couleurs_rdv) {
const couleurRdv = crviACFData.couleurs_rdv[typeLocal];
if (couleurRdv) {
backgroundColor = couleurRdv;
textColor = getTextColor(couleurRdv);
console.log('🎨 [COULEUR] RDV non assigné selon type de local:', {
eventId: ev.id,
type: ev.type,
typeLocal: typeLocal,
backgroundColor: backgroundColor,
textColor: textColor,
source: 'type_local'
});
}
}
}
}
// Pour permanences (événements de type 'permanence') : logique simplifiée
// 1) Non attribuée : dispo ? couleur dispo : couleur indispo
// 2) Attribuée : a type intervention ? couleur type : couleur défaut
let isPermanenceDisabled = false;
if (!backgroundColor && ev.type === 'permanence') {
// Une permanence est assignée si elle a un bénéficiaire ET un local (assign = 1)
const isPermanenceAssigned = ev.assign === 1 || (ev.id_beneficiaire && ev.id_local);
// Une permanence est non attribuée si elle n'est pas assignée (assign = 0)
const isPermanenceNonAttribuee = ev.assign === 0 && !ev.id_beneficiaire && !ev.id_local;
// Vérifier si le bénéficiaire est en congé (indisponibilitee_ponctuelle)
let isPermanenceNonDisponible = false;
if (ev.beneficiaire && ev.beneficiaire.indisponibilitee_ponctuelle && Array.isArray(ev.beneficiaire.indisponibilitee_ponctuelle)) {
const eventDate = ev.date_rdv; // Format YYYY-MM-DD
const eventDateObj = new Date(eventDate + 'T00:00:00');
// Vérifier si la date de l'événement est dans une période d'indisponibilité
for (const indispo of ev.beneficiaire.indisponibilitee_ponctuelle) {
if (indispo.debut && indispo.fin) {
let debutDate, finDate;
// Gérer le format d/m/Y (format ACF)
if (typeof indispo.debut === 'string' && indispo.debut.includes('/')) {
const debutParts = indispo.debut.split('/');
const finParts = indispo.fin.split('/');
if (debutParts.length === 3 && finParts.length === 3) {
// Format d/m/Y
debutDate = new Date(parseInt(debutParts[2]), parseInt(debutParts[1]) - 1, parseInt(debutParts[0]));
finDate = new Date(parseInt(finParts[2]), parseInt(finParts[1]) - 1, parseInt(finParts[0]));
}
} else {
// Format YYYY-MM-DD ou timestamp
debutDate = new Date(indispo.debut);
finDate = new Date(indispo.fin);
}
if (debutDate && finDate && !isNaN(debutDate.getTime()) && !isNaN(finDate.getTime())) {
// Ajuster pour inclure toute la journée
debutDate.setHours(0, 0, 0, 0);
finDate.setHours(23, 59, 59, 999);
if (eventDateObj >= debutDate && eventDateObj <= finDate) {
isPermanenceNonDisponible = true;
isPermanenceDisabled = true;
break;
}
}
}
}
}
// Vérifier si l'intervenant est indisponible (congés ou jour non disponible)
if (!isPermanenceNonDisponible && ev.id_intervenant && window.crviACFData && window.crviACFData.indisponibilites_intervenants) {
const intervenantId = parseInt(ev.id_intervenant);
const intervenantDispo = window.crviACFData.indisponibilites_intervenants[intervenantId];
if (intervenantDispo) {
const eventDate = ev.date_rdv; // Format YYYY-MM-DD
const eventDateObj = new Date(eventDate + 'T00:00:00');
// Vérifier les jours de disponibilité (0 = dimanche, 1 = lundi, etc.)
const dayOfWeek = eventDateObj.getDay();
const dayNames = ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'];
const dayName = dayNames[dayOfWeek];
// Si l'intervenant a des jours de disponibilité définis et que ce jour n'en fait pas partie
if (intervenantDispo.jours_dispo && Array.isArray(intervenantDispo.jours_dispo) &&
intervenantDispo.jours_dispo.length > 0 &&
!intervenantDispo.jours_dispo.includes(dayName)) {
console.log('🚫 [PERMANENCE] Intervenant non disponible ce jour:', {
intervenantId,
date: eventDate,
dayName,
joursDisponibles: intervenantDispo.jours_dispo
});
isPermanenceNonDisponible = true;
isPermanenceDisabled = true;
}
// Vérifier les congés/indisponibilités ponctuelles
if (!isPermanenceNonDisponible && intervenantDispo.conges && Array.isArray(intervenantDispo.conges)) {
for (const conge of intervenantDispo.conges) {
if (conge.debut && conge.fin) {
let debutDate, finDate;
// Gérer le format d/m/Y (format ACF)
if (typeof conge.debut === 'string' && conge.debut.includes('/')) {
const debutParts = conge.debut.split('/');
const finParts = conge.fin.split('/');
if (debutParts.length === 3 && finParts.length === 3) {
debutDate = new Date(parseInt(debutParts[2]), parseInt(debutParts[1]) - 1, parseInt(debutParts[0]));
finDate = new Date(parseInt(finParts[2]), parseInt(finParts[1]) - 1, parseInt(finParts[0]));
}
} else {
// Format YYYY-MM-DD ou timestamp
debutDate = new Date(conge.debut);
finDate = new Date(conge.fin);
}
if (debutDate && finDate && !isNaN(debutDate.getTime()) && !isNaN(finDate.getTime())) {
debutDate.setHours(0, 0, 0, 0);
finDate.setHours(23, 59, 59, 999);
if (eventDateObj >= debutDate && eventDateObj <= finDate) {
console.log('🚫 [PERMANENCE] Intervenant en congé:', {
intervenantId,
date: eventDate,
congeDebut: conge.debut,
congeFin: conge.fin,
congeType: conge.type
});
isPermanenceNonDisponible = true;
isPermanenceDisabled = true;
break;
}
}
}
}
}
}
}
// Vérifier aussi le statut si pas déjà déterminé
if (!isPermanenceNonDisponible && ev.statut && ['annule', 'non_tenu', 'absence'].includes(ev.statut)) {
isPermanenceNonDisponible = true;
}
// LOGIQUE SIMPLIFIÉE
if (isPermanenceNonAttribuee) {
// 1) Non attribuée : dispo ? couleur dispo : couleur indispo
if (isPermanenceNonDisponible && window.crviAjax && window.crviAjax.couleur_permanence_non_disponible) {
backgroundColor = window.crviAjax.couleur_permanence_non_disponible;
console.log('🎨 [COULEUR] Permanence non attribuée - indisponible:', {
eventId: ev.id,
backgroundColor: backgroundColor,
source: 'permanence_non_attribuee_indispo'
});
} else if (window.crviAjax && window.crviAjax.couleur_permanence_non_attribuee) {
backgroundColor = window.crviAjax.couleur_permanence_non_attribuee;
console.log('🎨 [COULEUR] Permanence non attribuée - disponible:', {
eventId: ev.id,
backgroundColor: backgroundColor,
source: 'permanence_non_attribuee_dispo'
});
}
} else if (isPermanenceAssigned) {
// 2) Attribuée : a type intervention ? couleur type : couleur défaut
let typeInterventionId = null;
if (ev.id_type_intervention) {
typeInterventionId = parseInt(ev.id_type_intervention);
} else if (ev.type_intervention && ev.type_intervention.id) {
typeInterventionId = parseInt(ev.type_intervention.id);
}
if (typeInterventionId && !isNaN(typeInterventionId) && window.crviAjax && window.crviAjax.couleurs_types_intervention) {
const couleurTypeIntervention = window.crviAjax.couleurs_types_intervention[typeInterventionId];
if (couleurTypeIntervention) {
backgroundColor = couleurTypeIntervention;
console.log('🎨 [COULEUR] Permanence assignée - avec type intervention:', {
eventId: ev.id,
typeInterventionId: typeInterventionId,
backgroundColor: backgroundColor,
source: 'permanence_assignee_type'
});
}
}
// Si pas de type d'intervention, utiliser couleur défaut
if (!backgroundColor) {
backgroundColor = (crviACFData && crviACFData.couleurs_permanence && crviACFData.couleurs_permanence.permanence)
? crviACFData.couleurs_permanence.permanence
: '#9e9e9e';
console.log('🎨 [COULEUR] Permanence assignée - défaut:', {
eventId: ev.id,
backgroundColor: backgroundColor,
source: 'permanence_assignee_defaut'
});
}
}
// S'assurer que backgroundColor n'est pas null avant d'appeler getTextColor
if (backgroundColor) {
textColor = getTextColor(backgroundColor);
} else {
textColor = '#000000'; // Par défaut
}
}
// Couleur par défaut si aucune condition n'est remplie
if (!backgroundColor) {
backgroundColor = '#6c757d'; // Gris par défaut
textColor = getTextColor(backgroundColor);
console.log('🎨 [COULEUR] Couleur par défaut (gris):', {
eventId: ev.id,
type: ev.type,
backgroundColor: backgroundColor,
textColor: textColor,
source: 'defaut_general'
});
}
// Log final de la couleur appliquée
console.log('🎨 [COULEUR FINALE] Événement:', {
eventId: ev.id,
title: ev.type || 'Sans type',
backgroundColor: backgroundColor,
borderColor: backgroundColor,
textColor: textColor,
isAssigned: isEventAssigned,
type: ev.type
});
// Générer un titre plus explicite
let title = ev.type || 'Sans type';
// Gestion spéciale pour les permanences
if (ev.type === 'permanence') {
if (ev.intervenant) {
const intervenant = ev.intervenant;
const nomComplet = `${intervenant.prenom || ''} ${intervenant.nom || ''}`.trim();
title = 'p. ' + (nomComplet || 'Intervenant');
} else {
title = 'p. Intervenant';
}
} else {
// Utiliser le nom de l'intervenant comme titre principal pour les autres événements
if (ev.intervenant) {
const intervenant = ev.intervenant;
const nomComplet = `${intervenant.prenom || ''} ${intervenant.nom || ''}`.trim();
if (nomComplet) {
title = nomComplet;
}
}
// Ajouter le type d'intervention principal si disponible
if (ev.intervenant && ev.intervenant.types_intervention_noms && ev.intervenant.types_intervention_noms.length > 0) {
const primaryType = ev.intervenant.types_intervention_noms[0]; // Premier type d'intervention
title += ` - ${primaryType}`;
}
// Ajouter le commentaire si disponible
if (ev.commentaire) {
title += ' - ' + ev.commentaire;
}
}
// S'assurer que les couleurs sont toujours définies
if (!backgroundColor || typeof backgroundColor !== 'string') {
console.warn('⚠️ [Mapping] backgroundColor invalide pour l\'événement:', ev.id, backgroundColor);
backgroundColor = '#6c757d'; // Gris par défaut
}
if (!textColor || typeof textColor !== 'string') {
textColor = getTextColor(backgroundColor);
}
// Vérifier si l'événement est passé
const eventStartDate = new Date(ev.date_rdv + 'T' + ev.heure_rdv);
const now = new Date();
const isEventPast = eventStartDate < now;
// Un événement est éditable seulement si :
// 1. Il n'est pas désactivé (permanence)
// 2. Il n'est pas passé
const isEditable = !isPermanenceDisabled && !isEventPast;
return {
id: ev.id,
title: title,
start: ev.date_rdv + 'T' + ev.heure_rdv,
end: ev.date_fin + 'T' + ev.heure_fin,
backgroundColor: backgroundColor,
borderColor: backgroundColor,
textColor: textColor,
editable: isEditable, // Désactiver l'édition si la permanence est désactivée OU si l'événement est passé
durationEditable: isEditable, // Désactiver le redimensionnement si la permanence est désactivée OU si l'événement est passé
startEditable: isEditable, // Désactiver le déplacement si la permanence est désactivée OU si l'événement est passé
classNames: isPermanenceDisabled ? ['permanence-disabled'] : (isEventPast ? ['event-past'] : []),
extendedProps: {
// Données de base
type: ev.type,
commentaire: ev.commentaire,
date_rdv: ev.date_rdv,
heure_rdv: ev.heure_rdv,
date_fin: ev.date_fin,
heure_fin: ev.heure_fin,
// Relations avec les entités
id_beneficiaire: ev.id_beneficiaire,
id_intervenant: ev.id_intervenant,
id_traducteur: ev.id_traducteur,
id_local: ev.id_local,
id_departement: ev.id_departement,
id_type_intervention: ev.id_type_intervention || (ev.type_intervention && ev.type_intervention.id ? ev.type_intervention.id : null),
langue: ev.langue,
langues_disponibles: ev.langues_disponibles || null,
// Données des entités (si disponibles)
beneficiaire: ev.beneficiaire,
intervenant: ev.intervenant,
traducteur: ev.traducteur,
local: ev.local,
// Données spécifiques aux groupes
nb_participants: ev.nb_participants,
nb_hommes: ev.nb_hommes,
nb_femmes: ev.nb_femmes,
// Autres données
statut: ev.statut,
assign: ev.assign || 0,
isDisabled: isPermanenceDisabled,
created_at: ev.created_at,
updated_at: ev.updated_at
}
};
});
const events = apiEvents.map(ev => mapEventToFullCalendar(ev));
successCallback(events);
} catch (e) {
console.error('Erreur lors du chargement des événements:', e);
@ -1061,16 +558,97 @@ export function initializeCalendar() {
bgColor = window.crviAjax.couleur_permanence_non_attribuee;
}
} else if (isPermanenceAssigned) {
// 2) Attribuée : a type intervention ? couleur type : couleur défaut
const typeInterventionId = eventProps.id_type_intervention ? parseInt(eventProps.id_type_intervention) : null;
// 2) Attribuée : ordre de priorité - département puis local
if (typeInterventionId && !isNaN(typeInterventionId) && window.crviAjax && window.crviAjax.couleurs_types_intervention) {
const couleurType = window.crviAjax.couleurs_types_intervention[typeInterventionId];
if (couleurType) {
bgColor = couleurType;
// Priorité 1 : Couleur du département
const departementId = eventProps.id_departement ? parseInt(eventProps.id_departement) : null;
// 🔍 DEBUG pour événement 410
if (event.id === '410') {
console.log('🔍 [DEBUG 410] Début analyse couleur département:', {
eventId: event.id,
id_departement_brut: eventProps.id_departement,
departementId_parsed: departementId,
crviACFData_existe: !!crviACFData,
departements_existe: !!(crviACFData && crviACFData.departements),
departements_data: crviACFData ? crviACFData.departements : 'N/A'
});
}
if (departementId && !isNaN(departementId) && crviACFData && crviACFData.departements) {
// 🔍 DEBUG pour événement 410 - liste des départements
if (event.id === '410') {
console.log('🔍 [DEBUG 410] Recherche département ID:', departementId);
console.log('🔍 [DEBUG 410] Départements disponibles:',
Object.keys(crviACFData.departements).map(key => ({
key: key,
id: crviACFData.departements[key].id,
nom: crviACFData.departements[key].nom,
couleur: crviACFData.departements[key].couleur
}))
);
}
// Chercher le département par ID
for (const key in crviACFData.departements) {
const dept = crviACFData.departements[key];
// 🔍 DEBUG pour événement 410 - comparaison
if (event.id === '410') {
console.log('🔍 [DEBUG 410] Comparaison:', {
key: key,
dept_id: dept.id,
dept_id_type: typeof dept.id,
recherche_id: departementId,
recherche_id_type: typeof departementId,
sont_egaux: dept.id === departementId,
dept_couleur: dept.couleur
});
}
if (dept.id === departementId && dept.couleur) {
bgColor = dept.couleur;
console.log('🎨 [COULEUR] Département trouvé:', {
eventId: event.id,
departementId: departementId,
departementNom: dept.nom,
couleur: bgColor
});
// 🔍 DEBUG pour événement 410 - succès
if (event.id === '410') {
console.log('✅ [DEBUG 410] Couleur département appliquée:', {
departementNom: dept.nom,
couleurAppliquee: bgColor
});
}
break;
}
}
// 🔍 DEBUG pour événement 410 - échec de recherche
if (event.id === '410' && !bgColor) {
console.warn('⚠️ [DEBUG 410] Aucun département correspondant trouvé!');
}
}
// Priorité 2 : Couleur du local (type de local)
if (!bgColor && eventProps.local) {
const localType = eventProps.local.type || eventProps.local_type;
if (localType && crviACFData && crviACFData.types_local) {
const typeLocalConfig = crviACFData.types_local[localType];
if (typeLocalConfig && typeLocalConfig.couleur) {
bgColor = typeLocalConfig.couleur;
console.log('🎨 [COULEUR] Type de local trouvé:', {
eventId: event.id,
localType: localType,
couleur: bgColor
});
}
}
}
// Fallback : couleur par défaut des permanences
if (!bgColor) {
bgColor = (crviACFData && crviACFData.couleurs_permanence && crviACFData.couleurs_permanence.permanence)
? crviACFData.couleurs_permanence.permanence
@ -1078,16 +656,115 @@ export function initializeCalendar() {
}
}
} else {
// Pour les RDV : type d'intervention ou orange par défaut
const typeInterventionId = eventProps.id_type_intervention ? parseInt(eventProps.id_type_intervention) : null;
// Pour les RDV : ordre de priorité - département > type d'intervention > local > défaut
if (typeInterventionId && !isNaN(typeInterventionId) && window.crviAjax && window.crviAjax.couleurs_types_intervention) {
const couleurType = window.crviAjax.couleurs_types_intervention[typeInterventionId];
if (couleurType) {
bgColor = couleurType;
// Priorité 1 : Couleur du département
const departementId = eventProps.id_departement ? parseInt(eventProps.id_departement) : null;
// 🔍 DEBUG pour événement 410
if (event.id === '410') {
console.log('🔍 [DEBUG 410 - RDV] Début analyse couleur département:', {
eventId: event.id,
type: eventProps.type,
id_departement_brut: eventProps.id_departement,
departementId_parsed: departementId,
crviACFData_existe: !!crviACFData,
departements_existe: !!(crviACFData && crviACFData.departements)
});
}
if (departementId && !isNaN(departementId) && crviACFData && crviACFData.departements) {
// 🔍 DEBUG pour événement 410 - liste des départements
if (event.id === '410') {
console.log('🔍 [DEBUG 410 - RDV] Recherche département ID:', departementId);
console.log('🔍 [DEBUG 410 - RDV] Départements disponibles:',
Object.keys(crviACFData.departements).map(key => ({
key: key,
id: crviACFData.departements[key].id,
nom: crviACFData.departements[key].nom,
couleur: crviACFData.departements[key].couleur
}))
);
}
// Chercher le département par ID
for (const key in crviACFData.departements) {
const dept = crviACFData.departements[key];
// 🔍 DEBUG pour événement 410 - comparaison
if (event.id === '410') {
console.log('🔍 [DEBUG 410 - RDV] Comparaison:', {
key: key,
dept_id: dept.id,
dept_id_type: typeof dept.id,
recherche_id: departementId,
recherche_id_type: typeof departementId,
sont_egaux: dept.id === departementId,
dept_couleur: dept.couleur
});
}
if (dept.id === departementId && dept.couleur) {
bgColor = dept.couleur;
console.log('🎨 [COULEUR] RDV - département trouvé:', {
eventId: event.id,
type: eventProps.type,
departementId: departementId,
departementNom: dept.nom,
couleur: bgColor
});
// 🔍 DEBUG pour événement 410 - succès
if (event.id === '410') {
console.log('✅ [DEBUG 410 - RDV] Couleur département appliquée:', {
departementNom: dept.nom,
couleurAppliquee: bgColor
});
}
break;
}
}
// 🔍 DEBUG pour événement 410 - échec de recherche
if (event.id === '410' && !bgColor) {
console.warn('⚠️ [DEBUG 410 - RDV] Aucun département correspondant trouvé!');
}
}
// Priorité 2 : Type d'intervention (si pas de département)
if (!bgColor) {
const typeInterventionId = eventProps.id_type_intervention ? parseInt(eventProps.id_type_intervention) : null;
if (typeInterventionId && !isNaN(typeInterventionId) && window.crviAjax && window.crviAjax.couleurs_types_intervention) {
const couleurType = window.crviAjax.couleurs_types_intervention[typeInterventionId];
if (couleurType) {
bgColor = couleurType;
console.log('🎨 [COULEUR] RDV - type intervention trouvé:', {
eventId: event.id,
typeInterventionId: typeInterventionId,
couleur: bgColor
});
}
}
}
// Priorité 3 : Couleur du local (type de local)
if (!bgColor && eventProps.local) {
const localType = eventProps.local.type || eventProps.local_type;
if (localType && crviACFData && crviACFData.types_local) {
const typeLocalConfig = crviACFData.types_local[localType];
if (typeLocalConfig && typeLocalConfig.couleur) {
bgColor = typeLocalConfig.couleur;
console.log('🎨 [COULEUR] RDV - type de local trouvé:', {
eventId: event.id,
localType: localType,
couleur: bgColor
});
}
}
}
// Fallback : orange par défaut
if (!bgColor) {
bgColor = '#ff9800'; // Orange par défaut
}
@ -1105,6 +782,16 @@ export function initializeCalendar() {
// Déterminer la couleur
let { bgColor, txtColor } = determineEventColor();
// 🔍 DEBUG FINAL pour événement 410
if (event.id === '410') {
console.log('🔍 [DEBUG 410] COULEUR FINALE après determineEventColor:', {
eventId: event.id,
bgColor: bgColor,
txtColor: txtColor,
eventProps_complet: eventProps
});
}
// Vérifier que les couleurs sont valides et utiliser des valeurs par défaut si nécessaire
if (!bgColor || !txtColor) {
console.warn('⚠️ [eventDidMount] Couleurs invalides détectées:', { bgColor, txtColor, eventId: event.id });
@ -1120,6 +807,15 @@ export function initializeCalendar() {
textColor: txtColor
});
// 🔍 DEBUG FINAL pour événement 410 - après validation
if (event.id === '410') {
console.log('🔍 [DEBUG 410] COULEUR APPLIQUÉE (après validation):', {
eventId: event.id,
bgColor_final: bgColor,
txtColor_final: txtColor
});
}
// Fonction pour appliquer les styles
function applyEventStyles() {
try {
@ -1188,7 +884,9 @@ export function initializeCalendar() {
// Déterminer la couleur pour le popover (utiliser la même logique que pour l'événement)
const popoverColor = bgColor || '#6c757d';
const popoverTextColor = txtColor || '#ffffff';
// Recalculer la couleur du texte en fonction de la luminosité du fond du popover
// pour éviter un texte blanc sur fond blanc pour les permanences non attribuées
const popoverTextColor = getTextColor(popoverColor);
console.log('🎨 [POPOVER] Couleurs déterminées:', {
eventId: event.id,
@ -1228,6 +926,47 @@ export function initializeCalendar() {
return statutConfig ? statutConfig.couleur : '#6c757d';
}
// Fonction pour obtenir le statut et sa couleur avec gestion des cas spéciaux
function getStatutDisplay(statut, eventType) {
let displayText = statut;
let displayColor = '#6c757d'; // Gris par défaut
// Cas spécial : permanence non attribuée
if (!statut && eventType === 'permanence') {
displayText = 'Non attribué';
displayColor = '#6c757d'; // Gris
return { text: displayText, color: displayColor };
}
// Si pas de statut, retourner une valeur par défaut
if (!statut) {
displayText = 'Non défini';
return { text: displayText, color: displayColor };
}
// Normaliser le statut (enlever accents et mettre en minuscules pour comparaison)
const statutLower = statut.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
// Déterminer la couleur selon le statut
if (statutLower.includes('absent')) {
displayColor = '#ffb3ba'; // Rouge pastel
} else if (statutLower.includes('present')) {
displayColor = '#90ee90'; // Vert clair
} else if (statutLower.includes('prevu')) {
displayColor = '#add8e6'; // Bleu clair
} else {
// Essayer de récupérer depuis les données ACF
if (crviACFData && crviACFData.statuts) {
const statutConfig = crviACFData.statuts[statut];
if (statutConfig && statutConfig.couleur) {
displayColor = statutConfig.couleur;
}
}
}
return { text: displayText, color: displayColor };
}
// Fonction utilitaire pour extraire le nom complet d'une entité
function getEntityDisplayName(entity, entityType = '') {
if (!entity) return '';
@ -1260,6 +999,9 @@ export function initializeCalendar() {
console.log('eventProps:', eventProps);
// Obtenir le statut formaté avec sa couleur
const statutDisplay = getStatutDisplay(eventProps.statut, eventProps.type);
// Créer le contenu du popover avec titre coloré
const popoverContent = `
<div class="event-popover">
@ -1272,7 +1014,7 @@ export function initializeCalendar() {
${eventProps.intervenant ? `<div class="mb-1"><strong>Intervenant:</strong> ${getEntityDisplayName(eventProps.intervenant, 'intervenant')}</div>` : ''}
${eventProps.local ? `<div class="mb-1"><strong>Local:</strong> ${getEntityDisplayName(eventProps.local, 'local')}</div>` : ''}
${eventProps.commentaire ? `<div class="mb-1"><strong>Commentaire:</strong> ${eventProps.commentaire}</div>` : ''}
<div class="mb-1"><strong>Statut:</strong> <span class="event-status" style="background-color: ${getStatutColor(eventProps.statut)};">${eventProps.statut}</span></div>
<div class="mb-1"><strong>Statut:</strong> <span class="event-status" style="background-color: ${statutDisplay.color}; color: ${getTextColor(statutDisplay.color)}; padding: 2px 8px; border-radius: 3px;">${statutDisplay.text}</span></div>
<div class="mt-2">
<small class="text-muted">Cliquez pour plus de détails</small>
</div>
@ -1306,10 +1048,25 @@ export function initializeCalendar() {
});
}
// Appliquer aussi les styles au body du popover si nécessaire
// S'assurer que le body du popover a une couleur de texte lisible
const bodyElement = popoverElement.querySelector('.popover-body');
if (bodyElement && txtColor) {
bodyElement.style.color = txtColor;
if (bodyElement) {
// Le body du popover a un fond blanc, donc le texte doit toujours être sombre
bodyElement.style.color = '#000000';
}
// Appliquer les styles du statut
const statutElement = popoverElement.querySelector('.event-status');
if (statutElement) {
const statutDisplay = getStatutDisplay(eventProps.statut, eventProps.type);
statutElement.style.backgroundColor = statutDisplay.color;
statutElement.style.color = getTextColor(statutDisplay.color);
statutElement.style.padding = '2px 8px';
statutElement.style.borderRadius = '3px';
console.log('🎨 [POPOVER] Styles de statut appliqués:', {
statut: statutDisplay.text,
backgroundColor: statutDisplay.color
});
}
}
});
@ -1329,6 +1086,27 @@ export function initializeCalendar() {
color: popoverTextColor
});
}
// S'assurer que le body du popover a une couleur de texte lisible
const bodyElement = node.querySelector('.popover-body');
if (bodyElement) {
// Le body du popover a un fond blanc, donc le texte doit toujours être sombre
bodyElement.style.color = '#000000';
}
// Appliquer aussi les styles du statut
const statutElement = node.querySelector('.event-status');
if (statutElement) {
const statutDisplay = getStatutDisplay(eventProps.statut, eventProps.type);
statutElement.style.backgroundColor = statutDisplay.color;
statutElement.style.color = getTextColor(statutDisplay.color);
statutElement.style.padding = '2px 8px';
statutElement.style.borderRadius = '3px';
console.log('🎨 [POPOVER] Styles de statut appliqués via observer:', {
statut: statutDisplay.text,
backgroundColor: statutDisplay.color
});
}
}
});
});

View File

@ -590,10 +590,13 @@ function collectIntervenantFilters() {
const typeRdv = document.getElementById('type_rdv')?.value;
if (typeRdv) filters.type_rdv = typeRdv;
const departement = document.getElementById('departement')?.value;
if (departement) filters.departement = parseInt(departement);
const typeIntervention = document.getElementById('type_intervention')?.value;
if (typeIntervention) filters.type_intervention = parseInt(typeIntervention);
const langue = document.getElementById('langue')?.value;
const langue = document.getElementById('langue_filtre')?.value;
if (langue) filters.langue = langue;
const permanences = document.getElementById('permanences_non_assignees')?.checked;
@ -617,6 +620,9 @@ function collectColleaguesFilters() {
const typeRdv = document.getElementById('type_rdv-colleagues')?.value;
if (typeRdv) filters.type_rdv = typeRdv;
const departement = document.getElementById('departement-colleagues')?.value;
if (departement) filters.departement = parseInt(departement);
const typeIntervention = document.getElementById('type_intervention-colleagues')?.value;
if (typeIntervention) filters.type_intervention = parseInt(typeIntervention);
@ -758,14 +764,37 @@ async function loadFilterOptions(mode = 'intervenant') {
});
}
// Types d'intervention
const typeInterventionSelect = document.getElementById(`type_intervention${prefix}`);
if (typeInterventionSelect && disponibilites.types_intervention) {
disponibilites.types_intervention.forEach(type => {
// Départements
const departementSelect = document.getElementById(`departement${prefix}`);
if (departementSelect && disponibilites.departements) {
disponibilites.departements.forEach(dept => {
const option = document.createElement('option');
option.value = type.id;
option.textContent = type.nom;
typeInterventionSelect.appendChild(option);
option.value = dept.id;
option.textContent = dept.nom;
departementSelect.appendChild(option);
});
}
// Types d'intervention groupés par département
const typeInterventionSelect = document.getElementById(`type_intervention${prefix}`);
if (typeInterventionSelect && disponibilites.types_intervention_groupes) {
// Parcourir les départements et créer des optgroup
Object.keys(disponibilites.types_intervention_groupes).forEach(departementId => {
const departementData = disponibilites.types_intervention_groupes[departementId];
const optgroup = document.createElement('optgroup');
optgroup.label = departementData.nom;
// Ajouter les types d'intervention de ce département
if (departementData.types && Array.isArray(departementData.types)) {
departementData.types.forEach(type => {
const option = document.createElement('option');
option.value = type.id;
option.textContent = type.nom;
optgroup.appendChild(option);
});
}
typeInterventionSelect.appendChild(optgroup);
});
}

View File

@ -0,0 +1,784 @@
// Module de gestion des boutons de la modale
// Contient les gestionnaires d'événements pour tous les boutons de la modale
import { deleteEvent, changeEventStatus, apiFetch } from './agenda-api.js';
import { notifyError, notifySuccess } from './agenda-notifications.js';
import { populateSelects } from './agenda-modal-select.js';
import { fillFormWithEvent, clearFormErrors, handleEventFormSubmit } from './agenda-modal-forms.js';
import { updateModalDisplay, fillViewBlock } from './agenda-modal-display.js';
/**
* Helper pour gérer l'overlay de chargement
* @param {Function} asyncAction - Action asynchrone à exécuter
* @returns {Promise<void>}
*/
async function withLoadingOverlay(asyncAction) {
const overlayTarget = document.querySelector('#eventModal .modal-content') || document.getElementById('eventModal');
try {
if (window.CRVI_OVERLAY && overlayTarget) {
window.CRVI_OVERLAY.show(overlayTarget);
}
await asyncAction();
} finally {
if (window.CRVI_OVERLAY && overlayTarget) {
window.CRVI_OVERLAY.hide(overlayTarget);
}
}
}
/**
* Helper pour rafraîchir les calendriers
*/
function refreshCalendars() {
if (window.currentCalendar) {
window.currentCalendar.refetchEvents();
}
if (window.currentColleaguesCalendar) {
window.currentColleaguesCalendar.refetchEvents();
}
}
/**
* Helper pour fermer le modal
* @param {Function} onClosed - Callback après fermeture
*/
function closeModal(onClosed = null) {
const modal = document.getElementById('eventModal');
if (modal) {
const bsModal = bootstrap.Modal.getInstance(modal);
if (bsModal) {
if (onClosed) {
modal.addEventListener('hidden.bs.modal', onClosed, { once: true });
}
bsModal.hide();
}
}
}
/**
* Vérifie les permissions utilisateur
* @param {string} permission - Type de permission ('can_edit', 'can_delete', 'can_create')
* @returns {boolean}
*/
function checkPermission(permission) {
const hasPermission = window.crviPermissions && window.crviPermissions[permission];
if (!hasPermission) {
console.warn(`Utilisateur non autorisé: ${permission}`);
}
return hasPermission;
}
/**
* Initialise le bouton de fermeture
* @param {string} buttonId - ID du bouton
*/
export function initializeCloseButton(buttonId) {
const button = document.getElementById(buttonId);
if (button) {
button.onclick = () => closeModal();
}
}
/**
* Initialise le bouton de suppression
* @param {Function} getCurrentEventData - Fonction pour obtenir les données de l'événement
* @param {Function} onDeleted - Callback après suppression
*/
export function initializeDeleteButton(getCurrentEventData, onDeleted = null) {
const deleteBtn = document.getElementById('deleteEvent');
if (!deleteBtn) return;
deleteBtn.onclick = async function() {
if (!checkPermission('can_delete')) return;
if (!confirm('Êtes-vous sûr de vouloir supprimer cet événement ?')) {
return;
}
const currentEventData = getCurrentEventData();
const eventId = currentEventData ? currentEventData.id : null;
if (!eventId) {
notifyError('ID d\'événement manquant');
return;
}
await withLoadingOverlay(async () => {
try {
await deleteEvent(eventId);
refreshCalendars();
closeModal(onDeleted);
} catch (error) {
console.error('Erreur lors de la suppression:', error);
notifyError('Erreur lors de la suppression de l\'événement');
}
});
};
}
/**
* Initialise le bouton de validation de présence
* @param {Function} getCurrentEventData - Fonction pour obtenir les données de l'événement
* @param {Function} openCheckPresenceModal - Fonction pour ouvrir la modal de validation de présence de groupe
* @param {Function} onStatusChanged - Callback après changement de statut
*/
export function initializeMarkPresentButton(getCurrentEventData, openCheckPresenceModal, onStatusChanged = null) {
const markPresentBtn = document.getElementById('markPresentBtn');
if (!markPresentBtn) return;
markPresentBtn.onclick = async function() {
if (!checkPermission('can_edit')) return;
const currentEventData = getCurrentEventData();
const eventId = currentEventData ? currentEventData.id : null;
if (!eventId) {
notifyError('ID d\'événement manquant');
return;
}
// Vérifier le type d'événement
const eventType = currentEventData?.type || currentEventData?.extendedProps?.type || '';
const isGroupe = eventType === 'groupe';
if (isGroupe) {
// Pour les événements de groupe, ouvrir la modal de validation des présences
openCheckPresenceModal(currentEventData);
} else {
// Pour les événements individuels
if (!confirm('Confirmer la présence à cet événement ?')) {
return;
}
await withLoadingOverlay(async () => {
try {
await changeEventStatus(eventId, 'present');
notifySuccess('Présence validée');
refreshCalendars();
closeModal(onStatusChanged);
} catch (error) {
console.error('Erreur lors du changement de statut:', error);
notifyError('Erreur lors de la validation de présence');
}
});
}
};
}
/**
* Initialise le bouton pour afficher le modal de gestion des présences (groupes uniquement)
* @param {Function} getCurrentEventData - Fonction pour obtenir les données de l'événement
* @param {Function} openCheckPresenceModal - Fonction pour ouvrir la modal de validation de présence de groupe
*/
export function initializeShowPresenceModalButton(getCurrentEventData, openCheckPresenceModal) {
const showPresenceModalBtn = document.getElementById('showPresenceModalBtn');
if (!showPresenceModalBtn) return;
showPresenceModalBtn.onclick = function() {
if (!checkPermission('can_edit')) return;
const currentEventData = getCurrentEventData();
if (!currentEventData) {
notifyError('Données d\'événement manquantes');
return;
}
// Ouvrir le modal de gestion des présences
openCheckPresenceModal(currentEventData);
};
}
/**
* Initialise le bouton pour marquer comme absent
* @param {Function} getCurrentEventData - Fonction pour obtenir les données de l'événement
* @param {Function} onStatusChanged - Callback après changement de statut
*/
export function initializeMarkAbsentButton(getCurrentEventData, onStatusChanged = null) {
const markAbsentBtn = document.getElementById('markAbsentBtn');
if (!markAbsentBtn) return;
markAbsentBtn.onclick = async function() {
if (!checkPermission('can_edit')) return;
const currentEventData = getCurrentEventData();
const eventId = currentEventData ? currentEventData.id : null;
if (!eventId) {
notifyError('ID d\'événement manquant');
return;
}
if (!confirm('Marquer cet événement comme absent ?')) {
return;
}
await withLoadingOverlay(async () => {
try {
await changeEventStatus(eventId, 'absence');
notifySuccess('Événement marqué comme absent');
refreshCalendars();
closeModal(onStatusChanged);
} catch (error) {
console.error('Erreur lors du changement de statut:', error);
notifyError('Erreur lors du changement de statut');
}
});
};
}
/**
* Initialise le bouton d'annulation de rendez-vous
* @param {Function} getCurrentEventData - Fonction pour obtenir les données de l'événement
* @param {Function} onCancelled - Callback après annulation
*/
export function initializeCancelAppointmentButton(getCurrentEventData, onCancelled = null) {
const cancelAppointmentBtn = document.getElementById('cancelAppointmentBtn');
if (!cancelAppointmentBtn) return;
cancelAppointmentBtn.onclick = async function() {
if (!checkPermission('can_edit')) return;
const currentEventData = getCurrentEventData();
const eventId = currentEventData ? currentEventData.id : null;
if (!eventId) {
notifyError('ID d\'événement manquant');
return;
}
const motif = prompt('Motif de l\'annulation (optionnel):');
if (motif === null) {
return; // L'utilisateur a cliqué sur Annuler
}
await withLoadingOverlay(async () => {
try {
await changeEventStatus(eventId, 'annule', motif);
notifySuccess('Rendez-vous annulé');
refreshCalendars();
closeModal(onCancelled);
} catch (error) {
console.error('Erreur lors de l\'annulation:', error);
notifyError('Erreur lors de l\'annulation du rendez-vous');
}
});
};
}
/**
* Initialise le bouton Edit
* @param {Function} getCurrentEventData - Fonction pour obtenir les données de l'événement
* @param {Function} getCurrentMode - Fonction pour obtenir le mode actuel
* @param {Function} setCurrentMode - Fonction pour changer le mode
* @param {Function} enableDateSynchronization - Fonction pour activer la synchronisation des dates
*/
export function initializeEditButton(getCurrentEventData, getCurrentMode, setCurrentMode, enableDateSynchronization) {
const editBtn = document.getElementById('editEventBtn');
if (!editBtn) return;
editBtn.onclick = async function() {
if (!checkPermission('can_edit')) return;
const currentEventData = getCurrentEventData();
// Changer le mode
setCurrentMode('edit');
// Mettre à jour l'affichage
const userCanEdit = window.crviPermissions && window.crviPermissions.can_edit;
const userCanDelete = window.crviPermissions && window.crviPermissions.can_delete;
updateModalDisplay('edit', userCanEdit, userCanDelete, currentEventData);
// Remplir le formulaire avec les données actuelles
fillFormWithEvent(currentEventData);
// Peupler les selects avec overlay de chargement
await withLoadingOverlay(async () => {
try {
await populateSelects(currentEventData);
} catch (error) {
console.error('Erreur lors du peuplement des selects:', error);
}
});
clearFormErrors();
// Activer la synchronisation des dates
if (enableDateSynchronization) {
enableDateSynchronization();
}
};
}
/**
* Initialise le bouton Cancel
* @param {Function} getCurrentEventData - Fonction pour obtenir les données de l'événement
* @param {Function} getCurrentMode - Fonction pour obtenir le mode actuel
* @param {Function} setCurrentMode - Fonction pour changer le mode
* @param {Function} disableDateSynchronization - Fonction pour désactiver la synchronisation des dates
*/
export function initializeCancelButton(getCurrentEventData, getCurrentMode, setCurrentMode, disableDateSynchronization) {
const cancelBtn = document.getElementById('cancelEditBtn');
if (!cancelBtn) return;
cancelBtn.onclick = function() {
const currentMode = getCurrentMode();
const currentEventData = getCurrentEventData();
if (currentMode === 'create') {
// En mode création, fermer le modal
closeModal();
} else if (currentMode === 'edit' && currentEventData) {
// En mode édition, retourner en mode vue
setCurrentMode('view');
const userCanEdit = window.crviPermissions && window.crviPermissions.can_edit;
const userCanDelete = window.crviPermissions && window.crviPermissions.can_delete;
updateModalDisplay('view', userCanEdit, userCanDelete, currentEventData);
// Réafficher les données en mode lecture
fillViewBlock(currentEventData);
clearFormErrors();
// Désactiver la synchronisation des dates
if (disableDateSynchronization) {
disableDateSynchronization();
}
}
};
}
/**
* Initialise le bouton Save
* @param {Function} getCurrentEventData - Fonction pour obtenir les données de l'événement
* @param {Function} getCurrentMode - Fonction pour obtenir le mode actuel
* @param {Function} disableDateSynchronization - Fonction pour désactiver la synchronisation des dates
*/
export function initializeSaveButton(getCurrentEventData, getCurrentMode, disableDateSynchronization) {
const saveBtn = document.getElementById('saveEvent');
if (!saveBtn) return;
saveBtn.onclick = async function() {
const currentMode = getCurrentMode();
const currentEventData = getCurrentEventData();
const eventId = currentEventData ? currentEventData.id : null;
await withLoadingOverlay(async () => {
await handleEventFormSubmit(currentMode, eventId, function() {
// Succès : fermer le modal et rafraîchir les calendriers
refreshCalendars();
closeModal();
// Désactiver la synchronisation des dates
if (disableDateSynchronization) {
disableDateSynchronization();
}
});
});
};
}
/**
* Initialise le bouton "Signaler un incident"
* @param {Function} getCurrentEventData - Fonction pour obtenir les données de l'événement
* @param {Function} openSubModal - Fonction pour ouvrir une sous-modale
*/
export function initializeReportIncidentButton(getCurrentEventData, openSubModal) {
const reportIncidentBtn = document.getElementById('reportIncidentBtn');
if (!reportIncidentBtn) return;
reportIncidentBtn.onclick = async function() {
const currentEventData = getCurrentEventData();
if (!currentEventData || !currentEventData.id) {
notifyError('Aucun événement sélectionné');
return;
}
// Récupérer le type d'événement
const eventType = currentEventData.type || currentEventData.extendedProps?.type || 'individuel';
const isGroupe = eventType === 'groupe';
// Ouvrir la sous-modale avec préparation des données
openSubModal(
'declarationIncidentModal',
(subModal) => {
// Pré-remplir le formulaire
const eventIdInput = document.getElementById('incident_event_id');
const eventTypeInput = document.getElementById('incident_event_type');
const sectionIndividuel = document.getElementById('incident_individuel_section');
const sectionGroupe = document.getElementById('incident_groupe_section');
if (eventIdInput) eventIdInput.value = currentEventData.id;
if (eventTypeInput) eventTypeInput.value = eventType;
// Afficher/masquer les sections selon le type
if (isGroupe) {
if (sectionIndividuel) sectionIndividuel.style.display = 'none';
if (sectionGroupe) sectionGroupe.style.display = 'block';
} else {
if (sectionIndividuel) sectionIndividuel.style.display = 'block';
if (sectionGroupe) sectionGroupe.style.display = 'none';
// Pré-remplir les infos du bénéficiaire
const beneficiaireId = currentEventData.id_beneficiaire || currentEventData.extendedProps?.id_beneficiaire;
const beneficiaireNom = currentEventData.beneficiaire?.nom_complet ||
currentEventData.extendedProps?.beneficiaire?.nom_complet ||
'Non spécifié';
const beneficiaireNomInput = document.getElementById('incident_beneficiaire_nom');
const beneficiaireIdInput = document.getElementById('incident_beneficiaire_id');
if (beneficiaireNomInput) beneficiaireNomInput.value = beneficiaireNom;
if (beneficiaireIdInput) beneficiaireIdInput.value = beneficiaireId || '';
}
}
);
};
}
/**
* Charge l'historique des 3 derniers rendez-vous d'un bénéficiaire
* @param {number} beneficiaireId - ID du bénéficiaire
* @returns {Promise<Array>} Tableau des rendez-vous avec leurs incidents
*/
async function loadBeneficiaireHistorique(beneficiaireId) {
try {
const historique = await apiFetch(`beneficiaires/${beneficiaireId}/historique`);
return historique || [];
} catch (error) {
console.error('Erreur lors du chargement de l\'historique:', error);
notifyError('Erreur lors du chargement de l\'historique du bénéficiaire');
return [];
}
}
/**
* Affiche l'historique sous forme de ligne du temps verticale
* @param {Array} historiqueData - Tableau des rendez-vous avec leurs incidents
*/
function displayHistoriqueTimeline(historiqueData) {
const timelineContainer = document.getElementById('historiqueTimeline');
if (!timelineContainer) {
console.error('Conteneur de timeline non trouvé');
return;
}
if (!historiqueData || historiqueData.length === 0) {
timelineContainer.innerHTML = `
<div class="alert alert-info text-center">
<i class="fas fa-info-circle me-2"></i>
Aucun rendez-vous trouvé pour ce bénéficiaire.
</div>
`;
return;
}
let html = '<div class="crvi-timeline">';
historiqueData.forEach((rdv, index) => {
const date = new Date(rdv.date_rdv + 'T' + rdv.heure_rdv);
const dateFormatted = date.toLocaleDateString('fr-FR', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
});
const heureFormatted = date.toLocaleTimeString('fr-FR', {
hour: '2-digit',
minute: '2-digit'
});
const intervenantNom = rdv.intervenant
? `${rdv.intervenant.nom || ''} ${rdv.intervenant.prenom || ''}`.trim()
: 'Non renseigné';
const typeInterventionNom = rdv.type_intervention?.nom || 'Non renseigné';
// Déterminer le type d'événement et la couleur
const hasIncident = rdv.incident && rdv.incident.id;
const statutLower = rdv.statut ? rdv.statut.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "") : '';
let eventType = 'default';
let icon = 'fa-calendar-alt';
if (hasIncident || statutLower.includes('absence') || statutLower.includes('absent')) {
eventType = 'incident'; // Rouge
icon = 'fa-exclamation-triangle';
} else if (statutLower.includes('present')) {
eventType = 'present'; // Vert
icon = 'fa-check-circle';
} else if (statutLower.includes('prevu')) {
eventType = 'prevu'; // Bleu
icon = 'fa-clock';
} else {
eventType = 'default'; // Gris
icon = 'fa-calendar-alt';
}
const isLastItem = index === historiqueData.length - 1;
const eventNumber = index + 1;
html += `
<div class="crvi-timeline__event crvi-timeline__event--${eventType} ${isLastItem ? 'crvi-timeline__event--last' : ''}" data-number="${eventNumber}">
<div class="crvi-timeline__event__header">
<div class="crvi-timeline__event__date-wrapper">
<div class="crvi-timeline__event__date-icon">
<i class="fas ${icon}"></i>
</div>
<div class="crvi-timeline__event__date-info">
<div class="crvi-timeline__event__date">${dateFormatted}</div>
<div class="crvi-timeline__event__time">${heureFormatted}</div>
</div>
</div>
</div>
<div class="crvi-timeline__event__title">
${hasIncident ? '<i class="fas fa-exclamation-triangle me-2"></i>' : ''}
${rdv.type || 'Rendez-vous'}
</div>
<div class="crvi-timeline__event__content">
${rdv.personne_en_liste_rouge ? `
<div class="alert alert-danger mb-2" style="font-size: 90%; padding: 8px;">
<i class="fas fa-exclamation-triangle me-2"></i>
<strong>Personne en liste rouge</strong>
</div>
` : ''}
<div class="mb-2">
<strong><i class="fas fa-user-md me-2"></i>Intervenant:</strong> ${intervenantNom}
</div>
<div class="mb-2">
<strong><i class="fas fa-tools me-2"></i>Type d'intervention:</strong> ${typeInterventionNom}
</div>
${rdv.statut ? `
<div class="mb-2">
<strong><i class="fas fa-info-circle me-2"></i>Statut:</strong>
<span class="badge bg-${eventType === 'present' ? 'success' : eventType === 'prevu' ? 'primary' : eventType === 'incident' ? 'danger' : 'secondary'}">${rdv.statut}</span>
</div>
` : ''}
${rdv.commentaire ? `
<div class="mb-2">
<strong><i class="fas fa-comment me-2"></i>Commentaire:</strong> ${rdv.commentaire}
</div>
` : ''}
${hasIncident ? `
<div class="alert alert-danger mt-3 mb-0" style="background-color: #fee; border-left: 4px solid #dc3545; padding: 12px;">
<strong><i class="fas fa-exclamation-triangle me-2"></i>Incident signalé</strong>
<div class="mt-2">
<strong>Résumé:</strong> ${rdv.incident.resume_incident || 'Non renseigné'}
</div>
${rdv.incident.commentaire_incident ? `
<div class="mt-2">
<strong>Commentaire:</strong> ${rdv.incident.commentaire_incident}
</div>
` : ''}
</div>
` : ''}
</div>
</div>
`;
});
html += '</div>';
timelineContainer.innerHTML = html;
}
/**
* Initialise le bouton "Historique bénéficiaire"
* @param {Function} getCurrentEventData - Fonction pour obtenir les données de l'événement
* @param {Function} openSubModal - Fonction pour ouvrir une sous-modale
*/
export function initializeHistoriqueButton(getCurrentEventData, openSubModal) {
const historiqueBtn = document.getElementById('showBeneficiaireHistoriqueBtn');
if (!historiqueBtn) return;
historiqueBtn.onclick = async function() {
const currentEventData = getCurrentEventData();
const beneficiaireId = historiqueBtn.getAttribute('data-benef') ||
currentEventData?.id_beneficiaire ||
currentEventData?.extendedProps?.id_beneficiaire;
if (!beneficiaireId) {
notifyError('ID du bénéficiaire introuvable');
return;
}
// Ouvrir la sous-modale avec chargement des données
openSubModal(
'beneficiaireHistoriqueModal',
async (subModal) => {
// Afficher le spinner de chargement
const timelineContainer = document.getElementById('historiqueTimeline');
if (timelineContainer) {
timelineContainer.innerHTML = `
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Chargement...</span>
</div>
</div>
`;
}
// Charger l'historique
try {
const historiqueData = await loadBeneficiaireHistorique(parseInt(beneficiaireId));
displayHistoriqueTimeline(historiqueData);
} catch (error) {
console.error('Erreur lors du chargement de l\'historique:', error);
if (timelineContainer) {
timelineContainer.innerHTML = `
<div class="alert alert-danger">
<i class="fas fa-exclamation-triangle me-2"></i>
Erreur lors du chargement de l'historique.
</div>
`;
}
}
},
(subModal) => {
// Nettoyer après fermeture
const timelineContainer = document.getElementById('historiqueTimeline');
if (timelineContainer) {
timelineContainer.innerHTML = '';
}
}
);
};
}
/**
* Initialise le bouton "Debug SMS"
* @param {Function} getCurrentEventData - Fonction pour obtenir les données de l'événement
*/
export function initializeDebugSmsButton(getCurrentEventData) {
const debugSmsBtn = document.getElementById('debugSmsBtn');
if (!debugSmsBtn) return;
debugSmsBtn.onclick = function() {
const currentEventData = getCurrentEventData();
const eventId = currentEventData ? currentEventData.id : null;
console.log('[DEBUG_SMS] Données événement actuelles:', currentEventData);
// Construire un message simple pour le debug
const dateStr = (document.getElementById('date_rdv') && document.getElementById('date_rdv').value) || currentEventData?.date || '';
const timeStr = (document.getElementById('heure_rdv') && document.getElementById('heure_rdv').value) || currentEventData?.time || '';
const typeStr = (document.getElementById('type') && document.getElementById('type').value) || currentEventData?.type || '';
const msg = `Debug SMS - Event ${eventId ?? 'N/A'} - ${dateStr} ${timeStr} - type: ${typeStr}`;
const phone = '0485500723';
if (!window.crviAjax || !window.crviAjax.url || !window.crviAjax.nonce) {
alert('Debug SMS: configuration AJAX manquante.');
return;
}
const params = new URLSearchParams();
params.append('action', 'crvi_send_sms_debug');
params.append('nonce', window.crviAjax.nonce);
params.append('phone', phone);
params.append('message', msg);
if (eventId) {
params.append('id_event', String(eventId));
}
params.append('sujet', 'debug');
fetch(window.crviAjax.url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
},
body: params.toString()
}).then(async (res) => {
let json;
try { json = await res.json(); } catch (_) {}
if (!res.ok || !json?.success) {
const errMsg = json?.data?.message || 'Erreur lors de l\'envoi SMS (debug).';
throw new Error(errMsg);
}
alert('SMS de debug envoyé.');
}).catch((err) => {
console.error('[DEBUG_SMS] Échec envoi:', err);
alert('Échec envoi SMS de debug: ' + (err?.message || 'Erreur inconnue'));
});
};
}
/**
* Initialise le bouton "Voir les incidents"
* @param {Function} getCurrentEventData - Fonction pour obtenir les données de l'événement
* @param {Function} openIncidentsViewModal - Fonction pour ouvrir la modal de visualisation des incidents
*/
export function initializeViewIncidentsButton(getCurrentEventData, openIncidentsViewModal) {
const viewIncidentsBtn = document.getElementById('viewIncidentsBtn');
if (!viewIncidentsBtn) return;
viewIncidentsBtn.onclick = async function() {
const currentEventData = getCurrentEventData();
const eventId = currentEventData?.id || currentEventData?.extendedProps?.id;
if (!eventId) {
notifyError('ID de l\'événement introuvable');
return;
}
await openIncidentsViewModal(eventId);
};
}
/**
* Initialise tous les boutons de la modale
* @param {Object} options - Options de configuration
* @param {Function} options.getCurrentEventData - Fonction pour obtenir les données de l'événement
* @param {Function} options.getCurrentMode - Fonction pour obtenir le mode actuel
* @param {Function} options.setCurrentMode - Fonction pour changer le mode
* @param {Function} options.openSubModal - Fonction pour ouvrir une sous-modale
* @param {Function} options.openCheckPresenceModal - Fonction pour ouvrir la modal de validation de présence
* @param {Function} options.openIncidentsViewModal - Fonction pour ouvrir la modal de visualisation des incidents
* @param {Function} options.enableDateSynchronization - Fonction pour activer la synchronisation des dates
* @param {Function} options.disableDateSynchronization - Fonction pour désactiver la synchronisation des dates
* @param {Function} options.onDeleted - Callback après suppression
* @param {Function} options.onStatusChanged - Callback après changement de statut
*/
export function initializeModalButtons(options = {}) {
const {
getCurrentEventData,
getCurrentMode,
setCurrentMode,
openSubModal,
openCheckPresenceModal,
openIncidentsViewModal,
enableDateSynchronization,
disableDateSynchronization,
onDeleted,
onStatusChanged
} = options;
// Boutons de fermeture
initializeCloseButton('closeModalBtn');
initializeCloseButton('closeViewBtn');
// Boutons d'action
if (getCurrentEventData) {
initializeEditButton(getCurrentEventData, getCurrentMode, setCurrentMode, enableDateSynchronization);
initializeCancelButton(getCurrentEventData, getCurrentMode, setCurrentMode, disableDateSynchronization);
initializeSaveButton(getCurrentEventData, getCurrentMode, disableDateSynchronization);
initializeDeleteButton(getCurrentEventData, onDeleted);
initializeMarkPresentButton(getCurrentEventData, openCheckPresenceModal, onStatusChanged);
initializeShowPresenceModalButton(getCurrentEventData, openCheckPresenceModal);
initializeMarkAbsentButton(getCurrentEventData, onStatusChanged);
initializeCancelAppointmentButton(getCurrentEventData, onStatusChanged);
initializeDebugSmsButton(getCurrentEventData);
// Boutons spécifiques avec sous-modales
if (openSubModal) {
initializeReportIncidentButton(getCurrentEventData, openSubModal);
initializeHistoriqueButton(getCurrentEventData, openSubModal);
}
// Bouton de visualisation des incidents
if (openIncidentsViewModal) {
initializeViewIncidentsButton(getCurrentEventData, openIncidentsViewModal);
}
}
}

View File

@ -0,0 +1,359 @@
// Module d'affichage des données de la modale
// Contient les fonctions d'affichage en mode lecture seule
import { setText, toggleElement } from './agenda-modal-dom.js';
import { checkAndDisplayIncidentsButton } from './agenda-modal.js';
/**
* Extrait la date et l'heure d'un événement selon différentes sources
* @param {Object} event - Données de l'événement
* @returns {Object} - {dateFormatted, heureFormatted}
*/
function extractDateTime(event) {
let dateFormatted = '';
let heureFormatted = '';
// Priorité 1: Données directes de l'API
if (event?.date && event?.heure) {
const dateObj = new Date(event.date + 'T' + event.heure);
dateFormatted = dateObj.toLocaleDateString('fr-FR');
heureFormatted = dateObj.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' });
}
// Priorité 2: Données dans extendedProps
else if (event?.extendedProps?.date && event?.extendedProps?.heure) {
const dateObj = new Date(event.extendedProps.date + 'T' + event.extendedProps.heure);
dateFormatted = dateObj.toLocaleDateString('fr-FR');
heureFormatted = dateObj.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' });
}
// Priorité 3: Données FullCalendar
else if (event?.start) {
const startDate = new Date(event.start);
dateFormatted = startDate.toLocaleDateString('fr-FR');
heureFormatted = startDate.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' });
}
return { dateFormatted, heureFormatted };
}
/**
* Récupère le nom d'une entité depuis le select ou les données
* @param {string} entityName - Nom de l'entité ('beneficiaire', 'intervenant', etc.)
* @param {Object} event - Données de l'événement
* @param {string} selectId - ID du select
* @param {string} idField - Nom du champ ID
* @returns {string} - Nom formaté
*/
function getEntityName(entityName, event, selectId, idField) {
let name = '';
// Vérifier dans les relations directes
if (event?.[entityName]?.nom) {
name = event[entityName].nom + (event[entityName].prenom ? ' ' + event[entityName].prenom : '');
} else if (event?.extendedProps?.[entityName]?.nom) {
name = event.extendedProps[entityName].nom + (event.extendedProps[entityName].prenom ? ' ' + event.extendedProps[entityName].prenom : '');
} else {
// Chercher par ID dans le select
const entityId = event?.[idField] || event?.extendedProps?.[idField];
if (entityId) {
const select = document.getElementById(selectId);
// Vérifier si c'est un SELECT (pour intervenant côté front c'est un input hidden)
if (select && select.tagName === 'SELECT') {
const option = select.querySelector(`option[value="${entityId}"]`);
if (option) {
name = option.textContent;
}
} else if (select && selectId === 'id_intervenant') {
// Cas spécial pour intervenant côté front (input hidden)
const displayEl = document.getElementById('id_intervenant_display');
if (displayEl && displayEl.textContent) {
name = displayEl.textContent;
}
}
if (!name) {
name = `ID: ${entityId}`;
}
}
}
return name;
}
/**
* Récupère le nom d'un terme taxonomy depuis les données ou le select
* @param {Object} event - Données de l'événement
* @param {string} labelField - Champ de label (_label)
* @param {string} relationField - Champ de relation
* @param {string} idField - Champ ID
* @param {string} selectId - ID du select
* @returns {string} - Nom du terme
*/
function getTermName(event, labelField, relationField, idField, selectId) {
let name = '';
// Priorité 1: Label direct
name = event?.[labelField] || event?.extendedProps?.[labelField] || '';
// Priorité 2: Relation
if (!name && event?.[relationField]?.nom) {
name = event[relationField].nom;
} else if (!name && event?.extendedProps?.[relationField]?.nom) {
name = event.extendedProps[relationField].nom;
}
// Priorité 3: Chercher dans le select
if (!name) {
const termId = event?.[idField] || event?.extendedProps?.[idField];
if (termId && termId !== '0' && termId !== 0) {
const select = document.getElementById(selectId);
if (select) {
const option = select.querySelector(`option[value="${termId}"]`);
if (option) {
name = option.textContent;
}
}
if (!name) {
name = `ID: ${termId}`;
}
}
}
return name;
}
/**
* Remplit le bloc de vue avec les données d'un événement
* @param {Object} event - Données de l'événement
*/
export function fillViewBlock(event) {
// Date et heure
const { dateFormatted, heureFormatted } = extractDateTime(event);
setText('view_date_rdv', dateFormatted);
setText('view_heure_rdv', heureFormatted);
// Type et langue
const type = event?.type || event?.extendedProps?.type || '';
setText('view_type', type);
let langue = event?.langue_label || event?.extendedProps?.langue_label || '';
if (!langue || /^\d+$/.test(langue)) {
const langueId = langue || event?.langue || event?.extendedProps?.langue || '';
if (langueId) {
const langueSelect = document.getElementById('langue');
if (langueSelect) {
const option = langueSelect.querySelector(`option[value="${langueId}"]`);
if (option) {
langue = option.textContent;
} else {
langue = langueId;
}
} else {
langue = langueId;
}
}
}
if (!langue) {
langue = event?.langue || event?.extendedProps?.langue || '';
}
setText('view_langue', langue);
// Entités
setText('view_beneficiaire', getEntityName('beneficiaire', event, 'id_beneficiaire', 'id_beneficiaire'));
setText('view_intervenant', getEntityName('intervenant', event, 'id_intervenant', 'id_intervenant'));
setText('view_local', getEntityName('local', event, 'id_local', 'id_local'));
// Termes taxonomy
setText('view_departement', getTermName(event, 'departement_label', 'departement', 'id_departement', 'id_departement'));
setText('view_type_intervention', getTermName(event, 'type_intervention_label', 'type_intervention', 'id_type_intervention', 'id_type_intervention'));
// Traducteur (logique spéciale)
let traducteurNom = '';
const traducteurId = event?.id_traducteur || event?.extendedProps?.id_traducteur;
const hasValidTraducteurId = traducteurId && parseInt(traducteurId, 10) > 0;
if (hasValidTraducteurId) {
traducteurNom = getEntityName('traducteur', event, 'id_traducteur', 'id_traducteur');
} else {
traducteurNom = event?.nom_traducteur || event?.extendedProps?.nom_traducteur || '';
}
setText('view_traducteur', traducteurNom);
// Données de groupe
const eventType = event?.type || event?.extendedProps?.type || '';
const groupeFields = document.querySelectorAll('.groupe-only-field');
if (eventType === 'groupe') {
groupeFields.forEach(field => field.style.display = '');
setText('view_nb_participants', event?.nb_participants || event?.extendedProps?.nb_participants || '');
setText('view_nb_hommes', event?.nb_hommes || event?.extendedProps?.nb_hommes || '');
setText('view_nb_femmes', event?.nb_femmes || event?.extendedProps?.nb_femmes || '');
} else {
groupeFields.forEach(field => field.style.display = 'none');
setText('view_nb_participants', '');
setText('view_nb_hommes', '');
setText('view_nb_femmes', '');
}
// Commentaire
setText('view_commentaire', event?.commentaire || event?.extendedProps?.commentaire || '');
// Bouton historique bénéficiaire (uniquement pour événements individuels)
const historiqueBtn = document.getElementById('showBeneficiaireHistoriqueBtn');
if (historiqueBtn) {
const beneficiaireId = event?.id_beneficiaire || event?.extendedProps?.id_beneficiaire;
if (eventType === 'individuel' && beneficiaireId) {
historiqueBtn.style.display = 'block';
historiqueBtn.setAttribute('data-benef', beneficiaireId);
} else {
historiqueBtn.style.display = 'none';
historiqueBtn.removeAttribute('data-benef');
}
}
// Vérifier et afficher le bouton "Détail incident(s)" si l'événement a des incidents
const eventId = event?.id || event?.extendedProps?.id;
if (eventId) {
checkAndDisplayIncidentsButton(eventId);
}
// Afficher l'alerte de statut si présent ou absent
const statut = event?.statut || event?.extendedProps?.statut || '';
const statutAlert = document.getElementById('statutAlert');
const statutAlertText = document.getElementById('statutAlertText');
if (statutAlert && statutAlertText) {
if (statut === 'absent' || statut === 'absence') {
statutAlert.className = 'alert alert-danger mb-3';
statutAlertText.textContent = 'La personne a été marquée comme absente à ce rendez-vous.';
statutAlert.style.display = 'block';
} else if (statut === 'present') {
statutAlert.className = 'alert alert-success mb-3';
statutAlertText.textContent = 'La personne a été présente à ce rendez-vous.';
statutAlert.style.display = 'block';
} else {
statutAlert.style.display = 'none';
}
}
}
/**
* Met à jour l'affichage de la modale selon le mode
* @param {string} mode - Mode actuel ('view', 'edit', 'create')
* @param {boolean} canEdit - Permission d'édition
* @param {boolean} canDelete - Permission de suppression
* @param {Object} eventData - Données de l'événement (optionnel)
*/
export function updateModalDisplay(mode, canEdit, canDelete, eventData = null) {
const viewBlock = document.getElementById('eventViewBlock');
const formBlock = document.getElementById('eventForm');
const statusButtons = document.getElementById('eventStatusButtons');
const viewFooter = document.getElementById('eventViewFooter');
const editFooter = document.getElementById('eventEditFooter');
const editBtn = document.getElementById('editEventBtn');
const deleteBtn = document.getElementById('deleteEvent');
const saveBtn = document.getElementById('saveEvent');
const cancelBtn = document.getElementById('cancelEditBtn');
const closeViewBtn = document.getElementById('closeViewBtn');
// Vérifier le statut et si l'événement est clôturé
const statut = eventData?.statut || eventData?.extendedProps?.statut || '';
const clotureFlag = eventData?.cloture_flag || eventData?.extendedProps?.cloture_flag;
const isEventCloture = clotureFlag === 1 || clotureFlag === '1' || clotureFlag === true;
const eventType = eventData?.type || eventData?.extendedProps?.type || '';
const isGroupe = eventType === 'groupe';
// Vérifier si les boutons de statut doivent être cachés
const shouldHideStatusButtons = statut === 'present' || statut === 'absence' || isEventCloture;
if (mode === 'view') {
if (viewBlock) viewBlock.style.display = 'block';
if (formBlock) formBlock.style.display = 'none';
if (viewFooter) viewFooter.style.display = 'block';
if (editFooter) editFooter.style.display = 'none';
if (editBtn) editBtn.style.display = canEdit ? 'inline-block' : 'none';
if (deleteBtn) deleteBtn.style.display = 'none';
if (saveBtn) saveBtn.style.display = 'none';
if (cancelBtn) cancelBtn.style.display = 'none';
if (closeViewBtn) closeViewBtn.style.display = 'inline-block';
// Cacher tous les boutons de statut en mode vue
if (statusButtons) statusButtons.style.display = 'none';
} else if (mode === 'edit' || mode === 'create') {
if (viewBlock) viewBlock.style.display = 'none';
if (formBlock) formBlock.style.display = 'block';
if (viewFooter) viewFooter.style.display = 'none';
if (editFooter) editFooter.style.display = 'block';
if (editBtn) editBtn.style.display = 'none';
if (deleteBtn) deleteBtn.style.display = (mode === 'edit' && canDelete) ? 'inline-block' : 'none';
if (saveBtn) saveBtn.style.display = 'inline-block';
if (cancelBtn) cancelBtn.style.display = 'inline-block';
if (closeViewBtn) closeViewBtn.style.display = 'none';
// Afficher ou cacher les boutons de statut selon le statut de l'événement
if (statusButtons) {
if (mode === 'edit' && canEdit && !shouldHideStatusButtons) {
statusButtons.style.display = 'block';
// Gérer l'affichage des boutons individuels selon le type d'événement
const allButtons = statusButtons.querySelectorAll('button');
allButtons.forEach(btn => {
// Masquer "Absent" pour les événements de groupe
if (btn.id === 'markAbsentBtn') {
btn.style.display = isGroupe ? 'none' : 'inline-block';
}
// Boutons spécifiques aux groupes (classe groupe-only-button)
else if (btn.classList.contains('groupe-only-button')) {
btn.style.display = isGroupe ? 'inline-block' : 'none';
}
// Boutons spécifiques aux individuels (classe individuel-only-button)
else if (btn.classList.contains('individuel-only-button')) {
btn.style.display = isGroupe ? 'none' : 'inline-block';
}
// Le bouton "Détail incident(s)" garde son état (géré par checkAndDisplayIncidentsButton)
else if (btn.id === 'viewIncidentsBtn') {
// Ne rien faire, l'état est géré ailleurs
}
// Afficher tous les autres boutons
else {
btn.style.display = 'inline-block';
}
});
} else {
statusButtons.style.display = 'none';
}
}
}
}
/**
* Vérifie si un événement est passé
* @param {Object} eventData - Données de l'événement
* @returns {boolean} - True si l'événement est passé
*/
export function checkIfEventIsPast(eventData) {
if (!eventData) return false;
let eventDate = null;
let eventTime = null;
// Essayer différentes sources pour la date
if (eventData.date_rdv) {
eventDate = eventData.date_rdv;
eventTime = eventData.heure_rdv || '00:00';
} else if (eventData.start) {
const startDate = new Date(eventData.start);
eventDate = startDate.toISOString().split('T')[0];
eventTime = startDate.toTimeString().substring(0, 5);
} else if (eventData.extendedProps?.date_rdv) {
eventDate = eventData.extendedProps.date_rdv;
eventTime = eventData.extendedProps.heure_rdv || '00:00';
}
if (!eventDate) return false;
const eventDateTime = new Date(`${eventDate}T${eventTime}`);
const now = new Date();
return eventDateTime <= now;
}

View File

@ -0,0 +1,189 @@
// Module de gestion du DOM pour les modales
// Contient les helpers pour accéder et manipuler les éléments DOM de manière optimisée
/**
* Cache pour les éléments DOM fréquemment accédés
*/
const domCache = new Map();
/**
* Obtient un élément du DOM avec mise en cache
* @param {string} elementId - ID de l'élément
* @returns {HTMLElement|null} - L'élément ou null
*/
export function getElement(elementId) {
if (domCache.has(elementId)) {
const cached = domCache.get(elementId);
// Vérifier que l'élément est toujours dans le DOM
if (cached && document.contains(cached)) {
return cached;
}
domCache.delete(elementId);
}
const element = document.getElementById(elementId);
if (element) {
domCache.set(elementId, element);
}
return element;
}
/**
* Vide le cache DOM (à appeler lors de la fermeture du modal)
*/
export function clearDomCache() {
domCache.clear();
}
/**
* Définit la valeur d'un élément de manière sécurisée
* @param {string} elementId - ID de l'élément
* @param {*} value - Valeur à définir
* @returns {boolean} - True si succès
*/
export function safeSetValue(elementId, value) {
const element = getElement(elementId);
if (!element) {
return false;
}
// Si c'est un select avec Select2 initialisé
if (element.tagName === 'SELECT' && window.jQuery && jQuery(element).hasClass('select2-hidden-accessible')) {
jQuery(element).val(value).trigger('change');
} else {
element.value = value;
}
return true;
}
/**
* Récupère la valeur d'un élément de manière sécurisée
* @param {string} elementId - ID de l'élément
* @returns {string|null} - Valeur de l'élément ou null
*/
export function safeGetValue(elementId) {
const element = getElement(elementId);
if (!element) {
return null;
}
return element.value;
}
/**
* Affiche ou cache un élément
* @param {string} elementId - ID de l'élément
* @param {boolean} show - True pour afficher, false pour cacher
*/
export function toggleElement(elementId, show) {
const element = getElement(elementId);
if (element) {
element.style.display = show ? '' : 'none';
}
}
/**
* Ajoute une classe à un élément
* @param {string} elementId - ID de l'élément
* @param {string} className - Classe à ajouter
*/
export function addClass(elementId, className) {
const element = getElement(elementId);
if (element) {
element.classList.add(className);
}
}
/**
* Retire une classe d'un élément
* @param {string} elementId - ID de l'élément
* @param {string} className - Classe à retirer
*/
export function removeClass(elementId, className) {
const element = getElement(elementId);
if (element) {
element.classList.remove(className);
}
}
/**
* Vérifie si un élément a une classe
* @param {string} elementId - ID de l'élément
* @param {string} className - Classe à vérifier
* @returns {boolean} - True si l'élément a la classe
*/
export function hasClass(elementId, className) {
const element = getElement(elementId);
return element ? element.classList.contains(className) : false;
}
/**
* Définit le texte HTML d'un élément
* @param {string} elementId - ID de l'élément
* @param {string} html - HTML à définir
*/
export function setHTML(elementId, html) {
const element = getElement(elementId);
if (element) {
element.innerHTML = html;
}
}
/**
* Définit le texte d'un élément
* @param {string} elementId - ID de l'élément
* @param {string} text - Texte à définir
*/
export function setText(elementId, text) {
const element = getElement(elementId);
if (element) {
element.textContent = text;
}
}
/**
* Ajoute un event listener à un élément
* @param {string} elementId - ID de l'élément
* @param {string} event - Type d'événement
* @param {Function} handler - Gestionnaire d'événement
*/
export function addListener(elementId, event, handler) {
const element = getElement(elementId);
if (element) {
element.addEventListener(event, handler);
}
}
/**
* Nettoie tous les champs d'un formulaire
* @param {Array<string>} textFieldIds - IDs des champs texte
* @param {Array<string>} selectFieldIds - IDs des selects
*/
export function clearFormFields(textFieldIds, selectFieldIds) {
// Nettoyer les champs de texte
textFieldIds.forEach(fieldId => {
const field = getElement(fieldId);
if (field) {
field.value = '';
}
});
// Nettoyer les selects
selectFieldIds.forEach(selectId => {
const select = getElement(selectId);
if (select) {
select.value = '';
// Si c'est un <select>, gérer Select2 et les options
if (select.tagName === 'SELECT' && select.options) {
if (window.jQuery && jQuery(select).hasClass('select2-hidden-accessible')) {
jQuery(select).val('').trigger('change');
}
Array.from(select.options).forEach(option => {
option.style.display = '';
option.disabled = false;
});
}
}
});
}

View File

@ -0,0 +1,517 @@
// Module de gestion des formulaires de la modale
// Contient la logique de remplissage, validation et soumission des formulaires
import { createEvent, updateEvent } from './agenda-api.js';
import { notifyError, notifySuccess } from './agenda-notifications.js';
import { safeSetValue, safeGetValue } from './agenda-modal-dom.js';
/**
* Calcule l'heure de fin (+1h par rapport au début)
* @param {string} heureDebut - Heure de début au format HH:MM
* @returns {string} - Heure de fin au format HH:MM
*/
function calculateHeureFin(heureDebut) {
const [h, m] = heureDebut.split(':').map(Number);
const heureFin = ((h + 1) % 24).toString().padStart(2, '0') + ':' + m.toString().padStart(2, '0');
return heureFin;
}
/**
* Remplit le formulaire avec une date (pour création)
* @param {Object} data - Données avec startStr et endStr
*/
export function fillFormWithDate(data) {
const dateRdv = data?.startStr?.split('T')[0] || '';
safeSetValue('date_rdv', dateRdv);
const heureDebut = data?.startStr?.split('T')[1]?.substring(0, 5) || '09:00';
safeSetValue('heure_rdv', heureDebut);
safeSetValue('date_fin', dateRdv);
const heureFin = data?.endStr?.split('T')[1]?.substring(0, 5) || '09:15';
safeSetValue('heure_fin', heureFin);
}
/**
* Remplit le formulaire avec les données d'un événement (pour édition)
* @param {Object} event - Données de l'événement
* @param {Function} filterTraducteursByLangue - Fonction pour filtrer les traducteurs
*/
export function fillFormWithEvent(event, filterTraducteursByLangue = null) {
if (!event) {
return;
}
// Gérer les dates/heures selon les sources disponibles
if (event.date && event.heure) {
// Données directes de l'API
safeSetValue('date_rdv', event.date);
const heureDebut = event.heure.substring(0, 5);
safeSetValue('heure_rdv', heureDebut);
const dateFin = event.date_fin && event.date_fin >= event.date ? event.date_fin : event.date;
safeSetValue('date_fin', dateFin);
const heureFin = event.heure_fin ? event.heure_fin.substring(0, 5) : calculateHeureFin(heureDebut);
safeSetValue('heure_fin', heureFin);
} else if (event.extendedProps?.date && event.extendedProps?.heure) {
// Données dans extendedProps
safeSetValue('date_rdv', event.extendedProps.date);
const heureDebut = event.extendedProps.heure.substring(0, 5);
safeSetValue('heure_rdv', heureDebut);
const dateFin = event.extendedProps.date_fin && event.extendedProps.date_fin >= event.extendedProps.date ? event.extendedProps.date_fin : event.extendedProps.date;
safeSetValue('date_fin', dateFin);
const heureFin = event.extendedProps.heure_fin ? event.extendedProps.heure_fin.substring(0, 5) : calculateHeureFin(heureDebut);
safeSetValue('heure_fin', heureFin);
} else if (event.start && event.end) {
// Données FullCalendar
try {
const startDate = new Date(event.start);
const endDate = new Date(event.end);
if (!isNaN(startDate.getTime()) && !isNaN(endDate.getTime())) {
const startDateStr = formatDate(startDate);
const endDateStr = formatDate(endDate);
const heureDebut = formatTime(startDate);
const heureFin = formatTime(endDate);
safeSetValue('date_rdv', startDateStr);
safeSetValue('heure_rdv', heureDebut);
safeSetValue('date_fin', endDateStr >= startDateStr ? endDateStr : startDateStr);
safeSetValue('heure_fin', heureFin);
} else {
setCurrentDateTime();
}
} catch (error) {
setCurrentDateTime();
}
} else {
setCurrentDateTime();
}
// Remplir les autres champs
const extendedProps = event.extendedProps || {};
safeSetValue('type', extendedProps.type || event.type || '');
const langueValue = extendedProps.langue || event.langue || '';
safeSetValue('langue', langueValue);
const beneficiaireId = extendedProps.id_beneficiaire || event.id_beneficiaire || '';
safeSetValue('id_beneficiaire', beneficiaireId);
safeSetValue('id_intervenant', extendedProps.id_intervenant || event.id_intervenant || '');
safeSetValue('id_traducteur', extendedProps.id_traducteur || event.id_traducteur || '');
safeSetValue('nom_traducteur', extendedProps.nom_traducteur || event.nom_traducteur || '');
safeSetValue('id_local', extendedProps.id_local || event.id_local || '');
safeSetValue('id_departement', extendedProps.id_departement || event.id_departement || '');
safeSetValue('id_type_intervention', extendedProps.id_type_intervention || event.id_type_intervention || '');
safeSetValue('commentaire', extendedProps.commentaire || event.commentaire || '');
// Mettre à jour les selects Select2
updateSelect2Fields();
// Filtrer les options langue selon langues_disponibles si c'est une permanence
const type = extendedProps.type || event.type || '';
if (type === 'permanence') {
const languesDisponibles = extendedProps.langues_disponibles;
const langueSelect = document.getElementById('langue');
if (langueSelect && languesDisponibles && typeof languesDisponibles === 'string' && languesDisponibles.trim() !== '') {
const languesPermises = languesDisponibles.split(',').map(l => l.trim()).filter(l => l !== '');
if (languesPermises.length > 0) {
const currentValue = langueSelect.value; // Récupérer la valeur actuelle (définie juste avant)
Array.from(langueSelect.options).forEach(option => {
if (option.value === '') {
option.style.display = '';
option.disabled = false;
return;
}
// Garder l'option sélectionnée visible même si elle n'est pas dans langues_disponibles
const isCurrentlySelected = option.value === currentValue;
const optionSlug = option.getAttribute('data-slug');
const isPermise = optionSlug && languesPermises.includes(optionSlug);
if (isPermise || isCurrentlySelected) {
option.style.display = '';
option.disabled = false;
} else {
option.style.display = 'none';
option.disabled = true;
}
});
}
}
}
// Filtrer les traducteurs selon la langue
if (filterTraducteursByLangue) {
setTimeout(() => filterTraducteursByLangue(), 50);
}
// Gérer la case "traducteur existant"
handleTraducteurExistingCheckbox(extendedProps.id_traducteur || event.id_traducteur);
// Gérer les champs conditionnels selon le type
handleTypeConditionalFields(type, event, extendedProps);
// Charger le statut liste rouge si bénéficiaire sélectionné
if (beneficiaireId) {
setTimeout(() => {
const beneficiaireSelect = document.getElementById('id_beneficiaire');
if (beneficiaireSelect) {
beneficiaireSelect.dispatchEvent(new Event('change', { bubbles: true }));
}
}, 100);
}
}
/**
* Formate une date en YYYY-MM-DD
* @param {Date} date - Date à formater
* @returns {string}
*/
function formatDate(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
/**
* Formate une heure en HH:MM
* @param {Date} date - Date à formater
* @returns {string}
*/
function formatTime(date) {
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${hours}:${minutes}`;
}
/**
* Définit la date et l'heure actuelles
*/
function setCurrentDateTime() {
const now = new Date();
const nowStr = formatDate(now);
const heureDebut = formatTime(now);
safeSetValue('date_rdv', nowStr);
safeSetValue('heure_rdv', heureDebut);
safeSetValue('date_fin', nowStr);
safeSetValue('heure_fin', calculateHeureFin(heureDebut));
}
/**
* Met à jour les champs Select2
*/
function updateSelect2Fields() {
const select2Fields = ['type', 'id_beneficiaire', 'id_intervenant', 'id_traducteur', 'id_local', 'id_departement', 'id_type_intervention'];
select2Fields.forEach(fieldId => {
const element = document.getElementById(fieldId);
if (element && element.tagName === 'SELECT' && window.jQuery && jQuery(element).hasClass('select2-hidden-accessible')) {
const currentValue = element.value;
if (currentValue) {
jQuery(element).val(currentValue).trigger('change');
}
}
});
}
/**
* Gère la case à cocher "traducteur existant"
* @param {number|string} traducteurId - ID du traducteur
*/
function handleTraducteurExistingCheckbox(traducteurId) {
const tradSelect = document.getElementById('id_traducteur');
const useExistingCheckbox = document.getElementById('use_existing_traducteur');
const tradContainer = document.getElementById('traducteur-select-container');
if (useExistingCheckbox && tradContainer && tradSelect) {
const hasTraducteur = traducteurId && parseInt(traducteurId, 10) > 0;
useExistingCheckbox.checked = hasTraducteur;
if (hasTraducteur) {
tradContainer.classList.remove('d-none');
tradSelect.setAttribute('required', 'required');
} else {
tradContainer.classList.add('d-none');
tradSelect.removeAttribute('required');
safeSetValue('id_traducteur', '');
}
}
}
/**
* Gère les champs conditionnels selon le type d'événement
* @param {string} type - Type d'événement
* @param {Object} event - Données de l'événement
* @param {Object} extendedProps - Propriétés étendues
*/
function handleTypeConditionalFields(type, event, extendedProps) {
// Gérer les champs conditionnels
const groupeFields = document.getElementById('groupeFields');
const nbParticipantsField = document.getElementById('nb_participants');
const beneficiaireContainer = document.getElementById('id_beneficiaire')?.closest('.col-md-6');
const beneficiaireField = document.getElementById('id_beneficiaire');
if (!groupeFields || !nbParticipantsField || !beneficiaireContainer || !beneficiaireField) return;
if (type === 'groupe') {
// Afficher les champs de groupe
groupeFields.style.display = '';
nbParticipantsField.required = true;
beneficiaireContainer.style.display = 'none';
beneficiaireField.required = false;
// Remplir les champs de groupe
if (extendedProps.nb_participants || event.nb_participants) {
safeSetValue('nb_participants', extendedProps.nb_participants || event.nb_participants);
}
if (extendedProps.nb_hommes || event.nb_hommes) {
safeSetValue('nb_hommes', extendedProps.nb_hommes || event.nb_hommes);
}
if (extendedProps.nb_femmes || event.nb_femmes) {
safeSetValue('nb_femmes', extendedProps.nb_femmes || event.nb_femmes);
}
} else {
// Masquer les champs de groupe
groupeFields.style.display = 'none';
nbParticipantsField.required = false;
beneficiaireContainer.style.display = '';
beneficiaireField.required = true;
}
}
/**
* Réinitialise le formulaire
*/
export function resetForm() {
const form = document.getElementById('eventForm');
if (form) {
form.reset();
}
clearFormErrors();
// Réinitialiser les champs conditionnels
const groupeFields = document.getElementById('groupeFields');
const nbParticipantsField = document.getElementById('nb_participants');
const beneficiaireContainer = document.getElementById('id_beneficiaire')?.closest('.col-md-6');
const beneficiaireField = document.getElementById('id_beneficiaire');
if (groupeFields) groupeFields.style.display = 'none';
if (nbParticipantsField) nbParticipantsField.required = false;
if (beneficiaireContainer) beneficiaireContainer.style.display = '';
if (beneficiaireField) beneficiaireField.required = true;
// Réinitialiser le bloc traducteur
const tradContainer = document.getElementById('traducteur-select-container');
const tradSelect = document.getElementById('id_traducteur');
const useExistingCheckbox = document.getElementById('use_existing_traducteur');
if (tradContainer) tradContainer.classList.add('d-none');
if (tradSelect) {
tradSelect.removeAttribute('required');
tradSelect.value = '';
if (window.jQuery && jQuery(tradSelect).hasClass('select2-hidden-accessible')) {
jQuery(tradSelect).val('').trigger('change');
}
}
if (useExistingCheckbox) useExistingCheckbox.checked = false;
}
/**
* Affiche les erreurs de formulaire
* @param {Object} errors - Objet des erreurs {field: message}
*/
export function showFormErrors(errors) {
clearFormErrors();
let hasGeneral = false;
for (const [field, message] of Object.entries(errors)) {
const input = document.getElementById(field);
if (input) {
input.classList.add('is-invalid');
let feedback = input.parentNode.querySelector('.invalid-feedback');
if (!feedback) {
feedback = document.createElement('div');
feedback.className = 'invalid-feedback';
input.parentNode.appendChild(feedback);
}
feedback.textContent = message;
} else {
hasGeneral = true;
}
}
// Message général si erreur non liée à un champ
if (hasGeneral) {
const errorBox = document.getElementById('eventFormErrors');
if (errorBox) {
errorBox.textContent = errors._general || 'Erreur lors de la validation du formulaire';
errorBox.classList.remove('d-none');
}
}
}
/**
* Efface les erreurs de formulaire
*/
export function clearFormErrors() {
const form = document.getElementById('eventForm');
if (!form) return;
form.querySelectorAll('.is-invalid').forEach(el => el.classList.remove('is-invalid'));
form.querySelectorAll('.invalid-feedback').forEach(el => el.remove());
const errorBox = document.getElementById('eventFormErrors');
if (errorBox) {
errorBox.textContent = '';
errorBox.classList.add('d-none');
}
}
/**
* Validation du formulaire
* @param {Object} data - Données du formulaire
* @param {string} mode - Mode ('create' ou 'edit')
* @returns {Object} - Objet des erreurs ou {}
*/
function validateForm(data, mode) {
const errors = {};
// Champs requis de base
const requiredFields = ['date_rdv', 'heure_rdv', 'type', 'langue', 'id_intervenant', 'id_local'];
// Ajouter le bénéficiaire seulement si ce n'est pas un RDV de groupe
if (data.type !== 'groupe') {
requiredFields.push('id_beneficiaire');
}
requiredFields.forEach(field => {
const value = data[field];
if (!value || value === '' || value === '0' || value === 'null') {
errors[field] = 'Le champ est requis';
}
});
// Champs conditionnels pour groupe
if (data.type === 'groupe') {
if (!data.nb_participants || parseInt(data.nb_participants) < 1) {
errors['nb_participants'] = 'Nombre de participants requis (>0)';
}
}
// Validation: empêcher la création d'événements à une date passée
if (mode === 'create' && data.date_rdv) {
const today = new Date();
today.setHours(0, 0, 0, 0);
const eventDate = new Date(data.date_rdv + 'T00:00:00');
eventDate.setHours(0, 0, 0, 0);
if (eventDate < today) {
errors['date_rdv'] = 'La date de rendez-vous ne peut pas être dans le passé';
}
}
// Cohérence des dates/heures
if (data.date_fin && data.date_rdv) {
if (data.date_fin < data.date_rdv) {
errors['date_fin'] = 'La date de fin doit être après ou égale à la date de début';
} else if (data.date_fin === data.date_rdv && data.heure_fin && data.heure_rdv) {
if (data.heure_fin <= data.heure_rdv) {
errors['heure_fin'] = 'L\'heure de fin doit être après l\'heure de début';
}
}
}
return errors;
}
/**
* Prépare les données du formulaire
* @param {Object} data - Données brutes du formulaire
* @returns {Object} - Données préparées
*/
function prepareFormData(data) {
// Gérer la logique "traducteur existant"
const useExisting = data.use_existing_traducteur === '1' || data.use_existing_traducteur === 'on' || data.use_existing_traducteur === 'true';
const traducteurId = parseInt(data.id_traducteur, 10);
const hasValidTraducteurId = !isNaN(traducteurId) && traducteurId > 0;
if (useExisting && hasValidTraducteurId) {
// Utiliser id_traducteur et ignorer nom_traducteur
delete data.nom_traducteur;
data.id_traducteur = traducteurId.toString();
} else {
// Utiliser nom_traducteur et mettre id_traducteur à null
data.id_traducteur = null;
if (!data.nom_traducteur || data.nom_traducteur.trim() === '') {
delete data.nom_traducteur;
}
}
return data;
}
/**
* Gestion de la soumission du formulaire
* @param {string} mode - Mode ('create' ou 'edit')
* @param {number|null} eventId - ID de l'événement (si édition)
* @param {Function} onSuccess - Callback de succès
*/
export async function handleEventFormSubmit(mode, eventId = null, onSuccess = null) {
clearFormErrors();
const form = document.getElementById('eventForm');
if (!form) {
notifyError('Formulaire non trouvé');
return;
}
const formData = new FormData(form);
let data = Object.fromEntries(formData.entries());
// Gérer les champs multiples (comme langues[]) UNIQUEMENT pour les permanences
// Ne pas traiter pour les autres types d'événements pour ne pas casser la mise à jour classique
if (data.type === 'permanence') {
const langues = formData.getAll('langues[]');
if (langues && langues.length > 0) {
// Filtrer les valeurs vides et convertir en tableau de slugs
data.langues = langues.filter(value => value !== '' && value !== null);
}
}
// Validation côté client
const errors = validateForm(data, mode);
if (Object.keys(errors).length > 0) {
showFormErrors(errors);
notifyError('Veuillez corriger les erreurs du formulaire');
return;
}
// Préparer les données
data = prepareFormData(data);
// Appel API
try {
if (mode === 'create') {
await createEvent(data);
notifySuccess('Événement créé avec succès');
} else if (mode === 'edit' && eventId) {
await updateEvent(eventId, data);
notifySuccess('Événement modifié avec succès');
}
if (onSuccess) onSuccess();
} catch (e) {
// Gestion des erreurs serveur
const serverErrors = {};
if (e && e.message) {
if (e.details && e.details.field) {
serverErrors[e.details.field] = e.details.message;
} else {
serverErrors._general = e.message;
}
} else {
serverErrors._general = 'Erreur serveur inconnue';
}
showFormErrors(serverErrors);
notifyError(e.message || 'Erreur lors de la sauvegarde');
}
}

View File

@ -0,0 +1,707 @@
// Module de gestion des selects pour les modales
// Contient la logique de filtrage, Select2 et population des selects
import { getFilters } from './agenda-api.js';
import { notifyError } from './agenda-notifications.js';
// Configuration
const DEBUG_SELECTS = false; // Activer pour déboguer
// Flags pour éviter les boucles infinies
let isUpdatingSelects = false;
let isFilteringTraducteurs = false;
// Cache pour les disponibilités
const disponibilitesCache = new Map();
let lastPopulateCall = null;
let isPopulateSelectsRunning = false;
/**
* Initialise Select2 sur tous les selects de la modale
*/
export function initializeSelect2() {
if (!window.jQuery || !window.jQuery.fn.select2) {
console.warn('Select2 non disponible, réessai dans 100ms...');
setTimeout(initializeSelect2, 100);
return;
}
// Vérifier spécifiquement le select langue avant initialisation
const langueSelect = document.getElementById('langue');
if (langueSelect) {
// Forcer l'ajout de la classe skip-select2 si elle n'est pas présente
if (!langueSelect.classList.contains('skip-select2')) {
console.warn('⚠️ [LANGUE] Classe skip-select2 manquante, ajout forcé');
langueSelect.classList.add('skip-select2');
}
const hasSkipSelect2 = langueSelect.classList.contains('skip-select2');
const hasSelect2Class = langueSelect.classList.contains('select2');
const isAlreadySelect2 = window.jQuery && jQuery(langueSelect).hasClass('select2-hidden-accessible');
console.log('🔵 [LANGUE] initializeSelect2 - skip-select2:', hasSkipSelect2, '| classe select2:', hasSelect2Class, '| Déjà Select2:', isAlreadySelect2, '| ID:', langueSelect.id, '| Classes:', langueSelect.className, '| HTML:', langueSelect.outerHTML.substring(0, 200));
// Si le select langue a skip-select2 mais est déjà en Select2, le détruire
if (hasSkipSelect2 && isAlreadySelect2) {
console.log('⚠️ [LANGUE] Select2 détecté sur select avec skip-select2, destruction...');
try {
jQuery(langueSelect).select2('destroy');
// Nettoyer les classes et attributs Select2
langueSelect.classList.remove('select2-hidden-accessible', 'select2');
langueSelect.removeAttribute('data-select2-id');
langueSelect.removeAttribute('tabindex');
langueSelect.removeAttribute('aria-hidden');
console.log('✅ [LANGUE] Select2 détruit, classes après:', langueSelect.className);
} catch (e) {
console.warn('Erreur lors de la destruction Select2 pour langue:', e);
}
}
// S'assurer que la classe select2 n'est pas présente si skip-select2 est là
if (hasSkipSelect2 && hasSelect2Class) {
langueSelect.classList.remove('select2');
console.log('🧹 [LANGUE] Classe select2 retirée (skip-select2 présent)');
}
}
jQuery('#eventModal select:not(.skip-select2)').each(function() {
const $select = jQuery(this);
// Ignorer explicitement le select langue s'il a skip-select2
if ($select.attr('id') === 'langue' && $select.hasClass('skip-select2')) {
console.log('⏭️ [LANGUE] Ignoré dans initializeSelect2 (skip-select2)');
return;
}
// Si Select2 est déjà initialisé, le détruire pour réappliquer
if ($select.hasClass('select2-hidden-accessible') || $select.data('select2')) {
try { $select.select2('destroy'); } catch (e) {}
}
$select.select2({
width: '100%',
placeholder: 'Sélectionner...',
allowClear: true,
dropdownParent: jQuery('#eventModal')
});
});
}
/**
* Fusionne les données de l'événement avec les disponibilités
* @param {Object} availabilityData - Données de disponibilité de l'API
* @param {Object} eventData - Données de l'événement
* @returns {Object} - Données fusionnées
*/
function mergeEventDataWithAvailability(availabilityData, eventData) {
if (DEBUG_SELECTS) {
console.log('🔍 [MERGE] Fusion des données', { availabilityData, eventData });
}
const merged = { ...availabilityData };
if (!eventData) return merged;
const extendedProps = eventData.extendedProps || {};
// Helper pour fusionner un tableau d'entités
function mergeEntities(availabilityEntities, eventEntityId, eventEntityData) {
if (!eventEntityId || !eventEntityData) {
return availabilityEntities;
}
// Vérifier si l'entité existe déjà (conversion en string pour comparaison)
const eventEntityIdStr = eventEntityId != null ? eventEntityId.toString() : null;
const exists = availabilityEntities.some(entity =>
entity.id != null && entity.id.toString() === eventEntityIdStr
);
if (!exists) {
return [...availabilityEntities, eventEntityData];
}
return availabilityEntities;
}
// Fusionner chaque type d'entité
if (extendedProps.id_beneficiaire && eventData.beneficiaire) {
merged.beneficiaires = mergeEntities(
merged.beneficiaires || [],
extendedProps.id_beneficiaire,
{
id: extendedProps.id_beneficiaire,
nom: eventData.beneficiaire.nom + ' ' + (eventData.beneficiaire.prenom || ''),
prenom: eventData.beneficiaire.prenom
}
);
}
if (extendedProps.id_intervenant && eventData.intervenant) {
merged.intervenants = mergeEntities(
merged.intervenants || [],
extendedProps.id_intervenant,
{
id: extendedProps.id_intervenant,
nom: eventData.intervenant.nom + ' ' + (eventData.intervenant.prenom || ''),
prenom: eventData.intervenant.prenom
}
);
}
if (extendedProps.id_traducteur && eventData.traducteur) {
merged.traducteurs = mergeEntities(
merged.traducteurs || [],
extendedProps.id_traducteur,
{
id: extendedProps.id_traducteur,
nom: eventData.traducteur.nom + ' ' + (eventData.traducteur.prenom || ''),
prenom: eventData.traducteur.prenom
}
);
}
if (extendedProps.id_local && eventData.local) {
merged.locaux = mergeEntities(
merged.locaux || [],
extendedProps.id_local,
{
id: extendedProps.id_local,
nom: eventData.local.nom
}
);
}
if (extendedProps.langue || eventData.langue) {
const langueId = extendedProps.langue || eventData.langue;
let langueNom = 'Langue';
let langueSlug = null;
if (typeof eventData.langue === 'object' && eventData.langue) {
langueNom = eventData.langue.nom || 'Langue';
langueSlug = eventData.langue.slug || eventData.langue.id || langueId;
} else {
// Vérifier si langue_label existe et n'est pas égal à l'ID
if (eventData.langue_label && eventData.langue_label !== langueId) {
langueNom = eventData.langue_label;
} else if (extendedProps.langue_label && extendedProps.langue_label !== langueId) {
langueNom = extendedProps.langue_label;
}
langueSlug = langueId;
}
const langueData = { id: langueId, nom: langueNom };
if (langueSlug) langueData.slug = langueSlug;
merged.langues = mergeEntities(
merged.langues || [],
langueId,
langueData
);
}
if (extendedProps.id_departement || eventData.id_departement) {
const departementId = extendedProps.id_departement || eventData.id_departement;
const departementNom = eventData.departement?.nom || eventData.departement || 'Département';
merged.departements = mergeEntities(
merged.departements || [],
departementId,
{
id: departementId,
nom: departementNom
}
);
}
if (extendedProps.id_type_intervention || eventData.id_type_intervention) {
const typeInterventionId = extendedProps.id_type_intervention || eventData.id_type_intervention;
const typeInterventionNom = eventData.type_intervention?.nom || eventData.type_intervention || 'Type d\'intervention';
merged.types_intervention = mergeEntities(
merged.types_intervention || [],
typeInterventionId,
{
id: typeInterventionId,
nom: typeInterventionNom
}
);
}
return merged;
}
/**
* Filtre un select selon les items disponibles
* @param {string} selectId - ID du select
* @param {Array} availableItems - Items disponibles
*/
function filterSelect(selectId, availableItems) {
const select = document.getElementById(selectId);
if (!select || select.tagName !== 'SELECT') return;
if (!Array.isArray(availableItems)) {
if (DEBUG_SELECTS) console.warn(`Items invalides pour ${selectId}:`, availableItems);
return;
}
// Créer un ensemble des IDs disponibles
const availableIds = new Set(
availableItems
.filter(item => item && item.id != null)
.map(item => item.id.toString())
);
// Pour les langues, créer aussi un set des slugs
const availableSlugs = selectId === 'langue' ? new Set(
availableItems
.filter(item => item != null)
.flatMap(item => {
const slugs = [];
if (item.slug != null) slugs.push(item.slug.toString());
if (item.id != null && isNaN(item.id)) slugs.push(item.id.toString());
return slugs;
})
) : new Set();
const currentValue = select.value;
// Parcourir les options du select
if (!select.options) return;
Array.from(select.options).forEach(option => {
if (option.value === '') {
option.style.display = '';
return;
}
const isCurrentlySelected = option.value === currentValue;
let isAvailable = availableIds.has(option.value);
// Pour les langues, vérifier aussi le slug
if (!isAvailable && selectId === 'langue') {
const optionSlug = option.getAttribute('data-slug');
if (optionSlug && availableSlugs.has(optionSlug)) {
isAvailable = true;
} else if (availableSlugs.has(option.value)) {
isAvailable = true;
} else if (availableSlugs.size > 0) {
const optionText = option.text.toLowerCase().trim();
if (availableSlugs.has(optionText)) {
isAvailable = true;
} else {
const normalizedText = optionText.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
if (availableSlugs.has(normalizedText)) {
isAvailable = true;
}
}
}
}
// Afficher ou cacher l'option
if (isAvailable || isCurrentlySelected) {
option.style.display = '';
option.disabled = false;
} else {
option.style.display = 'none';
option.disabled = true;
}
});
// Mettre à jour Select2 si initialisé
if (window.jQuery && jQuery(select).hasClass('select2-hidden-accessible')) {
jQuery(select).trigger('change.select2');
}
}
/**
* Filtre les options des selects selon les disponibilités
* @param {Object} data - Données de disponibilité
* @param {Object} currentEventData - Données de l'événement actuel
*/
function filterSelectOptions(data, currentEventData = null) {
if (DEBUG_SELECTS) {
console.log('🎯 [FILTER] Filtrage des selects', { data });
}
// Filtrer chaque type de select
filterSelect('id_beneficiaire', data.beneficiaires || []);
filterSelect('id_intervenant', data.intervenants || []);
filterSelect('id_traducteur', data.traducteurs || []);
filterSelect('id_local', data.locaux || []);
filterSelect('id_departement', data.departements || []);
filterSelect('id_type_intervention', data.types_intervention || []);
// Filtrer les langues selon langues_disponibles si c'est une permanence
const extendedProps = currentEventData?.extendedProps || {};
const isPermanence = extendedProps.type === 'permanence';
const languesDisponibles = extendedProps.langues_disponibles;
const langueSelect = document.getElementById('langue');
if (langueSelect && isPermanence && languesDisponibles && typeof languesDisponibles === 'string' && languesDisponibles.trim() !== '') {
const languesPermises = languesDisponibles.split(',').map(l => l.trim()).filter(l => l !== '');
if (languesPermises.length > 0) {
// Parcourir les options et cacher celles dont le data-slug n'est pas dans langues_disponibles
const currentValue = langueSelect.value; // Récupérer la valeur actuellement sélectionnée
Array.from(langueSelect.options).forEach(option => {
if (option.value === '') {
// Garder l'option vide visible
option.style.display = '';
option.disabled = false;
return;
}
// Garder l'option sélectionnée visible même si elle n'est pas dans langues_disponibles
const isCurrentlySelected = option.value === currentValue;
const optionSlug = option.getAttribute('data-slug');
const isPermise = optionSlug && languesPermises.includes(optionSlug);
if (isPermise || isCurrentlySelected) {
option.style.display = '';
option.disabled = false;
} else {
option.style.display = 'none';
option.disabled = true;
}
});
} else {
// Si langues_disponibles est vide, afficher toutes les options
Array.from(langueSelect.options).forEach(option => {
option.style.display = '';
option.disabled = false;
});
}
} else {
// Pas de permanence ou pas de langues_disponibles : afficher toutes les options
if (langueSelect) {
Array.from(langueSelect.options).forEach(option => {
option.style.display = '';
option.disabled = false;
});
}
// Ne pas filtrer les langues en mode edit (quand l'événement a un ID)
// Le filtrage par disponibilités ne doit s'appliquer qu'en mode création
const isEditMode = currentEventData && (currentEventData.id || currentEventData.extendedProps?.id);
if (!isEditMode) {
filterSelect('langue', data.langues || []);
}
}
}
/**
* Réinitialise le display des options du select langue
* À appeler lors de la fermeture du modal pour restaurer toutes les options
*/
export function resetLangueSelectDisplay() {
const langueSelect = document.getElementById('langue');
if (!langueSelect || langueSelect.tagName !== 'SELECT') return;
// Réinitialiser le display de toutes les options
Array.from(langueSelect.options).forEach(option => {
option.style.display = '';
option.disabled = false;
});
}
/**
* Présélectionne les valeurs dans les selects
* @param {Object} eventData - Données de l'événement
*/
function preselectValues(eventData) {
if (!eventData) return;
const extendedProps = eventData.extendedProps || {};
// Mapper les champs à présélectionner
const fieldsToPreselect = {
'id_beneficiaire': extendedProps.id_beneficiaire,
'id_intervenant': extendedProps.id_intervenant,
'id_traducteur': extendedProps.id_traducteur,
'id_local': extendedProps.id_local,
'id_departement': extendedProps.id_departement,
'id_type_intervention': extendedProps.id_type_intervention,
'langue': extendedProps.langue || eventData.langue
};
Object.entries(fieldsToPreselect).forEach(([fieldId, value]) => {
if (!value) return;
const element = document.getElementById(fieldId);
if (!element || element.tagName !== 'SELECT') return;
// Tracer spécifiquement pour le champ langue
if (fieldId === 'langue') {
const currentValue = element.value || '';
const isSelect2 = window.jQuery && jQuery(element).hasClass('select2-hidden-accessible');
const hasSkipSelect2 = element.classList.contains('skip-select2');
const optionCount = element.options.length;
const optionExists = Array.from(element.options).some(opt => opt.value === value.toString());
console.log('🟣 [LANGUE] preselectValues - Valeur actuelle:', currentValue, '| Valeur à appliquer:', value, '| Source:', extendedProps.langue ? 'extendedProps' : eventData.langue ? 'eventData' : 'N/A', '| Select2:', isSelect2, '| skip-select2:', hasSkipSelect2, '| Options:', optionCount, '| Option existe:', optionExists);
}
// Vérifier que l'option existe dans le select avant de la sélectionner
const optionExists = Array.from(element.options).some(opt => opt.value === value.toString());
if (!optionExists) {
if (fieldId === 'langue') {
console.warn(`⚠️ [LANGUE] Option avec valeur "${value}" non trouvée dans le select`);
} else {
console.warn(`⚠️ Option avec valeur "${value}" non trouvée dans le select ${fieldId}`);
}
return;
}
// Si Select2 est actif
if (window.jQuery && jQuery(element).hasClass('select2-hidden-accessible')) {
// Définir la valeur et déclencher l'événement change.select2
jQuery(element).val(value).trigger('change.select2');
} else {
// Select natif
element.value = value;
// Déclencher l'événement change pour notifier les autres listeners
element.dispatchEvent(new Event('change', { bubbles: true }));
}
// Vérifier la valeur après application pour le champ langue
if (fieldId === 'langue') {
const finalValue = element.value || '';
const selectedOption = element.options[element.selectedIndex];
const selectedText = selectedOption ? selectedOption.text : 'N/A';
console.log('🟠 [LANGUE] preselectValues - Valeur après application:', finalValue, '| Option sélectionnée:', selectedText, '| selectedIndex:', element.selectedIndex);
}
});
}
/**
* Population des selects selon les disponibilités
* @param {Object} eventData - Données de l'événement
*/
export async function populateSelects(eventData = null) {
// Vérifier si une exécution est déjà en cours
if (isPopulateSelectsRunning) {
if (DEBUG_SELECTS) console.warn('⚠️ populateSelects déjà en cours');
return;
}
isPopulateSelectsRunning = true;
isUpdatingSelects = true;
try {
const dateInput = document.getElementById('date_rdv');
const heureInput = document.getElementById('heure_rdv');
if (!dateInput || !heureInput) {
console.warn('Champs date/heure non trouvés');
return;
}
const date = dateInput.value;
const heure = heureInput.value;
if (!date || !heure) {
console.warn('Date et heure requises pour filtrer les disponibilités');
return;
}
// Créer une clé de cache
const cacheKey = `${date}|${heure}|${eventData ? eventData.id || 'new' : 'new'}`;
// Vérifier le cache
if (disponibilitesCache.has(cacheKey)) {
const cachedData = disponibilitesCache.get(cacheKey);
const mergedData = mergeEventDataWithAvailability(cachedData, eventData);
filterSelectOptions(mergedData, eventData);
// S'assurer que Select2 est initialisé avant de présélectionner
initializeSelect2();
// Petit délai pour s'assurer que le filtrage des langues est bien appliqué avant la présélection
// Et que les options sont bien dans le DOM
setTimeout(() => {
preselectValues(eventData);
}, 100);
return;
}
// Éviter les appels simultanés
const currentCall = `${date}|${heure}`;
if (lastPopulateCall === currentCall) return;
lastPopulateCall = currentCall;
// Appel API
const params = { date, heure };
const data = await getFilters('disponibilites', params);
// Mettre en cache (expire après 5 minutes)
disponibilitesCache.set(cacheKey, data);
setTimeout(() => disponibilitesCache.delete(cacheKey), 5 * 60 * 1000);
// Fusionner et filtrer
const mergedData = mergeEventDataWithAvailability(data, eventData);
filterSelectOptions(mergedData, eventData);
// S'assurer que Select2 est initialisé avant de présélectionner
initializeSelect2();
// Petit délai pour s'assurer que le filtrage des langues est bien appliqué avant la présélection
// Et que les options sont bien dans le DOM
setTimeout(() => {
preselectValues(eventData);
}, 100);
} catch (error) {
console.error('Erreur lors de la récupération des disponibilités:', error);
notifyError('Erreur lors de la récupération des disponibilités');
} finally {
isPopulateSelectsRunning = false;
isUpdatingSelects = false;
}
}
/**
* Filtre les traducteurs selon la langue sélectionnée
*/
export function filterTraducteursByLangue() {
if (isUpdatingSelects || isFilteringTraducteurs) return;
isFilteringTraducteurs = true;
try {
const langueSelect = document.getElementById('langue');
const traducteurSelect = document.getElementById('id_traducteur');
if (!langueSelect || !traducteurSelect || traducteurSelect.tagName !== 'SELECT') {
return;
}
const selectedLangue = langueSelect.value ? langueSelect.value.toString() : '';
const currentTraducteurValue = traducteurSelect.value;
// Si aucune langue sélectionnée, afficher tous les traducteurs
if (!selectedLangue) {
if (traducteurSelect.options) {
Array.from(traducteurSelect.options).forEach(option => {
option.style.display = '';
option.disabled = false;
});
if (window.jQuery && jQuery(traducteurSelect).hasClass('select2-hidden-accessible')) {
jQuery(traducteurSelect).trigger('change.select2');
}
}
return;
}
// Récupérer le slug de la langue
const langueOption = langueSelect.options[langueSelect.selectedIndex];
let langueSlug = null;
if (langueOption && langueOption.dataset.slug) {
langueSlug = langueOption.dataset.slug;
} else {
console.warn('Aucun slug disponible pour la langue sélectionnée');
return;
}
// Filtrer les traducteurs
if (traducteurSelect.options) {
Array.from(traducteurSelect.options).forEach(option => {
if (option.value === '') {
option.style.display = '';
return;
}
const traducteurLangues = option.getAttribute('data-langue');
const isCurrentlySelected = option.value === currentTraducteurValue;
if (!traducteurLangues) {
option.style.display = isCurrentlySelected ? '' : 'none';
option.disabled = !isCurrentlySelected;
} else {
const languesArray = traducteurLangues.split(',');
const langueMatches = languesArray.some(lang => {
return lang.trim() === langueSlug || lang.trim() === selectedLangue;
});
if (langueMatches || isCurrentlySelected) {
option.style.display = '';
option.disabled = false;
} else {
option.style.display = 'none';
option.disabled = true;
}
}
});
// Si le traducteur actuel ne correspond plus, le vider
if (currentTraducteurValue && selectedLangue) {
const currentOption = traducteurSelect.options[traducteurSelect.selectedIndex];
if (currentOption && currentOption.value !== '' && currentOption.style.display === 'none') {
traducteurSelect.value = '';
if (window.jQuery && jQuery(traducteurSelect).hasClass('select2-hidden-accessible')) {
jQuery(traducteurSelect).val('').trigger('change.select2');
}
}
}
// Mettre à jour Select2
if (window.jQuery && jQuery(traducteurSelect).hasClass('select2-hidden-accessible')) {
jQuery(traducteurSelect).trigger('change.select2');
}
}
} finally {
isFilteringTraducteurs = false;
}
}
/**
* Obtient l'état du flag isUpdatingSelects
* @returns {boolean}
*/
export function getIsUpdatingSelects() {
return isUpdatingSelects;
}
/**
* Vide le cache des disponibilités
*/
export function clearDisponibilitesCache() {
disponibilitesCache.clear();
lastPopulateCall = null;
}
/**
* Initialise le listener pour afficher l'alerte "Personne en liste rouge"
*/
export function initializeBeneficiaireListeRougeAlert() {
const beneficiaireSelect = document.getElementById('id_beneficiaire');
const alertElement = document.getElementById('beneficiaire-liste-rouge-alert');
if (!beneficiaireSelect || !alertElement) {
return;
}
// Fonction pour vérifier et afficher l'alerte
const checkListeRouge = () => {
const selectedBeneficiaireId = beneficiaireSelect.value;
// Vérifier si le bénéficiaire est en liste rouge via crviACFData
if (selectedBeneficiaireId && window.crviACFData && window.crviACFData.beneficiaires) {
const beneficiaireData = window.crviACFData.beneficiaires[selectedBeneficiaireId];
if (beneficiaireData && beneficiaireData.personne_en_liste_rouge) {
alertElement.style.display = 'block';
} else {
alertElement.style.display = 'none';
}
} else {
alertElement.style.display = 'none';
}
};
// Écouter les changements du select
beneficiaireSelect.addEventListener('change', checkListeRouge);
// Écouter aussi les événements Select2 si présent
if (window.jQuery) {
jQuery(beneficiaireSelect).on('select2:select', checkListeRouge);
jQuery(beneficiaireSelect).on('select2:clear', checkListeRouge);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -85,6 +85,12 @@ function collectFilters() {
filters.annee = parseInt(anneeInput.value, 10);
}
// Statut
const statutSelect = document.getElementById('stats_statut');
if (statutSelect && statutSelect.value) {
filters.statut = statutSelect.value;
}
// Filtre permanence
const permanenceCheckbox = document.getElementById('stats_filtre_permanence');
if (permanenceCheckbox && permanenceCheckbox.checked) {
@ -160,7 +166,8 @@ async function loadEvents() {
// Afficher les résultats
displayEvents(result.events || []);
updateCounters(result.total || 0, result.filtered || 0);
// Afficher le nombre d'événements filtrés comme total, et le nombre d'événements sur la page courante comme affichés
updateCounters(result.filtered || 0, (result.events || []).length);
displayPagination(result.page || 1, result.total_pages || 0);
// IMPORTANT: Masquer le loader EN PREMIER, puis afficher le tableau
@ -178,8 +185,8 @@ async function loadEvents() {
table.style.display = 'table';
}
// Afficher la pagination si nécessaire
if (paginationContainer && result.total_pages > 0) {
// Afficher la pagination si nécessaire (seulement s'il y a plus d'une page)
if (paginationContainer && result.total_pages > 1) {
paginationContainer.style.display = 'block';
}

View File

@ -0,0 +1,440 @@
/**
* Gestion des filtres visuels de l'agenda admin
* Filtres par département et capacités de traduction
*/
import { getTraductionsCapacites } from './agenda-api.js';
class AgendaVisualFilters {
constructor() {
this.departements = window.crviACFData?.departements || {};
this.traductions = window.crviACFData?.traductions_capacites || {};
this.selectedDepartement = null;
this.selectedTraduction = null;
this.currentDateDebut = null;
this.currentDateFin = null;
this.calendarInstance = 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();
// Initialiser les dates depuis le calendrier si disponible
this.initializeDatesFromCalendar();
}
/**
* Initialise les dates depuis le calendrier au chargement
*/
initializeDatesFromCalendar() {
// Attendre que le calendrier soit disponible
const checkCalendar = () => {
if (window.currentCalendar) {
try {
const view = window.currentCalendar.view;
if (view && view.activeStart && view.activeEnd) {
const startStr = view.activeStart.toISOString().split('T')[0];
const endStr = view.activeEnd.toISOString().split('T')[0];
this.currentDateDebut = startStr;
this.currentDateFin = endStr;
console.log('📅 Dates initiales du calendrier:', { start: startStr, end: endStr });
// Recharger les capacités pour cette période initiale
this.reloadTraductionsCapacites(startStr, endStr);
}
} catch (error) {
console.warn('⚠️ Impossible de récupérer les dates initiales du calendrier:', error);
}
} else {
// Réessayer après un court délai
setTimeout(checkCalendar, 200);
}
};
// Attendre un peu plus longtemps pour que le calendrier soit complètement initialisé
setTimeout(checkCalendar, 500);
}
/**
* 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_filtre');
if (langueSelect) {
// Récupérer l'ID de la langue depuis les données de traduction
const traductionData = this.traductions[filters.traduction];
if (traductionData && traductionData.id) {
// Utiliser l'ID de la langue pour le filtre
langueSelect.value = traductionData.id;
} else {
// Fallback : utiliser le slug si l'ID n'est pas disponible
langueSelect.value = filters.traduction;
}
// Déclencher l'événement change pour que le système de filtrage existant prenne le relais
// Support pour Select2 si présent
if (window.jQuery && jQuery(langueSelect).hasClass('select2-hidden-accessible')) {
jQuery(langueSelect).trigger('change');
} else {
// Déclencher l'événement change natif
langueSelect.dispatchEvent(new Event('change', { bubbles: true }));
}
}
} else {
const langueSelect = document.getElementById('langue_filtre');
if (langueSelect) {
langueSelect.value = '';
// Déclencher l'événement change pour que le système de filtrage existant prenne le relais
// Support pour Select2 si présent
if (window.jQuery && jQuery(langueSelect).hasClass('select2-hidden-accessible')) {
jQuery(langueSelect).trigger('change');
} else {
// Déclencher l'événement change natif
langueSelect.dispatchEvent(new Event('change', { bubbles: true }));
}
}
}
// 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();
});
}
// Écouter les changements de vue/dates de FullCalendar
this.setupCalendarListeners();
}
/**
* Configure les écouteurs pour les changements de vue/dates du calendrier
*/
setupCalendarListeners() {
// Attendre que le calendrier soit disponible
const checkCalendar = () => {
if (window.currentCalendar) {
this.calendarInstance = window.currentCalendar;
// Écouter l'événement datesSet de FullCalendar (déclenché lors des changements de dates/vue)
this.calendarInstance.on('datesSet', (arg) => {
this.handleCalendarDatesChange(arg);
});
console.log('✅ Écouteur datesSet configuré pour les filtres visuels');
} else {
// Réessayer après un court délai
setTimeout(checkCalendar, 100);
}
};
checkCalendar();
}
/**
* Gère les changements de dates/vue du calendrier
*/
async handleCalendarDatesChange(arg) {
const startStr = arg.startStr.split('T')[0];
const endStr = arg.endStr.split('T')[0];
// Vérifier si les dates ont vraiment changé
if (this.currentDateDebut === startStr && this.currentDateFin === endStr) {
return; // Pas de changement, ne rien faire
}
this.currentDateDebut = startStr;
this.currentDateFin = endStr;
console.log('📅 Changement de dates/vue détecté:', { start: startStr, end: endStr, view: arg.view.type });
// Recharger les capacités de traduction pour la nouvelle période
await this.reloadTraductionsCapacites(startStr, endStr);
}
/**
* Recharge les capacités de traduction pour une période donnée
*/
async reloadTraductionsCapacites(date_debut, date_fin) {
try {
console.log('🔄 Rechargement des capacités de traduction pour:', { date_debut, date_fin });
const capacites = await getTraductionsCapacites(date_debut, date_fin);
if (capacites && typeof capacites === 'object') {
this.traductions = capacites;
// Sauvegarder la sélection actuelle
const currentSelection = this.selectedTraduction;
// Re-rendre les boutons avec les nouvelles données
this.renderTraductionsButtons();
// Restaurer la sélection si elle existe toujours
if (currentSelection && this.traductions[currentSelection]) {
this.selectedTraduction = currentSelection;
const btnId = `trad-${currentSelection}`;
const activeBtn = document.getElementById(btnId);
if (activeBtn) {
activeBtn.classList.add('active');
}
}
console.log('✅ Capacités de traduction mises à jour');
} else {
console.warn('⚠️ Aucune donnée de capacité reçue');
}
} catch (error) {
console.error('❌ Erreur lors du rechargement des capacités:', error);
}
}
/**
* 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;

View File

@ -1,15 +1,15 @@
{
"scripts": {
"dev": "vite --mode development",
"build": "vite build",
"watch": "vite build --watch",
"dev": "vite build --watch --mode development"
"watch": "vite build --watch --mode development"
},
"dependencies": {
"@fullcalendar/core": "^6.1.18",
"@fullcalendar/daygrid": "^6.1.18",
"@fullcalendar/timegrid": "^6.1.18",
"@fullcalendar/list": "^6.1.18",
"@fullcalendar/interaction": "^6.1.18",
"@fullcalendar/list": "^6.1.18",
"@fullcalendar/timegrid": "^6.1.18",
"bootstrap": "^5.3.7",
"select2": "^4.0.13",
"toastr": "^2.1.4"
@ -18,3 +18,4 @@
"vite": "^7.0.3"
}
}

View File

@ -1,89 +1,70 @@
import { defineConfig } from 'vite';
import { defineConfig } from 'vite'
export default defineConfig({
root: '.',
build: {
outDir: 'dist',
target: 'es2015',
minify: true,
rollupOptions: {
input: {
crvi_libraries: 'crvi_libraries.js',
crvi_main: 'crvi_main.js',
crvi_libraries: './crvi_libraries.js',
crvi_main: './crvi_main.js',
// Entrées CSS (OK avec Vite/Rollup)
crvi_main_css: '../css/crvi_main.css',
intervenant_profile_css: '../css/intervenant-profile.css',
traduction_langue_admin: 'traduction-langue-admin.js',
traduction_langue_admin_css: '../css/traduction-langue-admin.css',
traduction_langue_list: 'traduction-langue-list.js',
traduction_langue_list_css: '../css/traduction-langue-list.css'
traduction_langue_list_css: '../css/traduction-langue-list.css',
traduction_langue_admin: './traduction-langue-admin.js',
traduction_langue_list: './traduction-langue-list.js'
},
// Important: en "es", `globals` ne sert pas.
// Si tu veux éviter de bundler jquery/toastr/select2 car chargés via <script>:
// external: ['jquery', 'toastr', 'select2'],
output: {
format: 'es', // Format ES modules
format: 'es',
entryFileNames: '[name].min.js',
assetFileNames: (assetInfo) => {
// Renommer le CSS extrait automatiquement de crvi_libraries.js
if (assetInfo.name && (assetInfo.name.includes('crvi_libraries') && assetInfo.name.endsWith('.css'))) {
return 'crvi_libraries.min.css';
}
// Renommer le CSS des libraries (si entrée séparée)
if (assetInfo.name && assetInfo.name.includes('crvi_libraries_css')) {
return 'crvi_libraries.min.css';
}
// Renommer le fichier CSS principal en crvi_main.min.css
if (assetInfo.name && assetInfo.name.includes('crvi_main_css')) {
return 'crvi_main.min.css';
}
// Renommer le CSS du profil intervenant
if (assetInfo.name && assetInfo.name.includes('intervenant_profile_css')) {
return 'intervenant-profile.min.css';
}
// Renommer le CSS de traduction-langue admin
if (assetInfo.name && assetInfo.name.includes('traduction_langue_admin_css')) {
return 'traduction-langue-admin.min.css';
}
// Renommer le CSS de traduction-langue list
if (assetInfo.name && assetInfo.name.includes('traduction_langue_list_css')) {
return 'traduction-langue-list.min.css';
}
// Pour les autres assets CSS, utiliser le pattern par défaut
if (assetInfo.name && assetInfo.name.endsWith('.css')) {
return '[name].min.css';
}
return '[name].min.[ext]';
},
// Séparer les librairies externes des modules locaux
manualChunks: (id) => {
// Si c'est le fichier crvi_libraries.js, tout va dans crvi_libraries
if (id.includes('crvi_libraries.js')) {
return 'crvi_libraries';
}
// Détecter les node_modules (librairies externes)
if (id.includes('node_modules')) {
// Séparer Bootstrap, Toastr, FullCalendar et leurs dépendances dans le chunk libraries
if (id.includes('bootstrap') ||
id.includes('toastr') ||
id.includes('select2') ||
id.includes('@fullcalendar') ||
id.includes('@popperjs') ||
id.includes('jquery')) {
return 'crvi_libraries';
}
// Autres node_modules - inclure dans libraries aussi
return 'crvi_libraries';
}
// Les modules locaux restent dans crvi_main
return null;
},
chunkFileNames: '[name].min.js',
globals: {
'jquery': 'jQuery',
'toastr': 'toastr',
'select2': 'select2'
assetFileNames: (assetInfo) => {
const name = assetInfo.name || ''
// CSS extrait depuis une entry JS (ex: crvi_libraries.js -> crvi_libraries.css)
if (name.includes('crvi_libraries') && name.endsWith('.css')) {
return 'crvi_libraries.min.css'
}
// Entrées CSS séparées
if (name.includes('crvi_main_css')) return 'crvi_main.min.css'
if (name.includes('intervenant_profile_css')) return 'intervenant-profile.min.css'
if (name.includes('traduction_langue_admin_css')) return 'traduction-langue-admin.min.css'
if (name.includes('traduction_langue_list_css')) return 'traduction-langue-list.min.css'
// Autres CSS / assets
if (name.endsWith('.css')) return '[name].min.css'
return '[name].min.[ext]'
},
manualChunks: (id) => {
// Node_modules -> crvi_libraries (bundle commun)
if (id.includes('node_modules')) return 'crvi_libraries'
// Optionnel: forcer aussi certains fichiers locaux dans crvi_libraries
if (id.includes('crvi_libraries.js')) return 'crvi_libraries'
return null
}
}
},
// Forcer la transpilation pour compatibilité navigateur
target: 'es2015',
minify: true // Utilise esbuild par défaut (plus rapide que terser)
}
},
define: {
global: 'window'
}
});
})

View File

@ -7,6 +7,7 @@
<section class="agenda-container">
<!--filters-->
<section class="filters-container">
<form class="filters" method="get" onsubmit="return false;">
@ -39,13 +40,29 @@
</select>
</div>
<div class="filter">
<label for="type_intervention">Type d'intervention</label>
<select id="type_intervention" name="type_intervention" class="select2">
<label for="departement">Département</label>
<select id="departement" name="departement" class="select2">
<option value="">Tous</option>
<!-- Options dynamiques -->
<?php
foreach ($types_intervention as $type_intervention) {
echo '<option value="' . $type_intervention['id'] . '">' . $type_intervention['nom'] . '</option>';
foreach ($departements as $departement) {
echo '<option value="' . $departement['id'] . '">' . $departement['nom'] . '</option>';
}
?>
</select>
</div>
<div class="filter" style="display: none;">
<label for="type_intervention">Type d'intervention</label>
<select id="type_intervention" name="type_intervention" class="select2">
<option value="">Tous</option>
<!-- Options dynamiques groupées par département -->
<?php
foreach ($types_intervention_groupes as $departement_id => $departement_data) {
echo '<optgroup label="' . esc_attr($departement_data['nom']) . '">';
foreach ($departement_data['types'] as $type_intervention) {
echo '<option value="' . esc_attr($type_intervention['id']) . '">' . esc_html($type_intervention['nom']) . '</option>';
}
echo '</optgroup>';
}
?>
</select>
@ -63,8 +80,8 @@
</select>
</div>
<div class="filter">
<label for="langue">Langue du rendez-vous</label>
<select id="langue" name="langue" class="select2">
<label for="langue_filtre">Langue du rendez-vous</label>
<select id="langue_filtre" name="langue" class="select2">
<option value="">Toutes</option>
<!-- Options dynamiques -->
<?php
@ -110,6 +127,43 @@
</form>
</section>
<!-- Filtres visuels -->
<section class="visual-filters-container mb-4">
<div class="card crvi-quick-filters">
<div class="card-header d-flex justify-content-between align-items-center">
<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" data-bs-toggle="collapse" data-bs-target="#visualFiltersCollapse" aria-expanded="true" aria-controls="visualFiltersCollapse">
<i class="fas fa-chevron-down transition-transform" 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;">
@ -124,4 +178,29 @@
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) {
// Définir l'état initial de l'icône
toggleIcon.style.transition = 'transform 0.3s ease';
if (collapseElement.classList.contains('show')) {
toggleIcon.style.transform = 'rotate(180deg)';
}
// Écouter les événements de collapse
collapseElement.addEventListener('show.bs.collapse', function() {
toggleIcon.style.transform = 'rotate(180deg)';
});
collapseElement.addEventListener('hide.bs.collapse', function() {
toggleIcon.style.transform = 'rotate(0deg)';
});
}
});
</script>
</div>

View File

@ -81,11 +81,16 @@
<label for="stats_annee" style="display: block; margin-bottom: 5px; font-weight: 500;"><?php esc_html_e('Année', 'esi_crvi_agenda'); ?></label>
<input type="number" id="stats_annee" name="annee" min="2000" max="2100" placeholder="<?php echo date('Y'); ?>" class="form-control" style="width: 100%;">
</div>
<div class="filter" style="display: flex; flex-direction: column; gap: 10px;">
<label for="stats_filtre_permanence" style="display: flex; align-items: center; gap: 8px; cursor: pointer; margin-top: 25px;">
<input type="checkbox" id="stats_filtre_permanence" name="filtre_permanence" value="permanence">
<span><?php esc_html_e('Afficher uniquement les permanences', 'esi_crvi_agenda'); ?></span>
</label>
<div class="filter">
<label for="stats_statut" style="display: block; margin-bottom: 5px; font-weight: 500;"><?php esc_html_e('Statut', 'esi_crvi_agenda'); ?></label>
<select id="stats_statut" name="statut" class="select2 form-control" style="width: 100%;">
<option value=""><?php esc_html_e('Tous', 'esi_crvi_agenda'); ?></option>
<option value="prevu"><?php esc_html_e('Prévu', 'esi_crvi_agenda'); ?></option>
<option value="cloture"><?php esc_html_e('Clôturé', 'esi_crvi_agenda'); ?></option>
<option value="absence"><?php esc_html_e('Absence', 'esi_crvi_agenda'); ?></option>
<option value="annule"><?php esc_html_e('Annulé', 'esi_crvi_agenda'); ?></option>
<option value="non_tenu"><?php esc_html_e('Non tenu', 'esi_crvi_agenda'); ?></option>
</select>
</div>
<div class="filter" style="display: flex; gap: 10px; justify-content: flex-end; align-items: end;">
<button type="button" id="stats_filterBtn" class="btn btn-primary" style="min-width: 100px;">

View File

@ -281,23 +281,34 @@ if (!is_wp_error($langues_terms) && !empty($langues_terms)) {
</h3>
</div>
<div class="card-body">
<div class="mb-3">
<label for="langues-permanences" class="form-label fw-bold">Langues disponibles (optionnel) :</label>
<select class="form-select form-select-lg" id="langues-permanences" name="langues[]" multiple>
<?php if (!empty($langues)): ?>
<?php if (!empty($langues)): ?>
<div class="mb-3">
<label for="langues-permanences" class="form-label fw-bold">Langues disponibles (optionnel) :</label>
<select class="form-select form-select-lg" id="langues-permanences" name="langues[]" multiple>
<?php foreach ($langues as $langue): ?>
<option value="<?php echo esc_attr($langue['id']); ?>">
<?php echo esc_html($langue['nom']); ?>
</option>
<?php endforeach; ?>
<?php else: ?>
<option value="">Aucune langue disponible</option>
<?php endif; ?>
</select>
<small class="form-text text-muted">
Sélectionnez une ou plusieurs langues pour ces permanences.
</small>
</div>
</select>
<small class="form-text text-muted">
Sélectionnez une ou plusieurs langues pour ces permanences. Maintenez Ctrl (ou Cmd sur Mac) pour sélectionner plusieurs langues.
</small>
</div>
<?php else: ?>
<div class="alert alert-warning" role="alert">
<i class="fas fa-exclamation-triangle me-2"></i>
<strong>Aucune langue disponible</strong>
<p class="mb-2 mt-2">Aucune langue n'a été créée dans la taxonomie "Langues". Les permanences seront créées sans langue associée.</p>
<p class="mb-0">
Pour ajouter des langues, rendez-vous dans
<a href="<?php echo esc_url(admin_url('edit-tags.php?taxonomy=langue&post_type=traducteur')); ?>" class="alert-link" target="_blank">
Traducteurs &gt; Langues
<i class="fas fa-external-link-alt ms-1"></i>
</a>
</p>
</div>
<?php endif; ?>
</div>
</div>

View File

@ -0,0 +1,223 @@
<?php
/**
* Template email : Notification de conflit de disponibilité
*
* Variables disponibles :
* @var WP_User $user L'utilisateur (intervenant) concerné
* @var array $conflicts Les conflits détectés
*/
if (!defined('ABSPATH')) {
exit;
}
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Alerte : Conflit de disponibilité</title>
<style>
body {
font-family: Arial, sans-serif;
line-height: 1.6;
color: #333;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background-color: #f5f5f5;
}
.email-container {
background-color: #ffffff;
border-radius: 8px;
padding: 30px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.header {
background-color: #dc3545;
color: #ffffff;
padding: 20px;
border-radius: 6px;
margin-bottom: 25px;
}
.header h1 {
margin: 0;
font-size: 24px;
}
.intervenant-info {
background-color: #f8f9fa;
padding: 15px;
border-radius: 6px;
margin-bottom: 25px;
}
.intervenant-info h2 {
margin-top: 0;
color: #333;
font-size: 18px;
}
.intervenant-info p {
margin: 5px 0;
}
.conflict-section {
margin-bottom: 30px;
border-left: 4px solid #dc3545;
padding-left: 20px;
}
.conflict-section h3 {
color: #dc3545;
margin-top: 0;
font-size: 18px;
}
.conflict-period {
background-color: #fff3cd;
padding: 12px;
border-radius: 4px;
margin-bottom: 15px;
border-left: 3px solid #ffc107;
}
.event-item {
background-color: #f8f9fa;
padding: 12px;
margin: 10px 0;
border-radius: 4px;
border-left: 3px solid #007bff;
}
.event-item strong {
color: #007bff;
}
.event-details {
margin-top: 8px;
font-size: 14px;
color: #666;
}
.event-details span {
display: inline-block;
margin-right: 15px;
}
.beneficiaires-list {
margin-top: 5px;
padding-left: 0;
color: #495057;
}
.footer {
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #dee2e6;
color: #666;
font-size: 14px;
}
.footer ul {
padding-left: 20px;
}
.footer li {
margin-bottom: 8px;
}
</style>
</head>
<body>
<div class="email-container">
<div class="header">
<h1>⚠️ Alerte : Conflit de disponibilité détecté</h1>
</div>
<div class="intervenant-info">
<h2>Intervenant concerné</h2>
<p><strong>Nom :</strong> <?php echo esc_html($user->display_name); ?></p>
<p><strong>Email :</strong> <?php echo esc_html($user->user_email); ?></p>
</div>
<p style="font-size: 16px; margin-bottom: 25px;">
Des événements sont planifiés pendant les périodes d'indisponibilité de cet intervenant.
Veuillez vérifier et prendre les mesures nécessaires.
</p>
<?php foreach ($conflicts as $key => $conflict): ?>
<div class="conflict-section">
<?php if ($key === 'jours_indisponibles'): ?>
<!-- Conflit de jours indisponibles -->
<h3>Jours non disponibles</h3>
<p><strong><?php echo count($conflict['events']); ?> événement(s)</strong> planifié(s) sur des jours où l'intervenant n'est pas disponible.</p>
<?php foreach ($conflict['events'] as $event): ?>
<?php
$enriched = $event['enriched'] ?? null;
if ($enriched):
$date_formatted = date_i18n('d/m/Y', strtotime($enriched['date_rdv']));
$day_name = date_i18n('l', strtotime($enriched['date_rdv']));
?>
<div class="event-item">
<strong><?php echo esc_html($date_formatted); ?> (<?php echo esc_html($day_name); ?>) à <?php echo esc_html(substr($enriched['heure_rdv'], 0, 5)); ?></strong>
<div class="event-details">
<span><strong>Type :</strong> <?php echo esc_html(ucfirst($enriched['type_rdv'])); ?></span>
<span><strong>Département :</strong> <?php echo esc_html($enriched['departement']); ?></span>
<span><strong>Type intervention :</strong> <?php echo esc_html($enriched['type_intervention']); ?></span>
<?php if (!empty($enriched['beneficiaires'])): ?>
<div class="beneficiaires-list">
<strong>Bénéficiaire(s) :</strong> <?php echo esc_html(implode(', ', $enriched['beneficiaires'])); ?>
</div>
<?php endif; ?>
<span style="color: #999;"><strong>ID :</strong> <?php echo esc_html($enriched['id']); ?></span>
</div>
</div>
<?php endif; ?>
<?php endforeach; ?>
<?php else: ?>
<!-- Conflit de période d'indisponibilité -->
<?php
$periode = $conflict['periode'];
$debut_formatted = date_i18n('d/m/Y', strtotime($periode['debut']));
$fin_formatted = date_i18n('d/m/Y', strtotime($periode['fin']));
?>
<h3><?php echo esc_html($periode['type']); ?></h3>
<div class="conflict-period">
<strong>Période :</strong> du <?php echo esc_html($debut_formatted); ?> au <?php echo esc_html($fin_formatted); ?>
</div>
<p><strong><?php echo count($conflict['events']); ?> événement(s)</strong> planifié(s) pendant cette période.</p>
<?php foreach ($conflict['events'] as $event): ?>
<?php
$enriched = $event['enriched'] ?? null;
if ($enriched):
$date_formatted = date_i18n('d/m/Y', strtotime($enriched['date_rdv']));
?>
<div class="event-item">
<strong><?php echo esc_html($date_formatted); ?> à <?php echo esc_html(substr($enriched['heure_rdv'], 0, 5)); ?></strong>
<div class="event-details">
<span><strong>Type :</strong> <?php echo esc_html(ucfirst($enriched['type_rdv'])); ?></span>
<span><strong>Département :</strong> <?php echo esc_html($enriched['departement']); ?></span>
<span><strong>Type intervention :</strong> <?php echo esc_html($enriched['type_intervention']); ?></span>
<?php if (!empty($enriched['beneficiaires'])): ?>
<div class="beneficiaires-list">
<strong>Bénéficiaire(s) :</strong> <?php echo esc_html(implode(', ', $enriched['beneficiaires'])); ?>
</div>
<?php endif; ?>
<span style="color: #999;"><strong>ID :</strong> <?php echo esc_html($enriched['id']); ?></span>
</div>
</div>
<?php endif; ?>
<?php endforeach; ?>
<?php endif; ?>
</div>
<?php endforeach; ?>
<div class="footer">
<p><strong>Actions recommandées :</strong></p>
<ul>
<li>Vérifier les événements listés ci-dessus</li>
<li>Contacter l'intervenant pour confirmer sa disponibilité</li>
<li>Annuler ou déplacer les événements en conflit si nécessaire</li>
<li>Ajuster les périodes d'indisponibilité si besoin</li>
</ul>
<p style="margin-top: 20px; color: #999;">
Cet email a été envoyé automatiquement par le système Agenda CRVI.
</p>
</div>
</div>
</body>
</html>

View File

@ -18,7 +18,7 @@ $user = wp_get_current_user();
<div class="row mb-4">
<div class="col-12">
<h1 class="h2 mb-2">
<i class="fas fa-calendar-alt me-2"></i>Mon Agenda
<!-- <i class="fas fa-calendar-alt me-2"></i> -->Mon Agenda
</h1>
<nav class="navbar navbar-expand-lg navbar-light bg-light rounded p-3">
<div class="container-fluid">
@ -96,14 +96,20 @@ $user = wp_get_current_user();
</select>
</div>
<div class="filter">
<label for="departement">Département</label>
<select id="departement" name="departement" class="select2">
<option value="">Tous</option>
</select>
</div>
<div class="filter" style="display: none;">
<label for="type_intervention">Type d'intervention</label>
<select id="type_intervention" name="type_intervention" class="select2">
<option value="">Tous</option>
</select>
</div>
<div class="filter">
<label for="langue">Langue</label>
<select id="langue" name="langue" class="select2">
<label for="langue_filtre">Langue</label>
<select id="langue_filtre" name="langue" class="select2">
<option value="">Toutes</option>
</select>
</div>
@ -167,6 +173,12 @@ $user = wp_get_current_user();
</select>
</div>
<div class="filter">
<label for="departement-colleagues">Département</label>
<select id="departement-colleagues" name="departement" class="select2">
<option value="">Tous</option>
</select>
</div>
<div class="filter" style="display: none;">
<label for="type_intervention-colleagues">Type d'intervention</label>
<select id="type_intervention-colleagues" name="type_intervention" class="select2">
<option value="">Tous</option>

View File

@ -18,7 +18,7 @@ $user = wp_get_current_user();
<div class="row mb-4">
<div class="col-12">
<h1 class="h2 mb-2">
<i class="fas fa-calendar-alt me-2"></i>Mon Agenda
<!-- <i class="fas fa-calendar-alt me-2"></i> -->Mon Agenda
</h1>
<nav class="navbar navbar-expand-lg navbar-light bg-light rounded p-3">
<div class="container-fluid">
@ -96,14 +96,20 @@ $user = wp_get_current_user();
</select>
</div>
<div class="filter">
<label for="departement">Département</label>
<select id="departement" name="departement" class="select2">
<option value="">Tous</option>
</select>
</div>
<div class="filter" style="display: none;">
<label for="type_intervention">Type d'intervention</label>
<select id="type_intervention" name="type_intervention" class="select2">
<option value="">Tous</option>
</select>
</div>
<div class="filter">
<label for="langue">Langue</label>
<select id="langue" name="langue" class="select2">
<label for="langue_filtre">Langue</label>
<select id="langue_filtre" name="langue" class="select2">
<option value="">Toutes</option>
</select>
</div>
@ -167,6 +173,12 @@ $user = wp_get_current_user();
</select>
</div>
<div class="filter">
<label for="departement-colleagues">Département</label>
<select id="departement-colleagues" name="departement" class="select2">
<option value="">Tous</option>
</select>
</div>
<div class="filter" style="display: none;">
<label for="type_intervention-colleagues">Type d'intervention</label>
<select id="type_intervention-colleagues" name="type_intervention" class="select2">
<option value="">Tous</option>

View File

@ -20,7 +20,7 @@ $today = date('d/m/Y');
<div class="row mb-4">
<div class="col-12">
<h1 class="h2 mb-2">
<i class="fas fa-home me-2"></i>Mon Espace Intervenant
<!-- <i class="fas fa-home me-2"></i> -->Mon Espace Intervenant
</h1>
<p class="text-muted">Bonjour <?php echo esc_html($intervenant_nom); ?></p>
</div>

View File

@ -34,7 +34,7 @@ if (!is_wp_error($langues_terms) && !empty($langues_terms)) {
<div class="row mb-4">
<div class="col-12">
<h1 class="h2 mb-2">
<i class="fas fa-clock me-2"></i>Mes Permanences
<!-- <i class="fas fa-clock me-2"></i> -->Mes Permanences
</h1>
<nav class="navbar navbar-expand-lg navbar-light bg-light rounded p-3">
<div class="container-fluid">

View File

@ -80,7 +80,7 @@ $jours_labels = [
<div class="row mb-4">
<div class="col-12">
<h1 class="h2 mb-2">
<i class="fas fa-user me-2"></i>Mon Profil
<!-- <i class="fas fa-user me-2"></i> -->Mon Profil
</h1>
<nav class="navbar navbar-expand-lg navbar-light bg-light rounded p-3">
<div class="container-fluid">

View File

@ -14,6 +14,11 @@ $modals_dir = dirname(__FILE__) . '/modals';
include $modals_dir . '/event-modal.php';
?>
<?php
// Modal de validation des présences (pour les rendez-vous de groupe)
include $modals_dir . '/event-check-presence-modal.php';
?>
<?php
// Modal de création de bénéficiaire
include $modals_dir . '/create-beneficiaire-modal.php';

View File

@ -15,6 +15,12 @@ $crvi_is_front_context = ($crvi_agenda_context !== 'admin');
<button type="button" class="btn-close" id="closeModalBtn" aria-label="Fermer"></button>
</div>
<div class="modal-body">
<!-- Alerte de statut (absent/présent) -->
<div class="alert alert-warning mb-3" id="statutAlert" style="display: none;">
<i class="fas fa-info-circle me-2"></i>
<span id="statutAlertText"></span>
</div>
<!-- Bloc lecture seule (vue) avec grille Bootstrap en 2 colonnes -->
<div id="eventViewBlock">
<div class="event-grid">
@ -220,10 +226,10 @@ $crvi_is_front_context = ($crvi_agenda_context !== 'admin');
</select>
</div>
</div>
<div class="col-md-6">
<div class="col-md-6" id="langue-container">
<div class="mb-3">
<label for="langue" class="form-label">Langue *</label>
<select class="form-select" id="langue" name="langue" required>
<select class="form-select skip-select2" id="langue" name="langue" required>
<option value="">Sélectionner...</option>
<?php
if (isset($langues_beneficiaire) && is_array($langues_beneficiaire)) {
@ -298,6 +304,11 @@ $crvi_is_front_context = ($crvi_agenda_context !== 'admin');
<i class="fas fa-exclamation-triangle text-danger me-1"></i>Ajouter bénéficiaire à la liste rouge
</label>
</div>
<!-- Alerte liste rouge (s'affiche si le bénéficiaire est en liste rouge) -->
<div class="alert alert-danger mt-2 mb-0" id="beneficiaire-liste-rouge-alert" style="font-size: 90%; display: none;">
<i class="fas fa-exclamation-triangle me-2"></i>
Personne en liste rouge
</div>
</div>
</div>
<div class="col-md-6">
@ -468,10 +479,13 @@ $crvi_is_front_context = ($crvi_agenda_context !== 'admin');
<div id="eventFormErrors" class="alert alert-danger d-none" role="alert"></div>
<!-- Boutons de changement de statut rapide (mode édition uniquement) -->
<div id="eventStatusButtons" style="display: none;">
<div id="eventStatusButtons">
<div class="btn-group" role="group">
<button type="button" class="btn btn-success btn-sm" id="markPresentBtn" title="Valider la présence">
<i class="fas fa-user-check me-1"></i>Valider présence
<button type="button" class="btn btn-success btn-sm individuel-only-button" id="markPresentBtn" title="Valider la présence" style="display: none;">
<i class="fas fa-user-check me-1"></i>Valider présences
</button>
<button type="button" class="btn btn-primary btn-sm groupe-only-button" id="showPresenceModalBtn" title="Afficher le modal de présence" style="display: none;">
<i class="fas fa-clipboard-list"></i> Gérer les présences
</button>
<button type="button" class="btn btn-warning btn-sm" id="markAbsentBtn" title="Marquer comme absent">
<i class="fas fa-user-times me-1"></i>Absent
@ -547,12 +561,20 @@ jQuery(document).ready(function($){
// Masquer le champ bénéficiaire pour les RDV de groupe
$('#id_beneficiaire').closest('.col-md-6').hide();
$('#id_beneficiaire').removeAttr('required');
// Afficher les boutons spécifiques aux groupes
$('.groupe-only-button').show();
// Masquer les boutons spécifiques aux individuels
$('.individuel-only-button').hide();
} else {
$('#groupeFields').hide();
$('#nb_participants').removeAttr('required');
// Afficher le champ bénéficiaire pour les RDV individuels
$('#id_beneficiaire').closest('.col-md-6').show();
$('#id_beneficiaire').attr('required', true);
// Masquer les boutons spécifiques aux groupes
$('.groupe-only-button').hide();
// Afficher les boutons spécifiques aux individuels
$('.individuel-only-button').show();
}
});
});