'GET', 'callback' => [self::class, 'get_rdv_today'], 'permission_callback' => [self::class, 'check_intervenant_permission'], ], ]); // Marquage présence/absence \register_rest_route('crvi/v1', '/intervenant/mark-presence', [ [ 'methods' => 'POST', 'callback' => [self::class, 'mark_presence'], 'permission_callback' => [self::class, 'check_intervenant_permission'], ], ]); // Récupération de l'agenda \register_rest_route('crvi/v1', '/intervenant/agenda', [ [ 'methods' => 'GET', 'callback' => [self::class, 'get_agenda'], 'permission_callback' => [self::class, 'check_intervenant_permission'], ], ]); // Récupération du profil \register_rest_route('crvi/v1', '/intervenant/profile', [ [ 'methods' => 'GET', 'callback' => [self::class, 'get_profile'], 'permission_callback' => [self::class, 'check_intervenant_permission'], ], [ 'methods' => 'PUT', 'callback' => [self::class, 'update_profile'], 'permission_callback' => [self::class, 'check_intervenant_permission'], ], ]); // Vérification des conflits d'indisponibilités \register_rest_route('crvi/v1', '/intervenant/conflicts-check', [ [ 'methods' => 'GET', 'callback' => [self::class, 'check_conflicts'], 'permission_callback' => [self::class, 'check_intervenant_permission'], ], ]); } /** * Vérifie les permissions de l'intervenant * @param \WP_REST_Request $request * @return bool|\WP_Error */ public static function check_intervenant_permission($request = null) { if (!is_user_logged_in()) { return new WP_Error('unauthorized', 'Accès non autorisé', ['status' => 401]); } $user = wp_get_current_user(); // Vérifier si l'utilisateur est intervenant ou administrateur if (!in_array('intervenant', $user->roles, true) && !current_user_can('administrator')) { return new WP_Error('forbidden', 'Accès réservé aux intervenants', ['status' => 403]); } return true; } /** * Vérifie les permissions admin * @param \WP_REST_Request $request * @return bool|\WP_Error */ public static function check_admin_permission($request = null) { if (!is_user_logged_in()) { return new WP_Error('unauthorized', 'Accès non autorisé', ['status' => 401]); } if (!current_user_can('manage_options')) { return new WP_Error('forbidden', 'Accès réservé aux administrateurs', ['status' => 403]); } return true; } /** * Récupère l'ID de l'intervenant à utiliser dans le contexte * Si l'utilisateur est admin, retourne l'ID 11 (Jean Dupont) pour simulation * Sinon, retourne l'ID de l'utilisateur connecté * @return int */ private static function get_intervenant_id_for_context(): int { $user = wp_get_current_user(); // Si l'utilisateur est admin, simuler l'intervenant ID 11 (Jean Dupont) if (current_user_can('administrator')) { return 11; } // Sinon, utiliser l'ID de l'utilisateur connecté return get_current_user_id(); } /** * Récupère les rendez-vous du jour de l'intervenant connecté * GET /wp-json/crvi/v1/intervenant/rdv-today */ public static function get_rdv_today($request) { $intervenant_id = self::get_intervenant_id_for_context(); $intervenant = CRVI_Intervenant_Model::load($intervenant_id); if (!$intervenant) { return Api_Helper::json_error('Intervenant introuvable', 404); } $today = date('Y-m-d'); $model = new CRVI_Event_Model(); // Récupérer les RDV du jour pour cet intervenant $events = $model->get_events_by_filters([ 'intervenant' => $intervenant->id, 'date_debut' => $today, 'date_fin' => $today, 'statut' => 'prevu', // Uniquement les RDV prévus ]); $formatted_events = []; foreach ($events as $event) { // Charger les détails de l'événement $event_details = $model->get_details($event['id']); $formatted_events[] = [ 'id' => $event['id'], 'date_rdv' => $event['date_rdv'], 'heure_rdv' => $event['heure_rdv'], 'heure_fin' => $event['heure_fin'] ?? $event['heure_rdv'], 'statut' => $event['statut'], 'type' => $event['type'], 'beneficiaire' => $event_details->beneficiaire ?? null, 'local' => isset($event_details->local) ? [ 'id' => $event_details->local->id ?? null, 'nom' => $event_details->local->nom ?? null, ] : null, 'traducteur' => isset($event_details->traducteur) ? [ 'id' => $event_details->traducteur->id ?? null, 'nom' => $event_details->traducteur->nom ?? null, 'prenom' => $event_details->traducteur->prenom ?? null, ] : null, 'langue' => $event['langue'] ?? null, 'type_intervention' => $event['type_intervention'] ?? null, 'departement' => $event['departement'] ?? null, 'can_mark_presence' => true, // L'intervenant peut toujours marquer présence pour ses propres RDV ]; } return Api_Helper::json_success($formatted_events); } /** * Marque la présence ou l'absence pour un RDV * POST /wp-json/crvi/v1/intervenant/mark-presence */ public static function mark_presence($request) { $data = $request->get_json_params(); $event_id = isset($data['event_id']) ? (int)$data['event_id'] : 0; $statut = isset($data['statut']) ? sanitize_text_field($data['statut']) : ''; if (!$event_id || !in_array($statut, ['present', 'absent'], true)) { return Api_Helper::json_error('Paramètres invalides', 400); } $intervenant_id = self::get_intervenant_id_for_context(); $intervenant = CRVI_Intervenant_Model::load($intervenant_id); if (!$intervenant) { return Api_Helper::json_error('Intervenant introuvable', 404); } // Charger l'événement $event = CRVI_Event_Model::load($event_id); if (!$event) { return Api_Helper::json_error('Rendez-vous introuvable', 404); } // Vérifier que l'intervenant est bien propriétaire du RDV if ($event->id_intervenant != $intervenant->id) { return Api_Helper::json_error('Vous ne pouvez marquer la présence que pour vos propres rendez-vous', 403); } // Vérifier que le RDV est prévu pour aujourd'hui $today = date('Y-m-d'); if ($event->date !== $today) { return Api_Helper::json_error('Vous ne pouvez marquer la présence que pour les rendez-vous du jour', 400); } // Vérifier que le statut actuel est "prevu" if ($event->statut !== 'prevu') { return Api_Helper::json_error('Ce rendez-vous a déjà été clôturé', 400); } // Mettre à jour le statut $model = new CRVI_Event_Model(); $new_statut = $statut === 'present' ? 'present' : 'absent'; $result = $model->change_statut($event_id, ['statut' => $new_statut]); if (is_wp_error($result)) { return Api_Helper::json_error($result->get_error_message(), 500); } $message = $statut === 'present' ? 'Présence marquée avec succès' : 'Absence enregistrée'; return Api_Helper::json_success(['message' => $message]); } /** * Récupère l'agenda de l'intervenant (personnel ou collègues) * GET /wp-json/crvi/v1/intervenant/agenda */ public static function get_agenda($request) { $params = $request->get_params(); $intervenant_id = self::get_intervenant_id_for_context(); $intervenant = CRVI_Intervenant_Model::load($intervenant_id); if (!$intervenant) { return Api_Helper::json_error('Intervenant introuvable', 404); } $start = $params['start'] ?? date('Y-m-d'); $end = $params['end'] ?? date('Y-m-d', strtotime('+1 month')); $view_mode = $params['view_mode'] ?? 'mine'; // 'mine' ou 'colleagues' $filters = isset($params['filters']) ? json_decode($params['filters'], true) : []; $model = new CRVI_Event_Model(); $filters_query = [ 'date_debut' => $start, 'date_fin' => $end, ]; // Filtre par intervenant selon le mode de vue if ($view_mode === 'mine') { // Mon agenda : uniquement mes événements $filters_query['intervenant'] = $intervenant->id; } elseif ($view_mode === 'colleagues') { // Agenda des collègues : tous les événements SAUF les permanences non attribuées // On exclut les permanences avec assign = 0 (non attribuées) $filters_query['exclude_unassigned_permanences'] = true; } // Appliquer les filtres supplémentaires if (!empty($filters)) { if (isset($filters['local_id'])) { $filters_query['local'] = (int)$filters['local_id']; } if (isset($filters['beneficiaire_id'])) { $filters_query['beneficiaire'] = (int)$filters['beneficiaire_id']; } if (isset($filters['type_rdv'])) { $filters_query['type'] = sanitize_text_field($filters['type_rdv']); } if (isset($filters['type_intervention'])) { $filters_query['type_intervention'] = (int)$filters['type_intervention']; } if (isset($filters['langue'])) { $filters_query['langue'] = sanitize_text_field($filters['langue']); } if (isset($filters['permanences_non_assignees']) && $filters['permanences_non_assignees']) { // Filtrer uniquement les permanences non assignées (assign = 0) $filters_query['assign'] = 0; } } $events = $model->get_events_by_filters($filters_query); // Formater pour FullCalendar $formatted_events = []; foreach ($events as $event) { $event_details = $model->get_details($event['id']); $is_mine = isset($event['id_intervenant']) && $event['id_intervenant'] == $intervenant->id; // Déterminer le nom de l'intervenant (nécessaire pour le titre des permanences) $intervenant_nom = ''; if (!empty($event_details->intervenant)) { $intervenant_nom = ($event_details->intervenant->prenom ?? '') . ' ' . ($event_details->intervenant->nom ?? ''); $intervenant_nom = trim($intervenant_nom); } // Déterminer le titre $title = ''; if (!empty($event_details->beneficiaire)) { $title = 'RDV - ' . ($event_details->beneficiaire->prenom ?? '') . ' ' . ($event_details->beneficiaire->nom ?? ''); } elseif ($event['type'] === 'permanence') { $title = 'p. ' . ($intervenant_nom ?: 'Intervenant'); } else { $title = 'Événement'; } // Récupérer le nom de la langue depuis la taxonomie $langue_nom = null; if (!empty($event['langue'])) { // Essayer d'abord par ID numérique $langue_term = get_term((int)$event['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', $event['langue'], 'langue'); if ($langue_term && !is_wp_error($langue_term)) { $langue_nom = $langue_term->name; } } } // Récupérer le nom du département depuis les CPT $departement_nom = null; if (!empty($event['departement'])) { $departement_post = get_post((int)$event['departement']); if ($departement_post) { $departement_nom = $departement_post->post_title; } } // Récupérer le nom du type d'intervention depuis les CPT $type_intervention_nom = null; if (!empty($event['type_intervention'])) { $type_post = get_post((int)$event['type_intervention']); if ($type_post) { $type_intervention_nom = $type_post->post_title; } } $formatted_events[] = [ 'id' => $event['id'], 'title' => $title, 'start' => $event['date_rdv'] . 'T' . $event['heure_rdv'], 'end' => ($event['date_fin'] ?? $event['date_rdv']) . 'T' . ($event['heure_fin'] ?? $event['heure_rdv']), 'id_type_intervention' => $event['type_intervention'] ?? null, 'id_departement' => $event['id_departement'] ?? ($event['departement'] ?? null), 'assign' => isset($event['assign']) ? (int)$event['assign'] : 0, 'type' => $event['type'] ?? 'individuel', 'statut' => $event['statut'] ?? 'prevu', 'langue' => $langue_nom, // Utiliser le nom au lieu de l'ID/slug 'departement' => $departement_nom, // Utiliser le nom au lieu de l'ID 'type_intervention' => $type_intervention_nom, // Utiliser le nom au lieu de l'ID 'is_mine' => $is_mine, 'intervenant_nom' => $intervenant_nom, 'beneficiaire' => !empty($event_details->beneficiaire) ? ($event_details->beneficiaire->prenom ?? '') . ' ' . ($event_details->beneficiaire->nom ?? '') : null, 'local' => !empty($event_details->local) ? $event_details->local->nom : null, 'traducteur' => !empty($event_details->traducteur) ? (($event_details->traducteur->prenom ?? '') . ' ' . ($event_details->traducteur->nom ?? '')) : null, 'show_comments' => $is_mine, // Les commentaires sont visibles uniquement pour les propres RDV 'commentaire' => $is_mine ? ($event['commentaire'] ?? null) : null, ]; } return Api_Helper::json_success($formatted_events); } /** * Récupère le profil de l'intervenant connecté * GET /wp-json/crvi/v1/intervenant/profile */ public static function get_profile($request) { // Utiliser le contexte intervenant (si admin, utiliser l'ID simulé) $intervenant_id = self::get_intervenant_id_for_context(); $user = get_user_by('id', $intervenant_id); if (!$user) { return Api_Helper::json_error('Utilisateur introuvable', 404); } $intervenant = CRVI_Intervenant_Model::load($intervenant_id); if (!$intervenant) { return Api_Helper::json_error('Intervenant introuvable', 404); } // Récupérer les champs ACF $telephone = function_exists('get_field') ? get_field('telephone', 'user_' . $intervenant_id) : ''; $departements_acf = function_exists('get_field') ? get_field('departements', 'user_' . $intervenant_id) : []; $specialisations_acf = function_exists('get_field') ? get_field('specialisations', 'user_' . $intervenant_id) : []; $jours_disponibilite = function_exists('get_field') ? get_field('jours_de_disponibilite', 'user_' . $intervenant_id) : []; $indisponibilites_ponctuelles = function_exists('get_field') ? get_field('indisponibilitee_ponctuelle', 'user_' . $intervenant_id) : []; $commentaires = function_exists('get_field') ? get_field('commentaires', 'user_' . $intervenant_id) : ''; $couleur = function_exists('get_field') ? get_field('couleur', 'user_' . $intervenant_id) : ''; $heures_de_permanences = function_exists('get_field') ? get_field('heures_de_permanences', 'user_' . $intervenant_id) : []; // Formater les départements (ACF retourne des objets post_object avec return_format: "object") $departements = []; if (!empty($departements_acf)) { // Normaliser en tableau si ce n'est pas déjà le cas $departements_acf = is_array($departements_acf) ? $departements_acf : [$departements_acf]; foreach ($departements_acf as $dept) { // Si c'est un objet WP_Post, utiliser directement if (is_object($dept) && isset($dept->ID)) { $departements[] = [ 'id' => $dept->ID, 'nom' => $dept->post_title ?? '', ]; } // Si c'est un ID numérique, récupérer le post elseif (is_numeric($dept)) { $post = get_post((int)$dept); if ($post) { $departements[] = [ 'id' => $post->ID, 'nom' => $post->post_title, ]; } } } } // Formater les spécialisations (types d'intervention) - ACF retourne des objets post_object avec return_format: "object" $specialisations = []; if (!empty($specialisations_acf)) { // Normaliser en tableau si ce n'est pas déjà le cas $specialisations_acf = is_array($specialisations_acf) ? $specialisations_acf : [$specialisations_acf]; foreach ($specialisations_acf as $spec) { // Si c'est un objet WP_Post, utiliser directement if (is_object($spec) && isset($spec->ID)) { $specialisations[] = [ 'id' => $spec->ID, 'nom' => $spec->post_title ?? '', ]; } // Si c'est un ID numérique, récupérer le post elseif (is_numeric($spec)) { $post = get_post((int)$spec); if ($post) { $specialisations[] = [ 'id' => $post->ID, 'nom' => $post->post_title, ]; } } } } // Formater les indisponibilités ponctuelles // ACF retourne les dates au format d/m/Y (return_format: "d/m/Y") $indisponibilites_formatted = []; if (!empty($indisponibilites_ponctuelles) && is_array($indisponibilites_ponctuelles)) { foreach ($indisponibilites_ponctuelles as $indispo) { // Les champs ACF sont: debut, fin, type, commentaire $debut = $indispo['debut'] ?? ''; $fin = $indispo['fin'] ?? ''; // Les dates sont déjà au format d/m/Y depuis ACF // Si elles sont vides ou invalides, on les laisse telles quelles $indisponibilites_formatted[] = [ 'debut' => $debut, 'fin' => $fin ?: $debut, // Si pas de date de fin, utiliser la date de début 'type' => $indispo['type'] ?? 'conge', 'commentaire' => $indispo['commentaire'] ?? '', ]; } } return Api_Helper::json_success([ 'id' => $intervenant_id, 'nom' => $user->last_name, 'prenom' => $user->first_name, 'email' => $user->user_email, 'telephone' => $telephone ?? '', 'jours_de_disponibilite' => $jours_disponibilite ?: [], 'heures_de_permanences' => is_array($heures_de_permanences) ? $heures_de_permanences : [], 'indisponibilites_ponctuelles' => $indisponibilites_formatted, 'departements' => $departements, 'specialisations' => $specialisations, 'commentaires' => $commentaires ?? '', 'couleur' => $couleur ?? '', ]); } /** * Met à jour le profil de l'intervenant connecté * PUT /wp-json/crvi/v1/intervenant/profile */ public static function update_profile($request) { // Aligner avec le contexte intervenant (si admin, utiliser l'ID simulé) $user_id = self::get_intervenant_id_for_context(); $data = $request->get_json_params(); if (!function_exists('update_field')) { return Api_Helper::json_error('ACF n\'est pas disponible', 500); } // Map des clés ACF (selon export ACF fourni) $acf_keys = [ 'telephone' => 'field_685bf3fab0747', 'jours_de_disponibilite' => 'field_685bfebbf3ef0', 'indisponibilitee_ponctuelle' => 'field_685bffc4f02c6', 'heures_de_permanences' => 'field_69178ab1a9f6c', 'indispo_debut' => 'field_685c00077197d', 'indispo_fin' => 'field_685c00237197e', 'indispo_type' => 'field_685c00307197f', 'indispo_commentaire' => 'field_685c004271980', ]; $updated = false; $errors = []; // Debug: journaliser les données reçues (sans données sensibles) CRVI_Plugin::write_debug_file([ 'action' => 'update_profile_start', 'user_id' => $user_id, 'payload_keys' => array_keys((array)$data), ]); // Mise à jour du téléphone if (isset($data['telephone'])) { $telephone = sanitize_text_field($data['telephone']); // Idempotent: si la valeur est identique, ne pas traiter comme erreur $current_telephone = function_exists('get_field') ? get_field('telephone', 'user_' . $user_id) : ''; if ((string) $current_telephone === (string) $telephone) { $updated = true; CRVI_Plugin::write_debug_file([ 'action' => 'update_profile_telephone_no_change', 'user_id' => $user_id, 'telephone' => $telephone, ], 'DEBUG'); } else { // Utiliser la clé de champ ACF pour éviter toute ambiguïté $result = update_field($acf_keys['telephone'], $telephone, 'user_' . $user_id); if ($result !== false) { $updated = true; CRVI_Plugin::write_debug_file([ 'action' => 'update_profile_telephone_updated', 'user_id' => $user_id, 'telephone' => $telephone, 'result' => $result, ], 'INFO'); } else { $errors[] = 'Erreur lors de la mise à jour du téléphone'; CRVI_Plugin::write_debug_file([ 'action' => 'update_profile_telephone_error', 'user_id' => $user_id, 'telephone' => $telephone, 'result' => $result, ], 'ERROR'); } } } // Mise à jour des heures de permanences if (isset($data['heures_de_permanences'])) { $heures = $data['heures_de_permanences']; if (is_array($heures)) { // Valider les choix (selon ACF) $choix_valides = ['09:00','10:00','11:00','12:00','13:00','14:00','15:00','16:00']; $heures = array_values(array_filter($heures, function($h) use ($choix_valides) { return in_array($h, $choix_valides, true); })); $normalize_hours = function (array $arr): array { $filtered = array_filter(array_map('strval', $arr)); sort($filtered); return $filtered; }; $current_hours = function_exists('get_field') ? get_field('heures_de_permanences', 'user_' . $user_id) : []; $current_hours = is_array($current_hours) ? $current_hours : []; $expected_hours = $normalize_hours($heures); $actual_hours = $normalize_hours($current_hours); if ($expected_hours === $actual_hours) { $updated = true; CRVI_Plugin::write_debug_file([ 'action' => 'update_profile_heures_no_change', 'user_id' => $user_id, 'heures' => $expected_hours, ], 'DEBUG'); } else { CRVI_Plugin::write_debug_file([ 'action' => 'update_profile_heures_attempt', 'user_id' => $user_id, 'expected' => $expected_hours, 'current' => $actual_hours, ], 'INFO'); $result = update_field($acf_keys['heures_de_permanences'], $expected_hours, 'user_' . $user_id); if ($result !== false) { $updated = true; CRVI_Plugin::write_debug_file([ 'action' => 'update_profile_heures_updated', 'user_id' => $user_id, 'result' => $result, ], 'INFO'); } else { $post_update = function_exists('get_field') ? get_field('heures_de_permanences', 'user_' . $user_id) : []; $post_norm = $normalize_hours(is_array($post_update) ? $post_update : []); if ($post_norm === $expected_hours) { $updated = true; CRVI_Plugin::write_debug_file([ 'action' => 'update_profile_heures_updated_after_check', 'user_id' => $user_id, 'heures' => $post_norm, ], 'INFO'); } else { $errors[] = 'Erreur lors de la mise à jour des heures de permanences'; CRVI_Plugin::write_debug_file([ 'action' => 'update_profile_heures_error', 'user_id' => $user_id, 'expected' => $expected_hours, 'actual_after' => $post_norm, ], 'ERROR'); } } } } else { $errors[] = 'Format invalide pour les heures de permanences'; } } // Mise à jour des jours de disponibilité if (isset($data['jours_de_disponibilite'])) { $jours_disponibilite = $data['jours_de_disponibilite']; // Valider que c'est un tableau if (is_array($jours_disponibilite)) { // Valider les valeurs (doivent être parmi les jours valides) $jours_valides = ['lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi', 'dimanche']; $jours_disponibilite = array_filter($jours_disponibilite, function($jour) use ($jours_valides) { return in_array($jour, $jours_valides, true); }); // Idempotent: comparer avec la valeur actuelle avant d'appeler update_field $current_jours = function_exists('get_field') ? get_field('jours_de_disponibilite', 'user_' . $user_id) : []; $current_jours = is_array($current_jours) ? $current_jours : []; $normalize_days = function (array $days): array { $filtered = array_filter(array_map('strval', $days)); sort($filtered); return $filtered; }; $expected_days = $normalize_days(array_values($jours_disponibilite)); $actual_days = $normalize_days($current_jours); if ($expected_days === $actual_days) { $updated = true; CRVI_Plugin::write_debug_file([ 'action' => 'update_profile_jours_no_change', 'user_id' => $user_id, 'jours' => $expected_days, ], 'DEBUG'); } else { CRVI_Plugin::write_debug_file([ 'action' => 'update_profile_jours_attempt', 'user_id' => $user_id, 'expected' => $expected_days, 'current' => $actual_days, ], 'INFO'); // Utiliser la clé ACF pour les jours $result = update_field($acf_keys['jours_de_disponibilite'], $expected_days, 'user_' . $user_id); if ($result !== false) { $updated = true; CRVI_Plugin::write_debug_file([ 'action' => 'update_profile_jours_updated', 'user_id' => $user_id, 'result' => $result, ], 'INFO'); } else { // Vérifier après tentative $post_update_days = function_exists('get_field') ? get_field('jours_de_disponibilite', 'user_' . $user_id) : []; $post_update_days = is_array($post_update_days) ? $post_update_days : []; $post_norm = $normalize_days($post_update_days); if ($post_norm === $expected_days) { $updated = true; CRVI_Plugin::write_debug_file([ 'action' => 'update_profile_jours_updated_after_check', 'user_id' => $user_id, 'jours' => $post_norm, ], 'INFO'); } else { $errors[] = 'Erreur lors de la mise à jour des jours de disponibilité'; CRVI_Plugin::write_debug_file([ 'action' => 'update_profile_jours_error', 'user_id' => $user_id, 'expected' => $expected_days, 'actual_after' => $post_norm, ], 'ERROR'); } } } } else { $errors[] = 'Format invalide pour les jours de disponibilité'; } } // Mise à jour des indisponibilités ponctuelles if (isset($data['indisponibilites_ponctuelles'])) { $indisponibilites = $data['indisponibilites_ponctuelles']; // Valider que c'est un tableau if (is_array($indisponibilites)) { // Debug: payload reçu CRVI_Plugin::write_debug_file([ 'action' => 'update_profile_indispos_received', 'user_id' => $user_id, 'rows' => count($indisponibilites), 'sample' => $indisponibilites[0] ?? null, ], 'DEBUG'); // Formater chaque indisponibilité pour ACF $indisponibilites_formatted = []; // d/m/Y (pour comparaison avec get_field qui respecte return_format) $indisponibilites_update = []; // Ymd (format attendu par update_field pour date_picker) // Helper: convertit d/m/Y vers Ymd (ex: 15/07/2025 -> 20250715) $toYmd = function (string $dmy): string { $parts = explode('/', $dmy); if (count($parts) === 3) { return $parts[2] . str_pad($parts[1], 2, '0', STR_PAD_LEFT) . str_pad($parts[0], 2, '0', STR_PAD_LEFT); } // Fallback: si déjà au bon format (YYYY-MM-DD), normaliser vers Ymd if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $dmy)) { return str_replace('-', '', $dmy); } // En dernier recours, retourner tel quel return $dmy; }; foreach ($indisponibilites as $indispo) { // Valider et formater les dates (doivent être au format d/m/Y pour ACF) $debut = isset($indispo['debut']) ? sanitize_text_field($indispo['debut']) : ''; $fin = isset($indispo['fin']) ? sanitize_text_field($indispo['fin']) : ''; $type = isset($indispo['type']) ? sanitize_text_field($indispo['type']) : 'conge'; $commentaire = isset($indispo['commentaire']) ? sanitize_text_field($indispo['commentaire']) : ''; // Valider le type $types_valides = ['conge', 'absence', 'maladie']; if (!in_array($type, $types_valides, true)) { $type = 'conge'; } // Valider les dates (format d/m/Y) if (!empty($debut) && !empty($fin)) { $indisponibilites_formatted[] = [ 'debut' => $debut, 'fin' => $fin, 'type' => $type, 'commentaire' => $commentaire, ]; $indisponibilites_update[] = [ 'debut' => $toYmd($debut), 'fin' => $toYmd($fin), 'type' => $type, 'commentaire' => $commentaire, ]; } } CRVI_Plugin::write_debug_file([ 'action' => 'update_profile_indispos_formatted', 'user_id' => $user_id, 'rows' => count($indisponibilites_formatted), 'sample' => $indisponibilites_formatted[0] ?? null, 'update_rows' => count($indisponibilites_update), 'update_sample' => $indisponibilites_update[0] ?? null, ], 'DEBUG'); // Idempotent: si la valeur est identique, succès sans update $current_indispos_raw = function_exists('get_field') ? get_field('indisponibilitee_ponctuelle', 'user_' . $user_id) : null; // S'assurer que current_indispos est toujours un tableau $current_indispos = is_array($current_indispos_raw) ? $current_indispos_raw : []; // Comparaison normalisée (ignore l'ordre et clés additionnelles) $normalize_rows = function (array $rows): array { $normalized = array_map(function ($r) { $debut = $r['debut'] ?? ''; $fin = $r['fin'] ?? ''; $type = $r['type'] ?? ''; $commentaire = $r['commentaire'] ?? ''; return $debut . '|' . $fin . '|' . $type . '|' . $commentaire; }, $rows); sort($normalized); return $normalized; }; $equal_now = is_array($current_indispos) && !empty($current_indispos) ? ($normalize_rows($current_indispos) === $normalize_rows($indisponibilites_formatted)) : (empty($indisponibilites_formatted)); if ($equal_now) { $updated = true; CRVI_Plugin::write_debug_file([ 'action' => 'update_profile_indispos_no_change', 'user_id' => $user_id, 'rows' => count($indisponibilites_formatted), ], 'DEBUG'); } else { CRVI_Plugin::write_debug_file([ 'action' => 'update_profile_indispos_attempt', 'user_id' => $user_id, 'rows_expected' => count($indisponibilites_formatted), 'rows_current' => is_array($current_indispos) ? count($current_indispos) : 'non_array', 'sample_current' => is_array($current_indispos) ? ($current_indispos[0] ?? null) : null, 'update_rows' => count($indisponibilites_update), 'update_sample' => $indisponibilites_update[0] ?? null, ], 'INFO'); // IMPORTANT: update_field attend Ymd pour les champs date_picker // Construire la structure avec clés de sous-champs pour fiabiliser $rows_with_keys = array_map(function ($row) use ($acf_keys) { return [ $acf_keys['indispo_debut'] => $row['debut'], $acf_keys['indispo_fin'] => $row['fin'], $acf_keys['indispo_type'] => $row['type'], $acf_keys['indispo_commentaire'] => $row['commentaire'], ]; }, $indisponibilites_update); // Utiliser la clé du répéteur $result = update_field($acf_keys['indisponibilitee_ponctuelle'], $rows_with_keys, 'user_' . $user_id); if ($result !== false) { $updated = true; CRVI_Plugin::write_debug_file([ 'action' => 'update_profile_indispos_updated', 'user_id' => $user_id, 'result' => $result, ], 'INFO'); } else { // Vérifier la valeur réelle après tentative: si elle correspond, considérer comme succès $post_update_value_raw = function_exists('get_field') ? get_field('indisponibilitee_ponctuelle', 'user_' . $user_id) : null; // S'assurer que post_update_value est toujours un tableau $post_update_value = is_array($post_update_value_raw) ? $post_update_value_raw : []; $equal_after = is_array($post_update_value) && !empty($post_update_value) ? ($normalize_rows($post_update_value) === $normalize_rows($indisponibilites_formatted)) : (empty($indisponibilites_formatted)); if ($equal_after) { $updated = true; CRVI_Plugin::write_debug_file([ 'action' => 'update_profile_indispos_updated_after_check', 'user_id' => $user_id, 'rows' => is_array($post_update_value) ? count($post_update_value) : 'non_array', ], 'INFO'); } else { $errors[] = 'Erreur lors de la mise à jour des indisponibilités ponctuelles'; CRVI_Plugin::write_debug_file([ 'action' => 'update_profile_indispos_error', 'user_id' => $user_id, 'result' => $result, 'expected_rows' => count($indisponibilites_formatted), 'actual_rows' => is_array($post_update_value) ? count($post_update_value) : 'non_array', 'expected_sample' => $indisponibilites_formatted[0] ?? null, 'actual_sample' => is_array($post_update_value) ? ($post_update_value[0] ?? null) : null, 'expected_normalized' => $normalize_rows($indisponibilites_formatted), 'actual_normalized' => is_array($post_update_value) && !empty($post_update_value) ? $normalize_rows($post_update_value) : 'non_array', 'post_update_value_type' => gettype($post_update_value_raw), 'post_update_value_raw' => $post_update_value_raw, ], 'ERROR'); } } } } else { $errors[] = 'Format invalide pour les indisponibilités ponctuelles'; } } if (!$updated && empty($errors)) { return Api_Helper::json_error('Aucune modification valide', 400); } if (!empty($errors)) { return Api_Helper::json_error(implode(', ', $errors), 500); } // Déclencher le hook acf/save_post pour permettre les vérifications de conflit d'indisponibilités // Ce hook est utilisé par CRVI_Plugin::check_intervenant_availability_on_save() do_action('acf/save_post', 'user_' . $user_id); CRVI_Plugin::write_debug_file([ 'action' => 'update_profile_end', 'user_id' => $user_id, 'updated' => $updated, ], 'INFO'); return Api_Helper::json_success(['message' => 'Profil mis à jour avec succès']); } /** * Vérifie s'il y a des conflits d'indisponibilités pour l'intervenant * GET /wp-json/crvi/v1/intervenant/conflicts-check */ public static function check_conflicts($request) { $user_id = self::get_intervenant_id_for_context(); // Récupérer le message de conflit depuis le transient $message = get_transient('crvi_intervenant_conflicts_' . $user_id); if ($message) { // Convertir le message admin en message front-end (Bootstrap alert) $frontend_message = str_replace( '
Vous devez être connecté pour accéder à cette page.
'; } $user = wp_get_current_user(); if (!in_array('intervenant', $user->roles, true) && !current_user_can('administrator')) { return 'Accès réservé aux intervenants.
'; } // Charger le template $template_path = plugin_dir_path(__FILE__) . '../../templates/frontend/intervenant-hub.php'; if (!file_exists($template_path)) { return 'Template introuvable. Veuillez créer le fichier templates/frontend/intervenant-hub.php
'; } ob_start(); include $template_path; return ob_get_clean(); } /** * Shortcode pour l'agenda intervenant * [crvi_intervenant_agenda] */ public static function shortcode_agenda($atts) { // Vérifier les permissions if (!is_user_logged_in()) { return 'Vous devez être connecté pour accéder à cette page.
'; } $user = wp_get_current_user(); if (!in_array('intervenant', $user->roles, true) && !current_user_can('administrator')) { return 'Accès réservé aux intervenants.
'; } // Préparer les données pour les selects du modal (même logique qu'en admin) $locals = CRVI_Local_Model::get_locals([], true); $intervenants = CRVI_Intervenant_Model::get_intervenants([], true); $departements = CRVI_Departement_Model::all(true); $types_intervention = CRVI_Type_Intervention_Model::all(true); $traducteurs = CRVI_Traducteur_Model::get_traducteurs([], true); $langues_beneficiaire = Api_Helper::get_languages(true); $genres = Api_Helper::get_acf_field_options('field_685e466352755'); $genres = $genres['options'] ?? []; $types_locaux = Api_Helper::get_acf_field_options('field_685bc6db12678'); $types_locaux = $types_locaux['options'] ?? []; $jours_disponibles = Api_Helper::get_acf_field_options('field_685bdf6d66ef9'); $jours_disponibles = $jours_disponibles['options'] ?? []; // Tous les bénéficiaires pour le select principal $beneficiaire_model = new \ESI_CRVI_AGENDA\models\CRVI_Beneficiaire_Model(); $beneficiaires_objects = $beneficiaire_model->get_all_beneficiaires(); $beneficiaires = []; foreach ($beneficiaires_objects as $beneficiaire) { $beneficiaires[] = [ 'id' => $beneficiaire->id, 'nom' => $beneficiaire->nom . ' ' . $beneficiaire->prenom, ]; } // Contexte front pour le modal $crvi_agenda_context = 'front_intervenant'; $crvi_is_front_context = true; // Charger le template (les variables seront accessibles via le scope) $template_path = plugin_dir_path(__FILE__) . '../../templates/frontend/intervenant-agenda.php'; if (!file_exists($template_path)) { return 'Template introuvable. Veuillez créer le fichier templates/frontend/intervenant-agenda.php
'; } ob_start(); include $template_path; return ob_get_clean(); } /** * Shortcode pour le profil intervenant * [crvi_intervenant_profil] */ public static function shortcode_profil($atts) { // Vérifier les permissions if (!is_user_logged_in()) { return 'Vous devez être connecté pour accéder à cette page.
'; } $user = wp_get_current_user(); if (!in_array('intervenant', $user->roles, true) && !current_user_can('administrator')) { return 'Accès réservé aux intervenants.
'; } // Charger le template $template_path = plugin_dir_path(__FILE__) . '../../templates/frontend/intervenant-profile.php'; if (!file_exists($template_path)) { return 'Template introuvable. Veuillez créer le fichier templates/frontend/intervenant-profile.php
'; } ob_start(); include $template_path; return ob_get_clean(); } /** * Shortcode pour les permanences intervenant * [crvi_intervenant_permanences] */ public static function shortcode_permanences($atts) { // Vérifier les permissions if (!is_user_logged_in()) { return 'Vous devez être connecté pour accéder à cette page.
'; } $user = wp_get_current_user(); if (!in_array('intervenant', $user->roles, true) && !current_user_can('administrator')) { return 'Accès réservé aux intervenants.
'; } // Charger le template $template_path = plugin_dir_path(__FILE__) . '../../templates/frontend/intervenant-permanences.php'; if (!file_exists($template_path)) { return 'Template introuvable. Veuillez créer le fichier templates/frontend/intervenant-permanences.php
'; } ob_start(); include $template_path; return ob_get_clean(); } }