type (ex : group, repeater, taxonomy, text...) */ public static $acf_schema = [ 'date' => 'date_picker', 'heure' => 'time_picker', 'id_beneficiaire' => 'relation', 'id_intervenant' => 'relation', 'departements' => 'taxonomy', 'type_intervention' => 'taxonomy', 'local' => 'relation', 'langue' => 'taxonomy', 'statut' => 'select', 'commentaire' => 'textarea', ]; public function __construct($data = []) { foreach ($data as $key => $value) { if (property_exists($this, $key)) { $this->$key = $value; } } } /** * Charger un événement par ID * @param int $id * @param array $fields Champs spécifiques à charger (optionnel) * @return self|null */ public static function load($id, $fields = []) { global $wpdb; $table_name = $wpdb->prefix . 'crvi_agenda'; $row = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table_name WHERE id = %d AND is_deleted = 0", $id), ARRAY_A); if (!$row) { 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'] = (int)$row['id']; } elseif ($field === 'beneficiaire' && isset($row['id_beneficiaire'])) { $data['beneficiaire'] = CRVI_Beneficiaire_Model::load($row['id_beneficiaire'], ['id', 'nom', 'prenom']); } elseif ($field === 'intervenant' && isset($row['id_intervenant'])) { $data['intervenant'] = CRVI_Intervenant_Model::load($row['id_intervenant'], ['id', 'nom', 'prenom']); } elseif ($field === 'traducteur' && isset($row['id_traducteur'])) { $data['traducteur'] = CRVI_Traducteur_Model::load($row['id_traducteur'], ['id', 'nom', 'prenom']); } elseif ($field === 'local' && isset($row['id_local'])) { $data['local'] = CRVI_Local_Model::load($row['id_local'], ['id', 'nom', 'type_de_local']); } elseif (isset($row[$field])) { $data[$field] = $row[$field]; } } return new self($data); } // Sinon, charger tous les champs par défaut $beneficiaire = CRVI_Beneficiaire_Model::load($row['id_beneficiaire'], ['id', 'nom', 'prenom']); $intervenant = CRVI_Intervenant_Model::load($row['id_intervenant'], ['id', 'nom', 'prenom']); $traducteur = CRVI_Traducteur_Model::load($row['id_traducteur'], ['id', 'nom', 'prenom']); $local = CRVI_Local_Model::load($row['id_local'], ['id', 'nom', 'type_de_local']); // Mapping des champs SQL -> propriétés de l'objet $data = [ 'id' => (int)$row['id'], 'date' => $row['date_rdv'], 'heure' => $row['heure_rdv'], 'date_fin' => $row['date_fin'], 'heure_fin' => $row['heure_fin'], 'type' => $row['type'], 'statut' => $row['statut'], 'motif_annulation' => $row['motif_annulation'], 'commentaire' => $row['commentaire'], 'departement' => $row['id_departement'], 'type_intervention' => $row['id_type_intervention'], 'langue' => $row['langue'], 'id_beneficiaire' => $row['id_beneficiaire'], 'id_intervenant' => $row['id_intervenant'], 'id_traducteur' => $row['id_traducteur'], 'id_local' => $row['id_local'], 'nb_participants' => $row['nb_participants'], 'nb_hommes' => $row['nb_hommes'], 'nb_femmes' => $row['nb_femmes'], 'cree_par' => $row['cree_par'], 'modifie_par' => $row['modifie_par'], 'date_creation' => $row['date_creation'], 'date_modification' => $row['date_modification'], 'cloture_rdv' => $row['cloture_rdv'], 'cloture_par' => $row['cloture_par'], 'cloture_flag' => $row['cloture_flag'], 'cloture_commentaire' => $row['cloture_commentaire'], 'assign' => isset($row['assign']) ? (int)$row['assign'] : 0, 'beneficiaire' => $beneficiaire, 'intervenant' => $intervenant, 'traducteur' => $traducteur, 'local' => $local, ]; return new self($data); } /** * Retourne un événement enrichi (même structure que la liste). */ public function get_event_enriched($id) { global $wpdb; $table_name = $wpdb->prefix . 'crvi_agenda'; $row = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table_name WHERE id = %d AND is_deleted = 0", (int)$id), ARRAY_A); if (!$row) { return null; } return $this->enrich_event_row($row); } /** * Enrichit un enregistrement brut d'événement avec les objets liés. */ private function enrich_event_row(array $event) { $enriched_event = $event; // Bénéficiaire if (!empty($event['id_beneficiaire'])) { $beneficiaire = CRVI_Beneficiaire_Model::load($event['id_beneficiaire'], ['id', 'nom', 'prenom', 'email', 'telephone', 'langues_parlees']); if ($beneficiaire) { $langues_parlees = $beneficiaire->langues_parlees; if (!empty($langues_parlees) && !is_array($langues_parlees)) { $langues_parlees = explode('|', $langues_parlees); } // Récupérer les indisponibilités ponctuelles du bénéficiaire $indisponibilites = []; if (function_exists('get_field')) { $indisponibilites_acf = get_field('indisponibilitee_ponctuelle', $beneficiaire->id); if (is_array($indisponibilites_acf)) { $indisponibilites = $indisponibilites_acf; } } $enriched_event['beneficiaire'] = [ 'id' => $beneficiaire->id, 'nom' => $beneficiaire->nom, 'prenom' => $beneficiaire->prenom, 'nom_complet' => $beneficiaire->nom . ' ' . $beneficiaire->prenom, 'email' => $beneficiaire->email, 'telephone' => $beneficiaire->telephone, 'langues_parlees' => $langues_parlees, 'indisponibilitee_ponctuelle' => $indisponibilites, ]; } } // Intervenant if (!empty($event['id_intervenant'])) { $intervenant = CRVI_Intervenant_Model::load($event['id_intervenant'], ['id', 'nom', 'prenom', 'email', 'departements_ids', 'types_intervention_ids']); if ($intervenant) { $types_intervention_noms = []; if (!empty($intervenant->types_intervention_ids)) { if (!is_array($intervenant->types_intervention_ids)) { $intervenant->types_intervention_ids = explode('|', $intervenant->types_intervention_ids); } foreach ($intervenant->types_intervention_ids as $type_id) { $type_nom = $this->get_type_intervention_nom($type_id); if ($type_nom) { $types_intervention_noms[] = $type_nom; } } } $departements_noms = []; if (!empty($intervenant->departements_ids)) { if (!is_array($intervenant->departements_ids)) { $intervenant->departements_ids = explode('|', $intervenant->departements_ids); } foreach ($intervenant->departements_ids as $dept_id) { $dept_nom = $this->get_departement_nom($dept_id); if ($dept_nom) { $departements_noms[] = $dept_nom; } } } $intervenant_jours = []; $intervenant_heures = []; if (function_exists('get_field')) { $intervenant_jours = get_field('jours_de_disponibilite', 'user_' . $intervenant->id) ?: []; $intervenant_heures = get_field('heures_de_permanences', 'user_' . $intervenant->id) ?: []; } $enriched_event['intervenant'] = [ 'id' => $intervenant->id, 'nom' => $intervenant->nom, 'prenom' => $intervenant->prenom, 'nom_complet' => $intervenant->nom . ' ' . $intervenant->prenom, 'email' => $intervenant->email, 'departements_ids' => $intervenant->departements_ids, 'departements_noms' => $departements_noms, 'types_intervention_ids' => $intervenant->types_intervention_ids, 'types_intervention_noms' => $types_intervention_noms, 'jours_de_disponibilite' => is_array($intervenant_jours) ? array_values($intervenant_jours) : [], 'heures_de_permanences' => is_array($intervenant_heures) ? array_values($intervenant_heures) : [], ]; } } // Traducteur if (!empty($event['id_traducteur'])) { $traducteur = CRVI_Traducteur_Model::load($event['id_traducteur'], ['id', 'nom', 'prenom', 'email', 'langues_parlees', 'organisme']); if ($traducteur) { $langues_parlees = $traducteur->langues_parlees; if (!empty($langues_parlees) && !is_array($langues_parlees)) { $langues_parlees = explode('|', $langues_parlees); } $enriched_event['traducteur'] = [ 'id' => $traducteur->id, 'nom' => $traducteur->nom, 'prenom' => $traducteur->prenom, 'nom_complet' => $traducteur->nom . ' ' . $traducteur->prenom, 'email' => $traducteur->email, 'langues_parlees' => $langues_parlees, 'organisme' => $traducteur->organisme, ]; } } // Local if (!empty($event['id_local'])) { $local = CRVI_Local_Model::load($event['id_local'], ['id', 'nom', 'type_de_local', 'capacite']); if ($local) { $enriched_event['local'] = [ 'id' => $local->id, 'nom' => $local->nom, 'type_de_local' => $local->type_de_local, 'capacite' => $local->capacite, ]; } } return $enriched_event; } /** * Sauvegarder l'événement */ public function save() { // À implémenter : sauvegarder dans la base/CPT/meta return true; } /** * Vérifier si l'entité est disponible à la date/heure de l'événement */ public function is_disponible($date, $heure) { global $wpdb; $table_name = $wpdb->prefix . 'crvi_agenda'; $sql = $wpdb->prepare("SELECT * FROM $table_name WHERE date_rdv = %s AND heure_rdv = %s AND statut = 'prevu' AND is_deleted = 0", $date, $heure); $result = $wpdb->get_results($sql, ARRAY_A); if(!empty($result)) { return false; } return true; } /** * Retourner les conflits éventuels pour cet événement */ public function get_conflits($date, $heure, $id_intervenant, $id_traducteur, $id_local) { // À implémenter : logique de détection de conflits global $wpdb; $table_name = $wpdb->prefix . 'crvi_agenda'; $sql = $wpdb->prepare("SELECT * FROM $table_name WHERE date_rdv = %s AND heure_rdv = %s AND id_intervenant = %d AND id_traducteur = %d AND id_local = %d AND is_deleted = 0", $date, $heure, $id_intervenant, $id_traducteur, $id_local); $result = $wpdb->get_results($sql, ARRAY_A); if(!empty($result)) { return false; } return []; } /** * Retourner l'historique des modifications */ public function get_historique() { // À implémenter : récupérer l'historique return []; } /** * Correspondance des filtres API (nom métier) avec les colonnes SQL : * * | Paramètre API | Colonne SQL | * |-------------------|---------------------| * | date | date_rdv | * | type | type | * | statut | statut | * | departement | departement | * | type_intervention | type_intervention | * | langue | langue | * | beneficiaire | id_beneficiaire | * | intervenant | id_intervenant | * | traducteur | id_traducteur | * | local | id_local | * | cree_par | cree_par | * * Les paramètres API doivent être mappés vers les colonnes SQL correspondantes pour l'appel à cette méthode. * * @param string $type Colonne principale (ex: 'id_intervenant') * @param mixed $value Valeur principale * @param array $attributes Tableau associatif clé => valeur pour filtres supplémentaires * @param string $date_debut Date de début (format 'Y-m-d') * @param string $date_fin Date de fin (format 'Y-m-d') * @return array Résultats bruts (ARRAY_A) */ public function get_events_by($type, $value, $attributes = [], $date_debut = null, $date_fin = null) { global $wpdb; $table_name = $wpdb->prefix . 'crvi_agenda'; // Liste blanche des colonnes autorisées pour la sécurité $allowed_columns = [ 'date_rdv', 'heure_rdv', 'type', 'statut', 'id_departement', 'id_type_intervention', 'langue', 'id_beneficiaire', 'id_intervenant', 'id_traducteur', 'id_local','cree_par' ]; $where = []; $values = []; // Filtre principal if (in_array($type, $allowed_columns, true)) { $where[] = "$type = %s"; $values[] = $value; } else { return []; } // Filtres supplémentaires foreach ($attributes as $key => $val) { if (in_array($key, $allowed_columns, true)) { $where[] = "$key = %s"; $values[] = $val; } } // Plage de dates if ($date_debut && $date_fin) { $where[] = "date_rdv BETWEEN %s AND %s"; $values[] = $date_debut; $values[] = $date_fin; } elseif ($date_debut) { $where[] = "date_rdv >= %s"; $values[] = $date_debut; } elseif ($date_fin) { $where[] = "date_rdv <= %s"; $values[] = $date_fin; } // Filtre soft delete - exclure les événements supprimés $where[] = "is_deleted = 0"; $where_sql = $where ? ('WHERE ' . implode(' AND ', $where)) : ''; $sql = "SELECT * FROM $table_name $where_sql"; $prepared_sql = $wpdb->prepare($sql, $values); $results = $wpdb->get_results($prepared_sql, ARRAY_A); return $results; } /** * Retourne un objet avec les détails principaux des entités liées à l'événement. * @return object */ public function get_details($id_event) { $details = new \stdClass(); $event = self::load($id_event); if (!$event) { return $details; } // Bénéficiaire if (!empty($event->id_beneficiaire)) { $benef = CRVI_Beneficiaire_Model::load($event->id_beneficiaire); if ($benef) { $details->beneficiaire = (object) [ 'id' => $benef->id, 'nom' => $benef->nom ?? '', 'prenom' => $benef->prenom ?? '', 'email' => $benef->email ?? '', ]; } } // Intervenant if (!empty($event->id_intervenant)) { $interv = CRVI_Intervenant_Model::load($event->id_intervenant); if ($interv) { $details->intervenant = (object) [ 'id' => $interv->id, 'nom' => $interv->nom ?? '', 'prenom' => $interv->prenom ?? '', 'email' => $interv->email ?? '', ]; } } // Traducteur if (!empty($event->id_traducteur)) { $trad = CRVI_Traducteur_Model::load($event->id_traducteur); if ($trad) { $details->traducteur = (object) [ 'id' => $trad->id, 'nom' => $trad->nom ?? '', 'prenom' => $trad->prenom ?? '', 'email' => $trad->email ?? '', ]; } } // Local if (!empty($event->id_local)) { $local = CRVI_Local_Model::load($event->id_local); if ($local) { $details->local = (object) [ 'id' => $local->id, 'nom' => $local->nom ?? '', 'type_de_local' => $local->type_de_local ?? '', ]; } } return $details; } /** * Crée la table personnalisée wp_crvi_agenda pour stocker les rendez-vous. * À appeler à l'activation du plugin. * statut : prevu, annulé, non tenu, etc. */ public static function create_table() { global $wpdb; $table_name = $wpdb->prefix . 'crvi_agenda'; // Vérification si la table existe déjà if ($wpdb->get_var("SHOW TABLES LIKE '$table_name'") === $table_name) { // Vérifier si le champ is_deleted existe, sinon l'ajouter $column_exists = $wpdb->get_results("SHOW COLUMNS FROM $table_name LIKE 'is_deleted'"); if (empty($column_exists)) { $wpdb->query("ALTER TABLE $table_name ADD COLUMN is_deleted tinyint(1) DEFAULT 0"); } // Vérifier si la colonne assign existe, sinon l'ajouter $column_assign = $wpdb->get_results($wpdb->prepare( "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s AND COLUMN_NAME = 'assign'", DB_NAME, $table_name )); if (empty($column_assign)) { $wpdb->query("ALTER TABLE $table_name ADD COLUMN assign tinyint(1) DEFAULT 0"); } // Harmoniser le schéma: renommer les colonnes si nécessaire // departement -> id_departement $has_id_dept = $wpdb->get_var($wpdb->prepare( "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s AND COLUMN_NAME = 'id_departement'", DB_NAME, $table_name )); $has_dept = $wpdb->get_var($wpdb->prepare( "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s AND COLUMN_NAME = 'departement'", DB_NAME, $table_name )); if (!$has_id_dept && $has_dept) { $wpdb->query("ALTER TABLE $table_name CHANGE COLUMN departement id_departement int(10) unsigned DEFAULT NULL"); } // type_intervention -> id_type_intervention $has_id_type = $wpdb->get_var($wpdb->prepare( "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s AND COLUMN_NAME = 'id_type_intervention'", DB_NAME, $table_name )); $has_type = $wpdb->get_var($wpdb->prepare( "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s AND COLUMN_NAME = 'type_intervention'", DB_NAME, $table_name )); if (!$has_id_type && $has_type) { $wpdb->query("ALTER TABLE $table_name CHANGE COLUMN type_intervention id_type_intervention int(10) unsigned DEFAULT NULL"); } // Vérifier si la colonne langues_disponibles existe, sinon l'ajouter $column_langues_disponibles = $wpdb->get_results($wpdb->prepare( "SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s AND COLUMN_NAME = 'langues_disponibles'", DB_NAME, $table_name )); if (empty($column_langues_disponibles)) { $wpdb->query("ALTER TABLE $table_name ADD COLUMN langues_disponibles varchar(255) DEFAULT NULL"); } return; // La table existe déjà, on ne fait rien } $charset_collate = $wpdb->get_charset_collate(); $sql = "CREATE TABLE $table_name ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, date_rdv date NOT NULL, heure_rdv time NOT NULL, date_fin date DEFAULT NULL, heure_fin time DEFAULT NULL, type varchar(20) NOT NULL, statut varchar(20) DEFAULT 'prevu', motif_annulation varchar(255) DEFAULT NULL, commentaire text DEFAULT NULL, id_departement int(10) unsigned DEFAULT NULL, id_type_intervention int(10) unsigned DEFAULT NULL, langue varchar(20) NOT NULL, id_beneficiaire bigint(20) unsigned DEFAULT NULL, id_intervenant bigint(20) unsigned DEFAULT NULL, id_traducteur bigint(20) unsigned DEFAULT NULL, id_local bigint(20) unsigned DEFAULT NULL, nb_participants int DEFAULT NULL, nb_hommes int DEFAULT NULL, nb_femmes int DEFAULT NULL, cree_par bigint(20) unsigned DEFAULT NULL, modifie_par bigint(20) unsigned DEFAULT NULL, date_creation datetime DEFAULT CURRENT_TIMESTAMP, date_modification datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, cloture_rdv datetime DEFAULT NULL, cloture_par bigint(20) unsigned DEFAULT NULL, cloture_commentaire text DEFAULT NULL, cloture_flag tinyint(1) DEFAULT 0, is_deleted tinyint(1) DEFAULT 0, assign tinyint(1) DEFAULT 0, langues_disponibles varchar(255) DEFAULT NULL, PRIMARY KEY (id) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); } /** * Calcule automatiquement le flag assign basé sur id_beneficiaire et id_local * assign = 1 si les deux sont présents et non null, sinon 0 * @param array $data Données de l'événement * @return int 0 ou 1 */ public static function calculate_assign_flag($data) { $id_beneficiaire = isset($data['id_beneficiaire']) ? $data['id_beneficiaire'] : null; $id_local = isset($data['id_local']) ? $data['id_local'] : null; // assign = 1 si bénéficiaire ET local sont présents et non null if (!empty($id_beneficiaire) && !empty($id_local)) { return 1; } return 0; } /** * get departement nom */ public function get_departement_nom($id_departement) { $departement = get_term_by('id', $id_departement, 'departement'); return $departement->name; } /** * get type_intervention nom */ public function get_type_intervention_nom($id_type_intervention) { $type_intervention = get_term_by('id', $id_type_intervention, 'type_intervention'); return $type_intervention->name; } /** * Créer un événement. * @param array $data * @return int|WP_Error */ public function create_event(array $data) { global $wpdb; // 1. Vérifier les droits if (!current_user_can('edit_posts')) { return new \WP_Error('unauthorized', 'Non autorisé', ['status' => 403]); } // 2. Valider les données (assouplissement: bénéficiaire requis seulement hors groupe; traducteur optionnel) $requiredFields = ['date_rdv', 'heure_rdv', 'type', 'langue', 'id_intervenant', 'id_local']; foreach ($requiredFields as $field) { if (!isset($data[$field]) || $data[$field] === '' || $data[$field] === null) { return new \WP_Error('validation_error', 'Données invalides', ['status' => 400]); } } if (($data['type'] ?? '') !== 'groupe') { if (!isset($data['id_beneficiaire']) || $data['id_beneficiaire'] === '' || $data['id_beneficiaire'] === null) { return new \WP_Error('validation_error', 'Données invalides', ['status' => 400]); } } // 3. Vérifier les conflits (créneau, disponibilité) // OPTIMISATION : Utiliser les permissions stockées si disponibles (pour les événements existants modifiés) // Pour les nouvelles créations, on vérifie via l'intervenant puis on crée les permissions $intervenant = CRVI_Intervenant_Model::load($data['id_intervenant']); if (!$intervenant) { return new \WP_Error('validation_error', 'Intervenant non trouvé', ['status' => 400]); } $nom_intervenant = $intervenant->nom . ' ' . $intervenant->prenom; if (!$intervenant->is_disponible($data['id_intervenant'], $data['date_rdv'])) { return new \WP_Error('conflict_error', 'Conflit de disponibilité, l\'intervenant ' . $nom_intervenant . ' est déjà pris pour le créneau ' . $data['date_rdv'] . ' à ' . $data['heure_rdv'], ['status' => 400]); } if (isset($data['id_traducteur']) && $data['id_traducteur'] !== '' && $data['id_traducteur'] !== null) { $traducteur = CRVI_Traducteur_Model::load($data['id_traducteur']); if (!$traducteur) { return new \WP_Error('validation_error', 'Traducteur non trouvé', ['status' => 400]); } $nom_traducteur = $traducteur->nom . ' ' . $traducteur->prenom; if (!$traducteur->is_disponible($data['id_traducteur'], $data['date_rdv'])) { return new \WP_Error('conflict_error', 'Conflit de disponibilité, le traducteur ' . $nom_traducteur . ' est déjà pris pour le créneau ' . $data['date_rdv'] . ' à ' . $data['heure_rdv'], ['status' => 400]); } } $local = CRVI_Local_Model::load($data['id_local']); if (!$local) { return new \WP_Error('validation_error', 'Local non trouvé', ['status' => 400]); } $nom_local = $local->nom; if (!$local->is_disponible($data['date_rdv'])) { return new \WP_Error('conflict_error', 'Conflit de disponibilité, le local ' . $nom_local . ' est déjà pris pour le créneau ' . $data['date_rdv'] . ' à ' . $data['heure_rdv'], ['status' => 400]); } // 4. Calculer automatiquement le flag assign $data['assign'] = self::calculate_assign_flag($data); // 5. Insérer en base $result = $wpdb->insert($wpdb->prefix . 'crvi_agenda', $data); if ($result === false) { return new \WP_Error('database_error', 'Erreur lors de l\'insertion en base de données', ['status' => 500]); } // 6. Retourner l'ID ou une erreur return $wpdb->insert_id; } /** * Crée une permanence (événement sans bénéficiaire ni local) * Les permanences sont des créneaux horaires disponibles pour les intervenants * @param array $data Données de la permanence * @return int|WP_Error ID de l'événement créé ou WP_Error */ public function create_permanence(array $data) { global $wpdb; // Validation des champs obligatoires pour une permanence $requiredFields = ['date_rdv', 'heure_rdv', 'id_intervenant']; foreach ($requiredFields as $field) { if (!isset($data[$field]) || $data[$field] === '' || $data[$field] === null) { return new \WP_Error('validation_error', "Le champ '{$field}' est requis pour créer une permanence", ['status' => 400]); } } // Vérifier les indisponibilités ponctuelles de l'intervenant pour cette date/heure $intervenant_id = (int) $data['id_intervenant']; $date_rdv = $data['date_rdv']; $heure_rdv = $data['heure_rdv']; if (function_exists('get_field') && $intervenant_id > 0) { $indisponibilites = get_field('indisponibilitee_ponctuelle', 'user_' . $intervenant_id); if (is_array($indisponibilites)) { foreach ($indisponibilites as $indispo) { // Support des deux formats potentiels: // - date (Y-m-d) + optionnel heure_debut/heure_fin (H:i) // - debut/fin (date-time) utilisés ailleurs $indispo_date = isset($indispo['date']) ? $indispo['date'] : null; $indispo_heure_debut = isset($indispo['heure_debut']) ? $indispo['heure_debut'] : null; $indispo_heure_fin = isset($indispo['heure_fin']) ? $indispo['heure_fin'] : null; // Fallback si le repeater utilise 'debut' / 'fin' (timestamp/date) if (!$indispo_date && (isset($indispo['debut']) || isset($indispo['fin']))) { $debut_ts = !empty($indispo['debut']) ? strtotime((string) $indispo['debut']) : null; $fin_ts = !empty($indispo['fin']) ? strtotime((string) $indispo['fin']) : null; $slot_ts = strtotime($date_rdv . ' ' . $heure_rdv); if ($debut_ts && $fin_ts && $slot_ts && $slot_ts >= $debut_ts && $slot_ts <= $fin_ts) { return new \WP_Error('unavailable', 'Créneau indisponible (indisponibilité ponctuelle intervenant)', ['status' => 409]); } } // Cas avec champ 'date' (et éventuelles heures) if ($indispo_date === $date_rdv) { // Si aucune heure précisée, la journée entière est indisponible if (empty($indispo_heure_debut) && empty($indispo_heure_fin)) { return new \WP_Error('unavailable', 'Créneau indisponible (indisponibilité ponctuelle intervenant)', ['status' => 409]); } // Si plage horaire précisée, vérifier l'appartenance du créneau if (!empty($indispo_heure_debut) && !empty($indispo_heure_fin)) { // Comparaison sur format H:i (zéro‑rempli) => comparaison lexicographique OK if ($heure_rdv >= $indispo_heure_debut && $heure_rdv < $indispo_heure_fin) { return new \WP_Error('unavailable', 'Créneau indisponible (indisponibilité ponctuelle intervenant)', ['status' => 409]); } } } } } } // Empêcher la création si un événement/permanence existe déjà sur le même slot pour cet intervenant $existing_id = $wpdb->get_var($wpdb->prepare( "SELECT id FROM {$wpdb->prefix}crvi_agenda WHERE is_deleted = 0 AND id_intervenant = %d AND date_rdv = %s AND heure_rdv = %s LIMIT 1", $intervenant_id, $date_rdv, $heure_rdv )); if (!empty($existing_id)) { return new \WP_Error('conflict_error', 'Un événement existe déjà pour ce créneau', ['status' => 409]); } // Traiter les langues disponibles (tableau de slugs séparés par virgules) $langues_disponibles_value = ''; if (isset($data['langues']) && is_array($data['langues']) && !empty($data['langues'])) { // Nettoyer et valider les slugs de langues $langues_valides = []; foreach ($data['langues'] as $langue_slug) { $langue_slug = sanitize_text_field($langue_slug); if (!empty($langue_slug)) { $langues_valides[] = $langue_slug; } } // Joindre les langues avec le séparateur virgule (,) if (!empty($langues_valides)) { $langues_disponibles_value = implode(',', $langues_valides); } } // Préparer les données pour l'insertion $event_data = [ 'date_rdv' => $data['date_rdv'], 'heure_rdv' => $data['heure_rdv'], 'date_fin' => $data['date_fin'] ?? $data['date_rdv'], 'heure_fin' => $data['heure_fin'] ?? $data['heure_rdv'], 'type' => 'permanence', 'statut' => 'prevu', 'id_intervenant' => $data['id_intervenant'], 'id_beneficiaire' => null, 'id_local' => null, 'id_traducteur' => null, 'langue' => '', // Garder vide pour les permanences 'langues_disponibles' => $langues_disponibles_value, // Langues séparées par virgules (peut être vide) 'assign' => 0, // Non assigné par défaut (pas de bénéficiaire ni local) 'is_deleted' => 0, 'commentaire' => $data['commentaire'] ?? '', 'cree_par' => get_current_user_id(), 'date_creation' => current_time('mysql'), ]; // Insérer en base $result = $wpdb->insert($wpdb->prefix . 'crvi_agenda', $event_data); if ($result === false) { return new \WP_Error('database_error', 'Erreur lors de l\'insertion de la permanence en base de données', ['status' => 500]); } return $wpdb->insert_id; } /** * Mettre à jour un événement. * @param int $id * @param array $data * @return bool|WP_Error */ public function update_event(int $id, array $data) { global $wpdb; // 1. Vérifier les droits if (!current_user_can('edit_posts')) { return new \WP_Error('unauthorized', 'Non autorisé', ['status' => 403]); } // 2. Charger l'événement existant $existing_event = self::load($id); if (!$existing_event) { return new \WP_Error('not_found', 'Événement non trouvé', ['status' => 404]); } // 2.5. Les permanences peuvent être déplacées par un non-propriétaire, // le blocage se fera uniquement en cas d'indisponibilité détectée plus bas. // (Ancien blocage "permanence_immutable" retiré) // 3. S'assurer que $data est un tableau if (!is_array($data)) { $data = []; } // 4. Fusionner les données existantes avec les nouvelles données $update_data = array_merge([ 'date_rdv' => $existing_event->date ?? null, 'heure_rdv' => $existing_event->heure ?? null, 'date_fin' => $existing_event->date_fin ?? null, 'heure_fin' => $existing_event->heure_fin ?? null, 'type' => $existing_event->type ?? null, 'langue' => $existing_event->langue ?? null, 'id_beneficiaire' => $existing_event->id_beneficiaire ?? null, 'id_intervenant' => $existing_event->id_intervenant ?? null, 'id_traducteur' => $existing_event->id_traducteur ?? null, 'id_local' => $existing_event->id_local ?? null, ], $data); // 5. Valider les données fusionnées (aligné avec create_event) // Les permanences ont des règles de validation différentes (pas de langue/local requis) if (($update_data['type'] ?? '') === 'permanence') { // Pour les permanences, seuls date_rdv, heure_rdv, type et id_intervenant sont requis $requiredFields = ['date_rdv', 'heure_rdv', 'type', 'id_intervenant']; foreach ($requiredFields as $field) { if (!isset($update_data[$field]) || $update_data[$field] === '' || $update_data[$field] === null) { $current_value = $update_data[$field] ?? 'non défini'; return new \WP_Error('validation_error', "Le champ '{$field}' est requis (valeur actuelle: " . var_export($current_value, true) . ")", ['status' => 400]); } } } else { // Pour les autres types d'événements (individuel, groupe) $requiredFields = ['date_rdv', 'heure_rdv', 'type', 'langue', 'id_intervenant', 'id_local']; foreach ($requiredFields as $field) { if (!isset($update_data[$field]) || $update_data[$field] === '' || $update_data[$field] === null) { $current_value = $update_data[$field] ?? 'non défini'; return new \WP_Error('validation_error', "Le champ '{$field}' est requis (valeur actuelle: " . var_export($current_value, true) . ")", ['status' => 400]); } } if (($update_data['type'] ?? '') !== 'groupe') { if (!isset($update_data['id_beneficiaire']) || $update_data['id_beneficiaire'] === '' || $update_data['id_beneficiaire'] === null) { return new \WP_Error('validation_error', "Le champ 'id_beneficiaire' est requis pour les événements individuels", ['status' => 400]); } } } // 6. Vérifier les disponibilités via les modèles // Intervenant (toujours présent) if (!empty($update_data['id_intervenant'])) { $intervenant = CRVI_Intervenant_Model::load($update_data['id_intervenant']); if ($intervenant && !$intervenant->is_disponible($update_data['id_intervenant'], $update_data['date_rdv'])) { // Message spécifique pour les permanences déplacées par un non-propriétaire if (($update_data['type'] ?? '') === 'permanence') { $current_user_id = get_current_user_id(); $is_owner = ($current_user_id > 0 && (int)$existing_event->id_intervenant === (int)$current_user_id); if (!$is_owner) { return new \WP_Error( 'conflict_error', 'Cet intervenant n\'a pas de permanence ce jour-là.', ['status' => 400] ); } } return new \WP_Error( 'conflict_error', 'L\'intervenant n\'est pas disponible à cette date', ['status' => 400] ); } } // Traducteur (optionnel - vérification seulement si présent) if (!empty($update_data['id_traducteur'])) { $traducteur = CRVI_Traducteur_Model::load($update_data['id_traducteur']); if ($traducteur && !$traducteur->is_disponible($update_data['id_traducteur'], $update_data['date_rdv'])) { return new \WP_Error( 'conflict_error', 'Le traducteur n\'est pas disponible à cette date', ['status' => 400] ); } } // Local (toujours présent sauf permanences - vérification simple) if (!empty($update_data['id_local'])) { $local = CRVI_Local_Model::load($update_data['id_local']); if ($local && !$local->is_disponible($update_data['date_rdv'])) { return new \WP_Error( 'conflict_error', 'Le local n\'est pas disponible à cette date', ['status' => 400] ); } } // 6.5. Vérifier les conflits de disponibilité (exclure l'événement actuel) // Pour les permanences, on ne vérifie pas les conflits de local (peut être null) $id_local_to_check = ($update_data['type'] === 'permanence') ? null : ($update_data['id_local'] ?? null); $conflits = $this->get_conflits( $update_data['date_rdv'], $update_data['heure_rdv'], $update_data['id_intervenant'], $update_data['id_traducteur'] ?? null, $id_local_to_check ); // Filtrer les conflits pour exclure l'événement actuel $conflits_filtered = []; foreach ($conflits as $type => $conflit) { if ($conflit['id'] != $id) { $conflits_filtered[$type] = $conflit; } } if (!empty($conflits_filtered)) { return new \WP_Error('conflict_error', 'Conflit de disponibilité détecté', ['status' => 409, 'conflicts' => $conflits_filtered]); } // 7. Calculer automatiquement le flag assign $update_data['assign'] = self::calculate_assign_flag($update_data); // 8. Ajouter les métadonnées de modification $update_data['modifie_par'] = get_current_user_id(); $update_data['date_modification'] = current_time('mysql'); // 9. Filtrer les champs pour ne garder que ceux qui existent dans la table // Liste des colonnes valides de la table crvi_agenda $valid_columns = [ 'date_rdv', 'heure_rdv', 'date_fin', 'heure_fin', 'type', 'statut', 'motif_annulation', 'commentaire', 'id_departement', 'id_type_intervention', 'langue', 'id_beneficiaire', 'id_intervenant', 'id_traducteur', 'id_local', 'assign', 'modifie_par', 'date_modification' ]; $filtered_data = []; foreach ($valid_columns as $column) { // Inclure le champ même s'il est null (pour permettre de mettre à null des valeurs existantes) // Mais exclure les chaînes vides qui doivent être converties en null if (array_key_exists($column, $update_data)) { $value = $update_data[$column]; // Convertir les chaînes vides, '0', ou 0 en null pour les champs optionnels if (in_array($column, ['id_traducteur', 'id_beneficiaire']) && ($value === '' || $value === '0' || $value === 0)) { $filtered_data[$column] = null; } else { $filtered_data[$column] = $value; } } } // 10. Mettre à jour en base $result = $wpdb->update($wpdb->prefix . 'crvi_agenda', $filtered_data, ['id' => $id]); if ($result === false && !empty($wpdb->last_error)) { return new \WP_Error('db_error', 'Erreur lors de la mise à jour: ' . $wpdb->last_error, ['status' => 500]); } return true; } /** * Supprimer un événement (suppression logique) * @param int $id * @return bool|WP_Error */ public function delete_event(int $id) { global $wpdb; // 1. Vérifier les droits if (!current_user_can('delete_posts')) { return Api_Helper::json_error('Non autorisé', 403); } // 2. Marquer comme supprimé (champ is_deleted) $wpdb->update($wpdb->prefix . 'crvi_agenda', ['is_deleted' => 1], ['id' => $id]); // 3. Retourner true ou une erreur if ($wpdb->last_error) { return Api_Helper::json_error('Erreur lors de la suppression', 500); } return true; } /** * Clôturer un événement * @param int $id * @param string $statut * @return bool|WP_Error */ public function cloture_event(int $id, string $statut) { global $wpdb; // 1. Vérifier les droits if (!current_user_can('edit_posts')) { return Api_Helper::json_error('Non autorisé', 403); } // 2. Mettre à jour le statut (clôturé, absent, etc.) $wpdb->update($wpdb->prefix . 'crvi_agenda', ['statut' => $statut], ['id' => $id]); // 3. Retourner true ou une erreur if ($wpdb->last_error) { return Api_Helper::json_error('Erreur lors de la clôture', 500); } return true; } /** * Statistiques sur les événements * @param array $params * @return array */ public function get_events_stats(array $params = []) { global $wpdb; $table_name = $wpdb->prefix . 'crvi_agenda'; $where = []; $values = []; $where_sql = $where ? ('WHERE ' . implode(' AND ', $where)) : ''; // 1. Appliquer les filtres (année, code postal, etc.) if (!empty($params['year'])) { $where[] = "YEAR(date_rdv) = %s"; $values[] = $params['year']; } if (!empty($params['code_postal'])) { $where[] = "code_postal = %s"; $values[] = $params['code_postal']; } // Filtre soft delete - exclure les événements supprimés $where[] = "is_deleted = 0"; // 2. Agréger les données $sql = "SELECT * FROM $table_name $where_sql"; $results = $wpdb->get_results($sql, ARRAY_A); // 3. Retourner le tableau de stats return $results; } /** * Filtrer les entités disponibles pour un créneau donné. * @param array $params (ex : ['date' => ..., 'heure' => ..., 'type' => ...]) * @return array */ public function filtrer_disponibles(array $params) { global $wpdb; $table_name = $wpdb->prefix . 'crvi_agenda'; $where = []; $values = []; $where_sql = $where ? ('WHERE ' . implode(' AND ', $where)) : ''; // 1. Récupérer la liste des entités du type demandé if (!empty($params['date'])) { $where[] = "date_rdv = %s"; $values[] = $params['date']; } if (!empty($params['heure'])) { $where[] = "heure_rdv = %s"; $values[] = $params['heure']; } if (!empty($params['type'])) { $where[] = "type = %s"; $values[] = $params['type']; } // Filtre soft delete - exclure les événements supprimés $where[] = "is_deleted = 0"; // 2. Exclure celles déjà prises ou indisponibles à ce créneau $sql = "SELECT * FROM $table_name $where_sql"; // 3. Retourner la liste filtrée $results = $wpdb->get_results($sql, ARRAY_A); return $results; } /** * Wrapper pour obtenir les événements avec filtres API (GET /events) */ public function get_events_by_filters($params = []) { global $wpdb; $table_name = $wpdb->prefix . 'crvi_agenda'; // Mapping des filtres API vers les colonnes SQL $map = [ 'date_debut' => 'date_rdv', 'date_fin' => 'date_rdv', 'type' => 'type', 'statut' => 'statut', 'departement' => 'id_departement', 'type_intervention' => 'id_type_intervention', 'langue' => 'langue', 'beneficiaire' => 'id_beneficiaire', 'intervenant' => 'id_intervenant', 'traducteur' => 'id_traducteur', 'local' => 'id_local', 'cree_par' => 'cree_par', 'assign' => 'assign', ]; $where = []; $values = []; // Traitement des filtres foreach ($map as $api => $col) { if (!empty($params[$api]) || (isset($params[$api]) && $params[$api] === '0')) { if ($api === 'date_debut') { $where[] = "$col >= %s"; $values[] = $params[$api]; } elseif ($api === 'date_fin') { $where[] = "$col <= %s"; $values[] = $params[$api]; } elseif ($api === 'assign') { // Support du filtre assign (0 ou 1) $where[] = "$col = %d"; $values[] = (int)$params[$api]; } else { $where[] = "$col = %s"; $values[] = $params[$api]; } } } // Support des paramètres FullCalendar if (!empty($params['start'])) { $where[] = "date_rdv >= %s"; $values[] = $params['start']; } if (!empty($params['end'])) { $where[] = "date_rdv <= %s"; $values[] = $params['end']; } // Exclure les permanences non attribuées (pour agenda des collègues) if (!empty($params['exclude_unassigned_permanences'])) { // Exclure les événements de type permanence sans intervenant assigné (assign = 0) $where[] = "(type != 'permanence' OR (type = 'permanence' AND assign = 1))"; } // Filtre soft delete - exclure les événements supprimés $where[] = "is_deleted = 0"; $where_sql = $where ? ('WHERE ' . implode(' AND ', $where)) : ''; $sql = "SELECT * FROM $table_name $where_sql ORDER BY date_rdv, heure_rdv"; if (!empty($values)) { $sql = $wpdb->prepare($sql, $values); } $row = $wpdb->get_results($sql, ARRAY_A); // Enrichir les données avec les objets liés $events = []; foreach ($row as $event) { $enriched_event = $event; // Charger le bénéficiaire if (!empty($event['id_beneficiaire'])) { $beneficiaire = CRVI_Beneficiaire_Model::load($event['id_beneficiaire'], ['id', 'nom', 'prenom', 'email', 'telephone', 'langues_parlees']); if ($beneficiaire) { // Convertir langues_parlees en tableau si nécessaire $langues_parlees = $beneficiaire->langues_parlees; if (!empty($langues_parlees) && !is_array($langues_parlees)) { $langues_parlees = explode('|', $langues_parlees); } $enriched_event['beneficiaire'] = [ 'id' => $beneficiaire->id, 'nom' => $beneficiaire->nom, 'prenom' => $beneficiaire->prenom, 'nom_complet' => $beneficiaire->nom . ' ' . $beneficiaire->prenom, 'email' => $beneficiaire->email, 'telephone' => $beneficiaire->telephone, 'langues_parlees' => $langues_parlees, ]; } } // Charger l'intervenant if (!empty($event['id_intervenant'])) { $intervenant = CRVI_Intervenant_Model::load($event['id_intervenant'], ['id', 'nom', 'prenom', 'email', 'departements_ids', 'types_intervention_ids']); if ($intervenant) { // Récupérer les noms des types d'intervention $types_intervention_noms = []; if (!empty($intervenant->types_intervention_ids)) { if(!is_array($intervenant->types_intervention_ids)){ $intervenant->types_intervention_ids = explode('|', $intervenant->types_intervention_ids); } foreach ($intervenant->types_intervention_ids as $type_id) { $type_nom = $this->get_type_intervention_nom($type_id); if ($type_nom) { $types_intervention_noms[] = $type_nom; } } } // Récupérer les noms des départements $departements_noms = []; if (!empty($intervenant->departements_ids)) { if(!is_array($intervenant->departements_ids)){ $intervenant->departements_ids = explode('|', $intervenant->departements_ids); } foreach ($intervenant->departements_ids as $dept_id) { $dept_nom = $this->get_departement_nom($dept_id); if ($dept_nom) { $departements_noms[] = $dept_nom; } } } // Récupérer les disponibilités (jours/heures) de l'intervenant depuis ACF $intervenant_jours = []; $intervenant_heures = []; if (function_exists('get_field')) { $intervenant_jours = get_field('jours_de_disponibilite', 'user_' . $intervenant->id) ?: []; $intervenant_heures = get_field('heures_de_permanences', 'user_' . $intervenant->id) ?: []; } $enriched_event['intervenant'] = [ 'id' => $intervenant->id, 'nom' => $intervenant->nom, 'prenom' => $intervenant->prenom, 'nom_complet' => $intervenant->nom . ' ' . $intervenant->prenom, 'email' => $intervenant->email, 'departements_ids' => $intervenant->departements_ids, 'departements_noms' => $departements_noms, 'types_intervention_ids' => $intervenant->types_intervention_ids, 'types_intervention_noms' => $types_intervention_noms, // Disponibilités ACF 'jours_de_disponibilite' => is_array($intervenant_jours) ? array_values($intervenant_jours) : [], 'heures_de_permanences' => is_array($intervenant_heures) ? array_values($intervenant_heures) : [], ]; } } // Charger le traducteur if (!empty($event['id_traducteur'])) { $traducteur = CRVI_Traducteur_Model::load($event['id_traducteur'], ['id', 'nom', 'prenom', 'email', 'langues_parlees', 'organisme']); if ($traducteur) { // Convertir langues_parlees en tableau si nécessaire $langues_parlees = $traducteur->langues_parlees; if (!empty($langues_parlees) && !is_array($langues_parlees)) { $langues_parlees = explode('|', $langues_parlees); } $enriched_event['traducteur'] = [ 'id' => $traducteur->id, 'nom' => $traducteur->nom, 'prenom' => $traducteur->prenom, 'nom_complet' => $traducteur->nom . ' ' . $traducteur->prenom, 'email' => $traducteur->email, 'langues_parlees' => $langues_parlees, 'organisme' => $traducteur->organisme, ]; } } // Charger le local if (!empty($event['id_local'])) { $local = CRVI_Local_Model::load($event['id_local'], ['id', 'nom', 'type_de_local', 'capacite']); if ($local) { $enriched_event['local'] = [ 'id' => $local->id, 'nom' => $local->nom, 'type_de_local' => $local->type_de_local, 'capacite' => $local->capacite, ]; } } // Ajouter langues_disponibles si présent (pour les permanences) if (isset($event['langues_disponibles']) && !empty($event['langues_disponibles'])) { $enriched_event['langues_disponibles'] = $event['langues_disponibles']; } $events[] = $enriched_event; } return $events; } /** * Retourne les événements au format FullCalendar (GET /events/fullcalendar) */ public function get_events_fullcalendar($params = []) { $events = $this->get_events_by_filters($params); $result = []; foreach ($events as $event) { // Déterminer le titre de l'événement $title = ''; if ($event['type'] === 'groupe') { $title = 'Groupe'; } elseif (!empty($event['beneficiaire'])) { $title = $event['beneficiaire']['nom_complet']; } else { $title = 'Événement sans bénéficiaire'; } $result[] = [ 'id' => $event['id'], 'title' => $title, 'start' => $event['date_rdv'] . 'T' . $event['heure_rdv'], 'end' => ($event['date_fin'] ?? $event['date_rdv']) . 'T' . ($event['heure_fin'] ?? $event['heure_rdv']), 'backgroundColor' => '#28a745', 'borderColor' => '#28a745', 'textColor' => '#fff', 'extendedProps' => [ 'type' => $event['type'], 'statut' => $event['statut'], 'langue' => $event['langue'], 'langues_disponibles' => $event['langues_disponibles'] ?? null, 'beneficiaire' => $event['beneficiaire'] ?? null, 'intervenant' => $event['intervenant'] ?? null, 'traducteur' => $event['traducteur'] ?? null, 'local' => $event['local'] ?? null, 'commentaire' => $event['commentaire'], ], ]; } return $result; } /** * Change le statut d'un événement (PUT /events/{id}/statut) */ public function change_statut($id, $data) { global $wpdb; if (!current_user_can('edit_posts')) { return Api_Helper::json_error('Non autorisé', 403); } if (empty($data['statut'])) { return Api_Helper::json_error('Statut manquant', 400); } $update = [ 'statut' => $data['statut'], ]; if (!empty($data['motif_annulation'])) { $update['motif_annulation'] = $data['motif_annulation']; } if(!empty($data['commentaire'])){ $update['commentaire'] = $data['commentaire']; } // Si le statut est 'present' ou 'absence', marquer comme clôturé if ($data['statut'] === 'present' || $data['statut'] === 'absence') { $current_user = wp_get_current_user(); $update['cloture_flag'] = 1; $update['cloture_par'] = $current_user->ID; $update['cloture_rdv'] = current_time('mysql'); } $wpdb->update($wpdb->prefix . 'crvi_agenda', $update, ['id' => $id]); if ($wpdb->last_error) { return Api_Helper::json_error('Erreur lors du changement de statut', 500); } return true; } /** * Export des événements (GET /events/export) */ public function export_events($params = []) { $events = $this->get_events_by_filters($params); // Générer un CSV ou Excel (ici, on retourne juste les données brutes) // Note: get_events_by_filters inclut déjà le filtre soft delete return $events; } /** * Récupère les événements pour le tableau de stats avec pagination * @param array $params Paramètres de filtres * @param int $page Numéro de page (défaut: 1) * @param int $per_page Nombre d'éléments par page (défaut: 20) * @return array Tableau avec events, total, filtered, page, per_page, total_pages */ public function get_events_table($params = [], $page = 1, $per_page = 20) { global $wpdb; $table_name = $wpdb->prefix . 'crvi_agenda'; // Mapping des filtres API vers les colonnes SQL $map = [ 'date_debut' => 'date_rdv', 'date_fin' => 'date_rdv', 'type' => 'type', 'statut' => 'statut', 'departement' => 'id_departement', 'type_intervention' => 'id_type_intervention', 'langue' => 'langue', 'beneficiaire' => 'id_beneficiaire', 'intervenant' => 'id_intervenant', 'traducteur' => 'id_traducteur', 'local' => 'id_local', 'cree_par' => 'cree_par', 'assign' => 'assign', ]; $where = []; $values = []; // Traitement des filtres foreach ($map as $api => $col) { if (!empty($params[$api]) || (isset($params[$api]) && $params[$api] === '0')) { if ($api === 'date_debut') { $where[] = "$col >= %s"; $values[] = $params[$api]; } elseif ($api === 'date_fin') { $where[] = "$col <= %s"; $values[] = $params[$api]; } elseif ($api === 'assign') { $where[] = "$col = %d"; $values[] = (int)$params[$api]; } else { $where[] = "$col = %s"; $values[] = $params[$api]; } } } // Support des paramètres FullCalendar if (!empty($params['start'])) { $where[] = "date_rdv >= %s"; $values[] = $params['start']; } if (!empty($params['end'])) { $where[] = "date_rdv <= %s"; $values[] = $params['end']; } // Support du filtre date (si date_debut et date_fin sont identiques, c'est une recherche sur une seule date) if (!empty($params['date']) && empty($params['date_debut']) && empty($params['date_fin'])) { $where[] = "date_rdv = %s"; $values[] = $params['date']; } // Filtre soft delete - exclure les événements supprimés $where[] = "is_deleted = 0"; $where_sql = $where ? ('WHERE ' . implode(' AND ', $where)) : ''; // Compter le total d'événements (sans filtres) $total_sql = "SELECT COUNT(*) FROM $table_name WHERE is_deleted = 0"; $total = (int)$wpdb->get_var($total_sql); // Compter les événements filtrés $filtered_sql = "SELECT COUNT(*) FROM $table_name $where_sql"; if (!empty($values)) { $filtered_sql = $wpdb->prepare($filtered_sql, $values); } $filtered = (int)$wpdb->get_var($filtered_sql); // Calculer l'offset pour la pagination $offset = ($page - 1) * $per_page; // Requête avec pagination if (!empty($values)) { // Si on a des valeurs, on doit les ajouter avant LIMIT et OFFSET $sql = "SELECT * FROM $table_name $where_sql ORDER BY date_rdv DESC, heure_rdv DESC LIMIT %d OFFSET %d"; $values_with_pagination = array_merge($values, [$per_page, $offset]); $sql = $wpdb->prepare($sql, $values_with_pagination); } else { // Si pas de valeurs, on peut utiliser directement LIMIT et OFFSET $sql = $wpdb->prepare( "SELECT * FROM $table_name $where_sql ORDER BY date_rdv DESC, heure_rdv DESC LIMIT %d OFFSET %d", $per_page, $offset ); } $row = $wpdb->get_results($sql, ARRAY_A); // Enrichir les données avec les objets liés $events = []; foreach ($row as $event) { $details = $this->get_details($event['id']); // Convertir la langue (ID/slug vers nom) $langue_nom = $event['langue'] ?? ''; if (!empty($event['langue'])) { // Essayer d'abord par ID numérique $langue_term = get_term((int)$event['langue'], 'langue'); if ($langue_term && !is_wp_error($langue_term)) { $langue_nom = $langue_term->name; } else { // Essayer par slug si l'ID ne fonctionne pas $langue_term = get_term_by('slug', $event['langue'], 'langue'); if ($langue_term && !is_wp_error($langue_term)) { $langue_nom = $langue_term->name; } } } $formatted_event = [ 'id' => $event['id'], 'date_rdv' => $event['date_rdv'], 'heure_rdv' => $event['heure_rdv'], 'intervenant_nom' => $details->intervenant->nom_complet ?? '', 'beneficiaire_nom' => $details->beneficiaire->nom_complet ?? '', 'traducteur_nom' => $details->traducteur->nom_complet ?? '', 'langue' => $langue_nom, 'statut' => $event['statut'] ?? '', 'commentaire' => $event['commentaire'] ?? '', ]; $events[] = $formatted_event; } $total_pages = $filtered > 0 ? (int)ceil($filtered / $per_page) : 0; return [ 'events' => $events, 'total' => $total, 'filtered' => $filtered, 'page' => $page, 'per_page' => $per_page, 'total_pages' => $total_pages, ]; } /** * Restaurer un événement supprimé (soft delete) * @param int $id * @return bool|WP_Error */ public function restore_event(int $id) { global $wpdb; // 1. Vérifier les droits if (!current_user_can('delete_posts')) { return new \WP_Error('unauthorized', 'Non autorisé', ['status' => 403]); } // 2. Restaurer l'événement (remettre is_deleted à 0) $result = $wpdb->update($wpdb->prefix . 'crvi_agenda', ['is_deleted' => 0], ['id' => $id]); if ($result === false) { return new \WP_Error('database_error', 'Erreur lors de la restauration', ['status' => 500]); } return true; } /** * Supprimer définitivement un événement (hard delete) * @param int $id * @return bool|WP_Error */ public function hard_delete_event(int $id) { global $wpdb; // 1. Vérifier les droits if (!current_user_can('delete_posts')) { return new \WP_Error('unauthorized', 'Non autorisé', ['status' => 403]); } // 2. Supprimer définitivement l'événement $result = $wpdb->delete($wpdb->prefix . 'crvi_agenda', ['id' => $id]); if ($result === false) { return new \WP_Error('database_error', 'Erreur lors de la suppression définitive', ['status' => 500]); } return true; } /** * Lister les événements supprimés (soft delete) * @param array $params * @return array */ public function get_deleted_events($params = []) { global $wpdb; $table_name = $wpdb->prefix . 'crvi_agenda'; // Mapping des filtres API vers les colonnes SQL $map = [ 'date_debut' => 'date_rdv', 'date_fin' => 'date_rdv', 'type' => 'type', 'statut' => 'statut', 'departement' => 'departement', 'type_intervention' => 'type_intervention', 'langue' => 'langue', 'beneficiaire' => 'id_beneficiaire', 'intervenant' => 'id_intervenant', 'traducteur' => 'id_traducteur', 'local' => 'id_local', 'cree_par' => 'cree_par', ]; $where = ['is_deleted = 1']; // Seulement les événements supprimés $values = []; // Traitement des filtres foreach ($map as $api => $col) { if (!empty($params[$api])) { if ($api === 'date_debut') { $where[] = "$col >= %s"; $values[] = $params[$api]; } elseif ($api === 'date_fin') { $where[] = "$col <= %s"; $values[] = $params[$api]; } else { $where[] = "$col = %s"; $values[] = $params[$api]; } } } $where_sql = 'WHERE ' . implode(' AND ', $where); $sql = "SELECT * FROM $table_name $where_sql ORDER BY date_rdv DESC, heure_rdv DESC"; if (!empty($values)) { $sql = $wpdb->prepare($sql, $values); } return $wpdb->get_results($sql, ARRAY_A); } /** * Récupérer les 3 derniers rendez-vous d'un bénéficiaire avec leurs incidents * @param int $beneficiaire_id * @return array */ public static function get_last_3_rdv_by_beneficiaire($beneficiaire_id) { global $wpdb; $table_name = $wpdb->prefix . 'crvi_agenda'; $incident_table = $wpdb->prefix . 'crvi_agenda_incident'; // Récupérer les 3 derniers RDV individuels du bénéficiaire $events = $wpdb->get_results($wpdb->prepare( "SELECT * FROM $table_name WHERE id_beneficiaire = %d AND type = 'individuel' AND is_deleted = 0 ORDER BY date_rdv DESC, heure_rdv DESC LIMIT 3", $beneficiaire_id ), ARRAY_A); if (empty($events)) { return []; } $result = []; foreach ($events as $event) { $enriched_event = [ 'id' => (int) $event['id'], 'date_rdv' => $event['date_rdv'], 'heure_rdv' => $event['heure_rdv'], 'type' => $event['type'] ?? '', 'statut' => $event['statut'] ?? '', 'commentaire' => $event['commentaire'] ?? '', ]; // Charger le bénéficiaire et son statut liste rouge if (!empty($event['id_beneficiaire'])) { $personne_en_liste_rouge = false; if (function_exists('get_field')) { $personne_en_liste_rouge = get_field('field_69495826ac495', $event['id_beneficiaire']); } $enriched_event['personne_en_liste_rouge'] = (bool) $personne_en_liste_rouge; } // Charger l'intervenant if (!empty($event['id_intervenant'])) { $intervenant = CRVI_Intervenant_Model::load($event['id_intervenant'], ['id', 'nom', 'prenom']); if ($intervenant) { $enriched_event['intervenant'] = [ 'id' => $intervenant->id, 'nom' => $intervenant->nom ?? '', 'prenom' => $intervenant->prenom ?? '', ]; } } // Charger le type d'intervention if (!empty($event['id_type_intervention'])) { $type_intervention = get_term_by('id', $event['id_type_intervention'], 'type_intervention'); if ($type_intervention && !is_wp_error($type_intervention)) { $enriched_event['type_intervention'] = [ 'id' => (int) $event['id_type_intervention'], 'nom' => $type_intervention->name, ]; } } // Charger l'incident associé (s'il existe) $incident = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $incident_table WHERE beneficiaire_id = %d AND event_id = %d LIMIT 1", $beneficiaire_id, $event['id'] )); if ($incident) { $enriched_event['incident'] = [ 'id' => (int) $incident->id, 'resume_incident' => $incident->resume_incident, 'commentaire_incident' => $incident->commentaire_incident ?? '', ]; } $result[] = $enriched_event; } return $result; } /** * Récupère tous les événements d'aujourd'hui dont le statut est non complété * (présence non validée et non marqué absent) * * Statuts non complétés : 'prevu', 'annule', 'non_tenu' * Statuts complétés : 'cloture', 'absence' * * @return array Tableau d'événements enrichis */ public static function get_today_incomplete_events() { global $wpdb; $table_name = $wpdb->prefix . 'crvi_agenda'; $today = current_time('Y-m-d'); // Récupérer les événements d'aujourd'hui avec statut non complété $sql = $wpdb->prepare( "SELECT * FROM $table_name WHERE date_rdv = %s AND statut NOT IN ('cloture', 'absence') AND is_deleted = 0 ORDER BY heure_rdv ASC", $today ); $events = $wpdb->get_results($sql, ARRAY_A); if (empty($events)) { return [ 'success' => true, 'count' => 0, 'events' => [], 'date' => $today, 'message' => 'Aucun événement non complété trouvé pour aujourd\'hui' ]; } // Enrichir les événements avec les données liées $enriched_events = []; foreach ($events as $event) { $enriched_event = [ 'id' => (int) $event['id'], 'date_rdv' => $event['date_rdv'], 'heure_rdv' => $event['heure_rdv'], 'type' => $event['type'], 'statut' => $event['statut'], 'langue' => $event['langue'], ]; // Charger le bénéficiaire si présent if (!empty($event['id_beneficiaire'])) { $beneficiaire = CRVI_Beneficiaire_Model::load($event['id_beneficiaire'], ['id', 'nom', 'prenom', 'telephone']); if ($beneficiaire) { $enriched_event['beneficiaire'] = [ 'id' => $beneficiaire->id, 'nom' => $beneficiaire->nom ?? '', 'prenom' => $beneficiaire->prenom ?? '', 'nom_complet' => ($beneficiaire->nom ?? '') . ' ' . ($beneficiaire->prenom ?? ''), 'telephone' => $beneficiaire->telephone ?? '', ]; } } // Charger l'intervenant si présent if (!empty($event['id_intervenant'])) { $intervenant = CRVI_Intervenant_Model::load($event['id_intervenant'], ['id', 'nom', 'prenom']); if ($intervenant) { $enriched_event['intervenant'] = [ 'id' => $intervenant->id, 'nom' => $intervenant->nom ?? '', 'prenom' => $intervenant->prenom ?? '', 'nom_complet' => ($intervenant->nom ?? '') . ' ' . ($intervenant->prenom ?? ''), ]; } } $enriched_events[] = $enriched_event; } return [ 'success' => true, 'count' => count($enriched_events), 'date' => $today, 'events' => $enriched_events ]; } }