import ReactDOM from "react-dom/client";
import React, { useState, useMemo, useRef, useEffect } from "react";
import { Plus, Trash2, Copy, Check, AlertTriangle, Save, FolderOpen, ChevronUp, ChevronDown, Settings, Calculator, Search, RotateCcw, Download, Upload, Bookmark, GitCompare } from "lucide-react";

const TEF_BLUE = "#0019A5", TEF_BLUE_2 = "#0066FF", TEF_CYAN = "#00A9E0", TEF_GRAY = "#333333", TEF_RED = "#c0392b", TEF_BG = "#F4F6FB";
const lbl = "block text-xs font-semibold mb-1";
const ctl = "w-full px-2 py-2 text-sm rounded border border-gray-300 bg-white focus:outline-none";

function Field({ label, children, className = "" }) {
  return <div className={className}>{label && <label className={lbl} style={{ color: TEF_BLUE }}>{label}</label>}{children}</div>;
}

const D_CU = ["Asymmetrische Kupfer-Anbindung inkl. Business-Router", "Bis zu 5 feste IPv4 Adressen kostenfrei inklusive"];
const D_UGG = ["Asymmetrische UGG Glasfaser-Anbindung inkl. Business-Router", "Bis zu 5 feste IPv4 Adressen kostenfrei inklusive"];
const D_FTTH = ["Asymmetrische Telekom Glasfaser-Anbindung inkl. Business-Router", "Bis zu 5 feste IPv4 Adressen kostenfrei inklusive"];
const D_LINE = ["Hochverfügbare, symmetrische Glasfaser-Anbindung inkl. Business-Router", "Dedizierte Anbindung, kein shared Medium", "Bis zu 5 feste IPv4 Adressen kostenfrei inklusive, mehr auf Wunsch ebenfalls kostenfrei"];
const D_BACKUP = ["Automatisiertes Mobilfunk-Backup innerhalb unseres Business Routers", "Übernahme Ihrer IP-Adressen auf die Backup-Anbindung", "Übernahme von bis zu 16 Sprachkanälen auf die Backup-Anbindung", "Automatischer Reconnect über Originär-Anbindung nach Entstörung", "Datenvolumen unbegrenzt"];
const D_EXPRESS = ["Schnellstmögliche Aktivierung Ihrer Anbindung über eine Mobilfunk-Strecke", "Schnellstmögliche Mitteilung und Schaltung Ihrer IP-Adressen"];
const D_MOBILE = ["Mobilfunk-Anbindung für Ihren Standort", "inkl. bis zu 5 festen IPv4 Adressen", "inkl. unbegrenztem Datenvolumen", "inkl. Business-Hardware"];
const D_WAVE = ["Bereitstellung und Installation des Anschlusses und des Endgerätes (Business-Router)", "Business Light: Business-Hotline Mo–Fr 8–18 Uhr, Entstörung innerhalb von 24 Stunden"];
const WAVE_LOS_PRICE = 999;
const WAVE_SUPPORT_PRICE = 150;
const D_BASIS = ["Zugang zur virtuellen Telefonanlage inkl. 5 Lizenzen", "Flatrate in alle dt. Mobilfunk- & Festnetze", "Zugang zum Cloudya-Client via Web, Smartphone, Tablet oder als installierte Applikation"];
const D_SIP = ["Cloud Sprachkanäle powered by NFON", "Erreichbar über jeden Internet-Zugang", "Inkl. Flatrate in alle dt. Mobilfunk- & Festnetze", "Preise für internationale Gespräche: https://www.o2business.de/content/dam/b2bchannels/de/pdfs-o2-business/teams-telefonie-preisliste.pdf"];
const D_MDM = ["Umfassender Schutz für Ihre mobilen Endgeräte", "Management per Remote im Browser", "DSGVO-Konform"];
const D_MDM_MANAGED = [...D_MDM, "inkl. Management durch Telefónica"];

const LZ = [24, 36, 48, 60], DP_LZ = [36, 48, 60];
const BACKUP_PRICE = { vdsl: { m24: 31, m36: 25 }, line: { m24: 48, m36: 41 } };
const EXPRESS_PRICE = { vdsl: 199, line: 399 };
const backupMonthly = (kind, term) => { const b = BACKUP_PRICE[kind]; return b ? (term >= 36 ? b.m36 : b.m24) : 0; };

const KANAL_STEPS = [0, 2, 4, 6, 8, 10, 12, 14, 16, 20, 30, 40, 50, 60, 70, 80, 90, 100, 120, 150, 180, 210, 240, 270, 300, 350, 400, 450, 500, 550, 600];
const RICHTUNGEN = [
  { key: "fest", label: "dt. Festnetz", fair: 2, poolCt: 0.7, fairText: "Flatrate in Richtung dt. Festnetz" },
  { key: "mob", label: "dt. Mobilfunk", fair: 6, poolCt: 4.5, fairText: "Flatrate in Richtung dt. Mobilfunk" },
  { key: "eu", label: "EU Plus", fair: 3.5, poolCt: 2.25, fairText: "500 Minuten je Sprachkanal in die EU Plus Länder inklusive" },
  { key: "world", label: "World Select", fair: 11, poolCt: 8, fairText: "250 Minuten je Sprachkanal in die World Select Länder inklusive" },
];
const blankKanalTarife = () => ({ fest: { mode: "standard", min: 0 }, mob: { mode: "standard", min: 0 }, eu: { mode: "standard", min: 0 }, world: { mode: "standard", min: 0 } });

const MDM_STAFFEL = [{ min: 100, pct: 25 }, { min: 50, pct: 20 }, { min: 25, pct: 15 }, { min: 10, pct: 10 }, { min: 1, pct: 5 }];
const staffelPct = (staffel, qty) => { for (const t of staffel || []) if (qty >= t.min) return t.pct; return 0; };

const officePrice = (p, managed) => managed && p.mgdPrice != null ? p.mgdPrice : p.loPrice;
const officeMax = (p, managed) => managed && p.mgdPrice != null ? p.mgdMax : p.loMax;
const officeNoMgmt = (p, managed) => !!managed && p.mgdPrice == null;

const DP_TABLE_KOMBI = [
  { pct: 20, min: 2628 }, { pct: 21, min: 3132 }, { pct: 22, min: 4140 }, { pct: 23, min: 5148 }, { pct: 24, min: 6156 },
  { pct: 25, min: 7164 }, { pct: 26, min: 8172 }, { pct: 27, min: 9180 }, { pct: 28, min: 10188 }, { pct: 29, min: 11196 },
  { pct: 30, min: 12204 }, { pct: 31, min: 13212 }, { pct: 32, min: 14220 }, { pct: 33, min: 15228 }, { pct: 34, min: 16236 },
  { pct: 35, min: 17244 }, { pct: 36, min: 18252 }, { pct: 37, min: 19260 }, { pct: 38, min: 20268 }, { pct: 39, min: 21276 },
  { pct: 40, min: 22284 }, { pct: 41, min: 22788 }, { pct: 42, min: 23796 }, { pct: 43, min: 24804 }, { pct: 44, min: 25812 },
  { pct: 45, min: 26820 }, { pct: 46, min: 27828 }, { pct: 47, min: 28836 }, { pct: 48, min: 29844 },
];
const DP_TABLE_SOLO = [
  { pct: 15, min: 2628 }, { pct: 16, min: 3132 }, { pct: 17, min: 4140 }, { pct: 18, min: 5148 }, { pct: 19, min: 6156 },
  { pct: 20, min: 7164 }, { pct: 21, min: 8172 }, { pct: 22, min: 9180 }, { pct: 23, min: 10188 }, { pct: 24, min: 11196 },
  { pct: 25, min: 12204 }, { pct: 26, min: 13212 }, { pct: 27, min: 14220 }, { pct: 28, min: 15228 }, { pct: 29, min: 16236 },
  { pct: 30, min: 17244 }, { pct: 31, min: 18252 }, { pct: 32, min: 19260 }, { pct: 33, min: 20268 }, { pct: 34, min: 21276 },
  { pct: 35, min: 22284 }, { pct: 36, min: 22788 }, { pct: 37, min: 23796 }, { pct: 38, min: 24804 }, { pct: 39, min: 25812 },
  { pct: 40, min: 26820 }, { pct: 41, min: 27828 }, { pct: 42, min: 28836 }, { pct: 43, min: 29844 }, { pct: 44, min: 30852 },
  { pct: 45, min: 31860 }, { pct: 46, min: 32868 }, { pct: 47, min: 33876 }, { pct: 48, min: 34884 },
];
const dpMaxPct = (umsatz, kombi) => { const t = kombi ? DP_TABLE_KOMBI : DP_TABLE_SOLO; let p = t[0].pct; for (const r of t) if (umsatz >= r.min) p = r.pct; return p; };

const DB_ALLIP = { label: "All IP Business", products: [
  { inetFlat: true, id: "cu-6", maxKanal: 2, name: "Asymmetrisch bis 6 Mbit/s", desc: D_CU, laufzeiten: LZ, m24: 45, m36: 40, o24: 99, o36: 0, standort: true, extrasKind: "vdsl" },
  { inetFlat: true, id: "cu-16", maxKanal: 6, name: "Asymmetrisch bis 16 Mbit/s", desc: D_CU, laufzeiten: LZ, m24: 45, m36: 40, o24: 99, o36: 0, standort: true, extrasKind: "vdsl" },
  { inetFlat: true, id: "cu-25", maxKanal: 20, name: "Asymmetrisch bis 25 Mbit/s", desc: D_CU, laufzeiten: LZ, m24: 45, m36: 40, o24: 99, o36: 0, standort: true, extrasKind: "vdsl" },
  { inetFlat: true, id: "cu-50", maxKanal: 30, name: "Asymmetrisch bis 50 Mbit/s", desc: D_CU, laufzeiten: LZ, m24: 45, m36: 40, o24: 99, o36: 0, standort: true, extrasKind: "vdsl" },
  { inetFlat: true, id: "cu-100", maxKanal: 60, name: "Asymmetrisch bis 100 Mbit/s", desc: D_CU, laufzeiten: LZ, m24: 45, m36: 40, o24: 99, o36: 0, standort: true, extrasKind: "vdsl" },
  { inetFlat: true, id: "cu-175", maxKanal: 60, name: "Asymmetrisch bis 175 Mbit/s", desc: D_CU, laufzeiten: LZ, m24: 65, m36: 60, o24: 99, o36: 0, standort: true, extrasKind: "vdsl" },
  { inetFlat: true, id: "cu-250", maxKanal: 60, name: "Asymmetrisch bis 250 Mbit/s", desc: D_CU, laufzeiten: LZ, m24: 65, m36: 60, o24: 99, o36: 0, standort: true, extrasKind: "vdsl" },
  { inetFlat: true, id: "ugg-100", maxKanal: 8, name: "Asymmetrisch bis 100 Mbit/s (UGG)", desc: D_UGG, laufzeiten: LZ, m24: 60, m36: 50, o24: 99, o36: 0, standort: true },
  { inetFlat: true, id: "ugg-250", maxKanal: 8, name: "Asymmetrisch bis 250 Mbit/s (UGG)", desc: D_UGG, laufzeiten: LZ, m24: 80, m36: 70, o24: 99, o36: 0, standort: true },
  { inetFlat: true, id: "ugg-500", maxKanal: 8, name: "Asymmetrisch bis 500 Mbit/s (UGG)", desc: D_UGG, laufzeiten: LZ, m24: 140, m36: 120, o24: 99, o36: 0, standort: true },
  { inetFlat: true, id: "ugg-1000", maxKanal: 8, name: "Asymmetrisch bis 1.000 Mbit/s (UGG)", desc: D_UGG, laufzeiten: LZ, m24: 160, m36: 140, o24: 99, o36: 0, standort: true },
  { inetFlat: true, id: "ftth-150", maxKanal: 8, name: "Asymmetrisch bis 150 Mbit/s FTTH", desc: D_FTTH, laufzeiten: LZ, m24: 65, m36: 60, o24: 99, o36: 0, standort: true },
  { inetFlat: true, id: "ftth-300", maxKanal: 8, name: "Asymmetrisch bis 300 Mbit/s FTTH", desc: D_FTTH, laufzeiten: LZ, m24: 75, m36: 70, o24: 99, o36: 0, standort: true },
  { inetFlat: true, id: "ftth-600", maxKanal: 8, name: "Asymmetrisch bis 600 Mbit/s FTTH", desc: D_FTTH, laufzeiten: LZ, m24: 105, m36: 100, o24: 99, o36: 0, standort: true },
  { inetFlat: true, id: "ftth-1000", maxKanal: 8, name: "Asymmetrisch bis 1.000 Mbit/s FTTH", desc: D_FTTH, laufzeiten: LZ, m24: 115, m36: 110, o24: 99, o36: 0, standort: true },
  { inetFlat: true, id: "line-2", maxKanal: 16, name: "Line 2 Mbit/s", desc: D_LINE, laufzeiten: LZ, priceOpen: true, standort: true, extrasKind: "line" },
  { inetFlat: true, id: "line-4", maxKanal: 30, name: "Line 4 Mbit/s", desc: D_LINE, laufzeiten: LZ, priceOpen: true, standort: true, extrasKind: "line" },
  { inetFlat: true, id: "line-10", maxKanal: 90, name: "Line 10 Mbit/s", desc: D_LINE, laufzeiten: LZ, priceOpen: true, standort: true, extrasKind: "line" },
  { inetFlat: true, id: "line-20", maxKanal: 180, name: "Line 20 Mbit/s", desc: D_LINE, laufzeiten: LZ, priceOpen: true, standort: true, extrasKind: "line" },
  { inetFlat: true, id: "line-50", maxKanal: 450, name: "Line 50 Mbit/s", desc: D_LINE, laufzeiten: LZ, priceOpen: true, standort: true, extrasKind: "line" },
  { inetFlat: true, id: "line-100", maxKanal: 600, name: "Line 100 Mbit/s", desc: D_LINE, laufzeiten: LZ, priceOpen: true, standort: true, extrasKind: "line" },
  { inetFlat: true, id: "line-200", maxKanal: 600, name: "Line 200 Mbit/s", desc: D_LINE, laufzeiten: LZ, priceOpen: true, standort: true, extrasKind: "line" },
  { inetFlat: true, id: "line-500", maxKanal: 600, name: "Line 500 Mbit/s", desc: D_LINE, laufzeiten: LZ, priceOpen: true, standort: true, extrasKind: "line" },
  { inetFlat: true, id: "line-1000", maxKanal: 600, name: "Line 1.000 Mbit/s", desc: D_LINE, laufzeiten: LZ, priceOpen: true, standort: true, extrasKind: "line" },
  { id: "mobile-main", maxKanal: 16, name: "Mobile Main", desc: D_MOBILE, laufzeiten: LZ, standort: true, mobile: true },
  { inetFlat: true, id: "wave5000-100", name: "Wave 5.000 bis 100 Mbit/s", desc: D_WAVE, laufzeiten: [36], m36: 950, o36: 9000, standort: true, wave: true },
  { inetFlat: true, id: "wave5000-250", name: "Wave 5.000 bis 250 Mbit/s", desc: D_WAVE, laufzeiten: [36], m36: 1050, o36: 9000, standort: true, wave: true },
  { inetFlat: true, id: "wave5000-500", name: "Wave 5.000 bis 500 Mbit/s", desc: D_WAVE, laufzeiten: [36], m36: 1150, o36: 9000, standort: true, wave: true },
  { inetFlat: true, id: "wave5000-1000", name: "Wave 5.000 bis 1 Gbit/s", desc: D_WAVE, laufzeiten: [36], m36: 1250, o36: 9000, standort: true, wave: true },
  { inetFlat: true, id: "wave15000-100", name: "Wave 15.000 bis 100 Mbit/s", desc: D_WAVE, laufzeiten: [36], m36: 1150, o36: 13000, standort: true, wave: true },
  { inetFlat: true, id: "wave15000-250", name: "Wave 15.000 bis 250 Mbit/s", desc: D_WAVE, laufzeiten: [36], m36: 1250, o36: 13000, standort: true, wave: true },
  { inetFlat: true, id: "wave15000-500", name: "Wave 15.000 bis 500 Mbit/s", desc: D_WAVE, laufzeiten: [36], m36: 1350, o36: 13000, standort: true, wave: true },
  { inetFlat: true, id: "wave15000-1000", name: "Wave 15.000 bis 1 Gbit/s", desc: D_WAVE, laufzeiten: [36], m36: 1450, o36: 13000, standort: true, wave: true },
]};

const BASIC_ROUTER = { dsl: { name: "AVM FRITZ!Box 7590 AX", price: 0 }, cable: { name: "AVM FRITZ!Box 6690", price: 5 }, fiber: { name: "AVM FRITZ!Box 5530 Fiber", price: 0 } };
const euPlusPrice = (term) => (Number(term) >= 36 ? 3 : 3.5);
const worldSelectPrice = (term) => (Number(term) >= 36 ? 10 : 11);
// ===== DTAG Leased Line: Region-Zuordnung (ONKZ 5-stellig) + Preislogik =====
// Kompakt eingebettet: nur Metro + Regio gelistet; alles andere (inkl. nicht gefundener ONKZ) = Country.
const _LL_METRO = "2010020200203002041020510209002102021100212002131021510216102181021910221002280022910230202306023100234002351023610237102381024100242102431024410246102471025100252102541025510256102610026710268102691027100275102831028610291002951029610297102981029910300003301033060332053321033310334403346033500336603371033762337703381033850339403410034210342303437034500346603473035040351003535035370354103544035460355003571035760358803594035960360503610036340364703663036790373503737038100383603839238470387103874038760388103883038860390103904039070390903910039210392303933039350393703941039490397103973039760398403987039940399603998040000415104171041810421004241042510427104281043100432104381044100443104441044510447104491045100452104541046410466104671047100472104751047610477104851048610487104881049100493604971050210505105071051100512105181051910521005221052510531005341053710541005421054310544105461054710548105510055210557105610056210563105641056610567105681056910571005721057610577105821058310584105851059410595105961060210603106041060610611006151061610618106204062100627106281063100635106361063810639106410064510651006531065510656106571065910661006621066310664106661066810673106741067510676106810068510686106871069000704107110071210713107134071410715107158071610719107210072610730307310073810739107410074410745107461075100755107561075710758107610076410767107681077100772107741077610777107831078410785107941079610797108021080410807108081080910810208210082410825108261082710828108291083210836108381084100842108431084410845008461085410855108561085710858108610086210865108681087210874108751087610877108781088070881008856089000907109091091010911009131091410916109171092100924109261093100932109341093510937109391094100946109491095100952109531095510961009631096410965109671097100974109761097710983109843098510992109931099410";
const _LL_REGIO = "20430204502052020530205402056020580206402065020660208002103021040212902132021330213702140021500215202162021630216602171021730217402175021820219202195021960220202203022040220502206022070220802222022230222402225022260222702228022320223302234022350223602237022380224102242022440224602248022510225402261022620226302266022670227102272022730230102303023040230502307023090232302324023250232702330023310233202333023340233502336023390235202353023550236202363023650236602367023680237202373023740237802389023910239202401024020240302404024050240602407024080240902423024290245102454024560246402465025010258102591025920259602602026300263102641026420265102661027210274102761027710281002821028410284202845028710292102931029410330203303033056331003320133203332203328033290333403338033394333973341033420334393361033620336383370133780337903391034000342023420334204342053420634207342413425034291342923429334294342953429734298342993431034347343503443034444344503447034602346103463834640347103475034910349303496035010350253502635027350553520035201352033520435205352063520735208352093521035230352433525035280352903573035780358103583035850359103601036200362013620236203362043620636208362093621036258362803631036371364103643036450364523645836500366103671036750367703681036830368503691036930369503710037220372303731037330374103744037500377103820138202382033820438205382063820738208382093822438231382923829338295383103834038410384303844038453384543845938466385003925039310394303946039500398103991041010410204103041040410504106041070410804109041210412204131041410415204154041610416204168041740417604177041790419104193042020420304206042070420804209042210422204223042240423104242042610429204293042940429604298043020430304305043070430804322043260432904330043310433404340043420434304344043460434704348043490435104357043840438504392043940440404406044210448804526045310453204535045510461004621046510473104791047920479504810048210484104921049310494104961050310503505045050660510105102051030510505108051090513005131051320513605137051380513905141051510516105171051740520105202052030520405205052060520705208052090522205223052240522505231052320523705241052420524405245052460524705261052710530005301053020530305304053050530605307053080530905321053310533305337053440535105353053610536205373053740538105424054250542805451055310554105551055610565105731057320574105751058100591005921059310597106007060230602706034060390604706051060710607406078060810610106102061030610406105061060610706108061090612206123061240612606127061280612906130061310613206134061350613606139061420614406145061460615006152061540615506157061620617106172061730617406175061820618406187061880619006192061950619606198062010620206203062050620606220062210622206224062270623106232062330623406235062360623706238062390624106245062510625206253062560625706258062610632106324063310633206340063410634806359063710642106431064410646106471066910671006721067230678106802068030680406805068060680906821068240682506826068270683106834068360683806841068420684306844068480684906881068930689406897068980702107022070240703107033070340705107071070820708307123071270714207144071450714607150071520715307154071560715707159071620717107181071950720207203072040722107222072240722507231072320723607240072420724307244072450724607247072480724907251072520725307254072550725707271072720727307275072770730207304073050730607307073080730907321073310733607340073440734507346073480735107361073940743107471075310754107602076210763107633076340763607642076450766007661076620766307664076650766607667076680766907683076840768507731078100782107910079310795108031080510810408105081060811008121081220812308131081330814108142081510815308161081650817008171081780819108202082030820508207082080822108226082300823108233082340823608237082380825208293082940831008331083410840408405084060840708424084260844208443084460845208453084540845608457084580845908466085100853108631086710871008731088210886109060091030912009122091230912609127091280912909132091340915109170091810918709191092010920209203092040920509206092080920909221092270922809231092460927009271092730927509276092770927809279092810930209303093050930609307093240933109333093340933709344093490936009364093650936609367093690938609398094010940209403094040940509406094070940809409094210943109441094530947309498095610957109621096610972109810098410987609910099710";
const _chunk5 = (s) => { const set = new Set(); for (let i = 0; i < s.length; i += 5) set.add(s.slice(i, i + 5)); return set; };
const METRO_SET = _chunk5(_LL_METRO), REGIO_SET = _chunk5(_LL_REGIO);
const onkzNorm = (onkz) => { const s = String(onkz || "").replace(/\D/g, "").replace(/^0+/, ""); return s ? (s + "00000").slice(0, 5) : ""; };
const onkzRegion = (onkz) => { const k = onkzNorm(onkz); if (k && METRO_SET.has(k)) return "Metro"; if (k && REGIO_SET.has(k)) return "Regio"; return "Country"; };
const LINE_PRICE = {
  Metro:   { bis20: { m24: 360, m36: 320 }, "50": { m24: 460, m36: 390 }, "100": { m24: 480, m36: 415 }, "200": { m24: 550, m36: 490 }, "500": { m24: 750, m36: 690 }, "1g": { m24: 890, m36: 800 } },
  Regio:   { bis20: { m24: 420, m36: 390 }, "50": { m24: 580, m36: 540 }, "100": { m24: 630, m36: 550 }, "200": { m24: 700, m36: 610 }, "500": { m24: 850, m36: 830 }, "1g": { m24: 990, m36: 950 } },
  Country: { bis20: { m24: 470, m36: 440 }, "50": { m24: 680, m36: 610 }, "100": { m24: 710, m36: 620 }, "200": { m24: 760, m36: 690 }, "500": { m24: 960, m36: 890 }, "1g": { m24: 1060, m36: 1020 } },
};
const LINE_BW = { "line-2": "bis20", "line-4": "bis20", "line-10": "bis20", "line-20": "bis20", "line-50": "50", "line-100": "100", "line-200": "200", "line-500": "500", "line-1000": "1g" };
const lineDtagBase = (p, r) => { const region = onkzRegion(r.onkz); const bw = LINE_BW[p.id] || "bis20"; const tier = Number(r.term) >= 36 ? "m36" : "m24"; const cell = (LINE_PRICE[region] || {})[bw]; const monthly = cell ? cell[tier] : 0; const oneoff = Number(r.term) >= 36 ? 0 : 99; return { monthly, oneoff, region }; };

// ===== Mobile Main: Basis + Geschwindigkeits-Aufpreis + Internet-Flat =====
const MOBILE_BASE = { m24: 46, o24: 99, m36: 42, o36: 0 };
const MOBILE_FLAT = { m24: 11, m36: 10 };
const INET_FLAT = { m24: 11, m36: 10 }; // Internet-Flatrate für All IP Business & Wave – separat bepreist, NICHT rabattiert
const MOBILE_SPEEDS = [
  { key: "25", label: "bis 25 Mbit/s", m24: 44, m36: 37 },
  { key: "300", label: "bis 300 Mbit/s", m24: 80, m36: 71 },
];
const mobileMainBase = (r) => {
  const t36 = Number(r.term) >= 36;
  const speed = MOBILE_SPEEDS.find(s => s.key === r.mobileSpeed) || MOBILE_SPEEDS[0];
  const speedM = (t36 ? speed.m36 : speed.m24) ?? 0;
  const baseM = t36 ? MOBILE_BASE.m36 : MOBILE_BASE.m24;
  const baseO = t36 ? MOBILE_BASE.o36 : MOBILE_BASE.o24;
  const flatM = t36 ? MOBILE_FLAT.m36 : MOBILE_FLAT.m24;
  return { baseM, baseO, speedM, flatM, speed, speedUnpriced: (t36 ? speed.m36 : speed.m24) == null, monthly: baseM + speedM + flatM, oneoff: baseO };
};

// ===== Antenne (für Mobile Main & Mobilfunk-Backup) =====
const ANTENNEN = [
  { key: "outdoor-blitz", label: "Outdoor mit 15 m Kabel und Blitzableiter (Bundle)", oneoff: 399 },
  { key: "outdoor-15", label: "Outdoor mit 15 m Kabel Mobile Main / Mobile Backup", oneoff: 209 },
  { key: "indoor-10", label: "Indoor mit 10 m Kabel", oneoff: 99 },
  { key: "indoor-5", label: "Indoor mit 5 m Kabel", oneoff: 89 },
];
const antennaFor = (key) => ANTENNEN.find(a => a.key === key) || null;

// ===== Editierbare Options- & Tarif-Texte (über Bearbeiten-Reiter anpassbar) =====
// Reihenfolge bestimmt die Anzeige im Editor. type "text" = einzeilig, "lines" = mehrzeilig (Array).
const OPT_SCHEMA = [
  { key: "backup", label: "Mobilfunk-Backup – Beschreibung", type: "lines" },
  { key: "backup_kanal_16", label: "Mobilfunk-Backup – Sprachkanäle (> 16)", type: "text" },
  { key: "backup_kanal_all", label: "Mobilfunk-Backup – Sprachkanäle (≤ 16)", type: "text" },
  { key: "express", label: "Express-Bereitstellung – Beschreibung", type: "lines" },
  { key: "euplus", label: "Fair Use EU Plus – Beschreibung", type: "text" },
  { key: "worldselect", label: "Fair Use World Select – Beschreibung", type: "text" },
  { key: "kanal_fest", label: "Sprachkanal Fair Use – dt. Festnetz", type: "text" },
  { key: "kanal_mob", label: "Sprachkanal Fair Use – dt. Mobilfunk", type: "text" },
  { key: "kanal_eu", label: "Sprachkanal Fair Use – EU Plus", type: "text" },
  { key: "kanal_world", label: "Sprachkanal Fair Use – World Select", type: "text" },
  { key: "teams", label: "Microsoft Teams-Kopplung – Beschreibung", type: "text" },
  { key: "wave_support", label: "Wave – Business Class Support", type: "text" },
  { key: "wave_los_name", label: "Wave – Line-of-Sight-Test (Name)", type: "text" },
  { key: "wave_los_desc", label: "Wave – Line-of-Sight-Test (Beschreibung)", type: "text" },
  { key: "ant_outdoor-blitz", label: "Antenne – Outdoor 15 m + Blitzableiter", type: "text" },
  { key: "ant_outdoor-15", label: "Antenne – Outdoor 15 m", type: "text" },
  { key: "ant_indoor-10", label: "Antenne – Indoor 10 m", type: "text" },
  { key: "ant_indoor-5", label: "Antenne – Indoor 5 m", type: "text" },
];
const OPT_DEFAULTS = {
  backup: [...D_BACKUP],
  backup_kanal_16: "Automatisierte Übernahme von 16 Sprachkanälen auf das Mobilfunk-Backup",
  backup_kanal_all: "Automatisierte Übernahme Ihrer Sprachkanäle auf das Mobilfunk-Backup",
  express: [...D_EXPRESS],
  euplus: "Fair Use EU Plus – 500 Minuten in die EU Plus Länder inklusive",
  worldselect: "Fair Use World Select – 250 Minuten in die World Select Länder inklusive",
  kanal_fest: RICHTUNGEN[0].fairText,
  kanal_mob: RICHTUNGEN[1].fairText,
  kanal_eu: RICHTUNGEN[2].fairText,
  kanal_world: RICHTUNGEN[3].fairText,
  teams: "inkl. Microsoft Teams-Integration",
  wave_support: "Upgrade zu Business Class 8 Stunden Support",
  wave_los_name: "„Line of Sight“ Test",
  wave_los_desc: "Einmaliges Einrichtungsentgelt – zwingend vor Inbetriebnahme erforderlich",
  "ant_outdoor-blitz": ANTENNEN[0].label,
  "ant_outdoor-15": ANTENNEN[1].label,
  "ant_indoor-10": ANTENNEN[2].label,
  "ant_indoor-5": ANTENNEN[3].label,
};
const cloneVal = (v) => Array.isArray(v) ? [...v] : v;
let OPT = Object.fromEntries(Object.entries(OPT_DEFAULTS).map(([k, v]) => [k, cloneVal(v)]));
const optText = (key) => OPT[key] != null ? OPT[key] : OPT_DEFAULTS[key];
// ===== All IP Pure: einfache Asymmetrisch/FTTH-Anbindungen, kein Backup/Sprachkanäle =====
const PURE_LZ = [24, 36, 48, 60];
const PURE_DESC_ASYM = ["Asymmetrische Kupfer-Anbindung (VDSL)"];
const PURE_DESC_FTTH = ["Asymmetrische Glasfaser-Anbindung (FTTH)"];
const PUREP = (id, name, m24, desc, ftth) => ({ id, name, desc, laufzeiten: PURE_LZ, m24, m36: m24 - 5, o24: 99, o36: 0, qty: true, qtyLabel: "Anzahl", standort: true, pure: true, pureDiscount: !!ftth });
const DB_ALLIPPURE = { label: "All IP Pure", products: [
  PUREP("pure-asym-25", "Asymmetrisch bis 25 Mbit/s", 45, PURE_DESC_ASYM),
  PUREP("pure-asym-50", "Asymmetrisch bis 50 Mbit/s", 45, PURE_DESC_ASYM),
  PUREP("pure-asym-100", "Asymmetrisch bis 100 Mbit/s", 45, PURE_DESC_ASYM),
  PUREP("pure-asym-175", "Asymmetrisch bis 175 Mbit/s", 65, PURE_DESC_ASYM),
  PUREP("pure-asym-250", "Asymmetrisch bis 250 Mbit/s", 65, PURE_DESC_ASYM),
  PUREP("pure-ftth-150", "Asymmetrisch FTTH bis 150 Mbit/s", 65, PURE_DESC_FTTH, true),
  PUREP("pure-ftth-300", "Asymmetrisch FTTH bis 300 Mbit/s", 75, PURE_DESC_FTTH, true),
  PUREP("pure-ftth-600", "Asymmetrisch FTTH bis 600 Mbit/s", 105, PURE_DESC_FTTH, true),
  PUREP("pure-ftth-1000", "Asymmetrisch FTTH bis 1000 Mbit/s", 115, PURE_DESC_FTTH, true),
]};
const DB_ALLIPBASIC = { label: "All IP Basic", products: [
  { id: "b-dsl-250", name: "All-IP Basic 250", desc: [], laufzeiten: LZ, m24: 47.5, m36: 42.5, o24: 50, o36: 0, connKind: "dsl", standort: true },
  { id: "b-dsl-100", name: "All-IP Basic 100", desc: [], laufzeiten: LZ, m24: 40, m36: 35, o24: 50, o36: 0, connKind: "dsl", standort: true, zweiterKanal: true },
  { id: "b-dsl-50", name: "All-IP Basic 50", desc: [], laufzeiten: LZ, m24: 35, m36: 30, o24: 50, o36: 0, connKind: "dsl", standort: true, zweiterKanal: true },
  { id: "b-dsl-16", name: "All-IP Basic 16", desc: [], laufzeiten: LZ, m24: 35, m36: 30, o24: 50, o36: 0, connKind: "dsl", standort: true, zweiterKanal: true },
  { id: "b-cable-1000", name: "All-IP Basic Cable 1000", desc: [], laufzeiten: LZ, m24: 70, m36: 65, o24: 50, o36: 0, connKind: "cable", standort: true },
  { id: "b-cable-500", name: "All-IP Basic Cable 500", desc: [], laufzeiten: LZ, m24: 60, m36: 55, o24: 50, o36: 0, connKind: "cable", standort: true },
  { id: "b-cable-300", name: "All-IP Basic Cable 300", desc: [], laufzeiten: LZ, m24: 50, m36: 45, o24: 50, o36: 0, connKind: "cable", standort: true },
  { id: "b-cable-100", name: "All-IP Basic Cable 100", desc: [], laufzeiten: LZ, m24: 40, m36: 35, o24: 50, o36: 0, connKind: "cable", standort: true, zweiterKanal: true },
  { id: "b-cable-50", name: "All-IP Basic Cable 50", desc: [], laufzeiten: LZ, m24: 35, m36: 30, o24: 50, o36: 0, connKind: "cable", standort: true, zweiterKanal: true },
  { id: "b-fiber-1000", name: "All-IP Basic Fiber 1000", desc: [], laufzeiten: LZ, m24: 94.44, m36: 88.89, o24: 50, o36: 0, connKind: "fiber", standort: true },
  { id: "b-fiber-600", name: "All-IP Basic Fiber 600", desc: [], laufzeiten: LZ, m24: 66.67, m36: 61.11, o24: 50, o36: 0, connKind: "fiber", standort: true },
  { id: "b-fiber-300", name: "All-IP Basic Fiber 300", desc: [], laufzeiten: LZ, m24: 55.56, m36: 47.22, o24: 50, o36: 0, connKind: "fiber", standort: true },
  { id: "b-fiber-150", name: "All-IP Basic Fiber 150", desc: [], laufzeiten: LZ, m24: 50, m36: 38.89, o24: 50, o36: 0, connKind: "fiber", standort: true, zweiterKanal: true },
]};

const DP_PRODUCTS = [
  { id: "dp-basis-bus", name: "Basispaket Business (5 Lizenzen)", desc: D_BASIS, laufzeiten: DP_LZ, m36: 59, o36: 99, produkttyp: "basispaket", rabattType: "fix10", abloese: true },
  { id: "dp-basis-bas", name: "Basispaket Basic (5 Lizenzen)", desc: ["Zugang zur virtuellen Telefonanlage inkl. 5 Lizenzen (Basic-Funktionsumfang)"], laufzeiten: DP_LZ, m36: 35, o36: 99, produkttyp: "basispaket", rabattType: "fix10", abloese: true },
  { id: "dp-liz-bus", name: "Zusätzliche Lizenz Business", desc: ["Inhalte identisch zum Basispaket"], laufzeiten: DP_LZ, m36: 14, o36: 29, qty: true, qtyLabel: "Lizenzen", produkttyp: "lizenz", abloese: true, dpTier: "business" },
  { id: "dp-liz-bas", name: "Zusätzliche Lizenz Basic", desc: ["Inhalte identisch zum Basispaket"], laufzeiten: DP_LZ, m36: 9.2, o36: 29, qty: true, qtyLabel: "Lizenzen", produkttyp: "lizenz", abloese: true, dpTier: "basic" },
  { id: "dp-einrichtung", name: "Basiseinrichtung", desc: ["Einmalige Basiseinrichtung der Telefonanlage"], laufzeiten: [], o36: 149, qty: true, qtyLabel: "Anzahl", produkttyp: "einrichtung" },
  { id: "dp-clip", name: "CLIP no Screening", desc: ["Einrichtung CLIP – no screening"], laufzeiten: [], o36: 69, qty: true, qtyLabel: "Anzahl", produkttyp: "rufnummern" },
  { id: "dp-teams", name: "MS Teams Option", desc: [], laufzeiten: [], m36: 1, qty: true, qtyLabel: "Nebenstellen", produkttyp: "option" },
  { id: "dp-block10", name: "Bereitstellung 10er Rufnummernblock", desc: [], laufzeiten: [], o36: 19, qty: true, qtyLabel: "Blöcke", produkttyp: "rufnummern" },
  { id: "dp-block100", name: "Bereitstellung 100er Rufnummernblock", desc: [], laufzeiten: [], o36: 149, qty: true, qtyLabel: "Blöcke", produkttyp: "rufnummern" },
  { id: "dp-blockverl", name: "Blockverlängerung", desc: [], laufzeiten: [], o36: 199, qty: true, qtyLabel: "Anzahl", produkttyp: "rufnummern" },
  { id: "dp-blockverk", name: "Blockverkürzung", desc: [], laufzeiten: [], o36: 199, qty: true, qtyLabel: "Anzahl", produkttyp: "rufnummern" },
  { id: "dp-efax", name: "eFax Nebenstelle", desc: ["eFax Client für Windows"], laufzeiten: [], o36: 10, m36: 1, qty: true, qtyLabel: "Nebenstellen", produkttyp: "option" },
  { id: "dp-crm", name: "CRM Connect Plus", desc: [], laufzeiten: [], m36: 1.8, qty: true, qtyLabel: "Nebenstellen", produkttyp: "option" },
  { id: "dp-ccm", name: "Call Center Monitoring (je Agent)", desc: [], laufzeiten: [], m36: 8, qty: true, qtyLabel: "Agents", qtyMin: 5, produkttyp: "option" },
  { id: "dp-cti-std-win", name: "CTI standard für Windows", desc: [], laufzeiten: [], m36: 1, qty: true, qtyLabel: "Lizenzen", produkttyp: "option" },
  { id: "dp-cti-std-crm", name: "CTI standard CRM für Windows", desc: [], laufzeiten: [], m36: 1.5, qty: true, qtyLabel: "Lizenzen", produkttyp: "option" },
  { id: "dp-cti-std-mac", name: "CTI standard für Mac", desc: [], laufzeiten: [], m36: 1, qty: true, qtyLabel: "Lizenzen", produkttyp: "option" },
  { id: "dp-cti-prem-win", name: "CTI Premium für Windows", desc: ["Premium-Lizenz – aktiviert einmalig CTI Premium Server (299 €)"], laufzeiten: [], m36: 1, qty: true, qtyLabel: "Lizenzen", produkttyp: "option", premiumCti: true },
  { id: "dp-cti-prem-crm", name: "CTI Premium CRM für Windows", desc: ["Premium-Lizenz – aktiviert einmalig CTI Premium Server (299 €)"], laufzeiten: [], m36: 1.8, qty: true, qtyLabel: "Lizenzen", produkttyp: "option", premiumCti: true },
  { id: "dp-cti-standort", name: "CTI Standortlizenz", desc: [], laufzeiten: [], m36: 7.9, qty: true, qtyLabel: "Standorte", produkttyp: "option" },
  { id: "dp-cti-server", name: "CTI Premium Server (Einrichtung)", desc: ["Wird einmalig berechnet, sobald eine CTI-Premium-Lizenz gebucht ist"], laufzeiten: [], o36: 299, produkttyp: "server", isServer: true },
  { id: "dp-ncti", name: "NCTI Pro (estos ProCall)", desc: [], laufzeiten: [], m36: 4.8, qty: true, qtyLabel: "Lizenzen", produkttyp: "option" },
  { id: "dp-intl", name: "International Paket", desc: ["Muss für alle Nebenstellen inkl. Basispaket gebucht werden", "Festnetz Europazone 1 und Weltzone 1 kostenlos"], laufzeiten: [], o36: 9, m36: 5, qty: true, qtyLabel: "Nebenstellen", produkttyp: "option", mengenBezug: "nebenstellen" },
];
// Digital Phone Business / Basic: identischer Katalog, je Bereich nur die passende "Zusätzliche Lizenz"
const DB_DIGITALPHONE_BUSINESS = { label: "Digital Phone Business", products: DP_PRODUCTS.filter(p => p.dpTier !== "basic") };
const DB_DIGITALPHONE_BASIC = { label: "Digital Phone Basic", products: DP_PRODUCTS.filter(p => p.dpTier !== "business") };

const DB_SIPTRUNK = { label: "SIP Trunk Only", products: [
  { id: "sip-basic", name: "SIP-Trunk Basic inkl. Deutschland-Flat", desc: D_SIP, laufzeiten: [], m36: 8, o36: 0, qty: true, qtyLabel: "Sprachkanäle", teamsCoupling: true },
  { id: "sip-premium", name: "SIP-Trunk Premium inkl. Deutschland-Flat", desc: D_SIP, laufzeiten: [], m36: 12.5, o36: 0, qty: true, qtyLabel: "Sprachkanäle", teamsCoupling: true },
  { id: "sip-sep-weitere", name: "── Weitere ──", desc: [], isSeparator: true },
  { id: "sip-efax", name: "eFax", desc: [], laufzeiten: [], m36: 6.8, o36: 19, qty: true, qtyLabel: "Anzahl eFax" },
  { id: "sip-block10", name: "Geografischer 10er-Rufnummernblock", desc: [], laufzeiten: [], o36: 19, qty: true, qtyLabel: "Blöcke" },
  { id: "sip-block30", name: "Geografischer 30er-Rufnummernblock", desc: [], laufzeiten: [], o36: 59, qty: true, qtyLabel: "Blöcke" },
  { id: "sip-block50", name: "Geografischer 50er-Rufnummernblock", desc: [], laufzeiten: [], o36: 99, qty: true, qtyLabel: "Blöcke" },
  { id: "sip-block100", name: "Geografischer 100er-Rufnummernblock", desc: [], laufzeiten: [], o36: 149, qty: true, qtyLabel: "Blöcke" },
  { id: "sip-umzug", name: "Rufnummernumzug", desc: [], laufzeiten: [], o36: 99, qty: true, qtyLabel: "Anzahl" },
  { id: "sip-inbetrieb", name: "Inbetriebnahme SIP-Trunk inkl. Integration Microsoft Teams 3 (max. 180 Minuten)", desc: [], laufzeiten: [], o36: 333 },
]};

const DB_MDM = { label: "Mobile Device Management", products: [
  { id: "mdm-basic", name: "MDM Basic", desc: D_MDM, laufzeiten: [24, 36], m36: 2.5, o36: 0, qty: true, qtyLabel: "Lizenzen", rabattType: "staffelMax", staffel: MDM_STAFFEL, mdmKind: "unmanaged" },
  { id: "mdm-managed", name: "MDM Basic Managed", desc: D_MDM_MANAGED, laufzeiten: [24, 36], m36: 8, o36: 0, qty: true, qtyLabel: "Lizenzen", rabattType: "staffelMax", staffel: MDM_STAFFEL, mdmKind: "managed" },
  { id: "mdm-training", name: "MDM Basic Training", desc: [], laufzeiten: [24, 36], o36: 333 },
]};

const O = (id, name, loPrice, loMax, mgdPrice, mgdMax) => ({ id, name, desc: [], laufzeiten: [], qty: true, qtyLabel: "Anzahl", office: true, rabattType: "officeMax", produkttyp: "option", loPrice, loMax, mgdPrice: mgdPrice ?? null, mgdMax: mgdMax ?? null });
const DB_OFFICE365 = { label: "Microsoft 365 / Office", products: [
  O("o-bb","Microsoft 365 Business Basic",6.73,30,11.73,25), O("o-bb-eea","Microsoft 365 Business Basic EEA",4.90,30,9.90,25),
  O("o-bs","Microsoft 365 Business Standard",12.74,40,17.74,35), O("o-bs-eea","Microsoft 365 Business Standard EEA",9.81,40,14.81,35),
  O("o-bp","Microsoft 365 Business Premium",20.06,40,25.06,35), O("o-bp-eea","Microsoft 365 Business Premium EEA",17.12,40,22.12,35),
  O("o-phone-std","Microsoft 365 Phone Standard",9.14,45,null,null), O("o-teams-ess","Microsoft Teams Essentials",3.68,20,null,null),
  O("o-exo-1","Exchange Online (Plan 1)",3.68,5,8.68,5), O("o-exo-2","Exchange Online (Plan 2)",7.25,5,12.25,5),
  { id: "o-sep-weitere", name: "── Weitere Lizenzen ──", desc: [], isSeparator: true },
  O("o-ems-e3","Enterprise Mobility + Security E3",10.92,5,15.92,5), O("o-ems-e5","Enterprise Mobility + Security E5",16.38,5,21.38,5),
  O("o-exo-arch-online","Exchange Online Archiving for Exchange Online",2.73,5,null,null), O("o-exo-arch-server","Exchange Online Archiving for Exchange Server",2.73,5,null,null),
  O("o-exo-kiosk","Exchange Online Kiosk",1.82,5,null,null), O("o-exo-protection","Exchange Online Protection",1.01,5,null,null),
  O("o-apps-bus","Microsoft 365 Apps for Business",11.55,5,16.55,5), O("o-apps-ent","Microsoft 365 Apps for Enterprise",16.17,5,21.17,5),
  O("o-copilot-1x","Microsoft 365 Copilot (1xZahlung + keine Provision)",26.78,5,null,null), O("o-copilot-month","Microsoft 365 Copilot - monatlich",29.99,5,null,null),
  O("o-m365-e3","Microsoft 365 E3",39.67,5,44.67,5), O("o-m365-e3-eea","Microsoft 365 E3 EEA (no Teams)",31.88,5,36.88,5),
  O("o-m365-e5","Microsoft 365 E5",62.10,5,67.10,5), O("o-m365-e5-eea","Microsoft 365 E5 EEA (no Teams)",53.25,5,58.25,5),
  O("o-purview","Microsoft 365 Purview Suite",10.92,5,15.92,5), O("o-defender-suite","Microsoft 365 Defender Suite",10.92,5,null,null),
  O("o-m365-f1","Microsoft 365 F1",2.73,5,7.73,5), O("o-m365-f1-eea","Microsoft 365 F1 EEA (no Teams)",2.27,5,7.27,5),
  O("o-m365-f3","Microsoft 365 F3",9.09,5,14.09,5), O("o-m365-f3-eea","Microsoft 365 F3 EEA (no Teams)",8.12,5,13.12,5),
  O("o-def-bus","Microsoft Defender for Business",2.73,5,null,null), O("o-def-bus-srv","Microsoft Defender for Business servers",2.73,5,null,null),
  O("o-def-cloudapps","Microsoft Defender for Cloud Apps",3.15,5,null,null), O("o-def-cloudapps-f1","Microsoft Defender for Cloud Apps F1",2.21,5,null,null),
  O("o-def-ep-f1","Microsoft Defender for Endpoint F1",1.82,5,null,null), O("o-def-ep-f2","Microsoft Defender for Endpoint F2",3.15,5,null,null),
  O("o-def-ep-p1","Microsoft Defender for Endpoint P1",2.73,5,null,null), O("o-def-ep-p2","Microsoft Defender for Endpoint P2",4.73,5,null,null),
  O("o-def-ep-server","Microsoft Defender for Endpoint Server",4.73,5,null,null), O("o-def-id","Microsoft Defender for Identity",5.04,5,null,null),
  O("o-def-id-f1","Microsoft Defender for Identity F1",3.36,5,null,null), O("o-def-o365-f1","Microsoft Defender for Office 365 F1",1.33,5,null,null),
  O("o-def-o365-f2","Microsoft Defender for Office 365 F2",3.05,5,null,null), O("o-def-o365-p1","Microsoft Defender for Office 365 (Plan 1)",1.82,5,null,null),
  O("o-def-o365-p2","Microsoft Defender for Office 365 (Plan 2)",4.52,5,null,null), O("o-entra-id-f2","Microsoft Entra ID F2",6.41,5,11.41,5),
  O("o-entra-id-p1","Microsoft Entra ID P1",6.80,5,11.80,5), O("o-entra-id-p2","Microsoft Entra ID P2",9.14,5,14.14,5),
  O("o-entra-suite","Microsoft Entra Suite",10.92,5,null,null), O("o-intune-epm","Microsoft Intune Endpoint Privilege Management",2.73,5,null,null),
  O("o-intune-p1","Microsoft Intune Plan 1",7.25,5,12.25,5), O("o-intune-p1-device","Microsoft Intune Plan 1 Device",2.42,5,null,null),
  O("o-intune-p1-storage","Microsoft Intune Plan 1 Storage Add-On",3.68,5,null,null), O("o-intune-p2","Microsoft Intune Plan 2",3.68,5,null,null),
  O("o-intune-remote","Microsoft Intune Remote Help",3.15,5,null,null), O("o-intune-suite","Microsoft Intune Suite",9.14,5,null,null),
  O("o-teams-eea","Microsoft Teams EEA",7.77,5,12.77,5), O("o-teams-rooms","Microsoft Teams Rooms Pro",36.44,5,null,null),
  O("o-teams-shared","Microsoft Teams Shared Devices",7.25,5,null,null), O("o-o365-dlp","Office 365 Data Loss Prevention",2.73,5,null,null),
  O("o-o365-e1","Office 365 E1",9.14,5,14.14,5), O("o-o365-e1-eea","Office 365 E1 EEA (no Teams)",6.20,5,11.20,5),
  O("o-o365-e3","Office 365 E3",27.58,5,32.58,5), O("o-o365-e3-eea","Office 365 E3 EEA (no Teams)",19.79,5,24.79,5),
  O("o-o365-e5","Office 365 E5",43.83,5,48.83,5), O("o-o365-e5-eea","Office 365 E5 EEA (no Teams)",35.71,5,40.71,5),
  O("o-o365-storage","Office 365 Extra File Storage",0.21,25,null,null), O("o-o365-f3","Office 365 F3",3.68,5,8.68,5),
  O("o-o365-f3-eea","Office 365 F3 EEA (no Teams)",3.15,5,null,null), O("o-onedrive-p1","OneDrive for business (Plan 1)",4.52,5,null,null),
  O("o-onedrive-p2","OneDrive for business (Plan 2)",9.14,5,null,null), O("o-planner-proj-p3","Planner and Project Plan 3",27.30,5,32.30,5),
  O("o-planner-proj-p5","Planner and Project Plan 5",50.09,5,55.09,5), O("o-planner-p1","Planner Plan 1",9.14,5,null,null),
  O("o-powerapps-prem","Power Apps Premium",18.17,5,null,null), O("o-pa-flow","Power Automate per flow plan",90.93,5,null,null),
  O("o-pa-user","Power Automate per user plan",13.65,5,null,null), O("o-pbi-prem-user","Power BI Premium Per User",21.84,5,null,null),
  O("o-pbi-prem-addon","Power BI Premium Per User Add-On",9.14,5,null,null), O("o-pbi-pro","Power BI Pro",12.71,5,null,null),
  O("o-sp-p1","SharePoint (Plan 1)",4.52,5,null,null), O("o-sp-p2","SharePoint (Plan 2)",9.14,5,null,null),
  O("o-sp-adv","SharePoint advanced management plan 1",2.73,5,null,null), O("o-visio-p1","Visio Plan 1",4.52,5,9.52,5),
  O("o-visio-p2","Visio Plan 2",13.65,5,18.65,5),
]};

// ===== SD-WAN Fortinet =====
const SDWAN_TERMS = [12, 24, 36, 48, 60];
const T5 = (a, b, c, d, e) => ({ 12: a, 24: b, 36: c, 48: d, 60: e });
const SDWAN_LIC_LABEL = { sdwan: "SD-WAN", atp: "Basic Security (ATP)", utp: "Advanced Security (UTP)" };
const SDWAN_LIC_LABEL_MERAKI = { sdwan: "SD-WAN", atp: "Basic Security", utp: "Advanced Security" };
const sdwanLicLabel = (vendor, lic) => (vendor === "meraki" ? SDWAN_LIC_LABEL_MERAKI : SDWAN_LIC_LABEL)[lic] || lic;
// Redundanz-Nachlass je Vendor (modellunabhängig, je Lizenztyp × Laufzeit) – Werte negativ. NIE mischen.
const SDWAN_RED = { sdwan: T5(-50, -47.5, -45, -42.5, -40), atp: T5(-80, -76, -72, -68, -64), utp: T5(-90, -85.5, -81, -76.5, -72) };
const SDWAN_RED_MERAKI = { sdwan: T5(-60, -57, -54, -51, -48), atp: T5(-90, -85.5, -81, -76.5, -72), utp: T5(-120, -114, -108, -102, -96) };
const sdwanRedTable = (vendor) => vendor === "meraki" ? SDWAN_RED_MERAKI : SDWAN_RED;
// Einrichtung SD-WAN-Service (laufzeitabhängig, je Lizenz/Stück)
const SDWAN_EINR_ROUTER = T5(600, 300, 150, 75, 0);   // FG-Router, VM-Router, Mobilfunk-WAN-Extender
const SDWAN_EINR_APSW = T5(200, 100, 50, 25, 0);       // WLAN-Accesspoint, LAN-Switch
const SDWAN_REMOTE_SETUP = 75;                          // Einrichtung Remote User je 25er-Paket
const SDWAN_INSTALL_PER_DEVICE = 100;                   // Vor-Ort-Installation je Gerät
const SDWAN_AD_ONEOFF = 790;                            // Anbindung kundenseitiger AD-Instanz je Standort
const sdwanFloor = (p, lic, term) => { const m = p.lic ? p.lic[lic] : p.price; return m ? (m[term] ?? m[12]) : 0; };
const sdwanCeil = (p, lic) => { const m = p.lic ? p.lic[lic] : p.price; return m ? m[12] : 0; };
const sdwanEinr = (kind, term) => (kind === "ap_switch" ? SDWAN_EINR_APSW : SDWAN_EINR_ROUTER)[term] ?? 0;
const sdwanProjectCost = (standorte) => standorte >= 11 ? 0 : standorte >= 5 ? 500 : standorte >= 2 ? 1000 : 0;
const SEP = (id, name) => ({ id, name, isSeparator: true });

// Monatspreis einer SD-WAN-Zeile (inkl. Redundanz). chosen = gewählter Preis (leer => Boden = Preis der Laufzeit).
const sdwanChosen = (p, r) => {
  const lic = p.lic ? (r.sdwanLicense || "sdwan") : null;
  const term = Number(r.term) || 60;
  const floor = sdwanFloor(p, lic, term);
  const raw = r.sdwanPrice === "" || r.sdwanPrice == null ? floor : Number(r.sdwanPrice);
  return isFinite(raw) ? raw : floor;
};
const sdwanRowMonthlyEach = (p, r) => {
  const chosen = sdwanChosen(p, r);
  if (p.sdwanKind === "ad") return 0;
  if (p.sdwanKind === "fg" && p.redundanzAllowed && r.redundanz) {
    const lic = r.sdwanLicense || "sdwan"; const term = Number(r.term) || 60;
    return 2 * chosen + (sdwanRedTable(p.vendor)[lic]?.[term] ?? 0);
  }
  return chosen;
};

const FG = (id, name, lic, redundanzAllowed, vendor = "fortinet") => ({ id, name, sdwan: true, sdwanKind: "fg", einrKind: "router", lic, redundanzAllowed, vendor, qty: true, qtyLabel: "Menge", standort: true });
const VM = (id, name, lic) => ({ id, name, sdwan: true, sdwanKind: "vm", einrKind: "router", lic, vendor: "fortinet", qty: true, qtyLabel: "Menge", standort: true });
// Meraki-Router: physischer Primär-Router, Redundanz erlaubt, Meraki-Preistabellen/-Labels
const MKR = (id, name, lic) => ({ id, name, sdwan: true, sdwanKind: "fg", einrKind: "router", lic, redundanzAllowed: true, vendor: "meraki", qty: true, qtyLabel: "Menge", standort: true });
const SDEV = (id, name, kind, einrKind, price, extra) => ({ id, name, sdwan: true, sdwanKind: kind, einrKind, price, qty: true, qtyLabel: "Menge", standort: true, ...(extra || {}) });

const DB_SDWAN = { label: "SD-WAN Fortinet", products: [
  FG("fg40", "Fortinet FG 40 (S – kleiner Standort)", { sdwan: T5(110, 104.5, 99, 93.5, 88), atp: T5(170, 161.5, 153, 144.5, 136), utp: T5(200, 190, 180, 170, 160) }, false),
  FG("fg60", "Fortinet FG 60 (M – mittlerer Standort)", { sdwan: T5(120, 114, 108, 102, 96), atp: T5(190, 180.5, 171, 161.5, 152), utp: T5(230, 218.5, 207, 195.5, 184) }, true),
  FG("fg80", "Fortinet FG 80 (M – mittlerer Standort)", { sdwan: T5(160, 152, 144, 136, 128), atp: T5(240, 228, 216, 204, 192), utp: T5(300, 285, 270, 255, 240) }, true),
  FG("fg100", "Fortinet FG 100 (L – großer Standort)", { sdwan: T5(280, 266, 252, 238, 224), atp: T5(430, 408.5, 387, 365.5, 344), utp: T5(540, 513, 486, 459, 432) }, true),
  FG("fg200", "Fortinet FG 200 (L – großer Standort)", { sdwan: T5(340, 323, 306, 289, 272), atp: T5(530, 503.5, 477, 450.5, 424), utp: T5(650, 617.5, 585, 552.5, 520) }, true),
  FG("fg400", "Fortinet FG 400 (XL – sehr großer Standort)", { sdwan: T5(500, 475, 450, 425, 400), atp: T5(790, 750.5, 711, 671.5, 632), utp: T5(1000, 950, 900, 850, 800) }, true),
  FG("fg600", "Fortinet FG 600 (XL – sehr großer Standort)", { sdwan: T5(660, 627, 594, 561, 528), atp: T5(990, 940.5, 891, 841.5, 792), utp: T5(1260, 1197, 1134, 1071, 1008) }, true),
  SEP("sep-vm", "──────── Virtual Machine ────────"),
  VM("vm01", "Fortinet Virtual Machine FG VM01", { sdwan: T5(130, 123.5, 117, 110.5, 104), atp: T5(200, 190, 180, 170, 160), utp: T5(260, 247, 234, 221, 208) }),
  VM("vm02", "Fortinet Virtual Machine FG VM02", { sdwan: T5(140, 133, 126, 119, 112), atp: T5(220, 209, 198, 187, 176), utp: T5(280, 266, 252, 238, 224) }),
  VM("vm04", "Fortinet Virtual Machine FG VM04", { sdwan: T5(220, 209, 198, 187, 176), atp: T5(350, 332.5, 315, 297.5, 280), utp: T5(480, 456, 432, 408, 384) }),
  VM("vm08", "Fortinet Virtual Machine FG VM08", { sdwan: T5(530, 503.5, 477, 450.5, 424), atp: T5(860, 817, 774, 731, 688), utp: T5(1180, 1121, 1062, 1003, 944) }),
  VM("vm16", "Fortinet Virtual Machine FG VM16", { sdwan: T5(1080, 1026, 972, 918, 864), atp: T5(1750, 1662.5, 1575, 1487.5, 1400), utp: T5(2390, 2270.5, 2151, 2031.5, 1912) }),
  SEP("sep-fex", "──────── Mobilfunk Extender ────────"),
  SDEV("fex101", "Fortinet FEX 101 (Mobilfunk-WAN-Extender)", "fex", "router", T5(38, 36.1, 34.2, 32.3, 30.4)),
  SDEV("fex511", "Fortinet FEX 511 (Mobilfunk-WAN-Extender)", "fex", "router", T5(70, 66.5, 63, 59.5, 56)),
  SEP("sep-sw", "──────── LAN-Switches ────────"),
  SDEV("fs108", "Fortinet FS 108", "switch", "ap_switch", T5(28, 26.6, 25.2, 23.8, 22.4)),
  SDEV("fs108poe", "Fortinet FS 108 POE", "switch", "ap_switch", T5(32, 30.4, 28.8, 27.2, 25.6)),
  SDEV("fs124", "Fortinet FS 124", "switch", "ap_switch", T5(38, 36.1, 34.2, 32.3, 30.4)),
  SDEV("fs124poe", "Fortinet FS 124 POE", "switch", "ap_switch", T5(56, 53.2, 50.4, 47.6, 44.8)),
  SDEV("fs148", "Fortinet FS 148", "switch", "ap_switch", T5(52, 49.4, 46.8, 44.2, 41.6)),
  SDEV("fs148poe", "Fortinet FS 148 POE", "switch", "ap_switch", T5(72, 68.4, 64.8, 61.2, 57.6)),
  SDEV("fs424", "Fortinet FS 424", "switch", "ap_switch", T5(62, 58.9, 55.8, 52.7, 49.6)),
  SDEV("fs424poe", "Fortinet FS 424 POE", "switch", "ap_switch", T5(90, 85.5, 81, 76.5, 72)),
  SDEV("fs448", "Fortinet FS 448", "switch", "ap_switch", T5(86, 81.7, 77.4, 73.1, 68.8)),
  SDEV("fs448poe", "Fortinet FS 448 POE", "switch", "ap_switch", T5(148, 140.6, 133.2, 125.8, 118.4)),
  SEP("sep-ap", "──────── WLAN-Accesspoints ────────"),
  SDEV("fap231", "Fortinet FAP 231", "ap", "ap_switch", T5(32, 30.4, 28.8, 27.2, 25.6)),
  SDEV("fap431", "Fortinet FAP 431", "ap", "ap_switch", T5(52, 49.4, 46.8, 44.2, 41.6)),
  SDEV("fap831", "Fortinet FAP 831", "ap", "ap_switch", T5(62, 58.9, 55.8, 52.7, 49.6)),
  SEP("sep-remote", "──────── Remote User ────────"),
  SDEV("ru-vpn", "Remote User VPN", "remote", null, T5(98, 93.1, 88.2, 83.3, 78.4), { pack25: true }),
  SDEV("ru-ztna", "Remote User ZTNA", "remote", null, T5(128, 121.6, 115.2, 108.8, 102.4), { pack25: true }),
  SDEV("ru-epp", "Remote User EPP", "remote", null, T5(188, 178.6, 169.2, 159.8, 150.4), { pack25: true }),
  SEP("sep-weitere", "──────── Weitere Leistungen ────────"),
  { id: "sd-ad", name: "Anbindung kundenseitiger Active-Directory-Instanz", sdwan: true, sdwanKind: "ad", qty: true, qtyLabel: "Standorte (Hub)", standort: true },
]};
const DB_SDWAN_MERAKI = { label: "SD-WAN Meraki", products: [
  MKR("mk-z3", "Meraki Z3 (XS – Teleworker)", { sdwan: T5(60, 57, 54, 51, 48) }),
  MKR("mk-mx67", "Meraki MX67 (S – kleiner Standort)", { sdwan: T5(100, 95, 90, 85, 80), atp: T5(130, 123.5, 117, 110.5, 104), utp: T5(170, 161.5, 153, 144.5, 136) }),
  MKR("mk-mx95", "Meraki MX95 (M – mittlerer Standort)", { sdwan: T5(320, 304, 288, 272, 256), atp: T5(470, 446.5, 423, 399.5, 376), utp: T5(570, 541.5, 513, 484.5, 456) }),
  MKR("mk-mx250", "Meraki MX250 (L – großer Standort)", { sdwan: T5(530, 503.5, 477, 450.5, 424), atp: T5(760, 722, 684, 646, 608), utp: T5(1100, 1045, 990, 935, 880) }),
  MKR("mk-mx450", "Meraki MX450 (XL – sehr großer Standort)", { sdwan: T5(1060, 1007, 954, 901, 848), atp: T5(1540, 1463, 1386, 1309, 1232), utp: T5(2260, 2147, 2034, 1921, 1808) }),
  SEP("mk-sep-fex", "──────── Mobilfunk Extender ────────"),
  SDEV("mk-mg21", "Meraki MG21 (Mobilfunk-WAN-Extender)", "fex", "router", T5(35, 33.25, 31.5, 29.75, 28)),
  SEP("mk-sep-sw", "──────── LAN-Switches ────────"),
  SDEV("mk-ms120-8lp", "Meraki MS120-8LP", "switch", "ap_switch", T5(50, 47.5, 45, 42.5, 40)),
  SDEV("mk-ms120-24p", "Meraki MS120-24P", "switch", "ap_switch", T5(100, 95, 90, 85, 80)),
  SDEV("mk-ms120-48lp", "Meraki MS120-48LP", "switch", "ap_switch", T5(150, 142.5, 135, 127.5, 120)),
  SDEV("mk-ms125-24p", "Meraki MS125-24P", "switch", "ap_switch", T5(150, 142.5, 135, 127.5, 120)),
  SDEV("mk-ms125-48lp", "Meraki MS125-48LP", "switch", "ap_switch", T5(170, 161.5, 153, 144.5, 136)),
  SDEV("mk-ms225-24p", "Meraki MS225-24P", "switch", "ap_switch", T5(190, 180.5, 171, 161.5, 152)),
  SDEV("mk-ms225-48lp", "Meraki MS225-48LP", "switch", "ap_switch", T5(270, 256.5, 243, 229.5, 216)),
  SDEV("mk-ms390-24p", "Meraki MS390-24P", "switch", "ap_switch", T5(270, 256.5, 243, 229.5, 216)),
  SDEV("mk-ms390-48p", "Meraki MS390-48P", "switch", "ap_switch", T5(250, 237.5, 225, 212.5, 200)),
  SEP("mk-sep-ap", "──────── WLAN-Accesspoints ────────"),
  SDEV("mk-mr28", "Meraki MR28", "ap", "ap_switch", T5(30, 28.5, 27, 25.5, 24)),
  SDEV("mk-mr44", "Meraki MR44", "ap", "ap_switch", T5(50, 47.5, 45, 42.5, 40)),
  SDEV("mk-mr56", "Meraki MR56", "ap", "ap_switch", T5(60, 57, 54, 51, 48)),
]};
// ===== SD-WAN Smart Connect (nur Hardware – kein Redundanz, keine Auto-Positionen; läuft über die normale
// generische Produktlogik, da Fixpreis über die gesamte Laufzeit 36/48/60 und kein Sonderverhalten nötig ist) =====
const SC_LZ = [36, 48, 60];
const SC_DESC = ["Smart Connect: nur in Kombination mit O2 Business All-IP Access Asymmetrisch am selben Standort", "Mindestlaufzeit 36 Monate · Bereitstellung & Abrechnung getrennt von All-IP"];
const SC = (id, name, m) => ({ id, name, desc: SC_DESC, laufzeiten: SC_LZ, m36: m, o36: 0, qty: true, qtyLabel: "Menge", standort: true });
const DB_SDWAN_SMART_FORTINET = { label: "SD-WAN Smart Connect Fortinet", products: [
  SC("sc-fg40-sdwan", "Fortinet FG 40 – SD-WAN (Smart Connect)", 60),
  SC("sc-fg40-atp", "Fortinet FG 40 – Basic Security (ATP) (Smart Connect)", 100),
  SC("sc-fg40-utp", "Fortinet FG 40 – Advanced Security (UTP) (Smart Connect)", 120),
  SC("sc-fg60-sdwan", "Fortinet FG 60 – SD-WAN (Smart Connect)", 70),
  SC("sc-fg60-atp", "Fortinet FG 60 – Basic Security (ATP) (Smart Connect)", 120),
  SC("sc-fg60-utp", "Fortinet FG 60 – Advanced Security (UTP) (Smart Connect)", 140),
  SC("sc-fg80-sdwan", "Fortinet FG 80 – SD-WAN (Smart Connect)", 100),
  SC("sc-fg80-atp", "Fortinet FG 80 – Basic Security (ATP) (Smart Connect)", 150),
  SC("sc-fg80-utp", "Fortinet FG 80 – Advanced Security (UTP) (Smart Connect)", 180),
  SC("sc-fg100-sdwan", "Fortinet FG 100 – SD-WAN (Smart Connect)", 170),
  SC("sc-fg100-atp", "Fortinet FG 100 – Basic Security (ATP) (Smart Connect)", 270),
  SC("sc-fg100-utp", "Fortinet FG 100 – Advanced Security (UTP) (Smart Connect)", 340),
]};
const DB_SDWAN_SMART_MERAKI = { label: "SD-WAN Smart Connect Meraki", products: [
  SC("sc-mkz3-sdwan", "Meraki Z3 – SD-WAN (Smart Connect)", 35),
  SC("sc-mkmx67-sdwan", "Meraki MX67 – SD-WAN (Smart Connect)", 60),
  SC("sc-mkmx67-atp", "Meraki MX67 – Basic Security (Smart Connect)", 80),
  SC("sc-mkmx67-utp", "Meraki MX67 – Advanced Security (Smart Connect)", 100),
  SC("sc-mkmx95-sdwan", "Meraki MX95 – SD-WAN (Smart Connect)", 200),
  SC("sc-mkmx95-atp", "Meraki MX95 – Basic Security (Smart Connect)", 290),
  SC("sc-mkmx95-utp", "Meraki MX95 – Advanced Security (Smart Connect)", 350),
]};
const DB_VPNCONNECT = { label: "VPN Connect", products: [] };

const BASE_CATALOG = { allip: DB_ALLIP, allippure: DB_ALLIPPURE, allipbasic: DB_ALLIPBASIC, dpbusiness: DB_DIGITALPHONE_BUSINESS, dpbasic: DB_DIGITALPHONE_BASIC, siptrunk: DB_SIPTRUNK, mdm: DB_MDM, office365: DB_OFFICE365, sdwan: DB_SDWAN, sdwanmeraki: DB_SDWAN_MERAKI, sdwansmartfortinet: DB_SDWAN_SMART_FORTINET, sdwansmartmeraki: DB_SDWAN_SMART_MERAKI, vpnconnect: DB_VPNCONNECT, individuell: { label: "Individuell", products: [] }, standort: { label: "Standortangebot", products: [] } };
const AREAS = Object.entries(BASE_CATALOG).map(([key, v]) => ({ key, label: v.label }));
const SUBAREAS = AREAS.filter(a => a.key !== "standort");

// ===== Editierbarer Katalog: Basis bleibt unveränderlich, CATALOG wird aus Overrides aufgebaut =====
const PRICE_FIELDS = ["m24", "m36", "o24", "o36", "loPrice", "mgdPrice"];
const cloneCatalog = () => {
  const out = {};
  for (const [area, db] of Object.entries(BASE_CATALOG)) {
    out[area] = { ...db, products: (db.products || []).map(p => ({ ...p, desc: Array.isArray(p.desc) ? [...p.desc] : p.desc })) };
  }
  return out;
};
let CATALOG = cloneCatalog();
// Wendet Produkt- und Options-Overrides auf die mutablen Module-Strukturen an.
const applyOverrides = (ov) => {
  const next = cloneCatalog();
  const po = (ov && ov.products) || {};
  for (const [pk, patch] of Object.entries(po)) {
    const sep = pk.indexOf("::"); if (sep < 0) continue;
    const area = pk.slice(0, sep), id = pk.slice(sep + 2);
    const prod = next[area]?.products.find(p => p.id === id);
    if (!prod) continue;
    if (patch.name != null && patch.name !== "") prod.name = patch.name;
    if (Array.isArray(patch.desc)) prod.desc = [...patch.desc];
    for (const f of PRICE_FIELDS) if (patch[f] != null && patch[f] !== "" && f in prod) prod[f] = Number(patch[f]);
  }
  // Eigene, im Editor neu angelegte Produkte einmischen (haben kein Original in BASE_CATALOG)
  for (const c of (ov && ov.custom) || []) {
    if (!next[c.area]) continue;
    next[c.area].products.push({ ...c });
  }
  CATALOG = next;
  // Options-Texte
  const oo = (ov && ov.opts) || {};
  OPT = Object.fromEntries(Object.entries(OPT_DEFAULTS).map(([k, v]) => [k, oo[k] != null ? cloneVal(oo[k]) : cloneVal(v)]));
  // Live-Strukturen, die Texte spiegeln
  RICHTUNGEN[0].fairText = optText("kanal_fest"); RICHTUNGEN[1].fairText = optText("kanal_mob");
  RICHTUNGEN[2].fairText = optText("kanal_eu"); RICHTUNGEN[3].fairText = optText("kanal_world");
  for (const a of ANTENNEN) a.label = optText("ant_" + a.key);
};

const CATALOG_STORE_KEY = "tef-catalog-overrides-v1";
const eur = (n) => new Intl.NumberFormat("de-DE", { style: "currency", currency: "EUR" }).format(isFinite(n) ? n : 0);

let ridc = 1, gidc = 1;
const newRow = () => ({ id: ridc++, productId: "", qty: 1, standort: "", term: null, monthly: 0, oneoff: 0, discountFix: false, discountPct: 0, discountMode: "verrechnet", abloese: 0, teamsCoupling: false, backup: false, express: false, kanal: 0, kanalTarife: blankKanalTarife(), custName: "", custDesc: "", zweiterKanal: false, router: false, euPlus: false, worldSelect: false, basicRabatt: "", basicMode: "verrechnet", lineProvider: "dtag", onkz: "", lineRabatt: "0", mobileSpeed: "25", mobileRabatt: "0", antenne: "", businessClass: false, internetFlat: false, sdwanLicense: "sdwan", sdwanPrice: "", redundanz: false, pureRabatt: "0" });
const newGroup = () => ({ id: gidc++, area: "", rows: [newRow()], kombi: false, dpPct: 0, dpMode: "verrechnet", managed: false, customArea: "", showDesc: false, subGroups: [], standortName: "", sdwanTerm: 60 });
let cidc = 1;
const newVpnConn = (access = "asym") => ({ id: cidc++, access, bw: access === "asym" ? "100" : access === "link" ? "100" : access === "ipsec" ? "25" : "", onkz: "", provider: "dtag", rabatt: "0", manMonthly: "", manOneoff: "", internet: false, qos: false, antenne: "", dataPack: "1g" });
// Findet eine Gruppe per id – entweder eine Top-Level-Gruppe oder eine Unterbereich-Gruppe innerhalb eines
// Standortangebots – und wendet fn darauf an. Ermöglicht, dass Zeilen-/Bereichsfunktionen unverändert für
// beide Ebenen funktionieren, ohne sie zu duplizieren.
const mapGroupById = (gs, gid, fn) => gs.map(g => {
  if (g.id === gid) return fn(g);
  if (g.area === "standort" && g.subGroups) {
    const idx = g.subGroups.findIndex(sg => sg.id === gid);
    if (idx >= 0) { const subGroups = [...g.subGroups]; subGroups[idx] = fn(subGroups[idx]); return { ...g, subGroups }; }
  }
  return g;
});

const getProduct = (area, id) => (area && id && CATALOG[area]?.products.find(p => p.id === id)) || null;
const defaultTerm = (p) => { const lz = p?.laufzeiten; if (!lz?.length) return null; return lz.includes(36) ? 36 : lz[0]; };
const resolvePrice = (p, term) => {
  if (!p || p.priceOpen) return { monthly: 0, oneoff: 0 };
  const tier = term && term >= 36 ? "36" : "24", alt = tier === "36" ? "24" : "36";
  const pick = (b) => p[b + tier] != null ? p[b + tier] : p[b + alt] != null ? p[b + alt] : 0;
  return { monthly: pick("m"), oneoff: pick("o") };
};
const hasLizenz = (g) => (g.area === "dpbusiness" || g.area === "dpbasic") && g.rows.some(r => getProduct(g.area, r.productId)?.produkttyp === "lizenz");
const dpStats = (g) => {
  let monthlyList = 0, bezahl = 0, found = false;
  for (const r of g.rows) {
    const p = getProduct(g.area, r.productId);
    if (!p) continue;
    if (p.produkttyp === "basispaket" || p.produkttyp === "lizenz") {
      const qty = p.qty ? Math.max(1, Number(r.qty) || 1) : 1;
      monthlyList += resolvePrice(p, r.term).monthly * qty;
      if (!found) { found = true; bezahl = Math.max(0, (Number(r.term) || 0) - (Number(r.abloese) || 0)); }
    }
  }
  const umsatz = monthlyList * bezahl;
  return { monthlyList, bezahl, umsatz, maxPct: dpMaxPct(umsatz, g.kombi) };
};

// Baut alle Angebotszeilen eines SD-WAN-Bereichs (Produkte + Auto-Positionen). gid = Bereichs-ID für die Zeilen.
const buildSdwanLines = (g, gid) => {
  const out = [];
  let installDevices = 0, standorte = 0, einrTotal = 0;
  for (const r of g.rows) {
    const p = getProduct(g.area, r.productId);
    if (!p || !p.sdwan) continue;
    const qty = Math.max(1, Number(r.qty) || 1);
    const term = Number(r.term) || 60;
    const lic = p.lic ? (r.sdwanLicense || "sdwan") : null;
    const meta = [];
    const desc = [];
    let monthly = 0, oneoff = 0, name = p.name;
    if (p.sdwanKind === "ad") {
      oneoff = SDWAN_AD_ONEOFF * qty;
      meta.push("Anzahl: " + qty);
      if (r.standort) meta.push("Standort: " + r.standort);
      out.push({ type: "product", rowId: r.id, name, desc, meta, monthly: 0, oneoff, abloeseNote: null, rabattNote: null, gid, listMonthly: 0 });
      continue;
    }
    const each = sdwanRowMonthlyEach(p, r);
    // Listenpreis = Boden (Bestpreis der Laufzeit). Ersparnis entsteht nur, wenn ein niedrigerer Preis eingetragen wurde.
    const floor = sdwanFloor(p, lic, term);
    const floorEach = (p.sdwanKind === "fg" && p.redundanzAllowed && r.redundanz) ? 2 * floor + (sdwanRedTable(p.vendor)[lic]?.[term] ?? 0) : floor;
    const listEach = Math.max(each, floorEach);
    if (p.sdwanKind === "remote") {
      monthly = each * qty;
      oneoff = SDWAN_REMOTE_SETUP * qty;
      desc.push("je Paket mit 25 Lizenzen");
      meta.push("Laufzeit: " + term + " Monate");
      meta.push(`Anzahl: ${qty} × 25 = ${qty * 25} Lizenzen`);
    } else {
      monthly = each * qty;
      if (lic) name = `${p.name} – ${sdwanLicLabel(p.vendor, lic)}`;
      if (p.sdwanKind === "fg" && p.redundanzAllowed && r.redundanz) desc.push(`inkl. redundantem SD-WAN-Router (Laufzeit-Nachlass ${eur(sdwanRedTable(p.vendor)[lic]?.[term] ?? 0)})`);
      meta.push("Laufzeit: " + term + " Monate");
      if (qty > 1) meta.push("Menge: " + qty);
    }
    if (r.standort) meta.push("Standort: " + r.standort);
    out.push({ type: "product", rowId: r.id, name, desc, meta, monthly, oneoff, abloeseNote: null, rabattNote: null, gid, listMonthly: listEach * qty });

    // Aggregationen für Auto-Positionen
    const redFactor = (p.sdwanKind === "fg" && p.redundanzAllowed && r.redundanz) ? 2 : 1;
    if (p.sdwanKind === "fg") { standorte += qty; installDevices += qty * redFactor; einrTotal += qty * redFactor * sdwanEinr(p.einrKind, term); }
    else if (p.sdwanKind === "vm") { standorte += qty; einrTotal += qty * sdwanEinr(p.einrKind, term); }
    else if (p.sdwanKind === "fex") { installDevices += qty; einrTotal += qty * sdwanEinr(p.einrKind, term); }
    else if (p.sdwanKind === "ap" || p.sdwanKind === "switch") { installDevices += qty; einrTotal += qty * sdwanEinr(p.einrKind, term); }
    // remote & ad: weder Installation noch SD-WAN-Service-Einrichtung
  }
  if (!out.length) return { lines: [], standorte: 0 };
  // Auto-Positionen
  if (installDevices > 0) out.push({ type: "auto", name: "Vor-Ort-Installation durch Service-Techniker", desc: [`${installDevices} Gerät(e) × ${eur(SDWAN_INSTALL_PER_DEVICE)}`], monthly: 0, oneoff: SDWAN_INSTALL_PER_DEVICE * installDevices, abloeseNote: null, gid });
  if (einrTotal > 0) out.push({ type: "auto", name: "Einrichtung SD-WAN-Service", desc: ["Konfiguration & Aktivierung (laufzeitabhängig, je Lizenz)"], monthly: 0, oneoff: einrTotal, abloeseNote: null, gid });
  const proj = sdwanProjectCost(standorte);
  const projDesc = [`${standorte} Standort(e) im Bereich`];
  out.push({ type: "auto", name: "Projektkosten SD-WAN", desc: projDesc, monthly: 0, oneoff: proj, abloeseNote: null, gid });
  return { lines: out, standorte };
};

// ===== VPN Connect – Preis-Engine (Tabellen 1:1 aus der VPN-Connect-Preisliste; NICHT mit anderen Produkten mischen) =====
const T3 = (a, b, c) => ({ 12: a, 24: b, 36: c });
const VPN_TERMS = [12, 24, 36];
const VPN_ASYM_LOW = T3(39.90, 27.90, 29.90), VPN_ASYM_HIGH = T3(54.90, 47.90, 44.90), VPN_ASYM_ONEOFF = T3(199, 149, 99);
const VPN_MOBILE_M = T3(34.90, 32.90, 29.90), VPN_MOBILE_ONEOFF = T3(199, 149, 99);
const VPN_IPSEC_ONEOFF = T3(199, 149, 99), VPN_LINK_ONEOFF = T3(199, 99, 0);
const VPN_LINK = {
  Metro:   { "2": T3(400,360,320), "4": T3(400,360,320), "10": T3(400,360,320), "20": T3(400,360,320), "50": T3(510,460,390), "100": T3(540,480,415), "200": T3(590,550,490), "500": T3(780,750,690), "1000": T3(940,890,800) },
  Regio:   { "2": T3(460,420,390), "4": T3(460,420,390), "10": T3(460,420,390), "20": T3(460,420,390), "50": T3(640,580,540), "100": T3(660,630,550), "200": T3(750,700,610), "500": T3(870,850,830), "1000": T3(1000,990,950) },
  Country: { "2": T3(510,470,440), "4": T3(510,470,440), "10": T3(510,470,440), "20": T3(510,470,440), "50": T3(700,680,610), "100": T3(730,710,620), "200": T3(800,760,690), "500": T3(1030,960,890), "1000": T3(1070,1060,1020) },
};
const VPN_SVC_LOW = T3(40, 35, 30), VPN_SVC_HIGH = T3(60, 55, 50), VPN_SVC_BACKUP = T3(7, 6, 5), VPN_MPLS = 1;
const VPN_DATAPACK = { "1g": T3(7,6,5), "5g": T3(40,30,20), "10g": T3(50,40,30), "25g": T3(60,50,40), "flat": T3(70,60,50) };
const VPN_DATAPACK_LABEL = { "1g": "1 GB", "5g": "5 GB", "10g": "10 GB", "25g": "25 GB", "flat": "Flatrate" };
const VPN_INTERNET_M = T3(54.90, 49.41, 43.92), VPN_INTERNET_O = 229;
const VPN_QOS = { low: T3(12,11,10), mid: T3(120,110,100), high: T3(240,220,200) };
const VPN_CITRIX_O = 229, VPN_NETFLOW_O = 149;
const VPN_ANTENNE = { in6: { label: "Indoor Decke bis 6 m", o: 219 }, in15: { label: "Indoor Decke bis 15 m", o: 329 }, outmast: { label: "Outdoor Mast", o: 379 }, in25: { label: "Indoor Decke bis 2,5 m", o: 39 }, in5: { label: "Indoor Decke bis 5 m", o: 59 } };
const VPN_ASYM_BW = ["1","2","6","16","25","50","100","175","250"];
const VPN_LINK_BW = ["2","4","10","20","50","100","200","500","1000"];
const VPN_IPSEC_BW = ["8","16","25","50"];
const VPN_ACCESS_LABEL = { asym: "Asymmetrisch", link: "Link Symmetrisch", mobile: "Mobilfunk", ipsec: "IPSec" };
const VPN_DESC_KEYS = ["asym", "link", "mobile", "ipsec"];
const DEFAULT_VPN_AREA_DESC = ["Hardware inklusive", "Gemanaged durch Telefónica"];
const resolveVpnDesc = (ov) => ({ area: (ov && "area" in ov) ? ov.area : DEFAULT_VPN_AREA_DESC, asym: ov?.asym || [], link: ov?.link || [], mobile: ov?.mobile || [], ipsec: ov?.ipsec || [] });

const vpnAccessBase = (conn, term) => {
  const t = Number(term);
  if (conn.access === "asym") return { monthly: (Number(conn.bw) >= 175 ? VPN_ASYM_HIGH : VPN_ASYM_LOW)[t], oneoff: VPN_ASYM_ONEOFF[t] };
  if (conn.access === "mobile") return { monthly: VPN_MOBILE_M[t], oneoff: VPN_MOBILE_ONEOFF[t] };
  if (conn.access === "ipsec") return { monthly: 0, oneoff: VPN_IPSEC_ONEOFF[t] };
  if (conn.access === "link") {
    if (conn.provider === "andere" || conn.rabatt === "pricing") return { monthly: Number(conn.manMonthly) || 0, oneoff: Number(conn.manOneoff) || 0, region: onkzRegion(conn.onkz) };
    const region = onkzRegion(conn.onkz);
    const cell = (VPN_LINK[region] || {})[conn.bw];
    let m = cell ? cell[t] : 0;
    const pct = Number(conn.rabatt) || 0;
    return { monthly: m * (1 - pct / 100), oneoff: VPN_LINK_ONEOFF[t], region, listMonthly: cell ? cell[t] : 0 };
  }
  return { monthly: 0, oneoff: 0 };
};
const vpnService = (conn, term, role) => {
  const t = Number(term);
  if (conn.access === "mobile") return { svc: role === "backup" ? VPN_SVC_BACKUP[t] : (VPN_DATAPACK[conn.dataPack] || VPN_DATAPACK["1g"])[t], mpls: VPN_MPLS };
  if (conn.access === "asym" || conn.access === "ipsec") return { svc: (Number(conn.bw) <= 16 ? VPN_SVC_LOW : VPN_SVC_HIGH)[t], mpls: 0 };
  if (conn.access === "link") return { svc: VPN_SVC_HIGH[t], mpls: 0 };
  return { svc: 0, mpls: 0 };
};
const vpnQosTier = (conn) => { const n = Number(conn.bw); return n <= 100 ? "low" : n <= 500 ? "mid" : "high"; };
const vpnBackupOptions = (main) => {
  if (!main || !main.access) return [];
  if (main.access === "ipsec") return [{ access: "mobile" }];
  if (main.access === "link") {
    const bw = String(main.bw), ipsecAll = { access: "ipsec", bw: ["8","16","25","50"] }, link = (b) => ({ access: "link", bw: [b] });
    const asym = (arr) => ({ access: "asym", bw: arr });
    if (["1000","500","200"].includes(bw)) return [link(bw), ipsecAll];
    if (bw === "100" || bw === "50") return [link(bw), ipsecAll, asym(["1","2","6","16","25","50","100","175","250"]), { access: "mobile" }];
    if (bw === "20") return [link(bw), { access: "ipsec", bw: ["8","16"] }, asym(["1","2","6","16","25","50","100"]), { access: "mobile" }];
    if (bw === "10") return [link(bw), { access: "ipsec", bw: ["8"] }, asym(["1","2","6","16","25","50"]), { access: "mobile" }];
    if (bw === "4" || bw === "2") return [link(bw), { access: "ipsec", bw: ["8"] }, asym(["1","2","6","16"]), { access: "mobile" }];
  }
  return [];
};

// Baut die Angebotszeilen EINES VPN-Standorts (eine vpnconnect-Gruppe = ein Standort). gid = Gruppen-ID.
const buildVpnLines = (g, vpnDesc) => {
  const vd = vpnDesc || resolveVpnDesc(null);
  const term = Number(g.vpnTerm) || 36, gid = g.id, out = [];
  const bwLabel = (c) => c.access === "mobile" ? "" : ` bis ${c.bw} Mbit/s`;
  const connLines = (c, role) => {
    if (!c || !c.access) return;
    const roleLabel = role === "backup" ? "Backup" : "Hauptanschluss";
    const base = vpnAccessBase(c, term);
    const svc = vpnService(c, term, role);
    const meta = [];
    if (c.access === "link") { meta.push("Region: " + (base.region || onkzRegion(c.onkz))); if (c.onkz) meta.push("ONKZ: " + c.onkz); if (Number(c.rabatt) > 0) meta.push(`Rabatt ${c.rabatt} %`); if (c.rabatt === "pricing" || c.provider === "andere") meta.push("Preis manuell"); }
    // Access-Zeile (inkl. evtl. Rabatt bereits eingerechnet)
    out.push({ type: "product", name: `${roleLabel}: ${VPN_ACCESS_LABEL[c.access]}${bwLabel(c)}`, desc: [...(vd[c.access] || [])], meta, monthly: base.monthly, oneoff: base.oneoff, gid, listMonthly: base.monthly });
    // VPN-Service (immer)
    if (c.access === "mobile") {
      out.push({ type: "product", name: `${roleLabel}: O2 Data-Pack ${role === "backup" ? "Backup" : VPN_DATAPACK_LABEL[c.dataPack] || ""}`.trim(), desc: [], meta: [], monthly: svc.svc, oneoff: 0, gid, listMonthly: svc.svc });
      out.push({ type: "product", name: `${roleLabel}: MPLS Link Option`, desc: [], meta: [], monthly: svc.mpls, oneoff: 0, gid, listMonthly: svc.mpls });
    } else {
      out.push({ type: "product", name: `${roleLabel}: VPN-Service`, desc: [], meta: [], monthly: svc.svc, oneoff: 0, gid, listMonthly: svc.svc });
    }
    // Optionen je Anschluss
    if (c.access === "link" && c.internet) out.push({ type: "product", name: `${roleLabel}: Option Internet`, desc: [], meta: [], monthly: VPN_INTERNET_M[term], oneoff: VPN_INTERNET_O, gid, listMonthly: VPN_INTERNET_M[term] });
    if ((c.access === "asym" || c.access === "link") && c.qos) { const q = VPN_QOS[vpnQosTier(c)][term]; out.push({ type: "product", name: `${roleLabel}: Quality of Service`, desc: [], meta: [], monthly: q, oneoff: 0, gid, listMonthly: q }); }
    if (c.access === "mobile" && c.antenne && VPN_ANTENNE[c.antenne]) out.push({ type: "product", name: `${roleLabel}: Antenne ${VPN_ANTENNE[c.antenne].label}`, desc: [], meta: [], monthly: 0, oneoff: VPN_ANTENNE[c.antenne].o, gid, listMonthly: 0 });
  };

  out.push({ type: "cluster", name: (g.vpnName || "").trim() || "VPN-Standort", gid, desc: (vd.area && vd.area.length) ? vd.area : undefined });
  connLines(g.main, "main");
  if (g.backup && g.backup.access) connLines(g.backup, "backup");
  // Standort-Optionen
  const anyQos = (g.main?.qos && (g.main.access === "asym" || g.main.access === "link")) || (g.backup?.qos && (g.backup.access === "asym" || g.backup.access === "link"));
  if (g.backup && g.backup.access && g.backupPlus) out.push({ type: "product", name: "Backup-Variante ++ (Redundanz über zwei Router)", desc: [], meta: [], monthly: Number(g.bpM) || 0, oneoff: Number(g.bpO) || 0, gid, listMonthly: Number(g.bpM) || 0 });
  if (g.netflow) out.push({ type: "product", name: "Option NetFlow", desc: [], meta: [], monthly: 0, oneoff: VPN_NETFLOW_O, gid, listMonthly: 0 });
  if (g.citrix && anyQos) out.push({ type: "product", name: "Option Citrix Priorisierung", desc: [], meta: [], monthly: 0, oneoff: VPN_CITRIX_O, gid, listMonthly: 0 });
  if (g.advHw) out.push({ type: "product", name: "Option Advanced Hardware", desc: [], meta: [], monthly: Number(g.advHwM) || 0, oneoff: Number(g.advHwO) || 0, gid, listMonthly: Number(g.advHwM) || 0 });
  return out;
};

// ===== Vorher/Nachher-Vergleich: vergleicht eine zuvor gesicherte Baseline mit dem aktuellen Stand einer Gruppe =====
const DIFF_FIELDS = [
  { key: "qty", label: "Menge" },
  { key: "term", label: "Laufzeit", fmt: v => v ? `${v} Mon.` : "—" },
  { key: "monthly", label: "Preis/Monat", fmt: v => eur(Number(v) || 0) },
  { key: "oneoff", label: "Einmalig", fmt: v => eur(Number(v) || 0) },
  { key: "standort", label: "Standort", fmt: v => v || "—" },
  { key: "redundanz", label: "Redundanz", fmt: v => v ? "an" : "aus" },
  { key: "sdwanLicense", label: "Lizenztyp" },
  { key: "sdwanPrice", label: "SD-WAN-Preis", fmt: v => v === "" || v == null ? "Bestpreis" : eur(Number(v)) },
  { key: "lineRabatt", label: "Rabatt", fmt: v => v === "pricing" ? "Pricing" : `${v || 0} %` },
  { key: "mobileRabatt", label: "Rabatt", fmt: v => v === "pricing" ? "Pricing" : `${v || 0} %` },
  { key: "pureRabatt", label: "Rabatt", fmt: v => `${v || 0} %` },
  { key: "basicRabatt", label: "Rabatt", fmt: v => v === "pricing" ? "Pricing" : (v ? `${v} %` : "kein Rabatt") },
  { key: "mobileSpeed", label: "Geschwindigkeit" },
  { key: "antenne", label: "Antenne", fmt: v => v || "keine" },
];
const diffRows = (baseRows, curRows, area) => {
  const out = { added: [], removed: [], changed: [] };
  const baseMap = new Map((baseRows || []).map(r => [r.id, r]));
  const curMap = new Map((curRows || []).map(r => [r.id, r]));
  const nameOf = (r) => getProduct(area, r.productId)?.name || r.custName || "—";
  for (const r of (curRows || [])) if (!baseMap.has(r.id)) out.added.push({ name: nameOf(r) });
  for (const r of (baseRows || [])) if (!curMap.has(r.id)) out.removed.push({ name: nameOf(r) });
  for (const r of (curRows || [])) {
    const b = baseMap.get(r.id); if (!b) continue;
    if (b.productId !== r.productId) { out.changed.push({ name: `${nameOf(b)} → ${nameOf(r)}`, changes: [] }); continue; }
    const changes = [];
    for (const f of DIFF_FIELDS) {
      if (!(f.key in r)) continue;
      const bv = b[f.key], cv = r[f.key];
      if (bv === cv || ((bv == null || bv === "") && (cv == null || cv === ""))) continue;
      const fmt = f.fmt || (v => (v == null || v === "" ? "—" : String(v)));
      changes.push(`${f.label} ${fmt(bv)} → ${fmt(cv)}`);
    }
    if (changes.length) out.changed.push({ name: nameOf(r), changes });
  }
  return out;
};
const buildGroupDiff = (baseline, current) => {
  if (!baseline || !current) return null;
  if (current.area === "standort") {
    const baseSubs = new Map((baseline.subGroups || []).map(sg => [sg.id, sg]));
    const curSubIds = new Set((current.subGroups || []).map(sg => sg.id));
    const subDiffs = [];
    for (const sg of (current.subGroups || [])) {
      const label = CATALOG[sg.area]?.label || sg.area || "Unterbereich";
      const bsg = baseSubs.get(sg.id);
      if (!bsg) { subDiffs.push({ label, addedSub: true }); continue; }
      const d = diffRows(bsg.rows, sg.rows, sg.area);
      if (d.added.length || d.removed.length || d.changed.length) subDiffs.push({ label, ...d });
    }
    for (const sg of (baseline.subGroups || [])) if (!curSubIds.has(sg.id)) subDiffs.push({ label: CATALOG[sg.area]?.label || sg.area, removedSub: true });
    return { kind: "standort", subDiffs };
  }
  if (current.area === "vpnconnect") {
    const changes = [];
    const cmpConn = (b, c, role) => {
      if (!b?.access && !c?.access) return;
      if (!b?.access && c?.access) { changes.push(`${role} hinzugefügt (${VPN_ACCESS_LABEL[c.access] || c.access})`); return; }
      if (b?.access && !c?.access) { changes.push(`${role} entfernt`); return; }
      if (b.access !== c.access) changes.push(`${role}: ${VPN_ACCESS_LABEL[b.access] || b.access} → ${VPN_ACCESS_LABEL[c.access] || c.access}`);
      else if (b.bw !== c.bw) changes.push(`${role}: Bandbreite ${b.bw} → ${c.bw}`);
      if (b.rabatt !== c.rabatt) changes.push(`${role}: Rabatt ${b.rabatt} → ${c.rabatt}`);
    };
    cmpConn(baseline.main, current.main, "Hauptanschluss");
    cmpConn(baseline.backup, current.backup, "Backup");
    if (baseline.vpnTerm !== current.vpnTerm) changes.push(`Laufzeit ${baseline.vpnTerm} → ${current.vpnTerm} Monate`);
    for (const [k, lbl] of [["citrix", "Citrix"], ["netflow", "NetFlow"], ["advHw", "Advanced Hardware"], ["backupPlus", "Backup ++"]]) {
      if (!!baseline[k] !== !!current[k]) changes.push(`${lbl} ${baseline[k] ? "an" : "aus"} → ${current[k] ? "an" : "aus"}`);
    }
    return { kind: "vpn", changes };
  }
  return { kind: "rows", ...diffRows(baseline.rows, current.rows, current.area) };
};

export default function App() {
  const today = new Date().toLocaleDateString("de-DE");
  const [customer, setCustomer] = useState("");
  const [groups, setGroups] = useState([newGroup()]);
  const [copied, setCopied] = useState(false);
  const [visibleSums, setVisibleSums] = useState(new Set());
  const [showSavings, setShowSavings] = useState(false);
  const [showTCO, setShowTCO] = useState(false);
  const [showAllSums, setShowAllSums] = useState(false);
  const [istKosten, setIstKosten] = useState("");
  const fileRef = useRef(null);
  const cfgRef = useRef(null);
  const rowFieldRefs = useRef({});
  const [focusRowId, setFocusRowId] = useState(null);

  // ===== Editierbarer Katalog =====
  const [tab, setTab] = useState("calc");                 // "calc" | "edit" | "import"
  const [importText, setImportText] = useState("");
  const [importAsStandort, setImportAsStandort] = useState(true);
  const [importMsg, setImportMsg] = useState("");
  const [overrides, setOverrides] = useState({ products: {}, opts: {}, deleted: [], custom: [], areaDesc: {}, vpnDesc: {} });
  const [catVer, setCatVer] = useState(0);                // erzwingt Neuberechnung nach Katalogänderung
  const [editSearch, setEditSearch] = useState("");
  const [editArea, setEditArea] = useState("all");
  const [editChangedOnly, setEditChangedOnly] = useState(false);
  const [editEmptyOnly, setEditEmptyOnly] = useState(false);
  const [addOpenAreas, setAddOpenAreas] = useState(new Set());
  const [newDrafts, setNewDrafts] = useState({});
  const [cfgStatus, setCfgStatus] = useState("");
  const [lastAutoSave, setLastAutoSave] = useState(null);
  const AUTOSAVE_KEY = "tef-autosave-v1";
  const [baselines, setBaselines] = useState({});
  const [diffOpen, setDiffOpen] = useState({});
  const saveBaseline = (g) => setBaselines(b => ({ ...b, [g.id]: JSON.parse(JSON.stringify(g)) }));
  const clearBaseline = (gid) => { setBaselines(b => { const n = { ...b }; delete n[gid]; return n; }); setDiffOpen(d => ({ ...d, [gid]: false })); };
  const toggleDiff = (gid) => setDiffOpen(d => ({ ...d, [gid]: !d[gid] }));

  const resetOffer = () => {
    if (!window.confirm("Gesamtes Angebot wirklich leeren? Kunde, alle Bereiche und Produkte werden entfernt. Rückgängig nur über den Button 'Letzten Stand wiederherstellen' oder eine zuvor gespeicherte .json-Datei.")) return;
    setGroups([newGroup()]);
    setCustomer("");
    setIstKosten("");
    setBaselines({});
    setDiffOpen({});
    setVisibleSums(new Set());
    setShowAllSums(false);
    setShowTCO(false);
    setShowSavings(false);
  };

  // Beim Start: gespeicherte Overrides laden (window.storage, persistent über Sitzungen)
  useEffect(() => {
    (async () => {
      try {
        const res = await window.storage?.get(CATALOG_STORE_KEY, true);
        if (res?.value) {
          const ov = JSON.parse(res.value);
          const norm = { products: ov.products || {}, opts: ov.opts || {}, deleted: ov.deleted || [], custom: ov.custom || [], areaDesc: ov.areaDesc || {}, vpnDesc: ov.vpnDesc || {} };
          applyOverrides(norm); setOverrides(norm); setCatVer(v => v + 1);
        }
      } catch { /* keine gespeicherten Daten / Storage nicht verfügbar */ }
    })();
  }, []);

  // Neue Zeile automatisch fokussieren (nach Klick auf „Produkt hinzufügen" oder Enter im Mengenfeld)
  useEffect(() => {
    if (focusRowId == null) return;
    const el = rowFieldRefs.current[focusRowId];
    if (el) { el.focus(); setFocusRowId(null); }
  }, [focusRowId, groups]);

  // Auto-Save: sichert den aktuellen Stand alle 60 Sekunden im Hintergrund (Schutz vor Datenverlust)
  useEffect(() => {
    const iv = setInterval(() => {
      if (!groups.some(g => g.area)) return; // nichts Sinnvolles zu sichern
      (async () => {
        try {
          await window.storage?.set(AUTOSAVE_KEY, buildState(), false);
          setLastAutoSave(new Date());
        } catch { /* Storage evtl. nicht verfügbar */ }
      })();
    }, 60000);
    return () => clearInterval(iv);
  }, [groups, customer]);

  const restoreAutoSave = async () => {
    try {
      const res = await window.storage?.get(AUTOSAVE_KEY, false);
      if (!res?.value) { alert("Kein automatisch gesicherter Stand gefunden."); return; }
      const data = JSON.parse(res.value);
      const when = data.savedAt ? new Date(data.savedAt).toLocaleString("de-DE") : "unbekannt";
      if (!window.confirm(`Letzten automatisch gesicherten Stand wiederherstellen (gesichert am ${when})? Die aktuelle Eingabe wird ersetzt.`)) return;
      restore(data);
    } catch { alert("Automatische Sicherung konnte nicht geladen werden."); }
  };

  const persistOverrides = async (ov) => {
    applyOverrides(ov); setOverrides(ov); setCatVer(v => v + 1);
    try { await window.storage?.set(CATALOG_STORE_KEY, JSON.stringify(ov), true); } catch { /* ignore */ }
  };
  // Effektiven (= ggf. überschriebenen) Produktwert holen
  const effProduct = (area, id) => CATALOG[area]?.products.find(p => p.id === id) || null;
  const setProductField = (area, id, field, value) => {
    const key = `${area}::${id}`;
    const next = { ...overrides, products: { ...overrides.products, [key]: { ...(overrides.products[key] || {}), [field]: value } } };
    persistOverrides(next);
  };
  const setProductDesc = (area, id, text) => setProductField(area, id, "desc", descLines(text));
  const resetProduct = (area, id) => {
    const key = `${area}::${id}`; const prods = { ...overrides.products }; delete prods[key];
    persistOverrides({ ...overrides, products: prods });
  };
  const setOptField = (key, value) => persistOverrides({ ...overrides, opts: { ...overrides.opts, [key]: value } });
  const resetOpt = (key) => { const opts = { ...overrides.opts }; delete opts[key]; persistOverrides({ ...overrides, opts }); };

  // Bereichsbeschreibung (global je Bereich, im Editor anpassbar)
  const descLines = (text) => { const arr = (text || "").split("\n"); return arr.every(l => l.trim() === "") ? [] : arr; };
  const setAreaDescText = (area, text) => {
    persistOverrides({ ...overrides, areaDesc: { ...overrides.areaDesc, [area]: descLines(text) } });
  };
  const resetAreaDesc = (area) => { const ad = { ...overrides.areaDesc }; delete ad[area]; persistOverrides({ ...overrides, areaDesc: ad }); };
  // Liefert die im Header anzuzeigende Beschreibung und entfernt – wenn aktiv – die gemeinsamen Zeilen aus den Produktzeilen.
  // manuell gepflegt (overrides.areaDesc) hat Vorrang; sonst automatisch = Schnittmenge der Beschreibungen aller Produktzeilen.
  const applyAreaHeaderDesc = (g, groupLines) => {
    if (!g.showDesc) return null;
    const productLines = groupLines.filter(l => l.type === "product");
    if (!productLines.length) return null;
    const manual = overrides.areaDesc?.[g.area];
    let header;
    if (manual && manual.length) header = manual;
    else { const sets = productLines.map(l => new Set(l.desc || [])); header = (productLines[0].desc || []).filter(line => sets.every(s => s.has(line))); }
    if (!header.length) return null;
    const headerSet = new Set(header);
    for (const l of productLines) l.desc = (l.desc || []).filter(line => !headerSet.has(line));
    return header;
  };

  // Basis-Produkte: löschen = nur aus der Auswahl-Liste ausblenden (bereits platzierte Angebotszeilen bleiben unverändert rechenbar)
  const isDeleted = (area, id) => (overrides.deleted || []).includes(`${area}::${id}`);
  const deleteProduct = (area, id, name) => {
    if (!window.confirm(`„${name}" aus der Produktauswahl entfernen?\n\nBereits in Angeboten platzierte Zeilen mit diesem Produkt rechnen unverändert weiter. Über „Wiederherstellen" kannst du es jederzeit zurückholen.`)) return;
    const key = `${area}::${id}`;
    if ((overrides.deleted || []).includes(key)) return;
    persistOverrides({ ...overrides, deleted: [...(overrides.deleted || []), key] });
  };
  const restoreProduct = (area, id) => persistOverrides({ ...overrides, deleted: (overrides.deleted || []).filter(k => k !== `${area}::${id}`) });

  // Eigene Produkte: vollständig im Editor verwaltet (kein Original in BASE_CATALOG)
  const addCustomProduct = (area, draft) => {
    const name = (draft.name || "").trim();
    if (!name) return;
    const id = `custom-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`;
    const desc = (draft.desc || "").split("\n").map(s => s.trim()).filter(Boolean);
    const product = draft.priceOpen
      ? { area, id, name, desc, laufzeiten: [], priceOpen: true, ...(draft.qty ? { qty: true, qtyLabel: (draft.qtyLabel || "Menge").trim() || "Menge" } : {}) }
      : { area, id, name, desc, laufzeiten: LZ, m24: Number(draft.m24) || 0, m36: Number(draft.m36) || 0, o24: Number(draft.o24) || 0, o36: Number(draft.o36) || 0, ...(draft.qty ? { qty: true, qtyLabel: (draft.qtyLabel || "Menge").trim() || "Menge" } : {}) };
    persistOverrides({ ...overrides, custom: [...(overrides.custom || []), product] });
  };
  const deleteCustomProduct = (area, id, name) => {
    if (!window.confirm(`Eigenes Produkt „${name}" endgültig löschen?\n\nBereits in Angeboten platzierte Zeilen mit diesem Produkt rechnen danach nicht mehr korrekt.`)) return;
    persistOverrides({ ...overrides, custom: (overrides.custom || []).filter(c => !(c.area === area && c.id === id)) });
  };
  const setVpnDescField = (key, text) => { persistOverrides({ ...overrides, vpnDesc: { ...overrides.vpnDesc, [key]: descLines(text) } }); };
  const resetVpnDesc = () => { const ov = { ...overrides }; delete ov.vpnDesc; persistOverrides({ ...ov, vpnDesc: {} }); };
  const setCustomField = (area, id, field, value) => persistOverrides({ ...overrides, custom: (overrides.custom || []).map(c => (c.area === area && c.id === id) ? { ...c, [field]: value } : c) });
  const setCustomDesc = (area, id, text) => setCustomField(area, id, "desc", descLines(text));

  // Produkt duplizieren: vollständiger, unabhängiger Klon (inkl. aller Preis-/Logik-Felder) als eigenes Produkt im selben Bereich
  const cloneId = (srcId) => `${srcId}-copy-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
  const duplicateProduct = (area, baseProd) => {
    const ov = overrides.products[`${area}::${baseProd.id}`] || {};
    const merged = { ...baseProd, ...("name" in ov ? { name: ov.name } : {}), ...("desc" in ov ? { desc: ov.desc } : {}) };
    for (const f of PRICE_FIELDS) if (f in ov && ov[f] !== "" && ov[f] != null) merged[f] = Number(ov[f]);
    const clone = JSON.parse(JSON.stringify(merged));
    clone.id = cloneId(baseProd.id); clone.name = `${merged.name} (Kopie)`; clone.area = area;
    persistOverrides({ ...overrides, custom: [...(overrides.custom || []), clone] });
  };
  const duplicateCustomProduct = (area, c) => {
    const clone = JSON.parse(JSON.stringify(c));
    clone.id = cloneId(c.id); clone.name = `${c.name} (Kopie)`; clone.area = area;
    persistOverrides({ ...overrides, custom: [...(overrides.custom || []), clone] });
  };

  // 'Neues Produkt'-Formular (ein Entwurf pro Bereich, lokal bis zum Absenden)
  const blankDraft = { name: "", desc: "", priceOpen: false, m24: "", m36: "", o24: "", o36: "", qty: false, qtyLabel: "" };
  const getDraft = (area) => newDrafts[area] || blankDraft;
  const setDraftField = (area, field, value) => setNewDrafts(d => ({ ...d, [area]: { ...getDraft(area), [field]: value } }));
  const toggleAddOpen = (area) => setAddOpenAreas(prev => { const n = new Set(prev); n.has(area) ? n.delete(area) : n.add(area); return n; });
  const submitNewProduct = (area) => {
    const draft = getDraft(area);
    if (!draft.name?.trim()) return;
    addCustomProduct(area, draft);
    setNewDrafts(d => ({ ...d, [area]: undefined }));
    setAddOpenAreas(prev => { const n = new Set(prev); n.delete(area); return n; });
  };
  const resetAllOverrides = () => { if (window.confirm("Alle Anpassungen zurücksetzen und Originalwerte wiederherstellen? Eigene hinzugefügte Produkte werden dabei ebenfalls gelöscht.")) persistOverrides({ products: {}, opts: {}, deleted: [], custom: [], areaDesc: {}, vpnDesc: {} }); };

  const exportConfig = () => {
    const blob = new Blob([JSON.stringify({ app: "tef-kalkulator-config", version: 1, savedAt: new Date().toISOString(), overrides }, null, 2)], { type: "application/json" });
    triggerDownload(blob, "tef-kalkulator-konfiguration.json");
  };
  const importConfig = (e) => {
    const f = e.target.files?.[0]; if (!f) return;
    const reader = new FileReader();
    reader.onload = () => { try { const d = JSON.parse(String(reader.result)); const ov = d.overrides || d; const norm = { products: ov.products || {}, opts: ov.opts || {}, deleted: ov.deleted || [], custom: ov.custom || [], areaDesc: ov.areaDesc || {}, vpnDesc: ov.vpnDesc || {} }; persistOverrides(norm); setCfgStatus("Konfiguration importiert."); setTimeout(() => setCfgStatus(""), 3000); } catch { setCfgStatus("Datei konnte nicht gelesen werden."); } };
    reader.readAsText(f); e.target.value = "";
  };

  const toggleSum = (gid) => setVisibleSums(prev => { const n = new Set(prev); n.has(gid) ? n.delete(gid) : n.add(gid); return n; });
  const toggleAreaDesc = (gid) => setGroups(gs => mapGroupById(gs, gid, g => ({ ...g, showDesc: !g.showDesc })));
  // VPN Connect: bereichsweite Laufzeit (gilt für ALLE VPN-Gruppen), Connection-Patch, Backup an/aus
  const setVpnTerm = (term) => setGroups(gs => gs.map(g => g.area === "vpnconnect" ? { ...g, vpnTerm: term } : (g.area === "standort" ? { ...g, subGroups: (g.subGroups || []).map(sg => sg.area === "vpnconnect" ? { ...sg, vpnTerm: term } : sg) } : g)));
  // Globale Laufzeit: setzt die Laufzeit aller Produkte/Bereiche (je Produkt auf zulässigen Wert geklemmt)
  const clampTerm = (lz, term) => { if (!lz || !lz.length) return null; if (lz.includes(term)) return term; const below = lz.filter(t => t <= term); return below.length ? Math.max(...below) : Math.min(...lz); };
  const applyTermToGroup = (g, term) => {
    if (g.area === "vpnconnect") return { ...g, vpnTerm: VPN_TERMS.includes(term) ? term : (term > 36 ? 36 : 12) };
    const ng = { ...g };
    if (g.area === "individuell") { ng.rows = (g.rows || []).map(r => ({ ...r, term })); ng.subGroups = (g.subGroups || []).map(sg => applyTermToGroup(sg, term)); return ng; }
    if (g.area === "sdwan" || g.area === "sdwanmeraki") {
      const st = SDWAN_TERMS.includes(term) ? term : (term < 12 ? 12 : 60);
      ng.sdwanTerm = st;
      ng.rows = (g.rows || []).map(r => { const p = getProduct(g.area, r.productId); if (!p || p.sdwanKind === "ad") return r; return { ...r, term: st, sdwanPrice: "" }; });
    } else {
      ng.rows = (g.rows || []).map(r => {
        const p = getProduct(g.area, r.productId);
        const ct = clampTerm(p?.laufzeiten, term);
        if (ct == null) return r;
        const keepManual = p.priceOpen || r.lineRabatt === "pricing" || r.mobileRabatt === "pricing" || r.basicRabatt === "pricing";
        if (keepManual) return { ...r, term: ct };
        const pr = resolvePrice(p, ct);
        return { ...r, term: ct, monthly: pr.monthly, oneoff: pr.oneoff };
      });
    }
    ng.subGroups = (g.subGroups || []).map(sg => applyTermToGroup(sg, term));
    return ng;
  };
  const setAllTerms = (term) => { if (!term) return; setGroups(gs => gs.map(g => applyTermToGroup(g, term))); };
  const patchVpnConn = (gid, role, patch) => setGroups(gs => mapGroupById(gs, gid, g => ({ ...g, [role]: { ...g[role], ...patch } })));
  const setVpnAccess = (gid, role, access) => setGroups(gs => mapGroupById(gs, gid, g => {
    const fresh = { ...newVpnConn(access), id: g[role]?.id || newVpnConn().id };
    if (role === "backup") { const opt = vpnBackupOptions(g.main).find(o => o.access === access); if (opt?.bw?.length) fresh.bw = opt.bw[opt.bw.length - 1]; }
    return { ...g, [role]: fresh };
  }));
  const toggleVpnBackup = (gid) => setGroups(gs => mapGroupById(gs, gid, g => {
    if (g.backup) return { ...g, backup: null, backupPlus: false };
    const opts = vpnBackupOptions(g.main);
    if (!opts.length) return g;
    const first = opts[0];
    return { ...g, backup: { ...newVpnConn(first.access), bw: first.bw ? first.bw[first.bw.length - 1] : "" } };
  }));

  const patchGroup = (gid, p) => setGroups(gs => mapGroupById(gs, gid, g => ({ ...g, ...p })));
  // area kann ein Top-Level-Bereich oder ein Unterbereich innerhalb eines Standortangebots sein – mapGroupById findet beide.
  const setArea = (gid, area) => setGroups(gs => {
    const existingVpnTerm = gs.find(x => x.area === "vpnconnect")?.vpnTerm;
    return mapGroupById(gs, gid, g => ({
      ...g, area,
      rows: area === "standort" || area === "vpnconnect" ? [] : [newRow()],
      subGroups: area === "standort" ? [newGroup()] : [],
      standortName: "",
      kombi: false, dpPct: 0, dpMode: "verrechnet", managed: false, customArea: "", showDesc: false,
      ...(area === "vpnconnect" ? {
        vpnTerm: VPN_TERMS.includes(Number(existingVpnTerm)) ? Number(existingVpnTerm) : 36,
        vpnName: "", main: newVpnConn("asym"), backup: null,
        citrix: false, netflow: false, advHw: false, advHwM: "0", advHwO: "0", backupPlus: false, bpM: "0", bpO: "0",
      } : {}),
    }));
  });
  const addGroup = () => setGroups(gs => [...gs, newGroup()]);
  const removeGroup = (gid) => setGroups(gs => gs.length <= 1 ? [newGroup()] : gs.filter(g => g.id !== gid));
  const moveGroupDir = (gid, dir) => setGroups(gs => { const i = gs.findIndex(g => g.id === gid), j = i + dir; if (i < 0 || j < 0 || j >= gs.length) return gs; const arr = [...gs]; [arr[i], arr[j]] = [arr[j], arr[i]]; return arr; });
  const addRow = (gid) => { const nr = newRow(); setGroups(gs => mapGroupById(gs, gid, g => ({ ...g, rows: [...g.rows, nr] }))); setFocusRowId(nr.id); };
  const removeRow = (gid, rid) => setGroups(gs => mapGroupById(gs, gid, g => ({ ...g, rows: g.rows.length <= 1 ? [newRow()] : g.rows.filter(r => r.id !== rid) })));
  const patchRow = (gid, rid, p) => setGroups(gs => mapGroupById(gs, gid, g => ({ ...g, rows: g.rows.map(r => r.id === rid ? { ...r, ...p } : r) })));
  const moveRowDir = (gid, rid, dir) => setGroups(gs => mapGroupById(gs, gid, g => {
    const rows = [...g.rows];
    const i = rows.findIndex(r => r.id === rid), j = i + dir;
    if (i < 0 || j < 0 || j >= rows.length) return g;
    [rows[i], rows[j]] = [rows[j], rows[i]];
    return { ...g, rows };
  }));
  // Duplizieren: Zeile / Bereich / Unterbereich – jeweils mit frisch vergebenen IDs, direkt hinter dem Original eingefügt
  const reidGroup = (g) => ({
    ...g, id: gidc++,
    rows: (g.rows || []).map(r => ({ ...r, id: ridc++ })),
    subGroups: (g.subGroups || []).map(reidGroup),
    ...(g.area === "vpnconnect" ? { main: g.main ? { ...g.main, id: cidc++ } : g.main, backup: g.backup ? { ...g.backup, id: cidc++ } : g.backup } : {}),
  });
  const duplicateRow = (gid, rid) => setGroups(gs => mapGroupById(gs, gid, g => {
    const i = g.rows.findIndex(r => r.id === rid); if (i < 0) return g;
    const clone = { ...JSON.parse(JSON.stringify(g.rows[i])), id: ridc++ };
    const rows = [...g.rows]; rows.splice(i + 1, 0, clone); return { ...g, rows };
  }));
  const duplicateGroup = (gid) => setGroups(gs => {
    const i = gs.findIndex(g => g.id === gid); if (i < 0) return gs;
    const clone = reidGroup(JSON.parse(JSON.stringify(gs[i])));
    const arr = [...gs]; arr.splice(i + 1, 0, clone); return arr;
  });
  const duplicateSubGroup = (parentGid, sgid) => setGroups(gs => gs.map(g => {
    if (g.id !== parentGid) return g;
    const i = (g.subGroups || []).findIndex(sg => sg.id === sgid); if (i < 0) return g;
    const clone = reidGroup(JSON.parse(JSON.stringify(g.subGroups[i])));
    const arr = [...g.subGroups]; arr.splice(i + 1, 0, clone); return { ...g, subGroups: arr };
  }));

  // Standortangebot: Verwaltung der Unterbereiche selbst (Hinzufügen/Entfernen/Sortieren) – setzt am Eltern-Standort an.
  const addSubGroup = (parentGid) => setGroups(gs => gs.map(g => g.id === parentGid ? { ...g, subGroups: [...(g.subGroups || []), newGroup()] } : g));
  const removeSubGroup = (parentGid, sgid) => setGroups(gs => gs.map(g => g.id !== parentGid ? g : { ...g, subGroups: (g.subGroups || []).length <= 1 ? [newGroup()] : g.subGroups.filter(sg => sg.id !== sgid) }));
  const moveSubGroupDir = (parentGid, sgid, dir) => setGroups(gs => gs.map(g => {
    if (g.id !== parentGid) return g;
    const arr = [...(g.subGroups || [])];
    const i = arr.findIndex(sg => sg.id === sgid), j = i + dir;
    if (i < 0 || j < 0 || j >= arr.length) return g;
    [arr[i], arr[j]] = [arr[j], arr[i]];
    return { ...g, subGroups: arr };
  }));

  const findGroupById = (gid) => { for (const g of groups) { if (g.id === gid) return g; if (g.area === "standort") for (const sg of g.subGroups || []) if (sg.id === gid) return sg; } return null; };
  const onProduct = (gid, rid, area, productId) => {
    if (area === "sdwan" || area === "sdwanmeraki") {
      const grp = findGroupById(gid);
      const term = SDWAN_TERMS.includes(Number(grp?.sdwanTerm)) ? Number(grp.sdwanTerm) : 60;
      patchRow(gid, rid, { productId, qty: 1, standort: "", term, sdwanLicense: "sdwan", sdwanPrice: "", redundanz: false });
      return;
    }
    const p = getProduct(area, productId), term = defaultTerm(p), pr = resolvePrice(p, term);
    patchRow(gid, rid, { productId, qty: p?.qtyMin || 1, standort: "", term, monthly: pr.monthly, oneoff: pr.oneoff, discountFix: false, discountPct: 0, discountMode: "verrechnet", abloese: 0, teamsCoupling: false, backup: false, express: false, kanal: 0, kanalTarife: blankKanalTarife(), zweiterKanal: false, router: false, euPlus: false, worldSelect: false, basicRabatt: "", basicMode: "verrechnet", lineProvider: "dtag", onkz: "", lineRabatt: "0", mobileSpeed: "25", mobileRabatt: "0", antenne: "", businessClass: false, internetFlat: false, pureRabatt: "0" });
  };
  const onRowTerm = (gid, rid, area, term) => {
    setGroups(gs => mapGroupById(gs, gid, g => ({ ...g, rows: g.rows.map(r => {
      if (r.id !== rid) return r;
      const prod = getProduct(area, r.productId);
      if (prod?.priceOpen || r.basicRabatt === "pricing" || r.mobileRabatt === "pricing") return { ...r, term };
      const pr = resolvePrice(prod, term);
      const extra = prod?.pureDiscount && Number(term) < 36 && Number(r.pureRabatt) > 20 ? { pureRabatt: "20" } : {};
      return { ...r, term, monthly: pr.monthly, oneoff: pr.oneoff, ...extra };
    }) })));
  };

  // Standortangebote besitzen selbst keine Zeilen, sondern Unterbereiche – für Berechnungen, die über
  // alle "echten" Bereiche gehen, werden diese hier eingeklappt (jeder Unterbereich verhält sich wie eine normale Gruppe).
  const flattenGroups = (gs) => { const out = []; for (const g of gs) { if (g.area === "standort") out.push(...(g.subGroups || [])); else out.push(g); } return out; };

  const totalNST = useMemo(() => {
    let n = 0;
    for (const g of flattenGroups(groups)) for (const r of (g.rows || [])) {
      const p = getProduct(g.area, r.productId);
      if (p?.produkttyp === "basispaket") n += 5;
      else if (p?.produkttyp === "lizenz") n += Math.max(1, Number(r.qty) || 1);
    }
    return n;
  }, [groups]);

  const offerLines = useMemo(() => {
    // Verarbeitet genau einen "echten" Bereich (Top-Level-Gruppe ODER Unterbereich eines Standortangebots)
    // und hängt seine Zeilen an outLines an. clusterType ist "cluster" für normale Top-Level-Bereiche bzw.
    // "subcluster" für einen Unterbereich innerhalb eines Standortangebots (pgid = ID des Standorts).
    const processGroup = (g, outLines, clusterType, pgid) => {
      let premium = false, server = false;
      if (g.area === "sdwan" || g.area === "sdwanmeraki") {
        const { lines: gl } = buildSdwanLines(g, g.id);
        if (gl.length) {
          const headerDesc = applyAreaHeaderDesc(g, gl);
          outLines.push({ type: clusterType, name: CATALOG[g.area].label, gid: g.id, pgid, desc: headerDesc || undefined });
          outLines.push(...gl);
        }
        return { premium, server };
      }
      if (g.area === "individuell") {
        const groupLines = [];
        for (const r of g.rows) {
          const name = (r.custName || "").trim();
          const qty = Math.max(1, Number(r.qty) || 1);
          const m = (Number(r.monthly) || 0) * qty, o = (Number(r.oneoff) || 0) * qty;
          const desc = (r.custDesc || "").split("\n").map(s => s.trim()).filter(Boolean);
          if (!name && !m && !o && !desc.length) continue;
          const meta = [];
          if (r.term) meta.push("Laufzeit: " + r.term + " Monate");
          if (qty > 1) meta.push("Anzahl: " + qty);
          if (r.standort) meta.push("Standort: " + r.standort);
          groupLines.push({ type: "product", rowId: r.id, name: name || "—", desc, meta, monthly: m, oneoff: o, abloeseNote: null, rabattNote: null, gid: g.id, listMonthly: m });
        }
        if (groupLines.length) {
          const headerDesc = applyAreaHeaderDesc(g, groupLines);
          outLines.push({ type: clusterType, name: (g.customArea || "").trim() || "Individuell", gid: g.id, pgid, desc: headerDesc || undefined });
          outLines.push(...groupLines);
        }
        return { premium, server };
      }
      if (g.area === "allipbasic") {
        const groupLines = [];
        for (const r of g.rows) {
          const p = getProduct(g.area, r.productId);
          if (!p) continue;
          const qty = Math.max(1, Number(r.qty) || 1);
          const pricing = r.basicRabatt === "pricing";
          const base = resolvePrice(p, r.term);
          let prodM = pricing ? (Number(r.monthly) || 0) : base.monthly;
          const baseO = pricing ? (Number(r.oneoff) || 0) : base.oneoff;
          const desc = [];
          if (!p.zweiterKanal) desc.push("2 Leitungen / 2–10 Rufnummern inklusive");
          else desc.push(r.zweiterKanal ? "2 Leitungen / 2–10 Rufnummern" : "1 Leitung / 1 Rufnummer inklusive");
          desc.push("Flatrate ins dt. Festnetz", "Flatrate ins dt. Mobilfunknetz", "Taktung 1:1 (sekundengenau)");
          desc.push(p.connKind === "cable" ? "Dynamische IPv6-Adresse inklusive" : "Dynamische IPv4-Adresse inklusive · feste IPv4-Adresse optional ohne Aufpreis");
          if (p.zweiterKanal && r.zweiterKanal) prodM += 3;
          let pct = 0;
          if (r.basicRabatt === "10") pct = 10; else if (r.basicRabatt === "20") pct = 20; else if (r.basicRabatt === "30") pct = 30;
          let addM = 0;
          if (r.router) { const rt = BASIC_ROUTER[p.connKind]; if (rt) { addM += rt.price; desc.push(rt.price > 0 ? `inkl. ${rt.name} (+${eur(rt.price)}/Monat)` : `inkl. ${rt.name}`); } }
          if (r.euPlus) { addM += euPlusPrice(r.term); desc.push(optText("euplus")); }
          if (r.worldSelect) { addM += worldSelectPrice(r.term); desc.push(optText("worldselect")); }
          const discAmt = prodM * (pct / 100) * qty;
          const mode = r.basicMode || "verrechnet";
          const listFull = (prodM + addM) * qty;
          let monthly, rabattNote = null;
          if (pct > 0 && mode === "zeile") monthly = listFull;
          else { monthly = ((prodM * (1 - pct / 100)) + addM) * qty; if (pct > 0 && mode === "abzug") rabattNote = `inkl. ${eur(discAmt)} Rabatt`; }
          const oneoff = baseO * qty;
          const meta = [];
          if (r.term) meta.push("Laufzeit: " + r.term + " Monate");
          if (qty > 1) meta.push("Anzahl: " + qty);
          if (r.standort) meta.push("Standort: " + r.standort);
          groupLines.push({ type: "product", rowId: r.id, name: p.name, desc, meta, monthly, oneoff, abloeseNote: null, rabattNote, gid: g.id, listMonthly: listFull });
          if (pct > 0 && mode === "zeile") groupLines.push({ type: "discount", name: `Rabatt ${pct} %`, monthly: -discAmt, oneoff: 0, abloeseNote: null, rabattNote: null, gid: g.id });
        }
        if (groupLines.length) {
          const headerDesc = applyAreaHeaderDesc(g, groupLines);
          outLines.push({ type: clusterType, name: CATALOG[g.area].label, gid: g.id, pgid, desc: headerDesc || undefined });
          outLines.push(...groupLines);
        }
        return { premium, server };
      }
      const dp = hasLizenz(g) ? dpStats(g) : null;
      const effPct = dp ? Math.min(Math.max(0, Number(g.dpPct) || 0), dp.maxPct) : 0;
      const groupLines = [];
      let dpAccum = 0;
      for (const r of g.rows) {
        const p = getProduct(g.area, r.productId);
        if (!p) continue;
        if (p.premiumCti) premium = true;
        if (p.isServer) server = true;
        const qty = p.mengenBezug === "nebenstellen" ? totalNST : p.qty ? Math.max(p.qtyMin || 1, Number(r.qty) || 1) : 1;
        let perM = Number(r.monthly) || 0, perO = Number(r.oneoff) || 0;
        let pctDiscPerM = 0; // Prozent-Rabatt je Einheit (Line/Mobile/Pure) – getrennt erfasst, damit die Ersparnis korrekt zählt
        let pctRabattLabel = "";
        // Fixpreis-Produkte (Asymmetrisch, Wave, SIP, Digital Phone, MDM …) live aus dem editierbaren Katalog,
        // damit Preisänderungen im Bearbeiten-Reiter sofort auch auf bereits platzierte Zeilen wirken.
        if (!p.priceOpen && !p.office && !p.mobile && ("m24" in p || "m36" in p)) { const pr = resolvePrice(p, r.term); perM = pr.monthly; perO = pr.oneoff; }
        let lineDesc = [...(p.desc || [])];
        if (p.pure && p.pureDiscount) {
          const maxPct = Number(r.term) >= 36 ? 30 : 20;
          const pct = Math.min(Math.max(0, Number(r.pureRabatt) || 0), maxPct);
          if (pct > 0) { pctDiscPerM += perM * pct / 100; pctRabattLabel = `Rabatt ${pct} %`; }
        }
        if (p.extrasKind === "line" && r.lineProvider === "dtag" && r.lineRabatt !== "pricing") {
          const b = lineDtagBase(p, r);
          let lpct = 0; if (r.lineRabatt === "10") lpct = 10; else if (r.lineRabatt === "20") lpct = 20; else if (r.lineRabatt === "30") lpct = 30;
          perM = b.monthly;
          if (lpct > 0) { pctDiscPerM += b.monthly * lpct / 100; pctRabattLabel = `Rabatt ${lpct} %`; }
          perO = b.oneoff;
        }
        if (p.mobile) {
          const b = mobileMainBase(r);
          if (r.mobileRabatt !== "pricing") {
            let mpct = 0; if (r.mobileRabatt === "10") mpct = 10; else if (r.mobileRabatt === "20") mpct = 20; else if (r.mobileRabatt === "30") mpct = 30;
            perM = b.baseM + b.speedM + b.flatM;
            if (mpct > 0) { pctDiscPerM += (b.baseM + b.speedM) * mpct / 100; pctRabattLabel = `Rabatt ${mpct} %`; }
            perO = b.baseO;
          }
          lineDesc.push(`Geschwindigkeit: ${b.speed.label}`, "Internet-Flat inklusive");
          if (b.speedUnpriced) lineDesc.push("⚠️ Preis für diese Geschwindigkeit noch offen – bitte ergänzen");
          const ant = antennaFor(r.antenne);
          if (ant) { perO += ant.oneoff; lineDesc.push(`inkl. Antenne ${ant.label}`); }
        }
        if (p.wave && r.businessClass) { perM += WAVE_SUPPORT_PRICE; lineDesc.push(optText("wave_support")); }
        if (p.inetFlat && r.internetFlat) { const f = Number(r.term) >= 36 ? INET_FLAT.m36 : INET_FLAT.m24; perM += f; lineDesc.push(`Internet-Flatrate (${eur(f)}/Monat, nicht rabattiert)`); }
        if (p.office) { perM = officePrice(p, g.managed); perO = 0; if (officeNoMgmt(p, g.managed)) lineDesc.push("Kein Management durch Telefónica"); }
        if (p.teamsCoupling && r.teamsCoupling) { perM += 4; lineDesc.push(optText("teams")); }
        if (p.maxKanal && r.kanal > 0) {
          for (const dir of RICHTUNGEN) {
            const t = (r.kanalTarife && r.kanalTarife[dir.key]) || { mode: "standard", min: 0 };
            if (t.mode === "fairuse") { perM += dir.fair * r.kanal; lineDesc.push(dir.fairText); }
            else if (t.mode === "pooling") { const mins = Number(t.min) || 0; perM += mins * dir.poolCt / 100; lineDesc.push(`${mins} Minuten in Richtung ${dir.label}`); }
            else if (dir.key === "fest" || dir.key === "mob") lineDesc.push(`Minutenabrechnung in Richtung ${dir.label}`);
          }
        }
        if (p.extrasKind && r.backup) {
          perM += backupMonthly(p.extrasKind, r.term);
          let bdesc = [...optText("backup")];
          if (p.maxKanal && r.kanal > 0) {
            const kt = r.kanal > 16 ? optText("backup_kanal_16") : optText("backup_kanal_all");
            bdesc = bdesc.map(line => line === optText("backup")[2] ? kt : line);
          }
          lineDesc = lineDesc.concat(bdesc);
          const ant = antennaFor(r.antenne);
          if (ant) { perO += ant.oneoff; lineDesc.push(`inkl. Antenne ${ant.label}`); }
        }
        if (p.extrasKind && r.backup && r.express) { perO += EXPRESS_PRICE[p.extrasKind]; lineDesc = lineDesc.concat(optText("express")); }
        const baseM = perM * qty, baseO = perO * qty;
        let amt = 0, dlabel = "";
        if (p.rabattType === "fix10" && r.discountFix) { amt = 10; dlabel = "Bestandskundenrabatt (−10 €)"; }
        else if (p.rabattType === "staffelMax" && Number(r.discountPct) > 0) { const mx = staffelPct(p.staffel, qty); const pct = Math.min(mx, Number(r.discountPct)); amt = baseM * pct / 100; dlabel = `Mengenrabatt ${pct} %`; }
        else if (p.rabattType === "officeMax" && Number(r.discountPct) > 0) { const mx = officeMax(p, g.managed); const pct = Math.min(mx, Number(r.discountPct)); amt = baseM * pct / 100; dlabel = `Rabatt ${pct} %`; }
        if (pctDiscPerM > 0) { amt += pctDiscPerM * qty; if (!dlabel) dlabel = pctRabattLabel; }
        let dpAmt = 0;
        if (dp && effPct > 0 && p.produkttyp === "lizenz") dpAmt = baseM * effPct / 100;
        const rowReduces = r.discountMode === "verrechnet" || r.discountMode === "abzug";
        const dpReduces = g.dpMode === "verrechnet" || g.dpMode === "abzug";
        const verrLine = (rowReduces ? amt : 0) + (dpReduces ? dpAmt : 0);
        const shownM = baseM - verrLine;
        let rabattAmt = 0;
        if (r.discountMode === "abzug") rabattAmt += amt;
        if (g.dpMode === "abzug") rabattAmt += dpAmt;
        const rabattNote = rabattAmt > 0 ? `inkl. ${eur(rabattAmt)} Rabatt` : null;
        const meta = [];
        if (r.term) meta.push("Laufzeit: " + r.term + " Monate");
        if (p.maxKanal && r.kanal > 0) meta.push("Sprachkanäle: " + r.kanal);
        if (p.mengenBezug === "nebenstellen") meta.push("Nebenstellen: " + qty);
        else if (p.qty) meta.push((p.qtyLabel || "Menge") + ": " + qty);
        if (r.standort) meta.push("Standort: " + r.standort);
        if (p.extrasKind === "line" && r.lineProvider === "dtag" && r.onkz) meta.push("ONKZ: " + r.onkz);
        const abloeseNote = p.abloese && Number(r.abloese) > 0 ? `inkl. ${r.abloese} Monate Basispreisbefreiung` : null;
        groupLines.push({ type: "product", rowId: r.id, name: p.name, desc: lineDesc, meta, monthly: shownM, oneoff: baseO, abloeseNote, rabattNote, gid: g.id, listMonthly: baseM });
        if (p.wave) groupLines.push({ type: "auto", name: optText("wave_los_name"), desc: [optText("wave_los_desc")], monthly: 0, oneoff: WAVE_LOS_PRICE, abloeseNote: null, gid: g.id });
        if (amt > 0 && r.discountMode === "zeile") groupLines.push({ type: "discount", name: dlabel, monthly: -amt, oneoff: 0, abloeseNote: null, gid: g.id });
        if (dpAmt > 0 && g.dpMode === "zeile") dpAccum += dpAmt;
      }
      if (dpAccum > 0) groupLines.push({ type: "discount", name: `${g.kombi ? "Kombi-" : ""}Umsatzrabatt ${effPct} %`, monthly: -dpAccum, oneoff: 0, abloeseNote: null, gid: g.id });
      if (groupLines.length) {
        const headerDesc = applyAreaHeaderDesc(g, groupLines);
        outLines.push({ type: clusterType, name: CATALOG[g.area].label + (g.area === "office365" && g.managed ? " (Managed)" : ""), gid: g.id, pgid, desc: headerDesc || undefined });
        outLines.push(...groupLines);
      }
      return { premium, server };
    };

    const lines = [];
    let hasPremium = false, hasServer = false;
    for (const g of groups) {
      if (!g.area) continue;
      if (g.area === "standort") {
        const subLines = [];
        for (const sg of g.subGroups || []) {
          if (!sg.area) continue;
          if (sg.area === "vpnconnect") {
            const vl = buildVpnLines(sg, resolveVpnDesc(overrides.vpnDesc));
            for (const l of vl) { if (l.type === "cluster") subLines.push({ ...l, type: "subcluster", pgid: g.id }); else subLines.push(l); }
            continue;
          }
          const res = processGroup(sg, subLines, "subcluster", g.id);
          hasPremium = hasPremium || res.premium;
          hasServer = hasServer || res.server;
        }
        if (subLines.length) {
          lines.push({ type: "cluster", name: (g.standortName || "").trim() || "Standortangebot", gid: g.id });
          lines.push(...subLines);
        }
        continue;
      }
      if (g.area === "vpnconnect") { lines.push(...buildVpnLines(g, resolveVpnDesc(overrides.vpnDesc))); continue; }
      const res = processGroup(g, lines, "cluster");
      hasPremium = hasPremium || res.premium;
      hasServer = hasServer || res.server;
    }
    if (hasPremium && !hasServer) lines.push({ type: "auto", name: "CTI Premium Server (Einrichtung)", desc: ["Einmalig bei Buchung von CTI-Premium-Lizenzen"], monthly: 0, oneoff: 299, abloeseNote: null, gid: null });
    return lines;
  }, [groups, totalNST, catVer]);

  const sumM = offerLines.reduce((a, l) => a + (l.monthly || 0), 0);
  const sumO = offerLines.reduce((a, l) => a + (l.oneoff || 0), 0);

  const clusterSumsData = useMemo(() => {
    const d = {}; let curr = null;
    for (const l of offerLines) {
      if (l.type === "cluster" || l.type === "subcluster") { curr = l.gid; d[curr] = { name: l.name, sumM: 0, sumO: 0 }; }
      else if (curr) { d[curr].sumM += l.monthly || 0; d[curr].sumO += l.oneoff || 0; }
    }
    return d;
  }, [offerLines]);

  const standortSums = useMemo(() => {
    const d = {};
    for (const g of groups) {
      if (g.area !== "standort") continue;
      let sm = 0, so = 0;
      for (const sg of g.subGroups || []) { const c = clusterSumsData[sg.id]; if (c) { sm += c.sumM; so += c.sumO; } }
      d[g.id] = { sumM: sm, sumO: so };
    }
    return d;
  }, [groups, clusterSumsData]);

  const savingsData = useMemo(() => {
    let savM = 0;
    for (const l of offerLines) {
      if (l.type === "product") savM += (l.listMonthly || 0) - l.monthly;
      if (l.type === "discount") savM -= l.monthly;
    }
    return { savM: Math.max(0, savM), savO: 0 };
  }, [offerLines]);
  const istM = Number(istKosten) || 0;
  const vsIst = istM > 0;
  const effSavM = vsIst ? Math.max(0, istM - sumM) : savingsData.savM;

  const primaryTerm = useMemo(() => {
    const counts = {};
    const vote = (t, w = 1) => { if (t) counts[t] = (counts[t] || 0) + w; };
    for (const g of flattenGroups(groups)) {
      if (g.area === "vpnconnect") { const w = (g.main?.access ? 1 : 0) + (g.backup?.access ? 1 : 0) || 1; vote(g.vpnTerm, w); continue; }
      for (const r of (g.rows || [])) vote(r.term);
    }
    const sorted = Object.entries(counts).sort((a, b) => b[1] - a[1]);
    return sorted.length ? Number(sorted[0][0]) : 0;
  }, [groups]);

  const totalSavings = effSavM * primaryTerm + savingsData.savO;

  // Plausibilitäts-Ampel: konkrete Hinweise zu fehlenden/auffälligen Angaben
  const plausibility = useMemo(() => {
    const w = [];
    let totalSdwanHardware = 0;
    const checkRow = (area, label, r, gid) => {
      const p = getProduct(area, r.productId);
      if (!p) return;
      if (p.mobile) { const b = mobileMainBase(r); if (b.speedUnpriced) w.push({ level: "warn", gid, msg: `${label} · ${p.name}: Preis für Geschwindigkeit „${b.speed.label}" ist noch nicht hinterlegt.` }); }
      if (p.id === "dp-ccm" && Number(r.qty) > 0 && Number(r.qty) < (p.qtyMin || 5)) w.push({ level: "info", gid, msg: `${label} · Call Center Monitoring: mind. ${p.qtyMin} Agents – wird automatisch mit ${p.qtyMin} berechnet.` });
      if (p.extrasKind === "line" && r.lineProvider === "dtag" && r.lineRabatt !== "pricing" && !r.onkz) w.push({ level: "warn", gid, msg: `${label} · ${p.name}: ONKZ fehlt – Link-/Leitungspreis kann nicht korrekt ermittelt werden.` });
      if (p.priceOpen && !(Number(r.monthly) > 0) && !(Number(r.oneoff) > 0)) w.push({ level: "info", gid, msg: `${label} · ${p.name}: Position ohne Preis (0 €) – bitte prüfen.` });
    };
    for (const g of groups) {
      if (!g.area) continue;
      const label = CATALOG[g.area]?.label || g.area;
      if (g.area === "sdwan" || g.area === "sdwanmeraki") { totalSdwanHardware += buildSdwanLines(g).standorte; }
      for (const r of (g.rows || [])) checkRow(g.area, label, r, g.id);
      for (const sg of (g.subGroups || [])) {
        const sl = `${label} · ${CATALOG[sg.area]?.label || sg.area}`;
        if (sg.area === "sdwan" || sg.area === "sdwanmeraki") { totalSdwanHardware += buildSdwanLines(sg).standorte; }
        for (const r of (sg.rows || [])) checkRow(sg.area, sl, r, sg.id);
      }
      if (g.area === "vpnconnect") {
        const nm = (g.vpnName || "").trim() || "VPN-Standort";
        const cc = (c, role) => { if (!c || !c.access) return; if (c.access === "link" && c.provider === "dtag" && c.rabatt !== "pricing" && !c.onkz) w.push({ level: "warn", gid: g.id, msg: `${nm} (${role}): ONKZ für Link-Anschluss fehlt.` }); if ((c.rabatt === "pricing" || c.provider === "andere") && !(Number(c.manMonthly) > 0)) w.push({ level: "info", gid: g.id, msg: `${nm} (${role}): manueller Preis ist 0 € – bitte prüfen.` }); };
        cc(g.main, "Hauptanschluss"); cc(g.backup, "Backup");
      }
    }
    // Mindestabnahme SD-WAN: 2 Geräte insgesamt, Fortinet + Meraki zusammengezählt, unabhängig vom Standort
    if (totalSdwanHardware === 1) w.push({ level: "warn", gid: null, msg: `SD-WAN: insgesamt nur 1 Gerät im gesamten Angebot (Fortinet + Meraki zusammengezählt) – mindestens 2 Standorte/Geräte erforderlich.` });
    return w;
  }, [groups, catVer]);

  // ===== Export =====
  const buildHtml = () => {
    const cb = "padding:8px 12px;border:1px solid #cccccc;font-family:Arial,Helvetica,sans-serif;font-size:11pt;vertical-align:top;";
    const th = (t, a = "left") => `<th style="${cb}background:${TEF_BLUE};color:#fff;text-align:${a};font-weight:bold;">${t}</th>`;
    const td = (t, a = "left", fs = "11pt") => `<td style="${cb.replace("11pt", fs)}color:${TEF_GRAY};text-align:${a};">${t}</td>`;
    let body = "", currGid = null;
    for (let i = 0; i < offerLines.length; i++) {
      const l = offerLines[i], next = offerLines[i + 1];
      if (l.type === "cluster") { currGid = l.gid; const cd = l.desc?.length ? `<div style="font-weight:normal;font-size:9.5pt;color:${TEF_GRAY};margin-top:3px;">${l.desc.join("<br>")}</div>` : ""; body += `<tr><td colspan="4" style="${cb}background:#D7E0FF;color:${TEF_BLUE};font-weight:bold;">${l.name}${cd}</td></tr>`; }
      else if (l.type === "subcluster") { currGid = l.gid; const cd = l.desc?.length ? `<div style="font-weight:normal;font-size:9.5pt;color:${TEF_GRAY};margin-top:2px;">${l.desc.join("<br>")}</div>` : ""; body += `<tr><td colspan="4" style="${cb}background:#EAF0FF;color:${TEF_BLUE};font-weight:600;padding-left:24px;">${l.name}${cd}</td></tr>`; }
      else {
        const meta = l.meta?.length ? `<br><span style="font-size:9pt;color:#777;">${l.meta.join(" · ")}</span>` : "";
        const mcell = eur(l.monthly) + [l.abloeseNote, l.rabattNote].filter(Boolean).map(n => `<br><span style="color:${TEF_RED};font-size:8.5pt;">${n}</span>`).join("");
        body += `<tr>${td(l.name + meta)}${td((l.desc || []).join("<br>"), "left", "9.5pt")}${td(mcell, "right")}${td(eur(l.oneoff), "right")}</tr>`;
        if (currGid && (visibleSums.has(currGid) || showAllSums) && (!next || next.type === "cluster" || next.type === "subcluster")) {
          const s = clusterSumsData[currGid];
          if (s) { const ss2 = `${cb}background:#E8EDFF;color:${TEF_BLUE};font-weight:600;`; body += `<tr><td colspan="2" style="${ss2}">Summe ${s.name}</td><td style="${ss2}text-align:right;">${eur(s.sumM)}</td><td style="${ss2}text-align:right;">${eur(s.sumO)}</td></tr>`; }
        }
      }
    }
    const ss = `${cb}background:#E6ECFF;color:${TEF_BLUE};font-weight:bold;`;
    body += `<tr><td style="${ss}" colspan="2">Gesamtsumme</td><td style="${ss}text-align:right;">${eur(sumM)}</td><td style="${ss}text-align:right;">${eur(sumO)}</td></tr>`;
    if (showTCO && primaryTerm > 0) { const ts = `${cb}background:#EEF2FF;color:${TEF_BLUE};font-weight:600;`; body += `<tr><td style="${ts}" colspan="2">Gesamtkosten über die Laufzeit</td><td style="${ts}text-align:right;" colspan="2">${eur(sumM * primaryTerm + sumO)}</td></tr>`; }
    if (showSavings && effSavM > 0) {
      const rs = `${cb}background:#FFF5F5;color:${TEF_RED};font-weight:600;`;
      const vsLabel = vsIst ? "gegenüber Ihren bisherigen Kosten" : "gegenüber Listenpreis";
      body += `<tr><td style="${rs}" colspan="2">Ihre monatliche Ersparnis ${vsLabel}</td><td style="${rs}text-align:right;" colspan="2">${eur(effSavM)}</td></tr>`;
      if (primaryTerm > 0) body += `<tr><td style="${rs}" colspan="2">Ihre Gesamtersparnis über die Laufzeit</td><td style="${rs}text-align:right;" colspan="2">${eur(totalSavings)}</td></tr>`;
    }
    const titleRow = `<tr><td colspan="4" style="${cb}background:#ffffff;border:1px solid #cccccc;"><div style="font-size:14pt;font-weight:bold;color:${TEF_BLUE};margin:0 0 2px 0;">Angebot${customer ? " – " + customer : ""}</div><div style="font-size:10pt;font-weight:normal;color:${TEF_GRAY};margin:0;">Stand: ${today}</div></td></tr>`;
    return `<table style="border-collapse:collapse;font-family:Arial,Helvetica,sans-serif;">${titleRow}<thead><tr>${th("Produkt")}${th("Beschreibung")}${th("Monatlich", "right")}${th("Einmalig", "right")}</tr></thead><tbody>${body}</tbody></table>`;
  };

  const buildText = () => {
    const lines = []; let currGid = null;
    for (let i = 0; i < offerLines.length; i++) {
      const l = offerLines[i], next = offerLines[i + 1];
      if (l.type === "cluster") { currGid = l.gid; lines.push(`\n■ ${l.name}`); if (l.desc?.length) for (const d of l.desc) lines.push(`   ${d}`); }
      else if (l.type === "subcluster") { currGid = l.gid; lines.push(`  ▸ ${l.name}`); if (l.desc?.length) for (const d of l.desc) lines.push(`     ${d}`); }
      else {
        const meta = l.meta?.length ? ` (${l.meta.join(" · ")})` : "";
        lines.push(`${l.name}${meta}\t${(l.desc || []).join("; ")}\t${eur(l.monthly)}${[l.abloeseNote, l.rabattNote].filter(Boolean).map(n => ` [${n}]`).join("")}\t${eur(l.oneoff)}`);
        if (currGid && (visibleSums.has(currGid) || showAllSums) && (!next || next.type === "cluster" || next.type === "subcluster")) { const s = clusterSumsData[currGid]; if (s) lines.push(`Summe ${s.name}\t\t${eur(s.sumM)}\t${eur(s.sumO)}`); }
      }
    }
    lines.push(`Gesamtsumme\t\t${eur(sumM)}\t${eur(sumO)}`);
    if (showTCO && primaryTerm > 0) lines.push(`Gesamtkosten über die Laufzeit\t\t${eur(sumM * primaryTerm + sumO)}`);
    if (showSavings && effSavM > 0) {
      const vsLabel = vsIst ? "ggü. Ihren bisherigen Kosten" : "ggü. Listenpreis";
      lines.push(`Ihre monatliche Ersparnis ${vsLabel}\t\t${eur(effSavM)}`);
      if (primaryTerm > 0) lines.push(`Ihre Gesamtersparnis über die Laufzeit\t\t${eur(totalSavings)}`);
    }
    return [`Angebot${customer ? " – " + customer : ""} (Stand ${today})`, `Produkt\tBeschreibung\tMonatlich\tEinmalig`, ...lines].join("\n");
  };

  const copyOffer = async () => {
    try {
      if (navigator.clipboard && window.ClipboardItem) {
        await navigator.clipboard.write([new ClipboardItem({ "text/html": new Blob([buildHtml()], { type: "text/html" }), "text/plain": new Blob([buildText()], { type: "text/plain" }) })]);
      } else await navigator.clipboard.writeText(buildText());
      setCopied(true); setTimeout(() => setCopied(false), 2000);
    } catch { alert("Kopieren nicht möglich – bitte Tabelle manuell markieren."); }
  };

  const buildState = () => JSON.stringify({ app: "tef-kalkulator", version: 10, savedAt: new Date().toISOString(), customer, groups }, null, 2);
  const suggestedName = () => {
    const d = new Date();
    const yy = String(d.getFullYear()).slice(2), mm = String(d.getMonth() + 1).padStart(2, "0"), dd = String(d.getDate()).padStart(2, "0");
    return `${yy}${mm}${dd} ${(customer || "ohne Kunde").replace(/[<>:"/\\|?*]+/g, "").trim()}`;
  };

  const buildJpg = () => new Promise((resolve) => {
    const PAD = 40, SC = 2, COL = [320, 260, 120, 120], TW = COL.reduce((a, b) => a + b, 0), W = TW + PAD * 2;
    const HDR_H = 40, ROW_H = 32, CLUST_H = 28;
    const noteLines = (l) => (l.abloeseNote ? 1 : 0) + (l.rabattNote ? 1 : 0);
    const rowHeight = (l) => Math.max(ROW_H, 14 + (l.desc || []).length * 13, 20 + noteLines(l) * 13);
    const clusterHeight = (l) => CLUST_H + (l.desc?.length ? l.desc.length * 12 + 4 : 0);
    let totalH = PAD * 2 + 56 + HDR_H + ROW_H;
    for (const l of offerLines) totalH += (l.type === "cluster" || l.type === "subcluster") ? clusterHeight(l) : rowHeight(l);
    if (showTCO && primaryTerm > 0) totalH += ROW_H;
    if (showSavings && effSavM > 0) { totalH += ROW_H; if (primaryTerm > 0) totalH += ROW_H; }
    const cv = document.createElement("canvas");
    cv.width = W * SC; cv.height = totalH * SC;
    const cx = cv.getContext("2d"); cx.scale(SC, SC);
    cx.fillStyle = "#F4F6FB"; cx.fillRect(0, 0, W, totalH);
    cx.fillStyle = TEF_BLUE; cx.font = "bold 17px Arial,sans-serif"; cx.fillText(`Angebot${customer ? " – " + customer : ""}`, PAD, PAD + 20);
    cx.fillStyle = "#555"; cx.font = "12px Arial,sans-serif"; cx.fillText(`Stand: ${today}`, PAD, PAD + 38);
    let y = PAD + 56;
    const box = (x, yy, w, h, fill) => { cx.fillStyle = fill; cx.fillRect(x, yy, w, h); cx.strokeStyle = "#ccc"; cx.lineWidth = 0.5; cx.strokeRect(x, yy, w, h); };
    const txt = (s, x, yy, col, bold, size = 11, align = "left") => { cx.fillStyle = col; cx.font = `${bold ? "bold " : ""}${size}px Arial,sans-serif`; cx.textAlign = align; cx.fillText(String(s), x, yy); cx.textAlign = "left"; };
    let x = PAD;
    const hdrs = ["Produkt", "Beschreibung", "Monatlich", "Einmalig"], alns = ["left","left","right","right"];
    for (let i = 0; i < 4; i++) { box(x, y, COL[i], HDR_H, TEF_BLUE); txt(hdrs[i], alns[i]==="right"?x+COL[i]-8:x+8, y+26, "#fff", true, 12, alns[i]); x += COL[i]; }
    y += HDR_H;
    let currGid2 = null;
    for (let i = 0; i < offerLines.length; i++) {
      const l = offerLines[i], next = offerLines[i + 1]; x = PAD;
      if (l.type === "cluster") { currGid2 = l.gid; const ch = clusterHeight(l); box(x, y, TW, ch, "#D7E0FF"); txt(l.name, x+8, y+19, TEF_BLUE, true, 12); if (l.desc?.length) { let dy = y + CLUST_H + 4; for (const d of l.desc) { txt(d.length > 90 ? d.slice(0, 90) + "…" : d, x+8, dy, TEF_GRAY, false, 10); dy += 12; } } y += ch; }
      else if (l.type === "subcluster") { currGid2 = l.gid; const ch = clusterHeight(l); box(x, y, TW, ch, "#EAF0FF"); txt(l.name, x+24, y+18, TEF_BLUE, true, 11); if (l.desc?.length) { let dy = y + CLUST_H + 3; for (const d of l.desc) { txt(d.length > 90 ? d.slice(0, 90) + "…" : d, x+24, dy, TEF_GRAY, false, 9.5); dy += 12; } } y += ch; }
      else {
        const desc = l.desc || [], rh = rowHeight(l), bg = l.type==="product"?"#fff":"#f7f7f7", fg = l.type==="product"?TEF_GRAY:"#888";
        box(x,y,COL[0],rh,bg); txt(l.name,x+8,y+16,fg,l.type==="product",11); if(l.meta?.length) txt(l.meta.join(" · "),x+8,y+28,"#999",false,9); x+=COL[0];
        box(x,y,COL[1],rh,bg); let dy=y+13; for(const d of desc){txt(d.length>44?d.slice(0,44)+"…":d,x+8,dy,"#666",false,10); dy+=13;} x+=COL[1];
        box(x,y,COL[2],rh,bg); txt(eur(l.monthly),x+COL[2]-8,y+20,fg,l.type==="product",11,"right");
        { let ny=y+20; if(l.abloeseNote){ny+=13; txt(l.abloeseNote,x+COL[2]-8,ny,TEF_RED,false,8.5,"right");} if(l.rabattNote){ny+=13; txt(l.rabattNote,x+COL[2]-8,ny,TEF_RED,false,8.5,"right");} } x+=COL[2];
        box(x,y,COL[3],rh,bg); txt(eur(l.oneoff),x+COL[3]-8,y+20,fg,l.type==="product",11,"right"); y+=rh;
        if (currGid2 && (visibleSums.has(currGid2) || showAllSums) && (!next || next.type === "cluster" || next.type === "subcluster")) {
          const s = clusterSumsData[currGid2];
          if (s) { x=PAD; box(x,y,COL[0]+COL[1],ROW_H,"#E8EDFF"); txt(`Summe ${s.name}`,x+8,y+21,TEF_BLUE,true,11); x+=COL[0]+COL[1]; box(x,y,COL[2],ROW_H,"#E8EDFF"); txt(eur(s.sumM),x+COL[2]-8,y+21,TEF_BLUE,true,11,"right"); x+=COL[2]; box(x,y,COL[3],ROW_H,"#E8EDFF"); txt(eur(s.sumO),x+COL[3]-8,y+21,TEF_BLUE,true,11,"right"); y+=ROW_H; }
        }
      }
    }
    x=PAD; box(x,y,COL[0]+COL[1],ROW_H,"#E6ECFF"); txt("Gesamtsumme",x+8,y+21,TEF_BLUE,true,12); x+=COL[0]+COL[1]; box(x,y,COL[2],ROW_H,"#E6ECFF"); txt(eur(sumM),x+COL[2]-8,y+21,TEF_BLUE,true,12,"right"); x+=COL[2]; box(x,y,COL[3],ROW_H,"#E6ECFF"); txt(eur(sumO),x+COL[3]-8,y+21,TEF_BLUE,true,12,"right"); y+=ROW_H;
    if (showTCO && primaryTerm > 0) { x=PAD; box(x,y,COL[0]+COL[1],ROW_H,"#EEF2FF"); txt("Gesamtkosten über die Laufzeit",x+8,y+21,TEF_BLUE,true,11); x+=COL[0]+COL[1]; box(x,y,COL[2]+COL[3],ROW_H,"#EEF2FF"); txt(eur(sumM * primaryTerm + sumO),x+COL[2]+COL[3]-8,y+21,TEF_BLUE,true,11,"right"); y+=ROW_H; }
    if (showSavings && effSavM > 0) {
      const vsLabelJ = vsIst ? "Monatliche Ersparnis ggü. bisherigen Kosten" : "Monatliche Ersparnis ggü. Listenpreis";
      x=PAD; box(x,y,COL[0]+COL[1],ROW_H,"#FFF5F5"); txt(vsLabelJ,x+8,y+21,TEF_RED,true,11); x+=COL[0]+COL[1]; box(x,y,COL[2]+COL[3],ROW_H,"#FFF5F5"); txt(eur(effSavM),x+COL[2]+COL[3]-8,y+21,TEF_RED,true,11,"right"); y+=ROW_H;
      if (primaryTerm > 0) { x=PAD; box(x,y,COL[0]+COL[1],ROW_H,"#FFF5F5"); txt(`Gesamtersparnis über die Laufzeit`,x+8,y+21,TEF_RED,true,11); x+=COL[0]+COL[1]; box(x,y,COL[2]+COL[3],ROW_H,"#FFF5F5"); txt(eur(totalSavings),x+COL[2]+COL[3]-8,y+21,TEF_RED,true,11,"right"); y+=ROW_H; }
    }
    cv.toBlob(resolve, "image/jpeg", 0.93);
  });

  const triggerDownload = (blob, filename) => { const url = URL.createObjectURL(blob), a = document.createElement("a"); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(url), 1500); };
  const saveOffer = async () => {
    const name = suggestedName();
    triggerDownload(new Blob([buildState()], { type: "application/json" }), name + ".json");
    if (offerLines.length > 0) { const jpg = await buildJpg(); setTimeout(() => triggerDownload(jpg, name + ".jpg"), 400); }
  };

  const restore = (data) => {
    if (!data || data.app !== "tef-kalkulator") { alert("Das ist keine gültige Kalkulator-Datei."); return; }
    setCustomer(data.customer || "");
    const rebuildRows = (rows) => (Array.isArray(rows) && rows.length ? rows : [{}]).map(r => ({ ...newRow(), ...r, id: ridc++ }));
    const rebuildGroup = (g) => ({
      id: gidc++,
      area: g.area || "",
      kombi: !!g.kombi,
      dpPct: Number(g.dpPct) || 0,
      dpMode: g.dpMode || "verrechnet",
      managed: !!g.managed,
      customArea: g.customArea || "",
      showDesc: !!g.showDesc,
      standortName: g.standortName || "",
      sdwanTerm: SDWAN_TERMS.includes(Number(g.sdwanTerm)) ? Number(g.sdwanTerm) : 60,
      rows: rebuildRows(g.rows),
      subGroups: Array.isArray(g.subGroups) ? g.subGroups.map(rebuildGroup) : [],
      ...(g.area === "vpnconnect" ? {
        vpnTerm: VPN_TERMS.includes(Number(g.vpnTerm)) ? Number(g.vpnTerm) : 36,
        vpnName: g.vpnName || "",
        main: g.main ? { ...newVpnConn(g.main.access || "asym"), ...g.main, id: cidc++ } : newVpnConn("asym"),
        backup: g.backup && g.backup.access ? { ...newVpnConn(g.backup.access), ...g.backup, id: cidc++ } : null,
        citrix: !!g.citrix, netflow: !!g.netflow, advHw: !!g.advHw, advHwM: g.advHwM ?? "0", advHwO: g.advHwO ?? "0",
        backupPlus: !!g.backupPlus, bpM: g.bpM ?? "0", bpO: g.bpO ?? "0",
      } : {}),
    });
    const gs = (Array.isArray(data.groups) ? data.groups : []).map(rebuildGroup);
    setGroups(gs.length ? gs : [newGroup()]);
  };
  const onLoadFile = (e) => { const f = e.target.files?.[0]; if (!f) return; const reader = new FileReader(); reader.onload = () => { try { restore(JSON.parse(String(reader.result))); } catch { alert("Datei konnte nicht gelesen werden."); } }; reader.readAsText(f); e.target.value = ""; };

  // Sortier-Pfeile (überall einsetzbar)
  const SortArrows = ({ g, r, size = 16 }) => {
    const idx = g.rows.findIndex(x => x.id === r.id);
    return (
      <div className="flex flex-col shrink-0">
        <button onClick={() => moveRowDir(g.id, r.id, -1)} disabled={idx <= 0} className="p-0.5 rounded hover:bg-gray-100 disabled:opacity-25" title="nach oben"><ChevronUp size={size} color="#6b7280" /></button>
        <button onClick={() => moveRowDir(g.id, r.id, 1)} disabled={idx >= g.rows.length - 1} className="p-0.5 rounded hover:bg-gray-100 disabled:opacity-25" title="nach unten"><ChevronDown size={size} color="#6b7280" /></button>
      </div>
    );
  };

  // ===== Produktzeile (SD-WAN Fortinet / Meraki) =====
  const renderSdwanRow = (g, r) => {
    const area = g.area;
    const products = CATALOG[area].products.filter(pp => pp.isSeparator || !isDeleted(area, pp.id) || pp.id === r.productId);
    const p = getProduct(area, r.productId);
    const term = Number(r.term) || 60;
    const isLicensed = !!p?.lic;
    const licKeys = isLicensed ? Object.keys(p.lic) : [];
    const lic = isLicensed ? (licKeys.includes(r.sdwanLicense) ? r.sdwanLicense : licKeys[0]) : null;
    const isAd = p?.sdwanKind === "ad";
    const isRemote = p?.sdwanKind === "remote";
    const canRed = p?.sdwanKind === "fg" && p?.redundanzAllowed;
    const floor = p && !isAd ? sdwanFloor(p, lic, term) : 0;
    const ceil = p && !isAd ? sdwanCeil(p, lic) : 0;
    const eachM = p && !isAd ? sdwanRowMonthlyEach(p, r) : 0;
    const qty = Math.max(1, Number(r.qty) || 1);
    const redMalus = p ? (sdwanRedTable(p.vendor)[lic]?.[term] ?? 0) : 0;
    return (
      <div key={r.id} className="rounded-lg border border-gray-200 bg-white p-3">
        <div className="flex flex-wrap gap-3 items-end">
          <SortArrows g={g} r={r} />
          <button onClick={() => duplicateRow(g.id, r.id)} className="p-2 rounded hover:bg-blue-50 mb-0.5 shrink-0" title="Produkt duplizieren"><Copy size={18} color={TEF_BLUE} /></button><button onClick={() => removeRow(g.id, r.id)} className="p-2 rounded hover:bg-red-50 mb-0.5 shrink-0" title="Produkt entfernen"><Trash2 size={18} color={TEF_RED} /></button>
          <Field label="Produkt" className="grow min-w-[220px]">
            <select ref={el => { if (el) rowFieldRefs.current[r.id] = el; }} className={ctl} value={r.productId} onChange={e => onProduct(g.id, r.id, g.area, e.target.value)}>
              <option value="">— wählen —</option>
              {products.map(pp => pp.isSeparator ? <option key={pp.id} disabled style={{ color: "#aaa" }}>{pp.name}</option> : <option key={pp.id} value={pp.id}>{pp.name}{isDeleted(area, pp.id) ? " (gelöscht)" : ""}</option>)}
            </select>
          </Field>
          {isLicensed && licKeys.length > 1 && <Field label="Lizenztyp" className="w-52">
            <select className={ctl} value={lic} onChange={e => patchRow(g.id, r.id, { sdwanLicense: e.target.value, sdwanPrice: "" })}>
              {licKeys.map(k => <option key={k} value={k}>{sdwanLicLabel(p.vendor, k)}</option>)}
            </select>
          </Field>}
          {!isAd && <Field label="Laufzeit" className="w-40">
            <select className={ctl} value={term} onChange={e => patchRow(g.id, r.id, { term: Number(e.target.value), sdwanPrice: "" })}>
              {SDWAN_TERMS.map(t => <option key={t} value={t}>{t} Monate</option>)}
            </select>
          </Field>}
          <Field label={isRemote ? "25er-Pakete" : p?.qtyLabel || "Menge"} className="w-28"><input type="number" min="1" className={ctl} value={r.qty} onChange={e => patchRow(g.id, r.id, { qty: e.target.value })} onKeyDown={e => { if (e.key === "Enter") { e.preventDefault(); addRow(g.id); } }} /></Field>
          {p?.standort && <Field label="Standort (optional)" className="w-48"><input className={ctl} placeholder="z. B. Werk Kirchheim" value={r.standort} onChange={e => patchRow(g.id, r.id, { standort: e.target.value })} /></Field>}
          {!isAd && <Field label="Preis / Monat (€)" className="w-36"><input type="number" step="0.01" className={ctl} placeholder={String(floor)} value={r.sdwanPrice} onChange={e => patchRow(g.id, r.id, { sdwanPrice: e.target.value })} /></Field>}
          {canRed && <label className="flex items-center gap-2 text-sm mb-2" style={{ color: TEF_GRAY }}><input type="checkbox" checked={!!r.redundanz} onChange={e => patchRow(g.id, r.id, { redundanz: e.target.checked })} />Redundanz</label>}
        </div>
        {isLicensed && licKeys.length === 1 && <div className="mt-1 text-[11px] text-gray-500">Lizenztyp: {sdwanLicLabel(p.vendor, lic)} (einzige Variante)</div>}
        {p && !isAd && <div className="mt-2 text-xs text-gray-600">
          Spanne {term} Monate: <b style={{ color: TEF_BLUE }}>{eur(floor)}</b> (max. Nachlass) bis <b style={{ color: TEF_BLUE }}>{eur(ceil)}</b> (12-Monats-Preis). Leer = Bestpreis {eur(floor)}.
          {canRed && r.redundanz && <> · Redundant: 2 × {eur(sdwanChosen(p, r))} {eur(redMalus)} = <b style={{ color: TEF_BLUE }}>{eur(eachM)}</b></>}
          {" "}· Monatlich gesamt: <b style={{ color: TEF_BLUE }}>{eur(eachM * qty)}</b>
        </div>}
        {isAd && <div className="mt-2 text-xs text-gray-600">Einmalig {eur(SDWAN_AD_ONEOFF)} je Standort · gesamt <b style={{ color: TEF_BLUE }}>{eur(SDWAN_AD_ONEOFF * qty)}</b></div>}
        {isRemote && <div className="mt-2 text-xs text-gray-600">{qty} × 25 = <b style={{ color: TEF_BLUE }}>{qty * 25} Lizenzen</b> · zzgl. {eur(SDWAN_REMOTE_SETUP)} Einrichtung je Paket ({eur(SDWAN_REMOTE_SETUP * qty)})</div>}
      </div>
    );
  };

  // ===== Produktzeile (All IP Basic) =====
  const renderBasicRow = (g, r) => {
    const p = getProduct(g.area, r.productId), products = CATALOG[g.area].products.filter(pp => !isDeleted(g.area, pp.id) || pp.id === r.productId);
    const pricing = r.basicRabatt === "pricing";
    const rt = p ? BASIC_ROUTER[p.connKind] : null;
    return (
      <div key={r.id} className="rounded-lg border border-gray-200 bg-white p-3">
        <div className="flex flex-wrap gap-3 items-end">
          <SortArrows g={g} r={r} />
          <button onClick={() => duplicateRow(g.id, r.id)} className="p-2 rounded hover:bg-blue-50 mb-0.5 shrink-0" title="Produkt duplizieren"><Copy size={18} color={TEF_BLUE} /></button><button onClick={() => removeRow(g.id, r.id)} className="p-2 rounded hover:bg-red-50 mb-0.5 shrink-0" title="Produkt entfernen"><Trash2 size={18} color={TEF_RED} /></button>
          <Field label="Produkt" className="grow min-w-[200px]">
            <select ref={el => { if (el) rowFieldRefs.current[r.id] = el; }} className={ctl} value={r.productId} onChange={e => onProduct(g.id, r.id, g.area, e.target.value)}>
              <option value="">— wählen —</option>
              {products.map(pp => pp.isSeparator ? <option key={pp.id} disabled style={{ color: "#aaa" }}>{pp.name}</option> : <option key={pp.id} value={pp.id}>{pp.name}{isDeleted(g.area, pp.id) ? " (gelöscht)" : ""}</option>)}
            </select>
          </Field>
          {p && <Field label="Laufzeit" className="w-40"><select className={ctl} value={r.term ?? ""} onChange={e => onRowTerm(g.id, r.id, g.area, Number(e.target.value))}>{p.laufzeiten.map(t => <option key={t} value={t}>{t} Monate</option>)}</select></Field>}
          {p && <Field label="Anzahl" className="w-28"><input type="number" min="1" className={ctl} value={r.qty} onChange={e => patchRow(g.id, r.id, { qty: e.target.value })} onKeyDown={e => { if (e.key === "Enter") { e.preventDefault(); addRow(g.id); } }} /></Field>}
          {p && <Field label="Standort (optional)" className="w-48"><input className={ctl} placeholder="z. B. Werk Kirchheim" value={r.standort} onChange={e => patchRow(g.id, r.id, { standort: e.target.value })} /></Field>}
        </div>
        {p && <div className="mt-2 flex flex-wrap gap-x-6 gap-y-2">
          {p.zweiterKanal && <label className="flex items-center gap-2 text-sm" style={{ color: TEF_GRAY }}><input type="checkbox" checked={!!r.zweiterKanal} onChange={e => patchRow(g.id, r.id, { zweiterKanal: e.target.checked })} />Zweiter Sprachkanal <span className="text-xs text-gray-500">(+{eur(3)}/Monat)</span></label>}
          {rt && <label className="flex items-center gap-2 text-sm" style={{ color: TEF_GRAY }}><input type="checkbox" checked={!!r.router} onChange={e => patchRow(g.id, r.id, { router: e.target.checked })} />Router {rt.name} <span className="text-xs text-gray-500">{rt.price > 0 ? `(+${eur(rt.price)}/Monat)` : "(inklusive)"}</span></label>}
          <label className="flex items-center gap-2 text-sm" style={{ color: TEF_GRAY }}><input type="checkbox" checked={!!r.euPlus} onChange={e => patchRow(g.id, r.id, { euPlus: e.target.checked })} />Fair Use EU Plus <span className="text-xs text-gray-500">(+{eur(euPlusPrice(r.term))}/Monat)</span></label>
          <label className="flex items-center gap-2 text-sm" style={{ color: TEF_GRAY }}><input type="checkbox" checked={!!r.worldSelect} onChange={e => patchRow(g.id, r.id, { worldSelect: e.target.checked })} />Fair Use World Select <span className="text-xs text-gray-500">(+{eur(worldSelectPrice(r.term))}/Monat)</span></label>
        </div>}
        {p && <div className="mt-2 flex flex-wrap gap-3 items-end">
          <Field label="Rabatt" className="w-36">
            <select className={ctl} value={r.basicRabatt} onChange={e => { const v = e.target.value; if (v === "pricing") { const b = resolvePrice(p, r.term); patchRow(g.id, r.id, { basicRabatt: v, monthly: b.monthly, oneoff: b.oneoff }); } else patchRow(g.id, r.id, { basicRabatt: v }); }}>
              <option value="">kein Rabatt</option>
              <option value="10">10 %</option>
              <option value="20">20 %</option>
              <option value="30">30 %</option>
              <option value="pricing">Pricing</option>
            </select>
          </Field>
          {pricing && <><Field label="Monatlich (€)" className="w-32"><input type="number" step="0.01" className={ctl} value={r.monthly} onChange={e => patchRow(g.id, r.id, { monthly: e.target.value })} /></Field><Field label="Einmalig (€)" className="w-32"><input type="number" step="0.01" className={ctl} value={r.oneoff} onChange={e => patchRow(g.id, r.id, { oneoff: e.target.value })} /></Field></>}
          {(r.basicRabatt === "10" || r.basicRabatt === "20" || r.basicRabatt === "30") && <Field label="Darstellung" className="w-52"><select className={ctl} value={r.basicMode || "verrechnet"} onChange={e => patchRow(g.id, r.id, { basicMode: e.target.value })}><option value="verrechnet">im Preis verrechnen</option><option value="zeile">als separate Zeile</option><option value="abzug">unter Preis abziehen</option></select></Field>}
        </div>}
        {p && <div className="mt-1 text-[11px] text-gray-500">Prozent-Rabatt gilt nur auf den monatlichen Produktpreis{p.zweiterKanal ? " (inkl. zweitem Sprachkanal)" : ""} – nicht auf Router, EU Plus, World Select.</div>}
      </div>
    );
  };

  // ===== Produktzeile (Individuell) =====
  const renderCustomRow = (g, r) => (
    <div key={r.id} className="rounded-lg border border-gray-200 bg-white p-3">
      <div className="flex flex-wrap gap-3 items-end">
        <SortArrows g={g} r={r} />
        <button onClick={() => duplicateRow(g.id, r.id)} className="p-2 rounded hover:bg-blue-50 mb-0.5 shrink-0" title="Produkt duplizieren"><Copy size={18} color={TEF_BLUE} /></button><button onClick={() => removeRow(g.id, r.id)} className="p-2 rounded hover:bg-red-50 mb-0.5 shrink-0" title="Produkt entfernen"><Trash2 size={18} color={TEF_RED} /></button>
        <Field label="Produkt" className="grow min-w-[200px]"><input ref={el => { if (el) rowFieldRefs.current[r.id] = el; }} className={ctl} placeholder="z. B. Beratungspauschale" value={r.custName} onChange={e => patchRow(g.id, r.id, { custName: e.target.value })} /></Field>
        <Field label="Laufzeit" className="w-40"><select className={ctl} value={r.term ?? ""} onChange={e => patchRow(g.id, r.id, { term: e.target.value === "" ? null : Number(e.target.value) })}><option value="">— ohne —</option>{[12, 24, 36, 48, 60].map(t => <option key={t} value={t}>{t} Monate</option>)}</select></Field>
        <Field label="Anzahl" className="w-28"><input type="number" min="1" className={ctl} value={r.qty} onChange={e => patchRow(g.id, r.id, { qty: e.target.value })} onKeyDown={e => { if (e.key === "Enter") { e.preventDefault(); addRow(g.id); } }} /></Field>
        <Field label="Standort (optional)" className="w-48"><input className={ctl} placeholder="z. B. Werk Kirchheim" value={r.standort} onChange={e => patchRow(g.id, r.id, { standort: e.target.value })} /></Field>
        <Field label="Monatlich (€)" className="w-32"><input type="number" step="0.01" className={ctl} value={r.monthly} onChange={e => patchRow(g.id, r.id, { monthly: e.target.value })} /></Field>
        <Field label="Einmalig (€)" className="w-32"><input type="number" step="0.01" className={ctl} value={r.oneoff} onChange={e => patchRow(g.id, r.id, { oneoff: e.target.value })} /></Field>
      </div>
      <Field label="Beschreibung (eine Zeile je Stichpunkt)" className="mt-2">
        <textarea className={ctl + " min-h-[64px] resize-y"} placeholder={"z. B.\nPersönlicher Ansprechpartner\nReaktionszeit 4 Stunden"} value={r.custDesc} onChange={e => patchRow(g.id, r.id, { custDesc: e.target.value })} />
      </Field>
    </div>
  );

  // ===== Produktzeile (Kalkulator) =====
  const renderRow = (g, r) => {
    if (g.area === "individuell") return renderCustomRow(g, r);
    if (g.area === "allipbasic") return renderBasicRow(g, r);
    if (g.area === "sdwan" || g.area === "sdwanmeraki") return renderSdwanRow(g, r);
    const p = getProduct(g.area, r.productId), products = CATALOG[g.area].products.filter(pp => !isDeleted(g.area, pp.id) || pp.id === r.productId);
    const hasLz = p?.laufzeiten?.length > 0, isQtyManual = p?.qty && p?.mengenBezug !== "nebenstellen", isOpen = p?.priceOpen, isLine = p?.extrasKind === "line", isMobile = !!p?.mobile;
    const qv = Math.max(1, Number(r.qty) || 1), stMax = p?.rabattType === "staffelMax" ? staffelPct(p.staffel, qv) : 0;
    const oMax = p?.office ? officeMax(p, g.managed) : 0, oPrice = p?.office ? officePrice(p, g.managed) : 0, oNoMgmt = p?.office ? officeNoMgmt(p, g.managed) : false;
    const showDarstellung = (p?.rabattType === "fix10" && r.discountFix) || (p?.rabattType === "staffelMax" && Number(r.discountPct) > 0) || (p?.rabattType === "officeMax" && Number(r.discountPct) > 0) || (isLine && r.lineProvider === "dtag" && ["10", "20", "30"].includes(r.lineRabatt)) || (isMobile && ["10", "20", "30"].includes(r.mobileRabatt)) || (p?.pureDiscount && Number(r.pureRabatt) > 0);
    return (
      <div key={r.id} className="rounded-lg border border-gray-200 bg-white p-3">
        <div className="flex flex-wrap gap-3 items-end">
          <SortArrows g={g} r={r} />
          <button onClick={() => duplicateRow(g.id, r.id)} className="p-2 rounded hover:bg-blue-50 mb-0.5 shrink-0" title="Produkt duplizieren"><Copy size={18} color={TEF_BLUE} /></button><button onClick={() => removeRow(g.id, r.id)} className="p-2 rounded hover:bg-red-50 mb-0.5 shrink-0" title="Produkt entfernen"><Trash2 size={18} color={TEF_RED} /></button>
          <Field label="Produkt" className="grow min-w-[200px]">
            <select ref={el => { if (el) rowFieldRefs.current[r.id] = el; }} className={ctl} value={r.productId} onChange={e => onProduct(g.id, r.id, g.area, e.target.value)}>
              <option value="">— wählen —</option>
              {products.map(pp => pp.isSeparator ? <option key={pp.id} disabled style={{ color: "#aaa" }}>{pp.name}</option> : <option key={pp.id} value={pp.id}>{pp.name}{isDeleted(g.area, pp.id) ? " (gelöscht)" : ""}</option>)}
            </select>
          </Field>
          {hasLz && <Field label="Laufzeit" className="w-40"><select className={ctl} value={r.term ?? ""} onChange={e => onRowTerm(g.id, r.id, g.area, Number(e.target.value))}>{p.laufzeiten.map(t => <option key={t} value={t}>{t} Monate</option>)}</select></Field>}
          {isQtyManual && <Field label={p.qtyLabel || "Menge"} className="w-28"><input type="number" min={p.qtyMin || 1} className={ctl} value={r.qty} onChange={e => patchRow(g.id, r.id, { qty: e.target.value })} onKeyDown={e => { if (e.key === "Enter") { e.preventDefault(); addRow(g.id); } }} /></Field>}
          {isQtyManual && p?.qtyMin > 1 && Number(r.qty) < p.qtyMin && <div className="text-[11px] self-center mb-2" style={{ color: TEF_RED }}>Mindestmenge {p.qtyMin} – wird in der Kalkulation automatisch angesetzt</div>}
          {p?.standort && <Field label="Standort (optional)" className="w-48"><input className={ctl} placeholder="z. B. Werk Kirchheim" value={r.standort} onChange={e => patchRow(g.id, r.id, { standort: e.target.value })} /></Field>}
          {isOpen && !isLine && <><Field label="Monatlich (€)" className="w-32"><input type="number" step="0.01" className={ctl} value={r.monthly} onChange={e => patchRow(g.id, r.id, { monthly: e.target.value })} /></Field><Field label="Einmalig (€)" className="w-32"><input type="number" step="0.01" className={ctl} value={r.oneoff} onChange={e => patchRow(g.id, r.id, { oneoff: e.target.value })} /></Field></>}
          {isLine && <>
            <Field label="Anbindung" className="w-36"><select className={ctl} value={r.lineProvider} onChange={e => patchRow(g.id, r.id, { lineProvider: e.target.value })}><option value="dtag">DTAG</option><option value="andere">Andere</option></select></Field>
            {r.lineProvider === "dtag" && <Field label="ONKZ" className="w-32"><input className={ctl} placeholder="z. B. 0711" value={r.onkz} onChange={e => patchRow(g.id, r.id, { onkz: e.target.value })} /></Field>}
            {r.lineProvider === "dtag" && <Field label="Rabatt" className="w-36"><select className={ctl} value={r.lineRabatt} onChange={e => { const v = e.target.value; if (v === "pricing") { const b = lineDtagBase(p, r); patchRow(g.id, r.id, { lineRabatt: v, monthly: b.monthly, oneoff: b.oneoff }); } else patchRow(g.id, r.id, { lineRabatt: v }); }}><option value="0">0 %</option><option value="10">10 %</option><option value="20">20 %</option><option value="30">30 %</option><option value="pricing">Pricing</option></select></Field>}
            {(r.lineProvider === "andere" || (r.lineProvider === "dtag" && r.lineRabatt === "pricing")) && <><Field label="Monatlich (€)" className="w-32"><input type="number" step="0.01" className={ctl} value={r.monthly} onChange={e => patchRow(g.id, r.id, { monthly: e.target.value })} /></Field><Field label="Einmalig (€)" className="w-32"><input type="number" step="0.01" className={ctl} value={r.oneoff} onChange={e => patchRow(g.id, r.id, { oneoff: e.target.value })} /></Field></>}
          </>}
          {isMobile && <>
            <Field label="Geschwindigkeit" className="w-44">
              <select className={ctl} value={r.mobileSpeed} onChange={e => patchRow(g.id, r.id, { mobileSpeed: e.target.value })}>
                {MOBILE_SPEEDS.map(s => <option key={s.key} value={s.key}>{s.label}</option>)}
              </select>
            </Field>
            <Field label="Rabatt" className="w-36">
              <select className={ctl} value={r.mobileRabatt} onChange={e => { const v = e.target.value; if (v === "pricing") { const b = mobileMainBase(r); patchRow(g.id, r.id, { mobileRabatt: v, monthly: b.monthly, oneoff: b.oneoff }); } else patchRow(g.id, r.id, { mobileRabatt: v }); }}>
                <option value="0">0 %</option><option value="10">10 %</option><option value="20">20 %</option><option value="30">30 %</option><option value="pricing">Pricing</option>
              </select>
            </Field>
            {r.mobileRabatt === "pricing" && <><Field label="Monatlich (€)" className="w-32"><input type="number" step="0.01" className={ctl} value={r.monthly} onChange={e => patchRow(g.id, r.id, { monthly: e.target.value })} /></Field><Field label="Einmalig (€)" className="w-32"><input type="number" step="0.01" className={ctl} value={r.oneoff} onChange={e => patchRow(g.id, r.id, { oneoff: e.target.value })} /></Field></>}
            <Field label="Antenne" className="w-64">
              <select className={ctl} value={r.antenne} onChange={e => patchRow(g.id, r.id, { antenne: e.target.value })}>
                <option value="">keine</option>
                {ANTENNEN.map(a => <option key={a.key} value={a.key}>{a.label} (+{eur(a.oneoff)} einmalig)</option>)}
              </select>
            </Field>
          </>}
          {p?.pureDiscount && <Field label="Rabatt" className="w-36">
            <select className={ctl} value={r.pureRabatt} onChange={e => patchRow(g.id, r.id, { pureRabatt: e.target.value })}>
              <option value="0">0 %</option><option value="10">10 %</option><option value="20">20 %</option>
              {Number(r.term) >= 36 && <option value="30">30 %</option>}
            </select>
          </Field>}
        </div>
        {p?.office && <div className="mt-2 text-xs text-gray-600">Preis: <b style={{ color: TEF_BLUE }}>{eur(oPrice)}</b> / Monat{oNoMgmt && <span style={{ color: TEF_RED }}> · Kein Management durch Telefónica</span>}</div>}
        {p?.extrasKind && <div className="mt-2 flex flex-wrap gap-x-6 gap-y-2">
          <label className="flex items-center gap-2 text-sm" style={{ color: TEF_GRAY }}><input type="checkbox" checked={!!r.backup} onChange={e => { const checked = e.target.checked; patchRow(g.id, r.id, checked ? { backup: true } : { backup: false, express: false }); }} />Mobilfunk-Backup <span className="text-xs text-gray-500">(+{eur(backupMonthly(p.extrasKind, r.term))} / Monat)</span></label>
          {r.backup && <label className="flex items-center gap-2 text-sm" style={{ color: TEF_GRAY }}><input type="checkbox" checked={!!r.express} onChange={e => patchRow(g.id, r.id, { express: e.target.checked })} />Express-Bereitstellung <span className="text-xs text-gray-500">(+{eur(EXPRESS_PRICE[p.extrasKind])} einmalig)</span></label>}
        </div>}
        {p?.extrasKind && r.backup && <div className="mt-2"><Field label="Antenne" className="w-64">
          <select className={ctl} value={r.antenne} onChange={e => patchRow(g.id, r.id, { antenne: e.target.value })}>
            <option value="">keine</option>
            {ANTENNEN.map(a => <option key={a.key} value={a.key}>{a.label} (+{eur(a.oneoff)} einmalig)</option>)}
          </select>
        </Field></div>}
        {p?.wave && <div className="mt-2 flex flex-wrap gap-x-6 gap-y-2">
          <label className="flex items-center gap-2 text-sm" style={{ color: TEF_GRAY }}><input type="checkbox" checked={!!r.businessClass} onChange={e => patchRow(g.id, r.id, { businessClass: e.target.checked })} />Business Class Support (8h) <span className="text-xs text-gray-500">(+{eur(WAVE_SUPPORT_PRICE)} / Monat)</span></label>
        </div>}
        {p?.wave && <div className="mt-2 text-xs text-gray-600">„Line of Sight"-Test wird automatisch mit <b style={{ color: TEF_BLUE }}>{eur(WAVE_LOS_PRICE)}</b> einmalig als eigene Position ergänzt.</div>}
        {p?.inetFlat && <div className="mt-2">
          <label className="flex items-center gap-2 text-sm" style={{ color: TEF_GRAY }}><input type="checkbox" checked={!!r.internetFlat} onChange={e => patchRow(g.id, r.id, { internetFlat: e.target.checked })} />Internet-Flatrate <span className="text-xs text-gray-500">(+{eur(Number(r.term) >= 36 ? INET_FLAT.m36 : INET_FLAT.m24)} / Monat · separat, nicht rabattiert)</span></label>
        </div>}
        {p?.maxKanal && <div className="mt-2 rounded-lg border p-3" style={{ borderColor: TEF_BLUE_2, background: "#EEF2FF" }}>
          <div className="flex items-end gap-3 flex-wrap">
            <Field label="Sprachkanäle" className="w-36">
              <select className={ctl} value={r.kanal} onChange={e => patchRow(g.id, r.id, { kanal: Number(e.target.value) })}>
                {KANAL_STEPS.filter(s => s <= p.maxKanal).map(s => <option key={s} value={s}>{s}</option>)}
              </select>
            </Field>
            <div className="text-[11px] text-gray-500 mb-2">max. {p.maxKanal} Kanäle für diese Anbindung</div>
          </div>
          {r.kanal > 0 && <div className="mt-3 grid grid-cols-1 md:grid-cols-2 gap-3">
            {RICHTUNGEN.map(dir => {
              const t = (r.kanalTarife && r.kanalTarife[dir.key]) || { mode: "standard", min: 0 };
              const setT = (patch) => patchRow(g.id, r.id, { kanalTarife: { ...(r.kanalTarife || blankKanalTarife()), [dir.key]: { ...t, ...patch } } });
              return (
                <div key={dir.key} className="rounded-lg border border-gray-200 bg-white p-2.5">
                  <div className="text-sm font-semibold mb-1.5" style={{ color: TEF_BLUE }}>{dir.label}</div>
                  <select className={ctl} value={t.mode} onChange={e => setT({ mode: e.target.value })}>
                    <option value="standard">Standard (kostenlos)</option>
                    <option value="fairuse">Fair Use</option>
                    <option value="pooling">Pooling</option>
                  </select>
                  {t.mode === "fairuse" && <div className="text-[11px] text-gray-500 mt-1">+{eur(dir.fair)} je Sprachkanal</div>}
                  {t.mode === "pooling" && <div className="mt-2 flex items-end gap-2">
                    <Field label="Minuten" className="w-28"><input type="number" min="0" className={ctl} value={t.min} onChange={e => setT({ min: Number(e.target.value) || 0 })} /></Field>
                    <div className="text-[11px] text-gray-500 mb-2">{dir.poolCt.toLocaleString("de-DE")} ct/Min</div>
                  </div>}
                </div>
              );
            })}
          </div>}
        </div>}
        {p?.mengenBezug === "nebenstellen" && <div className="mt-2 text-xs text-gray-600 bg-blue-50 rounded p-2 inline-block">Nebenstellen automatisch aus Basispaket + Lizenzen: <b style={{ color: TEF_BLUE }}>{totalNST}</b></div>}
        {p?.teamsCoupling && <label className="mt-2 flex items-center gap-2 text-sm" style={{ color: TEF_GRAY }}><input type="checkbox" checked={!!r.teamsCoupling} onChange={e => patchRow(g.id, r.id, { teamsCoupling: e.target.checked })} />Kopplung mit Microsoft Teams gewünscht <span className="text-xs text-gray-500">(+4 € je Sprachkanal)</span></label>}
        {(p?.abloese || p?.rabattType || showDarstellung) && <div className="mt-2 flex flex-wrap gap-3 items-end">
          {p?.abloese && <Field label="Ablöse" className="w-36"><select className={ctl} value={r.abloese} onChange={e => patchRow(g.id, r.id, { abloese: Number(e.target.value) })}><option value={0}>keine</option><option value={3}>3 Monate</option><option value={6}>6 Monate</option></select></Field>}
          {p?.rabattType === "fix10" && <label className="flex items-center gap-2 text-sm mb-2" style={{ color: TEF_GRAY }}><input type="checkbox" checked={r.discountFix} onChange={e => patchRow(g.id, r.id, { discountFix: e.target.checked })} />Bestandskundenrabatt 10 € / Monat</label>}
          {p?.rabattType === "staffelMax" && <Field label={`Rabatt monatlich (0–${stMax} %)`} className="w-48"><input type="number" min="0" max={stMax} className={ctl} value={r.discountPct} onChange={e => patchRow(g.id, r.id, { discountPct: Math.min(stMax, Math.max(0, Number(e.target.value) || 0)) })} /></Field>}
          {p?.rabattType === "officeMax" && <Field label={`Rabatt monatlich (0–${oMax} %)`} className="w-48"><input type="number" min="0" max={oMax} className={ctl} value={r.discountPct} onChange={e => patchRow(g.id, r.id, { discountPct: Math.min(oMax, Math.max(0, Number(e.target.value) || 0)) })} /></Field>}
          {showDarstellung && <Field label="Darstellung" className="w-52"><select className={ctl} value={r.discountMode} onChange={e => patchRow(g.id, r.id, { discountMode: e.target.value })}><option value="verrechnet">im Preis verrechnen</option><option value="zeile">als separate Zeile</option><option value="abzug">unter Preis abziehen</option></select></Field>}
        </div>}
        {p?.produkttyp === "lizenz" && (() => { const dp = dpStats(g); return (
          <div className="mt-2 rounded-lg border p-3" style={{ borderColor: TEF_BLUE_2, background: "#EEF2FF" }}>
            <label className="flex items-center gap-2 text-sm font-medium" style={{ color: TEF_GRAY }}><input type="checkbox" checked={!!g.kombi} onChange={e => patchGroup(g.id, { kombi: e.target.checked })} />Kombi-Vorteil nutzen</label>
            <div className="text-xs text-gray-600 mt-2">Listenumsatz: <b style={{ color: TEF_BLUE }}>{eur(dp.umsatz)}</b> <span className="text-gray-500">({dp.bezahl} Bezahlmonate)</span> · Max. Rabatt: <b style={{ color: TEF_BLUE }}>{dp.maxPct} %</b></div>
            <div className="flex flex-wrap gap-3 items-end mt-2">
              <Field label={`Rabatt monatlich (0–${dp.maxPct} %)`} className="w-44"><input type="number" min="0" max={dp.maxPct} className={ctl} value={g.dpPct} onChange={e => patchGroup(g.id, { dpPct: Math.min(dp.maxPct, Math.max(0, Number(e.target.value) || 0)) })} /></Field>
              {Number(g.dpPct) > 0 && <Field label="Darstellung" className="w-52"><select className={ctl} value={g.dpMode} onChange={e => patchGroup(g.id, { dpMode: e.target.value })}><option value="verrechnet">im Preis verrechnen</option><option value="zeile">als separate Zeile</option><option value="abzug">unter Preis abziehen</option></select></Field>}
            </div>
            <div className="text-[11px] text-gray-500 mt-1">Gilt nur auf zusätzliche Lizenzen.</div>
          </div>
        ); })()}
        {isOpen && !isLine && <div className="mt-2 inline-flex items-center gap-1 text-amber-600 font-medium text-xs"><AlertTriangle size={13} /> Preis offen – bitte Monats-/Einmalpreis eintragen</div>}
        {isLine && r.lineProvider === "dtag" && r.lineRabatt !== "pricing" && (() => { const b = lineDtagBase(p, r); return <div className="mt-2 text-xs text-gray-600">Preisband: <b style={{ color: TEF_BLUE }}>{b.region}</b>{!r.onkz && <span className="text-gray-400"> (Standard – bitte ONKZ eingeben)</span>} · DTAG-Listenpreis: <b style={{ color: TEF_BLUE }}>{eur(b.monthly)}</b> / Monat{b.oneoff ? <> · {eur(b.oneoff)} einmalig</> : ""}{Number(r.lineRabatt) > 0 && <> · nach {r.lineRabatt}% Rabatt: <b style={{ color: TEF_BLUE }}>{eur(b.monthly * (1 - Number(r.lineRabatt) / 100))}</b> / Monat</>}</div>; })()}
        {isLine && r.lineProvider === "dtag" && r.lineRabatt === "pricing" && <div className="mt-2 text-xs text-gray-600">Preisband: <b style={{ color: TEF_BLUE }}>{onkzRegion(r.onkz)}</b> · Preisfelder mit DTAG-Listenpreis vorbefüllt, frei überschreibbar.</div>}
        {isLine && (r.lineProvider === "andere" || (r.lineProvider === "dtag" && r.lineRabatt === "pricing")) && <div className="mt-2 inline-flex items-center gap-1 text-amber-600 font-medium text-xs"><AlertTriangle size={13} /> Preis offen – bitte Monats-/Einmalpreis eintragen</div>}
        {isMobile && r.mobileRabatt !== "pricing" && (() => { const b = mobileMainBase(r); const pct = Number(r.mobileRabatt) || 0; const disc = (b.baseM + b.speedM) * (1 - pct / 100) + b.flatM; return <div className="mt-2 text-xs text-gray-600">Mobile Main: <b style={{ color: TEF_BLUE }}>{eur(b.baseM)}</b> + {b.speed.label}: <b style={{ color: TEF_BLUE }}>{eur(b.speedM)}</b> + Internet-Flat: <b style={{ color: TEF_BLUE }}>{eur(b.flatM)}</b>{pct > 0 && <> · nach {pct}% Rabatt (auf Mobile Main + Geschwindigkeit): <b style={{ color: TEF_BLUE }}>{eur(disc)}</b> / Monat</>}</div>; })()}
        {isMobile && r.mobileRabatt === "pricing" && <div className="mt-2 text-xs text-gray-600">Preisfelder mit Listenpreis (ohne Rabatt) vorbefüllt, frei überschreibbar.</div>}
        {isMobile && r.mobileRabatt === "pricing" && <div className="mt-2 inline-flex items-center gap-1 text-amber-600 font-medium text-xs"><AlertTriangle size={13} /> Preis offen – bitte Monats-/Einmalpreis eintragen</div>}
        {isMobile && mobileMainBase(r).speedUnpriced && <div className="mt-2 inline-flex items-center gap-1 text-amber-600 font-medium text-xs"><AlertTriangle size={13} /> Preis für {mobileMainBase(r).speed.label} noch nicht hinterlegt</div>}
      </div>
    );
  };

  // helper: find row object by gid/rowId for preview arrows
  const findRow = (gid, rid) => {
    for (const g of groups) {
      if (g.id === gid) return { g, r: g.rows.find(x => x.id === rid) };
      if (g.area === "standort") for (const sg of g.subGroups || []) if (sg.id === gid) return { g: sg, r: sg.rows.find(x => x.id === rid) };
    }
    return null;
  };

  // ===== Unterbereich-Karte innerhalb eines Standortangebots (gleiche Logik wie eine normale Bereichs-Karte) =====
  const renderSubGroupCard = (parentG, sg, sgi, totalSub) => {
    const kinds = new Set();
    for (const r of sg.rows) { const pp = getProduct(sg.area, r.productId); if (pp?.mdmKind) kinds.add(pp.mdmKind); }
    const mdmMix = kinds.has("managed") && kinds.has("unmanaged");
    return (
      <div key={sg.id} className="rounded-lg border-2 border-gray-200 overflow-hidden bg-white">
        <div className="flex flex-col md:flex-row">
          <div className="md:w-40 shrink-0 p-2.5 border-b md:border-b-0 md:border-r border-gray-200" style={{ background: "#F0F4FF" }}>
            <div className="flex items-center justify-between mb-1.5">
              <span className="text-[10px] font-bold inline-flex items-center gap-1" style={{ color: TEF_BLUE }}>
                Unterbereich {sgi + 1}
                {(() => { const n = plausibility.filter(x => x.gid === sg.id).length; const hasWarn = plausibility.some(x => x.gid === sg.id && x.level === "warn"); return n > 0 ? <span title={`${n} Hinweis(e) – siehe Plausibilitäts-Prüfung im Angebot`} className="inline-flex items-center justify-center rounded-full text-white text-[10px] font-bold w-4 h-4" style={{ background: hasWarn ? TEF_RED : TEF_BLUE }}>{n}</span> : null; })()}
              </span>
              <div className="flex items-center gap-0.5">
                <button onClick={() => moveSubGroupDir(parentG.id, sg.id, -1)} disabled={sgi <= 0} className="p-0.5 rounded hover:bg-white disabled:opacity-25" title="Unterbereich nach oben"><ChevronUp size={14} color={TEF_BLUE} /></button>
                <button onClick={() => moveSubGroupDir(parentG.id, sg.id, 1)} disabled={sgi >= totalSub - 1} className="p-0.5 rounded hover:bg-white disabled:opacity-25" title="Unterbereich nach unten"><ChevronDown size={14} color={TEF_BLUE} /></button>
              </div>
            </div>
            <label className={lbl} style={{ color: TEF_BLUE }}>Produktbereich</label>
            <select className={ctl} value={sg.area} onChange={e => setArea(sg.id, e.target.value)}>
              <option value="">— wählen —</option>
              {SUBAREAS.map(a => <option key={a.key} value={a.key}>{a.label}</option>)}
            </select>
            <div className="mt-2 flex items-center gap-2">
              <button onClick={() => duplicateSubGroup(parentG.id, sg.id)} className="inline-flex items-center gap-1.5 text-[11px] font-medium px-2 py-1 rounded hover:bg-blue-50" style={{ color: TEF_BLUE }} title="Unterbereich duplizieren"><Copy size={12} /> duplizieren</button>
              <button onClick={() => removeSubGroup(parentG.id, sg.id)} className="inline-flex items-center gap-1.5 text-[11px] font-medium px-2 py-1 rounded hover:bg-red-50" style={{ color: TEF_RED }}><Trash2 size={12} /> entfernen</button>
            </div>
          </div>
          <div className="flex-1 min-w-0 p-3 space-y-2.5 bg-gray-50">
            {mdmMix && <div className="flex items-start gap-2 text-sm rounded p-2 border" style={{ color: TEF_RED, borderColor: TEF_RED, background: "#fdecea" }}><AlertTriangle size={15} className="mt-0.5 shrink-0" />Kein Mix aus Managed und Unmanaged möglich – bitte nur eine Variante wählen.</div>}
            {sg.area === "office365" && <div className="rounded-lg border p-2.5" style={{ borderColor: TEF_BLUE_2, background: "#EEF2FF" }}>
              <label className="flex items-center gap-2 text-sm font-medium" style={{ color: TEF_GRAY }}><input type="checkbox" checked={!!sg.managed} onChange={e => patchGroup(sg.id, { managed: e.target.checked })} />Managed (Verwaltung durch Telefónica)</label>
              <div className="text-[11px] text-gray-500 mt-1">Steuert Preise & max. Rabatt aller Lizenzen. Ohne Managed-Preis wird License-Only-Preis genutzt.</div>
            </div>}
            {(sg.area === "sdwan" || sg.area === "sdwanmeraki") && <div className="rounded-lg border p-2.5" style={{ borderColor: TEF_BLUE_2, background: "#EEF2FF" }}>
              <label className={lbl} style={{ color: TEF_BLUE }}>Standard-Laufzeit (Vorauswahl neuer Produkte)</label>
              <select className={ctl} value={sg.sdwanTerm || 60} onChange={e => patchGroup(sg.id, { sdwanTerm: Number(e.target.value) })}>{SDWAN_TERMS.map(t => <option key={t} value={t}>{t} Monate</option>)}</select>
              <div className="text-[11px] text-gray-500 mt-1">Neu hinzugefügte Produkte starten mit dieser Laufzeit. Je Zeile weiterhin einzeln änderbar.</div>
            </div>}
            {sg.area === "individuell" && <div className="rounded-lg border p-2.5" style={{ borderColor: TEF_BLUE_2, background: "#EEF2FF" }}>
              <label className={lbl} style={{ color: TEF_BLUE }}>Produktbereich (frei)</label>
              <input className={ctl} placeholder="z. B. Professional Services" value={sg.customArea || ""} onChange={e => patchGroup(sg.id, { customArea: e.target.value })} />
              <div className="text-[11px] text-gray-500 mt-1">Erscheint im Angebot als Unter-Überschrift dieses Bereichs.</div>
            </div>}
            {!sg.area ? <p className="text-sm text-gray-400">Bitte links einen Produktbereich wählen.</p> : sg.area === "vpnconnect" ? renderVpnEditor(sg) : <>
              {sg.rows.map(r => renderRow(sg, r))}
              <button onClick={() => addRow(sg.id)} style={{ color: TEF_BLUE_2, borderColor: TEF_BLUE_2 }} className="inline-flex items-center gap-2 px-3 py-1.5 rounded-lg border-2 border-dashed text-sm font-semibold hover:bg-blue-50"><Plus size={15} /> Produkt hinzufügen</button>
            </>}
          </div>
        </div>
      </div>
    );
  };

  // ===== Bearbeiten-Reiter =====
  const PRICE_LABELS = { m24: "Monatl. 24 Mon. (€)", m36: "Monatl. 36+ Mon. (€)", o24: "Einmalig 24 Mon. (€)", o36: "Einmalig 36+ Mon. (€)", loPrice: "Monatlich (€)", mgdPrice: "Monatl. Managed (€)" };
  const editAreas = AREAS.filter(a => (BASE_CATALOG[a.key]?.products || []).length > 0);
  const q = editSearch.trim().toLowerCase();
  const productOverridden = (key) => { const ov = overrides.products[key]; return ov && Object.keys(ov).length > 0; };
  const isDescEmpty = (areaKey, baseProd) => {
    const ov = overrides.products[`${areaKey}::${baseProd.id}`] || {};
    const desc = "desc" in ov ? ov.desc : baseProd.desc;
    return !Array.isArray(desc) || desc.length === 0;
  };

  const renderProductCard = (areaKey, areaLabel, baseProd) => {
    const key = `${areaKey}::${baseProd.id}`;
    const ov = overrides.products[key] || {};
    const nameVal = "name" in ov ? ov.name : baseProd.name;
    const descVal = "desc" in ov ? (ov.desc || []).join("\n") : (Array.isArray(baseProd.desc) ? baseProd.desc : []).join("\n");
    const priceKeys = PRICE_FIELDS.filter(f => f in baseProd);
    const computed = baseProd.priceOpen || baseProd.mobile || baseProd.sdwan;
    const changed = productOverridden(key);
    const deleted = isDeleted(areaKey, baseProd.id);
    if (deleted) {
      return (
        <div key={key} className="rounded-lg border bg-gray-50 p-3 opacity-70">
          <div className="flex items-center justify-between gap-2">
            <div className="text-sm">
              <span className="font-semibold" style={{ color: TEF_GRAY }}>{nameVal}</span>
              <span className="ml-2 text-[10px] font-bold px-2 py-0.5 rounded-full" style={{ color: "#fff", background: TEF_RED }}>gelöscht</span>
              <div className="text-[11px] text-gray-400 mt-0.5">{areaLabel} · {baseProd.id}</div>
            </div>
            <button onClick={() => restoreProduct(areaKey, baseProd.id)} className="inline-flex items-center gap-1 text-[11px] font-medium px-2 py-1 rounded hover:bg-blue-50 shrink-0" style={{ color: TEF_BLUE }}><RotateCcw size={12} /> wiederherstellen</button>
          </div>
        </div>
      );
    }
    return (
      <div key={key} className="rounded-lg border bg-white p-3" style={{ borderColor: changed ? TEF_BLUE_2 : "#e5e7eb" }}>
        <div className="flex items-center justify-between gap-2 mb-2">
          <div className="text-[11px] font-semibold" style={{ color: TEF_BLUE }}>{areaLabel} <span className="text-gray-400 font-normal">· {baseProd.id}</span></div>
          <div className="flex items-center gap-2">
            {changed && <span className="text-[10px] font-bold px-2 py-0.5 rounded-full" style={{ color: "#fff", background: TEF_BLUE_2 }}>geändert</span>}
            {changed && <button onClick={() => resetProduct(areaKey, baseProd.id)} className="inline-flex items-center gap-1 text-[11px] font-medium px-2 py-1 rounded hover:bg-blue-50" style={{ color: TEF_BLUE }} title="Auf Originalwerte zurücksetzen"><RotateCcw size={12} /> zurücksetzen</button>}
            <button onClick={() => duplicateProduct(areaKey, baseProd)} className="inline-flex items-center gap-1 text-[11px] font-medium px-2 py-1 rounded hover:bg-blue-50" style={{ color: TEF_BLUE }} title="Als eigene, bearbeitbare Kopie anlegen"><Copy size={12} /> duplizieren</button>
            <button onClick={() => deleteProduct(areaKey, baseProd.id, nameVal)} className="inline-flex items-center gap-1 text-[11px] font-medium px-2 py-1 rounded hover:bg-red-50" style={{ color: TEF_RED }} title="Aus der Produktauswahl entfernen"><Trash2 size={12} /> löschen</button>
          </div>
        </div>
        <Field label="Produktname"><input className={ctl} value={nameVal} onChange={e => setProductField(areaKey, baseProd.id, "name", e.target.value)} /></Field>
        {priceKeys.length > 0 && <div className="mt-2 grid grid-cols-2 md:grid-cols-4 gap-2">
          {priceKeys.map(f => { const v = f in ov ? ov[f] : baseProd[f]; return (
            <Field key={f} label={PRICE_LABELS[f] || f}><input type="number" step="0.01" className={ctl} value={v ?? ""} onChange={e => setProductField(areaKey, baseProd.id, f, e.target.value)} /></Field>
          ); })}
        </div>}
        {computed && <div className="mt-2 text-[11px] text-amber-600 inline-flex items-center gap-1"><AlertTriangle size={12} /> Preis wird dynamisch ermittelt (ONKZ / Tarifoptionen / SD-WAN-Preislogik) – Name & Beschreibung anpassbar.</div>}
        <Field label="Beschreibung (eine Zeile je Stichpunkt)" className="mt-2"><textarea className={ctl + " min-h-[60px] resize-y"} value={descVal} onChange={e => setProductDesc(areaKey, baseProd.id, e.target.value)} placeholder="leer = keine Stichpunkte" /></Field>
      </div>
    );
  };

  const renderCustomProductCard = (areaKey, areaLabel, c) => {
    const descVal = (c.desc || []).join("\n");
    const computed = c.priceOpen || c.mobile || c.sdwan;
    const priceKeys = PRICE_FIELDS.filter(f => f in c);
    return (
      <div key={`custom::${c.id}`} className="rounded-lg border bg-white p-3" style={{ borderColor: TEF_BLUE_2 }}>
        <div className="flex items-center justify-between gap-2 mb-2">
          <div className="text-[11px] font-semibold" style={{ color: TEF_BLUE }}>{areaLabel} <span className="text-gray-400 font-normal">· eigenes Produkt</span></div>
          <div className="flex items-center gap-2">
            <span className="text-[10px] font-bold px-2 py-0.5 rounded-full" style={{ color: "#fff", background: TEF_BLUE_2 }}>eigenes Produkt</span>
            <button onClick={() => duplicateCustomProduct(areaKey, c)} className="inline-flex items-center gap-1 text-[11px] font-medium px-2 py-1 rounded hover:bg-blue-50" style={{ color: TEF_BLUE }} title="Als eigene, bearbeitbare Kopie anlegen"><Copy size={12} /> duplizieren</button>
            <button onClick={() => deleteCustomProduct(areaKey, c.id, c.name)} className="inline-flex items-center gap-1 text-[11px] font-medium px-2 py-1 rounded hover:bg-red-50" style={{ color: TEF_RED }} title="Eigenes Produkt löschen"><Trash2 size={12} /> löschen</button>
          </div>
        </div>
        <Field label="Produktname"><input className={ctl} value={c.name} onChange={e => setCustomField(areaKey, c.id, "name", e.target.value)} /></Field>
        {!computed && priceKeys.length > 0 && <div className="mt-2 grid grid-cols-2 md:grid-cols-4 gap-2">
          {priceKeys.map(f => (
            <Field key={f} label={PRICE_LABELS[f] || f}><input type="number" step="0.01" className={ctl} value={c[f] ?? ""} onChange={e => setCustomField(areaKey, c.id, f, e.target.value)} /></Field>
          ))}
        </div>}
        {c.priceOpen && <div className="mt-2 text-[11px] text-amber-600 inline-flex items-center gap-1"><AlertTriangle size={12} /> Preis offen – wird im Kalkulator je Angebotszeile frei eingegeben.</div>}
        {computed && !c.priceOpen && <div className="mt-2 text-[11px] text-amber-600 inline-flex items-center gap-1"><AlertTriangle size={12} /> Preis wird über die Produktlogik berechnet (SD-WAN / Tarifoptionen) – Name & Beschreibung anpassbar.</div>}
        <div className="mt-2 flex items-center gap-4 flex-wrap">
          <label className="flex items-center gap-2 text-xs" style={{ color: TEF_GRAY }}><input type="checkbox" checked={!!c.qty} onChange={e => setCustomField(areaKey, c.id, "qty", e.target.checked)} />Mengenfeld anzeigen</label>
          {c.qty && <Field label="Bezeichnung Menge" className="w-40"><input className={ctl} value={c.qtyLabel || ""} onChange={e => setCustomField(areaKey, c.id, "qtyLabel", e.target.value)} /></Field>}
        </div>
        <Field label="Beschreibung (eine Zeile je Stichpunkt)" className="mt-2"><textarea className={ctl + " min-h-[60px] resize-y"} value={descVal} onChange={e => setCustomDesc(areaKey, c.id, e.target.value)} placeholder="leer = keine Stichpunkte" /></Field>
      </div>
    );
  };

  const renderAddForm = (areaKey) => {
    const open = addOpenAreas.has(areaKey);
    const draft = getDraft(areaKey);
    if (!open) return (
      <button onClick={() => toggleAddOpen(areaKey)} className="rounded-lg border-2 border-dashed flex items-center justify-center gap-2 text-sm font-semibold p-3 hover:bg-blue-50 min-h-[56px]" style={{ color: TEF_BLUE, borderColor: TEF_BLUE_2 }}><Plus size={16} /> Neues Produkt</button>
    );
    return (
      <div className="rounded-lg border-2 p-3" style={{ borderColor: TEF_BLUE_2, background: "#F7FAFF" }}>
        <div className="flex items-center justify-between gap-2 mb-2">
          <div className="text-[11px] font-semibold" style={{ color: TEF_BLUE }}>Neues Produkt</div>
          <button onClick={() => toggleAddOpen(areaKey)} className="text-[11px] font-medium px-2 py-1 rounded hover:bg-blue-100" style={{ color: TEF_BLUE }}>abbrechen</button>
        </div>
        <Field label="Produktname"><input className={ctl} value={draft.name} onChange={e => setDraftField(areaKey, "name", e.target.value)} placeholder="z. B. Standortvernetzung Premium" /></Field>
        <label className="flex items-center gap-2 text-xs mt-2" style={{ color: TEF_GRAY }}><input type="checkbox" checked={!!draft.priceOpen} onChange={e => setDraftField(areaKey, "priceOpen", e.target.checked)} />Preis offen (im Kalkulator je Angebotszeile frei eingeben statt fester Preise)</label>
        {!draft.priceOpen && <div className="mt-2 grid grid-cols-2 md:grid-cols-4 gap-2">
          {["m24", "m36", "o24", "o36"].map(f => (
            <Field key={f} label={PRICE_LABELS[f]}><input type="number" step="0.01" className={ctl} value={draft[f]} onChange={e => setDraftField(areaKey, f, e.target.value)} /></Field>
          ))}
        </div>}
        <div className="mt-2 flex items-center gap-4 flex-wrap">
          <label className="flex items-center gap-2 text-xs" style={{ color: TEF_GRAY }}><input type="checkbox" checked={!!draft.qty} onChange={e => setDraftField(areaKey, "qty", e.target.checked)} />Mengenfeld anzeigen</label>
          {draft.qty && <Field label="Bezeichnung Menge" className="w-40"><input className={ctl} value={draft.qtyLabel} onChange={e => setDraftField(areaKey, "qtyLabel", e.target.value)} placeholder="z. B. Lizenzen" /></Field>}
        </div>
        <Field label="Beschreibung (eine Zeile je Stichpunkt)" className="mt-2"><textarea className={ctl + " min-h-[60px] resize-y"} value={draft.desc} onChange={e => setDraftField(areaKey, "desc", e.target.value)} placeholder="leer = keine Stichpunkte" /></Field>
        <button onClick={() => submitNewProduct(areaKey)} disabled={!draft.name?.trim()} className="mt-3 inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-semibold disabled:opacity-40" style={{ background: TEF_BLUE, color: "#fff" }}><Plus size={16} /> Produkt anlegen</button>
      </div>
    );
  };

  const renderAreaDescCard = (areaKey, areaLabel) => {
    const cur = overrides.areaDesc?.[areaKey] || [];
    const set = cur.length > 0;
    return (
      <div className="rounded-lg border p-3 mb-3" style={{ borderColor: TEF_BLUE_2, background: "#F7FAFF" }}>
        <div className="flex items-center justify-between gap-2 mb-1.5">
          <div className="text-xs font-semibold" style={{ color: TEF_BLUE }}>Bereichsbeschreibung – {areaLabel}</div>
          {set && <button onClick={() => resetAreaDesc(areaKey)} className="inline-flex items-center gap-1 text-[11px] font-medium px-2 py-1 rounded hover:bg-blue-100" style={{ color: TEF_BLUE }}><RotateCcw size={12} /> leeren (Automatik)</button>}
        </div>
        <textarea className={ctl + " min-h-[60px] resize-y"} value={cur.join("\n")} onChange={e => setAreaDescText(areaKey, e.target.value)} placeholder={"leer = Automatik: gemeinsame Beschreibungszeilen aller Produkte dieses Bereichs werden automatisch in den Header gehoben"} />
        <div className="mt-1 text-[11px] text-gray-500">Wird im Angebot über „Produktbeschreibung einblenden" je Bereich angezeigt. Leer lassen = automatische Erkennung gemeinsamer Zeilen; eigener Text überschreibt die Automatik.</div>
      </div>
    );
  };

  // ===== VPN Connect: ein Anschluss (Haupt oder Backup) =====
  const renderVpnConn = (g, role) => {
    const c = g[role];
    if (!c) return null;
    const term = Number(g.vpnTerm) || 36;
    const isMain = role === "main";
    const accessOpts = isMain ? ["asym", "link", "mobile", "ipsec"] : (vpnBackupOptions(g.main).map(o => o.access));
    const allowedForAccess = isMain ? null : (vpnBackupOptions(g.main).find(o => o.access === c.access));
    const bwList = c.access === "asym" ? (allowedForAccess?.bw || VPN_ASYM_BW) : c.access === "link" ? (allowedForAccess?.bw || VPN_LINK_BW) : c.access === "ipsec" ? (allowedForAccess?.bw || VPN_IPSEC_BW) : [];
    const base = vpnAccessBase(c, term), svc = vpnService(c, term, role);
    const manual = c.access === "link" && (c.provider === "andere" || c.rabatt === "pricing");
    return (
      <div className="rounded-lg border p-3" style={{ borderColor: TEF_BLUE_2, background: isMain ? "#F7FAFF" : "#FFF7F0" }}>
        <div className="text-xs font-bold mb-2" style={{ color: TEF_BLUE }}>{isMain ? "Hauptanschluss" : "Backup-Anschluss"}</div>
        <div className="flex flex-wrap gap-3 items-end">
          <Field label="Anschlussart" className="w-44"><select className={ctl} value={c.access} onChange={e => setVpnAccess(g.id, role, e.target.value)}>{accessOpts.map(a => <option key={a} value={a}>{VPN_ACCESS_LABEL[a]}</option>)}</select></Field>
          {c.access !== "mobile" && <Field label="Bandbreite" className="w-36"><select className={ctl} value={c.bw} onChange={e => patchVpnConn(g.id, role, { bw: e.target.value })}>{bwList.map(b => <option key={b} value={b}>bis {b} Mbit/s</option>)}</select></Field>}
          {c.access === "link" && <>
            <Field label="Anbindung" className="w-32"><select className={ctl} value={c.provider} onChange={e => patchVpnConn(g.id, role, { provider: e.target.value })}><option value="dtag">DTAG</option><option value="andere">Andere</option></select></Field>
            {c.provider === "dtag" && <Field label="ONKZ" className="w-28"><input className={ctl} placeholder="z. B. 0711" value={c.onkz} onChange={e => patchVpnConn(g.id, role, { onkz: e.target.value })} /></Field>}
          </>}
          {c.access === "mobile" && isMain && <Field label="Datentarif" className="w-36"><select className={ctl} value={c.dataPack} onChange={e => patchVpnConn(g.id, role, { dataPack: e.target.value })}>{Object.keys(VPN_DATAPACK).map(k => <option key={k} value={k}>{VPN_DATAPACK_LABEL[k]}</option>)}</select></Field>}
          <Field label="Rabatt" className="w-32"><select className={ctl} value={c.rabatt} onChange={e => patchVpnConn(g.id, role, { rabatt: e.target.value })}>
            <option value="0">0 %</option>
            {c.access === "link" && <><option value="10">10 %</option><option value="20">20 %</option><option value="30">30 %</option></>}
            <option value="pricing">Pricing</option>
          </select></Field>
          {manual && <><Field label="Monatlich (€)" className="w-32"><input type="number" step="0.01" className={ctl} value={c.manMonthly} onChange={e => patchVpnConn(g.id, role, { manMonthly: e.target.value })} /></Field><Field label="Einmalig (€)" className="w-32"><input type="number" step="0.01" className={ctl} value={c.manOneoff} onChange={e => patchVpnConn(g.id, role, { manOneoff: e.target.value })} /></Field></>}
          {c.access === "mobile" && <Field label="Antenne (optional)" className="w-52"><select className={ctl} value={c.antenne} onChange={e => patchVpnConn(g.id, role, { antenne: e.target.value })}><option value="">— keine —</option>{Object.entries(VPN_ANTENNE).map(([k, v]) => <option key={k} value={k}>{v.label} ({eur(v.o)})</option>)}</select></Field>}
        </div>
        <div className="mt-2 flex items-center gap-4 flex-wrap">
          {c.access === "link" && <label className="flex items-center gap-2 text-sm" style={{ color: TEF_GRAY }}><input type="checkbox" checked={!!c.internet} onChange={e => patchVpnConn(g.id, role, { internet: e.target.checked })} />Option Internet</label>}
          {(c.access === "asym" || c.access === "link") && <label className="flex items-center gap-2 text-sm" style={{ color: TEF_GRAY }}><input type="checkbox" checked={!!c.qos} onChange={e => patchVpnConn(g.id, role, { qos: e.target.checked })} />Quality of Service</label>}
        </div>
        <div className="mt-2 text-xs text-gray-600">
          Access: <b style={{ color: TEF_BLUE }}>{eur(base.monthly)}</b>/Mon. {base.oneoff ? <>· {eur(base.oneoff)} einmalig </> : ""}
          {c.access === "link" && !manual && <>· Region <b>{base.region}</b>{!c.onkz && <span className="text-gray-400"> (ONKZ eingeben)</span>} </>}
          · {c.access === "mobile" ? <>Data-Pack <b style={{ color: TEF_BLUE }}>{eur(svc.svc)}</b> + MPLS {eur(svc.mpls)}</> : <>VPN-Service <b style={{ color: TEF_BLUE }}>{eur(svc.svc)}</b></>}
          {manual && <span className="text-amber-600"> · Preis manuell</span>}
        </div>
      </div>
    );
  };

  // ===== VPN Connect: kompletter Standort-Editor =====
  const renderVpnEditor = (g) => {
    const backupPossible = vpnBackupOptions(g.main).length > 0;
    const anyQos = (g.main?.qos && (g.main.access === "asym" || g.main.access === "link")) || (g.backup?.qos && (g.backup.access === "asym" || g.backup.access === "link"));
    return (
      <div className="space-y-3">
        <div className="rounded-lg border p-3 flex flex-wrap gap-3 items-end" style={{ borderColor: TEF_BLUE_2, background: "#EEF2FF" }}>
          <Field label="Standortname" className="grow min-w-[200px]"><input className={ctl} placeholder="z. B. Werk Kirchheim" value={g.vpnName || ""} onChange={e => patchGroup(g.id, { vpnName: e.target.value })} /></Field>
          <Field label="Laufzeit (bereichsweit)" className="w-44"><select className={ctl} value={g.vpnTerm || 36} onChange={e => setVpnTerm(Number(e.target.value))}>{VPN_TERMS.map(t => <option key={t} value={t}>{t} Monate</option>)}</select></Field>
        </div>
        {renderVpnConn(g, "main")}
        {g.backup ? <div className="space-y-2">
          {renderVpnConn(g, "backup")}
          <button onClick={() => toggleVpnBackup(g.id)} className="inline-flex items-center gap-1.5 text-[11px] font-medium px-2 py-1 rounded hover:bg-red-50" style={{ color: TEF_RED }}><Trash2 size={12} /> Backup-Anschluss entfernen</button>
        </div> : backupPossible ? (
          <button onClick={() => toggleVpnBackup(g.id)} style={{ color: TEF_BLUE_2, borderColor: TEF_BLUE_2 }} className="inline-flex items-center gap-2 px-3 py-1.5 rounded-lg border-2 border-dashed text-sm font-semibold hover:bg-blue-50"><Plus size={15} /> Backup-Anschluss hinzufügen</button>
        ) : <div className="text-xs text-gray-500 inline-flex items-center gap-1"><AlertTriangle size={12} /> Für die gewählte Hauptanschlussart ist laut Backup-Matrix kein Backup vorgesehen.</div>}
        <div className="rounded-lg border p-3" style={{ borderColor: "#e5e7eb" }}>
          <div className="text-xs font-bold mb-2" style={{ color: TEF_BLUE }}>Weitere Optionen (Standort)</div>
          <div className="flex flex-col gap-2">
            {g.backup && g.backup.access && <div className="flex items-center gap-3 flex-wrap">
              <label className="flex items-center gap-2 text-sm" style={{ color: TEF_GRAY }}><input type="checkbox" checked={!!g.backupPlus} onChange={e => patchGroup(g.id, { backupPlus: e.target.checked })} />Backup-Variante ++ (Redundanz über zwei Router, kostenfrei)</label>
              {g.backupPlus && <><Field label="Monatlich (€)" className="w-32"><input type="number" step="0.01" className={ctl} value={g.bpM} onChange={e => patchGroup(g.id, { bpM: e.target.value })} /></Field><Field label="Einmalig (€)" className="w-32"><input type="number" step="0.01" className={ctl} value={g.bpO} onChange={e => patchGroup(g.id, { bpO: e.target.value })} /></Field></>}
            </div>}
            <label className="flex items-center gap-2 text-sm" style={{ color: TEF_GRAY }}><input type="checkbox" checked={!!g.netflow} onChange={e => patchGroup(g.id, { netflow: e.target.checked })} />NetFlow ({eur(VPN_NETFLOW_O)} einmalig)</label>
            <label className={"flex items-center gap-2 text-sm " + (anyQos ? "" : "opacity-40")} style={{ color: TEF_GRAY }}><input type="checkbox" disabled={!anyQos} checked={!!g.citrix && anyQos} onChange={e => patchGroup(g.id, { citrix: e.target.checked })} />Citrix Priorisierung ({eur(VPN_CITRIX_O)} einmalig){!anyQos && <span className="text-[11px] text-gray-400">– nur mit Quality of Service an einem Anschluss</span>}</label>
            <div className="flex items-center gap-3 flex-wrap">
              <label className="flex items-center gap-2 text-sm" style={{ color: TEF_GRAY }}><input type="checkbox" checked={!!g.advHw} onChange={e => patchGroup(g.id, { advHw: e.target.checked })} />Advanced Hardware (Aufpreis manuell)</label>
              {g.advHw && <><Field label="Monatlich (€)" className="w-32"><input type="number" step="0.01" className={ctl} value={g.advHwM} onChange={e => patchGroup(g.id, { advHwM: e.target.value })} /></Field><Field label="Einmalig (€)" className="w-32"><input type="number" step="0.01" className={ctl} value={g.advHwO} onChange={e => patchGroup(g.id, { advHwO: e.target.value })} /></Field></>}
            </div>
          </div>
        </div>
      </div>
    );
  };

  // ===== Standort-Import (CSV/Text, eine Spalte) =====
  const parseImportText = (text) => {
    const lines = (text || "").split(/\r?\n/);
    let names = lines.map(l => (l.split(/[;,\t]/)[0] || "").trim()).filter(Boolean);
    if (names.length && /^(standort|standorte|name|site|sites|filiale|filialen|location|standortname)$/i.test(names[0])) names = names.slice(1);
    return names;
  };
  const importSites = parseImportText(importText);
  const onImportFile = (e) => { const f = e.target.files?.[0]; if (!f) return; const rd = new FileReader(); rd.onload = () => { setImportText(String(rd.result || "")); setImportMsg(""); }; rd.readAsText(f); e.target.value = ""; };
  const applySiteName = (g, site) => {
    if (g.area === "vpnconnect") { g.vpnName = site; return; }
    if (g.area === "standort") { g.standortName = site; (g.subGroups || []).forEach(sg => applySiteName(sg, site)); return; }
    (g.rows || []).forEach(r => { r.standort = site; });
  };
  const doImportSites = () => {
    const sites = importSites;
    if (!sites.length) { setImportMsg("Keine Standorte erkannt – bitte eine Liste einfügen oder Datei hochladen (ein Standort je Zeile)."); return; }
    const template = groups.filter(g => g.area);
    let result = [];
    if (importAsStandort) {
      let subTemplate = [];
      for (const g of template) { if (g.area === "standort") subTemplate.push(...(g.subGroups || [])); else subTemplate.push(g); }
      subTemplate = subTemplate.filter(sg => sg.area);
      for (const site of sites) {
        const subs = subTemplate.length
          ? subTemplate.map(t => { const c = reidGroup(JSON.parse(JSON.stringify({ ...t, standortName: "" }))); return c; })
          : [newGroup()];
        result.push({ ...newGroup(), area: "standort", rows: [], standortName: site, subGroups: subs });
      }
    } else {
      if (!template.length) { setImportMsg("Für den Modus Vervielfachen bitte zuerst im Kalkulator ein Produkt anlegen, das je Standort kopiert werden soll."); return; }
      for (const site of sites) for (const t of template) { const c = reidGroup(JSON.parse(JSON.stringify(t))); applySiteName(c, site); result.push(c); }
    }
    if (!result.length) result = [newGroup()];
    setGroups(result);
    setImportMsg(`${sites.length} Standort(e) angelegt${importAsStandort ? " (als Standortangebote)" : " (vervielfacht)"}.`);
    setTab("calc");
  };

  const renderImport = () => {
    const tplCount = groups.filter(g => g.area).length;
    return (
      <div className="bg-white rounded-b-xl shadow-sm p-5 md:p-7">
        <h2 style={{ color: TEF_BLUE }} className="font-bold text-lg mb-1">Standorte importieren</h2>
        <p className="text-sm text-gray-600 mb-5 max-w-3xl">Lade eine CSV-/Text-Datei mit <b>einer Spalte</b> hoch (ein Standort je Zeile) oder füge die Liste direkt ein. Die im Reiter „Kalkulator" angelegte Konfiguration dient als <b>Vorlage</b> und wird je Standort vervielfältigt.</p>
        <div className="grid grid-cols-1 lg:grid-cols-2 gap-5">
          <div>
            <label style={{ color: TEF_BLUE }} className={lbl}>Datei (CSV / TXT / TSV)</label>
            <input type="file" accept=".csv,.txt,.tsv,text/csv,text/plain" onChange={onImportFile} className="block w-full text-sm mb-3 file:mr-3 file:px-3 file:py-1.5 file:rounded file:border-0 file:text-white file:text-sm file:cursor-pointer" style={{ }} />
            <label style={{ color: TEF_BLUE }} className={lbl}>… oder Liste einfügen (ein Standort je Zeile)</label>
            <textarea className={ctl + " min-h-[160px] resize-y font-mono text-sm"} placeholder={"Werk Kirchheim\nFiliale Stuttgart\nLogistik Ulm"} value={importText} onChange={e => { setImportText(e.target.value); setImportMsg(""); }} />
          </div>
          <div>
            <label style={{ color: TEF_BLUE }} className={lbl}>Erkannte Standorte ({importSites.length})</label>
            <div className="rounded-lg border border-gray-200 p-2 min-h-[160px] max-h-[260px] overflow-auto bg-gray-50">
              {importSites.length === 0 ? <p className="text-sm text-gray-400 p-2">Noch keine Standorte erkannt.</p>
                : <ol className="list-decimal list-inside text-sm space-y-0.5">{importSites.map((s, i) => <li key={i} style={{ color: TEF_GRAY }}>{s}</li>)}</ol>}
            </div>
          </div>
        </div>

        <div className="mt-5 rounded-lg border p-4" style={{ borderColor: TEF_BLUE_2, background: "#EEF2FF" }}>
          <label className="flex items-center gap-2 text-sm font-semibold mb-2" style={{ color: TEF_GRAY }}>
            <input type="checkbox" checked={importAsStandort} onChange={e => setImportAsStandort(e.target.checked)} />
            Als Standortangebote anlegen
          </label>
          {importAsStandort
            ? <p className="text-xs text-gray-600">Je Standort wird ein <b>Standortangebot</b> mit dem Standortnamen angelegt. Die aktuellen Bereiche der Vorlage ({tplCount}) werden als <b>Unterbereiche</b> in jedes Standortangebot kopiert. Ideal für komplexe Vernetzungs-Konstrukte (z. B. je Standort All&nbsp;IP + SD-WAN + VPN-Backup).</p>
            : <p className="text-xs text-gray-600">Die aktuellen Bereiche/Produkte der Vorlage ({tplCount}) werden je Standort <b>kopiert</b>; der Standortname wird in das <b>Standort-Feld</b> jeder Zeile (bzw. den Namen bei VPN) eingetragen. Ideal, um <b>ein</b> Produkt mehrfach auszuweisen (z. B. 6× All&nbsp;IP Business mit Mobilfunk-Backup).</p>}
          <p className="text-[11px] text-amber-700 mt-2"><b>Achtung:</b> Der Import <b>ersetzt</b> die aktuelle Angebotsstruktur durch die vervielfältigte Vorlage.</p>
        </div>

        {tplCount === 0 && <div className="mt-3 text-xs text-amber-700 inline-flex items-center gap-1"><AlertTriangle size={13} /> Aktuell ist keine Vorlage angelegt. {importAsStandort ? "Es werden leere Standortangebote erstellt, die du danach befüllst." : "Bitte zuerst im Kalkulator ein Produkt anlegen."}</div>}

        <div className="mt-5 flex items-center gap-3">
          <button onClick={doImportSites} disabled={importSites.length === 0} style={{ background: TEF_BLUE }} className="inline-flex items-center gap-2 px-5 py-2.5 rounded-lg text-white text-sm font-semibold disabled:opacity-40"><Upload size={16} /> {importSites.length || 0} Standort(e) anlegen</button>
          {importMsg && <span className="text-sm" style={{ color: TEF_BLUE }}>{importMsg}</span>}
        </div>
      </div>
    );
  };

  const renderEditor = () => {
    const showOpts = editArea === "all" || editArea === "opts";
    const visibleAreas = editAreas.filter(a => editArea === "all" || editArea === a.key);
    const optMatches = OPT_SCHEMA.filter(o => !q || o.label.toLowerCase().includes(q) || o.key.toLowerCase().includes(q));
    const hasFilter = !!q || editChangedOnly || editEmptyOnly;
    let productCount = 0;
    const areaBlocks = visibleAreas.map(a => {
      const prods = (BASE_CATALOG[a.key].products || []).filter(p => {
        if (p.isSeparator) return false;
        if (editChangedOnly && !productOverridden(`${a.key}::${p.id}`) && !isDeleted(a.key, p.id)) return false;
        if (editEmptyOnly && !isDescEmpty(a.key, p)) return false;
        if (!q) return true;
        return p.name.toLowerCase().includes(q) || p.id.toLowerCase().includes(q) || a.label.toLowerCase().includes(q);
      });
      const customs = (overrides.custom || []).filter(c => {
        if (c.area !== a.key) return false;
        if (editEmptyOnly && (c.desc || []).length > 0) return false;
        if (!q) return true;
        return (c.name || "").toLowerCase().includes(q) || a.label.toLowerCase().includes(q);
      });
      productCount += prods.length + customs.length;
      if (!prods.length && !customs.length && hasFilter) return null;
      return (
        <div key={a.key} className="mb-5">
          <h3 className="text-sm font-bold mb-2 sticky top-0 py-1" style={{ color: TEF_BLUE, background: "#fff" }}>{a.label} <span className="text-gray-400 font-normal">({prods.length + customs.length})</span></h3>
          {!hasFilter && renderAreaDescCard(a.key, a.label)}
          <div className="grid grid-cols-1 lg:grid-cols-2 gap-3">
            {prods.map(p => renderProductCard(a.key, a.label, p))}
            {customs.map(c => renderCustomProductCard(a.key, a.label, c))}
            {!hasFilter && renderAddForm(a.key)}
          </div>
        </div>
      );
    });
    const optsChanged = editChangedOnly ? optMatches.filter(o => o.key in overrides.opts) : optMatches;

    return (
      <div className="bg-white rounded-b-xl shadow-sm p-5 md:p-7">
        <div className="flex items-start justify-between gap-3 flex-wrap mb-4">
          <div>
            <h2 style={{ color: TEF_BLUE }} className="font-bold text-lg">Produkte &amp; Texte bearbeiten</h2>
            <p className="text-xs text-gray-500 mt-0.5">Änderungen gelten dauerhaft für diesen Kalkulator und werden automatisch gespeichert. Preisänderungen wirken sofort – auch auf bereits platzierte Produkte.</p>
          </div>
          <div className="flex items-center gap-2 flex-wrap">
            <button onClick={() => cfgRef.current?.click()} className="inline-flex items-center gap-1.5 px-3 py-2 rounded-lg border text-xs font-semibold hover:bg-blue-50" style={{ color: TEF_BLUE, borderColor: TEF_BLUE }}><Upload size={14} /> Konfig importieren</button>
            <button onClick={exportConfig} className="inline-flex items-center gap-1.5 px-3 py-2 rounded-lg border text-xs font-semibold hover:bg-blue-50" style={{ color: TEF_BLUE, borderColor: TEF_BLUE }}><Download size={14} /> Konfig exportieren</button>
            <button onClick={resetAllOverrides} className="inline-flex items-center gap-1.5 px-3 py-2 rounded-lg border text-xs font-semibold hover:bg-red-50" style={{ color: TEF_RED, borderColor: TEF_RED }}><RotateCcw size={14} /> Alles zurücksetzen</button>
          </div>
        </div>
        {cfgStatus && <div className="mb-3 text-xs font-medium" style={{ color: TEF_BLUE }}>{cfgStatus}</div>}

        <div className="flex items-end gap-3 flex-wrap mb-5 p-3 rounded-lg" style={{ background: "#EEF2FF" }}>
          <Field label="Suche" className="grow min-w-[220px]">
            <div className="relative">
              <Search size={15} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-gray-400" />
              <input className={ctl + " pl-8"} placeholder="Produktname, ID oder Optionstext …" value={editSearch} onChange={e => setEditSearch(e.target.value)} />
            </div>
          </Field>
          <Field label="Bereich" className="w-56">
            <select className={ctl} value={editArea} onChange={e => setEditArea(e.target.value)}>
              <option value="all">Alle Bereiche</option>
              {editAreas.map(a => <option key={a.key} value={a.key}>{a.label}</option>)}
              <option value="vpnconnect">VPN Connect</option>
              <option value="opts">Nur Optionen &amp; Tarife</option>
            </select>
          </Field>
          <label className="flex items-center gap-2 text-sm mb-2" style={{ color: TEF_GRAY }}><input type="checkbox" checked={editChangedOnly} onChange={e => setEditChangedOnly(e.target.checked)} />nur geänderte</label>
          <label className="flex items-center gap-2 text-sm mb-2" style={{ color: TEF_GRAY }}><input type="checkbox" checked={editEmptyOnly} onChange={e => setEditEmptyOnly(e.target.checked)} />nur leere</label>
        </div>

        {editArea !== "opts" && editArea !== "vpnconnect" && (productCount === 0
          ? <p className="text-sm text-gray-400 mb-6">Keine Produkte für diese Suche/Filter.</p>
          : areaBlocks)}

        {(editArea === "all" || editArea === "vpnconnect") && !hasFilter && (() => {
          const vd = resolveVpnDesc(overrides.vpnDesc);
          const slot = (key, label) => (
            <div key={key} className="rounded-lg border bg-white p-3" style={{ borderColor: TEF_BLUE_2 }}>
              <div className="text-[11px] font-semibold mb-1" style={{ color: TEF_BLUE }}>{label}</div>
              <textarea className={ctl + " min-h-[60px] resize-y"} value={(vd[key] || []).join("\n")} onChange={e => setVpnDescField(key, e.target.value)} placeholder="eine Zeile je Stichpunkt · leer = keine" />
            </div>
          );
          return (
            <div className="mb-5">
              <h3 className="text-sm font-bold mb-2 pt-2 border-t flex items-center justify-between" style={{ color: TEF_BLUE }}>
                <span>VPN Connect – Beschreibungen</span>
                <button onClick={resetVpnDesc} className="inline-flex items-center gap-1 text-[11px] font-medium px-2 py-1 rounded hover:bg-blue-50" style={{ color: TEF_BLUE }}><RotateCcw size={12} /> zurücksetzen</button>
              </h3>
              <p className="text-[11px] text-gray-500 mb-2">Bereichsweite Texte erscheinen als Überschrift jedes VPN-Standorts; je Zugangsart erscheinen sie an der jeweiligen Anschlusszeile im Angebot.</p>
              <div className="grid grid-cols-1 lg:grid-cols-2 gap-3">
                {slot("area", "Bereichsweit (z. B. Hardware inklusive, Gemanaged durch Telefónica)")}
                {slot("asym", "Asymmetrisch")}
                {slot("link", "Link Symmetrisch")}
                {slot("mobile", "Mobilfunk")}
                {slot("ipsec", "IPSec")}
              </div>
            </div>
          );
        })()}

        {showOpts && optsChanged.length > 0 && <div className="mb-2">
          <h3 className="text-sm font-bold mb-2 pt-2 border-t" style={{ color: TEF_BLUE }}>Optionen &amp; Tarife</h3>
          <div className="grid grid-cols-1 lg:grid-cols-2 gap-3">
            {optsChanged.map(o => {
              const isLines = o.type === "lines";
              const cur = o.key in overrides.opts ? overrides.opts[o.key] : OPT_DEFAULTS[o.key];
              const changed = o.key in overrides.opts;
              return (
                <div key={o.key} className="rounded-lg border bg-white p-3" style={{ borderColor: changed ? TEF_BLUE_2 : "#e5e7eb" }}>
                  <div className="flex items-center justify-between gap-2 mb-1.5">
                    <div className="text-xs font-semibold" style={{ color: TEF_BLUE }}>{o.label}</div>
                    <div className="flex items-center gap-2">
                      {changed && <span className="text-[10px] font-bold px-2 py-0.5 rounded-full" style={{ color: "#fff", background: TEF_BLUE_2 }}>geändert</span>}
                      {changed && <button onClick={() => resetOpt(o.key)} className="inline-flex items-center gap-1 text-[11px] font-medium px-2 py-1 rounded hover:bg-blue-50" style={{ color: TEF_BLUE }}><RotateCcw size={12} /> zurücksetzen</button>}
                    </div>
                  </div>
                  {isLines
                    ? <textarea className={ctl + " min-h-[80px] resize-y"} value={(cur || []).join("\n")} onChange={e => setOptField(o.key, e.target.value.split("\n"))} />
                    : <input className={ctl} value={cur || ""} onChange={e => setOptField(o.key, e.target.value)} />}
                </div>
              );
            })}
          </div>
        </div>}
      </div>
    );
  };

  return (
    <div style={{ background: TEF_BG }} className="min-h-screen p-4 md:p-8">
      <input ref={fileRef} type="file" accept=".json,application/json" onChange={onLoadFile} style={{ display: "none" }} />
      <input ref={cfgRef} type="file" accept=".json,application/json" onChange={importConfig} style={{ display: "none" }} />
      <div className="max-w-6xl mx-auto">
        <div style={{ background: TEF_BLUE }} className="rounded-t-xl px-6 py-5 flex items-center justify-between gap-3 flex-wrap">
          <div className="flex items-center gap-3">
            <div style={{ background: "#fff" }} className="w-10 h-10 rounded-full flex items-center justify-center"><span style={{ color: TEF_BLUE }} className="font-black text-lg">T</span></div>
            <div><div className="text-white font-bold text-lg leading-tight">Telefónica</div><div style={{ color: TEF_CYAN }} className="text-xs font-medium">B2B Angebots-Kalkulator</div></div>
          </div>
          {tab === "calc" && <div className="flex items-center gap-2">
            <button onClick={() => fileRef.current?.click()} className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-white/15 text-white text-sm font-semibold hover:bg-white/25"><FolderOpen size={16} /> Öffnen</button>
            <button onClick={restoreAutoSave} title="Letzten automatisch gesicherten Stand laden (alle 60 Sek. im Hintergrund gesichert)" className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-white/15 text-white text-sm font-semibold hover:bg-white/25"><RotateCcw size={16} /> Letzten Stand wiederherstellen</button>
            <button onClick={saveOffer} className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-white text-sm font-semibold" style={{ color: TEF_BLUE }}><Save size={16} /> Speichern</button>
            <button onClick={resetOffer} title="Kunde, alle Bereiche und Produkte entfernen" className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-white/15 text-white text-sm font-semibold hover:bg-red-500/40"><Trash2 size={16} /> Leeren</button>
            {lastAutoSave && <span className="text-[11px] text-white/70 hidden lg:inline">Auto-Save {lastAutoSave.toLocaleTimeString("de-DE")}</span>}
          </div>}
        </div>

        {/* ===== TAB-LEISTE ===== */}
        <div className="bg-white border-b border-gray-200 flex items-stretch">
          <button onClick={() => setTab("calc")} className="inline-flex items-center gap-2 px-5 py-3 text-sm font-semibold border-b-2" style={{ color: tab === "calc" ? TEF_BLUE : "#6b7280", borderColor: tab === "calc" ? TEF_BLUE : "transparent" }}><Calculator size={16} /> Kalkulator</button>
          <button onClick={() => setTab("edit")} className="inline-flex items-center gap-2 px-5 py-3 text-sm font-semibold border-b-2" style={{ color: tab === "edit" ? TEF_BLUE : "#6b7280", borderColor: tab === "edit" ? TEF_BLUE : "transparent" }}><Settings size={16} /> Produkte &amp; Texte bearbeiten</button>
          <button onClick={() => setTab("import")} className="inline-flex items-center gap-2 px-5 py-3 text-sm font-semibold border-b-2" style={{ color: tab === "import" ? TEF_BLUE : "#6b7280", borderColor: tab === "import" ? TEF_BLUE : "transparent" }}><Upload size={16} /> Standorte importieren</button>
        </div>

        {tab === "edit" ? renderEditor() : tab === "import" ? renderImport() : <>
        <div className="bg-white rounded-b-xl shadow-sm p-5 md:p-7">
          <div className="mb-6 flex flex-wrap gap-4 items-end">
            <div className="grow max-w-md min-w-[240px]">
              <label style={{ color: TEF_BLUE }} className={lbl}>Kunde / Angebotsempfänger</label>
              <input className={ctl} placeholder="z. B. Ramsperger Automobile GmbH" value={customer} onChange={e => setCustomer(e.target.value)} />
            </div>
            <div className="w-56">
              <label style={{ color: TEF_BLUE }} className={lbl}>Laufzeit (alle Produkte setzen)</label>
              <select className={ctl} value="" onChange={e => { const v = Number(e.target.value); if (v) setAllTerms(v); }}>
                <option value="">— wählen —</option>
                {[12, 24, 36, 48, 60].map(t => <option key={t} value={t}>{t} Monate für alle</option>)}
              </select>
            </div>
          </div>
          <div className="space-y-4">
            {groups.map((g, gi) => {
              const kinds = new Set();
              for (const r of g.rows) { const pp = getProduct(g.area, r.productId); if (pp?.mdmKind) kinds.add(pp.mdmKind); }
              const mdmMix = kinds.has("managed") && kinds.has("unmanaged");
              return (
                <div key={g.id} className="rounded-xl border-2 border-gray-200 overflow-hidden">
                  <div className="flex flex-col md:flex-row">
                    <div className="md:w-44 shrink-0 p-3 border-b md:border-b-0 md:border-r border-gray-200" style={{ background: "#EEF2FF" }}>
                      <div className="flex items-center justify-between mb-2">
                        <span className="text-[11px] font-bold inline-flex items-center gap-1" style={{ color: TEF_BLUE }}>
                          Bereich {gi + 1}
                          {(() => { const n = plausibility.filter(x => x.gid === g.id).length; const hasWarn = plausibility.some(x => x.gid === g.id && x.level === "warn"); return n > 0 ? <span title={`${n} Hinweis(e) – siehe Plausibilitäts-Prüfung im Angebot`} className="inline-flex items-center justify-center rounded-full text-white text-[10px] font-bold w-4 h-4" style={{ background: hasWarn ? TEF_RED : TEF_BLUE }}>{n}</span> : null; })()}
                        </span>
                        <div className="flex items-center gap-0.5">
                          <button onClick={() => moveGroupDir(g.id, -1)} disabled={gi <= 0} className="p-1 rounded hover:bg-white disabled:opacity-25" title="Bereich nach oben"><ChevronUp size={16} color={TEF_BLUE} /></button>
                          <button onClick={() => moveGroupDir(g.id, 1)} disabled={gi >= groups.length - 1} className="p-1 rounded hover:bg-white disabled:opacity-25" title="Bereich nach unten"><ChevronDown size={16} color={TEF_BLUE} /></button>
                        </div>
                      </div>
                      <label className={lbl} style={{ color: TEF_BLUE }}>Produktbereich</label>
                      <select className={ctl} value={g.area} onChange={e => setArea(g.id, e.target.value)}>
                        <option value="">— wählen —</option>
                        {AREAS.map(a => <option key={a.key} value={a.key}>{a.label}</option>)}
                      </select>
                      <div className="mt-3 flex items-center gap-2">
                        <button onClick={() => duplicateGroup(g.id)} className="inline-flex items-center gap-1.5 text-xs font-medium px-2 py-1 rounded hover:bg-blue-50" style={{ color: TEF_BLUE }} title="Bereich mit allen Produkten duplizieren"><Copy size={14} /> duplizieren</button>
                        <button onClick={() => removeGroup(g.id)} className="inline-flex items-center gap-1.5 text-xs font-medium px-2 py-1 rounded hover:bg-red-50" style={{ color: TEF_RED }}><Trash2 size={14} /> entfernen</button>
                      </div>
                      <div className="mt-2 flex items-center gap-2 flex-wrap">
                        <button onClick={() => saveBaseline(g)} className="inline-flex items-center gap-1.5 text-[11px] font-medium px-2 py-1 rounded hover:bg-blue-50" style={{ color: TEF_BLUE }} title="Aktuellen Stand als Vergleichsbasis sichern (z. B. vor Nachverhandlung)"><Bookmark size={12} /> Status sichern</button>
                        {baselines[g.id] && <>
                          <button onClick={() => toggleDiff(g.id)} className="inline-flex items-center gap-1.5 text-[11px] font-medium px-2 py-1 rounded hover:bg-blue-50" style={{ color: TEF_BLUE }}><GitCompare size={12} /> {diffOpen[g.id] ? "Vergleich ausblenden" : "Vergleich anzeigen"}</button>
                          <button onClick={() => clearBaseline(g.id)} className="text-[11px] text-gray-400 hover:text-gray-600" title="Gesicherten Vergleichsstand verwerfen">Baseline löschen</button>
                        </>}
                      </div>
                    </div>
                    <div className="flex-1 min-w-0 p-4 space-y-3 bg-gray-50">
                      {mdmMix && <div className="flex items-start gap-2 text-sm rounded p-2.5 border" style={{ color: TEF_RED, borderColor: TEF_RED, background: "#fdecea" }}><AlertTriangle size={16} className="mt-0.5 shrink-0" />Kein Mix aus Managed und Unmanaged möglich – bitte nur eine Variante wählen.</div>}
                      {diffOpen[g.id] && baselines[g.id] && (() => {
                        const d = buildGroupDiff(baselines[g.id], g);
                        if (!d) return null;
                        const nothing = (d.kind === "rows" && !d.added.length && !d.removed.length && !d.changed.length) || (d.kind === "standort" && !d.subDiffs.length) || (d.kind === "vpn" && !d.changes.length);
                        return (
                          <div className="rounded-lg border p-3" style={{ borderColor: TEF_BLUE_2, background: "#F7FAFF" }}>
                            <div className="text-xs font-bold mb-2 flex items-center gap-1.5" style={{ color: TEF_BLUE }}><GitCompare size={13} /> Vergleich zum gesicherten Stand</div>
                            {nothing && <div className="text-xs text-gray-400">Keine Änderungen seit der letzten Sicherung.</div>}
                            {d.kind === "rows" && <div className="space-y-1">
                              {d.added.map((a, i) => <div key={"a" + i} className="text-xs" style={{ color: "#15803d" }}>+ Hinzugefügt: {a.name}</div>)}
                              {d.removed.map((a, i) => <div key={"r" + i} className="text-xs" style={{ color: TEF_RED }}>− Entfernt: {a.name}</div>)}
                              {d.changed.map((c, i) => <div key={"c" + i} className="text-xs" style={{ color: TEF_GRAY }}>✎ {c.name}{c.changes.length ? ": " + c.changes.join(" · ") : ""}</div>)}
                            </div>}
                            {d.kind === "standort" && <div className="space-y-1.5">
                              {d.subDiffs.map((s, i) => (
                                <div key={i} className="text-xs">
                                  <span className="font-semibold" style={{ color: TEF_BLUE }}>{s.label}:</span>{" "}
                                  {s.addedSub ? <span style={{ color: "#15803d" }}>neuer Unterbereich</span> : s.removedSub ? <span style={{ color: TEF_RED }}>Unterbereich entfernt</span> : <>
                                    {(s.added || []).map((a, j) => <span key={"a" + j} className="mr-2" style={{ color: "#15803d" }}>+ {a.name}</span>)}
                                    {(s.removed || []).map((a, j) => <span key={"r" + j} className="mr-2" style={{ color: TEF_RED }}>− {a.name}</span>)}
                                    {(s.changed || []).map((c, j) => <span key={"c" + j} className="mr-2" style={{ color: TEF_GRAY }}>✎ {c.name}{c.changes.length ? " (" + c.changes.join(" · ") + ")" : ""}</span>)}
                                  </>}
                                </div>
                              ))}
                            </div>}
                            {d.kind === "vpn" && <div className="space-y-1">{d.changes.map((c, i) => <div key={i} className="text-xs" style={{ color: TEF_GRAY }}>✎ {c}</div>)}</div>}
                          </div>
                        );
                      })()}
                      {g.area === "office365" && <div className="rounded-lg border p-3" style={{ borderColor: TEF_BLUE_2, background: "#EEF2FF" }}>
                        <label className="flex items-center gap-2 text-sm font-medium" style={{ color: TEF_GRAY }}><input type="checkbox" checked={!!g.managed} onChange={e => patchGroup(g.id, { managed: e.target.checked })} />Managed (Verwaltung durch Telefónica)</label>
                        <div className="text-[11px] text-gray-500 mt-1">Steuert Preise & max. Rabatt aller Lizenzen. Ohne Managed-Preis wird License-Only-Preis genutzt.</div>
                      </div>}
                      {(g.area === "sdwan" || g.area === "sdwanmeraki") && <div className="rounded-lg border p-3" style={{ borderColor: TEF_BLUE_2, background: "#EEF2FF" }}>
                        <label className={lbl} style={{ color: TEF_BLUE }}>Standard-Laufzeit (Vorauswahl neuer Produkte)</label>
                        <select className={ctl} value={g.sdwanTerm || 60} onChange={e => patchGroup(g.id, { sdwanTerm: Number(e.target.value) })}>{SDWAN_TERMS.map(t => <option key={t} value={t}>{t} Monate</option>)}</select>
                        <div className="text-[11px] text-gray-500 mt-1">Neu hinzugefügte Produkte starten mit dieser Laufzeit. Je Zeile weiterhin einzeln änderbar.</div>
                      </div>}
                      {g.area === "individuell" && <div className="rounded-lg border p-3" style={{ borderColor: TEF_BLUE_2, background: "#EEF2FF" }}>
                        <label className={lbl} style={{ color: TEF_BLUE }}>Produktbereich (frei)</label>
                        <input className={ctl} placeholder="z. B. Professional Services" value={g.customArea || ""} onChange={e => patchGroup(g.id, { customArea: e.target.value })} />
                        <div className="text-[11px] text-gray-500 mt-1">Erscheint im Angebot als Überschrift dieses Bereichs.</div>
                      </div>}
                      {g.area === "standort" && <div className="rounded-lg border p-3" style={{ borderColor: TEF_BLUE_2, background: "#EEF2FF" }}>
                        <label className={lbl} style={{ color: TEF_BLUE }}>Standortname</label>
                        <input className={ctl} placeholder="z. B. Kirchheim" value={g.standortName || ""} onChange={e => patchGroup(g.id, { standortName: e.target.value })} />
                        <div className="text-[11px] text-gray-500 mt-1">Erscheint im Angebot als gemeinsame Überschrift über allen Unterbereichen.</div>
                      </div>}
                      {g.area === "vpnconnect" ? renderVpnEditor(g) : g.area === "standort" ? <div className="space-y-3">
                        {(g.subGroups || []).map((sg, sgi) => renderSubGroupCard(g, sg, sgi, (g.subGroups || []).length))}
                        <button onClick={() => addSubGroup(g.id)} style={{ color: TEF_BLUE_2, borderColor: TEF_BLUE_2 }} className="inline-flex items-center gap-2 px-3 py-1.5 rounded-lg border-2 border-dashed text-sm font-semibold hover:bg-blue-50"><Plus size={15} /> Unterbereich hinzufügen</button>
                      </div> : !g.area ? <p className="text-sm text-gray-400">Bitte links zuerst einen Produktbereich wählen.</p> : <>
                        {g.rows.map(r => renderRow(g, r))}
                        <button onClick={() => addRow(g.id)} style={{ color: TEF_BLUE_2, borderColor: TEF_BLUE_2 }} className="inline-flex items-center gap-2 px-3 py-1.5 rounded-lg border-2 border-dashed text-sm font-semibold hover:bg-blue-50"><Plus size={15} /> Produkt hinzufügen</button>
                      </>}
                    </div>
                  </div>
                </div>
              );
            })}
          </div>
          <button onClick={addGroup} style={{ background: TEF_BLUE }} className="mt-4 inline-flex items-center gap-2 px-5 py-2.5 rounded-lg text-white text-sm font-semibold hover:opacity-90"><Plus size={16} /> Produktbereich hinzufügen</button>
        </div>

        {/* ===== VORSCHAU ===== */}
        <div className="bg-white rounded-xl shadow-sm p-5 md:p-7 mt-6">
          <div className="flex items-center justify-between mb-4 flex-wrap gap-2">
            <h2 style={{ color: TEF_BLUE }} className="font-bold text-lg">Angebot (so wird es eingefügt)</h2>
            <div className="flex items-center gap-2">
              <button onClick={saveOffer} className="inline-flex items-center gap-2 px-4 py-2.5 rounded-lg border-2 text-sm font-semibold hover:bg-blue-50" style={{ color: TEF_BLUE, borderColor: TEF_BLUE }}><Save size={16} /> Speichern</button>
              <button onClick={copyOffer} disabled={offerLines.length === 0} style={{ background: copied ? TEF_CYAN : TEF_BLUE }} className="inline-flex items-center gap-2 px-5 py-2.5 rounded-lg text-white text-sm font-semibold disabled:opacity-40">
                {copied ? <Check size={16} /> : <Copy size={16} />}{copied ? "Kopiert!" : "Für PowerPoint kopieren"}
              </button>
            </div>
          </div>
          {plausibility.length > 0 && <div className="mb-4 rounded-lg border p-3" style={{ borderColor: plausibility.some(x => x.level === "warn") ? TEF_RED : TEF_BLUE_2, background: plausibility.some(x => x.level === "warn") ? "#FFF5F5" : "#EEF2FF" }}>
            <div className="flex items-center gap-2 mb-2 text-sm font-bold" style={{ color: plausibility.some(x => x.level === "warn") ? TEF_RED : TEF_BLUE }}><AlertTriangle size={16} /> Plausibilitäts-Prüfung ({plausibility.length})</div>
            <ul className="space-y-1">
              {plausibility.map((x, i) => <li key={i} className="flex items-start gap-2 text-xs" style={{ color: TEF_GRAY }}>
                <span className="mt-0.5 inline-block w-2 h-2 rounded-full shrink-0" style={{ background: x.level === "warn" ? TEF_RED : TEF_BLUE }} />
                <span>{x.msg}</span>
              </li>)}
            </ul>
            <div className="text-[11px] text-gray-400 mt-2">Hinweise erscheinen nicht im Export.</div>
          </div>}
          {offerLines.length === 0 ? <p className="text-sm text-gray-400">Noch kein Produkt ausgewählt.</p> : (
            <div className="overflow-x-auto">
              <p style={{ color: TEF_BLUE }} className="font-bold mb-1">Angebot{customer ? " – " + customer : ""}</p>
              <p className="text-xs text-gray-500 mb-3">Stand: {today} · ▲▼ zum Sortieren · Mülleimer zum Löschen (nicht im Export)</p>
              <style>{`.tef-offer-table th, .tef-offer-table td { border-color: #d1d5db !important; }`}</style>
              <table className="tef-offer-table w-full border-collapse text-sm">
                <thead>
                  <tr>
                    <th style={{ background: TEF_BLUE, width: 36 }} className="border border-gray-300"></th>
                    <th style={{ background: TEF_BLUE }} className="text-white font-semibold px-3 py-2 border border-gray-300 text-left">Produkt</th>
                    <th style={{ background: TEF_BLUE }} className="text-white font-semibold px-3 py-2 border border-gray-300 text-left">Beschreibung</th>
                    <th style={{ background: TEF_BLUE }} className="text-white font-semibold px-3 py-2 border border-gray-300 text-right">Monatlich</th>
                    <th style={{ background: TEF_BLUE }} className="text-white font-semibold px-3 py-2 border border-gray-300 text-right">Einmalig</th>
                    <th style={{ background: TEF_BLUE, width: 40 }} className="border border-gray-300"></th>
                  </tr>
                </thead>
                <tbody>
                  {(() => {
                    const out = []; let currGid = null;
                    for (let i = 0; i < offerLines.length; i++) {
                      const l = offerLines[i], next = offerLines[i + 1];
                      if (l.type === "cluster") {
                        currGid = l.gid;
                        const gIdx = groups.findIndex(x => x.id === l.gid);
                        const gArea = groups.find(x => x.id === l.gid)?.area;
                        const isStandort = gArea === "standort";
                        const isVpn = gArea === "vpnconnect";
                        out.push(
                          <tr key={`cl-${l.gid}`} style={{ background: "#D7E0FF" }}>
                            <td colSpan={6} className="border border-gray-300 font-bold" style={{ color: TEF_BLUE }}>
                              <div className="flex items-center justify-between px-3 py-1.5">
                                <div className="flex items-center gap-1.5">
                                  <span>{l.name}</span>
                                  <button onClick={() => moveGroupDir(l.gid, -1)} disabled={gIdx <= 0} className="p-0.5 rounded hover:bg-white/60 disabled:opacity-25" title="Bereich nach oben"><ChevronUp size={15} color={TEF_BLUE} /></button>
                                  <button onClick={() => moveGroupDir(l.gid, 1)} disabled={gIdx >= groups.length - 1} className="p-0.5 rounded hover:bg-white/60 disabled:opacity-25" title="Bereich nach unten"><ChevronDown size={15} color={TEF_BLUE} /></button>
                                  <button onClick={() => duplicateGroup(l.gid)} className="p-0.5 rounded hover:bg-white/60" title="Bereich duplizieren"><Copy size={15} color={TEF_BLUE} /></button>
                                  <button onClick={() => removeGroup(l.gid)} className="p-0.5 rounded hover:bg-red-50" title="Bereich entfernen"><Trash2 size={15} color={TEF_RED} /></button>
                                </div>
                                {isStandort && <div className="flex items-center gap-2">
                                  {visibleSums.has(l.gid) && standortSums[l.gid] && <span className="text-xs font-bold" style={{ color: TEF_BLUE }}>Standort gesamt: {eur(standortSums[l.gid].sumM)}/Mon. · {eur(standortSums[l.gid].sumO)} einmalig</span>}
                                  <button onClick={() => toggleSum(l.gid)} className="text-xs px-2 py-0.5 rounded border font-medium" style={{ color: TEF_BLUE, borderColor: TEF_BLUE, background: "white" }}>
                                    {visibleSums.has(l.gid) ? "Standortkosten ausblenden" : "Standortkosten einblenden"}
                                  </button>
                                </div>}
                                {!isStandort && <div className="flex items-center gap-1.5">
                                  {!isVpn && <button onClick={() => toggleAreaDesc(l.gid)} className="text-xs px-2 py-0.5 rounded border font-medium" style={{ color: TEF_BLUE, borderColor: TEF_BLUE, background: "white" }}>
                                    {(groups.find(x => x.id === l.gid)?.showDesc) ? "Produktbeschreibung ausblenden" : "Produktbeschreibung einblenden"}
                                  </button>}
                                  <button onClick={() => toggleSum(l.gid)} className="text-xs px-2 py-0.5 rounded border font-medium" style={{ color: TEF_BLUE, borderColor: TEF_BLUE, background: "white" }}>
                                    {visibleSums.has(l.gid) ? "Produktsummen ausblenden" : "Produktsummen einblenden"}
                                  </button>
                                </div>}
                              </div>
                              {l.desc?.length > 0 && <div className="px-3 pb-1.5 -mt-0.5 text-xs font-normal text-gray-600">{l.desc.map((d, di) => <div key={di}>{d}</div>)}</div>}
                            </td>
                          </tr>
                        );
                      } else if (l.type === "subcluster") {
                        currGid = l.gid;
                        const parent = groups.find(x => x.id === l.pgid);
                        const siblings = parent?.subGroups || [];
                        const sIdx = siblings.findIndex(sg => sg.id === l.gid);
                        const sg = siblings[sIdx];
                        out.push(
                          <tr key={`scl-${l.gid}`} style={{ background: "#EAF0FF" }}>
                            <td colSpan={6} className="border border-gray-300 font-semibold" style={{ color: TEF_BLUE }}>
                              <div className="flex items-center justify-between pl-8 pr-3 py-1.5">
                                <div className="flex items-center gap-1.5">
                                  <span>{l.name}</span>
                                  <button onClick={() => moveSubGroupDir(l.pgid, l.gid, -1)} disabled={sIdx <= 0} className="p-0.5 rounded hover:bg-white/60 disabled:opacity-25" title="Unterbereich nach oben"><ChevronUp size={14} color={TEF_BLUE} /></button>
                                  <button onClick={() => moveSubGroupDir(l.pgid, l.gid, 1)} disabled={sIdx >= siblings.length - 1} className="p-0.5 rounded hover:bg-white/60 disabled:opacity-25" title="Unterbereich nach unten"><ChevronDown size={14} color={TEF_BLUE} /></button>
                                  <button onClick={() => duplicateSubGroup(l.pgid, l.gid)} className="p-0.5 rounded hover:bg-white/60" title="Unterbereich duplizieren"><Copy size={14} color={TEF_BLUE} /></button>
                                  <button onClick={() => removeSubGroup(l.pgid, l.gid)} className="p-0.5 rounded hover:bg-red-50" title="Unterbereich entfernen"><Trash2 size={14} color={TEF_RED} /></button>
                                </div>
                                <div className="flex items-center gap-1.5">
                                  <button onClick={() => toggleAreaDesc(l.gid)} className="text-xs px-2 py-0.5 rounded border font-medium" style={{ color: TEF_BLUE, borderColor: TEF_BLUE, background: "white" }}>
                                    {sg?.showDesc ? "Produktbeschreibung ausblenden" : "Produktbeschreibung einblenden"}
                                  </button>
                                  <button onClick={() => toggleSum(l.gid)} className="text-xs px-2 py-0.5 rounded border font-medium" style={{ color: TEF_BLUE, borderColor: TEF_BLUE, background: "white" }}>
                                    {visibleSums.has(l.gid) ? "Produktsummen ausblenden" : "Produktsummen einblenden"}
                                  </button>
                                </div>
                              </div>
                              {l.desc?.length > 0 && <div className="pl-8 pr-3 pb-1.5 -mt-0.5 text-xs font-normal text-gray-600">{l.desc.map((d, di) => <div key={di}>{d}</div>)}</div>}
                            </td>
                          </tr>
                        );
                      } else {
                        const isProd = l.type === "product";
                        const fr = isProd ? findRow(l.gid, l.rowId) : null;
                        out.push(
                          <tr key={`${l.type}-${i}`} className={isProd ? "" : "bg-gray-50 italic text-gray-500"}>
                            <td className="border border-gray-300 text-center align-middle" style={{ width: 36 }}>
                              {isProd && fr?.r && <SortArrows g={fr.g} r={fr.r} size={14} />}
                            </td>
                            <td className="px-3 py-2 border border-gray-300 align-top">{l.name}{l.meta?.length > 0 && <div className="text-xs text-gray-400">{l.meta.join(" · ")}</div>}</td>
                            <td className="px-3 py-2 border border-gray-300 align-top text-xs text-gray-600">{(l.desc || []).map((d, di) => <div key={di}>{d}</div>)}</td>
                            <td className="px-3 py-2 border border-gray-300 text-right align-top">{eur(l.monthly)}{l.abloeseNote && <div className="text-xs" style={{ color: TEF_RED }}>{l.abloeseNote}</div>}{l.rabattNote && <div className="text-xs" style={{ color: TEF_RED }}>{l.rabattNote}</div>}</td>
                            <td className="px-3 py-2 border border-gray-300 text-right align-top">{eur(l.oneoff)}</td>
                            <td className="border border-gray-300 text-center align-middle" style={{ width: 40 }}>
                              {isProd && l.rowId != null && <button onClick={() => duplicateRow(l.gid, l.rowId)} className="p-1 rounded hover:bg-blue-50" title="Zeile duplizieren"><Copy size={16} color={TEF_BLUE} /></button>}
                              {isProd && l.rowId != null && <button onClick={() => removeRow(l.gid, l.rowId)} className="p-1 rounded hover:bg-red-50" title="Zeile löschen"><Trash2 size={16} color={TEF_RED} /></button>}
                            </td>
                          </tr>
                        );
                        if (currGid && (visibleSums.has(currGid) || showAllSums) && (!next || next.type === "cluster" || next.type === "subcluster")) {
                          const s = clusterSumsData[currGid];
                          if (s) out.push(
                            <tr key={`csum-${currGid}`} style={{ background: "#E8EDFF" }}>
                              <td className="border border-gray-300" style={{ background: "#E8EDFF" }}></td>
                              <td colSpan={2} className="px-3 py-1.5 border border-gray-300 font-semibold text-sm" style={{ color: TEF_BLUE }}>Summe {s.name}</td>
                              <td className="px-3 py-1.5 border border-gray-300 text-right font-semibold text-sm" style={{ color: TEF_BLUE }}>{eur(s.sumM)}</td>
                              <td className="px-3 py-1.5 border border-gray-300 text-right font-semibold text-sm" style={{ color: TEF_BLUE }}>{eur(s.sumO)}</td>
                              <td className="border border-gray-300" style={{ background: "#E8EDFF" }}></td>
                            </tr>
                          );
                        }
                      }
                    }
                    return out;
                  })()}

                  <tr style={{ background: "#E6ECFF" }}>
                    <td className="border border-gray-300" style={{ background: "#E6ECFF" }}></td>
                    <td colSpan={2} style={{ color: TEF_BLUE }} className="px-3 py-2 border border-gray-300 font-bold">Gesamtsumme</td>
                    <td style={{ color: TEF_BLUE }} className="px-3 py-2 border border-gray-300 text-right font-bold">{eur(sumM)}</td>
                    <td style={{ color: TEF_BLUE }} className="px-3 py-2 border border-gray-300 text-right font-bold">{eur(sumO)}</td>
                    <td className="border border-gray-300" style={{ background: "#E6ECFF" }}></td>
                  </tr>

                  <tr>
                    <td colSpan={6} className="px-2 py-1.5 border border-gray-300 text-right bg-gray-50">
                      <div className="flex flex-wrap gap-2 justify-end">
                        <button onClick={() => setShowAllSums(v => !v)} className="text-xs font-semibold px-3 py-1 rounded border" style={{ color: TEF_BLUE, borderColor: TEF_BLUE }}>{showAllSums ? "Bereichssummen ausblenden" : "Zusammenfassung je Bereich"}</button>
                        <button onClick={() => setShowTCO(v => !v)} className="text-xs font-semibold px-3 py-1 rounded border" style={{ color: TEF_BLUE, borderColor: TEF_BLUE }}>{showTCO ? "Gesamtkosten ausblenden" : "Gesamtkosten (Laufzeit)"}</button>
                        <button onClick={() => setShowSavings(v => !v)} className="text-xs font-semibold px-3 py-1 rounded border" style={{ color: showSavings ? TEF_RED : TEF_BLUE, borderColor: showSavings ? TEF_RED : TEF_BLUE }}>{showSavings ? "Ersparnis ausblenden" : "Ersparnis einblenden"}</button>
                      </div>
                    </td>
                  </tr>

                  {showSavings && <tr style={{ background: "#FFF5F5" }}>
                    <td className="border border-gray-300" style={{ background: "#FFF5F5" }}></td>
                    <td colSpan={2} className="px-3 py-2 border border-gray-300 text-sm" style={{ color: TEF_RED }}>Bisherige IST-Kosten des Kunden (€/Monat, optional – sonst Vergleich ggü. Listenpreis)</td>
                    <td colSpan={2} className="px-3 py-1.5 border border-gray-300 text-right">
                      <input type="number" step="0.01" min="0" className={ctl + " text-right max-w-[140px] inline-block"} placeholder="z. B. 450" value={istKosten} onChange={e => setIstKosten(e.target.value)} />
                    </td>
                    <td className="border border-gray-300" style={{ background: "#FFF5F5" }}></td>
                  </tr>}

                  {showTCO && primaryTerm > 0 && <tr style={{ background: "#EEF2FF" }}>
                    <td className="border border-gray-300" style={{ background: "#EEF2FF" }}></td>
                    <td colSpan={2} className="px-3 py-2 border border-gray-300 font-semibold text-sm" style={{ color: TEF_BLUE }}>Gesamtkosten über die Laufzeit (monatlich × Laufzeit + einmalig)</td>
                    <td colSpan={2} className="px-3 py-2 border border-gray-300 text-right font-semibold text-sm" style={{ color: TEF_BLUE }}>{eur(sumM * primaryTerm + sumO)}</td>
                    <td className="border border-gray-300" style={{ background: "#EEF2FF" }}></td>
                  </tr>}

                  {showSavings && effSavM > 0 && <>
                    <tr style={{ background: "#FFF5F5" }}>
                      <td className="border border-gray-300" style={{ background: "#FFF5F5" }}></td>
                      <td colSpan={2} className="px-3 py-2 border border-gray-300 font-semibold text-sm" style={{ color: TEF_RED }}>Ihre monatliche Ersparnis {vsIst ? "gegenüber Ihren bisherigen Kosten" : "gegenüber Listenpreis"}</td>
                      <td colSpan={2} className="px-3 py-2 border border-gray-300 text-right font-semibold text-sm" style={{ color: TEF_RED }}>{eur(effSavM)}</td>
                      <td className="border border-gray-300" style={{ background: "#FFF5F5" }}></td>
                    </tr>
                    {primaryTerm > 0 && <tr style={{ background: "#FFF5F5" }}>
                      <td className="border border-gray-300" style={{ background: "#FFF5F5" }}></td>
                      <td colSpan={2} className="px-3 py-2 border border-gray-300 font-semibold text-sm" style={{ color: TEF_RED }}>Ihre Gesamtersparnis über die Laufzeit</td>
                      <td colSpan={2} className="px-3 py-2 border border-gray-300 text-right font-semibold text-sm" style={{ color: TEF_RED }}>{eur(totalSavings)}</td>
                      <td className="border border-gray-300" style={{ background: "#FFF5F5" }}></td>
                    </tr>}
                  </>}
                </tbody>
              </table>
            </div>
          )}
        </div>
        <p className="text-center text-xs text-gray-400 mt-4">Sortieren & Löschen wirken auch oben im Kalkulator · Buttons erscheinen nicht im Export · Speichern als .json + .jpg</p>
        </>}
      </div>
    </div>
  );
}


ReactDOM.createRoot(document.getElementById("root")).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);
