personnal/ecole/public/js/advalo-module.js

1085 lines
49 KiB
JavaScript

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 editModal = M.Modal.init(document.getElementById('advalo-edit-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();
};
// Mode "périodique" (parité v1 switchFacturation): pas de saisie ligne à ligne ->
// marchandise/départ/arrivée/mode forcés à "Multiple"/"Divers", taux=0, et masquage
// des champs transport + taux/prime. Le mode "ponctuel" restaure la saisie détaillée.
const PERIODIQUE_HIDDEN_IDS = ['p-marchandise', 'p-depart', 'p-arrivee', 'p-taux', 'p-primeMin'];
const applyTypeFacturation = () => {
const periodique = document.querySelector('input[name="p-typeFacturation"]:checked')?.value === 'periodique';
const modeBlock = document.getElementById('p-mode') ? document.getElementById('p-mode').closest('.col') : null;
if (periodique) {
document.getElementById('p-marchandise').value = 'Divers marchandise(s)';
document.getElementById('p-depart').value = 'Multiple';
document.getElementById('p-arrivee').value = 'Multiple';
document.getElementById('p-taux').value = '0';
document.querySelectorAll('.p-mode-check').forEach((c) => { c.checked = false; });
document.getElementById('p-mode').value = 'Divers mode(s)';
} else {
['p-marchandise', 'p-depart', 'p-arrivee'].forEach((id) => {
const el = document.getElementById(id);
if (el && ['Divers marchandise(s)', 'Multiple'].includes(el.value)) el.value = '';
});
document.getElementById('p-taux').value = '0.3';
syncModes();
}
PERIODIQUE_HIDDEN_IDS.forEach((id) => {
const wrap = document.getElementById(id) ? document.getElementById(id).closest('.input-field') : null;
if (wrap) wrap.style.display = periodique ? 'none' : '';
});
if (modeBlock) modeBlock.style.display = periodique ? 'none' : '';
recalcPonctuelPricing();
refreshTextFields();
};
// Édition d'une demande hors grille non facturée (parité v1 "Modification" de l'Historique).
const cleanNum = (value) => String(value == null ? '' : value).replace(/[^\d.,]/g, '').replace(',', '.');
const getSelectedEditModes = () => [...document.querySelectorAll('.e-mode-check:checked')].map((el) => el.value).filter((v) => TRANSPORT_MODES.includes(v));
const syncEditModes = () => { document.getElementById('e-mode').value = getSelectedEditModes().join(', '); };
const recalcEditPricing = () => {
const capital = parseAmount(document.getElementById('e-capital').value);
const taux = parseAmount(document.getElementById('e-taux').value);
const primeMin = parseAmount(document.getElementById('e-primeMin').value);
const coutActe = parseAmount(document.getElementById('e-coutActe').value);
const cotisationHT = Math.max((capital * taux) / 100, primeMin);
const cotisationTTC = cotisationHT + coutActe;
document.getElementById('e-cotisationHT').value = cotisationHT.toFixed(2);
document.getElementById('e-cotisationTTC').value = cotisationTTC.toFixed(2);
document.getElementById('e-tarif').value = cotisationTTC.toFixed(2);
refreshTextFields();
};
const openEditModal = (typedId, row, details) => {
const d = details || {};
document.getElementById('e-id').value = typedId;
document.getElementById('e-marchandise').value = d.marchandise || row.marchandise || '';
document.getElementById('e-depart').value = d.depart || row.depart || '';
document.getElementById('e-arrivee').value = d.arrivee || row.arrivee || '';
document.getElementById('e-dateDebut').value = row.dateDebut || '';
document.getElementById('e-dateFin').value = row.dateFin || '';
const modes = String(d.mode || row.mode || '').split(',').map((s) => s.trim());
document.querySelectorAll('.e-mode-check').forEach((c) => { c.checked = modes.includes(c.value); });
syncEditModes();
document.getElementById('e-capital').value = cleanNum(d.valeurAssuree || row.capital);
document.getElementById('e-taux').value = d.taux ? cleanNum(d.taux) : '0.3';
document.getElementById('e-primeMin').value = d.primeMinimum ? cleanNum(d.primeMinimum) : '15';
document.getElementById('e-coutActe').value = d.coutActe ? cleanNum(d.coutActe) : '36';
// On préserve les montants existants (pas de recalcul à l'ouverture) ; le recalcul
// ne se déclenche que si l'utilisateur modifie capital/taux/prime/coût d'acte.
const ttc = cleanNum(d.cotisationTTC || row.tarif);
document.getElementById('e-cotisationHT').value = cleanNum(d.cotisationHT || row.tarif);
document.getElementById('e-cotisationTTC').value = ttc;
document.getElementById('e-tarif').value = ttc;
document.getElementById('e-form-error').textContent = '';
refreshTextFields();
initDateFields();
editModal.open();
};
const submitEdit = async () => {
const errorSlot = document.getElementById('e-form-error');
errorSlot.textContent = '';
syncEditModes();
const required = ['e-marchandise', 'e-depart', 'e-arrivee', 'e-dateDebut', 'e-dateFin', 'e-capital', 'e-taux', 'e-primeMin', 'e-cotisationHT', 'e-cotisationTTC'];
if (!requireFields(required)) {
errorSlot.textContent = 'Complète les champs obligatoires.';
return;
}
if (!document.getElementById('e-mode').value.trim()) {
errorSlot.textContent = 'Sélectionne au moins un mode de transport.';
return;
}
const id = document.getElementById('e-id').value;
try {
const payload = {
marchandise: document.getElementById('e-marchandise').value.trim(),
mode: document.getElementById('e-mode').value.trim(),
depart: document.getElementById('e-depart').value.trim(),
arrivee: document.getElementById('e-arrivee').value.trim(),
dateDebut: document.getElementById('e-dateDebut').value.trim(),
dateFin: document.getElementById('e-dateFin').value.trim(),
capital: document.getElementById('e-capital').value.trim(),
taux: document.getElementById('e-taux').value.trim(),
primeMinimum: document.getElementById('e-primeMin').value.trim(),
coutActe: document.getElementById('e-coutActe').value.trim(),
cotisationHT: document.getElementById('e-cotisationHT').value.trim(),
cotisationTTC: document.getElementById('e-cotisationTTC').value.trim(),
tarif: document.getElementById('e-cotisationTTC').value.trim()
};
await api(`/advalo/demande/${encodeURIComponent(id)}`, { method: 'PUT', body: JSON.stringify(payload) });
editModal.close();
toast('Demande modifiée.', 'green');
await loadHistorique();
} catch (error) {
errorSlot.textContent = error.message;
toast(error.message);
}
};
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 `
<tr id="${detailId}" class="advalo-history-detail-row">
<td colspan="10">
<div class="card-panel" style="margin:6px 0;">
<div class="row" style="margin-bottom:0;">
<div class="col s12 m3"><b>Marchandise:</b> ${details.marchandise || '-'}</div>
<div class="col s12 m3"><b>Modes:</b> ${details.mode || '-'}</div>
<div class="col s12 m3"><b>Départ:</b> ${details.depart || '-'}</div>
<div class="col s12 m3"><b>Arrivée:</b> ${details.arrivee || '-'}</div>
</div>
<div class="row" style="margin-bottom:0;">
<div class="col s12 m2"><b>Valeur assurée:</b> ${details.valeurAssuree || '-'}</div>
<div class="col s12 m2"><b>Taux:</b> ${details.taux || '-'}</div>
<div class="col s12 m2"><b>Prime mini:</b> ${details.primeMinimum || '-'}</div>
<div class="col s12 m2"><b>Cotisation HT:</b> ${details.cotisationHT || '-'}</div>
<div class="col s12 m2"><b>Coût acte:</b> ${details.coutActe || '-'}</div>
<div class="col s12 m2"><b>Cotisation TTC:</b> ${details.cotisationTTC || '-'}</div>
</div>
<div class="row" style="margin-bottom:0;">
<div class="col s12 m4"><b>Acteur:</b> ${(details.actorPrenom || '')} ${(details.actorNom || '')} ${details.actorMatricule ? `(${details.actorMatricule})` : ''}</div>
<div class="col s12 m8 right-align">
${row.source === 'hors_grille' && String(row.statutFacturation || '').toLowerCase().includes('non') ? `<button class="btn green darken-2 advalo-h-edit" data-id="${typedId}">Modifier</button> <button class="btn red darken-3 advalo-h-delete" data-id="${typedId}">Supprimer</button>` : ''}
</div>
</div>
</div>
</td>
</tr>`;
};
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', `
<tr data-typed-id="${typedId}">
<td><button class="btn-flat advalo-h-expand" data-id="${typedId}" style="color:#1a237e;">▸</button></td>
<td>${sourceLabel(row.source)}</td>
<td>${row.numDemande || ''}</td>
<td>${row.numClient || ''}</td>
<td>${row.numContrat || ''}</td>
<td>${row.dateDebut || ''}</td>
<td>${row.dateFin || ''}</td>
<td>${row.tarif || ''}</td>
<td>${row.statutFacturation || ''}</td>
<td>
<button class="btn indigo darken-4 white-text advalo-doc-avenant" data-id="${typedId}">Avenant</button>
<button class="btn teal darken-1 white-text advalo-doc-attestation" data-id="${typedId}">Attestation</button>
</td>
</tr>
`);
});
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);
}
});
});
document.querySelectorAll('.advalo-h-edit').forEach((editBtn) => {
editBtn.addEventListener('click', () => openEditModal(editBtn.dataset.id, row || {}, details));
});
} 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', `
<tr>
<td><label><input type="checkbox" class="filled-in advalo-fact-check" value="${typedId}" checked><span></span></label></td>
<td>${sourceLabel(row.source)}</td>
<td>${row.numDemande || ''}</td>
<td>${row.numClient || ''}</td>
<td>${row.numContrat || ''}</td>
<td>${row.dateDebut || ''}</td>
<td>${row.tarif || ''}</td>
<td>${row.statutFacturation || ''}</td>
</tr>
`);
});
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', `
<tr>
<td>${row.numContrat || ''}</td>
<td>${row.nomClient || ''}</td>
<td>${row.region || ''}</td>
<td>${row.dpt || ''}</td>
<td>${row.souscripteur || ''}</td>
<td>${fmt(row.totalAdvalo)}</td>
<td>${fmt(row.totalFacture)}</td>
<td>${fmt(row.totalNonFacture)}</td>
<td>
<button class="btn indigo darken-4 white-text advalo-cumul-hist" data-contrat="${row.numContrat || ''}">Voir historique filtré</button>
<button class="btn teal darken-1 white-text advalo-cumul-fact" data-contrat="${row.numContrat || ''}">Aller facturation</button>
</td>
</tr>
`);
});
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', `
<tr>
<td>${row.numContrat || ''}</td>
<td>${row.nomClient || ''}</td>
<td>${row.region || ''}</td>
<td>${row.souscripteur || ''}</td>
<td>${fmt(row.totalAdvalo)}</td>
<td>${fmt(row.totalFacture)}</td>
<td>${fmt(row.totalNonFacture)}</td>
<td>${row.totalLignes || 0}</td>
</tr>`);
});
const actorBody = document.getElementById('reporting-actors-body');
actorBody.innerHTML = '';
(data.actorStats || []).forEach((a) => {
actorBody.insertAdjacentHTML('beforeend', `
<tr>
<td>${a.actorMatricule || ''}</td>
<td>${a.actorName || ''}</td>
<td>${a.actionType || ''}</td>
<td>${a.actionsCount || 0}</td>
</tr>
`);
});
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(); }));
document.querySelectorAll('input[name="p-typeFacturation"]').forEach((radio) => radio.addEventListener('change', applyTypeFacturation));
['p-capital', 'p-taux', 'p-primeMin', 'p-coutActe'].forEach((id) => {
const input = document.getElementById(id);
if (input) input.addEventListener('input', recalcPonctuelPricing);
});
// Modal d'édition (Historique) : recalcul + sauvegarde.
document.querySelectorAll('.e-mode-check').forEach((input) => input.addEventListener('change', () => { syncEditModes(); refreshTextFields(); }));
['e-capital', 'e-taux', 'e-primeMin', 'e-coutActe'].forEach((id) => {
const input = document.getElementById(id);
if (input) input.addEventListener('input', recalcEditPricing);
});
const editSaveBtn = document.getElementById('advalo-edit-save');
if (editSaveBtn) editSaveBtn.addEventListener('click', submitEdit);
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;
applyTypeFacturation();
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();
applyTypeFacturation();
recalcPonctuelPricing();
refreshTextFields();
activatePanel('advalo-tab-accueil').catch((error) => toast(error.message));
});