Compare commits
No commits in common. "script-migration-rc-fac" and "main" have entirely different histories.
script-mig
...
main
|
|
@ -1,15 +0,0 @@
|
|||
{
|
||||
"keep": {
|
||||
"days": true,
|
||||
"amount": 14
|
||||
},
|
||||
"auditLog": "C:\\Users\\B722DA\\Documents\\etv2\\logs\\.08fe637d6ef44c0231313d33bee64c93b9084214-audit.json",
|
||||
"files": [
|
||||
{
|
||||
"date": 1758200946499,
|
||||
"name": "C:\\Users\\B722DA\\Documents\\etv2\\logs\\easytransport-18092025.log",
|
||||
"hash": "1706becaf705914860a3975811363123969a9384d470caf37ee1ba3b6762cbcc"
|
||||
}
|
||||
],
|
||||
"hashType": "sha256"
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
{
|
||||
"keep": {
|
||||
"days": true,
|
||||
"amount": 14
|
||||
},
|
||||
"auditLog": "C:\\workspace\\etv2\\logs\\.3d67f34307b811ec6c096daeac0152b0eaa334a7-audit.json",
|
||||
"files": [
|
||||
{
|
||||
"date": 1781187673716,
|
||||
"name": "C:\\workspace\\etv2\\logs\\easytransport-11062026.log",
|
||||
"hash": "c19430c072d594d17c7e822d673b22a8a9eb97f9955f27a7543fec107e9788c3"
|
||||
}
|
||||
],
|
||||
"hashType": "sha256"
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
#DB_URL=http://ppsi_nanterre.axa-fr.intraxa:3005/
|
||||
DB_URL=http://127.0.0.1:8091/
|
||||
DB_ADMIN=admin@axa.fr
|
||||
DB_PASSWORD=DTadmin123TT
|
||||
NODE_ENV=developpement
|
||||
PORT=8082
|
||||
|
|
@ -1,368 +0,0 @@
|
|||
/* Historique Parcours */
|
||||
body>main>div>div.section.center-align {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
body>main>div>div.section.center-align>div.container {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
#historiqueParcours_wrapper {
|
||||
width: 100% !important;
|
||||
overflow-x: auto !important;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
table#historiqueParcours {
|
||||
width: 150%;
|
||||
border-collapse: collapse !important;
|
||||
border: 2px solid darkblue !important;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
#historiqueParcours th,
|
||||
#historiqueParcours td {
|
||||
color: black;
|
||||
text-align: left;
|
||||
font-size: 11px;
|
||||
max-width: 100px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
table.dataTable thead th {
|
||||
padding-right: 18px !important;
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
/* Div pour encapsuler le texte dans les cellules d'en-tête */
|
||||
table.dataTable thead th>div {
|
||||
position: absolute !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
}
|
||||
|
||||
#historiqueParcours_filter {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.dataTables_wrapper .dataTables_filter {
|
||||
position: relative;
|
||||
text-align: left;
|
||||
float: left;
|
||||
padding: 2px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/* Cacher complètement le label "Rechercher" de DataTables */
|
||||
.dataTables_wrapper .dataTables_filter label {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
font-size: 0 !important;
|
||||
line-height: 0 !important;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.dataTables_wrapper .dataTables_filter label > span:first-child {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* fond blanc, bordure grise fine, capsule arrondie */
|
||||
.dataTables_wrapper .dataTables_filter input[type="search"] {
|
||||
background-color: #fff;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 42px;
|
||||
outline: none;
|
||||
height: 42px;
|
||||
width: 220px;
|
||||
font-size: 15px;
|
||||
margin: 0;
|
||||
padding: 0 20px 0 52px;
|
||||
box-sizing: border-box;
|
||||
transition: all 0.2s ease;
|
||||
color: #333;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dataTables_wrapper .dataTables_filter input[type="search"]:focus {
|
||||
background-color: #fff;
|
||||
border-color: #1d9bf0;
|
||||
box-shadow: 0 0 0 2px #1d9bf0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.dataTables_wrapper .dataTables_filter input[type="search"]::placeholder {
|
||||
color: #71767a;
|
||||
}
|
||||
|
||||
/* Icône de loupe SVG */
|
||||
.dataTables_wrapper .dataTables_filter label::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2371767a' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cpath d='m21 21-4.35-4.35'/%3E%3C/svg%3E");
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Texte "Rechercher" à côté de l'icône */
|
||||
.dataTables_wrapper .dataTables_filter label::after {
|
||||
content: "Rechercher";
|
||||
position: absolute;
|
||||
left: 52px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 15px;
|
||||
color: #71767a;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Cacher le texte "Rechercher" quand on tape, focus, ou si l'input a une valeur */
|
||||
.dataTables_wrapper .dataTables_filter:focus-within label::after,
|
||||
.dataTables_wrapper .dataTables_filter label.has-value::after {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.dataTables_wrapper .dataTables_filter input[type="search"]:focus::placeholder {
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
#historiqueParcours_length>label {
|
||||
font-size: 14px;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
.dataTables_length select[name="historiqueParcours_length"] {
|
||||
display: block !important;
|
||||
font-size: 14px;
|
||||
color: #555;
|
||||
padding: 8px;
|
||||
margin: 0 0.5em;
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 5px;
|
||||
font-size: 16px;
|
||||
outline: none;
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
/* Style Input recherche par ligne */
|
||||
#historiqueParcours>thead>tr:nth-child(2)>th>input {
|
||||
font-size: 13px !important;
|
||||
padding: 6px !important;
|
||||
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* icone de tri sur les colonnes */
|
||||
table.dataTable thead .sorting,
|
||||
table.dataTable thead .sorting_asc,
|
||||
table.dataTable thead .sorting_desc {
|
||||
background-position: right center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
/* Ajouter un espacement entre le texte et l'icône */
|
||||
table.dataTable thead .sorting:before,
|
||||
table.dataTable thead .sorting_asc:before,
|
||||
table.dataTable thead .sorting_desc:before {
|
||||
content: "";
|
||||
}
|
||||
|
||||
/* boutons de navigation */
|
||||
.dataTables_wrapper .dataTables_paginate .paginate_button {
|
||||
background-color: white !important;
|
||||
border: darkblue solid 1.5px !important;
|
||||
color: black !important;
|
||||
padding: 6px 12px !important;
|
||||
margin: 0 2px !important;
|
||||
cursor: pointer !important;
|
||||
border-radius: 4px !important;
|
||||
transition: background-color 0.3s, color 0.3s, border-color 0.3s !important;
|
||||
}
|
||||
|
||||
#historiqueParcours_paginate>span>a.paginate_button.current,
|
||||
.dataTables_wrapper .dataTables_paginate:hover .paginate_button:hover {
|
||||
background: none !important;
|
||||
background-color: darkblue !important;
|
||||
border-color: white !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
/* NC value */
|
||||
td.nc-value {
|
||||
color: lightgray !important;
|
||||
}
|
||||
|
||||
/* Les bouton pour le filtres et les extraction */
|
||||
#divBtnFilter {
|
||||
display: inline-grid;
|
||||
width: 100%;
|
||||
grid-template-columns: auto;
|
||||
column-gap: 3%;
|
||||
row-gap: 10%;
|
||||
}
|
||||
|
||||
#checkRegionAdmin {
|
||||
grid-column: 1 / span 3;
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
#divToggleSearch {
|
||||
grid-column: 2;
|
||||
grid-row: 3;
|
||||
justify-self: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#toggleSearch {
|
||||
white-space: nowrap;
|
||||
width: auto;
|
||||
min-width: fit-content;
|
||||
padding: 0 24px;
|
||||
color: white !important;
|
||||
background-color: darkblue !important;
|
||||
border: none !important;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(16, 0, 75, 0.2), 0 4px 8px rgba(16, 0, 75, 0.1);
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
#toggleSearch:hover {
|
||||
background-color: #26a69a !important;
|
||||
}
|
||||
|
||||
#toggleSearch:active {
|
||||
background-color: gray !important;
|
||||
}
|
||||
|
||||
#divExtractAll {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
#divExtractFilter {
|
||||
grid-column: 3;
|
||||
grid-row: 1;
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
#divBtnFilter button {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(16, 0, 75, 0.2),
|
||||
0 4px 8px rgba(16, 0, 75, 0.1);
|
||||
}
|
||||
|
||||
/* Bouton Reprendre */
|
||||
#btnReprendre,
|
||||
#btnGenerate {
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 0px 15px;
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
#btnReprendre i {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* checkbox Filter Region Admin a supprimer probablement*/
|
||||
#checkRegionAdmin {
|
||||
border: 1px solid #ccc;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 10px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
[class^="checkbox-wrapper-"] {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
#checkRegionAdmin input[type="checkbox"] {
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#checkRegionAdmin label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#historiqueParcours tr.shown > td { background: #fffdf5; }
|
||||
.parcours-details { font-size: 0.95rem; }
|
||||
|
||||
|
||||
/* Style pour les boutons d'export désactivés */
|
||||
#divBtnFilter button:disabled {
|
||||
opacity: 0.5 !important;
|
||||
cursor: not-allowed !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
#historiqueParcours td .col-with-text {
|
||||
white-space: normal !important;
|
||||
}
|
||||
|
||||
|
||||
#historiqueParcours_wrapper {
|
||||
overflow-x: visible !important;
|
||||
}
|
||||
|
||||
table.dataTable {
|
||||
width: 100% !important;
|
||||
table-layout: auto !important;
|
||||
}
|
||||
|
||||
.col-with-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.col-with-text .np {
|
||||
white-space: normal !important;
|
||||
}
|
||||
|
||||
|
||||
.btn-row-details {
|
||||
cursor: pointer;
|
||||
width: 35px;
|
||||
height: 28px;
|
||||
background-color: #F44336 !important;
|
||||
border-radius: 6px;
|
||||
border: 3px solid darkblue;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
transition: 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
/* petit effet hover propre pour le bouton détails*/
|
||||
.btn-row-details:hover {
|
||||
background-color: #d7372f;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
|
@ -1,279 +0,0 @@
|
|||
// Module IIFE pour éviter la pollution de l'espace global
|
||||
(function () {
|
||||
// Variables globales du module
|
||||
let parcours, contrat, client, matricule;
|
||||
|
||||
// Initialisation du formulaire et des données
|
||||
function init() {
|
||||
const token = localStorage.getItem('jwtToken');
|
||||
|
||||
if (token) {
|
||||
const decoded = jwt_decode(token);
|
||||
matricule = decoded.userMatricule;
|
||||
}
|
||||
|
||||
// Accéder aux informations stockées du parcours
|
||||
parcours = JSON.parse(sessionStorage.getItem('parcours'));
|
||||
contrat = JSON.parse(sessionStorage.getItem('contrat'));
|
||||
client = contrat?.["@expand"]?.client || null;
|
||||
|
||||
console.log("Matricule user actuel:", matricule);
|
||||
console.log("Initialisation pour formulaire client :", contrat);
|
||||
|
||||
// Materialize init Modal
|
||||
var modals = document.querySelectorAll('.modal');
|
||||
M.Modal.init(modals);
|
||||
|
||||
// Appel des différentes fonctions d'initialisation
|
||||
setupEventListeners();
|
||||
populateFormData();
|
||||
updateSubmitButtonState('clientForm');
|
||||
}
|
||||
|
||||
// Configuration des écouteurs d'événements
|
||||
function setupEventListeners() {
|
||||
document.getElementById('clientFormBtn').addEventListener('click', handleSubmitForm);
|
||||
document.getElementById('creditsafe').addEventListener('click', (event) => handleOpenLink(event, 'creditsafeURL'));
|
||||
document.getElementById('easyQP').addEventListener('click', (event) => handleOpenLink(event, 'easyQPURL'));
|
||||
document.getElementById('refNoteFi').addEventListener('click', (event) => handleOpenLink(event, 'refNoteFiURL'));
|
||||
document.getElementById('cl063-client').addEventListener('click', (event) => handleExtractClient(event));
|
||||
document.getElementById('modalExtraireClient').addEventListener('click', (event) => handleModalExtract(event));
|
||||
|
||||
// Controle de saisie et format sur les champs du formulaire
|
||||
document.getElementById('noteFinanciereClient').addEventListener('input', function () {
|
||||
validateField('noteFinanciereClient', true);
|
||||
updateSubmitButtonState('clientForm');
|
||||
});
|
||||
|
||||
document.getElementById('numClient').addEventListener('input', function () {
|
||||
validateField('numClient', true);
|
||||
updateSubmitButtonState('clientForm');
|
||||
});
|
||||
|
||||
document.getElementById('nomClient').addEventListener('input', function () {
|
||||
validateField('nomClient', true);
|
||||
updateSubmitButtonState('clientForm');
|
||||
});
|
||||
|
||||
document.getElementById('emailClient').addEventListener('input', function () {
|
||||
validateField('emailClient', true);
|
||||
updateSubmitButtonState('clientForm');
|
||||
});
|
||||
|
||||
document.getElementById('adresseClient').addEventListener('input', function () {
|
||||
validateField('adresseClient', true);
|
||||
updateSubmitButtonState('clientForm');
|
||||
});
|
||||
|
||||
document.getElementById('codePostalClient').addEventListener('input', function () {
|
||||
validateField('codePostalClient', true);
|
||||
updateSubmitButtonState('clientForm');
|
||||
});
|
||||
|
||||
document.getElementById('villeClient').addEventListener('input', function () {
|
||||
validateField('villeClient', true);
|
||||
updateSubmitButtonState('clientForm');
|
||||
});
|
||||
|
||||
document.getElementById('modalNumClient').addEventListener('input', function () {
|
||||
validateField('modalNumClient', true);
|
||||
updateSubmitButtonState('modalExtraireClient');
|
||||
});
|
||||
}
|
||||
|
||||
// Peupler le formulaire avec les données
|
||||
function populateFormData() {
|
||||
const clientStorage = JSON.parse(sessionStorage.getItem('tmp'));
|
||||
|
||||
if (client) {
|
||||
document.getElementById('nomClient').value = client.nom || '';
|
||||
document.getElementById('numClient').value = client.numClient || '';
|
||||
document.getElementById('adresseClient').value = client.adresse || '';
|
||||
document.getElementById('emailClient').value = client.mail || '';
|
||||
document.getElementById('codePostalClient').value = client.codePostal || '';
|
||||
document.getElementById('villeClient').value = client.ville || '';
|
||||
document.getElementById('noteFinanciereClient').value = client.noteFinanciere || '';
|
||||
}
|
||||
|
||||
if (clientStorage) {
|
||||
document.getElementById('nomClient').value = clientStorage.nomClient || '';
|
||||
document.getElementById('numClient').value = clientStorage.numClient || '';
|
||||
document.getElementById('adresseClient').value = clientStorage.adresseClient || '';
|
||||
document.getElementById('codePostalClient').value = clientStorage.postalClient || '';
|
||||
document.getElementById('villeClient').value = clientStorage.villeClient || '';
|
||||
}
|
||||
}
|
||||
|
||||
// Gérer l'ouverture de liens externes
|
||||
function handleOpenLink(event, urlType) {
|
||||
event.preventDefault();
|
||||
let url = '';
|
||||
|
||||
switch (urlType) {
|
||||
case 'creditsafeURL':
|
||||
url = 'https://www.creditsafe.fr/csfr?UserName=735265dorothee&Password=UH04EuLocXZMxIRqY19w6A%3d%3d&BackOfficeCountry=FR&origincountry=FR&linkages=Y';
|
||||
break;
|
||||
case 'easyQPURL':
|
||||
if (document.getElementById('numClient').value != "") {
|
||||
url = `https://qualite-portefeuille-iard-ent.axa-fr.intraxa/client/${document.getElementById('numClient').value}`;
|
||||
} else {
|
||||
url = `https://qualite-portefeuille-iard-ent.axa-fr.intraxa/`;
|
||||
}
|
||||
break;
|
||||
case 'refNoteFiURL':
|
||||
url = 'https://app.powerbi.com/groups/me/apps/7b48f9a2-bd97-4178-bc1a-a5f7ef6e985f/reports/d29a9f83-cafe-4a0f-bc38-0929921e8cd3/ReportSection?ctid=396b38cc-aa65-492b-bb0e-3d94ed25a97b';
|
||||
break;
|
||||
}
|
||||
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
|
||||
// Gérer l'extraction axaPAC
|
||||
async function handleExtractClient(event) {
|
||||
event.preventDefault();
|
||||
|
||||
// Affiche le modal
|
||||
const elem = document.getElementById('modalExtractAxAPAC');
|
||||
const instance = M.Modal.getInstance(elem);
|
||||
instance.open();
|
||||
}
|
||||
|
||||
async function handleModalExtract() {
|
||||
document.getElementById('modalExtraireClient').disabled = true;
|
||||
|
||||
const response = await fetch(`/client/extract`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
"matricule": matricule,
|
||||
"numClient": document.getElementById("modalNumClient").value
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
const res = await response.json();
|
||||
|
||||
if (res.valid) {
|
||||
document.getElementById("error-extract").style.display = "none";
|
||||
document.getElementById('modalExtraireClient').disabled = false;
|
||||
console.log(res);
|
||||
|
||||
// Save sessionStorage for Intermediaire
|
||||
sessionStorage.setItem('tmp', JSON.stringify(res.data));
|
||||
|
||||
// Populate data
|
||||
document.getElementById("numClient").value = (res.data.numClient) ? res.data.numClient : null;
|
||||
document.getElementById("nomClient").value = (res.data.nomClient) ? res.data.nomClient : null;
|
||||
document.getElementById("adresseClient").value = (res.data.adresseClient) ? res.data.adresseClient : null;
|
||||
document.getElementById("codePostalClient").value = (res.data.postalClient) ? res.data.postalClient : null;
|
||||
document.getElementById("villeClient").value = (res.data.villeClient) ? res.data.villeClient : null;
|
||||
|
||||
validateField('numClient', true);
|
||||
validateField('nomClient', true);
|
||||
validateField('adresseClient', true);
|
||||
validateField('codePostalClient', true);
|
||||
validateField('villeClient', true);
|
||||
updateSubmitButtonState('clientForm');
|
||||
|
||||
// close le modal
|
||||
const elem = document.getElementById('modalExtractAxAPAC');
|
||||
const instance = M.Modal.getInstance(elem);
|
||||
instance.close();
|
||||
} else {
|
||||
document.getElementById("error-extract").style.display = "block";
|
||||
document.getElementById('modalExtraireClient').disabled = false;
|
||||
console.log("Problème rencontré lors de l'extraction cl063 AxA PAC :", res.error);
|
||||
}
|
||||
}
|
||||
|
||||
// Gérer la soumission du formulaire
|
||||
async function handleSubmitForm(event) {
|
||||
event.preventDefault();
|
||||
|
||||
let idClient = client?.id;
|
||||
const numClient = document.getElementById('numClient').value;
|
||||
|
||||
let responseClient;
|
||||
|
||||
if (numClient) {
|
||||
responseClient = await fetch(`/client/read/${numClient}`, {
|
||||
method: 'GET',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
const dataClient = await responseClient.json();
|
||||
|
||||
if (dataClient.valid) {
|
||||
idClient = dataClient.idClient;
|
||||
} else {
|
||||
// Créez le client ici si non trouvé
|
||||
const responseCreateClient = await fetch(`/client/create`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
});
|
||||
const dataCreateClient = await responseCreateClient.json();
|
||||
|
||||
if (dataCreateClient.valid) {
|
||||
idClient = dataCreateClient.client.id;
|
||||
}
|
||||
}
|
||||
|
||||
// Mettre à jour le contrat avec l'ID du client
|
||||
if (idClient) {
|
||||
const response = await fetch(`/client/update/${idClient}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
"nom": document.getElementById('nomClient').value.toUpperCase(),
|
||||
"numClient": document.getElementById('numClient').value,
|
||||
"adresse": document.getElementById('adresseClient').value.toUpperCase(),
|
||||
"mail": document.getElementById('emailClient').value,
|
||||
"codePostal": document.getElementById('codePostalClient').value,
|
||||
"ville": document.getElementById('villeClient').value.toUpperCase(),
|
||||
"noteFinanciere": document.getElementById('noteFinanciereClient').value
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
if (data.valid) {
|
||||
const idContrat = contrat?.id;
|
||||
|
||||
fetch(`/contrat/update/client/${idContrat}/${idClient}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.valid) {
|
||||
// Update Session storage
|
||||
const clientStorage = JSON.parse(sessionStorage.getItem('tmp'));
|
||||
|
||||
if (clientStorage) {
|
||||
clientStorage.nomClient = document.getElementById('nomClient').value.toUpperCase();
|
||||
clientStorage.numClient = document.getElementById('numClient').value;
|
||||
clientStorage.adresseClient = document.getElementById('adresseClient').value.toUpperCase();
|
||||
clientStorage.postalClient = document.getElementById('codePostalClient').value;
|
||||
clientStorage.villeClient = document.getElementById('villeClient').value.toUpperCase();
|
||||
|
||||
sessionStorage.setItem('tmp', JSON.stringify(clientStorage))
|
||||
}
|
||||
|
||||
// Redirect vers intermédiaire
|
||||
window.location.href = `/navParcours?numParcours=${getNumParcoursFromURL()}&submenu=intermediaire`;
|
||||
} else {
|
||||
console.log('Echec lors de la mise à jour de la relation id contrat - id client :', data);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Exposer init globalement pour y accéder depuis l'extérieur
|
||||
window.initSubmenuForm = init;
|
||||
})();
|
||||
|
|
@ -1,422 +0,0 @@
|
|||
// Événement de vérification d'authentification
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Vérification de la page sur laquelle nous sommes
|
||||
if (window.location.pathname !== '/auth') {
|
||||
// Récupération du token JWT du localStorage
|
||||
const token = localStorage.getItem('jwtToken');
|
||||
|
||||
// Si pas de token, redirige vers la page d'authentification
|
||||
if (!token) {
|
||||
window.location.replace('/auth');
|
||||
} else {
|
||||
// Si un token existe, vérification de sa validité
|
||||
fetch('/auth/verifyToken', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + token
|
||||
}
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
|
||||
// Si le token n'est pas valide, redirige vers la page d'authentification
|
||||
if (!data.valid) {
|
||||
localStorage.removeItem('jwtToken');
|
||||
window.location.replace('/auth');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Événement du bouton déconnexion
|
||||
document.addEventListener('DOMContentLoaded', function (event) {
|
||||
// Empêche le comportement par défaut du formulaire
|
||||
event.preventDefault();
|
||||
|
||||
const logoutBtn = document.getElementById('logoutBtn');
|
||||
|
||||
if (window.location.pathname === '/auth') {
|
||||
logoutBtn.style.display = 'none';
|
||||
} else {
|
||||
logoutBtn.addEventListener('click', function () {
|
||||
// Suppression du token JWT du localStorage et redirection
|
||||
localStorage.removeItem('jwtToken');
|
||||
window.location.replace('/auth');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Événement d'initialisation des Materialize content
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Init dropdown
|
||||
var sidenav = document.querySelectorAll('.sidenav');
|
||||
M.Sidenav.init(sidenav);
|
||||
|
||||
// Init Modal
|
||||
var modals = document.querySelectorAll('.modal');
|
||||
M.Modal.init(modals);
|
||||
});
|
||||
|
||||
// Événement condition de groupe pour les menus
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const token = localStorage.getItem('jwtToken');
|
||||
|
||||
if (token) {
|
||||
const decoded = jwt_decode(token);
|
||||
const userAuthGroupe = decoded.userAuthGroupe;
|
||||
|
||||
const reportingItem = document.getElementById("reportingSidenavSelect");
|
||||
const adminItem = document.getElementById("adminSidenavSelect");
|
||||
|
||||
// Cache par défaut
|
||||
reportingItem.style.display = 'none';
|
||||
adminItem.style.display = 'none';
|
||||
|
||||
if (userAuthGroupe === 'MANAGER' || userAuthGroupe === 'ADMIN') {
|
||||
reportingItem.style.display = 'block';
|
||||
}
|
||||
|
||||
if (userAuthGroupe === 'ADMIN') {
|
||||
adminItem.style.display = 'block';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Fonction pour extraire le numéro de parcours de l'URL
|
||||
function getNumParcoursFromURL() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
|
||||
return urlParams.get('numParcours');
|
||||
}
|
||||
|
||||
|
||||
// Fonction pour extraire le sous-menu de l'URL
|
||||
function getSubmenuFromURL() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
|
||||
return urlParams.get('submenu');
|
||||
}
|
||||
|
||||
// Fonction pour vérifier si une valeur est nulle ou non définie
|
||||
function isNullOrUndefined(value) {
|
||||
return value === null || value === undefined;
|
||||
}
|
||||
|
||||
// Fonction pour éviter la duplication du code fetch
|
||||
async function fetchWithJson(url, method, body = null) {
|
||||
const options = {
|
||||
method: method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
};
|
||||
if (body) options.body = JSON.stringify(body);
|
||||
|
||||
const response = await fetch(url, options);
|
||||
if (!response.ok) throw new Error('Réseau ou erreur serveur');
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
// Fonction de load du parcours en session storage
|
||||
async function loadParcours(numParcours) {
|
||||
try {
|
||||
const data = await fetchWithJson(`/parcours/read/${numParcours}`, 'GET');
|
||||
|
||||
if (data.valid) {
|
||||
sessionStorage.setItem('parcours', JSON.stringify(data.parcours));
|
||||
} else {
|
||||
sessionStorage.setItem('parcours', null);
|
||||
sessionStorage.setItem('admins', JSON.stringify(data.admins));
|
||||
console.log("Parcours introuvable");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Erreur lors de la récupération des informations parcours :", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Fonction générique de toggle
|
||||
function toggler(btn, div) {
|
||||
if (document.getElementById(btn).checked) {
|
||||
document.getElementById(div).style.display = 'block';
|
||||
} else {
|
||||
document.getElementById(div).style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Fonction de load du parcours en session storage
|
||||
async function loadContrat(idContrat) {
|
||||
try {
|
||||
const data = await fetchWithJson(`/contrat/read/id/${idContrat}`, 'GET');
|
||||
|
||||
if (data.valid) {
|
||||
sessionStorage.setItem('contrat', JSON.stringify(data.contrat));
|
||||
} else {
|
||||
sessionStorage.setItem('contrat', null);
|
||||
console.log("Contrat introuvable");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Erreur lors de la récupération des informations contrat :", error);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Fonctions utilitaires génériques ==========
|
||||
|
||||
/**
|
||||
* Formatage des dates (ISO vers format français)
|
||||
* @param {string} iso - Date au format ISO
|
||||
* @param {boolean} withTime - Inclure l'heure (défaut: true)
|
||||
* @returns {string} Date formatée (dd/mm/yyyy ou dd/mm/yyyy hh:mm)
|
||||
*/
|
||||
function fmtDate(iso, withTime = true) {
|
||||
// Si la valeur est null, undefined, ou vide
|
||||
if (!iso || (typeof iso === 'string' && iso.trim() === "")) return "NC";
|
||||
|
||||
// Convertir en string si ce n'est pas déjà le cas
|
||||
const dateStr = String(iso).trim();
|
||||
|
||||
// Vérifier les valeurs invalides connues
|
||||
if (dateStr === "00/00/0000" || dateStr === "00/00" || dateStr === "null" || dateStr === "undefined") {
|
||||
return "NC";
|
||||
}
|
||||
|
||||
let d;
|
||||
|
||||
// Si c'est déjà au format jj/mm/aaaa (format français)
|
||||
if (dateStr.includes("/") && dateStr.split("/").length === 3) {
|
||||
const parts = dateStr.split("/");
|
||||
const day = parseInt(parts[0], 10);
|
||||
const month = parseInt(parts[1], 10);
|
||||
const year = parseInt(parts[2], 10);
|
||||
|
||||
// Vérifier si les valeurs sont valides
|
||||
if (isNaN(day) || isNaN(month) || isNaN(year)) {
|
||||
return "NC";
|
||||
}
|
||||
|
||||
// Si le jour ou le mois est 00, considérer comme invalide
|
||||
if (day === 0 || month === 0) {
|
||||
return "NC";
|
||||
}
|
||||
|
||||
// Si le mois est invalide
|
||||
if (month < 1 || month > 12) {
|
||||
return "NC";
|
||||
}
|
||||
|
||||
// Si l'année est 0000, afficher juste jj/mm (sans l'année)
|
||||
if (year === 0) {
|
||||
return `${String(day).padStart(2, "0")}/${String(month).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
// Si l'année est valide, créer une vraie date et valider
|
||||
const monthIndex = month - 1; // Les mois commencent à 0
|
||||
d = new Date(year, monthIndex, day);
|
||||
// Vérifier si la date est valide (ex: 31/02/2000 serait invalide)
|
||||
if (d.getDate() !== day || d.getMonth() !== monthIndex || d.getFullYear() !== year) {
|
||||
return "NC";
|
||||
}
|
||||
|
||||
// Formater la date
|
||||
const dd = String(d.getDate()).padStart(2, "0");
|
||||
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
||||
const yyyy = d.getFullYear();
|
||||
if (!withTime) return `${dd}/${mm}/${yyyy}`;
|
||||
const hh = String(d.getHours()).padStart(2, "0");
|
||||
const mi = String(d.getMinutes()).padStart(2, "0");
|
||||
return `${dd}/${mm}/${yyyy} ${hh}:${mi}`;
|
||||
}
|
||||
// Si c'est au format jj/mm (pour date d'échéance)
|
||||
else if (dateStr.includes("/") && dateStr.split("/").length === 2) {
|
||||
const parts = dateStr.split("/");
|
||||
const day = parseInt(parts[0], 10);
|
||||
const month = parseInt(parts[1], 10);
|
||||
|
||||
// Pour l'échéance, on retourne juste jj/mm
|
||||
if (isNaN(day) || isNaN(month) || day === 0 || month === 0 || month > 12) {
|
||||
return "NC";
|
||||
}
|
||||
return `${String(day).padStart(2, "0")}/${String(month).padStart(2, "0")}`;
|
||||
}
|
||||
// Sinon, essayer de parser comme date ISO
|
||||
else {
|
||||
d = new Date(dateStr);
|
||||
if (isNaN(d.getTime())) return "NC";
|
||||
|
||||
const dd = String(d.getDate()).padStart(2, "0");
|
||||
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
||||
const yyyy = d.getFullYear();
|
||||
if (!withTime) return `${dd}/${mm}/${yyyy}`;
|
||||
const hh = String(d.getHours()).padStart(2, "0");
|
||||
const mi = String(d.getMinutes()).padStart(2, "0");
|
||||
return `${dd}/${mm}/${yyyy} ${hh}:${mi}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un élément key-value pour l'affichage de détails
|
||||
* @param {string} label - Libellé
|
||||
* @param {*} value - Valeur (booléen converti en Oui/Non)
|
||||
* @returns {string} HTML formaté
|
||||
*/
|
||||
function kv(label, value) {
|
||||
const v = (value === true) ? "Oui" : (value === false) ? "Non" : (value ?? "NC");
|
||||
return `<div style="display:flex; gap:8px; margin:2px 0;">
|
||||
<div style="min-width:220px;"><strong>${label}</strong> :</div>
|
||||
<div>${v}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée une grille à 2 colonnes pour l'affichage de détails
|
||||
* @param {string} innerLeft - Contenu colonne gauche
|
||||
* @param {string} innerRight - Contenu colonne droite
|
||||
* @returns {string} HTML formaté
|
||||
*/
|
||||
function gridWrap2cols(innerLeft, innerRight) {
|
||||
return `<div style="display:grid;grid-template-columns:repeat(2,minmax(280px,1fr));gap:14px;">${innerLeft}${innerRight}</div>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fonction debounce pour limiter la fréquence d'exécution d'une fonction
|
||||
* Utile pour les recherches en temps réel et éviter les appels API excessifs
|
||||
* @param {Function} fn - Fonction à débouncer
|
||||
* @param {number} delay - Délai en millisecondes (défaut: 300ms)
|
||||
* @returns {Function} Fonction débouncée avec méthode cancel()
|
||||
*/
|
||||
function debounce(fn, delay = 300) {
|
||||
let t;
|
||||
function wrapped(...args) {
|
||||
clearTimeout(t);
|
||||
t = setTimeout(() => fn(...args), delay);
|
||||
}
|
||||
wrapped.cancel = () => clearTimeout(t);
|
||||
return wrapped;
|
||||
}
|
||||
|
||||
/**
|
||||
* Décode un token JWT et retourne le payload
|
||||
* @param {string} token - Token JWT à décoder
|
||||
* @returns {Object|null} Payload décodé ou null en cas d'erreur
|
||||
*/
|
||||
function parseJwt(token) {
|
||||
try {
|
||||
const base64Url = token.split(".")[1];
|
||||
const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
|
||||
const jsonPayload = decodeURIComponent(
|
||||
atob(base64)
|
||||
.split("")
|
||||
.map(function (c) {
|
||||
return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2);
|
||||
})
|
||||
.join("")
|
||||
);
|
||||
return JSON.parse(jsonPayload);
|
||||
} catch (error) {
|
||||
console.error("Erreur lors du décodage du token:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Affiche un message d'erreur dans l'élément avec l'ID "error"
|
||||
* @param {string} message - Message d'erreur à afficher
|
||||
*/
|
||||
function displayError(message) {
|
||||
const errorElement = document.getElementById("error");
|
||||
if (errorElement) {
|
||||
errorElement.textContent = message;
|
||||
errorElement.style.display = "block";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée un message formaté avec différents types (info, warn, error, dev)
|
||||
* @param {string} type - Type de message : 'info', 'warn', 'error', 'dev'
|
||||
* @param {string} title - Titre du message
|
||||
* @param {string} description - Description du message (peut contenir du HTML)
|
||||
* @returns {string} HTML du message formaté
|
||||
*/
|
||||
function createMessageBox(type, title, description) {
|
||||
// Protection contre les paramètres invalides
|
||||
if (!type || typeof type !== 'string') type = 'info';
|
||||
if (!title || typeof title !== 'string') title = 'Message';
|
||||
if (!description || typeof description !== 'string') description = '';
|
||||
|
||||
const configs = {
|
||||
info: {
|
||||
icon: 'fa-info-circle',
|
||||
bgColor: '#e3f2fd',
|
||||
borderColor: '#2196f3',
|
||||
textColor: '#1565c0'
|
||||
},
|
||||
warn: {
|
||||
icon: 'fa-exclamation-triangle',
|
||||
bgColor: '#fff3e0',
|
||||
borderColor: '#ff9800',
|
||||
textColor: '#e65100'
|
||||
},
|
||||
error: {
|
||||
icon: 'fa-times-circle',
|
||||
bgColor: '#ffebee',
|
||||
borderColor: '#f44336',
|
||||
textColor: '#c62828'
|
||||
},
|
||||
dev: {
|
||||
icon: 'fa-tools',
|
||||
bgColor: '#fff3cd',
|
||||
borderColor: '#ffc107',
|
||||
textColor: '#856404'
|
||||
}
|
||||
};
|
||||
|
||||
const config = configs[type] || configs.info;
|
||||
|
||||
// Échappement basique pour éviter les problèmes (description peut contenir du HTML valide)
|
||||
const safeTitle = String(title).replace(/</g, '<').replace(/>/g, '>');
|
||||
// Description peut contenir du HTML (comme <br>), donc on ne l'échappe pas complètement
|
||||
// mais on s'assure qu'elle est une string
|
||||
const safeDescription = String(description);
|
||||
|
||||
return `
|
||||
<div style="padding: 20px; text-align: center; background-color: ${config.bgColor}; border: 1px solid ${config.borderColor}; border-radius: 4px; margin: 10px 0; width: 100%; box-sizing: border-box;">
|
||||
<p style="color: ${config.textColor}; font-weight: 600; margin: 0 0 10px 0;">
|
||||
<i class="fas ${config.icon}" style="margin-right: 8px;"></i>
|
||||
${safeTitle}
|
||||
</p>
|
||||
<p style="color: ${config.textColor}; margin: 0; font-size: 0.9rem;">
|
||||
${safeDescription}
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formate une valeur en euros :
|
||||
* - espace insécable fine entre milliers,
|
||||
* - virgule pour les décimales,
|
||||
* - 0 à 2 décimales max,
|
||||
* - symbole € à la fin.
|
||||
* Si la valeur est invalide => "NC".
|
||||
*/
|
||||
function formatEuro(value, options) {
|
||||
const opts = Object.assign({ minimumFractionDigits: 0, maximumFractionDigits: 2 }, options || {});
|
||||
if (value === null || value === undefined || value === '' || value === 'NC') return 'NC';
|
||||
|
||||
// Accepte string avec virgule ou espaces
|
||||
const normalized = (typeof value === 'string')
|
||||
? value.replace(/\s|\u00A0|\u202F| /g, '').replace(',', '.')
|
||||
: value;
|
||||
|
||||
const num = Number(normalized);
|
||||
if (!isFinite(num)) return 'NC';
|
||||
|
||||
const formatted = new Intl.NumberFormat('fr-FR', {
|
||||
minimumFractionDigits: opts.minimumFractionDigits,
|
||||
maximumFractionDigits: opts.maximumFractionDigits
|
||||
}).format(num);
|
||||
|
||||
return formatted + ' €';
|
||||
}
|
||||
|
|
@ -1,858 +0,0 @@
|
|||
// public/js/historiqueParcours.js
|
||||
document.addEventListener("DOMContentLoaded", async function () {
|
||||
// Récupération du token
|
||||
const token = localStorage.getItem("jwtToken");
|
||||
|
||||
if (!token) {
|
||||
throw new Error("Aucun token trouvé dans le localStorage.");
|
||||
}
|
||||
|
||||
const userData = parseJwt(token);
|
||||
|
||||
if (!userData) {
|
||||
displayError("Erreur lors de l'extraction des données utilisateur à partir du token.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialiser DataTables en mode server-side (obligé pour pagination)
|
||||
const table = initServerSideDataTable();
|
||||
|
||||
// Variable pour suivre l'état des exports
|
||||
let isExporting = false;
|
||||
|
||||
// Fonction pour désactiver/activer les boutons d'export
|
||||
function setExportButtonsState(disabled) {
|
||||
isExporting = disabled;
|
||||
const buttons = ["#exportCSV", "#exportCSVFilter", "#exportXlxs", "#exportXlxsFilter"];
|
||||
buttons.forEach(selector => {
|
||||
const $btn = $(selector);
|
||||
$btn.prop("disabled", disabled);
|
||||
if (disabled) {
|
||||
$btn.css("opacity", "0.5");
|
||||
$btn.css("cursor", "not-allowed");
|
||||
} else {
|
||||
$btn.css("opacity", "1");
|
||||
$btn.css("cursor", "pointer");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Exports CSV/XLSX
|
||||
$("#exportCSV").on("click", function () {
|
||||
if (isExporting) return;
|
||||
|
||||
const dt = $("#historiqueParcours").DataTable();
|
||||
if (!dt) {
|
||||
displayError("Impossible d'accéder à la table de données.");
|
||||
return;
|
||||
}
|
||||
|
||||
const settings = dt.settings()[0];
|
||||
if (!settings || !settings.aoColumns) {
|
||||
displayError("Structure de données invalide.");
|
||||
return;
|
||||
}
|
||||
|
||||
setExportButtonsState(true);
|
||||
|
||||
const payload = {
|
||||
mode: "full", // export total
|
||||
search: { value: "" },
|
||||
columns: settings.aoColumns.map((c, i) => ({
|
||||
data: i,
|
||||
search: { value: "" }
|
||||
})),
|
||||
// on garde l'ordre actuel pour la récuperation
|
||||
order: (dt.order() || []).map(([col, dir]) => ({ column: col || 0, dir: dir || "asc" }))
|
||||
};
|
||||
|
||||
fetch("/historiqueParcours/export/csv", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
.then(resp => {
|
||||
if (!resp.ok) throw new Error("Export CSV (complet) impossible");
|
||||
|
||||
return resp.blob();
|
||||
})
|
||||
.then(blob => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = "historique_parcours_complet.csv";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
a.remove();
|
||||
setExportButtonsState(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
displayError("Export CSV (complet) impossible");
|
||||
setExportButtonsState(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
$("#exportCSVFilter").on("click", function () {
|
||||
if (isExporting) return;
|
||||
|
||||
const dt = $("#historiqueParcours").DataTable();
|
||||
if (!dt) {
|
||||
displayError("Impossible d'accéder à la table de données.");
|
||||
return;
|
||||
}
|
||||
|
||||
const settings = dt.settings()[0];
|
||||
if (!settings || !settings.aoColumns) {
|
||||
displayError("Structure de données invalide.");
|
||||
return;
|
||||
}
|
||||
|
||||
setExportButtonsState(true);
|
||||
|
||||
const payload = {
|
||||
mode: "filtered", // export avec les filtres/colonnes/tri actuels
|
||||
search: { value: dt.search() || "" }, // recherche globale
|
||||
columns: settings.aoColumns.map((c, i) => ({
|
||||
data: i,
|
||||
search: { value: dt.column(i).search() || "" } // filtres par colonne
|
||||
})),
|
||||
order: (dt.order() || []).map(([col, dir]) => ({ column: col || 0, dir: dir || "asc" }))
|
||||
};
|
||||
|
||||
fetch("/historiqueParcours/export/csv", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
.then(resp => {
|
||||
if (!resp.ok) throw new Error("Export CSV (filtré) impossible");
|
||||
return resp.blob();
|
||||
})
|
||||
.then(blob => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = "historique_parcours_filtre.csv";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
a.remove();
|
||||
setExportButtonsState(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
displayError("Export CSV (filtré) impossible");
|
||||
setExportButtonsState(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
$("#exportXlxs").on("click", function () {
|
||||
if (isExporting) return;
|
||||
|
||||
const dt = $("#historiqueParcours").DataTable();
|
||||
if (!dt) {
|
||||
displayError("Impossible d'accéder à la table de données.");
|
||||
return;
|
||||
}
|
||||
|
||||
const settings = dt.settings()[0];
|
||||
if (!settings || !settings.aoColumns) {
|
||||
displayError("Structure de données invalide.");
|
||||
return;
|
||||
}
|
||||
|
||||
setExportButtonsState(true);
|
||||
|
||||
const payload = {
|
||||
mode: "full",
|
||||
search: { value: "" },
|
||||
columns: settings.aoColumns.map((c, i) => ({ data: i, search: { value: "" } })),
|
||||
order: (dt.order() || []).map(([col, dir]) => ({ column: col || 0, dir: dir || "asc" }))
|
||||
};
|
||||
|
||||
fetch("/historiqueParcours/export/xls", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
.then(resp => { if (!resp.ok) throw new Error(); return resp.blob(); })
|
||||
.then(blob => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url; a.download = "historique_parcours_complet.xls";
|
||||
document.body.appendChild(a); a.click();
|
||||
URL.revokeObjectURL(url); a.remove();
|
||||
setExportButtonsState(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
displayError("Export XLS (complet) impossible");
|
||||
setExportButtonsState(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
$("#exportXlxsFilter").on("click", function () {
|
||||
if (isExporting) return;
|
||||
|
||||
const dt = $("#historiqueParcours").DataTable();
|
||||
if (!dt) {
|
||||
displayError("Impossible d'accéder à la table de données.");
|
||||
return;
|
||||
}
|
||||
|
||||
const settings = dt.settings()[0];
|
||||
if (!settings || !settings.aoColumns) {
|
||||
displayError("Structure de données invalide.");
|
||||
return;
|
||||
}
|
||||
|
||||
setExportButtonsState(true);
|
||||
|
||||
const payload = {
|
||||
mode: "filtered", // export avec les filtres/colonnes/tri actuels
|
||||
search: { value: dt.search() || "" }, // recherche globale
|
||||
columns: settings.aoColumns.map((c, i) => ({
|
||||
data: i,
|
||||
search: { value: dt.column(i).search() || "" } // filtres par colonne
|
||||
})),
|
||||
order: (dt.order() || []).map(([col, dir]) => ({ column: col || 0, dir: dir || "asc" }))
|
||||
};
|
||||
|
||||
fetch("/historiqueParcours/export/xls", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
.then(resp => {
|
||||
if (!resp.ok) throw new Error("Export XLS (filtré) impossible");
|
||||
return resp.blob();
|
||||
})
|
||||
.then(blob => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = "historique_parcours_filtre.xls";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
a.remove();
|
||||
setExportButtonsState(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
displayError("Export XLS (filtré) impossible");
|
||||
setExportButtonsState(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// Délégation pour la génération de projet
|
||||
$("#historiqueParcours").on("click", "button#btnGenerate", function () {
|
||||
const numParcours = $(this).data("num-parcours");
|
||||
const produit = $(this).data("produit");
|
||||
generateProject(numParcours, produit);
|
||||
});
|
||||
});
|
||||
|
||||
/* =========================
|
||||
* Helpers spécifiques server-side
|
||||
* ========================= */
|
||||
|
||||
// Initialisation DataTables en server-side (recherche globale + par colonnes + tri + pagination)
|
||||
function initServerSideDataTable() {
|
||||
let inflightController = null;
|
||||
|
||||
const table = $("#historiqueParcours").DataTable({
|
||||
processing: true,
|
||||
serverSide: true,
|
||||
searching: true,
|
||||
paging: true,
|
||||
orderCellsTop: true,
|
||||
fixedHeader: true,
|
||||
responsive: false,
|
||||
autoWidth: false,
|
||||
scrollX: false,
|
||||
pageLength: 10,
|
||||
retrieve: true,
|
||||
order: [[0, "desc"]],
|
||||
searchDelay: 350,
|
||||
language: {
|
||||
search: "Rechercher",
|
||||
lengthMenu: "Afficher _MENU_ entrées par page",
|
||||
info: "Affichage de _START_ à _END_ sur _TOTAL_ entrées",
|
||||
infoEmpty: "Affichage de 0 à 0 sur 0 entrée",
|
||||
infoFiltered: "(filtré de _MAX_ entrées au total)",
|
||||
paginate: { first: "Début", previous: "Précédent", next: "Suivant", last: "Fin" },
|
||||
},
|
||||
|
||||
ajax: function (data, callback) {
|
||||
const body = {
|
||||
draw: data.draw || 1,
|
||||
start: data.start || 0,
|
||||
length: data.length || 10,
|
||||
order: data.order || [],
|
||||
columns: data.columns || [],
|
||||
search: data.search || { value: "" },
|
||||
};
|
||||
|
||||
if (inflightController) inflightController.abort(); // action en cours
|
||||
inflightController = new AbortController();
|
||||
const currentController = inflightController;
|
||||
|
||||
fetch("/historiqueParcours/datatable", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
signal: currentController.signal,
|
||||
})
|
||||
.then(res => {
|
||||
// Vérifier si la requête a été annulée
|
||||
if (currentController.signal.aborted || inflightController !== currentController) {
|
||||
return null;
|
||||
}
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
return res.json();
|
||||
})
|
||||
.then(payload => {
|
||||
// Vérifier si la requête a été annulée
|
||||
if (currentController.signal.aborted || inflightController !== currentController) {
|
||||
return;
|
||||
}
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
throw new Error("Réponse invalide du serveur");
|
||||
}
|
||||
callback({
|
||||
draw: payload.draw || 0,
|
||||
recordsTotal: payload.recordsTotal || 0,
|
||||
recordsFiltered: payload.recordsFiltered || payload.recordsTotal || 0,
|
||||
data: Array.isArray(payload.data) ? payload.data : []
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
// Ignorer silencieusement toutes les erreurs d'abort
|
||||
if (err && (err.name === "AbortError" || err.name === "DOMException")) {
|
||||
return;
|
||||
}
|
||||
// Vérifier aussi si le signal a été aborted
|
||||
if (currentController.signal.aborted || inflightController !== currentController) {
|
||||
return;
|
||||
}
|
||||
// Seulement afficher une erreur si ce n'est PAS un abort
|
||||
displayError("Failed to fetch data. Please try again later.");
|
||||
callback({ draw: 0, recordsTotal: 0, recordsFiltered: 0, data: [] });
|
||||
});
|
||||
},
|
||||
|
||||
initComplete: function () {
|
||||
const api = this.api();
|
||||
|
||||
// Recherche globale : debounce y compris ENTER (plus de bypass immédiat)
|
||||
const $globalInput = $('div.dataTables_filter input[type="search"]');
|
||||
const $filterLabel = $globalInput.closest('label');
|
||||
$globalInput.off('.DT'); // nettoie handlers datatables
|
||||
const debouncedGlobal = debounce((v) => {
|
||||
api.search(v);
|
||||
api.ajax.reload();
|
||||
}, 350);
|
||||
|
||||
// Fonction pour gérer l'affichage du texte "Rechercher"
|
||||
function toggleSearchPlaceholder() {
|
||||
if ($globalInput.val().trim() !== '') {
|
||||
$filterLabel.addClass('has-value');
|
||||
} else {
|
||||
$filterLabel.removeClass('has-value');
|
||||
}
|
||||
}
|
||||
|
||||
$globalInput.on('input keyup keydown', function () {
|
||||
debouncedGlobal(this.value);
|
||||
toggleSearchPlaceholder();
|
||||
});
|
||||
|
||||
// Vérifier l'état initial
|
||||
toggleSearchPlaceholder();
|
||||
|
||||
// Recherche par colonne avec DEBOUNCE (ENTER inclus)
|
||||
const debouncedColSearch = debounce((i, val) => {
|
||||
api.column(i).search(val);
|
||||
api.ajax.reload();
|
||||
}, 350);
|
||||
|
||||
$("#historiqueParcours thead tr:eq(1) th").each(function (i) {
|
||||
$("input", this).on("input keyup keydown change", function () {
|
||||
debouncedColSearch(i, this.value);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
$("#toggleSearch").on("click", function () {
|
||||
const row = $("#historiqueParcours thead tr:eq(1)");
|
||||
row.toggle();
|
||||
|
||||
if (row.is(":visible")) {
|
||||
$(this).text("ENLEVER LA RECHERCHE PAR COLONNE");
|
||||
} else {
|
||||
$(this).text("ACTIVER LA RECHERCHE PAR COLONNE");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Cacher la 2e ligne au départ
|
||||
$("#historiqueParcours thead tr:eq(1)").hide();
|
||||
},
|
||||
|
||||
// --- Ajout bouton détails en 1re colonne
|
||||
columnDefs: [
|
||||
{
|
||||
targets: 0,
|
||||
render: function (data, type, row) {
|
||||
const np = String(data ?? "");
|
||||
const numPacours = np.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">");
|
||||
|
||||
|
||||
return `
|
||||
<span class="col-with-text">
|
||||
<button class="btn-row-details" title="Détails" aria-label="Afficher les détails">?</button>
|
||||
<span class="np">${numPacours}</span>
|
||||
</span>
|
||||
`;
|
||||
|
||||
|
||||
}
|
||||
},
|
||||
{ type: "date-eu", targets: 1 }, // Date de Création (colonne 1)
|
||||
{
|
||||
// Appliquer la classe nc-value aux cellules contenant "NC"
|
||||
targets: "_all",
|
||||
createdCell: function (td, cellData, rowData, row, col) {
|
||||
// Exclure la première colonne (bouton détails) et les deux dernières (boutons)
|
||||
if (col !== 0 && col < rowData.length - 2) {
|
||||
const cellText = String(cellData || "").trim();
|
||||
if (cellText === "NC") {
|
||||
td.classList.add("nc-value");
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{ responsivePriority: 1, targets: -1 },
|
||||
{ responsivePriority: 2, targets: -2 }
|
||||
],
|
||||
});
|
||||
|
||||
// --- Toggle des détails
|
||||
$('#historiqueParcours tbody').on('click', '.btn-row-details', function (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const api = $('#historiqueParcours').DataTable();
|
||||
const $tr = $(this).closest('tr');
|
||||
|
||||
// si c'est une ligne enfant (responsive), remonter à la parent
|
||||
const row = api.row($tr.hasClass('child') ? $tr.prev() : $tr);
|
||||
|
||||
if (row.child.isShown()) {
|
||||
row.child.hide();
|
||||
// change icône
|
||||
this.textContent = "?";
|
||||
this.style.background = "#e74c3c";
|
||||
return;
|
||||
}
|
||||
|
||||
// récupérer le numParcours depuis la 1re cellule
|
||||
const raw = row.data()?.[0] ?? "";
|
||||
const tmp = document.createElement('div');
|
||||
tmp.innerHTML = String(raw);
|
||||
const numParcours = tmp.textContent.replace("?", "").trim();
|
||||
if (!numParcours) return;
|
||||
|
||||
row.child('<div style="padding:12px 16px; font-style:italic;">Chargement des détails…</div>').show();
|
||||
this.textContent = "?";
|
||||
this.style.background = "#c0392b";
|
||||
|
||||
fetch(`/historiqueParcours/details/${encodeURIComponent(numParcours)}`)
|
||||
.then(r => {
|
||||
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
||||
return r.json();
|
||||
})
|
||||
.then(payload => {
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
throw new Error("Réponse invalide");
|
||||
}
|
||||
if (!payload.valid) {
|
||||
row.child(createMessageBox('error', 'Impossible de charger les détails',
|
||||
'Une erreur est survenue lors du chargement des détails du parcours.'));
|
||||
this.textContent = "?";
|
||||
this.style.background = "#e74c3c";
|
||||
return;
|
||||
}
|
||||
row.child(formatDetailsPanel(payload));
|
||||
})
|
||||
.catch(() => {
|
||||
row.child(createMessageBox('error', 'Erreur de chargement',
|
||||
'Une erreur réseau est survenue lors du chargement des détails.'));
|
||||
this.textContent = "?";
|
||||
this.style.background = "#e74c3c";
|
||||
});
|
||||
});
|
||||
|
||||
return table;
|
||||
}
|
||||
|
||||
/* =========================
|
||||
* Fonctions annexes importantes
|
||||
* ========================= */
|
||||
|
||||
async function fetchUserDetails(matriculeUser) {
|
||||
try {
|
||||
const response = await fetch(`/user/read/matricule/${matriculeUser}`);
|
||||
const data = await response.json();
|
||||
|
||||
return data.valid ? data : null;
|
||||
} catch (error) {
|
||||
displayError(`Erreur lors de la récupération du contrat avec le matricule ${matriculeUser} :`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function generateProject(numParcours, produit) {
|
||||
try {
|
||||
if (!numParcours || !produit) {
|
||||
displayError("Paramètres manquants pour la génération du projet.");
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch(`/generate/${produit}/projet/${encodeURIComponent(numParcours)}`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error("Erreur réseau ou serveur");
|
||||
|
||||
const disposition = response.headers.get("content-disposition");
|
||||
let filename = "projet.docx";
|
||||
if (disposition) {
|
||||
const parts = disposition.split(";");
|
||||
if (parts.length > 1) {
|
||||
const filenamePart = parts[1].trim();
|
||||
if (filenamePart.startsWith("filename=")) {
|
||||
filename = filenamePart.split("=")[1]?.replace(/"/g, "") || filename;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
if (!blob || blob.size === 0) {
|
||||
throw new Error("Fichier vide reçu");
|
||||
}
|
||||
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
a.remove();
|
||||
} catch (error) {
|
||||
displayError("Erreur lors de la génération du projet : " + (error?.message || "Erreur inconnue"));
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Helpers rendu ----------
|
||||
// Les fonctions kv, fmtDate, gridWrap2cols, debounce, parseJwt et displayError sont dans global.js
|
||||
|
||||
//formatDetailsPanel : rend l'HTML pour la récuperation des données par parcours
|
||||
function formatDetailsPanel(payload) {
|
||||
const prodKey = (payload.produit || "").toUpperCase();
|
||||
const prod = payload.produitRecord || null;
|
||||
const contrat = payload.contrat || null;
|
||||
|
||||
let body = "";
|
||||
if (prodKey === "TPPC") body = sectionTPPC(prod, contrat);
|
||||
else if (prodKey === "RC") body = sectionRC(prod, contrat);
|
||||
else if (prodKey === "FAC") body = sectionFAC(prod, contrat);
|
||||
else body = createMessageBox('warn', 'Produit non renseigné', 'Le type de produit n\'a pas été spécifié pour ce parcours.');
|
||||
|
||||
return `
|
||||
<div class="parcours-details" style="padding:14px 16px; background:#fafafa; border-top:1px solid #e0e0e0;">
|
||||
<div style="font-weight:600; margin:0 0 8px;">Détails ${prodKey || ""}</div>
|
||||
${body}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
// Retourne le tarif en fonction du type de tarif
|
||||
function buildTarifBlock({ tarifRef, ht, ttc }) {
|
||||
if (tarifRef && String(tarifRef).trim() !== '') {
|
||||
return kv("Tarif de référence", tarifRef);
|
||||
}
|
||||
const htStr = (ht !== undefined && ht !== null) ? formatEuro(ht) : "NC";
|
||||
const ttcStr = (ttc !== undefined && ttc !== null) ? formatEuro(ttc) : "NC";
|
||||
return kv("Tarif commercial HT / TTC", `${htStr} / ${ttcStr}`);
|
||||
}
|
||||
|
||||
function safeCA(value) {
|
||||
return (value === null || value === undefined || value === '' || value === 'NC')
|
||||
? "NC"
|
||||
: formatEuro(value);
|
||||
}
|
||||
|
||||
|
||||
// ---------- Sections produit ----------
|
||||
|
||||
//sectionTTPC : récupere les détails d'un parcours TPPC pour le mettre en forme (2 colonnes)
|
||||
// ---------- TPPC ----------
|
||||
function sectionTPPC(produit, contrat) {
|
||||
if (!produit) {
|
||||
if (contrat) {
|
||||
return createMessageBox(
|
||||
'info',
|
||||
'Informations non disponibles',
|
||||
'Les informations sur ce Parcours TPPC ne sont pas encore disponibles.<br>La fiche TPPC n\'a pas encore été créée pour ce parcours.'
|
||||
);
|
||||
}
|
||||
return createMessageBox(
|
||||
'error',
|
||||
'Fiche TPPC introuvable',
|
||||
'Impossible de récupérer les informations de la fiche TPPC pour ce parcours.'
|
||||
);
|
||||
}
|
||||
|
||||
const tarif = produit?.["@expand"]?.tarif || null; // tppctarif
|
||||
const projet = produit?.["@expand"]?.projet || null; // tppcprojet
|
||||
|
||||
// 1) CA
|
||||
const caStr = safeCA(produit.ca);
|
||||
|
||||
// 2) Type de cotisation
|
||||
let typeCotStr = "NC";
|
||||
if (projet?.typeCot) {
|
||||
typeCotStr = (projet.typeCot === "revisable") ? "Révisable"
|
||||
: (projet.typeCot === "forfaitaire") ? "Forfaitaire"
|
||||
: projet.typeCot;
|
||||
}
|
||||
|
||||
// 3) Activité
|
||||
const activiteAssuree = produit.actAssuree || "NC";
|
||||
const nbVehicules = (produit.nbVehic !== undefined && produit.nbVehic !== null) ? String(produit.nbVehic) : "NC";
|
||||
|
||||
// 4) Marchandises / Garanties
|
||||
const garanties = Array.isArray(produit.garanties)
|
||||
? produit.garanties.join(", ")
|
||||
: (produit.garanties || "NC");
|
||||
|
||||
const extensions = [];
|
||||
if (produit.marCiternes) extensions.push("Citernes");
|
||||
if (produit.marDenreesSousTemp) extensions.push("Denrées sous température");
|
||||
if (produit.marAnimaux) extensions.push("Animaux vivants");
|
||||
if (produit.marFranchise) extensions.push("Franchise");
|
||||
const extensionsStr = extensions.length > 0 ? extensions.join(", ") : "Aucune";
|
||||
|
||||
// 5) Zones (supprimer pour TPPC)
|
||||
const zonesStr = "NC";
|
||||
|
||||
// 6) Dates (projet)
|
||||
const dateEffet = projet?.dateEffet ? fmtDate(projet.dateEffet, false) : "NC";
|
||||
const dateEcheance = projet?.dateEcheance ? fmtDate(projet.dateEcheance, false) : "NC";
|
||||
const dateFin = projet?.dateFin ? fmtDate(projet.dateFin, false) : "NC";
|
||||
const datesStr = `Effet: ${dateEffet} / Échéance: ${dateEcheance} / Fin: ${dateFin}`;
|
||||
|
||||
// 7) Tarif (ref ou com HT/TTC)
|
||||
const blocTarif = buildTarifBlock({
|
||||
tarifRef: tarif?.tarifRef,
|
||||
ht: (produit.primeHT ?? produit.cotTotalHT ?? null),
|
||||
ttc: (produit.primeTTC ?? produit.cotTotalTTC ?? null)
|
||||
});
|
||||
|
||||
// Disposition en 2 colonnes
|
||||
const gauche = [
|
||||
kv("Chiffre d'affaires", caStr),
|
||||
kv("Type de cotisation", typeCotStr),
|
||||
kv("Activité assurée", activiteAssuree),
|
||||
kv("Nombre de véhicules", nbVehicules),
|
||||
kv("Garanties", garanties),
|
||||
kv("Extensions de garanties", extensionsStr),
|
||||
].join("");
|
||||
|
||||
const droite = [
|
||||
kv("Zones", zonesStr),
|
||||
kv("Dates", datesStr),
|
||||
blocTarif,
|
||||
].join("");
|
||||
|
||||
return gridWrap2cols(gauche, droite);
|
||||
}
|
||||
|
||||
|
||||
//sectionRC : récupere les détails d'un parcours RC pour le mettre en forme (2 colonnes)
|
||||
// ---------- RC ----------
|
||||
function sectionRC(produit, contrat) {
|
||||
if (!produit && contrat) {
|
||||
return createMessageBox(
|
||||
'info',
|
||||
'Informations non disponibles',
|
||||
'Les informations sur ce Parcours RC ne sont pas encore disponibles.<br>La fiche RC n\'a pas encore été créée pour ce parcours.'
|
||||
);
|
||||
}
|
||||
if (!produit) {
|
||||
return createMessageBox(
|
||||
'dev',
|
||||
'Fonctionnalité en cours de développement',
|
||||
'L\'affichage des détails pour les parcours RC n\'est pas encore disponible.<br>Cette fonctionnalité sera bientôt implémentée.'
|
||||
);
|
||||
}
|
||||
|
||||
// 1) CA
|
||||
const caStr = safeCA(produit.ca);
|
||||
|
||||
// 2) Type de cotisation
|
||||
let typeCotStr = "NC";
|
||||
if (produit.typeCot) {
|
||||
typeCotStr = (produit.typeCot === "revisable") ? "Révisable"
|
||||
: (produit.typeCot === "forfaitaire") ? "Forfaitaire"
|
||||
: produit.typeCot;
|
||||
}
|
||||
|
||||
// 3) Activités
|
||||
const activites = [];
|
||||
if (produit.actVoiturier) activites.push("Voiturier");
|
||||
if (produit.actLoueur) activites.push("Loueur");
|
||||
if (produit.actMultimodal) activites.push("Multimodal");
|
||||
if (produit.actDouane) activites.push("Douane");
|
||||
if (produit.actLevageur) activites.push("Levageur");
|
||||
if (produit.actTransitaire) activites.push("Transitaire");
|
||||
const activiteAssuree = activites.length > 0 ? activites.join(", ") : "NC";
|
||||
|
||||
// 4) Marchandises
|
||||
const garanties = [];
|
||||
if (produit.marRoulant) garanties.push("Roulant");
|
||||
if (produit.marEngins) garanties.push("Engins");
|
||||
if (produit.marPerissable) garanties.push("Périssable");
|
||||
if (produit.marOrdinaire) garanties.push("Ordinaire");
|
||||
if (produit.marAnimaux) garanties.push("Animaux");
|
||||
if (produit.marCiterne)garanties.push("Citerne");
|
||||
if (produit.marBeton) garanties.push("Béton");
|
||||
if (produit.marExceptionnels) garanties.push("Exceptionnels");
|
||||
if (produit.marMobilerUsag) garanties.push("Mobilier usagé");
|
||||
if (produit.marVrac) garanties.push("Vrac");
|
||||
if (produit.marRoulantDem) garanties.push("Roulant déménagement");
|
||||
const garantiesStr = garanties.length > 0 ? garanties.join(", ") : "NC";
|
||||
|
||||
// 5) Zones
|
||||
const zones = [];
|
||||
if (produit.zone1) zones.push("1");
|
||||
if (produit.zone2) zones.push("2");
|
||||
if (produit.zone3) zones.push("3");
|
||||
if (produit.zone4) zones.push("4");
|
||||
if (produit.zone5) zones.push("5");
|
||||
if (produit.zone6) zones.push("6");
|
||||
const zonesStr = zones.length > 0 ? zones.join(", ") : "NC";
|
||||
|
||||
// 6) Dates
|
||||
const dateEffet = produit.dateEffet ? fmtDate(produit.dateEffet, false) : "NC";
|
||||
const dateEcheance = produit.dateEcheance ? fmtDate(produit.dateEcheance, false) : "NC";
|
||||
const dateFin = produit.dateFin ? fmtDate(produit.dateFin, false) : "NC";
|
||||
const datesStr = `Effet: ${dateEffet} / Échéance: ${dateEcheance} / Fin: ${dateFin}`;
|
||||
|
||||
// 7) Tarif
|
||||
const tarif = produit?.["@expand"]?.tarif || null;
|
||||
const blocTarif = buildTarifBlock({
|
||||
tarifRef: tarif?.tarifRef,
|
||||
ht: produit.cotTotalHT ?? null,
|
||||
ttc: produit.cotTotalTTC ?? null
|
||||
});
|
||||
|
||||
const gauche = [
|
||||
kv("Chiffre d'affaires", caStr),
|
||||
kv("Type de cotisation", typeCotStr),
|
||||
kv("Activités assurées", activiteAssuree),
|
||||
kv("Marchandises", garantiesStr),
|
||||
].join("");
|
||||
|
||||
const droite = [
|
||||
kv("Zones", zonesStr),
|
||||
kv("Dates", datesStr),
|
||||
blocTarif,
|
||||
].join("");
|
||||
|
||||
return gridWrap2cols(gauche, droite);
|
||||
}
|
||||
|
||||
|
||||
//sectionFAC : récupere les détails d'un parcours FAC pour le mettre en forme (2 colonnes)
|
||||
// ---------- FAC ----------
|
||||
function sectionFAC(produit, contrat) {
|
||||
if (!produit && contrat) {
|
||||
return createMessageBox(
|
||||
'info',
|
||||
'Informations non disponibles',
|
||||
'Les informations sur ce Parcours FAC ne sont pas encore disponibles.<br>La fiche FAC n\'a pas encore été créée pour ce parcours.'
|
||||
);
|
||||
}
|
||||
if (!produit) {
|
||||
return createMessageBox(
|
||||
'dev',
|
||||
'Fonctionnalité en cours de développement',
|
||||
'L\'affichage des détails pour les parcours FAC n\'est pas encore disponible.<br>Cette fonctionnalité sera bientôt implémentée.'
|
||||
);
|
||||
}
|
||||
|
||||
// 1) CA
|
||||
const caStr = safeCA(produit.ca);
|
||||
|
||||
// 2) Type de cotisation
|
||||
const typeCotStr = "NC";
|
||||
|
||||
// 3) Activité
|
||||
const activiteAssuree = produit.actAssure || "NC";
|
||||
|
||||
// 4) Garanties (modes de transport déclarés)
|
||||
const garanties = [];
|
||||
if (produit.terrestre && produit.terrestre !== "NC" && produit.terrestre !== "") garanties.push("Terrestre");
|
||||
if (produit.maritime && produit.maritime !== "NC" && produit.maritime !== "") garanties.push("Maritime");
|
||||
if (produit.aerien && produit.aerien !== "NC" && produit.aerien !== "") garanties.push("Aérien");
|
||||
if (produit.postal && produit.postal !== "NC" && produit.postal !== "") garanties.push("Postal");
|
||||
if (produit.fluvial && produit.fluvial !== "NC" && produit.fluvial !== "") garanties.push("Fluvial");
|
||||
const garantiesStr = garanties.length > 0 ? garanties.join(", ") : "NC";
|
||||
|
||||
// 5) Zones
|
||||
const zones = [];
|
||||
if (produit.zone1) zones.push("1");
|
||||
if (produit.zone2) zones.push("2");
|
||||
if (produit.zone3) zones.push("3");
|
||||
if (produit.zone4) zones.push("4");
|
||||
if (produit.zone5) zones.push("5");
|
||||
if (produit.zone6) zones.push("6");
|
||||
const zonesStr = zones.length > 0 ? zones.join(", ") : "NC";
|
||||
|
||||
// 6) Dates
|
||||
const dateEffet = produit.dateEffet ? fmtDate(produit.dateEffet, false) : "NC";
|
||||
const dateEcheance = produit.dateEcheance ? fmtDate(produit.dateEcheance, false) : "NC";
|
||||
const dateFin = produit.dateFin ? fmtDate(produit.dateFin, false) : "NC";
|
||||
const datesStr = `Effet: ${dateEffet} / Échéance: ${dateEcheance} / Fin: ${dateFin}`;
|
||||
|
||||
// 7) Tarif
|
||||
const tarif = produit?.["@expand"]?.tarif || null;
|
||||
const blocTarif = buildTarifBlock({
|
||||
tarifRef: tarif?.tarifRef,
|
||||
ht: produit.cotAnnuelleHT ?? null,
|
||||
ttc: produit.cotAnnuelleTTC ?? null
|
||||
});
|
||||
|
||||
const gauche = [
|
||||
kv("Chiffre d'affaires", caStr),
|
||||
kv("Type de cotisation", typeCotStr),
|
||||
kv("Activité assurée", activiteAssuree),
|
||||
kv("Garanties", garantiesStr),
|
||||
].join("");
|
||||
|
||||
const droite = [
|
||||
kv("Zones", zonesStr),
|
||||
kv("Dates", datesStr),
|
||||
blocTarif,
|
||||
].join("");
|
||||
|
||||
return gridWrap2cols(gauche, droite);
|
||||
}
|
||||
|
|
@ -1,765 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
═══════════════════════════════════════════════════════════════════════════════
|
||||
Migration PocketBase 0.7.5 — Éclatement des parcours RC et FAC en 3 collections
|
||||
═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
CONTEXTE
|
||||
--------
|
||||
Le parcours TPPC a déjà été éclaté en 3 collections :
|
||||
tppc (principale) / tppcprojet / tppctarif
|
||||
Les parcours RC et FAC sont encore "à plat" : tout est dans une seule collection
|
||||
(`rc` = 87 champs, `fac` = 51 champs). Le nouveau code front/back attend désormais :
|
||||
|
||||
RC : rc (principale) + projetRC + tarifRC (relations rc.projetRC / rc.tarifRC)
|
||||
FAC : fac (principale) + facprojet + factarif (relations fac.projet / fac.tarif)
|
||||
|
||||
Ce script :
|
||||
1. CRÉE les 4 collections manquantes : projetRC, tarifRC, facprojet, factarif
|
||||
(ligne _collections + table SQLite + index `<col>_created_idx`, à l'identique
|
||||
de la façon dont PocketBase 0.7.5 les crée — modèle copié sur tppc*).
|
||||
2. AJOUTE à `rc` et `fac` les colonnes/relations manquantes attendues par le code.
|
||||
3. MIGRE toutes les données existantes (TOUS les parcours) en répartissant
|
||||
les champs vers la bonne collection.
|
||||
|
||||
PRINCIPE DE FIABILITÉ : migration 100 % NON DESTRUCTIVE.
|
||||
- On AJOUTE des colonnes, on ne supprime ni ne renomme jamais une colonne existante.
|
||||
- On COPIE les données (les anciennes valeurs restent dans `rc`/`fac` en secours).
|
||||
- Les `id` de `rc`/`fac` ne changent pas → les relations `contrat.rc` / `contrat.fac`
|
||||
restent valides.
|
||||
- Tout s'exécute dans UNE transaction SQLite (atomique : tout ou rien).
|
||||
- Idempotent : relançable sans risque (les enregistrements déjà migrés sont ignorés).
|
||||
- Backup automatique du fichier data.db avant toute écriture.
|
||||
|
||||
UTILISATION
|
||||
-----------
|
||||
⚠️ ARRÊTER PocketBase avant (accès exclusif au fichier), puis le redémarrer après.
|
||||
|
||||
python3 migrate_split_rc_fac.py --db /chemin/vers/pb_data/data.db # migration
|
||||
python3 migrate_split_rc_fac.py --db .../data.db --dry-run # simulation
|
||||
python3 migrate_split_rc_fac.py --db .../data.db --verify-only # vérif seule
|
||||
python3 migrate_split_rc_fac.py --db .../data.db --yes # sans confirmation
|
||||
|
||||
Aucune dépendance externe (uniquement la bibliothèque standard Python 3).
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import shutil
|
||||
import sqlite3
|
||||
import sys
|
||||
|
||||
ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789"
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# SPÉCIFICATION DES SCHÉMAS
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Chaque champ = (nom, type) où type ∈ {
|
||||
# "bool", "number", "text", "json",
|
||||
# ("select", maxSelect, [valeurs]),
|
||||
# ("rel", "nomCollectionCible"),
|
||||
# }
|
||||
# Type SQLite de la colonne backing (identique à ce que crée PocketBase) :
|
||||
# bool→Boolean DEFAULT FALSE | number→REAL DEFAULT 0 |
|
||||
# text/select/rel→TEXT DEFAULT '' | json→JSON DEFAULT NULL
|
||||
|
||||
# ── projetRC : niveau "projet" du RC ≈ ancien `rc` à plat (+ activités compl. JSON)
|
||||
PROJET_RC_FIELDS = [
|
||||
("assureAdditionnel", "json"),
|
||||
("designationVehicule", "json"),
|
||||
("grilleMultimodal", "json"),
|
||||
("grilleTerrestre", "json"),
|
||||
("grilleAerien", "json"),
|
||||
("activitesVoiturier", "json"),
|
||||
("activitesCommissionnaire", "json"),
|
||||
("activitesDemenageur", "json"),
|
||||
("activitesLogistique", "json"),
|
||||
# Activités (chip) + capital "Nous consulter" possible → text
|
||||
("actVoiturier", "bool"), ("valueActVoiturier", "text"),
|
||||
("actLoueur", "bool"), ("valueActLoueur", "text"),
|
||||
("actMultimodal", "bool"), ("valueActMultimodal", "text"),
|
||||
("actDouane", "bool"), ("valueActDouane", "text"),
|
||||
("actDemPar", "bool"), ("valueActDemPar", "text"),
|
||||
("actDemParDom", "bool"), ("valueActDemParDom", "text"),
|
||||
("actDemParAdv", "bool"), ("valueActDemParAdv", "text"),
|
||||
("actDemEntr", "bool"), ("valueActDemEntr", "text"),
|
||||
("actDemInterne", "bool"), ("valueActDemInterne", "text"),
|
||||
("actGardeMeuble", "bool"), ("valueActGardeMeuble", "text"),
|
||||
("actEntDep", "bool"), ("valueActEntDep", "text"),
|
||||
("actPrestaLog", "bool"), ("valueActPrestaLog", "text"),
|
||||
("actLevageur", "bool"), ("valueActLevageur", "text"),
|
||||
("actTransitaire", "bool"), ("valueActTransitaire", "text"),
|
||||
# Marchandises
|
||||
("marOrdinaire", "bool"), ("marRoulant", "bool"), ("marEngins", "bool"),
|
||||
("marRoulantDem", "bool"), ("marMobilerUsag", "bool"), ("marPerissable", "bool"),
|
||||
("marAnimaux", "bool"), ("marCiterne", "bool"), ("marBeton", "bool"),
|
||||
("marExceptionnels", "bool"), ("marVrac", "bool"),
|
||||
# Zones
|
||||
("zone1", "bool"), ("zone2", "bool"), ("zone3", "bool"),
|
||||
("zone4", "bool"), ("zone5", "bool"), ("zone6", "bool"),
|
||||
# Extensions RCC / RCE
|
||||
("extRCCModifCalArrim", "bool"), ("extRCCFerroutage", "bool"),
|
||||
("extRCCFraisRecons", "bool"), ("extRCCConfie", "bool"),
|
||||
("typeExtConfies", "text"), ("extRCCTPPC", "bool"), ("extRCCRegie", "bool"),
|
||||
("extRCCSansMontageDemontage", "bool"),
|
||||
("autresRC", "bool"), ("extRCEBraDebra", "bool"), ("extRCEMontageDemontage", "bool"),
|
||||
# Temporalités
|
||||
("tempo", "text"), ("dateEffet", "text"), ("dateEcheance", "text"), ("dateFin", "text"),
|
||||
("pj", "bool"), ("programmeInternationale", "bool"), ("participationResultat", "bool"),
|
||||
# Cotisations (peuvent valoir "Nous consulter" → text)
|
||||
("typeCot", "text"), ("ca", "text"), ("cotIrreductible", "text"),
|
||||
("tauxRCCHT", "text"), ("tauxRCCTTC", "text"), ("tauxRCEHT", "text"), ("tauxRCETTC", "text"),
|
||||
("tauxTotalHT", "text"), ("tauxTotalTTC", "text"),
|
||||
("cotRCCHT", "text"), ("cotRCCTTC", "text"), ("cotRCEHT", "text"), ("cotRCETTC", "text"),
|
||||
("cotPJHT", "text"), ("cotPJTTC", "text"), ("cotTotalHT", "text"), ("cotTotalTTC", "text"),
|
||||
("cotFraisHT", "text"), ("cotFraisTTC", "text"),
|
||||
]
|
||||
|
||||
# ── tarifRC : niveau "tarif" du RC (chiffrage calculé). Aucune source ancienne.
|
||||
TARIF_RC_FIELDS = [
|
||||
("sinistre", "number"),
|
||||
("pourcentageVoiturier", "number"), ("isSetVoiturier", "bool"),
|
||||
("pourcentageCommissionnaire", "number"), ("isSetCommissionnaire", "bool"),
|
||||
("pourcentageDemenageur", "number"), ("isSetDemenageur", "bool"),
|
||||
("pourcentageLogistique", "number"), ("isSetLogistique", "bool"),
|
||||
("pourcentageAutocariste", "number"), ("isSetAutocariste", "bool"),
|
||||
("pourcentageAutres", "number"), ("isSetAutres", "bool"),
|
||||
("primeRCC_250", "number"), ("primeRCE_250", "number"), ("primePJ_250", "number"),
|
||||
("primeTotal_250", "number"), ("tauxRCC_250", "number"), ("tauxRCE_250", "number"),
|
||||
("tauxGlobal_250", "number"),
|
||||
("primeRCC_400", "number"), ("primeRCE_400", "number"), ("primePJ_400", "number"),
|
||||
("primeTotal_400", "number"), ("tauxRCC_400", "number"), ("tauxRCE_400", "number"),
|
||||
("tauxGlobal_400", "number"),
|
||||
("primeRCC_2000", "number"), ("primeRCE_2000", "number"), ("primePJ_2000", "number"),
|
||||
("primeTotal_2000", "number"), ("tauxRCC_2000", "number"), ("tauxRCE_2000", "number"),
|
||||
("tauxGlobal_2000", "number"),
|
||||
("franchiseChoisie", "text"),
|
||||
("checkDomImmat", "bool"), ("capitalDomImmat", "number"),
|
||||
("checkContConf", "bool"), ("capitalContConf", "number"),
|
||||
("checkDiffInv", "bool"), ("capitalDiffInv", "number"),
|
||||
("checkStationLavage", "bool"), ("checkGarageInterne", "bool"), ("checkCSE", "bool"),
|
||||
("checkTPPC", "bool"), ("capitalTPPC", "number"), ("vehiculesTPPC", "number"),
|
||||
("checkPJ", "bool"),
|
||||
("tarifcommercial", "number"),
|
||||
]
|
||||
|
||||
# ── Colonnes à AJOUTER à la collection `rc` (principale). zone1-6 existent déjà.
|
||||
RC_MAIN_ADD = [
|
||||
("tarifRC", ("rel", "tarifRC")),
|
||||
("projetRC", ("rel", "projetRC")),
|
||||
("typeCotisation", "text"),
|
||||
("chiffreAffaires", "number"),
|
||||
("nombreVehicules", "number"),
|
||||
("checkRCE", "bool"),
|
||||
("checkVoiturier", "bool"), ("capitalVoiturier", "number"),
|
||||
("checkCommissionnaire", "bool"), ("capitalCommissionnaire", "number"),
|
||||
("checkDemenageur", "bool"), ("capitalDemenageur", "number"),
|
||||
("checkLogistique", "bool"), ("capitalLogistique", "number"),
|
||||
("checkAutocariste", "bool"), ("capitalAutocariste", "number"),
|
||||
("checkAutres", "bool"), ("capitalAutres", "number"),
|
||||
("actComplVoiturier", "json"), ("actComplCommissionnaire", "json"),
|
||||
("actComplDemenageur", "json"), ("actComplLogistique", "json"),
|
||||
("marchandisesVoiturier", "json"), ("marchandisesCommissionnaire", "json"),
|
||||
("marchandisesDemenageur", "json"), ("marchandisesLogistique", "json"),
|
||||
("marchandisesAutocariste", "json"), ("marchandisesAutres", "json"),
|
||||
("commentaire", "text"),
|
||||
]
|
||||
|
||||
# ── facprojet : niveau "projet" du FAC ≈ champs "projet" de l'ancien `fac`.
|
||||
FAC_PROJET_FIELDS = [
|
||||
("assureAdditionnel", "json"),
|
||||
("valeurAssureeBase", "json"), # multi-select → tableau JSON
|
||||
("risqueTransport", "json"), # multi-select → tableau JSON
|
||||
("garOpt", "json"), # multi-select → tableau JSON
|
||||
("valeurAssuree", "text"), # single-select → string
|
||||
("typeTPPC", "text"),
|
||||
("tempo", "text"),
|
||||
("typeContrat", "text"),
|
||||
("dateEffet", "text"), ("dateEcheance", "text"), ("dateFin", "text"),
|
||||
("lieuDepart", "text"), ("lieuArrivee", "text"),
|
||||
("coassurance", "bool"), ("programmeInternational", "bool"),
|
||||
("participationResultat", "bool"),
|
||||
# Capitaux/cotisations : text (le code compare franchiseTransport !== "" et rend brut)
|
||||
("capitalMax", "text"), ("capitalColis", "text"), ("capitalExped", "text"),
|
||||
("franchiseTransport", "text"), ("cotAnnuelleTTC", "text"), ("cotComptant", "text"),
|
||||
]
|
||||
|
||||
# ── factarif : niveau "tarif" du FAC (chiffrage calculé). Aucune source ancienne.
|
||||
FAC_TARIF_FIELDS = [
|
||||
("fluxAchats", "json"), ("fluxVentes", "json"),
|
||||
("franchise350", "json"), ("franchise750", "json"), ("sansFranchise", "json"),
|
||||
("asIf", "json"),
|
||||
("fluxIntersites", "bool"),
|
||||
("typePolice", "text"), ("typeRO", "text"), ("conditionnement", "text"),
|
||||
("oldFranchise", "text"), ("sinistres", "text"), ("nbVehicTPPC", "text"),
|
||||
("typeFlux", "text"), ("montantGarantir", "text"), ("engagementRG", "text"),
|
||||
("selectedFranchise", "text"), ("typeMarExpo", "text"), ("commentaire", "text"),
|
||||
("zone", "text"), ("transport", "text"),
|
||||
]
|
||||
# Champs JSON dont la valeur est un OBJET (et non un tableau) → vide = "{}".
|
||||
# Tous les autres champs JSON sont des tableaux → vide = "[]".
|
||||
# (Le code de génération fait .includes()/.forEach()/.proposition sans garde : on
|
||||
# garantit donc toujours une valeur JSON valide, jamais NULL, pour éviter tout crash.)
|
||||
OBJECT_JSON_FIELDS = {"fluxAchats", "fluxVentes", "franchise350", "franchise750", "sansFranchise"}
|
||||
|
||||
|
||||
def json_empty(fname):
|
||||
return "{}" if fname in OBJECT_JSON_FIELDS else "[]"
|
||||
|
||||
# ── Colonnes à AJOUTER à la collection `fac` (principale).
|
||||
FAC_MAIN_ADD = [
|
||||
("projet", ("rel", "facprojet")),
|
||||
("tarif", ("rel", "factarif")),
|
||||
("nbVehicExpo", "text"),
|
||||
("actAssuree", "text"),
|
||||
("typeRG", "text"),
|
||||
("multimodal", "text"),
|
||||
("rg", "text"),
|
||||
("primeHT", "text"),
|
||||
("primeMini", "text"),
|
||||
("zones", "json"),
|
||||
("tppc", "bool"),
|
||||
]
|
||||
|
||||
# Renommages FAC (ancien champ `fac` → nouveau champ `fac` principal). Copie, non destructif.
|
||||
FAC_MAIN_RENAME = {
|
||||
"actAssuree": "actAssure",
|
||||
"nbVehicExpo": "marExpo",
|
||||
"typeRG": "typeGarantieRG",
|
||||
"rg": "risqueGuerre",
|
||||
"primeHT": "cotAnnuelleHT",
|
||||
"primeMini": "cotIrred",
|
||||
}
|
||||
|
||||
# Mapping "secours" RC principal ← ancien `rc` (d'après FIELD_MAPPING de rc-data-manager.js)
|
||||
RC_MAIN_FILL = {
|
||||
"chiffreAffaires": "ca",
|
||||
"typeCotisation": "typeCot",
|
||||
"checkRCE": "autresRC",
|
||||
"checkVoiturier": "actVoiturier", "capitalVoiturier": "valueActVoiturier",
|
||||
"checkCommissionnaire": "actMultimodal", "capitalCommissionnaire": "valueActMultimodal",
|
||||
"checkDemenageur": "actDemEntr", "capitalDemenageur": "valueActDemEntr",
|
||||
"checkLogistique": "actPrestaLog", "capitalLogistique": "valueActPrestaLog",
|
||||
}
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Outils bas niveau
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def now_pb():
|
||||
"""Horodatage au format PocketBase : 'YYYY-MM-DD HH:MM:SS.mmm'."""
|
||||
return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S.") + \
|
||||
f"{datetime.datetime.now().microsecond // 1000:03d}"
|
||||
|
||||
|
||||
def rand_id(n=15):
|
||||
return "".join(random.choices(ALPHABET, k=n))
|
||||
|
||||
|
||||
def sqlite_coltype(ftype):
|
||||
if ftype == "bool":
|
||||
return "Boolean DEFAULT FALSE"
|
||||
if ftype == "number":
|
||||
return "REAL DEFAULT 0"
|
||||
if ftype == "json":
|
||||
return "JSON DEFAULT NULL"
|
||||
# text, select, rel
|
||||
return "TEXT DEFAULT ''"
|
||||
|
||||
|
||||
def pb_field_options(ftype, resolve_collection):
|
||||
"""Bloc `options` au format PocketBase selon le type."""
|
||||
if ftype == "bool":
|
||||
return {}
|
||||
if ftype == "number":
|
||||
return {"min": None, "max": None}
|
||||
if ftype == "json":
|
||||
return {}
|
||||
if ftype == "text":
|
||||
return {"min": None, "max": None, "pattern": ""}
|
||||
if isinstance(ftype, tuple) and ftype[0] == "select":
|
||||
return {"maxSelect": ftype[1], "values": ftype[2]}
|
||||
if isinstance(ftype, tuple) and ftype[0] == "rel":
|
||||
return {"maxSelect": 1, "collectionId": resolve_collection(ftype[1]),
|
||||
"cascadeDelete": False}
|
||||
raise ValueError(f"type inconnu: {ftype}")
|
||||
|
||||
|
||||
def base_ftype(ftype):
|
||||
"""Type 'racine' pour la colonne SQLite."""
|
||||
if isinstance(ftype, tuple):
|
||||
return ftype[0] # 'select' ou 'rel' → TEXT
|
||||
return ftype
|
||||
|
||||
|
||||
def build_schema_json(fields, resolve_collection):
|
||||
out = []
|
||||
for name, ftype in fields:
|
||||
bt = base_ftype(ftype)
|
||||
pb_type = {"select": "select", "rel": "relation"}.get(bt, bt)
|
||||
out.append({
|
||||
"system": False,
|
||||
"id": rand_id(8),
|
||||
"name": name,
|
||||
"type": pb_type,
|
||||
"required": False,
|
||||
"unique": False,
|
||||
"options": pb_field_options(ftype, resolve_collection),
|
||||
})
|
||||
return out
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Accès schéma DB
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def table_columns(cur, table):
|
||||
cur.execute(f"PRAGMA table_info(`{table}`)")
|
||||
return [r[1] for r in cur.fetchall()]
|
||||
|
||||
|
||||
def collection_id(cur, name):
|
||||
cur.execute("SELECT id FROM _collections WHERE name=?", (name,))
|
||||
r = cur.fetchone()
|
||||
return r[0] if r else None
|
||||
|
||||
|
||||
def collection_exists(cur, name):
|
||||
return collection_id(cur, name) is not None
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Création de collection (ligne _collections + table + index)
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def create_collection(cur, name, fields, resolve_collection, log):
|
||||
if collection_exists(cur, name):
|
||||
log(f" • collection '{name}' existe déjà → ignorée")
|
||||
return collection_id(cur, name)
|
||||
|
||||
cid = rand_id(15)
|
||||
# garantir l'unicité de l'id de collection
|
||||
cur.execute("SELECT id FROM _collections")
|
||||
existing = {r[0] for r in cur.fetchall()}
|
||||
while cid in existing:
|
||||
cid = rand_id(15)
|
||||
|
||||
schema = build_schema_json(fields, resolve_collection)
|
||||
ts = now_pb()
|
||||
cur.execute(
|
||||
"INSERT INTO _collections "
|
||||
"(id, name, system, listRule, viewRule, createRule, updateRule, deleteRule, schema, created, updated) "
|
||||
"VALUES (?,?,?,?,?,?,?,?,?,?,?)",
|
||||
(cid, name, 0, None, None, None, None, None, json.dumps(schema), ts, ts),
|
||||
)
|
||||
|
||||
cols_sql = ["`id` TEXT PRIMARY KEY",
|
||||
"`created` TEXT DEFAULT \"\" NOT NULL",
|
||||
"`updated` TEXT DEFAULT \"\" NOT NULL"]
|
||||
for fname, ftype in fields:
|
||||
cols_sql.append(f'"{fname}" {sqlite_coltype(base_ftype(ftype))}')
|
||||
cur.execute(f"CREATE TABLE `{name}` (" + ", ".join(cols_sql) + ")")
|
||||
cur.execute(f"CREATE INDEX `{name}_created_idx` ON `{name}` (`created`)")
|
||||
log(f" ✓ collection '{name}' créée (id={cid}, {len(fields)} champs)")
|
||||
return cid
|
||||
|
||||
|
||||
def add_columns(cur, table, add_fields, resolve_collection, log):
|
||||
"""Ajoute des champs à une collection existante (schema JSON + ALTER TABLE)."""
|
||||
cid = collection_id(cur, table)
|
||||
cur.execute("SELECT schema FROM _collections WHERE id=?", (cid,))
|
||||
schema = json.loads(cur.fetchone()[0])
|
||||
existing_field_names = {f["name"] for f in schema}
|
||||
existing_cols = set(table_columns(cur, table))
|
||||
|
||||
added = []
|
||||
for fname, ftype in add_fields:
|
||||
# 1) ligne de schéma
|
||||
if fname not in existing_field_names:
|
||||
bt = base_ftype(ftype)
|
||||
pb_type = {"select": "select", "rel": "relation"}.get(bt, bt)
|
||||
schema.append({
|
||||
"system": False, "id": rand_id(8), "name": fname, "type": pb_type,
|
||||
"required": False, "unique": False,
|
||||
"options": pb_field_options(ftype, resolve_collection),
|
||||
})
|
||||
# 2) colonne SQLite
|
||||
if fname not in existing_cols:
|
||||
cur.execute(f'ALTER TABLE `{table}` ADD COLUMN "{fname}" {sqlite_coltype(base_ftype(ftype))}')
|
||||
added.append(fname)
|
||||
|
||||
cur.execute("UPDATE _collections SET schema=?, updated=? WHERE id=?",
|
||||
(json.dumps(schema), now_pb(), cid))
|
||||
if added:
|
||||
log(f" ✓ '{table}' : {len(added)} colonnes ajoutées ({', '.join(added)})")
|
||||
else:
|
||||
log(f" • '{table}' : aucune colonne à ajouter (déjà à jour)")
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Conversion de valeurs pour la copie
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def num_to_text(v):
|
||||
"""REAL → chaîne propre ('1000.0' → '1000', '12.5' → '12.5', None → '')."""
|
||||
if v is None:
|
||||
return ""
|
||||
if isinstance(v, float):
|
||||
return str(int(v)) if v.is_integer() else repr(v)
|
||||
return str(v)
|
||||
|
||||
|
||||
def default_for(ftype):
|
||||
bt = base_ftype(ftype)
|
||||
if bt == "bool":
|
||||
return 0
|
||||
if bt == "number":
|
||||
return 0
|
||||
if bt == "json":
|
||||
return None
|
||||
return "" # text / select / rel
|
||||
|
||||
|
||||
def copy_value(fname, ftype, raw):
|
||||
"""Valeur à écrire dans la collection cible en copiant `raw` (source) selon le type cible.
|
||||
|
||||
- JSON : on garantit toujours une valeur valide ('[]' tableau, '{}' objet) — jamais NULL —
|
||||
car le code de génération fait .includes()/.forEach()/.proposition sans garde.
|
||||
- text : un REAL source est converti en chaîne propre ('1000.0' → '1000').
|
||||
"""
|
||||
bt = base_ftype(ftype)
|
||||
if bt == "json":
|
||||
if raw is None or raw == "":
|
||||
return json_empty(fname)
|
||||
return raw
|
||||
if bt == "text":
|
||||
if isinstance(raw, float):
|
||||
return num_to_text(raw)
|
||||
return raw if raw is not None else ""
|
||||
# bool / number / select / rel
|
||||
return raw if raw is not None else default_for(ftype)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Migration des données
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def insert_record(cur, table, values: dict):
|
||||
"""Insère un enregistrement (ajoute id/created/updated)."""
|
||||
rid = rand_id(15)
|
||||
cur.execute(f"SELECT 1 FROM `{table}` WHERE id=?", (rid,))
|
||||
while cur.fetchone():
|
||||
rid = rand_id(15)
|
||||
cur.execute(f"SELECT 1 FROM `{table}` WHERE id=?", (rid,))
|
||||
ts = now_pb()
|
||||
cols = ["id", "created", "updated"] + list(values.keys())
|
||||
vals = [rid, ts, ts] + list(values.values())
|
||||
ph = ",".join("?" * len(cols))
|
||||
quoted = ",".join(f'"{c}"' for c in cols)
|
||||
cur.execute(f'INSERT INTO `{table}` ({quoted}) VALUES ({ph})', vals)
|
||||
return rid
|
||||
|
||||
|
||||
def migrate_rc(cur, log, dry):
|
||||
rc_cols = set(table_columns(cur, "rc"))
|
||||
projet_field_types = dict(PROJET_RC_FIELDS)
|
||||
projet_names = [n for n, _ in PROJET_RC_FIELDS]
|
||||
|
||||
cur.execute("SELECT * FROM rc")
|
||||
rows = cur.fetchall()
|
||||
colnames = [d[0] for d in cur.description]
|
||||
idx = {c: i for i, c in enumerate(colnames)}
|
||||
|
||||
migrated = skipped = 0
|
||||
for row in rows:
|
||||
rc_id = row[idx["id"]]
|
||||
# idempotence : déjà migré si projetRC renseigné
|
||||
if "projetRC" in idx and row[idx["projetRC"]]:
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
# 1) projetRC : copie des champs projet depuis rc (mêmes noms)
|
||||
projet_vals = {}
|
||||
for fname in projet_names:
|
||||
ftype = projet_field_types[fname]
|
||||
raw = row[idx[fname]] if fname in rc_cols else None
|
||||
projet_vals[fname] = copy_value(fname, ftype, raw)
|
||||
|
||||
# 2) tarifRC : enregistrement par défaut (aucune source ancienne)
|
||||
tarif_vals = {n: default_for(t) for n, t in TARIF_RC_FIELDS}
|
||||
|
||||
# 3) rc principal : relations + remplissage "secours" + JSON vides → []
|
||||
rc_update = {}
|
||||
for fname, ftype in RC_MAIN_ADD:
|
||||
if base_ftype(ftype) == "json":
|
||||
rc_update[fname] = "[]" # jamais NULL (le front parse ces tableaux)
|
||||
for dest, src in RC_MAIN_FILL.items():
|
||||
if src in rc_cols:
|
||||
rc_update[dest] = row[idx[src]]
|
||||
|
||||
if dry:
|
||||
migrated += 1
|
||||
continue
|
||||
|
||||
id_projet = insert_record(cur, "projetRC", projet_vals)
|
||||
id_tarif = insert_record(cur, "tarifRC", tarif_vals)
|
||||
rc_update["projetRC"] = id_projet
|
||||
rc_update["tarifRC"] = id_tarif
|
||||
sets = ", ".join(f'"{k}"=?' for k in rc_update)
|
||||
cur.execute(f'UPDATE rc SET {sets}, updated=? WHERE id=?',
|
||||
list(rc_update.values()) + [now_pb(), rc_id])
|
||||
migrated += 1
|
||||
|
||||
log(f" RC : {migrated} migrés, {skipped} déjà à jour (total {len(rows)})")
|
||||
return migrated, skipped
|
||||
|
||||
|
||||
def migrate_fac(cur, log, dry):
|
||||
fac_cols = set(table_columns(cur, "fac"))
|
||||
projet_field_types = dict(FAC_PROJET_FIELDS)
|
||||
projet_names = [n for n, _ in FAC_PROJET_FIELDS]
|
||||
tarif_field_types = dict(FAC_TARIF_FIELDS)
|
||||
|
||||
cur.execute("SELECT * FROM fac")
|
||||
rows = cur.fetchall()
|
||||
colnames = [d[0] for d in cur.description]
|
||||
idx = {c: i for i, c in enumerate(colnames)}
|
||||
|
||||
migrated = skipped = 0
|
||||
for row in rows:
|
||||
fac_id = row[idx["id"]]
|
||||
if "projet" in idx and row[idx["projet"]]:
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
# 1) facprojet : champs projet (mêmes noms, conversion REAL→text si besoin)
|
||||
projet_vals = {}
|
||||
for fname in projet_names:
|
||||
ftype = projet_field_types[fname]
|
||||
raw = row[idx[fname]] if fname in fac_cols else None
|
||||
projet_vals[fname] = copy_value(fname, ftype, raw)
|
||||
|
||||
# 2) factarif : défauts (JSON tableaux→[], objets→{} pour éviter tout crash)
|
||||
tarif_vals = {n: (json_empty(n) if base_ftype(t) == "json" else default_for(t))
|
||||
for n, t in FAC_TARIF_FIELDS}
|
||||
# défaut bénin : évite que la génération du doc tarif (qui fait
|
||||
# getSelectedFranchise(selectedFranchise).proposition sans garde) ne plante
|
||||
# pour un ancien enregistrement dépourvu de chiffrage. Rendu vide mais valide.
|
||||
tarif_vals["selectedFranchise"] = "sansFranchise"
|
||||
|
||||
# 3) fac principal : renommages + zones + relations + dérivation tppc
|
||||
fac_update = {}
|
||||
for dest, src in FAC_MAIN_RENAME.items():
|
||||
if src in fac_cols:
|
||||
raw = row[idx[src]]
|
||||
# destinations text → convertir REAL en chaîne propre
|
||||
fac_update[dest] = num_to_text(raw) if isinstance(raw, float) else (raw or "")
|
||||
# zones JSON depuis zone1..zone6
|
||||
zones = [z for z in ("zone1", "zone2", "zone3", "zone4", "zone5", "zone6")
|
||||
if z in fac_cols and row[idx[z]]]
|
||||
fac_update["zones"] = json.dumps(zones)
|
||||
# tppc dérivé de l'ancien typeTPPC (rempli ⇒ TPPC actif)
|
||||
if "typeTPPC" in fac_cols:
|
||||
fac_update["tppc"] = 1 if (row[idx["typeTPPC"]] not in (None, "")) else 0
|
||||
|
||||
if dry:
|
||||
migrated += 1
|
||||
continue
|
||||
|
||||
id_projet = insert_record(cur, "facprojet", projet_vals)
|
||||
id_tarif = insert_record(cur, "factarif", tarif_vals)
|
||||
fac_update["projet"] = id_projet
|
||||
fac_update["tarif"] = id_tarif
|
||||
sets = ", ".join(f'"{k}"=?' for k in fac_update)
|
||||
cur.execute(f'UPDATE fac SET {sets}, updated=? WHERE id=?',
|
||||
list(fac_update.values()) + [now_pb(), fac_id])
|
||||
migrated += 1
|
||||
|
||||
log(f" FAC : {migrated} migrés, {skipped} déjà à jour (total {len(rows)})")
|
||||
return migrated, skipped
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Vérification
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def verify(cur, log):
|
||||
ok = True
|
||||
|
||||
def check(cond, msg):
|
||||
nonlocal ok
|
||||
log((" ✓ " if cond else " ✗ ") + msg)
|
||||
if not cond:
|
||||
ok = False
|
||||
|
||||
# 1) collections présentes
|
||||
for c in ("projetRC", "tarifRC", "facprojet", "factarif"):
|
||||
check(collection_exists(cur, c), f"collection '{c}' présente")
|
||||
cur.execute("SELECT name FROM sqlite_master WHERE type='table' AND name=?", (c,))
|
||||
check(cur.fetchone() is not None, f"table SQLite '{c}' présente")
|
||||
|
||||
# 2) colonnes/relations ajoutées
|
||||
rc_cols = set(table_columns(cur, "rc"))
|
||||
check({"projetRC", "tarifRC"}.issubset(rc_cols), "rc possède projetRC & tarifRC")
|
||||
fac_cols = set(table_columns(cur, "fac"))
|
||||
check({"projet", "tarif", "zones"}.issubset(fac_cols), "fac possède projet, tarif, zones")
|
||||
|
||||
# 3) intégrité relationnelle + couverture
|
||||
cur.execute("SELECT COUNT(*) FROM rc")
|
||||
n_rc = cur.fetchone()[0]
|
||||
cur.execute("SELECT COUNT(*) FROM rc WHERE projetRC!='' AND projetRC IS NOT NULL")
|
||||
n_rc_mig = cur.fetchone()[0]
|
||||
check(n_rc == n_rc_mig, f"tous les rc ont un projetRC ({n_rc_mig}/{n_rc})")
|
||||
cur.execute("SELECT COUNT(*) FROM rc r LEFT JOIN projetRC p ON r.projetRC=p.id "
|
||||
"WHERE r.projetRC!='' AND p.id IS NULL")
|
||||
check(cur.fetchone()[0] == 0, "toutes les relations rc.projetRC résolvent")
|
||||
cur.execute("SELECT COUNT(*) FROM rc r LEFT JOIN tarifRC t ON r.tarifRC=t.id "
|
||||
"WHERE r.tarifRC!='' AND t.id IS NULL")
|
||||
check(cur.fetchone()[0] == 0, "toutes les relations rc.tarifRC résolvent")
|
||||
|
||||
cur.execute("SELECT COUNT(*) FROM fac")
|
||||
n_fac = cur.fetchone()[0]
|
||||
cur.execute("SELECT COUNT(*) FROM fac WHERE projet!='' AND projet IS NOT NULL")
|
||||
n_fac_mig = cur.fetchone()[0]
|
||||
check(n_fac == n_fac_mig, f"tous les fac ont un facprojet ({n_fac_mig}/{n_fac})")
|
||||
cur.execute("SELECT COUNT(*) FROM fac f LEFT JOIN facprojet p ON f.projet=p.id "
|
||||
"WHERE f.projet!='' AND p.id IS NULL")
|
||||
check(cur.fetchone()[0] == 0, "toutes les relations fac.projet résolvent")
|
||||
cur.execute("SELECT COUNT(*) FROM fac f LEFT JOIN factarif t ON f.tarif=t.id "
|
||||
"WHERE f.tarif!='' AND t.id IS NULL")
|
||||
check(cur.fetchone()[0] == 0, "toutes les relations fac.tarif résolvent")
|
||||
|
||||
# 4) contrôle de copie : champs RC identiques entre rc et projetRC
|
||||
cur.execute("""SELECT COUNT(*) FROM rc r JOIN projetRC p ON r.projetRC=p.id
|
||||
WHERE r.actVoiturier IS NOT p.actVoiturier
|
||||
OR IFNULL(r.dateEffet,'') != IFNULL(p.dateEffet,'')
|
||||
OR r.zone1 IS NOT p.zone1""")
|
||||
check(cur.fetchone()[0] == 0, "données RC copiées fidèlement (actVoiturier/dateEffet/zone1)")
|
||||
|
||||
# 5) contrôle FAC : facprojet récupère valeurAssuree & assureAdditionnel
|
||||
cur.execute("""SELECT COUNT(*) FROM fac f JOIN facprojet p ON f.projet=p.id
|
||||
WHERE IFNULL(f.valeurAssuree,'') != IFNULL(p.valeurAssuree,'')""")
|
||||
check(cur.fetchone()[0] == 0, "données FAC projet copiées (valeurAssuree)")
|
||||
|
||||
# 6) zones JSON cohérentes avec les anciens booléens
|
||||
cur.execute("""SELECT COUNT(*) FROM fac
|
||||
WHERE (zone1 AND instr(IFNULL(zones,''),'zone1')=0)
|
||||
OR (zone3 AND instr(IFNULL(zones,''),'zone3')=0)""")
|
||||
check(cur.fetchone()[0] == 0, "fac.zones (JSON) cohérent avec zone1..6")
|
||||
|
||||
# 7) relations contrat intactes
|
||||
cur.execute("SELECT COUNT(*) FROM contrat c LEFT JOIN rc r ON c.rc=r.id "
|
||||
"WHERE c.rc!='' AND r.id IS NULL")
|
||||
check(cur.fetchone()[0] == 0, "relations contrat.rc toujours valides")
|
||||
cur.execute("SELECT COUNT(*) FROM contrat c LEFT JOIN fac f ON c.fac=f.id "
|
||||
"WHERE c.fac!='' AND f.id IS NULL")
|
||||
check(cur.fetchone()[0] == 0, "relations contrat.fac toujours valides")
|
||||
|
||||
log("")
|
||||
log(" ═══ VÉRIFICATION " + ("RÉUSSIE ✓" if ok else "ÉCHOUÉE ✗") + " ═══")
|
||||
return ok
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Orchestration
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def run(db_path, dry_run, verify_only, assume_yes):
|
||||
if not os.path.isfile(db_path):
|
||||
print(f"ERREUR : fichier introuvable : {db_path}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
# garde-fou : WAL non checkpointé ⇒ PocketBase tourne peut-être encore
|
||||
wal = db_path + "-wal"
|
||||
if os.path.isfile(wal) and os.path.getsize(wal) > 0 and not verify_only:
|
||||
print("⚠️ Un fichier -wal non vide est présent : PocketBase est-il bien ARRÊTÉ ?")
|
||||
if not assume_yes and input(" Continuer quand même ? [o/N] ").strip().lower() not in ("o", "y"):
|
||||
return 1
|
||||
|
||||
def log(m):
|
||||
print(m)
|
||||
|
||||
log("═" * 70)
|
||||
log(" Migration RC/FAC → modèle éclaté (main / projet / tarif)")
|
||||
log(f" Base : {db_path}")
|
||||
log(f" Mode : {'VÉRIFICATION SEULE' if verify_only else ('SIMULATION (dry-run)' if dry_run else 'MIGRATION RÉELLE')}")
|
||||
log("═" * 70)
|
||||
|
||||
# backup
|
||||
if not dry_run and not verify_only:
|
||||
bak = db_path + ".bak-" + datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
|
||||
shutil.copy2(db_path, bak)
|
||||
log(f" ✓ backup : {bak}")
|
||||
if not assume_yes:
|
||||
if input(" Lancer la migration ? [o/N] ").strip().lower() not in ("o", "y"):
|
||||
log(" Annulé.")
|
||||
return 1
|
||||
|
||||
conn = sqlite3.connect(db_path)
|
||||
conn.isolation_level = None # gestion manuelle de la transaction
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
if verify_only:
|
||||
verify(cur, log)
|
||||
conn.close()
|
||||
return 0
|
||||
|
||||
cur.execute("BEGIN")
|
||||
resolver = lambda name: collection_id(cur, name)
|
||||
|
||||
log("\n[1/4] Création des collections manquantes")
|
||||
create_collection(cur, "projetRC", PROJET_RC_FIELDS, resolver, log)
|
||||
create_collection(cur, "tarifRC", TARIF_RC_FIELDS, resolver, log)
|
||||
create_collection(cur, "facprojet", FAC_PROJET_FIELDS, resolver, log)
|
||||
create_collection(cur, "factarif", FAC_TARIF_FIELDS, resolver, log)
|
||||
|
||||
log("\n[2/4] Ajout des colonnes/relations à rc et fac")
|
||||
add_columns(cur, "rc", RC_MAIN_ADD, resolver, log)
|
||||
add_columns(cur, "fac", FAC_MAIN_ADD, resolver, log)
|
||||
|
||||
log("\n[3/4] Migration des données")
|
||||
migrate_rc(cur, log, dry_run)
|
||||
migrate_fac(cur, log, dry_run)
|
||||
|
||||
if dry_run:
|
||||
log("\n[dry-run] Aucune modification écrite → ROLLBACK")
|
||||
cur.execute("ROLLBACK")
|
||||
conn.close()
|
||||
return 0
|
||||
|
||||
cur.execute("COMMIT")
|
||||
log("\n[4/4] Vérification post-migration")
|
||||
ok = verify(cur, log)
|
||||
cur.execute("PRAGMA wal_checkpoint(TRUNCATE)")
|
||||
conn.close()
|
||||
return 0 if ok else 3
|
||||
|
||||
except Exception as e:
|
||||
try:
|
||||
cur.execute("ROLLBACK")
|
||||
except Exception:
|
||||
pass
|
||||
conn.close()
|
||||
log(f"\n✗ ERREUR — transaction annulée (ROLLBACK), aucune modification appliquée :\n {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return 4
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description="Migration PocketBase : éclatement RC/FAC.")
|
||||
ap.add_argument("--db", required=True, help="chemin vers pb_data/data.db")
|
||||
ap.add_argument("--dry-run", action="store_true", help="simulation (rollback systématique)")
|
||||
ap.add_argument("--verify-only", action="store_true", help="vérifie l'état sans rien modifier")
|
||||
ap.add_argument("--yes", action="store_true", help="ne pas demander de confirmation")
|
||||
args = ap.parse_args()
|
||||
sys.exit(run(args.db, args.dry_run, args.verify_only, args.yes))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -1,647 +0,0 @@
|
|||
// controllers/historiqueParcoursController.js
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const renderPage = require("../utils/renderHelper");
|
||||
const logger = require("../utils/logger");
|
||||
const parcoursService = require("../services/parcoursService");
|
||||
const clientService = require("../services/clientService");
|
||||
const { fmtDateFR, xmlEsc, cellXml, rowXml } = require("../services/globalService");
|
||||
|
||||
/**
|
||||
* Construit les filtres et le tri PocketBase à partir des paramètres DataTables
|
||||
* @param {Object} params - Paramètres de recherche et filtrage
|
||||
* @param {string[]} params.regions - Liste des régions à filtrer
|
||||
* @param {Object} params.search - Objet de recherche globale
|
||||
* @param {Array} params.columns - Colonnes avec leurs filtres individuels
|
||||
* @param {Array} params.order - Ordre de tri
|
||||
* @returns {Object} - {filter: string, sort: string}
|
||||
*/
|
||||
function buildPocketBaseFilterAndSort({ regions = [], search = { value: "" }, columns = [], order = [] }) {
|
||||
const parts = [];
|
||||
|
||||
/**
|
||||
* Recherche globale : recherche dans tous les champs pertinents
|
||||
*/
|
||||
const q = (search?.value || "").trim();
|
||||
if (q) {
|
||||
const esc = q.replace(/"/g, '\\"');
|
||||
parts.push(`(
|
||||
numParcours ~ "${esc}"
|
||||
|| contrat.numSaisine ~ "${esc}"
|
||||
|| contrat.numContrat ~ "${esc}"
|
||||
|| contrat.produit ~ "${esc}"
|
||||
|| contrat.type ~ "${esc}"
|
||||
|| contrat.intermediaire.nom ~ "${esc}"
|
||||
|| contrat.intermediaire.numPortefeuille ~ "${esc}"
|
||||
|| contrat.client.nom ~ "${esc}"
|
||||
|| contrat.client.numClient ~ "${esc}"
|
||||
|| dernierUtilisateur.prenom ~ "${esc}"
|
||||
|| dernierUtilisateur.nom ~ "${esc}"
|
||||
|| dernierUtilisateur.matricule ~ "${esc}"
|
||||
|| dernierUtilisateur.region.nom ~ "${esc}"
|
||||
)`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recherche par colonne : filtre spécifique pour chaque colonne
|
||||
*/
|
||||
const colFilter = (idx, fieldPaths) => {
|
||||
const v = (columns[idx]?.search?.value || "").trim();
|
||||
if (!v) return null;
|
||||
const esc = v.replace(/"/g, '\\"');
|
||||
return `(${fieldPaths.map(fp => `${fp} ~ "${esc}"`).join(" || ")})`;
|
||||
};
|
||||
|
||||
const pushIf = (v) => { if (v) parts.push(v); };
|
||||
|
||||
// Filtres par colonne (index correspondant à l'ordre des colonnes DataTables)
|
||||
pushIf(colFilter(0, ["numParcours"]));
|
||||
pushIf(colFilter(1, ["created"]));
|
||||
pushIf(colFilter(2, ["dernierUtilisateur.matricule"]));
|
||||
pushIf(colFilter(3, ["dernierUtilisateur.prenom", "dernierUtilisateur.nom"]));
|
||||
pushIf(colFilter(4, ["dernierUtilisateur.region.nom"]));
|
||||
pushIf(colFilter(5, ["contrat.numSaisine"]));
|
||||
pushIf(colFilter(6, ["contrat.numContrat"]));
|
||||
pushIf(colFilter(7, ["contrat.produit"]));
|
||||
pushIf(colFilter(8, ["contrat.type"]));
|
||||
pushIf(colFilter(9, ["contrat.intermediaire.numPortefeuille"]));
|
||||
pushIf(colFilter(10, ["contrat.intermediaire.nom"]));
|
||||
pushIf(colFilter(11, ["contrat.client.numClient"]));
|
||||
pushIf(colFilter(12, ["contrat.client.nom"]));
|
||||
|
||||
const filter = parts.length ? parts.join(" && ") : "";
|
||||
|
||||
/**
|
||||
* Construction du tri PocketBase
|
||||
* Mapping des index de colonnes DataTables vers les champs PocketBase
|
||||
* Le préfixe "-" indique un tri décroissant
|
||||
*/
|
||||
const sortMap = {
|
||||
0: "numParcours",
|
||||
1: "created",
|
||||
2: "dernierUtilisateur.matricule",
|
||||
4: "dernierUtilisateur.region.nom",
|
||||
6: "contrat.numContrat",
|
||||
7: "contrat.produit",
|
||||
10: "contrat.intermediaire.nom",
|
||||
12: "contrat.client.nom"
|
||||
};
|
||||
|
||||
let sort = "-created"; // Tri par défaut : date de création décroissante
|
||||
if (order && order.length > 0) {
|
||||
const { column, dir } = order[0];
|
||||
const field = sortMap[column];
|
||||
if (field) {
|
||||
sort = (dir === "desc" ? "-" : "") + field;
|
||||
}
|
||||
}
|
||||
|
||||
return { filter, sort };
|
||||
}
|
||||
|
||||
/**
|
||||
* Route GET / : Affichage de la page Historique des parcours
|
||||
*/
|
||||
router.get("/", (req, res) => {
|
||||
renderPage("historiqueParcours.ejs", res);});
|
||||
|
||||
/**
|
||||
* /regionUser : requête sur la region de l'user actuel
|
||||
*/
|
||||
router.get("/:regionUser", async (req, res) => {
|
||||
try {
|
||||
const { regionUser } = req.params;
|
||||
const data = await parcoursService.getParcoursByRegionsPage([regionUser], 1, 10, { filter: "", sort: "-created" });
|
||||
if (data) {
|
||||
res.json({ valid: true, data });
|
||||
} else {
|
||||
res.json({ valid: false });
|
||||
}
|
||||
} catch (error) {
|
||||
logger.log("error", error);
|
||||
res.status(500).json({
|
||||
valid: false,
|
||||
error: "Erreur lors de la récupération des parcours.",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* /datatable : DataTables server-side (gestion de pagination)
|
||||
*/
|
||||
router.post("/datatable", async (req, res) => {
|
||||
try {
|
||||
const {
|
||||
draw = 1,
|
||||
start = 0,
|
||||
length = 10,
|
||||
regions = [],
|
||||
search = { value: "" },
|
||||
columns = [],
|
||||
order = []
|
||||
} = req.body || {};
|
||||
|
||||
const page = Math.floor(start / length) + 1; // nb de page
|
||||
const perPage = Number(length) || 10; //nb d'éléments par page
|
||||
|
||||
const { filter, sort } = buildPocketBaseFilterAndSort({ search, columns, order }); // construction du filtrage côté Back
|
||||
|
||||
const result = await parcoursService.getParcoursByRegionsPage([], page, perPage, { filter, sort });
|
||||
|
||||
/**
|
||||
* Construction des lignes de données pour DataTables
|
||||
* Traitement séquentiel pour garantir la récupération des clients
|
||||
*/
|
||||
const rows = [];
|
||||
for (const parcours of result.items) {
|
||||
try {
|
||||
const contrat = parcours["@expand"]?.contrat || null;
|
||||
|
||||
/**
|
||||
* Récupération du client avec fallback
|
||||
* L'expand PocketBase ne fonctionne pas toujours pour contrat.client,
|
||||
* donc on récupère directement via l'ID si nécessaire
|
||||
*/
|
||||
let client = null;
|
||||
if (contrat) {
|
||||
// Tentative via expand (si disponible)
|
||||
client = contrat["@expand"]?.client || null;
|
||||
|
||||
// Fallback : récupération directe via l'ID du client
|
||||
if (!client && contrat.client) {
|
||||
const clientId = typeof contrat.client === 'string'
|
||||
? contrat.client
|
||||
: (contrat.client?.id || contrat.client);
|
||||
|
||||
if (clientId) {
|
||||
try {
|
||||
client = await clientService.getClient(clientId);
|
||||
} catch (err) {
|
||||
// Erreur silencieuse : client non trouvé ou erreur de récupération
|
||||
client = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cas où contrat.client est déjà un objet (expand réussi mais pas dans @expand)
|
||||
if (!client && contrat.client && typeof contrat.client === 'object' && contrat.client.numClient) {
|
||||
client = contrat.client;
|
||||
}
|
||||
}
|
||||
const lastUser = parcours["@expand"]?.dernierUtilisateur;
|
||||
const region = lastUser?.["@expand"]?.region;
|
||||
const produit = contrat ? (contrat.produit || "NC") : "NC";
|
||||
|
||||
/**
|
||||
* Construction de la ligne DataTables
|
||||
* Ordre des colonnes : Numéro Parcours, Date Création, Matricule, Utilisateur, Région,
|
||||
* Numéro Saisine, Numéro Contrat, Produit, Type, Portefeuille, Intermédiaire,
|
||||
* Numéro Client, Nom Client, Bouton Reprendre, Bouton Générer
|
||||
*/
|
||||
rows.push([
|
||||
parcours.numParcours,
|
||||
fmtDateFR(parcours.created),
|
||||
lastUser?.matricule || "NC",
|
||||
lastUser ? `${lastUser.prenom} ${lastUser.nom}`.trim() || "NC" : "NC",
|
||||
region ? region.nom : "NC",
|
||||
contrat ? (contrat.numSaisine || "NC") : "NC",
|
||||
contrat ? (contrat.numContrat || "NC") : "NC",
|
||||
produit,
|
||||
contrat ? (contrat.type || "NC") : "NC",
|
||||
contrat ? (contrat["@expand"]?.intermediaire?.numPortefeuille || "NC") : "NC",
|
||||
contrat ? (contrat["@expand"]?.intermediaire?.nom || "NC") : "NC",
|
||||
client ? (client.numClient || "NC") : "NC",
|
||||
client ? (client.nom || "NC") : "NC",
|
||||
`<button type="button" id="btnReprendre" class="btn" onclick="window.location.href='${(!contrat || (!contrat.numSaisine && !contrat.numContrat)) ? `/contrat?numParcours=${parcours.numParcours}` : `/navParcours?numParcours=${parcours.numParcours}&submenu=client`}'"><i class="fas fa-arrow-right"></i></button>`,
|
||||
`<button type="button" id="btnGenerate" class="btn" data-produit="${produit}" data-num-parcours="${parcours.numParcours}" ${( !contrat || contrat.produit == "" || (contrat.fac == "" && contrat.rc == "" && contrat.tppc == "") || contrat.client == "" || contrat.intermediaire == "" ) ? 'disabled' : ''}><i class="fa-solid fa-file-arrow-down"></i></button>`
|
||||
]);
|
||||
} catch (err) {
|
||||
logger.log("error", `Erreur traitement parcours ${parcours?.numParcours || 'inconnu'}:`, err);
|
||||
// Ligne par défaut en cas d'erreur
|
||||
rows.push(["NC", "NC", "NC", "NC", "NC", "NC", "NC", "NC", "NC", "NC", "NC", "NC", "NC", "", ""]);
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
draw: Number(draw),
|
||||
recordsTotal: result.totalItems,
|
||||
recordsFiltered: result.totalItems,
|
||||
data: rows
|
||||
});
|
||||
} catch (error) {
|
||||
// Ignorer silencieusement les erreurs d'abort (requête annulée côté client)
|
||||
if (error && (error.name === "AbortError" || error.name === "DOMException" || error.message?.includes("aborted"))) {
|
||||
return;
|
||||
}
|
||||
// Pour toutes les autres erreurs, on les log et on renvoie une réponse d'erreur
|
||||
logger.log("error", "ça ne marche pas mais, aucune erreur" + error);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({ draw: 0, recordsTotal: 0, recordsFiltered: 0, data: [] });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* EXPORT CSV
|
||||
* Exporte l'historique des parcours au format CSV
|
||||
* Supporte l'export complet ou filtré selon les paramètres de la requête
|
||||
*/
|
||||
router.post("/export/csv", async (req, res) => {
|
||||
let aborted = false;
|
||||
req.on("aborted", () => {
|
||||
aborted = true;
|
||||
logger.log("warn", "Client a interrompu la connexion pendant l'export CSV");
|
||||
});
|
||||
res.on("finish", () => {
|
||||
logger.log("info", "Export CSV terminé");
|
||||
});
|
||||
|
||||
try {
|
||||
const {
|
||||
regions = [],
|
||||
search = { value: "" },
|
||||
columns = [],
|
||||
order = [],
|
||||
mode = "filtered",
|
||||
} = req.body || {};
|
||||
|
||||
const effective = (mode === "full")
|
||||
? { regions: [], search: { value: "" }, columns: [], order }
|
||||
: { regions, search, columns, order };
|
||||
|
||||
const { filter, sort } = buildPocketBaseFilterAndSort(effective);
|
||||
|
||||
res.setHeader("Content-Type", "text/csv; charset=utf-8");
|
||||
res.setHeader("Content-Disposition", `attachment; filename="historique_parcours.csv"`);
|
||||
|
||||
// BOM UTF-8 pour Excel
|
||||
res.write("\uFEFF");
|
||||
|
||||
const headers = [
|
||||
"Numéro du Parcours","Date de Création","Matricule","Dernier Utilisateur","Region",
|
||||
"Numéro Saisine","Numéro Contrat","Produit","Type","Numéro de Portefeuille",
|
||||
"Nom Intermediaire","Numéro de Client","Nom Client"
|
||||
];
|
||||
res.write(headers.join(";") + "\n");
|
||||
|
||||
/**
|
||||
* OPTIMISATION : getFullList pour récupérer tous les parcours en une requête
|
||||
* + batch client pour récupérer tous les clients manquants en une requête
|
||||
*/
|
||||
|
||||
// Construction du filtre régions (identique à getParcoursByRegionsPage)
|
||||
let regFilter = "";
|
||||
if (Array.isArray(effective.regions) && effective.regions.length > 0) {
|
||||
const ors = effective.regions.map(r => `dernierUtilisateur.region.nom = "${r}"`);
|
||||
regFilter = `(${ors.join(" || ")})`;
|
||||
}
|
||||
const finalFilter = [regFilter, filter].filter(Boolean).join(" && ");
|
||||
const expandFields = "contrat, contrat.client, contrat.intermediaire, dernierUtilisateur.region";
|
||||
|
||||
// Récupération de tous les parcours en une seule requête
|
||||
let allParcours;
|
||||
try {
|
||||
allParcours = await parcoursService.getParcoursFullList({
|
||||
filter: finalFilter,
|
||||
sort: sort || "-created",
|
||||
expand: expandFields,
|
||||
batch: 500,
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
logger.log("error", "Erreur récupération parcours pour export CSV:", err);
|
||||
if (!res.headersSent) {
|
||||
return res.status(500).send("Erreur lors de la récupération des données");
|
||||
}
|
||||
try { res.end(); } catch {}
|
||||
return;
|
||||
}
|
||||
|
||||
// Collecte des IDs clients manquants (l'expand contrat.client ne fonctionne pas en SDK 0.7 pocket)
|
||||
const missingClientIds = [];
|
||||
for (const parcours of allParcours) {
|
||||
const contrat = parcours["@expand"]?.contrat;
|
||||
if (contrat && contrat.client && !contrat["@expand"]?.client) {
|
||||
missingClientIds.push(contrat.client);
|
||||
}
|
||||
}
|
||||
|
||||
// Récupération batch de tous les clients manquants en une seule requête
|
||||
const clientsMap = await clientService.getClientsBatch(missingClientIds);
|
||||
|
||||
// Traitement des parcours
|
||||
for (const parcours of allParcours) {
|
||||
if (aborted) break;
|
||||
|
||||
const contrat = parcours["@expand"]?.contrat || null;
|
||||
const intermediaire = contrat ? (contrat["@expand"]?.intermediaire || null) : null;
|
||||
|
||||
// Client : d'abord depuis l'expand, sinon depuis le batch
|
||||
let client = contrat ? (contrat["@expand"]?.client || null) : null;
|
||||
if (!client && contrat && contrat.client) {
|
||||
client = clientsMap.get(contrat.client) || null;
|
||||
}
|
||||
|
||||
const lastUser = parcours["@expand"]?.dernierUtilisateur;
|
||||
const region = lastUser?.["@expand"]?.region;
|
||||
|
||||
const row = [
|
||||
parcours.numParcours,
|
||||
fmtDateFR(parcours.created),
|
||||
lastUser?.matricule || "NC",
|
||||
lastUser ? `${lastUser.prenom || ""} ${lastUser.nom || ""}`.trim() || "NC" : "NC",
|
||||
region ? (region.nom || "NC") : "NC",
|
||||
contrat ? (contrat.numSaisine || "NC") : "NC",
|
||||
contrat ? (contrat.numContrat || "NC") : "NC",
|
||||
contrat ? (contrat.produit || "NC") : "NC",
|
||||
contrat ? (contrat.type || "NC") : "NC",
|
||||
intermediaire ? (intermediaire.numPortefeuille || "NC") : "NC",
|
||||
intermediaire ? (intermediaire.nom || "NC") : "NC",
|
||||
client ? (client.numClient || "NC") : "NC",
|
||||
client ? (client.nom || "NC") : "NC",
|
||||
];
|
||||
|
||||
const safe = row.map(v => String(v).replaceAll(";", ",").replace(/\r?\n/g, " "));
|
||||
try {
|
||||
res.write(safe.join(";") + "\n");
|
||||
}
|
||||
catch (werr) {
|
||||
logger.log("error", werr);
|
||||
aborted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!aborted) {
|
||||
res.end();
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
logger.log("error", error);
|
||||
if (!res.headersSent) {
|
||||
return res.status(500).send("Erreur export CSV");
|
||||
}
|
||||
try { res.end(); } catch {}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// ====== UTILITAIRES XML/XLS ======
|
||||
|
||||
/**
|
||||
* EXPORT XLS (SpreadsheetML 2003)
|
||||
* Format XLS utilisé car XLSX est trop complexe à générer manuellement.
|
||||
* Le format XLS est toujours supporté par Excel sans perte de données.
|
||||
*/
|
||||
router.post("/export/xls", async (req, res) => {
|
||||
let aborted = false;
|
||||
req.on("aborted", () => {
|
||||
aborted = true;
|
||||
logger.log("warn", "Client a interrompu la connexion pendant l'export XLS");
|
||||
});
|
||||
res.on("finish", () => {
|
||||
logger.log("info", "Export XLS terminé");
|
||||
});
|
||||
|
||||
try {
|
||||
const {
|
||||
regions = [],
|
||||
search = { value: "" },
|
||||
columns = [],
|
||||
order = [],
|
||||
mode = "filtered"
|
||||
} = req.body || {};
|
||||
|
||||
const effective = (mode === "full")
|
||||
? { regions: [], search: { value: "" }, columns, order }
|
||||
: { regions, search, columns, order };
|
||||
|
||||
const { filter, sort } = buildPocketBaseFilterAndSort(effective);
|
||||
|
||||
const headers = [
|
||||
"Numéro du Parcours","Date de Création","Matricule","Dernier Utilisateur","Region",
|
||||
"Numéro Saisine","Numéro Contrat","Produit","Type","Numéro de Portefeuille",
|
||||
"Nom Intermediaire","Numéro de Client","Nom Client"
|
||||
];
|
||||
|
||||
/**
|
||||
* OPTIMISATION : getFullList pour récupérer tous les parcours en une requête
|
||||
* + batch client pour récupérer tous les clients manquants en une requête
|
||||
*/
|
||||
|
||||
// Construction du filtre régions (identique à getParcoursByRegionsPage)
|
||||
let regFilter = "";
|
||||
if (Array.isArray(effective.regions) && effective.regions.length > 0) {
|
||||
const ors = effective.regions.map(r => `dernierUtilisateur.region.nom = "${r}"`);
|
||||
regFilter = `(${ors.join(" || ")})`;
|
||||
}
|
||||
const finalFilter = [regFilter, filter].filter(Boolean).join(" && ");
|
||||
const expandFields = "contrat, contrat.client, contrat.intermediaire, dernierUtilisateur.region";
|
||||
|
||||
// Récupération de tous les parcours en une seule requête
|
||||
let allParcours;
|
||||
try {
|
||||
allParcours = await parcoursService.getParcoursFullList({
|
||||
filter: finalFilter,
|
||||
sort: sort || "-created",
|
||||
expand: expandFields,
|
||||
batch: 500,
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
logger.log("error", "Erreur récupération parcours pour export XLS:", err);
|
||||
if (!res.headersSent) {
|
||||
return res.status(500).send("Erreur lors de la récupération des données");
|
||||
}
|
||||
try { res.end(); } catch {}
|
||||
return;
|
||||
}
|
||||
|
||||
// Collecte des IDs clients manquants (l'expand contrat.client ne fonctionne pas en SDK 0.7.x Pocket)
|
||||
const missingClientIds = [];
|
||||
for (const parcours of allParcours) {
|
||||
const contrat = parcours["@expand"]?.contrat;
|
||||
if (contrat && contrat.client && !contrat["@expand"]?.client) {
|
||||
missingClientIds.push(contrat.client);
|
||||
}
|
||||
}
|
||||
|
||||
// Récupération batch de tous les clients manquants en une seule requête
|
||||
const clientsMap = await clientService.getClientsBatch(missingClientIds);
|
||||
|
||||
const fileName = (mode === "full")
|
||||
? "historique_parcours_complet.xls"
|
||||
: "historique_parcours_filtre.xls";
|
||||
|
||||
res.setHeader("Content-Type", "application/vnd.ms-excel; charset=utf-8");
|
||||
res.setHeader("Content-Disposition", `attachment; filename="${fileName}"`);
|
||||
|
||||
// En-tête SpreadsheetML 2003
|
||||
res.write(
|
||||
`<?xml version="1.0"?>
|
||||
<?mso-application progid="Excel.Sheet"?>
|
||||
<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet"
|
||||
xmlns:o="urn:schemas-microsoft-com:office:office"
|
||||
xmlns:x="urn:schemas-microsoft-com:office:excel"
|
||||
xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet"
|
||||
xmlns:html="http://www.w3.org/TR/REC-html40">
|
||||
<Styles>
|
||||
<Style ss:ID="Default" ss:Name="Normal">
|
||||
<Alignment ss:Vertical="Center"/>
|
||||
<Borders/>
|
||||
<Font ss:FontName="Calibri" ss:Size="11"/>
|
||||
<Interior/>
|
||||
<NumberFormat/>
|
||||
<Protection/>
|
||||
</Style>
|
||||
<Style ss:ID="Header">
|
||||
<Font ss:Bold="1"/>
|
||||
<Alignment ss:Horizontal="Center" ss:Vertical="Center"/>
|
||||
</Style>
|
||||
</Styles>
|
||||
<Worksheet ss:Name="Historique">
|
||||
<Table>
|
||||
`
|
||||
);
|
||||
|
||||
res.write(
|
||||
`<Row ss:StyleID="Header">` +
|
||||
headers.map(h => `<Cell><Data ss:Type="String">${xmlEsc(h)}</Data></Cell>`).join("") +
|
||||
`</Row>\n`
|
||||
);
|
||||
|
||||
// Traitement des parcours
|
||||
for (const parcours of allParcours) {
|
||||
const contrat = parcours["@expand"]?.contrat || null;
|
||||
const intermediaire = contrat ? (contrat["@expand"]?.intermediaire || null) : null;
|
||||
|
||||
// Client : d'abord depuis l'expand, sinon depuis le batch
|
||||
let client = contrat ? (contrat["@expand"]?.client || null) : null;
|
||||
if (!client && contrat && contrat.client) {
|
||||
client = clientsMap.get(contrat.client) || null;
|
||||
}
|
||||
|
||||
const lastUser = parcours["@expand"]?.dernierUtilisateur;
|
||||
const region = lastUser?.["@expand"]?.region;
|
||||
|
||||
const row = [
|
||||
parcours.numParcours,
|
||||
fmtDateFR(parcours.created),
|
||||
lastUser?.matricule || "NC",
|
||||
lastUser ? `${lastUser.prenom || ""} ${lastUser.nom || ""}`.trim() || "NC" : "NC",
|
||||
region ? (region.nom || "NC") : "NC",
|
||||
contrat ? (contrat.numSaisine || "NC") : "NC",
|
||||
contrat ? (contrat.numContrat || "NC") : "NC",
|
||||
contrat ? (contrat.produit || "NC") : "NC",
|
||||
contrat ? (contrat.type || "NC") : "NC",
|
||||
intermediaire ? (intermediaire.numPortefeuille || "NC") : "NC",
|
||||
intermediaire ? (intermediaire.nom || "NC") : "NC",
|
||||
client ? (client.numClient || "NC") : "NC",
|
||||
client ? (client.nom || "NC") : "NC",
|
||||
].map(v => String(v).replace(/\r?\n/g, " "));
|
||||
|
||||
res.write(rowXml(row) + "\n");
|
||||
}
|
||||
|
||||
// Fermeture du fichier XML SpreadsheetML
|
||||
res.write(
|
||||
` </Table>
|
||||
</Worksheet>
|
||||
</Workbook>`
|
||||
);
|
||||
res.end();
|
||||
|
||||
}
|
||||
catch (error) {
|
||||
logger.log("error", error);
|
||||
if (!res.headersSent) return res.status(500).send("Erreur export XLS");
|
||||
try { res.end(); } catch {}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Route GET /details/:numParcours
|
||||
* Récupère les détails complets d'un parcours (parcours + contrat + fiche produit)
|
||||
* Utilisé pour afficher le panneau de détails dans la datatable
|
||||
*/
|
||||
router.get("/details/:numParcours", async (req, res) => {
|
||||
try {
|
||||
const { numParcours } = req.params;
|
||||
const parcours = await parcoursService.getDeepDetailsByNumParcours(numParcours);
|
||||
if (!parcours) return res.json({ valid: false, error: "Parcours introuvable" });
|
||||
|
||||
/**
|
||||
* Extraction des données pour faciliter l'accès côté frontend
|
||||
*/
|
||||
const contrat = parcours?.["@expand"]?.contrat || null;
|
||||
const produit = (contrat?.produit || "").toUpperCase();
|
||||
|
||||
/**
|
||||
* Récupération de la fiche produit selon le type (TPPC, RC, FAC)
|
||||
* Si l'expand n'a pas fonctionné, on essaie de récupérer directement via l'ID
|
||||
*/
|
||||
let produitRecord = null;
|
||||
if (produit === "TPPC") {
|
||||
produitRecord = contrat?.["@expand"]?.tppc || null;
|
||||
}
|
||||
else if (produit === "RC") {
|
||||
produitRecord = contrat?.["@expand"]?.rc || null;
|
||||
// Si l'expand n'a pas fonctionné mais qu'on a l'ID, on récupère directement
|
||||
if (!produitRecord && contrat?.rc) {
|
||||
try {
|
||||
const rcId = typeof contrat.rc === 'string' ? contrat.rc : contrat.rc?.id || contrat.rc;
|
||||
if (rcId) {
|
||||
const rcService = require("../services/rcService");
|
||||
const rcData = await rcService.getRCbyId(rcId);
|
||||
// fetchInfoByCriteria retourne directement l'item, pas un objet avec items
|
||||
produitRecord = rcData || null;
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
logger.log("info", `Erreur récupération RC directe pour ${numParcours}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (produit === "FAC") {
|
||||
produitRecord = contrat?.["@expand"]?.fac || null;
|
||||
// Si l'expand n'a pas fonctionné mais qu'on a l'ID, on récupère directement
|
||||
if (!produitRecord && contrat?.fac) {
|
||||
try {
|
||||
const facId = typeof contrat.fac === 'string' ? contrat.fac : contrat.fac?.id || contrat.fac;
|
||||
if (facId) {
|
||||
const facService = require("../services/facService");
|
||||
const facData = await facService.getFACbyId(facId);
|
||||
// fetchInfoByCriteria retourne directement l'item, pas un objet avec items
|
||||
produitRecord = facData || null;
|
||||
if (!produitRecord) {
|
||||
logger.log("warn", `FAC non trouvé pour ID ${facId} (parcours ${numParcours})`);
|
||||
}
|
||||
} else {
|
||||
logger.log("warn", `Pas d'ID FAC dans contrat pour parcours ${numParcours}`);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
logger.log("warn", `Erreur récupération FAC directe pour ${numParcours}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
else if (!produitRecord && !contrat?.fac) {
|
||||
logger.log("warn", `Contrat FAC sans relation FAC pour parcours ${numParcours}`);
|
||||
}
|
||||
}
|
||||
|
||||
return res.json({
|
||||
valid: true,
|
||||
produit,
|
||||
parcours,
|
||||
contrat,
|
||||
produitRecord,
|
||||
});
|
||||
}
|
||||
catch (e) {
|
||||
logger.log("error", e);
|
||||
return res.status(500).json({ valid: false, error: "Erreur recherche détails" });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
module.exports = router;
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
Pocketbase_0.7.5.exe serve --http="127.0.0.1:8091"
|
||||
pause
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
const { db } = require('../db/db-connect');
|
||||
const logger = require('../utils/logger');
|
||||
const globalService = require("../services/globalService");
|
||||
|
||||
async function createClient() {
|
||||
return await db.records.create('client', {});
|
||||
}
|
||||
|
||||
async function getClient(id) {
|
||||
const criteria = {
|
||||
filter: `id='${id}'`,
|
||||
};
|
||||
|
||||
return globalService.fetchInfoByCriteria("client", criteria);
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère plusieurs clients en plusieurs requêtes batch (optimisation pour exports)
|
||||
* Découpe en chunks de 50 IDs pour éviter les filtres trop longs
|
||||
* avec getFullList(collection, batchSize, options)
|
||||
* @param {string[]} clientIds - Tableau d'IDs de clients
|
||||
* @returns {Map<string, Object>} - Map des clients par ID
|
||||
*/
|
||||
async function getClientsBatch(clientIds) {
|
||||
const clientMap = new Map();
|
||||
if (!clientIds || clientIds.length === 0) return clientMap;
|
||||
|
||||
// Filtrer les IDs valides et uniques
|
||||
const uniqueIds = [...new Set(clientIds.filter(id => id && typeof id === 'string'))];
|
||||
if (uniqueIds.length === 0) return clientMap;
|
||||
|
||||
// Découper en chunks de 50 pour éviter les filtres trop longs
|
||||
const CHUNK_SIZE = 50;
|
||||
const chunks = [];
|
||||
for (let i = 0; i < uniqueIds.length; i += CHUNK_SIZE) {
|
||||
chunks.push(uniqueIds.slice(i, i + CHUNK_SIZE));
|
||||
}
|
||||
|
||||
// Traiter chaque chunk
|
||||
for (const chunk of chunks) {
|
||||
try {
|
||||
// Construire le filtre OR pour ce chunk
|
||||
const filter = chunk.map(id => `id = "${id}"`).join(" || ");
|
||||
|
||||
// getFullList(collection, batchSize, options)
|
||||
const clients = await db.records.getFullList("client", 500, {
|
||||
filter: filter,
|
||||
});
|
||||
|
||||
// Ajouter à la map
|
||||
clients.forEach(client => {
|
||||
if (client && client.id) {
|
||||
clientMap.set(client.id, client);
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
logger.log("warn", `Erreur récupération clients chunk (${chunk.length} IDs):`, err?.message || String(err));
|
||||
}
|
||||
}
|
||||
|
||||
return clientMap;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createClient,
|
||||
getClient,
|
||||
getClientsBatch,
|
||||
};
|
||||
|
|
@ -1,146 +0,0 @@
|
|||
const { db } = require("../db/db-connect");
|
||||
const logger = require("../utils/logger");
|
||||
const numeral = require("numeral");
|
||||
|
||||
require("numeral/locales/fr");
|
||||
numeral.locale("fr");
|
||||
|
||||
async function getRecordIdFromFieldValue(collection, field, value) {
|
||||
try {
|
||||
const resultList = await db.records.getList(collection, 1, 1, {
|
||||
filter: `${field} = '${value}'`,
|
||||
});
|
||||
|
||||
if (resultList.totalItems > 0) {
|
||||
return resultList.items[0].id;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.log("error", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchInfoByCriteria(collection, criteria) {
|
||||
try {
|
||||
const resultList = await db.records.getList(collection, 1, 1, criteria);
|
||||
|
||||
if (resultList.totalItems > 0) {
|
||||
return resultList.items[0];
|
||||
}
|
||||
} catch (error) {
|
||||
/**
|
||||
* Gestion silencieuse des erreurs d'abort (requêtes interrompues)
|
||||
* Ces erreurs sont normales lors de requêtes parallèles et ne doivent pas être loggées
|
||||
*/
|
||||
if (error?.isAbort || error?.name?.includes("Abort") || error?.status === 0) {
|
||||
return null;
|
||||
}
|
||||
// Autres erreurs loggées en info (pas en error) pour éviter le bruit dans les logs
|
||||
logger.log("info", `Erreur récupération ${collection}:`, error?.message || error);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function updateRecordFromData(collection, recordID, data) {
|
||||
try {
|
||||
const record = await db.records.update(collection, recordID, data);
|
||||
|
||||
return record;
|
||||
} catch (error) {
|
||||
logger.log("error", error);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function customFormatNumber(number, decimal, taux=false) {
|
||||
if (Number.isInteger(number)) {
|
||||
if (decimal == true) {
|
||||
return numeral(number).format("0,0.00");
|
||||
} else {
|
||||
return numeral(number).format("0,0");
|
||||
}
|
||||
} else {
|
||||
if (taux == true) {
|
||||
return numeral(number).format("0,0.000");
|
||||
} else {
|
||||
return numeral(number).format("0,0.00");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function cleanDoubleSpaces(inputString) {
|
||||
return inputString.replace(/\s{2,}/g, " ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Formate une date ISO en format français (jj/mm/aaaa)
|
||||
* @param {string|Date} iso - Date au format ISO
|
||||
* @param {boolean} withTime - Inclure l'heure (défaut: false)
|
||||
* @returns {string} Date formatée (dd/mm/yyyy ou dd/mm/yyyy hh:mm)
|
||||
*/
|
||||
function fmtDateFR(iso, withTime = false) {
|
||||
if (!iso) return "NC";
|
||||
|
||||
const d = new Date(iso);
|
||||
if (isNaN(d.getTime())) return "NC";
|
||||
|
||||
const dd = String(d.getDate()).padStart(2, "0");
|
||||
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
||||
const yyyy = d.getFullYear();
|
||||
|
||||
if (!withTime) {
|
||||
return `${dd}/${mm}/${yyyy}`;
|
||||
}
|
||||
|
||||
const hh = String(d.getHours()).padStart(2, "0");
|
||||
const mi = String(d.getMinutes()).padStart(2, "0");
|
||||
return `${dd}/${mm}/${yyyy} ${hh}:${mi}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Échappe les caractères spéciaux XML
|
||||
* @param {string} s - Chaîne à échapper
|
||||
* @returns {string} Chaîne échappée
|
||||
*/
|
||||
function xmlEsc(s) {
|
||||
return String(s ?? "")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère une cellule XML pour Excel
|
||||
* @param {string} v - Valeur de la cellule
|
||||
* @returns {string} Cellule XML formatée
|
||||
*/
|
||||
function cellXml(v) {
|
||||
return `<Cell><Data ss:Type="String">${xmlEsc(v)}</Data></Cell>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère une ligne XML pour Excel
|
||||
* @param {Array<string>} cells - Tableau de valeurs pour les cellules
|
||||
* @returns {string} Ligne XML formatée
|
||||
*/
|
||||
function rowXml(cells) {
|
||||
return `<Row>${cells.map(cellXml).join("")}</Row>`;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getRecordIdFromFieldValue,
|
||||
fetchInfoByCriteria,
|
||||
updateRecordFromData,
|
||||
cleanDoubleSpaces,
|
||||
customFormatNumber,
|
||||
fmtDateFR,
|
||||
xmlEsc,
|
||||
cellXml,
|
||||
rowXml,
|
||||
};
|
||||
|
|
@ -1,278 +0,0 @@
|
|||
// services/parcoursService.js
|
||||
const { db } = require("../db/db-connect");
|
||||
const logger = require("../utils/logger");
|
||||
const globalService = require("../services/globalService");
|
||||
|
||||
/**
|
||||
* Récupère un parcours par son numéro (avec expand utiles)
|
||||
*/
|
||||
async function getParcoursByNumParcours(numParcours) {
|
||||
const criteria = {
|
||||
filter: `numParcours='${numParcours}'`,
|
||||
expand: [
|
||||
"dernierUtilisateur.region",
|
||||
"contrat",
|
||||
"contrat.client",
|
||||
"contrat.intermediaire"
|
||||
].join(",")
|
||||
};
|
||||
return globalService.fetchInfoByCriteria("parcours", criteria);
|
||||
}
|
||||
|
||||
/**
|
||||
* Full list (batch côté PocketBase). | Fetch l'ensemble de la BD via chunk "batch"
|
||||
* avec getFullList(collection, batchSize, options)
|
||||
*/
|
||||
async function getParcoursFullList({ filter, sort, expand, fields, batch = 500 }) {
|
||||
const options = {
|
||||
sort: sort || "-created",
|
||||
};
|
||||
|
||||
// Ajouter expand si défini
|
||||
if (expand) {
|
||||
options.expand = expand;
|
||||
}
|
||||
|
||||
// Ajouter fields si défini
|
||||
if (fields) {
|
||||
options.fields = fields;
|
||||
}
|
||||
|
||||
// Ajouter filter SEULEMENT s'il n'est pas vide (Pocketbase 0.7 rejette les filtres vides)
|
||||
if (filter && filter.trim() !== "") {
|
||||
options.filter = filter;
|
||||
}
|
||||
|
||||
// getFullList(collection, batchSize, options)
|
||||
return db.records.getFullList("parcours", batch, options);
|
||||
}
|
||||
/**
|
||||
* Pagination multi-régions + filtres/tri optionnels (server-side DataTables)
|
||||
* – Parcours une seule fois db par requête
|
||||
* @param {string[]} regions
|
||||
* @param {number} page
|
||||
* @param {number} perPage
|
||||
* @param {{filter?: string, sort?: string}} opts
|
||||
*/
|
||||
async function getParcoursByRegionsPage(regions = [], page = 1, perPage = 10, opts = {}) {
|
||||
try {
|
||||
let regFilter = "";
|
||||
if (Array.isArray(regions) && regions.length > 0) {
|
||||
const ors = regions.map(r => `dernierUtilisateur.region.nom = "${r}"`);
|
||||
regFilter = `(${ors.join(" || ")})`;
|
||||
}
|
||||
|
||||
const filter = [regFilter, opts.filter].filter(Boolean).join(" && ");
|
||||
|
||||
/**
|
||||
* Récupération des parcours avec expands nécessaires
|
||||
* Note: L'expand de contrat.client ne fonctionne pas toujours,
|
||||
* d'où la nécessité d'un fallback dans le contrôleur
|
||||
*/
|
||||
const list = await db.records.getList("parcours", page, perPage, {
|
||||
sort: opts.sort || "-created",
|
||||
filter: filter || "",
|
||||
expand: [
|
||||
"contrat",
|
||||
"contrat.client",
|
||||
"contrat.intermediaire",
|
||||
"dernierUtilisateur.region"
|
||||
].join(","),
|
||||
});
|
||||
|
||||
return {
|
||||
page: list.page,
|
||||
perPage: list.perPage,
|
||||
totalItems: list.totalItems,
|
||||
totalPages: list.totalPages,
|
||||
items: list.items,
|
||||
};
|
||||
}
|
||||
catch (error) {
|
||||
logger.log('error', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Création d'un parcours vide
|
||||
*/
|
||||
async function createNewEmptyParcours(numParcours) {
|
||||
try {
|
||||
const data = { ["numParcours"]: numParcours };
|
||||
const record = await db.records.create("parcours", data);
|
||||
if (record) {
|
||||
return record.id;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
logger.log("error", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* MAJ d'un champ d'un parcours
|
||||
*/
|
||||
async function updateFieldValueParcours(id, field, value) {
|
||||
try {
|
||||
const data = { [field]: value };
|
||||
const record = await db.records.update("parcours", id, data);
|
||||
if (record) {
|
||||
return record.id;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
logger.log("error", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère le prochain numéro de parcours
|
||||
*/
|
||||
async function getNewParcoursNumber() {
|
||||
try {
|
||||
const list = await db.records.getList("parcours", 1, 1, { sort: "-numParcours" });
|
||||
const last = list?.items?.[0];
|
||||
if (!last?.numParcours) return null;
|
||||
|
||||
const numericValue = parseInt(String(last.numParcours).substring(1), 10);
|
||||
if (Number.isNaN(numericValue)) return null;
|
||||
|
||||
const next = numericValue + 1;
|
||||
return "P" + next.toString().padStart(9, "0");
|
||||
}
|
||||
catch (error) {
|
||||
logger.log("error", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Section détails profonds (contrat + fiche produit) --- //
|
||||
|
||||
/**
|
||||
* Récupère un parcours (via numParcours) avec les expands utiles pour détails.
|
||||
*/
|
||||
async function getParcoursForDetails(numParcours) {
|
||||
try {
|
||||
const list = await db.records.getList("parcours", 1, 1, {
|
||||
filter: `numParcours='${numParcours}'`,
|
||||
expand: [
|
||||
"contrat",
|
||||
"contrat.client",
|
||||
"contrat.intermediaire",
|
||||
"dernierUtilisateur.region"
|
||||
].join(","),
|
||||
});
|
||||
return list?.items?.[0] || null;
|
||||
}
|
||||
catch (e) {
|
||||
logger.log("error", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mappe un libellé produit vers la collection PocketBase (à ajuster si changement de parcours).
|
||||
*/
|
||||
function mapProduitToCollection(produitRaw = "") {
|
||||
const p = String(produitRaw || "").trim().toUpperCase();
|
||||
const map = {
|
||||
"TPPC": "tppc",
|
||||
"RC": "rc",
|
||||
"FAC": "fac",
|
||||
};
|
||||
return map[p] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère la fiche produit pour un contrat donné.
|
||||
* On tente d'abord par relation "contrat = contratId" si elle existe,
|
||||
* sinon fallback par "numContrat = x" si jamais la fiche stocke le numéro.
|
||||
*/
|
||||
async function getProduitRecordForContrat(contrat, opts = {}) {
|
||||
try {
|
||||
if (!contrat) return null;
|
||||
const collection = mapProduitToCollection(contrat.produit);
|
||||
if (!collection) return null;
|
||||
|
||||
// Tente via une relation directe "contrat" (champ le plus propre)
|
||||
try {
|
||||
const record = await db.records.getFirstListItem(collection, `contrat='${contrat.id}'`, {
|
||||
});
|
||||
if (record) return record;
|
||||
}
|
||||
catch (_) { /* ignore, on tente le fallback */ }
|
||||
|
||||
// Fallback
|
||||
if (contrat.numContrat) {
|
||||
try {
|
||||
const record = await db.records.getFirstListItem(collection, `numContrat='${contrat.numContrat}'`, {});
|
||||
if (record) return record;
|
||||
}
|
||||
catch (_) { /* ignore */ }
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (e) {
|
||||
logger.log("error", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* reformatage texte - a virer
|
||||
*/
|
||||
function escPB(s = "") {
|
||||
return String(s).replace(/"/g, '\\"');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Détails complets: parcours + contrat + fiche produit
|
||||
*/
|
||||
async function getDeepDetailsByNumParcours(numParcours) {
|
||||
try {
|
||||
const filter = `numParcours = "${escPB(numParcours)}"`;
|
||||
|
||||
const list = await db.records.getList("parcours", 1, 1, {
|
||||
filter,
|
||||
expand: [
|
||||
"contrat",
|
||||
"contrat.client",
|
||||
"contrat.intermediaire",
|
||||
"dernierUtilisateur.region",
|
||||
// produit lié
|
||||
"contrat.tppc",
|
||||
"contrat.rc",
|
||||
"contrat.fac",
|
||||
// sous-relations TPPC
|
||||
"contrat.tppc.tarif",
|
||||
"contrat.tppc.projet",
|
||||
].join(","),
|
||||
});
|
||||
|
||||
return list?.items?.[0] || null;
|
||||
} catch (e) {
|
||||
logger.log("error", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getNewParcoursNumber,
|
||||
getParcoursByNumParcours,
|
||||
createNewEmptyParcours,
|
||||
updateFieldValueParcours,
|
||||
getParcoursByRegionsPage,
|
||||
getParcoursFullList,
|
||||
getParcoursForDetails,
|
||||
getProduitRecordForContrat,
|
||||
getDeepDetailsByNumParcours,
|
||||
mapProduitToCollection,
|
||||
};
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
const { db } = require('../db/db-connect');
|
||||
const logger = require('../utils/logger');
|
||||
const globalService = require("../services/globalService");
|
||||
|
||||
async function createRc(data) {
|
||||
return await db.records.create('rc', data);
|
||||
}
|
||||
|
||||
async function getRCbyId(id) {
|
||||
const criteria = {
|
||||
filter: `id='${id}'`
|
||||
};
|
||||
return globalService.fetchInfoByCriteria("rc", criteria);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createRc,
|
||||
getRCbyId
|
||||
};
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="Outil souscripteur Transport">
|
||||
<title>EasyTransport</title>
|
||||
|
||||
<!-- Variable pour complément spécifique HEAD -->
|
||||
<%- typeof extraHeadContent !=='undefined' ? extraHeadContent : '' %>
|
||||
|
||||
<!-- Init CSS -->
|
||||
<!-- Favicon: icône affichée dans l'onglet du navigateur -->
|
||||
<link rel="icon" href="/images/favicon-axa.ico">
|
||||
<!-- Import de la police Roboto avec différents poids pour une utilisation dans le site -->
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap">
|
||||
<!-- Icônes Material Design : Bibliothèque d'icônes pour utilisation dans le site -->
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
<!-- Normalisation du CSS : élimine les différences de style par défaut entre les navigateurs -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css">
|
||||
<!-- Materialize CSS : Bibliothèque CSS pour des éléments de conception matérialisée -->
|
||||
<link rel="stylesheet" href="/materialize/css/materialize.min.css">
|
||||
<!-- Feuille de style globale : Styles CSS personnalisés pour ce projet -->
|
||||
<link rel="stylesheet" href="/css/global.css">
|
||||
<!-- DataTables CSS -->
|
||||
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/2.0.8/css/dataTables.dataTables.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<main>
|
||||
|
||||
<%- include('partials/navbar') %>
|
||||
|
||||
<div class="container">
|
||||
<%- typeof body !=='undefined' ? body : '' %>
|
||||
</div>
|
||||
|
||||
<!-- Script de Materialize -->
|
||||
<script src="/materialize/js/materialize.min.js"></script>
|
||||
<!-- Script de decoding de token -->
|
||||
<script src="/js/jwt-decode.js"></script>
|
||||
<!-- Feuille de script globale : JS personnalisés pour ce projet -->
|
||||
<script src="/js/global.js"></script>
|
||||
<!-- Feuille de script pour les contrôle de saisie formulaire : JS personnalisés pour ce projet -->
|
||||
<script src="/js/verif-form.js"></script>
|
||||
<!-- Feuille de script pour les donnée static JSON de saisie formulaire -->
|
||||
<script src="/js/json/json-verif-form.js"></script>
|
||||
|
||||
</main>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -6,8 +6,7 @@
|
|||
"scripts": {
|
||||
"start": "nodemon ./src/server.js",
|
||||
"build": "pkg ./src/server.js -o EasyTransport",
|
||||
"test": "jest",
|
||||
"db" : "cd ./src/db && start cmd /c Lancement_Pocketbase.cmd"
|
||||
"test": "jest"
|
||||
},
|
||||
"pkg": {
|
||||
"assets": [
|
||||
|
|
@ -102,6 +102,13 @@ hr.form {
|
|||
font-size: smaller;
|
||||
}
|
||||
|
||||
.helper-text.error {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.mrg {
|
||||
padding: 0 5% !important;
|
||||
}
|
||||
|
|
@ -444,4 +451,64 @@ a.grille-garanties:hover{
|
|||
.brand-logo img {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Overlay loader */
|
||||
#loader-overlay {
|
||||
position: fixed;
|
||||
top: 0; left: 0;
|
||||
width: 100%; height: 100%;
|
||||
background: linear-gradient(
|
||||
rgba(10, 20, 60, 0.2),
|
||||
rgba(0, 0, 0, 0.4)
|
||||
);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
opacity: 0;
|
||||
backdrop-filter: blur(0px);
|
||||
pointer-events: none;
|
||||
transition: opacity 0.5s ease, backdrop-filter 0.5s ease;
|
||||
}
|
||||
|
||||
#loader-overlay.active {
|
||||
opacity: 1;
|
||||
backdrop-filter: blur(3px);
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
#loader-overlay.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.loader-spin-wrap {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
transition: opacity 0.5s ease, transform 0.5s ease;
|
||||
transition-delay: 0.5s;
|
||||
}
|
||||
|
||||
#loader-overlay.active .loader-spin-wrap {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Spinner circulaire */
|
||||
.loader-spin {
|
||||
width: 50px;
|
||||
aspect-ratio: 1;
|
||||
border-radius: 50%;
|
||||
mask:1;
|
||||
background:
|
||||
radial-gradient(farthest-side,darkblue 94%,transparent) top/8px 8px no-repeat,
|
||||
conic-gradient(transparent 30%,darkblue);
|
||||
-webkit-mask: radial-gradient(farthest-side,transparent calc(100% - 8px),#000 0);
|
||||
animation: l13 1s infinite linear;
|
||||
}
|
||||
|
||||
@keyframes l13 {
|
||||
100% { transform: rotate(1turn); }
|
||||
}
|
||||
|
|
@ -0,0 +1,208 @@
|
|||
/* Historique Parcours */
|
||||
body>main>div>div.section.center-align {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
body>main>div>div.section.center-align>div.container {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
#historiqueParcours_wrapper {
|
||||
width: 100% !important;
|
||||
overflow-x: auto !important;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
table#historiqueParcours {
|
||||
width: 150%;
|
||||
border-collapse: collapse !important;
|
||||
border: 2px solid darkblue !important;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
#historiqueParcours th,
|
||||
#historiqueParcours td {
|
||||
color: black;
|
||||
text-align: left;
|
||||
font-size: 11px;
|
||||
max-width: 100px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
table.dataTable thead th {
|
||||
padding-right: 18px !important;
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
/* Div pour encapsuler le texte dans les cellules d'en-tête */
|
||||
table.dataTable thead th>div {
|
||||
position: absolute !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
}
|
||||
|
||||
#historiqueParcours_filter label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dataTables_wrapper .dataTables_filter input[type="search"] {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-bottom: 1px solid #26a69a;
|
||||
border-radius: 0;
|
||||
outline: none;
|
||||
height: 3rem;
|
||||
width: 100%;
|
||||
font-size: 16px;
|
||||
margin: 0 0 8px 0;
|
||||
padding: 0;
|
||||
box-shadow: none;
|
||||
box-sizing: content-box;
|
||||
transition: box-shadow .3s, border .3s, -webkit-box-shadow .3s;
|
||||
}
|
||||
|
||||
#historiqueParcours_length>label {
|
||||
font-size: 14px;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
.dataTables_length select[name="historiqueParcours_length"] {
|
||||
display: block !important;
|
||||
font-size: 14px;
|
||||
color: #555;
|
||||
padding: 8px;
|
||||
margin: 0 0.5em;
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 5px;
|
||||
font-size: 16px;
|
||||
outline: none;
|
||||
width: 60px;
|
||||
}
|
||||
|
||||
/* Style Input search by row */
|
||||
#historiqueParcours>thead>tr:nth-child(2)>th>input {
|
||||
font-size: 13px !important;
|
||||
padding: 6px !important;
|
||||
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* icone de tri sur les colonnes */
|
||||
table.dataTable thead .sorting,
|
||||
table.dataTable thead .sorting_asc,
|
||||
table.dataTable thead .sorting_desc {
|
||||
background-position: right center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
/* Ajouter un espacement entre le texte et l'icône */
|
||||
table.dataTable thead .sorting:before,
|
||||
table.dataTable thead .sorting_asc:before,
|
||||
table.dataTable thead .sorting_desc:before {
|
||||
content: "";
|
||||
}
|
||||
|
||||
/* boutons de navigationw */
|
||||
.dataTables_wrapper .dataTables_paginate .paginate_button {
|
||||
background-color: white !important;
|
||||
border: darkblue solid 1.5px !important;
|
||||
color: black !important;
|
||||
padding: 6px 12px !important;
|
||||
margin: 0 2px !important;
|
||||
cursor: pointer !important;
|
||||
border-radius: 4px !important;
|
||||
transition: background-color 0.3s, color 0.3s, border-color 0.3s !important;
|
||||
}
|
||||
|
||||
#historiqueParcours_paginate>span>a.paginate_button.current,
|
||||
.dataTables_wrapper .dataTables_paginate:hover .paginate_button:hover {
|
||||
background: none !important;
|
||||
background-color: darkblue !important;
|
||||
border-color: white !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
/* NC value */
|
||||
td.nc-value {
|
||||
color: lightgray !important;
|
||||
}
|
||||
|
||||
/* Les bouton pour le filtres et les extraction */
|
||||
#divBtnFilter {
|
||||
display: inline-grid;
|
||||
width: 100%;
|
||||
grid-template-columns: auto;
|
||||
column-gap: 3%;
|
||||
row-gap: 10%;
|
||||
}
|
||||
|
||||
#checkRegionAdmin {
|
||||
grid-column: 1 / span 3;
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
#divToggleSearch {
|
||||
width: 300px;
|
||||
grid-column: 2;
|
||||
grid-row: 3;
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
#divExtractAll {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
#divExtractFilter {
|
||||
grid-column: 3;
|
||||
grid-row: 1;
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
#divBtnFilter button {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(16, 0, 75, 0.2),
|
||||
0 4px 8px rgba(16, 0, 75, 0.1);
|
||||
}
|
||||
|
||||
/* Bouton Reprendre */
|
||||
#btnReprendre,
|
||||
#btnGenerate {
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 0px 15px;
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
#btnReprendre i {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* checkbox Filter Region Admin */
|
||||
#checkRegionAdmin {
|
||||
border: 1px solid #ccc;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 10px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
[class^="checkbox-wrapper-"] {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
#checkRegionAdmin input[type="checkbox"] {
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#checkRegionAdmin label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
/* Overlay loader (css pris du site https://css-loaders.com/)*/
|
||||
#loader-overlay {
|
||||
position: fixed;
|
||||
top: 0; left: 0;
|
||||
width: 100%; height: 100%;
|
||||
background: linear-gradient(
|
||||
rgba(10, 20, 60, 0.2),
|
||||
rgba(0, 0, 0, 0.4)
|
||||
);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
opacity: 0;
|
||||
backdrop-filter: blur(0px);
|
||||
pointer-events: none;
|
||||
transition: opacity 0.5s ease, backdrop-filter 0.5s ease;
|
||||
}
|
||||
#loader-overlay.active {
|
||||
opacity: 1;
|
||||
backdrop-filter: blur(3px);
|
||||
pointer-events: all;
|
||||
}
|
||||
#loader-overlay.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Spinner wrapper (fade/slide) */
|
||||
.loader-spin-wrap {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
transition: opacity 0.5s ease, transform 0.5s ease;
|
||||
transition-delay: 0.5s; /* apparaît après 0.5s */
|
||||
}
|
||||
#loader-overlay.active .loader-spin-wrap {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Spinner circulaire */
|
||||
.loader-spin {
|
||||
width: 50px;
|
||||
aspect-ratio: 1;
|
||||
border-radius: 50%;
|
||||
mask:1;
|
||||
background:
|
||||
radial-gradient(farthest-side,darkblue 94%,transparent) top/8px 8px no-repeat,
|
||||
conic-gradient(transparent 30%,darkblue);
|
||||
-webkit-mask: radial-gradient(farthest-side,transparent calc(100% - 8px),#000 0);
|
||||
animation: l13 1s infinite linear;
|
||||
}
|
||||
@keyframes l13 {
|
||||
100% { transform: rotate(1turn); }
|
||||
}
|
||||
|
||||
/* Erreur */
|
||||
#error-message {
|
||||
display: none;
|
||||
color: red;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
white-space: pre-line;
|
||||
}
|
||||