'GET', 'callback' => [self::class, 'get_user_permissions'], 'permission_callback' => '__return_true', ], ]); register_rest_route('crvi/v1', '/agenda/disponibilites', [ [ 'methods' => 'GET', 'callback' => [self::class, 'get_disponibilites'], 'permission_callback' => '__return_true', ], [ 'methods' => 'POST', 'callback' => [self::class, 'get_disponibilites'], 'permission_callback' => '__return_true', ], ]); // Endpoint alternatif pour les filtres de disponibilités register_rest_route('crvi/v1', '/filters/disponibilites', [ [ 'methods' => 'GET', 'callback' => [self::class, 'get_disponibilites'], 'permission_callback' => '__return_true', ], [ 'methods' => 'POST', 'callback' => [self::class, 'get_disponibilites'], 'permission_callback' => '__return_true', ], ]); \register_rest_route('crvi/v1', '/events/(?P\\d+)/conflits', [ [ 'methods' => 'GET', 'callback' => [self::class, 'conflits_item'], 'permission_callback' => [self::class, 'can_edit'], ], ]); \register_rest_route('crvi/v1', '/events/export', [ [ 'methods' => 'GET', 'callback' => [self::class, 'export_items'], 'permission_callback' => [self::class, 'can_edit'], ], ]); // --- Filtres dynamiques --- \register_rest_route('crvi/v1', '/filters/departements', [ 'methods' => 'GET', 'callback' => [self::class, 'get_departements'], 'permission_callback' => '__return_true', ]); \register_rest_route('crvi/v1', '/filters/types-intervention', [ 'methods' => 'GET', 'callback' => [self::class, 'get_types_intervention'], 'permission_callback' => '__return_true', ]); \register_rest_route('crvi/v1', '/filters/langues', [ 'methods' => 'GET', 'callback' => [self::class, 'get_langues'], 'permission_callback' => '__return_true', ]); \register_rest_route('crvi/v1', '/filters/langues-beneficiaire', [ 'methods' => 'GET', 'callback' => [self::class, 'get_langues_beneficiaire'], 'permission_callback' => '__return_true', ]); \register_rest_route('crvi/v1', '/filters/statuts', [ 'methods' => 'GET', 'callback' => [self::class, 'get_statuts'], 'permission_callback' => '__return_true', ]); // --- Historique bénéficiaire --- \register_rest_route('crvi/v1', '/beneficiaires/(?P\\d+)/historique', [ [ 'methods' => 'GET', 'callback' => [self::class, 'get_beneficiaire_historique'], 'permission_callback' => '__return_true', ], ]); // --- CRUD Events --- \register_rest_route('crvi/v1', '/events', [ [ 'methods' => 'GET', 'callback' => [self::class, 'get_events'], 'permission_callback' => '__return_true', ], [ 'methods' => 'POST', 'callback' => [self::class, 'create_event'], 'permission_callback' => [self::class, 'can_edit'], ], ]); // Endpoint pour le tableau de stats avec pagination \register_rest_route('crvi/v1', '/events/table', [ [ 'methods' => 'GET', 'callback' => [self::class, 'get_events_table'], 'permission_callback' => '__return_true', ], ]); // --- Validation des présences pour les rendez-vous de groupe --- \register_rest_route('crvi/v1', '/events/(?P\\d+)/presences', [ [ 'methods' => 'GET', 'callback' => [self::class, 'get_event_presences'], 'permission_callback' => '__return_true', ], [ 'methods' => 'POST', 'callback' => [self::class, 'save_group_presences'], 'permission_callback' => [self::class, 'can_edit'], ], ]); // Création de permanences (intervenant) \register_rest_route('crvi/v1', '/intervenant/permanences', [ [ 'methods' => 'POST', 'callback' => [self::class, 'create_permanences'], 'permission_callback' => [\ESI_CRVI_AGENDA\controllers\Intervenant_Space_Controller::class, 'check_intervenant_permission'], ], ]); // Import CSV de permanences (intervenant) \register_rest_route('crvi/v1', '/intervenant/permanences/import-csv', [ [ 'methods' => 'POST', 'callback' => [self::class, 'import_permanences_csv_intervenant'], 'permission_callback' => [\ESI_CRVI_AGENDA\controllers\Intervenant_Space_Controller::class, 'check_intervenant_permission'], ], ]); // Création de permanences (admin) \register_rest_route('crvi/v1', '/admin/permanences', [ [ 'methods' => 'POST', 'callback' => [self::class, 'create_permanences_admin'], 'permission_callback' => [\ESI_CRVI_AGENDA\controllers\Intervenant_Space_Controller::class, 'check_admin_permission'], ], ]); // Import CSV de permanences (admin) \register_rest_route('crvi/v1', '/admin/permanences/import-csv', [ [ 'methods' => 'POST', 'callback' => [self::class, 'import_permanences_csv'], 'permission_callback' => [\ESI_CRVI_AGENDA\controllers\Intervenant_Space_Controller::class, 'check_admin_permission'], ], ]); \register_rest_route('crvi/v1', '/events/(?P\\d+)', [ [ 'methods' => 'GET', 'callback' => [self::class, 'get_event'], 'permission_callback' => '__return_true', ], [ 'methods' => 'PUT,PATCH', 'callback' => [self::class, 'update_event'], 'permission_callback' => [self::class, 'can_edit'], ], [ 'methods' => 'DELETE', 'callback' => [self::class, 'delete_event'], 'permission_callback' => [self::class, 'can_edit'], ], ]); // Endpoints pour la gestion des événements supprimés \register_rest_route('crvi/v1', '/events/deleted', [ [ 'methods' => 'GET', 'callback' => [self::class, 'get_deleted_events'], 'permission_callback' => [self::class, 'can_edit'], ], ]); \register_rest_route('crvi/v1', '/events/(?P\\d+)/restore', [ [ 'methods' => 'POST', 'callback' => [self::class, 'restore_event'], 'permission_callback' => [self::class, 'can_edit'], ], ]); \register_rest_route('crvi/v1', '/events/(?P\\d+)/hard-delete', [ [ 'methods' => 'DELETE', 'callback' => [self::class, 'hard_delete_event'], 'permission_callback' => [self::class, 'can_edit'], ], ]); // Route pour changer le statut d'un événement \register_rest_route('crvi/v1', '/events/(?P\\d+)/statut', [ [ 'methods' => 'PUT', 'callback' => [self::class, 'change_statut'], 'permission_callback' => [self::class, 'can_edit'], ], ]); } /** * Endpoint global pour récupérer les entités disponibles à une date/créneau donné. * @param \WP_REST_Request $request * @return \WP_REST_Response */ public static function get_disponibilites($request) { // Récupérer les paramètres depuis query string ou JSON body $params = $request->get_params(); $json_params = $request->get_json_params() ?: []; // Fusionner les paramètres (JSON body a priorité sur query string) $all_params = array_merge($params, $json_params); // DEBUG: Afficher les paramètres reçus // Vérifier si on demande les dates indisponibles $id_intervenant = $all_params['id_intervenant'] ?? null; $id_traducteur = $all_params['id_traducteur'] ?? null; $id_local = $all_params['id_local'] ?? null; $date_debut = $all_params['date_debut'] ?? null; $date_fin = $all_params['date_fin'] ?? null; // Si on a tous les paramètres pour les dates indisponibles if ($id_intervenant && $id_traducteur && $id_local && $date_debut && $date_fin) { return self::get_available_dates($id_intervenant, $id_traducteur, $id_local, $date_debut, $date_fin); } // Sinon, logique existante pour les entités disponibles $date = $all_params['date'] ?? $all_params['date_rdv'] ?? null; $heure = $all_params['heure'] ?? $all_params['heure_rdv'] ?? null; $langue = $all_params['langue'] ?? null; $departement = $all_params['departement'] ?? null; $specialisation = $all_params['specialisation'] ?? null; $type = $all_params['type'] ?? null; // Normaliser event_id (convertir string en int si nécessaire) $event_id = isset($all_params['event_id']) ? (int) $all_params['event_id'] : null; if ($event_id === 0) { $event_id = null; } // Normaliser id_traducteur (convertir string en int, "0" signifie pas de traducteur) $id_traducteur_param = isset($all_params['id_traducteur']) ? (int) $all_params['id_traducteur'] : null; if ($id_traducteur_param === 0) { $id_traducteur_param = null; } // Normaliser id_intervenant et id_local $id_intervenant_param = isset($all_params['id_intervenant']) ? (int) $all_params['id_intervenant'] : null; $id_local_param = isset($all_params['id_local']) ? (int) $all_params['id_local'] : null; // Appel aux méthodes de chaque controller // Ne vérifier les traducteurs que s'il y en a un d'assigné à l'événement (id_traducteur > 0) OU si une langue est spécifiée $traducteurs = []; // Vérifier que id_traducteur est valide (supérieur à 0, pas null, pas 0) $hasValidTraducteurId = $id_traducteur_param !== null && ($id_traducteur_param > 0 || $id_traducteur_param != '0'); if ($hasValidTraducteurId || !empty($langue)) { try { $traducteurs_result = \ESI_CRVI_AGENDA\models\CRVI_Traducteur_Model::filtrer_disponibles($date, $langue, $event_id); $traducteurs = is_array($traducteurs_result) ? $traducteurs_result : []; } catch (\Exception $e) { error_log('[CRVI] Erreur lors de la récupération des traducteurs disponibles: ' . $e->getMessage()); $traducteurs = []; } } // Récupérer les intervenants disponibles $intervenants = []; try { $intervenants_result = \ESI_CRVI_AGENDA\models\CRVI_Intervenant_Model::filtrer_disponibles($date, $departement, $specialisation, $event_id); $intervenants = is_array($intervenants_result) ? $intervenants_result : []; } catch (\Exception $e) { error_log('[CRVI] Erreur lors de la récupération des intervenants disponibles: ' . $e->getMessage()); $intervenants = []; } // Formater les intervenants pour le JavaScript $intervenants_formatted = []; foreach ($intervenants as $intervenant) { // Vérifier que l'intervenant est un objet valide if (!is_object($intervenant) || !isset($intervenant->id)) { continue; } $intervenants_formatted[] = [ 'id' => $intervenant->id, 'nom' => ($intervenant->nom ?? '') . ' ' . ($intervenant->prenom ?? ''), ]; } // Formater les traducteurs pour le JavaScript $traducteurs_formatted = []; foreach ($traducteurs as $traducteur) { // Vérifier que le traducteur est un objet valide if (!is_object($traducteur) || !isset($traducteur->id)) { continue; } $traducteurs_formatted[] = [ 'id' => $traducteur->id, 'nom' => ($traducteur->nom ?? '') . ' ' . ($traducteur->prenom ?? ''), ]; } // Récupérer les locaux disponibles selon le type de RDV et la disponibilité $type_rdv = $all_params['type_rdv'] ?? null; $locals_posts = []; try { $locals_result = \ESI_CRVI_AGENDA\models\CRVI_Local_Model::all(); $locals_posts = is_array($locals_result) ? $locals_result : []; } catch (\Exception $e) { error_log('[CRVI] Erreur lors de la récupération des locaux: ' . $e->getMessage()); $locals_posts = []; } $locaux = []; foreach ($locals_posts as $local_post) { // Vérifier que le post est valide if (!is_object($local_post) || !isset($local_post->ID)) { continue; } $type_local = get_post_meta($local_post->ID, 'type_de_local', true); $capacite = get_post_meta($local_post->ID, 'capacite', true); // Normaliser le type_local (peut être vide ou null) $type_local = !empty($type_local) ? $type_local : ''; // Construire le nom du local avec le type si disponible $nom_local = $local_post->post_title ?? 'Local sans nom'; if (!empty($type_local)) { $nom_local .= ' (' . $type_local . ')'; } // Vérifier la disponibilité du local pour la date/heure spécifiée $disponible = true; if ($date && $heure) { // Calculer la date/heure de fin (par défaut +1h si pas spécifiée) $date_fin = $all_params['date_fin'] ?? $date; $heure_fin = $all_params['heure_fin'] ?? null; if (!$heure_fin) { // Si pas d'heure de fin spécifiée, ajouter 1h par défaut $heure_obj = \DateTime::createFromFormat('H:i', $heure); if ($heure_obj) { $heure_obj->add(new \DateInterval('PT1H')); $heure_fin = $heure_obj->format('H:i'); } else { $heure_fin = '10:00'; // Fallback } } // Vérifier les conflits d'événements pour ce local try { $conflits = self::verifier_conflits_local($local_post->ID, $date, $date_fin, $heure, $heure_fin, $event_id); if (!empty($conflits) && is_array($conflits)) { $disponible = false; } } catch (\Exception $e) { error_log('[CRVI] Erreur lors de la vérification des conflits pour le local ' . $local_post->ID . ': ' . $e->getMessage()); // En cas d'erreur, considérer le local comme disponible pour ne pas bloquer l'utilisateur $disponible = true; } } // Filtrer par type de RDV si spécifié // Si le type_local est vide, on considère le local comme compatible avec tous les types $type_compatible = true; if ($type_rdv && in_array($type_rdv, ['individuel', 'groupe']) && !empty($type_local)) { if ($type_rdv === 'individuel' && $type_local !== 'individuel') { $type_compatible = false; } elseif ($type_rdv === 'groupe' && $type_local !== 'groupe') { $type_compatible = false; } } // Ajouter le local seulement s'il est disponible et compatible if ($disponible && $type_compatible) { $locaux[] = [ 'id' => $local_post->ID, 'nom' => $nom_local, 'type_de_local' => $type_local, 'capacite' => $capacite ? $capacite : '', ]; } } // Récupérer tous les bénéficiaires $beneficiaires = []; try { $beneficiaire_model = new \ESI_CRVI_AGENDA\models\CRVI_Beneficiaire_Model(); $beneficiaires_objects = $beneficiaire_model->get_all_beneficiaires(); if (is_array($beneficiaires_objects)) { foreach ($beneficiaires_objects as $beneficiaire) { // Vérifier que le bénéficiaire est un objet valide if (!is_object($beneficiaire) || !isset($beneficiaire->id)) { continue; } $beneficiaires[] = [ 'id' => $beneficiaire->id, 'nom' => ($beneficiaire->nom ?? '') . ' ' . ($beneficiaire->prenom ?? ''), ]; } } } catch (\Exception $e) { error_log('[CRVI] Erreur lors de la récupération des bénéficiaires: ' . $e->getMessage()); $beneficiaires = []; } // Récupérer toutes les langues disponibles depuis la taxonomie $langues_terms = get_terms([ 'taxonomy' => 'langue', 'hide_empty' => false, 'orderby' => 'name', 'order' => 'ASC', ]); $langues = []; if (!is_wp_error($langues_terms) && !empty($langues_terms)) { foreach ($langues_terms as $term) { // Ignorer les termes invalides (slug vide ou seulement numérique suspect) if (empty($term->slug) || empty($term->name)) { continue; } // Utiliser le slug comme ID (standard WordPress) // Si le slug est numérique, c'est peut-être un terme mal configuré, mais on le garde $langues[] = [ 'id' => $term->slug, 'nom' => $term->name, ]; } } // Récupérer tous les départements actifs $departements = CRVI_Departement_Model::all(true, true); // Récupérer tous les types d'intervention disponibles depuis les CPT $types_intervention_posts = get_posts([ 'post_type' => 'type_intervention', 'numberposts' => -1, 'post_status' => 'publish', ]); $types_intervention = []; foreach ($types_intervention_posts as $post) { $types_intervention[] = [ 'id' => $post->ID, 'nom' => $post->post_title, ]; } $response = [ 'intervenants' => $intervenants_formatted, 'locaux' => $locaux, 'beneficiaires' => $beneficiaires, 'langues' => $langues, 'departements' => $departements, 'types_intervention' => $types_intervention, ]; // Inclure les traducteurs seulement si la vérification a été faite if ($traducteurs !== null) { $response['traducteurs'] = $traducteurs_formatted; } return Api_Helper::json_success($response); } /** * Endpoint pour récupérer les permissions de l'utilisateur courant. * @param WP_REST_Request $request * @return WP_REST_Response */ public static function get_user_permissions($request) { $permissions = \ESI_CRVI_AGENDA\helpers\Api_Helper::get_user_permissions(); return Api_Helper::json_success($permissions); } public static function conflits_item($request) { $id = (int) $request['id']; $date = $request->get_param('date'); $heure = $request->get_param('heure'); $id_intervenant = $request->get_param('id_intervenant'); $id_traducteur = $request->get_param('id_traducteur'); $id_local = $request->get_param('id_local'); $model = new CRVI_Event_Model(); $result = $model->get_conflits($date, $heure, $id_intervenant, $id_traducteur, $id_local); return Api_Helper::json_success($result); } public static function export_items($request) { $model = new CRVI_Event_Model(); $params = $request->get_params(); // Support de la pagination pour l'export $date_debut = $params['start'] ?? $params['date_debut'] ?? null; $date_fin = $params['end'] ?? $params['date_fin'] ?? null; // Filtrer les paramètres pour éviter les conflits $filters = array_diff_key($params, array_flip(['start', 'end', 'date_debut', 'date_fin'])); $result = $model->get_events_by('date_rdv', null, $filters, $date_debut, $date_fin); return Api_Helper::json_success($result); } // --- CRUD --- public static function get_events($request) { $params = $request->get_params(); $model = new CRVI_Event_Model(); $events = $model->get_events_by_filters($params); // Convertir les langues, départements et types d'intervention (IDs vers noms) foreach ($events as &$event) { // Convertir la langue - ajouter langue_label pour l'affichage if (!empty($event['langue'])) { $langue_original = $event['langue']; $langue_label = ''; // Essayer d'abord par ID numérique $langue_term = get_term((int)$langue_original, 'langue'); if ($langue_term && !is_wp_error($langue_term)) { $langue_label = $langue_term->name; } else { // Essayer par slug si l'ID ne fonctionne pas $langue_term = get_term_by('slug', $langue_original, 'langue'); if ($langue_term && !is_wp_error($langue_term)) { $langue_label = $langue_term->name; } else { // Debug: comprendre pourquoi on ne trouve pas le terme error_log('CRVI Debug - Langue non trouvée: ID=' . $langue_original); if (is_wp_error($langue_term)) { error_log('CRVI Debug - Erreur WP: ' . $langue_term->get_error_message()); } } } // Ajouter le label de la langue pour l'affichage dans la modal (seulement si on a trouvé le nom) if (!empty($langue_label)) { $event['langue_label'] = $langue_label; } else { // Ne pas assigner l'ID comme fallback - laisser le champ vide $event['langue_label'] = ''; } } // Convertir le département - ajouter departement_label pour l'affichage if (!empty($event['id_departement']) && $event['id_departement'] != '0' && $event['id_departement'] != 0) { $departement_post = get_post((int)$event['id_departement']); if ($departement_post) { $event['departement_nom'] = $departement_post->post_title; $event['departement_label'] = $departement_post->post_title; } } // Convertir le type d'intervention - ajouter type_intervention_label pour l'affichage if (!empty($event['id_type_intervention']) && $event['id_type_intervention'] != '0' && $event['id_type_intervention'] != 0) { $type_post = get_post((int)$event['id_type_intervention']); if ($type_post) { $event['type_intervention_nom'] = $type_post->post_title; $event['type_intervention_label'] = $type_post->post_title; } } // Ajouter nom_traducteur si id_traducteur est 0 ou null if (empty($event['id_traducteur']) || $event['id_traducteur'] == '0' || $event['id_traducteur'] == 0) { if (!empty($event['nom_traducteur'])) { // nom_traducteur est déjà dans les données de l'événement } } } return Api_Helper::json_success($events); } public static function get_event($request) { $id = (int) $request['id']; $model = new CRVI_Event_Model(); $event = $model->get_event_enriched($id); if (!$event) { return Api_Helper::json_error('Événement introuvable', 404); } // Convertir la langue - ajouter langue_label pour l'affichage if (!empty($event['langue'])) { $langue_original = $event['langue']; $langue_label = ''; // Essayer d'abord par ID numérique $langue_term = get_term((int)$langue_original, 'langue'); if ($langue_term && !is_wp_error($langue_term)) { $langue_label = $langue_term->name; } else { // Essayer par slug si l'ID ne fonctionne pas $langue_term = get_term_by('slug', $langue_original, 'langue'); if ($langue_term && !is_wp_error($langue_term)) { $langue_label = $langue_term->name; } else { // Debug: comprendre pourquoi on ne trouve pas le terme error_log('CRVI Debug get_event - Langue non trouvée: ID=' . $langue_original); if (is_wp_error($langue_term)) { error_log('CRVI Debug get_event - Erreur WP: ' . $langue_term->get_error_message()); } } } // Ajouter le label de la langue pour l'affichage dans la modal (seulement si on a trouvé le nom) if (!empty($langue_label)) { $event['langue_label'] = $langue_label; } else { // Ne pas assigner l'ID comme fallback - laisser le champ vide $event['langue_label'] = ''; } } // Convertir le département if (!empty($event['id_departement'])) { $departement_post = get_post((int)$event['id_departement']); if ($departement_post) { $event['departement_nom'] = $departement_post->post_title; } } // Convertir le type d'intervention if (!empty($event['id_type_intervention'])) { $type_post = get_post((int)$event['id_type_intervention']); if ($type_post) { $event['type_intervention_nom'] = $type_post->post_title; } } return Api_Helper::json_success($event); } public static function create_event($request) { $data = $request->get_json_params(); $model = new CRVI_Event_Model(); $result = $model->create_event($data); if (is_wp_error($result)) { return Api_Helper::json_error($result->get_error_message(), 400); } return Api_Helper::json_success(['id' => $result, 'message' => 'Événement créé avec succès']); } public static function update_event($request) { $id = (int) $request['id']; $data = $request->get_json_params(); // Vérifier que les données JSON sont présentes if ($data === null) { // Essayer de récupérer depuis le body brut $body = $request->get_body(); if (!empty($body)) { $data = json_decode($body, true); if (json_last_error() !== JSON_ERROR_NONE) { return Api_Helper::json_error('Données JSON invalides: ' . json_last_error_msg(), 400); } } } // Si toujours null, utiliser un tableau vide if ($data === null) { $data = []; } $model = new CRVI_Event_Model(); $result = $model->update_event($id, $data); if (is_wp_error($result)) { $error_code = $result->get_error_code(); $error_data = $result->get_error_data(); $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_success(['id' => $id, 'message' => 'Événement modifié avec succès']); } public static function delete_event($request) { $id = (int) $request['id']; $model = new CRVI_Event_Model(); $result = $model->delete_event($id); if (is_wp_error($result)) { return Api_Helper::json_error($result->get_error_message(), 400); } return Api_Helper::json_success(['id' => $id, 'message' => 'Événement supprimé avec succès']); } /** * Crée des permanences pour l'intervenant connecté * POST /wp-json/crvi/v1/intervenant/permanences * * Cette méthode crée des événements de type "permanence" pour l'intervenant connecté. * Les permanences sont des créneaux horaires disponibles sans bénéficiaire assigné. */ public static function create_permanences($request) { $data = $request->get_json_params(); $user_id = get_current_user_id(); $intervenant = CRVI_Intervenant_Model::load($user_id); if (!$intervenant) { return Api_Helper::json_error('Intervenant introuvable', 404); } // Validation des paramètres $periode = isset($data['periode']) ? (int)$data['periode'] : 0; $mois_debut = isset($data['mois_debut']) ? trim($data['mois_debut']) : ''; $jours = isset($data['jours']) && is_array($data['jours']) ? $data['jours'] : []; $plage_horaire = isset($data['plage_horaire']) && is_array($data['plage_horaire']) ? $data['plage_horaire'] : []; $duree_permanence = isset($data['duree_permanence']) ? sanitize_text_field($data['duree_permanence']) : '1h'; $nb_tranches = isset($data['nb_tranches']) && $duree_permanence === '15min' ? (int)$data['nb_tranches'] : null; $langues = isset($data['langues']) && is_array($data['langues']) ? $data['langues'] : []; $informations_complementaires = isset($data['informations_complementaires']) ? sanitize_textarea_field($data['informations_complementaires']) : ''; // Validation de la durée et du nombre de tranches if (!in_array($duree_permanence, ['1h', '15min'], true)) { return Api_Helper::json_error('Durée de permanence invalide (attendu: 1h ou 15min)', 400); } if ($duree_permanence === '15min') { if ($nb_tranches === null || $nb_tranches < 1 || $nb_tranches > 4) { return Api_Helper::json_error('Le nombre de tranches doit être entre 1 et 4 pour les permanences de 15 minutes', 400); } } // Nettoyer et valider les langues (slugs de la taxonomie) $langues_valides = []; if (!empty($langues)) { foreach ($langues as $langue_slug) { $langue_slug = sanitize_text_field($langue_slug); // Vérifier que la langue existe dans la taxonomie $term = get_term_by('slug', $langue_slug, 'langue'); if ($term && !is_wp_error($term)) { $langues_valides[] = $langue_slug; } } } // Validation if (!in_array($periode, [3, 6], true)) { return Api_Helper::json_error('La période doit être de 3 ou 6 mois', 400); } if (empty($mois_debut)) { return Api_Helper::json_error('Veuillez sélectionner un mois de début', 400); } // Validation du format du mois (YYYY-MM) if (!preg_match('/^\d{4}-\d{2}$/', $mois_debut)) { return Api_Helper::json_error('Format de mois invalide (attendu: YYYY-MM)', 400); } if (empty($jours)) { return Api_Helper::json_error('Veuillez sélectionner au moins un jour de la semaine', 400); } // Récupérer les heures sélectionnées $heures_selectionnees = isset($data['heures']) && is_array($data['heures']) ? $data['heures'] : []; if (empty($heures_selectionnees)) { return Api_Helper::json_error('Veuillez sélectionner au moins une heure de permanence', 400); } // Validation du format des heures (HH:mm) foreach ($heures_selectionnees as $heure) { if (!preg_match('/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/', $heure)) { return Api_Helper::json_error('Format d\'heure invalide (attendu: HH:mm)', 400); } } // Calculer la plage de dates à partir du mois de début sélectionné list($year, $month) = explode('-', $mois_debut); $date_debut = new \DateTime("{$year}-{$month}-01"); // Premier jour du mois $date_fin = clone $date_debut; $date_fin->modify("+{$periode} months"); $date_fin->modify('-1 day'); // Dernier jour du dernier mois // Générer les tranches horaires selon la durée choisie $tranches_horaires = []; if ($duree_permanence === '1h') { // Mode 1 heure : créer une tranche d'1 heure pour chaque heure sélectionnée foreach ($heures_selectionnees as $heure_debut) { list($h, $m) = explode(':', $heure_debut); $h = (int)$h; $m = (int)$m; $heure_fin = sprintf('%02d:%02d', ($h + 1) % 24, $m); $tranches_horaires[] = [ 'debut' => $heure_debut, 'fin' => $heure_fin, ]; } } else { // Mode 15 minutes : créer X tranches de 15 minutes pour chaque heure sélectionnée foreach ($heures_selectionnees as $heure_debut) { list($h, $m) = explode(':', $heure_debut); $h = (int)$h; $m = (int)$m; // Créer le nombre de tranches demandé (1 à 4) for ($i = 0; $i < $nb_tranches; $i++) { $debut_minutes = $m + ($i * 15); $fin_minutes = $debut_minutes + 15; $tranche_h = $h + floor($debut_minutes / 60); $tranche_m = $debut_minutes % 60; $tranche_fin_h = $h + floor($fin_minutes / 60); $tranche_fin_m = $fin_minutes % 60; $tranche_debut = sprintf('%02d:%02d', $tranche_h % 24, $tranche_m); $tranche_fin = sprintf('%02d:%02d', $tranche_fin_h % 24, $tranche_fin_m); $tranches_horaires[] = [ 'debut' => $tranche_debut, 'fin' => $tranche_fin, ]; } } } // Mapping des jours de la semaine (français vers numéro) $jours_mapping = [ 'lundi' => 1, 'mardi' => 2, 'mercredi' => 3, 'jeudi' => 4, 'vendredi' => 5, 'samedi' => 6, 'dimanche' => 0, ]; $jours_numeriques = []; foreach ($jours as $jour) { $jour_lower = strtolower($jour); if (isset($jours_mapping[$jour_lower])) { $jours_numeriques[] = $jours_mapping[$jour_lower]; } } if (empty($jours_numeriques)) { return Api_Helper::json_error('Jours de la semaine invalides', 400); } // Créer les événements pour chaque jour sélectionné dans la période $permanences_crees = 0; $current_date = clone $date_debut; while ($current_date <= $date_fin) { $jour_semaine = (int)$current_date->format('w'); // 0 = dimanche, 1 = lundi, etc. // Ignorer les dates passées $today = new \DateTime(); if ($current_date < $today) { $current_date->modify('+1 day'); continue; } // Si ce jour de la semaine est sélectionné if (in_array($jour_semaine, $jours_numeriques, true)) { // Créer une permanence pour chaque tranche horaire foreach ($tranches_horaires as $tranche) { // Utiliser la méthode dédiée du modèle pour créer une permanence $model = new CRVI_Event_Model(); $result = $model->create_permanence([ 'date_rdv' => $current_date->format('Y-m-d'), 'heure_rdv' => $tranche['debut'], 'date_fin' => $current_date->format('Y-m-d'), 'heure_fin' => $tranche['fin'], 'id_intervenant' => $intervenant->id, 'commentaire' => !empty($informations_complementaires) ? $informations_complementaires : '', 'langues' => $langues_valides, // Langues sélectionnées (tableau de slugs) // Passer les jours et heures sélectionnés dans le formulaire 'jours_permis' => $jours, // Jours sélectionnés (lundi, mardi, etc.) 'heures_selectionnees' => $heures_selectionnees, // Heures sélectionnées ]); if (!is_wp_error($result)) { $permanences_crees++; } } } $current_date->modify('+1 day'); } if ($permanences_crees === 0) { return Api_Helper::json_error('Aucune permanence n\'a pu être créée', 500); } return Api_Helper::json_success([ 'message' => "Permanences enregistrées avec succès", 'permanences_crees' => $permanences_crees, ]); } /** * Crée des permanences pour un intervenant (version admin) * POST /wp-json/crvi/v1/admin/permanences * * Différence avec create_permanences : accepte un intervenant_id dans les paramètres */ public static function create_permanences_admin($request) { $data = $request->get_json_params(); // Récupérer l'ID de l'intervenant depuis les paramètres (au lieu de get_current_user_id()) $intervenant_user_id = isset($data['intervenant_id']) ? (int)$data['intervenant_id'] : 0; if (!$intervenant_user_id) { return Api_Helper::json_error('Veuillez sélectionner un intervenant', 400); } $intervenant = CRVI_Intervenant_Model::load($intervenant_user_id); if (!$intervenant) { return Api_Helper::json_error('Intervenant introuvable', 404); } // Validation des paramètres (identique à create_permanences) $periode = isset($data['periode']) ? (int)$data['periode'] : 0; $mois_debut = isset($data['mois_debut']) ? trim($data['mois_debut']) : ''; $jours = isset($data['jours']) && is_array($data['jours']) ? $data['jours'] : []; $duree_permanence = isset($data['duree_permanence']) ? sanitize_text_field($data['duree_permanence']) : '1h'; $nb_tranches = isset($data['nb_tranches']) && $duree_permanence === '15min' ? (int)$data['nb_tranches'] : null; $langues = isset($data['langues']) && is_array($data['langues']) ? $data['langues'] : []; $informations_complementaires = isset($data['informations_complementaires']) ? sanitize_textarea_field($data['informations_complementaires']) : ''; // Validation de la durée et du nombre de tranches if (!in_array($duree_permanence, ['1h', '15min'], true)) { return Api_Helper::json_error('Durée de permanence invalide (attendu: 1h ou 15min)', 400); } if ($duree_permanence === '15min') { if ($nb_tranches === null || $nb_tranches < 1 || $nb_tranches > 4) { return Api_Helper::json_error('Le nombre de tranches doit être entre 1 et 4 pour les permanences de 15 minutes', 400); } } // Nettoyer et valider les langues (slugs de la taxonomie) $langues_valides = []; if (!empty($langues)) { foreach ($langues as $langue_slug) { $langue_slug = sanitize_text_field($langue_slug); // Vérifier que la langue existe dans la taxonomie $term = get_term_by('slug', $langue_slug, 'langue'); if ($term && !is_wp_error($term)) { $langues_valides[] = $langue_slug; } } } // Validation if (!in_array($periode, [3, 6], true)) { return Api_Helper::json_error('La période doit être de 3 ou 6 mois', 400); } if (empty($mois_debut)) { return Api_Helper::json_error('Veuillez sélectionner un mois de début', 400); } // Validation du format du mois (YYYY-MM) if (!preg_match('/^\d{4}-\d{2}$/', $mois_debut)) { return Api_Helper::json_error('Format de mois invalide (attendu: YYYY-MM)', 400); } if (empty($jours)) { return Api_Helper::json_error('Veuillez sélectionner au moins un jour de la semaine', 400); } // Récupérer les heures sélectionnées $heures_selectionnees = isset($data['heures']) && is_array($data['heures']) ? $data['heures'] : []; if (empty($heures_selectionnees)) { return Api_Helper::json_error('Veuillez sélectionner au moins une heure de permanence', 400); } // Validation du format des heures (HH:mm) foreach ($heures_selectionnees as $heure) { if (!preg_match('/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/', $heure)) { return Api_Helper::json_error('Format d\'heure invalide (attendu: HH:mm)', 400); } } // Calculer la plage de dates à partir du mois de début sélectionné list($year, $month) = explode('-', $mois_debut); $date_debut = new \DateTime("{$year}-{$month}-01"); // Premier jour du mois $date_fin = clone $date_debut; $date_fin->modify("+{$periode} months"); $date_fin->modify('-1 day'); // Dernier jour du dernier mois // Générer les tranches horaires selon la durée choisie $tranches_horaires = []; if ($duree_permanence === '1h') { // Mode 1 heure : créer une tranche d'1 heure pour chaque heure sélectionnée foreach ($heures_selectionnees as $heure_debut) { list($h, $m) = explode(':', $heure_debut); $h = (int)$h; $m = (int)$m; $heure_fin = sprintf('%02d:%02d', ($h + 1) % 24, $m); $tranches_horaires[] = [ 'debut' => $heure_debut, 'fin' => $heure_fin, ]; } } else { // Mode 15 minutes : créer X tranches de 15 minutes pour chaque heure sélectionnée foreach ($heures_selectionnees as $heure_debut) { list($h, $m) = explode(':', $heure_debut); $h = (int)$h; $m = (int)$m; // Créer le nombre de tranches demandé (1 à 4) for ($i = 0; $i < $nb_tranches; $i++) { $debut_minutes = $m + ($i * 15); $fin_minutes = $debut_minutes + 15; $tranche_h = $h + floor($debut_minutes / 60); $tranche_m = $debut_minutes % 60; $tranche_fin_h = $h + floor($fin_minutes / 60); $tranche_fin_m = $fin_minutes % 60; $tranche_debut = sprintf('%02d:%02d', $tranche_h % 24, $tranche_m); $tranche_fin = sprintf('%02d:%02d', $tranche_fin_h % 24, $tranche_fin_m); $tranches_horaires[] = [ 'debut' => $tranche_debut, 'fin' => $tranche_fin, ]; } } } // Mapping des jours de la semaine (français vers numéro) $jours_mapping = [ 'lundi' => 1, 'mardi' => 2, 'mercredi' => 3, 'jeudi' => 4, 'vendredi' => 5, 'samedi' => 6, 'dimanche' => 0, ]; $jours_numeriques = []; foreach ($jours as $jour) { $jour_lower = strtolower($jour); if (isset($jours_mapping[$jour_lower])) { $jours_numeriques[] = $jours_mapping[$jour_lower]; } } if (empty($jours_numeriques)) { return Api_Helper::json_error('Jours de la semaine invalides', 400); } // Créer les événements pour chaque jour sélectionné dans la période $permanences_crees = 0; $current_date = clone $date_debut; while ($current_date <= $date_fin) { $jour_semaine = (int)$current_date->format('w'); // 0 = dimanche, 1 = lundi, etc. // Ignorer les dates passées $today = new \DateTime(); if ($current_date < $today) { $current_date->modify('+1 day'); continue; } // Si ce jour de la semaine est sélectionné if (in_array($jour_semaine, $jours_numeriques, true)) { // Créer une permanence pour chaque tranche horaire foreach ($tranches_horaires as $tranche) { // Utiliser la méthode dédiée du modèle pour créer une permanence $model = new CRVI_Event_Model(); $result = $model->create_permanence([ 'date_rdv' => $current_date->format('Y-m-d'), 'heure_rdv' => $tranche['debut'], 'date_fin' => $current_date->format('Y-m-d'), 'heure_fin' => $tranche['fin'], 'id_intervenant' => $intervenant->id, 'commentaire' => !empty($informations_complementaires) ? $informations_complementaires : '', 'langues' => $langues_valides, // Langues sélectionnées (tableau de slugs) // Passer les jours et heures sélectionnés dans le formulaire 'jours_permis' => $jours, // Jours sélectionnés (lundi, mardi, etc.) 'heures_selectionnees' => $heures_selectionnees, // Heures sélectionnées ]); if (!is_wp_error($result)) { $permanences_crees++; } } } $current_date->modify('+1 day'); } if ($permanences_crees === 0) { return Api_Helper::json_error('Aucune permanence n\'a pu être créée', 500); } return Api_Helper::json_success([ 'message' => "Permanences enregistrées avec succès pour " . $intervenant->nom . ' ' . $intervenant->prenom, 'permanences_crees' => $permanences_crees, ]); } /** * Import CSV de permanences (admin) * POST /wp-json/crvi/v1/admin/permanences/import-csv * * Format CSV attendu : * - intervenant_id : ID de l'intervenant (obligatoire) * - date_debut : Date de début (YYYY-MM-DD) (obligatoire) * - date_fin : Date de fin (YYYY-MM-DD) (obligatoire) * - heure_debut : Heure de début (HH:MM) (obligatoire) * - heure_fin : Heure de fin (HH:MM) (obligatoire) * - informations_complementaires : Notes (optionnel) */ public static function import_permanences_csv($request) { if (!current_user_can('manage_options')) { return Api_Helper::json_error('Non autorisé', 403); } $file = $request->get_file_params()['file'] ?? null; if (!$file || !is_uploaded_file($file['tmp_name'])) { return Api_Helper::json_error('Fichier CSV manquant ou invalide', 400); } $handle = fopen($file['tmp_name'], 'r'); if (!$handle) { return Api_Helper::json_error('Impossible d\'ouvrir le fichier', 400); } // Lire l'en-tête $header = fgetcsv($handle, 0, ','); if (!$header) { fclose($handle); return Api_Helper::json_error('Fichier CSV vide ou invalide', 400); } // Normaliser les noms de colonnes (minuscules avec underscores) $header = array_map(function($k) { return sanitize_title(str_replace(' ', '_', strtolower(trim($k)))); }, $header); $created = 0; $errors = []; $row_num = 1; // Lire chaque ligne while (($row = fgetcsv($handle, 0, ',')) !== false) { $row_num++; // Combiner l'en-tête avec les valeurs $data = array_combine($header, array_map('trim', $row)); // Validation des champs obligatoires $intervenant_id = isset($data['intervenant_id']) ? (int)$data['intervenant_id'] : 0; $date_debut = isset($data['date_debut']) ? trim($data['date_debut']) : ''; $date_fin = isset($data['date_fin']) ? trim($data['date_fin']) : ''; $heure_debut = isset($data['heure_debut']) ? trim($data['heure_debut']) : ''; $heure_fin = isset($data['heure_fin']) ? trim($data['heure_fin']) : ''; $informations_complementaires = isset($data['informations_complementaires']) ? trim($data['informations_complementaires']) : ''; // Validation if (!$intervenant_id) { $errors[] = ['line' => $row_num, 'message' => 'intervenant_id manquant ou invalide']; continue; } if (empty($date_debut) || empty($date_fin)) { $errors[] = ['line' => $row_num, 'message' => 'date_debut ou date_fin manquant']; continue; } // Validation du format des dates (YYYY-MM-DD) if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date_debut) || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $date_fin)) { $errors[] = ['line' => $row_num, 'message' => 'Format de date invalide (attendu: YYYY-MM-DD)']; continue; } // Validation du format des heures (HH:MM) if (empty($heure_debut) || empty($heure_fin)) { $errors[] = ['line' => $row_num, 'message' => 'heure_debut ou heure_fin manquant']; continue; } if (!preg_match('/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/', $heure_debut) || !preg_match('/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/', $heure_fin)) { $errors[] = ['line' => $row_num, 'message' => 'Format d\'heure invalide (attendu: HH:MM)']; continue; } // Vérifier que l'intervenant existe $intervenant = \ESI_CRVI_AGENDA\models\CRVI_Intervenant_Model::load($intervenant_id); if (!$intervenant) { $errors[] = ['line' => $row_num, 'message' => "Intervenant avec l'ID $intervenant_id introuvable"]; continue; } // Créer la permanence pour chaque date entre date_debut et date_fin $date_start = new \DateTime($date_debut); $date_end = new \DateTime($date_fin); $date_end->modify('+1 day'); // Inclure le jour de fin $current_date = clone $date_start; $permanences_ligne = 0; while ($current_date < $date_end) { $date_str = $current_date->format('Y-m-d'); // Utiliser la méthode du modèle pour créer une permanence $model = new CRVI_Event_Model(); $result = $model->create_permanence([ 'date_rdv' => $date_str, 'heure_rdv' => $heure_debut, 'date_fin' => $date_str, 'heure_fin' => $heure_fin, 'id_intervenant' => $intervenant_id, 'commentaire' => $informations_complementaires, ]); if (!is_wp_error($result)) { $permanences_ligne++; $created++; } else { $errors[] = [ 'line' => $row_num, 'message' => "Erreur création permanence pour $date_str : " . $result->get_error_message() ]; } $current_date->modify('+1 day'); } } fclose($handle); return Api_Helper::json_success([ 'message' => "Import terminé : $created permanence(s) créée(s)", 'created' => $created, 'errors' => $errors, ]); } /** * Import CSV de permanences (intervenant) * POST /wp-json/crvi/v1/intervenant/permanences/import-csv * * Format CSV attendu (sans intervenant_id - utilise l'intervenant connecté) : * - date_debut : Date de début (YYYY-MM-DD) (obligatoire) * - date_fin : Date de fin (YYYY-MM-DD) (obligatoire) * - heure_debut : Heure de début (HH:MM) (obligatoire) * - heure_fin : Heure de fin (HH:MM) (obligatoire) * - informations_complementaires : Notes (optionnel) */ public static function import_permanences_csv_intervenant($request) { // Vérifier les permissions (intervenant connecté) if (!\ESI_CRVI_AGENDA\controllers\Intervenant_Space_Controller::check_intervenant_permission()) { return Api_Helper::json_error('Non autorisé', 403); } $user_id = get_current_user_id(); $intervenant = \ESI_CRVI_AGENDA\models\CRVI_Intervenant_Model::load($user_id); if (!$intervenant) { return Api_Helper::json_error('Intervenant introuvable pour cet utilisateur', 404); } $file = $request->get_file_params()['file'] ?? null; if (!$file || !is_uploaded_file($file['tmp_name'])) { return Api_Helper::json_error('Fichier CSV manquant ou invalide', 400); } $handle = fopen($file['tmp_name'], 'r'); if (!$handle) { return Api_Helper::json_error('Impossible d\'ouvrir le fichier', 400); } // Lire l'en-tête $header = fgetcsv($handle, 0, ','); if (!$header) { fclose($handle); return Api_Helper::json_error('Fichier CSV vide ou invalide', 400); } // Normaliser les noms de colonnes (minuscules avec underscores) $header = array_map(function($k) { return sanitize_title(str_replace(' ', '_', strtolower(trim($k)))); }, $header); $created = 0; $errors = []; $row_num = 1; // Lire chaque ligne while (($row = fgetcsv($handle, 0, ',')) !== false) { $row_num++; // Combiner l'en-tête avec les valeurs $data = array_combine($header, array_map('trim', $row)); // Validation des champs obligatoires $date_debut = isset($data['date_debut']) ? trim($data['date_debut']) : ''; $date_fin = isset($data['date_fin']) ? trim($data['date_fin']) : ''; $heure_debut = isset($data['heure_debut']) ? trim($data['heure_debut']) : ''; $heure_fin = isset($data['heure_fin']) ? trim($data['heure_fin']) : ''; $informations_complementaires = isset($data['informations_complementaires']) ? trim($data['informations_complementaires']) : ''; // Validation if (empty($date_debut) || empty($date_fin)) { $errors[] = ['line' => $row_num, 'message' => 'date_debut ou date_fin manquant']; continue; } // Validation du format des dates (YYYY-MM-DD) if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date_debut) || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $date_fin)) { $errors[] = ['line' => $row_num, 'message' => 'Format de date invalide (attendu: YYYY-MM-DD)']; continue; } // Validation du format des heures (HH:MM) if (empty($heure_debut) || empty($heure_fin)) { $errors[] = ['line' => $row_num, 'message' => 'heure_debut ou heure_fin manquant']; continue; } if (!preg_match('/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/', $heure_debut) || !preg_match('/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/', $heure_fin)) { $errors[] = ['line' => $row_num, 'message' => 'Format d\'heure invalide (attendu: HH:MM)']; continue; } // Créer la permanence pour chaque date entre date_debut et date_fin $date_start = new \DateTime($date_debut); $date_end = new \DateTime($date_fin); $date_end->modify('+1 day'); // Inclure le jour de fin $current_date = clone $date_start; $permanences_ligne = 0; while ($current_date < $date_end) { $date_str = $current_date->format('Y-m-d'); // Utiliser la méthode du modèle pour créer une permanence $model = new CRVI_Event_Model(); $result = $model->create_permanence([ 'date_rdv' => $date_str, 'heure_rdv' => $heure_debut, 'date_fin' => $date_str, 'heure_fin' => $heure_fin, 'id_intervenant' => $intervenant->id, 'commentaire' => $informations_complementaires, ]); if (!is_wp_error($result)) { $permanences_ligne++; $created++; } else { $errors[] = [ 'line' => $row_num, 'message' => "Erreur création permanence pour $date_str : " . $result->get_error_message() ]; } $current_date->modify('+1 day'); } } fclose($handle); return Api_Helper::json_success([ 'message' => "Import terminé : $created permanence(s) créée(s)", 'created' => $created, 'errors' => $errors, ]); } public static function get_deleted_events($request) { $params = $request->get_params(); $model = new CRVI_Event_Model(); $events = $model->get_deleted_events($params); return Api_Helper::json_success($events); } public static function restore_event($request) { $id = (int) $request['id']; $model = new CRVI_Event_Model(); $result = $model->restore_event($id); if (is_wp_error($result)) { return Api_Helper::json_error($result->get_error_message(), 400); } return Api_Helper::json_success(['id' => $id, 'message' => 'Événement restauré avec succès']); } public static function hard_delete_event($request) { $id = (int) $request['id']; $model = new CRVI_Event_Model(); $result = $model->hard_delete_event($id); if (is_wp_error($result)) { return Api_Helper::json_error($result->get_error_message(), 400); } return Api_Helper::json_success(['id' => $id, 'message' => 'Événement supprimé définitivement']); } // --- Avancés --- public static function cloture_event($request) { $id = (int) $request['id']; $data = $request->get_json_params(); $model = new CRVI_Event_Model(); $result = $model->cloture_event($id, $data['statut'] ?? ''); if (is_wp_error($result)) { return Api_Helper::json_error($result->get_error_message(), 400); } return Api_Helper::json_success(['id' => $id, 'message' => 'Événement clôturé avec succès']); } public static function change_statut($request) { $id = (int) $request['id']; $data = $request->get_json_params(); $model = new CRVI_Event_Model(); $result = $model->change_statut($id, $data); if (is_wp_error($result)) { return Api_Helper::json_error($result->get_error_message(), 400); } return Api_Helper::json_success(['id' => $id, 'message' => 'Statut modifié avec succès']); } /** * Retourne la couleur selon le statut */ private static function get_status_color($statut) { switch ($statut) { case 'prevu': return '#28a745'; // Vert case 'annule': return '#dc3545'; // Rouge case 'non_tenu': return '#ffc107'; // Jaune case 'cloture': return '#6c757d'; // Gris case 'absence': return '#fd7e14'; // Orange default: return '#007bff'; // Bleu } } public static function get_events_fullcalendar($request) { $model = new CRVI_Event_Model(); $params = $request->get_params(); // Support de la pagination FullCalendar $date_debut = $params['start'] ?? $params['date_debut'] ?? null; $date_fin = $params['end'] ?? $params['date_fin'] ?? null; // Filtrer les paramètres pour éviter les conflits $filters = array_diff_key($params, array_flip(['start', 'end', 'date_debut', 'date_fin'])); // Ajouter les dates de début et fin aux filtres si elles existent if ($date_debut) { $filters['date_debut'] = $date_debut; } if ($date_fin) { $filters['date_fin'] = $date_fin; } // Utiliser get_events_by_filters au lieu de get_events_by pour une meilleure compatibilité $events = $model->get_events_by_filters($filters); // Formater pour FullCalendar $formatted_events = []; foreach ($events as $event) { // Récupérer les détails des entités liées $details = $model->get_details($event['id']); // Couleur basée sur le type d'intervention (ACF champ 'couleur' sur le CPT type_intervention) $type_color = '#6c757d'; if (!empty($event['type_intervention'])) { $color_field = function_exists('get_field') ? get_field('couleur', (int)$event['type_intervention']) : null; if (!empty($color_field)) { $type_color = $color_field; } } // Convertir la langue pour obtenir le label $langue_label = ''; if (!empty($event['langue'])) { $langue_original = $event['langue']; // Essayer d'abord par ID numérique $langue_term = get_term((int)$langue_original, 'langue'); if ($langue_term && !is_wp_error($langue_term)) { $langue_label = $langue_term->name; } else { // Essayer par slug si l'ID ne fonctionne pas $langue_term = get_term_by('slug', $langue_original, 'langue'); if ($langue_term && !is_wp_error($langue_term)) { $langue_label = $langue_term->name; } else { // Debug: comprendre pourquoi on ne trouve pas le terme error_log('CRVI Debug FullCalendar - Langue non trouvée: ID=' . $langue_original); if (is_wp_error($langue_term)) { error_log('CRVI Debug FullCalendar - Erreur WP: ' . $langue_term->get_error_message()); } } } } // Convertir le département pour obtenir le label $departement_label = ''; if (!empty($event['id_departement']) && $event['id_departement'] != '0' && $event['id_departement'] != 0) { $departement_post = get_post((int)$event['id_departement']); if ($departement_post) { $departement_label = $departement_post->post_title; } } // Convertir le type d'intervention pour obtenir le label $type_intervention_label = ''; if (!empty($event['type_intervention']) && $event['type_intervention'] != '0' && $event['type_intervention'] != 0) { $type_post = get_post((int)$event['type_intervention']); if ($type_post) { $type_intervention_label = $type_post->post_title; } } // Récupérer nom_traducteur si id_traducteur est 0 ou null $nom_traducteur = null; if (empty($event['id_traducteur']) || $event['id_traducteur'] == '0' || $event['id_traducteur'] == 0) { $nom_traducteur = $event['nom_traducteur'] ?? null; } $formatted_events[] = [ 'id' => $event['id'], 'title' => ($details->beneficiaire->nom ?? '') . ' ' . ($details->beneficiaire->prenom ?? '') . ' - ' . ($details->intervenant->nom ?? '') . ' ' . ($details->intervenant->prenom ?? ''), 'start' => $event['date_rdv'] . 'T' . $event['heure_rdv'], 'end' => $event['date_fin'] . 'T' . $event['heure_fin'], 'backgroundColor' => $type_color, 'borderColor' => $type_color, 'textColor' => '#fff', 'extendedProps' => [ 'type' => $event['type'], 'statut' => $event['statut'], 'langue' => $event['langue'], 'langue_label' => $langue_label, 'langues_disponibles' => $event['langues_disponibles'] ?? null, 'assign' => isset($event['assign']) ? (int)$event['assign'] : 0, 'id_type_intervention' => $event['type_intervention'] ?? null, 'type_intervention_label' => $type_intervention_label, 'id_departement' => $event['id_departement'] ?? null, 'departement_label' => $departement_label, 'beneficiaire' => $details->beneficiaire ?? null, 'intervenant' => $details->intervenant ?? null, 'traducteur' => $details->traducteur ?? null, 'local' => $details->local ?? null, 'commentaire' => $event['commentaire'], 'id_beneficiaire' => $event['id_beneficiaire'], 'id_intervenant' => $event['id_intervenant'], 'id_traducteur' => $event['id_traducteur'], 'nom_traducteur' => $nom_traducteur, 'id_local' => $event['id_local'] ] ]; } return Api_Helper::json_success($formatted_events); } /** * Endpoint pour récupérer les événements en format tableau avec pagination * @param \WP_REST_Request $request * @return \WP_REST_Response */ public static function get_events_table($request) { $model = new CRVI_Event_Model(); $params = $request->get_params(); // Pagination $page = isset($params['page']) ? max(1, (int)$params['page']) : 1; $per_page = isset($params['per_page']) ? max(1, min(100, (int)$params['per_page'])) : 20; // Gestion du filtre par année if (!empty($params['annee'])) { $annee = (int)$params['annee']; $params['date_debut'] = sprintf('%d-01-01', $annee); $params['date_fin'] = sprintf('%d-12-31', $annee); unset($params['annee']); } // Récupérer les événements avec pagination $result = $model->get_events_table($params, $page, $per_page); return Api_Helper::json_success($result); } public static function get_events_stats($request) { $params = $request->get_params(); $model = new CRVI_Event_Model(); $stats = $model->get_events_stats($params); return Api_Helper::json_success($stats); } public static function get_event_historique($request) { $id = (int) $request['id']; $model = new CRVI_Event_Model(); $historique = $model->get_historique(); return Api_Helper::json_success($historique); } // --- Filtres dynamiques --- public static function get_departements($request) { $departements = get_terms([ 'taxonomy' => 'departement', 'hide_empty' => false, ]); $result = []; foreach ($departements as $departement) { $result[] = [ 'id' => $departement->term_id, 'name' => $departement->name, 'slug' => $departement->slug, ]; } return Api_Helper::json_success($result); } public static function get_types_intervention($request) { $types = get_terms([ 'taxonomy' => 'type_intervention', 'hide_empty' => false, ]); $result = []; foreach ($types as $type) { $result[] = [ 'id' => $type->term_id, 'name' => $type->name, 'slug' => $type->slug, ]; } return Api_Helper::json_success($result); } public static function get_langues($request) { $langues = get_terms([ 'taxonomy' => 'langue', 'hide_empty' => false, ]); $result = []; foreach ($langues as $langue) { $result[] = [ 'id' => $langue->term_id, 'name' => $langue->name, 'slug' => $langue->slug, ]; } return Api_Helper::json_success($result); } public static function get_langues_beneficiaire($request) { $langues = \ESI_CRVI_AGENDA\helpers\Api_Helper::get_languages(true); return \ESI_CRVI_AGENDA\helpers\Api_Helper::json_success($langues); } public static function get_statuts($request) { $statuts = [ ['id' => 'prevu', 'label' => 'Prévu'], ['id' => 'annule', 'label' => 'Annulé'], ['id' => 'non_tenu', 'label' => 'Non tenu'], ['id' => 'cloture', 'label' => 'Clôturé'], ['id' => 'absence', 'label' => 'Absence'], ]; return Api_Helper::json_success($statuts); } /** * Vérifie si l'utilisateur peut modifier un événement * @param \WP_REST_Request $request * @return bool */ public static function can_edit($request = null) { // Si admin ou rôle ayant edit_posts : accès total if (current_user_can('edit_posts')) { return true; } // Récupérer l'id_intervenant depuis la requête ou l'événement $id_intervenant = null; if ($request) { // Si on modifie un événement existant, récupérer l'ID depuis l'événement if ($request->get_param('id')) { $event = \ESI_CRVI_AGENDA\models\CRVI_Event_Model::load((int)$request->get_param('id')); if ($event && isset($event->id_intervenant)) { $id_intervenant = $event->id_intervenant; } } // Sinon, essayer de le prendre dans les paramètres de la requête (création) if (!$id_intervenant) { $data = $request->get_json_params(); if ($data && isset($data['id_intervenant'])) { $id_intervenant = $data['id_intervenant']; } } } // Appel à la logique intervenant return CRVI_Intervenant_Controller::can_edit_own_event($id_intervenant); } /** * Fonction pour récupérer les dates et créneaux indisponibles pour une combinaison d'entités */ private static function get_available_dates($id_intervenant, $id_traducteur, $id_local, $date_debut, $date_fin) { $unavailable_dates = []; $unavailable_slots = []; // Convertir les dates en objets DateTime $debut = \DateTime::createFromFormat('Y-m-d', $date_debut); $fin = \DateTime::createFromFormat('Y-m-d', $date_fin); if (!$debut || !$fin) { return Api_Helper::json_error('Format de date invalide'); } // Définir les créneaux horaires possibles (8h-18h par exemple) $creneaux_horaires = [ '08:00', '08:30', '09:00', '09:30', '10:00', '10:30', '11:00', '11:30', '12:00', '12:30', '13:00', '13:30', '14:00', '14:30', '15:00', '15:30', '16:00', '16:30', '17:00', '17:30', '18:00' ]; // Parcourir chaque jour dans la plage $current_date = clone $debut; while ($current_date <= $fin) { $date_str = $current_date->format('Y-m-d'); // Vérifier la disponibilité pour cette date $disponible = self::check_availability_for_date($date_str, $id_intervenant, $id_traducteur, $id_local); if (!$disponible) { $unavailable_dates[] = $date_str; } else { // Si la date est disponible, vérifier les créneaux horaires foreach ($creneaux_horaires as $heure) { $disponible_creneau = self::check_availability_for_slot($date_str, $heure, $id_intervenant, $id_traducteur, $id_local); if (!$disponible_creneau) { $unavailable_slots[] = $date_str . ' ' . $heure; } } } $current_date->add(new \DateInterval('P1D')); } return Api_Helper::json_success([ 'unavailable_dates' => $unavailable_dates, 'unavailable_slots' => $unavailable_slots ]); } /** * Vérifie la disponibilité pour une date donnée */ private static function check_availability_for_date($date, $id_intervenant, $id_traducteur, $id_local) { // Vérifier les indisponibilités ponctuelles // Vérifier les indisponibilités ponctuelles de l'intervenant $intervenant = \ESI_CRVI_AGENDA\models\CRVI_Intervenant_Model::load($id_intervenant); if ($intervenant) { $indisponibilites = $intervenant->indisponibilitee_ponctuelle ?? null; if (!empty($indisponibilites)) { // Pour l'intervenant, le champ n'est pas défini dans le modèle actuel // donc on ne traite pas les indisponibilités pour l'instant } } // Vérifier les indisponibilités ponctuelles du traducteur $traducteur = \ESI_CRVI_AGENDA\models\CRVI_Traducteur_Model::load($id_traducteur); if ($traducteur) { $indisponibilites = $traducteur->indisponibilitee_ponctuelle ?? null; if (!empty($indisponibilites) && is_array($indisponibilites)) { // Le champ indisponibilitee_ponctuelle est un repeater ACF (array) foreach ($indisponibilites as $indisponibilite) { if (isset($indisponibilite['date']) && $indisponibilite['date'] === $date) { return false; // Indisponible } } } } // Vérifier les indisponibilités ponctuelles du local $local = \ESI_CRVI_AGENDA\models\CRVI_Local_Model::load($id_local); if ($local) { $indisponibilites = $local->indisponibilitee_ponctuelle ?? null; if (!empty($indisponibilites)) { // Pour le local, le champ n'est pas défini dans le modèle actuel // donc on ne traite pas les indisponibilités pour l'instant } } // Vérifier les événements existants pour cette date $event_model = new \ESI_CRVI_AGENDA\models\CRVI_Event_Model(); $events = $event_model->get_events_by_filters([ 'date_rdv' => $date, 'id_intervenant' => $id_intervenant, 'id_traducteur' => $id_traducteur, 'id_local' => $id_local ]); if (!empty($events)) { return false; // Il y a déjà des événements pour cette combinaison } return true; // Disponible } /** * Vérifie s'il y a des conflits d'événements pour un local donné * @param int $local_id * @param string $date_debut * @param string $date_fin * @param string $heure_debut * @param string $heure_fin * @param int|null $event_id - ID de l'événement à exclure (pour l'édition) * @return array */ private static function verifier_conflits_local($local_id, $date_debut, $date_fin, $heure_debut = null, $heure_fin = null, $event_id = null) { global $wpdb; try { $table_events = $wpdb->prefix . 'crvi_agenda'; // Vérifier que le local_id est valide if (empty($local_id) || !is_numeric($local_id)) { return []; } $where_conditions = ['id_local = %d', 'is_deleted = 0']; $where_values = [$local_id]; // Exclure l'événement en cours d'édition si spécifié if ($event_id && is_numeric($event_id)) { $where_conditions[] = 'id != %d'; $where_values[] = $event_id; } // Ajouter les conditions de date if ($date_debut && $date_fin) { $where_conditions[] = '( (date_rdv BETWEEN %s AND %s) OR (date_fin BETWEEN %s AND %s) OR (%s BETWEEN date_rdv AND date_fin) OR (%s BETWEEN date_rdv AND date_fin) )'; $where_values = array_merge($where_values, [$date_debut, $date_fin, $date_debut, $date_fin, $date_debut, $date_fin]); } // Ajouter les conditions d'heure si fournies if ($heure_debut && $heure_fin) { $where_conditions[] = '( (heure_rdv BETWEEN %s AND %s) OR (heure_fin BETWEEN %s AND %s) OR (%s BETWEEN heure_rdv AND heure_fin) OR (%s BETWEEN heure_rdv AND heure_fin) )'; $where_values = array_merge($where_values, [$heure_debut, $heure_fin, $heure_debut, $heure_fin, $heure_debut, $heure_fin]); } $where_clause = implode(' AND ', $where_conditions); $query = $wpdb->prepare( "SELECT * FROM {$table_events} WHERE {$where_clause}", $where_values ); if ($wpdb->last_error) { error_log('[CRVI] Erreur SQL lors de la vérification des conflits de local: ' . $wpdb->last_error); return []; } $results = $wpdb->get_results($query, ARRAY_A); return is_array($results) ? $results : []; } catch (\Exception $e) { error_log('[CRVI] Exception lors de la vérification des conflits de local: ' . $e->getMessage()); return []; } } /** * Vérifie la disponibilité pour un créneau horaire spécifique */ private static function check_availability_for_slot($date, $heure, $id_intervenant, $id_traducteur, $id_local) { // Vérifier les indisponibilités ponctuelles avec heure // Vérifier les indisponibilités ponctuelles de l'intervenant $intervenant = \ESI_CRVI_AGENDA\models\CRVI_Intervenant_Model::load($id_intervenant); if ($intervenant) { $indisponibilites = $intervenant->indisponibilitee_ponctuelle ?? null; if (!empty($indisponibilites)) { // Pour l'intervenant, le champ n'est pas défini dans le modèle actuel // donc on ne traite pas les indisponibilités pour l'instant } } // Vérifier les indisponibilités ponctuelles du traducteur $traducteur = \ESI_CRVI_AGENDA\models\CRVI_Traducteur_Model::load($id_traducteur); if ($traducteur) { $indisponibilites = $traducteur->indisponibilitee_ponctuelle ?? null; if (!empty($indisponibilites) && is_array($indisponibilites)) { // Le champ indisponibilitee_ponctuelle est un repeater ACF (array) foreach ($indisponibilites as $indisponibilite) { if (isset($indisponibilite['date']) && $indisponibilite['date'] === $date) { // Si pas d'heures spécifiées, toute la journée est indisponible if (empty($indisponibilite['heure_debut']) && empty($indisponibilite['heure_fin'])) { return false; } // Si heures spécifiées, vérifier si le créneau est dans la plage if (!empty($indisponibilite['heure_debut']) && !empty($indisponibilite['heure_fin'])) { if ($heure >= $indisponibilite['heure_debut'] && $heure < $indisponibilite['heure_fin']) { return false; } } } } } } // Vérifier les indisponibilités ponctuelles du local $local = \ESI_CRVI_AGENDA\models\CRVI_Local_Model::load($id_local); if ($local) { $indisponibilites = $local->indisponibilitee_ponctuelle ?? null; if (!empty($indisponibilites)) { // Pour le local, le champ n'est pas défini dans le modèle actuel // donc on ne traite pas les indisponibilités pour l'instant } } // Vérifier les événements existants pour ce créneau $event_model = new \ESI_CRVI_AGENDA\models\CRVI_Event_Model(); $events = $event_model->get_events_by_filters([ 'date_rdv' => $date, 'heure_rdv' => $heure, 'id_intervenant' => $id_intervenant, 'id_traducteur' => $id_traducteur, 'id_local' => $id_local ]); if (!empty($events)) { return false; // Il y a déjà des événements pour cette combinaison } return true; // Disponible } /** * Récupérer l'historique des 3 derniers rendez-vous d'un bénéficiaire * @param \WP_REST_Request $request * @return \WP_REST_Response|\WP_Error */ public static function get_beneficiaire_historique($request) { $beneficiaire_id = (int) $request['id']; if (!$beneficiaire_id) { return Api_Helper::json_error('ID du bénéficiaire requis', 400); } // Vérifier que le bénéficiaire existe $beneficiaire = \ESI_CRVI_AGENDA\models\CRVI_Beneficiaire_Model::load($beneficiaire_id); if (!$beneficiaire) { return Api_Helper::json_error('Bénéficiaire introuvable', 404); } // Récupérer les 3 derniers RDV avec leurs incidents $historique = CRVI_Event_Model::get_last_3_rdv_by_beneficiaire($beneficiaire_id); return Api_Helper::json_success($historique); } /** * Récupère les présences d'un événement * @param \WP_REST_Request $request * @return \WP_REST_Response|\WP_Error */ public static function get_event_presences($request) { $event_id = (int) $request['id']; if (!$event_id) { return Api_Helper::json_error('ID d\'événement requis', 400); } // Vérifier que l'événement existe $event = CRVI_Event_Model::load($event_id); if (!$event) { return Api_Helper::json_error('Événement introuvable', 404); } // Valider le type d'événement $event_type = $event->type ?? ''; $assign = isset($event->assign) ? (int)$event->assign : 0; // Bloquer les rendez-vous individuels if ($event_type === 'individuel') { return Api_Helper::json_error('Les présences ne sont pas applicables aux rendez-vous individuels', 400); } // Bloquer les permanences non attribuées if ($event_type === 'permanence' && $assign === 0) { return Api_Helper::json_error('Les présences ne sont pas applicables aux permanences non attribuées', 400); } // Seuls les rendez-vous de groupe sont acceptés if ($event_type !== 'groupe') { return Api_Helper::json_error('Cet endpoint est uniquement accessible pour les rendez-vous de groupe', 400); } // Récupérer les présences de l'événement $presences = CRVI_Presence_Model::get_presences_by_event($event_id); $statut = $event->statut ?? ''; $response = [ 'presences' => $presences, 'event_type' => $event_type, 'statut' => $statut, 'has_presence_data' => !empty($presences) ]; return Api_Helper::json_success($response); } /** * Enregistre les présences pour un rendez-vous de groupe * @param \WP_REST_Request $request * @return \WP_REST_Response|\WP_Error */ public static function save_group_presences($request) { $event_id = (int) $request['id']; if (!$event_id) { return Api_Helper::json_error('ID d\'événement requis', 400); } // Vérifier que l'événement existe $event = CRVI_Event_Model::load($event_id); if (!$event) { return Api_Helper::json_error('Événement introuvable', 404); } // Valider le type d'événement $event_type = $event->type ?? ''; $assign = isset($event->assign) ? (int)$event->assign : 0; // Bloquer les rendez-vous individuels if ($event_type === 'individuel') { return Api_Helper::json_error('Les présences ne sont pas applicables aux rendez-vous individuels', 400); } // Bloquer les permanences non attribuées if ($event_type === 'permanence' && $assign === 0) { return Api_Helper::json_error('Les présences ne sont pas applicables aux permanences non attribuées', 400); } // Seuls les rendez-vous de groupe sont acceptés if ($event_type !== 'groupe') { return Api_Helper::json_error('Cet événement n\'est pas un rendez-vous de groupe', 400); } // Récupérer les données de présences depuis le body $body = $request->get_json_params(); $presences = $body['presences'] ?? []; if (empty($presences) || !is_array($presences)) { return Api_Helper::json_error('Les données de présences sont requises', 400); } // Récupérer la langue de l'événement (convertir ID/slug en nom) $langue = $event->langue ?? ''; $langue_nom = ''; if (!empty($langue)) { // Essayer d'abord par ID numérique $langue_term = get_term((int)$langue, 'langue'); if ($langue_term && !is_wp_error($langue_term)) { $langue_nom = $langue_term->name; } else { // Essayer par slug si l'ID ne fonctionne pas $langue_term = get_term_by('slug', $langue, 'langue'); if ($langue_term && !is_wp_error($langue_term)) { $langue_nom = $langue_term->name; } else { // Si ni ID ni slug ne fonctionnent, utiliser la valeur telle quelle $langue_nom = $langue; } } } // Valider et enregistrer chaque présence $saved_count = 0; $errors = []; try { foreach ($presences as $index => $presence) { // Valider les données $nom = isset($presence['nom']) ? trim($presence['nom']) : ''; $prenom = isset($presence['prenom']) ? trim($presence['prenom']) : ''; $is_present = isset($presence['is_present']) ? (bool) $presence['is_present'] : false; // Ignorer les lignes vides (nom et prénom vides) if (empty($nom) && empty($prenom)) { continue; } // Vérifier que nom et prénom sont remplis if (empty($nom) || empty($prenom)) { $errors[] = "Ligne " . ($index + 1) . ": Le nom et le prénom sont requis"; continue; } try { // Vérifier si un beneficiaire_id est fourni (bénéficiaire existant sélectionné) $beneficiaire_id = isset($presence['beneficiaire_id']) && !empty($presence['beneficiaire_id']) ? (int) $presence['beneficiaire_id'] : null; if ($beneficiaire_id !== null) { // Utiliser l'ID du bénéficiaire existant CRVI_Presence_Model::save_presence($event_id, null, $is_present, $beneficiaire_id); } else { // Créer une nouvelle personne dans wp_crvi_agenda_persons $person_id = CRVI_Presence_Model::save_person($nom, $prenom, $langue_nom); // Enregistrer la présence avec person_id CRVI_Presence_Model::save_presence($event_id, $person_id, $is_present, null); } $saved_count++; } catch (\Exception $e) { $errors[] = "Ligne " . ($index + 1) . ": " . $e->getMessage(); } } if (!empty($errors) && $saved_count === 0) { // Toutes les présences ont échoué return Api_Helper::json_error('Erreurs lors de l\'enregistrement: ' . implode(', ', $errors), 400); } if (!empty($errors)) { // Certaines présences ont échoué mais d'autres ont réussi return Api_Helper::json_success([ 'message' => "$saved_count présence(s) enregistrée(s) avec succès", 'warnings' => $errors, 'saved_count' => $saved_count ]); } return Api_Helper::json_success([ 'message' => "$saved_count présence(s) enregistrée(s) avec succès", 'saved_count' => $saved_count ]); } catch (\Exception $e) { return Api_Helper::json_error('Erreur lors de l\'enregistrement des présences: ' . $e->getMessage(), 500); } } }