Compare commits
4 Commits
v2.0.2
...
feature/co
| Author | SHA1 | Date | |
|---|---|---|---|
| e96ad7a244 | |||
|
|
f6c5e96534 | ||
|
|
a4d1c22a93 | ||
| c46359deea |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -41,3 +41,4 @@ sessions/
|
||||
|
||||
# Fichiers système
|
||||
Thumbs.db*.swp
|
||||
.aider*
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
require_once dirname(__FILE__) . '/Database.php';
|
||||
|
||||
class Conf
|
||||
{
|
||||
class Conf {
|
||||
const admin = 1;
|
||||
const intra = 1;
|
||||
const erp = 1;
|
||||
@@ -11,7 +10,7 @@ class Conf
|
||||
|
||||
public $_appname = "cleo";
|
||||
public $_appscript = "login";
|
||||
public $_appversion = "2.0.2";
|
||||
public $_appversion = "2.0.4";
|
||||
public $_appenv;
|
||||
public $_apptitle = "CLEO - Gestion de devis";
|
||||
|
||||
@@ -48,8 +47,7 @@ class Conf
|
||||
public $_entite = '';
|
||||
public $_new_version = false;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
public function __construct() {
|
||||
$this->loadEnvironment();
|
||||
$this->loadConfiguration();
|
||||
$this->setupDebug();
|
||||
@@ -115,7 +113,6 @@ class Conf
|
||||
$this->_brandemail = $entite["email"] ?? "";
|
||||
$this->_brandlogo = $entite["appname"] ?? "cleo";
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Erreur de configuration: " . $e->getMessage());
|
||||
$this->setDefaultConfiguration();
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
245
controllers/cjxcontacts.php
Normal 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();
|
||||
@@ -54,7 +54,7 @@ switch ($Route->_action) {
|
||||
|
||||
//! 4. On met à jour la date_demande, date_remise, num_opportunite et fk_statut_devis du nouveau devis
|
||||
$newRowidSafe = intval($newRowid);
|
||||
$sql = 'UPDATE devis SET date_demande = "' . date("Y-m-d H:i:s") . '", date_remise = "", num_opportunite = "", fk_statut_devis = 1 WHERE rowid = ' . $newRowidSafe . ';';
|
||||
$sql = 'UPDATE devis SET date_demande = "' . date("Y-m-d H:i:s") . '", date_remise = NULL, num_opportunite = "", fk_statut_devis = 1 WHERE rowid = ' . $newRowidSafe . ';';
|
||||
eLog($sql);
|
||||
qSQL($sql, "gen");
|
||||
|
||||
@@ -124,14 +124,16 @@ switch ($Route->_action) {
|
||||
$sql = 'UPDATE devis SET chk_maj = 0 WHERE rowid = ' . $cidSafe . ';';
|
||||
qSQL($sql, "gen");
|
||||
}
|
||||
$sql = 'SELECT d.rowid, d.fk_user, d.fk_client, d.fk_marche, m.libelle AS lib_marche, d.fk_statut_devis, d.dossier, d.num_opportunite, d.montant_total_ht, ';
|
||||
$sql = 'SELECT d.rowid, d.fk_user, d.fk_client, d.fk_contact, d.fk_marche, m.libelle AS lib_marche, d.fk_statut_devis, d.dossier, d.num_opportunite, d.montant_total_ht, ';
|
||||
$sql .= 'd.date_demande, d.date_remise, d.montant_total_ht_remise, d.marge_totale, d.commentaire, d.comment_devis, d.chk_clients_secteur, d.chk_speciaux, ';
|
||||
$sql .= 'd.lib_new_client, d.type_new_client, d.adresse1_new_client, d.adresse2_new_client, d.adresse3_new_client, d.cp_new_client, d.ville_new_client,';
|
||||
$sql .= 'd.contact_new_nom, d.contact_new_prenom, d.contact_new_fonction, d.new_telephone, d.new_mobile, d.new_email, ';
|
||||
$sql .= 'd.comment_geste_comm, d.comment_validat, d.chk_validat, d.fk_user_validat, d.date_validat, ';
|
||||
$sql .= 'xs.libelle as lib_statut_devis, c.code, c.libelle, c.adresse1, c.adresse2, c.adresse3, c.cp, c.ville, c.type_client, ';
|
||||
$sql .= 'c.contact_nom, c.contact_prenom, c.contact_fonction, c.telephone, c.mobile, c.email, d.chk_devis_photos ';
|
||||
$sql .= 'FROM devis d LEFT JOIN clients c ON d.fk_client = c.rowid LEFT JOIN x_statuts_devis xs ON d.fk_statut_devis = xs.rowid ';
|
||||
$sql .= 'cc.rowid as contact_rowid, cc.nom as contact_nom, cc.prenom as contact_prenom, cc.fonction as contact_fonction, cc.telephone, cc.mobile, cc.email, d.chk_devis_photos ';
|
||||
$sql .= 'FROM devis d LEFT JOIN clients c ON d.fk_client = c.rowid ';
|
||||
$sql .= 'LEFT JOIN clients_contacts cc ON d.fk_contact = cc.rowid ';
|
||||
$sql .= 'LEFT JOIN x_statuts_devis xs ON d.fk_statut_devis = xs.rowid ';
|
||||
$sql .= 'LEFT JOIN marches m ON d.fk_marche = m.rowid ';
|
||||
$sql .= 'WHERE d.rowid = ' . $cidSafe . ';';
|
||||
echo getinfos($sql, "gen", "json");
|
||||
@@ -162,7 +164,7 @@ switch ($Route->_action) {
|
||||
if (isset($data->secteur)) {
|
||||
$chkSecteur = nettoie_input($data->secteur);
|
||||
$fkUser = nettoie_input($data->user);
|
||||
$sql = 'SELECT rowid, libelle, CONCAT(libelle, ", ", adresse1, ", ", cp, " ", ville) AS rech, adresse1, adresse2, adresse3, cp, ville, contact_nom, contact_prenom, contact_fonction, telephone, mobile, email, type_client FROM clients WHERE active=1 ';
|
||||
$sql = 'SELECT rowid, code, libelle, CONCAT(libelle, ", ", adresse1, ", ", cp, " ", ville) AS rech, adresse1, adresse2, adresse3, cp, ville, contact_nom, contact_prenom, contact_fonction, telephone, mobile, email, type_client FROM clients WHERE active=1 ';
|
||||
if ($chkSecteur == "1") {
|
||||
//! on ne prend que les clients du secteur de l'utilisateur
|
||||
$fkUserSafe = intval($fkUser);
|
||||
@@ -197,6 +199,32 @@ switch ($Route->_action) {
|
||||
}
|
||||
break;
|
||||
|
||||
case "load_produits_mercurial":
|
||||
//! Charge les produits du marché hybride pour l'onglet Mercurial
|
||||
$data = json_decode(file_get_contents("php://input"));
|
||||
if (isset($data->fk_marche)) {
|
||||
$fkMarche = nettoie_input($data->fk_marche);
|
||||
$fkMarcheSafe = intval($fkMarche);
|
||||
|
||||
// Vérifier que le marché est bien hybride
|
||||
$sql = 'SELECT chk_marche_hybride FROM marches WHERE rowid = ' . $fkMarcheSafe . ';';
|
||||
$retSql = getinfos($sql, "gen");
|
||||
|
||||
if (count($retSql) == 1 && $retSql[0]["chk_marche_hybride"] == 1) {
|
||||
// Le marché est hybride, on charge tous les produits de ce marché (hors 999)
|
||||
$sql = 'SELECT p.*, CONCAT(p.code, " - ", p.libelle) AS rech, pf.fk_famille, xf.libelle AS lib_famille ';
|
||||
$sql .= 'FROM produits p LEFT JOIN produits_familles pf ON p.groupe=pf.groupe LEFT JOIN x_familles xf on pf.fk_famille = xf.rowid ';
|
||||
$sql .= 'WHERE p.fk_marche = ' . $fkMarcheSafe . ' AND p.fk_marche != 999 AND p.active=1 ORDER BY xf.ordre, pf.ordre;';
|
||||
echo getinfos($sql, "gen", "json");
|
||||
} else {
|
||||
// Le marché n'est pas hybride, on retourne un tableau vide
|
||||
echo json_encode(array());
|
||||
}
|
||||
} else {
|
||||
echo json_encode(array());
|
||||
}
|
||||
break;
|
||||
|
||||
case "load_devis_marche_produits":
|
||||
//! Charge les produits enregistrés pour un marché
|
||||
$data = json_decode(file_get_contents("php://input"));
|
||||
@@ -234,7 +262,7 @@ switch ($Route->_action) {
|
||||
|
||||
$sql = 'SELECT p.*, CONCAT(p.code, " - ", p.libelle) AS rech, pf.fk_famille, xf.libelle AS lib_famille, "0" AS chk_prix_net ';
|
||||
$sql .= 'FROM produits p LEFT JOIN produits_familles pf ON p.groupe=pf.groupe LEFT JOIN x_familles xf on pf.fk_famille = xf.rowid ';
|
||||
$sql .= 'WHERE p.fk_marche = ' . $cidSafe . ' AND p.active=1 ORDER BY xf.ordre, pf.ordre;';
|
||||
$sql .= 'WHERE p.fk_marche = ' . intval($cid) . ' AND p.active=1 ORDER BY xf.ordre, pf.ordre;';
|
||||
$upls = getinfos($sql, "gen");
|
||||
|
||||
if ($cid != "999") {
|
||||
@@ -390,31 +418,13 @@ switch ($Route->_action) {
|
||||
$commentaire = nettoie_input($_POST["commentaire"]);
|
||||
$newCommentaire = 0;
|
||||
|
||||
$contact_nom = nettoie_input($_POST["contact_nom"]);
|
||||
$contact_prenom = nettoie_input($_POST["contact_prenom"]);
|
||||
$contact_fonction = nettoie_input($_POST["contact_fonction"]);
|
||||
$email = nettoie_input($_POST["email"]);
|
||||
$telephone = formattel(nettoie_input($_POST["telephone"]));
|
||||
$mobile = formattel(nettoie_input($_POST["mobile"]));
|
||||
// Récupération du contact sélectionné
|
||||
$fk_contact = isset($_POST["fk_contact"]) ? intval($_POST["fk_contact"]) : 0;
|
||||
if ($fk_contact == 0) $fk_contact = NULL;
|
||||
|
||||
$set = 'fk_client=' . $fk_client . ', num_opportunite="' . $num_opportunite . '", date_demande="' . $date_demande . '", date_remise="' . $date_remise . '", ';
|
||||
$set = 'fk_client=' . $fk_client . ', fk_contact=' . ($fk_contact === NULL ? 'NULL' : $fk_contact) . ', num_opportunite="' . $num_opportunite . '", date_demande="' . $date_demande . '", date_remise="' . $date_remise . '", ';
|
||||
$set .= 'fk_user=' . $fk_user . ', fk_marche=' . $fk_marche . ', commentaire="' . $commentaire . '", chk_devis_photos=' . $chk_devis_photos . ', ';
|
||||
|
||||
if ($fk_client == 0) {
|
||||
//! C'est un nouveau client : on enregistre ces infos et celle du contact dans le devis et non au niveau de la table clients
|
||||
$libNewClient = nettoie_input($_POST["lib_client"]);
|
||||
$typNewClient = nettoie_input($_POST["type_client"]);
|
||||
$adr1NewClient = nettoie_input($_POST["adresse1"]);
|
||||
$adr2NewClient = nettoie_input($_POST["adresse2"]);
|
||||
$adr3NewClient = nettoie_input($_POST["adresse3"]);
|
||||
$cpNewClient = nettoie_input($_POST["cp"]);
|
||||
// Si le CP a une longueur de 4, on rajoute un 0 devant
|
||||
if (strlen($cpNewClient) == 4) $cpNewClient = "0" . $cpNewClient;
|
||||
$villeNewClient = nettoie_input($_POST["ville"]);
|
||||
$set .= 'lib_new_client="' . $libNewClient . '", type_new_client="' . $typNewClient . '", adresse1_new_client="' . $adr1NewClient . '", adresse2_new_client="' . $adr2NewClient . '", adresse3_new_client="' . $adr3NewClient . '", cp_new_client="' . $cpNewClient . '", ville_new_client="' . $villeNewClient . '", ';
|
||||
$set .= 'contact_new_nom="' . $contact_nom . '", contact_new_prenom="' . $contact_prenom . '", contact_new_fonction="' . $contact_fonction . '", new_email="' . $email . '", new_telephone="' . $telephone . '", new_mobile="' . $mobile . '", ';
|
||||
}
|
||||
|
||||
if ($_POST["rowid"] == 0) {
|
||||
//! C'est un nouveau devis
|
||||
//! On le range dans un dossier
|
||||
@@ -465,14 +475,8 @@ switch ($Route->_action) {
|
||||
}
|
||||
eLog('Entete Devis Save : ' . $sql);
|
||||
|
||||
if ($fk_client != "0") {
|
||||
//! On sauvegarde aussi les infos complémentaires du client qui peuvent ête mises à jour
|
||||
$sql = 'UPDATE clients SET contact_nom="' . $contact_nom . '", contact_prenom="' . $contact_prenom . '", contact_fonction="' . $contact_fonction . '", ';
|
||||
$fkClientSafe = intval($fk_client);
|
||||
$sql .= 'email="' . $email . '", telephone="' . $telephone . '", mobile="' . $mobile . '" WHERE rowid=' . $fkClientSafe . ';';
|
||||
eLog('Entete Devis Save infos client : ' . $sql);
|
||||
qSQL($sql, "gen");
|
||||
}
|
||||
// NOTE: Les contacts sont maintenant gérés via la table clients_contacts
|
||||
// et non plus directement dans la table clients
|
||||
|
||||
// On inscrit l'enregistrement dans le journal si il y a eu un changement de commentaire ou bien si c'est une création avec commentaire
|
||||
if ($newCommentaire > 0) {
|
||||
@@ -651,11 +655,8 @@ switch ($Route->_action) {
|
||||
try {
|
||||
$db = Database::getInstance();
|
||||
$sql = 'SELECT rowid, code, libelle, prix_vente FROM produits WHERE active=1 AND (code LIKE :term OR libelle LIKE :term) ORDER BY code';
|
||||
$stmt = $db->prepare($sql);
|
||||
$termParam = '%' . $term . '%';
|
||||
$stmt->bindParam(':term', $termParam, PDO::PARAM_STR);
|
||||
$stmt->execute();
|
||||
$upls = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
$upls = $db->fetchAll($sql, [':term' => $termParam]);
|
||||
} catch (Exception $e) {
|
||||
error_log("Erreur recherche produits : " . $e->getMessage());
|
||||
$upls = [];
|
||||
@@ -1303,5 +1304,344 @@ switch ($Route->_action) {
|
||||
echo json_encode(array("success" => "true", "message" => "Devis refusé avec succès"));
|
||||
}
|
||||
break;
|
||||
|
||||
case "save_new_client":
|
||||
$data = json_decode(file_get_contents("php://input"));
|
||||
if (isset($data->libelle)) {
|
||||
try {
|
||||
$db = Database::getInstance();
|
||||
|
||||
$libelle = nettoie_input($data->libelle);
|
||||
$typeClient = nettoie_input($data->type_client);
|
||||
$adresse1 = nettoie_input($data->adresse1);
|
||||
$adresse2 = nettoie_input($data->adresse2);
|
||||
$adresse3 = nettoie_input($data->adresse3);
|
||||
$cp = nettoie_input($data->cp);
|
||||
if (strlen($cp) == 4) $cp = "0" . $cp;
|
||||
$ville = nettoie_input($data->ville);
|
||||
$fkUserSafe = intval($fk_user);
|
||||
|
||||
$sqlMaxCode = 'SELECT MAX(code) as max_code FROM clients';
|
||||
$resultCode = $db->fetchOne($sqlMaxCode);
|
||||
$newCode = ($resultCode && $resultCode['max_code']) ? intval($resultCode['max_code']) + 1 : 1;
|
||||
|
||||
$sql = 'INSERT INTO clients SET code = :code, libelle = :libelle, type_client = :type_client, adresse1 = :adresse1, adresse2 = :adresse2, adresse3 = :adresse3, ';
|
||||
$sql .= 'cp = :cp, ville = :ville, fk_user_creat = :fk_user_creat, date_creat = NOW(), active = 1';
|
||||
|
||||
$params = [
|
||||
':code' => $newCode,
|
||||
':libelle' => $libelle,
|
||||
':type_client' => $typeClient,
|
||||
':adresse1' => $adresse1,
|
||||
':adresse2' => $adresse2,
|
||||
':adresse3' => $adresse3,
|
||||
':cp' => $cp,
|
||||
':ville' => $ville,
|
||||
':fk_user_creat' => $fkUserSafe
|
||||
];
|
||||
|
||||
$db->query($sql, $params);
|
||||
$newClientId = $db->lastInsertId();
|
||||
|
||||
if ($newClientId > 0) {
|
||||
$sql = 'INSERT INTO clients_contacts SET fk_client = :fk_client, nom = :nom, prenom = :prenom, principal = 1, active = 1, date_creat = NOW()';
|
||||
|
||||
$params = [
|
||||
':fk_client' => $newCode,
|
||||
':nom' => 'À compléter',
|
||||
':prenom' => ''
|
||||
];
|
||||
|
||||
$db->query($sql, $params);
|
||||
|
||||
eLog("Nouveau client créé : ID=" . $newClientId . ", code=" . $newCode);
|
||||
echo json_encode(array("success" => true, "rowid" => $newClientId, "code" => $newCode, "message" => "Client créé avec succès"));
|
||||
} else {
|
||||
eLog("save_new_client ERREUR: newClientId = 0");
|
||||
echo json_encode(array("success" => false, "message" => "Erreur lors de la création du client - ID non récupéré"));
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$errorMsg = $e->getMessage();
|
||||
error_log("Erreur création client : " . $errorMsg);
|
||||
eLog("save_new_client EXCEPTION: " . $errorMsg);
|
||||
echo json_encode(array("success" => false, "message" => "Erreur : " . $errorMsg));
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "search_devis":
|
||||
eLog("=== search_devis case appelé ===");
|
||||
$rawData = file_get_contents("php://input");
|
||||
eLog("Raw data: " . $rawData);
|
||||
$data = json_decode($rawData);
|
||||
eLog("Data decoded: " . print_r($data, true));
|
||||
eLog("isset term: " . (isset($data->term) ? 'YES' : 'NO'));
|
||||
|
||||
if (isset($data->term)) {
|
||||
$term = nettoie_input($data->term);
|
||||
$context = nettoie_input($data->context);
|
||||
|
||||
if (strlen($term) < 3) {
|
||||
echo json_encode(array("success" => false, "message" => "Le terme de recherche doit contenir au moins 3 caractères"));
|
||||
break;
|
||||
}
|
||||
|
||||
$termSafe = '%' . $term . '%';
|
||||
|
||||
$whereParams = [];
|
||||
switch ($fk_role) {
|
||||
case 1:
|
||||
$whereRole = 'd.fk_user = :fkUser OR d.fk_statut_devis >= 2';
|
||||
$whereParams[':fkUser'] = $fk_user;
|
||||
break;
|
||||
case 2:
|
||||
try {
|
||||
$db = Database::getInstance();
|
||||
$sql = 'SELECT rowid FROM users WHERE fk_parent = :fkParent';
|
||||
$aRR = $db->fetchAll($sql, [':fkParent' => $fk_user]);
|
||||
|
||||
$rrIds = array_column($aRR, 'rowid');
|
||||
if (!empty($rrIds)) {
|
||||
$placeholders = [];
|
||||
foreach ($rrIds as $index => $id) {
|
||||
$placeholder = ':rr' . $index;
|
||||
$placeholders[] = $placeholder;
|
||||
$whereParams[$placeholder] = $id;
|
||||
}
|
||||
$whereRole = 'd.fk_user = :fkUser OR (d.fk_statut_devis >= 3 AND d.fk_user IN (' . implode(',', $placeholders) . '))';
|
||||
$whereParams[':fkUser'] = $fk_user;
|
||||
} else {
|
||||
$whereRole = 'd.fk_user = :fkUser';
|
||||
$whereParams[':fkUser'] = $fk_user;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
error_log("Erreur récupération RR : " . $e->getMessage());
|
||||
$whereRole = 'd.fk_user = :fkUser';
|
||||
$whereParams[':fkUser'] = $fk_user;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
$whereRole = 'd.fk_user = :fkUser';
|
||||
$whereParams[':fkUser'] = $fk_user;
|
||||
break;
|
||||
}
|
||||
|
||||
if ($context === "archives") {
|
||||
$whereStatut = ' AND d.fk_statut_devis = 20';
|
||||
} else {
|
||||
$whereStatut = ' AND d.fk_statut_devis != 20';
|
||||
}
|
||||
|
||||
$whereParams[':term1'] = $termSafe;
|
||||
$whereParams[':term2'] = $termSafe;
|
||||
$whereParams[':term3'] = $termSafe;
|
||||
$whereParams[':term4'] = $termSafe;
|
||||
$whereParams[':term5'] = $termSafe;
|
||||
$whereParams[':term6'] = $termSafe;
|
||||
$whereParams[':term7'] = $termSafe;
|
||||
$whereParams[':term8'] = $termSafe;
|
||||
$whereParams[':term9'] = $termSafe;
|
||||
$whereParams[':term10'] = $termSafe;
|
||||
$whereParams[':term11'] = $termSafe;
|
||||
$whereParams[':term12'] = $termSafe;
|
||||
$whereParams[':term13'] = $termSafe;
|
||||
$whereParams[':term14'] = $termSafe;
|
||||
$whereParams[':term15'] = $termSafe;
|
||||
$whereParams[':term16'] = $termSafe;
|
||||
$whereParams[':term17'] = $termSafe;
|
||||
|
||||
try {
|
||||
$db = Database::getInstance();
|
||||
$sql = 'SELECT DISTINCT d.rowid, d.dossier, d.date_demande, d.date_remise, d.num_opportunite, d.fk_client, d.montant_total_ht_remise, d.marge_totale, d.commentaire, d.chk_speciaux, c.libelle, c.ville, c.cp, d.fk_statut_devis, ';
|
||||
$sql .= 'd.lib_new_client, d.type_new_client, d.adresse1_new_client, d.adresse2_new_client, d.adresse3_new_client, d.cp_new_client, d.ville_new_client, d.comment_devis, d.comment_geste_comm, ';
|
||||
$sql .= 'd.contact_new_nom, d.contact_new_prenom, d.new_telephone, d.new_mobile, d.new_email, d.contact_new_fonction, LEFT(u.prenom,1) AS prenom, u.libelle as nom, ';
|
||||
$sql .= 'xs.libelle as lib_statut, d.chk_new_statut, m.libelle as lib_marche, d.chk_validat, d.fk_user_validat, d.date_validat ';
|
||||
$sql .= 'FROM devis d ';
|
||||
$sql .= 'LEFT JOIN clients c ON c.rowid=d.fk_client ';
|
||||
$sql .= 'LEFT JOIN x_statuts_devis xs ON xs.rowid=d.fk_statut_devis ';
|
||||
$sql .= 'LEFT JOIN marches m ON m.rowid=d.fk_marche ';
|
||||
$sql .= 'LEFT JOIN users u ON u.rowid=d.fk_user ';
|
||||
$sql .= 'LEFT JOIN clients_contacts ct ON ct.fk_client=d.fk_client ';
|
||||
$sql .= 'WHERE (' . $whereRole . ')' . $whereStatut . ' AND (';
|
||||
$sql .= 'd.rowid LIKE :term1 OR ';
|
||||
$sql .= 'c.libelle LIKE :term2 OR ';
|
||||
$sql .= 'c.adresse1 LIKE :term3 OR ';
|
||||
$sql .= 'c.adresse2 LIKE :term4 OR ';
|
||||
$sql .= 'c.adresse3 LIKE :term5 OR ';
|
||||
$sql .= 'c.cp LIKE :term6 OR ';
|
||||
$sql .= 'c.ville LIKE :term7 OR ';
|
||||
$sql .= 'm.libelle LIKE :term8 OR ';
|
||||
$sql .= 'd.num_opportunite LIKE :term9 OR ';
|
||||
$sql .= 'd.lib_new_client LIKE :term10 OR ';
|
||||
$sql .= 'd.cp_new_client LIKE :term11 OR ';
|
||||
$sql .= 'd.ville_new_client LIKE :term12 OR ';
|
||||
$sql .= 'ct.nom LIKE :term13 OR ';
|
||||
$sql .= 'ct.prenom LIKE :term14 OR ';
|
||||
$sql .= 'ct.fonction LIKE :term15 OR ';
|
||||
$sql .= 'ct.email LIKE :term16 OR ';
|
||||
$sql .= 'd.commentaire LIKE :term17) ';
|
||||
$sql .= 'ORDER BY d.dossier, d.date_remise DESC';
|
||||
|
||||
eLog("=== SEARCH DEVIS DEBUG ===");
|
||||
eLog("Terme recherché: " . $term);
|
||||
eLog("Context: " . $context);
|
||||
eLog("SQL: " . $sql);
|
||||
eLog("Params: " . print_r($whereParams, true));
|
||||
|
||||
$pdo = $db->getPDO();
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($whereParams);
|
||||
$devis = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
eLog("Nombre de devis trouvés: " . count($devis));
|
||||
|
||||
$nb_devis = array();
|
||||
foreach ($devis as $dev) {
|
||||
if (!isset($nb_devis[$dev["fk_statut_devis"]])) {
|
||||
$nb_devis[$dev["fk_statut_devis"]] = 1;
|
||||
} else {
|
||||
$nb_devis[$dev["fk_statut_devis"]]++;
|
||||
}
|
||||
}
|
||||
|
||||
$dossiers = array();
|
||||
foreach ($devis as $dev) {
|
||||
if (!in_array($dev["dossier"], array_column($dossiers, 'dossier'))) {
|
||||
$dossiers[] = array("dossier" => $dev["dossier"]);
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode(array(
|
||||
"success" => true,
|
||||
"devis" => $devis,
|
||||
"nb_devis" => $nb_devis,
|
||||
"dossiers" => $dossiers
|
||||
));
|
||||
} catch (Exception $e) {
|
||||
error_log("Erreur recherche devis : " . $e->getMessage());
|
||||
echo json_encode(array("success" => false, "message" => "Erreur lors de la recherche : " . $e->getMessage()));
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case "search_devis_sap":
|
||||
eLog("=== search_devis_sap case appelé ===");
|
||||
$rawData = file_get_contents("php://input");
|
||||
eLog("Raw data: " . $rawData);
|
||||
$data = json_decode($rawData);
|
||||
eLog("Data decoded: " . print_r($data, true));
|
||||
eLog("isset term: " . (isset($data->term) ? 'YES' : 'NO'));
|
||||
|
||||
if (isset($data->term)) {
|
||||
$term = nettoie_input($data->term);
|
||||
$context = nettoie_input($data->context);
|
||||
|
||||
if (strlen($term) < 3) {
|
||||
echo json_encode(array("success" => false, "message" => "Le terme de recherche doit contenir au moins 3 caractères"));
|
||||
break;
|
||||
}
|
||||
|
||||
$termSafe = '%' . $term . '%';
|
||||
|
||||
$whereParams = [];
|
||||
$whereRole = '1=1';
|
||||
|
||||
if ($context === "archives") {
|
||||
$whereStatut = ' AND d.fk_statut_devis = 20';
|
||||
} else {
|
||||
$whereStatut = ' AND d.fk_statut_devis != 20';
|
||||
}
|
||||
|
||||
$whereParams[':term1'] = $termSafe;
|
||||
$whereParams[':term2'] = $termSafe;
|
||||
$whereParams[':term3'] = $termSafe;
|
||||
$whereParams[':term4'] = $termSafe;
|
||||
$whereParams[':term5'] = $termSafe;
|
||||
$whereParams[':term6'] = $termSafe;
|
||||
$whereParams[':term7'] = $termSafe;
|
||||
$whereParams[':term8'] = $termSafe;
|
||||
$whereParams[':term9'] = $termSafe;
|
||||
$whereParams[':term10'] = $termSafe;
|
||||
$whereParams[':term11'] = $termSafe;
|
||||
$whereParams[':term12'] = $termSafe;
|
||||
$whereParams[':term13'] = $termSafe;
|
||||
$whereParams[':term14'] = $termSafe;
|
||||
$whereParams[':term15'] = $termSafe;
|
||||
$whereParams[':term16'] = $termSafe;
|
||||
$whereParams[':term17'] = $termSafe;
|
||||
|
||||
try {
|
||||
$db = Database::getInstance();
|
||||
$sql = 'SELECT DISTINCT d.rowid, d.dossier, d.date_demande, d.date_remise, d.num_opportunite, d.fk_client, d.montant_total_ht_remise, d.marge_totale, d.commentaire, d.chk_speciaux, c.libelle, c.ville, c.cp, d.fk_statut_devis, ';
|
||||
$sql .= 'd.lib_new_client, d.type_new_client, d.adresse1_new_client, d.adresse2_new_client, d.adresse3_new_client, d.cp_new_client, d.ville_new_client, d.comment_devis, d.comment_geste_comm, ';
|
||||
$sql .= 'd.contact_new_nom, d.contact_new_prenom, d.new_telephone, d.new_mobile, d.new_email, d.contact_new_fonction, LEFT(u.prenom,1) AS prenom, u.libelle as nom, ';
|
||||
$sql .= 'xs.libelle as lib_statut, d.chk_new_statut, m.libelle as lib_marche, d.chk_validat, d.fk_user_validat, d.date_validat, c.code ';
|
||||
$sql .= 'FROM devis d ';
|
||||
$sql .= 'LEFT JOIN clients c ON c.rowid=d.fk_client ';
|
||||
$sql .= 'LEFT JOIN x_statuts_devis xs ON xs.rowid=d.fk_statut_devis ';
|
||||
$sql .= 'LEFT JOIN marches m ON m.rowid=d.fk_marche ';
|
||||
$sql .= 'LEFT JOIN users u ON u.rowid=d.fk_user ';
|
||||
$sql .= 'LEFT JOIN clients_contacts ct ON ct.fk_client=d.fk_client ';
|
||||
$sql .= 'WHERE (' . $whereRole . ')' . $whereStatut . ' AND (';
|
||||
$sql .= 'd.rowid LIKE :term1 OR ';
|
||||
$sql .= 'c.libelle LIKE :term2 OR ';
|
||||
$sql .= 'c.adresse1 LIKE :term3 OR ';
|
||||
$sql .= 'c.adresse2 LIKE :term4 OR ';
|
||||
$sql .= 'c.adresse3 LIKE :term5 OR ';
|
||||
$sql .= 'c.cp LIKE :term6 OR ';
|
||||
$sql .= 'c.ville LIKE :term7 OR ';
|
||||
$sql .= 'c.code LIKE :term8 OR ';
|
||||
$sql .= 'm.libelle LIKE :term9 OR ';
|
||||
$sql .= 'd.num_opportunite LIKE :term10 OR ';
|
||||
$sql .= 'd.lib_new_client LIKE :term11 OR ';
|
||||
$sql .= 'd.cp_new_client LIKE :term12 OR ';
|
||||
$sql .= 'd.ville_new_client LIKE :term13 OR ';
|
||||
$sql .= 'ct.nom LIKE :term14 OR ';
|
||||
$sql .= 'ct.prenom LIKE :term15 OR ';
|
||||
$sql .= 'ct.fonction LIKE :term16 OR ';
|
||||
$sql .= 'ct.email LIKE :term17) ';
|
||||
$sql .= 'ORDER BY d.dossier, d.date_remise DESC';
|
||||
|
||||
eLog("=== SEARCH DEVIS SAP DEBUG ===");
|
||||
eLog("Terme recherché: " . $term);
|
||||
eLog("Context: " . $context);
|
||||
eLog("SQL: " . $sql);
|
||||
eLog("Params: " . print_r($whereParams, true));
|
||||
|
||||
$pdo = $db->getPDO();
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($whereParams);
|
||||
$devis = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
eLog("Nombre de devis trouvés: " . count($devis));
|
||||
|
||||
$nb_devis = array();
|
||||
foreach ($devis as $dev) {
|
||||
if (!isset($nb_devis[$dev["fk_statut_devis"]])) {
|
||||
$nb_devis[$dev["fk_statut_devis"]] = 1;
|
||||
} else {
|
||||
$nb_devis[$dev["fk_statut_devis"]]++;
|
||||
}
|
||||
}
|
||||
|
||||
$dossiers = array();
|
||||
foreach ($devis as $dev) {
|
||||
if (!in_array($dev["dossier"], array_column($dossiers, 'dossier'))) {
|
||||
$dossiers[] = array("dossier" => $dev["dossier"]);
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode(array(
|
||||
"success" => true,
|
||||
"devis" => $devis,
|
||||
"nb_devis" => $nb_devis,
|
||||
"dossiers" => $dossiers
|
||||
));
|
||||
} catch (Exception $e) {
|
||||
error_log("Erreur recherche devis SAP : " . $e->getMessage());
|
||||
echo json_encode(array("success" => false, "message" => "Erreur lors de la recherche : " . $e->getMessage()));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
exit();
|
||||
|
||||
@@ -49,6 +49,64 @@ function formate_date($sdate)
|
||||
return $ladate;
|
||||
}
|
||||
|
||||
function syncContactClient($code, $contactNom, $contactPrenom, $contactFonction, $telephone, $mobile, $email, $fkUser)
|
||||
{
|
||||
try {
|
||||
$db = Database::getInstance();
|
||||
|
||||
// 1. Compter les contacts actifs pour ce client
|
||||
$sql = 'SELECT COUNT(*) as nb FROM clients_contacts WHERE fk_client = :code AND active = 1';
|
||||
$countResult = $db->fetchAll($sql, [':code' => $code]);
|
||||
$nbContacts = $countResult[0]['nb'];
|
||||
|
||||
if ($nbContacts == 0) {
|
||||
// Aucun contact : créer directement avec principal=1
|
||||
$principal = 1;
|
||||
} else {
|
||||
// Des contacts existent : vérifier si ce nom+prénom existe (en MAJUSCULES)
|
||||
$sql = 'SELECT rowid FROM clients_contacts
|
||||
WHERE fk_client = :code
|
||||
AND UPPER(nom) = UPPER(:nom)
|
||||
AND UPPER(prenom) = UPPER(:prenom)
|
||||
AND active = 1';
|
||||
$existingContact = $db->fetchAll($sql, [
|
||||
':code' => $code,
|
||||
':nom' => $contactNom,
|
||||
':prenom' => $contactPrenom
|
||||
]);
|
||||
|
||||
if (count($existingContact) > 0) {
|
||||
// Contact déjà présent : ne rien faire
|
||||
eLog("syncContactClient : Contact existe déjà pour client " . $code);
|
||||
return;
|
||||
}
|
||||
|
||||
// Contact pas trouvé : créer avec principal=0
|
||||
$principal = 0;
|
||||
}
|
||||
|
||||
// Créer le contact
|
||||
$sql = 'INSERT INTO clients_contacts SET fk_client = :code, nom = :nom, prenom = :prenom, fonction = :fonction, telephone = :telephone, mobile = :mobile, email = :email, principal = :principal, active = 1, date_creat = NOW(), fk_user_creat = :fk_user';
|
||||
$db->query($sql, [
|
||||
':code' => $code,
|
||||
':nom' => $contactNom,
|
||||
':prenom' => $contactPrenom,
|
||||
':fonction' => $contactFonction,
|
||||
':telephone' => $telephone,
|
||||
':mobile' => $mobile,
|
||||
':email' => $email,
|
||||
':principal' => $principal,
|
||||
':fk_user' => $fkUser
|
||||
]);
|
||||
|
||||
eLog("syncContactClient : Contact créé pour client " . $code . " (principal=" . $principal . ")");
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Erreur syncContactClient : " . $e->getMessage());
|
||||
eLog("Erreur syncContactClient pour client " . $code . " : " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
switch ($Route->_action) {
|
||||
case "upload_clients":
|
||||
@@ -140,10 +198,7 @@ switch ($Route->_action) {
|
||||
try {
|
||||
$db = Database::getInstance();
|
||||
$sql = 'SELECT c.* FROM clients c WHERE c.code = :code';
|
||||
$stmt = $db->prepare($sql);
|
||||
$stmt->bindParam(':code', $code, PDO::PARAM_STR);
|
||||
$stmt->execute();
|
||||
$record = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
$record = $db->fetchAll($sql, [':code' => $code]);
|
||||
} catch (Exception $e) {
|
||||
error_log("Erreur recherche client : " . $e->getMessage());
|
||||
$record = [];
|
||||
@@ -155,8 +210,7 @@ switch ($Route->_action) {
|
||||
$db = Database::getInstance();
|
||||
$sql = 'INSERT INTO clients SET code = :code, libelle = :libelle, siret = :siret, adresse1 = :adresse1, adresse2 = :adresse2, adresse3 = :adresse3, cp = :cp, ville = :ville, ';
|
||||
$sql .= 'type_client = :type_client, contact_nom = :contact_nom, contact_prenom = :contact_prenom, contact_fonction = :contact_fonction, telephone = :telephone, mobile = :mobile, email = :email, chk_import = 1';
|
||||
$stmt = $db->prepare($sql);
|
||||
$stmt->execute([
|
||||
$stmt = $db->query($sql, [
|
||||
':code' => $code,
|
||||
':libelle' => $libelle,
|
||||
':siret' => $siret,
|
||||
@@ -175,6 +229,10 @@ switch ($Route->_action) {
|
||||
]);
|
||||
$fkClient = $db->lastInsertId();
|
||||
fwrite($fhlog, $row . "--- Ajout client avec requête préparée\r\n");
|
||||
|
||||
// Synchroniser le contact dans clients_contacts
|
||||
syncContactClient($code, $contactNom, $contactPrenom, $contactFonction, $telephone, $mobile, $email, $fkUser);
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Erreur insertion client : " . $e->getMessage());
|
||||
fwrite($fhlog, "Erreur insertion : " . $e->getMessage() . "\r\n");
|
||||
@@ -198,8 +256,7 @@ switch ($Route->_action) {
|
||||
$sql = 'UPDATE clients SET libelle = :libelle, siret = :siret, adresse1 = :adresse1, adresse2 = :adresse2, adresse3 = :adresse3, cp = :cp, ville = :ville, ';
|
||||
$sql .= 'type_client = :type_client, contact_nom = :contact_nom, contact_prenom = :contact_prenom, contact_fonction = :contact_fonction, telephone = :telephone, mobile = :mobile, email = :email, chk_import = 1 ';
|
||||
$sql .= 'WHERE code = :code';
|
||||
$stmt = $db->prepare($sql);
|
||||
$stmt->execute([
|
||||
$stmt = $db->query($sql, [
|
||||
':libelle' => $libelle,
|
||||
':siret' => $siret,
|
||||
':adresse1' => $adresse1,
|
||||
@@ -217,6 +274,10 @@ switch ($Route->_action) {
|
||||
':code' => $code
|
||||
]);
|
||||
fwrite($fhlog, $row . "--- MàJ client avec requête préparée\r\n");
|
||||
|
||||
// Synchroniser le contact dans clients_contacts
|
||||
syncContactClient($code, $contactNom, $contactPrenom, $contactFonction, $telephone, $mobile, $email, $fkUser);
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Erreur mise à jour client : " . $e->getMessage());
|
||||
fwrite($fhlog, "Erreur MàJ : " . $e->getMessage() . "\r\n");
|
||||
@@ -378,12 +439,18 @@ switch ($Route->_action) {
|
||||
$libelle = str_replace('"', '', trim($data[1])); // on remplace les doubles guillemets par rien
|
||||
// on réencode en ISO 8859-1 pour éviter les problèmes d'accent
|
||||
if ($codOrigin == "UTF-8") {
|
||||
$libelle = utf8_decode($libelle);
|
||||
}
|
||||
if ($codOrigin != "ISO-8859-1") {
|
||||
// Convertir en ISO 8859-1
|
||||
$libelle = iconv($codOrigin, "ISO-8859-15//IGNORE", $libelle);
|
||||
// Utiliser mb_convert_encoding au lieu de utf8_decode (déprécié en PHP 8.3)
|
||||
$libelle = mb_convert_encoding($libelle, "ISO-8859-1", "UTF-8");
|
||||
} elseif ($codOrigin == "Windows-1252" || $codOrigin == "CP1252") {
|
||||
// Windows-1252 est très proche de ISO-8859-1
|
||||
// On traite le texte tel quel car les caractères de base sont compatibles
|
||||
// Seulement quelques caractères spéciaux diffèrent (€, œ, etc.)
|
||||
$libelle = $libelle; // Pas de conversion, on garde tel quel
|
||||
} elseif ($codOrigin == "ISO-8859-15") {
|
||||
// ISO-8859-15 est compatible avec ISO-8859-1 sauf pour le symbole €
|
||||
$libelle = str_replace('€', 'EUR', $libelle);
|
||||
}
|
||||
// Pour ISO-8859-1, on ne fait rien, c'est déjà le bon format
|
||||
|
||||
$groupe = str_replace(" ", " ", trim($data[2])); // on remplace les doubles espaces par un simple espace
|
||||
$liste = trim($data[3]);
|
||||
@@ -532,7 +599,10 @@ switch ($Route->_action) {
|
||||
$ret = array('ret' => "ko", 'msg' => "Aucun fichier à importer");
|
||||
} else {
|
||||
if ($erreur == "") {
|
||||
$ret = array('ret' => "ok", 'msg' => "L'importation est terminée et s'est bien déroulée");
|
||||
$nbLignes = isset($row) ? $row - 1 : 0; // On enlève la ligne d'en-tête
|
||||
$nbMarches = isset($idMarches) ? count($idMarches) : 0;
|
||||
$msgMarches = $nbMarches > 1 ? " pour " . $nbMarches . " marchés" : ($nbMarches == 1 ? " pour 1 marché" : "");
|
||||
$ret = array('ret' => "ok", 'msg' => "Import terminé : " . $nbLignes . " produits traités" . $msgMarches);
|
||||
} else {
|
||||
$ret = array('ret' => "ko", 'msg' => $erreur);
|
||||
}
|
||||
|
||||
@@ -211,9 +211,7 @@ switch ($Route->_action) {
|
||||
$db = Database::getInstance();
|
||||
// SÉCURITÉ : Utilisation de requête préparée pour l'ID
|
||||
$sql = "SELECT `$chp` AS data FROM clients WHERE rowid = :id";
|
||||
$stmt = $db->prepare($sql);
|
||||
$stmt->execute([':id' => intval($fk_tiers)]);
|
||||
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$result = $db->fetchOne($sql, [':id' => intval($fk_tiers)]);
|
||||
if ($result) {
|
||||
$upls = $result;
|
||||
}
|
||||
@@ -290,12 +288,8 @@ switch ($Route->_action) {
|
||||
OR c.email LIKE :search
|
||||
ORDER BY c.libelle';
|
||||
|
||||
$stmt = $db->prepare($sql);
|
||||
$searchParam = '%' . $search . '%';
|
||||
$stmt->bindParam(':search', $searchParam, PDO::PARAM_STR);
|
||||
$stmt->execute();
|
||||
|
||||
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
$results = $db->fetchAll($sql, [':search' => $searchParam]);
|
||||
echo json_encode($results);
|
||||
} catch (Exception $e) {
|
||||
error_log("Erreur search_clients : " . $e->getMessage());
|
||||
@@ -522,16 +516,13 @@ switch ($Route->_action) {
|
||||
|
||||
// Utilisation de requêtes préparées pour la suppression
|
||||
$sql1 = 'DELETE FROM marches WHERE rowid = :id';
|
||||
$stmt1 = $db->prepare($sql1);
|
||||
$stmt1->execute(['id' => $cid]);
|
||||
$db->query($sql1, ['id' => $cid]);
|
||||
|
||||
$sql2 = 'DELETE FROM marches_listes WHERE fk_marche = :id';
|
||||
$stmt2 = $db->prepare($sql2);
|
||||
$stmt2->execute(['id' => $cid]);
|
||||
$db->query($sql2, ['id' => $cid]);
|
||||
|
||||
$sql3 = 'DELETE FROM produits WHERE fk_marche = :id';
|
||||
$stmt3 = $db->prepare($sql3);
|
||||
$stmt3->execute(['id' => $cid]);
|
||||
$db->query($sql3, ['id' => $cid]);
|
||||
|
||||
eLog("Marché supprimé : ID=$cid");
|
||||
$ret = array('ret' => "ok", 'msg' => 'Marché supprimé');
|
||||
@@ -785,9 +776,7 @@ switch ($Route->_action) {
|
||||
try {
|
||||
$db = Database::getInstance();
|
||||
$sql = 'SELECT c.code, c.libelle, c.adresse1, c.adresse2 FROM clients c WHERE c.rowid = :id';
|
||||
$stmt = $db->prepare($sql);
|
||||
$stmt->execute([':id' => intval($devis["fk_client"])]);
|
||||
$client = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$client = $db->fetchOne($sql, [':id' => intval($devis["fk_client"])]);
|
||||
if (!$client) {
|
||||
$client = ['code' => '', 'libelle' => '', 'adresse1' => '', 'adresse2' => ''];
|
||||
}
|
||||
@@ -884,8 +873,8 @@ switch ($Route->_action) {
|
||||
try {
|
||||
$db = Database::getInstance();
|
||||
$sql = 'DELETE FROM infos WHERE rowid = :id';
|
||||
$stmt = $db->prepare($sql);
|
||||
$result = $stmt->execute(['id' => $cid]);
|
||||
$stmt = $db->query($sql, ['id' => $cid]);
|
||||
$result = $stmt->rowCount() > 0;
|
||||
|
||||
if ($result) {
|
||||
eLog("Info supprimée : ID=$cid");
|
||||
|
||||
272
deploy-cleo.sh
272
deploy-cleo.sh
@@ -1,25 +1,53 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script de déploiement optimisé de Cleo vers l'environnement de développement
|
||||
# Version: 2.0 - Utilise tar.gz pour un transfert plus rapide
|
||||
# Script de déploiement optimisé de Cleo
|
||||
# Version: 2.1 - Supporte déploiement DEV et PROD
|
||||
# Usage: ./deploy-cleo.sh [pra]
|
||||
# Sans argument : Déploie depuis local vers IN3/dva-front (DEV)
|
||||
# Avec 'pra' : Déploie depuis IN3/dva-front vers IN4/pra-front (PROD)
|
||||
|
||||
cd /home/pierre/dev/cleo
|
||||
|
||||
# Configuration du serveur hôte Debian 12
|
||||
HOST_SSH_HOST=195.154.80.116 # Adresse IP du serveur hôte
|
||||
HOST_SSH_USER=root # Utilisateur SSH sur le serveur hôte
|
||||
HOST_SSH_PORT=22 # Port SSH du serveur hôte
|
||||
HOST_SSH_KEY=/home/pierre/.ssh/id_rsa_mbpi # Clé SSH privée pour accéder au serveur hôte
|
||||
# Détecter le mode de déploiement
|
||||
DEPLOY_MODE=${1:-dev}
|
||||
|
||||
# Configuration du conteneur Incus hébergeant cette application
|
||||
CT_PROJECT_NAME=default # Nom du projet Incus où se trouve le conteneur
|
||||
CT_NAME=dva-front # Nom du conteneur Incus
|
||||
CT_IP=13.23.33.42 # IP interne du conteneur Incus
|
||||
DEPLOY_DIR=/var/www # Répertoire de déploiement sur le conteneur
|
||||
APP_NAME=cleo # Nom de l'application
|
||||
if [ "$DEPLOY_MODE" = "pra" ]; then
|
||||
# Configuration PROD : IN3/dva-front → IN4/pra-front
|
||||
SOURCE_HOST=11.1.2.1
|
||||
SOURCE_SSH_USER=root
|
||||
SOURCE_SSH_PORT=22
|
||||
SOURCE_SSH_KEY=/home/pierre/.ssh/id_rsa_mbpi
|
||||
SOURCE_CT_PROJECT=default
|
||||
SOURCE_CT_NAME=dva-front
|
||||
SOURCE_DEPLOY_DIR=/var/www/cleo
|
||||
|
||||
TARGET_HOST=11.1.2.14
|
||||
TARGET_SSH_USER=root
|
||||
TARGET_SSH_PORT=22
|
||||
TARGET_SSH_KEY=/home/pierre/.ssh/id_rsa_mbpi
|
||||
TARGET_CT_PROJECT=default
|
||||
TARGET_CT_NAME=pra-front
|
||||
TARGET_DEPLOY_DIR=/var/www/cleo
|
||||
|
||||
APP_NAME=cleo
|
||||
SKIP_ENV=true
|
||||
else
|
||||
# Configuration DEV : local → IN3/dva-front
|
||||
HOST_SSH_HOST=195.154.80.116
|
||||
HOST_SSH_USER=root
|
||||
HOST_SSH_PORT=22
|
||||
HOST_SSH_KEY=/home/pierre/.ssh/id_rsa_mbpi
|
||||
|
||||
CT_PROJECT_NAME=default
|
||||
CT_NAME=dva-front
|
||||
CT_IP=13.23.33.42
|
||||
DEPLOY_DIR=/var/www
|
||||
APP_NAME=cleo
|
||||
SKIP_ENV=false
|
||||
fi
|
||||
|
||||
# Propriétaire et groupe pour les fichiers et dossiers de destination
|
||||
OWNER=nginx
|
||||
OWNER=nobody
|
||||
GROUP=nginx
|
||||
|
||||
# Couleurs pour l'affichage
|
||||
@@ -36,53 +64,181 @@ fi
|
||||
|
||||
# Afficher les paramètres
|
||||
echo -e "${GREEN}=== Déploiement optimisé CLEO ===${NC}"
|
||||
echo "Serveur hôte: $HOST_SSH_USER@$HOST_SSH_HOST:$HOST_SSH_PORT"
|
||||
echo "Conteneur: $CT_NAME"
|
||||
echo "Déploiement: $DEPLOY_DIR/$APP_NAME"
|
||||
if [ "$DEPLOY_MODE" = "pra" ]; then
|
||||
echo "Mode: PRODUCTION (DEV → PROD)"
|
||||
echo "Source: $SOURCE_CT_NAME ($SOURCE_DEPLOY_DIR)"
|
||||
echo "Destination: $TARGET_CT_NAME ($TARGET_DEPLOY_DIR)"
|
||||
echo "Note: Le fichier .env ne sera PAS écrasé"
|
||||
else
|
||||
echo "Mode: DÉVELOPPEMENT (Local → DEV)"
|
||||
echo "Serveur hôte: $HOST_SSH_USER@$HOST_SSH_HOST:$HOST_SSH_PORT"
|
||||
echo "Conteneur: $CT_NAME"
|
||||
echo "Déploiement: $DEPLOY_DIR/$APP_NAME"
|
||||
fi
|
||||
echo "=================================="
|
||||
|
||||
# 1. Créer l'archive tar.gz localement
|
||||
echo -e "${YELLOW}1. Création de l'archive...${NC}"
|
||||
tar -czf /tmp/cleo.tar.gz \
|
||||
if [ "$DEPLOY_MODE" = "pra" ]; then
|
||||
# ===== MODE PROD: Déploiement IN3/dva-front → IN4/pra-front =====
|
||||
|
||||
echo -e "${YELLOW}1. Création de l'archive depuis IN3/dva-front...${NC}"
|
||||
|
||||
# Définir les options SSH pour source et target
|
||||
SOURCE_SSH_OPTS="-p $SOURCE_SSH_PORT -i $SOURCE_SSH_KEY"
|
||||
TARGET_SSH_OPTS="-p $TARGET_SSH_PORT -i $TARGET_SSH_KEY"
|
||||
|
||||
# Définir les options SCP (port avec -P majuscule)
|
||||
SOURCE_SCP_OPTS="-P $SOURCE_SSH_PORT -i $SOURCE_SSH_KEY"
|
||||
TARGET_SCP_OPTS="-P $TARGET_SSH_PORT -i $TARGET_SSH_KEY"
|
||||
|
||||
# Script sur IN3 pour créer l'archive
|
||||
SOURCE_SCRIPT="
|
||||
set -e
|
||||
incus project switch $SOURCE_CT_PROJECT
|
||||
echo 'Création de l archive depuis dva-front...'
|
||||
incus exec $SOURCE_CT_NAME -- sh -c '
|
||||
cd $SOURCE_DEPLOY_DIR && \
|
||||
tar -czf /tmp/cleo-prod.tar.gz \
|
||||
--exclude=\".git\" \
|
||||
--exclude=\"log/*.log\" \
|
||||
--exclude=\"pub/files/upload/*\" \
|
||||
--exclude=\"docs\" \
|
||||
--exclude=\"migration\" \
|
||||
--exclude=\".claude\" \
|
||||
--exclude=\".vscode\" \
|
||||
--exclude=\"vendor\" \
|
||||
--exclude=\"backup_*\" \
|
||||
--exclude=\"*.tar.gz\" \
|
||||
--exclude=\".env\" \
|
||||
--exclude=\".env.swp\" \
|
||||
.
|
||||
'
|
||||
incus file pull $SOURCE_CT_NAME/tmp/cleo-prod.tar.gz /tmp/
|
||||
incus exec $SOURCE_CT_NAME -- rm -f /tmp/cleo-prod.tar.gz
|
||||
"
|
||||
|
||||
ssh $SOURCE_SSH_OPTS $SOURCE_SSH_USER@$SOURCE_HOST "$SOURCE_SCRIPT"
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${RED}Erreur lors de la création de l'archive${NC}"
|
||||
exit 1
|
||||
fi
|
||||
echo -e "${GREEN}✓ Archive créée sur IN3${NC}"
|
||||
|
||||
# Transférer l'archive de IN3 vers IN4
|
||||
echo -e "${YELLOW}2. Transfert de l'archive IN3 → IN4...${NC}"
|
||||
scp $SOURCE_SCP_OPTS $SOURCE_SSH_USER@$SOURCE_HOST:/tmp/cleo-prod.tar.gz /tmp/
|
||||
scp $TARGET_SCP_OPTS /tmp/cleo-prod.tar.gz $TARGET_SSH_USER@$TARGET_HOST:/tmp/
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${RED}Erreur lors du transfert de l'archive${NC}"
|
||||
rm -f /tmp/cleo-prod.tar.gz
|
||||
exit 1
|
||||
fi
|
||||
echo -e "${GREEN}✓ Archive transférée vers IN4${NC}"
|
||||
|
||||
# Déployer sur IN4/pra-front
|
||||
echo -e "${YELLOW}3. Déploiement dans IN4/pra-front...${NC}"
|
||||
|
||||
TARGET_SCRIPT="
|
||||
set -e
|
||||
incus project switch $TARGET_CT_PROJECT
|
||||
echo 'Transfert de l archive dans pra-front...'
|
||||
incus file push /tmp/cleo-prod.tar.gz $TARGET_CT_NAME/tmp/
|
||||
|
||||
echo 'Déploiement dans pra-front (backup .env)...'
|
||||
incus exec $TARGET_CT_NAME -- sh -c '
|
||||
cp $TARGET_DEPLOY_DIR/.env /tmp/.env.backup && \
|
||||
cd $TARGET_DEPLOY_DIR && \
|
||||
tar -xzf /tmp/cleo-prod.tar.gz && \
|
||||
mv /tmp/.env.backup $TARGET_DEPLOY_DIR/.env && \
|
||||
rm -f /tmp/cleo-prod.tar.gz
|
||||
'
|
||||
|
||||
echo 'Configuration des permissions...'
|
||||
incus exec $TARGET_CT_NAME -- sh -c '
|
||||
chown -R $OWNER:$GROUP $TARGET_DEPLOY_DIR && \
|
||||
find $TARGET_DEPLOY_DIR -type d -exec chmod 755 {} \; && \
|
||||
find $TARGET_DEPLOY_DIR -type f -exec chmod 644 {} \; && \
|
||||
mkdir -p $TARGET_DEPLOY_DIR/log && \
|
||||
chmod 775 $TARGET_DEPLOY_DIR/log && \
|
||||
touch $TARGET_DEPLOY_DIR/log/\$(date +%m%d).log && \
|
||||
chmod 664 $TARGET_DEPLOY_DIR/log/\$(date +%m%d).log && \
|
||||
chown $OWNER:$GROUP $TARGET_DEPLOY_DIR/log/*.log && \
|
||||
chmod 640 $TARGET_DEPLOY_DIR/.env && \
|
||||
if [ -d $TARGET_DEPLOY_DIR/pub/files/upload ]; then
|
||||
chmod 775 $TARGET_DEPLOY_DIR/pub/files/upload && \
|
||||
chown $OWNER:$GROUP $TARGET_DEPLOY_DIR/pub/files/upload
|
||||
fi
|
||||
'
|
||||
|
||||
rm -f /tmp/cleo-prod.tar.gz
|
||||
echo 'Déploiement PROD terminé!'
|
||||
"
|
||||
|
||||
ssh $TARGET_SSH_OPTS $TARGET_SSH_USER@$TARGET_HOST "$TARGET_SCRIPT"
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${RED}Erreur lors du déploiement PROD${NC}"
|
||||
rm -f /tmp/cleo-prod.tar.gz
|
||||
ssh $SOURCE_SSH_OPTS $SOURCE_SSH_USER@$SOURCE_HOST "rm -f /tmp/cleo-prod.tar.gz"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Nettoyer les archives locales et distantes
|
||||
echo -e "${YELLOW}4. Nettoyage...${NC}"
|
||||
rm -f /tmp/cleo-prod.tar.gz
|
||||
ssh $SOURCE_SSH_OPTS $SOURCE_SSH_USER@$SOURCE_HOST "rm -f /tmp/cleo-prod.tar.gz"
|
||||
echo -e "${GREEN}✓ Nettoyage terminé${NC}"
|
||||
|
||||
else
|
||||
# ===== MODE DEV: Déploiement local → IN3/dva-front =====
|
||||
|
||||
# 1. Créer l'archive tar.gz localement
|
||||
echo -e "${YELLOW}1. Création de l'archive...${NC}"
|
||||
tar -czf /tmp/cleo.tar.gz \
|
||||
--exclude='.git' \
|
||||
--exclude='log/*.log' \
|
||||
--exclude='pub/files/upload/*' \
|
||||
--exclude='docs/*.sql' \
|
||||
--exclude='docs' \
|
||||
--exclude='migration' \
|
||||
--exclude='.claude' \
|
||||
--exclude='.vscode' \
|
||||
--exclude='vendor' \
|
||||
--exclude='backup_*' \
|
||||
--exclude='*.tar.gz' \
|
||||
--exclude='.env' \
|
||||
--exclude='.env.swp' \
|
||||
.
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${RED}Erreur lors de la création de l'archive${NC}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
ARCHIVE_SIZE=$(du -h /tmp/cleo.tar.gz | cut -f1)
|
||||
echo -e "${GREEN}✓ Archive créée: /tmp/cleo.tar.gz ($ARCHIVE_SIZE)${NC}"
|
||||
ARCHIVE_SIZE=$(du -h /tmp/cleo.tar.gz | cut -f1)
|
||||
echo -e "${GREEN}✓ Archive créée: /tmp/cleo.tar.gz ($ARCHIVE_SIZE)${NC}"
|
||||
|
||||
# 2. Copier l'archive vers IN3
|
||||
echo -e "${YELLOW}2. Transfert de l'archive vers IN3...${NC}"
|
||||
scp -P $HOST_SSH_PORT -i $HOST_SSH_KEY /tmp/cleo.tar.gz $HOST_SSH_USER@$HOST_SSH_HOST:/tmp/
|
||||
# 2. Copier l'archive vers IN3
|
||||
echo -e "${YELLOW}2. Transfert de l'archive vers IN3...${NC}"
|
||||
scp -P $HOST_SSH_PORT -i $HOST_SSH_KEY /tmp/cleo.tar.gz $HOST_SSH_USER@$HOST_SSH_HOST:/tmp/
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${RED}Erreur lors du transfert de l'archive${NC}"
|
||||
rm -f /tmp/cleo.tar.gz
|
||||
exit 1
|
||||
fi
|
||||
echo -e "${GREEN}✓ Archive transférée sur IN3${NC}"
|
||||
fi
|
||||
echo -e "${GREEN}✓ Archive transférée sur IN3${NC}"
|
||||
|
||||
# 3. Nettoyer l'ancien répertoire /tmp/cleo sur IN3 s'il existe
|
||||
echo -e "${YELLOW}3. Nettoyage des anciens fichiers temporaires sur IN3...${NC}"
|
||||
ssh $SSH_OPTS $HOST_SSH_USER@$HOST_SSH_HOST "rm -rf /tmp/cleo 2>/dev/null || true"
|
||||
echo -e "${GREEN}✓ Nettoyage effectué${NC}"
|
||||
# 3. Nettoyer l'ancien répertoire /tmp/cleo sur IN3 s'il existe
|
||||
echo -e "${YELLOW}3. Nettoyage des anciens fichiers temporaires sur IN3...${NC}"
|
||||
ssh $SSH_OPTS $HOST_SSH_USER@$HOST_SSH_HOST "rm -rf /tmp/cleo 2>/dev/null || true"
|
||||
echo -e "${GREEN}✓ Nettoyage effectué${NC}"
|
||||
|
||||
# 4. Transférer et extraire dans le conteneur
|
||||
echo -e "${YELLOW}4. Déploiement dans le conteneur $CT_NAME...${NC}"
|
||||
# 4. Transférer et extraire dans le conteneur
|
||||
echo -e "${YELLOW}4. Déploiement dans le conteneur $CT_NAME...${NC}"
|
||||
|
||||
# Script à exécuter sur IN3
|
||||
REMOTE_SCRIPT="
|
||||
# Script à exécuter sur IN3
|
||||
REMOTE_SCRIPT="
|
||||
set -e
|
||||
|
||||
# Sélectionner le projet Incus
|
||||
@@ -108,11 +264,11 @@ incus exec $CT_NAME -- sh -c '
|
||||
find $DEPLOY_DIR/$APP_NAME -type d -exec chmod 755 {} \; && \
|
||||
find $DEPLOY_DIR/$APP_NAME -type f -exec chmod 644 {} \; && \
|
||||
mkdir -p $DEPLOY_DIR/$APP_NAME/log && \
|
||||
chmod 777 $DEPLOY_DIR/$APP_NAME/log && \
|
||||
touch $DEPLOY_DIR/$APP_NAME/log/$(date +%m%d).log && \
|
||||
chmod 777 $DEPLOY_DIR/$APP_NAME/log/$(date +%m%d).log && \
|
||||
chown nobody:nobody $DEPLOY_DIR/$APP_NAME/log/*.log && \
|
||||
chmod 644 $DEPLOY_DIR/$APP_NAME/.env && \
|
||||
chmod 775 $DEPLOY_DIR/$APP_NAME/log && \
|
||||
touch $DEPLOY_DIR/$APP_NAME/log/\$(date +%m%d).log && \
|
||||
chmod 664 $DEPLOY_DIR/$APP_NAME/log/\$(date +%m%d).log && \
|
||||
chown $OWNER:$GROUP $DEPLOY_DIR/$APP_NAME/log/*.log && \
|
||||
chmod 640 $DEPLOY_DIR/$APP_NAME/.env && \
|
||||
rm -f $DEPLOY_DIR/$APP_NAME/.env.swp && \
|
||||
if [ -d $DEPLOY_DIR/$APP_NAME/pub/files/upload ]; then
|
||||
chmod 775 $DEPLOY_DIR/$APP_NAME/pub/files/upload && \
|
||||
@@ -126,26 +282,34 @@ rm -f /tmp/cleo.tar.gz
|
||||
echo 'Déploiement terminé avec succès!'
|
||||
"
|
||||
|
||||
# Exécuter le script sur IN3
|
||||
ssh $SSH_OPTS $HOST_SSH_USER@$HOST_SSH_HOST "$REMOTE_SCRIPT"
|
||||
# Exécuter le script sur IN3
|
||||
ssh $SSH_OPTS $HOST_SSH_USER@$HOST_SSH_HOST "$REMOTE_SCRIPT"
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${RED}Erreur lors du déploiement${NC}"
|
||||
# Nettoyer l'archive locale et distante
|
||||
rm -f /tmp/cleo.tar.gz
|
||||
ssh $SSH_OPTS $HOST_SSH_USER@$HOST_SSH_HOST "rm -f /tmp/cleo.tar.gz 2>/dev/null || true"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# 5. Nettoyer l'archive locale
|
||||
echo -e "${YELLOW}5. Nettoyage local...${NC}"
|
||||
rm -f /tmp/cleo.tar.gz
|
||||
echo -e "${GREEN}✓ Archive locale supprimée${NC}"
|
||||
# 5. Nettoyer l'archive locale
|
||||
echo -e "${YELLOW}5. Nettoyage local...${NC}"
|
||||
rm -f /tmp/cleo.tar.gz
|
||||
echo -e "${GREEN}✓ Archive locale supprimée${NC}"
|
||||
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}===================================================${NC}"
|
||||
echo -e "${GREEN} Déploiement terminé avec succès ! ${NC}"
|
||||
echo -e "${GREEN}===================================================${NC}"
|
||||
echo "Site: http://dcleo.unikoffice.com"
|
||||
echo "Chemin: $DEPLOY_DIR/$APP_NAME sur $CT_NAME"
|
||||
if [ "$DEPLOY_MODE" = "pra" ]; then
|
||||
echo "Site: https://cleo.unikoffice.com"
|
||||
echo "Environnement: PRODUCTION (pra-front)"
|
||||
else
|
||||
echo "Site: http://dcleo.unikoffice.com"
|
||||
echo "Environnement: DÉVELOPPEMENT (dva-front)"
|
||||
echo "Chemin: $DEPLOY_DIR/$APP_NAME sur $CT_NAME"
|
||||
fi
|
||||
echo ""
|
||||
133
deploy-file.sh
Executable file
133
deploy-file.sh
Executable 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 ""
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
CLEO est une application web de gestion de devis développée en PHP 8.3 pour les PME. Elle utilise une architecture MVC classique avec un framework maison appelé "d6".
|
||||
|
||||
**Version actuelle** : 2.0.1 (migration complétée le 12 septembre 2025)
|
||||
**Version actuelle** : 2.0.3 (gestion multi-contacts complétée le 21 octobre 2025)
|
||||
|
||||
## Architecture technique
|
||||
|
||||
@@ -53,7 +53,9 @@ cleo/
|
||||
- Gestion des remises par paliers de quantité
|
||||
|
||||
2. **Gestion des clients** (`cclients.php`)
|
||||
- Base clients avec contacts
|
||||
- Base clients avec contacts multiples (table `clients_contacts`)
|
||||
- Gestion des contacts via modale Bootstrap intégrée aux devis
|
||||
- Contact principal automatique avec indicateur visuel
|
||||
- Segmentation par secteur géographique
|
||||
- Types de clients paramétrables
|
||||
- Import/export de données
|
||||
@@ -82,10 +84,11 @@ cleo/
|
||||
- **Sécurité** : Requêtes préparées systématiques
|
||||
|
||||
### Tables principales
|
||||
- `devis` : Table principale des devis
|
||||
- `devis` : Table principale des devis (champ `fk_contact` depuis v2.0.3)
|
||||
- `devis_produits` : Lignes de produits des devis
|
||||
- `devis_histo` : Historique des modifications
|
||||
- `clients` : Base clients
|
||||
- `clients_contacts` : Contacts multiples par client (v2.0.3)
|
||||
- `produits` : Catalogue produits
|
||||
- `produits_familles` : Familles de produits avec marges
|
||||
- `marches` : Référentiel des marchés
|
||||
@@ -95,22 +98,32 @@ cleo/
|
||||
|
||||
## Points de sécurité
|
||||
|
||||
### Vulnérabilités corrigées (v2.0.1)
|
||||
### Vulnérabilités corrigées
|
||||
|
||||
✅ **1. Stockage des mots de passe**
|
||||
✅ **v2.0.1 - Stockage des mots de passe**
|
||||
- Credentials externalisés dans `.env`
|
||||
- Variables d'environnement utilisées systématiquement
|
||||
|
||||
✅ **2. Protection contre les injections SQL**
|
||||
✅ **v2.0.1 - Protection contre les injections SQL**
|
||||
- Migration complète vers PDO
|
||||
- Requêtes préparées dans la classe `Database`
|
||||
- Pattern Singleton pour la connexion
|
||||
|
||||
✅ **3. Gestion des erreurs sécurisée**
|
||||
✅ **v2.0.1 - Gestion des erreurs sécurisée**
|
||||
- Classe `Database` avec gestion d'erreurs centralisée
|
||||
- Logging contrôlé par variables d'environnement
|
||||
- Mode debug désactivable en production
|
||||
|
||||
✅ **v2.0.2 - Corrections critiques**
|
||||
- Sanitisation stricte des entrées utilisateur
|
||||
- Validation des paramètres AJAX
|
||||
- Fonction `nettoie_input()` utilisée systématiquement
|
||||
|
||||
✅ **v2.0.3 - Gestion multi-contacts sécurisée**
|
||||
- Contrôleur AJAX `cjxcontacts.php` avec requêtes préparées
|
||||
- Validation des foreign keys et soft delete
|
||||
- Prévention de suppression du dernier contact actif
|
||||
|
||||
### Vulnérabilités restantes à traiter
|
||||
|
||||
#### 1. Injections SQL résiduelles
|
||||
@@ -208,15 +221,28 @@ cleo/
|
||||
|
||||
## Conclusion
|
||||
|
||||
CLEO v2.0.1 représente une évolution majeure avec la migration réussie vers une architecture sécurisée :
|
||||
- ✅ Base de données unique et centralisée
|
||||
- ✅ Connexions PDO avec requêtes préparées
|
||||
- ✅ Configuration externalisée
|
||||
- ✅ Séparation application/base de données
|
||||
CLEO v2.0.3 représente l'aboutissement de trois itérations majeures d'amélioration :
|
||||
|
||||
Les priorités de sécurité critiques ont été adressées. L'application peut maintenant évoluer sereinement vers des standards plus modernes tout en maintenant sa stabilité opérationnelle.
|
||||
**v2.0.1 - Architecture sécurisée**
|
||||
- Base de données unique et centralisée
|
||||
- Connexions PDO avec requêtes préparées
|
||||
- Configuration externalisée
|
||||
- Séparation application/base de données
|
||||
|
||||
**v2.0.2 - Sécurité renforcée**
|
||||
- Sanitisation systématique des entrées
|
||||
- Validation stricte des paramètres AJAX
|
||||
- Corrections de vulnérabilités critiques
|
||||
|
||||
**v2.0.3 - Gestion multi-contacts**
|
||||
- Migration vers table relationnelle `clients_contacts`
|
||||
- Interface modale intégrée dans les devis
|
||||
- CRUD complet avec soft delete
|
||||
- Gestion automatique du contact principal
|
||||
|
||||
L'application dispose maintenant d'une base solide pour évoluer vers des standards modernes tout en maintenant sa stabilité opérationnelle.
|
||||
|
||||
---
|
||||
|
||||
*Document mis à jour le 12 septembre 2025*
|
||||
*Version 2.0.1 - Post-migration*
|
||||
*Document mis à jour le 21 octobre 2025*
|
||||
*Version 2.0.3 - Gestion multi-contacts*
|
||||
111
docs/RULES.md
Normal file
111
docs/RULES.md
Normal 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*
|
||||
356
docs/TODO.md
356
docs/TODO.md
@@ -4,15 +4,6 @@
|
||||
|
||||
### Module Devis
|
||||
|
||||
#### 6. ✅ Modifier un devis archivé (TERMINÉ - 12/09/2025)
|
||||
**Priorité**: Haute
|
||||
**Description**: Permettre la modification d'un devis archivé et son renvoi pour traitement sans nécessiter de duplication.
|
||||
**Tâches**:
|
||||
- [x] Ajouter un bouton "Réactiver" sur les devis archivés (statut 20)
|
||||
- [x] Permettre le changement de statut d'archivé vers "En cours"
|
||||
- [x] Conserver l'historique de réactivation dans `devis_histo`
|
||||
- [x] Adapter les droits selon les rôles (RR, DV, DIR-CO)
|
||||
|
||||
#### 8. Dupliquer une ligne produit
|
||||
**Priorité**: Moyenne
|
||||
**Description**: Permettre la duplication d'une ligne produit dans un même devis (utile pour les gratuités).
|
||||
@@ -36,16 +27,6 @@
|
||||
- [ ] Paginer les résultats de recherche
|
||||
- [ ] Export des résultats en Excel
|
||||
|
||||
#### 19. Gestion des contacts multiples
|
||||
**Priorité**: Haute
|
||||
**Description**: Permettre la gestion de plusieurs contacts par client.
|
||||
**Tâches**:
|
||||
- [ ] Créer une table `clients_contacts`
|
||||
- [ ] Migration des contacts existants vers la nouvelle structure
|
||||
- [ ] Interface CRUD pour les contacts
|
||||
- [ ] Sélecteur de contact à la création/modification de devis
|
||||
- [ ] Historique des contacts par devis
|
||||
|
||||
#### 21. Actualisation tarifaire
|
||||
**Priorité**: Moyenne
|
||||
**Description**: Permettre l'actualisation des prix selon la dernière grille tarifaire.
|
||||
@@ -56,8 +37,73 @@
|
||||
- [ ] Recalculer automatiquement les marges
|
||||
- [ ] Tracer l'actualisation dans l'historique
|
||||
|
||||
#### 22. Marchés hybrides et onglet Mercurial
|
||||
**Priorité**: Haute
|
||||
**Description**: Ajouter un onglet "Mercurial" dans la page devis pour les marchés de type hybride, listant tous les produits du marché.
|
||||
|
||||
**Tâches - Phase 1 (Onglet Mercurial)** :
|
||||
- [x] Identifier le type de marché du devis sélectionné
|
||||
- [x] Détecter si le marché est de type "hybride"
|
||||
- [x] Ajouter un nouvel onglet "Mercurial" dans l'interface devis (au niveau de l'onglet Produits)
|
||||
- [x] Récupérer tous les produits associés au marché
|
||||
- [x] Filtrer les produits "Hors Marché 999"
|
||||
- [x] Afficher la liste des produits dans l'onglet Mercurial
|
||||
- [x] Gérer l'affichage/masquage de l'onglet selon le type de marché
|
||||
|
||||
**Tâches - Phase 2 (Améliorations visuelles et règles métier)** :
|
||||
- [ ] **Visibilité de l'onglet Mercurial** : Rendre l'onglet "Mercurial" visuellement distinct (couleur de fond différente, par exemple) pour qu'il soit clairement identifiable par les commerciaux
|
||||
|
||||
**Types de marchés hybrides** :
|
||||
Deux cas de marchés hybrides doivent être gérés différemment :
|
||||
|
||||
**CAS 1 - Mercuriale sans remise** :
|
||||
- Liste mercuriale en prix nets SANS remise applicable sur ces références
|
||||
- Reste du catalogue disponible avec possibilité de remises en autonomie
|
||||
|
||||
**CAS 2 - Mercuriale avec remise possible** :
|
||||
- Liste mercuriale en prix nets AVEC possibilité de remises sur ces références
|
||||
- Reste du catalogue disponible avec possibilité de remises en autonomie
|
||||
|
||||
**Règles communes aux 2 cas** :
|
||||
- Quand le devis contient UNIQUEMENT des références mercuriales → pas de demande d'accord nécessaire, le RR peut valider directement
|
||||
- Quand le devis contient références mercuriales + catalogue général → seules les références du catalogue sont concernées par les seuils de marge et peuvent générer une demande d'accord
|
||||
- Si geste commercial souhaité sur un devis 100% mercurial → utiliser le champ "Demande geste commercial" existant
|
||||
|
||||
**Tâches - Paramétrage base de données** :
|
||||
- [ ] Ajouter un champ dans la table `marches` pour définir le type de marché hybride :
|
||||
- `type_mercurial` (ENUM ou INT) : NULL = non hybride, 1 = CAS 1 (sans remise), 2 = CAS 2 (avec remise)
|
||||
- [ ] Modifier la fiche marché pour permettre la sélection du type de marché hybride
|
||||
|
||||
**Tâches - Logique métier de validation** :
|
||||
- [ ] Détecter si un devis contient uniquement des produits mercuriaux
|
||||
- [ ] Détecter si un devis contient un mix mercurial + catalogue
|
||||
- [ ] Adapter le calcul des seuils de marge :
|
||||
- Si devis 100% mercurial → pas de vérification de seuil, validation RR directe
|
||||
- Si devis mixte → calculer les seuils uniquement sur les produits du catalogue général
|
||||
- [ ] Bloquer/autoriser les remises sur produits mercuriaux selon le type de marché (CAS 1 vs CAS 2)
|
||||
- [ ] Tester le workflow complet avec les 2 types de marchés hybrides
|
||||
|
||||
### Module SAP
|
||||
|
||||
#### 13. Import et contrôle des clients SAP
|
||||
**Priorité**: Haute
|
||||
**Description**: Contrôler les nouveaux clients créés dans la base CLEO et vérifier la correspondance avec la base SAP.
|
||||
**Tâches**:
|
||||
- [ ] Identifier le script/contrôleur d'import des clients SAP
|
||||
- [ ] Analyser la structure des données importées
|
||||
- [ ] Mettre en place un système de contrôle de correspondance
|
||||
- [ ] Vérifier l'unicité du `clients.code` (identifiant SAP)
|
||||
- [ ] Détecter les doublons potentiels (nom, adresse)
|
||||
- [ ] Signaler les incohérences entre SAP et CLEO
|
||||
- [ ] Créer un rapport d'import avec :
|
||||
- [ ] Nombre de clients importés
|
||||
- [ ] Nombre de clients mis à jour
|
||||
- [ ] Nombre d'anomalies détectées
|
||||
- [ ] Gestion des cas particuliers :
|
||||
- [ ] Client existe dans CLEO mais pas dans SAP
|
||||
- [ ] Client existe dans SAP mais code différent dans CLEO
|
||||
- [ ] Contacts orphelins après import
|
||||
|
||||
#### 14. Gestion de la prise en charge
|
||||
**Priorité**: Haute
|
||||
**Description**: Ajouter la traçabilité de la prise en charge et du transfert EDI.
|
||||
@@ -117,65 +163,7 @@
|
||||
|
||||
### Plan de migration - État d'avancement
|
||||
|
||||
#### ✅ Phase 0 - Refactoring base de données (COMPLÉTÉ - 12/09/2025)
|
||||
- [x] Script de migration SQL créé
|
||||
- [x] Table `y_pages` migrée depuis `uof_frontal`
|
||||
- [x] Table `z_logs` créée dans `cleo`
|
||||
- [x] Base `cleo` créée avec toutes les tables
|
||||
- [x] Données migrées de `uof_linet` vers `cleo`
|
||||
- [x] Références à `uof_frontal` supprimées
|
||||
- [x] Classe Database PDO créée
|
||||
- [x] Variables d'environnement `.env` implémentées
|
||||
- [x] Tests validés en DEV
|
||||
|
||||
#### ✅ Phase 1 - Environnement DEV IN3 (COMPLÉTÉ - 12/09/2025)
|
||||
- [x] Container `maria3` créé sur IN3
|
||||
- [x] MariaDB 11.4 installé et configuré
|
||||
- [x] Base `cleo` migrée vers `maria3`
|
||||
- [x] Configuration pointant vers `maria3` (IP: 13.23.33.4)
|
||||
- [x] Application testée et fonctionnelle
|
||||
- [x] MariaDB supprimé de `dva-front`
|
||||
- [x] Script de déploiement optimisé (`deploy-cleo-fast.sh`)
|
||||
|
||||
#### Phase 2 - Préparation PROD IN4 (À FAIRE)
|
||||
**Export depuis IN3:**
|
||||
- [ ] Exporter le container `dva-front` depuis IN3
|
||||
```bash
|
||||
incus export dva-front dva-front-export.tar.gz
|
||||
```
|
||||
- [ ] Exporter le container `maria3` depuis IN3
|
||||
```bash
|
||||
incus export maria3 maria3-export.tar.gz
|
||||
```
|
||||
|
||||
**Import sur IN4:**
|
||||
- [ ] Importer `dva-front` comme `pra-front` sur IN4
|
||||
```bash
|
||||
incus import dva-front-export.tar.gz pra-front
|
||||
```
|
||||
- [ ] Importer `maria3` comme `maria4` sur IN4
|
||||
```bash
|
||||
incus import maria3-export.tar.gz maria4
|
||||
```
|
||||
- [ ] Configurer les IPs et paramètres réseau sur IN4
|
||||
- [ ] Adapter le fichier `.env` pour l'environnement PROD
|
||||
|
||||
#### Phase 3 - Migration des données PROD (À FAIRE)
|
||||
- [ ] Effectuer une sauvegarde complète des bases PROD sur IN2/nx4
|
||||
- [ ] Exporter les données de `uof_frontal` et `uof_linet` depuis IN2/nx4
|
||||
- [ ] Utiliser le script de migration SQL pour fusionner les données
|
||||
- [ ] Importer les données fusionnées dans `maria4` sur IN4
|
||||
- [ ] Configurer `pra-front` pour pointer vers `maria4`
|
||||
- [ ] Tests de validation en pré-production
|
||||
|
||||
#### Phase 4 - Bascule PROD (À FAIRE)
|
||||
- [ ] Planifier la fenêtre de maintenance
|
||||
- [ ] Arrêter l'application sur IN2
|
||||
- [ ] Synchronisation finale des données vers IN4/maria4
|
||||
- [ ] Basculer le DNS/proxy vers IN4
|
||||
- [ ] Valider le fonctionnement en production
|
||||
- [ ] Monitoring post-migration (48h)
|
||||
- [ ] Décommissionner IN2 après période de stabilisation
|
||||
✅ **Migration complétée** - Toutes les phases (0 à 4) sont terminées.
|
||||
|
||||
### Configuration technique
|
||||
|
||||
@@ -207,6 +195,159 @@ DB_PASSWORD=<PROD_PASSWORD> # À sécuriser
|
||||
- [ ] Scripts de backup automatisés à mettre en place
|
||||
- [ ] Réplication master-slave pour haute disponibilité (optionnel)
|
||||
|
||||
## ⚠️ CRITIQUE - Risque de collision de codes clients (EN ATTENTE CLIENT)
|
||||
|
||||
### Problématique identifiée
|
||||
**Date**: 26 novembre 2025
|
||||
**Statut**: 🔴 EN ATTENTE RÉPONSE CLIENT
|
||||
|
||||
#### Situation actuelle
|
||||
Lorsqu'un commercial crée un nouveau client manuellement dans CLEO (client non présent dans SAP), le système génère automatiquement un code via :
|
||||
```php
|
||||
$newCode = MAX(code) + 1; // cjxdevis.php ligne 1326
|
||||
```
|
||||
|
||||
#### Risque de collision
|
||||
**Scénario catastrophe** :
|
||||
1. Commercial crée un client manuel → code auto = `12345`
|
||||
2. Commercial ajoute des contacts, fait des devis
|
||||
3. **Import SAP suivant** : un nouveau client SAP arrive avec le code `12345`
|
||||
4. L'import trouve le client existant (même code) et **écrase toutes les données** du client manuel
|
||||
5. Les contacts du client manuel deviennent incohérents (pointent vers le mauvais client SAP)
|
||||
6. Les devis du client manuel sont rattachés au mauvais client SAP
|
||||
|
||||
#### Question posée au client
|
||||
**"Que se passe-t-il lorsqu'un devis avec un nouveau client (code = MAX+1) est intégré dans SAP ?"**
|
||||
- Le client manuel reçoit-il un vrai code SAP ?
|
||||
- Le code est-il synchronisé dans CLEO après intégration ?
|
||||
- Existe-t-il un processus de réconciliation ?
|
||||
|
||||
### Solutions techniques envisagées
|
||||
|
||||
#### Option A : Plage réservée pour clients manuels
|
||||
```php
|
||||
// Codes 9000000+ réservés aux créations manuelles
|
||||
$newCode = 9000000 + $compteur;
|
||||
```
|
||||
**Avantages** : Simple, pas de collision possible
|
||||
**Inconvénients** : Nécessite coordination avec SAP
|
||||
|
||||
#### Option B : Codes négatifs pour clients manuels
|
||||
```php
|
||||
// Codes négatifs = clients manuels non SAP
|
||||
$newCode = -1 * (MAX(ABS(code)) + 1);
|
||||
```
|
||||
**Avantages** : Distinction claire SAP/Manuel
|
||||
**Inconvénients** : Peut poser problème avec certains systèmes
|
||||
|
||||
#### Option C : Flag `chk_manual` + protection
|
||||
```sql
|
||||
ALTER TABLE clients ADD COLUMN chk_manual TINYINT DEFAULT 0;
|
||||
```
|
||||
- `chk_manual = 1` → Client créé manuellement, jamais écrasé par import SAP
|
||||
- Lors de l'import SAP, ignorer les clients avec `chk_manual = 1`
|
||||
- Processus manuel de réconciliation si le client est créé dans SAP
|
||||
|
||||
**Avantages** : Protection garantie, traçabilité
|
||||
**Inconvénients** : Nécessite gestion manuelle de la réconciliation
|
||||
|
||||
#### Option D : Code temporaire + synchronisation
|
||||
- Client manuel créé avec code `TEMP_XXXXX`
|
||||
- Lors de l'intégration SAP, récupération du vrai code SAP
|
||||
- Mise à jour du code client + tous les contacts/devis associés
|
||||
|
||||
**Avantages** : Cohérence totale avec SAP
|
||||
**Inconvénients** : Complexe, nécessite API ou process de sync
|
||||
|
||||
### Actions en attente
|
||||
- [ ] **Réponse client** sur le processus actuel d'intégration SAP
|
||||
- [ ] Choix de la solution technique selon la réponse
|
||||
- [ ] Implémentation de la solution retenue
|
||||
- [ ] Tests de non-régression sur imports SAP
|
||||
- [ ] Documentation du processus de gestion des clients manuels
|
||||
|
||||
### Impact sur le code existant
|
||||
**Fichiers concernés** :
|
||||
- `controllers/cjxdevis.php` : fonction `save_new_client` (ligne 1308)
|
||||
- `controllers/cjximport.php` : fonction `upload_clients` (ligne 112)
|
||||
- Documentation utilisateur à mettre à jour
|
||||
|
||||
---
|
||||
|
||||
## Modification Contacts Clients - Migration vers clients.code
|
||||
|
||||
### Contexte
|
||||
La relation entre `clients_contacts` et `clients` utilise `clients.code` comme clé de référence.
|
||||
Le système a été conçu pour utiliser le `code` SAP (clé métier immuable) plutôt que le `rowid` (clé technique auto-incrémentée).
|
||||
|
||||
### Plan de correction
|
||||
|
||||
#### 1. Vérification préalable
|
||||
- [ ] Lire la structure actuelle de la table `clients`
|
||||
- [ ] Confirmer que `code` est de type INT
|
||||
- [ ] Vérifier la contrainte UNIQUE sur `code`
|
||||
- [ ] Vérifier l'index sur `code`
|
||||
- [ ] Lire la structure de `clients_contacts`
|
||||
- [ ] État actuel de `fk_client`
|
||||
- [ ] Contraintes de clé étrangère existantes
|
||||
- [ ] Vérifier les données existantes
|
||||
- [ ] Nombre de contacts déjà enregistrés
|
||||
- [ ] Cohérence des relations actuelles
|
||||
|
||||
#### 2. Modification de la structure
|
||||
- [ ] Supprimer la contrainte FK actuelle sur `clients_contacts.fk_client`
|
||||
- [ ] Modifier le type de `clients_contacts.fk_client` pour correspondre à `clients.code`
|
||||
- [ ] Ajouter la nouvelle contrainte FK référençant `clients.code`
|
||||
- [ ] `ON DELETE CASCADE`
|
||||
- [ ] `ON UPDATE CASCADE`
|
||||
- [ ] Vérifier/ajouter index UNIQUE sur `clients.code` si nécessaire
|
||||
|
||||
#### 3. Migration des données
|
||||
- [ ] Créer un script de migration SQL
|
||||
- [ ] Sauvegarder les données actuelles de `clients_contacts`
|
||||
- [ ] Convertir les `fk_client` (rowid → code)
|
||||
- [ ] Valider la cohérence des données migrées
|
||||
- [ ] Tester l'intégrité référentielle
|
||||
|
||||
#### 4. Adaptation du code applicatif
|
||||
- [x] ✅ Contrôleur `cjxcontacts.php`
|
||||
- Aucune modification nécessaire (utilise déjà `fk_client` de manière générique)
|
||||
- [x] ✅ Contrôleur `cjxdevis.php`
|
||||
- `load_clients_devis` : modifié pour retourner `clients.code`
|
||||
- `save_new_client` : modifié pour utiliser `newCode` au lieu de `newClientId`
|
||||
- [x] ✅ JavaScript `jdevis.js`
|
||||
- Fonction `autocompleteClient` : modifiée pour utiliser `list[i]['code']` au lieu de `list[i]['rowid']`
|
||||
- `loadContactsClient(list[i]['code'])` : passe maintenant le code SAP
|
||||
- [ ] **Import clients SAP** : À TRAITER EN PRIORITÉ
|
||||
- [ ] Fichier concerné : identifier le contrôleur/script d'import
|
||||
- [ ] Lors de l'import, si un client existe déjà (même `code`), mettre à jour ses infos SANS changer le `code`
|
||||
- [ ] Gérer la mise à jour des contacts : les contacts existants doivent conserver leur lien via `fk_client = code`
|
||||
- [ ] Si import d'un nouveau client : créer avec le `code` SAP fourni
|
||||
- [ ] IMPORTANT : Ne jamais modifier `clients.code` après création (immuable)
|
||||
|
||||
#### 5. Tests et validation
|
||||
- [ ] Tests de création de contact
|
||||
- [ ] Tests de modification de contact
|
||||
- [ ] Tests de suppression de contact (soft delete)
|
||||
- [ ] Tests de sélection de contact dans un devis
|
||||
- [ ] Simuler un import SAP et vérifier la stabilité des relations
|
||||
|
||||
### Notes techniques
|
||||
```sql
|
||||
-- Exemple de modification FK
|
||||
ALTER TABLE clients_contacts
|
||||
DROP FOREIGN KEY fk_clients_contacts_client;
|
||||
|
||||
ALTER TABLE clients_contacts
|
||||
ADD CONSTRAINT fk_clients_contacts_client
|
||||
FOREIGN KEY (fk_client)
|
||||
REFERENCES clients(code)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Améliorations techniques prioritaires
|
||||
|
||||
### Sécurité
|
||||
@@ -252,26 +393,15 @@ DB_PASSWORD=<PROD_PASSWORD> # À sécuriser
|
||||
|
||||
## Notes de développement
|
||||
|
||||
### Structure de la table `clients_contacts` (à créer)
|
||||
```sql
|
||||
CREATE TABLE clients_contacts (
|
||||
rowid INT PRIMARY KEY AUTO_INCREMENT,
|
||||
fk_client INT NOT NULL,
|
||||
nom VARCHAR(100),
|
||||
prenom VARCHAR(100),
|
||||
fonction VARCHAR(100),
|
||||
telephone VARCHAR(20),
|
||||
mobile VARCHAR(20),
|
||||
email VARCHAR(255),
|
||||
principal TINYINT DEFAULT 0,
|
||||
active TINYINT DEFAULT 1,
|
||||
date_creat DATETIME,
|
||||
fk_user_creat INT,
|
||||
date_modif DATETIME,
|
||||
fk_user_modif INT,
|
||||
FOREIGN KEY (fk_client) REFERENCES clients(rowid)
|
||||
);
|
||||
```
|
||||
### ✅ Structure de la table `clients_contacts` (CRÉÉE - v2.0.3)
|
||||
Table créée et opérationnelle avec :
|
||||
- Clé étrangère vers `clients` avec CASCADE
|
||||
- Gestion du contact principal (un seul par client)
|
||||
- Soft delete via champ `active`
|
||||
- Traçabilité (date_creat, fk_user_creat, date_modif, fk_user_modif)
|
||||
- Index sur fk_client, principal et email
|
||||
- Contrainte UNIQUE sur rowid
|
||||
- Voir `docs/migration_clients_contacts.sql` pour la structure complète
|
||||
|
||||
### Modifications table `devis` pour SAP
|
||||
```sql
|
||||
@@ -285,24 +415,36 @@ ALTER TABLE devis ADD COLUMN erreur_transfert_edi TEXT;
|
||||
|
||||
## Résumé de l'état actuel
|
||||
|
||||
### ✅ Réalisations (v2.0.2 - 12 septembre 2025)
|
||||
### ✅ Réalisations
|
||||
**v2.0.1 (12 septembre 2025)**
|
||||
1. **Migration DEV complétée** : Architecture séparée application/BDD
|
||||
2. **Base unique `cleo`** : Fusion réussie de 3 bases en une seule
|
||||
3. **Sécurité renforcée** : PDO, requêtes préparées, variables d'environnement
|
||||
4. **Container `dva-front`** : MariaDB supprimé, application PHP uniquement
|
||||
5. **Container `maria3`** : Base de données centralisée opérationnelle
|
||||
6. **Audit de sécurité complété** : 14 vulnérabilités SQL identifiées et corrigées
|
||||
|
||||
**v2.0.2 (12 septembre 2025)**
|
||||
1. **Audit de sécurité complété** : 14 vulnérabilités SQL identifiées et corrigées
|
||||
- 8 critiques (fonction autocomplete, injections dans cjxpost.php, mclients.php, mdevis.php)
|
||||
- 6 moyennes (cjxdevis.php, cjxexport.php, cjximport.php, mexpxls.php)
|
||||
7. **Fonctionnalité Réactivation devis** : Bouton permettant de réactiver les devis archivés (statut 20 → 1)
|
||||
2. **Fonctionnalité Réactivation devis** : Bouton permettant de réactiver les devis archivés (statut 20 → 1)
|
||||
|
||||
**v2.0.3 (21 octobre 2025)**
|
||||
1. **Gestion multi-contacts par client** : Table `clients_contacts` opérationnelle
|
||||
2. **Interface CRUD complète** : Modale Bootstrap avec création/modification/suppression de contacts
|
||||
3. **Contrôleur AJAX `cjxcontacts.php`** : 5 endpoints sécurisés avec requêtes préparées
|
||||
4. **Intégration dans les devis** : Sélecteur de contact avec affichage des infos en lecture seule
|
||||
5. **Gestion automatique du contact principal** : Un seul contact principal par client
|
||||
6. **Soft delete** : Prévention de la suppression du dernier contact actif
|
||||
|
||||
### 🎯 Prochaines étapes prioritaires
|
||||
1. **Migration PROD vers IN4** : Export/Import des containers vers `pra-front` et `maria4`
|
||||
2. **Fonctionnalités métier** : Points 14, 16 (voir sections ci-dessus)
|
||||
3. **Sécurité XSS** : Audit et correction des failles XSS potentielles
|
||||
4. **Tests** : Mise en place de tests automatisés de sécurité
|
||||
1. **Nettoyage BDD** : Supprimer les anciens champs contact de la table `clients` (après validation)
|
||||
2. **Migration PROD vers IN4** : Export/Import des containers vers `pra-front` et `maria4`
|
||||
3. **Fonctionnalités métier** : Points 8, 14, 16, 21 (voir sections ci-dessus)
|
||||
4. **Sécurité XSS** : Audit et correction des failles XSS potentielles
|
||||
5. **Tests** : Mise en place de tests automatisés de sécurité
|
||||
|
||||
---
|
||||
|
||||
*Document mis à jour le 12 septembre 2025*
|
||||
*Version 2.0.2 - Sécurité SQL complète*
|
||||
*Document mis à jour le 21 octobre 2025*
|
||||
*Version 2.0.3 - Gestion multi-contacts*
|
||||
440
docs/backpm7.sh
Normal file
440
docs/backpm7.sh
Normal 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
|
||||
@@ -1,9 +1,9 @@
|
||||
/*M!999999\- enable the sandbox mode */
|
||||
-- MariaDB dump 10.19 Distrib 10.11.9-MariaDB, for debian-linux-gnu (x86_64)
|
||||
-- MariaDB dump 10.19-11.8.3-MariaDB, for debian-linux-gnu (x86_64)
|
||||
--
|
||||
-- Host: localhost Database: uof_linet
|
||||
-- Host: localhost Database: cleo
|
||||
-- ------------------------------------------------------
|
||||
-- Server version 10.11.9-MariaDB-deb12
|
||||
-- Server version 11.4.8-MariaDB-log
|
||||
|
||||
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
|
||||
@@ -14,7 +14,7 @@
|
||||
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
|
||||
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
|
||||
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
|
||||
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
|
||||
/*M!100616 SET @OLD_NOTE_VERBOSITY=@@NOTE_VERBOSITY, NOTE_VERBOSITY=0 */;
|
||||
|
||||
--
|
||||
-- Table structure for table `clients`
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
DROP TABLE IF EXISTS `clients`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `clients` (
|
||||
`rowid` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`code` int(11) NOT NULL,
|
||||
@@ -51,9 +51,38 @@ CREATE TABLE `clients` (
|
||||
UNIQUE KEY `code_UNIQUE` (`code`),
|
||||
KEY `libelle` (`libelle`),
|
||||
KEY `cp` (`cp`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=5307 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=5309 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Table structure for table `clients_contacts`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `clients_contacts`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `clients_contacts` (
|
||||
`rowid` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`fk_client` int(11) NOT NULL,
|
||||
`nom` varchar(50) DEFAULT NULL,
|
||||
`prenom` varchar(50) DEFAULT NULL,
|
||||
`fonction` varchar(50) DEFAULT NULL,
|
||||
`telephone` varchar(20) DEFAULT NULL,
|
||||
`mobile` varchar(20) DEFAULT NULL,
|
||||
`email` varchar(75) DEFAULT NULL,
|
||||
`principal` tinyint(1) DEFAULT 0 COMMENT 'Contact principal du client',
|
||||
`active` tinyint(1) DEFAULT 1,
|
||||
`date_creat` datetime DEFAULT NULL,
|
||||
`fk_user_creat` int(11) DEFAULT NULL,
|
||||
`date_modif` datetime DEFAULT NULL,
|
||||
`fk_user_modif` int(11) DEFAULT NULL,
|
||||
PRIMARY KEY (`rowid`),
|
||||
UNIQUE KEY `rowid_UNIQUE` (`rowid`),
|
||||
KEY `fk_client` (`fk_client`),
|
||||
KEY `principal` (`fk_client`,`principal`),
|
||||
KEY `email` (`email`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=8199 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='Contacts multiples par client' `PAGE_COMPRESSED`='ON';
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Table structure for table `clients_sites`
|
||||
@@ -61,7 +90,7 @@ CREATE TABLE `clients` (
|
||||
|
||||
DROP TABLE IF EXISTS `clients_sites`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `clients_sites` (
|
||||
`rowid` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`fk_client` int(11) NOT NULL,
|
||||
@@ -77,22 +106,13 @@ CREATE TABLE `clients_sites` (
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Dumping data for table `clients_sites`
|
||||
--
|
||||
|
||||
LOCK TABLES `clients_sites` WRITE;
|
||||
/*!40000 ALTER TABLE `clients_sites` DISABLE KEYS */;
|
||||
/*!40000 ALTER TABLE `clients_sites` ENABLE KEYS */;
|
||||
UNLOCK TABLES;
|
||||
|
||||
--
|
||||
-- Table structure for table `commerciaux`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `commerciaux`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `commerciaux` (
|
||||
`rowid` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`fk_entite` int(11) DEFAULT 0,
|
||||
@@ -133,14 +153,13 @@ CREATE TABLE `commerciaux` (
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=27 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
|
||||
--
|
||||
-- Table structure for table `commerciaux_entites`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `commerciaux_entites`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `commerciaux_entites` (
|
||||
`rowid` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`libelle` varchar(45) DEFAULT NULL,
|
||||
@@ -157,26 +176,13 @@ CREATE TABLE `commerciaux_entites` (
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Dumping data for table `commerciaux_entites`
|
||||
--
|
||||
|
||||
LOCK TABLES `commerciaux_entites` WRITE;
|
||||
/*!40000 ALTER TABLE `commerciaux_entites` DISABLE KEYS */;
|
||||
INSERT INTO `commerciaux_entites` VALUES
|
||||
(1,'LINET',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,1),
|
||||
(2,'WISSNER-BOSSERHOFF',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,1),
|
||||
(3,'LINET & WI-BO',NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,1);
|
||||
/*!40000 ALTER TABLE `commerciaux_entites` ENABLE KEYS */;
|
||||
UNLOCK TABLES;
|
||||
|
||||
--
|
||||
-- Table structure for table `commerciaux_params`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `commerciaux_params`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `commerciaux_params` (
|
||||
`rowid` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`fk_commercial` int(11) DEFAULT NULL,
|
||||
@@ -269,14 +275,13 @@ CREATE TABLE `commerciaux_params` (
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=16 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
|
||||
--
|
||||
-- Table structure for table `devis`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `devis`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `devis` (
|
||||
`rowid` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`fk_user` int(11) NOT NULL DEFAULT 0,
|
||||
@@ -285,6 +290,7 @@ CREATE TABLE `devis` (
|
||||
`date_remise` date DEFAULT NULL,
|
||||
`num_opportunite` varchar(8) NOT NULL DEFAULT '',
|
||||
`fk_client` int(11) NOT NULL DEFAULT 0,
|
||||
`fk_contact` int(11) DEFAULT NULL,
|
||||
`fk_marche` int(11) NOT NULL DEFAULT 0,
|
||||
`fk_statut_devis` int(11) NOT NULL DEFAULT 0,
|
||||
`chk_clients_secteur` tinyint(1) NOT NULL DEFAULT 1,
|
||||
@@ -327,18 +333,18 @@ CREATE TABLE `devis` (
|
||||
KEY `fk_client` (`fk_client`),
|
||||
KEY `fk_statut_devis` (`fk_statut_devis`),
|
||||
KEY `date_demande` (`date_demande`),
|
||||
KEY `dossier` (`fk_user`,`dossier`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=4611 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
|
||||
KEY `dossier` (`fk_user`,`dossier`),
|
||||
KEY `fk_contact` (`fk_contact`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=4624 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
|
||||
--
|
||||
-- Table structure for table `devis_histo`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `devis_histo`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `devis_histo` (
|
||||
`rowid` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`fk_devis` int(11) DEFAULT NULL,
|
||||
@@ -350,7 +356,7 @@ CREATE TABLE `devis_histo` (
|
||||
`fk_statut_devis` int(11) DEFAULT NULL,
|
||||
PRIMARY KEY (`rowid`),
|
||||
KEY `devis_histo_fk_devis_index` (`fk_devis`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=22331 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=22388 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
@@ -359,7 +365,7 @@ CREATE TABLE `devis_histo` (
|
||||
|
||||
DROP TABLE IF EXISTS `devis_produits`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `devis_produits` (
|
||||
`rowid` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`fk_devis` int(11) NOT NULL,
|
||||
@@ -392,17 +398,16 @@ CREATE TABLE `devis_produits` (
|
||||
PRIMARY KEY (`rowid`),
|
||||
KEY `devis_produits__devis` (`fk_devis`),
|
||||
KEY `devis_produits__produit` (`fk_produit`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=29277 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=29314 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
|
||||
--
|
||||
-- Table structure for table `devis_speciaux`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `devis_speciaux`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `devis_speciaux` (
|
||||
`rowid` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`fk_devis` int(11) NOT NULL DEFAULT 0,
|
||||
@@ -461,14 +466,13 @@ CREATE TABLE `devis_speciaux` (
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
|
||||
--
|
||||
-- Table structure for table `entites`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `entites`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `entites` (
|
||||
`rowid` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`libelle` varchar(45) DEFAULT '',
|
||||
@@ -513,14 +517,13 @@ CREATE TABLE `entites` (
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
|
||||
--
|
||||
-- Table structure for table `import_ventes`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `import_ventes`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `import_ventes` (
|
||||
`rowid` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`source` varchar(15) DEFAULT '',
|
||||
@@ -570,7 +573,7 @@ CREATE TABLE `import_ventes` (
|
||||
|
||||
DROP TABLE IF EXISTS `infos`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `infos` (
|
||||
`rowid` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`date_infos` date DEFAULT NULL,
|
||||
@@ -586,14 +589,13 @@ CREATE TABLE `infos` (
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=29 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
|
||||
--
|
||||
-- Table structure for table `marches`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `marches`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `marches` (
|
||||
`rowid` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`numero` varchar(20) NOT NULL DEFAULT '',
|
||||
@@ -642,7 +644,7 @@ CREATE TABLE `marches` (
|
||||
|
||||
DROP TABLE IF EXISTS `marches_listes`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `marches_listes` (
|
||||
`rowid` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`fk_marche` int(11) DEFAULT NULL,
|
||||
@@ -660,7 +662,7 @@ CREATE TABLE `marches_listes` (
|
||||
|
||||
DROP TABLE IF EXISTS `marches_produits`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `marches_produits` (
|
||||
`rowid` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`fk_marche` int(11) DEFAULT 0,
|
||||
@@ -702,7 +704,7 @@ CREATE TABLE `marches_produits` (
|
||||
|
||||
DROP TABLE IF EXISTS `marches_versions`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `marches_versions` (
|
||||
`rowid` int(11) NOT NULL AUTO_INCREMENT COMMENT 'Id',
|
||||
`libelle` varchar(75) DEFAULT NULL COMMENT 'Libellé',
|
||||
@@ -713,24 +715,13 @@ CREATE TABLE `marches_versions` (
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='Version des marchés' `PAGE_COMPRESSED`='ON';
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Dumping data for table `marches_versions`
|
||||
--
|
||||
|
||||
LOCK TABLES `marches_versions` WRITE;
|
||||
/*!40000 ALTER TABLE `marches_versions` DISABLE KEYS */;
|
||||
INSERT INTO `marches_versions` VALUES
|
||||
(1,'Version Avril 2022','2022-04-01','0000-00-00',1);
|
||||
/*!40000 ALTER TABLE `marches_versions` ENABLE KEYS */;
|
||||
UNLOCK TABLES;
|
||||
|
||||
--
|
||||
-- Table structure for table `medias`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `medias`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `medias` (
|
||||
`rowid` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`dir0` varchar(150) DEFAULT NULL,
|
||||
@@ -746,7 +737,7 @@ CREATE TABLE `medias` (
|
||||
PRIMARY KEY (`rowid`),
|
||||
UNIQUE KEY `rowid_UNIQUE` (`rowid`),
|
||||
KEY `support` (`support`,`support_rowid`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=3866 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=3878 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
@@ -755,7 +746,7 @@ CREATE TABLE `medias` (
|
||||
|
||||
DROP TABLE IF EXISTS `notifications`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `notifications` (
|
||||
`rowid` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`dateheure` datetime DEFAULT NULL,
|
||||
@@ -775,7 +766,7 @@ CREATE TABLE `notifications` (
|
||||
|
||||
DROP TABLE IF EXISTS `produits`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `produits` (
|
||||
`rowid` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`fk_marche` int(11) NOT NULL DEFAULT 0,
|
||||
@@ -821,7 +812,7 @@ CREATE TABLE `produits` (
|
||||
|
||||
DROP TABLE IF EXISTS `produits_familles`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `produits_familles` (
|
||||
`rowid` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`groupe` varchar(30) NOT NULL,
|
||||
@@ -841,7 +832,7 @@ CREATE TABLE `produits_familles` (
|
||||
|
||||
DROP TABLE IF EXISTS `regions`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `regions` (
|
||||
`rowid` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`libelle` varchar(75) DEFAULT NULL,
|
||||
@@ -851,22 +842,13 @@ CREATE TABLE `regions` (
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Dumping data for table `regions`
|
||||
--
|
||||
|
||||
LOCK TABLES `regions` WRITE;
|
||||
/*!40000 ALTER TABLE `regions` DISABLE KEYS */;
|
||||
/*!40000 ALTER TABLE `regions` ENABLE KEYS */;
|
||||
UNLOCK TABLES;
|
||||
|
||||
--
|
||||
-- Table structure for table `simul`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `simul`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `simul` (
|
||||
`rowid` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`fk_import_vente` int(11) DEFAULT NULL,
|
||||
@@ -885,14 +867,13 @@ CREATE TABLE `simul` (
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=1057 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
|
||||
--
|
||||
-- Table structure for table `users`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `users`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `users` (
|
||||
`rowid` int(10) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`fk_entite` int(11) DEFAULT NULL,
|
||||
@@ -940,13 +921,64 @@ CREATE TABLE `users` (
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=50 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Table structure for table `users_entites`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `users_entites`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `users_entites` (
|
||||
`rowid` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`libelle` varchar(45) DEFAULT '',
|
||||
`http_host` varchar(150) DEFAULT '',
|
||||
`adresse1` varchar(45) DEFAULT '',
|
||||
`adresse2` varchar(45) DEFAULT '',
|
||||
`cp` varchar(5) DEFAULT '',
|
||||
`ville` varchar(45) DEFAULT '',
|
||||
`type_entite` varchar(5) DEFAULT 'form',
|
||||
`tva_intra` varchar(15) DEFAULT '',
|
||||
`rcs` varchar(45) DEFAULT '',
|
||||
`siret` varchar(17) DEFAULT NULL,
|
||||
`ape` varchar(5) DEFAULT '',
|
||||
`num_opca` varchar(15) DEFAULT '',
|
||||
`logo` varchar(45) DEFAULT '',
|
||||
`tel1` varchar(20) DEFAULT '',
|
||||
`tel2` varchar(20) DEFAULT '',
|
||||
`couleur` varchar(7) DEFAULT '#FFFAF0',
|
||||
`prefecture` varchar(45) DEFAULT 'Bretagne',
|
||||
`fk_titre_gerant` int(11) DEFAULT 1,
|
||||
`gerant_prenom` varchar(45) DEFAULT '',
|
||||
`gerant_nom` varchar(45) DEFAULT '',
|
||||
`email` varchar(45) DEFAULT '',
|
||||
`site_url` varchar(45) DEFAULT '',
|
||||
`gerant_signature` varchar(45) DEFAULT '',
|
||||
`tampon_signature` varchar(45) DEFAULT '',
|
||||
`rib_banque` varchar(5) DEFAULT '',
|
||||
`rib_guichet` varchar(5) DEFAULT '',
|
||||
`rib_compte` varchar(11) DEFAULT '',
|
||||
`rib_cle` varchar(2) DEFAULT '',
|
||||
`rib_domiciliation` varchar(45) DEFAULT '',
|
||||
`iban` varchar(33) DEFAULT '',
|
||||
`bic` varchar(15) DEFAULT '',
|
||||
`demo` tinyint(1) DEFAULT 0,
|
||||
`genbase` varchar(45) DEFAULT '0',
|
||||
`groupebase` varchar(45) DEFAULT '0',
|
||||
`table_users_gen` varchar(50) DEFAULT '',
|
||||
`appname` varchar(45) DEFAULT '',
|
||||
`raz_num_devis` tinyint(1) DEFAULT 0,
|
||||
`active` tinyint(1) DEFAULT 1,
|
||||
PRIMARY KEY (`rowid`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci `PAGE_COMPRESSED`='ON';
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Table structure for table `ventes`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `ventes`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `ventes` (
|
||||
`rowid` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`source` varchar(45) DEFAULT NULL,
|
||||
@@ -977,22 +1009,13 @@ CREATE TABLE `ventes` (
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Dumping data for table `ventes`
|
||||
--
|
||||
|
||||
LOCK TABLES `ventes` WRITE;
|
||||
/*!40000 ALTER TABLE `ventes` DISABLE KEYS */;
|
||||
/*!40000 ALTER TABLE `ventes` ENABLE KEYS */;
|
||||
UNLOCK TABLES;
|
||||
|
||||
--
|
||||
-- Table structure for table `x_clients_types`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `x_clients_types`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `x_clients_types` (
|
||||
`rowid` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`code` char(3) DEFAULT NULL,
|
||||
@@ -1004,28 +1027,13 @@ CREATE TABLE `x_clients_types` (
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Dumping data for table `x_clients_types`
|
||||
--
|
||||
|
||||
LOCK TABLES `x_clients_types` WRITE;
|
||||
/*!40000 ALTER TABLE `x_clients_types` DISABLE KEYS */;
|
||||
INSERT INTO `x_clients_types` VALUES
|
||||
(1,'PUB','Public',1),
|
||||
(2,'PRA','Privé Associatif',1),
|
||||
(3,'PRD','Privé Distributeur',1),
|
||||
(4,'PRC','Privé Commercial',1),
|
||||
(5,'ESP','ESPIC',1);
|
||||
/*!40000 ALTER TABLE `x_clients_types` ENABLE KEYS */;
|
||||
UNLOCK TABLES;
|
||||
|
||||
--
|
||||
-- Table structure for table `x_familles`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `x_familles`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `x_familles` (
|
||||
`rowid` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`libelle` varchar(20) NOT NULL DEFAULT '',
|
||||
@@ -1037,34 +1045,13 @@ CREATE TABLE `x_familles` (
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Dumping data for table `x_familles`
|
||||
--
|
||||
|
||||
LOCK TABLES `x_familles` WRITE;
|
||||
/*!40000 ALTER TABLE `x_familles` DISABLE KEYS */;
|
||||
INSERT INTO `x_familles` VALUES
|
||||
(3,'Lits SBU1',1,1),
|
||||
(4,'Lits SBU2',2,1),
|
||||
(5,'Accessoires SBU1',3,1),
|
||||
(6,'Accessoires SBU2',4,1),
|
||||
(7,'Services',5,1),
|
||||
(8,'Matelas mousse',6,1),
|
||||
(9,'Matelas à air',7,1),
|
||||
(10,'Mobilier',8,1),
|
||||
(11,'Assises',9,1),
|
||||
(12,'Autres',11,1),
|
||||
(13,'Domalys',10,1);
|
||||
/*!40000 ALTER TABLE `x_familles` ENABLE KEYS */;
|
||||
UNLOCK TABLES;
|
||||
|
||||
--
|
||||
-- Table structure for table `x_regions`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `x_regions`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `x_regions` (
|
||||
`rowid` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`fk_entite` int(11) DEFAULT 0,
|
||||
@@ -1075,39 +1062,13 @@ CREATE TABLE `x_regions` (
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=20 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Dumping data for table `x_regions`
|
||||
--
|
||||
|
||||
LOCK TABLES `x_regions` WRITE;
|
||||
/*!40000 ALTER TABLE `x_regions` DISABLE KEYS */;
|
||||
INSERT INTO `x_regions` VALUES
|
||||
(1,1,'SUD-OUEST',1),
|
||||
(2,1,'RHONE-ALPES / AUVERGNE',1),
|
||||
(3,1,'PACA',1),
|
||||
(4,1,'EST',1),
|
||||
(5,1,'NORD',1),
|
||||
(6,1,'GRAND-OUEST',1),
|
||||
(7,1,'IDF',1),
|
||||
(8,1,'DOM-TOM',1),
|
||||
(9,2,'WB-NORD',1),
|
||||
(13,2,'WB-SUD OUEST',1),
|
||||
(14,2,'WB-EST',1),
|
||||
(15,2,'WB-ILE DE FRANCE',1),
|
||||
(16,2,'WB-SUD-EST',1),
|
||||
(17,2,'WB-CENTRE-EST ET DOM',1),
|
||||
(18,1,'DIRECTION',1),
|
||||
(19,2,'WB-NORD OUEST',1);
|
||||
/*!40000 ALTER TABLE `x_regions` ENABLE KEYS */;
|
||||
UNLOCK TABLES;
|
||||
|
||||
--
|
||||
-- Table structure for table `x_roles`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `x_roles`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `x_roles` (
|
||||
`rowid` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`libelle` varchar(45) DEFAULT '',
|
||||
@@ -1118,30 +1079,13 @@ CREATE TABLE `x_roles` (
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=91 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='Les différents rôles des utilisateurs' `PAGE_COMPRESSED`='ON';
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Dumping data for table `x_roles`
|
||||
--
|
||||
|
||||
LOCK TABLES `x_roles` WRITE;
|
||||
/*!40000 ALTER TABLE `x_roles` DISABLE KEYS */;
|
||||
INSERT INTO `x_roles` VALUES
|
||||
(1,'Direction Commerciale','DC',1),
|
||||
(2,'Direction des Ventes','DV',1),
|
||||
(3,'Commercial(e)','RR',1),
|
||||
(4,'Clinicien(ne)','CL',1),
|
||||
(5,'Direction Grands Comptes','GC',1),
|
||||
(20,'Administration des ventes','ADV',1),
|
||||
(90,'Administrateur','ADM',1);
|
||||
/*!40000 ALTER TABLE `x_roles` ENABLE KEYS */;
|
||||
UNLOCK TABLES;
|
||||
|
||||
--
|
||||
-- Table structure for table `x_statuts_devis`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `x_statuts_devis`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `x_statuts_devis` (
|
||||
`rowid` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`libelle` varchar(30) DEFAULT NULL,
|
||||
@@ -1152,22 +1096,47 @@ CREATE TABLE `x_statuts_devis` (
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Dumping data for table `x_statuts_devis`
|
||||
-- Table structure for table `y_pages`
|
||||
--
|
||||
|
||||
LOCK TABLES `x_statuts_devis` WRITE;
|
||||
/*!40000 ALTER TABLE `x_statuts_devis` DISABLE KEYS */;
|
||||
INSERT INTO `x_statuts_devis` VALUES
|
||||
(1,'En cours de création',1),
|
||||
(2,'En cours de validation DIR-CO',1),
|
||||
(3,'En cours de validation DV/DGC',1),
|
||||
(4,'A traiter sur SAP',1),
|
||||
(6,'A vérifier par le RR',1),
|
||||
(7,'A envoyer au client',1),
|
||||
(10,'Envoyé au client',0),
|
||||
(20,'Archivé',1);
|
||||
/*!40000 ALTER TABLE `x_statuts_devis` ENABLE KEYS */;
|
||||
UNLOCK TABLES;
|
||||
DROP TABLE IF EXISTS `y_pages`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `y_pages` (
|
||||
`rowid` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`fk_parent` int(11) DEFAULT 0,
|
||||
`link` varchar(75) DEFAULT NULL,
|
||||
`libelle` varchar(45) DEFAULT NULL,
|
||||
`titre` varchar(75) DEFAULT NULL,
|
||||
`tooltip` varchar(45) DEFAULT NULL,
|
||||
`description` varchar(200) DEFAULT NULL,
|
||||
`keywords` varchar(200) DEFAULT NULL,
|
||||
`script` varchar(45) DEFAULT NULL,
|
||||
`enmaintenance` tinyint(1) DEFAULT 0 COMMENT '0 libre d''accès, 1 en maintenance mais accès aux données, 2 en maintenance sans accès aux données',
|
||||
`admin` tinyint(1) DEFAULT 0,
|
||||
`mail` tinyint(1) DEFAULT 0,
|
||||
`admtools` tinyint(1) DEFAULT 0,
|
||||
`magazine` tinyint(1) DEFAULT 0,
|
||||
`files` tinyint(1) DEFAULT 0,
|
||||
`editor` tinyint(1) DEFAULT 0,
|
||||
`autocomplete` tinyint(1) DEFAULT 0,
|
||||
`print` tinyint(1) DEFAULT 0,
|
||||
`form` tinyint(1) DEFAULT 0,
|
||||
`sidebar` tinyint(1) DEFAULT 0,
|
||||
`chart` tinyint(1) DEFAULT 0,
|
||||
`agenda` tinyint(1) DEFAULT 0,
|
||||
`scheduler` tinyint(1) DEFAULT 0,
|
||||
`osm` tinyint(1) DEFAULT 0,
|
||||
`layout` varchar(45) DEFAULT 'default.php',
|
||||
`in_menu` tinyint(1) DEFAULT 1,
|
||||
`ordre_menu` int(11) DEFAULT 0,
|
||||
`active` tinyint(1) DEFAULT 1,
|
||||
PRIMARY KEY (`rowid`),
|
||||
UNIQUE KEY `rowid_UNIQUE` (`rowid`),
|
||||
KEY `script` (`script`),
|
||||
KEY `admin` (`admin`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=30 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci `PAGE_COMPRESSED`='ON';
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Table structure for table `z_history`
|
||||
@@ -1175,7 +1144,7 @@ UNLOCK TABLES;
|
||||
|
||||
DROP TABLE IF EXISTS `z_history`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `z_history` (
|
||||
`fk_user` int(11) NOT NULL,
|
||||
`libelle` varchar(20) NOT NULL DEFAULT 'tiers',
|
||||
@@ -1185,22 +1154,13 @@ CREATE TABLE `z_history` (
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Dumping data for table `z_history`
|
||||
--
|
||||
|
||||
LOCK TABLES `z_history` WRITE;
|
||||
/*!40000 ALTER TABLE `z_history` DISABLE KEYS */;
|
||||
/*!40000 ALTER TABLE `z_history` ENABLE KEYS */;
|
||||
UNLOCK TABLES;
|
||||
|
||||
--
|
||||
-- Table structure for table `z_logs`
|
||||
--
|
||||
|
||||
DROP TABLE IF EXISTS `z_logs`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `z_logs` (
|
||||
`date` datetime NOT NULL,
|
||||
`ip` varchar(15) NOT NULL,
|
||||
@@ -1220,7 +1180,7 @@ CREATE TABLE `z_logs` (
|
||||
|
||||
DROP TABLE IF EXISTS `z_sessions`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `z_sessions` (
|
||||
`sid` text NOT NULL,
|
||||
`fk_user` int(11) NOT NULL,
|
||||
@@ -1238,7 +1198,7 @@ CREATE TABLE `z_sessions` (
|
||||
|
||||
DROP TABLE IF EXISTS `z_stats`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!40101 SET character_set_client = utf8 */;
|
||||
/*!40101 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `z_stats` (
|
||||
`rowid` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`libelle` varchar(75) DEFAULT NULL,
|
||||
@@ -1253,6 +1213,10 @@ CREATE TABLE `z_stats` (
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci `PAGE_COMPRESSED`='ON';
|
||||
/*!40101 SET character_set_client = @saved_cs_client */;
|
||||
|
||||
--
|
||||
-- Dumping routines for database 'cleo'
|
||||
--
|
||||
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
|
||||
|
||||
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
|
||||
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
|
||||
@@ -1260,6 +1224,6 @@ CREATE TABLE `z_stats` (
|
||||
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
|
||||
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
|
||||
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
|
||||
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
|
||||
/*M!100616 SET NOTE_VERBOSITY=@OLD_NOTE_VERBOSITY */;
|
||||
|
||||
-- Dump completed on 2025-09-11 14:44:41
|
||||
-- Dump completed on 2025-12-02 11:57:09
|
||||
8977
docs/listes tarifaires générales_160725.csv
Normal file
8977
docs/listes tarifaires générales_160725.csv
Normal file
File diff suppressed because it is too large
Load Diff
143
docs/migration_clients_contacts.sql
Normal file
143
docs/migration_clients_contacts.sql
Normal 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
|
||||
-- ============================================================================
|
||||
177274
docs/uof_linet_20250911.sql
177274
docs/uof_linet_20250911.sql
File diff suppressed because one or more lines are too long
@@ -21,11 +21,8 @@
|
||||
OR c.contact_fonction LIKE :search
|
||||
ORDER BY c.libelle';
|
||||
|
||||
$stmt = $db->prepare($sql);
|
||||
$searchParam = '%' . $search . '%';
|
||||
$stmt->bindParam(':search', $searchParam, PDO::PARAM_STR);
|
||||
$stmt->execute();
|
||||
$aModel["clients"] = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
$aModel["clients"] = $db->fetchAll($sql, [':search' => $searchParam]);
|
||||
} catch (Exception $e) {
|
||||
error_log("Erreur recherche clients : " . $e->getMessage());
|
||||
$aModel["clients"] = [];
|
||||
|
||||
@@ -17,9 +17,7 @@ switch ($fkRole) {
|
||||
try {
|
||||
$db = Database::getInstance();
|
||||
$sql = 'SELECT rowid FROM users WHERE fk_parent = :fkParent';
|
||||
$stmt = $db->prepare($sql);
|
||||
$stmt->execute([':fkParent' => $fkUser]);
|
||||
$aRR = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
$aRR = $db->fetchAll($sql, [':fkParent' => $fkUser]);
|
||||
|
||||
$rrIds = array_column($aRR, 'rowid');
|
||||
if (!empty($rrIds)) {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<?php
|
||||
global $Route;
|
||||
|
||||
function cleanData(&$str)
|
||||
{
|
||||
function cleanData(&$str) {
|
||||
// Fonction de nettoyage des données pour l'export Excel
|
||||
if ($str == 't') $str = 'TRUE';
|
||||
if ($str == 'f') $str = 'FALSE';
|
||||
@@ -13,8 +12,7 @@ function cleanData(&$str)
|
||||
$str = mb_convert_encoding($str, 'UTF-16LE', 'UTF-8');
|
||||
}
|
||||
|
||||
function filterData(&$str)
|
||||
{
|
||||
function filterData(&$str) {
|
||||
$str = preg_replace("/\t/", "\\t", $str);
|
||||
$str = preg_replace("/\r?\n/", "\\n", $str);
|
||||
if ($str == 't') $str = 'TRUE';
|
||||
@@ -57,12 +55,18 @@ switch ($Route->_action) {
|
||||
|
||||
case "export_sap_devis":
|
||||
$cid = nettoie_input($Route->_param1);
|
||||
|
||||
$cidSafe = intval($cid);
|
||||
$sql = 'SELECT d.* FROM devis d WHERE d.rowid=' . $cidSafe . ';';
|
||||
eLog("Export Excel SAP Devis : " . $sql);
|
||||
$dev = getinfos($sql, "gen");
|
||||
$devis = $dev[0];
|
||||
|
||||
try {
|
||||
$db = Database::getInstance();
|
||||
$sql = 'SELECT d.* FROM devis d WHERE d.rowid = :devis_id';
|
||||
$devis = $db->fetchOne($sql, [':devis_id' => $cidSafe]);
|
||||
|
||||
if (!$devis) {
|
||||
throw new Exception("Devis non trouvé");
|
||||
}
|
||||
|
||||
eLog("Export Excel SAP Devis : " . $cidSafe);
|
||||
|
||||
$fileName = "devis_" . $cid . "_" . date('Y_m_d_hi') . ".xls";
|
||||
|
||||
@@ -71,48 +75,54 @@ switch ($Route->_action) {
|
||||
$excelData = implode("\t", array_values($fields)) . "\n";
|
||||
|
||||
$fkClientSafe = intval($devis["fk_client"]);
|
||||
$sql = 'SELECT c.code, c.libelle, c.adresse1, c.adresse2, c.adresse3, c.cp, c.ville FROM clients c WHERE c.rowid=' . $fkClientSafe . ';';
|
||||
$cli = getinfos($sql, "gen");
|
||||
if (count($cli) == 0) {
|
||||
// c'est un nouveau client, on affiche les données client enregistrées dans le devis
|
||||
$sql = 'SELECT "0" AS code, d.lib_new_client, d.adresse1_new_client, d.adresse2_new_client, d.adresse3_new_client, d.cp_new_client, d.ville_new_client FROM devis d WHERE d.rowid=' . $cidSafe . ';';
|
||||
$cli = getinfos($sql, "gen");
|
||||
$client = $cli[0];
|
||||
|
||||
if ($fkClientSafe == 0) {
|
||||
// Nouveau client : données depuis le devis
|
||||
$sql = 'SELECT "0" AS code, d.lib_new_client, d.adresse1_new_client, d.adresse2_new_client, d.adresse3_new_client, d.cp_new_client, d.ville_new_client FROM devis d WHERE d.rowid = :devis_id';
|
||||
$client = $db->fetchOne($sql, [':devis_id' => $cidSafe]);
|
||||
|
||||
array_walk($client, 'filterData');
|
||||
$excelData .= implode("\t", array_values($client)) . "\n";
|
||||
|
||||
// une ligne vierge de séparation
|
||||
$excelData .= "\n";
|
||||
|
||||
// Les données du contact à prendre aussi dans le devis
|
||||
$sql = 'SELECT d.contact_new_nom, d.contact_new_prenom, d.contact_new_fonction, d.new_telephone, d.new_mobile, d.new_email FROM devis d WHERE d.rowid=' . $cidSafe . ';';
|
||||
$cont = getinfos($sql, "gen");
|
||||
$contact = $cont[0];
|
||||
// Contact depuis le devis
|
||||
$sql = 'SELECT d.contact_new_nom, d.contact_new_prenom, d.contact_new_fonction, d.new_telephone, d.new_mobile, d.new_email FROM devis d WHERE d.rowid = :devis_id';
|
||||
$contact = $db->fetchOne($sql, [':devis_id' => $cidSafe]);
|
||||
|
||||
$fields = array("Contact Nom", "Prenom", "Fonction", "Fixe", "Mobile", "Email");
|
||||
$excelData .= implode("\t", array_values($fields)) . "\n";
|
||||
|
||||
array_walk($contact, 'filterData');
|
||||
$excelData .= implode("\t", array_values($contact)) . "\n";
|
||||
|
||||
} else {
|
||||
$client = $cli[0];
|
||||
// Client existant : données depuis la table clients
|
||||
$sql = 'SELECT c.code, c.libelle, c.adresse1, c.adresse2, c.adresse3, c.cp, c.ville FROM clients c WHERE c.rowid = :client_id';
|
||||
$client = $db->fetchOne($sql, [':client_id' => $fkClientSafe]);
|
||||
|
||||
array_walk($client, 'filterData');
|
||||
$excelData .= implode("\t", array_values($client)) . "\n";
|
||||
|
||||
// une ligne vierge de séparation
|
||||
$excelData .= "\n";
|
||||
|
||||
// Les données du contact
|
||||
$sql = 'SELECT c.contact_nom, c.contact_prenom, c.contact_fonction, c.telephone, c.mobile, c.email FROM clients c WHERE c.rowid=' . $fkClientSafe . ';';
|
||||
$cont = getinfos($sql, "gen");
|
||||
$contact = $cont[0];
|
||||
// Contact lié au devis via devis.fk_contact et clients_contacts
|
||||
$fkContactSafe = intval($devis["fk_contact"]);
|
||||
if ($fkContactSafe > 0) {
|
||||
$sql = 'SELECT cc.nom, cc.prenom, cc.fonction, cc.telephone, cc.mobile, cc.email FROM clients_contacts cc WHERE cc.rowid = :contact_id AND cc.active = 1';
|
||||
$contact = $db->fetchOne($sql, [':contact_id' => $fkContactSafe]);
|
||||
} else {
|
||||
// Fallback : contact principal du client
|
||||
$sql = 'SELECT cc.nom, cc.prenom, cc.fonction, cc.telephone, cc.mobile, cc.email FROM clients_contacts cc WHERE cc.fk_client = :client_id AND cc.principal = 1 AND cc.active = 1 LIMIT 1';
|
||||
$contact = $db->fetchOne($sql, [':client_id' => $fkClientSafe]);
|
||||
}
|
||||
|
||||
if ($contact) {
|
||||
$fields = array("Contact Nom", "Prenom", "Fonction", "Fixe", "Mobile", "Email");
|
||||
$excelData .= implode("\t", array_values($fields)) . "\n";
|
||||
|
||||
array_walk($contact, 'filterData');
|
||||
$excelData .= implode("\t", array_values($contact)) . "\n";
|
||||
} else {
|
||||
// Aucun contact trouvé
|
||||
$excelData .= "Contact Nom\tPrenom\tFonction\tFixe\tMobile\tEmail\n";
|
||||
$excelData .= "\t\t\t\t\t\n";
|
||||
}
|
||||
}
|
||||
// une ligne vierge de séparation
|
||||
$excelData .= "\n";
|
||||
@@ -121,30 +131,24 @@ switch ($Route->_action) {
|
||||
$sql = 'SELECT d.rowid, d.num_opportunite, IF(d.date_demande IS NULL OR d.date_demande="0000-00-00", "", DATE_FORMAT(d.date_demande, "%d/%m/%Y")) AS datedem, ';
|
||||
$sql .= 'IF(d.date_remise IS NULL OR d.date_remise="0000-00-00", "", DATE_FORMAT(d.date_remise, "%d/%m/%Y")) AS daterem, m.libelle AS lib_marche, m.numero AS num_marche, m.nom AS nom_marche, ';
|
||||
$sql .= 'IF(d.chk_devis_photos=1, "Oui", "Non") AS photos, d.commentaire, IF(d.chk_speciaux=1, "Oui", "Non") AS speciaux ';
|
||||
$sql .= 'FROM devis d LEFT JOIN marches m ON d.fk_marche=m.rowid WHERE d.rowid=' . $cidSafe . ';';
|
||||
$dev = getinfos($sql, "gen");
|
||||
$devis = $dev[0];
|
||||
$chkSpeciaux = $devis["speciaux"];
|
||||
$sql .= 'FROM devis d LEFT JOIN marches m ON d.fk_marche=m.rowid WHERE d.rowid = :devis_id';
|
||||
$devisData = $db->fetchOne($sql, [':devis_id' => $cidSafe]);
|
||||
$chkSpeciaux = $devisData["speciaux"];
|
||||
|
||||
$fields = array("Devis", "Opportunite", "Date Demande", "Date remise client", "Marche", "Num Marche", "Nom Marche", "Avec photos", "Commentaire RR", "Speciaux");
|
||||
$excelData .= implode("\t", array_values($fields)) . "\n";
|
||||
|
||||
array_walk($devis, 'filterData');
|
||||
$excelData .= implode("\t", array_values($devis)) . "\n";
|
||||
|
||||
// une ligne vierge de séparation
|
||||
array_walk($devisData, 'filterData');
|
||||
$excelData .= implode("\t", array_values($devisData)) . "\n";
|
||||
$excelData .= "\n";
|
||||
|
||||
// on affiche les totaux du devis
|
||||
$sql = 'SELECT d.montant_total_ht, d.montant_total_ht_remise, d.marge_totale FROM devis d WHERE d.rowid=' . $cidSafe . ';';
|
||||
$dev = getinfos($sql, "gen");
|
||||
$totaux = $dev[0];
|
||||
$sql = 'SELECT d.montant_total_ht, d.montant_total_ht_remise, d.marge_totale FROM devis d WHERE d.rowid = :devis_id';
|
||||
$totaux = $db->fetchOne($sql, [':devis_id' => $cidSafe]);
|
||||
|
||||
$fields = array("Total HT", "Total HT Remise", "Marge Totale");
|
||||
$excelData .= implode("\t", array_values($fields)) . "\n";
|
||||
|
||||
array_walk($totaux, 'filterData');
|
||||
$excelData .= implode("\t", array_values($totaux)) . "\n";
|
||||
|
||||
// une ligne vierge de séparation
|
||||
$excelData .= "\n";
|
||||
|
||||
// on affiche les produits
|
||||
@@ -155,8 +159,8 @@ switch ($Route->_action) {
|
||||
$sql .= 'LEFT JOIN produits p ON dp.fk_produit=p.rowid ';
|
||||
$sql .= 'LEFT JOIN produits_familles pf ON p.groupe=pf.groupe ';
|
||||
$sql .= 'LEFT JOIN x_familles xf ON pf.fk_famille=xf.rowid ';
|
||||
$sql .= 'WHERE dp.fk_devis=' . $cidSafe . ' ORDER BY dp.ordre, xf.ordre, p.libelle;';
|
||||
$data = getinfos($sql, "gen");
|
||||
$sql .= 'WHERE dp.fk_devis = :devis_id ORDER BY dp.ordre, xf.ordre, p.libelle';
|
||||
$data = $db->fetchAll($sql, [':devis_id' => $cidSafe]);
|
||||
|
||||
$fields = array("Code", "Designation", "Prix Vente", "Quantite", "Remise", "PU vente avec remise", "Total HT", "Marge", "Commentaire");
|
||||
$excelData .= implode("\t", array_values($fields)) . "\n";
|
||||
@@ -167,22 +171,19 @@ switch ($Route->_action) {
|
||||
}
|
||||
|
||||
if ($chkSpeciaux == "Oui") {
|
||||
// une ligne vierge de séparation
|
||||
$excelData .= "\n";
|
||||
$excelData .= "----" . "\n";
|
||||
$excelData .= "PRODUITS SPECIAUX" . "\n";
|
||||
$excelData .= "----" . "\n";
|
||||
|
||||
$sql = 'SELECT IF(ds.chk_livr_multi=1, "Oui", "Non") AS livr_multi, ds.nb_livr, DATE_FORMAT(ds.date_livr_1, "%d/%m/%Y") AS datelivr ';
|
||||
$sql .= 'FROM devis_speciaux ds WHERE ds.fk_devis=' . $cidSafe . ';';
|
||||
$spec = getinfos($sql, "gen");
|
||||
$speciaux = $spec[0];
|
||||
$sql .= 'FROM devis_speciaux ds WHERE ds.fk_devis = :devis_id';
|
||||
$speciaux = $db->fetchOne($sql, [':devis_id' => $cidSafe]);
|
||||
|
||||
$fields = array("Livraisons multiples", "Nbre livraisons", "Date 1ere livraison");
|
||||
$excelData .= implode("\t", array_values($fields)) . "\n";
|
||||
|
||||
array_walk($speciaux, 'filterData');
|
||||
$excelData .= implode("\t", array_values($speciaux)) . "\n";
|
||||
|
||||
$excelData .= "\n";
|
||||
|
||||
$fields = array("#", "Code", "Designation", "Quantite", "Surcout", "Echantillon", "Date echantillon", "Concurrent", "Description");
|
||||
@@ -191,29 +192,32 @@ switch ($Route->_action) {
|
||||
for ($i = 1; $i <= 5; $i++) {
|
||||
$sql = 'SELECT ds.fk_produit_' . $i . ', ds.code_produit_' . $i . ', ds.lib_produit_' . $i . ', ds.qte_' . $i . ', IF(ds.surcout_' . $i . '=0, "", FORMAT(ds.surcout_' . $i . ', 2, "fr_FR")), IF(ds.chk_echantillon_' . $i . '=1, "Oui", "Non") AS echantillon, ';
|
||||
$sql .= 'DATE_FORMAT(ds.date_echantillon_' . $i . ', "%d/%m/%Y") AS date_ech, ds.lib_concurrent_' . $i . ', ds.description_' . $i . ' ';
|
||||
$sql .= 'FROM devis_speciaux ds WHERE ds.fk_devis=' . $cidSafe . ';';
|
||||
eLog($sql, "sql");
|
||||
$spec = getinfos($sql, "gen");
|
||||
$speciaux = $spec[0];
|
||||
$sql .= 'FROM devis_speciaux ds WHERE ds.fk_devis = :devis_id';
|
||||
|
||||
if ($speciaux["fk_produit_" . $i] > 0) {
|
||||
array_walk($speciaux, 'filterData');
|
||||
$excelData .= implode("\t", array_values($speciaux)) . "\n";
|
||||
$produitSpecial = $db->fetchOne($sql, [':devis_id' => $cidSafe]);
|
||||
|
||||
if ($produitSpecial && $produitSpecial["fk_produit_" . $i] > 0) {
|
||||
array_walk($produitSpecial, 'filterData');
|
||||
$excelData .= implode("\t", array_values($produitSpecial)) . "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// une ligne vierge de séparation
|
||||
$excelData .= "\n";
|
||||
$excelData .= "----" . "\n";
|
||||
$excelData .= "FIN DU DEVIS" . "\n";
|
||||
$excelData .= "----" . "\n";
|
||||
$excelData .= "\n";
|
||||
|
||||
header('Content-Type: application/vnd.ms-excel; charset=utf-16le');
|
||||
header("Content-type: application/x-msexcel; charset=utf-16le");
|
||||
header('Content-Disposition: attachment; filename="' . $fileName . '"');
|
||||
header('Cache-Control: max-age=0');
|
||||
echo $excelData;
|
||||
} catch (Exception $e) {
|
||||
error_log("Erreur export Excel : " . $e->getMessage());
|
||||
http_response_code(500);
|
||||
echo "Erreur lors de l'export du devis";
|
||||
}
|
||||
exit();
|
||||
|
||||
}
|
||||
@@ -5,9 +5,10 @@ $aModel = array();
|
||||
$sql = 'SELECT m.* FROM medias m WHERE m.support="devis_pdf_sap" ORDER BY m.support_rowid;';
|
||||
$aModel["medias"] = getinfos($sql, "gen");
|
||||
|
||||
$sql = 'SELECT d.*, c.libelle, c.adresse1, c.adresse2, c.adresse3, c.code, c.cp, c.ville, c.email, u.libelle as nom, u.prenom, s.libelle as lib_statut, m.libelle as lib_marche ';
|
||||
$sql = 'SELECT d.*, c.libelle, c.adresse1, c.adresse2, c.adresse3, c.code, c.cp, c.ville, cc.email, u.libelle as nom, u.prenom, s.libelle as lib_statut, m.libelle as lib_marche ';
|
||||
$sql .= 'FROM devis d ';
|
||||
$sql .= 'LEFT JOIN clients c on d.fk_client = c.rowid ';
|
||||
$sql .= 'LEFT JOIN clients_contacts cc ON d.fk_contact = cc.rowid ';
|
||||
$sql .= 'LEFT JOIN users u ON d.fk_user = u.rowid ';
|
||||
$sql .= 'LEFT JOIN marches m ON d.fk_marche = m.rowid ';
|
||||
$sql .= 'LEFT JOIN x_statuts_devis s ON d.fk_statut_devis = s.rowid ';
|
||||
@@ -24,9 +25,10 @@ foreach ($aModel["devisEnCours"] as $devis) {
|
||||
}
|
||||
}
|
||||
|
||||
$sql = 'SELECT d.*, c.libelle, c.adresse1, c.adresse2, c.adresse3, c.code, c.cp, c.ville, u.libelle as nom, u.prenom, s.libelle as lib_statut, m.libelle as lib_marche ';
|
||||
$sql = 'SELECT d.*, c.libelle, c.adresse1, c.adresse2, c.adresse3, c.code, c.cp, c.ville, cc.email, u.libelle as nom, u.prenom, s.libelle as lib_statut, m.libelle as lib_marche ';
|
||||
$sql .= 'FROM devis d ';
|
||||
$sql .= 'LEFT JOIN clients c on d.fk_client = c.rowid ';
|
||||
$sql .= 'LEFT JOIN clients_contacts cc ON d.fk_contact = cc.rowid ';
|
||||
$sql .= 'LEFT JOIN users u ON d.fk_user = u.rowid ';
|
||||
$sql .= 'LEFT JOIN marches m ON d.fk_marche = m.rowid ';
|
||||
$sql .= 'LEFT JOIN x_statuts_devis s ON d.fk_statut_devis = s.rowid ';
|
||||
|
||||
@@ -254,3 +254,105 @@ function timeEnd($start, $label = '') {
|
||||
|
||||
return $time;
|
||||
}
|
||||
|
||||
function loadtel($numero, $prefix = "+33") {
|
||||
$lenumero = trim($numero);
|
||||
$lenumero = preg_replace('/[^0-9]/', '', $lenumero);
|
||||
if (strlen($lenumero) == 10) {
|
||||
$lenumero = substr($lenumero, 1);
|
||||
}
|
||||
if (strlen($lenumero) == 9) {
|
||||
$lenumero = $prefix . $lenumero;
|
||||
}
|
||||
return $lenumero;
|
||||
}
|
||||
|
||||
function formattel($numero, $separateur = " ") {
|
||||
if (strlen($numero) == 9) {
|
||||
$numero = "0" . $numero;
|
||||
}
|
||||
if (strlen($numero) == 10) {
|
||||
$numero = substr($numero, 0, 2) . $separateur . substr($numero, 2, 2) . $separateur . substr($numero, 4, 2) . $separateur . substr($numero, 6, 2) . $separateur . substr($numero, 8, 2);
|
||||
}
|
||||
return $numero;
|
||||
}
|
||||
|
||||
function str_normalize($string, $minuscules = true) {
|
||||
$result = "";
|
||||
$string = trim($string);
|
||||
if (strlen($string) > 0) {
|
||||
if ($minuscules) {
|
||||
$result = strtolower($string);
|
||||
} else {
|
||||
$result = $string;
|
||||
}
|
||||
$result = str_replace(" ", "_", $result);
|
||||
$result = str_replace("é", "e", $result);
|
||||
$result = str_replace("è", "e", $result);
|
||||
$result = str_replace("ê", "e", $result);
|
||||
$result = str_replace("ë", "e", $result);
|
||||
$result = str_replace("à", "a", $result);
|
||||
$result = str_replace("â", "a", $result);
|
||||
$result = str_replace("ä", "a", $result);
|
||||
$result = str_replace("ô", "o", $result);
|
||||
$result = str_replace("ö", "o", $result);
|
||||
$result = str_replace("ù", "u", $result);
|
||||
$result = str_replace("û", "u", $result);
|
||||
$result = str_replace("ü", "u", $result);
|
||||
$result = str_replace("ç", "c", $result);
|
||||
$result = str_replace("'", "", $result);
|
||||
$result = str_replace("\"", "", $result);
|
||||
$result = str_replace("/", "", $result);
|
||||
$result = str_replace("(", "_", $result);
|
||||
$result = str_replace(")", "_", $result);
|
||||
$result = str_replace("!", "_", $result);
|
||||
$result = str_replace("?", "_", $result);
|
||||
$result = trim($result);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
function generateRandomPassword() {
|
||||
$password = '';
|
||||
$desired_length = rand(8, 12);
|
||||
|
||||
for ($length = 0; $length < $desired_length; $length++) {
|
||||
$password .= chr(rand(44, 122));
|
||||
}
|
||||
$password = str_replace("/", "&", $password);
|
||||
$password = str_replace("<", "!", $password);
|
||||
$password = str_replace(">", "!", $password);
|
||||
$password = str_replace("=", "#", $password);
|
||||
$password = str_replace("\\", "&", $password);
|
||||
$password = str_replace("^", "%", $password);
|
||||
$password = str_replace(chr(96), "#", $password);
|
||||
|
||||
return $password;
|
||||
}
|
||||
|
||||
function purge_old_logs($log_dir, $app_name, $days_to_keep = 10) {
|
||||
if (!is_dir($log_dir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$date_limit = strtotime("-{$days_to_keep} days");
|
||||
|
||||
$patterns = array(
|
||||
$app_name . '_????-??-??.log',
|
||||
$app_name . '_debug_????-??-??.log'
|
||||
);
|
||||
|
||||
foreach ($patterns as $pattern) {
|
||||
$files = glob($log_dir . $pattern);
|
||||
if ($files) {
|
||||
foreach ($files as $file) {
|
||||
if (preg_match('/(\d{4}-\d{2}-\d{2})\.log$/', $file, $matches)) {
|
||||
$file_date = strtotime($matches[1]);
|
||||
if ($file_date < $date_limit) {
|
||||
@unlink($file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,8 @@ let chkShowDevisArchives = false // indique si on affiche les devis archivés ou
|
||||
let chkCreateClient = false
|
||||
// On charge les produits du marché du devis en cours dans un tableau pour ne pas avoir à les recharger à chaque fois
|
||||
let dataProduitsMarche = []
|
||||
let dataProduitsMercurial = [] // Produits du marché hybride pour l'onglet Mercurial
|
||||
let chkMarcheHybride = false // Indique si le marché du devis est hybride
|
||||
//! Pour ne charger les clients du secteur ou de toute la France qu'en cas de changement de la valeur du chkbox
|
||||
let oldChkClientsSecteur = 2
|
||||
let clients = []
|
||||
@@ -35,14 +37,107 @@ let devisTotalRemHT = 0
|
||||
let devisTotalMarge = 0
|
||||
|
||||
let chkRegleSeuilsMarge = false // indique si le marché sélectionné prend en compte les seuils de marge fixés dans les familles de produits
|
||||
let seuilMargeRR = 30 // le seuil de marge du RR sur ce devis, par défaut à 30 %
|
||||
let seuilMargeDV = 20 // le seuil de marge du DV sur ce devis, par défaut à 20 %
|
||||
let seuilMargeRR = 40 // le seuil de marge du RR sur ce devis, par défaut à 40 % (MAJ 05/11/2025)
|
||||
let seuilMargeDV = 30 // le seuil de marge du DV sur ce devis, par défaut à 30 % (MAJ 05/11/2025)
|
||||
|
||||
let intervalRefresh
|
||||
let nbCommentChat = 0
|
||||
|
||||
let draggedElement = null // l'élément qui est en train d'être déplacé (la ligne du produit du devis lors d'un drag and drop)
|
||||
|
||||
const tableSortStates = new Map()
|
||||
|
||||
function initTableSort() {
|
||||
const allTables = document.querySelectorAll('[id^="tblDos"], [id^="tblDosArch"]')
|
||||
|
||||
allTables.forEach(table => {
|
||||
const tableId = table.id
|
||||
const headers = table.querySelectorAll('th[data-sortable="true"]')
|
||||
const tbody = table.querySelector('tbody')
|
||||
|
||||
if (!tbody) return
|
||||
|
||||
if (!tableSortStates.has(tableId)) {
|
||||
tableSortStates.set(tableId, {
|
||||
originalOrder: null,
|
||||
currentSort: { column: null, direction: null }
|
||||
})
|
||||
}
|
||||
|
||||
headers.forEach(header => {
|
||||
header.addEventListener('click', function() {
|
||||
const columnIndex = parseInt(this.getAttribute('data-column-index'))
|
||||
const sortType = this.getAttribute('data-sort-type')
|
||||
sortTable(tableId, columnIndex, sortType, this)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function sortTable(tableId, columnIndex, sortType, headerElement) {
|
||||
const table = document.getElementById(tableId)
|
||||
const tbody = table.querySelector('tbody')
|
||||
const rows = Array.from(tbody.querySelectorAll('tr'))
|
||||
const state = tableSortStates.get(tableId)
|
||||
|
||||
if (!state.originalOrder) {
|
||||
state.originalOrder = rows.slice()
|
||||
}
|
||||
|
||||
const allHeaders = table.querySelectorAll('th[data-sortable="true"]')
|
||||
allHeaders.forEach(h => h.style.fontWeight = 'normal')
|
||||
|
||||
let sortedRows
|
||||
|
||||
if (state.currentSort.column === columnIndex && state.currentSort.direction === 'desc') {
|
||||
sortedRows = state.originalOrder.slice()
|
||||
state.currentSort = { column: null, direction: null }
|
||||
} else {
|
||||
const direction = (state.currentSort.column === columnIndex && state.currentSort.direction === 'asc') ? 'desc' : 'asc'
|
||||
|
||||
sortedRows = rows.slice().sort((a, b) => {
|
||||
const aCell = a.cells[columnIndex]
|
||||
const bCell = b.cells[columnIndex]
|
||||
|
||||
if (!aCell || !bCell) return 0
|
||||
|
||||
let aValue = aCell.textContent.trim()
|
||||
let bValue = bCell.textContent.trim()
|
||||
|
||||
if (sortType === 'number') {
|
||||
aValue = aValue.replace(/[^\d,.-]/g, '').replace(',', '.')
|
||||
bValue = bValue.replace(/[^\d,.-]/g, '').replace(',', '.')
|
||||
aValue = parseFloat(aValue) || 0
|
||||
bValue = parseFloat(bValue) || 0
|
||||
return direction === 'asc' ? aValue - bValue : bValue - aValue
|
||||
} else if (sortType === 'date') {
|
||||
aValue = parseDateFromText(aValue)
|
||||
bValue = parseDateFromText(bValue)
|
||||
if (!aValue && !bValue) return 0
|
||||
if (!aValue) return direction === 'asc' ? 1 : -1
|
||||
if (!bValue) return direction === 'asc' ? -1 : 1
|
||||
return direction === 'asc' ? aValue - bValue : bValue - aValue
|
||||
} else {
|
||||
const comparison = aValue.localeCompare(bValue, 'fr')
|
||||
return direction === 'asc' ? comparison : -comparison
|
||||
}
|
||||
})
|
||||
|
||||
state.currentSort = { column: columnIndex, direction }
|
||||
headerElement.style.fontWeight = 'bold'
|
||||
}
|
||||
|
||||
tbody.innerHTML = ''
|
||||
sortedRows.forEach(row => tbody.appendChild(row))
|
||||
}
|
||||
|
||||
function parseDateFromText(dateText) {
|
||||
const match = dateText.match(/(\d{2})\/(\d{2})[\/\s](\d{4})/)
|
||||
if (!match) return null
|
||||
const [, day, month, year] = match
|
||||
return new Date(year, month - 1, day)
|
||||
}
|
||||
|
||||
window.addEventListener('DOMContentLoaded', (event) => {
|
||||
console.log('#')
|
||||
|
||||
@@ -407,18 +502,24 @@ window.addEventListener('DOMContentLoaded', (event) => {
|
||||
document.getElementById('inp_adresse3').value = data.adresse3
|
||||
document.getElementById('inp_cp').value = data.cp
|
||||
document.getElementById('inp_ville').value = data.ville
|
||||
document.getElementById('inp_contact_nom').value = data.contact_nom
|
||||
document.getElementById('inp_contact_prenom').value = data.contact_prenom
|
||||
document.getElementById('inp_contact_fonction').value = data.contact_fonction
|
||||
document.getElementById('inp_email').value = data.email
|
||||
document.getElementById('inp_telephone').value = data.telephone
|
||||
document.getElementById('inp_mobile').value = data.mobile
|
||||
document.getElementById('selTypeEtab').value = data.type_client
|
||||
elBtnCreateClient.innerHTML = 'Créer un nouveau client'
|
||||
if (elBtnCreateClient.classList.contains('btn-info')) {
|
||||
elBtnCreateClient.classList.remove('btn-info')
|
||||
elBtnCreateClient.classList.add('btn-primary')
|
||||
}
|
||||
|
||||
// Charger les contacts du client et sélectionner le contact du devis
|
||||
loadContactsClient(data.code).then(() => {
|
||||
if (data.fk_contact && data.fk_contact > 0) {
|
||||
document.getElementById('sel_contact').value = data.fk_contact
|
||||
// Afficher les infos du contact
|
||||
const contact = contactsClient.find(c => c.rowid == data.fk_contact)
|
||||
if (contact) {
|
||||
displayContactInfos(contact)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (data.chk_devis_photos == '1') {
|
||||
@@ -908,6 +1009,86 @@ window.addEventListener('DOMContentLoaded', (event) => {
|
||||
if (ret.length == 1) {
|
||||
let line = ret[0]
|
||||
chkPrixNets = line.chk_prix_nets == 1 ? true : false
|
||||
chkMarcheHybride = line.chk_marche_hybride == 1 ? true : false
|
||||
|
||||
// Si le marché est hybride, créer le panel et charger les produits
|
||||
if (chkMarcheHybride) {
|
||||
// Créer le panel Mercurial s'il n'existe pas
|
||||
if (!document.getElementById('tabMercurial')) {
|
||||
const tabContent = document.querySelector('#divProduitsDisponibles .tab-content')
|
||||
const divPanel = document.createElement('div')
|
||||
divPanel.setAttribute('role', 'tabpanel')
|
||||
divPanel.className = 'tab-pane'
|
||||
divPanel.id = 'tabMercurial'
|
||||
divPanel.innerHTML = `
|
||||
<div class="form-group">
|
||||
<label for="inpSearchProduct_Mercurial">Recherche de produits Mercurial : </label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="inpSearchProduct_Mercurial" placeholder="code ou libellé" size="50px" />
|
||||
<div class="input-group-addon">
|
||||
<svg width="18px" height="18px" viewBox="0 0 20 20" role="img" xmlns="http://www.w3.org/2000/svg" aria-labelledby="returnIconTitle" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none" color="#000000">
|
||||
<path d="M19,8 L19,11 C19,12.1045695 18.1045695,13 17,13 L6,13"/>
|
||||
<polyline points="8 16 5 13 8 10"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border cm-scrollbar cm-table-w-scroll table-responsive mt-1 table-400">
|
||||
<table class="table table-striped table-bordered table-responsive table-fixed" id="tblProduits_Mercurial">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="header" scope="col" width="10%">Sélection</th>
|
||||
<th class="header" scope="col" width="20%">Code</th>
|
||||
<th class="header" scope="col" width="30%">Libellé</th>
|
||||
<th class="header" scope="col" width="10%">Famille</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
`
|
||||
tabContent.appendChild(divPanel)
|
||||
}
|
||||
|
||||
// Charger les produits Mercurial
|
||||
fetch('/jxdevis/load_produits_mercurial', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ fk_marche: idMarche }),
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=utf-8',
|
||||
Accept: 'application/json;charset=utf-8',
|
||||
},
|
||||
}).then((response) => {
|
||||
if (response.ok) {
|
||||
const ret = response.json()
|
||||
ret.then(function (data) {
|
||||
dataProduitsMercurial = data
|
||||
showProduitsMercurial(data)
|
||||
|
||||
// Créer l'onglet Mercurial en dernière position après affichage des produits
|
||||
if (!document.getElementById('liOngletMercurial')) {
|
||||
const ulOnglets = document.getElementById('listOngletsProduits')
|
||||
const liOnglet = document.createElement('li')
|
||||
liOnglet.setAttribute('role', 'presentation')
|
||||
liOnglet.id = 'liOngletMercurial'
|
||||
liOnglet.innerHTML = '<a href="#tabMercurial" id="onglet_mercurial" aria-controls="tabMercurial" role="tab" data-toggle="tab" style="background-color: #5cb85c; color: white; font-weight: bold;">Mercurial<br/> </a>'
|
||||
ulOnglets.appendChild(liOnglet)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// Supprimer l'onglet et le panel Mercurial s'ils existent
|
||||
const liOnglet = document.getElementById('liOngletMercurial')
|
||||
if (liOnglet) {
|
||||
liOnglet.remove()
|
||||
}
|
||||
const divPanel = document.getElementById('tabMercurial')
|
||||
if (divPanel) {
|
||||
divPanel.remove()
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('inp_latitudeRR').value = seuilMargeRR
|
||||
document.getElementById('inp_latitudeDV').value = seuilMargeDV
|
||||
document.getElementById('titleMarche').innerHTML = '<bolder>Informations du marché ' + line.libelle + '</bolder>' // le titre du panel des infos marché dans l'onglet 3. Devis
|
||||
@@ -1146,6 +1327,54 @@ window.addEventListener('DOMContentLoaded', (event) => {
|
||||
celFamille.innerHTML = lineProduit.lib_famille
|
||||
}
|
||||
|
||||
function showProduitsMercurial(dProduits) {
|
||||
// Affiche tous les produits Mercurial dans l'onglet dédié
|
||||
let tblBody = document.getElementById('tblProduits_Mercurial').getElementsByTagName('tbody')[0]
|
||||
tblBody.innerHTML = ''
|
||||
|
||||
if (dProduits && dProduits.length > 0) {
|
||||
dProduits.forEach(function (lineProduit) {
|
||||
let newRow = tblBody.insertRow(-1)
|
||||
newRow.className = 'ligProduit_Mercurial'
|
||||
newRow.id = 'ligProduit_Mercurial_' + lineProduit.rowid
|
||||
newRow.setAttribute('data-rid', lineProduit.rowid)
|
||||
|
||||
let celChkBox = newRow.insertCell(0)
|
||||
celChkBox.className = 'chkBox_Mercurial text-center'
|
||||
celChkBox.setAttribute('data-rid', lineProduit.rowid)
|
||||
celChkBox.innerHTML =
|
||||
'<input type="checkbox" class="chkBox" id="chkBoxProd_' +
|
||||
lineProduit.rowid +
|
||||
'" name="chkBoxProd_' +
|
||||
lineProduit.rowid +
|
||||
'" data-rid="' +
|
||||
lineProduit.rowid +
|
||||
'" data-code="' +
|
||||
lineProduit.code +
|
||||
'" data-libelle="' +
|
||||
lineProduit.libelle +
|
||||
'" data-famille="Mercurial" />'
|
||||
|
||||
let celCode = newRow.insertCell(1)
|
||||
celCode.innerHTML = lineProduit.code
|
||||
|
||||
let celLibelle = newRow.insertCell(2)
|
||||
celLibelle.innerHTML = lineProduit.libelle
|
||||
|
||||
let celFamille = newRow.insertCell(3)
|
||||
celFamille.innerHTML = lineProduit.lib_famille || '-'
|
||||
})
|
||||
|
||||
// Brancher l'autocomplete sur le champ de recherche
|
||||
autocompleteProduitsFamille(
|
||||
document.getElementById('inpSearchProduct_Mercurial'),
|
||||
dProduits,
|
||||
'Mercurial',
|
||||
0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
$('a[data-toggle="tab"]').on('show.bs.tab', function (e) {
|
||||
if (idDevis == 0) {
|
||||
if ($(this).attr('href') == '#tabproduits' || $(this).attr('href') == '#tabdevis') {
|
||||
@@ -1346,33 +1575,63 @@ window.addEventListener('DOMContentLoaded', (event) => {
|
||||
}
|
||||
|
||||
let clickSaveCreateClient = function () {
|
||||
// on regarde si c'est une création de devis ou une modification
|
||||
// on enregistre le fait que ça soit un nouveau client
|
||||
// on met à jour les champs du devis avec les infos du nouveau client
|
||||
// et quand on enregistre le devis on enregistre le nouveau client dans le devis
|
||||
document.getElementById('inp_fk_client').value = '0'
|
||||
document.getElementById('inp_lib_client').value = document.getElementById('inp_create_libelle').value
|
||||
document.getElementById('selTypeEtab').value = document.getElementById('inp_create_type_client').value
|
||||
document.getElementById('inp_type_client').value = document.getElementById('inp_create_type_client').value
|
||||
document.getElementById('inp_adresse1').value = document.getElementById('inp_create_adresse1').value
|
||||
document.getElementById('inp_adresse2').value = document.getElementById('inp_create_adresse2').value
|
||||
document.getElementById('inp_adresse3').value = document.getElementById('inp_create_adresse3').value
|
||||
document.getElementById('inp_cp').value = document.getElementById('inp_create_cp').value
|
||||
document.getElementById('inp_ville').value = document.getElementById('inp_create_ville').value
|
||||
showLoading()
|
||||
|
||||
const clientData = {
|
||||
libelle: document.getElementById('inp_create_libelle').value,
|
||||
type_client: document.getElementById('inp_create_type_client').value,
|
||||
adresse1: document.getElementById('inp_create_adresse1').value,
|
||||
adresse2: document.getElementById('inp_create_adresse2').value,
|
||||
adresse3: document.getElementById('inp_create_adresse3').value,
|
||||
cp: document.getElementById('inp_create_cp').value,
|
||||
ville: document.getElementById('inp_create_ville').value
|
||||
}
|
||||
|
||||
fetch('/jxdevis/save_new_client', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(clientData),
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=utf-8',
|
||||
Accept: 'application/json;charset=utf-8',
|
||||
},
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
hideLoading()
|
||||
if (data.success) {
|
||||
const newClientId = data.rowid
|
||||
const newClientCode = data.code
|
||||
|
||||
document.getElementById('inp_fk_client').value = newClientId
|
||||
document.getElementById('inp_lib_client').value = clientData.libelle
|
||||
document.getElementById('selTypeEtab').value = clientData.type_client
|
||||
document.getElementById('inp_type_client').value = clientData.type_client
|
||||
document.getElementById('inp_adresse1').value = clientData.adresse1
|
||||
document.getElementById('inp_adresse2').value = clientData.adresse2
|
||||
document.getElementById('inp_adresse3').value = clientData.adresse3
|
||||
document.getElementById('inp_cp').value = clientData.cp
|
||||
document.getElementById('inp_ville').value = clientData.ville
|
||||
|
||||
console.log(
|
||||
'nouveau client créé : ' +
|
||||
document.getElementById('inp_create_type_client').value +
|
||||
' -> ' +
|
||||
document.getElementById('selTypeEtab').value
|
||||
)
|
||||
hideModal(document.getElementById('modalCreateClient'))
|
||||
// on change le texte et la couleur du bouton de nouveau client
|
||||
document.getElementById('btnCreateClient').innerHTML = 'Modifier le nouveau client'
|
||||
|
||||
document.getElementById('btnCreateClient').innerHTML = 'Modifier le client'
|
||||
document.getElementById('btnCreateClient').classList.remove('btn-primary')
|
||||
document.getElementById('btnCreateClient').classList.add('btn-info')
|
||||
|
||||
chkCreateClient = true
|
||||
loadContactsClient(newClientCode).then(() => {
|
||||
showNotification('Succès', 'Client créé avec succès. Vous pouvez maintenant gérer ses contacts.', 'success')
|
||||
})
|
||||
|
||||
chkCreateClient = false
|
||||
} else {
|
||||
showNotification('Erreur', data.message || 'Erreur lors de la création du client', 'error')
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
hideLoading()
|
||||
console.error('Erreur création client:', error)
|
||||
showNotification('Erreur', 'Impossible de créer le client', 'error')
|
||||
})
|
||||
}
|
||||
|
||||
let clickSpeciaux = function () {
|
||||
@@ -1510,45 +1769,13 @@ window.addEventListener('DOMContentLoaded', (event) => {
|
||||
return false
|
||||
}
|
||||
|
||||
if (document.getElementById('inp_contact_nom').value == '') {
|
||||
if (document.getElementById('sel_contact').value == '0') {
|
||||
showNotification(
|
||||
'Erreur',
|
||||
'Enregistrement impossible : vous devez renseigner le nom et prénom du contact',
|
||||
'Enregistrement impossible : vous devez sélectionner un contact',
|
||||
'error'
|
||||
)
|
||||
document.getElementById('inp_contact_nom').focus()
|
||||
return false
|
||||
}
|
||||
|
||||
if (document.getElementById('inp_contact_prenom').value == '') {
|
||||
showNotification(
|
||||
'Erreur',
|
||||
'Enregistrement impossible : vous devez renseigner le nom et prénom du contact',
|
||||
'error'
|
||||
)
|
||||
document.getElementById('inp_contact_prenom').focus()
|
||||
return false
|
||||
}
|
||||
|
||||
if (document.getElementById('inp_contact_fonction').value == '') {
|
||||
showNotification('Erreur', 'Enregistrement impossible : vous devez renseigner la fonction du contact', 'error')
|
||||
document.getElementById('inp_contact_fonction').focus()
|
||||
return false
|
||||
}
|
||||
|
||||
if (document.getElementById('inp_email').value == '') {
|
||||
showNotification('Erreur', "Enregistrement impossible : vous devez renseigner l'email du contact", 'error')
|
||||
document.getElementById('inp_email').focus()
|
||||
return false
|
||||
}
|
||||
|
||||
if (document.getElementById('inp_telephone').value == '' && document.getElementById('inp_mobile').value == '') {
|
||||
showNotification(
|
||||
'Erreur',
|
||||
'Enregistrement impossible : vous devez renseigner au moins un numéro de téléphone du contact (fixe ou mobile)',
|
||||
'error'
|
||||
)
|
||||
document.getElementById('inp_telephone').focus()
|
||||
document.getElementById('sel_contact').focus()
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1817,10 +2044,10 @@ window.addEventListener('DOMContentLoaded', (event) => {
|
||||
showDevisPro(data)
|
||||
hideLoading()
|
||||
showNotification('Succès', 'Enregistrement des ' + nbProduits + ' produits de ce devis effectué', 'success')
|
||||
chkChange = 0
|
||||
})
|
||||
}
|
||||
})
|
||||
chkChange = 0
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -2891,13 +3118,9 @@ window.addEventListener('DOMContentLoaded', (event) => {
|
||||
document.getElementById('inp_adresse3').value = list[i]['adresse3']
|
||||
document.getElementById('inp_cp').value = list[i]['cp']
|
||||
document.getElementById('inp_ville').value = list[i]['ville']
|
||||
document.getElementById('inp_contact_nom').value = list[i]['contact_nom']
|
||||
document.getElementById('inp_contact_prenom').value = list[i]['contact_prenom']
|
||||
document.getElementById('inp_contact_fonction').value = list[i]['contact_fonction']
|
||||
document.getElementById('inp_telephone').value = list[i]['telephone']
|
||||
document.getElementById('inp_email').value = list[i]['email']
|
||||
document.getElementById('inp_mobile').value = list[i]['mobile']
|
||||
document.getElementById('selTypeEtab').value = list[i]['type_client']
|
||||
// Charger les contacts du client sélectionné
|
||||
loadContactsClient(list[i]['code'])
|
||||
// on ferme la liste des suggestions
|
||||
closeList()
|
||||
})
|
||||
@@ -3305,6 +3528,284 @@ window.addEventListener('DOMContentLoaded', (event) => {
|
||||
hideModal(document.getElementById('modalCommentProd'))
|
||||
}
|
||||
|
||||
//! ========== GESTION DES CONTACTS ==========
|
||||
|
||||
let contactsClient = [] // Liste des contacts du client en cours
|
||||
let currentFkClient = 0 // ID du client en cours
|
||||
|
||||
// Charger les contacts d'un client
|
||||
function loadContactsClient(fkClient) {
|
||||
if (fkClient == 0) {
|
||||
document.getElementById('sel_contact').innerHTML = '<option value="0">- Sélectionner un contact -</option>'
|
||||
document.getElementById('btnGererContacts').disabled = true
|
||||
document.getElementById('divContactInfos').style.display = 'none'
|
||||
contactsClient = []
|
||||
currentFkClient = 0
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
currentFkClient = fkClient
|
||||
return fetch('/jxcontacts/load_contacts', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ fk_client: fkClient }),
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=utf-8',
|
||||
Accept: 'application/json;charset=utf-8',
|
||||
},
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
contactsClient = data
|
||||
const selContact = document.getElementById('sel_contact')
|
||||
selContact.innerHTML = '<option value="0">- Sélectionner un contact -</option>'
|
||||
|
||||
data.forEach((contact) => {
|
||||
const option = document.createElement('option')
|
||||
option.value = contact.rowid
|
||||
option.textContent = contact.nom + ' ' + contact.prenom + (contact.principal == 1 ? ' ⭐' : '')
|
||||
selContact.appendChild(option)
|
||||
})
|
||||
|
||||
document.getElementById('btnGererContacts').disabled = false
|
||||
|
||||
// Si un seul contact, le sélectionner automatiquement
|
||||
if (data.length === 1) {
|
||||
selContact.value = data[0].rowid
|
||||
displayContactInfos(data[0])
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Erreur chargement contacts:', error)
|
||||
showNotification('Erreur', 'Impossible de charger les contacts', 'error')
|
||||
})
|
||||
}
|
||||
|
||||
// Afficher les infos d'un contact en lecture seule
|
||||
function displayContactInfos(contact) {
|
||||
document.getElementById('info_contact_nom_prenom').textContent = (contact.prenom || '') + ' ' + (contact.nom || '')
|
||||
document.getElementById('info_contact_fonction').textContent = contact.fonction || '-'
|
||||
document.getElementById('info_contact_email').textContent = contact.email || '-'
|
||||
document.getElementById('info_contact_telephone').textContent = contact.telephone || '-'
|
||||
document.getElementById('info_contact_mobile').textContent = contact.mobile || '-'
|
||||
document.getElementById('divContactInfos').style.display = 'block'
|
||||
}
|
||||
|
||||
// Événement changement de contact
|
||||
document.getElementById('sel_contact').addEventListener('change', function () {
|
||||
const contactId = this.value
|
||||
if (contactId == 0) {
|
||||
document.getElementById('divContactInfos').style.display = 'none'
|
||||
return
|
||||
}
|
||||
|
||||
const contact = contactsClient.find((c) => c.rowid == contactId)
|
||||
if (contact) {
|
||||
displayContactInfos(contact)
|
||||
}
|
||||
})
|
||||
|
||||
// Ouvrir la modale de gestion des contacts
|
||||
document.getElementById('btnGererContacts').addEventListener('click', function () {
|
||||
document.getElementById('inp_fk_client_contacts').value = currentFkClient
|
||||
loadContactsTable()
|
||||
showModal(document.getElementById('modalGererContacts'))
|
||||
})
|
||||
|
||||
// Charger la table des contacts
|
||||
function loadContactsTable() {
|
||||
const tbody = document.querySelector('#tblContacts tbody')
|
||||
tbody.innerHTML = '<tr><td colspan="7" class="text-center">Chargement...</td></tr>'
|
||||
|
||||
fetch('/jxcontacts/load_contacts', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ fk_client: currentFkClient }),
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=utf-8',
|
||||
Accept: 'application/json;charset=utf-8',
|
||||
},
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
tbody.innerHTML = ''
|
||||
if (data.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="7" class="text-center">Aucun contact trouvé</td></tr>'
|
||||
return
|
||||
}
|
||||
|
||||
data.forEach((contact) => {
|
||||
const tr = document.createElement('tr')
|
||||
tr.innerHTML = `
|
||||
<td>${contact.nom || ''}</td>
|
||||
<td>${contact.prenom || ''}</td>
|
||||
<td>${contact.fonction || ''}</td>
|
||||
<td>${contact.telephone || ''}</td>
|
||||
<td>${contact.email || ''}</td>
|
||||
<td class="text-center">
|
||||
${contact.principal == 1 ? '<span class="label label-success">Oui</span>' : '<button class="btn btn-xs btn-default btnSetPrincipal" data-id="' + contact.rowid + '">Définir</button>'}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<button class="btn btn-xs btn-primary btnEditContact" data-id="${contact.rowid}"><i class="fa fa-edit"></i></button>
|
||||
<button class="btn btn-xs btn-danger btnDeleteContact" data-id="${contact.rowid}"><i class="fa fa-trash"></i></button>
|
||||
</td>
|
||||
`
|
||||
tbody.appendChild(tr)
|
||||
})
|
||||
|
||||
// Attacher les événements
|
||||
document.querySelectorAll('.btnEditContact').forEach((btn) => {
|
||||
btn.addEventListener('click', function () {
|
||||
editContact(this.getAttribute('data-id'))
|
||||
})
|
||||
})
|
||||
|
||||
document.querySelectorAll('.btnDeleteContact').forEach((btn) => {
|
||||
btn.addEventListener('click', function () {
|
||||
deleteContact(this.getAttribute('data-id'))
|
||||
})
|
||||
})
|
||||
|
||||
document.querySelectorAll('.btnSetPrincipal').forEach((btn) => {
|
||||
btn.addEventListener('click', function () {
|
||||
setPrincipalContact(this.getAttribute('data-id'))
|
||||
})
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Erreur chargement table contacts:', error)
|
||||
tbody.innerHTML = '<tr><td colspan="7" class="text-center text-danger">Erreur de chargement</td></tr>'
|
||||
})
|
||||
}
|
||||
|
||||
// Nouveau contact
|
||||
document.getElementById('btnNouveauContact').addEventListener('click', function () {
|
||||
document.getElementById('modEditContactTitreText').textContent = 'Nouveau contact'
|
||||
document.getElementById('frmEditContact').reset()
|
||||
document.getElementById('inp_contact_rowid').value = '0'
|
||||
document.getElementById('inp_contact_fk_client').value = currentFkClient
|
||||
showModal(document.getElementById('modalEditContact'))
|
||||
})
|
||||
|
||||
// Éditer un contact
|
||||
function editContact(contactId) {
|
||||
fetch('/jxcontacts/load_contact', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ rowid: contactId }),
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=utf-8',
|
||||
Accept: 'application/json;charset=utf-8',
|
||||
},
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((contact) => {
|
||||
document.getElementById('modEditContactTitreText').textContent = 'Modifier le contact'
|
||||
document.getElementById('inp_contact_rowid').value = contact.rowid
|
||||
document.getElementById('inp_contact_fk_client').value = contact.fk_client
|
||||
document.getElementById('inp_contact_nom_edit').value = contact.nom || ''
|
||||
document.getElementById('inp_contact_prenom_edit').value = contact.prenom || ''
|
||||
document.getElementById('inp_contact_fonction_edit').value = contact.fonction || ''
|
||||
document.getElementById('inp_contact_email_edit').value = contact.email || ''
|
||||
document.getElementById('inp_contact_telephone_edit').value = contact.telephone || ''
|
||||
document.getElementById('inp_contact_mobile_edit').value = contact.mobile || ''
|
||||
document.getElementById('inp_contact_principal_edit').checked = contact.principal == 1
|
||||
showModal(document.getElementById('modalEditContact'))
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Erreur chargement contact:', error)
|
||||
showNotification('Erreur', 'Impossible de charger le contact', 'error')
|
||||
})
|
||||
}
|
||||
|
||||
// Sauvegarder un contact
|
||||
document.getElementById('btnSaveEditContact').addEventListener('click', function () {
|
||||
const form = document.getElementById('frmEditContact')
|
||||
const formData = new FormData(form)
|
||||
|
||||
fetch('/jxcontacts/save_contact', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
if (data.ret === 'ok') {
|
||||
showNotification('Succès', data.msg, 'success')
|
||||
hideModal(document.getElementById('modalEditContact'))
|
||||
loadContactsTable()
|
||||
loadContactsClient(currentFkClient) // Recharger le sélecteur
|
||||
} else {
|
||||
showNotification('Erreur', data.msg, 'error')
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Erreur sauvegarde contact:', error)
|
||||
showNotification('Erreur', 'Impossible de sauvegarder le contact', 'error')
|
||||
})
|
||||
})
|
||||
|
||||
// Supprimer un contact
|
||||
function deleteContact(contactId) {
|
||||
if (!confirm('Êtes-vous sûr de vouloir supprimer ce contact ?')) return
|
||||
|
||||
fetch('/jxcontacts/delete_contact', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ rowid: contactId }),
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=utf-8',
|
||||
Accept: 'application/json;charset=utf-8',
|
||||
},
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
if (data.ret === 'ok') {
|
||||
showNotification('Succès', data.msg, 'success')
|
||||
loadContactsTable()
|
||||
loadContactsClient(currentFkClient)
|
||||
} else {
|
||||
showNotification('Erreur', data.msg, 'error')
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Erreur suppression contact:', error)
|
||||
showNotification('Erreur', 'Impossible de supprimer le contact', 'error')
|
||||
})
|
||||
}
|
||||
|
||||
// Définir contact principal
|
||||
function setPrincipalContact(contactId) {
|
||||
fetch('/jxcontacts/set_principal', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ rowid: contactId, fk_client: currentFkClient }),
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=utf-8',
|
||||
Accept: 'application/json;charset=utf-8',
|
||||
},
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
if (data.ret === 'ok') {
|
||||
showNotification('Succès', data.msg, 'success')
|
||||
loadContactsTable()
|
||||
loadContactsClient(currentFkClient)
|
||||
} else {
|
||||
showNotification('Erreur', data.msg, 'error')
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Erreur définition contact principal:', error)
|
||||
showNotification('Erreur', 'Impossible de définir le contact principal', 'error')
|
||||
})
|
||||
}
|
||||
|
||||
// Fermer les modales
|
||||
document.getElementById('btnFermerContacts').addEventListener('click', function () {
|
||||
hideModal(document.getElementById('modalGererContacts'))
|
||||
})
|
||||
|
||||
document.getElementById('btnCancelEditContact').addEventListener('click', function () {
|
||||
hideModal(document.getElementById('modalEditContact'))
|
||||
})
|
||||
|
||||
//! ========== FIN GESTION DES CONTACTS ==========
|
||||
|
||||
//! Configuration des événements
|
||||
//! Sur chaque cellule du tableau des devis ayant la classe celDevis, on affecte un événement click qui appelle la fonction clickLigDevis()
|
||||
Array.from(elCelDevis).forEach(function (lnDevis) {
|
||||
@@ -3382,6 +3883,148 @@ window.addEventListener('DOMContentLoaded', (event) => {
|
||||
chkVariante.addEventListener('change', calculDevis)
|
||||
})
|
||||
|
||||
let elSearchDevis = document.getElementById('searchDevis')
|
||||
let elBtnResetSearch = document.getElementById('btnResetSearch')
|
||||
let searchTimeout = null
|
||||
|
||||
function restoreSearch() {
|
||||
const savedTerm = sessionStorage.getItem('devisSearchTerm')
|
||||
if (savedTerm && savedTerm.length >= 3) {
|
||||
elSearchDevis.value = savedTerm
|
||||
elBtnResetSearch.style.display = 'inline-block'
|
||||
performSearch(savedTerm)
|
||||
}
|
||||
}
|
||||
|
||||
function performSearch(term) {
|
||||
if (term.length < 3) {
|
||||
return
|
||||
}
|
||||
|
||||
const context = chkShowDevisArchives ? 'archives' : 'encours'
|
||||
|
||||
fetch('/jxdevis/search_devis', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
term: term,
|
||||
context: context,
|
||||
}),
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
if (data.success) {
|
||||
const devisIds = data.devis.map((d) => d.rowid)
|
||||
filterDevisTables(devisIds, context)
|
||||
updateBadges(data.nb_devis)
|
||||
} else {
|
||||
console.error('Erreur recherche:', data.message)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Erreur AJAX:', error)
|
||||
})
|
||||
}
|
||||
|
||||
function filterDevisTables(devisIds, context) {
|
||||
if (context === 'encours') {
|
||||
const statuts = document.querySelectorAll('[id^="tblBodyDos"]')
|
||||
statuts.forEach((tbody) => {
|
||||
const rows = tbody.querySelectorAll('tr')
|
||||
rows.forEach((row) => {
|
||||
if (row.id && row.id.startsWith('tr_')) {
|
||||
const rowId = parseInt(row.id.replace('tr_', ''))
|
||||
if (devisIds.includes(rowId)) {
|
||||
row.style.display = ''
|
||||
} else {
|
||||
row.style.display = 'none'
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
} else {
|
||||
const archives = document.querySelectorAll('[id^="tblBodyDosArch"]')
|
||||
archives.forEach((tbody) => {
|
||||
const rows = tbody.querySelectorAll('tr')
|
||||
rows.forEach((row) => {
|
||||
if (row.id && row.id.startsWith('trArch_')) {
|
||||
const rowId = parseInt(row.id.replace('trArch_', ''))
|
||||
if (devisIds.includes(rowId)) {
|
||||
row.style.display = ''
|
||||
} else {
|
||||
row.style.display = 'none'
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function updateBadges(nbDevis) {
|
||||
Object.keys(nbDevis).forEach((statutId) => {
|
||||
const badge = document.querySelector('[id^="liStat"]')
|
||||
if (badge) {
|
||||
badge.setAttribute('data-after-text', nbDevis[statutId])
|
||||
badge.setAttribute('data-after-type', 'orange badge top left')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function resetSearch() {
|
||||
elSearchDevis.value = ''
|
||||
elBtnResetSearch.style.display = 'none'
|
||||
sessionStorage.removeItem('devisSearchTerm')
|
||||
|
||||
const context = chkShowDevisArchives ? 'archives' : 'encours'
|
||||
if (context === 'encours') {
|
||||
const statuts = document.querySelectorAll('[id^="tblBodyDos"]')
|
||||
statuts.forEach((tbody) => {
|
||||
const rows = tbody.querySelectorAll('tr')
|
||||
rows.forEach((row) => {
|
||||
row.style.display = ''
|
||||
})
|
||||
})
|
||||
} else {
|
||||
const archives = document.querySelectorAll('[id^="tblBodyDosArch"]')
|
||||
archives.forEach((tbody) => {
|
||||
const rows = tbody.querySelectorAll('tr')
|
||||
rows.forEach((row) => {
|
||||
row.style.display = ''
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
elSearchDevis.addEventListener('input', function () {
|
||||
const term = this.value.trim()
|
||||
|
||||
if (searchTimeout) {
|
||||
clearTimeout(searchTimeout)
|
||||
}
|
||||
|
||||
if (term.length >= 3) {
|
||||
elBtnResetSearch.style.display = 'inline-block'
|
||||
sessionStorage.setItem('devisSearchTerm', term)
|
||||
searchTimeout = setTimeout(() => {
|
||||
performSearch(term)
|
||||
}, 300)
|
||||
} else if (term.length === 0) {
|
||||
resetSearch()
|
||||
} else {
|
||||
elBtnResetSearch.style.display = 'none'
|
||||
}
|
||||
})
|
||||
|
||||
elBtnResetSearch.addEventListener('click', function () {
|
||||
resetSearch()
|
||||
})
|
||||
|
||||
restoreSearch()
|
||||
|
||||
initTableSort()
|
||||
|
||||
elBtnSideBarDevis.addEventListener('click', function () {
|
||||
if (elVerticalBar.style.width == '10px') {
|
||||
elVerticalBar.style.width = '1100px' // Largeur de la barre lorsqu'elle est ouverte
|
||||
|
||||
@@ -11,6 +11,100 @@ let oldIdLnEnCours;
|
||||
let oldIdLnArchives;
|
||||
let nbCommentChat = 0;
|
||||
let selectedXmlDevis = new Set();
|
||||
let searchSapTimeout = null;
|
||||
|
||||
const tableSortStates = new Map();
|
||||
|
||||
function initTableSort() {
|
||||
const allTables = document.querySelectorAll('[id^="tblDos"], [id^="tblDosArch"]');
|
||||
|
||||
allTables.forEach(table => {
|
||||
const tableId = table.id;
|
||||
const headers = table.querySelectorAll('th[data-sortable="true"]');
|
||||
const tbody = table.querySelector('tbody');
|
||||
|
||||
if (!tbody) return;
|
||||
|
||||
if (!tableSortStates.has(tableId)) {
|
||||
tableSortStates.set(tableId, {
|
||||
originalOrder: null,
|
||||
currentSort: { column: null, direction: null }
|
||||
});
|
||||
}
|
||||
|
||||
headers.forEach(header => {
|
||||
header.addEventListener('click', function() {
|
||||
const columnIndex = parseInt(this.getAttribute('data-column-index'));
|
||||
const sortType = this.getAttribute('data-sort-type');
|
||||
sortTable(tableId, columnIndex, sortType, this);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function sortTable(tableId, columnIndex, sortType, headerElement) {
|
||||
const table = document.getElementById(tableId);
|
||||
const tbody = table.querySelector('tbody');
|
||||
const rows = Array.from(tbody.querySelectorAll('tr'));
|
||||
const state = tableSortStates.get(tableId);
|
||||
|
||||
if (!state.originalOrder) {
|
||||
state.originalOrder = rows.slice();
|
||||
}
|
||||
|
||||
const allHeaders = table.querySelectorAll('th[data-sortable="true"]');
|
||||
allHeaders.forEach(h => h.style.fontWeight = 'normal');
|
||||
|
||||
let sortedRows;
|
||||
|
||||
if (state.currentSort.column === columnIndex && state.currentSort.direction === 'desc') {
|
||||
sortedRows = state.originalOrder.slice();
|
||||
state.currentSort = { column: null, direction: null };
|
||||
} else {
|
||||
const direction = (state.currentSort.column === columnIndex && state.currentSort.direction === 'asc') ? 'desc' : 'asc';
|
||||
|
||||
sortedRows = rows.slice().sort((a, b) => {
|
||||
const aCell = a.cells[columnIndex];
|
||||
const bCell = b.cells[columnIndex];
|
||||
|
||||
if (!aCell || !bCell) return 0;
|
||||
|
||||
let aValue = aCell.textContent.trim();
|
||||
let bValue = bCell.textContent.trim();
|
||||
|
||||
if (sortType === 'number') {
|
||||
aValue = aValue.replace(/[^\d,.-]/g, '').replace(',', '.');
|
||||
bValue = bValue.replace(/[^\d,.-]/g, '').replace(',', '.');
|
||||
aValue = parseFloat(aValue) || 0;
|
||||
bValue = parseFloat(bValue) || 0;
|
||||
return direction === 'asc' ? aValue - bValue : bValue - aValue;
|
||||
} else if (sortType === 'date') {
|
||||
aValue = parseDateFromText(aValue);
|
||||
bValue = parseDateFromText(bValue);
|
||||
if (!aValue && !bValue) return 0;
|
||||
if (!aValue) return direction === 'asc' ? 1 : -1;
|
||||
if (!bValue) return direction === 'asc' ? -1 : 1;
|
||||
return direction === 'asc' ? aValue - bValue : bValue - aValue;
|
||||
} else {
|
||||
const comparison = aValue.localeCompare(bValue, 'fr');
|
||||
return direction === 'asc' ? comparison : -comparison;
|
||||
}
|
||||
});
|
||||
|
||||
state.currentSort = { column: columnIndex, direction };
|
||||
headerElement.style.fontWeight = 'bold';
|
||||
}
|
||||
|
||||
tbody.innerHTML = '';
|
||||
sortedRows.forEach(row => tbody.appendChild(row));
|
||||
}
|
||||
|
||||
function parseDateFromText(dateText) {
|
||||
const match = dateText.match(/(\d{2})\/(\d{2})[\/\s](\d{4})/);
|
||||
if (!match) return null;
|
||||
const [, day, month, year] = match;
|
||||
return new Date(year, month - 1, day);
|
||||
}
|
||||
|
||||
window.addEventListener('DOMContentLoaded', (event) => {
|
||||
console.log('#');
|
||||
@@ -786,6 +880,201 @@ window.addEventListener('DOMContentLoaded', (event) => {
|
||||
return false;
|
||||
};
|
||||
|
||||
// Fonctions de recherche SAP
|
||||
let elSearchSAP = document.getElementById('searchSAP');
|
||||
let elBtnResetSearchSAP = document.getElementById('btnResetSearchSAP');
|
||||
|
||||
function restoreSearchSAP() {
|
||||
const storageKey = panel === 'enCours' ? 'sapSearchTermEnCours' : 'sapSearchTermArchives';
|
||||
const savedTerm = sessionStorage.getItem(storageKey);
|
||||
if (savedTerm && savedTerm.length >= 3) {
|
||||
elSearchSAP.value = savedTerm;
|
||||
elBtnResetSearchSAP.style.display = 'inline-block';
|
||||
performSearchSAP(savedTerm);
|
||||
} else {
|
||||
elSearchSAP.value = '';
|
||||
elBtnResetSearchSAP.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function performSearchSAP(term) {
|
||||
if (term.length < 3) {
|
||||
return;
|
||||
}
|
||||
|
||||
const context = panel === 'archives' ? 'archives' : 'encours';
|
||||
|
||||
fetch('/jxdevis/search_devis_sap', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
term: term,
|
||||
context: context,
|
||||
}),
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
if (data.success) {
|
||||
const devisIds = data.devis.map((d) => d.rowid);
|
||||
filterDevisTablesSAP(devisIds, context);
|
||||
updateBadgesSAP(data.nb_devis);
|
||||
} else {
|
||||
console.error('Erreur recherche:', data.message);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Erreur AJAX:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function filterDevisTablesSAP(devisIds, context) {
|
||||
if (context === 'encours') {
|
||||
const statuts = document.querySelectorAll('[id^="tblBodyDos"]');
|
||||
statuts.forEach((tbody) => {
|
||||
const rows = tbody.querySelectorAll('tr.ligEnCours');
|
||||
rows.forEach((row) => {
|
||||
const cells = row.querySelectorAll('.celEnCours');
|
||||
if (cells.length > 0) {
|
||||
const rowId = parseInt(cells[0].getAttribute('data-rid'));
|
||||
if (devisIds.includes(rowId)) {
|
||||
row.style.display = '';
|
||||
} else {
|
||||
row.style.display = 'none';
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
const archives = document.querySelectorAll('[id^="tblBodyDosArch"]');
|
||||
archives.forEach((tbody) => {
|
||||
const rows = tbody.querySelectorAll('tr');
|
||||
rows.forEach((row) => {
|
||||
if (row.id && (row.id.startsWith('trArch_') || row.id.startsWith('trArchTous_'))) {
|
||||
const rowId = parseInt(row.id.replace('trArch_', '').replace('trArchTous_', ''));
|
||||
if (devisIds.includes(rowId)) {
|
||||
row.style.display = '';
|
||||
} else {
|
||||
row.style.display = 'none';
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function updateBadgesSAP(nbDevis) {
|
||||
Object.keys(nbDevis).forEach((statutId) => {
|
||||
const liElements = document.querySelectorAll('[id^="liStat"]');
|
||||
liElements.forEach((li) => {
|
||||
li.setAttribute('data-after-text', nbDevis[statutId] || '0');
|
||||
li.setAttribute('data-after-type', 'orange badge top left');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function resetSearchSAP() {
|
||||
elSearchSAP.value = '';
|
||||
elBtnResetSearchSAP.style.display = 'none';
|
||||
const storageKey = panel === 'enCours' ? 'sapSearchTermEnCours' : 'sapSearchTermArchives';
|
||||
sessionStorage.removeItem(storageKey);
|
||||
|
||||
const context = panel === 'archives' ? 'archives' : 'encours';
|
||||
if (context === 'encours') {
|
||||
const statuts = document.querySelectorAll('[id^="tblBodyDos"]');
|
||||
statuts.forEach((tbody) => {
|
||||
const rows = tbody.querySelectorAll('tr');
|
||||
rows.forEach((row) => {
|
||||
row.style.display = '';
|
||||
});
|
||||
});
|
||||
} else {
|
||||
const archives = document.querySelectorAll('[id^="tblBodyDosArch"]');
|
||||
archives.forEach((tbody) => {
|
||||
const rows = tbody.querySelectorAll('tr');
|
||||
rows.forEach((row) => {
|
||||
row.style.display = '';
|
||||
});
|
||||
});
|
||||
const tbodyTous = document.getElementById('tblBodyDosArchTous');
|
||||
if (tbodyTous) {
|
||||
const rowsTous = tbodyTous.querySelectorAll('tr');
|
||||
rowsTous.forEach((row) => {
|
||||
row.style.display = '';
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
elSearchSAP.addEventListener('input', function () {
|
||||
const term = this.value.trim();
|
||||
|
||||
if (searchSapTimeout) {
|
||||
clearTimeout(searchSapTimeout);
|
||||
}
|
||||
|
||||
if (term.length >= 3) {
|
||||
elBtnResetSearchSAP.style.display = 'inline-block';
|
||||
const storageKey = panel === 'enCours' ? 'sapSearchTermEnCours' : 'sapSearchTermArchives';
|
||||
sessionStorage.setItem(storageKey, term);
|
||||
searchSapTimeout = setTimeout(() => {
|
||||
performSearchSAP(term);
|
||||
}, 300);
|
||||
} else if (term.length === 0) {
|
||||
resetSearchSAP();
|
||||
} else {
|
||||
elBtnResetSearchSAP.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
elBtnResetSearchSAP.addEventListener('click', function () {
|
||||
resetSearchSAP();
|
||||
});
|
||||
|
||||
// Hook sur changement d'onglet pour restaurer la recherche appropriée
|
||||
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
|
||||
if ($(this).attr('href') == '#tabEnCours') {
|
||||
panel = 'enCours';
|
||||
restoreSearchSAP();
|
||||
} else if ($(this).attr('href') == '#tabArchives') {
|
||||
panel = 'archives';
|
||||
restoreSearchSAP();
|
||||
}
|
||||
});
|
||||
|
||||
restoreSearchSAP();
|
||||
|
||||
initTableSort();
|
||||
|
||||
// Gestion des états actifs des onglets départements (multiples <ul>)
|
||||
// Utiliser l'événement Bootstrap 'shown.bs.tab' au lieu de 'click'
|
||||
const allDeptTabs = document.querySelectorAll('.dept-tab a');
|
||||
allDeptTabs.forEach((tab) => {
|
||||
$(tab).on('shown.bs.tab', function(e) {
|
||||
// Retirer 'active' de tous les onglets départements sauf celui-ci
|
||||
document.querySelectorAll('.dept-tab').forEach((li) => {
|
||||
if (li !== this.parentElement) {
|
||||
li.classList.remove('active');
|
||||
}
|
||||
});
|
||||
// Retirer 'active' de l'onglet "Tous"
|
||||
const tousLi = document.querySelector('a[href="#dosArchTous"]').parentElement;
|
||||
tousLi.classList.remove('active');
|
||||
});
|
||||
});
|
||||
|
||||
// Gestion du clic sur "Tous"
|
||||
const tousTab = document.querySelector('a[href="#dosArchTous"]');
|
||||
if (tousTab) {
|
||||
$(tousTab).on('shown.bs.tab', function(e) {
|
||||
// Retirer 'active' de tous les onglets départements
|
||||
document.querySelectorAll('.dept-tab').forEach((li) => {
|
||||
li.classList.remove('active');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Add new functions
|
||||
function updateExportButton() {
|
||||
if (selectedXmlDevis.size > 0) {
|
||||
|
||||
214
views/vdevis.php
214
views/vdevis.php
@@ -16,6 +16,16 @@ ob_start();
|
||||
<div id="vb-buttons" class="mb-1">
|
||||
<button class="btn btn-default" id="btnDevisArchives" title="Voir les devis archivés"><i class="fa fa-stack-overflow fa-lg"></i> Devis archivés</button>
|
||||
<button class="btn btn-success" id="btnCreateDevis" title="Créer un nouveau devis"><i class="fa fa-plus fa-lg"></i> Créer un devis</button>
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
<div class="input-group mt-1">
|
||||
<input type="text" class="form-control" id="searchDevis" placeholder="Rechercher un devis par le nom du client, du contact, ville, opportunité..." maxlength="30">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-times clickable" id="btnResetSearch" style="display:none;"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
if ($aModel["last_devis"] > 0) {
|
||||
@@ -87,21 +97,23 @@ ob_start();
|
||||
echo '<div class="border cm-scrollbar cm-table-w-scroll table-responsive table-800">';
|
||||
echo '<table class="table table-responsive table-bordered table-fixed" id="tblDos' . $iDos . '">';
|
||||
echo '<thead><tr>';
|
||||
echo '<th class="header" scope="col" width="5%">#</th>';
|
||||
echo '<th class="header" scope="col" width="10%">Demande</th>';
|
||||
echo '<th class="header clickable" scope="col" width="5%" data-sortable="true" data-sort-type="number" data-column-index="0">#</th>';
|
||||
echo '<th class="header clickable" scope="col" width="10%" data-sortable="true" data-sort-type="date" data-column-index="1">Demande</th>';
|
||||
$colIndex = 2;
|
||||
if (($fkRole == 2 && $iDos == 2) || ($fkRole == 1 && $iDos == 1)) {
|
||||
// C'est un DV et sur le dossier Encours de validation DV
|
||||
// Ou le Dir-CO sur le dossier de validation DIR-CO
|
||||
// on affiche la colonne du nom du RR pour qu'il puisse savoir qui a fait la demande
|
||||
echo '<th class="header" scope="col" width="15%">RR</th>';
|
||||
echo '<th class="header clickable" scope="col" width="15%" data-sortable="true" data-sort-type="text" data-column-index="' . $colIndex . '">RR</th>';
|
||||
$colIndex++;
|
||||
}
|
||||
echo '<th class="header" scope="col" width="10%">Opport.</th>';
|
||||
echo '<th class="header" scope="col" width="5%">CP</th>';
|
||||
echo '<th class="header" scope="col" width="10%">Ville</th>';
|
||||
echo '<th class="header" scope="col" width="15%">Client</th>';
|
||||
echo '<th class="header" scope="col" width="10%">Marché</th>';
|
||||
echo '<th class="header" scope="col" width="10%">Total HT</th>';
|
||||
echo '<th class="header" scope="col" width="10%">Marge Totale</th>';
|
||||
echo '<th class="header clickable" scope="col" width="10%" data-sortable="true" data-sort-type="text" data-column-index="' . $colIndex . '">Opport.</th>';
|
||||
echo '<th class="header clickable" scope="col" width="5%" data-sortable="true" data-sort-type="number" data-column-index="' . ($colIndex + 1) . '">CP</th>';
|
||||
echo '<th class="header clickable" scope="col" width="10%" data-sortable="true" data-sort-type="text" data-column-index="' . ($colIndex + 2) . '">Ville</th>';
|
||||
echo '<th class="header clickable" scope="col" width="15%" data-sortable="true" data-sort-type="text" data-column-index="' . ($colIndex + 3) . '">Client</th>';
|
||||
echo '<th class="header clickable" scope="col" width="10%" data-sortable="true" data-sort-type="text" data-column-index="' . ($colIndex + 4) . '">Marché</th>';
|
||||
echo '<th class="header clickable" scope="col" width="10%" data-sortable="true" data-sort-type="number" data-column-index="' . ($colIndex + 5) . '">Total HT</th>';
|
||||
echo '<th class="header clickable" scope="col" width="10%" data-sortable="true" data-sort-type="number" data-column-index="' . ($colIndex + 6) . '">Marge Totale</th>';
|
||||
echo '<th class="header" scope="col" width="12%"></th>';
|
||||
echo '</tr></thead>';
|
||||
echo '<tbody id="tblBodyDos' . $iDos . '">';
|
||||
@@ -203,14 +215,14 @@ ob_start();
|
||||
echo '<div class="border cm-scrollbar cm-table-w-scroll table-responsive table-800">';
|
||||
echo '<table class="table table-responsive table-bordered table-fixed" id="tblDosArch' . $iDos . '">';
|
||||
echo '<thead><tr>';
|
||||
echo '<th class="header" scope="col" width="5%">#</th>';
|
||||
echo '<th class="header" scope="col" width="10%">Demande</th>';
|
||||
echo '<th class="header" scope="col" width="10%">Opport.</th>';
|
||||
echo '<th class="header" scope="col" width="10%">Ville</th>';
|
||||
echo '<th class="header" scope="col" width="20%">Client</th>';
|
||||
echo '<th class="header" scope="col" width="10%">Marché</th>';
|
||||
echo '<th class="header" scope="col" width="10%">Total HT</th>';
|
||||
echo '<th class="header" scope="col" width="10%">Marge Totale</th>';
|
||||
echo '<th class="header clickable" scope="col" width="5%" data-sortable="true" data-sort-type="number" data-column-index="0">#</th>';
|
||||
echo '<th class="header clickable" scope="col" width="10%" data-sortable="true" data-sort-type="date" data-column-index="1">Demande</th>';
|
||||
echo '<th class="header clickable" scope="col" width="10%" data-sortable="true" data-sort-type="text" data-column-index="2">Opport.</th>';
|
||||
echo '<th class="header clickable" scope="col" width="10%" data-sortable="true" data-sort-type="text" data-column-index="3">Ville</th>';
|
||||
echo '<th class="header clickable" scope="col" width="20%" data-sortable="true" data-sort-type="text" data-column-index="4">Client</th>';
|
||||
echo '<th class="header clickable" scope="col" width="10%" data-sortable="true" data-sort-type="text" data-column-index="5">Marché</th>';
|
||||
echo '<th class="header clickable" scope="col" width="10%" data-sortable="true" data-sort-type="number" data-column-index="6">Total HT</th>';
|
||||
echo '<th class="header clickable" scope="col" width="10%" data-sortable="true" data-sort-type="number" data-column-index="7">Marge Totale</th>';
|
||||
echo '<th class="header" scope="col" width="10%"></th>';
|
||||
echo '</tr></thead>';
|
||||
echo '<tbody id="tblBodyDosArch' . $iDos . '">';
|
||||
@@ -376,34 +388,34 @@ ob_start();
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="inp_contact_nom">Contact :</label>
|
||||
<label class="control-label col-md-2" for="sel_contact">Contact :</label>
|
||||
<div class="col-md-4">
|
||||
<input type="text" class="form-control" id="inp_contact_nom" name="contact_nom" placeholder="Nom" required="required"/>
|
||||
<p class="help-block">Nom du contact</p>
|
||||
<select class="form-control" id="sel_contact" name="fk_contact" required="required">
|
||||
<option value="0">- Sélectionner un contact -</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<input type="text" class="form-control" id="inp_contact_prenom" name="contact_prenom" placeholder="Prénom" required="required"/>
|
||||
<p class="help-block">Prénom du contact</p>
|
||||
<div class="col-md-3">
|
||||
<button type="button" class="btn btn-primary" id="btnGererContacts" disabled>Gérer les contacts</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="divContactInfos" style="display: none;">
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-2" for="inp_contact_fonction">Fonction du contact :</label>
|
||||
<div class="col-md-3">
|
||||
<input type="text" class="form-control" id="inp_contact_fonction" name="contact_fonction" required="required"/>
|
||||
<label class="control-label col-md-2"></label>
|
||||
<div class="col-md-8">
|
||||
<div class="well well-sm">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<p class="mb-0"><strong>Nom et Prénom :</strong> <span id="info_contact_nom_prenom"></span></p>
|
||||
<p class="mb-0"><strong>Fonction :</strong> <span id="info_contact_fonction"></span></p>
|
||||
<p class="mb-0"><strong>Email :</strong> <span id="info_contact_email"></span></p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<p class="mb-0"><strong>Téléphone :</strong> <span id="info_contact_telephone"></span></p>
|
||||
<p class="mb-0"><strong>Mobile :</strong> <span id="info_contact_mobile"></span></p>
|
||||
</div>
|
||||
<label class="control-label col-md-2" for="inp_email">Email :</label>
|
||||
<div class="col-md-3">
|
||||
<input type="text" class="form-control" id="inp_email" name="email" required="required"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class=" form-group">
|
||||
<label class=" control-label col-md-2" for="inp_telephone">Tél :</label>
|
||||
<div class="col-md-2">
|
||||
<input type="text" class="form-control" id="inp_telephone" name="telephone" size="10" maxlength="18" placeholder="Fixe"/>
|
||||
</div>
|
||||
<label class=" control-label col-md-2" for="inp_mobile">Mob :</label>
|
||||
<div class="col-md-2">
|
||||
<input type="text" class="form-control" id="inp_mobile" name="mobile" size=" 10" maxlength="18" placeholder="Mobile"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@@ -556,25 +568,14 @@ ob_start();
|
||||
<input type="text" class="form-control numeric" id="inpTotalHT" name="inpTotalHT" readonly="readonly" tabindex="-1" size="12" maxlength="12"/>
|
||||
<div class="input-group-addon">€</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<?php
|
||||
if ($Conf->_devIp) {
|
||||
echo '<div class="form-group">';
|
||||
echo '<label for="inpCoutTotalAchat">Coût total achat :</label>';
|
||||
}
|
||||
?>
|
||||
</td>
|
||||
<td>
|
||||
<?php
|
||||
if ($Conf->_devIp) {
|
||||
echo '<div class="input-group">';
|
||||
echo '<input type="text" class="form-control numeric" id="inpCoutTotalAchat" name="inpCoutTotalAchat" readonly="readonly" tabindex="-1" size="12" maxlength="12"/>';
|
||||
echo '<div class="input-group-addon">€</div>';
|
||||
echo '</div></div>';
|
||||
echo '<input type="hidden" id="inpCoutTotalAchat" name="inpCoutTotalAchat"/>';
|
||||
}
|
||||
?>
|
||||
</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
@@ -952,6 +953,117 @@ ob_start();
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal draggable fade" id="modalGererContacts" tabindex="-1" role="dialog" aria-labelledby="modalGererContactsTitre" data-backdrop="static">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modGererContactsTitre"><i class="fa fa-address-book fa-lg"></i> Gestion des contacts du client</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="inp_fk_client_contacts" value="0"/>
|
||||
<div class="row mb-2">
|
||||
<div class="col-md-12">
|
||||
<button type="button" class="btn btn-success btn-sm" id="btnNouveauContact"><i class="fa fa-plus"></i> Nouveau contact</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-striped" id="tblContacts">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="20%">Nom</th>
|
||||
<th width="15%">Prénom</th>
|
||||
<th width="20%">Fonction</th>
|
||||
<th width="15%">Téléphone</th>
|
||||
<th width="20%">Email</th>
|
||||
<th width="10%" class="text-center">Principal</th>
|
||||
<th width="15%" class="text-center">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" id="btnFermerContacts">Fermer</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal draggable fade" id="modalEditContact" tabindex="-1" role="dialog" aria-labelledby="modalEditContactTitre" data-backdrop="static">
|
||||
<div class="modal-dialog modal-md">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="modEditContactTitre"><i class="fa fa-user fa-lg"></i> <span id="modEditContactTitreText">Nouveau contact</span></h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="frmEditContact" class="form-horizontal" autocomplete="off">
|
||||
<input type="hidden" id="inp_contact_rowid" name="rowid" value="0"/>
|
||||
<input type="hidden" id="inp_contact_fk_client" name="fk_client" value="0"/>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-3" for="inp_contact_nom_edit">Nom :</label>
|
||||
<div class="col-md-8">
|
||||
<input type="text" class="form-control" id="inp_contact_nom_edit" name="nom" maxlength="50" required/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-3" for="inp_contact_prenom_edit">Prénom :</label>
|
||||
<div class="col-md-8">
|
||||
<input type="text" class="form-control" id="inp_contact_prenom_edit" name="prenom" maxlength="50" required/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-3" for="inp_contact_fonction_edit">Fonction :</label>
|
||||
<div class="col-md-8">
|
||||
<input type="text" class="form-control" id="inp_contact_fonction_edit" name="fonction" maxlength="50"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-3" for="inp_contact_email_edit">Email :</label>
|
||||
<div class="col-md-8">
|
||||
<input type="email" class="form-control" id="inp_contact_email_edit" name="email" maxlength="75"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-3" for="inp_contact_telephone_edit">Téléphone :</label>
|
||||
<div class="col-md-8">
|
||||
<input type="text" class="form-control" id="inp_contact_telephone_edit" name="telephone" maxlength="20"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-3" for="inp_contact_mobile_edit">Mobile :</label>
|
||||
<div class="col-md-8">
|
||||
<input type="text" class="form-control" id="inp_contact_mobile_edit" name="mobile" maxlength="20"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="control-label col-md-3" for="inp_contact_principal_edit">Contact principal :</label>
|
||||
<div class="col-md-8">
|
||||
<input type="checkbox" id="inp_contact_principal_edit" name="principal" value="1"/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" id="btnCancelEditContact">Annuler</button>
|
||||
<button type="button" class="btn btn-success" id="btnSaveEditContact">Enregistrer</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
$modal = ob_get_contents();
|
||||
ob_clean();
|
||||
|
||||
175
views/vsap.php
175
views/vsap.php
@@ -5,7 +5,37 @@ $metacss = '<link href="/pub/res/css/schat.css" rel="stylesheet" type="text/css"
|
||||
$barre = "";
|
||||
ob_start();
|
||||
?>
|
||||
<div id="divSAP">
|
||||
<style>
|
||||
.dept-tab a {
|
||||
min-width: 50px !important;
|
||||
padding: 8px 10px !important;
|
||||
font-size: 13px;
|
||||
}
|
||||
.table-800 {
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
#tabArchives .table-800 {
|
||||
max-height: 680px !important;
|
||||
}
|
||||
.table-800 thead th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
</style>
|
||||
<div class="row" style="margin-bottom: 20px;">
|
||||
<div class="col-md-2"></div>
|
||||
<div class="col-md-8">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="searchSAP" placeholder="Rechercher un devis par le nom du client, du contact, ville, opportunité..." maxlength="30">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-times clickable" id="btnResetSearchSAP" style="display:none;"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2"></div>
|
||||
</div>
|
||||
<div id="divSAP" style="margin-top: 20px;">
|
||||
<ul class="nav nav-tabs nav-justified" role="tablist">
|
||||
<li role="presentation" class="active"><a href="#tabEnCours" aria-controls="tabEnCours" role="tab" data-toggle="tab">Les devis en cours</a></li>
|
||||
<li role="presentation"><a href="#tabArchives" aria-controls="tabArchives" role="tab" data-toggle="tab">Les devis archivés</a></li>
|
||||
@@ -40,20 +70,22 @@ ob_start();
|
||||
echo '<div class="border cm-scrollbar cm-table-w-scroll table-responsive table-800">';
|
||||
echo '<table class="table table-responsive table-bordered table-fixed" id="tblDos' . $iDos . '">';
|
||||
echo '<thead><tr>';
|
||||
echo '<th class="header text-center" scope="col" width="5%">#</th>';
|
||||
echo '<th class="header text-center" scope="col" width="10%">Date Demande</th>';
|
||||
echo '<th class="header text-center" scope="col" width="10%">Date Remise</th>';
|
||||
echo '<th class="header text-center" scope="col" width="10%">Responsable Régional</th>';
|
||||
echo '<th class="header text-center" scope="col" width="10%">Code Etabliss.</th>';
|
||||
echo '<th class="header text-center" scope="col" width="15%">Etablissement</th>';
|
||||
echo '<th class="header text-center" scope="col" width="7%">CP</th>';
|
||||
echo '<th class="header text-center" scope="col" width="10%">Ville</th>';
|
||||
echo '<th class="header text-center" scope="col" width="10%">Marché</th>';
|
||||
echo '<th class="header text-center" scope="col" width="10%">Montant Total HT</th>';
|
||||
echo '<th class="header text-center" scope="col" width="10%">Marge totale</th>';
|
||||
echo '<th class="header text-center clickable" scope="col" width="5%" data-sortable="true" data-sort-type="number" data-column-index="0">#</th>';
|
||||
echo '<th class="header text-center clickable" scope="col" width="10%" data-sortable="true" data-sort-type="date" data-column-index="1">Date Demande</th>';
|
||||
echo '<th class="header text-center clickable" scope="col" width="10%" data-sortable="true" data-sort-type="date" data-column-index="2">Date Remise</th>';
|
||||
echo '<th class="header text-center clickable" scope="col" width="10%" data-sortable="true" data-sort-type="text" data-column-index="3">Responsable Régional</th>';
|
||||
echo '<th class="header text-center clickable" scope="col" width="10%" data-sortable="true" data-sort-type="text" data-column-index="4">Code Etabliss.</th>';
|
||||
echo '<th class="header text-center clickable" scope="col" width="15%" data-sortable="true" data-sort-type="text" data-column-index="5">Etablissement</th>';
|
||||
echo '<th class="header text-center clickable" scope="col" width="7%" data-sortable="true" data-sort-type="number" data-column-index="6">CP</th>';
|
||||
echo '<th class="header text-center clickable" scope="col" width="10%" data-sortable="true" data-sort-type="text" data-column-index="7">Ville</th>';
|
||||
echo '<th class="header text-center clickable" scope="col" width="10%" data-sortable="true" data-sort-type="text" data-column-index="8">Marché</th>';
|
||||
echo '<th class="header text-center clickable" scope="col" width="10%" data-sortable="true" data-sort-type="number" data-column-index="9">Montant Total HT</th>';
|
||||
echo '<th class="header text-center clickable" scope="col" width="10%" data-sortable="true" data-sort-type="number" data-column-index="10">Marge totale</th>';
|
||||
$colIndexActions = 11;
|
||||
if ($dossier["rowid"] == 7) {
|
||||
// Si le dossier est "A envoyer au client", on affiche la colonne "Email"
|
||||
echo '<th class="header text-center" scope="col" width="10%">Email</th>';
|
||||
$colIndexActions = 12;
|
||||
}
|
||||
echo '<th class="header text-center" scope="col" width="20%">Actions <button class="btn btn-info btn-xs btnExportSelectedXML hidden" title="Export XML SAP des devis sélectionnés"><i class="fa fa-scribd fa-lg"></i></button></th>';
|
||||
echo '</tr></thead>';
|
||||
@@ -167,39 +199,118 @@ ob_start();
|
||||
<div role="tabpanel" class="tab-pane" id="tabArchives">
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
<ul class="nav nav-tabs nav-justified" role="tablist">
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li role="presentation" class="active"><a href="#dosArchTous" aria-controls="dosArchTous" role="tab" data-toggle="tab">Tous</a></li>
|
||||
</ul>
|
||||
<?php
|
||||
$i = 0;
|
||||
foreach ($aModel["dossiers"] as $dossier) {
|
||||
if ($i % 15 == 0 && $i > 0) {
|
||||
echo '<div class="spacer"></div>';
|
||||
}
|
||||
$active = ($i == 0) ? "active" : "";
|
||||
$nbPerLine = 25;
|
||||
$totalDepts = count($aModel["dossiers"]);
|
||||
|
||||
for ($line = 0; $line < 4; $line++) {
|
||||
echo '<ul class="nav nav-tabs" role="tablist">';
|
||||
$start = $line * $nbPerLine;
|
||||
$end = min($start + $nbPerLine, $totalDepts);
|
||||
|
||||
for ($j = $start; $j < $end; $j++) {
|
||||
$dossier = $aModel["dossiers"][$j];
|
||||
$ceDossier = ($dossier["dossier"] == "") ? "?" : $dossier["dossier"];
|
||||
echo '<li role="presentation" class="' . $active . '"><a href="#dosArch' . $i . '" aria-controls="dosArch' . $i . '" role="tab" data-toggle="tab">' . $ceDossier . '</a></li>';
|
||||
$i++;
|
||||
echo '<li role="presentation" class="dept-tab"><a href="#dosArch' . $j . '" aria-controls="dosArch' . $j . '" role="tab" data-toggle="tab">' . $ceDossier . '</a></li>';
|
||||
}
|
||||
echo '</ul>';
|
||||
}
|
||||
?>
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane p-0 active" id="dosArchTous">
|
||||
<div class="border cm-scrollbar cm-table-w-scroll table-responsive table-800">
|
||||
<table class="table table-responsive table-bordered table-fixed" id="tblDosArchTous">
|
||||
<thead><tr>
|
||||
<th class="header text-center clickable" scope="col" width="5%" data-sortable="true" data-sort-type="number" data-column-index="0">#</th>
|
||||
<th class="header text-center clickable" scope="col" width="10%" data-sortable="true" data-sort-type="date" data-column-index="1">Date Demande</th>
|
||||
<th class="header text-center clickable" scope="col" width="10%" data-sortable="true" data-sort-type="text" data-column-index="2">Resp. Régional</th>
|
||||
<th class="header text-center clickable" scope="col" width="7%" data-sortable="true" data-sort-type="text" data-column-index="3">Code Etabliss.</th>
|
||||
<th class="header text-center clickable" scope="col" width="15%" data-sortable="true" data-sort-type="text" data-column-index="4">Etablissement</th>
|
||||
<th class="header text-center clickable" scope="col" width="5%" data-sortable="true" data-sort-type="number" data-column-index="5">CP</th>
|
||||
<th class="header text-center clickable" scope="col" width="10%" data-sortable="true" data-sort-type="text" data-column-index="6">Ville</th>
|
||||
<th class="header text-center clickable" scope="col" width="8%" data-sortable="true" data-sort-type="text" data-column-index="7">Marché</th>
|
||||
<th class="header text-center clickable" scope="col" width="5%" data-sortable="true" data-sort-type="text" data-column-index="8">Dép.</th>
|
||||
<th class="header text-center clickable" scope="col" width="10%" data-sortable="true" data-sort-type="number" data-column-index="9">Montant Total HT</th>
|
||||
<th class="header text-center clickable" scope="col" width="10%" data-sortable="true" data-sort-type="number" data-column-index="10">Marge totale</th>
|
||||
<th class="header text-center" scope="col" width="10%">Actions</th>
|
||||
</tr></thead>
|
||||
<tbody id="tblBodyDosArchTous">
|
||||
<?php
|
||||
$i = 0;
|
||||
foreach ($aModel["devisArchives"] as $devis) {
|
||||
echo '<tr id="trArchTous_' . $devis["rowid"] . '">';
|
||||
echo '<td class="text-center">' . $devis["rowid"] . '</td>';
|
||||
$dateDem = substr($devis["date_demande"], 8, 2) . '/' . substr($devis["date_demande"], 5, 2) . ' ' . substr($devis["date_demande"], 0, 4);
|
||||
if ($devis["chk_speciaux"] == 1) {
|
||||
$cellDateDem = '<span data-after-text="S" data-after-type="blue circle">' . $dateDem . '</span>';
|
||||
} else {
|
||||
$cellDateDem = $dateDem;
|
||||
}
|
||||
echo '<td class="clickable celArchives" data-rid="' . $devis["rowid"] . '">' . $cellDateDem . '</td>';
|
||||
echo '<td class="clickable celArchives" data-rid="' . $devis["rowid"] . '">' . $devis["prenom"] . " " . $devis["nom"] . '</td>';
|
||||
echo '<td class="clickable celArchives" data-rid="' . $devis["rowid"] . '">' . $devis["code"] . '</td>';
|
||||
|
||||
if ($devis["fk_client"] == 0) {
|
||||
$ville = $devis["ville_new_client"];
|
||||
$libelle = '<span data-after-text="N" data-after-type="red circle">' . $devis["lib_new_client"] . '</span>';
|
||||
} else {
|
||||
$ville = $devis["ville"];
|
||||
$libelle = $devis["libelle"];
|
||||
}
|
||||
echo '<td class="clickable celArchives" data-rid="' . $devis["rowid"] . '">' . $libelle . '</td>';
|
||||
echo '<td class="clickable celArchives" data-rid="' . $devis["rowid"] . '">' . $devis["cp"] . '</td>';
|
||||
echo '<td class="clickable celArchives" data-rid="' . $devis["rowid"] . '">' . $ville . '</td>';
|
||||
echo '<td class="clickable celArchives" data-rid="' . $devis["rowid"] . '">' . $devis["lib_marche"] . '</td>';
|
||||
$dossierLabel = ($devis["dossier"] == "") ? "?" : $devis["dossier"];
|
||||
echo '<td class="clickable celArchives text-center" data-rid="' . $devis["rowid"] . '">' . $dossierLabel . '</td>';
|
||||
$montant = floatval($devis["montant_total_ht_remise"]);
|
||||
echo '<td class="clickable celArchives right" data-rid="' . $devis["rowid"] . '">' . number_format($montant, 2, ',', ' ') . ' €</td>';
|
||||
$margeTotale = floatval($devis["marge_totale"]);
|
||||
echo '<td class="clickable celArchives right" data-rid="' . $devis["rowid"] . '">' . number_format($margeTotale, 2, ',', ' ') . ' %</td>';
|
||||
echo '<td class="text-center">';
|
||||
echo '<div class="btn-group">';
|
||||
echo '<button class="btn btn-primary btn-xs btnViewDevisArchives" title="Consulter le devis" data-rid="' . $devis["rowid"] . '"><i class="fa fa-eye fa-lg"></i></button>';
|
||||
echo '<button class="btn btn-info btn-xs btnExportDevisEnCours" title="Export Excel du devis" data-rid="' . $devis["rowid"] . '" data-libelle="' . $devis["libelle"] . '"><i class="fa fa-file-excel-o fa-lg"></i></button>';
|
||||
$typBtn = "btn-success";
|
||||
foreach ($aModel["medias"] as $media) {
|
||||
if ($media["support_rowid"] == $devis["rowid"]) {
|
||||
$typBtn = "btn-warning";
|
||||
break;
|
||||
}
|
||||
}
|
||||
echo '<button class="btn ' . $typBtn . ' btn-xs btnImportPDFEnCours" title="Consulter le PDF SAP du devis" data-rid="' . $devis["rowid"] . '" data-libelle="' . $devis["libelle"] . '"><i class="fa fa-file-pdf-o fa-lg"></i></button>';
|
||||
echo '</div>';
|
||||
echo '</td></tr>';
|
||||
$i++;
|
||||
}
|
||||
if ($i == 0) echo '<tr><td colspan="12" class="center">Aucun devis archivé trouvé</td></tr>';
|
||||
?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
$iDos = 0;
|
||||
foreach ($aModel["dossiers"] as $dossier) {
|
||||
$active = ($iDos == 0) ? "active" : "";
|
||||
$active = "";
|
||||
echo '<div role="tabpanel" class="tab-pane p-0 ' . $active . '" id="dosArch' . $iDos . '">';
|
||||
echo '<div class="border cm-scrollbar cm-table-w-scroll table-responsive table-800">';
|
||||
echo '<table class="table table-responsive table-bordered table-fixed" id="tblDosArch' . $iDos . '">';
|
||||
echo '<thead><tr>';
|
||||
echo '<th class="header text-center" scope="col" width="5%">#</th>';
|
||||
echo '<th class="header text-center" scope="col" width="10%">Date Demande</th>';
|
||||
echo '<th class="header text-center" scope="col" width="10%">Resp. Régional</th>';
|
||||
echo '<th class="header text-center" scope="col" width="7%">Code Etabliss.</th>';
|
||||
echo '<th class="header text-center" scope="col" width="20%">Etablissement</th>';
|
||||
echo '<th class="header text-center" scope="col" width="5%">CP</th>';
|
||||
echo '<th class="header text-center" scope="col" width="13%">Ville</th>';
|
||||
echo '<th class="header text-center" scope="col" width="10%">Marché</th>';
|
||||
echo '<th class="header text-center" scope="col" width="10%">Montant Total HT</th>';
|
||||
echo '<th class="header text-center" scope="col" width="10%">Marge totale</th>';
|
||||
echo '<th class="header text-center clickable" scope="col" width="5%" data-sortable="true" data-sort-type="number" data-column-index="0">#</th>';
|
||||
echo '<th class="header text-center clickable" scope="col" width="10%" data-sortable="true" data-sort-type="date" data-column-index="1">Date Demande</th>';
|
||||
echo '<th class="header text-center clickable" scope="col" width="10%" data-sortable="true" data-sort-type="text" data-column-index="2">Resp. Régional</th>';
|
||||
echo '<th class="header text-center clickable" scope="col" width="7%" data-sortable="true" data-sort-type="text" data-column-index="3">Code Etabliss.</th>';
|
||||
echo '<th class="header text-center clickable" scope="col" width="20%" data-sortable="true" data-sort-type="text" data-column-index="4">Etablissement</th>';
|
||||
echo '<th class="header text-center clickable" scope="col" width="5%" data-sortable="true" data-sort-type="number" data-column-index="5">CP</th>';
|
||||
echo '<th class="header text-center clickable" scope="col" width="13%" data-sortable="true" data-sort-type="text" data-column-index="6">Ville</th>';
|
||||
echo '<th class="header text-center clickable" scope="col" width="10%" data-sortable="true" data-sort-type="text" data-column-index="7">Marché</th>';
|
||||
echo '<th class="header text-center clickable" scope="col" width="10%" data-sortable="true" data-sort-type="number" data-column-index="8">Montant Total HT</th>';
|
||||
echo '<th class="header text-center clickable" scope="col" width="10%" data-sortable="true" data-sort-type="number" data-column-index="9">Marge totale</th>';
|
||||
echo '<th class="header text-center" scope="col" width="10%">Actions</th>';
|
||||
echo '</tr></thead>';
|
||||
echo '<tbody id="tblBodyDosArch' . $iDos . '">';
|
||||
|
||||
Reference in New Issue
Block a user