diff --git a/ecole/public/css/global.css b/ecole/public/css/global.css
index af6c9276..eb8fe6fb 100644
--- a/ecole/public/css/global.css
+++ b/ecole/public/css/global.css
@@ -240,6 +240,63 @@ hr.form {
background: #26a69a;
}
+#advaloNavSelect .active a {
+ background: #26a69a;
+}
+
+#advaloNavSelect li a {
+ font-weight: 500;
+ letter-spacing: 0.2px;
+}
+
+.advalo-panel .input-field > label {
+ color: #1a237e !important;
+ transition: transform .18s ease, color .18s ease, font-size .18s ease;
+}
+
+.advalo-panel .input-field > label.active {
+ transform: translateY(-24px) scale(0.82) !important;
+ transform-origin: 0 0;
+}
+
+.advalo-loader-wrap {
+ min-height: 46px;
+}
+
+.advalo-ring-loader {
+ width: 34px;
+ height: 34px;
+ border: 3px solid rgba(0, 0, 139, 0.18);
+ border-top-color: #00008b;
+ border-right-color: #26a69a;
+ border-radius: 50%;
+ margin: 0 auto;
+ animation: advalo-spin 0.9s cubic-bezier(0.5, 0.1, 0.5, 0.9) infinite;
+}
+
+.advalo-cumul-hist,
+.advalo-cumul-fact {
+ margin-right: 6px;
+ margin-bottom: 6px;
+}
+
+.advalo-panel .btn,
+.advalo-panel .btn-flat {
+ color: #fff !important;
+}
+
+@keyframes advalo-spin {
+ 0% {
+ transform: rotate(0deg) scale(1);
+ }
+ 40% {
+ transform: rotate(180deg) scale(1.04);
+ }
+ 100% {
+ transform: rotate(360deg) scale(1);
+ }
+}
+
.border {
border-radius: 10px !important;
}
diff --git a/ecole/public/js/advalo-module.js b/ecole/public/js/advalo-module.js
new file mode 100644
index 00000000..210dcecc
--- /dev/null
+++ b/ecole/public/js/advalo-module.js
@@ -0,0 +1,955 @@
+document.addEventListener('DOMContentLoaded', function () {
+ if (window.location.pathname !== '/advalo') return;
+
+ const token = localStorage.getItem('jwtToken');
+ const pageSize = 20;
+ const state = {
+ historique: { page: 1, totalPages: 1 },
+ reporting: { page: 1, totalPages: 1 },
+ cumul: { page: 1, totalPages: 1 },
+ loaded: { historique: false, reporting: false, cumul: false },
+ confirmation: { demandId: '', summary: '' },
+ batchConfirmation: { batchId: '', summary: '' },
+ facturationRows: [],
+ removedFacturationIds: new Set(),
+ openHistoriqueId: null
+ };
+ const requestControllers = {};
+ const loadingState = {};
+ const TRANSPORT_MODES = ['Terrestre', 'Aérien', 'Fluvial', 'Maritime', 'Postal'];
+
+ const toast = (message, classes = 'red') => M.toast({ html: message, classes });
+
+ const authHeaders = () => {
+ if (!token) throw new Error('Session expirée.');
+ return { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` };
+ };
+
+ const authOnlyHeaders = () => {
+ if (!token) throw new Error('Session expirée.');
+ return { Authorization: `Bearer ${token}` };
+ };
+
+ const isAbortError = (error) => error && (error.name === 'AbortError' || String(error.message || '').toLowerCase().includes('aborted'));
+
+ const api = async (url, options = {}) => {
+ const res = await fetch(url, { ...options, headers: options.headers || authHeaders() });
+ const contentType = res.headers.get('content-type') || '';
+ if (contentType.includes('text/csv')) {
+ if (!res.ok) throw new Error('Erreur export CSV');
+ return res.text();
+ }
+ const data = await res.json().catch(() => ({}));
+ if (!res.ok || data.valid === false) throw new Error(data.message || 'Erreur serveur');
+ return data;
+ };
+
+ const apiLatest = async (key, url, options = {}) => {
+ if (requestControllers[key]) requestControllers[key].abort();
+ const controller = new AbortController();
+ requestControllers[key] = controller;
+ try {
+ return await api(url, { ...options, signal: controller.signal });
+ } finally {
+ if (requestControllers[key] === controller) delete requestControllers[key];
+ }
+ };
+
+ const abortDataRequests = () => {
+ ['historique', 'reporting', 'cumul', 'facturation'].forEach((k) => {
+ if (requestControllers[k]) requestControllers[k].abort();
+ });
+ };
+
+ const parseAmount = (value) => {
+ const n = Number(String(value || '').replace(',', '.').replace(/\s/g, ''));
+ return Number.isFinite(n) ? n : 0;
+ };
+
+ const fmt = (n) => Number(n || 0).toLocaleString('fr-FR', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
+
+ const sourceLabel = (source) => (source === 'deleguee' ? 'Grille déléguée' : 'Hors grille');
+
+ const setIndicator = (id, page, totalPages, totalRows) => {
+ const el = document.getElementById(id);
+ if (!el) return;
+ const pages = Math.max(1, Number(totalPages || 1));
+ const current = Math.min(Math.max(1, Number(page || 1)), pages);
+ el.textContent = `Page ${current} / ${pages} - ${Number(totalRows || 0)} lignes`;
+ };
+
+ const setLoading = (section, isLoading) => {
+ const el = document.getElementById(`advalo-loading-${section}`);
+ if (!el) return;
+ if (isLoading) {
+ const marker = Symbol(section);
+ loadingState[section] = { marker, startedAt: Date.now() };
+ el.style.display = 'block';
+ return;
+ }
+ const current = loadingState[section];
+ if (!current) {
+ el.style.display = 'none';
+ return;
+ }
+ const elapsed = Date.now() - current.startedAt;
+ setTimeout(() => {
+ const latest = loadingState[section];
+ if (latest && latest.marker === current.marker) {
+ el.style.display = 'none';
+ delete loadingState[section];
+ }
+ }, Math.max(0, 280 - elapsed));
+ };
+
+ const syncTextFields = (scope = document) => {
+ if (window.M && typeof window.M.updateTextFields === 'function') window.M.updateTextFields();
+ const root = scope && scope.querySelectorAll ? scope : document;
+ const fields = root.querySelectorAll('.advalo-panel .input-field input:not([type="hidden"]), .advalo-panel .input-field textarea');
+ fields.forEach((field) => {
+ if (!field.id) return;
+ const label = document.querySelector(`label[for="${field.id}"]`);
+ if (!label) return;
+ const shouldFloat = String(field.value || '').trim().length > 0 || document.activeElement === field;
+ label.classList.toggle('active', shouldFloat);
+ });
+ };
+
+ const refreshTextFields = (scope = document) => {
+ syncTextFields(scope);
+ requestAnimationFrame(() => syncTextFields(scope));
+ setTimeout(() => syncTextFields(scope), 60);
+ };
+
+ const initSelects = (scope = document) => {
+ const selects = scope.querySelectorAll('select');
+ selects.forEach((select) => {
+ const instance = M.FormSelect.getInstance(select);
+ if (instance) instance.destroy();
+ M.FormSelect.init(select);
+ });
+ };
+
+ const formatDateInput = (raw) => {
+ const digits = String(raw || '').replace(/\D/g, '').slice(0, 8);
+ if (digits.length <= 2) return digits;
+ if (digits.length <= 4) return `${digits.slice(0, 2)}/${digits.slice(2)}`;
+ return `${digits.slice(0, 2)}/${digits.slice(2, 4)}/${digits.slice(4)}`;
+ };
+
+ const initDateFields = () => {
+ document.querySelectorAll('input.advalo-date').forEach((input) => {
+ if (input.dataset.dateMaskBound === '1') return;
+ input.addEventListener('input', () => {
+ const formatted = formatDateInput(input.value);
+ if (formatted !== input.value) input.value = formatted;
+ refreshTextFields(input.closest('.advalo-panel') || document);
+ });
+ input.dataset.dateMaskBound = '1';
+ });
+ };
+
+ const downloadDocx = async (url, options = {}, fallbackName = 'document_advalo.docx') => {
+ const res = await fetch(url, { ...options, headers: { ...authOnlyHeaders(), ...(options.headers || {}) } });
+ if (!res.ok) {
+ const payload = await res.json().catch(() => ({}));
+ throw new Error(payload.message || 'Erreur génération document');
+ }
+ const disposition = res.headers.get('content-disposition') || '';
+ const filenameMatch = disposition.match(/filename=\"?([^\";]+)\"?/i);
+ const filename = filenameMatch ? filenameMatch[1] : fallbackName;
+ const blob = await res.blob();
+ const link = document.createElement('a');
+ link.href = URL.createObjectURL(blob);
+ link.download = filename;
+ link.click();
+ URL.revokeObjectURL(link.href);
+ };
+
+ const ensureContract16 = (value, { required = false } = {}) => {
+ const digits = String(value || '').replace(/\D/g, '');
+ if (!digits) return required ? null : '';
+ if (digits.length !== 16) return null;
+ return digits;
+ };
+
+ const validateContractField = (id, required = false) => {
+ const input = document.getElementById(id);
+ if (!input) return true;
+ const v = ensureContract16(input.value, { required });
+ if (v === null) {
+ input.classList.add('invalid');
+ return false;
+ }
+ if (v) input.value = v;
+ input.classList.remove('invalid');
+ return true;
+ };
+
+ const requireFields = (ids) => {
+ let ok = true;
+ ids.forEach((id) => {
+ const input = document.getElementById(id);
+ if (!input) return;
+ if (!String(input.value || '').trim()) {
+ input.classList.add('invalid');
+ ok = false;
+ } else {
+ input.classList.remove('invalid');
+ }
+ });
+ return ok;
+ };
+
+ const applyNumericGuards = () => {
+ const integerOnlyIds = ['p-numContrat', 'p-numClient', 'p-numAgent', 'h-numClient', 'h-numContrat', 'c-numContrat', 'c-numClient', 'f-numContrat', 'r-numClient', 'r-numContrat', 'r-actorMatricule'];
+ integerOnlyIds.forEach((id) => {
+ const input = document.getElementById(id);
+ if (!input) return;
+ input.addEventListener('input', () => {
+ const cleaned = String(input.value || '').replace(/\D/g, '');
+ if (cleaned !== input.value) input.value = cleaned;
+ });
+ });
+
+ const decimalIds = ['p-capital', 'p-taux', 'p-primeMin', 'p-coutActe', 'p-cotisationHT', 'p-cotisationTTC'];
+ decimalIds.forEach((id) => {
+ const input = document.getElementById(id);
+ if (!input) return;
+ input.addEventListener('input', () => {
+ let cleaned = String(input.value || '').replace(/[^0-9.,]/g, '');
+ const comma = cleaned.indexOf(',');
+ const dot = cleaned.indexOf('.');
+ const splitAt = comma >= 0 ? comma : dot;
+ if (splitAt >= 0) cleaned = cleaned.slice(0, splitAt + 1) + cleaned.slice(splitAt + 1).replace(/[.,]/g, '');
+ if (cleaned !== input.value) input.value = cleaned;
+ });
+ });
+ };
+
+ const bindFloatingLabels = () => {
+ const inputs = document.querySelectorAll('#advalo-tab-ponctuel input, #advalo-tab-facturation input, #advalo-tab-historique input, #advalo-tab-cumul input, #advalo-tab-reporting input');
+ inputs.forEach((input) => {
+ if (input.dataset.floatBound === '1') return;
+ const resync = () => requestAnimationFrame(() => refreshTextFields(input.closest('.advalo-panel') || document));
+ input.addEventListener('focus', resync);
+ input.addEventListener('input', resync);
+ input.addEventListener('change', resync);
+ input.addEventListener('blur', resync);
+ input.dataset.floatBound = '1';
+ });
+ };
+
+ const navLinks = document.querySelectorAll('#advaloNavSelect a[data-target]');
+ const panels = document.querySelectorAll('.advalo-panel');
+ const confirmModal = M.Modal.init(document.getElementById('advalo-confirm-modal'), { dismissible: true });
+ const batchModal = M.Modal.init(document.getElementById('advalo-batch-modal'), { dismissible: true });
+
+ const getSelectedModes = () => [...document.querySelectorAll('.p-mode-check:checked')].map((el) => el.value).filter((v) => TRANSPORT_MODES.includes(v));
+ const syncModes = () => {
+ const modes = getSelectedModes();
+ document.getElementById('p-mode').value = modes.join(', ');
+ };
+
+ const recalcPonctuelPricing = () => {
+ const capital = parseAmount(document.getElementById('p-capital').value);
+ const taux = parseAmount(document.getElementById('p-taux').value);
+ const primeMin = parseAmount(document.getElementById('p-primeMin').value);
+ const coutActe = parseAmount(document.getElementById('p-coutActe').value);
+ if (!capital && !taux && !primeMin) return;
+ const cotisationHT = Math.max((capital * taux) / 100, primeMin);
+ const cotisationTTC = cotisationHT + coutActe;
+ document.getElementById('p-cotisationHT').value = cotisationHT.toFixed(2);
+ document.getElementById('p-cotisationTTC').value = cotisationTTC.toFixed(2);
+ document.getElementById('p-tarif').value = cotisationTTC.toFixed(2);
+ refreshTextFields();
+ };
+
+ const fillContractInfo = (info) => {
+ document.getElementById('p-numContrat').value = info.numContrat || document.getElementById('p-numContrat').value;
+ document.getElementById('p-numClient').value = info.numClient || '';
+ document.getElementById('p-nomClient').value = info.nomClient || '';
+ document.getElementById('p-numAgent').value = info.numAgent || '';
+ document.getElementById('p-nomAgent').value = info.nomAgent || '';
+ refreshTextFields();
+ };
+
+ const lookupContract = async () => {
+ if (!validateContractField('p-numContrat', true)) {
+ toast('N° contrat invalide: 16 chiffres requis.');
+ return;
+ }
+ const contract = document.getElementById('p-numContrat').value.trim();
+ const params = new URLSearchParams({ numContrat: contract });
+ const data = await api(`/advalo/lookup-contract?${params.toString()}`, { method: 'GET' });
+ fillContractInfo(data.info || {});
+ if (data.info?.source && data.info.source !== 'none') toast(`Informations chargées (${data.info.source}).`, 'green');
+ else toast('Aucune donnée trouvée pour ce contrat. Saisie manuelle possible.', 'orange');
+ };
+
+ const renderHistoriqueDetail = (typedId, row, details) => {
+ const detailId = `h-detail-${typedId.replace(/[^a-zA-Z0-9_-]/g, '-')}`;
+ return `
+
+
+
+
+ Marchandise: ${details.marchandise || '-'}
+ Modes: ${details.mode || '-'}
+ Départ: ${details.depart || '-'}
+ Arrivée: ${details.arrivee || '-'}
+
+
+ Valeur assurée: ${details.valeurAssuree || '-'}
+ Taux: ${details.taux || '-'}
+ Prime mini: ${details.primeMinimum || '-'}
+ Cotisation HT: ${details.cotisationHT || '-'}
+ Coût acte: ${details.coutActe || '-'}
+ Cotisation TTC: ${details.cotisationTTC || '-'}
+
+
+ Acteur: ${(details.actorPrenom || '')} ${(details.actorNom || '')} ${details.actorMatricule ? `(${details.actorMatricule})` : ''}
+
+ ${row.source === 'hors_grille' && String(row.statutFacturation || '').toLowerCase().includes('non') ? `` : ''}
+
+
+
+ |
+
`;
+ };
+
+ const loadHistorique = async () => {
+ setLoading('historique', true);
+ try {
+ const statut = document.getElementById('h-statut').value || 'all';
+ const params = new URLSearchParams({
+ numClient: document.getElementById('h-numClient').value || '',
+ numContrat: document.getElementById('h-numContrat').value || '',
+ dateDebut: document.getElementById('h-dateDebut').value || '',
+ dateFin: document.getElementById('h-dateFin').value || '',
+ sourceType: document.getElementById('h-sourceType').value || 'all',
+ statutFacturation: statut === 'all' ? '' : statut,
+ page: String(state.historique.page),
+ pageSize: String(pageSize),
+ sort: 'dateDebutIso',
+ order: 'desc'
+ });
+ const data = await apiLatest('historique', `/advalo/historique?${params.toString()}`, { method: 'GET' });
+ const body = document.getElementById('historique-body');
+ body.innerHTML = '';
+
+ (data.rows || []).forEach((row) => {
+ const typedId = `${row.source === 'deleguee' ? 'g' : 'd'}:${row.id}`;
+ body.insertAdjacentHTML('beforeend', `
+
+ |
+ ${sourceLabel(row.source)} |
+ ${row.numDemande || ''} |
+ ${row.numClient || ''} |
+ ${row.numContrat || ''} |
+ ${row.dateDebut || ''} |
+ ${row.dateFin || ''} |
+ ${row.tarif || ''} |
+ ${row.statutFacturation || ''} |
+
+
+
+ |
+
+ `);
+ });
+
+ document.querySelectorAll('.advalo-h-expand').forEach((btn) => {
+ btn.addEventListener('click', async () => {
+ const typedId = btn.dataset.id;
+ const existing = document.getElementById(`h-detail-${typedId.replace(/[^a-zA-Z0-9_-]/g, '-')}`);
+ if (existing) {
+ existing.remove();
+ state.openHistoriqueId = null;
+ btn.textContent = '▸';
+ return;
+ }
+
+ if (state.openHistoriqueId) {
+ const old = document.getElementById(`h-detail-${state.openHistoriqueId.replace(/[^a-zA-Z0-9_-]/g, '-')}`);
+ if (old) old.remove();
+ const oldBtn = document.querySelector(`.advalo-h-expand[data-id="${state.openHistoriqueId}"]`);
+ if (oldBtn) oldBtn.textContent = '▸';
+ }
+
+ try {
+ const detailData = await api(`/advalo/historique/${encodeURIComponent(typedId)}`, { method: 'GET' });
+ const row = (data.rows || []).find((r) => `${r.source === 'deleguee' ? 'g' : 'd'}:${r.id}` === typedId);
+ const details = detailData.row?.details || row?.details || {};
+ const currentRow = btn.closest('tr');
+ currentRow.insertAdjacentHTML('afterend', renderHistoriqueDetail(typedId, row || {}, details));
+ state.openHistoriqueId = typedId;
+ btn.textContent = '▾';
+ document.querySelectorAll('.advalo-h-delete').forEach((deleteBtn) => {
+ deleteBtn.addEventListener('click', async () => {
+ if (!confirm('Supprimer cette demande hors grille ?')) return;
+ try {
+ await api(`/advalo/demande/${encodeURIComponent(deleteBtn.dataset.id)}`, { method: 'DELETE' });
+ toast('Demande supprimée.', 'green');
+ await loadHistorique();
+ } catch (error) {
+ toast(error.message);
+ }
+ });
+ });
+ } catch (error) {
+ toast(error.message);
+ }
+ });
+ });
+
+ document.querySelectorAll('.advalo-doc-avenant').forEach((button) => {
+ button.addEventListener('click', async () => {
+ try {
+ await downloadDocx(`/advalo/demande/${encodeURIComponent(button.dataset.id)}/avenant`, { method: 'POST' }, 'Avenant_Advalo.docx');
+ toast('Avenant généré.', 'green');
+ } catch (error) {
+ toast(error.message);
+ }
+ });
+ });
+
+ document.querySelectorAll('.advalo-doc-attestation').forEach((button) => {
+ button.addEventListener('click', async () => {
+ try {
+ await downloadDocx(`/advalo/demande/${encodeURIComponent(button.dataset.id)}/attestation`, { method: 'POST' }, 'Attestation_Advalo.docx');
+ toast('Attestation générée.', 'green');
+ } catch (error) {
+ toast(error.message);
+ }
+ });
+ });
+
+ state.historique.totalPages = Number(data.meta?.totalPages || 1);
+ state.historique.page = Number(data.meta?.page || 1);
+ setIndicator('h-page-indicator', state.historique.page, state.historique.totalPages, data.meta?.totalRows || 0);
+ refreshTextFields();
+ } catch (error) {
+ if (isAbortError(error)) return;
+ throw error;
+ } finally {
+ setLoading('historique', false);
+ }
+ };
+
+ const updateFacturationInfo = () => {
+ const selected = [...document.querySelectorAll('.advalo-fact-check')].filter((input) => input.checked).length;
+ const total = [...document.querySelectorAll('.advalo-fact-check')].length;
+ document.getElementById('f-selection-info').textContent = `${selected} / ${total} ligne(s) sélectionnée(s)`;
+ };
+
+ const renderFacturationRows = () => {
+ const body = document.getElementById('facturation-body');
+ body.innerHTML = '';
+ state.facturationRows.forEach((row) => {
+ const typedId = `${row.source === 'deleguee' ? 'g' : 'd'}:${row.id}`;
+ body.insertAdjacentHTML('beforeend', `
+
+ |
+ ${sourceLabel(row.source)} |
+ ${row.numDemande || ''} |
+ ${row.numClient || ''} |
+ ${row.numContrat || ''} |
+ ${row.dateDebut || ''} |
+ ${row.tarif || ''} |
+ ${row.statutFacturation || ''} |
+
+ `);
+ });
+ document.querySelectorAll('.advalo-fact-check').forEach((input) => input.addEventListener('change', updateFacturationInfo));
+ updateFacturationInfo();
+
+ const first = state.facturationRows[0];
+ document.getElementById('facturation-client-agent-row').style.display = first ? 'block' : 'none';
+ document.getElementById('f-client-recap').textContent = first ? `${first.nomClient || '-'} (${first.numClient || '-'})` : '-';
+ document.getElementById('f-agent-recap').textContent = first ? (first.souscripteur || '-') : '-';
+ };
+
+ const loadFacturationCandidates = async () => {
+ if (!validateContractField('f-numContrat', false)) {
+ toast('N° contrat invalide: 16 chiffres requis.');
+ return;
+ }
+ setLoading('facturation', true);
+ try {
+ const mode = document.getElementById('f-sourceMode').value || 'hors_grille';
+ const params = new URLSearchParams({
+ numContrat: document.getElementById('f-numContrat').value || '',
+ dateDebut: document.getElementById('f-dateDebut').value || '',
+ dateFin: document.getElementById('f-dateFin').value || '',
+ facture: 'false',
+ nonFacture: 'true',
+ deleguee: mode === 'mixte' ? 'true' : 'false',
+ nonDeleguee: 'true',
+ sourceType: mode === 'mixte' ? 'all' : 'hors_grille',
+ page: '1',
+ pageSize: '200',
+ sort: 'dateDebutIso',
+ order: 'desc'
+ });
+ const data = await apiLatest('facturation', `/advalo/historique?${params.toString()}`, { method: 'GET' });
+ state.facturationRows = (data.rows || []);
+ state.removedFacturationIds = new Set();
+ renderFacturationRows();
+ if ((data.meta?.totalRows || 0) === 0) toast('Aucune ligne non facturée sur la période/contrat.', 'orange');
+ else toast(`${data.meta?.totalRows || 0} ligne(s) facturable(s).`, 'green');
+ } catch (error) {
+ if (isAbortError(error)) return;
+ throw error;
+ } finally {
+ setLoading('facturation', false);
+ }
+ };
+
+ const loadCumul = async () => {
+ setLoading('cumul', true);
+ try {
+ if (!validateContractField('c-numContrat', false)) {
+ toast('N° contrat invalide: 16 chiffres requis.');
+ return;
+ }
+ const params = new URLSearchParams({
+ numContrat: document.getElementById('c-numContrat').value || '',
+ numClient: document.getElementById('c-numClient').value || '',
+ dateDebut: document.getElementById('c-dateDebut').value || '',
+ dateFin: document.getElementById('c-dateFin').value || '',
+ page: String(state.cumul.page),
+ pageSize: String(pageSize),
+ sort: 'totalNonFacture',
+ order: 'desc'
+ });
+ const data = await apiLatest('cumul', `/advalo/cumul?${params.toString()}`, { method: 'GET' });
+ document.getElementById('k-total-advalo').textContent = fmt(data.totalAdvalo);
+ document.getElementById('k-total-facture').textContent = fmt(data.totalFacture);
+ document.getElementById('k-total-nonfacture').textContent = fmt(data.totalNonFacture);
+ document.getElementById('k-total-lignes').textContent = String(data.totalLignes || 0);
+
+ const body = document.getElementById('cumul-body');
+ body.innerHTML = '';
+ (data.rows || []).forEach((row) => {
+ body.insertAdjacentHTML('beforeend', `
+
+ | ${row.numContrat || ''} |
+ ${row.nomClient || ''} |
+ ${row.region || ''} |
+ ${row.dpt || ''} |
+ ${row.souscripteur || ''} |
+ ${fmt(row.totalAdvalo)} |
+ ${fmt(row.totalFacture)} |
+ ${fmt(row.totalNonFacture)} |
+
+
+
+ |
+
+ `);
+ });
+
+ state.cumul.totalPages = Number(data.meta?.totalPages || 1);
+ state.cumul.page = Number(data.meta?.page || 1);
+ setIndicator('c-page-indicator', state.cumul.page, state.cumul.totalPages, data.meta?.totalRows || 0);
+
+ document.querySelectorAll('.advalo-cumul-hist').forEach((btn) => {
+ btn.addEventListener('click', async () => {
+ document.getElementById('h-numContrat').value = btn.dataset.contrat || '';
+ state.historique.page = 1;
+ state.loaded.historique = true;
+ await activatePanel('advalo-tab-historique');
+ await loadHistorique();
+ });
+ });
+
+ document.querySelectorAll('.advalo-cumul-fact').forEach((btn) => {
+ btn.addEventListener('click', async () => {
+ document.getElementById('f-numContrat').value = btn.dataset.contrat || '';
+ await activatePanel('advalo-tab-facturation');
+ refreshTextFields();
+ });
+ });
+ refreshTextFields();
+ } catch (error) {
+ if (isAbortError(error)) return;
+ throw error;
+ } finally {
+ setLoading('cumul', false);
+ }
+ };
+
+ const loadReporting = async () => {
+ setLoading('reporting', true);
+ try {
+ if (!validateContractField('r-numContrat', false)) {
+ toast('N° contrat invalide: 16 chiffres requis.');
+ return;
+ }
+ const statut = document.getElementById('r-statut').value || 'all';
+ const params = new URLSearchParams({
+ numClient: document.getElementById('r-numClient').value || '',
+ numContrat: document.getElementById('r-numContrat').value || '',
+ souscripteur: document.getElementById('r-souscripteur').value || '',
+ region: document.getElementById('r-region').value || '',
+ dateDebut: document.getElementById('r-dateDebut').value || '',
+ dateFin: document.getElementById('r-dateFin').value || '',
+ actorMatricule: document.getElementById('r-actorMatricule').value || '',
+ actorNom: document.getElementById('r-actorNom').value || '',
+ actionType: document.getElementById('r-actionType').value || '',
+ sourceType: document.getElementById('r-sourceType').value || 'all',
+ statutFacturation: statut === 'all' ? '' : statut,
+ sort: document.getElementById('r-sort').value || 'totalAdvalo',
+ order: document.getElementById('r-order').value || 'desc',
+ page: String(state.reporting.page),
+ pageSize: String(pageSize)
+ });
+ const data = await apiLatest('reporting', `/advalo/reporting?${params.toString()}`, { method: 'GET' });
+ document.getElementById('r-total-advalo').textContent = fmt(data.totaux?.totalAdvalo || 0);
+ document.getElementById('r-total-facture').textContent = fmt(data.totaux?.totalFacture || 0);
+ document.getElementById('r-total-nonfacture').textContent = fmt(data.totaux?.totalNonFacture || 0);
+ document.getElementById('r-total-lignes').textContent = String(data.totaux?.totalLignes || 0);
+
+ const body = document.getElementById('reporting-body');
+ body.innerHTML = '';
+ (data.rows || []).forEach((row) => {
+ body.insertAdjacentHTML('beforeend', `
+
+ | ${row.numContrat || ''} |
+ ${row.nomClient || ''} |
+ ${row.region || ''} |
+ ${row.souscripteur || ''} |
+ ${fmt(row.totalAdvalo)} |
+ ${fmt(row.totalFacture)} |
+ ${fmt(row.totalNonFacture)} |
+ ${row.totalLignes || 0} |
+
`);
+ });
+
+ const actorBody = document.getElementById('reporting-actors-body');
+ actorBody.innerHTML = '';
+ (data.actorStats || []).forEach((a) => {
+ actorBody.insertAdjacentHTML('beforeend', `
+
+ | ${a.actorMatricule || ''} |
+ ${a.actorName || ''} |
+ ${a.actionType || ''} |
+ ${a.actionsCount || 0} |
+
+ `);
+ });
+
+ state.reporting.totalPages = Number(data.meta?.totalPages || 1);
+ state.reporting.page = Number(data.meta?.page || 1);
+ setIndicator('r-page-indicator', state.reporting.page, state.reporting.totalPages, data.meta?.totalRows || 0);
+ refreshTextFields();
+ } catch (error) {
+ if (isAbortError(error)) return;
+ throw error;
+ } finally {
+ setLoading('reporting', false);
+ }
+ };
+
+ const activatePanel = async (targetId) => {
+ abortDataRequests();
+ panels.forEach((panel) => { panel.style.display = panel.id === targetId ? 'block' : 'none'; });
+ navLinks.forEach((link) => {
+ const li = link.parentElement;
+ if (!li) return;
+ if (link.dataset.target === targetId) li.classList.add('active');
+ else li.classList.remove('active');
+ });
+
+ initDateFields();
+ refreshTextFields();
+
+ if (targetId === 'advalo-tab-historique' && !state.loaded.historique) {
+ state.historique.page = 1;
+ await loadHistorique();
+ state.loaded.historique = true;
+ }
+ if (targetId === 'advalo-tab-reporting' && !state.loaded.reporting) {
+ state.reporting.page = 1;
+ await loadReporting();
+ state.loaded.reporting = true;
+ }
+ if (targetId === 'advalo-tab-cumul' && !state.loaded.cumul) {
+ state.cumul.page = 1;
+ await loadCumul();
+ state.loaded.cumul = true;
+ }
+ };
+
+ navLinks.forEach((link) => {
+ link.addEventListener('click', async (event) => {
+ event.preventDefault();
+ try { await activatePanel(link.dataset.target); } catch (error) { toast(error.message); }
+ });
+ });
+
+ document.querySelectorAll('.p-mode-check').forEach((input) => input.addEventListener('change', () => { syncModes(); refreshTextFields(); }));
+ ['p-capital', 'p-taux', 'p-primeMin', 'p-coutActe'].forEach((id) => {
+ const input = document.getElementById(id);
+ if (input) input.addEventListener('input', recalcPonctuelPricing);
+ });
+
+ document.getElementById('p-numContrat').addEventListener('blur', async () => {
+ const value = String(document.getElementById('p-numContrat').value || '').trim();
+ if (value.length === 16) {
+ try { await lookupContract(); } catch (error) { toast(error.message); }
+ }
+ });
+ document.getElementById('p-numContrat').addEventListener('keydown', async (e) => {
+ if (e.key !== 'Enter') return;
+ e.preventDefault();
+ try { await lookupContract(); } catch (error) { toast(error.message); }
+ });
+ document.getElementById('btn-load-contract').addEventListener('click', async (event) => {
+ event.preventDefault();
+ try { await lookupContract(); } catch (error) { toast(error.message); }
+ });
+
+ document.getElementById('advalo-ponctuel-form').addEventListener('submit', async (event) => {
+ event.preventDefault();
+ const errorSlot = document.getElementById('p-form-error');
+ if (errorSlot) errorSlot.textContent = '';
+
+ syncModes();
+ const required = ['p-numContrat', 'p-marchandise', 'p-depart', 'p-arrivee', 'p-dateDebut', 'p-dateFin', 'p-capital', 'p-taux', 'p-primeMin', 'p-cotisationHT', 'p-cotisationTTC'];
+ if (!validateContractField('p-numContrat', true) || !requireFields(required)) {
+ if (errorSlot) errorSlot.textContent = 'Complète les champs obligatoires (contrat 16 chiffres).';
+ toast('Complète les champs obligatoires (contrat 16 chiffres).');
+ return;
+ }
+ if (!document.getElementById('p-mode').value.trim()) {
+ if (errorSlot) errorSlot.textContent = 'Sélectionne au moins un mode de transport.';
+ toast('Sélectionne au moins un mode de transport.');
+ return;
+ }
+
+ try {
+ const payload = {
+ typeFacturation: document.querySelector('input[name="p-typeFacturation"]:checked')?.value || 'ponctuel',
+ numClient: document.getElementById('p-numClient').value.trim(),
+ nomClient: document.getElementById('p-nomClient').value.trim(),
+ numContrat: document.getElementById('p-numContrat').value.trim(),
+ marchandise: document.getElementById('p-marchandise').value.trim(),
+ mode: document.getElementById('p-mode').value.trim(),
+ capital: document.getElementById('p-capital').value.trim(),
+ depart: document.getElementById('p-depart').value.trim(),
+ arrivee: document.getElementById('p-arrivee').value.trim(),
+ dateDebut: document.getElementById('p-dateDebut').value.trim(),
+ dateFin: document.getElementById('p-dateFin').value.trim(),
+ taux: document.getElementById('p-taux').value.trim(),
+ primeMinimum: document.getElementById('p-primeMin').value.trim(),
+ coutActe: document.getElementById('p-coutActe').value.trim(),
+ cotisationHT: document.getElementById('p-cotisationHT').value.trim(),
+ cotisationTTC: document.getElementById('p-cotisationTTC').value.trim(),
+ tarif: document.getElementById('p-cotisationTTC').value.trim(),
+ facturer: document.getElementById('p-facturer').checked
+ };
+ const data = await api('/advalo/ponctuel', { method: 'POST', body: JSON.stringify(payload) });
+ toast(`Demande créée: ${data.row.numDemande || data.row.id}`, 'green');
+ state.confirmation.demandId = `d:${data.row.id}`;
+ state.confirmation.summary = `Demande ${data.row.numDemande || data.row.id} - Contrat ${data.row.numContrat || ''}`;
+ const summary = document.getElementById('advalo-confirm-summary');
+ if (summary) summary.textContent = state.confirmation.summary;
+ confirmModal.open();
+ event.target.reset();
+ document.querySelectorAll('.p-mode-check').forEach((i) => { i.checked = false; });
+ document.getElementById('p-coutActe').value = '36';
+ document.getElementById('p-taux').value = '0.3';
+ document.getElementById('p-primeMin').value = '15';
+ document.querySelector('input[name="p-typeFacturation"][value="ponctuel"]').checked = true;
+ recalcPonctuelPricing();
+ refreshTextFields();
+ state.loaded.historique = false;
+ state.loaded.reporting = false;
+ state.loaded.cumul = false;
+ } catch (error) {
+ if (errorSlot) errorSlot.textContent = error.message;
+ toast(error.message);
+ }
+ });
+
+ document.getElementById('btn-f-load').addEventListener('click', async (event) => {
+ event.preventDefault();
+ try { await loadFacturationCandidates(); } catch (error) { toast(error.message); }
+ });
+
+ document.getElementById('btn-f-remove').addEventListener('click', (event) => {
+ event.preventDefault();
+ const selected = [...document.querySelectorAll('.advalo-fact-check:checked')].map((i) => i.value);
+ if (!selected.length) {
+ toast('Sélectionne au moins une ligne à retirer.');
+ return;
+ }
+ selected.forEach((id) => state.removedFacturationIds.add(id));
+ state.facturationRows = state.facturationRows.filter((row) => {
+ const typedId = `${row.source === 'deleguee' ? 'g' : 'd'}:${row.id}`;
+ return !state.removedFacturationIds.has(typedId);
+ });
+ renderFacturationRows();
+ toast('Lignes retirées de la liste.', 'green');
+ });
+
+ document.getElementById('btn-facturer').addEventListener('click', async (event) => {
+ event.preventDefault();
+ const ids = [...document.querySelectorAll('.advalo-fact-check:checked')].map((i) => i.value).filter(Boolean);
+ if (!ids.length) {
+ toast('Sélectionne au moins une ligne.');
+ return;
+ }
+ try {
+ const sourceMode = document.getElementById('f-sourceMode').value || 'hors_grille';
+ const includeTransportDetails = document.querySelector('input[name="f-include-details"]:checked')?.value !== 'false';
+ const data = await api('/advalo/facturation/batch', {
+ method: 'POST',
+ body: JSON.stringify({
+ demandeIds: ids,
+ sourceMode,
+ includeTransportDetails,
+ removedDemandeIds: [...state.removedFacturationIds]
+ })
+ });
+ if (data.idempotent) toast(`Lot déjà traité: ${data.batch.id}`, 'blue');
+ else toast(`Facturation OK: lot ${data.batch.id}`, 'green');
+ state.batchConfirmation.batchId = data.batch.id;
+ state.batchConfirmation.summary = `Lot ${data.batch.id} - Contrat ${data.batch.numContrat || ''} - ${data.batch.dateDebut || ''} / ${data.batch.dateFin || ''}`;
+ const batchSummary = document.getElementById('advalo-batch-summary');
+ if (batchSummary) batchSummary.textContent = state.batchConfirmation.summary;
+ batchModal.open();
+ await loadFacturationCandidates();
+ state.loaded.reporting = false;
+ state.loaded.cumul = false;
+ state.loaded.historique = false;
+ } catch (error) {
+ toast(error.message);
+ }
+ });
+
+ document.getElementById('btn-h-search').addEventListener('click', async () => {
+ if (!validateContractField('h-numContrat', false)) {
+ toast('N° contrat invalide: 16 chiffres requis.');
+ return;
+ }
+ state.historique.page = 1;
+ try { await loadHistorique(); toast('Historique chargé.', 'green'); } catch (error) { toast(error.message); }
+ });
+
+ document.getElementById('btn-h-export').addEventListener('click', async () => {
+ if (!validateContractField('h-numContrat', false)) {
+ toast('N° contrat invalide: 16 chiffres requis.');
+ return;
+ }
+ try {
+ const statut = document.getElementById('h-statut').value || 'all';
+ const params = new URLSearchParams({
+ numClient: document.getElementById('h-numClient').value || '',
+ numContrat: document.getElementById('h-numContrat').value || '',
+ dateDebut: document.getElementById('h-dateDebut').value || '',
+ dateFin: document.getElementById('h-dateFin').value || '',
+ sourceType: document.getElementById('h-sourceType').value || 'all',
+ statutFacturation: statut === 'all' ? '' : statut,
+ sort: 'dateDebutIso',
+ order: 'desc'
+ });
+ const csv = await api(`/advalo/export?${params.toString()}`, { method: 'GET', headers: { Authorization: `Bearer ${token}` } });
+ const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
+ const url = URL.createObjectURL(blob);
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = 'advalo_export.csv';
+ link.click();
+ URL.revokeObjectURL(url);
+ toast('Export CSV généré.', 'green');
+ } catch (error) {
+ toast(error.message);
+ }
+ });
+
+ document.getElementById('btn-h-prev').addEventListener('click', async () => {
+ if (state.historique.page <= 1) return;
+ state.historique.page -= 1;
+ try { await loadHistorique(); } catch (error) { toast(error.message); }
+ });
+ document.getElementById('btn-h-next').addEventListener('click', async () => {
+ if (state.historique.page >= state.historique.totalPages) return;
+ state.historique.page += 1;
+ try { await loadHistorique(); } catch (error) { toast(error.message); }
+ });
+
+ document.getElementById('btn-cumul').addEventListener('click', async (event) => {
+ event.preventDefault();
+ state.cumul.page = 1;
+ try { await loadCumul(); toast('Cumul calculé.', 'green'); } catch (error) { toast(error.message); }
+ });
+ document.getElementById('btn-c-prev').addEventListener('click', async () => {
+ if (state.cumul.page <= 1) return;
+ state.cumul.page -= 1;
+ try { await loadCumul(); } catch (error) { toast(error.message); }
+ });
+ document.getElementById('btn-c-next').addEventListener('click', async () => {
+ if (state.cumul.page >= state.cumul.totalPages) return;
+ state.cumul.page += 1;
+ try { await loadCumul(); } catch (error) { toast(error.message); }
+ });
+
+ document.getElementById('btn-reporting').addEventListener('click', async (event) => {
+ event.preventDefault();
+ state.reporting.page = 1;
+ try { await loadReporting(); toast('Reporting chargé.', 'green'); } catch (error) { toast(error.message); }
+ });
+ document.getElementById('btn-r-prev').addEventListener('click', async () => {
+ if (state.reporting.page <= 1) return;
+ state.reporting.page -= 1;
+ try { await loadReporting(); } catch (error) { toast(error.message); }
+ });
+ document.getElementById('btn-r-next').addEventListener('click', async () => {
+ if (state.reporting.page >= state.reporting.totalPages) return;
+ state.reporting.page += 1;
+ try { await loadReporting(); } catch (error) { toast(error.message); }
+ });
+
+ document.getElementById('advalo-confirm-avenant').addEventListener('click', async () => {
+ if (!state.confirmation.demandId) return;
+ try {
+ await downloadDocx(`/advalo/demande/${encodeURIComponent(state.confirmation.demandId)}/avenant`, { method: 'POST' }, 'Avenant_Advalo.docx');
+ toast('Avenant généré.', 'green');
+ } catch (error) {
+ toast(error.message);
+ }
+ });
+
+ document.getElementById('advalo-confirm-attestation').addEventListener('click', async () => {
+ if (!state.confirmation.demandId) return;
+ try {
+ await downloadDocx(`/advalo/demande/${encodeURIComponent(state.confirmation.demandId)}/attestation`, { method: 'POST' }, 'Attestation_Advalo.docx');
+ toast('Attestation générée.', 'green');
+ } catch (error) {
+ toast(error.message);
+ }
+ });
+
+ document.getElementById('advalo-batch-avenant').addEventListener('click', async () => {
+ if (!state.batchConfirmation.batchId) return;
+ try {
+ await downloadDocx(`/advalo/facturation/batch/${encodeURIComponent(state.batchConfirmation.batchId)}/avenant`, { method: 'POST' }, 'Avenant_Periodique_Advalo.docx');
+ toast('Avenant périodique généré.', 'green');
+ } catch (error) {
+ toast(error.message);
+ }
+ });
+
+ applyNumericGuards();
+ bindFloatingLabels();
+ initDateFields();
+ initSelects(document);
+ syncModes();
+ recalcPonctuelPricing();
+ refreshTextFields();
+
+ activatePanel('advalo-tab-accueil').catch((error) => toast(error.message));
+});
diff --git a/ecole/src/controllers/advaloController.js b/ecole/src/controllers/advaloController.js
new file mode 100644
index 00000000..805a80ab
--- /dev/null
+++ b/ecole/src/controllers/advaloController.js
@@ -0,0 +1,174 @@
+const express = require('express');
+const jwt = require('jsonwebtoken');
+const renderPage = require('../utils/renderHelper');
+const advaloService = require('../services/advaloService');
+const logger = require('../utils/logger');
+
+const router = express.Router();
+
+function getActor(req) {
+ const authHeader = req.headers.authorization || '';
+ const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : null;
+ if (!token) {
+ const err = new Error('Session invalide, reconnectez-vous.');
+ err.status = 401;
+ throw err;
+ }
+ return jwt.verify(token, 'no-mdp');
+}
+
+function handleError(res, error) {
+ logger.log('error', `Advalo error: ${error.message}`, {
+ status: error.status || 500,
+ stack: error.stack,
+ data: error.data,
+ originalError: error.originalError ? String(error.originalError) : undefined
+ });
+ return res.status(error.status || 500).json({
+ valid: false,
+ message: error.message || 'Erreur serveur Advalorem'
+ });
+}
+
+router.get('/', (_req, res) => {
+ renderPage('advalo.ejs', res);
+});
+
+router.get('/lookup-contract', async (req, res) => {
+ try {
+ const actor = getActor(req);
+ const info = await advaloService.lookupContract(req.query.numContrat, actor);
+ return res.json({ valid: true, info });
+ } catch (error) {
+ return handleError(res, error);
+ }
+});
+
+router.get('/historique', async (req, res) => {
+ try {
+ const actor = getActor(req);
+ const data = await advaloService.getHistorique(req.query, actor);
+ return res.json({
+ valid: true,
+ rows: data.rows,
+ meta: {
+ totalRows: data.totalRows,
+ totalPages: data.totalPages,
+ page: data.page,
+ pageSize: data.pageSize
+ }
+ });
+ } catch (error) {
+ return handleError(res, error);
+ }
+});
+
+router.get('/historique/:id', async (req, res) => {
+ try {
+ const actor = getActor(req);
+ const data = await advaloService.getHistoriqueDetail(req.params.id, actor);
+ return res.json({ valid: true, row: data });
+ } catch (error) {
+ return handleError(res, error);
+ }
+});
+
+router.get('/cumul', async (req, res) => {
+ try {
+ const actor = getActor(req);
+ const cumul = await advaloService.getCumul(req.query, actor);
+ return res.json({ valid: true, ...cumul });
+ } catch (error) {
+ return handleError(res, error);
+ }
+});
+
+router.post('/ponctuel', async (req, res) => {
+ try {
+ const actor = getActor(req);
+ const row = await advaloService.createPonctuel(req.body, actor);
+ return res.status(201).json({ valid: true, row });
+ } catch (error) {
+ return handleError(res, error);
+ }
+});
+
+router.delete('/demande/:id', async (req, res) => {
+ try {
+ const actor = getActor(req);
+ const row = await advaloService.softDeleteDemande(req.params.id, actor);
+ return res.json({ valid: true, row });
+ } catch (error) {
+ return handleError(res, error);
+ }
+});
+
+router.post('/facturation/batch', async (req, res) => {
+ try {
+ const actor = getActor(req);
+ const result = await advaloService.facturerBatch(req.body, actor);
+ return res.json({ valid: true, ...result });
+ } catch (error) {
+ return handleError(res, error);
+ }
+});
+
+router.post('/demande/:id/avenant', async (req, res) => {
+ try {
+ const actor = getActor(req);
+ const doc = await advaloService.generateDemandeDocument(req.params.id, 'avenant', actor);
+ res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document');
+ res.setHeader('Content-Disposition', `attachment; filename="${doc.filename}"`);
+ return res.send(doc.buffer);
+ } catch (error) {
+ return handleError(res, error);
+ }
+});
+
+router.post('/demande/:id/attestation', async (req, res) => {
+ try {
+ const actor = getActor(req);
+ const doc = await advaloService.generateDemandeDocument(req.params.id, 'attestation', actor);
+ res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document');
+ res.setHeader('Content-Disposition', `attachment; filename="${doc.filename}"`);
+ return res.send(doc.buffer);
+ } catch (error) {
+ return handleError(res, error);
+ }
+});
+
+router.post('/facturation/batch/:id/avenant', async (req, res) => {
+ try {
+ const actor = getActor(req);
+ const doc = await advaloService.generateBatchAvenant(req.params.id, actor);
+ res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document');
+ res.setHeader('Content-Disposition', `attachment; filename="${doc.filename}"`);
+ return res.send(doc.buffer);
+ } catch (error) {
+ return handleError(res, error);
+ }
+});
+
+router.get('/reporting', async (req, res) => {
+ try {
+ const actor = getActor(req);
+ const data = await advaloService.getReporting(req.query, actor);
+ return res.json({ valid: true, ...data });
+ } catch (error) {
+ return handleError(res, error);
+ }
+});
+
+router.get('/export', async (req, res) => {
+ try {
+ const actor = getActor(req);
+ const csv = await advaloService.exportHistorique(req.query, actor);
+ res.setHeader('Content-Type', 'text/csv; charset=utf-8');
+ res.setHeader('Content-Disposition', 'attachment; filename="advalo_export.csv"');
+ return res.send(csv);
+ } catch (error) {
+ return handleError(res, error);
+ }
+});
+
+module.exports = router;
diff --git a/ecole/src/routes.js b/ecole/src/routes.js
index 3b64bb33..f2a08288 100644
--- a/ecole/src/routes.js
+++ b/ecole/src/routes.js
@@ -21,6 +21,7 @@ const historiqueParcoursController = require('./controllers/historiqueParcoursCo
const userController = require('./controllers/userController');
const regionController = require('./controllers/regionController');
const downloadController = require('./controllers/utilsController');
+const advaloController = require('./controllers/advaloController');
// Association des contrôleurs aux routes
router.use('/', rootController);
@@ -38,5 +39,6 @@ router.use('/historiqueParcours', historiqueParcoursController);
router.use('/user', userController);
router.use('/region', regionController);
router.use('/download', downloadController);
+router.use('/advalo', advaloController);
-module.exports = router;
\ No newline at end of file
+module.exports = router;
diff --git a/ecole/src/services/advaloService.js b/ecole/src/services/advaloService.js
new file mode 100644
index 00000000..6b6b0a9b
--- /dev/null
+++ b/ecole/src/services/advaloService.js
@@ -0,0 +1,1754 @@
+const crypto = require('crypto');
+const { spawn, execFile } = require('child_process');
+const path = require('path');
+const fs = require('fs');
+const { promisify } = require('util');
+const PizZip = require('pizzip');
+require('dotenv').config();
+const { db } = require('../db/db-connect');
+const logger = require('../utils/logger');
+
+const execFileAsync = promisify(execFile);
+
+const JWT_ROLES = ['SOUSCRIPTEUR', 'MANAGER', 'ADMIN'];
+const TRANSPORT_MODES = ['Terrestre', 'Aérien', 'Fluvial', 'Maritime', 'Postal'];
+const DEFAULT_PAGE_SIZE = 20;
+const MAX_PAGE_SIZE = 100;
+const PB_SQLITE_PATH = path.resolve(__dirname, '..', 'db', 'pb_data', 'data.db');
+const ADVALO_TEMPLATE_DIR = path.resolve(__dirname, '..', 'templates', 'advalo');
+const ADVALO_DOCUMENTS_DIR = path.resolve(process.cwd(), 'documents', 'advalorem');
+const AXA_SCRIPTS_ROOT = path.resolve(process.cwd(), 'vbs');
+const AXA_TIMEOUT_MS = Number(process.env.AXA_TIMEOUT_MS || 65000);
+const AXA_RETRY_COUNT = Number(process.env.AXA_RETRY_COUNT || 1);
+const AXA_RETRY_DELAY_MS = Number(process.env.AXA_RETRY_DELAY_MS || 1200);
+
+if (!db.baseUrl || db.baseUrl === '/') {
+ db.baseUrl = process.env.DB_URL || db.baseUrl;
+}
+
+function ensureRole(actor) {
+ if (!actor || !JWT_ROLES.includes(actor.userAuthGroupe)) {
+ const err = new Error('Droits insuffisants pour accéder au module Advalorem.');
+ err.status = 403;
+ throw err;
+ }
+}
+
+function ensureReportingRole(actor) {
+ ensureRole(actor);
+ if (!['MANAGER', 'ADMIN'].includes(actor.userAuthGroupe)) {
+ const err = new Error('Le reporting Advalorem est réservé aux managers/admins.');
+ err.status = 403;
+ throw err;
+ }
+}
+
+function padContract(value) {
+ const digits = String(value || '').replace(/\D/g, '');
+ if (!digits) return '';
+ return digits.padStart(16, '0').slice(-16);
+}
+
+function normalizeContractStrict(value, { required = false } = {}) {
+ const digits = String(value || '').replace(/\D/g, '');
+ if (!digits) {
+ if (required) {
+ const err = new Error('Numéro de contrat obligatoire.');
+ err.status = 400;
+ throw err;
+ }
+ return '';
+ }
+ if (digits.length !== 16) {
+ const err = new Error('Le numéro de contrat doit contenir exactement 16 chiffres.');
+ err.status = 400;
+ throw err;
+ }
+ return digits;
+}
+
+function parseFrDate(value) {
+ const v = String(value || '').trim();
+ const match = v.match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
+ if (!match) return null;
+ const date = new Date(`${match[3]}-${match[2]}-${match[1]}T00:00:00.000Z`);
+ return Number.isNaN(date.getTime()) ? null : date;
+}
+
+function formatFrDate(date) {
+ const d = new Date(date);
+ const day = String(d.getUTCDate()).padStart(2, '0');
+ const month = String(d.getUTCMonth() + 1).padStart(2, '0');
+ const year = d.getUTCFullYear();
+ return `${day}/${month}/${year}`;
+}
+
+function parseAmount(value) {
+ const normalized = String(value || '')
+ .replace(/€/g, '')
+ .replace(/\u202F/g, '')
+ .replace(/\u00A0/g, '')
+ .replace(/\s/g, '')
+ .replace(',', '.');
+ const out = Number(normalized);
+ return Number.isFinite(out) ? out : 0;
+}
+
+function prettyNumber(value) {
+ const amount = parseAmount(value);
+ return amount.toLocaleString('fr-FR', {
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2
+ });
+}
+
+function collapseSpaces(value) {
+ return String(value || '').replace(/\s+/g, ' ').trim();
+}
+
+function escapeRegex(value) {
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+}
+
+function safeDocValue(value) {
+ return String(value ?? '')
+ .trim()
+ .replace(/&/g, '&')
+ .replace(//g, '>');
+}
+
+function ensureDirectory(dirPath) {
+ fs.mkdirSync(dirPath, { recursive: true });
+}
+
+function sanitizeFilePart(value) {
+ return String(value || '')
+ .replace(/[^a-zA-Z0-9-_]+/g, '_')
+ .replace(/_+/g, '_')
+ .replace(/^_+|_+$/g, '');
+}
+
+function isAllZeroDigits(value) {
+ const digits = String(value || '').replace(/\D/g, '');
+ return Boolean(digits) && /^0+$/.test(digits);
+}
+
+function buildActorDisplayName(actor = {}) {
+ const first = String(actor.userFirstName || '').trim();
+ const last = String(actor.userLastName || '').trim();
+ const full = `${first} ${last}`.trim();
+ if (full) return full;
+ return String(actor.userMatricule || '').trim();
+}
+
+function withMatriculeSuffix(name, actor = {}) {
+ const matricule = String(actor.userMatricule || '').trim();
+ const safeName = String(name || '').trim();
+ if (!matricule) return safeName;
+ if (!safeName) return matricule;
+ return `${safeName} (${matricule})`;
+}
+
+function replaceTokens(text, replacements) {
+ let output = text;
+ Object.entries(replacements).forEach(([token, value]) => {
+ const val = safeDocValue(value);
+ output = output.replace(new RegExp(escapeRegex(token), 'g'), val);
+ });
+ return output;
+}
+
+function buildDocxFromTemplate(templatePath, replacements) {
+ const content = fs.readFileSync(templatePath);
+ const zip = new PizZip(content);
+ const documentXml = zip.file('word/document.xml').asText();
+ const replacedXml = replaceTokens(documentXml, replacements);
+ zip.file('word/document.xml', replacedXml);
+ return zip.generate({ type: 'nodebuffer' });
+}
+
+async function saveAdvaloDocumentTrace({ type, filePath, buffer, demandeId = '', batchId = '' }) {
+ const sha256 = crypto.createHash('sha256').update(buffer).digest('hex');
+ await db.records.create('advalo_document', {
+ type,
+ path: filePath,
+ sha256,
+ demandeId,
+ batchId,
+ createdAt: new Date().toISOString()
+ });
+}
+
+function toCsv(rows) {
+ const headers = [
+ 'source',
+ 'numDemande',
+ 'numClient',
+ 'nomClient',
+ 'numContrat',
+ 'dateDebut',
+ 'dateFin',
+ 'tarif',
+ 'statutFacturation'
+ ];
+ const escape = (value) => `"${String(value ?? '').replace(/"/g, '""')}"`;
+ const lines = rows.map((row) => headers.map((h) => escape(row[h])).join(';'));
+ return `${headers.join(';')}\n${lines.join('\n')}`;
+}
+
+function hashFingerprint(payload) {
+ return crypto.createHash('sha256').update(JSON.stringify(payload)).digest('hex');
+}
+
+function escapeFilterValue(value) {
+ return String(value || '').replace(/\\/g, '\\\\').replace(/"/g, '\\"');
+}
+
+function normalizeStatus(status) {
+ const v = String(status || '').toLowerCase();
+ return v.includes('facturé') && !v.includes('non') ? 'facture' : 'non_facture';
+}
+
+async function logAudit(eventType, actor = {}, data = {}) {
+ const payload = {
+ actorMatricule: String(actor.userMatricule || ''),
+ actorNom: String(actor.userLastName || ''),
+ actorPrenom: String(actor.userFirstName || ''),
+ actorGroupe: String(actor.userAuthGroupe || ''),
+ timestamp: new Date().toISOString(),
+ ...data
+ };
+ try {
+ await db.records.create('advalo_audit', {
+ eventType,
+ createdAt: new Date().toISOString(),
+ data: payload
+ });
+ } catch (error) {
+ logger.log('warn', 'advalo_audit write skipped', { eventType, error: error.message });
+ }
+}
+
+function normalizeTransportModes(value) {
+ const toTokens = (v) => {
+ if (Array.isArray(v)) return v;
+ return String(v || '')
+ .split(',')
+ .map((x) => x.trim())
+ .filter(Boolean);
+ };
+ const unique = [...new Set(toTokens(value).map((token) => {
+ const normalized = token.toLowerCase();
+ return TRANSPORT_MODES.find((mode) => mode.toLowerCase() === normalized) || null;
+ }).filter(Boolean))];
+ return unique;
+}
+
+function toSourceLabel(source) {
+ return source === 'deleguee' ? 'Grille déléguée' : 'Hors grille';
+}
+
+function parsePagination(filters = {}) {
+ const page = Math.max(1, Number.parseInt(filters.page, 10) || 1);
+ const requestedPageSize = Number.parseInt(filters.pageSize, 10) || DEFAULT_PAGE_SIZE;
+ const pageSize = Math.min(MAX_PAGE_SIZE, Math.max(1, requestedPageSize));
+ return { page, pageSize };
+}
+
+function parseDateRange(filters = {}) {
+ const rawFrom = String(filters.dateDebut || '').trim();
+ const rawTo = String(filters.dateFin || '').trim();
+
+ if (!rawFrom && !rawTo) {
+ return { fromIso: null, toIso: null };
+ }
+
+ const from = rawFrom ? parseFrDate(rawFrom) : null;
+ const to = rawTo ? parseFrDate(rawTo) : null;
+
+ if ((rawFrom && !from) || (rawTo && !to)) {
+ const err = new Error('Format de date invalide (attendu: JJ/MM/AAAA).');
+ err.status = 400;
+ throw err;
+ }
+
+ if (from && to && from > to) {
+ const err = new Error('La date de début doit être antérieure à la date de fin.');
+ err.status = 400;
+ throw err;
+ }
+
+ return {
+ fromIso: from ? from.toISOString().slice(0, 10) : null,
+ toIso: to ? to.toISOString().slice(0, 10) : null
+ };
+}
+
+function resolveSourceType(filters = {}) {
+ const value = String(filters.sourceType || '').toLowerCase();
+ if (['deleguee', 'grille_deleguee', 'grille'].includes(value)) return 'deleguee';
+ if (['hors_grille', 'demande', 'ponctuel'].includes(value)) return 'hors_grille';
+ return 'all';
+}
+
+function sqlQuote(value) {
+ return `'${String(value || '').replace(/'/g, "''")}'`;
+}
+
+function parseSqlJson(stdout) {
+ const raw = String(stdout || '').trim();
+ if (!raw) return [];
+ try {
+ const parsed = JSON.parse(raw);
+ return Array.isArray(parsed) ? parsed : [];
+ } catch (_error) {
+ return [];
+ }
+}
+
+async function runSqlJson(sql) {
+ const { stdout } = await execFileAsync('sqlite3', ['-json', PB_SQLITE_PATH, sql], {
+ maxBuffer: 1024 * 1024 * 30
+ });
+ return parseSqlJson(stdout);
+}
+
+function sqlTarifNumberExpr(columnName = 'tarif') {
+ return `CAST(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(${columnName}, ''), '€', ''), char(160), ''), char(8239), ''), ' ', ''), ',', '.') AS REAL)`;
+}
+
+const MERGED_CTE_SQL = `
+WITH merged AS (
+ SELECT
+ id,
+ 'deleguee' AS source,
+ numDemande,
+ numClient,
+ nomClient,
+ numContrat,
+ dateDebut,
+ dateFin,
+ dateDebutIso,
+ dateFinIso,
+ tarif,
+ statutFacturation,
+ marchandise,
+ mode,
+ capital,
+ depart,
+ arrivee,
+ '' AS region,
+ '' AS dpt,
+ '' AS souscripteur,
+ '' AS createdBy,
+ 0 AS isDeleted
+ FROM advalo_deleguee
+
+ UNION ALL
+
+ SELECT
+ id,
+ 'hors_grille' AS source,
+ numDemande,
+ numClient,
+ nomClient,
+ numContrat,
+ dateDebut,
+ dateFin,
+ dateDebutIso,
+ dateFinIso,
+ tarif,
+ statutFacturation,
+ marchandise,
+ mode,
+ capital,
+ depart,
+ arrivee,
+ COALESCE(region, '') AS region,
+ '' AS dpt,
+ '' AS souscripteur,
+ COALESCE(createdBy, '') AS createdBy,
+ COALESCE(isDeleted, 0) AS isDeleted
+ FROM advalo_demande
+),
+normalized AS (
+ SELECT
+ id,
+ source,
+ CASE WHEN source = 'deleguee' THEN 'Grille déléguée' ELSE 'Hors grille' END AS sourceLabel,
+ COALESCE(numDemande, '') AS numDemande,
+ COALESCE(numClient, '') AS numClient,
+ COALESCE(nomClient, '') AS nomClient,
+ COALESCE(numContrat, '') AS numContrat,
+ COALESCE(dateDebut, '') AS dateDebut,
+ COALESCE(dateFin, '') AS dateFin,
+ COALESCE(dateDebutIso, '') AS dateDebutIso,
+ COALESCE(dateFinIso, '') AS dateFinIso,
+ COALESCE(tarif, '') AS tarif,
+ COALESCE(statutFacturation, '') AS statutFacturation,
+ COALESCE(marchandise, '') AS marchandise,
+ COALESCE(mode, '') AS mode,
+ COALESCE(capital, '') AS capital,
+ COALESCE(depart, '') AS depart,
+ COALESCE(arrivee, '') AS arrivee,
+ CASE
+ WHEN lower(COALESCE(statutFacturation, '')) LIKE '%factur%' AND lower(COALESCE(statutFacturation, '')) NOT LIKE '%non%'
+ THEN 'facture'
+ ELSE 'non_facture'
+ END AS statutNormalized,
+ COALESCE(region, '') AS region,
+ COALESCE(dpt, '') AS dpt,
+ COALESCE(souscripteur, '') AS souscripteur,
+ COALESCE(createdBy, '') AS createdBy,
+ ${sqlTarifNumberExpr('tarif')} AS tarifNum,
+ COALESCE(
+ date(substr(COALESCE(dateDebutIso, ''), 1, 10)),
+ date(substr(COALESCE(dateDebut, ''), 7, 4) || '-' || substr(COALESCE(dateDebut, ''), 4, 2) || '-' || substr(COALESCE(dateDebut, ''), 1, 2))
+ ) AS dateDebutSql,
+ COALESCE(
+ date(substr(COALESCE(dateFinIso, ''), 1, 10)),
+ date(substr(COALESCE(dateFin, ''), 7, 4) || '-' || substr(COALESCE(dateFin, ''), 4, 2) || '-' || substr(COALESCE(dateFin, ''), 1, 2))
+ ) AS dateFinSql
+ FROM merged
+ WHERE source = 'deleguee' OR isDeleted = 0
+)
+`;
+
+function buildCommonWhere(filters = {}, actor) {
+ const where = [];
+ const dateRange = parseDateRange(filters);
+ const contract = normalizeContractStrict(filters.numContrat, { required: false });
+ const numClient = String(filters.numClient || '').trim();
+ const region = String(filters.region || '').trim();
+ const souscripteur = String(filters.souscripteur || '').trim();
+ const actorMatricule = String(filters.actorMatricule || '').trim();
+ const actorNom = String(filters.actorNom || '').trim();
+ const actionType = String(filters.actionType || '').trim().toLowerCase();
+ const sourceType = resolveSourceType(filters);
+
+ if (contract) where.push(`numContrat = ${sqlQuote(contract)}`);
+ if (numClient) where.push(`numClient LIKE ${sqlQuote(`%${numClient}%`)}`);
+ if (region) where.push(`region LIKE ${sqlQuote(`%${region}%`)}`);
+ if (souscripteur) where.push(`souscripteur LIKE ${sqlQuote(`%${souscripteur}%`)}`);
+
+ if (dateRange.fromIso) where.push(`dateDebutSql >= date(${sqlQuote(dateRange.fromIso)})`);
+ if (dateRange.toIso) where.push(`dateDebutSql <= date(${sqlQuote(dateRange.toIso)})`);
+
+ if (sourceType === 'deleguee') where.push(`source = 'deleguee'`);
+ if (sourceType === 'hors_grille') where.push(`source = 'hors_grille'`);
+
+ const statusFilter = String(filters.statutFacturation || '').toLowerCase();
+ if (statusFilter === 'facture') where.push(`statutNormalized = 'facture'`);
+ if (statusFilter === 'non_facture') where.push(`statutNormalized = 'non_facture'`);
+
+ const facture = String(filters.facture || '').toLowerCase();
+ const nonFacture = String(filters.nonFacture || '').toLowerCase();
+ const deleguee = String(filters.deleguee || '').toLowerCase();
+ const nonDeleguee = String(filters.nonDeleguee || '').toLowerCase();
+
+ if (facture === 'false') where.push(`statutNormalized != 'facture'`);
+ if (nonFacture === 'false') where.push(`statutNormalized != 'non_facture'`);
+ if (deleguee === 'false') where.push(`source != 'deleguee'`);
+ if (nonDeleguee === 'false') where.push(`source != 'hors_grille'`);
+
+ if (actor?.userAuthGroupe === 'SOUSCRIPTEUR') {
+ where.push(`(source = 'deleguee' OR createdBy = ${sqlQuote(actor.userMatricule || '')})`);
+ }
+
+ if (actorMatricule || actorNom || actionType) {
+ const auditClauses = [];
+ if (actorMatricule) {
+ auditClauses.push(`json_extract(a.data, '$.actorMatricule') LIKE ${sqlQuote(`%${actorMatricule}%`)}`);
+ }
+ if (actorNom) {
+ auditClauses.push(`(
+ json_extract(a.data, '$.actorNom') LIKE ${sqlQuote(`%${actorNom}%`)}
+ OR json_extract(a.data, '$.actorPrenom') LIKE ${sqlQuote(`%${actorNom}%`)}
+ )`);
+ }
+ if (actionType) {
+ auditClauses.push(`a.eventType = ${sqlQuote(actionType)}`);
+ }
+ where.push(`EXISTS (
+ SELECT 1
+ FROM advalo_audit a
+ WHERE json_extract(a.data, '$.numContrat') = normalized.numContrat
+ ${auditClauses.length ? `AND ${auditClauses.join(' AND ')}` : ''}
+ )`);
+ }
+
+ return where;
+}
+
+function normalizeSqlRow(row) {
+ return {
+ ...row,
+ sourceType: row.source,
+ sourceLabel: row.sourceLabel || toSourceLabel(row.source),
+ statutNormalized: row.statutNormalized || normalizeStatus(row.statutFacturation),
+ numContrat: padContract(row.numContrat),
+ tarif: String(row.tarif ?? ''),
+ tarifNum: Number(row.tarifNum || 0)
+ };
+}
+
+function normalizeSort(sort, fallback, allowList) {
+ const value = String(sort || fallback);
+ return allowList.includes(value) ? value : fallback;
+}
+
+function normalizeOrder(order) {
+ return String(order || 'desc').toLowerCase() === 'asc' ? 'ASC' : 'DESC';
+}
+
+function buildPaginationMeta(totalRows, page, pageSize) {
+ const totalPages = Math.max(1, Math.ceil(totalRows / pageSize));
+ const safePage = Math.min(Math.max(1, page), totalPages);
+ return {
+ totalRows,
+ totalPages,
+ page: safePage,
+ pageSize,
+ offset: (safePage - 1) * pageSize
+ };
+}
+
+async function runQt550Axa(payload) {
+ if (process.platform !== 'win32') {
+ const err = new Error('Pont AXA indisponible sur cet environnement (Windows requis).');
+ err.status = 502;
+ throw err;
+ }
+
+ const matricule = sanitizeFilePart(payload?.matricule || 'SYSTEM');
+ const configPath = path.join(AXA_SCRIPTS_ROOT, 'script_qt550', 'config', `bordereau_${matricule}.txt`);
+ ensureDirectory(path.dirname(configPath));
+ const content = Object.entries({
+ numContrat: payload?.numContrat || '',
+ totalCapitaux: payload?.totalCapitaux || payload?.capital || '',
+ totalCotisation: payload?.totalCotisation || payload?.tarif || '',
+ dateDebut: payload?.dateDebut || '',
+ dateFin: payload?.dateFin || ''
+ })
+ .map(([key, value]) => `${key}:${value}`)
+ .join('##');
+ fs.writeFileSync(configPath, content, 'utf8');
+
+ await runWScriptScript('script_qt550/bordereau.vbs', { payload, errorLabel: 'QT550' });
+}
+
+function readAxaKeyValueFile(filePath) {
+ if (!fs.existsSync(filePath)) return {};
+ const raw = fs.readFileSync(filePath, 'utf8');
+ return raw.split(/\r?\n/).reduce((acc, line) => {
+ if (!line || !line.includes(':')) return acc;
+ const [key, ...rest] = line.split(':');
+ const normalizedKey = String(key || '').trim();
+ if (!normalizedKey) return acc;
+ acc[normalizedKey] = rest.join(':').trim();
+ return acc;
+ }, {});
+}
+
+async function runWScriptScript(relativeScriptPath, { payload = {}, errorLabel = 'AXA' } = {}) {
+ const scriptPath = path.join(AXA_SCRIPTS_ROOT, relativeScriptPath);
+ if (!fs.existsSync(scriptPath)) {
+ const err = new Error(`Script AXA manquant: ${relativeScriptPath}`);
+ err.status = 500;
+ throw err;
+ }
+
+ let lastError = null;
+ for (let attempt = 0; attempt <= AXA_RETRY_COUNT; attempt += 1) {
+ try {
+ await new Promise((resolve, reject) => {
+ const child = spawn('wscript.exe', [scriptPath], { cwd: process.cwd(), stdio: 'ignore' });
+ const timeout = setTimeout(() => {
+ child.kill('SIGTERM');
+ const err = new Error(`Timeout lors de l'appel AXA ${errorLabel}.`);
+ err.status = 504;
+ reject(err);
+ }, AXA_TIMEOUT_MS);
+
+ child.on('error', (error) => {
+ clearTimeout(timeout);
+ const err = new Error(`Erreur technique AXA ${errorLabel}: ${error.message}`);
+ err.status = 502;
+ reject(err);
+ });
+ child.on('exit', (code) => {
+ clearTimeout(timeout);
+ if (code === 0) return resolve();
+ const err = new Error(`AXA ${errorLabel} en échec (code ${code}).`);
+ err.status = 502;
+ return reject(err);
+ });
+ });
+ return;
+ } catch (error) {
+ lastError = error;
+ logger.log('warn', `AXA ${errorLabel} failed`, {
+ attempt: attempt + 1,
+ retries: AXA_RETRY_COUNT + 1,
+ error: error.message,
+ payload
+ });
+ if (attempt < AXA_RETRY_COUNT) await new Promise((resolve) => setTimeout(resolve, AXA_RETRY_DELAY_MS));
+ }
+ }
+ throw lastError || new Error(`AXA ${errorLabel} failed`);
+}
+
+function normalizeLookupInfo(raw = {}, fallbackContract = '') {
+ const numContrat = normalizeContractStrict(raw.numContrat || fallbackContract, { required: true });
+ return {
+ numContrat,
+ numClient: String(raw.numClient || '').replace(/\D/g, '').slice(0, 16),
+ nomClient: collapseSpaces(raw.nomClient || ''),
+ adresseClient: collapseSpaces(raw.adresseClient || ''),
+ codePostalClient: collapseSpaces(raw.codePostal || raw.codePostalClient || ''),
+ nomAgent: collapseSpaces(raw.nomAgent || ''),
+ numAgent: String(raw.numAgent || '').replace(/\D/g, '').slice(0, 16)
+ };
+}
+
+async function lookupContractFromAxaScripts(numContrat, actor) {
+ if (process.platform !== 'win32') return null;
+ const matricule = sanitizeFilePart(actor?.userMatricule || 'SYSTEM');
+ const pa025ConfigPath = path.join(AXA_SCRIPTS_ROOT, 'script_pa025', 'config', `client_${matricule}.txt`);
+ const cl063ConfigPath = path.join(AXA_SCRIPTS_ROOT, 'script_cl063', 'config', `client_${matricule}.txt`);
+ ensureDirectory(path.dirname(pa025ConfigPath));
+ ensureDirectory(path.dirname(cl063ConfigPath));
+
+ const pa025Payload = [
+ `numContrat:${numContrat}`,
+ 'dateDebutBordereau:',
+ 'dateFinBordereau:'
+ ].join('\r\n');
+
+ fs.writeFileSync(pa025ConfigPath, pa025Payload, 'utf8');
+ await runWScriptScript('script_pa025/attestation.vbs', { payload: { numContrat }, errorLabel: 'PA025' });
+ const pa025Data = readAxaKeyValueFile(pa025ConfigPath);
+
+ let cl063Data = {};
+ const pa025NumClient = String(pa025Data.numClient || '').replace(/\D/g, '');
+ if (pa025NumClient) {
+ fs.writeFileSync(cl063ConfigPath, `numClient:${pa025NumClient}`, 'utf8');
+ await runWScriptScript('script_cl063/extract.vbs', { payload: { numClient: pa025NumClient }, errorLabel: 'CL063' });
+ cl063Data = readAxaKeyValueFile(cl063ConfigPath);
+ }
+
+ const merged = { ...pa025Data, ...cl063Data };
+ return {
+ ...normalizeLookupInfo(merged, numContrat),
+ source: 'axa scripts'
+ };
+}
+
+async function lookupContract(numContrat, actor) {
+ ensureRole(actor);
+ const formatted = normalizeContractStrict(numContrat, { required: true });
+
+ try {
+ const scriptInfo = await lookupContractFromAxaScripts(formatted, actor);
+ if (scriptInfo?.numContrat) {
+ await logAudit('lookup_contrat', actor, {
+ sourceType: 'axa_scripts',
+ numContrat: scriptInfo.numContrat,
+ numClient: scriptInfo.numClient || ''
+ });
+ return scriptInfo;
+ }
+ } catch (error) {
+ logger.log('warn', 'lookupContract AXA scripts failed, fallback DB', {
+ numContrat: formatted,
+ actor: actor?.userMatricule || '',
+ error: error.message
+ });
+ }
+
+ const contratList = await db.records.getList('contrat', 1, 1, {
+ filter: `numContrat = "${escapeFilterValue(formatted)}"`,
+ expand: 'client,intermediaire'
+ });
+
+ if (contratList.totalItems > 0) {
+ const contrat = contratList.items[0];
+ await logAudit('lookup_contrat', actor, {
+ sourceType: 'base_contrat',
+ numContrat: formatted,
+ numClient: contrat?.['@expand']?.client?.numClient || ''
+ });
+ return {
+ numContrat: contrat.numContrat || formatted,
+ numClient: contrat?.['@expand']?.client?.numClient || '',
+ nomClient: contrat?.['@expand']?.client?.nom || '',
+ adresseClient: contrat?.['@expand']?.client?.adresse || '',
+ codePostalClient: contrat?.['@expand']?.client?.codePostal || '',
+ nomAgent: contrat?.['@expand']?.intermediaire?.nom || '',
+ numAgent: contrat?.['@expand']?.intermediaire?.numPortefeuille || '',
+ source: 'base contrat'
+ };
+ }
+
+ const delegate = await db.records.getList('advalo_deleguee', 1, 1, {
+ filter: `numContrat = "${escapeFilterValue(formatted)}"`,
+ sort: '-dateDebutIso'
+ });
+ if (delegate.totalItems > 0) {
+ const row = delegate.items[0];
+ await logAudit('lookup_contrat', actor, {
+ sourceType: 'grille_deleguee',
+ numContrat: formatted,
+ numClient: row.numClient || ''
+ });
+ return {
+ numContrat: row.numContrat || formatted,
+ numClient: row.numClient || '',
+ nomClient: row.nomClient || '',
+ adresseClient: '',
+ codePostalClient: '',
+ nomAgent: row.souscripteur || '',
+ numAgent: row.numPortefeuille || '',
+ source: 'grille déléguée'
+ };
+ }
+
+ const demande = await db.records.getList('advalo_demande', 1, 1, {
+ filter: `numContrat = "${escapeFilterValue(formatted)}" && isDeleted = false`,
+ sort: '-dateDebutIso'
+ });
+ if (demande.totalItems > 0) {
+ const row = demande.items[0];
+ await logAudit('lookup_contrat', actor, {
+ sourceType: 'hors_grille',
+ numContrat: formatted,
+ numClient: row.numClient || ''
+ });
+ return {
+ numContrat: row.numContrat || formatted,
+ numClient: row.numClient || '',
+ nomClient: row.nomClient || '',
+ adresseClient: '',
+ codePostalClient: '',
+ nomAgent: row.souscripteur || '',
+ numAgent: row.numPortefeuille || '',
+ source: 'hors grille'
+ };
+ }
+
+ const result = {
+ numContrat: formatted,
+ numClient: '',
+ nomClient: '',
+ adresseClient: '',
+ codePostalClient: '',
+ nomAgent: '',
+ numAgent: '',
+ source: 'none'
+ };
+ await logAudit('lookup_contrat', actor, {
+ sourceType: 'none',
+ numContrat: formatted,
+ numClient: ''
+ });
+ return result;
+}
+
+async function getHistorique(filters, actor) {
+ ensureRole(actor);
+ const { page, pageSize } = parsePagination(filters);
+ const sort = normalizeSort(filters.sort, 'dateDebutSql', [
+ 'dateDebutSql',
+ 'dateDebutIso',
+ 'dateDebut',
+ 'dateFinSql',
+ 'dateFinIso',
+ 'dateFin',
+ 'numContrat',
+ 'numClient',
+ 'tarifNum',
+ 'tarif'
+ ]);
+ const sqlSort = ['dateDebut', 'dateDebutIso'].includes(sort)
+ ? 'dateDebutSql'
+ : ['dateFin', 'dateFinIso'].includes(sort)
+ ? 'dateFinSql'
+ : ['tarif', 'tarifNum'].includes(sort)
+ ? 'tarifNum'
+ : sort;
+ const order = normalizeOrder(filters.order);
+
+ const where = buildCommonWhere(filters, actor);
+ const whereClause = where.length ? `WHERE ${where.join(' AND ')}` : '';
+
+ const countRows = await runSqlJson(`
+${MERGED_CTE_SQL}
+SELECT COUNT(*) AS totalRows FROM normalized ${whereClause};
+`);
+ const totalRows = Number(countRows[0]?.totalRows || 0);
+ const meta = buildPaginationMeta(totalRows, page, pageSize);
+
+ const rows = await runSqlJson(`
+${MERGED_CTE_SQL}
+SELECT
+ id,
+ source,
+ sourceLabel,
+ numDemande,
+ numClient,
+ nomClient,
+ numContrat,
+ dateDebut,
+ dateFin,
+ dateDebutIso,
+ dateFinIso,
+ marchandise,
+ mode,
+ capital,
+ depart,
+ arrivee,
+ tarif,
+ tarifNum,
+ statutFacturation,
+ statutNormalized,
+ region,
+ dpt,
+ souscripteur,
+ createdBy
+FROM normalized
+${whereClause}
+ORDER BY ${sqlSort} ${order}, id ASC
+LIMIT ${meta.pageSize} OFFSET ${meta.offset};
+`);
+
+ const normalizedRows = rows.map(normalizeSqlRow);
+ const detailRows = normalizedRows.filter((row) => row.source === 'hors_grille');
+ let detailById = {};
+ if (detailRows.length) {
+ const idsSql = detailRows.map((row) => sqlQuote(row.id)).join(', ');
+ const audits = await runSqlJson(`
+SELECT
+ json_extract(data, '$.demandeId') AS demandeId,
+ json_extract(data, '$.actorMatricule') AS actorMatricule,
+ json_extract(data, '$.actorNom') AS actorNom,
+ json_extract(data, '$.actorPrenom') AS actorPrenom,
+ json_extract(data, '$.pricing.valeurAssuree') AS valeurAssuree,
+ json_extract(data, '$.pricing.taux') AS taux,
+ json_extract(data, '$.pricing.primeMinimum') AS primeMinimum,
+ json_extract(data, '$.pricing.cotisationHT') AS cotisationHT,
+ json_extract(data, '$.pricing.coutActe') AS coutActe,
+ json_extract(data, '$.pricing.cotisationTTC') AS cotisationTTC,
+ created
+FROM advalo_audit
+WHERE eventType IN ('create','update')
+ AND json_extract(data, '$.demandeId') IN (${idsSql})
+ORDER BY created DESC;
+`);
+ audits.forEach((audit) => {
+ const key = String(audit.demandeId || '');
+ if (!key || detailById[key]) return;
+ detailById[key] = {
+ actorMatricule: String(audit.actorMatricule || ''),
+ actorNom: String(audit.actorNom || ''),
+ actorPrenom: String(audit.actorPrenom || ''),
+ valeurAssuree: String(audit.valeurAssuree || ''),
+ taux: String(audit.taux || ''),
+ primeMinimum: String(audit.primeMinimum || ''),
+ cotisationHT: String(audit.cotisationHT || ''),
+ coutActe: String(audit.coutActe || ''),
+ cotisationTTC: String(audit.cotisationTTC || '')
+ };
+ });
+ }
+
+ return {
+ rows: normalizedRows.map((row) => ({
+ ...row,
+ details: {
+ marchandise: row.marchandise || '',
+ mode: row.mode || '',
+ depart: row.depart || '',
+ arrivee: row.arrivee || '',
+ valeurAssuree: detailById[row.id]?.valeurAssuree || row.capital || '',
+ taux: detailById[row.id]?.taux || '',
+ primeMinimum: detailById[row.id]?.primeMinimum || '',
+ cotisationHT: detailById[row.id]?.cotisationHT || '',
+ coutActe: detailById[row.id]?.coutActe || '',
+ cotisationTTC: detailById[row.id]?.cotisationTTC || row.tarif || '',
+ actorMatricule: detailById[row.id]?.actorMatricule || row.createdBy || '',
+ actorNom: detailById[row.id]?.actorNom || '',
+ actorPrenom: detailById[row.id]?.actorPrenom || ''
+ }
+ })),
+ totalRows: meta.totalRows,
+ totalPages: meta.totalPages,
+ page: meta.page,
+ pageSize: meta.pageSize
+ };
+}
+
+async function getCumul(filters, actor) {
+ ensureRole(actor);
+ const { page, pageSize } = parsePagination(filters);
+ const sort = normalizeSort(filters.sort, 'totalNonFacture', [
+ 'numContrat',
+ 'nomClient',
+ 'region',
+ 'dpt',
+ 'souscripteur',
+ 'totalAdvalo',
+ 'totalFacture',
+ 'totalNonFacture',
+ 'totalLignes'
+ ]);
+ const order = normalizeOrder(filters.order || 'desc');
+
+ const where = buildCommonWhere({
+ ...filters,
+ facture: 'true',
+ nonFacture: 'true',
+ deleguee: 'true',
+ nonDeleguee: 'true'
+ }, actor);
+ const whereClause = where.length ? `WHERE ${where.join(' AND ')}` : '';
+
+ const totalsRows = await runSqlJson(`
+${MERGED_CTE_SQL}
+SELECT
+ COALESCE(SUM(tarifNum), 0) AS totalAdvalo,
+ COALESCE(SUM(CASE WHEN statutNormalized = 'facture' THEN tarifNum ELSE 0 END), 0) AS totalFacture,
+ COALESCE(SUM(CASE WHEN statutNormalized = 'non_facture' THEN tarifNum ELSE 0 END), 0) AS totalNonFacture,
+ COUNT(*) AS totalLignes
+FROM normalized
+${whereClause};
+`);
+ const totals = {
+ totalAdvalo: Number(totalsRows[0]?.totalAdvalo || 0),
+ totalFacture: Number(totalsRows[0]?.totalFacture || 0),
+ totalNonFacture: Number(totalsRows[0]?.totalNonFacture || 0),
+ totalLignes: Number(totalsRows[0]?.totalLignes || 0)
+ };
+
+ const contractsCountRows = await runSqlJson(`
+${MERGED_CTE_SQL}
+SELECT COUNT(*) AS totalRows FROM (
+ SELECT numContrat
+ FROM normalized
+ ${whereClause}
+ GROUP BY numContrat
+) x;
+`);
+ const totalRows = Number(contractsCountRows[0]?.totalRows || 0);
+ const meta = buildPaginationMeta(totalRows, page, pageSize);
+
+ const rows = await runSqlJson(`
+${MERGED_CTE_SQL}
+SELECT
+ numContrat,
+ MAX(nomClient) AS nomClient,
+ MAX(region) AS region,
+ MAX(dpt) AS dpt,
+ MAX(souscripteur) AS souscripteur,
+ COALESCE(SUM(tarifNum), 0) AS totalAdvalo,
+ COALESCE(SUM(CASE WHEN statutNormalized = 'facture' THEN tarifNum ELSE 0 END), 0) AS totalFacture,
+ COALESCE(SUM(CASE WHEN statutNormalized = 'non_facture' THEN tarifNum ELSE 0 END), 0) AS totalNonFacture,
+ COUNT(*) AS totalLignes
+FROM normalized
+${whereClause}
+GROUP BY numContrat
+ORDER BY ${sort} ${order}, numContrat ASC
+LIMIT ${meta.pageSize} OFFSET ${meta.offset};
+`);
+
+ return {
+ ...totals,
+ rows: rows.map((row) => ({
+ numContrat: padContract(row.numContrat),
+ nomClient: row.nomClient || '',
+ region: row.region || '',
+ dpt: row.dpt || '',
+ souscripteur: row.souscripteur || '',
+ totalAdvalo: Number(row.totalAdvalo || 0),
+ totalFacture: Number(row.totalFacture || 0),
+ totalNonFacture: Number(row.totalNonFacture || 0),
+ totalLignes: Number(row.totalLignes || 0)
+ })),
+ meta: {
+ totalRows: meta.totalRows,
+ totalPages: meta.totalPages,
+ page: meta.page,
+ pageSize: meta.pageSize
+ }
+ };
+}
+
+function toGroupRows(rows) {
+ return rows.map((row) => ({
+ label: row.label || 'Non renseigné',
+ totalAdvalo: Number(row.totalAdvalo || 0),
+ totalFacture: Number(row.totalFacture || 0),
+ totalNonFacture: Number(row.totalNonFacture || 0),
+ totalLignes: Number(row.totalLignes || 0)
+ }));
+}
+
+async function getReporting(filters, actor) {
+ ensureReportingRole(actor);
+
+ const { page, pageSize } = parsePagination(filters);
+ const sort = normalizeSort(filters.sort, 'totalAdvalo', [
+ 'numContrat',
+ 'nomClient',
+ 'region',
+ 'souscripteur',
+ 'totalAdvalo',
+ 'totalFacture',
+ 'totalNonFacture',
+ 'totalLignes'
+ ]);
+ const order = normalizeOrder(filters.order || 'desc');
+
+ const where = buildCommonWhere(filters, actor);
+ const whereClause = where.length ? `WHERE ${where.join(' AND ')}` : '';
+
+ const totalsRows = await runSqlJson(`
+${MERGED_CTE_SQL}
+SELECT
+ COALESCE(SUM(tarifNum), 0) AS totalAdvalo,
+ COALESCE(SUM(CASE WHEN statutNormalized = 'facture' THEN tarifNum ELSE 0 END), 0) AS totalFacture,
+ COALESCE(SUM(CASE WHEN statutNormalized = 'non_facture' THEN tarifNum ELSE 0 END), 0) AS totalNonFacture,
+ COUNT(*) AS totalLignes
+FROM normalized
+${whereClause};
+`);
+ const totaux = {
+ totalAdvalo: Number(totalsRows[0]?.totalAdvalo || 0),
+ totalFacture: Number(totalsRows[0]?.totalFacture || 0),
+ totalNonFacture: Number(totalsRows[0]?.totalNonFacture || 0),
+ totalLignes: Number(totalsRows[0]?.totalLignes || 0)
+ };
+
+ const contractsCountRows = await runSqlJson(`
+${MERGED_CTE_SQL}
+SELECT COUNT(*) AS totalRows FROM (
+ SELECT numContrat
+ FROM normalized
+ ${whereClause}
+ GROUP BY numContrat
+) x;
+`);
+ const totalRows = Number(contractsCountRows[0]?.totalRows || 0);
+ const meta = buildPaginationMeta(totalRows, page, pageSize);
+
+ const rows = await runSqlJson(`
+${MERGED_CTE_SQL}
+SELECT
+ numContrat,
+ MAX(nomClient) AS nomClient,
+ MAX(region) AS region,
+ MAX(souscripteur) AS souscripteur,
+ COALESCE(SUM(tarifNum), 0) AS totalAdvalo,
+ COALESCE(SUM(CASE WHEN statutNormalized = 'facture' THEN tarifNum ELSE 0 END), 0) AS totalFacture,
+ COALESCE(SUM(CASE WHEN statutNormalized = 'non_facture' THEN tarifNum ELSE 0 END), 0) AS totalNonFacture,
+ COUNT(*) AS totalLignes
+FROM normalized
+${whereClause}
+GROUP BY numContrat
+ORDER BY ${sort} ${order}, numContrat ASC
+LIMIT ${meta.pageSize} OFFSET ${meta.offset};
+`);
+
+ const groupedClients = await runSqlJson(`
+${MERGED_CTE_SQL}
+SELECT
+ CASE WHEN trim(nomClient) = '' THEN 'Non renseigné' ELSE trim(nomClient) END AS label,
+ COALESCE(SUM(tarifNum), 0) AS totalAdvalo,
+ COALESCE(SUM(CASE WHEN statutNormalized = 'facture' THEN tarifNum ELSE 0 END), 0) AS totalFacture,
+ COALESCE(SUM(CASE WHEN statutNormalized = 'non_facture' THEN tarifNum ELSE 0 END), 0) AS totalNonFacture,
+ COUNT(*) AS totalLignes
+FROM normalized
+${whereClause}
+GROUP BY label
+ORDER BY totalAdvalo DESC
+LIMIT 20;
+`);
+
+ const groupedSouscripteurs = await runSqlJson(`
+${MERGED_CTE_SQL}
+SELECT
+ CASE WHEN trim(souscripteur) = '' THEN 'Non renseigné' ELSE trim(souscripteur) END AS label,
+ COALESCE(SUM(tarifNum), 0) AS totalAdvalo,
+ COALESCE(SUM(CASE WHEN statutNormalized = 'facture' THEN tarifNum ELSE 0 END), 0) AS totalFacture,
+ COALESCE(SUM(CASE WHEN statutNormalized = 'non_facture' THEN tarifNum ELSE 0 END), 0) AS totalNonFacture,
+ COUNT(*) AS totalLignes
+FROM normalized
+${whereClause}
+GROUP BY label
+ORDER BY totalAdvalo DESC
+LIMIT 20;
+`);
+
+ const groupedRegions = await runSqlJson(`
+${MERGED_CTE_SQL}
+SELECT
+ CASE WHEN trim(region) = '' THEN 'Non renseigné' ELSE trim(region) END AS label,
+ COALESCE(SUM(tarifNum), 0) AS totalAdvalo,
+ COALESCE(SUM(CASE WHEN statutNormalized = 'facture' THEN tarifNum ELSE 0 END), 0) AS totalFacture,
+ COALESCE(SUM(CASE WHEN statutNormalized = 'non_facture' THEN tarifNum ELSE 0 END), 0) AS totalNonFacture,
+ COUNT(*) AS totalLignes
+FROM normalized
+${whereClause}
+GROUP BY label
+ORDER BY totalAdvalo DESC
+LIMIT 20;
+`);
+
+ const actorStats = await runSqlJson(`
+SELECT
+ trim(
+ COALESCE(json_extract(data, '$.actorPrenom'), '') ||
+ CASE WHEN trim(COALESCE(json_extract(data, '$.actorPrenom'), '')) != '' THEN ' ' ELSE '' END ||
+ COALESCE(json_extract(data, '$.actorNom'), '')
+ ) AS actorName,
+ COALESCE(json_extract(data, '$.actorMatricule'), '') AS actorMatricule,
+ COALESCE(eventType, '') AS actionType,
+ COUNT(*) AS actionsCount
+FROM advalo_audit
+WHERE eventType IN ('create','update','delete_soft','facturation_batch','doc_avenant','doc_attestation','lookup_contrat')
+GROUP BY actorMatricule, actorName, actionType
+ORDER BY actionsCount DESC
+LIMIT 100;
+`);
+
+ return {
+ rows: rows.map((row) => ({
+ numContrat: padContract(row.numContrat),
+ nomClient: row.nomClient || '',
+ region: row.region || '',
+ souscripteur: row.souscripteur || '',
+ totalAdvalo: Number(row.totalAdvalo || 0),
+ totalFacture: Number(row.totalFacture || 0),
+ totalNonFacture: Number(row.totalNonFacture || 0),
+ totalLignes: Number(row.totalLignes || 0)
+ })),
+ meta: {
+ totalRows: meta.totalRows,
+ totalPages: meta.totalPages,
+ page: meta.page,
+ pageSize: meta.pageSize
+ },
+ totaux,
+ groupes: {
+ clients: toGroupRows(groupedClients),
+ souscripteurs: toGroupRows(groupedSouscripteurs),
+ regions: toGroupRows(groupedRegions)
+ },
+ actorStats: actorStats.map((row) => ({
+ actorMatricule: String(row.actorMatricule || ''),
+ actorName: String(row.actorName || '').trim() || 'Non renseigné',
+ actionType: String(row.actionType || ''),
+ actionsCount: Number(row.actionsCount || 0)
+ }))
+ };
+}
+
+async function createPonctuel(payload, actor) {
+ ensureRole(actor);
+ const required = [
+ 'typeFacturation',
+ 'numContrat',
+ 'marchandise',
+ 'capital',
+ 'depart',
+ 'arrivee',
+ 'dateDebut',
+ 'dateFin',
+ 'tarif',
+ 'taux',
+ 'primeMinimum',
+ 'cotisationHT',
+ 'cotisationTTC'
+ ];
+ const missing = required.filter((key) => !String(payload[key] || '').trim());
+ if (missing.length) {
+ const err = new Error(`Champs obligatoires manquants: ${missing.join(', ')}`);
+ err.status = 400;
+ throw err;
+ }
+
+ const typeFacturation = String(payload.typeFacturation || '').toLowerCase();
+ if (!['ponctuel', 'periodique'].includes(typeFacturation)) {
+ const err = new Error('Type de facturation invalide (ponctuel|periodique).');
+ err.status = 400;
+ throw err;
+ }
+ const numContrat = normalizeContractStrict(payload.numContrat, { required: true });
+ const modes = normalizeTransportModes(payload.mode);
+ if (!modes.length) {
+ const err = new Error('Sélectionne au moins un mode de transport.');
+ err.status = 400;
+ throw err;
+ }
+
+ const now = new Date();
+ const numDemande =
+ payload.numDemande || `${actor.userMatricule}_${now.toISOString().slice(0, 19).replace(/[T:]/g, '-')}`;
+
+ const row = {
+ sourceType: typeFacturation,
+ numDemande,
+ numClient: String(payload.numClient || '').trim(),
+ nomClient: String(payload.nomClient || '').trim(),
+ numContrat,
+ dateDemande: formatFrDate(now),
+ marchandise: String(payload.marchandise).trim(),
+ mode: modes.join(', '),
+ capital: String(payload.capital).trim(),
+ depart: String(payload.depart).trim(),
+ arrivee: String(payload.arrivee).trim(),
+ dateDebut: String(payload.dateDebut).trim(),
+ dateFin: String(payload.dateFin).trim(),
+ dateDebutIso: parseFrDate(payload.dateDebut)?.toISOString() || null,
+ dateFinIso: parseFrDate(payload.dateFin)?.toISOString() || null,
+ nomDevis: payload.nomDevis || 'Pas de nom de devis',
+ proprietaire: payload.proprietaire || 'Proprietaires multiple',
+ tarif: String(payload.tarif).trim(),
+ statutCommande: 'Validé',
+ statutFacturation: 'Non facturé',
+ isDeleted: false,
+ createdBy: actor.userMatricule,
+ region: actor.userRegion || ''
+ };
+
+ if (payload.facturer === true) {
+ await runQt550Axa({
+ matricule: actor.userMatricule,
+ numContrat: row.numContrat,
+ capital: row.capital,
+ tarif: row.tarif,
+ dateDebut: row.dateDebut,
+ dateFin: row.dateFin
+ });
+ row.statutFacturation = `Facturé ${formatFrDate(new Date())}`;
+ }
+ const created = await db.records.create('advalo_demande', row);
+ await logAudit('create', actor, {
+ sourceType: 'hors_grille',
+ demandeId: created.id,
+ numContrat: created.numContrat,
+ numClient: created.numClient || '',
+ pricing: {
+ valeurAssuree: String(payload.capital || ''),
+ taux: String(payload.taux || ''),
+ primeMinimum: String(payload.primeMinimum || ''),
+ cotisationHT: String(payload.cotisationHT || ''),
+ coutActe: String(payload.coutActe || ''),
+ cotisationTTC: String(payload.cotisationTTC || '')
+ },
+ mode: created.mode,
+ marchandise: created.marchandise,
+ depart: created.depart,
+ arrivee: created.arrivee,
+ statutFacturation: created.statutFacturation
+ });
+ return created;
+}
+
+function parseSourceId(composedId) {
+ const raw = String(composedId || '');
+ if (raw.startsWith('d:')) return { source: 'advalo_demande', id: raw.slice(2) };
+ if (raw.startsWith('g:')) return { source: 'advalo_deleguee', id: raw.slice(2) };
+ return { source: 'advalo_demande', id: raw };
+}
+
+async function getDemandByTypedId(composedId) {
+ const parsed = parseSourceId(composedId);
+ const record = await db.records.getOne(parsed.source, parsed.id);
+ return {
+ ...record,
+ id: parsed.id,
+ source: parsed.source === 'advalo_deleguee' ? 'deleguee' : 'hors_grille',
+ typedId: `${parsed.source === 'advalo_deleguee' ? 'g' : 'd'}:${parsed.id}`
+ };
+}
+
+async function getHistoriqueDetail(composedId, actor) {
+ ensureRole(actor);
+ const row = await getDemandByTypedId(composedId);
+ const [lastAudit] = await runSqlJson(`
+SELECT
+ json_extract(data, '$.actorMatricule') AS actorMatricule,
+ json_extract(data, '$.actorNom') AS actorNom,
+ json_extract(data, '$.actorPrenom') AS actorPrenom,
+ json_extract(data, '$.pricing.valeurAssuree') AS valeurAssuree,
+ json_extract(data, '$.pricing.taux') AS taux,
+ json_extract(data, '$.pricing.primeMinimum') AS primeMinimum,
+ json_extract(data, '$.pricing.cotisationHT') AS cotisationHT,
+ json_extract(data, '$.pricing.coutActe') AS coutActe,
+ json_extract(data, '$.pricing.cotisationTTC') AS cotisationTTC
+FROM advalo_audit
+WHERE json_extract(data, '$.demandeId') = ${sqlQuote(row.id)}
+ AND eventType IN ('create','update')
+ORDER BY created DESC
+LIMIT 1;
+`);
+ return {
+ ...row,
+ details: {
+ marchandise: row.marchandise || '',
+ mode: row.mode || '',
+ depart: row.depart || '',
+ arrivee: row.arrivee || '',
+ valeurAssuree: String(lastAudit?.valeurAssuree || row.capital || ''),
+ taux: String(lastAudit?.taux || ''),
+ primeMinimum: String(lastAudit?.primeMinimum || ''),
+ cotisationHT: String(lastAudit?.cotisationHT || ''),
+ coutActe: String(lastAudit?.coutActe || ''),
+ cotisationTTC: String(lastAudit?.cotisationTTC || row.tarif || ''),
+ actorMatricule: String(lastAudit?.actorMatricule || row.createdBy || ''),
+ actorNom: String(lastAudit?.actorNom || ''),
+ actorPrenom: String(lastAudit?.actorPrenom || '')
+ }
+ };
+}
+
+async function softDeleteDemande(composedId, actor) {
+ ensureRole(actor);
+ const row = await getDemandByTypedId(composedId);
+ if (row.source !== 'hors_grille') {
+ const err = new Error('Suppression autorisée uniquement sur les demandes hors grille.');
+ err.status = 400;
+ throw err;
+ }
+ if (normalizeStatus(row.statutFacturation) === 'facture') {
+ const err = new Error('Une demande facturée ne peut pas être supprimée.');
+ err.status = 400;
+ throw err;
+ }
+ const updated = await db.records.update('advalo_demande', row.id, { isDeleted: true });
+ await logAudit('delete_soft', actor, {
+ sourceType: 'hors_grille',
+ demandeId: row.id,
+ numContrat: row.numContrat || '',
+ numClient: row.numClient || ''
+ });
+ return updated;
+}
+
+function getDocDateStamp() {
+ const now = new Date();
+ const yyyy = now.getUTCFullYear();
+ const mm = String(now.getUTCMonth() + 1).padStart(2, '0');
+ const dd = String(now.getUTCDate()).padStart(2, '0');
+ const hh = String(now.getUTCHours()).padStart(2, '0');
+ const mi = String(now.getUTCMinutes()).padStart(2, '0');
+ const ss = String(now.getUTCSeconds()).padStart(2, '0');
+ return `${yyyy}-${mm}-${dd}-${hh}-${mi}-${ss}`;
+}
+
+function buildCommonDocContext(row, contractInfo = {}, actor = {}) {
+ const dateEmission = formatFrDate(new Date());
+ const rawContrat = row.numContrat || contractInfo.numContrat || '';
+ const normalizedContrat = padContract(rawContrat);
+ const numContrat = isAllZeroDigits(normalizedContrat) ? '' : normalizedContrat;
+ const rawClient = String(contractInfo.numClient || row.numClient || '').trim();
+ const numClient = safeDocValue(isAllZeroDigits(rawClient) ? '' : rawClient);
+ const nomClient = safeDocValue(contractInfo.nomClient || row.nomClient);
+ const adresseClient = safeDocValue(contractInfo.adresseClient || '');
+ const codePostal = safeDocValue(contractInfo.codePostalClient || '');
+ const actorDisplayName = buildActorDisplayName(actor);
+ const nomAgentSource = actorDisplayName || contractInfo.nomAgent || row.souscripteur || '';
+ const nomAgent = safeDocValue(withMatriculeSuffix(nomAgentSource, actor));
+ const numAgent = safeDocValue(contractInfo.numAgent || '');
+
+ return {
+ nomAgent,
+ numAgent,
+ adresseAgent: '',
+ postalAgent: '',
+ telAgent: '',
+ faxAgent: '',
+ nomClient,
+ adresseClient,
+ codePostal,
+ numContrat,
+ numClient,
+ dateEmission,
+ dateDebut: safeDocValue(row.dateDebut),
+ dateFin: safeDocValue(row.dateFin),
+ capitaux: prettyNumber(row.capital),
+ capitauxField: prettyNumber(row.capital),
+ cotisation: prettyNumber(row.tarif),
+ cotisationField: prettyNumber(row.tarif),
+ cotisationTTC: prettyNumber(row.tarif),
+ coutActeField: '36,00',
+ tauxField: '0,30',
+ taux: '0,30',
+ intervalle: `${safeDocValue(row.dateDebut)} au ${safeDocValue(row.dateFin)}`,
+ typeMarchandise: safeDocValue(row.marchandise),
+ marchandise: safeDocValue(row.marchandise),
+ modes: safeDocValue(row.mode),
+ mode: safeDocValue(row.mode),
+ depart: safeDocValue(row.depart),
+ arrivee: safeDocValue(row.arrivee),
+ primeMinimum: '15,00',
+ numDemande: safeDocValue(row.numDemande),
+ matricule: safeDocValue(actor.userMatricule || ''),
+ prenomSouscripteur: safeDocValue(actor.userFirstName || ''),
+ nomSouscripteur: safeDocValue(actor.userLastName || ''),
+ souscripteur: safeDocValue(withMatriculeSuffix(actorDisplayName, actor)),
+ regionSouscripteur: safeDocValue(actor.userRegion || ''),
+ listeDemandes: safeDocValue(row.numDemande || '')
+ };
+}
+
+async function generateDemandeDocument(composedId, type, actor) {
+ ensureRole(actor);
+ const row = await getDemandByTypedId(composedId);
+ const contractInfo = await lookupContract(row.numContrat, actor);
+ const context = buildCommonDocContext(row, contractInfo, actor);
+ const nowStamp = getDocDateStamp();
+ const clientSlug = sanitizeFilePart(context.nomClient || 'client');
+ const contractSlug = sanitizeFilePart(context.numContrat || 'contrat');
+
+ let templateName = '';
+ let docType = '';
+ let outputFolder = '';
+ let filename = '';
+ let replacements = {};
+
+ if (type === 'attestation') {
+ templateName = 'Certificat_Assurance_Advalo.docx';
+ docType = 'attestation';
+ outputFolder = path.join(ADVALO_DOCUMENTS_DIR, 'attestations');
+ filename = `Attestation_Advalo_${clientSlug}_${contractSlug}_${nowStamp}.docx`;
+ replacements = {
+ '«numContrat»': context.numContrat,
+ '«nomClient»': context.nomClient,
+ '«dateDebut»': context.dateDebut,
+ '«dateFin»': context.dateFin,
+ '«capital»': context.capitauxField,
+ '«mode»': context.modes,
+ '«depart»': context.depart,
+ '«arrivee»': context.arrivee,
+ '«marchandise»': context.marchandise,
+ '«numDemande»': context.numDemande,
+ '«dateEmission»': context.dateEmission
+ };
+ } else {
+ templateName = 'Avenant_Ponctuel.docx';
+ docType = 'avenant_ponctuel';
+ outputFolder = path.join(ADVALO_DOCUMENTS_DIR, 'ponctuel');
+ filename = `Avenant_Advalo_${clientSlug}_${contractSlug}_${nowStamp}.docx`;
+ replacements = {
+ nomAgent: context.nomAgent,
+ numAgent: context.numAgent,
+ adresseAgent: context.adresseAgent,
+ postalAgent: context.postalAgent,
+ telAgent: context.telAgent,
+ faxAgent: context.faxAgent,
+ nomClient: context.nomClient,
+ adresseClient: context.adresseClient,
+ codePostal: context.codePostal,
+ numContrat: context.numContrat,
+ numClient: context.numClient,
+ typeMarchandise: context.typeMarchandise,
+ modes: context.modes,
+ depart: context.depart,
+ arrivee: context.arrivee,
+ dateDebut: context.dateDebut,
+ dateFin: context.dateFin,
+ capitauxField: context.capitauxField,
+ tauxField: context.tauxField,
+ primeMinimum: context.primeMinimum,
+ cotisationField: context.cotisationField,
+ coutActeField: context.coutActeField,
+ cotisationTTC: context.cotisationTTC,
+ dateEmission: context.dateEmission,
+ numDemande: context.numDemande
+ };
+ }
+
+ const templatePath = path.join(ADVALO_TEMPLATE_DIR, templateName);
+ if (!fs.existsSync(templatePath)) {
+ const err = new Error(`Template introuvable: ${templateName}`);
+ err.status = 500;
+ throw err;
+ }
+
+ ensureDirectory(outputFolder);
+ const buffer = buildDocxFromTemplate(templatePath, replacements);
+ const filePath = path.join(outputFolder, filename);
+ fs.writeFileSync(filePath, buffer);
+ await saveAdvaloDocumentTrace({
+ type: docType,
+ filePath,
+ buffer,
+ demandeId: row.id,
+ batchId: ''
+ });
+ await logAudit(type === 'attestation' ? 'doc_attestation' : 'doc_avenant', actor, {
+ sourceType: row.source,
+ demandeId: row.id,
+ numContrat: row.numContrat || '',
+ numClient: row.numClient || '',
+ filename
+ });
+
+ return { filename, filePath, buffer };
+}
+
+async function generateBatchAvenant(batchId, actor) {
+ ensureRole(actor);
+ const batch = await db.records.getOne('advalo_facturation_batch', batchId);
+ const [batchAudit] = await runSqlJson(`
+SELECT json_extract(data, '$.includeTransportDetails') AS includeTransportDetails
+FROM advalo_audit
+WHERE eventType = 'facturation_batch'
+ AND json_extract(data, '$.batchId') = ${sqlQuote(batch.id)}
+ORDER BY created DESC
+LIMIT 1;
+`);
+ const includeTransportDetails = String(batchAudit?.includeTransportDetails || 'true') !== 'false';
+ const contractInfo = await lookupContract(batch.numContrat, actor);
+ const nowStamp = getDocDateStamp();
+ const clientSlug = sanitizeFilePart(contractInfo.nomClient || 'client');
+ const contractSlug = sanitizeFilePart(batch.numContrat || 'contrat');
+ const templatePath = path.join(ADVALO_TEMPLATE_DIR, 'Avenant.docx');
+
+ if (!fs.existsSync(templatePath)) {
+ const err = new Error('Template Avenant.docx introuvable.');
+ err.status = 500;
+ throw err;
+ }
+
+ const outputFolder = path.join(ADVALO_DOCUMENTS_DIR, 'periodique');
+ ensureDirectory(outputFolder);
+ const filename = `Avenant_Periodique_${clientSlug}_${contractSlug}_${nowStamp}.docx`;
+ const filePath = path.join(outputFolder, filename);
+ const actorDisplayName = buildActorDisplayName(actor);
+ const interlocuteur = withMatriculeSuffix(
+ actorDisplayName || contractInfo.nomAgent || '',
+ actor
+ );
+ const replacements = {
+ nomAgent: safeDocValue(interlocuteur),
+ adresseAgent: '',
+ postalAgent: '',
+ telAgent: '',
+ faxAgent: '',
+ nomClient: safeDocValue(contractInfo.nomClient || ''),
+ adresseClient: safeDocValue(contractInfo.adresseClient || ''),
+ codePostal: safeDocValue(contractInfo.codePostalClient || ''),
+ numContrat: padContract(batch.numContrat || ''),
+ numClient: safeDocValue(contractInfo.numClient || ''),
+ intervalle: `${safeDocValue(batch.dateDebut)} au ${safeDocValue(batch.dateFin)}`,
+ cotisationTTC: prettyNumber(batch.totalCotisation),
+ dateEmission: formatFrDate(new Date()),
+ capitauxField: prettyNumber(batch.totalCapitaux),
+ tauxField: '0,30',
+ cotisationField: prettyNumber(batch.totalCotisation),
+ coutActeField: '36,00',
+ listeDemandes: includeTransportDetails ? (Array.isArray(batch.demandeIds) ? batch.demandeIds.join(', ') : '') : ''
+ };
+
+ const buffer = buildDocxFromTemplate(templatePath, replacements);
+ fs.writeFileSync(filePath, buffer);
+ await saveAdvaloDocumentTrace({
+ type: 'avenant_periodique',
+ filePath,
+ buffer,
+ demandeId: '',
+ batchId: batch.id
+ });
+ await logAudit('doc_avenant', actor, {
+ sourceType: 'batch',
+ batchId: batch.id,
+ numContrat: batch.numContrat || '',
+ filename,
+ includeTransportDetails
+ });
+
+ return { filename, filePath, buffer };
+}
+
+async function facturerBatch(payload, actor) {
+ ensureRole(actor);
+ const sourceMode = String(payload.sourceMode || 'hors_grille').toLowerCase();
+ const includeTransportDetails = payload.includeTransportDetails !== false;
+ const removed = new Set(Array.isArray(payload.removedDemandeIds) ? payload.removedDemandeIds.filter(Boolean) : []);
+ const ids = (Array.isArray(payload.demandeIds) ? payload.demandeIds.filter(Boolean) : [])
+ .filter((id) => !removed.has(id));
+
+ if (!ids.length) {
+ const err = new Error('Aucune ligne sélectionnée pour la facturation.');
+ err.status = 400;
+ throw err;
+ }
+
+ const parsedIds = ids.map(parseSourceId);
+ if (sourceMode === 'hors_grille' && parsedIds.some((item) => item.source !== 'advalo_demande')) {
+ const err = new Error('Le mode Hors grille ne permet pas de facturer des lignes grille déléguée.');
+ err.status = 400;
+ throw err;
+ }
+
+ const rows = await Promise.all(
+ parsedIds.map(async (entry) => {
+ const row = await db.records.getOne(entry.source, entry.id);
+ return { ...row, _collection: entry.source, _typedId: `${entry.source === 'advalo_demande' ? 'd' : 'g'}:${entry.id}` };
+ })
+ );
+
+ const active = rows.filter((row) => row && !row.isDeleted && normalizeStatus(row.statutFacturation) !== 'facture');
+ if (!active.length) {
+ const err = new Error('Aucune ligne non facturée sur la période/contrat.');
+ err.status = 400;
+ throw err;
+ }
+
+ const numContrat = padContract(active[0].numContrat);
+ const dateDebut = active[0].dateDebut;
+ const dateFin = active[0].dateFin;
+ const fingerprint = hashFingerprint({
+ numContrat,
+ dateDebut,
+ dateFin,
+ sourceMode,
+ ids: active.map((row) => row._typedId).sort()
+ });
+
+ const existing = await db.records.getList('advalo_facturation_batch', 1, 1, {
+ filter: `fingerprint="${fingerprint}" && status="DONE"`
+ });
+ if (existing.totalItems > 0) {
+ return { idempotent: true, batch: existing.items[0] };
+ }
+
+ const totalCapitaux = active.reduce((acc, row) => acc + parseAmount(row.capital), 0);
+ const totalCotisation = active.reduce((acc, row) => acc + parseAmount(row.tarif), 0);
+
+ const batch = await db.records.create('advalo_facturation_batch', {
+ numContrat,
+ dateDebut,
+ dateFin,
+ sourceMode,
+ demandeIds: active.map((row) => row._typedId),
+ totalCapitaux,
+ totalCotisation,
+ fingerprint,
+ status: 'IN_PROGRESS',
+ createdBy: actor.userMatricule,
+ createdAt: new Date().toISOString()
+ });
+
+ try {
+ await runQt550Axa({
+ matricule: actor.userMatricule,
+ numContrat,
+ totalCapitaux,
+ totalCotisation,
+ dateDebut,
+ dateFin
+ });
+ const factDate = `Facturé ${formatFrDate(new Date())}`;
+
+ await Promise.all(
+ active.map((row) =>
+ db.records.update(row._collection, row.id, {
+ statutFacturation: factDate
+ })
+ )
+ );
+
+ const done = await db.records.update('advalo_facturation_batch', batch.id, {
+ status: 'DONE',
+ finishedAt: new Date().toISOString()
+ });
+ await logAudit('facturation_batch', actor, {
+ sourceType: sourceMode,
+ batchId: done.id,
+ numContrat,
+ numClient: active[0].numClient || '',
+ includeTransportDetails,
+ demandeIds: active.map((row) => row._typedId),
+ totals: { totalCapitaux, totalCotisation }
+ });
+
+ return { idempotent: false, batch: done };
+ } catch (error) {
+ await db.records.update('advalo_facturation_batch', batch.id, {
+ status: 'FAILED',
+ errorMessage: error.message,
+ finishedAt: new Date().toISOString()
+ });
+ throw error;
+ }
+}
+
+async function exportHistorique(filters, actor) {
+ ensureRole(actor);
+ const sort = normalizeSort(filters.sort, 'dateDebutSql', [
+ 'dateDebutSql',
+ 'dateDebutIso',
+ 'dateDebut',
+ 'dateFinSql',
+ 'dateFinIso',
+ 'dateFin',
+ 'numContrat',
+ 'numClient',
+ 'tarifNum',
+ 'tarif'
+ ]);
+ const sqlSort = ['dateDebut', 'dateDebutIso'].includes(sort)
+ ? 'dateDebutSql'
+ : ['dateFin', 'dateFinIso'].includes(sort)
+ ? 'dateFinSql'
+ : ['tarif', 'tarifNum'].includes(sort)
+ ? 'tarifNum'
+ : sort;
+ const order = normalizeOrder(filters.order);
+
+ const where = buildCommonWhere(filters, actor);
+ const whereClause = where.length ? `WHERE ${where.join(' AND ')}` : '';
+
+ const rows = await runSqlJson(`
+${MERGED_CTE_SQL}
+SELECT
+ id,
+ source,
+ sourceLabel,
+ numDemande,
+ numClient,
+ nomClient,
+ numContrat,
+ dateDebut,
+ dateFin,
+ dateDebutIso,
+ dateFinIso,
+ tarif,
+ statutFacturation,
+ statutNormalized
+FROM normalized
+${whereClause}
+ORDER BY ${sqlSort} ${order}, id ASC;
+`);
+
+ return toCsv(rows.map(normalizeSqlRow));
+}
+
+module.exports = {
+ lookupContract,
+ getHistorique,
+ getHistoriqueDetail,
+ getCumul,
+ createPonctuel,
+ softDeleteDemande,
+ facturerBatch,
+ generateDemandeDocument,
+ generateBatchAvenant,
+ getReporting,
+ exportHistorique
+};
diff --git a/ecole/src/templates/advalo/Avenant.docx b/ecole/src/templates/advalo/Avenant.docx
new file mode 100644
index 00000000..e3646f21
Binary files /dev/null and b/ecole/src/templates/advalo/Avenant.docx differ
diff --git a/ecole/src/templates/advalo/Avenant_Ponctuel.docx b/ecole/src/templates/advalo/Avenant_Ponctuel.docx
new file mode 100644
index 00000000..7d21c6c0
Binary files /dev/null and b/ecole/src/templates/advalo/Avenant_Ponctuel.docx differ
diff --git a/ecole/src/templates/advalo/Certificat_Assurance_Advalo.docx b/ecole/src/templates/advalo/Certificat_Assurance_Advalo.docx
new file mode 100644
index 00000000..a2c79833
Binary files /dev/null and b/ecole/src/templates/advalo/Certificat_Assurance_Advalo.docx differ
diff --git a/ecole/vbs/script_cl063/Connexion.xlsm b/ecole/vbs/script_cl063/Connexion.xlsm
new file mode 100644
index 00000000..3bea48df
Binary files /dev/null and b/ecole/vbs/script_cl063/Connexion.xlsm differ
diff --git a/ecole/vbs/script_cl063/ECFDDEViva.exe b/ecole/vbs/script_cl063/ECFDDEViva.exe
new file mode 100644
index 00000000..3abaed46
Binary files /dev/null and b/ecole/vbs/script_cl063/ECFDDEViva.exe differ
diff --git a/ecole/vbs/script_cl063/config/.gitkeep b/ecole/vbs/script_cl063/config/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/ecole/vbs/script_cl063/extract.vbs b/ecole/vbs/script_cl063/extract.vbs
new file mode 100644
index 00000000..ca93976b
--- /dev/null
+++ b/ecole/vbs/script_cl063/extract.vbs
@@ -0,0 +1,24 @@
+Option Explicit
+On Error Resume Next
+ExempleMacroExcel
+
+Sub ExempleMacroExcel()
+
+ Dim ApplicationExcel
+ Dim ClasseurExcel
+ Set ApplicationExcel = CreateObject("Excel.Application")
+
+ Dim WshShell, strCurDir
+ Set WshShell = CreateObject("WScript.Shell")
+ strCurDir = WshShell.CurrentDirectory
+
+
+ Set ClasseurExcel = ApplicationExcel.Workbooks.Open( strCurDir & "\vbs\script_cl063\Connexion.xlsm")
+ ApplicationExcel.Visible = False
+ ApplicationExcel.Run "CL063_AC800_sub" 'va lancer la macro
+ ApplicationExcel.Quit
+
+ Set ClasseurExcel = Nothing
+ Set ApplicationExcel = Nothing
+
+End Sub
\ No newline at end of file
diff --git a/ecole/vbs/script_pa025/Attestation.xlsm b/ecole/vbs/script_pa025/Attestation.xlsm
new file mode 100644
index 00000000..6c3644e1
Binary files /dev/null and b/ecole/vbs/script_pa025/Attestation.xlsm differ
diff --git a/ecole/vbs/script_pa025/ECFDDEViva.exe b/ecole/vbs/script_pa025/ECFDDEViva.exe
new file mode 100644
index 00000000..3abaed46
Binary files /dev/null and b/ecole/vbs/script_pa025/ECFDDEViva.exe differ
diff --git a/ecole/vbs/script_pa025/attestation.vbs b/ecole/vbs/script_pa025/attestation.vbs
new file mode 100644
index 00000000..70a4dfa7
--- /dev/null
+++ b/ecole/vbs/script_pa025/attestation.vbs
@@ -0,0 +1,24 @@
+Option Explicit
+On Error Resume Next
+ExempleMacroExcel
+
+Sub ExempleMacroExcel()
+
+ Dim ApplicationExcel
+ Dim ClasseurExcel
+ Set ApplicationExcel = CreateObject("Excel.Application")
+
+ Dim WshShell, strCurDir
+ Set WshShell = CreateObject("WScript.Shell")
+ strCurDir = WshShell.CurrentDirectory
+
+
+ Set ClasseurExcel = ApplicationExcel.Workbooks.Open( strCurDir & "\vbs\script_pa025\attestation.xlsm")
+ ApplicationExcel.Visible = False
+ ApplicationExcel.Run "PA025_AC800_sub" 'va lancer la macro
+ ApplicationExcel.Quit
+
+ Set ClasseurExcel = Nothing
+ Set ApplicationExcel = Nothing
+
+End Sub
\ No newline at end of file
diff --git a/ecole/vbs/script_pa025/config/.gitkeep b/ecole/vbs/script_pa025/config/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/ecole/vbs/script_qt550/Bordereau.xlsm b/ecole/vbs/script_qt550/Bordereau.xlsm
new file mode 100644
index 00000000..66813f58
Binary files /dev/null and b/ecole/vbs/script_qt550/Bordereau.xlsm differ
diff --git a/ecole/vbs/script_qt550/ECFDDEViva.exe b/ecole/vbs/script_qt550/ECFDDEViva.exe
new file mode 100644
index 00000000..3abaed46
Binary files /dev/null and b/ecole/vbs/script_qt550/ECFDDEViva.exe differ
diff --git a/ecole/vbs/script_qt550/bordereau.vbs b/ecole/vbs/script_qt550/bordereau.vbs
new file mode 100644
index 00000000..041d209e
--- /dev/null
+++ b/ecole/vbs/script_qt550/bordereau.vbs
@@ -0,0 +1,24 @@
+Option Explicit
+On Error Resume Next
+ExempleMacroExcel
+
+Sub ExempleMacroExcel()
+
+ Dim ApplicationExcel
+ Dim ClasseurExcel
+ Set ApplicationExcel = CreateObject("Excel.Application")
+
+ Dim WshShell, strCurDir
+ Set WshShell = CreateObject("WScript.Shell")
+ strCurDir = WshShell.CurrentDirectory
+
+
+ Set ClasseurExcel = ApplicationExcel.Workbooks.Open( strCurDir & "\vbs\script_qt550\Bordereau.xlsm")
+ ApplicationExcel.Visible = False
+ ApplicationExcel.Run "QT550_sub" 'va lancer la macro
+ ApplicationExcel.Quit
+
+ Set ClasseurExcel = Nothing
+ Set ApplicationExcel = Nothing
+
+End Sub
\ No newline at end of file
diff --git a/ecole/vbs/script_qt550/config/.gitkeep b/ecole/vbs/script_qt550/config/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/ecole/views/advalo.ejs b/ecole/views/advalo.ejs
new file mode 100644
index 00000000..ca8f93b5
--- /dev/null
+++ b/ecole/views/advalo.ejs
@@ -0,0 +1,326 @@
+
+
Advalorem
+
+
+
+
+
+
+
+
Advalorem est intégré à EasyTransport.
+
Parité V1: Hors grille (ponctuel/périodique), Facturation, Historique, Cumul, Reporting et documents.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Client: -
+
Intermédiaire: -
+
+
+
+
+
+
+
+
+
+
+
+
+ | Source | Demande | Client | Contrat | Date début | Tarif | Facturation |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Page 1 / 1
+
+
+
+
+
+
+
+ | Source | Demande | Client | Contrat | Date début | Date fin | Tarif | Statut | Actions |
+
+
+
+
+
+
+
+
+
+
Page 1 / 1
+
+
+ | Contrat | Client | Région | DPT | Souscripteur | Total Advalo | Total Facturé | Total Non facturé | Actions |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Page 1 / 1
+
+
+
+ | Contrat | Client | Région | Souscripteur | Total Advalo | Total Facturé | Total Non facturé | Lignes |
+
+
+
+
+
+
Qui a fait quoi
+
+ | Matricule | Acteur | Action | Occurrences |
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ecole/views/partials/navbar.ejs b/ecole/views/partials/navbar.ejs
index 8547600d..9bfcb1f2 100644
--- a/ecole/views/partials/navbar.ejs
+++ b/ecole/views/partials/navbar.ejs
@@ -17,10 +17,10 @@
EasyTransport 2.0.0
- Advalorem
+ Advalorem
Documentation
Liens utiles
Reporting
Admin
Déconnexion
-
\ No newline at end of file
+