'Capacités de traduction', 'labels' => [ 'name' => 'Capacités de traduction', 'singular_name' => 'Capacité de traduction', 'add_new' => 'Ajouter une capacité', 'add_new_item' => 'Ajouter une nouvelle capacité de traduction', 'edit_item' => 'Modifier la capacité de traduction', 'new_item' => 'Nouvelle capacité de traduction', 'view_item' => 'Voir la capacité de traduction', 'search_items' => 'Rechercher une capacité', 'not_found' => 'Aucune capacité trouvée', 'not_found_in_trash' => 'Aucune capacité dans la corbeille', ], 'public' => false, // CPT non public 'show_ui' => true, // UI active dans l'admin 'show_in_menu' => false, // Menu masqué 'hierarchical' => true, // CPT hiérarchique (pour parent/enfant) 'supports' => ['title'], 'has_archive' => false, 'show_in_rest' => true, // Support REST API 'rewrite' => false, // Pas de rewrite car non public ]); // Association de la taxonomie 'langue' au CPT 'traduction_langue' // La taxonomie existe déjà (déclarée dans Traducteur_Controller) if (taxonomy_exists('langue')) { \register_taxonomy_for_object_type('langue', 'traduction_langue'); } // Création du groupe ACF capacite_traduction /* self::register_acf_field_group(); */ // Enregistrement des validations ACF \add_filter('acf/validate_value', [self::class, 'validate_acf_fields'], 10, 4); // Enregistrement du hook de suppression en cascade \add_action('before_delete_post', [self::class, 'handle_cascade_delete'], 10, 2); // Enregistrement du hook de verrouillage si événements existent \add_filter('acf/validate_save_post', [self::class, 'validate_save_post_with_events'], 10, 1); } /** * Enregistre le groupe ACF capacite_traduction avec tous les champs nécessaires */ private static function register_acf_field_group() { // Vérifier que ACF est actif if (!function_exists('acf_add_local_field_group')) { return; } // Création du groupe ACF avec tous les champs \acf_add_local_field_group([ 'key' => 'group_capacite_traduction', 'title' => 'Capacité de traduction', 'fields' => [ // Champ: langue (taxonomy) [ 'key' => 'field_capacite_langue', 'label' => 'Langue', 'name' => 'langue', 'type' => 'taxonomy', 'instructions' => 'Sélectionnez la langue pour cette capacité', 'required' => 1, 'taxonomy' => 'langue', 'field_type' => 'select', 'allow_null' => 0, 'return_format' => 'id', ], // Champ: jour (lundi → dimanche) [ 'key' => 'field_capacite_jour', 'label' => 'Jour', 'name' => 'jour', 'type' => 'select', 'instructions' => 'Sélectionnez le jour de la semaine', 'required' => 1, 'choices' => [ 'lundi' => 'Lundi', 'mardi' => 'Mardi', 'mercredi' => 'Mercredi', 'jeudi' => 'Jeudi', 'vendredi' => 'Vendredi', 'samedi' => 'Samedi', 'dimanche' => 'Dimanche', ], 'default_value' => '', 'allow_null' => 0, 'return_format' => 'value', ], // Champ: periode (matin / apres_midi / journee) [ 'key' => 'field_capacite_periode', 'label' => 'Période', 'name' => 'periode', 'type' => 'select', 'instructions' => 'Sélectionnez la période de la journée', 'required' => 1, 'choices' => [ 'matin' => 'Matin', 'apres_midi' => 'Après-midi', 'journee' => 'Journée', ], 'default_value' => '', 'allow_null' => 0, 'return_format' => 'value', ], // Champ: limite (number) [ 'key' => 'field_capacite_limite', 'label' => 'Limite', 'name' => 'limite', 'type' => 'number', 'instructions' => 'Nombre maximum de créneaux disponibles', 'required' => 1, 'default_value' => 0, 'min' => 0, 'step' => 1, ], // Champ: limite_par (semaine / mois) [ 'key' => 'field_capacite_limite_par', 'label' => 'Limite par', 'name' => 'limite_par', 'type' => 'select', 'instructions' => 'Période de calcul de la limite', 'required' => 1, 'choices' => [ 'semaine' => 'Semaine', 'mois' => 'Mois', ], 'default_value' => 'semaine', 'allow_null' => 0, 'return_format' => 'value', ], // Champ: actif (true/false) [ 'key' => 'field_capacite_actif', 'label' => 'Actif', 'name' => 'actif', 'type' => 'true_false', 'instructions' => 'Activez ou désactivez cette capacité de traduction', 'required' => 0, 'default_value' => 1, 'ui' => 1, 'ui_on_text' => 'Oui', 'ui_off_text' => 'Non', ], ], 'location' => [ [ [ 'param' => 'post_type', 'operator' => '==', 'value' => 'traduction_langue', ], ], ], 'menu_order' => 0, 'position' => 'normal', 'style' => 'default', 'label_placement' => 'top', 'instruction_placement' => 'label', 'hide_on_screen' => '', 'active' => true, 'description' => 'Configuration des capacités de traduction par langue, jour et période', ]); } /** * Valide les champs ACF avant la sauvegarde * * @param bool|string $valid État de validation (true ou message d'erreur) * @param mixed $value Valeur du champ * @param array $field Configuration du champ ACF * @param string $input Nom de l'input HTML * @return bool|string */ public static function validate_acf_fields($valid, $value, $field, $input) { // Ne valider que si c'est déjà valide et que c'est un champ de notre groupe if (!$valid || !isset($field['key']) || strpos($field['key'], 'field_capacite_') !== 0) { return $valid; } // Récupérer le post_id depuis $_POST $post_id = isset($_POST['post_ID']) ? (int) $_POST['post_ID'] : 0; // Ne pas valider pour les nouveaux posts if (!$post_id || $post_id === 0) { return $valid; } // Vérifier que c'est bien un post traduction_langue $post = \get_post($post_id); if (!$post || $post->post_type !== 'traduction_langue') { return $valid; } // Pas de validation spécifique pour le moment return $valid; } /** * Gère la suppression des capacités de traduction * - Bloque la suppression si des événements existent * * @param int $post_id ID du post à supprimer * @param \WP_Post $post Objet du post */ public static function handle_cascade_delete($post_id, $post) { // Vérifier que c'est bien un post traduction_langue if (!$post || $post->post_type !== 'traduction_langue') { return; } // Vérifier si la capacité a des événements associés avec vérification précise $has_events = self::check_events_for_capacite($post_id); if ($has_events) { // Bloquer la suppression avec un message d'erreur \wp_die( '

Suppression impossible

' . '

Cette capacité de traduction ne peut pas être supprimée car elle a des événements associés.

' . '

Vous devez d\'abord supprimer ou réassigner les événements liés à cette capacité.

' . '

← Retour à la liste

', 'Suppression bloquée', [ 'response' => 403, 'back_link' => true, ] ); } // Invalider le cache des capacités \ESI_CRVI_AGENDA\models\CRVI_TraductionLangue_Model::invalidate_cache($post_id); } /** * Vérifie si une capacité a des événements associés avec vérification précise * Utilise la même logique que countUsed() mais retourne un booléen * * @param int $capacite_id ID de la capacité * @return bool True si la capacité a des événements */ public static function check_events_for_capacite($capacite_id): bool { $capacite = \ESI_CRVI_AGENDA\models\CRVI_TraductionLangue_Model::load($capacite_id); if (!$capacite) { return false; } // Récupérer les langues (peut être un tableau) $langue_ids = $capacite->langue; if (empty($langue_ids)) { return false; } // S'assurer que c'est un tableau if (!is_array($langue_ids)) { $langue_ids = [$langue_ids]; } // Récupérer les slugs de toutes les langues $langue_slugs = []; foreach ($langue_ids as $langue_id) { $langue_term = \get_term($langue_id, 'langue'); if ($langue_term && !\is_wp_error($langue_term)) { $langue_slugs[] = $langue_term->slug; } } if (empty($langue_slugs)) { return false; } // Mapper les jours en format MySQL DAYOFWEEK $jours_map = [ 'dimanche' => 1, 'lundi' => 2, 'mardi' => 3, 'mercredi' => 4, 'jeudi' => 5, 'vendredi' => 6, 'samedi' => 7, ]; // Requête optimisée avec EXISTS global $wpdb; $table_name = $wpdb->prefix . 'crvi_agenda'; // Construire la requête WHERE avec support multi-langues $langue_placeholders = implode(',', array_fill(0, count($langue_slugs), '%s')); $where = [ "langue IN ($langue_placeholders)", "is_deleted = 0", "statut != 'annule'", "statut != 'brouillon'", ]; $values = $langue_slugs; // Filtrer par jour de la semaine si défini if (!empty($capacite->jour) && isset($jours_map[$capacite->jour])) { $where[] = "DAYOFWEEK(date_rdv) = %d"; $values[] = $jours_map[$capacite->jour]; } // Filtrer par période si défini if (!empty($capacite->periode)) { if ($capacite->periode === 'matin') { $where[] = "heure_rdv < '12:00:00'"; } elseif ($capacite->periode === 'apres_midi') { $where[] = "heure_rdv >= '12:00:00'"; } // Si période = 'journee', on ne filtre pas par heure } $where_sql = 'WHERE ' . implode(' AND ', $where); $sql = "SELECT EXISTS(SELECT 1 FROM {$table_name} {$where_sql} LIMIT 1)"; $prepared_sql = $wpdb->prepare($sql, $values); $exists = (int) $wpdb->get_var($prepared_sql); return $exists === 1; } /** * Valide la sauvegarde d'un post traduction_langue * Empêche toute modification si des événements existent déjà * * @param int $post_id ID du post en cours de sauvegarde */ public static function validate_save_post_with_events($post_id) { // Vérifier que c'est bien un post traduction_langue $post = \get_post($post_id); if (!$post || $post->post_type !== 'traduction_langue') { return; } // Ignorer pour les nouveaux posts (auto-draft) if ($post->post_status === 'auto-draft') { return; } // Ignorer si c'est une création (le post n'existait pas avant) // On vérifie si le post a été créé récemment (moins de 2 minutes) $post_date = strtotime($post->post_date); $current_time = current_time('timestamp'); $is_new_post = ($current_time - $post_date) < 120; // 2 minutes if ($is_new_post) { return; } // Vérifier si la capacité a des événements associés $has_events = self::check_events_for_capacite($post_id); if (!$has_events) { return; // Pas d'événements, on autorise la modification } // Si des événements existent, vérifier si des champs structurants ont été modifiés $fields_to_check = ['langue', 'jour', 'periode', 'limite_par']; $has_changes = false; $changed_fields = []; foreach ($fields_to_check as $field_name) { // Récupérer la valeur actuelle en BDD $current_value = \get_field($field_name, $post_id); // Récupérer la nouvelle valeur depuis $_POST $new_value = null; if ($field_name === 'langue') { // Pour le champ ACF langue (peut être multiple) $field_key = 'field_capacite_langue'; if (isset($_POST['acf'][$field_key])) { $new_value = $_POST['acf'][$field_key]; } // Normaliser les valeurs pour comparaison (toujours en tableau) if (!is_array($current_value)) { $current_value = !empty($current_value) ? [$current_value] : []; } if (!is_array($new_value)) { $new_value = !empty($new_value) ? [$new_value] : []; } // Trier pour comparaison correcte sort($current_value); sort($new_value); } else { // Pour les champs ACF standards $field_key = 'field_capacite_' . $field_name; if (isset($_POST['acf'][$field_key])) { $new_value = $_POST['acf'][$field_key]; } } // Comparer les valeurs if ($new_value !== null && $new_value != $current_value) { $has_changes = true; $changed_fields[] = $field_name; } } // Si des modifications ont été détectées sur les champs structurants if ($has_changes) { $fields_labels = [ 'langue' => 'Langue', 'jour' => 'Jour', 'periode' => 'Période', 'limite_par' => 'Limite par' ]; $changed_labels = array_map(function($field) use ($fields_labels) { return $fields_labels[$field] ?? $field; }, $changed_fields); $error_message = sprintf( 'Impossible de modifier cette capacité de traduction car elle est utilisée par des événements existants. ' . 'Champs modifiés : %s. ' . 'Vous devez d\'abord supprimer ou réassigner les événements liés.', implode(', ', $changed_labels) ); \acf_add_validation_error('', $error_message); } } /** * Enregistre la page admin personnalisée pour les capacités de traduction */ public static function register_admin_page() { \add_menu_page( 'Capacités de traduction', // Titre de la page 'Capacités traduction', // Titre du menu 'edit_posts', // Capability requise 'traduction-langues', // Slug de la page [self::class, 'render_admin_page'], // Fonction de rendu 'dashicons-translation', // Icône 30 // Position dans le menu ); } /** * Affiche la page admin personnalisée */ public static function render_admin_page() { // Récupérer toutes les capacités $parent_capacites = \get_posts([ 'post_type' => 'traduction_langue', 'posts_per_page' => -1, 'post_status' => 'any', 'orderby' => 'title', 'order' => 'ASC', ]); // Charger les scripts et styles \wp_enqueue_style( 'traduction-langue-list', \plugins_url('assets/js/dist/traduction-langue-list.min.css', dirname(__DIR__, 1)), [], '1.0.0' ); \wp_enqueue_script( 'traduction-langue-list', \plugins_url('assets/js/dist/traduction-langue-list.min.js', dirname(__DIR__, 1)), ['jquery'], '1.0.0', true ); // Afficher le template include dirname(__DIR__, 2) . '/templates/admin/traduction-langue-list.php'; } /** * Enregistre les routes REST API pour les capacités de traduction */ public static function register_routes() { // Pas de routes API nécessaires pour le moment // Cette méthode est requise par le système de Plugin::register_routes() } }