css
/* Les flèches entre tables, à droite de l'arbre (T2.3.22) */ .sdm-lx-canvas { position: relative; } .sdm-lx-arrows { position: absolute; left: 0; top: 0; overflow: visible; pointer-events: none; } .sdm-lx-arrow path { fill: none; stroke-width: 2; } .sdm-lx-arrow.is-natif path { stroke: #047857; } .sdm-lx-arrow.is-declare path { stroke: #6d28d9; stroke-dasharray: 6 4; } .sdm-lx-arrow.is-parente path { stroke: #6b7280; stroke-dasharray: 2 3; } .sdm-lx-arrow.is-natif circle, .sdm-lx-head-natif { fill: #047857; } .sdm-lx-arrow.is-declare circle, .sdm-lx-head-declare { fill: #6d28d9; } .sdm-lx-arrow.is-parente circle, .sdm-lx-head-parente { fill: #6b7280; } .sdm-lx-arrow.is-inactive path { stroke-dasharray: 3 3; } .sdm-lx-arrow.is-faded { opacity: .18; } .sdm-lx-arrow.is-faded path { stroke-width: 1.5; } .sdm-lx-arrow-label { position: absolute; z-index: 1; transform: translate(-50%, -50%); white-space: nowrap; cursor: pointer; padding: 1px 8px; border: 1px solid currentColor; border-radius: 999px; background: #fff; font-size: 11px; font-weight: 600; } .sdm-lx-arrow-label.sdm-lx-kind-natif { color: #047857; background: #fff; } .sdm-lx-arrow-label.sdm-lx-kind-declare { color: #6d28d9; background: #fff; } .sdm-lx-arrow-label.sdm-lx-kind-parente { color: #4b5563; background: #fff; } .sdm-lx-arrow-label.is-faded { opacity: .35; } .sdm-lx-arrow-label.is-hot { z-index: 2; } .sdm-lx-arrow-label:focus-visible { outline: 2px solid #4680ff; outline-offset: 1px; } .sdm-lx-goto { padding: 0; border: 0; background: none; color: #2f5fd0; font: inherit; font-weight: 700; cursor: pointer; } .sdm-lx-goto:hover { text-decoration: underline; } /** * sdm-fichiers.utils.js — des deux fichiers déposés à la sortie attendue par * l'aiguillage : `{ ok, format, headers, fr, en }`. * * Comme Standards, un seul format par référentiel : le fichier officiel * (T2.3.21, demande du chef — plus de fichier plat fabriqué à la main) : * * source le référentiel a un traitement Standards (`BUILDERS[ref]`) : * le fichier partagé par l'entreprise, transformé par ce * traitement, appelé comme Standards l'appelle selon sa sorte : * bilingual deux classeurs FR et EN ; * csv une extraction CSV (+ la dépendance) ; * workbook un classeur, les deux langues dedans (+ dépendances) ; * risks deux extractions, niveaux 1 à 3 puis niveau 4 ; * jobs une extraction, deux compléments FR / EN facultatifs. * Un fichier plat déposé pour ce référentiel est refusé. * * plat le référentiel n'a pas de traitement : son format officiel est * le plat de Standards, en-tête en ligne 1 portant les colonnes de * structure de la règle (code, parent, discriminant). Lu comme * Standards le lit (`parseImportWorkbook`). Les colonnes EN sont * remises dans l'ordre FR : l'aiguillage partage une seule ligne * d'en-tête. * * Les extensions acceptées sont celles de Standards (`EXTENSIONS`). Les * en-têtes des classeurs FR / EN source sont remis au texte exact que le * traitement Standards attend avant de l'appeler : par position pour * GOVERNANCE (`headersByPosition`, ligne 4, colonnes A à G), par nom sans * majuscules, accents ni s final pour les régulateurs (`alignHeaders`). */ const XLSX = require('xlsx'); const standards = require('../../../controllers/admin/settings/settings-standards.utils'); const integrations = require('../../../controllers/admin/settings/standards-integrations.utils'); /** * Les fichiers qu'un import peut recevoir : [clé, champ du formulaire]. * L'ordre fixe celui de la signature : FR puis EN d'abord, comme avant. */ const FILES = [ ['fr', 'fileFr'], ['en', 'fileEn'], ['source', 'file'], ['l4', 'fileL4'], ['stdFr', 'fileStdFr'], ['stdEn', 'fileStdEn'], ]; /** Ce que chaque sorte de traitement attend, pour l'aide et les messages. */ const EXPECTED = { bilingual: 'les deux classeurs source FR et EN', csv: 'l\'extraction CSV', workbook: 'le classeur source (FR et EN dedans)', risks: 'les deux extractions : niveaux 1 à 3, puis niveau 4', jobs: 'l\'extraction des postes (compléments FR / EN facultatifs)', }; /** Les sortes dont le traitement lit les référentiels dont il dépend. */ const WITH_DEPENDENCIES = ['csv', 'workbook', 'risks']; const text = (v) => (v === undefined || v === null ? '' : String(v).trim()); /** Les extensions que Standards accepte, par sorte et par fichier ; `flat` sans traitement. */ const XLS = ['.xlsx', '.xls']; const EXTENSIONS = { flat: { fr: XLS, en: XLS }, bilingual: { fr: ['.xlsx'], en: ['.xlsx'] }, csv: { source: ['.csv'] }, workbook: { source: XLS }, risks: { source: XLS, l4: XLS }, jobs: { source: ['.xlsx'], stdFr: ['.xlsx'], stdEn: ['.xlsx'] }, }; /** Les extensions par fichier pour ce traitement (ou le plat sans traitement). */ const extensionsOf = (builder) => EXTENSIONS[builder ? builder.kind : 'flat'] || {}; /** * Le premier fichier reçu dont l'extension n'est pas celle de Standards : * message de refus, ou null. `received` : [[clé, { originalname }]]. */ function checkExtensions(builder, received) { const allowed = extensionsOf(builder); for (const [key, f] of received) { const ok = allowed[key] || []; const name = text(f.originalname).toLowerCase(); if (!ok.some((ext) => name.endsWith(ext))) { return ok.length ? `${f.originalname} : seuls les fichiers ${ok.join(' ou ')} sont acceptés ici` : `${f.originalname} : ce fichier n'est pas attendu pour ce référentiel`; } } return null; } /** * Les en-têtes que le traitement Standards attend dans les classeurs FR / EN * source. Ceux de GOVERNANCE recopient `GOV_SOURCE_COLUMNS`, que Standards * n'exporte pas ; ceux des régulateurs viennent de `REGULATORS`. */ const SOURCE_HEADERS = { GOVERNANCE: { fr: ['Procédure', 'Processus', 'Règle', 'Exigence', 'Référence Catalogue', 'Standard(s) associé(s)', 'Modifications vs last version (Y/N)'], en: ['Procedure', 'Processus', 'Rule', 'Requirement', 'Catalogue Reference', 'Associated Standard(s)', 'Modifications vs last version (Y/N)'], }, ...Object.fromEntries(Object.entries(integrations.REGULATORS || {}).map(([code, spec]) => [code, spec.source])), }; /** Un en-tête comparé sans majuscules, accents, espaces doublés ni s final de chaque mot. */ const loose = (s) => text(s).normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase() .split(/\s+/).map((w) => w.replace(/s$/, '')).join(' '); /** * Remet au texte exact attendu la ligne d'en-tête d'un classeur dont les * colonnes ne diffèrent que par la casse, les accents ou un s final. La ligne * est la première des dix premières où toutes les colonnes attendues se * retrouvent. Rend le buffer d'origine s'il n'y a rien à changer. */ function alignHeaders(buffer, expected) { if (!buffer || !expected) return buffer; let wb; try { wb = XLSX.read(buffer, { type: 'buffer', cellDates: false }); } catch (_) { return buffer; } const ws = wb.Sheets[wb.SheetNames[0]]; if (!ws || !ws['!ref']) return buffer; const range = XLSX.utils.decode_range(ws['!ref']); const wanted = new Map(expected.map((c) => [loose(c), c])); for (let r = range.s.r; r <= Math.min(range.e.r, range.s.r + 9); r++) { const cells = []; for (let c = range.s.c; c <= range.e.c; c++) { const address = XLSX.utils.encode_cell({ r, c }); if (ws[address] && wanted.has(loose(ws[address].v))) cells.push(address); } const found = new Set(cells.map((a) => loose(ws[a].v))); if (found.size < wanted.size) continue; const changed = cells.filter((a) => ws[a].v !== wanted.get(loose(ws[a].v))); if (!changed.length) return buffer; changed.forEach((a) => { ws[a] = { t: 's', v: wanted.get(loose(ws[a].v)) }; }); return XLSX.write(wb, { type: 'buffer', bookType: 'xlsx' }); } return buffer; } /** * Les classeurs source lus par position, comme le traitement Standards les lit * (`GOV_COL`) : la ligne d'en-tête et le nombre de colonnes utiles (A à F). * Les titres changent d'une version et d'une langue à l'autre (« Associated * Standards », « Référence Catalogues ») : seule leur place compte. */ const BY_POSITION = { GOVERNANCE: { row: 4, useful: 6 } }; /** * Écrit en ligne `row` les en-têtes attendus, colonne par colonne, quel que * soit leur texte dans le fichier. Rend le buffer d'origine si une des * colonnes utiles est vide à cette ligne : le traitement le refusera alors. */ function headersByPosition(buffer, expected, { row, useful }) { if (!buffer || !expected) return buffer; let wb; try { wb = XLSX.read(buffer, { type: 'buffer', cellDates: false }); } catch (_) { return buffer; } const ws = wb.Sheets[wb.SheetNames[0]]; if (!ws || !ws['!ref']) return buffer; const address = (c) => XLSX.utils.encode_cell({ r: row - 1, c }); for (let c = 0; c < useful; c++) if (!text(ws[address(c)] && ws[address(c)].v)) return buffer; if (expected.every((name, c) => ws[address(c)] && ws[address(c)].v === name)) return buffer; expected.forEach((name, c) => { ws[address(c)] = { t: 's', v: name }; }); const range = XLSX.utils.decode_range(ws['!ref']); range.e.c = Math.max(range.e.c, expected.length - 1); ws['!ref'] = XLSX.utils.encode_range(range); return XLSX.write(wb, { type: 'buffer', bookType: 'xlsx' }); } /** Les colonnes qui signent un fichier plat pour cette règle. */ const structureColumns = (rule) => [rule.code, rule.parent, rule.column].filter(Boolean); /** Lit un classeur comme Standards ; `null` s'il est illisible. */ function readFlat(buffer, parse) { try { const { columns, data } = parse(buffer); return { columns: columns.map(text), rows: data }; } catch (_) { return null; } } /** Appelle le traitement d'une extraction source unique, selon sa sorte. */ function buildSource(builder, { sourceBuffer, l4Buffer, stdFrBuffer, stdEnBuffer, deps = {} }) { const dependencies = Object.keys(deps); switch (builder.kind) { case 'csv': { const rows = integrations.parseCsv(integrations.decode(sourceBuffer)); if (rows.length < 2) return { ok: false, message: 'fichier vide' }; const dep = dependencies[0]; return builder.build(rows, dep ? { depFr: deps[dep].fr, depEn: deps[dep].en, dependency: dep } : null); } case 'workbook': return builder.build(sourceBuffer, dependencies.length ? { deps } : undefined); case 'risks': if (!l4Buffer) return { ok: falseSource:https://dev.to/mi_rak_e7b22a0ae19a6797fd/css-5bmiPreviousWhich AWS certification to take first in Nepal, and what to do before itNextSamestate: one interaction, every device 📱💻
Related posts
first-name survives the draft. status becomes Statu
0dev.to
Treat agent-suggested any / as casts on UI props as a merge blocker
0dev.to
The Math Behind a Peptide Reconstitution Calculator (and How to Test It)
0dev.to
Guardrail para educación
0dev.to
Small input hints that make web tools easier to use
0dev.to UX
From Pretty Charts to Decisions: What I Learned Building Dashboards in Power BI
0dev.to Data