Crvi/app/models/Event_Model.php
2026-01-20 21:17:45 +01:00

1823 lines
75 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
namespace ESI_CRVI_AGENDA\models;
use ESI_CRVI_AGENDA\models\CRVI_Intervenant_Model;
use ESI_CRVI_AGENDA\controllers\CRVI_Plugin;
use ESI_CRVI_AGENDA\models\CRVI_Beneficiaire_Model;
use ESI_CRVI_AGENDA\models\CRVI_Local_Model;
use ESI_CRVI_AGENDA\models\CRVI_Traducteur_Model;
use ESI_CRVI_AGENDA\helpers\Api_Helper;
class CRVI_Event_Model extends Main_Model {
public $id;
public $date;
public $heure;
public $date_fin;
public $heure_fin;
public $type; // individuel/groupe
public $statut; // prévu, annulé, non tenu, cloture, absence.
public $motif_annulation;
public $commentaire;
public $langue;
public $id_beneficiaire;
public $id_intervenant;
public $id_traducteur;
public $id_local;
public $nb_participants;
public $nb_hommes;
public $nb_femmes;
public $cree_par;
public $modifie_par;
public $date_creation;
public $date_modification;
// Optionnellement présents selon l'environnement (compat multi-schémas)
public $departement;
public $type_intervention;
/**
* Schéma des champs ACF : nom => 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érorempli) => 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
];
}
}