Gestion-des-Marches-RLA/services/calc.js

208 lines
7.7 KiB
JavaScript

/**
* services/calc.js
* Helpers partagés : calculs, formatage, seuils métier RLA
* Champs Baserow table 856 : id_marche, nature{value}, region, observation{value},
* taux_phy, taux_fin, avt_phy, avt_fin, m_min, m_max, tot_marche,
* date_debut, date_fin, delai_restant, debut_marche, date_fin_marche
*/
const SEUIL_STANDARD = parseFloat(process.env.SEUIL_STANDARD || 70);
const SEUIL_MODERNISATION = parseFloat(process.env.SEUIL_MODERNISATION || 50);
const SEUIL_CRITIQUE_PCT = parseFloat(process.env.SEUIL_CRITIQUE_PCT || 90);
const DELAI_CRITIQUE = parseInt(process.env.DELAI_CRITIQUE || 45, 10);
const DELAI_ATTENTION = parseInt(process.env.DELAI_ATTENTION || 90, 10);
// ─── Helpers Baserow select/multi-select ─────────────────────────────────────
/** Extrait la valeur d'un champ Baserow (select ou string) */
function selectVal(v) {
if (!v) return '';
if (typeof v === 'object' && v.value !== undefined) return String(v.value);
if (Array.isArray(v)) return v.map(x => (x.value !== undefined ? x.value : x)).join(', ');
return String(v);
}
// ─── Parseurs ────────────────────────────────────────────────────────────────
function parseNum(v) {
if (v === null || v === undefined || v === '') return 0;
if (typeof v === 'object') return 0; // objet Baserow non-numérique
const n = parseFloat(String(v).replace(/\s/g, '').replace(',', '.'));
return isNaN(n) ? 0 : n;
}
function parseDateFR(d) {
if (!d) return null;
const parts = String(d).split(/[\/\-]/);
if (parts.length === 3) {
const [a, b, c] = parts;
if (a.length === 4) return new Date(`${a}-${b}-${c}`);
if (c.length === 4) return new Date(`${c}-${b}-${a}`);
}
const dt = new Date(d);
return isNaN(dt.getTime()) ? null : dt;
}
// ─── Formatage ───────────────────────────────────────────────────────────────
function formatMontant(v) {
const n = parseNum(v);
if (n === 0) return '—';
return n.toLocaleString('fr-TN', { minimumFractionDigits: 0, maximumFractionDigits: 0 }) + ' DT';
}
function formatDateFR(d) {
const dt = parseDateFR(d);
if (!dt) return '—';
return dt.toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', year: 'numeric' });
}
function formatPct(v) {
const n = parseNum(v);
return n === 0 ? '0 %' : `${n.toFixed(1)} %`;
}
// ─── Statuts métier ───────────────────────────────────────────────────────────
function isCloture(r) {
const obs = selectVal(r.observation).toLowerCase();
return obs.includes('clôtur') || obs.includes('clotur') || !!r.date_cloture;
}
function getDelaiRestant(r) {
// Champ calculé Baserow
const dField = r.delai_restant;
if (dField !== null && dField !== undefined && dField !== '') {
const v = parseInt(String(dField), 10);
if (!isNaN(v)) return v;
}
// Calculer depuis date_fin
const fin = r.date_fin || r.date_fin_marche || r.datefin;
const dt = parseDateFR(fin);
if (!dt) return null;
return Math.ceil((dt - new Date()) / 86400000);
}
function niveauAlerte(delai) {
if (delai === null) return 'indéterminé';
if (delai <= DELAI_CRITIQUE) return 'critique';
if (delai <= DELAI_ATTENTION) return 'attention';
return 'normal';
}
function niveauAvancement(tauxPhy, nature) {
const t = parseNum(tauxPhy);
const nat = String(nature || '').toLowerCase();
const seuil = nat.includes('modern') ? SEUIL_MODERNISATION : SEUIL_STANDARD;
if (t >= SEUIL_CRITIQUE_PCT) return 'dépassé';
if (t >= seuil) return 'sous_avancement';
return 'normal';
}
/**
* Résultat financier du marché (pour pilotage proactif)
* Compare avt_fin (avancement financier en DT) vs m_min / m_max
*/
function resultatFinancier(r) {
const avt = parseNum(r.avt_fin);
const mMin = parseNum(r.m_min);
const mMax = parseNum(r.m_max ?? r.tot_marche);
if (avt === 0 && mMin === 0) return 'Non déterminé';
if (avt > mMax && mMax > 0) return 'Dépassement';
if (avt < mMin && mMin > 0) return 'Sous Min';
return 'Normal';
}
/**
* Résultat basé sur l'avancement PHYSIQUE du marché (pilotage proactif)
* Compare taux_phy vs seuils standards / critique
*/
function resultatPhysique(r) {
const taux = parseNum(r.taux_phy || r.avt_phy);
const nat = String(selectVal(r.nature) || '').toLowerCase();
const seuil = nat.includes('modern') ? SEUIL_MODERNISATION : SEUIL_STANDARD;
if (taux === 0) return 'Non déterminé';
if (taux >= SEUIL_CRITIQUE_PCT) return 'Dépassement';
if (taux >= seuil) return 'Normal';
return 'Sous Avancement';
}
function niveauRisque(r) {
const delai = getDelaiRestant(r);
const avt = parseNum(r.taux_phy || r.avt_phy);
const nd = niveauAlerte(delai);
if (nd === 'critique' || avt >= SEUIL_CRITIQUE_PCT) return 'critique';
if (nd === 'attention') return 'élevé';
if (avt >= SEUIL_STANDARD) return 'moyen';
return 'faible';
}
// ─── Normalisation d'un marché ───────────────────────────────────────────────
function buildRef(r) {
const base = r.id_marche || r.reference || String(r.id || '');
const reg = r.region_csc || r.region || '';
return reg ? `${base} - ${reg}` : base;
}
function normalizeMarche(r) {
const obsValue = selectVal(r.observation);
const natureValue = selectVal(r.nature);
const delaiRestant = getDelaiRestant(r);
const tauxPhy = parseNum(r.taux_phy || r.avt_phy);
const tauxFin = parseNum(r.taux_fin || r.avt_fin);
const montant = parseNum(r.tot_marche || r.totmarche || r.montant);
const mMin = parseNum(r.m_min);
const mMax = parseNum(r.m_max || r.tot_marche);
const avt_fin_raw = parseNum(r.avt_fin);
return {
id: r.id,
ref: buildRef(r),
projet: r.projet || '',
region: r.region || r.region_csc || '',
region_csc: r.region_csc || r.region || '',
entrepreneur: r.entrepreneur || '',
nature: natureValue,
statut: obsValue,
observation: obsValue,
lots: r.lots || '',
cloture: isCloture(r),
date_debut: formatDateFR(r.date_debut || r.debut_marche),
date_fin: formatDateFR(r.date_fin || r.date_fin_marche),
date_cloture: formatDateFR(r.date_cloture),
alerte_echeance: r.Alerte_Echeance || '',
montant_raw: montant,
montant: formatMontant(montant),
m_min_raw: mMin,
m_min: formatMontant(mMin),
m_max_raw: mMax,
m_max: formatMontant(mMax),
montant_proj_raw: avt_fin_raw,
montant_proj: formatMontant(avt_fin_raw),
taux_phy_raw: tauxPhy,
taux_phy: formatPct(tauxPhy),
taux_fin_raw: tauxFin,
taux_fin: formatPct(tauxFin),
delai_restant: delaiRestant,
niveau_alerte: niveauAlerte(delaiRestant),
niveau_avancement: niveauAvancement(tauxPhy, natureValue),
niveau_risque: niveauRisque(r),
resultat: resultatFinancier(r),
};
}
module.exports = {
SEUIL_STANDARD, SEUIL_MODERNISATION, SEUIL_CRITIQUE_PCT,
DELAI_CRITIQUE, DELAI_ATTENTION,
selectVal, parseNum, parseDateFR,
formatMontant, formatDateFR, formatPct,
isCloture, getDelaiRestant, niveauAlerte,
niveauAvancement, resultatFinancier, resultatPhysique, niveauRisque,
normalizeMarche, buildRef,
};