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 # Fichiers système
Thumbs.db*.swp Thumbs.db*.swp
.aider*

View File

@@ -2,8 +2,7 @@
require_once dirname(__FILE__) . '/Database.php'; require_once dirname(__FILE__) . '/Database.php';
class Conf class Conf {
{
const admin = 1; const admin = 1;
const intra = 1; const intra = 1;
const erp = 1; const erp = 1;
@@ -11,7 +10,7 @@ class Conf
public $_appname = "cleo"; public $_appname = "cleo";
public $_appscript = "login"; public $_appscript = "login";
public $_appversion = "2.0.2"; public $_appversion = "2.0.4";
public $_appenv; public $_appenv;
public $_apptitle = "CLEO - Gestion de devis"; public $_apptitle = "CLEO - Gestion de devis";
@@ -48,8 +47,7 @@ class Conf
public $_entite = ''; public $_entite = '';
public $_new_version = false; public $_new_version = false;
public function __construct() public function __construct() {
{
$this->loadEnvironment(); $this->loadEnvironment();
$this->loadConfiguration(); $this->loadConfiguration();
$this->setupDebug(); $this->setupDebug();
@@ -115,7 +113,6 @@ class Conf
$this->_brandemail = $entite["email"] ?? ""; $this->_brandemail = $entite["email"] ?? "";
$this->_brandlogo = $entite["appname"] ?? "cleo"; $this->_brandlogo = $entite["appname"] ?? "cleo";
} }
} catch (Exception $e) { } catch (Exception $e) {
error_log("Erreur de configuration: " . $e->getMessage()); error_log("Erreur de configuration: " . $e->getMessage());
$this->setDefaultConfiguration(); $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 //! 4. On met à jour la date_demande, date_remise, num_opportunite et fk_statut_devis du nouveau devis
$newRowidSafe = intval($newRowid); $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); eLog($sql);
qSQL($sql, "gen"); qSQL($sql, "gen");
@@ -124,14 +124,16 @@ switch ($Route->_action) {
$sql = 'UPDATE devis SET chk_maj = 0 WHERE rowid = ' . $cidSafe . ';'; $sql = 'UPDATE devis SET chk_maj = 0 WHERE rowid = ' . $cidSafe . ';';
qSQL($sql, "gen"); 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.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.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.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 .= '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 .= '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 .= '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 LEFT JOIN x_statuts_devis xs ON d.fk_statut_devis = xs.rowid '; $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 .= 'LEFT JOIN marches m ON d.fk_marche = m.rowid ';
$sql .= 'WHERE d.rowid = ' . $cidSafe . ';'; $sql .= 'WHERE d.rowid = ' . $cidSafe . ';';
echo getinfos($sql, "gen", "json"); echo getinfos($sql, "gen", "json");
@@ -162,7 +164,7 @@ switch ($Route->_action) {
if (isset($data->secteur)) { if (isset($data->secteur)) {
$chkSecteur = nettoie_input($data->secteur); $chkSecteur = nettoie_input($data->secteur);
$fkUser = nettoie_input($data->user); $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") { if ($chkSecteur == "1") {
//! on ne prend que les clients du secteur de l'utilisateur //! on ne prend que les clients du secteur de l'utilisateur
$fkUserSafe = intval($fkUser); $fkUserSafe = intval($fkUser);
@@ -197,6 +199,32 @@ switch ($Route->_action) {
} }
break; 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": case "load_devis_marche_produits":
//! Charge les produits enregistrés pour un marché //! Charge les produits enregistrés pour un marché
$data = json_decode(file_get_contents("php://input")); $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 = '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 .= '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"); $upls = getinfos($sql, "gen");
if ($cid != "999") { if ($cid != "999") {
@@ -390,31 +418,13 @@ switch ($Route->_action) {
$commentaire = nettoie_input($_POST["commentaire"]); $commentaire = nettoie_input($_POST["commentaire"]);
$newCommentaire = 0; $newCommentaire = 0;
$contact_nom = nettoie_input($_POST["contact_nom"]); // Récupération du contact sélectionné
$contact_prenom = nettoie_input($_POST["contact_prenom"]); $fk_contact = isset($_POST["fk_contact"]) ? intval($_POST["fk_contact"]) : 0;
$contact_fonction = nettoie_input($_POST["contact_fonction"]); if ($fk_contact == 0) $fk_contact = NULL;
$email = nettoie_input($_POST["email"]);
$telephone = formattel(nettoie_input($_POST["telephone"]));
$mobile = formattel(nettoie_input($_POST["mobile"]));
$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 . ', '; $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) { if ($_POST["rowid"] == 0) {
//! C'est un nouveau devis //! C'est un nouveau devis
//! On le range dans un dossier //! On le range dans un dossier
@@ -465,14 +475,8 @@ switch ($Route->_action) {
} }
eLog('Entete Devis Save : ' . $sql); eLog('Entete Devis Save : ' . $sql);
if ($fk_client != "0") { // NOTE: Les contacts sont maintenant gérés via la table clients_contacts
//! On sauvegarde aussi les infos complémentaires du client qui peuvent ête mises à jour // et non plus directement dans la table clients
$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");
}
// 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 // 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) { if ($newCommentaire > 0) {
@@ -651,11 +655,8 @@ switch ($Route->_action) {
try { try {
$db = Database::getInstance(); $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'; $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 . '%'; $termParam = '%' . $term . '%';
$stmt->bindParam(':term', $termParam, PDO::PARAM_STR); $upls = $db->fetchAll($sql, [':term' => $termParam]);
$stmt->execute();
$upls = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (Exception $e) { } catch (Exception $e) {
error_log("Erreur recherche produits : " . $e->getMessage()); error_log("Erreur recherche produits : " . $e->getMessage());
$upls = []; $upls = [];
@@ -1303,5 +1304,344 @@ switch ($Route->_action) {
echo json_encode(array("success" => "true", "message" => "Devis refusé avec succès")); echo json_encode(array("success" => "true", "message" => "Devis refusé avec succès"));
} }
break; 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(); exit();

View File

@@ -49,6 +49,64 @@ function formate_date($sdate)
return $ladate; 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) { switch ($Route->_action) {
case "upload_clients": case "upload_clients":
@@ -140,10 +198,7 @@ switch ($Route->_action) {
try { try {
$db = Database::getInstance(); $db = Database::getInstance();
$sql = 'SELECT c.* FROM clients c WHERE c.code = :code'; $sql = 'SELECT c.* FROM clients c WHERE c.code = :code';
$stmt = $db->prepare($sql); $record = $db->fetchAll($sql, [':code' => $code]);
$stmt->bindParam(':code', $code, PDO::PARAM_STR);
$stmt->execute();
$record = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (Exception $e) { } catch (Exception $e) {
error_log("Erreur recherche client : " . $e->getMessage()); error_log("Erreur recherche client : " . $e->getMessage());
$record = []; $record = [];
@@ -155,8 +210,7 @@ switch ($Route->_action) {
$db = Database::getInstance(); $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 = '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'; $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 = $db->query($sql, [
$stmt->execute([
':code' => $code, ':code' => $code,
':libelle' => $libelle, ':libelle' => $libelle,
':siret' => $siret, ':siret' => $siret,
@@ -175,6 +229,10 @@ switch ($Route->_action) {
]); ]);
$fkClient = $db->lastInsertId(); $fkClient = $db->lastInsertId();
fwrite($fhlog, $row . "--- Ajout client avec requête préparée\r\n"); 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) { } catch (Exception $e) {
error_log("Erreur insertion client : " . $e->getMessage()); error_log("Erreur insertion client : " . $e->getMessage());
fwrite($fhlog, "Erreur insertion : " . $e->getMessage() . "\r\n"); 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 = '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 .= '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'; $sql .= 'WHERE code = :code';
$stmt = $db->prepare($sql); $stmt = $db->query($sql, [
$stmt->execute([
':libelle' => $libelle, ':libelle' => $libelle,
':siret' => $siret, ':siret' => $siret,
':adresse1' => $adresse1, ':adresse1' => $adresse1,
@@ -217,6 +274,10 @@ switch ($Route->_action) {
':code' => $code ':code' => $code
]); ]);
fwrite($fhlog, $row . "--- MàJ client avec requête préparée\r\n"); 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) { } catch (Exception $e) {
error_log("Erreur mise à jour client : " . $e->getMessage()); error_log("Erreur mise à jour client : " . $e->getMessage());
fwrite($fhlog, "Erreur MàJ : " . $e->getMessage() . "\r\n"); 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 $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 // on réencode en ISO 8859-1 pour éviter les problèmes d'accent
if ($codOrigin == "UTF-8") { if ($codOrigin == "UTF-8") {
$libelle = utf8_decode($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");
if ($codOrigin != "ISO-8859-1") { } elseif ($codOrigin == "Windows-1252" || $codOrigin == "CP1252") {
// Convertir en ISO 8859-1 // Windows-1252 est très proche de ISO-8859-1
$libelle = iconv($codOrigin, "ISO-8859-15//IGNORE", $libelle); // 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 $groupe = str_replace(" ", " ", trim($data[2])); // on remplace les doubles espaces par un simple espace
$liste = trim($data[3]); $liste = trim($data[3]);
@@ -532,7 +599,10 @@ switch ($Route->_action) {
$ret = array('ret' => "ko", 'msg' => "Aucun fichier à importer"); $ret = array('ret' => "ko", 'msg' => "Aucun fichier à importer");
} else { } else {
if ($erreur == "") { 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 { } else {
$ret = array('ret' => "ko", 'msg' => $erreur); $ret = array('ret' => "ko", 'msg' => $erreur);
} }

View File

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

View File

@@ -1,25 +1,53 @@
#!/bin/bash #!/bin/bash
# Script de déploiement optimisé de Cleo vers l'environnement de développement # Script de déploiement optimisé de Cleo
# Version: 2.0 - Utilise tar.gz pour un transfert plus rapide # 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 cd /home/pierre/dev/cleo
# Configuration du serveur hôte Debian 12 # Détecter le mode de déploiement
HOST_SSH_HOST=195.154.80.116 # Adresse IP du serveur hôte DEPLOY_MODE=${1:-dev}
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
# Configuration du conteneur Incus hébergeant cette application if [ "$DEPLOY_MODE" = "pra" ]; then
CT_PROJECT_NAME=default # Nom du projet Incus où se trouve le conteneur # Configuration PROD : IN3/dva-front → IN4/pra-front
CT_NAME=dva-front # Nom du conteneur Incus SOURCE_HOST=11.1.2.1
CT_IP=13.23.33.42 # IP interne du conteneur Incus SOURCE_SSH_USER=root
DEPLOY_DIR=/var/www # Répertoire de déploiement sur le conteneur SOURCE_SSH_PORT=22
APP_NAME=cleo # Nom de l'application 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 # Propriétaire et groupe pour les fichiers et dossiers de destination
OWNER=nginx OWNER=nobody
GROUP=nginx GROUP=nginx
# Couleurs pour l'affichage # Couleurs pour l'affichage
@@ -36,21 +64,149 @@ fi
# Afficher les paramètres # Afficher les paramètres
echo -e "${GREEN}=== Déploiement optimisé CLEO ===${NC}" echo -e "${GREEN}=== Déploiement optimisé CLEO ===${NC}"
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 "Serveur hôte: $HOST_SSH_USER@$HOST_SSH_HOST:$HOST_SSH_PORT"
echo "Conteneur: $CT_NAME" echo "Conteneur: $CT_NAME"
echo "Déploiement: $DEPLOY_DIR/$APP_NAME" echo "Déploiement: $DEPLOY_DIR/$APP_NAME"
fi
echo "==================================" echo "=================================="
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 # 1. Créer l'archive tar.gz localement
echo -e "${YELLOW}1. Création de l'archive...${NC}" echo -e "${YELLOW}1. Création de l'archive...${NC}"
tar -czf /tmp/cleo.tar.gz \ tar -czf /tmp/cleo.tar.gz \
--exclude='.git' \ --exclude='.git' \
--exclude='log/*.log' \ --exclude='log/*.log' \
--exclude='pub/files/upload/*' \ --exclude='pub/files/upload/*' \
--exclude='docs/*.sql' \ --exclude='docs' \
--exclude='migration' \
--exclude='.claude' \
--exclude='.vscode' \
--exclude='vendor' \ --exclude='vendor' \
--exclude='backup_*' \ --exclude='backup_*' \
--exclude='*.tar.gz' \ --exclude='*.tar.gz' \
--exclude='.env' \
--exclude='.env.swp' \ --exclude='.env.swp' \
. .
@@ -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 d -exec chmod 755 {} \; && \
find $DEPLOY_DIR/$APP_NAME -type f -exec chmod 644 {} \; && \ find $DEPLOY_DIR/$APP_NAME -type f -exec chmod 644 {} \; && \
mkdir -p $DEPLOY_DIR/$APP_NAME/log && \ mkdir -p $DEPLOY_DIR/$APP_NAME/log && \
chmod 777 $DEPLOY_DIR/$APP_NAME/log && \ chmod 775 $DEPLOY_DIR/$APP_NAME/log && \
touch $DEPLOY_DIR/$APP_NAME/log/$(date +%m%d).log && \ touch $DEPLOY_DIR/$APP_NAME/log/\$(date +%m%d).log && \
chmod 777 $DEPLOY_DIR/$APP_NAME/log/$(date +%m%d).log && \ chmod 664 $DEPLOY_DIR/$APP_NAME/log/\$(date +%m%d).log && \
chown nobody:nobody $DEPLOY_DIR/$APP_NAME/log/*.log && \ chown $OWNER:$GROUP $DEPLOY_DIR/$APP_NAME/log/*.log && \
chmod 644 $DEPLOY_DIR/$APP_NAME/.env && \ chmod 640 $DEPLOY_DIR/$APP_NAME/.env && \
rm -f $DEPLOY_DIR/$APP_NAME/.env.swp && \ rm -f $DEPLOY_DIR/$APP_NAME/.env.swp && \
if [ -d $DEPLOY_DIR/$APP_NAME/pub/files/upload ]; then if [ -d $DEPLOY_DIR/$APP_NAME/pub/files/upload ]; then
chmod 775 $DEPLOY_DIR/$APP_NAME/pub/files/upload && \ chmod 775 $DEPLOY_DIR/$APP_NAME/pub/files/upload && \
@@ -142,10 +298,18 @@ echo -e "${YELLOW}5. Nettoyage local...${NC}"
rm -f /tmp/cleo.tar.gz rm -f /tmp/cleo.tar.gz
echo -e "${GREEN}✓ Archive locale supprimée${NC}" echo -e "${GREEN}✓ Archive locale supprimée${NC}"
fi
echo "" echo ""
echo -e "${GREEN}===================================================${NC}" echo -e "${GREEN}===================================================${NC}"
echo -e "${GREEN} Déploiement terminé avec succès ! ${NC}" echo -e "${GREEN} Déploiement terminé avec succès ! ${NC}"
echo -e "${GREEN}===================================================${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 "Site: http://dcleo.unikoffice.com"
echo "Environnement: DÉVELOPPEMENT (dva-front)"
echo "Chemin: $DEPLOY_DIR/$APP_NAME sur $CT_NAME" echo "Chemin: $DEPLOY_DIR/$APP_NAME sur $CT_NAME"
fi
echo "" 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". 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 ## Architecture technique
@@ -53,7 +53,9 @@ cleo/
- Gestion des remises par paliers de quantité - Gestion des remises par paliers de quantité
2. **Gestion des clients** (`cclients.php`) 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 - Segmentation par secteur géographique
- Types de clients paramétrables - Types de clients paramétrables
- Import/export de données - Import/export de données
@@ -82,10 +84,11 @@ cleo/
- **Sécurité** : Requêtes préparées systématiques - **Sécurité** : Requêtes préparées systématiques
### Tables principales ### 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_produits` : Lignes de produits des devis
- `devis_histo` : Historique des modifications - `devis_histo` : Historique des modifications
- `clients` : Base clients - `clients` : Base clients
- `clients_contacts` : Contacts multiples par client (v2.0.3)
- `produits` : Catalogue produits - `produits` : Catalogue produits
- `produits_familles` : Familles de produits avec marges - `produits_familles` : Familles de produits avec marges
- `marches` : Référentiel des marchés - `marches` : Référentiel des marchés
@@ -95,22 +98,32 @@ cleo/
## Points de sécurité ## 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` - Credentials externalisés dans `.env`
- Variables d'environnement utilisées systématiquement - 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 - Migration complète vers PDO
- Requêtes préparées dans la classe `Database` - Requêtes préparées dans la classe `Database`
- Pattern Singleton pour la connexion - 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 - Classe `Database` avec gestion d'erreurs centralisée
- Logging contrôlé par variables d'environnement - Logging contrôlé par variables d'environnement
- Mode debug désactivable en production - 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 ### Vulnérabilités restantes à traiter
#### 1. Injections SQL résiduelles #### 1. Injections SQL résiduelles
@@ -208,15 +221,28 @@ cleo/
## Conclusion ## Conclusion
CLEO v2.0.1 représente une évolution majeure avec la migration réussie vers une architecture sécurisée : CLEO v2.0.3 représente l'aboutissement de trois itérations majeures d'amélioration :
- ✅ 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
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* *Document mis à jour le 21 octobre 2025*
*Version 2.0.1 - Post-migration* *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 ### 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 #### 8. Dupliquer une ligne produit
**Priorité**: Moyenne **Priorité**: Moyenne
**Description**: Permettre la duplication d'une ligne produit dans un même devis (utile pour les gratuités). **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 - [ ] Paginer les résultats de recherche
- [ ] Export des résultats en Excel - [ ] 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 #### 21. Actualisation tarifaire
**Priorité**: Moyenne **Priorité**: Moyenne
**Description**: Permettre l'actualisation des prix selon la dernière grille tarifaire. **Description**: Permettre l'actualisation des prix selon la dernière grille tarifaire.
@@ -56,8 +37,73 @@
- [ ] Recalculer automatiquement les marges - [ ] Recalculer automatiquement les marges
- [ ] Tracer l'actualisation dans l'historique - [ ] 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 ### 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 #### 14. Gestion de la prise en charge
**Priorité**: Haute **Priorité**: Haute
**Description**: Ajouter la traçabilité de la prise en charge et du transfert EDI. **Description**: Ajouter la traçabilité de la prise en charge et du transfert EDI.
@@ -117,65 +163,7 @@
### Plan de migration - État d'avancement ### Plan de migration - État d'avancement
#### ✅ Phase 0 - Refactoring base de données (COMPLÉTÉ - 12/09/2025) **Migration complétée** - Toutes les phases (0 à 4) sont terminées.
- [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
### Configuration technique ### Configuration technique
@@ -207,6 +195,159 @@ DB_PASSWORD=<PROD_PASSWORD> # À sécuriser
- [ ] Scripts de backup automatisés à mettre en place - [ ] Scripts de backup automatisés à mettre en place
- [ ] Réplication master-slave pour haute disponibilité (optionnel) - [ ] 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 ## Améliorations techniques prioritaires
### Sécurité ### Sécurité
@@ -252,26 +393,15 @@ DB_PASSWORD=<PROD_PASSWORD> # À sécuriser
## Notes de développement ## Notes de développement
### Structure de la table `clients_contacts` (à créer) ### Structure de la table `clients_contacts` (CRÉÉE - v2.0.3)
```sql Table créée et opérationnelle avec :
CREATE TABLE clients_contacts ( - Clé étrangère vers `clients` avec CASCADE
rowid INT PRIMARY KEY AUTO_INCREMENT, - Gestion du contact principal (un seul par client)
fk_client INT NOT NULL, - Soft delete via champ `active`
nom VARCHAR(100), - Traçabilité (date_creat, fk_user_creat, date_modif, fk_user_modif)
prenom VARCHAR(100), - Index sur fk_client, principal et email
fonction VARCHAR(100), - Contrainte UNIQUE sur rowid
telephone VARCHAR(20), - Voir `docs/migration_clients_contacts.sql` pour la structure complète
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)
);
```
### Modifications table `devis` pour SAP ### Modifications table `devis` pour SAP
```sql ```sql
@@ -285,24 +415,36 @@ ALTER TABLE devis ADD COLUMN erreur_transfert_edi TEXT;
## Résumé de l'état actuel ## 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 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 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 3. **Sécurité renforcée** : PDO, requêtes préparées, variables d'environnement
4. **Container `dva-front`** : MariaDB supprimé, application PHP uniquement 4. **Container `dva-front`** : MariaDB supprimé, application PHP uniquement
5. **Container `maria3`** : Base de données centralisée opérationnelle 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) - 8 critiques (fonction autocomplete, injections dans cjxpost.php, mclients.php, mdevis.php)
- 6 moyennes (cjxdevis.php, cjxexport.php, cjximport.php, mexpxls.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 ### 🎯 Prochaines étapes prioritaires
1. **Migration PROD vers IN4** : Export/Import des containers vers `pra-front` et `maria4` 1. **Nettoyage BDD** : Supprimer les anciens champs contact de la table `clients` (après validation)
2. **Fonctionnalités métier** : Points 14, 16 (voir sections ci-dessus) 2. **Migration PROD vers IN4** : Export/Import des containers vers `pra-front` et `maria4`
3. **Sécurité XSS** : Audit et correction des failles XSS potentielles 3. **Fonctionnalités métier** : Points 8, 14, 16, 21 (voir sections ci-dessus)
4. **Tests** : Mise en place de tests automatisés de sécurité 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* *Document mis à jour le 21 octobre 2025*
*Version 2.0.2 - Sécurité SQL complète* *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 */ /*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_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; /*!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_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_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' */; /*!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` -- Table structure for table `clients`
@@ -22,7 +22,7 @@
DROP TABLE IF EXISTS `clients`; DROP TABLE IF EXISTS `clients`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */; /*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `clients` ( CREATE TABLE `clients` (
`rowid` int(11) NOT NULL AUTO_INCREMENT, `rowid` int(11) NOT NULL AUTO_INCREMENT,
`code` int(11) NOT NULL, `code` int(11) NOT NULL,
@@ -51,9 +51,38 @@ CREATE TABLE `clients` (
UNIQUE KEY `code_UNIQUE` (`code`), UNIQUE KEY `code_UNIQUE` (`code`),
KEY `libelle` (`libelle`), KEY `libelle` (`libelle`),
KEY `cp` (`cp`) 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 */; /*!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` -- Table structure for table `clients_sites`
@@ -61,7 +90,7 @@ CREATE TABLE `clients` (
DROP TABLE IF EXISTS `clients_sites`; DROP TABLE IF EXISTS `clients_sites`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */; /*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `clients_sites` ( CREATE TABLE `clients_sites` (
`rowid` int(11) NOT NULL AUTO_INCREMENT, `rowid` int(11) NOT NULL AUTO_INCREMENT,
`fk_client` int(11) NOT NULL, `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'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
/*!40101 SET character_set_client = @saved_cs_client */; /*!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` -- Table structure for table `commerciaux`
-- --
DROP TABLE IF EXISTS `commerciaux`; DROP TABLE IF EXISTS `commerciaux`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */; /*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `commerciaux` ( CREATE TABLE `commerciaux` (
`rowid` int(11) NOT NULL AUTO_INCREMENT, `rowid` int(11) NOT NULL AUTO_INCREMENT,
`fk_entite` int(11) DEFAULT 0, `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'; ) ENGINE=InnoDB AUTO_INCREMENT=27 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
-- --
-- Table structure for table `commerciaux_entites` -- Table structure for table `commerciaux_entites`
-- --
DROP TABLE IF EXISTS `commerciaux_entites`; DROP TABLE IF EXISTS `commerciaux_entites`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */; /*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `commerciaux_entites` ( CREATE TABLE `commerciaux_entites` (
`rowid` int(11) NOT NULL AUTO_INCREMENT, `rowid` int(11) NOT NULL AUTO_INCREMENT,
`libelle` varchar(45) DEFAULT NULL, `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'; ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
/*!40101 SET character_set_client = @saved_cs_client */; /*!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` -- Table structure for table `commerciaux_params`
-- --
DROP TABLE IF EXISTS `commerciaux_params`; DROP TABLE IF EXISTS `commerciaux_params`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */; /*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `commerciaux_params` ( CREATE TABLE `commerciaux_params` (
`rowid` int(11) NOT NULL AUTO_INCREMENT, `rowid` int(11) NOT NULL AUTO_INCREMENT,
`fk_commercial` int(11) DEFAULT NULL, `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'; ) ENGINE=InnoDB AUTO_INCREMENT=16 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
-- --
-- Table structure for table `devis` -- Table structure for table `devis`
-- --
DROP TABLE IF EXISTS `devis`; DROP TABLE IF EXISTS `devis`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */; /*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `devis` ( CREATE TABLE `devis` (
`rowid` int(11) NOT NULL AUTO_INCREMENT, `rowid` int(11) NOT NULL AUTO_INCREMENT,
`fk_user` int(11) NOT NULL DEFAULT 0, `fk_user` int(11) NOT NULL DEFAULT 0,
@@ -285,6 +290,7 @@ CREATE TABLE `devis` (
`date_remise` date DEFAULT NULL, `date_remise` date DEFAULT NULL,
`num_opportunite` varchar(8) NOT NULL DEFAULT '', `num_opportunite` varchar(8) NOT NULL DEFAULT '',
`fk_client` int(11) NOT NULL DEFAULT 0, `fk_client` int(11) NOT NULL DEFAULT 0,
`fk_contact` int(11) DEFAULT NULL,
`fk_marche` int(11) NOT NULL DEFAULT 0, `fk_marche` int(11) NOT NULL DEFAULT 0,
`fk_statut_devis` int(11) NOT NULL DEFAULT 0, `fk_statut_devis` int(11) NOT NULL DEFAULT 0,
`chk_clients_secteur` tinyint(1) NOT NULL DEFAULT 1, `chk_clients_secteur` tinyint(1) NOT NULL DEFAULT 1,
@@ -327,18 +333,18 @@ CREATE TABLE `devis` (
KEY `fk_client` (`fk_client`), KEY `fk_client` (`fk_client`),
KEY `fk_statut_devis` (`fk_statut_devis`), KEY `fk_statut_devis` (`fk_statut_devis`),
KEY `date_demande` (`date_demande`), KEY `date_demande` (`date_demande`),
KEY `dossier` (`fk_user`,`dossier`) KEY `dossier` (`fk_user`,`dossier`),
) ENGINE=InnoDB AUTO_INCREMENT=4611 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON'; 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 */; /*!40101 SET character_set_client = @saved_cs_client */;
-- --
-- Table structure for table `devis_histo` -- Table structure for table `devis_histo`
-- --
DROP TABLE IF EXISTS `devis_histo`; DROP TABLE IF EXISTS `devis_histo`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */; /*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `devis_histo` ( CREATE TABLE `devis_histo` (
`rowid` int(11) NOT NULL AUTO_INCREMENT, `rowid` int(11) NOT NULL AUTO_INCREMENT,
`fk_devis` int(11) DEFAULT NULL, `fk_devis` int(11) DEFAULT NULL,
@@ -350,7 +356,7 @@ CREATE TABLE `devis_histo` (
`fk_statut_devis` int(11) DEFAULT NULL, `fk_statut_devis` int(11) DEFAULT NULL,
PRIMARY KEY (`rowid`), PRIMARY KEY (`rowid`),
KEY `devis_histo_fk_devis_index` (`fk_devis`) 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 */; /*!40101 SET character_set_client = @saved_cs_client */;
-- --
@@ -359,7 +365,7 @@ CREATE TABLE `devis_histo` (
DROP TABLE IF EXISTS `devis_produits`; DROP TABLE IF EXISTS `devis_produits`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */; /*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `devis_produits` ( CREATE TABLE `devis_produits` (
`rowid` int(11) NOT NULL AUTO_INCREMENT, `rowid` int(11) NOT NULL AUTO_INCREMENT,
`fk_devis` int(11) NOT NULL, `fk_devis` int(11) NOT NULL,
@@ -392,17 +398,16 @@ CREATE TABLE `devis_produits` (
PRIMARY KEY (`rowid`), PRIMARY KEY (`rowid`),
KEY `devis_produits__devis` (`fk_devis`), KEY `devis_produits__devis` (`fk_devis`),
KEY `devis_produits__produit` (`fk_produit`) 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 */; /*!40101 SET character_set_client = @saved_cs_client */;
-- --
-- Table structure for table `devis_speciaux` -- Table structure for table `devis_speciaux`
-- --
DROP TABLE IF EXISTS `devis_speciaux`; DROP TABLE IF EXISTS `devis_speciaux`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */; /*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `devis_speciaux` ( CREATE TABLE `devis_speciaux` (
`rowid` int(11) NOT NULL AUTO_INCREMENT, `rowid` int(11) NOT NULL AUTO_INCREMENT,
`fk_devis` int(11) NOT NULL DEFAULT 0, `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'; ) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
-- --
-- Table structure for table `entites` -- Table structure for table `entites`
-- --
DROP TABLE IF EXISTS `entites`; DROP TABLE IF EXISTS `entites`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */; /*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `entites` ( CREATE TABLE `entites` (
`rowid` int(11) NOT NULL AUTO_INCREMENT, `rowid` int(11) NOT NULL AUTO_INCREMENT,
`libelle` varchar(45) DEFAULT '', `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'; ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
-- --
-- Table structure for table `import_ventes` -- Table structure for table `import_ventes`
-- --
DROP TABLE IF EXISTS `import_ventes`; DROP TABLE IF EXISTS `import_ventes`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */; /*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `import_ventes` ( CREATE TABLE `import_ventes` (
`rowid` int(11) NOT NULL AUTO_INCREMENT, `rowid` int(11) NOT NULL AUTO_INCREMENT,
`source` varchar(15) DEFAULT '', `source` varchar(15) DEFAULT '',
@@ -570,7 +573,7 @@ CREATE TABLE `import_ventes` (
DROP TABLE IF EXISTS `infos`; DROP TABLE IF EXISTS `infos`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */; /*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `infos` ( CREATE TABLE `infos` (
`rowid` int(11) NOT NULL AUTO_INCREMENT, `rowid` int(11) NOT NULL AUTO_INCREMENT,
`date_infos` date DEFAULT NULL, `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'; ) ENGINE=InnoDB AUTO_INCREMENT=29 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
-- --
-- Table structure for table `marches` -- Table structure for table `marches`
-- --
DROP TABLE IF EXISTS `marches`; DROP TABLE IF EXISTS `marches`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */; /*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `marches` ( CREATE TABLE `marches` (
`rowid` int(11) NOT NULL AUTO_INCREMENT, `rowid` int(11) NOT NULL AUTO_INCREMENT,
`numero` varchar(20) NOT NULL DEFAULT '', `numero` varchar(20) NOT NULL DEFAULT '',
@@ -642,7 +644,7 @@ CREATE TABLE `marches` (
DROP TABLE IF EXISTS `marches_listes`; DROP TABLE IF EXISTS `marches_listes`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */; /*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `marches_listes` ( CREATE TABLE `marches_listes` (
`rowid` int(11) NOT NULL AUTO_INCREMENT, `rowid` int(11) NOT NULL AUTO_INCREMENT,
`fk_marche` int(11) DEFAULT NULL, `fk_marche` int(11) DEFAULT NULL,
@@ -660,7 +662,7 @@ CREATE TABLE `marches_listes` (
DROP TABLE IF EXISTS `marches_produits`; DROP TABLE IF EXISTS `marches_produits`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */; /*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `marches_produits` ( CREATE TABLE `marches_produits` (
`rowid` int(11) NOT NULL AUTO_INCREMENT, `rowid` int(11) NOT NULL AUTO_INCREMENT,
`fk_marche` int(11) DEFAULT 0, `fk_marche` int(11) DEFAULT 0,
@@ -702,7 +704,7 @@ CREATE TABLE `marches_produits` (
DROP TABLE IF EXISTS `marches_versions`; DROP TABLE IF EXISTS `marches_versions`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */; /*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `marches_versions` ( CREATE TABLE `marches_versions` (
`rowid` int(11) NOT NULL AUTO_INCREMENT COMMENT 'Id', `rowid` int(11) NOT NULL AUTO_INCREMENT COMMENT 'Id',
`libelle` varchar(75) DEFAULT NULL COMMENT 'Libellé', `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'; ) 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 */; /*!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` -- Table structure for table `medias`
-- --
DROP TABLE IF EXISTS `medias`; DROP TABLE IF EXISTS `medias`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */; /*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `medias` ( CREATE TABLE `medias` (
`rowid` int(11) NOT NULL AUTO_INCREMENT, `rowid` int(11) NOT NULL AUTO_INCREMENT,
`dir0` varchar(150) DEFAULT NULL, `dir0` varchar(150) DEFAULT NULL,
@@ -746,7 +737,7 @@ CREATE TABLE `medias` (
PRIMARY KEY (`rowid`), PRIMARY KEY (`rowid`),
UNIQUE KEY `rowid_UNIQUE` (`rowid`), UNIQUE KEY `rowid_UNIQUE` (`rowid`),
KEY `support` (`support`,`support_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 */; /*!40101 SET character_set_client = @saved_cs_client */;
-- --
@@ -755,7 +746,7 @@ CREATE TABLE `medias` (
DROP TABLE IF EXISTS `notifications`; DROP TABLE IF EXISTS `notifications`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */; /*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `notifications` ( CREATE TABLE `notifications` (
`rowid` int(11) NOT NULL AUTO_INCREMENT, `rowid` int(11) NOT NULL AUTO_INCREMENT,
`dateheure` datetime DEFAULT NULL, `dateheure` datetime DEFAULT NULL,
@@ -775,7 +766,7 @@ CREATE TABLE `notifications` (
DROP TABLE IF EXISTS `produits`; DROP TABLE IF EXISTS `produits`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */; /*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `produits` ( CREATE TABLE `produits` (
`rowid` int(11) NOT NULL AUTO_INCREMENT, `rowid` int(11) NOT NULL AUTO_INCREMENT,
`fk_marche` int(11) NOT NULL DEFAULT 0, `fk_marche` int(11) NOT NULL DEFAULT 0,
@@ -821,7 +812,7 @@ CREATE TABLE `produits` (
DROP TABLE IF EXISTS `produits_familles`; DROP TABLE IF EXISTS `produits_familles`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */; /*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `produits_familles` ( CREATE TABLE `produits_familles` (
`rowid` int(11) NOT NULL AUTO_INCREMENT, `rowid` int(11) NOT NULL AUTO_INCREMENT,
`groupe` varchar(30) NOT NULL, `groupe` varchar(30) NOT NULL,
@@ -841,7 +832,7 @@ CREATE TABLE `produits_familles` (
DROP TABLE IF EXISTS `regions`; DROP TABLE IF EXISTS `regions`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */; /*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `regions` ( CREATE TABLE `regions` (
`rowid` int(11) NOT NULL AUTO_INCREMENT, `rowid` int(11) NOT NULL AUTO_INCREMENT,
`libelle` varchar(75) DEFAULT NULL, `libelle` varchar(75) DEFAULT NULL,
@@ -851,22 +842,13 @@ CREATE TABLE `regions` (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
/*!40101 SET character_set_client = @saved_cs_client */; /*!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` -- Table structure for table `simul`
-- --
DROP TABLE IF EXISTS `simul`; DROP TABLE IF EXISTS `simul`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */; /*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `simul` ( CREATE TABLE `simul` (
`rowid` int(11) NOT NULL AUTO_INCREMENT, `rowid` int(11) NOT NULL AUTO_INCREMENT,
`fk_import_vente` int(11) DEFAULT NULL, `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'; ) ENGINE=InnoDB AUTO_INCREMENT=1057 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
/*!40101 SET character_set_client = @saved_cs_client */; /*!40101 SET character_set_client = @saved_cs_client */;
-- --
-- Table structure for table `users` -- Table structure for table `users`
-- --
DROP TABLE IF EXISTS `users`; DROP TABLE IF EXISTS `users`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */; /*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `users` ( CREATE TABLE `users` (
`rowid` int(10) unsigned NOT NULL AUTO_INCREMENT, `rowid` int(10) unsigned NOT NULL AUTO_INCREMENT,
`fk_entite` int(11) DEFAULT NULL, `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'; ) ENGINE=InnoDB AUTO_INCREMENT=50 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
/*!40101 SET character_set_client = @saved_cs_client */; /*!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` -- Table structure for table `ventes`
-- --
DROP TABLE IF EXISTS `ventes`; DROP TABLE IF EXISTS `ventes`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */; /*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `ventes` ( CREATE TABLE `ventes` (
`rowid` int(11) NOT NULL AUTO_INCREMENT, `rowid` int(11) NOT NULL AUTO_INCREMENT,
`source` varchar(45) DEFAULT NULL, `source` varchar(45) DEFAULT NULL,
@@ -977,22 +1009,13 @@ CREATE TABLE `ventes` (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
/*!40101 SET character_set_client = @saved_cs_client */; /*!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` -- Table structure for table `x_clients_types`
-- --
DROP TABLE IF EXISTS `x_clients_types`; DROP TABLE IF EXISTS `x_clients_types`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!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` ( CREATE TABLE `x_clients_types` (
`rowid` int(11) NOT NULL AUTO_INCREMENT, `rowid` int(11) NOT NULL AUTO_INCREMENT,
`code` char(3) DEFAULT NULL, `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'; ) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
/*!40101 SET character_set_client = @saved_cs_client */; /*!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` -- Table structure for table `x_familles`
-- --
DROP TABLE IF EXISTS `x_familles`; DROP TABLE IF EXISTS `x_familles`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */; /*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `x_familles` ( CREATE TABLE `x_familles` (
`rowid` int(11) NOT NULL AUTO_INCREMENT, `rowid` int(11) NOT NULL AUTO_INCREMENT,
`libelle` varchar(20) NOT NULL DEFAULT '', `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'; ) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
/*!40101 SET character_set_client = @saved_cs_client */; /*!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` -- Table structure for table `x_regions`
-- --
DROP TABLE IF EXISTS `x_regions`; DROP TABLE IF EXISTS `x_regions`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */; /*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `x_regions` ( CREATE TABLE `x_regions` (
`rowid` int(11) NOT NULL AUTO_INCREMENT, `rowid` int(11) NOT NULL AUTO_INCREMENT,
`fk_entite` int(11) DEFAULT 0, `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'; ) ENGINE=InnoDB AUTO_INCREMENT=20 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
/*!40101 SET character_set_client = @saved_cs_client */; /*!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` -- Table structure for table `x_roles`
-- --
DROP TABLE IF EXISTS `x_roles`; DROP TABLE IF EXISTS `x_roles`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */; /*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `x_roles` ( CREATE TABLE `x_roles` (
`rowid` int(11) NOT NULL AUTO_INCREMENT, `rowid` int(11) NOT NULL AUTO_INCREMENT,
`libelle` varchar(45) DEFAULT '', `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'; ) 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 */; /*!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` -- Table structure for table `x_statuts_devis`
-- --
DROP TABLE IF EXISTS `x_statuts_devis`; DROP TABLE IF EXISTS `x_statuts_devis`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!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` ( CREATE TABLE `x_statuts_devis` (
`rowid` int(11) NOT NULL AUTO_INCREMENT, `rowid` int(11) NOT NULL AUTO_INCREMENT,
`libelle` varchar(30) DEFAULT NULL, `libelle` varchar(30) DEFAULT NULL,
@@ -1152,22 +1096,47 @@ CREATE TABLE `x_statuts_devis` (
/*!40101 SET character_set_client = @saved_cs_client */; /*!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; DROP TABLE IF EXISTS `y_pages`;
/*!40000 ALTER TABLE `x_statuts_devis` DISABLE KEYS */; /*!40101 SET @saved_cs_client = @@character_set_client */;
INSERT INTO `x_statuts_devis` VALUES /*!40101 SET character_set_client = utf8mb4 */;
(1,'En cours de création',1), CREATE TABLE `y_pages` (
(2,'En cours de validation DIR-CO',1), `rowid` int(11) NOT NULL AUTO_INCREMENT,
(3,'En cours de validation DV/DGC',1), `fk_parent` int(11) DEFAULT 0,
(4,'A traiter sur SAP',1), `link` varchar(75) DEFAULT NULL,
(6,'A vérifier par le RR',1), `libelle` varchar(45) DEFAULT NULL,
(7,'A envoyer au client',1), `titre` varchar(75) DEFAULT NULL,
(10,'Envoyé au client',0), `tooltip` varchar(45) DEFAULT NULL,
(20,'Archivé',1); `description` varchar(200) DEFAULT NULL,
/*!40000 ALTER TABLE `x_statuts_devis` ENABLE KEYS */; `keywords` varchar(200) DEFAULT NULL,
UNLOCK TABLES; `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` -- Table structure for table `z_history`
@@ -1175,7 +1144,7 @@ UNLOCK TABLES;
DROP TABLE IF EXISTS `z_history`; DROP TABLE IF EXISTS `z_history`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */; /*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `z_history` ( CREATE TABLE `z_history` (
`fk_user` int(11) NOT NULL, `fk_user` int(11) NOT NULL,
`libelle` varchar(20) NOT NULL DEFAULT 'tiers', `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'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
/*!40101 SET character_set_client = @saved_cs_client */; /*!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` -- Table structure for table `z_logs`
-- --
DROP TABLE IF EXISTS `z_logs`; DROP TABLE IF EXISTS `z_logs`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */; /*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `z_logs` ( CREATE TABLE `z_logs` (
`date` datetime NOT NULL, `date` datetime NOT NULL,
`ip` varchar(15) NOT NULL, `ip` varchar(15) NOT NULL,
@@ -1220,7 +1180,7 @@ CREATE TABLE `z_logs` (
DROP TABLE IF EXISTS `z_sessions`; DROP TABLE IF EXISTS `z_sessions`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */; /*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `z_sessions` ( CREATE TABLE `z_sessions` (
`sid` text NOT NULL, `sid` text NOT NULL,
`fk_user` int(11) NOT NULL, `fk_user` int(11) NOT NULL,
@@ -1238,7 +1198,7 @@ CREATE TABLE `z_sessions` (
DROP TABLE IF EXISTS `z_stats`; DROP TABLE IF EXISTS `z_stats`;
/*!40101 SET @saved_cs_client = @@character_set_client */; /*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */; /*!40101 SET character_set_client = utf8mb4 */;
CREATE TABLE `z_stats` ( CREATE TABLE `z_stats` (
`rowid` int(11) NOT NULL AUTO_INCREMENT, `rowid` int(11) NOT NULL AUTO_INCREMENT,
`libelle` varchar(75) DEFAULT NULL, `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'; ) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
/*!40101 SET character_set_client = @saved_cs_client */; /*!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 */; /*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; /*!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_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!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 OR c.contact_fonction LIKE :search
ORDER BY c.libelle'; ORDER BY c.libelle';
$stmt = $db->prepare($sql);
$searchParam = '%' . $search . '%'; $searchParam = '%' . $search . '%';
$stmt->bindParam(':search', $searchParam, PDO::PARAM_STR); $aModel["clients"] = $db->fetchAll($sql, [':search' => $searchParam]);
$stmt->execute();
$aModel["clients"] = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (Exception $e) { } catch (Exception $e) {
error_log("Erreur recherche clients : " . $e->getMessage()); error_log("Erreur recherche clients : " . $e->getMessage());
$aModel["clients"] = []; $aModel["clients"] = [];

View File

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

View File

@@ -1,8 +1,7 @@
<?php <?php
global $Route; global $Route;
function cleanData(&$str) function cleanData(&$str) {
{
// Fonction de nettoyage des données pour l'export Excel // Fonction de nettoyage des données pour l'export Excel
if ($str == 't') $str = 'TRUE'; if ($str == 't') $str = 'TRUE';
if ($str == 'f') $str = 'FALSE'; if ($str == 'f') $str = 'FALSE';
@@ -13,8 +12,7 @@ function cleanData(&$str)
$str = mb_convert_encoding($str, 'UTF-16LE', 'UTF-8'); $str = mb_convert_encoding($str, 'UTF-16LE', 'UTF-8');
} }
function filterData(&$str) function filterData(&$str) {
{
$str = preg_replace("/\t/", "\\t", $str); $str = preg_replace("/\t/", "\\t", $str);
$str = preg_replace("/\r?\n/", "\\n", $str); $str = preg_replace("/\r?\n/", "\\n", $str);
if ($str == 't') $str = 'TRUE'; if ($str == 't') $str = 'TRUE';
@@ -57,12 +55,18 @@ switch ($Route->_action) {
case "export_sap_devis": case "export_sap_devis":
$cid = nettoie_input($Route->_param1); $cid = nettoie_input($Route->_param1);
$cidSafe = intval($cid); $cidSafe = intval($cid);
$sql = 'SELECT d.* FROM devis d WHERE d.rowid=' . $cidSafe . ';';
eLog("Export Excel SAP Devis : " . $sql); try {
$dev = getinfos($sql, "gen"); $db = Database::getInstance();
$devis = $dev[0]; $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"; $fileName = "devis_" . $cid . "_" . date('Y_m_d_hi') . ".xls";
@@ -71,48 +75,54 @@ switch ($Route->_action) {
$excelData = implode("\t", array_values($fields)) . "\n"; $excelData = implode("\t", array_values($fields)) . "\n";
$fkClientSafe = intval($devis["fk_client"]); $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 ($fkClientSafe == 0) {
if (count($cli) == 0) { // Nouveau client : données depuis le devis
// 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 = :devis_id';
$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 . ';'; $client = $db->fetchOne($sql, [':devis_id' => $cidSafe]);
$cli = getinfos($sql, "gen");
$client = $cli[0];
array_walk($client, 'filterData'); array_walk($client, 'filterData');
$excelData .= implode("\t", array_values($client)) . "\n"; $excelData .= implode("\t", array_values($client)) . "\n";
// une ligne vierge de séparation
$excelData .= "\n"; $excelData .= "\n";
// Les données du contact à prendre aussi dans le devis // 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=' . $cidSafe . ';'; $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';
$cont = getinfos($sql, "gen"); $contact = $db->fetchOne($sql, [':devis_id' => $cidSafe]);
$contact = $cont[0];
$fields = array("Contact Nom", "Prenom", "Fonction", "Fixe", "Mobile", "Email"); $fields = array("Contact Nom", "Prenom", "Fonction", "Fixe", "Mobile", "Email");
$excelData .= implode("\t", array_values($fields)) . "\n"; $excelData .= implode("\t", array_values($fields)) . "\n";
array_walk($contact, 'filterData'); array_walk($contact, 'filterData');
$excelData .= implode("\t", array_values($contact)) . "\n"; $excelData .= implode("\t", array_values($contact)) . "\n";
} else { } 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'); array_walk($client, 'filterData');
$excelData .= implode("\t", array_values($client)) . "\n"; $excelData .= implode("\t", array_values($client)) . "\n";
// une ligne vierge de séparation
$excelData .= "\n"; $excelData .= "\n";
// Les données du contact // Contact lié au devis via devis.fk_contact et clients_contacts
$sql = 'SELECT c.contact_nom, c.contact_prenom, c.contact_fonction, c.telephone, c.mobile, c.email FROM clients c WHERE c.rowid=' . $fkClientSafe . ';'; $fkContactSafe = intval($devis["fk_contact"]);
$cont = getinfos($sql, "gen"); if ($fkContactSafe > 0) {
$contact = $cont[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"); $fields = array("Contact Nom", "Prenom", "Fonction", "Fixe", "Mobile", "Email");
$excelData .= implode("\t", array_values($fields)) . "\n"; $excelData .= implode("\t", array_values($fields)) . "\n";
array_walk($contact, 'filterData'); array_walk($contact, 'filterData');
$excelData .= implode("\t", array_values($contact)) . "\n"; $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 // une ligne vierge de séparation
$excelData .= "\n"; $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 = '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.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 .= '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 . ';'; $sql .= 'FROM devis d LEFT JOIN marches m ON d.fk_marche=m.rowid WHERE d.rowid = :devis_id';
$dev = getinfos($sql, "gen"); $devisData = $db->fetchOne($sql, [':devis_id' => $cidSafe]);
$devis = $dev[0]; $chkSpeciaux = $devisData["speciaux"];
$chkSpeciaux = $devis["speciaux"];
$fields = array("Devis", "Opportunite", "Date Demande", "Date remise client", "Marche", "Num Marche", "Nom Marche", "Avec photos", "Commentaire RR", "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"; $excelData .= implode("\t", array_values($fields)) . "\n";
array_walk($devisData, 'filterData');
array_walk($devis, 'filterData'); $excelData .= implode("\t", array_values($devisData)) . "\n";
$excelData .= implode("\t", array_values($devis)) . "\n";
// une ligne vierge de séparation
$excelData .= "\n"; $excelData .= "\n";
// on affiche les totaux du devis // 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 . ';'; $sql = 'SELECT d.montant_total_ht, d.montant_total_ht_remise, d.marge_totale FROM devis d WHERE d.rowid = :devis_id';
$dev = getinfos($sql, "gen"); $totaux = $db->fetchOne($sql, [':devis_id' => $cidSafe]);
$totaux = $dev[0];
$fields = array("Total HT", "Total HT Remise", "Marge Totale"); $fields = array("Total HT", "Total HT Remise", "Marge Totale");
$excelData .= implode("\t", array_values($fields)) . "\n"; $excelData .= implode("\t", array_values($fields)) . "\n";
array_walk($totaux, 'filterData'); array_walk($totaux, 'filterData');
$excelData .= implode("\t", array_values($totaux)) . "\n"; $excelData .= implode("\t", array_values($totaux)) . "\n";
// une ligne vierge de séparation
$excelData .= "\n"; $excelData .= "\n";
// on affiche les produits // 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 p ON dp.fk_produit=p.rowid ';
$sql .= 'LEFT JOIN produits_familles pf ON p.groupe=pf.groupe '; $sql .= 'LEFT JOIN produits_familles pf ON p.groupe=pf.groupe ';
$sql .= 'LEFT JOIN x_familles xf ON pf.fk_famille=xf.rowid '; $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;'; $sql .= 'WHERE dp.fk_devis = :devis_id ORDER BY dp.ordre, xf.ordre, p.libelle';
$data = getinfos($sql, "gen"); $data = $db->fetchAll($sql, [':devis_id' => $cidSafe]);
$fields = array("Code", "Designation", "Prix Vente", "Quantite", "Remise", "PU vente avec remise", "Total HT", "Marge", "Commentaire"); $fields = array("Code", "Designation", "Prix Vente", "Quantite", "Remise", "PU vente avec remise", "Total HT", "Marge", "Commentaire");
$excelData .= implode("\t", array_values($fields)) . "\n"; $excelData .= implode("\t", array_values($fields)) . "\n";
@@ -167,22 +171,19 @@ switch ($Route->_action) {
} }
if ($chkSpeciaux == "Oui") { if ($chkSpeciaux == "Oui") {
// une ligne vierge de séparation
$excelData .= "\n"; $excelData .= "\n";
$excelData .= "----" . "\n"; $excelData .= "----" . "\n";
$excelData .= "PRODUITS SPECIAUX" . "\n"; $excelData .= "PRODUITS SPECIAUX" . "\n";
$excelData .= "----" . "\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 = '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 . ';'; $sql .= 'FROM devis_speciaux ds WHERE ds.fk_devis = :devis_id';
$spec = getinfos($sql, "gen"); $speciaux = $db->fetchOne($sql, [':devis_id' => $cidSafe]);
$speciaux = $spec[0];
$fields = array("Livraisons multiples", "Nbre livraisons", "Date 1ere livraison"); $fields = array("Livraisons multiples", "Nbre livraisons", "Date 1ere livraison");
$excelData .= implode("\t", array_values($fields)) . "\n"; $excelData .= implode("\t", array_values($fields)) . "\n";
array_walk($speciaux, 'filterData'); array_walk($speciaux, 'filterData');
$excelData .= implode("\t", array_values($speciaux)) . "\n"; $excelData .= implode("\t", array_values($speciaux)) . "\n";
$excelData .= "\n"; $excelData .= "\n";
$fields = array("#", "Code", "Designation", "Quantite", "Surcout", "Echantillon", "Date echantillon", "Concurrent", "Description"); $fields = array("#", "Code", "Designation", "Quantite", "Surcout", "Echantillon", "Date echantillon", "Concurrent", "Description");
@@ -191,29 +192,32 @@ switch ($Route->_action) {
for ($i = 1; $i <= 5; $i++) { 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 = '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 .= '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 . ';'; $sql .= 'FROM devis_speciaux ds WHERE ds.fk_devis = :devis_id';
eLog($sql, "sql");
$spec = getinfos($sql, "gen");
$speciaux = $spec[0];
if ($speciaux["fk_produit_" . $i] > 0) { $produitSpecial = $db->fetchOne($sql, [':devis_id' => $cidSafe]);
array_walk($speciaux, 'filterData');
$excelData .= implode("\t", array_values($speciaux)) . "\n"; 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 .= "----" . "\n"; $excelData .= "----" . "\n";
$excelData .= "FIN DU DEVIS" . "\n"; $excelData .= "FIN DU DEVIS" . "\n";
$excelData .= "----" . "\n"; $excelData .= "----" . "\n";
$excelData .= "\n"; $excelData .= "\n";
header('Content-Type: application/vnd.ms-excel; charset=utf-16le'); header('Content-Type: application/vnd.ms-excel; charset=utf-16le');
header("Content-type: application/x-msexcel; charset=utf-16le"); header("Content-type: application/x-msexcel; charset=utf-16le");
header('Content-Disposition: attachment; filename="' . $fileName . '"'); header('Content-Disposition: attachment; filename="' . $fileName . '"');
header('Cache-Control: max-age=0'); header('Cache-Control: max-age=0');
echo $excelData; echo $excelData;
exit(); } 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;'; $sql = 'SELECT m.* FROM medias m WHERE m.support="devis_pdf_sap" ORDER BY m.support_rowid;';
$aModel["medias"] = getinfos($sql, "gen"); $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 .= 'FROM devis d ';
$sql .= 'LEFT JOIN clients c on d.fk_client = c.rowid '; $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 users u ON d.fk_user = u.rowid ';
$sql .= 'LEFT JOIN marches m ON d.fk_marche = m.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 '; $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 .= 'FROM devis d ';
$sql .= 'LEFT JOIN clients c on d.fk_client = c.rowid '; $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 users u ON d.fk_user = u.rowid ';
$sql .= 'LEFT JOIN marches m ON d.fk_marche = m.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 '; $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; 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 let chkCreateClient = false
// On charge les produits du marché du devis en cours dans un tableau pour ne pas avoir à les recharger à chaque fois // On charge les produits du marché du devis en cours dans un tableau pour ne pas avoir à les recharger à chaque fois
let dataProduitsMarche = [] 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 //! 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 oldChkClientsSecteur = 2
let clients = [] let clients = []
@@ -35,14 +37,107 @@ let devisTotalRemHT = 0
let devisTotalMarge = 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 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 seuilMargeRR = 40 // le seuil de marge du RR sur ce devis, par défaut à 40 % (MAJ 05/11/2025)
let seuilMargeDV = 20 // le seuil de marge du DV sur ce devis, par défaut à 20 % let seuilMargeDV = 30 // le seuil de marge du DV sur ce devis, par défaut à 30 % (MAJ 05/11/2025)
let intervalRefresh let intervalRefresh
let nbCommentChat = 0 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) 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) => { window.addEventListener('DOMContentLoaded', (event) => {
console.log('#') console.log('#')
@@ -407,18 +502,24 @@ window.addEventListener('DOMContentLoaded', (event) => {
document.getElementById('inp_adresse3').value = data.adresse3 document.getElementById('inp_adresse3').value = data.adresse3
document.getElementById('inp_cp').value = data.cp document.getElementById('inp_cp').value = data.cp
document.getElementById('inp_ville').value = data.ville 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 document.getElementById('selTypeEtab').value = data.type_client
elBtnCreateClient.innerHTML = 'Créer un nouveau client' elBtnCreateClient.innerHTML = 'Créer un nouveau client'
if (elBtnCreateClient.classList.contains('btn-info')) { if (elBtnCreateClient.classList.contains('btn-info')) {
elBtnCreateClient.classList.remove('btn-info') elBtnCreateClient.classList.remove('btn-info')
elBtnCreateClient.classList.add('btn-primary') 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') { if (data.chk_devis_photos == '1') {
@@ -908,6 +1009,86 @@ window.addEventListener('DOMContentLoaded', (event) => {
if (ret.length == 1) { if (ret.length == 1) {
let line = ret[0] let line = ret[0]
chkPrixNets = line.chk_prix_nets == 1 ? true : false 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_latitudeRR').value = seuilMargeRR
document.getElementById('inp_latitudeDV').value = seuilMargeDV 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 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 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) { $('a[data-toggle="tab"]').on('show.bs.tab', function (e) {
if (idDevis == 0) { if (idDevis == 0) {
if ($(this).attr('href') == '#tabproduits' || $(this).attr('href') == '#tabdevis') { if ($(this).attr('href') == '#tabproduits' || $(this).attr('href') == '#tabdevis') {
@@ -1346,33 +1575,63 @@ window.addEventListener('DOMContentLoaded', (event) => {
} }
let clickSaveCreateClient = function () { let clickSaveCreateClient = function () {
// on regarde si c'est une création de devis ou une modification showLoading()
// on enregistre le fait que ça soit un nouveau client
// on met à jour les champs du devis avec les infos du nouveau client const clientData = {
// et quand on enregistre le devis on enregistre le nouveau client dans le devis libelle: document.getElementById('inp_create_libelle').value,
document.getElementById('inp_fk_client').value = '0' type_client: document.getElementById('inp_create_type_client').value,
document.getElementById('inp_lib_client').value = document.getElementById('inp_create_libelle').value adresse1: document.getElementById('inp_create_adresse1').value,
document.getElementById('selTypeEtab').value = document.getElementById('inp_create_type_client').value adresse2: document.getElementById('inp_create_adresse2').value,
document.getElementById('inp_type_client').value = document.getElementById('inp_create_type_client').value adresse3: document.getElementById('inp_create_adresse3').value,
document.getElementById('inp_adresse1').value = document.getElementById('inp_create_adresse1').value cp: document.getElementById('inp_create_cp').value,
document.getElementById('inp_adresse2').value = document.getElementById('inp_create_adresse2').value ville: document.getElementById('inp_create_ville').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 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')) 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.remove('btn-primary')
document.getElementById('btnCreateClient').classList.add('btn-info') 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 () { let clickSpeciaux = function () {
@@ -1510,45 +1769,13 @@ window.addEventListener('DOMContentLoaded', (event) => {
return false return false
} }
if (document.getElementById('inp_contact_nom').value == '') { if (document.getElementById('sel_contact').value == '0') {
showNotification( showNotification(
'Erreur', 'Erreur',
'Enregistrement impossible : vous devez renseigner le nom et prénom du contact', 'Enregistrement impossible : vous devez sélectionner un contact',
'error' 'error'
) )
document.getElementById('inp_contact_nom').focus() document.getElementById('sel_contact').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()
return false return false
} }
@@ -1817,10 +2044,10 @@ window.addEventListener('DOMContentLoaded', (event) => {
showDevisPro(data) showDevisPro(data)
hideLoading() hideLoading()
showNotification('Succès', 'Enregistrement des ' + nbProduits + ' produits de ce devis effectué', 'success') showNotification('Succès', 'Enregistrement des ' + nbProduits + ' produits de ce devis effectué', 'success')
chkChange = 0
}) })
} }
}) })
chkChange = 0
return false return false
} }
@@ -2891,13 +3118,9 @@ window.addEventListener('DOMContentLoaded', (event) => {
document.getElementById('inp_adresse3').value = list[i]['adresse3'] document.getElementById('inp_adresse3').value = list[i]['adresse3']
document.getElementById('inp_cp').value = list[i]['cp'] document.getElementById('inp_cp').value = list[i]['cp']
document.getElementById('inp_ville').value = list[i]['ville'] 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'] 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 // on ferme la liste des suggestions
closeList() closeList()
}) })
@@ -3305,6 +3528,284 @@ window.addEventListener('DOMContentLoaded', (event) => {
hideModal(document.getElementById('modalCommentProd')) 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 //! 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() //! 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) { Array.from(elCelDevis).forEach(function (lnDevis) {
@@ -3382,6 +3883,148 @@ window.addEventListener('DOMContentLoaded', (event) => {
chkVariante.addEventListener('change', calculDevis) 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 () { elBtnSideBarDevis.addEventListener('click', function () {
if (elVerticalBar.style.width == '10px') { if (elVerticalBar.style.width == '10px') {
elVerticalBar.style.width = '1100px' // Largeur de la barre lorsqu'elle est ouverte elVerticalBar.style.width = '1100px' // Largeur de la barre lorsqu'elle est ouverte

View File

@@ -11,6 +11,100 @@ let oldIdLnEnCours;
let oldIdLnArchives; let oldIdLnArchives;
let nbCommentChat = 0; let nbCommentChat = 0;
let selectedXmlDevis = new Set(); 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) => { window.addEventListener('DOMContentLoaded', (event) => {
console.log('#'); console.log('#');
@@ -786,6 +880,201 @@ window.addEventListener('DOMContentLoaded', (event) => {
return false; 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 // Add new functions
function updateExportButton() { function updateExportButton() {
if (selectedXmlDevis.size > 0) { if (selectedXmlDevis.size > 0) {

View File

@@ -16,6 +16,16 @@ ob_start();
<div id="vb-buttons" class="mb-1"> <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-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> <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> </div>
<?php <?php
if ($aModel["last_devis"] > 0) { 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 '<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 '<table class="table table-responsive table-bordered table-fixed" id="tblDos' . $iDos . '">';
echo '<thead><tr>'; echo '<thead><tr>';
echo '<th class="header" scope="col" width="5%">#</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" scope="col" width="10%">Demande</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)) { if (($fkRole == 2 && $iDos == 2) || ($fkRole == 1 && $iDos == 1)) {
// C'est un DV et sur le dossier Encours de validation DV // C'est un DV et sur le dossier Encours de validation DV
// Ou le Dir-CO sur le dossier de validation DIR-CO // 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 // 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 clickable" scope="col" width="10%" data-sortable="true" data-sort-type="text" data-column-index="' . $colIndex . '">Opport.</th>';
echo '<th class="header" scope="col" width="5%">CP</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" scope="col" width="10%">Ville</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" scope="col" width="15%">Client</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" scope="col" width="10%">Marché</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" scope="col" width="10%">Total HT</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" scope="col" width="10%">Marge Totale</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 '<th class="header" scope="col" width="12%"></th>';
echo '</tr></thead>'; echo '</tr></thead>';
echo '<tbody id="tblBodyDos' . $iDos . '">'; 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 '<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 '<table class="table table-responsive table-bordered table-fixed" id="tblDosArch' . $iDos . '">';
echo '<thead><tr>'; echo '<thead><tr>';
echo '<th class="header" scope="col" width="5%">#</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" scope="col" width="10%">Demande</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" scope="col" width="10%">Opport.</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" scope="col" width="10%">Ville</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" scope="col" width="20%">Client</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" scope="col" width="10%">Marché</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" scope="col" width="10%">Total HT</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" scope="col" width="10%">Marge Totale</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 '<th class="header" scope="col" width="10%"></th>';
echo '</tr></thead>'; echo '</tr></thead>';
echo '<tbody id="tblBodyDosArch' . $iDos . '">'; echo '<tbody id="tblBodyDosArch' . $iDos . '">';
@@ -376,34 +388,34 @@ ob_start();
</div> </div>
</div> </div>
<div class="form-group"> <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"> <div class="col-md-4">
<input type="text" class="form-control" id="inp_contact_nom" name="contact_nom" placeholder="Nom" required="required"/> <select class="form-control" id="sel_contact" name="fk_contact" required="required">
<p class="help-block">Nom du contact</p> <option value="0">- Sélectionner un contact -</option>
</select>
</div> </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>
</div>
<div class="form-group">
<label class="control-label col-md-2" for="inp_contact_fonction">Fonction du contact :</label>
<div class="col-md-3"> <div class="col-md-3">
<input type="text" class="form-control" id="inp_contact_fonction" name="contact_fonction" required="required"/> <button type="button" class="btn btn-primary" id="btnGererContacts" disabled>Gérer les contacts</button>
</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> </div>
<div id="divContactInfos" style="display: none;">
<div class="form-group"> <div class="form-group">
<label class=" control-label col-md-2" for="inp_telephone">Tél :</label> <label class="control-label col-md-2"></label>
<div class="col-md-2"> <div class="col-md-8">
<input type="text" class="form-control" id="inp_telephone" name="telephone" size="10" maxlength="18" placeholder="Fixe"/> <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>
</div>
</div>
</div> </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> </div>
<div class="form-group"> <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"/> <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 class="input-group-addon">&euro;</div>
</div> </div>
</td>
<td>
<?php <?php
if ($Conf->_devIp) { if ($Conf->_devIp) {
echo '<div class="form-group">'; echo '<input type="hidden" id="inpCoutTotalAchat" name="inpCoutTotalAchat"/>';
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>';
} }
?> ?>
</td> </td>
<td></td>
<td></td>
</tr> </tr>
<tr> <tr>
<td> <td>
@@ -952,6 +953,117 @@ ob_start();
</div> </div>
</div> </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 <?php
$modal = ob_get_contents(); $modal = ob_get_contents();
ob_clean(); ob_clean();

View File

@@ -5,7 +5,37 @@ $metacss = '<link href="/pub/res/css/schat.css" rel="stylesheet" type="text/css"
$barre = ""; $barre = "";
ob_start(); 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"> <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" 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> <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 '<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 '<table class="table table-responsive table-bordered table-fixed" id="tblDos' . $iDos . '">';
echo '<thead><tr>'; echo '<thead><tr>';
echo '<th class="header text-center" scope="col" width="5%">#</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" scope="col" width="10%">Date Demande</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" scope="col" width="10%">Date Remise</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" scope="col" width="10%">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="3">Responsable Régional</th>';
echo '<th class="header text-center" scope="col" width="10%">Code Etabliss.</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" scope="col" width="15%">Etablissement</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" scope="col" width="7%">CP</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" scope="col" width="10%">Ville</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" scope="col" width="10%">Marché</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" scope="col" width="10%">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">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="10%" data-sortable="true" data-sort-type="number" data-column-index="10">Marge totale</th>';
$colIndexActions = 11;
if ($dossier["rowid"] == 7) { if ($dossier["rowid"] == 7) {
// Si le dossier est "A envoyer au client", on affiche la colonne "Email" // 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>'; 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 '<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>'; echo '</tr></thead>';
@@ -167,39 +199,118 @@ ob_start();
<div role="tabpanel" class="tab-pane" id="tabArchives"> <div role="tabpanel" class="tab-pane" id="tabArchives">
<div class="row"> <div class="row">
<div class="col-md-9"> <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 <?php
$i = 0; $i = 0;
foreach ($aModel["dossiers"] as $dossier) { $nbPerLine = 25;
if ($i % 15 == 0 && $i > 0) { $totalDepts = count($aModel["dossiers"]);
echo '<div class="spacer"></div>';
} for ($line = 0; $line < 4; $line++) {
$active = ($i == 0) ? "active" : ""; 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"]; $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>'; echo '<li role="presentation" class="dept-tab"><a href="#dosArch' . $j . '" aria-controls="dosArch' . $j . '" role="tab" data-toggle="tab">' . $ceDossier . '</a></li>';
$i++; }
echo '</ul>';
} }
?> ?>
</ul>
<div class="tab-content"> <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 <?php
$iDos = 0; $iDos = 0;
foreach ($aModel["dossiers"] as $dossier) { 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 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 '<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 '<table class="table table-responsive table-bordered table-fixed" id="tblDosArch' . $iDos . '">';
echo '<thead><tr>'; echo '<thead><tr>';
echo '<th class="header text-center" scope="col" width="5%">#</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" scope="col" width="10%">Date Demande</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" scope="col" width="10%">Resp. Régional</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" scope="col" width="7%">Code Etabliss.</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" scope="col" width="20%">Etablissement</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" scope="col" width="5%">CP</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" scope="col" width="13%">Ville</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" scope="col" width="10%">Marché</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" scope="col" width="10%">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="8">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="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 '<th class="header text-center" scope="col" width="10%">Actions</th>';
echo '</tr></thead>'; echo '</tr></thead>';
echo '<tbody id="tblBodyDosArch' . $iDos . '">'; echo '<tbody id="tblBodyDosArch' . $iDos . '">';