Crvi/app/controllers/TraductionLangue_Controller.php

505 lines
19 KiB
PHP

<?php
declare(strict_types=1);
namespace ESI_CRVI_AGENDA\controllers;
/**
* Contrôleur pour le CPT traduction_langue
* Gère les capacités de traduction par langue, jour et période
*/
class CRVI_TraductionLangue_Controller {
/**
* Enregistre le CPT traduction_langue et le groupe ACF associé
*/
public static function register_cpt() {
// Enregistrement du CPT traduction_langue
\register_post_type('traduction_langue', [
'label' => '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(
'<h1>Suppression impossible</h1>' .
'<p>Cette capacité de traduction ne peut pas être supprimée car elle a des événements associés.</p>' .
'<p>Vous devez d\'abord supprimer ou réassigner les événements liés à cette capacité.</p>' .
'<p><a href="' . \admin_url('edit.php?post_type=traduction_langue') . '">← Retour à la liste</a></p>',
'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()
}
}