Compare commits
69 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
282958a13d | ||
| 24e8a54ff9 | |||
|
|
b123b75b27 | ||
|
|
760e4d4f78 | ||
|
|
a815a21d1b | ||
|
|
036e6cf9ee | ||
|
|
79dd7564bc | ||
|
|
e58d3eaf4e | ||
|
|
975426b01c | ||
|
|
31bc09759a | ||
|
|
200f6aab15 | ||
|
|
9369ee17ce | ||
|
|
444ab59ff7 | ||
|
|
06a7848540 | ||
|
|
1f988d8064 | ||
|
|
842f41c860 | ||
|
|
3349b8b1f7 | ||
|
|
040a1075e4 | ||
|
|
90bb68af53 | ||
|
|
62cf615ed4 | ||
|
|
c95ec9d09a | ||
|
|
d6dad2a7a9 | ||
|
|
c647a5082c | ||
|
|
587646aca8 | ||
|
|
f31fa748b1 | ||
|
|
80618e0b96 | ||
| 9f1cad89be | |||
| 75bff25eb0 | |||
| 7c426d892d | |||
| 884be3761d | |||
| b0b5a2eb53 | |||
| d577ed90f1 | |||
| f54f1060e8 | |||
| 14a7c216c5 | |||
| 23dd434546 | |||
|
|
f7d1bb29a5 | ||
|
|
62fa707291 | ||
|
|
3e2d5743ff | ||
|
|
227f39c50f | ||
|
|
b95ed600e9 | ||
|
|
683b160804 | ||
|
|
3c8254cb86 | ||
|
|
fa36f16291 | ||
| cdbb2f344f | |||
| 2120db133e | |||
| e545165450 | |||
| dde9503e07 | |||
| 0b07a03925 | |||
| bfdd9b10f1 | |||
| f5997d89ad | |||
| c07395efcf | |||
| 3dffdc5649 | |||
| af6953bcb8 | |||
| b18f5a874b | |||
| 9a007e5b63 | |||
| 58f9c86f9c | |||
| 1d8b536e32 | |||
| e7873ecd1e | |||
| 524e4bf12c | |||
| ee0f3e8ddb | |||
| b4e2ed7f0e | |||
| 4e0f28d354 | |||
| f6ae2c7a59 | |||
| 2236e7f773 | |||
| 4fa1747e07 | |||
| 88367d0530 | |||
| ec0d7da5f9 | |||
| 26e35e6c68 | |||
| 0401b8a1aa |
@ -79,6 +79,15 @@ class CRVI_Beneficiaire_Controller {
|
|||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Endpoint pour rechercher des bénéficiaires par nom/prénom
|
||||||
|
register_rest_route('crvi/v1', '/beneficiaires/search', [
|
||||||
|
[
|
||||||
|
'methods' => 'GET',
|
||||||
|
'callback' => [self::class, 'search_beneficiaires'],
|
||||||
|
'permission_callback' => '__return_true',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
register_rest_route('crvi/v1', '/beneficiaires/(?P<id>\\d+)', [
|
register_rest_route('crvi/v1', '/beneficiaires/(?P<id>\\d+)', [
|
||||||
[
|
[
|
||||||
'methods' => 'GET',
|
'methods' => 'GET',
|
||||||
@ -122,6 +131,61 @@ class CRVI_Beneficiaire_Controller {
|
|||||||
return Api_Helper::json_success($items);
|
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) {
|
public static function get_item($request) {
|
||||||
$id = (int) $request['id'];
|
$id = (int) $request['id'];
|
||||||
$item = CRVI_Beneficiaire_Model::load($id);
|
$item = CRVI_Beneficiaire_Model::load($id);
|
||||||
|
|||||||
@ -10,6 +10,7 @@ use ESI_CRVI_AGENDA\models\CRVI_Event_Model;
|
|||||||
use ESI_CRVI_AGENDA\models\CRVI_Traducteur_Model;
|
use ESI_CRVI_AGENDA\models\CRVI_Traducteur_Model;
|
||||||
use ESI_CRVI_AGENDA\models\CRVI_Intervenant_Model;
|
use ESI_CRVI_AGENDA\models\CRVI_Intervenant_Model;
|
||||||
use ESI_CRVI_AGENDA\models\CRVI_Presence_Model;
|
use ESI_CRVI_AGENDA\models\CRVI_Presence_Model;
|
||||||
|
use ESI_CRVI_AGENDA\models\CRVI_Departement_Model;
|
||||||
use ESI_CRVI_AGENDA\controllers\Intervenant_Controller;
|
use ESI_CRVI_AGENDA\controllers\Intervenant_Controller;
|
||||||
|
|
||||||
class CRVI_Event_Controller {
|
class CRVI_Event_Controller {
|
||||||
@ -92,6 +93,11 @@ class CRVI_Event_Controller {
|
|||||||
'callback' => [self::class, 'get_statuts'],
|
'callback' => [self::class, 'get_statuts'],
|
||||||
'permission_callback' => '__return_true',
|
'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 ---
|
// --- Historique bénéficiaire ---
|
||||||
\register_rest_route('crvi/v1', '/beneficiaires/(?P<id>\\d+)/historique', [
|
\register_rest_route('crvi/v1', '/beneficiaires/(?P<id>\\d+)/historique', [
|
||||||
@ -460,35 +466,11 @@ class CRVI_Event_Controller {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Récupérer tous les départements disponibles depuis les CPT
|
// Récupérer tous les départements actifs
|
||||||
$departements_posts = get_posts([
|
$departements = CRVI_Departement_Model::all(true, true);
|
||||||
'post_type' => 'departement',
|
|
||||||
'numberposts' => -1,
|
|
||||||
'post_status' => 'publish',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$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
|
// Récupérer les types d'intervention groupés par département
|
||||||
$types_intervention_posts = get_posts([
|
$types_intervention_groupes = self::get_types_intervention_by_departement();
|
||||||
'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 = [
|
$response = [
|
||||||
'intervenants' => $intervenants_formatted,
|
'intervenants' => $intervenants_formatted,
|
||||||
@ -496,7 +478,7 @@ class CRVI_Event_Controller {
|
|||||||
'beneficiaires' => $beneficiaires,
|
'beneficiaires' => $beneficiaires,
|
||||||
'langues' => $langues,
|
'langues' => $langues,
|
||||||
'departements' => $departements,
|
'departements' => $departements,
|
||||||
'types_intervention' => $types_intervention,
|
'types_intervention_groupes' => $types_intervention_groupes,
|
||||||
];
|
];
|
||||||
|
|
||||||
// Inclure les traducteurs seulement si la vérification a été faite
|
// Inclure les traducteurs seulement si la vérification a été faite
|
||||||
@ -507,6 +489,52 @@ class CRVI_Event_Controller {
|
|||||||
return Api_Helper::json_success($response);
|
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.
|
* Endpoint pour récupérer les permissions de l'utilisateur courant.
|
||||||
* @param WP_REST_Request $request
|
* @param WP_REST_Request $request
|
||||||
@ -567,12 +595,21 @@ class CRVI_Event_Controller {
|
|||||||
$langue_term = get_term_by('slug', $langue_original, 'langue');
|
$langue_term = get_term_by('slug', $langue_original, 'langue');
|
||||||
if ($langue_term && !is_wp_error($langue_term)) {
|
if ($langue_term && !is_wp_error($langue_term)) {
|
||||||
$langue_label = $langue_term->name;
|
$langue_label = $langue_term->name;
|
||||||
|
} else {
|
||||||
|
// Debug: comprendre pourquoi on ne trouve pas le terme
|
||||||
|
error_log('CRVI Debug - Langue non trouvée: ID=' . $langue_original);
|
||||||
|
if (is_wp_error($langue_term)) {
|
||||||
|
error_log('CRVI Debug - Erreur WP: ' . $langue_term->get_error_message());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ajouter le label de la langue pour l'affichage dans la modal
|
// Ajouter le label de la langue pour l'affichage dans la modal (seulement si on a trouvé le nom)
|
||||||
if (!empty($langue_label)) {
|
if (!empty($langue_label)) {
|
||||||
$event['langue_label'] = $langue_label;
|
$event['langue_label'] = $langue_label;
|
||||||
|
} else {
|
||||||
|
// Ne pas assigner l'ID comme fallback - laisser le champ vide
|
||||||
|
$event['langue_label'] = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -626,12 +663,21 @@ class CRVI_Event_Controller {
|
|||||||
$langue_term = get_term_by('slug', $langue_original, 'langue');
|
$langue_term = get_term_by('slug', $langue_original, 'langue');
|
||||||
if ($langue_term && !is_wp_error($langue_term)) {
|
if ($langue_term && !is_wp_error($langue_term)) {
|
||||||
$langue_label = $langue_term->name;
|
$langue_label = $langue_term->name;
|
||||||
|
} else {
|
||||||
|
// Debug: comprendre pourquoi on ne trouve pas le terme
|
||||||
|
error_log('CRVI Debug get_event - Langue non trouvée: ID=' . $langue_original);
|
||||||
|
if (is_wp_error($langue_term)) {
|
||||||
|
error_log('CRVI Debug get_event - Erreur WP: ' . $langue_term->get_error_message());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ajouter le label de la langue pour l'affichage dans la modal
|
// Ajouter le label de la langue pour l'affichage dans la modal (seulement si on a trouvé le nom)
|
||||||
if (!empty($langue_label)) {
|
if (!empty($langue_label)) {
|
||||||
$event['langue_label'] = $langue_label;
|
$event['langue_label'] = $langue_label;
|
||||||
|
} else {
|
||||||
|
// Ne pas assigner l'ID comme fallback - laisser le champ vide
|
||||||
|
$event['langue_label'] = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -655,11 +701,26 @@ class CRVI_Event_Controller {
|
|||||||
}
|
}
|
||||||
public static function create_event($request) {
|
public static function create_event($request) {
|
||||||
$data = $request->get_json_params();
|
$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();
|
$model = new CRVI_Event_Model();
|
||||||
$result = $model->create_event($data);
|
$result = $model->create_event($data);
|
||||||
if (is_wp_error($result)) {
|
if (is_wp_error($result)) {
|
||||||
return Api_Helper::json_error($result->get_error_message(), 400);
|
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']);
|
return Api_Helper::json_success(['id' => $result, 'message' => 'Événement créé avec succès']);
|
||||||
}
|
}
|
||||||
public static function update_event($request) {
|
public static function update_event($request) {
|
||||||
@ -683,7 +744,33 @@ class CRVI_Event_Controller {
|
|||||||
$data = [];
|
$data = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Récupérer l'événement existant pour obtenir les valeurs actuelles
|
||||||
$model = new CRVI_Event_Model();
|
$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);
|
$result = $model->update_event($id, $data);
|
||||||
if (is_wp_error($result)) {
|
if (is_wp_error($result)) {
|
||||||
$error_code = $result->get_error_code();
|
$error_code = $result->get_error_code();
|
||||||
@ -691,6 +778,10 @@ class CRVI_Event_Controller {
|
|||||||
$status_code = isset($error_data['status']) ? $error_data['status'] : 400;
|
$status_code = isset($error_data['status']) ? $error_data['status'] : 400;
|
||||||
return Api_Helper::json_error($result->get_error_message(), $status_code);
|
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']);
|
return Api_Helper::json_success(['id' => $id, 'message' => 'Événement modifié avec succès']);
|
||||||
}
|
}
|
||||||
public static function delete_event($request) {
|
public static function delete_event($request) {
|
||||||
@ -700,9 +791,156 @@ class CRVI_Event_Controller {
|
|||||||
if (is_wp_error($result)) {
|
if (is_wp_error($result)) {
|
||||||
return Api_Helper::json_error($result->get_error_message(), 400);
|
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']);
|
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é
|
* Crée des permanences pour l'intervenant connecté
|
||||||
* POST /wp-json/crvi/v1/intervenant/permanences
|
* POST /wp-json/crvi/v1/intervenant/permanences
|
||||||
@ -954,13 +1192,34 @@ class CRVI_Event_Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Nettoyer et valider les langues (slugs de la taxonomie)
|
// Nettoyer et valider les langues (slugs de la taxonomie)
|
||||||
|
// Accepter soit des slugs soit des IDs (convertir les IDs en slugs)
|
||||||
$langues_valides = [];
|
$langues_valides = [];
|
||||||
if (!empty($langues)) {
|
if (!empty($langues)) {
|
||||||
foreach ($langues as $langue_slug) {
|
foreach ($langues as $langue_value) {
|
||||||
$langue_slug = sanitize_text_field($langue_slug);
|
$langue_value = sanitize_text_field($langue_value);
|
||||||
// Vérifier que la langue existe dans la taxonomie
|
if (empty($langue_value)) {
|
||||||
$term = get_term_by('slug', $langue_slug, 'langue');
|
continue;
|
||||||
if ($term && !is_wp_error($term)) {
|
}
|
||||||
|
|
||||||
|
$term = null;
|
||||||
|
$langue_slug = null;
|
||||||
|
|
||||||
|
// Si c'est un nombre, essayer de récupérer par ID
|
||||||
|
if (is_numeric($langue_value)) {
|
||||||
|
$term = get_term((int)$langue_value, 'langue');
|
||||||
|
if ($term && !is_wp_error($term)) {
|
||||||
|
$langue_slug = $term->slug;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Sinon, essayer par slug
|
||||||
|
$term = get_term_by('slug', $langue_value, 'langue');
|
||||||
|
if ($term && !is_wp_error($term)) {
|
||||||
|
$langue_slug = $term->slug;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ajouter le slug si la langue existe
|
||||||
|
if ($langue_slug) {
|
||||||
$langues_valides[] = $langue_slug;
|
$langues_valides[] = $langue_slug;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1412,6 +1671,10 @@ class CRVI_Event_Controller {
|
|||||||
if (is_wp_error($result)) {
|
if (is_wp_error($result)) {
|
||||||
return Api_Helper::json_error($result->get_error_message(), 400);
|
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']);
|
return Api_Helper::json_success(['id' => $id, 'message' => 'Événement restauré avec succès']);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1422,6 +1685,10 @@ class CRVI_Event_Controller {
|
|||||||
if (is_wp_error($result)) {
|
if (is_wp_error($result)) {
|
||||||
return Api_Helper::json_error($result->get_error_message(), 400);
|
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']);
|
return Api_Helper::json_success(['id' => $id, 'message' => 'Événement supprimé définitivement']);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1517,6 +1784,12 @@ class CRVI_Event_Controller {
|
|||||||
$langue_term = get_term_by('slug', $langue_original, 'langue');
|
$langue_term = get_term_by('slug', $langue_original, 'langue');
|
||||||
if ($langue_term && !is_wp_error($langue_term)) {
|
if ($langue_term && !is_wp_error($langue_term)) {
|
||||||
$langue_label = $langue_term->name;
|
$langue_label = $langue_term->name;
|
||||||
|
} else {
|
||||||
|
// Debug: comprendre pourquoi on ne trouve pas le terme
|
||||||
|
error_log('CRVI Debug FullCalendar - Langue non trouvée: ID=' . $langue_original);
|
||||||
|
if (is_wp_error($langue_term)) {
|
||||||
|
error_log('CRVI Debug FullCalendar - Erreur WP: ' . $langue_term->get_error_message());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1686,6 +1959,26 @@ class CRVI_Event_Controller {
|
|||||||
return Api_Helper::json_success($statuts);
|
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
|
* Vérifie si l'utilisateur peut modifier un événement
|
||||||
* @param \WP_REST_Request $request
|
* @param \WP_REST_Request $request
|
||||||
|
|||||||
@ -558,4 +558,156 @@ class CRVI_Notifications_Controller {
|
|||||||
|
|
||||||
return $results;
|
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,6 +171,10 @@ class CRVI_Plugin {
|
|||||||
add_action('acf/save_post', [self::class, 'check_intervenant_availability_on_save'], 20);
|
add_action('acf/save_post', [self::class, 'check_intervenant_availability_on_save'], 20);
|
||||||
// Hook pour afficher les messages de conflit d'indisponibilités
|
// Hook pour afficher les messages de conflit d'indisponibilités
|
||||||
add_action('admin_notices', [self::class, 'display_intervenant_conflicts_notice']);
|
add_action('admin_notices', [self::class, 'display_intervenant_conflicts_notice']);
|
||||||
|
// Hook pour vider le cache via paramètre URL
|
||||||
|
add_action('admin_init', [self::class, 'handle_clear_cache_request']);
|
||||||
|
// Redirection des utilisateurs non connectés depuis les pages intervenant
|
||||||
|
add_action('template_redirect', [self::class, 'redirect_non_logged_from_intervenant_pages']);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -181,7 +185,7 @@ class CRVI_Plugin {
|
|||||||
add_menu_page(
|
add_menu_page(
|
||||||
__('Agenda CRVI', 'esi_crvi_agenda'),
|
__('Agenda CRVI', 'esi_crvi_agenda'),
|
||||||
__('Agenda CRVI', 'esi_crvi_agenda'),
|
__('Agenda CRVI', 'esi_crvi_agenda'),
|
||||||
'manage_options',
|
'edit_posts',
|
||||||
'crvi_agenda',
|
'crvi_agenda',
|
||||||
[CRVI_Main_View::class, 'render_hub_admin_page'],
|
[CRVI_Main_View::class, 'render_hub_admin_page'],
|
||||||
'dashicons-calendar-alt',
|
'dashicons-calendar-alt',
|
||||||
@ -193,7 +197,7 @@ class CRVI_Plugin {
|
|||||||
'crvi_agenda',
|
'crvi_agenda',
|
||||||
__('Hub', 'esi_crvi_agenda'),
|
__('Hub', 'esi_crvi_agenda'),
|
||||||
__('Hub', 'esi_crvi_agenda'),
|
__('Hub', 'esi_crvi_agenda'),
|
||||||
'manage_options',
|
'edit_posts',
|
||||||
'crvi_agenda',
|
'crvi_agenda',
|
||||||
[CRVI_Main_View::class, 'render_hub_admin_page']
|
[CRVI_Main_View::class, 'render_hub_admin_page']
|
||||||
);
|
);
|
||||||
@ -203,7 +207,7 @@ class CRVI_Plugin {
|
|||||||
'crvi_agenda',
|
'crvi_agenda',
|
||||||
__('Stats', 'esi_crvi_agenda'),
|
__('Stats', 'esi_crvi_agenda'),
|
||||||
__('Stats', 'esi_crvi_agenda'),
|
__('Stats', 'esi_crvi_agenda'),
|
||||||
'manage_options',
|
'edit_posts',
|
||||||
'crvi_agenda_stats',
|
'crvi_agenda_stats',
|
||||||
[self::class, 'render_stats_page']
|
[self::class, 'render_stats_page']
|
||||||
);
|
);
|
||||||
@ -213,7 +217,7 @@ class CRVI_Plugin {
|
|||||||
'crvi_agenda',
|
'crvi_agenda',
|
||||||
__('Agenda', 'esi_crvi_agenda'),
|
__('Agenda', 'esi_crvi_agenda'),
|
||||||
__('Agenda', 'esi_crvi_agenda'),
|
__('Agenda', 'esi_crvi_agenda'),
|
||||||
'manage_options',
|
'edit_posts',
|
||||||
'crvi_agenda_hub',
|
'crvi_agenda_hub',
|
||||||
[CRVI_Agenda_View::class, 'render_agenda_page']
|
[CRVI_Agenda_View::class, 'render_agenda_page']
|
||||||
);
|
);
|
||||||
@ -223,7 +227,7 @@ class CRVI_Plugin {
|
|||||||
'crvi_agenda',
|
'crvi_agenda',
|
||||||
__('Permanences', 'esi_crvi_agenda'),
|
__('Permanences', 'esi_crvi_agenda'),
|
||||||
__('Permanences', 'esi_crvi_agenda'),
|
__('Permanences', 'esi_crvi_agenda'),
|
||||||
'manage_options',
|
'edit_posts',
|
||||||
'crvi_agenda_permanences',
|
'crvi_agenda_permanences',
|
||||||
[self::class, 'render_permanences_page']
|
[self::class, 'render_permanences_page']
|
||||||
);
|
);
|
||||||
@ -280,6 +284,85 @@ class CRVI_Plugin {
|
|||||||
public static function load_filters() {
|
public static function load_filters() {
|
||||||
/* add_filter('rest_endpoints', [self::class, 'register_routes']); */
|
/* add_filter('rest_endpoints', [self::class, 'register_routes']); */
|
||||||
add_filter('wp_script_attributes', [self::class, 'custom_script_tag'], 10, 2);
|
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() {
|
public function load_shortcodes() {
|
||||||
@ -328,14 +411,13 @@ class CRVI_Plugin {
|
|||||||
// CSS des librairies (Bootstrap, Toastr, Select2) - compilé par Vite
|
// 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');
|
wp_enqueue_style('crvi_libraries_css', ESI_CRVI_AGENDA_URL . 'assets/js/dist/crvi_libraries.min.css', [], '1.0.0');
|
||||||
|
|
||||||
// CSS principal du plugin - compilé par Vite
|
// CSS principal du plugin - compilé par Vite (inclut déjà agenda-visual-filters.css)
|
||||||
wp_enqueue_style('crvi_main_css', ESI_CRVI_AGENDA_URL . 'assets/js/dist/crvi_main.min.css', ['crvi_libraries_css'], '1.0.0');
|
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
|
// 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);
|
wp_enqueue_script('crvi_libraries', ESI_CRVI_AGENDA_URL . 'assets/js/dist/crvi_libraries.min.js', ['jquery', 'select2'], '1.0.0', true);
|
||||||
|
|
||||||
// Script principal du plugin (modules locaux) - dépend des librairies
|
// Script principal du plugin (modules locaux, inclut déjà agenda-visual-filters.js) - dépend des librairies
|
||||||
wp_enqueue_script('crvi_main_agenda', ESI_CRVI_AGENDA_URL . 'assets/js/dist/crvi_main.min.js', ['crvi_libraries'], '1.0.0', true);
|
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');
|
self::localize_acf_data('crvi_main_agenda');
|
||||||
@ -467,7 +549,9 @@ class CRVI_Plugin {
|
|||||||
'permissions' => self::get_user_permissions(),
|
'permissions' => self::get_user_permissions(),
|
||||||
'couleurs_rdv' => self::get_couleurs_rdv_acf_data(),
|
'couleurs_rdv' => self::get_couleurs_rdv_acf_data(),
|
||||||
'couleurs_permanence' => self::get_couleurs_permanence_acf_data(),
|
'couleurs_permanence' => self::get_couleurs_permanence_acf_data(),
|
||||||
'indisponibilites_intervenants' => self::get_intervenants_disponibilites()
|
'indisponibilites_intervenants' => self::get_intervenants_disponibilites(),
|
||||||
|
'beneficiaires' => self::get_beneficiaires_acf_data(),
|
||||||
|
'traductions_capacites' => self::get_traductions_capacites_data()
|
||||||
];
|
];
|
||||||
|
|
||||||
wp_localize_script($script_handle, 'crviACFData', $acf_data);
|
wp_localize_script($script_handle, 'crviACFData', $acf_data);
|
||||||
@ -840,6 +924,70 @@ class CRVI_Plugin {
|
|||||||
return $intervenants_data;
|
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
|
* crvi+script loader
|
||||||
*/
|
*/
|
||||||
@ -948,7 +1096,7 @@ class CRVI_Plugin {
|
|||||||
delete_transient('crvi_intervenants_disponibilites');
|
delete_transient('crvi_intervenants_disponibilites');
|
||||||
|
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
$table_name = $wpdb->prefix . 'crvi_agenda_events';
|
$table_name = $wpdb->prefix . 'crvi_agenda';
|
||||||
$conflicts = [];
|
$conflicts = [];
|
||||||
|
|
||||||
// 1. Vérifier les jours de disponibilité
|
// 1. Vérifier les jours de disponibilité
|
||||||
@ -1123,6 +1271,9 @@ class CRVI_Plugin {
|
|||||||
|
|
||||||
// Stocker le message dans un transient pour l'afficher après la redirection
|
// Stocker le message dans un transient pour l'afficher après la redirection
|
||||||
set_transient('crvi_intervenant_conflicts_' . $user_id, $message, 30);
|
set_transient('crvi_intervenant_conflicts_' . $user_id, $message, 30);
|
||||||
|
|
||||||
|
// Envoyer un email de notification aux administrateurs via le contrôleur de notifications
|
||||||
|
CRVI_Notifications_Controller::send_conflict_notification($user_id, $conflicts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1159,6 +1310,9 @@ class CRVI_Plugin {
|
|||||||
* Affiche les messages de conflit d'indisponibilités stockés dans les transients (admin)
|
* Affiche les messages de conflit d'indisponibilités stockés dans les transients (admin)
|
||||||
*/
|
*/
|
||||||
public static function display_intervenant_conflicts_notice() {
|
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
|
// Récupérer l'ID utilisateur courant si on est sur une page de profil
|
||||||
$user_id = 0;
|
$user_id = 0;
|
||||||
|
|
||||||
@ -1217,4 +1371,58 @@ class CRVI_Plugin {
|
|||||||
delete_transient('crvi_intervenant_conflicts_' . $user_id);
|
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,4 +493,12 @@ class CRVI_TraductionLangue_Controller {
|
|||||||
// Afficher le template
|
// Afficher le template
|
||||||
include dirname(__DIR__, 2) . '/templates/admin/traduction-langue-list.php';
|
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,6 +14,7 @@ class CRVI_Departement_Model extends Main_Model
|
|||||||
public static $acf_schema = [
|
public static $acf_schema = [
|
||||||
'nom' => 'text',
|
'nom' => 'text',
|
||||||
'type_dinterventions' => 'repeater',
|
'type_dinterventions' => 'repeater',
|
||||||
|
'actif' => 'true_false',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -37,8 +38,11 @@ class CRVI_Departement_Model extends Main_Model
|
|||||||
$data['id'] = $post->ID;
|
$data['id'] = $post->ID;
|
||||||
} elseif ($field === 'nom') {
|
} elseif ($field === 'nom') {
|
||||||
$data['nom'] = $post->post_title;
|
$data['nom'] = $post->post_title;
|
||||||
} elseif (property_exists(self::class, $field)) {
|
} elseif ($field === 'actif') {
|
||||||
$data[$field] = get_field($field, $post->ID);
|
// Charger le champ ACF 'actif'
|
||||||
|
$data['actif'] = function_exists('get_field') ? get_field('actif', $post->ID) : true;
|
||||||
|
} elseif (isset(self::$acf_schema[$field])) {
|
||||||
|
$data[$field] = function_exists('get_field') ? get_field($field, $post->ID) : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new self($data);
|
return new self($data);
|
||||||
@ -50,8 +54,10 @@ class CRVI_Departement_Model extends Main_Model
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Retourne la liste de tous les départements (CPT)
|
* Retourne la liste de tous les départements (CPT)
|
||||||
|
* @param bool $simple_list Si true, retourne uniquement id et nom
|
||||||
|
* @param bool $only_active Si true (défaut), ne retourne que les départements actifs
|
||||||
*/
|
*/
|
||||||
public static function all($simple_list = false)
|
public static function all($simple_list = false, $only_active = true)
|
||||||
{
|
{
|
||||||
$posts = get_posts([
|
$posts = get_posts([
|
||||||
'post_type' => 'departement',
|
'post_type' => 'departement',
|
||||||
@ -59,6 +65,16 @@ class CRVI_Departement_Model extends Main_Model
|
|||||||
'post_status' => 'publish',
|
'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) {
|
if ($simple_list) {
|
||||||
$posts = array_map(function($post) {
|
$posts = array_map(function($post) {
|
||||||
return [
|
return [
|
||||||
|
|||||||
@ -594,8 +594,15 @@ class CRVI_Event_Model extends Main_Model {
|
|||||||
* get departement nom
|
* get departement nom
|
||||||
*/
|
*/
|
||||||
public function get_departement_nom($id_departement) {
|
public function get_departement_nom($id_departement) {
|
||||||
|
if (empty($id_departement)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
$departement = get_term_by('id', $id_departement, 'departement');
|
$departement = get_term_by('id', $id_departement, 'departement');
|
||||||
|
if (!$departement || is_wp_error($departement)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
return $departement->name;
|
return $departement->name;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -603,7 +610,15 @@ class CRVI_Event_Model extends Main_Model {
|
|||||||
* get type_intervention nom
|
* get type_intervention nom
|
||||||
*/
|
*/
|
||||||
public function get_type_intervention_nom($id_type_intervention) {
|
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');
|
$type_intervention = get_term_by('id', $id_type_intervention, 'type_intervention');
|
||||||
|
if (!$type_intervention || is_wp_error($type_intervention)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
return $type_intervention->name;
|
return $type_intervention->name;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -845,6 +860,7 @@ class CRVI_Event_Model extends Main_Model {
|
|||||||
'id_intervenant' => $existing_event->id_intervenant ?? null,
|
'id_intervenant' => $existing_event->id_intervenant ?? null,
|
||||||
'id_traducteur' => $existing_event->id_traducteur ?? null,
|
'id_traducteur' => $existing_event->id_traducteur ?? null,
|
||||||
'id_local' => $existing_event->id_local ?? null,
|
'id_local' => $existing_event->id_local ?? null,
|
||||||
|
'langues_disponibles' => $existing_event->langues_disponibles ?? null,
|
||||||
], $data);
|
], $data);
|
||||||
|
|
||||||
// 5. Valider les données fusionnées (aligné avec create_event)
|
// 5. Valider les données fusionnées (aligné avec create_event)
|
||||||
@ -949,6 +965,38 @@ class CRVI_Event_Model extends Main_Model {
|
|||||||
// 7. Calculer automatiquement le flag assign
|
// 7. Calculer automatiquement le flag assign
|
||||||
$update_data['assign'] = self::calculate_assign_flag($update_data);
|
$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
|
// 8. Ajouter les métadonnées de modification
|
||||||
$update_data['modifie_par'] = get_current_user_id();
|
$update_data['modifie_par'] = get_current_user_id();
|
||||||
$update_data['date_modification'] = current_time('mysql');
|
$update_data['date_modification'] = current_time('mysql');
|
||||||
@ -959,7 +1007,7 @@ class CRVI_Event_Model extends Main_Model {
|
|||||||
'date_rdv', 'heure_rdv', 'date_fin', 'heure_fin', 'type', 'statut',
|
'date_rdv', 'heure_rdv', 'date_fin', 'heure_fin', 'type', 'statut',
|
||||||
'motif_annulation', 'commentaire', 'id_departement', 'id_type_intervention',
|
'motif_annulation', 'commentaire', 'id_departement', 'id_type_intervention',
|
||||||
'langue', 'id_beneficiaire', 'id_intervenant', 'id_traducteur', 'id_local',
|
'langue', 'id_beneficiaire', 'id_intervenant', 'id_traducteur', 'id_local',
|
||||||
'assign', 'modifie_par', 'date_modification'
|
'langues_disponibles', 'assign', 'modifie_par', 'date_modification'
|
||||||
];
|
];
|
||||||
|
|
||||||
$filtered_data = [];
|
$filtered_data = [];
|
||||||
@ -1124,6 +1172,25 @@ class CRVI_Event_Model extends Main_Model {
|
|||||||
$where = [];
|
$where = [];
|
||||||
$values = [];
|
$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
|
// Traitement des filtres
|
||||||
foreach ($map as $api => $col) {
|
foreach ($map as $api => $col) {
|
||||||
if (!empty($params[$api]) || (isset($params[$api]) && $params[$api] === '0')) {
|
if (!empty($params[$api]) || (isset($params[$api]) && $params[$api] === '0')) {
|
||||||
@ -1137,6 +1204,19 @@ class CRVI_Event_Model extends Main_Model {
|
|||||||
// Support du filtre assign (0 ou 1)
|
// Support du filtre assign (0 ou 1)
|
||||||
$where[] = "$col = %d";
|
$where[] = "$col = %d";
|
||||||
$values[] = (int)$params[$api];
|
$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 {
|
} else {
|
||||||
$where[] = "$col = %s";
|
$where[] = "$col = %s";
|
||||||
$values[] = $params[$api];
|
$values[] = $params[$api];
|
||||||
@ -1423,6 +1503,25 @@ class CRVI_Event_Model extends Main_Model {
|
|||||||
$where = [];
|
$where = [];
|
||||||
$values = [];
|
$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
|
// Traitement des filtres
|
||||||
foreach ($map as $api => $col) {
|
foreach ($map as $api => $col) {
|
||||||
if (!empty($params[$api]) || (isset($params[$api]) && $params[$api] === '0')) {
|
if (!empty($params[$api]) || (isset($params[$api]) && $params[$api] === '0')) {
|
||||||
@ -1435,6 +1534,19 @@ class CRVI_Event_Model extends Main_Model {
|
|||||||
} elseif ($api === 'assign') {
|
} elseif ($api === 'assign') {
|
||||||
$where[] = "$col = %d";
|
$where[] = "$col = %d";
|
||||||
$values[] = (int)$params[$api];
|
$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 {
|
} else {
|
||||||
$where[] = "$col = %s";
|
$where[] = "$col = %s";
|
||||||
$values[] = $params[$api];
|
$values[] = $params[$api];
|
||||||
@ -1673,8 +1785,20 @@ class CRVI_Event_Model extends Main_Model {
|
|||||||
'id' => (int) $event['id'],
|
'id' => (int) $event['id'],
|
||||||
'date_rdv' => $event['date_rdv'],
|
'date_rdv' => $event['date_rdv'],
|
||||||
'heure_rdv' => $event['heure_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
|
// Charger l'intervenant
|
||||||
if (!empty($event['id_intervenant'])) {
|
if (!empty($event['id_intervenant'])) {
|
||||||
$intervenant = CRVI_Intervenant_Model::load($event['id_intervenant'], ['id', 'nom', 'prenom']);
|
$intervenant = CRVI_Intervenant_Model::load($event['id_intervenant'], ['id', 'nom', 'prenom']);
|
||||||
|
|||||||
@ -118,16 +118,6 @@ class CRVI_TraductionLangue_Model extends Main_Model {
|
|||||||
];
|
];
|
||||||
|
|
||||||
// Appliquer les filtres
|
// Appliquer les filtres
|
||||||
if (!empty($filters['langue'])) {
|
|
||||||
$args['tax_query'] = [
|
|
||||||
[
|
|
||||||
'taxonomy' => 'langue',
|
|
||||||
'field' => 'term_id',
|
|
||||||
'terms' => (int) $filters['langue'],
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!empty($filters['jour'])) {
|
if (!empty($filters['jour'])) {
|
||||||
$args['meta_query'][] = [
|
$args['meta_query'][] = [
|
||||||
'key' => 'jour',
|
'key' => 'jour',
|
||||||
@ -147,8 +137,24 @@ class CRVI_TraductionLangue_Model extends Main_Model {
|
|||||||
$posts = \get_posts($args);
|
$posts = \get_posts($args);
|
||||||
$capacites = [];
|
$capacites = [];
|
||||||
|
|
||||||
foreach ($posts as $post) {
|
// Filtrer par langue si demandé (le champ langue est un ACF taxonomy multi-select stocké en meta)
|
||||||
$capacites[] = self::load($post->ID);
|
if (!empty($filters['langue'])) {
|
||||||
|
$langue_id_filter = (int) $filters['langue'];
|
||||||
|
foreach ($posts as $post) {
|
||||||
|
$capacite = self::load($post->ID);
|
||||||
|
if ($capacite && !empty($capacite->langue)) {
|
||||||
|
// Le champ langue est un tableau d'IDs
|
||||||
|
$langue_ids = is_array($capacite->langue) ? $capacite->langue : [$capacite->langue];
|
||||||
|
if (in_array($langue_id_filter, $langue_ids)) {
|
||||||
|
$capacites[] = $capacite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Pas de filtre par langue, charger toutes les capacités
|
||||||
|
foreach ($posts as $post) {
|
||||||
|
$capacites[] = self::load($post->ID);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mettre en cache si activé et sans filtres
|
// Mettre en cache si activé et sans filtres
|
||||||
@ -621,7 +627,7 @@ class CRVI_TraductionLangue_Model extends Main_Model {
|
|||||||
|
|
||||||
// Calculer la limite mensuelle pour la période principale
|
// Calculer la limite mensuelle pour la période principale
|
||||||
$limite_mensuelle_principale = self::convertToMonthlyLimit(
|
$limite_mensuelle_principale = self::convertToMonthlyLimit(
|
||||||
$capacite->limite,
|
(int) $capacite->limite,
|
||||||
$capacite->limite_par,
|
$capacite->limite_par,
|
||||||
$nb_semaines
|
$nb_semaines
|
||||||
);
|
);
|
||||||
@ -683,15 +689,29 @@ class CRVI_TraductionLangue_Model extends Main_Model {
|
|||||||
$result['total_available'] = max(0, $result['total'] - $result['total_used']);
|
$result['total_available'] = max(0, $result['total'] - $result['total_used']);
|
||||||
|
|
||||||
// Calculer les totaux par période
|
// Calculer les totaux par période
|
||||||
foreach ($result['by_periode'] as $periode => &$items) {
|
// Créer une copie des clés pour éviter de modifier le tableau pendant l'itération
|
||||||
|
$periodes_keys = array_keys($result['by_periode']);
|
||||||
|
foreach ($periodes_keys as $periode) {
|
||||||
|
// Ignorer les clés qui sont déjà des summaries
|
||||||
|
if (strpos($periode, '_summary') !== false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = $result['by_periode'][$periode];
|
||||||
$periode_total = 0;
|
$periode_total = 0;
|
||||||
$periode_used = 0;
|
$periode_used = 0;
|
||||||
$periode_available = 0;
|
$periode_available = 0;
|
||||||
|
|
||||||
foreach ($items as $item) {
|
// Vérifier que $items est un tableau
|
||||||
$periode_total += $item['limite'];
|
if (is_array($items)) {
|
||||||
$periode_used += $item['used'];
|
foreach ($items as $item) {
|
||||||
$periode_available += $item['available'];
|
// Vérifier que $item est un tableau et non un int
|
||||||
|
if (is_array($item) && isset($item['limite']) && isset($item['used']) && isset($item['available'])) {
|
||||||
|
$periode_total += $item['limite'];
|
||||||
|
$periode_used += $item['used'];
|
||||||
|
$periode_available += $item['available'];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ajouter un résumé pour cette période
|
// Ajouter un résumé pour cette période
|
||||||
@ -840,6 +860,340 @@ class CRVI_TraductionLangue_Model extends Main_Model {
|
|||||||
return $result;
|
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
|
* Invalide le cache pour une capacité donnée
|
||||||
* À appeler lors de la création/modification/suppression d'événements ou de capacités
|
* À appeler lors de la création/modification/suppression d'événements ou de capacités
|
||||||
@ -862,9 +1216,13 @@ class CRVI_TraductionLangue_Model extends Main_Model {
|
|||||||
$wpdb->prepare(
|
$wpdb->prepare(
|
||||||
"DELETE FROM {$wpdb->options}
|
"DELETE FROM {$wpdb->options}
|
||||||
WHERE option_name LIKE %s
|
WHERE option_name LIKE %s
|
||||||
|
OR 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_crvi_dispos_langue_' . $langue_id . '_%',
|
||||||
'_transient_timeout_crvi_dispos_langue_' . $langue_id . '_%'
|
'_transient_timeout_crvi_dispos_langue_' . $langue_id . '_%',
|
||||||
|
'_transient_crvi_dispos_langue_jours_' . $langue_id . '_%',
|
||||||
|
'_transient_timeout_crvi_dispos_langue_jours_' . $langue_id . '_%'
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -873,7 +1231,9 @@ class CRVI_TraductionLangue_Model extends Main_Model {
|
|||||||
$wpdb->query(
|
$wpdb->query(
|
||||||
"DELETE FROM {$wpdb->options}
|
"DELETE FROM {$wpdb->options}
|
||||||
WHERE option_name LIKE '_transient_crvi_all_langues_dispos_%'
|
WHERE option_name LIKE '_transient_crvi_all_langues_dispos_%'
|
||||||
OR option_name LIKE '_transient_timeout_crvi_all_langues_dispos_%'"
|
OR option_name LIKE '_transient_timeout_crvi_all_langues_dispos_%'
|
||||||
|
OR option_name LIKE '_transient_crvi_langues_capacites_acf_%'
|
||||||
|
OR option_name LIKE '_transient_timeout_crvi_langues_capacites_acf_%'"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -888,7 +1248,9 @@ class CRVI_TraductionLangue_Model extends Main_Model {
|
|||||||
OR option_name LIKE '_transient_crvi_all_langues_dispos_%'
|
OR option_name LIKE '_transient_crvi_all_langues_dispos_%'
|
||||||
OR option_name LIKE '_transient_timeout_crvi_all_langues_dispos_%'
|
OR option_name LIKE '_transient_timeout_crvi_all_langues_dispos_%'
|
||||||
OR option_name LIKE '_transient_crvi_count_used_%'
|
OR option_name LIKE '_transient_crvi_count_used_%'
|
||||||
OR option_name LIKE '_transient_timeout_crvi_count_used_%'"
|
OR option_name LIKE '_transient_timeout_crvi_count_used_%'
|
||||||
|
OR option_name LIKE '_transient_crvi_langues_capacites_acf_%'
|
||||||
|
OR option_name LIKE '_transient_timeout_crvi_langues_capacites_acf_%'"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Invalider le cache général des capacités
|
// Invalider le cache général des capacités
|
||||||
|
|||||||
@ -15,7 +15,10 @@ class CRVI_Agenda_View {
|
|||||||
$locals = CRVI_Local_Model::get_locals([],true);
|
$locals = CRVI_Local_Model::get_locals([],true);
|
||||||
$intervenants = CRVI_Intervenant_Model::get_intervenants([],true);
|
$intervenants = CRVI_Intervenant_Model::get_intervenants([],true);
|
||||||
$departements = CRVI_Departement_Model::all(true);
|
$departements = CRVI_Departement_Model::all(true);
|
||||||
$types_intervention = CRVI_Type_Intervention_Model::all(true);
|
|
||||||
|
// Récupérer les types d'intervention groupés par département
|
||||||
|
$types_intervention_groupes = self::get_types_intervention_by_departement();
|
||||||
|
|
||||||
$traducteurs = CRVI_Traducteur_Model::get_traducteurs([],true);
|
$traducteurs = CRVI_Traducteur_Model::get_traducteurs([],true);
|
||||||
$langues_beneficiaire = Api_Helper::get_languages(true);
|
$langues_beneficiaire = Api_Helper::get_languages(true);
|
||||||
$genres = Api_Helper::get_acf_field_options('field_685e466352755');
|
$genres = Api_Helper::get_acf_field_options('field_685e466352755');
|
||||||
@ -45,4 +48,50 @@ class CRVI_Agenda_View {
|
|||||||
echo '<p style="color:red">Template agenda-page.php introuvable.</p>';
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
276
assets/css/agenda-visual-filters.css
Normal file
276
assets/css/agenda-visual-filters.css
Normal file
@ -0,0 +1,276 @@
|
|||||||
|
/**
|
||||||
|
* Styles pour les filtres visuels de l'agenda admin
|
||||||
|
* Boutons de départements et capacités de traduction
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Conteneur principal */
|
||||||
|
.visual-filters-container {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visual-filters-container .card {
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.visual-filters-container .card-header {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-bottom: 2px solid #007bff;
|
||||||
|
padding: 12px 20px;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visual-filters-container .card-header:hover {
|
||||||
|
background-color: #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visual-filters-container .card-header h5 {
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visual-filters-container .card-header .btn-link {
|
||||||
|
color: #333;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visual-filters-container .card-header .btn-link:hover {
|
||||||
|
color: #007bff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#visual-filters-toggle-icon {
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visual-filters-container .card-body {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Groupes de filtres */
|
||||||
|
.filter-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-group:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-group-title {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #495057;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Conteneur des boutons */
|
||||||
|
.button-filters {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bouton de filtre commun */
|
||||||
|
.filter-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 2px solid #dee2e6;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #495057;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-btn i {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-btn:hover:not(.disabled) {
|
||||||
|
border-color: #007bff;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 8px rgba(0,123,255,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-btn.active {
|
||||||
|
background-color: #007bff;
|
||||||
|
border-color: #007bff;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-btn.active i {
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-btn.disabled {
|
||||||
|
background-color: #e9ecef;
|
||||||
|
border-color: #dee2e6;
|
||||||
|
color: #6c757d;
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bouton de capacité de traduction */
|
||||||
|
.trad-btn {
|
||||||
|
min-width: 200px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trad-btn-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trad-btn-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trad-btn-label {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trad-btn-stats {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trad-stat {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trad-btn.active .trad-stat {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Barre de progression */
|
||||||
|
.trad-progress {
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
background-color: #e9ecef;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trad-progress-bar {
|
||||||
|
height: 100%;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Détails (matin/après-midi) */
|
||||||
|
.trad-btn-details {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trad-detail {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #6c757d;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trad-btn.active .trad-detail {
|
||||||
|
background-color: rgba(255,255,255,0.2);
|
||||||
|
color: rgba(255,255,255,0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.trad-btn.disabled .trad-detail {
|
||||||
|
background-color: #e9ecef;
|
||||||
|
color: #adb5bd;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation de chargement */
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-btn.loading {
|
||||||
|
animation: pulse 1.5s ease-in-out infinite;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.button-filters {
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-btn {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
padding: 6px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trad-btn {
|
||||||
|
min-width: 150px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trad-btn-label {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trad-stat {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trad-detail {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
padding: 2px 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* États spéciaux */
|
||||||
|
.filter-btn.warning {
|
||||||
|
border-color: #ffc107;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-btn.danger {
|
||||||
|
border-color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tooltip pour informations supplémentaires */
|
||||||
|
.filter-btn[data-bs-toggle="tooltip"] {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Badge de notification */
|
||||||
|
.filter-btn .badge {
|
||||||
|
position: absolute;
|
||||||
|
top: -8px;
|
||||||
|
right: -8px;
|
||||||
|
background-color: #dc3545;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
@ -142,4 +142,271 @@
|
|||||||
.fc-more-popover {
|
.fc-more-popover {
|
||||||
max-width: 300px !important;
|
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,5 +7,8 @@
|
|||||||
/* CSS personnalisé du plugin */
|
/* CSS personnalisé du plugin */
|
||||||
@import './crvi.css';
|
@import './crvi.css';
|
||||||
@import './agenda-events.css';
|
@import './agenda-events.css';
|
||||||
|
@import './crvi-agenda.css';
|
||||||
/* Overrides admin calendrier (sera minifié avec le reste via Vite) */
|
/* Overrides admin calendrier (sera minifié avec le reste via Vite) */
|
||||||
@import './admin-calendar-overrides.css';
|
@import './admin-calendar-overrides.css';
|
||||||
|
/* Filtres visuels agenda admin */
|
||||||
|
@import './agenda-visual-filters.css';
|
||||||
@ -1,12 +1,17 @@
|
|||||||
/* Styles pour l'autocomplétion des présences dans la modal de validation des présences */
|
/* 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 {
|
.presence-nom-input {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.autocomplete-suggestions {
|
.autocomplete-suggestions {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 1050;
|
z-index: 1060;
|
||||||
background: white;
|
background: white;
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@ -14,7 +19,6 @@
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
width: 100%;
|
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import { initializeProfile } from './modules/agenda-intervenant-profile.js';
|
|||||||
import { initializePermanences } from './modules/agenda-intervenant-permanences.js';
|
import { initializePermanences } from './modules/agenda-intervenant-permanences.js';
|
||||||
import { initializeAdminPermanences } from './modules/agenda-admin-permanences.js';
|
import { initializeAdminPermanences } from './modules/agenda-admin-permanences.js';
|
||||||
import { initStatsTable } from './modules/agenda-stats-table.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
|
* FIX GLOBAL : Corriger le stacking context de TOUTES les modales Bootstrap
|
||||||
@ -126,6 +127,13 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
console.warn('⚠️ Échec de l\'initialisation du calendrier');
|
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');
|
const modal = document.getElementById('eventModal');
|
||||||
if (modal) {
|
if (modal) {
|
||||||
// Vérifier que bootstrap est disponible
|
// Vérifier que bootstrap est disponible
|
||||||
@ -151,7 +159,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialiser Select2 sur les <select> (commun à tous les contextes)
|
// Initialiser Select2 sur les <select> (commun à tous les contextes)
|
||||||
jQuery('.select2').select2();
|
// Exclure les selects avec skip-select2 (comme le select langue dans le modal)
|
||||||
|
jQuery('.select2:not(.skip-select2)').select2();
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -31,7 +31,13 @@ export function initializeAdminPermanences() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialiser Select2 pour le champ langues
|
// Initialiser Select2 pour le champ langues
|
||||||
initializeSelect2();
|
// Utiliser un petit délai pour s'assurer que le DOM est complètement prêt
|
||||||
|
setTimeout(() => {
|
||||||
|
initializeSelect2();
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
// Réinitialiser Select2 quand l'onglet devient visible (problème avec éléments cachés)
|
||||||
|
setupSelect2TabListener();
|
||||||
|
|
||||||
// Écouter les changements pour mettre à jour l'aperçu
|
// Écouter les changements pour mettre à jour l'aperçu
|
||||||
setupPreviewListeners();
|
setupPreviewListeners();
|
||||||
@ -46,13 +52,109 @@ export function initializeAdminPermanences() {
|
|||||||
initializeCsvImport();
|
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
|
* Initialise Select2 pour le champ de sélection des langues
|
||||||
*/
|
*/
|
||||||
function initializeSelect2() {
|
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');
|
const languesSelect = document.getElementById('langues-permanences');
|
||||||
if (languesSelect && typeof jQuery !== 'undefined' && jQuery.fn.select2) {
|
if (languesSelect && select2Initialized) {
|
||||||
jQuery(languesSelect).select2({
|
const $select = jQuery(languesSelect);
|
||||||
|
if ($select.data('select2') && $select.hasClass('select2-hidden-accessible')) {
|
||||||
|
console.log('✅ Select2 déjà initialisé (flag=true), pas de réinitialisation');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!languesSelect) {
|
||||||
|
console.warn('Select langues-permanences non trouvé');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier que jQuery et Select2 sont disponibles
|
||||||
|
if (typeof jQuery === 'undefined' || !jQuery.fn.select2) {
|
||||||
|
console.warn('jQuery ou Select2 non disponible, réessai dans 100ms...');
|
||||||
|
setTimeout(initializeSelect2, 100);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const $select = jQuery(languesSelect);
|
||||||
|
|
||||||
|
// DESTRUCTION AGRESSIVE : Détruire tout Select2 au début, sans pitié !
|
||||||
|
try {
|
||||||
|
// Détruire l'instance Select2 si elle existe
|
||||||
|
if ($select.data('select2')) {
|
||||||
|
$select.select2('destroy');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supprimer TOUS les conteneurs Select2 dans le parent
|
||||||
|
const parentContainer = languesSelect.closest('.card-body') || languesSelect.parentElement;
|
||||||
|
if (parentContainer) {
|
||||||
|
// Supprimer tous les conteneurs Select2
|
||||||
|
const select2Containers = parentContainer.querySelectorAll('span.select2-container');
|
||||||
|
select2Containers.forEach(container => container.remove());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nettoyer tous les attributs Select2 du select
|
||||||
|
$select.removeClass('select2-hidden-accessible');
|
||||||
|
$select.removeAttr('data-select2-id');
|
||||||
|
$select.removeAttr('aria-hidden');
|
||||||
|
$select.removeAttr('tabindex');
|
||||||
|
|
||||||
|
console.log('💥 Select2 complètement détruit (méchamment)');
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Erreur lors de la destruction agressive:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marquer qu'on est en train d'initialiser
|
||||||
|
isInitializingSelect2 = true;
|
||||||
|
select2Initialized = false; // Réinitialiser le flag
|
||||||
|
|
||||||
|
// Vérifier que le select a des options
|
||||||
|
const options = languesSelect.querySelectorAll('option');
|
||||||
|
if (options.length === 0) {
|
||||||
|
console.warn('Le select langues-permanences n\'a pas d\'options');
|
||||||
|
isInitializingSelect2 = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialiser Select2
|
||||||
|
try {
|
||||||
|
// Vérifier que les options sont valides
|
||||||
|
const validOptions = Array.from(options).filter(opt => {
|
||||||
|
return opt.value && opt.value.trim() !== '' && opt.textContent && opt.textContent.trim() !== '';
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('📋 Options valides trouvées:', validOptions.length, 'sur', options.length);
|
||||||
|
|
||||||
|
if (validOptions.length === 0) {
|
||||||
|
console.error('Aucune option valide trouvée dans le select');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// S'assurer que le select est visible avant l'initialisation
|
||||||
|
const isVisible = languesSelect.offsetParent !== null;
|
||||||
|
if (!isVisible) {
|
||||||
|
console.warn('Le select langues-permanences n\'est pas visible, réessai dans 200ms...');
|
||||||
|
setTimeout(initializeSelect2, 200);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialiser Select2 - laisser Select2 lire automatiquement depuis le select natif
|
||||||
|
// Ne pas passer data pour éviter les conflits avec le select natif
|
||||||
|
$select.select2({
|
||||||
placeholder: 'Sélectionnez une ou plusieurs langues',
|
placeholder: 'Sélectionnez une ou plusieurs langues',
|
||||||
allowClear: true,
|
allowClear: true,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@ -62,9 +164,58 @@ function initializeSelect2() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Vérifier que Select2 a bien chargé les options
|
||||||
|
setTimeout(() => {
|
||||||
|
const select2Instance = $select.data('select2');
|
||||||
|
if (select2Instance) {
|
||||||
|
// Forcer Select2 à recharger les options depuis le select natif
|
||||||
|
$select.trigger('change.select2');
|
||||||
|
console.log('✅ Select2 initialisé pour langues-permanences avec', validOptions.length, 'options');
|
||||||
|
// Marquer comme initialisé
|
||||||
|
select2Initialized = true;
|
||||||
|
} else {
|
||||||
|
console.warn('⚠️ Select2 n\'a pas été correctement initialisé');
|
||||||
|
select2Initialized = false;
|
||||||
|
}
|
||||||
|
// Libérer la garde
|
||||||
|
isInitializingSelect2 = false;
|
||||||
|
}, 50);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Erreur lors de l\'initialisation de Select2:', e);
|
||||||
|
isInitializingSelect2 = false;
|
||||||
|
select2Initialized = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure un listener pour réinitialiser Select2 quand l'onglet devient visible
|
||||||
|
* (uniquement si Select2 n'est pas déjà correctement initialisé)
|
||||||
|
*/
|
||||||
|
function setupSelect2TabListener() {
|
||||||
|
const tabButtons = document.querySelectorAll('#permanences-tabs button[data-bs-toggle="tab"]');
|
||||||
|
tabButtons.forEach(button => {
|
||||||
|
button.addEventListener('shown.bs.tab', function(e) {
|
||||||
|
// Vérifier si l'onglet qui devient visible contient le select langues-permanences
|
||||||
|
const targetTabId = e.target.getAttribute('data-bs-target');
|
||||||
|
const targetTab = document.querySelector(targetTabId);
|
||||||
|
|
||||||
|
if (targetTab && targetTab.querySelector('#langues-permanences')) {
|
||||||
|
// Ne PAS réinitialiser si Select2 est déjà initialisé
|
||||||
|
// La garde globale select2Initialized empêche toute réinitialisation
|
||||||
|
if (!select2Initialized) {
|
||||||
|
console.log('🔄 Onglet changé, vérification Select2...');
|
||||||
|
setTimeout(() => {
|
||||||
|
initializeSelect2();
|
||||||
|
}, 100);
|
||||||
|
} else {
|
||||||
|
console.log('✅ Select2 déjà initialisé, pas de réinitialisation');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configure la préselection des inputs jours[] et heures[]
|
* Configure la préselection des inputs jours[] et heures[]
|
||||||
* à partir des attributs data-days et data-time-slots de l'option sélectionnée
|
* à partir des attributs data-days et data-time-slots de l'option sélectionnée
|
||||||
@ -187,6 +338,18 @@ function setupPreviewListeners() {
|
|||||||
heuresInputs.forEach(input => {
|
heuresInputs.forEach(input => {
|
||||||
input.addEventListener('change', updatePreview);
|
input.addEventListener('change', updatePreview);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Écouter les changements de durée de permanence (1h ou 15min)
|
||||||
|
const dureeInputs = document.querySelectorAll('input[name="duree_permanence"]');
|
||||||
|
dureeInputs.forEach(input => {
|
||||||
|
input.addEventListener('change', updatePreview);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Écouter les changements du nombre de tranches (si 15min)
|
||||||
|
const nbTranchesSelect = document.getElementById('nb-tranches');
|
||||||
|
if (nbTranchesSelect) {
|
||||||
|
nbTranchesSelect.addEventListener('change', updatePreview);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -200,6 +363,8 @@ function updatePreview() {
|
|||||||
.map(input => input.value)
|
.map(input => input.value)
|
||||||
.sort(); // Trier les heures pour un affichage cohérent
|
.sort(); // Trier les heures pour un affichage cohérent
|
||||||
const moisDebut = document.getElementById('mois-debut')?.value;
|
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) {
|
if (heuresChecked.length === 0) {
|
||||||
clearPreview();
|
clearPreview();
|
||||||
@ -217,15 +382,37 @@ function updatePreview() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Calculer les tranches horaires à partir des heures sélectionnées
|
// Calculer les tranches horaires à partir des heures sélectionnées
|
||||||
// Chaque heure sélectionnée = 1 tranche d'1 heure
|
let tranches = [];
|
||||||
const tranches = heuresChecked.map(heureDebut => {
|
|
||||||
const [h, m] = heureDebut.split(':').map(Number);
|
if (dureePermanence === '15min') {
|
||||||
const heureFin = `${String(h + 1).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
|
// Mode 15 minutes : créer des tranches de 15 minutes
|
||||||
return {
|
heuresChecked.forEach(heureDebut => {
|
||||||
debut: heureDebut,
|
const [h, m] = heureDebut.split(':').map(Number);
|
||||||
fin: heureFin
|
|
||||||
};
|
for (let i = 0; i < nbTranches; i++) {
|
||||||
});
|
const minutesDebut = m + (i * 15);
|
||||||
|
const minutesFin = m + ((i + 1) * 15);
|
||||||
|
|
||||||
|
const trancheDebut = `${String(h).padStart(2, '0')}:${String(minutesDebut).padStart(2, '0')}`;
|
||||||
|
const trancheFin = `${String(h).padStart(2, '0')}:${String(minutesFin).padStart(2, '0')}`;
|
||||||
|
|
||||||
|
tranches.push({
|
||||||
|
debut: trancheDebut,
|
||||||
|
fin: trancheFin
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Mode 1 heure : chaque heure sélectionnée = 1 tranche d'1 heure
|
||||||
|
tranches = heuresChecked.map(heureDebut => {
|
||||||
|
const [h, m] = heureDebut.split(':').map(Number);
|
||||||
|
const heureFin = `${String(h + 1).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
|
||||||
|
return {
|
||||||
|
debut: heureDebut,
|
||||||
|
fin: heureFin
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Calculer les dates à partir du mois de début sélectionné
|
// Calculer les dates à partir du mois de début sélectionné
|
||||||
const [year, month] = moisDebut.split('-').map(Number);
|
const [year, month] = moisDebut.split('-').map(Number);
|
||||||
|
|||||||
@ -97,4 +97,13 @@ export async function getFilters(type, params = {}) {
|
|||||||
return apiFetch(endpoint);
|
return apiFetch(endpoint);
|
||||||
}
|
}
|
||||||
return apiFetch(`filters/${type}`);
|
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,7 +1,8 @@
|
|||||||
// Module ES6 pour la création d'entités depuis le modal d'événement
|
// Module ES6 pour la création d'entités depuis le modal d'événement
|
||||||
import { apiFetch } from './agenda-api.js';
|
import { apiFetch } from './agenda-api.js';
|
||||||
import { notifyError, notifySuccess } from './agenda-notifications.js';
|
import { notifyError, notifySuccess } from './agenda-notifications.js';
|
||||||
import { populateSelects, preserveModalData } from './agenda-modal.js';
|
import { openSubModal } from './agenda-modal.js';
|
||||||
|
import { populateSelects } from './agenda-modal-select.js';
|
||||||
|
|
||||||
// Configuration des entités
|
// Configuration des entités
|
||||||
const ENTITY_CONFIG = {
|
const ENTITY_CONFIG = {
|
||||||
@ -97,49 +98,18 @@ function openCreateEntityModal(entityType) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const modal = document.getElementById(config.modalId);
|
// Utiliser la fonction générique pour ouvrir la sous-modale
|
||||||
if (!modal) {
|
openSubModal(
|
||||||
console.error(`Modal non trouvé: ${config.modalId}`);
|
config.modalId,
|
||||||
return;
|
// Callback avant ouverture : réinitialiser le formulaire
|
||||||
}
|
(subModal) => {
|
||||||
|
const form = document.getElementById(config.formId);
|
||||||
// Réinitialiser le formulaire
|
if (form) {
|
||||||
const form = document.getElementById(config.formId);
|
form.reset();
|
||||||
if (form) {
|
}
|
||||||
form.reset();
|
// Les selects sont maintenant initialisés automatiquement par jQuery('.select2').select2()
|
||||||
}
|
|
||||||
|
|
||||||
// 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()
|
// Les selects sont maintenant initialisés automatiquement par jQuery('.select2').select2()
|
||||||
|
|||||||
623
assets/js/modules/agenda-event-mapper.js
Normal file
623
assets/js/modules/agenda-event-mapper.js
Normal file
@ -0,0 +1,623 @@
|
|||||||
|
// Module de mapping des événements pour FullCalendar
|
||||||
|
// Contient toute la logique de transformation des données API vers le format FullCalendar
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calcule la luminosité d'une couleur hexadécimale
|
||||||
|
* @param {string} hexColor - Couleur au format #RRGGBB
|
||||||
|
* @returns {number} - Luminosité entre 0 et 1
|
||||||
|
*/
|
||||||
|
export function getLuminance(hexColor) {
|
||||||
|
// Vérifier que hexColor n'est pas null ou undefined
|
||||||
|
if (!hexColor || typeof hexColor !== 'string') {
|
||||||
|
console.warn('⚠️ [getLuminance] Valeur hexColor invalide:', hexColor);
|
||||||
|
return 0.5; // Retourner une valeur moyenne par défaut
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convertir hex en RGB
|
||||||
|
const hex = hexColor.replace('#', '');
|
||||||
|
const r = parseInt(hex.substr(0, 2), 16);
|
||||||
|
const g = parseInt(hex.substr(2, 2), 16);
|
||||||
|
const b = parseInt(hex.substr(4, 2), 16);
|
||||||
|
|
||||||
|
// Calculer la luminosité relative selon WCAG
|
||||||
|
return (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Détermine la couleur de texte optimale selon le contraste
|
||||||
|
* @param {string} backgroundColor - Couleur de fond au format #RRGGBB
|
||||||
|
* @returns {string} - Couleur de texte (#000000 ou #ffffff)
|
||||||
|
*/
|
||||||
|
export function getTextColor(backgroundColor) {
|
||||||
|
// Vérifier que backgroundColor n'est pas null ou undefined
|
||||||
|
if (!backgroundColor || typeof backgroundColor !== 'string') {
|
||||||
|
console.warn('⚠️ [getTextColor] Valeur backgroundColor invalide:', backgroundColor);
|
||||||
|
return '#000000'; // Retourner noir par défaut
|
||||||
|
}
|
||||||
|
|
||||||
|
const luminance = getLuminance(backgroundColor);
|
||||||
|
return luminance > 0.5 ? '#000000' : '#ffffff';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mappe un événement API vers le format FullCalendar
|
||||||
|
* @param {Object} ev - Événement depuis l'API
|
||||||
|
* @returns {Object} - Événement au format FullCalendar
|
||||||
|
*/
|
||||||
|
export function mapEventToFullCalendar(ev) {
|
||||||
|
// Hiérarchie de détermination des couleurs :
|
||||||
|
// 1) Pour RDV assignés (individuel/groupe) avec type d'intervention : couleur du type d'intervention
|
||||||
|
// 2) Pour RDV assignés (individuel/groupe) sans type d'intervention : orange (par défaut)
|
||||||
|
// 3) Pour permanences : couleur selon le type (assignée, non attribuée, non disponible)
|
||||||
|
|
||||||
|
let backgroundColor = null;
|
||||||
|
let textColor = null;
|
||||||
|
|
||||||
|
// Vérifier si l'événement est assigné (a un intervenant et un local/beneficiaire)
|
||||||
|
const isEventAssigned = ev.id_intervenant && (ev.id_local || ev.id_beneficiaire);
|
||||||
|
|
||||||
|
// Pour les RDV (individuel/groupe) assignés
|
||||||
|
if (ev.type && ev.type !== 'permanence' && isEventAssigned) {
|
||||||
|
// Ordre de priorité : département > type d'intervention > local > défaut
|
||||||
|
|
||||||
|
// Priorité 1 : Couleur du département
|
||||||
|
const departementId = ev.id_departement ? parseInt(ev.id_departement) : null;
|
||||||
|
|
||||||
|
// 🔍 DEBUG pour événement 410
|
||||||
|
if (ev.id === 410 || ev.id === '410') {
|
||||||
|
console.log('🔍 [MAPPER DEBUG 410 - RDV] Début analyse couleur département:', {
|
||||||
|
eventId: ev.id,
|
||||||
|
type: ev.type,
|
||||||
|
id_departement_brut: ev.id_departement,
|
||||||
|
departementId_parsed: departementId,
|
||||||
|
crviACFData_existe: !!window.crviACFData,
|
||||||
|
departements_existe: !!(window.crviACFData && window.crviACFData.departements)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (departementId && !isNaN(departementId) && window.crviACFData && window.crviACFData.departements) {
|
||||||
|
// 🔍 DEBUG pour événement 410 - liste des départements
|
||||||
|
if (ev.id === 410 || ev.id === '410') {
|
||||||
|
console.log('🔍 [MAPPER DEBUG 410 - RDV] Recherche département ID:', departementId);
|
||||||
|
console.log('🔍 [MAPPER DEBUG 410 - RDV] Départements disponibles:',
|
||||||
|
Object.keys(window.crviACFData.departements).map(key => ({
|
||||||
|
key: key,
|
||||||
|
id: window.crviACFData.departements[key].id,
|
||||||
|
nom: window.crviACFData.departements[key].nom,
|
||||||
|
couleur: window.crviACFData.departements[key].couleur
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chercher le département par ID
|
||||||
|
for (const key in window.crviACFData.departements) {
|
||||||
|
const dept = window.crviACFData.departements[key];
|
||||||
|
|
||||||
|
// 🔍 DEBUG pour événement 410 - comparaison
|
||||||
|
if (ev.id === 410 || ev.id === '410') {
|
||||||
|
console.log('🔍 [MAPPER DEBUG 410 - RDV] Comparaison:', {
|
||||||
|
key: key,
|
||||||
|
dept_id: dept.id,
|
||||||
|
dept_id_type: typeof dept.id,
|
||||||
|
recherche_id: departementId,
|
||||||
|
recherche_id_type: typeof departementId,
|
||||||
|
sont_egaux: dept.id === departementId,
|
||||||
|
dept_couleur: dept.couleur
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dept.id === departementId && dept.couleur) {
|
||||||
|
backgroundColor = dept.couleur;
|
||||||
|
textColor = getTextColor(backgroundColor);
|
||||||
|
console.log('🎨 [COULEUR] RDV assigné - département:', {
|
||||||
|
eventId: ev.id,
|
||||||
|
type: ev.type,
|
||||||
|
departementId: departementId,
|
||||||
|
departementNom: dept.nom,
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
source: 'departement'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 🔍 DEBUG pour événement 410 - succès
|
||||||
|
if (ev.id === 410 || ev.id === '410') {
|
||||||
|
console.log('✅ [MAPPER DEBUG 410 - RDV] Couleur département appliquée:', {
|
||||||
|
departementNom: dept.nom,
|
||||||
|
couleurAppliquee: backgroundColor
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔍 DEBUG pour événement 410 - échec de recherche
|
||||||
|
if ((ev.id === 410 || ev.id === '410') && !backgroundColor) {
|
||||||
|
console.warn('⚠️ [MAPPER DEBUG 410 - RDV] Aucun département correspondant trouvé!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priorité 2 : Type d'intervention (si pas de département)
|
||||||
|
if (!backgroundColor) {
|
||||||
|
let typeInterventionId = null;
|
||||||
|
if (ev.id_type_intervention) {
|
||||||
|
typeInterventionId = parseInt(ev.id_type_intervention);
|
||||||
|
} else if (ev.type_intervention && ev.type_intervention.id) {
|
||||||
|
typeInterventionId = parseInt(ev.type_intervention.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si l'événement a un type d'intervention, utiliser sa couleur depuis crviAjax
|
||||||
|
if (typeInterventionId && !isNaN(typeInterventionId) && window.crviAjax && window.crviAjax.couleurs_types_intervention) {
|
||||||
|
const couleurTypeIntervention = window.crviAjax.couleurs_types_intervention[typeInterventionId];
|
||||||
|
if (couleurTypeIntervention) {
|
||||||
|
backgroundColor = couleurTypeIntervention;
|
||||||
|
textColor = getTextColor(couleurTypeIntervention);
|
||||||
|
console.log('🎨 [COULEUR] RDV assigné avec type d\'intervention:', {
|
||||||
|
eventId: ev.id,
|
||||||
|
type: ev.type,
|
||||||
|
typeInterventionId: typeInterventionId,
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
textColor: textColor,
|
||||||
|
source: 'type_intervention'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priorité 3 : Couleur du local (type de local)
|
||||||
|
if (!backgroundColor && ev.local) {
|
||||||
|
const localType = ev.local.type || ev.local_type;
|
||||||
|
if (localType && window.crviACFData && window.crviACFData.types_local) {
|
||||||
|
const typeLocalConfig = window.crviACFData.types_local[localType];
|
||||||
|
if (typeLocalConfig && typeLocalConfig.couleur) {
|
||||||
|
backgroundColor = typeLocalConfig.couleur;
|
||||||
|
textColor = getTextColor(backgroundColor);
|
||||||
|
console.log('🎨 [COULEUR] RDV assigné - type de local:', {
|
||||||
|
eventId: ev.id,
|
||||||
|
type: ev.type,
|
||||||
|
localType: localType,
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
source: 'type_local'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback : orange par défaut
|
||||||
|
if (!backgroundColor) {
|
||||||
|
backgroundColor = '#ff9800'; // Orange pour les événements assignés sans type d'intervention
|
||||||
|
textColor = getTextColor(backgroundColor);
|
||||||
|
console.log('🎨 [COULEUR] RDV assigné sans type d\'intervention (défaut orange):', {
|
||||||
|
eventId: ev.id,
|
||||||
|
type: ev.type,
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
textColor: textColor,
|
||||||
|
source: 'defaut_assigné'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pour les RDV non assignés : couleur selon type_de_local (bureau ou salle)
|
||||||
|
if (!backgroundColor && ev.type && ev.type !== 'permanence' && !isEventAssigned) {
|
||||||
|
// Vérifier si l'événement a un local avec un type_de_local
|
||||||
|
if (ev.local && ev.local.type_de_local) {
|
||||||
|
const typeLocal = ev.local.type_de_local.toLowerCase();
|
||||||
|
if (window.crviACFData && window.crviACFData.couleurs_rdv) {
|
||||||
|
const couleurRdv = window.crviACFData.couleurs_rdv[typeLocal];
|
||||||
|
if (couleurRdv) {
|
||||||
|
backgroundColor = couleurRdv;
|
||||||
|
textColor = getTextColor(couleurRdv);
|
||||||
|
console.log('🎨 [COULEUR] RDV non assigné selon type de local:', {
|
||||||
|
eventId: ev.id,
|
||||||
|
type: ev.type,
|
||||||
|
typeLocal: typeLocal,
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
textColor: textColor,
|
||||||
|
source: 'type_local'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pour permanences (événements de type 'permanence') : logique simplifiée
|
||||||
|
// 1) Non attribuée : dispo ? couleur dispo : couleur indispo
|
||||||
|
// 2) Attribuée : a type intervention ? couleur type : couleur défaut
|
||||||
|
let isPermanenceDisabled = false;
|
||||||
|
if (!backgroundColor && ev.type === 'permanence') {
|
||||||
|
// Une permanence est assignée si elle a un bénéficiaire ET un local (assign = 1)
|
||||||
|
const isPermanenceAssigned = ev.assign === 1 || (ev.id_beneficiaire && ev.id_local);
|
||||||
|
|
||||||
|
// Une permanence est non attribuée si elle n'est pas assignée (assign = 0)
|
||||||
|
const isPermanenceNonAttribuee = ev.assign === 0 && !ev.id_beneficiaire && !ev.id_local;
|
||||||
|
|
||||||
|
// Vérifier si le bénéficiaire est en congé (indisponibilitee_ponctuelle)
|
||||||
|
let isPermanenceNonDisponible = false;
|
||||||
|
if (ev.beneficiaire && ev.beneficiaire.indisponibilitee_ponctuelle && Array.isArray(ev.beneficiaire.indisponibilitee_ponctuelle)) {
|
||||||
|
const eventDate = ev.date_rdv; // Format YYYY-MM-DD
|
||||||
|
const eventDateObj = new Date(eventDate + 'T00:00:00');
|
||||||
|
|
||||||
|
// Vérifier si la date de l'événement est dans une période d'indisponibilité
|
||||||
|
for (const indispo of ev.beneficiaire.indisponibilitee_ponctuelle) {
|
||||||
|
if (indispo.debut && indispo.fin) {
|
||||||
|
let debutDate, finDate;
|
||||||
|
|
||||||
|
// Gérer le format d/m/Y (format ACF)
|
||||||
|
if (typeof indispo.debut === 'string' && indispo.debut.includes('/')) {
|
||||||
|
const debutParts = indispo.debut.split('/');
|
||||||
|
const finParts = indispo.fin.split('/');
|
||||||
|
|
||||||
|
if (debutParts.length === 3 && finParts.length === 3) {
|
||||||
|
// Format d/m/Y
|
||||||
|
debutDate = new Date(parseInt(debutParts[2]), parseInt(debutParts[1]) - 1, parseInt(debutParts[0]));
|
||||||
|
finDate = new Date(parseInt(finParts[2]), parseInt(finParts[1]) - 1, parseInt(finParts[0]));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Format YYYY-MM-DD ou timestamp
|
||||||
|
debutDate = new Date(indispo.debut);
|
||||||
|
finDate = new Date(indispo.fin);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (debutDate && finDate && !isNaN(debutDate.getTime()) && !isNaN(finDate.getTime())) {
|
||||||
|
// Ajuster pour inclure toute la journée
|
||||||
|
debutDate.setHours(0, 0, 0, 0);
|
||||||
|
finDate.setHours(23, 59, 59, 999);
|
||||||
|
|
||||||
|
if (eventDateObj >= debutDate && eventDateObj <= finDate) {
|
||||||
|
isPermanenceNonDisponible = true;
|
||||||
|
isPermanenceDisabled = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier si l'intervenant est indisponible (congés ou jour non disponible)
|
||||||
|
if (!isPermanenceNonDisponible && ev.id_intervenant && window.crviACFData && window.crviACFData.indisponibilites_intervenants) {
|
||||||
|
const intervenantId = parseInt(ev.id_intervenant);
|
||||||
|
const intervenantDispo = window.crviACFData.indisponibilites_intervenants[intervenantId];
|
||||||
|
|
||||||
|
if (intervenantDispo) {
|
||||||
|
const eventDate = ev.date_rdv; // Format YYYY-MM-DD
|
||||||
|
const eventDateObj = new Date(eventDate + 'T00:00:00');
|
||||||
|
|
||||||
|
// Vérifier les jours de disponibilité (0 = dimanche, 1 = lundi, etc.)
|
||||||
|
const dayOfWeek = eventDateObj.getDay();
|
||||||
|
const dayNames = ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'];
|
||||||
|
const dayName = dayNames[dayOfWeek];
|
||||||
|
|
||||||
|
// Si l'intervenant a des jours de disponibilité définis et que ce jour n'en fait pas partie
|
||||||
|
if (intervenantDispo.jours_dispo && Array.isArray(intervenantDispo.jours_dispo) &&
|
||||||
|
intervenantDispo.jours_dispo.length > 0 &&
|
||||||
|
!intervenantDispo.jours_dispo.includes(dayName)) {
|
||||||
|
console.log('🚫 [PERMANENCE] Intervenant non disponible ce jour:', {
|
||||||
|
intervenantId,
|
||||||
|
date: eventDate,
|
||||||
|
dayName,
|
||||||
|
joursDisponibles: intervenantDispo.jours_dispo
|
||||||
|
});
|
||||||
|
isPermanenceNonDisponible = true;
|
||||||
|
isPermanenceDisabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier les congés/indisponibilités ponctuelles
|
||||||
|
if (!isPermanenceNonDisponible && intervenantDispo.conges && Array.isArray(intervenantDispo.conges)) {
|
||||||
|
for (const conge of intervenantDispo.conges) {
|
||||||
|
if (conge.debut && conge.fin) {
|
||||||
|
let debutDate, finDate;
|
||||||
|
|
||||||
|
// Gérer le format d/m/Y (format ACF)
|
||||||
|
if (typeof conge.debut === 'string' && conge.debut.includes('/')) {
|
||||||
|
const debutParts = conge.debut.split('/');
|
||||||
|
const finParts = conge.fin.split('/');
|
||||||
|
|
||||||
|
if (debutParts.length === 3 && finParts.length === 3) {
|
||||||
|
debutDate = new Date(parseInt(debutParts[2]), parseInt(debutParts[1]) - 1, parseInt(debutParts[0]));
|
||||||
|
finDate = new Date(parseInt(finParts[2]), parseInt(finParts[1]) - 1, parseInt(finParts[0]));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Format YYYY-MM-DD ou timestamp
|
||||||
|
debutDate = new Date(conge.debut);
|
||||||
|
finDate = new Date(conge.fin);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (debutDate && finDate && !isNaN(debutDate.getTime()) && !isNaN(finDate.getTime())) {
|
||||||
|
debutDate.setHours(0, 0, 0, 0);
|
||||||
|
finDate.setHours(23, 59, 59, 999);
|
||||||
|
|
||||||
|
if (eventDateObj >= debutDate && eventDateObj <= finDate) {
|
||||||
|
console.log('🚫 [PERMANENCE] Intervenant en congé:', {
|
||||||
|
intervenantId,
|
||||||
|
date: eventDate,
|
||||||
|
congeDebut: conge.debut,
|
||||||
|
congeFin: conge.fin,
|
||||||
|
congeType: conge.type
|
||||||
|
});
|
||||||
|
isPermanenceNonDisponible = true;
|
||||||
|
isPermanenceDisabled = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier aussi le statut si pas déjà déterminé
|
||||||
|
if (!isPermanenceNonDisponible && ev.statut && ['annule', 'non_tenu', 'absence'].includes(ev.statut)) {
|
||||||
|
isPermanenceNonDisponible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// LOGIQUE SIMPLIFIÉE
|
||||||
|
if (isPermanenceNonAttribuee) {
|
||||||
|
// 1) Non attribuée : dispo ? couleur dispo : couleur indispo
|
||||||
|
if (isPermanenceNonDisponible && window.crviAjax && window.crviAjax.couleur_permanence_non_disponible) {
|
||||||
|
backgroundColor = window.crviAjax.couleur_permanence_non_disponible;
|
||||||
|
console.log('🎨 [COULEUR] Permanence non attribuée - indisponible:', {
|
||||||
|
eventId: ev.id,
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
source: 'permanence_non_attribuee_indispo'
|
||||||
|
});
|
||||||
|
} else if (window.crviAjax && window.crviAjax.couleur_permanence_non_attribuee) {
|
||||||
|
backgroundColor = window.crviAjax.couleur_permanence_non_attribuee;
|
||||||
|
console.log('🎨 [COULEUR] Permanence non attribuée - disponible:', {
|
||||||
|
eventId: ev.id,
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
source: 'permanence_non_attribuee_dispo'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (isPermanenceAssigned) {
|
||||||
|
// 2) Attribuée : ordre de priorité - département puis local
|
||||||
|
|
||||||
|
// Priorité 1 : Couleur du département
|
||||||
|
const departementId = ev.id_departement ? parseInt(ev.id_departement) : null;
|
||||||
|
|
||||||
|
// 🔍 DEBUG pour événement 410
|
||||||
|
if (ev.id === 410 || ev.id === '410') {
|
||||||
|
console.log('🔍 [MAPPER DEBUG 410] Début analyse couleur département:', {
|
||||||
|
eventId: ev.id,
|
||||||
|
id_departement_brut: ev.id_departement,
|
||||||
|
departementId_parsed: departementId,
|
||||||
|
crviACFData_existe: !!window.crviACFData,
|
||||||
|
departements_existe: !!(window.crviACFData && window.crviACFData.departements),
|
||||||
|
departements_data: window.crviACFData ? window.crviACFData.departements : 'N/A'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (departementId && !isNaN(departementId) && window.crviACFData && window.crviACFData.departements) {
|
||||||
|
// 🔍 DEBUG pour événement 410 - liste des départements
|
||||||
|
if (ev.id === 410 || ev.id === '410') {
|
||||||
|
console.log('🔍 [MAPPER DEBUG 410] Recherche département ID:', departementId);
|
||||||
|
console.log('🔍 [MAPPER DEBUG 410] Départements disponibles:',
|
||||||
|
Object.keys(window.crviACFData.departements).map(key => ({
|
||||||
|
key: key,
|
||||||
|
id: window.crviACFData.departements[key].id,
|
||||||
|
nom: window.crviACFData.departements[key].nom,
|
||||||
|
couleur: window.crviACFData.departements[key].couleur
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chercher le département par ID
|
||||||
|
for (const key in window.crviACFData.departements) {
|
||||||
|
const dept = window.crviACFData.departements[key];
|
||||||
|
|
||||||
|
// 🔍 DEBUG pour événement 410 - comparaison
|
||||||
|
if (ev.id === 410 || ev.id === '410') {
|
||||||
|
console.log('🔍 [MAPPER DEBUG 410] Comparaison:', {
|
||||||
|
key: key,
|
||||||
|
dept_id: dept.id,
|
||||||
|
dept_id_type: typeof dept.id,
|
||||||
|
recherche_id: departementId,
|
||||||
|
recherche_id_type: typeof departementId,
|
||||||
|
sont_egaux: dept.id === departementId,
|
||||||
|
dept_couleur: dept.couleur
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dept.id === departementId && dept.couleur) {
|
||||||
|
backgroundColor = dept.couleur;
|
||||||
|
console.log('🎨 [COULEUR] Permanence assignée - département:', {
|
||||||
|
eventId: ev.id,
|
||||||
|
departementId: departementId,
|
||||||
|
departementNom: dept.nom,
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
source: 'departement'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 🔍 DEBUG pour événement 410 - succès
|
||||||
|
if (ev.id === 410 || ev.id === '410') {
|
||||||
|
console.log('✅ [MAPPER DEBUG 410] Couleur département appliquée:', {
|
||||||
|
departementNom: dept.nom,
|
||||||
|
couleurAppliquee: backgroundColor
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔍 DEBUG pour événement 410 - échec de recherche
|
||||||
|
if ((ev.id === 410 || ev.id === '410') && !backgroundColor) {
|
||||||
|
console.warn('⚠️ [MAPPER DEBUG 410] Aucun département correspondant trouvé!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priorité 2 : Couleur du local (type de local)
|
||||||
|
if (!backgroundColor && ev.local) {
|
||||||
|
const localType = ev.local.type || ev.local_type;
|
||||||
|
if (localType && window.crviACFData && window.crviACFData.types_local) {
|
||||||
|
const typeLocalConfig = window.crviACFData.types_local[localType];
|
||||||
|
if (typeLocalConfig && typeLocalConfig.couleur) {
|
||||||
|
backgroundColor = typeLocalConfig.couleur;
|
||||||
|
console.log('🎨 [COULEUR] Permanence assignée - type de local:', {
|
||||||
|
eventId: ev.id,
|
||||||
|
localType: localType,
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
source: 'type_local'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback : couleur par défaut des permanences
|
||||||
|
if (!backgroundColor) {
|
||||||
|
backgroundColor = (window.crviACFData && window.crviACFData.couleurs_permanence && window.crviACFData.couleurs_permanence.permanence)
|
||||||
|
? window.crviACFData.couleurs_permanence.permanence
|
||||||
|
: '#9e9e9e';
|
||||||
|
console.log('🎨 [COULEUR] Permanence assignée - défaut:', {
|
||||||
|
eventId: ev.id,
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
source: 'permanence_assignee_defaut'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// S'assurer que backgroundColor n'est pas null avant d'appeler getTextColor
|
||||||
|
if (backgroundColor) {
|
||||||
|
textColor = getTextColor(backgroundColor);
|
||||||
|
} else {
|
||||||
|
textColor = '#000000'; // Par défaut
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Couleur par défaut si aucune condition n'est remplie
|
||||||
|
if (!backgroundColor) {
|
||||||
|
backgroundColor = '#6c757d'; // Gris par défaut
|
||||||
|
textColor = getTextColor(backgroundColor);
|
||||||
|
console.log('🎨 [COULEUR] Couleur par défaut (gris):', {
|
||||||
|
eventId: ev.id,
|
||||||
|
type: ev.type,
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
textColor: textColor,
|
||||||
|
source: 'defaut_general'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔍 DEBUG FINAL pour événement 410
|
||||||
|
if (ev.id === 410 || ev.id === '410') {
|
||||||
|
console.log('🔍 [MAPPER DEBUG 410] COULEUR FINALE avant return:', {
|
||||||
|
eventId: ev.id,
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
textColor: textColor,
|
||||||
|
type: ev.type,
|
||||||
|
id_departement: ev.id_departement,
|
||||||
|
isPermanenceAssigned: ev.assign === 1 || (ev.id_beneficiaire && ev.id_local),
|
||||||
|
ev_complet: ev
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log final de la couleur appliquée
|
||||||
|
console.log('🎨 [COULEUR FINALE] Événement:', {
|
||||||
|
eventId: ev.id,
|
||||||
|
title: ev.type || 'Sans type',
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
borderColor: backgroundColor,
|
||||||
|
textColor: textColor,
|
||||||
|
isAssigned: isEventAssigned,
|
||||||
|
type: ev.type
|
||||||
|
});
|
||||||
|
|
||||||
|
// Générer un titre plus explicite
|
||||||
|
let title = ev.type || 'Sans type';
|
||||||
|
|
||||||
|
// Gestion spéciale pour les permanences
|
||||||
|
if (ev.type === 'permanence') {
|
||||||
|
if (ev.intervenant) {
|
||||||
|
const intervenant = ev.intervenant;
|
||||||
|
const nomComplet = `${intervenant.prenom || ''} ${intervenant.nom || ''}`.trim();
|
||||||
|
title = 'p. ' + (nomComplet || 'Intervenant');
|
||||||
|
} else {
|
||||||
|
title = 'p. Intervenant';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Utiliser le nom de l'intervenant comme titre principal pour les autres événements
|
||||||
|
if (ev.intervenant) {
|
||||||
|
const intervenant = ev.intervenant;
|
||||||
|
const nomComplet = `${intervenant.prenom || ''} ${intervenant.nom || ''}`.trim();
|
||||||
|
if (nomComplet) {
|
||||||
|
title = nomComplet;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ajouter le type d'intervention principal si disponible
|
||||||
|
if (ev.intervenant && ev.intervenant.types_intervention_noms && ev.intervenant.types_intervention_noms.length > 0) {
|
||||||
|
const primaryType = ev.intervenant.types_intervention_noms[0]; // Premier type d'intervention
|
||||||
|
title += ` - ${primaryType}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ajouter le commentaire si disponible
|
||||||
|
if (ev.commentaire) {
|
||||||
|
title += ' - ' + ev.commentaire;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// S'assurer que les couleurs sont toujours définies
|
||||||
|
if (!backgroundColor || typeof backgroundColor !== 'string') {
|
||||||
|
console.warn('⚠️ [Mapping] backgroundColor invalide pour l\'événement:', ev.id, backgroundColor);
|
||||||
|
backgroundColor = '#6c757d'; // Gris par défaut
|
||||||
|
}
|
||||||
|
if (!textColor || typeof textColor !== 'string') {
|
||||||
|
textColor = getTextColor(backgroundColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier si l'événement est passé
|
||||||
|
const eventStartDate = new Date(ev.date_rdv + 'T' + ev.heure_rdv);
|
||||||
|
const now = new Date();
|
||||||
|
const isEventPast = eventStartDate < now;
|
||||||
|
|
||||||
|
// Un événement est éditable seulement si :
|
||||||
|
// 1. Il n'est pas désactivé (permanence)
|
||||||
|
// 2. Il n'est pas passé
|
||||||
|
const isEditable = !isPermanenceDisabled && !isEventPast;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: ev.id,
|
||||||
|
title: title,
|
||||||
|
start: ev.date_rdv + 'T' + ev.heure_rdv,
|
||||||
|
end: ev.date_fin + 'T' + ev.heure_fin,
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
borderColor: backgroundColor,
|
||||||
|
textColor: textColor,
|
||||||
|
editable: isEditable, // Désactiver l'édition si la permanence est désactivée OU si l'événement est passé
|
||||||
|
durationEditable: isEditable, // Désactiver le redimensionnement si la permanence est désactivée OU si l'événement est passé
|
||||||
|
startEditable: isEditable, // Désactiver le déplacement si la permanence est désactivée OU si l'événement est passé
|
||||||
|
classNames: isPermanenceDisabled ? ['permanence-disabled'] : (isEventPast ? ['event-past'] : []),
|
||||||
|
extendedProps: {
|
||||||
|
// Données de base
|
||||||
|
type: ev.type,
|
||||||
|
commentaire: ev.commentaire,
|
||||||
|
date_rdv: ev.date_rdv,
|
||||||
|
heure_rdv: ev.heure_rdv,
|
||||||
|
date_fin: ev.date_fin,
|
||||||
|
heure_fin: ev.heure_fin,
|
||||||
|
|
||||||
|
// Relations avec les entités
|
||||||
|
id_beneficiaire: ev.id_beneficiaire,
|
||||||
|
id_intervenant: ev.id_intervenant,
|
||||||
|
id_traducteur: ev.id_traducteur,
|
||||||
|
id_local: ev.id_local,
|
||||||
|
id_departement: ev.id_departement,
|
||||||
|
id_type_intervention: ev.id_type_intervention || (ev.type_intervention && ev.type_intervention.id ? ev.type_intervention.id : null),
|
||||||
|
langue: ev.langue,
|
||||||
|
langues_disponibles: ev.langues_disponibles || null,
|
||||||
|
|
||||||
|
// Données des entités (si disponibles)
|
||||||
|
beneficiaire: ev.beneficiaire,
|
||||||
|
intervenant: ev.intervenant,
|
||||||
|
traducteur: ev.traducteur,
|
||||||
|
local: ev.local,
|
||||||
|
|
||||||
|
// Données spécifiques aux groupes
|
||||||
|
nb_participants: ev.nb_participants,
|
||||||
|
nb_hommes: ev.nb_hommes,
|
||||||
|
nb_femmes: ev.nb_femmes,
|
||||||
|
|
||||||
|
// Autres données
|
||||||
|
statut: ev.statut,
|
||||||
|
assign: ev.assign || 0,
|
||||||
|
isDisabled: isPermanenceDisabled,
|
||||||
|
created_at: ev.created_at,
|
||||||
|
updated_at: ev.updated_at
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -1,46 +1,10 @@
|
|||||||
// Module de gestion des filtres dynamiques pour l'agenda
|
// Module de gestion des filtres dynamiques pour l'agenda
|
||||||
import { getEvents } from './agenda-api.js';
|
import { getEvents } from './agenda-api.js';
|
||||||
|
import { mapEventToFullCalendar } from './agenda-event-mapper.js';
|
||||||
|
|
||||||
let currentFilters = {};
|
let currentFilters = {};
|
||||||
let calendarInstance = null;
|
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
|
* Initialise les filtres dynamiques
|
||||||
* @param {Object} calendar - Instance FullCalendar
|
* @param {Object} calendar - Instance FullCalendar
|
||||||
@ -67,37 +31,7 @@ export function initializeFilters(calendar) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const apiEvents = await getEvents(params);
|
const apiEvents = await getEvents(params);
|
||||||
const events = apiEvents.map(ev => ({
|
const events = apiEvents.map(ev => mapEventToFullCalendar(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);
|
successCallback(events);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Erreur lors du chargement des événements:', e);
|
console.error('Erreur lors du chargement des événements:', e);
|
||||||
@ -215,10 +149,16 @@ function collectFilters() {
|
|||||||
filters.intervenant = personne;
|
filters.intervenant = personne;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Département
|
||||||
|
const departement = document.getElementById('departement');
|
||||||
|
if (departement && departement.value && departement.value.trim() !== '') {
|
||||||
|
filters.departement = departement.value;
|
||||||
|
}
|
||||||
|
|
||||||
// Type d'intervention
|
// Type d'intervention
|
||||||
const typeIntervention = document.getElementById('type_intervention').value;
|
const typeIntervention = document.getElementById('type_intervention');
|
||||||
if (typeIntervention && typeIntervention.trim() !== '') {
|
if (typeIntervention && typeIntervention.value && typeIntervention.value.trim() !== '') {
|
||||||
filters.type_intervention = typeIntervention;
|
filters.type_intervention = typeIntervention.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bénéficiaire
|
// Bénéficiaire
|
||||||
@ -228,7 +168,7 @@ function collectFilters() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Langue
|
// Langue
|
||||||
const langue = document.getElementById('langue').value;
|
const langue = document.getElementById('langue_filtre')?.value;
|
||||||
if (langue && langue.trim() !== '') {
|
if (langue && langue.trim() !== '') {
|
||||||
filters.langue = langue;
|
filters.langue = langue;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { openModal } from './agenda-modal.js';
|
|||||||
import { getEvents, updateEvent, getEvent } from './agenda-api.js';
|
import { getEvents, updateEvent, getEvent } from './agenda-api.js';
|
||||||
import { notifyError } from './agenda-notifications.js';
|
import { notifyError } from './agenda-notifications.js';
|
||||||
import { initializeFilters } from './agenda-filters.js';
|
import { initializeFilters } from './agenda-filters.js';
|
||||||
|
import { mapEventToFullCalendar, getTextColor, getLuminance } from './agenda-event-mapper.js';
|
||||||
import toastr from 'toastr';
|
import toastr from 'toastr';
|
||||||
import { Calendar } from '@fullcalendar/core';
|
import { Calendar } from '@fullcalendar/core';
|
||||||
import dayGridPlugin from '@fullcalendar/daygrid';
|
import dayGridPlugin from '@fullcalendar/daygrid';
|
||||||
@ -12,66 +13,6 @@ import interactionPlugin from '@fullcalendar/interaction';
|
|||||||
import { apiFetch } from './agenda-api.js';
|
import { apiFetch } from './agenda-api.js';
|
||||||
// Ajoutez d'autres plugins si besoin
|
// 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() {
|
export function initializeCalendar() {
|
||||||
console.log('🚀 Initialisation de FullCalendar...');
|
console.log('🚀 Initialisation de FullCalendar...');
|
||||||
const calendarEl = document.getElementById('agenda-calendar');
|
const calendarEl = document.getElementById('agenda-calendar');
|
||||||
@ -139,451 +80,7 @@ export function initializeCalendar() {
|
|||||||
};
|
};
|
||||||
const apiEvents = await getEvents(params);
|
const apiEvents = await getEvents(params);
|
||||||
// Mapping des objets API vers le format FullCalendar
|
// Mapping des objets API vers le format FullCalendar
|
||||||
const events = apiEvents.map(ev => {
|
const events = apiEvents.map(ev => mapEventToFullCalendar(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);
|
successCallback(events);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Erreur lors du chargement des événements:', e);
|
console.error('Erreur lors du chargement des événements:', e);
|
||||||
@ -1061,16 +558,97 @@ export function initializeCalendar() {
|
|||||||
bgColor = window.crviAjax.couleur_permanence_non_attribuee;
|
bgColor = window.crviAjax.couleur_permanence_non_attribuee;
|
||||||
}
|
}
|
||||||
} else if (isPermanenceAssigned) {
|
} else if (isPermanenceAssigned) {
|
||||||
// 2) Attribuée : a type intervention ? couleur type : couleur défaut
|
// 2) Attribuée : ordre de priorité - département puis local
|
||||||
const typeInterventionId = eventProps.id_type_intervention ? parseInt(eventProps.id_type_intervention) : null;
|
|
||||||
|
|
||||||
if (typeInterventionId && !isNaN(typeInterventionId) && window.crviAjax && window.crviAjax.couleurs_types_intervention) {
|
// Priorité 1 : Couleur du département
|
||||||
const couleurType = window.crviAjax.couleurs_types_intervention[typeInterventionId];
|
const departementId = eventProps.id_departement ? parseInt(eventProps.id_departement) : null;
|
||||||
if (couleurType) {
|
|
||||||
bgColor = couleurType;
|
// 🔍 DEBUG pour événement 410
|
||||||
|
if (event.id === '410') {
|
||||||
|
console.log('🔍 [DEBUG 410] Début analyse couleur département:', {
|
||||||
|
eventId: event.id,
|
||||||
|
id_departement_brut: eventProps.id_departement,
|
||||||
|
departementId_parsed: departementId,
|
||||||
|
crviACFData_existe: !!crviACFData,
|
||||||
|
departements_existe: !!(crviACFData && crviACFData.departements),
|
||||||
|
departements_data: crviACFData ? crviACFData.departements : 'N/A'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (departementId && !isNaN(departementId) && crviACFData && crviACFData.departements) {
|
||||||
|
// 🔍 DEBUG pour événement 410 - liste des départements
|
||||||
|
if (event.id === '410') {
|
||||||
|
console.log('🔍 [DEBUG 410] Recherche département ID:', departementId);
|
||||||
|
console.log('🔍 [DEBUG 410] Départements disponibles:',
|
||||||
|
Object.keys(crviACFData.departements).map(key => ({
|
||||||
|
key: key,
|
||||||
|
id: crviACFData.departements[key].id,
|
||||||
|
nom: crviACFData.departements[key].nom,
|
||||||
|
couleur: crviACFData.departements[key].couleur
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chercher le département par ID
|
||||||
|
for (const key in crviACFData.departements) {
|
||||||
|
const dept = crviACFData.departements[key];
|
||||||
|
|
||||||
|
// 🔍 DEBUG pour événement 410 - comparaison
|
||||||
|
if (event.id === '410') {
|
||||||
|
console.log('🔍 [DEBUG 410] Comparaison:', {
|
||||||
|
key: key,
|
||||||
|
dept_id: dept.id,
|
||||||
|
dept_id_type: typeof dept.id,
|
||||||
|
recherche_id: departementId,
|
||||||
|
recherche_id_type: typeof departementId,
|
||||||
|
sont_egaux: dept.id === departementId,
|
||||||
|
dept_couleur: dept.couleur
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dept.id === departementId && dept.couleur) {
|
||||||
|
bgColor = dept.couleur;
|
||||||
|
console.log('🎨 [COULEUR] Département trouvé:', {
|
||||||
|
eventId: event.id,
|
||||||
|
departementId: departementId,
|
||||||
|
departementNom: dept.nom,
|
||||||
|
couleur: bgColor
|
||||||
|
});
|
||||||
|
|
||||||
|
// 🔍 DEBUG pour événement 410 - succès
|
||||||
|
if (event.id === '410') {
|
||||||
|
console.log('✅ [DEBUG 410] Couleur département appliquée:', {
|
||||||
|
departementNom: dept.nom,
|
||||||
|
couleurAppliquee: bgColor
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔍 DEBUG pour événement 410 - échec de recherche
|
||||||
|
if (event.id === '410' && !bgColor) {
|
||||||
|
console.warn('⚠️ [DEBUG 410] Aucun département correspondant trouvé!');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Priorité 2 : Couleur du local (type de local)
|
||||||
|
if (!bgColor && eventProps.local) {
|
||||||
|
const localType = eventProps.local.type || eventProps.local_type;
|
||||||
|
if (localType && crviACFData && crviACFData.types_local) {
|
||||||
|
const typeLocalConfig = crviACFData.types_local[localType];
|
||||||
|
if (typeLocalConfig && typeLocalConfig.couleur) {
|
||||||
|
bgColor = typeLocalConfig.couleur;
|
||||||
|
console.log('🎨 [COULEUR] Type de local trouvé:', {
|
||||||
|
eventId: event.id,
|
||||||
|
localType: localType,
|
||||||
|
couleur: bgColor
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback : couleur par défaut des permanences
|
||||||
if (!bgColor) {
|
if (!bgColor) {
|
||||||
bgColor = (crviACFData && crviACFData.couleurs_permanence && crviACFData.couleurs_permanence.permanence)
|
bgColor = (crviACFData && crviACFData.couleurs_permanence && crviACFData.couleurs_permanence.permanence)
|
||||||
? crviACFData.couleurs_permanence.permanence
|
? crviACFData.couleurs_permanence.permanence
|
||||||
@ -1078,16 +656,115 @@ export function initializeCalendar() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Pour les RDV : type d'intervention ou orange par défaut
|
// Pour les RDV : ordre de priorité - département > type d'intervention > local > défaut
|
||||||
const typeInterventionId = eventProps.id_type_intervention ? parseInt(eventProps.id_type_intervention) : null;
|
|
||||||
|
|
||||||
if (typeInterventionId && !isNaN(typeInterventionId) && window.crviAjax && window.crviAjax.couleurs_types_intervention) {
|
// Priorité 1 : Couleur du département
|
||||||
const couleurType = window.crviAjax.couleurs_types_intervention[typeInterventionId];
|
const departementId = eventProps.id_departement ? parseInt(eventProps.id_departement) : null;
|
||||||
if (couleurType) {
|
|
||||||
bgColor = couleurType;
|
// 🔍 DEBUG pour événement 410
|
||||||
|
if (event.id === '410') {
|
||||||
|
console.log('🔍 [DEBUG 410 - RDV] Début analyse couleur département:', {
|
||||||
|
eventId: event.id,
|
||||||
|
type: eventProps.type,
|
||||||
|
id_departement_brut: eventProps.id_departement,
|
||||||
|
departementId_parsed: departementId,
|
||||||
|
crviACFData_existe: !!crviACFData,
|
||||||
|
departements_existe: !!(crviACFData && crviACFData.departements)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (departementId && !isNaN(departementId) && crviACFData && crviACFData.departements) {
|
||||||
|
// 🔍 DEBUG pour événement 410 - liste des départements
|
||||||
|
if (event.id === '410') {
|
||||||
|
console.log('🔍 [DEBUG 410 - RDV] Recherche département ID:', departementId);
|
||||||
|
console.log('🔍 [DEBUG 410 - RDV] Départements disponibles:',
|
||||||
|
Object.keys(crviACFData.departements).map(key => ({
|
||||||
|
key: key,
|
||||||
|
id: crviACFData.departements[key].id,
|
||||||
|
nom: crviACFData.departements[key].nom,
|
||||||
|
couleur: crviACFData.departements[key].couleur
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chercher le département par ID
|
||||||
|
for (const key in crviACFData.departements) {
|
||||||
|
const dept = crviACFData.departements[key];
|
||||||
|
|
||||||
|
// 🔍 DEBUG pour événement 410 - comparaison
|
||||||
|
if (event.id === '410') {
|
||||||
|
console.log('🔍 [DEBUG 410 - RDV] Comparaison:', {
|
||||||
|
key: key,
|
||||||
|
dept_id: dept.id,
|
||||||
|
dept_id_type: typeof dept.id,
|
||||||
|
recherche_id: departementId,
|
||||||
|
recherche_id_type: typeof departementId,
|
||||||
|
sont_egaux: dept.id === departementId,
|
||||||
|
dept_couleur: dept.couleur
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dept.id === departementId && dept.couleur) {
|
||||||
|
bgColor = dept.couleur;
|
||||||
|
console.log('🎨 [COULEUR] RDV - département trouvé:', {
|
||||||
|
eventId: event.id,
|
||||||
|
type: eventProps.type,
|
||||||
|
departementId: departementId,
|
||||||
|
departementNom: dept.nom,
|
||||||
|
couleur: bgColor
|
||||||
|
});
|
||||||
|
|
||||||
|
// 🔍 DEBUG pour événement 410 - succès
|
||||||
|
if (event.id === '410') {
|
||||||
|
console.log('✅ [DEBUG 410 - RDV] Couleur département appliquée:', {
|
||||||
|
departementNom: dept.nom,
|
||||||
|
couleurAppliquee: bgColor
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔍 DEBUG pour événement 410 - échec de recherche
|
||||||
|
if (event.id === '410' && !bgColor) {
|
||||||
|
console.warn('⚠️ [DEBUG 410 - RDV] Aucun département correspondant trouvé!');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Priorité 2 : Type d'intervention (si pas de département)
|
||||||
|
if (!bgColor) {
|
||||||
|
const typeInterventionId = eventProps.id_type_intervention ? parseInt(eventProps.id_type_intervention) : null;
|
||||||
|
|
||||||
|
if (typeInterventionId && !isNaN(typeInterventionId) && window.crviAjax && window.crviAjax.couleurs_types_intervention) {
|
||||||
|
const couleurType = window.crviAjax.couleurs_types_intervention[typeInterventionId];
|
||||||
|
if (couleurType) {
|
||||||
|
bgColor = couleurType;
|
||||||
|
console.log('🎨 [COULEUR] RDV - type intervention trouvé:', {
|
||||||
|
eventId: event.id,
|
||||||
|
typeInterventionId: typeInterventionId,
|
||||||
|
couleur: bgColor
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priorité 3 : Couleur du local (type de local)
|
||||||
|
if (!bgColor && eventProps.local) {
|
||||||
|
const localType = eventProps.local.type || eventProps.local_type;
|
||||||
|
if (localType && crviACFData && crviACFData.types_local) {
|
||||||
|
const typeLocalConfig = crviACFData.types_local[localType];
|
||||||
|
if (typeLocalConfig && typeLocalConfig.couleur) {
|
||||||
|
bgColor = typeLocalConfig.couleur;
|
||||||
|
console.log('🎨 [COULEUR] RDV - type de local trouvé:', {
|
||||||
|
eventId: event.id,
|
||||||
|
localType: localType,
|
||||||
|
couleur: bgColor
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback : orange par défaut
|
||||||
if (!bgColor) {
|
if (!bgColor) {
|
||||||
bgColor = '#ff9800'; // Orange par défaut
|
bgColor = '#ff9800'; // Orange par défaut
|
||||||
}
|
}
|
||||||
@ -1105,6 +782,16 @@ export function initializeCalendar() {
|
|||||||
// Déterminer la couleur
|
// Déterminer la couleur
|
||||||
let { bgColor, txtColor } = determineEventColor();
|
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
|
// Vérifier que les couleurs sont valides et utiliser des valeurs par défaut si nécessaire
|
||||||
if (!bgColor || !txtColor) {
|
if (!bgColor || !txtColor) {
|
||||||
console.warn('⚠️ [eventDidMount] Couleurs invalides détectées:', { bgColor, txtColor, eventId: event.id });
|
console.warn('⚠️ [eventDidMount] Couleurs invalides détectées:', { bgColor, txtColor, eventId: event.id });
|
||||||
@ -1120,6 +807,15 @@ export function initializeCalendar() {
|
|||||||
textColor: txtColor
|
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
|
// Fonction pour appliquer les styles
|
||||||
function applyEventStyles() {
|
function applyEventStyles() {
|
||||||
try {
|
try {
|
||||||
@ -1188,7 +884,9 @@ export function initializeCalendar() {
|
|||||||
|
|
||||||
// Déterminer la couleur pour le popover (utiliser la même logique que pour l'événement)
|
// Déterminer la couleur pour le popover (utiliser la même logique que pour l'événement)
|
||||||
const popoverColor = bgColor || '#6c757d';
|
const popoverColor = bgColor || '#6c757d';
|
||||||
const popoverTextColor = txtColor || '#ffffff';
|
// Recalculer la couleur du texte en fonction de la luminosité du fond du popover
|
||||||
|
// pour éviter un texte blanc sur fond blanc pour les permanences non attribuées
|
||||||
|
const popoverTextColor = getTextColor(popoverColor);
|
||||||
|
|
||||||
console.log('🎨 [POPOVER] Couleurs déterminées:', {
|
console.log('🎨 [POPOVER] Couleurs déterminées:', {
|
||||||
eventId: event.id,
|
eventId: event.id,
|
||||||
@ -1228,6 +926,47 @@ export function initializeCalendar() {
|
|||||||
return statutConfig ? statutConfig.couleur : '#6c757d';
|
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é
|
// Fonction utilitaire pour extraire le nom complet d'une entité
|
||||||
function getEntityDisplayName(entity, entityType = '') {
|
function getEntityDisplayName(entity, entityType = '') {
|
||||||
if (!entity) return '';
|
if (!entity) return '';
|
||||||
@ -1260,6 +999,9 @@ export function initializeCalendar() {
|
|||||||
|
|
||||||
console.log('eventProps:', eventProps);
|
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é
|
// Créer le contenu du popover avec titre coloré
|
||||||
const popoverContent = `
|
const popoverContent = `
|
||||||
<div class="event-popover">
|
<div class="event-popover">
|
||||||
@ -1272,7 +1014,7 @@ export function initializeCalendar() {
|
|||||||
${eventProps.intervenant ? `<div class="mb-1"><strong>Intervenant:</strong> ${getEntityDisplayName(eventProps.intervenant, 'intervenant')}</div>` : ''}
|
${eventProps.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.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>` : ''}
|
${eventProps.commentaire ? `<div class="mb-1"><strong>Commentaire:</strong> ${eventProps.commentaire}</div>` : ''}
|
||||||
<div class="mb-1"><strong>Statut:</strong> <span class="event-status" style="background-color: ${getStatutColor(eventProps.statut)};">${eventProps.statut}</span></div>
|
<div class="mb-1"><strong>Statut:</strong> <span class="event-status" style="background-color: ${statutDisplay.color}; color: ${getTextColor(statutDisplay.color)}; padding: 2px 8px; border-radius: 3px;">${statutDisplay.text}</span></div>
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<small class="text-muted">Cliquez pour plus de détails</small>
|
<small class="text-muted">Cliquez pour plus de détails</small>
|
||||||
</div>
|
</div>
|
||||||
@ -1306,10 +1048,25 @@ export function initializeCalendar() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Appliquer aussi les styles au body du popover si nécessaire
|
// S'assurer que le body du popover a une couleur de texte lisible
|
||||||
const bodyElement = popoverElement.querySelector('.popover-body');
|
const bodyElement = popoverElement.querySelector('.popover-body');
|
||||||
if (bodyElement && txtColor) {
|
if (bodyElement) {
|
||||||
bodyElement.style.color = txtColor;
|
// Le body du popover a un fond blanc, donc le texte doit toujours être sombre
|
||||||
|
bodyElement.style.color = '#000000';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Appliquer les styles du statut
|
||||||
|
const statutElement = popoverElement.querySelector('.event-status');
|
||||||
|
if (statutElement) {
|
||||||
|
const statutDisplay = getStatutDisplay(eventProps.statut, eventProps.type);
|
||||||
|
statutElement.style.backgroundColor = statutDisplay.color;
|
||||||
|
statutElement.style.color = getTextColor(statutDisplay.color);
|
||||||
|
statutElement.style.padding = '2px 8px';
|
||||||
|
statutElement.style.borderRadius = '3px';
|
||||||
|
console.log('🎨 [POPOVER] Styles de statut appliqués:', {
|
||||||
|
statut: statutDisplay.text,
|
||||||
|
backgroundColor: statutDisplay.color
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -1329,6 +1086,27 @@ export function initializeCalendar() {
|
|||||||
color: popoverTextColor
|
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,10 +590,13 @@ function collectIntervenantFilters() {
|
|||||||
const typeRdv = document.getElementById('type_rdv')?.value;
|
const typeRdv = document.getElementById('type_rdv')?.value;
|
||||||
if (typeRdv) filters.type_rdv = typeRdv;
|
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;
|
const typeIntervention = document.getElementById('type_intervention')?.value;
|
||||||
if (typeIntervention) filters.type_intervention = parseInt(typeIntervention);
|
if (typeIntervention) filters.type_intervention = parseInt(typeIntervention);
|
||||||
|
|
||||||
const langue = document.getElementById('langue')?.value;
|
const langue = document.getElementById('langue_filtre')?.value;
|
||||||
if (langue) filters.langue = langue;
|
if (langue) filters.langue = langue;
|
||||||
|
|
||||||
const permanences = document.getElementById('permanences_non_assignees')?.checked;
|
const permanences = document.getElementById('permanences_non_assignees')?.checked;
|
||||||
@ -617,6 +620,9 @@ function collectColleaguesFilters() {
|
|||||||
const typeRdv = document.getElementById('type_rdv-colleagues')?.value;
|
const typeRdv = document.getElementById('type_rdv-colleagues')?.value;
|
||||||
if (typeRdv) filters.type_rdv = typeRdv;
|
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;
|
const typeIntervention = document.getElementById('type_intervention-colleagues')?.value;
|
||||||
if (typeIntervention) filters.type_intervention = parseInt(typeIntervention);
|
if (typeIntervention) filters.type_intervention = parseInt(typeIntervention);
|
||||||
|
|
||||||
@ -758,14 +764,37 @@ async function loadFilterOptions(mode = 'intervenant') {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Types d'intervention
|
// Départements
|
||||||
const typeInterventionSelect = document.getElementById(`type_intervention${prefix}`);
|
const departementSelect = document.getElementById(`departement${prefix}`);
|
||||||
if (typeInterventionSelect && disponibilites.types_intervention) {
|
if (departementSelect && disponibilites.departements) {
|
||||||
disponibilites.types_intervention.forEach(type => {
|
disponibilites.departements.forEach(dept => {
|
||||||
const option = document.createElement('option');
|
const option = document.createElement('option');
|
||||||
option.value = type.id;
|
option.value = dept.id;
|
||||||
option.textContent = type.nom;
|
option.textContent = dept.nom;
|
||||||
typeInterventionSelect.appendChild(option);
|
departementSelect.appendChild(option);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Types d'intervention groupés par département
|
||||||
|
const typeInterventionSelect = document.getElementById(`type_intervention${prefix}`);
|
||||||
|
if (typeInterventionSelect && disponibilites.types_intervention_groupes) {
|
||||||
|
// Parcourir les départements et créer des optgroup
|
||||||
|
Object.keys(disponibilites.types_intervention_groupes).forEach(departementId => {
|
||||||
|
const departementData = disponibilites.types_intervention_groupes[departementId];
|
||||||
|
const optgroup = document.createElement('optgroup');
|
||||||
|
optgroup.label = departementData.nom;
|
||||||
|
|
||||||
|
// Ajouter les types d'intervention de ce département
|
||||||
|
if (departementData.types && Array.isArray(departementData.types)) {
|
||||||
|
departementData.types.forEach(type => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = type.id;
|
||||||
|
option.textContent = type.nom;
|
||||||
|
optgroup.appendChild(option);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
typeInterventionSelect.appendChild(optgroup);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
784
assets/js/modules/agenda-modal-buttons.js
Normal file
784
assets/js/modules/agenda-modal-buttons.js
Normal file
@ -0,0 +1,784 @@
|
|||||||
|
// Module de gestion des boutons de la modale
|
||||||
|
// Contient les gestionnaires d'événements pour tous les boutons de la modale
|
||||||
|
|
||||||
|
import { deleteEvent, changeEventStatus, apiFetch } from './agenda-api.js';
|
||||||
|
import { notifyError, notifySuccess } from './agenda-notifications.js';
|
||||||
|
import { populateSelects } from './agenda-modal-select.js';
|
||||||
|
import { fillFormWithEvent, clearFormErrors, handleEventFormSubmit } from './agenda-modal-forms.js';
|
||||||
|
import { updateModalDisplay, fillViewBlock } from './agenda-modal-display.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper pour gérer l'overlay de chargement
|
||||||
|
* @param {Function} asyncAction - Action asynchrone à exécuter
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async function withLoadingOverlay(asyncAction) {
|
||||||
|
const overlayTarget = document.querySelector('#eventModal .modal-content') || document.getElementById('eventModal');
|
||||||
|
try {
|
||||||
|
if (window.CRVI_OVERLAY && overlayTarget) {
|
||||||
|
window.CRVI_OVERLAY.show(overlayTarget);
|
||||||
|
}
|
||||||
|
await asyncAction();
|
||||||
|
} finally {
|
||||||
|
if (window.CRVI_OVERLAY && overlayTarget) {
|
||||||
|
window.CRVI_OVERLAY.hide(overlayTarget);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper pour rafraîchir les calendriers
|
||||||
|
*/
|
||||||
|
function refreshCalendars() {
|
||||||
|
if (window.currentCalendar) {
|
||||||
|
window.currentCalendar.refetchEvents();
|
||||||
|
}
|
||||||
|
if (window.currentColleaguesCalendar) {
|
||||||
|
window.currentColleaguesCalendar.refetchEvents();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper pour fermer le modal
|
||||||
|
* @param {Function} onClosed - Callback après fermeture
|
||||||
|
*/
|
||||||
|
function closeModal(onClosed = null) {
|
||||||
|
const modal = document.getElementById('eventModal');
|
||||||
|
if (modal) {
|
||||||
|
const bsModal = bootstrap.Modal.getInstance(modal);
|
||||||
|
if (bsModal) {
|
||||||
|
if (onClosed) {
|
||||||
|
modal.addEventListener('hidden.bs.modal', onClosed, { once: true });
|
||||||
|
}
|
||||||
|
bsModal.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie les permissions utilisateur
|
||||||
|
* @param {string} permission - Type de permission ('can_edit', 'can_delete', 'can_create')
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function checkPermission(permission) {
|
||||||
|
const hasPermission = window.crviPermissions && window.crviPermissions[permission];
|
||||||
|
if (!hasPermission) {
|
||||||
|
console.warn(`Utilisateur non autorisé: ${permission}`);
|
||||||
|
}
|
||||||
|
return hasPermission;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise le bouton de fermeture
|
||||||
|
* @param {string} buttonId - ID du bouton
|
||||||
|
*/
|
||||||
|
export function initializeCloseButton(buttonId) {
|
||||||
|
const button = document.getElementById(buttonId);
|
||||||
|
if (button) {
|
||||||
|
button.onclick = () => closeModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise le bouton de suppression
|
||||||
|
* @param {Function} getCurrentEventData - Fonction pour obtenir les données de l'événement
|
||||||
|
* @param {Function} onDeleted - Callback après suppression
|
||||||
|
*/
|
||||||
|
export function initializeDeleteButton(getCurrentEventData, onDeleted = null) {
|
||||||
|
const deleteBtn = document.getElementById('deleteEvent');
|
||||||
|
if (!deleteBtn) return;
|
||||||
|
|
||||||
|
deleteBtn.onclick = async function() {
|
||||||
|
if (!checkPermission('can_delete')) return;
|
||||||
|
|
||||||
|
if (!confirm('Êtes-vous sûr de vouloir supprimer cet événement ?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentEventData = getCurrentEventData();
|
||||||
|
const eventId = currentEventData ? currentEventData.id : null;
|
||||||
|
|
||||||
|
if (!eventId) {
|
||||||
|
notifyError('ID d\'événement manquant');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await withLoadingOverlay(async () => {
|
||||||
|
try {
|
||||||
|
await deleteEvent(eventId);
|
||||||
|
refreshCalendars();
|
||||||
|
closeModal(onDeleted);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors de la suppression:', error);
|
||||||
|
notifyError('Erreur lors de la suppression de l\'événement');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise le bouton de validation de présence
|
||||||
|
* @param {Function} getCurrentEventData - Fonction pour obtenir les données de l'événement
|
||||||
|
* @param {Function} openCheckPresenceModal - Fonction pour ouvrir la modal de validation de présence de groupe
|
||||||
|
* @param {Function} onStatusChanged - Callback après changement de statut
|
||||||
|
*/
|
||||||
|
export function initializeMarkPresentButton(getCurrentEventData, openCheckPresenceModal, onStatusChanged = null) {
|
||||||
|
const markPresentBtn = document.getElementById('markPresentBtn');
|
||||||
|
if (!markPresentBtn) return;
|
||||||
|
|
||||||
|
markPresentBtn.onclick = async function() {
|
||||||
|
if (!checkPermission('can_edit')) return;
|
||||||
|
|
||||||
|
const currentEventData = getCurrentEventData();
|
||||||
|
const eventId = currentEventData ? currentEventData.id : null;
|
||||||
|
|
||||||
|
if (!eventId) {
|
||||||
|
notifyError('ID d\'événement manquant');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier le type d'événement
|
||||||
|
const eventType = currentEventData?.type || currentEventData?.extendedProps?.type || '';
|
||||||
|
const isGroupe = eventType === 'groupe';
|
||||||
|
|
||||||
|
if (isGroupe) {
|
||||||
|
// Pour les événements de groupe, ouvrir la modal de validation des présences
|
||||||
|
openCheckPresenceModal(currentEventData);
|
||||||
|
} else {
|
||||||
|
// Pour les événements individuels
|
||||||
|
if (!confirm('Confirmer la présence à cet événement ?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await withLoadingOverlay(async () => {
|
||||||
|
try {
|
||||||
|
await changeEventStatus(eventId, 'present');
|
||||||
|
notifySuccess('Présence validée');
|
||||||
|
refreshCalendars();
|
||||||
|
closeModal(onStatusChanged);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors du changement de statut:', error);
|
||||||
|
notifyError('Erreur lors de la validation de présence');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise le bouton pour afficher le modal de gestion des présences (groupes uniquement)
|
||||||
|
* @param {Function} getCurrentEventData - Fonction pour obtenir les données de l'événement
|
||||||
|
* @param {Function} openCheckPresenceModal - Fonction pour ouvrir la modal de validation de présence de groupe
|
||||||
|
*/
|
||||||
|
export function initializeShowPresenceModalButton(getCurrentEventData, openCheckPresenceModal) {
|
||||||
|
const showPresenceModalBtn = document.getElementById('showPresenceModalBtn');
|
||||||
|
if (!showPresenceModalBtn) return;
|
||||||
|
|
||||||
|
showPresenceModalBtn.onclick = function() {
|
||||||
|
if (!checkPermission('can_edit')) return;
|
||||||
|
|
||||||
|
const currentEventData = getCurrentEventData();
|
||||||
|
if (!currentEventData) {
|
||||||
|
notifyError('Données d\'événement manquantes');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ouvrir le modal de gestion des présences
|
||||||
|
openCheckPresenceModal(currentEventData);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise le bouton pour marquer comme absent
|
||||||
|
* @param {Function} getCurrentEventData - Fonction pour obtenir les données de l'événement
|
||||||
|
* @param {Function} onStatusChanged - Callback après changement de statut
|
||||||
|
*/
|
||||||
|
export function initializeMarkAbsentButton(getCurrentEventData, onStatusChanged = null) {
|
||||||
|
const markAbsentBtn = document.getElementById('markAbsentBtn');
|
||||||
|
if (!markAbsentBtn) return;
|
||||||
|
|
||||||
|
markAbsentBtn.onclick = async function() {
|
||||||
|
if (!checkPermission('can_edit')) return;
|
||||||
|
|
||||||
|
const currentEventData = getCurrentEventData();
|
||||||
|
const eventId = currentEventData ? currentEventData.id : null;
|
||||||
|
|
||||||
|
if (!eventId) {
|
||||||
|
notifyError('ID d\'événement manquant');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirm('Marquer cet événement comme absent ?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await withLoadingOverlay(async () => {
|
||||||
|
try {
|
||||||
|
await changeEventStatus(eventId, 'absence');
|
||||||
|
notifySuccess('Événement marqué comme absent');
|
||||||
|
refreshCalendars();
|
||||||
|
closeModal(onStatusChanged);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors du changement de statut:', error);
|
||||||
|
notifyError('Erreur lors du changement de statut');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise le bouton d'annulation de rendez-vous
|
||||||
|
* @param {Function} getCurrentEventData - Fonction pour obtenir les données de l'événement
|
||||||
|
* @param {Function} onCancelled - Callback après annulation
|
||||||
|
*/
|
||||||
|
export function initializeCancelAppointmentButton(getCurrentEventData, onCancelled = null) {
|
||||||
|
const cancelAppointmentBtn = document.getElementById('cancelAppointmentBtn');
|
||||||
|
if (!cancelAppointmentBtn) return;
|
||||||
|
|
||||||
|
cancelAppointmentBtn.onclick = async function() {
|
||||||
|
if (!checkPermission('can_edit')) return;
|
||||||
|
|
||||||
|
const currentEventData = getCurrentEventData();
|
||||||
|
const eventId = currentEventData ? currentEventData.id : null;
|
||||||
|
|
||||||
|
if (!eventId) {
|
||||||
|
notifyError('ID d\'événement manquant');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const motif = prompt('Motif de l\'annulation (optionnel):');
|
||||||
|
if (motif === null) {
|
||||||
|
return; // L'utilisateur a cliqué sur Annuler
|
||||||
|
}
|
||||||
|
|
||||||
|
await withLoadingOverlay(async () => {
|
||||||
|
try {
|
||||||
|
await changeEventStatus(eventId, 'annule', motif);
|
||||||
|
notifySuccess('Rendez-vous annulé');
|
||||||
|
refreshCalendars();
|
||||||
|
closeModal(onCancelled);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors de l\'annulation:', error);
|
||||||
|
notifyError('Erreur lors de l\'annulation du rendez-vous');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise le bouton Edit
|
||||||
|
* @param {Function} getCurrentEventData - Fonction pour obtenir les données de l'événement
|
||||||
|
* @param {Function} getCurrentMode - Fonction pour obtenir le mode actuel
|
||||||
|
* @param {Function} setCurrentMode - Fonction pour changer le mode
|
||||||
|
* @param {Function} enableDateSynchronization - Fonction pour activer la synchronisation des dates
|
||||||
|
*/
|
||||||
|
export function initializeEditButton(getCurrentEventData, getCurrentMode, setCurrentMode, enableDateSynchronization) {
|
||||||
|
const editBtn = document.getElementById('editEventBtn');
|
||||||
|
if (!editBtn) return;
|
||||||
|
|
||||||
|
editBtn.onclick = async function() {
|
||||||
|
if (!checkPermission('can_edit')) return;
|
||||||
|
|
||||||
|
const currentEventData = getCurrentEventData();
|
||||||
|
|
||||||
|
// Changer le mode
|
||||||
|
setCurrentMode('edit');
|
||||||
|
|
||||||
|
// Mettre à jour l'affichage
|
||||||
|
const userCanEdit = window.crviPermissions && window.crviPermissions.can_edit;
|
||||||
|
const userCanDelete = window.crviPermissions && window.crviPermissions.can_delete;
|
||||||
|
updateModalDisplay('edit', userCanEdit, userCanDelete, currentEventData);
|
||||||
|
|
||||||
|
// Remplir le formulaire avec les données actuelles
|
||||||
|
fillFormWithEvent(currentEventData);
|
||||||
|
|
||||||
|
// Peupler les selects avec overlay de chargement
|
||||||
|
await withLoadingOverlay(async () => {
|
||||||
|
try {
|
||||||
|
await populateSelects(currentEventData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors du peuplement des selects:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
clearFormErrors();
|
||||||
|
|
||||||
|
// Activer la synchronisation des dates
|
||||||
|
if (enableDateSynchronization) {
|
||||||
|
enableDateSynchronization();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise le bouton Cancel
|
||||||
|
* @param {Function} getCurrentEventData - Fonction pour obtenir les données de l'événement
|
||||||
|
* @param {Function} getCurrentMode - Fonction pour obtenir le mode actuel
|
||||||
|
* @param {Function} setCurrentMode - Fonction pour changer le mode
|
||||||
|
* @param {Function} disableDateSynchronization - Fonction pour désactiver la synchronisation des dates
|
||||||
|
*/
|
||||||
|
export function initializeCancelButton(getCurrentEventData, getCurrentMode, setCurrentMode, disableDateSynchronization) {
|
||||||
|
const cancelBtn = document.getElementById('cancelEditBtn');
|
||||||
|
if (!cancelBtn) return;
|
||||||
|
|
||||||
|
cancelBtn.onclick = function() {
|
||||||
|
const currentMode = getCurrentMode();
|
||||||
|
const currentEventData = getCurrentEventData();
|
||||||
|
|
||||||
|
if (currentMode === 'create') {
|
||||||
|
// En mode création, fermer le modal
|
||||||
|
closeModal();
|
||||||
|
} else if (currentMode === 'edit' && currentEventData) {
|
||||||
|
// En mode édition, retourner en mode vue
|
||||||
|
setCurrentMode('view');
|
||||||
|
|
||||||
|
const userCanEdit = window.crviPermissions && window.crviPermissions.can_edit;
|
||||||
|
const userCanDelete = window.crviPermissions && window.crviPermissions.can_delete;
|
||||||
|
updateModalDisplay('view', userCanEdit, userCanDelete, currentEventData);
|
||||||
|
|
||||||
|
// Réafficher les données en mode lecture
|
||||||
|
fillViewBlock(currentEventData);
|
||||||
|
clearFormErrors();
|
||||||
|
|
||||||
|
// Désactiver la synchronisation des dates
|
||||||
|
if (disableDateSynchronization) {
|
||||||
|
disableDateSynchronization();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise le bouton Save
|
||||||
|
* @param {Function} getCurrentEventData - Fonction pour obtenir les données de l'événement
|
||||||
|
* @param {Function} getCurrentMode - Fonction pour obtenir le mode actuel
|
||||||
|
* @param {Function} disableDateSynchronization - Fonction pour désactiver la synchronisation des dates
|
||||||
|
*/
|
||||||
|
export function initializeSaveButton(getCurrentEventData, getCurrentMode, disableDateSynchronization) {
|
||||||
|
const saveBtn = document.getElementById('saveEvent');
|
||||||
|
if (!saveBtn) return;
|
||||||
|
|
||||||
|
saveBtn.onclick = async function() {
|
||||||
|
const currentMode = getCurrentMode();
|
||||||
|
const currentEventData = getCurrentEventData();
|
||||||
|
const eventId = currentEventData ? currentEventData.id : null;
|
||||||
|
|
||||||
|
await withLoadingOverlay(async () => {
|
||||||
|
await handleEventFormSubmit(currentMode, eventId, function() {
|
||||||
|
// Succès : fermer le modal et rafraîchir les calendriers
|
||||||
|
refreshCalendars();
|
||||||
|
closeModal();
|
||||||
|
|
||||||
|
// Désactiver la synchronisation des dates
|
||||||
|
if (disableDateSynchronization) {
|
||||||
|
disableDateSynchronization();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise le bouton "Signaler un incident"
|
||||||
|
* @param {Function} getCurrentEventData - Fonction pour obtenir les données de l'événement
|
||||||
|
* @param {Function} openSubModal - Fonction pour ouvrir une sous-modale
|
||||||
|
*/
|
||||||
|
export function initializeReportIncidentButton(getCurrentEventData, openSubModal) {
|
||||||
|
const reportIncidentBtn = document.getElementById('reportIncidentBtn');
|
||||||
|
if (!reportIncidentBtn) return;
|
||||||
|
|
||||||
|
reportIncidentBtn.onclick = async function() {
|
||||||
|
const currentEventData = getCurrentEventData();
|
||||||
|
|
||||||
|
if (!currentEventData || !currentEventData.id) {
|
||||||
|
notifyError('Aucun événement sélectionné');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupérer le type d'événement
|
||||||
|
const eventType = currentEventData.type || currentEventData.extendedProps?.type || 'individuel';
|
||||||
|
const isGroupe = eventType === 'groupe';
|
||||||
|
|
||||||
|
// Ouvrir la sous-modale avec préparation des données
|
||||||
|
openSubModal(
|
||||||
|
'declarationIncidentModal',
|
||||||
|
(subModal) => {
|
||||||
|
// Pré-remplir le formulaire
|
||||||
|
const eventIdInput = document.getElementById('incident_event_id');
|
||||||
|
const eventTypeInput = document.getElementById('incident_event_type');
|
||||||
|
const sectionIndividuel = document.getElementById('incident_individuel_section');
|
||||||
|
const sectionGroupe = document.getElementById('incident_groupe_section');
|
||||||
|
|
||||||
|
if (eventIdInput) eventIdInput.value = currentEventData.id;
|
||||||
|
if (eventTypeInput) eventTypeInput.value = eventType;
|
||||||
|
|
||||||
|
// Afficher/masquer les sections selon le type
|
||||||
|
if (isGroupe) {
|
||||||
|
if (sectionIndividuel) sectionIndividuel.style.display = 'none';
|
||||||
|
if (sectionGroupe) sectionGroupe.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
if (sectionIndividuel) sectionIndividuel.style.display = 'block';
|
||||||
|
if (sectionGroupe) sectionGroupe.style.display = 'none';
|
||||||
|
|
||||||
|
// Pré-remplir les infos du bénéficiaire
|
||||||
|
const beneficiaireId = currentEventData.id_beneficiaire || currentEventData.extendedProps?.id_beneficiaire;
|
||||||
|
const beneficiaireNom = currentEventData.beneficiaire?.nom_complet ||
|
||||||
|
currentEventData.extendedProps?.beneficiaire?.nom_complet ||
|
||||||
|
'Non spécifié';
|
||||||
|
|
||||||
|
const beneficiaireNomInput = document.getElementById('incident_beneficiaire_nom');
|
||||||
|
const beneficiaireIdInput = document.getElementById('incident_beneficiaire_id');
|
||||||
|
|
||||||
|
if (beneficiaireNomInput) beneficiaireNomInput.value = beneficiaireNom;
|
||||||
|
if (beneficiaireIdInput) beneficiaireIdInput.value = beneficiaireId || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Charge l'historique des 3 derniers rendez-vous d'un bénéficiaire
|
||||||
|
* @param {number} beneficiaireId - ID du bénéficiaire
|
||||||
|
* @returns {Promise<Array>} Tableau des rendez-vous avec leurs incidents
|
||||||
|
*/
|
||||||
|
async function loadBeneficiaireHistorique(beneficiaireId) {
|
||||||
|
try {
|
||||||
|
const historique = await apiFetch(`beneficiaires/${beneficiaireId}/historique`);
|
||||||
|
return historique || [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors du chargement de l\'historique:', error);
|
||||||
|
notifyError('Erreur lors du chargement de l\'historique du bénéficiaire');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Affiche l'historique sous forme de ligne du temps verticale
|
||||||
|
* @param {Array} historiqueData - Tableau des rendez-vous avec leurs incidents
|
||||||
|
*/
|
||||||
|
function displayHistoriqueTimeline(historiqueData) {
|
||||||
|
const timelineContainer = document.getElementById('historiqueTimeline');
|
||||||
|
if (!timelineContainer) {
|
||||||
|
console.error('Conteneur de timeline non trouvé');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!historiqueData || historiqueData.length === 0) {
|
||||||
|
timelineContainer.innerHTML = `
|
||||||
|
<div class="alert alert-info text-center">
|
||||||
|
<i class="fas fa-info-circle me-2"></i>
|
||||||
|
Aucun rendez-vous trouvé pour ce bénéficiaire.
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '<div class="crvi-timeline">';
|
||||||
|
|
||||||
|
historiqueData.forEach((rdv, index) => {
|
||||||
|
const date = new Date(rdv.date_rdv + 'T' + rdv.heure_rdv);
|
||||||
|
const dateFormatted = date.toLocaleDateString('fr-FR', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric'
|
||||||
|
});
|
||||||
|
const heureFormatted = date.toLocaleTimeString('fr-FR', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
|
||||||
|
const intervenantNom = rdv.intervenant
|
||||||
|
? `${rdv.intervenant.nom || ''} ${rdv.intervenant.prenom || ''}`.trim()
|
||||||
|
: 'Non renseigné';
|
||||||
|
|
||||||
|
const typeInterventionNom = rdv.type_intervention?.nom || 'Non renseigné';
|
||||||
|
|
||||||
|
// Déterminer le type d'événement et la couleur
|
||||||
|
const hasIncident = rdv.incident && rdv.incident.id;
|
||||||
|
const statutLower = rdv.statut ? rdv.statut.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "") : '';
|
||||||
|
|
||||||
|
let eventType = 'default';
|
||||||
|
let icon = 'fa-calendar-alt';
|
||||||
|
|
||||||
|
if (hasIncident || statutLower.includes('absence') || statutLower.includes('absent')) {
|
||||||
|
eventType = 'incident'; // Rouge
|
||||||
|
icon = 'fa-exclamation-triangle';
|
||||||
|
} else if (statutLower.includes('present')) {
|
||||||
|
eventType = 'present'; // Vert
|
||||||
|
icon = 'fa-check-circle';
|
||||||
|
} else if (statutLower.includes('prevu')) {
|
||||||
|
eventType = 'prevu'; // Bleu
|
||||||
|
icon = 'fa-clock';
|
||||||
|
} else {
|
||||||
|
eventType = 'default'; // Gris
|
||||||
|
icon = 'fa-calendar-alt';
|
||||||
|
}
|
||||||
|
|
||||||
|
const isLastItem = index === historiqueData.length - 1;
|
||||||
|
const eventNumber = index + 1;
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<div class="crvi-timeline__event crvi-timeline__event--${eventType} ${isLastItem ? 'crvi-timeline__event--last' : ''}" data-number="${eventNumber}">
|
||||||
|
<div class="crvi-timeline__event__header">
|
||||||
|
<div class="crvi-timeline__event__date-wrapper">
|
||||||
|
<div class="crvi-timeline__event__date-icon">
|
||||||
|
<i class="fas ${icon}"></i>
|
||||||
|
</div>
|
||||||
|
<div class="crvi-timeline__event__date-info">
|
||||||
|
<div class="crvi-timeline__event__date">${dateFormatted}</div>
|
||||||
|
<div class="crvi-timeline__event__time">${heureFormatted}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="crvi-timeline__event__title">
|
||||||
|
${hasIncident ? '<i class="fas fa-exclamation-triangle me-2"></i>' : ''}
|
||||||
|
${rdv.type || 'Rendez-vous'}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="crvi-timeline__event__content">
|
||||||
|
${rdv.personne_en_liste_rouge ? `
|
||||||
|
<div class="alert alert-danger mb-2" style="font-size: 90%; padding: 8px;">
|
||||||
|
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||||
|
<strong>Personne en liste rouge</strong>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
<div class="mb-2">
|
||||||
|
<strong><i class="fas fa-user-md me-2"></i>Intervenant:</strong> ${intervenantNom}
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<strong><i class="fas fa-tools me-2"></i>Type d'intervention:</strong> ${typeInterventionNom}
|
||||||
|
</div>
|
||||||
|
${rdv.statut ? `
|
||||||
|
<div class="mb-2">
|
||||||
|
<strong><i class="fas fa-info-circle me-2"></i>Statut:</strong>
|
||||||
|
<span class="badge bg-${eventType === 'present' ? 'success' : eventType === 'prevu' ? 'primary' : eventType === 'incident' ? 'danger' : 'secondary'}">${rdv.statut}</span>
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
${rdv.commentaire ? `
|
||||||
|
<div class="mb-2">
|
||||||
|
<strong><i class="fas fa-comment me-2"></i>Commentaire:</strong> ${rdv.commentaire}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
${hasIncident ? `
|
||||||
|
<div class="alert alert-danger mt-3 mb-0" style="background-color: #fee; border-left: 4px solid #dc3545; padding: 12px;">
|
||||||
|
<strong><i class="fas fa-exclamation-triangle me-2"></i>Incident signalé</strong>
|
||||||
|
<div class="mt-2">
|
||||||
|
<strong>Résumé:</strong> ${rdv.incident.resume_incident || 'Non renseigné'}
|
||||||
|
</div>
|
||||||
|
${rdv.incident.commentaire_incident ? `
|
||||||
|
<div class="mt-2">
|
||||||
|
<strong>Commentaire:</strong> ${rdv.incident.commentaire_incident}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
timelineContainer.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise le bouton "Historique bénéficiaire"
|
||||||
|
* @param {Function} getCurrentEventData - Fonction pour obtenir les données de l'événement
|
||||||
|
* @param {Function} openSubModal - Fonction pour ouvrir une sous-modale
|
||||||
|
*/
|
||||||
|
export function initializeHistoriqueButton(getCurrentEventData, openSubModal) {
|
||||||
|
const historiqueBtn = document.getElementById('showBeneficiaireHistoriqueBtn');
|
||||||
|
if (!historiqueBtn) return;
|
||||||
|
|
||||||
|
historiqueBtn.onclick = async function() {
|
||||||
|
const currentEventData = getCurrentEventData();
|
||||||
|
const beneficiaireId = historiqueBtn.getAttribute('data-benef') ||
|
||||||
|
currentEventData?.id_beneficiaire ||
|
||||||
|
currentEventData?.extendedProps?.id_beneficiaire;
|
||||||
|
|
||||||
|
if (!beneficiaireId) {
|
||||||
|
notifyError('ID du bénéficiaire introuvable');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ouvrir la sous-modale avec chargement des données
|
||||||
|
openSubModal(
|
||||||
|
'beneficiaireHistoriqueModal',
|
||||||
|
async (subModal) => {
|
||||||
|
// Afficher le spinner de chargement
|
||||||
|
const timelineContainer = document.getElementById('historiqueTimeline');
|
||||||
|
if (timelineContainer) {
|
||||||
|
timelineContainer.innerHTML = `
|
||||||
|
<div class="text-center py-4">
|
||||||
|
<div class="spinner-border text-primary" role="status">
|
||||||
|
<span class="visually-hidden">Chargement...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Charger l'historique
|
||||||
|
try {
|
||||||
|
const historiqueData = await loadBeneficiaireHistorique(parseInt(beneficiaireId));
|
||||||
|
displayHistoriqueTimeline(historiqueData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors du chargement de l\'historique:', error);
|
||||||
|
if (timelineContainer) {
|
||||||
|
timelineContainer.innerHTML = `
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||||
|
Erreur lors du chargement de l'historique.
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(subModal) => {
|
||||||
|
// Nettoyer après fermeture
|
||||||
|
const timelineContainer = document.getElementById('historiqueTimeline');
|
||||||
|
if (timelineContainer) {
|
||||||
|
timelineContainer.innerHTML = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise le bouton "Debug SMS"
|
||||||
|
* @param {Function} getCurrentEventData - Fonction pour obtenir les données de l'événement
|
||||||
|
*/
|
||||||
|
export function initializeDebugSmsButton(getCurrentEventData) {
|
||||||
|
const debugSmsBtn = document.getElementById('debugSmsBtn');
|
||||||
|
if (!debugSmsBtn) return;
|
||||||
|
|
||||||
|
debugSmsBtn.onclick = function() {
|
||||||
|
const currentEventData = getCurrentEventData();
|
||||||
|
const eventId = currentEventData ? currentEventData.id : null;
|
||||||
|
|
||||||
|
console.log('[DEBUG_SMS] Données événement actuelles:', currentEventData);
|
||||||
|
|
||||||
|
// Construire un message simple pour le debug
|
||||||
|
const dateStr = (document.getElementById('date_rdv') && document.getElementById('date_rdv').value) || currentEventData?.date || '';
|
||||||
|
const timeStr = (document.getElementById('heure_rdv') && document.getElementById('heure_rdv').value) || currentEventData?.time || '';
|
||||||
|
const typeStr = (document.getElementById('type') && document.getElementById('type').value) || currentEventData?.type || '';
|
||||||
|
|
||||||
|
const msg = `Debug SMS - Event ${eventId ?? 'N/A'} - ${dateStr} ${timeStr} - type: ${typeStr}`;
|
||||||
|
const phone = '0485500723';
|
||||||
|
|
||||||
|
if (!window.crviAjax || !window.crviAjax.url || !window.crviAjax.nonce) {
|
||||||
|
alert('Debug SMS: configuration AJAX manquante.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.append('action', 'crvi_send_sms_debug');
|
||||||
|
params.append('nonce', window.crviAjax.nonce);
|
||||||
|
params.append('phone', phone);
|
||||||
|
params.append('message', msg);
|
||||||
|
if (eventId) {
|
||||||
|
params.append('id_event', String(eventId));
|
||||||
|
}
|
||||||
|
params.append('sujet', 'debug');
|
||||||
|
|
||||||
|
fetch(window.crviAjax.url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
|
||||||
|
},
|
||||||
|
body: params.toString()
|
||||||
|
}).then(async (res) => {
|
||||||
|
let json;
|
||||||
|
try { json = await res.json(); } catch (_) {}
|
||||||
|
if (!res.ok || !json?.success) {
|
||||||
|
const errMsg = json?.data?.message || 'Erreur lors de l\'envoi SMS (debug).';
|
||||||
|
throw new Error(errMsg);
|
||||||
|
}
|
||||||
|
alert('SMS de debug envoyé.');
|
||||||
|
}).catch((err) => {
|
||||||
|
console.error('[DEBUG_SMS] Échec envoi:', err);
|
||||||
|
alert('Échec envoi SMS de debug: ' + (err?.message || 'Erreur inconnue'));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise le bouton "Voir les incidents"
|
||||||
|
* @param {Function} getCurrentEventData - Fonction pour obtenir les données de l'événement
|
||||||
|
* @param {Function} openIncidentsViewModal - Fonction pour ouvrir la modal de visualisation des incidents
|
||||||
|
*/
|
||||||
|
export function initializeViewIncidentsButton(getCurrentEventData, openIncidentsViewModal) {
|
||||||
|
const viewIncidentsBtn = document.getElementById('viewIncidentsBtn');
|
||||||
|
if (!viewIncidentsBtn) return;
|
||||||
|
|
||||||
|
viewIncidentsBtn.onclick = async function() {
|
||||||
|
const currentEventData = getCurrentEventData();
|
||||||
|
const eventId = currentEventData?.id || currentEventData?.extendedProps?.id;
|
||||||
|
|
||||||
|
if (!eventId) {
|
||||||
|
notifyError('ID de l\'événement introuvable');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await openIncidentsViewModal(eventId);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise tous les boutons de la modale
|
||||||
|
* @param {Object} options - Options de configuration
|
||||||
|
* @param {Function} options.getCurrentEventData - Fonction pour obtenir les données de l'événement
|
||||||
|
* @param {Function} options.getCurrentMode - Fonction pour obtenir le mode actuel
|
||||||
|
* @param {Function} options.setCurrentMode - Fonction pour changer le mode
|
||||||
|
* @param {Function} options.openSubModal - Fonction pour ouvrir une sous-modale
|
||||||
|
* @param {Function} options.openCheckPresenceModal - Fonction pour ouvrir la modal de validation de présence
|
||||||
|
* @param {Function} options.openIncidentsViewModal - Fonction pour ouvrir la modal de visualisation des incidents
|
||||||
|
* @param {Function} options.enableDateSynchronization - Fonction pour activer la synchronisation des dates
|
||||||
|
* @param {Function} options.disableDateSynchronization - Fonction pour désactiver la synchronisation des dates
|
||||||
|
* @param {Function} options.onDeleted - Callback après suppression
|
||||||
|
* @param {Function} options.onStatusChanged - Callback après changement de statut
|
||||||
|
*/
|
||||||
|
export function initializeModalButtons(options = {}) {
|
||||||
|
const {
|
||||||
|
getCurrentEventData,
|
||||||
|
getCurrentMode,
|
||||||
|
setCurrentMode,
|
||||||
|
openSubModal,
|
||||||
|
openCheckPresenceModal,
|
||||||
|
openIncidentsViewModal,
|
||||||
|
enableDateSynchronization,
|
||||||
|
disableDateSynchronization,
|
||||||
|
onDeleted,
|
||||||
|
onStatusChanged
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
// Boutons de fermeture
|
||||||
|
initializeCloseButton('closeModalBtn');
|
||||||
|
initializeCloseButton('closeViewBtn');
|
||||||
|
|
||||||
|
// Boutons d'action
|
||||||
|
if (getCurrentEventData) {
|
||||||
|
initializeEditButton(getCurrentEventData, getCurrentMode, setCurrentMode, enableDateSynchronization);
|
||||||
|
initializeCancelButton(getCurrentEventData, getCurrentMode, setCurrentMode, disableDateSynchronization);
|
||||||
|
initializeSaveButton(getCurrentEventData, getCurrentMode, disableDateSynchronization);
|
||||||
|
initializeDeleteButton(getCurrentEventData, onDeleted);
|
||||||
|
initializeMarkPresentButton(getCurrentEventData, openCheckPresenceModal, onStatusChanged);
|
||||||
|
initializeShowPresenceModalButton(getCurrentEventData, openCheckPresenceModal);
|
||||||
|
initializeMarkAbsentButton(getCurrentEventData, onStatusChanged);
|
||||||
|
initializeCancelAppointmentButton(getCurrentEventData, onStatusChanged);
|
||||||
|
initializeDebugSmsButton(getCurrentEventData);
|
||||||
|
|
||||||
|
// Boutons spécifiques avec sous-modales
|
||||||
|
if (openSubModal) {
|
||||||
|
initializeReportIncidentButton(getCurrentEventData, openSubModal);
|
||||||
|
initializeHistoriqueButton(getCurrentEventData, openSubModal);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bouton de visualisation des incidents
|
||||||
|
if (openIncidentsViewModal) {
|
||||||
|
initializeViewIncidentsButton(getCurrentEventData, openIncidentsViewModal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
359
assets/js/modules/agenda-modal-display.js
Normal file
359
assets/js/modules/agenda-modal-display.js
Normal file
@ -0,0 +1,359 @@
|
|||||||
|
// Module d'affichage des données de la modale
|
||||||
|
// Contient les fonctions d'affichage en mode lecture seule
|
||||||
|
|
||||||
|
import { setText, toggleElement } from './agenda-modal-dom.js';
|
||||||
|
import { checkAndDisplayIncidentsButton } from './agenda-modal.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extrait la date et l'heure d'un événement selon différentes sources
|
||||||
|
* @param {Object} event - Données de l'événement
|
||||||
|
* @returns {Object} - {dateFormatted, heureFormatted}
|
||||||
|
*/
|
||||||
|
function extractDateTime(event) {
|
||||||
|
let dateFormatted = '';
|
||||||
|
let heureFormatted = '';
|
||||||
|
|
||||||
|
// Priorité 1: Données directes de l'API
|
||||||
|
if (event?.date && event?.heure) {
|
||||||
|
const dateObj = new Date(event.date + 'T' + event.heure);
|
||||||
|
dateFormatted = dateObj.toLocaleDateString('fr-FR');
|
||||||
|
heureFormatted = dateObj.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' });
|
||||||
|
}
|
||||||
|
// Priorité 2: Données dans extendedProps
|
||||||
|
else if (event?.extendedProps?.date && event?.extendedProps?.heure) {
|
||||||
|
const dateObj = new Date(event.extendedProps.date + 'T' + event.extendedProps.heure);
|
||||||
|
dateFormatted = dateObj.toLocaleDateString('fr-FR');
|
||||||
|
heureFormatted = dateObj.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' });
|
||||||
|
}
|
||||||
|
// Priorité 3: Données FullCalendar
|
||||||
|
else if (event?.start) {
|
||||||
|
const startDate = new Date(event.start);
|
||||||
|
dateFormatted = startDate.toLocaleDateString('fr-FR');
|
||||||
|
heureFormatted = startDate.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' });
|
||||||
|
}
|
||||||
|
|
||||||
|
return { dateFormatted, heureFormatted };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère le nom d'une entité depuis le select ou les données
|
||||||
|
* @param {string} entityName - Nom de l'entité ('beneficiaire', 'intervenant', etc.)
|
||||||
|
* @param {Object} event - Données de l'événement
|
||||||
|
* @param {string} selectId - ID du select
|
||||||
|
* @param {string} idField - Nom du champ ID
|
||||||
|
* @returns {string} - Nom formaté
|
||||||
|
*/
|
||||||
|
function getEntityName(entityName, event, selectId, idField) {
|
||||||
|
let name = '';
|
||||||
|
|
||||||
|
// Vérifier dans les relations directes
|
||||||
|
if (event?.[entityName]?.nom) {
|
||||||
|
name = event[entityName].nom + (event[entityName].prenom ? ' ' + event[entityName].prenom : '');
|
||||||
|
} else if (event?.extendedProps?.[entityName]?.nom) {
|
||||||
|
name = event.extendedProps[entityName].nom + (event.extendedProps[entityName].prenom ? ' ' + event.extendedProps[entityName].prenom : '');
|
||||||
|
} else {
|
||||||
|
// Chercher par ID dans le select
|
||||||
|
const entityId = event?.[idField] || event?.extendedProps?.[idField];
|
||||||
|
if (entityId) {
|
||||||
|
const select = document.getElementById(selectId);
|
||||||
|
// Vérifier si c'est un SELECT (pour intervenant côté front c'est un input hidden)
|
||||||
|
if (select && select.tagName === 'SELECT') {
|
||||||
|
const option = select.querySelector(`option[value="${entityId}"]`);
|
||||||
|
if (option) {
|
||||||
|
name = option.textContent;
|
||||||
|
}
|
||||||
|
} else if (select && selectId === 'id_intervenant') {
|
||||||
|
// Cas spécial pour intervenant côté front (input hidden)
|
||||||
|
const displayEl = document.getElementById('id_intervenant_display');
|
||||||
|
if (displayEl && displayEl.textContent) {
|
||||||
|
name = displayEl.textContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
name = `ID: ${entityId}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère le nom d'un terme taxonomy depuis les données ou le select
|
||||||
|
* @param {Object} event - Données de l'événement
|
||||||
|
* @param {string} labelField - Champ de label (_label)
|
||||||
|
* @param {string} relationField - Champ de relation
|
||||||
|
* @param {string} idField - Champ ID
|
||||||
|
* @param {string} selectId - ID du select
|
||||||
|
* @returns {string} - Nom du terme
|
||||||
|
*/
|
||||||
|
function getTermName(event, labelField, relationField, idField, selectId) {
|
||||||
|
let name = '';
|
||||||
|
|
||||||
|
// Priorité 1: Label direct
|
||||||
|
name = event?.[labelField] || event?.extendedProps?.[labelField] || '';
|
||||||
|
|
||||||
|
// Priorité 2: Relation
|
||||||
|
if (!name && event?.[relationField]?.nom) {
|
||||||
|
name = event[relationField].nom;
|
||||||
|
} else if (!name && event?.extendedProps?.[relationField]?.nom) {
|
||||||
|
name = event.extendedProps[relationField].nom;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priorité 3: Chercher dans le select
|
||||||
|
if (!name) {
|
||||||
|
const termId = event?.[idField] || event?.extendedProps?.[idField];
|
||||||
|
if (termId && termId !== '0' && termId !== 0) {
|
||||||
|
const select = document.getElementById(selectId);
|
||||||
|
if (select) {
|
||||||
|
const option = select.querySelector(`option[value="${termId}"]`);
|
||||||
|
if (option) {
|
||||||
|
name = option.textContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!name) {
|
||||||
|
name = `ID: ${termId}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remplit le bloc de vue avec les données d'un événement
|
||||||
|
* @param {Object} event - Données de l'événement
|
||||||
|
*/
|
||||||
|
export function fillViewBlock(event) {
|
||||||
|
// Date et heure
|
||||||
|
const { dateFormatted, heureFormatted } = extractDateTime(event);
|
||||||
|
setText('view_date_rdv', dateFormatted);
|
||||||
|
setText('view_heure_rdv', heureFormatted);
|
||||||
|
|
||||||
|
// Type et langue
|
||||||
|
const type = event?.type || event?.extendedProps?.type || '';
|
||||||
|
setText('view_type', type);
|
||||||
|
|
||||||
|
let langue = event?.langue_label || event?.extendedProps?.langue_label || '';
|
||||||
|
if (!langue || /^\d+$/.test(langue)) {
|
||||||
|
const langueId = langue || event?.langue || event?.extendedProps?.langue || '';
|
||||||
|
if (langueId) {
|
||||||
|
const langueSelect = document.getElementById('langue');
|
||||||
|
if (langueSelect) {
|
||||||
|
const option = langueSelect.querySelector(`option[value="${langueId}"]`);
|
||||||
|
if (option) {
|
||||||
|
langue = option.textContent;
|
||||||
|
} else {
|
||||||
|
langue = langueId;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
langue = langueId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!langue) {
|
||||||
|
langue = event?.langue || event?.extendedProps?.langue || '';
|
||||||
|
}
|
||||||
|
setText('view_langue', langue);
|
||||||
|
|
||||||
|
// Entités
|
||||||
|
setText('view_beneficiaire', getEntityName('beneficiaire', event, 'id_beneficiaire', 'id_beneficiaire'));
|
||||||
|
setText('view_intervenant', getEntityName('intervenant', event, 'id_intervenant', 'id_intervenant'));
|
||||||
|
setText('view_local', getEntityName('local', event, 'id_local', 'id_local'));
|
||||||
|
|
||||||
|
// Termes taxonomy
|
||||||
|
setText('view_departement', getTermName(event, 'departement_label', 'departement', 'id_departement', 'id_departement'));
|
||||||
|
setText('view_type_intervention', getTermName(event, 'type_intervention_label', 'type_intervention', 'id_type_intervention', 'id_type_intervention'));
|
||||||
|
|
||||||
|
// Traducteur (logique spéciale)
|
||||||
|
let traducteurNom = '';
|
||||||
|
const traducteurId = event?.id_traducteur || event?.extendedProps?.id_traducteur;
|
||||||
|
const hasValidTraducteurId = traducteurId && parseInt(traducteurId, 10) > 0;
|
||||||
|
|
||||||
|
if (hasValidTraducteurId) {
|
||||||
|
traducteurNom = getEntityName('traducteur', event, 'id_traducteur', 'id_traducteur');
|
||||||
|
} else {
|
||||||
|
traducteurNom = event?.nom_traducteur || event?.extendedProps?.nom_traducteur || '';
|
||||||
|
}
|
||||||
|
setText('view_traducteur', traducteurNom);
|
||||||
|
|
||||||
|
// Données de groupe
|
||||||
|
const eventType = event?.type || event?.extendedProps?.type || '';
|
||||||
|
const groupeFields = document.querySelectorAll('.groupe-only-field');
|
||||||
|
|
||||||
|
if (eventType === 'groupe') {
|
||||||
|
groupeFields.forEach(field => field.style.display = '');
|
||||||
|
setText('view_nb_participants', event?.nb_participants || event?.extendedProps?.nb_participants || '');
|
||||||
|
setText('view_nb_hommes', event?.nb_hommes || event?.extendedProps?.nb_hommes || '');
|
||||||
|
setText('view_nb_femmes', event?.nb_femmes || event?.extendedProps?.nb_femmes || '');
|
||||||
|
} else {
|
||||||
|
groupeFields.forEach(field => field.style.display = 'none');
|
||||||
|
setText('view_nb_participants', '');
|
||||||
|
setText('view_nb_hommes', '');
|
||||||
|
setText('view_nb_femmes', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commentaire
|
||||||
|
setText('view_commentaire', event?.commentaire || event?.extendedProps?.commentaire || '');
|
||||||
|
|
||||||
|
// Bouton historique bénéficiaire (uniquement pour événements individuels)
|
||||||
|
const historiqueBtn = document.getElementById('showBeneficiaireHistoriqueBtn');
|
||||||
|
if (historiqueBtn) {
|
||||||
|
const beneficiaireId = event?.id_beneficiaire || event?.extendedProps?.id_beneficiaire;
|
||||||
|
if (eventType === 'individuel' && beneficiaireId) {
|
||||||
|
historiqueBtn.style.display = 'block';
|
||||||
|
historiqueBtn.setAttribute('data-benef', beneficiaireId);
|
||||||
|
} else {
|
||||||
|
historiqueBtn.style.display = 'none';
|
||||||
|
historiqueBtn.removeAttribute('data-benef');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier et afficher le bouton "Détail incident(s)" si l'événement a des incidents
|
||||||
|
const eventId = event?.id || event?.extendedProps?.id;
|
||||||
|
if (eventId) {
|
||||||
|
checkAndDisplayIncidentsButton(eventId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Afficher l'alerte de statut si présent ou absent
|
||||||
|
const statut = event?.statut || event?.extendedProps?.statut || '';
|
||||||
|
const statutAlert = document.getElementById('statutAlert');
|
||||||
|
const statutAlertText = document.getElementById('statutAlertText');
|
||||||
|
|
||||||
|
if (statutAlert && statutAlertText) {
|
||||||
|
if (statut === 'absent' || statut === 'absence') {
|
||||||
|
statutAlert.className = 'alert alert-danger mb-3';
|
||||||
|
statutAlertText.textContent = 'La personne a été marquée comme absente à ce rendez-vous.';
|
||||||
|
statutAlert.style.display = 'block';
|
||||||
|
} else if (statut === 'present') {
|
||||||
|
statutAlert.className = 'alert alert-success mb-3';
|
||||||
|
statutAlertText.textContent = 'La personne a été présente à ce rendez-vous.';
|
||||||
|
statutAlert.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
statutAlert.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour l'affichage de la modale selon le mode
|
||||||
|
* @param {string} mode - Mode actuel ('view', 'edit', 'create')
|
||||||
|
* @param {boolean} canEdit - Permission d'édition
|
||||||
|
* @param {boolean} canDelete - Permission de suppression
|
||||||
|
* @param {Object} eventData - Données de l'événement (optionnel)
|
||||||
|
*/
|
||||||
|
export function updateModalDisplay(mode, canEdit, canDelete, eventData = null) {
|
||||||
|
const viewBlock = document.getElementById('eventViewBlock');
|
||||||
|
const formBlock = document.getElementById('eventForm');
|
||||||
|
const statusButtons = document.getElementById('eventStatusButtons');
|
||||||
|
const viewFooter = document.getElementById('eventViewFooter');
|
||||||
|
const editFooter = document.getElementById('eventEditFooter');
|
||||||
|
const editBtn = document.getElementById('editEventBtn');
|
||||||
|
const deleteBtn = document.getElementById('deleteEvent');
|
||||||
|
const saveBtn = document.getElementById('saveEvent');
|
||||||
|
const cancelBtn = document.getElementById('cancelEditBtn');
|
||||||
|
const closeViewBtn = document.getElementById('closeViewBtn');
|
||||||
|
|
||||||
|
// Vérifier le statut et si l'événement est clôturé
|
||||||
|
const statut = eventData?.statut || eventData?.extendedProps?.statut || '';
|
||||||
|
const clotureFlag = eventData?.cloture_flag || eventData?.extendedProps?.cloture_flag;
|
||||||
|
const isEventCloture = clotureFlag === 1 || clotureFlag === '1' || clotureFlag === true;
|
||||||
|
const eventType = eventData?.type || eventData?.extendedProps?.type || '';
|
||||||
|
const isGroupe = eventType === 'groupe';
|
||||||
|
|
||||||
|
// Vérifier si les boutons de statut doivent être cachés
|
||||||
|
const shouldHideStatusButtons = statut === 'present' || statut === 'absence' || isEventCloture;
|
||||||
|
|
||||||
|
if (mode === 'view') {
|
||||||
|
if (viewBlock) viewBlock.style.display = 'block';
|
||||||
|
if (formBlock) formBlock.style.display = 'none';
|
||||||
|
if (viewFooter) viewFooter.style.display = 'block';
|
||||||
|
if (editFooter) editFooter.style.display = 'none';
|
||||||
|
if (editBtn) editBtn.style.display = canEdit ? 'inline-block' : 'none';
|
||||||
|
if (deleteBtn) deleteBtn.style.display = 'none';
|
||||||
|
if (saveBtn) saveBtn.style.display = 'none';
|
||||||
|
if (cancelBtn) cancelBtn.style.display = 'none';
|
||||||
|
if (closeViewBtn) closeViewBtn.style.display = 'inline-block';
|
||||||
|
|
||||||
|
// Cacher tous les boutons de statut en mode vue
|
||||||
|
if (statusButtons) statusButtons.style.display = 'none';
|
||||||
|
|
||||||
|
} else if (mode === 'edit' || mode === 'create') {
|
||||||
|
if (viewBlock) viewBlock.style.display = 'none';
|
||||||
|
if (formBlock) formBlock.style.display = 'block';
|
||||||
|
if (viewFooter) viewFooter.style.display = 'none';
|
||||||
|
if (editFooter) editFooter.style.display = 'block';
|
||||||
|
if (editBtn) editBtn.style.display = 'none';
|
||||||
|
if (deleteBtn) deleteBtn.style.display = (mode === 'edit' && canDelete) ? 'inline-block' : 'none';
|
||||||
|
if (saveBtn) saveBtn.style.display = 'inline-block';
|
||||||
|
if (cancelBtn) cancelBtn.style.display = 'inline-block';
|
||||||
|
if (closeViewBtn) closeViewBtn.style.display = 'none';
|
||||||
|
|
||||||
|
// Afficher ou cacher les boutons de statut selon le statut de l'événement
|
||||||
|
if (statusButtons) {
|
||||||
|
if (mode === 'edit' && canEdit && !shouldHideStatusButtons) {
|
||||||
|
statusButtons.style.display = 'block';
|
||||||
|
|
||||||
|
// Gérer l'affichage des boutons individuels selon le type d'événement
|
||||||
|
const allButtons = statusButtons.querySelectorAll('button');
|
||||||
|
allButtons.forEach(btn => {
|
||||||
|
// Masquer "Absent" pour les événements de groupe
|
||||||
|
if (btn.id === 'markAbsentBtn') {
|
||||||
|
btn.style.display = isGroupe ? 'none' : 'inline-block';
|
||||||
|
}
|
||||||
|
// Boutons spécifiques aux groupes (classe groupe-only-button)
|
||||||
|
else if (btn.classList.contains('groupe-only-button')) {
|
||||||
|
btn.style.display = isGroupe ? 'inline-block' : 'none';
|
||||||
|
}
|
||||||
|
// Boutons spécifiques aux individuels (classe individuel-only-button)
|
||||||
|
else if (btn.classList.contains('individuel-only-button')) {
|
||||||
|
btn.style.display = isGroupe ? 'none' : 'inline-block';
|
||||||
|
}
|
||||||
|
// Le bouton "Détail incident(s)" garde son état (géré par checkAndDisplayIncidentsButton)
|
||||||
|
else if (btn.id === 'viewIncidentsBtn') {
|
||||||
|
// Ne rien faire, l'état est géré ailleurs
|
||||||
|
}
|
||||||
|
// Afficher tous les autres boutons
|
||||||
|
else {
|
||||||
|
btn.style.display = 'inline-block';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
statusButtons.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si un événement est passé
|
||||||
|
* @param {Object} eventData - Données de l'événement
|
||||||
|
* @returns {boolean} - True si l'événement est passé
|
||||||
|
*/
|
||||||
|
export function checkIfEventIsPast(eventData) {
|
||||||
|
if (!eventData) return false;
|
||||||
|
|
||||||
|
let eventDate = null;
|
||||||
|
let eventTime = null;
|
||||||
|
|
||||||
|
// Essayer différentes sources pour la date
|
||||||
|
if (eventData.date_rdv) {
|
||||||
|
eventDate = eventData.date_rdv;
|
||||||
|
eventTime = eventData.heure_rdv || '00:00';
|
||||||
|
} else if (eventData.start) {
|
||||||
|
const startDate = new Date(eventData.start);
|
||||||
|
eventDate = startDate.toISOString().split('T')[0];
|
||||||
|
eventTime = startDate.toTimeString().substring(0, 5);
|
||||||
|
} else if (eventData.extendedProps?.date_rdv) {
|
||||||
|
eventDate = eventData.extendedProps.date_rdv;
|
||||||
|
eventTime = eventData.extendedProps.heure_rdv || '00:00';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!eventDate) return false;
|
||||||
|
|
||||||
|
const eventDateTime = new Date(`${eventDate}T${eventTime}`);
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
return eventDateTime <= now;
|
||||||
|
}
|
||||||
189
assets/js/modules/agenda-modal-dom.js
Normal file
189
assets/js/modules/agenda-modal-dom.js
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
// Module de gestion du DOM pour les modales
|
||||||
|
// Contient les helpers pour accéder et manipuler les éléments DOM de manière optimisée
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache pour les éléments DOM fréquemment accédés
|
||||||
|
*/
|
||||||
|
const domCache = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtient un élément du DOM avec mise en cache
|
||||||
|
* @param {string} elementId - ID de l'élément
|
||||||
|
* @returns {HTMLElement|null} - L'élément ou null
|
||||||
|
*/
|
||||||
|
export function getElement(elementId) {
|
||||||
|
if (domCache.has(elementId)) {
|
||||||
|
const cached = domCache.get(elementId);
|
||||||
|
// Vérifier que l'élément est toujours dans le DOM
|
||||||
|
if (cached && document.contains(cached)) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
domCache.delete(elementId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const element = document.getElementById(elementId);
|
||||||
|
if (element) {
|
||||||
|
domCache.set(elementId, element);
|
||||||
|
}
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vide le cache DOM (à appeler lors de la fermeture du modal)
|
||||||
|
*/
|
||||||
|
export function clearDomCache() {
|
||||||
|
domCache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Définit la valeur d'un élément de manière sécurisée
|
||||||
|
* @param {string} elementId - ID de l'élément
|
||||||
|
* @param {*} value - Valeur à définir
|
||||||
|
* @returns {boolean} - True si succès
|
||||||
|
*/
|
||||||
|
export function safeSetValue(elementId, value) {
|
||||||
|
const element = getElement(elementId);
|
||||||
|
if (!element) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si c'est un select avec Select2 initialisé
|
||||||
|
if (element.tagName === 'SELECT' && window.jQuery && jQuery(element).hasClass('select2-hidden-accessible')) {
|
||||||
|
jQuery(element).val(value).trigger('change');
|
||||||
|
} else {
|
||||||
|
element.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère la valeur d'un élément de manière sécurisée
|
||||||
|
* @param {string} elementId - ID de l'élément
|
||||||
|
* @returns {string|null} - Valeur de l'élément ou null
|
||||||
|
*/
|
||||||
|
export function safeGetValue(elementId) {
|
||||||
|
const element = getElement(elementId);
|
||||||
|
if (!element) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return element.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Affiche ou cache un élément
|
||||||
|
* @param {string} elementId - ID de l'élément
|
||||||
|
* @param {boolean} show - True pour afficher, false pour cacher
|
||||||
|
*/
|
||||||
|
export function toggleElement(elementId, show) {
|
||||||
|
const element = getElement(elementId);
|
||||||
|
if (element) {
|
||||||
|
element.style.display = show ? '' : 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ajoute une classe à un élément
|
||||||
|
* @param {string} elementId - ID de l'élément
|
||||||
|
* @param {string} className - Classe à ajouter
|
||||||
|
*/
|
||||||
|
export function addClass(elementId, className) {
|
||||||
|
const element = getElement(elementId);
|
||||||
|
if (element) {
|
||||||
|
element.classList.add(className);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retire une classe d'un élément
|
||||||
|
* @param {string} elementId - ID de l'élément
|
||||||
|
* @param {string} className - Classe à retirer
|
||||||
|
*/
|
||||||
|
export function removeClass(elementId, className) {
|
||||||
|
const element = getElement(elementId);
|
||||||
|
if (element) {
|
||||||
|
element.classList.remove(className);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si un élément a une classe
|
||||||
|
* @param {string} elementId - ID de l'élément
|
||||||
|
* @param {string} className - Classe à vérifier
|
||||||
|
* @returns {boolean} - True si l'élément a la classe
|
||||||
|
*/
|
||||||
|
export function hasClass(elementId, className) {
|
||||||
|
const element = getElement(elementId);
|
||||||
|
return element ? element.classList.contains(className) : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Définit le texte HTML d'un élément
|
||||||
|
* @param {string} elementId - ID de l'élément
|
||||||
|
* @param {string} html - HTML à définir
|
||||||
|
*/
|
||||||
|
export function setHTML(elementId, html) {
|
||||||
|
const element = getElement(elementId);
|
||||||
|
if (element) {
|
||||||
|
element.innerHTML = html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Définit le texte d'un élément
|
||||||
|
* @param {string} elementId - ID de l'élément
|
||||||
|
* @param {string} text - Texte à définir
|
||||||
|
*/
|
||||||
|
export function setText(elementId, text) {
|
||||||
|
const element = getElement(elementId);
|
||||||
|
if (element) {
|
||||||
|
element.textContent = text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ajoute un event listener à un élément
|
||||||
|
* @param {string} elementId - ID de l'élément
|
||||||
|
* @param {string} event - Type d'événement
|
||||||
|
* @param {Function} handler - Gestionnaire d'événement
|
||||||
|
*/
|
||||||
|
export function addListener(elementId, event, handler) {
|
||||||
|
const element = getElement(elementId);
|
||||||
|
if (element) {
|
||||||
|
element.addEventListener(event, handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nettoie tous les champs d'un formulaire
|
||||||
|
* @param {Array<string>} textFieldIds - IDs des champs texte
|
||||||
|
* @param {Array<string>} selectFieldIds - IDs des selects
|
||||||
|
*/
|
||||||
|
export function clearFormFields(textFieldIds, selectFieldIds) {
|
||||||
|
// Nettoyer les champs de texte
|
||||||
|
textFieldIds.forEach(fieldId => {
|
||||||
|
const field = getElement(fieldId);
|
||||||
|
if (field) {
|
||||||
|
field.value = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Nettoyer les selects
|
||||||
|
selectFieldIds.forEach(selectId => {
|
||||||
|
const select = getElement(selectId);
|
||||||
|
if (select) {
|
||||||
|
select.value = '';
|
||||||
|
|
||||||
|
// Si c'est un <select>, gérer Select2 et les options
|
||||||
|
if (select.tagName === 'SELECT' && select.options) {
|
||||||
|
if (window.jQuery && jQuery(select).hasClass('select2-hidden-accessible')) {
|
||||||
|
jQuery(select).val('').trigger('change');
|
||||||
|
}
|
||||||
|
Array.from(select.options).forEach(option => {
|
||||||
|
option.style.display = '';
|
||||||
|
option.disabled = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
517
assets/js/modules/agenda-modal-forms.js
Normal file
517
assets/js/modules/agenda-modal-forms.js
Normal file
@ -0,0 +1,517 @@
|
|||||||
|
// Module de gestion des formulaires de la modale
|
||||||
|
// Contient la logique de remplissage, validation et soumission des formulaires
|
||||||
|
|
||||||
|
import { createEvent, updateEvent } from './agenda-api.js';
|
||||||
|
import { notifyError, notifySuccess } from './agenda-notifications.js';
|
||||||
|
import { safeSetValue, safeGetValue } from './agenda-modal-dom.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calcule l'heure de fin (+1h par rapport au début)
|
||||||
|
* @param {string} heureDebut - Heure de début au format HH:MM
|
||||||
|
* @returns {string} - Heure de fin au format HH:MM
|
||||||
|
*/
|
||||||
|
function calculateHeureFin(heureDebut) {
|
||||||
|
const [h, m] = heureDebut.split(':').map(Number);
|
||||||
|
const heureFin = ((h + 1) % 24).toString().padStart(2, '0') + ':' + m.toString().padStart(2, '0');
|
||||||
|
return heureFin;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remplit le formulaire avec une date (pour création)
|
||||||
|
* @param {Object} data - Données avec startStr et endStr
|
||||||
|
*/
|
||||||
|
export function fillFormWithDate(data) {
|
||||||
|
const dateRdv = data?.startStr?.split('T')[0] || '';
|
||||||
|
safeSetValue('date_rdv', dateRdv);
|
||||||
|
|
||||||
|
const heureDebut = data?.startStr?.split('T')[1]?.substring(0, 5) || '09:00';
|
||||||
|
safeSetValue('heure_rdv', heureDebut);
|
||||||
|
|
||||||
|
safeSetValue('date_fin', dateRdv);
|
||||||
|
|
||||||
|
const heureFin = data?.endStr?.split('T')[1]?.substring(0, 5) || '09:15';
|
||||||
|
safeSetValue('heure_fin', heureFin);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remplit le formulaire avec les données d'un événement (pour édition)
|
||||||
|
* @param {Object} event - Données de l'événement
|
||||||
|
* @param {Function} filterTraducteursByLangue - Fonction pour filtrer les traducteurs
|
||||||
|
*/
|
||||||
|
export function fillFormWithEvent(event, filterTraducteursByLangue = null) {
|
||||||
|
if (!event) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gérer les dates/heures selon les sources disponibles
|
||||||
|
if (event.date && event.heure) {
|
||||||
|
// Données directes de l'API
|
||||||
|
safeSetValue('date_rdv', event.date);
|
||||||
|
const heureDebut = event.heure.substring(0, 5);
|
||||||
|
safeSetValue('heure_rdv', heureDebut);
|
||||||
|
const dateFin = event.date_fin && event.date_fin >= event.date ? event.date_fin : event.date;
|
||||||
|
safeSetValue('date_fin', dateFin);
|
||||||
|
const heureFin = event.heure_fin ? event.heure_fin.substring(0, 5) : calculateHeureFin(heureDebut);
|
||||||
|
safeSetValue('heure_fin', heureFin);
|
||||||
|
} else if (event.extendedProps?.date && event.extendedProps?.heure) {
|
||||||
|
// Données dans extendedProps
|
||||||
|
safeSetValue('date_rdv', event.extendedProps.date);
|
||||||
|
const heureDebut = event.extendedProps.heure.substring(0, 5);
|
||||||
|
safeSetValue('heure_rdv', heureDebut);
|
||||||
|
const dateFin = event.extendedProps.date_fin && event.extendedProps.date_fin >= event.extendedProps.date ? event.extendedProps.date_fin : event.extendedProps.date;
|
||||||
|
safeSetValue('date_fin', dateFin);
|
||||||
|
const heureFin = event.extendedProps.heure_fin ? event.extendedProps.heure_fin.substring(0, 5) : calculateHeureFin(heureDebut);
|
||||||
|
safeSetValue('heure_fin', heureFin);
|
||||||
|
} else if (event.start && event.end) {
|
||||||
|
// Données FullCalendar
|
||||||
|
try {
|
||||||
|
const startDate = new Date(event.start);
|
||||||
|
const endDate = new Date(event.end);
|
||||||
|
|
||||||
|
if (!isNaN(startDate.getTime()) && !isNaN(endDate.getTime())) {
|
||||||
|
const startDateStr = formatDate(startDate);
|
||||||
|
const endDateStr = formatDate(endDate);
|
||||||
|
const heureDebut = formatTime(startDate);
|
||||||
|
const heureFin = formatTime(endDate);
|
||||||
|
|
||||||
|
safeSetValue('date_rdv', startDateStr);
|
||||||
|
safeSetValue('heure_rdv', heureDebut);
|
||||||
|
safeSetValue('date_fin', endDateStr >= startDateStr ? endDateStr : startDateStr);
|
||||||
|
safeSetValue('heure_fin', heureFin);
|
||||||
|
} else {
|
||||||
|
setCurrentDateTime();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setCurrentDateTime();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setCurrentDateTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remplir les autres champs
|
||||||
|
const extendedProps = event.extendedProps || {};
|
||||||
|
safeSetValue('type', extendedProps.type || event.type || '');
|
||||||
|
|
||||||
|
const langueValue = extendedProps.langue || event.langue || '';
|
||||||
|
safeSetValue('langue', langueValue);
|
||||||
|
|
||||||
|
const beneficiaireId = extendedProps.id_beneficiaire || event.id_beneficiaire || '';
|
||||||
|
safeSetValue('id_beneficiaire', beneficiaireId);
|
||||||
|
safeSetValue('id_intervenant', extendedProps.id_intervenant || event.id_intervenant || '');
|
||||||
|
safeSetValue('id_traducteur', extendedProps.id_traducteur || event.id_traducteur || '');
|
||||||
|
safeSetValue('nom_traducteur', extendedProps.nom_traducteur || event.nom_traducteur || '');
|
||||||
|
safeSetValue('id_local', extendedProps.id_local || event.id_local || '');
|
||||||
|
safeSetValue('id_departement', extendedProps.id_departement || event.id_departement || '');
|
||||||
|
safeSetValue('id_type_intervention', extendedProps.id_type_intervention || event.id_type_intervention || '');
|
||||||
|
safeSetValue('commentaire', extendedProps.commentaire || event.commentaire || '');
|
||||||
|
|
||||||
|
// Mettre à jour les selects Select2
|
||||||
|
updateSelect2Fields();
|
||||||
|
|
||||||
|
// Filtrer les options langue selon langues_disponibles si c'est une permanence
|
||||||
|
const type = extendedProps.type || event.type || '';
|
||||||
|
if (type === 'permanence') {
|
||||||
|
const languesDisponibles = extendedProps.langues_disponibles;
|
||||||
|
const langueSelect = document.getElementById('langue');
|
||||||
|
if (langueSelect && languesDisponibles && typeof languesDisponibles === 'string' && languesDisponibles.trim() !== '') {
|
||||||
|
const languesPermises = languesDisponibles.split(',').map(l => l.trim()).filter(l => l !== '');
|
||||||
|
if (languesPermises.length > 0) {
|
||||||
|
const currentValue = langueSelect.value; // Récupérer la valeur actuelle (définie juste avant)
|
||||||
|
Array.from(langueSelect.options).forEach(option => {
|
||||||
|
if (option.value === '') {
|
||||||
|
option.style.display = '';
|
||||||
|
option.disabled = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Garder l'option sélectionnée visible même si elle n'est pas dans langues_disponibles
|
||||||
|
const isCurrentlySelected = option.value === currentValue;
|
||||||
|
const optionSlug = option.getAttribute('data-slug');
|
||||||
|
const isPermise = optionSlug && languesPermises.includes(optionSlug);
|
||||||
|
if (isPermise || isCurrentlySelected) {
|
||||||
|
option.style.display = '';
|
||||||
|
option.disabled = false;
|
||||||
|
} else {
|
||||||
|
option.style.display = 'none';
|
||||||
|
option.disabled = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtrer les traducteurs selon la langue
|
||||||
|
if (filterTraducteursByLangue) {
|
||||||
|
setTimeout(() => filterTraducteursByLangue(), 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gérer la case "traducteur existant"
|
||||||
|
handleTraducteurExistingCheckbox(extendedProps.id_traducteur || event.id_traducteur);
|
||||||
|
|
||||||
|
// Gérer les champs conditionnels selon le type
|
||||||
|
handleTypeConditionalFields(type, event, extendedProps);
|
||||||
|
|
||||||
|
// Charger le statut liste rouge si bénéficiaire sélectionné
|
||||||
|
if (beneficiaireId) {
|
||||||
|
setTimeout(() => {
|
||||||
|
const beneficiaireSelect = document.getElementById('id_beneficiaire');
|
||||||
|
if (beneficiaireSelect) {
|
||||||
|
beneficiaireSelect.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formate une date en YYYY-MM-DD
|
||||||
|
* @param {Date} date - Date à formater
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function formatDate(date) {
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formate une heure en HH:MM
|
||||||
|
* @param {Date} date - Date à formater
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function formatTime(date) {
|
||||||
|
const hours = String(date.getHours()).padStart(2, '0');
|
||||||
|
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||||
|
return `${hours}:${minutes}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Définit la date et l'heure actuelles
|
||||||
|
*/
|
||||||
|
function setCurrentDateTime() {
|
||||||
|
const now = new Date();
|
||||||
|
const nowStr = formatDate(now);
|
||||||
|
const heureDebut = formatTime(now);
|
||||||
|
safeSetValue('date_rdv', nowStr);
|
||||||
|
safeSetValue('heure_rdv', heureDebut);
|
||||||
|
safeSetValue('date_fin', nowStr);
|
||||||
|
safeSetValue('heure_fin', calculateHeureFin(heureDebut));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour les champs Select2
|
||||||
|
*/
|
||||||
|
function updateSelect2Fields() {
|
||||||
|
const select2Fields = ['type', 'id_beneficiaire', 'id_intervenant', 'id_traducteur', 'id_local', 'id_departement', 'id_type_intervention'];
|
||||||
|
select2Fields.forEach(fieldId => {
|
||||||
|
const element = document.getElementById(fieldId);
|
||||||
|
if (element && element.tagName === 'SELECT' && window.jQuery && jQuery(element).hasClass('select2-hidden-accessible')) {
|
||||||
|
const currentValue = element.value;
|
||||||
|
if (currentValue) {
|
||||||
|
jQuery(element).val(currentValue).trigger('change');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gère la case à cocher "traducteur existant"
|
||||||
|
* @param {number|string} traducteurId - ID du traducteur
|
||||||
|
*/
|
||||||
|
function handleTraducteurExistingCheckbox(traducteurId) {
|
||||||
|
const tradSelect = document.getElementById('id_traducteur');
|
||||||
|
const useExistingCheckbox = document.getElementById('use_existing_traducteur');
|
||||||
|
const tradContainer = document.getElementById('traducteur-select-container');
|
||||||
|
|
||||||
|
if (useExistingCheckbox && tradContainer && tradSelect) {
|
||||||
|
const hasTraducteur = traducteurId && parseInt(traducteurId, 10) > 0;
|
||||||
|
useExistingCheckbox.checked = hasTraducteur;
|
||||||
|
|
||||||
|
if (hasTraducteur) {
|
||||||
|
tradContainer.classList.remove('d-none');
|
||||||
|
tradSelect.setAttribute('required', 'required');
|
||||||
|
} else {
|
||||||
|
tradContainer.classList.add('d-none');
|
||||||
|
tradSelect.removeAttribute('required');
|
||||||
|
safeSetValue('id_traducteur', '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gère les champs conditionnels selon le type d'événement
|
||||||
|
* @param {string} type - Type d'événement
|
||||||
|
* @param {Object} event - Données de l'événement
|
||||||
|
* @param {Object} extendedProps - Propriétés étendues
|
||||||
|
*/
|
||||||
|
function handleTypeConditionalFields(type, event, extendedProps) {
|
||||||
|
// Gérer les champs conditionnels
|
||||||
|
const groupeFields = document.getElementById('groupeFields');
|
||||||
|
const nbParticipantsField = document.getElementById('nb_participants');
|
||||||
|
const beneficiaireContainer = document.getElementById('id_beneficiaire')?.closest('.col-md-6');
|
||||||
|
const beneficiaireField = document.getElementById('id_beneficiaire');
|
||||||
|
|
||||||
|
if (!groupeFields || !nbParticipantsField || !beneficiaireContainer || !beneficiaireField) return;
|
||||||
|
|
||||||
|
if (type === 'groupe') {
|
||||||
|
// Afficher les champs de groupe
|
||||||
|
groupeFields.style.display = '';
|
||||||
|
nbParticipantsField.required = true;
|
||||||
|
beneficiaireContainer.style.display = 'none';
|
||||||
|
beneficiaireField.required = false;
|
||||||
|
|
||||||
|
// Remplir les champs de groupe
|
||||||
|
if (extendedProps.nb_participants || event.nb_participants) {
|
||||||
|
safeSetValue('nb_participants', extendedProps.nb_participants || event.nb_participants);
|
||||||
|
}
|
||||||
|
if (extendedProps.nb_hommes || event.nb_hommes) {
|
||||||
|
safeSetValue('nb_hommes', extendedProps.nb_hommes || event.nb_hommes);
|
||||||
|
}
|
||||||
|
if (extendedProps.nb_femmes || event.nb_femmes) {
|
||||||
|
safeSetValue('nb_femmes', extendedProps.nb_femmes || event.nb_femmes);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Masquer les champs de groupe
|
||||||
|
groupeFields.style.display = 'none';
|
||||||
|
nbParticipantsField.required = false;
|
||||||
|
beneficiaireContainer.style.display = '';
|
||||||
|
beneficiaireField.required = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Réinitialise le formulaire
|
||||||
|
*/
|
||||||
|
export function resetForm() {
|
||||||
|
const form = document.getElementById('eventForm');
|
||||||
|
if (form) {
|
||||||
|
form.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
clearFormErrors();
|
||||||
|
|
||||||
|
// Réinitialiser les champs conditionnels
|
||||||
|
const groupeFields = document.getElementById('groupeFields');
|
||||||
|
const nbParticipantsField = document.getElementById('nb_participants');
|
||||||
|
const beneficiaireContainer = document.getElementById('id_beneficiaire')?.closest('.col-md-6');
|
||||||
|
const beneficiaireField = document.getElementById('id_beneficiaire');
|
||||||
|
|
||||||
|
if (groupeFields) groupeFields.style.display = 'none';
|
||||||
|
if (nbParticipantsField) nbParticipantsField.required = false;
|
||||||
|
if (beneficiaireContainer) beneficiaireContainer.style.display = '';
|
||||||
|
if (beneficiaireField) beneficiaireField.required = true;
|
||||||
|
|
||||||
|
// Réinitialiser le bloc traducteur
|
||||||
|
const tradContainer = document.getElementById('traducteur-select-container');
|
||||||
|
const tradSelect = document.getElementById('id_traducteur');
|
||||||
|
const useExistingCheckbox = document.getElementById('use_existing_traducteur');
|
||||||
|
|
||||||
|
if (tradContainer) tradContainer.classList.add('d-none');
|
||||||
|
if (tradSelect) {
|
||||||
|
tradSelect.removeAttribute('required');
|
||||||
|
tradSelect.value = '';
|
||||||
|
if (window.jQuery && jQuery(tradSelect).hasClass('select2-hidden-accessible')) {
|
||||||
|
jQuery(tradSelect).val('').trigger('change');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (useExistingCheckbox) useExistingCheckbox.checked = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Affiche les erreurs de formulaire
|
||||||
|
* @param {Object} errors - Objet des erreurs {field: message}
|
||||||
|
*/
|
||||||
|
export function showFormErrors(errors) {
|
||||||
|
clearFormErrors();
|
||||||
|
let hasGeneral = false;
|
||||||
|
|
||||||
|
for (const [field, message] of Object.entries(errors)) {
|
||||||
|
const input = document.getElementById(field);
|
||||||
|
if (input) {
|
||||||
|
input.classList.add('is-invalid');
|
||||||
|
let feedback = input.parentNode.querySelector('.invalid-feedback');
|
||||||
|
if (!feedback) {
|
||||||
|
feedback = document.createElement('div');
|
||||||
|
feedback.className = 'invalid-feedback';
|
||||||
|
input.parentNode.appendChild(feedback);
|
||||||
|
}
|
||||||
|
feedback.textContent = message;
|
||||||
|
} else {
|
||||||
|
hasGeneral = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Message général si erreur non liée à un champ
|
||||||
|
if (hasGeneral) {
|
||||||
|
const errorBox = document.getElementById('eventFormErrors');
|
||||||
|
if (errorBox) {
|
||||||
|
errorBox.textContent = errors._general || 'Erreur lors de la validation du formulaire';
|
||||||
|
errorBox.classList.remove('d-none');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Efface les erreurs de formulaire
|
||||||
|
*/
|
||||||
|
export function clearFormErrors() {
|
||||||
|
const form = document.getElementById('eventForm');
|
||||||
|
if (!form) return;
|
||||||
|
|
||||||
|
form.querySelectorAll('.is-invalid').forEach(el => el.classList.remove('is-invalid'));
|
||||||
|
form.querySelectorAll('.invalid-feedback').forEach(el => el.remove());
|
||||||
|
|
||||||
|
const errorBox = document.getElementById('eventFormErrors');
|
||||||
|
if (errorBox) {
|
||||||
|
errorBox.textContent = '';
|
||||||
|
errorBox.classList.add('d-none');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validation du formulaire
|
||||||
|
* @param {Object} data - Données du formulaire
|
||||||
|
* @param {string} mode - Mode ('create' ou 'edit')
|
||||||
|
* @returns {Object} - Objet des erreurs ou {}
|
||||||
|
*/
|
||||||
|
function validateForm(data, mode) {
|
||||||
|
const errors = {};
|
||||||
|
|
||||||
|
// Champs requis de base
|
||||||
|
const requiredFields = ['date_rdv', 'heure_rdv', 'type', 'langue', 'id_intervenant', 'id_local'];
|
||||||
|
|
||||||
|
// Ajouter le bénéficiaire seulement si ce n'est pas un RDV de groupe
|
||||||
|
if (data.type !== 'groupe') {
|
||||||
|
requiredFields.push('id_beneficiaire');
|
||||||
|
}
|
||||||
|
|
||||||
|
requiredFields.forEach(field => {
|
||||||
|
const value = data[field];
|
||||||
|
if (!value || value === '' || value === '0' || value === 'null') {
|
||||||
|
errors[field] = 'Le champ est requis';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Champs conditionnels pour groupe
|
||||||
|
if (data.type === 'groupe') {
|
||||||
|
if (!data.nb_participants || parseInt(data.nb_participants) < 1) {
|
||||||
|
errors['nb_participants'] = 'Nombre de participants requis (>0)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation: empêcher la création d'événements à une date passée
|
||||||
|
if (mode === 'create' && data.date_rdv) {
|
||||||
|
const today = new Date();
|
||||||
|
today.setHours(0, 0, 0, 0);
|
||||||
|
const eventDate = new Date(data.date_rdv + 'T00:00:00');
|
||||||
|
eventDate.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
if (eventDate < today) {
|
||||||
|
errors['date_rdv'] = 'La date de rendez-vous ne peut pas être dans le passé';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cohérence des dates/heures
|
||||||
|
if (data.date_fin && data.date_rdv) {
|
||||||
|
if (data.date_fin < data.date_rdv) {
|
||||||
|
errors['date_fin'] = 'La date de fin doit être après ou égale à la date de début';
|
||||||
|
} else if (data.date_fin === data.date_rdv && data.heure_fin && data.heure_rdv) {
|
||||||
|
if (data.heure_fin <= data.heure_rdv) {
|
||||||
|
errors['heure_fin'] = 'L\'heure de fin doit être après l\'heure de début';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prépare les données du formulaire
|
||||||
|
* @param {Object} data - Données brutes du formulaire
|
||||||
|
* @returns {Object} - Données préparées
|
||||||
|
*/
|
||||||
|
function prepareFormData(data) {
|
||||||
|
// Gérer la logique "traducteur existant"
|
||||||
|
const useExisting = data.use_existing_traducteur === '1' || data.use_existing_traducteur === 'on' || data.use_existing_traducteur === 'true';
|
||||||
|
const traducteurId = parseInt(data.id_traducteur, 10);
|
||||||
|
const hasValidTraducteurId = !isNaN(traducteurId) && traducteurId > 0;
|
||||||
|
|
||||||
|
if (useExisting && hasValidTraducteurId) {
|
||||||
|
// Utiliser id_traducteur et ignorer nom_traducteur
|
||||||
|
delete data.nom_traducteur;
|
||||||
|
data.id_traducteur = traducteurId.toString();
|
||||||
|
} else {
|
||||||
|
// Utiliser nom_traducteur et mettre id_traducteur à null
|
||||||
|
data.id_traducteur = null;
|
||||||
|
if (!data.nom_traducteur || data.nom_traducteur.trim() === '') {
|
||||||
|
delete data.nom_traducteur;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gestion de la soumission du formulaire
|
||||||
|
* @param {string} mode - Mode ('create' ou 'edit')
|
||||||
|
* @param {number|null} eventId - ID de l'événement (si édition)
|
||||||
|
* @param {Function} onSuccess - Callback de succès
|
||||||
|
*/
|
||||||
|
export async function handleEventFormSubmit(mode, eventId = null, onSuccess = null) {
|
||||||
|
clearFormErrors();
|
||||||
|
|
||||||
|
const form = document.getElementById('eventForm');
|
||||||
|
if (!form) {
|
||||||
|
notifyError('Formulaire non trouvé');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData(form);
|
||||||
|
let data = Object.fromEntries(formData.entries());
|
||||||
|
|
||||||
|
// Gérer les champs multiples (comme langues[]) UNIQUEMENT pour les permanences
|
||||||
|
// Ne pas traiter pour les autres types d'événements pour ne pas casser la mise à jour classique
|
||||||
|
if (data.type === 'permanence') {
|
||||||
|
const langues = formData.getAll('langues[]');
|
||||||
|
if (langues && langues.length > 0) {
|
||||||
|
// Filtrer les valeurs vides et convertir en tableau de slugs
|
||||||
|
data.langues = langues.filter(value => value !== '' && value !== null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation côté client
|
||||||
|
const errors = validateForm(data, mode);
|
||||||
|
if (Object.keys(errors).length > 0) {
|
||||||
|
showFormErrors(errors);
|
||||||
|
notifyError('Veuillez corriger les erreurs du formulaire');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Préparer les données
|
||||||
|
data = prepareFormData(data);
|
||||||
|
|
||||||
|
// Appel API
|
||||||
|
try {
|
||||||
|
if (mode === 'create') {
|
||||||
|
await createEvent(data);
|
||||||
|
notifySuccess('Événement créé avec succès');
|
||||||
|
} else if (mode === 'edit' && eventId) {
|
||||||
|
await updateEvent(eventId, data);
|
||||||
|
notifySuccess('Événement modifié avec succès');
|
||||||
|
}
|
||||||
|
if (onSuccess) onSuccess();
|
||||||
|
} catch (e) {
|
||||||
|
// Gestion des erreurs serveur
|
||||||
|
const serverErrors = {};
|
||||||
|
if (e && e.message) {
|
||||||
|
if (e.details && e.details.field) {
|
||||||
|
serverErrors[e.details.field] = e.details.message;
|
||||||
|
} else {
|
||||||
|
serverErrors._general = e.message;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
serverErrors._general = 'Erreur serveur inconnue';
|
||||||
|
}
|
||||||
|
showFormErrors(serverErrors);
|
||||||
|
notifyError(e.message || 'Erreur lors de la sauvegarde');
|
||||||
|
}
|
||||||
|
}
|
||||||
707
assets/js/modules/agenda-modal-select.js
Normal file
707
assets/js/modules/agenda-modal-select.js
Normal file
@ -0,0 +1,707 @@
|
|||||||
|
// Module de gestion des selects pour les modales
|
||||||
|
// Contient la logique de filtrage, Select2 et population des selects
|
||||||
|
|
||||||
|
import { getFilters } from './agenda-api.js';
|
||||||
|
import { notifyError } from './agenda-notifications.js';
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
const DEBUG_SELECTS = false; // Activer pour déboguer
|
||||||
|
|
||||||
|
// Flags pour éviter les boucles infinies
|
||||||
|
let isUpdatingSelects = false;
|
||||||
|
let isFilteringTraducteurs = false;
|
||||||
|
|
||||||
|
// Cache pour les disponibilités
|
||||||
|
const disponibilitesCache = new Map();
|
||||||
|
let lastPopulateCall = null;
|
||||||
|
let isPopulateSelectsRunning = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise Select2 sur tous les selects de la modale
|
||||||
|
*/
|
||||||
|
export function initializeSelect2() {
|
||||||
|
if (!window.jQuery || !window.jQuery.fn.select2) {
|
||||||
|
console.warn('Select2 non disponible, réessai dans 100ms...');
|
||||||
|
setTimeout(initializeSelect2, 100);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier spécifiquement le select langue avant initialisation
|
||||||
|
const langueSelect = document.getElementById('langue');
|
||||||
|
if (langueSelect) {
|
||||||
|
// Forcer l'ajout de la classe skip-select2 si elle n'est pas présente
|
||||||
|
if (!langueSelect.classList.contains('skip-select2')) {
|
||||||
|
console.warn('⚠️ [LANGUE] Classe skip-select2 manquante, ajout forcé');
|
||||||
|
langueSelect.classList.add('skip-select2');
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasSkipSelect2 = langueSelect.classList.contains('skip-select2');
|
||||||
|
const hasSelect2Class = langueSelect.classList.contains('select2');
|
||||||
|
const isAlreadySelect2 = window.jQuery && jQuery(langueSelect).hasClass('select2-hidden-accessible');
|
||||||
|
console.log('🔵 [LANGUE] initializeSelect2 - skip-select2:', hasSkipSelect2, '| classe select2:', hasSelect2Class, '| Déjà Select2:', isAlreadySelect2, '| ID:', langueSelect.id, '| Classes:', langueSelect.className, '| HTML:', langueSelect.outerHTML.substring(0, 200));
|
||||||
|
|
||||||
|
// Si le select langue a skip-select2 mais est déjà en Select2, le détruire
|
||||||
|
if (hasSkipSelect2 && isAlreadySelect2) {
|
||||||
|
console.log('⚠️ [LANGUE] Select2 détecté sur select avec skip-select2, destruction...');
|
||||||
|
try {
|
||||||
|
jQuery(langueSelect).select2('destroy');
|
||||||
|
// Nettoyer les classes et attributs Select2
|
||||||
|
langueSelect.classList.remove('select2-hidden-accessible', 'select2');
|
||||||
|
langueSelect.removeAttribute('data-select2-id');
|
||||||
|
langueSelect.removeAttribute('tabindex');
|
||||||
|
langueSelect.removeAttribute('aria-hidden');
|
||||||
|
console.log('✅ [LANGUE] Select2 détruit, classes après:', langueSelect.className);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Erreur lors de la destruction Select2 pour langue:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// S'assurer que la classe select2 n'est pas présente si skip-select2 est là
|
||||||
|
if (hasSkipSelect2 && hasSelect2Class) {
|
||||||
|
langueSelect.classList.remove('select2');
|
||||||
|
console.log('🧹 [LANGUE] Classe select2 retirée (skip-select2 présent)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jQuery('#eventModal select:not(.skip-select2)').each(function() {
|
||||||
|
const $select = jQuery(this);
|
||||||
|
|
||||||
|
// Ignorer explicitement le select langue s'il a skip-select2
|
||||||
|
if ($select.attr('id') === 'langue' && $select.hasClass('skip-select2')) {
|
||||||
|
console.log('⏭️ [LANGUE] Ignoré dans initializeSelect2 (skip-select2)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si Select2 est déjà initialisé, le détruire pour réappliquer
|
||||||
|
if ($select.hasClass('select2-hidden-accessible') || $select.data('select2')) {
|
||||||
|
try { $select.select2('destroy'); } catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
$select.select2({
|
||||||
|
width: '100%',
|
||||||
|
placeholder: 'Sélectionner...',
|
||||||
|
allowClear: true,
|
||||||
|
dropdownParent: jQuery('#eventModal')
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fusionne les données de l'événement avec les disponibilités
|
||||||
|
* @param {Object} availabilityData - Données de disponibilité de l'API
|
||||||
|
* @param {Object} eventData - Données de l'événement
|
||||||
|
* @returns {Object} - Données fusionnées
|
||||||
|
*/
|
||||||
|
function mergeEventDataWithAvailability(availabilityData, eventData) {
|
||||||
|
if (DEBUG_SELECTS) {
|
||||||
|
console.log('🔍 [MERGE] Fusion des données', { availabilityData, eventData });
|
||||||
|
}
|
||||||
|
|
||||||
|
const merged = { ...availabilityData };
|
||||||
|
|
||||||
|
if (!eventData) return merged;
|
||||||
|
|
||||||
|
const extendedProps = eventData.extendedProps || {};
|
||||||
|
|
||||||
|
// Helper pour fusionner un tableau d'entités
|
||||||
|
function mergeEntities(availabilityEntities, eventEntityId, eventEntityData) {
|
||||||
|
if (!eventEntityId || !eventEntityData) {
|
||||||
|
return availabilityEntities;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier si l'entité existe déjà (conversion en string pour comparaison)
|
||||||
|
const eventEntityIdStr = eventEntityId != null ? eventEntityId.toString() : null;
|
||||||
|
const exists = availabilityEntities.some(entity =>
|
||||||
|
entity.id != null && entity.id.toString() === eventEntityIdStr
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!exists) {
|
||||||
|
return [...availabilityEntities, eventEntityData];
|
||||||
|
}
|
||||||
|
|
||||||
|
return availabilityEntities;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fusionner chaque type d'entité
|
||||||
|
if (extendedProps.id_beneficiaire && eventData.beneficiaire) {
|
||||||
|
merged.beneficiaires = mergeEntities(
|
||||||
|
merged.beneficiaires || [],
|
||||||
|
extendedProps.id_beneficiaire,
|
||||||
|
{
|
||||||
|
id: extendedProps.id_beneficiaire,
|
||||||
|
nom: eventData.beneficiaire.nom + ' ' + (eventData.beneficiaire.prenom || ''),
|
||||||
|
prenom: eventData.beneficiaire.prenom
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extendedProps.id_intervenant && eventData.intervenant) {
|
||||||
|
merged.intervenants = mergeEntities(
|
||||||
|
merged.intervenants || [],
|
||||||
|
extendedProps.id_intervenant,
|
||||||
|
{
|
||||||
|
id: extendedProps.id_intervenant,
|
||||||
|
nom: eventData.intervenant.nom + ' ' + (eventData.intervenant.prenom || ''),
|
||||||
|
prenom: eventData.intervenant.prenom
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extendedProps.id_traducteur && eventData.traducteur) {
|
||||||
|
merged.traducteurs = mergeEntities(
|
||||||
|
merged.traducteurs || [],
|
||||||
|
extendedProps.id_traducteur,
|
||||||
|
{
|
||||||
|
id: extendedProps.id_traducteur,
|
||||||
|
nom: eventData.traducteur.nom + ' ' + (eventData.traducteur.prenom || ''),
|
||||||
|
prenom: eventData.traducteur.prenom
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extendedProps.id_local && eventData.local) {
|
||||||
|
merged.locaux = mergeEntities(
|
||||||
|
merged.locaux || [],
|
||||||
|
extendedProps.id_local,
|
||||||
|
{
|
||||||
|
id: extendedProps.id_local,
|
||||||
|
nom: eventData.local.nom
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extendedProps.langue || eventData.langue) {
|
||||||
|
const langueId = extendedProps.langue || eventData.langue;
|
||||||
|
let langueNom = 'Langue';
|
||||||
|
let langueSlug = null;
|
||||||
|
|
||||||
|
if (typeof eventData.langue === 'object' && eventData.langue) {
|
||||||
|
langueNom = eventData.langue.nom || 'Langue';
|
||||||
|
langueSlug = eventData.langue.slug || eventData.langue.id || langueId;
|
||||||
|
} else {
|
||||||
|
// Vérifier si langue_label existe et n'est pas égal à l'ID
|
||||||
|
if (eventData.langue_label && eventData.langue_label !== langueId) {
|
||||||
|
langueNom = eventData.langue_label;
|
||||||
|
} else if (extendedProps.langue_label && extendedProps.langue_label !== langueId) {
|
||||||
|
langueNom = extendedProps.langue_label;
|
||||||
|
}
|
||||||
|
langueSlug = langueId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const langueData = { id: langueId, nom: langueNom };
|
||||||
|
if (langueSlug) langueData.slug = langueSlug;
|
||||||
|
|
||||||
|
merged.langues = mergeEntities(
|
||||||
|
merged.langues || [],
|
||||||
|
langueId,
|
||||||
|
langueData
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extendedProps.id_departement || eventData.id_departement) {
|
||||||
|
const departementId = extendedProps.id_departement || eventData.id_departement;
|
||||||
|
const departementNom = eventData.departement?.nom || eventData.departement || 'Département';
|
||||||
|
|
||||||
|
merged.departements = mergeEntities(
|
||||||
|
merged.departements || [],
|
||||||
|
departementId,
|
||||||
|
{
|
||||||
|
id: departementId,
|
||||||
|
nom: departementNom
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extendedProps.id_type_intervention || eventData.id_type_intervention) {
|
||||||
|
const typeInterventionId = extendedProps.id_type_intervention || eventData.id_type_intervention;
|
||||||
|
const typeInterventionNom = eventData.type_intervention?.nom || eventData.type_intervention || 'Type d\'intervention';
|
||||||
|
|
||||||
|
merged.types_intervention = mergeEntities(
|
||||||
|
merged.types_intervention || [],
|
||||||
|
typeInterventionId,
|
||||||
|
{
|
||||||
|
id: typeInterventionId,
|
||||||
|
nom: typeInterventionNom
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filtre un select selon les items disponibles
|
||||||
|
* @param {string} selectId - ID du select
|
||||||
|
* @param {Array} availableItems - Items disponibles
|
||||||
|
*/
|
||||||
|
function filterSelect(selectId, availableItems) {
|
||||||
|
const select = document.getElementById(selectId);
|
||||||
|
if (!select || select.tagName !== 'SELECT') return;
|
||||||
|
|
||||||
|
if (!Array.isArray(availableItems)) {
|
||||||
|
if (DEBUG_SELECTS) console.warn(`Items invalides pour ${selectId}:`, availableItems);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Créer un ensemble des IDs disponibles
|
||||||
|
const availableIds = new Set(
|
||||||
|
availableItems
|
||||||
|
.filter(item => item && item.id != null)
|
||||||
|
.map(item => item.id.toString())
|
||||||
|
);
|
||||||
|
|
||||||
|
// Pour les langues, créer aussi un set des slugs
|
||||||
|
const availableSlugs = selectId === 'langue' ? new Set(
|
||||||
|
availableItems
|
||||||
|
.filter(item => item != null)
|
||||||
|
.flatMap(item => {
|
||||||
|
const slugs = [];
|
||||||
|
if (item.slug != null) slugs.push(item.slug.toString());
|
||||||
|
if (item.id != null && isNaN(item.id)) slugs.push(item.id.toString());
|
||||||
|
return slugs;
|
||||||
|
})
|
||||||
|
) : new Set();
|
||||||
|
|
||||||
|
const currentValue = select.value;
|
||||||
|
|
||||||
|
// Parcourir les options du select
|
||||||
|
if (!select.options) return;
|
||||||
|
|
||||||
|
Array.from(select.options).forEach(option => {
|
||||||
|
if (option.value === '') {
|
||||||
|
option.style.display = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isCurrentlySelected = option.value === currentValue;
|
||||||
|
let isAvailable = availableIds.has(option.value);
|
||||||
|
|
||||||
|
// Pour les langues, vérifier aussi le slug
|
||||||
|
if (!isAvailable && selectId === 'langue') {
|
||||||
|
const optionSlug = option.getAttribute('data-slug');
|
||||||
|
if (optionSlug && availableSlugs.has(optionSlug)) {
|
||||||
|
isAvailable = true;
|
||||||
|
} else if (availableSlugs.has(option.value)) {
|
||||||
|
isAvailable = true;
|
||||||
|
} else if (availableSlugs.size > 0) {
|
||||||
|
const optionText = option.text.toLowerCase().trim();
|
||||||
|
if (availableSlugs.has(optionText)) {
|
||||||
|
isAvailable = true;
|
||||||
|
} else {
|
||||||
|
const normalizedText = optionText.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
|
||||||
|
if (availableSlugs.has(normalizedText)) {
|
||||||
|
isAvailable = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Afficher ou cacher l'option
|
||||||
|
if (isAvailable || isCurrentlySelected) {
|
||||||
|
option.style.display = '';
|
||||||
|
option.disabled = false;
|
||||||
|
} else {
|
||||||
|
option.style.display = 'none';
|
||||||
|
option.disabled = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mettre à jour Select2 si initialisé
|
||||||
|
if (window.jQuery && jQuery(select).hasClass('select2-hidden-accessible')) {
|
||||||
|
jQuery(select).trigger('change.select2');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filtre les options des selects selon les disponibilités
|
||||||
|
* @param {Object} data - Données de disponibilité
|
||||||
|
* @param {Object} currentEventData - Données de l'événement actuel
|
||||||
|
*/
|
||||||
|
function filterSelectOptions(data, currentEventData = null) {
|
||||||
|
if (DEBUG_SELECTS) {
|
||||||
|
console.log('🎯 [FILTER] Filtrage des selects', { data });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtrer chaque type de select
|
||||||
|
filterSelect('id_beneficiaire', data.beneficiaires || []);
|
||||||
|
filterSelect('id_intervenant', data.intervenants || []);
|
||||||
|
filterSelect('id_traducteur', data.traducteurs || []);
|
||||||
|
filterSelect('id_local', data.locaux || []);
|
||||||
|
filterSelect('id_departement', data.departements || []);
|
||||||
|
filterSelect('id_type_intervention', data.types_intervention || []);
|
||||||
|
|
||||||
|
// Filtrer les langues selon langues_disponibles si c'est une permanence
|
||||||
|
const extendedProps = currentEventData?.extendedProps || {};
|
||||||
|
const isPermanence = extendedProps.type === 'permanence';
|
||||||
|
const languesDisponibles = extendedProps.langues_disponibles;
|
||||||
|
|
||||||
|
const langueSelect = document.getElementById('langue');
|
||||||
|
if (langueSelect && isPermanence && languesDisponibles && typeof languesDisponibles === 'string' && languesDisponibles.trim() !== '') {
|
||||||
|
const languesPermises = languesDisponibles.split(',').map(l => l.trim()).filter(l => l !== '');
|
||||||
|
|
||||||
|
if (languesPermises.length > 0) {
|
||||||
|
// Parcourir les options et cacher celles dont le data-slug n'est pas dans langues_disponibles
|
||||||
|
const currentValue = langueSelect.value; // Récupérer la valeur actuellement sélectionnée
|
||||||
|
Array.from(langueSelect.options).forEach(option => {
|
||||||
|
if (option.value === '') {
|
||||||
|
// Garder l'option vide visible
|
||||||
|
option.style.display = '';
|
||||||
|
option.disabled = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Garder l'option sélectionnée visible même si elle n'est pas dans langues_disponibles
|
||||||
|
const isCurrentlySelected = option.value === currentValue;
|
||||||
|
const optionSlug = option.getAttribute('data-slug');
|
||||||
|
const isPermise = optionSlug && languesPermises.includes(optionSlug);
|
||||||
|
|
||||||
|
if (isPermise || isCurrentlySelected) {
|
||||||
|
option.style.display = '';
|
||||||
|
option.disabled = false;
|
||||||
|
} else {
|
||||||
|
option.style.display = 'none';
|
||||||
|
option.disabled = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Si langues_disponibles est vide, afficher toutes les options
|
||||||
|
Array.from(langueSelect.options).forEach(option => {
|
||||||
|
option.style.display = '';
|
||||||
|
option.disabled = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Pas de permanence ou pas de langues_disponibles : afficher toutes les options
|
||||||
|
if (langueSelect) {
|
||||||
|
Array.from(langueSelect.options).forEach(option => {
|
||||||
|
option.style.display = '';
|
||||||
|
option.disabled = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Ne pas filtrer les langues en mode edit (quand l'événement a un ID)
|
||||||
|
// Le filtrage par disponibilités ne doit s'appliquer qu'en mode création
|
||||||
|
const isEditMode = currentEventData && (currentEventData.id || currentEventData.extendedProps?.id);
|
||||||
|
if (!isEditMode) {
|
||||||
|
filterSelect('langue', data.langues || []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Réinitialise le display des options du select langue
|
||||||
|
* À appeler lors de la fermeture du modal pour restaurer toutes les options
|
||||||
|
*/
|
||||||
|
export function resetLangueSelectDisplay() {
|
||||||
|
const langueSelect = document.getElementById('langue');
|
||||||
|
if (!langueSelect || langueSelect.tagName !== 'SELECT') return;
|
||||||
|
|
||||||
|
// Réinitialiser le display de toutes les options
|
||||||
|
Array.from(langueSelect.options).forEach(option => {
|
||||||
|
option.style.display = '';
|
||||||
|
option.disabled = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Présélectionne les valeurs dans les selects
|
||||||
|
* @param {Object} eventData - Données de l'événement
|
||||||
|
*/
|
||||||
|
function preselectValues(eventData) {
|
||||||
|
if (!eventData) return;
|
||||||
|
|
||||||
|
const extendedProps = eventData.extendedProps || {};
|
||||||
|
|
||||||
|
// Mapper les champs à présélectionner
|
||||||
|
const fieldsToPreselect = {
|
||||||
|
'id_beneficiaire': extendedProps.id_beneficiaire,
|
||||||
|
'id_intervenant': extendedProps.id_intervenant,
|
||||||
|
'id_traducteur': extendedProps.id_traducteur,
|
||||||
|
'id_local': extendedProps.id_local,
|
||||||
|
'id_departement': extendedProps.id_departement,
|
||||||
|
'id_type_intervention': extendedProps.id_type_intervention,
|
||||||
|
'langue': extendedProps.langue || eventData.langue
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.entries(fieldsToPreselect).forEach(([fieldId, value]) => {
|
||||||
|
if (!value) return;
|
||||||
|
|
||||||
|
const element = document.getElementById(fieldId);
|
||||||
|
if (!element || element.tagName !== 'SELECT') return;
|
||||||
|
|
||||||
|
// Tracer spécifiquement pour le champ langue
|
||||||
|
if (fieldId === 'langue') {
|
||||||
|
const currentValue = element.value || '';
|
||||||
|
const isSelect2 = window.jQuery && jQuery(element).hasClass('select2-hidden-accessible');
|
||||||
|
const hasSkipSelect2 = element.classList.contains('skip-select2');
|
||||||
|
const optionCount = element.options.length;
|
||||||
|
const optionExists = Array.from(element.options).some(opt => opt.value === value.toString());
|
||||||
|
console.log('🟣 [LANGUE] preselectValues - Valeur actuelle:', currentValue, '| Valeur à appliquer:', value, '| Source:', extendedProps.langue ? 'extendedProps' : eventData.langue ? 'eventData' : 'N/A', '| Select2:', isSelect2, '| skip-select2:', hasSkipSelect2, '| Options:', optionCount, '| Option existe:', optionExists);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier que l'option existe dans le select avant de la sélectionner
|
||||||
|
const optionExists = Array.from(element.options).some(opt => opt.value === value.toString());
|
||||||
|
if (!optionExists) {
|
||||||
|
if (fieldId === 'langue') {
|
||||||
|
console.warn(`⚠️ [LANGUE] Option avec valeur "${value}" non trouvée dans le select`);
|
||||||
|
} else {
|
||||||
|
console.warn(`⚠️ Option avec valeur "${value}" non trouvée dans le select ${fieldId}`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si Select2 est actif
|
||||||
|
if (window.jQuery && jQuery(element).hasClass('select2-hidden-accessible')) {
|
||||||
|
// Définir la valeur et déclencher l'événement change.select2
|
||||||
|
jQuery(element).val(value).trigger('change.select2');
|
||||||
|
} else {
|
||||||
|
// Select natif
|
||||||
|
element.value = value;
|
||||||
|
// Déclencher l'événement change pour notifier les autres listeners
|
||||||
|
element.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier la valeur après application pour le champ langue
|
||||||
|
if (fieldId === 'langue') {
|
||||||
|
const finalValue = element.value || '';
|
||||||
|
const selectedOption = element.options[element.selectedIndex];
|
||||||
|
const selectedText = selectedOption ? selectedOption.text : 'N/A';
|
||||||
|
console.log('🟠 [LANGUE] preselectValues - Valeur après application:', finalValue, '| Option sélectionnée:', selectedText, '| selectedIndex:', element.selectedIndex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Population des selects selon les disponibilités
|
||||||
|
* @param {Object} eventData - Données de l'événement
|
||||||
|
*/
|
||||||
|
export async function populateSelects(eventData = null) {
|
||||||
|
// Vérifier si une exécution est déjà en cours
|
||||||
|
if (isPopulateSelectsRunning) {
|
||||||
|
if (DEBUG_SELECTS) console.warn('⚠️ populateSelects déjà en cours');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isPopulateSelectsRunning = true;
|
||||||
|
isUpdatingSelects = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const dateInput = document.getElementById('date_rdv');
|
||||||
|
const heureInput = document.getElementById('heure_rdv');
|
||||||
|
|
||||||
|
if (!dateInput || !heureInput) {
|
||||||
|
console.warn('Champs date/heure non trouvés');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = dateInput.value;
|
||||||
|
const heure = heureInput.value;
|
||||||
|
|
||||||
|
if (!date || !heure) {
|
||||||
|
console.warn('Date et heure requises pour filtrer les disponibilités');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Créer une clé de cache
|
||||||
|
const cacheKey = `${date}|${heure}|${eventData ? eventData.id || 'new' : 'new'}`;
|
||||||
|
|
||||||
|
// Vérifier le cache
|
||||||
|
if (disponibilitesCache.has(cacheKey)) {
|
||||||
|
const cachedData = disponibilitesCache.get(cacheKey);
|
||||||
|
const mergedData = mergeEventDataWithAvailability(cachedData, eventData);
|
||||||
|
filterSelectOptions(mergedData, eventData);
|
||||||
|
|
||||||
|
// S'assurer que Select2 est initialisé avant de présélectionner
|
||||||
|
initializeSelect2();
|
||||||
|
// Petit délai pour s'assurer que le filtrage des langues est bien appliqué avant la présélection
|
||||||
|
// Et que les options sont bien dans le DOM
|
||||||
|
setTimeout(() => {
|
||||||
|
preselectValues(eventData);
|
||||||
|
}, 100);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Éviter les appels simultanés
|
||||||
|
const currentCall = `${date}|${heure}`;
|
||||||
|
if (lastPopulateCall === currentCall) return;
|
||||||
|
|
||||||
|
lastPopulateCall = currentCall;
|
||||||
|
|
||||||
|
// Appel API
|
||||||
|
const params = { date, heure };
|
||||||
|
const data = await getFilters('disponibilites', params);
|
||||||
|
|
||||||
|
// Mettre en cache (expire après 5 minutes)
|
||||||
|
disponibilitesCache.set(cacheKey, data);
|
||||||
|
setTimeout(() => disponibilitesCache.delete(cacheKey), 5 * 60 * 1000);
|
||||||
|
|
||||||
|
// Fusionner et filtrer
|
||||||
|
const mergedData = mergeEventDataWithAvailability(data, eventData);
|
||||||
|
filterSelectOptions(mergedData, eventData);
|
||||||
|
|
||||||
|
// S'assurer que Select2 est initialisé avant de présélectionner
|
||||||
|
initializeSelect2();
|
||||||
|
// Petit délai pour s'assurer que le filtrage des langues est bien appliqué avant la présélection
|
||||||
|
// Et que les options sont bien dans le DOM
|
||||||
|
setTimeout(() => {
|
||||||
|
preselectValues(eventData);
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors de la récupération des disponibilités:', error);
|
||||||
|
notifyError('Erreur lors de la récupération des disponibilités');
|
||||||
|
} finally {
|
||||||
|
isPopulateSelectsRunning = false;
|
||||||
|
isUpdatingSelects = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filtre les traducteurs selon la langue sélectionnée
|
||||||
|
*/
|
||||||
|
export function filterTraducteursByLangue() {
|
||||||
|
if (isUpdatingSelects || isFilteringTraducteurs) return;
|
||||||
|
|
||||||
|
isFilteringTraducteurs = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const langueSelect = document.getElementById('langue');
|
||||||
|
const traducteurSelect = document.getElementById('id_traducteur');
|
||||||
|
|
||||||
|
if (!langueSelect || !traducteurSelect || traducteurSelect.tagName !== 'SELECT') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedLangue = langueSelect.value ? langueSelect.value.toString() : '';
|
||||||
|
const currentTraducteurValue = traducteurSelect.value;
|
||||||
|
|
||||||
|
// Si aucune langue sélectionnée, afficher tous les traducteurs
|
||||||
|
if (!selectedLangue) {
|
||||||
|
if (traducteurSelect.options) {
|
||||||
|
Array.from(traducteurSelect.options).forEach(option => {
|
||||||
|
option.style.display = '';
|
||||||
|
option.disabled = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (window.jQuery && jQuery(traducteurSelect).hasClass('select2-hidden-accessible')) {
|
||||||
|
jQuery(traducteurSelect).trigger('change.select2');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Récupérer le slug de la langue
|
||||||
|
const langueOption = langueSelect.options[langueSelect.selectedIndex];
|
||||||
|
let langueSlug = null;
|
||||||
|
|
||||||
|
if (langueOption && langueOption.dataset.slug) {
|
||||||
|
langueSlug = langueOption.dataset.slug;
|
||||||
|
} else {
|
||||||
|
console.warn('Aucun slug disponible pour la langue sélectionnée');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtrer les traducteurs
|
||||||
|
if (traducteurSelect.options) {
|
||||||
|
Array.from(traducteurSelect.options).forEach(option => {
|
||||||
|
if (option.value === '') {
|
||||||
|
option.style.display = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const traducteurLangues = option.getAttribute('data-langue');
|
||||||
|
const isCurrentlySelected = option.value === currentTraducteurValue;
|
||||||
|
|
||||||
|
if (!traducteurLangues) {
|
||||||
|
option.style.display = isCurrentlySelected ? '' : 'none';
|
||||||
|
option.disabled = !isCurrentlySelected;
|
||||||
|
} else {
|
||||||
|
const languesArray = traducteurLangues.split(',');
|
||||||
|
const langueMatches = languesArray.some(lang => {
|
||||||
|
return lang.trim() === langueSlug || lang.trim() === selectedLangue;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (langueMatches || isCurrentlySelected) {
|
||||||
|
option.style.display = '';
|
||||||
|
option.disabled = false;
|
||||||
|
} else {
|
||||||
|
option.style.display = 'none';
|
||||||
|
option.disabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Si le traducteur actuel ne correspond plus, le vider
|
||||||
|
if (currentTraducteurValue && selectedLangue) {
|
||||||
|
const currentOption = traducteurSelect.options[traducteurSelect.selectedIndex];
|
||||||
|
if (currentOption && currentOption.value !== '' && currentOption.style.display === 'none') {
|
||||||
|
traducteurSelect.value = '';
|
||||||
|
if (window.jQuery && jQuery(traducteurSelect).hasClass('select2-hidden-accessible')) {
|
||||||
|
jQuery(traducteurSelect).val('').trigger('change.select2');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mettre à jour Select2
|
||||||
|
if (window.jQuery && jQuery(traducteurSelect).hasClass('select2-hidden-accessible')) {
|
||||||
|
jQuery(traducteurSelect).trigger('change.select2');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
isFilteringTraducteurs = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtient l'état du flag isUpdatingSelects
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function getIsUpdatingSelects() {
|
||||||
|
return isUpdatingSelects;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vide le cache des disponibilités
|
||||||
|
*/
|
||||||
|
export function clearDisponibilitesCache() {
|
||||||
|
disponibilitesCache.clear();
|
||||||
|
lastPopulateCall = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise le listener pour afficher l'alerte "Personne en liste rouge"
|
||||||
|
*/
|
||||||
|
export function initializeBeneficiaireListeRougeAlert() {
|
||||||
|
const beneficiaireSelect = document.getElementById('id_beneficiaire');
|
||||||
|
const alertElement = document.getElementById('beneficiaire-liste-rouge-alert');
|
||||||
|
|
||||||
|
if (!beneficiaireSelect || !alertElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonction pour vérifier et afficher l'alerte
|
||||||
|
const checkListeRouge = () => {
|
||||||
|
const selectedBeneficiaireId = beneficiaireSelect.value;
|
||||||
|
|
||||||
|
// Vérifier si le bénéficiaire est en liste rouge via crviACFData
|
||||||
|
if (selectedBeneficiaireId && window.crviACFData && window.crviACFData.beneficiaires) {
|
||||||
|
const beneficiaireData = window.crviACFData.beneficiaires[selectedBeneficiaireId];
|
||||||
|
|
||||||
|
if (beneficiaireData && beneficiaireData.personne_en_liste_rouge) {
|
||||||
|
alertElement.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
alertElement.style.display = 'none';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alertElement.style.display = 'none';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Écouter les changements du select
|
||||||
|
beneficiaireSelect.addEventListener('change', checkListeRouge);
|
||||||
|
|
||||||
|
// Écouter aussi les événements Select2 si présent
|
||||||
|
if (window.jQuery) {
|
||||||
|
jQuery(beneficiaireSelect).on('select2:select', checkListeRouge);
|
||||||
|
jQuery(beneficiaireSelect).on('select2:clear', checkListeRouge);
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@ -85,6 +85,12 @@ function collectFilters() {
|
|||||||
filters.annee = parseInt(anneeInput.value, 10);
|
filters.annee = parseInt(anneeInput.value, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Statut
|
||||||
|
const statutSelect = document.getElementById('stats_statut');
|
||||||
|
if (statutSelect && statutSelect.value) {
|
||||||
|
filters.statut = statutSelect.value;
|
||||||
|
}
|
||||||
|
|
||||||
// Filtre permanence
|
// Filtre permanence
|
||||||
const permanenceCheckbox = document.getElementById('stats_filtre_permanence');
|
const permanenceCheckbox = document.getElementById('stats_filtre_permanence');
|
||||||
if (permanenceCheckbox && permanenceCheckbox.checked) {
|
if (permanenceCheckbox && permanenceCheckbox.checked) {
|
||||||
@ -160,7 +166,8 @@ async function loadEvents() {
|
|||||||
|
|
||||||
// Afficher les résultats
|
// Afficher les résultats
|
||||||
displayEvents(result.events || []);
|
displayEvents(result.events || []);
|
||||||
updateCounters(result.total || 0, result.filtered || 0);
|
// Afficher le nombre d'événements filtrés comme total, et le nombre d'événements sur la page courante comme affichés
|
||||||
|
updateCounters(result.filtered || 0, (result.events || []).length);
|
||||||
displayPagination(result.page || 1, result.total_pages || 0);
|
displayPagination(result.page || 1, result.total_pages || 0);
|
||||||
|
|
||||||
// IMPORTANT: Masquer le loader EN PREMIER, puis afficher le tableau
|
// IMPORTANT: Masquer le loader EN PREMIER, puis afficher le tableau
|
||||||
@ -178,8 +185,8 @@ async function loadEvents() {
|
|||||||
table.style.display = 'table';
|
table.style.display = 'table';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Afficher la pagination si nécessaire
|
// Afficher la pagination si nécessaire (seulement s'il y a plus d'une page)
|
||||||
if (paginationContainer && result.total_pages > 0) {
|
if (paginationContainer && result.total_pages > 1) {
|
||||||
paginationContainer.style.display = 'block';
|
paginationContainer.style.display = 'block';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
440
assets/js/modules/agenda-visual-filters.js
Normal file
440
assets/js/modules/agenda-visual-filters.js
Normal file
@ -0,0 +1,440 @@
|
|||||||
|
/**
|
||||||
|
* Gestion des filtres visuels de l'agenda admin
|
||||||
|
* Filtres par département et capacités de traduction
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { getTraductionsCapacites } from './agenda-api.js';
|
||||||
|
|
||||||
|
class AgendaVisualFilters {
|
||||||
|
constructor() {
|
||||||
|
this.departements = window.crviACFData?.departements || {};
|
||||||
|
this.traductions = window.crviACFData?.traductions_capacites || {};
|
||||||
|
this.selectedDepartement = null;
|
||||||
|
this.selectedTraduction = null;
|
||||||
|
this.currentDateDebut = null;
|
||||||
|
this.currentDateFin = null;
|
||||||
|
this.calendarInstance = null;
|
||||||
|
|
||||||
|
this.departementsContainer = document.getElementById('departements-filter-buttons');
|
||||||
|
this.traductionsContainer = document.getElementById('traductions-filter-buttons');
|
||||||
|
|
||||||
|
if (this.departementsContainer && this.traductionsContainer) {
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.renderDepartementsButtons();
|
||||||
|
this.renderTraductionsButtons();
|
||||||
|
this.setupEventListeners();
|
||||||
|
|
||||||
|
// Initialiser les dates depuis le calendrier si disponible
|
||||||
|
this.initializeDatesFromCalendar();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise les dates depuis le calendrier au chargement
|
||||||
|
*/
|
||||||
|
initializeDatesFromCalendar() {
|
||||||
|
// Attendre que le calendrier soit disponible
|
||||||
|
const checkCalendar = () => {
|
||||||
|
if (window.currentCalendar) {
|
||||||
|
try {
|
||||||
|
const view = window.currentCalendar.view;
|
||||||
|
if (view && view.activeStart && view.activeEnd) {
|
||||||
|
const startStr = view.activeStart.toISOString().split('T')[0];
|
||||||
|
const endStr = view.activeEnd.toISOString().split('T')[0];
|
||||||
|
this.currentDateDebut = startStr;
|
||||||
|
this.currentDateFin = endStr;
|
||||||
|
console.log('📅 Dates initiales du calendrier:', { start: startStr, end: endStr });
|
||||||
|
|
||||||
|
// Recharger les capacités pour cette période initiale
|
||||||
|
this.reloadTraductionsCapacites(startStr, endStr);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('⚠️ Impossible de récupérer les dates initiales du calendrier:', error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Réessayer après un court délai
|
||||||
|
setTimeout(checkCalendar, 200);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Attendre un peu plus longtemps pour que le calendrier soit complètement initialisé
|
||||||
|
setTimeout(checkCalendar, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère les boutons pour les départements
|
||||||
|
*/
|
||||||
|
renderDepartementsButtons() {
|
||||||
|
const container = this.departementsContainer;
|
||||||
|
container.innerHTML = '';
|
||||||
|
|
||||||
|
// Bouton "Tous"
|
||||||
|
const btnTous = this.createButton({
|
||||||
|
id: 'dept-all',
|
||||||
|
label: 'Tous les départements',
|
||||||
|
icon: 'fas fa-th',
|
||||||
|
color: '#6c757d',
|
||||||
|
active: this.selectedDepartement === null
|
||||||
|
});
|
||||||
|
btnTous.addEventListener('click', () => this.selectDepartement(null));
|
||||||
|
container.appendChild(btnTous);
|
||||||
|
|
||||||
|
// Boutons pour chaque département
|
||||||
|
Object.entries(this.departements).forEach(([slug, dept]) => {
|
||||||
|
const btn = this.createButton({
|
||||||
|
id: `dept-${slug}`,
|
||||||
|
label: dept.nom,
|
||||||
|
icon: dept.icone || 'fas fa-building',
|
||||||
|
color: dept.couleur || '#6c757d',
|
||||||
|
active: this.selectedDepartement === slug
|
||||||
|
});
|
||||||
|
btn.addEventListener('click', () => this.selectDepartement(slug));
|
||||||
|
container.appendChild(btn);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Génère les boutons pour les capacités de traduction
|
||||||
|
*/
|
||||||
|
renderTraductionsButtons() {
|
||||||
|
const container = this.traductionsContainer;
|
||||||
|
container.innerHTML = '';
|
||||||
|
|
||||||
|
// Bouton "Tous"
|
||||||
|
const btnTous = this.createButton({
|
||||||
|
id: 'trad-all',
|
||||||
|
label: 'Toutes les langues',
|
||||||
|
icon: 'fas fa-language',
|
||||||
|
color: '#6c757d',
|
||||||
|
active: this.selectedTraduction === null
|
||||||
|
});
|
||||||
|
btnTous.addEventListener('click', () => this.selectTraduction(null));
|
||||||
|
container.appendChild(btnTous);
|
||||||
|
|
||||||
|
// Boutons pour chaque langue avec barre de progression
|
||||||
|
Object.entries(this.traductions).forEach(([slug, langue]) => {
|
||||||
|
const btn = this.createTraductionButton(slug, langue);
|
||||||
|
container.appendChild(btn);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée un bouton de filtre simple
|
||||||
|
*/
|
||||||
|
createButton({ id, label, icon, color, active = false }) {
|
||||||
|
const btn = document.createElement('button');
|
||||||
|
btn.type = 'button';
|
||||||
|
btn.className = `filter-btn ${active ? 'active' : ''}`;
|
||||||
|
btn.id = id;
|
||||||
|
btn.innerHTML = `
|
||||||
|
<i class="${icon}" style="color: ${color};"></i>
|
||||||
|
<span>${label}</span>
|
||||||
|
`;
|
||||||
|
return btn;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crée un bouton de capacité de traduction avec barre de progression
|
||||||
|
*/
|
||||||
|
createTraductionButton(slug, langue) {
|
||||||
|
const isDisabled = langue.remaining === 0;
|
||||||
|
const pourcentage = langue.total > 0
|
||||||
|
? Math.round((langue.remaining / langue.total) * 100)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const btn = document.createElement('button');
|
||||||
|
btn.type = 'button';
|
||||||
|
btn.className = `filter-btn trad-btn ${this.selectedTraduction === slug ? 'active' : ''} ${isDisabled ? 'disabled' : ''}`;
|
||||||
|
btn.id = `trad-${slug}`;
|
||||||
|
btn.disabled = isDisabled;
|
||||||
|
|
||||||
|
// Couleur de la barre selon le pourcentage
|
||||||
|
let barColor = '#28a745'; // Vert
|
||||||
|
if (pourcentage < 50) barColor = '#ffc107'; // Jaune
|
||||||
|
if (pourcentage < 25) barColor = '#dc3545'; // Rouge
|
||||||
|
if (pourcentage === 0) barColor = '#6c757d'; // Gris
|
||||||
|
|
||||||
|
btn.innerHTML = `
|
||||||
|
<div class="trad-btn-content">
|
||||||
|
<div class="trad-btn-header">
|
||||||
|
<i class="fas fa-language"></i>
|
||||||
|
<span class="trad-btn-label">${langue.name}</span>
|
||||||
|
</div>
|
||||||
|
<div class="trad-btn-stats">
|
||||||
|
<span class="trad-stat">${langue.remaining}/${langue.total}</span>
|
||||||
|
</div>
|
||||||
|
<div class="trad-progress">
|
||||||
|
<div class="trad-progress-bar" style="width: ${pourcentage}%; background-color: ${barColor};"></div>
|
||||||
|
</div>
|
||||||
|
<div class="trad-btn-details">
|
||||||
|
<span class="trad-detail">Matin: ${langue.by_periode.matin.remaining}/${langue.by_periode.matin.total}</span>
|
||||||
|
<span class="trad-detail">AM: ${langue.by_periode.apres_midi.remaining}/${langue.by_periode.apres_midi.total}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (!isDisabled) {
|
||||||
|
btn.addEventListener('click', () => this.selectTraduction(slug));
|
||||||
|
}
|
||||||
|
|
||||||
|
return btn;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sélectionne un département
|
||||||
|
*/
|
||||||
|
selectDepartement(slug) {
|
||||||
|
this.selectedDepartement = slug;
|
||||||
|
|
||||||
|
// Mettre à jour l'UI
|
||||||
|
this.departementsContainer.querySelectorAll('.filter-btn').forEach(btn => {
|
||||||
|
btn.classList.remove('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
const btnId = slug ? `dept-${slug}` : 'dept-all';
|
||||||
|
const activeBtn = document.getElementById(btnId);
|
||||||
|
if (activeBtn) {
|
||||||
|
activeBtn.classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Déclencher le filtrage
|
||||||
|
this.applyFilters();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sélectionne une capacité de traduction
|
||||||
|
*/
|
||||||
|
selectTraduction(slug) {
|
||||||
|
this.selectedTraduction = slug;
|
||||||
|
|
||||||
|
// Mettre à jour l'UI
|
||||||
|
this.traductionsContainer.querySelectorAll('.filter-btn').forEach(btn => {
|
||||||
|
btn.classList.remove('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
const btnId = slug ? `trad-${slug}` : 'trad-all';
|
||||||
|
const activeBtn = document.getElementById(btnId);
|
||||||
|
if (activeBtn) {
|
||||||
|
activeBtn.classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Déclencher le filtrage
|
||||||
|
this.applyFilters();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applique les filtres sélectionnés
|
||||||
|
*/
|
||||||
|
applyFilters() {
|
||||||
|
const filters = {
|
||||||
|
departement: this.selectedDepartement,
|
||||||
|
traduction: this.selectedTraduction
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('Filtres appliqués:', filters);
|
||||||
|
|
||||||
|
// Mettre à jour les filtres dans le formulaire principal si nécessaire
|
||||||
|
if (filters.departement) {
|
||||||
|
const deptSelect = document.getElementById('departement');
|
||||||
|
if (deptSelect) {
|
||||||
|
// Trouver l'option correspondante par le nom
|
||||||
|
const deptData = this.departements[filters.departement];
|
||||||
|
if (deptData) {
|
||||||
|
Array.from(deptSelect.options).forEach(option => {
|
||||||
|
if (option.textContent === deptData.nom) {
|
||||||
|
deptSelect.value = option.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const deptSelect = document.getElementById('departement');
|
||||||
|
if (deptSelect) {
|
||||||
|
deptSelect.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.traduction) {
|
||||||
|
const langueSelect = document.getElementById('langue_filtre');
|
||||||
|
if (langueSelect) {
|
||||||
|
// Récupérer l'ID de la langue depuis les données de traduction
|
||||||
|
const traductionData = this.traductions[filters.traduction];
|
||||||
|
if (traductionData && traductionData.id) {
|
||||||
|
// Utiliser l'ID de la langue pour le filtre
|
||||||
|
langueSelect.value = traductionData.id;
|
||||||
|
} else {
|
||||||
|
// Fallback : utiliser le slug si l'ID n'est pas disponible
|
||||||
|
langueSelect.value = filters.traduction;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Déclencher l'événement change pour que le système de filtrage existant prenne le relais
|
||||||
|
// Support pour Select2 si présent
|
||||||
|
if (window.jQuery && jQuery(langueSelect).hasClass('select2-hidden-accessible')) {
|
||||||
|
jQuery(langueSelect).trigger('change');
|
||||||
|
} else {
|
||||||
|
// Déclencher l'événement change natif
|
||||||
|
langueSelect.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const langueSelect = document.getElementById('langue_filtre');
|
||||||
|
if (langueSelect) {
|
||||||
|
langueSelect.value = '';
|
||||||
|
|
||||||
|
// Déclencher l'événement change pour que le système de filtrage existant prenne le relais
|
||||||
|
// Support pour Select2 si présent
|
||||||
|
if (window.jQuery && jQuery(langueSelect).hasClass('select2-hidden-accessible')) {
|
||||||
|
jQuery(langueSelect).trigger('change');
|
||||||
|
} else {
|
||||||
|
// Déclencher l'événement change natif
|
||||||
|
langueSelect.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Déclencher un événement personnalisé pour que le calendrier se mette à jour
|
||||||
|
document.dispatchEvent(new CustomEvent('visualFiltersChanged', {
|
||||||
|
detail: filters
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Déclencher le bouton de filtrage du formulaire principal
|
||||||
|
const filterBtn = document.getElementById('filterBtn');
|
||||||
|
if (filterBtn) {
|
||||||
|
filterBtn.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure les écouteurs d'événements globaux
|
||||||
|
*/
|
||||||
|
setupEventListeners() {
|
||||||
|
// Écouter les changements de date pour mettre à jour les capacités
|
||||||
|
const dateInput = document.getElementById('date');
|
||||||
|
if (dateInput) {
|
||||||
|
dateInput.addEventListener('change', () => {
|
||||||
|
// Recharger les capacités de traduction pour cette date
|
||||||
|
// (à implémenter si nécessaire)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Réinitialiser les filtres visuels quand le bouton reset est cliqué
|
||||||
|
const resetBtn = document.getElementById('resetFiltersBtn');
|
||||||
|
if (resetBtn) {
|
||||||
|
resetBtn.addEventListener('click', () => {
|
||||||
|
this.selectedDepartement = null;
|
||||||
|
this.selectedTraduction = null;
|
||||||
|
this.renderDepartementsButtons();
|
||||||
|
this.renderTraductionsButtons();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Écouter les changements de vue/dates de FullCalendar
|
||||||
|
this.setupCalendarListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure les écouteurs pour les changements de vue/dates du calendrier
|
||||||
|
*/
|
||||||
|
setupCalendarListeners() {
|
||||||
|
// Attendre que le calendrier soit disponible
|
||||||
|
const checkCalendar = () => {
|
||||||
|
if (window.currentCalendar) {
|
||||||
|
this.calendarInstance = window.currentCalendar;
|
||||||
|
|
||||||
|
// Écouter l'événement datesSet de FullCalendar (déclenché lors des changements de dates/vue)
|
||||||
|
this.calendarInstance.on('datesSet', (arg) => {
|
||||||
|
this.handleCalendarDatesChange(arg);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ Écouteur datesSet configuré pour les filtres visuels');
|
||||||
|
} else {
|
||||||
|
// Réessayer après un court délai
|
||||||
|
setTimeout(checkCalendar, 100);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkCalendar();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gère les changements de dates/vue du calendrier
|
||||||
|
*/
|
||||||
|
async handleCalendarDatesChange(arg) {
|
||||||
|
const startStr = arg.startStr.split('T')[0];
|
||||||
|
const endStr = arg.endStr.split('T')[0];
|
||||||
|
|
||||||
|
// Vérifier si les dates ont vraiment changé
|
||||||
|
if (this.currentDateDebut === startStr && this.currentDateFin === endStr) {
|
||||||
|
return; // Pas de changement, ne rien faire
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentDateDebut = startStr;
|
||||||
|
this.currentDateFin = endStr;
|
||||||
|
|
||||||
|
console.log('📅 Changement de dates/vue détecté:', { start: startStr, end: endStr, view: arg.view.type });
|
||||||
|
|
||||||
|
// Recharger les capacités de traduction pour la nouvelle période
|
||||||
|
await this.reloadTraductionsCapacites(startStr, endStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recharge les capacités de traduction pour une période donnée
|
||||||
|
*/
|
||||||
|
async reloadTraductionsCapacites(date_debut, date_fin) {
|
||||||
|
try {
|
||||||
|
console.log('🔄 Rechargement des capacités de traduction pour:', { date_debut, date_fin });
|
||||||
|
|
||||||
|
const capacites = await getTraductionsCapacites(date_debut, date_fin);
|
||||||
|
|
||||||
|
if (capacites && typeof capacites === 'object') {
|
||||||
|
this.traductions = capacites;
|
||||||
|
|
||||||
|
// Sauvegarder la sélection actuelle
|
||||||
|
const currentSelection = this.selectedTraduction;
|
||||||
|
|
||||||
|
// Re-rendre les boutons avec les nouvelles données
|
||||||
|
this.renderTraductionsButtons();
|
||||||
|
|
||||||
|
// Restaurer la sélection si elle existe toujours
|
||||||
|
if (currentSelection && this.traductions[currentSelection]) {
|
||||||
|
this.selectedTraduction = currentSelection;
|
||||||
|
const btnId = `trad-${currentSelection}`;
|
||||||
|
const activeBtn = document.getElementById(btnId);
|
||||||
|
if (activeBtn) {
|
||||||
|
activeBtn.classList.add('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ Capacités de traduction mises à jour');
|
||||||
|
} else {
|
||||||
|
console.warn('⚠️ Aucune donnée de capacité reçue');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Erreur lors du rechargement des capacités:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rafraîchit l'affichage des boutons
|
||||||
|
*/
|
||||||
|
refresh() {
|
||||||
|
this.renderDepartementsButtons();
|
||||||
|
this.renderTraductionsButtons();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtient les filtres actifs
|
||||||
|
*/
|
||||||
|
getActiveFilters() {
|
||||||
|
return {
|
||||||
|
departement: this.selectedDepartement,
|
||||||
|
traduction: this.selectedTraduction
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exporter pour utilisation dans d'autres modules
|
||||||
|
export default AgendaVisualFilters;
|
||||||
@ -1,15 +1,15 @@
|
|||||||
{
|
{
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"dev": "vite --mode development",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"watch": "vite build --watch",
|
"watch": "vite build --watch --mode development"
|
||||||
"dev": "vite build --watch --mode development"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fullcalendar/core": "^6.1.18",
|
"@fullcalendar/core": "^6.1.18",
|
||||||
"@fullcalendar/daygrid": "^6.1.18",
|
"@fullcalendar/daygrid": "^6.1.18",
|
||||||
"@fullcalendar/timegrid": "^6.1.18",
|
|
||||||
"@fullcalendar/list": "^6.1.18",
|
|
||||||
"@fullcalendar/interaction": "^6.1.18",
|
"@fullcalendar/interaction": "^6.1.18",
|
||||||
|
"@fullcalendar/list": "^6.1.18",
|
||||||
|
"@fullcalendar/timegrid": "^6.1.18",
|
||||||
"bootstrap": "^5.3.7",
|
"bootstrap": "^5.3.7",
|
||||||
"select2": "^4.0.13",
|
"select2": "^4.0.13",
|
||||||
"toastr": "^2.1.4"
|
"toastr": "^2.1.4"
|
||||||
@ -18,3 +18,4 @@
|
|||||||
"vite": "^7.0.3"
|
"vite": "^7.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,89 +1,70 @@
|
|||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
root: '.',
|
||||||
|
|
||||||
build: {
|
build: {
|
||||||
outDir: 'dist',
|
outDir: 'dist',
|
||||||
|
target: 'es2015',
|
||||||
|
minify: true,
|
||||||
|
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
input: {
|
input: {
|
||||||
crvi_libraries: 'crvi_libraries.js',
|
crvi_libraries: './crvi_libraries.js',
|
||||||
crvi_main: 'crvi_main.js',
|
crvi_main: './crvi_main.js',
|
||||||
|
|
||||||
|
// Entrées CSS (OK avec Vite/Rollup)
|
||||||
crvi_main_css: '../css/crvi_main.css',
|
crvi_main_css: '../css/crvi_main.css',
|
||||||
intervenant_profile_css: '../css/intervenant-profile.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_admin_css: '../css/traduction-langue-admin.css',
|
||||||
traduction_langue_list: 'traduction-langue-list.js',
|
traduction_langue_list_css: '../css/traduction-langue-list.css',
|
||||||
traduction_langue_list_css: '../css/traduction-langue-list.css'
|
|
||||||
|
traduction_langue_admin: './traduction-langue-admin.js',
|
||||||
|
traduction_langue_list: './traduction-langue-list.js'
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Important: en "es", `globals` ne sert pas.
|
||||||
|
// Si tu veux éviter de bundler jquery/toastr/select2 car chargés via <script>:
|
||||||
|
// external: ['jquery', 'toastr', 'select2'],
|
||||||
|
|
||||||
output: {
|
output: {
|
||||||
format: 'es', // Format ES modules
|
format: 'es',
|
||||||
entryFileNames: '[name].min.js',
|
entryFileNames: '[name].min.js',
|
||||||
assetFileNames: (assetInfo) => {
|
|
||||||
// Renommer le CSS extrait automatiquement de crvi_libraries.js
|
|
||||||
if (assetInfo.name && (assetInfo.name.includes('crvi_libraries') && assetInfo.name.endsWith('.css'))) {
|
|
||||||
return 'crvi_libraries.min.css';
|
|
||||||
}
|
|
||||||
// Renommer le CSS des libraries (si entrée séparée)
|
|
||||||
if (assetInfo.name && assetInfo.name.includes('crvi_libraries_css')) {
|
|
||||||
return 'crvi_libraries.min.css';
|
|
||||||
}
|
|
||||||
// Renommer le fichier CSS principal en crvi_main.min.css
|
|
||||||
if (assetInfo.name && assetInfo.name.includes('crvi_main_css')) {
|
|
||||||
return 'crvi_main.min.css';
|
|
||||||
}
|
|
||||||
// Renommer le CSS du profil intervenant
|
|
||||||
if (assetInfo.name && assetInfo.name.includes('intervenant_profile_css')) {
|
|
||||||
return 'intervenant-profile.min.css';
|
|
||||||
}
|
|
||||||
// Renommer le CSS de traduction-langue admin
|
|
||||||
if (assetInfo.name && assetInfo.name.includes('traduction_langue_admin_css')) {
|
|
||||||
return 'traduction-langue-admin.min.css';
|
|
||||||
}
|
|
||||||
// Renommer le CSS de traduction-langue list
|
|
||||||
if (assetInfo.name && assetInfo.name.includes('traduction_langue_list_css')) {
|
|
||||||
return 'traduction-langue-list.min.css';
|
|
||||||
}
|
|
||||||
// Pour les autres assets CSS, utiliser le pattern par défaut
|
|
||||||
if (assetInfo.name && assetInfo.name.endsWith('.css')) {
|
|
||||||
return '[name].min.css';
|
|
||||||
}
|
|
||||||
return '[name].min.[ext]';
|
|
||||||
},
|
|
||||||
// Séparer les librairies externes des modules locaux
|
|
||||||
manualChunks: (id) => {
|
|
||||||
// Si c'est le fichier crvi_libraries.js, tout va dans crvi_libraries
|
|
||||||
if (id.includes('crvi_libraries.js')) {
|
|
||||||
return 'crvi_libraries';
|
|
||||||
}
|
|
||||||
// Détecter les node_modules (librairies externes)
|
|
||||||
if (id.includes('node_modules')) {
|
|
||||||
// Séparer Bootstrap, Toastr, FullCalendar et leurs dépendances dans le chunk libraries
|
|
||||||
if (id.includes('bootstrap') ||
|
|
||||||
id.includes('toastr') ||
|
|
||||||
id.includes('select2') ||
|
|
||||||
id.includes('@fullcalendar') ||
|
|
||||||
id.includes('@popperjs') ||
|
|
||||||
id.includes('jquery')) {
|
|
||||||
return 'crvi_libraries';
|
|
||||||
}
|
|
||||||
// Autres node_modules - inclure dans libraries aussi
|
|
||||||
return 'crvi_libraries';
|
|
||||||
}
|
|
||||||
// Les modules locaux restent dans crvi_main
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
chunkFileNames: '[name].min.js',
|
chunkFileNames: '[name].min.js',
|
||||||
globals: {
|
|
||||||
'jquery': 'jQuery',
|
assetFileNames: (assetInfo) => {
|
||||||
'toastr': 'toastr',
|
const name = assetInfo.name || ''
|
||||||
'select2': 'select2'
|
|
||||||
|
// CSS extrait depuis une entry JS (ex: crvi_libraries.js -> crvi_libraries.css)
|
||||||
|
if (name.includes('crvi_libraries') && name.endsWith('.css')) {
|
||||||
|
return 'crvi_libraries.min.css'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Entrées CSS séparées
|
||||||
|
if (name.includes('crvi_main_css')) return 'crvi_main.min.css'
|
||||||
|
if (name.includes('intervenant_profile_css')) return 'intervenant-profile.min.css'
|
||||||
|
if (name.includes('traduction_langue_admin_css')) return 'traduction-langue-admin.min.css'
|
||||||
|
if (name.includes('traduction_langue_list_css')) return 'traduction-langue-list.min.css'
|
||||||
|
|
||||||
|
// Autres CSS / assets
|
||||||
|
if (name.endsWith('.css')) return '[name].min.css'
|
||||||
|
return '[name].min.[ext]'
|
||||||
|
},
|
||||||
|
|
||||||
|
manualChunks: (id) => {
|
||||||
|
// Node_modules -> crvi_libraries (bundle commun)
|
||||||
|
if (id.includes('node_modules')) return 'crvi_libraries'
|
||||||
|
|
||||||
|
// Optionnel: forcer aussi certains fichiers locaux dans crvi_libraries
|
||||||
|
if (id.includes('crvi_libraries.js')) return 'crvi_libraries'
|
||||||
|
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
// Forcer la transpilation pour compatibilité navigateur
|
|
||||||
target: 'es2015',
|
|
||||||
minify: true // Utilise esbuild par défaut (plus rapide que terser)
|
|
||||||
},
|
},
|
||||||
|
|
||||||
define: {
|
define: {
|
||||||
global: 'window'
|
global: 'window'
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
|
|
||||||
<section class="agenda-container">
|
<section class="agenda-container">
|
||||||
|
|
||||||
<!--filters-->
|
<!--filters-->
|
||||||
<section class="filters-container">
|
<section class="filters-container">
|
||||||
<form class="filters" method="get" onsubmit="return false;">
|
<form class="filters" method="get" onsubmit="return false;">
|
||||||
@ -39,13 +40,29 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="filter">
|
<div class="filter">
|
||||||
<label for="type_intervention">Type d'intervention</label>
|
<label for="departement">Département</label>
|
||||||
<select id="type_intervention" name="type_intervention" class="select2">
|
<select id="departement" name="departement" class="select2">
|
||||||
<option value="">Tous</option>
|
<option value="">Tous</option>
|
||||||
<!-- Options dynamiques -->
|
<!-- Options dynamiques -->
|
||||||
<?php
|
<?php
|
||||||
foreach ($types_intervention as $type_intervention) {
|
foreach ($departements as $departement) {
|
||||||
echo '<option value="' . $type_intervention['id'] . '">' . $type_intervention['nom'] . '</option>';
|
echo '<option value="' . $departement['id'] . '">' . $departement['nom'] . '</option>';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="filter" style="display: none;">
|
||||||
|
<label for="type_intervention">Type d'intervention</label>
|
||||||
|
<select id="type_intervention" name="type_intervention" class="select2">
|
||||||
|
<option value="">Tous</option>
|
||||||
|
<!-- Options dynamiques groupées par département -->
|
||||||
|
<?php
|
||||||
|
foreach ($types_intervention_groupes as $departement_id => $departement_data) {
|
||||||
|
echo '<optgroup label="' . esc_attr($departement_data['nom']) . '">';
|
||||||
|
foreach ($departement_data['types'] as $type_intervention) {
|
||||||
|
echo '<option value="' . esc_attr($type_intervention['id']) . '">' . esc_html($type_intervention['nom']) . '</option>';
|
||||||
|
}
|
||||||
|
echo '</optgroup>';
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
</select>
|
</select>
|
||||||
@ -63,8 +80,8 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="filter">
|
<div class="filter">
|
||||||
<label for="langue">Langue du rendez-vous</label>
|
<label for="langue_filtre">Langue du rendez-vous</label>
|
||||||
<select id="langue" name="langue" class="select2">
|
<select id="langue_filtre" name="langue" class="select2">
|
||||||
<option value="">Toutes</option>
|
<option value="">Toutes</option>
|
||||||
<!-- Options dynamiques -->
|
<!-- Options dynamiques -->
|
||||||
<?php
|
<?php
|
||||||
@ -110,6 +127,43 @@
|
|||||||
</form>
|
</form>
|
||||||
</section>
|
</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-->
|
<!--agenda-->
|
||||||
<section class="agenda-inner-container" id="agenda-calendar">
|
<section class="agenda-inner-container" id="agenda-calendar">
|
||||||
<div id="loading-indicator" style="text-align: center; padding: 20px;">
|
<div id="loading-indicator" style="text-align: center; padding: 20px;">
|
||||||
@ -124,4 +178,29 @@
|
|||||||
require_once dirname(__DIR__, 1) . '/modules/agenda-modal.php';
|
require_once dirname(__DIR__, 1) . '/modules/agenda-modal.php';
|
||||||
?>
|
?>
|
||||||
</section>
|
</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>
|
</div>
|
||||||
@ -81,11 +81,16 @@
|
|||||||
<label for="stats_annee" style="display: block; margin-bottom: 5px; font-weight: 500;"><?php esc_html_e('Année', 'esi_crvi_agenda'); ?></label>
|
<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%;">
|
<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>
|
||||||
<div class="filter" style="display: flex; flex-direction: column; gap: 10px;">
|
<div class="filter">
|
||||||
<label for="stats_filtre_permanence" style="display: flex; align-items: center; gap: 8px; cursor: pointer; margin-top: 25px;">
|
<label for="stats_statut" style="display: block; margin-bottom: 5px; font-weight: 500;"><?php esc_html_e('Statut', 'esi_crvi_agenda'); ?></label>
|
||||||
<input type="checkbox" id="stats_filtre_permanence" name="filtre_permanence" value="permanence">
|
<select id="stats_statut" name="statut" class="select2 form-control" style="width: 100%;">
|
||||||
<span><?php esc_html_e('Afficher uniquement les permanences', 'esi_crvi_agenda'); ?></span>
|
<option value=""><?php esc_html_e('Tous', 'esi_crvi_agenda'); ?></option>
|
||||||
</label>
|
<option value="prevu"><?php esc_html_e('Prévu', 'esi_crvi_agenda'); ?></option>
|
||||||
|
<option value="cloture"><?php esc_html_e('Clôturé', 'esi_crvi_agenda'); ?></option>
|
||||||
|
<option value="absence"><?php esc_html_e('Absence', 'esi_crvi_agenda'); ?></option>
|
||||||
|
<option value="annule"><?php esc_html_e('Annulé', 'esi_crvi_agenda'); ?></option>
|
||||||
|
<option value="non_tenu"><?php esc_html_e('Non tenu', 'esi_crvi_agenda'); ?></option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="filter" style="display: flex; gap: 10px; justify-content: flex-end; align-items: end;">
|
<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;">
|
<button type="button" id="stats_filterBtn" class="btn btn-primary" style="min-width: 100px;">
|
||||||
|
|||||||
@ -281,23 +281,34 @@ if (!is_wp_error($langues_terms) && !empty($langues_terms)) {
|
|||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="mb-3">
|
<?php if (!empty($langues)): ?>
|
||||||
<label for="langues-permanences" class="form-label fw-bold">Langues disponibles (optionnel) :</label>
|
<div class="mb-3">
|
||||||
<select class="form-select form-select-lg" id="langues-permanences" name="langues[]" multiple>
|
<label for="langues-permanences" class="form-label fw-bold">Langues disponibles (optionnel) :</label>
|
||||||
<?php if (!empty($langues)): ?>
|
<select class="form-select form-select-lg" id="langues-permanences" name="langues[]" multiple>
|
||||||
<?php foreach ($langues as $langue): ?>
|
<?php foreach ($langues as $langue): ?>
|
||||||
<option value="<?php echo esc_attr($langue['id']); ?>">
|
<option value="<?php echo esc_attr($langue['id']); ?>">
|
||||||
<?php echo esc_html($langue['nom']); ?>
|
<?php echo esc_html($langue['nom']); ?>
|
||||||
</option>
|
</option>
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<?php else: ?>
|
</select>
|
||||||
<option value="">Aucune langue disponible</option>
|
<small class="form-text text-muted">
|
||||||
<?php endif; ?>
|
Sélectionnez une ou plusieurs langues pour ces permanences. Maintenez Ctrl (ou Cmd sur Mac) pour sélectionner plusieurs langues.
|
||||||
</select>
|
</small>
|
||||||
<small class="form-text text-muted">
|
</div>
|
||||||
Sélectionnez une ou plusieurs langues pour ces permanences.
|
<?php else: ?>
|
||||||
</small>
|
<div class="alert alert-warning" role="alert">
|
||||||
</div>
|
<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; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
223
templates/email/conflict-notification.php
Normal file
223
templates/email/conflict-notification.php
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Template email : Notification de conflit de disponibilité
|
||||||
|
*
|
||||||
|
* Variables disponibles :
|
||||||
|
* @var WP_User $user L'utilisateur (intervenant) concerné
|
||||||
|
* @var array $conflicts Les conflits détectés
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Alerte : Conflit de disponibilité</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
.email-container {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 30px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
background-color: #dc3545;
|
||||||
|
color: #ffffff;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
.header h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
.intervenant-info {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
.intervenant-info h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: #333;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
.intervenant-info p {
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
.conflict-section {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
border-left: 4px solid #dc3545;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
.conflict-section h3 {
|
||||||
|
color: #dc3545;
|
||||||
|
margin-top: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
.conflict-period {
|
||||||
|
background-color: #fff3cd;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
border-left: 3px solid #ffc107;
|
||||||
|
}
|
||||||
|
.event-item {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 12px;
|
||||||
|
margin: 10px 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
border-left: 3px solid #007bff;
|
||||||
|
}
|
||||||
|
.event-item strong {
|
||||||
|
color: #007bff;
|
||||||
|
}
|
||||||
|
.event-details {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.event-details span {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
.beneficiaires-list {
|
||||||
|
margin-top: 5px;
|
||||||
|
padding-left: 0;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
margin-top: 30px;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid #dee2e6;
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.footer ul {
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
.footer li {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="email-container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>⚠️ Alerte : Conflit de disponibilité détecté</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="intervenant-info">
|
||||||
|
<h2>Intervenant concerné</h2>
|
||||||
|
<p><strong>Nom :</strong> <?php echo esc_html($user->display_name); ?></p>
|
||||||
|
<p><strong>Email :</strong> <?php echo esc_html($user->user_email); ?></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="font-size: 16px; margin-bottom: 25px;">
|
||||||
|
Des événements sont planifiés pendant les périodes d'indisponibilité de cet intervenant.
|
||||||
|
Veuillez vérifier et prendre les mesures nécessaires.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<?php foreach ($conflicts as $key => $conflict): ?>
|
||||||
|
<div class="conflict-section">
|
||||||
|
<?php if ($key === 'jours_indisponibles'): ?>
|
||||||
|
<!-- Conflit de jours indisponibles -->
|
||||||
|
<h3>Jours non disponibles</h3>
|
||||||
|
<p><strong><?php echo count($conflict['events']); ?> événement(s)</strong> planifié(s) sur des jours où l'intervenant n'est pas disponible.</p>
|
||||||
|
|
||||||
|
<?php foreach ($conflict['events'] as $event): ?>
|
||||||
|
<?php
|
||||||
|
$enriched = $event['enriched'] ?? null;
|
||||||
|
if ($enriched):
|
||||||
|
$date_formatted = date_i18n('d/m/Y', strtotime($enriched['date_rdv']));
|
||||||
|
$day_name = date_i18n('l', strtotime($enriched['date_rdv']));
|
||||||
|
?>
|
||||||
|
<div class="event-item">
|
||||||
|
<strong><?php echo esc_html($date_formatted); ?> (<?php echo esc_html($day_name); ?>) à <?php echo esc_html(substr($enriched['heure_rdv'], 0, 5)); ?></strong>
|
||||||
|
<div class="event-details">
|
||||||
|
<span><strong>Type :</strong> <?php echo esc_html(ucfirst($enriched['type_rdv'])); ?></span>
|
||||||
|
<span><strong>Département :</strong> <?php echo esc_html($enriched['departement']); ?></span>
|
||||||
|
<span><strong>Type intervention :</strong> <?php echo esc_html($enriched['type_intervention']); ?></span>
|
||||||
|
|
||||||
|
<?php if (!empty($enriched['beneficiaires'])): ?>
|
||||||
|
<div class="beneficiaires-list">
|
||||||
|
<strong>Bénéficiaire(s) :</strong> <?php echo esc_html(implode(', ', $enriched['beneficiaires'])); ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<span style="color: #999;"><strong>ID :</strong> <?php echo esc_html($enriched['id']); ?></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
|
||||||
|
<?php else: ?>
|
||||||
|
<!-- Conflit de période d'indisponibilité -->
|
||||||
|
<?php
|
||||||
|
$periode = $conflict['periode'];
|
||||||
|
$debut_formatted = date_i18n('d/m/Y', strtotime($periode['debut']));
|
||||||
|
$fin_formatted = date_i18n('d/m/Y', strtotime($periode['fin']));
|
||||||
|
?>
|
||||||
|
|
||||||
|
<h3><?php echo esc_html($periode['type']); ?></h3>
|
||||||
|
<div class="conflict-period">
|
||||||
|
<strong>Période :</strong> du <?php echo esc_html($debut_formatted); ?> au <?php echo esc_html($fin_formatted); ?>
|
||||||
|
</div>
|
||||||
|
<p><strong><?php echo count($conflict['events']); ?> événement(s)</strong> planifié(s) pendant cette période.</p>
|
||||||
|
|
||||||
|
<?php foreach ($conflict['events'] as $event): ?>
|
||||||
|
<?php
|
||||||
|
$enriched = $event['enriched'] ?? null;
|
||||||
|
if ($enriched):
|
||||||
|
$date_formatted = date_i18n('d/m/Y', strtotime($enriched['date_rdv']));
|
||||||
|
?>
|
||||||
|
<div class="event-item">
|
||||||
|
<strong><?php echo esc_html($date_formatted); ?> à <?php echo esc_html(substr($enriched['heure_rdv'], 0, 5)); ?></strong>
|
||||||
|
<div class="event-details">
|
||||||
|
<span><strong>Type :</strong> <?php echo esc_html(ucfirst($enriched['type_rdv'])); ?></span>
|
||||||
|
<span><strong>Département :</strong> <?php echo esc_html($enriched['departement']); ?></span>
|
||||||
|
<span><strong>Type intervention :</strong> <?php echo esc_html($enriched['type_intervention']); ?></span>
|
||||||
|
|
||||||
|
<?php if (!empty($enriched['beneficiaires'])): ?>
|
||||||
|
<div class="beneficiaires-list">
|
||||||
|
<strong>Bénéficiaire(s) :</strong> <?php echo esc_html(implode(', ', $enriched['beneficiaires'])); ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<span style="color: #999;"><strong>ID :</strong> <?php echo esc_html($enriched['id']); ?></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<p><strong>Actions recommandées :</strong></p>
|
||||||
|
<ul>
|
||||||
|
<li>Vérifier les événements listés ci-dessus</li>
|
||||||
|
<li>Contacter l'intervenant pour confirmer sa disponibilité</li>
|
||||||
|
<li>Annuler ou déplacer les événements en conflit si nécessaire</li>
|
||||||
|
<li>Ajuster les périodes d'indisponibilité si besoin</li>
|
||||||
|
</ul>
|
||||||
|
<p style="margin-top: 20px; color: #999;">
|
||||||
|
Cet email a été envoyé automatiquement par le système Agenda CRVI.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -18,7 +18,7 @@ $user = wp_get_current_user();
|
|||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<h1 class="h2 mb-2">
|
<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>
|
</h1>
|
||||||
<nav class="navbar navbar-expand-lg navbar-light bg-light rounded p-3">
|
<nav class="navbar navbar-expand-lg navbar-light bg-light rounded p-3">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
@ -96,14 +96,20 @@ $user = wp_get_current_user();
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="filter">
|
<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>
|
<label for="type_intervention">Type d'intervention</label>
|
||||||
<select id="type_intervention" name="type_intervention" class="select2">
|
<select id="type_intervention" name="type_intervention" class="select2">
|
||||||
<option value="">Tous</option>
|
<option value="">Tous</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="filter">
|
<div class="filter">
|
||||||
<label for="langue">Langue</label>
|
<label for="langue_filtre">Langue</label>
|
||||||
<select id="langue" name="langue" class="select2">
|
<select id="langue_filtre" name="langue" class="select2">
|
||||||
<option value="">Toutes</option>
|
<option value="">Toutes</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@ -167,6 +173,12 @@ $user = wp_get_current_user();
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="filter">
|
<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>
|
<label for="type_intervention-colleagues">Type d'intervention</label>
|
||||||
<select id="type_intervention-colleagues" name="type_intervention" class="select2">
|
<select id="type_intervention-colleagues" name="type_intervention" class="select2">
|
||||||
<option value="">Tous</option>
|
<option value="">Tous</option>
|
||||||
|
|||||||
@ -18,7 +18,7 @@ $user = wp_get_current_user();
|
|||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<h1 class="h2 mb-2">
|
<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>
|
</h1>
|
||||||
<nav class="navbar navbar-expand-lg navbar-light bg-light rounded p-3">
|
<nav class="navbar navbar-expand-lg navbar-light bg-light rounded p-3">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
@ -96,14 +96,20 @@ $user = wp_get_current_user();
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="filter">
|
<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>
|
<label for="type_intervention">Type d'intervention</label>
|
||||||
<select id="type_intervention" name="type_intervention" class="select2">
|
<select id="type_intervention" name="type_intervention" class="select2">
|
||||||
<option value="">Tous</option>
|
<option value="">Tous</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="filter">
|
<div class="filter">
|
||||||
<label for="langue">Langue</label>
|
<label for="langue_filtre">Langue</label>
|
||||||
<select id="langue" name="langue" class="select2">
|
<select id="langue_filtre" name="langue" class="select2">
|
||||||
<option value="">Toutes</option>
|
<option value="">Toutes</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@ -167,6 +173,12 @@ $user = wp_get_current_user();
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="filter">
|
<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>
|
<label for="type_intervention-colleagues">Type d'intervention</label>
|
||||||
<select id="type_intervention-colleagues" name="type_intervention" class="select2">
|
<select id="type_intervention-colleagues" name="type_intervention" class="select2">
|
||||||
<option value="">Tous</option>
|
<option value="">Tous</option>
|
||||||
|
|||||||
@ -20,7 +20,7 @@ $today = date('d/m/Y');
|
|||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<h1 class="h2 mb-2">
|
<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>
|
</h1>
|
||||||
<p class="text-muted">Bonjour <?php echo esc_html($intervenant_nom); ?></p>
|
<p class="text-muted">Bonjour <?php echo esc_html($intervenant_nom); ?></p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -34,7 +34,7 @@ if (!is_wp_error($langues_terms) && !empty($langues_terms)) {
|
|||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<h1 class="h2 mb-2">
|
<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>
|
</h1>
|
||||||
<nav class="navbar navbar-expand-lg navbar-light bg-light rounded p-3">
|
<nav class="navbar navbar-expand-lg navbar-light bg-light rounded p-3">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
|
|||||||
@ -80,7 +80,7 @@ $jours_labels = [
|
|||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<h1 class="h2 mb-2">
|
<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>
|
</h1>
|
||||||
<nav class="navbar navbar-expand-lg navbar-light bg-light rounded p-3">
|
<nav class="navbar navbar-expand-lg navbar-light bg-light rounded p-3">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
|
|||||||
@ -14,6 +14,11 @@ $modals_dir = dirname(__FILE__) . '/modals';
|
|||||||
include $modals_dir . '/event-modal.php';
|
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
|
<?php
|
||||||
// Modal de création de bénéficiaire
|
// Modal de création de bénéficiaire
|
||||||
include $modals_dir . '/create-beneficiaire-modal.php';
|
include $modals_dir . '/create-beneficiaire-modal.php';
|
||||||
|
|||||||
@ -15,6 +15,12 @@ $crvi_is_front_context = ($crvi_agenda_context !== 'admin');
|
|||||||
<button type="button" class="btn-close" id="closeModalBtn" aria-label="Fermer"></button>
|
<button type="button" class="btn-close" id="closeModalBtn" aria-label="Fermer"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<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 -->
|
<!-- Bloc lecture seule (vue) avec grille Bootstrap en 2 colonnes -->
|
||||||
<div id="eventViewBlock">
|
<div id="eventViewBlock">
|
||||||
<div class="event-grid">
|
<div class="event-grid">
|
||||||
@ -220,10 +226,10 @@ $crvi_is_front_context = ($crvi_agenda_context !== 'admin');
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6" id="langue-container">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="langue" class="form-label">Langue *</label>
|
<label for="langue" class="form-label">Langue *</label>
|
||||||
<select class="form-select" id="langue" name="langue" required>
|
<select class="form-select skip-select2" id="langue" name="langue" required>
|
||||||
<option value="">Sélectionner...</option>
|
<option value="">Sélectionner...</option>
|
||||||
<?php
|
<?php
|
||||||
if (isset($langues_beneficiaire) && is_array($langues_beneficiaire)) {
|
if (isset($langues_beneficiaire) && is_array($langues_beneficiaire)) {
|
||||||
@ -298,6 +304,11 @@ $crvi_is_front_context = ($crvi_agenda_context !== 'admin');
|
|||||||
<i class="fas fa-exclamation-triangle text-danger me-1"></i>Ajouter bénéficiaire à la liste rouge
|
<i class="fas fa-exclamation-triangle text-danger me-1"></i>Ajouter bénéficiaire à la liste rouge
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
@ -468,10 +479,13 @@ $crvi_is_front_context = ($crvi_agenda_context !== 'admin');
|
|||||||
<div id="eventFormErrors" class="alert alert-danger d-none" role="alert"></div>
|
<div id="eventFormErrors" class="alert alert-danger d-none" role="alert"></div>
|
||||||
|
|
||||||
<!-- Boutons de changement de statut rapide (mode édition uniquement) -->
|
<!-- Boutons de changement de statut rapide (mode édition uniquement) -->
|
||||||
<div id="eventStatusButtons" style="display: none;">
|
<div id="eventStatusButtons">
|
||||||
<div class="btn-group" role="group">
|
<div class="btn-group" role="group">
|
||||||
<button type="button" class="btn btn-success btn-sm" id="markPresentBtn" title="Valider la présence">
|
<button type="button" class="btn btn-success btn-sm individuel-only-button" id="markPresentBtn" title="Valider la présence" style="display: none;">
|
||||||
<i class="fas fa-user-check me-1"></i>Valider présence
|
<i class="fas fa-user-check me-1"></i>Valider présences
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-primary btn-sm groupe-only-button" id="showPresenceModalBtn" title="Afficher le modal de présence" style="display: none;">
|
||||||
|
<i class="fas fa-clipboard-list"></i> Gérer les présences
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-warning btn-sm" id="markAbsentBtn" title="Marquer comme absent">
|
<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
|
<i class="fas fa-user-times me-1"></i>Absent
|
||||||
@ -547,12 +561,20 @@ jQuery(document).ready(function($){
|
|||||||
// Masquer le champ bénéficiaire pour les RDV de groupe
|
// Masquer le champ bénéficiaire pour les RDV de groupe
|
||||||
$('#id_beneficiaire').closest('.col-md-6').hide();
|
$('#id_beneficiaire').closest('.col-md-6').hide();
|
||||||
$('#id_beneficiaire').removeAttr('required');
|
$('#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 {
|
} else {
|
||||||
$('#groupeFields').hide();
|
$('#groupeFields').hide();
|
||||||
$('#nb_participants').removeAttr('required');
|
$('#nb_participants').removeAttr('required');
|
||||||
// Afficher le champ bénéficiaire pour les RDV individuels
|
// Afficher le champ bénéficiaire pour les RDV individuels
|
||||||
$('#id_beneficiaire').closest('.col-md-6').show();
|
$('#id_beneficiaire').closest('.col-md-6').show();
|
||||||
$('#id_beneficiaire').attr('required', true);
|
$('#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