4 Commits
v2.0.2 ... main

Author SHA1 Message Date
e96ad7a244 feat(v2.0.4): Corrections diverses et tri des tableaux devis
- Correction affichage email contact dans SAP (models/msap.php)
- Ajout fonctionnalité tri des tableaux devis (jsap.js, jdevis.js)
- Améliorations diverses vues devis et SAP
- Mise à jour contrôleurs et modèles export

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-05 10:32:19 +01:00
pierre
f6c5e96534 Merge branch 'main' of https://gitea.d6soft.fr/D6/Cleo 2025-11-05 15:47:25 +01:00
pierre
a4d1c22a93 feat(v2.0.3): Marchés hybrides et améliorations multiples
Fonctionnalités principales :

1. Marchés hybrides - Onglet Mercurial
   - Ajout onglet Mercurial avec style distinct (vert, gras, blanc)
   - Affichage des produits mercuriaux pour marchés hybrides
   - Filtrage automatique des produits "Hors Marché 999"
   - Documentation Phase 2 avec CAS 1 et CAS 2 de marchés hybrides
   - Règles métier pour validation différenciée (devis 100% mercurial vs mixte)

2. Corrections bugs
   - Fix flag chkChange sur onglet "Sélection Produits" (callback asynchrone)
   - Plus d'alerte intempestive après sauvegarde des produits

3. Outils de déploiement
   - Nouveau script deploy-file.sh pour déploiement unitaire (DEV/PROD)
   - Amélioration deploy-cleo.sh

4. Gestion multi-contacts (v2.0.3)
   - Contrôleur AJAX cjxcontacts.php
   - Script migration clients_contacts
   - Documentation complète

5. Documentation
   - Mise à jour TODO.md avec Phase 2 marchés hybrides
   - Mise à jour README.md v2.0.3
   - Ajout RULES.md
   - Ajout migration_clients_contacts.sql

6. Nettoyage
   - Suppression fichiers obsolètes (conf_new.php, conf_old.php, uof_linet_20250911.sql)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-05 15:40:06 +01:00
c46359deea Supprimer docs/uof_linet_20250911.sql 2025-09-12 20:28:55 +02:00
27 changed files with 12886 additions and 178492 deletions

1
.gitignore vendored
View File

@@ -41,3 +41,4 @@ sessions/
# Fichiers système
Thumbs.db*.swp
.aider*

View File

@@ -2,8 +2,7 @@
require_once dirname(__FILE__) . '/Database.php';
class Conf
{
class Conf {
const admin = 1;
const intra = 1;
const erp = 1;
@@ -11,7 +10,7 @@ class Conf
public $_appname = "cleo";
public $_appscript = "login";
public $_appversion = "2.0.2";
public $_appversion = "2.0.4";
public $_appenv;
public $_apptitle = "CLEO - Gestion de devis";
@@ -48,8 +47,7 @@ class Conf
public $_entite = '';
public $_new_version = false;
public function __construct()
{
public function __construct() {
$this->loadEnvironment();
$this->loadConfiguration();
$this->setupDebug();
@@ -115,7 +113,6 @@ class Conf
$this->_brandemail = $entite["email"] ?? "";
$this->_brandlogo = $entite["appname"] ?? "cleo";
}
} catch (Exception $e) {
error_log("Erreur de configuration: " . $e->getMessage());
$this->setDefaultConfiguration();

View File

@@ -1,184 +0,0 @@
<?php
require_once dirname(__FILE__) . '/Database.php';
class Conf
{
const admin = 1;
const intra = 1;
const erp = 1;
const magazine = 0;
public $_appname = "cleo";
public $_appscript = "login";
public $_appversion = "2.0.1";
public $_appenv;
public $_apptitle = "CLEO - Gestion de devis";
public $_brandname;
public $_brandadresse1;
public $_brandadresse2;
public $_brandcp;
public $_brandville;
public $_brandtel;
public $_brandemail;
public $_brandlogo;
public $_brandgroupe;
public $_brandmulti;
public $_piwikid;
public $_googlid;
public $_excludeIp;
public $_clientIp;
public $_devIp = false;
public $_debug_level = 0;
public $_log_sql = false;
public $_log_performance = false;
public $_log_file_path = '';
public $_pathupload;
public $_dbhost;
public $_dbname;
public $_dbuser;
public $_dbpass;
public $_entite = '';
public $_new_version = false;
public function __construct()
{
$this->loadEnvironment();
$this->loadConfiguration();
$this->setupDebug();
}
private function loadEnvironment() {
$envFile = dirname(__DIR__) . '/.env';
if (file_exists($envFile)) {
$lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
if (strpos(trim($line), '#') === 0) continue;
list($name, $value) = explode('=', $line, 2);
$name = trim($name);
$value = trim($value);
if (!isset($_ENV[$name])) {
putenv(sprintf('%s=%s', $name, $value));
$_ENV[$name] = $value;
$_SERVER[$name] = $value;
}
}
}
$this->_dbhost = $_ENV['DB_HOST'] ?? 'localhost';
$this->_dbname = $_ENV['DB_DATABASE'] ?? 'cleo';
$this->_dbuser = $_ENV['DB_USERNAME'] ?? 'cleo_user';
$this->_dbpass = $_ENV['DB_PASSWORD'] ?? '';
$this->_excludeIp = $_ENV['EXCLUDE_IP'] ?? '';
$this->_pathupload = $_ENV['UPLOAD_PATH'] ?? '/pub/files/upload/';
$this->_appenv = $_ENV['APP_ENV'] ?? 'production';
$this->_debug_level = $_ENV['LOG_LEVEL'] === 'debug' ? 4 : 0;
$this->_log_sql = filter_var($_ENV['LOG_SQL'] ?? false, FILTER_VALIDATE_BOOLEAN);
$this->_log_performance = filter_var($_ENV['LOG_PERFORMANCE'] ?? false, FILTER_VALIDATE_BOOLEAN);
}
private function loadConfiguration() {
$http_host = $_SERVER['HTTP_HOST'];
try {
$db = Database::getInstance();
$sql = "SELECT * FROM users_entites WHERE http_host LIKE :host AND active = 1 LIMIT 1";
$entite = $db->fetchOne($sql, ['host' => "%$http_host%"]);
if (empty($entite)) {
$sql = "SELECT * FROM users_entites WHERE rowid = 1";
$entite = $db->fetchOne($sql);
}
if ($entite) {
$this->_entite = $entite;
$this->_appname = $entite["appname"] ?? "cleo";
$this->_apptitle = $entite["libelle"] ?? "CLEO";
$this->_brandname = $entite["libelle"] ?? "";
$this->_brandadresse1 = $entite["adresse1"] ?? "";
$this->_brandadresse2 = $entite["adresse2"] ?? "";
$this->_brandcp = $entite["cp"] ?? "";
$this->_brandville = $entite["ville"] ?? "";
$this->_brandtel = $entite["tel1"] ?? "";
$this->_brandemail = $entite["email"] ?? "";
$this->_brandlogo = $entite["appname"] ?? "cleo";
}
} catch (Exception $e) {
error_log("Erreur de configuration: " . $e->getMessage());
$this->setDefaultConfiguration();
}
}
private function setDefaultConfiguration() {
$this->_appname = "cleo";
$this->_apptitle = "CLEO - Gestion de devis";
$this->_brandname = "CLEO";
$this->_brandemail = $_ENV['MAIL_FROM_ADDRESS'] ?? "noreply@example.com";
}
private function setupDebug() {
if (!empty($_SERVER["HTTP_CLIENT_IP"])) {
$this->_clientIp = $_SERVER["HTTP_CLIENT_IP"];
} elseif (!empty($_SERVER["HTTP_X_FORWARDED_FOR"])) {
$this->_clientIp = $_SERVER["HTTP_X_FORWARDED_FOR"];
} else {
$this->_clientIp = $_SERVER["REMOTE_ADDR"];
}
$http_host = $_SERVER['HTTP_HOST'] ?? '';
$isDev = strpos($http_host, 'dcleo.unikoffice.com') !== false;
$isRecette = strpos($http_host, 'rcleo.unikoffice.com') !== false;
$isDebugEnv = $_ENV['APP_DEBUG'] === 'true' || $_ENV['APP_ENV'] === 'development';
if ($isDev || $isRecette || $isDebugEnv) {
ini_set('error_reporting', -1);
ini_set('display_errors', '1');
$this->_devIp = true;
$this->_debug_level = 4;
$this->_log_sql = true;
$this->_log_performance = true;
$this->_log_file_path = dirname(__DIR__) . '/log/' . $this->_appname . '_debug_' . date('Y-m-d') . '.log';
ini_set('log_errors', '1');
ini_set('error_log', $this->_log_file_path);
ini_set('display_startup_errors', '1');
} else {
ini_set('error_reporting', 0);
ini_set('display_errors', '0');
ini_set('log_errors', '0');
$this->_debug_level = 0;
$this->_log_sql = false;
$this->_log_performance = false;
}
}
public function debug($data, $type = 'DEBUG', $level = 3) {
if ($this->_debug_level < $level) return;
$levels = ['ERROR', 'WARNING', 'INFO', 'DEBUG'];
$timestamp = date('Y-m-d H:i:s');
$message = "[$timestamp] [$type] " . (is_array($data) ? json_encode($data) : $data) . PHP_EOL;
if ($this->_log_file_path) {
error_log($message, 3, $this->_log_file_path);
}
if ($this->_devIp && ini_get('display_errors')) {
echo "<!-- $message -->\n";
}
}
}

View File

@@ -1,148 +0,0 @@
<?php
class Conf
{
const admin = 1; // TRUE ou FALSE pour indiquer si l'application est admin ou non
const intra = 1; // Est-ce un intranet privé TRUE 1, ou un site public FALSE 0
const erp = 1; //! Est-ce un ERP ? Utile pour la gestion documentaire avec les paths spéciaux pour l'ERP
const magazine = 0; //! Est-ce qu'on veut transformer les PDF en JPG pour la lecture Magazine dans le d6tools.upload ?
public $_appname;
public $_appscript;
public $_appversion;
public $_appenv;
public $_apptitle;
public $_brandname;
public $_brandadresse1;
public $_brandadresse2;
public $_brandcp;
public $_brandville;
public $_brandtel;
public $_brandemail;
public $_brandlogo;
public $_brandgroupe;
public $_brandmulti;
public $_piwikid;
public $_googlid;
public $_excludeIp = "82.67.142.214"; //! IP à exclure pour le comptage des visites et pour le debug
public $_clientIp;
public $_devIp = false;
//! Configuration du debug
public $_debug_level = 0; //! 0=off, 1=errors, 2=warnings, 3=info, 4=debug
public $_log_sql = false; //! Logger les requêtes SQL
public $_log_performance = false; //! Logger les temps d'exécution
public $_log_file_path = ''; //! Chemin du fichier de log
public $_pathupload = "/pub/files/upload/"; //! le path de base pour les uploads
//! les infos de connexion de la base de données
public $_dbhost = 'localhost';
public $_dbname = 'uof_frontal';
public $_dbuser = 'uof_front_user';
public $_dbpass = 'd66,UnikOffice.User';
public $_dbghost = 'localhost';
public $_dbgname = '';
public $_dbguser = 'uof_linet_user';
public $_dbgpass = 'd66,UOF-LinetRH.User';
public $_dbuhost = 'localhost';
public $_dbuname = '';
public $_dbuuser = 'uof_linet_user';
public $_dbupass = 'd66,UOF-LinetRH.User';
public $_tbusers = ""; // Spécifie la table des users de cette application, par défaut uof_frontal.users, mais sur Linet c'est dans uof_linet.commerciaux
//! les infos de l'entité de l'utilisateur
public $_entite = '';
//! indique si c'est une nouvelle version pour les tests de nouveaux modules et librairies
public $_new_version = false;
public function __construct()
{
//! on va chercher la configuration de l'application dans la table ce_frontal.y_conf
$mysqli = new mysqli($this->_dbhost, $this->_dbuser, $this->_dbpass, $this->_dbname);
$sql = 'SELECT * FROM y_conf WHERE admin=' . self::admin . ' AND active=1 LIMIT 1;';
$mysqli->set_charset("utf8");
$res = $mysqli->query($sql);
$resconf = $res->fetch_assoc();
$this->_appenv = $resconf["appenv"];
$this->_appversion = "2.0.1";
$this->_appscript = $resconf["appscript"]; //! le script à appeler par défaut si l'utilisateur n'est pas reconnu
$this->_brandgroupe = $resconf["brandgroupe"];
$this->_brandmulti = $resconf["brandmulti"];
//! On va chercher les infos de base de cette appname dans ce_frontal.users_entites en fonction du http_host
$http_host = $_SERVER['HTTP_HOST'];
error_log("http_host : ".$http_host);
$sql = 'SELECT * FROM users_entites WHERE http_host LIKE "%' . $http_host . '%" AND active=1 LIMIT 1;';
$res = $mysqli->query($sql);
$mysqli->close();
$resentite = $res->fetch_assoc();
if (empty($resentite)) {
//! on ne trouve pas ce http_host, on part sur la demo
$this->_appname = "udo_demo";
$mysqli = new mysqli($this->_dbhost, $this->_dbuser, $this->_dbpass, $this->_dbname);
$sql = 'SELECT * FROM users_entites WHERE rowid=1;'; // appname="' . $this->_appname . '" AND active=1 LIMIT 1;';
$res = $mysqli->query($sql);
$mysqli->close();
$resentite = $res->fetch_assoc();
}
$this->_entite = $resentite;
$this->_appname = $resentite["appname"];
$this->_apptitle = $resentite["libelle"];
$this->_brandname = $resentite["libelle"];
$this->_brandadresse1 = $resentite["adresse1"];
$this->_brandadresse2 = $resentite["adresse2"];
$this->_brandcp = $resentite["cp"];
$this->_brandville = $resentite["ville"];
$this->_brandtel = $resentite["tel1"];
$this->_brandemail = $resentite["email"];
$this->_brandlogo = $resentite["appname"];
$this->_dbgname = $resentite["groupebase"];
$this->_dbuname = $resentite["genbase"];
$this->_tbusers = $resentite["table_users_gen"]; //! Spécifie la table des users de cette application, par défaut dans uof_frontal.users
if (!empty($_SERVER["HTTP_CLIENT_IP"])) {
$this->_clientIp = $_SERVER["HTTP_CLIENT_IP"];
} elseif (!empty($_SERVER["HTTP_X_FORWARDED_FOR"])) {
$this->_clientIp = $_SERVER["HTTP_X_FORWARDED_FOR"];
} else {
$this->_clientIp = $_SERVER["REMOTE_ADDR"];
}
// Active le debug uniquement pour dev et recette
if (strpos($http_host, 'dcleo.unikoffice.com') !== false || strpos($http_host, 'rcleo.unikoffice.com') !== false) {
ini_set('error_reporting', -1);
ini_set('display_errors', '1');
$this->_devIp = true;
// Configuration avancée du debug pour dev/recette
$this->_debug_level = 4; // Niveau debug complet
$this->_log_sql = true; // Logger les requêtes SQL
$this->_log_performance = true; // Mesurer les performances
$this->_log_file_path = dirname(__DIR__) . '/log/' . $this->_appname . '_debug_' . date('Y-m-d') . '.log';
// Options PHP supplémentaires pour le debug
ini_set('log_errors', '1');
ini_set('error_log', $this->_log_file_path);
ini_set('display_startup_errors', '1');
ini_set('track_errors', '1');
ini_set('html_errors', '1');
ini_set('xmlrpc_errors', '0');
} else {
ini_set('error_reporting', 0);
ini_set('display_errors', '0');
ini_set('log_errors', '0');
$this->_debug_level = 0;
$this->_log_sql = false;
$this->_log_performance = false;
}
}
}

245
controllers/cjxcontacts.php Normal file
View File

@@ -0,0 +1,245 @@
<?php
//! Page des requêtes AJAX pour la gestion des contacts clients
global $Session;
global $Route;
$fk_user = $Session->_user["rowid"];
switch ($Route->_action) {
case "load_contacts":
// Charge tous les contacts d'un client
$data = json_decode(file_get_contents("php://input"));
if (isset($data->fk_client)) {
$fk_client = nettoie_input($data->fk_client);
$fkClientSafe = intval($fk_client);
try {
$db = Database::getInstance();
$sql = 'SELECT rowid, nom, prenom, fonction, telephone, mobile, email, principal, active
FROM clients_contacts
WHERE fk_client = :fk_client AND active = 1
ORDER BY principal DESC, nom, prenom';
$contacts = $db->fetchAll($sql, [':fk_client' => $fkClientSafe]);
echo json_encode($contacts);
} catch (Exception $e) {
error_log("Erreur load_contacts : " . $e->getMessage());
echo json_encode(array('ret' => 'ko', 'msg' => 'Erreur lors du chargement des contacts'));
}
} else {
echo json_encode(array('ret' => 'ko', 'msg' => 'Client non spécifié'));
}
break;
case "load_contact":
// Charge un contact spécifique
$data = json_decode(file_get_contents("php://input"));
if (isset($data->rowid)) {
$rowid = nettoie_input($data->rowid);
$rowidSafe = intval($rowid);
try {
$db = Database::getInstance();
$sql = 'SELECT rowid, fk_client, nom, prenom, fonction, telephone, mobile, email, principal, active
FROM clients_contacts
WHERE rowid = :rowid';
$contact = $db->fetchAll($sql, [':rowid' => $rowidSafe]);
if (count($contact) == 1) {
echo json_encode($contact[0]);
} else {
echo json_encode(array('ret' => 'ko', 'msg' => 'Contact introuvable'));
}
} catch (Exception $e) {
error_log("Erreur load_contact : " . $e->getMessage());
echo json_encode(array('ret' => 'ko', 'msg' => 'Erreur lors du chargement du contact'));
}
} else {
echo json_encode(array('ret' => 'ko', 'msg' => 'Contact non spécifié'));
}
break;
case "save_contact":
// Crée ou met à jour un contact
if ($_POST) {
$rowid = nettoie_input($_POST["rowid"]);
$fk_client = nettoie_input($_POST["fk_client"]);
$nom = nettoie_input($_POST["nom"]);
$prenom = nettoie_input($_POST["prenom"]);
$fonction = nettoie_input($_POST["fonction"]);
$telephone = formattel(nettoie_input($_POST["telephone"]));
$mobile = formattel(nettoie_input($_POST["mobile"]));
$email = nettoie_input($_POST["email"]);
$principal = 0;
if (isset($_POST["principal"]) && $_POST["principal"] == "1") {
$principal = 1;
}
$fkClientSafe = intval($fk_client);
$fkUserSafe = intval($fk_user);
try {
$db = Database::getInstance();
if ($rowid == 0) {
// Création d'un nouveau contact
// Si ce contact est marqué comme principal, on retire le flag principal des autres contacts du client
if ($principal == 1) {
$sqlUpdate = 'UPDATE clients_contacts SET principal = 0 WHERE fk_client = :fk_client';
$db->query($sqlUpdate, [':fk_client' => $fkClientSafe]);
}
$data = [
'fk_client' => $fkClientSafe,
'nom' => $nom,
'prenom' => $prenom,
'fonction' => $fonction,
'telephone' => $telephone,
'mobile' => $mobile,
'email' => $email,
'principal' => $principal,
'active' => 1,
'date_creat' => date("Y-m-d H:i:s"),
'fk_user_creat' => $fkUserSafe
];
$newId = $db->insert('clients_contacts', $data);
eLog("Contact créé avec l'ID : " . $newId);
echo json_encode(array('ret' => 'ok', 'msg' => 'Contact créé avec succès', 'rowid' => $newId));
} else {
// Mise à jour d'un contact existant
$rowidSafe = intval($rowid);
// Si ce contact est marqué comme principal, on retire le flag principal des autres contacts du client
if ($principal == 1) {
$sqlUpdate = 'UPDATE clients_contacts SET principal = 0 WHERE fk_client = :fk_client AND rowid != :rowid';
$db->query($sqlUpdate, [':fk_client' => $fkClientSafe, ':rowid' => $rowidSafe]);
}
$sql = 'UPDATE clients_contacts
SET nom = :nom, prenom = :prenom, fonction = :fonction, telephone = :telephone,
mobile = :mobile, email = :email, principal = :principal,
date_modif = :date_modif, fk_user_modif = :fk_user_modif
WHERE rowid = :rowid';
$params = [
':nom' => $nom,
':prenom' => $prenom,
':fonction' => $fonction,
':telephone' => $telephone,
':mobile' => $mobile,
':email' => $email,
':principal' => $principal,
':date_modif' => date("Y-m-d H:i:s"),
':fk_user_modif' => $fkUserSafe,
':rowid' => $rowidSafe
];
$db->query($sql, $params);
eLog("Contact mis à jour : " . $rowidSafe);
echo json_encode(array('ret' => 'ok', 'msg' => 'Contact mis à jour avec succès', 'rowid' => $rowidSafe));
}
} catch (Exception $e) {
error_log("Erreur save_contact : " . $e->getMessage());
echo json_encode(array('ret' => 'ko', 'msg' => 'Erreur lors de l\'enregistrement du contact'));
}
}
break;
case "delete_contact":
// Supprime (désactive) un contact
$data = json_decode(file_get_contents("php://input"));
if (isset($data->rowid)) {
$rowid = nettoie_input($data->rowid);
$rowidSafe = intval($rowid);
try {
$db = Database::getInstance();
// Vérifier qu'il reste au moins un autre contact actif pour ce client
$sqlCheck = 'SELECT cc.fk_client, COUNT(*) as nb_contacts
FROM clients_contacts cc
WHERE cc.rowid = :rowid';
$result = $db->fetchAll($sqlCheck, [':rowid' => $rowidSafe]);
if (count($result) == 1) {
$fkClient = $result[0]['fk_client'];
// Compter les contacts actifs restants
$sqlCount = 'SELECT COUNT(*) as nb FROM clients_contacts WHERE fk_client = :fk_client AND active = 1 AND rowid != :rowid';
$countResult = $db->fetchAll($sqlCount, [':fk_client' => $fkClient, ':rowid' => $rowidSafe]);
if ($countResult[0]['nb'] == 0) {
echo json_encode(array('ret' => 'ko', 'msg' => 'Impossible de supprimer le dernier contact actif du client'));
} else {
// Désactiver le contact
$fkUserSafe = intval($fk_user);
$sql = 'UPDATE clients_contacts
SET active = 0, date_modif = :date_modif, fk_user_modif = :fk_user_modif
WHERE rowid = :rowid';
$params = [
':date_modif' => date("Y-m-d H:i:s"),
':fk_user_modif' => $fkUserSafe,
':rowid' => $rowidSafe
];
$db->query($sql, $params);
eLog("Contact désactivé : " . $rowidSafe);
echo json_encode(array('ret' => 'ok', 'msg' => 'Contact supprimé avec succès'));
}
} else {
echo json_encode(array('ret' => 'ko', 'msg' => 'Contact introuvable'));
}
} catch (Exception $e) {
error_log("Erreur delete_contact : " . $e->getMessage());
echo json_encode(array('ret' => 'ko', 'msg' => 'Erreur lors de la suppression du contact'));
}
} else {
echo json_encode(array('ret' => 'ko', 'msg' => 'Contact non spécifié'));
}
break;
case "set_principal":
// Définit un contact comme principal
$data = json_decode(file_get_contents("php://input"));
if (isset($data->rowid) && isset($data->fk_client)) {
$rowid = nettoie_input($data->rowid);
$fk_client = nettoie_input($data->fk_client);
$rowidSafe = intval($rowid);
$fkClientSafe = intval($fk_client);
try {
$db = Database::getInstance();
$fkUserSafe = intval($fk_user);
// Retirer le flag principal de tous les contacts du client
$sqlReset = 'UPDATE clients_contacts SET principal = 0 WHERE fk_client = :fk_client';
$db->query($sqlReset, [':fk_client' => $fkClientSafe]);
// Définir le contact comme principal
$sql = 'UPDATE clients_contacts
SET principal = 1, date_modif = :date_modif, fk_user_modif = :fk_user_modif
WHERE rowid = :rowid';
$params = [
':date_modif' => date("Y-m-d H:i:s"),
':fk_user_modif' => $fkUserSafe,
':rowid' => $rowidSafe
];
$db->query($sql, $params);
eLog("Contact principal défini : " . $rowidSafe);
echo json_encode(array('ret' => 'ok', 'msg' => 'Contact principal défini avec succès'));
} catch (Exception $e) {
error_log("Erreur set_principal : " . $e->getMessage());
echo json_encode(array('ret' => 'ko', 'msg' => 'Erreur lors de la définition du contact principal'));
}
} else {
echo json_encode(array('ret' => 'ko', 'msg' => 'Données manquantes'));
}
break;
default:
echo json_encode(array('ret' => 'ko', 'msg' => 'Action inconnue'));
break;
}
exit();

View File

@@ -54,7 +54,7 @@ switch ($Route->_action) {
//! 4. On met à jour la date_demande, date_remise, num_opportunite et fk_statut_devis du nouveau devis
$newRowidSafe = intval($newRowid);
$sql = 'UPDATE devis SET date_demande = "' . date("Y-m-d H:i:s") . '", date_remise = "", num_opportunite = "", fk_statut_devis = 1 WHERE rowid = ' . $newRowidSafe . ';';
$sql = 'UPDATE devis SET date_demande = "' . date("Y-m-d H:i:s") . '", date_remise = NULL, num_opportunite = "", fk_statut_devis = 1 WHERE rowid = ' . $newRowidSafe . ';';
eLog($sql);
qSQL($sql, "gen");
@@ -124,14 +124,16 @@ switch ($Route->_action) {
$sql = 'UPDATE devis SET chk_maj = 0 WHERE rowid = ' . $cidSafe . ';';
qSQL($sql, "gen");
}
$sql = 'SELECT d.rowid, d.fk_user, d.fk_client, d.fk_marche, m.libelle AS lib_marche, d.fk_statut_devis, d.dossier, d.num_opportunite, d.montant_total_ht, ';
$sql = 'SELECT d.rowid, d.fk_user, d.fk_client, d.fk_contact, d.fk_marche, m.libelle AS lib_marche, d.fk_statut_devis, d.dossier, d.num_opportunite, d.montant_total_ht, ';
$sql .= 'd.date_demande, d.date_remise, d.montant_total_ht_remise, d.marge_totale, d.commentaire, d.comment_devis, d.chk_clients_secteur, d.chk_speciaux, ';
$sql .= 'd.lib_new_client, d.type_new_client, d.adresse1_new_client, d.adresse2_new_client, d.adresse3_new_client, d.cp_new_client, d.ville_new_client,';
$sql .= 'd.contact_new_nom, d.contact_new_prenom, d.contact_new_fonction, d.new_telephone, d.new_mobile, d.new_email, ';
$sql .= 'd.comment_geste_comm, d.comment_validat, d.chk_validat, d.fk_user_validat, d.date_validat, ';
$sql .= 'xs.libelle as lib_statut_devis, c.code, c.libelle, c.adresse1, c.adresse2, c.adresse3, c.cp, c.ville, c.type_client, ';
$sql .= 'c.contact_nom, c.contact_prenom, c.contact_fonction, c.telephone, c.mobile, c.email, d.chk_devis_photos ';
$sql .= 'FROM devis d LEFT JOIN clients c ON d.fk_client = c.rowid LEFT JOIN x_statuts_devis xs ON d.fk_statut_devis = xs.rowid ';
$sql .= 'cc.rowid as contact_rowid, cc.nom as contact_nom, cc.prenom as contact_prenom, cc.fonction as contact_fonction, cc.telephone, cc.mobile, cc.email, d.chk_devis_photos ';
$sql .= 'FROM devis d LEFT JOIN clients c ON d.fk_client = c.rowid ';
$sql .= 'LEFT JOIN clients_contacts cc ON d.fk_contact = cc.rowid ';
$sql .= 'LEFT JOIN x_statuts_devis xs ON d.fk_statut_devis = xs.rowid ';
$sql .= 'LEFT JOIN marches m ON d.fk_marche = m.rowid ';
$sql .= 'WHERE d.rowid = ' . $cidSafe . ';';
echo getinfos($sql, "gen", "json");
@@ -162,7 +164,7 @@ switch ($Route->_action) {
if (isset($data->secteur)) {
$chkSecteur = nettoie_input($data->secteur);
$fkUser = nettoie_input($data->user);
$sql = 'SELECT rowid, libelle, CONCAT(libelle, ", ", adresse1, ", ", cp, " ", ville) AS rech, adresse1, adresse2, adresse3, cp, ville, contact_nom, contact_prenom, contact_fonction, telephone, mobile, email, type_client FROM clients WHERE active=1 ';
$sql = 'SELECT rowid, code, libelle, CONCAT(libelle, ", ", adresse1, ", ", cp, " ", ville) AS rech, adresse1, adresse2, adresse3, cp, ville, contact_nom, contact_prenom, contact_fonction, telephone, mobile, email, type_client FROM clients WHERE active=1 ';
if ($chkSecteur == "1") {
//! on ne prend que les clients du secteur de l'utilisateur
$fkUserSafe = intval($fkUser);
@@ -197,6 +199,32 @@ switch ($Route->_action) {
}
break;
case "load_produits_mercurial":
//! Charge les produits du marché hybride pour l'onglet Mercurial
$data = json_decode(file_get_contents("php://input"));
if (isset($data->fk_marche)) {
$fkMarche = nettoie_input($data->fk_marche);
$fkMarcheSafe = intval($fkMarche);
// Vérifier que le marché est bien hybride
$sql = 'SELECT chk_marche_hybride FROM marches WHERE rowid = ' . $fkMarcheSafe . ';';
$retSql = getinfos($sql, "gen");
if (count($retSql) == 1 && $retSql[0]["chk_marche_hybride"] == 1) {
// Le marché est hybride, on charge tous les produits de ce marché (hors 999)
$sql = 'SELECT p.*, CONCAT(p.code, " - ", p.libelle) AS rech, pf.fk_famille, xf.libelle AS lib_famille ';
$sql .= 'FROM produits p LEFT JOIN produits_familles pf ON p.groupe=pf.groupe LEFT JOIN x_familles xf on pf.fk_famille = xf.rowid ';
$sql .= 'WHERE p.fk_marche = ' . $fkMarcheSafe . ' AND p.fk_marche != 999 AND p.active=1 ORDER BY xf.ordre, pf.ordre;';
echo getinfos($sql, "gen", "json");
} else {
// Le marché n'est pas hybride, on retourne un tableau vide
echo json_encode(array());
}
} else {
echo json_encode(array());
}
break;
case "load_devis_marche_produits":
//! Charge les produits enregistrés pour un marché
$data = json_decode(file_get_contents("php://input"));
@@ -234,7 +262,7 @@ switch ($Route->_action) {
$sql = 'SELECT p.*, CONCAT(p.code, " - ", p.libelle) AS rech, pf.fk_famille, xf.libelle AS lib_famille, "0" AS chk_prix_net ';
$sql .= 'FROM produits p LEFT JOIN produits_familles pf ON p.groupe=pf.groupe LEFT JOIN x_familles xf on pf.fk_famille = xf.rowid ';
$sql .= 'WHERE p.fk_marche = ' . $cidSafe . ' AND p.active=1 ORDER BY xf.ordre, pf.ordre;';
$sql .= 'WHERE p.fk_marche = ' . intval($cid) . ' AND p.active=1 ORDER BY xf.ordre, pf.ordre;';
$upls = getinfos($sql, "gen");
if ($cid != "999") {
@@ -390,31 +418,13 @@ switch ($Route->_action) {
$commentaire = nettoie_input($_POST["commentaire"]);
$newCommentaire = 0;
$contact_nom = nettoie_input($_POST["contact_nom"]);
$contact_prenom = nettoie_input($_POST["contact_prenom"]);
$contact_fonction = nettoie_input($_POST["contact_fonction"]);
$email = nettoie_input($_POST["email"]);
$telephone = formattel(nettoie_input($_POST["telephone"]));
$mobile = formattel(nettoie_input($_POST["mobile"]));
// Récupération du contact sélectionné
$fk_contact = isset($_POST["fk_contact"]) ? intval($_POST["fk_contact"]) : 0;
if ($fk_contact == 0) $fk_contact = NULL;
$set = 'fk_client=' . $fk_client . ', num_opportunite="' . $num_opportunite . '", date_demande="' . $date_demande . '", date_remise="' . $date_remise . '", ';
$set = 'fk_client=' . $fk_client . ', fk_contact=' . ($fk_contact === NULL ? 'NULL' : $fk_contact) . ', num_opportunite="' . $num_opportunite . '", date_demande="' . $date_demande . '", date_remise="' . $date_remise . '", ';
$set .= 'fk_user=' . $fk_user . ', fk_marche=' . $fk_marche . ', commentaire="' . $commentaire . '", chk_devis_photos=' . $chk_devis_photos . ', ';
if ($fk_client == 0) {
//! C'est un nouveau client : on enregistre ces infos et celle du contact dans le devis et non au niveau de la table clients
$libNewClient = nettoie_input($_POST["lib_client"]);
$typNewClient = nettoie_input($_POST["type_client"]);
$adr1NewClient = nettoie_input($_POST["adresse1"]);
$adr2NewClient = nettoie_input($_POST["adresse2"]);
$adr3NewClient = nettoie_input($_POST["adresse3"]);
$cpNewClient = nettoie_input($_POST["cp"]);
// Si le CP a une longueur de 4, on rajoute un 0 devant
if (strlen($cpNewClient) == 4) $cpNewClient = "0" . $cpNewClient;
$villeNewClient = nettoie_input($_POST["ville"]);
$set .= 'lib_new_client="' . $libNewClient . '", type_new_client="' . $typNewClient . '", adresse1_new_client="' . $adr1NewClient . '", adresse2_new_client="' . $adr2NewClient . '", adresse3_new_client="' . $adr3NewClient . '", cp_new_client="' . $cpNewClient . '", ville_new_client="' . $villeNewClient . '", ';
$set .= 'contact_new_nom="' . $contact_nom . '", contact_new_prenom="' . $contact_prenom . '", contact_new_fonction="' . $contact_fonction . '", new_email="' . $email . '", new_telephone="' . $telephone . '", new_mobile="' . $mobile . '", ';
}
if ($_POST["rowid"] == 0) {
//! C'est un nouveau devis
//! On le range dans un dossier
@@ -465,14 +475,8 @@ switch ($Route->_action) {
}
eLog('Entete Devis Save : ' . $sql);
if ($fk_client != "0") {
//! On sauvegarde aussi les infos complémentaires du client qui peuvent ête mises à jour
$sql = 'UPDATE clients SET contact_nom="' . $contact_nom . '", contact_prenom="' . $contact_prenom . '", contact_fonction="' . $contact_fonction . '", ';
$fkClientSafe = intval($fk_client);
$sql .= 'email="' . $email . '", telephone="' . $telephone . '", mobile="' . $mobile . '" WHERE rowid=' . $fkClientSafe . ';';
eLog('Entete Devis Save infos client : ' . $sql);
qSQL($sql, "gen");
}
// NOTE: Les contacts sont maintenant gérés via la table clients_contacts
// et non plus directement dans la table clients
// On inscrit l'enregistrement dans le journal si il y a eu un changement de commentaire ou bien si c'est une création avec commentaire
if ($newCommentaire > 0) {
@@ -651,11 +655,8 @@ switch ($Route->_action) {
try {
$db = Database::getInstance();
$sql = 'SELECT rowid, code, libelle, prix_vente FROM produits WHERE active=1 AND (code LIKE :term OR libelle LIKE :term) ORDER BY code';
$stmt = $db->prepare($sql);
$termParam = '%' . $term . '%';
$stmt->bindParam(':term', $termParam, PDO::PARAM_STR);
$stmt->execute();
$upls = $stmt->fetchAll(PDO::FETCH_ASSOC);
$upls = $db->fetchAll($sql, [':term' => $termParam]);
} catch (Exception $e) {
error_log("Erreur recherche produits : " . $e->getMessage());
$upls = [];
@@ -1303,5 +1304,344 @@ switch ($Route->_action) {
echo json_encode(array("success" => "true", "message" => "Devis refusé avec succès"));
}
break;
case "save_new_client":
$data = json_decode(file_get_contents("php://input"));
if (isset($data->libelle)) {
try {
$db = Database::getInstance();
$libelle = nettoie_input($data->libelle);
$typeClient = nettoie_input($data->type_client);
$adresse1 = nettoie_input($data->adresse1);
$adresse2 = nettoie_input($data->adresse2);
$adresse3 = nettoie_input($data->adresse3);
$cp = nettoie_input($data->cp);
if (strlen($cp) == 4) $cp = "0" . $cp;
$ville = nettoie_input($data->ville);
$fkUserSafe = intval($fk_user);
$sqlMaxCode = 'SELECT MAX(code) as max_code FROM clients';
$resultCode = $db->fetchOne($sqlMaxCode);
$newCode = ($resultCode && $resultCode['max_code']) ? intval($resultCode['max_code']) + 1 : 1;
$sql = 'INSERT INTO clients SET code = :code, libelle = :libelle, type_client = :type_client, adresse1 = :adresse1, adresse2 = :adresse2, adresse3 = :adresse3, ';
$sql .= 'cp = :cp, ville = :ville, fk_user_creat = :fk_user_creat, date_creat = NOW(), active = 1';
$params = [
':code' => $newCode,
':libelle' => $libelle,
':type_client' => $typeClient,
':adresse1' => $adresse1,
':adresse2' => $adresse2,
':adresse3' => $adresse3,
':cp' => $cp,
':ville' => $ville,
':fk_user_creat' => $fkUserSafe
];
$db->query($sql, $params);
$newClientId = $db->lastInsertId();
if ($newClientId > 0) {
$sql = 'INSERT INTO clients_contacts SET fk_client = :fk_client, nom = :nom, prenom = :prenom, principal = 1, active = 1, date_creat = NOW()';
$params = [
':fk_client' => $newCode,
':nom' => 'À compléter',
':prenom' => ''
];
$db->query($sql, $params);
eLog("Nouveau client créé : ID=" . $newClientId . ", code=" . $newCode);
echo json_encode(array("success" => true, "rowid" => $newClientId, "code" => $newCode, "message" => "Client créé avec succès"));
} else {
eLog("save_new_client ERREUR: newClientId = 0");
echo json_encode(array("success" => false, "message" => "Erreur lors de la création du client - ID non récupéré"));
}
} catch (Exception $e) {
$errorMsg = $e->getMessage();
error_log("Erreur création client : " . $errorMsg);
eLog("save_new_client EXCEPTION: " . $errorMsg);
echo json_encode(array("success" => false, "message" => "Erreur : " . $errorMsg));
}
}
break;
case "search_devis":
eLog("=== search_devis case appelé ===");
$rawData = file_get_contents("php://input");
eLog("Raw data: " . $rawData);
$data = json_decode($rawData);
eLog("Data decoded: " . print_r($data, true));
eLog("isset term: " . (isset($data->term) ? 'YES' : 'NO'));
if (isset($data->term)) {
$term = nettoie_input($data->term);
$context = nettoie_input($data->context);
if (strlen($term) < 3) {
echo json_encode(array("success" => false, "message" => "Le terme de recherche doit contenir au moins 3 caractères"));
break;
}
$termSafe = '%' . $term . '%';
$whereParams = [];
switch ($fk_role) {
case 1:
$whereRole = 'd.fk_user = :fkUser OR d.fk_statut_devis >= 2';
$whereParams[':fkUser'] = $fk_user;
break;
case 2:
try {
$db = Database::getInstance();
$sql = 'SELECT rowid FROM users WHERE fk_parent = :fkParent';
$aRR = $db->fetchAll($sql, [':fkParent' => $fk_user]);
$rrIds = array_column($aRR, 'rowid');
if (!empty($rrIds)) {
$placeholders = [];
foreach ($rrIds as $index => $id) {
$placeholder = ':rr' . $index;
$placeholders[] = $placeholder;
$whereParams[$placeholder] = $id;
}
$whereRole = 'd.fk_user = :fkUser OR (d.fk_statut_devis >= 3 AND d.fk_user IN (' . implode(',', $placeholders) . '))';
$whereParams[':fkUser'] = $fk_user;
} else {
$whereRole = 'd.fk_user = :fkUser';
$whereParams[':fkUser'] = $fk_user;
}
} catch (Exception $e) {
error_log("Erreur récupération RR : " . $e->getMessage());
$whereRole = 'd.fk_user = :fkUser';
$whereParams[':fkUser'] = $fk_user;
}
break;
default:
$whereRole = 'd.fk_user = :fkUser';
$whereParams[':fkUser'] = $fk_user;
break;
}
if ($context === "archives") {
$whereStatut = ' AND d.fk_statut_devis = 20';
} else {
$whereStatut = ' AND d.fk_statut_devis != 20';
}
$whereParams[':term1'] = $termSafe;
$whereParams[':term2'] = $termSafe;
$whereParams[':term3'] = $termSafe;
$whereParams[':term4'] = $termSafe;
$whereParams[':term5'] = $termSafe;
$whereParams[':term6'] = $termSafe;
$whereParams[':term7'] = $termSafe;
$whereParams[':term8'] = $termSafe;
$whereParams[':term9'] = $termSafe;
$whereParams[':term10'] = $termSafe;
$whereParams[':term11'] = $termSafe;
$whereParams[':term12'] = $termSafe;
$whereParams[':term13'] = $termSafe;
$whereParams[':term14'] = $termSafe;
$whereParams[':term15'] = $termSafe;
$whereParams[':term16'] = $termSafe;
$whereParams[':term17'] = $termSafe;
try {
$db = Database::getInstance();
$sql = 'SELECT DISTINCT d.rowid, d.dossier, d.date_demande, d.date_remise, d.num_opportunite, d.fk_client, d.montant_total_ht_remise, d.marge_totale, d.commentaire, d.chk_speciaux, c.libelle, c.ville, c.cp, d.fk_statut_devis, ';
$sql .= 'd.lib_new_client, d.type_new_client, d.adresse1_new_client, d.adresse2_new_client, d.adresse3_new_client, d.cp_new_client, d.ville_new_client, d.comment_devis, d.comment_geste_comm, ';
$sql .= 'd.contact_new_nom, d.contact_new_prenom, d.new_telephone, d.new_mobile, d.new_email, d.contact_new_fonction, LEFT(u.prenom,1) AS prenom, u.libelle as nom, ';
$sql .= 'xs.libelle as lib_statut, d.chk_new_statut, m.libelle as lib_marche, d.chk_validat, d.fk_user_validat, d.date_validat ';
$sql .= 'FROM devis d ';
$sql .= 'LEFT JOIN clients c ON c.rowid=d.fk_client ';
$sql .= 'LEFT JOIN x_statuts_devis xs ON xs.rowid=d.fk_statut_devis ';
$sql .= 'LEFT JOIN marches m ON m.rowid=d.fk_marche ';
$sql .= 'LEFT JOIN users u ON u.rowid=d.fk_user ';
$sql .= 'LEFT JOIN clients_contacts ct ON ct.fk_client=d.fk_client ';
$sql .= 'WHERE (' . $whereRole . ')' . $whereStatut . ' AND (';
$sql .= 'd.rowid LIKE :term1 OR ';
$sql .= 'c.libelle LIKE :term2 OR ';
$sql .= 'c.adresse1 LIKE :term3 OR ';
$sql .= 'c.adresse2 LIKE :term4 OR ';
$sql .= 'c.adresse3 LIKE :term5 OR ';
$sql .= 'c.cp LIKE :term6 OR ';
$sql .= 'c.ville LIKE :term7 OR ';
$sql .= 'm.libelle LIKE :term8 OR ';
$sql .= 'd.num_opportunite LIKE :term9 OR ';
$sql .= 'd.lib_new_client LIKE :term10 OR ';
$sql .= 'd.cp_new_client LIKE :term11 OR ';
$sql .= 'd.ville_new_client LIKE :term12 OR ';
$sql .= 'ct.nom LIKE :term13 OR ';
$sql .= 'ct.prenom LIKE :term14 OR ';
$sql .= 'ct.fonction LIKE :term15 OR ';
$sql .= 'ct.email LIKE :term16 OR ';
$sql .= 'd.commentaire LIKE :term17) ';
$sql .= 'ORDER BY d.dossier, d.date_remise DESC';
eLog("=== SEARCH DEVIS DEBUG ===");
eLog("Terme recherché: " . $term);
eLog("Context: " . $context);
eLog("SQL: " . $sql);
eLog("Params: " . print_r($whereParams, true));
$pdo = $db->getPDO();
$stmt = $pdo->prepare($sql);
$stmt->execute($whereParams);
$devis = $stmt->fetchAll(PDO::FETCH_ASSOC);
eLog("Nombre de devis trouvés: " . count($devis));
$nb_devis = array();
foreach ($devis as $dev) {
if (!isset($nb_devis[$dev["fk_statut_devis"]])) {
$nb_devis[$dev["fk_statut_devis"]] = 1;
} else {
$nb_devis[$dev["fk_statut_devis"]]++;
}
}
$dossiers = array();
foreach ($devis as $dev) {
if (!in_array($dev["dossier"], array_column($dossiers, 'dossier'))) {
$dossiers[] = array("dossier" => $dev["dossier"]);
}
}
echo json_encode(array(
"success" => true,
"devis" => $devis,
"nb_devis" => $nb_devis,
"dossiers" => $dossiers
));
} catch (Exception $e) {
error_log("Erreur recherche devis : " . $e->getMessage());
echo json_encode(array("success" => false, "message" => "Erreur lors de la recherche : " . $e->getMessage()));
}
}
break;
case "search_devis_sap":
eLog("=== search_devis_sap case appelé ===");
$rawData = file_get_contents("php://input");
eLog("Raw data: " . $rawData);
$data = json_decode($rawData);
eLog("Data decoded: " . print_r($data, true));
eLog("isset term: " . (isset($data->term) ? 'YES' : 'NO'));
if (isset($data->term)) {
$term = nettoie_input($data->term);
$context = nettoie_input($data->context);
if (strlen($term) < 3) {
echo json_encode(array("success" => false, "message" => "Le terme de recherche doit contenir au moins 3 caractères"));
break;
}
$termSafe = '%' . $term . '%';
$whereParams = [];
$whereRole = '1=1';
if ($context === "archives") {
$whereStatut = ' AND d.fk_statut_devis = 20';
} else {
$whereStatut = ' AND d.fk_statut_devis != 20';
}
$whereParams[':term1'] = $termSafe;
$whereParams[':term2'] = $termSafe;
$whereParams[':term3'] = $termSafe;
$whereParams[':term4'] = $termSafe;
$whereParams[':term5'] = $termSafe;
$whereParams[':term6'] = $termSafe;
$whereParams[':term7'] = $termSafe;
$whereParams[':term8'] = $termSafe;
$whereParams[':term9'] = $termSafe;
$whereParams[':term10'] = $termSafe;
$whereParams[':term11'] = $termSafe;
$whereParams[':term12'] = $termSafe;
$whereParams[':term13'] = $termSafe;
$whereParams[':term14'] = $termSafe;
$whereParams[':term15'] = $termSafe;
$whereParams[':term16'] = $termSafe;
$whereParams[':term17'] = $termSafe;
try {
$db = Database::getInstance();
$sql = 'SELECT DISTINCT d.rowid, d.dossier, d.date_demande, d.date_remise, d.num_opportunite, d.fk_client, d.montant_total_ht_remise, d.marge_totale, d.commentaire, d.chk_speciaux, c.libelle, c.ville, c.cp, d.fk_statut_devis, ';
$sql .= 'd.lib_new_client, d.type_new_client, d.adresse1_new_client, d.adresse2_new_client, d.adresse3_new_client, d.cp_new_client, d.ville_new_client, d.comment_devis, d.comment_geste_comm, ';
$sql .= 'd.contact_new_nom, d.contact_new_prenom, d.new_telephone, d.new_mobile, d.new_email, d.contact_new_fonction, LEFT(u.prenom,1) AS prenom, u.libelle as nom, ';
$sql .= 'xs.libelle as lib_statut, d.chk_new_statut, m.libelle as lib_marche, d.chk_validat, d.fk_user_validat, d.date_validat, c.code ';
$sql .= 'FROM devis d ';
$sql .= 'LEFT JOIN clients c ON c.rowid=d.fk_client ';
$sql .= 'LEFT JOIN x_statuts_devis xs ON xs.rowid=d.fk_statut_devis ';
$sql .= 'LEFT JOIN marches m ON m.rowid=d.fk_marche ';
$sql .= 'LEFT JOIN users u ON u.rowid=d.fk_user ';
$sql .= 'LEFT JOIN clients_contacts ct ON ct.fk_client=d.fk_client ';
$sql .= 'WHERE (' . $whereRole . ')' . $whereStatut . ' AND (';
$sql .= 'd.rowid LIKE :term1 OR ';
$sql .= 'c.libelle LIKE :term2 OR ';
$sql .= 'c.adresse1 LIKE :term3 OR ';
$sql .= 'c.adresse2 LIKE :term4 OR ';
$sql .= 'c.adresse3 LIKE :term5 OR ';
$sql .= 'c.cp LIKE :term6 OR ';
$sql .= 'c.ville LIKE :term7 OR ';
$sql .= 'c.code LIKE :term8 OR ';
$sql .= 'm.libelle LIKE :term9 OR ';
$sql .= 'd.num_opportunite LIKE :term10 OR ';
$sql .= 'd.lib_new_client LIKE :term11 OR ';
$sql .= 'd.cp_new_client LIKE :term12 OR ';
$sql .= 'd.ville_new_client LIKE :term13 OR ';
$sql .= 'ct.nom LIKE :term14 OR ';
$sql .= 'ct.prenom LIKE :term15 OR ';
$sql .= 'ct.fonction LIKE :term16 OR ';
$sql .= 'ct.email LIKE :term17) ';
$sql .= 'ORDER BY d.dossier, d.date_remise DESC';
eLog("=== SEARCH DEVIS SAP DEBUG ===");
eLog("Terme recherché: " . $term);
eLog("Context: " . $context);
eLog("SQL: " . $sql);
eLog("Params: " . print_r($whereParams, true));
$pdo = $db->getPDO();
$stmt = $pdo->prepare($sql);
$stmt->execute($whereParams);
$devis = $stmt->fetchAll(PDO::FETCH_ASSOC);
eLog("Nombre de devis trouvés: " . count($devis));
$nb_devis = array();
foreach ($devis as $dev) {
if (!isset($nb_devis[$dev["fk_statut_devis"]])) {
$nb_devis[$dev["fk_statut_devis"]] = 1;
} else {
$nb_devis[$dev["fk_statut_devis"]]++;
}
}
$dossiers = array();
foreach ($devis as $dev) {
if (!in_array($dev["dossier"], array_column($dossiers, 'dossier'))) {
$dossiers[] = array("dossier" => $dev["dossier"]);
}
}
echo json_encode(array(
"success" => true,
"devis" => $devis,
"nb_devis" => $nb_devis,
"dossiers" => $dossiers
));
} catch (Exception $e) {
error_log("Erreur recherche devis SAP : " . $e->getMessage());
echo json_encode(array("success" => false, "message" => "Erreur lors de la recherche : " . $e->getMessage()));
}
}
break;
}
exit();

View File

@@ -49,6 +49,64 @@ function formate_date($sdate)
return $ladate;
}
function syncContactClient($code, $contactNom, $contactPrenom, $contactFonction, $telephone, $mobile, $email, $fkUser)
{
try {
$db = Database::getInstance();
// 1. Compter les contacts actifs pour ce client
$sql = 'SELECT COUNT(*) as nb FROM clients_contacts WHERE fk_client = :code AND active = 1';
$countResult = $db->fetchAll($sql, [':code' => $code]);
$nbContacts = $countResult[0]['nb'];
if ($nbContacts == 0) {
// Aucun contact : créer directement avec principal=1
$principal = 1;
} else {
// Des contacts existent : vérifier si ce nom+prénom existe (en MAJUSCULES)
$sql = 'SELECT rowid FROM clients_contacts
WHERE fk_client = :code
AND UPPER(nom) = UPPER(:nom)
AND UPPER(prenom) = UPPER(:prenom)
AND active = 1';
$existingContact = $db->fetchAll($sql, [
':code' => $code,
':nom' => $contactNom,
':prenom' => $contactPrenom
]);
if (count($existingContact) > 0) {
// Contact déjà présent : ne rien faire
eLog("syncContactClient : Contact existe déjà pour client " . $code);
return;
}
// Contact pas trouvé : créer avec principal=0
$principal = 0;
}
// Créer le contact
$sql = 'INSERT INTO clients_contacts SET fk_client = :code, nom = :nom, prenom = :prenom, fonction = :fonction, telephone = :telephone, mobile = :mobile, email = :email, principal = :principal, active = 1, date_creat = NOW(), fk_user_creat = :fk_user';
$db->query($sql, [
':code' => $code,
':nom' => $contactNom,
':prenom' => $contactPrenom,
':fonction' => $contactFonction,
':telephone' => $telephone,
':mobile' => $mobile,
':email' => $email,
':principal' => $principal,
':fk_user' => $fkUser
]);
eLog("syncContactClient : Contact créé pour client " . $code . " (principal=" . $principal . ")");
} catch (Exception $e) {
error_log("Erreur syncContactClient : " . $e->getMessage());
eLog("Erreur syncContactClient pour client " . $code . " : " . $e->getMessage());
}
}
switch ($Route->_action) {
case "upload_clients":
@@ -140,10 +198,7 @@ switch ($Route->_action) {
try {
$db = Database::getInstance();
$sql = 'SELECT c.* FROM clients c WHERE c.code = :code';
$stmt = $db->prepare($sql);
$stmt->bindParam(':code', $code, PDO::PARAM_STR);
$stmt->execute();
$record = $stmt->fetchAll(PDO::FETCH_ASSOC);
$record = $db->fetchAll($sql, [':code' => $code]);
} catch (Exception $e) {
error_log("Erreur recherche client : " . $e->getMessage());
$record = [];
@@ -155,8 +210,7 @@ switch ($Route->_action) {
$db = Database::getInstance();
$sql = 'INSERT INTO clients SET code = :code, libelle = :libelle, siret = :siret, adresse1 = :adresse1, adresse2 = :adresse2, adresse3 = :adresse3, cp = :cp, ville = :ville, ';
$sql .= 'type_client = :type_client, contact_nom = :contact_nom, contact_prenom = :contact_prenom, contact_fonction = :contact_fonction, telephone = :telephone, mobile = :mobile, email = :email, chk_import = 1';
$stmt = $db->prepare($sql);
$stmt->execute([
$stmt = $db->query($sql, [
':code' => $code,
':libelle' => $libelle,
':siret' => $siret,
@@ -175,6 +229,10 @@ switch ($Route->_action) {
]);
$fkClient = $db->lastInsertId();
fwrite($fhlog, $row . "--- Ajout client avec requête préparée\r\n");
// Synchroniser le contact dans clients_contacts
syncContactClient($code, $contactNom, $contactPrenom, $contactFonction, $telephone, $mobile, $email, $fkUser);
} catch (Exception $e) {
error_log("Erreur insertion client : " . $e->getMessage());
fwrite($fhlog, "Erreur insertion : " . $e->getMessage() . "\r\n");
@@ -198,8 +256,7 @@ switch ($Route->_action) {
$sql = 'UPDATE clients SET libelle = :libelle, siret = :siret, adresse1 = :adresse1, adresse2 = :adresse2, adresse3 = :adresse3, cp = :cp, ville = :ville, ';
$sql .= 'type_client = :type_client, contact_nom = :contact_nom, contact_prenom = :contact_prenom, contact_fonction = :contact_fonction, telephone = :telephone, mobile = :mobile, email = :email, chk_import = 1 ';
$sql .= 'WHERE code = :code';
$stmt = $db->prepare($sql);
$stmt->execute([
$stmt = $db->query($sql, [
':libelle' => $libelle,
':siret' => $siret,
':adresse1' => $adresse1,
@@ -217,6 +274,10 @@ switch ($Route->_action) {
':code' => $code
]);
fwrite($fhlog, $row . "--- MàJ client avec requête préparée\r\n");
// Synchroniser le contact dans clients_contacts
syncContactClient($code, $contactNom, $contactPrenom, $contactFonction, $telephone, $mobile, $email, $fkUser);
} catch (Exception $e) {
error_log("Erreur mise à jour client : " . $e->getMessage());
fwrite($fhlog, "Erreur MàJ : " . $e->getMessage() . "\r\n");
@@ -378,12 +439,18 @@ switch ($Route->_action) {
$libelle = str_replace('"', '', trim($data[1])); // on remplace les doubles guillemets par rien
// on réencode en ISO 8859-1 pour éviter les problèmes d'accent
if ($codOrigin == "UTF-8") {
$libelle = utf8_decode($libelle);
}
if ($codOrigin != "ISO-8859-1") {
// Convertir en ISO 8859-1
$libelle = iconv($codOrigin, "ISO-8859-15//IGNORE", $libelle);
// Utiliser mb_convert_encoding au lieu de utf8_decode (déprécié en PHP 8.3)
$libelle = mb_convert_encoding($libelle, "ISO-8859-1", "UTF-8");
} elseif ($codOrigin == "Windows-1252" || $codOrigin == "CP1252") {
// Windows-1252 est très proche de ISO-8859-1
// On traite le texte tel quel car les caractères de base sont compatibles
// Seulement quelques caractères spéciaux diffèrent (€, œ, etc.)
$libelle = $libelle; // Pas de conversion, on garde tel quel
} elseif ($codOrigin == "ISO-8859-15") {
// ISO-8859-15 est compatible avec ISO-8859-1 sauf pour le symbole €
$libelle = str_replace('€', 'EUR', $libelle);
}
// Pour ISO-8859-1, on ne fait rien, c'est déjà le bon format
$groupe = str_replace(" ", " ", trim($data[2])); // on remplace les doubles espaces par un simple espace
$liste = trim($data[3]);
@@ -532,7 +599,10 @@ switch ($Route->_action) {
$ret = array('ret' => "ko", 'msg' => "Aucun fichier à importer");
} else {
if ($erreur == "") {
$ret = array('ret' => "ok", 'msg' => "L'importation est terminée et s'est bien déroulée");
$nbLignes = isset($row) ? $row - 1 : 0; // On enlève la ligne d'en-tête
$nbMarches = isset($idMarches) ? count($idMarches) : 0;
$msgMarches = $nbMarches > 1 ? " pour " . $nbMarches . " marchés" : ($nbMarches == 1 ? " pour 1 marché" : "");
$ret = array('ret' => "ok", 'msg' => "Import terminé : " . $nbLignes . " produits traités" . $msgMarches);
} else {
$ret = array('ret' => "ko", 'msg' => $erreur);
}

View File

@@ -211,9 +211,7 @@ switch ($Route->_action) {
$db = Database::getInstance();
// SÉCURITÉ : Utilisation de requête préparée pour l'ID
$sql = "SELECT `$chp` AS data FROM clients WHERE rowid = :id";
$stmt = $db->prepare($sql);
$stmt->execute([':id' => intval($fk_tiers)]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
$result = $db->fetchOne($sql, [':id' => intval($fk_tiers)]);
if ($result) {
$upls = $result;
}
@@ -290,12 +288,8 @@ switch ($Route->_action) {
OR c.email LIKE :search
ORDER BY c.libelle';
$stmt = $db->prepare($sql);
$searchParam = '%' . $search . '%';
$stmt->bindParam(':search', $searchParam, PDO::PARAM_STR);
$stmt->execute();
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
$results = $db->fetchAll($sql, [':search' => $searchParam]);
echo json_encode($results);
} catch (Exception $e) {
error_log("Erreur search_clients : " . $e->getMessage());
@@ -522,16 +516,13 @@ switch ($Route->_action) {
// Utilisation de requêtes préparées pour la suppression
$sql1 = 'DELETE FROM marches WHERE rowid = :id';
$stmt1 = $db->prepare($sql1);
$stmt1->execute(['id' => $cid]);
$db->query($sql1, ['id' => $cid]);
$sql2 = 'DELETE FROM marches_listes WHERE fk_marche = :id';
$stmt2 = $db->prepare($sql2);
$stmt2->execute(['id' => $cid]);
$db->query($sql2, ['id' => $cid]);
$sql3 = 'DELETE FROM produits WHERE fk_marche = :id';
$stmt3 = $db->prepare($sql3);
$stmt3->execute(['id' => $cid]);
$db->query($sql3, ['id' => $cid]);
eLog("Marché supprimé : ID=$cid");
$ret = array('ret' => "ok", 'msg' => 'Marché supprimé');
@@ -785,9 +776,7 @@ switch ($Route->_action) {
try {
$db = Database::getInstance();
$sql = 'SELECT c.code, c.libelle, c.adresse1, c.adresse2 FROM clients c WHERE c.rowid = :id';
$stmt = $db->prepare($sql);
$stmt->execute([':id' => intval($devis["fk_client"])]);
$client = $stmt->fetch(PDO::FETCH_ASSOC);
$client = $db->fetchOne($sql, [':id' => intval($devis["fk_client"])]);
if (!$client) {
$client = ['code' => '', 'libelle' => '', 'adresse1' => '', 'adresse2' => ''];
}
@@ -884,8 +873,8 @@ switch ($Route->_action) {
try {
$db = Database::getInstance();
$sql = 'DELETE FROM infos WHERE rowid = :id';
$stmt = $db->prepare($sql);
$result = $stmt->execute(['id' => $cid]);
$stmt = $db->query($sql, ['id' => $cid]);
$result = $stmt->rowCount() > 0;
if ($result) {
eLog("Info supprimée : ID=$cid");

View File

@@ -1,25 +1,53 @@
#!/bin/bash
# Script de déploiement optimisé de Cleo vers l'environnement de développement
# Version: 2.0 - Utilise tar.gz pour un transfert plus rapide
# Script de déploiement optimisé de Cleo
# Version: 2.1 - Supporte déploiement DEV et PROD
# Usage: ./deploy-cleo.sh [pra]
# Sans argument : Déploie depuis local vers IN3/dva-front (DEV)
# Avec 'pra' : Déploie depuis IN3/dva-front vers IN4/pra-front (PROD)
cd /home/pierre/dev/cleo
# Configuration du serveur hôte Debian 12
HOST_SSH_HOST=195.154.80.116 # Adresse IP du serveur hôte
HOST_SSH_USER=root # Utilisateur SSH sur le serveur hôte
HOST_SSH_PORT=22 # Port SSH du serveur hôte
HOST_SSH_KEY=/home/pierre/.ssh/id_rsa_mbpi # Clé SSH privée pour accéder au serveur hôte
# Détecter le mode de déploiement
DEPLOY_MODE=${1:-dev}
# Configuration du conteneur Incus hébergeant cette application
CT_PROJECT_NAME=default # Nom du projet Incus où se trouve le conteneur
CT_NAME=dva-front # Nom du conteneur Incus
CT_IP=13.23.33.42 # IP interne du conteneur Incus
DEPLOY_DIR=/var/www # Répertoire de déploiement sur le conteneur
APP_NAME=cleo # Nom de l'application
if [ "$DEPLOY_MODE" = "pra" ]; then
# Configuration PROD : IN3/dva-front → IN4/pra-front
SOURCE_HOST=11.1.2.1
SOURCE_SSH_USER=root
SOURCE_SSH_PORT=22
SOURCE_SSH_KEY=/home/pierre/.ssh/id_rsa_mbpi
SOURCE_CT_PROJECT=default
SOURCE_CT_NAME=dva-front
SOURCE_DEPLOY_DIR=/var/www/cleo
TARGET_HOST=11.1.2.14
TARGET_SSH_USER=root
TARGET_SSH_PORT=22
TARGET_SSH_KEY=/home/pierre/.ssh/id_rsa_mbpi
TARGET_CT_PROJECT=default
TARGET_CT_NAME=pra-front
TARGET_DEPLOY_DIR=/var/www/cleo
APP_NAME=cleo
SKIP_ENV=true
else
# Configuration DEV : local → IN3/dva-front
HOST_SSH_HOST=195.154.80.116
HOST_SSH_USER=root
HOST_SSH_PORT=22
HOST_SSH_KEY=/home/pierre/.ssh/id_rsa_mbpi
CT_PROJECT_NAME=default
CT_NAME=dva-front
CT_IP=13.23.33.42
DEPLOY_DIR=/var/www
APP_NAME=cleo
SKIP_ENV=false
fi
# Propriétaire et groupe pour les fichiers et dossiers de destination
OWNER=nginx
OWNER=nobody
GROUP=nginx
# Couleurs pour l'affichage
@@ -36,53 +64,181 @@ fi
# Afficher les paramètres
echo -e "${GREEN}=== Déploiement optimisé CLEO ===${NC}"
echo "Serveur hôte: $HOST_SSH_USER@$HOST_SSH_HOST:$HOST_SSH_PORT"
echo "Conteneur: $CT_NAME"
echo "Déploiement: $DEPLOY_DIR/$APP_NAME"
if [ "$DEPLOY_MODE" = "pra" ]; then
echo "Mode: PRODUCTION (DEV → PROD)"
echo "Source: $SOURCE_CT_NAME ($SOURCE_DEPLOY_DIR)"
echo "Destination: $TARGET_CT_NAME ($TARGET_DEPLOY_DIR)"
echo "Note: Le fichier .env ne sera PAS écrasé"
else
echo "Mode: DÉVELOPPEMENT (Local → DEV)"
echo "Serveur hôte: $HOST_SSH_USER@$HOST_SSH_HOST:$HOST_SSH_PORT"
echo "Conteneur: $CT_NAME"
echo "Déploiement: $DEPLOY_DIR/$APP_NAME"
fi
echo "=================================="
# 1. Créer l'archive tar.gz localement
echo -e "${YELLOW}1. Création de l'archive...${NC}"
tar -czf /tmp/cleo.tar.gz \
if [ "$DEPLOY_MODE" = "pra" ]; then
# ===== MODE PROD: Déploiement IN3/dva-front → IN4/pra-front =====
echo -e "${YELLOW}1. Création de l'archive depuis IN3/dva-front...${NC}"
# Définir les options SSH pour source et target
SOURCE_SSH_OPTS="-p $SOURCE_SSH_PORT -i $SOURCE_SSH_KEY"
TARGET_SSH_OPTS="-p $TARGET_SSH_PORT -i $TARGET_SSH_KEY"
# Définir les options SCP (port avec -P majuscule)
SOURCE_SCP_OPTS="-P $SOURCE_SSH_PORT -i $SOURCE_SSH_KEY"
TARGET_SCP_OPTS="-P $TARGET_SSH_PORT -i $TARGET_SSH_KEY"
# Script sur IN3 pour créer l'archive
SOURCE_SCRIPT="
set -e
incus project switch $SOURCE_CT_PROJECT
echo 'Création de l archive depuis dva-front...'
incus exec $SOURCE_CT_NAME -- sh -c '
cd $SOURCE_DEPLOY_DIR && \
tar -czf /tmp/cleo-prod.tar.gz \
--exclude=\".git\" \
--exclude=\"log/*.log\" \
--exclude=\"pub/files/upload/*\" \
--exclude=\"docs\" \
--exclude=\"migration\" \
--exclude=\".claude\" \
--exclude=\".vscode\" \
--exclude=\"vendor\" \
--exclude=\"backup_*\" \
--exclude=\"*.tar.gz\" \
--exclude=\".env\" \
--exclude=\".env.swp\" \
.
'
incus file pull $SOURCE_CT_NAME/tmp/cleo-prod.tar.gz /tmp/
incus exec $SOURCE_CT_NAME -- rm -f /tmp/cleo-prod.tar.gz
"
ssh $SOURCE_SSH_OPTS $SOURCE_SSH_USER@$SOURCE_HOST "$SOURCE_SCRIPT"
if [ $? -ne 0 ]; then
echo -e "${RED}Erreur lors de la création de l'archive${NC}"
exit 1
fi
echo -e "${GREEN}✓ Archive créée sur IN3${NC}"
# Transférer l'archive de IN3 vers IN4
echo -e "${YELLOW}2. Transfert de l'archive IN3 → IN4...${NC}"
scp $SOURCE_SCP_OPTS $SOURCE_SSH_USER@$SOURCE_HOST:/tmp/cleo-prod.tar.gz /tmp/
scp $TARGET_SCP_OPTS /tmp/cleo-prod.tar.gz $TARGET_SSH_USER@$TARGET_HOST:/tmp/
if [ $? -ne 0 ]; then
echo -e "${RED}Erreur lors du transfert de l'archive${NC}"
rm -f /tmp/cleo-prod.tar.gz
exit 1
fi
echo -e "${GREEN}✓ Archive transférée vers IN4${NC}"
# Déployer sur IN4/pra-front
echo -e "${YELLOW}3. Déploiement dans IN4/pra-front...${NC}"
TARGET_SCRIPT="
set -e
incus project switch $TARGET_CT_PROJECT
echo 'Transfert de l archive dans pra-front...'
incus file push /tmp/cleo-prod.tar.gz $TARGET_CT_NAME/tmp/
echo 'Déploiement dans pra-front (backup .env)...'
incus exec $TARGET_CT_NAME -- sh -c '
cp $TARGET_DEPLOY_DIR/.env /tmp/.env.backup && \
cd $TARGET_DEPLOY_DIR && \
tar -xzf /tmp/cleo-prod.tar.gz && \
mv /tmp/.env.backup $TARGET_DEPLOY_DIR/.env && \
rm -f /tmp/cleo-prod.tar.gz
'
echo 'Configuration des permissions...'
incus exec $TARGET_CT_NAME -- sh -c '
chown -R $OWNER:$GROUP $TARGET_DEPLOY_DIR && \
find $TARGET_DEPLOY_DIR -type d -exec chmod 755 {} \; && \
find $TARGET_DEPLOY_DIR -type f -exec chmod 644 {} \; && \
mkdir -p $TARGET_DEPLOY_DIR/log && \
chmod 775 $TARGET_DEPLOY_DIR/log && \
touch $TARGET_DEPLOY_DIR/log/\$(date +%m%d).log && \
chmod 664 $TARGET_DEPLOY_DIR/log/\$(date +%m%d).log && \
chown $OWNER:$GROUP $TARGET_DEPLOY_DIR/log/*.log && \
chmod 640 $TARGET_DEPLOY_DIR/.env && \
if [ -d $TARGET_DEPLOY_DIR/pub/files/upload ]; then
chmod 775 $TARGET_DEPLOY_DIR/pub/files/upload && \
chown $OWNER:$GROUP $TARGET_DEPLOY_DIR/pub/files/upload
fi
'
rm -f /tmp/cleo-prod.tar.gz
echo 'Déploiement PROD terminé!'
"
ssh $TARGET_SSH_OPTS $TARGET_SSH_USER@$TARGET_HOST "$TARGET_SCRIPT"
if [ $? -ne 0 ]; then
echo -e "${RED}Erreur lors du déploiement PROD${NC}"
rm -f /tmp/cleo-prod.tar.gz
ssh $SOURCE_SSH_OPTS $SOURCE_SSH_USER@$SOURCE_HOST "rm -f /tmp/cleo-prod.tar.gz"
exit 1
fi
# Nettoyer les archives locales et distantes
echo -e "${YELLOW}4. Nettoyage...${NC}"
rm -f /tmp/cleo-prod.tar.gz
ssh $SOURCE_SSH_OPTS $SOURCE_SSH_USER@$SOURCE_HOST "rm -f /tmp/cleo-prod.tar.gz"
echo -e "${GREEN}✓ Nettoyage terminé${NC}"
else
# ===== MODE DEV: Déploiement local → IN3/dva-front =====
# 1. Créer l'archive tar.gz localement
echo -e "${YELLOW}1. Création de l'archive...${NC}"
tar -czf /tmp/cleo.tar.gz \
--exclude='.git' \
--exclude='log/*.log' \
--exclude='pub/files/upload/*' \
--exclude='docs/*.sql' \
--exclude='docs' \
--exclude='migration' \
--exclude='.claude' \
--exclude='.vscode' \
--exclude='vendor' \
--exclude='backup_*' \
--exclude='*.tar.gz' \
--exclude='.env' \
--exclude='.env.swp' \
.
if [ $? -ne 0 ]; then
if [ $? -ne 0 ]; then
echo -e "${RED}Erreur lors de la création de l'archive${NC}"
exit 1
fi
fi
ARCHIVE_SIZE=$(du -h /tmp/cleo.tar.gz | cut -f1)
echo -e "${GREEN}✓ Archive créée: /tmp/cleo.tar.gz ($ARCHIVE_SIZE)${NC}"
ARCHIVE_SIZE=$(du -h /tmp/cleo.tar.gz | cut -f1)
echo -e "${GREEN}✓ Archive créée: /tmp/cleo.tar.gz ($ARCHIVE_SIZE)${NC}"
# 2. Copier l'archive vers IN3
echo -e "${YELLOW}2. Transfert de l'archive vers IN3...${NC}"
scp -P $HOST_SSH_PORT -i $HOST_SSH_KEY /tmp/cleo.tar.gz $HOST_SSH_USER@$HOST_SSH_HOST:/tmp/
# 2. Copier l'archive vers IN3
echo -e "${YELLOW}2. Transfert de l'archive vers IN3...${NC}"
scp -P $HOST_SSH_PORT -i $HOST_SSH_KEY /tmp/cleo.tar.gz $HOST_SSH_USER@$HOST_SSH_HOST:/tmp/
if [ $? -ne 0 ]; then
if [ $? -ne 0 ]; then
echo -e "${RED}Erreur lors du transfert de l'archive${NC}"
rm -f /tmp/cleo.tar.gz
exit 1
fi
echo -e "${GREEN}✓ Archive transférée sur IN3${NC}"
fi
echo -e "${GREEN}✓ Archive transférée sur IN3${NC}"
# 3. Nettoyer l'ancien répertoire /tmp/cleo sur IN3 s'il existe
echo -e "${YELLOW}3. Nettoyage des anciens fichiers temporaires sur IN3...${NC}"
ssh $SSH_OPTS $HOST_SSH_USER@$HOST_SSH_HOST "rm -rf /tmp/cleo 2>/dev/null || true"
echo -e "${GREEN}✓ Nettoyage effectué${NC}"
# 3. Nettoyer l'ancien répertoire /tmp/cleo sur IN3 s'il existe
echo -e "${YELLOW}3. Nettoyage des anciens fichiers temporaires sur IN3...${NC}"
ssh $SSH_OPTS $HOST_SSH_USER@$HOST_SSH_HOST "rm -rf /tmp/cleo 2>/dev/null || true"
echo -e "${GREEN}✓ Nettoyage effectué${NC}"
# 4. Transférer et extraire dans le conteneur
echo -e "${YELLOW}4. Déploiement dans le conteneur $CT_NAME...${NC}"
# 4. Transférer et extraire dans le conteneur
echo -e "${YELLOW}4. Déploiement dans le conteneur $CT_NAME...${NC}"
# Script à exécuter sur IN3
REMOTE_SCRIPT="
# Script à exécuter sur IN3
REMOTE_SCRIPT="
set -e
# Sélectionner le projet Incus
@@ -108,11 +264,11 @@ incus exec $CT_NAME -- sh -c '
find $DEPLOY_DIR/$APP_NAME -type d -exec chmod 755 {} \; && \
find $DEPLOY_DIR/$APP_NAME -type f -exec chmod 644 {} \; && \
mkdir -p $DEPLOY_DIR/$APP_NAME/log && \
chmod 777 $DEPLOY_DIR/$APP_NAME/log && \
touch $DEPLOY_DIR/$APP_NAME/log/$(date +%m%d).log && \
chmod 777 $DEPLOY_DIR/$APP_NAME/log/$(date +%m%d).log && \
chown nobody:nobody $DEPLOY_DIR/$APP_NAME/log/*.log && \
chmod 644 $DEPLOY_DIR/$APP_NAME/.env && \
chmod 775 $DEPLOY_DIR/$APP_NAME/log && \
touch $DEPLOY_DIR/$APP_NAME/log/\$(date +%m%d).log && \
chmod 664 $DEPLOY_DIR/$APP_NAME/log/\$(date +%m%d).log && \
chown $OWNER:$GROUP $DEPLOY_DIR/$APP_NAME/log/*.log && \
chmod 640 $DEPLOY_DIR/$APP_NAME/.env && \
rm -f $DEPLOY_DIR/$APP_NAME/.env.swp && \
if [ -d $DEPLOY_DIR/$APP_NAME/pub/files/upload ]; then
chmod 775 $DEPLOY_DIR/$APP_NAME/pub/files/upload && \
@@ -126,26 +282,34 @@ rm -f /tmp/cleo.tar.gz
echo 'Déploiement terminé avec succès!'
"
# Exécuter le script sur IN3
ssh $SSH_OPTS $HOST_SSH_USER@$HOST_SSH_HOST "$REMOTE_SCRIPT"
# Exécuter le script sur IN3
ssh $SSH_OPTS $HOST_SSH_USER@$HOST_SSH_HOST "$REMOTE_SCRIPT"
if [ $? -ne 0 ]; then
if [ $? -ne 0 ]; then
echo -e "${RED}Erreur lors du déploiement${NC}"
# Nettoyer l'archive locale et distante
rm -f /tmp/cleo.tar.gz
ssh $SSH_OPTS $HOST_SSH_USER@$HOST_SSH_HOST "rm -f /tmp/cleo.tar.gz 2>/dev/null || true"
exit 1
fi
fi
# 5. Nettoyer l'archive locale
echo -e "${YELLOW}5. Nettoyage local...${NC}"
rm -f /tmp/cleo.tar.gz
echo -e "${GREEN}✓ Archive locale supprimée${NC}"
# 5. Nettoyer l'archive locale
echo -e "${YELLOW}5. Nettoyage local...${NC}"
rm -f /tmp/cleo.tar.gz
echo -e "${GREEN}✓ Archive locale supprimée${NC}"
fi
echo ""
echo -e "${GREEN}===================================================${NC}"
echo -e "${GREEN} Déploiement terminé avec succès ! ${NC}"
echo -e "${GREEN}===================================================${NC}"
echo "Site: http://dcleo.unikoffice.com"
echo "Chemin: $DEPLOY_DIR/$APP_NAME sur $CT_NAME"
if [ "$DEPLOY_MODE" = "pra" ]; then
echo "Site: https://cleo.unikoffice.com"
echo "Environnement: PRODUCTION (pra-front)"
else
echo "Site: http://dcleo.unikoffice.com"
echo "Environnement: DÉVELOPPEMENT (dva-front)"
echo "Chemin: $DEPLOY_DIR/$APP_NAME sur $CT_NAME"
fi
echo ""

133
deploy-file.sh Executable file
View File

@@ -0,0 +1,133 @@
#!/bin/bash
# Script de déploiement d'un fichier unique
# Version: 1.0
# Usage: ./deploy-file.sh <chemin_fichier> [pra]
# <chemin_fichier> : Chemin relatif du fichier à déployer (ex: pub/res/js/jdevis.js)
# pra (optionnel) : Déploie vers PROD (IN4/pra-front), sinon déploie vers DEV (IN3/dva-front)
# Vérifier les paramètres
if [ -z "$1" ]; then
echo "Usage: $0 <chemin_fichier> [pra]"
echo "Exemple: $0 pub/res/js/jdevis.js"
echo "Exemple: $0 pub/res/js/jdevis.js pra"
exit 1
fi
FILE_PATH="$1"
DEPLOY_MODE=${2:-dev}
# Vérifier que le fichier existe localement
if [ ! -f "$FILE_PATH" ]; then
echo "Erreur: Le fichier '$FILE_PATH' n'existe pas"
exit 1
fi
# Détecter le mode de déploiement
if [ "$DEPLOY_MODE" = "pra" ]; then
# Configuration PROD : IN4/pra-front
TARGET_HOST=11.1.2.14
TARGET_SSH_USER=root
TARGET_SSH_PORT=22
TARGET_SSH_KEY=/home/pierre/.ssh/id_rsa_mbpi
TARGET_CT_PROJECT=default
TARGET_CT_NAME=pra-front
TARGET_DEPLOY_DIR=/var/www/cleo
ENV_NAME="PRODUCTION"
else
# Configuration DEV : IN3/dva-front
TARGET_HOST=195.154.80.116
TARGET_SSH_USER=root
TARGET_SSH_PORT=22
TARGET_SSH_KEY=/home/pierre/.ssh/id_rsa_mbpi
TARGET_CT_PROJECT=default
TARGET_CT_NAME=dva-front
TARGET_DEPLOY_DIR=/var/www/cleo
ENV_NAME="DÉVELOPPEMENT"
fi
# Propriétaire et groupe
OWNER=nobody
GROUP=nginx
# Couleurs
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m'
# Options SSH
TARGET_SSH_OPTS="-p $TARGET_SSH_PORT -i $TARGET_SSH_KEY"
TARGET_SCP_OPTS="-P $TARGET_SSH_PORT -i $TARGET_SSH_KEY"
echo -e "${GREEN}=== Déploiement fichier unique ===${NC}"
echo "Fichier: $FILE_PATH"
echo "Environnement: $ENV_NAME ($TARGET_CT_NAME)"
echo "Destination: $TARGET_DEPLOY_DIR/$FILE_PATH"
echo "=================================="
# 1. Copier le fichier vers le serveur hôte
echo -e "${YELLOW}1. Transfert du fichier vers le serveur...${NC}"
scp $TARGET_SCP_OPTS "$FILE_PATH" $TARGET_SSH_USER@$TARGET_HOST:/tmp/deploy-file.tmp
if [ $? -ne 0 ]; then
echo -e "${RED}Erreur lors du transfert${NC}"
exit 1
fi
echo -e "${GREEN}✓ Fichier transféré${NC}"
# 2. Déployer dans le conteneur
echo -e "${YELLOW}2. Déploiement dans le conteneur...${NC}"
# Déterminer les permissions selon le type de fichier
if [[ "$FILE_PATH" == ".env"* ]]; then
FILE_PERMS=640
else
FILE_PERMS=644
fi
TARGET_SCRIPT="
set -e
incus project switch $TARGET_CT_PROJECT
# Créer le répertoire parent si nécessaire
incus exec $TARGET_CT_NAME -- sh -c 'mkdir -p \$(dirname $TARGET_DEPLOY_DIR/$FILE_PATH)'
# Transférer le fichier dans le conteneur
incus file push /tmp/deploy-file.tmp $TARGET_CT_NAME$TARGET_DEPLOY_DIR/$FILE_PATH
# Appliquer les permissions
incus exec $TARGET_CT_NAME -- sh -c '
chown $OWNER:$GROUP $TARGET_DEPLOY_DIR/$FILE_PATH && \
chmod $FILE_PERMS $TARGET_DEPLOY_DIR/$FILE_PATH
'
# Nettoyer
rm -f /tmp/deploy-file.tmp
echo 'Déploiement terminé!'
"
ssh $TARGET_SSH_OPTS $TARGET_SSH_USER@$TARGET_HOST "$TARGET_SCRIPT"
if [ $? -ne 0 ]; then
echo -e "${RED}Erreur lors du déploiement${NC}"
ssh $TARGET_SSH_OPTS $TARGET_SSH_USER@$TARGET_HOST "rm -f /tmp/deploy-file.tmp"
exit 1
fi
echo -e "${GREEN}✓ Fichier déployé avec succès${NC}"
echo ""
echo -e "${GREEN}===================================================${NC}"
echo -e "${GREEN} Déploiement terminé avec succès ! ${NC}"
echo -e "${GREEN}===================================================${NC}"
if [ "$DEPLOY_MODE" = "pra" ]; then
echo "Site: https://cleo.unikoffice.com"
echo "Environnement: PRODUCTION (pra-front)"
else
echo "Site: http://dcleo.unikoffice.com"
echo "Environnement: DÉVELOPPEMENT (dva-front)"
fi
echo "Fichier: $TARGET_DEPLOY_DIR/$FILE_PATH"
echo ""

View File

@@ -4,7 +4,7 @@
CLEO est une application web de gestion de devis développée en PHP 8.3 pour les PME. Elle utilise une architecture MVC classique avec un framework maison appelé "d6".
**Version actuelle** : 2.0.1 (migration complétée le 12 septembre 2025)
**Version actuelle** : 2.0.3 (gestion multi-contacts complétée le 21 octobre 2025)
## Architecture technique
@@ -53,7 +53,9 @@ cleo/
- Gestion des remises par paliers de quantité
2. **Gestion des clients** (`cclients.php`)
- Base clients avec contacts
- Base clients avec contacts multiples (table `clients_contacts`)
- Gestion des contacts via modale Bootstrap intégrée aux devis
- Contact principal automatique avec indicateur visuel
- Segmentation par secteur géographique
- Types de clients paramétrables
- Import/export de données
@@ -82,10 +84,11 @@ cleo/
- **Sécurité** : Requêtes préparées systématiques
### Tables principales
- `devis` : Table principale des devis
- `devis` : Table principale des devis (champ `fk_contact` depuis v2.0.3)
- `devis_produits` : Lignes de produits des devis
- `devis_histo` : Historique des modifications
- `clients` : Base clients
- `clients_contacts` : Contacts multiples par client (v2.0.3)
- `produits` : Catalogue produits
- `produits_familles` : Familles de produits avec marges
- `marches` : Référentiel des marchés
@@ -95,22 +98,32 @@ cleo/
## Points de sécurité
### Vulnérabilités corrigées (v2.0.1)
### Vulnérabilités corrigées
**1. Stockage des mots de passe**
**v2.0.1 - Stockage des mots de passe**
- Credentials externalisés dans `.env`
- Variables d'environnement utilisées systématiquement
**2. Protection contre les injections SQL**
**v2.0.1 - Protection contre les injections SQL**
- Migration complète vers PDO
- Requêtes préparées dans la classe `Database`
- Pattern Singleton pour la connexion
**3. Gestion des erreurs sécurisée**
**v2.0.1 - Gestion des erreurs sécurisée**
- Classe `Database` avec gestion d'erreurs centralisée
- Logging contrôlé par variables d'environnement
- Mode debug désactivable en production
**v2.0.2 - Corrections critiques**
- Sanitisation stricte des entrées utilisateur
- Validation des paramètres AJAX
- Fonction `nettoie_input()` utilisée systématiquement
**v2.0.3 - Gestion multi-contacts sécurisée**
- Contrôleur AJAX `cjxcontacts.php` avec requêtes préparées
- Validation des foreign keys et soft delete
- Prévention de suppression du dernier contact actif
### Vulnérabilités restantes à traiter
#### 1. Injections SQL résiduelles
@@ -208,15 +221,28 @@ cleo/
## Conclusion
CLEO v2.0.1 représente une évolution majeure avec la migration réussie vers une architecture sécurisée :
- ✅ Base de données unique et centralisée
- ✅ Connexions PDO avec requêtes préparées
- ✅ Configuration externalisée
- ✅ Séparation application/base de données
CLEO v2.0.3 représente l'aboutissement de trois itérations majeures d'amélioration :
Les priorités de sécurité critiques ont été adressées. L'application peut maintenant évoluer sereinement vers des standards plus modernes tout en maintenant sa stabilité opérationnelle.
**v2.0.1 - Architecture sécurisée**
- Base de données unique et centralisée
- Connexions PDO avec requêtes préparées
- Configuration externalisée
- Séparation application/base de données
**v2.0.2 - Sécurité renforcée**
- Sanitisation systématique des entrées
- Validation stricte des paramètres AJAX
- Corrections de vulnérabilités critiques
**v2.0.3 - Gestion multi-contacts**
- Migration vers table relationnelle `clients_contacts`
- Interface modale intégrée dans les devis
- CRUD complet avec soft delete
- Gestion automatique du contact principal
L'application dispose maintenant d'une base solide pour évoluer vers des standards modernes tout en maintenant sa stabilité opérationnelle.
---
*Document mis à jour le 12 septembre 2025*
*Version 2.0.1 - Post-migration*
*Document mis à jour le 21 octobre 2025*
*Version 2.0.3 - Gestion multi-contacts*

111
docs/RULES.md Normal file
View File

@@ -0,0 +1,111 @@
# Règles métier - Application CLEO
Ce document recense les règles métier et de développement identifiées dans l'application CLEO.
## 1. Gestion des rôles et permissions
### 1.1 Hiérarchie des rôles
- **DIR-CO** (fk_role = 1) : Direction commerciale
- Accès complet aux devis (propres + statut >= 2)
- Vision globale de l'activité
- **DV** (fk_role = 2) : Directeur des ventes
- Accès à ses propres devis
- Accès aux devis de ses RR subordonnés (statut >= 3)
- Récupération des RR via `fk_parent` dans la table `users`
- **RR** (fk_role = 3) : Responsable régional
- Accès uniquement à ses propres devis
- **Admin** (fk_role = 90) : Administration système
- Accès complet à l'administration
### 1.2 Visibilité des devis (mdevis.php)
La clause WHERE pour filtrer les devis dépend du rôle :
- **DIR-CO** : `d.fk_user = :fkUser OR d.fk_statut_devis >= 2`
- **DV** : `d.fk_user = :fkUser OR (d.fk_statut_devis >= 3 AND d.fk_user IN ([RR_IDS]))`
- **RR** : `d.fk_user = :fkUser`
## 2. Gestion des marchés et produits
### 2.1 Types de marchés
- **Marché standard** : Contient ses propres produits uniquement
- **Marché hybride** (`chk_marche_hybride = 1`) : Combine les produits du marché + produits du marché 999
- **Marché avec remise sur TG** (`chk_remise_sur_tg = 1`) : Charge uniquement les produits du marché 999
- **Marché 999** : Marché "Hors marché" ou "Tarif général"
### 2.2 Chargement des produits (load_devis_marche_produits)
| Type de marché | chk_remise_sur_tg | chk_marche_hybride | Produits chargés |
|---------------|-------------------|-------------------|------------------|
| Spécifique | 1 | - | Tous les produits du marché 999 |
| Spécifique | 0 | 0 | Produits du marché uniquement |
| Spécifique | 0 | 1 | Produits du marché + produits du 999 non présents |
| 999 (Hors marché) | - | - | Tous les produits du marché 999 |
### 2.3 Terme "Purchasing"
Quand `terme_achat = 'Purchasing'` dans `marches_listes` :
- Récupère les prix d'achat nets depuis le marché 999
- Applique les paliers de remise du marché 999
## 3. Sécurité et développement
### 3.1 Accès base de données
- **Obligatoire** : Utiliser la classe `Database` avec ses méthodes
- **Interdit** : Appeler directement `$db->prepare()` sur l'objet Database
- **Méthodes disponibles** :
- `$db->fetchAll($sql, $params)` : Récupérer plusieurs lignes
- `$db->fetchOne($sql, $params)` : Récupérer une ligne
- `$db->query($sql, $params)` : Exécuter une requête
- `$db->lastInsertId()` : Récupérer le dernier ID inséré
### 3.2 Protection contre les injections SQL
- Utiliser `intval()` pour les entiers dans les requêtes non préparées
- Utiliser `nettoie_input()` pour nettoyer les entrées utilisateur
- Privilégier les requêtes préparées via la classe Database
### 3.3 Variables de sécurité
- `$cidSafe = intval($cid)` : Version sécurisée pour les requêtes SQL
- Attention lors de la modification de variables : recalculer ou utiliser directement `intval()`
## 4. Statuts des devis
### 4.1 Statuts principaux
- **1** : En cours
- **2** : Validé niveau 1
- **3** : Validé niveau 2
- **20** : Archivé
### 4.2 Réactivation des devis
- Un devis archivé (statut 20) peut être réactivé (statut 1)
- La réactivation est tracée dans `devis_histo`
- Disponible selon les droits du rôle
## 5. Conventions de nommage
### 5.1 Fichiers
- **Contrôleurs** : `c*.php` pour les standards, `cjx*.php` pour AJAX
- **Modèles** : `m*.php`
- **Vues** : `v*.php`
### 5.2 Tables de base de données
- **Tables principales** : Nom simple (`devis`, `clients`, `produits`)
- **Tables de référence** : Préfixe `x_` (`x_familles`, `x_statuts`)
- **Tables système** : Préfixe `y_` (`y_pages`) ou `z_` (`z_logs`, `z_sessions`)
## 6. Gestion des prix et marges
### 6.1 Prix nets
- `chk_prix_net = 1` : Prix non modifiable (marché hybride)
- `chk_prix_net = 0` : Prix modifiable avec marges
### 6.2 Paliers de remise
Les produits peuvent avoir jusqu'à 6 paliers de remise :
- `prc_discount_1` avec `quantite_1`
- `prc_discount_2` avec `quantite_2`
- ... jusqu'à 6
---
*Document créé le 16 septembre 2025*
*À mettre à jour au fur et à mesure des découvertes*

View File

@@ -4,15 +4,6 @@
### Module Devis
#### 6. ✅ Modifier un devis archivé (TERMINÉ - 12/09/2025)
**Priorité**: Haute
**Description**: Permettre la modification d'un devis archivé et son renvoi pour traitement sans nécessiter de duplication.
**Tâches**:
- [x] Ajouter un bouton "Réactiver" sur les devis archivés (statut 20)
- [x] Permettre le changement de statut d'archivé vers "En cours"
- [x] Conserver l'historique de réactivation dans `devis_histo`
- [x] Adapter les droits selon les rôles (RR, DV, DIR-CO)
#### 8. Dupliquer une ligne produit
**Priorité**: Moyenne
**Description**: Permettre la duplication d'une ligne produit dans un même devis (utile pour les gratuités).
@@ -36,16 +27,6 @@
- [ ] Paginer les résultats de recherche
- [ ] Export des résultats en Excel
#### 19. Gestion des contacts multiples
**Priorité**: Haute
**Description**: Permettre la gestion de plusieurs contacts par client.
**Tâches**:
- [ ] Créer une table `clients_contacts`
- [ ] Migration des contacts existants vers la nouvelle structure
- [ ] Interface CRUD pour les contacts
- [ ] Sélecteur de contact à la création/modification de devis
- [ ] Historique des contacts par devis
#### 21. Actualisation tarifaire
**Priorité**: Moyenne
**Description**: Permettre l'actualisation des prix selon la dernière grille tarifaire.
@@ -56,8 +37,73 @@
- [ ] Recalculer automatiquement les marges
- [ ] Tracer l'actualisation dans l'historique
#### 22. Marchés hybrides et onglet Mercurial
**Priorité**: Haute
**Description**: Ajouter un onglet "Mercurial" dans la page devis pour les marchés de type hybride, listant tous les produits du marché.
**Tâches - Phase 1 (Onglet Mercurial)** :
- [x] Identifier le type de marché du devis sélectionné
- [x] Détecter si le marché est de type "hybride"
- [x] Ajouter un nouvel onglet "Mercurial" dans l'interface devis (au niveau de l'onglet Produits)
- [x] Récupérer tous les produits associés au marché
- [x] Filtrer les produits "Hors Marché 999"
- [x] Afficher la liste des produits dans l'onglet Mercurial
- [x] Gérer l'affichage/masquage de l'onglet selon le type de marché
**Tâches - Phase 2 (Améliorations visuelles et règles métier)** :
- [ ] **Visibilité de l'onglet Mercurial** : Rendre l'onglet "Mercurial" visuellement distinct (couleur de fond différente, par exemple) pour qu'il soit clairement identifiable par les commerciaux
**Types de marchés hybrides** :
Deux cas de marchés hybrides doivent être gérés différemment :
**CAS 1 - Mercuriale sans remise** :
- Liste mercuriale en prix nets SANS remise applicable sur ces références
- Reste du catalogue disponible avec possibilité de remises en autonomie
**CAS 2 - Mercuriale avec remise possible** :
- Liste mercuriale en prix nets AVEC possibilité de remises sur ces références
- Reste du catalogue disponible avec possibilité de remises en autonomie
**Règles communes aux 2 cas** :
- Quand le devis contient UNIQUEMENT des références mercuriales → pas de demande d'accord nécessaire, le RR peut valider directement
- Quand le devis contient références mercuriales + catalogue général → seules les références du catalogue sont concernées par les seuils de marge et peuvent générer une demande d'accord
- Si geste commercial souhaité sur un devis 100% mercurial → utiliser le champ "Demande geste commercial" existant
**Tâches - Paramétrage base de données** :
- [ ] Ajouter un champ dans la table `marches` pour définir le type de marché hybride :
- `type_mercurial` (ENUM ou INT) : NULL = non hybride, 1 = CAS 1 (sans remise), 2 = CAS 2 (avec remise)
- [ ] Modifier la fiche marché pour permettre la sélection du type de marché hybride
**Tâches - Logique métier de validation** :
- [ ] Détecter si un devis contient uniquement des produits mercuriaux
- [ ] Détecter si un devis contient un mix mercurial + catalogue
- [ ] Adapter le calcul des seuils de marge :
- Si devis 100% mercurial → pas de vérification de seuil, validation RR directe
- Si devis mixte → calculer les seuils uniquement sur les produits du catalogue général
- [ ] Bloquer/autoriser les remises sur produits mercuriaux selon le type de marché (CAS 1 vs CAS 2)
- [ ] Tester le workflow complet avec les 2 types de marchés hybrides
### Module SAP
#### 13. Import et contrôle des clients SAP
**Priorité**: Haute
**Description**: Contrôler les nouveaux clients créés dans la base CLEO et vérifier la correspondance avec la base SAP.
**Tâches**:
- [ ] Identifier le script/contrôleur d'import des clients SAP
- [ ] Analyser la structure des données importées
- [ ] Mettre en place un système de contrôle de correspondance
- [ ] Vérifier l'unicité du `clients.code` (identifiant SAP)
- [ ] Détecter les doublons potentiels (nom, adresse)
- [ ] Signaler les incohérences entre SAP et CLEO
- [ ] Créer un rapport d'import avec :
- [ ] Nombre de clients importés
- [ ] Nombre de clients mis à jour
- [ ] Nombre d'anomalies détectées
- [ ] Gestion des cas particuliers :
- [ ] Client existe dans CLEO mais pas dans SAP
- [ ] Client existe dans SAP mais code différent dans CLEO
- [ ] Contacts orphelins après import
#### 14. Gestion de la prise en charge
**Priorité**: Haute
**Description**: Ajouter la traçabilité de la prise en charge et du transfert EDI.
@@ -117,65 +163,7 @@
### Plan de migration - État d'avancement
#### ✅ Phase 0 - Refactoring base de données (COMPLÉTÉ - 12/09/2025)
- [x] Script de migration SQL créé
- [x] Table `y_pages` migrée depuis `uof_frontal`
- [x] Table `z_logs` créée dans `cleo`
- [x] Base `cleo` créée avec toutes les tables
- [x] Données migrées de `uof_linet` vers `cleo`
- [x] Références à `uof_frontal` supprimées
- [x] Classe Database PDO créée
- [x] Variables d'environnement `.env` implémentées
- [x] Tests validés en DEV
#### ✅ Phase 1 - Environnement DEV IN3 (COMPLÉTÉ - 12/09/2025)
- [x] Container `maria3` créé sur IN3
- [x] MariaDB 11.4 installé et configuré
- [x] Base `cleo` migrée vers `maria3`
- [x] Configuration pointant vers `maria3` (IP: 13.23.33.4)
- [x] Application testée et fonctionnelle
- [x] MariaDB supprimé de `dva-front`
- [x] Script de déploiement optimisé (`deploy-cleo-fast.sh`)
#### Phase 2 - Préparation PROD IN4 (À FAIRE)
**Export depuis IN3:**
- [ ] Exporter le container `dva-front` depuis IN3
```bash
incus export dva-front dva-front-export.tar.gz
```
- [ ] Exporter le container `maria3` depuis IN3
```bash
incus export maria3 maria3-export.tar.gz
```
**Import sur IN4:**
- [ ] Importer `dva-front` comme `pra-front` sur IN4
```bash
incus import dva-front-export.tar.gz pra-front
```
- [ ] Importer `maria3` comme `maria4` sur IN4
```bash
incus import maria3-export.tar.gz maria4
```
- [ ] Configurer les IPs et paramètres réseau sur IN4
- [ ] Adapter le fichier `.env` pour l'environnement PROD
#### Phase 3 - Migration des données PROD (À FAIRE)
- [ ] Effectuer une sauvegarde complète des bases PROD sur IN2/nx4
- [ ] Exporter les données de `uof_frontal` et `uof_linet` depuis IN2/nx4
- [ ] Utiliser le script de migration SQL pour fusionner les données
- [ ] Importer les données fusionnées dans `maria4` sur IN4
- [ ] Configurer `pra-front` pour pointer vers `maria4`
- [ ] Tests de validation en pré-production
#### Phase 4 - Bascule PROD (À FAIRE)
- [ ] Planifier la fenêtre de maintenance
- [ ] Arrêter l'application sur IN2
- [ ] Synchronisation finale des données vers IN4/maria4
- [ ] Basculer le DNS/proxy vers IN4
- [ ] Valider le fonctionnement en production
- [ ] Monitoring post-migration (48h)
- [ ] Décommissionner IN2 après période de stabilisation
**Migration complétée** - Toutes les phases (0 à 4) sont terminées.
### Configuration technique
@@ -207,6 +195,159 @@ DB_PASSWORD=<PROD_PASSWORD> # À sécuriser
- [ ] Scripts de backup automatisés à mettre en place
- [ ] Réplication master-slave pour haute disponibilité (optionnel)
## ⚠️ CRITIQUE - Risque de collision de codes clients (EN ATTENTE CLIENT)
### Problématique identifiée
**Date**: 26 novembre 2025
**Statut**: 🔴 EN ATTENTE RÉPONSE CLIENT
#### Situation actuelle
Lorsqu'un commercial crée un nouveau client manuellement dans CLEO (client non présent dans SAP), le système génère automatiquement un code via :
```php
$newCode = MAX(code) + 1; // cjxdevis.php ligne 1326
```
#### Risque de collision
**Scénario catastrophe** :
1. Commercial crée un client manuel → code auto = `12345`
2. Commercial ajoute des contacts, fait des devis
3. **Import SAP suivant** : un nouveau client SAP arrive avec le code `12345`
4. L'import trouve le client existant (même code) et **écrase toutes les données** du client manuel
5. Les contacts du client manuel deviennent incohérents (pointent vers le mauvais client SAP)
6. Les devis du client manuel sont rattachés au mauvais client SAP
#### Question posée au client
**"Que se passe-t-il lorsqu'un devis avec un nouveau client (code = MAX+1) est intégré dans SAP ?"**
- Le client manuel reçoit-il un vrai code SAP ?
- Le code est-il synchronisé dans CLEO après intégration ?
- Existe-t-il un processus de réconciliation ?
### Solutions techniques envisagées
#### Option A : Plage réservée pour clients manuels
```php
// Codes 9000000+ réservés aux créations manuelles
$newCode = 9000000 + $compteur;
```
**Avantages** : Simple, pas de collision possible
**Inconvénients** : Nécessite coordination avec SAP
#### Option B : Codes négatifs pour clients manuels
```php
// Codes négatifs = clients manuels non SAP
$newCode = -1 * (MAX(ABS(code)) + 1);
```
**Avantages** : Distinction claire SAP/Manuel
**Inconvénients** : Peut poser problème avec certains systèmes
#### Option C : Flag `chk_manual` + protection
```sql
ALTER TABLE clients ADD COLUMN chk_manual TINYINT DEFAULT 0;
```
- `chk_manual = 1` → Client créé manuellement, jamais écrasé par import SAP
- Lors de l'import SAP, ignorer les clients avec `chk_manual = 1`
- Processus manuel de réconciliation si le client est créé dans SAP
**Avantages** : Protection garantie, traçabilité
**Inconvénients** : Nécessite gestion manuelle de la réconciliation
#### Option D : Code temporaire + synchronisation
- Client manuel créé avec code `TEMP_XXXXX`
- Lors de l'intégration SAP, récupération du vrai code SAP
- Mise à jour du code client + tous les contacts/devis associés
**Avantages** : Cohérence totale avec SAP
**Inconvénients** : Complexe, nécessite API ou process de sync
### Actions en attente
- [ ] **Réponse client** sur le processus actuel d'intégration SAP
- [ ] Choix de la solution technique selon la réponse
- [ ] Implémentation de la solution retenue
- [ ] Tests de non-régression sur imports SAP
- [ ] Documentation du processus de gestion des clients manuels
### Impact sur le code existant
**Fichiers concernés** :
- `controllers/cjxdevis.php` : fonction `save_new_client` (ligne 1308)
- `controllers/cjximport.php` : fonction `upload_clients` (ligne 112)
- Documentation utilisateur à mettre à jour
---
## Modification Contacts Clients - Migration vers clients.code
### Contexte
La relation entre `clients_contacts` et `clients` utilise `clients.code` comme clé de référence.
Le système a été conçu pour utiliser le `code` SAP (clé métier immuable) plutôt que le `rowid` (clé technique auto-incrémentée).
### Plan de correction
#### 1. Vérification préalable
- [ ] Lire la structure actuelle de la table `clients`
- [ ] Confirmer que `code` est de type INT
- [ ] Vérifier la contrainte UNIQUE sur `code`
- [ ] Vérifier l'index sur `code`
- [ ] Lire la structure de `clients_contacts`
- [ ] État actuel de `fk_client`
- [ ] Contraintes de clé étrangère existantes
- [ ] Vérifier les données existantes
- [ ] Nombre de contacts déjà enregistrés
- [ ] Cohérence des relations actuelles
#### 2. Modification de la structure
- [ ] Supprimer la contrainte FK actuelle sur `clients_contacts.fk_client`
- [ ] Modifier le type de `clients_contacts.fk_client` pour correspondre à `clients.code`
- [ ] Ajouter la nouvelle contrainte FK référençant `clients.code`
- [ ] `ON DELETE CASCADE`
- [ ] `ON UPDATE CASCADE`
- [ ] Vérifier/ajouter index UNIQUE sur `clients.code` si nécessaire
#### 3. Migration des données
- [ ] Créer un script de migration SQL
- [ ] Sauvegarder les données actuelles de `clients_contacts`
- [ ] Convertir les `fk_client` (rowid → code)
- [ ] Valider la cohérence des données migrées
- [ ] Tester l'intégrité référentielle
#### 4. Adaptation du code applicatif
- [x] ✅ Contrôleur `cjxcontacts.php`
- Aucune modification nécessaire (utilise déjà `fk_client` de manière générique)
- [x] ✅ Contrôleur `cjxdevis.php`
- `load_clients_devis` : modifié pour retourner `clients.code`
- `save_new_client` : modifié pour utiliser `newCode` au lieu de `newClientId`
- [x] ✅ JavaScript `jdevis.js`
- Fonction `autocompleteClient` : modifiée pour utiliser `list[i]['code']` au lieu de `list[i]['rowid']`
- `loadContactsClient(list[i]['code'])` : passe maintenant le code SAP
- [ ] **Import clients SAP** : À TRAITER EN PRIORITÉ
- [ ] Fichier concerné : identifier le contrôleur/script d'import
- [ ] Lors de l'import, si un client existe déjà (même `code`), mettre à jour ses infos SANS changer le `code`
- [ ] Gérer la mise à jour des contacts : les contacts existants doivent conserver leur lien via `fk_client = code`
- [ ] Si import d'un nouveau client : créer avec le `code` SAP fourni
- [ ] IMPORTANT : Ne jamais modifier `clients.code` après création (immuable)
#### 5. Tests et validation
- [ ] Tests de création de contact
- [ ] Tests de modification de contact
- [ ] Tests de suppression de contact (soft delete)
- [ ] Tests de sélection de contact dans un devis
- [ ] Simuler un import SAP et vérifier la stabilité des relations
### Notes techniques
```sql
-- Exemple de modification FK
ALTER TABLE clients_contacts
DROP FOREIGN KEY fk_clients_contacts_client;
ALTER TABLE clients_contacts
ADD CONSTRAINT fk_clients_contacts_client
FOREIGN KEY (fk_client)
REFERENCES clients(code)
ON DELETE CASCADE
ON UPDATE CASCADE;
```
---
## Améliorations techniques prioritaires
### Sécurité
@@ -252,26 +393,15 @@ DB_PASSWORD=<PROD_PASSWORD> # À sécuriser
## Notes de développement
### Structure de la table `clients_contacts` (à créer)
```sql
CREATE TABLE clients_contacts (
rowid INT PRIMARY KEY AUTO_INCREMENT,
fk_client INT NOT NULL,
nom VARCHAR(100),
prenom VARCHAR(100),
fonction VARCHAR(100),
telephone VARCHAR(20),
mobile VARCHAR(20),
email VARCHAR(255),
principal TINYINT DEFAULT 0,
active TINYINT DEFAULT 1,
date_creat DATETIME,
fk_user_creat INT,
date_modif DATETIME,
fk_user_modif INT,
FOREIGN KEY (fk_client) REFERENCES clients(rowid)
);
```
### Structure de la table `clients_contacts` (CRÉÉE - v2.0.3)
Table créée et opérationnelle avec :
- Clé étrangère vers `clients` avec CASCADE
- Gestion du contact principal (un seul par client)
- Soft delete via champ `active`
- Traçabilité (date_creat, fk_user_creat, date_modif, fk_user_modif)
- Index sur fk_client, principal et email
- Contrainte UNIQUE sur rowid
- Voir `docs/migration_clients_contacts.sql` pour la structure complète
### Modifications table `devis` pour SAP
```sql
@@ -285,24 +415,36 @@ ALTER TABLE devis ADD COLUMN erreur_transfert_edi TEXT;
## Résumé de l'état actuel
### ✅ Réalisations (v2.0.2 - 12 septembre 2025)
### ✅ Réalisations
**v2.0.1 (12 septembre 2025)**
1. **Migration DEV complétée** : Architecture séparée application/BDD
2. **Base unique `cleo`** : Fusion réussie de 3 bases en une seule
3. **Sécurité renforcée** : PDO, requêtes préparées, variables d'environnement
4. **Container `dva-front`** : MariaDB supprimé, application PHP uniquement
5. **Container `maria3`** : Base de données centralisée opérationnelle
6. **Audit de sécurité complété** : 14 vulnérabilités SQL identifiées et corrigées
**v2.0.2 (12 septembre 2025)**
1. **Audit de sécurité complété** : 14 vulnérabilités SQL identifiées et corrigées
- 8 critiques (fonction autocomplete, injections dans cjxpost.php, mclients.php, mdevis.php)
- 6 moyennes (cjxdevis.php, cjxexport.php, cjximport.php, mexpxls.php)
7. **Fonctionnalité Réactivation devis** : Bouton permettant de réactiver les devis archivés (statut 20 → 1)
2. **Fonctionnalité Réactivation devis** : Bouton permettant de réactiver les devis archivés (statut 20 → 1)
**v2.0.3 (21 octobre 2025)**
1. **Gestion multi-contacts par client** : Table `clients_contacts` opérationnelle
2. **Interface CRUD complète** : Modale Bootstrap avec création/modification/suppression de contacts
3. **Contrôleur AJAX `cjxcontacts.php`** : 5 endpoints sécurisés avec requêtes préparées
4. **Intégration dans les devis** : Sélecteur de contact avec affichage des infos en lecture seule
5. **Gestion automatique du contact principal** : Un seul contact principal par client
6. **Soft delete** : Prévention de la suppression du dernier contact actif
### 🎯 Prochaines étapes prioritaires
1. **Migration PROD vers IN4** : Export/Import des containers vers `pra-front` et `maria4`
2. **Fonctionnalités métier** : Points 14, 16 (voir sections ci-dessus)
3. **Sécurité XSS** : Audit et correction des failles XSS potentielles
4. **Tests** : Mise en place de tests automatisés de sécurité
1. **Nettoyage BDD** : Supprimer les anciens champs contact de la table `clients` (après validation)
2. **Migration PROD vers IN4** : Export/Import des containers vers `pra-front` et `maria4`
3. **Fonctionnalités métier** : Points 8, 14, 16, 21 (voir sections ci-dessus)
4. **Sécurité XSS** : Audit et correction des failles XSS potentielles
5. **Tests** : Mise en place de tests automatisés de sécurité
---
*Document mis à jour le 12 septembre 2025*
*Version 2.0.2 - Sécurité SQL complète*
*Document mis à jour le 21 octobre 2025*
*Version 2.0.3 - Gestion multi-contacts*

440
docs/backpm7.sh Normal file
View File

@@ -0,0 +1,440 @@
#!/bin/bash
set -uo pipefail
# Note: Removed -e to allow script to continue on errors
# Errors are handled explicitly with ERROR_COUNT
# Parse command line arguments
ONLY_DB=false
if [[ "${1:-}" == "-onlydb" ]]; then
ONLY_DB=true
echo "Mode: Database backup only"
fi
# Configuration
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE="$SCRIPT_DIR/backpm7.yaml"
LOG_DIR="$SCRIPT_DIR/logs"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/backpm7-$(date +%Y%m%d).log"
ERROR_COUNT=0
EMAIL_TO="support@unikoffice.com"
RECAP_FILE="/tmp/backup_recap_$$.txt"
# Clean old log files (keep only last 10)
find "$LOG_DIR" -maxdepth 1 -name "backpm7-*.log" -type f 2>/dev/null | sort -r | tail -n +11 | xargs -r rm -f || true
# Check dependencies - COMMENTED OUT
# for cmd in yq ssh tar openssl; do
# if ! command -v "$cmd" &> /dev/null; then
# echo "ERROR: $cmd is required but not installed" | tee -a "$LOG_FILE"
# exit 1
# fi
# done
# Load config
DIR_BACKUP=$(yq '.global.dir_backup' "$CONFIG_FILE" | tr -d '"')
ENC_KEY_PATH=$(yq '.global.enc_key' "$CONFIG_FILE" | tr -d '"')
# Load encryption key
if [[ ! -f "$ENC_KEY_PATH" ]]; then
echo "ERROR: Encryption key not found: $ENC_KEY_PATH" | tee -a "$LOG_FILE"
exit 1
fi
ENC_KEY=$(cat "$ENC_KEY_PATH")
echo "=== Backup Started $(date) ===" | tee -a "$LOG_FILE"
echo "Backup directory: $DIR_BACKUP" | tee -a "$LOG_FILE"
# Check available disk space
DISK_USAGE=$(df "$DIR_BACKUP" | tail -1 | awk '{print $5}' | sed 's/%//')
DISK_FREE=$((100 - DISK_USAGE))
if [[ $DISK_FREE -lt 20 ]]; then
echo "WARNING: Low disk space! Only ${DISK_FREE}% free on backup partition" | tee -a "$LOG_FILE"
# Send warning email
echo "Sending DISK SPACE WARNING email to $EMAIL_TO (${DISK_FREE}% free)" | tee -a "$LOG_FILE"
if command -v msmtp &> /dev/null; then
{
echo "To: $EMAIL_TO"
echo "Subject: BackupPM7 WARNING - Low disk space (${DISK_FREE}% free)"
echo ""
echo "WARNING: Low disk space on $(hostname)"
echo ""
echo "Backup directory: $DIR_BACKUP"
echo "Disk usage: ${DISK_USAGE}%"
echo "Free space: ${DISK_FREE}%"
echo ""
echo "The backup will continue but please free up some space soon."
echo ""
echo "Date: $(date '+%d.%m.%Y %H:%M')"
} | msmtp "$EMAIL_TO"
echo "DISK SPACE WARNING email sent successfully to $EMAIL_TO" | tee -a "$LOG_FILE"
else
echo "WARNING: msmtp not found - DISK WARNING email NOT sent" | tee -a "$LOG_FILE"
fi
else
echo "Disk space OK: ${DISK_FREE}% free" | tee -a "$LOG_FILE"
fi
# Initialize recap file
echo "BACKUP REPORT - $(hostname) - $(date '+%d.%m.%Y %H')h" > "$RECAP_FILE"
echo "========================================" >> "$RECAP_FILE"
echo "" >> "$RECAP_FILE"
# Function to format size in MB with thousand separator
format_size_mb() {
local file="$1"
if [[ -f "$file" ]]; then
local size_kb=$(du -k "$file" | cut -f1)
local size_mb=$((size_kb / 1024))
# Add thousand separator with printf and sed
printf "%d" "$size_mb" | sed ':a;s/\B[0-9]\{3\}\>/\.&/;ta'
else
echo "0"
fi
}
# Function to backup a single database (must be defined before use)
backup_database() {
local database="$1"
local backup_file="$backup_dir/sql/${database}_$(date +%Y%m%d_%H).sql.gz.enc"
echo " Backing up database: $database" | tee -a "$LOG_FILE"
if [[ "$ssh_user" != "root" ]]; then
CMD_PREFIX="sudo"
else
CMD_PREFIX=""
fi
# Execute backup with encryption
# First test MySQL connection to get clear error messages (|| true to continue on error)
MYSQL_TEST=$(ssh -i "$ssh_key" -p "$ssh_port" -o ConnectTimeout=20 "$ssh_user@$host_ip" \
"$CMD_PREFIX incus exec $container_name -- mariadb -h $db_host -u$db_user -p$db_pass -e 'SELECT 1' 2>&1" 2>/dev/null || true)
if ssh -i "$ssh_key" -p "$ssh_port" -o ConnectTimeout=20 "$ssh_user@$host_ip" \
"$CMD_PREFIX incus exec $container_name -- bash -c 'mariadb-dump -h $db_host -u$db_user -p$db_pass --add-drop-table --create-options --databases $database 2>/dev/null | gzip'" | \
openssl enc -aes-256-cbc -salt -pass pass:"$ENC_KEY" -pbkdf2 > "$backup_file" 2>/dev/null; then
# Validate backup file size (encrypted SQL should be > 100 bytes)
if [[ -f "$backup_file" ]]; then
file_size=$(stat -c%s "$backup_file" 2>/dev/null || echo 0)
if [[ $file_size -lt 100 ]]; then
# Analyze MySQL connection test results
if [[ "$MYSQL_TEST" == *"Access denied"* ]]; then
echo " ERROR: MySQL authentication failed for $database on $host_name/$container_name" | tee -a "$LOG_FILE"
echo " User: $db_user@$db_host - Check password in configuration" | tee -a "$LOG_FILE"
elif [[ "$MYSQL_TEST" == *"Unknown database"* ]]; then
echo " ERROR: Database '$database' does not exist on $host_name/$container_name" | tee -a "$LOG_FILE"
elif [[ "$MYSQL_TEST" == *"Can't connect"* ]]; then
echo " ERROR: Cannot connect to MySQL server at $db_host in $container_name" | tee -a "$LOG_FILE"
else
echo " ERROR: Backup file too small (${file_size} bytes): $database on $host_name/$container_name" | tee -a "$LOG_FILE"
fi
((ERROR_COUNT++))
rm -f "$backup_file"
else
size=$(du -h "$backup_file" | cut -f1)
size_mb=$(format_size_mb "$backup_file")
echo " ✓ Saved (encrypted): $(basename "$backup_file") ($size)" | tee -a "$LOG_FILE"
echo " SQL: $(basename "$backup_file") - ${size_mb} Mo" >> "$RECAP_FILE"
fi
else
echo " ERROR: Backup file not created: $database" | tee -a "$LOG_FILE"
((ERROR_COUNT++))
fi
else
# Analyze MySQL connection test for failed backup
if [[ "$MYSQL_TEST" == *"Access denied"* ]]; then
echo " ERROR: MySQL authentication failed for $database on $host_name/$container_name" | tee -a "$LOG_FILE"
echo " User: $db_user@$db_host - Check password in configuration" | tee -a "$LOG_FILE"
elif [[ "$MYSQL_TEST" == *"Unknown database"* ]]; then
echo " ERROR: Database '$database' does not exist on $host_name/$container_name" | tee -a "$LOG_FILE"
elif [[ "$MYSQL_TEST" == *"Can't connect"* ]]; then
echo " ERROR: Cannot connect to MySQL server at $db_host in $container_name" | tee -a "$LOG_FILE"
else
echo " ERROR: Failed to backup database $database on $host_name/$container_name" | tee -a "$LOG_FILE"
fi
((ERROR_COUNT++))
rm -f "$backup_file"
fi
}
# Process each host
host_count=$(yq '.hosts | length' "$CONFIG_FILE")
for ((i=0; i<$host_count; i++)); do
host_name=$(yq ".hosts[$i].name" "$CONFIG_FILE" | tr -d '"')
host_ip=$(yq ".hosts[$i].ip" "$CONFIG_FILE" | tr -d '"')
ssh_user=$(yq ".hosts[$i].user" "$CONFIG_FILE" | tr -d '"')
ssh_key=$(yq ".hosts[$i].key" "$CONFIG_FILE" | tr -d '"')
ssh_port=$(yq ".hosts[$i].port // 22" "$CONFIG_FILE" | tr -d '"')
echo "Processing host: $host_name ($host_ip)" | tee -a "$LOG_FILE"
echo "" >> "$RECAP_FILE"
echo "HOST: $host_name ($host_ip)" >> "$RECAP_FILE"
echo "----------------------------" >> "$RECAP_FILE"
# Test SSH connection
if ! ssh -i "$ssh_key" -p "$ssh_port" -o ConnectTimeout=20 -o StrictHostKeyChecking=no "$ssh_user@$host_ip" "true" 2>/dev/null; then
echo " ERROR: Cannot connect to $host_name ($host_ip)" | tee -a "$LOG_FILE"
((ERROR_COUNT++))
continue
fi
# Process containers
container_count=$(yq ".hosts[$i].containers | length" "$CONFIG_FILE" 2>/dev/null || echo "0")
for ((c=0; c<$container_count; c++)); do
container_name=$(yq ".hosts[$i].containers[$c].name" "$CONFIG_FILE" | tr -d '"')
echo " Processing container: $container_name" | tee -a "$LOG_FILE"
# Add container to recap
echo " Container: $container_name" >> "$RECAP_FILE"
# Create backup directories
backup_dir="$DIR_BACKUP/$host_name/$container_name"
mkdir -p "$backup_dir"
mkdir -p "$backup_dir/sql"
# Backup directories (skip if -onlydb mode)
if [[ "$ONLY_DB" == "false" ]]; then
dir_count=$(yq ".hosts[$i].containers[$c].dirs | length" "$CONFIG_FILE" 2>/dev/null || echo "0")
for ((d=0; d<$dir_count; d++)); do
dir_path=$(yq ".hosts[$i].containers[$c].dirs[$d]" "$CONFIG_FILE" | sed 's/^"\|"$//g')
# Use sudo if not root
if [[ "$ssh_user" != "root" ]]; then
CMD_PREFIX="sudo"
else
CMD_PREFIX=""
fi
# Special handling for /var/www - backup each subdirectory separately
if [[ "$dir_path" == "/var/www" ]]; then
echo " Backing up subdirectories of $dir_path" | tee -a "$LOG_FILE"
# Get list of subdirectories
subdirs=$(ssh -i "$ssh_key" -p "$ssh_port" -o ConnectTimeout=20 "$ssh_user@$host_ip" \
"$CMD_PREFIX incus exec $container_name -- find /var/www -maxdepth 1 -type d ! -path /var/www" 2>/dev/null || echo "")
for subdir in $subdirs; do
subdir_name=$(basename "$subdir" | tr '/' '_')
backup_file="$backup_dir/www_${subdir_name}_$(date +%Y%m%d_%H).tar.gz"
echo " Backing up: $subdir" | tee -a "$LOG_FILE"
if ssh -i "$ssh_key" -p "$ssh_port" -o ConnectTimeout=20 "$ssh_user@$host_ip" \
"$CMD_PREFIX incus exec $container_name -- tar czf - $subdir 2>/dev/null" > "$backup_file"; then
# Validate backup file size (tar.gz should be > 1KB)
if [[ -f "$backup_file" ]]; then
file_size=$(stat -c%s "$backup_file" 2>/dev/null || echo 0)
if [[ $file_size -lt 1024 ]]; then
echo " WARNING: Backup file very small (${file_size} bytes): $subdir" | tee -a "$LOG_FILE"
# Keep the file but note it's small
size=$(du -h "$backup_file" | cut -f1)
size_mb=$(format_size_mb "$backup_file")
echo " ✓ Saved (small): $(basename "$backup_file") ($size)" | tee -a "$LOG_FILE"
echo " DIR: $(basename "$backup_file") - ${size_mb} Mo (WARNING: small)" >> "$RECAP_FILE"
else
size=$(du -h "$backup_file" | cut -f1)
size_mb=$(format_size_mb "$backup_file")
echo " ✓ Saved: $(basename "$backup_file") ($size)" | tee -a "$LOG_FILE"
echo " DIR: $(basename "$backup_file") - ${size_mb} Mo" >> "$RECAP_FILE"
fi
else
echo " ERROR: Backup file not created: $subdir" | tee -a "$LOG_FILE"
((ERROR_COUNT++))
fi
else
echo " ERROR: Failed to backup $subdir" | tee -a "$LOG_FILE"
((ERROR_COUNT++))
rm -f "$backup_file"
fi
done
else
# Normal backup for other directories
dir_name=$(basename "$dir_path" | tr '/' '_')
backup_file="$backup_dir/${dir_name}_$(date +%Y%m%d_%H).tar.gz"
echo " Backing up: $dir_path" | tee -a "$LOG_FILE"
if ssh -i "$ssh_key" -p "$ssh_port" -o ConnectTimeout=20 "$ssh_user@$host_ip" \
"$CMD_PREFIX incus exec $container_name -- tar czf - $dir_path 2>/dev/null" > "$backup_file"; then
# Validate backup file size (tar.gz should be > 1KB)
if [[ -f "$backup_file" ]]; then
file_size=$(stat -c%s "$backup_file" 2>/dev/null || echo 0)
if [[ $file_size -lt 1024 ]]; then
echo " WARNING: Backup file very small (${file_size} bytes): $dir_path" | tee -a "$LOG_FILE"
# Keep the file but note it's small
size=$(du -h "$backup_file" | cut -f1)
size_mb=$(format_size_mb "$backup_file")
echo " ✓ Saved (small): $(basename "$backup_file") ($size)" | tee -a "$LOG_FILE"
echo " DIR: $(basename "$backup_file") - ${size_mb} Mo (WARNING: small)" >> "$RECAP_FILE"
else
size=$(du -h "$backup_file" | cut -f1)
size_mb=$(format_size_mb "$backup_file")
echo " ✓ Saved: $(basename "$backup_file") ($size)" | tee -a "$LOG_FILE"
echo " DIR: $(basename "$backup_file") - ${size_mb} Mo" >> "$RECAP_FILE"
fi
else
echo " ERROR: Backup file not created: $dir_path" | tee -a "$LOG_FILE"
((ERROR_COUNT++))
fi
else
echo " ERROR: Failed to backup $dir_path" | tee -a "$LOG_FILE"
((ERROR_COUNT++))
rm -f "$backup_file"
fi
fi
done
fi # End of directory backup section
# Backup databases
db_user=$(yq ".hosts[$i].containers[$c].db_user" "$CONFIG_FILE" 2>/dev/null | tr -d '"')
db_pass=$(yq ".hosts[$i].containers[$c].db_pass" "$CONFIG_FILE" 2>/dev/null | tr -d '"')
db_host=$(yq ".hosts[$i].containers[$c].db_host // \"localhost\"" "$CONFIG_FILE" 2>/dev/null | tr -d '"')
# Check if we're in onlydb mode
if [[ "$ONLY_DB" == "true" ]]; then
# Use onlydb list if it exists
onlydb_count=$(yq ".hosts[$i].containers[$c].onlydb | length" "$CONFIG_FILE" 2>/dev/null || echo "0")
if [[ "$onlydb_count" != "0" ]] && [[ "$onlydb_count" != "null" ]]; then
db_count="$onlydb_count"
use_onlydb=true
else
# No onlydb list, skip this container in onlydb mode
continue
fi
else
# Normal mode - use databases list
db_count=$(yq ".hosts[$i].containers[$c].databases | length" "$CONFIG_FILE" 2>/dev/null || echo "0")
use_onlydb=false
fi
if [[ -n "$db_user" ]] && [[ -n "$db_pass" ]] && [[ "$db_count" != "0" ]]; then
for ((db=0; db<$db_count; db++)); do
if [[ "$use_onlydb" == "true" ]]; then
db_name=$(yq ".hosts[$i].containers[$c].onlydb[$db]" "$CONFIG_FILE" | tr -d '"')
else
db_name=$(yq ".hosts[$i].containers[$c].databases[$db]" "$CONFIG_FILE" | tr -d '"')
fi
if [[ "$db_name" == "ALL" ]]; then
echo " Fetching all databases..." | tee -a "$LOG_FILE"
# Get database list
if [[ "$ssh_user" != "root" ]]; then
db_list=$(ssh -i "$ssh_key" -p "$ssh_port" -o ConnectTimeout=20 "$ssh_user@$host_ip" \
"sudo incus exec $container_name -- mariadb -h $db_host -u$db_user -p$db_pass -e 'SHOW DATABASES;' 2>/dev/null" | \
grep -Ev '^(Database|information_schema|performance_schema|mysql|sys)$' || echo "")
else
db_list=$(ssh -i "$ssh_key" -p "$ssh_port" -o ConnectTimeout=20 "$ssh_user@$host_ip" \
"incus exec $container_name -- mariadb -h $db_host -u$db_user -p$db_pass -e 'SHOW DATABASES;' 2>/dev/null" | \
grep -Ev '^(Database|information_schema|performance_schema|mysql|sys)$' || echo "")
fi
# Backup each database
for single_db in $db_list; do
backup_database "$single_db"
done
else
backup_database "$db_name"
fi
done
fi
done
done
echo "=== Backup Completed $(date) ===" | tee -a "$LOG_FILE"
# Show summary
total_size=$(du -sh "$DIR_BACKUP" 2>/dev/null | cut -f1)
echo "Total backup size: $total_size" | tee -a "$LOG_FILE"
# Add summary to recap
echo "" >> "$RECAP_FILE"
echo "========================================" >> "$RECAP_FILE"
# Add size details per host/container
echo "BACKUP SIZES:" >> "$RECAP_FILE"
for host_dir in "$DIR_BACKUP"/*; do
if [[ -d "$host_dir" ]]; then
host_name=$(basename "$host_dir")
host_size=$(du -sh "$host_dir" 2>/dev/null | cut -f1)
echo " $host_name: $host_size" >> "$RECAP_FILE"
# Size per container
for container_dir in "$host_dir"/*; do
if [[ -d "$container_dir" ]]; then
container_name=$(basename "$container_dir")
container_size=$(du -sh "$container_dir" 2>/dev/null | cut -f1)
echo " - $container_name: $container_size" >> "$RECAP_FILE"
fi
done
fi
done
echo "" >> "$RECAP_FILE"
echo "TOTAL SIZE: $total_size" >> "$RECAP_FILE"
echo "COMPLETED: $(date '+%d.%m.%Y %H:%M')" >> "$RECAP_FILE"
# Prepare email subject with date format
DATE_SUBJECT=$(date '+%d.%m.%Y %H')
# Send recap email
if [[ $ERROR_COUNT -gt 0 ]]; then
echo "Total errors: $ERROR_COUNT" | tee -a "$LOG_FILE"
# Add errors to recap
echo "" >> "$RECAP_FILE"
echo "ERRORS DETECTED: $ERROR_COUNT" >> "$RECAP_FILE"
echo "----------------------------" >> "$RECAP_FILE"
grep -i "ERROR" "$LOG_FILE" >> "$RECAP_FILE"
# Send email with ERROR in subject
echo "Sending ERROR email to $EMAIL_TO (Errors found: $ERROR_COUNT)" | tee -a "$LOG_FILE"
if command -v msmtp &> /dev/null; then
{
echo "To: $EMAIL_TO"
echo "Subject: BackupPM7 ERROR $DATE_SUBJECT"
echo ""
cat "$RECAP_FILE"
} | msmtp "$EMAIL_TO"
echo "ERROR email sent successfully to $EMAIL_TO" | tee -a "$LOG_FILE"
else
echo "WARNING: msmtp not found - ERROR email NOT sent" | tee -a "$LOG_FILE"
fi
else
echo "Backup completed successfully with no errors" | tee -a "$LOG_FILE"
# Send success recap email
echo "Sending SUCCESS recap email to $EMAIL_TO" | tee -a "$LOG_FILE"
if command -v msmtp &> /dev/null; then
{
echo "To: $EMAIL_TO"
echo "Subject: BackupPM7 $DATE_SUBJECT"
echo ""
cat "$RECAP_FILE"
} | msmtp "$EMAIL_TO"
echo "SUCCESS recap email sent successfully to $EMAIL_TO" | tee -a "$LOG_FILE"
else
echo "WARNING: msmtp not found - SUCCESS recap email NOT sent" | tee -a "$LOG_FILE"
fi
fi
# Clean up recap file
rm -f "$RECAP_FILE"
# Exit with error code if there were errors
if [[ $ERROR_COUNT -gt 0 ]]; then
exit 1
fi

View File

@@ -1,9 +1,9 @@
/*M!999999\- enable the sandbox mode */
-- MariaDB dump 10.19 Distrib 10.11.9-MariaDB, for debian-linux-gnu (x86_64)
-- MariaDB dump 10.19-11.8.3-MariaDB, for debian-linux-gnu (x86_64)
--
-- Host: localhost Database: uof_linet
-- Host: localhost Database: cleo
-- ------------------------------------------------------
-- Server version 10.11.9-MariaDB-deb12
-- Server version 11.4.8-MariaDB-log
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
@@ -14,7 +14,7 @@
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
/*M!100616 SET @OLD_NOTE_VERBOSITY=@@NOTE_VERBOSITY, NOTE_VERBOSITY=0 */;
--
-- Table structure for table `clients`
@@ -22,7 +22,7 @@
DROP TABLE IF EXISTS `clients`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `clients` (
`rowid` int(11) NOT NULL AUTO_INCREMENT,
`code` int(11) NOT NULL,
@@ -51,9 +51,38 @@ CREATE TABLE `clients` (
UNIQUE KEY `code_UNIQUE` (`code`),
KEY `libelle` (`libelle`),
KEY `cp` (`cp`)
) ENGINE=InnoDB AUTO_INCREMENT=5307 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
) ENGINE=InnoDB AUTO_INCREMENT=5309 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `clients_contacts`
--
DROP TABLE IF EXISTS `clients_contacts`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `clients_contacts` (
`rowid` int(11) NOT NULL AUTO_INCREMENT,
`fk_client` int(11) NOT NULL,
`nom` varchar(50) DEFAULT NULL,
`prenom` varchar(50) DEFAULT NULL,
`fonction` varchar(50) DEFAULT NULL,
`telephone` varchar(20) DEFAULT NULL,
`mobile` varchar(20) DEFAULT NULL,
`email` varchar(75) DEFAULT NULL,
`principal` tinyint(1) DEFAULT 0 COMMENT 'Contact principal du client',
`active` tinyint(1) DEFAULT 1,
`date_creat` datetime DEFAULT NULL,
`fk_user_creat` int(11) DEFAULT NULL,
`date_modif` datetime DEFAULT NULL,
`fk_user_modif` int(11) DEFAULT NULL,
PRIMARY KEY (`rowid`),
UNIQUE KEY `rowid_UNIQUE` (`rowid`),
KEY `fk_client` (`fk_client`),
KEY `principal` (`fk_client`,`principal`),
KEY `email` (`email`)
) ENGINE=InnoDB AUTO_INCREMENT=8199 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='Contacts multiples par client' `PAGE_COMPRESSED`='ON';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `clients_sites`
@@ -61,7 +90,7 @@ CREATE TABLE `clients` (
DROP TABLE IF EXISTS `clients_sites`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `clients_sites` (
`rowid` int(11) NOT NULL AUTO_INCREMENT,
`fk_client` int(11) NOT NULL,
@@ -77,22 +106,13 @@ CREATE TABLE `clients_sites` (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `clients_sites`
--
LOCK TABLES `clients_sites` WRITE;
/*!40000 ALTER TABLE `clients_sites` DISABLE KEYS */;
/*!40000 ALTER TABLE `clients_sites` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `commerciaux`
--
DROP TABLE IF EXISTS `commerciaux`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `commerciaux` (
`rowid` int(11) NOT NULL AUTO_INCREMENT,
`fk_entite` int(11) DEFAULT 0,
@@ -133,14 +153,13 @@ CREATE TABLE `commerciaux` (
) ENGINE=InnoDB AUTO_INCREMENT=27 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `commerciaux_entites`
--
DROP TABLE IF EXISTS `commerciaux_entites`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `commerciaux_entites` (
`rowid` int(11) NOT NULL AUTO_INCREMENT,
`libelle` varchar(45) DEFAULT NULL,
@@ -157,26 +176,13 @@ CREATE TABLE `commerciaux_entites` (
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `commerciaux_entites`
--
LOCK TABLES `commerciaux_entites` WRITE;
/*!40000 ALTER TABLE `commerciaux_entites` DISABLE KEYS */;
INSERT INTO `commerciaux_entites` VALUES
(1,'LINET',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,1),
(2,'WISSNER-BOSSERHOFF',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,1),
(3,'LINET & WI-BO',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,1);
/*!40000 ALTER TABLE `commerciaux_entites` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `commerciaux_params`
--
DROP TABLE IF EXISTS `commerciaux_params`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `commerciaux_params` (
`rowid` int(11) NOT NULL AUTO_INCREMENT,
`fk_commercial` int(11) DEFAULT NULL,
@@ -269,14 +275,13 @@ CREATE TABLE `commerciaux_params` (
) ENGINE=InnoDB AUTO_INCREMENT=16 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `devis`
--
DROP TABLE IF EXISTS `devis`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `devis` (
`rowid` int(11) NOT NULL AUTO_INCREMENT,
`fk_user` int(11) NOT NULL DEFAULT 0,
@@ -285,6 +290,7 @@ CREATE TABLE `devis` (
`date_remise` date DEFAULT NULL,
`num_opportunite` varchar(8) NOT NULL DEFAULT '',
`fk_client` int(11) NOT NULL DEFAULT 0,
`fk_contact` int(11) DEFAULT NULL,
`fk_marche` int(11) NOT NULL DEFAULT 0,
`fk_statut_devis` int(11) NOT NULL DEFAULT 0,
`chk_clients_secteur` tinyint(1) NOT NULL DEFAULT 1,
@@ -327,18 +333,18 @@ CREATE TABLE `devis` (
KEY `fk_client` (`fk_client`),
KEY `fk_statut_devis` (`fk_statut_devis`),
KEY `date_demande` (`date_demande`),
KEY `dossier` (`fk_user`,`dossier`)
) ENGINE=InnoDB AUTO_INCREMENT=4611 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
KEY `dossier` (`fk_user`,`dossier`),
KEY `fk_contact` (`fk_contact`)
) ENGINE=InnoDB AUTO_INCREMENT=4624 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `devis_histo`
--
DROP TABLE IF EXISTS `devis_histo`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `devis_histo` (
`rowid` int(11) NOT NULL AUTO_INCREMENT,
`fk_devis` int(11) DEFAULT NULL,
@@ -350,7 +356,7 @@ CREATE TABLE `devis_histo` (
`fk_statut_devis` int(11) DEFAULT NULL,
PRIMARY KEY (`rowid`),
KEY `devis_histo_fk_devis_index` (`fk_devis`)
) ENGINE=InnoDB AUTO_INCREMENT=22331 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
) ENGINE=InnoDB AUTO_INCREMENT=22388 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
/*!40101 SET character_set_client = @saved_cs_client */;
--
@@ -359,7 +365,7 @@ CREATE TABLE `devis_histo` (
DROP TABLE IF EXISTS `devis_produits`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `devis_produits` (
`rowid` int(11) NOT NULL AUTO_INCREMENT,
`fk_devis` int(11) NOT NULL,
@@ -392,17 +398,16 @@ CREATE TABLE `devis_produits` (
PRIMARY KEY (`rowid`),
KEY `devis_produits__devis` (`fk_devis`),
KEY `devis_produits__produit` (`fk_produit`)
) ENGINE=InnoDB AUTO_INCREMENT=29277 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
) ENGINE=InnoDB AUTO_INCREMENT=29314 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `devis_speciaux`
--
DROP TABLE IF EXISTS `devis_speciaux`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `devis_speciaux` (
`rowid` int(11) NOT NULL AUTO_INCREMENT,
`fk_devis` int(11) NOT NULL DEFAULT 0,
@@ -461,14 +466,13 @@ CREATE TABLE `devis_speciaux` (
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `entites`
--
DROP TABLE IF EXISTS `entites`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `entites` (
`rowid` int(11) NOT NULL AUTO_INCREMENT,
`libelle` varchar(45) DEFAULT '',
@@ -513,14 +517,13 @@ CREATE TABLE `entites` (
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `import_ventes`
--
DROP TABLE IF EXISTS `import_ventes`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `import_ventes` (
`rowid` int(11) NOT NULL AUTO_INCREMENT,
`source` varchar(15) DEFAULT '',
@@ -570,7 +573,7 @@ CREATE TABLE `import_ventes` (
DROP TABLE IF EXISTS `infos`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `infos` (
`rowid` int(11) NOT NULL AUTO_INCREMENT,
`date_infos` date DEFAULT NULL,
@@ -586,14 +589,13 @@ CREATE TABLE `infos` (
) ENGINE=InnoDB AUTO_INCREMENT=29 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `marches`
--
DROP TABLE IF EXISTS `marches`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `marches` (
`rowid` int(11) NOT NULL AUTO_INCREMENT,
`numero` varchar(20) NOT NULL DEFAULT '',
@@ -642,7 +644,7 @@ CREATE TABLE `marches` (
DROP TABLE IF EXISTS `marches_listes`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `marches_listes` (
`rowid` int(11) NOT NULL AUTO_INCREMENT,
`fk_marche` int(11) DEFAULT NULL,
@@ -660,7 +662,7 @@ CREATE TABLE `marches_listes` (
DROP TABLE IF EXISTS `marches_produits`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `marches_produits` (
`rowid` int(11) NOT NULL AUTO_INCREMENT,
`fk_marche` int(11) DEFAULT 0,
@@ -702,7 +704,7 @@ CREATE TABLE `marches_produits` (
DROP TABLE IF EXISTS `marches_versions`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `marches_versions` (
`rowid` int(11) NOT NULL AUTO_INCREMENT COMMENT 'Id',
`libelle` varchar(75) DEFAULT NULL COMMENT 'Libellé',
@@ -713,24 +715,13 @@ CREATE TABLE `marches_versions` (
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='Version des marchés' `PAGE_COMPRESSED`='ON';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `marches_versions`
--
LOCK TABLES `marches_versions` WRITE;
/*!40000 ALTER TABLE `marches_versions` DISABLE KEYS */;
INSERT INTO `marches_versions` VALUES
(1,'Version Avril 2022','2022-04-01','0000-00-00',1);
/*!40000 ALTER TABLE `marches_versions` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `medias`
--
DROP TABLE IF EXISTS `medias`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `medias` (
`rowid` int(11) NOT NULL AUTO_INCREMENT,
`dir0` varchar(150) DEFAULT NULL,
@@ -746,7 +737,7 @@ CREATE TABLE `medias` (
PRIMARY KEY (`rowid`),
UNIQUE KEY `rowid_UNIQUE` (`rowid`),
KEY `support` (`support`,`support_rowid`)
) ENGINE=InnoDB AUTO_INCREMENT=3866 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
) ENGINE=InnoDB AUTO_INCREMENT=3878 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
/*!40101 SET character_set_client = @saved_cs_client */;
--
@@ -755,7 +746,7 @@ CREATE TABLE `medias` (
DROP TABLE IF EXISTS `notifications`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `notifications` (
`rowid` int(11) NOT NULL AUTO_INCREMENT,
`dateheure` datetime DEFAULT NULL,
@@ -775,7 +766,7 @@ CREATE TABLE `notifications` (
DROP TABLE IF EXISTS `produits`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `produits` (
`rowid` int(11) NOT NULL AUTO_INCREMENT,
`fk_marche` int(11) NOT NULL DEFAULT 0,
@@ -821,7 +812,7 @@ CREATE TABLE `produits` (
DROP TABLE IF EXISTS `produits_familles`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `produits_familles` (
`rowid` int(11) NOT NULL AUTO_INCREMENT,
`groupe` varchar(30) NOT NULL,
@@ -841,7 +832,7 @@ CREATE TABLE `produits_familles` (
DROP TABLE IF EXISTS `regions`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `regions` (
`rowid` int(11) NOT NULL AUTO_INCREMENT,
`libelle` varchar(75) DEFAULT NULL,
@@ -851,22 +842,13 @@ CREATE TABLE `regions` (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `regions`
--
LOCK TABLES `regions` WRITE;
/*!40000 ALTER TABLE `regions` DISABLE KEYS */;
/*!40000 ALTER TABLE `regions` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `simul`
--
DROP TABLE IF EXISTS `simul`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `simul` (
`rowid` int(11) NOT NULL AUTO_INCREMENT,
`fk_import_vente` int(11) DEFAULT NULL,
@@ -885,14 +867,13 @@ CREATE TABLE `simul` (
) ENGINE=InnoDB AUTO_INCREMENT=1057 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `users`
--
DROP TABLE IF EXISTS `users`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `users` (
`rowid` int(10) unsigned NOT NULL AUTO_INCREMENT,
`fk_entite` int(11) DEFAULT NULL,
@@ -940,13 +921,64 @@ CREATE TABLE `users` (
) ENGINE=InnoDB AUTO_INCREMENT=50 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `users_entites`
--
DROP TABLE IF EXISTS `users_entites`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `users_entites` (
`rowid` int(11) NOT NULL AUTO_INCREMENT,
`libelle` varchar(45) DEFAULT '',
`http_host` varchar(150) DEFAULT '',
`adresse1` varchar(45) DEFAULT '',
`adresse2` varchar(45) DEFAULT '',
`cp` varchar(5) DEFAULT '',
`ville` varchar(45) DEFAULT '',
`type_entite` varchar(5) DEFAULT 'form',
`tva_intra` varchar(15) DEFAULT '',
`rcs` varchar(45) DEFAULT '',
`siret` varchar(17) DEFAULT NULL,
`ape` varchar(5) DEFAULT '',
`num_opca` varchar(15) DEFAULT '',
`logo` varchar(45) DEFAULT '',
`tel1` varchar(20) DEFAULT '',
`tel2` varchar(20) DEFAULT '',
`couleur` varchar(7) DEFAULT '#FFFAF0',
`prefecture` varchar(45) DEFAULT 'Bretagne',
`fk_titre_gerant` int(11) DEFAULT 1,
`gerant_prenom` varchar(45) DEFAULT '',
`gerant_nom` varchar(45) DEFAULT '',
`email` varchar(45) DEFAULT '',
`site_url` varchar(45) DEFAULT '',
`gerant_signature` varchar(45) DEFAULT '',
`tampon_signature` varchar(45) DEFAULT '',
`rib_banque` varchar(5) DEFAULT '',
`rib_guichet` varchar(5) DEFAULT '',
`rib_compte` varchar(11) DEFAULT '',
`rib_cle` varchar(2) DEFAULT '',
`rib_domiciliation` varchar(45) DEFAULT '',
`iban` varchar(33) DEFAULT '',
`bic` varchar(15) DEFAULT '',
`demo` tinyint(1) DEFAULT 0,
`genbase` varchar(45) DEFAULT '0',
`groupebase` varchar(45) DEFAULT '0',
`table_users_gen` varchar(50) DEFAULT '',
`appname` varchar(45) DEFAULT '',
`raz_num_devis` tinyint(1) DEFAULT 0,
`active` tinyint(1) DEFAULT 1,
PRIMARY KEY (`rowid`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci `PAGE_COMPRESSED`='ON';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `ventes`
--
DROP TABLE IF EXISTS `ventes`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `ventes` (
`rowid` int(11) NOT NULL AUTO_INCREMENT,
`source` varchar(45) DEFAULT NULL,
@@ -977,22 +1009,13 @@ CREATE TABLE `ventes` (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `ventes`
--
LOCK TABLES `ventes` WRITE;
/*!40000 ALTER TABLE `ventes` DISABLE KEYS */;
/*!40000 ALTER TABLE `ventes` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `x_clients_types`
--
DROP TABLE IF EXISTS `x_clients_types`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `x_clients_types` (
`rowid` int(11) NOT NULL AUTO_INCREMENT,
`code` char(3) DEFAULT NULL,
@@ -1004,28 +1027,13 @@ CREATE TABLE `x_clients_types` (
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `x_clients_types`
--
LOCK TABLES `x_clients_types` WRITE;
/*!40000 ALTER TABLE `x_clients_types` DISABLE KEYS */;
INSERT INTO `x_clients_types` VALUES
(1,'PUB','Public',1),
(2,'PRA','Privé Associatif',1),
(3,'PRD','Privé Distributeur',1),
(4,'PRC','Privé Commercial',1),
(5,'ESP','ESPIC',1);
/*!40000 ALTER TABLE `x_clients_types` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `x_familles`
--
DROP TABLE IF EXISTS `x_familles`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `x_familles` (
`rowid` int(11) NOT NULL AUTO_INCREMENT,
`libelle` varchar(20) NOT NULL DEFAULT '',
@@ -1037,34 +1045,13 @@ CREATE TABLE `x_familles` (
) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `x_familles`
--
LOCK TABLES `x_familles` WRITE;
/*!40000 ALTER TABLE `x_familles` DISABLE KEYS */;
INSERT INTO `x_familles` VALUES
(3,'Lits SBU1',1,1),
(4,'Lits SBU2',2,1),
(5,'Accessoires SBU1',3,1),
(6,'Accessoires SBU2',4,1),
(7,'Services',5,1),
(8,'Matelas mousse',6,1),
(9,'Matelas à air',7,1),
(10,'Mobilier',8,1),
(11,'Assises',9,1),
(12,'Autres',11,1),
(13,'Domalys',10,1);
/*!40000 ALTER TABLE `x_familles` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `x_regions`
--
DROP TABLE IF EXISTS `x_regions`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `x_regions` (
`rowid` int(11) NOT NULL AUTO_INCREMENT,
`fk_entite` int(11) DEFAULT 0,
@@ -1075,39 +1062,13 @@ CREATE TABLE `x_regions` (
) ENGINE=InnoDB AUTO_INCREMENT=20 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `x_regions`
--
LOCK TABLES `x_regions` WRITE;
/*!40000 ALTER TABLE `x_regions` DISABLE KEYS */;
INSERT INTO `x_regions` VALUES
(1,1,'SUD-OUEST',1),
(2,1,'RHONE-ALPES / AUVERGNE',1),
(3,1,'PACA',1),
(4,1,'EST',1),
(5,1,'NORD',1),
(6,1,'GRAND-OUEST',1),
(7,1,'IDF',1),
(8,1,'DOM-TOM',1),
(9,2,'WB-NORD',1),
(13,2,'WB-SUD OUEST',1),
(14,2,'WB-EST',1),
(15,2,'WB-ILE DE FRANCE',1),
(16,2,'WB-SUD-EST',1),
(17,2,'WB-CENTRE-EST ET DOM',1),
(18,1,'DIRECTION',1),
(19,2,'WB-NORD OUEST',1);
/*!40000 ALTER TABLE `x_regions` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `x_roles`
--
DROP TABLE IF EXISTS `x_roles`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `x_roles` (
`rowid` int(11) NOT NULL AUTO_INCREMENT,
`libelle` varchar(45) DEFAULT '',
@@ -1118,30 +1079,13 @@ CREATE TABLE `x_roles` (
) ENGINE=InnoDB AUTO_INCREMENT=91 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='Les différents rôles des utilisateurs' `PAGE_COMPRESSED`='ON';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `x_roles`
--
LOCK TABLES `x_roles` WRITE;
/*!40000 ALTER TABLE `x_roles` DISABLE KEYS */;
INSERT INTO `x_roles` VALUES
(1,'Direction Commerciale','DC',1),
(2,'Direction des Ventes','DV',1),
(3,'Commercial(e)','RR',1),
(4,'Clinicien(ne)','CL',1),
(5,'Direction Grands Comptes','GC',1),
(20,'Administration des ventes','ADV',1),
(90,'Administrateur','ADM',1);
/*!40000 ALTER TABLE `x_roles` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `x_statuts_devis`
--
DROP TABLE IF EXISTS `x_statuts_devis`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `x_statuts_devis` (
`rowid` int(11) NOT NULL AUTO_INCREMENT,
`libelle` varchar(30) DEFAULT NULL,
@@ -1152,22 +1096,47 @@ CREATE TABLE `x_statuts_devis` (
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `x_statuts_devis`
-- Table structure for table `y_pages`
--
LOCK TABLES `x_statuts_devis` WRITE;
/*!40000 ALTER TABLE `x_statuts_devis` DISABLE KEYS */;
INSERT INTO `x_statuts_devis` VALUES
(1,'En cours de création',1),
(2,'En cours de validation DIR-CO',1),
(3,'En cours de validation DV/DGC',1),
(4,'A traiter sur SAP',1),
(6,'A vérifier par le RR',1),
(7,'A envoyer au client',1),
(10,'Envoyé au client',0),
(20,'Archivé',1);
/*!40000 ALTER TABLE `x_statuts_devis` ENABLE KEYS */;
UNLOCK TABLES;
DROP TABLE IF EXISTS `y_pages`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `y_pages` (
`rowid` int(11) NOT NULL AUTO_INCREMENT,
`fk_parent` int(11) DEFAULT 0,
`link` varchar(75) DEFAULT NULL,
`libelle` varchar(45) DEFAULT NULL,
`titre` varchar(75) DEFAULT NULL,
`tooltip` varchar(45) DEFAULT NULL,
`description` varchar(200) DEFAULT NULL,
`keywords` varchar(200) DEFAULT NULL,
`script` varchar(45) DEFAULT NULL,
`enmaintenance` tinyint(1) DEFAULT 0 COMMENT '0 libre d''accès, 1 en maintenance mais accès aux données, 2 en maintenance sans accès aux données',
`admin` tinyint(1) DEFAULT 0,
`mail` tinyint(1) DEFAULT 0,
`admtools` tinyint(1) DEFAULT 0,
`magazine` tinyint(1) DEFAULT 0,
`files` tinyint(1) DEFAULT 0,
`editor` tinyint(1) DEFAULT 0,
`autocomplete` tinyint(1) DEFAULT 0,
`print` tinyint(1) DEFAULT 0,
`form` tinyint(1) DEFAULT 0,
`sidebar` tinyint(1) DEFAULT 0,
`chart` tinyint(1) DEFAULT 0,
`agenda` tinyint(1) DEFAULT 0,
`scheduler` tinyint(1) DEFAULT 0,
`osm` tinyint(1) DEFAULT 0,
`layout` varchar(45) DEFAULT 'default.php',
`in_menu` tinyint(1) DEFAULT 1,
`ordre_menu` int(11) DEFAULT 0,
`active` tinyint(1) DEFAULT 1,
PRIMARY KEY (`rowid`),
UNIQUE KEY `rowid_UNIQUE` (`rowid`),
KEY `script` (`script`),
KEY `admin` (`admin`)
) ENGINE=InnoDB AUTO_INCREMENT=30 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci `PAGE_COMPRESSED`='ON';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Table structure for table `z_history`
@@ -1175,7 +1144,7 @@ UNLOCK TABLES;
DROP TABLE IF EXISTS `z_history`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `z_history` (
`fk_user` int(11) NOT NULL,
`libelle` varchar(20) NOT NULL DEFAULT 'tiers',
@@ -1185,22 +1154,13 @@ CREATE TABLE `z_history` (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping data for table `z_history`
--
LOCK TABLES `z_history` WRITE;
/*!40000 ALTER TABLE `z_history` DISABLE KEYS */;
/*!40000 ALTER TABLE `z_history` ENABLE KEYS */;
UNLOCK TABLES;
--
-- Table structure for table `z_logs`
--
DROP TABLE IF EXISTS `z_logs`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `z_logs` (
`date` datetime NOT NULL,
`ip` varchar(15) NOT NULL,
@@ -1220,7 +1180,7 @@ CREATE TABLE `z_logs` (
DROP TABLE IF EXISTS `z_sessions`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `z_sessions` (
`sid` text NOT NULL,
`fk_user` int(11) NOT NULL,
@@ -1238,7 +1198,7 @@ CREATE TABLE `z_sessions` (
DROP TABLE IF EXISTS `z_stats`;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
/*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `z_stats` (
`rowid` int(11) NOT NULL AUTO_INCREMENT,
`libelle` varchar(75) DEFAULT NULL,
@@ -1253,6 +1213,10 @@ CREATE TABLE `z_stats` (
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
/*!40101 SET character_set_client = @saved_cs_client */;
--
-- Dumping routines for database 'cleo'
--
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
@@ -1260,6 +1224,6 @@ CREATE TABLE `z_stats` (
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
/*M!100616 SET NOTE_VERBOSITY=@OLD_NOTE_VERBOSITY */;
-- Dump completed on 2025-09-11 14:44:41
-- Dump completed on 2025-12-02 11:57:09

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,143 @@
-- ============================================================================
-- MIGRATION: Gestion multi-contacts par client
-- Version: 2.0.3
-- Date: 2025-10-21
--
-- Description:
-- - Création de la table clients_contacts
-- - Migration des contacts existants depuis la table clients
-- - Ajout du champ fk_contact dans la table devis
--
-- IMPORTANT: Ce script ne modifie PAS la table clients (champs conservés)
-- ============================================================================
USE cleo;
-- ============================================================================
-- ÉTAPE 1: Création de la table clients_contacts
-- ============================================================================
DROP TABLE IF EXISTS `clients_contacts`;
CREATE TABLE `clients_contacts` (
`rowid` int(11) NOT NULL AUTO_INCREMENT,
`fk_client` int(11) NOT NULL,
`nom` varchar(50) DEFAULT NULL,
`prenom` varchar(50) DEFAULT NULL,
`fonction` varchar(50) DEFAULT NULL,
`telephone` varchar(20) DEFAULT NULL,
`mobile` varchar(20) DEFAULT NULL,
`email` varchar(75) DEFAULT NULL,
`principal` tinyint(1) DEFAULT 0 COMMENT 'Contact principal du client',
`active` tinyint(1) DEFAULT 1,
`date_creat` datetime DEFAULT NULL,
`fk_user_creat` int(11) DEFAULT NULL,
`date_modif` datetime DEFAULT NULL,
`fk_user_modif` int(11) DEFAULT NULL,
PRIMARY KEY (`rowid`),
UNIQUE KEY `rowid_UNIQUE` (`rowid`),
KEY `fk_client` (`fk_client`),
KEY `principal` (`fk_client`, `principal`),
KEY `email` (`email`),
CONSTRAINT `clients_contacts_fk_client` FOREIGN KEY (`fk_client`) REFERENCES `clients` (`rowid`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='Contacts multiples par client' `PAGE_COMPRESSED`='ON';
-- ============================================================================
-- ÉTAPE 2: Migration des contacts existants depuis la table clients
-- ============================================================================
INSERT INTO `clients_contacts` (
`fk_client`,
`nom`,
`prenom`,
`fonction`,
`telephone`,
`mobile`,
`email`,
`principal`,
`active`,
`date_creat`,
`fk_user_creat`,
`date_modif`,
`fk_user_modif`
)
SELECT
c.rowid AS fk_client,
c.contact_nom AS nom,
c.contact_prenom AS prenom,
c.contact_fonction AS fonction,
c.telephone,
c.mobile,
c.email,
1 AS principal,
c.active,
c.date_creat,
c.fk_user_creat,
c.date_modif,
c.fk_user_modif
FROM `clients` c
WHERE c.active = 1
AND (
c.contact_nom IS NOT NULL
OR c.contact_prenom IS NOT NULL
OR c.email IS NOT NULL
OR c.telephone IS NOT NULL
OR c.mobile IS NOT NULL
);
-- ============================================================================
-- ÉTAPE 3: Ajout du champ fk_contact dans la table devis
-- ============================================================================
ALTER TABLE `devis`
ADD COLUMN `fk_contact` int(11) DEFAULT NULL AFTER `fk_client`,
ADD KEY `fk_contact` (`fk_contact`);
-- ============================================================================
-- ÉTAPE 4: Liaison des devis existants avec les contacts principaux
-- ============================================================================
UPDATE `devis` d
INNER JOIN `clients_contacts` cc ON d.fk_client = cc.fk_client AND cc.principal = 1
SET d.fk_contact = cc.rowid
WHERE d.fk_client > 0;
-- ============================================================================
-- ÉTAPE 5: Vérifications post-migration
-- ============================================================================
-- Nombre de clients avec contacts
SELECT COUNT(*) AS 'Clients avec contacts migrés'
FROM clients_contacts;
-- Nombre de clients actifs
SELECT COUNT(*) AS 'Total clients actifs'
FROM clients
WHERE active = 1;
-- Nombre de contacts principaux
SELECT COUNT(*) AS 'Contacts principaux'
FROM clients_contacts
WHERE principal = 1;
-- Devis avec contact associé
SELECT COUNT(*) AS 'Devis avec contact associé'
FROM devis
WHERE fk_contact IS NOT NULL;
-- Devis sans contact (à vérifier)
SELECT COUNT(*) AS 'Devis SANS contact (à vérifier)'
FROM devis
WHERE fk_client > 0 AND fk_contact IS NULL;
-- Clients sans contact migré (potentiellement vides)
SELECT c.rowid, c.code, c.libelle
FROM clients c
LEFT JOIN clients_contacts cc ON c.rowid = cc.fk_client
WHERE c.active = 1
AND cc.rowid IS NULL
LIMIT 10;
-- ============================================================================
-- FIN DE LA MIGRATION
-- ============================================================================

File diff suppressed because one or more lines are too long

View File

@@ -21,11 +21,8 @@
OR c.contact_fonction LIKE :search
ORDER BY c.libelle';
$stmt = $db->prepare($sql);
$searchParam = '%' . $search . '%';
$stmt->bindParam(':search', $searchParam, PDO::PARAM_STR);
$stmt->execute();
$aModel["clients"] = $stmt->fetchAll(PDO::FETCH_ASSOC);
$aModel["clients"] = $db->fetchAll($sql, [':search' => $searchParam]);
} catch (Exception $e) {
error_log("Erreur recherche clients : " . $e->getMessage());
$aModel["clients"] = [];

View File

@@ -17,9 +17,7 @@ switch ($fkRole) {
try {
$db = Database::getInstance();
$sql = 'SELECT rowid FROM users WHERE fk_parent = :fkParent';
$stmt = $db->prepare($sql);
$stmt->execute([':fkParent' => $fkUser]);
$aRR = $stmt->fetchAll(PDO::FETCH_ASSOC);
$aRR = $db->fetchAll($sql, [':fkParent' => $fkUser]);
$rrIds = array_column($aRR, 'rowid');
if (!empty($rrIds)) {

View File

@@ -1,8 +1,7 @@
<?php
global $Route;
function cleanData(&$str)
{
function cleanData(&$str) {
// Fonction de nettoyage des données pour l'export Excel
if ($str == 't') $str = 'TRUE';
if ($str == 'f') $str = 'FALSE';
@@ -13,8 +12,7 @@ function cleanData(&$str)
$str = mb_convert_encoding($str, 'UTF-16LE', 'UTF-8');
}
function filterData(&$str)
{
function filterData(&$str) {
$str = preg_replace("/\t/", "\\t", $str);
$str = preg_replace("/\r?\n/", "\\n", $str);
if ($str == 't') $str = 'TRUE';
@@ -57,12 +55,18 @@ switch ($Route->_action) {
case "export_sap_devis":
$cid = nettoie_input($Route->_param1);
$cidSafe = intval($cid);
$sql = 'SELECT d.* FROM devis d WHERE d.rowid=' . $cidSafe . ';';
eLog("Export Excel SAP Devis : " . $sql);
$dev = getinfos($sql, "gen");
$devis = $dev[0];
try {
$db = Database::getInstance();
$sql = 'SELECT d.* FROM devis d WHERE d.rowid = :devis_id';
$devis = $db->fetchOne($sql, [':devis_id' => $cidSafe]);
if (!$devis) {
throw new Exception("Devis non trouvé");
}
eLog("Export Excel SAP Devis : " . $cidSafe);
$fileName = "devis_" . $cid . "_" . date('Y_m_d_hi') . ".xls";
@@ -71,48 +75,54 @@ switch ($Route->_action) {
$excelData = implode("\t", array_values($fields)) . "\n";
$fkClientSafe = intval($devis["fk_client"]);
$sql = 'SELECT c.code, c.libelle, c.adresse1, c.adresse2, c.adresse3, c.cp, c.ville FROM clients c WHERE c.rowid=' . $fkClientSafe . ';';
$cli = getinfos($sql, "gen");
if (count($cli) == 0) {
// c'est un nouveau client, on affiche les données client enregistrées dans le devis
$sql = 'SELECT "0" AS code, d.lib_new_client, d.adresse1_new_client, d.adresse2_new_client, d.adresse3_new_client, d.cp_new_client, d.ville_new_client FROM devis d WHERE d.rowid=' . $cidSafe . ';';
$cli = getinfos($sql, "gen");
$client = $cli[0];
if ($fkClientSafe == 0) {
// Nouveau client : données depuis le devis
$sql = 'SELECT "0" AS code, d.lib_new_client, d.adresse1_new_client, d.adresse2_new_client, d.adresse3_new_client, d.cp_new_client, d.ville_new_client FROM devis d WHERE d.rowid = :devis_id';
$client = $db->fetchOne($sql, [':devis_id' => $cidSafe]);
array_walk($client, 'filterData');
$excelData .= implode("\t", array_values($client)) . "\n";
// une ligne vierge de séparation
$excelData .= "\n";
// Les données du contact à prendre aussi dans le devis
$sql = 'SELECT d.contact_new_nom, d.contact_new_prenom, d.contact_new_fonction, d.new_telephone, d.new_mobile, d.new_email FROM devis d WHERE d.rowid=' . $cidSafe . ';';
$cont = getinfos($sql, "gen");
$contact = $cont[0];
// Contact depuis le devis
$sql = 'SELECT d.contact_new_nom, d.contact_new_prenom, d.contact_new_fonction, d.new_telephone, d.new_mobile, d.new_email FROM devis d WHERE d.rowid = :devis_id';
$contact = $db->fetchOne($sql, [':devis_id' => $cidSafe]);
$fields = array("Contact Nom", "Prenom", "Fonction", "Fixe", "Mobile", "Email");
$excelData .= implode("\t", array_values($fields)) . "\n";
array_walk($contact, 'filterData');
$excelData .= implode("\t", array_values($contact)) . "\n";
} else {
$client = $cli[0];
// Client existant : données depuis la table clients
$sql = 'SELECT c.code, c.libelle, c.adresse1, c.adresse2, c.adresse3, c.cp, c.ville FROM clients c WHERE c.rowid = :client_id';
$client = $db->fetchOne($sql, [':client_id' => $fkClientSafe]);
array_walk($client, 'filterData');
$excelData .= implode("\t", array_values($client)) . "\n";
// une ligne vierge de séparation
$excelData .= "\n";
// Les données du contact
$sql = 'SELECT c.contact_nom, c.contact_prenom, c.contact_fonction, c.telephone, c.mobile, c.email FROM clients c WHERE c.rowid=' . $fkClientSafe . ';';
$cont = getinfos($sql, "gen");
$contact = $cont[0];
// Contact lié au devis via devis.fk_contact et clients_contacts
$fkContactSafe = intval($devis["fk_contact"]);
if ($fkContactSafe > 0) {
$sql = 'SELECT cc.nom, cc.prenom, cc.fonction, cc.telephone, cc.mobile, cc.email FROM clients_contacts cc WHERE cc.rowid = :contact_id AND cc.active = 1';
$contact = $db->fetchOne($sql, [':contact_id' => $fkContactSafe]);
} else {
// Fallback : contact principal du client
$sql = 'SELECT cc.nom, cc.prenom, cc.fonction, cc.telephone, cc.mobile, cc.email FROM clients_contacts cc WHERE cc.fk_client = :client_id AND cc.principal = 1 AND cc.active = 1 LIMIT 1';
$contact = $db->fetchOne($sql, [':client_id' => $fkClientSafe]);
}
if ($contact) {
$fields = array("Contact Nom", "Prenom", "Fonction", "Fixe", "Mobile", "Email");
$excelData .= implode("\t", array_values($fields)) . "\n";
array_walk($contact, 'filterData');
$excelData .= implode("\t", array_values($contact)) . "\n";
} else {
// Aucun contact trouvé
$excelData .= "Contact Nom\tPrenom\tFonction\tFixe\tMobile\tEmail\n";
$excelData .= "\t\t\t\t\t\n";
}
}
// une ligne vierge de séparation
$excelData .= "\n";
@@ -121,30 +131,24 @@ switch ($Route->_action) {
$sql = 'SELECT d.rowid, d.num_opportunite, IF(d.date_demande IS NULL OR d.date_demande="0000-00-00", "", DATE_FORMAT(d.date_demande, "%d/%m/%Y")) AS datedem, ';
$sql .= 'IF(d.date_remise IS NULL OR d.date_remise="0000-00-00", "", DATE_FORMAT(d.date_remise, "%d/%m/%Y")) AS daterem, m.libelle AS lib_marche, m.numero AS num_marche, m.nom AS nom_marche, ';
$sql .= 'IF(d.chk_devis_photos=1, "Oui", "Non") AS photos, d.commentaire, IF(d.chk_speciaux=1, "Oui", "Non") AS speciaux ';
$sql .= 'FROM devis d LEFT JOIN marches m ON d.fk_marche=m.rowid WHERE d.rowid=' . $cidSafe . ';';
$dev = getinfos($sql, "gen");
$devis = $dev[0];
$chkSpeciaux = $devis["speciaux"];
$sql .= 'FROM devis d LEFT JOIN marches m ON d.fk_marche=m.rowid WHERE d.rowid = :devis_id';
$devisData = $db->fetchOne($sql, [':devis_id' => $cidSafe]);
$chkSpeciaux = $devisData["speciaux"];
$fields = array("Devis", "Opportunite", "Date Demande", "Date remise client", "Marche", "Num Marche", "Nom Marche", "Avec photos", "Commentaire RR", "Speciaux");
$excelData .= implode("\t", array_values($fields)) . "\n";
array_walk($devis, 'filterData');
$excelData .= implode("\t", array_values($devis)) . "\n";
// une ligne vierge de séparation
array_walk($devisData, 'filterData');
$excelData .= implode("\t", array_values($devisData)) . "\n";
$excelData .= "\n";
// on affiche les totaux du devis
$sql = 'SELECT d.montant_total_ht, d.montant_total_ht_remise, d.marge_totale FROM devis d WHERE d.rowid=' . $cidSafe . ';';
$dev = getinfos($sql, "gen");
$totaux = $dev[0];
$sql = 'SELECT d.montant_total_ht, d.montant_total_ht_remise, d.marge_totale FROM devis d WHERE d.rowid = :devis_id';
$totaux = $db->fetchOne($sql, [':devis_id' => $cidSafe]);
$fields = array("Total HT", "Total HT Remise", "Marge Totale");
$excelData .= implode("\t", array_values($fields)) . "\n";
array_walk($totaux, 'filterData');
$excelData .= implode("\t", array_values($totaux)) . "\n";
// une ligne vierge de séparation
$excelData .= "\n";
// on affiche les produits
@@ -155,8 +159,8 @@ switch ($Route->_action) {
$sql .= 'LEFT JOIN produits p ON dp.fk_produit=p.rowid ';
$sql .= 'LEFT JOIN produits_familles pf ON p.groupe=pf.groupe ';
$sql .= 'LEFT JOIN x_familles xf ON pf.fk_famille=xf.rowid ';
$sql .= 'WHERE dp.fk_devis=' . $cidSafe . ' ORDER BY dp.ordre, xf.ordre, p.libelle;';
$data = getinfos($sql, "gen");
$sql .= 'WHERE dp.fk_devis = :devis_id ORDER BY dp.ordre, xf.ordre, p.libelle';
$data = $db->fetchAll($sql, [':devis_id' => $cidSafe]);
$fields = array("Code", "Designation", "Prix Vente", "Quantite", "Remise", "PU vente avec remise", "Total HT", "Marge", "Commentaire");
$excelData .= implode("\t", array_values($fields)) . "\n";
@@ -167,22 +171,19 @@ switch ($Route->_action) {
}
if ($chkSpeciaux == "Oui") {
// une ligne vierge de séparation
$excelData .= "\n";
$excelData .= "----" . "\n";
$excelData .= "PRODUITS SPECIAUX" . "\n";
$excelData .= "----" . "\n";
$sql = 'SELECT IF(ds.chk_livr_multi=1, "Oui", "Non") AS livr_multi, ds.nb_livr, DATE_FORMAT(ds.date_livr_1, "%d/%m/%Y") AS datelivr ';
$sql .= 'FROM devis_speciaux ds WHERE ds.fk_devis=' . $cidSafe . ';';
$spec = getinfos($sql, "gen");
$speciaux = $spec[0];
$sql .= 'FROM devis_speciaux ds WHERE ds.fk_devis = :devis_id';
$speciaux = $db->fetchOne($sql, [':devis_id' => $cidSafe]);
$fields = array("Livraisons multiples", "Nbre livraisons", "Date 1ere livraison");
$excelData .= implode("\t", array_values($fields)) . "\n";
array_walk($speciaux, 'filterData');
$excelData .= implode("\t", array_values($speciaux)) . "\n";
$excelData .= "\n";
$fields = array("#", "Code", "Designation", "Quantite", "Surcout", "Echantillon", "Date echantillon", "Concurrent", "Description");
@@ -191,29 +192,32 @@ switch ($Route->_action) {
for ($i = 1; $i <= 5; $i++) {
$sql = 'SELECT ds.fk_produit_' . $i . ', ds.code_produit_' . $i . ', ds.lib_produit_' . $i . ', ds.qte_' . $i . ', IF(ds.surcout_' . $i . '=0, "", FORMAT(ds.surcout_' . $i . ', 2, "fr_FR")), IF(ds.chk_echantillon_' . $i . '=1, "Oui", "Non") AS echantillon, ';
$sql .= 'DATE_FORMAT(ds.date_echantillon_' . $i . ', "%d/%m/%Y") AS date_ech, ds.lib_concurrent_' . $i . ', ds.description_' . $i . ' ';
$sql .= 'FROM devis_speciaux ds WHERE ds.fk_devis=' . $cidSafe . ';';
eLog($sql, "sql");
$spec = getinfos($sql, "gen");
$speciaux = $spec[0];
$sql .= 'FROM devis_speciaux ds WHERE ds.fk_devis = :devis_id';
if ($speciaux["fk_produit_" . $i] > 0) {
array_walk($speciaux, 'filterData');
$excelData .= implode("\t", array_values($speciaux)) . "\n";
$produitSpecial = $db->fetchOne($sql, [':devis_id' => $cidSafe]);
if ($produitSpecial && $produitSpecial["fk_produit_" . $i] > 0) {
array_walk($produitSpecial, 'filterData');
$excelData .= implode("\t", array_values($produitSpecial)) . "\n";
}
}
}
// une ligne vierge de séparation
$excelData .= "\n";
$excelData .= "----" . "\n";
$excelData .= "FIN DU DEVIS" . "\n";
$excelData .= "----" . "\n";
$excelData .= "\n";
header('Content-Type: application/vnd.ms-excel; charset=utf-16le');
header("Content-type: application/x-msexcel; charset=utf-16le");
header('Content-Disposition: attachment; filename="' . $fileName . '"');
header('Cache-Control: max-age=0');
echo $excelData;
} catch (Exception $e) {
error_log("Erreur export Excel : " . $e->getMessage());
http_response_code(500);
echo "Erreur lors de l'export du devis";
}
exit();
}

View File

@@ -5,9 +5,10 @@ $aModel = array();
$sql = 'SELECT m.* FROM medias m WHERE m.support="devis_pdf_sap" ORDER BY m.support_rowid;';
$aModel["medias"] = getinfos($sql, "gen");
$sql = 'SELECT d.*, c.libelle, c.adresse1, c.adresse2, c.adresse3, c.code, c.cp, c.ville, c.email, u.libelle as nom, u.prenom, s.libelle as lib_statut, m.libelle as lib_marche ';
$sql = 'SELECT d.*, c.libelle, c.adresse1, c.adresse2, c.adresse3, c.code, c.cp, c.ville, cc.email, u.libelle as nom, u.prenom, s.libelle as lib_statut, m.libelle as lib_marche ';
$sql .= 'FROM devis d ';
$sql .= 'LEFT JOIN clients c on d.fk_client = c.rowid ';
$sql .= 'LEFT JOIN clients_contacts cc ON d.fk_contact = cc.rowid ';
$sql .= 'LEFT JOIN users u ON d.fk_user = u.rowid ';
$sql .= 'LEFT JOIN marches m ON d.fk_marche = m.rowid ';
$sql .= 'LEFT JOIN x_statuts_devis s ON d.fk_statut_devis = s.rowid ';
@@ -24,9 +25,10 @@ foreach ($aModel["devisEnCours"] as $devis) {
}
}
$sql = 'SELECT d.*, c.libelle, c.adresse1, c.adresse2, c.adresse3, c.code, c.cp, c.ville, u.libelle as nom, u.prenom, s.libelle as lib_statut, m.libelle as lib_marche ';
$sql = 'SELECT d.*, c.libelle, c.adresse1, c.adresse2, c.adresse3, c.code, c.cp, c.ville, cc.email, u.libelle as nom, u.prenom, s.libelle as lib_statut, m.libelle as lib_marche ';
$sql .= 'FROM devis d ';
$sql .= 'LEFT JOIN clients c on d.fk_client = c.rowid ';
$sql .= 'LEFT JOIN clients_contacts cc ON d.fk_contact = cc.rowid ';
$sql .= 'LEFT JOIN users u ON d.fk_user = u.rowid ';
$sql .= 'LEFT JOIN marches m ON d.fk_marche = m.rowid ';
$sql .= 'LEFT JOIN x_statuts_devis s ON d.fk_statut_devis = s.rowid ';

View File

@@ -254,3 +254,105 @@ function timeEnd($start, $label = '') {
return $time;
}
function loadtel($numero, $prefix = "+33") {
$lenumero = trim($numero);
$lenumero = preg_replace('/[^0-9]/', '', $lenumero);
if (strlen($lenumero) == 10) {
$lenumero = substr($lenumero, 1);
}
if (strlen($lenumero) == 9) {
$lenumero = $prefix . $lenumero;
}
return $lenumero;
}
function formattel($numero, $separateur = " ") {
if (strlen($numero) == 9) {
$numero = "0" . $numero;
}
if (strlen($numero) == 10) {
$numero = substr($numero, 0, 2) . $separateur . substr($numero, 2, 2) . $separateur . substr($numero, 4, 2) . $separateur . substr($numero, 6, 2) . $separateur . substr($numero, 8, 2);
}
return $numero;
}
function str_normalize($string, $minuscules = true) {
$result = "";
$string = trim($string);
if (strlen($string) > 0) {
if ($minuscules) {
$result = strtolower($string);
} else {
$result = $string;
}
$result = str_replace(" ", "_", $result);
$result = str_replace("é", "e", $result);
$result = str_replace("è", "e", $result);
$result = str_replace("ê", "e", $result);
$result = str_replace("ë", "e", $result);
$result = str_replace("à", "a", $result);
$result = str_replace("â", "a", $result);
$result = str_replace("ä", "a", $result);
$result = str_replace("ô", "o", $result);
$result = str_replace("ö", "o", $result);
$result = str_replace("ù", "u", $result);
$result = str_replace("û", "u", $result);
$result = str_replace("ü", "u", $result);
$result = str_replace("ç", "c", $result);
$result = str_replace("'", "", $result);
$result = str_replace("\"", "", $result);
$result = str_replace("/", "", $result);
$result = str_replace("(", "_", $result);
$result = str_replace(")", "_", $result);
$result = str_replace("!", "_", $result);
$result = str_replace("?", "_", $result);
$result = trim($result);
}
return $result;
}
function generateRandomPassword() {
$password = '';
$desired_length = rand(8, 12);
for ($length = 0; $length < $desired_length; $length++) {
$password .= chr(rand(44, 122));
}
$password = str_replace("/", "&", $password);
$password = str_replace("<", "!", $password);
$password = str_replace(">", "!", $password);
$password = str_replace("=", "#", $password);
$password = str_replace("\\", "&", $password);
$password = str_replace("^", "%", $password);
$password = str_replace(chr(96), "#", $password);
return $password;
}
function purge_old_logs($log_dir, $app_name, $days_to_keep = 10) {
if (!is_dir($log_dir)) {
return;
}
$date_limit = strtotime("-{$days_to_keep} days");
$patterns = array(
$app_name . '_????-??-??.log',
$app_name . '_debug_????-??-??.log'
);
foreach ($patterns as $pattern) {
$files = glob($log_dir . $pattern);
if ($files) {
foreach ($files as $file) {
if (preg_match('/(\d{4}-\d{2}-\d{2})\.log$/', $file, $matches)) {
$file_date = strtotime($matches[1]);
if ($file_date < $date_limit) {
@unlink($file);
}
}
}
}
}
}

View File

@@ -21,6 +21,8 @@ let chkShowDevisArchives = false // indique si on affiche les devis archivés ou
let chkCreateClient = false
// On charge les produits du marché du devis en cours dans un tableau pour ne pas avoir à les recharger à chaque fois
let dataProduitsMarche = []
let dataProduitsMercurial = [] // Produits du marché hybride pour l'onglet Mercurial
let chkMarcheHybride = false // Indique si le marché du devis est hybride
//! Pour ne charger les clients du secteur ou de toute la France qu'en cas de changement de la valeur du chkbox
let oldChkClientsSecteur = 2
let clients = []
@@ -35,14 +37,107 @@ let devisTotalRemHT = 0
let devisTotalMarge = 0
let chkRegleSeuilsMarge = false // indique si le marché sélectionné prend en compte les seuils de marge fixés dans les familles de produits
let seuilMargeRR = 30 // le seuil de marge du RR sur ce devis, par défaut à 30 %
let seuilMargeDV = 20 // le seuil de marge du DV sur ce devis, par défaut à 20 %
let seuilMargeRR = 40 // le seuil de marge du RR sur ce devis, par défaut à 40 % (MAJ 05/11/2025)
let seuilMargeDV = 30 // le seuil de marge du DV sur ce devis, par défaut à 30 % (MAJ 05/11/2025)
let intervalRefresh
let nbCommentChat = 0
let draggedElement = null // l'élément qui est en train d'être déplacé (la ligne du produit du devis lors d'un drag and drop)
const tableSortStates = new Map()
function initTableSort() {
const allTables = document.querySelectorAll('[id^="tblDos"], [id^="tblDosArch"]')
allTables.forEach(table => {
const tableId = table.id
const headers = table.querySelectorAll('th[data-sortable="true"]')
const tbody = table.querySelector('tbody')
if (!tbody) return
if (!tableSortStates.has(tableId)) {
tableSortStates.set(tableId, {
originalOrder: null,
currentSort: { column: null, direction: null }
})
}
headers.forEach(header => {
header.addEventListener('click', function() {
const columnIndex = parseInt(this.getAttribute('data-column-index'))
const sortType = this.getAttribute('data-sort-type')
sortTable(tableId, columnIndex, sortType, this)
})
})
})
}
function sortTable(tableId, columnIndex, sortType, headerElement) {
const table = document.getElementById(tableId)
const tbody = table.querySelector('tbody')
const rows = Array.from(tbody.querySelectorAll('tr'))
const state = tableSortStates.get(tableId)
if (!state.originalOrder) {
state.originalOrder = rows.slice()
}
const allHeaders = table.querySelectorAll('th[data-sortable="true"]')
allHeaders.forEach(h => h.style.fontWeight = 'normal')
let sortedRows
if (state.currentSort.column === columnIndex && state.currentSort.direction === 'desc') {
sortedRows = state.originalOrder.slice()
state.currentSort = { column: null, direction: null }
} else {
const direction = (state.currentSort.column === columnIndex && state.currentSort.direction === 'asc') ? 'desc' : 'asc'
sortedRows = rows.slice().sort((a, b) => {
const aCell = a.cells[columnIndex]
const bCell = b.cells[columnIndex]
if (!aCell || !bCell) return 0
let aValue = aCell.textContent.trim()
let bValue = bCell.textContent.trim()
if (sortType === 'number') {
aValue = aValue.replace(/[^\d,.-]/g, '').replace(',', '.')
bValue = bValue.replace(/[^\d,.-]/g, '').replace(',', '.')
aValue = parseFloat(aValue) || 0
bValue = parseFloat(bValue) || 0
return direction === 'asc' ? aValue - bValue : bValue - aValue
} else if (sortType === 'date') {
aValue = parseDateFromText(aValue)
bValue = parseDateFromText(bValue)
if (!aValue && !bValue) return 0
if (!aValue) return direction === 'asc' ? 1 : -1
if (!bValue) return direction === 'asc' ? -1 : 1
return direction === 'asc' ? aValue - bValue : bValue - aValue
} else {
const comparison = aValue.localeCompare(bValue, 'fr')
return direction === 'asc' ? comparison : -comparison
}
})
state.currentSort = { column: columnIndex, direction }
headerElement.style.fontWeight = 'bold'
}
tbody.innerHTML = ''
sortedRows.forEach(row => tbody.appendChild(row))
}
function parseDateFromText(dateText) {
const match = dateText.match(/(\d{2})\/(\d{2})[\/\s](\d{4})/)
if (!match) return null
const [, day, month, year] = match
return new Date(year, month - 1, day)
}
window.addEventListener('DOMContentLoaded', (event) => {
console.log('#')
@@ -407,18 +502,24 @@ window.addEventListener('DOMContentLoaded', (event) => {
document.getElementById('inp_adresse3').value = data.adresse3
document.getElementById('inp_cp').value = data.cp
document.getElementById('inp_ville').value = data.ville
document.getElementById('inp_contact_nom').value = data.contact_nom
document.getElementById('inp_contact_prenom').value = data.contact_prenom
document.getElementById('inp_contact_fonction').value = data.contact_fonction
document.getElementById('inp_email').value = data.email
document.getElementById('inp_telephone').value = data.telephone
document.getElementById('inp_mobile').value = data.mobile
document.getElementById('selTypeEtab').value = data.type_client
elBtnCreateClient.innerHTML = 'Créer un nouveau client'
if (elBtnCreateClient.classList.contains('btn-info')) {
elBtnCreateClient.classList.remove('btn-info')
elBtnCreateClient.classList.add('btn-primary')
}
// Charger les contacts du client et sélectionner le contact du devis
loadContactsClient(data.code).then(() => {
if (data.fk_contact && data.fk_contact > 0) {
document.getElementById('sel_contact').value = data.fk_contact
// Afficher les infos du contact
const contact = contactsClient.find(c => c.rowid == data.fk_contact)
if (contact) {
displayContactInfos(contact)
}
}
})
}
if (data.chk_devis_photos == '1') {
@@ -908,6 +1009,86 @@ window.addEventListener('DOMContentLoaded', (event) => {
if (ret.length == 1) {
let line = ret[0]
chkPrixNets = line.chk_prix_nets == 1 ? true : false
chkMarcheHybride = line.chk_marche_hybride == 1 ? true : false
// Si le marché est hybride, créer le panel et charger les produits
if (chkMarcheHybride) {
// Créer le panel Mercurial s'il n'existe pas
if (!document.getElementById('tabMercurial')) {
const tabContent = document.querySelector('#divProduitsDisponibles .tab-content')
const divPanel = document.createElement('div')
divPanel.setAttribute('role', 'tabpanel')
divPanel.className = 'tab-pane'
divPanel.id = 'tabMercurial'
divPanel.innerHTML = `
<div class="form-group">
<label for="inpSearchProduct_Mercurial">Recherche de produits Mercurial : </label>
<div class="input-group">
<input type="text" class="form-control" id="inpSearchProduct_Mercurial" placeholder="code ou libellé" size="50px" />
<div class="input-group-addon">
<svg width="18px" height="18px" viewBox="0 0 20 20" role="img" xmlns="http://www.w3.org/2000/svg" aria-labelledby="returnIconTitle" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none" color="#000000">
<path d="M19,8 L19,11 C19,12.1045695 18.1045695,13 17,13 L6,13"/>
<polyline points="8 16 5 13 8 10"/>
</svg>
</div>
</div>
</div>
<div class="border cm-scrollbar cm-table-w-scroll table-responsive mt-1 table-400">
<table class="table table-striped table-bordered table-responsive table-fixed" id="tblProduits_Mercurial">
<thead>
<tr>
<th class="header" scope="col" width="10%">Sélection</th>
<th class="header" scope="col" width="20%">Code</th>
<th class="header" scope="col" width="30%">Libellé</th>
<th class="header" scope="col" width="10%">Famille</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
`
tabContent.appendChild(divPanel)
}
// Charger les produits Mercurial
fetch('/jxdevis/load_produits_mercurial', {
method: 'POST',
body: JSON.stringify({ fk_marche: idMarche }),
headers: {
'Content-Type': 'application/json;charset=utf-8',
Accept: 'application/json;charset=utf-8',
},
}).then((response) => {
if (response.ok) {
const ret = response.json()
ret.then(function (data) {
dataProduitsMercurial = data
showProduitsMercurial(data)
// Créer l'onglet Mercurial en dernière position après affichage des produits
if (!document.getElementById('liOngletMercurial')) {
const ulOnglets = document.getElementById('listOngletsProduits')
const liOnglet = document.createElement('li')
liOnglet.setAttribute('role', 'presentation')
liOnglet.id = 'liOngletMercurial'
liOnglet.innerHTML = '<a href="#tabMercurial" id="onglet_mercurial" aria-controls="tabMercurial" role="tab" data-toggle="tab" style="background-color: #5cb85c; color: white; font-weight: bold;">Mercurial<br/>&nbsp;</a>'
ulOnglets.appendChild(liOnglet)
}
})
}
})
} else {
// Supprimer l'onglet et le panel Mercurial s'ils existent
const liOnglet = document.getElementById('liOngletMercurial')
if (liOnglet) {
liOnglet.remove()
}
const divPanel = document.getElementById('tabMercurial')
if (divPanel) {
divPanel.remove()
}
}
document.getElementById('inp_latitudeRR').value = seuilMargeRR
document.getElementById('inp_latitudeDV').value = seuilMargeDV
document.getElementById('titleMarche').innerHTML = '<bolder>Informations du marché ' + line.libelle + '</bolder>' // le titre du panel des infos marché dans l'onglet 3. Devis
@@ -1146,6 +1327,54 @@ window.addEventListener('DOMContentLoaded', (event) => {
celFamille.innerHTML = lineProduit.lib_famille
}
function showProduitsMercurial(dProduits) {
// Affiche tous les produits Mercurial dans l'onglet dédié
let tblBody = document.getElementById('tblProduits_Mercurial').getElementsByTagName('tbody')[0]
tblBody.innerHTML = ''
if (dProduits && dProduits.length > 0) {
dProduits.forEach(function (lineProduit) {
let newRow = tblBody.insertRow(-1)
newRow.className = 'ligProduit_Mercurial'
newRow.id = 'ligProduit_Mercurial_' + lineProduit.rowid
newRow.setAttribute('data-rid', lineProduit.rowid)
let celChkBox = newRow.insertCell(0)
celChkBox.className = 'chkBox_Mercurial text-center'
celChkBox.setAttribute('data-rid', lineProduit.rowid)
celChkBox.innerHTML =
'<input type="checkbox" class="chkBox" id="chkBoxProd_' +
lineProduit.rowid +
'" name="chkBoxProd_' +
lineProduit.rowid +
'" data-rid="' +
lineProduit.rowid +
'" data-code="' +
lineProduit.code +
'" data-libelle="' +
lineProduit.libelle +
'" data-famille="Mercurial" />'
let celCode = newRow.insertCell(1)
celCode.innerHTML = lineProduit.code
let celLibelle = newRow.insertCell(2)
celLibelle.innerHTML = lineProduit.libelle
let celFamille = newRow.insertCell(3)
celFamille.innerHTML = lineProduit.lib_famille || '-'
})
// Brancher l'autocomplete sur le champ de recherche
autocompleteProduitsFamille(
document.getElementById('inpSearchProduct_Mercurial'),
dProduits,
'Mercurial',
0
)
}
}
$('a[data-toggle="tab"]').on('show.bs.tab', function (e) {
if (idDevis == 0) {
if ($(this).attr('href') == '#tabproduits' || $(this).attr('href') == '#tabdevis') {
@@ -1346,33 +1575,63 @@ window.addEventListener('DOMContentLoaded', (event) => {
}
let clickSaveCreateClient = function () {
// on regarde si c'est une création de devis ou une modification
// on enregistre le fait que ça soit un nouveau client
// on met à jour les champs du devis avec les infos du nouveau client
// et quand on enregistre le devis on enregistre le nouveau client dans le devis
document.getElementById('inp_fk_client').value = '0'
document.getElementById('inp_lib_client').value = document.getElementById('inp_create_libelle').value
document.getElementById('selTypeEtab').value = document.getElementById('inp_create_type_client').value
document.getElementById('inp_type_client').value = document.getElementById('inp_create_type_client').value
document.getElementById('inp_adresse1').value = document.getElementById('inp_create_adresse1').value
document.getElementById('inp_adresse2').value = document.getElementById('inp_create_adresse2').value
document.getElementById('inp_adresse3').value = document.getElementById('inp_create_adresse3').value
document.getElementById('inp_cp').value = document.getElementById('inp_create_cp').value
document.getElementById('inp_ville').value = document.getElementById('inp_create_ville').value
showLoading()
const clientData = {
libelle: document.getElementById('inp_create_libelle').value,
type_client: document.getElementById('inp_create_type_client').value,
adresse1: document.getElementById('inp_create_adresse1').value,
adresse2: document.getElementById('inp_create_adresse2').value,
adresse3: document.getElementById('inp_create_adresse3').value,
cp: document.getElementById('inp_create_cp').value,
ville: document.getElementById('inp_create_ville').value
}
fetch('/jxdevis/save_new_client', {
method: 'POST',
body: JSON.stringify(clientData),
headers: {
'Content-Type': 'application/json;charset=utf-8',
Accept: 'application/json;charset=utf-8',
},
})
.then((response) => response.json())
.then((data) => {
hideLoading()
if (data.success) {
const newClientId = data.rowid
const newClientCode = data.code
document.getElementById('inp_fk_client').value = newClientId
document.getElementById('inp_lib_client').value = clientData.libelle
document.getElementById('selTypeEtab').value = clientData.type_client
document.getElementById('inp_type_client').value = clientData.type_client
document.getElementById('inp_adresse1').value = clientData.adresse1
document.getElementById('inp_adresse2').value = clientData.adresse2
document.getElementById('inp_adresse3').value = clientData.adresse3
document.getElementById('inp_cp').value = clientData.cp
document.getElementById('inp_ville').value = clientData.ville
console.log(
'nouveau client créé : ' +
document.getElementById('inp_create_type_client').value +
' -> ' +
document.getElementById('selTypeEtab').value
)
hideModal(document.getElementById('modalCreateClient'))
// on change le texte et la couleur du bouton de nouveau client
document.getElementById('btnCreateClient').innerHTML = 'Modifier le nouveau client'
document.getElementById('btnCreateClient').innerHTML = 'Modifier le client'
document.getElementById('btnCreateClient').classList.remove('btn-primary')
document.getElementById('btnCreateClient').classList.add('btn-info')
chkCreateClient = true
loadContactsClient(newClientCode).then(() => {
showNotification('Succès', 'Client créé avec succès. Vous pouvez maintenant gérer ses contacts.', 'success')
})
chkCreateClient = false
} else {
showNotification('Erreur', data.message || 'Erreur lors de la création du client', 'error')
}
})
.catch((error) => {
hideLoading()
console.error('Erreur création client:', error)
showNotification('Erreur', 'Impossible de créer le client', 'error')
})
}
let clickSpeciaux = function () {
@@ -1510,45 +1769,13 @@ window.addEventListener('DOMContentLoaded', (event) => {
return false
}
if (document.getElementById('inp_contact_nom').value == '') {
if (document.getElementById('sel_contact').value == '0') {
showNotification(
'Erreur',
'Enregistrement impossible : vous devez renseigner le nom et prénom du contact',
'Enregistrement impossible : vous devez sélectionner un contact',
'error'
)
document.getElementById('inp_contact_nom').focus()
return false
}
if (document.getElementById('inp_contact_prenom').value == '') {
showNotification(
'Erreur',
'Enregistrement impossible : vous devez renseigner le nom et prénom du contact',
'error'
)
document.getElementById('inp_contact_prenom').focus()
return false
}
if (document.getElementById('inp_contact_fonction').value == '') {
showNotification('Erreur', 'Enregistrement impossible : vous devez renseigner la fonction du contact', 'error')
document.getElementById('inp_contact_fonction').focus()
return false
}
if (document.getElementById('inp_email').value == '') {
showNotification('Erreur', "Enregistrement impossible : vous devez renseigner l'email du contact", 'error')
document.getElementById('inp_email').focus()
return false
}
if (document.getElementById('inp_telephone').value == '' && document.getElementById('inp_mobile').value == '') {
showNotification(
'Erreur',
'Enregistrement impossible : vous devez renseigner au moins un numéro de téléphone du contact (fixe ou mobile)',
'error'
)
document.getElementById('inp_telephone').focus()
document.getElementById('sel_contact').focus()
return false
}
@@ -1817,10 +2044,10 @@ window.addEventListener('DOMContentLoaded', (event) => {
showDevisPro(data)
hideLoading()
showNotification('Succès', 'Enregistrement des ' + nbProduits + ' produits de ce devis effectué', 'success')
chkChange = 0
})
}
})
chkChange = 0
return false
}
@@ -2891,13 +3118,9 @@ window.addEventListener('DOMContentLoaded', (event) => {
document.getElementById('inp_adresse3').value = list[i]['adresse3']
document.getElementById('inp_cp').value = list[i]['cp']
document.getElementById('inp_ville').value = list[i]['ville']
document.getElementById('inp_contact_nom').value = list[i]['contact_nom']
document.getElementById('inp_contact_prenom').value = list[i]['contact_prenom']
document.getElementById('inp_contact_fonction').value = list[i]['contact_fonction']
document.getElementById('inp_telephone').value = list[i]['telephone']
document.getElementById('inp_email').value = list[i]['email']
document.getElementById('inp_mobile').value = list[i]['mobile']
document.getElementById('selTypeEtab').value = list[i]['type_client']
// Charger les contacts du client sélectionné
loadContactsClient(list[i]['code'])
// on ferme la liste des suggestions
closeList()
})
@@ -3305,6 +3528,284 @@ window.addEventListener('DOMContentLoaded', (event) => {
hideModal(document.getElementById('modalCommentProd'))
}
//! ========== GESTION DES CONTACTS ==========
let contactsClient = [] // Liste des contacts du client en cours
let currentFkClient = 0 // ID du client en cours
// Charger les contacts d'un client
function loadContactsClient(fkClient) {
if (fkClient == 0) {
document.getElementById('sel_contact').innerHTML = '<option value="0">- Sélectionner un contact -</option>'
document.getElementById('btnGererContacts').disabled = true
document.getElementById('divContactInfos').style.display = 'none'
contactsClient = []
currentFkClient = 0
return Promise.resolve()
}
currentFkClient = fkClient
return fetch('/jxcontacts/load_contacts', {
method: 'POST',
body: JSON.stringify({ fk_client: fkClient }),
headers: {
'Content-Type': 'application/json;charset=utf-8',
Accept: 'application/json;charset=utf-8',
},
})
.then((response) => response.json())
.then((data) => {
contactsClient = data
const selContact = document.getElementById('sel_contact')
selContact.innerHTML = '<option value="0">- Sélectionner un contact -</option>'
data.forEach((contact) => {
const option = document.createElement('option')
option.value = contact.rowid
option.textContent = contact.nom + ' ' + contact.prenom + (contact.principal == 1 ? ' ⭐' : '')
selContact.appendChild(option)
})
document.getElementById('btnGererContacts').disabled = false
// Si un seul contact, le sélectionner automatiquement
if (data.length === 1) {
selContact.value = data[0].rowid
displayContactInfos(data[0])
}
})
.catch((error) => {
console.error('Erreur chargement contacts:', error)
showNotification('Erreur', 'Impossible de charger les contacts', 'error')
})
}
// Afficher les infos d'un contact en lecture seule
function displayContactInfos(contact) {
document.getElementById('info_contact_nom_prenom').textContent = (contact.prenom || '') + ' ' + (contact.nom || '')
document.getElementById('info_contact_fonction').textContent = contact.fonction || '-'
document.getElementById('info_contact_email').textContent = contact.email || '-'
document.getElementById('info_contact_telephone').textContent = contact.telephone || '-'
document.getElementById('info_contact_mobile').textContent = contact.mobile || '-'
document.getElementById('divContactInfos').style.display = 'block'
}
// Événement changement de contact
document.getElementById('sel_contact').addEventListener('change', function () {
const contactId = this.value
if (contactId == 0) {
document.getElementById('divContactInfos').style.display = 'none'
return
}
const contact = contactsClient.find((c) => c.rowid == contactId)
if (contact) {
displayContactInfos(contact)
}
})
// Ouvrir la modale de gestion des contacts
document.getElementById('btnGererContacts').addEventListener('click', function () {
document.getElementById('inp_fk_client_contacts').value = currentFkClient
loadContactsTable()
showModal(document.getElementById('modalGererContacts'))
})
// Charger la table des contacts
function loadContactsTable() {
const tbody = document.querySelector('#tblContacts tbody')
tbody.innerHTML = '<tr><td colspan="7" class="text-center">Chargement...</td></tr>'
fetch('/jxcontacts/load_contacts', {
method: 'POST',
body: JSON.stringify({ fk_client: currentFkClient }),
headers: {
'Content-Type': 'application/json;charset=utf-8',
Accept: 'application/json;charset=utf-8',
},
})
.then((response) => response.json())
.then((data) => {
tbody.innerHTML = ''
if (data.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" class="text-center">Aucun contact trouvé</td></tr>'
return
}
data.forEach((contact) => {
const tr = document.createElement('tr')
tr.innerHTML = `
<td>${contact.nom || ''}</td>
<td>${contact.prenom || ''}</td>
<td>${contact.fonction || ''}</td>
<td>${contact.telephone || ''}</td>
<td>${contact.email || ''}</td>
<td class="text-center">
${contact.principal == 1 ? '<span class="label label-success">Oui</span>' : '<button class="btn btn-xs btn-default btnSetPrincipal" data-id="' + contact.rowid + '">Définir</button>'}
</td>
<td class="text-center">
<button class="btn btn-xs btn-primary btnEditContact" data-id="${contact.rowid}"><i class="fa fa-edit"></i></button>
<button class="btn btn-xs btn-danger btnDeleteContact" data-id="${contact.rowid}"><i class="fa fa-trash"></i></button>
</td>
`
tbody.appendChild(tr)
})
// Attacher les événements
document.querySelectorAll('.btnEditContact').forEach((btn) => {
btn.addEventListener('click', function () {
editContact(this.getAttribute('data-id'))
})
})
document.querySelectorAll('.btnDeleteContact').forEach((btn) => {
btn.addEventListener('click', function () {
deleteContact(this.getAttribute('data-id'))
})
})
document.querySelectorAll('.btnSetPrincipal').forEach((btn) => {
btn.addEventListener('click', function () {
setPrincipalContact(this.getAttribute('data-id'))
})
})
})
.catch((error) => {
console.error('Erreur chargement table contacts:', error)
tbody.innerHTML = '<tr><td colspan="7" class="text-center text-danger">Erreur de chargement</td></tr>'
})
}
// Nouveau contact
document.getElementById('btnNouveauContact').addEventListener('click', function () {
document.getElementById('modEditContactTitreText').textContent = 'Nouveau contact'
document.getElementById('frmEditContact').reset()
document.getElementById('inp_contact_rowid').value = '0'
document.getElementById('inp_contact_fk_client').value = currentFkClient
showModal(document.getElementById('modalEditContact'))
})
// Éditer un contact
function editContact(contactId) {
fetch('/jxcontacts/load_contact', {
method: 'POST',
body: JSON.stringify({ rowid: contactId }),
headers: {
'Content-Type': 'application/json;charset=utf-8',
Accept: 'application/json;charset=utf-8',
},
})
.then((response) => response.json())
.then((contact) => {
document.getElementById('modEditContactTitreText').textContent = 'Modifier le contact'
document.getElementById('inp_contact_rowid').value = contact.rowid
document.getElementById('inp_contact_fk_client').value = contact.fk_client
document.getElementById('inp_contact_nom_edit').value = contact.nom || ''
document.getElementById('inp_contact_prenom_edit').value = contact.prenom || ''
document.getElementById('inp_contact_fonction_edit').value = contact.fonction || ''
document.getElementById('inp_contact_email_edit').value = contact.email || ''
document.getElementById('inp_contact_telephone_edit').value = contact.telephone || ''
document.getElementById('inp_contact_mobile_edit').value = contact.mobile || ''
document.getElementById('inp_contact_principal_edit').checked = contact.principal == 1
showModal(document.getElementById('modalEditContact'))
})
.catch((error) => {
console.error('Erreur chargement contact:', error)
showNotification('Erreur', 'Impossible de charger le contact', 'error')
})
}
// Sauvegarder un contact
document.getElementById('btnSaveEditContact').addEventListener('click', function () {
const form = document.getElementById('frmEditContact')
const formData = new FormData(form)
fetch('/jxcontacts/save_contact', {
method: 'POST',
body: formData,
})
.then((response) => response.json())
.then((data) => {
if (data.ret === 'ok') {
showNotification('Succès', data.msg, 'success')
hideModal(document.getElementById('modalEditContact'))
loadContactsTable()
loadContactsClient(currentFkClient) // Recharger le sélecteur
} else {
showNotification('Erreur', data.msg, 'error')
}
})
.catch((error) => {
console.error('Erreur sauvegarde contact:', error)
showNotification('Erreur', 'Impossible de sauvegarder le contact', 'error')
})
})
// Supprimer un contact
function deleteContact(contactId) {
if (!confirm('Êtes-vous sûr de vouloir supprimer ce contact ?')) return
fetch('/jxcontacts/delete_contact', {
method: 'POST',
body: JSON.stringify({ rowid: contactId }),
headers: {
'Content-Type': 'application/json;charset=utf-8',
Accept: 'application/json;charset=utf-8',
},
})
.then((response) => response.json())
.then((data) => {
if (data.ret === 'ok') {
showNotification('Succès', data.msg, 'success')
loadContactsTable()
loadContactsClient(currentFkClient)
} else {
showNotification('Erreur', data.msg, 'error')
}
})
.catch((error) => {
console.error('Erreur suppression contact:', error)
showNotification('Erreur', 'Impossible de supprimer le contact', 'error')
})
}
// Définir contact principal
function setPrincipalContact(contactId) {
fetch('/jxcontacts/set_principal', {
method: 'POST',
body: JSON.stringify({ rowid: contactId, fk_client: currentFkClient }),
headers: {
'Content-Type': 'application/json;charset=utf-8',
Accept: 'application/json;charset=utf-8',
},
})
.then((response) => response.json())
.then((data) => {
if (data.ret === 'ok') {
showNotification('Succès', data.msg, 'success')
loadContactsTable()
loadContactsClient(currentFkClient)
} else {
showNotification('Erreur', data.msg, 'error')
}
})
.catch((error) => {
console.error('Erreur définition contact principal:', error)
showNotification('Erreur', 'Impossible de définir le contact principal', 'error')
})
}
// Fermer les modales
document.getElementById('btnFermerContacts').addEventListener('click', function () {
hideModal(document.getElementById('modalGererContacts'))
})
document.getElementById('btnCancelEditContact').addEventListener('click', function () {
hideModal(document.getElementById('modalEditContact'))
})
//! ========== FIN GESTION DES CONTACTS ==========
//! Configuration des événements
//! Sur chaque cellule du tableau des devis ayant la classe celDevis, on affecte un événement click qui appelle la fonction clickLigDevis()
Array.from(elCelDevis).forEach(function (lnDevis) {
@@ -3382,6 +3883,148 @@ window.addEventListener('DOMContentLoaded', (event) => {
chkVariante.addEventListener('change', calculDevis)
})
let elSearchDevis = document.getElementById('searchDevis')
let elBtnResetSearch = document.getElementById('btnResetSearch')
let searchTimeout = null
function restoreSearch() {
const savedTerm = sessionStorage.getItem('devisSearchTerm')
if (savedTerm && savedTerm.length >= 3) {
elSearchDevis.value = savedTerm
elBtnResetSearch.style.display = 'inline-block'
performSearch(savedTerm)
}
}
function performSearch(term) {
if (term.length < 3) {
return
}
const context = chkShowDevisArchives ? 'archives' : 'encours'
fetch('/jxdevis/search_devis', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
term: term,
context: context,
}),
})
.then((response) => response.json())
.then((data) => {
if (data.success) {
const devisIds = data.devis.map((d) => d.rowid)
filterDevisTables(devisIds, context)
updateBadges(data.nb_devis)
} else {
console.error('Erreur recherche:', data.message)
}
})
.catch((error) => {
console.error('Erreur AJAX:', error)
})
}
function filterDevisTables(devisIds, context) {
if (context === 'encours') {
const statuts = document.querySelectorAll('[id^="tblBodyDos"]')
statuts.forEach((tbody) => {
const rows = tbody.querySelectorAll('tr')
rows.forEach((row) => {
if (row.id && row.id.startsWith('tr_')) {
const rowId = parseInt(row.id.replace('tr_', ''))
if (devisIds.includes(rowId)) {
row.style.display = ''
} else {
row.style.display = 'none'
}
}
})
})
} else {
const archives = document.querySelectorAll('[id^="tblBodyDosArch"]')
archives.forEach((tbody) => {
const rows = tbody.querySelectorAll('tr')
rows.forEach((row) => {
if (row.id && row.id.startsWith('trArch_')) {
const rowId = parseInt(row.id.replace('trArch_', ''))
if (devisIds.includes(rowId)) {
row.style.display = ''
} else {
row.style.display = 'none'
}
}
})
})
}
}
function updateBadges(nbDevis) {
Object.keys(nbDevis).forEach((statutId) => {
const badge = document.querySelector('[id^="liStat"]')
if (badge) {
badge.setAttribute('data-after-text', nbDevis[statutId])
badge.setAttribute('data-after-type', 'orange badge top left')
}
})
}
function resetSearch() {
elSearchDevis.value = ''
elBtnResetSearch.style.display = 'none'
sessionStorage.removeItem('devisSearchTerm')
const context = chkShowDevisArchives ? 'archives' : 'encours'
if (context === 'encours') {
const statuts = document.querySelectorAll('[id^="tblBodyDos"]')
statuts.forEach((tbody) => {
const rows = tbody.querySelectorAll('tr')
rows.forEach((row) => {
row.style.display = ''
})
})
} else {
const archives = document.querySelectorAll('[id^="tblBodyDosArch"]')
archives.forEach((tbody) => {
const rows = tbody.querySelectorAll('tr')
rows.forEach((row) => {
row.style.display = ''
})
})
}
}
elSearchDevis.addEventListener('input', function () {
const term = this.value.trim()
if (searchTimeout) {
clearTimeout(searchTimeout)
}
if (term.length >= 3) {
elBtnResetSearch.style.display = 'inline-block'
sessionStorage.setItem('devisSearchTerm', term)
searchTimeout = setTimeout(() => {
performSearch(term)
}, 300)
} else if (term.length === 0) {
resetSearch()
} else {
elBtnResetSearch.style.display = 'none'
}
})
elBtnResetSearch.addEventListener('click', function () {
resetSearch()
})
restoreSearch()
initTableSort()
elBtnSideBarDevis.addEventListener('click', function () {
if (elVerticalBar.style.width == '10px') {
elVerticalBar.style.width = '1100px' // Largeur de la barre lorsqu'elle est ouverte

View File

@@ -11,6 +11,100 @@ let oldIdLnEnCours;
let oldIdLnArchives;
let nbCommentChat = 0;
let selectedXmlDevis = new Set();
let searchSapTimeout = null;
const tableSortStates = new Map();
function initTableSort() {
const allTables = document.querySelectorAll('[id^="tblDos"], [id^="tblDosArch"]');
allTables.forEach(table => {
const tableId = table.id;
const headers = table.querySelectorAll('th[data-sortable="true"]');
const tbody = table.querySelector('tbody');
if (!tbody) return;
if (!tableSortStates.has(tableId)) {
tableSortStates.set(tableId, {
originalOrder: null,
currentSort: { column: null, direction: null }
});
}
headers.forEach(header => {
header.addEventListener('click', function() {
const columnIndex = parseInt(this.getAttribute('data-column-index'));
const sortType = this.getAttribute('data-sort-type');
sortTable(tableId, columnIndex, sortType, this);
});
});
});
}
function sortTable(tableId, columnIndex, sortType, headerElement) {
const table = document.getElementById(tableId);
const tbody = table.querySelector('tbody');
const rows = Array.from(tbody.querySelectorAll('tr'));
const state = tableSortStates.get(tableId);
if (!state.originalOrder) {
state.originalOrder = rows.slice();
}
const allHeaders = table.querySelectorAll('th[data-sortable="true"]');
allHeaders.forEach(h => h.style.fontWeight = 'normal');
let sortedRows;
if (state.currentSort.column === columnIndex && state.currentSort.direction === 'desc') {
sortedRows = state.originalOrder.slice();
state.currentSort = { column: null, direction: null };
} else {
const direction = (state.currentSort.column === columnIndex && state.currentSort.direction === 'asc') ? 'desc' : 'asc';
sortedRows = rows.slice().sort((a, b) => {
const aCell = a.cells[columnIndex];
const bCell = b.cells[columnIndex];
if (!aCell || !bCell) return 0;
let aValue = aCell.textContent.trim();
let bValue = bCell.textContent.trim();
if (sortType === 'number') {
aValue = aValue.replace(/[^\d,.-]/g, '').replace(',', '.');
bValue = bValue.replace(/[^\d,.-]/g, '').replace(',', '.');
aValue = parseFloat(aValue) || 0;
bValue = parseFloat(bValue) || 0;
return direction === 'asc' ? aValue - bValue : bValue - aValue;
} else if (sortType === 'date') {
aValue = parseDateFromText(aValue);
bValue = parseDateFromText(bValue);
if (!aValue && !bValue) return 0;
if (!aValue) return direction === 'asc' ? 1 : -1;
if (!bValue) return direction === 'asc' ? -1 : 1;
return direction === 'asc' ? aValue - bValue : bValue - aValue;
} else {
const comparison = aValue.localeCompare(bValue, 'fr');
return direction === 'asc' ? comparison : -comparison;
}
});
state.currentSort = { column: columnIndex, direction };
headerElement.style.fontWeight = 'bold';
}
tbody.innerHTML = '';
sortedRows.forEach(row => tbody.appendChild(row));
}
function parseDateFromText(dateText) {
const match = dateText.match(/(\d{2})\/(\d{2})[\/\s](\d{4})/);
if (!match) return null;
const [, day, month, year] = match;
return new Date(year, month - 1, day);
}
window.addEventListener('DOMContentLoaded', (event) => {
console.log('#');
@@ -786,6 +880,201 @@ window.addEventListener('DOMContentLoaded', (event) => {
return false;
};
// Fonctions de recherche SAP
let elSearchSAP = document.getElementById('searchSAP');
let elBtnResetSearchSAP = document.getElementById('btnResetSearchSAP');
function restoreSearchSAP() {
const storageKey = panel === 'enCours' ? 'sapSearchTermEnCours' : 'sapSearchTermArchives';
const savedTerm = sessionStorage.getItem(storageKey);
if (savedTerm && savedTerm.length >= 3) {
elSearchSAP.value = savedTerm;
elBtnResetSearchSAP.style.display = 'inline-block';
performSearchSAP(savedTerm);
} else {
elSearchSAP.value = '';
elBtnResetSearchSAP.style.display = 'none';
}
}
function performSearchSAP(term) {
if (term.length < 3) {
return;
}
const context = panel === 'archives' ? 'archives' : 'encours';
fetch('/jxdevis/search_devis_sap', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
term: term,
context: context,
}),
})
.then((response) => response.json())
.then((data) => {
if (data.success) {
const devisIds = data.devis.map((d) => d.rowid);
filterDevisTablesSAP(devisIds, context);
updateBadgesSAP(data.nb_devis);
} else {
console.error('Erreur recherche:', data.message);
}
})
.catch((error) => {
console.error('Erreur AJAX:', error);
});
}
function filterDevisTablesSAP(devisIds, context) {
if (context === 'encours') {
const statuts = document.querySelectorAll('[id^="tblBodyDos"]');
statuts.forEach((tbody) => {
const rows = tbody.querySelectorAll('tr.ligEnCours');
rows.forEach((row) => {
const cells = row.querySelectorAll('.celEnCours');
if (cells.length > 0) {
const rowId = parseInt(cells[0].getAttribute('data-rid'));
if (devisIds.includes(rowId)) {
row.style.display = '';
} else {
row.style.display = 'none';
}
}
});
});
} else {
const archives = document.querySelectorAll('[id^="tblBodyDosArch"]');
archives.forEach((tbody) => {
const rows = tbody.querySelectorAll('tr');
rows.forEach((row) => {
if (row.id && (row.id.startsWith('trArch_') || row.id.startsWith('trArchTous_'))) {
const rowId = parseInt(row.id.replace('trArch_', '').replace('trArchTous_', ''));
if (devisIds.includes(rowId)) {
row.style.display = '';
} else {
row.style.display = 'none';
}
}
});
});
}
}
function updateBadgesSAP(nbDevis) {
Object.keys(nbDevis).forEach((statutId) => {
const liElements = document.querySelectorAll('[id^="liStat"]');
liElements.forEach((li) => {
li.setAttribute('data-after-text', nbDevis[statutId] || '0');
li.setAttribute('data-after-type', 'orange badge top left');
});
});
}
function resetSearchSAP() {
elSearchSAP.value = '';
elBtnResetSearchSAP.style.display = 'none';
const storageKey = panel === 'enCours' ? 'sapSearchTermEnCours' : 'sapSearchTermArchives';
sessionStorage.removeItem(storageKey);
const context = panel === 'archives' ? 'archives' : 'encours';
if (context === 'encours') {
const statuts = document.querySelectorAll('[id^="tblBodyDos"]');
statuts.forEach((tbody) => {
const rows = tbody.querySelectorAll('tr');
rows.forEach((row) => {
row.style.display = '';
});
});
} else {
const archives = document.querySelectorAll('[id^="tblBodyDosArch"]');
archives.forEach((tbody) => {
const rows = tbody.querySelectorAll('tr');
rows.forEach((row) => {
row.style.display = '';
});
});
const tbodyTous = document.getElementById('tblBodyDosArchTous');
if (tbodyTous) {
const rowsTous = tbodyTous.querySelectorAll('tr');
rowsTous.forEach((row) => {
row.style.display = '';
});
}
}
}
elSearchSAP.addEventListener('input', function () {
const term = this.value.trim();
if (searchSapTimeout) {
clearTimeout(searchSapTimeout);
}
if (term.length >= 3) {
elBtnResetSearchSAP.style.display = 'inline-block';
const storageKey = panel === 'enCours' ? 'sapSearchTermEnCours' : 'sapSearchTermArchives';
sessionStorage.setItem(storageKey, term);
searchSapTimeout = setTimeout(() => {
performSearchSAP(term);
}, 300);
} else if (term.length === 0) {
resetSearchSAP();
} else {
elBtnResetSearchSAP.style.display = 'none';
}
});
elBtnResetSearchSAP.addEventListener('click', function () {
resetSearchSAP();
});
// Hook sur changement d'onglet pour restaurer la recherche appropriée
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
if ($(this).attr('href') == '#tabEnCours') {
panel = 'enCours';
restoreSearchSAP();
} else if ($(this).attr('href') == '#tabArchives') {
panel = 'archives';
restoreSearchSAP();
}
});
restoreSearchSAP();
initTableSort();
// Gestion des états actifs des onglets départements (multiples <ul>)
// Utiliser l'événement Bootstrap 'shown.bs.tab' au lieu de 'click'
const allDeptTabs = document.querySelectorAll('.dept-tab a');
allDeptTabs.forEach((tab) => {
$(tab).on('shown.bs.tab', function(e) {
// Retirer 'active' de tous les onglets départements sauf celui-ci
document.querySelectorAll('.dept-tab').forEach((li) => {
if (li !== this.parentElement) {
li.classList.remove('active');
}
});
// Retirer 'active' de l'onglet "Tous"
const tousLi = document.querySelector('a[href="#dosArchTous"]').parentElement;
tousLi.classList.remove('active');
});
});
// Gestion du clic sur "Tous"
const tousTab = document.querySelector('a[href="#dosArchTous"]');
if (tousTab) {
$(tousTab).on('shown.bs.tab', function(e) {
// Retirer 'active' de tous les onglets départements
document.querySelectorAll('.dept-tab').forEach((li) => {
li.classList.remove('active');
});
});
}
// Add new functions
function updateExportButton() {
if (selectedXmlDevis.size > 0) {

View File

@@ -16,6 +16,16 @@ ob_start();
<div id="vb-buttons" class="mb-1">
<button class="btn btn-default" id="btnDevisArchives" title="Voir les devis archivés"><i class="fa fa-stack-overflow fa-lg"></i> Devis archivés</button>
<button class="btn btn-success" id="btnCreateDevis" title="Créer un nouveau devis"><i class="fa fa-plus fa-lg"></i> Créer un devis</button>
<div class="row">
<div class="col-md-9">
<div class="input-group mt-1">
<input type="text" class="form-control" id="searchDevis" placeholder="Rechercher un devis par le nom du client, du contact, ville, opportunité..." maxlength="30">
<span class="input-group-addon">
<i class="fa fa-times clickable" id="btnResetSearch" style="display:none;"></i>
</span>
</div>
</div>
</div>
</div>
<?php
if ($aModel["last_devis"] > 0) {
@@ -87,21 +97,23 @@ ob_start();
echo '<div class="border cm-scrollbar cm-table-w-scroll table-responsive table-800">';
echo '<table class="table table-responsive table-bordered table-fixed" id="tblDos' . $iDos . '">';
echo '<thead><tr>';
echo '<th class="header" scope="col" width="5%">#</th>';
echo '<th class="header" scope="col" width="10%">Demande</th>';
echo '<th class="header clickable" scope="col" width="5%" data-sortable="true" data-sort-type="number" data-column-index="0">#</th>';
echo '<th class="header clickable" scope="col" width="10%" data-sortable="true" data-sort-type="date" data-column-index="1">Demande</th>';
$colIndex = 2;
if (($fkRole == 2 && $iDos == 2) || ($fkRole == 1 && $iDos == 1)) {
// C'est un DV et sur le dossier Encours de validation DV
// Ou le Dir-CO sur le dossier de validation DIR-CO
// on affiche la colonne du nom du RR pour qu'il puisse savoir qui a fait la demande
echo '<th class="header" scope="col" width="15%">RR</th>';
echo '<th class="header clickable" scope="col" width="15%" data-sortable="true" data-sort-type="text" data-column-index="' . $colIndex . '">RR</th>';
$colIndex++;
}
echo '<th class="header" scope="col" width="10%">Opport.</th>';
echo '<th class="header" scope="col" width="5%">CP</th>';
echo '<th class="header" scope="col" width="10%">Ville</th>';
echo '<th class="header" scope="col" width="15%">Client</th>';
echo '<th class="header" scope="col" width="10%">Marché</th>';
echo '<th class="header" scope="col" width="10%">Total HT</th>';
echo '<th class="header" scope="col" width="10%">Marge Totale</th>';
echo '<th class="header clickable" scope="col" width="10%" data-sortable="true" data-sort-type="text" data-column-index="' . $colIndex . '">Opport.</th>';
echo '<th class="header clickable" scope="col" width="5%" data-sortable="true" data-sort-type="number" data-column-index="' . ($colIndex + 1) . '">CP</th>';
echo '<th class="header clickable" scope="col" width="10%" data-sortable="true" data-sort-type="text" data-column-index="' . ($colIndex + 2) . '">Ville</th>';
echo '<th class="header clickable" scope="col" width="15%" data-sortable="true" data-sort-type="text" data-column-index="' . ($colIndex + 3) . '">Client</th>';
echo '<th class="header clickable" scope="col" width="10%" data-sortable="true" data-sort-type="text" data-column-index="' . ($colIndex + 4) . '">Marché</th>';
echo '<th class="header clickable" scope="col" width="10%" data-sortable="true" data-sort-type="number" data-column-index="' . ($colIndex + 5) . '">Total HT</th>';
echo '<th class="header clickable" scope="col" width="10%" data-sortable="true" data-sort-type="number" data-column-index="' . ($colIndex + 6) . '">Marge Totale</th>';
echo '<th class="header" scope="col" width="12%"></th>';
echo '</tr></thead>';
echo '<tbody id="tblBodyDos' . $iDos . '">';
@@ -203,14 +215,14 @@ ob_start();
echo '<div class="border cm-scrollbar cm-table-w-scroll table-responsive table-800">';
echo '<table class="table table-responsive table-bordered table-fixed" id="tblDosArch' . $iDos . '">';
echo '<thead><tr>';
echo '<th class="header" scope="col" width="5%">#</th>';
echo '<th class="header" scope="col" width="10%">Demande</th>';
echo '<th class="header" scope="col" width="10%">Opport.</th>';
echo '<th class="header" scope="col" width="10%">Ville</th>';
echo '<th class="header" scope="col" width="20%">Client</th>';
echo '<th class="header" scope="col" width="10%">Marché</th>';
echo '<th class="header" scope="col" width="10%">Total HT</th>';
echo '<th class="header" scope="col" width="10%">Marge Totale</th>';
echo '<th class="header clickable" scope="col" width="5%" data-sortable="true" data-sort-type="number" data-column-index="0">#</th>';
echo '<th class="header clickable" scope="col" width="10%" data-sortable="true" data-sort-type="date" data-column-index="1">Demande</th>';
echo '<th class="header clickable" scope="col" width="10%" data-sortable="true" data-sort-type="text" data-column-index="2">Opport.</th>';
echo '<th class="header clickable" scope="col" width="10%" data-sortable="true" data-sort-type="text" data-column-index="3">Ville</th>';
echo '<th class="header clickable" scope="col" width="20%" data-sortable="true" data-sort-type="text" data-column-index="4">Client</th>';
echo '<th class="header clickable" scope="col" width="10%" data-sortable="true" data-sort-type="text" data-column-index="5">Marché</th>';
echo '<th class="header clickable" scope="col" width="10%" data-sortable="true" data-sort-type="number" data-column-index="6">Total HT</th>';
echo '<th class="header clickable" scope="col" width="10%" data-sortable="true" data-sort-type="number" data-column-index="7">Marge Totale</th>';
echo '<th class="header" scope="col" width="10%"></th>';
echo '</tr></thead>';
echo '<tbody id="tblBodyDosArch' . $iDos . '">';
@@ -376,34 +388,34 @@ ob_start();
</div>
</div>
<div class="form-group">
<label class="control-label col-md-2" for="inp_contact_nom">Contact :</label>
<label class="control-label col-md-2" for="sel_contact">Contact :</label>
<div class="col-md-4">
<input type="text" class="form-control" id="inp_contact_nom" name="contact_nom" placeholder="Nom" required="required"/>
<p class="help-block">Nom du contact</p>
<select class="form-control" id="sel_contact" name="fk_contact" required="required">
<option value="0">- Sélectionner un contact -</option>
</select>
</div>
<div class="col-md-4">
<input type="text" class="form-control" id="inp_contact_prenom" name="contact_prenom" placeholder="Prénom" required="required"/>
<p class="help-block">Prénom du contact</p>
<div class="col-md-3">
<button type="button" class="btn btn-primary" id="btnGererContacts" disabled>Gérer les contacts</button>
</div>
</div>
<div id="divContactInfos" style="display: none;">
<div class="form-group">
<label class="control-label col-md-2" for="inp_contact_fonction">Fonction du contact :</label>
<div class="col-md-3">
<input type="text" class="form-control" id="inp_contact_fonction" name="contact_fonction" required="required"/>
<label class="control-label col-md-2"></label>
<div class="col-md-8">
<div class="well well-sm">
<div class="row">
<div class="col-md-6">
<p class="mb-0"><strong>Nom et Prénom :</strong> <span id="info_contact_nom_prenom"></span></p>
<p class="mb-0"><strong>Fonction :</strong> <span id="info_contact_fonction"></span></p>
<p class="mb-0"><strong>Email :</strong> <span id="info_contact_email"></span></p>
</div>
<div class="col-md-6">
<p class="mb-0"><strong>Téléphone :</strong> <span id="info_contact_telephone"></span></p>
<p class="mb-0"><strong>Mobile :</strong> <span id="info_contact_mobile"></span></p>
</div>
<label class="control-label col-md-2" for="inp_email">Email :</label>
<div class="col-md-3">
<input type="text" class="form-control" id="inp_email" name="email" required="required"/>
</div>
</div>
<div class=" form-group">
<label class=" control-label col-md-2" for="inp_telephone">Tél :</label>
<div class="col-md-2">
<input type="text" class="form-control" id="inp_telephone" name="telephone" size="10" maxlength="18" placeholder="Fixe"/>
</div>
<label class=" control-label col-md-2" for="inp_mobile">Mob :</label>
<div class="col-md-2">
<input type="text" class="form-control" id="inp_mobile" name="mobile" size=" 10" maxlength="18" placeholder="Mobile"/>
</div>
</div>
<div class="form-group">
@@ -556,25 +568,14 @@ ob_start();
<input type="text" class="form-control numeric" id="inpTotalHT" name="inpTotalHT" readonly="readonly" tabindex="-1" size="12" maxlength="12"/>
<div class="input-group-addon">&euro;</div>
</div>
</td>
<td>
<?php
if ($Conf->_devIp) {
echo '<div class="form-group">';
echo '<label for="inpCoutTotalAchat">Coût total achat :</label>';
}
?>
</td>
<td>
<?php
if ($Conf->_devIp) {
echo '<div class="input-group">';
echo '<input type="text" class="form-control numeric" id="inpCoutTotalAchat" name="inpCoutTotalAchat" readonly="readonly" tabindex="-1" size="12" maxlength="12"/>';
echo '<div class="input-group-addon">&euro;</div>';
echo '</div></div>';
echo '<input type="hidden" id="inpCoutTotalAchat" name="inpCoutTotalAchat"/>';
}
?>
</td>
<td></td>
<td></td>
</tr>
<tr>
<td>
@@ -952,6 +953,117 @@ ob_start();
</div>
</div>
</div>
<div class="modal draggable fade" id="modalGererContacts" tabindex="-1" role="dialog" aria-labelledby="modalGererContactsTitre" data-backdrop="static">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="modGererContactsTitre"><i class="fa fa-address-book fa-lg"></i> Gestion des contacts du client</h4>
</div>
<div class="modal-body">
<input type="hidden" id="inp_fk_client_contacts" value="0"/>
<div class="row mb-2">
<div class="col-md-12">
<button type="button" class="btn btn-success btn-sm" id="btnNouveauContact"><i class="fa fa-plus"></i> Nouveau contact</button>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="table-responsive">
<table class="table table-bordered table-striped" id="tblContacts">
<thead>
<tr>
<th width="20%">Nom</th>
<th width="15%">Prénom</th>
<th width="20%">Fonction</th>
<th width="15%">Téléphone</th>
<th width="20%">Email</th>
<th width="10%" class="text-center">Principal</th>
<th width="15%" class="text-center">Actions</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" id="btnFermerContacts">Fermer</button>
</div>
</div>
</div>
</div>
<div class="modal draggable fade" id="modalEditContact" tabindex="-1" role="dialog" aria-labelledby="modalEditContactTitre" data-backdrop="static">
<div class="modal-dialog modal-md">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="modEditContactTitre"><i class="fa fa-user fa-lg"></i> <span id="modEditContactTitreText">Nouveau contact</span></h4>
</div>
<div class="modal-body">
<form id="frmEditContact" class="form-horizontal" autocomplete="off">
<input type="hidden" id="inp_contact_rowid" name="rowid" value="0"/>
<input type="hidden" id="inp_contact_fk_client" name="fk_client" value="0"/>
<div class="form-group">
<label class="control-label col-md-3" for="inp_contact_nom_edit">Nom :</label>
<div class="col-md-8">
<input type="text" class="form-control" id="inp_contact_nom_edit" name="nom" maxlength="50" required/>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-3" for="inp_contact_prenom_edit">Prénom :</label>
<div class="col-md-8">
<input type="text" class="form-control" id="inp_contact_prenom_edit" name="prenom" maxlength="50" required/>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-3" for="inp_contact_fonction_edit">Fonction :</label>
<div class="col-md-8">
<input type="text" class="form-control" id="inp_contact_fonction_edit" name="fonction" maxlength="50"/>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-3" for="inp_contact_email_edit">Email :</label>
<div class="col-md-8">
<input type="email" class="form-control" id="inp_contact_email_edit" name="email" maxlength="75"/>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-3" for="inp_contact_telephone_edit">Téléphone :</label>
<div class="col-md-8">
<input type="text" class="form-control" id="inp_contact_telephone_edit" name="telephone" maxlength="20"/>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-3" for="inp_contact_mobile_edit">Mobile :</label>
<div class="col-md-8">
<input type="text" class="form-control" id="inp_contact_mobile_edit" name="mobile" maxlength="20"/>
</div>
</div>
<div class="form-group">
<label class="control-label col-md-3" for="inp_contact_principal_edit">Contact principal :</label>
<div class="col-md-8">
<input type="checkbox" id="inp_contact_principal_edit" name="principal" value="1"/>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" id="btnCancelEditContact">Annuler</button>
<button type="button" class="btn btn-success" id="btnSaveEditContact">Enregistrer</button>
</div>
</div>
</div>
</div>
<?php
$modal = ob_get_contents();
ob_clean();

View File

@@ -5,7 +5,37 @@ $metacss = '<link href="/pub/res/css/schat.css" rel="stylesheet" type="text/css"
$barre = "";
ob_start();
?>
<div id="divSAP">
<style>
.dept-tab a {
min-width: 50px !important;
padding: 8px 10px !important;
font-size: 13px;
}
.table-800 {
overflow-y: auto !important;
}
#tabArchives .table-800 {
max-height: 680px !important;
}
.table-800 thead th {
position: sticky;
top: 0;
z-index: 10;
}
</style>
<div class="row" style="margin-bottom: 20px;">
<div class="col-md-2"></div>
<div class="col-md-8">
<div class="input-group">
<input type="text" class="form-control" id="searchSAP" placeholder="Rechercher un devis par le nom du client, du contact, ville, opportunité..." maxlength="30">
<span class="input-group-addon">
<i class="fa fa-times clickable" id="btnResetSearchSAP" style="display:none;"></i>
</span>
</div>
</div>
<div class="col-md-2"></div>
</div>
<div id="divSAP" style="margin-top: 20px;">
<ul class="nav nav-tabs nav-justified" role="tablist">
<li role="presentation" class="active"><a href="#tabEnCours" aria-controls="tabEnCours" role="tab" data-toggle="tab">Les devis en cours</a></li>
<li role="presentation"><a href="#tabArchives" aria-controls="tabArchives" role="tab" data-toggle="tab">Les devis archivés</a></li>
@@ -40,20 +70,22 @@ ob_start();
echo '<div class="border cm-scrollbar cm-table-w-scroll table-responsive table-800">';
echo '<table class="table table-responsive table-bordered table-fixed" id="tblDos' . $iDos . '">';
echo '<thead><tr>';
echo '<th class="header text-center" scope="col" width="5%">#</th>';
echo '<th class="header text-center" scope="col" width="10%">Date Demande</th>';
echo '<th class="header text-center" scope="col" width="10%">Date Remise</th>';
echo '<th class="header text-center" scope="col" width="10%">Responsable Régional</th>';
echo '<th class="header text-center" scope="col" width="10%">Code Etabliss.</th>';
echo '<th class="header text-center" scope="col" width="15%">Etablissement</th>';
echo '<th class="header text-center" scope="col" width="7%">CP</th>';
echo '<th class="header text-center" scope="col" width="10%">Ville</th>';
echo '<th class="header text-center" scope="col" width="10%">Marché</th>';
echo '<th class="header text-center" scope="col" width="10%">Montant Total HT</th>';
echo '<th class="header text-center" scope="col" width="10%">Marge totale</th>';
echo '<th class="header text-center clickable" scope="col" width="5%" data-sortable="true" data-sort-type="number" data-column-index="0">#</th>';
echo '<th class="header text-center clickable" scope="col" width="10%" data-sortable="true" data-sort-type="date" data-column-index="1">Date Demande</th>';
echo '<th class="header text-center clickable" scope="col" width="10%" data-sortable="true" data-sort-type="date" data-column-index="2">Date Remise</th>';
echo '<th class="header text-center clickable" scope="col" width="10%" data-sortable="true" data-sort-type="text" data-column-index="3">Responsable Régional</th>';
echo '<th class="header text-center clickable" scope="col" width="10%" data-sortable="true" data-sort-type="text" data-column-index="4">Code Etabliss.</th>';
echo '<th class="header text-center clickable" scope="col" width="15%" data-sortable="true" data-sort-type="text" data-column-index="5">Etablissement</th>';
echo '<th class="header text-center clickable" scope="col" width="7%" data-sortable="true" data-sort-type="number" data-column-index="6">CP</th>';
echo '<th class="header text-center clickable" scope="col" width="10%" data-sortable="true" data-sort-type="text" data-column-index="7">Ville</th>';
echo '<th class="header text-center clickable" scope="col" width="10%" data-sortable="true" data-sort-type="text" data-column-index="8">Marché</th>';
echo '<th class="header text-center clickable" scope="col" width="10%" data-sortable="true" data-sort-type="number" data-column-index="9">Montant Total HT</th>';
echo '<th class="header text-center clickable" scope="col" width="10%" data-sortable="true" data-sort-type="number" data-column-index="10">Marge totale</th>';
$colIndexActions = 11;
if ($dossier["rowid"] == 7) {
// Si le dossier est "A envoyer au client", on affiche la colonne "Email"
echo '<th class="header text-center" scope="col" width="10%">Email</th>';
$colIndexActions = 12;
}
echo '<th class="header text-center" scope="col" width="20%">Actions <button class="btn btn-info btn-xs btnExportSelectedXML hidden" title="Export XML SAP des devis sélectionnés"><i class="fa fa-scribd fa-lg"></i></button></th>';
echo '</tr></thead>';
@@ -167,39 +199,118 @@ ob_start();
<div role="tabpanel" class="tab-pane" id="tabArchives">
<div class="row">
<div class="col-md-9">
<ul class="nav nav-tabs nav-justified" role="tablist">
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active"><a href="#dosArchTous" aria-controls="dosArchTous" role="tab" data-toggle="tab">Tous</a></li>
</ul>
<?php
$i = 0;
foreach ($aModel["dossiers"] as $dossier) {
if ($i % 15 == 0 && $i > 0) {
echo '<div class="spacer"></div>';
}
$active = ($i == 0) ? "active" : "";
$nbPerLine = 25;
$totalDepts = count($aModel["dossiers"]);
for ($line = 0; $line < 4; $line++) {
echo '<ul class="nav nav-tabs" role="tablist">';
$start = $line * $nbPerLine;
$end = min($start + $nbPerLine, $totalDepts);
for ($j = $start; $j < $end; $j++) {
$dossier = $aModel["dossiers"][$j];
$ceDossier = ($dossier["dossier"] == "") ? "?" : $dossier["dossier"];
echo '<li role="presentation" class="' . $active . '"><a href="#dosArch' . $i . '" aria-controls="dosArch' . $i . '" role="tab" data-toggle="tab">' . $ceDossier . '</a></li>';
$i++;
echo '<li role="presentation" class="dept-tab"><a href="#dosArch' . $j . '" aria-controls="dosArch' . $j . '" role="tab" data-toggle="tab">' . $ceDossier . '</a></li>';
}
echo '</ul>';
}
?>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane p-0 active" id="dosArchTous">
<div class="border cm-scrollbar cm-table-w-scroll table-responsive table-800">
<table class="table table-responsive table-bordered table-fixed" id="tblDosArchTous">
<thead><tr>
<th class="header text-center clickable" scope="col" width="5%" data-sortable="true" data-sort-type="number" data-column-index="0">#</th>
<th class="header text-center clickable" scope="col" width="10%" data-sortable="true" data-sort-type="date" data-column-index="1">Date Demande</th>
<th class="header text-center clickable" scope="col" width="10%" data-sortable="true" data-sort-type="text" data-column-index="2">Resp. Régional</th>
<th class="header text-center clickable" scope="col" width="7%" data-sortable="true" data-sort-type="text" data-column-index="3">Code Etabliss.</th>
<th class="header text-center clickable" scope="col" width="15%" data-sortable="true" data-sort-type="text" data-column-index="4">Etablissement</th>
<th class="header text-center clickable" scope="col" width="5%" data-sortable="true" data-sort-type="number" data-column-index="5">CP</th>
<th class="header text-center clickable" scope="col" width="10%" data-sortable="true" data-sort-type="text" data-column-index="6">Ville</th>
<th class="header text-center clickable" scope="col" width="8%" data-sortable="true" data-sort-type="text" data-column-index="7">Marché</th>
<th class="header text-center clickable" scope="col" width="5%" data-sortable="true" data-sort-type="text" data-column-index="8">Dép.</th>
<th class="header text-center clickable" scope="col" width="10%" data-sortable="true" data-sort-type="number" data-column-index="9">Montant Total HT</th>
<th class="header text-center clickable" scope="col" width="10%" data-sortable="true" data-sort-type="number" data-column-index="10">Marge totale</th>
<th class="header text-center" scope="col" width="10%">Actions</th>
</tr></thead>
<tbody id="tblBodyDosArchTous">
<?php
$i = 0;
foreach ($aModel["devisArchives"] as $devis) {
echo '<tr id="trArchTous_' . $devis["rowid"] . '">';
echo '<td class="text-center">' . $devis["rowid"] . '</td>';
$dateDem = substr($devis["date_demande"], 8, 2) . '/' . substr($devis["date_demande"], 5, 2) . ' ' . substr($devis["date_demande"], 0, 4);
if ($devis["chk_speciaux"] == 1) {
$cellDateDem = '<span data-after-text="S" data-after-type="blue circle">' . $dateDem . '</span>';
} else {
$cellDateDem = $dateDem;
}
echo '<td class="clickable celArchives" data-rid="' . $devis["rowid"] . '">' . $cellDateDem . '</td>';
echo '<td class="clickable celArchives" data-rid="' . $devis["rowid"] . '">' . $devis["prenom"] . " " . $devis["nom"] . '</td>';
echo '<td class="clickable celArchives" data-rid="' . $devis["rowid"] . '">' . $devis["code"] . '</td>';
if ($devis["fk_client"] == 0) {
$ville = $devis["ville_new_client"];
$libelle = '<span data-after-text="N" data-after-type="red circle">' . $devis["lib_new_client"] . '</span>';
} else {
$ville = $devis["ville"];
$libelle = $devis["libelle"];
}
echo '<td class="clickable celArchives" data-rid="' . $devis["rowid"] . '">' . $libelle . '</td>';
echo '<td class="clickable celArchives" data-rid="' . $devis["rowid"] . '">' . $devis["cp"] . '</td>';
echo '<td class="clickable celArchives" data-rid="' . $devis["rowid"] . '">' . $ville . '</td>';
echo '<td class="clickable celArchives" data-rid="' . $devis["rowid"] . '">' . $devis["lib_marche"] . '</td>';
$dossierLabel = ($devis["dossier"] == "") ? "?" : $devis["dossier"];
echo '<td class="clickable celArchives text-center" data-rid="' . $devis["rowid"] . '">' . $dossierLabel . '</td>';
$montant = floatval($devis["montant_total_ht_remise"]);
echo '<td class="clickable celArchives right" data-rid="' . $devis["rowid"] . '">' . number_format($montant, 2, ',', ' ') . ' &euro;</td>';
$margeTotale = floatval($devis["marge_totale"]);
echo '<td class="clickable celArchives right" data-rid="' . $devis["rowid"] . '">' . number_format($margeTotale, 2, ',', ' ') . ' &percnt;</td>';
echo '<td class="text-center">';
echo '<div class="btn-group">';
echo '<button class="btn btn-primary btn-xs btnViewDevisArchives" title="Consulter le devis" data-rid="' . $devis["rowid"] . '"><i class="fa fa-eye fa-lg"></i></button>';
echo '<button class="btn btn-info btn-xs btnExportDevisEnCours" title="Export Excel du devis" data-rid="' . $devis["rowid"] . '" data-libelle="' . $devis["libelle"] . '"><i class="fa fa-file-excel-o fa-lg"></i></button>';
$typBtn = "btn-success";
foreach ($aModel["medias"] as $media) {
if ($media["support_rowid"] == $devis["rowid"]) {
$typBtn = "btn-warning";
break;
}
}
echo '<button class="btn ' . $typBtn . ' btn-xs btnImportPDFEnCours" title="Consulter le PDF SAP du devis" data-rid="' . $devis["rowid"] . '" data-libelle="' . $devis["libelle"] . '"><i class="fa fa-file-pdf-o fa-lg"></i></button>';
echo '</div>';
echo '</td></tr>';
$i++;
}
if ($i == 0) echo '<tr><td colspan="12" class="center">Aucun devis archivé trouvé</td></tr>';
?>
</tbody>
</table>
</div>
</div>
<?php
$iDos = 0;
foreach ($aModel["dossiers"] as $dossier) {
$active = ($iDos == 0) ? "active" : "";
$active = "";
echo '<div role="tabpanel" class="tab-pane p-0 ' . $active . '" id="dosArch' . $iDos . '">';
echo '<div class="border cm-scrollbar cm-table-w-scroll table-responsive table-800">';
echo '<table class="table table-responsive table-bordered table-fixed" id="tblDosArch' . $iDos . '">';
echo '<thead><tr>';
echo '<th class="header text-center" scope="col" width="5%">#</th>';
echo '<th class="header text-center" scope="col" width="10%">Date Demande</th>';
echo '<th class="header text-center" scope="col" width="10%">Resp. Régional</th>';
echo '<th class="header text-center" scope="col" width="7%">Code Etabliss.</th>';
echo '<th class="header text-center" scope="col" width="20%">Etablissement</th>';
echo '<th class="header text-center" scope="col" width="5%">CP</th>';
echo '<th class="header text-center" scope="col" width="13%">Ville</th>';
echo '<th class="header text-center" scope="col" width="10%">Marché</th>';
echo '<th class="header text-center" scope="col" width="10%">Montant Total HT</th>';
echo '<th class="header text-center" scope="col" width="10%">Marge totale</th>';
echo '<th class="header text-center clickable" scope="col" width="5%" data-sortable="true" data-sort-type="number" data-column-index="0">#</th>';
echo '<th class="header text-center clickable" scope="col" width="10%" data-sortable="true" data-sort-type="date" data-column-index="1">Date Demande</th>';
echo '<th class="header text-center clickable" scope="col" width="10%" data-sortable="true" data-sort-type="text" data-column-index="2">Resp. Régional</th>';
echo '<th class="header text-center clickable" scope="col" width="7%" data-sortable="true" data-sort-type="text" data-column-index="3">Code Etabliss.</th>';
echo '<th class="header text-center clickable" scope="col" width="20%" data-sortable="true" data-sort-type="text" data-column-index="4">Etablissement</th>';
echo '<th class="header text-center clickable" scope="col" width="5%" data-sortable="true" data-sort-type="number" data-column-index="5">CP</th>';
echo '<th class="header text-center clickable" scope="col" width="13%" data-sortable="true" data-sort-type="text" data-column-index="6">Ville</th>';
echo '<th class="header text-center clickable" scope="col" width="10%" data-sortable="true" data-sort-type="text" data-column-index="7">Marché</th>';
echo '<th class="header text-center clickable" scope="col" width="10%" data-sortable="true" data-sort-type="number" data-column-index="8">Montant Total HT</th>';
echo '<th class="header text-center clickable" scope="col" width="10%" data-sortable="true" data-sort-type="number" data-column-index="9">Marge totale</th>';
echo '<th class="header text-center" scope="col" width="10%">Actions</th>';
echo '</tr></thead>';
echo '<tbody id="tblBodyDosArch' . $iDos . '">';