Compare commits
No commits in common. "main" and "master" have entirely different histories.
@ -79,15 +79,6 @@ 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',
|
||||
@ -131,61 +122,6 @@ 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);
|
||||
|
||||
@ -10,7 +10,6 @@ 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 {
|
||||
@ -93,11 +92,6 @@ 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', [
|
||||
@ -466,11 +460,35 @@ class CRVI_Event_Controller {
|
||||
}
|
||||
}
|
||||
|
||||
// Récupérer tous les départements actifs
|
||||
$departements = CRVI_Departement_Model::all(true, true);
|
||||
// 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 les types d'intervention groupés par département
|
||||
$types_intervention_groupes = self::get_types_intervention_by_departement();
|
||||
$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,
|
||||
];
|
||||
}
|
||||
|
||||
$response = [
|
||||
'intervenants' => $intervenants_formatted,
|
||||
@ -478,7 +496,7 @@ class CRVI_Event_Controller {
|
||||
'beneficiaires' => $beneficiaires,
|
||||
'langues' => $langues,
|
||||
'departements' => $departements,
|
||||
'types_intervention_groupes' => $types_intervention_groupes,
|
||||
'types_intervention' => $types_intervention,
|
||||
];
|
||||
|
||||
// Inclure les traducteurs seulement si la vérification a été faite
|
||||
@ -489,52 +507,6 @@ 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
|
||||
@ -595,21 +567,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 - 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 (seulement si on a trouvé le nom)
|
||||
// Ajouter le label de la langue pour l'affichage dans la modal
|
||||
if (!empty($langue_label)) {
|
||||
$event['langue_label'] = $langue_label;
|
||||
} else {
|
||||
// Ne pas assigner l'ID comme fallback - laisser le champ vide
|
||||
$event['langue_label'] = '';
|
||||
}
|
||||
}
|
||||
|
||||
@ -663,21 +626,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 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 (seulement si on a trouvé le nom)
|
||||
// Ajouter le label de la langue pour l'affichage dans la modal
|
||||
if (!empty($langue_label)) {
|
||||
$event['langue_label'] = $langue_label;
|
||||
} else {
|
||||
// Ne pas assigner l'ID comme fallback - laisser le champ vide
|
||||
$event['langue_label'] = '';
|
||||
}
|
||||
}
|
||||
|
||||
@ -701,26 +655,11 @@ 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) {
|
||||
@ -744,33 +683,7 @@ 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();
|
||||
@ -778,10 +691,6 @@ 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) {
|
||||
@ -791,156 +700,9 @@ 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
|
||||
@ -1192,34 +954,13 @@ 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_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) {
|
||||
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)) {
|
||||
$langues_valides[] = $langue_slug;
|
||||
}
|
||||
}
|
||||
@ -1671,10 +1412,6 @@ 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']);
|
||||
}
|
||||
|
||||
@ -1685,10 +1422,6 @@ 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']);
|
||||
}
|
||||
|
||||
@ -1784,12 +1517,6 @@ 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1959,26 +1686,6 @@ 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
|
||||
|
||||
@ -558,156 +558,4 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -171,10 +171,6 @@ 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']);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -185,7 +181,7 @@ class CRVI_Plugin {
|
||||
add_menu_page(
|
||||
__('Agenda CRVI', 'esi_crvi_agenda'),
|
||||
__('Agenda CRVI', 'esi_crvi_agenda'),
|
||||
'edit_posts',
|
||||
'manage_options',
|
||||
'crvi_agenda',
|
||||
[CRVI_Main_View::class, 'render_hub_admin_page'],
|
||||
'dashicons-calendar-alt',
|
||||
@ -197,7 +193,7 @@ class CRVI_Plugin {
|
||||
'crvi_agenda',
|
||||
__('Hub', 'esi_crvi_agenda'),
|
||||
__('Hub', 'esi_crvi_agenda'),
|
||||
'edit_posts',
|
||||
'manage_options',
|
||||
'crvi_agenda',
|
||||
[CRVI_Main_View::class, 'render_hub_admin_page']
|
||||
);
|
||||
@ -207,7 +203,7 @@ class CRVI_Plugin {
|
||||
'crvi_agenda',
|
||||
__('Stats', 'esi_crvi_agenda'),
|
||||
__('Stats', 'esi_crvi_agenda'),
|
||||
'edit_posts',
|
||||
'manage_options',
|
||||
'crvi_agenda_stats',
|
||||
[self::class, 'render_stats_page']
|
||||
);
|
||||
@ -217,7 +213,7 @@ class CRVI_Plugin {
|
||||
'crvi_agenda',
|
||||
__('Agenda', 'esi_crvi_agenda'),
|
||||
__('Agenda', 'esi_crvi_agenda'),
|
||||
'edit_posts',
|
||||
'manage_options',
|
||||
'crvi_agenda_hub',
|
||||
[CRVI_Agenda_View::class, 'render_agenda_page']
|
||||
);
|
||||
@ -227,7 +223,7 @@ class CRVI_Plugin {
|
||||
'crvi_agenda',
|
||||
__('Permanences', 'esi_crvi_agenda'),
|
||||
__('Permanences', 'esi_crvi_agenda'),
|
||||
'edit_posts',
|
||||
'manage_options',
|
||||
'crvi_agenda_permanences',
|
||||
[self::class, 'render_permanences_page']
|
||||
);
|
||||
@ -284,85 +280,6 @@ 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() {
|
||||
@ -411,13 +328,14 @@ 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 (inclut déjà agenda-visual-filters.css)
|
||||
// CSS principal du plugin - compilé par Vite
|
||||
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, inclut déjà agenda-visual-filters.js) - dépend des librairies
|
||||
// Script principal du plugin (modules locaux) - 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');
|
||||
@ -549,9 +467,7 @@ 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(),
|
||||
'beneficiaires' => self::get_beneficiaires_acf_data(),
|
||||
'traductions_capacites' => self::get_traductions_capacites_data()
|
||||
'indisponibilites_intervenants' => self::get_intervenants_disponibilites()
|
||||
];
|
||||
|
||||
wp_localize_script($script_handle, 'crviACFData', $acf_data);
|
||||
@ -924,70 +840,6 @@ 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
|
||||
*/
|
||||
@ -1096,7 +948,7 @@ class CRVI_Plugin {
|
||||
delete_transient('crvi_intervenants_disponibilites');
|
||||
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'crvi_agenda';
|
||||
$table_name = $wpdb->prefix . 'crvi_agenda_events';
|
||||
$conflicts = [];
|
||||
|
||||
// 1. Vérifier les jours de disponibilité
|
||||
@ -1271,9 +1123,6 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1310,9 +1159,6 @@ 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;
|
||||
|
||||
@ -1371,58 +1217,4 @@ 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;
|
||||
}
|
||||
}
|
||||
@ -493,12 +493,4 @@ 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()
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,7 +14,6 @@ class CRVI_Departement_Model extends Main_Model
|
||||
public static $acf_schema = [
|
||||
'nom' => 'text',
|
||||
'type_dinterventions' => 'repeater',
|
||||
'actif' => 'true_false',
|
||||
];
|
||||
|
||||
/**
|
||||
@ -38,11 +37,8 @@ class CRVI_Departement_Model extends Main_Model
|
||||
$data['id'] = $post->ID;
|
||||
} elseif ($field === 'nom') {
|
||||
$data['nom'] = $post->post_title;
|
||||
} 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;
|
||||
} elseif (property_exists(self::class, $field)) {
|
||||
$data[$field] = get_field($field, $post->ID);
|
||||
}
|
||||
}
|
||||
return new self($data);
|
||||
@ -54,10 +50,8 @@ 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, $only_active = true)
|
||||
public static function all($simple_list = false)
|
||||
{
|
||||
$posts = get_posts([
|
||||
'post_type' => 'departement',
|
||||
@ -65,16 +59,6 @@ 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 [
|
||||
|
||||
@ -594,15 +594,8 @@ 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;
|
||||
}
|
||||
|
||||
@ -610,15 +603,7 @@ 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;
|
||||
}
|
||||
|
||||
@ -860,7 +845,6 @@ 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)
|
||||
@ -965,38 +949,6 @@ 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');
|
||||
@ -1007,7 +959,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',
|
||||
'langues_disponibles', 'assign', 'modifie_par', 'date_modification'
|
||||
'assign', 'modifie_par', 'date_modification'
|
||||
];
|
||||
|
||||
$filtered_data = [];
|
||||
@ -1172,25 +1124,6 @@ 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')) {
|
||||
@ -1204,19 +1137,6 @@ 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];
|
||||
@ -1503,25 +1423,6 @@ 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')) {
|
||||
@ -1534,19 +1435,6 @@ 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];
|
||||
@ -1785,20 +1673,8 @@ 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']);
|
||||
|
||||
@ -118,6 +118,16 @@ 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',
|
||||
@ -137,24 +147,8 @@ class CRVI_TraductionLangue_Model extends Main_Model {
|
||||
$posts = \get_posts($args);
|
||||
$capacites = [];
|
||||
|
||||
// Filtrer par langue si demandé (le champ langue est un ACF taxonomy multi-select stocké en meta)
|
||||
if (!empty($filters['langue'])) {
|
||||
$langue_id_filter = (int) $filters['langue'];
|
||||
foreach ($posts as $post) {
|
||||
$capacite = self::load($post->ID);
|
||||
if ($capacite && !empty($capacite->langue)) {
|
||||
// Le champ langue est un tableau d'IDs
|
||||
$langue_ids = is_array($capacite->langue) ? $capacite->langue : [$capacite->langue];
|
||||
if (in_array($langue_id_filter, $langue_ids)) {
|
||||
$capacites[] = $capacite;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Pas de filtre par langue, charger toutes les capacités
|
||||
foreach ($posts as $post) {
|
||||
$capacites[] = self::load($post->ID);
|
||||
}
|
||||
foreach ($posts as $post) {
|
||||
$capacites[] = self::load($post->ID);
|
||||
}
|
||||
|
||||
// Mettre en cache si activé et sans filtres
|
||||
@ -627,7 +621,7 @@ class CRVI_TraductionLangue_Model extends Main_Model {
|
||||
|
||||
// Calculer la limite mensuelle pour la période principale
|
||||
$limite_mensuelle_principale = self::convertToMonthlyLimit(
|
||||
(int) $capacite->limite,
|
||||
$capacite->limite,
|
||||
$capacite->limite_par,
|
||||
$nb_semaines
|
||||
);
|
||||
@ -689,29 +683,15 @@ class CRVI_TraductionLangue_Model extends Main_Model {
|
||||
$result['total_available'] = max(0, $result['total'] - $result['total_used']);
|
||||
|
||||
// Calculer les totaux par période
|
||||
// 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];
|
||||
foreach ($result['by_periode'] as $periode => &$items) {
|
||||
$periode_total = 0;
|
||||
$periode_used = 0;
|
||||
$periode_available = 0;
|
||||
|
||||
// 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'];
|
||||
}
|
||||
}
|
||||
foreach ($items as $item) {
|
||||
$periode_total += $item['limite'];
|
||||
$periode_used += $item['used'];
|
||||
$periode_available += $item['available'];
|
||||
}
|
||||
|
||||
// Ajouter un résumé pour cette période
|
||||
@ -860,340 +840,6 @@ 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
|
||||
@ -1216,13 +862,9 @@ 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_crvi_dispos_langue_jours_' . $langue_id . '_%',
|
||||
'_transient_timeout_crvi_dispos_langue_jours_' . $langue_id . '_%'
|
||||
'_transient_timeout_crvi_dispos_langue_' . $langue_id . '_%'
|
||||
)
|
||||
);
|
||||
}
|
||||
@ -1231,9 +873,7 @@ 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_crvi_langues_capacites_acf_%'
|
||||
OR option_name LIKE '_transient_timeout_crvi_langues_capacites_acf_%'"
|
||||
OR option_name LIKE '_transient_timeout_crvi_all_langues_dispos_%'"
|
||||
);
|
||||
}
|
||||
|
||||
@ -1248,9 +888,7 @@ 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_crvi_langues_capacites_acf_%'
|
||||
OR option_name LIKE '_transient_timeout_crvi_langues_capacites_acf_%'"
|
||||
OR option_name LIKE '_transient_timeout_crvi_count_used_%'"
|
||||
);
|
||||
|
||||
// Invalider le cache général des capacités
|
||||
|
||||
@ -15,10 +15,7 @@ class CRVI_Agenda_View {
|
||||
$locals = CRVI_Local_Model::get_locals([],true);
|
||||
$intervenants = CRVI_Intervenant_Model::get_intervenants([],true);
|
||||
$departements = CRVI_Departement_Model::all(true);
|
||||
|
||||
// Récupérer les types d'intervention groupés par département
|
||||
$types_intervention_groupes = self::get_types_intervention_by_departement();
|
||||
|
||||
$types_intervention = CRVI_Type_Intervention_Model::all(true);
|
||||
$traducteurs = CRVI_Traducteur_Model::get_traducteurs([],true);
|
||||
$langues_beneficiaire = Api_Helper::get_languages(true);
|
||||
$genres = Api_Helper::get_acf_field_options('field_685e466352755');
|
||||
@ -48,50 +45,4 @@ 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;
|
||||
}
|
||||
}
|
||||
@ -1,276 +0,0 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
@ -143,270 +143,3 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -7,8 +7,5 @@
|
||||
/* 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';
|
||||
@ -1,17 +1,12 @@
|
||||
/* 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: 1060;
|
||||
z-index: 1050;
|
||||
background: white;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
@ -19,6 +14,7 @@
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||||
margin-top: 2px;
|
||||
width: 100%;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
|
||||
@ -11,7 +11,6 @@ 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
|
||||
@ -127,13 +126,6 @@ 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
|
||||
@ -159,8 +151,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
|
||||
// Initialiser Select2 sur les <select> (commun à tous les contextes)
|
||||
// Exclure les selects avec skip-select2 (comme le select langue dans le modal)
|
||||
jQuery('.select2:not(.skip-select2)').select2();
|
||||
jQuery('.select2').select2();
|
||||
|
||||
});
|
||||
|
||||
|
||||
@ -31,13 +31,7 @@ export function initializeAdminPermanences() {
|
||||
}
|
||||
|
||||
// Initialiser Select2 pour le champ langues
|
||||
// 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();
|
||||
initializeSelect2();
|
||||
|
||||
// Écouter les changements pour mettre à jour l'aperçu
|
||||
setupPreviewListeners();
|
||||
@ -52,109 +46,13 @@ 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 && 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({
|
||||
if (languesSelect && typeof jQuery !== 'undefined' && jQuery.fn.select2) {
|
||||
jQuery(languesSelect).select2({
|
||||
placeholder: 'Sélectionnez une ou plusieurs langues',
|
||||
allowClear: true,
|
||||
width: '100%',
|
||||
@ -164,58 +62,9 @@ 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
|
||||
@ -338,18 +187,6 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -363,8 +200,6 @@ 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();
|
||||
@ -382,37 +217,15 @@ function updatePreview() {
|
||||
}
|
||||
|
||||
// Calculer les tranches horaires à partir des heures sélectionnées
|
||||
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
|
||||
};
|
||||
});
|
||||
}
|
||||
// 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
|
||||
};
|
||||
});
|
||||
|
||||
// Calculer les dates à partir du mois de début sélectionné
|
||||
const [year, month] = moisDebut.split('-').map(Number);
|
||||
|
||||
@ -98,12 +98,3 @@ 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);
|
||||
}
|
||||
@ -1,8 +1,7 @@
|
||||
// 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 { openSubModal } from './agenda-modal.js';
|
||||
import { populateSelects } from './agenda-modal-select.js';
|
||||
import { populateSelects, preserveModalData } from './agenda-modal.js';
|
||||
|
||||
// Configuration des entités
|
||||
const ENTITY_CONFIG = {
|
||||
@ -98,18 +97,49 @@ function openCreateEntityModal(entityType) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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()
|
||||
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();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// 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()
|
||||
|
||||
@ -1,623 +0,0 @@
|
||||
// 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
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -1,10 +1,46 @@
|
||||
// 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
|
||||
@ -31,7 +67,37 @@ export function initializeFilters(calendar) {
|
||||
}
|
||||
|
||||
const apiEvents = await getEvents(params);
|
||||
const events = apiEvents.map(ev => mapEventToFullCalendar(ev));
|
||||
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
|
||||
}
|
||||
}));
|
||||
successCallback(events);
|
||||
} catch (e) {
|
||||
console.error('Erreur lors du chargement des événements:', e);
|
||||
@ -149,16 +215,10 @@ 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');
|
||||
if (typeIntervention && typeIntervention.value && typeIntervention.value.trim() !== '') {
|
||||
filters.type_intervention = typeIntervention.value;
|
||||
const typeIntervention = document.getElementById('type_intervention').value;
|
||||
if (typeIntervention && typeIntervention.trim() !== '') {
|
||||
filters.type_intervention = typeIntervention;
|
||||
}
|
||||
|
||||
// Bénéficiaire
|
||||
@ -168,7 +228,7 @@ function collectFilters() {
|
||||
}
|
||||
|
||||
// Langue
|
||||
const langue = document.getElementById('langue_filtre')?.value;
|
||||
const langue = document.getElementById('langue').value;
|
||||
if (langue && langue.trim() !== '') {
|
||||
filters.langue = langue;
|
||||
}
|
||||
|
||||
@ -3,7 +3,6 @@ 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';
|
||||
@ -13,6 +12,66 @@ 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');
|
||||
@ -80,7 +139,451 @@ export function initializeCalendar() {
|
||||
};
|
||||
const apiEvents = await getEvents(params);
|
||||
// Mapping des objets API vers le format FullCalendar
|
||||
const events = apiEvents.map(ev => mapEventToFullCalendar(ev));
|
||||
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
|
||||
}
|
||||
};
|
||||
});
|
||||
successCallback(events);
|
||||
} catch (e) {
|
||||
console.error('Erreur lors du chargement des événements:', e);
|
||||
@ -558,97 +1061,16 @@ export function initializeCalendar() {
|
||||
bgColor = window.crviAjax.couleur_permanence_non_attribuee;
|
||||
}
|
||||
} else if (isPermanenceAssigned) {
|
||||
// 2) Attribuée : ordre de priorité - département puis local
|
||||
// 2) Attribuée : a type intervention ? couleur type : couleur défaut
|
||||
const typeInterventionId = eventProps.id_type_intervention ? parseInt(eventProps.id_type_intervention) : null;
|
||||
|
||||
// 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é!');
|
||||
if (typeInterventionId && !isNaN(typeInterventionId) && window.crviAjax && window.crviAjax.couleurs_types_intervention) {
|
||||
const couleurType = window.crviAjax.couleurs_types_intervention[typeInterventionId];
|
||||
if (couleurType) {
|
||||
bgColor = couleurType;
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
@ -656,115 +1078,16 @@ export function initializeCalendar() {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Pour les RDV : ordre de priorité - département > type d'intervention > local > défaut
|
||||
// Pour les RDV : type d'intervention ou orange par défaut
|
||||
const typeInterventionId = eventProps.id_type_intervention ? parseInt(eventProps.id_type_intervention) : null;
|
||||
|
||||
// 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é!');
|
||||
if (typeInterventionId && !isNaN(typeInterventionId) && window.crviAjax && window.crviAjax.couleurs_types_intervention) {
|
||||
const couleurType = window.crviAjax.couleurs_types_intervention[typeInterventionId];
|
||||
if (couleurType) {
|
||||
bgColor = couleurType;
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
@ -782,16 +1105,6 @@ 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 });
|
||||
@ -807,15 +1120,6 @@ 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 {
|
||||
@ -884,9 +1188,7 @@ export function initializeCalendar() {
|
||||
|
||||
// Déterminer la couleur pour le popover (utiliser la même logique que pour l'événement)
|
||||
const popoverColor = bgColor || '#6c757d';
|
||||
// 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);
|
||||
const popoverTextColor = txtColor || '#ffffff';
|
||||
|
||||
console.log('🎨 [POPOVER] Couleurs déterminées:', {
|
||||
eventId: event.id,
|
||||
@ -926,47 +1228,6 @@ 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 '';
|
||||
@ -999,9 +1260,6 @@ 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">
|
||||
@ -1014,7 +1272,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: ${statutDisplay.color}; color: ${getTextColor(statutDisplay.color)}; padding: 2px 8px; border-radius: 3px;">${statutDisplay.text}</span></div>
|
||||
<div class="mb-1"><strong>Statut:</strong> <span class="event-status" style="background-color: ${getStatutColor(eventProps.statut)};">${eventProps.statut}</span></div>
|
||||
<div class="mt-2">
|
||||
<small class="text-muted">Cliquez pour plus de détails</small>
|
||||
</div>
|
||||
@ -1048,25 +1306,10 @@ export function initializeCalendar() {
|
||||
});
|
||||
}
|
||||
|
||||
// S'assurer que le body du popover a une couleur de texte lisible
|
||||
// Appliquer aussi les styles au body du popover si nécessaire
|
||||
const bodyElement = popoverElement.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 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
|
||||
});
|
||||
if (bodyElement && txtColor) {
|
||||
bodyElement.style.color = txtColor;
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -1086,27 +1329,6 @@ 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
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@ -590,13 +590,10 @@ 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_filtre')?.value;
|
||||
const langue = document.getElementById('langue')?.value;
|
||||
if (langue) filters.langue = langue;
|
||||
|
||||
const permanences = document.getElementById('permanences_non_assignees')?.checked;
|
||||
@ -620,9 +617,6 @@ 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);
|
||||
|
||||
@ -764,37 +758,14 @@ async function loadFilterOptions(mode = 'intervenant') {
|
||||
});
|
||||
}
|
||||
|
||||
// Départements
|
||||
const departementSelect = document.getElementById(`departement${prefix}`);
|
||||
if (departementSelect && disponibilites.departements) {
|
||||
disponibilites.departements.forEach(dept => {
|
||||
const option = document.createElement('option');
|
||||
option.value = dept.id;
|
||||
option.textContent = dept.nom;
|
||||
departementSelect.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
// Types d'intervention groupés par département
|
||||
// Types d'intervention
|
||||
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);
|
||||
if (typeInterventionSelect && disponibilites.types_intervention) {
|
||||
disponibilites.types_intervention.forEach(type => {
|
||||
const option = document.createElement('option');
|
||||
option.value = type.id;
|
||||
option.textContent = type.nom;
|
||||
typeInterventionSelect.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -1,784 +0,0 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,359 +0,0 @@
|
||||
// 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;
|
||||
}
|
||||
@ -1,189 +0,0 @@
|
||||
// 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -1,517 +0,0 @@
|
||||
// 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');
|
||||
}
|
||||
}
|
||||
@ -1,707 +0,0 @@
|
||||
// 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
@ -85,12 +85,6 @@ 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) {
|
||||
@ -166,8 +160,7 @@ async function loadEvents() {
|
||||
|
||||
// Afficher les résultats
|
||||
displayEvents(result.events || []);
|
||||
// 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);
|
||||
updateCounters(result.total || 0, result.filtered || 0);
|
||||
displayPagination(result.page || 1, result.total_pages || 0);
|
||||
|
||||
// IMPORTANT: Masquer le loader EN PREMIER, puis afficher le tableau
|
||||
@ -185,8 +178,8 @@ async function loadEvents() {
|
||||
table.style.display = 'table';
|
||||
}
|
||||
|
||||
// Afficher la pagination si nécessaire (seulement s'il y a plus d'une page)
|
||||
if (paginationContainer && result.total_pages > 1) {
|
||||
// Afficher la pagination si nécessaire
|
||||
if (paginationContainer && result.total_pages > 0) {
|
||||
paginationContainer.style.display = 'block';
|
||||
}
|
||||
|
||||
|
||||
@ -1,440 +0,0 @@
|
||||
/**
|
||||
* 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;
|
||||
@ -1,15 +1,15 @@
|
||||
{
|
||||
"scripts": {
|
||||
"dev": "vite --mode development",
|
||||
"build": "vite build",
|
||||
"watch": "vite build --watch --mode development"
|
||||
"watch": "vite build --watch",
|
||||
"dev": "vite build --watch --mode development"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fullcalendar/core": "^6.1.18",
|
||||
"@fullcalendar/daygrid": "^6.1.18",
|
||||
"@fullcalendar/interaction": "^6.1.18",
|
||||
"@fullcalendar/list": "^6.1.18",
|
||||
"@fullcalendar/timegrid": "^6.1.18",
|
||||
"@fullcalendar/list": "^6.1.18",
|
||||
"@fullcalendar/interaction": "^6.1.18",
|
||||
"bootstrap": "^5.3.7",
|
||||
"select2": "^4.0.13",
|
||||
"toastr": "^2.1.4"
|
||||
@ -18,4 +18,3 @@
|
||||
"vite": "^7.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,70 +1,89 @@
|
||||
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',
|
||||
|
||||
// Entrées CSS (OK avec Vite/Rollup)
|
||||
crvi_libraries: 'crvi_libraries.js',
|
||||
crvi_main: 'crvi_main.js',
|
||||
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_css: '../css/traduction-langue-list.css',
|
||||
|
||||
traduction_langue_admin: './traduction-langue-admin.js',
|
||||
traduction_langue_list: './traduction-langue-list.js'
|
||||
traduction_langue_list: 'traduction-langue-list.js',
|
||||
traduction_langue_list_css: '../css/traduction-langue-list.css'
|
||||
},
|
||||
|
||||
// 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', // Format ES modules
|
||||
entryFileNames: '[name].min.js',
|
||||
chunkFileNames: '[name].min.js',
|
||||
|
||||
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'
|
||||
// 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';
|
||||
}
|
||||
|
||||
// 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]'
|
||||
// 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) => {
|
||||
// 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
|
||||
// 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'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// Forcer la transpilation pour compatibilité navigateur
|
||||
target: 'es2015',
|
||||
minify: true // Utilise esbuild par défaut (plus rapide que terser)
|
||||
},
|
||||
|
||||
define: {
|
||||
global: 'window'
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
@ -7,7 +7,6 @@
|
||||
|
||||
|
||||
<section class="agenda-container">
|
||||
|
||||
<!--filters-->
|
||||
<section class="filters-container">
|
||||
<form class="filters" method="get" onsubmit="return false;">
|
||||
@ -40,29 +39,13 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter">
|
||||
<label for="departement">Département</label>
|
||||
<select id="departement" name="departement" class="select2">
|
||||
<option value="">Tous</option>
|
||||
<!-- Options dynamiques -->
|
||||
<?php
|
||||
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 -->
|
||||
<!-- Options dynamiques -->
|
||||
<?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>';
|
||||
foreach ($types_intervention as $type_intervention) {
|
||||
echo '<option value="' . $type_intervention['id'] . '">' . $type_intervention['nom'] . '</option>';
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
@ -80,8 +63,8 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter">
|
||||
<label for="langue_filtre">Langue du rendez-vous</label>
|
||||
<select id="langue_filtre" name="langue" class="select2">
|
||||
<label for="langue">Langue du rendez-vous</label>
|
||||
<select id="langue" name="langue" class="select2">
|
||||
<option value="">Toutes</option>
|
||||
<!-- Options dynamiques -->
|
||||
<?php
|
||||
@ -127,43 +110,6 @@
|
||||
</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;">
|
||||
@ -178,29 +124,4 @@
|
||||
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>
|
||||
@ -81,16 +81,11 @@
|
||||
<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">
|
||||
<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 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>
|
||||
<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;">
|
||||
|
||||
@ -281,34 +281,23 @@ if (!is_wp_error($langues_terms) && !empty($langues_terms)) {
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?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>
|
||||
<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 foreach ($langues as $langue): ?>
|
||||
<option value="<?php echo esc_attr($langue['id']); ?>">
|
||||
<?php echo esc_html($langue['nom']); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</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 > Langues
|
||||
<i class="fas fa-external-link-alt ms-1"></i>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,223 +0,0 @@
|
||||
<?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>
|
||||
@ -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,20 +96,14 @@ $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_filtre">Langue</label>
|
||||
<select id="langue_filtre" name="langue" class="select2">
|
||||
<label for="langue">Langue</label>
|
||||
<select id="langue" name="langue" class="select2">
|
||||
<option value="">Toutes</option>
|
||||
</select>
|
||||
</div>
|
||||
@ -173,12 +167,6 @@ $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>
|
||||
|
||||
@ -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,20 +96,14 @@ $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_filtre">Langue</label>
|
||||
<select id="langue_filtre" name="langue" class="select2">
|
||||
<label for="langue">Langue</label>
|
||||
<select id="langue" name="langue" class="select2">
|
||||
<option value="">Toutes</option>
|
||||
</select>
|
||||
</div>
|
||||
@ -173,12 +167,6 @@ $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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -14,11 +14,6 @@ $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';
|
||||
|
||||
@ -15,12 +15,6 @@ $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">
|
||||
@ -226,10 +220,10 @@ $crvi_is_front_context = ($crvi_agenda_context !== 'admin');
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6" id="langue-container">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="langue" class="form-label">Langue *</label>
|
||||
<select class="form-select skip-select2" id="langue" name="langue" required>
|
||||
<select class="form-select" id="langue" name="langue" required>
|
||||
<option value="">Sélectionner...</option>
|
||||
<?php
|
||||
if (isset($langues_beneficiaire) && is_array($langues_beneficiaire)) {
|
||||
@ -304,11 +298,6 @@ $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">
|
||||
@ -479,13 +468,10 @@ $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">
|
||||
<div id="eventStatusButtons" style="display: none;">
|
||||
<div class="btn-group" role="group">
|
||||
<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 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>
|
||||
<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
|
||||
@ -561,20 +547,12 @@ 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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user