From 9108c90a53fbbf0e953690d54f1d306aab1c0c26 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 8 Aug 2026 21:42:13 +0000 Subject: [PATCH] Rechnungsanalyse: Kalkulationsseite fuer Leads im Admin-Panel Bei Leads (customerStatus=lead) steht in der Kundenliste jetzt der Button "Rechnungsanalyse". Er oeffnet /kalkulation.html?customer= - die Seite, die im Gespraech vor Ort oder am Telefon ausgefuellt wird. - Positionen nach Bereich (Website, Onlineshop, Softwareprojekt, Automatisierung/KI, laufende Kosten) mit Stundenwerten und Mengen - Preis ueber die Stundenstaffel (150/130/115/100), Brutto mit 19 % MwSt., Zahlungsmodalitaeten ab 1.000 / 3.000 EUR - Hinweis, ob selbst zusagbar (<= 5 Std), Termin abzustimmen oder ob Kenso freigeben muss (Shop, Software, Automatisierung) - Termin mit Justin/Kenso aus den hinterlegten Zeitfenstern, Notizen, Kundenportal-Zugang mit QR-Code - Zusammenfassung als Text zum Kopieren -> gehoert ins Akquise-Ticket Bewusst ohne eigenen Datensatz: dokumentiert wird im Ticket. Der Stand liegt pro Kunde nur im localStorage, damit ein angefangenes Gespraech nicht verloren geht. Fonts (Caprasimo, Figtree) liegen lokal unter public/fonts, kein Abruf beim Google-CDN. Neue Route GET /api/portal-admin/customers/:customerId liefert die Stammdaten zum Vorbefuellen. Co-Authored-By: Claude Opus 5 --- public/admin.html | 1 + public/admin.js | 9 +- public/fonts/caprasimo-latin-ext.woff2 | Bin 0 -> 2332 bytes public/fonts/caprasimo-latin.woff2 | Bin 0 -> 20888 bytes public/fonts/figtree-latin-ext.woff2 | Bin 0 -> 10280 bytes public/fonts/figtree-latin.woff2 | Bin 0 -> 20156 bytes public/img/portal-qr.png | Bin 0 -> 2959 bytes public/kalkulation.css | 362 +++++++++++++ public/kalkulation.html | 169 ++++++ public/kalkulation.js | 707 +++++++++++++++++++++++++ server/routes/portalAdmin.js | 22 + 11 files changed, 1269 insertions(+), 1 deletion(-) create mode 100644 public/fonts/caprasimo-latin-ext.woff2 create mode 100644 public/fonts/caprasimo-latin.woff2 create mode 100644 public/fonts/figtree-latin-ext.woff2 create mode 100644 public/fonts/figtree-latin.woff2 create mode 100644 public/img/portal-qr.png create mode 100644 public/kalkulation.css create mode 100644 public/kalkulation.html create mode 100644 public/kalkulation.js diff --git a/public/admin.html b/public/admin.html index 5cf8cd0..45f486a 100644 --- a/public/admin.html +++ b/public/admin.html @@ -38,6 +38,7 @@ .mini-btn { background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.18); color: inherit; padding: 5px 11px; border-radius: 8px; cursor: pointer; font-size: 13px; white-space: nowrap; } .mini-btn:hover { background: rgba(255,255,255,0.14); } + a.mini-btn { display: inline-flex; align-items: center; text-decoration: none; } .mini-btn.primary { background: rgba(94,234,212,0.16); border-color: rgba(94,234,212,0.5); } .code-input { width: 100%; min-width: 60px; max-width: 90px; padding: 4px 8px; border-radius: 7px; border: 1px solid rgba(255,255,255,0.2); background: rgba(255,255,255,0.06); color: inherit; font: inherit; font-size: 13px; } diff --git a/public/admin.js b/public/admin.js index c702c4e..e08df62 100644 --- a/public/admin.js +++ b/public/admin.js @@ -41,6 +41,10 @@ function fmtEuro(value) { return Number(value || 0).toLocaleString('de-DE', { style: 'currency', currency: 'EUR' }) } +function isLead(customer) { + return String(customer.customerStatus || '').toLowerCase() === 'lead' +} + function stagePill(status) { const s = String(status || '').toLowerCase() if (s === 'lead') return 'Lead' @@ -233,7 +237,10 @@ async function loadCustomers(search = '') { ${c.projectCount}
- + ${isLead(c) ? `Rechnungsanalyse` : ''} +
diff --git a/public/fonts/caprasimo-latin-ext.woff2 b/public/fonts/caprasimo-latin-ext.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..e83e8d883926d1e44447668f8acd11ce6b14a6b7 GIT binary patch literal 2332 zcmV+%3FG#6Pew8T0RR9100|rb4FCWD01of~00_$f0RR9100000000000000000000 z0000RRt8`IfgBMC3X%+uJPRxU0X70815^Yc1%ws{fes7@8;%T7lu%X#z>kBRjMtp> z^z>X}wcWC80#gg(QQZ6_Ygh2V=UMOnq#83zB86wNuukifaDs4y-G;FXW`IuZvN_(G z6pVSHid>T-DU1wvo70IXW&|%_-u)W!50z#~v|Xd}NLC`HY&)A^KFo%wCe=pBRJA1D z#7ibNLTHDIH9drYEmPX=y?Y^m7)q;2wr<xFv28l|$Zr$3^9BnJh z@Ms^{`irn;8%>*7)&h-$94I{P!DT0v1y7eJi9#HjmTWFv42pGqL0*y%6rm`JUDHex z(6m4V9&XO<%qQk4Q%7`rbX>$1u|@nC9XxS?uX z0NVkF(AZ!Eg*@jUKc7~yeK!g@cI^}jiDC#NVE{gHV|{Nn6eU@YvIv#kk@CfP_|KV*A^VCs0$7@kh6G!30Uu{Nn!o=t-E7C<=)TArUkh zfWs8hCUpRK=n^^@LI-cr_C;u$cB_$>YIx4G2rH5x2AZA1%8TqZM5g$*0`Ay*3_~L>scV*<#-&jYZnY(@Tt15f*FHAqsy z=FM^EH=CPDsj6PA3X7ry@Ty^9s(^qi#`d;xAfCYgjR<}N)WJZ%ttB%Z9010Vf z;Z{GuXguqY<8guDXBcf5IysU}*?Lagzv)hPtn|N>ET(x+{1t(Ai39%^d=9SgE7mN0i#x3@1PyFUNdD=AA4Ol;cmD#E2zPxE zvd(opDUS%6+7iXiVT!SZsX4)=coSX2Fsd%B`^cR#XYT}zsO_BL;7BqrZ~BtiiWr_Jg{OVK?`iDZF+NUgm-E>RRZzOS zU7jzi7Z`YWN8%risduw?^_885Hv}JiULWLorw>iH&Ze=>b9t0h=Niv{K9ba-5%YJL ztqliPFOhnsD#-&MC95*^JhZAS_h4UF@zJdvH&uW;sZDE>fhMJ0u2IDN7tdQAv zp7>R(waY+FBV0(fxA|BS$-U-QcBs4M2|lhdu}Un*;sAz(s5l`pyUu4^$D3|WnGB?T z+(#>8Q(j(w$AbB+BxZMc^@jT8fxS;0!)dLAov00OW75u0_|xOvYg%Gc6!Vn|n?PX! zt&l*+UBgySeBmQ*6ZU)T3mXHAw>`^f;bu>5HA8xU`W=(DmQH)8$4s2& zF0+&<3^sjDj6_mHvg`GrwFD}ib~$@H9Trmul&)ehci1|!c?MWwx;MKJ?8x-FM^m$T zE%Z+T<>X%owz2MZg#YNDNou9dHVc5<5<%b?OakIQe-`!yCm{U16-mjU^8 zjVAmwp=4Uh{|=Hf?r*l_FaGhk`*9C{e*H*2htw*w7rRQD^ZatFSmfY2Oe{(fg{Mq+ zK+0lZ@__<@sDQj!(CBugTPq9fOo7*GC7j8NJR&A_j58^h+$&V&teu6GnbyWUr#ap8 z(GtOdQ_k>P#3UHt-Z|iSTkW(O#vVTVfJX-@aaGBGp8Mk*wkww6r1L%iCLg$>(Kz*c zbsDEyt#Ru1YITn7hvp>G^7y$A=Ew`Pw7Sf;N+`dt-I;vQ5|-{;TlRQ!4-p3NCR{l-oi^9z+I#9L zgG76S8h4uie14j(I~}K@_AHr{_E6P6vMefKBgtFgr*au0q2dxWZ_N zH7IwJRa@Fvx}EGyM77H$4RRg5C|0T?<1Fv%-hvc&y(I};^;W2Fjkiikqr5HT=ufSe zSzVfNN$@i!^*@5qG*k^<(#0qOR63^aRMlu29;Cr2e3<6lXiH~Wp|4^TdQXcMSw&Ag z105?t^@aAO@KK20GAKqza58AJsJpBN+eMK3iq zRnKKDn%*Pu^y(w+ex(`&G2A7Y+JdoV;m3$`eogtd6g~o|)Uh}(>B69hBitBm3_L_1 zQB73gZ6(y=2s4T+&aQo}3aXMM;hHHz@Q&#~p|mpUYOF7_Z1IRE+!azNLKPNa6*iu8 z#0!V+8au}0GK!pzhEe*snp|PsJ8}R%`5O7DVfHnFkqqwR|AA$Rt2ZcFzpSl#S C^f}Z3 literal 0 HcmV?d00001 diff --git a/public/fonts/caprasimo-latin.woff2 b/public/fonts/caprasimo-latin.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..a1e50c3fabdaf5c81037cb4d88adc62ea730f65f GIT binary patch literal 20888 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 literal 0 HcmV?d00001 diff --git a/public/fonts/figtree-latin-ext.woff2 b/public/fonts/figtree-latin-ext.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..a8a112383ac0345ddab6d8373e5cc6a5a0213bb3 GIT binary patch literal 10280 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 literal 0 HcmV?d00001 diff --git a/public/img/portal-qr.png b/public/img/portal-qr.png new file mode 100644 index 0000000000000000000000000000000000000000..d35c9dc42b12a943d976bb20d73af7d98d59bdca GIT binary patch literal 2959 zcmcImT}V`E7(QkjW(Iu7Xu63IUI_Z+0bQvZ)_ZE(4=5pTfA(yt9{ouKGkf)k-a4}nYh`+BD{fmIm+lJ!Yp+g(ZL*8u zN4ulTr*-C$BWkd9!k?Px6j<dg>s4l3fkB`pGv{fg%jb&349%c1$5k{fz@D}j( z(7>y^SdX^vGw{l5hGM|*VKwNQ@K0Z`I-E0YKOml@ zhB#Hp$`Ms|`(I^ZUjzT4(=9+AmDHo>Q3u!#-;Ai1{_OG|n@qbP+oO7XH69&?p#+lW z&A_BR_L_ZnHAyV3#^Ir80kN~Cp4zSiOv`Keu8GCkyMNxVJyd2$Wjc@~h3t>2)O0QT zqvWwL9dY@?F+LnVreNzy}c>h%DqIY7gNSwVOoLWr=ecVI+kCHcyLb45^EISWJQ!UXT-FaEETb z7oA>xzye)rCKjKVDw_BC7gVKi8FDiP15{zK0R-p&U3TQ9mC@)S=O+F}IpFbQiN1WJORRi^_j_+<_T)V^QEi?cQW&7}N?X>2 zJxPU54?L+W_8Uu6^FAJ{I9zq?5*s;e^wV(y0!f}Mc%V|PB*)}xGA!#yJRIzMp&TX| yQVvd9^_SUU&|<8dG0!}9uR$QJ|Mvsz{91SEs-^a`v86a+O9kI;%fFtx_vcUC6S`mk literal 0 HcmV?d00001 diff --git a/public/kalkulation.css b/public/kalkulation.css new file mode 100644 index 0000000..73cbbfb --- /dev/null +++ b/public/kalkulation.css @@ -0,0 +1,362 @@ +/* Rechnungsanalyse / Projektkalkulation. + Bewusst eigenes helles "Organic"-Design (nicht das dunkle Portal-Theme), + weil die Seite im Kundengespraech gezeigt wird. Fonts liegen lokal unter + /fonts (kein Google-CDN-Abruf). */ + +@font-face { + font-family: 'Caprasimo'; font-style: normal; font-weight: 400; font-display: swap; + src: url(/fonts/caprasimo-latin-ext.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: 'Caprasimo'; font-style: normal; font-weight: 400; font-display: swap; + src: url(/fonts/caprasimo-latin.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* Figtree ist ein Variable Font – eine Datei deckt 400–700 ab. */ +@font-face { + font-family: 'Figtree'; font-style: normal; font-weight: 400 700; font-display: swap; + src: url(/fonts/figtree-latin-ext.woff2) format('woff2'); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +@font-face { + font-family: 'Figtree'; font-style: normal; font-weight: 400 700; font-display: swap; + src: url(/fonts/figtree-latin.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +:root { + --color-bg: #f5ead8; + --color-surface: #ebddc5; + --color-text: #201e1d; + --color-accent: #c67139; + --color-accent-2: #7a8a5e; + + --color-neutral-100: #f9f4ed; + --color-neutral-200: #eee7db; + --color-neutral-300: #dcd3c4; + --color-neutral-400: #c0b6a5; + --color-neutral-500: #a19786; + --color-neutral-600: #82796a; + --color-neutral-700: #645c50; + --color-neutral-800: #474238; + --color-neutral-900: #2e2b25; + + --color-accent-100: #fff2eb; + --color-accent-200: #ffe1d0; + --color-accent-700: #8c491a; + --color-accent-800: #643312; + + --color-accent-2-100: #f0fae1; + --color-accent-2-200: #e1eecc; + --color-accent-2-500: #8fa073; + --color-accent-2-600: #728157; + --color-accent-2-700: #56633f; + --color-accent-2-800: #3d472b; + + --font-heading: 'Caprasimo', Georgia, serif; + --font-body: 'Figtree', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; + + --radius-md: 16px; + --radius-lg: 28px; + --shadow-sm: 0 1px 2px rgba(46, 43, 37, 0.14); + --shadow-md: 0 3px 10px rgba(46, 43, 37, 0.16); + --shadow-lg: 0 12px 32px rgba(46, 43, 37, 0.22); +} + +*, *::before, *::after { box-sizing: border-box; } + +body { + margin: 0; + min-height: 100vh; + background: var(--color-bg); + color: var(--color-text); + font-family: var(--font-body); + font-size: 15px; + line-height: 1.55; +} + +h1, h2, h3 { font-family: var(--font-heading); font-weight: 400; line-height: 1.12; margin: 0; } +a { color: var(--color-accent-700); } +a:hover { color: var(--color-accent-800); } +button { font: inherit; } +:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; } +::selection { background: var(--color-accent-200); } +input[type=number]::-webkit-outer-spin-button, +input[type=number]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; } +input[type=number] { -moz-appearance: textfield; } + +/* ------------------------------------------------------------- Kopfzeile --- */ +.kal-header { + position: sticky; top: 0; z-index: 20; + background: var(--color-accent-2-500); color: var(--color-accent-2-100); + padding: 14px 20px; box-shadow: var(--shadow-md); +} +.kal-header-inner { + max-width: 1040px; margin: 0 auto; + display: flex; align-items: center; gap: clamp(10px, 2.4vw, 18px); flex-wrap: wrap; +} +.kal-brand { font-family: var(--font-heading); font-size: clamp(17px, 4vw, 22px); color: #fff; } +.kal-back { + color: var(--color-accent-2-100); text-decoration: none; font-size: 13.5px; font-weight: 600; + border: 1px solid rgba(255, 255, 255, 0.4); border-radius: 999px; padding: 5px 14px; white-space: nowrap; +} +.kal-back:hover { background: rgba(255, 255, 255, 0.16); color: #fff; } +.kal-spacer { flex: 1; min-width: 0; } +.kal-stat { display: flex; align-items: baseline; gap: 8px; } +.kal-stat-lbl { + font-size: 11.5px; font-weight: 700; letter-spacing: 0.14em; + text-transform: uppercase; color: var(--color-accent-2-200); +} +.kal-stat-val { font-family: var(--font-heading); line-height: 1; color: #fff; font-size: clamp(16px, 3.6vw, 22px); } +.kal-stat-val.big { font-size: clamp(19px, 4.6vw, 30px); } + +main { max-width: 1040px; margin: 0 auto; padding: clamp(18px, 3.4vw, 28px) clamp(12px, 3.2vw, 20px) 100px; } + +/* ------------------------------------------------------------------ Hero --- */ +.hero { + border-radius: var(--radius-lg); background: var(--color-accent-2-500); + color: var(--color-accent-2-100); + padding: clamp(20px, 4vw, 34px) clamp(20px, 4vw, 34px) clamp(20px, 3.6vw, 30px); +} +.kicker { font-size: 12px; font-weight: 700; letter-spacing: 0.16em; text-transform: uppercase; color: var(--color-accent-2-200); } +.hero-price-row { display: flex; align-items: flex-end; gap: 16px; flex-wrap: wrap; margin-top: 10px; } +.hero-price { font-family: var(--font-heading); font-size: clamp(46px, 11vw, 72px); line-height: 0.95; color: #fff; } +.hero-price-sub { font-size: 15px; color: var(--color-accent-2-200); padding-bottom: 12px; } +.hero-tiles { + display: grid; grid-template-columns: repeat(auto-fit, minmax(clamp(120px, 44%, 210px), 1fr)); + gap: 10px; margin-top: 24px; +} +.tile { background: rgba(255, 255, 255, 0.16); border-radius: var(--radius-md); padding: 16px 18px; } +.tile-lbl { font-size: 11.5px; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; color: var(--color-accent-2-200); } +.tile-val { font-family: var(--font-heading); font-size: 30px; color: #fff; margin-top: 4px; } +.tile-sub { font-size: 12.5px; color: var(--color-accent-2-200); margin-top: 2px; } +.tile-pay { grid-column: 1 / -1; background: var(--color-accent-2-100); color: var(--color-accent-2-800); } +.tile-pay .tile-lbl { color: var(--color-accent-2-600); } +.tile-pay-title { font-weight: 700; font-size: 16px; margin-top: 4px; } +.tile-pay-text { font-size: 13px; line-height: 1.45; margin-top: 4px; text-wrap: pretty; } +.flag { + margin-top: 12px; padding: 15px 18px; border-radius: var(--radius-md); + font-size: 13.5px; line-height: 1.5; text-wrap: pretty; + background: rgba(255, 255, 255, 0.16); color: var(--color-accent-2-100); +} +.flag.ok { background: var(--color-accent-2-100); color: var(--color-accent-2-800); } +.flag.talk { background: var(--color-accent-200); color: var(--color-accent-800); } +.flag.kenso { background: var(--color-neutral-100); color: var(--color-neutral-800); } +.hero-actions { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 20px; } +.btn-solid { + padding: 14px 26px; border-radius: 999px; border: none; background: #fff; + color: var(--color-accent-2-700); font-weight: 700; font-size: 15px; cursor: pointer; +} +.btn-outline { + padding: 14px 22px; border-radius: 999px; border: 2px solid rgba(255, 255, 255, 0.4); + background: transparent; color: var(--color-accent-2-100); font-weight: 700; font-size: 15px; cursor: pointer; +} +.btn-outline:hover { background: rgba(255, 255, 255, 0.14); } + +/* -------------------------------------------------------------- Bausteine --- */ +.block { margin-top: 38px; } +.block h2 { font-size: clamp(23px, 5.4vw, 32px); } +.block-lead { font-size: 14.5px; color: var(--color-neutral-600); margin: 4px 0 14px; max-width: 620px; text-wrap: pretty; } +.section-head { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 6px; } +.section-head h2 { flex: 1; min-width: 150px; } +.section-tag { + font-size: 11px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; + padding: 6px 13px; border-radius: 999px; + background: var(--color-accent-200); color: var(--color-accent-800); +} +.section-tag.kenso { background: var(--color-accent-2-200); color: var(--color-accent-2-800); } +.group-label { + font-size: 11.5px; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; + color: var(--color-neutral-500); margin: 22px 0 10px; +} +.chips { display: flex; flex-wrap: wrap; gap: 10px; } + +.chip { + display: inline-flex; align-items: center; gap: 9px; padding: 12px 20px; border-radius: 999px; + border: 2px solid var(--color-neutral-300); background: transparent; + color: var(--color-neutral-700); font-weight: 700; font-size: 15px; cursor: pointer; min-height: 48px; +} +.chip.on { border-color: var(--color-accent); background: var(--color-accent-100); color: var(--color-accent-800); } +.chip .mark { + flex: 0 0 20px; height: 20px; border-radius: 999px; border: 2px solid var(--color-neutral-400); + display: flex; align-items: center; justify-content: center; color: #fff; font-size: 11px; +} +.chip.on .mark { border-color: var(--color-accent); background: var(--color-accent); } +.chip .count { + font-weight: 700; font-size: 13px; padding: 2px 9px; border-radius: 999px; + background: var(--color-accent-200); color: var(--color-accent-800); +} + +.item-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(clamp(130px, 45%, 300px), 1fr)); gap: 10px; } +.item { + display: flex; align-items: flex-start; gap: clamp(9px, 2vw, 14px); + padding: clamp(13px, 2.6vw, 18px) clamp(14px, 2.8vw, 20px); + border-radius: var(--radius-lg); border: 2px solid var(--color-neutral-200); + background: var(--color-neutral-100); cursor: pointer; min-height: 72px; text-align: left; + color: inherit; width: 100%; +} +.item.on { border-color: var(--color-accent); background: var(--color-accent-100); } +.item .mark { + flex: 0 0 clamp(20px, 5vw, 26px); width: clamp(20px, 5vw, 26px); height: clamp(20px, 5vw, 26px); + border-radius: 999px; border: 2px solid var(--color-neutral-400); + display: flex; align-items: center; justify-content: center; + color: #fff; font-size: 13px; font-weight: 700; margin-top: 2px; +} +.item.on .mark { border-color: var(--color-accent); background: var(--color-accent); } +.item-body { flex: 1; min-width: 0; } +.item-name { display: block; font-weight: 600; font-size: clamp(14px, 2.9vw, 16px); line-height: 1.3; } +.item-desc { + display: block; font-size: clamp(11.5px, 2.4vw, 13px); line-height: 1.45; + color: var(--color-neutral-600); margin-top: 4px; text-wrap: pretty; +} +.item-hrs { + display: inline-block; margin-top: 9px; padding: 4px 11px; border-radius: 999px; + background: var(--color-neutral-200); color: var(--color-neutral-600); + font-size: 12.5px; font-weight: 700; font-variant-numeric: tabular-nums; white-space: nowrap; +} +.item.on .item-hrs { background: var(--color-accent-200); color: var(--color-accent-800); } +.qty { display: flex; align-items: center; gap: 6px; margin-top: 10px; max-width: 100%; } +.qty button { + flex: 0 0 auto; width: clamp(34px, 9vw, 44px); height: 44px; border-radius: 999px; + border: 2px solid var(--color-neutral-300); background: #fff; color: var(--color-accent-700); + font-size: 20px; font-weight: 700; cursor: pointer; line-height: 1; padding: 0; +} +.qty input { + flex: 1 1 auto; min-width: 0; width: clamp(44px, 13vw, 64px); height: 44px; border-radius: 999px; + border: 2px solid var(--color-neutral-300); background: #fff; font: inherit; font-size: 15px; + font-weight: 700; text-align: center; color: var(--color-text); padding: 0; +} + +.empty-hint { + margin-top: 34px; padding: 34px; border-radius: var(--radius-lg); + background: var(--color-neutral-100); border: 2px dashed var(--color-neutral-300); text-align: center; +} +.empty-hint h3 { font-size: 24px; margin-bottom: 6px; } +.empty-hint p { font-size: 14.5px; color: var(--color-neutral-600); margin: 0; } + +.panel { margin-top: 38px; border-radius: var(--radius-lg); background: var(--color-surface); padding: clamp(20px, 3.6vw, 30px); } +.panel h2 { font-size: clamp(23px, 5.4vw, 30px); margin: 6px 0 4px; } +.panel-lead { font-size: 14.5px; line-height: 1.55; color: var(--color-neutral-700); margin: 0 0 16px; max-width: 560px; text-wrap: pretty; } +.chosen-chip { + display: inline-flex; align-items: center; gap: 10px; padding: 9px 16px; border-radius: 999px; + background: var(--color-neutral-100); font-size: 14px; +} +.chosen-chip b { color: var(--color-accent-700); font-weight: 700; } + +/* ---------------------------------------------------------------- Termin --- */ +.seg { display: inline-flex; gap: 4px; padding: 4px; border-radius: 999px; background: var(--color-neutral-200); margin-bottom: 18px; } +.seg button { + padding: 11px 22px; border-radius: 999px; border: none; background: transparent; + color: var(--color-neutral-700); font-weight: 700; font-size: 14.5px; cursor: pointer; min-height: 44px; +} +.seg button.on { background: var(--color-accent); color: #fff; } +.person { + display: flex; align-items: center; gap: 12px; padding: 12px 20px 12px 12px; border-radius: 999px; + border: 2px solid var(--color-neutral-200); background: var(--color-neutral-100); + color: var(--color-text); cursor: pointer; min-height: 52px; text-align: left; +} +.person.on { border-color: var(--color-accent); background: var(--color-accent-100); } +.person .ava { + flex: 0 0 34px; height: 34px; border-radius: 999px; background: var(--color-neutral-400); color: #fff; + display: flex; align-items: center; justify-content: center; font-family: var(--font-heading); font-size: 15px; +} +.person.on .ava { background: var(--color-accent); } +.person b { display: block; font-weight: 700; font-size: 15px; } +.person span.role { display: block; font-size: 12.5px; color: var(--color-neutral-600); } +.day-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(clamp(74px, 22%, 110px), 1fr)); gap: 8px; } +.day { + padding: 12px 6px; border-radius: var(--radius-md); border: 2px solid var(--color-neutral-200); + background: var(--color-neutral-100); color: var(--color-text); cursor: pointer; min-height: 74px; + display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px; +} +.day.on { border-color: var(--color-accent); background: var(--color-accent); color: #fff; } +.day:disabled { background: transparent; color: var(--color-neutral-500); opacity: 0.45; cursor: default; } +.day .wd { font-size: 11.5px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; } +.day .dm { font-family: var(--font-heading); font-size: 20px; line-height: 1.1; } +.day .free { font-size: 11px; font-weight: 600; } +.slot { + padding: 12px 22px; border-radius: 999px; border: 2px solid var(--color-neutral-200); + background: var(--color-neutral-100); color: var(--color-text); + font-weight: 700; font-size: 15px; cursor: pointer; min-height: 48px; +} +.slot.on { border-color: var(--color-accent); background: var(--color-accent); color: #fff; } +.field-label { + display: block; font-size: 11.5px; font-weight: 700; letter-spacing: 0.14em; + text-transform: uppercase; color: var(--color-neutral-500); margin-bottom: 8px; +} +textarea.kal-input, input.kal-input { + width: 100%; border-radius: var(--radius-md); border: 2px solid var(--color-neutral-300); + background: var(--color-neutral-100); padding: 14px 16px; font: inherit; font-size: 15px; + line-height: 1.5; color: var(--color-text); resize: vertical; +} +textarea.kal-input { min-height: 84px; } +.term-status { + margin-top: 16px; padding: 15px 18px; border-radius: var(--radius-md); + font-size: 14.5px; line-height: 1.5; text-wrap: pretty; + background: var(--color-neutral-100); color: var(--color-neutral-700); +} +.term-status.fixed { background: var(--color-accent-2-100); color: var(--color-accent-2-800); } + +/* ------------------------------------------------------------ Kundenportal --- */ +.portal-box { + margin-top: 38px; border-radius: var(--radius-lg); background: var(--color-neutral-100); + border: 2px solid var(--color-neutral-200); padding: clamp(18px, 3.4vw, 28px) clamp(18px, 3.6vw, 30px); + display: flex; gap: clamp(16px, 3vw, 30px); align-items: center; flex-wrap: wrap; +} +.portal-qr { flex: 0 0 auto; background: #fff; border-radius: var(--radius-md); padding: clamp(10px, 2vw, 14px); box-shadow: var(--shadow-sm); } +.portal-qr img { display: block; width: clamp(104px, 26vw, 180px); height: clamp(104px, 26vw, 180px); } +.portal-info { flex: 1; min-width: 190px; } +.portal-info h2 { font-size: 28px; margin: 6px 0 8px; } +.cred-row { display: flex; flex-wrap: wrap; gap: 10px; } +.cred { + background: #fff; border-radius: 999px; padding: 8px 20px; + display: flex; align-items: center; gap: 10px; flex: 1 1 240px; +} +.cred span { + font-size: 11px; font-weight: 700; letter-spacing: 0.12em; + text-transform: uppercase; color: var(--color-neutral-500); white-space: nowrap; +} +.cred input { + flex: 1; min-width: 0; border: none; background: transparent; font: inherit; + font-size: 15.5px; font-weight: 700; color: var(--color-text); padding: 4px 0; +} +.cred input.mono { color: var(--color-accent-700); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; } + +footer { margin-top: 34px; font-size: 12.5px; color: var(--color-neutral-500); line-height: 1.6; } + +/* ---------------------------------------------------------------- Dialog --- */ +.backdrop { + position: fixed; inset: 0; z-index: 50; background: rgba(32, 30, 29, 0.55); + display: flex; align-items: center; justify-content: center; padding: 20px; +} +.backdrop[hidden] { display: none; } +.dialog { + width: 100%; max-width: 640px; max-height: 84vh; overflow: auto; background: var(--color-bg); + border-radius: var(--radius-lg); padding: 26px 28px; box-shadow: var(--shadow-lg); +} +.dialog h2 { font-size: 26px; margin-bottom: 12px; } +.dialog textarea { + width: 100%; height: 340px; border-radius: var(--radius-md); border: 2px solid var(--color-neutral-300); + background: var(--color-neutral-100); padding: 16px; + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12.5px; line-height: 1.55; + color: var(--color-text); resize: vertical; +} +.dialog-actions { display: flex; gap: 10px; margin-top: 14px; flex-wrap: wrap; } +.btn-accent { + padding: 13px 24px; border-radius: 999px; border: none; background: var(--color-accent); + color: #fff; font-weight: 700; font-size: 15px; cursor: pointer; +} +.btn-quiet { + padding: 13px 22px; border-radius: 999px; border: 2px solid var(--color-neutral-300); + background: transparent; color: var(--color-neutral-700); font-weight: 700; font-size: 15px; cursor: pointer; +} + +[hidden] { display: none !important; } + +@media print { + .kal-header, .hero-actions, .seg, .qty, .backdrop { display: none !important; } + body { background: #fff; } + .hero { color: #201e1d; background: var(--color-accent-2-100); } + .hero-price, .tile-val, .kal-brand { color: var(--color-accent-2-800); } +} diff --git a/public/kalkulation.html b/public/kalkulation.html new file mode 100644 index 0000000..46eeea0 --- /dev/null +++ b/public/kalkulation.html @@ -0,0 +1,169 @@ + + + + + + Rechnungsanalyse – Webklar + + + +
+
+ webklar + ← Admin + + + Netto + 0 € + + + Stunden + 0,00 + + + Monatlich + + +
+
+ +
+ +
+
Ihr Projekt
+
+
0 €
+
netto · 0 € brutto inkl. 19 % MwSt.
+
+
+
+
Aufwand
+
0,00 Std
+
Stundensatz 150 €/Std
+
+
+
Monatlich
+
+
nichts Laufendes gewählt
+
+
+
Zahlung
+
Noch nichts gewählt
+
Kreuze an, was der Kunde braucht.
+
+
+
Noch offen. Sobald etwas gewählt ist, steht hier, ob du selbst abschließen kannst.
+
+ + +
+
+ + +
+

Worum geht es?

+

Bereiche antippen — nur die ausgewählten werden unten angezeigt.

+
+
+ + +
+ + + + + + + +
+
Termin
+

Gespräch festlegen

+

Nur wenn gewünscht: Mit wem, wann — und worum es geht. Der Termin hängt an der Zusammenfassung.

+ +
+ + +
+ + + +
+ + +
+
+ + +
+
+ QR-Code zum Kundenportal project.webklar.com +
+
+
Kundenportal
+

Code scannen und einloggen

+

Im Portal sieht der Kunde den Stand des Projekts, alle Dateien und die Rechnungen — + project.webklar.com

+
+ + +
+
+
+ +
+ webklar GbR · Am Schwimmbad 10 · 67722 Winnweiler · 0170 4969375
+ Alle Preise netto zzgl. gesetzlicher Umsatzsteuer. Stundenwerte sind Richtwerte. +
+
+ + + + + + + + diff --git a/public/kalkulation.js b/public/kalkulation.js new file mode 100644 index 0000000..1b11112 --- /dev/null +++ b/public/kalkulation.js @@ -0,0 +1,707 @@ +/* Rechnungsanalyse / Projektkalkulation. + Wird aus dem Admin-Panel bei einem Lead geoeffnet (kalkulation.html?customer=) + und im Gespraech vor Ort oder am Telefon ausgefuellt. + + Ergebnis ist bewusst KEIN eigener Datensatz: die Zusammenfassung wird kopiert + und im Akquise-Ticket dokumentiert (dort wird gearbeitet). Damit ein + angefangenes Gespraech nicht verloren geht, liegt der Stand pro Kunde im + localStorage des Browsers. */ + +/* --------------------------------------------------------------- Katalog --- */ +const MWST = 19 + +const CATS = [ + { + id: 'web', name: 'Website', kenso: false, + hint: 'Alles, was zu einer Website gehört. Das Grundpaket deckt Startseite und drei Unterseiten ab.', + groups: [ + { g: 'Grundlage', items: [ + { id: 'w1', n: 'Grundpaket Website', d: 'Startseite und bis zu 3 Unterseiten, Texte, Design, mobil, Impressum und Datenschutz', h: 2 }, + { id: 'w2', n: 'Weitere Unterseite inkl. Text', d: 'Anzahl eintragen', h: 0.25, q: true }, + { id: 'w3', n: 'Bilder beschaffen und aufbereiten', h: 0.5 }, + { id: 'w4', n: 'Logo neu gestalten', d: 'einfaches Logo, kein Corporate Design', h: 0.5 }, + { id: 'w5', n: 'Domain registrieren und einrichten', h: 0.25 }, + { id: 'w6', n: 'Bestehende Domain umziehen', d: 'Relaunch: Kunde bringt Domain mit', h: 0.5 }, + { id: 'w7', n: 'Inhalte von der alten Seite übernehmen', d: 'Relaunch', h: 1 }, + { id: 'w8', n: 'E-Mail-Postfächer einrichten', h: 0.5 }, + ] }, + { g: 'Funktionen', items: [ + { id: 'f1', n: 'Kontaktformular', h: 0.25 }, + { id: 'f2', n: 'Anfahrt, Karte, Öffnungszeiten', h: 0.25 }, + { id: 'f3', n: 'Bildergalerie oder Referenzen', h: 0.5 }, + { id: 'f4', n: 'Google-Bewertungen einbinden', h: 0.5 }, + { id: 'f5', n: 'Cookie-Banner und Consent-Tool', h: 0.5 }, + { id: 'f6', n: 'Newsletter-Anmeldung', h: 0.5 }, + { id: 'f7', n: 'Stellenanzeigen mit Bewerbungsformular', h: 0.75 }, + { id: 'f8', n: 'Blog- oder Newsbereich', h: 1 }, + { id: 'f9', n: 'Terminbuchung', h: 1.5 }, + { id: 'f10', n: 'Weitere Sprache', d: 'Anzahl der zusätzlichen Sprachen', h: 1, q: true }, + ] }, + { g: 'Sichtbarkeit', items: [ + { id: 's1', n: 'Google-Profil einrichten oder aufräumen', h: 0.75 }, + { id: 's2', n: 'SEO-Grundoptimierung', h: 1 }, + { id: 's3', n: 'Statistik und Tracking einrichten', h: 0.5 }, + { id: 's4', n: 'Google Ads einrichten', d: 'Werbebudget zahlt der Kunde selbst', h: 1.5 }, + ] }, + ], + }, + { + id: 'shop', name: 'Onlineshop', kenso: true, + hint: 'Shop-Positionen stimmt Kenso mit ab — du nimmst hier nur den Bedarf auf.', + groups: [ + { g: 'Shop', items: [ + { id: 'o1', n: 'Shop-Grundeinrichtung', d: 'Warenkorb, Kasse, Bestellabwicklung', h: 6 }, + { id: 'o2', n: 'Produkte anlegen', d: 'Anzahl in 25er-Schritten', h: 1, q: true }, + { id: 'o3', n: 'Zahlungsanbindung', h: 1.5 }, + { id: 'o4', n: 'Versand- und Steuerregeln', h: 1 }, + { id: 'o5', n: 'Gutscheine und Rabatte', h: 1 }, + { id: 'o6', n: 'Kundenkonten im Shop', h: 1.5 }, + { id: 'o7', n: 'Anbindung an die Warenwirtschaft', h: 4 }, + ] }, + ], + }, + { + id: 'soft', name: 'Softwareprojekt', kenso: true, + hint: 'Eigenentwicklung: Umfang aufnehmen, den Preis bestätigt Kenso.', + groups: [ + { g: 'Fundament', items: [ + { id: 'p1', n: 'Projektfundament', d: 'Architektur, Datenmodell, Grundgerüst, Deployment', h: 8 }, + { id: 'p2', n: 'Datenbank aufsetzen und modellieren', h: 4 }, + { id: 'p3', n: 'Benutzerkonten und Login', h: 3 }, + { id: 'p4', n: 'Rollen und Rechte', h: 2.5 }, + ] }, + { g: 'Oberfläche und Verwaltung', items: [ + { id: 'a1', n: 'Admin-Panel Grundgerüst', h: 5 }, + { id: 'a2', n: 'Weiterer Verwaltungsbereich', d: 'je Bereich zum Anlegen, Bearbeiten, Löschen', h: 1.5, q: true }, + { id: 'a3', n: 'Produktkatalog mit Kategorien', h: 3 }, + { id: 'a4', n: 'Such- und Filterfunktion', h: 2 }, + { id: 'a5', n: 'Auswertungen und Dashboard', h: 3 }, + { id: 'a6', n: 'Import und Export', d: 'CSV oder Excel', h: 1.5 }, + { id: 'a7', n: 'Datei-Upload und Dokumentenablage', h: 1.5 }, + { id: 'a8', n: 'Benachrichtigungen per E-Mail', h: 1.5 }, + ] }, + { g: 'App', items: [ + { id: 'm1', n: 'Web-App', d: 'läuft im Browser, auch auf dem Handy', h: 6 }, + { id: 'm2', n: 'Mobile App zusätzlich', d: 'iOS und Android, inkl. Store-Veröffentlichung', h: 12 }, + { id: 'm3', n: 'Offline-Funktion', h: 4 }, + { id: 'm4', n: 'Push-Benachrichtigungen', h: 2 }, + ] }, + { g: 'Schnittstellen', items: [ + { id: 'i1', n: 'Schnittstelle zu einem Fremdsystem', d: 'Anzahl der Systeme', h: 4, q: true }, + { id: 'i2', n: 'Zahlungsanbieter anbinden', h: 2 }, + { id: 'i3', n: 'Datenübernahme aus einem Altsystem', h: 3 }, + ] }, + ], + }, + { + id: 'auto', name: 'Automatisierung und KI', kenso: true, + hint: 'Jeder Ablauf wird einzeln gezählt — lieber grob schätzen als weglassen.', + groups: [ + { g: 'Automatisierung', items: [ + { id: 'z1', n: 'Wiederkehrenden Ablauf automatisieren', d: 'je Ablauf, z. B. Terminbestätigung oder Datenweitergabe', h: 3, q: true }, + { id: 'z2', n: 'Automatische Angebots- oder Rechnungserzeugung', h: 3 }, + { id: 'z3', n: 'Dokumente automatisch auslesen', d: 'Lieferscheine, Rechnungen, Formulare', h: 4 }, + { id: 'z4', n: 'KI-Assistent mit eigenem Firmenwissen', d: 'laufende Nutzungskosten trägt der Kunde', h: 6 }, + ] }, + ], + }, + { + id: 'run', name: 'Laufende Kosten', kenso: false, monthly: true, + hint: 'Was nach dem Start jeden Monat läuft.', + groups: [ + { g: 'Monatlich', items: [ + { id: 'r1', n: 'Wartung und Hosting', d: 'Betrieb, Backups, 1 Stunde Änderungen pro Monat', m: 50 }, + { id: 'r2', n: 'Erhöhter Serverbedarf', d: 'eigene Datenbank oder App-Backend', m: 0, ask: true }, + { id: 'r3', n: 'KI-Nutzung', d: 'ab 50 € je Anliegen pro Monat – je nach Umfang mehr', m: 50, q: true }, + ] }, + ], + }, +] + +const STAFFEL = [{ max: 5, r: 150 }, { max: 15, r: 130 }, { max: 40, r: 115 }, { max: 1e9, r: 100 }] + +const PEOPLE = { + justin: { + name: 'Justin', role: 'Website, Angebote, Betreuung', initial: 'J', + slots: { + 1: ['09:00', '10:00', '11:00', '16:00', '17:00'], + 2: ['09:00', '10:00', '16:00', '17:00'], + 3: ['09:00', '10:00', '11:00', '16:00'], + 4: ['09:00', '10:00', '16:00', '17:00'], + 5: ['09:00', '10:00', '11:00'], + }, + }, + kenso: { + name: 'Kenso', role: 'Shop, Software, Automatisierung', initial: 'K', + slots: { 2: ['14:00', '15:00', '18:00'], 4: ['14:00', '15:00', '18:00'], 6: ['10:00', '11:00'] }, + }, +} + +const ITEM_BY_ID = {} +for (const cat of CATS) { + for (const grp of cat.groups) { + for (const item of grp.items) ITEM_BY_ID[item.id] = { cat, item } + } +} + +/* ----------------------------------------------------------------- State --- */ +const state = { + types: { web: true }, + sel: {}, + qty: {}, + wantTermin: false, + person: 'justin', + day: '', + slot: '', + termNote: '', + notes: '', + portalEmail: '', + portalPass: '', +} + +let customer = null +let storageKey = 'wk-kalkulation:neu' + +/* --------------------------------------------------------------- Helfer --- */ +function escapeHtml(str) { + return String(str ?? '') + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') +} + +function fmt(n) { + return Number(n || 0).toLocaleString('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) +} + +function eur(n) { + return Number(n || 0).toLocaleString('de-DE', { maximumFractionDigits: 0 }) + ' €' +} + +async function api(path) { + const response = await fetch(path, { + credentials: 'same-origin', + headers: { 'Content-Type': 'application/json' }, + }) + const data = await response.json().catch(() => ({})) + if (!response.ok) throw new Error(data.error || `Fehler ${response.status}`) + return data +} + +function el(id) { + return document.getElementById(id) +} + +/* ------------------------------------------------------- Zwischenspeicher --- */ +function saveState() { + try { + localStorage.setItem(storageKey, JSON.stringify({ ...state, savedAt: new Date().toISOString() })) + } catch { + /* z. B. privater Modus: dann eben ohne Zwischenspeicher */ + } +} + +function restoreState() { + try { + const raw = localStorage.getItem(storageKey) + if (!raw) return + const saved = JSON.parse(raw) + for (const key of Object.keys(state)) { + if (saved[key] !== undefined && saved[key] !== null) state[key] = saved[key] + } + } catch { + /* kaputter Eintrag: ignorieren */ + } +} + +/* --------------------------------------------------------------- Rechnen --- */ +function picked() { + const out = [] + for (const cat of CATS) { + for (const grp of cat.groups) { + for (const item of grp.items) { + if (!state.sel[item.id]) continue + const q = item.q ? (state.qty[item.id] || 1) : 1 + out.push({ + cat, item, q, + h: cat.monthly ? 0 : (item.h || 0) * q, + m: cat.monthly ? (item.m || 0) * q : 0, + }) + } + } + } + return out +} + +function totals() { + const sel = picked() + const hrsNum = sel.reduce((a, x) => a + x.h, 0) + const rateNum = STAFFEL.find((s) => hrsNum <= s.max).r + const netNum = hrsNum * rateNum + const monthlyItems = sel.filter((x) => x.cat.monthly) + const askServer = monthlyItems.some((x) => x.item.ask) + const mSum = monthlyItems.reduce((a, x) => a + x.m, 0) + const kenso = sel.some((x) => x.cat.kenso) + + let payTitle = 'Noch nichts gewählt' + let payText = 'Kreuze an, was der Kunde braucht.' + if (netNum >= 3000) { + payTitle = '40 / 40 / 20' + payText = `${eur(netNum * 0.4)} bei Auftrag, ${eur(netNum * 0.4)} bei Designfreigabe, ${eur(netNum * 0.2)} nach Fertigstellung.` + } else if (netNum >= 1000) { + payTitle = '50 % Anzahlung' + payText = `${eur(netNum * 0.5)} bei Auftragserteilung, der Rest nach der Abnahme.` + } else if (netNum > 0) { + payTitle = 'Keine Anzahlung' + payText = 'Eine Rechnung nach der Abnahme, zahlbar in 14 Tagen.' + } + + let flagTitle = 'Noch offen.' + let flagText = 'Sobald etwas gewählt ist, steht hier, ob du selbst abschließen kannst.' + let tone = 'neutral' + if (netNum > 0 || monthlyItems.length) { + if (kenso) { + tone = 'kenso' + flagTitle = 'Kenso gibt frei.' + flagText = 'Ein paar Positionen laufen über ihn. Preis und Termin nennst du erst nach seinem Okay — die Zahl hier ist deine Orientierung.' + } else if (hrsNum <= 5) { + tone = 'ok' + flagTitle = 'Kannst du selbst zusagen.' + flagText = 'Vorschau in unter einer Woche, fertig in maximal einem Monat.' + } else { + tone = 'talk' + flagTitle = 'Termin kurz abstimmen.' + flagText = `Bei ${fmt(hrsNum)} Stunden legt Kenso den Fertigstellungstermin fest. Der Preis steht.` + } + } + + return { + sel, hrsNum, rateNum, netNum, grossNum: netNum * (1 + MWST / 100), + mSum, askServer, monthlyItems, hasMonthly: monthlyItems.length > 0, + payTitle, payText, flagTitle, flagText, tone, + monthly: eur(mSum) + (askServer ? ' +' : ''), + } +} + +/* ---------------------------------------------------------------- Termin --- */ +function slotsFor(personId, date) { + return PEOPLE[personId].slots[date.getDay()] || [] +} + +function dayKey(date) { + return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}` +} + +function terminDays() { + const today = new Date() + today.setHours(0, 0, 0, 0) + const list = [] + for (let i = 1; i <= 14; i += 1) { + const d = new Date(today) + d.setDate(today.getDate() + i) + list.push({ key: dayKey(d), date: d, slots: slotsFor(state.person, d) }) + } + return list +} + +function selectedDay() { + return terminDays().find((d) => d.key === state.day && d.slots.length) || null +} + +function longDay(date) { + return date.toLocaleDateString('de-DE', { weekday: 'long', day: 'numeric', month: 'long' }) +} + +function terminLine() { + const day = selectedDay() + if (!state.wantTermin || !day || !state.slot) return '' + return `${longDay(day.date)} um ${state.slot} Uhr mit ${PEOPLE[state.person].name}` +} + +/* ---------------------------------------------------------- Zusammenfassung --- */ +function summaryText() { + const t = totals() + const lines = ['PROJEKTKALKULATION webklar', ''] + lines.push(`Kunde: ${customer?.name || '______________________'}`) + if (customer?.code) lines.push(`Kundennr.: ${customer.code}`) + if (customer?.email) lines.push(`E-Mail: ${customer.email}`) + if (customer?.phone) lines.push(`Telefon: ${customer.phone}`) + lines.push(`Datum: ${new Date().toLocaleDateString('de-DE')}`) + lines.push('', 'UMFANG') + + let cur = '' + for (const x of t.sel.filter((s) => !s.cat.monthly)) { + if (x.cat.name !== cur) { + cur = x.cat.name + lines.push('', `[${cur}]`) + } + lines.push(`- ${x.item.n}${x.q > 1 ? ` (${x.q}×)` : ''} – ${fmt(x.h)} Std`) + } + if (!t.sel.length) lines.push('(noch nichts gewählt)') + + lines.push('') + lines.push(`Summe: ${fmt(t.hrsNum)} Stunden`) + lines.push(`Stundensatz: ${eur(t.rateNum)}`) + lines.push(`Projektpreis: ${eur(t.netNum)} netto (${eur(t.grossNum)} brutto inkl. ${MWST} % MwSt.)`) + lines.push('', 'ZAHLUNG', `${t.payTitle} — ${t.payText}`) + + if (t.hasMonthly) { + lines.push('', 'LAUFEND') + for (const x of t.monthlyItems) { + lines.push(`- ${x.item.n}${x.q > 1 ? ` (${x.q}×)` : ''}: ${x.item.ask ? 'nach Last – rechnet Kenso aus' : `${eur(x.m)} pro Monat`}`) + } + lines.push(`Summe: ${eur(t.mSum)} pro Monat${t.askServer ? ' zzgl. Serveranteil – rechnet Kenso aus' : ''}`) + } + + lines.push('', 'HINWEIS', `${t.flagTitle} ${t.flagText}`) + + const term = terminLine() + if (term) { + lines.push('', 'TERMIN', term) + if (state.termNote) lines.push(`Thema: ${state.termNote}`) + } + if (state.notes) lines.push('', 'NOTIZEN', state.notes) + if (state.portalEmail || state.portalPass) { + lines.push('', 'KUNDENPORTAL (project.webklar.com)') + if (state.portalEmail) lines.push(`E-Mail: ${state.portalEmail}`) + if (state.portalPass) lines.push(`Passwort: ${state.portalPass}`) + } + lines.push('', 'Alle Preise netto zzgl. gesetzlicher Umsatzsteuer.') + return lines.join('\n') +} + +/* --------------------------------------------------------------- Rendern --- */ +function renderNumbers(t) { + el('hdr-net').textContent = eur(t.netNum) + el('hdr-hrs').textContent = fmt(t.hrsNum) + el('hdr-monthly').textContent = t.hasMonthly ? t.monthly : '–' + + el('hero-net').textContent = eur(t.netNum) + el('hero-sub').textContent = `netto · ${eur(t.grossNum)} brutto inkl. ${MWST} % MwSt.` + el('hero-hrs').textContent = `${fmt(t.hrsNum)} Std` + el('hero-rate').textContent = `Stundensatz ${eur(t.rateNum)}/Std` + el('hero-monthly').textContent = t.hasMonthly ? t.monthly : '–' + el('hero-monthly-sub').textContent = t.askServer + ? 'zzgl. Serveranteil, rechnet Kenso aus' + : (t.hasMonthly ? 'Wartung, Hosting, Backups' : 'nichts Laufendes gewählt') + + el('pay-title').textContent = t.payTitle + el('pay-text').textContent = t.payText + + const flag = el('flag') + flag.className = `flag ${t.tone === 'neutral' ? '' : t.tone}`.trim() + flag.innerHTML = `${escapeHtml(t.flagTitle)} ${escapeHtml(t.flagText)}` +} + +function renderTypeChips(t) { + el('type-chips').innerHTML = CATS.map((cat) => { + const on = Boolean(state.types[cat.id]) + const count = t.sel.filter((x) => x.cat.id === cat.id).length + return `` + }).join('') +} + +function itemHours(cat, item) { + const q = item.q ? (state.qty[item.id] || 1) : 1 + if (cat.monthly) return item.ask ? 'n. Last' : `${eur((item.m || 0) * q)}/Mon` + return `${fmt((item.h || 0) * q)} Std` +} + +function renderItem(cat, item) { + const on = Boolean(state.sel[item.id]) + const q = state.qty[item.id] || 1 + return `
+ ${on ? '✓' : ''} + + ${escapeHtml(item.n)} + ${item.d ? `${escapeHtml(item.d)}` : ''} + ${escapeHtml(itemHours(cat, item))} + ${item.q ? ` + + + + ` : ''} + +
` +} + +function renderSections() { + const visible = CATS.filter((cat) => state.types[cat.id]) + el('empty-hint').hidden = visible.length > 0 + el('sections').innerHTML = visible.map((cat) => ` +
+
+

${escapeHtml(cat.name)}

+ +
+

${escapeHtml(cat.hint)}

+ ${cat.groups.map((grp) => ` +
${escapeHtml(grp.g)}
+
${grp.items.map((item) => renderItem(cat, item)).join('')}
+ `).join('')} +
`).join('') +} + +function renderChosen(t) { + el('chosen-panel').hidden = t.sel.length === 0 + el('chosen-label').textContent = `Ausgewählt (${t.sel.length})` + el('chosen-list').innerHTML = t.sel.map((x) => { + const value = x.cat.monthly + ? (x.item.ask ? 'n. Last' : `${eur(x.m)}/M`) + : `${fmt(x.h)} Std` + return `${escapeHtml(x.item.n)}${x.q > 1 ? ` ×${x.q}` : ''}${escapeHtml(value)}` + }).join('') +} + +function renderTermin() { + el('termin-body').hidden = !state.wantTermin + document.querySelectorAll('#want-seg button').forEach((btn) => { + btn.classList.toggle('on', (btn.dataset.want === '1') === state.wantTermin) + }) + if (!state.wantTermin) return + + el('person-list').innerHTML = Object.keys(PEOPLE).map((id) => { + const p = PEOPLE[id] + const on = state.person === id + return `` + }).join('') + + const days = terminDays() + el('days-label').textContent = `Freie Tage — ${PEOPLE[state.person].name}` + el('day-list').innerHTML = days.map((d) => { + const free = d.slots.length + const on = state.day === d.key && free > 0 + return `` + }).join('') + + const day = selectedDay() + el('slot-wrap').hidden = !day + if (day) { + el('slots-label').textContent = `Uhrzeit am ${longDay(day.date)}` + el('slot-list').innerHTML = day.slots.map((s) => { + const on = state.slot === s + return `` + }).join('') + } + + renderTermStatus() +} + +function renderTermStatus() { + const box = el('term-status') + const line = terminLine() + if (line) { + box.className = 'term-status fixed' + box.innerHTML = `Termin steht: ${escapeHtml(line)}${state.termNote ? ` — Thema: ${escapeHtml(state.termNote)}` : ''}` + } else { + box.className = 'term-status' + box.innerHTML = 'Noch kein Termin. Wähle einen Tag und eine Uhrzeit — beides landet in der Zusammenfassung.' + } +} + +function render() { + const t = totals() + renderNumbers(t) + renderTypeChips(t) + renderSections() + renderChosen(t) + renderTermin() + saveState() +} + +/* --------------------------------------------------------------- Aktionen --- */ +function toggleItem(id) { + if (!ITEM_BY_ID[id]) return + state.sel[id] = !state.sel[id] + render() +} + +function changeQty(id, delta) { + state.qty[id] = Math.max(1, (state.qty[id] || 1) + delta) + if (!state.sel[id]) state.sel[id] = true + render() +} + +function setQty(id, value) { + const n = Math.max(1, parseInt(value, 10) || 1) + state.qty[id] = n + render() +} + +function openSummary() { + el('summary-text').value = summaryText() + el('summary-dialog').hidden = false +} + +function closeSummary() { + el('summary-dialog').hidden = true +} + +async function copySummary(btn) { + const text = el('summary-text').value + let ok = false + try { + await navigator.clipboard.writeText(text) + ok = true + } catch { + // Fallback fuer Browser ohne Clipboard-API bzw. ohne HTTPS-Kontext + const area = el('summary-text') + area.removeAttribute('readonly') + area.select() + ok = document.execCommand('copy') + area.setAttribute('readonly', '') + } + btn.textContent = ok ? 'Kopiert ✓' : 'Bitte manuell markieren' + setTimeout(() => { btn.textContent = 'Kopieren' }, 2200) +} + +function resetSelection() { + if (!confirm('Alle angekreuzten Positionen zurücksetzen? Notizen und Termin bleiben erhalten.')) return + state.sel = {} + state.qty = {} + render() +} + +/* ------------------------------------------------------------------- Init --- */ +function bindEvents() { + document.addEventListener('click', (event) => { + const target = event.target.closest('[data-act]') + if (!target) return + const act = target.dataset.act + + // Klicks auf die Anzahl-Steuerung treffen zuerst deren eigenes data-act, + // schalten die Karte also nicht mit um. + switch (act) { + case 'toggle-type': + state.types[target.dataset.id] = !state.types[target.dataset.id] + render() + break + case 'toggle-item': + toggleItem(target.dataset.id) + break + case 'qty': + changeQty(target.dataset.id, Number(target.dataset.delta)) + break + case 'want': + state.wantTermin = target.dataset.want === '1' + render() + break + case 'person': + state.person = target.dataset.id + state.day = '' + state.slot = '' + render() + break + case 'day': + state.day = target.dataset.key + state.slot = '' + render() + break + case 'slot': + state.slot = target.dataset.t + render() + break + case 'open-summary': openSummary(); break + case 'close-summary': closeSummary(); break + case 'copy': copySummary(target); break + case 'reset': resetSelection(); break + default: break + } + }) + + document.addEventListener('change', (event) => { + const input = event.target.closest('[data-act="qty-set"]') + if (!input) return + setQty(input.dataset.id, input.value) + }) + + // Karten sind divs mit role=button -> Tastatur nachruesten. + // Im Anzahl-Feld und auf den +/- Buttons darf das nicht greifen. + document.addEventListener('keydown', (event) => { + if (event.key !== 'Enter' && event.key !== ' ') return + if (event.target.closest('input, textarea, button')) return + const card = event.target.closest('[data-act="toggle-item"]') + if (!card) return + event.preventDefault() + toggleItem(card.dataset.id) + }) + + el('term-note').addEventListener('input', (event) => { + state.termNote = event.target.value + renderTermStatus() + saveState() + }) + el('notes').addEventListener('input', (event) => { + state.notes = event.target.value + saveState() + }) + el('portal-email').addEventListener('input', (event) => { + state.portalEmail = event.target.value + saveState() + }) + el('portal-pass').addEventListener('input', (event) => { + state.portalPass = event.target.value + saveState() + }) + + el('summary-dialog').addEventListener('click', (event) => { + if (event.target === el('summary-dialog')) closeSummary() + }) + document.addEventListener('keydown', (event) => { + if (event.key === 'Escape' && !el('summary-dialog').hidden) closeSummary() + }) +} + +async function initKalkulation() { + try { + const me = await api('/api/auth/me') + if (!me.authenticated || me.role !== 'admin') { + window.location.href = '/login.html' + return + } + } catch { + window.location.href = '/login.html' + return + } + + const customerId = new URLSearchParams(window.location.search).get('customer') || '' + if (customerId) { + storageKey = `wk-kalkulation:${customerId}` + try { + const data = await api(`/api/portal-admin/customers/${encodeURIComponent(customerId)}`) + customer = data.customer + } catch (err) { + console.warn('Kunde konnte nicht geladen werden:', err.message) + } + } + + restoreState() + + if (customer) { + const parts = [customer.name, customer.location].filter(Boolean) + el('kal-lead').innerHTML = + `Lead: ${escapeHtml(parts.join(' · '))}` + el('hero-kicker').textContent = `Angebot für ${customer.name}` + if (!state.portalEmail) state.portalEmail = customer.email || '' + } + + el('term-note').value = state.termNote + el('notes').value = state.notes + el('portal-email').value = state.portalEmail + el('portal-pass').value = state.portalPass + + bindEvents() + render() +} + +window.initKalkulation = initKalkulation diff --git a/server/routes/portalAdmin.js b/server/routes/portalAdmin.js index 0fcc2c3..a5aee9b 100644 --- a/server/routes/portalAdmin.js +++ b/server/routes/portalAdmin.js @@ -185,6 +185,28 @@ router.get('/customers', async (req, res) => { } }) +// --------------------------------------------- Einzelner Kunde (Stammdaten) -- +// Fuer die Rechnungsanalyse (kalkulation.html), die den Lead vorbefuellt. +router.get('/customers/:customerId', async (req, res) => { + try { + const customer = await getDocument(config.collections.customers, req.params.customerId) + return res.json({ + customer: { + id: customer.$id, + code: customer.code || '', + name: customer.name || '', + email: customer.email || '', + phone: customer.phone || '', + location: customer.location || '', + customerStatus: customer.customerStatus || '', + }, + }) + } catch (err) { + const status = err.status || 500 + return res.status(status).json({ error: err.message || 'Kunde konnte nicht geladen werden' }) + } +}) + // ------------------------------------------- Kundennummer/Stammdaten pflegen -- router.patch('/customers/:customerId', async (req, res) => { const { code, location, phone } = req.body || {}