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 (
{/* Status */}
{/* Keuze mutualiteit en topics */}
{sheets.length > 0 && (
{/* Topics */}
)}
{/* Resultaat */}
{summary.length > 0 && (
)}
);
}
L
Vergelijker – LM vs. gekozen mutualiteit
Kies je mutualiteit en bekijk de samenvatting. Het Excel-bestand wordt automatisch geladen (geen upload nodig).
Dataset
{loading &&Excel aan het laden…
} {error && (
{error}
)}
{!loading && !error && sheets.length > 0 && (
Excel-bestand geladen. {sheets.length} tabblad(en) gevonden.
)}Stap 1 – Kies mutualiteit & rubrieken
{availableTopics.map((t) => (
))}
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.