diff --git a/ESI_crvi_agenda.php b/ESI_crvi_agenda.php new file mode 100644 index 0000000..31f1b1c --- /dev/null +++ b/ESI_crvi_agenda.php @@ -0,0 +1,94 @@ + Event_Controller.php + $file = $base_dir . 'controllers/' . $class_name . '.php'; + + if (file_exists($file)) { + require_once $file; + return; + } + } + + if(strpos($relative_class, 'views\\CRVI_') === 0) { + $class_name = substr($relative_class, strlen('views\\CRVI_')); + $file = $base_dir . 'views/' . $class_name . '.php'; + + if (file_exists($file)) { + require_once $file; + return; + } + } + + if(strpos($relative_class, 'models\\CRVI_') === 0) { + $class_name = substr($relative_class, strlen('models\\CRVI_')); + $file = $base_dir . 'models/' . $class_name . '.php'; + + if (file_exists($file)) { + require_once $file; + return; + } + } + + // Fallback : essayer le mapping standard + $file = $base_dir . str_replace('\\', '/', $relative_class) . '.php'; + + if (file_exists($file)) { + require_once $file; + return; + } + } + } +}); + +// Import de la classe principale du plugin +use ESI_CRVI_AGENDA\controllers\CRVI_Plugin; + + +// Hooks d'activation/désactivation +register_activation_hook(__FILE__, ['CRVI_Plugin', 'activate']); +register_deactivation_hook(__FILE__, ['CRVI_Plugin', 'deactivate']); + +// Initialisation du plugin +/* add_action('init', function () { + + if (class_exists('ESI_CRVI_AGENDA\controllers\CRVI_Plugin')) { + CRVI_Plugin::init(); + } +}); */ + +$plugin = new CRVI_Plugin(); +$plugin->load_actions(); +$plugin->load_filters(); +$plugin->load_shortcodes(); +$plugin->load_frontend_assets(); +$plugin->load_admin_assets(); \ No newline at end of file diff --git a/README_old.md b/README_old.md new file mode 100644 index 0000000..42684a3 --- /dev/null +++ b/README_old.md @@ -0,0 +1,15 @@ +# CRVI Agenda + +Plugin WordPress pour la gestion d'agenda (CPT, ACF, FullCalendar, API export). + +## Structure +- app/controllers +- app/models +- app/libraries +- templates/front +- templates/email +- assets +- vendor + +## Installation +Copiez le dossier dans `wp-content/plugins` et activez-le depuis l'admin WordPress. \ No newline at end of file diff --git a/app/config.php b/app/config.php new file mode 100644 index 0000000..6d16c37 --- /dev/null +++ b/app/config.php @@ -0,0 +1,27 @@ + Options > Option sms + +// Anciennes variables (conservées pour compatibilité) +$sms_api_url = 'https://api.sms.com/v1/sms'; +$sms_api_key = 'your_api_key'; +$sms_api_secret = 'your_api_secret'; +$sms_api_sender = 'your_sender'; +$sms_api_sender_name = 'your_sender_name'; +$sms_api_sender_email = 'your_sender_email'; +$sms_api_sender_phone = 'your_sender_phone'; +$sms_api_sender_password = 'your_sender_password'; +$sms_api_sender_username = 'your_sender_username'; + +// Activer le bouton de debug SMS dans la modal d'édition +$debug_sms = false; \ No newline at end of file diff --git a/app/controllers/Agenda_Controller.ofd.php b/app/controllers/Agenda_Controller.ofd.php new file mode 100644 index 0000000..fcb4490 --- /dev/null +++ b/app/controllers/Agenda_Controller.ofd.php @@ -0,0 +1,71 @@ + 'GET', + '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'], + ], + ]); + } + + /** + * 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) { + $date = $request->get_param('date'); + $langue = $request->get_param('langue'); + $departement = $request->get_param('departement'); + $specialisation = $request->get_param('specialisation'); + + // Appel aux méthodes de chaque controller (à implémenter) + $traducteurs = \ESI_CRVI_AGENDA\controllers\CRVI_Traducteur_Controller::filtrer_disponibles($date, $langue); + $intervenants = \ESI_CRVI_AGENDA\controllers\CRVI_Intervenant_Controller::filtrer_disponibles($date, $departement, $specialisation); + $locaux = \ESI_CRVI_AGENDA\controllers\CRVI_Local_Controller::filtrer_disponibles($date); + + return new \WP_REST_Response([ + 'traducteurs' => $traducteurs, + 'intervenants' => $intervenants, + 'locaux' => $locaux, + ]); + } + + public static function conflits_item($request) { + $id = (int) $request['id']; + $model = new Event_Model(); + $result = $model->get_conflits($id); + return Api_Helper::json_success($result); + } + + public static function export_items($request) { + $model = new Event_Model(); + $result = $model->export_events($request->get_params()); + return Api_Helper::json_success($result); + } +} \ No newline at end of file diff --git a/app/controllers/Beneficiaire_Controller.php b/app/controllers/Beneficiaire_Controller.php new file mode 100644 index 0000000..f07ff13 --- /dev/null +++ b/app/controllers/Beneficiaire_Controller.php @@ -0,0 +1,338 @@ + 'Bénéficiaires', + 'labels' => [ + 'name' => 'Bénéficiaires', + 'singular_name' => 'Bénéficiaire', + 'add_new' => 'Ajouter un bénéficiaire', + 'add_new_item' => 'Ajouter un nouveau bénéficiaire', + 'edit_item' => 'Modifier le bénéficiaire', + 'new_item' => 'Nouveau bénéficiaire', + 'view_item' => 'Voir le bénéficiaire', + 'search_items' => 'Rechercher un bénéficiaire', + 'not_found' => 'Aucun bénéficiaire trouvé', + 'not_found_in_trash' => 'Aucun bénéficiaire dans la corbeille', + ], + 'public' => true, + 'show_in_menu' => true, + 'menu_position' => 21, + 'menu_icon' => 'dashicons-groups', + 'supports' => ['title'], + 'has_archive' => false, + 'show_in_rest' => true, + ]); + + // Déclaration de la taxonomie 'langue' (si pas déjà déclarée) + self::register_taxonomy(); + // Association de la taxonomie 'langue' au CPT 'beneficiaire' + \register_taxonomy_for_object_type('langue', 'beneficiaire'); + } + + public static function register_taxonomy() { + \register_taxonomy('langue_beneficiaire', ['beneficiaire'], [ + 'label' => 'Langues', + 'labels' => [ + 'name' => 'Langues', + 'singular_name' => 'Langue', + 'search_items' => 'Rechercher des langues', + 'all_items' => 'Toutes les langues', + 'edit_item' => 'Modifier la langue', + 'update_item' => 'Mettre à jour la langue', + 'add_new_item' => 'Ajouter une nouvelle langue', + 'new_item_name' => 'Nom de la nouvelle langue', + 'menu_name' => 'Langues', + ], + 'public' => true, + 'show_ui' => true, + 'show_in_menu' => true, + 'show_in_nav_menus' => true, + 'show_tagcloud' => false, + 'show_in_quick_edit' => true, + 'show_admin_column' => true, + 'hierarchical' => false, // comme une étiquette + 'show_in_rest' => true, + 'rewrite' => [ 'slug' => 'langue' ], + ]); + } + + public static function register_routes() { + register_rest_route('crvi/v1', '/beneficiaires', [ + [ + 'methods' => 'GET', + 'callback' => [self::class, 'get_items'], + 'permission_callback' => '__return_true', + ], + [ + 'methods' => 'POST', + 'callback' => [self::class, 'create_item'], + 'permission_callback' => [self::class, 'can_edit'], + ], + ]); + + register_rest_route('crvi/v1', '/beneficiaires/(?P\\d+)', [ + [ + 'methods' => 'GET', + 'callback' => [self::class, 'get_item'], + 'permission_callback' => '__return_true', + ], + [ + 'methods' => 'PUT,PATCH', + 'callback' => [self::class, 'update_item'], + 'permission_callback' => [self::class, 'can_edit'], + ], + [ + 'methods' => 'DELETE', + 'callback' => [self::class, 'delete_item'], + 'permission_callback' => [self::class, 'can_delete'], + ], + ]); + + // Endpoint pour récupérer les absences d'un bénéficiaire + register_rest_route('crvi/v1', '/beneficiaires/(?P\\d+)/absences', [ + [ + 'methods' => 'GET', + 'callback' => [self::class, 'get_absences'], + 'permission_callback' => '__return_true', + ], + ]); + + // Endpoint pour mettre à jour le statut liste rouge d'un bénéficiaire + register_rest_route('crvi/v1', '/beneficiaires/(?P\\d+)/liste-rouge', [ + [ + 'methods' => 'PUT,PATCH', + 'callback' => [self::class, 'update_liste_rouge'], + 'permission_callback' => [self::class, 'can_edit'], + ], + ]); + } + + public static function get_items($request) { + $model = new CRVI_Beneficiaire_Model(); + $items = $model->get_all_beneficiaires($request->get_params()); + return Api_Helper::json_success($items); + } + + public static function get_item($request) { + $id = (int) $request['id']; + $item = CRVI_Beneficiaire_Model::load($id); + if (!$item) { + return Api_Helper::json_error('Bénéficiaire introuvable', 404); + } + + // Convertir l'objet en tableau pour pouvoir ajouter des champs + $data = (array) $item; + + // Ajouter le champ ACF personne_en_liste_rouge + if (function_exists('get_field')) { + $personne_en_liste_rouge = get_field('field_69495826ac495', $id); + $data['personne_en_liste_rouge'] = $personne_en_liste_rouge; + } + + return Api_Helper::json_success($data); + } + + public static function create_item($request) { + + $data = $request->get_json_params(); + + $validation = self::validate_beneficiaire_data($data); + if (is_wp_error($validation)) { + return Api_Helper::json_error($validation->get_error_message(), 400); + } + + $model = new CRVI_Beneficiaire_Model(); + $result = $model->create($data); + if (is_wp_error($result)) { + return $result; + } + return Api_Helper::json_success([ + 'id' => $result, + 'nom' => $data['nom'], + 'prenom' => $data['prenom'] ?? '', + 'message' => 'Bénéficiaire créé avec succès' + ]); + } + + public static function update_item($request) { + $id = (int) $request['id']; + $model = new CRVI_Beneficiaire_Model(); + $result = $model->update($id, $request->get_json_params()); + if (is_wp_error($result)) { + return $result; + } + + } + + public static function delete_item($request) { + $id = (int) $request['id']; + $model = new CRVI_Beneficiaire_Model(); + $result = $model->delete($id); + if (is_wp_error($result)) { + return $result; + } + return Api_Helper::json_success(['id' => $id, 'deleted' => true]); + } + + /** + * Récupère les absences d'un bénéficiaire + * @param \WP_REST_Request $request + * @return \WP_REST_Response + */ + public static function get_absences($request) { + $id = (int) $request['id']; + + if (!$id) { + return Api_Helper::json_error('ID du bénéficiaire requis', 400); + } + + $model = new CRVI_Beneficiaire_Model(); + $absences = $model->get_absences($id); + + if (is_wp_error($absences)) { + return $absences; + } + + return Api_Helper::json_success($absences); + } + + /** + * Met à jour le statut liste rouge d'un bénéficiaire + * @param \WP_REST_Request $request + * @return \WP_REST_Response + */ + public static function update_liste_rouge($request) { + $id = (int) $request['id']; + + if (!$id) { + return Api_Helper::json_error('ID du bénéficiaire requis', 400); + } + + // Vérifier que le bénéficiaire existe + $beneficiaire = get_post($id); + if (!$beneficiaire || $beneficiaire->post_type !== 'beneficiaire') { + return Api_Helper::json_error('Bénéficiaire introuvable', 404); + } + + $data = $request->get_json_params(); + $is_liste_rouge = isset($data['liste_rouge']) ? (bool) $data['liste_rouge'] : false; + + // Mettre à jour le champ ACF personne_en_liste_rouge + if (!function_exists('update_field')) { + return Api_Helper::json_error('ACF n\'est pas disponible', 500); + } + + // Le champ checkbox ACF retourne un tableau avec "oui" si coché + $field_value = $is_liste_rouge ? ['oui'] : []; + $result = update_field('field_69495826ac495', $field_value, $id); + + if ($result === false) { + return Api_Helper::json_error('Erreur lors de la mise à jour du statut liste rouge', 500); + } + + return Api_Helper::json_success([ + 'id' => $id, + 'liste_rouge' => $is_liste_rouge, + 'message' => $is_liste_rouge ? 'Bénéficiaire ajouté à la liste rouge' : 'Bénéficiaire retiré de la liste rouge' + ]); + } + + // Permissions personnalisées + public static function can_edit() { + return current_user_can('edit_posts'); + } + public static function can_delete() { + return current_user_can('delete_posts'); + } + + public static function import_csv() { + if (!current_user_can('manage_options')) { + wp_die('Non autorisé'); + } + if (empty($_FILES['import_csv']['tmp_name'])) { + wp_redirect(admin_url('admin.php?page=crvi_agenda&import=error&msg=Fichier manquant')); + exit; + } + $file = $_FILES['import_csv']['tmp_name']; + $handle = fopen($file, 'r'); + if (!$handle) { + wp_redirect(admin_url('admin.php?page=crvi_agenda&import=error&msg=Impossible d\'ouvrir le fichier')); + exit; + } + $header = fgetcsv($handle, 0, ','); + $created = $updated = $errors = 0; + while (($row = fgetcsv($handle, 0, ',')) !== false) { + $data = array_combine($header, $row); + $result = CRVI_Beneficiaire_Model::create($data, true); + if ($result === 'created') $created++; + elseif ($result === 'updated') $updated++; + else $errors++; + } + fclose($handle); + $msg = "Créés: $created, Modifiés: $updated, Erreurs: $errors"; + wp_redirect(admin_url('admin.php?page=crvi_agenda&import=success&msg=' . urlencode($msg))); + exit; + } + + // Handler pour l'import CSV via formulaire admin + public static function import_csv_admin() { + if (!current_user_can('edit_posts')) { + wp_die('Non autorisé'); + } + check_admin_referer('crvi_import_beneficiaire'); + if (empty($_FILES['import_csv']['tmp_name'])) { + wp_redirect(admin_url('admin.php?page=crvi_agenda&import=error&msg=Fichier manquant')); + exit; + } + $file = $_FILES['import_csv']['tmp_name']; + $handle = fopen($file, 'r'); + if (!$handle) { + wp_redirect(admin_url('admin.php?page=crvi_agenda&import=error&msg=Impossible d\'ouvrir le fichier')); + exit; + } + $header = fgetcsv($handle, 0, ','); + $created = $updated = $errors = 0; + while (($row = fgetcsv($handle, 0, ',')) !== false) { + $data = array_combine($header, $row); + + //sanitize data + $data = array_combine( + array_map(function($k) { return sanitize_title($k); }, array_keys($data)), + array_map('trim', array_values($data)) + ); + + $result = CRVI_Beneficiaire_Model::create($data, false); + if ($result === 'created') $created++; + elseif ($result === 'updated') $updated++; + else $errors++; + } + fclose($handle); + $msg = "Créés: $created, Modifiés: $updated, Erreurs: $errors"; + wp_redirect(admin_url('admin.php?page=crvi_agenda&import=success&msg=' . urlencode($msg))); + exit; + } + + /** + * Validation des données de bénéficiaire + */ + public static function validate_beneficiaire_data($data) { + if (empty($data['nom'])) { + return new \WP_Error('missing_nom', 'Le nom est obligatoire'); + } + + if (!empty($data['email']) && !is_email($data['email'])) { + return new \WP_Error('invalid_email', 'L\'email n\'est pas valide'); + } + + return true; + } +} \ No newline at end of file diff --git a/app/controllers/Departement_Controller.php b/app/controllers/Departement_Controller.php new file mode 100644 index 0000000..ec3c7c9 --- /dev/null +++ b/app/controllers/Departement_Controller.php @@ -0,0 +1,103 @@ + 'Départements', + 'labels' => [ + 'name' => 'Départements', + 'singular_name' => 'Département', + 'add_new' => 'Ajouter un département', + 'add_new_item' => 'Ajouter un nouveau département', + 'edit_item' => 'Modifier le département', + 'new_item' => 'Nouveau département', + 'view_item' => 'Voir le département', + 'search_items' => 'Rechercher un département', + 'not_found' => 'Aucun département trouvé', + 'not_found_in_trash' => 'Aucun département dans la corbeille', + ], + 'public' => false, + 'show_ui' => true, + 'show_in_menu' => true, + 'menu_position' => 25, + 'supports' => ['title'], + 'capability_type' => 'post', + ]); + } + + public static function register_routes() { + register_rest_route('crvi_agenda/v1', '/departements', [ + 'methods' => 'GET', + 'callback' => [self::class, 'get_items'], + 'permission_callback' => [self::class, 'can_edit'], + ]); + + register_rest_route('crvi_agenda/v1', '/departements/(?P\d+)', [ + 'methods' => 'GET', + 'callback' => [self::class, 'get_item'], + 'permission_callback' => [self::class, 'can_edit'], + ]); + + register_rest_route('crvi_agenda/v1', '/departements', [ + 'methods' => 'POST', + 'callback' => [self::class, 'create_item'], + 'permission_callback' => [self::class, 'can_edit'], + ]); + + register_rest_route('crvi_agenda/v1', '/departements/(?P\d+)', [ + 'methods' => 'PUT', + 'callback' => [self::class, 'update_item'], + 'permission_callback' => [self::class, 'can_edit'], + ]); + + register_rest_route('crvi_agenda/v1', '/departements/(?P\d+)', [ + 'methods' => 'DELETE', + 'callback' => [self::class, 'delete_item'], + 'permission_callback' => [self::class, 'can_delete'], + ]); + } + + + //import csv + public static function import_csv_admin() { + if (!current_user_can('manage_options')) { + wp_die('Non autorisé'); + } + if (empty($_FILES['import_csv']['tmp_name'])) { + wp_redirect(admin_url('admin.php?page=crvi_agenda&import=error&msg=Fichier manquant')); + exit; + } + $file = $_FILES['import_csv']['tmp_name']; + $handle = fopen($file, 'r'); + if (!$handle) { + wp_redirect(admin_url('admin.php?page=crvi_agenda&import=error&msg=Impossible d\'ouvrir le fichier')); + exit; + } + $header = fgetcsv($handle, 0, ','); + $created = $updated = $errors = 0; + while (($row = fgetcsv($handle, 0, ',')) !== false) { + if (count($row) !== count($header)) continue; + $data = array_combine($header, $row); + + + $data = array_combine( + array_map(function($k) { return sanitize_title($k); }, array_keys($data)), + array_map('trim', array_values($data)) + ); + + + $result = CRVI_Departement_Model::create($data, false); + if ($result === 'created') $created++; + elseif ($result === 'updated') $updated++; + else $errors++; + } + fclose($handle); + $msg = "Créés: $created, Modifiés: $updated, Erreurs: $errors"; + wp_redirect(admin_url('admin.php?page=crvi_agenda&import=success&msg=' . urlencode($msg))); + exit; + } + +} \ No newline at end of file diff --git a/app/controllers/Entity_Controller.php b/app/controllers/Entity_Controller.php new file mode 100644 index 0000000..b5b7810 --- /dev/null +++ b/app/controllers/Entity_Controller.php @@ -0,0 +1,361 @@ + 'POST', + 'callback' => [CRVI_Beneficiaire_Controller::class, 'create_item'], + 'permission_callback' => [self::class, 'can_create_entity'], + ], + ]); + + // Endpoints pour la création d'intervenants + \register_rest_route('crvi/v1', '/agenda/intervenants', [ + [ + 'methods' => 'POST', + 'callback' => [CRVI_Intervenant_Controller::class, 'create_item'], + 'permission_callback' => [self::class, 'can_create_entity'], + ], + ]); + + // Endpoints pour la création de traducteurs + \register_rest_route('crvi/v1', '/agenda/traducteurs', [ + [ + 'methods' => 'POST', + 'callback' => [CRVI_Traducteur_Controller::class, 'create_item'], + 'permission_callback' => [self::class, 'can_create_entity'], + ], + ]); + + // Endpoints pour la création de locaux + \register_rest_route('crvi/v1', '/agenda/locaux', [ + [ + 'methods' => 'GET', + 'callback' => [CRVI_Local_Controller::class, 'get_items'], + 'permission_callback' => '__return_true', + ], + [ + 'methods' => 'POST', + 'callback' => [CRVI_Local_Controller::class, 'create'], + 'permission_callback' => '__return_true', // Temporairement désactivé pour les tests + ], + ]); + + // Route de test pour diagnostiquer l'authentification + \register_rest_route('crvi/v1', '/agenda/test-auth', [ + [ + 'methods' => 'GET', + 'callback' => [self::class, 'test_auth'], + 'permission_callback' => '__return_true', + ], + ]); + + // Route pour obtenir un nonce d'authentification + \register_rest_route('crvi/v1', '/agenda/get-nonce', [ + [ + 'methods' => 'GET', + 'callback' => [self::class, 'get_nonce'], + 'permission_callback' => '__return_true', + ], + ]); + } + + /** + * Vérifie si l'utilisateur peut créer des entités + * Seuls les administrateurs et opérateurs peuvent créer des entités + */ + public static function can_create_entity($request = null) { + // Log pour débogage + error_log('CRVI Auth - Checking permissions for user: ' . (is_user_logged_in() ? get_current_user_id() : 'not logged in')); + + // Vérifier d'abord l'authentification par nonce + $nonce = $request->get_header('X-WP-Nonce'); + if ($nonce && wp_verify_nonce($nonce, 'wp_rest')) { + error_log('CRVI Auth - Valid nonce provided'); + // Nonce valide, vérifier les permissions + if (current_user_can('manage_options') || current_user_can('edit_posts')) { + error_log('CRVI Auth - User has required permissions'); + return true; + } else { + error_log('CRVI Auth - User lacks required permissions'); + return new \WP_Error('insufficient_permissions', 'Permissions insuffisantes. Seuls les administrateurs et opérateurs peuvent créer des entités.', ['status' => 403]); + } + } + + // Fallback : vérifier l'authentification normale + if (!is_user_logged_in()) { + error_log('CRVI Auth - User not logged in'); + return new \WP_Error('not_logged_in', 'Utilisateur non connecté. Utilisez un nonce valide ou connectez-vous.', ['status' => 401]); + } + + $user = wp_get_current_user(); + error_log('CRVI Auth - User roles: ' . implode(', ', $user->roles)); + + // Administrateurs : accès total + if (current_user_can('manage_options')) { + error_log('CRVI Auth - Administrator access granted'); + return true; + } + + // Opérateurs : peuvent créer des entités + if (current_user_can('edit_posts')) { + error_log('CRVI Auth - Operator access granted'); + return true; + } + + // Intervenants : lecture seule (pas de création d'entités) + if (current_user_can('read')) { + error_log('CRVI Auth - Intervenant access denied (read-only)'); + return new \WP_Error('insufficient_permissions', 'Les intervenants ont un accès en lecture seule.', ['status' => 403]); + } + + error_log('CRVI Auth - No valid permissions found'); + return new \WP_Error('insufficient_permissions', 'Permissions insuffisantes.', ['status' => 403]); + } + + /** + * Test d'authentification pour diagnostiquer les problèmes + */ + public static function test_auth($request) { + $auth_info = [ + 'is_logged_in' => is_user_logged_in(), + 'current_user_id' => get_current_user_id(), + 'user_roles' => [], + 'permissions' => [ + 'manage_options' => current_user_can('manage_options'), + 'edit_posts' => current_user_can('edit_posts'), + 'read' => current_user_can('read'), + ] + ]; + + if (is_user_logged_in()) { + $user = wp_get_current_user(); + $auth_info['user_roles'] = $user->roles; + $auth_info['user_login'] = $user->user_login; + } + + return \ESI_CRVI_AGENDA\helpers\Api_Helper::json_success($auth_info); + } + + /** + * Obtient un nonce pour l'authentification REST API + */ + public static function get_nonce($request) { + $nonce = wp_create_nonce('wp_rest'); + return \ESI_CRVI_AGENDA\helpers\Api_Helper::json_success([ + 'nonce' => $nonce, + 'instructions' => 'Utilisez ce nonce dans l\'en-tête X-WP-Nonce pour les requêtes POST' + ]); + } + + /** + * Crée un nouveau bénéficiaire + */ + /* public static function create_beneficiaire($request) { + $data = $request->get_json_params(); + + // Validation des données + $validation = CRVI_Beneficiaire_Controller::validate_beneficiaire_data($data); + if (is_wp_error($validation)) { + return Api_Helper::json_error($validation->get_error_message(), 400); + } + + // Création du post bénéficiaire + $post_data = [ + 'post_title' => $data['nom'] . ' ' . ($data['prenom'] ?? ''), + 'post_type' => 'beneficiaire', + 'post_status' => 'publish', + 'meta_input' => [ + 'nom' => sanitize_text_field($data['nom']), + 'prenom' => sanitize_text_field($data['prenom'] ?? ''), + 'email' => sanitize_email($data['email'] ?? ''), + 'telephone' => sanitize_text_field($data['telephone'] ?? ''), + ] + ]; + + $post_id = wp_insert_post($post_data); + + if (is_wp_error($post_id)) { + return Api_Helper::json_error('Erreur lors de la création du bénéficiaire', 500); + } + + return Api_Helper::json_success([ + 'id' => $post_id, + 'nom' => $data['nom'], + 'prenom' => $data['prenom'] ?? '', + 'message' => 'Bénéficiaire créé avec succès' + ]); + } */ + + /** + * Crée un nouvel intervenant + */ + /* public static function create_intervenant($request) { + $data = $request->get_json_params(); + + // Validation des données + $validation = self::validate_intervenant_data($data); + if (is_wp_error($validation)) { + return Api_Helper::json_error($validation->get_error_message(), 400); + } + + // Création du post intervenant + $post_data = [ + 'post_title' => $data['nom'] . ' ' . ($data['prenom'] ?? ''), + 'post_type' => 'intervenant', + 'post_status' => 'publish', + 'meta_input' => [ + 'nom' => sanitize_text_field($data['nom']), + 'prenom' => sanitize_text_field($data['prenom'] ?? ''), + 'email' => sanitize_email($data['email'] ?? ''), + 'telephone' => sanitize_text_field($data['telephone'] ?? ''), + 'specialite' => sanitize_text_field($data['specialite'] ?? ''), + ] + ]; + + $post_id = wp_insert_post($post_data); + + if (is_wp_error($post_id)) { + return Api_Helper::json_error('Erreur lors de la création de l\'intervenant', 500); + } + + return Api_Helper::json_success([ + 'id' => $post_id, + 'nom' => $data['nom'], + 'prenom' => $data['prenom'] ?? '', + 'message' => 'Intervenant créé avec succès' + ]); + } */ + + /** + * Crée un nouveau traducteur + */ + /* public static function create_traducteur($request) { + $data = $request->get_json_params(); + + // Validation des données + $validation = self::validate_traducteur_data($data); + if (is_wp_error($validation)) { + return Api_Helper::json_error($validation->get_error_message(), 400); + } + + // Création du post traducteur + $post_data = [ + 'post_title' => $data['nom'] . ' ' . ($data['prenom'] ?? ''), + 'post_type' => 'traducteur', + 'post_status' => 'publish', + 'meta_input' => [ + 'nom' => sanitize_text_field($data['nom']), + 'prenom' => sanitize_text_field($data['prenom'] ?? ''), + 'email' => sanitize_email($data['email'] ?? ''), + 'telephone' => sanitize_text_field($data['telephone'] ?? ''), + 'langues_parlees' => sanitize_text_field($data['langues_parlees'] ?? ''), + ] + ]; + + $post_id = wp_insert_post($post_data); + + if (is_wp_error($post_id)) { + return Api_Helper::json_error('Erreur lors de la création du traducteur', 500); + } + + return Api_Helper::json_success([ + 'id' => $post_id, + 'nom' => $data['nom'], + 'prenom' => $data['prenom'] ?? '', + 'message' => 'Traducteur créé avec succès' + ]); + } */ + + /** + * Crée un nouveau local + */ + /* public static function create_local($request) { + $data = $request->get_json_params(); + + // Debug: Log les données reçues + error_log('CRVI Local Create - Data received: ' . json_encode($data)); + + // Validation des données + $validation = CRVI_Local_Controller::validate_local_data($data); + if (is_wp_error($validation)) { + error_log('CRVI Local Create - Validation error: ' . $validation->get_error_message()); + return Api_Helper::json_error($validation->get_error_message(), 400); + } + + // Utiliser le modèle pour créer le local + $model = new \ESI_CRVI_AGENDA\models\CRVI_Local_Model(); + $result = $model->create($data); + + if (is_wp_error($result)) { + error_log('CRVI Local Create - Model error: ' . $result->get_error_message()); + return Api_Helper::json_error($result->get_error_message(), $result->get_error_data()['status'] ?? 500); + } + + error_log('CRVI Local Create - Success: ' . $result); + return Api_Helper::json_success([ + 'id' => $result, + 'nom' => $data['nom'], + 'message' => 'Local créé avec succès' + ]); + } */ + + /** + * Validation des données de bénéficiaire + */ + /* private static function validate_beneficiaire_data($data) { + if (empty($data['nom'])) { + return new \WP_Error('missing_nom', 'Le nom est obligatoire'); + } + + if (!empty($data['email']) && !is_email($data['email'])) { + return new \WP_Error('invalid_email', 'L\'email n\'est pas valide'); + } + + return true; + } */ + + /** + * Validation des données d'intervenant + */ + /* private static function validate_intervenant_data($data) { + if (empty($data['nom'])) { + return new \WP_Error('missing_nom', 'Le nom est obligatoire'); + } + + if (!empty($data['email']) && !is_email($data['email'])) { + return new \WP_Error('invalid_email', 'L\'email n\'est pas valide'); + } + + return true; + } */ + + /** + * Validation des données de traducteur + */ + /* private static function validate_traducteur_data($data) { + if (empty($data['nom'])) { + return new \WP_Error('missing_nom', 'Le nom est obligatoire'); + } + + if (!empty($data['email']) && !is_email($data['email'])) { + return new \WP_Error('invalid_email', 'L\'email n\'est pas valide'); + } + + return true; + } */ + +} \ No newline at end of file diff --git a/app/controllers/Event_Controller.php b/app/controllers/Event_Controller.php new file mode 100644 index 0000000..ff360ae --- /dev/null +++ b/app/controllers/Event_Controller.php @@ -0,0 +1,2175 @@ + '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 disponibles depuis les CPT + $departements_posts = get_posts([ + 'post_type' => 'departement', + 'numberposts' => -1, + 'post_status' => 'publish', + ]); + + $departements = []; + foreach ($departements_posts as $post) { + $departements[] = [ + 'id' => $post->ID, + 'nom' => $post->post_title, + ]; + } + + // Récupérer tous les types d'intervention disponibles depuis les CPT + $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; + } + } + + // Ajouter le label de la langue pour l'affichage dans la modal + if (!empty($langue_label)) { + $event['langue_label'] = $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; + } + } + + // Ajouter le label de la langue pour l'affichage dans la modal + if (!empty($langue_label)) { + $event['langue_label'] = $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; + } + } + } + + // 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); + } + } +} \ No newline at end of file diff --git a/app/controllers/Event_controller.old.php b/app/controllers/Event_controller.old.php new file mode 100644 index 0000000..4c0ee62 --- /dev/null +++ b/app/controllers/Event_controller.old.php @@ -0,0 +1,486 @@ + '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', + ], + ]); + + \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', + ]); + + // --- 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'], + ], + ]); + \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'], + ], + ]); + } + + /** + * 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); + + $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; + $event_id = $all_params['event_id'] ?? null; + + // Appel aux méthodes de chaque controller + $traducteurs = \ESI_CRVI_AGENDA\models\CRVI_Traducteur_Model::filtrer_disponibles($date, $langue, $event_id); + $intervenants = \ESI_CRVI_AGENDA\models\CRVI_Intervenant_Model::filtrer_disponibles($date, $departement, $specialisation, $event_id); + + // Récupérer les locaux selon le type de RDV + $type_rdv = $all_params['type_rdv'] ?? null; + if ($type_rdv && in_array($type_rdv, ['individuel', 'groupe'])) { + $locaux = \ESI_CRVI_AGENDA\models\CRVI_Local_Model::get_locaux_par_type($type_rdv); + } else { + // Si pas de type spécifié, récupérer tous les locaux + $locals_posts = get_posts([ + 'post_type' => 'local', + 'numberposts' => -1, + 'post_status' => 'publish', + ]); + + $locaux = []; + foreach ($locals_posts as $local_post) { + $type_local = get_post_meta($local_post->ID, 'type_de_local', true); + $capacite = get_post_meta($local_post->ID, 'capacite', true); + + // Filtrer par type de RDV si spécifié + if ($type_rdv && in_array($type_rdv, ['individuel', 'groupe'])) { + // Logique de filtrage basée sur le type de local + if ($type_rdv === 'individuel' && $type_local === 'individuel') { + $locaux[] = [ + 'id' => $local_post->ID, + 'nom' => $local_post->post_title, + 'type_de_local' => $type_local, + 'capacite' => $capacite, + ]; + } elseif ($type_rdv === 'groupe' && $type_local === 'groupe') { + $locaux[] = [ + 'id' => $local_post->ID, + 'nom' => $local_post->post_title, + 'type_de_local' => $type_local, + 'capacite' => $capacite, + ]; + } + } else { + // Si pas de type spécifié, récupérer tous les locaux + $locaux[] = [ + 'id' => $local_post->ID, + 'nom' => $local_post->post_title, + 'type_de_local' => $type_local, + 'capacite' => $capacite, + ]; + } + } + } + + // Récupérer tous les bénéficiaires + $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, + ]; + } + + return Api_Helper::json_success([ + 'traducteurs' => $traducteurs, + 'intervenants' => $intervenants, + 'locaux' => $locaux, + 'beneficiaires' => $beneficiaires, + ]); + } + + /** + * 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 = 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); + return Api_Helper::json_success($events); + } + public static function get_event($request) { + $id = (int) $request['id']; + $event = CRVI_Event_Model::load($id); + if (!$event) { + return Api_Helper::json_error('Événement introuvable', 404); + } + 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(); + $model = new CRVI_Event_Model(); + $result = $model->update_event($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' => 'É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']); + } + + // --- 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'])); + + // Récupérer les événements pour cette période + $events = $model->get_events_by('date_rdv', null, $filters, $date_debut, $date_fin); + + // 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']); + + $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' => self::get_status_color($event['statut']), + 'borderColor' => self::get_status_color($event['statut']), + 'textColor' => $event['statut'] === 'non_tenu' ? '#000' : '#fff', + 'extendedProps' => [ + 'type' => $event['type'], + 'statut' => $event['statut'], + 'langue' => $event['langue'], + 'beneficiaire' => ($details->beneficiaire->nom ?? '') . ' ' . ($details->beneficiaire->prenom ?? ''), + 'intervenant' => ($details->intervenant->nom ?? '') . ' ' . ($details->intervenant->prenom ?? ''), + 'traducteur' => ($details->traducteur->nom ?? '') . ' ' . ($details->traducteur->prenom ?? ''), + 'local' => $details->local->nom ?? '', + 'commentaire' => $event['commentaire'], + 'id_beneficiaire' => $event['id_beneficiaire'], + 'id_intervenant' => $event['id_intervenant'], + 'id_traducteur' => $event['id_traducteur'], + 'id_local' => $event['id_local'] + ] + ]; + } + + return Api_Helper::json_success($formatted_events); + } + 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 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); + } +} \ No newline at end of file diff --git a/app/controllers/Incident_Controller.php b/app/controllers/Incident_Controller.php new file mode 100644 index 0000000..c88f635 --- /dev/null +++ b/app/controllers/Incident_Controller.php @@ -0,0 +1,196 @@ + 'GET', + 'callback' => [self::class, 'get_all_incidents'], + 'permission_callback' => [self::class, 'can_view'], + ], + [ + 'methods' => 'POST', + 'callback' => [self::class, 'create_incident'], + 'permission_callback' => [self::class, 'can_view'], + ], + ]); + + \register_rest_route('crvi/v1', '/incidents/(?P\\d+)', [ + [ + 'methods' => 'GET', + 'callback' => [self::class, 'get_incident'], + 'permission_callback' => [self::class, 'can_view'], + ], + ]); + + \register_rest_route('crvi/v1', '/events/(?P\\d+)/incidents', [ + [ + 'methods' => 'GET', + 'callback' => [self::class, 'get_incidents_by_event'], + 'permission_callback' => [self::class, 'can_view'], + ], + ]); + } + + /** + * Récupérer tous les incidents + * @param \WP_REST_Request $request + * @return \WP_REST_Response|\WP_Error + */ + public static function get_all_incidents($request) { + // Récupérer les paramètres de pagination + $page = $request->get_param('page') ?? 1; + $per_page = $request->get_param('per_page') ?? 20; + $event_id = $request->get_param('event_id') ?? null; + $beneficiaire_id = $request->get_param('beneficiaire_id') ?? null; + + // Construire les filtres + $filters = []; + if ($event_id) { + $filters['event_id'] = (int) $event_id; + } + if ($beneficiaire_id) { + $filters['beneficiaire_id'] = (int) $beneficiaire_id; + } + + // Récupérer les incidents + $incidents = CRVI_Incident_Model::get_all($filters); + + $result = []; + foreach ($incidents as $incident) { + $result[] = [ + 'id' => $incident->id, + 'beneficiaire_id' => $incident->beneficiaire_id, + 'event_id' => $incident->event_id, + 'resume_incident' => $incident->resume_incident, + 'commentaire_incident' => $incident->commentaire_incident, + 'created_at' => $incident->created_at ?? null, + ]; + } + + return Api_Helper::json_success($result); + } + + /** + * Créer un incident + * @param \WP_REST_Request $request + * @return \WP_REST_Response|\WP_Error + */ + public static function create_incident($request) { + $data = $request->get_json_params(); + + // Validation des données + if (empty($data['event_id'])) { + return Api_Helper::json_error('L\'ID de l\'événement est requis', 400); + } + + if (empty($data['beneficiaire_id'])) { + return Api_Helper::json_error('L\'ID du bénéficiaire est requis', 400); + } + + if (empty($data['resume_incident'])) { + return Api_Helper::json_error('Le résumé de l\'incident est requis', 400); + } + + // Vérifier que l'événement existe + $event = \ESI_CRVI_AGENDA\models\CRVI_Event_Model::load((int) $data['event_id']); + if (!$event) { + return Api_Helper::json_error('Événement introuvable', 404); + } + + // Vérifier que le bénéficiaire existe + $beneficiaire = \ESI_CRVI_AGENDA\models\CRVI_Beneficiaire_Model::load((int) $data['beneficiaire_id']); + if (!$beneficiaire) { + return Api_Helper::json_error('Bénéficiaire introuvable', 404); + } + + // Créer l'incident + $result = CRVI_Incident_Model::create($data); + + if (is_wp_error($result)) { + return Api_Helper::json_error($result->get_error_message(), $result->get_error_data()['status'] ?? 500); + } + + // Charger l'incident créé pour le retourner + $incident = CRVI_Incident_Model::load($result); + + return Api_Helper::json_success([ + 'id' => $incident->id, + 'beneficiaire_id' => $incident->beneficiaire_id, + 'event_id' => $incident->event_id, + 'resume_incident' => $incident->resume_incident, + 'commentaire_incident' => $incident->commentaire_incident, + 'message' => 'Incident créé avec succès' + ]); + } + + /** + * Récupérer un incident par ID + * @param \WP_REST_Request $request + * @return \WP_REST_Response|\WP_Error + */ + public static function get_incident($request) { + $id = (int) $request['id']; + $incident = CRVI_Incident_Model::load($id); + + if (!$incident) { + return Api_Helper::json_error('Incident introuvable', 404); + } + + return Api_Helper::json_success([ + 'id' => $incident->id, + 'beneficiaire_id' => $incident->beneficiaire_id, + 'event_id' => $incident->event_id, + 'resume_incident' => $incident->resume_incident, + 'commentaire_incident' => $incident->commentaire_incident, + ]); + } + + /** + * Récupérer tous les incidents pour un événement + * @param \WP_REST_Request $request + * @return \WP_REST_Response|\WP_Error + */ + public static function get_incidents_by_event($request) { + $event_id = (int) $request['event_id']; + + if (!$event_id) { + return Api_Helper::json_error('ID de l\'événement requis', 400); + } + + $incidents = CRVI_Incident_Model::get_by_event($event_id); + + $result = []; + foreach ($incidents as $incident) { + $result[] = [ + 'id' => $incident->id, + 'beneficiaire_id' => $incident->beneficiaire_id, + 'event_id' => $incident->event_id, + 'resume_incident' => $incident->resume_incident, + 'commentaire_incident' => $incident->commentaire_incident, + ]; + } + + return Api_Helper::json_success($result); + } + + /** + * Vérifie si l'utilisateur peut voir les incidents + * @return bool + */ + public static function can_view() { + return current_user_can('read'); + } +} + diff --git a/app/controllers/Intervenant_Controller.php b/app/controllers/Intervenant_Controller.php new file mode 100644 index 0000000..ef345cc --- /dev/null +++ b/app/controllers/Intervenant_Controller.php @@ -0,0 +1,466 @@ + 'Intervenants', + 'labels' => [ + 'name' => 'Intervenants', + 'singular_name' => 'Intervenant', + 'add_new' => 'Ajouter un intervenant', + 'add_new_item' => 'Ajouter un nouvel intervenant', + 'edit_item' => 'Modifier l\'intervenant', + 'new_item' => 'Nouvel intervenant', + 'view_item' => 'Voir l\'intervenant', + 'search_items' => 'Rechercher un intervenant', + 'not_found' => 'Aucun intervenant trouvé', + 'not_found_in_trash' => 'Aucun intervenant dans la corbeille', + ], + 'public' => true, + 'show_in_menu' => true, + 'menu_position' => 22, + 'menu_icon' => 'dashicons-businessman', + 'supports' => ['title'], + 'has_archive' => false, + 'show_in_rest' => true, + ]); + + self::register_taxonomy(); + + \register_taxonomy_for_object_type('departement', 'intervenant'); + \register_taxonomy_for_object_type('type_intervention', 'intervenant'); + } + + public static function register_taxonomy() { + \register_taxonomy('departement', 'intervenant', [ + 'label' => 'Départements', + 'labels' => [ + 'name' => 'Départements', + 'singular_name' => 'Département', + ], + 'show_in_rest' => true, + 'hierarchical' => true, + 'rewrite' => ['slug' => 'departement'], + 'show_admin_column' => true, + 'query_var' => true, + 'public' => true, + 'show_in_menu' => true, + ]); + + \register_taxonomy('type_intervention', 'intervenant', [ + 'label' => 'Types d\'intervention', + 'labels' => [ + 'name' => 'Types d\'intervention', + 'singular_name' => 'Type d\'intervention', + ], + 'show_in_rest' => true, + 'hierarchical' => true, + 'rewrite' => ['slug' => 'type-intervention'], + 'show_admin_column' => true, + 'query_var' => true, + 'public' => true, + 'show_in_menu' => true, + ]); + } + + /** + * Retourne les intervenants disponibles à une date donnée. + * @param string $date au format Y-m-d + * @param string|null $departement (optionnel) + * @param string|null $specialisation (optionnel) + * @return array Liste des intervenants disponibles (WP_Post) + */ + public static function filtrer_disponibles($date, $departement = null, $specialisation = null) { + if (empty($date)) { + return []; + } + $timestamp = strtotime($date); + $jour = strtolower(date('l', $timestamp)); // ex: 'monday' + $jours_fr = [ + 'monday' => 'lundi', + 'tuesday' => 'mardi', + 'wednesday' => 'mercredi', + 'thursday' => 'jeudi', + 'friday' => 'vendredi', + 'saturday' => 'samedi', + 'sunday' => 'dimanche', + ]; + $jour_semaine = $jours_fr[$jour] ?? ''; + if (!$jour_semaine) return []; + + // 1. Récupérer les intervenants (filtrage taxonomie si besoin) + $tax_query = []; + if ($departement) { + $tax_query[] = [ + 'taxonomy' => 'departement', + 'field' => 'slug', + 'terms' => $departement, + ]; + } + if ($specialisation) { + $tax_query[] = [ + 'taxonomy' => 'type_intervention', + 'field' => 'slug', + 'terms' => $specialisation, + ]; + } + $args = [ + 'post_type' => 'intervenant', + 'numberposts' => -1, + ]; + if (!empty($tax_query)) { + $args['tax_query'] = $tax_query; + } + $intervenants = get_posts($args); + $disponibles = []; + foreach ($intervenants as $intervenant) { + // Vérifier jours de disponibilité + $jours = get_field('jours_de_disponibilite', $intervenant->ID); + if (!is_array($jours) || !in_array($jour_semaine, $jours, true)) { + continue; + } + // Vérifier absences ponctuelles + $absences = get_field('indisponibilitee_ponctuelle', $intervenant->ID); + $est_absent = false; + if (is_array($absences)) { + foreach ($absences as $absence) { + $debut = isset($absence['debut']) ? strtotime($absence['debut']) : null; + $fin = isset($absence['fin']) ? strtotime($absence['fin']) : null; + if ($debut && $fin && $timestamp >= $debut && $timestamp <= $fin) { + $est_absent = true; + break; + } + } + } + if ($est_absent) continue; + $disponibles[] = $intervenant; + } + return $disponibles; + } + + public static function register_routes() { + register_rest_route('crvi/v1', '/intervenants', [ + [ + 'methods' => 'GET', + 'callback' => [self::class, 'get_items'], + 'permission_callback' => '__return_true', + ], + [ + 'methods' => 'POST', + 'callback' => [self::class, 'create_item'], + 'permission_callback' => [self::class, 'can_edit'], + ], + ]); + + register_rest_route('crvi/v1', '/intervenants/import', [ + [ + 'methods' => 'POST', + 'callback' => [self::class, 'import_csv'], + 'permission_callback' => [self::class, 'can_edit'], + 'args' => [ + 'file' => [ + 'required' => true, + 'description' => 'Fichier CSV à importer', + 'type' => 'file', + ], + ], + ], + ]); + + register_rest_route('crvi/v1', '/intervenants/(?P\\d+)', [ + [ + 'methods' => 'GET', + 'callback' => [self::class, 'get_item'], + 'permission_callback' => '__return_true', + ], + [ + 'methods' => 'PUT,PATCH', + 'callback' => [self::class, 'update_item'], + 'permission_callback' => [self::class, 'can_edit'], + ], + [ + 'methods' => 'DELETE', + 'callback' => [self::class, 'delete_item'], + 'permission_callback' => [self::class, 'can_delete'], + ], + ]); + } + + public static function get_items($request) { + $items = CRVI_Intervenant_Model::all(); + return Api_Helper::json_success($items); + } + + public static function get_item($request) { + $id = (int) $request['id']; + $item = CRVI_Intervenant_Model::load($id); + if (!$item) { + return Api_Helper::json_error('Intervenant introuvable', 404); + } + return Api_Helper::json_success($item); + } + + public static function create_item($request) { + + $data = $request->get_json_params(); + + $validation = self::validate_intervenant_data($data); + if (is_wp_error($validation)) { + return Api_Helper::json_error($validation->get_error_message(), 400); + } + + $model = new CRVI_Intervenant_Model(); + $result = $model->create($data); + if (is_wp_error($result)) { + return $result; + } + return Api_Helper::json_success([ + 'id' => $result, + 'nom' => $data['nom'], + 'prenom' => $data['prenom'] ?? '', + 'message' => 'Intervenant créé avec succès' + ]); + } + + public static function update_item($request) { + $id = (int) $request['id']; + $model = new CRVI_Intervenant_Model(); + $result = $model->update($id, $request->get_json_params()); + if (is_wp_error($result)) { + return $result; + } + return Api_Helper::json_success(['id' => $id]); + } + + public static function delete_item($request) { + $id = (int) $request['id']; + $model = new CRVI_Intervenant_Model(); + $result = $model->delete($id); + if (is_wp_error($result)) { + return $result; + } + return Api_Helper::json_success(['id' => $id, 'deleted' => true]); + } + + /** + * Import CSV d'intervenants via REST (aligné sur Traducteur_Controller) + * @param WP_REST_Request $request + * @return array + */ + public static function import_csv($request) { + if (!current_user_can('edit_posts')) { + 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); + } + $header = fgetcsv($handle, 0, ','); + $created = $updated = $errors = 0; + while (($row = fgetcsv($handle, 0, ',')) !== false) { + $data = array_combine($header, $row); + $data = array_combine( + array_map(function($k) { return sanitize_title($k); }, array_keys($data)), + array_map('trim', array_values($data)) + ); + // Conversion titres -> IDs pour departements_ids et types_intervention_ids + foreach ([ + 'departements_ids' => 'departement', + 'types_intervention_ids' => 'type_intervention', + ] as $csv_key => $cpt) { + if (!empty($data[$csv_key])) { + $titles = explode('|', $data[$csv_key]); + $ids = []; + foreach ($titles as $title) { + $title = trim($title); + if (!$title) continue; + $slug = sanitize_title($title); + $posts = get_posts([ + 'post_type' => $cpt, + 'name' => $slug, + 'posts_per_page' => 1, + 'post_status' => 'publish', + 'fields' => 'ids', + ]); + if (!empty($posts)) { + $ids[] = $posts[0]; + } + } + // Adapter la clé pour le modèle (departements, specialisations) + if ($csv_key === 'departements_ids') { + $data['departements'] = $ids; + } elseif ($csv_key === 'types_intervention_ids') { + $data['specialisations'] = $ids; + } + } + } + $result = CRVI_Intervenant_Model::create($data, false); + if (is_numeric($result)) $created++; + else $errors++; + } + fclose($handle); + $msg = "Créés: $created, Modifiés: $updated, Erreurs: $errors"; + return Api_Helper::json_success(['message' => $msg]); + } + + // Permissions personnalisées + public static function can_edit() { + $user = wp_get_current_user(); + // Intervenant : lecture seule (GET autorisé via __return_true) + if (in_array('intervenant', (array)$user->roles, true)) { + return false; + } + // Admins ou autres rôles ayant edit_posts : accès autorisé + return current_user_can('edit_posts'); + } + public static function can_delete() { + return current_user_can('delete_posts'); + } + + // Handler pour l'import CSV via formulaire admin + public static function import_csv_admin() { + if (!current_user_can('edit_users')) { + wp_die('Non autorisé'); + } + check_admin_referer('crvi_import_intervenant'); + if (empty($_FILES['import_csv']['tmp_name'])) { + wp_redirect(admin_url('admin.php?page=crvi_agenda&import=error&msg=Fichier manquant')); + exit; + } + $file = $_FILES['import_csv']['tmp_name']; + $handle = fopen($file, 'r'); + if (!$handle) { + wp_redirect(admin_url('admin.php?page=crvi_agenda&import=error&msg=Impossible d\'ouvrir le fichier')); + exit; + } + $header = fgetcsv($handle, 0, ','); + $created = $updated = $errors = 0; + while (($row = fgetcsv($handle, 0, ',')) !== false) { + $data = array_combine($header, $row); + $data = array_combine( + array_map(function($k) { return sanitize_title($k); }, array_keys($data)), + array_map('trim', array_values($data)) + ); + // Conversion titres -> IDs pour departements_ids et types_intervention_ids + foreach ([ + 'departements_ids' => 'departement', + 'types_intervention_ids' => 'type_intervention', + ] as $csv_key => $cpt) { + if (!empty($data[$csv_key])) { + $titles = explode('|', $data[$csv_key]); + $ids = []; + foreach ($titles as $title) { + $title = trim($title); + if (!$title) continue; + $slug = sanitize_title($title); + $posts = get_posts([ + 'post_type' => $cpt, + 'name' => $slug, + 'posts_per_page' => 1, + 'post_status' => 'publish', + 'fields' => 'ids', + ]); + if (!empty($posts)) { + $ids[] = $posts[0]; + } else { + // Fallback LIKE sur le slug + global $wpdb; + $like = '%' . $wpdb->esc_like($slug) . '%'; + $sql = $wpdb->prepare( + "SELECT ID FROM $wpdb->posts WHERE post_type = %s AND post_status = 'publish' AND post_name LIKE %s LIMIT 1", + $cpt, $like + ); + $like_id = $wpdb->get_var($sql); + if ($like_id) { + $ids[] = (int)$like_id; + } + } + } + if ($csv_key === 'departements_ids') { + $data['departements'] = $ids; + } elseif ($csv_key === 'types_intervention_ids') { + $data['specialisations'] = $ids; + } + } + } + $result = CRVI_Intervenant_Model::create($data, false); + if (is_numeric($result)) $created++; + else $errors++; + } + fclose($handle); + $msg = "Créés: $created, Modifiés: $updated, Erreurs: $errors"; + wp_redirect(admin_url('admin.php?page=crvi_agenda&import=success&msg=' . urlencode($msg))); + exit; + } + + /** + * Parse un champ répéteur au format pipe/point-virgule/deux-points. + * @param string $value + * @return array + */ + function parse_repeater_field(string $value): array { + if (empty($value)) return []; + $items = explode('|', $value); + $result = []; + foreach ($items as $item) { + $fields = explode(';', $item); + $assoc = []; + foreach ($fields as $field) { + [$key, $val] = array_pad(explode(':', $field, 2), 2, null); + if ($key !== null) { + $assoc[trim($key)] = trim($val ?? ''); + } + } + if ($assoc) $result[] = $assoc; + } + return $result; + } + + /** + * Permet à un intervenant de modifier uniquement ses propres événements (liés à son intervenant_id). + * @param int|null $event_intervenant_id L'ID de l'intervenant dans l'événement + * @return bool + */ + public static function can_edit_own_event($event_intervenant_id = null) { + $user = wp_get_current_user(); + + // Admins ou rôles ayant edit_posts : accès total + if (current_user_can('edit_posts')) { + return true; + } + + // Intervenant : peut modifier uniquement ses propres événements + if (in_array('intervenant', (array)$user->roles, true)) { + // L'ID de l'intervenant dans l'événement doit correspondre à l'ID de l'utilisateur connecté + if ($event_intervenant_id && (int)$event_intervenant_id === (int)$user->ID) { + return true; + } + return false; + } + + return false; + } + + public static function validate_intervenant_data($data) { + if (empty($data['nom'])) { + return new \WP_Error('missing_nom', 'Le nom est obligatoire'); + } + + if (!empty($data['email']) && !is_email($data['email'])) { + return new \WP_Error('invalid_email', 'L\'email n\'est pas valide'); + } + + return true; + } +} \ No newline at end of file diff --git a/app/controllers/Intervenant_Space_Controller.php b/app/controllers/Intervenant_Space_Controller.php new file mode 100644 index 0000000..a5082f3 --- /dev/null +++ b/app/controllers/Intervenant_Space_Controller.php @@ -0,0 +1,1097 @@ + '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( + '
', + '', + $message + ); + $frontend_message = str_replace( + '
', + '', + $frontend_message + ); + + // Supprimer le transient après lecture + delete_transient('crvi_intervenant_conflicts_' . $user_id); + + return Api_Helper::json_success([ + 'has_conflicts' => true, + 'message' => trim($frontend_message) + ]); + } + + return Api_Helper::json_success([ + 'has_conflicts' => false, + 'message' => null + ]); + } + + /** + * Shortcode pour le hub intervenant + * [crvi_intervenant_hub] + */ + public static function shortcode_hub($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-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(); + } +} + diff --git a/app/controllers/Local_Controller.php b/app/controllers/Local_Controller.php new file mode 100644 index 0000000..9c3a00c --- /dev/null +++ b/app/controllers/Local_Controller.php @@ -0,0 +1,358 @@ + 'Locaux', + 'labels' => [ + 'name' => 'Locaux', + 'singular_name' => 'Local', + 'add_new' => 'Ajouter un local', + 'add_new_item' => 'Ajouter un nouveau local', + 'edit_item' => 'Modifier le local', + 'new_item' => 'Nouveau local', + 'view_item' => 'Voir le local', + 'search_items' => 'Rechercher un local', + 'not_found' => 'Aucun local trouvé', + 'not_found_in_trash' => 'Aucun local dans la corbeille', + ], + 'public' => true, + 'show_in_menu' => true, + 'menu_position' => 23, + 'menu_icon' => 'dashicons-building', + 'supports' => ['title'], + 'has_archive' => false, + 'show_in_rest' => true, + ]); + } + + public static function register_routes() { + register_rest_route('crvi/v1', '/locaux', [ + [ + 'methods' => 'GET', + 'callback' => [self::class, 'get_items'], + 'permission_callback' => '__return_true', + ], + [ + 'methods' => 'POST', + 'callback' => [self::class, 'create_item'], + 'permission_callback' => [self::class, 'can_edit'], + ], + ]); + + register_rest_route('crvi/v1', '/locaux/disponibles', [ + [ + 'methods' => 'GET', + 'callback' => [self::class, 'filtrer_disponibles'], + 'permission_callback' => '__return_true', + 'args' => [ + 'date_debut' => [ + 'required' => false, + 'type' => 'string', + 'description' => 'Date de début (YYYY-MM-DD)', + ], + 'date_fin' => [ + 'required' => false, + 'type' => 'string', + 'description' => 'Date de fin (YYYY-MM-DD)', + ], + 'heure_debut' => [ + 'required' => false, + 'type' => 'string', + 'description' => 'Heure de début (HH:MM)', + ], + 'heure_fin' => [ + 'required' => false, + 'type' => 'string', + 'description' => 'Heure de fin (HH:MM)', + ], + ], + ], + ]); + + register_rest_route('crvi/v1', '/locaux/import', [ + [ + 'methods' => 'POST', + 'callback' => [self::class, 'import_csv'], + 'permission_callback' => [self::class, 'can_edit'], + 'args' => [ + 'file' => [ + 'required' => true, + 'description' => 'Fichier CSV à importer', + 'type' => 'file', + ], + ], + ], + ]); + + register_rest_route('crvi/v1', '/locaux/(?P\\d+)', [ + [ + 'methods' => 'GET', + 'callback' => [self::class, 'get_item'], + 'permission_callback' => '__return_true', + ], + [ + 'methods' => 'PUT,PATCH', + 'callback' => [self::class, 'update_item'], + 'permission_callback' => [self::class, 'can_edit'], + ], + [ + 'methods' => 'DELETE', + 'callback' => [self::class, 'delete_item'], + 'permission_callback' => [self::class, 'can_delete'], + ], + ]); + } + + public static function get_items($request) { + $items = CRVI_Local_Model::all(); + return Api_Helper::json_success($items); + } + + public static function get_item($request) { + $id = (int) $request['id']; + $item = CRVI_Local_Model::load($id); + if (!$item) { + return Api_Helper::json_error('Local introuvable', 404); + } + return Api_Helper::json_success($item); + } + + /** + * Filtre les locaux disponibles selon les critères fournis + * @param WP_REST_Request $request + * @return WP_REST_Response + */ + public static function filtrer_disponibles($request) { + $date_debut = $request->get_param('date_debut'); + $date_fin = $request->get_param('date_fin'); + $heure_debut = $request->get_param('heure_debut'); + $heure_fin = $request->get_param('heure_fin'); + + // Récupérer tous les locaux + $tous_locaux = CRVI_Local_Model::all(); + + // Si aucun critère de temps n'est fourni, retourner tous les locaux + if (empty($date_debut) && empty($date_fin) && empty($heure_debut) && empty($heure_fin)) { + return Api_Helper::json_success($tous_locaux); + } + + // Filtrer les locaux disponibles selon les critères + $locaux_disponibles = []; + + foreach ($tous_locaux as $local) { + $disponible = true; + + // Vérifier la disponibilité selon les critères fournis + if ($date_debut && $date_fin) { + // Vérifier s'il y a des conflits d'événements dans cette période + $conflits = self::verifier_conflits_local($local['id'], $date_debut, $date_fin, $heure_debut, $heure_fin); + if (!empty($conflits)) { + $disponible = false; + } + } + + if ($disponible) { + $locaux_disponibles[] = $local; + } + } + + return Api_Helper::json_success($locaux_disponibles); + } + + /** + * 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 + * @return array + */ + private static function verifier_conflits_local($local_id, $date_debut, $date_fin, $heure_debut = null, $heure_fin = null) { + global $wpdb; + + $table_events = $wpdb->prefix . 'crvi_agenda'; + + $where_conditions = ['local_id = %d']; + $where_values = [$local_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 + ); + + return $wpdb->get_results($query, ARRAY_A); + } + + public static function create($request) { + + $data = $request->get_json_params(); + + + $model = new CRVI_Local_Model(); + + $validation = self::validate_local_data($data); + if (is_wp_error($validation)) { + return Api_Helper::json_error($validation->get_error_message(), 400); + } + + $result = $model->create($data); + if (is_wp_error($result)) { + return $result; + } + + return Api_Helper::json_success([ + 'id' => $result, + 'nom' => $data['nom'], + 'message' => 'Local créé avec succès' + ]); + } + + public static function update_item($request) { + $id = (int) $request['id']; + $model = new CRVI_Local_Model(); + $result = $model->update($id, $request->get_json_params()); + if (is_wp_error($result)) { + return $result; + } + return Api_Helper::json_success(['id' => $id]); + } + + public static function delete_item($request) { + $id = (int) $request['id']; + $model = new CRVI_Local_Model(); + $result = $model->delete($id); + if (is_wp_error($result)) { + return $result; + } + return Api_Helper::json_success(['id' => $id, 'deleted' => true]); + } + + /** + * Import CSV de locaux via REST + * @param WP_REST_Request $request + * @return array + */ + public static function import_csv($request) { + if (!current_user_can('edit_posts')) { + 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); + } + $header = fgetcsv($handle, 0, ','); + $results = []; + $row_num = 1; + while (($row = fgetcsv($handle, 0, ',')) !== false) { + $row_num++; + $data = array_combine($header, $row); + $model = new CRVI_Local_Model(); + $result = $model->create($data); + $results[] = [ + 'ligne' => $row_num, + 'data' => $data, + 'resultat' => $result + ]; + } + fclose($handle); + return Api_Helper::json_success(['import' => $results]); + } + + // Permissions personnalisées + public static function can_edit() { + return current_user_can('edit_posts'); + } + public static function can_delete() { + return current_user_can('delete_posts'); + } + + // Handler pour l'import CSV via formulaire admin + public static function import_csv_admin() { + if (!current_user_can('edit_posts')) { + wp_die('Non autorisé'); + } + check_admin_referer('crvi_import_local'); + if (empty($_FILES['import_csv']['tmp_name'])) { + wp_redirect(admin_url('admin.php?page=crvi_agenda&import=error&msg=Fichier manquant')); + exit; + } + $file = $_FILES['import_csv']['tmp_name']; + $handle = fopen($file, 'r'); + if (!$handle) { + wp_redirect(admin_url('admin.php?page=crvi_agenda&import=error&msg=Impossible d\'ouvrir le fichier')); + exit; + } + $header = fgetcsv($handle, 0, ','); + $created = $updated = $errors = 0; + while (($row = fgetcsv($handle, 0, ',')) !== false) { + $data = array_combine($header, $row); + + $data = array_combine( + array_map(function($k) { return sanitize_title($k); }, array_keys($data)), + array_map('trim', array_values($data)) + ); + + $model = new CRVI_Local_Model(); + $result = $model->create($data); + + if (is_numeric($result)) { + $created++; + } elseif (is_wp_error($result) && $result->get_error_code() === '409') { + // Nom déjà utilisé - on pourrait mettre à jour au lieu d'ignorer + $errors++; + } else { + $errors++; + } + } + fclose($handle); + $msg = "Créés: $created, Erreurs: $errors"; + wp_redirect(admin_url('admin.php?page=crvi_agenda&import=success&msg=' . urlencode($msg))); + exit; + } + + public static function validate_local_data($data) { + if (empty($data['nom'])) { + return new \WP_Error('missing_nom', 'Le nom du local est obligatoire'); + } + if (!empty($data['capacite']) && !is_numeric($data['capacite'])) { + return new \WP_Error('invalid_capacite', 'La capacité doit être un nombre'); + } + return true; + } +} \ No newline at end of file diff --git a/app/controllers/Notifications_Controller.php b/app/controllers/Notifications_Controller.php new file mode 100644 index 0000000..1a71bba --- /dev/null +++ b/app/controllers/Notifications_Controller.php @@ -0,0 +1,561 @@ +prefix . 'crvi_sms_log'; + $charset_collate = $wpdb->get_charset_collate(); + + $sql = "CREATE TABLE {$table_name} ( + id int(11) NOT NULL AUTO_INCREMENT, + numsms varchar(255) NOT NULL, + msgsms text NOT NULL, + status varchar(50) NOT NULL, + date_sms datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + sujet varchar(50) NOT NULL, + id_event int(11) NOT NULL, + full_log longtext NOT NULL, + PRIMARY KEY (id) + ) {$charset_collate};"; + + require_once ABSPATH . 'wp-admin/includes/upgrade.php'; + dbDelta($sql); + } + + /** + * AJAX: Envoi d'un SMS de debug via admin-ajax.php + * action: crvi_send_sms_debug + * champs attendus: + * - nonce: string + * - phone: string (optionnel, défaut 0485500723) + * - message: string (si présent => envoi direct via send_sms) + * - OU champs de datas pour send_sms_rdv_cloture (langue, type, date, time, name, gender, intervenant) + * - id_event: int (optionnel pour log) + * - sujet: string (optionnel pour log) + */ + public static function ajax_send_sms_debug() { + // Vérifier que c'est une requête AJAX valide + if (!defined('DOING_AJAX') || !DOING_AJAX) { + wp_send_json_error(['message' => 'Requête invalide'], 400); + return; + } + + // Vérif nonce + $nonce = isset($_REQUEST['nonce']) ? sanitize_text_field(wp_unslash($_REQUEST['nonce'])) : ''; + if (empty($nonce) || !wp_verify_nonce($nonce, 'crvi_sms')) { + wp_send_json_error(['message' => 'Nonce invalide'], 403); + return; + } + + // Vérif permissions (réservé aux utilisateurs pouvant éditer) + if (!is_user_logged_in() || !current_user_can('edit_posts')) { + wp_send_json_error(['message' => 'Non autorisé'], 403); + return; + } + + $phone = isset($_REQUEST['phone']) ? sanitize_text_field(wp_unslash($_REQUEST['phone'])) : '0485500723'; + $message = isset($_REQUEST['message']) ? sanitize_text_field(wp_unslash($_REQUEST['message'])) : null; + $id_event = isset($_REQUEST['id_event']) ? (int) $_REQUEST['id_event'] : 0; + $sujet = isset($_REQUEST['sujet']) ? sanitize_text_field(wp_unslash($_REQUEST['sujet'])) : 'debug'; + + + + try { + if ($message) { + $result = self::sms_send($phone, $message, ['sujet' => $sujet, 'id_event' => $id_event]); + if ($result === true) { + wp_send_json_success(['sent' => true, 'via' => 'send_sms']); + } else { + // $result contient maintenant des détails sur l'erreur + $error_details = is_array($result) ? $result : ['message' => 'Échec d\'envoi (send_sms)', 'reason' => 'unknown']; + wp_send_json_error($error_details); + } + } + + // Construction des datas pour le template de clôture par défaut + $datas = [ + 'phone' => $phone, + 'langue' => isset($_REQUEST['langue']) ? sanitize_text_field(wp_unslash($_REQUEST['langue'])) : 'fr', + 'type' => isset($_REQUEST['type']) ? sanitize_text_field(wp_unslash($_REQUEST['type'])) : '', + 'date' => isset($_REQUEST['date']) ? sanitize_text_field(wp_unslash($_REQUEST['date'])) : '', + 'time' => isset($_REQUEST['time']) ? sanitize_text_field(wp_unslash($_REQUEST['time'])) : '', + 'name' => isset($_REQUEST['name']) ? sanitize_text_field(wp_unslash($_REQUEST['name'])) : '', + 'gender' => isset($_REQUEST['gender']) ? sanitize_text_field(wp_unslash($_REQUEST['gender'])) : '', + 'intervenant' => isset($_REQUEST['intervenant']) ? sanitize_text_field(wp_unslash($_REQUEST['intervenant'])) : '', + ]; + + $result = self::send_sms_rdv_cloture($datas); + if ($result === true) { + wp_send_json_success(['sent' => true, 'via' => 'send_sms_rdv_cloture', 'datas' => $datas]); + } else { + // $result contient maintenant des détails sur l'erreur + $error_details = is_array($result) ? $result : ['message' => 'Échec d\'envoi (send_sms_rdv_cloture)', 'reason' => 'unknown', 'datas' => $datas]; + wp_send_json_error($error_details); + } + } catch (\Throwable $e) { + wp_send_json_error(['message' => 'Exception: ' . $e->getMessage()], 500); + } + } + + public static function send_rdv_cloture_automatique($datas) { + + $template_path = plugin_dir_path(__FILE__) . '../../templates/email/rdv-cloture-email-template.php'; + + // Vérifier que le template existe + if (!file_exists($template_path)) { + error_log('[CRVI_AGENDA] Template email introuvable : ' . $template_path); + return false; + } + + // Exécuter le template PHP avec output buffering pour capturer le HTML généré + ob_start(); + // Passer les données au template (la variable $datas sera disponible dans le template) + include $template_path; + $body = ob_get_clean(); + + $subject = 'Rendez-vous clôturés'; + $to = 'test@test.com'; + $headers = ['Content-Type: text/html; charset=UTF-8']; + + $result = wp_mail($to, $subject, $body, $headers); + + if (!$result) { + error_log('[CRVI_AGENDA] Échec d\'envoi de l\'email de clôture automatique'); + } + + return $result; + } + + /** + * SMS de clôture de rendez-vous + * Datas attendues : phone, name, gender, date, time, type, langue, intervenant + */ + public static function send_sms_rdv_cloture($datas) { + + $langue = $datas['langue'] ?? 'fr'; + $type = $datas['type'] ?? ''; + $date = $datas['date'] ?? ''; + $time = $datas['time'] ?? ''; + $name = $datas['name'] ?? ''; + $gender = $datas['gender'] ?? ''; + $phone = $datas['phone'] ?? ''; + $intervenant = $datas['intervenant'] ?? ''; + + // 1. Récupérer le texte du SMS depuis les options ACF, + // en fonction de la langue du rendez-vous. + $sms_text = ''; + + // Priorité au répéteur "texte_sms" (texte + langue) + $texte_sms_rows = get_field('texte_sms', 'option'); + if (!empty($texte_sms_rows) && is_array($texte_sms_rows)) { + foreach ($texte_sms_rows as $row) { + $row_langue_id = $row['langue'] ?? null; + if ($row_langue_id) { + $term = get_term($row_langue_id); + if ($term && !is_wp_error($term) && $term->slug === $langue) { + $sms_text = $row['texte'] ?? ''; + break; + } + } + } + } + + // Fallback : texte_fr simple si aucun texte n'a été trouvé + if (!$sms_text) { + $sms_text = get_field('texte_fr', 'option'); + } + + // Fallback final: modèle par défaut si rien en ACF + if (!$sms_text) { + $sms_text = "Bonjour %titre% %nom%,\n\nVous avez rendez-vous le %date% à %heure%,\n\nLe crvi"; + } + + if (!$sms_text) { + // Rien de configuré : on ne tente pas d'envoyer + $error_msg = '[CRVI_AGENDA] Aucun texte SMS configuré pour la langue "' . $langue . '".'; + error_log($error_msg); + return ['message' => 'Échec d\'envoi (send_sms_rdv_cloture)', 'reason' => 'no_sms_text', 'details' => 'Aucun texte SMS configuré pour la langue "' . $langue . '"']; + } + + // 2. Déterminer le titre selon le genre (h/f/i) et la langue + $titres = [ + 'fr' => ['h' => 'Mr', 'f' => 'Mme', 'i' => ''], + 'ar' => ['h' => 'السيد', 'f' => 'السيدة', 'i' => ''], + ]; + $titre = $titres[$langue][$gender] ?? ($titres['fr'][$gender] ?? ''); + + // 3. Remplacer les placeholders dans le texte + $replacements = [ + '%titre%' => $titre, + '%nom%' => $name, + '%date%' => $date, + '%heure%' => $time, + '%intervenant%' => $intervenant, + ]; + $sms_text_final = strtr($sms_text, $replacements); + + if (empty($phone)) { + $error_msg = '[CRVI_AGENDA] Numéro de téléphone vide, SMS non envoyé.'; + error_log($error_msg); + // Log en base le skip pour traçabilité + self::log_sms([ + 'numsms' => '', + 'msgsms' => $sms_text_final, + 'status' => 'skipped_no_phone', + 'sujet' => isset($datas['sujet']) ? (string) $datas['sujet'] : 'rdv_cloture', + 'id_event' => isset($datas['id_event']) ? (int) $datas['id_event'] : 0, + 'full_log' => 'No phone' + ]); + return ['message' => 'Échec d\'envoi (send_sms_rdv_cloture)', 'reason' => 'empty_phone', 'details' => 'Numéro de téléphone vide']; + } + + // 4. Envoi via OVH en utilisant login/mot de passe définis dans les options ACF + $options = []; + if (isset($datas['sujet'])) { + $options['sujet'] = (string) $datas['sujet']; + } + if (isset($datas['id_event'])) { + $options['id_event'] = (int) $datas['id_event']; + } + $result = self::sms_send($phone, $sms_text_final, $options); + // Si sms_send retourne true, retourner true, sinon retourner les détails de l'erreur + return ($result === true) ? true : $result; + } + + /** + * Envoi générique d'un SMS via OVH HTTP2SMS + * Utilise les options ACF "option_sms" (login, mot_de_passe, quota_sms). + */ + public static function send_sms($tel, $message, $options = []) { + // Compatibilité ancien appel: si $options n'est pas un array, ignorer + if (!is_array($options)) { + $options = []; + } + + $sujet = isset($options['sujet']) ? (string) $options['sujet'] : ''; + $id_event = isset($options['id_event']) ? (int) $options['id_event'] : 0; + + // Options SMS stockées dans ACF > Options > Option > Option sms + $option_sms = get_field('option_sms', 'option'); + + $login = $option_sms['login'] ?? ''; + $password = $option_sms['mot_de_passe'] ?? ''; + // $quota_sms = isset($option_sms['quota_sms']) ? (int) $option_sms['quota_sms'] : 0; // non utilisé ici, mais disponible + + // smsAccount peut maintenant être configuré dans ACF (champ texte "compte_ovh" en options). + // Si le champ est vide ou non défini, on conserve le fallback sur la constante OVH_SMS_ACCOUNT. + $smsAccount_acf = ''; + if (function_exists('get_field')) { + // Champ texte simple en options : compte_ovh + $smsAccount_acf = (string) get_field('compte_ovh', 'option'); + + // Si rien n'est défini à la racine, on tente éventuellement dans le groupe option_sms + if (!$smsAccount_acf && is_array($option_sms) && !empty($option_sms['compte_ovh'])) { + $smsAccount_acf = (string) $option_sms['compte_ovh']; + } + } + + if (empty($login) || empty($password)) { + $error_msg = '[CRVI_AGENDA] SMS non envoyé : login ou mot de passe OVH non configuré dans ACF.'; + error_log($error_msg); + return ['message' => 'Échec d\'envoi (send_sms)', 'reason' => 'missing_credentials', 'details' => 'Login ou mot de passe OVH non configuré dans ACF']; + } + + // Normalisation du numéro (reprise de l'algorithme existant) + $tel = str_replace(['+', ' ', '-', '/'], '', $tel); + + if (substr($tel, 0, 2) === '00') { + $to_sms = $tel; // pas de changement + } elseif (substr($tel, 0, 2) === '32' || substr($tel, 0, 2) === '33') { + $to_sms = '00' . $tel; + } else { + $to_sms = '0032' . substr($tel, 1); + } + + $text_sms = urlencode(utf8_decode($message)); + + // Récupérer smsAccount et from depuis ACF / config.php, avec valeurs par défaut + // Priorité au champ texte ACF "compte_ovh" (options), puis fallback sur la constante. + $smsAccount = $smsAccount_acf ?: (\defined('OVH_SMS_ACCOUNT') ? \constant('OVH_SMS_ACCOUNT') : 'sms-xxxxxxx'); + $from = \defined('OVH_SMS_FROM') ? \constant('OVH_SMS_FROM') : '0032496390437'; + + $url = "https://www.ovh.com/cgi-bin/sms/http2sms.cgi?smsAccount=" . rawurlencode($smsAccount) + . "&login=" . rawurlencode($login) + . "&password=" . rawurlencode($password) + . "&from=" . rawurlencode($from) + . "&to=" . rawurlencode($to_sms) + . "&contentType=text/xml" + . "&message={$text_sms}"; + + $ret = @file($url); + $raw_response = ($ret !== false && isset($ret[0])) ? $ret[0] : ''; + $xml = $raw_response ? @simplexml_load_string($raw_response) : false; + + $success = false; + $status_str = 'failed'; + $error_details = []; + + if ($xml && isset($xml->status)) { + $status_code = (int) $xml->status; + $success = ($status_code === 100); + $status_str = $success ? 'success' : ('failed(' . $status_code . ')'); + + if (!$success) { + $error_details = [ + 'message' => 'Échec d\'envoi (send_sms)', + 'reason' => 'ovh_api_error', + 'status_code' => $status_code, + 'status_message' => (string) ($xml->message ?? 'Unknown error'), + 'raw_response' => $raw_response + ]; + error_log('[CRVI_AGENDA] Réponse OVH HTTP2SMS: statut ' . $status_code . ' - ' . (string) ($xml->message ?? 'Unknown error')); + } + } else { + $error_details = [ + 'message' => 'Échec d\'envoi (send_sms)', + 'reason' => 'invalid_response', + 'details' => 'Réponse OVH HTTP2SMS invalide ou vide', + 'raw_response' => $raw_response + ]; + error_log('[CRVI_AGENDA] Réponse OVH HTTP2SMS invalide ou vide. Raw response: ' . $raw_response); + } + + // Mettre à jour le quota ACF si l'envoi a réussi, en se basant sur la réponse API (creditLeft) + if ($success && $xml && isset($xml->creditLeft) && function_exists('get_field') && function_exists('update_field')) { + $creditLeftVal = (int) $xml->creditLeft; + $option_sms = get_field('option_sms', 'option'); + if (is_array($option_sms)) { + $option_sms['quota_sms'] = $creditLeftVal; + // Mettre à jour le groupe complet pour rester compatible ACF + update_field('option_sms', $option_sms, 'option'); + } + } + + // Log en base + self::log_sms([ + 'numsms' => $to_sms, + 'msgsms' => $message, + 'status' => $status_str, + 'sujet' => $sujet, + 'id_event' => $id_event, + 'full_log' => $raw_response + ]); + + // Retourner true si succès, sinon retourner les détails de l'erreur + if ($success) { + return true; + } else { + return $error_details; + } + } + + /** + * Alias demandé: utiliser sms_send comme point d'entrée standard. + * Garde la même signature et délègue à send_sms. + */ + public static function sms_send($tel, $message, $options = []) { + // Prise en charge d'un appel "cron" avec un array de données d'événement + // Exemple: sms_send(['phone'=>'...', 'name'=>'...', 'date'=>'...', 'time'=>'...', 'langue'=>'fr', 'sujet'=>'...', 'id_event'=>123], null); + if (is_array($tel)) { + $datas = $tel; + return self::send_sms_rdv_cloture($datas); + } + return self::send_sms($tel, $message, $options); + } + + /** + * Insère une ligne de log dans la table wp_crvi_sms_log + */ + private static function log_sms(array $fields) { + global $wpdb; + $table = $wpdb->prefix . 'crvi_sms_log'; + $data = [ + 'numsms' => isset($fields['numsms']) ? (string) $fields['numsms'] : '', + 'msgsms' => isset($fields['msgsms']) ? (string) $fields['msgsms'] : '', + 'status' => isset($fields['status']) ? (string) $fields['status'] : '', + 'sujet' => isset($fields['sujet']) ? (string) $fields['sujet'] : '', + 'id_event' => isset($fields['id_event']) ? (int) $fields['id_event'] : 0, + 'full_log' => isset($fields['full_log']) ? (string) $fields['full_log'] : '', + ]; + // date_sms a une valeur par défaut CURRENT_TIMESTAMP côté DB + // Sécuriser l'insertion via $wpdb->insert (prépare sous le capot) + $wpdb->insert($table, $data, ['%s','%s','%s','%s','%d','%s']); + } + + /** + * Envoie des SMS de rappel pour les événements individuels dans X jours + * + * @param int $days Nombre de jours avant l'événement (1, 2 ou 3) + * @return array Rapport détaillé avec statistiques d'envoi + */ + public static function send_sms_reminder($days = 3) { + global $wpdb; + + // Valider le paramètre days + if (!in_array($days, [1, 2, 3])) { + return [ + 'error' => 'Le paramètre days doit être 1, 2 ou 3', + 'received' => $days + ]; + } + + $table_name = $wpdb->prefix . 'crvi_agenda'; + + // Calculer la date cible (aujourd'hui + X jours) + $target_date = date('Y-m-d', strtotime("+{$days} days")); + + // Récupérer les événements individuels à la date cible avec statut 'prevu' + $sql = $wpdb->prepare( + "SELECT * FROM $table_name + WHERE date_rdv = %s + AND type = 'individuel' + AND statut = 'prevu' + AND is_deleted = 0 + AND id_beneficiaire IS NOT NULL + ORDER BY heure_rdv ASC", + $target_date + ); + + $events = $wpdb->get_results($sql, ARRAY_A); + + if (empty($events)) { + return [ + 'success' => true, + 'count' => 0, + 'date' => $target_date, + 'days' => $days, + 'message' => "Aucun événement individuel trouvé pour dans {$days} jour(s) ({$target_date})" + ]; + } + + $results = [ + 'success' => true, + 'date' => $target_date, + 'days' => $days, + 'total_events' => count($events), + 'sms_sent' => 0, + 'sms_failed' => 0, + 'sms_skipped' => 0, + 'details' => [] + ]; + + // Traiter chaque événement + foreach ($events as $event) { + $event_result = [ + 'event_id' => (int) $event['id'], + 'date' => $event['date_rdv'], + 'heure' => $event['heure_rdv'], + 'status' => 'skipped', + 'reason' => '' + ]; + + // Charger le bénéficiaire + if (empty($event['id_beneficiaire'])) { + $event_result['reason'] = 'Pas de bénéficiaire associé'; + $results['sms_skipped']++; + $results['details'][] = $event_result; + continue; + } + + $beneficiaire = \ESI_CRVI_AGENDA\models\CRVI_Beneficiaire_Model::load($event['id_beneficiaire']); + if (!$beneficiaire) { + $event_result['reason'] = 'Bénéficiaire introuvable'; + $results['sms_skipped']++; + $results['details'][] = $event_result; + continue; + } + + // Vérifier que le bénéficiaire a un téléphone + $telephone = $beneficiaire->telephone ?? ''; + if (empty($telephone)) { + $event_result['reason'] = 'Pas de numéro de téléphone pour le bénéficiaire'; + $results['sms_skipped']++; + $results['details'][] = $event_result; + continue; + } + + // Récupérer le genre du bénéficiaire depuis ACF + $genre = ''; + if (function_exists('get_field')) { + $genre = get_field('genre', $beneficiaire->id) ?? ''; + } + + // Récupérer la langue de l'événement + $langue = $event['langue'] ?? 'fr'; + // Si la langue est un ID de taxonomy, récupérer le slug + if (is_numeric($langue) && function_exists('get_term')) { + $langue_term = get_term((int) $langue, 'langue'); + if ($langue_term && !is_wp_error($langue_term)) { + $langue = $langue_term->slug ?? 'fr'; + } + } + + // Récupérer le nom de l'intervenant + $intervenant_nom = ''; + if (!empty($event['id_intervenant'])) { + $intervenant = \ESI_CRVI_AGENDA\models\CRVI_Intervenant_Model::load($event['id_intervenant'], ['id', 'nom', 'prenom']); + if ($intervenant) { + $intervenant_nom = trim(($intervenant->nom ?? '') . ' ' . ($intervenant->prenom ?? '')); + } + } + + // Formater la date et l'heure + $date_formatted = date('d/m/Y', strtotime($event['date_rdv'])); + $heure_formatted = date('H:i', strtotime($event['heure_rdv'])); + + // Préparer les données pour l'envoi de SMS + $sms_data = [ + 'phone' => $telephone, + 'name' => trim(($beneficiaire->nom ?? '') . ' ' . ($beneficiaire->prenom ?? '')), + 'gender' => $genre, // h, f, ou i + 'date' => $date_formatted, + 'time' => $heure_formatted, + 'type' => $event['type'], + 'langue' => $langue, + 'intervenant' => $intervenant_nom, + 'sujet' => 'rappel_rdv', + 'id_event' => (int) $event['id'] + ]; + + // Envoyer le SMS + $sms_result = self::send_sms_rdv_cloture($sms_data); + + if ($sms_result === true) { + $event_result['status'] = 'sent'; + $event_result['beneficiaire'] = $sms_data['name']; + $event_result['telephone'] = $telephone; + $results['sms_sent']++; + } else { + $event_result['status'] = 'failed'; + $event_result['reason'] = is_array($sms_result) ? ($sms_result['message'] ?? 'Erreur inconnue') : 'Erreur lors de l\'envoi'; + $event_result['beneficiaire'] = $sms_data['name']; + $event_result['telephone'] = $telephone; + $results['sms_failed']++; + } + + $results['details'][] = $event_result; + } + + return $results; + } +} diff --git a/app/controllers/Plugin - Copie.php b/app/controllers/Plugin - Copie.php new file mode 100644 index 0000000..97de02c --- /dev/null +++ b/app/controllers/Plugin - Copie.php @@ -0,0 +1,301 @@ +getMessage()."\n"); + } + } else { + error_log("Classe {$controller} non trouvée lors de l'enregistrement des routes\n"); + } + } */ + CRVI_Local_Controller::register_routes(); + CRVI_Beneficiaire_Controller::register_routes(); + CRVI_Traducteur_Controller::register_routes(); + CRVI_Intervenant_Controller::register_routes(); + CRVI_Departement_Controller::register_routes(); + + CRVI_Event_Controller::register_routes(); + CRVI_Entity_Controller::register_routes(); + } + + public function load_actions() { + add_action('init', [self::class, 'register_cpt']); + add_action('init', [self::class, 'register_routes']); // Lancer register_routes en même temps que register_cpt + add_action('admin_menu', [self::class, 'add_admin_menu']); + add_action('admin_post_crvi_import_beneficiaire', [\ESI_CRVI_AGENDA\controllers\CRVI_Beneficiaire_Controller::class, 'import_csv_admin']); + add_action('admin_post_crvi_import_local', [\ESI_CRVI_AGENDA\controllers\CRVI_Local_Controller::class, 'import_csv_admin']); + add_action('admin_post_crvi_import_traducteur', [\ESI_CRVI_AGENDA\controllers\CRVI_Traducteur_Controller::class, 'import_csv_admin']); + add_action('admin_post_crvi_import_intervenant', [\ESI_CRVI_AGENDA\controllers\CRVI_Intervenant_Controller::class, 'import_csv_admin']); + add_action('admin_post_crvi_import_departement', [\ESI_CRVI_AGENDA\controllers\CRVI_Departement_Controller::class, 'import_csv_admin']); + add_action('admin_post_crvi_import_type_intervention', [\ESI_CRVI_AGENDA\controllers\CRVI_Type_Intervention_Controller::class, 'import_csv_admin']); + // Enqueue assets conditionnellement + add_action('admin_enqueue_scripts', [self::class, 'enqueue_admin_assets']); + add_action('wp_enqueue_scripts', [self::class, 'enqueue_front_assets']); + } + + /** + * Ajoute la page d'admin Agenda CRVI avec sous-menus Hub, Stats, Agenda + */ + public static function add_admin_menu() { + // Menu principal : Agenda CRVI (affiche le Hub) + add_menu_page( + __('Agenda CRVI', 'esi_crvi_agenda'), + __('Agenda CRVI', 'esi_crvi_agenda'), + 'manage_options', + 'crvi_agenda', + [CRVI_Main_View::class, 'render_hub_admin_page'], + 'dashicons-calendar-alt', + 6 + ); + + // Sous-menu : Hub (même callback que le menu principal) + add_submenu_page( + 'crvi_agenda', + __('Hub', 'esi_crvi_agenda'), + __('Hub', 'esi_crvi_agenda'), + 'manage_options', + 'crvi_agenda', + [CRVI_Main_View::class, 'render_hub_admin_page'] + ); + + // Sous-menu : Stats + add_submenu_page( + 'crvi_agenda', + __('Stats', 'esi_crvi_agenda'), + __('Stats', 'esi_crvi_agenda'), + 'manage_options', + 'crvi_agenda_stats', + [self::class, 'render_stats_page'] + ); + + // Sous-menu : Agenda + add_submenu_page( + 'crvi_agenda', + __('Agenda', 'esi_crvi_agenda'), + __('Agenda', 'esi_crvi_agenda'), + 'manage_options', + 'crvi_agenda_hub', + [CRVI_Agenda_View::class, 'render_agenda_page'] + ); + } + + /** + * Affiche la page d'admin des stats + */ + public static function render_stats_page() { + echo '

' . esc_html__('Statistiques Agenda', 'esi_crvi_agenda') . '

'; + $template = plugin_dir_path(__FILE__) . '../../templates/admin/agenda-stats-form.php'; + if (file_exists($template)) { + include $template; + } else { + echo '

Template agenda-stats-form.php introuvable.

'; + } + // Traitement du formulaire (debug) + if ($_SERVER['REQUEST_METHOD'] === 'POST' && current_user_can('manage_options')) { + $entite = sanitize_text_field($_POST['entite'] ?? 'global'); + $annee = isset($_POST['annee']) ? intval($_POST['annee']) : null; + $date_debut = sanitize_text_field($_POST['date_debut'] ?? ''); + $date_fin = sanitize_text_field($_POST['date_fin'] ?? ''); + echo '

'; + printf('Debug : entité = %s, année = %s, date_debut = %s, date_fin = %s', esc_html($entite), esc_html((string)$annee), esc_html($date_debut), esc_html($date_fin)); + echo '

'; + } + echo '
'; + } + + public function load_filters() { + /* add_filter('rest_endpoints', [self::class, 'register_routes']); */ + add_filter('wp_script_attributes', [self::class, 'custom_script_tag'], 10, 2); + } + + public function load_shortcodes() { + /* add_shortcode('crvi_agenda', [self::class, 'crvi_agenda_shortcode']); */ + } + + public function load_frontend_assets() { + self::enqueue_admin_assets(); + self::enqueue_front_assets(); + } + + public function load_admin_assets() { + + } + + public function add_pages() { + //hub admin + + } + + /** + * Charge les assets uniquement sur les pages admin CRVI + */ + public static function enqueue_admin_assets() { + // Les slugs des pages admin CRVI + $crvi_pages = [ + 'crvi_agenda', + 'crvi_agenda_stats', + 'crvi_agenda_hub', + ]; + $page = $_GET['page'] ?? ''; + if (in_array($page, $crvi_pages, true)) { + // Charger Select2 via WordPress + wp_enqueue_style('select2', 'https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/css/select2.min.css', [], '4.0.13'); + wp_enqueue_script('select2', 'https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/js/select2.full.min.js', ['jquery'], '4.0.13', true); + + // Bundle CSS principal (inclut Bootstrap, Toastr, FullCalendar) + wp_enqueue_style('crvi_main_css', ESI_CRVI_AGENDA_URL . 'assets/js/dist/crvi_main_css.min.css', [], '1.0.0'); + + // Script principal du plugin (inclut Bootstrap, FullCalendar, Toastr) + wp_enqueue_script('crvi_main_agenda', ESI_CRVI_AGENDA_URL . 'assets/js/dist/crvi_main.min.js', ['jquery', 'select2'], '1.0.0', true); + + // Transmettre les permissions utilisateur au JavaScript (seulement si l'utilisateur est connecté) + if (did_action('init') && is_user_logged_in()) { + $permissions = \ESI_CRVI_AGENDA\helpers\Api_Helper::get_user_permissions(); + wp_localize_script('crvi_main_agenda', 'crviPermissions', $permissions); + } else { + // Permissions par défaut si l'utilisateur n'est pas encore chargé + wp_localize_script('crvi_main_agenda', 'crviPermissions', [ + 'can_create' => false, + 'can_edit' => false, + 'can_delete' => false, + 'can_close' => false, + 'can_view' => false, + 'user_roles' => [], + 'user_id' => 0, + ]); + } + } + } + + /** + * Charge les assets uniquement sur la page publique crvi_agenda_interne + */ + public static function enqueue_front_assets() { + if (is_page('crvi_agenda_interne')) { + // Charger Select2 via WordPress + wp_enqueue_style('select2', 'https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/css/select2.min.css', [], '4.0.13'); + wp_enqueue_script('select2', 'https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/js/select2.full.min.js', ['jquery'], '4.0.13', true); + + // Bundle CSS principal (inclut Bootstrap, Toastr, FullCalendar) + wp_enqueue_style('crvi_main_css', ESI_CRVI_AGENDA_URL . 'assets/js/dist/crvi_main_css.min.css', [], '1.0.0'); + + // Script principal du plugin (inclut Bootstrap, FullCalendar, Toastr) + wp_enqueue_script('crvi_main_agenda', ESI_CRVI_AGENDA_URL . 'assets/js/dist/crvi_main.min.js', ['jquery', 'select2'], '1.0.0', true); + + // Transmettre les permissions utilisateur au JavaScript (seulement si l'utilisateur est connecté) + if (did_action('init') && is_user_logged_in()) { + $permissions = \ESI_CRVI_AGENDA\helpers\Api_Helper::get_user_permissions(); + wp_localize_script('crvi_main_agenda', 'crviPermissions', $permissions); + } else { + // Permissions par défaut si l'utilisateur n'est pas encore chargé + wp_localize_script('crvi_main_agenda', 'crviPermissions', [ + 'can_create' => false, + 'can_edit' => false, + 'can_delete' => false, + 'can_close' => false, + 'can_view' => false, + 'user_roles' => [], + 'user_id' => 0, + ]); + } + } + } + + /** + * crvi+script loader + */ + + public function custom_script_tag(array $attr) { + + if('crvi_main_agenda-js' !== $attr['id']) { + return $attr; + } + + // Le script est maintenant compilé avec toutes les dépendances, pas besoin de type module + // $attr['type'] = 'module'; + return $attr; + } + + public static function write_log($message, $level = 'INFO') { + $message = self::format_message_for_log($message); + $log_entry = sprintf("[%s] [%s] %s\n", date('Y-m-d H:i:s'), $level, $message); + error_log($log_entry); + } + + private static function format_message_for_log($message) { + if (is_array($message)) { + return print_r($message, true); + } elseif (is_object($message)) { + return print_r($message, true); + } elseif (is_bool($message)) { + return $message ? 'TRUE' : 'FALSE'; + } elseif (is_numeric($message)) { + return (string) $message; + } elseif (is_null($message)) { + return 'NULL'; + } else { + return var_export($message, true); + } + } +} \ No newline at end of file diff --git a/app/controllers/Plugin.php b/app/controllers/Plugin.php new file mode 100644 index 0000000..5a22af6 --- /dev/null +++ b/app/controllers/Plugin.php @@ -0,0 +1,1220 @@ + XXX_Controller.php + if (strpos($relative_class, 'controllers\\CRVI_') === 0) { + $class_name = substr($relative_class, strlen('controllers\\CRVI_')); + $possible_files[] = plugin_dir_path(__FILE__) . $class_name . '.php'; + } + + // Pattern 2: controllers\XXX_Controller -> XXX_Controller.php (sans CRVI_) + if (strpos($relative_class, 'controllers\\') === 0) { + $class_name = substr($relative_class, strlen('controllers\\')); + $possible_files[] = plugin_dir_path(__FILE__) . $class_name . '.php'; + } + + // Essayer de charger le premier fichier qui existe + foreach ($possible_files as $file) { + if (file_exists($file)) { + try { + require_once $file; + $file_loaded = true; + break; + } catch (\Throwable $e) { + error_log("Erreur lors du chargement du fichier {$file}: " . $e->getMessage()); + } + } + } + + // Si le fichier n'a pas pu être chargé, logger les détails + if (!$file_loaded && !class_exists($controller)) { + $debug_info = [ + 'controller' => $controller, + 'relative_class' => $relative_class, + 'files_tried' => $possible_files, + 'files_exist' => array_map('file_exists', $possible_files) + ]; + error_log("Classe {$controller} non trouvée lors de l'enregistrement des routes. Détails: " . print_r($debug_info, true)); + continue; // Passer au contrôleur suivant + } + } + + if (class_exists($controller)) { + try { + // Vérifier que la méthode register_routes existe avant de l'appeler + if (method_exists($controller, 'register_routes')) { + $controller::register_routes(); + } else { + error_log("La méthode register_routes() n'existe pas pour la classe {$controller}"); + } + } catch (\Throwable $e) { + // Capturer toutes les exceptions et erreurs (Exception, Error, etc.) + $error_details = [ + 'controller' => $controller, + 'message' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'trace' => $e->getTraceAsString() + ]; + error_log("Erreur lors de l'enregistrement des routes pour {$controller}: " . print_r($error_details, true)); + } + } else { + // Cette ligne ne devrait plus être atteinte si le chargement a réussi + // Mais on la garde pour sécurité + error_log("Classe {$controller} non trouvée après tentative de chargement manuel"); + } + } + } + + public function load_actions() { + add_action('init', [self::class, 'register_cpt']); + add_action('rest_api_init', [self::class, 'register_routes']); // Enregistrer les routes REST API + add_action('admin_menu', [self::class, 'add_admin_menu']); + + // Initialiser le contrôleur de notifications pour enregistrer les hooks AJAX + new \ESI_CRVI_AGENDA\controllers\CRVI_Notifications_Controller(); + add_action('admin_post_crvi_import_beneficiaire', [\ESI_CRVI_AGENDA\controllers\CRVI_Beneficiaire_Controller::class, 'import_csv_admin']); + add_action('admin_post_crvi_import_local', [\ESI_CRVI_AGENDA\controllers\CRVI_Local_Controller::class, 'import_csv_admin']); + add_action('admin_post_crvi_import_traducteur', [\ESI_CRVI_AGENDA\controllers\CRVI_Traducteur_Controller::class, 'import_csv_admin']); + add_action('admin_post_crvi_import_intervenant', [\ESI_CRVI_AGENDA\controllers\CRVI_Intervenant_Controller::class, 'import_csv_admin']); + add_action('admin_post_crvi_import_departement', [\ESI_CRVI_AGENDA\controllers\CRVI_Departement_Controller::class, 'import_csv_admin']); + add_action('admin_post_crvi_import_type_intervention', [\ESI_CRVI_AGENDA\controllers\CRVI_Type_Intervention_Controller::class, 'import_csv_admin']); + // Enqueue assets conditionnellement + add_action('admin_enqueue_scripts', [self::class, 'enqueue_admin_assets']); + add_action('wp_enqueue_scripts', [self::class, 'enqueue_front_assets']); + // Localisation des scripts après que toutes les fonctions WordPress soient disponibles + add_action('wp_loaded', [self::class, 'localize_scripts']); + // Ajouter type="module" aux scripts ES6 + add_filter('script_loader_tag', [self::class, 'add_module_type_to_scripts'], 10, 3); + // Hook pour vérifier les indisponibilités lors de la sauvegarde d'un intervenant + add_action('acf/save_post', [self::class, 'check_intervenant_availability_on_save'], 20); + // Hook pour afficher les messages de conflit d'indisponibilités + add_action('admin_notices', [self::class, 'display_intervenant_conflicts_notice']); + } + + /** + * Ajoute la page d'admin Agenda CRVI avec sous-menus Hub, Stats, Agenda + */ + public static function add_admin_menu() { + // Menu principal : Agenda CRVI (affiche le Hub) + add_menu_page( + __('Agenda CRVI', 'esi_crvi_agenda'), + __('Agenda CRVI', 'esi_crvi_agenda'), + 'manage_options', + 'crvi_agenda', + [CRVI_Main_View::class, 'render_hub_admin_page'], + 'dashicons-calendar-alt', + 6 + ); + + // Sous-menu : Hub (même callback que le menu principal) + add_submenu_page( + 'crvi_agenda', + __('Hub', 'esi_crvi_agenda'), + __('Hub', 'esi_crvi_agenda'), + 'manage_options', + 'crvi_agenda', + [CRVI_Main_View::class, 'render_hub_admin_page'] + ); + + // Sous-menu : Stats + add_submenu_page( + 'crvi_agenda', + __('Stats', 'esi_crvi_agenda'), + __('Stats', 'esi_crvi_agenda'), + 'manage_options', + 'crvi_agenda_stats', + [self::class, 'render_stats_page'] + ); + + // Sous-menu : Agenda + add_submenu_page( + 'crvi_agenda', + __('Agenda', 'esi_crvi_agenda'), + __('Agenda', 'esi_crvi_agenda'), + 'manage_options', + 'crvi_agenda_hub', + [CRVI_Agenda_View::class, 'render_agenda_page'] + ); + + // Sous-menu : Gestion des permanences + add_submenu_page( + 'crvi_agenda', + __('Permanences', 'esi_crvi_agenda'), + __('Permanences', 'esi_crvi_agenda'), + 'manage_options', + 'crvi_agenda_permanences', + [self::class, 'render_permanences_page'] + ); + + // Menu : Capacités de traduction + CRVI_TraductionLangue_Controller::register_admin_page(); + } + + /** + * Affiche la page d'admin des stats + */ + public static function render_stats_page() { + // Charger les mêmes données que pour la page agenda + $locals = \ESI_CRVI_AGENDA\models\CRVI_Local_Model::get_locals([], true); + $intervenants = \ESI_CRVI_AGENDA\models\CRVI_Intervenant_Model::get_intervenants([], true); + $departements = \ESI_CRVI_AGENDA\models\CRVI_Departement_Model::all(true); + $types_intervention = \ESI_CRVI_AGENDA\models\CRVI_Type_Intervention_Model::all(true); + $traducteurs = \ESI_CRVI_AGENDA\models\CRVI_Traducteur_Model::get_traducteurs([], true); + $langues_beneficiaire = \ESI_CRVI_AGENDA\helpers\Api_Helper::get_languages(true); + + // Récupérer tous les bénéficiaires + $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, + ]; + } + + $template = plugin_dir_path(__FILE__) . '../../templates/admin/agenda-stats-form.php'; + if (file_exists($template)) { + include $template; + } else { + echo '

Template agenda-stats-form.php introuvable.

'; + } + } + + /** + * Affiche la page d'admin de gestion des permanences + */ + public static function render_permanences_page() { + echo '

' . esc_html__('Gestion des Permanences', 'esi_crvi_agenda') . '

'; + $template = plugin_dir_path(__FILE__) . '../../templates/admin/permanences-admin.php'; + if (file_exists($template)) { + include $template; + } else { + echo '

Template permanences-admin.php introuvable.

'; + } + echo '
'; + } + + public static function load_filters() { + /* add_filter('rest_endpoints', [self::class, 'register_routes']); */ + add_filter('wp_script_attributes', [self::class, 'custom_script_tag'], 10, 2); + } + + public function load_shortcodes() { + /* add_shortcode('crvi_agenda', [self::class, 'crvi_agenda_shortcode']); */ + + // Shortcodes pour l'espace intervenant + add_shortcode('crvi_intervenant_hub', [\ESI_CRVI_AGENDA\controllers\Intervenant_Space_Controller::class, 'shortcode_hub']); + add_shortcode('crvi_intervenant_agenda', [\ESI_CRVI_AGENDA\controllers\Intervenant_Space_Controller::class, 'shortcode_agenda']); + add_shortcode('crvi_intervenant_profil', [\ESI_CRVI_AGENDA\controllers\Intervenant_Space_Controller::class, 'shortcode_profil']); + add_shortcode('crvi_intervenant_permanences', [\ESI_CRVI_AGENDA\controllers\Intervenant_Space_Controller::class, 'shortcode_permanences']); + } + + public function load_frontend_assets() { + self::enqueue_admin_assets(); + self::enqueue_front_assets(); + } + + public function load_admin_assets() { + + } + + public function add_pages() { + //hub admin + + } + + /** + * Charge les assets uniquement sur les pages admin CRVI + */ + public static function enqueue_admin_assets() { + // Les slugs des pages admin CRVI + $crvi_pages = [ + 'crvi_agenda', + 'crvi_agenda_stats', + 'crvi_agenda_hub', + 'crvi_agenda_permanences', + ]; + $page = $_GET['page'] ?? ''; + if (in_array($page, $crvi_pages, true)) { + // Charger Select2 via WordPress + wp_enqueue_style('select2', 'https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/css/select2.min.css', [], '4.0.13'); + wp_enqueue_script('select2', 'https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/js/select2.full.min.js', ['jquery'], '4.0.13', true); + + wp_enqueue_script('crvi_fa', 'https://kit.fontawesome.com/08d831faac.js', [], '6.0.0', true); + + // CSS des librairies (Bootstrap, Toastr, Select2) - compilé par Vite + wp_enqueue_style('crvi_libraries_css', ESI_CRVI_AGENDA_URL . 'assets/js/dist/crvi_libraries.min.css', [], '1.0.0'); + + // CSS principal du plugin - compilé par Vite + wp_enqueue_style('crvi_main_css', ESI_CRVI_AGENDA_URL . 'assets/js/dist/crvi_main.min.css', ['crvi_libraries_css'], '1.0.0'); + + + // Librairies externes (Bootstrap, Toastr, FullCalendar) - chargées en premier + wp_enqueue_script('crvi_libraries', ESI_CRVI_AGENDA_URL . 'assets/js/dist/crvi_libraries.min.js', ['jquery', 'select2'], '1.0.0', true); + + // Script principal du plugin (modules locaux) - dépend des librairies + wp_enqueue_script('crvi_main_agenda', ESI_CRVI_AGENDA_URL . 'assets/js/dist/crvi_main.min.js', ['crvi_libraries'], '1.0.0', true); + + self::localize_acf_data('crvi_main_agenda'); + } + } + + /** + * Charge les assets uniquement sur la page publique crvi_agenda_interne + * et sur les pages contenant les shortcodes de l'espace intervenant + */ + public static function enqueue_front_assets() { + global $post; + + // Vérifier si on est sur la page crvi_agenda_interne + $is_agenda_page = is_page('crvi_agenda_interne'); + + // Vérifier si on est sur une page de l'espace intervenant (par slug) + $is_intervenant_page = false; + $intervenant_page_slugs = [ + 'espace-intervenant', + 'espace-intervenant-profil', + 'espace-intervenant-agenda', + 'encodage-permanences' + ]; + + // Vérifier si la page actuelle correspond à un slug intervenant + if ($post && $post->post_type === 'page') { + $page_slug = $post->post_name; + + // Vérifier directement le slug de la page + if (in_array($page_slug, $intervenant_page_slugs, true)) { + $is_intervenant_page = true; + } + + // Vérifier si c'est une page enfant d'espace-intervenant + if (!$is_intervenant_page && $post->post_parent) { + $parent = get_post($post->post_parent); + if ($parent && $parent->post_name === 'espace-intervenant') { + $is_intervenant_page = true; + } + } + + // Vérifier aussi avec is_page() pour plus de robustesse + if (!$is_intervenant_page) { + foreach ($intervenant_page_slugs as $slug) { + if (is_page($slug)) { + $is_intervenant_page = true; + break; + } + } + } + } + + // Vérifier aussi par le chemin de l'URL (pour les pages avec permalink hiérarchique) + if (!$is_intervenant_page) { + $current_url = $_SERVER['REQUEST_URI'] ?? ''; + $current_path = parse_url($current_url, PHP_URL_PATH); + if ($current_path && ( + strpos($current_path, '/espace-intervenant') !== false || + strpos($current_path, '/encodage-permanences') !== false + )) { + $is_intervenant_page = true; + } + } + + // Vérifier si la page contient un shortcode intervenant + $has_intervenant_shortcode = false; + if ($post && isset($post->post_content)) { + $shortcodes = [ + 'crvi_intervenant_hub', + 'crvi_intervenant_agenda', + 'crvi_intervenant_profil', + 'crvi_intervenant_permanences' + ]; + foreach ($shortcodes as $shortcode) { + if (has_shortcode($post->post_content, $shortcode)) { + $has_intervenant_shortcode = true; + break; + } + } + } + + if ($is_agenda_page || $is_intervenant_page || $has_intervenant_shortcode) { + wp_enqueue_script('crvi_fa', 'https://kit.fontawesome.com/08d831faac.js', [], '6.0.0', true); + + // CSS des librairies (Bootstrap, Toastr, Select2) - compilé par Vite + wp_enqueue_style('crvi_libraries_css', ESI_CRVI_AGENDA_URL . 'assets/js/dist/crvi_libraries.min.css', [], '1.0.0'); + + // CSS principal du plugin - compilé par Vite + wp_enqueue_style('crvi_main_css', ESI_CRVI_AGENDA_URL . 'assets/js/dist/crvi_main.min.css', ['crvi_libraries_css'], '1.0.0'); + + // CSS spécifique pour la page de profil intervenant (compilé par Vite) + if ($is_intervenant_page && $post && $post->post_name === 'espace-intervenant-profil') { + wp_enqueue_style('crvi_intervenant_profile_css', ESI_CRVI_AGENDA_URL . 'assets/js/dist/intervenant-profile.min.css', ['crvi_main_css'], '1.0.0'); + } + + // Select2 JS via CDN (utilisé par crvi_libraries) + wp_enqueue_script('select2', 'https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.13/js/select2.full.min.js', ['jquery'], '4.0.13', true); + + // Librairies externes (Bootstrap, Toastr, Select2) - compilées par Vite + wp_enqueue_script('crvi_libraries', ESI_CRVI_AGENDA_URL . 'assets/js/dist/crvi_libraries.min.js', ['jquery', 'select2'], '1.0.0', true); + + // Script principal du plugin (modules locaux) - dépend des librairies + wp_enqueue_script('crvi_main_agenda', ESI_CRVI_AGENDA_URL . 'assets/js/dist/crvi_main.min.js', ['crvi_libraries'], '1.0.0', true); + + // Localiser les données si c'est une page intervenant + if ($is_intervenant_page || $has_intervenant_shortcode) { + self::localize_acf_data('crvi_main_agenda'); + } + } + } + + /** + * Récupère toutes les données ACF pour les couleurs et icônes + * et les passe au JavaScript via wp_localize_script + */ + public static function localize_acf_data($script_handle) { + // Vérifier que ACF est actif + if (!function_exists('get_field')) { + return; + } + + $acf_data = [ + 'departements' => self::get_departements_acf_data(), + 'statuts' => self::get_statuts_acf_data(), + 'types_local' => self::get_types_local_acf_data(), + 'types_intervention' => self::get_types_intervention_acf_data(), + 'intervenants' => self::get_intervenants_acf_data(), + 'permissions' => self::get_user_permissions(), + 'couleurs_rdv' => self::get_couleurs_rdv_acf_data(), + 'couleurs_permanence' => self::get_couleurs_permanence_acf_data(), + 'indisponibilites_intervenants' => self::get_intervenants_disponibilites() + ]; + + wp_localize_script($script_handle, 'crviACFData', $acf_data); + } + + /** + * Récupère les données des départements (icône et couleur) + */ + private static function get_departements_acf_data() { + $departements = []; + + // Récupérer tous les départements + $departements_posts = get_posts([ + 'post_type' => 'departement', + 'numberposts' => -1, + 'post_status' => 'publish' + ]); + + foreach ($departements_posts as $post) { + $nom = get_field('nom', $post->ID); + $icone = get_field('icone_departement', $post->ID); + $couleur = get_field('couleur_departement', $post->ID); + + if ($nom) { + $departements[strtolower($nom)] = [ + 'id' => $post->ID, + 'nom' => $nom, + 'icone' => $icone ?: '📋', + 'couleur' => $couleur ?: '#6c757d' + ]; + } + } + + return $departements; + } + + /** + * Récupère les données des statuts de RDV + */ + private static function get_statuts_acf_data() { + $statuts = []; + + // Récupérer les statuts depuis les options ACF + $statuts_rdv = get_field('statut_rdv', 'option'); + + if (is_array($statuts_rdv)) { + foreach ($statuts_rdv as $statut) { + $nom = $statut['statut'] ?? ''; + $couleur = $statut['couleur'] ?? ''; + + if ($nom) { + $statuts[sanitize_title($nom)] = [ + 'nom' => $nom, + 'couleur' => $couleur ?: '#3788d8' + ]; + } + } + } + + // Valeurs par défaut si aucun statut configuré + if (empty($statuts)) { + $statuts = [ + 'prevu' => ['nom' => 'Prévu', 'couleur' => '#28a745'], + 'annule' => ['nom' => 'Annulé', 'couleur' => '#dc3545'], + 'non_tenu' => ['nom' => 'Non tenu', 'couleur' => '#ffc107'], + 'cloture' => ['nom' => 'Clôturé', 'couleur' => '#6c757d'], + 'absence' => ['nom' => 'Absence', 'couleur' => '#fd7e14'] + ]; + } + + return $statuts; + } + + /** + * Récupère les données des types de local + */ + private static function get_types_local_acf_data() { + $types_local = []; + + // Récupérer les types de local depuis les options ACF + $types_local_config = get_field('type_de_local', 'option'); + + if (is_array($types_local_config)) { + foreach ($types_local_config as $type) { + $nom = $type['type'] ?? ''; + $couleur = $type['couleur'] ?? ''; + $icone = $type['icone'] ?? ''; + + if ($nom) { + $types_local[strtolower($nom)] = [ + 'nom' => $nom, + 'couleur' => $couleur ?: '#6c757d', + 'icone' => $icone ?: '🏢' + ]; + } + } + } + + return $types_local; + } + + /** + * Récupère les données des types d'intervention + */ + private static function get_types_intervention_acf_data() { + $types_intervention = []; + + // Récupérer tous les types d'intervention (CPT) + $types_intervention_posts = get_posts([ + 'post_type' => 'type_intervention', + 'numberposts' => -1, + 'post_status' => 'publish', + ]); + + foreach ($types_intervention_posts as $post) { + $couleur = get_field('couleur', $post->ID); + $icone = get_field('icone', $post->ID); + + $types_intervention[$post->ID] = [ + 'id' => $post->ID, + 'nom' => $post->post_title, + 'couleur' => $couleur ?: '#6c757d', + 'icone' => $icone ?: '📋' + ]; + } + + return $types_intervention; + } + + /** + * Récupère les données des intervennats (user role intervenant) + */ + private static function get_intervenants_acf_data() { + $intervenants_data = []; + + $intervenants = get_users([ + 'role' => 'intervenant' + ]); + + //champs acf 'couleur' + foreach ($intervenants as $intervenant) { + $couleur = get_field('couleur', 'user_'.$intervenant->ID); + $intervenants_data[$intervenant->ID] = [ + 'id' => $intervenant->ID, + 'nom' => $intervenant->display_name, + 'couleur' => $couleur ?: '#6c757d' + ]; + } + + return $intervenants_data; + } + + /** + * Récupère les permissions de l'utilisateur actuel + */ + private static function get_user_permissions() { + $user = wp_get_current_user(); + + return [ + 'can_view' => current_user_can('read'), + 'can_create' => current_user_can('edit_posts'), + 'can_edit' => current_user_can('edit_posts'), + 'can_delete' => current_user_can('delete_posts'), + 'user_id' => $user->ID, + 'user_roles' => $user->roles + ]; + } + + /** + * Récupère les couleurs des RDV selon le type de local depuis les options ACF + */ + private static function get_couleurs_rdv_acf_data() { + return [ + 'bureau' => get_field('couleur_rdv_bureau', 'option') ?: '#6c757d', + 'salle' => get_field('couleur_rdv_salle', 'option') ?: '#6c757d' + ]; + } + + /** + * Récupère les couleurs des permanences depuis les options ACF + */ + private static function get_couleurs_permanence_acf_data() { + return [ + 'permanence' => get_field('couleur_permanence', 'option') ?: '#9e9e9e', + 'permanence_non_attribuee' => get_field('couleur_permanence_non_attribuee', 'option') ?: '#9e9e9e', + 'permanence_non_disponible' => get_field('couleur_permanence_non_disponible', 'option') ?: '#9e9e9e' + ]; + } + + /** + * Localise les scripts après que toutes les fonctions WordPress soient disponibles + */ + public static function localize_scripts() { + // Vérifier si le script crvi_main_agenda est enregistré + if (!wp_script_is('crvi_main_agenda', 'registered')) { + return; + } + + // Ajouter le nonce pour l'authentification API + wp_localize_script('crvi_main_agenda', 'wpApiSettings', [ + 'nonce' => \wp_create_nonce('wp_rest'), + 'root' => \esc_url_raw(\rest_url()) + ]); + + // Transmettre les permissions utilisateur au JavaScript + if (is_user_logged_in()) { + $permissions = \ESI_CRVI_AGENDA\helpers\Api_Helper::get_user_permissions(); + wp_localize_script('crvi_main_agenda', 'crviPermissions', $permissions); + } else { + // Permissions par défaut si l'utilisateur n'est pas connecté + wp_localize_script('crvi_main_agenda', 'crviPermissions', [ + 'can_create' => false, + 'can_edit' => false, + 'can_delete' => false, + 'can_close' => false, + 'can_view' => false, + 'user_roles' => [], + 'user_id' => 0, + ]); + } + + // Fournir l'URL AJAX et un nonce pour les appels admin-ajax (ex: debug SMS) + // + données statiques avec cache (couleurs, permanences, disponibilités intervenants) + wp_localize_script('crvi_main_agenda', 'crviAjax', [ + 'url' => \admin_url('admin-ajax.php'), + 'nonce' => \wp_create_nonce('crvi_sms'), + 'couleurs_types_intervention' => self::get_couleurs_types_intervention(), + 'couleur_permanence_non_attribuee' => self::get_couleur_permanence_non_attribuee(), + 'couleur_permanence_non_disponible' => self::get_couleur_permanence_non_disponible(), + 'intervenants_disponibilites' => self::get_intervenants_disponibilites() + ]); + } + + /** + * Récupère les couleurs de tous les types d'intervention (id => couleur) + * Utilise un transient de 12h pour le cache + */ + private static function get_couleurs_types_intervention() { + $transient_key = 'crvi_couleurs_types_intervention'; + $couleurs = get_transient($transient_key); + + if (false === $couleurs) { + $couleurs = []; + + // Vérifier que ACF est actif + if (!function_exists('get_field')) { + return $couleurs; + } + + // Récupérer tous les types d'intervention (CPT) + $types_intervention_posts = get_posts([ + 'post_type' => 'type_intervention', + 'numberposts' => -1, + 'post_status' => 'publish', + ]); + + foreach ($types_intervention_posts as $post) { + $couleur = get_field('couleur', $post->ID); + $couleurs[$post->ID] = $couleur ?: '#6c757d'; + } + + // Mettre en cache pour 12 heures (43200 secondes) + set_transient($transient_key, $couleurs, 12 * HOUR_IN_SECONDS); + } + + return $couleurs; + } + + /** + * Récupère la couleur de permanence non attribuée depuis les options ACF + * Utilise un transient de 12h pour le cache + */ + private static function get_couleur_permanence_non_attribuee() { + $transient_key = 'crvi_permanence_non_attribuee'; + $couleur = get_transient($transient_key); + + if (false === $couleur) { + // Vérifier que ACF est actif + if (!function_exists('get_field')) { + return '#9e9e9e'; + } + + $couleur = get_field('couleur_permanence_non_attribuee', 'option') ?: '#9e9e9e'; + + // Mettre en cache pour 12 heures + set_transient($transient_key, $couleur, 12 * HOUR_IN_SECONDS); + } + + return $couleur; + } + + /** + * Récupère la couleur de permanence non disponible depuis les options ACF + * Utilise un transient de 12h pour le cache + */ + private static function get_couleur_permanence_non_disponible() { + $transient_key = 'crvi_permanence_non_disponible'; + $couleur = get_transient($transient_key); + + if (false === $couleur) { + // Vérifier que ACF est actif + if (!function_exists('get_field')) { + return '#9e9e9e'; + } + + $couleur = get_field('couleur_permanence_non_disponible', 'option') ?: '#9e9e9e'; + + // Mettre en cache pour 12 heures + set_transient($transient_key, $couleur, 12 * HOUR_IN_SECONDS); + } + + return $couleur; + } + + /** + * Récupère les disponibilités de tous les intervenants + * Structure: array de tous les intervenants avec jours_dispo => array, conges => array + * Utilise un transient de 12h pour le cache + */ + private static function get_intervenants_disponibilites() { + $transient_key = 'crvi_intervenants_disponibilites'; + $intervenants_data = get_transient($transient_key); + + if (false === $intervenants_data) { + $intervenants_data = []; + + // Vérifier que ACF est actif + if (!function_exists('get_field')) { + return $intervenants_data; + } + + // Récupérer tous les intervenants (users avec role 'intervenant') + $intervenants = get_users([ + 'role' => 'intervenant' + ]); + + foreach ($intervenants as $intervenant) { + // Récupérer les jours de disponibilité (checkbox) + $jours_dispo = get_field('jours_de_disponibilite', 'user_' . $intervenant->ID); + if (!is_array($jours_dispo)) { + $jours_dispo = []; + } + + // Récupérer les indisponibilités ponctuelles (repeater) + $indisponibilites = get_field('indisponibilitee_ponctuelle', 'user_' . $intervenant->ID); + $conges = []; + + if (is_array($indisponibilites)) { + foreach ($indisponibilites as $indispo) { + $conges[] = [ + 'debut' => $indispo['debut'] ?? '', + 'fin' => $indispo['fin'] ?? '', + 'type' => $indispo['type'] ?? '', + 'commentaire' => $indispo['commentaire'] ?? '' + ]; + } + } + + $intervenants_data[$intervenant->ID] = [ + 'id' => $intervenant->ID, + 'jours_dispo' => $jours_dispo, + 'conges' => $conges + ]; + } + + // Mettre en cache pour 12 heures + set_transient($transient_key, $intervenants_data, 3 * HOUR_IN_SECONDS); + } + + return $intervenants_data; + } + + /** + * crvi+script loader + */ + + public static function custom_script_tag(array $attr, $handle = null) { + if (!isset($attr['id']) || 'crvi_main_agenda-js' !== $attr['id']) { + return $attr; + } + + // Le script est maintenant compilé avec toutes les dépendances, pas besoin de type module + // $attr['type'] = 'module'; + return $attr; + } + + public static function write_log($message, $level = 'INFO') { + $message = self::format_message_for_log($message); + $log_entry = sprintf("[%s] [%s] %s\n", date('Y-m-d H:i:s'), $level, $message); + error_log($log_entry); + } + + /** + * Écrit un message dans un fichier debug.txt sous le dossier uploads + * Chemin: wp-content/uploads/crvi-debug.txt + */ + public static function write_debug_file($message, $level = 'INFO') { + $message = self::format_message_for_log($message); + $log_entry = sprintf("[%s] [%s] %s\n", date('Y-m-d H:i:s'), $level, $message); + + $upload_dir = wp_upload_dir(); + $file_path = trailingslashit($upload_dir['basedir']) . 'crvi-debug.txt'; + + // Tente d'écrire le log. En cas d'échec, fallback sur error_log + @file_put_contents($file_path, $log_entry, FILE_APPEND); + + // Écrire également dans le log PHP (wp-content/debug.log si WP_DEBUG_LOG actif, sinon error_log du serveur) + error_log($log_entry); + } + + private static function format_message_for_log($message) { + if (is_array($message)) { + return print_r($message, true); + } elseif (is_object($message)) { + return print_r($message, true); + } elseif (is_bool($message)) { + return $message ? 'TRUE' : 'FALSE'; + } elseif (is_numeric($message)) { + return (string) $message; + } elseif (is_null($message)) { + return 'NULL'; + } else { + return var_export($message, true); + } + } + + /** + * Ajoute type="module" aux scripts ES6 pour permettre les imports/exports + */ + public static function add_module_type_to_scripts($tag, $handle, $src) { + // Scripts qui utilisent ES modules + $module_scripts = ['crvi_libraries', 'crvi_main_agenda']; + + if (in_array($handle, $module_scripts)) { + // Remplacer type='text/javascript' par type='module' + $tag = str_replace("type='text/javascript'", "type='module'", $tag); + $tag = str_replace('type="text/javascript"', 'type="module"', $tag); + + // Si pas de type existant, l'ajouter + if (strpos($tag, "type=") === false) { + $tag = str_replace(' + diff --git a/templates/admin/traduction-langue-list.php b/templates/admin/traduction-langue-list.php new file mode 100644 index 0000000..7f827f8 --- /dev/null +++ b/templates/admin/traduction-langue-list.php @@ -0,0 +1,191 @@ +Non définie'; + } + + // S'assurer que c'est un tableau + if (!is_array($langue_ids)) { + $langue_ids = [$langue_ids]; + } + + $langue_names = []; + foreach ($langue_ids as $langue_id) { + $term = get_term($langue_id, 'langue'); + if ($term && !is_wp_error($term)) { + $langue_names[] = esc_html($term->name); + } + } + + if (empty($langue_names)) { + return 'Non définie'; + } + + // Si plusieurs langues, les afficher avec des badges + if (count($langue_names) > 1) { + $output = '
'; + foreach ($langue_names as $name) { + $output .= '' . $name . ''; + } + $output .= '
'; + return $output; + } + + return $langue_names[0]; +} + +// Helper pour récupérer les exceptions d'une capacité +function get_exceptions_info($post_id) { + $ajouter_exception = get_field('ajouter_exception', $post_id); + if (!$ajouter_exception) { + return null; + } + + $exceptions = get_field('exceptions_disponibilites', $post_id); + if (empty($exceptions)) { + return null; + } + + return $exceptions; +} + +// Helper pour formater l'affichage de l'exception +function format_exception_display($exceptions) { + if (empty($exceptions)) { + return ''; + } + + $frequence = isset($exceptions['frequence']) ? $exceptions['frequence'] : ''; + $frequence_periode = isset($exceptions['frequence_periode']) ? $exceptions['frequence_periode'] : ''; + $periode = isset($exceptions['periode']) ? $exceptions['periode'] : ''; + + if (empty($frequence) || empty($frequence_periode) || empty($periode)) { + return ''; + } + + $periode_label = $periode === 'matin' ? 'Matin' : ($periode === 'apres_midi' ? 'Après-midi' : 'Journée'); + $frequence_periode_label = $frequence_periode === 'semaine' ? 'semaine' : 'mois'; + + return sprintf('%d × %s / %s', $frequence, $periode_label, $frequence_periode_label); +} + +// Helper pour vérifier si la capacité a des événements +function has_events_badge($post_id) { + $has_events = ESI_CRVI_AGENDA\controllers\CRVI_TraductionLangue_Controller::check_events_for_capacite($post_id); + if ($has_events) { + return '🔒 Événements'; + } + return ''; +} +?> + +
+

+ + Capacités de traduction +

+ + + Ajouter une capacité + + +
+ + +
+

Aucune capacité de traduction trouvée. Créer la première capacité

+
+ +
+ ID); + $jour = get_field('jour', $parent->ID); + $periode = get_field('periode', $parent->ID); + $limite = get_field('limite', $parent->ID); + $limite_par = get_field('limite_par', $parent->ID); + $actif = get_field('actif', $parent->ID); + $has_events = has_events_badge($parent->ID); + + // Récupérer les exceptions + $exceptions = get_exceptions_info($parent->ID); + $exception_display = format_exception_display($exceptions); + + // Calculer le nombre de créneaux utilisés + $model = ESI_CRVI_AGENDA\models\CRVI_TraductionLangue_Model::load($parent->ID); + $used = $model ? ESI_CRVI_AGENDA\models\CRVI_TraductionLangue_Model::countUsed($parent->ID, null, $model) : 0; + $percentage = $limite > 0 ? round(($used / $limite) * 100) : 0; + $is_full = $percentage >= 100; + ?> +
+
+
+

post_title); ?>

+
+ + Inactif + + +
+
+ +
+ +
+
+
+ Langue + +
+
+ Jour + +
+
+ Période + +
+
+ Limite + / +
+ +
+ Exception + +
+ +
+ +
+
+ Utilisation + / (%) +
+
+
+
+
+
+
+ +
+ +
diff --git a/templates/email/.gitkeep b/templates/email/.gitkeep new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/templates/email/.gitkeep @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/templates/email/rdv-cloture-email-template.php b/templates/email/rdv-cloture-email-template.php new file mode 100644 index 0000000..b430df9 --- /dev/null +++ b/templates/email/rdv-cloture-email-template.php @@ -0,0 +1,56 @@ + + + + + Rendez-vous clôturés + + + + + + +
+ + + + + + + + + + + + + +
+

Liste des rendez-vous clôturés

+
+ Voici la liste des rendez-vous clôturés récemment : +
+ + + + + + + + + + + + + + + + + + + +
DateHeureIntervenantBénéficiaire
+
+ Ceci est un message automatique, merci de ne pas répondre directement à cet email. +
+
+ + \ No newline at end of file diff --git a/templates/front/.gitkeep b/templates/front/.gitkeep new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/templates/front/.gitkeep @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/templates/front/intervenant-agenda.php b/templates/front/intervenant-agenda.php new file mode 100644 index 0000000..73fb489 --- /dev/null +++ b/templates/front/intervenant-agenda.php @@ -0,0 +1,238 @@ + + +
+
+ +
+
+

+ Mon Agenda +

+ +
+
+ + +
+
+ +
+
+ + +
+ +
+
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+ +
+
+ +
+
+
+ + +
+
+

Chargement du calendrier...

+
+
+
+
+ + +
+
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ +
+
+
+ + +
+
+

Chargement du calendrier des collègues...

+
+
+
+
+
+
+
+ + + + + diff --git a/templates/frontend/intervenant-agenda.php b/templates/frontend/intervenant-agenda.php new file mode 100644 index 0000000..e22aad0 --- /dev/null +++ b/templates/frontend/intervenant-agenda.php @@ -0,0 +1,239 @@ + + +
+
+ +
+
+

+ Mon Agenda +

+ +
+
+ + +
+
+ +
+
+ + +
+ +
+
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+ +
+
+ +
+
+
+ + +
+
+

Chargement du calendrier...

+
+
+
+
+ + +
+
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ +
+
+
+ + +
+
+

Chargement du calendrier des collègues...

+
+
+
+
+
+
+
+ + + + + + diff --git a/templates/frontend/intervenant-hub.php b/templates/frontend/intervenant-hub.php new file mode 100644 index 0000000..7d1e0ac --- /dev/null +++ b/templates/frontend/intervenant-hub.php @@ -0,0 +1,191 @@ +first_name . ' ' . $user->last_name); +$today = date('d/m/Y'); +?> + +
+
+ +
+
+

+ Mon Espace Intervenant +

+

Bonjour

+
+
+ + +
+
+
+
+

+ + Mes rendez-vous aujourd'hui () +

+
+
+
+
+
+ Chargement... +
+

Chargement des rendez-vous...

+
+
+ +
+
+
+
+ + + + + +
+
+
+
+

+ Mes Permanences +

+
+
+

Gérez vos créneaux de permanence disponibles.

+ + Encoder mes permanences + +
+
+
+
+
+
+ + + + + + + + + diff --git a/templates/frontend/intervenant-modal-rdv.php b/templates/frontend/intervenant-modal-rdv.php new file mode 100644 index 0000000..c38c8fa --- /dev/null +++ b/templates/frontend/intervenant-modal-rdv.php @@ -0,0 +1,175 @@ + + + + + + + diff --git a/templates/frontend/intervenant-permanences.php b/templates/frontend/intervenant-permanences.php new file mode 100644 index 0000000..1a94da1 --- /dev/null +++ b/templates/frontend/intervenant-permanences.php @@ -0,0 +1,552 @@ + 'langue', + 'hide_empty' => false, + 'orderby' => 'name', + 'order' => 'ASC', +]); + +$langues = []; +if (!is_wp_error($langues_terms) && !empty($langues_terms)) { + foreach ($langues_terms as $term) { + $langues[] = [ + 'id' => $term->slug, + 'nom' => $term->name, + ]; + } +} +?> + +
+
+ +
+
+

+ Mes Permanences +

+ +
+
+ +
+
+ + + + +
+ +
+
+ +
+
+

+ SECTION 1 : Sélection des permanences +

+
+
+ +
+ + + + Sélectionnez le mois et l'année à partir desquels les permanences seront + créées. + +
+ + +
+ +
+ + + + + +
+
+ + +
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+ +
+ + + + + +
+ + Choisissez la durée de chaque permanence. + +
+ + + + + +
+ + + Sélectionnez les heures de début. Chaque heure sélectionnée créera une + tranche d'1 heure (ex: 09:00 → 10:00). + +
+
+ +
+ +
+
+ + +
+
+ + +
+ +
+ +
+
+ + +
+
+ +
+
+ + +
+ +
+ + Veuillez sélectionner une plage horaire pour voir l'aperçu. +
+
+ + +
+ +
+
+
+ + +
+
+

+ SECTION 2 : Langues des permanences +

+
+
+
+ + + + Sélectionnez une ou plusieurs langues pour ces permanences. + +
+
+
+ + +
+
+

+ SECTION 3 : Informations complémentaires +

+
+
+
+ + + + Ces informations seront associées aux permanences créées. + +
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
+

+ Import de permanences via CSV +

+
+
+
+ + Format CSV attendu : Le fichier CSV doit contenir les colonnes + suivantes : +
    +
  • date_debut : Date de début (format YYYY-MM-DD)
  • +
  • date_fin : Date de fin (format YYYY-MM-DD)
  • +
  • heure_debut : Heure de début (format HH:MM)
  • +
  • heure_fin : Heure de fin (format HH:MM)
  • +
  • informations_complementaires : Notes (optionnel)
  • +
+

Note : Les permanences seront créées pour votre + compte.

+
+ +
+
+ + + + Format accepté : CSV (séparateur virgule) + +
+ +
+ + +
+
+ + +
+
+
+
+
+
+
+
+ + + + \ No newline at end of file diff --git a/templates/frontend/intervenant-profile.php b/templates/frontend/intervenant-profile.php new file mode 100644 index 0000000..5949c2b --- /dev/null +++ b/templates/frontend/intervenant-profile.php @@ -0,0 +1,403 @@ +ID; + +// Récupérer tous les champs ACF pour pré-remplir le formulaire +$telephone = ''; +$jours_disponibilite = []; +$indisponibilites_ponctuelles = []; +$heures_de_permanences = []; +if (function_exists('get_field')) { + $telephone = get_field('telephone', 'user_' . $user_id); + if (!is_string($telephone)) { + $telephone = ''; + } + + $jours_disponibilite = get_field('jours_de_disponibilite', 'user_' . $user_id); + if (!is_array($jours_disponibilite)) { + $jours_disponibilite = []; + } + + $indisponibilites_ponctuelles = get_field('indisponibilitee_ponctuelle', 'user_' . $user_id); + if (!is_array($indisponibilites_ponctuelles)) { + $indisponibilites_ponctuelles = []; + } + + $heures_de_permanences = get_field('heures_de_permanences', 'user_' . $user_id); + if (!is_array($heures_de_permanences)) { + $heures_de_permanences = []; + } +} + +// Labels des types d'indisponibilité +$types_indisponibilite = [ + 'conge' => 'Congé', + 'absence' => 'Absence', + 'maladie' => 'Maladie' +]; + +// Fonction pour convertir une date au format d/m/Y vers YYYY-MM-DD pour les champs date +function convert_date_to_input($date_str) { + if (empty($date_str)) { + return ''; + } + // Si déjà au format YYYY-MM-DD, retourner tel quel + if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $date_str)) { + return $date_str; + } + // Si au format d/m/Y, convertir + if (preg_match('/^(\d{2})\/(\d{2})\/(\d{4})$/', $date_str, $matches)) { + return $matches[3] . '-' . $matches[2] . '-' . $matches[1]; + } + return ''; +} + +// Labels des jours +$jours_labels = [ + 'lundi' => 'Lundi', + 'mardi' => 'Mardi', + 'mercredi' => 'Mercredi', + 'jeudi' => 'Jeudi', + 'vendredi' => 'Vendredi', + 'samedi' => 'Samedi', + 'dimanche' => 'Dimanche' +]; +?> + +
+
+ +
+
+

+ Mon Profil +

+ +
+
+ + + + +
+
+ +
+
+

+ Mes informations personnelles +

+
+
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ + +
+ Vous pouvez modifier votre numéro de téléphone +
+ + +
+ + +
+
+
+
+ + +
+
+

+ Mes disponibilités +

+
+
+
+ +
+ +
+ $jour_label) : + $is_checked = in_array($jour_value, $jours_disponibilite, true); + ?> +
+
+ + > + +
+
+ +
+ Cochez les jours où vous êtes disponible +
+ + +
+ +
+ '09:00 → 10:00', + '10:00' => '10:00 → 11:00', + '11:00' => '11:00 → 12:00', + '12:00' => '12:00 → 13:00', + '13:00' => '13:00 → 14:00', + '14:00' => '14:00 → 15:00', + '15:00' => '15:00 → 16:00', + '16:00' => '16:00 → 17:00', + ]; + foreach ($heures_choices as $hval => $hlabel) : + $is_checked = in_array($hval, $heures_de_permanences, true); + ?> +
+
+ + > + +
+
+ +
+ Cochez les créneaux horaires où vous êtes disponible +
+ + +
+
+ + +
+ + +
+ + $indispo) : + $debut = $indispo['debut'] ?? ''; + $fin = $indispo['fin'] ?? ''; + $type = $indispo['type'] ?? 'conge'; + $commentaire = $indispo['commentaire'] ?? ''; + $type_label = $types_indisponibilite[$type] ?? $type; + + // Convertir les dates pour l'affichage + $debut_formatted = $debut; + $fin_formatted = $fin; + + // Si les dates sont au format d/m/Y, les formater pour l'affichage + if (preg_match('/^(\d{2})\/(\d{2})\/(\d{4})$/', $debut)) { + $debut_formatted = $debut; + } + if (preg_match('/^(\d{2})\/(\d{2})\/(\d{4})$/', $fin)) { + $fin_formatted = $fin; + } + + // Afficher la période + $periode_text = ''; + if ($debut_formatted && $fin_formatted) { + if ($debut_formatted === $fin_formatted) { + $periode_text = 'Le ' . $debut_formatted; + } else { + $periode_text = 'Du ' . $debut_formatted . ' au ' . $fin_formatted; + } + } elseif ($debut_formatted) { + $periode_text = 'Le ' . $debut_formatted; + } + ?> +
+
+ + + + +

+ +
+ +
+ + +
+ Aucune indisponibilité prévue. +
+ +
+ + + +
+ + +
+ +
+
+
+
+ + +
+
+

+ Départements et spécialisations +

+
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ + + Ces informations sont gérées par l'administrateur. + +
+
+
+
+
+
+ diff --git a/templates/modules/agenda-modal.php b/templates/modules/agenda-modal.php new file mode 100644 index 0000000..54228ff --- /dev/null +++ b/templates/modules/agenda-modal.php @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/templates/modules/intervenant-profile.php b/templates/modules/intervenant-profile.php new file mode 100644 index 0000000..7a60b45 --- /dev/null +++ b/templates/modules/intervenant-profile.php @@ -0,0 +1,346 @@ +ID; + +// Récupérer tous les champs ACF pour pré-remplir le formulaire +$telephone = ''; +$jours_disponibilite = []; +$indisponibilites_ponctuelles = []; +if (function_exists('get_field')) { + $telephone = get_field('telephone', 'user_' . $user_id); + if (!is_string($telephone)) { + $telephone = ''; + } + + $jours_disponibilite = get_field('jours_de_disponibilite', 'user_' . $user_id); + if (!is_array($jours_disponibilite)) { + $jours_disponibilite = []; + } + + $indisponibilites_ponctuelles = get_field('indisponibilitee_ponctuelle', 'user_' . $user_id); + if (!is_array($indisponibilites_ponctuelles)) { + $indisponibilites_ponctuelles = []; + } +} + +// Labels des types d'indisponibilité +$types_indisponibilite = [ + 'conge' => 'Congé', + 'absence' => 'Absence', + 'maladie' => 'Maladie' +]; + +// Fonction pour convertir une date au format d/m/Y vers YYYY-MM-DD pour les champs date +function convert_date_to_input($date_str) { + if (empty($date_str)) { + return ''; + } + // Si déjà au format YYYY-MM-DD, retourner tel quel + if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $date_str)) { + return $date_str; + } + // Si au format d/m/Y, convertir + if (preg_match('/^(\d{2})\/(\d{2})\/(\d{4})$/', $date_str, $matches)) { + return $matches[3] . '-' . $matches[2] . '-' . $matches[1]; + } + return ''; +} + +// Labels des jours +$jours_labels = [ + 'lundi' => 'Lundi', + 'mardi' => 'Mardi', + 'mercredi' => 'Mercredi', + 'jeudi' => 'Jeudi', + 'vendredi' => 'Vendredi', + 'samedi' => 'Samedi', + 'dimanche' => 'Dimanche' +]; +?> + +
+
+ +
+
+

+ Mon Profil +

+ +
+
+ +
+
+ +
+
+

+ Mes informations personnelles +

+
+
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ + +
+ Vous pouvez modifier votre numéro de téléphone +
+ + +
+ + +
+
+
+
+ + +
+
+

+ Mes disponibilités +

+
+
+
+ +
+ +
+ $jour_label) : + $is_checked = in_array($jour_value, $jours_disponibilite, true); + ?> +
+
+ + > + +
+
+ +
+ Cochez les jours où vous êtes disponible +
+ + +
+
+ + +
+ + +
+ + $indispo) : + $debut = $indispo['debut'] ?? ''; + $fin = $indispo['fin'] ?? ''; + $type = $indispo['type'] ?? 'conge'; + $commentaire = $indispo['commentaire'] ?? ''; + $type_label = $types_indisponibilite[$type] ?? $type; + + // Convertir les dates pour l'affichage + $debut_formatted = $debut; + $fin_formatted = $fin; + + // Si les dates sont au format d/m/Y, les formater pour l'affichage + if (preg_match('/^(\d{2})\/(\d{2})\/(\d{4})$/', $debut)) { + $debut_formatted = $debut; + } + if (preg_match('/^(\d{2})\/(\d{2})\/(\d{4})$/', $fin)) { + $fin_formatted = $fin; + } + + // Afficher la période + $periode_text = ''; + if ($debut_formatted && $fin_formatted) { + if ($debut_formatted === $fin_formatted) { + $periode_text = 'Le ' . $debut_formatted; + } else { + $periode_text = 'Du ' . $debut_formatted . ' au ' . $fin_formatted; + } + } elseif ($debut_formatted) { + $periode_text = 'Le ' . $debut_formatted; + } + ?> +
+
+ + + +

+ +
+ +
+ + +
+ Aucune indisponibilité prévue. +
+ +
+ + + +
+ + +
+ +
+
+
+
+ + +
+
+

+ Départements et spécialisations +

+
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ + + Ces informations sont gérées par l'administrateur. + +
+
+
+
+
+
+ diff --git a/templates/modules/modals/beneficiaire-historique-modal.php b/templates/modules/modals/beneficiaire-historique-modal.php new file mode 100644 index 0000000..a4aa274 --- /dev/null +++ b/templates/modules/modals/beneficiaire-historique-modal.php @@ -0,0 +1,32 @@ + + + + diff --git a/templates/modules/modals/create-beneficiaire-modal.php b/templates/modules/modals/create-beneficiaire-modal.php new file mode 100644 index 0000000..bf90fbf --- /dev/null +++ b/templates/modules/modals/create-beneficiaire-modal.php @@ -0,0 +1,104 @@ + + + + + + diff --git a/templates/modules/modals/create-intervenant-modal.php b/templates/modules/modals/create-intervenant-modal.php new file mode 100644 index 0000000..e8731c3 --- /dev/null +++ b/templates/modules/modals/create-intervenant-modal.php @@ -0,0 +1,103 @@ + + + + + + diff --git a/templates/modules/modals/create-local-modal.php b/templates/modules/modals/create-local-modal.php new file mode 100644 index 0000000..5c971eb --- /dev/null +++ b/templates/modules/modals/create-local-modal.php @@ -0,0 +1,64 @@ + + + + + + diff --git a/templates/modules/modals/create-traducteur-modal.php b/templates/modules/modals/create-traducteur-modal.php new file mode 100644 index 0000000..bcc0b77 --- /dev/null +++ b/templates/modules/modals/create-traducteur-modal.php @@ -0,0 +1,85 @@ + + + + + + diff --git a/templates/modules/modals/declaration-incident-modal.php b/templates/modules/modals/declaration-incident-modal.php new file mode 100644 index 0000000..bee90b3 --- /dev/null +++ b/templates/modules/modals/declaration-incident-modal.php @@ -0,0 +1,69 @@ + + + + diff --git a/templates/modules/modals/event-check-presence-modal.php b/templates/modules/modals/event-check-presence-modal.php new file mode 100644 index 0000000..d087220 --- /dev/null +++ b/templates/modules/modals/event-check-presence-modal.php @@ -0,0 +1,51 @@ + + + + + + diff --git a/templates/modules/modals/event-modal.php b/templates/modules/modals/event-modal.php new file mode 100644 index 0000000..b522811 --- /dev/null +++ b/templates/modules/modals/event-modal.php @@ -0,0 +1,560 @@ + + + + + + diff --git a/templates/modules/permanences-admin.php b/templates/modules/permanences-admin.php new file mode 100644 index 0000000..02986c8 --- /dev/null +++ b/templates/modules/permanences-admin.php @@ -0,0 +1,516 @@ + 'langue', + 'hide_empty' => false, + 'orderby' => 'name', + 'order' => 'ASC', +]); + +$langues = []; +if (!is_wp_error($langues_terms) && !empty($langues_terms)) { + foreach ($langues_terms as $term) { + $langues[] = [ + 'id' => $term->slug, + 'nom' => $term->name, + ]; + } +} +?> + +
+
+ +
+
+

+ Créez des permanences pour un intervenant en sélectionnant la période, les jours et la plage horaire, ou importez-les via CSV. +

+
+
+ +
+
+ + + + +
+ +
+
+ +
+
+

+ Intervenant +

+
+
+
+ + +
+
+
+ + +
+
+

+ SECTION 1 : Sélection des permanences +

+
+
+ +
+ + + + Sélectionnez le mois et l'année à partir desquels les permanences seront créées. + +
+ + +
+ +
+ + + + + +
+
+ + +
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+ +
+ + + + + +
+ + Choisissez la durée de chaque permanence. + +
+ + + + + +
+ + + Sélectionnez les heures de début. Chaque heure sélectionnée créera une tranche d'1 heure (ex: 09:00 → 10:00). + +
+
+ +
+ +
+
+ + +
+
+ + +
+ +
+ +
+
+ + +
+
+ +
+
+ + +
+ +
+ + Veuillez sélectionner une plage horaire pour voir l'aperçu. +
+
+ + +
+ +
+
+
+ + +
+
+

+ SECTION 2 : Langues des permanences +

+
+
+
+ + + + Sélectionnez une ou plusieurs langues pour ces permanences. + +
+
+
+ + +
+
+

+ SECTION 3 : Informations complémentaires +

+
+
+
+ + + + Ces informations seront associées aux permanences créées. + +
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
+

+ Import de permanences via CSV +

+
+
+
+ + Format CSV attendu : Le fichier CSV doit contenir les colonnes suivantes : +
    +
  • intervenant_id : ID de l'intervenant
  • +
  • date_debut : Date de début (format YYYY-MM-DD)
  • +
  • date_fin : Date de fin (format YYYY-MM-DD)
  • +
  • heure_debut : Heure de début (format HH:MM)
  • +
  • heure_fin : Heure de fin (format HH:MM)
  • +
  • informations_complementaires : Notes (optionnel)
  • +
+
+ +
+
+ + + + Format accepté : CSV (séparateur virgule) + +
+ +
+ + +
+
+ + +
+
+
+
+
+
+
+
+ + + + + diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..76c1e51 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,22 @@ + '2024-07-01', + 'commentaire' => 'Vacances', + ], + [ + 'date' => '2024-07-15', + 'commentaire' => 'Congé maladie', + ], + ]; + $controller = new CRVI_Intervenant_Controller(); + $result = $controller->parse_repeater_field($input); + $this->assertEquals($expected, $result); + } +} \ No newline at end of file