import React, { useEffect, useMemo, useRef, useState } from “react”; /** * Vergelijkingstool – publieke dataset (zonder upload) * —————————————————- * Bezoeker kiest enkel zijn/haar mutualiteit (tabblad) en klikt op “Vergelijken”. * Het Excel-bestand wordt automatisch ingeladen vanaf een vaste URL (DATA_URL). * * Hoe uitrollen: * 1) Plaats je Excel op een publiek bereikbare URL (zelfde domein of CORS-enabled). * Voorbeeld: /assets/Concurrentieanalyse%20per%20ziekenfonds%202026.xlsx * 2) Zet hieronder DATA_URL naar de definitieve locatie. * 3) (Optioneel) Voor SharePoint/OneDrive: gebruik een direct download link (dl=1) of * configureer een proxy zodat ‘fetch’ een binary ArrayBuffer kan ophalen. * * Alles gebeurt lokaal in de browser; we POSTen niets. */ // === CONFIG === const DATA_URL = “https://usercontent.one/wp/www.lm-plus.be/wp-content/uploads/2026/02/Concurrentieanalyse-per-ziekenfonds-2026.xlsx”; // <-- pas aan naar jouw host-pad const CACHE_BUST = true; // zet op false als je niet wil busteren // ---- XLSX loader (CDN) async function loadScript(src) { return new Promise((resolve, reject) => { const existing = document.querySelector(`script[src=”${src}”]`); if (existing) return resolve(true); const s = document.createElement(“script”); s.src = src; s.async = true; s.onload = () => resolve(true); s.onerror = () => reject(new Error(`Kon script niet laden: ${src}`)); document.head.appendChild(s); }); } // —- helpers const stripUrls = (text) => { if (!text) return “”; let t = String(text); // verwijder Markdown-achtige url en losse urls t = t.replace(/\[[^\]]*\]\([^)]*\)/g, “”); t = t.replace(/https?:\/\/\S+/g, “”); t = t.replace(/file:\/\/\/\S+/g, “”); // verwijder losse [..] die enkel bronvermelding bevatten t = t.replace(/\[[^\]]*https?:\/\/[^\]]*\]/g, “”); // normaliseer spaties en streepjes t = t.replace(/\s+/g, ” “).replace(/\s*;\s*/g, “; “).trim(); return t; }; const truncate = (text, max = 140) => { const t = stripUrls(text); if (t.length <= max) return t; return t.slice(0, max - 1).trim() + "…"; }; const defaultTopics = [ "anticonceptie", "sportprikkel", "geboortepremie", "zwangerschapsbon", "logopedie", "psychologische begeleiding", "orthodontie", "optiek", "vaccins", "rookstopmiddelen", "jeugdbeweging", "school, speelplein, jeugdkampen", "tandzorgen", ]; export default function LMVergelijkerPubliek() { const [loading, setLoading] = useState(false); const [error, setError] = useState(""); const [wb, setWb] = useState(null); // XLSX workbook const [sheets, setSheets] = useState([]); // namen const [selectedSheet, setSelectedSheet] = useState(""); const [rows2D, setRows2D] = useState([]); // actieve sheet als 2D array const [topics, setTopics] = useState(defaultTopics); const [summary, setSummary] = useState([]); const [compact, setCompact] = useState(true); const ensureXLSX = async () => { if (window.XLSX) return true; await loadScript(“https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js”); return !!window.XLSX; }; // Laad workbook vanaf DATA_URL useEffect(() => { (async () => { try { setLoading(true); setError(“”); await ensureXLSX(); const url = CACHE_BUST ? `${DATA_URL}${DATA_URL.includes(“?”) ? “&” : “?”}_=${Date.now()}` : DATA_URL; const res = await fetch(url, { method: “GET” }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const buf = await res.arrayBuffer(); const _wb = window.XLSX.read(buf, { type: “array” }); setWb(_wb); setSheets(_wb.SheetNames || []); setSelectedSheet(_wb.SheetNames?.[0] || “”); } catch (e) { setError( `Kon het Excel-bestand niet laden. Controleer DATA_URL of CORS-instellingen.\nFout: ${e.message}` ); } finally { setLoading(false); } })(); }, []); // laad geselecteerde sheet als 2D array (header:1) useEffect(() => { if (!wb || !selectedSheet) return; const ws = wb.Sheets[selectedSheet]; if (!ws) return; const data = window.XLSX.utils.sheet_to_json(ws, { header: 1, defval: “” }); // filter lege rijen const cleaned = data .map((r) => r.map((c) => (typeof c === “string” ? c.trim() : c))) .filter((r) => r.some((c) => String(c).trim() !== “”)); setRows2D(cleaned); setSummary([]); // reset bij sheet-wissel }, [wb, selectedSheet]); const availableTopics = useMemo(() => { // veronderstel structuur: kolom 0 = rubriek/onderwerp const labels = new Set(); rows2D.forEach((r) => { const label = String(r[0] || “”).trim(); if (!label) return; const low = label.toLowerCase(); if (low.includes(“nieuw in”)) return; if (low.startsWith(“##”)) return; if ([“lm”, “lm plus”].includes(low)) return; if (label.length > 1) labels.add(label); }); return Array.from(labels); }, [rows2D]); // heuristiek: bepaal indexen van kolommen [rubriek, LM, andere mutualiteit] const columnMap = useMemo(() => { // default aanname: col0 = rubriek, col1 = LM, col2 = andere mutualiteit let iLabel = 0, iLM = 1, iOther = 2; // probeer header-rij te vinden met “LM” en/of “LM PLUS” for (let i = 0; i < Math.min(rows2D.length, 12); i++) { const row = rows2D[i].map((c) => String(c || “”).toLowerCase()); const hasLM = row.some((c) => c.includes(“lm plus”) || c === “lm” || c.includes(“liberela”)); if (hasLM) { // neem labelkolom = eerste niet-lege index < lmIndex, en other = eerstvolgende na lm const lmIdx = rows2D[i].findIndex((c) => String(c || “”).toLowerCase().includes(“lm”)); if (lmIdx !== -1) { iLM = lmIdx; // label: meestal kolom 0 iLabel = 0; // other: eerste kolom na LM met niet-lege waarden in de meeste rijen let bestIdx = lmIdx + 1; let bestCount = -1; const maxCols = Math.max(…rows2D.map((r) => r.length)); for (let j = lmIdx + 1; j < maxCols; j++) { let count = 0; for (let r = i + 1; r < Math.min(rows2D.length, i + 40); r++) { if (String(rows2D[r][j] || "").trim()) count++; } if (count > bestCount) { bestCount = count; bestIdx = j; } } iOther = bestIdx; break; } } } return { iLabel, iLM, iOther }; }, [rows2D]); const mutualityName = useMemo(() => selectedSheet || “Andere mutualiteit”, [selectedSheet]); const toggleTopic = (t) => { setTopics((prev) => (prev.includes(t) ? prev.filter((x) => x !== t) : […prev, t])); }; const runCompare = () => { const out = []; const { iLabel, iLM, iOther } = columnMap; rows2D.forEach((r) => { const label = String(r[iLabel] || “”).trim(); if (!label) return; const low = label.toLowerCase(); // enkel geselecteerde topics of – als geen selectie – alles uit defaultTopics const keep = topics.length ? topics.some((t) => low.includes(t.toLowerCase())) : defaultTopics.some((t) => low.includes(t)); if (!keep) return; const lmTxt = stripUrls(r[iLM]); const otherTxt = stripUrls(r[iOther]); if (!lmTxt && !otherTxt) return; out.push({ label, lm: lmTxt, other: otherTxt }); }); setSummary(out); }; return (
L

Vergelijker – LM vs. gekozen mutualiteit

Kies je mutualiteit en bekijk de samenvatting. Het Excel-bestand wordt automatisch geladen (geen upload nodig).

{/* Status */}

Dataset

{loading &&

Excel aan het laden…

} {error && (
{error}
)} {!loading && !error && sheets.length > 0 && (

Excel-bestand geladen. {sheets.length} tabblad(en) gevonden.

)}
{/* Keuze mutualiteit en topics */} {sheets.length > 0 && (

Stap 1 – Kies mutualiteit & rubrieken

{/* Topics */}
{availableTopics.map((t) => ( ))}
)} {/* Resultaat */} {summary.length > 0 && (

Resultaat – LM PLUS vs. {mutualityName}

Korte samenvatting van terugbetalingen, zonder hyperlinks.

    {summary.map(({ label, lm, other }) => (
  • {label}

    LM PLUS: {compact ? truncate(lm) : stripUrls(lm) || ‘—’}
    {mutualityName}: {compact ? truncate(other) : stripUrls(other) || ‘—’}
  • ))}

* Samenvatting gebaseerd op de inhoud van het Excel-bestand. Controleer steeds de actuele voorwaarden bij de betrokken mutualiteiten.

)}
); }