First commit
This commit is contained in:
parent
54605c1715
commit
412268a186
94
ESI_crvi_agenda.php
Normal file
94
ESI_crvi_agenda.php
Normal file
@ -0,0 +1,94 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: ESI_crvi_agenda
|
||||
* Description: Gestion d'agenda avancée pour CRVI (Custom Post Types, ACF, FullCalendar, API export...)
|
||||
* Version: 0.1.0
|
||||
* Author: Votre Nom
|
||||
* Text Domain: esi_crvi_agenda
|
||||
*/
|
||||
declare(strict_types=1);
|
||||
|
||||
define('ESI_CRVI_AGENDA_VERSION', '0.1.0');
|
||||
define('ESI_CRVI_AGENDA_DIR', plugin_dir_path(__FILE__));
|
||||
define('ESI_CRVI_AGENDA_URL', plugin_dir_url(__FILE__));
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Autoloader simple pour les classes CRVI et celles avec namespace
|
||||
spl_autoload_register(function ($class) {
|
||||
|
||||
if (strpos($class, 'ESI_CRVI_AGENDA\\') === 0 || strpos($class, 'CRVI_') === 0) {
|
||||
|
||||
// Gestion des classes avec namespace ESI_CRVI_AGENDA
|
||||
if (strpos($class, 'ESI_CRVI_AGENDA') === 0) {
|
||||
$base_dir = __DIR__ . '/app/';
|
||||
|
||||
$relative_class = substr($class, strlen('ESI_CRVI_AGENDA\\'));
|
||||
|
||||
// Mapping simple pour les classes CRVI_
|
||||
if (strpos($relative_class, 'controllers\\CRVI_') === 0) {
|
||||
$class_name = substr($relative_class, strlen('controllers\\CRVI_'));
|
||||
// Pour CRVI_Event_Controller -> 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();
|
||||
15
README_old.md
Normal file
15
README_old.md
Normal file
@ -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.
|
||||
27
app/config.php
Normal file
27
app/config.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
// Configuration du plugin CRVI Agenda
|
||||
|
||||
// Configuration SMS OVH
|
||||
// Ces valeurs peuvent être surchargées par des constantes définies ailleurs
|
||||
if (!defined('OVH_SMS_ACCOUNT')) {
|
||||
define('OVH_SMS_ACCOUNT', 'sms-xxxxxxx'); // Identifiant SMS OVH (à configurer)
|
||||
}
|
||||
if (!defined('OVH_SMS_FROM')) {
|
||||
define('OVH_SMS_FROM', '0032496390437'); // Numéro expéditeur (à configurer)
|
||||
}
|
||||
|
||||
// Note: login et mot_de_passe sont configurés dans ACF > 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;
|
||||
71
app/controllers/Agenda_Controller.ofd.php
Normal file
71
app/controllers/Agenda_Controller.ofd.php
Normal file
@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ESI_CRVI_AGENDA\controllers;
|
||||
|
||||
use ESI_CRVI_AGENDA\models\Event_Model;
|
||||
use ESI_CRVI_AGENDA\helpers\Api_Helper;
|
||||
use ESI_CRVI_AGENDA\models\CRVI_Event_Model;
|
||||
|
||||
class CRVI_Agenda_Controller {
|
||||
public static function register_routes() {
|
||||
\register_rest_route('crvi/v1', '/agenda/disponibilites', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [self::class, 'get_disponibilites'],
|
||||
'permission_callback' => '__return_true',
|
||||
]);
|
||||
|
||||
\register_rest_route('crvi/v1', '/events/(?P<id>\\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);
|
||||
}
|
||||
}
|
||||
338
app/controllers/Beneficiaire_Controller.php
Normal file
338
app/controllers/Beneficiaire_Controller.php
Normal file
@ -0,0 +1,338 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ESI_CRVI_AGENDA\controllers;
|
||||
|
||||
use ESI_CRVI_AGENDA\models\CRVI_Beneficiaire_Model;
|
||||
use ESI_CRVI_AGENDA\helpers\Api_Helper;
|
||||
|
||||
class CRVI_Beneficiaire_Controller {
|
||||
public static function register_cpt() {
|
||||
\register_post_type('beneficiaire', [
|
||||
'label' => '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<id>\\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<id>\\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<id>\\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;
|
||||
}
|
||||
}
|
||||
103
app/controllers/Departement_Controller.php
Normal file
103
app/controllers/Departement_Controller.php
Normal file
@ -0,0 +1,103 @@
|
||||
<?php
|
||||
namespace ESI_CRVI_AGENDA\controllers;
|
||||
|
||||
use ESI_CRVI_AGENDA\models\CRVI_Departement_Model;
|
||||
|
||||
class CRVI_Departement_Controller {
|
||||
public static function register_cpt() {
|
||||
register_post_type('departement', [
|
||||
'label' => '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<id>\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<id>\d+)', [
|
||||
'methods' => 'PUT',
|
||||
'callback' => [self::class, 'update_item'],
|
||||
'permission_callback' => [self::class, 'can_edit'],
|
||||
]);
|
||||
|
||||
register_rest_route('crvi_agenda/v1', '/departements/(?P<id>\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;
|
||||
}
|
||||
|
||||
}
|
||||
361
app/controllers/Entity_Controller.php
Normal file
361
app/controllers/Entity_Controller.php
Normal file
@ -0,0 +1,361 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ESI_CRVI_AGENDA\controllers;
|
||||
|
||||
use ESI_CRVI_AGENDA\helpers\Api_Helper;
|
||||
use ESI_CRVI_AGENDA\controllers\CRVI_Local_Controller;
|
||||
use ESI_CRVI_AGENDA\controllers\CRVI_Traducteur_Controller;
|
||||
use ESI_CRVI_AGENDA\controllers\CRVI_Intervenant_Controller;
|
||||
use ESI_CRVI_AGENDA\controllers\CRVI_Beneficiaire_Controller;
|
||||
|
||||
class CRVI_Entity_Controller {
|
||||
|
||||
public static function register_routes() {
|
||||
// Endpoints pour la création de bénéficiaires
|
||||
\register_rest_route('crvi/v1', '/agenda/beneficiaires', [
|
||||
[
|
||||
'methods' => '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;
|
||||
} */
|
||||
|
||||
}
|
||||
2175
app/controllers/Event_Controller.php
Normal file
2175
app/controllers/Event_Controller.php
Normal file
File diff suppressed because it is too large
Load Diff
486
app/controllers/Event_controller.old.php
Normal file
486
app/controllers/Event_controller.old.php
Normal file
@ -0,0 +1,486 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ESI_CRVI_AGENDA\controllers;
|
||||
|
||||
use ESI_CRVI_AGENDA\models\Event_Model;
|
||||
use ESI_CRVI_AGENDA\helpers\Api_Helper;
|
||||
use ESI_CRVI_AGENDA\models\CRVI_Event_Model;
|
||||
use ESI_CRVI_AGENDA\models\CRVI_Traducteur_Model;
|
||||
use ESI_CRVI_AGENDA\models\CRVI_Intervenant_Model;
|
||||
use ESI_CRVI_AGENDA\controllers\Intervenant_Controller;
|
||||
|
||||
class CRVI_Event_Controller {
|
||||
public static function register_routes() {
|
||||
|
||||
register_rest_route('crvi/v1', '/agenda/permissions', [
|
||||
[
|
||||
'methods' => '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<id>\\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<id>\\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);
|
||||
}
|
||||
}
|
||||
196
app/controllers/Incident_Controller.php
Normal file
196
app/controllers/Incident_Controller.php
Normal file
@ -0,0 +1,196 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ESI_CRVI_AGENDA\controllers;
|
||||
|
||||
use ESI_CRVI_AGENDA\models\CRVI_Incident_Model;
|
||||
use ESI_CRVI_AGENDA\helpers\Api_Helper;
|
||||
|
||||
class CRVI_Incident_Controller {
|
||||
|
||||
/**
|
||||
* Enregistre les routes REST pour les incidents
|
||||
*/
|
||||
public static function register_routes() {
|
||||
\register_rest_route('crvi/v1', '/incidents', [
|
||||
[
|
||||
'methods' => '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<id>\\d+)', [
|
||||
[
|
||||
'methods' => 'GET',
|
||||
'callback' => [self::class, 'get_incident'],
|
||||
'permission_callback' => [self::class, 'can_view'],
|
||||
],
|
||||
]);
|
||||
|
||||
\register_rest_route('crvi/v1', '/events/(?P<event_id>\\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');
|
||||
}
|
||||
}
|
||||
|
||||
466
app/controllers/Intervenant_Controller.php
Normal file
466
app/controllers/Intervenant_Controller.php
Normal file
@ -0,0 +1,466 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ESI_CRVI_AGENDA\controllers;
|
||||
|
||||
use ESI_CRVI_AGENDA\models\CRVI_Intervenant_Model;
|
||||
use ESI_CRVI_AGENDA\helpers\Api_Helper;
|
||||
|
||||
class CRVI_Intervenant_Controller {
|
||||
public static function register_cpt() {
|
||||
\register_post_type('intervenant', [
|
||||
'label' => '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<id>\\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;
|
||||
}
|
||||
}
|
||||
1097
app/controllers/Intervenant_Space_Controller.php
Normal file
1097
app/controllers/Intervenant_Space_Controller.php
Normal file
File diff suppressed because it is too large
Load Diff
358
app/controllers/Local_Controller.php
Normal file
358
app/controllers/Local_Controller.php
Normal file
@ -0,0 +1,358 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ESI_CRVI_AGENDA\controllers;
|
||||
|
||||
use ESI_CRVI_AGENDA\models\CRVI_Local_Model;
|
||||
use ESI_CRVI_AGENDA\helpers\Api_Helper;
|
||||
|
||||
class CRVI_Local_Controller {
|
||||
public static function register_cpt() {
|
||||
register_post_type('local', [
|
||||
'label' => '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<id>\\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;
|
||||
}
|
||||
}
|
||||
561
app/controllers/Notifications_Controller.php
Normal file
561
app/controllers/Notifications_Controller.php
Normal file
@ -0,0 +1,561 @@
|
||||
<?php
|
||||
|
||||
namespace ESI_CRVI_AGENDA\controllers;
|
||||
|
||||
use ESI_CRVI_AGENDA\controllers\CRVI_Plugin;
|
||||
|
||||
include_once plugin_dir_path(__FILE__) . '../config.php';
|
||||
|
||||
class CRVI_Notifications_Controller {
|
||||
public function __construct() {
|
||||
add_action('init', [$this, 'init']);
|
||||
}
|
||||
|
||||
public function init() {
|
||||
/* add_action('wp_ajax_crvi_notifications', [$this, 'crvi_notifications']); */
|
||||
add_action('wp_ajax_crvi_send_sms_debug', [self::class, 'ajax_send_sms_debug']);
|
||||
add_action('wp_ajax_nopriv_crvi_send_sms_debug', [self::class, 'ajax_send_sms_debug']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée la table de log des SMS si elle n'existe pas
|
||||
* Schéma basé sur DOC-2/wp_crvi_sms_log.sql
|
||||
*/
|
||||
public static function create_sms_log_table() {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->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;
|
||||
}
|
||||
}
|
||||
301
app/controllers/Plugin - Copie.php
Normal file
301
app/controllers/Plugin - Copie.php
Normal file
@ -0,0 +1,301 @@
|
||||
<?php
|
||||
|
||||
namespace ESI_CRVI_AGENDA\controllers;
|
||||
|
||||
use ESI_CRVI_AGENDA\controllers\CRVI_Local_Controller;
|
||||
use ESI_CRVI_AGENDA\controllers\CRVI_Beneficiaire_Controller;
|
||||
use ESI_CRVI_AGENDA\controllers\CRVI_Intervenant_Controller;
|
||||
use ESI_CRVI_AGENDA\controllers\CRVI_Traducteur_Controller;
|
||||
use ESI_CRVI_AGENDA\controllers\CRVI_Departement_Controller;
|
||||
use ESI_CRVI_AGENDA\controllers\CRVI_Type_Intervention_Controller;
|
||||
use ESI_CRVI_AGENDA\models\CRVI_Event_Model;
|
||||
use ESI_CRVI_AGENDA\controllers\CRVI_Event_Controller;
|
||||
use ESI_CRVI_AGENDA\controllers\CRVI_Entity_Controller;
|
||||
use ESI_CRVI_AGENDA\views\CRVI_Main_View;
|
||||
use ESI_CRVI_AGENDA\views\CRVI_Agenda_View;
|
||||
|
||||
class CRVI_Plugin {
|
||||
public static function init() {
|
||||
// Initialisation du plugin (CPT, hooks, etc.)
|
||||
/* add_action('init', [self::class, 'register_cpt']); */
|
||||
/* self::register_cpt(); */
|
||||
self::load_actions();
|
||||
self::load_filters();
|
||||
self::load_shortcodes();
|
||||
self::load_frontend_assets();
|
||||
self::load_admin_assets();
|
||||
}
|
||||
public static function activate() {
|
||||
// Code d'activation (création de tables, etc.)
|
||||
|
||||
// create table
|
||||
CRVI_Event_Model::create_table();
|
||||
}
|
||||
public static function deactivate() {
|
||||
// Code de désactivation (nettoyage, etc.)
|
||||
}
|
||||
public static function register_cpt() {
|
||||
CRVI_Local_Controller::register_cpt();
|
||||
CRVI_Beneficiaire_Controller::register_cpt();
|
||||
/* CRVI_Intervenant_Controller::register_cpt(); */
|
||||
CRVI_Traducteur_Controller::register_cpt();
|
||||
|
||||
|
||||
//register departement et type_intervention
|
||||
CRVI_Departement_Controller::register_cpt();
|
||||
CRVI_Type_Intervention_Controller::register_cpt();
|
||||
}
|
||||
|
||||
public function register_routes() {
|
||||
// Vérification et enregistrement des routes avec gestion d'erreur
|
||||
/* $controllers = [
|
||||
'CRVI_Local_Controller',
|
||||
'CRVI_Beneficiaire_Controller',
|
||||
'CRVI_Intervenant_Controller',
|
||||
'CRVI_Traducteur_Controller',
|
||||
'CRVI_Departement_Controller',
|
||||
'CRVI_Event_Controller',
|
||||
'CRVI_Entity_Controller'
|
||||
];
|
||||
|
||||
foreach ($controllers as $controller) {
|
||||
if (class_exists($controller)) {
|
||||
try {
|
||||
$controller::register_routes();
|
||||
} catch (\Exception $e) {
|
||||
error_log("Erreur lors de l'enregistrement des routes pour {$controller}: " . $e->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 '<div class="wrap"><h1>' . esc_html__('Statistiques Agenda', 'esi_crvi_agenda') . '</h1>';
|
||||
$template = plugin_dir_path(__FILE__) . '../../templates/admin/agenda-stats-form.php';
|
||||
if (file_exists($template)) {
|
||||
include $template;
|
||||
} else {
|
||||
echo '<p style="color:red">Template agenda-stats-form.php introuvable.</p>';
|
||||
}
|
||||
// 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 '<div class="notice notice-info"><p>';
|
||||
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 '</p></div>';
|
||||
}
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
1220
app/controllers/Plugin.php
Normal file
1220
app/controllers/Plugin.php
Normal file
File diff suppressed because it is too large
Load Diff
265
app/controllers/Traducteur_Controller.php
Normal file
265
app/controllers/Traducteur_Controller.php
Normal file
@ -0,0 +1,265 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ESI_CRVI_AGENDA\controllers;
|
||||
|
||||
use ESI_CRVI_AGENDA\models\CRVI_Traducteur_Model;
|
||||
use ESI_CRVI_AGENDA\helpers\Api_Helper;
|
||||
|
||||
class CRVI_Traducteur_Controller {
|
||||
public static function register_cpt() {
|
||||
\register_post_type('traducteur', [
|
||||
'label' => 'Traducteurs',
|
||||
'labels' => [
|
||||
'name' => 'Traducteurs',
|
||||
'singular_name' => 'Traducteur',
|
||||
'add_new' => 'Ajouter un traducteur',
|
||||
'add_new_item' => 'Ajouter un nouveau traducteur',
|
||||
'edit_item' => 'Modifier le traducteur',
|
||||
'new_item' => 'Nouveau traducteur',
|
||||
'view_item' => 'Voir le traducteur',
|
||||
'search_items' => 'Rechercher un traducteur',
|
||||
'not_found' => 'Aucun traducteur trouvé',
|
||||
'not_found_in_trash' => 'Aucun traducteur dans la corbeille',
|
||||
],
|
||||
'public' => true,
|
||||
'show_in_menu' => true,
|
||||
'menu_position' => 24,
|
||||
'menu_icon' => 'dashicons-translation',
|
||||
'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 'traducteur'
|
||||
\register_taxonomy_for_object_type('langue', 'traducteur');
|
||||
\register_taxonomy_for_object_type('organisme', 'traducteur');
|
||||
}
|
||||
|
||||
public static function register_taxonomy() {
|
||||
\register_taxonomy('langue', ['traducteur'], [
|
||||
'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' ],
|
||||
]);
|
||||
|
||||
\register_taxonomy('organisme', ['traducteur'], [
|
||||
'label' => 'Organismes',
|
||||
'labels' => [
|
||||
'name' => 'Organismes',
|
||||
'singular_name' => 'Organisme',
|
||||
],
|
||||
'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' => 'organisme' ],
|
||||
]);
|
||||
}
|
||||
|
||||
public static function get_traducteur_by_langue($langue) {
|
||||
$langue_obj = \get_term_by('slug', $langue, 'langue');
|
||||
|
||||
if (!$langue_obj) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$traducteurs = \get_posts([
|
||||
'post_type' => 'traducteur',
|
||||
'tax_query' => [
|
||||
[
|
||||
'taxonomy' => 'langue',
|
||||
'field' => 'slug',
|
||||
'terms' => $langue, // ou un tableau de slugs
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
return $traducteurs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne les traducteurs disponibles à une date donnée et pour une langue donnée.
|
||||
* @param string $date au format Y-m-d
|
||||
* @param string $langue_slug (slug de la langue)
|
||||
* @return array Liste des traducteurs disponibles (CRVI_Traducteur_Model)
|
||||
*/
|
||||
public static function filtrer_disponibles($date, $langue_slug) {
|
||||
return CRVI_Traducteur_Model::filtrer_disponibles($date, $langue_slug);
|
||||
}
|
||||
|
||||
public static function register_routes() {
|
||||
\register_rest_route('crvi/v1', '/traducteurs', [
|
||||
[
|
||||
'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', '/traducteurs/(?P<id>\\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_edit'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public static function get_item($request) {
|
||||
$id = (int) $request['id'];
|
||||
$traducteur = CRVI_Traducteur_Model::load($id);
|
||||
if (!$traducteur) {
|
||||
return [
|
||||
'success' => false,
|
||||
'code' => 404,
|
||||
'message' => 'Traducteur introuvable',
|
||||
'data' => null
|
||||
];
|
||||
}
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => $traducteur
|
||||
];
|
||||
}
|
||||
|
||||
public static function create_item($request) {
|
||||
$data = $request->get_json_params();
|
||||
$validation = self::validate_traducteur_data($data);
|
||||
if (is_wp_error($validation)) {
|
||||
return Api_Helper::json_error($validation->get_error_message(), 400);
|
||||
}
|
||||
|
||||
$result = CRVI_Traducteur_Model::create($data, true);
|
||||
if (!is_numeric($result)) {
|
||||
return $result;
|
||||
}
|
||||
return Api_Helper::json_success([
|
||||
'id' => $result,
|
||||
'nom' => $data['nom'],
|
||||
'message' => 'Traducteur créé avec succès'
|
||||
]);
|
||||
}
|
||||
|
||||
public static function update_item($request) {
|
||||
$id = (int) $request['id'];
|
||||
$data = $request->get_json_params();
|
||||
$traducteur = new CRVI_Traducteur_Model();
|
||||
$result = $traducteur->update($id, $data);
|
||||
if ($result !== true) {
|
||||
return $result;
|
||||
}
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => [ 'id' => $id, 'message' => 'Traducteur modifié avec succès' ]
|
||||
];
|
||||
}
|
||||
|
||||
public static function delete_item($request) {
|
||||
$id = (int) $request['id'];
|
||||
$traducteur = new CRVI_Traducteur_Model();
|
||||
$result = $traducteur->delete($id);
|
||||
if ($result !== true) {
|
||||
return $result;
|
||||
}
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => [ 'id' => $id, 'message' => 'Traducteur supprimé avec succès' ]
|
||||
];
|
||||
}
|
||||
|
||||
public static function can_edit($request = null) {
|
||||
return current_user_can('edit_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_traducteur');
|
||||
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))
|
||||
);
|
||||
|
||||
$result = CRVI_Traducteur_Model::create($data, true);
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation des données de traducteur
|
||||
*/
|
||||
public 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;
|
||||
}
|
||||
}
|
||||
496
app/controllers/TraductionLangue_Controller.php
Normal file
496
app/controllers/TraductionLangue_Controller.php
Normal file
@ -0,0 +1,496 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ESI_CRVI_AGENDA\controllers;
|
||||
|
||||
/**
|
||||
* Contrôleur pour le CPT traduction_langue
|
||||
* Gère les capacités de traduction par langue, jour et période
|
||||
*/
|
||||
class CRVI_TraductionLangue_Controller {
|
||||
|
||||
/**
|
||||
* Enregistre le CPT traduction_langue et le groupe ACF associé
|
||||
*/
|
||||
public static function register_cpt() {
|
||||
// Enregistrement du CPT traduction_langue
|
||||
\register_post_type('traduction_langue', [
|
||||
'label' => 'Capacités de traduction',
|
||||
'labels' => [
|
||||
'name' => 'Capacités de traduction',
|
||||
'singular_name' => 'Capacité de traduction',
|
||||
'add_new' => 'Ajouter une capacité',
|
||||
'add_new_item' => 'Ajouter une nouvelle capacité de traduction',
|
||||
'edit_item' => 'Modifier la capacité de traduction',
|
||||
'new_item' => 'Nouvelle capacité de traduction',
|
||||
'view_item' => 'Voir la capacité de traduction',
|
||||
'search_items' => 'Rechercher une capacité',
|
||||
'not_found' => 'Aucune capacité trouvée',
|
||||
'not_found_in_trash' => 'Aucune capacité dans la corbeille',
|
||||
],
|
||||
'public' => false, // CPT non public
|
||||
'show_ui' => true, // UI active dans l'admin
|
||||
'show_in_menu' => false, // Menu masqué
|
||||
'hierarchical' => true, // CPT hiérarchique (pour parent/enfant)
|
||||
'supports' => ['title'],
|
||||
'has_archive' => false,
|
||||
'show_in_rest' => true, // Support REST API
|
||||
'rewrite' => false, // Pas de rewrite car non public
|
||||
]);
|
||||
|
||||
// Association de la taxonomie 'langue' au CPT 'traduction_langue'
|
||||
// La taxonomie existe déjà (déclarée dans Traducteur_Controller)
|
||||
if (taxonomy_exists('langue')) {
|
||||
\register_taxonomy_for_object_type('langue', 'traduction_langue');
|
||||
}
|
||||
|
||||
// Création du groupe ACF capacite_traduction
|
||||
/* self::register_acf_field_group(); */
|
||||
|
||||
// Enregistrement des validations ACF
|
||||
\add_filter('acf/validate_value', [self::class, 'validate_acf_fields'], 10, 4);
|
||||
|
||||
// Enregistrement du hook de suppression en cascade
|
||||
\add_action('before_delete_post', [self::class, 'handle_cascade_delete'], 10, 2);
|
||||
|
||||
// Enregistrement du hook de verrouillage si événements existent
|
||||
\add_filter('acf/validate_save_post', [self::class, 'validate_save_post_with_events'], 10, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enregistre le groupe ACF capacite_traduction avec tous les champs nécessaires
|
||||
*/
|
||||
private static function register_acf_field_group() {
|
||||
// Vérifier que ACF est actif
|
||||
if (!function_exists('acf_add_local_field_group')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Création du groupe ACF avec tous les champs
|
||||
\acf_add_local_field_group([
|
||||
'key' => 'group_capacite_traduction',
|
||||
'title' => 'Capacité de traduction',
|
||||
'fields' => [
|
||||
// Champ: langue (taxonomy)
|
||||
[
|
||||
'key' => 'field_capacite_langue',
|
||||
'label' => 'Langue',
|
||||
'name' => 'langue',
|
||||
'type' => 'taxonomy',
|
||||
'instructions' => 'Sélectionnez la langue pour cette capacité',
|
||||
'required' => 1,
|
||||
'taxonomy' => 'langue',
|
||||
'field_type' => 'select',
|
||||
'allow_null' => 0,
|
||||
'return_format' => 'id',
|
||||
],
|
||||
// Champ: jour (lundi → dimanche)
|
||||
[
|
||||
'key' => 'field_capacite_jour',
|
||||
'label' => 'Jour',
|
||||
'name' => 'jour',
|
||||
'type' => 'select',
|
||||
'instructions' => 'Sélectionnez le jour de la semaine',
|
||||
'required' => 1,
|
||||
'choices' => [
|
||||
'lundi' => 'Lundi',
|
||||
'mardi' => 'Mardi',
|
||||
'mercredi' => 'Mercredi',
|
||||
'jeudi' => 'Jeudi',
|
||||
'vendredi' => 'Vendredi',
|
||||
'samedi' => 'Samedi',
|
||||
'dimanche' => 'Dimanche',
|
||||
],
|
||||
'default_value' => '',
|
||||
'allow_null' => 0,
|
||||
'return_format' => 'value',
|
||||
],
|
||||
// Champ: periode (matin / apres_midi / journee)
|
||||
[
|
||||
'key' => 'field_capacite_periode',
|
||||
'label' => 'Période',
|
||||
'name' => 'periode',
|
||||
'type' => 'select',
|
||||
'instructions' => 'Sélectionnez la période de la journée',
|
||||
'required' => 1,
|
||||
'choices' => [
|
||||
'matin' => 'Matin',
|
||||
'apres_midi' => 'Après-midi',
|
||||
'journee' => 'Journée',
|
||||
],
|
||||
'default_value' => '',
|
||||
'allow_null' => 0,
|
||||
'return_format' => 'value',
|
||||
],
|
||||
// Champ: limite (number)
|
||||
[
|
||||
'key' => 'field_capacite_limite',
|
||||
'label' => 'Limite',
|
||||
'name' => 'limite',
|
||||
'type' => 'number',
|
||||
'instructions' => 'Nombre maximum de créneaux disponibles',
|
||||
'required' => 1,
|
||||
'default_value' => 0,
|
||||
'min' => 0,
|
||||
'step' => 1,
|
||||
],
|
||||
// Champ: limite_par (semaine / mois)
|
||||
[
|
||||
'key' => 'field_capacite_limite_par',
|
||||
'label' => 'Limite par',
|
||||
'name' => 'limite_par',
|
||||
'type' => 'select',
|
||||
'instructions' => 'Période de calcul de la limite',
|
||||
'required' => 1,
|
||||
'choices' => [
|
||||
'semaine' => 'Semaine',
|
||||
'mois' => 'Mois',
|
||||
],
|
||||
'default_value' => 'semaine',
|
||||
'allow_null' => 0,
|
||||
'return_format' => 'value',
|
||||
],
|
||||
// Champ: actif (true/false)
|
||||
[
|
||||
'key' => 'field_capacite_actif',
|
||||
'label' => 'Actif',
|
||||
'name' => 'actif',
|
||||
'type' => 'true_false',
|
||||
'instructions' => 'Activez ou désactivez cette capacité de traduction',
|
||||
'required' => 0,
|
||||
'default_value' => 1,
|
||||
'ui' => 1,
|
||||
'ui_on_text' => 'Oui',
|
||||
'ui_off_text' => 'Non',
|
||||
],
|
||||
],
|
||||
'location' => [
|
||||
[
|
||||
[
|
||||
'param' => 'post_type',
|
||||
'operator' => '==',
|
||||
'value' => 'traduction_langue',
|
||||
],
|
||||
],
|
||||
],
|
||||
'menu_order' => 0,
|
||||
'position' => 'normal',
|
||||
'style' => 'default',
|
||||
'label_placement' => 'top',
|
||||
'instruction_placement' => 'label',
|
||||
'hide_on_screen' => '',
|
||||
'active' => true,
|
||||
'description' => 'Configuration des capacités de traduction par langue, jour et période',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Valide les champs ACF avant la sauvegarde
|
||||
*
|
||||
* @param bool|string $valid État de validation (true ou message d'erreur)
|
||||
* @param mixed $value Valeur du champ
|
||||
* @param array $field Configuration du champ ACF
|
||||
* @param string $input Nom de l'input HTML
|
||||
* @return bool|string
|
||||
*/
|
||||
public static function validate_acf_fields($valid, $value, $field, $input) {
|
||||
// Ne valider que si c'est déjà valide et que c'est un champ de notre groupe
|
||||
if (!$valid || !isset($field['key']) || strpos($field['key'], 'field_capacite_') !== 0) {
|
||||
return $valid;
|
||||
}
|
||||
|
||||
// Récupérer le post_id depuis $_POST
|
||||
$post_id = isset($_POST['post_ID']) ? (int) $_POST['post_ID'] : 0;
|
||||
|
||||
// Ne pas valider pour les nouveaux posts
|
||||
if (!$post_id || $post_id === 0) {
|
||||
return $valid;
|
||||
}
|
||||
|
||||
// Vérifier que c'est bien un post traduction_langue
|
||||
$post = \get_post($post_id);
|
||||
if (!$post || $post->post_type !== 'traduction_langue') {
|
||||
return $valid;
|
||||
}
|
||||
|
||||
// Pas de validation spécifique pour le moment
|
||||
return $valid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gère la suppression des capacités de traduction
|
||||
* - Bloque la suppression si des événements existent
|
||||
*
|
||||
* @param int $post_id ID du post à supprimer
|
||||
* @param \WP_Post $post Objet du post
|
||||
*/
|
||||
public static function handle_cascade_delete($post_id, $post) {
|
||||
// Vérifier que c'est bien un post traduction_langue
|
||||
if (!$post || $post->post_type !== 'traduction_langue') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Vérifier si la capacité a des événements associés avec vérification précise
|
||||
$has_events = self::check_events_for_capacite($post_id);
|
||||
|
||||
if ($has_events) {
|
||||
// Bloquer la suppression avec un message d'erreur
|
||||
\wp_die(
|
||||
'<h1>Suppression impossible</h1>' .
|
||||
'<p>Cette capacité de traduction ne peut pas être supprimée car elle a des événements associés.</p>' .
|
||||
'<p>Vous devez d\'abord supprimer ou réassigner les événements liés à cette capacité.</p>' .
|
||||
'<p><a href="' . \admin_url('edit.php?post_type=traduction_langue') . '">← Retour à la liste</a></p>',
|
||||
'Suppression bloquée',
|
||||
[
|
||||
'response' => 403,
|
||||
'back_link' => true,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// Invalider le cache des capacités
|
||||
\ESI_CRVI_AGENDA\models\CRVI_TraductionLangue_Model::invalidate_cache($post_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si une capacité a des événements associés avec vérification précise
|
||||
* Utilise la même logique que countUsed() mais retourne un booléen
|
||||
*
|
||||
* @param int $capacite_id ID de la capacité
|
||||
* @return bool True si la capacité a des événements
|
||||
*/
|
||||
public static function check_events_for_capacite($capacite_id): bool {
|
||||
$capacite = \ESI_CRVI_AGENDA\models\CRVI_TraductionLangue_Model::load($capacite_id);
|
||||
if (!$capacite) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Récupérer les langues (peut être un tableau)
|
||||
$langue_ids = $capacite->langue;
|
||||
if (empty($langue_ids)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// S'assurer que c'est un tableau
|
||||
if (!is_array($langue_ids)) {
|
||||
$langue_ids = [$langue_ids];
|
||||
}
|
||||
|
||||
// Récupérer les slugs de toutes les langues
|
||||
$langue_slugs = [];
|
||||
foreach ($langue_ids as $langue_id) {
|
||||
$langue_term = \get_term($langue_id, 'langue');
|
||||
if ($langue_term && !\is_wp_error($langue_term)) {
|
||||
$langue_slugs[] = $langue_term->slug;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($langue_slugs)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Mapper les jours en format MySQL DAYOFWEEK
|
||||
$jours_map = [
|
||||
'dimanche' => 1,
|
||||
'lundi' => 2,
|
||||
'mardi' => 3,
|
||||
'mercredi' => 4,
|
||||
'jeudi' => 5,
|
||||
'vendredi' => 6,
|
||||
'samedi' => 7,
|
||||
];
|
||||
|
||||
// Requête optimisée avec EXISTS
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'crvi_agenda';
|
||||
|
||||
// Construire la requête WHERE avec support multi-langues
|
||||
$langue_placeholders = implode(',', array_fill(0, count($langue_slugs), '%s'));
|
||||
|
||||
$where = [
|
||||
"langue IN ($langue_placeholders)",
|
||||
"is_deleted = 0",
|
||||
"statut != 'annule'",
|
||||
"statut != 'brouillon'",
|
||||
];
|
||||
$values = $langue_slugs;
|
||||
|
||||
// Filtrer par jour de la semaine si défini
|
||||
if (!empty($capacite->jour) && isset($jours_map[$capacite->jour])) {
|
||||
$where[] = "DAYOFWEEK(date_rdv) = %d";
|
||||
$values[] = $jours_map[$capacite->jour];
|
||||
}
|
||||
|
||||
// Filtrer par période si défini
|
||||
if (!empty($capacite->periode)) {
|
||||
if ($capacite->periode === 'matin') {
|
||||
$where[] = "heure_rdv < '12:00:00'";
|
||||
} elseif ($capacite->periode === 'apres_midi') {
|
||||
$where[] = "heure_rdv >= '12:00:00'";
|
||||
}
|
||||
// Si période = 'journee', on ne filtre pas par heure
|
||||
}
|
||||
|
||||
$where_sql = 'WHERE ' . implode(' AND ', $where);
|
||||
$sql = "SELECT EXISTS(SELECT 1 FROM {$table_name} {$where_sql} LIMIT 1)";
|
||||
$prepared_sql = $wpdb->prepare($sql, $values);
|
||||
|
||||
$exists = (int) $wpdb->get_var($prepared_sql);
|
||||
|
||||
return $exists === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Valide la sauvegarde d'un post traduction_langue
|
||||
* Empêche toute modification si des événements existent déjà
|
||||
*
|
||||
* @param int $post_id ID du post en cours de sauvegarde
|
||||
*/
|
||||
public static function validate_save_post_with_events($post_id) {
|
||||
// Vérifier que c'est bien un post traduction_langue
|
||||
$post = \get_post($post_id);
|
||||
if (!$post || $post->post_type !== 'traduction_langue') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignorer pour les nouveaux posts (auto-draft)
|
||||
if ($post->post_status === 'auto-draft') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignorer si c'est une création (le post n'existait pas avant)
|
||||
// On vérifie si le post a été créé récemment (moins de 2 minutes)
|
||||
$post_date = strtotime($post->post_date);
|
||||
$current_time = current_time('timestamp');
|
||||
$is_new_post = ($current_time - $post_date) < 120; // 2 minutes
|
||||
|
||||
if ($is_new_post) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Vérifier si la capacité a des événements associés
|
||||
$has_events = self::check_events_for_capacite($post_id);
|
||||
|
||||
if (!$has_events) {
|
||||
return; // Pas d'événements, on autorise la modification
|
||||
}
|
||||
|
||||
// Si des événements existent, vérifier si des champs structurants ont été modifiés
|
||||
$fields_to_check = ['langue', 'jour', 'periode', 'limite_par'];
|
||||
$has_changes = false;
|
||||
$changed_fields = [];
|
||||
|
||||
foreach ($fields_to_check as $field_name) {
|
||||
// Récupérer la valeur actuelle en BDD
|
||||
$current_value = \get_field($field_name, $post_id);
|
||||
|
||||
// Récupérer la nouvelle valeur depuis $_POST
|
||||
$new_value = null;
|
||||
|
||||
if ($field_name === 'langue') {
|
||||
// Pour le champ ACF langue (peut être multiple)
|
||||
$field_key = 'field_capacite_langue';
|
||||
if (isset($_POST['acf'][$field_key])) {
|
||||
$new_value = $_POST['acf'][$field_key];
|
||||
}
|
||||
|
||||
// Normaliser les valeurs pour comparaison (toujours en tableau)
|
||||
if (!is_array($current_value)) {
|
||||
$current_value = !empty($current_value) ? [$current_value] : [];
|
||||
}
|
||||
if (!is_array($new_value)) {
|
||||
$new_value = !empty($new_value) ? [$new_value] : [];
|
||||
}
|
||||
|
||||
// Trier pour comparaison correcte
|
||||
sort($current_value);
|
||||
sort($new_value);
|
||||
} else {
|
||||
// Pour les champs ACF standards
|
||||
$field_key = 'field_capacite_' . $field_name;
|
||||
|
||||
if (isset($_POST['acf'][$field_key])) {
|
||||
$new_value = $_POST['acf'][$field_key];
|
||||
}
|
||||
}
|
||||
|
||||
// Comparer les valeurs
|
||||
if ($new_value !== null && $new_value != $current_value) {
|
||||
$has_changes = true;
|
||||
$changed_fields[] = $field_name;
|
||||
}
|
||||
}
|
||||
|
||||
// Si des modifications ont été détectées sur les champs structurants
|
||||
if ($has_changes) {
|
||||
$fields_labels = [
|
||||
'langue' => 'Langue',
|
||||
'jour' => 'Jour',
|
||||
'periode' => 'Période',
|
||||
'limite_par' => 'Limite par'
|
||||
];
|
||||
|
||||
$changed_labels = array_map(function($field) use ($fields_labels) {
|
||||
return $fields_labels[$field] ?? $field;
|
||||
}, $changed_fields);
|
||||
|
||||
$error_message = sprintf(
|
||||
'Impossible de modifier cette capacité de traduction car elle est utilisée par des événements existants. ' .
|
||||
'Champs modifiés : %s. ' .
|
||||
'Vous devez d\'abord supprimer ou réassigner les événements liés.',
|
||||
implode(', ', $changed_labels)
|
||||
);
|
||||
|
||||
\acf_add_validation_error('', $error_message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enregistre la page admin personnalisée pour les capacités de traduction
|
||||
*/
|
||||
public static function register_admin_page() {
|
||||
\add_menu_page(
|
||||
'Capacités de traduction', // Titre de la page
|
||||
'Capacités traduction', // Titre du menu
|
||||
'edit_posts', // Capability requise
|
||||
'traduction-langues', // Slug de la page
|
||||
[self::class, 'render_admin_page'], // Fonction de rendu
|
||||
'dashicons-translation', // Icône
|
||||
30 // Position dans le menu
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Affiche la page admin personnalisée
|
||||
*/
|
||||
public static function render_admin_page() {
|
||||
// Récupérer toutes les capacités
|
||||
$parent_capacites = \get_posts([
|
||||
'post_type' => 'traduction_langue',
|
||||
'posts_per_page' => -1,
|
||||
'post_status' => 'any',
|
||||
'orderby' => 'title',
|
||||
'order' => 'ASC',
|
||||
]);
|
||||
|
||||
// Charger les scripts et styles
|
||||
\wp_enqueue_style(
|
||||
'traduction-langue-list',
|
||||
\plugins_url('assets/js/dist/traduction-langue-list.min.css', dirname(__DIR__, 1)),
|
||||
[],
|
||||
'1.0.0'
|
||||
);
|
||||
|
||||
\wp_enqueue_script(
|
||||
'traduction-langue-list',
|
||||
\plugins_url('assets/js/dist/traduction-langue-list.min.js', dirname(__DIR__, 1)),
|
||||
['jquery'],
|
||||
'1.0.0',
|
||||
true
|
||||
);
|
||||
|
||||
// Afficher le template
|
||||
include dirname(__DIR__, 2) . '/templates/admin/traduction-langue-list.php';
|
||||
}
|
||||
}
|
||||
28
app/controllers/Type_Intervention_Controller.php
Normal file
28
app/controllers/Type_Intervention_Controller.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
namespace ESI_CRVI_AGENDA\controllers;
|
||||
|
||||
class CRVI_Type_Intervention_Controller {
|
||||
public static function register_cpt() {
|
||||
register_post_type('type_intervention', [
|
||||
'label' => "Types d'intervention",
|
||||
'labels' => [
|
||||
'name' => "Types d'intervention",
|
||||
'singular_name' => "Type d'intervention",
|
||||
'add_new' => "Ajouter un type d'intervention",
|
||||
'add_new_item' => "Ajouter un nouveau type d'intervention",
|
||||
'edit_item' => "Modifier le type d'intervention",
|
||||
'new_item' => "Nouveau type d'intervention",
|
||||
'view_item' => "Voir le type d'intervention",
|
||||
'search_items' => "Rechercher un type d'intervention",
|
||||
'not_found' => "Aucun type d'intervention trouvé",
|
||||
'not_found_in_trash' => "Aucun type d'intervention dans la corbeille",
|
||||
],
|
||||
'public' => false,
|
||||
'show_ui' => true,
|
||||
'show_in_menu' => true,
|
||||
'menu_position' => 26,
|
||||
'supports' => ['title'],
|
||||
'capability_type' => 'post',
|
||||
]);
|
||||
}
|
||||
}
|
||||
88
app/crons/actions.php
Normal file
88
app/crons/actions.php
Normal file
@ -0,0 +1,88 @@
|
||||
<?php
|
||||
/**
|
||||
* Fichier d'actions pour les crons du plugin ESI_crvi_agenda
|
||||
*
|
||||
* Ce fichier fait uniquement le routage vers les méthodes appropriées
|
||||
* dans les classes Event_Model et Notifications_Controller.
|
||||
*
|
||||
* Actions disponibles via paramètres GET :
|
||||
* - action=get_today_incomplete : Récupère tous les événements d'aujourd'hui non complétés
|
||||
* - action=send_sms_reminder : Envoie des SMS de rappel pour les événements dans X jours
|
||||
* Paramètres additionnels :
|
||||
* - days : nombre de jours avant l'événement (1, 2 ou 3, défaut: 3)
|
||||
*
|
||||
* Usage :
|
||||
* /wp-content/plugins/ESI_crvi_agenda/app/crons/actions.php?key=K7mP9xQ2vN4rT8&action=get_today_incomplete
|
||||
* /wp-content/plugins/ESI_crvi_agenda/app/crons/actions.php?key=K7mP9xQ2vN4rT8&action=send_sms_reminder&days=3
|
||||
*
|
||||
* Sécurité : Une clé secrète est requise via le paramètre GET 'key'
|
||||
*/
|
||||
|
||||
define('WP_USE_THEMES', false);
|
||||
|
||||
// Clé secrète pour sécuriser l'accès aux actions cron
|
||||
define('CRON_ACTIONS_SECRET_KEY', 'K7mP9xQ2vN4rT8');
|
||||
|
||||
// Vérification de la clé secrète via paramètre GET
|
||||
if (!isset($_GET['key']) || $_GET['key'] !== CRON_ACTIONS_SECRET_KEY) {
|
||||
http_response_code(403);
|
||||
header('Content-Type: application/json');
|
||||
die(json_encode(['error' => 'Accès refusé. Clé secrète requise.']));
|
||||
}
|
||||
|
||||
// Déterminer le chemin du plugin
|
||||
$plugin_dir = dirname(dirname(__DIR__));
|
||||
$wp_load_path = $plugin_dir . '/../../../../wp-load.php';
|
||||
|
||||
// Vérifier que wp-load.php existe
|
||||
if (!file_exists($wp_load_path)) {
|
||||
http_response_code(500);
|
||||
header('Content-Type: application/json');
|
||||
die(json_encode(['error' => 'Erreur : wp-load.php introuvable.']));
|
||||
}
|
||||
|
||||
require_once $wp_load_path;
|
||||
|
||||
// Charger les classes nécessaires
|
||||
require_once dirname(__DIR__) . '/models/Event_Model.php';
|
||||
require_once dirname(__DIR__) . '/controllers/Notifications_Controller.php';
|
||||
|
||||
use ESI_CRVI_AGENDA\models\CRVI_Event_Model;
|
||||
use ESI_CRVI_AGENDA\controllers\CRVI_Notifications_Controller;
|
||||
|
||||
// Récupérer l'action demandée
|
||||
$action = isset($_GET['action']) ? sanitize_text_field($_GET['action']) : '';
|
||||
|
||||
// Définir le header JSON
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
// Router vers l'action appropriée
|
||||
switch ($action) {
|
||||
case 'get_today_incomplete':
|
||||
$result = CRVI_Event_Model::get_today_incomplete_events();
|
||||
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||
break;
|
||||
|
||||
case 'send_sms_reminder':
|
||||
$days = isset($_GET['days']) ? (int) $_GET['days'] : 3;
|
||||
$result = CRVI_Notifications_Controller::send_sms_reminder($days);
|
||||
|
||||
// Si c'est une erreur de validation, retourner un code 400
|
||||
if (isset($result['error'])) {
|
||||
http_response_code(400);
|
||||
}
|
||||
|
||||
echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||
break;
|
||||
|
||||
default:
|
||||
http_response_code(400);
|
||||
echo json_encode([
|
||||
'error' => 'Action non reconnue',
|
||||
'available_actions' => [
|
||||
'get_today_incomplete' => 'Récupère tous les événements d\'aujourd\'hui non complétés',
|
||||
'send_sms_reminder' => 'Envoie des SMS de rappel pour les événements dans X jours (paramètre: days=1|2|3)'
|
||||
]
|
||||
], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||
break;
|
||||
}
|
||||
44
app/crons/crons_autoloader.php
Normal file
44
app/crons/crons_autoloader.php
Normal file
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
/**
|
||||
* Autoloader pour les crons du plugin ESI_crvi_agenda
|
||||
*
|
||||
* Sécurité : Vérification d'une clé secrète via paramètre GET
|
||||
* Usage : /wp-content/plugins/ESI_crvi_agenda/app/crons/crons_autoloader.php?key=VOTRE_CLE_SECRETE
|
||||
*/
|
||||
|
||||
// Clé secrète pour sécuriser l'accès aux crons
|
||||
/* define('CRON_SECRET_KEY', 'votre_cle_secrete_changez_moi_123456789');
|
||||
|
||||
// Vérification de la clé secrète via paramètre GET
|
||||
if (!isset($_GET['key']) || $_GET['key'] !== CRON_SECRET_KEY) {
|
||||
http_response_code(403);
|
||||
die('Accès refusé. Clé secrète requise.');
|
||||
} */
|
||||
|
||||
define('WP_USE_THEMES', false);
|
||||
|
||||
// Déterminer le chemin du plugin
|
||||
$plugin_dir = dirname(dirname(__DIR__));
|
||||
$wp_load_path = $plugin_dir . '/../../../../wp-load.php';
|
||||
|
||||
// Vérifier que wp-load.php existe
|
||||
if (!file_exists($wp_load_path)) {
|
||||
http_response_code(500);
|
||||
die('Erreur : wp-load.php introuvable.');
|
||||
}
|
||||
|
||||
require_once $wp_load_path;
|
||||
|
||||
// Charger gold-init.php si la classe GOLD n'existe pas
|
||||
// Le fichier gold-init.php devrait être dans le répertoire app/
|
||||
$gold_init_path = dirname(__DIR__) . '/gold-init.php';
|
||||
if (!class_exists('GOLD') && file_exists($gold_init_path)) {
|
||||
require_once $gold_init_path;
|
||||
}
|
||||
|
||||
// Initialiser GOLD si disponible
|
||||
if (class_exists('GOLD')) {
|
||||
/** @var GOLD $GOLD */
|
||||
$GOLD = GOLD::instance();
|
||||
$GOLD->init();
|
||||
}
|
||||
2
app/factory.php
Normal file
2
app/factory.php
Normal file
@ -0,0 +1,2 @@
|
||||
<?php
|
||||
// Factory pour CRVI Agenda
|
||||
563
app/helpers/Api_Helper.php
Normal file
563
app/helpers/Api_Helper.php
Normal file
@ -0,0 +1,563 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ESI_CRVI_AGENDA\helpers;
|
||||
|
||||
use WP_Error;
|
||||
|
||||
class Api_Helper {
|
||||
/**
|
||||
* Retourne une erreur API standardisée (format JSON).
|
||||
* @param string $message
|
||||
* @param int $code
|
||||
* @return WP_Error
|
||||
*/
|
||||
public static function json_error(string $message, int $code = 400): WP_Error {
|
||||
return new WP_Error('api_error', $message, ['status' => $code]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne une réponse API standardisée pour succès.
|
||||
* @param mixed $data
|
||||
* @return array
|
||||
*/
|
||||
public static function json_success($data): array {
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => $data,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si l'utilisateur est prêt à être vérifié (WordPress initialisé + utilisateur connecté).
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_user_ready(): bool {
|
||||
return did_action('init') && is_user_logged_in();
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si l'utilisateur courant a un rôle donné.
|
||||
* @param string|array $roles
|
||||
* @return bool
|
||||
*/
|
||||
public static function check_role($roles): bool {
|
||||
// S'assurer que WordPress est complètement chargé
|
||||
if (!did_action('init')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Vérifier si l'utilisateur est connecté
|
||||
if (!is_user_logged_in()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Récupérer l'utilisateur courant
|
||||
$user = wp_get_current_user();
|
||||
|
||||
// Vérifier que l'utilisateur est valide
|
||||
if (!$user || !$user->exists()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Vérifier que les rôles sont bien définis
|
||||
if (empty($user->roles) || !is_array($user->roles)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (is_array($roles)) {
|
||||
foreach ($roles as $role) {
|
||||
if (in_array($role, $user->roles, true)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
return in_array($roles, $user->roles, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère toutes les langues de la taxonomie 'langue_beneficiaire'.
|
||||
* @param bool $simple_list Si true, retourne un tableau simple [id => nom]
|
||||
* @return array
|
||||
*/
|
||||
public static function get_languages($simple_list = false): array {
|
||||
$terms = get_terms([
|
||||
'taxonomy' => 'langue_beneficiaire',
|
||||
'hide_empty' => false,
|
||||
'orderby' => 'name',
|
||||
'order' => 'ASC',
|
||||
]);
|
||||
|
||||
if (is_wp_error($terms) || empty($terms)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ($simple_list) {
|
||||
$languages = [];
|
||||
foreach ($terms as $term) {
|
||||
$languages[] = [
|
||||
'id' => $term->term_id,
|
||||
'nom' => $term->name,
|
||||
'slug' => $term->slug,
|
||||
];
|
||||
}
|
||||
return $languages;
|
||||
}
|
||||
|
||||
return $terms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère une langue spécifique par son slug.
|
||||
* @param string $slug
|
||||
* @return array|null
|
||||
*/
|
||||
public static function get_language_by_slug($slug): ?array {
|
||||
$term = get_term_by('slug', $slug, 'langue_beneficiaire');
|
||||
if (!$term || is_wp_error($term)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $term->term_id,
|
||||
'nom' => $term->name,
|
||||
'slug' => $term->slug,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si l'utilisateur peut créer des événements.
|
||||
* @return bool
|
||||
*/
|
||||
public static function can_create_events(): bool {
|
||||
// Les admins ont tous les droits
|
||||
if (self::check_role(['administrator', 'editor', 'crvi_manager'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Les intervenants peuvent créer des événements pour eux-mêmes
|
||||
if (self::check_role(['intervenant'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si l'utilisateur peut modifier des événements.
|
||||
* @param int|null $event_id ID de l'événement (optionnel)
|
||||
* @return bool
|
||||
*/
|
||||
public static function can_edit_events($event_id = null): bool {
|
||||
// Les admins ont tous les droits
|
||||
if (self::check_role(['administrator', 'editor', 'crvi_manager'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Les intervenants peuvent éditer leurs propres événements
|
||||
if (self::check_role(['intervenant'])) {
|
||||
// Si un event_id est fourni, vérifier que l'événement appartient à l'intervenant
|
||||
if ($event_id) {
|
||||
$model = new \ESI_CRVI_AGENDA\models\CRVI_Event_Model();
|
||||
$event = $model->get_details($event_id);
|
||||
$current_user_id = get_current_user_id();
|
||||
|
||||
// Vérifier si l'utilisateur est l'intervenant assigné à cet événement
|
||||
if ($event && isset($event->id_intervenant) && $event->id_intervenant == $current_user_id) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// Sans event_id spécifique, autoriser (le filtre se fera au niveau de l'événement)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si l'utilisateur peut supprimer des événements.
|
||||
* @param int|null $event_id ID de l'événement (optionnel)
|
||||
* @return bool
|
||||
*/
|
||||
public static function can_delete_events($event_id = null): bool {
|
||||
return self::check_role(['administrator', 'editor']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si l'utilisateur peut clôturer des événements.
|
||||
* @param int|null $event_id ID de l'événement (optionnel)
|
||||
* @return bool
|
||||
*/
|
||||
public static function can_close_events($event_id = null): bool {
|
||||
return self::check_role(['administrator', 'editor', 'crvi_manager']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si l'utilisateur peut voir les événements.
|
||||
* @return bool
|
||||
*/
|
||||
public static function can_view_events(): bool {
|
||||
// Les admins et viewers ont accès
|
||||
if (self::check_role(['administrator', 'editor', 'crvi_manager', 'crvi_viewer'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Les intervenants peuvent voir leurs événements et ceux de leurs collègues
|
||||
if (self::check_role(['intervenant'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne toutes les permissions de l'utilisateur courant pour l'agenda.
|
||||
* @return array
|
||||
*/
|
||||
public static function get_user_permissions(): array {
|
||||
// Vérifier si l'utilisateur est connecté et chargé
|
||||
if (!is_user_logged_in()) {
|
||||
return [
|
||||
'can_create' => false,
|
||||
'can_edit' => false,
|
||||
'can_delete' => false,
|
||||
'can_close' => false,
|
||||
'can_view' => false,
|
||||
'user_roles' => [],
|
||||
'user_id' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
$user = wp_get_current_user();
|
||||
$user_roles = $user && $user->exists() ? $user->roles : [];
|
||||
$user_id = get_current_user_id();
|
||||
|
||||
return [
|
||||
'can_create' => self::can_create_events(),
|
||||
'can_edit' => self::can_edit_events(),
|
||||
'can_delete' => self::can_delete_events(),
|
||||
'can_close' => self::can_close_events(),
|
||||
'can_view' => self::can_view_events(),
|
||||
'user_roles' => $user_roles,
|
||||
'user_id' => $user_id,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Valide les données d'un événement.
|
||||
* @param array $data Données de l'événement
|
||||
* @param string $action 'create' ou 'update'
|
||||
* @return array ['valid' => bool, 'errors' => array]
|
||||
*/
|
||||
public static function validate_event_data($data, $action = 'create'): array {
|
||||
$errors = [];
|
||||
$required_fields = [
|
||||
'date_rdv' => 'Date de rendez-vous',
|
||||
'heure_rdv' => 'Heure de rendez-vous',
|
||||
'id_beneficiaire' => 'Bénéficiaire',
|
||||
'id_intervenant' => 'Intervenant',
|
||||
'id_local' => 'Local',
|
||||
];
|
||||
|
||||
// Vérifier les champs requis
|
||||
foreach ($required_fields as $field => $label) {
|
||||
if (empty($data[$field])) {
|
||||
$errors[] = "Le champ '$label' est requis.";
|
||||
}
|
||||
}
|
||||
|
||||
// Validation spécifique pour la création
|
||||
if ($action === 'create') {
|
||||
// Vérifier que la date n'est pas dans le passé
|
||||
if (!empty($data['date_rdv'])) {
|
||||
$date_rdv = strtotime($data['date_rdv']);
|
||||
$today = strtotime('today');
|
||||
if ($date_rdv < $today) {
|
||||
$errors[] = "La date de rendez-vous ne peut pas être dans le passé.";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validation de l'heure
|
||||
if (!empty($data['heure_rdv'])) {
|
||||
if (!preg_match('/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/', $data['heure_rdv'])) {
|
||||
$errors[] = "Le format de l'heure n'est pas valide (HH:MM).";
|
||||
}
|
||||
}
|
||||
|
||||
// Validation de la date
|
||||
if (!empty($data['date_rdv'])) {
|
||||
$date = \DateTime::createFromFormat('Y-m-d', $data['date_rdv']);
|
||||
if (!$date || $date->format('Y-m-d') !== $data['date_rdv']) {
|
||||
$errors[] = "Le format de la date n'est pas valide (YYYY-MM-DD).";
|
||||
}
|
||||
}
|
||||
|
||||
// Validation des IDs (doivent être des entiers positifs)
|
||||
$id_fields = ['id_beneficiaire', 'id_intervenant', 'id_local', 'id_traducteur'];
|
||||
foreach ($id_fields as $field) {
|
||||
if (!empty($data[$field]) && (!is_numeric($data[$field]) || (int)$data[$field] <= 0)) {
|
||||
$errors[] = "L'ID du champ '$field' doit être un nombre entier positif.";
|
||||
}
|
||||
}
|
||||
|
||||
// Validation de cohérence des dates/heures
|
||||
if (!empty($data['date_fin']) && !empty($data['date_rdv'])) {
|
||||
if ($data['date_fin'] < $data['date_rdv']) {
|
||||
$errors[] = "La date de fin doit être après ou égale à la date de début";
|
||||
} elseif ($data['date_fin'] === $data['date_rdv'] && !empty($data['heure_fin']) && !empty($data['heure_rdv'])) {
|
||||
// Si même jour, vérifier que l'heure de fin est après l'heure de début
|
||||
if ($data['heure_fin'] <= $data['heure_rdv']) {
|
||||
$errors[] = "L'heure de fin doit être après l'heure de début pour un événement sur la même journée";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'valid' => empty($errors),
|
||||
'errors' => $errors,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Valide les données de disponibilité.
|
||||
* @param array $data Données de disponibilité
|
||||
* @return array ['valid' => bool, 'errors' => array]
|
||||
*/
|
||||
public static function validate_availability_data($data): array {
|
||||
$errors = [];
|
||||
|
||||
// Vérifier que au moins une date est fournie
|
||||
if (empty($data['date']) && empty($data['date_rdv'])) {
|
||||
$errors[] = "Une date doit être fournie pour vérifier les disponibilités.";
|
||||
}
|
||||
|
||||
// Validation de la date si fournie
|
||||
$date = $data['date'] ?? $data['date_rdv'] ?? null;
|
||||
if ($date) {
|
||||
$date_obj = \DateTime::createFromFormat('Y-m-d', $date);
|
||||
if (!$date_obj || $date_obj->format('Y-m-d') !== $date) {
|
||||
$errors[] = "Le format de la date n'est pas valide (YYYY-MM-DD).";
|
||||
}
|
||||
}
|
||||
|
||||
// Validation de l'heure si fournie
|
||||
$heure = $data['heure'] ?? $data['heure_rdv'] ?? null;
|
||||
if ($heure && !preg_match('/^([01]?[0-9]|2[0-3]):[0-5][0-9]$/', $heure)) {
|
||||
$errors[] = "Le format de l'heure n'est pas valide (HH:MM).";
|
||||
}
|
||||
|
||||
return [
|
||||
'valid' => empty($errors),
|
||||
'errors' => $errors,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie les conflits de disponibilité.
|
||||
* @param string $date Date au format Y-m-d
|
||||
* @param string $heure Heure au format H:i
|
||||
* @param int $intervenant_id ID de l'intervenant
|
||||
* @param int $traducteur_id ID du traducteur
|
||||
* @param int $local_id ID du local
|
||||
* @param int|null $exclude_event_id ID de l'événement à exclure (pour l'édition)
|
||||
* @return array ['has_conflicts' => bool, 'conflicts' => array]
|
||||
*/
|
||||
public static function check_availability_conflicts($date, $heure, $intervenant_id, $traducteur_id, $local_id, $exclude_event_id = null): array {
|
||||
global $wpdb;
|
||||
|
||||
$conflicts = [];
|
||||
|
||||
// Vérifier les conflits pour l'intervenant
|
||||
if ($intervenant_id) {
|
||||
$intervenant_conflicts = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT * FROM {$wpdb->prefix}crvi_agenda
|
||||
WHERE date_rdv = %s
|
||||
AND heure_rdv = %s
|
||||
AND id_intervenant = %d
|
||||
AND id != %d",
|
||||
$date, $heure, $intervenant_id, $exclude_event_id ?? 0
|
||||
));
|
||||
|
||||
if (!empty($intervenant_conflicts)) {
|
||||
$conflicts['intervenant'] = $intervenant_conflicts;
|
||||
}
|
||||
}
|
||||
|
||||
// Vérifier les conflits pour le traducteur
|
||||
if ($traducteur_id) {
|
||||
$traducteur_conflicts = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT * FROM {$wpdb->prefix}crvi_agenda
|
||||
WHERE date_rdv = %s
|
||||
AND heure_rdv = %s
|
||||
AND id_traducteur = %d
|
||||
AND id != %d",
|
||||
$date, $heure, $traducteur_id, $exclude_event_id ?? 0
|
||||
));
|
||||
|
||||
if (!empty($traducteur_conflicts)) {
|
||||
$conflicts['traducteur'] = $traducteur_conflicts;
|
||||
}
|
||||
}
|
||||
|
||||
// Vérifier les conflits pour le local
|
||||
if ($local_id) {
|
||||
$local_conflicts = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT * FROM {$wpdb->prefix}crvi_agenda
|
||||
WHERE date_rdv = %s
|
||||
AND heure_rdv = %s
|
||||
AND id_local = %d
|
||||
AND id != %d",
|
||||
$date, $heure, $local_id, $exclude_event_id ?? 0
|
||||
));
|
||||
|
||||
if (!empty($local_conflicts)) {
|
||||
$conflicts['local'] = $local_conflicts;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'has_conflicts' => !empty($conflicts),
|
||||
'conflicts' => $conflicts,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les options d'un champ ACF pour créer des éléments HTML dynamiques.
|
||||
* Basé sur get_field_object() d'Advanced Custom Fields.
|
||||
*
|
||||
* @param string $field_name Nom ou clé du champ ACF
|
||||
* @param int|false $post_id ID du post (optionnel, défaut: post courant)
|
||||
* @param bool $format_value Si true, applique le formatage (défaut: true)
|
||||
* @param bool $load_value Si true, charge la valeur du champ (défaut: true)
|
||||
* @param bool $escape_html Si true, échappe le HTML (défaut: false)
|
||||
* @return array|null Retourne un tableau avec les options formatées ou null si erreur
|
||||
*/
|
||||
public static function get_acf_field_options($field_name, $post_id = false, $format_value = true, $load_value = true, $escape_html = false): ?array {
|
||||
// Vérifier que ACF est disponible
|
||||
if (!function_exists('get_field_object')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
// Récupérer l'objet champ ACF
|
||||
$field_object = get_field_object($field_name, $post_id, $format_value, $load_value, $escape_html);
|
||||
|
||||
if (!$field_object || is_wp_error($field_object)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$result = [
|
||||
'field_info' => [
|
||||
'name' => $field_object['name'] ?? '',
|
||||
'label' => $field_object['label'] ?? '',
|
||||
'type' => $field_object['type'] ?? '',
|
||||
'key' => $field_object['key'] ?? '',
|
||||
'required' => (bool)($field_object['required'] ?? false),
|
||||
'instructions' => $field_object['instructions'] ?? '',
|
||||
],
|
||||
'options' => [],
|
||||
'current_value' => $field_object['value'] ?? null,
|
||||
'html_attributes' => [
|
||||
'id' => $field_object['id'] ?? '',
|
||||
'class' => $field_object['class'] ?? '',
|
||||
]
|
||||
];
|
||||
|
||||
// Traiter selon le type de champ
|
||||
switch ($field_object['type']) {
|
||||
case 'select':
|
||||
case 'checkbox':
|
||||
case 'radio':
|
||||
if (isset($field_object['choices']) && is_array($field_object['choices'])) {
|
||||
$result['options'] = [];
|
||||
foreach ($field_object['choices'] as $value => $label) {
|
||||
$result['options'][] = [
|
||||
'value' => $value,
|
||||
'label' => $label,
|
||||
'selected' => $field_object['value'] == $value
|
||||
];
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'true_false':
|
||||
$result['options'] = [
|
||||
[
|
||||
'value' => '1',
|
||||
'label' => $field_object['ui_on_text'] ?? 'Oui',
|
||||
'selected' => (bool)$field_object['value']
|
||||
],
|
||||
[
|
||||
'value' => '0',
|
||||
'label' => $field_object['ui_off_text'] ?? 'Non',
|
||||
'selected' => !(bool)$field_object['value']
|
||||
]
|
||||
];
|
||||
break;
|
||||
|
||||
case 'page_link':
|
||||
case 'post_object':
|
||||
if (isset($field_object['choices']) && is_array($field_object['choices'])) {
|
||||
$result['options'] = [];
|
||||
foreach ($field_object['choices'] as $value => $label) {
|
||||
$result['options'][] = [
|
||||
'value' => $value,
|
||||
'label' => $label,
|
||||
'selected' => $field_object['value'] == $value
|
||||
];
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'user':
|
||||
if (isset($field_object['choices']) && is_array($field_object['choices'])) {
|
||||
$result['options'] = [];
|
||||
foreach ($field_object['choices'] as $value => $label) {
|
||||
$result['options'][] = [
|
||||
'value' => $value,
|
||||
'label' => $label,
|
||||
'selected' => $field_object['value'] == $value
|
||||
];
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'taxonomy':
|
||||
if (isset($field_object['choices']) && is_array($field_object['choices'])) {
|
||||
$result['options'] = [];
|
||||
foreach ($field_object['choices'] as $value => $label) {
|
||||
$result['options'][] = [
|
||||
'value' => $value,
|
||||
'label' => $label,
|
||||
'selected' => $field_object['value'] == $value
|
||||
];
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'repeater':
|
||||
// Pour les repeaters, on retourne la structure mais pas les options
|
||||
$result['is_repeater'] = true;
|
||||
$result['sub_fields'] = isset($field_object['sub_fields']) ? $field_object['sub_fields'] : [];
|
||||
break;
|
||||
|
||||
case 'flexible_content':
|
||||
// Pour le contenu flexible, on retourne les layouts disponibles
|
||||
$result['is_flexible'] = true;
|
||||
$result['layouts'] = isset($field_object['layouts']) ? $field_object['layouts'] : [];
|
||||
break;
|
||||
|
||||
default:
|
||||
// Pour les autres types (text, textarea, etc.), on retourne juste les infos de base
|
||||
$result['is_simple_field'] = true;
|
||||
break;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
227
app/helpers/Stats_Helper.php
Normal file
227
app/helpers/Stats_Helper.php
Normal file
@ -0,0 +1,227 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ESI_CRVI_AGENDA\helpers;
|
||||
|
||||
use ESI_CRVI_AGENDA\models\CRVI_Event_Model;
|
||||
use ESI_CRVI_AGENDA\models\CRVI_Departement_Model;
|
||||
use ESI_CRVI_AGENDA\models\CRVI_Type_Intervention_Model;
|
||||
use ESI_CRVI_AGENDA\models\CRVI_Traducteur_Model;
|
||||
use ESI_CRVI_AGENDA\models\CRVI_Intervenant_Model;
|
||||
|
||||
class Stats_Helper
|
||||
{
|
||||
/**
|
||||
* Traite les paramètres du formulaire et retourne un tableau structuré pour l'export CSV.
|
||||
* @param array $params
|
||||
* @return array [success, code, message, csv: [header, rows]]
|
||||
*/
|
||||
public static function handle_form(array $params = []): array
|
||||
{
|
||||
// Validation des paramètres
|
||||
$entite = $params['entite'] ?? 'global';
|
||||
$annee = isset($params['annee']) ? intval($params['annee']) : null;
|
||||
$date_debut = $params['date_debut'] ?? '';
|
||||
$date_fin = $params['date_fin'] ?? '';
|
||||
$attributes = [];
|
||||
|
||||
// Détermination de la période
|
||||
if ($annee) {
|
||||
$begin = $annee . '-01-01';
|
||||
$end = $annee . '-12-31';
|
||||
} elseif ($date_debut && $date_fin) {
|
||||
$begin = $date_debut;
|
||||
$end = $date_fin;
|
||||
} else {
|
||||
return [
|
||||
'success' => false,
|
||||
'code' => 'invalid_params',
|
||||
'message' => __('Veuillez renseigner une année ou une plage de dates.', 'esi_crvi_agenda'),
|
||||
'csv' => null
|
||||
];
|
||||
}
|
||||
|
||||
// Dispatch selon l'entité
|
||||
switch ($entite) {
|
||||
case 'global':
|
||||
$result = self::events_stats($begin, $end, $attributes);
|
||||
break;
|
||||
// case 'intervenant':
|
||||
// $result = self::intervenant_stats($begin, $end, $attributes);
|
||||
// break;
|
||||
// case 'departement':
|
||||
// $result = self::departement_stats($begin, $end, $attributes);
|
||||
// break;
|
||||
// case 'type_intervention':
|
||||
// $result = self::type_intervention_stats($begin, $end, $attributes);
|
||||
// break;
|
||||
// case 'traducteur':
|
||||
// $result = self::traducteur_stats($begin, $end, $attributes);
|
||||
// break;
|
||||
default:
|
||||
return [
|
||||
'success' => false,
|
||||
'code' => 'invalid_params',
|
||||
'message' => __('Entité non reconnue.', 'esi_crvi_agenda'),
|
||||
'csv' => null
|
||||
];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exemple : Statistiques globales sur les événements.
|
||||
* @param string $begin
|
||||
* @param string $end
|
||||
* @param array $attributes
|
||||
* @return array
|
||||
*/
|
||||
public static function events_stats(string $begin, string $end, array $attributes = []): array
|
||||
{
|
||||
$event_model = new CRVI_Event_Model();
|
||||
$events = $event_model->get_events_by('date_rdv', $begin, $attributes, $begin, $end);
|
||||
if (empty($events)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'code' => 'no_data',
|
||||
'message' => __('Aucune donnée trouvée pour la période sélectionnée.', 'esi_crvi_agenda'),
|
||||
'csv' => null
|
||||
];
|
||||
}
|
||||
// Exemple d'en-tête et de lignes (à adapter selon besoins réels)
|
||||
$header = ['ID', 'Date', 'Heure', 'Type', 'Statut', 'Département', 'Type intervention', 'Langue'];
|
||||
$rows = [];
|
||||
foreach ($events as $event) {
|
||||
$rows[] = [
|
||||
$event['id'] ?? '',
|
||||
$event['date_rdv'] ?? '',
|
||||
$event['heure_rdv'] ?? '',
|
||||
$event['type'] ?? '',
|
||||
$event['statut'] ?? '',
|
||||
$event['departement'] ?? '',
|
||||
$event['type_intervention'] ?? '',
|
||||
$event['langue'] ?? '',
|
||||
];
|
||||
}
|
||||
return [
|
||||
'success' => true,
|
||||
'code' => 'success',
|
||||
'message' => __('Export CSV généré avec succès.', 'esi_crvi_agenda'),
|
||||
'csv' => [
|
||||
'header' => $header,
|
||||
'rows' => $rows
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
// À compléter : intervenant_stats, departement_stats, etc.
|
||||
|
||||
//intervenant_stats
|
||||
/* public static function intervenant_stats(string $begin, string $end, array $attributes = []): array
|
||||
{
|
||||
$intervenants = CRVI_Intervenant_Model::all();
|
||||
$event_model = new CRVI_Event_Model();
|
||||
$header = [__('Intervenant', 'esi_crvi_agenda'), __('Nombre d\'événements', 'esi_crvi_agenda')];
|
||||
$rows = [];
|
||||
foreach ($intervenants as $intervenant) {
|
||||
$count = count($event_model->get_events_by('id_intervenant', (string)$intervenant->ID, $attributes, $begin, $end));
|
||||
if ($count > 0) {
|
||||
$rows[] = [
|
||||
get_the_title($intervenant->ID),
|
||||
$count
|
||||
];
|
||||
}
|
||||
}
|
||||
if (empty($rows)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'code' => 'no_data',
|
||||
'message' => __('Aucune donnée trouvée pour la période sélectionnée.', 'esi_crvi_agenda'),
|
||||
'csv' => null
|
||||
];
|
||||
}
|
||||
return [
|
||||
'success' => true,
|
||||
'code' => 'success',
|
||||
'message' => __('Export CSV généré avec succès.', 'esi_crvi_agenda'),
|
||||
'csv' => [
|
||||
'header' => $header,
|
||||
'rows' => $rows
|
||||
]
|
||||
];
|
||||
} */
|
||||
|
||||
//departement_stats
|
||||
public static function departement_stats(string $begin, string $end, array $attributes = []): array
|
||||
{
|
||||
// Récupérer tous les départements (CPT)
|
||||
$departements = \ESI_CRVI_AGENDA\models\CRVI_Departement_Model::all();
|
||||
$event_model = new \ESI_CRVI_AGENDA\models\CRVI_Event_Model();
|
||||
$header = [__('Département', 'esi_crvi_agenda'), __('Nombre d\'événements', 'esi_crvi_agenda')];
|
||||
$rows = [];
|
||||
foreach ($departements as $departement) {
|
||||
$count = count($event_model->get_events_by('departement', (string)$departement->ID, $attributes, $begin, $end));
|
||||
if ($count > 0) {
|
||||
$rows[] = [
|
||||
get_the_title($departement->ID),
|
||||
$count
|
||||
];
|
||||
}
|
||||
}
|
||||
if (empty($rows)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'code' => 'no_data',
|
||||
'message' => __('Aucune donnée trouvée pour la période sélectionnée.', 'esi_crvi_agenda'),
|
||||
'csv' => null
|
||||
];
|
||||
}
|
||||
return [
|
||||
'success' => true,
|
||||
'code' => 'success',
|
||||
'message' => __('Export CSV généré avec succès.', 'esi_crvi_agenda'),
|
||||
'csv' => [
|
||||
'header' => $header,
|
||||
'rows' => $rows
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
//type_intervention_stats
|
||||
public static function type_intervention_stats(string $begin, string $end, array $attributes = []): array
|
||||
{
|
||||
// Récupérer tous les types d'intervention (CPT)
|
||||
$types = \ESI_CRVI_AGENDA\models\CRVI_Type_Intervention_Model::all();
|
||||
$event_model = new \ESI_CRVI_AGENDA\models\CRVI_Event_Model();
|
||||
$header = [__('Type d\'intervention', 'esi_crvi_agenda'), __('Nombre d\'événements', 'esi_crvi_agenda')];
|
||||
$rows = [];
|
||||
foreach ($types as $type) {
|
||||
$count = count($event_model->get_events_by('type_intervention', (string)$type->ID, $attributes, $begin, $end));
|
||||
if ($count > 0) {
|
||||
$rows[] = [
|
||||
get_the_title($type->ID),
|
||||
$count
|
||||
];
|
||||
}
|
||||
}
|
||||
if (empty($rows)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'code' => 'no_data',
|
||||
'message' => __('Aucune donnée trouvée pour la période sélectionnée.', 'esi_crvi_agenda'),
|
||||
'csv' => null
|
||||
];
|
||||
}
|
||||
return [
|
||||
'success' => true,
|
||||
'code' => 'success',
|
||||
'message' => __('Export CSV généré avec succès.', 'esi_crvi_agenda'),
|
||||
'csv' => [
|
||||
'header' => $header,
|
||||
'rows' => $rows
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
2
app/hooks.php
Normal file
2
app/hooks.php
Normal file
@ -0,0 +1,2 @@
|
||||
<?php
|
||||
// Hooks personnalisés pour CRVI Agenda
|
||||
2
app/install.php
Normal file
2
app/install.php
Normal file
@ -0,0 +1,2 @@
|
||||
<?php
|
||||
// Installation et désinstallation du plugin CRVI Agenda
|
||||
1
app/libraries/.gitkeep
Normal file
1
app/libraries/.gitkeep
Normal file
@ -0,0 +1 @@
|
||||
|
||||
2
app/libraries/factory.php
Normal file
2
app/libraries/factory.php
Normal file
@ -0,0 +1,2 @@
|
||||
<?php
|
||||
// Factory pour CRVI Agenda
|
||||
2
app/libraries/hooks.php
Normal file
2
app/libraries/hooks.php
Normal file
@ -0,0 +1,2 @@
|
||||
<?php
|
||||
// Hooks personnalisés pour CRVI Agenda
|
||||
0
app/logs/debug.txt
Normal file
0
app/logs/debug.txt
Normal file
1
app/models/.gitkeep
Normal file
1
app/models/.gitkeep
Normal file
@ -0,0 +1 @@
|
||||
|
||||
654
app/models/Beneficiaire_Model.php
Normal file
654
app/models/Beneficiaire_Model.php
Normal file
@ -0,0 +1,654 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ESI_CRVI_AGENDA\models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use ESI_CRVI_AGENDA\helpers\Api_Helper;
|
||||
|
||||
class CRVI_Beneficiaire_Model extends Main_Model {
|
||||
public $id;
|
||||
public $nom;
|
||||
public $prenom;
|
||||
public $email;
|
||||
public $telephone;
|
||||
public $langues_parlees;
|
||||
public $commentaire;
|
||||
public $statut;
|
||||
|
||||
/**
|
||||
* Carte des codes de langue vers le nom de la langue (Europe continentale + Afrique du Nord)
|
||||
*/
|
||||
public static $lg_map = [
|
||||
'fr' => 'Français',
|
||||
'de' => 'Allemand',
|
||||
'it' => 'Italien',
|
||||
'es' => 'Espagnol',
|
||||
'pt' => 'Portugais',
|
||||
'nl' => 'Néerlandais',
|
||||
'en' => 'Anglais',
|
||||
'ru' => 'Russe',
|
||||
'pl' => 'Polonais',
|
||||
'ro' => 'Roumain',
|
||||
'el' => 'Grec',
|
||||
'tr' => 'Turc',
|
||||
'ar' => 'Arabe',
|
||||
'kab' => 'Kabyle',
|
||||
'ber' => 'Berbère',
|
||||
'tzm' => 'Tamazight',
|
||||
'da' => 'Danois',
|
||||
'sv' => 'Suédois',
|
||||
'no' => 'Norvégien',
|
||||
'fi' => 'Finnois',
|
||||
'cs' => 'Tchèque',
|
||||
'sk' => 'Slovaque',
|
||||
'hu' => 'Hongrois',
|
||||
'bg' => 'Bulgare',
|
||||
'hr' => 'Croate',
|
||||
'sr' => 'Serbe',
|
||||
'bs' => 'Bosnien',
|
||||
'sq' => 'Albanais',
|
||||
'mk' => 'Macédonien',
|
||||
'sl' => 'Slovène',
|
||||
'he' => 'Hébreu',
|
||||
'lt' => 'Lituanien',
|
||||
'lv' => 'Letton',
|
||||
'et' => 'Estonien',
|
||||
];
|
||||
|
||||
/**
|
||||
* Schéma des champs ACF : nom => type (ex : group, repeater, taxonomy, text...)
|
||||
*/
|
||||
public static $acf_schema = [
|
||||
'nom' => 'text',
|
||||
'prenom' => 'text',
|
||||
'email' => 'email',
|
||||
'telephone' => 'text',
|
||||
'code_postal' => 'number',
|
||||
'date_de_naissance' => 'date_picker',
|
||||
'langues_parlees' => 'taxonomy',
|
||||
'genre' => 'radio',
|
||||
'commentaire' => 'textarea',
|
||||
'statut' => 'select',
|
||||
];
|
||||
|
||||
public function __construct($data = []) {
|
||||
foreach ($data as $key => $value) {
|
||||
if (property_exists($this, $key)) {
|
||||
$this->$key = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function load_previous_benef_events($id) {
|
||||
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'crvi_agenda';
|
||||
$events = $wpdb->get_results($wpdb->prepare("SELECT * FROM $table_name WHERE id_beneficiaire = %d", $id));
|
||||
return $events;
|
||||
}
|
||||
|
||||
public function benef_previous_events($events) {
|
||||
|
||||
$previous_events = [];
|
||||
|
||||
if(!empty($events)) {
|
||||
foreach($events as $event) {
|
||||
$event = CRVI_Event_Model::load($event->id);
|
||||
$event->get_details($event->id);
|
||||
|
||||
$traducteur = CRVI_Traducteur_Model::load($event->id_traducteur);
|
||||
$local = CRVI_Local_Model::load($event->id_local);
|
||||
|
||||
$previous_events['date'] = Carbon::parse($event->date)->format('d/m/Y');
|
||||
$previous_events['heure'] = Carbon::parse($event->heure)->format('H:i');
|
||||
$previous_events['local'] = $local->nom;
|
||||
$previous_events['traducteur'] = $traducteur->nom.' '.$traducteur->prenom;
|
||||
$previous_events['statut'] = $event->statut;
|
||||
$previous_events['commentaire'] = $event->commentaire;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function benef_previous_events_list($events) {
|
||||
$previous_events = [];
|
||||
$event_count = 0;
|
||||
if(!empty($events)) {
|
||||
foreach($events as $event) {
|
||||
$previous_events[$event_count]['date'] = Carbon::parse($event->date)->format('d/m/Y');
|
||||
$previous_events[$event_count]['heure'] = Carbon::parse($event->heure)->format('H:i');
|
||||
$previous_events[$event_count]['local'] = $event->local;
|
||||
$previous_events[$event_count]['traducteur'] = $event->traducteur;
|
||||
$previous_events[$event_count]['statut'] = $event->statut;
|
||||
$previous_events[$event_count]['commentaire'] = $event->commentaire;
|
||||
}
|
||||
$event_count++;
|
||||
}
|
||||
return $previous_events;
|
||||
}
|
||||
|
||||
public function benef_previous_non_attended_events($events, $count = true) {
|
||||
$previous_events = [];
|
||||
$event_count = 0;
|
||||
if(!empty($events)) {
|
||||
foreach($events as $event) {
|
||||
$statut = $event->statut;
|
||||
|
||||
if($statut == 'non_tenu') {
|
||||
$previous_events[$event_count]['date'] = Carbon::parse($event->date)->format('d/m/Y');
|
||||
$previous_events[$event_count]['heure'] = Carbon::parse($event->heure)->format('H:i');
|
||||
$previous_events[$event_count]['local'] = $event->local;
|
||||
$previous_events[$event_count]['traducteur'] = $event->traducteur;
|
||||
$previous_events[$event_count]['statut'] = $event->statut;
|
||||
}
|
||||
}
|
||||
$event_count++;
|
||||
}
|
||||
return $previous_events;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les absences d'un bénéficiaire
|
||||
* @param int $beneficiaire_id
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function get_absences(int $beneficiaire_id) {
|
||||
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'crvi_agenda';
|
||||
|
||||
// Vérifier si la table existe
|
||||
$table_exists = $wpdb->get_var("SHOW TABLES LIKE '$table_name'");
|
||||
if (!$table_exists) {
|
||||
return new \WP_Error('table_not_found', 'Table agenda non trouvée');
|
||||
}
|
||||
|
||||
// Récupérer les événements où le bénéficiaire était absent
|
||||
$query = $wpdb->prepare(
|
||||
"SELECT id, date_rdv, heure_rdv, statut, commentaire
|
||||
FROM $table_name
|
||||
WHERE id_beneficiaire = %d
|
||||
AND statut IN ('non_tenu', 'absent', 'annule')
|
||||
ORDER BY date_rdv DESC, heure_rdv DESC
|
||||
LIMIT 10",
|
||||
$beneficiaire_id
|
||||
);
|
||||
|
||||
$absences = $wpdb->get_results($query, ARRAY_A);
|
||||
|
||||
if ($wpdb->last_error) {
|
||||
return new \WP_Error('db_error', 'Erreur lors de la récupération des absences: ' . $wpdb->last_error);
|
||||
}
|
||||
|
||||
return $absences;
|
||||
}
|
||||
|
||||
public static function load($id, $fields = []) {
|
||||
// Charger depuis CPT/meta
|
||||
$beneficiaire = get_post($id);
|
||||
if (!$beneficiaire) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Si des champs spécifiques sont demandés, ne charger que ceux-ci
|
||||
if (!empty($fields)) {
|
||||
$data = [];
|
||||
foreach ($fields as $field) {
|
||||
if ($field === 'id') {
|
||||
$data['id'] = $beneficiaire->ID;
|
||||
} elseif (property_exists(self::class, $field)) {
|
||||
$data[$field] = get_field($field, $beneficiaire->ID);
|
||||
}
|
||||
}
|
||||
return new self($data);
|
||||
}
|
||||
|
||||
// Sinon, charger tous les champs par défaut
|
||||
return new self([
|
||||
'id' => $beneficiaire->ID,
|
||||
'nom' => get_field('nom', $beneficiaire->ID),
|
||||
'prenom' => get_field('prenom', $beneficiaire->ID),
|
||||
'email' => get_field('email', $beneficiaire->ID),
|
||||
'telephone' => get_field('telephone', $beneficiaire->ID),
|
||||
'langues_parlees' => get_field('langues_parlees', $beneficiaire->ID),
|
||||
'commentaire' => get_field('commentaire', $beneficiaire->ID),
|
||||
'statut' => get_field('statut', $beneficiaire->ID),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère tous les rendez-vous liés à un bénéficiaire
|
||||
* @param int $beneficiaire_id
|
||||
* @return array
|
||||
*/
|
||||
public static function get_rendezvous_by_beneficiaire(int $beneficiaire_id): array {
|
||||
$args = [
|
||||
'post_type' => 'crvi_agenda',
|
||||
'numberposts' => -1,
|
||||
'meta_query' => [
|
||||
[
|
||||
'key' => 'id_beneficiaire',
|
||||
'value' => $beneficiaire_id,
|
||||
'compare' => '=',
|
||||
],
|
||||
],
|
||||
];
|
||||
$rdvs = get_posts($args);
|
||||
$result = [];
|
||||
foreach ($rdvs as $rdv) {
|
||||
$date = get_field('date', $rdv->ID);
|
||||
$heure = get_field('heure', $rdv->ID);
|
||||
$departements = get_field('departements', $rdv->ID); // à adapter selon structure
|
||||
$departement_nom =get_term_by('id', $departements, 'departement');
|
||||
$departement_nom = $departement_nom->name;
|
||||
$type_intervention = get_field('type_intervention', $rdv->ID);
|
||||
$type_intervention_nom =get_term_by('id', $type_intervention, 'type_intervention');
|
||||
$type_intervention_nom = $type_intervention_nom->name;
|
||||
$intervenant_id = get_field('id_intervenant', $rdv->ID);
|
||||
$langue = get_field('langue', $rdv->ID);
|
||||
$result[] = [
|
||||
'id' => $rdv->ID,
|
||||
'date' => $date,
|
||||
'heure' => $heure,
|
||||
'departements' => $departement_nom,
|
||||
'type_intervention' => $type_intervention_nom,
|
||||
'intervenant_id' => $intervenant_id,
|
||||
'langue' => $langue,
|
||||
];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les intervenants liés à un bénéficiaire, avec le nombre de rdv
|
||||
* @param int $beneficiaire_id
|
||||
* @return array
|
||||
*/
|
||||
public static function get_intervenants_by_beneficiaire(int $beneficiaire_id): array {
|
||||
$rdvs = self::get_rendezvous_by_beneficiaire($beneficiaire_id);
|
||||
$intervenants = [];
|
||||
foreach ($rdvs as $rdv) {
|
||||
$intervenant_id = $rdv['intervenant_id'];
|
||||
if (!$intervenant_id) continue;
|
||||
if (!isset($intervenants[$intervenant_id])) {
|
||||
$nom = get_field('nom', $intervenant_id);
|
||||
$prenom = get_field('prenom', $intervenant_id);
|
||||
$intervenants[$intervenant_id] = [
|
||||
'id' => $intervenant_id,
|
||||
'nom' => $nom,
|
||||
'prenom' => $prenom,
|
||||
'nb_rdv' => 1,
|
||||
];
|
||||
} else {
|
||||
$intervenants[$intervenant_id]['nb_rdv']++;
|
||||
}
|
||||
}
|
||||
return array_values($intervenants);
|
||||
}
|
||||
|
||||
public function get_relations() {
|
||||
$benef_id = $this->id;
|
||||
$rdvs = self::get_rendezvous_by_beneficiaire($benef_id);
|
||||
$futurs = [];
|
||||
$passe = [];
|
||||
$now = Carbon::now();
|
||||
foreach ($rdvs as $rdv) {
|
||||
// On suppose que la date est au format d/m/Y ou Y-m-d
|
||||
$date = $rdv['date'];
|
||||
$heure = $rdv['heure'] ?? '00:00';
|
||||
$dt = null;
|
||||
if ($date) {
|
||||
$dt = Carbon::createFromFormat('d/m/Y H:i', $date.' '.$heure);
|
||||
if (!$dt) {
|
||||
$dt = Carbon::createFromFormat('Y-m-d H:i', $date.' '.$heure);
|
||||
}
|
||||
}
|
||||
$rdv_info = [
|
||||
'date' => $date,
|
||||
'heure' => $heure,
|
||||
'departements' => $rdv['departements'],
|
||||
'type_intervention' => $rdv['type_intervention'],
|
||||
'intervenant' => $rdv['intervenant_id'] ? [
|
||||
'id' => $rdv['intervenant_id'],
|
||||
'nom' => get_field('nom', $rdv['intervenant_id']),
|
||||
'prenom' => get_field('prenom', $rdv['intervenant_id']),
|
||||
] : null,
|
||||
'langue' => $rdv['langue'],
|
||||
];
|
||||
if ($dt && $dt->greaterThanOrEqualTo($now)) {
|
||||
$futurs[] = $rdv_info;
|
||||
} else {
|
||||
$passe[] = $rdv_info;
|
||||
}
|
||||
}
|
||||
$intervenants = self::get_intervenants_by_beneficiaire($benef_id);
|
||||
return [
|
||||
'rendezvous' => [
|
||||
'futurs' => $futurs,
|
||||
'passe' => $passe,
|
||||
],
|
||||
'intervenants' => $intervenants,
|
||||
];
|
||||
}
|
||||
|
||||
public function get_all_beneficiaires($attributes = []) {
|
||||
$args = [
|
||||
'post_type' => 'beneficiaire',
|
||||
'numberposts' => -1,
|
||||
];
|
||||
|
||||
// Si des critères sont fournis, construire le meta_query
|
||||
if (!empty($attributes)) {
|
||||
$meta_query = ['relation' => 'AND'];
|
||||
foreach ($attributes as $key => $value) {
|
||||
$meta_query[] = [
|
||||
'key' => $key,
|
||||
'value' => $value,
|
||||
'compare' => '=',
|
||||
];
|
||||
}
|
||||
$args['meta_query'] = $meta_query;
|
||||
}
|
||||
|
||||
$beneficiaires = get_posts($args);
|
||||
$result = [];
|
||||
foreach ($beneficiaires as $beneficiaire) {
|
||||
$result[] = new self([
|
||||
'id' => $beneficiaire->ID,
|
||||
'nom' => get_field('nom', $beneficiaire->ID),
|
||||
'prenom' => get_field('prenom', $beneficiaire->ID),
|
||||
'email' => get_field('email', $beneficiaire->ID),
|
||||
'telephone' => get_field('telephone', $beneficiaire->ID),
|
||||
'langues_parlees' => get_field('langues_parlees', $beneficiaire->ID),
|
||||
'commentaire' => get_field('commentaire', $beneficiaire->ID),
|
||||
'statut' => get_field('statut', $beneficiaire->ID),
|
||||
]);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Créer ou mettre à jour un bénéficiaire (upsert par email).
|
||||
* Utilisable en REST (retour structuré) ou import CSV (retour simple).
|
||||
* @param array $data
|
||||
* @param bool $as_rest
|
||||
* @return array|string|false
|
||||
*/
|
||||
public static function create(array $data, bool $as_rest = false) {
|
||||
// 1. Vérifier les droits (admin ou opérateur)
|
||||
if (!current_user_can('edit_posts')) {
|
||||
if ($as_rest) {
|
||||
return [
|
||||
'success' => false,
|
||||
'code' => 403,
|
||||
'message' => 'Non autorisé',
|
||||
'data' => null
|
||||
];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// 2. Valider les données (ex : nom, prénom, email obligatoires)
|
||||
if (empty($data['nom']) || empty($data['prenom']) || empty($data['email'])) {
|
||||
|
||||
if ($as_rest) {
|
||||
return [
|
||||
'success' => false,
|
||||
'code' => 400,
|
||||
'message' => 'Champs obligatoires manquants',
|
||||
'data' => null
|
||||
];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3. Vérifier si le bénéficiaire existe déjà (unicité email)
|
||||
$existing = get_posts([
|
||||
'post_type' => 'beneficiaire',
|
||||
'meta_key' => 'email',
|
||||
'meta_value' => $data['email'],
|
||||
'post_status'=> 'any',
|
||||
'numberposts'=> 1,
|
||||
]);
|
||||
|
||||
if ($existing) {
|
||||
|
||||
// Mise à jour
|
||||
$post_id = $existing[0]->ID ?? $existing[0];
|
||||
wp_update_post([
|
||||
'ID' => $post_id,
|
||||
'post_title' => ($data['nom'] ?? get_post_meta($post_id, 'nom', true)) . ' ' . ($data['prenom'] ?? get_post_meta($post_id, 'prenom', true)),
|
||||
]);
|
||||
foreach ($data as $key => $value) {
|
||||
if ($key === 'langues_parlees') {
|
||||
// Logique spécifique : créer le terme avec le bon nom (lg_map) et slug
|
||||
if (!taxonomy_exists('langue_beneficiaire')) {
|
||||
register_taxonomy(
|
||||
'langue_beneficiaire',
|
||||
'beneficiaire',
|
||||
[
|
||||
'label' => 'Langues bénéficiaire',
|
||||
'public' => false,
|
||||
'hierarchical' => false,
|
||||
'show_ui' => true,
|
||||
'show_in_rest' => false,
|
||||
]
|
||||
);
|
||||
}
|
||||
$langue_ids = [];
|
||||
$langues = is_array($value) ? $value : explode('|', $value);
|
||||
foreach ($langues as $slug) {
|
||||
$slug = trim($slug);
|
||||
if (empty($slug)) continue;
|
||||
$term = get_term_by('slug', $slug, 'langue_beneficiaire');
|
||||
if (!$term) {
|
||||
$nom = self::$lg_map[$slug] ?? $slug;
|
||||
$result = wp_insert_term($nom, 'langue_beneficiaire', ['slug' => $slug]);
|
||||
if (!is_wp_error($result) && isset($result['term_id'])) {
|
||||
$langue_ids[] = $result['term_id'];
|
||||
}
|
||||
} else {
|
||||
$langue_ids[] = $term->term_id;
|
||||
}
|
||||
|
||||
}
|
||||
if (!empty($langue_ids)) {
|
||||
update_field('langues_parlees', $langue_ids, $post_id);
|
||||
}
|
||||
} elseif (isset(self::$acf_schema[$key])) {
|
||||
self::set_acf_field($post_id, $key, $value, self::$acf_schema);
|
||||
} else {
|
||||
update_post_meta($post_id, $key, $value);
|
||||
}
|
||||
}
|
||||
if ($as_rest) {
|
||||
return [
|
||||
'success' => true,
|
||||
'code' => 200,
|
||||
'message' => 'Bénéficiaire mis à jour',
|
||||
'data' => [ 'id' => $post_id ]
|
||||
];
|
||||
}
|
||||
|
||||
return 'updated';
|
||||
} else {
|
||||
// Création
|
||||
$post_id = wp_insert_post([
|
||||
'post_type' => 'beneficiaire',
|
||||
'post_title' => $data['nom'] . ' ' . $data['prenom'],
|
||||
'post_status' => 'publish',
|
||||
]);
|
||||
if (is_wp_error($post_id)) {
|
||||
if ($as_rest) {
|
||||
return [
|
||||
'success' => false,
|
||||
'code' => 500,
|
||||
'message' => 'Erreur lors de la création',
|
||||
'data' => null
|
||||
];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
foreach ($data as $key => $value) {
|
||||
if ($key === 'langues_parlees') {
|
||||
// Logique spécifique : créer le terme avec le bon nom (lg_map) et slug
|
||||
if (!taxonomy_exists('langue_beneficiaire')) {
|
||||
register_taxonomy(
|
||||
'langue_beneficiaire',
|
||||
'beneficiaire',
|
||||
[
|
||||
'label' => 'Langues bénéficiaire',
|
||||
'public' => false,
|
||||
'hierarchical' => false,
|
||||
'show_ui' => true,
|
||||
'show_in_rest' => false,
|
||||
]
|
||||
);
|
||||
}
|
||||
$langue_ids = [];
|
||||
$langues = is_array($value) ? $value : explode('|', $value);
|
||||
foreach ($langues as $slug) {
|
||||
$slug = trim($slug);
|
||||
if (empty($slug)) continue;
|
||||
$term = get_term_by('slug', $slug, 'langue_beneficiaire');
|
||||
if (!$term) {
|
||||
$nom = self::$lg_map[$slug] ?? $slug;
|
||||
$result = wp_insert_term($nom, 'langue_beneficiaire', ['slug' => $slug]);
|
||||
if (!is_wp_error($result) && isset($result['term_id'])) {
|
||||
$langue_ids[] = $result['term_id'];
|
||||
}
|
||||
} else {
|
||||
$langue_ids[] = $term->term_id;
|
||||
}
|
||||
}
|
||||
if (!empty($langue_ids)) {
|
||||
update_field('langue_beneficiaire', $langue_ids, $post_id);
|
||||
}
|
||||
} elseif (isset(self::$acf_schema[$key])) {
|
||||
self::set_acf_field($post_id, $key, $value, self::$acf_schema);
|
||||
} else {
|
||||
update_post_meta($post_id, $key, $value);
|
||||
}
|
||||
}
|
||||
if ($as_rest) {
|
||||
return [
|
||||
'success' => true,
|
||||
'code' => 201,
|
||||
'message' => 'Bénéficiaire créé',
|
||||
'data' => [ 'id' => $post_id ]
|
||||
];
|
||||
}
|
||||
return 'created';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mettre à jour un bénéficiaire.
|
||||
* @param int $id
|
||||
* @param array $data
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
public function update(int $id, array $data) {
|
||||
// 1. Vérifier les droits
|
||||
if (!current_user_can('edit_posts')) {
|
||||
return Api_Helper::json_error('Non autorisé', 403);
|
||||
}
|
||||
|
||||
// 2. Vérifier l'existence du post
|
||||
$post = get_post($id);
|
||||
if (!$post || $post->post_type !== 'beneficiaire') {
|
||||
return Api_Helper::json_error('Bénéficiaire introuvable', 404);
|
||||
}
|
||||
|
||||
// 3. Valider les données (ex : email unique si modifié)
|
||||
if (!empty($data['email'])) {
|
||||
$existing = get_posts([
|
||||
'post_type' => 'beneficiaire',
|
||||
'meta_key' => 'email',
|
||||
'meta_value' => $data['email'],
|
||||
'post_status'=> 'any',
|
||||
'exclude' => [$id],
|
||||
'numberposts'=> 1,
|
||||
]);
|
||||
if ($existing) {
|
||||
return Api_Helper::json_error('Email déjà utilisé', 409);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Mettre à jour le post
|
||||
$result = wp_update_post([
|
||||
'ID' => $id,
|
||||
'post_title' => ($data['nom'] ?? get_post_meta($id, 'nom', true)) . ' ' . ($data['prenom'] ?? get_post_meta($id, 'prenom', true)),
|
||||
], true);
|
||||
if (is_wp_error($result)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// 5. Mettre à jour les champs personnalisés
|
||||
foreach ($data as $key => $value) {
|
||||
update_post_meta($id, $key, $value);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprimer un bénéficiaire (corbeille WordPress)
|
||||
* @param int $id
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
public function delete(int $id) {
|
||||
// 1. Vérifier les droits (admin uniquement)
|
||||
if (!current_user_can('delete_posts')) {
|
||||
return Api_Helper::json_error('Non autorisé', 403);
|
||||
}
|
||||
|
||||
// 2. Vérifier l'existence du post
|
||||
$post = get_post($id);
|
||||
if (!$post || $post->post_type !== 'beneficiaire') {
|
||||
return Api_Helper::json_error('Bénéficiaire introuvable', 404);
|
||||
}
|
||||
|
||||
// 3. Mettre à la corbeille
|
||||
$result = wp_trash_post($id);
|
||||
if (!$result) {
|
||||
return Api_Helper::json_error('Erreur lors de la suppression', 500);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule l'âge à partir d'une date de naissance (format d/m/Y ou Y-m-d) ou d'un ID de bénéficiaire
|
||||
* @param string|int|null $date_naissance_ou_id
|
||||
* @param string $champ_acf Nom du champ ACF pour la date de naissance (par défaut 'date_de_naissance')
|
||||
* @return int|null
|
||||
*/
|
||||
public static function calculer_age($date_naissance_ou_id, string $champ_acf = 'date_de_naissance'): ?int {
|
||||
$date_naissance = null;
|
||||
// Si c'est un entier, on considère que c'est un ID de bénéficiaire
|
||||
if (is_int($date_naissance_ou_id)) {
|
||||
$date_naissance = get_field($champ_acf, $date_naissance_ou_id);
|
||||
} elseif (is_string($date_naissance_ou_id)) {
|
||||
$date_naissance = $date_naissance_ou_id;
|
||||
}
|
||||
if (empty($date_naissance)) {
|
||||
return null;
|
||||
}
|
||||
// Gérer le format d/m/Y (ACF) ou Y-m-d
|
||||
$formats = ['d/m/Y', 'Y-m-d'];
|
||||
foreach ($formats as $format) {
|
||||
$date = \DateTime::createFromFormat($format, $date_naissance);
|
||||
if ($date !== false) {
|
||||
$now = new \DateTime();
|
||||
$age = $now->diff($date)->y;
|
||||
return $age;
|
||||
}
|
||||
}
|
||||
// Si aucun format ne correspond
|
||||
return null;
|
||||
}
|
||||
}
|
||||
279
app/models/CRVI_Presence_Model.php
Normal file
279
app/models/CRVI_Presence_Model.php
Normal file
@ -0,0 +1,279 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ESI_CRVI_AGENDA\models;
|
||||
|
||||
/**
|
||||
* Modèle pour la gestion des présences et des personnes
|
||||
*/
|
||||
class CRVI_Presence_Model {
|
||||
|
||||
/**
|
||||
* Crée les tables nécessaires pour les présences
|
||||
*/
|
||||
public static function create_tables(): void {
|
||||
global $wpdb;
|
||||
|
||||
$charset_collate = $wpdb->get_charset_collate();
|
||||
|
||||
// Table des personnes (pour les participants non-bénéficiaires)
|
||||
$persons_table = $wpdb->prefix . 'crvi_agenda_persons';
|
||||
$sql_persons = "CREATE TABLE IF NOT EXISTS `{$persons_table}` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`nom` varchar(255) NOT NULL,
|
||||
`prenom` varchar(255) NOT NULL,
|
||||
`langue` varchar(100) DEFAULT NULL,
|
||||
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_nom_prenom` (`nom`, `prenom`)
|
||||
) {$charset_collate};";
|
||||
|
||||
// Table des présences
|
||||
$presence_table = $wpdb->prefix . 'crvi_agenda_presence';
|
||||
$sql_presence = "CREATE TABLE IF NOT EXISTS `{$presence_table}` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`event_id` int(11) NOT NULL,
|
||||
`person_id` int(11) DEFAULT NULL,
|
||||
`beneficiaire_id` int(11) DEFAULT NULL,
|
||||
`is_present` tinyint(1) NOT NULL DEFAULT 0,
|
||||
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_event_id` (`event_id`),
|
||||
KEY `idx_person_id` (`person_id`),
|
||||
KEY `idx_beneficiaire_id` (`beneficiaire_id`)
|
||||
) {$charset_collate};";
|
||||
|
||||
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
|
||||
dbDelta($sql_persons);
|
||||
dbDelta($sql_presence);
|
||||
}
|
||||
|
||||
/**
|
||||
* Insère ou récupère une personne dans la table wp_crvi_agenda_persons
|
||||
* Si une personne avec le même nom et prénom existe déjà, retourne son ID
|
||||
* Sinon, crée une nouvelle entrée
|
||||
*
|
||||
* @param string $nom
|
||||
* @param string $prenom
|
||||
* @param string $langue
|
||||
* @return int L'ID de la personne
|
||||
*/
|
||||
public static function save_person(string $nom, string $prenom, string $langue): int {
|
||||
global $wpdb;
|
||||
|
||||
$table_name = $wpdb->prefix . 'crvi_agenda_persons';
|
||||
|
||||
// Nettoyer les données
|
||||
$nom = sanitize_text_field(trim($nom));
|
||||
$prenom = sanitize_text_field(trim($prenom));
|
||||
$langue = sanitize_text_field(trim($langue));
|
||||
|
||||
if (empty($nom) || empty($prenom)) {
|
||||
throw new \InvalidArgumentException('Le nom et le prénom sont requis');
|
||||
}
|
||||
|
||||
// Vérifier si une personne avec le même nom et prénom existe déjà
|
||||
$existing = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT id FROM $table_name WHERE nom = %s AND prenom = %s LIMIT 1",
|
||||
$nom,
|
||||
$prenom
|
||||
));
|
||||
|
||||
if ($existing) {
|
||||
// Mettre à jour la langue si elle est différente
|
||||
$wpdb->update(
|
||||
$table_name,
|
||||
['langue' => $langue],
|
||||
['id' => $existing],
|
||||
['%s'],
|
||||
['%d']
|
||||
);
|
||||
return (int) $existing;
|
||||
}
|
||||
|
||||
// Créer une nouvelle personne
|
||||
$result = $wpdb->insert(
|
||||
$table_name,
|
||||
[
|
||||
'nom' => $nom,
|
||||
'prenom' => $prenom,
|
||||
'langue' => $langue
|
||||
],
|
||||
['%s', '%s', '%s']
|
||||
);
|
||||
|
||||
if ($result === false) {
|
||||
throw new \RuntimeException('Erreur lors de l\'insertion de la personne');
|
||||
}
|
||||
|
||||
return (int) $wpdb->insert_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enregistre ou met à jour une présence dans la table wp_crvi_agenda_presence
|
||||
* Si une présence existe déjà pour cet event_id et person_id/beneficiaire_id, elle est mise à jour
|
||||
* Sinon, une nouvelle entrée est créée
|
||||
*
|
||||
* @param int $event_id
|
||||
* @param int|null $person_id ID de la personne (si ce n'est pas un bénéficiaire existant)
|
||||
* @param bool $is_present
|
||||
* @param int|null $beneficiaire_id ID du bénéficiaire (si c'est un bénéficiaire existant)
|
||||
* @return int L'ID de la présence (nouvelle ou mise à jour)
|
||||
*/
|
||||
public static function save_presence(int $event_id, ?int $person_id, bool $is_present, ?int $beneficiaire_id = null): int {
|
||||
global $wpdb;
|
||||
|
||||
$table_name = $wpdb->prefix . 'crvi_agenda_presence';
|
||||
|
||||
// Vérifier si la table a le champ beneficiaire_id, sinon l'ajouter
|
||||
self::ensure_beneficiaire_id_column($table_name);
|
||||
|
||||
// Si beneficiaire_id est fourni, utiliser beneficiaire_id, sinon utiliser person_id
|
||||
if ($beneficiaire_id !== null) {
|
||||
// Vérifier si une présence existe déjà pour cet événement et ce bénéficiaire
|
||||
$existing = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT id FROM $table_name WHERE event_id = %d AND beneficiaire_id = %d LIMIT 1",
|
||||
$event_id,
|
||||
$beneficiaire_id
|
||||
));
|
||||
|
||||
if ($existing) {
|
||||
// Mettre à jour la présence existante
|
||||
$wpdb->update(
|
||||
$table_name,
|
||||
['is_present' => $is_present ? 1 : 0],
|
||||
['id' => $existing],
|
||||
['%d'],
|
||||
['%d']
|
||||
);
|
||||
return (int) $existing;
|
||||
}
|
||||
|
||||
// Créer une nouvelle présence avec beneficiaire_id
|
||||
$result = $wpdb->insert(
|
||||
$table_name,
|
||||
[
|
||||
'event_id' => $event_id,
|
||||
'person_id' => 0, // 0 ou NULL pour indiquer que c'est un bénéficiaire
|
||||
'beneficiaire_id' => $beneficiaire_id,
|
||||
'is_present' => $is_present ? 1 : 0
|
||||
],
|
||||
['%d', '%d', '%d', '%d']
|
||||
);
|
||||
} else {
|
||||
// Utiliser person_id (comportement original)
|
||||
if ($person_id === null) {
|
||||
throw new \InvalidArgumentException('person_id ou beneficiaire_id doit être fourni');
|
||||
}
|
||||
|
||||
// Vérifier si une présence existe déjà pour cet événement et cette personne
|
||||
$existing = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT id FROM $table_name WHERE event_id = %d AND person_id = %d AND (beneficiaire_id IS NULL OR beneficiaire_id = 0) LIMIT 1",
|
||||
$event_id,
|
||||
$person_id
|
||||
));
|
||||
|
||||
if ($existing) {
|
||||
// Mettre à jour la présence existante
|
||||
$wpdb->update(
|
||||
$table_name,
|
||||
['is_present' => $is_present ? 1 : 0],
|
||||
['id' => $existing],
|
||||
['%d'],
|
||||
['%d']
|
||||
);
|
||||
return (int) $existing;
|
||||
}
|
||||
|
||||
// Créer une nouvelle présence avec person_id
|
||||
$result = $wpdb->insert(
|
||||
$table_name,
|
||||
[
|
||||
'event_id' => $event_id,
|
||||
'person_id' => $person_id,
|
||||
'beneficiaire_id' => null,
|
||||
'is_present' => $is_present ? 1 : 0
|
||||
],
|
||||
['%d', '%d', '%d', '%d']
|
||||
);
|
||||
}
|
||||
|
||||
if ($result === false) {
|
||||
throw new \RuntimeException('Erreur lors de l\'insertion de la présence');
|
||||
}
|
||||
|
||||
return (int) $wpdb->insert_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie et ajoute la colonne beneficiaire_id si elle n'existe pas
|
||||
* @param string $table_name
|
||||
*/
|
||||
private static function ensure_beneficiaire_id_column(string $table_name): void {
|
||||
global $wpdb;
|
||||
|
||||
// Vérifier si la colonne existe (le nom de la table est sûr car il vient de $wpdb->prefix)
|
||||
$column_exists = $wpdb->get_results(
|
||||
"SHOW COLUMNS FROM `{$table_name}` LIKE 'beneficiaire_id'"
|
||||
);
|
||||
|
||||
if (empty($column_exists)) {
|
||||
// Ajouter la colonne beneficiaire_id
|
||||
$wpdb->query("ALTER TABLE `{$table_name}` ADD COLUMN `beneficiaire_id` int(11) DEFAULT NULL AFTER `person_id`");
|
||||
// Ajouter un index pour améliorer les performances
|
||||
$wpdb->query("ALTER TABLE `{$table_name}` ADD INDEX `idx_beneficiaire_id` (`beneficiaire_id`)");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère toutes les présences pour un événement donné
|
||||
*
|
||||
* @param int $event_id
|
||||
* @return array Tableau associatif avec les données des présences et des personnes
|
||||
*/
|
||||
public static function get_presences_by_event(int $event_id): array {
|
||||
global $wpdb;
|
||||
|
||||
$presence_table = $wpdb->prefix . 'crvi_agenda_presence';
|
||||
$persons_table = $wpdb->prefix . 'crvi_agenda_persons';
|
||||
|
||||
$results = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT
|
||||
p.id as presence_id,
|
||||
p.event_id,
|
||||
p.person_id,
|
||||
p.is_present,
|
||||
per.nom,
|
||||
per.prenom,
|
||||
per.langue
|
||||
FROM $presence_table p
|
||||
INNER JOIN $persons_table per ON p.person_id = per.id
|
||||
WHERE p.event_id = %d
|
||||
ORDER BY per.nom, per.prenom",
|
||||
$event_id
|
||||
), ARRAY_A);
|
||||
|
||||
return $results ?: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime toutes les présences pour un événement donné
|
||||
*
|
||||
* @param int $event_id
|
||||
* @return bool True si succès, false sinon
|
||||
*/
|
||||
public static function delete_presences_by_event(int $event_id): bool {
|
||||
global $wpdb;
|
||||
|
||||
$table_name = $wpdb->prefix . 'crvi_agenda_presence';
|
||||
|
||||
$result = $wpdb->delete(
|
||||
$table_name,
|
||||
['event_id' => $event_id],
|
||||
['%d']
|
||||
);
|
||||
|
||||
return $result !== false;
|
||||
}
|
||||
}
|
||||
|
||||
208
app/models/Departement_Model.php
Normal file
208
app/models/Departement_Model.php
Normal file
@ -0,0 +1,208 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ESI_CRVI_AGENDA\models;
|
||||
|
||||
use ESI_CRVI_AGENDA\helpers\Api_Helper;
|
||||
|
||||
class CRVI_Departement_Model extends Main_Model
|
||||
{
|
||||
/**
|
||||
* Schéma des champs ACF : nom => type (ex : group, repeater, taxonomy, text...)
|
||||
*/
|
||||
public static $acf_schema = [
|
||||
'nom' => 'text',
|
||||
'type_dinterventions' => 'repeater',
|
||||
];
|
||||
|
||||
/**
|
||||
* Charge un département par ID (CPT)
|
||||
* @param int $id
|
||||
* @param array $fields Champs spécifiques à charger (optionnel)
|
||||
* @return self|null
|
||||
*/
|
||||
public static function load($id, $fields = [])
|
||||
{
|
||||
$post = get_post($id);
|
||||
if (!$post || $post->post_type !== 'departement') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Si des champs spécifiques sont demandés, ne charger que ceux-ci
|
||||
if (!empty($fields)) {
|
||||
$data = [];
|
||||
foreach ($fields as $field) {
|
||||
if ($field === 'id') {
|
||||
$data['id'] = $post->ID;
|
||||
} elseif ($field === 'nom') {
|
||||
$data['nom'] = $post->post_title;
|
||||
} elseif (property_exists(self::class, $field)) {
|
||||
$data[$field] = get_field($field, $post->ID);
|
||||
}
|
||||
}
|
||||
return new self($data);
|
||||
}
|
||||
|
||||
// Sinon, retourner le post complet (comportement par défaut)
|
||||
return $post;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne la liste de tous les départements (CPT)
|
||||
*/
|
||||
public static function all($simple_list = false)
|
||||
{
|
||||
$posts = get_posts([
|
||||
'post_type' => 'departement',
|
||||
'posts_per_page' => -1,
|
||||
'post_status' => 'publish',
|
||||
]);
|
||||
|
||||
if ($simple_list) {
|
||||
$posts = array_map(function($post) {
|
||||
return [
|
||||
'id' => $post->ID,
|
||||
'nom' => $post->post_title,
|
||||
];
|
||||
}, $posts);
|
||||
}
|
||||
return $posts;
|
||||
}
|
||||
|
||||
//crete or update if exist
|
||||
//check if nom is unique
|
||||
public static function create(array $data, bool $as_rest = false) {
|
||||
|
||||
$is_update = false;
|
||||
|
||||
// 1. Vérifier les droits
|
||||
if (!current_user_can('edit_posts')) {
|
||||
if ($as_rest) {
|
||||
return [
|
||||
'success' => false,
|
||||
'code' => 403,
|
||||
'message' => 'Non autorisé',
|
||||
'data' => null
|
||||
];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. Valider les données (nom obligatoire)
|
||||
if (empty($data['nom'])) {
|
||||
if ($as_rest) {
|
||||
return [
|
||||
'success' => false,
|
||||
'code' => 400,
|
||||
'message' => 'Le nom du département est obligatoire',
|
||||
'data' => null
|
||||
];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3. Vérifier si le département existe déjà (unicité nom)
|
||||
$existing = get_posts([
|
||||
'post_type' => 'departement',
|
||||
'meta_key' => 'nom',
|
||||
'meta_value' => $data['nom'],
|
||||
'post_status'=> 'any',
|
||||
'numberposts'=> 1,
|
||||
]);
|
||||
if ($existing) {
|
||||
$is_update = true;
|
||||
// Mise à jour
|
||||
$post_id = $existing[0]->ID ?? $existing[0];
|
||||
wp_update_post([
|
||||
'ID' => $post_id,
|
||||
'post_title' => $data['nom'],
|
||||
]);
|
||||
foreach ($data as $key => $value) {
|
||||
if (isset(self::$acf_schema[$key])) {
|
||||
self::set_acf_field($post_id, $key, $value, self::$acf_schema);
|
||||
} else {
|
||||
update_post_meta($post_id, $key, $value);
|
||||
}
|
||||
}
|
||||
/* if ($as_rest) {
|
||||
return [
|
||||
'success' => true,
|
||||
'code' => 200,
|
||||
'message' => 'Département mis à jour',
|
||||
'data' => [ 'id' => $post_id ]
|
||||
];
|
||||
} */
|
||||
/* return 'updated'; */
|
||||
} else {
|
||||
// Création
|
||||
$post_id = wp_insert_post([
|
||||
'post_type' => 'departement',
|
||||
'post_title' => $data['nom'],
|
||||
'post_status' => 'publish',
|
||||
]);
|
||||
if (is_wp_error($post_id)) {
|
||||
if ($as_rest) {
|
||||
return [
|
||||
'success' => false,
|
||||
'code' => 500,
|
||||
'message' => 'Erreur lors de la création',
|
||||
'data' => null
|
||||
];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
foreach ($data as $key => $value) {
|
||||
update_post_meta($post_id, $key, $value);
|
||||
}
|
||||
/* if ($as_rest) {
|
||||
return [
|
||||
'success' => true,
|
||||
'code' => 201,
|
||||
'message' => 'Département créé',
|
||||
'data' => [ 'id' => $post_id ]
|
||||
];
|
||||
} */
|
||||
/* return 'created'; */
|
||||
}
|
||||
|
||||
// 4. Créer le type d'intervention associé
|
||||
|
||||
$type_interventions = $data['types-dintervention'] ?? [];
|
||||
|
||||
if (!is_array($type_interventions)) {
|
||||
if (strstr($type_interventions, '|')) {
|
||||
$type_interventions = explode('|', $type_interventions);
|
||||
} else {
|
||||
$type_interventions = [$type_interventions];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//delete all type_intervention_id meta
|
||||
//acf type_dinterventions -> repeater
|
||||
foreach ($type_interventions as $type_intervention) {
|
||||
$type_intervention_id = CRVI_Type_Intervention_Model::create([
|
||||
'post_title' => $type_intervention,
|
||||
]);
|
||||
|
||||
$type_interventions_ar[] = [
|
||||
'type' => $type_intervention,
|
||||
'article_intervention' => $type_intervention_id
|
||||
];
|
||||
|
||||
self::set_acf_field($post_id, 'type_dinterventions', $type_interventions_ar, self::$acf_schema);
|
||||
}
|
||||
|
||||
if ($as_rest) {
|
||||
return [
|
||||
'success' => true,
|
||||
'code' => 201,
|
||||
'message' => 'Département créé',
|
||||
'data' => [ 'id' => $post_id ]
|
||||
];
|
||||
}
|
||||
|
||||
return $is_update ? 'updated' : 'created';
|
||||
}
|
||||
}
|
||||
1811
app/models/Event_Model.php
Normal file
1811
app/models/Event_Model.php
Normal file
File diff suppressed because it is too large
Load Diff
724
app/models/Event_Permission_Model.php
Normal file
724
app/models/Event_Permission_Model.php
Normal file
@ -0,0 +1,724 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ESI_CRVI_AGENDA\models;
|
||||
|
||||
/**
|
||||
* Modèle pour la table crvi_event_permission
|
||||
*
|
||||
* Cette table stocke les DISPONIBILITÉS de l'intervenant pour un événement :
|
||||
* - Jours de la semaine disponibles (lundi, mardi, etc. - PAS des dates spécifiques)
|
||||
* - Plages horaires disponibles
|
||||
* - Indisponibilités ponctuelles (dates spécifiques où l'intervenant n'est pas disponible)
|
||||
*
|
||||
* OPTIMISATION : Permet de vérifier rapidement les disponibilités sans charger l'intervenant
|
||||
*/
|
||||
class Event_Permission_Model extends Main_Model {
|
||||
public $id;
|
||||
public $event_id;
|
||||
public $jours_permis; // JSON array ["lundi", "mardi"] - JOURS DE LA SEMAINE de l'intervenant
|
||||
public $plages_horaires; // JSON array [{"debut":"09:00","fin":"17:00"}] - Heures disponibles (intervenant)
|
||||
public $indisponibilites; // JSON array - Indisponibilités ponctuelles de l'intervenant
|
||||
public $traducteur_jours_permis; // JSON array - Jours de la semaine du traducteur (si présent)
|
||||
public $traducteur_indisponibilites; // JSON array - Indisponibilités ponctuelles du traducteur (si présent)
|
||||
public $local_indisponibilites; // JSON array - Indisponibilités ponctuelles du local
|
||||
public $locaux_autories; // JSON array [1,2,3] (IDs de locaux)
|
||||
public $departements_autories; // JSON array [1,2,3] (IDs de départements)
|
||||
public $types_intervention_autories; // JSON array [1,2,3]
|
||||
public $traducteurs_disponibles_par_langue; // JSON object {"fr": [1,2,3], "en": [4,5]} - IDs des traducteurs disponibles par langue
|
||||
public $date_creation;
|
||||
public $date_modification;
|
||||
public $cree_par;
|
||||
public $modifie_par;
|
||||
|
||||
/**
|
||||
* Crée la table crvi_event_permission
|
||||
*
|
||||
* Stocke les disponibilités de l'intervenant pour chaque événement :
|
||||
* - Jours de la semaine (lundi, mardi, etc.)
|
||||
* - Plages horaires (heures disponibles)
|
||||
* - Indisponibilités ponctuelles (dates spécifiques)
|
||||
*/
|
||||
public static function create_table() {
|
||||
global $wpdb;
|
||||
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
||||
|
||||
$table_name = $wpdb->prefix . 'crvi_event_permission';
|
||||
$charset_collate = $wpdb->get_charset_collate();
|
||||
|
||||
// Vérifier si la table existe déjà
|
||||
if ($wpdb->get_var("SHOW TABLES LIKE '$table_name'") === $table_name) {
|
||||
return; // La table existe déjà
|
||||
}
|
||||
|
||||
$sql = "CREATE TABLE $table_name (
|
||||
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
event_id BIGINT(20) UNSIGNED NOT NULL,
|
||||
jours_permis TEXT DEFAULT NULL,
|
||||
plages_horaires TEXT DEFAULT NULL,
|
||||
indisponibilites TEXT DEFAULT NULL,
|
||||
-- Disponibilités traducteur (si présent)
|
||||
traducteur_jours_permis TEXT DEFAULT NULL,
|
||||
traducteur_indisponibilites TEXT DEFAULT NULL,
|
||||
-- Disponibilités local
|
||||
local_indisponibilites TEXT DEFAULT NULL,
|
||||
locaux_autories TEXT DEFAULT NULL,
|
||||
departements_autories TEXT DEFAULT NULL,
|
||||
types_intervention_autories TEXT DEFAULT NULL,
|
||||
traducteurs_disponibles_par_langue TEXT DEFAULT NULL COMMENT 'Présets traducteurs disponibles par langue (JSON)',
|
||||
date_creation DATETIME NOT NULL,
|
||||
date_modification DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
|
||||
cree_par BIGINT(20) UNSIGNED DEFAULT NULL,
|
||||
modifie_par BIGINT(20) UNSIGNED DEFAULT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY event_id (event_id),
|
||||
KEY event_id_index (event_id)
|
||||
) $charset_collate;";
|
||||
|
||||
// Si la table existe déjà, ajouter les nouvelles colonnes si nécessaire
|
||||
if ($wpdb->get_var("SHOW TABLES LIKE '$table_name'") === $table_name) {
|
||||
// Ajouter les colonnes pour traducteur et local si elles n'existent pas
|
||||
$columns_to_add = [
|
||||
'traducteur_jours_permis' => 'ALTER TABLE ' . $table_name . ' ADD COLUMN traducteur_jours_permis TEXT DEFAULT NULL',
|
||||
'traducteur_indisponibilites' => 'ALTER TABLE ' . $table_name . ' ADD COLUMN traducteur_indisponibilites TEXT DEFAULT NULL',
|
||||
'local_indisponibilites' => 'ALTER TABLE ' . $table_name . ' ADD COLUMN local_indisponibilites TEXT DEFAULT NULL',
|
||||
'traducteurs_disponibles_par_langue' => 'ALTER TABLE ' . $table_name . ' ADD COLUMN traducteurs_disponibles_par_langue TEXT DEFAULT NULL',
|
||||
];
|
||||
|
||||
foreach ($columns_to_add as $column => $sql_add) {
|
||||
$column_exists = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s AND COLUMN_NAME = %s",
|
||||
DB_NAME, $table_name, $column
|
||||
));
|
||||
|
||||
if (empty($column_exists)) {
|
||||
$wpdb->query($sql_add);
|
||||
}
|
||||
}
|
||||
|
||||
return; // Table existe, colonnes ajoutées si nécessaire
|
||||
}
|
||||
|
||||
dbDelta($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée les disponibilités à partir de l'intervenant, traducteur et local lors de la création d'un événement
|
||||
*
|
||||
* Stocke :
|
||||
* - Les jours de la semaine disponibles (intervenant, traducteur)
|
||||
* - Les plages horaires disponibles (intervenant)
|
||||
* - Les indisponibilités ponctuelles (intervenant, traducteur, local)
|
||||
* - Les traducteurs disponibles par langue (preset pour optimisation)
|
||||
*
|
||||
* @param int $event_id ID de l'événement
|
||||
* @param int $intervenant_id ID de l'intervenant
|
||||
* @param array|null $plages_horaires Plages horaires spécifiques (optionnel, pour les permanences)
|
||||
* @param int|null $traducteur_id ID du traducteur (optionnel)
|
||||
* @param int|null $local_id ID du local (optionnel)
|
||||
* @param string|null $langue_slug Slug de la langue de l'événement (pour précalculer traducteurs disponibles)
|
||||
* @param string|null $date_rdv Date de l'événement (pour précalculer traducteurs disponibles)
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
public static function create_from_intervenant($event_id, $intervenant_id, $plages_horaires = null, $traducteur_id = null, $local_id = null, $langue_slug = null, $date_rdv = null) {
|
||||
global $wpdb;
|
||||
|
||||
$intervenant = CRVI_Intervenant_Model::load($intervenant_id);
|
||||
if (!$intervenant) {
|
||||
return new \WP_Error('intervenant_not_found', 'Intervenant introuvable', ['status' => 404]);
|
||||
}
|
||||
|
||||
// Extraire les jours de la semaine disponibles de l'intervenant (lundi, mardi, etc. - PAS des dates)
|
||||
$jours_disponibles = get_field('jours_de_disponibilite', 'user_' . $intervenant_id);
|
||||
$jours_permis = is_array($jours_disponibles) ? json_encode($jours_disponibles) : json_encode([]);
|
||||
|
||||
// Extraire les indisponibilités ponctuelles de l'intervenant
|
||||
$indisponibilites_acf = get_field('indisponibilitee_ponctuelle', 'user_' . $intervenant_id);
|
||||
$indisponibilites = is_array($indisponibilites_acf) ? json_encode($indisponibilites_acf) : json_encode([]);
|
||||
|
||||
// Extraire les départements et types d'intervention
|
||||
$departements_autories = !empty($intervenant->departements_ids)
|
||||
? json_encode($intervenant->departements_ids)
|
||||
: json_encode([]);
|
||||
|
||||
$types_intervention_autories = !empty($intervenant->types_intervention_ids)
|
||||
? json_encode($intervenant->types_intervention_ids)
|
||||
: json_encode([]);
|
||||
|
||||
// Plages horaires : si fournies, les utiliser, sinon null
|
||||
$plages_horaires_json = null;
|
||||
if ($plages_horaires !== null && is_array($plages_horaires)) {
|
||||
$plages_horaires_json = json_encode($plages_horaires);
|
||||
}
|
||||
|
||||
// Traducteur : jours et indisponibilités (si présent)
|
||||
$traducteur_jours_permis = null;
|
||||
$traducteur_indisponibilites = null;
|
||||
if ($traducteur_id) {
|
||||
$traducteur = CRVI_Traducteur_Model::load($traducteur_id);
|
||||
if ($traducteur) {
|
||||
// Jours de disponibilité du traducteur
|
||||
$traducteur_jours = get_field('jours_de_disponibilite', $traducteur_id);
|
||||
$traducteur_jours_permis = is_array($traducteur_jours) ? json_encode($traducteur_jours) : json_encode([]);
|
||||
|
||||
// Indisponibilités ponctuelles du traducteur
|
||||
$traducteur_indispo_acf = get_field('indisponibilitee_ponctuelle', $traducteur_id);
|
||||
$traducteur_indisponibilites = is_array($traducteur_indispo_acf) ? json_encode($traducteur_indispo_acf) : json_encode([]);
|
||||
}
|
||||
}
|
||||
|
||||
// Local : indisponibilités (si présent)
|
||||
$local_indisponibilites = null;
|
||||
if ($local_id) {
|
||||
$local = CRVI_Local_Model::load($local_id);
|
||||
if ($local) {
|
||||
// Indisponibilités ponctuelles du local (si le champ existe)
|
||||
$local_indispo_acf = get_field('indisponibilitee_ponctuelle', $local_id);
|
||||
$local_indisponibilites = is_array($local_indispo_acf) ? json_encode($local_indispo_acf) : json_encode([]);
|
||||
}
|
||||
}
|
||||
|
||||
// Précalculer les traducteurs disponibles par langue (preset pour optimisation)
|
||||
$traducteurs_disponibles_par_langue = null;
|
||||
if ($langue_slug && $date_rdv) {
|
||||
$traducteurs_disponibles_par_langue = self::precalculate_traducteurs_par_langue($date_rdv, $langue_slug);
|
||||
}
|
||||
|
||||
// Créer l'entrée de permission
|
||||
$table_name = $wpdb->prefix . 'crvi_event_permission';
|
||||
|
||||
$result = $wpdb->insert($table_name, [
|
||||
'event_id' => $event_id,
|
||||
'jours_permis' => $jours_permis,
|
||||
'plages_horaires' => $plages_horaires_json,
|
||||
'indisponibilites' => $indisponibilites,
|
||||
'traducteur_jours_permis' => $traducteur_jours_permis,
|
||||
'traducteur_indisponibilites' => $traducteur_indisponibilites,
|
||||
'local_indisponibilites' => $local_indisponibilites,
|
||||
'departements_autories' => $departements_autories,
|
||||
'types_intervention_autories' => $types_intervention_autories,
|
||||
'traducteurs_disponibles_par_langue' => $traducteurs_disponibles_par_langue,
|
||||
'date_creation' => current_time('mysql'),
|
||||
'cree_par' => get_current_user_id(),
|
||||
], [
|
||||
'%d', // event_id
|
||||
'%s', // jours_permis
|
||||
'%s', // plages_horaires
|
||||
'%s', // indisponibilites
|
||||
'%s', // traducteur_jours_permis
|
||||
'%s', // traducteur_indisponibilites
|
||||
'%s', // local_indisponibilites
|
||||
'%s', // departements_autories
|
||||
'%s', // types_intervention_autories
|
||||
'%s', // traducteurs_disponibles_par_langue
|
||||
'%s', // date_creation
|
||||
'%d', // cree_par
|
||||
]);
|
||||
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Précalcule les IDs des traducteurs disponibles par langue pour une date donnée
|
||||
*
|
||||
* Retourne un tableau JSON avec les IDs des traducteurs disponibles pour chaque langue :
|
||||
* {"fr": [1,2,3], "en": [4,5], "es": [2,6]}
|
||||
*
|
||||
* @param string $date_rdv Date au format Y-m-d
|
||||
* @param string|null $langue_slug Slug de la langue principale (si null, calcule pour toutes les langues)
|
||||
* @return string|null JSON string ou null
|
||||
*/
|
||||
private static function precalculate_traducteurs_par_langue($date_rdv, $langue_slug = null) {
|
||||
// Récupérer toutes les langues ou seulement celle demandée
|
||||
$langues_to_check = [];
|
||||
if ($langue_slug) {
|
||||
$term = get_term_by('slug', $langue_slug, 'langue');
|
||||
if ($term) {
|
||||
$langues_to_check[] = [
|
||||
'slug' => $langue_slug,
|
||||
'term_id' => $term->term_id,
|
||||
];
|
||||
}
|
||||
} else {
|
||||
// Récupérer toutes les langues
|
||||
$terms = get_terms([
|
||||
'taxonomy' => 'langue',
|
||||
'hide_empty' => false,
|
||||
]);
|
||||
foreach ($terms as $term) {
|
||||
$langues_to_check[] = [
|
||||
'slug' => $term->slug,
|
||||
'term_id' => $term->term_id,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($langues_to_check)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Pour chaque langue, récupérer les traducteurs disponibles
|
||||
$result = [];
|
||||
foreach ($langues_to_check as $langue_info) {
|
||||
$traducteurs_disponibles = CRVI_Traducteur_Model::filtrer_disponibles(
|
||||
$date_rdv,
|
||||
$langue_info['slug']
|
||||
);
|
||||
|
||||
// Extraire les IDs des traducteurs disponibles
|
||||
$ids = [];
|
||||
foreach ($traducteurs_disponibles as $traducteur) {
|
||||
if (isset($traducteur->id)) {
|
||||
$ids[] = (int)$traducteur->id;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($ids)) {
|
||||
$result[$langue_info['slug']] = $ids;
|
||||
}
|
||||
}
|
||||
|
||||
return !empty($result) ? json_encode($result) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée les disponibilités pour une permanence à partir des jours et heures sélectionnés dans le formulaire
|
||||
*
|
||||
* Stocke directement les jours et plages horaires sélectionnés dans le formulaire de création de permanences,
|
||||
* sans charger les disponibilités générales de l'intervenant.
|
||||
*
|
||||
* @param int $event_id ID de l'événement (permanence)
|
||||
* @param int $intervenant_id ID de l'intervenant
|
||||
* @param array $jours_permis Jours de la semaine sélectionnés (ex: ["lundi", "mardi"])
|
||||
* @param array $plages_horaires Plages horaires sélectionnées (ex: [{"debut":"11:00","fin":"15:00"}])
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
public static function create_for_permanence($event_id, $intervenant_id, $jours_permis = [], $plages_horaires = []) {
|
||||
global $wpdb;
|
||||
|
||||
// Vérifier que l'intervenant existe
|
||||
$intervenant = CRVI_Intervenant_Model::load($intervenant_id);
|
||||
if (!$intervenant) {
|
||||
return new \WP_Error('intervenant_not_found', 'Intervenant introuvable', ['status' => 404]);
|
||||
}
|
||||
|
||||
// Convertir les jours et plages horaires en JSON
|
||||
$jours_permis_json = is_array($jours_permis) ? json_encode($jours_permis) : json_encode([]);
|
||||
$plages_horaires_json = is_array($plages_horaires) ? json_encode($plages_horaires) : json_encode([]);
|
||||
|
||||
// Charger les indisponibilités ponctuelles de l'intervenant (pour les vérifications)
|
||||
$indisponibilites_acf = get_field('indisponibilitee_ponctuelle', 'user_' . $intervenant_id);
|
||||
$indisponibilites = is_array($indisponibilites_acf) ? json_encode($indisponibilites_acf) : json_encode([]);
|
||||
|
||||
// Extraire les départements et types d'intervention
|
||||
$departements_autories = !empty($intervenant->departements_ids)
|
||||
? json_encode($intervenant->departements_ids)
|
||||
: json_encode([]);
|
||||
|
||||
$types_intervention_autories = !empty($intervenant->types_intervention_ids)
|
||||
? json_encode($intervenant->types_intervention_ids)
|
||||
: json_encode([]);
|
||||
|
||||
// Créer l'entrée de permission avec les jours et heures sélectionnés
|
||||
$table_name = $wpdb->prefix . 'crvi_event_permission';
|
||||
|
||||
$result = $wpdb->insert($table_name, [
|
||||
'event_id' => $event_id,
|
||||
'jours_permis' => $jours_permis_json,
|
||||
'plages_horaires' => $plages_horaires_json,
|
||||
'indisponibilites' => $indisponibilites,
|
||||
'traducteur_jours_permis' => null,
|
||||
'traducteur_indisponibilites' => null,
|
||||
'local_indisponibilites' => null,
|
||||
'departements_autories' => $departements_autories,
|
||||
'types_intervention_autories' => $types_intervention_autories,
|
||||
'date_creation' => current_time('mysql'),
|
||||
'cree_par' => get_current_user_id(),
|
||||
], [
|
||||
'%d', // event_id
|
||||
'%s', // jours_permis
|
||||
'%s', // plages_horaires
|
||||
'%s', // indisponibilites
|
||||
'%s', // traducteur_jours_permis
|
||||
'%s', // traducteur_indisponibilites
|
||||
'%s', // local_indisponibilites
|
||||
'%s', // departements_autories
|
||||
'%s', // types_intervention_autories
|
||||
'%s', // date_creation
|
||||
'%d', // cree_par
|
||||
]);
|
||||
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie rapidement si une date/heure est disponible selon les disponibilités stockées
|
||||
*
|
||||
* Vérifie :
|
||||
* - Si le jour de la semaine de la date correspond aux jours disponibles
|
||||
* - Si l'heure est dans les plages horaires autorisées
|
||||
* - Si la date n'est pas dans une période d'indisponibilité
|
||||
*
|
||||
* OPTIMISÉ : Une seule requête SQL, pas de chargement de modèle Intervenant
|
||||
*
|
||||
* @param int $event_id ID de l'événement
|
||||
* @param string $date Date au format Y-m-d
|
||||
* @param string $heure Heure au format H:i (optionnel, pour vérifier les plages horaires)
|
||||
* @return bool|null true=disponible, false=non disponible, null=pas de disponibilité définie (fallback requis)
|
||||
*/
|
||||
public static function is_date_heure_authorized($event_id, $date, $heure = null) {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'crvi_event_permission';
|
||||
|
||||
// Requête optimisée : une seule requête SQL pour toutes les disponibilités
|
||||
// Récupère intervenant, traducteur et local en une seule fois (si présents dans la table)
|
||||
$permission = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT jours_permis, plages_horaires, indisponibilites,
|
||||
traducteur_jours_permis, traducteur_indisponibilites,
|
||||
local_indisponibilites
|
||||
FROM $table_name
|
||||
WHERE event_id = %d",
|
||||
$event_id
|
||||
), ARRAY_A);
|
||||
|
||||
if (!$permission) {
|
||||
// Pas de permission définie, fallback vers l'intervenant
|
||||
return null;
|
||||
}
|
||||
|
||||
// Vérifier le jour de la semaine (lundi, mardi, etc. - pas une date spécifique)
|
||||
$timestamp = strtotime($date);
|
||||
if ($timestamp === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$jour_semaine = (int)date('w', $timestamp); // 0=dimanche, 1=lundi, etc.
|
||||
|
||||
// Mapping jours français vers numériques (jours de la semaine)
|
||||
$jours_mapping = [
|
||||
'dimanche' => 0,
|
||||
'lundi' => 1,
|
||||
'mardi' => 2,
|
||||
'mercredi' => 3,
|
||||
'jeudi' => 4,
|
||||
'vendredi' => 5,
|
||||
'samedi' => 6,
|
||||
];
|
||||
|
||||
// Vérifier si ce jour de la semaine est disponible
|
||||
$jours_permis_json = $permission['jours_permis'] ?? '[]';
|
||||
$jours_permis = json_decode($jours_permis_json, true) ?? [];
|
||||
|
||||
if (!empty($jours_permis)) {
|
||||
$jours_permis_numeriques = [];
|
||||
foreach ($jours_permis as $jour) {
|
||||
$jour_lower = strtolower($jour);
|
||||
if (isset($jours_mapping[$jour_lower])) {
|
||||
$jours_permis_numeriques[] = $jours_mapping[$jour_lower];
|
||||
}
|
||||
}
|
||||
|
||||
// Vérifier si le jour de la semaine de la date est dans les jours disponibles
|
||||
if (!in_array($jour_semaine, $jours_permis_numeriques, true)) {
|
||||
return false; // Ce jour de la semaine n'est pas disponible
|
||||
}
|
||||
}
|
||||
|
||||
// Vérifier les indisponibilités ponctuelles
|
||||
$indisponibilites_json = $permission['indisponibilites'] ?? '[]';
|
||||
$indisponibilites = json_decode($indisponibilites_json, true) ?? [];
|
||||
|
||||
foreach ($indisponibilites as $indispo) {
|
||||
$debut_str = $indispo['debut'] ?? $indispo['date_debut'] ?? '';
|
||||
$fin_str = $indispo['fin'] ?? $indispo['date_fin'] ?? '';
|
||||
|
||||
if (empty($debut_str) || empty($fin_str)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Convertir les dates ACF (format d/m/Y) en timestamp
|
||||
$debut_timestamp = self::parse_acf_date($debut_str);
|
||||
$fin_timestamp = self::parse_acf_date($fin_str);
|
||||
|
||||
if ($debut_timestamp && $fin_timestamp && $timestamp >= $debut_timestamp && $timestamp <= $fin_timestamp) {
|
||||
return false; // Date dans une période d'indisponibilité
|
||||
}
|
||||
}
|
||||
|
||||
// Vérifier les plages horaires si une heure est fournie
|
||||
if ($heure !== null && !empty($permission['plages_horaires'])) {
|
||||
$plages_json = $permission['plages_horaires'];
|
||||
$plages = json_decode($plages_json, true) ?? [];
|
||||
|
||||
if (!empty($plages)) {
|
||||
$heure_timestamp = strtotime($heure);
|
||||
$heure_autorisee = false;
|
||||
|
||||
foreach ($plages as $plage) {
|
||||
$debut_plage = strtotime($plage['debut'] ?? '');
|
||||
$fin_plage = strtotime($plage['fin'] ?? '');
|
||||
|
||||
if ($debut_plage && $fin_plage && $heure_timestamp >= $debut_plage && $heure_timestamp <= $fin_plage) {
|
||||
$heure_autorisee = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$heure_autorisee) {
|
||||
return false; // Heure non dans les plages autorisées
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Vérifier les disponibilités du traducteur (si présent dans les permissions stockées)
|
||||
// OPTIMISATION : Si les données sont dans la table, on les utilise, sinon on laisse fallback
|
||||
if (!empty($permission['traducteur_jours_permis']) || !empty($permission['traducteur_indisponibilites'])) {
|
||||
// Vérifier le jour de la semaine pour le traducteur
|
||||
$traducteur_jours_json = $permission['traducteur_jours_permis'] ?? '[]';
|
||||
$traducteur_jours = json_decode($traducteur_jours_json, true) ?? [];
|
||||
|
||||
if (!empty($traducteur_jours)) {
|
||||
$traducteur_jours_numeriques = [];
|
||||
foreach ($traducteur_jours as $jour) {
|
||||
$jour_lower = strtolower($jour);
|
||||
if (isset($jours_mapping[$jour_lower])) {
|
||||
$traducteur_jours_numeriques[] = $jours_mapping[$jour_lower];
|
||||
}
|
||||
}
|
||||
|
||||
if (!in_array($jour_semaine, $traducteur_jours_numeriques, true)) {
|
||||
return false; // Ce jour de la semaine n'est pas disponible pour le traducteur
|
||||
}
|
||||
}
|
||||
|
||||
// Vérifier les indisponibilités ponctuelles du traducteur
|
||||
$traducteur_indispo_json = $permission['traducteur_indisponibilites'] ?? '[]';
|
||||
$traducteur_indispo = json_decode($traducteur_indispo_json, true) ?? [];
|
||||
|
||||
foreach ($traducteur_indispo as $indispo) {
|
||||
$debut_str = $indispo['debut'] ?? $indispo['date_debut'] ?? '';
|
||||
$fin_str = $indispo['fin'] ?? $indispo['date_fin'] ?? '';
|
||||
|
||||
if (empty($debut_str) || empty($fin_str)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$debut_timestamp = self::parse_acf_date($debut_str);
|
||||
$fin_timestamp = self::parse_acf_date($fin_str);
|
||||
|
||||
if ($debut_timestamp && $fin_timestamp && $timestamp >= $debut_timestamp && $timestamp <= $fin_timestamp) {
|
||||
return false; // Date dans une période d'indisponibilité du traducteur
|
||||
}
|
||||
}
|
||||
}
|
||||
// Si traducteur pas dans la table, on laisse le fallback dans update_event() le gérer
|
||||
|
||||
// Vérifier les disponibilités du local (si présent dans les permissions stockées)
|
||||
// OPTIMISATION : Si les données sont dans la table, on les utilise, sinon on laisse fallback
|
||||
if (!empty($permission['local_indisponibilites'])) {
|
||||
$local_indispo_json = $permission['local_indisponibilites'] ?? '[]';
|
||||
$local_indispo = json_decode($local_indispo_json, true) ?? [];
|
||||
|
||||
foreach ($local_indispo as $indispo) {
|
||||
$debut_str = $indispo['debut'] ?? $indispo['date_debut'] ?? '';
|
||||
$fin_str = $indispo['fin'] ?? $indispo['date_fin'] ?? '';
|
||||
|
||||
if (empty($debut_str) || empty($fin_str)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$debut_timestamp = self::parse_acf_date($debut_str);
|
||||
$fin_timestamp = self::parse_acf_date($fin_str);
|
||||
|
||||
if ($debut_timestamp && $fin_timestamp && $timestamp >= $debut_timestamp && $timestamp <= $fin_timestamp) {
|
||||
return false; // Date dans une période d'indisponibilité du local
|
||||
}
|
||||
}
|
||||
}
|
||||
// Si local pas dans la table, on laisse le fallback dans update_event() le gérer
|
||||
|
||||
return true; // Autorisé (intervenant, traducteur et local disponibles)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse une date ACF (format d/m/Y ou Y-m-d)
|
||||
* @param string $date_str
|
||||
* @return int|false Timestamp ou false
|
||||
*/
|
||||
private static function parse_acf_date($date_str) {
|
||||
// Essayer le format ACF (d/m/Y)
|
||||
$date = \DateTime::createFromFormat('d/m/Y', $date_str);
|
||||
if ($date) {
|
||||
return $date->getTimestamp();
|
||||
}
|
||||
|
||||
// Essayer le format standard (Y-m-d)
|
||||
$date = \DateTime::createFromFormat('Y-m-d', $date_str);
|
||||
if ($date) {
|
||||
return $date->getTimestamp();
|
||||
}
|
||||
|
||||
// Fallback : strtotime
|
||||
$timestamp = strtotime($date_str);
|
||||
return $timestamp !== false ? $timestamp : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour les disponibilités d'un événement (ex: après modification de l'intervenant, traducteur ou local)
|
||||
*
|
||||
* Met à jour :
|
||||
* - Les jours de la semaine disponibles (intervenant, traducteur)
|
||||
* - Les indisponibilités ponctuelles (intervenant, traducteur, local)
|
||||
* - Les départements/types d'intervention
|
||||
* - Les traducteurs disponibles par langue (preset)
|
||||
*
|
||||
* Les plages horaires existantes sont conservées par défaut (utile pour les permanences)
|
||||
*
|
||||
* @param int $event_id
|
||||
* @param int|null $intervenant_id Si null, utilise l'intervenant de l'événement
|
||||
* @param bool $preserve_plages_horaires Si true, conserve les plages horaires existantes (défaut: true)
|
||||
* @param int|null $traducteur_id ID du traducteur (si changé)
|
||||
* @param int|null $local_id ID du local (si changé)
|
||||
* @param string|null $langue_slug Slug de la langue de l'événement (pour précalculer traducteurs disponibles)
|
||||
* @param string|null $date_rdv Date de l'événement (pour précalculer traducteurs disponibles)
|
||||
* @return bool
|
||||
*/
|
||||
public static function update_from_intervenant($event_id, $intervenant_id = null, $preserve_plages_horaires = true, $traducteur_id = null, $local_id = null, $langue_slug = null, $date_rdv = null) {
|
||||
global $wpdb;
|
||||
|
||||
// Si pas d'intervenant fourni, le récupérer depuis l'événement
|
||||
if ($intervenant_id === null) {
|
||||
$event = CRVI_Event_Model::load($event_id);
|
||||
if (!$event || !$event->id_intervenant) {
|
||||
return false;
|
||||
}
|
||||
$intervenant_id = $event->id_intervenant;
|
||||
}
|
||||
|
||||
// Si traducteur/local non fournis, les récupérer depuis l'événement
|
||||
if ($traducteur_id === null || $local_id === null) {
|
||||
if (!$event) {
|
||||
$event = CRVI_Event_Model::load($event_id);
|
||||
}
|
||||
if ($event) {
|
||||
if ($traducteur_id === null) {
|
||||
$traducteur_id = $event->id_traducteur ?? null;
|
||||
}
|
||||
if ($local_id === null) {
|
||||
$local_id = $event->id_local ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$table_name = $wpdb->prefix . 'crvi_event_permission';
|
||||
|
||||
// Vérifier si la permission existe déjà
|
||||
$existing = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT id, plages_horaires, traducteur_jours_permis, traducteur_indisponibilites, local_indisponibilites
|
||||
FROM $table_name WHERE event_id = %d",
|
||||
$event_id
|
||||
), ARRAY_A);
|
||||
|
||||
$intervenant = CRVI_Intervenant_Model::load($intervenant_id);
|
||||
if (!$intervenant) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Récupérer les nouvelles données depuis l'intervenant
|
||||
$jours_disponibles = get_field('jours_de_disponibilite', 'user_' . $intervenant_id);
|
||||
$jours_permis = is_array($jours_disponibles) ? json_encode($jours_disponibles) : json_encode([]);
|
||||
|
||||
$indisponibilites_acf = get_field('indisponibilitee_ponctuelle', 'user_' . $intervenant_id);
|
||||
$indisponibilites = is_array($indisponibilites_acf) ? json_encode($indisponibilites_acf) : json_encode([]);
|
||||
|
||||
$departements_autories = !empty($intervenant->departements_ids)
|
||||
? json_encode($intervenant->departements_ids)
|
||||
: json_encode([]);
|
||||
|
||||
$types_intervention_autories = !empty($intervenant->types_intervention_ids)
|
||||
? json_encode($intervenant->types_intervention_ids)
|
||||
: json_encode([]);
|
||||
|
||||
// Récupérer les disponibilités du traducteur si présent
|
||||
$traducteur_jours_permis = null;
|
||||
$traducteur_indisponibilites = null;
|
||||
if ($traducteur_id) {
|
||||
$traducteur = CRVI_Traducteur_Model::load($traducteur_id);
|
||||
if ($traducteur) {
|
||||
$traducteur_jours = get_field('jours_de_disponibilite', $traducteur_id);
|
||||
$traducteur_jours_permis = is_array($traducteur_jours) ? json_encode($traducteur_jours) : json_encode([]);
|
||||
|
||||
$traducteur_indispo_acf = get_field('indisponibilitee_ponctuelle', $traducteur_id);
|
||||
$traducteur_indisponibilites = is_array($traducteur_indispo_acf) ? json_encode($traducteur_indispo_acf) : json_encode([]);
|
||||
}
|
||||
}
|
||||
|
||||
// Récupérer les indisponibilités du local si présent
|
||||
$local_indisponibilites = null;
|
||||
if ($local_id) {
|
||||
$local = CRVI_Local_Model::load($local_id);
|
||||
if ($local) {
|
||||
$local_indispo_acf = get_field('indisponibilitee_ponctuelle', $local_id);
|
||||
$local_indisponibilites = is_array($local_indispo_acf) ? json_encode($local_indispo_acf) : json_encode([]);
|
||||
}
|
||||
}
|
||||
|
||||
// Précalculer les traducteurs disponibles par langue si langue et date fournies
|
||||
$traducteurs_disponibles_par_langue = null;
|
||||
if ($langue_slug && $date_rdv) {
|
||||
$traducteurs_disponibles_par_langue = self::precalculate_traducteurs_par_langue($date_rdv, $langue_slug);
|
||||
}
|
||||
|
||||
if ($existing) {
|
||||
// Mettre à jour l'existant
|
||||
$update_data = [
|
||||
'jours_permis' => $jours_permis,
|
||||
'indisponibilites' => $indisponibilites,
|
||||
'traducteur_jours_permis' => $traducteur_jours_permis,
|
||||
'traducteur_indisponibilites' => $traducteur_indisponibilites,
|
||||
'local_indisponibilites' => $local_indisponibilites,
|
||||
'departements_autories' => $departements_autories,
|
||||
'types_intervention_autories' => $types_intervention_autories,
|
||||
'traducteurs_disponibles_par_langue' => $traducteurs_disponibles_par_langue,
|
||||
'date_modification' => current_time('mysql'),
|
||||
'modifie_par' => get_current_user_id(),
|
||||
];
|
||||
|
||||
// Conserver les plages horaires existantes si demandé
|
||||
if ($preserve_plages_horaires && !empty($existing['plages_horaires'])) {
|
||||
$update_data['plages_horaires'] = $existing['plages_horaires'];
|
||||
$formats = ['%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%d']; // Avec plages_horaires
|
||||
} else {
|
||||
$formats = ['%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%d']; // Sans plages_horaires
|
||||
}
|
||||
|
||||
$result = $wpdb->update(
|
||||
$table_name,
|
||||
$update_data,
|
||||
['event_id' => $event_id],
|
||||
$formats,
|
||||
['%d'] // Format pour WHERE
|
||||
);
|
||||
|
||||
return $result !== false;
|
||||
} else {
|
||||
// Créer une nouvelle entrée
|
||||
return self::create_from_intervenant($event_id, $intervenant_id, null, $traducteur_id, $local_id, $langue_slug, $date_rdv) !== false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprime les permissions d'un événement (lors de la suppression de l'événement)
|
||||
* @param int $event_id
|
||||
* @return bool
|
||||
*/
|
||||
public static function delete_by_event_id($event_id) {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'crvi_event_permission';
|
||||
|
||||
return $wpdb->delete($table_name, ['event_id' => $event_id], ['%d']) !== false;
|
||||
}
|
||||
}
|
||||
|
||||
210
app/models/Incident_Model.php
Normal file
210
app/models/Incident_Model.php
Normal file
@ -0,0 +1,210 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ESI_CRVI_AGENDA\models;
|
||||
|
||||
class CRVI_Incident_Model extends Main_Model {
|
||||
public $id;
|
||||
public $beneficiaire_id;
|
||||
public $event_id;
|
||||
public $resume_incident;
|
||||
public $commentaire_incident;
|
||||
public $created_at;
|
||||
|
||||
public function __construct($data = []) {
|
||||
foreach ($data as $key => $value) {
|
||||
if (property_exists($this, $key)) {
|
||||
$this->$key = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Charger un incident par ID
|
||||
* @param int $id
|
||||
* @return self|null
|
||||
*/
|
||||
public static function load($id) {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'crvi_agenda_incident';
|
||||
|
||||
$incident = $wpdb->get_row($wpdb->prepare(
|
||||
"SELECT * FROM $table_name WHERE id = %d",
|
||||
$id
|
||||
));
|
||||
|
||||
if (!$incident) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new self([
|
||||
'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,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Créer un incident
|
||||
* @param array $data
|
||||
* @return int|WP_Error ID de l'incident créé ou erreur
|
||||
*/
|
||||
public static function create(array $data) {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'crvi_agenda_incident';
|
||||
|
||||
// Validation des champs requis
|
||||
if (empty($data['event_id'])) {
|
||||
return new \WP_Error('validation_error', 'L\'ID de l\'événement est requis', ['status' => 400]);
|
||||
}
|
||||
|
||||
if (empty($data['beneficiaire_id'])) {
|
||||
return new \WP_Error('validation_error', 'L\'ID du bénéficiaire est requis', ['status' => 400]);
|
||||
}
|
||||
|
||||
if (empty($data['resume_incident'])) {
|
||||
return new \WP_Error('validation_error', 'Le résumé de l\'incident est requis', ['status' => 400]);
|
||||
}
|
||||
|
||||
// Préparer les données pour l'insertion
|
||||
$insert_data = [
|
||||
'beneficiaire_id' => (int) $data['beneficiaire_id'],
|
||||
'event_id' => (int) $data['event_id'],
|
||||
'resume_incident' => sanitize_text_field($data['resume_incident']),
|
||||
'commentaire_incident' => isset($data['commentaire_incident']) ? sanitize_textarea_field($data['commentaire_incident']) : null,
|
||||
];
|
||||
|
||||
// Insérer en base
|
||||
$result = $wpdb->insert(
|
||||
$table_name,
|
||||
$insert_data,
|
||||
[
|
||||
'%d', // beneficiaire_id
|
||||
'%d', // event_id
|
||||
'%s', // resume_incident
|
||||
'%s', // commentaire_incident
|
||||
]
|
||||
);
|
||||
|
||||
if ($result === false) {
|
||||
return new \WP_Error('database_error', 'Erreur lors de l\'insertion de l\'incident en base de données', ['status' => 500]);
|
||||
}
|
||||
|
||||
return $wpdb->insert_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupérer tous les incidents avec filtres optionnels
|
||||
* @param array $filters Filtres optionnels (event_id, beneficiaire_id)
|
||||
* @return array
|
||||
*/
|
||||
public static function get_all($filters = []) {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'crvi_agenda_incident';
|
||||
|
||||
$where_clauses = [];
|
||||
$where_values = [];
|
||||
|
||||
// Construire les clauses WHERE selon les filtres
|
||||
if (!empty($filters['event_id'])) {
|
||||
$where_clauses[] = 'event_id = %d';
|
||||
$where_values[] = (int) $filters['event_id'];
|
||||
}
|
||||
|
||||
if (!empty($filters['beneficiaire_id'])) {
|
||||
$where_clauses[] = 'beneficiaire_id = %d';
|
||||
$where_values[] = (int) $filters['beneficiaire_id'];
|
||||
}
|
||||
|
||||
// Construire la requête
|
||||
$query = "SELECT * FROM $table_name";
|
||||
if (!empty($where_clauses)) {
|
||||
$query .= ' WHERE ' . implode(' AND ', $where_clauses);
|
||||
}
|
||||
$query .= ' ORDER BY id DESC';
|
||||
|
||||
// Exécuter la requête
|
||||
if (!empty($where_values)) {
|
||||
$incidents = $wpdb->get_results($wpdb->prepare($query, $where_values));
|
||||
} else {
|
||||
$incidents = $wpdb->get_results($query);
|
||||
}
|
||||
|
||||
$result = [];
|
||||
foreach ($incidents as $incident) {
|
||||
$result[] = new self([
|
||||
'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 $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupérer tous les incidents pour un événement
|
||||
* @param int $event_id
|
||||
* @return array
|
||||
*/
|
||||
public static function get_by_event($event_id) {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'crvi_agenda_incident';
|
||||
|
||||
$incidents = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT * FROM $table_name WHERE event_id = %d ORDER BY id DESC",
|
||||
$event_id
|
||||
));
|
||||
|
||||
$result = [];
|
||||
foreach ($incidents as $incident) {
|
||||
$result[] = new self([
|
||||
'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 $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupérer tous les incidents pour un bénéficiaire
|
||||
* @param int $beneficiaire_id
|
||||
* @return array
|
||||
*/
|
||||
public static function get_by_beneficiaire($beneficiaire_id) {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'crvi_agenda_incident';
|
||||
|
||||
$incidents = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT * FROM $table_name WHERE beneficiaire_id = %d ORDER BY id DESC",
|
||||
$beneficiaire_id
|
||||
));
|
||||
|
||||
$result = [];
|
||||
foreach ($incidents as $incident) {
|
||||
$result[] = new self([
|
||||
'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 $result;
|
||||
}
|
||||
}
|
||||
|
||||
542
app/models/Intervenant_Model.php
Normal file
542
app/models/Intervenant_Model.php
Normal file
@ -0,0 +1,542 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
namespace ESI_CRVI_AGENDA\models;
|
||||
|
||||
use WP_Error;
|
||||
|
||||
class CRVI_Intervenant_Model extends Main_Model {
|
||||
public $id;
|
||||
public $nom;
|
||||
public $prenom;
|
||||
public $email;
|
||||
public $departements_ids; // array d'IDs CPT departement
|
||||
public $types_intervention_ids; // array d'IDs CPT type_intervention
|
||||
public $jours_de_disponibilite;
|
||||
public $indisponibilites;
|
||||
public $commentaires;
|
||||
|
||||
/**
|
||||
* Schéma des champs ACF : nom => type (ex : group, repeater, taxonomy, text...)
|
||||
*/
|
||||
public static $acf_schema = [
|
||||
'telephone' => 'text',
|
||||
'departements' => 'taxonomy',
|
||||
'specialisations' => 'taxonomy',
|
||||
'commentaires' => 'textarea',
|
||||
'jours_de_disponibilite' => 'checkbox',
|
||||
'indisponibilitee_ponctuelle' => 'repeater',
|
||||
];
|
||||
|
||||
public function __construct($data = []) {
|
||||
foreach ($data as $key => $value) {
|
||||
if (property_exists($this, $key)) {
|
||||
$this->$key = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function load($user_id, $fields = []) {
|
||||
$user = get_user_by('id', $user_id);
|
||||
if (!$user || !in_array('intervenant', (array) $user->roles, true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Fonction helper pour récupérer les champs ACF ou meta
|
||||
$get_field_value = function($user_id, $field_name) {
|
||||
if (function_exists('get_field')) {
|
||||
return get_field($field_name, 'user_' . $user_id) ?: [];
|
||||
} else {
|
||||
return get_user_meta($user_id, $field_name, true) ?: [];
|
||||
}
|
||||
};
|
||||
|
||||
// Si des champs spécifiques sont demandés, ne charger que ceux-ci
|
||||
if (!empty($fields)) {
|
||||
$data = [];
|
||||
foreach ($fields as $field) {
|
||||
if ($field === 'id') {
|
||||
$data['id'] = $user->ID;
|
||||
} elseif ($field === 'nom') {
|
||||
$data['nom'] = $user->last_name;
|
||||
} elseif ($field === 'prenom') {
|
||||
$data['prenom'] = $user->first_name;
|
||||
} elseif ($field === 'email') {
|
||||
$data['email'] = $user->user_email;
|
||||
} elseif (property_exists(self::class, $field)) {
|
||||
$data[$field] = $get_field_value($user->ID, $field);
|
||||
}
|
||||
}
|
||||
return new self($data);
|
||||
}
|
||||
|
||||
// Sinon, charger tous les champs par défaut
|
||||
return new self([
|
||||
'id' => $user->ID,
|
||||
'nom' => $user->last_name,
|
||||
'prenom' => $user->first_name,
|
||||
'email' => $user->user_email,
|
||||
'departements_ids' => $get_field_value($user->ID, 'departements_ids'),
|
||||
'types_intervention_ids' => $get_field_value($user->ID, 'types_intervention_ids'),
|
||||
'jours_de_disponibilite' => $get_field_value($user->ID, 'jours_de_disponibilite'),
|
||||
'indisponibilites' => $get_field_value($user->ID, 'indisponibilites'),
|
||||
'commentaires' => $get_field_value($user->ID, 'commentaires'),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function get_intervenants($filters = [],$simple_list = false) {
|
||||
$users = get_users([
|
||||
'role' => 'intervenant',
|
||||
'number' => -1,
|
||||
'meta_query' => $filters,
|
||||
]);
|
||||
|
||||
if ($simple_list) {
|
||||
$users = array_map(function($user) {
|
||||
return [
|
||||
'id' => $user->ID,
|
||||
'nom' => $user->last_name,
|
||||
'prenom' => $user->first_name,
|
||||
];
|
||||
}, $users);
|
||||
}
|
||||
return $users;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne les intervenants disponibles à une date donnée, avec filtres optionnels.
|
||||
* @param string $date au format Y-m-d
|
||||
* @param int|null $departement_id (ID CPT departement)
|
||||
* @param int|null $type_intervention_id (ID CPT type_intervention)
|
||||
* @param int|null $event_id ID de l'événement en cours d'édition (pour inclure l'intervenant actuel)
|
||||
* @return array Liste des intervenants disponibles (CRVI_Intervenant_Model)
|
||||
*/
|
||||
public static function filtrer_disponibles($date, $departement_id = null, $type_intervention_id = null, $event_id = null) {
|
||||
if (empty($date)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$meta_query = [
|
||||
[
|
||||
'key' => 'jours_de_disponibilite',
|
||||
'compare' => 'EXISTS',
|
||||
],
|
||||
];
|
||||
if ($departement_id) {
|
||||
$meta_query[] = [
|
||||
'key' => 'departements_ids',
|
||||
'value' => $departement_id,
|
||||
'compare' => 'LIKE',
|
||||
];
|
||||
}
|
||||
if ($type_intervention_id) {
|
||||
$meta_query[] = [
|
||||
'key' => 'types_intervention_ids',
|
||||
'value' => $type_intervention_id,
|
||||
'compare' => 'LIKE',
|
||||
];
|
||||
}
|
||||
$args = [
|
||||
'role' => 'intervenant',
|
||||
'number' => -1,
|
||||
'meta_query' => $meta_query,
|
||||
];
|
||||
$users = get_users($args);
|
||||
|
||||
$disponibles = [];
|
||||
foreach ($users as $user) {
|
||||
$intervenant = self::load($user->ID);
|
||||
if ($intervenant) {
|
||||
$is_disponible = $intervenant->is_disponible($user->ID ,$date, $departement_id, $type_intervention_id);
|
||||
|
||||
// Vérifier les conflits d'événements existants
|
||||
if ($is_disponible) {
|
||||
$conflits = self::verifier_conflits_intervenant($intervenant->id, $date, $event_id);
|
||||
if (!empty($conflits)) {
|
||||
$is_disponible = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Si c'est l'événement en cours d'édition, inclure l'intervenant même s'il est "pris"
|
||||
if ($event_id && self::is_intervenant_of_event($intervenant->id, $event_id)) {
|
||||
$is_disponible = true;
|
||||
}
|
||||
|
||||
if ($is_disponible) {
|
||||
$disponibles[] = $intervenant;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// error_log("CRVI_Intervenant_Model::filtrer_disponibles - Intervenants disponibles: " . count($disponibles));
|
||||
return $disponibles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si un intervenant est associé à un événement donné
|
||||
* @param int $intervenant_id
|
||||
* @param int $event_id
|
||||
* @return bool
|
||||
*/
|
||||
private static function is_intervenant_of_event($intervenant_id, $event_id) {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'crvi_agenda';
|
||||
$result = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT id FROM $table_name WHERE id = %d AND id_intervenant = %d",
|
||||
$event_id,
|
||||
$intervenant_id
|
||||
));
|
||||
return !empty($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si cet intervenant est disponible à une date donnée, avec filtres optionnels.
|
||||
* @param string $date au format Y-m-d
|
||||
* @param int|null $departement_id
|
||||
* @param int|null $type_intervention_id
|
||||
* @return bool
|
||||
*/
|
||||
public function is_disponible($intervenant_id, $date, $departement_id = null, $type_intervention_id = null) {
|
||||
// Vérifier que la date n'est pas null ou vide
|
||||
if (empty($date)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$timestamp = strtotime($date);
|
||||
if ($timestamp === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$jour = strtolower(date('l', $timestamp));
|
||||
$jours_fr = [
|
||||
'monday' => 'lundi',
|
||||
'tuesday' => 'mardi',
|
||||
'wednesday' => 'mercredi',
|
||||
'thursday' => 'jeudi',
|
||||
'friday' => 'vendredi',
|
||||
'saturday' => 'samedi',
|
||||
'sunday' => 'dimanche',
|
||||
];
|
||||
$jour_semaine = $jours_fr[$jour] ?? '';
|
||||
|
||||
$jours_disponibles = get_field('jours_de_disponibilite', 'user_' . $intervenant_id);
|
||||
if (!is_array($jours_disponibles) || !in_array($jour_semaine, $jours_disponibles, true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$indisponibilites = get_field('indisponibilitee_ponctuelle', 'user_' . $intervenant_id);
|
||||
if (is_array($indisponibilites)) {
|
||||
foreach ($indisponibilites 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) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($departement_id && (!is_array($this->departements_ids) || !in_array($departement_id, $this->departements_ids))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($type_intervention_id && (!is_array($this->types_intervention_ids) || !in_array($type_intervention_id, $this->types_intervention_ids))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function get_relations() {
|
||||
// À adapter selon la nouvelle structure (ex : récupérer les rendez-vous liés via user_id)
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Créer un intervenant (user avec rôle intervenant).
|
||||
* @param array $data
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public static function create(array $data, bool $as_rest = false) {
|
||||
// Suppression du debug
|
||||
// echo '<pre>';
|
||||
// print_r($data);
|
||||
// echo '</pre>';
|
||||
// die();
|
||||
|
||||
if (!is_user_logged_in() || !get_current_user_id()) {
|
||||
if ($as_rest) {
|
||||
return [
|
||||
'success' => false,
|
||||
'code' => 401,
|
||||
'message' => 'Authentification requise',
|
||||
'data' => null
|
||||
];
|
||||
}
|
||||
return new WP_Error('auth_required', 'Authentification requise', ['status' => 401]);
|
||||
}
|
||||
|
||||
if (!current_user_can('edit_users')) {
|
||||
if ($as_rest) {
|
||||
return [
|
||||
'success' => false,
|
||||
'code' => 403,
|
||||
'message' => 'Non autorisé',
|
||||
'data' => null
|
||||
];
|
||||
}
|
||||
return new WP_Error('not_allowed', 'Non autorisé', ['status' => 403]);
|
||||
}
|
||||
|
||||
if (empty($data['nom']) || empty($data['prenom']) || empty($data['email'])) {
|
||||
if ($as_rest) {
|
||||
return [
|
||||
'success' => false,
|
||||
'code' => 400,
|
||||
'message' => 'Champs obligatoires manquants',
|
||||
'data' => null
|
||||
];
|
||||
}
|
||||
return new WP_Error('missing_fields', 'Champs obligatoires manquants', ['status' => 400]);
|
||||
}
|
||||
|
||||
|
||||
$user = get_user_by('email', $data['email']);
|
||||
|
||||
/* echo '<pre>';
|
||||
print_r($user);
|
||||
echo '</pre>';
|
||||
die(); */
|
||||
|
||||
if ($user && in_array('intervenant', (array)$user->roles, true)) {
|
||||
// Mise à jour
|
||||
$user_id = $user->ID;
|
||||
$user_data = [
|
||||
'ID' => $user_id,
|
||||
'user_email' => $data['email'],
|
||||
'first_name' => $data['prenom'],
|
||||
'last_name' => $data['nom'],
|
||||
];
|
||||
$result = wp_update_user($user_data);
|
||||
if (is_wp_error($result)) {
|
||||
if ($as_rest) {
|
||||
return [
|
||||
'success' => false,
|
||||
'code' => 500,
|
||||
'message' => 'Erreur lors de la mise à jour',
|
||||
'data' => json_encode($result)
|
||||
];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
// Mettre à jour les champs ACF
|
||||
foreach ($data as $key => $value) {
|
||||
// Pour departements et types_intervention, on attend des IDs de posts CPT (séparés par pipe si string)
|
||||
if ($key === 'departements') {
|
||||
$ids = is_array($value) ? $value : explode('|', $value);
|
||||
$ids = array_filter(array_map('intval', $ids));
|
||||
\update_field('departements', $ids, 'user_' . $user_id);
|
||||
} else if ($key === 'types_intervention' || $key === 'specialisations') {
|
||||
$ids = is_array($value) ? $value : explode('|', $value);
|
||||
$ids = array_filter(array_map('intval', $ids));
|
||||
\update_field('specialisations', $ids, 'user_' . $user_id);
|
||||
} else if (isset(self::$acf_schema[$key])) {
|
||||
self::set_acf_field('user_' . $user_id, $key, $value, self::$acf_schema);
|
||||
} else {
|
||||
// Pour les autres champs non ACF, on ignore ou on peut loguer
|
||||
}
|
||||
}
|
||||
if ($as_rest) {
|
||||
return [
|
||||
'success' => true,
|
||||
'code' => 200,
|
||||
'message' => 'Intervenant mis à jour',
|
||||
'data' => [ 'user_id' => $user_id ]
|
||||
];
|
||||
}
|
||||
return ['user_id' => $user_id, 'updated' => true];
|
||||
} else if($user) {
|
||||
// L'utilisateur existe mais n'a pas le rôle intervenant
|
||||
if ($as_rest) {
|
||||
return [
|
||||
'success' => false,
|
||||
'code' => 409,
|
||||
'message' => 'Email déjà utilisé par un autre rôle',
|
||||
'data' => null
|
||||
];
|
||||
}
|
||||
return new WP_Error('email_exists', 'Email déjà utilisé par un autre rôle', ['status' => 409]);
|
||||
}
|
||||
|
||||
echo 'vhvh';
|
||||
die();
|
||||
// Création
|
||||
$user_id = wp_insert_user([
|
||||
'user_login' => $data['email'],
|
||||
'user_email' => $data['email'],
|
||||
'first_name' => $data['prenom'],
|
||||
'last_name' => $data['nom'],
|
||||
'role' => 'intervenant',
|
||||
'user_pass' => wp_generate_password(12, true),
|
||||
]);
|
||||
if (is_wp_error($user_id)) {
|
||||
if ($as_rest) {
|
||||
return [
|
||||
'success' => false,
|
||||
'code' => 500,
|
||||
'message' => 'Erreur lors de la création',
|
||||
'data' => null
|
||||
];
|
||||
}
|
||||
return $user_id;
|
||||
}
|
||||
// Générer un mot de passe applicatif (Application Password)
|
||||
if (!class_exists('WP_Application_Passwords') && file_exists(ABSPATH . 'wp-includes/class-wp-application-passwords.php')) {
|
||||
require_once ABSPATH . 'wp-includes/class-wp-application-passwords.php';
|
||||
}
|
||||
if (class_exists('WP_Application_Passwords')) {
|
||||
$app_pass_result = \WP_Application_Passwords::create_new_application_password($user_id, [
|
||||
'name' => 'CRVI API',
|
||||
]);
|
||||
if (is_array($app_pass_result) && isset($app_pass_result[0], $app_pass_result[1]['new_password'])) {
|
||||
$application_password = $app_pass_result[1]['new_password'];
|
||||
} else {
|
||||
$application_password = null;
|
||||
}
|
||||
} else {
|
||||
$application_password = null;
|
||||
}
|
||||
// Stocker les champs ACF
|
||||
foreach ($data as $key => $value) {
|
||||
if ($key === 'departements') {
|
||||
$ids = is_array($value) ? $value : explode('|', $value);
|
||||
$ids = array_filter(array_map('intval', $ids));
|
||||
\update_field('departements', $ids, 'user_' . $user_id);
|
||||
} else if ($key === 'types_intervention' || $key === 'specialisations') {
|
||||
$ids = is_array($value) ? $value : explode('|', $value);
|
||||
$ids = array_filter(array_map('intval', $ids));
|
||||
\update_field('specialisations', $ids, 'user_' . $user_id);
|
||||
} else if (isset(self::$acf_schema[$key])) {
|
||||
self::set_acf_field('user_' . $user_id, $key, $value, self::$acf_schema);
|
||||
} else {
|
||||
// Pour les autres champs non ACF, on ignore ou on peut loguer
|
||||
}
|
||||
}
|
||||
if ($as_rest) {
|
||||
return [
|
||||
'success' => true,
|
||||
'code' => 201,
|
||||
'message' => 'Intervenant créé',
|
||||
'data' => [ 'user_id' => $user_id, 'application_password' => $application_password ]
|
||||
];
|
||||
}
|
||||
return ['user_id' => $user_id, 'created' => true, 'application_password' => $application_password];
|
||||
}
|
||||
|
||||
/**
|
||||
* Mettre à jour un intervenant (user).
|
||||
* @param int $user_id
|
||||
* @param array $data
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
public static function update(int $user_id, array $data) {
|
||||
if (!current_user_can('edit_users')) {
|
||||
return new WP_Error('not_allowed', 'Non autorisé', ['status' => 403]);
|
||||
}
|
||||
$user = get_user_by('id', $user_id);
|
||||
if (!$user || !in_array('intervenant', (array) $user->roles, true)) {
|
||||
return new WP_Error('not_found', 'Intervenant introuvable', ['status' => 404]);
|
||||
}
|
||||
$user_data = ['ID' => $user_id];
|
||||
if (!empty($data['email'])) {
|
||||
if (email_exists($data['email']) && $data['email'] !== $user->user_email) {
|
||||
return new WP_Error('email_exists', 'Email déjà utilisé', ['status' => 409]);
|
||||
}
|
||||
$user_data['user_email'] = $data['email'];
|
||||
}
|
||||
if (!empty($data['prenom'])) {
|
||||
$user_data['first_name'] = $data['prenom'];
|
||||
}
|
||||
if (!empty($data['nom'])) {
|
||||
$user_data['last_name'] = $data['nom'];
|
||||
}
|
||||
$user_update = wp_update_user($user_data);
|
||||
if (is_wp_error($user_update)) {
|
||||
return $user_update;
|
||||
}
|
||||
// Mettre à jour les meta
|
||||
foreach (['departements_ids', 'types_intervention_ids', 'jours_de_disponibilite', 'indisponibilites', 'commentaires'] as $meta_key) {
|
||||
if (isset($data[$meta_key])) {
|
||||
\update_field($meta_key, $data[$meta_key], 'user_' . $user_id);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprimer un intervenant (user).
|
||||
* @param int $user_id
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
public function delete(int $user_id) {
|
||||
if (!current_user_can('delete_users')) {
|
||||
return new WP_Error('not_allowed', 'Non autorisé', ['status' => 403]);
|
||||
}
|
||||
$user = get_user_by('id', $user_id);
|
||||
if (!$user || !in_array('intervenant', (array) $user->roles, true)) {
|
||||
return new WP_Error('not_found', 'Intervenant introuvable', ['status' => 404]);
|
||||
}
|
||||
require_once ABSPATH . 'wp-admin/includes/user.php';
|
||||
$result = wp_delete_user($user_id);
|
||||
if (!$result) {
|
||||
return new WP_Error('delete_failed', 'Erreur lors de la suppression', ['status' => 500]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function all() {
|
||||
$users = get_users([
|
||||
'role' => 'intervenant',
|
||||
'number' => -1,
|
||||
]);
|
||||
return $users;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie s'il y a des conflits d'événements pour un intervenant donné
|
||||
* @param int $intervenant_id
|
||||
* @param string $date
|
||||
* @param int|null $event_id - ID de l'événement à exclure (pour l'édition)
|
||||
* @return array
|
||||
*/
|
||||
private static function verifier_conflits_intervenant($intervenant_id, $date, $event_id = null) {
|
||||
global $wpdb;
|
||||
|
||||
$table_events = $wpdb->prefix . 'crvi_agenda';
|
||||
|
||||
$where_conditions = ['id_intervenant = %d'];
|
||||
$where_values = [$intervenant_id];
|
||||
|
||||
// Exclure l'événement en cours d'édition si spécifié
|
||||
if ($event_id) {
|
||||
$where_conditions[] = 'id != %d';
|
||||
$where_values[] = $event_id;
|
||||
}
|
||||
|
||||
// Ajouter la condition de date
|
||||
$where_conditions[] = 'date_rdv = %s';
|
||||
$where_values[] = $date;
|
||||
|
||||
// Exclure les événements annulés ou terminés
|
||||
$where_conditions[] = 'statut NOT IN (%s, %s)';
|
||||
$where_values[] = 'annule';
|
||||
$where_values[] = 'termine';
|
||||
|
||||
$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);
|
||||
}
|
||||
}
|
||||
303
app/models/Local_Model.php
Normal file
303
app/models/Local_Model.php
Normal file
@ -0,0 +1,303 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ESI_CRVI_AGENDA\models;
|
||||
|
||||
use ESI_CRVI_AGENDA\helpers\Api_Helper;
|
||||
|
||||
class CRVI_Local_Model extends Main_Model {
|
||||
public $id;
|
||||
public $nom;
|
||||
public $type_de_local;
|
||||
public $capacite;
|
||||
public $local_bloque;
|
||||
public $commentaire;
|
||||
|
||||
/**
|
||||
* Schéma des champs ACF : nom => type (ex : group, repeater, taxonomy, text...)
|
||||
*/
|
||||
public static $acf_schema = [
|
||||
'nom' => 'text',
|
||||
'type_de_local' => 'select',
|
||||
'local_bloque' => 'checkbox',
|
||||
'commentaire' => 'textarea',
|
||||
];
|
||||
|
||||
public function __construct($data = []) {
|
||||
foreach ($data as $key => $value) {
|
||||
if (property_exists($this, $key)) {
|
||||
$this->$key = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function load($id, $fields = []) {
|
||||
// Charger depuis CPT/meta
|
||||
$local = get_post($id);
|
||||
if (!$local) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Si des champs spécifiques sont demandés, ne charger que ceux-ci
|
||||
if (!empty($fields)) {
|
||||
$data = [];
|
||||
foreach ($fields as $field) {
|
||||
if ($field === 'id') {
|
||||
$data['id'] = $local->ID;
|
||||
} elseif (property_exists(self::class, $field)) {
|
||||
$data[$field] = get_field($field, $local->ID);
|
||||
}
|
||||
}
|
||||
return new self($data);
|
||||
}
|
||||
|
||||
// Sinon, charger tous les champs par défaut
|
||||
return new self([
|
||||
'id' => $local->ID,
|
||||
'nom' => get_field('nom', $local->ID),
|
||||
'type_de_local' => get_field('type_de_local', $local->ID),
|
||||
'capacite' => get_field('capacite', $local->ID),
|
||||
'local_bloque' => get_field('local_bloque', $local->ID),
|
||||
'commentaire' => get_field('commentaire', $local->ID),
|
||||
]);
|
||||
}
|
||||
|
||||
public function save() {
|
||||
// À implémenter : sauvegarder dans CPT/meta
|
||||
return true;
|
||||
}
|
||||
|
||||
public function is_disponible($date) {
|
||||
// À implémenter : logique de disponibilité
|
||||
return true;
|
||||
}
|
||||
|
||||
public function get_relations() {
|
||||
// À implémenter : récupérer les rendez-vous associés, etc.
|
||||
return [];
|
||||
}
|
||||
|
||||
public static function get_locals($filters = [],$simple_list = false) {
|
||||
$locals = get_posts([
|
||||
'post_type' => 'local',
|
||||
'posts_per_page' => -1,
|
||||
'post_status' => 'publish',
|
||||
'meta_query' => $filters,
|
||||
]);
|
||||
if ($simple_list) {
|
||||
$locals = array_map(function($local) {
|
||||
return [
|
||||
'id' => $local->ID,
|
||||
'nom' => $local->post_title,
|
||||
];
|
||||
}, $locals);
|
||||
}
|
||||
return $locals;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne les locaux filtrés par type de rendez-vous
|
||||
* @param string $type_rdv 'individuel' ou 'groupe'
|
||||
* @return array Liste des locaux filtrés
|
||||
*/
|
||||
public static function get_locaux_par_type($type_rdv) {
|
||||
$meta_query = [];
|
||||
|
||||
// Filtrer selon le type de rendez-vous
|
||||
if ($type_rdv === 'individuel') {
|
||||
// Pour les RDV individuels : locaux individuels ou mixtes
|
||||
$meta_query[] = [
|
||||
'relation' => 'OR',
|
||||
[
|
||||
'key' => 'type_de_local',
|
||||
'value' => 'individuel',
|
||||
'compare' => '='
|
||||
],
|
||||
[
|
||||
'key' => 'type_de_local',
|
||||
'value' => 'mixte',
|
||||
'compare' => '='
|
||||
]
|
||||
];
|
||||
} elseif ($type_rdv === 'groupe') {
|
||||
// Pour les RDV de groupe : locaux de groupe ou mixtes
|
||||
$meta_query[] = [
|
||||
'relation' => 'OR',
|
||||
[
|
||||
'key' => 'type_de_local',
|
||||
'value' => 'groupe',
|
||||
'compare' => '='
|
||||
],
|
||||
[
|
||||
'key' => 'type_de_local',
|
||||
'value' => 'mixte',
|
||||
'compare' => '='
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
// Exclure les locaux bloqués
|
||||
$meta_query[] = [
|
||||
'key' => 'local_bloque',
|
||||
'value' => '1',
|
||||
'compare' => '!='
|
||||
];
|
||||
|
||||
$args = [
|
||||
'post_type' => 'local',
|
||||
'numberposts' => -1,
|
||||
'post_status' => 'publish',
|
||||
'meta_query' => $meta_query
|
||||
];
|
||||
|
||||
$locals_posts = get_posts($args);
|
||||
$locaux_filtres = [];
|
||||
|
||||
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);
|
||||
|
||||
$locaux_filtres[] = [
|
||||
'id' => $local_post->ID,
|
||||
'nom' => $local_post->post_title,
|
||||
'type_de_local' => $type_local,
|
||||
'capacite' => $capacite,
|
||||
];
|
||||
}
|
||||
|
||||
return $locaux_filtres;
|
||||
}
|
||||
|
||||
/**
|
||||
* Créer un local.
|
||||
* @param array $data
|
||||
* @return int|WP_Error
|
||||
*/
|
||||
public function create(array $data) {
|
||||
// Vérifier les permissions (désactivé temporairement pour les tests)
|
||||
// if (!is_user_logged_in() || !get_current_user_id()) {
|
||||
// return Api_Helper::json_error('Authentification requise', 401);
|
||||
// }
|
||||
// if (!current_user_can('edit_posts')) {
|
||||
// return Api_Helper::json_error('Non autorisé', 403);
|
||||
// }
|
||||
|
||||
// Vérifier que le nom est fourni
|
||||
if (empty($data['nom'])) {
|
||||
return Api_Helper::json_error('Le nom du local est obligatoire', 400);
|
||||
}
|
||||
|
||||
// Vérifier l'unicité du nom
|
||||
$existing = get_posts([
|
||||
'post_type' => 'local',
|
||||
'meta_key' => 'nom',
|
||||
'meta_value' => $data['nom'],
|
||||
'post_status'=> 'any',
|
||||
'numberposts'=> 1,
|
||||
]);
|
||||
|
||||
if ($existing) {
|
||||
return Api_Helper::json_error('Un local avec ce nom existe déjà', 409);
|
||||
}
|
||||
$post_id = wp_insert_post([
|
||||
'post_type' => 'local',
|
||||
'post_title' => $data['nom'],
|
||||
'post_status' => 'publish',
|
||||
]);
|
||||
if (is_wp_error($post_id)) {
|
||||
return $post_id;
|
||||
}
|
||||
update_post_meta($post_id, 'nom', $data['nom']);
|
||||
if (!empty($data['type_de_local'])) {
|
||||
update_post_meta($post_id, 'type_de_local', $data['type_de_local']);
|
||||
}
|
||||
if (!empty($data['capacite'])) {
|
||||
update_post_meta($post_id, 'capacite', $data['capacite']);
|
||||
}
|
||||
if (isset($data['local_bloque'])) {
|
||||
update_post_meta($post_id, 'local_bloque', $data['local_bloque']);
|
||||
}
|
||||
if (!empty($data['commentaire'])) {
|
||||
update_post_meta($post_id, 'commentaire', $data['commentaire']);
|
||||
}
|
||||
// ... autres champs
|
||||
return $post_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mettre à jour un local.
|
||||
* @param int $id
|
||||
* @param array $data
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
public function update(int $id, array $data) {
|
||||
// Vérifier les permissions (désactivé temporairement pour les tests)
|
||||
// if (!is_user_logged_in() || !get_current_user_id()) {
|
||||
// return Api_Helper::json_error('Authentification requise', 401);
|
||||
// }
|
||||
// if (!current_user_can('edit_posts')) {
|
||||
// return Api_Helper::json_error('Non autorisé', 403);
|
||||
// }
|
||||
$post = get_post($id);
|
||||
if (!$post || $post->post_type !== 'local') {
|
||||
return Api_Helper::json_error('Local introuvable', 404);
|
||||
}
|
||||
// Vérifier unicité du nom si modifié
|
||||
if (!empty($data['nom'])) {
|
||||
$existing = get_posts([
|
||||
'post_type' => 'local',
|
||||
'meta_key' => 'nom',
|
||||
'meta_value' => $data['nom'],
|
||||
'post_status'=> 'any',
|
||||
'exclude' => [$id],
|
||||
'numberposts'=> 1,
|
||||
]);
|
||||
if ($existing) {
|
||||
return Api_Helper::json_error('Un local avec ce nom existe déjà', 409);
|
||||
}
|
||||
}
|
||||
$result = wp_update_post([
|
||||
'ID' => $id,
|
||||
'post_title' => $data['nom'] ?? get_post_meta($id, 'nom', true),
|
||||
], true);
|
||||
if (is_wp_error($result)) {
|
||||
return $result;
|
||||
}
|
||||
foreach ($data as $key => $value) {
|
||||
update_post_meta($id, $key, $value);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprimer un local (corbeille WordPress)
|
||||
* @param int $id
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
public function delete(int $id) {
|
||||
if (!is_user_logged_in() || !get_current_user_id()) {
|
||||
return Api_Helper::json_error('Authentification requise', 401);
|
||||
}
|
||||
if (!current_user_can('delete_posts')) {
|
||||
return Api_Helper::json_error('Non autorisé', 403);
|
||||
}
|
||||
$post = get_post($id);
|
||||
if (!$post || $post->post_type !== 'local') {
|
||||
return Api_Helper::json_error('Local introuvable', 404);
|
||||
}
|
||||
$result = wp_trash_post($id);
|
||||
if (!$result) {
|
||||
return Api_Helper::json_error('Erreur lors de la suppression', 500);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function all() {
|
||||
return get_posts([
|
||||
'post_type' => 'local',
|
||||
'posts_per_page' => -1,
|
||||
'post_status' => 'publish',
|
||||
]);
|
||||
}
|
||||
}
|
||||
118
app/models/Main_Model.php
Normal file
118
app/models/Main_Model.php
Normal file
@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ESI_CRVI_AGENDA\models;
|
||||
|
||||
/**
|
||||
* Modèle générique pour la gestion des imports ACF
|
||||
*/
|
||||
class Main_Model {
|
||||
/**
|
||||
* Applique la logique d'import selon le type de champ défini dans $acf_schema
|
||||
* @param int $post_id
|
||||
* @param string $field
|
||||
* @param mixed $value
|
||||
* @param array $acf_schema
|
||||
*/
|
||||
public static function set_acf_field($post_id, $field, $value, $acf_schema) {
|
||||
if (!isset($acf_schema[$field])) {
|
||||
// Champ inconnu, fallback update_field
|
||||
update_field($field, $value, $post_id);
|
||||
return;
|
||||
}
|
||||
$type = $acf_schema[$field];
|
||||
switch ($type) {
|
||||
case 'taxonomy':
|
||||
// Gérer plusieurs valeurs séparées par |
|
||||
$terms = is_array($value) ? $value : explode('|', (string)$value);
|
||||
$term_ids = [];
|
||||
foreach ($terms as $term) {
|
||||
$term = trim($term);
|
||||
if (!$term) continue;
|
||||
$slug = sanitize_title($term);
|
||||
$taxonomy = $field; // On suppose que le nom du champ = nom de la taxonomie
|
||||
if (!taxonomy_exists($taxonomy)) {
|
||||
register_taxonomy($taxonomy, '', ['label' => ucfirst($taxonomy), 'public' => false]);
|
||||
}
|
||||
$term_obj = get_term_by('slug', $slug, $taxonomy);
|
||||
if (!$term_obj) {
|
||||
$result = wp_insert_term($term, $taxonomy, ['slug' => $slug]);
|
||||
if (!is_wp_error($result) && isset($result['term_id'])) {
|
||||
$term_ids[] = $result['term_id'];
|
||||
}
|
||||
} else {
|
||||
$term_ids[] = $term_obj->term_id;
|
||||
}
|
||||
}
|
||||
if (!empty($term_ids)) {
|
||||
update_field($field, $term_ids, $post_id);
|
||||
}
|
||||
break;
|
||||
case 'repeater':
|
||||
// Valeur attendue : string format pipe ou array d'items
|
||||
$rows = [];
|
||||
if (is_string($value)) {
|
||||
$items = explode('|', $value);
|
||||
foreach ($items as $item) {
|
||||
$row = [];
|
||||
$pairs = explode(';', $item);
|
||||
foreach ($pairs as $pair) {
|
||||
[$k, $v] = array_pad(explode(':', $pair, 2), 2, null);
|
||||
if ($k && $v !== null) {
|
||||
if (self::is_date($v)) {
|
||||
$v = \DateTime::createFromFormat('d/m/Y', $v);
|
||||
$v = $v->format('Y-m-d');
|
||||
}
|
||||
$row[trim($k)] = trim($v);
|
||||
}
|
||||
}
|
||||
if ($row) $rows[] = $row;
|
||||
}
|
||||
} elseif (is_array($value)) {
|
||||
$rows = $value;
|
||||
}
|
||||
|
||||
/* echo '<pre>';
|
||||
print_r($rows);
|
||||
echo '</pre>';
|
||||
die(); */
|
||||
|
||||
update_field($field, $rows, $post_id);
|
||||
break;
|
||||
case 'group':
|
||||
// Valeur attendue : array associatif
|
||||
if (is_string($value)) {
|
||||
$group = [];
|
||||
$pairs = explode(';', $value);
|
||||
foreach ($pairs as $pair) {
|
||||
[$k, $v] = array_pad(explode(':', $pair, 2), 2, null);
|
||||
if ($k && $v !== null) $group[trim($k)] = trim($v);
|
||||
}
|
||||
update_field($field, $group, $post_id);
|
||||
} elseif (is_array($value)) {
|
||||
update_field($field, $value, $post_id);
|
||||
}
|
||||
break;
|
||||
case 'checkbox':
|
||||
// Valeur attendue : string format pipe ou array d'items
|
||||
$value = is_array($value) ? $value : explode('|', (string)$value);
|
||||
update_field($field, $value, $post_id);
|
||||
break;
|
||||
case 'date_picker':
|
||||
// Valeur attendue : string format d/m/Y
|
||||
$value = \DateTime::createFromFormat('d/m/Y', $value);
|
||||
$value = $value->format('Y-m-d');
|
||||
update_field($field, $value, $post_id);
|
||||
break;
|
||||
default:
|
||||
// Champ simple (text, select, radio, etc.)
|
||||
update_field($field, $value, $post_id);
|
||||
}
|
||||
}
|
||||
|
||||
public static function is_date($input, $format = 'd/m/Y') {
|
||||
$d = \DateTime::createFromFormat($format, $input);
|
||||
return $d && $d->format($format) === $input;
|
||||
}
|
||||
}
|
||||
579
app/models/Traducteur_Model.php
Normal file
579
app/models/Traducteur_Model.php
Normal file
@ -0,0 +1,579 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ESI_CRVI_AGENDA\models;
|
||||
|
||||
use ESI_CRVI_AGENDA\helpers\Api_Helper;
|
||||
|
||||
class CRVI_Traducteur_Model extends Main_Model {
|
||||
public $id;
|
||||
public $nom;
|
||||
public $prenom;
|
||||
public $email;
|
||||
public $langues_parlees;
|
||||
public $jours_de_disponibilite;
|
||||
public $indisponibilitee_ponctuelle;
|
||||
public $organisme;
|
||||
public $commentaires;
|
||||
public $type_de_fiche;
|
||||
|
||||
/**
|
||||
* Carte des codes de langue vers le nom de la langue (Europe continentale + Afrique du Nord)
|
||||
*/
|
||||
public static $lg_map = [
|
||||
'fr' => 'Français',
|
||||
'de' => 'Allemand',
|
||||
'it' => 'Italien',
|
||||
'es' => 'Espagnol',
|
||||
'pt' => 'Portugais',
|
||||
'nl' => 'Néerlandais',
|
||||
'en' => 'Anglais',
|
||||
'ru' => 'Russe',
|
||||
'pl' => 'Polonais',
|
||||
'ro' => 'Roumain',
|
||||
'el' => 'Grec',
|
||||
'tr' => 'Turc',
|
||||
'ar' => 'Arabe',
|
||||
'kab' => 'Kabyle',
|
||||
'ber' => 'Berbère',
|
||||
'tzm' => 'Tamazight',
|
||||
'da' => 'Danois',
|
||||
'sv' => 'Suédois',
|
||||
'no' => 'Norvégien',
|
||||
'fi' => 'Finnois',
|
||||
'cs' => 'Tchèque',
|
||||
'sk' => 'Slovaque',
|
||||
'hu' => 'Hongrois',
|
||||
'bg' => 'Bulgare',
|
||||
'hr' => 'Croate',
|
||||
'sr' => 'Serbe',
|
||||
'bs' => 'Bosnien',
|
||||
'sq' => 'Albanais',
|
||||
'mk' => 'Macédonien',
|
||||
'sl' => 'Slovène',
|
||||
'he' => 'Hébreu',
|
||||
'lt' => 'Lituanien',
|
||||
'lv' => 'Letton',
|
||||
'et' => 'Estonien',
|
||||
];
|
||||
|
||||
/**
|
||||
* Schéma des champs ACF : nom => type (ex : group, repeater, taxonomy, text...)
|
||||
*/
|
||||
public static $acf_schema = [
|
||||
'nom' => 'text',
|
||||
'prenom' => 'text',
|
||||
'email' => 'email',
|
||||
'organisme' => 'taxonomy',
|
||||
'langues_parlees' => 'taxonomy',
|
||||
'jours_de_disponibilite' => 'checkbox',
|
||||
'indisponibilitee_ponctuelle' => 'repeater',
|
||||
'coordonnees' => 'group',
|
||||
'commentaires' => 'textarea',
|
||||
'type_de_fiche' => 'select',
|
||||
];
|
||||
|
||||
public function __construct($data = []) {
|
||||
foreach ($data as $key => $value) {
|
||||
if (property_exists($this, $key)) {
|
||||
$this->$key = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function get_traducteurs($filters = [],$simple_list = false) {
|
||||
$posts = get_posts([
|
||||
'post_type' => 'traducteur',
|
||||
'numberposts' => -1,
|
||||
'meta_query' => $filters,
|
||||
]);
|
||||
|
||||
if ($simple_list) {
|
||||
$posts = array_map(function($post) {
|
||||
return [
|
||||
'id' => $post->ID,
|
||||
'nom' => $post->post_title,
|
||||
];
|
||||
}, $posts);
|
||||
}
|
||||
return $posts;
|
||||
}
|
||||
|
||||
public static function load($id, $fields = []) {
|
||||
// Charger depuis CPT/meta
|
||||
$traducteur = get_post($id);
|
||||
if (!$traducteur) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Si des champs spécifiques sont demandés, ne charger que ceux-ci
|
||||
if (!empty($fields)) {
|
||||
$data = [];
|
||||
foreach ($fields as $field) {
|
||||
if ($field === 'id') {
|
||||
$data['id'] = $traducteur->ID;
|
||||
} elseif (property_exists(self::class, $field)) {
|
||||
$data[$field] = get_field($field, $traducteur->ID);
|
||||
}
|
||||
}
|
||||
return new self($data);
|
||||
}
|
||||
|
||||
// Sinon, charger tous les champs par défaut
|
||||
return new self([
|
||||
'id' => $traducteur->ID,
|
||||
'nom' => get_field('nom', $traducteur->ID),
|
||||
'prenom' => get_field('prenom', $traducteur->ID),
|
||||
'email' => get_field('email', $traducteur->ID),
|
||||
'langues_parlees' => get_field('langues_parlees', $traducteur->ID),
|
||||
'jours_de_disponibilite' => get_field('jours_de_disponibilite', $traducteur->ID),
|
||||
'indisponibilitee_ponctuelle' => get_field('indisponibilitee_ponctuelle', $traducteur->ID),
|
||||
'organisme' => get_field('organisme', $traducteur->ID),
|
||||
'commentaires' => get_field('commentaires', $traducteur->ID),
|
||||
'type_de_fiche' => get_field('type_de_fiche', $traducteur->ID),
|
||||
]);
|
||||
}
|
||||
|
||||
public function save() {
|
||||
// À implémenter : sauvegarder dans CPT/meta
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne les traducteurs disponibles à une date donnée et pour une langue donnée.
|
||||
* @param string $date au format Y-m-d
|
||||
* @param string $langue_slug (slug de la langue)
|
||||
* @param int|null $event_id ID de l'événement en cours d'édition (pour inclure le traducteur actuel)
|
||||
* @return array Liste des traducteurs disponibles (CRVI_Traducteur_Model)
|
||||
*/
|
||||
public static function filtrer_disponibles($date, $langue_slug, $event_id = null) {
|
||||
if (empty($date)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Construire la requête de base
|
||||
$args = [
|
||||
'post_type' => 'traducteur',
|
||||
'numberposts' => -1,
|
||||
];
|
||||
|
||||
// Ajouter le filtre de langue seulement si spécifié
|
||||
if (!empty($langue_slug)) {
|
||||
$args['tax_query'] = [
|
||||
[
|
||||
'taxonomy' => 'langue',
|
||||
'field' => 'slug',
|
||||
'terms' => $langue_slug,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
$traducteurs = get_posts($args);
|
||||
$disponibles = [];
|
||||
|
||||
foreach ($traducteurs as $traducteur_post) {
|
||||
$traducteur = new self([
|
||||
'id' => $traducteur_post->ID,
|
||||
'nom' => get_field('nom', $traducteur_post->ID),
|
||||
'prenom' => get_field('prenom', $traducteur_post->ID),
|
||||
'email' => get_field('email', $traducteur_post->ID),
|
||||
'langues_parlees' => get_field('langues_parlees', $traducteur_post->ID),
|
||||
'jours_de_disponibilite' => get_field('jours_de_disponibilite', $traducteur_post->ID),
|
||||
'indisponibilitee_ponctuelle' => get_field('indisponibilitee_ponctuelle', $traducteur_post->ID),
|
||||
'organisme' => get_field('organisme', $traducteur_post->ID),
|
||||
'commentaires' => get_field('commentaires', $traducteur_post->ID),
|
||||
'type_de_fiche' => get_field('type_de_fiche', $traducteur_post->ID),
|
||||
]);
|
||||
|
||||
// Vérifier si le traducteur est disponible
|
||||
$is_disponible = $traducteur->is_disponible($traducteur->id, $date, $langue_slug);
|
||||
|
||||
// Vérifier les conflits d'événements existants
|
||||
if ($is_disponible) {
|
||||
$conflits = self::verifier_conflits_traducteur($traducteur->id, $date, $event_id);
|
||||
if (!empty($conflits)) {
|
||||
$is_disponible = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Si c'est l'événement en cours d'édition, inclure le traducteur même s'il est "pris"
|
||||
if ($event_id && self::is_traducteur_of_event($traducteur->id, $event_id)) {
|
||||
$is_disponible = true;
|
||||
}
|
||||
|
||||
if ($is_disponible) {
|
||||
$disponibles[] = $traducteur;
|
||||
}
|
||||
}
|
||||
return $disponibles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si un traducteur est associé à un événement donné
|
||||
* @param int $traducteur_id
|
||||
* @param int $event_id
|
||||
* @return bool
|
||||
*/
|
||||
private static function is_traducteur_of_event($traducteur_id, $event_id) {
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'crvi_agenda';
|
||||
$result = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT id FROM $table_name WHERE id = %d AND id_traducteur = %d",
|
||||
$event_id,
|
||||
$traducteur_id
|
||||
));
|
||||
return !empty($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si ce traducteur est disponible à une date donnée et pour une langue donnée.
|
||||
* @param string $date au format Y-m-d
|
||||
* @param string|null $langue_slug
|
||||
* @return bool
|
||||
*/
|
||||
public function is_disponible($traducteur_id, $date, $langue_slug = null) {
|
||||
// Vérifier que la date n'est pas null ou vide
|
||||
if (empty($date)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$timestamp = strtotime($date);
|
||||
if ($timestamp === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$jour = strtolower(date('l', $timestamp));
|
||||
|
||||
$jours_disponibles = get_field('jours_de_disponibilite', 'user_' . $traducteur_id);
|
||||
if (!is_array($jours_disponibles) || !in_array($jour, $jours_disponibles, true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$indisponibilites = get_field('indisponibilitee_ponctuelle', 'user_' . $traducteur_id);
|
||||
if (is_array($indisponibilites)) {
|
||||
foreach ($indisponibilites 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) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Optionnel : vérifier la langue si besoin
|
||||
if ($langue_slug && is_array($this->langues_parlees) && !in_array($langue_slug, $this->langues_parlees)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function get_relations() {
|
||||
// À implémenter : récupérer les rendez-vous associés, etc.
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Créer un traducteur.
|
||||
* @param array $data
|
||||
* @return int|WP_Error
|
||||
*/
|
||||
public static function create(array $data, bool $as_rest = false) {
|
||||
if (empty($data['nom']) || empty($data['prenom']) || empty($data['email'])) {
|
||||
if ($as_rest) {
|
||||
return [
|
||||
'success' => false,
|
||||
'code' => 400,
|
||||
'message' => 'Champs obligatoires manquants',
|
||||
'data' => null
|
||||
];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// Chercher un traducteur existant par email
|
||||
$existing = get_posts([
|
||||
'post_type' => 'traducteur',
|
||||
'meta_key' => 'email',
|
||||
'meta_value' => $data['email'],
|
||||
'post_status'=> 'any',
|
||||
'numberposts'=> 1,
|
||||
]);
|
||||
if ($existing) {
|
||||
// Mise à jour
|
||||
$post_id = $existing[0]->ID;
|
||||
// Mettre à jour le titre si nom/prenom fournis
|
||||
$post_title = ($data['nom'] ?? get_post_meta($post_id, 'nom', true)) . ' ' . ($data['prenom'] ?? get_post_meta($post_id, 'prenom', true));
|
||||
wp_update_post([
|
||||
'ID' => $post_id,
|
||||
'post_title' => $post_title,
|
||||
]);
|
||||
foreach ($data as $key => $value) {
|
||||
if ($key === 'organisme') {
|
||||
if (!taxonomy_exists('organisme')) {
|
||||
register_taxonomy(
|
||||
'organisme',
|
||||
'traducteur',
|
||||
[
|
||||
'label' => 'Organismes',
|
||||
'public' => false,
|
||||
'hierarchical' => false,
|
||||
'show_ui' => true,
|
||||
'show_in_rest' => false,
|
||||
]
|
||||
);
|
||||
}
|
||||
$org_ids = [];
|
||||
$organismes = is_array($value) ? $value : explode('|', $value);
|
||||
foreach ($organismes as $org_nom) {
|
||||
$org_nom = trim($org_nom);
|
||||
if (empty($org_nom)) continue;
|
||||
$slug = sanitize_title($org_nom);
|
||||
$term = get_term_by('slug', $slug, 'organisme');
|
||||
if (!$term) {
|
||||
$result = wp_insert_term($org_nom, 'organisme', ['slug' => $slug]);
|
||||
if (!is_wp_error($result) && isset($result['term_id'])) {
|
||||
$org_ids[] = $result['term_id'];
|
||||
}
|
||||
} else {
|
||||
$org_ids[] = $term->term_id;
|
||||
}
|
||||
}
|
||||
if (!empty($org_ids)) {
|
||||
update_field('organisme', $org_ids, $post_id);
|
||||
}
|
||||
} else if ($key === 'langues_parlees') {
|
||||
if (!taxonomy_exists('langue')) {
|
||||
register_taxonomy(
|
||||
'langue',
|
||||
'traducteur',
|
||||
[
|
||||
'label' => 'Langues',
|
||||
'public' => false,
|
||||
'hierarchical' => false,
|
||||
'show_ui' => true,
|
||||
'show_in_rest' => false,
|
||||
]
|
||||
);
|
||||
}
|
||||
$langue_ids = [];
|
||||
$langues = is_array($value) ? $value : explode('|', $value);
|
||||
foreach ($langues as $slug) {
|
||||
$slug = trim($slug);
|
||||
if (empty($slug)) continue;
|
||||
$term = get_term_by('slug', $slug, 'langue');
|
||||
if (!$term) {
|
||||
$nom = self::$lg_map[$slug] ?? $slug;
|
||||
$result = wp_insert_term($nom, 'langue', ['slug' => $slug]);
|
||||
if (!is_wp_error($result) && isset($result['term_id'])) {
|
||||
$langue_ids[] = $result['term_id'];
|
||||
}
|
||||
} else {
|
||||
$langue_ids[] = $term->term_id;
|
||||
}
|
||||
}
|
||||
if (!empty($langue_ids)) {
|
||||
update_field('langue', $langue_ids, $post_id);
|
||||
}
|
||||
} else if (isset(self::$acf_schema[$key])) {
|
||||
self::set_acf_field($post_id, $key, $value, self::$acf_schema);
|
||||
} else {
|
||||
update_post_meta($post_id, $key, $value);
|
||||
}
|
||||
}
|
||||
return $post_id;
|
||||
}
|
||||
// Création classique
|
||||
$post_id = wp_insert_post([
|
||||
'post_type' => 'traducteur',
|
||||
'post_title' => $data['nom'] . ' ' . $data['prenom'],
|
||||
'post_status' => 'publish',
|
||||
]);
|
||||
if (is_wp_error($post_id)) {
|
||||
if ($as_rest) {
|
||||
return [
|
||||
'success' => false,
|
||||
'code' => 500,
|
||||
'message' => 'Erreur lors de la création',
|
||||
'data' => json_encode($post_id)
|
||||
];
|
||||
}
|
||||
return $post_id;
|
||||
}
|
||||
foreach ($data as $key => $value) {
|
||||
if ($key === 'organisme') {
|
||||
if (!taxonomy_exists('organisme')) {
|
||||
register_taxonomy(
|
||||
'organisme',
|
||||
'traducteur',
|
||||
[
|
||||
'label' => 'Organismes',
|
||||
'public' => false,
|
||||
'hierarchical' => false,
|
||||
'show_ui' => true,
|
||||
'show_in_rest' => false,
|
||||
]
|
||||
);
|
||||
}
|
||||
$org_ids = [];
|
||||
$organismes = is_array($value) ? $value : explode('|', $value);
|
||||
foreach ($organismes as $org_nom) {
|
||||
$org_nom = trim($org_nom);
|
||||
if (empty($org_nom)) continue;
|
||||
$slug = sanitize_title($org_nom);
|
||||
$term = get_term_by('slug', $slug, 'organisme');
|
||||
if (!$term) {
|
||||
$result = wp_insert_term($org_nom, 'organisme', ['slug' => $slug]);
|
||||
if (!is_wp_error($result) && isset($result['term_id'])) {
|
||||
$org_ids[] = $result['term_id'];
|
||||
}
|
||||
} else {
|
||||
$org_ids[] = $term->term_id;
|
||||
}
|
||||
}
|
||||
if (!empty($org_ids)) {
|
||||
update_field('organisme', $org_ids, $post_id);
|
||||
}
|
||||
} else if ($key === 'langues_parlees') {
|
||||
if (!taxonomy_exists('langue')) {
|
||||
register_taxonomy(
|
||||
'langue',
|
||||
'traducteur',
|
||||
[
|
||||
'label' => 'Langues',
|
||||
'public' => false,
|
||||
'hierarchical' => false,
|
||||
'show_ui' => true,
|
||||
'show_in_rest' => false,
|
||||
]
|
||||
);
|
||||
}
|
||||
$langue_ids = [];
|
||||
$langues = is_array($value) ? $value : explode('|', $value);
|
||||
foreach ($langues as $slug) {
|
||||
$slug = trim($slug);
|
||||
if (empty($slug)) continue;
|
||||
$term = get_term_by('slug', $slug, 'langue');
|
||||
if (!$term) {
|
||||
$nom = self::$lg_map[$slug] ?? $slug;
|
||||
$result = wp_insert_term($nom, 'langue', ['slug' => $slug]);
|
||||
if (!is_wp_error($result) && isset($result['term_id'])) {
|
||||
$langue_ids[] = $result['term_id'];
|
||||
}
|
||||
} else {
|
||||
$langue_ids[] = $term->term_id;
|
||||
}
|
||||
}
|
||||
if (!empty($langue_ids)) {
|
||||
update_field('langue', $langue_ids, $post_id);
|
||||
}
|
||||
} else if (isset(self::$acf_schema[$key])) {
|
||||
self::set_acf_field($post_id, $key, $value, self::$acf_schema);
|
||||
} else {
|
||||
update_post_meta($post_id, $key, $value);
|
||||
}
|
||||
}
|
||||
return $post_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mettre à jour un traducteur.
|
||||
* @param int $id
|
||||
* @param array $data
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
public function update(int $id, array $data) {
|
||||
if (!is_user_logged_in() || !get_current_user_id()) {
|
||||
return Api_Helper::json_error('Authentification requise', 401);
|
||||
}
|
||||
if (!current_user_can('edit_posts')) {
|
||||
return Api_Helper::json_error('Non autorisé', 403);
|
||||
}
|
||||
$post = get_post($id);
|
||||
if (!$post || $post->post_type !== 'traducteur') {
|
||||
return Api_Helper::json_error('Traducteur introuvable', 404);
|
||||
}
|
||||
// Vérifier unicité de l'email si modifié
|
||||
if (!empty($data['email'])) {
|
||||
$existing = get_posts([
|
||||
'post_type' => 'traducteur',
|
||||
'meta_key' => 'email',
|
||||
'meta_value' => $data['email'],
|
||||
'post_status'=> 'any',
|
||||
'exclude' => [$id],
|
||||
'numberposts'=> 1,
|
||||
]);
|
||||
if ($existing) {
|
||||
return Api_Helper::json_error('Email déjà utilisé', 409);
|
||||
}
|
||||
}
|
||||
$result = wp_update_post([
|
||||
'ID' => $id,
|
||||
'post_title' => ($data['nom'] ?? get_post_meta($id, 'nom', true)) . ' ' . ($data['prenom'] ?? get_post_meta($id, 'prenom', true)),
|
||||
], true);
|
||||
if (is_wp_error($result)) {
|
||||
return $result;
|
||||
}
|
||||
foreach ($data as $key => $value) {
|
||||
update_post_meta($id, $key, $value);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Supprimer un traducteur (corbeille WordPress)
|
||||
* @param int $id
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
public function delete(int $id) {
|
||||
if (!is_user_logged_in() || !get_current_user_id()) {
|
||||
return Api_Helper::json_error('Authentification requise', 401);
|
||||
}
|
||||
if (!current_user_can('delete_posts')) {
|
||||
return Api_Helper::json_error('Non autorisé', 403);
|
||||
}
|
||||
$post = get_post($id);
|
||||
if (!$post || $post->post_type !== 'traducteur') {
|
||||
return Api_Helper::json_error('Traducteur introuvable', 404);
|
||||
}
|
||||
$result = wp_trash_post($id);
|
||||
if (!$result) {
|
||||
return Api_Helper::json_error('Erreur lors de la suppression', 500);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie s'il y a des conflits d'événements pour un traducteur donné
|
||||
* @param int $traducteur_id
|
||||
* @param string $date
|
||||
* @param int|null $event_id - ID de l'événement à exclure (pour l'édition)
|
||||
* @return array
|
||||
*/
|
||||
private static function verifier_conflits_traducteur($traducteur_id, $date, $event_id = null) {
|
||||
global $wpdb;
|
||||
|
||||
$table_events = $wpdb->prefix . 'crvi_agenda';
|
||||
|
||||
$where_conditions = ['id_traducteur = %d'];
|
||||
$where_values = [$traducteur_id];
|
||||
|
||||
// Exclure l'événement en cours d'édition si spécifié
|
||||
if ($event_id) {
|
||||
$where_conditions[] = 'id != %d';
|
||||
$where_values[] = $event_id;
|
||||
}
|
||||
|
||||
// Ajouter la condition de date
|
||||
$where_conditions[] = 'date_rdv = %s';
|
||||
$where_values[] = $date;
|
||||
|
||||
$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);
|
||||
}
|
||||
}
|
||||
898
app/models/TraductionLangue_Model.php
Normal file
898
app/models/TraductionLangue_Model.php
Normal file
@ -0,0 +1,898 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ESI_CRVI_AGENDA\models;
|
||||
|
||||
use ESI_CRVI_AGENDA\models\CRVI_Event_Model;
|
||||
|
||||
/**
|
||||
* Modèle pour les capacités de traduction
|
||||
* Gère les règles de capacité par langue, jour et période
|
||||
*/
|
||||
class CRVI_TraductionLangue_Model extends Main_Model {
|
||||
public $id;
|
||||
public $langue; // Peut être un tableau d'IDs de langues (multi-select)
|
||||
public $jour;
|
||||
public $periode;
|
||||
public $limite;
|
||||
public $limite_par;
|
||||
public $actif;
|
||||
public $ajouter_exception;
|
||||
public $exceptions_disponibilites;
|
||||
|
||||
/**
|
||||
* Schéma des champs ACF : nom => type
|
||||
*/
|
||||
public static $acf_schema = [
|
||||
'langue' => 'taxonomy', // multi_select - retourne un tableau d'IDs
|
||||
'jour' => 'select',
|
||||
'periode' => 'select',
|
||||
'limite' => 'number',
|
||||
'limite_par' => 'select',
|
||||
'actif' => 'true_false',
|
||||
'ajouter_exception' => 'true_false',
|
||||
'exceptions_disponibilites' => 'group',
|
||||
];
|
||||
|
||||
public function __construct($data = []) {
|
||||
foreach ($data as $key => $value) {
|
||||
if (property_exists($this, $key)) {
|
||||
$this->$key = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Charge une capacité de traduction par ID
|
||||
* @param int $id
|
||||
* @param array $fields Champs spécifiques à charger (optionnel)
|
||||
* @return self|null
|
||||
*/
|
||||
public static function load($id, $fields = []) {
|
||||
$post = \get_post($id);
|
||||
if (!$post || $post->post_type !== 'traduction_langue') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Si des champs spécifiques sont demandés
|
||||
if (!empty($fields)) {
|
||||
$data = ['id' => $post->ID];
|
||||
foreach ($fields as $field) {
|
||||
if (property_exists(self::class, $field)) {
|
||||
$data[$field] = \get_field($field, $post->ID);
|
||||
}
|
||||
}
|
||||
return new self($data);
|
||||
}
|
||||
|
||||
// Charger tous les champs
|
||||
$langue_field = \get_field('langue', $post->ID);
|
||||
// S'assurer que langue est toujours un tableau
|
||||
if (!is_array($langue_field)) {
|
||||
$langue_field = !empty($langue_field) ? [$langue_field] : [];
|
||||
}
|
||||
|
||||
return new self([
|
||||
'id' => $post->ID,
|
||||
'langue' => $langue_field, // Tableau d'IDs de langues
|
||||
'jour' => \get_field('jour', $post->ID),
|
||||
'periode' => \get_field('periode', $post->ID),
|
||||
'limite' => \get_field('limite', $post->ID),
|
||||
'limite_par' => \get_field('limite_par', $post->ID) ?: 'semaine',
|
||||
'actif' => \get_field('actif', $post->ID) !== false,
|
||||
'ajouter_exception' => \get_field('ajouter_exception', $post->ID) ?: false,
|
||||
'exceptions_disponibilites' => \get_field('exceptions_disponibilites', $post->ID) ?: null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère toutes les capacités actives
|
||||
* @param array $filters Filtres optionnels (langue, jour, periode, type_capacite)
|
||||
* @param bool $use_cache Utiliser le cache (par défaut: true)
|
||||
* @return array Liste des capacités actives
|
||||
*/
|
||||
public static function getActiveCapacites(array $filters = [], bool $use_cache = true): array {
|
||||
// Générer une clé de cache basée sur les filtres
|
||||
$cache_key = 'crvi_capacites_actives_' . md5(serialize($filters));
|
||||
|
||||
// Essayer de récupérer depuis le cache si activé et sans filtres complexes
|
||||
if ($use_cache && empty($filters)) {
|
||||
$cached = \get_transient($cache_key);
|
||||
if ($cached !== false) {
|
||||
return $cached;
|
||||
}
|
||||
}
|
||||
|
||||
$args = [
|
||||
'post_type' => 'traduction_langue',
|
||||
'posts_per_page' => -1,
|
||||
'post_status' => 'publish',
|
||||
'meta_query' => [
|
||||
[
|
||||
'key' => 'actif',
|
||||
'value' => '1',
|
||||
'compare' => '=',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
// Appliquer les filtres
|
||||
if (!empty($filters['langue'])) {
|
||||
$args['tax_query'] = [
|
||||
[
|
||||
'taxonomy' => 'langue',
|
||||
'field' => 'term_id',
|
||||
'terms' => (int) $filters['langue'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
if (!empty($filters['jour'])) {
|
||||
$args['meta_query'][] = [
|
||||
'key' => 'jour',
|
||||
'value' => $filters['jour'],
|
||||
'compare' => '=',
|
||||
];
|
||||
}
|
||||
|
||||
if (!empty($filters['periode'])) {
|
||||
$args['meta_query'][] = [
|
||||
'key' => 'periode',
|
||||
'value' => $filters['periode'],
|
||||
'compare' => '=',
|
||||
];
|
||||
}
|
||||
|
||||
$posts = \get_posts($args);
|
||||
$capacites = [];
|
||||
|
||||
foreach ($posts as $post) {
|
||||
$capacites[] = self::load($post->ID);
|
||||
}
|
||||
|
||||
// Mettre en cache si activé et sans filtres
|
||||
if ($use_cache && empty($filters)) {
|
||||
\set_transient($cache_key, $capacites, 1 * \HOUR_IN_SECONDS);
|
||||
}
|
||||
|
||||
return $capacites;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compte le nombre de créneaux utilisés pour une capacité donnée
|
||||
* Gère la période (semaine/mois) et exclut les événements annulés/brouillons
|
||||
*
|
||||
* @param int $capacite_id ID de la capacité
|
||||
* @param string|null $date Date de référence (Y-m-d). Si null, utilise la date actuelle
|
||||
* @param self|null $capacite Capacité déjà chargée (évite un appel load() supplémentaire)
|
||||
* @return int Nombre de créneaux utilisés
|
||||
*/
|
||||
public static function countUsed(int $capacite_id, ?string $date = null, ?self $capacite = null): int {
|
||||
// Charger la capacité si non fournie
|
||||
if (!$capacite) {
|
||||
$capacite = self::load($capacite_id);
|
||||
if (!$capacite) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Date de référence
|
||||
if (!$date) {
|
||||
$date = \current_time('Y-m-d');
|
||||
}
|
||||
|
||||
// Générer une clé de cache unique
|
||||
$cache_key = 'crvi_count_used_' . $capacite_id . '_' . $date . '_' . md5(serialize([
|
||||
$capacite->langue,
|
||||
$capacite->jour,
|
||||
$capacite->periode,
|
||||
$capacite->limite_par,
|
||||
]));
|
||||
|
||||
// Essayer de récupérer depuis le cache (cache de 15 minutes)
|
||||
$cached = \get_transient($cache_key);
|
||||
if ($cached !== false) {
|
||||
return (int) $cached;
|
||||
}
|
||||
|
||||
// Déterminer la plage de dates selon limite_par
|
||||
$date_debut = $date;
|
||||
$date_fin = $date;
|
||||
$date_obj = new \DateTime($date);
|
||||
|
||||
if ($capacite->limite_par === 'semaine') {
|
||||
// Calculer le début et la fin de la semaine
|
||||
$start_of_week = (int) \get_option('start_of_week', 1); // 0=dimanche, 1=lundi, etc.
|
||||
|
||||
// Obtenir le jour de la semaine (0=dimanche, 1=lundi, ..., 6=samedi)
|
||||
$day_of_week = (int) $date_obj->format('w');
|
||||
|
||||
// Calculer le nombre de jours à soustraire pour arriver au début de la semaine
|
||||
$days_to_subtract = ($day_of_week - $start_of_week + 7) % 7;
|
||||
|
||||
$date_obj->modify("-{$days_to_subtract} days");
|
||||
$date_debut = $date_obj->format('Y-m-d');
|
||||
|
||||
$date_obj->modify('+6 days');
|
||||
$date_fin = $date_obj->format('Y-m-d');
|
||||
} elseif ($capacite->limite_par === 'mois') {
|
||||
// Premier et dernier jour du mois
|
||||
$date_debut = $date_obj->format('Y-m-01');
|
||||
$date_fin = $date_obj->format('Y-m-t');
|
||||
}
|
||||
|
||||
// Récupérer les langues (peut être un tableau)
|
||||
$langue_ids = $capacite->langue;
|
||||
if (empty($langue_ids)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// S'assurer que c'est un tableau
|
||||
if (!is_array($langue_ids)) {
|
||||
$langue_ids = [$langue_ids];
|
||||
}
|
||||
|
||||
// Récupérer les slugs de toutes les langues (avec cache)
|
||||
$langue_slugs = [];
|
||||
foreach ($langue_ids as $langue_id) {
|
||||
$slug = self::get_langue_slug($langue_id);
|
||||
if ($slug) {
|
||||
$langue_slugs[] = $slug;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($langue_slugs)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Convertir le jour en format de requête
|
||||
$jour_nom = $capacite->jour; // lundi, mardi, etc.
|
||||
|
||||
// Requête pour compter les événements
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'crvi_agenda';
|
||||
|
||||
// Construire la requête SQL
|
||||
// Pour les langues multiples, utiliser IN au lieu de =
|
||||
$langue_placeholders = implode(',', array_fill(0, count($langue_slugs), '%s'));
|
||||
|
||||
$where = [
|
||||
"date_rdv BETWEEN %s AND %s",
|
||||
"langue IN ($langue_placeholders)",
|
||||
"is_deleted = 0",
|
||||
"statut != 'annule'",
|
||||
"statut != 'brouillon'",
|
||||
];
|
||||
$values = array_merge([$date_debut, $date_fin], $langue_slugs);
|
||||
|
||||
// Filtrer par jour de la semaine
|
||||
// DAYOFWEEK() en MySQL retourne: 1=dimanche, 2=lundi, 3=mardi, 4=mercredi, 5=jeudi, 6=vendredi, 7=samedi
|
||||
$jours_map = [
|
||||
'dimanche' => 1,
|
||||
'lundi' => 2,
|
||||
'mardi' => 3,
|
||||
'mercredi' => 4,
|
||||
'jeudi' => 5,
|
||||
'vendredi' => 6,
|
||||
'samedi' => 7,
|
||||
];
|
||||
|
||||
if (isset($jours_map[$jour_nom])) {
|
||||
$where[] = "DAYOFWEEK(date_rdv) = %d";
|
||||
$values[] = $jours_map[$jour_nom];
|
||||
}
|
||||
|
||||
// Filtrer par période (matin, après-midi, journée)
|
||||
// Matin: heure_rdv < 12:00
|
||||
// Après-midi: heure_rdv >= 12:00
|
||||
// Journée: pas de filtre sur l'heure (tous les événements du jour)
|
||||
if ($capacite->periode === 'matin') {
|
||||
$where[] = "heure_rdv < '12:00:00'";
|
||||
} elseif ($capacite->periode === 'apres_midi') {
|
||||
$where[] = "heure_rdv >= '12:00:00'";
|
||||
}
|
||||
// Si période = 'journee', on ne filtre pas par heure
|
||||
|
||||
$where_sql = 'WHERE ' . implode(' AND ', $where);
|
||||
$sql = "SELECT COUNT(*) FROM {$table_name} {$where_sql}";
|
||||
$prepared_sql = $wpdb->prepare($sql, $values);
|
||||
|
||||
$count = (int) $wpdb->get_var($prepared_sql);
|
||||
|
||||
// Mettre en cache le résultat (15 minutes)
|
||||
\set_transient($cache_key, $count, 15 * \MINUTE_IN_SECONDS);
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère le slug d'une langue avec cache
|
||||
* @param int $langue_term_id Term ID de la langue
|
||||
* @return string|null Slug de la langue ou null si erreur
|
||||
*/
|
||||
private static function get_langue_slug(int $langue_term_id): ?string {
|
||||
$cache_key = 'crvi_langue_slug_' . $langue_term_id;
|
||||
$cached = \get_transient($cache_key);
|
||||
|
||||
if ($cached !== false) {
|
||||
return $cached;
|
||||
}
|
||||
|
||||
$langue_term = \get_term($langue_term_id, 'langue');
|
||||
if (!$langue_term || \is_wp_error($langue_term)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$slug = $langue_term->slug;
|
||||
|
||||
// Mettre en cache (1 heure)
|
||||
\set_transient($cache_key, $slug, 1 * \HOUR_IN_SECONDS);
|
||||
|
||||
return $slug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si une capacité a des événements associés
|
||||
* Optimisé avec EXISTS au lieu de COUNT pour meilleure performance
|
||||
* Gère les langues multiples (limite partagée)
|
||||
* @param int $capacite_id ID de la capacité
|
||||
* @return bool True si la capacité a des événements
|
||||
*/
|
||||
public static function hasEvents(int $capacite_id): bool {
|
||||
$capacite = self::load($capacite_id);
|
||||
if (!$capacite) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Récupérer les langues (peut être un tableau)
|
||||
$langue_ids = $capacite->langue;
|
||||
if (empty($langue_ids)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// S'assurer que c'est un tableau
|
||||
if (!is_array($langue_ids)) {
|
||||
$langue_ids = [$langue_ids];
|
||||
}
|
||||
|
||||
// Récupérer les slugs de toutes les langues
|
||||
$langue_slugs = [];
|
||||
foreach ($langue_ids as $langue_id) {
|
||||
$slug = self::get_langue_slug($langue_id);
|
||||
if ($slug) {
|
||||
$langue_slugs[] = $slug;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($langue_slugs)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Requête optimisée avec EXISTS (plus rapide que COUNT)
|
||||
global $wpdb;
|
||||
$table_name = $wpdb->prefix . 'crvi_agenda';
|
||||
|
||||
// Construire les placeholders pour IN
|
||||
$langue_placeholders = implode(',', array_fill(0, count($langue_slugs), '%s'));
|
||||
|
||||
$sql = $wpdb->prepare(
|
||||
"SELECT EXISTS(
|
||||
SELECT 1 FROM {$table_name}
|
||||
WHERE langue IN ($langue_placeholders)
|
||||
AND is_deleted = 0
|
||||
AND statut != 'annule'
|
||||
AND statut != 'brouillon'
|
||||
LIMIT 1
|
||||
)",
|
||||
$langue_slugs
|
||||
);
|
||||
|
||||
$exists = (int) $wpdb->get_var($sql);
|
||||
|
||||
return $exists === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si une capacité est disponible (limite non atteinte)
|
||||
* @param int $capacite_id ID de la capacité
|
||||
* @param string|null $date Date de référence (Y-m-d). Si null, utilise la date actuelle
|
||||
* @param self|null $capacite Capacité déjà chargée (évite un appel load() supplémentaire)
|
||||
* @return bool True si la capacité est disponible
|
||||
*/
|
||||
public static function isAvailable(int $capacite_id, ?string $date = null, ?self $capacite = null): bool {
|
||||
// Charger la capacité si non fournie
|
||||
if (!$capacite) {
|
||||
$capacite = self::load($capacite_id);
|
||||
if (!$capacite) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Si la capacité est inactive, elle n'est pas disponible
|
||||
if (!$capacite->actif) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Si pas de limite définie, la capacité est toujours disponible
|
||||
if (empty($capacite->limite) || $capacite->limite <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Compter les créneaux utilisés (en passant la capacité pour éviter un load() supplémentaire)
|
||||
$used = self::countUsed($capacite_id, $date, $capacite);
|
||||
|
||||
// Vérifier si la limite est atteinte
|
||||
return $used < $capacite->limite;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule la limite effective en prenant en compte les exceptions
|
||||
* Exemple : 1 fois/semaine matin (= ~4/mois) + exception 1/mois après-midi = 4 matins + 1 après-midi
|
||||
*
|
||||
* @param self $capacite Capacité
|
||||
* @param string|null $date Date de référence (Y-m-d). Si null, utilise la date actuelle
|
||||
* @return array ['principale' => ['limite' => int, 'periode' => string], 'exception' => ['limite' => int, 'periode' => string] | null]
|
||||
*/
|
||||
public static function getEffectiveLimit(self $capacite, ?string $date = null): array {
|
||||
if (!$date) {
|
||||
$date = \current_time('Y-m-d');
|
||||
}
|
||||
|
||||
$result = [
|
||||
'principale' => [
|
||||
'limite' => (int) $capacite->limite,
|
||||
'periode' => $capacite->periode,
|
||||
'limite_par' => $capacite->limite_par,
|
||||
],
|
||||
'exception' => null,
|
||||
];
|
||||
|
||||
// Si pas d'exception, retourner seulement la limite principale
|
||||
if (!$capacite->ajouter_exception || empty($capacite->exceptions_disponibilites)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$exceptions = $capacite->exceptions_disponibilites;
|
||||
|
||||
// Vérifier que l'exception a les champs nécessaires
|
||||
if (empty($exceptions['frequence']) || empty($exceptions['frequence_periode']) || empty($exceptions['periode'])) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Calculer la limite de l'exception
|
||||
$exception_limite = (int) $exceptions['frequence'];
|
||||
$exception_periode = $exceptions['periode'];
|
||||
$exception_frequence_periode = $exceptions['frequence_periode']; // 'semaine' ou 'mois'
|
||||
|
||||
// Si l'exception est par mois, utiliser directement la fréquence
|
||||
if ($exception_frequence_periode === 'mois') {
|
||||
$result['exception'] = [
|
||||
'limite' => $exception_limite,
|
||||
'periode' => $exception_periode,
|
||||
'limite_par' => 'mois',
|
||||
];
|
||||
} else {
|
||||
// Si l'exception est par semaine, convertir en nombre par mois
|
||||
$date_obj = new \DateTime($date);
|
||||
$nb_semaines = self::getNbSemainesDansMois($date_obj);
|
||||
|
||||
$result['exception'] = [
|
||||
'limite' => $exception_limite * $nb_semaines,
|
||||
'periode' => $exception_periode,
|
||||
'limite_par' => 'mois',
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule le nombre de semaines dans un mois donné
|
||||
*
|
||||
* @param \DateTime $date Date de référence
|
||||
* @return int Nombre de semaines
|
||||
*/
|
||||
private static function getNbSemainesDansMois(\DateTime $date): int {
|
||||
$premier_jour = new \DateTime($date->format('Y-m-01'));
|
||||
$dernier_jour = new \DateTime($date->format('Y-m-t'));
|
||||
|
||||
$start_of_week = (int) \get_option('start_of_week', 1); // 0=dimanche, 1=lundi
|
||||
|
||||
// Calculer le nombre de semaines
|
||||
$diff = $premier_jour->diff($dernier_jour);
|
||||
$nb_jours = $diff->days + 1;
|
||||
|
||||
// Arrondir au nombre de semaines (4 ou 5 selon le mois)
|
||||
return (int) ceil($nb_jours / 7);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compte les événements utilisés pour la période principale et l'exception
|
||||
*
|
||||
* @param int $capacite_id ID de la capacité
|
||||
* @param string|null $date Date de référence (Y-m-d). Si null, utilise la date actuelle
|
||||
* @param self|null $capacite Capacité déjà chargée
|
||||
* @return array ['principale' => int, 'exception' => int, 'total' => int]
|
||||
*/
|
||||
public static function countUsedWithExceptions(int $capacite_id, ?string $date = null, ?self $capacite = null): array {
|
||||
// Charger la capacité si non fournie
|
||||
if (!$capacite) {
|
||||
$capacite = self::load($capacite_id);
|
||||
if (!$capacite) {
|
||||
return ['principale' => 0, 'exception' => 0, 'total' => 0];
|
||||
}
|
||||
}
|
||||
|
||||
// Compter les événements pour la période principale
|
||||
$count_principale = self::countUsed($capacite_id, $date, $capacite);
|
||||
|
||||
$result = [
|
||||
'principale' => $count_principale,
|
||||
'exception' => 0,
|
||||
'total' => $count_principale,
|
||||
];
|
||||
|
||||
// Si pas d'exception, retourner
|
||||
if (!$capacite->ajouter_exception || empty($capacite->exceptions_disponibilites)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$exceptions = $capacite->exceptions_disponibilites;
|
||||
|
||||
// Vérifier que l'exception a les champs nécessaires
|
||||
if (empty($exceptions['periode'])) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Compter les événements pour l'exception (période différente)
|
||||
$exception_periode = $exceptions['periode'];
|
||||
|
||||
// Si la période de l'exception est différente de la période principale
|
||||
if ($exception_periode !== $capacite->periode) {
|
||||
// Créer une capacité temporaire avec la période de l'exception
|
||||
$temp_capacite = clone $capacite;
|
||||
$temp_capacite->periode = $exception_periode;
|
||||
|
||||
$count_exception = self::countUsed($capacite_id, $date, $temp_capacite);
|
||||
|
||||
$result['exception'] = $count_exception;
|
||||
$result['total'] = $count_principale + $count_exception;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les disponibilités pour une langue donnée en agrégeant toutes les capacités
|
||||
* Exemple : Si 2 capacités existent pour l'arabe (1/mois matin + 1/semaine après-midi)
|
||||
* Le résultat sera : 1 matin/mois + ~4 après-midi/mois = 5 créneaux/mois au total
|
||||
*
|
||||
* @param int $langue_term_id ID du terme de la taxonomie langue
|
||||
* @param string|null $date Date de référence (Y-m-d). Si null, utilise la date actuelle
|
||||
* @param bool $use_cache Utiliser le cache transient (par défaut: true)
|
||||
* @return array ['total' => int, 'by_periode' => ['matin' => [...], 'apres_midi' => [...], 'journee' => [...]]]
|
||||
*/
|
||||
public static function getDisponibilitesByLangue(int $langue_term_id, ?string $date = null, bool $use_cache = true): array {
|
||||
if (!$date) {
|
||||
$date = \current_time('Y-m-d');
|
||||
}
|
||||
|
||||
// Générer une clé de cache unique basée sur la langue et la date
|
||||
$cache_key = 'crvi_dispos_langue_' . $langue_term_id . '_' . $date;
|
||||
|
||||
// Essayer de récupérer depuis le cache
|
||||
if ($use_cache) {
|
||||
$cached = \get_transient($cache_key);
|
||||
if ($cached !== false) {
|
||||
return $cached;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialiser le résultat
|
||||
$result = [
|
||||
'total' => 0,
|
||||
'total_used' => 0,
|
||||
'total_available' => 0,
|
||||
'by_periode' => [
|
||||
'matin' => [],
|
||||
'apres_midi' => [],
|
||||
'journee' => [],
|
||||
],
|
||||
];
|
||||
|
||||
// Récupérer toutes les capacités actives pour cette langue
|
||||
$capacites = self::getActiveCapacites(['langue' => $langue_term_id], false);
|
||||
|
||||
if (empty($capacites)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$date_obj = new \DateTime($date);
|
||||
$nb_semaines = self::getNbSemainesDansMois($date_obj);
|
||||
|
||||
// Parcourir chaque capacité et calculer sa contribution
|
||||
foreach ($capacites as $capacite) {
|
||||
// Vérifier que la langue correspond (peut être un tableau)
|
||||
$capacite_langues = is_array($capacite->langue) ? $capacite->langue : [$capacite->langue];
|
||||
if (!in_array($langue_term_id, $capacite_langues)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calculer la limite mensuelle pour la période principale
|
||||
$limite_mensuelle_principale = self::convertToMonthlyLimit(
|
||||
$capacite->limite,
|
||||
$capacite->limite_par,
|
||||
$nb_semaines
|
||||
);
|
||||
|
||||
// Compter les événements utilisés pour la période principale
|
||||
$used_principale = self::countUsed($capacite->id, $date, $capacite);
|
||||
|
||||
// Ajouter la capacité principale
|
||||
$periode_principale = $capacite->periode;
|
||||
$result['by_periode'][$periode_principale][] = [
|
||||
'capacite_id' => $capacite->id,
|
||||
'capacite_title' => \get_the_title($capacite->id),
|
||||
'jour' => $capacite->jour,
|
||||
'limite' => $limite_mensuelle_principale,
|
||||
'used' => $used_principale,
|
||||
'available' => max(0, $limite_mensuelle_principale - $used_principale),
|
||||
'type' => 'principale',
|
||||
];
|
||||
|
||||
$result['total'] += $limite_mensuelle_principale;
|
||||
$result['total_used'] += $used_principale;
|
||||
|
||||
// Si la capacité a une exception
|
||||
if ($capacite->ajouter_exception && !empty($capacite->exceptions_disponibilites)) {
|
||||
$exceptions = $capacite->exceptions_disponibilites;
|
||||
|
||||
if (!empty($exceptions['frequence']) && !empty($exceptions['frequence_periode']) && !empty($exceptions['periode'])) {
|
||||
// Calculer la limite mensuelle pour l'exception
|
||||
$limite_mensuelle_exception = self::convertToMonthlyLimit(
|
||||
(int) $exceptions['frequence'],
|
||||
$exceptions['frequence_periode'],
|
||||
$nb_semaines
|
||||
);
|
||||
|
||||
// Compter les événements utilisés pour l'exception
|
||||
$temp_capacite = clone $capacite;
|
||||
$temp_capacite->periode = $exceptions['periode'];
|
||||
$used_exception = self::countUsed($capacite->id, $date, $temp_capacite);
|
||||
|
||||
// Ajouter l'exception
|
||||
$periode_exception = $exceptions['periode'];
|
||||
$result['by_periode'][$periode_exception][] = [
|
||||
'capacite_id' => $capacite->id,
|
||||
'capacite_title' => \get_the_title($capacite->id) . ' (Exception)',
|
||||
'jour' => $capacite->jour,
|
||||
'limite' => $limite_mensuelle_exception,
|
||||
'used' => $used_exception,
|
||||
'available' => max(0, $limite_mensuelle_exception - $used_exception),
|
||||
'type' => 'exception',
|
||||
];
|
||||
|
||||
$result['total'] += $limite_mensuelle_exception;
|
||||
$result['total_used'] += $used_exception;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculer le total disponible
|
||||
$result['total_available'] = max(0, $result['total'] - $result['total_used']);
|
||||
|
||||
// Calculer les totaux par période
|
||||
foreach ($result['by_periode'] as $periode => &$items) {
|
||||
$periode_total = 0;
|
||||
$periode_used = 0;
|
||||
$periode_available = 0;
|
||||
|
||||
foreach ($items as $item) {
|
||||
$periode_total += $item['limite'];
|
||||
$periode_used += $item['used'];
|
||||
$periode_available += $item['available'];
|
||||
}
|
||||
|
||||
// Ajouter un résumé pour cette période
|
||||
$result['by_periode'][$periode . '_summary'] = [
|
||||
'total' => $periode_total,
|
||||
'used' => $periode_used,
|
||||
'available' => $periode_available,
|
||||
];
|
||||
}
|
||||
|
||||
// Mettre en cache le résultat (2 jours)
|
||||
if ($use_cache) {
|
||||
\set_transient($cache_key, $result, 2 * \DAY_IN_SECONDS);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit une limite (semaine ou mois) en limite mensuelle
|
||||
*
|
||||
* @param int $limite Limite originale
|
||||
* @param string $limite_par 'semaine' ou 'mois'
|
||||
* @param int $nb_semaines Nombre de semaines dans le mois
|
||||
* @return int Limite mensuelle
|
||||
*/
|
||||
private static function convertToMonthlyLimit(int $limite, string $limite_par, int $nb_semaines): int {
|
||||
if ($limite_par === 'semaine') {
|
||||
// Convertir hebdomadaire en mensuel
|
||||
return $limite * $nb_semaines;
|
||||
}
|
||||
|
||||
// Déjà mensuel
|
||||
return $limite;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère un résumé formaté des disponibilités par langue
|
||||
* Version simplifiée pour affichage rapide
|
||||
*
|
||||
* @param int $langue_term_id ID du terme de la taxonomie langue
|
||||
* @param string|null $date Date de référence (Y-m-d). Si null, utilise la date actuelle
|
||||
* @param bool $use_cache Utiliser le cache transient (par défaut: true)
|
||||
* @return array ['matin' => 'X disponibles / Y total', 'apres_midi' => '...', 'journee' => '...', 'total' => '...']
|
||||
*/
|
||||
public static function getDisponibilitesSummaryByLangue(int $langue_term_id, ?string $date = null, bool $use_cache = true): array {
|
||||
$dispos = self::getDisponibilitesByLangue($langue_term_id, $date, $use_cache);
|
||||
|
||||
$summary = [];
|
||||
|
||||
foreach (['matin', 'apres_midi', 'journee'] as $periode) {
|
||||
if (isset($dispos['by_periode'][$periode . '_summary'])) {
|
||||
$data = $dispos['by_periode'][$periode . '_summary'];
|
||||
|
||||
if ($data['total'] > 0) {
|
||||
$summary[$periode] = sprintf(
|
||||
'%d disponibles / %d total (%d utilisés)',
|
||||
$data['available'],
|
||||
$data['total'],
|
||||
$data['used']
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$summary['total'] = sprintf(
|
||||
'%d disponibles / %d total (%d utilisés)',
|
||||
$dispos['total_available'],
|
||||
$dispos['total'],
|
||||
$dispos['total_used']
|
||||
);
|
||||
|
||||
return $summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère toutes les langues avec leurs disponibilités
|
||||
* Utile pour afficher un tableau récapitulatif
|
||||
*
|
||||
* @param string|null $date Date de référence (Y-m-d). Si null, utilise la date actuelle
|
||||
* @param bool $use_cache Utiliser le cache transient (par défaut: true)
|
||||
* @return array ['langue_slug' => ['name' => '...', 'total' => X, 'available' => Y, ...]]
|
||||
*/
|
||||
public static function getAllLanguesDisponibilites(?string $date = null, bool $use_cache = true): array {
|
||||
if (!$date) {
|
||||
$date = \current_time('Y-m-d');
|
||||
}
|
||||
|
||||
// Générer une clé de cache unique
|
||||
$cache_key = 'crvi_all_langues_dispos_' . $date;
|
||||
|
||||
// Essayer de récupérer depuis le cache
|
||||
if ($use_cache) {
|
||||
$cached = \get_transient($cache_key);
|
||||
if ($cached !== false) {
|
||||
return $cached;
|
||||
}
|
||||
}
|
||||
|
||||
// Récupérer toutes les langues utilisées dans les capacités actives
|
||||
$capacites = self::getActiveCapacites([], false);
|
||||
|
||||
$langues_ids = [];
|
||||
foreach ($capacites as $capacite) {
|
||||
$capacite_langues = is_array($capacite->langue) ? $capacite->langue : [$capacite->langue];
|
||||
$langues_ids = array_merge($langues_ids, $capacite_langues);
|
||||
}
|
||||
|
||||
$langues_ids = array_unique($langues_ids);
|
||||
|
||||
$result = [];
|
||||
|
||||
foreach ($langues_ids as $langue_id) {
|
||||
$langue_term = \get_term($langue_id, 'langue');
|
||||
if ($langue_term && !\is_wp_error($langue_term)) {
|
||||
// Utiliser le cache pour chaque langue
|
||||
$dispos = self::getDisponibilitesByLangue($langue_id, $date, $use_cache);
|
||||
|
||||
$result[$langue_term->slug] = [
|
||||
'id' => $langue_id,
|
||||
'name' => $langue_term->name,
|
||||
'slug' => $langue_term->slug,
|
||||
'total' => $dispos['total'],
|
||||
'used' => $dispos['total_used'],
|
||||
'available' => $dispos['total_available'],
|
||||
'by_periode' => [
|
||||
'matin' => $dispos['by_periode']['matin_summary'] ?? ['total' => 0, 'used' => 0, 'available' => 0],
|
||||
'apres_midi' => $dispos['by_periode']['apres_midi_summary'] ?? ['total' => 0, 'used' => 0, 'available' => 0],
|
||||
'journee' => $dispos['by_periode']['journee_summary'] ?? ['total' => 0, 'used' => 0, 'available' => 0],
|
||||
],
|
||||
'details' => $dispos['by_periode'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Trier par nom de langue
|
||||
uasort($result, function($a, $b) {
|
||||
return strcmp($a['name'], $b['name']);
|
||||
});
|
||||
|
||||
// Mettre en cache le résultat global (2 jours)
|
||||
if ($use_cache) {
|
||||
\set_transient($cache_key, $result, 2 * \DAY_IN_SECONDS);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalide le cache pour une capacité donnée
|
||||
* À appeler lors de la création/modification/suppression d'événements ou de capacités
|
||||
* @param int|null $capacite_id ID de la capacité (optionnel, invalide tout si null)
|
||||
*/
|
||||
public static function invalidate_cache(?int $capacite_id = null): void {
|
||||
global $wpdb;
|
||||
|
||||
if ($capacite_id) {
|
||||
// Invalider le cache de la capacité spécifique
|
||||
$capacite = self::load($capacite_id);
|
||||
if ($capacite) {
|
||||
// Invalider le cache pour toutes les langues de cette capacité
|
||||
$langue_ids = is_array($capacite->langue) ? $capacite->langue : [$capacite->langue];
|
||||
|
||||
foreach ($langue_ids as $langue_id) {
|
||||
// Récupérer toutes les clés de transient pour cette langue
|
||||
// Format: crvi_dispos_langue_{langue_id}_{date}
|
||||
$wpdb->query(
|
||||
$wpdb->prepare(
|
||||
"DELETE FROM {$wpdb->options}
|
||||
WHERE option_name LIKE %s
|
||||
OR option_name LIKE %s",
|
||||
'_transient_crvi_dispos_langue_' . $langue_id . '_%',
|
||||
'_transient_timeout_crvi_dispos_langue_' . $langue_id . '_%'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Invalider aussi le cache global de toutes les langues
|
||||
$wpdb->query(
|
||||
"DELETE FROM {$wpdb->options}
|
||||
WHERE option_name LIKE '_transient_crvi_all_langues_dispos_%'
|
||||
OR option_name LIKE '_transient_timeout_crvi_all_langues_dispos_%'"
|
||||
);
|
||||
}
|
||||
|
||||
// Invalider le cache général des capacités
|
||||
\delete_transient('crvi_capacites_actives_' . md5(serialize([])));
|
||||
} else {
|
||||
// Invalider tous les caches de disponibilités
|
||||
$wpdb->query(
|
||||
"DELETE FROM {$wpdb->options}
|
||||
WHERE option_name LIKE '_transient_crvi_dispos_langue_%'
|
||||
OR option_name LIKE '_transient_timeout_crvi_dispos_langue_%'
|
||||
OR option_name LIKE '_transient_crvi_all_langues_dispos_%'
|
||||
OR option_name LIKE '_transient_timeout_crvi_all_langues_dispos_%'
|
||||
OR option_name LIKE '_transient_crvi_count_used_%'
|
||||
OR option_name LIKE '_transient_timeout_crvi_count_used_%'"
|
||||
);
|
||||
|
||||
// Invalider le cache général des capacités
|
||||
\delete_transient('crvi_capacites_actives_' . md5(serialize([])));
|
||||
}
|
||||
}
|
||||
}
|
||||
84
app/models/Type_Intervention_Model.php
Normal file
84
app/models/Type_Intervention_Model.php
Normal file
@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ESI_CRVI_AGENDA\models;
|
||||
|
||||
class CRVI_Type_Intervention_Model extends Main_Model
|
||||
{
|
||||
/**
|
||||
* Schéma des champs ACF : nom => type (ex : group, repeater, taxonomy, text...)
|
||||
*/
|
||||
public static $acf_schema = [
|
||||
'nom' => 'text',
|
||||
];
|
||||
|
||||
/**
|
||||
* Charge un type d'intervention par ID (CPT)
|
||||
*/
|
||||
public static function load($id)
|
||||
{
|
||||
$post = get_post($id);
|
||||
if (!$post || $post->post_type !== 'type_intervention') {
|
||||
return null;
|
||||
}
|
||||
return $post;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne la liste de tous les types d'intervention (CPT)
|
||||
*/
|
||||
public static function all($simple_list = false)
|
||||
{
|
||||
$posts = get_posts([
|
||||
'post_type' => 'type_intervention',
|
||||
'posts_per_page' => -1,
|
||||
'post_status' => 'publish',
|
||||
]);
|
||||
|
||||
if ($simple_list) {
|
||||
$posts = array_map(function($post) {
|
||||
return [
|
||||
'id' => $post->ID,
|
||||
'nom' => $post->post_title,
|
||||
];
|
||||
}, $posts);
|
||||
}
|
||||
return $posts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée ou met à jour un type d'intervention par titre
|
||||
* @param array $data (doit contenir au moins 'post_title')
|
||||
* @return int ID du type d'intervention
|
||||
*/
|
||||
public static function create(array $data): int
|
||||
{
|
||||
if (empty($data['post_title'])) {
|
||||
return 0;
|
||||
}
|
||||
$existing = get_posts([
|
||||
'post_type' => 'type_intervention',
|
||||
'title' => $data['post_title'],
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => 1,
|
||||
'fields' => 'ids',
|
||||
]);
|
||||
if (!empty($existing)) {
|
||||
$post_id = (int)$existing[0];
|
||||
$update_data = array_merge([
|
||||
'ID' => $post_id,
|
||||
'post_type' => 'type_intervention',
|
||||
'post_status'=> 'publish',
|
||||
], $data);
|
||||
wp_update_post($update_data);
|
||||
return $post_id;
|
||||
}
|
||||
$insert_data = array_merge([
|
||||
'post_type' => 'type_intervention',
|
||||
'post_status'=> 'publish',
|
||||
], $data);
|
||||
$post_id = wp_insert_post($insert_data);
|
||||
return (int)$post_id;
|
||||
}
|
||||
}
|
||||
48
app/views/Agenda_View.php
Normal file
48
app/views/Agenda_View.php
Normal file
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace ESI_CRVI_AGENDA\views;
|
||||
|
||||
use ESI_CRVI_AGENDA\models\CRVI_Local_Model;
|
||||
use ESI_CRVI_AGENDA\models\CRVI_Intervenant_Model;
|
||||
use ESI_CRVI_AGENDA\models\CRVI_Departement_Model;
|
||||
use ESI_CRVI_AGENDA\models\CRVI_Traducteur_Model;
|
||||
use ESI_CRVI_AGENDA\helpers\Api_Helper;
|
||||
use ESI_CRVI_AGENDA\models\CRVI_Type_Intervention_Model;
|
||||
|
||||
class CRVI_Agenda_View {
|
||||
public static function render_agenda_page() {
|
||||
|
||||
$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'] ?? [];
|
||||
|
||||
// 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-page.php';
|
||||
if (file_exists($template)) {
|
||||
include $template;
|
||||
} else {
|
||||
echo '<p style="color:red">Template agenda-page.php introuvable.</p>';
|
||||
}
|
||||
}
|
||||
}
|
||||
10
app/views/Beneficiaire_View.php
Normal file
10
app/views/Beneficiaire_View.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
namespace ESI_CRVI_AGENDA\views;
|
||||
|
||||
class Beneficiaire_View {
|
||||
/**
|
||||
* La vue des bénéficiaires est gérée par le Custom Post Type WordPress.
|
||||
*/
|
||||
public function __construct() {}
|
||||
public function init() {}
|
||||
}
|
||||
10
app/views/Event_View.php
Normal file
10
app/views/Event_View.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
namespace ESI_CRVI_AGENDA\views;
|
||||
|
||||
class Event_View {
|
||||
/**
|
||||
* La vue des événements est gérée par FullCalendar (JS côté client).
|
||||
*/
|
||||
public function __construct() {}
|
||||
public function init() {}
|
||||
}
|
||||
10
app/views/Intervenant_View.php
Normal file
10
app/views/Intervenant_View.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
namespace ESI_CRVI_AGENDA\views;
|
||||
|
||||
class Intervenant_View {
|
||||
/**
|
||||
* La vue des intervenants est gérée par le Custom Post Type WordPress.
|
||||
*/
|
||||
public function __construct() {}
|
||||
public function init() {}
|
||||
}
|
||||
10
app/views/Local_View.php
Normal file
10
app/views/Local_View.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
namespace ESI_CRVI_AGENDA\views;
|
||||
|
||||
class Local_View {
|
||||
/**
|
||||
* La vue des locaux est gérée par le Custom Post Type WordPress.
|
||||
*/
|
||||
public function __construct() {}
|
||||
public function init() {}
|
||||
}
|
||||
14
app/views/Main_View.php
Normal file
14
app/views/Main_View.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace ESI_CRVI_AGENDA\views;
|
||||
|
||||
class CRVI_Main_View {
|
||||
public static function render_hub_admin_page() {
|
||||
$template = plugin_dir_path(__FILE__) . '../../templates/admin/admin_hub.php';
|
||||
if (file_exists($template)) {
|
||||
include $template;
|
||||
} else {
|
||||
echo '<p style="color:red">Template admin_hub.php introuvable.</p>';
|
||||
}
|
||||
}
|
||||
}
|
||||
10
app/views/Traducteur_View.php
Normal file
10
app/views/Traducteur_View.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
namespace ESI_CRVI_AGENDA\views;
|
||||
|
||||
class Traducteur_View {
|
||||
/**
|
||||
* La vue des traducteurs est gérée par le Custom Post Type WordPress.
|
||||
*/
|
||||
public function __construct() {}
|
||||
public function init() {}
|
||||
}
|
||||
1
assets/.gitkeep
Normal file
1
assets/.gitkeep
Normal file
@ -0,0 +1 @@
|
||||
|
||||
27
assets/css/admin-calendar-overrides.css
Normal file
27
assets/css/admin-calendar-overrides.css
Normal file
@ -0,0 +1,27 @@
|
||||
/* Améliorations UI du calendrier (admin) */
|
||||
/* Scope strict au conteneur admin du calendrier pour éviter tout effet de bord */
|
||||
|
||||
/* Augmente légèrement la hauteur minimale des lignes du mois */
|
||||
#agenda-calendar .fc .fc-daygrid-body {
|
||||
--fc-daygrid-row-min-height: 5.5em;
|
||||
}
|
||||
|
||||
/* Ajoute un peu d'air dans chaque case */
|
||||
#agenda-calendar .fc .fc-daygrid-day-frame {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
/* Chiffres des jours en gris et non soulignés (par défaut et au survol) */
|
||||
#agenda-calendar a.fc-daygrid-day-number,
|
||||
#agenda-calendar .fc-daygrid-day-number {
|
||||
color: #6c757d;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#agenda-calendar a.fc-daygrid-day-number:hover,
|
||||
#agenda-calendar .fc-daygrid-day-number:hover {
|
||||
text-decoration: none;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
|
||||
297
assets/css/agenda-events.css
Normal file
297
assets/css/agenda-events.css
Normal file
@ -0,0 +1,297 @@
|
||||
/* Styles pour les événements CRVI Agenda */
|
||||
|
||||
/* Styles de base pour tous les événements */
|
||||
.crvi-event {
|
||||
border-radius: 4px !important;
|
||||
border-left: 4px solid !important;
|
||||
font-weight: 500;
|
||||
font-size: 0.85em;
|
||||
padding: 2px 4px !important;
|
||||
margin: 1px 0 !important;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
/* Pastille de statut (cercle coloré) */
|
||||
.crvi-event::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
margin-right: 4px;
|
||||
background-color: var(--statut-color, #28a745);
|
||||
border: 1px solid rgba(255,255,255,0.8);
|
||||
}
|
||||
|
||||
/* Styles par type de RDV (fond) */
|
||||
.crvi-type-individuel {
|
||||
background-color: #f8f9fa !important;
|
||||
border-color: #dee2e6 !important;
|
||||
}
|
||||
|
||||
.crvi-type-groupe {
|
||||
background-color: #e9ecef !important;
|
||||
border-color: #ced4da !important;
|
||||
}
|
||||
|
||||
/* Styles par statut (pastille) */
|
||||
.crvi-statut-prevu::before {
|
||||
background-color: #28a745 !important;
|
||||
}
|
||||
|
||||
.crvi-statut-annule::before {
|
||||
background-color: #dc3545 !important;
|
||||
}
|
||||
|
||||
.crvi-statut-non_tenu::before {
|
||||
background-color: #ffc107 !important;
|
||||
}
|
||||
|
||||
.crvi-statut-cloture::before {
|
||||
background-color: #6c757d !important;
|
||||
}
|
||||
|
||||
.crvi-statut-absence::before {
|
||||
background-color: #fd7e14 !important;
|
||||
}
|
||||
|
||||
/* Styles par département (pour les icônes personnalisées) */
|
||||
.crvi-departement-soutien-social {
|
||||
/* Styles spécifiques pour le département Soutien Social */
|
||||
}
|
||||
|
||||
.crvi-departement-accompagnement {
|
||||
/* Styles spécifiques pour le département Accompagnement */
|
||||
}
|
||||
|
||||
.crvi-departement-formation {
|
||||
/* Styles spécifiques pour le département Formation */
|
||||
}
|
||||
|
||||
.crvi-departement-consultation {
|
||||
/* Styles spécifiques pour le département Consultation */
|
||||
}
|
||||
|
||||
.crvi-departement-urgence {
|
||||
/* Styles spécifiques pour le département Urgence */
|
||||
}
|
||||
|
||||
/* Hover effects */
|
||||
.crvi-event:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.15);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
/* Styles pour les événements en mode liste */
|
||||
.fc-list-event.crvi-event {
|
||||
padding: 8px 12px !important;
|
||||
margin: 2px 0 !important;
|
||||
}
|
||||
|
||||
.fc-list-event.crvi-event::before {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* Styles pour les événements en mode jour/semaine */
|
||||
.fc-daygrid-event.crvi-event,
|
||||
.fc-timegrid-event.crvi-event {
|
||||
border-left-width: 6px !important;
|
||||
padding: 3px 6px !important;
|
||||
}
|
||||
|
||||
.fc-daygrid-event.crvi-event::before,
|
||||
.fc-timegrid-event.crvi-event::before {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.crvi-event {
|
||||
font-size: 0.8em;
|
||||
padding: 1px 3px !important;
|
||||
}
|
||||
|
||||
.crvi-event::before {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Animation pour les nouveaux événements */
|
||||
@keyframes crviEventAppear {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.crvi-event-new {
|
||||
animation: crviEventAppear 0.3s ease-out;
|
||||
}
|
||||
|
||||
/* Styles pour les événements sélectionnés */
|
||||
.crvi-event.fc-event-selected {
|
||||
box-shadow: 0 0 0 2px rgba(0,123,255,0.5);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
/* Styles pour les événements en cours */
|
||||
.crvi-event.crvi-en-cours {
|
||||
border-left-color: #007bff !important;
|
||||
background-color: rgba(0,123,255,0.1) !important;
|
||||
}
|
||||
|
||||
/* Styles pour les événements en retard */
|
||||
.crvi-event.crvi-en-retard {
|
||||
border-left-color: #dc3545 !important;
|
||||
background-color: rgba(220,53,69,0.1) !important;
|
||||
}
|
||||
|
||||
/* Styles pour les événements terminés */
|
||||
.crvi-event.crvi-termine {
|
||||
opacity: 0.7;
|
||||
background-color: rgba(108,117,125,0.1) !important;
|
||||
}
|
||||
|
||||
/* Icônes de département dans le titre */
|
||||
.crvi-departement-icon {
|
||||
font-size: 1.1em;
|
||||
margin-right: 4px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Tooltip personnalisé pour les événements */
|
||||
.crvi-event-tooltip {
|
||||
position: absolute;
|
||||
background: rgba(0,0,0,0.9);
|
||||
color: white;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.85em;
|
||||
z-index: 1000;
|
||||
max-width: 300px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.crvi-event-tooltip::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
left: 10px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
border-bottom: 4px solid rgba(0,0,0,0.9);
|
||||
}
|
||||
|
||||
/* Styles pour les filtres visuels */
|
||||
.crvi-filter-legend {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.crvi-filter-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.9em;
|
||||
padding: 4px 8px;
|
||||
border-radius: 3px;
|
||||
background: white;
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.crvi-filter-color {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
margin-right: 6px;
|
||||
border: 1px solid rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
/* Styles pour les statistiques visuelles */
|
||||
.crvi-stats-bar {
|
||||
height: 4px;
|
||||
background: #e9ecef;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
.crvi-stats-fill {
|
||||
height: 100%;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.crvi-stats-fill.prevu { background-color: #28a745; }
|
||||
.crvi-stats-fill.annule { background-color: #dc3545; }
|
||||
.crvi-stats-fill.non_tenu { background-color: #ffc107; }
|
||||
.crvi-stats-fill.cloture { background-color: #6c757d; }
|
||||
.crvi-stats-fill.absence { background-color: #fd7e14; }
|
||||
|
||||
.fade.show {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.modal-backdrop.fade.show {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.popover.event-popover.fade,
|
||||
.popover.event-popover.fade.show {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
/* Effets de survol pour les boutons de statut */
|
||||
#eventStatusButtons .btn {
|
||||
transition: all 0.3s ease;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
#eventStatusButtons .btn:hover {
|
||||
transform: scale(1.05);
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
/* Styles pour les événements passés (non éditables) */
|
||||
.fc-event.event-past {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
|
||||
.fc-event.event-past .fc-event-title {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.fc-event.event-past:hover {
|
||||
opacity: 0.6;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* Désactiver le curseur de déplacement pour les événements passés */
|
||||
.fc-event.event-past .fc-event-resizer {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.fc-event.event-past .fc-event-main {
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
145
assets/css/crvi-agenda.css
Normal file
145
assets/css/crvi-agenda.css
Normal file
@ -0,0 +1,145 @@
|
||||
/* Styles pour le popover des événements */
|
||||
.event-popover {
|
||||
max-width: 300px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.event-popover .popover-header {
|
||||
background-color: #f8f9fa;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
font-weight: 600;
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
.event-popover .popover-body {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.event-popover h6 {
|
||||
color: #495057;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.event-popover .mb-1 {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.event-popover .mb-2 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.event-popover .mt-2 {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.event-popover strong {
|
||||
color: #495057;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.event-popover .text-muted {
|
||||
color: #6c757d !important;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
/* Animation du popover */
|
||||
.popover {
|
||||
animation: fadeIn 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.fade.show {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Forcer l'opacité du popover d'événement en front,
|
||||
même si une règle .fade externe met opacity:0 */
|
||||
.popover.event-popover.fade,
|
||||
.popover.event-popover.fade.show {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Styles pour le lien "plus" des événements */
|
||||
.fc-daygrid-more-link {
|
||||
background-color: #007bff !important;
|
||||
color: white !important;
|
||||
border-radius: 3px !important;
|
||||
padding: 2px 6px !important;
|
||||
font-size: 0.75rem !important;
|
||||
font-weight: 500 !important;
|
||||
text-decoration: none !important;
|
||||
display: inline-block !important;
|
||||
margin-top: 2px !important;
|
||||
transition: background-color 0.2s ease !important;
|
||||
}
|
||||
|
||||
.fc-daygrid-more-link:hover {
|
||||
background-color: #0056b3 !important;
|
||||
color: white !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
/* Style pour le popover des événements cachés */
|
||||
.fc-more-popover {
|
||||
max-width: 400px !important;
|
||||
border-radius: 8px !important;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
|
||||
}
|
||||
|
||||
.fc-more-popover .fc-popover-header {
|
||||
background-color: #f8f9fa !important;
|
||||
border-bottom: 1px solid #dee2e6 !important;
|
||||
padding: 0.75rem 1rem !important;
|
||||
font-weight: 600 !important;
|
||||
color: #495057 !important;
|
||||
}
|
||||
|
||||
.fc-more-popover .fc-popover-body {
|
||||
padding: 0.75rem 1rem !important;
|
||||
max-height: 300px !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
.fc-more-popover .fc-event {
|
||||
margin-bottom: 0.5rem !important;
|
||||
padding: 0.5rem !important;
|
||||
border-radius: 4px !important;
|
||||
cursor: pointer !important;
|
||||
transition: background-color 0.2s ease !important;
|
||||
}
|
||||
|
||||
.fc-more-popover .fc-event:hover {
|
||||
opacity: 0.8 !important;
|
||||
}
|
||||
|
||||
.fc-more-popover .fc-event:last-child {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
/* Responsive pour les petits écrans */
|
||||
@media (max-width: 768px) {
|
||||
.event-popover {
|
||||
max-width: 250px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.fc-daygrid-more-link {
|
||||
font-size: 0.7rem !important;
|
||||
padding: 1px 4px !important;
|
||||
}
|
||||
|
||||
.fc-more-popover {
|
||||
max-width: 300px !important;
|
||||
}
|
||||
}
|
||||
391
assets/css/crvi.css
Normal file
391
assets/css/crvi.css
Normal file
@ -0,0 +1,391 @@
|
||||
.crvi-hub-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: repeat(3, 1fr);
|
||||
gap: 2rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
.crvi-hub-block {
|
||||
background: #fff;
|
||||
border: 1px solid #e5e5e5;
|
||||
border-radius: 6px;
|
||||
padding: 2rem 1.5rem;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.03);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.crvi-hub-block h2 {
|
||||
margin-top: 0;
|
||||
font-size: 1.1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.crvi-hub-block .button {
|
||||
width: 100%;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
.crvi-hub-block form {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Styles pour l'agenda */
|
||||
.agenda-container {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.filters-container {
|
||||
background: #fff;
|
||||
border: 1px solid #e5e5e5;
|
||||
border-radius: 6px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.03);
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.filter {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filter label {
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.5rem;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.filter input,
|
||||
.filter select {
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.filter button {
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.filter button[type="submit"] {
|
||||
background-color: #0073aa;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.filter button[type="submit"]:hover {
|
||||
background-color: #005a87;
|
||||
}
|
||||
|
||||
/* Boutons de filtres */
|
||||
.filter button.btn-primary {
|
||||
background-color: #0073aa;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.filter button.btn-primary:hover {
|
||||
background-color: #005a87;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.filter button.btn-secondary {
|
||||
background-color: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.filter button.btn-secondary:hover {
|
||||
background-color: #545b62;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Indicateur de chargement des filtres */
|
||||
#loading-indicator {
|
||||
display: none;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 4px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
#loading-indicator p {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Animation de chargement */
|
||||
@keyframes pulse {
|
||||
0% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
#loading-indicator p {
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
/* Bouton "Ajouter un RDV" */
|
||||
#addEventBtn {
|
||||
background-color: #28a745 !important;
|
||||
color: white !important;
|
||||
border: none !important;
|
||||
padding: 0.5rem 1rem !important;
|
||||
border-radius: 4px !important;
|
||||
font-size: 0.9rem !important;
|
||||
cursor: pointer !important;
|
||||
transition: background-color 0.2s !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
gap: 0.5rem !important;
|
||||
}
|
||||
|
||||
#addEventBtn:hover {
|
||||
background-color: #218838 !important;
|
||||
}
|
||||
|
||||
#addEventBtn i {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.filters {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.filter button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Styles pour le bloc de vue avec grille Bootstrap en 2 colonnes */
|
||||
#eventViewBlock .event-grid {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#eventViewBlock .event-grid > .row {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
#eventViewBlock .event-grid .row .row {
|
||||
margin-bottom: 0.5rem;
|
||||
align-items: center;
|
||||
min-height: 2.5rem;
|
||||
border-bottom: 1px solid #f8f9fa;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
#eventViewBlock .event-grid .row .row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
#eventViewBlock .event-label {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Icônes :before commentées - on utilise maintenant les balises <i> */
|
||||
/*
|
||||
#eventViewBlock .event-label::before {
|
||||
margin-right: 0.5rem;
|
||||
font-size: 1rem;
|
||||
opacity: 0.8;
|
||||
width: 16px;
|
||||
text-align: center;
|
||||
font-family: "Font Awesome 5 Free", "Font Awesome 5 Pro", "Font Awesome 6 Free", "Font Awesome 6 Pro";
|
||||
}
|
||||
|
||||
#eventViewBlock .event-label.date::before { content: "\f073"; font-weight: 900; }
|
||||
#eventViewBlock .event-label.time::before { content: "\f017"; font-weight: 900; }
|
||||
#eventViewBlock .event-label.type::before { content: "\f007"; font-weight: 900; }
|
||||
#eventViewBlock .event-label.language::before { content: "\f1ab"; font-weight: 900; }
|
||||
#eventViewBlock .event-label.beneficiary::before { content: "\f0c0"; font-weight: 900; }
|
||||
#eventViewBlock .event-label.intervenant::before { content: "\f0f0"; font-weight: 900; }
|
||||
#eventViewBlock .event-label.translator::before { content: "\f1e0"; font-weight: 900; }
|
||||
#eventViewBlock .event-label.location::before { content: "\f015"; font-weight: 900; }
|
||||
#eventViewBlock .event-label.participants::before { content: "\f0c0"; font-weight: 900; }
|
||||
#eventViewBlock .event-label.men::before { content: "\f183"; font-weight: 900; }
|
||||
#eventViewBlock .event-label.women::before { content: "\f182"; font-weight: 900; }
|
||||
#eventViewBlock .event-label.comment::before { content: "\f075"; font-weight: 900; }
|
||||
|
||||
#eventViewBlock .event-label.has-dept-icon::before {
|
||||
content: var(--dept-icon);
|
||||
font-weight: normal;
|
||||
}
|
||||
*/
|
||||
|
||||
#eventViewBlock .event-value {
|
||||
font-size: 0.9rem;
|
||||
margin: 0;
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
/* Pastille de statut dans le header du modal */
|
||||
#eventModal .modal-header {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#eventModal .statut-badge {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 50px;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid white;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.event-status {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
color: #fff;
|
||||
background-color: #6c757d;
|
||||
}
|
||||
|
||||
/* Styles pour les liens "+ Ajouter" */
|
||||
.crvi-add-link {
|
||||
font-size: 0.8rem;
|
||||
color: #6b7c93;
|
||||
text-decoration: none;
|
||||
font-weight: 400;
|
||||
transition: color 0.2s ease;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.crvi-add-link:hover {
|
||||
color: #4a5a6b;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Augmentation de la taille des icônes FontAwesome de 25% pour l'ensemble de l'agenda */
|
||||
.agenda-container .fas,
|
||||
.agenda-container .far,
|
||||
.agenda-container .fab,
|
||||
.agenda-container .fal,
|
||||
#eventModal .fas,
|
||||
#eventModal .far,
|
||||
#eventModal .fab,
|
||||
#eventModal .fal,
|
||||
.crvi-add-link .fas,
|
||||
.crvi-add-link .far,
|
||||
.crvi-add-link .fab,
|
||||
.crvi-add-link .fal {
|
||||
font-size: 1.25em !important;
|
||||
}
|
||||
|
||||
.plus-label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.plus-label a {
|
||||
color: #0097e6;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.plus-label a:hover {
|
||||
color: #0073aa;
|
||||
}
|
||||
|
||||
.plus-label a i {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.modal .select2-container {
|
||||
z-index: 2000!important;
|
||||
width: 100%
|
||||
}
|
||||
|
||||
.modal .select2-dropdown,.modal .select2-results,.modal.show .select2-container--open .select2-dropdown,.entity-create-modal .select2-container,.entity-create-modal .select2-dropdown,.entity-create-modal .select2-results {
|
||||
z-index: 2000!important;
|
||||
}
|
||||
|
||||
.entity-create-modal .select2-container,.entity-create-modal .select2-dropdown,.entity-create-modal .select2-results {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
body>.select2-container {
|
||||
z-index: 2000!important
|
||||
}
|
||||
|
||||
body>.select2-dropdown {
|
||||
z-index: 2000!important
|
||||
}
|
||||
|
||||
.modal.show~.select2-container,.modal.show~.select2-dropdown {
|
||||
z-index: 2000!important
|
||||
}
|
||||
|
||||
/* Contexte front uniquement (shortcode agenda intervenant) :
|
||||
Forcer le Select2 à passer au-dessus du modal Avada (z-index modal ~ 99999) */
|
||||
.crvi-intervenant-agenda #eventModal .select2-container,
|
||||
.crvi-intervenant-agenda #eventModal .select2-dropdown,
|
||||
.crvi-intervenant-agenda #eventModal .select2-results {
|
||||
z-index: 100002 !important;
|
||||
}
|
||||
|
||||
/* Contexte front: forcer affichage et largeur des conteneurs Select2 */
|
||||
.crvi-intervenant-agenda .select2-container {
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
/* Contexte front: couleur de police foncée pour la grille du modal */
|
||||
.crvi-intervenant-agenda #eventModal .event-grid {
|
||||
color: #333 !important;
|
||||
}
|
||||
.crvi-intervenant-agenda #eventModal .event-grid .event-label,
|
||||
.crvi-intervenant-agenda #eventModal .event-grid .event-value {
|
||||
color: #333 !important;
|
||||
}
|
||||
|
||||
/* Correction du z-index de TOUTES les modales et backdrop
|
||||
|
||||
SOLUTION : Combinaison JavaScript + CSS
|
||||
- JavaScript force toutes les modales et backdrop à être enfants directs du <body>
|
||||
- JavaScript réordonne : backdrop PUIS modal dans le DOM
|
||||
- CSS force des z-index élevés pour passer au-dessus d'Avada
|
||||
|
||||
Avec cette approche, les stacking contexts sont correctement gérés pour :
|
||||
- eventModal (modal principale)
|
||||
- createBeneficiaireModal, createIntervenantModal, createTraducteurModal, createLocalModal
|
||||
- declarationIncidentModal
|
||||
- beneficiaireHistoriqueModal
|
||||
- rdvModal
|
||||
*/
|
||||
|
||||
/* Backdrop : sous toutes les modales, au-dessus d'Avada */
|
||||
body.modal-open .modal-backdrop {
|
||||
z-index: 99999 !important;
|
||||
}
|
||||
|
||||
/* TOUTES les modales : au-dessus du backdrop et d'Avada */
|
||||
.modal {
|
||||
z-index: 100000 !important;
|
||||
position: fixed !important;
|
||||
}
|
||||
11
assets/css/crvi_main.css
Normal file
11
assets/css/crvi_main.css
Normal file
@ -0,0 +1,11 @@
|
||||
/* CSS principal du plugin CRVI Agenda */
|
||||
|
||||
/* Les librairies CSS sont importées depuis crvi_libraries.css */
|
||||
|
||||
|
||||
|
||||
/* CSS personnalisé du plugin */
|
||||
@import './crvi.css';
|
||||
@import './agenda-events.css';
|
||||
/* Overrides admin calendrier (sera minifié avec le reste via Vite) */
|
||||
@import './admin-calendar-overrides.css';
|
||||
76
assets/css/intervenant-profile.css
Normal file
76
assets/css/intervenant-profile.css
Normal file
@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Styles pour la page de profil intervenant
|
||||
*/
|
||||
|
||||
.crvi-intervenant-profile {
|
||||
min-height: 60vh;
|
||||
}
|
||||
|
||||
.form-control[readonly] {
|
||||
background-color: #e9ecef;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.langues-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.badge-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.badge-container .badge {
|
||||
font-size: 0.875rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
.indisponibilites-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.indisponibilites-list li {
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
background-color: #f8f9fa;
|
||||
border-left: 4px solid #ffc107;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.indisponibilite-item {
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
background-color: #f8f9fa;
|
||||
border-left: 4px solid #ffc107;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.indisponibilite-item .btn-remove {
|
||||
color: #dc3545;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
|
||||
.indisponibilite-item .btn-remove:hover {
|
||||
color: #bb2d3b;
|
||||
}
|
||||
|
||||
.input-group .btn {
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.col-lg-8 {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
35
assets/css/presence-autocomplete.css
Normal file
35
assets/css/presence-autocomplete.css
Normal file
@ -0,0 +1,35 @@
|
||||
/* Styles pour l'autocomplétion des présences dans la modal de validation des présences */
|
||||
|
||||
.presence-nom-input {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.autocomplete-suggestions {
|
||||
position: absolute;
|
||||
z-index: 1050;
|
||||
background: white;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||||
margin-top: 2px;
|
||||
width: 100%;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.autocomplete-item {
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.autocomplete-item:hover,
|
||||
.autocomplete-item.selected {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.autocomplete-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
149
assets/css/traduction-langue-admin.css
Normal file
149
assets/css/traduction-langue-admin.css
Normal file
@ -0,0 +1,149 @@
|
||||
/**
|
||||
* Styles pour l'interface admin des capacités de traduction
|
||||
*/
|
||||
|
||||
/* Metabox des règles dépendantes */
|
||||
.traduction-langue-child-rules {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.traduction-langue-child-rules h4 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 13px;
|
||||
color: #1d2327;
|
||||
}
|
||||
|
||||
.traduction-langue-child-rules ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.traduction-langue-child-rules li {
|
||||
margin: 0 0 8px 0;
|
||||
padding: 8px;
|
||||
background: #f6f7f7;
|
||||
border-left: 3px solid #2271b1;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.traduction-langue-child-rules li a {
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.traduction-langue-child-rules li a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Bouton d'ajout de règle */
|
||||
.traduction-langue-add-sub-rule {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.traduction-langue-add-sub-rule .spinner {
|
||||
visibility: hidden;
|
||||
margin: 0 0 0 10px;
|
||||
}
|
||||
|
||||
.traduction-langue-add-sub-rule .spinner.is-active {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
#sub-rule-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 160000;
|
||||
}
|
||||
|
||||
.sub-rule-modal-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
.sub-rule-modal-content {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: #fff;
|
||||
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.3);
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
padding: 20px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.sub-rule-modal-content h2 {
|
||||
margin: 0 0 15px 0;
|
||||
font-size: 18px;
|
||||
line-height: 1.4;
|
||||
color: #1d2327;
|
||||
}
|
||||
|
||||
.sub-rule-modal-content p {
|
||||
margin: 0 0 15px 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: #50575e;
|
||||
}
|
||||
|
||||
.sub-rule-modal-content ul {
|
||||
margin: 0 0 15px 20px;
|
||||
padding: 0;
|
||||
list-style: disc;
|
||||
}
|
||||
|
||||
.sub-rule-modal-content li {
|
||||
margin: 0 0 5px 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: #50575e;
|
||||
}
|
||||
|
||||
.sub-rule-modal-actions {
|
||||
margin-top: 20px;
|
||||
text-align: right;
|
||||
border-top: 1px solid #dcdcde;
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
.sub-rule-modal-actions .button {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
/* Désactiver le scroll sur le body quand la modal est ouverte */
|
||||
body.modal-open {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media screen and (max-width: 782px) {
|
||||
.sub-rule-modal-content {
|
||||
max-width: 90%;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.sub-rule-modal-content h2 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.sub-rule-modal-actions {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sub-rule-modal-actions .button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin: 0 0 8px 0;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
282
assets/css/traduction-langue-list.css
Normal file
282
assets/css/traduction-langue-list.css
Normal file
@ -0,0 +1,282 @@
|
||||
/**
|
||||
* Styles pour la page admin de liste des capacités de traduction
|
||||
* Interface moderne avec vue Parent → Enfant
|
||||
*/
|
||||
|
||||
.traduction-langue-list-wrap {
|
||||
padding: 20px 20px 40px;
|
||||
max-width: 1400px;
|
||||
}
|
||||
|
||||
.traduction-langue-list-wrap h1 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 28px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.traduction-langue-list-wrap h1 .dashicons {
|
||||
font-size: 32px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
/* Grid des cartes */
|
||||
.traduction-langue-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(450px, 1fr));
|
||||
gap: 24px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
/* Carte de capacité */
|
||||
.capacite-card {
|
||||
background: #fff;
|
||||
border: 1px solid #dcdcde;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.capacite-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
border-color: #2271b1;
|
||||
}
|
||||
|
||||
.capacite-card.inactive {
|
||||
opacity: 0.7;
|
||||
background: #f6f7f7;
|
||||
}
|
||||
|
||||
.capacite-card.full {
|
||||
border-left: 4px solid #d63638;
|
||||
}
|
||||
|
||||
/* Header de la carte */
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid #f0f0f1;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.card-title h2 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 8px 0;
|
||||
color: #1d2327;
|
||||
}
|
||||
|
||||
.card-badges {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.badge-inactive {
|
||||
background: #f0f0f1;
|
||||
color: #50575e;
|
||||
}
|
||||
|
||||
.badge-inactive-small {
|
||||
padding: 2px 6px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.badge-warning {
|
||||
background: #fcf3cf;
|
||||
color: #856404;
|
||||
border: 1px solid #f9e79f;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.card-actions .button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.card-actions .button .dashicons {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
/* Body de la carte */
|
||||
.card-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.info-item-full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #646970;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #1d2327;
|
||||
}
|
||||
|
||||
.info-value .na {
|
||||
color: #a7aaad;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Badges de langues multiples */
|
||||
.langue-badges {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.langue-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 10px;
|
||||
background: #2271b1;
|
||||
color: #fff;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.langue-badge:first-child {
|
||||
background: #135e96;
|
||||
}
|
||||
|
||||
.langue-badge:nth-child(2) {
|
||||
background: #2271b1;
|
||||
}
|
||||
|
||||
.langue-badge:nth-child(3) {
|
||||
background: #3582c4;
|
||||
}
|
||||
|
||||
/* Badge d'exception */
|
||||
.exception-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 6px 12px;
|
||||
background: #fef5e7;
|
||||
color: #856404;
|
||||
border: 1px solid #f9e79f;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Barre d'utilisation */
|
||||
.usage-bar {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.usage-label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.usage-label > span:first-child {
|
||||
font-weight: 600;
|
||||
color: #1d2327;
|
||||
}
|
||||
|
||||
.usage-stats {
|
||||
color: #50575e;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.usage-progress {
|
||||
height: 8px;
|
||||
background: #f0f0f1;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.usage-progress-bar {
|
||||
height: 100%;
|
||||
background: #2271b1;
|
||||
transition: width 0.3s ease;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.usage-progress-bar.warning {
|
||||
background: #f0b849;
|
||||
}
|
||||
|
||||
.usage-progress-bar.full {
|
||||
background: #d63638;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media screen and (max-width: 1200px) {
|
||||
.traduction-langue-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 782px) {
|
||||
.traduction-langue-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
12
assets/js/agenda.js
Normal file
12
assets/js/agenda.js
Normal file
@ -0,0 +1,12 @@
|
||||
// agenda.js
|
||||
// Fichier principal pour la gestion JS de l'agenda admin
|
||||
export default function Agenda() {
|
||||
console.log('agenda.js chargé');
|
||||
}
|
||||
|
||||
// À compléter : logique de récupération dynamique des options de filtres, gestion des événements, etc.
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Exemple : point d'entrée pour l'initialisation future
|
||||
console.log('agenda.js chargé');
|
||||
});
|
||||
15
assets/js/crvi_libraries.js
Normal file
15
assets/js/crvi_libraries.js
Normal file
@ -0,0 +1,15 @@
|
||||
// Librairies JS externes
|
||||
// Import des CSS via imports directs (seront extraits automatiquement par Vite)
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import 'toastr/build/toastr.min.css';
|
||||
import 'select2/dist/css/select2.css';
|
||||
|
||||
import * as bootstrap from 'bootstrap';
|
||||
window.bootstrap = bootstrap; // Pour activer les composants JS de Bootstrap
|
||||
|
||||
import toastr from 'toastr';
|
||||
window.toastr = toastr; // Exposer toastr globalement
|
||||
|
||||
// FullCalendar CSS - chargé depuis CDN
|
||||
// Select2 JS est chargé depuis CDN dans Plugin.php
|
||||
|
||||
172
assets/js/crvi_main.js
Normal file
172
assets/js/crvi_main.js
Normal file
@ -0,0 +1,172 @@
|
||||
// Les librairies JS sont importées depuis crvi_libraries.js
|
||||
|
||||
|
||||
|
||||
// Import de vos modules locaux
|
||||
/* import './extra/agenda-loading-feedback.js'; */
|
||||
import { initializeCalendar } from './modules/agenda-fullcalendar.js';
|
||||
import { initializeIntervenantCalendar, initializeIntervenantAgendaTabs } from './modules/agenda-intervenant-calendar.js';
|
||||
import { initializeIntervenantHub } from './modules/agenda-intervenant-hub.js';
|
||||
import { initializeProfile } from './modules/agenda-intervenant-profile.js';
|
||||
import { initializePermanences } from './modules/agenda-intervenant-permanences.js';
|
||||
import { initializeAdminPermanences } from './modules/agenda-admin-permanences.js';
|
||||
import { initStatsTable } from './modules/agenda-stats-table.js';
|
||||
|
||||
/**
|
||||
* FIX GLOBAL : Corriger le stacking context de TOUTES les modales Bootstrap
|
||||
* pour éviter que le backdrop passe devant les modales (problème avec Avada)
|
||||
*/
|
||||
function fixAllModalsStackingContext() {
|
||||
// Attendre que Bootstrap soit chargé
|
||||
if (typeof window.bootstrap === 'undefined') {
|
||||
setTimeout(fixAllModalsStackingContext, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
// Récupérer TOUTES les modales Bootstrap
|
||||
const allModals = document.querySelectorAll('.modal');
|
||||
|
||||
allModals.forEach(modal => {
|
||||
// 1. Forcer la modal à être enfant direct du body (échappe au stacking context Avada)
|
||||
if (modal.parentNode !== document.body) {
|
||||
document.body.appendChild(modal);
|
||||
}
|
||||
|
||||
// 2. Fixer l'ordre backdrop/modal à chaque ouverture
|
||||
modal.addEventListener('shown.bs.modal', function fixBackdropOrder() {
|
||||
const backdrop = document.querySelector('.modal-backdrop');
|
||||
|
||||
if (backdrop && modal && document.body && document.body.children) {
|
||||
try {
|
||||
// Vérifier l'ordre dans le DOM
|
||||
const bodyChildren = Array.from(document.body.children);
|
||||
const modalIndex = bodyChildren.indexOf(modal);
|
||||
const backdropIndex = bodyChildren.indexOf(backdrop);
|
||||
|
||||
// Si backdrop vient après modal, réordonner (backdrop AVANT modal)
|
||||
if (backdropIndex > modalIndex && modalIndex !== -1 && backdropIndex !== -1) {
|
||||
document.body.insertBefore(backdrop, modal);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Erreur lors de la réorganisation du backdrop:', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
console.log(`✅ Fix stacking context appliqué à ${allModals.length} modal(es)`);
|
||||
}
|
||||
|
||||
// Initialisation au chargement de la page
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('DOM chargé, initialisation des modules...');
|
||||
console.log('CRVI crviACFData', crviACFData);
|
||||
/* configureToastr(); */
|
||||
|
||||
// ✨ FIX : Appliquer le fix stacking context à TOUTES les modales
|
||||
fixAllModalsStackingContext();
|
||||
|
||||
// Détecter le contexte : intervenant ou admin
|
||||
const isIntervenantContext = detectIntervenantContext();
|
||||
|
||||
if (isIntervenantContext) {
|
||||
console.log('🔄 Contexte détecté : Espace Intervenant');
|
||||
|
||||
// Initialiser les modules selon la page
|
||||
const hubEl = document.getElementById('intervenant-hub-container');
|
||||
if (hubEl) {
|
||||
console.log('📋 Initialisation du Hub intervenant...');
|
||||
initializeIntervenantHub();
|
||||
}
|
||||
|
||||
const agendaEl = document.getElementById('intervenant-agenda-container');
|
||||
if (agendaEl) {
|
||||
console.log('📅 Initialisation de l\'agenda intervenant...');
|
||||
// Vérifier si on a des onglets (agenda + collègues) ou juste l'agenda simple
|
||||
const agendaTabs = document.getElementById('agendaTabs');
|
||||
if (agendaTabs) {
|
||||
console.log('📑 Onglets détectés, initialisation avec basculement...');
|
||||
initializeIntervenantAgendaTabs();
|
||||
} else {
|
||||
console.log('📅 Mode simple, initialisation agenda seul...');
|
||||
initializeIntervenantCalendar();
|
||||
}
|
||||
}
|
||||
|
||||
const profileEl = document.getElementById('intervenant-profile-container');
|
||||
if (profileEl) {
|
||||
console.log('👤 Initialisation du profil intervenant...');
|
||||
initializeProfile();
|
||||
}
|
||||
|
||||
const permanencesEl = document.getElementById('intervenant-permanences-container');
|
||||
if (permanencesEl) {
|
||||
console.log('📝 Initialisation du formulaire permanences...');
|
||||
initializePermanences();
|
||||
}
|
||||
} else {
|
||||
// Contexte admin - vérifier quelle page on est
|
||||
console.log('🔄 Contexte détecté : Admin');
|
||||
|
||||
// Vérifier si on est sur la page stats (priorité)
|
||||
const statsTableEl = document.getElementById('stats-events-table');
|
||||
const statsFiltersEl = document.querySelector('.stats-filters-container');
|
||||
if (statsTableEl || statsFiltersEl) {
|
||||
console.log('📊 Initialisation du tableau de stats...');
|
||||
initStatsTable();
|
||||
} else {
|
||||
// Sinon, vérifier si on est sur la page agenda avec calendrier
|
||||
const calendarEl = document.getElementById('agenda-calendar');
|
||||
if (calendarEl) {
|
||||
console.log('📅 Initialisation du calendrier admin...');
|
||||
const calendar = initializeCalendar();
|
||||
if (calendar) {
|
||||
console.log('✅ Calendrier initialisé avec succès');
|
||||
} else {
|
||||
console.warn('⚠️ Échec de l\'initialisation du calendrier');
|
||||
}
|
||||
|
||||
const modal = document.getElementById('eventModal');
|
||||
if (modal) {
|
||||
// Vérifier que bootstrap est disponible
|
||||
if (typeof window.bootstrap !== 'undefined' && window.bootstrap.Modal) {
|
||||
const bsModal = new window.bootstrap.Modal(modal);
|
||||
} else {
|
||||
console.warn('Bootstrap n\'est pas encore chargé, modal ne sera pas initialisé');
|
||||
}
|
||||
} else {
|
||||
console.warn('Modal eventModal non trouvé (normal si pas sur page agenda admin)');
|
||||
}
|
||||
} else {
|
||||
console.log('ℹ️ Élément agenda-calendar non trouvé (normal si pas sur page agenda)');
|
||||
}
|
||||
}
|
||||
|
||||
// Vérifier si on est sur la page admin permanences
|
||||
const adminPermanencesEl = document.getElementById('admin-permanences-container');
|
||||
if (adminPermanencesEl) {
|
||||
console.log('📝 Initialisation du formulaire permanences admin...');
|
||||
initializeAdminPermanences();
|
||||
}
|
||||
}
|
||||
|
||||
// Initialiser Select2 sur les <select> (commun à tous les contextes)
|
||||
jQuery('.select2').select2();
|
||||
|
||||
});
|
||||
|
||||
/**
|
||||
* Détecte si on est dans un contexte intervenant
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function detectIntervenantContext() {
|
||||
// Vérifier la présence des conteneurs intervenant
|
||||
const intervenantContainers = [
|
||||
'intervenant-hub-container',
|
||||
'intervenant-agenda-container',
|
||||
'intervenant-profile-container',
|
||||
'intervenant-permanences-container'
|
||||
];
|
||||
|
||||
return intervenantContainers.some(id => document.getElementById(id) !== null);
|
||||
}
|
||||
230
assets/js/extra/agenda-loading-feedback.js
Normal file
230
assets/js/extra/agenda-loading-feedback.js
Normal file
@ -0,0 +1,230 @@
|
||||
/* Feedback de chargement pour FullCalendar et spinner sur le bouton Enregistrer du modal */
|
||||
(function () {
|
||||
function setDisabled(containerSelector, disabled) {
|
||||
try {
|
||||
var container = document.querySelector(containerSelector);
|
||||
if (!container) return;
|
||||
var els = container.querySelectorAll('button, input, select, textarea');
|
||||
els.forEach(function (el) {
|
||||
el.disabled = !!disabled;
|
||||
});
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function showIndicator(id, show) {
|
||||
try {
|
||||
var el = document.getElementById(id);
|
||||
if (el) {
|
||||
el.style.display = show ? 'block' : 'none';
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function attachLoading(calendar, indicatorId, filtersSelector) {
|
||||
try {
|
||||
if (!calendar || typeof calendar.setOption !== 'function') return;
|
||||
calendar.setOption('loading', function (isLoading) {
|
||||
showIndicator(indicatorId, isLoading);
|
||||
// Ne pas désactiver les filtres pendant le chargement, uniquement overlay sur le calendrier
|
||||
try {
|
||||
var calEl = document.getElementById('agenda-calendar') || (calendar.el || null);
|
||||
if (calEl && window.CRVI_OVERLAY) {
|
||||
if (isLoading) {
|
||||
window.CRVI_OVERLAY.show(calEl);
|
||||
} else {
|
||||
window.CRVI_OVERLAY.hide(calEl);
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
});
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// Gestionnaire de chargement global, pour couvrir les appels AJAX génériques (création, update, delete, etc.)
|
||||
// Usage: window.CRVI_LOADING.start(); ... finally window.CRVI_LOADING.end();
|
||||
(function initGlobalLoading() {
|
||||
var counter = 0;
|
||||
var defaultIndicatorId = 'loading-indicator';
|
||||
// Ne pas désactiver automatiquement les zones de filtres
|
||||
var defaultDisableSelectors = [];
|
||||
|
||||
function updateUi() {
|
||||
var isLoading = counter > 0;
|
||||
try {
|
||||
showIndicator(defaultIndicatorId, isLoading);
|
||||
// Désactiver quelques containers communs si présents
|
||||
defaultDisableSelectors.forEach(function (sel) {
|
||||
setDisabled(sel, isLoading);
|
||||
});
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
if (!window.CRVI_LOADING) {
|
||||
window.CRVI_LOADING = {
|
||||
start: function () {
|
||||
counter++;
|
||||
updateUi();
|
||||
},
|
||||
end: function () {
|
||||
if (counter > 0) counter--;
|
||||
updateUi();
|
||||
},
|
||||
// Permet d'ajuster la cible si besoin ailleurs
|
||||
configure: function (opts) {
|
||||
if (opts && typeof opts.indicatorId === 'string') {
|
||||
defaultIndicatorId = opts.indicatorId;
|
||||
}
|
||||
if (opts && Array.isArray(opts.disableSelectors)) {
|
||||
defaultDisableSelectors = opts.disableSelectors.slice();
|
||||
}
|
||||
updateUi();
|
||||
},
|
||||
// Pour diagnostics
|
||||
_getCount: function () {
|
||||
return counter;
|
||||
}
|
||||
};
|
||||
}
|
||||
})();
|
||||
|
||||
// Overlay ciblé par élément: window.CRVI_OVERLAY.show(elementOrSelector), .hide(...)
|
||||
(function initElementOverlay() {
|
||||
function getTarget(elOrSelector) {
|
||||
if (!elOrSelector) return null;
|
||||
if (typeof elOrSelector === 'string') return document.querySelector(elOrSelector);
|
||||
return elOrSelector.nodeType === 1 ? elOrSelector : null;
|
||||
}
|
||||
|
||||
function ensurePositioned(target) {
|
||||
var style = window.getComputedStyle(target);
|
||||
if (style.position === 'static' || !style.position) {
|
||||
target.style.position = 'relative';
|
||||
}
|
||||
}
|
||||
|
||||
function createOverlay(target) {
|
||||
var overlay = document.createElement('div');
|
||||
overlay.className = 'crvi-overlay';
|
||||
overlay.style.position = 'absolute';
|
||||
overlay.style.top = '0';
|
||||
overlay.style.left = '0';
|
||||
overlay.style.right = '0';
|
||||
overlay.style.bottom = '0';
|
||||
overlay.style.background = 'rgba(255,255,255,0.6)';
|
||||
overlay.style.display = 'flex';
|
||||
overlay.style.alignItems = 'center';
|
||||
overlay.style.justifyContent = 'center';
|
||||
overlay.style.zIndex = '20';
|
||||
|
||||
var spinner = document.createElement('div');
|
||||
spinner.className = 'spinner-border text-primary';
|
||||
spinner.setAttribute('role', 'status');
|
||||
spinner.setAttribute('aria-hidden', 'true');
|
||||
|
||||
overlay.appendChild(spinner);
|
||||
return overlay;
|
||||
}
|
||||
|
||||
function getCount(target) {
|
||||
var c = parseInt(target.getAttribute('data-overlay-count') || '0', 10);
|
||||
return isNaN(c) ? 0 : c;
|
||||
}
|
||||
|
||||
function setCount(target, c) {
|
||||
target.setAttribute('data-overlay-count', String(c));
|
||||
}
|
||||
|
||||
function show(elOrSelector) {
|
||||
var target = getTarget(elOrSelector);
|
||||
if (!target) return;
|
||||
ensurePositioned(target);
|
||||
var count = getCount(target);
|
||||
setCount(target, count + 1);
|
||||
if (count > 0 && target.querySelector(':scope > .crvi-overlay')) {
|
||||
return;
|
||||
}
|
||||
var overlay = createOverlay(target);
|
||||
target.appendChild(overlay);
|
||||
}
|
||||
|
||||
function hide(elOrSelector) {
|
||||
var target = getTarget(elOrSelector);
|
||||
if (!target) return;
|
||||
var count = getCount(target);
|
||||
count = Math.max(0, count - 1);
|
||||
setCount(target, count);
|
||||
if (count === 0) {
|
||||
var overlay = target.querySelector(':scope > .crvi-overlay');
|
||||
if (overlay) {
|
||||
overlay.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!window.CRVI_OVERLAY) {
|
||||
window.CRVI_OVERLAY = { show: show, hide: hide };
|
||||
}
|
||||
})();
|
||||
|
||||
function tryAttach() {
|
||||
if (window.currentCalendar) {
|
||||
attachLoading(window.currentCalendar, 'loading-indicator', '.filters');
|
||||
}
|
||||
if (window.currentColleaguesCalendar) {
|
||||
attachLoading(
|
||||
window.currentColleaguesCalendar,
|
||||
'loading-indicator-colleagues',
|
||||
'.filters-colleagues'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Essayer d'attacher au démarrage puis quelques tentatives (calendriers init asynchrones)
|
||||
tryAttach();
|
||||
var iv = setInterval(tryAttach, 500);
|
||||
setTimeout(function () {
|
||||
clearInterval(iv);
|
||||
}, 10000);
|
||||
|
||||
// Spinner + désactivation sur le bouton Enregistrer du modal
|
||||
document.addEventListener(
|
||||
'click',
|
||||
function (e) {
|
||||
var btn = e.target && (e.target.closest ? e.target.closest('#saveEvent') : null);
|
||||
if (!btn) return;
|
||||
if (btn.dataset.loading === '1') return;
|
||||
|
||||
btn.dataset.loading = '1';
|
||||
btn.disabled = true;
|
||||
if (!btn.dataset.originalHtml) {
|
||||
btn.dataset.originalHtml = btn.innerHTML;
|
||||
}
|
||||
btn.innerHTML =
|
||||
'<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>Enregistrer';
|
||||
|
||||
var onDone = function () {
|
||||
btn.disabled = false;
|
||||
btn.dataset.loading = '0';
|
||||
if (btn.dataset.originalHtml) {
|
||||
btn.innerHTML = btn.dataset.originalHtml;
|
||||
}
|
||||
};
|
||||
|
||||
// Réactiver à la fermeture du modal ou après un timeout de secours
|
||||
var modal = document.getElementById('eventModal');
|
||||
if (modal) {
|
||||
var handler = function () {
|
||||
modal.removeEventListener('hidden.bs.modal', handler);
|
||||
onDone();
|
||||
};
|
||||
modal.addEventListener('hidden.bs.modal', handler);
|
||||
}
|
||||
setTimeout(onDone, 8000);
|
||||
},
|
||||
true
|
||||
);
|
||||
});
|
||||
})();
|
||||
|
||||
|
||||
654
assets/js/modules/agenda-admin-permanences.js
Normal file
654
assets/js/modules/agenda-admin-permanences.js
Normal file
@ -0,0 +1,654 @@
|
||||
/**
|
||||
* Module Permanences Admin
|
||||
* Gestion du formulaire d'encodage des permanences pour l'admin (avec sélection d'intervenant)
|
||||
*/
|
||||
|
||||
import { apiFetch } from './agenda-api.js';
|
||||
import toastr from 'toastr';
|
||||
|
||||
/**
|
||||
* Initialise le module permanences admin
|
||||
*/
|
||||
export function initializeAdminPermanences() {
|
||||
console.log('🚀 Initialisation du module permanences admin...');
|
||||
|
||||
const form = document.getElementById('admin-permanences-form');
|
||||
if (!form) {
|
||||
console.error('Formulaire admin-permanences-form non trouvé');
|
||||
return;
|
||||
}
|
||||
|
||||
// Préselection des jours et heures selon l'intervenant
|
||||
setupIntervenantDefaults();
|
||||
|
||||
// Initialiser le champ mois de début avec le mois actuel
|
||||
const moisDebutInput = document.getElementById('mois-debut');
|
||||
if (moisDebutInput && !moisDebutInput.value) {
|
||||
const today = new Date();
|
||||
const year = today.getFullYear();
|
||||
const month = String(today.getMonth() + 1).padStart(2, '0');
|
||||
moisDebutInput.value = `${year}-${month}`;
|
||||
}
|
||||
|
||||
// Initialiser Select2 pour le champ langues
|
||||
initializeSelect2();
|
||||
|
||||
// Écouter les changements pour mettre à jour l'aperçu
|
||||
setupPreviewListeners();
|
||||
|
||||
// Gérer la soumission du formulaire
|
||||
form.addEventListener('submit', handleFormSubmit);
|
||||
|
||||
// Calculer l'aperçu initial
|
||||
updatePreview();
|
||||
|
||||
// Initialiser le formulaire d'import CSV
|
||||
initializeCsvImport();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise Select2 pour le champ de sélection des langues
|
||||
*/
|
||||
function initializeSelect2() {
|
||||
const languesSelect = document.getElementById('langues-permanences');
|
||||
if (languesSelect && typeof jQuery !== 'undefined' && jQuery.fn.select2) {
|
||||
jQuery(languesSelect).select2({
|
||||
placeholder: 'Sélectionnez une ou plusieurs langues',
|
||||
allowClear: true,
|
||||
width: '100%',
|
||||
language: {
|
||||
noResults: function() {
|
||||
return "Aucun résultat trouvé";
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure la préselection des inputs jours[] et heures[]
|
||||
* à partir des attributs data-days et data-time-slots de l'option sélectionnée
|
||||
*/
|
||||
function setupIntervenantDefaults() {
|
||||
const select = document.getElementById('intervenant-select');
|
||||
if (!select) return;
|
||||
|
||||
// Suivi des changements manuels pour ne pas écraser l'utilisateur
|
||||
let hasUserChanged = false;
|
||||
|
||||
// Marquer qu'il y a eu modification dès qu'une case est cochée/décochée
|
||||
const markChanged = () => { hasUserChanged = true; };
|
||||
document.querySelectorAll('input[name="jours[]"]').forEach(input => {
|
||||
input.addEventListener('change', markChanged);
|
||||
});
|
||||
document.querySelectorAll('input[name="heures[]"]').forEach(input => {
|
||||
input.addEventListener('change', markChanged);
|
||||
});
|
||||
|
||||
const applyDefaults = () => {
|
||||
// Si l'utilisateur a déjà modifié les cases, ne pas écraser ses choix
|
||||
if (hasUserChanged) {
|
||||
// Laisser l'aperçu se recalculer d'après l'état courant
|
||||
updatePreview();
|
||||
return;
|
||||
}
|
||||
|
||||
const option = select.options[select.selectedIndex];
|
||||
if (!option) return;
|
||||
|
||||
const daysStr = option.getAttribute('data-days') || '';
|
||||
const timesStr = option.getAttribute('data-time-slots') || '';
|
||||
|
||||
const days = daysStr
|
||||
.split(',')
|
||||
.map(s => s.trim().toLowerCase())
|
||||
.filter(Boolean);
|
||||
const times = timesStr
|
||||
.split(',')
|
||||
.map(s => s.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
// Réinitialiser toutes les cases
|
||||
document.querySelectorAll('input[name="jours[]"]').forEach(input => {
|
||||
input.checked = false;
|
||||
});
|
||||
document.querySelectorAll('input[name="heures[]"]').forEach(input => {
|
||||
input.checked = false;
|
||||
});
|
||||
|
||||
// Appliquer les jours
|
||||
if (days.length > 0) {
|
||||
document.querySelectorAll('input[name="jours[]"]').forEach(input => {
|
||||
const value = String(input.value || '').toLowerCase();
|
||||
if (days.includes(value)) {
|
||||
input.checked = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Appliquer les heures
|
||||
if (times.length > 0) {
|
||||
document.querySelectorAll('input[name="heures[]"]').forEach(input => {
|
||||
const value = String(input.value || '').trim();
|
||||
if (times.includes(value)) {
|
||||
input.checked = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Mettre à jour l'aperçu
|
||||
updatePreview();
|
||||
};
|
||||
|
||||
// Sur changement d'intervenant
|
||||
select.addEventListener('change', applyDefaults);
|
||||
|
||||
// Initialisation à l'ouverture si une option est déjà sélectionnée
|
||||
applyDefaults();
|
||||
|
||||
// Si le formulaire est réinitialisé, réactiver l'application auto des valeurs par défaut
|
||||
const form = document.getElementById('admin-permanences-form');
|
||||
if (form) {
|
||||
form.addEventListener('reset', () => {
|
||||
// Attendre la remise à zéro effective du DOM
|
||||
setTimeout(() => {
|
||||
hasUserChanged = false;
|
||||
applyDefaults();
|
||||
}, 0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure les listeners pour mettre à jour l'aperçu en temps réel
|
||||
*/
|
||||
function setupPreviewListeners() {
|
||||
// Écouter les changements de mois de début
|
||||
const moisDebut = document.getElementById('mois-debut');
|
||||
if (moisDebut) {
|
||||
moisDebut.addEventListener('change', updatePreview);
|
||||
moisDebut.addEventListener('input', updatePreview);
|
||||
}
|
||||
|
||||
// Écouter les changements de période
|
||||
const periodInputs = document.querySelectorAll('input[name="periode"]');
|
||||
periodInputs.forEach(input => {
|
||||
input.addEventListener('change', updatePreview);
|
||||
});
|
||||
|
||||
// Écouter les changements de jours
|
||||
const joursInputs = document.querySelectorAll('input[name="jours[]"]');
|
||||
joursInputs.forEach(input => {
|
||||
input.addEventListener('change', updatePreview);
|
||||
});
|
||||
|
||||
// Écouter les changements des heures sélectionnées
|
||||
const heuresInputs = document.querySelectorAll('input[name="heures[]"]');
|
||||
heuresInputs.forEach(input => {
|
||||
input.addEventListener('change', updatePreview);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule et affiche l'aperçu des permanences qui seront créées
|
||||
*/
|
||||
function updatePreview() {
|
||||
const periode = parseInt(document.querySelector('input[name="periode"]:checked')?.value || '3');
|
||||
const joursChecked = Array.from(document.querySelectorAll('input[name="jours[]"]:checked'))
|
||||
.map(input => input.value);
|
||||
const heuresChecked = Array.from(document.querySelectorAll('input[name="heures[]"]:checked'))
|
||||
.map(input => input.value)
|
||||
.sort(); // Trier les heures pour un affichage cohérent
|
||||
const moisDebut = document.getElementById('mois-debut')?.value;
|
||||
|
||||
if (heuresChecked.length === 0) {
|
||||
clearPreview();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!moisDebut) {
|
||||
clearPreview();
|
||||
return;
|
||||
}
|
||||
|
||||
if (joursChecked.length === 0) {
|
||||
clearPreview();
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculer les tranches horaires à partir des heures sélectionnées
|
||||
// Chaque heure sélectionnée = 1 tranche d'1 heure
|
||||
const tranches = heuresChecked.map(heureDebut => {
|
||||
const [h, m] = heureDebut.split(':').map(Number);
|
||||
const heureFin = `${String(h + 1).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
|
||||
return {
|
||||
debut: heureDebut,
|
||||
fin: heureFin
|
||||
};
|
||||
});
|
||||
|
||||
// Calculer les dates à partir du mois de début sélectionné
|
||||
const [year, month] = moisDebut.split('-').map(Number);
|
||||
const startDate = new Date(year, month - 1, 1); // Premier jour du mois sélectionné
|
||||
const endDate = new Date(year, month - 1 + periode, 0); // Dernier jour du mois de fin
|
||||
|
||||
// Compter les occurrences pour chaque jour sélectionné
|
||||
let totalEvents = 0;
|
||||
const joursMapping = {
|
||||
'lundi': 1,
|
||||
'mardi': 2,
|
||||
'mercredi': 3,
|
||||
'jeudi': 4,
|
||||
'vendredi': 5,
|
||||
'samedi': 6,
|
||||
'dimanche': 0
|
||||
};
|
||||
|
||||
joursChecked.forEach(jour => {
|
||||
const jourNum = joursMapping[jour.toLowerCase()];
|
||||
if (jourNum !== undefined) {
|
||||
const occurrences = countDayOccurrences(jourNum, startDate, endDate);
|
||||
totalEvents += occurrences * tranches.length;
|
||||
}
|
||||
});
|
||||
|
||||
// Afficher les tranches
|
||||
displayTranches(tranches);
|
||||
|
||||
// Afficher l'estimation
|
||||
displayEstimation(totalEvents);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule les tranches horaires d'1 heure dans une plage
|
||||
*/
|
||||
function calculateTranches(heureDebut, heureFin) {
|
||||
const tranches = [];
|
||||
const [debutH, debutM] = heureDebut.split(':').map(Number);
|
||||
const [finH, finM] = heureFin.split(':').map(Number);
|
||||
|
||||
let currentH = debutH;
|
||||
let currentM = debutM;
|
||||
|
||||
while (currentH < finH || (currentH === finH && currentM < finM)) {
|
||||
const trancheDebut = `${String(currentH).padStart(2, '0')}:${String(currentM).padStart(2, '0')}`;
|
||||
|
||||
// Ajouter 1 heure
|
||||
currentH += 1;
|
||||
|
||||
const trancheFin = `${String(currentH).padStart(2, '0')}:${String(currentM).padStart(2, '0')}`;
|
||||
|
||||
// Vérifier que la tranche ne dépasse pas l'heure de fin
|
||||
if (currentH < finH || (currentH === finH && currentM <= finM)) {
|
||||
tranches.push({
|
||||
debut: trancheDebut,
|
||||
fin: trancheFin
|
||||
});
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return tranches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compte le nombre d'occurrences d'un jour de la semaine dans une plage de dates
|
||||
*/
|
||||
function countDayOccurrences(jourSemaine, startDate, endDate) {
|
||||
let count = 0;
|
||||
const current = new Date(startDate);
|
||||
|
||||
// Ajuster au premier jour de la semaine ciblée
|
||||
const currentDay = current.getDay();
|
||||
const diff = (jourSemaine - currentDay + 7) % 7;
|
||||
current.setDate(current.getDate() + diff);
|
||||
|
||||
// Compter les occurrences
|
||||
while (current <= endDate) {
|
||||
count++;
|
||||
current.setDate(current.getDate() + 7); // Semaine suivante
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Affiche les tranches horaires générées
|
||||
*/
|
||||
function displayTranches(tranches) {
|
||||
const previewEl = document.getElementById('tranches-preview');
|
||||
if (!previewEl) return;
|
||||
|
||||
if (tranches.length === 0) {
|
||||
previewEl.innerHTML = '<i class="fas fa-info-circle me-2"></i>Veuillez sélectionner une plage horaire valide.';
|
||||
return;
|
||||
}
|
||||
|
||||
const tranchesList = tranches.map(t => `<li>${t.debut} → ${t.fin}</li>`).join('');
|
||||
previewEl.innerHTML = `
|
||||
<ul>${tranchesList}</ul>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Affiche l'estimation du nombre d'événements
|
||||
*/
|
||||
function displayEstimation(count) {
|
||||
const containerEl = document.getElementById('estimation-container');
|
||||
const countEl = document.getElementById('estimation-count');
|
||||
|
||||
if (!containerEl || !countEl) return;
|
||||
|
||||
if (count > 0) {
|
||||
countEl.textContent = count;
|
||||
containerEl.style.display = 'block';
|
||||
} else {
|
||||
containerEl.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Efface l'aperçu
|
||||
*/
|
||||
function clearPreview() {
|
||||
const previewEl = document.getElementById('tranches-preview');
|
||||
if (previewEl) {
|
||||
previewEl.innerHTML = '<i class="fas fa-info-circle me-2"></i>Veuillez sélectionner une plage horaire pour voir l\'aperçu.';
|
||||
}
|
||||
|
||||
const containerEl = document.getElementById('estimation-container');
|
||||
if (containerEl) {
|
||||
containerEl.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gère la soumission du formulaire
|
||||
*/
|
||||
async function handleFormSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const form = e.target;
|
||||
const submitBtn = document.getElementById('submit-permanences-btn');
|
||||
|
||||
// Validation
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Récupérer les données du formulaire
|
||||
const formData = new FormData(form);
|
||||
const intervenantId = formData.get('intervenant_id');
|
||||
const periode = parseInt(formData.get('periode'));
|
||||
const moisDebut = formData.get('mois_debut');
|
||||
const jours = formData.getAll('jours[]');
|
||||
const heures = formData.getAll('heures[]');
|
||||
const dureePermanence = document.querySelector('input[name="duree_permanence"]:checked')?.value || '1h';
|
||||
const nbTranches = dureePermanence === '15min' ? parseInt(document.getElementById('nb-tranches')?.value || '1') : null;
|
||||
// Récupérer les langues sélectionnées via Select2
|
||||
const languesSelect = document.getElementById('langues-permanences');
|
||||
const langues = languesSelect && typeof jQuery !== 'undefined' && jQuery(languesSelect).val()
|
||||
? jQuery(languesSelect).val().filter(value => value !== '')
|
||||
: [];
|
||||
const informationsComplementaires = formData.get('informations_complementaires') || '';
|
||||
|
||||
// Validation supplémentaire
|
||||
if (!intervenantId) {
|
||||
toastr.error('Veuillez sélectionner un intervenant', 'Erreur');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!moisDebut) {
|
||||
toastr.error('Veuillez sélectionner un mois de début', 'Erreur');
|
||||
return;
|
||||
}
|
||||
|
||||
if (jours.length === 0) {
|
||||
toastr.error('Veuillez sélectionner au moins un jour de la semaine', 'Erreur');
|
||||
return;
|
||||
}
|
||||
|
||||
if (heures.length === 0) {
|
||||
toastr.error('Veuillez sélectionner au moins une heure de permanence', 'Erreur');
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculer la plage horaire globale (min et max des heures sélectionnées)
|
||||
const heuresSorted = heures.map(h => {
|
||||
const [hour] = h.split(':').map(Number);
|
||||
return hour;
|
||||
}).sort((a, b) => a - b);
|
||||
|
||||
const heureMin = heuresSorted[0];
|
||||
const heureMax = heuresSorted[heuresSorted.length - 1];
|
||||
const plageDebut = `${String(heureMin).padStart(2, '0')}:00`;
|
||||
const plageFin = `${String(heureMax + 1).padStart(2, '0')}:00`;
|
||||
|
||||
// Désactiver le bouton
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Création en cours...';
|
||||
|
||||
// Préparer les données pour l'API
|
||||
const data = {
|
||||
intervenant_id: parseInt(intervenantId),
|
||||
periode: periode,
|
||||
mois_debut: moisDebut,
|
||||
jours: jours,
|
||||
plage_horaire: {
|
||||
debut: plageDebut,
|
||||
fin: plageFin
|
||||
},
|
||||
heures: heures, // Envoyer aussi les heures individuelles pour le backend
|
||||
duree_permanence: dureePermanence, // '1h' ou '15min'
|
||||
nb_tranches: nbTranches, // Nombre de tranches si 15min (1-4), null si 1h
|
||||
langues: langues, // Langues sélectionnées (peut être un tableau vide)
|
||||
informations_complementaires: informationsComplementaires
|
||||
};
|
||||
|
||||
try {
|
||||
// Appel API (apiFetch retourne directement data, pas response.data)
|
||||
const result = await apiFetch('admin/permanences', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
toastr.success(
|
||||
`${result.message} (${result.permanences_crees} permanences créées)`,
|
||||
'Succès',
|
||||
{ timeOut: 5000 }
|
||||
);
|
||||
|
||||
// Réinitialiser le formulaire
|
||||
form.reset();
|
||||
clearPreview();
|
||||
|
||||
// Réinitialiser la période par défaut
|
||||
document.getElementById('periode-3').checked = true;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la création des permanences:', error);
|
||||
toastr.error('Une erreur est survenue lors de la création des permanences', 'Erreur');
|
||||
} finally {
|
||||
// Réactiver le bouton
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = '<i class="fas fa-save me-2"></i>Enregistrer les permanences';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Valide le formulaire avant soumission
|
||||
*/
|
||||
function validateForm() {
|
||||
const intervenantId = document.getElementById('intervenant-select')?.value;
|
||||
const periode = document.querySelector('input[name="periode"]:checked')?.value;
|
||||
const moisDebut = document.getElementById('mois-debut')?.value;
|
||||
const joursChecked = document.querySelectorAll('input[name="jours[]"]:checked');
|
||||
const heuresChecked = document.querySelectorAll('input[name="heures[]"]:checked');
|
||||
|
||||
if (!intervenantId) {
|
||||
toastr.error('Veuillez sélectionner un intervenant', 'Validation');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!moisDebut) {
|
||||
toastr.error('Veuillez sélectionner un mois de début', 'Validation');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!periode) {
|
||||
toastr.error('Veuillez sélectionner une période (3 ou 6 mois)', 'Validation');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (joursChecked.length === 0) {
|
||||
toastr.error('Veuillez sélectionner au moins un jour de la semaine', 'Validation');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (heuresChecked.length === 0) {
|
||||
toastr.error('Veuillez sélectionner au moins une heure de permanence', 'Validation');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise le formulaire d'import CSV
|
||||
*/
|
||||
function initializeCsvImport() {
|
||||
const csvForm = document.getElementById('import-csv-form');
|
||||
if (!csvForm) {
|
||||
console.warn('Formulaire import-csv-form non trouvé');
|
||||
return;
|
||||
}
|
||||
|
||||
csvForm.addEventListener('submit', handleCsvImportSubmit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gère la soumission du formulaire d'import CSV
|
||||
*/
|
||||
async function handleCsvImportSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const form = e.target;
|
||||
const fileInput = document.getElementById('csv-file');
|
||||
const submitBtn = document.getElementById('submit-csv-import-btn');
|
||||
const resultContainer = document.getElementById('csv-import-result');
|
||||
|
||||
if (!fileInput || !fileInput.files || fileInput.files.length === 0) {
|
||||
toastr.error('Veuillez sélectionner un fichier CSV', 'Erreur');
|
||||
return;
|
||||
}
|
||||
|
||||
const file = fileInput.files[0];
|
||||
|
||||
// Vérifier que c'est un fichier CSV
|
||||
if (!file.name.toLowerCase().endsWith('.csv')) {
|
||||
toastr.error('Le fichier doit être au format CSV', 'Erreur');
|
||||
return;
|
||||
}
|
||||
|
||||
// Désactiver le bouton
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Import en cours...';
|
||||
|
||||
// Créer FormData pour l'upload
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
try {
|
||||
// Appel API pour l'import CSV
|
||||
const url = '/wp-json/crvi/v1/admin/permanences/import-csv';
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-WP-Nonce': window.wpApiSettings?.nonce || ''
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok || !data.success) {
|
||||
const errorMessage = data.error?.message || data.message || 'Erreur lors de l\'import';
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
// Afficher les résultats
|
||||
displayCsvImportResult(data.data, resultContainer);
|
||||
|
||||
toastr.success(
|
||||
`Import réussi : ${data.data.created || 0} créés, ${data.data.errors || 0} erreurs`,
|
||||
'Succès',
|
||||
{ timeOut: 5000 }
|
||||
);
|
||||
|
||||
// Réinitialiser le formulaire
|
||||
form.reset();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de l\'import CSV:', error);
|
||||
toastr.error(error.message || 'Une erreur est survenue lors de l\'import CSV', 'Erreur');
|
||||
|
||||
if (resultContainer) {
|
||||
resultContainer.innerHTML = `
|
||||
<div class="alert alert-danger">
|
||||
<i class="fas fa-exclamation-circle me-2"></i>
|
||||
<strong>Erreur :</strong> ${error.message || 'Une erreur est survenue lors de l\'import'}
|
||||
</div>
|
||||
`;
|
||||
resultContainer.style.display = 'block';
|
||||
}
|
||||
} finally {
|
||||
// Réactiver le bouton
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = '<i class="fas fa-upload me-2"></i>Importer le CSV';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Affiche les résultats de l'import CSV
|
||||
*/
|
||||
function displayCsvImportResult(result, container) {
|
||||
if (!container) return;
|
||||
|
||||
let html = '';
|
||||
|
||||
if (result.errors && result.errors.length > 0) {
|
||||
html += `
|
||||
<div class="alert alert-warning">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
<strong>Attention :</strong> ${result.errors.length} erreur(s) détectée(s)
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (result.errors.length <= 10) {
|
||||
html += '<ul class="list-group mt-2">';
|
||||
result.errors.forEach((error, index) => {
|
||||
html += `
|
||||
<li class="list-group-item">
|
||||
<strong>Ligne ${error.line || '?'} :</strong> ${error.message || 'Erreur inconnue'}
|
||||
</li>
|
||||
`;
|
||||
});
|
||||
html += '</ul>';
|
||||
}
|
||||
}
|
||||
|
||||
if (result.created > 0) {
|
||||
html += `
|
||||
<div class="alert alert-success mt-3">
|
||||
<i class="fas fa-check-circle me-2"></i>
|
||||
<strong>Succès :</strong> ${result.created} permanence(s) créée(s) avec succès
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
container.innerHTML = html;
|
||||
container.style.display = 'block';
|
||||
}
|
||||
|
||||
|
||||
100
assets/js/modules/agenda-api.js
Normal file
100
assets/js/modules/agenda-api.js
Normal file
@ -0,0 +1,100 @@
|
||||
// Module ES6 pour les appels API de l'agenda
|
||||
export async function apiFetch(endpoint, options = {}) {
|
||||
const root = (window.wpApiSettings && window.wpApiSettings.root) ? window.wpApiSettings.root : '/wp-json/';
|
||||
const base = root.endsWith('/') ? root : (root + '/');
|
||||
const url = `${base}crvi/v1/${endpoint}`.replace(/([^:]\/)\/+/g, '$1'); // normaliser les doubles /
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
...(window.wpApiSettings ? { 'X-WP-Nonce': window.wpApiSettings.nonce } : {})
|
||||
};
|
||||
const opts = { ...options, headers: { ...headers, ...(options.headers || {}) } };
|
||||
|
||||
try {
|
||||
// Démarrage indicateur global
|
||||
try { window.CRVI_LOADING && typeof window.CRVI_LOADING.start === 'function' && window.CRVI_LOADING.start(); } catch (_) {}
|
||||
|
||||
const response = await fetch(url, opts);
|
||||
|
||||
if (!response.ok) {
|
||||
let serverText = '';
|
||||
try {
|
||||
serverText = await response.text();
|
||||
} catch (_) {}
|
||||
const err = new Error(`HTTP ${response.status}: ${response.statusText} | endpoint=${url} | body=${serverText?.slice(0,300)}`);
|
||||
console.error('API error:', err);
|
||||
throw err;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.success) {
|
||||
const errorMessage = data.error?.message || data.message || 'Erreur API';
|
||||
console.error('Erreur API détectée:', errorMessage, '| endpoint=', url);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
return data.data;
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de l\'appel API:', error);
|
||||
throw error;
|
||||
} finally {
|
||||
// Fin indicateur global
|
||||
try { window.CRVI_LOADING && typeof window.CRVI_LOADING.end === 'function' && window.CRVI_LOADING.end(); } catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
export async function getEvents(params = {}) {
|
||||
// console.log('Appel API getEvents avec paramètres:', params);
|
||||
const query = new URLSearchParams(params).toString();
|
||||
const endpoint = `events${query ? '?' + query : ''}`;
|
||||
// console.log('Endpoint appelé:', endpoint);
|
||||
const result = await apiFetch(endpoint);
|
||||
// console.log('Résultat API getEvents:', result);
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function getEvent(id) {
|
||||
return apiFetch(`events/${id}`);
|
||||
}
|
||||
|
||||
export async function createEvent(data) {
|
||||
return apiFetch('events', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateEvent(id, data) {
|
||||
return apiFetch(`events/${id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteEvent(id) {
|
||||
return apiFetch(`events/${id}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
}
|
||||
|
||||
export async function changeEventStatus(id, status, motifAnnulation = null) {
|
||||
const data = { statut: status };
|
||||
if (motifAnnulation) {
|
||||
data.motif_annulation = motifAnnulation;
|
||||
}
|
||||
return apiFetch(`events/${id}/statut`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
}
|
||||
|
||||
export async function getFilters(type, params = {}) {
|
||||
// Gestion spéciale pour les disponibilités
|
||||
if (type === 'disponibilites') {
|
||||
const query = new URLSearchParams(params).toString();
|
||||
const endpoint = `agenda/disponibilites${query ? '?' + query : ''}`;
|
||||
return apiFetch(endpoint);
|
||||
}
|
||||
return apiFetch(`filters/${type}`);
|
||||
}
|
||||
305
assets/js/modules/agenda-entity-creator.js
Normal file
305
assets/js/modules/agenda-entity-creator.js
Normal file
@ -0,0 +1,305 @@
|
||||
// Module ES6 pour la création d'entités depuis le modal d'événement
|
||||
import { apiFetch } from './agenda-api.js';
|
||||
import { notifyError, notifySuccess } from './agenda-notifications.js';
|
||||
import { populateSelects, preserveModalData } from './agenda-modal.js';
|
||||
|
||||
// Configuration des entités
|
||||
const ENTITY_CONFIG = {
|
||||
beneficiaire: {
|
||||
modalId: 'createBeneficiaireModal',
|
||||
formId: 'createBeneficiaireForm',
|
||||
saveBtnId: 'saveBeneficiaireBtn',
|
||||
selectId: 'id_beneficiaire',
|
||||
endpoint: 'agenda/beneficiaires',
|
||||
displayField: 'nom',
|
||||
linkId: 'createBeneficiaireLink'
|
||||
},
|
||||
intervenant: {
|
||||
modalId: 'createIntervenantModal',
|
||||
formId: 'createIntervenantForm',
|
||||
saveBtnId: 'saveIntervenantBtn',
|
||||
selectId: 'id_intervenant',
|
||||
endpoint: 'agenda/intervenants',
|
||||
displayField: 'nom',
|
||||
linkId: 'createIntervenantLink'
|
||||
},
|
||||
traducteur: {
|
||||
modalId: 'createTraducteurModal',
|
||||
formId: 'createTraducteurForm',
|
||||
saveBtnId: 'saveTraducteurBtn',
|
||||
selectId: 'id_traducteur',
|
||||
endpoint: 'agenda/traducteurs',
|
||||
displayField: 'nom',
|
||||
linkId: 'createTraducteurLink'
|
||||
},
|
||||
local: {
|
||||
modalId: 'createLocalModal',
|
||||
formId: 'createLocalForm',
|
||||
saveBtnId: 'saveLocalBtn',
|
||||
selectId: 'id_local',
|
||||
endpoint: 'agenda/locaux',
|
||||
displayField: 'nom',
|
||||
linkId: 'createLocalLink'
|
||||
}
|
||||
};
|
||||
|
||||
// Variable pour suivre si les créateurs d'entités ont déjà été initialisés
|
||||
let entityCreatorsInitialized = false;
|
||||
|
||||
// Initialisation des modals de création d'entités
|
||||
export function initializeEntityCreators() {
|
||||
// Éviter la double initialisation
|
||||
if (entityCreatorsInitialized) {
|
||||
// console.log('Créateurs d\'entités déjà initialisés, ignoré.');
|
||||
return;
|
||||
}
|
||||
|
||||
// console.log('Initialisation des créateurs d\'entités...');
|
||||
|
||||
// Ajout des event listeners pour chaque type d'entité
|
||||
Object.entries(ENTITY_CONFIG).forEach(([entityType, config]) => {
|
||||
const createLink = document.getElementById(config.linkId);
|
||||
const saveBtn = document.getElementById(config.saveBtnId);
|
||||
|
||||
if (createLink) {
|
||||
// Supprimer les anciens event listeners s'ils existent
|
||||
createLink.removeEventListener('click', createLink._entityCreatorHandler);
|
||||
|
||||
// Créer un nouveau handler et le stocker pour pouvoir le supprimer plus tard
|
||||
createLink._entityCreatorHandler = (e) => {
|
||||
// console.log('click sur le lien de création d\'entité');
|
||||
e.preventDefault();
|
||||
openCreateEntityModal(entityType);
|
||||
};
|
||||
|
||||
createLink.addEventListener('click', createLink._entityCreatorHandler);
|
||||
}
|
||||
|
||||
if (saveBtn) {
|
||||
// Supprimer les anciens event listeners s'ils existent
|
||||
saveBtn.removeEventListener('click', saveBtn._entityCreatorHandler);
|
||||
|
||||
// Créer un nouveau handler et le stocker pour pouvoir le supprimer plus tard
|
||||
saveBtn._entityCreatorHandler = () => handleEntityCreation(entityType);
|
||||
|
||||
saveBtn.addEventListener('click', saveBtn._entityCreatorHandler);
|
||||
}
|
||||
});
|
||||
|
||||
entityCreatorsInitialized = true;
|
||||
}
|
||||
|
||||
// Ouvre le modal de création d'une entité
|
||||
function openCreateEntityModal(entityType) {
|
||||
const config = ENTITY_CONFIG[entityType];
|
||||
if (!config) {
|
||||
console.error(`Configuration non trouvée pour l'entité: ${entityType}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const modal = document.getElementById(config.modalId);
|
||||
if (!modal) {
|
||||
console.error(`Modal non trouvé: ${config.modalId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Réinitialiser le formulaire
|
||||
const form = document.getElementById(config.formId);
|
||||
if (form) {
|
||||
form.reset();
|
||||
}
|
||||
|
||||
// Les selects sont maintenant initialisés automatiquement par jQuery('.select2').select2()
|
||||
|
||||
// Préserver les données de la modale principale avant de la fermer
|
||||
preserveModalData();
|
||||
|
||||
// Fermer le modal principal d'événement s'il est ouvert
|
||||
const eventModal = document.getElementById('eventModal');
|
||||
if (eventModal) {
|
||||
const eventBsModal = bootstrap.Modal.getInstance(eventModal);
|
||||
if (eventBsModal) {
|
||||
eventBsModal.hide();
|
||||
}
|
||||
}
|
||||
|
||||
// Attendre un peu que le modal principal se ferme avant d'ouvrir le nouveau
|
||||
setTimeout(() => {
|
||||
// Ouvrir le modal de création
|
||||
if (window.bootstrap && window.bootstrap.Modal) {
|
||||
const bsModal = new window.bootstrap.Modal(modal);
|
||||
bsModal.show();
|
||||
|
||||
// Ajouter un event listener pour rouvrir le modal principal quand on ferme
|
||||
modal.addEventListener('hidden.bs.modal', function() {
|
||||
// Rouvrir le modal principal d'événement avec les données préservées
|
||||
if (eventModal && window.bootstrap && window.bootstrap.Modal) {
|
||||
const newEventModal = new window.bootstrap.Modal(eventModal);
|
||||
newEventModal.show();
|
||||
}
|
||||
}, { once: true }); // Une seule fois
|
||||
}
|
||||
}, 300); // Délai pour la transition
|
||||
}
|
||||
|
||||
// Les selects sont maintenant initialisés automatiquement par jQuery('.select2').select2()
|
||||
// Les options sont générées en PHP dans le template
|
||||
|
||||
// Gère la création d'une entité
|
||||
async function handleEntityCreation(entityType) {
|
||||
const config = ENTITY_CONFIG[entityType];
|
||||
if (!config) {
|
||||
console.error(`Configuration non trouvée pour l'entité: ${entityType}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const form = document.getElementById(config.formId);
|
||||
if (!form) {
|
||||
console.error(`Formulaire non trouvé: ${config.formId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validation du formulaire
|
||||
if (!form.checkValidity()) {
|
||||
form.reportValidity();
|
||||
return;
|
||||
}
|
||||
|
||||
// Récupération des données du formulaire
|
||||
const formData = new FormData(form);
|
||||
const data = Object.fromEntries(formData.entries());
|
||||
|
||||
// Gestion spéciale pour les selects multiples
|
||||
if (entityType === 'traducteur') {
|
||||
const languesSelect = document.getElementById('traducteur_langues');
|
||||
if (languesSelect && window.jQuery) {
|
||||
const selectedLangues = jQuery(languesSelect).select2('data').map(item => item.id);
|
||||
data.langues_parlees = selectedLangues.join('|');
|
||||
}
|
||||
} else if (entityType === 'intervenant') {
|
||||
const departementsSelect = document.getElementById('intervenant_departements');
|
||||
if (departementsSelect && window.jQuery) {
|
||||
const selectedDepartements = jQuery(departementsSelect).select2('data').map(item => item.id);
|
||||
data.departements_ids = selectedDepartements.join('|');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// console.log(`Création de l'entité ${entityType}:`, data);
|
||||
|
||||
// Appel API pour créer l'entité
|
||||
const response = await apiFetch(config.endpoint, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
// console.log(`Entité ${entityType} créée:`, response);
|
||||
|
||||
// Fermer le modal de création
|
||||
const modal = document.getElementById(config.modalId);
|
||||
if (modal && window.bootstrap && window.bootstrap.Modal) {
|
||||
const bsModal = window.bootstrap.Modal.getInstance(modal);
|
||||
if (bsModal) {
|
||||
bsModal.hide();
|
||||
}
|
||||
}
|
||||
|
||||
// Attendre que le modal se ferme avant de rouvrir le modal principal
|
||||
setTimeout(async () => {
|
||||
// Ajouter la nouvelle entité au select correspondant
|
||||
await addEntityToSelect(entityType, response);
|
||||
|
||||
// Rafraîchir les selects pour mettre à jour les disponibilités
|
||||
await populateSelects();
|
||||
|
||||
// Rouvrir le modal principal d'événement avec les données préservées
|
||||
const eventModal = document.getElementById('eventModal');
|
||||
if (eventModal && window.bootstrap && window.bootstrap.Modal) {
|
||||
const newEventModal = new window.bootstrap.Modal(eventModal);
|
||||
newEventModal.show();
|
||||
}
|
||||
|
||||
notifySuccess(`${entityType.charAt(0).toUpperCase() + entityType.slice(1)} créé avec succès`);
|
||||
}, 300);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Erreur lors de la création de l'entité ${entityType}:`, error);
|
||||
notifyError(`Erreur lors de la création du ${entityType}: ${error.message || 'Erreur inconnue'}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Ajoute une nouvelle entité au select correspondant
|
||||
async function addEntityToSelect(entityType, entityData) {
|
||||
const config = ENTITY_CONFIG[entityType];
|
||||
if (!config) return;
|
||||
|
||||
const select = document.getElementById(config.selectId);
|
||||
if (!select) return;
|
||||
|
||||
// Créer l'option pour la nouvelle entité
|
||||
const option = document.createElement('option');
|
||||
option.value = entityData.id;
|
||||
|
||||
// Construire le texte d'affichage enrichi selon le type d'entité
|
||||
let displayText = '';
|
||||
|
||||
switch (entityType) {
|
||||
case 'beneficiaire':
|
||||
displayText = `${entityData.prenom || ''} ${entityData.nom || ''}`.trim();
|
||||
if (entityData.email) {
|
||||
displayText += ` (${entityData.email})`;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'intervenant':
|
||||
displayText = `${entityData.prenom || ''} ${entityData.nom || ''}`.trim();
|
||||
if (entityData.specialite) {
|
||||
displayText += ` - ${entityData.specialite}`;
|
||||
}
|
||||
if (entityData.email) {
|
||||
displayText += ` (${entityData.email})`;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'traducteur':
|
||||
displayText = `${entityData.prenom || ''} ${entityData.nom || ''}`.trim();
|
||||
if (entityData.langues_parlees) {
|
||||
displayText += ` - ${entityData.langues_parlees}`;
|
||||
}
|
||||
if (entityData.email) {
|
||||
displayText += ` (${entityData.email})`;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'local':
|
||||
displayText = entityData.nom || '';
|
||||
if (entityData.capacite) {
|
||||
displayText += ` (${entityData.capacite} places)`;
|
||||
}
|
||||
if (entityData.adresse) {
|
||||
displayText += ` - ${entityData.adresse}`;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
displayText = entityData[config.displayField] || entityData.nom || '';
|
||||
if (entityData.prenom) {
|
||||
displayText += ` ${entityData.prenom}`;
|
||||
}
|
||||
}
|
||||
|
||||
option.textContent = displayText;
|
||||
|
||||
// Ajouter l'option au select
|
||||
select.appendChild(option);
|
||||
|
||||
// Sélectionner automatiquement la nouvelle entité
|
||||
select.value = entityData.id;
|
||||
|
||||
// console.log(`Entité ${entityType} ajoutée au select ${config.selectId}:`, entityData);
|
||||
}
|
||||
|
||||
// Fonction utilitaire pour obtenir la configuration d'une entité
|
||||
export function getEntityConfig(entityType) {
|
||||
return ENTITY_CONFIG[entityType];
|
||||
}
|
||||
383
assets/js/modules/agenda-filters.js
Normal file
383
assets/js/modules/agenda-filters.js
Normal file
@ -0,0 +1,383 @@
|
||||
// Module de gestion des filtres dynamiques pour l'agenda
|
||||
import { getEvents } from './agenda-api.js';
|
||||
|
||||
let currentFilters = {};
|
||||
let calendarInstance = null;
|
||||
|
||||
/**
|
||||
* Génère le titre d'un événement en utilisant les données enrichies
|
||||
* @param {Object} event - Données de l'événement
|
||||
* @returns {string} Titre de l'événement
|
||||
*/
|
||||
function getEventTitle(event) {
|
||||
let title = '';
|
||||
|
||||
// Utiliser le nom du bénéficiaire si disponible
|
||||
if (event.beneficiaire && event.beneficiaire.nom_complet) {
|
||||
title = event.beneficiaire.nom_complet;
|
||||
} else if (event.type === 'groupe') {
|
||||
title = 'Groupe';
|
||||
} else {
|
||||
title = event.type || 'Sans type';
|
||||
}
|
||||
|
||||
// Ajouter le type d'intervention principal si disponible
|
||||
if (event.intervenant && event.intervenant.types_intervention_noms && event.intervenant.types_intervention_noms.length > 0) {
|
||||
const primaryType = event.intervenant.types_intervention_noms[0]; // Premier type d'intervention
|
||||
title += ` - ${primaryType}`;
|
||||
}
|
||||
|
||||
// Ajouter le département si disponible (optionnel - décommenter si souhaité)
|
||||
// if (event.intervenant && event.intervenant.departements_noms && event.intervenant.departements_noms.length > 0) {
|
||||
// const departements = event.intervenant.departements_noms.join(', ');
|
||||
// title += ` [${departements}]`;
|
||||
// }
|
||||
|
||||
// Ajouter le commentaire si disponible
|
||||
if (event.commentaire) {
|
||||
title += ' - ' + event.commentaire;
|
||||
}
|
||||
|
||||
return title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise les filtres dynamiques
|
||||
* @param {Object} calendar - Instance FullCalendar
|
||||
*/
|
||||
export function initializeFilters(calendar) {
|
||||
calendarInstance = calendar;
|
||||
|
||||
// Remplacer la fonction events du calendrier pour inclure les filtres
|
||||
if (calendarInstance) {
|
||||
const originalEventsFunction = calendarInstance.getOption('events');
|
||||
|
||||
calendarInstance.setOption('events', async function(fetchInfo, successCallback, failureCallback) {
|
||||
try {
|
||||
const filters = collectFilters();
|
||||
const params = {
|
||||
start: fetchInfo.startStr.split('T')[0],
|
||||
end: fetchInfo.endStr.split('T')[0],
|
||||
...filters
|
||||
};
|
||||
|
||||
// console.log('Chargement des événements pour la période:', params.start, 'à', params.end);
|
||||
if (Object.keys(filters).length > 0) {
|
||||
// console.log('Filtres appliqués:', filters);
|
||||
}
|
||||
|
||||
const apiEvents = await getEvents(params);
|
||||
const events = apiEvents.map(ev => ({
|
||||
id: ev.id,
|
||||
title: getEventTitle(ev),
|
||||
start: ev.date_rdv + 'T' + ev.heure_rdv,
|
||||
end: ev.date_fin + 'T' + ev.heure_fin,
|
||||
extendedProps: {
|
||||
type: ev.type,
|
||||
commentaire: ev.commentaire,
|
||||
date_rdv: ev.date_rdv,
|
||||
heure_rdv: ev.heure_rdv,
|
||||
date_fin: ev.date_fin,
|
||||
heure_fin: ev.heure_fin,
|
||||
// Garder les IDs pour compatibilité
|
||||
id_beneficiaire: ev.id_beneficiaire,
|
||||
id_intervenant: ev.id_intervenant,
|
||||
id_traducteur: ev.id_traducteur,
|
||||
id_local: ev.id_local,
|
||||
langue: ev.langue,
|
||||
// Nouvelles données enrichies
|
||||
beneficiaire: ev.beneficiaire,
|
||||
intervenant: ev.intervenant,
|
||||
traducteur: ev.traducteur,
|
||||
local: ev.local,
|
||||
nb_participants: ev.nb_participants,
|
||||
nb_hommes: ev.nb_hommes,
|
||||
nb_femmes: ev.nb_femmes,
|
||||
statut: ev.statut,
|
||||
created_at: ev.created_at,
|
||||
updated_at: ev.updated_at
|
||||
}
|
||||
}));
|
||||
successCallback(events);
|
||||
} catch (e) {
|
||||
console.error('Erreur lors du chargement des événements:', e);
|
||||
failureCallback(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Intercepter la soumission du formulaire
|
||||
const filterForm = document.querySelector('.filters');
|
||||
if (filterForm) {
|
||||
filterForm.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
applyFilters();
|
||||
});
|
||||
}
|
||||
|
||||
// Écouter les changements sur les selects pour filtrage automatique
|
||||
const filterSelects = filterForm.querySelectorAll('select');
|
||||
filterSelects.forEach(select => {
|
||||
select.addEventListener('change', function() {
|
||||
// Vérifier et mettre à jour la visibilité du bouton de réinitialisation
|
||||
const currentFilters = collectFilters();
|
||||
updateResetButtonVisibility(currentFilters);
|
||||
|
||||
// Appliquer les filtres automatiquement après un délai
|
||||
clearTimeout(window.filterTimeout);
|
||||
window.filterTimeout = setTimeout(() => {
|
||||
applyFilters();
|
||||
}, 500);
|
||||
});
|
||||
});
|
||||
|
||||
// Écouter les changements sur l'input date
|
||||
const dateInput = document.getElementById('date');
|
||||
if (dateInput) {
|
||||
dateInput.addEventListener('change', function() {
|
||||
// Vérifier et mettre à jour la visibilité du bouton de réinitialisation
|
||||
const currentFilters = collectFilters();
|
||||
updateResetButtonVisibility(currentFilters);
|
||||
|
||||
clearTimeout(window.filterTimeout);
|
||||
window.filterTimeout = setTimeout(() => {
|
||||
applyFilters();
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
|
||||
// Écouter les changements sur la checkbox filtre permanence
|
||||
const filtrePermanenceCheckbox = document.getElementById('filtre_permanence');
|
||||
if (filtrePermanenceCheckbox) {
|
||||
filtrePermanenceCheckbox.addEventListener('change', function() {
|
||||
// Vérifier et mettre à jour la visibilité du bouton de réinitialisation
|
||||
const currentFilters = collectFilters();
|
||||
updateResetButtonVisibility(currentFilters);
|
||||
|
||||
clearTimeout(window.filterTimeout);
|
||||
window.filterTimeout = setTimeout(() => {
|
||||
applyFilters();
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
|
||||
// Gérer le bouton de réinitialisation
|
||||
const resetBtn = document.getElementById('resetFiltersBtn');
|
||||
if (resetBtn) {
|
||||
resetBtn.addEventListener('click', function() {
|
||||
resetFilters();
|
||||
});
|
||||
}
|
||||
|
||||
// Gérer le bouton de filtrage
|
||||
const filterBtn = document.getElementById('filterBtn');
|
||||
if (filterBtn) {
|
||||
// console.log('Bouton filterBtn trouvé, ajout du gestionnaire d\'événement');
|
||||
// Désactiver le comportement par défaut du bouton
|
||||
filterBtn.addEventListener('click', function(e) {
|
||||
// console.log('Clic sur le bouton filterBtn détecté');
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
applyFilters();
|
||||
});
|
||||
} else {
|
||||
console.warn('Bouton filterBtn non trouvé');
|
||||
}
|
||||
|
||||
// Masquer le bouton de réinitialisation au démarrage (aucun filtre actif)
|
||||
updateResetButtonVisibility({});
|
||||
|
||||
// console.log('Filtres dynamiques initialisés');
|
||||
}
|
||||
|
||||
/**
|
||||
* Collecte les valeurs des filtres depuis le formulaire
|
||||
* @returns {Object} - Objet contenant les filtres
|
||||
*/
|
||||
function collectFilters() {
|
||||
const filters = {};
|
||||
|
||||
// Date
|
||||
const date = document.getElementById('date').value;
|
||||
if (date && date.trim() !== '') {
|
||||
filters.date = date;
|
||||
}
|
||||
|
||||
// Local
|
||||
const local = document.getElementById('local').value;
|
||||
if (local && local.trim() !== '') {
|
||||
filters.local = local;
|
||||
}
|
||||
|
||||
// Personne (intervenant)
|
||||
const personne = document.getElementById('personne').value;
|
||||
if (personne && personne.trim() !== '') {
|
||||
filters.intervenant = personne;
|
||||
}
|
||||
|
||||
// Type d'intervention
|
||||
const typeIntervention = document.getElementById('type_intervention').value;
|
||||
if (typeIntervention && typeIntervention.trim() !== '') {
|
||||
filters.type_intervention = typeIntervention;
|
||||
}
|
||||
|
||||
// Bénéficiaire
|
||||
const beneficiaire = document.getElementById('beneficiaire').value;
|
||||
if (beneficiaire && beneficiaire.trim() !== '') {
|
||||
filters.beneficiaire = beneficiaire;
|
||||
}
|
||||
|
||||
// Langue
|
||||
const langue = document.getElementById('langue').value;
|
||||
if (langue && langue.trim() !== '') {
|
||||
filters.langue = langue;
|
||||
}
|
||||
|
||||
// Intervenant externe (traducteur)
|
||||
const intervenantExterne = document.getElementById('intervenant_externe').value;
|
||||
if (intervenantExterne && intervenantExterne.trim() !== '') {
|
||||
filters.traducteur = intervenantExterne;
|
||||
}
|
||||
|
||||
// Filtre permanence uniquement
|
||||
const filtrePermanence = document.getElementById('filtre_permanence');
|
||||
if (filtrePermanence && filtrePermanence.checked) {
|
||||
filters.type = 'permanence';
|
||||
}
|
||||
|
||||
return filters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applique les filtres au calendrier
|
||||
*/
|
||||
async function applyFilters() {
|
||||
if (!calendarInstance) {
|
||||
console.error('Instance FullCalendar non disponible');
|
||||
return;
|
||||
}
|
||||
|
||||
const filters = collectFilters();
|
||||
|
||||
// Vérifier si les filtres ont changé
|
||||
if (JSON.stringify(filters) === JSON.stringify(currentFilters)) {
|
||||
// console.log('Filtres identiques, pas de rechargement nécessaire');
|
||||
return;
|
||||
}
|
||||
|
||||
currentFilters = filters;
|
||||
|
||||
// Mettre à jour la visibilité du bouton de réinitialisation
|
||||
updateResetButtonVisibility(filters);
|
||||
|
||||
// Afficher un indicateur de chargement
|
||||
showLoadingIndicator();
|
||||
|
||||
try {
|
||||
// Recharger les événements avec les nouveaux filtres
|
||||
await calendarInstance.refetchEvents();
|
||||
|
||||
// Afficher un message de succès
|
||||
if (window.toastr) {
|
||||
const filterCount = Object.keys(filters).length;
|
||||
if (filterCount > 0) {
|
||||
window.toastr.success(`${filterCount} filtre(s) appliqué(s)`);
|
||||
} else {
|
||||
window.toastr.info('Tous les filtres ont été supprimés');
|
||||
}
|
||||
}
|
||||
|
||||
// console.log('Filtres appliqués avec succès:', filters);
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de l\'application des filtres:', error);
|
||||
if (window.toastr) {
|
||||
window.toastr.error('Erreur lors de l\'application des filtres');
|
||||
}
|
||||
} finally {
|
||||
hideLoadingIndicator();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Affiche l'indicateur de chargement
|
||||
*/
|
||||
function showLoadingIndicator() {
|
||||
const loadingIndicator = document.getElementById('loading-indicator');
|
||||
if (loadingIndicator) {
|
||||
loadingIndicator.style.display = 'block';
|
||||
loadingIndicator.innerHTML = '<p>Application des filtres...</p>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache l'indicateur de chargement
|
||||
*/
|
||||
function hideLoadingIndicator() {
|
||||
const loadingIndicator = document.getElementById('loading-indicator');
|
||||
if (loadingIndicator) {
|
||||
loadingIndicator.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gère l'affichage du bouton de réinitialisation
|
||||
* @param {Object} filters - Filtres actuels
|
||||
*/
|
||||
function updateResetButtonVisibility(filters) {
|
||||
const resetBtn = document.getElementById('resetFiltersBtn');
|
||||
if (resetBtn) {
|
||||
const hasActiveFilters = Object.keys(filters).length > 0;
|
||||
// Masquer le conteneur parent au lieu de juste le bouton
|
||||
const resetBtnContainer = resetBtn.closest('.filter');
|
||||
if (resetBtnContainer) {
|
||||
resetBtnContainer.style.display = hasActiveFilters ? 'flex' : 'none';
|
||||
} else {
|
||||
// Fallback si le conteneur n'est pas trouvé
|
||||
resetBtn.style.display = hasActiveFilters ? 'inline-block' : 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Réinitialise tous les filtres
|
||||
*/
|
||||
export function resetFilters() {
|
||||
const filterForm = document.querySelector('.filters');
|
||||
if (filterForm) {
|
||||
// Réinitialiser le formulaire
|
||||
filterForm.reset();
|
||||
|
||||
// Réinitialiser Select2 si il est utilisé
|
||||
const selects = filterForm.querySelectorAll('select');
|
||||
selects.forEach(select => {
|
||||
if (jQuery(select).hasClass('select2-hidden-accessible')) {
|
||||
jQuery(select).val('').trigger('change');
|
||||
}
|
||||
});
|
||||
|
||||
// Réinitialiser l'input date
|
||||
const dateInput = document.getElementById('date');
|
||||
if (dateInput) {
|
||||
dateInput.value = '';
|
||||
}
|
||||
|
||||
// Masquer le bouton de réinitialisation
|
||||
updateResetButtonVisibility({});
|
||||
|
||||
// Appliquer les filtres (qui seront vides) et recharger l'agenda
|
||||
applyFilters();
|
||||
|
||||
// Afficher un message de confirmation
|
||||
if (window.toastr) {
|
||||
window.toastr.info('Tous les filtres ont été réinitialisés');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne les filtres actuels
|
||||
* @returns {Object} - Filtres actuels
|
||||
*/
|
||||
export function getCurrentFilters() {
|
||||
return { ...currentFilters };
|
||||
}
|
||||
1553
assets/js/modules/agenda-fullcalendar.js
Normal file
1553
assets/js/modules/agenda-fullcalendar.js
Normal file
File diff suppressed because it is too large
Load Diff
971
assets/js/modules/agenda-intervenant-calendar.js
Normal file
971
assets/js/modules/agenda-intervenant-calendar.js
Normal file
@ -0,0 +1,971 @@
|
||||
/**
|
||||
* Module Agenda Intervenant
|
||||
* Adapte l'agenda existant pour utiliser les endpoints intervenant
|
||||
* Réutilise les modules agenda-fullcalendar.js, agenda-filters.js, etc.
|
||||
*/
|
||||
|
||||
import { apiFetch, updateEvent, getEvent } from './agenda-api.js';
|
||||
import { notifyError, notifySuccess } from './agenda-notifications.js';
|
||||
import { openModal } from './agenda-modal.js';
|
||||
import { Calendar } from '@fullcalendar/core';
|
||||
import dayGridPlugin from '@fullcalendar/daygrid';
|
||||
import timeGridPlugin from '@fullcalendar/timegrid';
|
||||
import listPlugin from '@fullcalendar/list';
|
||||
import interactionPlugin from '@fullcalendar/interaction';
|
||||
|
||||
// Mode de vue : 'mine' ou 'colleagues'
|
||||
let currentViewMode = 'mine';
|
||||
|
||||
/**
|
||||
* Initialise le calendrier pour l'intervenant (mon agenda)
|
||||
*/
|
||||
export function initializeIntervenantCalendar() {
|
||||
console.log('🚀 Initialisation du calendrier intervenant (mon agenda)...');
|
||||
|
||||
const calendarEl = document.getElementById('agenda-calendar');
|
||||
if (!calendarEl) {
|
||||
console.error('❌ Élément agenda-calendar non trouvé');
|
||||
return null;
|
||||
}
|
||||
|
||||
currentViewMode = 'mine';
|
||||
|
||||
const calendar = new Calendar(calendarEl, {
|
||||
plugins: [dayGridPlugin, timeGridPlugin, listPlugin, interactionPlugin],
|
||||
initialView: 'dayGridMonth',
|
||||
eventDisplay: 'block',
|
||||
locale: 'fr',
|
||||
firstDay: 1,
|
||||
headerToolbar: {
|
||||
left: 'prev,next today',
|
||||
center: 'title',
|
||||
right: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek'
|
||||
},
|
||||
buttonText: {
|
||||
today: 'Aujourd\'hui',
|
||||
month: 'Mois',
|
||||
week: 'Semaine',
|
||||
day: 'Jour',
|
||||
list: 'Liste'
|
||||
},
|
||||
height: 'auto',
|
||||
// On active l'édition globalement, mais on contrôle au niveau de chaque
|
||||
// événement + des permissions (is_mine + crviPermissions.can_edit)
|
||||
editable: true,
|
||||
eventStartEditable: true,
|
||||
eventDurationEditable: true,
|
||||
eventResizableFromStart: true,
|
||||
selectable: true, // Activer la sélection pour créer des RDV
|
||||
dayMaxEvents: 3,
|
||||
weekends: true,
|
||||
moreLinkClick: 'popover', // Afficher les événements cachés dans un popover
|
||||
moreLinkContent: function(arg) {
|
||||
// Personnaliser le texte du lien "+x plus"
|
||||
const count = arg.num;
|
||||
return `+${count} plus`;
|
||||
},
|
||||
// Gestionnaire de sélection de plage de dates pour créer un événement
|
||||
select: function(arg) {
|
||||
// Vérifier les permissions de création
|
||||
const userCanCreate = window.crviPermissions && window.crviPermissions.can_create;
|
||||
if (!userCanCreate) {
|
||||
notifyError('Vous n\'êtes pas autorisé à créer des événements');
|
||||
console.warn('❌ Utilisateur non autorisé à créer des événements (permissions WP)');
|
||||
return;
|
||||
}
|
||||
|
||||
// Vérifier que la date de début n'est pas dans le passé
|
||||
if (arg.startStr) {
|
||||
const startDateStr = arg.startStr.split('T')[0];
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const selectedDate = new Date(startDateStr + 'T00:00:00');
|
||||
selectedDate.setHours(0, 0, 0, 0);
|
||||
|
||||
if (selectedDate < today) {
|
||||
notifyError('Impossible de créer un événement à une date passée');
|
||||
console.warn('⚠️ Tentative de création d\'événement à une date passée:', startDateStr);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Préremplir avec l'intervenant actuel (utilisateur connecté)
|
||||
const currentUserId = window.crviPermissions && window.crviPermissions.user_id;
|
||||
|
||||
// Ouvrir le modal de création avec l'intervenant prérempli
|
||||
const eventData = {
|
||||
...arg,
|
||||
extendedProps: {
|
||||
id_intervenant: currentUserId
|
||||
}
|
||||
};
|
||||
|
||||
openModal('create', eventData);
|
||||
},
|
||||
// Charger les événements depuis l'endpoint intervenant
|
||||
events: async function(fetchInfo, successCallback, failureCallback) {
|
||||
try {
|
||||
const filters = collectIntervenantFilters();
|
||||
const params = {
|
||||
start: fetchInfo.startStr.split('T')[0],
|
||||
end: fetchInfo.endStr.split('T')[0],
|
||||
view_mode: 'mine', // Toujours en mode "mon agenda"
|
||||
filters: JSON.stringify(filters)
|
||||
};
|
||||
|
||||
const events = await apiFetch('intervenant/agenda?' + new URLSearchParams(params));
|
||||
|
||||
// Formater pour FullCalendar
|
||||
const formattedEvents = events.map(event => {
|
||||
// Extraire la date et l'heure depuis start si disponibles
|
||||
let date_rdv = null;
|
||||
let heure_rdv = null;
|
||||
if (event.start) {
|
||||
const startDate = new Date(event.start);
|
||||
date_rdv = startDate.toISOString().split('T')[0];
|
||||
heure_rdv = startDate.toTimeString().substring(0, 5);
|
||||
}
|
||||
|
||||
return {
|
||||
id: event.id,
|
||||
title: event.title,
|
||||
start: event.start,
|
||||
end: event.end,
|
||||
backgroundColor: getEventColor(event),
|
||||
borderColor: getEventColor(event),
|
||||
textColor: '#fff',
|
||||
// Seuls les événements "à moi" sont éditables côté UI
|
||||
editable: !!event.is_mine,
|
||||
durationEditable: !!event.is_mine,
|
||||
startEditable: !!event.is_mine,
|
||||
extendedProps: {
|
||||
type: event.type,
|
||||
statut: event.statut,
|
||||
is_mine: event.is_mine,
|
||||
show_comments: event.show_comments,
|
||||
commentaire: event.commentaire,
|
||||
beneficiaire: event.beneficiaire,
|
||||
local: event.local,
|
||||
intervenant_nom: event.intervenant_nom,
|
||||
date: date_rdv,
|
||||
date_rdv: date_rdv,
|
||||
heure: heure_rdv,
|
||||
heure_rdv: heure_rdv,
|
||||
langue: event.langue,
|
||||
langues_disponibles: event.langues_disponibles || null
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
successCallback(formattedEvents);
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement de l\'agenda:', error);
|
||||
failureCallback(error);
|
||||
}
|
||||
},
|
||||
// Déplacement d'un événement (drag & drop)
|
||||
eventDrop: async function(info) {
|
||||
const props = info.event.extendedProps || {};
|
||||
|
||||
// Vérifier que l'événement appartient bien à l'intervenant
|
||||
if (!props.is_mine) {
|
||||
console.warn('❌ Tentative de déplacement d’un événement qui ne vous appartient pas');
|
||||
info.revert();
|
||||
notifyError('Vous ne pouvez déplacer que vos propres rendez-vous');
|
||||
return;
|
||||
}
|
||||
|
||||
// Vérifier les permissions globales
|
||||
const userCanEdit = window.crviPermissions && window.crviPermissions.can_edit;
|
||||
if (!userCanEdit) {
|
||||
console.warn('❌ Utilisateur non autorisé à déplacer des événements (permissions WP)');
|
||||
info.revert();
|
||||
notifyError('Vous n\'êtes pas autorisé à modifier les événements');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const newStart = info.event.start;
|
||||
const newEnd = info.event.end || new Date(newStart.getTime() + 60 * 60 * 1000); // +1h par défaut
|
||||
|
||||
const updateData = {
|
||||
date_rdv: newStart.toISOString().split('T')[0],
|
||||
heure_rdv: newStart.toTimeString().substring(0, 5),
|
||||
date_fin: newEnd.toISOString().split('T')[0],
|
||||
heure_fin: newEnd.toTimeString().substring(0, 5)
|
||||
};
|
||||
|
||||
await updateEvent(info.event.id, updateData);
|
||||
notifySuccess('Événement déplacé avec succès');
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du déplacement de l\'événement (front intervenant):', error);
|
||||
info.revert();
|
||||
notifyError('Erreur lors du déplacement de l\'événement');
|
||||
}
|
||||
},
|
||||
// Redimensionnement d'un événement (changement de durée)
|
||||
eventResize: async function(info) {
|
||||
const props = info.event.extendedProps || {};
|
||||
|
||||
if (!props.is_mine) {
|
||||
console.warn('❌ Tentative de redimensionnement d’un événement qui ne vous appartient pas');
|
||||
info.revert();
|
||||
notifyError('Vous ne pouvez modifier que vos propres rendez-vous');
|
||||
return;
|
||||
}
|
||||
|
||||
const userCanEdit = window.crviPermissions && window.crviPermissions.can_edit;
|
||||
if (!userCanEdit) {
|
||||
console.warn('❌ Utilisateur non autorisé à redimensionner des événements (permissions WP)');
|
||||
info.revert();
|
||||
notifyError('Vous n\'êtes pas autorisé à modifier les événements');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const newEnd = info.event.end;
|
||||
|
||||
const updateData = {
|
||||
date_fin: newEnd.toISOString().split('T')[0],
|
||||
heure_fin: newEnd.toTimeString().substring(0, 5)
|
||||
};
|
||||
|
||||
await updateEvent(info.event.id, updateData);
|
||||
notifySuccess('Événement redimensionné avec succès');
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du redimensionnement de l\'événement (front intervenant):', error);
|
||||
info.revert();
|
||||
notifyError('Erreur lors du redimensionnement de l\'événement');
|
||||
}
|
||||
},
|
||||
eventClick: function(info) {
|
||||
// Ouvrir le modal avec les détails (adapter le modal existant)
|
||||
openIntervenantEventModal(info.event);
|
||||
},
|
||||
// Ajouter les popovers Bootstrap sur les événements
|
||||
eventDidMount: function(arg) {
|
||||
const eventEl = arg.el;
|
||||
const event = arg.event;
|
||||
const eventProps = event.extendedProps || {};
|
||||
|
||||
// Fonction utilitaire pour formater une date au format français
|
||||
function formatDateToFrench(dateString) {
|
||||
if (!dateString) return '';
|
||||
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
if (isNaN(date.getTime())) {
|
||||
// Si ce n'est pas une date valide, essayer le format YYYY-MM-DD
|
||||
const parts = dateString.split('-');
|
||||
if (parts.length === 3) {
|
||||
return `${parts[2]}/${parts[1]}/${parts[0]}`;
|
||||
}
|
||||
return dateString;
|
||||
}
|
||||
|
||||
return date.toLocaleDateString('fr-FR');
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du formatage de la date:', error);
|
||||
return dateString;
|
||||
}
|
||||
}
|
||||
|
||||
// Fonction utilitaire pour extraire le nom complet d'une entité
|
||||
function getEntityDisplayName(entity, entityType = '') {
|
||||
if (!entity) return '';
|
||||
|
||||
// Si c'est déjà une chaîne, la retourner
|
||||
if (typeof entity === 'string') return entity;
|
||||
|
||||
// Si c'est un objet avec une propriété nom
|
||||
if (entity && typeof entity === 'object') {
|
||||
// Pour les bénéficiaires et intervenants, combiner prénom et nom
|
||||
if (entityType === 'beneficiaire' || entityType === 'intervenant') {
|
||||
const prenom = entity.prenom || '';
|
||||
const nom = entity.nom || '';
|
||||
return `${prenom} ${nom}`.trim() || entity.nom || entity.display_name || '';
|
||||
}
|
||||
|
||||
// Pour les locaux, utiliser le nom
|
||||
if (entityType === 'local') {
|
||||
return entity.nom || entity.display_name || '';
|
||||
}
|
||||
|
||||
// Fallback générique
|
||||
return entity.nom || entity.display_name || entity.name || '';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
// Extraire la date et l'heure depuis event.start si pas dans extendedProps
|
||||
let dateStr = eventProps.date || eventProps.date_rdv || '';
|
||||
let heureStr = eventProps.heure || eventProps.heure_rdv || '';
|
||||
|
||||
if (!dateStr && event.start) {
|
||||
const startDate = new Date(event.start);
|
||||
dateStr = startDate.toISOString().split('T')[0];
|
||||
}
|
||||
if (!heureStr && event.start) {
|
||||
const startDate = new Date(event.start);
|
||||
heureStr = startDate.toTimeString().substring(0, 5);
|
||||
}
|
||||
|
||||
// Créer le contenu du popover
|
||||
const popoverContent = `
|
||||
<div class="event-popover">
|
||||
<h6 class="mb-2">${event.title}</h6>
|
||||
<div class="mb-1"><strong>Date:</strong> ${formatDateToFrench(dateStr)}</div>
|
||||
<div class="mb-1"><strong>Heure:</strong> ${heureStr}</div>
|
||||
<div class="mb-1"><strong>Type:</strong> ${eventProps.type || ''}</div>
|
||||
${eventProps.langue ? `<div class="mb-1"><strong>Langue:</strong> ${eventProps.langue}</div>` : ''}
|
||||
${eventProps.beneficiaire ? `<div class="mb-1"><strong>Bénéficiaire:</strong> ${typeof eventProps.beneficiaire === 'string' ? eventProps.beneficiaire : getEntityDisplayName(eventProps.beneficiaire, 'beneficiaire')}</div>` : ''}
|
||||
${eventProps.intervenant_nom ? `<div class="mb-1"><strong>Intervenant:</strong> ${eventProps.intervenant_nom}</div>` : ''}
|
||||
${eventProps.local ? `<div class="mb-1"><strong>Local:</strong> ${typeof eventProps.local === 'string' ? eventProps.local : getEntityDisplayName(eventProps.local, 'local')}</div>` : ''}
|
||||
${eventProps.commentaire && eventProps.show_comments ? `<div class="mb-1"><strong>Commentaire:</strong> ${eventProps.commentaire}</div>` : ''}
|
||||
<div class="mb-1"><strong>Statut:</strong> ${eventProps.statut || ''}</div>
|
||||
<div class="mt-2">
|
||||
<small class="text-muted">Cliquez pour plus de détails</small>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Initialiser le popover Bootstrap
|
||||
if (window.bootstrap && window.bootstrap.Popover) {
|
||||
const popover = new bootstrap.Popover(eventEl, {
|
||||
title: 'Détails de l\'événement',
|
||||
content: popoverContent,
|
||||
html: true,
|
||||
trigger: 'hover',
|
||||
placement: 'top',
|
||||
container: 'body',
|
||||
template: '<div class="popover event-popover" role="tooltip"><div class="popover-arrow"></div><h3 class="popover-header"></h3><div class="popover-body"></div></div>'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
calendar.render();
|
||||
window.currentCalendar = calendar;
|
||||
|
||||
// Masquer l'indicateur de chargement
|
||||
const loadingIndicator = document.getElementById('loading-indicator');
|
||||
if (loadingIndicator) {
|
||||
loadingIndicator.style.display = 'none';
|
||||
}
|
||||
|
||||
// Initialiser les filtres adaptés pour intervenant
|
||||
initializeIntervenantFilters(calendar);
|
||||
|
||||
return calendar;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise le calendrier pour les collègues
|
||||
*/
|
||||
export function initializeColleaguesCalendar() {
|
||||
console.log('🚀 Initialisation du calendrier collègues...');
|
||||
|
||||
const calendarEl = document.getElementById('agenda-calendar-colleagues');
|
||||
if (!calendarEl) {
|
||||
console.error('❌ Élément agenda-calendar-colleagues non trouvé');
|
||||
return null;
|
||||
}
|
||||
|
||||
currentViewMode = 'colleagues';
|
||||
|
||||
const calendar = new Calendar(calendarEl, {
|
||||
plugins: [dayGridPlugin, timeGridPlugin, listPlugin, interactionPlugin],
|
||||
initialView: 'dayGridMonth',
|
||||
eventDisplay: 'block',
|
||||
locale: 'fr',
|
||||
firstDay: 1,
|
||||
headerToolbar: {
|
||||
left: 'prev,next today',
|
||||
center: 'title',
|
||||
right: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek'
|
||||
},
|
||||
buttonText: {
|
||||
today: 'Aujourd\'hui',
|
||||
month: 'Mois',
|
||||
week: 'Semaine',
|
||||
day: 'Jour',
|
||||
list: 'Liste'
|
||||
},
|
||||
height: 'auto',
|
||||
editable: false,
|
||||
selectable: false,
|
||||
dayMaxEvents: 3,
|
||||
weekends: true,
|
||||
moreLinkClick: 'popover', // Afficher les événements cachés dans un popover
|
||||
moreLinkContent: function(arg) {
|
||||
// Personnaliser le texte du lien "+x plus"
|
||||
const count = arg.num;
|
||||
return `+${count} plus`;
|
||||
},
|
||||
events: async function(fetchInfo, successCallback, failureCallback) {
|
||||
try {
|
||||
const filters = collectColleaguesFilters();
|
||||
const params = {
|
||||
start: fetchInfo.startStr.split('T')[0],
|
||||
end: fetchInfo.endStr.split('T')[0],
|
||||
view_mode: 'colleagues',
|
||||
filters: JSON.stringify(filters)
|
||||
};
|
||||
|
||||
const events = await apiFetch('intervenant/agenda?' + new URLSearchParams(params));
|
||||
|
||||
const formattedEvents = events.map(event => {
|
||||
// Extraire la date et l'heure depuis start si disponibles
|
||||
let date_rdv = null;
|
||||
let heure_rdv = null;
|
||||
if (event.start) {
|
||||
const startDate = new Date(event.start);
|
||||
date_rdv = startDate.toISOString().split('T')[0];
|
||||
heure_rdv = startDate.toTimeString().substring(0, 5);
|
||||
}
|
||||
|
||||
return {
|
||||
id: event.id,
|
||||
title: event.title,
|
||||
start: event.start,
|
||||
end: event.end,
|
||||
backgroundColor: getEventColor(event),
|
||||
borderColor: getEventColor(event),
|
||||
textColor: '#fff',
|
||||
extendedProps: {
|
||||
type: event.type,
|
||||
statut: event.statut,
|
||||
is_mine: event.is_mine,
|
||||
show_comments: false, // Jamais de commentaires pour les collègues
|
||||
beneficiaire: event.beneficiaire,
|
||||
local: event.local,
|
||||
intervenant_nom: event.intervenant_nom,
|
||||
date: date_rdv,
|
||||
date_rdv: date_rdv,
|
||||
heure: heure_rdv,
|
||||
heure_rdv: heure_rdv,
|
||||
langue: event.langue
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
successCallback(formattedEvents);
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement de l\'agenda des collègues:', error);
|
||||
failureCallback(error);
|
||||
}
|
||||
},
|
||||
eventClick: function(info) {
|
||||
openIntervenantEventModal(info.event);
|
||||
},
|
||||
// Ajouter les popovers Bootstrap sur les événements
|
||||
eventDidMount: function(arg) {
|
||||
const eventEl = arg.el;
|
||||
const event = arg.event;
|
||||
const eventProps = event.extendedProps || {};
|
||||
|
||||
// Fonction utilitaire pour formater une date au format français
|
||||
function formatDateToFrench(dateString) {
|
||||
if (!dateString) return '';
|
||||
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
if (isNaN(date.getTime())) {
|
||||
// Si ce n'est pas une date valide, essayer le format YYYY-MM-DD
|
||||
const parts = dateString.split('-');
|
||||
if (parts.length === 3) {
|
||||
return `${parts[2]}/${parts[1]}/${parts[0]}`;
|
||||
}
|
||||
return dateString;
|
||||
}
|
||||
|
||||
return date.toLocaleDateString('fr-FR');
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du formatage de la date:', error);
|
||||
return dateString;
|
||||
}
|
||||
}
|
||||
|
||||
// Fonction utilitaire pour extraire le nom complet d'une entité
|
||||
function getEntityDisplayName(entity, entityType = '') {
|
||||
if (!entity) return '';
|
||||
|
||||
// Si c'est déjà une chaîne, la retourner
|
||||
if (typeof entity === 'string') return entity;
|
||||
|
||||
// Si c'est un objet avec une propriété nom
|
||||
if (entity && typeof entity === 'object') {
|
||||
// Pour les bénéficiaires et intervenants, combiner prénom et nom
|
||||
if (entityType === 'beneficiaire' || entityType === 'intervenant') {
|
||||
const prenom = entity.prenom || '';
|
||||
const nom = entity.nom || '';
|
||||
return `${prenom} ${nom}`.trim() || entity.nom || entity.display_name || '';
|
||||
}
|
||||
|
||||
// Pour les locaux, utiliser le nom
|
||||
if (entityType === 'local') {
|
||||
return entity.nom || entity.display_name || '';
|
||||
}
|
||||
|
||||
// Fallback générique
|
||||
return entity.nom || entity.display_name || entity.name || '';
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
// Extraire la date et l'heure depuis event.start si pas dans extendedProps
|
||||
let dateStr = eventProps.date || eventProps.date_rdv || '';
|
||||
let heureStr = eventProps.heure || eventProps.heure_rdv || '';
|
||||
|
||||
if (!dateStr && event.start) {
|
||||
const startDate = new Date(event.start);
|
||||
dateStr = startDate.toISOString().split('T')[0];
|
||||
}
|
||||
if (!heureStr && event.start) {
|
||||
const startDate = new Date(event.start);
|
||||
heureStr = startDate.toTimeString().substring(0, 5);
|
||||
}
|
||||
|
||||
// Créer le contenu du popover
|
||||
const popoverContent = `
|
||||
<div class="event-popover">
|
||||
<h6 class="mb-2">${event.title}</h6>
|
||||
<div class="mb-1"><strong>Date:</strong> ${formatDateToFrench(dateStr)}</div>
|
||||
<div class="mb-1"><strong>Heure:</strong> ${heureStr}</div>
|
||||
<div class="mb-1"><strong>Type:</strong> ${eventProps.type || ''}</div>
|
||||
${eventProps.langue ? `<div class="mb-1"><strong>Langue:</strong> ${eventProps.langue}</div>` : ''}
|
||||
${eventProps.beneficiaire ? `<div class="mb-1"><strong>Bénéficiaire:</strong> ${typeof eventProps.beneficiaire === 'string' ? eventProps.beneficiaire : getEntityDisplayName(eventProps.beneficiaire, 'beneficiaire')}</div>` : ''}
|
||||
${eventProps.intervenant_nom ? `<div class="mb-1"><strong>Intervenant:</strong> ${eventProps.intervenant_nom}</div>` : ''}
|
||||
${eventProps.local ? `<div class="mb-1"><strong>Local:</strong> ${typeof eventProps.local === 'string' ? eventProps.local : getEntityDisplayName(eventProps.local, 'local')}</div>` : ''}
|
||||
${eventProps.commentaire && eventProps.show_comments ? `<div class="mb-1"><strong>Commentaire:</strong> ${eventProps.commentaire}</div>` : ''}
|
||||
<div class="mb-1"><strong>Statut:</strong> ${eventProps.statut || ''}</div>
|
||||
<div class="mt-2">
|
||||
<small class="text-muted">Cliquez pour plus de détails</small>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Initialiser le popover Bootstrap
|
||||
if (window.bootstrap && window.bootstrap.Popover) {
|
||||
const popover = new bootstrap.Popover(eventEl, {
|
||||
title: 'Détails de l\'événement',
|
||||
content: popoverContent,
|
||||
html: true,
|
||||
trigger: 'hover',
|
||||
placement: 'top',
|
||||
container: 'body',
|
||||
template: '<div class="popover event-popover" role="tooltip"><div class="popover-arrow"></div><h3 class="popover-header"></h3><div class="popover-body"></div></div>'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
calendar.render();
|
||||
window.currentColleaguesCalendar = calendar;
|
||||
|
||||
const loadingIndicator = document.getElementById('loading-indicator-colleagues');
|
||||
if (loadingIndicator) {
|
||||
loadingIndicator.style.display = 'none';
|
||||
}
|
||||
|
||||
initializeColleaguesFilters(calendar);
|
||||
|
||||
return calendar;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collecte les filtres depuis le formulaire "Mon agenda"
|
||||
*/
|
||||
function collectIntervenantFilters() {
|
||||
const filters = {};
|
||||
|
||||
const local = document.getElementById('local')?.value;
|
||||
if (local) filters.local_id = parseInt(local);
|
||||
|
||||
const beneficiaire = document.getElementById('beneficiaire')?.value;
|
||||
if (beneficiaire) filters.beneficiaire_id = parseInt(beneficiaire);
|
||||
|
||||
const typeRdv = document.getElementById('type_rdv')?.value;
|
||||
if (typeRdv) filters.type_rdv = typeRdv;
|
||||
|
||||
const typeIntervention = document.getElementById('type_intervention')?.value;
|
||||
if (typeIntervention) filters.type_intervention = parseInt(typeIntervention);
|
||||
|
||||
const langue = document.getElementById('langue')?.value;
|
||||
if (langue) filters.langue = langue;
|
||||
|
||||
const permanences = document.getElementById('permanences_non_assignees')?.checked;
|
||||
if (permanences) filters.permanences_non_assignees = true;
|
||||
|
||||
return filters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collecte les filtres depuis le formulaire "Agenda des collègues"
|
||||
*/
|
||||
function collectColleaguesFilters() {
|
||||
const filters = {};
|
||||
|
||||
const local = document.getElementById('local-colleagues')?.value;
|
||||
if (local) filters.local_id = parseInt(local);
|
||||
|
||||
const beneficiaire = document.getElementById('beneficiaire-colleagues')?.value;
|
||||
if (beneficiaire) filters.beneficiaire_id = parseInt(beneficiaire);
|
||||
|
||||
const typeRdv = document.getElementById('type_rdv-colleagues')?.value;
|
||||
if (typeRdv) filters.type_rdv = typeRdv;
|
||||
|
||||
const typeIntervention = document.getElementById('type_intervention-colleagues')?.value;
|
||||
if (typeIntervention) filters.type_intervention = parseInt(typeIntervention);
|
||||
|
||||
const langue = document.getElementById('langue-colleagues')?.value;
|
||||
if (langue) filters.langue = langue;
|
||||
|
||||
return filters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise les filtres pour "Mon agenda"
|
||||
*/
|
||||
function initializeIntervenantFilters(calendar) {
|
||||
// Charger les options de filtres depuis l'endpoint disponibilités
|
||||
loadFilterOptions('intervenant');
|
||||
|
||||
// Écouter les changements de filtres
|
||||
const filterForm = document.querySelector('.filters');
|
||||
if (filterForm) {
|
||||
const filterBtn = document.getElementById('filterBtn');
|
||||
if (filterBtn) {
|
||||
filterBtn.addEventListener('click', () => {
|
||||
calendar.refetchEvents();
|
||||
});
|
||||
}
|
||||
|
||||
const resetBtn = document.getElementById('resetFiltersBtn');
|
||||
if (resetBtn) {
|
||||
resetBtn.addEventListener('click', () => {
|
||||
filterForm.reset();
|
||||
calendar.refetchEvents();
|
||||
});
|
||||
}
|
||||
|
||||
// Bouton "Ajouter un événement"
|
||||
const addEventBtn = document.getElementById('addEventBtn');
|
||||
if (addEventBtn) {
|
||||
addEventBtn.addEventListener('click', () => {
|
||||
// Vérifier les permissions de création
|
||||
const userCanCreate = window.crviPermissions && window.crviPermissions.can_create;
|
||||
if (!userCanCreate) {
|
||||
notifyError('Vous n\'êtes pas autorisé à créer des événements');
|
||||
console.warn('❌ Utilisateur non autorisé à créer des événements (permissions WP)');
|
||||
return;
|
||||
}
|
||||
|
||||
// Ouvrir le modal de création avec la date du jour et l'intervenant prérempli
|
||||
const today = new Date();
|
||||
const dateStr = today.toISOString().split('T')[0];
|
||||
const startStr = dateStr + 'T09:00:00';
|
||||
const endStr = dateStr + 'T10:00:00';
|
||||
|
||||
// Préremplir avec l'intervenant actuel (utilisateur connecté)
|
||||
const currentUserId = window.crviPermissions && window.crviPermissions.user_id;
|
||||
|
||||
openModal('create', {
|
||||
start: new Date(startStr),
|
||||
end: new Date(endStr),
|
||||
startStr: startStr,
|
||||
endStr: endStr,
|
||||
allDay: false,
|
||||
extendedProps: {
|
||||
id_intervenant: currentUserId
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-filtrer sur changement
|
||||
const selects = filterForm.querySelectorAll('select, input[type="checkbox"]');
|
||||
selects.forEach(el => {
|
||||
el.addEventListener('change', () => {
|
||||
setTimeout(() => calendar.refetchEvents(), 300);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise les filtres pour "Agenda des collègues"
|
||||
*/
|
||||
function initializeColleaguesFilters(calendar) {
|
||||
loadFilterOptions('colleagues');
|
||||
|
||||
const filterForm = document.querySelector('.filters-colleagues');
|
||||
if (filterForm) {
|
||||
const filterBtn = document.getElementById('filterBtn-colleagues');
|
||||
if (filterBtn) {
|
||||
filterBtn.addEventListener('click', () => {
|
||||
calendar.refetchEvents();
|
||||
});
|
||||
}
|
||||
|
||||
const resetBtn = document.getElementById('resetFiltersBtn-colleagues');
|
||||
if (resetBtn) {
|
||||
resetBtn.addEventListener('click', () => {
|
||||
filterForm.reset();
|
||||
calendar.refetchEvents();
|
||||
});
|
||||
}
|
||||
|
||||
const selects = filterForm.querySelectorAll('select');
|
||||
selects.forEach(el => {
|
||||
el.addEventListener('change', () => {
|
||||
setTimeout(() => calendar.refetchEvents(), 300);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Charge les options des filtres depuis l'endpoint disponibilités
|
||||
*/
|
||||
async function loadFilterOptions(mode = 'intervenant') {
|
||||
try {
|
||||
const disponibilites = await apiFetch('agenda/disponibilites');
|
||||
|
||||
const prefix = mode === 'colleagues' ? '-colleagues' : '';
|
||||
|
||||
// Locaux
|
||||
const localSelect = document.getElementById(`local${prefix}`);
|
||||
if (localSelect && disponibilites.locaux) {
|
||||
disponibilites.locaux.forEach(local => {
|
||||
const option = document.createElement('option');
|
||||
option.value = local.id;
|
||||
option.textContent = local.nom;
|
||||
localSelect.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
// Bénéficiaires
|
||||
const beneficiaireSelect = document.getElementById(`beneficiaire${prefix}`);
|
||||
if (beneficiaireSelect && disponibilites.beneficiaires) {
|
||||
disponibilites.beneficiaires.forEach(benef => {
|
||||
const option = document.createElement('option');
|
||||
option.value = benef.id;
|
||||
option.textContent = benef.nom;
|
||||
beneficiaireSelect.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
// Types d'intervention
|
||||
const typeInterventionSelect = document.getElementById(`type_intervention${prefix}`);
|
||||
if (typeInterventionSelect && disponibilites.types_intervention) {
|
||||
disponibilites.types_intervention.forEach(type => {
|
||||
const option = document.createElement('option');
|
||||
option.value = type.id;
|
||||
option.textContent = type.nom;
|
||||
typeInterventionSelect.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
// Langues
|
||||
const langueSelect = document.getElementById(`langue${prefix}`);
|
||||
if (langueSelect && disponibilites.langues) {
|
||||
disponibilites.langues.forEach(langue => {
|
||||
const option = document.createElement('option');
|
||||
option.value = langue.id;
|
||||
option.textContent = langue.nom;
|
||||
langueSelect.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
// Initialiser Select2
|
||||
if (window.jQuery && jQuery().select2) {
|
||||
jQuery(`select${prefix ? '[id$="-colleagues"]' : ':not([id$="-colleagues"])'}`).select2();
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement des options de filtres:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Détermine la couleur d'un événement selon son type/statut
|
||||
*/
|
||||
function getEventColor(event) {
|
||||
if (event.type === 'permanence') {
|
||||
return '#17a2b8'; // Teal pour les permanences
|
||||
}
|
||||
|
||||
// Couleur selon le statut
|
||||
switch (event.statut) {
|
||||
case 'prevu':
|
||||
return '#28a745'; // Vert
|
||||
case 'present':
|
||||
return '#0d6efd'; // Bleu
|
||||
case 'absent':
|
||||
return '#dc3545'; // Rouge
|
||||
case 'cloture':
|
||||
return '#6c757d'; // Gris
|
||||
default:
|
||||
return '#ffc107'; // Jaune
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ouvre le modal complet (eventModal) en mode vue, comme côté admin.
|
||||
* Charge d'abord les détails complets de l'événement via l'API.
|
||||
*/
|
||||
async function openIntervenantEventModal(event) {
|
||||
const userCanView = window.crviPermissions && window.crviPermissions.can_view;
|
||||
if (!userCanView) {
|
||||
console.warn('❌ Utilisateur non autorisé à voir les événements (permissions WP)');
|
||||
notifyError('Vous n\'êtes pas autorisé à consulter les événements');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Overlay sur le calendrier pendant le chargement des détails
|
||||
const calendarEl = document.getElementById('agenda-calendar') || document.getElementById('agenda-calendar-colleagues');
|
||||
if (window.CRVI_OVERLAY && calendarEl) { window.CRVI_OVERLAY.show(calendarEl); }
|
||||
|
||||
const eventId = event.id;
|
||||
const eventDetails = await getEvent(eventId);
|
||||
|
||||
let startDate;
|
||||
let endDate;
|
||||
|
||||
try {
|
||||
// Date de début
|
||||
if (eventDetails.date_rdv && eventDetails.heure_rdv) {
|
||||
startDate = new Date(eventDetails.date_rdv + 'T' + eventDetails.heure_rdv);
|
||||
if (isNaN(startDate.getTime())) {
|
||||
startDate = new Date();
|
||||
}
|
||||
} else {
|
||||
startDate = new Date(event.start);
|
||||
}
|
||||
|
||||
// Date de fin
|
||||
if (eventDetails.date_fin && eventDetails.heure_fin) {
|
||||
endDate = new Date(eventDetails.date_fin + 'T' + eventDetails.heure_fin);
|
||||
if (isNaN(endDate.getTime())) {
|
||||
endDate = startDate;
|
||||
}
|
||||
} else {
|
||||
endDate = event.end ? new Date(event.end) : startDate;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Erreur lors de la création des dates (intervenant):', e);
|
||||
startDate = new Date();
|
||||
endDate = new Date();
|
||||
}
|
||||
|
||||
const fullEvent = {
|
||||
id: eventDetails.id,
|
||||
start: startDate,
|
||||
end: endDate,
|
||||
extendedProps: {
|
||||
// Données de base
|
||||
type: eventDetails.type,
|
||||
commentaire: eventDetails.commentaire,
|
||||
date: eventDetails.date_rdv,
|
||||
heure: eventDetails.heure_rdv,
|
||||
date_fin: eventDetails.date_fin,
|
||||
heure_fin: eventDetails.heure_fin,
|
||||
|
||||
// Relations
|
||||
id_beneficiaire: eventDetails.id_beneficiaire,
|
||||
id_intervenant: eventDetails.id_intervenant,
|
||||
id_traducteur: eventDetails.id_traducteur,
|
||||
id_local: eventDetails.id_local,
|
||||
langue: eventDetails.langue,
|
||||
langues_disponibles: eventDetails.langues_disponibles || null,
|
||||
|
||||
beneficiaire: eventDetails.beneficiaire,
|
||||
intervenant: eventDetails.intervenant,
|
||||
traducteur: eventDetails.traducteur,
|
||||
local: eventDetails.local,
|
||||
|
||||
// Données groupe
|
||||
nb_participants: eventDetails.nb_participants,
|
||||
nb_hommes: eventDetails.nb_hommes,
|
||||
nb_femmes: eventDetails.nb_femmes,
|
||||
|
||||
// Autres
|
||||
statut: eventDetails.statut,
|
||||
created_at: eventDetails.created_at,
|
||||
updated_at: eventDetails.updated_at
|
||||
}
|
||||
};
|
||||
|
||||
// Ouvrir le modal complet en mode "view" (même pattern qu'en admin)
|
||||
openModal('view', fullEvent);
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement des détails de l\'événement (intervenant):', error);
|
||||
notifyError('Erreur lors du chargement des détails de l\'événement');
|
||||
} finally {
|
||||
const calendarEl = document.getElementById('agenda-calendar') || document.getElementById('agenda-calendar-colleagues');
|
||||
if (window.CRVI_OVERLAY && calendarEl) { window.CRVI_OVERLAY.hide(calendarEl); }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne la classe Bootstrap pour le badge de statut
|
||||
*/
|
||||
function getStatusBadgeClass(statut) {
|
||||
switch (statut) {
|
||||
case 'prevu':
|
||||
return 'success';
|
||||
case 'present':
|
||||
return 'primary';
|
||||
case 'absent':
|
||||
return 'danger';
|
||||
case 'cloture':
|
||||
return 'secondary';
|
||||
case 'annule':
|
||||
return 'warning';
|
||||
default:
|
||||
return 'secondary';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise les onglets pour basculer entre mon agenda et agenda des collègues
|
||||
*/
|
||||
export function initializeIntervenantAgendaTabs() {
|
||||
const myAgendaTab = document.getElementById('my-agenda-tab');
|
||||
const colleaguesTab = document.getElementById('colleagues-agenda-tab');
|
||||
|
||||
let myCalendar = null;
|
||||
let colleaguesCalendar = null;
|
||||
|
||||
if (myAgendaTab) {
|
||||
myAgendaTab.addEventListener('shown.bs.tab', () => {
|
||||
console.log('🔄 Basculement vers Mon Agenda (mode: mine)');
|
||||
if (!myCalendar) {
|
||||
myCalendar = initializeIntervenantCalendar();
|
||||
} else {
|
||||
myCalendar.refetchEvents();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (colleaguesTab) {
|
||||
colleaguesTab.addEventListener('shown.bs.tab', () => {
|
||||
console.log('🔄 Basculement vers Agenda des collègues (mode: colleagues)');
|
||||
if (!colleaguesCalendar) {
|
||||
colleaguesCalendar = initializeColleaguesCalendar();
|
||||
} else {
|
||||
colleaguesCalendar.refetchEvents();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialiser le premier onglet (mon agenda) par défaut
|
||||
if (myAgendaTab && myAgendaTab.classList.contains('active')) {
|
||||
myCalendar = initializeIntervenantCalendar();
|
||||
}
|
||||
}
|
||||
|
||||
198
assets/js/modules/agenda-intervenant-hub.js
Normal file
198
assets/js/modules/agenda-intervenant-hub.js
Normal file
@ -0,0 +1,198 @@
|
||||
/**
|
||||
* Module Hub Intervenant
|
||||
* Gestion des RDV du jour et marquage présence/absence
|
||||
*/
|
||||
|
||||
import { apiFetch } from './agenda-api.js';
|
||||
import toastr from 'toastr';
|
||||
|
||||
/**
|
||||
* Charge les RDV du jour de l'intervenant connecté
|
||||
*/
|
||||
export async function loadRdvToday() {
|
||||
const container = document.getElementById('rdv-today-container');
|
||||
const emptyMessage = document.getElementById('rdv-today-empty');
|
||||
|
||||
if (!container) {
|
||||
console.error('Container rdv-today-container non trouvé');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (window.CRVI_OVERLAY) { window.CRVI_OVERLAY.show(container); }
|
||||
container.innerHTML = `
|
||||
<div class="text-center py-4">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Chargement...</span>
|
||||
</div>
|
||||
<p class="mt-2 text-muted">Chargement des rendez-vous...</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const rdvs = await apiFetch('intervenant/rdv-today');
|
||||
|
||||
if (!rdvs || rdvs.length === 0) {
|
||||
container.style.display = 'none';
|
||||
if (emptyMessage) {
|
||||
emptyMessage.style.display = 'block';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
container.style.display = 'block';
|
||||
if (emptyMessage) {
|
||||
emptyMessage.style.display = 'none';
|
||||
}
|
||||
|
||||
// Récupérer le template
|
||||
const template = document.getElementById('rdv-item-template');
|
||||
if (!template) {
|
||||
console.error('Template rdv-item-template non trouvé');
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = '';
|
||||
|
||||
rdvs.forEach(rdv => {
|
||||
const clone = template.content.cloneNode(true);
|
||||
|
||||
// Formatage de l'heure
|
||||
const heureDebut = formatTime(rdv.heure_rdv);
|
||||
const heureFin = formatTime(rdv.heure_fin || rdv.heure_rdv);
|
||||
|
||||
// Remplir les données
|
||||
const timeStartEl = clone.querySelector('[data-time-start]');
|
||||
const timeEndEl = clone.querySelector('[data-time-end]');
|
||||
const localEl = clone.querySelector('[data-local]');
|
||||
const beneficiaireEl = clone.querySelector('[data-beneficiaire]');
|
||||
const typeInterventionEl = clone.querySelector('[data-type-intervention]');
|
||||
const presentBtn = clone.querySelector('button.mark-presence[data-statut="present"]');
|
||||
const absentBtn = clone.querySelector('button.mark-presence[data-statut="absent"]');
|
||||
|
||||
if (timeStartEl) timeStartEl.textContent = heureDebut;
|
||||
if (timeEndEl) timeEndEl.textContent = heureFin;
|
||||
|
||||
if (localEl && rdv.local) {
|
||||
localEl.textContent = rdv.local.nom || 'Non assigné';
|
||||
}
|
||||
|
||||
if (beneficiaireEl && rdv.beneficiaire) {
|
||||
const nom = rdv.beneficiaire.prenom && rdv.beneficiaire.nom
|
||||
? `${rdv.beneficiaire.prenom} ${rdv.beneficiaire.nom}`
|
||||
: (rdv.beneficiaire.nom || 'Non assigné');
|
||||
beneficiaireEl.textContent = nom;
|
||||
}
|
||||
|
||||
if (typeInterventionEl) {
|
||||
typeInterventionEl.textContent = rdv.type_intervention || '';
|
||||
}
|
||||
|
||||
// Configurer les boutons présence/absence
|
||||
if (presentBtn) {
|
||||
presentBtn.setAttribute('data-event-id', rdv.id);
|
||||
presentBtn.addEventListener('click', () => markPresence(rdv.id, 'present'));
|
||||
}
|
||||
|
||||
if (absentBtn) {
|
||||
absentBtn.setAttribute('data-event-id', rdv.id);
|
||||
absentBtn.addEventListener('click', () => markPresence(rdv.id, 'absent'));
|
||||
}
|
||||
|
||||
// Désactiver les boutons si le statut n'est plus "prevu"
|
||||
if (rdv.statut !== 'prevu') {
|
||||
if (presentBtn) presentBtn.disabled = true;
|
||||
if (absentBtn) absentBtn.disabled = true;
|
||||
}
|
||||
|
||||
container.appendChild(clone);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement des RDV du jour:', error);
|
||||
container.innerHTML = `
|
||||
<div class="alert alert-danger">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
Erreur lors du chargement des rendez-vous : ${error.message}
|
||||
</div>
|
||||
`;
|
||||
} finally {
|
||||
if (window.CRVI_OVERLAY) { window.CRVI_OVERLAY.hide(container); }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formate une heure (format HH:MM:SS) en HH:MM
|
||||
*/
|
||||
function formatTime(timeStr) {
|
||||
if (!timeStr) return '';
|
||||
return timeStr.substring(0, 5); // Prendre HH:MM
|
||||
}
|
||||
|
||||
/**
|
||||
* Marque la présence ou l'absence pour un RDV
|
||||
*/
|
||||
async function markPresence(eventId, statut) {
|
||||
const statutLabel = statut === 'present' ? 'présent' : 'absent';
|
||||
|
||||
// Demander confirmation pour l'absence
|
||||
if (statut === 'absent') {
|
||||
const confirmed = confirm(`Êtes-vous sûr de vouloir marquer ce rendez-vous comme absent ?`);
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Désactiver les boutons pendant la requête
|
||||
const buttons = document.querySelectorAll(`button[data-event-id="${eventId}"]`);
|
||||
buttons.forEach(btn => btn.disabled = true);
|
||||
const container = document.getElementById('rdv-today-container');
|
||||
if (window.CRVI_OVERLAY && container) { window.CRVI_OVERLAY.show(container); }
|
||||
|
||||
await apiFetch('intervenant/mark-presence', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
event_id: eventId,
|
||||
statut: statut
|
||||
})
|
||||
});
|
||||
|
||||
toastr.success(
|
||||
`Présence marquée comme ${statutLabel} avec succès`,
|
||||
'Confirmation',
|
||||
{ timeOut: 3000 }
|
||||
);
|
||||
|
||||
// Recharger les RDV du jour
|
||||
await loadRdvToday();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du marquage de présence:', error);
|
||||
toastr.error(
|
||||
error.message || `Erreur lors du marquage ${statutLabel}`,
|
||||
'Erreur',
|
||||
{ timeOut: 5000 }
|
||||
);
|
||||
|
||||
// Réactiver les boutons en cas d'erreur
|
||||
const buttons = document.querySelectorAll(`button[data-event-id="${eventId}"]`);
|
||||
buttons.forEach(btn => btn.disabled = false);
|
||||
} finally {
|
||||
const container = document.getElementById('rdv-today-container');
|
||||
if (window.CRVI_OVERLAY && container) { window.CRVI_OVERLAY.hide(container); }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise le module hub intervenant
|
||||
*/
|
||||
export function initializeIntervenantHub() {
|
||||
console.log('Initialisation du hub intervenant...');
|
||||
|
||||
// Charger les RDV du jour au chargement
|
||||
loadRdvToday();
|
||||
|
||||
// Recharger toutes les 5 minutes
|
||||
setInterval(loadRdvToday, 5 * 60 * 1000);
|
||||
}
|
||||
|
||||
575
assets/js/modules/agenda-intervenant-permanences.js
Normal file
575
assets/js/modules/agenda-intervenant-permanences.js
Normal file
@ -0,0 +1,575 @@
|
||||
/**
|
||||
* Module Permanences Intervenant
|
||||
* Gestion du formulaire d'encodage des permanences
|
||||
*/
|
||||
|
||||
import { apiFetch } from './agenda-api.js';
|
||||
import toastr from 'toastr';
|
||||
|
||||
/**
|
||||
* Initialise le module permanences intervenant
|
||||
*/
|
||||
export function initializePermanences() {
|
||||
console.log('🚀 Initialisation du module permanences intervenant...');
|
||||
|
||||
const form = document.getElementById('permanences-form');
|
||||
if (!form) {
|
||||
console.error('Formulaire permanences-form non trouvé');
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialiser le champ mois de début avec le mois actuel
|
||||
const moisDebutInput = document.getElementById('mois-debut');
|
||||
if (moisDebutInput && !moisDebutInput.value) {
|
||||
const today = new Date();
|
||||
const year = today.getFullYear();
|
||||
const month = String(today.getMonth() + 1).padStart(2, '0');
|
||||
moisDebutInput.value = `${year}-${month}`;
|
||||
}
|
||||
|
||||
// Initialiser Select2 pour le champ langues
|
||||
initializeSelect2();
|
||||
|
||||
// Écouter les changements pour mettre à jour l'aperçu
|
||||
setupPreviewListeners();
|
||||
|
||||
// Gérer la soumission du formulaire
|
||||
form.addEventListener('submit', handleFormSubmit);
|
||||
|
||||
// Calculer l'aperçu initial
|
||||
updatePreview();
|
||||
|
||||
// Initialiser le formulaire d'import CSV
|
||||
initializeCsvImport();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise Select2 pour le champ de sélection des langues
|
||||
*/
|
||||
function initializeSelect2() {
|
||||
const languesSelect = document.getElementById('langues-permanences');
|
||||
if (languesSelect && typeof jQuery !== 'undefined' && jQuery.fn.select2) {
|
||||
jQuery(languesSelect).select2({
|
||||
placeholder: 'Sélectionnez une ou plusieurs langues',
|
||||
allowClear: true,
|
||||
width: '100%',
|
||||
language: {
|
||||
noResults: function() {
|
||||
return "Aucun résultat trouvé";
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure les listeners pour mettre à jour l'aperçu en temps réel
|
||||
*/
|
||||
function setupPreviewListeners() {
|
||||
// Écouter les changements de mois de début
|
||||
const moisDebut = document.getElementById('mois-debut');
|
||||
if (moisDebut) {
|
||||
moisDebut.addEventListener('change', updatePreview);
|
||||
moisDebut.addEventListener('input', updatePreview);
|
||||
}
|
||||
|
||||
// Écouter les changements de période
|
||||
const periodInputs = document.querySelectorAll('input[name="periode"]');
|
||||
periodInputs.forEach(input => {
|
||||
input.addEventListener('change', updatePreview);
|
||||
});
|
||||
|
||||
// Écouter les changements de jours
|
||||
const joursInputs = document.querySelectorAll('input[name="jours[]"]');
|
||||
joursInputs.forEach(input => {
|
||||
input.addEventListener('change', updatePreview);
|
||||
});
|
||||
|
||||
// Écouter les changements des heures sélectionnées
|
||||
const heuresInputs = document.querySelectorAll('input[name="heures[]"]');
|
||||
heuresInputs.forEach(input => {
|
||||
input.addEventListener('change', updatePreview);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule et affiche l'aperçu des permanences qui seront créées
|
||||
*/
|
||||
function updatePreview() {
|
||||
const periode = parseInt(document.querySelector('input[name="periode"]:checked')?.value || '3');
|
||||
const joursChecked = Array.from(document.querySelectorAll('input[name="jours[]"]:checked'))
|
||||
.map(input => input.value);
|
||||
const heuresChecked = Array.from(document.querySelectorAll('input[name="heures[]"]:checked'))
|
||||
.map(input => input.value)
|
||||
.sort(); // Trier les heures pour un affichage cohérent
|
||||
const moisDebut = document.getElementById('mois-debut')?.value;
|
||||
|
||||
if (heuresChecked.length === 0) {
|
||||
clearPreview();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!moisDebut) {
|
||||
clearPreview();
|
||||
return;
|
||||
}
|
||||
|
||||
if (joursChecked.length === 0) {
|
||||
clearPreview();
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculer les tranches horaires à partir des heures sélectionnées
|
||||
// Chaque heure sélectionnée = 1 tranche d'1 heure
|
||||
const tranches = heuresChecked.map(heureDebut => {
|
||||
const [h, m] = heureDebut.split(':').map(Number);
|
||||
const heureFin = `${String(h + 1).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
|
||||
return {
|
||||
debut: heureDebut,
|
||||
fin: heureFin
|
||||
};
|
||||
});
|
||||
|
||||
// Calculer les dates à partir du mois de début sélectionné
|
||||
const [year, month] = moisDebut.split('-').map(Number);
|
||||
const startDate = new Date(year, month - 1, 1); // Premier jour du mois sélectionné
|
||||
const endDate = new Date(year, month - 1 + periode, 0); // Dernier jour du mois de fin
|
||||
|
||||
// Compter les occurrences pour chaque jour sélectionné
|
||||
let totalEvents = 0;
|
||||
const joursMapping = {
|
||||
'lundi': 1,
|
||||
'mardi': 2,
|
||||
'mercredi': 3,
|
||||
'jeudi': 4,
|
||||
'vendredi': 5,
|
||||
'samedi': 6,
|
||||
'dimanche': 0
|
||||
};
|
||||
|
||||
const currentDate = new Date(startDate);
|
||||
while (currentDate <= endDate) {
|
||||
const dayOfWeek = currentDate.getDay();
|
||||
const jourName = Object.keys(joursMapping).find(k => joursMapping[k] === dayOfWeek);
|
||||
|
||||
if (joursChecked.includes(jourName)) {
|
||||
totalEvents += tranches.length;
|
||||
}
|
||||
|
||||
currentDate.setDate(currentDate.getDate() + 1);
|
||||
}
|
||||
|
||||
// Afficher l'aperçu
|
||||
displayPreview({
|
||||
periode: periode,
|
||||
jours: joursChecked,
|
||||
tranches: tranches,
|
||||
totalEvents: totalEvents,
|
||||
dateDebut: startDate,
|
||||
dateFin: endDate
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcule les tranches horaires (par tranche de 1 heure)
|
||||
*/
|
||||
function calculateTranches(debut, fin) {
|
||||
const tranches = [];
|
||||
|
||||
const [debutHour, debutMin] = debut.split(':').map(Number);
|
||||
const [finHour, finMin] = fin.split(':').map(Number);
|
||||
|
||||
let currentHour = debutHour;
|
||||
let currentMin = debutMin;
|
||||
|
||||
while (currentHour < finHour || (currentHour === finHour && currentMin < finMin)) {
|
||||
const trancheDebut = `${String(currentHour).padStart(2, '0')}:${String(currentMin).padStart(2, '0')}`;
|
||||
|
||||
// Ajouter 1 heure
|
||||
currentHour++;
|
||||
if (currentHour >= 24) {
|
||||
break; // Ne pas dépasser minuit
|
||||
}
|
||||
|
||||
const trancheFin = `${String(currentHour).padStart(2, '0')}:${String(currentMin).padStart(2, '0')}`;
|
||||
|
||||
tranches.push({
|
||||
debut: trancheDebut,
|
||||
fin: trancheFin
|
||||
});
|
||||
}
|
||||
|
||||
return tranches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Affiche l'aperçu des permanences
|
||||
*/
|
||||
function displayPreview(data) {
|
||||
const container = document.getElementById('preview-container');
|
||||
if (!container) return;
|
||||
|
||||
const joursLabels = {
|
||||
'lundi': 'Lundi',
|
||||
'mardi': 'Mardi',
|
||||
'mercredi': 'Mercredi',
|
||||
'jeudi': 'Jeudi',
|
||||
'vendredi': 'Vendredi',
|
||||
'samedi': 'Samedi',
|
||||
'dimanche': 'Dimanche'
|
||||
};
|
||||
|
||||
const joursDisplay = data.jours.map(j => joursLabels[j] || j).join(', ');
|
||||
const dateDebutStr = data.dateDebut.toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
||||
const dateFinStr = data.dateFin.toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="alert alert-info">
|
||||
<h5 class="alert-heading">
|
||||
<i class="fas fa-info-circle me-2"></i>Aperçu des permanences
|
||||
</h5>
|
||||
<hr>
|
||||
<p class="mb-2">
|
||||
<strong>Période :</strong> ${data.periode} mois (du ${dateDebutStr} au ${dateFinStr})
|
||||
</p>
|
||||
<p class="mb-2">
|
||||
<strong>Jours sélectionnés :</strong> ${joursDisplay || 'Aucun'}
|
||||
</p>
|
||||
<p class="mb-2">
|
||||
<strong>Tranches horaires :</strong>
|
||||
${data.tranches.length > 0
|
||||
? data.tranches.map(t => `${t.debut} - ${t.fin}`).join(', ')
|
||||
: 'Aucune tranche valide'}
|
||||
</p>
|
||||
<p class="mb-0">
|
||||
<strong class="text-primary">Nombre total d'événements à créer : ${data.totalEvents}</strong>
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Efface l'aperçu
|
||||
*/
|
||||
function clearPreview() {
|
||||
const container = document.getElementById('preview-container');
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="alert alert-warning">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
Veuillez sélectionner au moins un jour et une plage horaire valide pour voir l'aperçu.
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gère la soumission du formulaire
|
||||
*/
|
||||
async function handleFormSubmit(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const formElement = document.getElementById('permanences-form');
|
||||
|
||||
// Récupérer les données
|
||||
const periode = parseInt(document.querySelector('input[name="periode"]:checked')?.value || '3');
|
||||
const moisDebut = document.getElementById('mois-debut')?.value;
|
||||
const jours = Array.from(document.querySelectorAll('input[name="jours[]"]:checked'))
|
||||
.map(input => input.value);
|
||||
const heures = Array.from(document.querySelectorAll('input[name="heures[]"]:checked'))
|
||||
.map(input => input.value);
|
||||
const dureePermanence = document.querySelector('input[name="duree_permanence"]:checked')?.value || '1h';
|
||||
const nbTranches = dureePermanence === '15min' ? parseInt(document.getElementById('nb-tranches')?.value || '1') : null;
|
||||
// Récupérer les langues sélectionnées via Select2
|
||||
const languesSelect = document.getElementById('langues-permanences');
|
||||
const langues = languesSelect && typeof jQuery !== 'undefined' && jQuery(languesSelect).val()
|
||||
? jQuery(languesSelect).val().filter(value => value !== '')
|
||||
: [];
|
||||
const informationsComplementaires = document.getElementById('informations-complementaires')?.value || '';
|
||||
|
||||
// Validation
|
||||
if (!moisDebut) {
|
||||
toastr.warning('Veuillez sélectionner un mois de début', 'Validation');
|
||||
return;
|
||||
}
|
||||
|
||||
if (jours.length === 0) {
|
||||
toastr.warning('Veuillez sélectionner au moins un jour', 'Validation');
|
||||
return;
|
||||
}
|
||||
|
||||
if (heures.length === 0) {
|
||||
toastr.warning('Veuillez sélectionner au moins une heure de permanence', 'Validation');
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculer les tranches à partir des heures sélectionnées
|
||||
const tranches = heures.map(heureDebut => {
|
||||
const [h, m] = heureDebut.split(':').map(Number);
|
||||
const heureFin = `${String(h + 1).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
|
||||
return {
|
||||
debut: heureDebut,
|
||||
fin: heureFin
|
||||
};
|
||||
});
|
||||
|
||||
// Calculer la plage horaire globale (min et max des heures sélectionnées)
|
||||
const heuresSorted = heures.map(h => {
|
||||
const [hour] = h.split(':').map(Number);
|
||||
return hour;
|
||||
}).sort((a, b) => a - b);
|
||||
|
||||
const heureMin = heuresSorted[0];
|
||||
const heureMax = heuresSorted[heuresSorted.length - 1];
|
||||
const plageDebut = `${String(heureMin).padStart(2, '0')}:00`;
|
||||
const plageFin = `${String(heureMax + 1).padStart(2, '0')}:00`;
|
||||
|
||||
// Calculer les dates à partir du mois de début pour l'estimation
|
||||
const [year, month] = moisDebut.split('-').map(Number);
|
||||
const startDate = new Date(year, month - 1, 1);
|
||||
const endDate = new Date(year, month - 1 + periode, 0);
|
||||
|
||||
// Demander confirmation si beaucoup d'événements
|
||||
const estimatedEvents = estimateTotalEvents(startDate, endDate, jours, tranches);
|
||||
if (estimatedEvents > 50) {
|
||||
const confirmed = confirm(
|
||||
`Vous allez créer environ ${estimatedEvents} événements. Êtes-vous sûr de vouloir continuer ?`
|
||||
);
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Désactiver le formulaire
|
||||
const submitBtn = event.target.querySelector('button[type="submit"]');
|
||||
if (submitBtn) {
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Création en cours...';
|
||||
}
|
||||
if (window.CRVI_OVERLAY && formElement) { window.CRVI_OVERLAY.show(formElement); }
|
||||
|
||||
// Préparer les données
|
||||
const data = {
|
||||
periode: periode,
|
||||
mois_debut: moisDebut,
|
||||
jours: jours,
|
||||
plage_horaire: {
|
||||
debut: plageDebut,
|
||||
fin: plageFin
|
||||
},
|
||||
heures: heures, // Envoyer aussi les heures individuelles pour le backend
|
||||
duree_permanence: dureePermanence, // '1h' ou '15min'
|
||||
nb_tranches: nbTranches, // Nombre de tranches si 15min (1-4), null si 1h
|
||||
langues: langues, // Langues sélectionnées (peut être un tableau vide)
|
||||
informations_complementaires: informationsComplementaires
|
||||
};
|
||||
|
||||
// Envoyer à l'API
|
||||
const result = await apiFetch('intervenant/permanences', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
toastr.success(
|
||||
`${result.events_created || 'Les'} permanence(s) ont été créées avec succès`,
|
||||
'Succès',
|
||||
{ timeOut: 5000 }
|
||||
);
|
||||
|
||||
// Réinitialiser le formulaire
|
||||
event.target.reset();
|
||||
clearPreview();
|
||||
|
||||
// Recalculer l'aperçu avec les valeurs par défaut
|
||||
setTimeout(updatePreview, 100);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la création des permanences:', error);
|
||||
toastr.error(
|
||||
error.message || 'Erreur lors de la création des permanences',
|
||||
'Erreur',
|
||||
{ timeOut: 5000 }
|
||||
);
|
||||
} finally {
|
||||
// Réactiver le formulaire
|
||||
const submitBtn = event.target.querySelector('button[type="submit"]');
|
||||
if (submitBtn) {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = '<i class="fas fa-save me-2"></i>Créer les permanences';
|
||||
}
|
||||
if (window.CRVI_OVERLAY && formElement) { window.CRVI_OVERLAY.hide(formElement); }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Estime le nombre total d'événements qui seront créés
|
||||
*/
|
||||
function estimateTotalEvents(startDate, endDate, jours, tranches) {
|
||||
const joursMapping = {
|
||||
'lundi': 1,
|
||||
'mardi': 2,
|
||||
'mercredi': 3,
|
||||
'jeudi': 4,
|
||||
'vendredi': 5,
|
||||
'samedi': 6,
|
||||
'dimanche': 0
|
||||
};
|
||||
|
||||
let total = 0;
|
||||
const currentDate = new Date(startDate);
|
||||
|
||||
while (currentDate <= endDate) {
|
||||
const dayOfWeek = currentDate.getDay();
|
||||
const jourName = Object.keys(joursMapping).find(k => joursMapping[k] === dayOfWeek);
|
||||
|
||||
if (jours.includes(jourName)) {
|
||||
total += tranches.length;
|
||||
}
|
||||
|
||||
currentDate.setDate(currentDate.getDate() + 1);
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise le formulaire d'import CSV
|
||||
*/
|
||||
function initializeCsvImport() {
|
||||
const csvForm = document.getElementById('import-csv-form');
|
||||
if (!csvForm) {
|
||||
console.warn('Formulaire import-csv-form non trouvé');
|
||||
return;
|
||||
}
|
||||
|
||||
csvForm.addEventListener('submit', handleCsvImportSubmit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gère la soumission du formulaire d'import CSV
|
||||
*/
|
||||
async function handleCsvImportSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const form = e.target;
|
||||
const fileInput = document.getElementById('csv-file');
|
||||
const submitBtn = document.getElementById('submit-csv-import-btn');
|
||||
const resultContainer = document.getElementById('csv-import-result');
|
||||
const csvForm = document.getElementById('import-csv-form');
|
||||
|
||||
if (!fileInput || !fileInput.files || fileInput.files.length === 0) {
|
||||
toastr.error('Veuillez sélectionner un fichier CSV', 'Erreur');
|
||||
return;
|
||||
}
|
||||
|
||||
const file = fileInput.files[0];
|
||||
|
||||
// Vérifier que c'est un fichier CSV
|
||||
if (!file.name.toLowerCase().endsWith('.csv')) {
|
||||
toastr.error('Le fichier doit être au format CSV', 'Erreur');
|
||||
return;
|
||||
}
|
||||
|
||||
// Désactiver le bouton
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Import en cours...';
|
||||
|
||||
// Créer FormData pour l'upload
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
try {
|
||||
// Appel API pour l'import CSV
|
||||
const url = '/wp-json/crvi/v1/intervenant/permanences/import-csv';
|
||||
if (window.CRVI_OVERLAY && csvForm) { window.CRVI_OVERLAY.show(csvForm); }
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-WP-Nonce': window.wpApiSettings?.nonce || ''
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok || !data.success) {
|
||||
const errorMessage = data.error?.message || data.message || 'Erreur lors de l\'import';
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
// Afficher les résultats
|
||||
displayCsvImportResult(data.data, resultContainer);
|
||||
|
||||
toastr.success(
|
||||
`Import réussi : ${data.data.created || 0} créés, ${data.data.errors?.length || 0} erreurs`,
|
||||
'Succès',
|
||||
{ timeOut: 5000 }
|
||||
);
|
||||
|
||||
// Réinitialiser le formulaire
|
||||
form.reset();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de l\'import CSV:', error);
|
||||
toastr.error(error.message || 'Une erreur est survenue lors de l\'import CSV', 'Erreur');
|
||||
|
||||
if (resultContainer) {
|
||||
resultContainer.innerHTML = `
|
||||
<div class="alert alert-danger">
|
||||
<i class="fas fa-exclamation-circle me-2"></i>
|
||||
<strong>Erreur :</strong> ${error.message || 'Une erreur est survenue lors de l\'import'}
|
||||
</div>
|
||||
`;
|
||||
resultContainer.style.display = 'block';
|
||||
}
|
||||
} finally {
|
||||
// Réactiver le bouton
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = '<i class="fas fa-upload me-2"></i>Importer le CSV';
|
||||
if (window.CRVI_OVERLAY && csvForm) { window.CRVI_OVERLAY.hide(csvForm); }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Affiche les résultats de l'import CSV
|
||||
*/
|
||||
function displayCsvImportResult(result, container) {
|
||||
if (!container) return;
|
||||
|
||||
let html = '';
|
||||
|
||||
if (result.errors && result.errors.length > 0) {
|
||||
html += `
|
||||
<div class="alert alert-warning">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
<strong>Attention :</strong> ${result.errors.length} erreur(s) détectée(s)
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (result.errors.length <= 10) {
|
||||
html += '<ul class="list-group mt-2">';
|
||||
result.errors.forEach((error, index) => {
|
||||
html += `
|
||||
<li class="list-group-item">
|
||||
<strong>Ligne ${error.line || '?'} :</strong> ${error.message || 'Erreur inconnue'}
|
||||
</li>
|
||||
`;
|
||||
});
|
||||
html += '</ul>';
|
||||
}
|
||||
}
|
||||
|
||||
if (result.created > 0) {
|
||||
html += `
|
||||
<div class="alert alert-success mt-3">
|
||||
<i class="fas fa-check-circle me-2"></i>
|
||||
<strong>Succès :</strong> ${result.created} permanence(s) créée(s) avec succès
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
container.innerHTML = html;
|
||||
container.style.display = 'block';
|
||||
}
|
||||
|
||||
608
assets/js/modules/agenda-intervenant-profile.js
Normal file
608
assets/js/modules/agenda-intervenant-profile.js
Normal file
@ -0,0 +1,608 @@
|
||||
/**
|
||||
* Module Profil Intervenant
|
||||
* Gestion du formulaire de profil et mise à jour des informations
|
||||
*/
|
||||
|
||||
import { apiFetch } from './agenda-api.js';
|
||||
import toastr from 'toastr';
|
||||
|
||||
// Variables globales pour gérer les indisponibilités
|
||||
let indisponibilitesList = [];
|
||||
let indisponibiliteCounter = 0;
|
||||
|
||||
/**
|
||||
* Initialise le module profil intervenant
|
||||
*/
|
||||
export function initializeProfile() {
|
||||
console.log('🚀 Initialisation du module profil intervenant...');
|
||||
|
||||
// Charger les données du profil
|
||||
loadProfile();
|
||||
|
||||
// Écouter les événements du formulaire principal
|
||||
const profileForm = document.getElementById('profile-form');
|
||||
if (profileForm) {
|
||||
profileForm.addEventListener('submit', handleProfileSubmit);
|
||||
}
|
||||
|
||||
const cancelBtn = document.getElementById('cancel-profile-btn');
|
||||
if (cancelBtn) {
|
||||
cancelBtn.addEventListener('click', () => {
|
||||
loadProfile(); // Recharger pour annuler les modifications
|
||||
});
|
||||
}
|
||||
|
||||
// Écouter les événements du formulaire de disponibilités
|
||||
const disponibilitesForm = document.getElementById('disponibilites-form');
|
||||
if (disponibilitesForm) {
|
||||
disponibilitesForm.addEventListener('submit', handleDisponibilitesSubmit);
|
||||
}
|
||||
|
||||
// Gestion de l'ajout d'indisponibilité
|
||||
const addIndispoBtn = document.getElementById('add-indisponibilite-btn');
|
||||
if (addIndispoBtn) {
|
||||
addIndispoBtn.addEventListener('click', showIndisponibiliteForm);
|
||||
}
|
||||
|
||||
const saveIndispoBtn = document.getElementById('save-indisponibilite-btn');
|
||||
if (saveIndispoBtn) {
|
||||
saveIndispoBtn.addEventListener('click', saveIndisponibilite);
|
||||
}
|
||||
|
||||
const cancelIndispoBtn = document.getElementById('cancel-indisponibilite-btn');
|
||||
if (cancelIndispoBtn) {
|
||||
cancelIndispoBtn.addEventListener('click', hideIndisponibiliteForm);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Charge les données du profil depuis l'API
|
||||
*/
|
||||
async function loadProfile() {
|
||||
try {
|
||||
const profileContainer = document.getElementById('intervenant-profile-container') || document.getElementById('profile-form') || document.body;
|
||||
if (window.CRVI_OVERLAY && profileContainer) { window.CRVI_OVERLAY.show(profileContainer); }
|
||||
const profile = await apiFetch('intervenant/profile');
|
||||
|
||||
// Remplir les champs non modifiables
|
||||
const nomEl = document.getElementById('profile-nom');
|
||||
const prenomEl = document.getElementById('profile-prenom');
|
||||
const emailEl = document.getElementById('profile-email');
|
||||
|
||||
if (nomEl) nomEl.value = profile.nom || '';
|
||||
if (prenomEl) prenomEl.value = profile.prenom || '';
|
||||
if (emailEl) emailEl.value = profile.email || '';
|
||||
|
||||
// Téléphone (modifiable)
|
||||
const telephoneEl = document.getElementById('profile-telephone');
|
||||
if (telephoneEl) {
|
||||
telephoneEl.value = profile.telephone || '';
|
||||
}
|
||||
|
||||
// Jours de disponibilité (checkboxes) - déjà générés en PHP, on met juste à jour les valeurs
|
||||
updateJoursDisponibiliteCheckboxes(profile.jours_de_disponibilite || []);
|
||||
|
||||
// Heures de permanences (checkboxes)
|
||||
updateHeuresPermanencesCheckboxes(profile.heures_de_permanences || []);
|
||||
|
||||
// Indisponibilités ponctuelles - déjà générées en PHP, on synchronise juste la liste
|
||||
indisponibilitesList = profile.indisponibilites_ponctuelles || [];
|
||||
// Synchroniser la liste avec celle affichée en PHP (pour les mises à jour après sauvegarde)
|
||||
syncIndisponibilitesList(indisponibilitesList);
|
||||
// Rafraîchir explicitement l'affichage pour éviter tout écart visuel
|
||||
displayIndisponibilites(indisponibilitesList);
|
||||
|
||||
// Départements (format: [{id, nom}, ...])
|
||||
displayDepartements(profile.departements || []);
|
||||
|
||||
// Spécialisations (format: [{id, nom}, ...])
|
||||
displaySpecialisations(profile.specialisations || []);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement du profil:', error);
|
||||
toastr.error(
|
||||
error.message || 'Erreur lors du chargement du profil',
|
||||
'Erreur',
|
||||
{ timeOut: 5000 }
|
||||
);
|
||||
} finally {
|
||||
const profileContainer = document.getElementById('intervenant-profile-container') || document.getElementById('profile-form') || document.body;
|
||||
if (window.CRVI_OVERLAY && profileContainer) { window.CRVI_OVERLAY.hide(profileContainer); }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour les checkboxes des jours de disponibilité (déjà générées en PHP)
|
||||
*/
|
||||
function updateJoursDisponibiliteCheckboxes(joursActuels) {
|
||||
const joursValues = ['lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi', 'dimanche'];
|
||||
|
||||
joursValues.forEach(jour => {
|
||||
const checkbox = document.getElementById(`jour-${jour}`);
|
||||
if (checkbox) {
|
||||
checkbox.checked = joursActuels.includes(jour);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour les checkboxes des heures de permanences
|
||||
*/
|
||||
function updateHeuresPermanencesCheckboxes(heuresActuelles) {
|
||||
const heuresValues = ['09:00','10:00','11:00','12:00','13:00','14:00','15:00','16:00'];
|
||||
heuresValues.forEach(h => {
|
||||
const checkbox = document.getElementById(`heure-${h}`);
|
||||
if (checkbox) {
|
||||
checkbox.checked = Array.isArray(heuresActuelles) && heuresActuelles.includes(h);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronise la liste JS des indisponibilités avec les données de l'API
|
||||
* (les indisponibilités sont déjà affichées en PHP, on synchronise juste la liste interne)
|
||||
*/
|
||||
function syncIndisponibilitesList(indisponibilites) {
|
||||
// La liste est déjà affichée en PHP, on synchronise juste la liste JS
|
||||
// pour les opérations d'ajout/suppression
|
||||
indisponibilitesList = indisponibilites || [];
|
||||
|
||||
// Mettre à jour l'affichage si des indisponibilités ont été ajoutées/supprimées
|
||||
const container = document.getElementById('profile-indisponibilites-list');
|
||||
const emptyContainer = document.getElementById('profile-indisponibilites-empty');
|
||||
|
||||
if (!container) return;
|
||||
|
||||
// Si la liste est vide, afficher le message
|
||||
if (indisponibilitesList.length === 0) {
|
||||
const existingItems = container.querySelectorAll('.indisponibilite-item');
|
||||
existingItems.forEach(item => item.remove());
|
||||
if (emptyContainer) {
|
||||
emptyContainer.style.display = 'block';
|
||||
}
|
||||
} else {
|
||||
if (emptyContainer) {
|
||||
emptyContainer.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Affiche les indisponibilités ponctuelles (utilisée après ajout/suppression)
|
||||
*/
|
||||
function displayIndisponibilites(indisponibilites) {
|
||||
const container = document.getElementById('profile-indisponibilites-list');
|
||||
const emptyContainer = document.getElementById('profile-indisponibilites-empty');
|
||||
|
||||
if (!container) return;
|
||||
|
||||
if (!indisponibilites || indisponibilites.length === 0) {
|
||||
container.innerHTML = '';
|
||||
if (emptyContainer) {
|
||||
emptyContainer.style.display = 'block';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (emptyContainer) {
|
||||
emptyContainer.style.display = 'none';
|
||||
}
|
||||
|
||||
// Types d'indisponibilité
|
||||
const typesLabels = {
|
||||
'conge': 'Congé',
|
||||
'absence': 'Absence',
|
||||
'maladie': 'Maladie'
|
||||
};
|
||||
|
||||
container.innerHTML = indisponibilites.map((indisp, index) => {
|
||||
const debut = indisp.debut || '';
|
||||
const fin = indisp.fin || '';
|
||||
const type = indisp.type || 'conge';
|
||||
const typeLabel = typesLabels[type] || type;
|
||||
const commentaire = indisp.commentaire || '';
|
||||
|
||||
// Badge classes selon type
|
||||
let badgeClass = 'badge-indispo-unknown';
|
||||
if (type === 'conge') badgeClass = 'badge-indispo-conge';
|
||||
else if (type === 'absence') badgeClass = 'badge-indispo-absence';
|
||||
else if (type === 'maladie') badgeClass = 'badge-indispo-maladie';
|
||||
|
||||
// Convertir les dates au format d/m/Y pour l'affichage
|
||||
let debutFormatted = debut;
|
||||
let finFormatted = fin;
|
||||
|
||||
// Si les dates sont au format YYYY-MM-DD, les convertir
|
||||
if (debut && debut.match(/^\d{4}-\d{2}-\d{2}$/)) {
|
||||
const dateObj = new Date(debut);
|
||||
debutFormatted = dateObj.toLocaleDateString('fr-FR');
|
||||
}
|
||||
if (fin && fin.match(/^\d{4}-\d{2}-\d{2}$/)) {
|
||||
const dateObj = new Date(fin);
|
||||
finFormatted = dateObj.toLocaleDateString('fr-FR');
|
||||
}
|
||||
|
||||
// Afficher la période
|
||||
let periodeText = '';
|
||||
if (debutFormatted && finFormatted) {
|
||||
if (debutFormatted === finFormatted) {
|
||||
periodeText = `Le ${debutFormatted}`;
|
||||
} else {
|
||||
periodeText = `Du ${debutFormatted} au ${finFormatted}`;
|
||||
}
|
||||
} else if (debutFormatted) {
|
||||
periodeText = `Le ${debutFormatted}`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="indisponibilite-item indispo-${type}" data-index="${index}">
|
||||
<div>
|
||||
<strong>${periodeText}</strong>
|
||||
<span class="badge ${badgeClass} ms-2">${typeLabel}</span>
|
||||
${commentaire ? `<p class="mb-0 text-muted mt-1">${commentaire}</p>` : ''}
|
||||
</div>
|
||||
<button type="button" class="btn-remove" onclick="removeIndisponibilite(${index})" title="Supprimer">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Fonction globale pour supprimer une indisponibilité
|
||||
window.removeIndisponibilite = function(index) {
|
||||
if (confirm('Êtes-vous sûr de vouloir supprimer cette indisponibilité ?')) {
|
||||
indisponibilitesList.splice(index, 1);
|
||||
displayIndisponibilites(indisponibilitesList);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Affiche le formulaire d'ajout d'indisponibilité
|
||||
*/
|
||||
function showIndisponibiliteForm() {
|
||||
const container = document.getElementById('indisponibilite-form-container');
|
||||
if (container) {
|
||||
container.style.display = 'block';
|
||||
|
||||
// Ajouter les attributs required quand le formulaire est visible
|
||||
const debutInput = document.getElementById('indispo-debut');
|
||||
const finInput = document.getElementById('indispo-fin');
|
||||
const typeSelect = document.getElementById('indispo-type');
|
||||
|
||||
if (debutInput) debutInput.setAttribute('required', 'required');
|
||||
if (finInput) finInput.setAttribute('required', 'required');
|
||||
if (typeSelect) typeSelect.setAttribute('required', 'required');
|
||||
|
||||
// Réinitialiser les champs
|
||||
if (debutInput) debutInput.value = '';
|
||||
if (finInput) finInput.value = '';
|
||||
if (typeSelect) typeSelect.value = '';
|
||||
const commentaireTextarea = document.getElementById('indispo-commentaire');
|
||||
if (commentaireTextarea) commentaireTextarea.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache le formulaire d'ajout d'indisponibilité
|
||||
*/
|
||||
function hideIndisponibiliteForm() {
|
||||
const container = document.getElementById('indisponibilite-form-container');
|
||||
if (container) {
|
||||
container.style.display = 'none';
|
||||
|
||||
// Retirer les attributs required quand le formulaire est caché
|
||||
const debutInput = document.getElementById('indispo-debut');
|
||||
const finInput = document.getElementById('indispo-fin');
|
||||
const typeSelect = document.getElementById('indispo-type');
|
||||
|
||||
if (debutInput) debutInput.removeAttribute('required');
|
||||
if (finInput) finInput.removeAttribute('required');
|
||||
if (typeSelect) typeSelect.removeAttribute('required');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sauvegarde une nouvelle indisponibilité dans la liste
|
||||
*/
|
||||
function saveIndisponibilite() {
|
||||
const debut = document.getElementById('indispo-debut').value;
|
||||
const fin = document.getElementById('indispo-fin').value;
|
||||
const type = document.getElementById('indispo-type').value;
|
||||
const commentaire = document.getElementById('indispo-commentaire').value.trim();
|
||||
|
||||
// Validation
|
||||
if (!debut || !fin || !type) {
|
||||
toastr.warning('Veuillez remplir tous les champs obligatoires', 'Validation');
|
||||
return;
|
||||
}
|
||||
|
||||
// Vérifier que la date de fin est après la date de début
|
||||
if (new Date(fin) < new Date(debut)) {
|
||||
toastr.warning('La date de fin doit être supérieure ou égale à la date de début', 'Validation');
|
||||
return;
|
||||
}
|
||||
|
||||
// Convertir les dates au format d/m/Y pour ACF
|
||||
const debutFormatted = formatDateToDMY(debut);
|
||||
const finFormatted = formatDateToDMY(fin);
|
||||
|
||||
// Ajouter à la liste
|
||||
indisponibilitesList.push({
|
||||
debut: debutFormatted,
|
||||
fin: finFormatted,
|
||||
type: type,
|
||||
commentaire: commentaire
|
||||
});
|
||||
|
||||
// Rafraîchir l'affichage
|
||||
displayIndisponibilites(indisponibilitesList);
|
||||
|
||||
// Cacher le formulaire
|
||||
hideIndisponibiliteForm();
|
||||
|
||||
toastr.success('Indisponibilité ajoutée. N\'oubliez pas d\'enregistrer.', 'Succès', { timeOut: 3000 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit une date au format YYYY-MM-DD vers d/m/Y
|
||||
*/
|
||||
function formatDateToDMY(dateStr) {
|
||||
if (!dateStr) return '';
|
||||
const date = new Date(dateStr);
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const year = date.getFullYear();
|
||||
return `${day}/${month}/${year}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convertit une date au format d/m/Y vers YYYY-MM-DD
|
||||
*/
|
||||
function formatDateToYMD(dateStr) {
|
||||
if (!dateStr) return '';
|
||||
// Format d/m/Y vers YYYY-MM-DD
|
||||
const parts = dateStr.split('/');
|
||||
if (parts.length === 3) {
|
||||
return `${parts[2]}-${parts[1]}-${parts[0]}`;
|
||||
}
|
||||
return dateStr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Affiche les départements
|
||||
*/
|
||||
function displayDepartements(departements) {
|
||||
const container = document.getElementById('profile-departements');
|
||||
if (!container) return;
|
||||
|
||||
if (!departements || departements.length === 0) {
|
||||
container.innerHTML = '<span class="text-muted">Aucun département assigné</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Les départements viennent comme objets {id, nom}
|
||||
container.innerHTML = departements.map(dep => {
|
||||
const nom = dep.nom || dep.post_title || `Département #${dep.id || dep.ID || dep}`;
|
||||
return `<span class="badge bg-secondary me-2 mb-2">${nom}</span>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Affiche les spécialisations (types d'intervention)
|
||||
*/
|
||||
function displaySpecialisations(specialisations) {
|
||||
const container = document.getElementById('profile-specialisations');
|
||||
if (!container) return;
|
||||
|
||||
if (!specialisations || specialisations.length === 0) {
|
||||
container.innerHTML = '<span class="text-muted">Aucune spécialisation assignée</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Les spécialisations viennent comme objets {id, nom}
|
||||
container.innerHTML = specialisations.map(spec => {
|
||||
const nom = spec.nom || spec.post_title || `Spécialisation #${spec.id || spec.ID || spec}`;
|
||||
return `<span class="badge bg-primary me-2 mb-2">${nom}</span>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gère la soumission du formulaire de profil
|
||||
*/
|
||||
async function handleProfileSubmit(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const telephoneEl = document.getElementById('profile-telephone');
|
||||
if (!telephoneEl) {
|
||||
toastr.error('Champ téléphone non trouvé', 'Erreur');
|
||||
return;
|
||||
}
|
||||
|
||||
const telephone = telephoneEl.value.trim();
|
||||
|
||||
// Validation basique
|
||||
if (!telephone) {
|
||||
toastr.warning('Le numéro de téléphone est requis', 'Validation');
|
||||
telephoneEl.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// Format de validation simple (à adapter selon les besoins)
|
||||
const phoneRegex = /^[0-9+\s\-()]{8,}$/;
|
||||
if (!phoneRegex.test(telephone)) {
|
||||
toastr.warning('Format de numéro de téléphone invalide', 'Validation');
|
||||
telephoneEl.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Désactiver le bouton de soumission
|
||||
const submitBtn = event.target.querySelector('button[type="submit"]');
|
||||
if (submitBtn) {
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Enregistrement...';
|
||||
}
|
||||
const profileContainer = document.getElementById('intervenant-profile-container') || document.getElementById('profile-form') || document.body;
|
||||
if (window.CRVI_OVERLAY && profileContainer) { window.CRVI_OVERLAY.show(profileContainer); }
|
||||
|
||||
await apiFetch('intervenant/profile', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
telephone: telephone
|
||||
})
|
||||
});
|
||||
|
||||
toastr.success(
|
||||
'Profil mis à jour avec succès',
|
||||
'Succès',
|
||||
{ timeOut: 3000 }
|
||||
);
|
||||
|
||||
// Recharger le profil pour avoir les données à jour
|
||||
await loadProfile();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la mise à jour du profil:', error);
|
||||
toastr.error(
|
||||
error.message || 'Erreur lors de la mise à jour du profil',
|
||||
'Erreur',
|
||||
{ timeOut: 5000 }
|
||||
);
|
||||
} finally {
|
||||
// Réactiver le bouton
|
||||
const submitBtn = event.target.querySelector('button[type="submit"]');
|
||||
if (submitBtn) {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = '<i class="fas fa-save me-2"></i>Enregistrer les modifications';
|
||||
}
|
||||
const profileContainer = document.getElementById('intervenant-profile-container') || document.getElementById('profile-form') || document.body;
|
||||
if (window.CRVI_OVERLAY && profileContainer) { window.CRVI_OVERLAY.hide(profileContainer); }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gère la soumission du formulaire de disponibilités
|
||||
*/
|
||||
async function handleDisponibilitesSubmit(event) {
|
||||
event.preventDefault();
|
||||
|
||||
try {
|
||||
// Récupérer les jours de disponibilité cochés
|
||||
const joursCheckboxes = document.querySelectorAll('input[name="jours_disponibilite[]"]:checked');
|
||||
const joursDisponibilite = Array.from(joursCheckboxes).map(cb => cb.value);
|
||||
|
||||
// Récupérer les heures de permanences cochées
|
||||
const heuresCheckboxes = document.querySelectorAll('input[name="heures_permanences[]"]:checked');
|
||||
const heuresPermanences = Array.from(heuresCheckboxes).map(cb => cb.value);
|
||||
|
||||
// Désactiver le bouton de soumission
|
||||
const submitBtn = event.target.querySelector('button[type="submit"]');
|
||||
if (submitBtn) {
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Enregistrement...';
|
||||
}
|
||||
const profileContainer = document.getElementById('intervenant-profile-container') || document.getElementById('disponibilites-form') || document.body;
|
||||
if (window.CRVI_OVERLAY && profileContainer) { window.CRVI_OVERLAY.show(profileContainer); }
|
||||
|
||||
await apiFetch('intervenant/profile', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
jours_de_disponibilite: joursDisponibilite,
|
||||
heures_de_permanences: heuresPermanences,
|
||||
indisponibilites_ponctuelles: indisponibilitesList
|
||||
})
|
||||
});
|
||||
|
||||
toastr.success(
|
||||
'Disponibilités mises à jour avec succès',
|
||||
'Succès',
|
||||
{ timeOut: 3000 }
|
||||
);
|
||||
|
||||
// Vérifier s'il y a des conflits d'indisponibilités
|
||||
await checkAndDisplayConflicts();
|
||||
|
||||
// Recharger le profil pour avoir les données à jour
|
||||
await loadProfile();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la mise à jour des disponibilités:', error);
|
||||
toastr.error(
|
||||
error.message || 'Erreur lors de la mise à jour des disponibilités',
|
||||
'Erreur',
|
||||
{ timeOut: 5000 }
|
||||
);
|
||||
} finally {
|
||||
// Réactiver le bouton
|
||||
const submitBtn = event.target.querySelector('button[type="submit"]');
|
||||
if (submitBtn) {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = '<i class="fas fa-save me-2"></i>Enregistrer les disponibilités';
|
||||
}
|
||||
const profileContainer = document.getElementById('intervenant-profile-container') || document.getElementById('disponibilites-form') || document.body;
|
||||
if (window.CRVI_OVERLAY && profileContainer) { window.CRVI_OVERLAY.hide(profileContainer); }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie et affiche les conflits d'indisponibilités après la sauvegarde
|
||||
*/
|
||||
async function checkAndDisplayConflicts() {
|
||||
try {
|
||||
// Faire une requête à l'API WordPress pour récupérer le message de conflit
|
||||
const response = await apiFetch('intervenant/conflicts-check');
|
||||
|
||||
if (response && response.has_conflicts && response.message) {
|
||||
// Afficher le message de conflit en haut de la page
|
||||
displayConflictMessage(response.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la vérification des conflits:', error);
|
||||
// Ne pas afficher d'erreur à l'utilisateur pour ne pas perturber l'expérience
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Affiche un message de conflit en haut de la page
|
||||
* @param {string} message - Le message HTML à afficher
|
||||
*/
|
||||
function displayConflictMessage(message) {
|
||||
// Trouver le conteneur où afficher le message (après le header, avant le contenu)
|
||||
const container = document.querySelector('.crvi-intervenant-profile .container-fluid');
|
||||
if (!container) return;
|
||||
|
||||
// Supprimer les anciens messages de conflit s'il y en a
|
||||
const oldAlert = container.querySelector('.conflict-alert');
|
||||
if (oldAlert) {
|
||||
oldAlert.remove();
|
||||
}
|
||||
|
||||
// Créer un nouvel élément d'alerte
|
||||
const alertDiv = document.createElement('div');
|
||||
alertDiv.className = 'conflict-alert alert alert-danger alert-dismissible fade show mt-3';
|
||||
alertDiv.setAttribute('role', 'alert');
|
||||
alertDiv.innerHTML = message;
|
||||
|
||||
// Ajouter un bouton de fermeture
|
||||
const closeButton = document.createElement('button');
|
||||
closeButton.type = 'button';
|
||||
closeButton.className = 'btn-close';
|
||||
closeButton.setAttribute('data-bs-dismiss', 'alert');
|
||||
closeButton.setAttribute('aria-label', 'Close');
|
||||
alertDiv.appendChild(closeButton);
|
||||
|
||||
// Insérer le message après la navigation (row mb-4)
|
||||
const navRow = container.querySelector('.row.mb-4');
|
||||
if (navRow && navRow.nextSibling) {
|
||||
container.insertBefore(alertDiv, navRow.nextSibling);
|
||||
} else {
|
||||
container.insertBefore(alertDiv, container.firstChild);
|
||||
}
|
||||
|
||||
// Scroller vers le haut pour voir le message
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
4426
assets/js/modules/agenda-modal.js
Normal file
4426
assets/js/modules/agenda-modal.js
Normal file
File diff suppressed because it is too large
Load Diff
25
assets/js/modules/agenda-notifications.js
Normal file
25
assets/js/modules/agenda-notifications.js
Normal file
@ -0,0 +1,25 @@
|
||||
// Module ES6 pour la gestion des notifications Toastr
|
||||
export function notifySuccess(message) {
|
||||
toastr.success(message);
|
||||
}
|
||||
|
||||
export function notifyError(message) {
|
||||
toastr.error(message);
|
||||
}
|
||||
|
||||
export function notifyInfo(message) {
|
||||
toastr.info(message);
|
||||
}
|
||||
|
||||
export function notifyWarning(message) {
|
||||
toastr.warning(message);
|
||||
}
|
||||
|
||||
export function configureToastr() {
|
||||
toastr.options = {
|
||||
"closeButton": true,
|
||||
"progressBar": true,
|
||||
"positionClass": "toast-top-right",
|
||||
"timeOut": "5000"
|
||||
};
|
||||
}
|
||||
395
assets/js/modules/agenda-stats-table.js
Normal file
395
assets/js/modules/agenda-stats-table.js
Normal file
@ -0,0 +1,395 @@
|
||||
// Module ES6 pour le tableau de statistiques Agenda
|
||||
import { apiFetch } from './agenda-api.js';
|
||||
|
||||
let currentPage = 1;
|
||||
let currentFilters = {};
|
||||
|
||||
/**
|
||||
* Initialise le module de tableau de stats
|
||||
*/
|
||||
export function initStatsTable() {
|
||||
// Initialiser Select2 sur les filtres
|
||||
if (typeof jQuery !== 'undefined' && jQuery.fn.select2) {
|
||||
jQuery('.stats-filters .select2').select2({
|
||||
width: '100%'
|
||||
});
|
||||
}
|
||||
|
||||
// Écouter le bouton de filtre
|
||||
const filterBtn = document.getElementById('stats_filterBtn');
|
||||
if (filterBtn) {
|
||||
filterBtn.addEventListener('click', handleFilter);
|
||||
}
|
||||
|
||||
// Écouter le bouton de réinitialisation
|
||||
const resetBtn = document.getElementById('stats_resetFiltersBtn');
|
||||
if (resetBtn) {
|
||||
resetBtn.addEventListener('click', handleReset);
|
||||
}
|
||||
|
||||
// Charger les événements au démarrage
|
||||
loadEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* Collecte les filtres du formulaire
|
||||
*/
|
||||
function collectFilters() {
|
||||
const filters = {};
|
||||
|
||||
// Date (convertir en date pour recherche exacte sur une journée)
|
||||
const dateInput = document.getElementById('stats_date');
|
||||
if (dateInput && dateInput.value) {
|
||||
filters.date = dateInput.value;
|
||||
}
|
||||
|
||||
// Local
|
||||
const localSelect = document.getElementById('stats_local');
|
||||
if (localSelect && localSelect.value) {
|
||||
filters.local = localSelect.value;
|
||||
}
|
||||
|
||||
// Personne (intervenant)
|
||||
const personneSelect = document.getElementById('stats_personne');
|
||||
if (personneSelect && personneSelect.value) {
|
||||
filters.intervenant = personneSelect.value;
|
||||
}
|
||||
|
||||
// Type d'intervention
|
||||
const typeInterventionSelect = document.getElementById('stats_type_intervention');
|
||||
if (typeInterventionSelect && typeInterventionSelect.value) {
|
||||
filters.type_intervention = typeInterventionSelect.value;
|
||||
}
|
||||
|
||||
// Bénéficiaire
|
||||
const beneficiaireSelect = document.getElementById('stats_beneficiaire');
|
||||
if (beneficiaireSelect && beneficiaireSelect.value) {
|
||||
filters.beneficiaire = beneficiaireSelect.value;
|
||||
}
|
||||
|
||||
// Langue
|
||||
const langueSelect = document.getElementById('stats_langue');
|
||||
if (langueSelect && langueSelect.value) {
|
||||
filters.langue = langueSelect.value;
|
||||
}
|
||||
|
||||
// Intervenant externe (traducteur)
|
||||
const intervenantExterneSelect = document.getElementById('stats_intervenant_externe');
|
||||
if (intervenantExterneSelect && intervenantExterneSelect.value) {
|
||||
filters.traducteur = intervenantExterneSelect.value;
|
||||
}
|
||||
|
||||
// Année
|
||||
const anneeInput = document.getElementById('stats_annee');
|
||||
if (anneeInput && anneeInput.value) {
|
||||
filters.annee = parseInt(anneeInput.value, 10);
|
||||
}
|
||||
|
||||
// Filtre permanence
|
||||
const permanenceCheckbox = document.getElementById('stats_filtre_permanence');
|
||||
if (permanenceCheckbox && permanenceCheckbox.checked) {
|
||||
filters.type = 'permanence';
|
||||
}
|
||||
|
||||
return filters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gère le clic sur le bouton Filtrer
|
||||
*/
|
||||
function handleFilter() {
|
||||
currentPage = 1;
|
||||
loadEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gère le clic sur le bouton Réinitialiser
|
||||
*/
|
||||
function handleReset() {
|
||||
// Réinitialiser tous les champs
|
||||
const form = document.querySelector('.stats-filters');
|
||||
if (form) {
|
||||
form.reset();
|
||||
// Réinitialiser Select2
|
||||
if (typeof jQuery !== 'undefined' && jQuery.fn.select2) {
|
||||
jQuery('.stats-filters .select2').val(null).trigger('change');
|
||||
}
|
||||
}
|
||||
currentPage = 1;
|
||||
currentFilters = {};
|
||||
loadEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* Charge les événements depuis l'API
|
||||
*/
|
||||
async function loadEvents() {
|
||||
const loadingIndicator = document.getElementById('stats-loading-indicator');
|
||||
const table = document.getElementById('stats-events-table');
|
||||
const tableBody = document.getElementById('stats-events-table-body');
|
||||
const paginationContainer = document.getElementById('stats-pagination-container');
|
||||
const tableWrapper = document.getElementById('stats-table-wrapper');
|
||||
|
||||
// Afficher le loading et masquer le tableau
|
||||
if (loadingIndicator) {
|
||||
loadingIndicator.style.display = 'flex';
|
||||
}
|
||||
if (tableWrapper) {
|
||||
tableWrapper.style.display = 'none';
|
||||
}
|
||||
if (table) {
|
||||
table.style.display = 'none';
|
||||
}
|
||||
if (paginationContainer) {
|
||||
paginationContainer.style.display = 'none';
|
||||
}
|
||||
|
||||
try {
|
||||
// Collecter les filtres
|
||||
currentFilters = collectFilters();
|
||||
|
||||
// Ajouter la pagination
|
||||
const params = {
|
||||
...currentFilters,
|
||||
page: currentPage,
|
||||
per_page: 20
|
||||
};
|
||||
|
||||
// Appel API
|
||||
const result = await apiFetch(`events/table?${new URLSearchParams(params).toString()}`);
|
||||
|
||||
// Afficher les résultats
|
||||
displayEvents(result.events || []);
|
||||
updateCounters(result.total || 0, result.filtered || 0);
|
||||
displayPagination(result.page || 1, result.total_pages || 0);
|
||||
|
||||
// IMPORTANT: Masquer le loader EN PREMIER, puis afficher le tableau
|
||||
if (loadingIndicator) {
|
||||
loadingIndicator.style.display = 'none';
|
||||
}
|
||||
|
||||
// Afficher le conteneur du tableau
|
||||
if (tableWrapper) {
|
||||
tableWrapper.style.display = 'block';
|
||||
}
|
||||
|
||||
// Afficher le tableau
|
||||
if (table) {
|
||||
table.style.display = 'table';
|
||||
}
|
||||
|
||||
// Afficher la pagination si nécessaire
|
||||
if (paginationContainer && result.total_pages > 0) {
|
||||
paginationContainer.style.display = 'block';
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement des événements:', error);
|
||||
|
||||
// En cas d'erreur, masquer le loader et afficher le tableau avec le message d'erreur
|
||||
if (loadingIndicator) {
|
||||
loadingIndicator.style.display = 'none';
|
||||
}
|
||||
|
||||
if (tableWrapper) {
|
||||
tableWrapper.style.display = 'block';
|
||||
}
|
||||
|
||||
if (table) {
|
||||
table.style.display = 'table';
|
||||
}
|
||||
|
||||
if (tableBody) {
|
||||
tableBody.innerHTML = `<tr><td colspan="8" style="text-align: center; color: red; padding: 20px;">Erreur lors du chargement des données</td></tr>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Affiche les événements dans le tableau
|
||||
*/
|
||||
function displayEvents(events) {
|
||||
const tableBody = document.getElementById('stats-events-table-body');
|
||||
if (!tableBody) return;
|
||||
|
||||
if (events.length === 0) {
|
||||
tableBody.innerHTML = '<tr><td colspan="8" style="text-align: center;">Aucun événement trouvé</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
events.forEach(event => {
|
||||
const dateHeure = event.date_rdv && event.heure_rdv
|
||||
? `${formatDate(event.date_rdv)} ${event.heure_rdv}`
|
||||
: '-';
|
||||
|
||||
const statutLabel = formatStatutLabel(event.statut);
|
||||
const statutClass = getStatutBadgeClass(event.statut);
|
||||
|
||||
html += `
|
||||
<tr>
|
||||
<td style="font-weight: 500; color: #495057;">${event.id || '-'}</td>
|
||||
<td>${dateHeure}</td>
|
||||
<td>${event.intervenant_nom || '-'}</td>
|
||||
<td>${event.beneficiaire_nom || '-'}</td>
|
||||
<td>${event.traducteur_nom || '-'}</td>
|
||||
<td>${event.langue || '-'}</td>
|
||||
<td><span class="badge badge-${statutClass}">${statutLabel}</span></td>
|
||||
<td style="max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="${escapeHtml(event.commentaire || '')}">${event.commentaire || '-'}</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
|
||||
tableBody.innerHTML = html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formate une date au format français
|
||||
*/
|
||||
function formatDate(dateString) {
|
||||
if (!dateString) return '-';
|
||||
const date = new Date(dateString + 'T00:00:00');
|
||||
return date.toLocaleDateString('fr-FR', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Formate le label du statut pour l'affichage
|
||||
*/
|
||||
function formatStatutLabel(statut) {
|
||||
if (!statut) return '-';
|
||||
const statutMap = {
|
||||
'prevu': 'Prévu',
|
||||
'annule': 'Annulé',
|
||||
'non_tenu': 'Non tenu',
|
||||
'cloture': 'Clôturé',
|
||||
'absence': 'Absence'
|
||||
};
|
||||
return statutMap[statut.toLowerCase()] || statut;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne la classe Bootstrap pour le badge de statut
|
||||
*/
|
||||
function getStatutBadgeClass(statut) {
|
||||
if (!statut) return 'secondary';
|
||||
const statutMap = {
|
||||
'prevu': 'success',
|
||||
'annule': 'danger',
|
||||
'non_tenu': 'warning',
|
||||
'cloture': 'secondary',
|
||||
'absence': 'warning'
|
||||
};
|
||||
return statutMap[statut.toLowerCase()] || 'secondary';
|
||||
}
|
||||
|
||||
/**
|
||||
* Échappe les caractères HTML pour éviter les injections XSS
|
||||
*/
|
||||
function escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour les compteurs
|
||||
*/
|
||||
function updateCounters(total, filtered) {
|
||||
const totalCount = document.getElementById('total-count');
|
||||
const filteredCount = document.getElementById('filtered-count');
|
||||
|
||||
if (totalCount) {
|
||||
totalCount.textContent = total;
|
||||
}
|
||||
if (filteredCount) {
|
||||
filteredCount.textContent = filtered;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Affiche la pagination (uniquement les numéros de page)
|
||||
*/
|
||||
function displayPagination(currentPageNum, totalPages) {
|
||||
const paginationContainer = document.getElementById('stats-pagination-container');
|
||||
if (!paginationContainer || totalPages <= 1) {
|
||||
if (paginationContainer) {
|
||||
paginationContainer.style.display = 'none';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Afficher le conteneur
|
||||
paginationContainer.style.display = 'block';
|
||||
|
||||
let html = '<nav aria-label="Pagination" style="display: flex; justify-content: center; align-items: center;"><ul class="pagination" style="margin: 0; display: flex; list-style: none; gap: 5px; flex-wrap: wrap;">';
|
||||
|
||||
// Afficher toutes les pages si peu nombreuses, sinon un sous-ensemble
|
||||
const maxPagesToShow = 10;
|
||||
|
||||
if (totalPages <= maxPagesToShow) {
|
||||
// Afficher toutes les pages
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
if (i === currentPageNum) {
|
||||
html += `<li class="page-item active"><span class="page-link" style="padding: 8px 12px; border: 1px solid #007bff; border-radius: 4px; background: #007bff; color: #fff; font-weight: 600;">${i}</span></li>`;
|
||||
} else {
|
||||
html += `<li class="page-item"><a class="page-link" href="#" data-page="${i}" style="padding: 8px 12px; border: 1px solid #dee2e6; border-radius: 4px; text-decoration: none; color: #007bff; background: #fff; cursor: pointer;">${i}</a></li>`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Afficher un sous-ensemble de pages avec ellipses
|
||||
let startPage = Math.max(1, currentPageNum - Math.floor(maxPagesToShow / 2));
|
||||
let endPage = Math.min(totalPages, startPage + maxPagesToShow - 1);
|
||||
|
||||
if (endPage - startPage < maxPagesToShow - 1) {
|
||||
startPage = Math.max(1, endPage - maxPagesToShow + 1);
|
||||
}
|
||||
|
||||
// Première page
|
||||
if (startPage > 1) {
|
||||
html += `<li class="page-item"><a class="page-link" href="#" data-page="1" style="padding: 8px 12px; border: 1px solid #dee2e6; border-radius: 4px; text-decoration: none; color: #007bff; background: #fff; cursor: pointer;">1</a></li>`;
|
||||
if (startPage > 2) {
|
||||
html += `<li class="page-item disabled"><span class="page-link" style="padding: 8px 12px; color: #6c757d;">...</span></li>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Pages du milieu
|
||||
for (let i = startPage; i <= endPage; i++) {
|
||||
if (i === currentPageNum) {
|
||||
html += `<li class="page-item active"><span class="page-link" style="padding: 8px 12px; border: 1px solid #007bff; border-radius: 4px; background: #007bff; color: #fff; font-weight: 600;">${i}</span></li>`;
|
||||
} else {
|
||||
html += `<li class="page-item"><a class="page-link" href="#" data-page="${i}" style="padding: 8px 12px; border: 1px solid #dee2e6; border-radius: 4px; text-decoration: none; color: #007bff; background: #fff; cursor: pointer;">${i}</a></li>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Dernière page
|
||||
if (endPage < totalPages) {
|
||||
if (endPage < totalPages - 1) {
|
||||
html += `<li class="page-item disabled"><span class="page-link" style="padding: 8px 12px; color: #6c757d;">...</span></li>`;
|
||||
}
|
||||
html += `<li class="page-item"><a class="page-link" href="#" data-page="${totalPages}" style="padding: 8px 12px; border: 1px solid #dee2e6; border-radius: 4px; text-decoration: none; color: #007bff; background: #fff; cursor: pointer;">${totalPages}</a></li>`;
|
||||
}
|
||||
}
|
||||
|
||||
html += '</ul></nav>';
|
||||
|
||||
paginationContainer.innerHTML = html;
|
||||
|
||||
// Ajouter les écouteurs d'événements
|
||||
const pageLinks = paginationContainer.querySelectorAll('a[data-page]');
|
||||
pageLinks.forEach(link => {
|
||||
link.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const page = parseInt(link.getAttribute('data-page'), 10);
|
||||
if (page && page !== currentPage) {
|
||||
currentPage = page;
|
||||
loadEvents();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
1141
assets/js/package-lock.json
generated
Normal file
1141
assets/js/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
assets/js/package.json
Normal file
20
assets/js/package.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"watch": "vite build --watch",
|
||||
"dev": "vite build --watch --mode development"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fullcalendar/core": "^6.1.18",
|
||||
"@fullcalendar/daygrid": "^6.1.18",
|
||||
"@fullcalendar/timegrid": "^6.1.18",
|
||||
"@fullcalendar/list": "^6.1.18",
|
||||
"@fullcalendar/interaction": "^6.1.18",
|
||||
"bootstrap": "^5.3.7",
|
||||
"select2": "^4.0.13",
|
||||
"toastr": "^2.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^7.0.3"
|
||||
}
|
||||
}
|
||||
157
assets/js/traduction-langue-admin.js
Normal file
157
assets/js/traduction-langue-admin.js
Normal file
@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Script admin pour la gestion des règles dépendantes des capacités de traduction
|
||||
*/
|
||||
(function($) {
|
||||
'use strict';
|
||||
|
||||
$(document).ready(function() {
|
||||
// Variables globales
|
||||
const $addBtn = $('#add-sub-rule-btn');
|
||||
const $modal = $('#sub-rule-modal');
|
||||
const $confirmBtn = $('#confirm-add-sub-rule');
|
||||
const $cancelBtn = $('#cancel-add-sub-rule');
|
||||
const $spinner = $('.traduction-langue-add-sub-rule .spinner');
|
||||
const parentId = $addBtn.data('parent-id');
|
||||
|
||||
// Ouvrir la modal au clic sur le bouton
|
||||
$addBtn.on('click', function(e) {
|
||||
e.preventDefault();
|
||||
openModal();
|
||||
});
|
||||
|
||||
// Fermer la modal au clic sur Annuler
|
||||
$cancelBtn.on('click', function(e) {
|
||||
e.preventDefault();
|
||||
closeModal();
|
||||
});
|
||||
|
||||
// Fermer la modal au clic sur l'overlay
|
||||
$('.sub-rule-modal-overlay').on('click', function(e) {
|
||||
closeModal();
|
||||
});
|
||||
|
||||
// Fermer la modal avec la touche ESC
|
||||
$(document).on('keyup', function(e) {
|
||||
if (e.key === 'Escape' && $modal.is(':visible')) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Confirmer la création de la règle dépendante
|
||||
$confirmBtn.on('click', function(e) {
|
||||
e.preventDefault();
|
||||
createSubRule();
|
||||
});
|
||||
|
||||
/**
|
||||
* Ouvre la modal de confirmation
|
||||
*/
|
||||
function openModal() {
|
||||
$modal.fadeIn(200);
|
||||
$('body').addClass('modal-open');
|
||||
}
|
||||
|
||||
/**
|
||||
* Ferme la modal de confirmation
|
||||
*/
|
||||
function closeModal() {
|
||||
$modal.fadeOut(200);
|
||||
$('body').removeClass('modal-open');
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée une règle dépendante via l'API REST
|
||||
*/
|
||||
function createSubRule() {
|
||||
// Désactiver les boutons pendant le traitement
|
||||
$confirmBtn.prop('disabled', true);
|
||||
$cancelBtn.prop('disabled', true);
|
||||
$spinner.addClass('is-active');
|
||||
|
||||
// Données à envoyer
|
||||
const data = {
|
||||
parent_id: parentId
|
||||
};
|
||||
|
||||
// Appel REST API
|
||||
$.ajax({
|
||||
url: traductionLangueAdmin.restUrl,
|
||||
method: 'POST',
|
||||
data: JSON.stringify(data),
|
||||
contentType: 'application/json',
|
||||
beforeSend: function(xhr) {
|
||||
// Ajouter le nonce pour l'authentification
|
||||
xhr.setRequestHeader('X-WP-Nonce', traductionLangueAdmin.nonce);
|
||||
},
|
||||
success: function(response) {
|
||||
if (response.success && response.data) {
|
||||
// Afficher un message de succès
|
||||
showNotice('success', response.data.message || 'Règle dépendante créée avec succès.');
|
||||
|
||||
// Recharger la page après un court délai pour voir la nouvelle règle
|
||||
setTimeout(function() {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
} else {
|
||||
showNotice('error', 'Une erreur est survenue lors de la création de la règle.');
|
||||
resetButtons();
|
||||
}
|
||||
},
|
||||
error: function(xhr) {
|
||||
let errorMessage = 'Une erreur est survenue lors de la création de la règle.';
|
||||
|
||||
if (xhr.responseJSON && xhr.responseJSON.message) {
|
||||
errorMessage = xhr.responseJSON.message;
|
||||
} else if (xhr.responseText) {
|
||||
try {
|
||||
const response = JSON.parse(xhr.responseText);
|
||||
if (response.message) {
|
||||
errorMessage = response.message;
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignorer les erreurs de parsing
|
||||
}
|
||||
}
|
||||
|
||||
showNotice('error', errorMessage);
|
||||
resetButtons();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Réinitialise l'état des boutons après une erreur
|
||||
*/
|
||||
function resetButtons() {
|
||||
$confirmBtn.prop('disabled', false);
|
||||
$cancelBtn.prop('disabled', false);
|
||||
$spinner.removeClass('is-active');
|
||||
closeModal();
|
||||
}
|
||||
|
||||
/**
|
||||
* Affiche une notification WordPress
|
||||
*
|
||||
* @param {string} type - Type de notification (success, error, warning, info)
|
||||
* @param {string} message - Message à afficher
|
||||
*/
|
||||
function showNotice(type, message) {
|
||||
const noticeClass = 'notice-' + type;
|
||||
const $notice = $('<div class="notice ' + noticeClass + ' is-dismissible"><p>' + message + '</p></div>');
|
||||
|
||||
// Insérer la notification après le titre de la page
|
||||
$('.wrap h1').first().after($notice);
|
||||
|
||||
// Rendre la notification dismissible
|
||||
$(document).trigger('wp-updates-notice-added');
|
||||
|
||||
// Auto-supprimer après 5 secondes
|
||||
setTimeout(function() {
|
||||
$notice.fadeOut(function() {
|
||||
$(this).remove();
|
||||
});
|
||||
}, 5000);
|
||||
}
|
||||
});
|
||||
|
||||
})(jQuery);
|
||||
102
assets/js/traduction-langue-list.js
Normal file
102
assets/js/traduction-langue-list.js
Normal file
@ -0,0 +1,102 @@
|
||||
/**
|
||||
* Script pour la page de liste des capacités de traduction
|
||||
* Gère les interactions (toggle des enfants, etc.)
|
||||
*/
|
||||
(function($) {
|
||||
'use strict';
|
||||
|
||||
$(document).ready(function() {
|
||||
// Toggle des règles dépendantes (enfants)
|
||||
$('.toggle-children').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const $button = $(this);
|
||||
const $card = $button.closest('.capacite-card');
|
||||
const $children = $card.find('.card-children');
|
||||
|
||||
// Toggle de l'affichage
|
||||
if ($children.is(':visible')) {
|
||||
$children.slideUp(300);
|
||||
$button.removeClass('active');
|
||||
} else {
|
||||
$children.slideDown(300);
|
||||
$button.addClass('active');
|
||||
}
|
||||
});
|
||||
|
||||
// Animation au survol des cartes
|
||||
$('.capacite-card').hover(
|
||||
function() {
|
||||
$(this).css('transform', 'translateY(-2px)');
|
||||
},
|
||||
function() {
|
||||
$(this).css('transform', 'translateY(0)');
|
||||
}
|
||||
);
|
||||
|
||||
// Highlight des cartes pleines
|
||||
$('.capacite-card.full').each(function() {
|
||||
const $card = $(this);
|
||||
// Animation subtile pour attirer l'attention
|
||||
setInterval(function() {
|
||||
$card.css('border-left-color', '#d63638');
|
||||
setTimeout(function() {
|
||||
$card.css('border-left-color', '#f0b849');
|
||||
}, 1000);
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
// Confirmation avant suppression (si implémenté plus tard)
|
||||
$(document).on('click', '.delete-capacite', function(e) {
|
||||
if (!confirm('Êtes-vous sûr de vouloir supprimer cette capacité de traduction ?')) {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// Recherche rapide (optionnel - peut être ajouté plus tard)
|
||||
if ($('#capacite-search').length) {
|
||||
$('#capacite-search').on('keyup', function() {
|
||||
const searchTerm = $(this).val().toLowerCase();
|
||||
|
||||
$('.capacite-card').each(function() {
|
||||
const $card = $(this);
|
||||
const title = $card.find('.card-title h2').text().toLowerCase();
|
||||
const langue = $card.find('.info-value').first().text().toLowerCase();
|
||||
|
||||
if (title.includes(searchTerm) || langue.includes(searchTerm)) {
|
||||
$card.show();
|
||||
} else {
|
||||
$card.hide();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Tooltip pour les badges
|
||||
$('.badge').each(function() {
|
||||
const $badge = $(this);
|
||||
if ($badge.attr('title')) {
|
||||
$badge.css('cursor', 'help');
|
||||
}
|
||||
});
|
||||
|
||||
// Auto-refresh des statistiques (optionnel - peut être ajouté plus tard)
|
||||
// Permet de rafraîchir les données d'utilisation sans recharger la page
|
||||
function refreshUsageStats() {
|
||||
// À implémenter si nécessaire avec un appel AJAX
|
||||
// vers un endpoint REST qui retourne les stats actualisées
|
||||
}
|
||||
|
||||
// Initialisation des tooltips WordPress (si disponible)
|
||||
if (typeof jQuery.fn.tooltip !== 'undefined') {
|
||||
$('.badge[title]').tooltip({
|
||||
position: {
|
||||
my: 'center bottom-5',
|
||||
at: 'center top'
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
})(jQuery);
|
||||
89
assets/js/vite.config.js
Normal file
89
assets/js/vite.config.js
Normal file
@ -0,0 +1,89 @@
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
rollupOptions: {
|
||||
input: {
|
||||
crvi_libraries: 'crvi_libraries.js',
|
||||
crvi_main: 'crvi_main.js',
|
||||
crvi_main_css: '../css/crvi_main.css',
|
||||
intervenant_profile_css: '../css/intervenant-profile.css',
|
||||
traduction_langue_admin: 'traduction-langue-admin.js',
|
||||
traduction_langue_admin_css: '../css/traduction-langue-admin.css',
|
||||
traduction_langue_list: 'traduction-langue-list.js',
|
||||
traduction_langue_list_css: '../css/traduction-langue-list.css'
|
||||
},
|
||||
output: {
|
||||
format: 'es', // Format ES modules
|
||||
entryFileNames: '[name].min.js',
|
||||
assetFileNames: (assetInfo) => {
|
||||
// Renommer le CSS extrait automatiquement de crvi_libraries.js
|
||||
if (assetInfo.name && (assetInfo.name.includes('crvi_libraries') && assetInfo.name.endsWith('.css'))) {
|
||||
return 'crvi_libraries.min.css';
|
||||
}
|
||||
// Renommer le CSS des libraries (si entrée séparée)
|
||||
if (assetInfo.name && assetInfo.name.includes('crvi_libraries_css')) {
|
||||
return 'crvi_libraries.min.css';
|
||||
}
|
||||
// Renommer le fichier CSS principal en crvi_main.min.css
|
||||
if (assetInfo.name && assetInfo.name.includes('crvi_main_css')) {
|
||||
return 'crvi_main.min.css';
|
||||
}
|
||||
// Renommer le CSS du profil intervenant
|
||||
if (assetInfo.name && assetInfo.name.includes('intervenant_profile_css')) {
|
||||
return 'intervenant-profile.min.css';
|
||||
}
|
||||
// Renommer le CSS de traduction-langue admin
|
||||
if (assetInfo.name && assetInfo.name.includes('traduction_langue_admin_css')) {
|
||||
return 'traduction-langue-admin.min.css';
|
||||
}
|
||||
// Renommer le CSS de traduction-langue list
|
||||
if (assetInfo.name && assetInfo.name.includes('traduction_langue_list_css')) {
|
||||
return 'traduction-langue-list.min.css';
|
||||
}
|
||||
// Pour les autres assets CSS, utiliser le pattern par défaut
|
||||
if (assetInfo.name && assetInfo.name.endsWith('.css')) {
|
||||
return '[name].min.css';
|
||||
}
|
||||
return '[name].min.[ext]';
|
||||
},
|
||||
// Séparer les librairies externes des modules locaux
|
||||
manualChunks: (id) => {
|
||||
// Si c'est le fichier crvi_libraries.js, tout va dans crvi_libraries
|
||||
if (id.includes('crvi_libraries.js')) {
|
||||
return 'crvi_libraries';
|
||||
}
|
||||
// Détecter les node_modules (librairies externes)
|
||||
if (id.includes('node_modules')) {
|
||||
// Séparer Bootstrap, Toastr, FullCalendar et leurs dépendances dans le chunk libraries
|
||||
if (id.includes('bootstrap') ||
|
||||
id.includes('toastr') ||
|
||||
id.includes('select2') ||
|
||||
id.includes('@fullcalendar') ||
|
||||
id.includes('@popperjs') ||
|
||||
id.includes('jquery')) {
|
||||
return 'crvi_libraries';
|
||||
}
|
||||
// Autres node_modules - inclure dans libraries aussi
|
||||
return 'crvi_libraries';
|
||||
}
|
||||
// Les modules locaux restent dans crvi_main
|
||||
return null;
|
||||
},
|
||||
chunkFileNames: '[name].min.js',
|
||||
globals: {
|
||||
'jquery': 'jQuery',
|
||||
'toastr': 'toastr',
|
||||
'select2': 'select2'
|
||||
}
|
||||
}
|
||||
},
|
||||
// Forcer la transpilation pour compatibilité navigateur
|
||||
target: 'es2015',
|
||||
minify: true // Utilise esbuild par défaut (plus rapide que terser)
|
||||
},
|
||||
define: {
|
||||
global: 'window'
|
||||
}
|
||||
});
|
||||
16
phpunit.xml.dist
Normal file
16
phpunit.xml.dist
Normal file
@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit bootstrap="tests/bootstrap.php"
|
||||
colors="true"
|
||||
verbose="true"
|
||||
stopOnFailure="false">
|
||||
<testsuites>
|
||||
<testsuite name="Plugin ESI_crvi_agenda">
|
||||
<directory>./tests/</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<php>
|
||||
<env name="WP_TESTS_DIR" value="C:\Users\jps\Documents\www\wptests\wordpress-develop\tests\phpunit"/>
|
||||
<env name="WP_PHP_BINARY" value="php"/>
|
||||
<env name="WP_PLUGIN_DIR" value="${PWD}"/>
|
||||
</php>
|
||||
</phpunit>
|
||||
4
plugin.php
Normal file
4
plugin.php
Normal file
@ -0,0 +1,4 @@
|
||||
<?php
|
||||
add_action('admin_post_crvi_import_intervenant', ['ESI_CRVI_AGENDA\\controllers\\CRVI_Intervenant_Controller', 'import_csv_admin']);
|
||||
add_action('admin_post_crvi_import_beneficiaire', ['ESI_CRVI_AGENDA\\controllers\\CRVI_Beneficiaire_Controller', 'import_csv_admin']);
|
||||
add_action('admin_post_crvi_import_traducteur', ['ESI_CRVI_AGENDA\\controllers\\CRVI_Traducteur_Controller', 'import_csv_admin']);
|
||||
105
templates/admin/admin_hub.php
Normal file
105
templates/admin/admin_hub.php
Normal file
@ -0,0 +1,105 @@
|
||||
<?php
|
||||
/**
|
||||
* Hub d'administration principal du plugin CRVI Agenda
|
||||
* Emplacement : templates/admin/admin_hub.php
|
||||
*
|
||||
* Ce fichier sert de point d'entrée pour l'interface d'administration centrale du plugin.
|
||||
* À compléter avec les composants d'administration (tableaux de bord, accès rapides, etc.)
|
||||
*/
|
||||
|
||||
// Sécurité WordPress : empêche l'accès direct au fichier
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Génération des URLs admin pour les entités et pages spécifiques
|
||||
function crvi_admin_url($page) {
|
||||
return admin_url('edit.php?post_type=' . $page);
|
||||
}
|
||||
|
||||
function crvi_import_form($entity) {
|
||||
$action = 'crvi_import_' . $entity;
|
||||
$label = 'Importer ' . ucfirst($entity) . ' (CSV)';
|
||||
$nonce = wp_create_nonce('crvi_import_' . $entity);
|
||||
return '<form method="post" enctype="multipart/form-data" action="' . esc_url(admin_url('admin-post.php')) . '" style="margin-top:1em;">'
|
||||
. '<input type="hidden" name="action" value="' . esc_attr($action) . '">'
|
||||
. '<input type="hidden" name="_wpnonce" value="' . esc_attr($nonce) . '">'
|
||||
. '<input type="file" name="import_csv" accept=".csv" required style="margin-bottom:0.5em;">'
|
||||
. '<button type="submit" class="button">' . esc_html($label) . '</button>'
|
||||
. '</form>';
|
||||
}
|
||||
|
||||
$entities = [
|
||||
'beneficiaire' => 'Bénéficiaires',
|
||||
'intervenant' => 'Intervenants',
|
||||
'local' => 'Locaux',
|
||||
'traducteur' => 'Traducteurs',
|
||||
'departement' => 'Départements',
|
||||
'type_intervention' => 'Types d\'intervention',
|
||||
];
|
||||
|
||||
// Affichage du message de succès/erreur après import
|
||||
function crvi_hub_notice() {
|
||||
if (!empty($_GET['import'])) {
|
||||
$type = $_GET['import'] === 'success' ? 'success' : 'error';
|
||||
$msg = isset($_GET['msg']) ? sanitize_text_field($_GET['msg']) : ($type === 'success' ? 'Import réussi.' : 'Erreur lors de l\'import.');
|
||||
echo '<div class="notice notice-' . esc_attr($type) . ' is-dismissible"><p>' . esc_html($msg) . '</p></div>';
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
<style>
|
||||
.crvi-hub-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: repeat(3, 1fr);
|
||||
gap: 2rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
.crvi-hub-block {
|
||||
background: #fff;
|
||||
border: 1px solid #e5e5e5;
|
||||
border-radius: 6px;
|
||||
padding: 2rem 1.5rem;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.03);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.crvi-hub-block h2 {
|
||||
margin-top: 0;
|
||||
font-size: 1.1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.crvi-hub-block .button {
|
||||
width: 100%;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
.crvi-hub-block form {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
<div class="wrap">
|
||||
<h1>CRVI Agenda – Hub d'administration</h1>
|
||||
<p>Bienvenue sur le hub d'administration du plugin CRVI Agenda.</p>
|
||||
<?php crvi_hub_notice(); ?>
|
||||
<div class="crvi-hub-grid">
|
||||
<?php foreach ($entities as $slug => $label): ?>
|
||||
<div class="crvi-hub-block">
|
||||
<h2><?php echo esc_html($label); ?></h2>
|
||||
<a href="<?php echo esc_url( crvi_admin_url($slug) ); ?>" class="button button-primary">Voir <?php echo esc_html(strtolower($label)); ?></a>
|
||||
<?php echo crvi_import_form($slug); ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<div class="crvi-hub-block">
|
||||
<h2>Agenda</h2>
|
||||
<a href="<?php echo esc_url( admin_url('admin.php?page=crvi_agenda') ); ?>" class="button button-primary">Accéder à l'agenda</a>
|
||||
</div>
|
||||
<div class="crvi-hub-block">
|
||||
<h2>Statistiques</h2>
|
||||
<a href="<?php echo esc_url( admin_url('admin.php?page=crvi_agenda_stats') ); ?>" class="button button-primary">Voir les statistiques</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
127
templates/admin/agenda-page.php
Normal file
127
templates/admin/agenda-page.php
Normal file
@ -0,0 +1,127 @@
|
||||
<?php
|
||||
// Page d'agenda (admin) - squelette vide
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1>Agenda</h1>
|
||||
<p>Cette page est en cours de construction.</p>
|
||||
|
||||
|
||||
<section class="agenda-container">
|
||||
<!--filters-->
|
||||
<section class="filters-container">
|
||||
<form class="filters" method="get" onsubmit="return false;">
|
||||
<div class="filter">
|
||||
<label for="date">Date</label>
|
||||
<input type="date" id="date" name="date">
|
||||
</div>
|
||||
<div class="filter">
|
||||
<label for="local">Local</label>
|
||||
<select id="local" name="local" class="select2">
|
||||
<option value="">Tous</option>
|
||||
<!-- Options dynamiques -->
|
||||
<?php
|
||||
foreach ($locals as $local) {
|
||||
echo '<option value="' . $local['id'] . '">' . $local['nom'] . '</option>';
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter">
|
||||
<label for="personne">Personne</label>
|
||||
<select id="personne" name="personne" class="select2">
|
||||
<option value="">Toutes</option>
|
||||
<!-- Options dynamiques -->
|
||||
<?php
|
||||
foreach ($intervenants as $intervenant) {
|
||||
echo '<option value="' . $intervenant['id'] . '">' . $intervenant['nom'] . '</option>';
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter">
|
||||
<label for="type_intervention">Type d'intervention</label>
|
||||
<select id="type_intervention" name="type_intervention" class="select2">
|
||||
<option value="">Tous</option>
|
||||
<!-- Options dynamiques -->
|
||||
<?php
|
||||
foreach ($types_intervention as $type_intervention) {
|
||||
echo '<option value="' . $type_intervention['id'] . '">' . $type_intervention['nom'] . '</option>';
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter">
|
||||
<label for="beneficiaire">Bénéficiaire</label>
|
||||
<select id="beneficiaire" name="beneficiaire" class="select2">
|
||||
<option value="">Tous</option>
|
||||
<!-- Options dynamiques -->
|
||||
<?php
|
||||
foreach ($beneficiaires as $beneficiaire) {
|
||||
echo '<option value="' . $beneficiaire['id'] . '">' . $beneficiaire['nom'] . '</option>';
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter">
|
||||
<label for="langue">Langue du rendez-vous</label>
|
||||
<select id="langue" name="langue" class="select2">
|
||||
<option value="">Toutes</option>
|
||||
<!-- Options dynamiques -->
|
||||
<?php
|
||||
foreach ($langues_beneficiaire as $langue) {
|
||||
echo '<option value="' . $langue['id'] . '">' . $langue['nom'] . '</option>';
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter">
|
||||
<label for="intervenant_externe">Intervenant externe</label>
|
||||
<select id="intervenant_externe" name="intervenant_externe" class="select2">
|
||||
<option value="">Tous</option>
|
||||
<!-- Options dynamiques -->
|
||||
<?php
|
||||
foreach ($traducteurs as $traducteur) {
|
||||
echo '<option value="' . $traducteur['id'] . '">' . $traducteur['nom'] . '</option>';
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter">
|
||||
<label for="filtre_permanence" style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
|
||||
<input type="checkbox" id="filtre_permanence" name="filtre_permanence" value="permanence">
|
||||
<span>Afficher uniquement les permanences</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="filter">
|
||||
<button type="button" id="addEventBtn" class="btn btn-primary">
|
||||
<i class="fas fa-plus"></i> Ajouter un RDV
|
||||
</button>
|
||||
</div>
|
||||
<div class="filter" style="display: none;">
|
||||
<button type="button" id="resetFiltersBtn" class="btn btn-secondary">
|
||||
<i class="fas fa-times"></i> Réinitialiser
|
||||
</button>
|
||||
</div>
|
||||
<div class="filter">
|
||||
<button type="button" id="filterBtn" class="btn btn-primary" onclick="return false;">
|
||||
<i class="fas fa-filter"></i> Filtrer
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<!--agenda-->
|
||||
<section class="agenda-inner-container" id="agenda-calendar">
|
||||
<div id="loading-indicator" style="text-align: center; padding: 20px;">
|
||||
<p>Chargement du calendrier...</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<!--modal-->
|
||||
|
||||
<?php
|
||||
require_once dirname(__DIR__, 1) . '/modules/agenda-modal.php';
|
||||
?>
|
||||
</section>
|
||||
</div>
|
||||
234
templates/admin/agenda-stats-form.php
Normal file
234
templates/admin/agenda-stats-form.php
Normal file
@ -0,0 +1,234 @@
|
||||
<?php
|
||||
// Template : Tableau de statistiques Agenda avec filtres et pagination
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1><?php esc_html_e('Statistiques Agenda', 'esi_crvi_agenda'); ?></h1>
|
||||
|
||||
<!-- Zone 1: Filtres -->
|
||||
<section class="stats-filters-container" style="background: #fff; padding: 20px; margin-bottom: 20px; border: 1px solid #ddd; border-radius: 4px;">
|
||||
<form class="stats-filters" method="get" onsubmit="return false;" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; align-items: end;">
|
||||
<div class="filter">
|
||||
<label for="stats_date" style="display: block; margin-bottom: 5px; font-weight: 500;"><?php esc_html_e('Date', 'esi_crvi_agenda'); ?></label>
|
||||
<input type="date" id="stats_date" name="date" class="form-control" style="width: 100%;">
|
||||
</div>
|
||||
<div class="filter">
|
||||
<label for="stats_local" style="display: block; margin-bottom: 5px; font-weight: 500;"><?php esc_html_e('Local', 'esi_crvi_agenda'); ?></label>
|
||||
<select id="stats_local" name="local" class="select2 form-control" style="width: 100%;">
|
||||
<option value=""><?php esc_html_e('Tous', 'esi_crvi_agenda'); ?></option>
|
||||
<?php
|
||||
foreach ($locals as $local) {
|
||||
echo '<option value="' . esc_attr($local['id']) . '">' . esc_html($local['nom']) . '</option>';
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter">
|
||||
<label for="stats_personne" style="display: block; margin-bottom: 5px; font-weight: 500;"><?php esc_html_e('Personne', 'esi_crvi_agenda'); ?></label>
|
||||
<select id="stats_personne" name="personne" class="select2 form-control" style="width: 100%;">
|
||||
<option value=""><?php esc_html_e('Toutes', 'esi_crvi_agenda'); ?></option>
|
||||
<?php
|
||||
foreach ($intervenants as $intervenant) {
|
||||
echo '<option value="' . esc_attr($intervenant['id']) . '">' . esc_html($intervenant['nom']) . '</option>';
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter">
|
||||
<label for="stats_type_intervention" style="display: block; margin-bottom: 5px; font-weight: 500;"><?php esc_html_e('Type d\'intervention', 'esi_crvi_agenda'); ?></label>
|
||||
<select id="stats_type_intervention" name="type_intervention" class="select2 form-control" style="width: 100%;">
|
||||
<option value=""><?php esc_html_e('Tous', 'esi_crvi_agenda'); ?></option>
|
||||
<?php
|
||||
foreach ($types_intervention as $type_intervention) {
|
||||
echo '<option value="' . esc_attr($type_intervention['id']) . '">' . esc_html($type_intervention['nom']) . '</option>';
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter">
|
||||
<label for="stats_beneficiaire" style="display: block; margin-bottom: 5px; font-weight: 500;"><?php esc_html_e('Bénéficiaire', 'esi_crvi_agenda'); ?></label>
|
||||
<select id="stats_beneficiaire" name="beneficiaire" class="select2 form-control" style="width: 100%;">
|
||||
<option value=""><?php esc_html_e('Tous', 'esi_crvi_agenda'); ?></option>
|
||||
<?php
|
||||
foreach ($beneficiaires as $beneficiaire) {
|
||||
echo '<option value="' . esc_attr($beneficiaire['id']) . '">' . esc_html($beneficiaire['nom']) . '</option>';
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter">
|
||||
<label for="stats_langue" style="display: block; margin-bottom: 5px; font-weight: 500;"><?php esc_html_e('Langue', 'esi_crvi_agenda'); ?></label>
|
||||
<select id="stats_langue" name="langue" class="select2 form-control" style="width: 100%;">
|
||||
<option value=""><?php esc_html_e('Toutes', 'esi_crvi_agenda'); ?></option>
|
||||
<?php
|
||||
foreach ($langues_beneficiaire as $langue) {
|
||||
echo '<option value="' . esc_attr($langue['id']) . '">' . esc_html($langue['nom']) . '</option>';
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter">
|
||||
<label for="stats_intervenant_externe" style="display: block; margin-bottom: 5px; font-weight: 500;"><?php esc_html_e('Intervenant externe', 'esi_crvi_agenda'); ?></label>
|
||||
<select id="stats_intervenant_externe" name="intervenant_externe" class="select2 form-control" style="width: 100%;">
|
||||
<option value=""><?php esc_html_e('Tous', 'esi_crvi_agenda'); ?></option>
|
||||
<?php
|
||||
foreach ($traducteurs as $traducteur) {
|
||||
echo '<option value="' . esc_attr($traducteur['id']) . '">' . esc_html($traducteur['nom']) . '</option>';
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter">
|
||||
<label for="stats_annee" style="display: block; margin-bottom: 5px; font-weight: 500;"><?php esc_html_e('Année', 'esi_crvi_agenda'); ?></label>
|
||||
<input type="number" id="stats_annee" name="annee" min="2000" max="2100" placeholder="<?php echo date('Y'); ?>" class="form-control" style="width: 100%;">
|
||||
</div>
|
||||
<div class="filter" style="display: flex; flex-direction: column; gap: 10px;">
|
||||
<label for="stats_filtre_permanence" style="display: flex; align-items: center; gap: 8px; cursor: pointer; margin-top: 25px;">
|
||||
<input type="checkbox" id="stats_filtre_permanence" name="filtre_permanence" value="permanence">
|
||||
<span><?php esc_html_e('Afficher uniquement les permanences', 'esi_crvi_agenda'); ?></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="filter" style="display: flex; gap: 10px; justify-content: flex-end; align-items: end;">
|
||||
<button type="button" id="stats_filterBtn" class="btn btn-primary" style="min-width: 100px;">
|
||||
<i class="fas fa-filter"></i> <?php esc_html_e('Filtrer', 'esi_crvi_agenda'); ?>
|
||||
</button>
|
||||
<button type="button" id="stats_resetFiltersBtn" class="btn btn-secondary" style="min-width: 120px;">
|
||||
<i class="fas fa-times"></i> <?php esc_html_e('Réinitialiser', 'esi_crvi_agenda'); ?>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<!-- Zone 2: Résultats -->
|
||||
<section class="stats-results-container" style="background: #f5f5f5; padding: 12px 20px; margin-bottom: 15px; border-radius: 4px;">
|
||||
<div class="stats-counters" style="display: flex; align-items: center; gap: 10px; font-size: 14px;">
|
||||
<strong><?php esc_html_e('Total événements:', 'esi_crvi_agenda'); ?></strong>
|
||||
<span id="total-count" style="font-weight: bold; color: #333;">0</span>
|
||||
<span style="margin: 0 5px;">|</span>
|
||||
<strong><?php esc_html_e('Affichés:', 'esi_crvi_agenda'); ?></strong>
|
||||
<span id="filtered-count" style="font-weight: bold; color: #333;">0</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Zone 3: Tableau -->
|
||||
<section class="stats-table-container" style="background: #fff; border: 1px solid #ddd; border-radius: 4px; overflow: hidden; min-height: 400px; position: relative;">
|
||||
<div id="stats-loading-indicator" style="display: none; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); flex-direction: column; align-items: center; justify-content: center; padding: 60px 20px; width: 100%; z-index: 10;">
|
||||
<div class="spinner-border text-primary" role="status" style="width: 3rem; height: 3rem; margin-bottom: 20px;">
|
||||
<span class="sr-only"><?php esc_html_e('Chargement...', 'esi_crvi_agenda'); ?></span>
|
||||
</div>
|
||||
<p style="color: #666; font-size: 16px; margin: 0; text-align: center;"><?php esc_html_e('Chargement des événements...', 'esi_crvi_agenda'); ?></p>
|
||||
</div>
|
||||
<div id="stats-table-wrapper" style="overflow-x: auto; display: none;">
|
||||
<table class="table table-striped table-hover stats-table" id="stats-events-table" style="display: none; margin: 0; width: 100%; border-collapse: collapse;">
|
||||
<thead style="background: #f8f9fa; border-bottom: 2px solid #dee2e6;">
|
||||
<tr>
|
||||
<th style="padding: 12px; text-align: left; font-weight: 600; border-bottom: 2px solid #dee2e6;"><?php esc_html_e('ID', 'esi_crvi_agenda'); ?></th>
|
||||
<th style="padding: 12px; text-align: left; font-weight: 600; border-bottom: 2px solid #dee2e6;"><?php esc_html_e('Date/Heure', 'esi_crvi_agenda'); ?></th>
|
||||
<th style="padding: 12px; text-align: left; font-weight: 600; border-bottom: 2px solid #dee2e6;"><?php esc_html_e('Intervenant', 'esi_crvi_agenda'); ?></th>
|
||||
<th style="padding: 12px; text-align: left; font-weight: 600; border-bottom: 2px solid #dee2e6;"><?php esc_html_e('Bénéficiaire', 'esi_crvi_agenda'); ?></th>
|
||||
<th style="padding: 12px; text-align: left; font-weight: 600; border-bottom: 2px solid #dee2e6;"><?php esc_html_e('Traducteur', 'esi_crvi_agenda'); ?></th>
|
||||
<th style="padding: 12px; text-align: left; font-weight: 600; border-bottom: 2px solid #dee2e6;"><?php esc_html_e('Langue', 'esi_crvi_agenda'); ?></th>
|
||||
<th style="padding: 12px; text-align: left; font-weight: 600; border-bottom: 2px solid #dee2e6;"><?php esc_html_e('Statut', 'esi_crvi_agenda'); ?></th>
|
||||
<th style="padding: 12px; text-align: left; font-weight: 600; border-bottom: 2px solid #dee2e6;"><?php esc_html_e('Commentaire', 'esi_crvi_agenda'); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="stats-events-table-body" style="background: #fff;">
|
||||
<!-- Rempli dynamiquement par JavaScript -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="stats-pagination" id="stats-pagination-container" style="display: none; padding: 15px 20px; background: #f8f9fa; border-top: 1px solid #dee2e6;">
|
||||
<!-- Rempli dynamiquement par JavaScript -->
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.stats-table tbody tr {
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.stats-table tbody tr:hover {
|
||||
background-color: #f8f9fa !important;
|
||||
}
|
||||
|
||||
.stats-table tbody tr:nth-child(even) {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.stats-table tbody tr:nth-child(odd) {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.stats-table td {
|
||||
padding: 12px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.stats-table .badge {
|
||||
padding: 4px 8px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.stats-table .badge-success {
|
||||
background-color: #28a745;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.stats-table .badge-danger {
|
||||
background-color: #dc3545;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.stats-table .badge-warning {
|
||||
background-color: #ffc107;
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
.stats-table .badge-secondary {
|
||||
background-color: #6c757d;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.stats-table .badge-info {
|
||||
background-color: #17a2b8;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Spinner Bootstrap */
|
||||
.spinner-border {
|
||||
display: inline-block;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
vertical-align: text-bottom;
|
||||
border: 0.25em solid currentColor;
|
||||
border-right-color: transparent;
|
||||
border-radius: 50%;
|
||||
animation: spinner-border 0.75s linear infinite;
|
||||
}
|
||||
|
||||
.spinner-border.text-primary {
|
||||
color: #007bff !important;
|
||||
}
|
||||
|
||||
@keyframes spinner-border {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border-width: 0;
|
||||
}
|
||||
</style>
|
||||
516
templates/admin/permanences-admin.php
Normal file
516
templates/admin/permanences-admin.php
Normal file
@ -0,0 +1,516 @@
|
||||
<?php
|
||||
/**
|
||||
* Template Admin - Gestion des Permanences
|
||||
* Permet à l'admin de créer des permanences pour n'importe quel intervenant
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Récupérer la liste des intervenants
|
||||
$intervenants = \ESI_CRVI_AGENDA\models\CRVI_Intervenant_Model::get_intervenants([], true);
|
||||
|
||||
// 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) {
|
||||
$langues[] = [
|
||||
'id' => $term->slug,
|
||||
'nom' => $term->name,
|
||||
];
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="crvi-admin-permanences" id="admin-permanences-container">
|
||||
<div class="container-fluid py-4">
|
||||
<!-- En-tête -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<p class="text-muted">
|
||||
Créez des permanences pour un intervenant en sélectionnant la période, les jours et la plage horaire, ou importez-les via CSV.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<!-- Navigation par onglets -->
|
||||
<ul class="nav nav-tabs mb-4" id="permanences-tabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="creation-manuelle-tab" data-bs-toggle="tab" data-bs-target="#creation-manuelle" type="button" role="tab" aria-controls="creation-manuelle" aria-selected="true">
|
||||
<i class="fas fa-plus-circle me-2"></i>Création manuelle
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="import-csv-tab" data-bs-toggle="tab" data-bs-target="#import-csv" type="button" role="tab" aria-controls="import-csv" aria-selected="false">
|
||||
<i class="fas fa-file-csv me-2"></i>Import CSV
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- Contenu des onglets -->
|
||||
<div class="tab-content" id="permanences-tab-content">
|
||||
<!-- Onglet 1 : Création manuelle -->
|
||||
<div class="tab-pane fade show active" id="creation-manuelle" role="tabpanel" aria-labelledby="creation-manuelle-tab">
|
||||
<form id="admin-permanences-form">
|
||||
<!-- Sélection de l'intervenant -->
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h3 class="h5 mb-0">
|
||||
<i class="fas fa-user me-2"></i>Intervenant
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label for="intervenant-select" class="form-label fw-bold">Sélectionner un intervenant :</label>
|
||||
<select class="form-select form-select-lg" id="intervenant-select" name="intervenant_id" required>
|
||||
<option value="">-- Sélectionner un intervenant --</option>
|
||||
<?php foreach ($intervenants as $intervenant): ?>
|
||||
<?php
|
||||
$days_attr = '';
|
||||
$times_attr = '';
|
||||
if (function_exists('get_field')) {
|
||||
$jours = get_field('jours_de_disponibilite', 'user_' . $intervenant['id']);
|
||||
$heures = get_field('heures_de_permanences', 'user_' . $intervenant['id']);
|
||||
if (is_array($jours) && !empty($jours)) {
|
||||
$days_attr = implode(',', array_map('strval', $jours));
|
||||
}
|
||||
if (is_array($heures) && !empty($heures)) {
|
||||
$times_attr = implode(',', array_map('strval', $heures));
|
||||
}
|
||||
}
|
||||
?>
|
||||
<option value="<?php echo esc_attr($intervenant['id']); ?>" <?php echo $days_attr !== '' ? 'data-days="' . esc_attr($days_attr) . '"' : ''; ?> <?php echo $times_attr !== '' ? 'data-time-slots="' . esc_attr($times_attr) . '"' : ''; ?>>
|
||||
<?php echo esc_html($intervenant['nom'] . ' ' . $intervenant['prenom']); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 1 : Sélection des permanences -->
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h3 class="h5 mb-0">
|
||||
<i class="fas fa-calendar-check me-2"></i>SECTION 1 : Sélection des permanences
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- Mois de début -->
|
||||
<div class="mb-4">
|
||||
<label for="mois-debut" class="form-label fw-bold">Mois de début :</label>
|
||||
<input type="month" class="form-control form-control-lg" id="mois-debut" name="mois_debut" required>
|
||||
<small class="form-text text-muted">
|
||||
Sélectionnez le mois et l'année à partir desquels les permanences seront créées.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<!-- Période -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label fw-bold">Période :</label>
|
||||
<div class="btn-group" role="group" aria-label="Période">
|
||||
<input type="radio" class="btn-check" name="periode" id="periode-3" value="3" checked>
|
||||
<label class="btn btn-outline-primary" for="periode-3">3 mois</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="periode" id="periode-6" value="6">
|
||||
<label class="btn btn-outline-primary" for="periode-6">6 mois</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Jours de la semaine -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label fw-bold">Jours de la semaine :</label>
|
||||
<div class="row g-2">
|
||||
<div class="col-md-3 col-sm-4 col-6">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="jours[]" value="lundi" id="jour-lundi">
|
||||
<label class="form-check-label" for="jour-lundi">Lundi</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-4 col-6">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="jours[]" value="mardi" id="jour-mardi">
|
||||
<label class="form-check-label" for="jour-mardi">Mardi</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-4 col-6">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="jours[]" value="mercredi" id="jour-mercredi">
|
||||
<label class="form-check-label" for="jour-mercredi">Mercredi</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-4 col-6">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="jours[]" value="jeudi" id="jour-jeudi">
|
||||
<label class="form-check-label" for="jour-jeudi">Jeudi</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-4 col-6">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="jours[]" value="vendredi" id="jour-vendredi">
|
||||
<label class="form-check-label" for="jour-vendredi">Vendredi</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-4 col-6">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="jours[]" value="samedi" id="jour-samedi">
|
||||
<label class="form-check-label" for="jour-samedi">Samedi</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-4 col-6">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="jours[]" value="dimanche" id="jour-dimanche">
|
||||
<label class="form-check-label" for="jour-dimanche">Dimanche</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Durée de permanence -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label fw-bold">Durée de permanence :</label>
|
||||
<div class="btn-group mb-3" role="group" aria-label="Durée">
|
||||
<input type="radio" class="btn-check" name="duree_permanence" id="duree-1h"
|
||||
value="1h" checked>
|
||||
<label class="btn btn-outline-primary" for="duree-1h">1 heure</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="duree_permanence" id="duree-15min"
|
||||
value="15min">
|
||||
<label class="btn btn-outline-primary" for="duree-15min">1/4 d'heure (15 min)</label>
|
||||
</div>
|
||||
<small class="form-text text-muted d-block mb-3">
|
||||
Choisissez la durée de chaque permanence.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<!-- Nombre de tranches (si 1/4 d'heure) -->
|
||||
<div class="mb-4" id="nb-tranches-container" style="display: none;">
|
||||
<label for="nb-tranches" class="form-label fw-bold">Nombre de tranches par heure :</label>
|
||||
<select class="form-select form-select-lg" id="nb-tranches" name="nb_tranches">
|
||||
<option value="1">1 tranche (ex: 11h00 → 11h15)</option>
|
||||
<option value="2">2 tranches (ex: 11h00 → 11h15, 11h15 → 11h30)</option>
|
||||
<option value="3">3 tranches (ex: 11h00 → 11h15, 11h15 → 11h30, 11h30 → 11h45)</option>
|
||||
<option value="4">4 tranches (ex: 11h00 → 11h15, 11h15 → 11h30, 11h30 → 11h45, 11h45 → 12h00)</option>
|
||||
</select>
|
||||
<small class="form-text text-muted">
|
||||
Si vous choisissez 1/4 d'heure, indiquez combien de tranches de 15 minutes créer pour chaque heure sélectionnée.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<!-- Heures de permanence -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label fw-bold">Heures de permanence :</label>
|
||||
<small class="form-text text-muted d-block mb-3" id="heures-description">
|
||||
Sélectionnez les heures de début. Chaque heure sélectionnée créera une tranche d'1 heure (ex: 09:00 → 10:00).
|
||||
</small>
|
||||
<div class="row g-2">
|
||||
<div class="col-12">
|
||||
<label class="form-label fw-semibold text-muted">Matin :</label>
|
||||
</div>
|
||||
<?php
|
||||
// Plage matin : 09:00 à 12:00
|
||||
for ($h = 9; $h <= 12; $h++):
|
||||
$heure = str_pad($h, 2, '0', STR_PAD_LEFT) . ':00';
|
||||
$heureFin = str_pad($h + 1, 2, '0', STR_PAD_LEFT) . ':00';
|
||||
?>
|
||||
<div class="col-md-3 col-sm-4 col-6">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="heures[]" value="<?php echo esc_attr($heure); ?>" id="heure-<?php echo $h; ?>">
|
||||
<label class="form-check-label" for="heure-<?php echo $h; ?>">
|
||||
<?php echo $heure; ?> → <?php echo $heureFin; ?>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<?php endfor; ?>
|
||||
|
||||
<div class="col-12 mt-2">
|
||||
<label class="form-label fw-semibold text-muted">Après-midi :</label>
|
||||
</div>
|
||||
<?php
|
||||
// Plage après-midi : 13:00 à 16:00
|
||||
for ($h = 13; $h <= 16; $h++):
|
||||
$heure = str_pad($h, 2, '0', STR_PAD_LEFT) . ':00';
|
||||
$heureFin = str_pad($h + 1, 2, '0', STR_PAD_LEFT) . ':00';
|
||||
?>
|
||||
<div class="col-md-3 col-sm-4 col-6">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="heures[]" value="<?php echo esc_attr($heure); ?>" id="heure-<?php echo $h; ?>">
|
||||
<label class="form-check-label" for="heure-<?php echo $h; ?>">
|
||||
<?php echo $heure; ?> → <?php echo $heureFin; ?>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<?php endfor; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Aperçu des tranches générées -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">Aperçu des tranches générées :</label>
|
||||
<div id="tranches-preview" class="alert alert-info">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
Veuillez sélectionner une plage horaire pour voir l'aperçu.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Estimation -->
|
||||
<div class="mb-3">
|
||||
<div id="estimation-container" class="alert alert-warning" style="display: none;">
|
||||
<i class="fas fa-calculator me-2"></i>
|
||||
<strong>Estimation :</strong> <span id="estimation-count">0</span> événements seront créés
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 2 : Langues -->
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header bg-info text-white">
|
||||
<h3 class="h5 mb-0">
|
||||
<i class="fas fa-language me-2"></i>SECTION 2 : Langues des permanences
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label for="langues-permanences" class="form-label fw-bold">Langues disponibles (optionnel) :</label>
|
||||
<select class="form-select form-select-lg" id="langues-permanences" name="langues[]" multiple>
|
||||
<?php if (!empty($langues)): ?>
|
||||
<?php foreach ($langues as $langue): ?>
|
||||
<option value="<?php echo esc_attr($langue['id']); ?>">
|
||||
<?php echo esc_html($langue['nom']); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<option value="">Aucune langue disponible</option>
|
||||
<?php endif; ?>
|
||||
</select>
|
||||
<small class="form-text text-muted">
|
||||
Sélectionnez une ou plusieurs langues pour ces permanences.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 3 : Informations complémentaires -->
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header bg-secondary text-white">
|
||||
<h3 class="h5 mb-0">
|
||||
<i class="fas fa-sticky-note me-2"></i>SECTION 3 : Informations complémentaires
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label for="informations-complementaires" class="form-label">Notes ou commentaires (optionnel)</label>
|
||||
<textarea class="form-control" id="informations-complementaires" name="informations_complementaires" rows="3" placeholder="Ajoutez des notes ou commentaires sur ces permanences..."></textarea>
|
||||
<small class="form-text text-muted">
|
||||
Ces informations seront associées aux permanences créées.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bouton de soumission -->
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body text-center">
|
||||
<button type="submit" class="btn btn-primary btn-lg" id="submit-permanences-btn">
|
||||
<i class="fas fa-save me-2"></i>Enregistrer les permanences
|
||||
</button>
|
||||
<button type="reset" class="btn btn-outline-secondary btn-lg ms-2" id="reset-permanences-btn">
|
||||
<i class="fas fa-redo me-2"></i>Réinitialiser
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Onglet 2 : Import CSV -->
|
||||
<div class="tab-pane fade" id="import-csv" role="tabpanel" aria-labelledby="import-csv-tab">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-info text-white">
|
||||
<h3 class="h5 mb-0">
|
||||
<i class="fas fa-file-csv me-2"></i>Import de permanences via CSV
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-info mb-4">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
<strong>Format CSV attendu :</strong> Le fichier CSV doit contenir les colonnes suivantes :
|
||||
<ul class="mt-2 mb-0">
|
||||
<li><code>intervenant_id</code> : ID de l'intervenant</li>
|
||||
<li><code>date_debut</code> : Date de début (format YYYY-MM-DD)</li>
|
||||
<li><code>date_fin</code> : Date de fin (format YYYY-MM-DD)</li>
|
||||
<li><code>heure_debut</code> : Heure de début (format HH:MM)</li>
|
||||
<li><code>heure_fin</code> : Heure de fin (format HH:MM)</li>
|
||||
<li><code>informations_complementaires</code> : Notes (optionnel)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<form id="import-csv-form" enctype="multipart/form-data">
|
||||
<div class="mb-3">
|
||||
<label for="csv-file" class="form-label fw-bold">Sélectionner un fichier CSV :</label>
|
||||
<input type="file" class="form-control form-control-lg" id="csv-file" name="csv_file" accept=".csv" required>
|
||||
<small class="form-text text-muted">
|
||||
Format accepté : CSV (séparateur virgule)
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-info btn-lg" id="submit-csv-import-btn">
|
||||
<i class="fas fa-upload me-2"></i>Importer le CSV
|
||||
</button>
|
||||
<button type="reset" class="btn btn-outline-secondary btn-lg" id="reset-csv-import-btn">
|
||||
<i class="fas fa-redo me-2"></i>Réinitialiser
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div id="csv-import-result" class="mt-4" style="display: none;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.crvi-admin-permanences {
|
||||
min-height: 60vh;
|
||||
}
|
||||
|
||||
/* Augmentation de la largeur de la page */
|
||||
.crvi-admin-permanences .container-fluid {
|
||||
max-width: 100%;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.btn-group .btn-check:checked + .btn {
|
||||
background-color: #0d6efd;
|
||||
border-color: #0d6efd;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.form-check-input:checked {
|
||||
background-color: #0d6efd;
|
||||
border-color: #0d6efd;
|
||||
}
|
||||
|
||||
#tranches-preview {
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
#tranches-preview ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
#tranches-preview li {
|
||||
background-color: #e7f3ff;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #b3d9ff;
|
||||
}
|
||||
|
||||
.alert {
|
||||
border-left: 4px solid;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
border-left-color: #0dcaf0;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
border-left-color: #ffc107;
|
||||
}
|
||||
|
||||
/* Styles pour les onglets */
|
||||
.nav-tabs {
|
||||
border-bottom: 2px solid #dee2e6;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link {
|
||||
color: #495057;
|
||||
border: none;
|
||||
border-bottom: 3px solid transparent;
|
||||
padding: 0.75rem 1.5rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link:hover {
|
||||
border-color: #dee2e6;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link.active {
|
||||
color: #0d6efd;
|
||||
background-color: transparent;
|
||||
border-color: #0d6efd;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
padding-top: 1.5rem;
|
||||
}
|
||||
|
||||
/* Amélioration de l'affichage des résultats d'import */
|
||||
#csv-import-result {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
#csv-import-result .alert {
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// Gérer l'affichage conditionnel du champ nombre de tranches
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const duree1h = document.getElementById('duree-1h');
|
||||
const duree15min = document.getElementById('duree-15min');
|
||||
const nbTranchesContainer = document.getElementById('nb-tranches-container');
|
||||
const heuresDescription = document.getElementById('heures-description');
|
||||
|
||||
function updateDescription() {
|
||||
if (duree15min && duree15min.checked) {
|
||||
const nbTranches = document.getElementById('nb-tranches')?.value || '1';
|
||||
heuresDescription.textContent = `Sélectionnez les heures de début. Pour chaque heure sélectionnée, ${nbTranches} tranche(s) de 15 minutes seront créées (ex: 11h00 → 11h15).`;
|
||||
} else {
|
||||
heuresDescription.textContent = `Sélectionnez les heures de début. Chaque heure sélectionnée créera une tranche d'1 heure (ex: 09:00 → 10:00).`;
|
||||
}
|
||||
}
|
||||
|
||||
if (duree1h && duree15min && nbTranchesContainer) {
|
||||
duree1h.addEventListener('change', function() {
|
||||
nbTranchesContainer.style.display = 'none';
|
||||
updateDescription();
|
||||
});
|
||||
|
||||
duree15min.addEventListener('change', function() {
|
||||
nbTranchesContainer.style.display = 'block';
|
||||
updateDescription();
|
||||
});
|
||||
|
||||
const nbTranches = document.getElementById('nb-tranches');
|
||||
if (nbTranches) {
|
||||
nbTranches.addEventListener('change', updateDescription);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
191
templates/admin/traduction-langue-list.php
Normal file
191
templates/admin/traduction-langue-list.php
Normal file
@ -0,0 +1,191 @@
|
||||
<?php
|
||||
/**
|
||||
* Template pour la page admin des capacités de traduction
|
||||
* Vue moderne avec structure Parent → Enfant
|
||||
*/
|
||||
|
||||
// Sécurité : vérifier que le fichier est bien appelé depuis WordPress
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Helper pour récupérer le(s) nom(s) de langue(s) - gère les langues multiples
|
||||
function get_langue_name($post_id) {
|
||||
$langue_ids = get_field('langue', $post_id);
|
||||
|
||||
if (empty($langue_ids)) {
|
||||
return '<span class="na">Non définie</span>';
|
||||
}
|
||||
|
||||
// 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 '<span class="na">Non définie</span>';
|
||||
}
|
||||
|
||||
// Si plusieurs langues, les afficher avec des badges
|
||||
if (count($langue_names) > 1) {
|
||||
$output = '<div class="langue-badges">';
|
||||
foreach ($langue_names as $name) {
|
||||
$output .= '<span class="langue-badge">' . $name . '</span>';
|
||||
}
|
||||
$output .= '</div>';
|
||||
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 '<span class="badge badge-warning" title="Cette capacité a des événements associés">🔒 Événements</span>';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="wrap traduction-langue-list-wrap">
|
||||
<h1 class="wp-heading-inline">
|
||||
<span class="dashicons dashicons-translation"></span>
|
||||
Capacités de traduction
|
||||
</h1>
|
||||
|
||||
<a href="<?php echo admin_url('post-new.php?post_type=traduction_langue'); ?>" class="page-title-action">
|
||||
Ajouter une capacité
|
||||
</a>
|
||||
|
||||
<hr class="wp-header-end">
|
||||
|
||||
<?php if (empty($parent_capacites)) : ?>
|
||||
<div class="notice notice-info">
|
||||
<p>Aucune capacité de traduction trouvée. <a href="<?php echo admin_url('post-new.php?post_type=traduction_langue'); ?>">Créer la première capacité</a></p>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<div class="traduction-langue-grid">
|
||||
<?php foreach ($parent_capacites as $parent) :
|
||||
$langue_name = get_langue_name($parent->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;
|
||||
?>
|
||||
<div class="capacite-card <?php echo !$actif ? 'inactive' : ''; ?> <?php echo $is_full ? 'full' : ''; ?>">
|
||||
<div class="card-header">
|
||||
<div class="card-title">
|
||||
<h2><?php echo esc_html($parent->post_title); ?></h2>
|
||||
<div class="card-badges">
|
||||
<?php if (!$actif) : ?>
|
||||
<span class="badge badge-inactive">Inactif</span>
|
||||
<?php endif; ?>
|
||||
<?php echo $has_events; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<a href="<?php echo get_edit_post_link($parent->ID); ?>" class="button button-primary">
|
||||
<span class="dashicons dashicons-edit"></span> Modifier
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<span class="info-label">Langue</span>
|
||||
<span class="info-value"><?php echo $langue_name; ?></span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Jour</span>
|
||||
<span class="info-value"><?php echo esc_html(ucfirst($jour ?: 'N/A')); ?></span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Période</span>
|
||||
<span class="info-value"><?php echo esc_html(ucfirst($periode ?: 'N/A')); ?></span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">Limite</span>
|
||||
<span class="info-value"><?php echo esc_html($limite); ?> / <?php echo esc_html($limite_par); ?></span>
|
||||
</div>
|
||||
<?php if (!empty($exception_display)) : ?>
|
||||
<div class="info-item info-item-full">
|
||||
<span class="info-label">Exception</span>
|
||||
<span class="info-value exception-badge"><?php echo esc_html($exception_display); ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="usage-bar">
|
||||
<div class="usage-label">
|
||||
<span>Utilisation</span>
|
||||
<span class="usage-stats"><?php echo $used; ?> / <?php echo $limite; ?> (<?php echo $percentage; ?>%)</span>
|
||||
</div>
|
||||
<div class="usage-progress">
|
||||
<div class="usage-progress-bar <?php echo $percentage >= 100 ? 'full' : ($percentage >= 80 ? 'warning' : ''); ?>"
|
||||
style="width: <?php echo min($percentage, 100); ?>%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
1
templates/email/.gitkeep
Normal file
1
templates/email/.gitkeep
Normal file
@ -0,0 +1 @@
|
||||
|
||||
56
templates/email/rdv-cloture-email-template.php
Normal file
56
templates/email/rdv-cloture-email-template.php
Normal file
@ -0,0 +1,56 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Rendez-vous clôturés</title>
|
||||
</head>
|
||||
<body style="font-family: Arial, sans-serif; background-color: #f7f7f7; margin:0; padding:0;">
|
||||
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color: #f7f7f7; padding: 20px 0;">
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table width="600" cellpadding="0" cellspacing="0" border="0" style="background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 6px #eee; padding: 24px;">
|
||||
<tr>
|
||||
<td align="center" style="padding-bottom: 16px;">
|
||||
<h2 style="color: #333333; margin: 0;">Liste des rendez-vous clôturés</h2>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding-bottom: 16px; color: #444444; font-size: 16px;">
|
||||
Voici la liste des rendez-vous clôturés récemment :
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<table width="100%" cellpadding="8" cellspacing="0" border="1" style="border-collapse: collapse; border: 1px solid #cccccc;">
|
||||
<thead>
|
||||
<tr style="background-color: #f0f0f0; color: #222;">
|
||||
<th align="left">Date</th>
|
||||
<th align="left">Heure</th>
|
||||
<th align="left">Intervenant</th>
|
||||
<th align="left">Bénéficiaire</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($datas as $rdv): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($rdv['date'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></td>
|
||||
<td><?= htmlspecialchars($rdv['heure'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></td>
|
||||
<td><?= htmlspecialchars($rdv['intervenant'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></td>
|
||||
<td><?= htmlspecialchars($rdv['beneficiaire'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding-top: 24px; color: #888888; font-size: 13px; text-align: center;">
|
||||
Ceci est un message automatique, merci de ne pas répondre directement à cet email.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
1
templates/front/.gitkeep
Normal file
1
templates/front/.gitkeep
Normal file
@ -0,0 +1 @@
|
||||
|
||||
238
templates/front/intervenant-agenda.php
Normal file
238
templates/front/intervenant-agenda.php
Normal file
@ -0,0 +1,238 @@
|
||||
<?php
|
||||
/**
|
||||
* Template Agenda Intervenant
|
||||
* Affiche le calendrier complet avec filtres
|
||||
* Shortcode: [crvi_intervenant_agenda]
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
$user = wp_get_current_user();
|
||||
?>
|
||||
|
||||
<div class="crvi-intervenant-agenda" id="intervenant-agenda-container">
|
||||
<div class="container-fluid py-4">
|
||||
<!-- En-tête -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h1 class="h2 mb-2">
|
||||
<i class="fas fa-calendar-alt me-2"></i>Mon Agenda
|
||||
</h1>
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light rounded p-3">
|
||||
<div class="container-fluid">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="<?php echo esc_url(home_url('/espace-intervenant')); ?>">
|
||||
<i class="fas fa-home me-1"></i>Hub
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="<?php echo esc_url(home_url('/espace-intervenant-agenda')); ?>">
|
||||
<i class="fas fa-calendar-alt me-1"></i>Agenda
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="<?php echo esc_url(home_url('/espace-intervenant-profil')); ?>">
|
||||
<i class="fas fa-user me-1"></i>Mon Profil
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="<?php echo esc_url(home_url('/encodage-permanences')); ?>">
|
||||
<i class="fas fa-clock me-1"></i>Permanences
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Onglets pour basculer entre mon agenda et celui des collègues -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<ul class="nav nav-tabs" id="agendaTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="my-agenda-tab" data-bs-toggle="tab" data-bs-target="#my-agenda" type="button" role="tab" aria-controls="my-agenda" aria-selected="true">
|
||||
<i class="fas fa-calendar-check me-1"></i>Mon Agenda
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="colleagues-agenda-tab" data-bs-toggle="tab" data-bs-target="#colleagues-agenda" type="button" role="tab" aria-controls="colleagues-agenda" aria-selected="false">
|
||||
<i class="fas fa-users me-1"></i>Agenda de mes collègues
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contenu des onglets -->
|
||||
<div class="tab-content" id="agendaTabsContent">
|
||||
<!-- Onglet Mon Agenda -->
|
||||
<div class="tab-pane fade show active" id="my-agenda" role="tabpanel" aria-labelledby="my-agenda-tab">
|
||||
<section class="agenda-container">
|
||||
<!-- Filtres -->
|
||||
<section class="filters-container">
|
||||
<form class="filters" method="get" onsubmit="return false;">
|
||||
<div class="filter">
|
||||
<label for="local">Local</label>
|
||||
<select id="local" name="local" class="select2">
|
||||
<option value="">Tous</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter">
|
||||
<label for="beneficiaire">Bénéficiaire</label>
|
||||
<select id="beneficiaire" name="beneficiaire" class="select2">
|
||||
<option value="">Tous</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter">
|
||||
<label for="type_rdv">Type de RDV</label>
|
||||
<select id="type_rdv" name="type_rdv" class="select2">
|
||||
<option value="">Tous</option>
|
||||
<option value="individuel">Individuel</option>
|
||||
<option value="groupe">Groupe</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter">
|
||||
<label for="type_intervention">Type d'intervention</label>
|
||||
<select id="type_intervention" name="type_intervention" class="select2">
|
||||
<option value="">Tous</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter">
|
||||
<label for="langue">Langue</label>
|
||||
<select id="langue" name="langue" class="select2">
|
||||
<option value="">Toutes</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter">
|
||||
<label>
|
||||
<input type="checkbox" id="permanences_non_assignees" name="permanences_non_assignees" value="1">
|
||||
Permanences non assignées uniquement
|
||||
</label>
|
||||
</div>
|
||||
<div class="filter" style="display: none;">
|
||||
<button type="button" id="resetFiltersBtn" class="btn btn-secondary">
|
||||
<i class="fas fa-times"></i> Réinitialiser
|
||||
</button>
|
||||
</div>
|
||||
<div class="filter">
|
||||
<button type="button" id="addEventBtn" class="btn btn-success">
|
||||
<i class="fas fa-plus"></i> Ajouter un événement
|
||||
</button>
|
||||
</div>
|
||||
<div class="filter">
|
||||
<button type="button" id="filterBtn" class="btn btn-primary">
|
||||
<i class="fas fa-filter"></i> Filtrer
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<!-- Calendrier Mon Agenda -->
|
||||
<section class="agenda-inner-container" id="agenda-calendar">
|
||||
<div id="loading-indicator" style="text-align: center; padding: 20px;">
|
||||
<p>Chargement du calendrier...</p>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Onglet Agenda des collègues -->
|
||||
<div class="tab-pane fade" id="colleagues-agenda" role="tabpanel" aria-labelledby="colleagues-agenda-tab">
|
||||
<section class="agenda-container">
|
||||
<!-- Filtres pour l'agenda des collègues -->
|
||||
<section class="filters-container">
|
||||
<form class="filters filters-colleagues" method="get" onsubmit="return false;">
|
||||
<div class="filter">
|
||||
<label for="local-colleagues">Local</label>
|
||||
<select id="local-colleagues" name="local" class="select2">
|
||||
<option value="">Tous</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter">
|
||||
<label for="beneficiaire-colleagues">Bénéficiaire</label>
|
||||
<select id="beneficiaire-colleagues" name="beneficiaire" class="select2">
|
||||
<option value="">Tous</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter">
|
||||
<label for="type_rdv-colleagues">Type de RDV</label>
|
||||
<select id="type_rdv-colleagues" name="type_rdv" class="select2">
|
||||
<option value="">Tous</option>
|
||||
<option value="individuel">Individuel</option>
|
||||
<option value="groupe">Groupe</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter">
|
||||
<label for="type_intervention-colleagues">Type d'intervention</label>
|
||||
<select id="type_intervention-colleagues" name="type_intervention" class="select2">
|
||||
<option value="">Tous</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter">
|
||||
<label for="langue-colleagues">Langue</label>
|
||||
<select id="langue-colleagues" name="langue" class="select2">
|
||||
<option value="">Toutes</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter" style="display: none;">
|
||||
<button type="button" id="resetFiltersBtn-colleagues" class="btn btn-secondary">
|
||||
<i class="fas fa-times"></i> Réinitialiser
|
||||
</button>
|
||||
</div>
|
||||
<div class="filter">
|
||||
<button type="button" id="filterBtn-colleagues" class="btn btn-primary">
|
||||
<i class="fas fa-filter"></i> Filtrer
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<!-- Calendrier Agenda des collègues -->
|
||||
<section class="agenda-inner-container" id="agenda-calendar-colleagues">
|
||||
<div id="loading-indicator-colleagues" style="text-align: center; padding: 20px;">
|
||||
<p>Chargement du calendrier des collègues...</p>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.crvi-intervenant-agenda {
|
||||
min-height: 60vh;
|
||||
}
|
||||
|
||||
#agenda-calendar {
|
||||
min-height: 600px;
|
||||
}
|
||||
|
||||
|
||||
.card {
|
||||
border: none;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.card-header.bg-light {
|
||||
background-color: #f8f9fa !important;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.col-md-3 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
#agenda-calendar {
|
||||
min-height: 400px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Le modal est injecté automatiquement dans le footer via wp_footer par Intervenant_Space_Controller::maybe_render_rdv_modal() -->
|
||||
|
||||
239
templates/frontend/intervenant-agenda.php
Normal file
239
templates/frontend/intervenant-agenda.php
Normal file
@ -0,0 +1,239 @@
|
||||
<?php
|
||||
/**
|
||||
* Template Agenda Intervenant
|
||||
* Affiche le calendrier complet avec filtres
|
||||
* Shortcode: [crvi_intervenant_agenda]
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
$user = wp_get_current_user();
|
||||
?>
|
||||
|
||||
<div class="crvi-intervenant-agenda" id="intervenant-agenda-container">
|
||||
<div class="container-fluid py-4">
|
||||
<!-- En-tête -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h1 class="h2 mb-2">
|
||||
<i class="fas fa-calendar-alt me-2"></i>Mon Agenda
|
||||
</h1>
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light rounded p-3">
|
||||
<div class="container-fluid">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="<?php echo esc_url(home_url('/espace-intervenant')); ?>">
|
||||
<i class="fas fa-home me-1"></i>Hub
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="<?php echo esc_url(home_url('/espace-intervenant-agenda')); ?>">
|
||||
<i class="fas fa-calendar-alt me-1"></i>Agenda
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="<?php echo esc_url(home_url('/espace-intervenant-profil')); ?>">
|
||||
<i class="fas fa-user me-1"></i>Mon Profil
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="<?php echo esc_url(home_url('/encodage-permanences')); ?>">
|
||||
<i class="fas fa-clock me-1"></i>Permanences
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Onglets pour basculer entre mon agenda et celui des collègues -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<ul class="nav nav-tabs" id="agendaTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="my-agenda-tab" data-bs-toggle="tab" data-bs-target="#my-agenda" type="button" role="tab" aria-controls="my-agenda" aria-selected="true">
|
||||
<i class="fas fa-calendar-check me-1"></i>Mon Agenda
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="colleagues-agenda-tab" data-bs-toggle="tab" data-bs-target="#colleagues-agenda" type="button" role="tab" aria-controls="colleagues-agenda" aria-selected="false">
|
||||
<i class="fas fa-users me-1"></i>Agenda de mes collègues
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contenu des onglets -->
|
||||
<div class="tab-content" id="agendaTabsContent">
|
||||
<!-- Onglet Mon Agenda -->
|
||||
<div class="tab-pane fade show active" id="my-agenda" role="tabpanel" aria-labelledby="my-agenda-tab">
|
||||
<section class="agenda-container">
|
||||
<!-- Filtres -->
|
||||
<section class="filters-container">
|
||||
<form class="filters" method="get" onsubmit="return false;">
|
||||
<div class="filter">
|
||||
<label for="local">Local</label>
|
||||
<select id="local" name="local" class="select2">
|
||||
<option value="">Tous</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter">
|
||||
<label for="beneficiaire">Bénéficiaire</label>
|
||||
<select id="beneficiaire" name="beneficiaire" class="select2">
|
||||
<option value="">Tous</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter">
|
||||
<label for="type_rdv">Type de RDV</label>
|
||||
<select id="type_rdv" name="type_rdv" class="select2">
|
||||
<option value="">Tous</option>
|
||||
<option value="individuel">Individuel</option>
|
||||
<option value="groupe">Groupe</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter">
|
||||
<label for="type_intervention">Type d'intervention</label>
|
||||
<select id="type_intervention" name="type_intervention" class="select2">
|
||||
<option value="">Tous</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter">
|
||||
<label for="langue">Langue</label>
|
||||
<select id="langue" name="langue" class="select2">
|
||||
<option value="">Toutes</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter">
|
||||
<label>
|
||||
<input type="checkbox" id="permanences_non_assignees" name="permanences_non_assignees" value="1">
|
||||
Permanences non assignées uniquement
|
||||
</label>
|
||||
</div>
|
||||
<div class="filter" style="display: none;">
|
||||
<button type="button" id="resetFiltersBtn" class="btn btn-secondary">
|
||||
<i class="fas fa-times"></i> Réinitialiser
|
||||
</button>
|
||||
</div>
|
||||
<div class="filter">
|
||||
<button type="button" id="addEventBtn" class="btn btn-success">
|
||||
<i class="fas fa-plus"></i> Ajouter un événement
|
||||
</button>
|
||||
</div>
|
||||
<div class="filter">
|
||||
<button type="button" id="filterBtn" class="btn btn-primary">
|
||||
<i class="fas fa-filter"></i> Filtrer
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<!-- Calendrier Mon Agenda -->
|
||||
<section class="agenda-inner-container" id="agenda-calendar">
|
||||
<div id="loading-indicator" style="text-align: center; padding: 20px;">
|
||||
<p>Chargement du calendrier...</p>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Onglet Agenda des collègues -->
|
||||
<div class="tab-pane fade" id="colleagues-agenda" role="tabpanel" aria-labelledby="colleagues-agenda-tab">
|
||||
<section class="agenda-container">
|
||||
<!-- Filtres pour l'agenda des collègues -->
|
||||
<section class="filters-container">
|
||||
<form class="filters filters-colleagues" method="get" onsubmit="return false;">
|
||||
<div class="filter">
|
||||
<label for="local-colleagues">Local</label>
|
||||
<select id="local-colleagues" name="local" class="select2">
|
||||
<option value="">Tous</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter">
|
||||
<label for="beneficiaire-colleagues">Bénéficiaire</label>
|
||||
<select id="beneficiaire-colleagues" name="beneficiaire" class="select2">
|
||||
<option value="">Tous</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter">
|
||||
<label for="type_rdv-colleagues">Type de RDV</label>
|
||||
<select id="type_rdv-colleagues" name="type_rdv" class="select2">
|
||||
<option value="">Tous</option>
|
||||
<option value="individuel">Individuel</option>
|
||||
<option value="groupe">Groupe</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter">
|
||||
<label for="type_intervention-colleagues">Type d'intervention</label>
|
||||
<select id="type_intervention-colleagues" name="type_intervention" class="select2">
|
||||
<option value="">Tous</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter">
|
||||
<label for="langue-colleagues">Langue</label>
|
||||
<select id="langue-colleagues" name="langue" class="select2">
|
||||
<option value="">Toutes</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter" style="display: none;">
|
||||
<button type="button" id="resetFiltersBtn-colleagues" class="btn btn-secondary">
|
||||
<i class="fas fa-times"></i> Réinitialiser
|
||||
</button>
|
||||
</div>
|
||||
<div class="filter">
|
||||
<button type="button" id="filterBtn-colleagues" class="btn btn-primary">
|
||||
<i class="fas fa-filter"></i> Filtrer
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<!-- Calendrier Agenda des collègues -->
|
||||
<section class="agenda-inner-container" id="agenda-calendar-colleagues">
|
||||
<div id="loading-indicator-colleagues" style="text-align: center; padding: 20px;">
|
||||
<p>Chargement du calendrier des collègues...</p>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.crvi-intervenant-agenda {
|
||||
min-height: 60vh;
|
||||
}
|
||||
|
||||
#agenda-calendar {
|
||||
min-height: 600px;
|
||||
}
|
||||
|
||||
|
||||
.card {
|
||||
border: none;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.card-header.bg-light {
|
||||
background-color: #f8f9fa !important;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.col-md-3 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
#agenda-calendar {
|
||||
min-height: 400px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Modal de création/édition d'événements -->
|
||||
<?php require_once dirname(__DIR__, 1) . '/modules/agenda-modal.php'; ?>
|
||||
|
||||
191
templates/frontend/intervenant-hub.php
Normal file
191
templates/frontend/intervenant-hub.php
Normal file
@ -0,0 +1,191 @@
|
||||
<?php
|
||||
/**
|
||||
* Template Hub Intervenant
|
||||
* Affiche le tableau de bord avec les RDV du jour et actions rapides
|
||||
* Shortcode: [crvi_intervenant_hub]
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
$user = wp_get_current_user();
|
||||
$intervenant_nom = trim($user->first_name . ' ' . $user->last_name);
|
||||
$today = date('d/m/Y');
|
||||
?>
|
||||
|
||||
<div class="crvi-intervenant-hub" id="intervenant-hub-container">
|
||||
<div class="container-fluid py-4">
|
||||
<!-- En-tête -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h1 class="h2 mb-2">
|
||||
<i class="fas fa-home me-2"></i>Mon Espace Intervenant
|
||||
</h1>
|
||||
<p class="text-muted">Bonjour <?php echo esc_html($intervenant_nom); ?></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section RDV du jour -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h3 class="h5 mb-0">
|
||||
<i class="fas fa-calendar-day me-2"></i>
|
||||
Mes rendez-vous aujourd'hui (<?php echo esc_html($today); ?>)
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="rdv-today-container" class="rdv-list">
|
||||
<div class="text-center py-4">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Chargement...</span>
|
||||
</div>
|
||||
<p class="mt-2 text-muted">Chargement des rendez-vous...</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="rdv-today-empty" class="alert alert-info" style="display: none;">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
Aucun rendez-vous prévu pour aujourd'hui.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions rapides -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6 mb-3">
|
||||
<a href="<?php echo esc_url(home_url('/espace-intervenant-agenda')); ?>" class="card card-hover text-decoration-none h-100">
|
||||
<div class="card-body text-center">
|
||||
<i class="fas fa-calendar-alt fa-3x text-primary mb-3"></i>
|
||||
<h4 class="h5">Voir mon agenda complet</h4>
|
||||
<p class="text-muted mb-0">Consulter tous mes rendez-vous</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<a href="<?php echo esc_url(home_url('/espace-intervenant-profil')); ?>" class="card card-hover text-decoration-none h-100">
|
||||
<div class="card-body text-center">
|
||||
<i class="fas fa-user-cog fa-3x text-secondary mb-3"></i>
|
||||
<h4 class="h5">Modifier mes informations</h4>
|
||||
<p class="text-muted mb-0">Gérer mon profil</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section Permanences -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-light">
|
||||
<h3 class="h5 mb-0">
|
||||
<i class="fas fa-clock me-2"></i>Mes Permanences
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted mb-3">Gérez vos créneaux de permanence disponibles.</p>
|
||||
<a href="<?php echo esc_url(home_url('/encodage-permanences')); ?>" class="btn btn-outline-primary">
|
||||
<i class="fas fa-plus me-2"></i>Encoder mes permanences
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Inclusion du modal de détails RDV (si nécessaire) -->
|
||||
<?php include plugin_dir_path(__FILE__) . 'intervenant-modal-rdv.php'; ?>
|
||||
|
||||
<!-- Template pour un RDV (utilisé en JavaScript) -->
|
||||
<template id="rdv-item-template">
|
||||
<div class="rdv-item card mb-3">
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-2">
|
||||
<div class="rdv-time text-center">
|
||||
<strong class="d-block fs-5" data-time-start></strong>
|
||||
<span class="text-muted small" data-time-end></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<p class="mb-1">
|
||||
<i class="fas fa-map-marker-alt text-primary me-2"></i>
|
||||
<strong data-local></strong>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<p class="mb-1">
|
||||
<i class="fas fa-user text-info me-2"></i>
|
||||
<span data-beneficiaire></span>
|
||||
</p>
|
||||
<p class="mb-0 small text-muted" data-type-intervention></p>
|
||||
</div>
|
||||
<div class="col-md-3 text-end">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-success btn-sm mark-presence" data-statut="present" data-event-id>
|
||||
<i class="fas fa-check me-1"></i>Présent
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger btn-sm mark-presence" data-statut="absent" data-event-id>
|
||||
<i class="fas fa-times me-1"></i>Absent
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.crvi-intervenant-hub {
|
||||
min-height: 60vh;
|
||||
}
|
||||
|
||||
.card-hover {
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.card-hover:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
|
||||
}
|
||||
|
||||
.rdv-item {
|
||||
border-left: 4px solid #0d6efd;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.rdv-item:hover {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.rdv-time strong {
|
||||
color: #0d6efd;
|
||||
}
|
||||
|
||||
.btn-group .btn {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.rdv-item .row > div {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.rdv-item .text-end {
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-group .btn {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
175
templates/frontend/intervenant-modal-rdv.php
Normal file
175
templates/frontend/intervenant-modal-rdv.php
Normal file
@ -0,0 +1,175 @@
|
||||
<?php
|
||||
/**
|
||||
* Modal de détails RDV pour l'espace intervenant
|
||||
* Affiche les détails d'un rendez-vous (lecture seule)
|
||||
* Les commentaires ne sont pas affichés pour les RDV des collègues
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
|
||||
<!-- Modal Détails RDV -->
|
||||
<div class="modal fade" id="rdvModal" tabindex="-1" aria-labelledby="rdvModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="rdvModalLabel">
|
||||
<i class="fas fa-calendar-check me-2"></i>Détails du rendez-vous
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Fermer"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<!-- Colonne gauche -->
|
||||
<div class="col-md-6">
|
||||
<!-- Date et heure -->
|
||||
<div class="mb-3">
|
||||
<p class="mb-1 text-muted small">
|
||||
<i class="fas fa-calendar me-2"></i>Date
|
||||
</p>
|
||||
<p class="fw-bold" id="modal-date-rdv">-</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<p class="mb-1 text-muted small">
|
||||
<i class="fas fa-clock me-2"></i>Heure
|
||||
</p>
|
||||
<p class="fw-bold" id="modal-heure-rdv">-</p>
|
||||
</div>
|
||||
|
||||
<!-- Type de RDV -->
|
||||
<div class="mb-3">
|
||||
<p class="mb-1 text-muted small">
|
||||
<i class="fas fa-users me-2"></i>Type
|
||||
</p>
|
||||
<p class="fw-bold" id="modal-type">-</p>
|
||||
</div>
|
||||
|
||||
<!-- Langue -->
|
||||
<div class="mb-3">
|
||||
<p class="mb-1 text-muted small">
|
||||
<i class="fas fa-language me-2"></i>Langue
|
||||
</p>
|
||||
<p class="fw-bold" id="modal-langue">-</p>
|
||||
</div>
|
||||
|
||||
<!-- Bénéficiaire -->
|
||||
<div class="mb-3">
|
||||
<p class="mb-1 text-muted small">
|
||||
<i class="fas fa-user me-2"></i>Bénéficiaire
|
||||
</p>
|
||||
<p class="fw-bold" id="modal-beneficiaire">-</p>
|
||||
</div>
|
||||
|
||||
<!-- Intervenant -->
|
||||
<div class="mb-3">
|
||||
<p class="mb-1 text-muted small">
|
||||
<i class="fas fa-user-md me-2"></i>Intervenant
|
||||
</p>
|
||||
<p class="fw-bold" id="modal-intervenant">-</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Colonne droite -->
|
||||
<div class="col-md-6">
|
||||
<!-- Local -->
|
||||
<div class="mb-3">
|
||||
<p class="mb-1 text-muted small">
|
||||
<i class="fas fa-map-marker-alt me-2"></i>Local
|
||||
</p>
|
||||
<p class="fw-bold" id="modal-local">-</p>
|
||||
</div>
|
||||
|
||||
<!-- Traducteur -->
|
||||
<div class="mb-3">
|
||||
<p class="mb-1 text-muted small">
|
||||
<i class="fas fa-language me-2"></i>Traducteur
|
||||
</p>
|
||||
<p class="fw-bold" id="modal-traducteur">-</p>
|
||||
</div>
|
||||
|
||||
<!-- Département -->
|
||||
<div class="mb-3">
|
||||
<p class="mb-1 text-muted small">
|
||||
<i class="fas fa-building me-2"></i>Département
|
||||
</p>
|
||||
<p class="fw-bold" id="modal-departement">-</p>
|
||||
</div>
|
||||
|
||||
<!-- Type d'intervention -->
|
||||
<div class="mb-3">
|
||||
<p class="mb-1 text-muted small">
|
||||
<i class="fas fa-tools me-2"></i>Type d'intervention
|
||||
</p>
|
||||
<p class="fw-bold" id="modal-type-intervention">-</p>
|
||||
</div>
|
||||
|
||||
<!-- Statut -->
|
||||
<div class="mb-3">
|
||||
<p class="mb-1 text-muted small">
|
||||
<i class="fas fa-info-circle me-2"></i>Statut
|
||||
</p>
|
||||
<p>
|
||||
<span class="badge" id="modal-statut-badge">-</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Commentaire (seulement si is_mine = true) -->
|
||||
<div class="mb-3" id="modal-commentaire-container" style="display: none;">
|
||||
<p class="mb-1 text-muted small">
|
||||
<i class="fas fa-comment me-2"></i>Commentaire
|
||||
</p>
|
||||
<p class="fw-normal" id="modal-commentaire">-</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" id="rdv-edit-btn" style="display: none;">
|
||||
<i class="fas fa-edit me-1"></i>Modifier
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
<i class="fas fa-times me-1"></i>Fermer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Forcer la modale RDV à passer au-dessus des éléments Avada (toTop, cadres, etc.) */
|
||||
/* #rdvModal.modal {
|
||||
z-index: 200001 !important;
|
||||
}
|
||||
|
||||
.modal-backdrop {
|
||||
z-index: 200000 !important;
|
||||
} */
|
||||
|
||||
#rdvModal .modal-body .text-muted {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
#rdvModal .modal-body .fw-bold {
|
||||
color: #212529;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
#rdvModal .badge {
|
||||
font-size: 0.875rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.modal-dialog.modal-lg {
|
||||
max-width: 95%;
|
||||
}
|
||||
|
||||
.col-md-6 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
552
templates/frontend/intervenant-permanences.php
Normal file
552
templates/frontend/intervenant-permanences.php
Normal file
@ -0,0 +1,552 @@
|
||||
<?php
|
||||
/**
|
||||
* Template Permanences Intervenant
|
||||
* Permet à l'intervenant d'encoder ses permanences
|
||||
* Shortcode: [crvi_intervenant_permanences]
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
$langues[] = [
|
||||
'id' => $term->slug,
|
||||
'nom' => $term->name,
|
||||
];
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="crvi-intervenant-permanences" id="intervenant-permanences-container">
|
||||
<div class="container-fluid py-4">
|
||||
<!-- En-tête -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h1 class="h2 mb-2">
|
||||
<i class="fas fa-clock me-2"></i>Mes Permanences
|
||||
</h1>
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light rounded p-3">
|
||||
<div class="container-fluid">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="<?php echo esc_url(home_url('/espace-intervenant')); ?>">
|
||||
<i class="fas fa-home me-1"></i>Hub
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link"
|
||||
href="<?php echo esc_url(home_url('/espace-intervenant-agenda')); ?>">
|
||||
<i class="fas fa-calendar-alt me-1"></i>Agenda
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link"
|
||||
href="<?php echo esc_url(home_url('/espace-intervenant-profil')); ?>">
|
||||
<i class="fas fa-user me-1"></i>Mon Profil
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active"
|
||||
href="<?php echo esc_url(home_url('/encodage-permanences')); ?>">
|
||||
<i class="fas fa-clock me-1"></i>Permanences
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<!-- Navigation par onglets -->
|
||||
<ul class="nav nav-tabs mb-4" id="permanences-tabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="creation-manuelle-tab" data-bs-toggle="tab"
|
||||
data-bs-target="#creation-manuelle" type="button" role="tab"
|
||||
aria-controls="creation-manuelle" aria-selected="true">
|
||||
<i class="fas fa-plus-circle me-2"></i>Création manuelle
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="import-csv-tab" data-bs-toggle="tab" data-bs-target="#import-csv"
|
||||
type="button" role="tab" aria-controls="import-csv" aria-selected="false">
|
||||
<i class="fas fa-file-csv me-2"></i>Import CSV
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- Contenu des onglets -->
|
||||
<div class="tab-content" id="permanences-tab-content">
|
||||
<!-- Onglet 1 : Création manuelle -->
|
||||
<div class="tab-pane show active" id="creation-manuelle" role="tabpanel"
|
||||
aria-labelledby="creation-manuelle-tab">
|
||||
<form id="permanences-form">
|
||||
<!-- Section 1 : Sélection des permanences -->
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h3 class="h5 mb-0">
|
||||
<i class="fas fa-calendar-check me-2"></i>SECTION 1 : Sélection des permanences
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- Mois de début -->
|
||||
<div class="mb-4">
|
||||
<label for="mois-debut" class="form-label fw-bold">Mois de début :</label>
|
||||
<input type="month" class="form-control form-control-lg" id="mois-debut"
|
||||
name="mois_debut" required>
|
||||
<small class="form-text text-muted">
|
||||
Sélectionnez le mois et l'année à partir desquels les permanences seront
|
||||
créées.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<!-- Période -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label fw-bold">Période :</label>
|
||||
<div class="btn-group" role="group" aria-label="Période">
|
||||
<input type="radio" class="btn-check" name="periode" id="periode-3"
|
||||
value="3" checked>
|
||||
<label class="btn btn-outline-primary" for="periode-3">3 mois</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="periode" id="periode-6"
|
||||
value="6">
|
||||
<label class="btn btn-outline-primary" for="periode-6">6 mois</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Jours de la semaine -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label fw-bold">Jours de la semaine :</label>
|
||||
<div class="row g-2">
|
||||
<div class="col-md-3 col-sm-4 col-6">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="jours[]"
|
||||
value="lundi" id="jour-lundi">
|
||||
<label class="form-check-label" for="jour-lundi">Lundi</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-4 col-6">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="jours[]"
|
||||
value="mardi" id="jour-mardi">
|
||||
<label class="form-check-label" for="jour-mardi">Mardi</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-4 col-6">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="jours[]"
|
||||
value="mercredi" id="jour-mercredi">
|
||||
<label class="form-check-label" for="jour-mercredi">Mercredi</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-4 col-6">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="jours[]"
|
||||
value="jeudi" id="jour-jeudi">
|
||||
<label class="form-check-label" for="jour-jeudi">Jeudi</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-4 col-6">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="jours[]"
|
||||
value="vendredi" id="jour-vendredi">
|
||||
<label class="form-check-label" for="jour-vendredi">Vendredi</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-4 col-6">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="jours[]"
|
||||
value="samedi" id="jour-samedi">
|
||||
<label class="form-check-label" for="jour-samedi">Samedi</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-4 col-6">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="jours[]"
|
||||
value="dimanche" id="jour-dimanche">
|
||||
<label class="form-check-label" for="jour-dimanche">Dimanche</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Durée de permanence -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label fw-bold">Durée de permanence :</label>
|
||||
<div class="btn-group mb-3" role="group" aria-label="Durée">
|
||||
<input type="radio" class="btn-check" name="duree_permanence" id="duree-1h"
|
||||
value="1h" checked>
|
||||
<label class="btn btn-outline-primary" for="duree-1h">1 heure</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="duree_permanence" id="duree-15min"
|
||||
value="15min">
|
||||
<label class="btn btn-outline-primary" for="duree-15min">1/4 d'heure (15 min)</label>
|
||||
</div>
|
||||
<small class="form-text text-muted d-block mb-3">
|
||||
Choisissez la durée de chaque permanence.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<!-- Nombre de tranches (si 1/4 d'heure) -->
|
||||
<div class="mb-4" id="nb-tranches-container" style="display: none;">
|
||||
<label for="nb-tranches" class="form-label fw-bold">Nombre de tranches par heure :</label>
|
||||
<select class="form-select form-select-lg" id="nb-tranches" name="nb_tranches">
|
||||
<option value="1">1 tranche (ex: 11h00 → 11h15)</option>
|
||||
<option value="2">2 tranches (ex: 11h00 → 11h15, 11h15 → 11h30)</option>
|
||||
<option value="3">3 tranches (ex: 11h00 → 11h15, 11h15 → 11h30, 11h30 → 11h45)</option>
|
||||
<option value="4">4 tranches (ex: 11h00 → 11h15, 11h15 → 11h30, 11h30 → 11h45, 11h45 → 12h00)</option>
|
||||
</select>
|
||||
<small class="form-text text-muted">
|
||||
Si vous choisissez 1/4 d'heure, indiquez combien de tranches de 15 minutes créer pour chaque heure sélectionnée.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<!-- Heures de permanence -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label fw-bold">Heures de permanence :</label>
|
||||
<small class="form-text text-muted d-block mb-3" id="heures-description">
|
||||
Sélectionnez les heures de début. Chaque heure sélectionnée créera une
|
||||
tranche d'1 heure (ex: 09:00 → 10:00).
|
||||
</small>
|
||||
<div class="row g-2">
|
||||
<div class="col-12">
|
||||
<label class="form-label fw-semibold text-muted">Matin :</label>
|
||||
</div>
|
||||
<?php
|
||||
// Plage matin : 09:00 à 12:00
|
||||
for ($h = 9; $h <= 12; $h++):
|
||||
$heure = str_pad($h, 2, '0', STR_PAD_LEFT) . ':00';
|
||||
$heureFin = str_pad($h + 1, 2, '0', STR_PAD_LEFT) . ':00';
|
||||
?>
|
||||
<div class="col-md-3 col-sm-4 col-6">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="heures[]"
|
||||
value="<?php echo esc_attr($heure); ?>"
|
||||
id="heure-<?php echo $h; ?>">
|
||||
<label class="form-check-label" for="heure-<?php echo $h; ?>">
|
||||
<?php echo $heure; ?> → <?php echo $heureFin; ?>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<?php endfor; ?>
|
||||
|
||||
<div class="col-12 mt-2">
|
||||
<label class="form-label fw-semibold text-muted">Après-midi :</label>
|
||||
</div>
|
||||
<?php
|
||||
// Plage après-midi : 13:00 à 16:00
|
||||
for ($h = 13; $h <= 16; $h++):
|
||||
$heure = str_pad($h, 2, '0', STR_PAD_LEFT) . ':00';
|
||||
$heureFin = str_pad($h + 1, 2, '0', STR_PAD_LEFT) . ':00';
|
||||
?>
|
||||
<div class="col-md-3 col-sm-4 col-6">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="heures[]"
|
||||
value="<?php echo esc_attr($heure); ?>"
|
||||
id="heure-<?php echo $h; ?>">
|
||||
<label class="form-check-label" for="heure-<?php echo $h; ?>">
|
||||
<?php echo $heure; ?> → <?php echo $heureFin; ?>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<?php endfor; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Aperçu des tranches générées -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">Aperçu des tranches générées :</label>
|
||||
<div id="tranches-preview" class="alert alert-info">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
Veuillez sélectionner une plage horaire pour voir l'aperçu.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Estimation -->
|
||||
<div class="mb-3">
|
||||
<div id="estimation-container" class="alert alert-warning"
|
||||
style="display: none;">
|
||||
<i class="fas fa-calculator me-2"></i>
|
||||
<strong>Estimation :</strong> <span id="estimation-count">0</span>
|
||||
événements seront créés
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 2 : Langues -->
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header bg-info text-white">
|
||||
<h3 class="h5 mb-0">
|
||||
<i class="fas fa-language me-2"></i>SECTION 2 : Langues des permanences
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label for="langues-permanences" class="form-label fw-bold">Langues disponibles (optionnel) :</label>
|
||||
<select class="form-select form-select-lg" id="langues-permanences" name="langues[]" multiple>
|
||||
<?php if (!empty($langues)): ?>
|
||||
<?php foreach ($langues as $langue): ?>
|
||||
<option value="<?php echo esc_attr($langue['id']); ?>">
|
||||
<?php echo esc_html($langue['nom']); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<option value="">Aucune langue disponible</option>
|
||||
<?php endif; ?>
|
||||
</select>
|
||||
<small class="form-text text-muted">
|
||||
Sélectionnez une ou plusieurs langues pour ces permanences.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section 3 : Informations complémentaires -->
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header bg-secondary text-white">
|
||||
<h3 class="h5 mb-0">
|
||||
<i class="fas fa-sticky-note me-2"></i>SECTION 3 : Informations complémentaires
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label for="informations-complementaires" class="form-label">Notes ou
|
||||
commentaires (optionnel)</label>
|
||||
<textarea class="form-control" id="informations-complementaires"
|
||||
name="informations_complementaires" rows="3"
|
||||
placeholder="Ajoutez des notes ou commentaires sur ces permanences..."></textarea>
|
||||
<small class="form-text text-muted">
|
||||
Ces informations seront associées aux permanences créées.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bouton de soumission -->
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body text-center">
|
||||
<button type="submit" class="btn btn-primary btn-lg" id="submit-permanences-btn">
|
||||
<i class="fas fa-save me-2"></i>Enregistrer mes permanences
|
||||
</button>
|
||||
<button type="reset" class="btn btn-outline-secondary btn-lg ms-2"
|
||||
id="reset-permanences-btn">
|
||||
<i class="fas fa-redo me-2"></i>Réinitialiser
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Onglet 2 : Import CSV -->
|
||||
<div class="tab-pane" id="import-csv" role="tabpanel" aria-labelledby="import-csv-tab">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-info text-white">
|
||||
<h3 class="h5 mb-0">
|
||||
<i class="fas fa-file-csv me-2"></i>Import de permanences via CSV
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-info mb-4">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
<strong>Format CSV attendu :</strong> Le fichier CSV doit contenir les colonnes
|
||||
suivantes :
|
||||
<ul class="mt-2 mb-0">
|
||||
<li><code>date_debut</code> : Date de début (format YYYY-MM-DD)</li>
|
||||
<li><code>date_fin</code> : Date de fin (format YYYY-MM-DD)</li>
|
||||
<li><code>heure_debut</code> : Heure de début (format HH:MM)</li>
|
||||
<li><code>heure_fin</code> : Heure de fin (format HH:MM)</li>
|
||||
<li><code>informations_complementaires</code> : Notes (optionnel)</li>
|
||||
</ul>
|
||||
<p class="mt-2 mb-0"><small><em>Note : Les permanences seront créées pour votre
|
||||
compte.</em></small></p>
|
||||
</div>
|
||||
|
||||
<form id="import-csv-form" enctype="multipart/form-data">
|
||||
<div class="mb-3">
|
||||
<label for="csv-file" class="form-label fw-bold">Sélectionner un fichier CSV
|
||||
:</label>
|
||||
<input type="file" class="form-control form-control-lg" id="csv-file"
|
||||
name="csv_file" accept=".csv" required>
|
||||
<small class="form-text text-muted">
|
||||
Format accepté : CSV (séparateur virgule)
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-info btn-lg" id="submit-csv-import-btn">
|
||||
<i class="fas fa-upload me-2"></i>Importer le CSV
|
||||
</button>
|
||||
<button type="reset" class="btn btn-outline-secondary btn-lg"
|
||||
id="reset-csv-import-btn">
|
||||
<i class="fas fa-redo me-2"></i>Réinitialiser
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div id="csv-import-result" class="mt-4" style="display: none;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.crvi-intervenant-permanences {
|
||||
min-height: 60vh;
|
||||
}
|
||||
|
||||
/* Augmentation de la largeur de la page */
|
||||
.crvi-intervenant-permanences .container-fluid {
|
||||
max-width: 100%;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.btn-group .btn-check:checked+.btn {
|
||||
background-color: #0d6efd;
|
||||
border-color: #0d6efd;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.form-check-input:checked {
|
||||
background-color: #0d6efd;
|
||||
border-color: #0d6efd;
|
||||
}
|
||||
|
||||
#tranches-preview {
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
#tranches-preview ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
#tranches-preview li {
|
||||
background-color: #e7f3ff;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #b3d9ff;
|
||||
}
|
||||
|
||||
.alert {
|
||||
border-left: 4px solid;
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
border-left-color: #0dcaf0;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
border-left-color: #ffc107;
|
||||
}
|
||||
|
||||
/* Styles pour les onglets */
|
||||
.nav-tabs {
|
||||
border-bottom: 2px solid #dee2e6;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link {
|
||||
color: #495057;
|
||||
border: none;
|
||||
border-bottom: 3px solid transparent;
|
||||
padding: 0.75rem 1.5rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link:hover {
|
||||
border-color: #dee2e6;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link.active {
|
||||
color: #0d6efd;
|
||||
background-color: transparent;
|
||||
border-color: #0d6efd;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
padding-top: 1.5rem;
|
||||
}
|
||||
|
||||
/* Amélioration de l'affichage des résultats d'import */
|
||||
#csv-import-result {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
#csv-import-result .alert {
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.col-lg-10 {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.ms-2 {
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// Gérer l'affichage conditionnel du champ nombre de tranches
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const duree1h = document.getElementById('duree-1h');
|
||||
const duree15min = document.getElementById('duree-15min');
|
||||
const nbTranchesContainer = document.getElementById('nb-tranches-container');
|
||||
const heuresDescription = document.getElementById('heures-description');
|
||||
|
||||
function updateDescription() {
|
||||
if (duree15min && duree15min.checked) {
|
||||
const nbTranches = document.getElementById('nb-tranches')?.value || '1';
|
||||
heuresDescription.textContent = `Sélectionnez les heures de début. Pour chaque heure sélectionnée, ${nbTranches} tranche(s) de 15 minutes seront créées (ex: 11h00 → 11h15).`;
|
||||
} else {
|
||||
heuresDescription.textContent = `Sélectionnez les heures de début. Chaque heure sélectionnée créera une tranche d'1 heure (ex: 09:00 → 10:00).`;
|
||||
}
|
||||
}
|
||||
|
||||
if (duree1h && duree15min && nbTranchesContainer) {
|
||||
duree1h.addEventListener('change', function() {
|
||||
nbTranchesContainer.style.display = 'none';
|
||||
updateDescription();
|
||||
});
|
||||
|
||||
duree15min.addEventListener('change', function() {
|
||||
nbTranchesContainer.style.display = 'block';
|
||||
updateDescription();
|
||||
});
|
||||
|
||||
const nbTranches = document.getElementById('nb-tranches');
|
||||
if (nbTranches) {
|
||||
nbTranches.addEventListener('change', updateDescription);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
403
templates/frontend/intervenant-profile.php
Normal file
403
templates/frontend/intervenant-profile.php
Normal file
@ -0,0 +1,403 @@
|
||||
<?php
|
||||
/**
|
||||
* Template Profil Intervenant
|
||||
* Affiche le formulaire de gestion du compte et des informations personnelles
|
||||
* Shortcode: [crvi_intervenant_profil]
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
$user = wp_get_current_user();
|
||||
$user_id = $user->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'
|
||||
];
|
||||
?>
|
||||
|
||||
<div class="crvi-intervenant-profile" id="intervenant-profile-container">
|
||||
<div class="container-fluid py-4">
|
||||
<!-- En-tête -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h1 class="h2 mb-2">
|
||||
<i class="fas fa-user me-2"></i>Mon Profil
|
||||
</h1>
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light rounded p-3">
|
||||
<div class="container-fluid">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="<?php echo esc_url(home_url('/espace-intervenant')); ?>">
|
||||
<i class="fas fa-home me-1"></i>Hub
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="<?php echo esc_url(home_url('/espace-intervenant-agenda')); ?>">
|
||||
<i class="fas fa-calendar-alt me-1"></i>Agenda
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="<?php echo esc_url(home_url('/espace-intervenant-profil')); ?>">
|
||||
<i class="fas fa-user me-1"></i>Mon Profil
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="<?php echo esc_url(home_url('/encodage-permanences')); ?>">
|
||||
<i class="fas fa-clock me-1"></i>Permanences
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Afficher les messages de conflit d'indisponibilités -->
|
||||
<?php
|
||||
if (class_exists('ESI_CRVI_AGENDA\controllers\CRVI_Plugin')) {
|
||||
\ESI_CRVI_AGENDA\controllers\CRVI_Plugin::display_intervenant_conflicts_frontend();
|
||||
}
|
||||
?>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8 offset-lg-2">
|
||||
<!-- Section Informations modifiables -->
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h3 class="h5 mb-0">
|
||||
<i class="fas fa-edit me-2"></i>Mes informations personnelles
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="profile-form">
|
||||
<!-- Informations non modifiables (affichage seul) -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-muted">Nom</label>
|
||||
<input type="text" class="form-control" id="profile-nom" value="<?php echo esc_attr($user->last_name); ?>" readonly>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-muted">Prénom</label>
|
||||
<input type="text" class="form-control" id="profile-prenom" value="<?php echo esc_attr($user->first_name); ?>" readonly>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-muted">Email</label>
|
||||
<input type="email" class="form-control" id="profile-email" value="<?php echo esc_attr($user->user_email); ?>" readonly>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Téléphone (modifiable) -->
|
||||
<div class="mb-3">
|
||||
<label for="profile-telephone" class="form-label">
|
||||
Téléphone <span class="text-danger">*</span>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<input type="tel" class="form-control" id="profile-telephone" name="telephone" value="<?php echo esc_attr($telephone); ?>" required>
|
||||
<button class="btn btn-outline-secondary" type="button" id="edit-telephone-btn">
|
||||
<i class="fas fa-edit"></i> Modifier
|
||||
</button>
|
||||
</div>
|
||||
<small class="form-text text-muted">Vous pouvez modifier votre numéro de téléphone</small>
|
||||
</div>
|
||||
|
||||
<!-- Boutons d'action -->
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save me-2"></i>Enregistrer les modifications
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" id="cancel-profile-btn">
|
||||
Annuler
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section Disponibilités -->
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h3 class="h5 mb-0">
|
||||
<i class="fas fa-calendar-check me-2"></i>Mes disponibilités
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="disponibilites-form">
|
||||
<!-- Jours de disponibilité (checkboxes) -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label fw-bold">Jours de disponibilité</label>
|
||||
<div id="profile-jours-disponibilite-checkboxes" class="row g-3">
|
||||
<?php foreach ($jours_labels as $jour_value => $jour_label) :
|
||||
$is_checked = in_array($jour_value, $jours_disponibilite, true);
|
||||
?>
|
||||
<div class="col-md-3 col-sm-4 col-6">
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
value="<?php echo esc_attr($jour_value); ?>"
|
||||
id="jour-<?php echo esc_attr($jour_value); ?>"
|
||||
name="jours_disponibilite[]"
|
||||
<?php echo $is_checked ? 'checked' : ''; ?>
|
||||
>
|
||||
<label class="form-check-label" for="jour-<?php echo esc_attr($jour_value); ?>">
|
||||
<?php echo esc_html($jour_label); ?>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<small class="form-text text-muted">Cochez les jours où vous êtes disponible</small>
|
||||
</div>
|
||||
|
||||
<!-- Heures de permanences (checkboxes) -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label fw-bold">Tranches horaires</label>
|
||||
<div id="profile-heures-permanences-checkboxes" class="row g-3">
|
||||
<?php
|
||||
$heures_choices = [
|
||||
'09:00' => '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);
|
||||
?>
|
||||
<div class="col-md-3 col-sm-4 col-6">
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
value="<?php echo esc_attr($hval); ?>"
|
||||
id="heure-<?php echo esc_attr($hval); ?>"
|
||||
name="heures_permanences[]"
|
||||
<?php echo $is_checked ? 'checked' : ''; ?>
|
||||
>
|
||||
<label class="form-check-label" for="heure-<?php echo esc_attr($hval); ?>">
|
||||
<?php echo esc_html($hlabel); ?>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<small class="form-text text-muted">Cochez les créneaux horaires où vous êtes disponible</small>
|
||||
</div>
|
||||
|
||||
<!-- Indisponibilités ponctuelles -->
|
||||
<div class="mb-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<label class="form-label fw-bold mb-0">Mes indisponibilités ponctuelles</label>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" id="add-indisponibilite-btn">
|
||||
<i class="fas fa-plus me-1"></i>Ajouter une indisponibilité
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Liste des indisponibilités existantes -->
|
||||
<div id="profile-indisponibilites-list" class="indisponibilites-list mb-3">
|
||||
<?php if (!empty($indisponibilites_ponctuelles)) : ?>
|
||||
<?php foreach ($indisponibilites_ponctuelles as $index => $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;
|
||||
}
|
||||
?>
|
||||
<div class="indisponibilite-item indispo-<?php echo esc_attr($type); ?>" data-index="<?php echo esc_attr($index); ?>">
|
||||
<div>
|
||||
<strong><?php echo esc_html($periode_text); ?></strong>
|
||||
<?php
|
||||
$badge_class = 'badge-indispo-unknown';
|
||||
if ($type === 'conge') $badge_class = 'badge-indispo-conge';
|
||||
elseif ($type === 'absence') $badge_class = 'badge-indispo-absence';
|
||||
elseif ($type === 'maladie') $badge_class = 'badge-indispo-maladie';
|
||||
?>
|
||||
<span class="badge <?php echo esc_attr($badge_class); ?> ms-2"><?php echo esc_html($type_label); ?></span>
|
||||
<?php if ($commentaire) : ?>
|
||||
<p class="mb-0 text-muted mt-1"><?php echo esc_html($commentaire); ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<button type="button" class="btn-remove" onclick="removeIndisponibilite(<?php echo esc_attr($index); ?>)" title="Supprimer">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php else : ?>
|
||||
<div id="profile-indisponibilites-empty" class="alert alert-info">
|
||||
<i class="fas fa-info-circle me-2"></i>Aucune indisponibilité prévue.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Zone pour ajouter une nouvelle indisponibilité -->
|
||||
<div id="indisponibilite-form-container" style="display: none;">
|
||||
<div class="card border-primary">
|
||||
<div class="card-header bg-light">
|
||||
<h6 class="mb-0">Nouvelle indisponibilité</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label for="indispo-debut" class="form-label">Date de début <span class="text-danger">*</span></label>
|
||||
<input type="date" class="form-control" id="indispo-debut">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="indispo-fin" class="form-label">Date de fin <span class="text-danger">*</span></label>
|
||||
<input type="date" class="form-control" id="indispo-fin">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="indispo-type" class="form-label">Type <span class="text-danger">*</span></label>
|
||||
<select class="form-select" id="indispo-type">
|
||||
<option value="">Sélectionnez un type</option>
|
||||
<option value="conge">Congé</option>
|
||||
<option value="absence">Absence</option>
|
||||
<option value="maladie">Maladie</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label for="indispo-commentaire" class="form-label">Commentaire</label>
|
||||
<textarea class="form-control" id="indispo-commentaire" rows="2" placeholder="Optionnel"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 d-flex gap-2">
|
||||
<button type="button" class="btn btn-primary btn-sm" id="save-indisponibilite-btn">
|
||||
<i class="fas fa-save me-1"></i>Enregistrer
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" id="cancel-indisponibilite-btn">
|
||||
Annuler
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bouton de sauvegarde -->
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save me-2"></i>Enregistrer les disponibilités
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section Départements et spécialisations (consultation seule) -->
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-light">
|
||||
<h3 class="h5 mb-0">
|
||||
<i class="fas fa-building me-2"></i>Départements et spécialisations
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Départements</label>
|
||||
<div id="profile-departements" class="badge-container">
|
||||
<!-- Sera rempli par JavaScript -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Spécialisations</label>
|
||||
<div id="profile-specialisations" class="badge-container">
|
||||
<!-- Sera rempli par JavaScript -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<small class="text-muted">
|
||||
<i class="fas fa-lock me-1"></i>
|
||||
Ces informations sont gérées par l'administrateur.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
45
templates/modules/agenda-modal.php
Normal file
45
templates/modules/agenda-modal.php
Normal file
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
// Template principal qui inclut tous les modales de l'agenda
|
||||
// Contexte : "admin" (par défaut) ou "front_intervenant" (défini côté front).
|
||||
/** @var string|null $crvi_agenda_context */
|
||||
$crvi_agenda_context = isset($crvi_agenda_context) ? $crvi_agenda_context : 'admin';
|
||||
$crvi_is_front_context = ($crvi_agenda_context !== 'admin');
|
||||
|
||||
// Définir le chemin de base pour les includes
|
||||
$modals_dir = dirname(__FILE__) . '/modals';
|
||||
?>
|
||||
|
||||
<?php
|
||||
// Modal principal d'événement
|
||||
include $modals_dir . '/event-modal.php';
|
||||
?>
|
||||
|
||||
<?php
|
||||
// Modal de création de bénéficiaire
|
||||
include $modals_dir . '/create-beneficiaire-modal.php';
|
||||
?>
|
||||
|
||||
<?php
|
||||
// Modal de création d'intervenant
|
||||
include $modals_dir . '/create-intervenant-modal.php';
|
||||
?>
|
||||
|
||||
<?php
|
||||
// Modal de création de traducteur
|
||||
include $modals_dir . '/create-traducteur-modal.php';
|
||||
?>
|
||||
|
||||
<?php
|
||||
// Modal de création de local
|
||||
include $modals_dir . '/create-local-modal.php';
|
||||
?>
|
||||
|
||||
<?php
|
||||
// Modal de déclaration d'incident
|
||||
include $modals_dir . '/declaration-incident-modal.php';
|
||||
?>
|
||||
|
||||
<?php
|
||||
// Modal d'historique du bénéficiaire
|
||||
include $modals_dir . '/beneficiaire-historique-modal.php';
|
||||
?>
|
||||
346
templates/modules/intervenant-profile.php
Normal file
346
templates/modules/intervenant-profile.php
Normal file
@ -0,0 +1,346 @@
|
||||
<?php
|
||||
/**
|
||||
* Template Profil Intervenant
|
||||
* Affiche le formulaire de gestion du compte et des informations personnelles
|
||||
* Shortcode: [crvi_intervenant_profil]
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
$user = wp_get_current_user();
|
||||
$user_id = $user->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'
|
||||
];
|
||||
?>
|
||||
|
||||
<div class="crvi-intervenant-profile" id="intervenant-profile-container">
|
||||
<div class="container-fluid py-4">
|
||||
<!-- En-tête -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h1 class="h2 mb-2">
|
||||
<i class="fas fa-user me-2"></i>Mon Profil
|
||||
</h1>
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-light rounded p-3">
|
||||
<div class="container-fluid">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="<?php echo esc_url(home_url('/espace-intervenant')); ?>">
|
||||
<i class="fas fa-home me-1"></i>Hub
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="<?php echo esc_url(home_url('/espace-intervenant-agenda')); ?>">
|
||||
<i class="fas fa-calendar-alt me-1"></i>Agenda
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="<?php echo esc_url(home_url('/espace-intervenant-profil')); ?>">
|
||||
<i class="fas fa-user me-1"></i>Mon Profil
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="<?php echo esc_url(home_url('/encodage-permanences')); ?>">
|
||||
<i class="fas fa-clock me-1"></i>Permanences
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8 offset-lg-2">
|
||||
<!-- Section Informations modifiables -->
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h3 class="h5 mb-0">
|
||||
<i class="fas fa-edit me-2"></i>Mes informations personnelles
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="profile-form">
|
||||
<!-- Informations non modifiables (affichage seul) -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-muted">Nom</label>
|
||||
<input type="text" class="form-control" id="profile-nom" value="<?php echo esc_attr($user->last_name); ?>" readonly>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-muted">Prénom</label>
|
||||
<input type="text" class="form-control" id="profile-prenom" value="<?php echo esc_attr($user->first_name); ?>" readonly>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-muted">Email</label>
|
||||
<input type="email" class="form-control" id="profile-email" value="<?php echo esc_attr($user->user_email); ?>" readonly>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Téléphone (modifiable) -->
|
||||
<div class="mb-3">
|
||||
<label for="profile-telephone" class="form-label">
|
||||
Téléphone <span class="text-danger">*</span>
|
||||
</label>
|
||||
<div class="input-group">
|
||||
<input type="tel" class="form-control" id="profile-telephone" name="telephone" value="<?php echo esc_attr($telephone); ?>" required>
|
||||
<button class="btn btn-outline-secondary" type="button" id="edit-telephone-btn">
|
||||
<i class="fas fa-edit"></i> Modifier
|
||||
</button>
|
||||
</div>
|
||||
<small class="form-text text-muted">Vous pouvez modifier votre numéro de téléphone</small>
|
||||
</div>
|
||||
|
||||
<!-- Boutons d'action -->
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save me-2"></i>Enregistrer les modifications
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary" id="cancel-profile-btn">
|
||||
Annuler
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section Disponibilités -->
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h3 class="h5 mb-0">
|
||||
<i class="fas fa-calendar-check me-2"></i>Mes disponibilités
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="disponibilites-form">
|
||||
<!-- Jours de disponibilité (checkboxes) -->
|
||||
<div class="mb-4">
|
||||
<label class="form-label fw-bold">Jours de disponibilité</label>
|
||||
<div id="profile-jours-disponibilite-checkboxes" class="row g-3">
|
||||
<?php foreach ($jours_labels as $jour_value => $jour_label) :
|
||||
$is_checked = in_array($jour_value, $jours_disponibilite, true);
|
||||
?>
|
||||
<div class="col-md-3 col-sm-4 col-6">
|
||||
<div class="form-check">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
value="<?php echo esc_attr($jour_value); ?>"
|
||||
id="jour-<?php echo esc_attr($jour_value); ?>"
|
||||
name="jours_disponibilite[]"
|
||||
<?php echo $is_checked ? 'checked' : ''; ?>
|
||||
>
|
||||
<label class="form-check-label" for="jour-<?php echo esc_attr($jour_value); ?>">
|
||||
<?php echo esc_html($jour_label); ?>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<small class="form-text text-muted">Cochez les jours où vous êtes disponible</small>
|
||||
</div>
|
||||
|
||||
<!-- Indisponibilités ponctuelles -->
|
||||
<div class="mb-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<label class="form-label fw-bold mb-0">Mes indisponibilités ponctuelles</label>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" id="add-indisponibilite-btn">
|
||||
<i class="fas fa-plus me-1"></i>Ajouter une indisponibilité
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Liste des indisponibilités existantes -->
|
||||
<div id="profile-indisponibilites-list" class="indisponibilites-list mb-3">
|
||||
<?php if (!empty($indisponibilites_ponctuelles)) : ?>
|
||||
<?php foreach ($indisponibilites_ponctuelles as $index => $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;
|
||||
}
|
||||
?>
|
||||
<div class="indisponibilite-item" data-index="<?php echo esc_attr($index); ?>">
|
||||
<div>
|
||||
<strong><?php echo esc_html($periode_text); ?></strong>
|
||||
<span class="badge bg-warning text-dark ms-2"><?php echo esc_html($type_label); ?></span>
|
||||
<?php if ($commentaire) : ?>
|
||||
<p class="mb-0 text-muted mt-1"><?php echo esc_html($commentaire); ?></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<button type="button" class="btn-remove" onclick="removeIndisponibilite(<?php echo esc_attr($index); ?>)" title="Supprimer">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php else : ?>
|
||||
<div id="profile-indisponibilites-empty" class="alert alert-info">
|
||||
<i class="fas fa-info-circle me-2"></i>Aucune indisponibilité prévue.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<!-- Zone pour ajouter une nouvelle indisponibilité -->
|
||||
<div id="indisponibilite-form-container" style="display: none;">
|
||||
<div class="card border-primary">
|
||||
<div class="card-header bg-light">
|
||||
<h6 class="mb-0">Nouvelle indisponibilité</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label for="indispo-debut" class="form-label">Date de début <span class="text-danger">*</span></label>
|
||||
<input type="date" class="form-control" id="indispo-debut" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="indispo-fin" class="form-label">Date de fin <span class="text-danger">*</span></label>
|
||||
<input type="date" class="form-control" id="indispo-fin" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="indispo-type" class="form-label">Type <span class="text-danger">*</span></label>
|
||||
<select class="form-select" id="indispo-type" required>
|
||||
<option value="">Sélectionnez un type</option>
|
||||
<option value="conge">Congé</option>
|
||||
<option value="absence">Absence</option>
|
||||
<option value="maladie">Maladie</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-12">
|
||||
<label for="indispo-commentaire" class="form-label">Commentaire</label>
|
||||
<textarea class="form-control" id="indispo-commentaire" rows="2" placeholder="Optionnel"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 d-flex gap-2">
|
||||
<button type="button" class="btn btn-primary btn-sm" id="save-indisponibilite-btn">
|
||||
<i class="fas fa-save me-1"></i>Enregistrer
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" id="cancel-indisponibilite-btn">
|
||||
Annuler
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bouton de sauvegarde -->
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save me-2"></i>Enregistrer les disponibilités
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section Départements et spécialisations (consultation seule) -->
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-light">
|
||||
<h3 class="h5 mb-0">
|
||||
<i class="fas fa-building me-2"></i>Départements et spécialisations
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Départements</label>
|
||||
<div id="profile-departements" class="badge-container">
|
||||
<!-- Sera rempli par JavaScript -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">Spécialisations</label>
|
||||
<div id="profile-specialisations" class="badge-container">
|
||||
<!-- Sera rempli par JavaScript -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<small class="text-muted">
|
||||
<i class="fas fa-lock me-1"></i>
|
||||
Ces informations sont gérées par l'administrateur.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user