Compare commits
189
Commits
69df9e7046
...
main
No files matched your search
@@ -5,6 +5,10 @@ export const CONSENT_KEY = 'wk-consent';
|
||||
export const CONSENT_MAX_AGE = 183 * 86400000;
|
||||
export const BANNER_VERSION = '2026-10-02';
|
||||
export const BANNER_TEXT = 'Um unsere Website zu verbessern, zählen wir Seitenaufrufe mit Land und Gerätetyp, ohne Kennung. Mit Deiner Zustimmung messen wir zusätzlich, von welcher Website Du kommst, wie lange Du bleibst, wie weit Du scrollst und wo Du klickst (Heatmap), mit einer täglich wechselnden Kennung. Die Messung betreibt für uns die WEBklar GbR in Deutschland, ohne Cookies und ohne Werbung. Nach „Ablehnen“ zählen wir nur noch, dass Du abgelehnt hast. Deine Zustimmung kannst Du jederzeit über „Datenschutz-Einstellungen“ mit Wirkung für die Zukunft widerrufen. Details: ';
|
||||
// Eigene Websites der WEBklar GbR (webklar.com, Skript-Attribut data-variant="eigen"): Sie-Form, WEBklar misst selbst
|
||||
// (keine Auftragsverarbeitung). Eigene Fassung, damit eine Textaenderung nur dort neu fragt.
|
||||
export const BANNER_VERSION_EIGEN = '2026-10-17';
|
||||
export const BANNER_TEXT_EIGEN = 'Um unsere Website zu verbessern, zählen wir Seitenaufrufe mit Land und Gerätetyp, ohne Kennung. Mit Ihrer Zustimmung messen wir zusätzlich, von welcher Website Sie kommen, wie lange Sie bleiben, wie weit Sie scrollen und wo Sie klicken (Heatmap), mit einer täglich wechselnden Kennung. Wir messen selbst auf unserem Server in Deutschland, ohne Cookies und ohne Werbung. Nach „Ablehnen“ zählen wir nur noch, dass Sie abgelehnt haben. Ihre Zustimmung können Sie jederzeit über „Datenschutz-Einstellungen“ mit Wirkung für die Zukunft widerrufen. Details: ';
|
||||
export const BANNER_LINK_LABEL = 'Datenschutz';
|
||||
export const BANNER_ARIA = 'Einwilligung zur Reichweitenmessung';
|
||||
export const SETTINGS_LABEL = 'Datenschutz-Einstellungen';
|
||||
@@ -88,13 +92,20 @@ export function contactKind(a, ownHost) {
|
||||
return stripWww(host) === stripWww(own || '') ? null : 'out';
|
||||
}
|
||||
|
||||
export function readConsent(storage, now) {
|
||||
// Banner-Fassung je Variante: 'eigen' fuer WEBklars eigene Websites, alles andere ist die Kunden-Fassung.
|
||||
export function bannerFor(variant) {
|
||||
return variant === 'eigen'
|
||||
? { version: BANNER_VERSION_EIGEN, text: BANNER_TEXT_EIGEN }
|
||||
: { version: BANNER_VERSION, text: BANNER_TEXT };
|
||||
}
|
||||
|
||||
export function readConsent(storage, now, version = BANNER_VERSION) {
|
||||
try {
|
||||
const raw = storage.getItem(CONSENT_KEY);
|
||||
if (!raw) return null;
|
||||
const c = JSON.parse(raw);
|
||||
if (!c || c.v !== 1 || (c.a !== 'accept' && c.a !== 'reject') || typeof c.t !== 'number' || !isFinite(c.t)) return null;
|
||||
if (c.bv !== BANNER_VERSION) return null; // andere/fehlende Fassung des Banner-Textes: neu fragen
|
||||
if (c.bv !== version) return null; // andere/fehlende Fassung des Banner-Textes: neu fragen
|
||||
const age = now - c.t;
|
||||
if (age < 0 || age >= CONSENT_MAX_AGE) return null;
|
||||
return { v: 1, a: c.a, t: c.t, bv: c.bv };
|
||||
|
||||
Vendored
+13
-2
@@ -8,6 +8,10 @@ const CONSENT_KEY = 'wk-consent';
|
||||
const CONSENT_MAX_AGE = 183 * 86400000;
|
||||
const BANNER_VERSION = '2026-10-02';
|
||||
const BANNER_TEXT = 'Um unsere Website zu verbessern, zählen wir Seitenaufrufe mit Land und Gerätetyp, ohne Kennung. Mit Deiner Zustimmung messen wir zusätzlich, von welcher Website Du kommst, wie lange Du bleibst, wie weit Du scrollst und wo Du klickst (Heatmap), mit einer täglich wechselnden Kennung. Die Messung betreibt für uns die WEBklar GbR in Deutschland, ohne Cookies und ohne Werbung. Nach „Ablehnen“ zählen wir nur noch, dass Du abgelehnt hast. Deine Zustimmung kannst Du jederzeit über „Datenschutz-Einstellungen“ mit Wirkung für die Zukunft widerrufen. Details: ';
|
||||
// Eigene Websites der WEBklar GbR (webklar.com, Skript-Attribut data-variant="eigen"): Sie-Form, WEBklar misst selbst
|
||||
// (keine Auftragsverarbeitung). Eigene Fassung, damit eine Textaenderung nur dort neu fragt.
|
||||
const BANNER_VERSION_EIGEN = '2026-10-17';
|
||||
const BANNER_TEXT_EIGEN = 'Um unsere Website zu verbessern, zählen wir Seitenaufrufe mit Land und Gerätetyp, ohne Kennung. Mit Ihrer Zustimmung messen wir zusätzlich, von welcher Website Sie kommen, wie lange Sie bleiben, wie weit Sie scrollen und wo Sie klicken (Heatmap), mit einer täglich wechselnden Kennung. Wir messen selbst auf unserem Server in Deutschland, ohne Cookies und ohne Werbung. Nach „Ablehnen“ zählen wir nur noch, dass Sie abgelehnt haben. Ihre Zustimmung können Sie jederzeit über „Datenschutz-Einstellungen“ mit Wirkung für die Zukunft widerrufen. Details: ';
|
||||
const BANNER_LINK_LABEL = 'Datenschutz';
|
||||
const BANNER_ARIA = 'Einwilligung zur Reichweitenmessung';
|
||||
const SETTINGS_LABEL = 'Datenschutz-Einstellungen';
|
||||
@@ -91,13 +95,20 @@ function contactKind(a, ownHost) {
|
||||
return stripWww(host) === stripWww(own || '') ? null : 'out';
|
||||
}
|
||||
|
||||
function readConsent(storage, now) {
|
||||
// Banner-Fassung je Variante: 'eigen' fuer WEBklars eigene Websites, alles andere ist die Kunden-Fassung.
|
||||
function bannerFor(variant) {
|
||||
return variant === 'eigen'
|
||||
? { version: BANNER_VERSION_EIGEN, text: BANNER_TEXT_EIGEN }
|
||||
: { version: BANNER_VERSION, text: BANNER_TEXT };
|
||||
}
|
||||
|
||||
function readConsent(storage, now, version = BANNER_VERSION) {
|
||||
try {
|
||||
const raw = storage.getItem(CONSENT_KEY);
|
||||
if (!raw) return null;
|
||||
const c = JSON.parse(raw);
|
||||
if (!c || c.v !== 1 || (c.a !== 'accept' && c.a !== 'reject') || typeof c.t !== 'number' || !isFinite(c.t)) return null;
|
||||
if (c.bv !== BANNER_VERSION) return null; // andere/fehlende Fassung des Banner-Textes: neu fragen
|
||||
if (c.bv !== version) return null; // andere/fehlende Fassung des Banner-Textes: neu fragen
|
||||
const age = now - c.t;
|
||||
if (age < 0 || age >= CONSENT_MAX_AGE) return null;
|
||||
return { v: 1, a: c.a, t: c.t, bv: c.bv };
|
||||
|
||||
Vendored
+24
-8
@@ -8,6 +8,10 @@ const CONSENT_KEY = 'wk-consent';
|
||||
const CONSENT_MAX_AGE = 183 * 86400000;
|
||||
const BANNER_VERSION = '2026-10-02';
|
||||
const BANNER_TEXT = 'Um unsere Website zu verbessern, zählen wir Seitenaufrufe mit Land und Gerätetyp, ohne Kennung. Mit Deiner Zustimmung messen wir zusätzlich, von welcher Website Du kommst, wie lange Du bleibst, wie weit Du scrollst und wo Du klickst (Heatmap), mit einer täglich wechselnden Kennung. Die Messung betreibt für uns die WEBklar GbR in Deutschland, ohne Cookies und ohne Werbung. Nach „Ablehnen“ zählen wir nur noch, dass Du abgelehnt hast. Deine Zustimmung kannst Du jederzeit über „Datenschutz-Einstellungen“ mit Wirkung für die Zukunft widerrufen. Details: ';
|
||||
// Eigene Websites der WEBklar GbR (webklar.com, Skript-Attribut data-variant="eigen"): Sie-Form, WEBklar misst selbst
|
||||
// (keine Auftragsverarbeitung). Eigene Fassung, damit eine Textaenderung nur dort neu fragt.
|
||||
const BANNER_VERSION_EIGEN = '2026-10-17';
|
||||
const BANNER_TEXT_EIGEN = 'Um unsere Website zu verbessern, zählen wir Seitenaufrufe mit Land und Gerätetyp, ohne Kennung. Mit Ihrer Zustimmung messen wir zusätzlich, von welcher Website Sie kommen, wie lange Sie bleiben, wie weit Sie scrollen und wo Sie klicken (Heatmap), mit einer täglich wechselnden Kennung. Wir messen selbst auf unserem Server in Deutschland, ohne Cookies und ohne Werbung. Nach „Ablehnen“ zählen wir nur noch, dass Sie abgelehnt haben. Ihre Zustimmung können Sie jederzeit über „Datenschutz-Einstellungen“ mit Wirkung für die Zukunft widerrufen. Details: ';
|
||||
const BANNER_LINK_LABEL = 'Datenschutz';
|
||||
const BANNER_ARIA = 'Einwilligung zur Reichweitenmessung';
|
||||
const SETTINGS_LABEL = 'Datenschutz-Einstellungen';
|
||||
@@ -91,13 +95,20 @@ function contactKind(a, ownHost) {
|
||||
return stripWww(host) === stripWww(own || '') ? null : 'out';
|
||||
}
|
||||
|
||||
function readConsent(storage, now) {
|
||||
// Banner-Fassung je Variante: 'eigen' fuer WEBklars eigene Websites, alles andere ist die Kunden-Fassung.
|
||||
function bannerFor(variant) {
|
||||
return variant === 'eigen'
|
||||
? { version: BANNER_VERSION_EIGEN, text: BANNER_TEXT_EIGEN }
|
||||
: { version: BANNER_VERSION, text: BANNER_TEXT };
|
||||
}
|
||||
|
||||
function readConsent(storage, now, version = BANNER_VERSION) {
|
||||
try {
|
||||
const raw = storage.getItem(CONSENT_KEY);
|
||||
if (!raw) return null;
|
||||
const c = JSON.parse(raw);
|
||||
if (!c || c.v !== 1 || (c.a !== 'accept' && c.a !== 'reject') || typeof c.t !== 'number' || !isFinite(c.t)) return null;
|
||||
if (c.bv !== BANNER_VERSION) return null; // andere/fehlende Fassung des Banner-Textes: neu fragen
|
||||
if (c.bv !== version) return null; // andere/fehlende Fassung des Banner-Textes: neu fragen
|
||||
const age = now - c.t;
|
||||
if (age < 0 || age >= CONSENT_MAX_AGE) return null;
|
||||
return { v: 1, a: c.a, t: c.t, bv: c.bv };
|
||||
@@ -134,6 +145,8 @@ function main() {
|
||||
if (!/^[a-z0-9]{16,40}$/.test(site)) return;
|
||||
const colorAttr = String(SELF.getAttribute('data-color') || '');
|
||||
const accent = contrastOk(colorAttr) ? colorAttr : '#111111';
|
||||
// data-variant="eigen": Banner-Fassung fuer WEBklars eigene Websites (Sie-Form, ohne Auftragsverarbeiter)
|
||||
const banner = bannerFor(String(SELF.getAttribute('data-variant') || ''));
|
||||
const src = String(SELF.src || SELF.getAttribute('src') || '').replace(/[?#].*$/, '');
|
||||
const base = src.replace(/[^/]*$/, '');
|
||||
const endpoint = base + 'e';
|
||||
@@ -173,12 +186,12 @@ function main() {
|
||||
const dnt = (v) => v === '1' || v === 'yes';
|
||||
const optOut = navigator.globalPrivacyControl === true || dnt(navigator.doNotTrack) || dnt(WIN.doNotTrack) || dnt(navigator.msDoNotTrack);
|
||||
if (optOut) return; // wie Ablehnen: nichts senden, kein Banner, keine Hooks
|
||||
const stored = readConsent(storage, Date.now());
|
||||
const stored = readConsent(storage, Date.now(), banner.version);
|
||||
let memChoice = null;
|
||||
let tracking = false;
|
||||
// Wahl bei jedem Aufruf frisch lesen (Speicher, sonst nur fuer diese Seite gemerkt)
|
||||
function choiceNow() {
|
||||
const c = readConsent(storage, Date.now());
|
||||
const c = readConsent(storage, Date.now(), banner.version);
|
||||
return c ? c.a : (memChoice ? memChoice.a : null);
|
||||
}
|
||||
const counting = () => choiceNow() !== 'reject';
|
||||
@@ -207,7 +220,10 @@ function main() {
|
||||
if (navTimer || !counting()) return;
|
||||
navTimer = setTimeout(safe(function () {
|
||||
navTimer = null;
|
||||
if (counting() && cleanPath(WIN.location.pathname) !== lastPath) sendPv(false);
|
||||
if (!counting() || cleanPath(WIN.location.pathname) === lastPath) return;
|
||||
// SPA-Seitenwechsel (Router mit preventDefault, onLinkClick greift dann nicht): en der vorigen Seite zuerst
|
||||
sendEn();
|
||||
sendPv(false);
|
||||
}), 300);
|
||||
}
|
||||
let hooksOn = false;
|
||||
@@ -312,7 +328,7 @@ function main() {
|
||||
|
||||
function choose(a) {
|
||||
memChoice = null;
|
||||
try { storage.setItem(CONSENT_KEY, JSON.stringify({ v: 1, a, t: Date.now(), bv: BANNER_VERSION })); } catch (e) { memChoice = { a }; /* ohne Speicher: gilt nur fuer diese Seite */ }
|
||||
try { storage.setItem(CONSENT_KEY, JSON.stringify({ v: 1, a, t: Date.now(), bv: banner.version })); } catch (e) { memChoice = { a }; /* ohne Speicher: gilt nur fuer diese Seite */ }
|
||||
send({ t: 'bn', site, a });
|
||||
if (a === 'accept') { startTracking(); onAccept(); } else stopTracking();
|
||||
closeBanner();
|
||||
@@ -352,7 +368,7 @@ function main() {
|
||||
const root = bannerHost.attachShadow({ mode: 'open' });
|
||||
root.appendChild(el('style', {}, CSS));
|
||||
const bar = el('div', { class: 'bar', role: 'dialog', 'aria-label': BANNER_ARIA });
|
||||
const p = el('p', { class: 't' }, BANNER_TEXT);
|
||||
const p = el('p', { class: 't' }, banner.text);
|
||||
p.appendChild(el('a', { href: '/datenschutz-analyse.html' }, BANNER_LINK_LABEL));
|
||||
const row = el('div', { class: 'r' });
|
||||
const no = el('button', { type: 'button' }, 'Ablehnen');
|
||||
@@ -433,7 +449,7 @@ function main() {
|
||||
// Einwilligung in anderem Tab geaendert
|
||||
WIN.addEventListener('storage', safe(function (e) {
|
||||
if (e.key !== null && e.key !== CONSENT_KEY) return;
|
||||
const c = readConsent(storage, Date.now());
|
||||
const c = readConsent(storage, Date.now(), banner.version);
|
||||
if (choiceNow() === 'accept') { startTracking(); onAccept(); } else stopTracking();
|
||||
if (c) closeBanner();
|
||||
}));
|
||||
|
||||
+12
-7
@@ -1,7 +1,7 @@
|
||||
// WEBklar Analyse: Einwilligungs-Banner + Ereignisse. Laeuft auf oeffentlichen Kunden-Websites.
|
||||
// Keine Cookies, keine Kennung im Browser. Ohne Wahl nur pv {t,site,path}; ref/vp/cx/ck/ct/en nur nach Annehmen;
|
||||
// nach Ablehnen oder bei GPC/DNT geht nichts mehr raus.
|
||||
import { cleanPath, vpClass, refHost, cssPath, contactKind, linkHost, readConsent, contrastOk, CONSENT_KEY, BANNER_VERSION, BANNER_TEXT, BANNER_LINK_LABEL, BANNER_ARIA, SETTINGS_LABEL } from './core.js';
|
||||
import { cleanPath, vpClass, refHost, cssPath, contactKind, linkHost, readConsent, contrastOk, bannerFor, CONSENT_KEY, BANNER_LINK_LABEL, BANNER_ARIA, SETTINGS_LABEL } from './core.js';
|
||||
|
||||
function main() {
|
||||
const DOC = document;
|
||||
@@ -15,6 +15,8 @@ function main() {
|
||||
if (!/^[a-z0-9]{16,40}$/.test(site)) return;
|
||||
const colorAttr = String(SELF.getAttribute('data-color') || '');
|
||||
const accent = contrastOk(colorAttr) ? colorAttr : '#111111';
|
||||
// data-variant="eigen": Banner-Fassung fuer WEBklars eigene Websites (Sie-Form, ohne Auftragsverarbeiter)
|
||||
const banner = bannerFor(String(SELF.getAttribute('data-variant') || ''));
|
||||
const src = String(SELF.src || SELF.getAttribute('src') || '').replace(/[?#].*$/, '');
|
||||
const base = src.replace(/[^/]*$/, '');
|
||||
const endpoint = base + 'e';
|
||||
@@ -54,12 +56,12 @@ function main() {
|
||||
const dnt = (v) => v === '1' || v === 'yes';
|
||||
const optOut = navigator.globalPrivacyControl === true || dnt(navigator.doNotTrack) || dnt(WIN.doNotTrack) || dnt(navigator.msDoNotTrack);
|
||||
if (optOut) return; // wie Ablehnen: nichts senden, kein Banner, keine Hooks
|
||||
const stored = readConsent(storage, Date.now());
|
||||
const stored = readConsent(storage, Date.now(), banner.version);
|
||||
let memChoice = null;
|
||||
let tracking = false;
|
||||
// Wahl bei jedem Aufruf frisch lesen (Speicher, sonst nur fuer diese Seite gemerkt)
|
||||
function choiceNow() {
|
||||
const c = readConsent(storage, Date.now());
|
||||
const c = readConsent(storage, Date.now(), banner.version);
|
||||
return c ? c.a : (memChoice ? memChoice.a : null);
|
||||
}
|
||||
const counting = () => choiceNow() !== 'reject';
|
||||
@@ -88,7 +90,10 @@ function main() {
|
||||
if (navTimer || !counting()) return;
|
||||
navTimer = setTimeout(safe(function () {
|
||||
navTimer = null;
|
||||
if (counting() && cleanPath(WIN.location.pathname) !== lastPath) sendPv(false);
|
||||
if (!counting() || cleanPath(WIN.location.pathname) === lastPath) return;
|
||||
// SPA-Seitenwechsel (Router mit preventDefault, onLinkClick greift dann nicht): en der vorigen Seite zuerst
|
||||
sendEn();
|
||||
sendPv(false);
|
||||
}), 300);
|
||||
}
|
||||
let hooksOn = false;
|
||||
@@ -193,7 +198,7 @@ function main() {
|
||||
|
||||
function choose(a) {
|
||||
memChoice = null;
|
||||
try { storage.setItem(CONSENT_KEY, JSON.stringify({ v: 1, a, t: Date.now(), bv: BANNER_VERSION })); } catch (e) { memChoice = { a }; /* ohne Speicher: gilt nur fuer diese Seite */ }
|
||||
try { storage.setItem(CONSENT_KEY, JSON.stringify({ v: 1, a, t: Date.now(), bv: banner.version })); } catch (e) { memChoice = { a }; /* ohne Speicher: gilt nur fuer diese Seite */ }
|
||||
send({ t: 'bn', site, a });
|
||||
if (a === 'accept') { startTracking(); onAccept(); } else stopTracking();
|
||||
closeBanner();
|
||||
@@ -233,7 +238,7 @@ function main() {
|
||||
const root = bannerHost.attachShadow({ mode: 'open' });
|
||||
root.appendChild(el('style', {}, CSS));
|
||||
const bar = el('div', { class: 'bar', role: 'dialog', 'aria-label': BANNER_ARIA });
|
||||
const p = el('p', { class: 't' }, BANNER_TEXT);
|
||||
const p = el('p', { class: 't' }, banner.text);
|
||||
p.appendChild(el('a', { href: '/datenschutz-analyse.html' }, BANNER_LINK_LABEL));
|
||||
const row = el('div', { class: 'r' });
|
||||
const no = el('button', { type: 'button' }, 'Ablehnen');
|
||||
@@ -314,7 +319,7 @@ function main() {
|
||||
// Einwilligung in anderem Tab geaendert
|
||||
WIN.addEventListener('storage', safe(function (e) {
|
||||
if (e.key !== null && e.key !== CONSENT_KEY) return;
|
||||
const c = readConsent(storage, Date.now());
|
||||
const c = readConsent(storage, Date.now(), banner.version);
|
||||
if (choiceNow() === 'accept') { startTracking(); onAccept(); } else stopTracking();
|
||||
if (c) closeBanner();
|
||||
}));
|
||||
|
||||
@@ -7,7 +7,7 @@ import { dirname, join } from 'node:path';
|
||||
import {
|
||||
vpClass, cleanPath, refHost, cssPath, contactKind, readConsent, contrastOk, escapeHtml,
|
||||
} from '../client/core.js';
|
||||
import { BANNER_VERSION, BANNER_TEXT, BANNER_LINK_LABEL, BANNER_ARIA, SETTINGS_LABEL } from '../client/core.js';
|
||||
import { BANNER_VERSION, BANNER_TEXT, BANNER_LINK_LABEL, BANNER_ARIA, SETTINGS_LABEL, BANNER_VERSION_EIGEN, BANNER_TEXT_EIGEN, bannerFor } from '../client/core.js';
|
||||
import { buildAll } from '../client/build.js';
|
||||
|
||||
const BV = '2026-10-02';
|
||||
@@ -170,12 +170,12 @@ function findAll(root, pred) { const out = []; walk(root, (n) => { if (pred(n))
|
||||
const btn = (root, label) => findAll(root, (n) => n.tagName === 'BUTTON' && n.textContent === label)[0];
|
||||
const settingsBtn = (h) => findAll(h.body, (n) => n.tagName === 'BUTTON' && n.attrs['aria-label'] === SETTINGS_LABEL)[0];
|
||||
|
||||
function harness({ gpc, dnt, stored, search = '', src = 'https://project.webklar.com/a/w.js', withConsentLink = false, bannerHeight = 0, bodyPad } = {}) {
|
||||
function harness({ gpc, dnt, stored, search = '', src = 'https://project.webklar.com/a/w.js', withConsentLink = false, bannerHeight = 0, bodyPad, variant } = {}) {
|
||||
const sent = []; const reads = { referrer: 0, innerWidth: 0 }; const beacon = []; const fetches = []; const store = {}; let now = 1_700_000_000_000;
|
||||
if (stored) store['wk-consent'] = JSON.stringify(stored);
|
||||
const body = makeEl('body'); if (bodyPad !== undefined) body.style.paddingBottom = bodyPad; const html = makeEl('html');
|
||||
const timers = [];
|
||||
const script = makeEl('script'); script.setAttribute('src', src); script.setAttribute('data-site', 'abcdef0123456789abcd'); script.setAttribute('data-color', '#0a5a8a'); script.src = src;
|
||||
const script = makeEl('script'); script.setAttribute('src', src); script.setAttribute('data-site', 'abcdef0123456789abcd'); script.setAttribute('data-color', '#0a5a8a'); if (variant !== undefined) script.setAttribute('data-variant', variant); script.src = src;
|
||||
const docL = {}; const winL = {};
|
||||
const link = withConsentLink ? makeEl('a') : null; if (link) link.setAttribute('data-webklar-consent', '');
|
||||
const injected = [];
|
||||
@@ -565,7 +565,9 @@ test('w.js: SPA-pv bei akzeptierter Einwilligung; nach Widerruf kein en', () =>
|
||||
const h = harness({ stored: { v: 1, a: 'accept', bv: BV, t: 1_700_000_000_000 - 1000 } });
|
||||
h.win.location.pathname = '/zwei';
|
||||
h.history.pushState(null, '', '/zwei'); h.timers.splice(0).forEach((f) => f());
|
||||
assert.deepEqual(h.sent.map((s) => s.d.path), ['/start', '/zwei']);
|
||||
// en der vorigen Seite vor dem pv der neuen (Router-Links verhindern den Standard-Klick, onLinkClick greift nicht)
|
||||
assert.deepEqual(h.sent.map((s) => `${s.d.t}:${s.d.path}`), ['pv:/start', 'en:/start', 'pv:/zwei']);
|
||||
assert.equal(h.sent[1].d.scroll, 55);
|
||||
h.ck(target());
|
||||
assert.equal(h.sent.at(-1).d.path, '/zwei');
|
||||
h.store['wk-consent'] = JSON.stringify({ v: 1, a: 'reject', bv: BV, t: 1_700_000_000_000 });
|
||||
@@ -641,3 +643,67 @@ test('w.js: ohne Einwilligung oder nach Widerruf kein fruehes en', () => {
|
||||
g.ck(lnk('/kontakt'));
|
||||
assert.equal(enCount(g), 0);
|
||||
});
|
||||
|
||||
// ---------- Variante "eigen" (WEBklars eigene Websites, z. B. webklar.com) ----------
|
||||
test('core: bannerFor und readConsent je Fassung', () => {
|
||||
assert.deepEqual(bannerFor('eigen'), { version: BANNER_VERSION_EIGEN, text: BANNER_TEXT_EIGEN });
|
||||
for (const v of ['', 'kunde', 'EIGEN', 'x', undefined]) assert.deepEqual(bannerFor(v), { version: BANNER_VERSION, text: BANNER_TEXT });
|
||||
assert.equal(BANNER_VERSION_EIGEN, '2026-10-17');
|
||||
assert.notEqual(BANNER_VERSION_EIGEN, BANNER_VERSION);
|
||||
const now = 1_700_000_000_000;
|
||||
const st = (o) => ({ getItem: () => JSON.stringify(o) });
|
||||
assert.equal(readConsent(st({ v: 1, a: 'accept', bv: BV, t: now }), now, BANNER_VERSION_EIGEN), null, 'Kunden-Fassung gilt nicht fuer eigen');
|
||||
assert.equal(readConsent(st({ v: 1, a: 'accept', bv: BANNER_VERSION_EIGEN, t: now }), now, BANNER_VERSION_EIGEN).a, 'accept');
|
||||
assert.equal(readConsent(st({ v: 1, a: 'accept', bv: BANNER_VERSION_EIGEN, t: now }), now), null, 'ohne Fassung gilt die Kunden-Fassung');
|
||||
});
|
||||
|
||||
test('core: Banner-Text eigen (Sie-Form, ohne Auftragsverarbeiter)', () => {
|
||||
assert.match(BANNER_TEXT_EIGEN, /^Um unsere Website zu verbessern, zählen wir Seitenaufrufe mit Land und Gerätetyp, ohne Kennung\./);
|
||||
assert.match(BANNER_TEXT_EIGEN, /Wir messen selbst auf unserem Server in Deutschland, ohne Cookies und ohne Werbung\./);
|
||||
assert.match(BANNER_TEXT_EIGEN, /Nach „Ablehnen“ zählen wir nur noch, dass Sie abgelehnt haben\./);
|
||||
assert.match(BANNER_TEXT_EIGEN, /über „Datenschutz-Einstellungen“ mit Wirkung für die Zukunft widerrufen\. Details: $/);
|
||||
assert.doesNotMatch(BANNER_TEXT_EIGEN, /\b(Du|Dein|Deine|Deiner|Dich|Dir)\b/);
|
||||
assert.doesNotMatch(BANNER_TEXT_EIGEN, /betreibt für uns/);
|
||||
});
|
||||
|
||||
test('w.js: data-variant="eigen" => Sie-Text, eigene Fassung im Speicher', () => {
|
||||
const h = harness({ variant: 'eigen' });
|
||||
const host = hostEl(h);
|
||||
const p = findAll(host.shadow, (n) => n.tagName === 'P')[0];
|
||||
assert.ok(p.textContent.startsWith(BANNER_TEXT_EIGEN));
|
||||
assert.ok(!p.textContent.startsWith(BANNER_TEXT));
|
||||
btn(host.shadow, 'Annehmen').click();
|
||||
assert.deepEqual(JSON.parse(h.store['wk-consent']), { v: 1, a: 'accept', bv: BANNER_VERSION_EIGEN, t: 1_700_000_000_000 });
|
||||
assert.deepEqual(h.types(), ['pv', 'bn:shown', 'bn:accept', 'cx']);
|
||||
});
|
||||
|
||||
test('w.js: eigen fragt neu bei Wahl aus der Kunden-Fassung, gilt mit eigener Fassung', () => {
|
||||
const alt = harness({ variant: 'eigen', stored: { v: 1, a: 'accept', bv: BV, t: 1_700_000_000_000 - 1000 } });
|
||||
assert.ok(hostEl(alt), 'Banner erscheint erneut');
|
||||
const ok = harness({ variant: 'eigen', stored: { v: 1, a: 'reject', bv: BANNER_VERSION_EIGEN, t: 1_700_000_000_000 - 1000 } });
|
||||
assert.equal(hostEl(ok), undefined);
|
||||
assert.deepEqual(ok.types(), []);
|
||||
});
|
||||
|
||||
test('w.js: unbekannte Variante => Kunden-Fassung', () => {
|
||||
const h = harness({ variant: 'sonstwas' });
|
||||
const p = findAll(hostEl(h).shadow, (n) => n.tagName === 'P')[0];
|
||||
assert.ok(p.textContent.startsWith(BANNER_TEXT));
|
||||
});
|
||||
|
||||
test('w.js: SPA-Wechsel ohne Einwilligung sendet nur pv, kein en', () => {
|
||||
const h = harness();
|
||||
h.win.location.pathname = '/zwei';
|
||||
h.history.pushState(null, '', '/zwei'); h.timers.splice(0).forEach((f) => f());
|
||||
assert.deepEqual(h.types(), ['pv', 'bn:shown', 'pv']);
|
||||
});
|
||||
|
||||
test('w.js: en nach SPA-Wechsel nur einmal je Seite (Link-Klick und Router)', () => {
|
||||
const h = harness({ stored: { v: 1, a: 'accept', bv: BV, t: 1_700_000_000_000 - 1000 } });
|
||||
h.leave();
|
||||
h.win.location.pathname = '/zwei';
|
||||
h.history.pushState(null, '', '/zwei'); h.timers.splice(0).forEach((f) => f());
|
||||
assert.deepEqual(h.sent.map((s) => `${s.d.t}:${s.d.path}`), ['pv:/start', 'en:/start', 'pv:/zwei']);
|
||||
h.leave();
|
||||
assert.deepEqual(h.sent.map((s) => `${s.d.t}:${s.d.path}`).slice(-1), ['en:/zwei']);
|
||||
});
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
8facae22cea59e23f6a9350374e03f0c42d0cba2e3adfda6062034d47081617a
|
||||
21d406e6836a7d0c1afdb99dc259e4174fb3fe499afcbdac30c8a45a1aa8d605
|
||||
@@ -25,7 +25,10 @@ Antwortformen, die die Portal-Oberfläche erwartet (Geld in Cent, Daten `YYYY-MM
|
||||
- `PUT /einstellungen` Body = genau diese Felder ohne Wrapper. `rechnungAuftrag` (boolean) leitet der Server aus `rechnungAuftragAt` ab: `true` setzt es, falls leer, `false` widerruft (null). `onlineZahlung` nur `true`, wenn Stripe aktiv ist; der Präfix ist bei vorhandenen Rechnungen nicht änderbar. Feldfehler als `felder.<name>`.
|
||||
- `GET /unterkuenfte` → `{ unterkuenfte:[{ id, name, beschreibung, maxPersonen, mindestNaechte, checkIn, checkOut, sort, aktiv, icalImporte:[{ id, name, url, letzterAbrufAt, letzterErfolgAt, fehler }], icalExportUrl }] }` (`icalExportUrl` = volle URL mit Token).
|
||||
- `POST /unterkuenfte`, `PUT /unterkuenfte/:id` Body `{ name, beschreibung, maxPersonen, mindestNaechte, checkIn, checkOut, sort, aktiv }` → `{ unterkunft }`; Deaktivieren = `PUT` mit `aktiv:false`. `DELETE /unterkuenfte/:id`.
|
||||
- `GET|PUT /unterkuenfte/:id/preise` → `{ preise:{ staffeln:[{abNaechten,preisProNachtCent}], reinigungCent, personenInklusive, aufpreisPersonNachtCent, direktrabattProzent, steuerart:'kleinunternehmer'|'regel', ustUebernachtung, ustReinigung, fristTage, stornobedingungen } | null }`; PUT sendet genau diese Felder.
|
||||
- `GET|PUT /unterkuenfte/:id/preise` → `{ preise:{ staffeln:[{abNaechten,preisProNachtCent,personenCent?}], reinigungCent, personenInklusive, aufpreisPersonNachtCent, direktrabattProzent, steuerart:'kleinunternehmer'|'regel', ustUebernachtung, ustReinigung, fristTage, stornobedingungen } | null }`; PUT sendet genau diese Felder.
|
||||
- Preismodell „pauschal“ (ohne `personenCent`): `preisProNachtCent` gilt für `personenInklusive` Gäste, jede weitere Person kostet `aufpreisPersonNachtCent` je Nacht (Posten „Zusatzperson“).
|
||||
- Preismodell „je Personenzahl“: jede Staffel trägt `personenCent:[c1,c2,…,ck]` = Preis pro Nacht für die ganze Unterkunft bei 1, 2, …, k Gästen (k = 2 bis 10, Werte ganze Cent 100 bis 100000). Entweder haben alle Staffeln `personenCent` mit gleicher Länge k oder keine; sonst 400 `felder.staffeln`. Der Server setzt `preisProNachtCent = personenCent[0]` (eine mitgeschickte Zahl wird überschrieben). Eine Staffel gilt weiterhin ab ihrer Nächtezahl für alle Nächte. Der Posten heißt „Übernachtung, 7 Nächte, 3 Personen“; `personenInklusive` wird ignoriert. Bei mehr als k Gästen gilt `ck` plus der Posten „Zusatzperson“ (`aufpreisPersonNachtCent`, nur wenn größer 0) für die Gäste über k. Der Direktrabatt gilt auf Übernachtung und Zusatzpersonen.
|
||||
- Kein neues Schema: `personenCent` liegt im JSON-Text `ukPreise.staffeln` (2000 Zeichen; der Höchstfall 6 Staffeln × 10 Werte braucht rund 800).
|
||||
- `GET /unterkuenfte/:id/kalender?von&bis` → `{ bereiche:[{ von, bis (Abreisetag, exklusiv), quelle:'gebucht'|'reserviert'|'ical'|'manuell' (auch 'ical:<importId>', 'anfrage:<id>' zählt als gebucht), anfrageNummer?, sperreId?, notiz? }] }`.
|
||||
- `POST /unterkuenfte/:id/sperren` `{ von, bis, notiz }` → 201, 409 `zeitraum_belegt`; `DELETE /sperren/:id`.
|
||||
- `POST /unterkuenfte/:id/ical-importe` `{ name, url }` (nur https; Feldfehler `felder.url`) → 201; `DELETE /unterkuenfte/:id/ical-importe/:importId`; `POST /unterkuenfte/:id/ical-export/erneuern`.
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
# Abo-Bedingungen Fassung 6 (Entwurf)
|
||||
|
||||
Fassung 2026-10-16-v6 · Stand 16.10.2026 · Entwurf – Prüfung durch Kanzlei offen · Entwurf zur Freigabe, keine Rechtsberatung · Basis: Fassung 5 (Stand 30.09.2026, `public/vertragsunterlagen/v5/abo-bedingungen.html`)
|
||||
|
||||
Dieser Entwurf listet alle Änderungen gegenüber Fassung 5 im Wortlaut. Die Paragrafenfolge § 1 bis § 16 und alle Anker bleiben, damit Querverweise in Mails und der Paragraf zum Vertragsende (§ 11) stimmen. Die Sätze unter jeder Überschrift sind die Pflichtsätze; sie stehen unverändert in der Seite `public/neu/rechtliches/abo-bedingungen.html`. Zu jeder Stelle nennt die Zeile „Begründung“ die Entscheidung des Inhabers (E…), die Festlegung nach dem Builder-Check (C…) oder den Spec-Abschnitt und verweist bei offenen Punkten auf die Entscheidungsliste (`docs/rechtliches/kundenansicht-entscheidungen.md`, R… und KQ…). Voraussetzung für den Livegang ist die Freigabe durch den Inhaber (E20).
|
||||
|
||||
## Das Wichtigste auf einen Blick
|
||||
|
||||
Das Hosting-Abo enthält die Website-Analyse, das Wartungs-Abo zusätzlich News-Pinnwand, Terminplaner, Webshop, Buchungskalender und Öffnungszeiten live. Das Einschalten kostet nichts extra; die Einrichtung geht vom Stundenkontingent ab (§ 4 Abs. 8).
|
||||
|
||||
**Einordnung.** Neue Zeile „Funktionen“ in der Übersichtsbox, hinter der Zeile „Wartungs-Abo“. Die Box behält ihre Preiszeilen und die Anrede „Ihr“.
|
||||
|
||||
**Begründung.** E6 (die Funktionen stecken im Abo: Hosting-Abo mit Analyse, Wartungs-Abo mit allen Funktionen), E7 und E17 (Einrichtung vom Stundenkonto, keine Extra-Rechnung), C10 (Analyse im Hosting-Abo), Spec 5.9. Die Zeile nennt für Funktionen keinen Preis. Offen: Geltung für laufende Abos (R2, KQ1), Anrede der Rechtsdokumente (R12).
|
||||
|
||||
## § 1 Abs. 2
|
||||
|
||||
Die Bestellseiten richten sich nach der Kundenart, die wir im Kundenkonto hinterlegt haben („Betrieb“ oder „privat“). Wir hinterlegen sie nach den Angaben des Kunden; ist keine hinterlegt, gilt der Kunde als privat. Ob der Kunde Verbraucher oder Unternehmer ist, richtet sich nach dem Zweck, zu dem er den Vertrag abschließt, nicht nach dieser Angabe.
|
||||
|
||||
**Einordnung.** Hängt an den bestehenden Absatz 2 an („Sie gelten für Verbraucher und Unternehmer“).
|
||||
|
||||
**Begründung.** E2 bis E4 und C13: Die Kundenart legt das Team beim Anlegen fest, leer gilt als privat, und die Bestellseiten richten sich danach. Rechtlich entscheidet der Zweck des Vertrags (§§ 13, 14 BGB), nicht die hinterlegte Angabe; der Satz sagt das ausdrücklich, damit die Angabe nicht als Selbstauskunft oder als Rechtsfolge gelesen wird. Offen: Belastbarkeit der Kundenart (R5, KQ4).
|
||||
|
||||
## § 2 Abs. 2 c
|
||||
|
||||
Direkt über der Schaltfläche steht der Hinweis, dass diese Bedingungen mit den Leistungsbeschreibungen, dem Auftragsverarbeitungsvertrag (Anlage 1) und der Widerrufsbelehrung gelten, jeweils mit Fassung und Link; die Unterlagen erhält der Kunde nach der Bestellung zusätzlich als PDF per E-Mail. Ist die Kundenart „privat“ hinterlegt, kann der Kunde dort durch ein Kästchen verlangen, dass wir sofort mit der Leistung beginnen; ohne das Kästchen beginnen wir erst nach Ablauf der 14-tägigen Widerrufsfrist. Ist die Kundenart „Betrieb“ hinterlegt, beginnen wir mit Vertragsschluss, ohne dass der Kunde etwas markieren muss.
|
||||
|
||||
**Einordnung.** Ersetzt den bisherigen Buchstaben c (Zustimmung zu den Bedingungen und Wahl des Beginns). Die Buchstaben a, b, d und e bleiben.
|
||||
|
||||
**Begründung.** Spec 5.6, E1, E3, E4, C4 und recht.md Kurzfassung 3 und 4: Eine Linkzeile über dem Knopf ersetzt die Häkchen für Bedingungen, Leistungsbeschreibung, Auftragsverarbeitung und Belehrung; übrig bleibt das eine Häkchen „Bitte beginnt sofort …“ nur für Privatkunden in der Widerrufsfrist. Bei Betrieben (kein Widerrufsrecht) beginnt die Leistung mit Vertragsschluss. Die Kundenart kommt vom Server (§ 1 Abs. 2). Offen: Wortlaut des Häkchens ohne Normzitat (R6, KQ5), Belehrung nur per Link und PDF (R11, KQ6), AVV per Klick ohne eigenes Häkchen (R10, KQ7).
|
||||
|
||||
## § 2 Abs. 6
|
||||
|
||||
Wir speichern außerdem den Wortlaut der angezeigten Hinweise und Kästchen, die hinterlegte Kundenart sowie Zeit, IP-Adresse und Browserkennung des Klicks.
|
||||
|
||||
**Einordnung.** Hängt an den bestehenden Absatz 6 (Vertragssprache und Speicherung des Vertragstextes) an.
|
||||
|
||||
**Begründung.** Nachweise (Global Constraints der Übersicht, N34): Wortlaut, Fassung, Zeit, IP-Adresse, Browserkennung und Konto werden weiter gespeichert, ab jetzt mit der Kundenart zum Klickzeitpunkt. Der Satz macht das für den Kunden sichtbar; die Datenschutzerklärung nennt die Speicherung heute schon (Prüfpunkte D1 bis D3). Offen: Datenschutz-Prüfpunkte (R14).
|
||||
|
||||
## § 3 Abs. 8
|
||||
|
||||
Zum Abo gehören Funktionen, die der Kunde im Kundenportal unter „Funktionen“ einschaltet. Das Hosting-Abo enthält die Website-Analyse. Das Wartungs-Abo enthält News-Pinnwand, Terminplaner, Webshop, Buchungskalender, Öffnungszeiten live und Website-Analyse. Umfang, Voraussetzungen und Pflichten jeder Funktion beschreiben ihre Leistungsbeschreibung und, soweit die Funktion personenbezogene Daten im Auftrag des Kunden verarbeitet, ihr Auftragsverarbeitungsvertrag, jeweils in der Fassung, die das Kundenportal beim Einschalten anzeigt. Eine Funktion kostet keine zusätzliche Gebühr; die Einrichtung richtet sich nach § 4 Abs. 8 und 9.
|
||||
|
||||
Vorbehaltlich § 11 Abs. 4 läuft eine Funktion nur, solange das Abo läuft, das sie enthält. Das gilt auch für eine Funktion, die wir auf Wunsch des Kunden schon bei der Erstellung seiner Website eingebaut haben. Funktionen, die der Kunde vor Veröffentlichung dieser Fassung einzeln gekauft hat, laufen nach den Bedingungen weiter, unter denen er sie gekauft hat.
|
||||
|
||||
**Einordnung.** Neuer Absatz „(8) Funktionen“ nach Absatz (7) mit zwei Absätzen.
|
||||
|
||||
**Begründung.** E6, E9, E10, E21, C10, C11, C12 und Spec 5.5, 5.9: Funktionen sind Teil des Abos und kosten keine zusätzliche Gebühr; die Einrichtung läuft nach § 4 Abs. 8 und 9. Eine Funktion läuft nur mit dem Abo, das sie enthält, auch wenn sie schon bei der Website-Erstellung eingebaut wurde (E21). Einzeln gekaufte Funktionen behalten ihre Bedingungen (E10). Offen: Geltung für laufende Abos (R2, KQ1), Einrichtung beim Projektstart (R4, KQ8), Ende und Fristen (R8, KQ8), Analyse im Hosting-Abo (R9).
|
||||
|
||||
## § 4 Abs. 2
|
||||
|
||||
Einen Vorschuss nach Absatz 8 gleichen die folgenden Kontingente zuerst aus.
|
||||
|
||||
**Einordnung.** Hängt als letzter Satz an den bestehenden Absatz 2 (Verfügbarkeit und Verfall der Kontingente) an.
|
||||
|
||||
**Begründung.** E17: Ein offener Vorschuss wird vor allem anderen aus den folgenden Monatskontingenten ausgeglichen (FIFO, Spec 5.5). Der Satz verknüpft die bestehende Verbrauchsregel mit Absatz 8. Offen: Vorschuss-Regel insgesamt (R3, KQ2).
|
||||
|
||||
## § 4 Abs. 8
|
||||
|
||||
Die Einrichtung einer Funktion rechnen wir als Arbeitszeit auf das Stundenkontingent an. Angerechnet wird die tatsächlich aufgewendete Zeit nach Absatz 4. Reicht das verfügbare Kontingent nicht aus, führen wir die Einrichtung trotzdem ohne weitere Rückfrage aus. Die fehlende Zeit ist ein Vorschuss, den die folgenden Monatskontingente ausgleichen, sobald sie bereitstehen. Dafür berechnen wir nichts, weder als Mehraufwand nach Absatz 6 noch in einer gesonderten Rechnung. Endet das Wartungs-Abo, solange noch ein Vorschuss offen ist, verfällt der Rest; der Kunde schuldet ihn nicht.
|
||||
|
||||
**Einordnung.** Neuer Absatz 8 nach Absatz 7.
|
||||
|
||||
**Begründung.** E7, E8 und E17 (E17 ersetzt die Absprache aus E8), Spec 5.5: Die Einrichtung startet ohne Rückfrage und geht mit der echten Arbeitszeit (Absatz 4) vom Stundenkonto ab. Reicht das Kontingent nicht, ist die fehlende Zeit ein Vorschuss, nie Mehraufwand nach Absatz 6; ein Rest bei Vertragsende verfällt zugunsten des Kunden. Normale Arbeit außerhalb von Einrichtungen bleibt beim Mehraufwand nach Absatz 6. Offen: Vorschuss ohne Obergrenze (R3, KQ2).
|
||||
|
||||
## § 4 Abs. 9
|
||||
|
||||
Im Hosting-Abo ist die Einrichtung der Website-Analyse für den Kunden kostenlos; ein Stundenkontingent gibt es dort nicht (Absatz 7). Lässt der Kunde eine Funktion auf seinen Wunsch schon bei der Erstellung seiner Website einbauen, rechnen wir die Einrichtung nicht über das Stundenkontingent ab, sondern über die Kosten des Projekts, wie wir es mit ihm vereinbart haben.
|
||||
|
||||
**Einordnung.** Neuer Absatz 9 nach Absatz 8.
|
||||
|
||||
**Begründung.** E9, E21, C6 und C7, Spec 5.5: Im Hosting-Abo gibt es kein Kontingent (Absatz 7), die Einrichtung der Analyse ist dort kostenlos (C6). Bei einer Funktion, die schon bei der Erstellung der Website eingebaut wird, zählen die Einrichtungs-Arbeitsblätter nicht aufs Stundenkonto, sondern laufen über die Projektkosten, wie mit dem Kunden vereinbart (C7). Offen: Einrichtung beim Projektstart (R4, KQ8), Analyse im Hosting-Abo (R9).
|
||||
|
||||
## § 6 Abs. 6
|
||||
|
||||
Schaltet der Kunde die Website-Analyse ein, ist er für die Messung auf seiner Website Verantwortlicher. Er entfernt oder umgeht das Cookie-Banner nicht, bindet ohne Abstimmung mit uns keine weiteren Mess- oder Tracking-Werkzeuge ein und ergänzt seine Datenschutzerklärung um einen Hinweis auf die von uns erzeugte Zusatzseite „/datenschutz-analyse.html“.
|
||||
|
||||
**Einordnung.** Neuer Absatz 6 nach Absatz 5.
|
||||
|
||||
**Begründung.** C10 und Spec 5.5 (Analyse): Wer die Website-Analyse einschaltet, ist für die Messung auf seiner Website Verantwortlicher. Der Absatz stellt die Pflichten aus der Abo-Ergänzung Analyse (Cookie-Banner, keine weiteren Mess- oder Tracking-Werkzeuge ohne Abstimmung, Hinweis auf die Zusatzseite) in die Abo-Bedingungen, weil die Analyse jetzt auch im Hosting-Abo liegt. Die Tatsachenangabe zur Zielgruppe bleibt als Zeile mit Häkchen in der Oberfläche (Spec 5.5, N22). Offen: Analyse im Hosting-Abo (R9, KQ8), Ausschlüsse als Vertragsklausel (R13, KQ9).
|
||||
|
||||
## § 9 Abs. 6
|
||||
|
||||
Will der Kunde im Hosting-Abo eine Funktion einschalten, die nur das Wartungs-Abo enthält, führt ihn das Kundenportal zum Wechsel. Nach Wirksamkeit des Wechsels schaltet er die Funktion in einem eigenen Vorgang ein. Der Wechsel bleibt eine eigene Bestellung nach Absatz 2.
|
||||
|
||||
**Einordnung.** Neuer Absatz 6 nach Absatz 5.
|
||||
|
||||
**Begründung.** E14 und C5: Wer im Hosting-Abo eine Funktion einschalten will, die nur das Wartungs-Abo enthält, wird zum Wechsel geführt und schaltet danach in einem eigenen Vorgang ein. Der Wechsel bleibt eine eigene Bestellung mit eigener Widerrufsfrist (§ 2 Abs. 5); das Einschalten ist keine Bestellung. Offen: Sofortbeginn des Wechsels nur mit Häkchen bei Privatkunden (R6, KQ5).
|
||||
|
||||
## § 11 Abs. 4
|
||||
|
||||
Mit dem Ende des Abos, das eine Funktion enthält, endet die Funktion; abweichend davon läuft Öffnungszeiten live noch 30 Tage unverändert weiter. Wir schalten sie ab und löschen ihre Daten nach den Fristen in ihrer Leistungsbeschreibung; die Messdaten der Website-Analyse löschen wir 60 Tage nach der Abschaltung. Beginnt innerhalb dieser Fristen wieder ein Abo, das die Funktion enthält, schalten wir sie mit den vorhandenen Daten wieder ein. Wechselt der Kunde vom Wartungs- ins Hosting-Abo, bleibt die Website-Analyse; die übrigen Funktionen enden. Das Gleiche gilt, wenn der Kunde den Wechsel ins Wartungs-Abo widerruft.
|
||||
|
||||
**Einordnung.** Neuer Absatz 4 nach Absatz 3.
|
||||
|
||||
**Begründung.** E21, C12 und Nachtrag N31 (mit P4 und P6 abgestimmt): Funktionen enden mit dem Abo, das sie enthält. Terminplaner, News, Webshop und Buchungskalender schalten spätestens am Folgetag ab (Tagesjob); Öffnungszeiten live läuft nach dem Ende noch 30 Tage unverändert weiter, ein „Behalten“ gegen Zahlung gibt es nicht mehr. Gelöscht wird nach den Fristen der Leistungsbeschreibungen: 30 Tage (Terminplaner, News, Öffnungszeiten live), 90 Tage (Webshop, Buchungskalender), 60 Tage für die Messdaten der Analyse. Ein neues passendes Abo in der Frist holt die Funktion zurück. Beim Wechsel ins Hosting-Abo und beim Widerruf des Wechsels bleibt die Analyse (E6, C10). Offen: R8 (KQ8).
|
||||
|
||||
## § 14 Abs. 2
|
||||
|
||||
Für Funktionen, die personenbezogene Daten im Auftrag des Kunden verarbeiten, gilt zusätzlich der Auftragsverarbeitungsvertrag der Funktion, dem der Kunde beim Einschalten mit dem Klick auf „Einschalten“ zustimmt.
|
||||
|
||||
**Einordnung.** Hängt an den bestehenden Absatz 2 (Auftragsverarbeitungsvertrag in Anlage 1) an.
|
||||
|
||||
**Begründung.** Spec 5.9 (AVV je Funktion): Option A der AVV-Struktur, die Auftragsverarbeitungsverträge der Funktionen bleiben eigene Verträge mit neuer Vorbemerkung, Anlage 1 bleibt der Rahmen; der Klick auf „Einschalten“ unter der Linkzeile ist die Zustimmung. Offen: Struktur (R10, KQ7), Klick statt eigenes Häkchen (KQ7).
|
||||
|
||||
## § 15
|
||||
|
||||
Fassung 6 gilt für Abos, die ab ihrer Veröffentlichung abgeschlossen werden. Für Abos, die nach einer früheren Fassung abgeschlossen wurden, gelten diese Fassungen weiter. Schaltet der Kunde mit einem solchen Abo eine Funktion ein, gelten für die Funktion ihre Leistungsbeschreibung und ihr Auftragsverarbeitungsvertrag in der angezeigten Fassung, einschließlich der Regeln zur Einrichtung und zum Stundenkontingent.
|
||||
|
||||
**Einordnung.** Neuer zweiter Absatz hinter dem bisherigen Text. Der bisherige Text bleibt wörtlich.
|
||||
|
||||
**Begründung.** C11 und E10: Fassung 6 gilt für neue Abschlüsse; laufende Abos der Fassungen 3 bis 5 behalten ihren Vertrag. Beim Einschalten gelten die Leistungsbeschreibung und der Auftragsverarbeitungsvertrag der Funktion über die Linkzeile und den Klick, auch mit den Regeln zur Einrichtung und zum Stundenkonto. Die Änderung ist für laufende Abos vorteilhaft (Spec 5.9), deshalb keine Zustimmungsabfrage nach dem bisherigen Text. Offen: Info-Mail an laufende Abos (R2, KQ1).
|
||||
|
||||
## Leistungsbeschreibung A Nr. 1
|
||||
|
||||
Website-Analyse im Kundenportal (§ 3 Abs. 8)
|
||||
|
||||
**Einordnung.** Neuer Listenpunkt am Ende von Nr. 1 „Umfang“.
|
||||
|
||||
**Begründung.** E6 und C10: Das Hosting-Abo enthält die Website-Analyse. Offen: Analyse im Hosting-Abo (R9, KQ8).
|
||||
|
||||
## Leistungsbeschreibung B Nr. 1
|
||||
|
||||
Funktionen im Kundenportal: News-Pinnwand, Terminplaner, Webshop, Buchungskalender, Öffnungszeiten live und Website-Analyse (§ 3 Abs. 8)
|
||||
|
||||
**Einordnung.** Neuer Listenpunkt am Ende von Nr. 1 „Umfang“.
|
||||
|
||||
**Begründung.** E6: Das Wartungs-Abo enthält alle Funktionen und als Obermenge die Analyse.
|
||||
|
||||
## Widerruf: Hinweise
|
||||
|
||||
Das Einschalten einer Funktion ist kein eigener Vertrag, sondern Teil des Abos. Es verlängert die Widerrufsfrist des Abos nicht. Widerruft ein Verbraucher das Abo, endet auch die Funktion (§ 11 Abs. 4).
|
||||
|
||||
**Einordnung.** Neue Nummer 5 am Ende der „Hinweise zum Widerruf“. Widerrufsbelehrung und Muster-Widerrufsformular bleiben wortgleich mit Fassung 5.
|
||||
|
||||
**Begründung.** Spec 5.5 und 5.9, recht.md § 2: Das Einschalten löst keine neue Zahlungspflicht aus und ist kein eigener Vertrag; deshalb gibt es keine eigene Widerrufsfrist. Bei Widerruf des Abos endet die Funktion (§ 11 Abs. 4). Wie sich die Einrichtungszeit innerhalb der Frist auf den Wertersatz auswirkt, regelt das Häkchen für Privatkunden (§ 2 Abs. 2 c); die Entscheidung dazu steht in R7. Offen: R7 (KQ3), Belehrung per Link (R11, KQ6).
|
||||
|
||||
## Anlage 3
|
||||
|
||||
Server für die Website-Analyse (Messdienst, nur bei eingeschalteter Funktion)
|
||||
|
||||
Deutschland (Rechenzentrum Falkenstein)
|
||||
|
||||
**Einordnung.** Neue Tabellenzeile hinter den beiden Hetzner-Zeilen (Unternehmen wie dort, Leistung und Ort wie im Pflichtsatz).
|
||||
|
||||
**Begründung.** Spec 5.9 (AVV je Funktion), Art. 28 Abs. 2 DSGVO: Der Server des Messdienstes der Website-Analyse (Hetzner, Rechenzentrum Falkenstein, wie in der Auftragsverarbeitung der Analyse) wird als Unterauftragsverarbeiter genannt, solange die Analyse eingeschaltet ist. Offen: AVV-Struktur (R10, KQ7).
|
||||
|
||||
## Was gleich bleibt
|
||||
|
||||
- Preise: Hosting-Abo 20,00 € pro Monat, Wartungs-Abo 50,00 € pro Monat, E-Mail-Zusatzspeicher 1,00 € pro Monat je Paket von 5 GB, Mehraufwand 100 € pro Stunde (alle Endpreise, `§ 19 UStG`). Funktionen bekommen keinen Preis.
|
||||
- Laufzeiten: Mindestlaufzeit zwölf Monate ab Leistungsbeginn, Kündigung zum Ende der Mindestlaufzeit mit einem Monat Frist, danach monatlich (§ 8), Sonderkündigung bei Geschäftsaufgabe.
|
||||
- Paragrafenfolge § 1 bis § 16 und alle Anker (`p1` bis `p16`, `wechsel`, `zusatzspeicher`, `hosting`, `wartung`, `widerruf`, `formular`, `widerruf-hinweise`, `avv`, `tom`, `sub`).
|
||||
- Widerrufsbelehrung und Muster-Widerrufsformular: wortgleich mit Fassung 5; die Hinweise zum Widerruf Nr. 1 bis 4 bleiben.
|
||||
- § 10 E-Mail-Zusatzspeicher mit Absatz 3 (Bereitstellung sofort, kein Wertersatz) und den Hinweisen zum Widerruf Nr. 4.
|
||||
- Anlage 1 (Auftragsverarbeitungsvertrag) und Anlage 2 (technische und organisatorische Maßnahmen); Anlage 3 behält ihre beiden Hetzner-Zeilen.
|
||||
- Die Rechtsdokumente bleiben in der bisherigen Anrede („der Kunde“, in der Übersichtsbox „Ihr“); nur Oberflächentexte stehen in „du“ (R12).
|
||||
@@ -8,6 +8,14 @@ Jede ausgelieferte Fassung bleibt hier nachvollziehbar. `BANNER_VERSION` im Clie
|
||||
|
||||
Änderung der Zusatzseite am 02.10.2026 (Fassung bleibt 2026-10-02, Banner-Text unverändert): Der Knopf zum Wiederöffnen des Banners ist jetzt ein Keks-Symbol (Name für Screenreader und Beschriftung beim Darüberfahren weiter „Datenschutz-Einstellungen“). Die Zusatzseite nennt deshalb bei Widerruf und Widerspruch zusätzlich „(das Keks-Symbol)“. Die vorige Formulierung war bis dahin nur auf der WEBklar-Testseite (test-website.project.webklar.com) ausgeliefert, keiner Kunden-Website: [datenschutz-analyse-2026-10-02-erstfassung.html](datenschutz-analyse-2026-10-02-erstfassung.html). In WOMS → Rechtliches als neue Version der Karte „datenschutz-website-analyse“ hinterlegt.
|
||||
|
||||
## Eigene Website webklar.com
|
||||
|
||||
Auf webklar.com läuft dieselbe Messung mit WEBklar als Verantwortlichem (keine AVV). Das Skript-Tag trägt `data-variant="eigen"`; der Banner hat dann eine eigene Sie-Fassung (`BANNER_VERSION_EIGEN`), die Zusatzseite liegt im Repository der Website (`public/datenschutz-analyse.html`), die Kopie hier ist byte-gleich zu der ausgelieferten Fassung. Der Abschnitt 4a der Datenschutzerklärung fasst sie zusammen.
|
||||
|
||||
| Fassung | gültig ab | Banner-Text | Zusatzseite |
|
||||
|---|---|---|---|
|
||||
| 2026-10-17 | offen bis Livegang | [banner-eigen-2026-10-17.md](banner-eigen-2026-10-17.md) (`BANNER_VERSION_EIGEN` 2026-10-17) | [datenschutz-analyse-webklar-com-2026-10-17.html](datenschutz-analyse-webklar-com-2026-10-17.html) |
|
||||
|
||||
AVV und Leistungsbeschreibung je Fassung: `public/rechtliches/avv-website-analyse-<Fassung>.html|pdf` und `leistungsbeschreibung-website-analyse-<Fassung>.html|pdf`. „Gültig ab“ wird beim Livegang eingetragen.
|
||||
|
||||
## Terminplaner
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# Banner-Text Fassung 2026-10-17 (eigene Websites der WEBklar GbR)
|
||||
|
||||
Quelle: `analytics/client/core.js` (Konstanten `BANNER_TEXT_EIGEN`, `BANNER_VERSION_EIGEN`). Gilt nur, wenn das Skript-Tag `data-variant="eigen"` trägt, derzeit nur auf webklar.com. Kunden-Websites behalten die Fassung 2026-10-02 ([banner-2026-10-02.md](banner-2026-10-02.md)).
|
||||
|
||||
- `BANNER_VERSION_EIGEN`: 2026-10-17
|
||||
|
||||
## Fließtext (BANNER_TEXT_EIGEN)
|
||||
|
||||
Um unsere Website zu verbessern, zählen wir Seitenaufrufe mit Land und Gerätetyp, ohne Kennung. Mit Ihrer Zustimmung messen wir zusätzlich, von welcher Website Sie kommen, wie lange Sie bleiben, wie weit Sie scrollen und wo Sie klicken (Heatmap), mit einer täglich wechselnden Kennung. Wir messen selbst auf unserem Server in Deutschland, ohne Cookies und ohne Werbung. Nach „Ablehnen“ zählen wir nur noch, dass Sie abgelehnt haben. Ihre Zustimmung können Sie jederzeit über „Datenschutz-Einstellungen“ mit Wirkung für die Zukunft widerrufen. Details: [Datenschutz](/datenschutz-analyse.html)
|
||||
|
||||
## aria-label, Schaltflächen, Einstellungs-Link
|
||||
|
||||
Wie in der Kunden-Fassung: „Einwilligung zur Reichweitenmessung“; „Ablehnen“ · „Annehmen“ (gleich gestaltet, Ablehnen zuerst); Keks-Symbol „Datenschutz-Einstellungen“ unten rechts.
|
||||
@@ -0,0 +1,94 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="robots" content="noindex">
|
||||
<title>Datenschutz bei der Reichweitenmessung – WEBklar</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||
<style>
|
||||
:root { color-scheme: light; }
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; background: #f7f6f3; color: #1d1d1f; font: 16px/1.6 system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; }
|
||||
main { max-width: 780px; margin: 0 auto; padding: 44px 20px 12rem; }
|
||||
.brand { font-weight: 800; letter-spacing: 0.12em; font-size: 13px; color: #6d1fd1; }
|
||||
h1 { font-size: 30px; line-height: 1.2; margin: 6px 0 6px; }
|
||||
h2 { font-size: 20px; margin: 36px 0 10px; padding-top: 14px; border-top: 1px solid #e3e0da; }
|
||||
p, li { margin: 6px 0; }
|
||||
ul { padding-left: 24px; }
|
||||
.meta { color: #6b6b70; font-size: 14px; }
|
||||
.box { background: #fff; border: 2px solid #6d1fd1; border-radius: 14px; padding: 16px 20px; margin: 18px 0; }
|
||||
a { color: #6d1fd1; }
|
||||
nav.top { font-size: 14px; margin-bottom: 22px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<nav class="top"><a href="/">← Zur Startseite</a> · <a href="https://project.webklar.com/datenschutz.html">Datenschutzerklärung</a> · <a href="/impressum">Impressum</a></nav>
|
||||
<div class="brand">WEBKLAR</div>
|
||||
<h1>Datenschutz bei der Reichweitenmessung</h1>
|
||||
<p class="meta">Ergänzung zu unserer Datenschutzerklärung (Abschnitt 4a) · gilt für webklar.com · Stand 17.10.2026</p>
|
||||
<p>Diese Seite beschreibt nur die Reichweitenmessung auf webklar.com. Alle übrigen Hinweise finden Sie in unserer <a href="https://project.webklar.com/datenschutz.html">Datenschutzerklärung</a>.</p>
|
||||
|
||||
<h2>Verantwortlicher</h2>
|
||||
<p>WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler<br>
|
||||
E-Mail: <a href="mailto:support@webklar.com">support@webklar.com</a></p>
|
||||
<p>Einen Datenschutzbeauftragten haben wir nicht benannt, weil wir dazu gesetzlich nicht verpflichtet sind.</p>
|
||||
|
||||
<h2>Worum es geht</h2>
|
||||
<p>Wir möchten verstehen, wie unsere Website genutzt wird, um sie zu verbessern: Welche Seiten werden gelesen, woher kommen die Besucher, wo wird geklickt? Dafür nutzen wir unsere eigene Website-Analyse, dieselbe, die wir auch unseren Kunden anbieten. Sie kommt ohne Cookies und ohne Dienste von Google, Meta oder anderen Werbenetzwerken aus. Wir nutzen die Daten nicht für Werbung und bilden keine Profile einzelner Besucher. Die Messung läuft auf unserem Server in Deutschland.</p>
|
||||
<p>Damit die Messung arbeiten kann, lädt Ihr Browser bei jedem Seitenaufruf ein Skript von unserem Server (project.webklar.com). Dabei erhält dieser Server, wie bei jeder Datei, die eine Website lädt, technisch Ihre IP-Adresse und Ihr Browser-Kennzeichen (User-Agent). Das gilt auch, wenn Sie ablehnen oder „Global Privacy Control“ aktiviert haben. Beim Abruf des Skripts und bei der Messung speichern und protokollieren wir beides nicht.</p>
|
||||
|
||||
<h2>Solange Sie im Banner nichts ausgewählt haben</h2>
|
||||
<p>Dann zählen wir den Aufruf der Seite. Das Skript übermittelt dafür nur die Adresse der Seite und die Angabe, dass das Banner angezeigt wurde. Gespeichert werden:</p>
|
||||
<ul>
|
||||
<li>der Seitenpfad (ohne Suchbegriffe oder Parameter hinter dem „?“),</li>
|
||||
<li>das Land, das wir aus Ihrer IP-Adresse ableiten,</li>
|
||||
<li>die Geräteklasse (Handy, Tablet oder Computer), die wir aus dem Browser-Kennzeichen ableiten,</li>
|
||||
<li>der Zeitpunkt, auf die volle Stunde gerundet, und</li>
|
||||
<li>ein Zähler dafür, dass das Banner angezeigt wurde.</li>
|
||||
</ul>
|
||||
<p>IP-Adresse und Browser-Kennzeichen verwenden wir nur kurz im Arbeitsspeicher: um Land und Geräteklasse zu bestimmen, um automatische Abrufe durch Programme (Bots) auszusortieren und um zu verhindern, dass die Messung durch massenhafte Anfragen verfälscht wird. Für diese Mengenbegrenzung bilden wir aus Ihrer IP-Adresse einen Prüfwert (Hash), den wir nur im Arbeitsspeicher halten und nicht speichern. Eine Kennung, mit der wir Sie wiedererkennen könnten, speichern wir ohne Ihre Einwilligung nicht.</p>
|
||||
<p>Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO. Unser berechtigtes Interesse ist, die Nutzung unserer Website in Zahlen zu kennen, das Banner zu betreiben und die Messung vor Missbrauch zu schützen.</p>
|
||||
|
||||
<h2>Wenn Sie „Ablehnen“ wählen</h2>
|
||||
<p>Dann zählen wir einmal, dass abgelehnt wurde, und das Skript sendet danach nichts mehr, auch keine Seitenaufrufe. Sendet Ihr Browser das Signal „Global Privacy Control“ oder „Do Not Track“, zeigen wir das Banner gar nicht an und zählen nichts.</p>
|
||||
|
||||
<h2>Wenn Sie „Annehmen“ wählen</h2>
|
||||
<p>Dann messen wir zusätzlich:</p>
|
||||
<ul>
|
||||
<li>von welcher Website Sie gekommen sind (nur der Domainname),</li>
|
||||
<li>die Größenklasse Ihres Browserfensters,</li>
|
||||
<li>eine Besucher-Kennung, die täglich wechselt. Wir berechnen sie aus IP-Adresse und Browser-Kennzeichen mit einem Schlüssel, der jeden Tag neu erzeugt und nicht gespeichert wird. Am selben Tag könnten wir die Kennung damit nachrechnen. Nach dem Wechsel des Schlüssels lässt sie sich nicht mehr aus Ihrer IP-Adresse berechnen und nicht mit Kennungen anderer Tage verknüpfen,</li>
|
||||
<li>wie lange Sie auf einer Seite bleiben (ab dem Laden der Seite) und wie weit Sie scrollen,</li>
|
||||
<li>wo auf der Seite geklickt wird: die Klickposition und den Aufbau des angeklickten Elements, ohne Texte und ohne Eingaben in Formulare (für eine „Heatmap“),</li>
|
||||
<li>ob Sie auf eine Telefonnummer, eine E-Mail-Adresse, einen WhatsApp-Link oder einen Link zu einer anderen Website klicken (dann mit deren Domainname, nicht mit der Nummer oder Adresse selbst), und</li>
|
||||
<li>einen Zähler dafür, dass Sie „Annehmen“ gewählt haben.</li>
|
||||
</ul>
|
||||
<p>Rechtsgrundlage ist Ihre Einwilligung nach § 25 Abs. 1 TDDDG und Art. 6 Abs. 1 lit. a DSGVO.</p>
|
||||
|
||||
<h2>Was in Ihrem Browser gespeichert wird</h2>
|
||||
<p>Wir speichern Ihre Auswahl („Annehmen“ oder „Ablehnen“) mit Datum und der Fassung des Banner-Textes im lokalen Speicher Ihres Browsers (Eintrag „wk-consent“), damit wir Sie nicht bei jedem Seitenaufruf erneut fragen. Das ist ohne Einwilligung zulässig (§ 25 Abs. 2 Nr. 2 TDDDG). Die Auswahl gilt 183 Tage, danach fragen wir erneut. Sie können den Eintrag jederzeit selbst löschen, indem Sie die Website-Daten in Ihrem Browser löschen.</p>
|
||||
|
||||
<h2>Speicherdauer</h2>
|
||||
<p>Einzelne Seitenaufrufe, Ereignisse und Klickpositionen löschen wir nach 30 Tagen. Danach bleiben nur zusammengefasste Tageswerte, also Summen je Tag, etwa Aufrufe je Seite oder je Land, ohne Besucher-Kennung. Wir löschen sie nach 25 Monaten. Beenden wir die Messung, löschen wir alle Messdaten spätestens rund 60 Tage danach. Unser Dienstleister sichert den Server täglich; gelöschte Daten bleiben dort bis zu sieben Tage erhalten.</p>
|
||||
|
||||
<h2>Empfänger</h2>
|
||||
<p>Die Messung betreiben wir selbst. Die Daten sehen nur unsere Mitarbeiter. Der Server steht bei der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, in Deutschland; Hetzner ist für uns als Auftragsverarbeiter tätig. Eine Übermittlung in Länder außerhalb der EU bzw. des EWR findet nicht statt.</p>
|
||||
|
||||
<h2>Freiwilligkeit</h2>
|
||||
<p>Sie sind weder gesetzlich noch vertraglich verpflichtet, uns diese Daten bereitzustellen. Wenn Sie ablehnen oder Ihre Einwilligung widerrufen, können Sie unsere Website trotzdem uneingeschränkt nutzen. Eine automatisierte Entscheidungsfindung findet nicht statt.</p>
|
||||
|
||||
<div class="box">
|
||||
<h2 style="border:0;margin-top:0;padding-top:0">Einwilligung widerrufen</h2>
|
||||
<p>Sie können Ihre Einwilligung jederzeit mit Wirkung für die Zukunft widerrufen: Klicken Sie auf „Datenschutz-Einstellungen“ (das Keks-Symbol unten rechts auf jeder Seite) und wählen Sie „Ablehnen“. Die Rechtmäßigkeit der Verarbeitung bis zum Widerruf bleibt unberührt.</p>
|
||||
<h2 style="border:0;padding-top:0">Widerspruchsrecht</h2>
|
||||
<p>Sie können der Zählung nach Art. 6 Abs. 1 lit. f DSGVO jederzeit aus Gründen widersprechen, die sich aus Ihrer besonderen Situation ergeben (Art. 21 DSGVO). Am einfachsten wählen Sie im Banner oder über „Datenschutz-Einstellungen“ (das Keks-Symbol) „Ablehnen“ oder aktivieren in Ihrem Browser das Signal „Global Privacy Control“. Dann sendet das Skript keine Daten mehr. Sie können Ihren Widerspruch auch an die oben genannte E-Mail-Adresse richten.</p>
|
||||
</div>
|
||||
|
||||
<h2>Ihre Rechte</h2>
|
||||
<p>Sie haben das Recht auf Auskunft, Berichtigung, Löschung, Einschränkung der Verarbeitung und Datenübertragbarkeit (Art. 15 bis 18 und 20 DSGVO). In den Messdaten speichern wir keine Namen, keine IP-Adressen und keine Kennung, die länger als einen Tag gilt, und wir runden Zeitpunkte auf die volle Stunde. Deshalb können wir gespeicherte Messdaten in der Regel nicht Ihnen zuordnen (Art. 11 DSGVO). Anders kann es sein, solange die Zugriffsprotokolle unseres Webservers noch vorhanden sind (Abschnitt 3 der Datenschutzerklärung). Wenn Sie uns Datum, ungefähre Uhrzeit, besuchte Seiten und, wenn Sie möchten, Ihre IP-Adresse nennen, prüfen wir, ob eine Zuordnung möglich ist. Ist das nicht möglich, teilen wir Ihnen das mit.</p>
|
||||
<p>Mit Ihren Anliegen können Sie sich jederzeit an die oben genannte Adresse wenden. Außerdem haben Sie das Recht, sich bei einer Datenschutz-Aufsichtsbehörde zu beschweren (Art. 77 DSGVO), für uns zuständig ist der Landesbeauftragte für den Datenschutz und die Informationsfreiheit Rheinland-Pfalz.</p>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,145 @@
|
||||
# Auftragsverarbeitungsvertrag Terminplaner (Art. 28 DSGVO)
|
||||
|
||||
**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen
|
||||
|
||||
zwischen dem Kunden als Verantwortlichem („Kunde“) und der WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, vertreten durch die Gesellschafter Kenso Grimm und Justin Klein, als Auftragsverarbeiter („WEBklar“).
|
||||
|
||||
## Vorbemerkung: Abschluss und Verhältnis zu anderen Verträgen
|
||||
|
||||
(1) Der Kunde schließt diesen Vertrag, wenn er im Kundenportal die Funktion „Terminplaner“ einschaltet und die dort angezeigte Zeile „Es gelten Leistungsbeschreibung und Auftragsverarbeitung (Fassung 2026-10-16)“ mit dem Klick auf „Einschalten“ bestätigt; diesem Vertrag und der Leistungsbeschreibung „Terminplaner“ („Leistungsbeschreibung“) in der angezeigten Fassung stimmt er damit zu. Das Portal speichert den Wortlaut der angezeigten Zeile, die Fassungen, den Zeitpunkt, das Kundenkonto und die angemeldete Person. Anschließend sendet WEBklar dem Kunden eine E-Mail, die beide Texte in dieser Fassung als PDF enthält und auf sie verlinkt.
|
||||
|
||||
(2) Ein Mitarbeiter von WEBklar darf die Funktion nur einschalten, wenn der Kunde vorher in Textform (etwa im Chat des Kundenportals oder per E-Mail) erklärt hat, dass er diesem Vertrag und der Leistungsbeschreibung in der aktuellen Fassung zustimmt. WEBklar speichert die Fassung, das Datum und den Weg dieser Erklärung, den Zeitpunkt des Einschaltens und den Namen des Mitarbeiters. Der Kunde erhält anschließend eine E-Mail mit diesen beiden Texten als PDF.
|
||||
|
||||
(3) Dieser Vertrag ist ein eigener Vertrag für den Terminplaner. Er ergänzt den Auftragsverarbeitungsvertrag in Anlage 1 der Abo-Bedingungen. Für den Terminplaner geht er Anlage 1 vor; im Übrigen gilt Anlage 1 ergänzend. In Datenschutzfragen geht er den Abo-Bedingungen und der Leistungsbeschreibung vor.
|
||||
|
||||
## 1. Gegenstand und Dauer
|
||||
|
||||
(1) WEBklar betreibt für die öffentliche Website des Kunden ein Buchungs-Widget mit einer Verwaltung im Kundenportal („Terminplaner“) und verarbeitet dabei personenbezogene Daten der Gäste und der Beschäftigten des Kunden im Auftrag des Kunden. Den Umfang beschreibt die Leistungsbeschreibung.
|
||||
|
||||
(2) Der Vertrag beginnt mit dem Einschalten der Funktion. Er endet mit dem Abschalten, spätestens mit dem Ende des Wartungs-Abos; den Terminplaner schaltet WEBklar dann spätestens am folgenden Tag ab (Nr. 8 Abs. 1 der Leistungsbeschreibung). Die Pflichten aus diesem Vertrag gelten fort, bis alle Daten nach Nummer 9 gelöscht sind.
|
||||
|
||||
## 2. Art und Zweck der Verarbeitung
|
||||
|
||||
(1) Art: Erheben über das Widget auf der Website des Kunden, Übermitteln an den Server von WEBklar, Speichern, Verwalten und Anzeigen im Kundenportal, Versenden von E-Mails, Bereitstellen eines Kalender-Links und einer CSV-Datei und Löschen.
|
||||
|
||||
(2) Zwecke: Der Kunde soll Termine seiner Gäste verwalten. Dazu nimmt der Terminplaner Buchungen und Terminanfragen entgegen, verhindert Doppelbuchungen, bestätigt, ändert und sagt Termine ab, erinnert Gäste auf deren Wunsch und benachrichtigt den Kunden. WEBklar versendet die E-Mails im Auftrag des Kunden. Außerdem schützt WEBklar den Terminplaner für den Kunden vor Missbrauch (Mengenbegrenzung, Erkennen automatisierter Buchungen).
|
||||
|
||||
(3) WEBklar verwendet die Daten für keine eigenen Zwecke, auch nicht in anonymisierter Form und nicht für Auswertungen über mehrere Kunden. Es findet keine Werbung und keine Zusammenführung mit anderen Daten statt. WEBklar bildet keine Profile einzelner Gäste. Zählungen ohne Personenbezug, die WEBklar für den sicheren Betrieb braucht (etwa die Zahl der gelöschten Buchungen je Tag oder die Länge der Versandwarteschlange), sind keine eigenen Zwecke.
|
||||
|
||||
## 3. Datenarten und Betroffene
|
||||
|
||||
(1) Daten der Gäste: Name und E-Mail-Adresse; auf Einstellung des Kunden außerdem Telefonnummer, eine Nachricht und Zusatzfelder, die der Kunde im Formular festlegt; die gebuchte Leistung, die Person oder der Platz und der Termin; der Status der Buchung mit den Zeitpunkten (Anlegen, Bestätigung, Annahme, Absage, Widerruf); Absagegrund und Vermerke zu einer Absage oder einem Widerruf (Freitext, auch mit dem Namen aus einer Widerrufserklärung); Preis, Zahlart, Zahlungsstatus und die Angabe, ob als Verbraucher gebucht wurde; der Nachweis der Zustimmungen (Fassung der Gästetexte, Zeitpunkt, Text des Buchungsknopfs, Anrede und Hashes (Prüfsummen) der gezeigten Texte); der Wunsch nach einer Erinnerung; der Zustellstatus der E-Mails an den Gast.
|
||||
|
||||
(2) Daten der Beschäftigten des Kunden: Namen oder Bezeichnungen der Personen oder Plätze, deren Arbeitszeiten und Abwesenheiten sowie ein geheimer Kalender-Link je Person oder Platz.
|
||||
|
||||
(3) Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO) werden nicht verarbeitet. Der Terminplaner ist nicht für den Gesundheitsbereich bestimmt; der Kunde fordert von seinen Gästen keine Gesundheitsangaben (Nummer 11 Abs. 4).
|
||||
|
||||
(4) IP-Adresse und Browser-Kennzeichen (User-Agent) erhält der Server von WEBklar technisch bei jedem Abruf des Widgets und jeder Anfrage. Die Mengenbegrenzung (Missbrauchsschutz) verwendet die IP-Adresse nur im Arbeitsspeicher des Servers; ein Neustart leert sie. Mit der Buchung wird keine IP-Adresse und kein Browser-Kennzeichen gespeichert. In den Server-Protokollen stehen sie 14 Tage (Anlage 2 der Abo-Bedingungen).
|
||||
|
||||
(5) Der Terminplaner setzt keine Cookies und speichert nichts im Browser des Gastes.
|
||||
|
||||
(6) Betroffene: Gäste, die über die Website des Kunden Termine buchen oder anfragen, und Beschäftigte des Kunden, die im Terminplaner geführt werden.
|
||||
|
||||
## 4. Weisungen
|
||||
|
||||
(1) WEBklar verarbeitet die Daten nur auf dokumentierte Weisung des Kunden, auch in Bezug auf eine Übermittlung in ein Drittland oder an eine internationale Organisation. Das gilt nicht, wenn das Recht der Europäischen Union oder Deutschlands WEBklar zu einer Verarbeitung verpflichtet. In diesem Fall teilt WEBklar dem Kunden die Pflicht vorher mit, soweit das Gesetz es nicht verbietet.
|
||||
|
||||
(2) Mit dem Einschalten weist der Kunde WEBklar an, den Terminplaner nach seinen Einstellungen und der Leistungsbeschreibung zu betreiben. Weitere Weisungen ergeben sich aus den Einstellungen im Kundenportal (Leistungen, Personen oder Plätze, Zeiten, Felder des Formulars, Aufbewahrungsdauer, Erinnerungen, Einschalten und Ausschalten). Sonstige Weisungen erteilt der Kunde in Textform, etwa im Chat des Kundenportals. Weisungen, die über diese Einstellungen hinausgehen, setzt WEBklar nur um, wenn das technisch vorgesehen ist; sonst kann der Kunde den Terminplaner abschalten lassen (Nr. 8 Abs. 6 der Leistungsbeschreibung). Die Löschung auf Weisung nach Nummer 9 Abs. 6 setzt WEBklar immer um.
|
||||
|
||||
(3) Hält WEBklar eine Weisung für rechtswidrig, weist es den Kunden unverzüglich darauf hin. WEBklar darf die Ausführung aussetzen, bis der Kunde die Weisung ändert oder die Rechtslage geklärt ist.
|
||||
|
||||
## 5. Vertraulichkeit
|
||||
|
||||
WEBklar setzt nur Personen ein, die zur Vertraulichkeit verpflichtet sind oder einer gesetzlichen Verschwiegenheitspflicht unterliegen.
|
||||
|
||||
## 6. Technische und organisatorische Maßnahmen
|
||||
|
||||
(1) WEBklar trifft die Maßnahmen nach Art. 32 DSGVO, die in der Anlage zu diesem Vertrag beschrieben sind. WEBklar darf sie an den Stand der Technik anpassen, sofern das Schutzniveau nicht sinkt.
|
||||
|
||||
(2) Für den Server gelten ergänzend die Maßnahmen der Anlage 2 der Abo-Bedingungen, soweit die Anlage zu diesem Vertrag nichts anderes regelt.
|
||||
|
||||
## 7. Unterauftragsverarbeiter
|
||||
|
||||
(1) Der Kunde genehmigt den Einsatz der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, für den Betrieb des Servers, auf dem der Terminplaner läuft, und für dessen Sicherung (Rechenzentrum Falkenstein, Deutschland) sowie für den Versand der Buchungs-E-Mails über den SMTP-Dienst des Webhostings (Rechenzentrum Nürnberg, Deutschland). Eine Übermittlung in Drittländer findet nicht statt.
|
||||
|
||||
(2) Will WEBklar einen weiteren Unterauftragsverarbeiter hinzuziehen oder einen ersetzen, teilt es das dem Kunden mindestens vier Wochen vorher in Textform mit. Der Kunde kann innerhalb von zwei Wochen nach Zugang der Mitteilung aus wichtigem datenschutzrechtlichem Grund in Textform widersprechen. WEBklar setzt den neuen Unterauftragsverarbeiter nicht vor Ablauf dieser Frist ein. Kündigt ein Unterauftragsverarbeiter von WEBklar eine eigene Änderung mit kürzerer Frist an, informiert WEBklar den Kunden unverzüglich, nachdem die Ankündigung eingegangen ist; der Kunde kann dann unverzüglich widersprechen. Einigen sich die Parteien nicht, kann der Kunde den Terminplaner abschalten lassen und WEBklar ihn für diese Website zu dem Zeitpunkt beenden, zu dem der neue Unterauftragsverarbeiter eingesetzt wird. Die Daten werden dann nach Nummer 9 gelöscht, auf Weisung des Kunden früher (Nummer 9 Abs. 6).
|
||||
|
||||
(3) WEBklar verpflichtet jeden Unterauftragsverarbeiter vertraglich auf die Datenschutzpflichten nach Art. 28 Abs. 4 DSGVO. Bei der Hetzner Online GmbH ist das deren Vertrag zur Auftragsverarbeitung, den WEBklar abgeschlossen hat. Kommt ein Unterauftragsverarbeiter seinen Datenschutzpflichten nicht nach, haftet WEBklar dem Kunden für die Einhaltung dieser Pflichten (Art. 28 Abs. 4 Satz 2 DSGVO).
|
||||
|
||||
(4) Die Zahlung der Einrichtung über Stripe betrifft nur den Vertrag zwischen dem Kunden und WEBklar. Daten von Gästen oder Beschäftigten des Kunden gehen nicht an Stripe. Stripe ist deshalb kein Unterauftragsverarbeiter dieses Vertrags.
|
||||
|
||||
## 8. Unterstützung des Kunden, Betroffenenrechte
|
||||
|
||||
(1) WEBklar leitet Anfragen von Gästen und Beschäftigten, die an WEBklar gerichtet sind, unverzüglich an den Kunden weiter. WEBklar unterstützt ihn mit geeigneten technischen und organisatorischen Maßnahmen bei der Beantwortung.
|
||||
|
||||
(2) Dafür bietet das Kundenportal die Aktion „Gastdaten löschen“ und den Export der Termine als CSV-Datei. Mit „Gastdaten löschen“ leert der Kunde Name, E-Mail-Adresse, Telefonnummer, Nachricht und Zusatzfelder einer Buchung; Status und Zeiten bleiben. Dabei löscht WEBklar die E-Mails zu dieser Buchung, die noch auf den Versand warten, und die Links des Gastes werden ungültig. E-Mails, deren Versand endgültig fehlgeschlagen ist, behalten Empfängeradresse und Inhalt, bis WEBklar sie nach Nummer 9 Abs. 4 löscht (48 Stunden nach dem Fehlschlag). Eine noch nicht bestätigte Buchung verfällt von selbst (Nummer 9 Abs. 1).
|
||||
|
||||
(3) Verletzungen des Schutzes personenbezogener Daten meldet WEBklar dem Kunden unverzüglich, möglichst innerhalb von 24 Stunden nach Kenntnis, mit den Informationen, die zu diesem Zeitpunkt vorliegen.
|
||||
|
||||
(4) WEBklar unterstützt den Kunden bei seinen Pflichten aus Art. 32 bis 36 DSGVO. Dazu gehören die Angaben, die er für sein Verzeichnis von Verarbeitungstätigkeiten und für eine etwaige Datenschutz-Folgenabschätzung braucht.
|
||||
|
||||
(5) Die Buchung speichert als Nachweis der Zustimmungen die Fassung der Gästetexte, den Zeitpunkt, den Text des Buchungsknopfs, die Anrede und Hashes der übrigen Texte, die der Gast gesehen hat, nicht diese Texte selbst. Damit sich der Wortlaut nachweisen lässt, bewahrt WEBklar jede Fassung der Gästetexte (Hinweis am Formular, Texte der Haken, Datenschutz-Absatz und Widerrufsbelehrung für Gäste) mit dem Zeitraum ihrer Verwendung auf, bis drei Jahre nach dem Ende des Terminplaners. WEBklar stellt sie dem Kunden auf Verlangen zur Verfügung.
|
||||
|
||||
(6) Die Gäste informiert der Kunde (Art. 13 DSGVO; Nummer 11 Abs. 2).
|
||||
|
||||
## 9. Löschung und Rückgabe
|
||||
|
||||
(1) Nicht bestätigte Buchungen löscht WEBklar 48 Stunden nach Ablauf der Bestätigungsfrist von 30 Minuten.
|
||||
|
||||
(2) Abgesagte und abgelehnte Termine löscht WEBklar 30 Tage nach der Absage oder Ablehnung. Terminanfragen, die der Kunde bis zum angefragten Terminbeginn nicht beantwortet, behandelt der Terminplaner als abgelehnt; die 30 Tage laufen dann ab diesem Zeitpunkt.
|
||||
|
||||
(3) Bestätigte und erledigte Termine löscht WEBklar nach der Aufbewahrungsdauer, gerechnet ab dem Ende des Termins. Der Kunde stellt sie im Kundenportal ein; die Vorgabe sind 90 Tage, einstellbar sind 14 bis 365 Tage.
|
||||
|
||||
(4) In der Versandwarteschlange für E-Mails leert WEBklar die Empfängeradresse und den Inhalt einer E-Mail nach dem Versand und löscht den Eintrag 7 Tage nach dem Versand. E-Mails, die nicht zugestellt werden können, löscht WEBklar mit Empfängeradresse und Inhalt 48 Stunden nach dem endgültigen Fehlschlag.
|
||||
|
||||
(5) Der Löschlauf läuft täglich; gelöscht wird frühestens nach Ablauf der jeweiligen Frist. Ist an einem Tag mehr zu löschen, als ein Lauf schafft, folgt der Rest im nächsten Lauf.
|
||||
|
||||
(6) Ist der Terminplaner abgeschaltet oder das Wartungs-Abo beendet, löscht WEBklar alle Termindaten und die Einstellungen 30 Tage danach. Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Einstellungen bestehen (zum Beispiel Betriebsangaben, Namen der Leistungen, die ersten acht Namen der Personen oder Plätze und die Wünsche des Kunden zum Einbau) und enthält keine Daten von Gästen; dafür gilt die Datenschutzerklärung von WEBklar (Abschnitt 6). Bis zur Löschung kann der Kunde die Termine im Kundenportal als CSV-Datei exportieren. Beginnt innerhalb dieser Frist wieder ein Wartungs-Abo, schaltet WEBklar den Terminplaner mit den vorhandenen Daten und Einstellungen wieder ein (Nr. 8 Abs. 4 der Leistungsbeschreibung). Früher löscht der Kunde einzelne Gastdaten selbst mit der Aktion „Gastdaten löschen“ (Nummer 8 Abs. 2); alle Daten löscht WEBklar auf Weisung des Kunden in Textform unverzüglich.
|
||||
|
||||
(7) Entwürfe aus dem Fragebogen, aus denen die Funktion nicht eingeschaltet wurde, löscht WEBklar 90 Tage nach der letzten Änderung.
|
||||
|
||||
(8) Die Daten sind in der täglichen Vollsicherung des Servers enthalten (§ 3 Abs. 5 der Abo-Bedingungen). Gelöschte Daten bleiben dort bis zu sieben Tage erhalten und werden dann überschrieben. Wird eine Sicherung zurückgespielt, holt der tägliche Löschlauf die fälligen Löschungen nach; Löschungen auf Weisung des Kunden wiederholt WEBklar.
|
||||
|
||||
(9) Gesetzliche Aufbewahrungspflichten bleiben unberührt.
|
||||
|
||||
## 10. Nachweise und Kontrollen
|
||||
|
||||
WEBklar stellt dem Kunden alle Informationen zur Verfügung, die zum Nachweis der Pflichten aus Art. 28 DSGVO nötig sind, und ermöglicht Überprüfungen einschließlich Inspektionen durch den Kunden oder einen von ihm beauftragten, zur Verschwiegenheit verpflichteten Prüfer. Sie finden nach Anmeldung mit angemessener Frist (in der Regel 14 Tage) zu den Servicezeiten statt, ohne den Betrieb unzumutbar zu stören. Für das Rechenzentrum genügen in der Regel die Nachweise des Betreibers (Zertifizierung der Hetzner Online GmbH nach ISO/IEC 27001). Für mehr als eine Vor-Ort-Kontrolle pro Jahr kann WEBklar eine angemessene Vergütung verlangen.
|
||||
|
||||
## 11. Pflichten des Kunden
|
||||
|
||||
(1) Der Kunde ist Verantwortlicher für die Verarbeitung der Daten seiner Gäste und seiner Beschäftigten im Terminplaner.
|
||||
|
||||
(2) Der Kunde informiert seine Gäste nach Art. 13 DSGVO. Dazu nimmt er den Datenschutz-Absatz für die Online-Terminbuchung in seine Datenschutzerklärung auf (Nr. 4 Abs. 9 der Leistungsbeschreibung) und lässt den Datenschutz-Hinweis am Buchungsformular unverändert.
|
||||
|
||||
(3) Der Kunde informiert seine Beschäftigten darüber, dass ihre Namen oder Bezeichnungen, ihre Arbeitszeiten und Abwesenheiten im Terminplaner stehen und dass es einen geheimen Kalender-Link gibt (Nr. 4 Abs. 7 der Leistungsbeschreibung).
|
||||
|
||||
(4) Der Kunde setzt den Terminplaner nicht im Gesundheitsbereich ein und fordert von seinen Gästen keine Angaben zu ihrer Gesundheit, auch nicht über Zusatzfelder oder das Nachrichtenfeld (Nr. 4 Abs. 3 der Leistungsbeschreibung).
|
||||
|
||||
(5) Der Kunde verändert den Haken für die Erinnerung und die Texte der übrigen Haken nicht (Nr. 4 Abs. 6 der Leistungsbeschreibung). Erinnerungen versendet der Terminplaner nur, wenn der Gast den Haken gesetzt hat.
|
||||
|
||||
## 12. Haftung, Änderungen, Schlussbestimmungen
|
||||
|
||||
(1) Gegenüber betroffenen Personen gilt Art. 82 DSGVO. Für die Haftung zwischen Kunde und WEBklar gilt § 13 der Abo-Bedingungen.
|
||||
|
||||
(2) Eine neue Fassung dieses Vertrags oder der Leistungsbeschreibung, auch wegen geänderter Datenerhebung, teilt WEBklar dem Kunden mindestens sechs Wochen vor ihrem Wirksamwerden in Textform mit. Sie wird wirksam, wenn der Kunde ihr im Kundenportal oder in Textform zustimmt. Stimmt er nicht zu, gilt die bisherige Fassung weiter.
|
||||
|
||||
(3) Ist die neue Fassung nötig, weil ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form nicht mehr zulässt, und stimmt der Kunde ihr nicht zu, darf WEBklar den Terminplaner für seine Website nach Nr. 9 Abs. 2 der Leistungsbeschreibung beenden oder nach Nr. 9 Abs. 3 der Leistungsbeschreibung aussetzen. Das Wartungs-Abo bleibt davon unberührt.
|
||||
|
||||
(4) Im Übrigen gilt § 16 der Abo-Bedingungen.
|
||||
|
||||
## Anlage: Technische und organisatorische Maßnahmen des Terminplaners (Art. 32 DSGVO)
|
||||
|
||||
| Bereich | Maßnahmen |
|
||||
|---|---|
|
||||
| Übertragung | Das Widget, die Seiten der Gäste und das Kundenportal werden nur über TLS (HTTPS) übertragen. Der Einbau-Code verweist auf eine Adresse mit https://. |
|
||||
| Speicher im Browser | Es werden keine Cookies gesetzt, und der Terminplaner speichert nichts im Browser des Gastes (kein lokaler Speicher, kein Sitzungsspeicher). Das Widget sendet seine Anfragen ohne Cookies, liest weder den Referrer noch die Zeitzone des Browsers und ruft nur die Schnittstelle des Terminplaners auf. |
|
||||
| Zugriffskontrolle | Das Kundenportal ist nur nach Anmeldung erreichbar; jeder Kunde sieht nur die Daten seiner eigenen Projekte. Die öffentliche Schnittstelle des Widgets liefert nur für einen eingeschalteten Terminplaner Daten und ist über den Website-Schlüssel (Site-Key) adressiert; Buchungen nimmt sie nur von Seiten des Kunden und des Portals an. Der Kalender-Link enthält ein geheimes Token, das mit einem Vergleich in konstanter Zeit geprüft wird. |
|
||||
| Missbrauchsschutz | Ein verstecktes Feld (Honeypot), eine Mindestzeit bis zum Absenden des Formulars mit signiertem Formular-Token und eine Mindestantwortzeit schützen vor automatisierten Buchungen. Grenzen je IP-Adresse, je Website und je E-Mail-Adresse sowie das Mailbudget je Stunde (insgesamt und je Funktion) gelten nur im Arbeitsspeicher; ein Neustart leert sie. |
|
||||
| Datenminimierung | Pflicht sind immer nur Name und E-Mail-Adresse; ob Telefonnummer, Nachricht und Zusatzfelder abgefragt werden, legt der Kunde fest. Mit der Buchung werden keine IP-Adresse und kein Browser-Kennzeichen gespeichert. Der Nachweis der Zustimmungen besteht aus der Fassung der Gästetexte, dem Zeitpunkt, dem Text des Buchungsknopfs, der Anrede und Hashes der übrigen gezeigten Texte, nicht aus diesen Texten. Nach dem Versand einer E-Mail werden Empfängeradresse und Inhalt in der Warteschlange geleert. |
|
||||
| Trennung | Alle Buchungen, Belegungen und Warteschlangeneinträge tragen die Kennung der Funktion (eines Terminplaners je Website-Projekt); das Kundenportal und die öffentliche Schnittstelle lesen immer nur die Daten der jeweiligen Funktion. Die Hintergrundläufe (Löschung, Versand der E-Mails, Erinnerungen) gehen die Dokumente aller Funktionen nacheinander durch und schreiben jeweils nur in das Dokument, um das es geht. Die Einstellungen liegen je Funktion in einem eigenen Dokument. |
|
||||
| Löschung | Täglicher Löschlauf für Buchungen, Belegungen, Warteschlange und Entwürfe nach den Fristen der Nummer 9; Termindaten und Einstellungen abgeschalteter Funktionen 30 Tage nach dem Abschalten; das Einrichtungs-Ticket bleibt als Arbeitsnachweis mit einer Kurzfassung der Einstellungen bestehen und enthält keine Daten von Gästen (Nummer 9 Abs. 6). Vor der Löschung einer Buchung, einer abgeschalteten Funktion oder eines nicht bestellten Fragebogens wird der Zustand frisch gelesen; bei einem Lesefehler wird nichts gelöscht. |
|
||||
| Protokolle | Die Programmprotokolle des Terminplaners und der Tagesbericht enthalten nur Zählungen und Buchungsnummern, keine Namen, E-Mail-Adressen oder Nachrichten von Gästen. Server-Protokolle des Webservers werden nach 14 Tagen gelöscht (Anlage 2 der Abo-Bedingungen). |
|
||||
| Datensicherung | Tägliche Vollsicherung des Servers durch Hetzner; verfügbar sind die letzten sieben Tagessicherungen (§ 3 Abs. 5 der Abo-Bedingungen). |
|
||||
| Rechenzentrum | Hetzner Online GmbH, Rechenzentren Falkenstein und Nürnberg (Deutschland). Hetzner ist nach ISO/IEC 27001 zertifiziert; der Geltungsbereich umfasst die Hosting-Dienstleistungen und die Rechenzentren. |
|
||||
| Organisation | Verpflichtung aller Beteiligten auf Vertraulichkeit, Ablauf für Datenschutzverletzungen (Nummer 8 Abs. 3), Archiv der Fassungen der Gästetexte (Nummer 8 Abs. 5), Überprüfung der Maßnahmen mindestens einmal im Jahr. |
|
||||
@@ -0,0 +1,157 @@
|
||||
# Auftragsverarbeitungsvertrag Buchungskalender (Art. 28 DSGVO)
|
||||
|
||||
**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen
|
||||
|
||||
zwischen dem Kunden als Verantwortlichem („Kunde“; in der Leistungsbeschreibung „Vermieter“) und der WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, vertreten durch die Gesellschafter Kenso Grimm und Justin Klein, als Auftragsverarbeiter („WEBklar“).
|
||||
|
||||
## Vorbemerkung: Abschluss und Verhältnis zu anderen Verträgen
|
||||
|
||||
(1) Der Kunde schließt diesen Vertrag, wenn er im Kundenportal die Funktion „Buchungskalender“ einschaltet und die dort angezeigte Zeile „Es gelten Leistungsbeschreibung und Auftragsverarbeitung (Fassung 2026-10-16)“ mit dem Klick auf „Einschalten“ bestätigt; diesem Vertrag und der Leistungsbeschreibung „Buchungskalender“ („Leistungsbeschreibung“) in der angezeigten Fassung stimmt er damit zu. Das Portal speichert den Wortlaut der angezeigten Zeile, die Fassungen, den Zeitpunkt, das Kundenkonto und die angemeldete Person. Anschließend sendet WEBklar dem Kunden eine E-Mail, die beide Texte in dieser Fassung als PDF enthält und auf sie verlinkt.
|
||||
|
||||
(2) Ein Mitarbeiter von WEBklar darf die Funktion auch ohne Bestellung im Portal einschalten (zum Beispiel für einen Kunden, dem WEBklar sie als Teil seines Website-Projekts bereitstellt), aber nur, wenn der Kunde vorher in Textform (etwa im Chat des Kundenportals oder per E-Mail) erklärt hat, dass er diesem Vertrag und der Leistungsbeschreibung in der aktuellen Fassung zustimmt. Das Portal verlangt dafür das Datum und den Weg der Erklärung; fehlt eine Angabe, schaltet es nicht ein. WEBklar speichert vor dem Einschalten einen Zustimmungsnachweis mit der Fassung der beiden Texte, dem Datum und dem Weg dieser Erklärung, dem Zeitpunkt des Einschaltens und dem Namen des Mitarbeiters (ohne IP-Adresse und Browser-Kennzeichen, weil der Kunde nicht im Portal zugestimmt hat). Der Kunde erhält anschließend eine E-Mail mit diesen beiden Texten als PDF.
|
||||
|
||||
(3) Dieser Vertrag ist ein eigener Vertrag für den Buchungskalender. Er ergänzt den Auftragsverarbeitungsvertrag in Anlage 1 der Abo-Bedingungen. Für den Buchungskalender geht er Anlage 1 vor; im Übrigen gilt Anlage 1 ergänzend. In Datenschutzfragen geht er den Abo-Bedingungen und der Leistungsbeschreibung vor.
|
||||
|
||||
(4) Die Rollen: Der Kunde ist Verantwortlicher für die personenbezogenen Daten seiner Gäste im Buchungskalender. WEBklar ist sein Auftragsverarbeiter. Stripe ist nicht Auftragsverarbeiter von WEBklar, sondern Vertragspartner des Kunden (Nummer 7 Abs. 4). Die Plattformen, deren Kalender der Kunde einbindet (zum Beispiel Booking.com oder Airbnb), sind weder Auftragsverarbeiter noch Unterauftragsverarbeiter von WEBklar.
|
||||
|
||||
## 1. Gegenstand und Dauer
|
||||
|
||||
(1) WEBklar betreibt für die öffentliche Website des Kunden ein Buchungs-Widget mit einer Buchungsseite und einer Verwaltung im Kundenportal („Buchungskalender“) und verarbeitet dabei personenbezogene Daten der Gäste des Kunden im Auftrag des Kunden. Den Umfang beschreibt die Leistungsbeschreibung.
|
||||
|
||||
(2) Der Vertrag beginnt mit dem Einschalten der Funktion. Er endet mit dem Abschalten, spätestens mit dem Ende des Wartungs-Abos; den Buchungskalender schaltet WEBklar dann spätestens am folgenden Tag ab (Nr. 10 Abs. 1 der Leistungsbeschreibung). Die Pflichten aus diesem Vertrag gelten fort, bis alle Daten nach Nummer 9 gelöscht sind.
|
||||
|
||||
## 2. Art und Zweck der Verarbeitung
|
||||
|
||||
(1) Art: Übermitteln der Anfrage und der Buchung vom Widget und von der Buchungsseite an den Server von WEBklar, Speichern, Verwalten und Anzeigen im Kundenportal, Berechnen des Preises, Erzeugen von Rechnungen und Stornorechnungen im Namen des Kunden (Nr. 3 Abs. 6 der Leistungsbeschreibung), Abgleichen von Kalendern, Versenden von E-Mails, Übernehmen von Zahlungsstatus aus Stripe, Weitergeben von Erstattungen an Stripe im Auftrag des Kunden, Anonymisieren und Löschen.
|
||||
|
||||
(2) Zwecke: Der Kunde soll Anfragen und Buchungen seiner Gäste abwickeln. Dazu nimmt der Buchungskalender Anfragen entgegen, reserviert Zeiträume nach der Freigabe durch den Kunden, nimmt die verbindliche Buchung des Gastes entgegen, stellt die Rechnung aus, gleicht Online-Zahlungen mit Stripe ab, bildet Zahlungen vor Ort ab, bildet Stornierungen ab und benachrichtigt den Kunden. WEBklar versendet die E-Mails im Auftrag des Kunden. Außerdem schützt WEBklar den Buchungskalender für den Kunden vor Missbrauch (Mengenbegrenzung, Begrenzung der Anfragen je IP-Adresse, Erkennen automatisierter Anfragen).
|
||||
|
||||
(3) WEBklar verwendet die Daten für keine eigenen Zwecke, auch nicht in anonymisierter Form und nicht für Auswertungen über mehrere Kunden. Es findet keine Werbung und keine Zusammenführung mit anderen Daten statt. WEBklar bildet keine Profile einzelner Gäste. Zählungen ohne Personenbezug, die WEBklar für den sicheren Betrieb braucht (etwa die Zahl der anonymisierten Anfragen je Tag oder die Länge der Versandwarteschlange), sind keine eigenen Zwecke.
|
||||
|
||||
## 3. Datenarten und Betroffene
|
||||
|
||||
(1) Daten der Gäste, die WEBklar aus dem Widget, von der Buchungsseite und aus Stripe erhält und speichert:
|
||||
- bei der Anfrage: Name, E-Mail-Adresse, auf Wunsch Firma, Telefonnummer und eine Nachricht, die gewünschte Unterkunft, der Zeitraum, die Zahl der Personen, die Anfragenummer, ein Hashwert der IP-Adresse (Absatz 4) und der Zeitpunkt;
|
||||
- bei der Freigabe: die Posten und Summen des Preises (Text, Menge, Einzelbetrag, Steuersatz), die Reservierungsfrist, eine Nachricht des Kunden an den Gast, die Stornobedingungen, die galten, und die Fassung der Rechtstexte, der der Gast gesehen hat, jeweils als Schnappschuss;
|
||||
- bei der Buchung: die Rechnungsadresse (Name, Firma, Straße, Postleitzahl, Ort, Land), die Zahlart, der Zeitpunkt der Buchung und die Rechnungsnummer;
|
||||
- bei der Zahlung und der Erstattung: Status, Zeitpunkt, Art (online über Stripe oder vor Ort), die Kennungen der Zahlung bei Stripe, erstattete Beträge und der Grund einer Stornierung;
|
||||
- die Rechnung und die Stornorechnung als unveränderlicher Schnappschuss (Angaben des Ausstellers, Empfänger, Unterkunft, Zeitraum, Posten, Summen, Steuerart, Bezug), aus dem WEBklar das PDF bei jedem Abruf erzeugt;
|
||||
- ein Hashwert des geheimen Zugangs zur Buchungsseite (der Link in der E-Mail wird nur als Prüfsumme gespeichert);
|
||||
- der Zustellstatus der E-Mails an den Gast.
|
||||
|
||||
**Kartendaten des Gastes erhält und speichert WEBklar nicht;** die gibt der Gast nur bei Stripe ein. Online-Zahlungen sind derzeit nur per Karte (einschließlich Apple Pay und Google Pay) möglich, nicht per SEPA-Lastschrift. Die Bankverbindung des Kunden selbst (Angabe auf der Rechnung) und seine Firmen- und Steuerangaben gehören nicht zu den Gästedaten.
|
||||
|
||||
(2) Von Kalendern anderer Plattformen übernimmt WEBklar nur die belegten Zeiträume (Datum von und bis), nicht Namen, Beschreibungen oder andere Angaben der dortigen Buchungen, auch wenn der Kalender sie enthält. Die iCal-Adresse, die WEBklar dem Kunden bereitstellt, enthält nur belegte Nächte, keine Angaben zu Gästen. Wer die geheime Adresse kennt, sieht die belegten Nächte; der Kunde kann sie jederzeit erneuern.
|
||||
|
||||
(3) Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO) werden nicht verarbeitet. Der Kunde fordert von seinen Gästen keine entsprechenden Angaben und bittet sie, solche Angaben nicht in das Nachrichtenfeld zu schreiben (Nummer 11 Abs. 4). Meldedaten nach dem Bundesmeldegesetz (Meldeschein) und Daten zur Kurtaxe verarbeitet der Buchungskalender nicht.
|
||||
|
||||
(4) IP-Adresse und Browser-Kennzeichen (User-Agent) erhält der Server von WEBklar technisch bei jedem Abruf des Widgets und jeder Anfrage. Die Mengenbegrenzung (Missbrauchsschutz) verwendet die IP-Adresse im Arbeitsspeicher des Servers; ein Neustart leert sie. Mit der Anfrage werden weder eine IP-Adresse im Klartext noch ein Browser-Kennzeichen gespeichert, sondern nur ein **Hashwert der IP-Adresse („IP-Hash“)**: ein SHA-256-Wert, den WEBklar mit einem geheimen Schlüssel des Servers salzt. Der IP-Hash ist ein pseudonymes technisches Datum und bleibt ein personenbezogenes Datum, weil WEBklar mit dem Schlüssel für dieselbe IP-Adresse denselben Wert berechnen kann; ohne den Schlüssel lässt sich aus ihm keine IP-Adresse ableiten. WEBklar verwendet ihn ausschließlich zum Missbrauchsschutz. Er dient nicht dazu, Gäste zu identifizieren, wiederzuerkennen oder zu profilieren, wird dem Kunden weder angezeigt noch exportiert und steht nur im Datensatz der Anfrage. WEBklar entfernt ihn aus dem Datensatz, sobald der Kunde die Anfrage freigibt oder ablehnt, spätestens nach 30 Tagen (Nummer 9 Abs. 1). In den Server-Protokollen stehen IP-Adresse und Browser-Kennzeichen 14 Tage (Anlage 2 der Abo-Bedingungen).
|
||||
|
||||
(5) Der Buchungskalender setzt keine Cookies. Das Widget speichert nichts im Browser. Auch die Buchungsseite legt weder Cookies noch Daten im lokalen oder im Sitzungsspeicher des Browsers ab.
|
||||
|
||||
(6) Betroffene: Gäste, die über die Website des Kunden Anfragen stellen oder buchen, und Personen, deren Angaben der Gast in der Anfrage oder Buchung macht (zum Beispiel Mitreisende oder abweichende Rechnungsempfänger). Beschäftigte des Kunden sind von diesem Vertrag nicht betroffen.
|
||||
|
||||
## 4. Weisungen
|
||||
|
||||
(1) WEBklar verarbeitet die Daten nur auf dokumentierte Weisung des Kunden, auch in Bezug auf eine Übermittlung in ein Drittland oder an eine internationale Organisation. Das gilt nicht, wenn das Recht der Europäischen Union oder Deutschlands WEBklar zu einer Verarbeitung verpflichtet. In diesem Fall teilt WEBklar dem Kunden die Pflicht vorher mit, soweit das Gesetz es nicht verbietet.
|
||||
|
||||
(2) Mit dem Einschalten weist der Kunde WEBklar an, den Buchungskalender nach seinen Einstellungen und der Leistungsbeschreibung zu betreiben, in seinem Namen Rechnungen auszustellen und Zahlungen, die nach einer Stornierung oder nach dem Ablauf einer Reservierung eingehen, sowie zweite Zahlungen auf eine bereits bezahlte Buchung ohne weitere Anweisung in voller Höhe über sein Stripe-Konto zu erstatten (dauerhafte Weisung; der Stand der Buchung ändert sich dabei nicht). Bei einer Stornierung schließt WEBklar eine noch offene Zahlungsseite bei Stripe. Weitere Weisungen ergeben sich aus den Einstellungen und Aktionen im Kundenportal (Unterkünfte, Preise, Sperren, Kalenderverbindungen, Freigabe und Ablehnung von Anfragen, Zahlungseingang vor Ort, Stornierungen und Erstattungen, Online-Zahlung, Öffnen und Schließen des Buchungskalenders). Sonstige Weisungen erteilt der Kunde in Textform, etwa im Chat des Kundenportals. Weisungen, die über diese Einstellungen hinausgehen, setzt WEBklar nur um, wenn das technisch vorgesehen ist; sonst kann der Kunde den Buchungskalender abschalten lassen (Nr. 10 Abs. 7 der Leistungsbeschreibung). Die Anonymisierung und Löschung auf Weisung nach Nummer 8 Abs. 2 und Nummer 9 Abs. 6 setzt WEBklar immer um, soweit Aufbewahrungspflichten dem nicht entgegenstehen (Nummer 9 Abs. 8).
|
||||
|
||||
(3) Hält WEBklar eine Weisung für rechtswidrig, weist es den Kunden unverzüglich darauf hin. WEBklar darf die Ausführung aussetzen, bis der Kunde die Weisung ändert oder die Rechtslage geklärt ist.
|
||||
|
||||
## 5. Vertraulichkeit
|
||||
|
||||
WEBklar setzt nur Personen ein, die zur Vertraulichkeit verpflichtet sind oder einer gesetzlichen Verschwiegenheitspflicht unterliegen.
|
||||
|
||||
## 6. Technische und organisatorische Maßnahmen
|
||||
|
||||
(1) WEBklar trifft die Maßnahmen nach Art. 32 DSGVO, die in der Anlage zu diesem Vertrag beschrieben sind. WEBklar darf sie an den Stand der Technik anpassen, sofern das Schutzniveau nicht sinkt.
|
||||
|
||||
(2) Für den Server gelten ergänzend die Maßnahmen der Anlage 2 der Abo-Bedingungen, soweit die Anlage zu diesem Vertrag nichts anderes regelt.
|
||||
|
||||
## 7. Unterauftragsverarbeiter
|
||||
|
||||
(1) Der Kunde genehmigt den Einsatz der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, für den Betrieb des Servers, auf dem der Buchungskalender läuft, und für dessen Sicherung (Rechenzentrum Falkenstein, Deutschland) sowie für den Versand der E-Mails über den SMTP-Dienst des Webhostings (Rechenzentrum Nürnberg, Deutschland). Eine Übermittlung in Drittländer findet nicht statt.
|
||||
|
||||
(2) Will WEBklar einen weiteren Unterauftragsverarbeiter hinzuziehen oder einen ersetzen, teilt es das dem Kunden mindestens vier Wochen vorher in Textform mit. Der Kunde kann innerhalb von zwei Wochen nach Zugang der Mitteilung aus wichtigem datenschutzrechtlichem Grund in Textform widersprechen. WEBklar setzt den neuen Unterauftragsverarbeiter nicht vor Ablauf dieser Frist ein. Kündigt ein Unterauftragsverarbeiter von WEBklar eine eigene Änderung mit kürzerer Frist an, informiert WEBklar den Kunden unverzüglich, nachdem die Ankündigung eingegangen ist; der Kunde kann dann unverzüglich widersprechen. Einigen sich die Parteien nicht, kann der Kunde den Buchungskalender abschalten lassen und WEBklar ihn für diese Website zu dem Zeitpunkt beenden, zu dem der neue Unterauftragsverarbeiter eingesetzt wird. Die Daten werden dann nach Nummer 9 gelöscht, auf Weisung des Kunden früher (Nummer 9 Abs. 6).
|
||||
|
||||
(3) WEBklar verpflichtet jeden Unterauftragsverarbeiter vertraglich auf die Datenschutzpflichten nach Art. 28 Abs. 4 DSGVO. Bei der Hetzner Online GmbH ist das deren Vertrag zur Auftragsverarbeitung, den WEBklar abgeschlossen hat. Kommt ein Unterauftragsverarbeiter seinen Datenschutzpflichten nicht nach, haftet WEBklar dem Kunden für die Einhaltung dieser Pflichten (Art. 28 Abs. 4 Satz 2 DSGVO).
|
||||
|
||||
(4) **Stripe ist kein Unterauftragsverarbeiter von WEBklar.** Der Kunde verwendet sein eigenes Stripe-Konto und hat den Vertrag darüber unmittelbar mit Stripe geschlossen (Nr. 3 der Leistungsbeschreibung); die Zahlung des Gastes fließt direkt auf dieses Konto. Die Zahlungsseite (Stripe Checkout), die Erhebung von Zahlungs- und Kartendaten (derzeit nur Karte, einschließlich Apple Pay und Google Pay), die Prüfung von Zahlungen und die Auszahlungen verantworten der Kunde und Stripe nach ihrem Vertrag; WEBklar erhält diese Daten nicht. Auf Weisung des Kunden legt WEBklar bei Stripe die Zahlung an, ruft die Angaben zu Zahlungen ab, die der Buchungskalender braucht (Nummer 2 Abs. 1), schließt offene Zahlungsseiten und veranlasst Erstattungen (auch die automatischen nach Nummer 4 Abs. 2). An Stripe gehen dabei von WEBklar nur: die Bezeichnung des Aufenthalts mit Rechnungsnummer als Zahlungsposition, der Betrag, die Kennungen der Buchung und der Zahlung (Metadaten), die E-Mail-Adresse des Gastes zur Vorbelegung der Zahlungsseite, soweit WEBklar sie übergibt, die Erstattung und der **geheime Link zur Buchungsseite mit dem Token als Rücksprungadresse** (bei Erfolg und bei Abbruch). Dieser Link ist damit für Stripe und im Stripe-Dashboard des Kunden sichtbar; Der Link gilt bis 90 Tage nach der Abreise (bei nicht zustande gekommenen Buchungen 30 Tage) und öffnet nur diese eine Buchungsseite. Die Weitergabe von Daten durch Stripe, auch an Stellen außerhalb der Europäischen Union, regeln der Kunde und Stripe; dafür ist WEBklar nicht verantwortlich.
|
||||
|
||||
(5) Plattformen, deren iCal-Kalender der Kunde einbindet oder denen er die iCal-Adresse des Buchungskalenders mitteilt, sind Empfänger nach seiner Entscheidung. WEBklar übermittelt ihnen keine Daten von Gästen.
|
||||
|
||||
## 8. Unterstützung des Kunden, Betroffenenrechte
|
||||
|
||||
(1) WEBklar leitet Anfragen von Gästen, die an WEBklar gerichtet sind, unverzüglich an den Kunden weiter. WEBklar unterstützt ihn mit geeigneten technischen und organisatorischen Maßnahmen bei der Beantwortung.
|
||||
|
||||
(2) Dafür bietet das Kundenportal die Anzeige der Anfragen und Buchungen, mit denen der Kunde Auskunft erteilen kann, und den Abruf der Rechnungen als PDF. Auf Weisung des Kunden in Textform (etwa im Chat des Kundenportals) anonymisiert WEBklar die Gästedaten einer Anfrage oder Buchung unverzüglich: Name, Firma, E-Mail-Adresse, Telefonnummer und die Freitexte werden gelöscht oder unkenntlich gemacht (ein IP-Hash ist zu diesem Zeitpunkt in der Regel längst entfernt); Anfragenummer, Zeitraum, Beträge und Status bleiben. Rechnungen und Rechnungsadresse einer gebuchten Anfrage bleiben, solange eine Aufbewahrungspflicht besteht (Nummer 9 Abs. 3). Dabei löscht WEBklar die E-Mails zu dieser Anfrage, die noch auf den Versand warten. E-Mails, deren Versand endgültig fehlgeschlagen ist, behalten Empfängeradresse und Inhalt, bis WEBklar sie nach Nummer 9 Abs. 5 löscht (48 Stunden nach dem Fehlschlag).
|
||||
|
||||
(3) Verletzungen des Schutzes personenbezogener Daten meldet WEBklar dem Kunden unverzüglich, möglichst innerhalb von 24 Stunden nach Kenntnis, mit den Informationen, die zu diesem Zeitpunkt vorliegen.
|
||||
|
||||
(4) WEBklar unterstützt den Kunden bei seinen Pflichten aus Art. 32 bis 36 DSGVO. Dazu gehören die Angaben, die er für sein Verzeichnis von Verarbeitungstätigkeiten und für eine etwaige Datenschutz-Folgenabschätzung braucht.
|
||||
|
||||
(5) Die Gäste informiert der Kunde (Art. 13 DSGVO; Nummer 11 Abs. 2).
|
||||
|
||||
## 9. Löschung und Rückgabe
|
||||
|
||||
(1) Den IP-Hash entfernt WEBklar aus dem Datensatz einer Anfrage, sobald der Kunde sie freigibt oder ablehnt, spätestens 30 Tage nach dem Anlegen.
|
||||
|
||||
(2) Anfragen, aus denen keine Buchung wurde (abgelehnt, abgelaufen, oder neu und nicht innerhalb von 60 Tagen entschieden), anonymisiert WEBklar nach 6 Monaten: Name, Firma, E-Mail-Adresse, Telefonnummer und Nachricht werden gelöscht. Anfragenummer, Zeitraum, Status und, soweit vorhanden, der Preisvorschlag bleiben ohne Personenbezug erhalten.
|
||||
|
||||
(3) Bei gebuchten Aufenthalten anonymisiert WEBklar die Gästedaten (Name, Firma, E-Mail-Adresse, Telefonnummer und Nachricht) 3 Jahre nach dem Ende des Kalenderjahres, in dem der Gast abgereist ist (Abreisejahr; maßgeblich ist die Zeit in Berlin). **Rechnungen, Stornorechnungen und die Rechnungsadresse bleiben 8 Jahre erhalten**, gerechnet ab dem Ende des Kalenderjahres der Rechnung, solange der Buchungskalender läuft (Buchungsbelege, § 147 AO); sie sind für den Kunden als Aussteller bestimmt. Anfragenummer, Zeitraum, Beträge und Status bleiben ebenfalls.
|
||||
|
||||
(4) Die Anonymisierung nach den Absätzen 2 und 3 sowie die Entfernung des IP-Hash laufen im täglichen Löschlauf; anonymisiert oder entfernt wird frühestens nach Ablauf der jeweiligen Frist. Ist an einem Tag mehr zu bearbeiten, als ein Lauf schafft, folgt der Rest im nächsten Lauf.
|
||||
|
||||
(5) In der Versandwarteschlange für E-Mails leert WEBklar die Empfängeradresse und den Inhalt einer E-Mail nach dem Versand und löscht den Eintrag 7 Tage nach dem Versand. E-Mails, die nicht zugestellt werden können, löscht WEBklar mit Empfängeradresse und Inhalt 48 Stunden nach dem endgültigen Fehlschlag.
|
||||
|
||||
(6) Ist der Buchungskalender abgeschaltet oder das Wartungs-Abo beendet, kann der Kunde die Anfragen, Buchungen und Rechnungen 90 Tage lang im Kundenportal lesen und die Rechnungen einzeln als PDF herunterladen; WEBklar weist ihn nach dem Abschalten per E-Mail mit dem Löschdatum darauf hin, seine Rechnungen zu sichern. Danach löscht WEBklar alle Daten des Buchungskalenders: Unterkünfte, Preise, Anfragen, Buchungen, Rechnungen, Kalenderverbindungen, Sperren und Einstellungen. Damit endet auch die Aufbewahrung der Rechnungen nach Absatz 3; sie bewahrt der Kunde dann selbst auf. Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Angaben zur Einrichtung bestehen (zum Beispiel Adresse der Website, Einbau-Code und die Angaben des Kunden aus der Anfrage) und enthält keine Daten von Gästen; dafür gilt die Datenschutzerklärung von WEBklar (Abschnitt 6). Beginnt innerhalb dieser Frist wieder ein Wartungs-Abo, schaltet WEBklar den Buchungskalender auf Wunsch des Kunden mit den vorhandenen Daten wieder ein (Nr. 10 Abs. 5 der Leistungsbeschreibung). Früher anonymisiert WEBklar einzelne Daten auf Weisung des Kunden (Nummer 8 Abs. 2); Wird das Kundenkonto gelöscht, schaltet WEBklar den Buchungskalender ab und löscht seine Daten wie nach Absatz 6 (bei Abschaltung 90 Tage später); einen Hinweis per E-Mail gibt es dann nicht, weil mit dem Konto auch die E-Mail-Adresse gelöscht wird. alle Daten des Buchungskalenders löscht WEBklar auf Weisung des Kunden in Textform unverzüglich.
|
||||
|
||||
(7) Die Daten sind in der täglichen Vollsicherung des Servers enthalten (§ 3 Abs. 5 der Abo-Bedingungen). Gelöschte oder anonymisierte Daten bleiben dort bis zu sieben Tage erhalten und werden dann überschrieben. Wird eine Sicherung zurückgespielt, holt der tägliche Löschlauf die fälligen Löschungen und Anonymisierungen nach; Löschungen auf Weisung des Kunden wiederholt WEBklar.
|
||||
|
||||
(8) Gesetzliche Aufbewahrungspflichten bleiben unberührt. Der Kunde ist für seine eigenen Aufbewahrungspflichten (zum Beispiel für Rechnungen und Buchungsbelege) selbst verantwortlich; WEBklar ist kein Archiv im Sinne der Buchführung. Die Aufbewahrung der Rechnungen nach Absatz 3 geschieht als Dienst für den Kunden und für die Dauer des Buchungskalenders. Der Kunde sichert die Belege, die er braucht, rechtzeitig vor der Löschung nach Absatz 6 durch den Download. Soweit eine gesetzliche Aufbewahrungspflicht besteht, löscht oder anonymisiert WEBklar auch auf Weisung des Kunden nicht früher; stattdessen sperrt es die Daten für die weitere Verarbeitung.
|
||||
|
||||
## 10. Nachweise und Kontrollen
|
||||
|
||||
WEBklar stellt dem Kunden alle Informationen zur Verfügung, die zum Nachweis der Pflichten aus Art. 28 DSGVO nötig sind, und ermöglicht Überprüfungen einschließlich Inspektionen durch den Kunden oder einen von ihm beauftragten, zur Verschwiegenheit verpflichteten Prüfer. Sie finden nach Anmeldung mit angemessener Frist (in der Regel 14 Tage) zu den Servicezeiten statt, ohne den Betrieb unzumutbar zu stören. Für das Rechenzentrum genügen in der Regel die Nachweise des Betreibers (Zertifizierung der Hetzner Online GmbH nach ISO/IEC 27001). Für mehr als eine Vor-Ort-Kontrolle pro Jahr kann WEBklar eine angemessene Vergütung verlangen.
|
||||
|
||||
## 11. Pflichten des Kunden
|
||||
|
||||
(1) Der Kunde ist Verantwortlicher für die Verarbeitung der Daten seiner Gäste im Buchungskalender.
|
||||
|
||||
(2) Der Kunde informiert seine Gäste nach Art. 13 DSGVO. Dazu nimmt er den Datenschutz-Absatz für den Buchungskalender in seine Datenschutzerklärung auf (Nr. 5 Abs. 10 der Leistungsbeschreibung) und nennt in seinen Rechtstexten (Datenschutzerklärung, Impressum) die Angaben, die das Gesetz verlangt.
|
||||
|
||||
(3) Der Kunde schließt mit Stripe den Vertrag über sein Stripe-Konto selbst, wenn er die Online-Zahlung nutzt, und verantwortet die Verarbeitung von Daten durch Stripe, soweit sie nicht in diesem Vertrag geregelt ist (Nummer 7 Abs. 4).
|
||||
|
||||
(4) Der Kunde fordert von seinen Gästen keine Angaben zu ihrer Gesundheit oder zu anderen besonderen Kategorien personenbezogener Daten und weist sie auf der Website darauf hin, solche Angaben nicht in das Nachrichtenfeld zu schreiben.
|
||||
|
||||
(5) Der Kunde verändert den Einbau-Code und die gesetzlich vorgeschriebenen Hinweise des Widgets und der Buchungsseite nicht (Nr. 5 Abs. 9 der Leistungsbeschreibung).
|
||||
|
||||
(6) Der Kunde erfüllt seine Pflichten zu Meldeschein, Kurtaxe und sonstigen Meldungen an Behörden selbst und verarbeitet die dafür nötigen Daten außerhalb des Buchungskalenders (Nr. 5 Abs. 6 der Leistungsbeschreibung).
|
||||
|
||||
## 12. Haftung, Änderungen, Schlussbestimmungen
|
||||
|
||||
(1) Gegenüber betroffenen Personen gilt Art. 82 DSGVO. Für die Haftung zwischen Kunde und WEBklar gilt § 13 der Abo-Bedingungen.
|
||||
|
||||
(2) Eine neue Fassung dieses Vertrags oder der Leistungsbeschreibung, auch wegen geänderter Datenerhebung, teilt WEBklar dem Kunden mindestens sechs Wochen vor ihrem Wirksamwerden in Textform mit. Sie wird wirksam, wenn der Kunde ihr im Kundenportal oder in Textform zustimmt. Stimmt er nicht zu, gilt die bisherige Fassung weiter.
|
||||
|
||||
(3) Ist die neue Fassung nötig, weil ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form nicht mehr zulässt, und stimmt der Kunde ihr nicht zu, darf WEBklar den Buchungskalender für seine Website nach Nr. 12 Abs. 2 der Leistungsbeschreibung beenden oder nach Nr. 12 Abs. 3 der Leistungsbeschreibung aussetzen. Das Abo bleibt davon unberührt.
|
||||
|
||||
(4) Im Übrigen gilt § 16 der Abo-Bedingungen.
|
||||
|
||||
## Anlage: Technische und organisatorische Maßnahmen des Buchungskalenders (Art. 32 DSGVO)
|
||||
|
||||
| Bereich | Maßnahmen |
|
||||
|---|---|
|
||||
| Übertragung | Das Widget, die Buchungsseite und das Kundenportal werden nur über TLS (HTTPS) übertragen. Der Einbau-Code verweist auf eine Adresse mit https://. Die Zahlung geschieht auf der Zahlungsseite von Stripe. Kalender anderer Plattformen ruft WEBklar nur über https ab. |
|
||||
| Speicher im Browser | Es werden keine Cookies gesetzt. Das Widget sendet seine Anfragen ohne Cookies und ruft nur die Schnittstelle des Buchungskalenders auf, keine Dienste Dritter. |
|
||||
| Zugriffskontrolle | Das Kundenportal ist nur nach Anmeldung erreichbar; jeder Kunde sieht nur die Daten seiner eigenen Projekte. Die öffentliche Schnittstelle des Widgets liefert nur für einen vorhandenen Buchungskalender Daten und ist über den Website-Schlüssel (Site-Key) adressiert; die belegten Nächte nennt sie ohne Quelle und ohne Daten von Gästen. Die Buchungsseite ruft der Gast nur mit einem geheimen Token ab, das nur als Prüfsumme gespeichert und zeitkonstant verglichen wird. Der Token wird nur als Prüfsumme gespeichert; er wird nur als Teil der Rücksprungadresse an Stripe übergeben (Nummer 7 Abs. 4) und öffnet nur diese eine Buchungsseite; er gilt bis 90 Tage nach der Abreise (bei nicht zustande gekommenen Buchungen 30 Tage), danach antwortet die Buchungsseite wie bei einem unbekannten Link. Benachrichtigungen von Stripe werden nur nach Prüfung ihrer Signatur verarbeitet; die Schlüssel für Stripe liegen nur auf dem Server. Die iCal-Adresse des Buchungskalenders ist ein geheimer, erneuerbarer Zufallswert. |
|
||||
| Missbrauchsschutz | Ein verstecktes Feld (Honeypot), eine Mindestzeit bis zum Absenden des Formulars mit signiertem Formular-Token und Grenzen je IP-Adresse und je Buchungskalender schützen vor automatisierten Anfragen. Die Grenzen gelten im Arbeitsspeicher; ein Neustart leert sie. Mit der Anfrage wird nur ein gesalzener IP-Hash gespeichert (Nummer 3 Abs. 4). Beim Abruf von Kalendern anderer Plattformen sind nur Adressen mit https erlaubt, private und lokale Ziele sind gesperrt, und Größe und Dauer des Abrufs sind begrenzt. |
|
||||
| Datenminimierung | Kartendaten erhält und speichert WEBklar nicht. Preise berechnet der Server aus den gespeicherten Preisen und Posten, nie aus Angaben des Browsers. Von anderen Kalendern werden nur Zeiträume übernommen. Mit der Anfrage werden keine IP-Adresse im Klartext und kein Browser-Kennzeichen gespeichert; der IP-Hash wird mit der Entscheidung des Kunden entfernt. Die Gästedaten werden nach den Fristen der Nummer 9 anonymisiert. Nach dem Versand einer E-Mail werden Empfängeradresse und Inhalt in der Warteschlange geleert. |
|
||||
| Integrität der Rechnungen | Rechnungen und Stornorechnungen werden als unveränderlicher Schnappschuss gespeichert; das PDF entsteht bei jedem Abruf aus diesem Schnappschuss. Rechnungsnummern werden unter Sperre lückenlos je Kunde und Jahr vergeben. Freigabe und Buchung prüfen unter Sperre erneut, dass der Zeitraum frei ist. |
|
||||
| Trennung | Alle Unterkünfte, Anfragen und Rechnungen tragen die Kennung des Kunden, des Projekts und der Funktion; das Kundenportal und die öffentliche Schnittstelle lesen immer nur die Daten des jeweiligen Buchungskalenders. Die Hintergrundläufe (Ablauf von Reservierungen, Kalenderabgleich, Versand der E-Mails, Anonymisierung und Löschung) schreiben jeweils nur in das Dokument, um das es geht. |
|
||||
| Löschung | Täglicher Löschlauf für Anfragen, Buchungen und Warteschlange nach den Fristen der Nummer 9; die Daten abgeschalteter Buchungskalender 90 Tage nach dem Abschalten; das Einrichtungs-Ticket bleibt als Arbeitsnachweis mit einer Kurzfassung der Angaben zur Einrichtung bestehen und enthält keine Daten von Gästen (Nummer 9 Abs. 6). |
|
||||
| Protokolle | Die Programmprotokolle des Buchungskalenders und der Tagesbericht enthalten nur Zählungen und Anfragenummern, keine Namen, E-Mail-Adressen oder Adressen von Gästen. Server-Protokolle des Webservers werden nach 14 Tagen gelöscht (Anlage 2 der Abo-Bedingungen). |
|
||||
| Datensicherung | Tägliche Vollsicherung des Servers durch Hetzner; verfügbar sind die letzten sieben Tagessicherungen (§ 3 Abs. 5 der Abo-Bedingungen). |
|
||||
| Rechenzentrum | Hetzner Online GmbH, Rechenzentren Falkenstein und Nürnberg (Deutschland). Hetzner ist nach ISO/IEC 27001 zertifiziert; der Geltungsbereich umfasst die Hosting-Dienstleistungen und die Rechenzentren. |
|
||||
| Organisation | Verpflichtung aller Beteiligten auf Vertraulichkeit, Ablauf für Datenschutzverletzungen (Nummer 8 Abs. 3), Überprüfung der Maßnahmen mindestens einmal im Jahr. |
|
||||
@@ -0,0 +1,145 @@
|
||||
# Auftragsverarbeitungsvertrag Webshop (Art. 28 DSGVO)
|
||||
|
||||
**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen
|
||||
|
||||
zwischen dem Kunden als Verantwortlichem („Kunde“; in der Leistungsbeschreibung „Händler“) und der WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, vertreten durch die Gesellschafter Kenso Grimm und Justin Klein, als Auftragsverarbeiter („WEBklar“).
|
||||
|
||||
## Vorbemerkung: Abschluss und Verhältnis zu anderen Verträgen
|
||||
|
||||
(1) Der Kunde schließt diesen Vertrag, wenn er im Kundenportal die Funktion „Webshop“ einschaltet und die dort angezeigte Zeile „Es gelten Leistungsbeschreibung und Auftragsverarbeitung (Fassung 2026-10-16)“ mit dem Klick auf „Einschalten“ bestätigt; diesem Vertrag und der Leistungsbeschreibung „Webshop“ („Leistungsbeschreibung“) in der angezeigten Fassung stimmt er damit zu. Das Portal speichert den Wortlaut der angezeigten Zeile, die Fassungen, den Zeitpunkt, das Kundenkonto und die angemeldete Person. Anschließend sendet WEBklar dem Kunden eine E-Mail, die beide Texte in dieser Fassung als PDF enthält und auf sie verlinkt.
|
||||
|
||||
(2) Ein Mitarbeiter von WEBklar darf die Funktion nur einschalten, wenn der Kunde vorher in Textform (etwa im Chat des Kundenportals oder per E-Mail) erklärt hat, dass er diesem Vertrag und der Leistungsbeschreibung in der aktuellen Fassung zustimmt. Das Portal verlangt dafür das Datum und den Weg der Erklärung; fehlt eine Angabe, schaltet es nicht ein. WEBklar speichert vor dem Einschalten einen Zustimmungsnachweis mit der Fassung der beiden Texte, dem Datum und dem Weg dieser Erklärung, dem Zeitpunkt des Einschaltens und dem Namen des Mitarbeiters (ohne IP-Adresse und Browser-Kennzeichen, weil der Kunde nicht im Portal zugestimmt hat). Der Kunde erhält anschließend eine E-Mail mit diesen beiden Texten als PDF.
|
||||
|
||||
(3) Dieser Vertrag ist ein eigener Vertrag für den Webshop. Er ergänzt den Auftragsverarbeitungsvertrag in Anlage 1 der Abo-Bedingungen. Für den Webshop geht er Anlage 1 vor; im Übrigen gilt Anlage 1 ergänzend. In Datenschutzfragen geht er den Abo-Bedingungen und der Leistungsbeschreibung vor.
|
||||
|
||||
(4) Die Rollen: Der Kunde ist Verantwortlicher für die personenbezogenen Daten seiner Käufer im Webshop. WEBklar ist sein Auftragsverarbeiter. Stripe ist nicht Auftragsverarbeiter von WEBklar, sondern Vertragspartner des Kunden (Nummer 7 Abs. 4).
|
||||
|
||||
## 1. Gegenstand und Dauer
|
||||
|
||||
(1) WEBklar betreibt für die öffentliche Website des Kunden ein Shop-Widget mit einer Bestellverwaltung im Kundenportal („Webshop“) und verarbeitet dabei personenbezogene Daten der Käufer des Kunden im Auftrag des Kunden. Den Umfang beschreibt die Leistungsbeschreibung.
|
||||
|
||||
(2) Der Vertrag beginnt mit dem Einschalten der Funktion. Er endet mit dem Abschalten, spätestens mit dem Ende des Wartungs-Abos; den Webshop schaltet WEBklar dann spätestens am folgenden Tag ab (Nr. 10 Abs. 1 der Leistungsbeschreibung). Die Pflichten aus diesem Vertrag gelten fort, bis alle Daten nach Nummer 9 gelöscht sind.
|
||||
|
||||
## 2. Art und Zweck der Verarbeitung
|
||||
|
||||
(1) Art: Übermitteln der Bestellung vom Widget an den Server von WEBklar, Übernehmen der Zahlungs- und Adressangaben aus der Checkout-Sitzung bei Stripe, Speichern, Verwalten und Anzeigen im Kundenportal, Versenden von E-Mails, Bereitstellen einer CSV-Datei, Weitergeben von Erstattungen an Stripe im Auftrag des Kunden, Anonymisieren und Löschen.
|
||||
|
||||
(2) Zwecke: Der Kunde soll Bestellungen seiner Käufer abwickeln. Dazu nimmt der Webshop Bestellungen entgegen, reserviert den Bestand, gleicht Zahlungen mit Stripe ab, bestätigt Bestellungen (bei einer Zahlung, die noch aussteht, etwa einer SEPA-Lastschrift, zunächst nur mit einer Eingangsmail ohne Vertragsschluss), bildet Versand und Abholung ab, nimmt Widerrufe entgegen, erstattet Zahlungen im Auftrag des Kunden und benachrichtigt den Kunden. WEBklar versendet die E-Mails im Auftrag des Kunden. Außerdem schützt WEBklar den Webshop für den Kunden vor Missbrauch (Mengenbegrenzung, Begrenzung der offenen Bestellungen je IP-Adresse, Erkennen automatisierter Bestellungen und Widerrufe).
|
||||
|
||||
(3) WEBklar verwendet die Daten für keine eigenen Zwecke, auch nicht in anonymisierter Form und nicht für Auswertungen über mehrere Kunden. Es findet keine Werbung und keine Zusammenführung mit anderen Daten statt. WEBklar bildet keine Profile einzelner Käufer. Zählungen ohne Personenbezug, die WEBklar für den sicheren Betrieb braucht (etwa die Zahl der anonymisierten Bestellungen je Tag oder die Länge der Versandwarteschlange), sind keine eigenen Zwecke.
|
||||
|
||||
## 3. Datenarten und Betroffene
|
||||
|
||||
(1) Daten der Käufer, die WEBklar aus der Checkout-Sitzung bei Stripe und aus dem Widget erhält und speichert: Name, E-Mail-Adresse, Lieferadresse und Rechnungsadresse (die Rechnungsadresse nur, soweit die Zahlungsseite von Stripe sie für die gewählte Zahlungsart abfragt); die Bestellung (Bestellnummer, Positionen mit Namen, Mengen und Preisen zum Zeitpunkt der Bestellung, Versandkosten, Lieferart, Steuerausweis, Gesamtbetrag); der Status der Bestellung mit den Zeitpunkten (bestellt, bezahlt, versendet, abholbereit, abgeschlossen, storniert, erstattet); Zahlungsstatus, Zahlungsart (zum Beispiel Karte oder SEPA-Lastschrift) und Kennungen der Zahlung bei Stripe; Versanddienst und Sendungsnummer, wenn der Kunde sie einträgt; Erstattungen mit Betrag, Zeitpunkt und Grund; die Prüfsumme (Hash) der Rechtstexte des Kunden, die der Käufer bei der Bestellung gesehen hat (den Wortlaut der Fassung verwahrt das Archiv nach Nummer 8 Abs. 5); ein Hashwert der IP-Adresse (Absatz 4); der Zustellstatus der E-Mails an den Käufer, auch der Eingangsmail bei ausstehender Zahlung. **Kartendaten und Kontodaten (IBAN) erhält und speichert WEBklar nicht;** die gibt der Käufer nur bei Stripe ein. **Eine Telefonnummer des Käufers erhebt und speichert WEBklar nicht:** Das Widget hat kein Telefonfeld, die Zahlungsseite von Stripe ist so eingestellt, dass sie keine Telefonnummer verlangt, und eine Telefonnummer, die Stripe in den Kundendaten der Sitzung dennoch übermitteln sollte, übernimmt WEBklar nicht in die Bestellung. Die Telefonnummer des Kunden selbst (Anbieterangaben in seinen Einstellungen) gehört nicht zu den Käuferdaten.
|
||||
|
||||
(2) Daten von Personen, die über die Widerrufsfunktion einen Vertrag widerrufen (Käufer und Interessenten, auch ohne passende Bestellung): Name, E-Mail-Adresse, Bestellnummer, die Erklärung als Freitext und der Zeitpunkt des Eingangs, außerdem der Zeitpunkt der Eingangsbestätigung und der Bearbeitung durch den Kunden.
|
||||
|
||||
(3) Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO) werden nicht verarbeitet. Waren, aus deren Kauf sich besondere Kategorien ergeben können, etwa Arzneimittel, sind nach Nr. 3 der Leistungsbeschreibung ausgeschlossen; der Kunde fordert von seinen Käufern keine entsprechenden Angaben (Nummer 11 Abs. 4).
|
||||
|
||||
(4) IP-Adresse und Browser-Kennzeichen (User-Agent) erhält der Server von WEBklar technisch bei jedem Abruf des Widgets und jeder Anfrage. Die Mengenbegrenzung (Missbrauchsschutz) verwendet die IP-Adresse im Arbeitsspeicher des Servers; ein Neustart leert sie. Mit dem Widerruf wird keine IP-Adresse und kein Browser-Kennzeichen gespeichert. Mit der Bestellung werden ebenfalls weder eine IP-Adresse im Klartext noch ein Browser-Kennzeichen gespeichert, sondern nur ein **Hashwert der IP-Adresse („IP-Hash“)**: die ersten 16 Hexadezimalzeichen eines SHA-256-Werts, den WEBklar mit einem geheimen Schlüssel des Servers salzt. Der IP-Hash ist ein pseudonymes technisches Datum und bleibt ein personenbezogenes Datum, weil WEBklar mit dem Schlüssel für dieselbe IP-Adresse denselben Wert berechnen kann; ohne den Schlüssel lässt sich aus ihm keine IP-Adresse ableiten. WEBklar verwendet ihn ausschließlich zum Missbrauchsschutz: Je IP-Hash und Webshop sind höchstens drei offene (noch nicht bezahlte) Bestellungen innerhalb von 30 Minuten zulässig. Er dient nicht dazu, Käufer zu identifizieren, wiederzuerkennen oder zu profilieren, wird dem Kunden weder angezeigt noch exportiert und steht nur im Datensatz der Bestellung. WEBklar löscht ihn aus dem Datensatz der Bestellung, sobald die Bestellung bezahlt oder storniert wird, spätestens mit der Löschung nicht bezahlter Bestellungen nach 30 Tagen (Nummer 9 Abs. 1); bei bezahlten Bestellungen bleibt er nicht bis zur Anonymisierung (Nummer 9 Abs. 2) erhalten. In den Server-Protokollen stehen IP-Adresse und Browser-Kennzeichen 14 Tage (Anlage 2 der Abo-Bedingungen).
|
||||
|
||||
(5) Der Webshop setzt keine Cookies. Das Widget speichert im Browser des Besuchers nur den Warenkorb (Kennungen der Artikel und Mengen, ohne Personenbezug) im Sitzungsspeicher (sessionStorage) des Tabs, solange der Tab geöffnet ist; das ist für den Warenkorb technisch erforderlich. Andere Daten legt das Widget im Browser nicht ab.
|
||||
|
||||
(6) Betroffene: Käufer, die über die Website des Kunden Waren bestellen, und Personen, die über die Widerrufsfunktion einen Vertrag widerrufen. Beschäftigte des Kunden sind von diesem Vertrag nicht betroffen.
|
||||
|
||||
## 4. Weisungen
|
||||
|
||||
(1) WEBklar verarbeitet die Daten nur auf dokumentierte Weisung des Kunden, auch in Bezug auf eine Übermittlung in ein Drittland oder an eine internationale Organisation. Das gilt nicht, wenn das Recht der Europäischen Union oder Deutschlands WEBklar zu einer Verarbeitung verpflichtet. In diesem Fall teilt WEBklar dem Kunden die Pflicht vorher mit, soweit das Gesetz es nicht verbietet.
|
||||
|
||||
(2) Mit dem Einschalten weist der Kunde WEBklar an, den Webshop nach seinen Einstellungen und der Leistungsbeschreibung zu betreiben. Weitere Weisungen ergeben sich aus den Einstellungen und Aktionen im Kundenportal (Artikel, Versand, Abholung, Zahlungsarten, Rechtstexte, Bestellstatus, Erstattungen, Öffnen und Schließen des Webshops, Export). Sonstige Weisungen erteilt der Kunde in Textform, etwa im Chat des Kundenportals. Weisungen, die über diese Einstellungen hinausgehen, setzt WEBklar nur um, wenn das technisch vorgesehen ist; sonst kann der Kunde den Webshop abschalten lassen (Nr. 10 Abs. 7 der Leistungsbeschreibung). Die Anonymisierung und Löschung auf Weisung nach Nummer 8 Abs. 2 und Nummer 9 Abs. 6 setzt WEBklar immer um.
|
||||
|
||||
(3) Hält WEBklar eine Weisung für rechtswidrig, weist es den Kunden unverzüglich darauf hin. WEBklar darf die Ausführung aussetzen, bis der Kunde die Weisung ändert oder die Rechtslage geklärt ist.
|
||||
|
||||
## 5. Vertraulichkeit
|
||||
|
||||
WEBklar setzt nur Personen ein, die zur Vertraulichkeit verpflichtet sind oder einer gesetzlichen Verschwiegenheitspflicht unterliegen.
|
||||
|
||||
## 6. Technische und organisatorische Maßnahmen
|
||||
|
||||
(1) WEBklar trifft die Maßnahmen nach Art. 32 DSGVO, die in der Anlage zu diesem Vertrag beschrieben sind. WEBklar darf sie an den Stand der Technik anpassen, sofern das Schutzniveau nicht sinkt.
|
||||
|
||||
(2) Für den Server gelten ergänzend die Maßnahmen der Anlage 2 der Abo-Bedingungen, soweit die Anlage zu diesem Vertrag nichts anderes regelt.
|
||||
|
||||
## 7. Unterauftragsverarbeiter
|
||||
|
||||
(1) Der Kunde genehmigt den Einsatz der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, für den Betrieb des Servers, auf dem der Webshop läuft, und für dessen Sicherung (Rechenzentrum Falkenstein, Deutschland) sowie für den Versand der Bestell-E-Mails über den SMTP-Dienst des Webhostings (Rechenzentrum Nürnberg, Deutschland). Eine Übermittlung in Drittländer findet nicht statt.
|
||||
|
||||
(2) Will WEBklar einen weiteren Unterauftragsverarbeiter hinzuziehen oder einen ersetzen, teilt es das dem Kunden mindestens vier Wochen vorher in Textform mit. Der Kunde kann innerhalb von zwei Wochen nach Zugang der Mitteilung aus wichtigem datenschutzrechtlichem Grund in Textform widersprechen. WEBklar setzt den neuen Unterauftragsverarbeiter nicht vor Ablauf dieser Frist ein. Kündigt ein Unterauftragsverarbeiter von WEBklar eine eigene Änderung mit kürzerer Frist an, informiert WEBklar den Kunden unverzüglich, nachdem die Ankündigung eingegangen ist; der Kunde kann dann unverzüglich widersprechen. Einigen sich die Parteien nicht, kann der Kunde den Webshop abschalten lassen und WEBklar ihn für diese Website zu dem Zeitpunkt beenden, zu dem der neue Unterauftragsverarbeiter eingesetzt wird. Die Daten werden dann nach Nummer 9 gelöscht, auf Weisung des Kunden früher (Nummer 9 Abs. 6).
|
||||
|
||||
(3) WEBklar verpflichtet jeden Unterauftragsverarbeiter vertraglich auf die Datenschutzpflichten nach Art. 28 Abs. 4 DSGVO. Bei der Hetzner Online GmbH ist das deren Vertrag zur Auftragsverarbeitung, den WEBklar abgeschlossen hat. Kommt ein Unterauftragsverarbeiter seinen Datenschutzpflichten nicht nach, haftet WEBklar dem Kunden für die Einhaltung dieser Pflichten (Art. 28 Abs. 4 Satz 2 DSGVO).
|
||||
|
||||
(4) **Stripe ist kein Unterauftragsverarbeiter von WEBklar.** Der Kunde verwendet sein eigenes Stripe-Konto und hat den Vertrag darüber unmittelbar mit Stripe geschlossen (Nr. 4 der Leistungsbeschreibung). Die Zahlungsseite (Stripe Checkout), die Erhebung von Zahlungs-, Karten- und Kontodaten, die Prüfung von Zahlungen und die Auszahlungen verantworten der Kunde und Stripe nach ihrem Vertrag; WEBklar erhält diese Daten nicht. Auf Weisung des Kunden ruft WEBklar bei Stripe die Angaben zu Zahlungen und Bestellungen ab, die der Webshop braucht (Nummer 2 Abs. 1), und veranlasst Erstattungen. Daten von Käufern gehen dabei nicht von WEBklar an Stripe, außer der Erstattung selbst und der Kennung der Zahlung. Die Weitergabe von Daten durch Stripe, auch an Stellen außerhalb der Europäischen Union, regeln der Kunde und Stripe; dafür ist WEBklar nicht verantwortlich.
|
||||
|
||||
## 8. Unterstützung des Kunden, Betroffenenrechte
|
||||
|
||||
(1) WEBklar leitet Anfragen von Käufern, die an WEBklar gerichtet sind, unverzüglich an den Kunden weiter. WEBklar unterstützt ihn mit geeigneten technischen und organisatorischen Maßnahmen bei der Beantwortung.
|
||||
|
||||
(2) Dafür bietet das Kundenportal den Export der Bestellungen als CSV-Datei und die Anzeige der Bestellungen und Widerrufe, mit denen der Kunde Auskunft erteilen kann. Auf Weisung des Kunden in Textform (etwa im Chat des Kundenportals) anonymisiert WEBklar die Käuferdaten einer Bestellung oder eines Widerrufs unverzüglich: Name, E-Mail-Adresse, Liefer- und Rechnungsadresse und die Freitexte werden gelöscht oder unkenntlich gemacht (ein IP-Hash ist zu diesem Zeitpunkt längst gelöscht); Bestellnummer, Positionen, Beträge und Status bleiben. Dabei löscht WEBklar die E-Mails zu dieser Bestellung, die noch auf den Versand warten. E-Mails, deren Versand endgültig fehlgeschlagen ist, behalten Empfängeradresse und Inhalt, bis WEBklar sie nach Nummer 9 Abs. 4 löscht (48 Stunden nach dem Fehlschlag). Gesetzliche Aufbewahrungspflichten des Kunden (Nummer 9 Abs. 8) prüft der Kunde vor seiner Weisung selbst.
|
||||
|
||||
(3) Verletzungen des Schutzes personenbezogener Daten meldet WEBklar dem Kunden unverzüglich, möglichst innerhalb von 24 Stunden nach Kenntnis, mit den Informationen, die zu diesem Zeitpunkt vorliegen.
|
||||
|
||||
(4) WEBklar unterstützt den Kunden bei seinen Pflichten aus Art. 32 bis 36 DSGVO. Dazu gehören die Angaben, die er für sein Verzeichnis von Verarbeitungstätigkeiten und für eine etwaige Datenschutz-Folgenabschätzung braucht.
|
||||
|
||||
(5) Bestätigt der Kunde seine Rechtstexte im Kundenportal (Allgemeine Geschäftsbedingungen, Widerrufsbelehrung, Muster-Widerrufsformular, Versand und Zahlung, Gewährleistungshinweis sowie die Adressen von Impressum und Datenschutzerklärung), legt WEBklar jede bestätigte Fassung einmal im Archiv der Rechtstexte ab: mit ihrer Prüfsumme (Hash), dem Zeitpunkt und der Angabe, welche angemeldete Person des Kunden (Name oder E-Mail-Adresse) sie bestätigt hat. Die Bestellung speichert die Prüfsumme der Fassung, die der Käufer bei der Bestellung gesehen hat. Die Bestellbestätigung (G09) zeigt die archivierte Fassung zu dieser Prüfsumme, nicht die Fassung, die beim Versand der E-Mail gerade gilt; fehlt die archivierte Fassung ausnahmsweise, verwendet WEBklar die aktuell bestätigte Fassung. Das Archiv enthält keine Daten von Käufern. WEBklar löscht es mit den übrigen Daten des Webshops (Nummer 9 Abs. 6), nicht früher. Das Archiv hilft dem Kunden, die Fassung nachzuweisen, die ein Käufer erhalten hat; es ersetzt nicht seine eigene Aufbewahrung (Nummer 9 Abs. 8). Die Bestellbestätigung enthält diese Texte; WEBklar löscht ihren Inhalt nach dem Versand aus der Versandwarteschlange (Nummer 9 Abs. 4).
|
||||
|
||||
(6) Die Käufer informiert der Kunde (Art. 13 DSGVO; Nummer 11 Abs. 2).
|
||||
|
||||
## 9. Löschung und Rückgabe
|
||||
|
||||
(1) Eine Bestellung, die nicht bezahlt wird, verfällt 30 Minuten nach dem Anlegen und gilt als storniert. WEBklar löscht nicht bezahlte Bestellungen (offen oder storniert) 30 Tage nach dem Anlegen. Den IP-Hash löscht WEBklar aus dem Datensatz, sobald die Bestellung bezahlt oder storniert wird, spätestens mit dieser Löschung.
|
||||
|
||||
(2) Bei bezahlten Bestellungen anonymisiert WEBklar die Käuferdaten 3 Jahre nach dem Ende des Kalenderjahres, in dem die Bestellung angelegt wurde (Bestelljahr; maßgeblich ist die Zeit in Berlin): Name, E-Mail-Adresse sowie Liefer- und Rechnungsadresse werden gelöscht (der IP-Hash ist mit der Zahlung bereits gelöscht; eine Telefonnummer des Käufers wird nicht gespeichert). Bestellnummer, Positionen, Beträge, Steuerausweis, Status, Versanddienst und Sendungsnummer bleiben für die Buchhaltung des Kunden erhalten.
|
||||
|
||||
(3) Widerrufe nach Nummer 3 Abs. 2 behandelt WEBklar wie bezahlte Bestellungen: Name, E-Mail-Adresse und Erklärung werden 3 Jahre nach dem Ende des Kalenderjahres des Eingangs gelöscht; die Bestellnummer, die die Person angegeben hat, und der Zeitpunkt des Eingangs bleiben.
|
||||
|
||||
(4) In der Versandwarteschlange für E-Mails leert WEBklar die Empfängeradresse und den Inhalt einer E-Mail nach dem Versand und löscht den Eintrag 7 Tage nach dem Versand. E-Mails, die nicht zugestellt werden können, löscht WEBklar mit Empfängeradresse und Inhalt 48 Stunden nach dem endgültigen Fehlschlag.
|
||||
|
||||
(5) Der Löschlauf läuft täglich; gelöscht oder anonymisiert wird frühestens nach Ablauf der jeweiligen Frist. Ist an einem Tag mehr zu bearbeiten, als ein Lauf schafft, folgt der Rest im nächsten Lauf.
|
||||
|
||||
(6) Ist der Webshop abgeschaltet oder das Wartungs-Abo beendet, kann der Kunde die Bestellungen und Widerrufe 90 Tage lang im Kundenportal lesen und als CSV-Datei exportieren. Danach löscht WEBklar alle Daten des Webshops: Artikel, Bilder, Bestellungen, Widerrufe, Einstellungen, Rechtstexte und das Archiv der bestätigten Fassungen der Rechtstexte (Nummer 8 Abs. 5). Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Angaben zur Einrichtung bestehen (zum Beispiel Adresse der Website, Einbau-Code und die Angaben des Kunden aus der Anfrage) und enthält keine Daten von Käufern; dafür gilt die Datenschutzerklärung von WEBklar (Abschnitt 6). Beginnt innerhalb dieser Frist wieder ein Wartungs-Abo, schaltet WEBklar den Webshop auf Wunsch des Kunden mit den vorhandenen Daten wieder ein (Nr. 10 Abs. 5 der Leistungsbeschreibung). Früher anonymisiert WEBklar einzelne Daten auf Weisung des Kunden (Nummer 8 Abs. 2); alle Daten des Webshops löscht WEBklar auf Weisung des Kunden in Textform unverzüglich.
|
||||
|
||||
(7) Die Daten sind in der täglichen Vollsicherung des Servers enthalten (§ 3 Abs. 5 der Abo-Bedingungen). Gelöschte oder anonymisierte Daten bleiben dort bis zu sieben Tage erhalten und werden dann überschrieben. Wird eine Sicherung zurückgespielt, holt der tägliche Löschlauf die fälligen Löschungen und Anonymisierungen nach; Löschungen auf Weisung des Kunden wiederholt WEBklar.
|
||||
|
||||
(8) Gesetzliche Aufbewahrungspflichten bleiben unberührt. Der Kunde ist für seine eigenen Aufbewahrungspflichten (zum Beispiel für Buchungsbelege) selbst verantwortlich; WEBklar ist kein Archiv im Sinne der Buchführung, und das Archiv der Rechtstexte dient nur dem Nachweis der Texte. Der Kunde sichert die Belege, die er braucht, rechtzeitig vor der Anonymisierung oder Löschung durch den Export und die Belege von Stripe.
|
||||
|
||||
## 10. Nachweise und Kontrollen
|
||||
|
||||
WEBklar stellt dem Kunden alle Informationen zur Verfügung, die zum Nachweis der Pflichten aus Art. 28 DSGVO nötig sind, und ermöglicht Überprüfungen einschließlich Inspektionen durch den Kunden oder einen von ihm beauftragten, zur Verschwiegenheit verpflichteten Prüfer. Sie finden nach Anmeldung mit angemessener Frist (in der Regel 14 Tage) zu den Servicezeiten statt, ohne den Betrieb unzumutbar zu stören. Für das Rechenzentrum genügen in der Regel die Nachweise des Betreibers (Zertifizierung der Hetzner Online GmbH nach ISO/IEC 27001). Für mehr als eine Vor-Ort-Kontrolle pro Jahr kann WEBklar eine angemessene Vergütung verlangen.
|
||||
|
||||
## 11. Pflichten des Kunden
|
||||
|
||||
(1) Der Kunde ist Verantwortlicher für die Verarbeitung der Daten seiner Käufer im Webshop.
|
||||
|
||||
(2) Der Kunde informiert seine Käufer nach Art. 13 DSGVO. Dazu nimmt er den Datenschutz-Absatz für den Webshop in seine Datenschutzerklärung auf (Nr. 5 Abs. 10 der Leistungsbeschreibung) und nennt in seinen Rechtstexten (Datenschutzerklärung, Impressum) die Angaben, die das Gesetz verlangt.
|
||||
|
||||
(3) Der Kunde schließt mit Stripe den Vertrag über sein Stripe-Konto selbst und verantwortet die Verarbeitung von Daten durch Stripe, soweit sie nicht in diesem Vertrag geregelt ist (Nummer 7 Abs. 4).
|
||||
|
||||
(4) Der Kunde verkauft keine ausgeschlossenen Waren (Nr. 3 der Leistungsbeschreibung) und fordert von seinen Käufern keine Angaben zu ihrer Gesundheit oder zu anderen besonderen Kategorien personenbezogener Daten, auch nicht im Freitext der Widerrufserklärung.
|
||||
|
||||
(5) Der Kunde verändert den Einbau-Code und die gesetzlich vorgeschriebenen Hinweise des Widgets nicht und entfernt den Link „Vertrag widerrufen“ nicht (Nr. 5 Abs. 6 und 9 der Leistungsbeschreibung).
|
||||
|
||||
## 12. Haftung, Änderungen, Schlussbestimmungen
|
||||
|
||||
(1) Gegenüber betroffenen Personen gilt Art. 82 DSGVO. Für die Haftung zwischen Kunde und WEBklar gilt § 13 der Abo-Bedingungen.
|
||||
|
||||
(2) Eine neue Fassung dieses Vertrags oder der Leistungsbeschreibung, auch wegen geänderter Datenerhebung, teilt WEBklar dem Kunden mindestens sechs Wochen vor ihrem Wirksamwerden in Textform mit. Sie wird wirksam, wenn der Kunde ihr im Kundenportal oder in Textform zustimmt. Stimmt er nicht zu, gilt die bisherige Fassung weiter.
|
||||
|
||||
(3) Ist die neue Fassung nötig, weil ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form nicht mehr zulässt, und stimmt der Kunde ihr nicht zu, darf WEBklar den Webshop für seine Website nach Nr. 11 Abs. 2 der Leistungsbeschreibung beenden oder nach Nr. 11 Abs. 3 der Leistungsbeschreibung aussetzen. Das Abo bleibt davon unberührt.
|
||||
|
||||
(4) Im Übrigen gilt § 16 der Abo-Bedingungen.
|
||||
|
||||
## Anlage: Technische und organisatorische Maßnahmen des Webshops (Art. 32 DSGVO)
|
||||
|
||||
| Bereich | Maßnahmen |
|
||||
|---|---|
|
||||
| Übertragung | Das Widget, die Seiten der Käufer und das Kundenportal werden nur über TLS (HTTPS) übertragen. Der Einbau-Code verweist auf eine Adresse mit https://. Die Zahlung geschieht auf der Zahlungsseite von Stripe. |
|
||||
| Speicher im Browser | Es werden keine Cookies gesetzt. Das Widget speichert nur den Warenkorb (Kennungen der Artikel und Mengen) im Sitzungsspeicher des Tabs. Es sendet seine Anfragen ohne Cookies und ruft nur die Schnittstelle des Webshops auf, keine Dienste Dritter. |
|
||||
| Zugriffskontrolle | Das Kundenportal ist nur nach Anmeldung erreichbar; jeder Kunde sieht nur die Daten seiner eigenen Projekte. Die öffentliche Schnittstelle des Widgets liefert nur für einen vorhandenen Webshop Daten und ist über den Website-Schlüssel (Site-Key) adressiert; Bestellungen und Widerrufe nimmt sie nur von Seiten des Kunden und des Portals an. Den Stand einer Bestellung ruft der Käufer nur mit einem geheimen Token ab, das nur als Prüfsumme gespeichert wird. Benachrichtigungen von Stripe werden nur nach Prüfung ihrer Signatur verarbeitet; die Schlüssel für Stripe liegen nur auf dem Server. |
|
||||
| Missbrauchsschutz | Ein verstecktes Feld (Honeypot), eine Mindestzeit bis zum Absenden des Formulars mit signiertem Formular-Token und eine Mindestantwortzeit schützen vor automatisierten Bestellungen und Widerrufen. Grenzen je IP-Adresse und je Webshop gelten im Arbeitsspeicher; ein Neustart leert sie. Die Obergrenze offener Bestellungen je IP-Adresse (drei in 30 Minuten) arbeitet mit einem gesalzenen Hashwert der IP-Adresse in der Bestellung (Nummer 3 Abs. 4). |
|
||||
| Datenminimierung | Kartendaten und Kontodaten erhält und speichert WEBklar nicht. Preise berechnet der Server aus den gespeicherten Artikeln, nie aus Angaben des Browsers. Eine Telefonnummer wird weder abgefragt noch gespeichert, die Rechnungsadresse nur, soweit Stripe sie für die Zahlungsart braucht. Mit der Bestellung und mit dem Widerruf werden keine IP-Adresse im Klartext und kein Browser-Kennzeichen gespeichert, bei der Bestellung nur der gesalzene IP-Hash, und der nur, solange die Bestellung offen ist (gelöscht mit Zahlung oder Stornierung). Die Käuferdaten werden nach den Fristen der Nummer 9 anonymisiert. Nach dem Versand einer E-Mail werden Empfängeradresse und Inhalt in der Warteschlange geleert. |
|
||||
| Trennung | Alle Artikel, Bestellungen und Widerrufe tragen die Kennung des Webshops und des Kunden; das Kundenportal und die öffentliche Schnittstelle lesen immer nur die Daten des jeweiligen Webshops. Artikelbilder liegen in einem eigenen Ordner je Webshop. Die Hintergrundläufe (Abgleich offener Bestellungen, Versand der E-Mails, Anonymisierung und Löschung) schreiben jeweils nur in das Dokument, um das es geht. |
|
||||
| Löschung | Täglicher Löschlauf für Bestellungen, Widerrufe und Warteschlange nach den Fristen der Nummer 9; die Daten abgeschalteter Webshops einschließlich des Archivs der bestätigten Rechtstexte 90 Tage nach dem Abschalten; das Einrichtungs-Ticket bleibt als Arbeitsnachweis mit einer Kurzfassung der Angaben zur Einrichtung bestehen und enthält keine Daten von Käufern (Nummer 9 Abs. 6). |
|
||||
| Protokolle | Die Programmprotokolle des Webshops und der Tagesbericht enthalten nur Zählungen und Bestellnummern, keine Namen, E-Mail-Adressen oder Adressen von Käufern. Server-Protokolle des Webservers werden nach 14 Tagen gelöscht (Anlage 2 der Abo-Bedingungen). |
|
||||
| Datensicherung | Tägliche Vollsicherung des Servers durch Hetzner; verfügbar sind die letzten sieben Tagessicherungen (§ 3 Abs. 5 der Abo-Bedingungen). |
|
||||
| Rechenzentrum | Hetzner Online GmbH, Rechenzentren Falkenstein und Nürnberg (Deutschland). Hetzner ist nach ISO/IEC 27001 zertifiziert; der Geltungsbereich umfasst die Hosting-Dienstleistungen und die Rechenzentren. |
|
||||
| Organisation | Verpflichtung aller Beteiligten auf Vertraulichkeit, Ablauf für Datenschutzverletzungen (Nummer 8 Abs. 3), Überprüfung der Maßnahmen mindestens einmal im Jahr. |
|
||||
@@ -0,0 +1,138 @@
|
||||
# Auftragsverarbeitungsvertrag Website-Analyse (Art. 28 DSGVO)
|
||||
|
||||
**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen
|
||||
|
||||
zwischen dem Kunden als Verantwortlichem („Kunde“) und der WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, vertreten durch die Gesellschafter Kenso Grimm und Justin Klein, als Auftragsverarbeiter („WEBklar“).
|
||||
|
||||
## Vorbemerkung: Abschluss und Verhältnis zu anderen Verträgen
|
||||
|
||||
(1) Der Kunde schließt diesen Vertrag, wenn er im Kundenportal die Funktion „Website-Analyse“ einschaltet und die dort angezeigte Zeile „Es gelten Leistungsbeschreibung und Auftragsverarbeitung (Fassung 2026-10-16)“ mit dem Klick auf „Einschalten“ bestätigt; diesem Vertrag und der Leistungsbeschreibung „Website-Analyse“ („Leistungsbeschreibung“) in der angezeigten Fassung stimmt er damit zu. Das Portal speichert den Wortlaut der angezeigten Zeile, die Fassungen, den Zeitpunkt, das Kundenkonto und die angemeldete Person. Anschließend sendet WEBklar dem Kunden eine E-Mail, die beide Texte in dieser Fassung als PDF enthält und auf sie verlinkt.
|
||||
|
||||
(2) Ein Mitarbeiter von WEBklar darf die Funktion nur einschalten, wenn der Kunde vorher in Textform (etwa im Chat des Kundenportals oder per E-Mail) erklärt hat, dass er diesem Vertrag und der Leistungsbeschreibung in der aktuellen Fassung zustimmt. WEBklar speichert die Fassung, das Datum und den Weg dieser Erklärung, den Zeitpunkt des Einschaltens und den Namen des Mitarbeiters. Der Kunde erhält die E-Mail nach Absatz 1.
|
||||
|
||||
(3) Dieser Vertrag ergänzt den Auftragsverarbeitungsvertrag in Anlage 1 der Abo-Bedingungen. Für die Website-Analyse geht er Anlage 1 vor; im Übrigen gilt Anlage 1 ergänzend. In Datenschutzfragen geht er den Abo-Bedingungen und der Leistungsbeschreibung „Website-Analyse“ vor.
|
||||
|
||||
## 1. Gegenstand und Dauer
|
||||
|
||||
(1) WEBklar betreibt für die öffentliche Website des Kunden eine Reichweitenmessung mit Einwilligungsbanner („Website-Analyse“) und verarbeitet dabei personenbezogene Daten der Besucher der Website im Auftrag des Kunden. Den Umfang beschreibt die Leistungsbeschreibung „Website-Analyse“.
|
||||
|
||||
(2) Der Vertrag beginnt mit dem Einschalten der Funktion. Er endet mit dem Ausschalten, spätestens mit dem Ende des Abos, das die Website-Analyse enthält (Hosting-Abo oder Wartungs-Abo); endet nur das Wartungs-Abo und läuft ein Hosting-Abo weiter, läuft dieser Vertrag weiter. Den Messdienst schaltet WEBklar bei Vertragsende spätestens am folgenden Tag ab. Die Pflichten aus diesem Vertrag gelten fort, bis alle Daten nach Nummer 9 gelöscht sind.
|
||||
|
||||
## 2. Art und Zweck der Verarbeitung
|
||||
|
||||
(1) Art: Erheben über ein Skript, das WEBklar beim Veröffentlichen in die Website des Kunden einfügt, Übermitteln an den Messdienst von WEBklar, Speichern, Verdichten zu Tageswerten, Auswerten im Kundenportal (Kennzahlen, Heatmap) und Löschen.
|
||||
|
||||
(2) Zwecke: Der Kunde soll erkennen, wie seine Website genutzt wird, um sie zu verbessern. Außerdem schützt WEBklar den Messdienst für den Kunden vor Missbrauch und verfälschten Zahlen (Mengenbegrenzung, Erkennen von Bots).
|
||||
|
||||
(3) WEBklar verwendet die Daten für keine eigenen Zwecke, auch nicht in anonymisierter Form und nicht für Auswertungen über mehrere Kunden. Es findet keine Werbung und keine Zusammenführung mit anderen Daten statt. WEBklar bildet keine Profile einzelner Besucher. Die Besucher-Kennung dient nur dazu, die Besucher eines Tages zu zählen.
|
||||
|
||||
## 3. Datenarten und Betroffene
|
||||
|
||||
(1) Solange der Besucher im Banner keine Wahl getroffen hat und sein Browser kein Signal nach Absatz 2 Satz 2 sendet: Pfad der aufgerufenen Seite (ohne Suchparameter und Anker), auch bei einem Seitenwechsel ohne Neuladen; Land (aus der IP-Adresse abgeleitet); Geräteklasse (Handy, Tablet oder Computer; aus dem Browser-Kennzeichen abgeleitet); Zeitpunkt (auf die volle Stunde gerundet); ein Zähler dafür, dass das Banner angezeigt wurde.
|
||||
|
||||
(2) Nach „Ablehnen“ sendet das Skript einmalig einen Zähler „abgelehnt“ und danach nichts mehr. Sendet der Browser die Signale „Global Privacy Control“ oder „Do Not Track“, zeigt das Skript kein Banner und sendet nichts.
|
||||
|
||||
(3) Nach „Annehmen“ zusätzlich: Domain der Website, von der der Besucher kam; Größenklasse des Browserfensters; eine täglich wechselnde pseudonyme Besucher-Kennung; Verweildauer (ab dem Laden der Seite) und Scrolltiefe je Seite; Klickpositionen mit einem Elementpfad aus Tag-Namen und Position (ohne Texte, Kennungen und Klassennamen); die Art von Kontakt-Klicks (Telefon, E-Mail, WhatsApp oder Link auf eine andere Website, dann mit deren Domain); ein Zähler „angenommen“.
|
||||
|
||||
(4) IP-Adresse und Browser-Kennzeichen (User-Agent) erhält der Server von WEBklar technisch bei jedem Abruf des Skripts und bei jeder Messanfrage, auch nach „Ablehnen“. Der Messdienst verarbeitet sie nur kurz im Arbeitsspeicher, um Land, Geräteklasse, Bots, die Mengenbegrenzung und, nur nach Einwilligung, die Besucher-Kennung zu bestimmen. Er speichert und protokolliert sie nicht. Nicht erhoben werden Cookies, Formularinhalte, Seitentexte, Mausbewegungen, Tastatureingaben und Sitzungsaufzeichnungen.
|
||||
|
||||
(5) Tageswerte sind Summen je Tag und Merkmal (etwa Aufrufe je Seite, je Land oder je Geräteklasse), ohne Besucher-Kennung. Enthalten die Pfade der Website personenbezogene Angaben, können auch Tageswerte personenbezogen sein (Nummer 11 Abs. 2).
|
||||
|
||||
(6) Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO) werden nicht gezielt verarbeitet. Je nach Inhalt der Website können sich aus Pfaden oder Domains aber Rückschlüsse ergeben (Nummer 11 Abs. 3).
|
||||
|
||||
(7) Betroffene: Besucher der Website des Kunden.
|
||||
|
||||
## 4. Weisungen
|
||||
|
||||
(1) WEBklar verarbeitet die Daten nur auf dokumentierte Weisung des Kunden, auch in Bezug auf eine Übermittlung in ein Drittland oder an eine internationale Organisation. Das gilt nicht, wenn das Recht der Europäischen Union oder Deutschlands WEBklar zu einer Verarbeitung verpflichtet. In diesem Fall teilt WEBklar dem Kunden die Pflicht vorher mit, soweit das Gesetz es nicht verbietet.
|
||||
|
||||
(2) Mit dem Einschalten weist der Kunde WEBklar an, die Website-Analyse in der Standardkonfiguration nach der Leistungsbeschreibung zu betreiben. Weitere Weisungen ergeben sich aus den Einstellungen im Kundenportal (Einschalten, Ausschalten, Akzentfarbe des Banners). Sonstige Weisungen erteilt der Kunde in Textform, etwa im Chat des Kundenportals. Weisungen, die von der Standardkonfiguration abweichen, setzt WEBklar nur um, wenn das technisch vorgesehen ist; sonst kann der Kunde die Funktion ausschalten.
|
||||
|
||||
(3) Hält WEBklar eine Weisung für rechtswidrig, weist es den Kunden unverzüglich darauf hin. WEBklar darf die Ausführung aussetzen, bis der Kunde die Weisung ändert oder die Rechtslage geklärt ist.
|
||||
|
||||
## 5. Vertraulichkeit
|
||||
|
||||
WEBklar setzt nur Personen ein, die zur Vertraulichkeit verpflichtet sind oder einer gesetzlichen Verschwiegenheitspflicht unterliegen.
|
||||
|
||||
## 6. Technische und organisatorische Maßnahmen
|
||||
|
||||
(1) WEBklar trifft die Maßnahmen nach Art. 32 DSGVO, die in der Anlage zu diesem Vertrag beschrieben sind. WEBklar darf sie an den Stand der Technik anpassen, sofern das Schutzniveau nicht sinkt.
|
||||
|
||||
(2) Für den Server gelten ergänzend die Maßnahmen der Anlage 2 der Abo-Bedingungen, soweit die Anlage zu diesem Vertrag nichts anderes regelt.
|
||||
|
||||
## 7. Unterauftragsverarbeiter
|
||||
|
||||
(1) Der Kunde genehmigt den Einsatz der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, für den Betrieb des Servers, auf dem der Messdienst läuft, und für dessen Sicherung. Ort der Verarbeitung ist das Rechenzentrum Falkenstein (Deutschland). Eine Übermittlung in Drittländer findet nicht statt.
|
||||
|
||||
(2) Will WEBklar einen weiteren Unterauftragsverarbeiter hinzuziehen oder einen ersetzen, teilt es das dem Kunden mindestens vier Wochen vorher in Textform mit. Der Kunde kann innerhalb von zwei Wochen nach Zugang der Mitteilung aus wichtigem datenschutzrechtlichem Grund in Textform widersprechen. WEBklar setzt den neuen Unterauftragsverarbeiter nicht vor Ablauf dieser Frist ein. Kündigt ein Unterauftragsverarbeiter von WEBklar eine eigene Änderung mit kürzerer Frist an, informiert WEBklar den Kunden unverzüglich, nachdem die Ankündigung eingegangen ist; der Kunde kann dann unverzüglich widersprechen. Einigen sich die Parteien nicht, kann der Kunde die Website-Analyse ausschalten und WEBklar sie für diese Website zu dem Zeitpunkt beenden, zu dem der neue Unterauftragsverarbeiter eingesetzt wird. Die Daten werden dann nach Nummer 9 gelöscht, auf Verlangen des Kunden sofort.
|
||||
|
||||
(3) WEBklar verpflichtet jeden Unterauftragsverarbeiter vertraglich auf die Datenschutzpflichten nach Art. 28 Abs. 4 DSGVO. Bei der Hetzner Online GmbH ist das deren Vertrag zur Auftragsverarbeitung, den WEBklar abgeschlossen hat. Kommt ein Unterauftragsverarbeiter seinen Datenschutzpflichten nicht nach, haftet WEBklar dem Kunden für die Einhaltung dieser Pflichten (Art. 28 Abs. 4 Satz 2 DSGVO).
|
||||
|
||||
(4) Die Länderzuordnung nutzt eine Datenbank, die WEBklar auf dem eigenen Server speichert („IP to Country Lite“ von DB-IP.com, Lizenz CC BY 4.0). Daten der Besucher werden dabei nicht an DB-IP übermittelt. DB-IP ist kein Unterauftragsverarbeiter.
|
||||
|
||||
## 8. Unterstützung des Kunden, Betroffenenrechte
|
||||
|
||||
(1) WEBklar leitet Anfragen betroffener Personen, die an WEBklar gerichtet sind, unverzüglich an den Kunden weiter. WEBklar unterstützt ihn mit geeigneten technischen und organisatorischen Maßnahmen bei der Beantwortung.
|
||||
|
||||
(2) Die gespeicherten Messdaten enthalten keine IP-Adresse, keinen Namen und keine Kennung, die länger als einen Tag gilt; Zeitpunkte sind auf die volle Stunde gerundet. Einzelne Besucher kann WEBklar daher in der Regel nicht identifizieren (Art. 11 DSGVO). Ein Abgleich kann aber möglich sein, solange die Zugriffsprotokolle der Website (Anlage 1 und 2 der Abo-Bedingungen, 14 Tage) vorhanden sind. Nennt eine betroffene Person zusätzliche Angaben, etwa Datum, Uhrzeit, besuchte Seiten oder ihre IP-Adresse, prüft WEBklar auf Weisung des Kunden, auch anhand dieser Protokolle, ob eine Zuordnung möglich ist, und teilt ihm das Ergebnis mit.
|
||||
|
||||
(3) Der Messung können Besucher technisch widersprechen (Art. 21 DSGVO): Nach „Ablehnen“ und bei den Browser-Signalen „Global Privacy Control“ und „Do Not Track“ sendet das Skript nichts mehr.
|
||||
|
||||
(4) WEBklar unterstützt den Kunden bei seinen Pflichten aus Art. 32 bis 36 DSGVO. Dazu gehören die Angaben, die er für sein Verzeichnis von Verarbeitungstätigkeiten und für eine etwaige Datenschutz-Folgenabschätzung braucht, und eine kurze Interessenabwägung für die Zählung nach Art. 6 Abs. 1 lit. f DSGVO, die WEBklar mit jeder Fassung dieses Vertrags bereitstellt.
|
||||
|
||||
(5) Verletzungen des Schutzes personenbezogener Daten meldet WEBklar dem Kunden unverzüglich, möglichst innerhalb von 24 Stunden nach Kenntnis, mit den Informationen, die zu diesem Zeitpunkt vorliegen.
|
||||
|
||||
(6) Zum Nachweis der Einwilligungen bewahrt WEBklar jede Fassung des Banner-Textes und der Zusatzseite mit dem Zeitraum ihrer Verwendung auf, bis drei Jahre nach dem Ende der Website-Analyse. WEBklar stellt sie dem Kunden auf Verlangen zur Verfügung. Ein Protokoll je Besucher führt WEBklar nicht.
|
||||
|
||||
(7) Die Besucher informiert der Kunde (Art. 13 DSGVO). WEBklar stellt ihm dafür die Zusatzseite „/datenschutz-analyse.html“ als Muster bereit. Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar dort einen Hinweis auf die Zusatzseite ein; sonst erhält der Kunde den Hinweis als Textbaustein (Leistungsbeschreibung Nr. 4 Abs. 4).
|
||||
|
||||
## 9. Löschung und Rückgabe
|
||||
|
||||
(1) Einzelne Ereignisse und Klickpositionen löscht WEBklar automatisch nach 30 Tagen. Der Löschlauf läuft täglich, die Löschung erfolgt also in der Regel spätestens am 31. Tag; fällt ein Lauf aus, holt der nächste ihn nach.
|
||||
|
||||
(2) Tageswerte löscht WEBklar 25 Monate nach dem jeweiligen Tag. Nach dem Ende der Website-Analyse (Ausschalten oder Ende des Abos, das sie enthält) löscht WEBklar alle Daten der Website am 61. Tag. Bis zum 46. Tag nach dem Ende kann der Kunde eine Kopie der Tageswerte und der noch vorhandenen Einzelereignisse verlangen; WEBklar stellt sie innerhalb von zwei Wochen als CSV-Datei bereit. Auf Weisung des Kunden löscht WEBklar die Daten früher.
|
||||
|
||||
(3) Die Daten sind in der täglichen Vollsicherung des Servers enthalten (§ 3 Abs. 5 der Abo-Bedingungen). Gelöschte Daten bleiben dort bis zu sieben Tage erhalten und werden dann überschrieben. Wird eine Sicherung zurückgespielt, holt der tägliche Löschlauf die fälligen Löschungen nach; Löschungen auf Weisung des Kunden wiederholt WEBklar.
|
||||
|
||||
(4) Gesetzliche Aufbewahrungspflichten bleiben unberührt.
|
||||
|
||||
## 10. Nachweise und Kontrollen
|
||||
|
||||
WEBklar stellt dem Kunden alle Informationen zur Verfügung, die zum Nachweis der Pflichten aus Art. 28 DSGVO nötig sind, und ermöglicht Überprüfungen einschließlich Inspektionen durch den Kunden oder einen von ihm beauftragten, zur Verschwiegenheit verpflichteten Prüfer. Sie finden nach Anmeldung mit angemessener Frist (in der Regel 14 Tage) zu den Servicezeiten statt, ohne den Betrieb unzumutbar zu stören. Für das Rechenzentrum genügen in der Regel die Nachweise des Betreibers (Zertifizierung der Hetzner Online GmbH nach ISO/IEC 27001). Für mehr als eine Vor-Ort-Kontrolle pro Jahr kann WEBklar eine angemessene Vergütung verlangen.
|
||||
|
||||
## 11. Pflichten des Kunden
|
||||
|
||||
(1) Der Kunde ist Verantwortlicher für die Messung. Er entfernt, verdeckt oder umgeht das Banner nicht. Er bindet ohne Abstimmung mit WEBklar keine weiteren Mess- oder Tracking-Werkzeuge ein.
|
||||
|
||||
(2) Enthalten die Pfade seiner Website personenbezogene Angaben, etwa Namen, Kundennummern oder E-Mail-Adressen, teilt der Kunde das WEBklar vor dem Einschalten mit.
|
||||
|
||||
(3) Richtet sich die Website auch gezielt an Kinder unter 16 Jahren oder lassen ihre Inhalte auf besondere Kategorien personenbezogener Daten schließen (etwa Gesundheit, Religion oder politische Meinung), gilt Nr. 3 Abs. 4 der Leistungsbeschreibung.
|
||||
|
||||
## 12. Haftung, Änderungen, Schlussbestimmungen
|
||||
|
||||
(1) Gegenüber betroffenen Personen gilt Art. 82 DSGVO. Für die Haftung zwischen Kunde und WEBklar gilt § 13 der Abo-Bedingungen.
|
||||
|
||||
(2) Eine neue Fassung dieses Vertrags oder der Leistungsbeschreibung teilt WEBklar dem Kunden mindestens sechs Wochen vor ihrem Wirksamwerden in Textform mit. Sie wird wirksam, wenn der Kunde ihr im Kundenportal oder in Textform zustimmt. Stimmt er nicht zu, gilt die bisherige Fassung weiter.
|
||||
|
||||
(3) Ist die neue Fassung nötig, weil ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form nicht mehr zulässt, und stimmt der Kunde ihr nicht zu, darf WEBklar die Website-Analyse für seine Website nach Nr. 8 Abs. 2 der Leistungsbeschreibung beenden oder aussetzen. Das Abo bleibt davon unberührt.
|
||||
|
||||
(4) Im Übrigen gilt § 16 der Abo-Bedingungen.
|
||||
|
||||
## Anlage: Technische und organisatorische Maßnahmen der Website-Analyse (Art. 32 DSGVO)
|
||||
|
||||
| Bereich | Maßnahmen |
|
||||
|---|---|
|
||||
| Übertragung | Skript und Messdaten werden nur über TLS (HTTPS) übertragen. |
|
||||
| Speicher im Browser | Es werden keine Cookies gesetzt, und der Messdienst wertet keine Cookies aus. Messanfragen sendet das Skript ohne Cookies. Im lokalen Speicher des Browsers liegt nur die Banner-Wahl (Eintrag „wk-consent“ mit Wahl, Zeitpunkt und Fassung des Banner-Textes). Sie gilt 183 Tage; danach und bei einer neuen Fassung des Banner-Textes fragt das Banner erneut. |
|
||||
| Einwilligung | Solange keine Wahl getroffen ist und kein Browser-Signal vorliegt, sendet das Skript nur den Pfad der Seite, auch beim Seitenwechsel ohne Neuladen, und den Zähler „Banner angezeigt“. Nach „Ablehnen“ sendet es einmalig den Zähler „abgelehnt“ und danach nichts mehr, auch nicht aus anderen geöffneten Tabs. Bei „Global Privacy Control“ oder „Do Not Track“ zeigt es kein Banner und sendet nichts. Klick-, Scroll- und Verweildauer-Ereignisse registriert es erst nach „Annehmen“. Die Schaltflächen „Ablehnen“ und „Annehmen“ sind gleich gestaltet. |
|
||||
| Datenminimierung | IP-Adresse und Browser-Kennzeichen werden weder gespeichert noch protokolliert. Der Messdienst verwendet sie nur im Arbeitsspeicher für Land, Geräteklasse, Bot-Erkennung, Mengenbegrenzung und, nur nach Einwilligung, die Besucher-Kennung. Zeitpunkte werden auf die volle Stunde gerundet gespeichert. Pfade werden ohne Suchparameter und Anker gespeichert; Abschnitte, die wie Kennungen aussehen (lange Ziffernfolgen, UUIDs, E-Mail-Adressen), werden vorher ersetzt. Elementpfade bestehen nur aus Tag-Namen und Position. Formulareingaben und Seitentexte werden nicht erfasst. |
|
||||
| Pseudonymisierung | Die Besucher-Kennung (nur nach Einwilligung) ist ein auf 64 Bit gekürzter HMAC-SHA256 aus Website-Schlüssel, IP-Adresse und Browser-Kennzeichen mit einem Tagesschlüssel. Auch der Schlüssel der Mengenbegrenzung wird mit dem Tagesschlüssel gebildet. Der Tagesschlüssel liegt nur im Arbeitsspeicher; die Auslagerung auf die Festplatte ist für den Messdienst gesperrt. Er wird beim ersten Ereignis eines neuen Kalendertags (Europe/Berlin) und bei jedem Neustart neu erzeugt und nie gesichert. Ältere Kennungen lassen sich danach nicht mehr zuordnen. |
|
||||
| Zugriffskontrolle | Die Verwaltungsschnittstelle des Messdienstes (Auswertung, Website-Verwaltung) läuft auf einem internen Port, der nicht öffentlich erreichbar ist, und ist durch ein Zugriffstoken geschützt. Zugriff haben nur das Kundenportal und die Verwaltung von WEBklar. Das Portal zeigt jedem Kunden nur die Daten seiner eigenen Websites. |
|
||||
| Heatmap | Die Heatmap öffnet ein signierter Link, den nur der angemeldete Kunde im Kundenportal erzeugen kann. Der Link gilt 30 Minuten und liefert nur Klickpunkte der gewählten Seite und Ansicht, ohne Zeitpunkte und Kennungen. |
|
||||
| Eingangsprüfung | Messdaten werden nur angenommen, wenn die Website registriert und eingeschaltet ist und Origin bzw. Referer zu ihren Domains passen. Bots werden verworfen. Größe und Menge der Anfragen sind je Website und Absender begrenzt (Zähler nur im Arbeitsspeicher). Für ausgeschaltete Websites liefert der Messdienst ein leeres Skript aus. |
|
||||
| Löschung | Täglicher Löschlauf um 02:30 Uhr (Europe/Berlin): Ereignisse und Klicks nach 30 Tagen, Tageswerte nach 25 Monaten, alle Daten einer Website 60 Tage nach dem Ausschalten. Gelöschte Datensätze werden in der Datenbankdatei überschrieben und aus dem Schreibprotokoll (WAL) entfernt. |
|
||||
| Trennung | Der Messdienst läuft in einem eigenen Container mit eigenem Datenverzeichnis und hat keinen Zugriff auf andere Kundendaten. Alle Datensätze tragen den Website-Schlüssel; Abfragen erfolgen nur je Website. |
|
||||
| Protokolle | Der Reverse-Proxy führt kein Zugriffsprotokoll, und der Messdienst protokolliert keine Anfragen. Wird ein Zugriffsprotokoll eingeschaltet, dann nicht für die Adresse des Messdienstes. Meldungen der Container werden nach 14 Tagen gelöscht. |
|
||||
| Datensicherung | Tägliche Vollsicherung des Servers durch Hetzner; verfügbar sind die letzten sieben Tagessicherungen (§ 3 Abs. 5 der Abo-Bedingungen). Einen gesonderten Datenbank-Export des Messdienstes gibt es nicht. |
|
||||
| Rechenzentrum | Hetzner Online GmbH, Rechenzentrum Falkenstein (Deutschland). Hetzner ist nach ISO/IEC 27001 zertifiziert; der Geltungsbereich umfasst die Hosting-Dienstleistungen und die Rechenzentren. |
|
||||
| Organisation | Verpflichtung aller Beteiligten auf Vertraulichkeit, Ablauf für Datenschutzverletzungen (Nummer 8 Abs. 5), Archiv der Fassungen von Banner-Text und Zusatzseite (Nummer 8 Abs. 6), Überprüfung der Maßnahmen mindestens einmal im Jahr. |
|
||||
@@ -0,0 +1,23 @@
|
||||
# Datenschutzerklärung: Prüfpunkte für die Kundenansicht neu
|
||||
|
||||
Stand 16.10.2026 · Entwurf – Prüfung durch Kanzlei offen · Entwurf zur Freigabe, keine Rechtsberatung
|
||||
|
||||
Diese Liste ändert die Datenschutzerklärung nicht. `public/datenschutz.html` bleibt unverändert (eingefroren); die Stellen unten sind die, die mit der neuen Kundenansicht und den Texten der Fassung 6 geprüft werden müssen. Die Änderung der Datenschutzerklärung erfolgt erst mit dem Umschalten (Entscheidung R14 in `docs/rechtliches/kundenansicht-entscheidungen.md`), als eigene Fassung, sonst würde sie sofort öffentlich. Zeilenangaben beziehen sich auf den Stand von `public/datenschutz.html` am 16.10.2026.
|
||||
|
||||
Je Prüfpunkt: **Fundstelle**, was dort steht, was sich ändert, und die Frage an den Inhaber oder die Kanzlei.
|
||||
|
||||
**D1** · Fundstelle `public/datenschutz.html:127`. Nachweis bei der Bestellung einer Funktion. Dort steht der „Klick auf ‚Zahlungspflichtig bestellen‘“, die Stripe-Sitzung und eine Funktionsliste nur mit News-Pinnwand und Terminplaner. Künftig löst das Einschalten einer Funktion im Abo keine Zahlung aus und hat keine Stripe-Sitzung; gespeichert werden der Wortlaut der angezeigten Zeilen, die Fassungen, Zeitpunkt des Klicks auf „Einschalten“, IP-Adresse, Browserkennung, Konto, angemeldete Person und die Kundenart (Fassungen: Abo-Bedingungen 2026-10-16-v6, Funktionsunterlagen 2026-10-16). Die Funktionsliste muss alle Funktionen nennen (News-Pinnwand, Terminplaner, Webshop, Buchungskalender, Öffnungszeiten live, Website-Analyse). Frage: Neuer Absatz „Funktion einschalten“ neben dem Absatz für Bestellungen, damit der Bestand (Funktionen, die einzeln gekauft wurden) unverändert beschrieben bleibt?
|
||||
|
||||
**D2** · Fundstelle `public/datenschutz.html:126`. Nachweis bei Abo, Wechsel und Zusatzspeicher. Dort stehen „gegebenenfalls Ihre Angaben zum Leistungsbeginn“ sowie Zeitpunkt, Version, IP-Adresse und Browserkennung. Künftig kommen der Wortlaut der angezeigten Zeilen (Linkzeile, Häkchen für Privatkunden) und die Kundenart zum Klickzeitpunkt hinzu (§ 2 Abs. 6 der Abo-Bedingungen Fassung 6). Frage: Genügt es, die Kundenart in den Aufzählungssatz aufzunehmen, oder braucht sie wegen D3 einen eigenen Hinweis?
|
||||
|
||||
**D3** · Fundstelle `public/datenschutz.html:88`. Abschnitt 6 „Kundenportal“, Kundenkonto-Absatz in der Zeile darunter. Die Kundenart („Betrieb“ oder „privat“) ist ein neues Datum im Kundenkonto. Das Team legt sie nach den Angaben des Kunden fest (Entscheidung E3); ist sie leer, gilt der Kunde als privat (E4). Zu ergänzen sind Zweck (Bestellseiten richten sich danach, Nachweis), Rechtsgrundlage (Art. 6 Abs. 1 lit. b und f DSGVO), Quelle (Angaben des Kunden an das Team) und Dauer (wie das Kundenkonto; im Nachweis wie in D1). Frage: Eigener Satz im Kundenkonto-Absatz oder in Abschnitt 7?
|
||||
|
||||
**D4** · Fundstelle `public/datenschutz.html:128`, ergänzend `public/datenschutz.html:115` und `public/datenschutz.html:121`. Der Stripe-Absatz beschreibt Zahlungen für Abos und Zusatzspeicher; das Einschalten einer Funktion im Abo löst keine Zahlung und keine Übermittlung an Stripe aus. Die Abschnitte zu Stripe Connect beim Webshop und beim Buchungskalender (Zahlungen der Käufer und Gäste auf das eigene Stripe-Konto des Kunden) bleiben inhaltlich wie sie sind. Frage: Genügt ein Satz, dass Funktionen im Abo ohne Zahlung eingeschaltet werden, oder sollen die Abschnitte nur an der Einleitung („bestellen“ statt „einschalten“) angepasst werden?
|
||||
|
||||
**D5** · Fundstelle `public/datenschutz.html:102`. Abschnitt zur Website-Analyse: „Kunden mit Wartungs-Abo“ können die Reichweitenmessung einschalten. Künftig steckt die Website-Analyse im Hosting-Abo und im Wartungs-Abo (Leistungsbeschreibung Website-Analyse Nr. 1 Abs. 1). Der Absatz muss beide Abos nennen und beschreiben, dass die Funktion mit dem Abo endet (Löschfrist 60 Tage, AVV Website-Analyse Nr. 9). Frage: Reicht eine Anpassung der Einleitung?
|
||||
|
||||
**D6** · Fundstellen `public/datenschutz.html:105` (Terminplaner), `public/datenschutz.html:113` (Webshop), `public/datenschutz.html:119` (Buchungskalender), außerdem der Abschnitt zur News-Pinnwand und zu Öffnungszeiten live. Die Funktionsabschnitte sprechen von „Kunden mit Wartungs-Abo“, vom „Freischalten“ und vom Ende mit dem Vertrag. Künftig gilt: Funktionen sind Teil des Wartungs-Abos, enden mit dem Abo (Terminplaner, News-Pinnwand, Webshop und Buchungskalender spätestens am Folgetag, Öffnungszeiten live nach 30 Tagen Weiterlauf) und werden nach den Fristen der Leistungsbeschreibungen gelöscht (30 Tage Terminplaner, News-Pinnwand und Öffnungszeiten live; 90 Tage Webshop und Buchungskalender). Frage: Die Fristen in den Abschnitten und in Abschnitt 7 (Speicherdauer) müssen übereinstimmen; soll Abschnitt 7 die Fristen künftig als Tabelle führen?
|
||||
|
||||
**D7** · Fundstelle `public/datenschutz.html:138`, ergänzend die Beschreibung des Stundenkontingents im Kundenkonto-Absatz. Das Stundenkonto und der Vorschuss für die Einrichtung (§ 4 Abs. 8 der Abo-Bedingungen Fassung 6) sind Abrechnungsdaten: Arbeitszeit, Verbrauch, Vorschuss, Ablaufdaten. Sie stehen heute unter „gegebenenfalls Ihr Stundenkontingent“. Frage: Ist der Vorschuss als Abrechnungsdatum mit demselben Zweck, derselben Rechtsgrundlage (Art. 6 Abs. 1 lit. b DSGVO) und derselben Dauer ausreichend beschrieben, oder braucht er einen eigenen Satz?
|
||||
|
||||
**D8** · Fundstelle `public/datenschutz.html:130`. Abschnitt zu den Seiten Kündigen und Widerrufen. Die Seiten erscheinen in der neuen Kundenansicht in der du-Form (Entscheidung R16) und zeigen angemeldeten Kunden die laufenden Widerrufsfristen aus bereits vorhandenen Daten (Sammelabfrage der Fristen im Kundenportal, `GET /api/fristen`). Das ist ein Lesen vorhandener Daten ohne neues Datum und ohne neuen Zweck; der Absatz bleibt inhaltlich unverändert. Frage: Genügt die Feststellung, dass kein neuer Absatz nötig ist, oder soll die Anzeige der Fristen im Konto erwähnt werden?
|
||||
@@ -0,0 +1,144 @@
|
||||
# Rechtsrunde Fassung 6: Entscheidungen, Kanzlei-Fragen und Ablauf
|
||||
|
||||
Stand 16.10.2026 · Entwurf – Prüfung durch Kanzlei offen · Entwurf zur Freigabe, keine Rechtsberatung
|
||||
|
||||
Dieses Blatt ist die Grundlage für die Freigabe der Rechtstexte der Kundenansicht neu durch den Inhaber (Entscheidung E20). Die Kanzlei-Fragen halten den Livegang nicht auf; die Fragen aus den früheren Rechtsrunden stehen in `/root/reports/kundenansicht-neu-2026-10-15/bestand/recht.md` § 4. Die Entwürfe liegen unter `docs/rechtliches/*-v6.md`, die Seiten unter `public/neu/rechtliches/` (Abo-Bedingungen: `abo-bedingungen.html`, Funktionsunterlagen: `<slug>-2026-10-16.html`). Datenschutz-Prüfpunkte D1 bis D8 stehen in `docs/rechtliches/datenschutz-kundenansicht-pruefpunkte.md`.
|
||||
|
||||
## Zu entscheiden
|
||||
|
||||
| Nr. | Punkt | Vorschlag | Alternative | Kanzlei-Frage |
|
||||
|---|---|---|---|---|
|
||||
| R1 | Fassungsnummer und Datum | Abo `2026-10-16-v6`, Funktionen `2026-10-16`; bei Freigabe auf den Freigabetag setzen | Entwurfstag bleibt (keine Umbenennung der Dateien und Fassungen) | – |
|
||||
| R2 | Geltung für laufende Abos v3 bis v5 (C11) | nur neue Abschlüsse (§ 15 Abs. 2); Funktionen gelten über Linkzeile und Klick, auch mit der Stundenkonto- und Vorschuss-Regel; keine Pflicht-Info-Mail, eine freiwillige Mail später | Info-Mail an laufende Abos mit Bitte um Zustimmung nach § 15 | KQ1 |
|
||||
| R3 | Vorschuss-Regel (E17) | § 4 Abs. 8 und jede Leistungsbeschreibung außer der Analyse: echte Arbeitszeit, nie Mehraufwand, ein Rest bei Vertragsende verfällt zugunsten des Kunden | Obergrenze (zum Beispiel 3 Stunden) mit Rückfrage darüber | KQ2 |
|
||||
| R4 | Einrichtung beim Projektstart (E9, E21) | § 4 Abs. 9 und Baustein „projekt“ der Leistungsbeschreibungen: Abrechnung über die Projektkosten, die Funktion läuft nur mit dem passenden Abo | nur in den Leistungsbeschreibungen, nicht in den Abo-Bedingungen | KQ8 |
|
||||
| R5 | Kundenart als Grundlage (E3, E4) | § 1 Abs. 2: hinterlegt nach Angabe des Kunden, leer gilt als privat, maßgeblich ist der Zweck des Vertrags | Selbstauskunft des Kunden in den Bedingungen | KQ4 |
|
||||
| R6 | Häkchen-Wortlaut für Abo, Wechsel und Einrichtung | Texte aus `WORTLAUT_V6.beginn` ohne Paragrafenzitate; Betriebe setzen nichts | Zitate §§ 356 Abs. 5 Nr. 2, 357a Abs. 2 BGB im Häkchen | KQ5 |
|
||||
| R7 | Einrichtung in der Widerrufsfrist | Teil des Abos, kein eigener Vertrag; Widerruf beendet die Funktion; Wertersatz nach aufgewendeter Einrichtungszeit zum Stundensatz des Abos | WEBklar verzichtet auf Wertersatz für die Einrichtung (wie beim Zusatzspeicher), das Häkchen für die Einrichtung entfällt | KQ3 |
|
||||
| R8 | Ende mit dem Abo, Fristen, kein „Behalten“ (C12); mit P4/P6 abgestimmt | Terminplaner, News, Webshop und Buchungskalender schalten spätestens am Folgetag ab (Tagesjob), die Analyse wie bisher; Öffnungszeiten live läuft nach dem Ende noch 30 Tage unverändert weiter und wird dann abgeschaltet (kein Behalten gegen 30 €); Eingaben danach: Terminplaner, News und Öffnungszeiten 30 Tage, Webshop und Buchungskalender 90, Analyse 60; ein neues Abo in der Frist holt die Funktion zurück | Öffnungszeiten live schalten wie die übrigen spätestens am Folgetag ab (P4 Text Z2, P6 Mail K40 und der Zeiten-Abgleich müssten dafür geändert werden) | KQ8 |
|
||||
| R9 | Analyse im Hosting-Abo (C10) | in beiden Abos, Einrichtung kostenlos (C6): § 3 Abs. 8, § 6 Abs. 6, Leistungsbeschreibung A | nur im Wartungs-Abo, wie heute | KQ8 |
|
||||
| R10 | AVV-Struktur | Option A: die AVV je Funktion bleiben eigene Verträge mit neuer Vorbemerkung, Anlage 1 bleibt der Rahmen (§ 14 Abs. 2) | B: Anlagen 4 bis 7 zu den Abo-Bedingungen; C: Rahmen-AVV mit einer Anlage je Funktion | KQ7 |
|
||||
| R11 | Widerrufsbelehrung der Funktionen | bleibt als Datei für den Bestand, die neuen Texte verlinken sie nicht | bleibt Anlage der Leistungsbeschreibungen | KQ6 |
|
||||
| R12 | Anrede der Rechtsdokumente (E15, Spec 5.7) | bleiben im bisherigen Stil („der Kunde“, in der Übersichtsbox „Ihr“); nur Oberflächentexte stehen in „du“ | Übersichtsbox und Leistungsbeschreibungen auf „du“ | – |
|
||||
| R13 | Webshop nur für Betriebe, Terminplaner nicht im Gesundheitsbereich (C15) | steht in den Leistungsbeschreibungen, das Team prüft bei der Freigabe | Webshop auch für Privatkunden mit Hinweis | KQ4, KQ9 |
|
||||
| R14 | Datenschutzerklärung | Prüfpunkte D1 bis D8; `public/datenschutz.html` wird erst mit dem Umschalten als eigene Fassung geändert (sonst sofort öffentlich) | sofort ändern | – |
|
||||
| R15 | Zusatzspeicher und Übernahmegutschrift | Zusatzspeicher: Linkzeile ohne Häkchen (§ 10 Abs. 3 und Hinweise zum Widerruf Nr. 4: kein Wertersatz); Gutschrift: Satz über dem Knopf (P7) | das Häkchen bleibt | KQ5 |
|
||||
| R16 | Du-Anrede der Seiten Kündigen und Widerrufen (N38) | beide Seiten sprechen ab Fassung 6 „du“; sie speichern keinen Wortlaut und haben keine eigene Version (C14); Belehrung und Muster-Formular folgen R12 | Sie-Form bleibt bis zur Freigabe von Belehrung und Formular | KQ6 |
|
||||
| R17 | Zeilen „Anbieter“ und „Abrechnung“ im Block der Bestellseiten (N37) | beide Zeilen entfallen im Block; der Anbieter steht im Impressum im Konto und in § 1 der Bedingungen, die Abrechnung (monatlich im Voraus per Stripe) in den Zeilen zu Preis und Fälligkeit und in § 7 | beide Zeilen bleiben im Block über dem Knopf | KQ6 |
|
||||
| R18 | Länge des Blocks „Was du bestellst“ am Handy (Abschlussprüfung F1) | Der Block über dem Knopf ist knapp: Leistung als eine Zeile mit Link „Leistungen ansehen“ auf die Leistungsbeschreibung, Preis, Fällig, Laufzeit, Beginn und Zahlung in je ein bis drei Zeilen; die ausführlichen Sätze (Leistung, Laufzeit mit Kündigungsfrist, Zahlungsweg, Gutschrift und Abbuchungstag beim Wechsel) stehen unter dem Knopf („Im Einzelnen“), ohne Klappe. So stehen Kerninfo und Knopf auch auf 375 × 667 zusammen im nutzbaren Bereich (zwischen Kopf und Leiste) | voller Wortlaut im Block über dem Knopf: am Handy dann nicht mehr auf einem Bildschirm mit dem Knopf | KQ6 |
|
||||
|
||||
## Fragen an die Kanzlei
|
||||
|
||||
Jede Frage hat vier Teile wie in `docs/rechtliches/zeiten-kanzlei-fragen.md`: **Frage** (was wir wissen möchten), **Fundstelle im Text** (wo der Wortlaut steht), **Unsere Annahme** (der Stand, mit dem wir arbeiten; bitte bestätigen oder korrigieren), **Alternative** (was wir stattdessen täten). Annahmen sind keine Rechtsurteile.
|
||||
|
||||
### KQ1: Geltung der Fassung 6 und Information an laufende Abos
|
||||
|
||||
**Frage.** Fassung 6 gilt nur für neue Abschlüsse. Wer ein laufendes Abo nach Fassung 3 bis 5 hat und eine Funktion einschaltet, stimmt Leistungsbeschreibung und Auftragsverarbeitung der Funktion in der angezeigten Fassung durch den Klick auf „Einschalten“ zu; dazu gehören die Regeln zur Einrichtung (Stundenkontingent) und zum Vorschuss. Reicht der Klick, um diese Regeln in das laufende Abo einzubeziehen, oder braucht es eine Zustimmung nach dem bisherigen § 15 (Mitteilung mit sechs Wochen Frist, Zustimmung)? Muss WEBklar laufende Kunden über Fassung 6 informieren, obwohl sie für sie nur vorteilhaft ist?
|
||||
|
||||
**Fundstelle im Text.** § 15 Abs. 2 der Abo-Bedingungen Fassung 6; § 3 Abs. 8; § 4 Abs. 8; Nr. 1 Abs. 1 der Leistungsbeschreibungen (Baustein „geltung“).
|
||||
|
||||
**Unsere Annahme.** Der Klick genügt, weil jede Funktion ein zusätzliches Angebot ohne Gebühr ist und die Änderung des Abos nur für neue Abschlüsse gilt. Eine Info-Mail ist keine Pflicht; wir würden später freiwillig informieren.
|
||||
|
||||
**Alternative.** Info-Mail an alle laufenden Abos mit Bitte um Zustimmung nach § 15; Funktionen für Altkunden erst nach dieser Zustimmung.
|
||||
|
||||
### KQ2: Vorschuss-Regel für die Einrichtung
|
||||
|
||||
**Frage.** Die Einrichtung einer Funktion geht mit der tatsächlich aufgewendeten Zeit vom Stundenkontingent ab. Reicht es nicht, entsteht ein Vorschuss, den die folgenden Monatskontingente ausgleichen; ein Rest bei Vertragsende verfällt zugunsten des Kunden, und es wird nie Mehraufwand berechnet. Ist das transparent und wirksam, auch gegenüber Verbrauchern (Überraschungsklausel, Kontingent als Vorleistung)? Braucht es eine Obergrenze oder eine Rückfrage?
|
||||
|
||||
**Fundstelle im Text.** § 4 Abs. 2 und 8 der Abo-Bedingungen Fassung 6; Nr. 5 Abs. 2 (Terminplaner), Nr. 3 Abs. 1 (News-Pinnwand), Nr. 4 Abs. 1 (Öffnungszeiten live), Nr. 9 Abs. 1 (Webshop, Buchungskalender).
|
||||
|
||||
**Unsere Annahme.** Die Regel ist für den Kunden günstig (keine zusätzliche Rechnung, Verfall zu seinen Gunsten) und wird im Kundenportal mit „Vorschuss für Einrichtung“ angezeigt.
|
||||
|
||||
**Alternative.** Obergrenze (zum Beispiel 3 Stunden) mit Rückfrage darüber; oder Einrichtung nur, soweit das Kontingent reicht.
|
||||
|
||||
### KQ3: Einrichtung in der Widerrufsfrist
|
||||
|
||||
**Frage.** Das Einschalten einer Funktion ist nach unserem Text Teil des Abos und kein eigener Vertrag; es verlängert die Widerrufsfrist nicht. Ist das richtig, oder ist die Einrichtung eine eigene Leistung mit eigenem Widerrufsrecht? Erlischt das Widerrufsrecht nach § 356 Abs. 5 Nr. 2 BGB bei einem Dauerschuldverhältnis überhaupt durch „vollständige Erbringung“, und wie bemisst sich der Wertersatz nach § 357a Abs. 2 BGB für Einrichtungszeit innerhalb der Frist?
|
||||
|
||||
**Fundstelle im Text.** Hinweise zum Widerruf Nr. 5 und § 2 Abs. 2 c der Abo-Bedingungen Fassung 6; Baustein „widerruf“ der Leistungsbeschreibungen; `WORTLAUT_V6.beginn.einrichtung` in `server/services/products.js`.
|
||||
|
||||
**Unsere Annahme.** Teil des Abos; Wertersatz nach aufgewendeter Einrichtungszeit zum Stundensatz des Abos; das Häkchen gibt es nur für Privatkunden in der Frist.
|
||||
|
||||
**Alternative.** WEBklar verzichtet auf Wertersatz für die Einrichtung (wie beim Zusatzspeicher nach § 10 Abs. 3) und das Häkchen für die Einrichtung entfällt.
|
||||
|
||||
### KQ4: Belastbarkeit der Kundenart
|
||||
|
||||
**Frage.** Das Team hinterlegt die Kundenart („Betrieb“ oder „privat“) nach den Angaben des Kunden; ist keine hinterlegt, gilt der Kunde als privat; maßgeblich bleibt der Zweck des Vertrags (§ 1 Abs. 2). Wie belastbar ist das, wenn sich später ein Verbraucher als Betrieb geführt herausstellt (Existenzgründer, Vereine, Freiberufler)? Wer trägt das Risiko einer fehlenden Belehrung? Das ist die Frage 5 aus `recht.md` § 4.
|
||||
|
||||
**Fundstelle im Text.** § 1 Abs. 2 und § 2 Abs. 2 c der Abo-Bedingungen Fassung 6; `WORTLAUT_V6.beginn`; Nr. 1 Abs. 1 und Nr. 5 Abs. 1 der Leistungsbeschreibung Webshop.
|
||||
|
||||
**Unsere Annahme.** Unsere Kunden sind fast nur Betriebe (E2). Widerrufsbelehrung, Muster-Formular und die Funktion „Vertrag widerrufen“ bleiben für alle erreichbar; nur das Häkchen und der Beginn nach der Frist hängen an der Kundenart.
|
||||
|
||||
**Alternative.** Selbstauskunft des Kunden bei der Bestellung („Ich bestelle als Unternehmer“) oder das Häkchen für alle Kunden.
|
||||
|
||||
### KQ5: Häkchentexte ohne Normzitat, Wechsel nur mit Häkchen
|
||||
|
||||
**Frage.** Dürfen die Häkchen für den Sofortbeginn auf zwei bis drei Sätze ohne Paragrafenzitate gekürzt werden, ohne die Information nach Art. 246a § 1 Abs. 3 EGBGB über das Erlöschen und den Wertersatz zu verlieren (Frage 2 aus `recht.md` § 4)? Ist es zulässig, dass der Wechsel ins Wartungs-Abo für Privatkunden nur mit Häkchen möglich ist, weil er sofort wirkt? Genügt beim Zusatzspeicher und bei der Übernahmegutschrift eine Linkzeile ohne Häkchen, weil WEBklar auf Wertersatz verzichtet?
|
||||
|
||||
**Fundstelle im Text.** `WORTLAUT_V6.beginn` (Abo, Wechsel, Einrichtung); § 2 Abs. 2 c und § 9 Abs. 6 der Abo-Bedingungen Fassung 6; § 10 Abs. 3 und Hinweise zum Widerruf Nr. 4.
|
||||
|
||||
**Unsere Annahme.** Die Wortlaute der Konstante genügen; für Privatkunden ist der Wechsel ohne Häkchen gesperrt; Zusatzspeicher und Gutschrift brauchen kein Häkchen.
|
||||
|
||||
**Alternative.** Zitate §§ 356 Abs. 5 Nr. 2, 357a Abs. 2 BGB im Häkchen; Wechsel für Privatkunden mit Beginn nach Ablauf der Frist.
|
||||
|
||||
### KQ6: Belehrung und Pflichtinformationen per Link und PDF, Anrede der Seiten Kündigen und Widerrufen
|
||||
|
||||
**Frage.** Genügt der Link auf Widerrufsbelehrung und Muster-Formular vor der Bestellung, wenn der Volltext erst im Bestätigungs-PDF steht (Frage 4 aus `recht.md` § 4)? Passen Belehrung und Formular zum Gestaltungshinweis in Anlage 1 EGBGB zu § 356a? Dürfen die Zeilen „Anbieter“ und „Abrechnung“ im Block über dem Knopf entfallen, solange der Anbieter im Impressum und in § 1 steht und die Abrechnung in den Zeilen zu Preis und Fälligkeit? Ändert die Du-Anrede der Seiten Kündigen und Widerrufen etwas an der Wirksamkeit von Kündigung (§ 312k BGB) und Widerruf (§ 356a BGB)?
|
||||
|
||||
**Fundstelle im Text.** § 2 Abs. 2 c, Widerrufsbelehrung, Muster-Widerrufsformular und Hinweise zum Widerruf der Abo-Bedingungen Fassung 6; Seiten `kuendigen.html` und `widerrufen.html` in der neuen Ansicht; Block „Was du bestellst“ der Bestellseiten.
|
||||
|
||||
**Unsere Annahme.** Der Link genügt, das PDF kommt vor Leistungsbeginn per E-Mail (§ 312f BGB); Anbieter und Abrechnung sind an anderer Stelle erreichbar; die Du-Anrede ändert die Wirksamkeit nicht.
|
||||
|
||||
**Alternative.** Volltext der Belehrung auf der Bestellseite; Zeilen „Anbieter“ und „Abrechnung“ bleiben im Block; Sie-Form der beiden Seiten.
|
||||
|
||||
### KQ7: AVV per Klick auf die Linkzeile, Struktur A, B oder C
|
||||
|
||||
**Frage.** Genügt der Klick auf „Einschalten“ unter der Linkzeile als Abschluss des Auftragsverarbeitungsvertrags nach Art. 28 Abs. 3 und 9 DSGVO („schriftlich, auch elektronisch“), ohne eigenes Häkchen (Frage 3 aus `recht.md` § 4)? Welche Struktur ist am sichersten: A (eigene Verträge je Funktion), B (Anlagen zu den Abo-Bedingungen) oder C (Rahmen-AVV mit Anlage je Funktion)?
|
||||
|
||||
**Fundstelle im Text.** § 14 Abs. 2 der Abo-Bedingungen Fassung 6; Vorbemerkung Abs. 1 jedes AVV in `docs/rechtliches/avv-*-v6.md`; Abschnitt „AVV-Struktur“ unten.
|
||||
|
||||
**Unsere Annahme.** Der Klick genügt, weil Wortlaut, Fassung, Zeit, Konto und angemeldete Person gespeichert werden; wir bleiben bei Option A.
|
||||
|
||||
**Alternative.** Ein eigenes Häkchen für den AVV; Option B oder C.
|
||||
|
||||
### KQ8: Funktionen als Teil des Abos, Kopplung an das Abo-Ende, Rückkehr in der Frist
|
||||
|
||||
**Frage.** Sind die Funktionen als Teil der Abo-Leistung wirksam geregelt (§ 3 Abs. 8), einschließlich der Kopplung an das Abo (§ 11 Abs. 4: Ende mit dem Abo, Weiterlauf von Öffnungszeiten live für 30 Tage, Löschfristen von 30, 60 und 90 Tagen)? Ist es zulässig, dass ein neues Abo in der Frist die Funktion mit den vorhandenen Daten ohne neue Zustimmung zurückholt? Tragen die Analyse im Hosting-Abo (C10) und die Abrechnung der Einrichtung beim Projektstart über die Projektkosten (E9, E21) ohne weitere Hinweise?
|
||||
|
||||
**Fundstelle im Text.** § 3 Abs. 8, § 4 Abs. 9 und § 11 Abs. 4 der Abo-Bedingungen Fassung 6; Abschnitt „Ende der Leistung“ jeder Leistungsbeschreibung; Nr. 10 Abs. 5 der Leistungsbeschreibungen Webshop und Buchungskalender („auf Wunsch in Textform“).
|
||||
|
||||
**Unsere Annahme.** Die Regeln sind als Leistungsbeschreibung wirksam; die Rückkehr in der Frist holt die Funktion zurück. Offen ist für uns, ob die Rückkehr bei Webshop und Buchungskalender wie heute „auf Wunsch in Textform“ oder automatisch geschehen soll.
|
||||
|
||||
**Alternative.** Öffnungszeiten live wie die übrigen Funktionen am Folgetag abschalten (R8); Rückkehr nur auf Wunsch in Textform; einheitliche Löschfrist von 30 Tagen.
|
||||
|
||||
### KQ9: Ausschlüsse als Vertragsklausel
|
||||
|
||||
**Frage.** Sind die Ausschlüsse in den Leistungsbeschreibungen als Leistungsgrenzen wirksam: Terminplaner nicht im Gesundheitsbereich, Webshop nur für Betriebe und ohne die ausgeschlossenen Waren, Buchungskalender nur für Unternehmer? Gilt das auch, wenn die Prüfung beim Einschalten nur durch das Team oder durch eine Selbstangabe im Fragebogen erfolgt? Darf WEBklar bei Verstößen abschalten?
|
||||
|
||||
**Fundstelle im Text.** Nr. 4 Abs. 3 der Leistungsbeschreibung Terminplaner; Nr. 1 Abs. 1, Nr. 3 und Nr. 5 Abs. 1 der Leistungsbeschreibung Webshop; Nr. 1 Abs. 1 der Leistungsbeschreibung Buchungskalender.
|
||||
|
||||
**Unsere Annahme.** Die Ausschlüsse sind als Leistungsbeschreibung wirksam; das Team prüft bei der Freigabe (R13).
|
||||
|
||||
**Alternative.** Webshop auch für Privatkunden mit Hinweis (C15 verschoben); keine automatische Abschaltung, nur ein Hinweis in Textform.
|
||||
|
||||
## AVV-Struktur
|
||||
|
||||
Die Auftragsverarbeitung der Funktionen ist in Fassung 6 so geregelt, dass der Kunde sie mit dem Klick auf „Einschalten“ unter der Linkzeile abschließt. Drei Aufbauten sind möglich; empfohlen wird Option A.
|
||||
|
||||
**Option A, empfohlen: eigene Verträge je Funktion, Anlage 1 bleibt der Rahmen.** Die AVV Terminplaner, Webshop, Buchungskalender und Website-Analyse bleiben eigene Dokumente mit einer neuen Vorbemerkung (Abschluss per Klick) und gelten neben Anlage 1 der Abo-Bedingungen (§ 14 Abs. 2). Vorteil: jede Funktion hat ihre eigenen Datenarten, Fristen und Unterauftragsverarbeiter, und die bestehenden Verträge ändern sich kaum. Nachteil: vier Einzelverträge, deren Gleichlauf mit Anlage 1 bei jeder Änderung geprüft werden muss.
|
||||
|
||||
**Option B: Anlagen 4 bis 7 zu den Abo-Bedingungen.** Die AVV der Funktionen werden Anlagen der Abo-Bedingungen. Vorteil: ein Dokument mit einer Fassung, der Kunde stimmt den Bedingungen einmal zu. Nachteil: die Abo-Bedingungen werden lang und müssen bei jeder Änderung einer Funktion neu versioniert werden; Funktionen, die ein Kunde nie nutzt, stehen trotzdem in seinem Vertrag.
|
||||
|
||||
**Option C: Rahmen-AVV mit einer Anlage je Funktion.** Ein neuer Rahmenvertrag regelt das Gemeinsame (Weisungen, Sicherheit, Löschung), die Anlagen je Funktion nennen nur Daten und Unterauftragsverarbeiter. Vorteil: einheitliche Regeln, bei Änderungen ändert sich nur die Anlage. Nachteil: ein neuer Vertrag neben Anlage 1, abzustimmen mit der Kanzlei, und mehr Arbeit als jetzt nötig.
|
||||
|
||||
## Ablauf nach der Freigabe
|
||||
|
||||
Die Reihenfolge ist fest; jeder Schritt erst, wenn der vorige grün ist. Die Skripte laufen nur auf ausdrückliche Anweisung des Controllers nach der Freigabe.
|
||||
|
||||
1. Freigabe des Inhabers je Punkt (R1 bis R17). Die Antworten auf die Kanzlei-Fragen KQ1 bis KQ9 halten den Livegang nicht auf (E20). Entschiedene Änderungen werden zuerst in den Texten und Tests nachgezogen.
|
||||
2. Datum der Fassungen auf den Freigabetag setzen: `config.abo.termsVersionNeu` (Abo), `FUNKTIONEN_FASSUNG_V6` in `server/services/products.js`, die Datei- und Testnamen mit dem Datum (`<slug>-2026-10-16`), die Fassungszeilen der Markdown-Dateien, `Version 6 · Stand …` und den Druckkopf in der Abo-Seite.
|
||||
3. Entwurfsmarke aus allen Dateien entfernen (Fassungszeilen der Markdown-Dateien, der Absatz unter der Kopfzeile und das meta robots in der Abo-Seite), `ENTWURF_V6 = false` in `scripts/build-legal-html.mjs`, und `ENTWURF = false` in `test/aboBedingungenV6.test.js`, `test/funktionenRechtV6.test.js` und `test/kundenansichtEntscheidungen.test.js`. Auch der Vermerk im Kopf dieser Datei und der Prüfpunkte-Datei fällt weg.
|
||||
4. `node --max-old-space-size=256 scripts/build-legal-html.mjs funktionen6` erzeugt die zehn Seiten neu; `scripts/build-legal-html.mjs funktionen6 --check` meldet danach nichts.
|
||||
5. `free -m` (mindestens 2000 MB verfügbar), dann `nice -n 10 scripts/build-legal-pdfs.sh abo6` und `nice -n 10 scripts/build-legal-pdfs.sh funktionen6`, einzeln und nie parallel (`FORCE=1` nur nach einer Textänderung). Die PDFs und die `.sha256`-Dateien werden mit committet.
|
||||
6. `node scripts/rechtliches-hochladen.mjs --probe`. Meldet der Vorflug `STOPP: Karte fehlt: …` (laut `setup-legal.mjs` des WOMS fehlen drei der elf Karten: News, Website-Analyse und AVV Website-Analyse), zuerst `node scripts/rechtliches-hochladen.mjs --karten-anlegen`, danach den Vorflug wiederholen. Ist er grün, folgt ein Aufruf je Dokument (elf Aufrufe): `node scripts/rechtliches-hochladen.mjs <documentKey> --freigabe=JJJJ-MM-TT`. Zuletzt `node scripts/rechtliches-hochladen.mjs --pruefen`: Exit 0 heißt, alle elf Dokumente haben eine aktuelle Version mit passender Quelle; sonst weiter bei Schritt 6.
|
||||
7. Erst danach `KUNDENANSICHT_NEU` auf `on` (P10); P10 prüft vorher, dass `WORTLAUT_STATUS` auf `'freigegeben'` steht und die Gleichheitstests grün sind (Nachtrag N48). Die Verdrahtung der Analyse (`config.analyse.avvVersion`, `legalPaths`, die Weiche der Mail K18) folgt der Freigabe (Nachtrag N33); P10 verschiebt Seite und PDF der Abo-Bedingungen nach `public/`.
|
||||
@@ -0,0 +1,45 @@
|
||||
# Leistungsbeschreibung News-Pinnwand
|
||||
|
||||
**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen · Zusatzbedingungen zum Wartungs-Abo
|
||||
|
||||
## 1. Geltung
|
||||
|
||||
(1) Funktion im Sinne dieser Leistungsbeschreibung ist die News-Pinnwand. Diese Leistungsbeschreibung gilt, wenn der Kunde die Funktion im Kundenportal einschaltet und die dort angezeigte Zeile mit dem Klick auf „Einschalten“ bestätigt. Sie gilt für Funktionen, die ab ihrer Veröffentlichung eingeschaltet werden; hat der Kunde die Funktion zuvor einzeln gekauft, gilt für sie weiter die Leistungsbeschreibung, unter der er sie gekauft hat. Die Funktion ist Teil des Wartungs-Abos. Sie setzt ein laufendes Wartungs-Abo voraus; ein Hosting-Abo genügt nicht.
|
||||
|
||||
(2) Im Übrigen gelten die Abo-Bedingungen. Bei Widersprüchen geht für die News-Pinnwand diese Leistungsbeschreibung vor.
|
||||
|
||||
## 2. Leistung
|
||||
|
||||
(1) WEBklar baut die News-Pinnwand in die Website ein, die der Kunde für das gewählte Projekt bei WEBklar im Kundenportal führt. Die Pinnwand zeigt die Beiträge des Kunden als Zettel mit Titel, Text und Foto; pro Seite erscheinen sechs Beiträge, die neuesten zuerst.
|
||||
|
||||
(2) Die Beiträge schreibt, ändert und löscht der Kunde selbst im Kundenportal im Bereich „Funktionen“. Er wählt je Beitrag Zettelfarbe und Anzeigedauer; danach verschwindet der Beitrag von seiner Website. Hochgeladene Fotos werden verkleinert, alle Metadaten einschließlich Aufnahmeort (GPS) werden entfernt.
|
||||
|
||||
(3) Änderungen an der Website, die über den Einbau der Pinnwand hinausgehen, sind nicht Teil dieser Leistung.
|
||||
|
||||
## 3. Einrichtung
|
||||
|
||||
(1) Die Einrichtung der Funktion rechnet WEBklar als Arbeitszeit auf das Stundenkontingent des Wartungs-Abos an (§ 4 der Abo-Bedingungen). Reicht das verfügbare Kontingent nicht aus, führt WEBklar die Einrichtung trotzdem aus. Die fehlende Zeit ist ein Vorschuss, den die folgenden Monatskontingente ausgleichen. Eine zusätzliche Rechnung gibt es dafür nicht. Endet das Wartungs-Abo, solange noch ein Vorschuss offen ist, verfällt der Rest zugunsten des Kunden.
|
||||
|
||||
(2) Der Einbau ist erfahrungsgemäß meist innerhalb von 1–2 Wochen abgeschlossen. Das ist eine Einschätzung und keine verbindliche Zusage eines Termins. Nach Abschluss des Einbaus schaltet WEBklar die Funktion frei; ab dann erscheinen die Beiträge auf der Website.
|
||||
|
||||
(3) Hat WEBklar die Funktion auf Wunsch des Kunden schon bei der Erstellung seiner Website eingebaut, richtet sich die Abrechnung der Einrichtung nach der Vereinbarung zum Projekt und nicht nach dem Stundenkontingent. Auch dann läuft die Funktion nur, solange das Wartungs-Abo läuft.
|
||||
|
||||
(4) Arbeiten, die nicht zur Einrichtung gehören, führt WEBklar nur nach Absprache vor der Ausführung und nach Freigabe des Kunden in Textform aus; es gilt § 4 der Abo-Bedingungen.
|
||||
|
||||
## 4. Pflichten des Kunden
|
||||
|
||||
(1) Der Kunde ist für die Inhalte seiner Beiträge und Fotos verantwortlich. Er versichert, dass er die nötigen Rechte an Texten und Fotos hat und dass die Inhalte keine Rechte Dritter verletzen, etwa Urheberrechte, Persönlichkeitsrechte oder das Recht am eigenen Bild abgebildeter Personen.
|
||||
|
||||
## 5. Ende der Leistung
|
||||
|
||||
(1) Die Funktion läuft, solange das Wartungs-Abo läuft. Endet es, auch durch einen Wechsel ins Hosting-Abo oder durch den Widerruf eines Wechsels, schaltet WEBklar die Funktion spätestens am folgenden Tag ab. WEBklar informiert den Kunden per E-Mail.
|
||||
|
||||
(2) Auf Wunsch des Kunden schaltet WEBklar die Pinnwand jederzeit ab; für den Widerruf gilt Nr. 6.
|
||||
|
||||
(3) Beiträge und Fotos bleiben nach dem Abschalten 30 Tage gespeichert und werden danach gelöscht; beginnt in dieser Zeit wieder ein Wartungs-Abo, schaltet WEBklar die Pinnwand mit den vorhandenen Beiträgen ohne weitere Kosten wieder ein. Der Kunde kann seine Beiträge und Fotos jederzeit selbst löschen; andernfalls gelten die Fristen der Datenschutzerklärung (Abschnitt 6a).
|
||||
|
||||
## 6. Widerruf
|
||||
|
||||
(1) Das Einschalten der Funktion ist Teil des Abos und kein eigener Vertrag. Für den Widerruf gilt die Widerrufsbelehrung der Abo-Bedingungen. Widerruft ein Verbraucher das Abo oder den Wechsel ins Wartungs-Abo, endet die Funktion.
|
||||
|
||||
(2) Hat der Kunde die Funktion zuvor einzeln gekauft, bleibt die Widerrufsbelehrung maßgeblich, die er bei der Bestellung erhalten hat.
|
||||
@@ -0,0 +1,125 @@
|
||||
# Leistungsbeschreibung Terminplaner
|
||||
|
||||
**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen · Zusatzbedingungen zum Wartungs-Abo
|
||||
|
||||
## 1. Geltung
|
||||
|
||||
(1) Funktion im Sinne dieser Leistungsbeschreibung ist der Terminplaner. Diese Leistungsbeschreibung gilt, wenn der Kunde die Funktion im Kundenportal einschaltet und die dort angezeigte Zeile mit dem Klick auf „Einschalten“ bestätigt. Sie gilt für Funktionen, die ab ihrer Veröffentlichung eingeschaltet werden; hat der Kunde die Funktion zuvor einzeln gekauft, gilt für sie weiter die Leistungsbeschreibung, unter der er sie gekauft hat. Die Funktion ist Teil des Wartungs-Abos. Sie setzt ein laufendes Wartungs-Abo voraus; ein Hosting-Abo genügt nicht. Wie die Zustimmung abläuft, regelt die Vorbemerkung des AVV Terminplaner.
|
||||
|
||||
(2) Im Übrigen gelten die Abo-Bedingungen. Bei Widersprüchen geht für den Terminplaner diese Leistungsbeschreibung vor, in Datenschutzfragen der AVV Terminplaner.
|
||||
|
||||
(3) Leistungsbeschreibung B Nr. 3 der Abo-Bedingungen („Rechtstexte … nicht enthalten“) gilt nicht für den Datenschutz-Absatz nach Nr. 4 Abs. 9 und die Gästetexte nach Nr. 2 Abs. 6. Beides ist Teil des Terminplaners.
|
||||
|
||||
## 2. Leistung
|
||||
|
||||
(1) Der Terminplaner ist ein Buchungs-Widget für die Website des Kunden mit einer Verwaltung im Kundenportal. WEBklar baut je Website-Projekt ein Widget an einer Stelle der Website ein, entweder eingebettet in die Seite oder hinter einem Knopf. Die Gestaltung richtet sich nach dem Fragebogen, den der Kunde vor dem Einschalten im Kundenportal beantwortet.
|
||||
|
||||
(2) Der Kunde legt seine Leistungen, seine Personen oder Plätze, seine Zeiten und seine Regeln für Buchungen fest. Gäste buchen im Widget einen Termin; je nach Einstellung ist die Buchung verbindlich oder eine Anfrage.
|
||||
|
||||
(3) Der Terminplaner versendet im Auftrag des Kunden E-Mails an die Gäste: den Bestätigungslink, die Bestätigung, Änderungen, Absagen, auf Wunsch des Gastes eine Erinnerung (nur mit seinem Haken) und die Eingangsbestätigung eines Widerrufs. Den Kunden benachrichtigt er über neue und geänderte Buchungen.
|
||||
|
||||
(4) Der Kunde sieht und verwaltet seine Termine im Kundenportal und kann sie als CSV-Datei exportieren. Dazu erhält er einen geheimen Kalender-Link (ICS), mit dem er seine Termine in einem Kalenderprogramm abonnieren kann.
|
||||
|
||||
(5) Der Terminplaner enthält einen Missbrauchsschutz gegen automatisierte Buchungen. Die Löschfristen für Buchungsdaten nennt Nummer 9 des AVV Terminplaner.
|
||||
|
||||
(6) Bezahlt wird nur vor Ort. Der Hinweis am Buchungsformular, die Texte der Haken, der Datenschutz-Absatz für die Website des Kunden und die Widerrufsbelehrung für Gäste sind Mustertexte der Standardkonfiguration (Nr. 11 Abs. 1).
|
||||
|
||||
## 3. Nicht enthalten
|
||||
|
||||
(1) Nicht enthalten sind SMS, Online-Zahlung, ein Kalender-Abgleich (es gibt nur das Abonnement des Kalender-Links nach Nr. 2 Abs. 4), Konten für Gäste, Marketing-E-Mails, Gruppentermine, Mehrsprachigkeit und der Einsatz im Gesundheitsbereich (Nr. 4 Abs. 3).
|
||||
|
||||
(2) Zusätzliche Wünsche, etwa ein zweites Widget, Knöpfe je Leistung oder ein Sonderdesign, gehören nicht zur Einrichtung nach Nr. 5. WEBklar führt sie nach Stunden aus (§ 4 der Abo-Bedingungen).
|
||||
|
||||
## 4. Voraussetzungen und Pflichten des Kunden
|
||||
|
||||
(1) Der Kunde hat ein laufendes Wartungs-Abo.
|
||||
|
||||
(2) Der Kunde trägt vor dem Einschalten seine Betriebsangaben ein, dazu die Adresse seines Impressums und die Adresse seiner Datenschutzerklärung.
|
||||
|
||||
(3) Der Terminplaner ist nicht für Angebote im Gesundheitsbereich bestimmt, weil dort besondere Kategorien personenbezogener Daten anfallen können (Art. 9 DSGVO). Der Kunde setzt ihn dort nicht ein und fordert von seinen Gästen keine Gesundheitsangaben. WEBklar darf die Einrichtung für eine solche Website ablehnen oder den Terminplaner dort abschalten. Der Fragebogen fragt die Branche ab; wählt der Kunde den Gesundheitsbereich, ist der Terminplaner für ihn nicht verfügbar.
|
||||
|
||||
(4) Richtet sich das Angebot des Kunden gezielt an Kinder unter 16 Jahren, nutzt der Kunde den Terminplaner nicht.
|
||||
|
||||
(5) Der Kunde nennt eine Telefonnummer, unter der Gäste Termine vereinbaren können, wenn das Widget nicht erreichbar ist (Nr. 6 Abs. 2).
|
||||
|
||||
(6) Er lässt die Texte der Haken und Hinweise im Widget unverändert.
|
||||
|
||||
(7) Er informiert seine Beschäftigten darüber, dass ihre Namen oder Bezeichnungen, ihre Arbeitszeiten und Abwesenheiten im Terminplaner stehen und dass es einen geheimen Kalender-Link gibt.
|
||||
|
||||
(8) Die Pflichten gegenüber seinen Gästen trägt der Kunde: Preisangaben, Allgemeine Geschäftsbedingungen und Stornobedingungen sowie die Einstufung seiner Leistungen als Leistung für Verbraucher oder Unternehmer und als Freizeitleistung.
|
||||
|
||||
(9) Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar beim Einbau dort den Datenschutz-Absatz für die Online-Terminbuchung ein; das gehört zur Einrichtung. Liegt sie woanders, etwa bei einem externen Generator, erhält der Kunde den Absatz als Textbaustein und fügt ihn selbst ein. Für den übrigen Inhalt seiner Datenschutzerklärung bleibt der Kunde verantwortlich (§ 6 Abs. 3 der Abo-Bedingungen). Fällt WEBklar beim Einfügen ein offensichtlicher Widerspruch auf, weist WEBklar den Kunden darauf hin; eine Pflicht zur Prüfung der Datenschutzerklärung entsteht daraus nicht.
|
||||
|
||||
## 5. Einrichtung
|
||||
|
||||
(1) Die Einrichtung umfasst die Einrichtung nach dem Fragebogen, den Einbau des Widgets in die Website, den Datenschutz-Absatz nach Nr. 4 Abs. 9, die Abnahme durch den Kunden und die Freischaltung durch WEBklar.
|
||||
|
||||
(2) Die Einrichtung der Funktion rechnet WEBklar als Arbeitszeit auf das Stundenkontingent des Wartungs-Abos an (§ 4 der Abo-Bedingungen). Reicht das verfügbare Kontingent nicht aus, führt WEBklar die Einrichtung trotzdem aus. Die fehlende Zeit ist ein Vorschuss, den die folgenden Monatskontingente ausgleichen. Eine zusätzliche Rechnung gibt es dafür nicht. Endet das Wartungs-Abo, solange noch ein Vorschuss offen ist, verfällt der Rest zugunsten des Kunden.
|
||||
|
||||
(3) Die voraussichtliche Dauer der Einrichtung beträgt meist 1–2 Wochen. Sie ist eine Einschätzung und keine verbindliche Zusage eines Termins.
|
||||
|
||||
(4) Hat WEBklar die Funktion auf Wunsch des Kunden schon bei der Erstellung seiner Website eingebaut, richtet sich die Abrechnung der Einrichtung nach der Vereinbarung zum Projekt und nicht nach dem Stundenkontingent. Auch dann läuft die Funktion nur, solange das Wartungs-Abo läuft.
|
||||
|
||||
(5) Arbeiten, die nicht zur Einrichtung gehören, führt WEBklar nur nach Absprache vor der Ausführung und nach Freigabe des Kunden in Textform aus; es gilt § 4 der Abo-Bedingungen. Für den Betrieb des Terminplaners berechnet WEBklar neben dem Wartungs-Abo nichts weiter; er läuft, solange das Wartungs-Abo läuft (Nr. 8).
|
||||
|
||||
## 6. Verfügbarkeit und Störungen
|
||||
|
||||
(1) Das Verfügbarkeitsziel nach § 5 der Abo-Bedingungen gilt für die Website, nicht für den Terminplaner. Störungen des Terminplaners beheben wir in angemessener Frist (§ 12 der Abo-Bedingungen).
|
||||
|
||||
(2) Ist der Terminplaner nicht erreichbar, zeigt das Widget einen Ausweichtext mit der Telefonnummer des Kunden (Nr. 4 Abs. 5).
|
||||
|
||||
(3) Alle Zeiten gelten in der Zeitzone Europe/Berlin.
|
||||
|
||||
(4) WEBklar übernimmt keine Gewähr dafür, dass E-Mails zugestellt und gelesen werden. Das hängt vom Postfach des Empfängers und von den Mailgrenzen seines Anbieters ab.
|
||||
|
||||
(5) Die gesetzlichen Rechte des Kunden sowie §§ 12 und 13 der Abo-Bedingungen bleiben unberührt.
|
||||
|
||||
## 7. Haftung für verpasste Termine
|
||||
|
||||
(1) Der Terminplaner ist ein Hilfsmittel. Die Einhaltung der Termine bleibt Sache des Kunden.
|
||||
|
||||
(2) WEBklar übernimmt keine Gewähr für die Zustellung der E-Mails (Nr. 6 Abs. 4).
|
||||
|
||||
(3) Für entgangenen Gewinn und Folgeschäden aus verpassten Terminen haftet WEBklar nur im Rahmen von § 13 Abs. 1 und 2 der Abo-Bedingungen.
|
||||
|
||||
(4) Der Haftungsrahmen des Kunden gegenüber seinen Gästen bleibt unberührt.
|
||||
|
||||
## 8. Ende der Leistung
|
||||
|
||||
(1) Die Funktion läuft, solange das Wartungs-Abo läuft. Endet es, auch durch einen Wechsel ins Hosting-Abo oder durch den Widerruf eines Wechsels, schaltet WEBklar die Funktion spätestens am folgenden Tag ab. WEBklar informiert den Kunden per E-Mail.
|
||||
|
||||
(2) Mit dem Abschalten nimmt der Terminplaner keine neuen Buchungen mehr an, und das Widget zeigt den Ausweichtext. Die Termine bleiben im Kundenportal sichtbar, und der Kunde kann sie als CSV-Datei exportieren. Gäste können über ihren Link absagen. Bereits angeforderte Erinnerungen laufen weiter. Der Kunde informiert seine Gäste.
|
||||
|
||||
(3) WEBklar löscht alle Termindaten und die Einstellungen 30 Tage nach dem Abschalten. Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Einstellungen bestehen und enthält keine Daten von Gästen (Nummer 9 Abs. 6 des AVV Terminplaner).
|
||||
|
||||
(4) Beginnt innerhalb dieser 30 Tage wieder ein Wartungs-Abo, schaltet WEBklar den Terminplaner mit den vorhandenen Daten und Einstellungen ohne weitere Kosten wieder ein. Nach der Löschung ist eine neue Einrichtung nötig (neuer Fragebogen, Einrichtung nach Nr. 5).
|
||||
|
||||
(5) Endet das Wartungs-Abo, bevor WEBklar den Terminplaner freigeschaltet hat, bricht WEBklar die Einrichtung ab; ein Vorschuss nach Nr. 5 Abs. 2 verfällt.
|
||||
|
||||
(6) Auf Wunsch des Kunden in Textform schaltet WEBklar den Terminplaner jederzeit ab; für den Widerruf gilt Nr. 10.
|
||||
|
||||
## 9. Änderungen und Einstellung
|
||||
|
||||
(1) WEBklar darf die Technik des Terminplaners verbessern, solange der Leistungsumfang nicht schlechter wird (§ 3 Abs. 7 der Abo-Bedingungen). Ändert sich dabei, welche Daten erhoben werden, gilt Nummer 12 des AVV Terminplaner.
|
||||
|
||||
(2) WEBklar darf den Terminplaner für eine Website mit einer Frist von vier Wochen beenden oder für alle Kunden mit einer Frist von drei Monaten einstellen, wenn
|
||||
- ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form unzulässig macht oder nur mit wesentlich anderer Technik erlaubt und der Kunde einer dafür nötigen neuen Fassung nicht zustimmt (AVV Terminplaner Nummer 12), oder
|
||||
- eine Sicherheitslücke sich nicht anders beheben lässt.
|
||||
|
||||
Die Mitteilung erfolgt in Textform und nennt den Grund, die Merkmale der Änderung, den Zeitpunkt und bei Verbrauchern das Recht, den Vertrag nach § 327r Abs. 3 BGB zu beenden, mit seiner Frist.
|
||||
|
||||
(3) Ist der Weiterbetrieb aus einem Grund nach Absatz 2 schon vor Ablauf der Frist nicht zulässig, darf WEBklar den Terminplaner ohne Vorankündigung aussetzen, solange das nötig ist. WEBklar informiert den Kunden darüber unverzüglich in Textform mit dem Grund und nimmt den Betrieb wieder auf, sobald das zulässig und technisch möglich ist.
|
||||
|
||||
(4) Eine Einstellung ohne einen Grund nach Absatz 2 ist ausgeschlossen. Gesetzliche Rechte von Verbrauchern bei Änderungen digitaler Produkte (§ 327r BGB) bleiben unberührt.
|
||||
|
||||
## 10. Widerruf
|
||||
|
||||
(1) Das Einschalten der Funktion ist Teil des Abos und kein eigener Vertrag. Für den Widerruf gilt die Widerrufsbelehrung der Abo-Bedingungen. Widerruft ein Verbraucher das Abo oder den Wechsel ins Wartungs-Abo, endet die Funktion.
|
||||
|
||||
(2) Hat der Kunde die Funktion zuvor einzeln gekauft, bleibt die Widerrufsbelehrung maßgeblich, die er bei der Bestellung erhalten hat.
|
||||
|
||||
## 11. Hinweise
|
||||
|
||||
(1) **Keine Rechtsberatung.** Der Hinweis am Buchungsformular, die Texte der Haken, der Datenschutz-Absatz und die Widerrufsbelehrung für Gäste sind Mustertexte für die Standardkonfiguration. WEBklar sorgt dafür, dass sie die Technik richtig beschreiben, und passt sie an, wenn sich die Technik ändert. Die Einstufung seiner Leistungen als Leistung für Verbraucher oder Unternehmer und als Freizeitleistung trägt der Kunde. Eine rechtliche Beratung im Einzelfall schuldet WEBklar nicht.
|
||||
|
||||
(2) **Barrierefreiheit.** Das Widget ist für die Bedienung mit Tastatur und Hilfstechniken gestaltet (Beschriftungen, Fokusführung, Fehlermeldungen als Live-Region). Eine Konformität nach WCAG 2.1 AA oder EN 301 549 sagt WEBklar nicht zu. Ob der Kunde als Dienstleistungserbringer unter das BFSG fällt (Ausnahme für Kleinstunternehmen, § 3 Abs. 3 BFSG), entscheidet und prüft der Kunde.
|
||||
@@ -0,0 +1,192 @@
|
||||
# Leistungsbeschreibung Buchungskalender
|
||||
|
||||
**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen · Zusatzbedingungen zum Wartungs-Abo
|
||||
|
||||
## 1. Geltung
|
||||
|
||||
(1) Diese Leistungsbeschreibung regelt die Funktion „Buchungskalender“. Mit dem Buchungskalender nimmt der Kunde über seine Website Anfragen für die Vermietung von Unterkünften entgegen (Ferienwohnungen, Monteurzimmer, kleine Pensionen) und rechnet die Aufenthalte ab („Vermieter“); Gäste sind die Besucher seiner Website, die im Buchungskalender anfragen und buchen („Gäste“). Die Funktion ist Teil des Wartungs-Abos. Sie setzt ein laufendes Wartungs-Abo voraus; ein Hosting-Abo genügt nicht. Sie richtet sich nur an Unternehmer, die in eigenem Namen und für eigene Rechnung vermieten.
|
||||
|
||||
(2) Diese Leistungsbeschreibung gilt, wenn der Vermieter die Funktion im Kundenportal einschaltet und die dort angezeigte Zeile mit dem Klick auf „Einschalten“ bestätigt. Sie gilt für Funktionen, die ab ihrer Veröffentlichung eingeschaltet werden; hat der Vermieter die Funktion zuvor einzeln gekauft, gilt für sie weiter die Leistungsbeschreibung, unter der er sie gekauft hat. Wie die Zustimmung abläuft, regelt die Vorbemerkung des AVV Buchungskalender. Einschalten kann der Vermieter erst, nachdem WEBklar seine Anfrage für den Buchungskalender geprüft und angenommen hat (Nr. 2 Abs. 1). Für einen Vermieter, dem WEBklar die Funktion als Teil seines Website-Projekts bereitstellt, schaltet WEBklar sie nach seiner Zustimmung in Textform frei; die Bedingungen dieser Leistungsbeschreibung gelten dann ebenso, die Einrichtung rechnet WEBklar nach Nr. 9 Abs. 3 ab.
|
||||
|
||||
(3) Im Übrigen gelten die Abo-Bedingungen. Bei Widersprüchen geht für den Buchungskalender diese Leistungsbeschreibung vor, in Datenschutzfragen der AVV Buchungskalender.
|
||||
|
||||
(4) Leistungsbeschreibung A Nr. 2 Abs. 3 und Nr. 4 sowie Leistungsbeschreibung B Nr. 3 der Abo-Bedingungen (Formulare mit Server, „Rechtstexte … nicht enthalten“) stehen dem Buchungskalender nicht entgegen. Das Widget des Buchungskalenders läuft auf dem Server von WEBklar, nicht auf dem Server der Website, und der Muster-Text zur Stornierung nach Nr. 6 ist Teil des Buchungskalenders.
|
||||
|
||||
## 2. Leistung
|
||||
|
||||
(1) Der Vermieter fragt den Buchungskalender im Kundenportal an und beschreibt seine Unterkünfte (Art der Unterkunft, Zahl der Einheiten, ob er Booking.com oder Airbnb nutzt). WEBklar prüft die Anfrage und nimmt sie an oder lehnt sie mit einer Begründung ab. Ein Anspruch auf Annahme besteht nicht. Erst nach der Annahme kann der Vermieter den Buchungskalender einschalten.
|
||||
|
||||
(2) Die Einrichtung umfasst den Einbau eines Kalender-Widgets an einer Stelle der Website des Vermieters, die Bereitstellung der Verwaltung im Kundenportal, den Einbau des Datenschutz-Absatzes für den Buchungskalender (Nr. 5 Abs. 10) und die Freischaltung durch WEBklar. Für jede Website wird ein Buchungskalender eingerichtet; er kann mehrere Unterkünfte desselben Vermieters enthalten. Zusätzlich gibt es für jeden freigegebenen Aufenthalt eine Buchungsseite bei WEBklar, die der Gast über den Link in seiner E-Mail erreicht.
|
||||
|
||||
(3) So läuft eine Buchung ab:
|
||||
- Das Widget zeigt dem Gast die belegten Nächte. Er wählt Anreise und Abreise, gibt Personenzahl, Namen, E-Mail-Adresse und auf Wunsch Firma, Telefonnummer und Nachricht an und sendet eine **unverbindliche Anfrage**. Er erhält eine Eingangsbestätigung, der Vermieter eine Benachrichtigung.
|
||||
- Der Vermieter prüft die Anfrage im Kundenportal, sieht den vorgerechneten Preis, kann die Posten ändern und gibt die Anfrage mit einer Reservierungsfrist frei oder lehnt sie ab. Gibt er sie frei, reserviert der Buchungskalender die Nächte bis zum Ablauf der Frist, und der Gast erhält eine E-Mail mit Preis, Stornobedingungen und dem Link zur Buchungsseite.
|
||||
- Auf der Buchungsseite sieht der Gast alle Angaben zum Aufenthalt, trägt seine Rechnungsadresse ein, wählt die Zahlart (online über Stripe, wenn der Vermieter sie eingeschaltet hat, oder vor Ort) und bucht mit der Schaltfläche zur zahlungspflichtigen Buchung. Erst damit kommt der Vertrag zwischen Vermieter und Gast zustande, nicht schon mit der Anfrage oder der Freigabe.
|
||||
- Mit der Buchung erzeugt der Buchungskalender eine Rechnung mit lückenloser Nummer und sendet dem Gast eine Buchungsbestätigung mit der Rechnung als PDF; der Vermieter wird benachrichtigt.
|
||||
- Verstreicht die Frist ohne Buchung, läuft die Reservierung ab und die Nächte werden frei; der Gast erhält darüber eine E-Mail.
|
||||
- Der Vermieter kann einen Aufenthalt stornieren. Dann erhält der Gast eine Stornorechnung mit eigener Nummer und, soweit der Vermieter eine Online-Zahlung erstattet, eine Mitteilung über den Erstattungsbetrag. Eine Stornierung durch den Gast gibt es im Widget nicht; er wendet sich an den Vermieter.
|
||||
|
||||
(4) Im Kundenportal verwaltet der Vermieter:
|
||||
- Unterkünfte mit Beschreibung, Höchstzahl der Personen, Mindestnächten und Check-in- und Check-out-Zeiten,
|
||||
- Preise (Staffel nach Aufenthaltsdauer, Endreinigung, Zusatzpersonen, Direktbuchungsrabatt, Steuerart und Steuersätze) und seine Stornobedingungen,
|
||||
- Anfragen und Buchungen mit Status, die Eintragung „bezahlt (vor Ort)“ und Stornierungen,
|
||||
- Kalender mit manuellen Sperren,
|
||||
- die Verbindungen zu anderen Kalendern (Nr. 4),
|
||||
- seine Rechnungen als PDF,
|
||||
- seine Angaben als Aussteller der Rechnungen (Name, Anschrift, Steuernummer oder Umsatzsteuer-Identifikationsnummer, Bankverbindung, Rechnungspräfix) und die Verbindung mit seinem Stripe-Konto (Nr. 3).
|
||||
|
||||
(5) Der Buchungskalender versendet im Namen des Vermieters E-Mails an die Gäste: Eingang der Anfrage, Freigabe mit Buchungslink, Ablehnung, Buchungsbestätigung mit Rechnung, Zahlungseingang, Ablauf der Reservierung und Stornierung. Absender ist die Versandadresse von WEBklar mit dem Namen des Vermieters, als Antwortadresse dient seine E-Mail-Adresse. Den Vermieter benachrichtigt der Buchungskalender über neue Anfragen und neue Buchungen sowie, wenn der Abgleich mit einem anderen Kalender seit mindestens sechs Stunden fehlschlägt.
|
||||
|
||||
(6) Der Buchungskalender enthält einen Missbrauchsschutz gegen automatisierte Anfragen (verstecktes Feld, Mindest-Ausfüllzeit, Obergrenzen je IP-Adresse). Dafür speichert WEBklar mit der Anfrage nur einen Hashwert der IP-Adresse, nicht die IP-Adresse selbst; er wird entfernt, sobald der Vermieter die Anfrage freigibt oder ablehnt, spätestens nach 30 Tagen (Nummer 3 Abs. 4 des AVV Buchungskalender).
|
||||
|
||||
(7) Nicht enthalten sind:
|
||||
- der Meldeschein nach dem Bundesmeldegesetz und die Verwaltung der Meldedaten,
|
||||
- Kurtaxe, Gästebeitrag und Beherbergungsabgaben,
|
||||
- Anzahlungen und Teilzahlungen (der Gast zahlt den Gesamtbetrag in einer Zahlung, online oder vor Ort),
|
||||
- Sofortbuchung ohne Freigabe,
|
||||
- saisonale Preise (die Preise richten sich nur nach der Aufenthaltsdauer),
|
||||
- Gutscheine, Channel-Manager-Anbindungen außer dem Kalenderabgleich nach Nr. 4 und Mehrsprachigkeit (der Buchungskalender ist deutschsprachig),
|
||||
- die Buchführung des Vermieters.
|
||||
|
||||
Zusätzliche Wünsche, etwa ein Sonderdesign, ein zweiter Buchungskalender oder eine über das übliche Maß hinausgehende Zahl von Unterkünften, gehören nicht zur Einrichtung nach Nr. 9. WEBklar führt sie nach Absprache nach Stunden aus (Nr. 9 Abs. 4).
|
||||
|
||||
## 3. Zahlungen über Stripe und Rechnungen im Namen des Vermieters
|
||||
|
||||
(1) Online-Zahlungen im Buchungskalender sind optional und laufen über Stripe Connect. Der Vermieter verbindet dazu sein eigenes Stripe-Konto mit dem Buchungskalender. Hat er für den Webshop von WEBklar bereits ein Stripe-Konto verbunden, kann er es auch für den Buchungskalender verwenden. Den Vertrag über dieses Konto schließt der Vermieter unmittelbar mit Stripe; WEBklar ist daran nicht beteiligt. Für das Konto, die Prüfung des Vermieters durch Stripe und die Auszahlungen gelten die Bedingungen von Stripe. Ohne verbundenes und freigeschaltetes Stripe-Konto ist nur die Zahlart „vor Ort“ möglich.
|
||||
|
||||
(2) Das Geld der Gäste fließt direkt an den Vermieter (Zahlung unmittelbar auf dem Konto des Vermieters, sogenannte Direct Charge). WEBklar erhält und verwahrt kein Geld der Gäste, leitet keines weiter und beteiligt sich nicht am Umsatz des Vermieters. WEBklar ist kein Zahlungsdienstleister. Die Gebühren von Stripe trägt der Vermieter; sie fallen nur zwischen dem Vermieter und Stripe an.
|
||||
|
||||
(3) Der Vermieter prüft selbst, ob sein Stripe-Konto Zahlungen annehmen und Auszahlungen leisten kann. WEBklar zeigt ihm im Kundenportal den Stand, den Stripe meldet, hat aber keinen Einfluss darauf, ob und wann Stripe ein Konto freischaltet, einschränkt oder sperrt, und haftet dafür nicht. Rückbuchungen, Streitfälle mit Gästen und Zahlungsausfälle regeln der Vermieter und Stripe.
|
||||
|
||||
(4) Erstattungen bei einer Stornierung veranlasst der Vermieter im Kundenportal; WEBklar führt die Erstattung über das Stripe-Konto des Vermieters aus. Er bestimmt den Betrag bis zur Höhe der Zahlung des Gastes. Storniert der Vermieter einen Aufenthalt, schließt WEBklar eine noch offene Zahlungsseite bei Stripe. **Automatische Erstattung:** Geht eine Zahlung ein, nachdem die Buchung storniert oder die Reservierung abgelaufen ist, oder zahlt der Gast eine bereits bezahlte Buchung ein zweites Mal, erstattet WEBklar den Betrag ohne weitere Anweisung im Namen des Vermieters in voller Höhe über dessen Stripe-Konto. Dazu erteilt der Vermieter WEBklar mit der Zustimmung zum Buchungskalender eine dauerhafte Weisung; der Stand der Buchung ändert sich dadurch nicht. Erstattungen aus dem Stripe-Dashboard trägt WEBklar in die Buchung nach, soweit Stripe sie meldet.
|
||||
|
||||
(5) Die Zahlungsseite selbst (Stripe Checkout) betreibt Stripe. Online-Zahlungen sind derzeit nur per Karte möglich, einschließlich Apple Pay und Google Pay; eine SEPA-Lastschrift bietet der Buchungskalender nicht an. Kartendaten gibt der Gast nur dort ein; WEBklar erhält sie nicht. Beim Start der Zahlung übergibt WEBklar Stripe als Rücksprungadresse (bei Erfolg und bei Abbruch) den geheimen Link zur Buchungsseite des Gastes samt Token. Dieser Link ist damit für Stripe und im Stripe-Dashboard des Vermieters sichtbar; Der Link gilt bis 90 Tage nach der Abreise (bei nicht zustande gekommenen Buchungen 30 Tage) und öffnet nur diese eine Buchungsseite.
|
||||
|
||||
(6) **Rechnungen im Namen des Vermieters.** Der Vermieter beauftragt WEBklar mit dem Rechnungsauftrag in den Einstellungen des Buchungskalenders, in seinem Namen und für seine Rechnung Rechnungen und Stornorechnungen an seine Gäste auszustellen (Rechnung durch einen Dritten im Namen und im Auftrag des Leistenden, § 14 Abs. 2 Satz 4 UStG). Aussteller der Rechnung ist der Vermieter; sie trägt seinen Namen, seine Anschrift, seine Steuernummer oder Umsatzsteuer-Identifikationsnummer und keinen Hinweis auf WEBklar. WEBklar erzeugt die Rechnung aus den Angaben, die der Vermieter hinterlegt und bei der Freigabe bestätigt hat. Er verantwortet die Richtigkeit seiner Angaben, der Preise, der Steuerart und der Steuersätze; WEBklar prüft sie nicht steuerrechtlich. Die Rechnung wird nach ihrer Ausstellung nicht mehr verändert; Fehler korrigiert der Vermieter durch Stornierung und neue Buchung nach den Möglichkeiten des Kundenportals. Ohne diesen Auftrag stellt WEBklar keine Rechnungen aus. Widerruft er den Auftrag in Textform, stellt der Buchungskalender keine Rechnungen mehr aus und ist für ihn nicht mehr nutzbar.
|
||||
|
||||
(7) Die Rechnungsnummern sind je Vermieter und Jahr lückenlos fortlaufend; sie bestehen aus dem Präfix, das der Vermieter vor der ersten Rechnung festlegt, dem Jahr und einer laufenden Nummer.
|
||||
|
||||
## 4. Verfügbarkeit und Kalenderabgleich (iCal)
|
||||
|
||||
(1) Der Vermieter kann Kalender anderer Plattformen (zum Beispiel Booking.com oder Airbnb) über eine iCal-Adresse einlesen lassen; die eingelesenen Zeiträume blockiert der Buchungskalender. Umgekehrt stellt der Buchungskalender dem Vermieter eine geheime iCal-Adresse bereit, die er bei anderen Plattformen einträgt; sie enthält die belegten Nächte ohne Daten der Gäste. Der Vermieter kann diese Adresse jederzeit erneuern; die bisherige Adresse funktioniert dann nicht mehr.
|
||||
|
||||
(2) Der Abgleich läuft regelmäßig, aber zeitversetzt: WEBklar liest die Kalender anderer Plattformen etwa alle 15 Minuten ein, und die anderen Plattformen lesen die Adresse des Buchungskalenders nach ihrem eigenen Takt, oft nur alle paar Stunden. **Zwischen einer Buchung hier und ihrem Eingang dort können deshalb Minuten bis Stunden vergehen, und umgekehrt.** WEBklar sagt keine Echtzeit-Verfügbarkeit zu und übernimmt keine Gewähr dafür, dass eine Unterkunft nicht doppelt gebucht wird.
|
||||
|
||||
(3) Dieses Risiko mindert die Freigabe: Jede Anfrage im Buchungskalender braucht die Freigabe des Vermieters. Er prüft vor der Freigabe seine anderen Plattformen und seinen Kalender. Beim Freigeben und beim Buchen prüft der Buchungskalender zusätzlich, ob die Nächte nach seinem Stand frei sind. Eine Doppelbuchung entsteht trotzdem, wenn eine andere Plattform eine Buchung noch nicht gemeldet hat oder der Abgleich ausfällt. Der Vermieter löst eine Doppelbuchung selbst (etwa durch Stornierung) und trägt die Folgen gegenüber seinen Gästen und den Plattformen.
|
||||
|
||||
(4) Fehlerhafte oder nicht erreichbare iCal-Adressen kann WEBklar nicht beheben. Schlägt der Abruf fehl, bleibt der zuletzt gelesene Stand erhalten, und der Vermieter wird benachrichtigt, wenn der Abruf seit mindestens sechs Stunden nicht gelungen ist (höchstens einmal je 24 Stunden und Kalender). Eingelesen werden nur Zeiträume von gestern bis 18 Monate in die Zukunft, höchstens 1 Megabyte je Kalender; nur Adressen mit https werden abgerufen.
|
||||
|
||||
## 5. Pflichten des Vermieters
|
||||
|
||||
(1) Der Vermieter hat ein laufendes Wartungs-Abo und ist Unternehmer. Will er Online-Zahlungen nutzen, stellt er sein eigenes Stripe-Konto bereit und schließt die Anmeldung bei Stripe selbst ab. Der Vermieter vermietet in eigenem Namen; für Preise, Stornobedingungen, Rechnungen und Meldepflichten ist er verantwortlich. Online-Zahlungen seiner Gäste laufen, sofern er sie nutzt, über sein eigenes Stripe-Konto zu den Bedingungen von Stripe; WEBklar erhält und verwahrt kein Geld seiner Gäste und erhebt keine Umsatzbeteiligung. Rechnungen an Gäste stellt WEBklar nur aus, wenn der Vermieter in den Einstellungen des Buchungskalenders den Rechnungsauftrag erteilt hat (§ 14 Abs. 2 Satz 4 UStG); für die Richtigkeit von Preisen und Steuerangaben bleibt er verantwortlich.
|
||||
|
||||
(2) Er verantwortet seine Unterkünfte, Preise und Angaben zum Angebot. Dazu gehören richtige Preise einschließlich Steuerausweis (Umsatzsteuer, für die Beherbergung und die Endreinigung nach den dafür geltenden Steuersätzen, oder Hinweis auf § 19 UStG), Check-in- und Check-out-Zeiten, Höchstbelegung, Mindestnächte und alle Angaben, die er seinen Gästen vor der Buchung geben muss. Die Steuerart und die Steuersätze trägt er selbst im Kundenportal ein; WEBklar berät dazu nicht.
|
||||
|
||||
(3) Er legt vor der Freischaltung seine Stornobedingungen fest und verantwortet sie. Der Muster-Text nach Nr. 6 ist nur ein Vorschlag.
|
||||
|
||||
(4) Er verantwortet die Verfügbarkeit seiner Unterkünfte, pflegt Sperren und Kalenderverbindungen im Kundenportal und prüft regelmäßig auf neue Anfragen und Buchungen. Die Folgen von Angaben, die er setzt (zum Beispiel einer fälschlich freien oder gesperrten Zeit), trägt er.
|
||||
|
||||
(5) Er gibt Anfragen nur frei, wenn er die Unterkunft für den Zeitraum zu den angegebenen Bedingungen bereitstellen kann, und erfüllt die gebuchten Aufenthalte. Er trägt alle Pflichten eines Vermieters gegenüber seinen Gästen, auch Gewährleistung, Kundenservice und Beschwerden.
|
||||
|
||||
(6) Er erfüllt seine gesetzlichen Pflichten rund um die Beherbergung selbst. Das betrifft insbesondere den Meldeschein nach dem Bundesmeldegesetz, die Kurtaxe und andere kommunale Abgaben, Anzeige- und Genehmigungspflichten für die Vermietung (zum Beispiel Zweckentfremdungs- oder Gewerberecht) sowie die Pflichten aus seinem Steuerrecht. Der Buchungskalender nimmt ihm davon nichts ab.
|
||||
|
||||
(7) Anzahlungen und Teilzahlungen bietet der Buchungskalender nicht an. Vereinbart der Vermieter außerhalb des Buchungskalenders etwas anderes mit seinem Gast, ist das nicht Gegenstand dieser Leistungsbeschreibung.
|
||||
|
||||
(8) Er erfüllt seine Aufbewahrungspflichten selbst, etwa nach der Abgabenordnung und dem Handelsgesetzbuch. WEBklar ist kein Archiv für die Buchführung und kein Buchführungssystem. WEBklar bewahrt die Rechnungen des Vermieters im Buchungskalender 8 Jahre auf, solange der Buchungskalender läuft und nach Nr. 10 bis zur Löschung, hält sie ihm zum Abruf als PDF bereit und anonymisiert personenbezogene Daten der Gäste nach Nummer 9 des AVV Buchungskalender. Der Vermieter lädt seine Rechnungen deshalb rechtzeitig vor der Löschung nach Nr. 10 Abs. 3 herunter und bewahrt sie selbst auf.
|
||||
|
||||
(9) Er hält die Vorgaben dieser Leistungsbeschreibung ein und verändert den Einbau-Code und die gesetzlich vorgeschriebenen Hinweise des Widgets und der Buchungsseite nicht.
|
||||
|
||||
(10) Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar beim Einbau dort den Datenschutz-Absatz für den Buchungskalender ein; das gehört zur Einrichtung. Liegt sie woanders, erhält der Vermieter den Absatz als Textbaustein und fügt ihn selbst ein. Für den übrigen Inhalt seiner Datenschutzerklärung bleibt er verantwortlich (§ 6 Abs. 3 der Abo-Bedingungen). Fällt WEBklar beim Einfügen ein offensichtlicher Widerspruch auf, weist WEBklar den Vermieter darauf hin; eine Pflicht zur Prüfung der Datenschutzerklärung entsteht daraus nicht.
|
||||
|
||||
(11) Der Vermieter informiert seine Gäste nach Art. 13 DSGVO. Die Rollen im Datenschutz regelt der AVV Buchungskalender: Der Vermieter ist Verantwortlicher, WEBklar Auftragsverarbeiter, Stripe Vertragspartner des Vermieters.
|
||||
|
||||
## 6. Muster-Text zur Stornierung, keine Rechtsberatung
|
||||
|
||||
(1) WEBklar stellt dem Vermieter einen Muster-Text für Stornobedingungen bereit. Er ist eine **Vorlage ohne Gewähr** und keine Rechtsberatung (kein Rechtsrat im Sinne des Rechtsdienstleistungsgesetzes). Er passt zur Technik des Buchungskalenders, kann aber auf den Betrieb des Vermieters nicht zugeschnitten sein.
|
||||
|
||||
(2) WEBklar empfiehlt, die Stornobedingungen und die Angaben zum Angebot vor der Freischaltung von einer Rechtsanwältin oder einem Rechtsanwalt prüfen zu lassen.
|
||||
|
||||
(3) Der Vermieter bleibt für seine Stornobedingungen und alle seine Texte verantwortlich, auch wenn er den Muster-Text unverändert übernimmt (Nr. 5 Abs. 3). Die Stornobedingungen, die bei der Freigabe galten, speichert der Buchungskalender mit der Anfrage; die Buchungsseite und die Buchungsbestätigung zeigen dem Gast diese Fassung.
|
||||
|
||||
## 7. Kein Widerrufsrecht des Gastes, Hinweise auf der Buchungsseite
|
||||
|
||||
(1) Für die Buchung von Beherbergungsleistungen zu einem bestimmten Zeitpunkt oder in einem bestimmten Zeitraum besteht nach § 312g Abs. 2 Satz 1 Nr. 9 BGB kein gesetzliches Widerrufsrecht. Die Buchungsseite weist den Gast darauf hin; der Vermieter kann ihm vertraglich ein weitergehendes Recht einräumen, der Buchungskalender bietet dafür aber keine Funktion.
|
||||
|
||||
(2) Die Buchungsseite zeigt dem Gast vor der Schaltfläche zur zahlungspflichtigen Buchung die wesentlichen Angaben nach Art. 246a EGBGB, soweit der Buchungskalender sie kennt (Anbieter, Unterkunft, Zeitraum, Leistung, Preis insgesamt, Zahlart, Stornobedingungen, Hinweis auf das fehlende Widerrufsrecht). Der Vermieter ergänzt, was darüber hinausgeht, in seinen Texten selbst (Nr. 5 Abs. 2).
|
||||
|
||||
(3) Die Buchungsbestätigung erhält der Gast als E-Mail mit der Rechnung als PDF; sie enthält die Angaben zum Vertrag und die Stornobedingungen auf einem dauerhaften Datenträger.
|
||||
|
||||
## 8. Verfügbarkeit und Störungen
|
||||
|
||||
(1) Das Verfügbarkeitsziel nach § 5 der Abo-Bedingungen gilt für die Website, nicht für den Buchungskalender. WEBklar sagt für den Buchungskalender keine bestimmte Verfügbarkeit zu, die über den Betrieb der Website im Abo hinausgeht. Störungen des Buchungskalenders beheben wir in angemessener Frist (§ 12 der Abo-Bedingungen).
|
||||
|
||||
(2) Angekündigte Wartungsfenster (§ 5 der Abo-Bedingungen) können den Buchungskalender kurzzeitig unterbrechen. Während einer Unterbrechung sind Anfragen und Buchungen nicht möglich; das Widget zeigt dann einen Ausweichtext.
|
||||
|
||||
(3) Zahlungen übernimmt der Buchungskalender über Benachrichtigungen von Stripe und einen Abgleich beim Aufruf der Buchungsseite. Fällt die Verbindung zu Stripe oder zum Server aus, kann sich die Bestätigung einer Zahlung verzögern. Eine Zahlung, die nach einer Stornierung oder nach dem Ablauf der Reservierung eingeht, und eine zweite Zahlung auf eine bereits bezahlte Buchung erstattet WEBklar automatisch und in voller Höhe (Nr. 3 Abs. 4); die Buchung bleibt dabei unverändert, und die Erstattung braucht einige Tage, bis sie beim Gast ankommt. Ausfälle von Stripe, von Zahlungsarten und von Banken liegen nicht im Einflussbereich von WEBklar.
|
||||
|
||||
(4) Alle Zeiten gelten in der Zeitzone Europe/Berlin.
|
||||
|
||||
(5) WEBklar übernimmt keine Gewähr dafür, dass E-Mails zugestellt und gelesen werden. Das hängt vom Postfach des Empfängers und von den Mailgrenzen seines Anbieters ab.
|
||||
|
||||
(6) Die gesetzlichen Rechte des Vermieters sowie §§ 12 und 13 der Abo-Bedingungen bleiben unberührt.
|
||||
|
||||
## 9. Einrichtung, Abrechnung und Widerruf
|
||||
|
||||
(1) Die Einrichtung der Funktion rechnet WEBklar als Arbeitszeit auf das Stundenkontingent des Wartungs-Abos an (§ 4 der Abo-Bedingungen). Reicht das verfügbare Kontingent nicht aus, führt WEBklar die Einrichtung trotzdem aus. Die fehlende Zeit ist ein Vorschuss, den die folgenden Monatskontingente ausgleichen. Eine zusätzliche Rechnung gibt es dafür nicht. Endet das Wartungs-Abo, solange noch ein Vorschuss offen ist, verfällt der Rest zugunsten des Vermieters. Für den Betrieb des Buchungskalenders berechnet WEBklar nichts weiter; es gibt keine laufende Gebühr und keine Beteiligung am Umsatz oder an den Buchungen.
|
||||
|
||||
(2) Die Einrichtung dauert nach der Erfahrung von WEBklar meist 1 bis 2 Wochen ab dem Einschalten. Sie hängt davon ab, wie schnell der Vermieter Unterkünfte, Preise und gegebenenfalls das Stripe-Konto bereitstellt und wie umfangreich der Einbau ist. Die Dauer ist eine Einschätzung und keine verbindliche Zusage eines Termins.
|
||||
|
||||
(3) Hat WEBklar die Funktion auf Wunsch des Vermieters schon bei der Erstellung seiner Website eingebaut, richtet sich die Abrechnung der Einrichtung nach der Vereinbarung zum Projekt und nicht nach dem Stundenkontingent. Auch dann läuft die Funktion nur, solange das Wartungs-Abo läuft.
|
||||
|
||||
(4) Arbeiten, die nicht zur Einrichtung gehören, führt WEBklar nur nach Absprache vor der Ausführung und nach Freigabe des Vermieters in Textform aus; es gilt § 4 der Abo-Bedingungen. Welche Wünsche nicht zur Einrichtung gehören, beschreibt Nr. 2 Abs. 7.
|
||||
|
||||
(5) Das Einschalten der Funktion ist Teil des Abos und kein eigener Vertrag. Für den Widerruf gilt die Widerrufsbelehrung der Abo-Bedingungen. Widerruft ein Verbraucher das Abo oder den Wechsel ins Wartungs-Abo, endet die Funktion. Hat der Vermieter die Funktion zuvor einzeln gekauft, bleibt die Widerrufsbelehrung maßgeblich, die er bei der Bestellung erhalten hat. Das Widerrufsrecht gegenüber WEBklar ist vom Widerrufsrecht der Gäste gegenüber dem Vermieter (Nr. 7) zu unterscheiden.
|
||||
|
||||
## 10. Laufzeit und Ende
|
||||
|
||||
(1) Die Funktion läuft, solange das Wartungs-Abo läuft. Endet es, auch durch einen Wechsel ins Hosting-Abo oder durch den Widerruf eines Wechsels, schaltet WEBklar die Funktion spätestens am folgenden Tag ab. WEBklar informiert den Vermieter per E-Mail und weist ihn darauf hin, seine Rechnungen als PDF herunterzuladen.
|
||||
|
||||
(2) Mit dem Abschalten nimmt der Buchungskalender keine neuen Anfragen und Buchungen mehr an, und das Widget zeigt den Ausweichtext. Reservierungen laufen aus. Bereits gebuchte Aufenthalte bleiben Verträge zwischen dem Vermieter und seinen Gästen; der Vermieter erfüllt sie und informiert seine Gäste, wenn er den Buchungskalender nicht mehr anbietet. Anfragen, Buchungen und Rechnungen bleiben im Kundenportal 90 Tage lesbar; Rechnungen kann er einzeln als PDF herunterladen.
|
||||
|
||||
(3) WEBklar löscht alle Daten des Buchungskalenders, also Unterkünfte, Preise, Anfragen, Buchungen, Rechnungen, Kalenderverbindungen, Sperren und Einstellungen, 90 Tage nach dem Abschalten. Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Angaben zur Einrichtung bestehen und enthält keine Daten von Gästen (Nummer 9 Abs. 6 des AVV Buchungskalender). Die Aufbewahrungsfrist der Rechnungen (Nr. 5 Abs. 8) endet damit mit der Löschung; der Vermieter bewahrt sie danach selbst auf. Nach dem Abschalten weist WEBklar den Vermieter per E-Mail mit dem Löschdatum darauf hin, seine Rechnungen zu sichern; bis dahin sind sie im Kundenportal abrufbar. Wird das Kundenkonto gelöscht, schaltet WEBklar den Buchungskalender ab und löscht seine Daten wie nach Absatz 3 (90 Tage nach dem Abschalten); einen Hinweis per E-Mail gibt es dann nicht, weil mit dem Konto auch die E-Mail-Adresse gelöscht wird.
|
||||
|
||||
(4) Das Stripe-Konto bleibt beim Vermieter. Das Ende des Buchungskalenders berührt weder das Konto noch die Auszahlungen noch den Vertrag mit Stripe. Der Vermieter kann die Verbindung zwischen dem Konto und dem Buchungskalender im Stripe-Dashboard beenden.
|
||||
|
||||
(5) Beginnt innerhalb der 90 Tage wieder ein Wartungs-Abo, schaltet WEBklar den Buchungskalender auf Wunsch des Vermieters in Textform mit den vorhandenen Daten und Einstellungen ohne weitere Kosten wieder ein. Nach der Löschung ist eine neue Anfrage und eine neue Einrichtung nötig (Einrichtung nach Nr. 9).
|
||||
|
||||
(6) Endet das Wartungs-Abo, bevor WEBklar den Buchungskalender freigeschaltet hat, bricht WEBklar die Einrichtung ab; ein Vorschuss nach Nr. 9 Abs. 1 verfällt.
|
||||
|
||||
(7) Auf Wunsch des Vermieters in Textform schaltet WEBklar den Buchungskalender jederzeit ab; für den Widerruf gilt Nr. 9 Abs. 5.
|
||||
|
||||
## 11. Haftung
|
||||
|
||||
(1) Der Buchungskalender ist ein Hilfsmittel für die Vermietung des Vermieters. Der Vertrag über die Unterkunft besteht allein zwischen dem Vermieter und dem Gast; WEBklar ist daran nicht beteiligt.
|
||||
|
||||
(2) Für entgangenen Gewinn und Folgeschäden aus Störungen des Buchungskalenders, aus Verzögerungen oder Fehlern beim Kalenderabgleich (Nr. 4), aus Doppelbuchungen und aus nicht zugestellten E-Mails haftet WEBklar nur im Rahmen von § 13 Abs. 1 und 2 der Abo-Bedingungen.
|
||||
|
||||
(3) WEBklar haftet nicht für Inhalte des Vermieters, insbesondere nicht für seine Unterkünfte, Preise, Steuerangaben, Stornobedingungen und Rechtstexte, auch nicht für den Muster-Text, den er übernommen hat (Nr. 6), und nicht für die Erfüllung seiner Pflichten gegenüber den Gästen und den Behörden (Nr. 5).
|
||||
|
||||
(4) WEBklar haftet nicht für Ausfälle, Sperren oder Entscheidungen von Stripe und der Zahlungsarten sowie für Rückbuchungen und Zahlungsausfälle (Nr. 3 Abs. 3), und nicht für Ausfälle, Änderungen und Entscheidungen anderer Plattformen, deren Kalender der Vermieter einbindet.
|
||||
|
||||
(5) Die Haftung nach § 13 Abs. 1 der Abo-Bedingungen (unter anderem bei Vorsatz, grober Fahrlässigkeit, Verletzung von Leben, Körper oder Gesundheit und nach dem Produkthaftungsgesetz) bleibt unberührt. Der Haftungsrahmen des Vermieters gegenüber seinen Gästen bleibt unberührt.
|
||||
|
||||
## 12. Änderungen und Einstellung
|
||||
|
||||
(1) WEBklar darf die Technik des Buchungskalenders verbessern, solange der Leistungsumfang nicht schlechter wird (§ 3 Abs. 7 der Abo-Bedingungen). Ändert sich dabei, welche Daten erhoben werden, gilt Nummer 12 des AVV Buchungskalender.
|
||||
|
||||
(2) WEBklar darf den Buchungskalender für eine Website mit einer Frist von vier Wochen beenden oder für alle Vermieter mit einer Frist von drei Monaten einstellen, wenn
|
||||
- ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form unzulässig macht oder nur mit wesentlich anderer Technik erlaubt und der Vermieter einer dafür nötigen neuen Fassung nicht zustimmt (AVV Buchungskalender Nummer 12),
|
||||
- Stripe die Anbindung nach Nr. 3 beendet oder wesentlich ändert, so dass die Online-Zahlung in der bisherigen Form nicht weiterbetrieben werden kann (dann entfällt nur die Online-Zahlung, soweit der Buchungskalender ohne sie weiterläuft), oder
|
||||
- eine Sicherheitslücke sich nicht anders beheben lässt.
|
||||
|
||||
Die Mitteilung erfolgt in Textform und nennt den Grund, die Merkmale der Änderung, den Zeitpunkt und bei Verbrauchern das Recht, den Vertrag nach § 327r Abs. 3 BGB zu beenden, mit seiner Frist.
|
||||
|
||||
(3) Ist der Weiterbetrieb aus einem Grund nach Absatz 2 schon vor Ablauf der Frist nicht zulässig, darf WEBklar den Buchungskalender ohne Vorankündigung aussetzen, solange das nötig ist. WEBklar informiert den Vermieter darüber unverzüglich in Textform mit dem Grund und nimmt den Betrieb wieder auf, sobald das zulässig und technisch möglich ist.
|
||||
|
||||
(4) Eine Einstellung ohne einen Grund nach Absatz 2 ist ausgeschlossen. Gesetzliche Rechte von Verbrauchern bei Änderungen digitaler Produkte (§ 327r BGB) bleiben unberührt.
|
||||
|
||||
## 13. Hinweise
|
||||
|
||||
(1) **Keine Rechtsberatung.** Der Buchungskalender und der Muster-Text sind technische Hilfsmittel, keine Rechtsberatung (Nr. 6). Die rechtliche und steuerliche Einordnung seiner Vermietung, seiner Preisangaben und seines Angebots trägt der Vermieter.
|
||||
|
||||
(2) **Barrierefreiheit.** Das Widget ist für die Bedienung mit Tastatur und Hilfstechniken gestaltet (Beschriftungen der Tage, Bedienung mit Pfeiltasten, Fehlermeldungen als Live-Region). Eine Konformität nach WCAG 2.1 AA oder EN 301 549 sagt WEBklar nicht zu. Ob der Vermieter als Anbieter von Dienstleistungen im elektronischen Geschäftsverkehr unter das BFSG fällt (Ausnahme für Kleinstunternehmen, § 3 Abs. 3 BFSG), entscheidet und prüft der Vermieter.
|
||||
@@ -0,0 +1,179 @@
|
||||
# Leistungsbeschreibung Webshop
|
||||
|
||||
**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen · Zusatzbedingungen zum Wartungs-Abo
|
||||
|
||||
## 1. Geltung
|
||||
|
||||
(1) Diese Leistungsbeschreibung regelt die Funktion „Webshop“. Mit dem Webshop verkauft der Kunde Waren über seine Website („Händler“); Käufer sind die Besucher seiner Website, die im Webshop bestellen („Käufer“). Die Funktion ist Teil des Wartungs-Abos. Sie setzt ein laufendes Wartungs-Abo voraus; ein Hosting-Abo genügt nicht. Sie richtet sich nur an Unternehmer, die in eigenem Namen und für eigene Rechnung verkaufen.
|
||||
|
||||
(2) Diese Leistungsbeschreibung gilt, wenn der Händler die Funktion im Kundenportal einschaltet und die dort angezeigte Zeile mit dem Klick auf „Einschalten“ bestätigt. Sie gilt für Funktionen, die ab ihrer Veröffentlichung eingeschaltet werden; hat der Händler die Funktion zuvor einzeln gekauft, gilt für sie weiter die Leistungsbeschreibung, unter der er sie gekauft hat. Wie die Zustimmung abläuft, regelt die Vorbemerkung des AVV Webshop. Einschalten kann der Händler erst, nachdem WEBklar seine Anfrage für den Webshop geprüft und freigegeben hat (Nr. 2 Abs. 1).
|
||||
|
||||
(3) Im Übrigen gelten die Abo-Bedingungen. Bei Widersprüchen geht für den Webshop diese Leistungsbeschreibung vor, in Datenschutzfragen der AVV Webshop.
|
||||
|
||||
(4) Leistungsbeschreibung A Nr. 2 Abs. 3 und Nr. 4 sowie Leistungsbeschreibung B Nr. 3 der Abo-Bedingungen (Formulare mit Server, „Rechtstexte … nicht enthalten“) stehen dem Webshop nicht entgegen. Das Widget des Webshops läuft auf dem Server von WEBklar, nicht auf dem Server der Website, und die Muster-Rechtstexte nach Nr. 6 sind Teil des Webshops.
|
||||
|
||||
## 2. Leistung
|
||||
|
||||
(1) Der Händler fragt den Webshop im Kundenportal an und beschreibt, was er verkauft. WEBklar prüft die Anfrage, vor allem nach Nr. 3, und gibt sie frei oder lehnt sie mit einer Begründung ab. Ein Anspruch auf Freigabe besteht nicht. Erst nach der Freigabe kann der Händler den Webshop einschalten.
|
||||
|
||||
(2) Die Einrichtung umfasst den Einbau eines Shop-Widgets an einer Stelle der Website des Händlers, die Seite des Webshops mit einem Verweis im Menü und den Link „Vertrag widerrufen“ in der Fußzeile der Seiten der Website, außerdem die Freischaltung durch WEBklar. Für jede Website wird ein Webshop eingerichtet. Zusätzlich steht dem Händler eine eigenständige Seite des Webshops bei WEBklar zur Verfügung (Standalone-Seite), etwa als Vorschau.
|
||||
|
||||
(3) Im Kundenportal verwaltet der Händler:
|
||||
- Artikel mit Beschreibung, Bildern, Preis, Varianten, Bestand und den Pflichtangaben zum Artikel,
|
||||
- Versand (nur nach Deutschland und Österreich) und Abholung mit Versandkosten und Lieferzeiten,
|
||||
- Bestellungen mit Status (zum Beispiel bezahlt, versendet, abholbereit, abgeschlossen), die er als CSV-Datei exportieren kann,
|
||||
- Erstattungen, ganz oder teilweise, über sein Stripe-Konto (Nr. 4),
|
||||
- die Widerrufe, die Käufer über die Widerrufsfunktion erklärt haben,
|
||||
- die Verbindung mit seinem Stripe-Konto, seine Einstellungen und seine Rechtstexte, deren bestätigte Fassungen WEBklar archiviert (Nr. 6 Abs. 5).
|
||||
|
||||
(4) Der Webshop enthält die Widerrufsfunktion nach § 356a BGB: Das Widget zeigt in seiner Fußzeile und die Standalone-Seite zeigt unter einer eigenen Adresse die Schaltfläche „Vertrag widerrufen“. Sie führt zu einem Formular und danach zur Bestätigungsschaltfläche „Widerruf bestätigen“. WEBklar speichert den Eingang, sendet dem Erklärenden sofort eine Eingangsbestätigung mit Inhalt, Datum und Uhrzeit und benachrichtigt den Händler. Die Funktion ist ohne Anmeldung erreichbar, auch wenn zur Erklärung keine Bestellung gefunden wird. Der Händler bearbeitet jeden Widerruf selbst (Nr. 5 Abs. 6).
|
||||
|
||||
(5) Der Webshop versendet im Namen des Händlers E-Mails an die Käufer: Eingang der Bestellung (bei einer Zahlung, die noch aussteht, etwa einer SEPA-Lastschrift, vor dem Vertragsschluss), Bestellbestätigung, Versand, Abholbereitschaft, Eingangsbestätigung eines Widerrufs, Erstattung und Hinweis auf eine fehlgeschlagene Zahlung. Absender ist die Versandadresse von WEBklar mit dem Namen des Shops, als Antwortadresse dient die E-Mail-Adresse des Händlers. Den Händler benachrichtigt der Webshop über neue Bestellungen und eingegangene Widerrufe.
|
||||
|
||||
(6) Der Webshop enthält einen Missbrauchsschutz gegen automatisierte Bestellungen und Widerrufe, darunter eine Obergrenze von drei offenen Bestellungen je IP-Adresse in 30 Minuten. Dafür speichert WEBklar mit der offenen Bestellung nur einen Hashwert der IP-Adresse, nicht die IP-Adresse selbst; er wird gelöscht, sobald die Bestellung bezahlt oder storniert wird (Nummer 3 Abs. 4 des AVV Webshop). Eine Telefonnummer des Käufers wird nicht erhoben und nicht gespeichert. Die Löschfristen für Bestelldaten nennt Nummer 9 des AVV Webshop.
|
||||
|
||||
(7) Nicht enthalten sind der Verkauf an Verbraucher außerhalb von Deutschland und Österreich, Rechnungen (Nr. 5 Abs. 7), die Verwaltung mehrerer Lager, Rabattcodes, Marktplatzfunktionen, die Anbindung an Warenwirtschaft oder Versanddienstleister, Mehrsprachigkeit und die Zahlung per PayPal (Nr. 4 Abs. 5). Zusätzliche Wünsche, etwa ein Sonderdesign, ein zweiter Webshop oder eine größere Zahl von Artikeln als vorgesehen, gehören nicht zur Einrichtung nach Nr. 9. WEBklar führt sie nach Absprache nach Stunden aus (Nr. 9 Abs. 4).
|
||||
|
||||
## 3. Ausgeschlossene Waren und Leistungen
|
||||
|
||||
(1) Über den Webshop dürfen nicht verkauft werden:
|
||||
- alkoholische Getränke,
|
||||
- Tabakwaren, E-Zigaretten und Zubehör,
|
||||
- Arzneimittel und Medizinprodukte,
|
||||
- Waffen, Munition, Feuerwerk und Sprengstoffe,
|
||||
- Waren mit erotischem oder pornografischem Inhalt,
|
||||
- lebende Tiere,
|
||||
- digitale Inhalte und Downloads,
|
||||
- Gutscheine, Geschenkkarten und Guthaben,
|
||||
- Abonnements und andere wiederkehrende Zahlungen,
|
||||
- Waren und Leistungen, die nach der Liste der verbotenen und eingeschränkten Geschäfte von Stripe nicht erlaubt sind, und
|
||||
- Waren, deren Verkauf oder Versand nach deutschem, österreichischem oder europäischem Recht verboten ist.
|
||||
|
||||
(2) Der Händler liefert nur nach Deutschland und Österreich. Eine Lieferung in andere Länder bietet der Webshop nicht an.
|
||||
|
||||
(3) Der Händler bestätigt bei der Anfrage und beim Einschalten, dass er keine dieser Waren und Leistungen verkauft. WEBklar darf eine Anfrage ablehnen, wenn der Händler Waren nach Absatz 1 verkaufen will oder die Angaben dazu nicht ausreichen. Verkauft der Händler später Waren nach Absatz 1, darf WEBklar den Webshop nach einem Hinweis in Textform abschalten; bei einem Verstoß, der Rechte Dritter oder Käufer gefährdet, darf WEBklar ihn auch ohne vorherigen Hinweis abschalten und informiert den Händler unverzüglich. Für das Abschalten gilt Nr. 10 Abs. 2 und 3. Die bereits auf das Stundenkontingent angerechnete Einrichtungszeit wird daraus nicht gutgeschrieben, wenn der Händler den Verstoß zu vertreten hat.
|
||||
|
||||
## 4. Zahlungen über Stripe
|
||||
|
||||
(1) Zahlungen im Webshop laufen über Stripe Connect. Der Händler verbindet dazu sein eigenes Stripe-Konto mit dem Webshop. Den Vertrag über dieses Konto schließt der Händler unmittelbar mit Stripe; WEBklar ist daran nicht beteiligt. Für das Konto, die Prüfung des Händlers durch Stripe (zum Beispiel zur Identität) und die Auszahlungen gelten die Bedingungen von Stripe.
|
||||
|
||||
(2) Das Geld der Käufer fließt direkt an den Händler. WEBklar erhält und verwahrt kein Geld der Käufer, leitet keines weiter und beteiligt sich nicht am Umsatz des Händlers. WEBklar ist kein Zahlungsdienstleister. Die Gebühren von Stripe trägt der Händler; sie fallen nur zwischen dem Händler und Stripe an.
|
||||
|
||||
(3) Der Händler prüft selbst, ob sein Stripe-Konto Zahlungen annehmen und Auszahlungen leisten kann. WEBklar zeigt dem Händler im Kundenportal den Stand, den Stripe meldet, hat aber keinen Einfluss darauf, ob und wann Stripe ein Konto freischaltet, einschränkt oder sperrt, und haftet dafür nicht. Rückbuchungen, Streitfälle mit Käufern und Zahlungsausfälle regeln der Händler und Stripe.
|
||||
|
||||
(4) Der Händler erstattet Zahlungen im Kundenportal oder im Stripe-Dashboard. WEBklar trägt Erstattungen aus dem Stripe-Dashboard in die Bestellung nach. Eine Erstattung führt der Händler rechtzeitig aus (Nr. 5 Abs. 6).
|
||||
|
||||
(5) Der Webshop bietet die Zahlungsarten an, die Stripe im Checkout für das Konto des Händlers bereitstellt und der Händler in seinen Einstellungen wählt, zum Beispiel Karte und SEPA-Lastschrift. PayPal ist über Stripe für diese Art der Anbindung nicht verfügbar und deshalb nicht Teil des Webshops.
|
||||
|
||||
(6) Die Zahlungsseite selbst (Stripe Checkout) betreibt Stripe. Kartendaten und Kontodaten gibt der Käufer nur dort ein; WEBklar erhält sie nicht.
|
||||
|
||||
## 5. Pflichten des Händlers
|
||||
|
||||
(1) Der Händler hat ein laufendes Wartungs-Abo, ist Unternehmer und stellt sein eigenes Stripe-Konto bereit; die Anmeldung bei Stripe schließt er selbst ab. Der Webshop steht nur Kunden zur Verfügung, deren Kundenart „Betrieb“ ist. Der Händler verkauft in eigenem Namen. Für seine Produkte, Preise, Produktangaben und Rechtstexte (Allgemeine Geschäftsbedingungen, Widerrufsbelehrung, Impressum, Datenschutzerklärung) ist er selbst verantwortlich; WEBklar stellt Muster bereit und berät nicht rechtlich. Er verkauft keine der in Nr. 3 ausgeschlossenen Waren. Die Zahlungen seiner Käufer laufen über sein eigenes Stripe-Konto zu den Bedingungen von Stripe; WEBklar erhält und verwahrt kein Geld seiner Käufer und erhebt keine Umsatzbeteiligung.
|
||||
|
||||
(2) Er verantwortet seine Rechtstexte: Impressum, Datenschutzerklärung, Allgemeine Geschäftsbedingungen, Widerrufsbelehrung, Muster-Widerrufsformular sowie die Angaben zu Versand und Zahlung. Die Mustertexte nach Nr. 6 prüft er, passt sie an seinen Betrieb an und bestätigt sie im Kundenportal, bevor er den Webshop öffnet. Er trägt vor dem Einschalten die Adressen von Impressum und Datenschutzerklärung seiner Website ein.
|
||||
|
||||
(3) Er verantwortet seine Artikel, Preise und Produktangaben. Dazu gehören richtige Preise einschließlich Steuerausweis (Umsatzsteuer oder Hinweis auf § 19 UStG), Grundpreise, Versandkosten und Lieferzeiten, die Pflichtangaben für Lebensmittel (zum Beispiel Zutaten, Allergene, Füllmenge, verantwortlicher Lebensmittelunternehmer), die Angaben zur Produktsicherheit (zum Beispiel Hersteller, Warn- und Sicherheitshinweise), Widerrufsausschlüsse bei einzelnen Artikeln und die Rechte an seinen Texten und Bildern.
|
||||
|
||||
(4) Er liefert die Waren, wie bestellt und beschrieben, und trägt alle Pflichten eines Verkäufers gegenüber seinen Käufern, auch Gewährleistung, Kundenservice und Beschwerden.
|
||||
|
||||
(5) Er pflegt Bestand, Bestellstatus und Versandstatus im Kundenportal. Die Folgen falscher Angaben oder Statusmeldungen, die der Händler setzt, trägt er.
|
||||
|
||||
(6) Er bearbeitet Widerrufe und Erstattungen selbst, fristgerecht und nach dem Gesetz. Er entfernt den Link „Vertrag widerrufen“ nicht und verändert die Widerrufsfunktion nicht. Er prüft das Kundenportal regelmäßig auf neue Bestellungen und Widerrufe und lässt sich über die Benachrichtigungen informieren.
|
||||
|
||||
(7) Rechnungen und Belege an die Käufer erstellt der Händler selbst; WEBklar stellt sie nicht aus. Die Quittung von Stripe und die Bestellbestätigung des Webshops ersetzen keine Rechnung nach dem Umsatzsteuergesetz, soweit der Händler eine ausstellen muss.
|
||||
|
||||
(8) Er erfüllt seine Aufbewahrungspflichten selbst, etwa nach der Abgabenordnung und dem Handelsgesetzbuch. WEBklar ist kein Archiv für die Buchführung und kein Buchführungssystem (das Archiv der Rechtstexte nach Nr. 6 Abs. 5 dient nur dem Nachweis der Texte): Personenbezogene Käuferdaten werden nach Nummer 9 des AVV Webshop anonymisiert und nach dem Ende des Webshops gelöscht. Der Händler exportiert seine Bestellungen deshalb rechtzeitig als CSV-Datei und bewahrt sie mit den Belegen von Stripe auf.
|
||||
|
||||
(9) Er hält die Vorgaben nach Nr. 3 ein und verändert den Einbau-Code und die gesetzlich vorgeschriebenen Hinweise des Widgets nicht.
|
||||
|
||||
(10) Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar beim Einbau dort den Datenschutz-Absatz für den Webshop ein; das gehört zur Einrichtung. Liegt sie woanders, erhält der Händler den Absatz als Textbaustein und fügt ihn selbst ein. Für den übrigen Inhalt seiner Datenschutzerklärung bleibt er verantwortlich (§ 6 Abs. 3 der Abo-Bedingungen). Fällt WEBklar beim Einfügen ein offensichtlicher Widerspruch auf, weist WEBklar den Händler darauf hin; eine Pflicht zur Prüfung der Datenschutzerklärung entsteht daraus nicht.
|
||||
|
||||
(11) Der Händler informiert seine Käufer nach Art. 13 DSGVO. Die Rollen im Datenschutz regelt der AVV Webshop: Der Händler ist Verantwortlicher, WEBklar Auftragsverarbeiter, Stripe Vertragspartner des Händlers.
|
||||
|
||||
## 6. Muster-Rechtstexte, keine Rechtsberatung
|
||||
|
||||
(1) WEBklar stellt dem Händler Mustertexte bereit: eine Muster-Widerrufsbelehrung, ein Muster-Widerrufsformular, Allgemeine Geschäftsbedingungen und den Text zu Versand und Zahlung. Das Widget zeigt außerdem die Hinweise, die für die Kasse und die Bestellbestätigung vorgesehen sind, darunter den Hinweis auf die gesetzliche Gewährleistung.
|
||||
|
||||
(2) Die Mustertexte sind **Muster ohne Gewähr**. Sie passen zur Standardkonfiguration des Webshops und können auf den Betrieb des Händlers nicht zugeschnitten sein. WEBklar sorgt dafür, dass sie die Technik des Webshops richtig beschreiben, und passt sie an, wenn sich die Technik ändert. Eine rechtliche Beratung im Einzelfall schuldet WEBklar nicht (kein Rechtsrat im Sinne des Rechtsdienstleistungsgesetzes).
|
||||
|
||||
(3) WEBklar empfiehlt, die Rechtstexte vor der Öffnung des Webshops durch eine Rechtsanwältin oder einen Rechtsanwalt prüfen zu lassen, besonders bei besonderen Waren (zum Beispiel Lebensmitteln, Kosmetik, Textilien mit Pflegekennzeichnung, Elektrogeräten).
|
||||
|
||||
(4) Der Händler bleibt für seine Rechtstexte verantwortlich, auch wenn er Mustertexte unverändert übernimmt (Nr. 5 Abs. 2).
|
||||
|
||||
(5) Bestätigt der Händler seine Rechtstexte im Kundenportal, legt WEBklar jede bestätigte Fassung mit Prüfsumme, Zeitpunkt und der bestätigenden Person im Archiv ab. Die Bestellbestätigung zeigt dem Käufer die archivierte Fassung, die zum Zeitpunkt seiner Bestellung galt. WEBklar löscht das Archiv mit den übrigen Daten des Webshops (Nr. 10 Abs. 3; Nummer 8 Abs. 5 des AVV Webshop). Der Händler bewahrt die Fassungen, die er für eigene Aufbewahrungspflichten braucht, selbst auf (Nr. 5 Abs. 8).
|
||||
|
||||
## 7. Verfügbarkeit und Störungen
|
||||
|
||||
(1) Das Verfügbarkeitsziel nach § 5 der Abo-Bedingungen gilt für die Website, nicht für den Webshop. WEBklar sagt für den Webshop keine bestimmte Verfügbarkeit zu, die über den Betrieb der Website im Abo hinausgeht. Störungen des Webshops beheben wir in angemessener Frist (§ 12 der Abo-Bedingungen).
|
||||
|
||||
(2) Angekündigte Wartungsfenster (§ 5 der Abo-Bedingungen) können den Webshop kurzzeitig unterbrechen. Während einer Unterbrechung sind Bestellungen im Widget nicht möglich; das Widget zeigt dann einen Ausweichtext.
|
||||
|
||||
(3) Zahlungen übernimmt der Webshop über Benachrichtigungen von Stripe und einen regelmäßigen Abgleich in die Bestellung. Fällt die Verbindung zu Stripe oder zum Server aus, kann sich die Bestätigung einer Bestellung verzögern. Ausfälle von Stripe, von Zahlungsarten und von Banken liegen nicht im Einflussbereich von WEBklar.
|
||||
|
||||
(4) Alle Zeiten gelten in der Zeitzone Europe/Berlin.
|
||||
|
||||
(5) WEBklar übernimmt keine Gewähr dafür, dass E-Mails zugestellt und gelesen werden. Das hängt vom Postfach des Empfängers und von den Mailgrenzen seines Anbieters ab.
|
||||
|
||||
(6) Die gesetzlichen Rechte des Händlers sowie §§ 12 und 13 der Abo-Bedingungen bleiben unberührt.
|
||||
|
||||
## 8. Haftung
|
||||
|
||||
(1) Der Webshop ist ein Hilfsmittel für den Verkauf des Händlers. Der Vertrag über die Ware besteht allein zwischen dem Händler und dem Käufer; WEBklar ist daran nicht beteiligt.
|
||||
|
||||
(2) Für entgangenen Gewinn und Folgeschäden aus Störungen des Webshops, aus Fehlern bei Bestand oder Bestellstatus und aus nicht zugestellten E-Mails haftet WEBklar nur im Rahmen von § 13 Abs. 1 und 2 der Abo-Bedingungen.
|
||||
|
||||
(3) WEBklar haftet nicht für Inhalte des Händlers, insbesondere nicht für seine Artikel, Preise, Produktangaben und Rechtstexte, auch nicht für Mustertexte, die er übernommen hat (Nr. 6), und nicht für die Erfüllung seiner Pflichten gegenüber den Käufern (Nr. 5).
|
||||
|
||||
(4) WEBklar haftet nicht für Ausfälle, Sperren oder Entscheidungen von Stripe und der Zahlungsarten sowie für Rückbuchungen und Zahlungsausfälle (Nr. 4 Abs. 3).
|
||||
|
||||
(5) Die Haftung nach § 13 Abs. 1 der Abo-Bedingungen (unter anderem bei Vorsatz, grober Fahrlässigkeit, Verletzung von Leben, Körper oder Gesundheit und nach dem Produkthaftungsgesetz) bleibt unberührt. Der Haftungsrahmen des Händlers gegenüber seinen Käufern bleibt unberührt.
|
||||
|
||||
## 9. Einrichtung, Abrechnung und Widerruf
|
||||
|
||||
(1) Die Einrichtung der Funktion rechnet WEBklar als Arbeitszeit auf das Stundenkontingent des Wartungs-Abos an (§ 4 der Abo-Bedingungen). Reicht das verfügbare Kontingent nicht aus, führt WEBklar die Einrichtung trotzdem aus. Die fehlende Zeit ist ein Vorschuss, den die folgenden Monatskontingente ausgleichen. Eine zusätzliche Rechnung gibt es dafür nicht. Endet das Wartungs-Abo, solange noch ein Vorschuss offen ist, verfällt der Rest zugunsten des Händlers. Für den Betrieb des Webshops berechnet WEBklar nichts weiter; es gibt keine laufende Gebühr und keine Beteiligung am Umsatz.
|
||||
|
||||
(2) Die Einrichtung dauert nach der Erfahrung von WEBklar meist 2 bis 3 Wochen ab dem Einschalten. Sie hängt davon ab, wie schnell der Händler Stripe-Konto, Artikel und Rechtstexte bereitstellt und wie umfangreich der Einbau ist. Die Dauer ist eine Einschätzung und keine verbindliche Zusage eines Termins.
|
||||
|
||||
(3) Hat WEBklar die Funktion auf Wunsch des Händlers schon bei der Erstellung seiner Website eingebaut, richtet sich die Abrechnung der Einrichtung nach der Vereinbarung zum Projekt und nicht nach dem Stundenkontingent. Auch dann läuft die Funktion nur, solange das Wartungs-Abo läuft.
|
||||
|
||||
(4) Arbeiten, die nicht zur Einrichtung gehören, führt WEBklar nur nach Absprache vor der Ausführung und nach Freigabe des Händlers in Textform aus; es gilt § 4 der Abo-Bedingungen. Welche Wünsche nicht zur Einrichtung gehören, beschreibt Nr. 2 Abs. 7.
|
||||
|
||||
(5) Das Einschalten der Funktion ist Teil des Abos und kein eigener Vertrag. Für den Widerruf gilt die Widerrufsbelehrung der Abo-Bedingungen. Widerruft ein Verbraucher das Abo oder den Wechsel ins Wartungs-Abo, endet die Funktion. Hat der Händler die Funktion zuvor einzeln gekauft, bleibt die Widerrufsbelehrung maßgeblich, die er bei der Bestellung erhalten hat.
|
||||
|
||||
## 10. Laufzeit und Ende
|
||||
|
||||
(1) Die Funktion läuft, solange das Wartungs-Abo läuft. Endet es, auch durch einen Wechsel ins Hosting-Abo oder durch den Widerruf eines Wechsels, schaltet WEBklar die Funktion spätestens am folgenden Tag ab. WEBklar informiert den Händler per E-Mail.
|
||||
|
||||
(2) Mit dem Abschalten nimmt der Webshop keine neuen Bestellungen mehr an, und das Widget zeigt den Ausweichtext. Bestellungen, die noch nicht bezahlt sind, laufen aus. Die Bestellungen und Widerrufe bleiben im Kundenportal 90 Tage lesbar, und der Händler kann sie als CSV-Datei exportieren. Die Widerrufsfunktion bleibt bis zur Löschung nach Absatz 3 erreichbar, soweit das Widget noch eingebunden ist. Der Händler erfüllt seine Pflichten gegenüber den Käufern weiter (Nr. 5) und informiert sie, wenn er den Webshop nicht mehr anbietet.
|
||||
|
||||
(3) WEBklar löscht alle Daten des Webshops, also Artikel, Bilder, Bestellungen, Widerrufe, Einstellungen, Rechtstexte und das Archiv der bestätigten Fassungen, 90 Tage nach dem Abschalten. Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Angaben zur Einrichtung bestehen und enthält keine Daten von Käufern (Nummer 9 Abs. 6 des AVV Webshop).
|
||||
|
||||
(4) Das Stripe-Konto bleibt beim Händler. Das Ende des Webshops berührt weder das Konto noch die Auszahlungen noch den Vertrag mit Stripe. Der Händler kann die Verbindung zwischen dem Konto und dem Webshop im Stripe-Dashboard beenden.
|
||||
|
||||
(5) Beginnt innerhalb der 90 Tage wieder ein Wartungs-Abo, schaltet WEBklar den Webshop auf Wunsch des Händlers in Textform mit den vorhandenen Daten und Einstellungen ohne weitere Kosten wieder ein. Nach der Löschung ist eine neue Anfrage und eine neue Einrichtung nötig (Einrichtung nach Nr. 9).
|
||||
|
||||
(6) Endet das Wartungs-Abo, bevor WEBklar den Webshop freigeschaltet hat, bricht WEBklar die Einrichtung ab; ein Vorschuss nach Nr. 9 Abs. 1 verfällt.
|
||||
|
||||
(7) Auf Wunsch des Händlers in Textform schaltet WEBklar den Webshop jederzeit ab; für den Widerruf gilt Nr. 9 Abs. 5.
|
||||
|
||||
## 11. Änderungen und Einstellung
|
||||
|
||||
(1) WEBklar darf die Technik des Webshops verbessern, solange der Leistungsumfang nicht schlechter wird (§ 3 Abs. 7 der Abo-Bedingungen). Ändert sich dabei, welche Daten erhoben werden, gilt Nummer 12 des AVV Webshop.
|
||||
|
||||
(2) WEBklar darf den Webshop für eine Website mit einer Frist von vier Wochen beenden oder für alle Händler mit einer Frist von drei Monaten einstellen, wenn
|
||||
- ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form unzulässig macht oder nur mit wesentlich anderer Technik erlaubt und der Händler einer dafür nötigen neuen Fassung nicht zustimmt (AVV Webshop Nummer 12),
|
||||
- Stripe die Anbindung nach Nr. 4 beendet oder wesentlich ändert, so dass der Webshop in der bisherigen Form nicht weiterbetrieben werden kann, oder
|
||||
- eine Sicherheitslücke sich nicht anders beheben lässt.
|
||||
|
||||
Die Mitteilung erfolgt in Textform und nennt den Grund, die Merkmale der Änderung, den Zeitpunkt und bei Verbrauchern das Recht, den Vertrag nach § 327r Abs. 3 BGB zu beenden, mit seiner Frist.
|
||||
|
||||
(3) Ist der Weiterbetrieb aus einem Grund nach Absatz 2 schon vor Ablauf der Frist nicht zulässig, darf WEBklar den Webshop ohne Vorankündigung aussetzen, solange das nötig ist. WEBklar informiert den Händler darüber unverzüglich in Textform mit dem Grund und nimmt den Betrieb wieder auf, sobald das zulässig und technisch möglich ist.
|
||||
|
||||
(4) Eine Einstellung ohne einen Grund nach Absatz 2 ist ausgeschlossen. Gesetzliche Rechte von Verbrauchern bei Änderungen digitaler Produkte (§ 327r BGB) bleiben unberührt.
|
||||
|
||||
## 12. Hinweise
|
||||
|
||||
(1) **Keine Rechtsberatung.** Der Webshop und die Mustertexte sind technische Hilfsmittel, keine Rechtsberatung (Nr. 6). Die rechtliche Einordnung seiner Waren, seiner Preisangaben und seines Angebots trägt der Händler.
|
||||
|
||||
(2) **Barrierefreiheit.** Das Widget ist für die Bedienung mit Tastatur und Hilfstechniken gestaltet (Beschriftungen, Fokusführung, Fehlermeldungen als Live-Region). Eine Konformität nach WCAG 2.1 AA oder EN 301 549 sagt WEBklar nicht zu. Ob der Händler als Anbieter von Dienstleistungen im elektronischen Geschäftsverkehr unter das BFSG fällt (Ausnahme für Kleinstunternehmen, § 3 Abs. 3 BFSG), entscheidet und prüft der Händler.
|
||||
@@ -0,0 +1,109 @@
|
||||
# Leistungsbeschreibung Website-Analyse
|
||||
|
||||
**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen · Zusatzbedingungen zum Hosting-Abo und zum Wartungs-Abo
|
||||
|
||||
## 1. Geltung
|
||||
|
||||
(1) Funktion im Sinne dieser Leistungsbeschreibung ist die Website-Analyse. Diese Leistungsbeschreibung gilt, wenn der Kunde die Funktion im Kundenportal einschaltet und die dort angezeigte Zeile mit dem Klick auf „Einschalten“ bestätigt. Sie gilt für Funktionen, die ab ihrer Veröffentlichung eingeschaltet werden; hat der Kunde die Funktion zuvor einzeln gekauft, gilt für sie weiter die Leistungsbeschreibung, unter der er sie gekauft hat. Die Funktion ist in beiden Abos enthalten. Sie setzt ein laufendes Hosting-Abo oder Wartungs-Abo voraus. Sie wird zusammen mit dem Auftragsverarbeitungsvertrag Website-Analyse („AVV Website-Analyse“) vereinbart. Wie die Zustimmung abläuft, regelt die Vorbemerkung des AVV Website-Analyse.
|
||||
|
||||
(2) Im Übrigen gelten die Abo-Bedingungen. Bei Widersprüchen geht für die Website-Analyse diese Leistungsbeschreibung vor, in Datenschutzfragen der AVV Website-Analyse.
|
||||
|
||||
(3) Leistungsbeschreibung B Nr. 3 der Abo-Bedingungen („Rechtstexte … nicht enthalten“) gilt nicht für die Zusatzseite nach Nr. 2 Abs. 2 und den Hinweis in der Datenschutzerklärung nach Nr. 4 Abs. 4. Beides ist Teil der Website-Analyse.
|
||||
|
||||
(4) Das Einschalten der Website-Analyse ist Teil des Abos und kein eigener Vertrag. Für den Widerruf gilt die Widerrufsbelehrung der Abo-Bedingungen.
|
||||
|
||||
## 2. Leistung
|
||||
|
||||
(1) Die Website-Analyse misst, wie die öffentliche Website des Kunden genutzt wird. Sie ist im Hosting-Abo und im Wartungs-Abo ohne Aufpreis enthalten. Die Einrichtung der Website-Analyse ist für den Kunden kostenlos. WEBklar rechnet sie weder auf ein Stundenkontingent an noch berechnet WEBklar sie gesondert.
|
||||
|
||||
(2) Beim Veröffentlichen der Website fügt WEBklar Folgendes ein:
|
||||
- ein Messskript in jede HTML-Datei der Website,
|
||||
- ein Einwilligungsbanner am unteren Rand mit den gleich gestalteten Schaltflächen „Ablehnen“ und „Annehmen“ in der Akzentfarbe, die der Kunde beim Einschalten wählt (ist sie für weiße Schrift zu hell, nimmt das Banner Schwarz),
|
||||
- eine Schaltfläche „Datenschutz-Einstellungen“ unten rechts, über die Besucher ihre Wahl jederzeit ändern können (hat die Website ein eigenes Element mit dem Attribut `data-webklar-consent`, übernimmt dieses Element die Aufgabe),
|
||||
- eine Zusatzseite „/datenschutz-analyse.html“ mit den Datenschutzhinweisen zur Messung (Verantwortlicher: der Kunde).
|
||||
|
||||
(3) Gemessen wird:
|
||||
- **solange der Besucher keine Wahl getroffen hat und sein Browser kein Signal nach dem nächsten Spiegelstrich sendet:** Seitenaufrufe mit Pfad (ohne Suchparameter), Land (aus der IP-Adresse, die nicht gespeichert wird), Geräteklasse und ein Zähler dafür, dass das Banner angezeigt wurde;
|
||||
- **bei den Browser-Signalen „Global Privacy Control“ und „Do Not Track“:** nichts, und es erscheint kein Banner;
|
||||
- **nach „Ablehnen“:** nichts mehr, außer einmal dem Zähler „abgelehnt“;
|
||||
- **nach „Annehmen“ zusätzlich:** die Domain der Website, von der der Besucher kam, die Größenklasse des Browserfensters, eine täglich wechselnde pseudonyme Besucher-Kennung, Verweildauer, Scrolltiefe, Klickpositionen (Heatmap), die Art von Kontakt-Klicks (Telefon, E-Mail, WhatsApp, Link auf eine andere Website mit deren Domain) und ein Zähler „angenommen“.
|
||||
|
||||
Die einzelnen Datenarten nennt der AVV Website-Analyse in Nummer 3.
|
||||
|
||||
(4) Es werden keine Cookies gesetzt. Gespeichert wird auf dem Gerät des Besuchers nur seine Banner-Wahl im lokalen Speicher des Browsers, für 183 Tage.
|
||||
|
||||
(5) Die Auswertung steht im Kundenportal zur Verfügung: Kennzahlen und Diagramme für die letzten 7, 30 oder 90 Tage und eine Heatmap je Seite und Ansicht (Handy, Tablet, Computer) aus den bis zu 5.000 jüngsten Klicks der letzten 30 Tage. Die Daten liegen auf dem Server von WEBklar bei der Hetzner Online GmbH in Deutschland; sie werden nicht in Drittländer übermittelt. Einzelne Ereignisse und Klicks werden nach 30 Tagen gelöscht, Tageswerte nach 25 Monaten, alle Daten rund 60 Tage nach dem Ende der Website-Analyse (AVV Website-Analyse Nummer 9).
|
||||
|
||||
(6) Die Zahlen sind Näherungswerte. Besucher mit Blockern, abgeschaltetem JavaScript, Browser-Signal oder Ablehnung werden nur eingeschränkt oder gar nicht erfasst. Besucherzahlen, Herkunftsseiten, Verweildauer, Scrolltiefe, Klicks und Kontakt-Klicks stammen nur von Besuchern, die „Annehmen“ gewählt haben.
|
||||
|
||||
(7) Nicht enthalten sind Sitzungsaufzeichnungen, die Auswertung von Formularinhalten, die Messung von Ladezeiten und Fehlern, Kampagnen- und Trichteranalysen sowie ein Datenexport außer nach Nr. 7 Abs. 3.
|
||||
|
||||
(8) Das Banner fragt nur nach der Einwilligung in die Website-Analyse von WEBklar. Andere einwilligungspflichtige Inhalte der Website, etwa eingebettete Karten, Videos oder Schriften von Drittanbietern, deckt es nicht ab.
|
||||
|
||||
## 3. Voraussetzungen
|
||||
|
||||
(1) Der Kunde hat ein laufendes Hosting-Abo oder Wartungs-Abo.
|
||||
|
||||
(2) Die Website ist öffentlich erreichbar und wird bei WEBklar gehostet (Adresse unter project.webklar.com oder eigene Domain, die auf den Server von WEBklar zeigt). Nicht öffentliche Websites und Vorschauen, die nur nach einer Anmeldung erreichbar sind, werden nicht gemessen, ebenso wenig Websites, die im Dev-Modus auf einem Entwicklungsserver laufen. Enthält eine öffentliche Website Bereiche, die erst nach einer Anmeldung zugänglich sind (etwa eine Web-Anwendung mit Kundenkonto), werden auch dort Seitenaufrufe gezählt; dafür gilt Nr. 4 Abs. 5.
|
||||
|
||||
(3) Auf der Zusatzseite erscheinen als Verantwortlicher die Firma (sonst der Name), die Anschrift, soweit sie im Kundenkonto hinterlegt ist, und die E-Mail-Adresse aus dem Kundenkonto. Das Portal zeigt diese Angaben vor dem Einschalten an. Der Kunde prüft sie und teilt Änderungen mit. Fehlen Firma und Name oder fehlt eine gültige E-Mail-Adresse, lässt sich die Funktion nicht einschalten.
|
||||
|
||||
(4) Richtet sich die Website auch gezielt an Kinder unter 16 Jahren, schaltet der Kunde die Website-Analyse nicht ein, weil eine Einwilligung über das Banner dann nicht genügt (Art. 8 DSGVO). Lassen Inhalte oder Adressen der Website auf besondere Kategorien personenbezogener Daten schließen, etwa Gesundheit, Religion oder politische Meinung, teilt der Kunde das WEBklar vor dem Einschalten mit. WEBklar darf das Einschalten für eine solche Website ablehnen oder die Website-Analyse dort ausschalten.
|
||||
|
||||
## 4. Pflichten des Kunden
|
||||
|
||||
(1) Der Kunde ist für die Messung auf seiner Website Verantwortlicher im Sinne der DSGVO.
|
||||
|
||||
(2) Er entfernt, verdeckt oder umgeht das Banner nicht und verändert das Skript nicht. Text und Gestaltung des Banners legt WEBklar fest.
|
||||
|
||||
(3) Er bindet ohne Abstimmung mit WEBklar keine weiteren Mess-, Tracking- oder Marketing-Werkzeuge ein (zum Beispiel Google Analytics oder Meta Pixel), weil sie eine eigene Einwilligung erfordern können.
|
||||
|
||||
(4) Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar beim Einschalten dort einen kurzen Hinweis mit Link auf die Zusatzseite ein und entfernt ihn beim Ausschalten wieder; das wird nicht auf das Stundenkontingent angerechnet. Liegt sie woanders, etwa bei einem externen Generator, erhält der Kunde den Hinweis als Textbaustein und fügt ihn selbst ein und entfernt ihn nach dem Ausschalten selbst. Für den übrigen Inhalt seiner Datenschutzerklärung bleibt der Kunde verantwortlich (§ 6 Abs. 3 der Abo-Bedingungen). Fällt WEBklar beim Einfügen ein offensichtlicher Widerspruch auf (etwa „Wir setzen keine Analyse-Werkzeuge ein“), weist WEBklar den Kunden darauf hin; eine Pflicht zur Prüfung der Datenschutzerklärung entsteht daraus nicht.
|
||||
|
||||
(5) Enthalten die Pfade seiner Website personenbezogene Angaben, etwa Namen, Kundennummern oder E-Mail-Adressen, teilt der Kunde das WEBklar vor dem Einschalten mit.
|
||||
|
||||
(6) Er nutzt die Auswertung nur für eigene Zwecke, versucht nicht, einzelne Besucher zu identifizieren, und gibt Heatmap-Links nicht weiter.
|
||||
|
||||
## 5. Verfügbarkeit und Störungen
|
||||
|
||||
(1) Das Verfügbarkeitsziel nach § 5 der Abo-Bedingungen gilt für die Website, nicht für die Messung. Störungen der Messung beheben wir in angemessener Frist (§ 12 der Abo-Bedingungen). Daten, die während einer Störung nicht erfasst wurden, lassen sich nicht nachträglich erheben.
|
||||
|
||||
(2) Fällt der Messdienst aus, wird die Website weiter ausgeliefert, nur ohne Banner und Messung.
|
||||
|
||||
(3) Die gesetzlichen Rechte des Kunden sowie §§ 12 und 13 der Abo-Bedingungen bleiben unberührt.
|
||||
|
||||
## 6. Hinweis zur Rechtslage
|
||||
|
||||
Für die Zählung, solange der Besucher keine Wahl getroffen hat (Nr. 2 Abs. 3), übermittelt das Skript nur die Adresse der aufgerufenen Seite und die Angabe, dass das Banner angezeigt wurde. Land und Geräteklasse bestimmt der Messdienst aus den Daten, die jeder Browser bei einer Anfrage ohnehin mitsendet. Ob auch dafür eine Einwilligung nach § 25 TDDDG nötig ist, haben Gerichte und Aufsichtsbehörden nicht entschieden; die Aufsichtsbehörden legen den Begriff des Zugriffs auf das Endgerät eher weit aus. Ändert sich die Rechtslage, passt WEBklar die Messung an (Nr. 8).
|
||||
|
||||
## 7. Ende der Leistung
|
||||
|
||||
(1) Der Kunde kann die Website-Analyse jederzeit im Kundenportal ausschalten; das Portal bestätigt die Abschaltung. Die Website-Analyse läuft, solange das Abo läuft, das sie enthält. Endet es, schaltet WEBklar sie spätestens am folgenden Tag ab. Wechselt der Kunde vom Wartungs- ins Hosting-Abo, bleibt die Website-Analyse; das Gleiche gilt, wenn er den Wechsel ins Wartungs-Abo widerruft. WEBklar informiert den Kunden per E-Mail.
|
||||
|
||||
(2) Mit dem Ausschalten speichert der Messdienst keine Messdaten mehr, und die Heatmap ist nicht mehr abrufbar. Das Messskript liefert dann kein Banner mehr aus, sobald die Website einmal mit der aktuellen Fassung des Skripts veröffentlicht wurde. Skript, Zusatzseite und, soweit WEBklar ihn eingefügt hat, den Hinweis in der Datenschutzerklärung entfernt WEBklar mit der nächsten Veröffentlichung der Website. WEBklar löst sie in der Regel sofort aus, spätestens innerhalb von fünf Werktagen.
|
||||
|
||||
(3) Die gespeicherten Daten löscht WEBklar nach Nummer 9 des AVV Website-Analyse. Bis zum 46. Tag nach dem Ende kann der Kunde eine Kopie der Tageswerte und der noch vorhandenen Einzelereignisse als CSV-Datei verlangen.
|
||||
|
||||
## 8. Änderungen und Einstellung
|
||||
|
||||
(1) WEBklar darf die Technik der Website-Analyse verbessern, wenn der Leistungsumfang nicht schlechter wird (§ 3 Abs. 7 der Abo-Bedingungen). Ändert sich dabei, welche Daten erhoben werden, gilt Nummer 12 des AVV Website-Analyse.
|
||||
|
||||
(2) WEBklar darf die Website-Analyse für eine Website mit einer Frist von vier Wochen beenden oder für alle Kunden mit einer Frist von drei Monaten einstellen, wenn
|
||||
- ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form unzulässig macht oder nur mit wesentlich anderer Technik erlaubt und der Kunde einer dafür nötigen neuen Fassung nicht zustimmt (AVV Website-Analyse Nummer 12 Abs. 3), oder
|
||||
- eine Sicherheitslücke sich nicht anders beheben lässt.
|
||||
|
||||
Die Mitteilung erfolgt in Textform und nennt den Grund, die Merkmale der Änderung, den Zeitpunkt und bei Verbrauchern das Recht, den Vertrag nach § 327r Abs. 3 BGB zu beenden, mit seiner Frist.
|
||||
|
||||
(3) Ist der Weiterbetrieb aus einem Grund nach Absatz 2 schon vor Ablauf der Frist nicht zulässig, darf WEBklar die Messung ohne Vorankündigung aussetzen, solange das nötig ist. WEBklar informiert den Kunden darüber unverzüglich in Textform mit dem Grund und nimmt die Messung wieder auf, sobald das zulässig und technisch möglich ist.
|
||||
|
||||
(4) Gegenüber Unternehmern darf WEBklar die Website-Analyse außerdem ohne besonderen Grund mit einer Frist von drei Monaten in Textform für alle Kunden einstellen. Der Kunde kann das Abo dann innerhalb von 30 Tagen nach der Mitteilung zum Zeitpunkt der Einstellung kündigen; die Mindestlaufzeit steht dem nicht entgegen.
|
||||
|
||||
(5) Weil für die Website-Analyse kein Aufpreis berechnet wird, ändert ihre Einstellung den Preis des Abos nicht. Gesetzliche Rechte von Verbrauchern bei Änderungen digitaler Produkte (§ 327r BGB) bleiben unberührt.
|
||||
|
||||
## 9. Hinweise
|
||||
|
||||
(1) **Keine Rechtsberatung.** Die Zusatzseite und der Hinweis in der Datenschutzerklärung sind Mustertexte für die Standardkonfiguration. WEBklar sorgt dafür, dass sie die Technik richtig beschreiben, und passt sie an, wenn sich die Technik ändert. Eine Prüfung, ob die Website des Kunden insgesamt datenschutzkonform ist, und eine rechtliche Beratung im Einzelfall schuldet WEBklar nicht. Abstimmungen nach dieser Leistungsbeschreibung betreffen nur die Technik.
|
||||
|
||||
(2) **Nachweis der Einwilligung.** Ein Protokoll je Besucher wird nicht geführt. Als Nachweis dienen die Dokumentation der Technik und das Archiv der Fassungen von Banner-Text und Zusatzseite (AVV Website-Analyse Nummer 8 Abs. 6).
|
||||
|
||||
(3) Die Länderzuordnung nutzt die Datenbank „IP to Country Lite“ von DB-IP.com (Lizenz CC BY 4.0). Im Kundenportal steht dazu unter der Auswertung der Hinweis „Länderdaten: IP-Geolocation by DB-IP“ mit Link auf db-ip.com.
|
||||
@@ -0,0 +1,135 @@
|
||||
# Leistungsbeschreibung Öffnungszeiten live
|
||||
|
||||
**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen · Zusatzbedingungen zum Wartungs-Abo
|
||||
|
||||
## 1. Geltung
|
||||
|
||||
(1) Funktion im Sinne dieser Leistungsbeschreibung ist Öffnungszeiten live. Diese Leistungsbeschreibung gilt, wenn der Kunde die Funktion im Kundenportal einschaltet und die dort angezeigte Zeile mit dem Klick auf „Einschalten“ bestätigt. Sie gilt für Funktionen, die ab ihrer Veröffentlichung eingeschaltet werden; hat der Kunde die Funktion zuvor einzeln gekauft, gilt für sie weiter die Leistungsbeschreibung, unter der er sie gekauft hat. Die Funktion ist Teil des Wartungs-Abos. Sie setzt ein laufendes Wartungs-Abo voraus; ein Hosting-Abo genügt nicht.
|
||||
|
||||
(2) Die Funktion ist im Wartungs-Abo ohne Aufpreis enthalten (§ 3 Abs. 8 der Abo-Bedingungen). Leistungsbeschreibung B Nr. 3 der Abo-Bedingungen („Nicht enthalten“: neue Funktionen und Rechtstexte) gilt dafür nicht: Die Funktion und der Datenschutz-Absatz nach Nr. 6 Abs. 4 sind Teil dieser Leistung.
|
||||
|
||||
(3) Im Übrigen gelten die Abo-Bedingungen. Bei Widersprüchen geht für Öffnungszeiten live diese Leistungsbeschreibung vor. Die Haftungsregeln stehen in Nr. 8 ausgeschrieben da, damit sie auch für Abos früherer Fassungen der Abo-Bedingungen gelten.
|
||||
|
||||
## 2. Leistung
|
||||
|
||||
(1) Der Kunde trägt seine Öffnungszeiten einmal im Kundenportal ein: bis zu fünf benannte Zeitpläne, je Wochentag bis zu zwei Zeitfenster („rund um die Uhr“ und „nach Vereinbarung“ sind möglich), Abweichungen an Feiertagen, Betriebsurlaub und Sondertage. Die Website zeigt diese Zeiten überall gleich an und berechnet daraus den Live-Status („Jetzt geöffnet · schließt um 18 Uhr“).
|
||||
|
||||
(2) An den Stellen der Website, die nach Nr. 3 Abs. 2 dafür vorgesehen sind, erscheinen die Zeiten als Tabelle, als Status oder als Kurzanzeige („Geöffnet“, „Geschlossen“, „Nach Vereinbarung“). Dazu gehören, soweit der Kunde sie einrichtet:
|
||||
- eine Hinweis-Leiste am Seitenanfang für befristete Hinweise (Info, Urlaub, Aktion, Wichtig) mit Beginn und Ende und auf Wunsch einem Knopf (Link oder Anruf),
|
||||
- auf dem Handy eine Kontakt-Leiste am unteren Rand mit den Knöpfen „Anrufen“, „Route“ und, wenn der Kunde sie einrichtet, „WhatsApp“,
|
||||
- die Öffnungszeiten im Seitencode in maschinenlesbarer Form (strukturierte Daten), die Suchmaschinen lesen können,
|
||||
- eine E-Mail vor Feiertagen, die den Kunden bittet, seine Zeiten für diese Tage zu prüfen, und
|
||||
- im Kundenportal der Knopf „Für Google kopieren“, der die Zeiten in der Reihenfolge von Google in die Zwischenablage legt, damit der Kunde sie in sein Google-Unternehmensprofil einträgt.
|
||||
|
||||
(3) Die Funktion rechnet die Feiertage des Bundeslands ein, das der Kunde wählt; je Feiertag kann der Kunde eigene Zeiten eintragen. Feiertage, die nur örtlich gelten, sowie Heiligabend und Silvester zeigt das Kundenportal als „bitte prüfen“. Alle Zeiten gelten in der Zeitzone Europe/Berlin.
|
||||
|
||||
(4) Beim Aufruf der Website lädt der Browser des Besuchers ein kleines Programm und die Öffnungszeiten von project.webklar.com, einem Server der WEBklar GbR bei der Hetzner Online GmbH in Falkenstein (Deutschland). Dabei werden keine Cookies gesetzt, und es wird nichts im Browser gespeichert. Das Programm lädt nichts von anderen Anbietern. WhatsApp und Google Maps werden erst nach dem Klick des Besuchers auf „WhatsApp“ oder „Route“ geöffnet; vorher werden keine Daten an diese Dienste übertragen.
|
||||
|
||||
(5) Das Team von WEBklar kann die Hinweis-Leiste und die Kontakt-Leiste für eine Website abschalten, etwa wenn die Seite schon eine eigene Anruf-Leiste hat. Auf Wunsch des Kunden können Mitarbeiter die Öffnungszeiten in der Kundenansicht für ihn ändern; ein Anspruch darauf besteht nicht (Nr. 5 Abs. 1). Den Haken für den WhatsApp-Hinweis (Nr. 6 Abs. 3) setzen sie nicht.
|
||||
|
||||
## 3. Einbau und Freischaltung
|
||||
|
||||
(1) Der Einbau des Programms erfolgt automatisch nur bei Websites, die WEBklar hostet und über den eigenen Veröffentlichungsablauf bereitstellt: Beim nächsten Veröffentlichen setzt WEBklar die Einbau-Zeile in jede Seite ein, und Hinweis-Leiste, Kontakt-Leiste und Angaben für Suchmaschinen erscheinen ohne weiteren Aufwand. In allen anderen Fällen, etwa bei einer Website bei einem anderen Anbieter, setzt das Team die Einbau-Zeile von Hand ein. Hat WEBklar keinen Zugriff auf die Website, erhält der Kunde die Zeile und setzt sie selbst oder durch seinen Anbieter ein.
|
||||
|
||||
(2) Die Tabelle, den Status und die Kurzanzeige setzt das Team an den Stellen ein, an denen auf der Website feste Öffnungszeiten stehen oder stehen sollen, und ersetzt dort die fest eingetragenen Zeiten. In jedem dieser Platzhalter bleibt eine statische Ersatz-Tabelle stehen. Sie erscheint, wenn der Browser kein JavaScript ausführt, wenn die Daten nicht erreichbar oder älter als 24 Stunden sind und nachdem die Funktion abgeschaltet wurde. Sie zeigt den Wochenplan zum Zeitpunkt des Einbaus.
|
||||
|
||||
(3) WEBklar schaltet die Funktion frei, sobald der Einbau abgeschlossen ist und der Kunde Öffnungszeiten eingetragen hat (mindestens ein geöffneter Tag im Hauptplan). Solange der Kunde keine Zeiten eingetragen hat, kann das Team die Tabelle nicht einbauen.
|
||||
|
||||
(4) Der Einbau dauert erfahrungsgemäß meist wenige Tage. Das ist eine Einschätzung und keine verbindliche Zusage eines Termins.
|
||||
|
||||
## 4. Einrichtung
|
||||
|
||||
(1) Die Einrichtung der Funktion rechnet WEBklar als Arbeitszeit auf das Stundenkontingent des Wartungs-Abos an (§ 4 der Abo-Bedingungen). Reicht das verfügbare Kontingent nicht aus, führt WEBklar die Einrichtung trotzdem aus. Die fehlende Zeit ist ein Vorschuss, den die folgenden Monatskontingente ausgleichen. Eine zusätzliche Rechnung gibt es dafür nicht. Endet das Wartungs-Abo, solange noch ein Vorschuss offen ist, verfällt der Rest zugunsten des Kunden.
|
||||
|
||||
(2) Hat WEBklar die Funktion auf Wunsch des Kunden schon bei der Erstellung seiner Website eingebaut, richtet sich die Abrechnung der Einrichtung nach der Vereinbarung zum Projekt und nicht nach dem Stundenkontingent. Auch dann läuft die Funktion nur, solange das Wartungs-Abo läuft.
|
||||
|
||||
(3) Im Umfang der Einrichtung ist ein Einbau je Website enthalten, einschließlich der Aktualisierung der Ersatz-Tabelle beim Abschalten (Nr. 9 Abs. 4). Weitere Wünsche, etwa zusätzliche Stellen für die Anzeige, eine Sondergestaltung oder andere Änderungen an der Website, gehören nicht zur Einrichtung. Arbeiten, die nicht zur Einrichtung gehören, führt WEBklar nur nach Absprache vor der Ausführung und nach Freigabe des Kunden in Textform aus; es gilt § 4 der Abo-Bedingungen.
|
||||
|
||||
## 5. Nicht enthalten
|
||||
|
||||
(1) Nicht enthalten sind:
|
||||
- die Übertragung der Öffnungszeiten zu Google: WEBklar schreibt nichts automatisch in das Google-Unternehmensprofil des Kunden; der Kunde trägt die Zeiten dort selbst ein, der Knopf „Für Google kopieren“ und die Erinnerung vor Feiertagen helfen dabei,
|
||||
- Feiertage einer Gemeinde und andere örtliche Besonderheiten (zum Beispiel Brückentage oder Stadtfeste); der Kunde trägt sie als Ausnahmen selbst ein,
|
||||
- eine Zusage, dass Suchmaschinen die Angaben im Seitencode übernehmen oder wie sie sie anzeigen,
|
||||
- die Pflege der Zeiten und der Hinweise durch WEBklar und
|
||||
- Änderungen an der Website, die über den Einbau nach Nr. 3 hinausgehen (Nr. 4 Abs. 3).
|
||||
|
||||
## 6. Pflichten des Kunden
|
||||
|
||||
(1) Der Kunde prüft seine Zeiten, Feiertage und Hinweistexte vor dem Speichern und hält sie aktuell. Er wählt das richtige Bundesland und trägt örtliche Feiertage und Abweichungen selbst ein.
|
||||
|
||||
(2) Preise und Rabatte in Hinweisen gibt er rechtmäßig an, zum Beispiel als Endpreise und, bei Preisermäßigungen für Waren, mit dem niedrigsten Preis der letzten 30 Tage. Hinweise zu Preisen sind Angaben des Kunden, nicht von WEBklar.
|
||||
|
||||
(3) Die Kontaktangaben (Telefonnummer, Anschrift, WhatsApp-Nummer) sind auf der Website öffentlich sichtbar. Der Kunde trägt nur Angaben ein, die er veröffentlichen darf. Die WhatsApp-Schaltfläche schaltet er erst ein, nachdem er den WhatsApp-Hinweis in seine Datenschutzerklärung aufgenommen hat, und bestätigt das im Kundenportal mit einem Haken.
|
||||
|
||||
(4) Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar beim Einbau dort den Datenschutz-Absatz zu Öffnungszeiten live ein; das gehört zum Einbau. Liegt sie woanders, etwa bei einem externen Generator, erhält der Kunde den Absatz als Textbaustein und fügt ihn selbst ein. Für den übrigen Inhalt seiner Datenschutzerklärung bleibt der Kunde verantwortlich (§ 6 Abs. 3 der Abo-Bedingungen). Fällt WEBklar beim Einfügen ein offensichtlicher Widerspruch auf, weist WEBklar den Kunden darauf hin; eine Pflicht zur Prüfung der Datenschutzerklärung entsteht daraus nicht.
|
||||
|
||||
(5) Der Kunde prüft nach dem Einbau, ob die Anzeige auf seiner Website stimmt, und meldet Fehler unverzüglich im Chat des Kundenportals.
|
||||
|
||||
(6) Er entfernt oder verändert das Programm von WEBklar nicht. Wünscht er die Hinweis-Leiste oder die Kontakt-Leiste nicht, teilt er das dem Team mit (Nr. 2 Abs. 5).
|
||||
|
||||
## 7. Verfügbarkeit und Aktualität
|
||||
|
||||
(1) Das Verfügbarkeitsziel nach § 5 der Abo-Bedingungen gilt für die Website, nicht für die Live-Anzeige. Störungen der Anzeige behebt WEBklar in angemessener Frist.
|
||||
|
||||
(2) Ist die Anzeige nicht erreichbar oder sind die Daten älter als 24 Stunden, bleibt auf der Website die Ersatz-Tabelle stehen (Nr. 3 Abs. 2); Hinweis-Leiste und Kontakt-Leiste erscheinen dann nicht.
|
||||
|
||||
(3) Änderungen, die der Kunde im Kundenportal speichert, erscheinen nicht sofort auf der Website, sondern mit einer Verzögerung von einigen Minuten (bis zu etwa zehn Minuten). Der Status folgt der Zeitzone Europe/Berlin; geht die Uhr des Geräts des Besuchers um mehr als fünf Minuten falsch, rechnet das Programm mit der Serverzeit von WEBklar.
|
||||
|
||||
## 8. Haftung
|
||||
|
||||
(1) Die Live-Anzeige ist ein Hilfsmittel. Die Richtigkeit der Zeiten, Ausnahmen, Hinweise und Kontaktangaben liegt beim Kunden: WEBklar zeigt die Angaben des Kunden an und prüft sie nicht auf Richtigkeit.
|
||||
|
||||
(2) Die Feiertage stammen aus einer Feiertagsliste für das gewählte Bundesland. WEBklar beseitigt Fehler in den Feiertagsdaten und andere Softwarefehler nach Meldung in angemessener Frist (Nr. 7 Abs. 1). Für Schäden daraus gelten die Absätze 3 bis 5.
|
||||
|
||||
(3) Für entgangenen Gewinn und Folgeschäden, etwa weil ein Besucher wegen einer falschen oder fehlenden Angabe vor verschlossener Tür steht, haftet WEBklar nur im Rahmen der Absätze 4 und 5.
|
||||
|
||||
(4) WEBklar haftet unbeschränkt bei Vorsatz und grober Fahrlässigkeit, bei Verletzung von Leben, Körper oder Gesundheit, nach dem Produkthaftungsgesetz und soweit WEBklar eine Garantie übernommen hat.
|
||||
|
||||
(5) Bei leicht fahrlässiger Verletzung einer wesentlichen Vertragspflicht haftet WEBklar nur für den vertragstypischen, vorhersehbaren Schaden. Wesentlich sind Pflichten, deren Erfüllung die ordnungsgemäße Durchführung des Vertrags überhaupt erst ermöglicht und auf deren Einhaltung der Kunde regelmäßig vertrauen darf. Im Übrigen ist die Haftung für leichte Fahrlässigkeit ausgeschlossen.
|
||||
|
||||
(6) Diese Regeln gelten auch zugunsten der Mitarbeiter und Beauftragten von WEBklar. Dieser Abschnitt entspricht § 13 Abs. 1 und 2 der Abo-Bedingungen und gilt unabhängig davon, nach welcher Fassung der Abo-Bedingungen das Wartungs-Abo besteht.
|
||||
|
||||
(7) Gesetzliche Rechte des Kunden, die sich nicht ausschließen lassen, bleiben unberührt. Der Haftungsrahmen des Kunden gegenüber seinen Besuchern und Kunden bleibt ebenfalls unberührt.
|
||||
|
||||
## 9. Ende der Leistung
|
||||
|
||||
(1) Auf Wunsch des Kunden in Textform (im Chat des Kundenportals oder per E-Mail) schaltet WEBklar Öffnungszeiten live jederzeit ab; für den Widerruf gilt Nr. 11.
|
||||
|
||||
(2) Die Funktion läuft, solange das Wartungs-Abo läuft. Endet es, auch durch einen Wechsel ins Hosting-Abo, läuft die Funktion noch 30 Tage unverändert weiter; danach schaltet WEBklar sie ab. Das Gleiche gilt beim Widerruf eines Wechsels ins Wartungs-Abo. WEBklar informiert den Kunden per E-Mail. Die Eingaben des Kunden bleiben nach dem Abschalten 30 Tage gespeichert und werden danach gelöscht; beginnt schon in den 30 Tagen Weiterlauf wieder ein Wartungs-Abo, läuft die Funktion einfach weiter, beginnt es in den 30 Tagen danach, schaltet WEBklar die Funktion mit den vorhandenen Eingaben ohne weitere Kosten wieder ein.
|
||||
|
||||
(3) Sobald ein neues Wartungs-Abo beginnt, solange die Eingaben des Kunden noch gespeichert sind (Absatz 4), läuft die Funktion mit den vorhandenen Eingaben weiter oder WEBklar schaltet sie ohne weitere Kosten wieder ein.
|
||||
|
||||
(4) Mit dem Abschalten verschwinden Hinweis-Leiste, Kontakt-Leiste, Live-Status und die Angaben für Suchmaschinen; auf der Website steht wieder die Ersatz-Tabelle (Nr. 3 Abs. 2). Hat sich der Wochenplan seit dem Einbau geändert, aktualisiert WEBklar die Ersatz-Tabelle einmal beim Abschalten; das gehört zum Einbau nach Nr. 4 Abs. 3. Hat WEBklar keinen Zugriff auf die Website, erhält der Kunde den aktuellen Text und setzt ihn selbst ein. Nach dem Abschalten wegen des Endes des Abos bleiben die Eingaben des Kunden noch weitere 30 Tage gespeichert und werden danach gelöscht; bis dahin kann der Kunde sie im Kundenportal ansehen.
|
||||
|
||||
(5) Schaltet WEBklar die Funktion auf Wunsch des Kunden oder aus einem anderen Grund als dem Ende des Abos ab, löscht WEBklar die Eingaben des Kunden auf sein Verlangen in Textform, spätestens mit dem Löschen des Kundenkontos.
|
||||
|
||||
## 10. Änderungen und Einstellung
|
||||
|
||||
(1) WEBklar darf die Technik von Öffnungszeiten live verbessern, wenn der Leistungsumfang nicht schlechter wird (§ 3 Abs. 7 der Abo-Bedingungen). Ändert sich dabei, welche Daten die Funktion verarbeitet, passt WEBklar die Datenschutzerklärung an und informiert den Kunden, weil sich dann sein Datenschutz-Absatz (Nr. 6 Abs. 4) ändern kann.
|
||||
|
||||
(2) WEBklar darf die Funktion für eine Website mit einer Frist von vier Wochen beenden oder für alle Kunden mit einer Frist von drei Monaten einstellen, wenn
|
||||
- ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form unzulässig macht oder nur mit wesentlich anderer Technik erlaubt und der Kunde einer dafür nötigen neuen Fassung dieser Leistungsbeschreibung nicht zustimmt, oder
|
||||
- eine Sicherheitslücke sich nicht anders beheben lässt.
|
||||
|
||||
Die Mitteilung erfolgt in Textform und nennt den Grund, die Merkmale der Änderung, den Zeitpunkt und bei Verbrauchern das Recht, den Vertrag nach § 327r Abs. 3 BGB zu beenden, mit seiner Frist.
|
||||
|
||||
(3) Ist der Weiterbetrieb aus einem Grund nach Absatz 2 schon vor Ablauf der Frist nicht zulässig, darf WEBklar die Funktion ohne Vorankündigung aussetzen, solange das nötig ist. WEBklar informiert den Kunden darüber unverzüglich in Textform mit dem Grund und nimmt den Betrieb wieder auf, sobald das zulässig und technisch möglich ist.
|
||||
|
||||
(4) Ist der Kunde Unternehmer, darf WEBklar die Funktion außerdem ohne besonderen Grund mit einer Frist von drei Monaten in Textform für alle Kunden einstellen. Der Kunde kann das Wartungs-Abo dann innerhalb von 30 Tagen nach der Mitteilung zum Zeitpunkt der Einstellung kündigen; die Mindestlaufzeit steht dem nicht entgegen.
|
||||
|
||||
(5) Gesetzliche Rechte von Verbrauchern bei Änderungen digitaler Produkte (§ 327r BGB) bleiben unberührt.
|
||||
|
||||
## 11. Widerruf
|
||||
|
||||
(1) Das Einschalten der Funktion ist Teil des Abos und kein eigener Vertrag. Für den Widerruf gilt die Widerrufsbelehrung der Abo-Bedingungen. Widerruft ein Verbraucher das Abo oder den Wechsel ins Wartungs-Abo, endet die Funktion.
|
||||
|
||||
(2) Hat der Kunde die Funktion zuvor einzeln gekauft, bleibt die Widerrufsbelehrung maßgeblich, die er bei der Bestellung erhalten hat.
|
||||
|
||||
## 12. Hinweise
|
||||
|
||||
(1) **Keine Rechtsberatung.** Der Datenschutz-Absatz, der WhatsApp-Hinweis und die Hinweise des Kundenportals zu Preisangaben sind Mustertexte für die Standardkonfiguration. WEBklar sorgt dafür, dass sie die Technik richtig beschreiben, und passt sie an, wenn sich die Technik ändert. Eine rechtliche Beratung im Einzelfall, etwa zu Preisangaben, zum Wettbewerbsrecht oder zum Datenschutz der Website des Kunden, schuldet WEBklar nicht.
|
||||
|
||||
(2) **Datenschutz.** Wie WEBklar im Kundenportal Daten zu Öffnungszeiten live verarbeitet, steht in der Datenschutzerklärung von WEBklar (Abschnitt 6e). Was beim Aufruf der Website des Kunden übertragen wird, beschreibt Nr. 2 Abs. 4; die Information der Besucher gehört in die Datenschutzerklärung der Website (Nr. 6 Abs. 4).
|
||||
|
||||
Anbieter: WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler. Es gelten ergänzend die Abo-Bedingungen und die Datenschutzerklärung.
|
||||
@@ -0,0 +1,684 @@
|
||||
# Kundenansicht neu: Umsetzungsplan, Übersicht und Verträge
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
> Diese Übersicht ist der Vertrag zwischen den Paketen. Namen, Signaturen, Datenformen, Endpunkte und Dateizuständigkeiten stehen nur hier und gelten für alle Paket-Pläne. Weicht ein Paket-Plan davon ab, gilt diese Übersicht.
|
||||
|
||||
**Goal:** Die Kundenansicht des Portals wird weiß und schlank wie das Ticketsystem, bekommt die Handy-Leiste „Fahrstuhl-Zeile“ und die Funktionen mit „Meine/Entdecken“. Die Funktionen stecken im Abo und werden eingeschaltet statt gekauft. Bestätigungen schrumpfen auf das rechtlich Nötige. Alles liegt hinter einem Pilot-Schalter, bis der Inhaber umschaltet.
|
||||
|
||||
**Architecture:**
|
||||
- Die neue Oberfläche ist eine eigene Seite `public/neu/dashboard.html` mit eigenen ES-Modulen und CSS unter `public/neu/`. Der Server liefert sie unter derselben Adresse `/dashboard.html` aus, wenn der Pilot-Schalter für die Sitzung gilt.
|
||||
- Die Funktionsmodule (`public/termine.js`, `shop.js` …) bleiben gemeinsam. Ihr Aussehen kommt aus dem CSS der jeweils geladenen Seite, ihr Verhalten unterscheidet sich nur über das Feld `modell` der Server-Antwort.
|
||||
- Neue Server-Logik ist nur für Sitzungen mit neuer Ansicht aktiv (`neueAnsicht(req)`) oder rein additiv. Die alte Ansicht verhält sich bis zum Umschalten exakt wie heute.
|
||||
|
||||
**Tech Stack:**
|
||||
- Portal: Node 20 (Prod-Container `node:20-alpine`, Host 24), Express 4, Appwrite-REST (`server/services/appwriteAdmin.js`), Stripe SDK 19, Vanilla JS ohne Build.
|
||||
- Tests Portal: `node --test` mit den Nachbauten aus `test/support/` (`appwriteMock`, `fakeStripe`, `testApp`, `fixtures`, `fakeDom`, `mailSink`) und Playwright-Skripte `test/ui/*.ui.mjs`.
|
||||
- WOMS: React 18, Vite, Tests `node --test` über `tests/unit/load.mjs`.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-10-15-kundenansicht-neu-design.md` (Commit d3d2849, Abschnitte 1–8 mit E1–E21 und C1–C15). Jeder Implementierer liest die Spec, diese Übersicht und seinen Paket-Plan.
|
||||
|
||||
**Code-Karten** (Stand 16.10., nur gelesen): `/root/reports/kundenansicht-neu-2026-10-15/karten/`
|
||||
- `huelle-tests.md`: dashboard, app.js, Router, Tests, Pins
|
||||
- `funktionen-server.md`: Funktionen, Zustände, Bestellen, Aktivieren, Abo-Abgleich
|
||||
- `abo-server.md`: Stundenkonto, Abo, Wechsel, Widerruf, Fassungen, Kundenart, Mails
|
||||
- `woms-vorschau.md`: WOMS-Stellen, Vorschau
|
||||
|
||||
Dazu die Navigations-Demo `/root/reports/kundenansicht-neu-2026-10-15/handy-leiste/final-2-frei.html`.
|
||||
|
||||
---
|
||||
|
||||
## Ausführungsmodell
|
||||
|
||||
```
|
||||
Phase A P0 Werkzeug, Pilot-Schalter, Vorschau-Gerüst (1 Agent, Hauptzweig)
|
||||
Phase B parallel in eigenen Worktrees:
|
||||
Strang UI: P1 Fundament+Hülle → P2 Website/Chat/Verträge
|
||||
Strang Server: P5 Abo-Modell Kern → P6 Abo-Modell Betrieb
|
||||
Strang Recht: P8 Rechtsrunde Fassung 6 (Entwürfe + Fassungs-Technik)
|
||||
Strang WOMS: P9 WOMS (gegen die Verträge V4/V9; Portal-Teile von P9 erst nach Merge von P5)
|
||||
Phase C Merge Server → Hauptzweig, dann Strang UI weiter: P3 Funktionen-Rahmen → P4 Funktionsmodule
|
||||
Phase D Merge Recht + UI → P7 Bestellseiten schlank (Hauptzweig)
|
||||
Phase E Integration, ganze Suite, Vorschau aktualisieren, Abschlussprüfung (mehrere Blickwinkel), Übergabe
|
||||
→ der Inhaber pusht (Schalter off = für Kunden unsichtbar), Pilot fürs Team, Rechtsrunde mit dem Inhaber
|
||||
Phase F P10 Umschalten und Aufräumen (nur mit ausdrücklichem OK des Inhabers)
|
||||
```
|
||||
|
||||
- **Ein Implementierer je Strang**, Modell `sonnet`. Die Tasks eines Strangs laufen der Reihe nach mit TDD.
|
||||
- **Prüfungen:** keine Reviewer je Task. Jeder Paket-Plan endet mit einer Selbstprüfung: eigene Tests grün, Verträge abgehakt, `git status` sauber. Eine gemeinsame Abschlussprüfung gibt es in Phase E. Kleine Nachprüfungen mit `haiku`. Kein `opus`, kein `fable` (Memory „feedback-billige-modelle“).
|
||||
- **Merge-Reihenfolge in den Hauptzweig:** P0 → Server (P5, P6) → Recht (P8) → UI (P1–P4) → P7. Konflikte entstehen nur in den Dateien, die die Zuständigkeitstabelle als „geteilt“ markiert.
|
||||
- **Tags:** nach jedem Paket `kn-p<N>` im jeweiligen Zweig. Nach dem Merge auf den Hauptzweig `kn-p<N>-haupt`.
|
||||
|
||||
### Pakete, Plan-Dateien, Worktrees
|
||||
|
||||
| Paket | Plan-Datei (`docs/superpowers/plans/`) | Worktree | Branch | Start |
|
||||
|---|---|---|---|---|
|
||||
| P0 Werkzeug, Schalter, Vorschau | `2026-10-16-kundenansicht-01-werkzeug-vorschau.md` | `/home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht` | `feature/kundenansicht-neu` | sofort |
|
||||
| P1 Fundament und Hülle | `…-02-fundament-huelle.md` | `…/dev-kn-ui` | `kn/ui` | nach P0 |
|
||||
| P2 Website, Chat, Verträge | `…-03-website-chat-vertraege.md` | `…/dev-kn-ui` | `kn/ui` | nach P1 |
|
||||
| P3 Funktionen-Rahmen | `…-04-funktionen-rahmen.md` | `…/dev-kn-ui` | `kn/ui` (rebased auf Hauptzweig nach Server-Merge) | nach P2 und Server-Merge |
|
||||
| P4 Funktionsmodule | `…-05-funktionsmodule.md` | `…/dev-kn-ui` | `kn/ui` | nach P3 |
|
||||
| P5 Abo-Modell Kern | `…-06-abo-modell-kern.md` | `…/dev-kn-server` | `kn/server` | nach P0 |
|
||||
| P6 Abo-Modell Betrieb | `…-07-abo-modell-betrieb.md` | `…/dev-kn-server` | `kn/server` | nach P5 |
|
||||
| P7 Bestellseiten schlank | `…-08-bestellseiten.md` | `…/dev-kundenansicht` | `feature/kundenansicht-neu` | nach Merge von UI und Recht |
|
||||
| P8 Rechtsrunde Fassung 6 | `…-09-rechtsrunde.md` | `…/dev-kn-recht` | `kn/recht` | nach P0 |
|
||||
| P9 WOMS | `…-10-woms.md` | `/home/webklar/apps/ticket.webklar.com/dev-kundenart` | `feature/kundenart` (WOMS, von `origin/test`) | nach P0 |
|
||||
| P10 Umschalten und Aufräumen | `…-11-umschalten-aufraeumen.md` | `…/dev-kundenansicht` | `feature/kundenansicht-neu` | nach Phase E und OK des Inhabers |
|
||||
|
||||
**Worktrees der Stränge** entstehen aus dem Stand nach P0, zum Beispiel:
|
||||
`git -C /home/webklar/apps/kundenbereich.webklar.com/dev-kundenansicht worktree add ../dev-kn-ui -b kn/ui kn-p0`
|
||||
- Danach `ln -s ../build/node_modules node_modules` im neuen Worktree. Der Symlink ist unversioniert, deshalb **nie `git add -A`**, Dateien immer einzeln hinzufügen.
|
||||
- Der WOMS-Worktree entsteht von `origin/test`: `git -C /home/webklar/apps/ticket.webklar.com/build worktree add ../dev-kundenart -b feature/kundenart origin/test`. Danach `npm ci` mit Speicherbremse, siehe Global Constraints.
|
||||
- Der WOMS-Zweig `fix/akquise-sichtbar` (`5f13553`, Worktree `dev-akquise-sichtbar`) ist nicht gepusht und ändert `CreateLeadForm` und `createLead`. P9 baut auf `origin/test`. Beim Merge in `test` gilt die Reihenfolge, die der Inhaber festlegt. Ist `fix/akquise-sichtbar` bis dahin in `test`, rebased P9 darauf.
|
||||
|
||||
### Dateizuständigkeit (Konfliktschutz)
|
||||
|
||||
Jede Datei hat genau einen Besitzer. Wer etwas aus einer fremden Datei braucht, nutzt die Verträge V1–V12.
|
||||
|
||||
| Datei / Bereich | Besitzer |
|
||||
|---|---|
|
||||
| `server/services/kundenansicht/schalter.js` (neu), Route `/dashboard.html` in `server/index.js`, Konfig `config.kundenansichtNeu`, Feld `ansicht` in `/api/auth/me`; ein **Platzhalter** `public/neu/dashboard.html` für den Routentest (ab P1 gehört die Datei P1) | P0 |
|
||||
| Werkzeug `/root/reports/kundenansicht-neu-2026-10-15/werkzeug/` (eigenes Git), `test/ui/kundenansicht-neu*.ui.mjs` | P0 legt an, P1–P4 und P7 ergänzen nur eigene Ansichten und Fixtures |
|
||||
| `public/neu/dashboard.html`, `public/neu/tokens.css`, `basis.css`, `huelle.css`, `public/neu/dom.js`, `api.js`, `router.js`, `routen.js`, `huelle.js`, `leiste.js`, `konto.js`, `fristen-streifen.js`, `zaehler.js`, `etage.js`, `reiter.js` | P1 |
|
||||
| `public/neu/ansicht-website.js`, `ansicht-chat.js`, `ansicht-vertraege.js`, `ansicht-stunden.js`, `ansicht-speicher.js`, `ansicht-gutschrift.js`, `ansicht-passwort.js`, `public/neu/ansichten.css`; `public/neu/ansicht-bestellen.js` und `ansicht-wechsel.js` als **1:1-Übernahme** der heutigen Bestell- und Wechselseiten im neuen Aussehen (alte Texte, Fassung 5, C14); öffentliche Seiten im neuen Stil unter `public/neu/oeffentlich/` (`login.html`, `kuendigen.html`, `widerrufen.html`, `offline.html`), die erst P10 ausliefert | P2 |
|
||||
| `public/neu/ansicht-funktionen.js`, `funktion-einschalten.js`, `public/neu/funktionen.css`, `public/neu/bilder/*` | P3 |
|
||||
| `public/neu/module.css` (neues Aussehen aller Funktionsmodule). In `public/termine*.js`, `shop*.js`, `buchungen*.js`, `zeiten*.js`, `funktionen.js` (nur News), `analyse*.js` und `funktion-bestellen.js` nur Verzweigungen nach `modell === 'abo'` und die Kurzlabels der Reiter (C2) | P4 |
|
||||
| Umbau von `public/neu/ansicht-bestellen.js` und `ansicht-wechsel.js` auf das schlanke Muster (Fassung 6). In `server/routes/abo.js` nur die Pfade für Fassung 6. `server/services/planSwitch.js:351` mit Umgebung. `server/services/vertragsNachweis.js` (neu) | P7 |
|
||||
| `server/services/kundenart.js` (neu), `server/services/aboFunktionen.js` (neu), `server/services/funktionEinschalten.js` (neu), `server/services/fristen.js` (neu), `server/routes/funktionen.js` (Katalog, Einschalten), `server/routes/fristen.js` (neu), `server/routes/auth.js` (nur `sanitizeCustomer` + Kundenart), `server/services/customerAdmin.js` (Kundenart), `server/services/featureRegistry.js` (Katalogfelder), `server/services/featureConsents.js` (Attribut `kundenart`), `scripts/kundenansicht-setup.mjs` (neu), Shop-/Unterkunft-Anfrage (Unternehmer aus Kundenart), Termine-Zustimmung ohne `gesundheit` im Abo-Modell | P5 |
|
||||
| `server/services/hourLedger.js` (Vorschuss), `server/routes/team/customers.js` (Stundenkonto-Endpunkt), `server/services/siteFeatures.js` (`adminActivate` mit Abo-Prüfung und Abrechnung), `server/routes/admin/funktionenAdmin.js`, `server/services/aboFunktionenAbgleich.js` (neu), Wechsel mit gemerkter Funktion in `planSwitch.js` (nur `details.funktion`, `completeSwitch`, `markSwitchFailed`) und `GET /api/abo` (`wechselFunktion`), Ticket-Texte zum Stundenkonto, neue Mailvorlagen K38–K41 und T24–T25, Anpassung K02/K03/K22/K29/K30/T16 nur für das Abo-Modell | P6 |
|
||||
| `docs/rechtliches/*-v6*.md` und neue Entwürfe, `public/vertragsunterlagen/v5/*` (Archiv), `public/neu/rechtliches/*` (neue Fassungen), `server/services/products.js` (Fassungstabelle), `config.abo.termsVersionNeu`, `scripts/build-legal-*`, `scripts/rechtliches-hochladen.mjs` (neu), `docs/rechtliches/kundenansicht-entscheidungen.md` (neu) | P8 |
|
||||
| WOMS-Repo komplett | P9 |
|
||||
| `scripts/kundenart-bestand.mjs` (neu), Umschalten, Aufräumen alter Dateien | P10 |
|
||||
| **geteilt:** `server/config.js` (P0: Abschnitt `kundenansichtNeu`; P5: Abschnitt `aboFunktionen`; P8: `abo.termsVersionNeu`; jeder nur sein eigener Abschnitt), `test/support/schema.js` (nur neue Collection-/Attribut-Quellen ergänzen), `scripts/setup-abo-collections.mjs` (nicht ändern; neue Attribute nur in `scripts/kundenansicht-setup.mjs`) | wie angegeben |
|
||||
|
||||
Bestehende Testdateien ändert ein Paket nur dort, wo sein Plan sie ausdrücklich nennt, etwa für Pins auf Texte, die das Paket ändert.
|
||||
|
||||
---
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- **Alte Ansicht unverändert.** Bis P10 ist jede Verhaltensänderung für Kunden ohne neue Ansicht verboten. Neue Server-Logik prüft `neueAnsicht(req)`, ist rein additiv (neue Felder, neue Endpunkte) oder betrifft nur Datensätze, die erst über die neue Ansicht entstehen (`unlockedBy: 'abo' | 'projekt'`).
|
||||
- **Schalter:** `KUNDENANSICHT_NEU` ist `off | pilot | on`, Vorgabe `off`. `KUNDENANSICHT_NEU_PILOT_EMAILS` ist eine Komma-Liste, `*` heißt alle. Der Modus `off` schlägt jede Liste.
|
||||
- **Aussehen** (Spec 5.1, Werte wörtlich aus dem Ticketsystem `src/styles/global.css:7-51`):
|
||||
- Farben: `#fff` Fläche, Text `#1a1f36`, Zweittext `#5e6678`, Tertiär `#8a93a3` nur für Nebensächliches, Linien `#ebeef1` und `#d8dde3`, Hover `#f6f8fa`, Fläche `#f8f9fb`, Akzent `#111`.
|
||||
- Status-Pillen (Fläche/Schrift): grau `#ebeef1`/`#414552`, blau `#e0f2fe`/`#075985`, orange `#fcedb9`/`#a82c00`, violett `#efeafe`/`#5b21b6`, grün `#d7f7c2`/`#006908`, rot `#ffe7f2`/`#b3093c`. Fehler `#c0123c`.
|
||||
- Radien 6 und 12 px. Ring-Schatten `0 0 0 1px rgba(60,66,87,.16), 0 1px 1px rgba(0,0,0,.06)`.
|
||||
- Systemschrift `-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif`. Kein Gewicht 800, Zahlen `tabular-nums`.
|
||||
- Verboten: Karten, Verläufe, Glow, Punktraster, Glas als Deko, Dunkelmodus, Lila.
|
||||
- Ladezustände als Text („Wird geladen …“). Dialoge schließen nur per × und Esc.
|
||||
- **Anrede „du“** in allen neuen Texten der Oberfläche. Ausnahmen: Rechtsdokumente und bestehende Mails (Spec 5.7); die Bestellseiten bekommen „du“ erst mit Fassung 6 (C14, P7).
|
||||
- **Keine Preise für Funktionen** in der neuen Oberfläche, nirgends.
|
||||
- **Hashes:** Alle alten Hashes bleiben gültig (`VIEW_HASHES`, Aliase `#wartung`, `#abo`, `#fortschritt`). Die Query nach `?` gelangt unverändert ins Modul. Neue Hash-Namen nur aus `[a-z]` wegen der Login-Whitelist `/^\/dashboard\.html(#[a-z]+)?$/` (`public/app.js:466`, `public/login.html`).
|
||||
- **Rechtlich fest** (Spec 4):
|
||||
- Kerninfo und „Zahlungspflichtig bestellen“ stehen auf demselben Bildschirm.
|
||||
- Der Knopftext bleibt wörtlich.
|
||||
- „Verträge hier kündigen“ ist immer erreichbar. „Vertrag widerrufen“ steht immer im Konto.
|
||||
- `kuendigen.html` und `widerrufen.html` bleiben schlicht.
|
||||
- **Kundenart:** Der Server ist die einzige Quelle. Er nimmt nie die Kundenart vom Client. Eine leere oder unbekannte Kundenart gilt als `privat` (E4), überall über `kundenartWirksam()` (V4).
|
||||
- **Nachweise:** Wortlaut, Fassung, Zeit, IP, Browser und Konto werden weiter gespeichert, ab jetzt mit der Kundenart zum Klickzeitpunkt.
|
||||
- **Tests Portal** (aus dem jeweiligen Worktree):
|
||||
- Einzeldatei: `timeout 900 nice -n 10 node --max-old-space-size=1024 --test test/<datei>.test.js`.
|
||||
- Mehrere Dateien in einem Aufruf nur mit `--test-concurrency=1`.
|
||||
- Die ganze Suite nur in Phase E und P10: `timeout 2400 nice -n 10 systemd-run --scope -q -p MemoryMax=2500M -p MemorySwapMax=0 env NODE_OPTIONS=--max-old-space-size=1024 node --test --test-concurrency=2 test/*.test.js`.
|
||||
- Vor jedem schweren Lauf `free -m`. Bei weniger als 2000 MB verfügbar warten und den Controller melden.
|
||||
- **Tests WOMS:** `cd /home/webklar/apps/ticket.webklar.com/dev-kundenart && timeout 900 nice -n 10 systemd-run --scope -q -p MemoryMax=1500M -p MemorySwapMax=0 env NODE_OPTIONS=--max-old-space-size=1024 npm test`.
|
||||
- **Playwright** nur mit dem Werkzeug aus P0 (`run-browser.sh`, eine Instanz, Speicherbremse). `chromium-1234`, `playwright-core` 1.62.1.
|
||||
- **Commits:** Dateien einzeln hinzufügen. Nachrichten deutsch, mit der Zeile `Co-Authored-By: …` aus dem eigenen Systemhinweis der ausführenden Sitzung (kein fester Modellname, Nachtrag N50).
|
||||
- Kein Push, kein `--force`, kein Merge auf `main` oder `test`. Pushen tut nur der Inhaber.
|
||||
- Keine Änderungen in `build/`, weder im Portal noch im WOMS.
|
||||
- **Sicherheitsblock** in jedem Agent-Auftrag (Memory „feedback-destruktive-skripte“):
|
||||
- Kein `rm -rf` außerhalb des eigenen Scratch-Ordners, nie `/dev`, `/etc` oder `/` anfassen.
|
||||
- Kein `docker run`, keine globalen Installationen.
|
||||
- Das Vorschau-Skript nie mit Ziel aufrufen, nur `--pruefen`. Nie mit Pfaden wie `..` testen.
|
||||
- `TMPDIR` auf den eigenen Scratch-Ordner setzen.
|
||||
- Nach großen Läufen prüfen, dass `ls -l /dev/null` `crw-rw-rw-` zeigt.
|
||||
- **Prod-Daten:** Kein Paket schreibt in die Live-Datenbank. Ausnahmen sind die Setup-Skripte (`scripts/kundenansicht-setup.mjs`) und `scripts/kundenart-bestand.mjs`. Beide laufen nur auf ausdrückliche Anweisung des Controllers nach OK des Inhabers, vorher immer mit `--pruefen`.
|
||||
|
||||
## Review Focus
|
||||
|
||||
1. **Kundenart leer, unbekannt oder vom Client gefälscht:** Ohne gesetzte Kundenart sieht der Kunde das eine Häkchen, und der Server verlangt es auch. Ein Body-Feld `kundenart` vom Client wird ignoriert. Tests: P5 Task „kundenartWirksam“ und „Einschalten privat in Frist“, P7 Task „Bestellung Fassung 6“.
|
||||
2. **Pilot-Schalter an den Rändern:** Groß-/Kleinschreibung und Leerraum der E-Mail, `*`, leere Liste, `off` mit `*`, Mitarbeiter-Kundenansicht (`viewAs` + `viewId`) im Pilot. Ohne neue Ansicht liefern die Abo-Modell-Endpunkte 404, und `/dashboard.html` ist die alte Seite. Tests: P0 Tasks „Schalter“ und „Dashboard-Weiche“, P5 Task „Einschalten nur in neuer Ansicht“.
|
||||
3. **Alte Links nach dem Umbau:** `#termine?bestellt=1&projekt=x`, `#webshop?stripe=zurueck&projekt=x`, `#rechnungen`, `#stunden`, `#gutschrift`, `#wartung`, `#fortschritt`, `?abo=success`, Login mit `next=/dashboard.html#chat`. Jeder Link landet am richtigen neuen Ort, und die Query bleibt erhalten. Test: P1 Task „Routen“.
|
||||
4. **Doppeltes oder widersprüchliches Einschalten:** Zwei Taps, schon aktiv, zweite Website, Hosting-Kunde bei einer Wartungs-Funktion (Wechsel mit gemerkter Funktion), gescheiterter Wechsel, Privatkunde in der Frist ohne Häkchen, Funktion per Schalter aus. Ergebnis: höchstens ein Ticket, klare 409-Codes, kein Zustand ohne Nachweis. Tests: P5 Task „Einschalten“, P6 Task „Wechsel mit Funktion“.
|
||||
5. **Vorschuss statt Mehraufwand:** Nicht gedeckte Einrichtungsminuten erscheinen nie als Mehraufwand. Normale Arbeit über dem Guthaben bleibt Mehraufwand. Eine spätere Gutschrift gleicht den Vorschuss zuerst aus. Bei Vertragsende bleibt ein Restminus stehen, wird aber nicht berechnet. Gilt auch für Git-Push-Arbeitsblätter auf Einrichtungs-Tickets. Tests: P6 Task „Vorschuss im Stundenkonto“, P9 Task „Abo-Vorgabe bei Einrichtungs-Tickets“.
|
||||
|
||||
---
|
||||
|
||||
## Nachträge nach der Planrunde (verbindlich, 16.10.2026)
|
||||
|
||||
Die Paket-Pläne wurden parallel geschrieben; ihre Vertragsfragen sind hier entschieden. Diese Nachträge gehen V1–V13 und der Zuständigkeitstabelle vor. Wo ein Paket-Plan davon abweicht, gilt dieser Abschnitt.
|
||||
|
||||
Kürzel in den Fundstellen: `Task n` und `VFn` = Task bzw. Vertragsfrage n im genannten Paket-Plan (P0 bis P10), `Fn` = Frage n der Planrunde (`/root/reports/kundenansicht-neu-2026-10-15/planrunde.json`, Feld `vertragsfragen`).
|
||||
|
||||
### Schalter und Sitzung
|
||||
|
||||
- **N1 Gültige Sitzung (V1).** Kunde heißt `customerId` und `appwriteUserId` ohne `role: 'admin'`; Kundenansicht heißt `viewAs === true` mit `viewId`. Alte Mitarbeiter-Sitzungen (`role: 'admin'` ohne `viewId`) sind in jedem Modus `false`, auch bei `*`. Eine `email`, die kein Text ist, zählt als leer.
|
||||
(P0 VF3; F12)
|
||||
- **N2 Schalter für Jobs ohne Cookie (V1).** `schalter.js` bekommt den vierten Export `export function neueAnsichtFuerKunde({ customerId, email }) // boolean`. Gleiche Modus- und Listenregeln wie `neueAnsichtFuerSession` (`off` false, `on` true, `pilot` bei `*` oder getrimmter, klein geschriebener `email` in der Liste), aber ohne Sitzungsprüfung und ohne Mitarbeiter-Regel; leere oder nicht-textliche `customerId` und Nicht-Objekte ergeben `false`. P5 `aboModellFuerKunde(customerId)` liest das Kundendokument und ruft sie; `runAboCheck` und P6 (Team-Aktivierung, Widerruf des Wechsels, Tagesjob) nutzen nur sie, nie `neueAnsichtFuerSession`.
|
||||
(P0 Task 2, VF10; P5 Task 13; F12)
|
||||
- **N3 Weiche (V1).** `server/services/kundenansicht/dashboardWeiche.js` ist eine Fabrik (`server/index.js` startet beim Import den Server). Der Direktaufruf `/neu/dashboard.html` wird für Sitzungen ohne neue Ansicht per 302 auf `/dashboard.html` geleitet; Module und CSS unter `/neu/` bleiben frei abrufbar. Bei Modus `on` liefert die Weiche von P10 (`UMSCHALT_SEITEN`, Zeile `/dashboard.html`) die neue Seite auch ohne Sitzung; `neueAnsichtFuerSession` bleibt unverändert.
|
||||
(P0 VF1, VF2; P10 Task 3, VF7; F12)
|
||||
- **N4 `/api/auth/me`.** `ansicht` steht nur in den beiden `authenticated: true`-Antworten von `GET /me`. P5 ändert in `auth.js` allein `sanitizeCustomer` (`kundenart`, `kundenartWirksam`); `test/kundenansichtMe.test.js` pinnt nur die Schlüssel der obersten Ebene, damit `customer` wachsen darf.
|
||||
(P0 Task 4, VF4; P5 Task 2; F13)
|
||||
- **N5 Platzhalter-Marker.** P0 Task 3 setzt im Platzhalter beide Marker (`<meta name="kn-seite" content="neu">` und `data-kn-platzhalter`) und pinnt in `istNeu` nur die Meta-Zeile. P1 Task 12 ersetzt die Seite, ohne einen P0-Test zu ändern; die Werkzeug-Ansichten `platzhalter` und `platzhalter-abgemeldet` bleiben, bis P1 sie ersetzt.
|
||||
(P1 VF1, Task 12/13; P0 Task 3; F11)
|
||||
|
||||
### Hülle, Routen, Reiter, Zähler
|
||||
|
||||
- **N6 Routen (V2, „Pflichtinhalt von ROUTEN“).** Die Zeile „`vertraege`, `abo`, `wartung` → vertraege, reiter `abo`“ entfällt. Neu: `vertraege` → Bereich `vertraege` ohne `reiter` (die Ansicht wählt den Standardreiter, bei offener Rechnung `rechnungen`, sonst `abo`); `abo` und `wartung` → Bereich `vertraege`, reiter `abo`, kanonischer Hash `#abo` (`#wartung` wird `#abo`, `#vertraege` bleibt `#vertraege`); `?abo=success|cancel` → `#abo`. `waehleReiter` (P2) entscheidet nur über `route.reiter`; Reiterklicks navigieren mit `ersetzen: true` zu `#abo`, `#rechnungen`, `#stunden`, `#speicher`, und `ankerKette`/`elternVon` liefern weiter `#vertraege` als Bereichs-Anker.
|
||||
(P1 Task 3/4, VF4; P2 Task 16, VF8; F17)
|
||||
- **N7 Routenfelder und Exporte (V2).** Route-Felder zusätzlich `produkt` (`'hosting' | 'wartung'`) und `scroll` (`'aktivitaet'`); `query` ist immer ein String. Zusätzliche Exporte von `routen.js`: `hashFuerAnsicht(name)` (die `data-view`-Brücke der gemeinsamen Module), `startHash`, `ankerKette`, `elternVon`, `titelFuer`, `FUNKTIONS_ETAGEN`. Die Projekt-ID heißt einheitlich `#projekt?projekt=<id>` (Lesen von `id=` bleibt Rückfall); ein `data-project` an einem `[data-view]`-Element wird zu `?projekt=<id>`.
|
||||
(P1 Task 3, VF4; P2 VF4; F17)
|
||||
- **N8 Kleinere Festlegungen der Hülle (V2).** `api(…, { roh })` meint einen Rohdaten-Body (`application/octet-stream`) wie `fn-util.js request({ raw })`, nicht eine rohe Antwort; der V2-Kommentar ist so zu lesen. `?abo=` bleibt in `location.search`; P2 räumt mit `history.replaceState(history.state, '', location.pathname + location.hash)` auf, und P4 ändert die `history.replaceState`-Aufrufe der Module nicht (`stempeln()` aus P1 stempelt danach neu). `public/neu/manifest.webmanifest` hat dieselbe `id`, `start_url` und `scope` wie die alte; `sw.js` bleibt gemeinsam und wird in P1 Task 11 registriert. Ansichten rendern keine eigene `h1`; der Titel kommt aus `titelFuer(route)` oder `kn:titel` (P7: `meldeTitel`).
|
||||
(P1 VF7 bis VF11; P7 Task 10/11/13; F20)
|
||||
- **N9 Reiter-Baustein und Meldungen (V2, Hülle).** `baueReiter({ eintraege: [{ key, label, kurz? }], aktiv, art, ariaLabel, bei(key) })` in `public/neu/reiter.js` liefert direkt ein `HTMLElement` mit `setzeAktiv`, `setzeAnzeige`, `messen`; P2 und P3 passen sich an. Inhalte in Etage 2 melden Reiter und Titel ohne Import per `CustomEvent` mit `bubbles: true`: `kn:reiter` (`detail { eintraege, aktiv, bei }`, leere Liste = keine Reiter) und `kn:titel` (`detail { text }`).
|
||||
(P1 Task 5, VF5; P2 Task 16; P3 Task 5; F18)
|
||||
- **N10 Reiter der Funktionsmodule in Etage 2.** Terminplaner, Webshop, Buchungskalender und Analyse zeichnen im Abo-Modell keine eigene Leiste, sondern melden `kn:reiter` (die Analyse aus `mountCockpit` mit `bei: pickTab`: Übersicht, Heatmap, Einstellungen). Die Ausnahme „außer analyse“ im Gate `p4` entfällt; der Satz „die Analyse behält ihre eigene Leiste“ in F18 ist überholt. Kurzlabels stehen in `KURZLABELS` (`public/fn-reiter.js`: Team, Start, Recht, Preise), die Vorschau prüft sie (C2).
|
||||
(P4 Task 1/8/15, VF2; P1 VF5; F4, F18)
|
||||
- **N11 Zähler-Ereignis (V3).** `kn:zaehler-neu` wird am `document` gesendet (`bubbles: true`), `detail.was` ist `'chat' | 'vertraege' | 'alle'`. `zaehler.stand()` liefert nur `{ chat, vertraege }`; die Hülle setzt den Chat-Zähler beim Öffnen des Chats auf 0.
|
||||
(P1 Task 7/11, VF6; P2 VF2; F19)
|
||||
- **N12 Zeile „Rechnung offen“.** P1 zeichnet sie nicht und reicht kein `zaehler` an die Ansichten; `zeigeX(wurzel, { route, me, nav })` bleibt wie in V2. P2 liefert `public/neu/rechnung-offen.js` (`offeneRechnungen`, `rechnungOffenZeile`, `ladeOffeneRechnungen`, optional `ladeOffeneRechnungen(apiFn = api)`); P3 `zeigeFunktionen` lädt `GET /api/invoices` selbst über `abh.api` und zeichnet die Zeile in Meine und Entdecken. Fällt der Abruf aus, fehlt nur die Zeile.
|
||||
(P3 Task 5, VF5; P2 Task 3, VF3; P1 VF6; F1, F19)
|
||||
- **N13 Option `einschalten` für die Funktionsmodule (V2, Absatz „Funktionsmodule“).** Die Hülle ruft `initNews`, `initTermine`, `initShop`, `initBuchungen`, `initZeiten` als `init*(wurzel, { einschalten })` mit `einschalten(platz, { key, projectId, onFertig, viewAs? }) → Promise`, das lazy `zeigeEinschalten` aus `public/neu/funktion-einschalten.js` (P3) lädt (`MIT_EINSCHALTEN`, `einschaltenOption(lade)` in `huelle.js`); `initAnalyse(wurzel)` bleibt unverändert, die Projektwahl bleibt `wurzel.dataset.wunschProjekt`. `test/knHuelleStart.test.js` („Option einschalten“) pinnt die Verdrahtung; P4 Task 15 ersetzt seine Zeile `command grep -a -n einschalten public/neu/huelle.js` durch den Lauf dieses Tests.
|
||||
(P1 Task 11, VF12; P3 VF1; P4 VF1; F1, F21)
|
||||
- **N14 Overlay.** Ansichten dürfen `oeffneOverlay` und `beendeOverlay` aus `router.js` rufen (P3 für die Bestätigungsseite). Es ist nie mehr als ein Overlay offen; P3 und das Konto-Sheet schließen vor dem Öffnen ein offenes Overlay.
|
||||
(P3 Task 6, VF5; P1 R6; F22)
|
||||
- **N15 Chat-Composer.** Die neue Seite lädt `/neu/chat-composer.css`, eine lilafreie Kopie von `public/chat-composer.css` (Akzent `#111`/Grau, weder `#9d3dfe` noch Gewicht 800 noch `backdrop-filter`); `/chat-composer.js` bleibt gemeinsam, das alte CSS unverändert. P2 Task 7 legt die Kopie an und ändert dafür genau die zwei Composer-Zeilen in `dashboard.html`; `test/knHuelleSeite.test.js` akzeptiert `/neu/chat-composer.css` statt `/chat-composer.css`.
|
||||
(P1 VF3; P2 Task 7, VF10; F23)
|
||||
- **N16 Analyse-Bild und Alt-Klassen.** `public/neu/bilder/analyse.webp` erzeugt P4 über die Werkzeug-Ansicht `kn-modul-analyse` (Fall `m-analyse-aktiv`); bis dahin entfernt der `error`-Handler in „Entdecken“ das fehlende Bild. `public/neu/module.css` Abschnitt 0 (P4) definiert `kb-panel`, `kb-btn*`, `kb-empty`, `kb-link`, `kb-checks`, `abo-actions`, `abo-btn`, `hidden`, `muted`, `error`, außer eine Datei unter `public/neu/` tut es schon; P2 und P3 nutzen eigene `kn-*`-Klassen.
|
||||
(P3 VF2; P4 Task 9/14, VF3, VF5; F24)
|
||||
|
||||
### Katalog, Einschalten, Analyse
|
||||
|
||||
- **N17 Katalogfelder (V5).** Jeder `katalog[]`-Eintrag trägt `consent: { version, knopf: 'Einschalten', items: [{ id, text, haken, nurPrivatInFrist? }] }` (`null` bei `analyse`) in der Reihenfolge `einschalten-zeile`, `hinweis`, `beginn` und dazu `hinweis: string | null`, den Text des Items `hinweis` (nur `shop`, `unterkunft`, `termine`: die Pflichten der alten Häkchen `verantwortung`, `stripe`, `gesundheit`). Der Server liefert beides, nie der Client; der Nachweis speichert den Hinweis als Text `hinweis` (`haken: false`). P3 `renderEinschalten` zeichnet `p.kn-fn-hinweis` direkt über dem Knopf (nach der Linkzeile, vor `beginn`), die P0-Fixtures und `vertragNeu` tragen beide Felder.
|
||||
(P5 Task 6/10/11, VF2; P3 Task 3; P0 Task 6/7, VF11; P8 VF3; F3, F27)
|
||||
- **N18 `anfrageStatus` (V5).** Der Pfad ist `projects[].features.<shop|unterkunft>.anfrageStatus` (`'' | 'offen' | 'angenommen' | 'abgelehnt' | 'zurueckgezogen'`), gebaut von `anfrageStatusJeProjekt` in `server/services/aboKatalog.js`; P3 liest ihn nur über `anfrageStatusVon(projekt, key)`.
|
||||
(P0 VF9; P5 Task 11; P3 Task 1; F16)
|
||||
- **N19 Antwortcodes der Einschalt-Route (V5).** Die Prüfreihenfolge nach P5 Task 10: 400 `zustimmung_fehlt`; Projektprüfung (403, 409 archiviert); dann 409 `nicht_verfuegbar` (danach 503 `unterlagen_fehlen`), `abo_fehlt`, `nur_betrieb`, `bestellung_offen`, `beginn_noetig`, `bereits_eingeschaltet`, `bestellung_laeuft` (Zustand `ordered`), `abgeschaltet` (Zustand `disabled`), `fassung_veraltet`. 503 kommt zusätzlich bei nicht lesbarem Abo oder gescheitertem Ticket; P3 zeigt den Text des Servers.
|
||||
(P5 Task 10, VF3; P3 VF4; F27)
|
||||
- **N20 Probezeit (V5).** `aboDeckt` kennt zusätzlich den Grund `beginnt_spaeter` mit `ab` (ISO, = `trialEnd`) für `trialing` mit offener Probezeit (Privatkunde ohne Sofortbeginn-Häkchen). `abo_fehlt` trägt dann `body.ab`; der Text lautet „Dein Abo beginnt am TT.MM.JJJJ. Ab dann kannst du einschalten.“, und P3 (`TEXTE`, `fehlerAktion`) zeigt Text und Datum im Knopf. P6 (`adminActivate`) und P9 kennen den Grund; die Analyse-Eignung meldet in der Probezeit weiter `kein_abo`.
|
||||
(P5 Task 4/9/10, VF9; P3 VF4; F8)
|
||||
- **N21 Abrechnung am Funktionsdokument (V7).** Das Attribut `portalFeatures.einrichtungAbrechnung` (string 8, `'abo' | 'projekt' | ''`, angelegt in `scripts/kundenansicht-setup.mjs`) hält die Abrechnung am Dokument, damit `ticketNachlegen` und `holeZeitenNach` den Marker `workorders.einrichtungAbrechnung` erneut setzen. P6 nutzt es für `ticketNachlegen` (Dokumente mit `unlockedBy` `abo` oder `projekt` ohne Stripe-Sitzung heilen über `einschalten`) und für die Ticket-Zeilen in `termine/ticketDetails.js`, `shop/ticketDetails.js`, `unterkunft/ticketDetails.js`; K27 bleibt die Einschalt-Mail der Zeiten.
|
||||
(P5 Task 1, VF5; P6 VF3, VF4; F27)
|
||||
- **N22 Analyse im Abo-Modell.** In der neuen Ansicht verlangt `POST /api/funktionen/analyse/activate` `audienceConfirmed === true` (sonst 400 `zielgruppe_fehlt`, du-Text) und schreibt einen Nachweis in `featureConsents`: Linkzeile A1, Zielgruppen-Zeile A2 (`haken: true`), Knopf `Analyse einschalten`, Kundenart und Fassungen; bei einem Fehler wird alles zurückgenommen. P4 sendet `audienceConfirmed: true`, die alte Ansicht bleibt unverändert.
|
||||
(P5 Task 13, VF8; P4 Task 8, VF4; F29)
|
||||
|
||||
### Kundenart und Push-Reihenfolge
|
||||
|
||||
- **N23 Kundenart-Pflicht beim Anlegen (V4; Entscheidung des Controllers, F2 und F28 widersprachen sich).** `createCustomerWithPortalAccess` verlangt `kundenart` nur bei `neueAnsichtModus() !== 'off'` (sonst 400 `kundenart_fehlt`); bei `off` ändert sich das Anlegen im WOMS und im Portal nicht. `updateCustomerWithPortalAccess` nimmt `kundenart` weiter optional mit gleicher Prüfung. Die unbedingte Pflicht aus F28 gilt nicht.
|
||||
(P5 Task 3, VF1; P9 VF3; F2, F28)
|
||||
- **N24 Push-Reihenfolge (Entscheidung des Controllers).** Der Inhaber pusht in dieser Reihenfolge: (1) `fix/akquise-sichtbar`, falls gewünscht; (2) WOMS-Tag `kn-p9-kundenart`; (3) Portal ab P5; (4) WOMS `kn-p9` zusammen mit P6. Vor dem Setzen von `KUNDENANSICHT_NEU=pilot` muss die WOMS-Kundenart (`kn-p9-kundenart`) live sein. Wird `fix/akquise-sichtbar` später gepusht, rebased P9 darauf, nur auf Anweisung des Controllers. Die Übergabe-Notiz nennt die Reihenfolge und vermerkt, dass die Abo-Vorgabe in WOMS zunächst nur für Pilotkunden wirkt (P6 wertet `abrechnung` nur mit `aboModellFuerKunde` aus).
|
||||
(P5 VF1, VF10; P9 VF3, VF4; F2, F28)
|
||||
|
||||
### Stundenkonto, Team, Mails
|
||||
|
||||
- **N25 Antwortformen des Stundenkontos (V8).** `nextExpiry` ist `{ minutes, at } | null`; bei `produkt: 'hosting'` und ohne Abo ist `ledger` gleich `null`. `workorders.einrichtungFuer` und `einrichtungAbrechnung` sind für Mitarbeiter lesbar (Setup-Skript P5). T24 trägt den Block `vorschuss`, wenn beim Einschalten `balanceMinutes <= 0` gilt oder schon `vorschussMinutes > 0` besteht; eine Schätzung je Funktion gibt es nicht.
|
||||
(P6 Task 2/4, VF6; P9 VF2; F33)
|
||||
- **N26 Geltung der Team-Aktivierung (V10).** Das neue Verhalten (`aboDeckt`-Prüfung, `einschalten` mit Ticket) gilt nur, wenn `aboModellFuerKunde(customerId)` wahr ist; sonst wird `abrechnung` im Body ignoriert und alles läuft wie heute, mit `on` (P10) für alle. P9 sendet `abrechnung` immer. Die 409-Antwort bei fehlendem Abo ist flach: `{ error, code: 'abo_fehlt', grund, wechsel }` mit `grund` aus `kein_abo | falsches_abo | gekuendigt | nicht_aktiv | beginnt_spaeter`; WOMS zeigt eigene Texte, nie den Kundentext in „du“-Form. Erfolg wie bisher `projects[0].features[key]` mit `state: 'setup'` und `ticketWoid`.
|
||||
(P6 Task 7, VF2; P9 VF2; F32)
|
||||
- **N27 Abo-Modell-Dokument (V10, V11).** Ein Dokument gehört zum Abo-Modell, wenn `unlockedBy` in `abo | projekt` steht und `einrichtungAbrechnung` in `abo | projekt` (`istAboModell` in `server/services/aboModell.js`); die Gratis-Zeiten der alten Ansicht (`unlockedBy: 'abo'` ohne Marker) bleiben unberührt. V11 ändert sich: Der Widerruf des Wechsels beendet alle Abo-Modell-Dokumente, deren Schlüssel nur in `wartung` steckt, also auch `projekt`-Dokumente; die Analyse eines Hosting-Kunden bleibt.
|
||||
(P6 Task 5/13, VF4; F32)
|
||||
- **N28 Team-Einschalten „im Projekt“ ohne Abo (E9/E21; Entscheidung des Controllers, dem Inhaber in der Übergabe zur Bestätigung vorlegen).** Es gilt die Aufbauphasen-Regel von P6 (`server/services/aboAufbauphase.js`, `ohneAboBisher(customerId)`, P6 Task 7, 8, 10). `abrechnung: 'abo'` braucht ein deckendes Abo, sonst 409 `abo_fehlt`. `abrechnung: 'projekt'` braucht es auch, **außer in der Aufbauphase**: Hatte der Kunde noch nie ein Abo (abgebrochene Bestellungen zählen nicht), wird eingeschaltet und mit 200 plus `warnung: { code: 'abo_fehlt', grund: 'kein_abo' }` geantwortet, flach neben `projects`. Das boolesche `warnung: true` für ein fehlendes K16 bleibt für die alte Ansicht. WOMS zeigt die Warnung (P9), der Wortlaut sagt dem Team: „Wartungs-Abo vor dem Livegang vereinbaren“. Der Tagesabgleich lässt `projekt`-Dokumente in der Aufbauphase in Ruhe. Ab dem ersten Abo gilt die Abo-Regel wie für `abo`: Deckt das Abo die Funktion nicht (Hosting, beendet), endet sie mit K39. Hat der Kunde schon ein nicht deckendes Abo, bleibt es auch im Projekt bei 409.
|
||||
(P6 Task 7/8/10, VF8; P9 Task 10/11; F5)
|
||||
- **N29 Mailkennungen (V13).** Belegt sind K38 „Deine Funktion wird eingerichtet“ (Einschalten im Abo-Modell, nicht für `zeiten`, dort bleibt K27); K39 „Deine Funktion wurde beendet“ (News, Terminplaner mit Block `termine`, Webshop, Unterkunft; Platzhalter `funktion`, `projekt`, `loeschtag`); K40 und K41 (Zeiten: Abo-Ende mit „läuft noch bis {{frist}}“, abgeschaltet; ohne Preis, ohne Behalten); T24 ans Team mit Block `vorschuss`; K42 (Webshop-Zusage) und K43 (Buchungskalender-Zusage) im Abo-Modell ohne Preiszeile, ohne „€“ und ohne „bestell…“ (Builder `anfrageEntschiedenMail`, Weiche in `sendeEntscheidung` über `aboModellFuerKunde`). K24 und K34 bleiben bytegleich. K44 und T25 bleiben frei; eine Rückkehr-Mail gibt es nicht (die Rückkehr geschieht still, K39 kündigt sie an), außer bei Zeiten im bestehenden Modul. K02 und K03 bekommen den Block `funktionen_abo`; K22, K29, K30 und T16 bleiben bytegleich. Alle neuen Vorlagen in „du“-Form und im Layout von K16.
|
||||
(P6 Task 3/4/5/15, VF1, VF5; F31)
|
||||
- **N30 Behalten-Weg der Zeiten.** Für Abo-Modell-Dokumente ist `POST /api/funktionen/zeiten/behalten` gesperrt (409, P6). Die Route und die Mailtexte K29/K30 (Builder `k29Mail`, `k30Mail` auf K40/K41 umgestellt, Vorlagen-Dateien bleiben) räumt P10 Task 20/21 auf. Die WOMS-Texte (`zeitenStatusText`, Bestätigungstexte „Aktivieren (ohne Zahlung)“) ändert ein eigener WOMS-Nachzug nach P10 Task 22 (Controller, Push durch den Inhaber, geführt in der Checkliste Task 6). Alt-Zweige hinter `neueAnsicht(` bleiben als toter Code; ihre Entfernung entscheidet der Inhaber.
|
||||
(P6 VF7; P9 VF5; P10 VF11, VF12; F34)
|
||||
- **N31 Abo-Ende-Verhalten (Rechtstexte = Server).** Terminplaner, News, Webshop und Buchungskalender schalten spätestens am Folgetag ab (Tagesjob). Öffnungszeiten live läuft nach dem Abo-Ende 30 Tage unverändert weiter (Zeiten-Modul, K40, P4 Text Z2), danach 30 Tage gespeicherte Eingaben; kein Behalten gegen 30 €. Löschfristen: 30 Tage (Terminplaner, News, Öffnungszeiten), 90 Tage (Webshop, Buchungskalender), 60 Tage (Analyse-Messdaten); ein neues passendes Abo in der Frist holt die Funktion zurück, P6 baut den News-Löschjob. P8 passt Texte und Tests an (R8 „mit P4/P6 abgestimmt“).
|
||||
(P8 Task 4/7, VF5; P6 Task 8/10; P4 Text Z2; F39)
|
||||
|
||||
### Bestellseiten, Fassung 6, Nachweis
|
||||
|
||||
- **N32 Pfade der Fassung 6 (V12).** `config.abo.termsVersionNeu = '2026-10-16-v6'` (Entwurfstag, bei der Freigabe wird das Datum gesetzt); `FUNKTIONEN_FASSUNG_V6 = '2026-10-16'`. Bis zum Umschalten gelten die Abo-Seite `/neu/rechtliches/abo-bedingungen.html`, das PDF `/neu/rechtliches/WEBklar-Abo-Vertragsunterlagen.pdf` und die Funktionsunterlagen `/neu/rechtliches/<slug>-2026-10-16.html|pdf`; `termsDocFor` liefert sie, `/abo-bedingungen-v6.html` aus V12 entfällt. P7-Fixtures und -Tests nutzen `termsDocFor(config.abo.termsVersionNeu).url` und `config.abo.termsVersionNeu` statt Literalen wie `'2026-10-30-v6'`. P10 Task 14 verschiebt Seite und PDF nach `public/` (`/abo-bedingungen.html`), stellt in `FASSUNGEN` v5 auf die `archiv`-Adressen (`public/vertragsunterlagen/v5/`) und v6 auf die Wurzel-Adressen, setzt 301-Weiterleitungen und passt `DOKUMENTE_V6`, den Modus `abo6` und die festen Links an.
|
||||
(P8 Task 1/5, VF1; P10 VF3; P7 Task 16; F35)
|
||||
- **N33 Fassung 6 verdrahten: ein Task, zwei Pläne (V5, V12).** P5 Task 15 und P7 Task 0 beschreiben dieselbe Arbeit und laufen einmal, als erste Aufgabe von P7 im Hauptzweig nach dem Merge von `kn/server` und `kn/recht`, mit den Dateien beider Tasks. Quellen sind `WORTLAUT_V6`, `FUNKTIONEN_FASSUNG_V6`, `FUNKTIONEN_UNTERLAGEN_V6` und `mitFassung` aus `server/services/products.js` (P8 liefert, verdrahtet nicht). Verdrahtet werden `config.aboFunktionen.fassung` und `texte.beginn` (Literale, `config.js` importiert keine Dienste), `consentAbo` mit dem Item `hinweis`, `aboKatalog.js` (`unterlagen`, `hinweis`), `aboUnterlagen.js` und `sichereAdresse` in `funktion-einschalten.js` (lässt `/neu/rechtliches/` zu). `bestelltexte.js` bleibt importfrei, `test/knBestelltexte.test.js` pinnt Linkzeile, Häkchen und Fassungsangabe gleich zu `WORTLAUT_V6` (eine einheitliche Form der Fassungsangabe), danach läuft `test/aboWortlautV6.test.js` grün. Die Analyse (`config.analyse.avvVersion`, `legalPaths`, K18-Weiche mit den PDFs aus `public/neu/rechtliches/`) folgt dieser Verdrahtung erst nach der Freigabe; wo P5 Task 15 sie sofort auf Fassung 6 stellt, gilt diese Regel.
|
||||
(P5 Task 15, VF7, VF10; P7 Task 0/1, VF12; P8 VF2; F3, F29, F30)
|
||||
- **N34 Vertragsnachweis (V12).** `speichereVertragsNachweis({ customerId, art, bezug = '', texte, fassungen, kundenart, req, at })` schreibt selbst mit `createDocument` und `texteHash` (aus `featureConsents.js`), weil `speichereZustimmung` `projectId = ''` ablehnt; `featureKey = art`, `projectId = ''`. `bezug` und `wortlaut` (`WORTLAUT_STATUS`) stehen als Schlüssel in `fassungen` (höchstens 1000 Zeichen), `texte` bleibt die reine Anzeigereihenfolge. `stripeSessionId` bleibt leer, weil der Nachweis vor der Stripe-Sitzung, dem Wechsel, der Speicheränderung und dem Antrag entsteht (kein Zustand ohne Nachweis); die Zuordnung läuft über Zeit, Konto und `bezug`.
|
||||
(P7 Task 2, VF3 bis VF5; P8 VF4; F36)
|
||||
- **N35 Fassung in den Bestellrouten.** P7 übernimmt die Stellen, die `config.abo.termsVersion` fest lesen: `stripeBilling.js:606-607` (`createAboCheckout`: Fassung aus `consent.termsVersion`, Vorgabe unverändert), `abo.js:236`, `:328`, `:388` (Prüfung) und `abo.js:201` (`GET /api/abo` liefert `termsVersion` über `aktuelleAboFassung(req)`). `fassungDerAnfrage(req, termsVersion)` prüft die Neu-Fassung zuerst; bei gleichen Werten von `termsVersion` und `termsVersionNeu` gilt `neueAnsicht(req) ? 'neu' : null`, sodass P10 Task 14 hier nichts ändert. `server/routes/takeoverCredits.js` `POST /` schreibt den Nachweis bei Fassung 6.
|
||||
(P7 VF2; P8 VF4; P10 VF4; F36, F42)
|
||||
- **N36 Speicher und Gutschrift (Fassung 6).** P7 übernimmt `ansicht-speicher.js` (Plus-Block) und `ansicht-gutschrift.js` (Zustimmungsblock) wie `ansicht-bestellen.js` und `ansicht-wechsel.js`: P2 baut sie 1:1 (Fassung 5), P7 baut sie auf das schlanke Muster um. Die Orakel-Vergleiche gegen `app.js` werden durch Pins auf die Fassung-6-Texte ersetzt; der Hash-Pin des Blocks `TAKEOVER_CONDITIONS` (`conditionsVersion` 2026-09-30) bleibt. Die Gutschrift-Bedingungen werden die eigene Ansicht `#gutschrift?bedingungen=1` mit unverändertem Rechtstext; `test/support/knWortlautOrakel.js` entfernt P10 Task 17 erst mit `app.js`.
|
||||
(P2 VF5; P7 VF1, VF8; P10 Task 17; F25)
|
||||
- **N37 Texte der Bestellseiten.** Fehlertexte der Dienste (`planSwitch.js`, `mailStorage.js`, `stripeBilling.js`, Abo-Routen) bleiben im Quelltext in Sie-Form und werden für Sitzungen mit neuer Ansicht über `server/services/duFehler.js` in du übersetzt; ein Quelltext-Test meldet jeden neuen Sie-Satz ohne Übersetzung. Die Zeilen „Anbieter“ und „Abrechnung“ entfallen aus dem Block der Bestellseiten (der Anbieter steht im Impressum im Konto) und gehen als Kanzlei-Frage in die Rechtsrunde (N46). K01 und K12 behaupten für Betriebe kein „ausdrücklich verlangt“ mehr (Block `sofortverlangen`). Bewusst der Mail-/Textrunde (Spec 5.7) überlassen: Validierungssätze in `takeoverCredits.js`, `custom_text` der Stripe-Zahlungsseite, die übrigen Mails.
|
||||
(P7 Task 6/14, VF6, VF7, VF11; F37)
|
||||
|
||||
### Öffentliche Seiten und Login
|
||||
|
||||
- **N38 Kündigen und Widerrufen in du.** P7 Task 15 stellt `public/neu/oeffentlich/kuendigen.html` und `widerrufen.html` auf du um; `public/legal-form.js` und `public/auftrag-widerruf.js` zeigen du nur im Zweig `data-anrede="du"` (ohne Attribut unverändert). Die Seiten speichern keinen Wortlaut und keine `termsVersion`, es gibt also keine neue Version (C14). Text-Pins in `test/knOeffentlichDu.test.js` ersetzen für diese zwei Seiten den Paarvergleich aus P2 Task 17 (Feldattribute und Skripte werden weiter verglichen); P10 Task 17 stellt die Sie-Pins in `test/publicForms.test.js:1439-1450`, `test/auftraegeKundenportalWiderruf.test.js:9` und `test/auftraegeAblauf.test.js:288` auf diese du-Sätze um.
|
||||
(P7 Task 15, VF13; P2 Task 17; F6)
|
||||
- **N39 Neue öffentliche Seiten im Pilot.** Angemeldete Pilot-Sitzungen (`neueAnsicht(req)`) bekommen `/kuendigen.html` und `/widerrufen.html` schon im Pilot aus `public/neu/oeffentlich/`; ausgeloggte Besucher, der Login (`/login.html`) und `/offline.html` bleiben bis `on` alt. P0 setzt das mit zwei weiteren Zeilen in `dashboardWeiche.js` um (sie liefert nur, wenn die Datei existiert, sonst `next()`); P10 Task 3 übernimmt die Zeilen in `UMSCHALT_SEITEN` und passt den Test „off und pilot liefern die Datei von heute“ für diese zwei Seiten an.
|
||||
(P10 Task 3; P0 Task 3; P2 Task 17; F7)
|
||||
- **N40 Login, Service Worker, Auftragsseite.** P10 Task 16 (`public/neu/oeffentlich/login.js`) übernimmt `initLoginPage` samt Helfern (`app.js:421-568`) vor dem Löschen von `app.js`; die neue Hülle registriert `/sw.js` selbst (P1 Task 11), P10 zieht in `sw.js` die `OFFLINE_URL` mit. iOS-Fokuszoom: 16-px-Felder in P1/P2-CSS genügen, ein JS-Schutz nur im Login; der doppelte `beforeinstallprompt`-Hörer (Hinweis P2, Konto P1) darf P10 zusammenlegen. P10 nimmt `/auftrag.html` in `UMSCHALT_SEITEN` und in die Nachprüf-Marker auf (Marker: `/neu/oeffentlich/oeffentlich.css` im Quelltext, kein `/auftrag.css`); P2-Tests, die alte Dateien lesen (`app.js`, alte Seiten, `auftrag.css`, `chat-composer.css`), friert P10 vor dem Löschen ein oder entfernt sie.
|
||||
(P2 VF6; P10 Task 16/17, VF5; F26)
|
||||
|
||||
### Werkzeug und Vorschau
|
||||
|
||||
- **N41 Werkzeug-Erweiterungen durch P1 bis P4 und P7.** Erlaubt sind nur `views.mjs`, `gates.mjs` (Gate `p<N>`), `fixtures/endpunkte-p<N>.mjs` mit je einem Import in `fixtures/endpunkte.mjs`, `fixtures/faelle-p<N>.mjs` (P4: `fixtures/module.mjs`) mit einem Import in `fixtures/faelle.mjs`, `vertrag-endpunkte.mjs` und der Marker in `test/statik.test.mjs`. Neue Fälle hängen nur in eigenen Dateien hinter den vierzehn Kernfällen; P0 lockert den Pin „Fälle in fester Reihenfolge“ auf `FALL_NAMEN.slice(0, 14)` und den `boot.js`-Test auf diese vierzehn (`boot.js` bekommt `FALL_MUSTER = /^[a-z0-9-]{1,40}$/`, die Leiste zeigt nur Kernfälle). P4 (17 Fälle `m-*`) und P7 (`privat-ohne-abo`, `hosting-privat`) ändern Pin und `boot.js` nicht; ändert ein Paket eine Antwortform, ergänzt es den Vertragseintrag im selben Commit.
|
||||
(P0 Task 6/11, VF5; P1 VF2; P4 VF5; P7 Task 16, VF10; F14)
|
||||
- **N42 Strang-Umgebung und Ansicht-IDs.** Die Strang-Umgebung kommt nur über `KN_STRANG_ENV=…/tmp/strang-ui.env`, nie über `KW/strang.env`. Ansicht-IDs tragen das Paketpräfix (`kn-huelle-`, `kn-p2-`, `kn-funktionen-`, `kn-modul-`, `b7-`); wer fremde Ansichten aufruft, nennt die echten IDs (P7: `--only kn-p2-bestellen,kn-p2-bestellen-hosting,kn-p2-wechsel,kn-p2-email,kn-p2-email-plus,kn-p2-gutschrift,kn-p2-gutschrift-frage`). Fixtures für V5, V6 und V8 folgen den Nachträgen oben. Die Ansicht für `/auftrag.html` ergänzt P2 Task 19 mit einem Eintrag in `vorschau/seiten.mjs`.
|
||||
(P3 VF3; P2 VF7; P7 Task 16; F14)
|
||||
- **N43 Vorschau-Technik.** `dark-surface` erlaubt genau `#111` (Akzent) für Elemente bis 25 % der Viewport-Fläche; andere dunkle Flächen brauchen `ignorieren` je Ansicht. In der Vorschau hat `public/neu/oeffentlich/<seite>` Vorrang vor `public/<seite>`; `boot.js` feuert nach dem Laden einmal `DOMContentLoaded` und `load`, Code prüft trotzdem `document.readyState`. Der Arbeitsauftrag an den Inhaber (WOMS „Neues Projekt“, Subdomain `kundenansicht-neu`, Status `ready` oder `deployed`) steht in `KW/UEBERGABE-P0.md` und blockiert Phase B nicht; `KN_ERSTLAUF_FREMDES_OK=ja` nur auf ausdrückliche Anweisung des Controllers.
|
||||
(P0 Task 10/11/14, VF6 bis VF8; F15)
|
||||
- **N44 WOMS-Werkzeug.** „Playwright nur mit dem Werkzeug aus P0“ gilt für das Portal; für das WOMS gilt der eigene Strang `werkzeug-kundenart` (P9 Task 1). Ein kleiner Auftrag „WOMS-Werkzeug auf `test` heben“ (Aufträge/Projektkonten, Unterkunft, `arbeitsblaetter/abgerechnet`, Reiter „Abgesagt“) läuft vor Phase E. Beide Werkzeuge halten die Speicherbremse (`free -m` über 2000 MB) und starten nie mehr als einen Browser zugleich.
|
||||
(P9 Task 1, VF1; F41)
|
||||
|
||||
### Rechtsrunde
|
||||
|
||||
- **N45 Rechtsrunden-Technik.** Die drei fehlenden WOMS-Karten (`leistungsbeschreibung-news`, `leistungsbeschreibung-website-analyse`, `avv-website-analyse`) legt `scripts/rechtliches-hochladen.mjs --karten-anlegen` mit OK des Inhabers vor dem ersten Upload an; `--probe` meldet fehlende als `STOPP: Karte fehlt: <key>`, das Hochladen ohne Karte bricht ab. Die Karte der Analyse-Leistungsbeschreibung bekommt eine abgeleitete ID (`kartenId`, ihr Schlüssel hat 37 Zeichen, erlaubt sind 36), WOMS liest Karten über das Attribut `key`, und die drei Karten werden nicht in `setup-legal.mjs` eingetragen. Die Entwurfsseiten unter `/neu/rechtliches/` gehen mit dem normalen Push live (der Pilot braucht die Links), tragen Entwurfsmarke und `<meta name="robots" content="noindex">` (P8 Task 5 bis 10 ergänzen) und werden bei der Freigabe ersetzt.
|
||||
(P8 Task 11/13, VF6, VF7; F40)
|
||||
- **N46 Entscheidungsliste.** `docs/rechtliches/kundenansicht-entscheidungen.md` führt R1 bis R17 (statt R1 bis R15; `test/kundenansichtEntscheidungen.test.js` entsprechend): R16 ist die Du-Anrede der Seiten Kündigen und Widerrufen (N38), R17 sind die Zeilen „Anbieter“ und „Abrechnung“ im Block der Bestellseiten (N37), je mit Vorschlag, Alternative und Kanzlei-Frage.
|
||||
(P8 Task 11; P7 VF7; F6, F37)
|
||||
|
||||
### Livegang und P10
|
||||
|
||||
- **N47 Vor dem Pilot (Ablaufhinweis 4 ergänzt).** Zwei Schritte vor dem Push des Inhabers, beide mit Sicherung und OK des Inhabers: (1) Compose- und `.env`-Zeilen `KUNDENANSICHT_NEU` (Modus `pilot`) und `KUNDENANSICHT_NEU_PILOT_EMAILS` (Pilot-Liste) in `/home/webklar/apps/docker-compose.yml` (zwei Zeilen `- KUNDENANSICHT_NEU=${KUNDENANSICHT_NEU:-off}` und `- KUNDENANSICHT_NEU_PILOT_EMAILS=${KUNDENANSICHT_NEU_PILOT_EMAILS:-}` bei den `ZEITEN_*`-Zeilen) und `/home/webklar/apps/.env`, Sicherung `.bak-<Datum>-kundenansicht`, dann `docker compose up -d --no-deps kundenbereich-server`, nie `preview-portal`; (2) `scripts/kundenansicht-setup.mjs --pruefen`, nach OK des Inhabers ausführen (sonst scheitert jeder Nachweis ohne `featureConsents.kundenart` mit 500). `UEBERGABE-P0` und die Übergabe-Notiz tragen beide Punkte, P10 Task 9 prüft sie nur noch.
|
||||
(P0 UEBERGABE-P0; P7 VF9; P10 VF2, VF13, Task 9; Befund zur Übersicht; F9, F38)
|
||||
- **N48 Vor `on`.** P10 Task 1 und 9 prüfen: `WORTLAUT_STATUS` steht auf `'freigegeben'` (E20); die Gleichheitstests `test/knBestelltexte.test.js`, `test/knVerdrahtung.test.js` und `test/aboWortlautV6.test.js` sind grün; `scripts/rechtliches-hochladen.mjs --probe` meldet keine fehlende Karte und `--pruefen` endet mit Exit 0 (alle elf `documentKey` aus `DOKUMENTE_V6` haben eine aktuelle Version mit passender `source`, deshalb prüft Task 9 Step 2 mit `--pruefen` und nicht nur mit `--probe`); `scripts/kundenart-bestand.mjs` (P10 Task 8) und `scripts/kundenansicht-setup.mjs` sind gelaufen.
|
||||
(P7 VF9; P8 VF9; P10 VF9, VF13; F38)
|
||||
- **N49 Bestätigungen zu P10.** Die Alt-Zweige der P4-Dateien entfernt P10 Task 21, die Playwright-Skripte der alten Seite löscht Task 17. Zwischen Task 11 und Task 14 liegen sieben Tage Beobachtung; Task 14 ist der Punkt ohne Rückweg per Schalter und braucht ein eigenes OK des Inhabers. Der Rückbau (Task 20) beschränkt sich auf die beiden Kaufrouten und ihre Helfer; Alt-Preise in `config.js` und die Antwortfelder von `GET /api/funktionen` bleiben.
|
||||
(P10 VF4, VF6, VF8, VF10, VF11; F42)
|
||||
- **N50 Commit-Zeile (Global Constraints).** Jede ausführende Sitzung nutzt die `Co-Authored-By`-Zeile aus ihrem eigenen Systemhinweis. Commit-Beispiele in den Plänen mit `Claude Opus 5.5` und Formulierungen wie „wörtlich laut Übersicht“ stehen dafür als Platzhalter.
|
||||
(alle Pläne, Global Constraints „Commits“; P8 Schlussvermerk; F43)
|
||||
|
||||
### Pilot-Nachträge
|
||||
|
||||
- **N52 Kurzinfo in „Meine“ (V5, Pilot-Feedback 2026-10-17, Strang kn/meine-pilot).** Mit neuer Ansicht und nur auf Anfrage (`GET /api/funktionen?kurzinfo=1`, sendet allein „Meine“; Etagen, Einschalt-Ablauf und Module fragen ohne den Parameter, dann fehlt das Feld und nichts wird gelesen; Runde 1, 17.10.) trägt jede Funktion unter `projects[].features.<news|termine|shop|unterkunft|zeiten>` zusätzlich `kurzinfo: string | null`, den Text der zweiten Zeile in „Meine“. Nur aktive Funktionen bekommen einen Text, alle anderen `null`; die Analyse trägt kein Feld (ihre Zahlen kommen aus dem Auswertungsdienst, nicht aus Appwrite). Quelle `server/services/kundenansicht/kurzinfo.js`: Terminplaner „3 Termine heute · nächster 10:30“ bzw. „Heute keine Termine“ (bestätigte und erledigte Termine ohne Sperren, Berliner Tag), Webshop „2 neue Bestellungen“ bzw. „Keine offenen Bestellungen“ (Status `bezahlt`), News „4 Beiträge sichtbar“ bzw. „Keine Beiträge sichtbar“ (`isHanging`), Buchungskalender „1 offene Anfrage“ (Status `neu`), sonst „Nächste Anreise 24.10.“ („Anreise heute“, „Anreise morgen“), sonst „Keine offenen Anfragen“, Öffnungszeiten live der Status der Website (`zeitenStatus.text`, ohne eigene Abfrage; ohne gespeicherte Zeiten „Noch keine Öffnungszeiten eingetragen“). Je Funktion höchstens zwei schmale Abfragen (`limit`, `select`, Filter auf Index-Felder), alle gleichzeitig; jede Funktion einzeln auf `kurzinfoDeps.limitMs` (1500 ms) begrenzt, Fehler oder Zeitüberschreitung ergibt `null`, eine volle Seite (Zahl unbekannt) ebenfalls. Die Texte enthalten nie Namen, Adressen oder Beträge. Die Oberfläche zeigt bei `null` „Aktiv“ und setzt die Zeile nur als Text (höchstens 80 Zeichen); eine Pille trägt nur noch eine Zeile, deren Funktion nicht läuft. Ohne neue Ansicht bleibt die Antwort bytegleich und es wird nichts gelesen.
|
||||
(Pilot-Feedback 2026-10-17; `test/kundenansichtKurzinfo.test.js`, `test/knFunktionen*.test.js`)
|
||||
|
||||
### Zuständigkeit
|
||||
|
||||
- **N51 Zusätzliche Dateien und geteilte Stellen.** Die folgende Untertabelle ergänzt die Zuständigkeitstabelle um alle in Plänen und Vertragsfragen genannten Zusatzdateien und legt für die Hotspots eine feste Reihenfolge fest. Stubs aus P1 gehören nach der Anlage P2, P3 und P4; P7 übernimmt `ansicht-speicher.js` und `ansicht-gutschrift.js`; P0 besitzt `server/services/kundenansicht/` ganz.
|
||||
(P0 VF1; P1 VF3; P2 VF1; P4 Dateiübersicht; P5 VF4; P6 VF3; P7 VF1, VF2; P10 VF1; Befund zur Übersicht; F10)
|
||||
|
||||
| Datei / Stelle | Besitzer | Reihenfolge, Hinweis |
|
||||
|---|---|---|
|
||||
| `server/services/kundenansicht/` ganz (`schalter.js`, `dashboardWeiche.js`), `test/kundenansichtSchalter.test.js`, `kundenansichtDashboard.test.js`, `kundenansichtMe.test.js` | P0 | andere Pakete legen dort nichts ab |
|
||||
| Werkzeug `views.mjs`, `gates.mjs`, `fixtures/*`, `vertrag-endpunkte.mjs`, `vorschau/seiten.mjs` (eigenes Git) | P0 legt an | P1 bis P4 und P7 ergänzen nur nach N41 |
|
||||
| `public/neu/manifest.webmanifest`, `test/support/knHuelle.js`, `test/knHuelle*.test.js` | P1 | P2 bis P4 dürfen die Testhilfe nutzen |
|
||||
| Stubs aus P1: `public/neu/ansicht-{website,chat,vertraege,passwort,gutschrift,wechsel,bestellen}.js`, `ansichten.css`; `ansicht-funktionen.js`, `funktionen.css`; `module.css` | P1 legt an; danach P2, P3, P4 | Besitz geht nach der Anlage an das Paket der Tabelle oben |
|
||||
| `public/neu/dashboard.html` | P1 | P2 ändert genau zwei Zeilen (Composer-Skript, Stil `/neu/chat-composer.css`) |
|
||||
| `public/neu/ansicht-hilfen.js`, `rechnung-offen.js`, `install-hinweis.js`, `website-details.js`, `vertraege-rechnungen.js`, `vertraege-abo.js`, `chat-composer.css`, `oeffentlich/oeffentlich.css`, `oeffentlich/auftrag.html`, `test/support/knDaten.js`, `knWortlautOrakel.js`, `test/knAnsicht*.test.js` | P2 | `knWortlautOrakel.js` friert P10 vor dem Löschen von `app.js` ein |
|
||||
| `public/neu/ansicht-bestellen.js`, `ansicht-wechsel.js`, `ansicht-speicher.js`, `ansicht-gutschrift.js` | P2 baut 1:1, P7 baut um | P7 erst nach dem Merge von UI und Recht |
|
||||
| `public/neu/funktion-einschalten.js`, `test/support/knFunktionenDaten.js`, `test/knEinschalten*.test.js`, `knFunktionen*.test.js` | P3 | P7 Task 0 ändert nur `sichereAdresse` |
|
||||
| `public/neu/bilder/*` | P3 | `analyse.webp` erzeugt P4 |
|
||||
| `public/fn-reiter.js` (neu), `test/support/modulAbo.js`, `test/kundenansichtNeuModul*.test.js`, Pins `test/shopWiring.test.js:31`, `test/unterkunftWiring.test.js:130` | P4 | |
|
||||
| `server/services/aboUnterlagen.js`, `aboKatalog.js`, `funktionEinschalten.js` (neu), `featureRegistry.js` (auch `consentAbo*`, `abowegAnalyse`), `test/featureSetup.test.js:191-192`, `test/aboFassung6Verdrahtung.test.js` | P5 | Verdrahtung der Fassung 6 siehe N33 |
|
||||
| P5, minimal in fremden Dateien: `server/routes/admin/customers.js` (`code`), `server/services/ticketAdmin.js` (`abrechnung`), `server/services/zeiten/einschalten.js` (`abrechnung`, `vorpruefung`), `server/services/analyse.js`, `server/services/shop/anfrage.js` (`stelleAnfrage`), `server/routes/shop.js` (Anfrage) | P5 | P6 vermeidet diese Funktionen |
|
||||
| `server/services/aboModell.js`, `aboFunktionenMails.js`, `aboFunktionenMeldung.js`, `aboAufbauphase.js`, `aboFunktionenAbgleich.js` (alle neu) | P6 | Aufbauphasen-Regel siehe N28 |
|
||||
| Mailvorlagen K38 bis K43, T24; Block `funktionen_abo` in K02 und K03 | P6 | K44 und T25 frei (N29) |
|
||||
| P6, minimal in fremden Dateien: `server/routes/admin/funktionenAdmin.js`, `termine/aboAbgleich.js`, `zeiten/aboAbgleich.js`, `zeiten/mails.js`, `zeiten/behalten.js`, `termine/ticketDetails.js`, `termine/einbauanleitung.js`, `shop/ticketDetails.js`, `unterkunft/ticketDetails.js`, `shop/anfrage.js` und `unterkunft/anfrageFunktion.js` (nur `sendeEntscheidung`) | P6 | nur die in Plan 07 benannten Stellen |
|
||||
| `server/services/bestelltexte.js`, `duFehler.js`, `vertragsNachweis.js`, `public/neu/bestell-bausteine.js`, `test/support/knBestellDaten.js`, `test/kn*.test.js` (Plan 08) | P7 | |
|
||||
| P7, minimal in fremden Dateien: `server/services/stripeBilling.js` (`createAboCheckout`), `server/routes/takeoverCredits.js`, `server/mail-templates/K12 Wechsel Wartungs-Abo.html` (Block `sofortverlangen`), `public/neu/oeffentlich/kuendigen.html`, `widerrufen.html`, `public/legal-form.js`, `public/auftrag-widerruf.js` (nur `data-anrede="du"`) | P7 | |
|
||||
| `test/aboWortlautV6.test.js`, `aboBedingungenV6.test.js`, `legalV6Build.test.js`, `funktionenRechtV6.test.js`, `kundenansichtEntscheidungen.test.js`, `rechtlichesHochladen.test.js` | P8 | |
|
||||
| `server/routes/umschaltSeiten.js`, `scripts/umschalten-nachpruefung.mjs`, `scripts/kauf-bestand-pruefen.mjs`, `docs/umschalten-kundenansicht.md`, `test/support/dateiListe.js`, `modulKlassen.json`, `oeffentlichStand.json`, `public/neu/oeffentlich/login.js`, `public/sw.js:5`, Tests `knUmschalt*`, `knFassung6Fest`, `knLoginModul`, `knAufraeumen`, `knKaufRueckbau`, `knAltZweigeWeg`, `knSchalterWeg`, `knOfflineCache`, `kundenartBestand`, `kaufBestandPruefen` | P10 | Rückbau der Kaufpfade und Alt-Zweige erst in Phase F |
|
||||
| **geteilt:** `server/routes/abo.js` | P6, dann P7 | P6: K02/K03, `POST /switch`, `GET /`, `withdrawOrder`, Widerruf des Wechsels; P7: nur die Pfade der Fassung 6 und `router.use` für `duFehler` |
|
||||
| **geteilt:** `server/services/planSwitch.js` | P6, dann P7 | P6: `details.funktion`, `completeSwitch`, `markSwitchFailed`; P7: `switchToWartung` (Z. 347-351, 388, 483), `performSwitch` (`details`, K12-Aufruf) |
|
||||
| **geteilt:** `server/services/siteFeatures.js` | P5, dann P6 | P5: `featureView`, `legeTicketAn`; P6: `adminActivate`, `ticketNachlegen` |
|
||||
| **geteilt:** `server/index.js` | je eine eigene Zeile | P0 Weiche vor `express.static`; P5 1 Import, 1 Mount; P6 1 Import, 1 Intervallblock; P10 eine Zeile direkt vor `app.use(express.static(publicDir))` |
|
||||
| **geteilt:** `server/services/products.js` | P8, dann P10 | P8: Fassungstabelle, `WORTLAUT_V6` und Konstanten; P10: Zeilen v6 und v5 der Tabelle |
|
||||
| **geteilt:** `server/config.js` | je eigener Abschnitt | P0 `kundenansichtNeu`, P5 `aboFunktionen`, P8 `abo.termsVersionNeu`; Ausnahme: die Literale `aboFunktionen.fassung` und `.texte.beginn` setzt die Verdrahtung (N33) |
|
||||
| **geteilt:** `server/services/featureRegistry.js`, `funktionEinschalten.js` | P5 | `consentAbo` ändert nur die Verdrahtung (N33); P6 fügt in `funktionEinschalten.js` genau einen Aufruf `meldeEingeschaltet` ein |
|
||||
| **geteilt:** `public/neu/ansichten.css` | P2, dann P7 | P7 hängt einen Abschnitt an |
|
||||
| **geteilt:** `test/mailTemplates.test.js` | P6, dann P7 | P6: Z. 9, 11, 718; P7: Z. 379 |
|
||||
|
||||
---
|
||||
|
||||
## Verträge
|
||||
|
||||
### V1 Pilot-Schalter (P0)
|
||||
|
||||
`server/config.js`:
|
||||
```js
|
||||
kundenansichtNeu: { mode: 'off'|'pilot'|'on', pilotEmails: string[] } // aus KUNDENANSICHT_NEU, KUNDENANSICHT_NEU_PILOT_EMAILS (commaList)
|
||||
```
|
||||
|
||||
`server/services/kundenansicht/schalter.js`:
|
||||
```js
|
||||
export function neueAnsichtModus() // 'off' | 'pilot' | 'on'
|
||||
export function neueAnsichtFuerSession(session) // boolean
|
||||
export function neueAnsicht(req) // = neueAnsichtFuerSession(req.session ?? parsePortalSession(req))
|
||||
```
|
||||
|
||||
Regeln für `neueAnsichtFuerSession`:
|
||||
- `off` → false.
|
||||
- `on` → true für jede gültige Kunden- oder Kundenansicht-Sitzung.
|
||||
- `pilot` → true, wenn eine dieser Bedingungen gilt:
|
||||
- `session.viewAs === true && session.viewId` (Mitarbeiter-Kundenansicht),
|
||||
- die Liste enthält `'*'`,
|
||||
- die Liste enthält die getrimmte, klein geschriebene `session.email`.
|
||||
- Ohne Sitzung → false.
|
||||
|
||||
Auslieferung und Clients:
|
||||
- `app.get('/dashboard.html')` steht **vor** `express.static` (`server/index.js:154`). Gilt die neue Ansicht, liefert die Route `res.sendFile(public/neu/dashboard.html)` mit `Cache-Control: no-cache`. Sonst `next()`, und die alte Seite kommt wie heute.
|
||||
- `GET /api/auth/me` bekommt zusätzlich `ansicht: 'neu' | 'alt'`.
|
||||
- Die alte `app.js` lädt ohne Weiche. Die neue Seite lädt nur `/neu/*`-Dateien und die gemeinsamen Funktionsmodule.
|
||||
|
||||
### V2 Dateien und Module der neuen Oberfläche (P1, P2, P3)
|
||||
|
||||
**Grundregeln**
|
||||
- Alle neuen Dateien liegen unter `public/neu/`. ES-Module (`<script type="module" src="/neu/huelle.js">`), kein Build.
|
||||
- Wurzel `<body class="kn">`. Klassen und Tokens tragen das Präfix `kn-` bzw. `--kn-`.
|
||||
- Die gemeinsamen Funktionsmodule behalten ihre Klassen (`tm-*`, `sh-*`, `bk-*`, `zt-*`, `an-*`, `fn-*`, `kb-*`). Ihr neues Aussehen steht in `public/neu/module.css` (P4) unter `.kn`.
|
||||
|
||||
**`public/neu/dom.js`**
|
||||
```js
|
||||
export function h(tag, attrs = {}, ...kinder) // wie termine-dom.js/shop-dom.js; attrs.on = { click: fn }
|
||||
export function leeren(el)
|
||||
export function text(el, wert)
|
||||
```
|
||||
|
||||
**`public/neu/api.js`**
|
||||
```js
|
||||
export async function api(pfad, { method = 'GET', body, roh = false } = {})
|
||||
// Wirft ApiFehler { status, code, message, field, body }. Bei code 'session_revoked' Redirect auf /login.html.
|
||||
// Netzwerkfehler: status 0.
|
||||
export class ApiFehler extends Error {}
|
||||
```
|
||||
|
||||
**`public/neu/routen.js`** (rein, ohne DOM, testbar)
|
||||
```js
|
||||
export const BEREICHE = ['website', 'funktionen', 'chat', 'vertraege']
|
||||
export const ROUTEN // hash → { bereich, reiter?: string, etage?: string }
|
||||
export function routeAusHash(hash) // '#termine?bestellt=1' → { bereich:'funktionen', etage:'termine', query:'bestellt=1' }
|
||||
export function hashFuerRoute(route) // Umkehrung
|
||||
```
|
||||
|
||||
Pflichtinhalt von `ROUTEN`:
|
||||
- `''`/`'#'` → website
|
||||
- `projekt` → website, etage `projekt`
|
||||
- `fortschritt` → website (scrollt zur Aktivität)
|
||||
- `funktionen` → funktionen, reiter `meine`; `entdecken` → funktionen, reiter `entdecken`
|
||||
- `news`, `termine`, `webshop`, `buchungen`, `zeiten`, `analyse` → funktionen, etage = gleicher Name
|
||||
- `chat` → chat
|
||||
- `vertraege`, `abo`, `wartung` → vertraege, reiter `abo`
|
||||
- `rechnungen` → vertraege, reiter `rechnungen`
|
||||
- `stunden` → vertraege, reiter `stunden`
|
||||
- `speicher` (neu) → vertraege, reiter `email`
|
||||
- `gutschrift` → vertraege, etage `gutschrift`
|
||||
- `wechsel` → vertraege, etage `wechsel`
|
||||
- `bestellen-hosting`, `bestellen-wartung` → vertraege, etage `bestellen`, plus Produkt
|
||||
- `passwort` → website, etage `passwort` (Rückweg = Herkunft)
|
||||
|
||||
**`public/neu/router.js`**
|
||||
```js
|
||||
export function starteRouter({ zeige }) // liest location.hash, hört auf popstate, ruft zeige(route, { nav: 'tab'|'push'|'back'|'same' })
|
||||
export function geheZu(hash, { ersetzen = false } = {}) // Reiterwechsel: replaceState; Etage 2, Sheet, Seiten: pushState
|
||||
```
|
||||
|
||||
**Hülle**
|
||||
- `huelle.js` (Einstieg): lädt `/api/auth/me` und leitet ab wie heute.
|
||||
- `leiste.js`: Fahrstuhl-Zeile Handy, Menü links am Desktop.
|
||||
- `konto.js`: Konto-Sheet bzw. Popover.
|
||||
- `fristen-streifen.js`, `zaehler.js`.
|
||||
- `etage.js`: Etage 2 mit Zurück und Reitern.
|
||||
- `reiter.js`: gemeinsame Unterstrich- und Balken-Reiter.
|
||||
|
||||
**Ansichten.** Jede exportiert:
|
||||
```js
|
||||
export async function zeigeX(wurzel, { route, me, nav }) // rendert in wurzel, darf Daten selbst laden
|
||||
```
|
||||
|
||||
**Funktionsmodule** werden von der neuen Hülle wie heute aufgerufen: `initTermine(document.getElementById('termine-body'))` usw. Die Projektwahl übergibt die Hülle einheitlich als `wurzel.dataset.wunschProjekt` (C1). News und Analyse lesen dasselbe Attribut zusätzlich zu ihren bisherigen Modulvariablen (P4).
|
||||
|
||||
### V3 Fahrstuhl-Zeile und Hülle (P1)
|
||||
|
||||
- **Referenz** ist `/root/reports/kundenansicht-neu-2026-10-15/handy-leiste/final-2-frei.html`: Balken mit Overlay-Kopie und Gegenbewegung, Rückfall `fb`, Etage 2 als `reel` mit `translateY(-56px)`, Blatt `t2-dp`.
|
||||
- **Handy-Layout** gilt mit derselben Medienabfrage wie heute: `(max-width: 880px), (pointer: coarse) and (max-height: 500px)`.
|
||||
- **Leiste:**
|
||||
- Wörter: `Website`, `Funktionen`, `Chat`, `Verträge`. Schrift 15 px/600, bei Platzmangel 14 px.
|
||||
- Balken: Radius 6, Höhe 36 px.
|
||||
- Tap-Ziele mindestens 44 px. Unten `env(safe-area-inset-bottom)`.
|
||||
- Bei offener Tastatur wird die Leiste ausgeblendet. Die `visualViewport`-Logik von heute wird übernommen.
|
||||
- **Etage 2:** „Zurück“ und die Reiter der Unterseite. Hat die Unterseite keine Reiter, füllt „Zurück“ die ganze Zeile. Bei Platzmangel scrollt die Zeile waagerecht, und der aktive Reiter wird in den sichtbaren Bereich gerückt (C2).
|
||||
- **Desktop:** Menü links mit 232 px und Haarlinie rechts, aktiver Eintrag schwarz. Unterseiten zeigen eine Brotkrume „Funktionen / Terminplaner“. Oben rechts das Profilbild, das Konto öffnet als Popover.
|
||||
- **Konto** (Reihenfolge laut Spec 5.2):
|
||||
1. Name und E-Mail
|
||||
2. Passwort ändern
|
||||
3. Datenschutz
|
||||
4. Impressum
|
||||
5. App installieren (nur wenn sinnvoll)
|
||||
6. Verträge kündigen (`/kuendigen.html`)
|
||||
7. Vertrag widerrufen (`/widerrufen.html`, immer da; hervorgehoben mit der Pille „Frist läuft“, wenn `GET /api/fristen` `anzeigen` liefert)
|
||||
8. Abmelden
|
||||
- **Widerrufsstreifen:** erscheint, wenn `anzeigen === true`, mit dem Text `Widerrufsfrist bis TT.MM. · Widerrufen` (früheste Frist, V6). Nicht wegtippbar, bei offener Tastatur ausgeblendet.
|
||||
- **Zähler:**
|
||||
- Chat: `GET /api/chat/unread-count`, Poll alle 45 s wie heute.
|
||||
- Verträge: Zahl der Rechnungen mit `balance > 0` aus `GET /api/invoices` plus Rückfragen aus `GET /api/abo/takeover-credits` (`status === 'rueckfrage'`). Beides wird nach einer Zahlung, bei der Rückkehr in den Tab und beim Reiterwechsel neu geladen.
|
||||
- „!“ steht bei `subscription.paymentProblem`. **Nicht** bei `switchPending`.
|
||||
- **Bewegung reduziert:** keine Federn, nur 150 ms Überblenden.
|
||||
|
||||
### V4 Kundenart (P5)
|
||||
|
||||
`server/services/kundenart.js`:
|
||||
```js
|
||||
export const KUNDENARTEN = ['unternehmen', 'privat'] // Re-Export aus auftraege/konstanten.js
|
||||
export function kundenartGespeichert(customerDoc) // 'unternehmen' | 'privat' | ''
|
||||
export function kundenartWirksam(customerDoc) // 'unternehmen' | 'privat' (leer/unbekannt → 'privat')
|
||||
```
|
||||
|
||||
**Wo die Kundenart erscheint**
|
||||
- `GET /api/auth/me` liefert in `customer` zusätzlich `kundenart: kundenartGespeichert(doc)` und `kundenartWirksam: kundenartWirksam(doc)`. Das gilt für alle drei Zweige (Login, Kundenansicht, Kunde).
|
||||
|
||||
**Anlegen und Ändern (`customerAdmin.js`)**
|
||||
- `createCustomerWithPortalAccess(payload)` nimmt `kundenart`. Pflichtwert ist `'unternehmen' | 'privat'`, sonst 400 mit `code: 'kundenart_fehlt'`.
|
||||
- `updateCustomerWithPortalAccess` nimmt `kundenart` optional, mit gleicher Prüfung.
|
||||
- `sanitizeCustomer` gibt `kundenart` zurück.
|
||||
|
||||
**Nachweis**
|
||||
- `featureConsents` bekommt das Attribut `kundenart` (string 12). Es wird in `scripts/kundenansicht-setup.mjs` angelegt und von `test/support/schema.js` gelesen.
|
||||
- Jede neue Zustimmung speichert `kundenartWirksam` zum Klickzeitpunkt.
|
||||
|
||||
### V5 Katalog und Einschalten (P5)
|
||||
|
||||
**Abo-Zuordnung.** `server/services/aboFunktionen.js`:
|
||||
```js
|
||||
export const ENTHALTEN_IN = Object.freeze({ analyse: ['hosting','wartung'], news: ['wartung'], termine: ['wartung'], shop: ['wartung'], unterkunft: ['wartung'], zeiten: ['wartung'] })
|
||||
export async function aboProdukt(customerId) // { produkt: 'hosting'|'wartung'|null, laeuft: boolean, gekuendigt: boolean, widerrufsfristBis: string|null /*ISO, nur Abschluss/Wechsel*/ }
|
||||
export async function aboDeckt(customerId, key) // { ok: true } | { ok: false, grund: 'kein_abo'|'falsches_abo'|'gekuendigt'|'nicht_aktiv' }
|
||||
```
|
||||
- `laeuft` gilt für die Status `active | trialing | past_due`, wie `ACTIVE_STATUSES`.
|
||||
- `gekuendigt` bedeutet „Kündigung vorgemerkt“. `aboDeckt` liefert dann `gekuendigt`, eingeschaltet wird nicht.
|
||||
|
||||
**`GET /api/funktionen`.** Ohne neue Ansicht bleibt die Antwort unverändert. Mit neuer Ansicht kommen hinzu:
|
||||
```js
|
||||
modell: 'abo',
|
||||
kundenart: 'unternehmen'|'privat',
|
||||
abo: { produkt, laeuft, gekuendigt, widerrufsfristBis },
|
||||
katalog: [{
|
||||
key, // Reihenfolge: news, termine, shop, unterkunft, zeiten, analyse
|
||||
name, beschreibung, // beschreibung: höchstens 2 Sätze, du-Form
|
||||
bild: '/neu/bilder/<key>.webp',
|
||||
enthaltenIn: 'hosting'|'wartung', // kleinstes Abo, das die Funktion enthält
|
||||
verfuegbar: boolean, // Schalter der Funktion für diesen Kunden (off → false, Pilot → Liste)
|
||||
ablauf: 'direkt'|'fragebogen'|'anfrage', // termine: fragebogen; shop, unterkunft: anfrage; sonst direkt
|
||||
einrichtung: string, // Dauer, z. B. 'meist in 1–2 Wochen'
|
||||
unterlagen: { leistungsbeschreibung: {version,url}|null, avv: {version,url}|null },
|
||||
nurBetrieb: boolean // shop: true (C15)
|
||||
}]
|
||||
```
|
||||
- Je Projekt und Funktion zusätzlich `anfrageStatus: ''|'offen'|'angenommen'|'abgelehnt'|'zurueckgezogen'` (nur Shop und Unterkunft).
|
||||
- Auf Anfrage (`?kurzinfo=1`, nur „Meine“) je Projekt und Funktion zusätzlich `kurzinfo: string | null` (Live-Detail der Zeile in „Meine“, nur aktive Funktionen, Fehler → `null`; ohne den Parameter fehlt das Feld; N52).
|
||||
- Funktionen mit `verfuegbar: false` stehen trotzdem im Katalog. Die Oberfläche blendet sie aus (C9).
|
||||
- Die Analyse erscheint im Katalog nur, wenn `ANALYSE_ENABLED === 'on'`.
|
||||
|
||||
**`POST /api/funktionen/:key/einschalten`.** Ohne neue Ansicht → 404. In der Kundenansicht → 403. Für `analyse` → 404, die Analyse nutzt weiter `POST /api/funktionen/analyse/activate`.
|
||||
|
||||
Body:
|
||||
```js
|
||||
{ projectId, zustimmung: { version, haken: [] | ['beginn'] } }
|
||||
```
|
||||
|
||||
Antwort 200:
|
||||
```js
|
||||
{ feature: featureView(doc, key), ticketWoid }
|
||||
```
|
||||
|
||||
Fehler 409 mit `code`, Prüfreihenfolge:
|
||||
1. `nicht_verfuegbar` (Schalter)
|
||||
2. `abo_fehlt` mit `body.grund` und `body.wechsel: true`, wenn Hosting läuft und die Funktion Wartung braucht
|
||||
3. `nur_betrieb` (Shop bei privat)
|
||||
4. `bestellung_offen` mit `body.offen`, wie heute: Fragebogen beim Terminplaner, Anfrage nicht angenommen bei Shop und Unterkunft
|
||||
5. `beginn_noetig`, wenn `kundenartWirksam === 'privat'`, die Widerrufsfrist des Abos läuft und `haken` nicht `'beginn'` enthält; `body.ab` nennt das ISO-Datum nach Fristende
|
||||
6. `bereits_eingeschaltet` (Zustand `setup` oder `active`)
|
||||
7. `fassung_veraltet`
|
||||
|
||||
Erfolg:
|
||||
- `einschalten({ customerId, projectId, key, by: 'kunde', abrechnung: 'abo', nachweis, now })` (V7).
|
||||
- Der Nachweis kommt in `featureConsents` mit Texten. Pflicht sind `einschalten-zeile` (Linkzeile) und `knopf` (`Einschalten`). Das Häkchen `beginn` kommt nur dazu, wenn es nötig war.
|
||||
|
||||
**Zustimmungs-Register für das Abo-Modell.** Jede Funktion im Register bekommt `consentAbo`:
|
||||
```js
|
||||
{ version, knopf: 'Einschalten', items: [{ id: 'einschalten-zeile', text, haken: false }, { id: 'beginn', text, haken: true, nurPrivatInFrist: true }] }
|
||||
```
|
||||
- Den Wortlaut von `beginn` legt P8 fest. Bis dahin gilt der Entwurf aus Spec 5.6, Punkt 3, für die Einrichtung.
|
||||
- Das Item `gesundheit` entfällt im Abo-Modell. Der Fragebogen sperrt den Gesundheitsbereich weiter.
|
||||
|
||||
**Anfragen im Abo-Modell**
|
||||
- Shop: `unternehmer` wird nicht mehr vom Client verlangt, sondern aus `kundenartWirksam === 'unternehmen'` gesetzt. Privat führt zu 409 `nur_betrieb`.
|
||||
- `keineAusgeschlossenen` bleibt Pflicht als Satz über dem Knopf. Der Client sendet `true` beim Klick auf „Anfrage senden“, der Nachweis kommt in die Anfrage.
|
||||
|
||||
### V6 Fristen (P5)
|
||||
|
||||
`server/services/fristen.js`:
|
||||
```js
|
||||
export async function sammleFristen(customerId, { now = new Date() } = {})
|
||||
// → { fristen: [{ quelle: 'abschluss'|'wechsel'|'speicher'|'funktion'|'projekt', bezeichnung, bis /*ISO*/, featureKey?, projektName? }],
|
||||
// fruehestesEnde: string|null, anzeigen: boolean }
|
||||
```
|
||||
- `anzeigen` ist wahr, wenn `kundenartWirksam(customer) === 'privat'` gilt und `fristen.length > 0`.
|
||||
- Quellen: `openWithdrawals` (Abo), `paidFeatureOrders` + `withdrawalDeadline` (gekaufte Funktionen) und die Aufträge (nur bei `AUFTRAEGE_ENABLED` und privat).
|
||||
|
||||
Route `GET /api/fristen` (`server/routes/fristen.js`, Mount `/api/fristen`, `requireSession`). Antwort wie oben. In der Kundenansicht ist der Aufruf erlaubt, weil er lesend ist.
|
||||
|
||||
### V7 Generisches Einschalten und Tickets (P5, P6, P9)
|
||||
|
||||
`server/services/funktionEinschalten.js`:
|
||||
```js
|
||||
export async function einschalten({ customerId, projectId, key, by, abrechnung, nachweis = null, staff = null, now = new Date() })
|
||||
// by: 'kunde'|'team'; abrechnung: 'abo'|'projekt'
|
||||
// → { doc, ticketWoid }
|
||||
```
|
||||
- Setzt `state: 'setup'`, `unlockedBy: abrechnung === 'projekt' ? 'projekt' : 'abo'` und `unlockedByPurchase: false`.
|
||||
- Legt das Ticket über `createFeatureTicket` an, unter der Sperre `feature-order:${customerId}:${projectId}`.
|
||||
- Für `zeiten` nutzt die Funktion den bestehenden Weg (`einschaltenZeiten`, mit `unlockedBy` je nach `abrechnung`).
|
||||
|
||||
**Ticket-Attribute** (`workorders`, angelegt in `scripts/kundenansicht-setup.mjs`):
|
||||
- `einrichtungFuer` (string 16): Funktionsschlüssel
|
||||
- `einrichtungAbrechnung` (string 8): `'abo' | 'projekt' | ''`
|
||||
|
||||
Gesetzt werden sie von `createFeatureTicket`, sobald der Aufrufer `abrechnung` übergibt (neuer optionaler Parameter). Der Ticket-Text sagt bei `abo`: „Zeit im Arbeitsblatt erfassen und aufs Abo-Stundenkonto anrechnen (Einrichtung aus dem Abo, Vorschuss möglich).“ Bei `projekt`: „Zeit im Arbeitsblatt erfassen, nicht aufs Abo anrechnen (im Projekt enthalten).“
|
||||
|
||||
**`unlockedBy`** hat die Werte `'kauf'|'abo'|'team'|'projekt'`. `unlockedByVon` und `featureView` liefern `unlockedBy` künftig für **alle** Funktionen, nicht nur für `aboInklusive`.
|
||||
|
||||
### V8 Stundenkonto mit Vorschuss (P6)
|
||||
|
||||
`computeLedger(credits, usage, opts)`:
|
||||
- Ein Eintrag mit `einrichtung === true` ist gesetzt, wenn das Arbeitsblatt zu einem Ticket mit `einrichtungAbrechnung === 'abo'` gehört. Den Join macht `listPlanUsage`, auch für Git-Blätter.
|
||||
- Der ungedeckte Rest eines solchen Eintrags zählt als `vorschussMinutes`, nicht als `overageMinutes`.
|
||||
- Gedeckt wird wie heute: FIFO, rückwirkend, `expiresAt` nach der Arbeitszeit. **Neu:** Gutschriften, die erst nach der Arbeitszeit gebucht wurden, decken Einrichtungseinträge vor jedem anderen Eintrag. So gleicht eine spätere Gutschrift zuerst den Vorschuss aus.
|
||||
- Ergebnis zusätzlich:
|
||||
```js
|
||||
{ vorschussMinutes, usage: [{ ..., einrichtung: boolean, vorschussMinutes }] }
|
||||
```
|
||||
- Bei `active === false` bleibt `vorschussMinutes` stehen. Die Oberfläche zeigt dann „verfällt, wird nicht berechnet“.
|
||||
- Team-Endpunkt `GET /api/team/customers/:id/stundenkonto` (`requireStaffJwt`):
|
||||
```js
|
||||
{ produkt: 'hosting'|'wartung'|null, ledger: { balanceMinutes, vorschussMinutes, overageMinutes, nextExpiry } | null }
|
||||
```
|
||||
|
||||
### V9 Kundenart und Abo-Vorgabe im WOMS (P9 gegen V4, V7, V8)
|
||||
|
||||
- **Pflichtfeld Kundenart**, ohne Vorgabe. Die Auswahl erfolgt per Segment-Schalter `.seg` mit den Labels „Unternehmen“ und „Privatperson“, Werte aus `KUNDENART_OPTIONEN` ohne den Leer-Eintrag. Es steht an diesen Stellen:
|
||||
- `CreateLeadForm`: `createLead` schreibt `kundenart`.
|
||||
- `CreateCustomerForm`: Body an `POST /api/admin/customers` mit `kundenart`.
|
||||
- „Zum festen Kunden machen“: Upgrade nur mit gesetzter Kundenart.
|
||||
- Arbeitsblatt-Fluss „Zugesagt“ (`worksheetFlow.js`): Ohne Kundenart wird nicht hochgestuft, das Team bekommt einen Hinweis.
|
||||
- **Kundenliste:** Schalter „Kundenart fehlt“ in der Toolbar, kein neuer Reiter.
|
||||
- **Arbeitsblätter:** Bei Tickets mit `einrichtungAbrechnung === 'abo'` steht `countsToPlan` standardmäßig auf an, im Modal und beim Nachtragen der Zeit eines Git-Blatts. Bei `'projekt'` steht es auf aus.
|
||||
- **Ticket-Seite eines Einrichtungs-Tickets:** zeigt Kontostand und Vorschuss aus V8.
|
||||
- **Funktionen-Reiter:**
|
||||
- Kein Preis mehr (`funktionenView.js:22`).
|
||||
- „Aktivieren“ fragt „aus dem Abo“ oder „im Projekt enthalten“ und sendet `abrechnung`.
|
||||
- Die Abo-Prüfung macht der Server (V10). Antwortet er mit `abo_fehlt`, zeigt WOMS den Grund.
|
||||
|
||||
### V10 Team-Aktivierung (P6)
|
||||
|
||||
`POST /api/team/funktionen/:customerId/:key` mit `action: 'activate'` nimmt zusätzlich `abrechnung: 'abo'|'projekt'` (Vorgabe `'abo'`).
|
||||
- Ohne `aboDeckt(customerId, key).ok` → 409 mit `code: 'abo_fehlt'` und `grund` (E21). Ausnahme: Dokumente mit `unlockedBy === 'kauf'` (Bestand).
|
||||
- Die Aktivierung läuft über `einschalten({ by: 'team', abrechnung, staff })` mit Ticket. Wie bisher wird nicht direkt `active` gesetzt. Ausnahme: `action: 'activate'` auf einen Zustand `disabled` mit `disabledReason === 'team'` setzt wie heute direkt `active`.
|
||||
- Die Textform-Regeln bleiben, wie sie heute sind (`textformFuerAktivierung`).
|
||||
|
||||
### V11 Wechsel mit gemerkter Funktion (P6)
|
||||
|
||||
- `POST /api/abo/switch` nimmt optional `funktion: { key, projectId }`, aber nur mit neuer Ansicht. Ohne neue Ansicht wird das Feld ignoriert. Es wird in `details.funktion` des Ereignisses `wechsel` gespeichert.
|
||||
- `GET /api/abo` liefert in `subscription` zusätzlich `wechselFunktion: { key, projectId } | null`, nur mit neuer Ansicht:
|
||||
- gesetzt, wenn das jüngste `wechsel`-Ereignis `erledigt` ist, `details.funktion` hat und diese Funktion für das Projekt noch `off` ist, längstens 14 Tage nach `doneAt`;
|
||||
- bei `gescheitert` oder `widerrufen` → `null`.
|
||||
- Ein Widerruf des Wechsels beendet zusätzlich alle Funktionen mit `unlockedBy === 'abo'`, deren Schlüssel nur in `wartung` steckt (`disable(..., { reason: 'abo_ende' })`).
|
||||
- **Client** (P3 → P2/P7): Antwortet „Einschalten“ mit `abo_fehlt` und `wechsel: true`, schreibt die Oberfläche `sessionStorage['kn-wechsel-funktion'] = JSON.stringify({ key, projectId })` in einem try/catch und geht zu `#wechsel`. `ansicht-wechsel.js` sendet den Wert als `funktion` mit und löscht ihn nach Erfolg. Steht in `GET /api/abo` ein `subscription.wechselFunktion`, öffnet `ansicht-funktionen.js` beim nächsten Aufruf den Einschalt-Ablauf dieser Funktion.
|
||||
|
||||
### V12 Abo-Fassung 6 und Bestellseiten (P8, P7)
|
||||
|
||||
`server/config.js` (P8):
|
||||
```js
|
||||
abo.termsVersionNeu = '<JJJJ-MM-TT>-v6' // das Datum setzt P8; config.abo.termsVersion bleibt v5
|
||||
```
|
||||
|
||||
`server/services/products.js` (P8): Statt `endsWith` steht dort eine Fassungstabelle.
|
||||
```js
|
||||
export const FASSUNGEN // [{ muster: /-v6$/, url:'/abo-bedingungen-v6.html'?, pdfFile, endParagraph }, { muster: /-v5$/, ... v5-Archiv }, { muster: /-v4$/, ... }, { sonst: v3 }]
|
||||
export function aktuelleAboFassung(req) // neueAnsicht(req) ? config.abo.termsVersionNeu : config.abo.termsVersion
|
||||
export function isTermsV4OrNewer(t) // v4, v5, v6
|
||||
```
|
||||
- `termsDocFor` und `contractAttachmentFor` lesen aus der Tabelle.
|
||||
- Die Seite der Fassung 6 entsteht unter einem eigenen Pfad, solange der Schalter nicht `on` ist. Erst P10 macht sie zu `/abo-bedingungen.html`, nach Archivierung von v5 unter `public/vertragsunterlagen/v5/`. Den Pfad legt P8 fest und trägt ihn in diese Übersicht nach.
|
||||
|
||||
Server-Regeln für Fassung 6 (P7). Sie gelten nur, wenn der Body `termsVersion === config.abo.termsVersionNeu` trägt und `neueAnsicht(req)` gilt:
|
||||
|
||||
**`POST /api/abo/checkout`**
|
||||
- `acceptTerms === true` (der Client sendet es beim Klick auf „Zahlungspflichtig bestellen“), dazu `earlyStart: boolean`.
|
||||
- `unternehmen` → `startChoice: 'sofort'`, `earlyStartConsentAt: ''`, Grund `kundenart: 'unternehmen'` im Ereignis.
|
||||
- `privat` und `earlyStart` → `sofort` mit `earlyStartConsentAt`.
|
||||
- `privat` ohne `earlyStart` → `nach_frist`.
|
||||
|
||||
**`POST /api/abo/switch`**
|
||||
- `earlyStart` ist nur bei `privat` Pflicht.
|
||||
- Bei `unternehmen` gilt der Wechsel ohne `earlyStartConsentAt`, der Grund steht im Ereignis. `planSwitch.js:351` bekommt dafür den Parameter `kundenart`.
|
||||
|
||||
**`POST /api/abo/storage`:** unverändert. Das Plus sendet `acceptTerms: true` beim Klick.
|
||||
|
||||
**Nachweis.** `server/services/vertragsNachweis.js`:
|
||||
```js
|
||||
export async function speichereVertragsNachweis({ customerId, art: 'abo'|'wechsel'|'speicher'|'gutschrift', bezug, texte, fassungen, kundenart, req, at })
|
||||
```
|
||||
- Speichert in `featureConsents` mit `featureKey = art` und `projectId = ''`.
|
||||
- `stripeSessionId` ist die Sitzungs-ID, wenn vorhanden.
|
||||
- `texte` = die angezeigten Zeilen, Häkchen und Knopf.
|
||||
|
||||
### V13 Mails (P6)
|
||||
|
||||
- Neue Kennungen K38–K41 und T24–T25 (frei laut `test/mailTemplates.test.js`). Belegt werden:
|
||||
- **K38** „Deine Funktion wird eingerichtet“ (Einschalten im Abo-Modell)
|
||||
- **K39** „Deine Funktion wurde beendet“ (Abo-Ende für News, Shop, Unterkunft im Abo-Modell; Platzhalter `funktion`, `projekt`, `loeschtag`)
|
||||
- **T24** „Funktion eingeschaltet (Abo)“ ans Team, mit Block `vorschuss`, wenn das Guthaben nicht reicht
|
||||
- K40, K41 und T25 bleiben frei für P6, falls nötig. P6 trägt die Belegung nach.
|
||||
- Alle neuen Vorlagen sind in „du“-Form und im Layout von K16.
|
||||
|
||||
---
|
||||
|
||||
## Ablaufhinweise für den Controller
|
||||
|
||||
1. **P0 zuerst.** P0 endet mit dem Tag `kn-p0` und dem Arbeitsauftrag an den Inhaber: In WOMS „Neues Projekt“ (Website, interner Kunde) mit Subdomain `kundenansicht-neu` anlegen, damit `https://kundenansicht-neu.project.webklar.com` existiert. Vorher wird die Vorschau nicht ausgeliefert.
|
||||
2. **Phase B:** vier Stränge parallel. P9 baut gegen V4, V7, V8 und V10, mit dem Testbackend der WOMS-Vorschau (Whitelist wie `customerAdmin.js`, siehe Karte woms-vorschau Fund 2).
|
||||
3. **Merges** in den Hauptzweig nach der Reihenfolge oben. Danach jeweils die betroffenen Testdateien mit den Befehlen aus den Global Constraints.
|
||||
4. **Phase E:**
|
||||
- ganze Suite Portal und WOMS
|
||||
- Werkzeug-Gates aller Ansichten auf Handy, Tablet und Desktop
|
||||
- Vorschau aktualisieren (nur `cp`)
|
||||
- Abschlussprüfung mit vier Blickwinkeln: Recht und Nachweise, alte Ansicht unverändert, Handy und Barrierefreiheit, Abo-Modell und Stundenkonto
|
||||
- Korrekturen
|
||||
- **Vor dem Push des Inhabers, beide mit Sicherung und OK des Inhabers (Nachtrag N47):**
|
||||
- Compose- und `.env`-Zeilen `KUNDENANSICHT_NEU` (Modus `pilot`) und `KUNDENANSICHT_NEU_PILOT_EMAILS` (Pilot-Liste) anlegen, Sicherung `.bak-<Datum>-kundenansicht`, dann `docker compose up -d --no-deps kundenbereich-server`, nie `preview-portal`. `pilot` erst setzen, wenn WOMS `kn-p9-kundenart` live ist (Nachtrag N24).
|
||||
- `scripts/kundenansicht-setup.mjs --pruefen`, nach OK des Inhabers ausführen (sonst scheitert jeder Nachweis ohne `featureConsents.kundenart` mit 500).
|
||||
- Übergabe-Notiz unter `/root/reports/kundenansicht-neu-2026-10-15/uebergabe/` mit den Push-Befehlen für den Inhaber, der Push-Reihenfolge (N24) und den beiden Schritten oben
|
||||
5. **Rechtsrunde:** P8 legt `docs/rechtliches/kundenansicht-entscheidungen.md` an. Der Controller geht die Liste mit dem Inhaber durch (E20). Hochgeladen wird erst nach seiner Freigabe, mit `scripts/rechtliches-hochladen.mjs`, zuerst `--probe`.
|
||||
6. **P10** startet erst, wenn der Inhaber ausdrücklich „umschalten“ sagt.
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,412 @@
|
||||
# Kundenansicht neu: weiß, schlank, Funktionen im Abo
|
||||
|
||||
Stand 15.10.2026. Brainstorming mit dem Inhaber (Kenso) in dieser Sitzung. Basis ist `origin/main` d33a483.
|
||||
Bestandsaufnahme (nur gelesen) unter `/root/reports/kundenansicht-neu-2026-10-15/bestand/`:
|
||||
- `ansichten.md`: Karte aller 18 Ansichten, Navigation, Design heute
|
||||
- `bestaetigungen.md`: jedes Häkchen mit Datei:Zeile, Server-Prüfung, Nachweis
|
||||
- `funktionen.md`: Katalog, Zustände, Einstieg, Reiter je Funktion
|
||||
- `woms-design.md`: Tokens und Regeln des weißen Ticketsystems
|
||||
- `recht.md`: Mindestanforderungen mit Quellen, Fragen an die Kanzlei
|
||||
|
||||
Navigations-Entwürfe (Workflow mit 5 Entwürfen, 2 Prüfern, 3 Nachschärfungen) liegen unter `/root/reports/kundenansicht-neu-2026-10-15/handy-leiste/`. Gewählt wurde `final-2-frei.html`, die „Fahrstuhl-Zeile“.
|
||||
|
||||
Kennzeichnung:
|
||||
- **[Inhaber]** hat Kenso entschieden.
|
||||
- **[Claude]** ist eine Festlegung von mir, die er gesehen und nicht verworfen hat.
|
||||
- **[Bauer]** ist dem Umsetzer überlassen.
|
||||
|
||||
---
|
||||
|
||||
## 1. Absicht und Warum
|
||||
|
||||
Die Kundenansicht (`project.webklar.com`, `public/dashboard.html` + `app.js` + Funktionsmodule) ist über Monate gewachsen. Heute ist sie dunkel und lila, hat 18 Ansichten und viele Erklärsätze. Beim Buchen einer Funktion stehen bis zu sechs Häkchen mit Paragrafen-Text und die ganze Widerrufsbelehrung als Scrolltext. Der Inhaber sagt dazu: „bei den Häkchen beim Funktionen buchen, viel zu viel Text“.
|
||||
|
||||
Die Kunden sind fast nur kleine Betriebe (Friseur, Bäckerei, Ferienwohnung, Physio), Privatkunden kaum. Viele nutzen das Portal 1–2× im Monat als installierte Web-App auf dem Handy.
|
||||
|
||||
Das Ziel hat drei Teile:
|
||||
1. Die Kundenansicht bekommt den Stil des neuen weißen Ticketsystems (WOMS, live seit 06.10.2026): minimal, aufgeräumt, wenig Text.
|
||||
2. Die Funktionen werden nicht mehr einzeln verkauft. Sie stecken im Abo.
|
||||
3. Bestätigungen werden auf das rechtlich Nötige reduziert. Ein Betrieb soll nichts „durchklicken“ müssen, und trotzdem bleibt alles rechtlich sauber.
|
||||
|
||||
Die Handy-Navigation soll laut Inhaber „zeitlos, modern, mit Wow-Effekt“ sein.
|
||||
|
||||
## 2. Ziele, Nicht-Ziele, Anti-Ziele
|
||||
|
||||
**Ziele**
|
||||
- Eine weiße Kundenansicht, die zum Ticketsystem passt, auf Handy und Desktop.
|
||||
- Eine Handy-Leiste mit vier Wörtern und federndem schwarzem Balken. Die Unterseiten fahren als „Etage“ hoch.
|
||||
- Funktionen mit den Reitern „Meine“ und „Entdecken“, ohne Preise, mit dem Knopf „Einschalten“.
|
||||
- Ein Betrieb setzt beim Buchen und Einschalten **kein** Häkchen, ein Privatkunde höchstens eins.
|
||||
- Überall „du“, deutlich weniger Erklärtext.
|
||||
- Zuerst eine Vorschau mit Testdaten, dann Livegang in Paketen.
|
||||
|
||||
**Nicht-Ziele**
|
||||
- Keine neue Funktion und keine neue Geschäftslogik außer dem Abo-Modell der Funktionen.
|
||||
- Kein Umbau der Gäste- und Käuferseiten der Kunden (`termin.html`, `unterkunft-seite.html`, `shop-seite.html`, `embed/*`). Ausnahme: Sie lesen Texte oder Zustände, die sich durch dieses Projekt ändern.
|
||||
- Kein Umbau der Mails im Aussehen. Mailtexte, die Preise oder Zahlung für Funktionen nennen, werden aber inhaltlich angepasst.
|
||||
- Kein Dunkelmodus und kein Umschalter. Das Ticketsystem hat auch keinen.
|
||||
- Keine Änderung der Abo-Preise (Hosting 20 €, Wartung 50 €).
|
||||
|
||||
**Anti-Ziele.** Das Projekt gilt als gescheitert, auch wenn es technisch läuft, wenn
|
||||
- ein Laie (Inhaberin, 60, Brille vergessen) Chat, „Rechnung zahlen“, eine Funktion oder „Kündigen“ nicht findet;
|
||||
- „Verträge hier kündigen“ oder „Vertrag widerrufen“ mehr als 2 Taps entfernt sind;
|
||||
- zwischen Kerninfo und „Zahlungspflichtig bestellen“ ein Akkordeon, Sheet oder Scrollen liegt;
|
||||
- das Einschalten einer Funktion versteckt Kosten auslöst;
|
||||
- Hashes, die Mails, Stripe oder Links schon nutzen, nicht mehr funktionieren (`#news`, `#termine`, `#webshop`, `#buchungen`, `#zeiten`, `#chat`, `#rechnungen`, `#vertraege`, `#wartung`, `#abo`, `#gutschrift`, `#passwort` …);
|
||||
- der Nachweis einer Zustimmung (Wortlaut, Fassung, Zeit) schlechter wird als heute.
|
||||
|
||||
## 3. Entscheidungen des Inhabers
|
||||
|
||||
| Nr. | Entscheidung |
|
||||
|---|---|
|
||||
| E1 | Bei den Häkchen, besonders beim Buchen einer Funktion, steht viel zu viel Text. Er soll auf das rechtlich Nötige schrumpfen. |
|
||||
| E2 | Die Kunden sind fast nur Betriebe. Privat kommt kaum vor. |
|
||||
| E3 | Die **Kundenart** (Betrieb/privat) wählt das Team **beim Anlegen des Leads** im Ticketsystem. Das Portal fragt den Kunden nicht. |
|
||||
| E4 | Ist die Kundenart leer, gilt der Kunde als **privat** und sieht das eine Häkchen. |
|
||||
| E5 | Aussehen weiß wie das Ticketsystem, Schwarz als Akzent. Das Lila fällt weg. |
|
||||
| E6 | Die **Preise der Funktionen fallen weg**. Die Funktionen stecken im Abo: Das **Hosting-Abo** enthält die **Analyse**. Das **Wartungs-Abo** enthält **alle anderen** Funktionen (News, Terminplaner, Webshop, Buchungskalender, Öffnungszeiten) und als Obermenge auch die Analyse [Claude, bestätigt]. |
|
||||
| E7 | Die Einrichtung einer Funktion geht als Arbeitszeit vom **Stundenkonto** des Abos ab. |
|
||||
| E8 | Das Einschalten löst **nie automatisch Mehrkosten** aus. Reicht das Guthaben nicht, meldet sich WEBklar vorher und klärt es mit dem Kunden [Claude, bestätigt]. |
|
||||
| E9 | Will ein Kunde eine Funktion **schon beim Projektstart**, baut WEBklar sie gleich ein und rechnet sie über die **Projektkosten** ab, nicht über das Stundenkonto. |
|
||||
| E10 | Einzeln gekaufte Funktionen laufen weiter wie bisher [Claude, bestätigt]. |
|
||||
| E11 | Handy-Leiste **Variante 2 „Fahrstuhl-Zeile“** (schwarzer Balken, siehe 5.2). |
|
||||
| E12 | Neue Gruppierung: Website · Funktionen · Chat · Verträge. Rechnungen werden ein Reiter unter Verträge. Konto liegt am Profilbild oben rechts. Desktop wie im Ticketsystem mit Menü links. Die Erklärsätze unter den Überschriften fallen weg [Claude, bestätigt]. |
|
||||
| E13 | „Entdecken“ zeigt je Funktion eine kurze Beschreibung, ein Bild und den Knopf **„Einschalten“**. |
|
||||
| E14 | Tippt ein Kunde mit Hosting-Abo bei einer Wartungs-Funktion auf „Einschalten“, landet er direkt beim Wechsel ins Wartungs-Abo. Danach schaltet sich die Funktion ein. |
|
||||
| E15 | Anrede überall **„du“**, auch bei Verträgen, Abo und Übernahmegutschrift. |
|
||||
| E16 | Erst eine **Vorschau mit Testdaten** wie beim Ticketsystem, danach Livegang in Paketen. |
|
||||
| E17 | Reicht das Stundenkonto für eine Einrichtung nicht, gibt es einen **Vorschuss**. Die Einrichtung geht voll ab, das Konto darf dafür ins Minus und wird mit den nächsten Monatsgutschriften ausgeglichen. Eine Extra-Rechnung gibt es nicht. Steht das Konto bei Vertragsende noch im Minus, trägt WEBklar den Rest. Abgezogen wird die **echte Arbeitszeit** [Claude, die Teilfrage blieb unbeantwortet]. E17 ersetzt die Absprache aus E8. |
|
||||
| E18 | Die Kundenart **aller 42 Bestandseinträge** wird auf **Betrieb** (`unternehmen`) gesetzt. Das sind 6 Kunden, 31 Leads und 5 verlorene, Stand 15.10. Das passiert als Schritt vor dem Umschalten. |
|
||||
| E19 | Die neue Ansicht ist zuerst **nur für das Team** sichtbar (Pilot-Schalter). Danach schalten alle Kunden auf einmal um. |
|
||||
| E20 | Rechtstexte gehen nach der **Freigabe durch den Inhaber** live. Kanzlei-Fragen werden gesammelt, halten den Livegang aber nicht auf. |
|
||||
| E21 | Eine beim Projektstart eingebaute Funktion (E9) läuft **nur mit dem passenden Abo**, wie alle anderen auch. Die Einrichtung läuft über die Projektkosten. |
|
||||
|
||||
## 4. Rahmen und Randbedingungen
|
||||
|
||||
**Code und Laufzeit**
|
||||
- Code: `knso/Webklar-Kundenbereich`, Express + Vanilla JS in `public/`, Node 20 (prod), Hash-Router, eine Seite mit Ansichten. Kein Framework einführen.
|
||||
- Deploy: Ein Push auf Gitea `main` startet den Webhook. `build/` wird nie direkt bearbeitet.
|
||||
- Der Push auf `main` ist im Auto-Modus für Claude gesperrt. Der Inhaber pusht, außer er gibt im Einzelfall frei.
|
||||
- Server-Speicher: Node-Tests immer mit `--max-old-space-size=1024`, mit Timeout und unter `systemd-run --scope -p MemoryMax=…`. Am 04.10. haben Testläufe den Server per OOM abgeschossen.
|
||||
- Daten: Appwrite `woms-database`. Die Kundenart gibt es schon als Attribut `customers.kundenart` (`'' | 'unternehmen' | 'privat'`, `server/services/auftraege/schema.js:53`). Das Ticketsystem bearbeitet sie heute auf der Kunden-Detailseite (`src/lib/kundenFelder.js`, `CustomerDetailPage.jsx:257`), nicht im Dialog „Neuer Lead“ (`CustomersPage.jsx:162`).
|
||||
|
||||
**Bestand, der weiterlaufen muss**
|
||||
- Die Kundenansicht der Mitarbeiter (Ansichtsmodus aus WOMS) sperrt alle Bestell- und Bestätigungswege.
|
||||
- PWA mit Offline-Seite, iOS-Statusleiste `black-translucent` → wird für Weiß angepasst [Bauer], Android `interactive-widget`, Chat-Composer über `visualViewport`.
|
||||
- Funktionsflags: `TERMINE_ENABLED`, `SHOP_ENABLED`, `UNTERKUNFT_ENABLED`, `ZEITEN_ENABLED` (off/pilot/on), `ANALYSE_ENABLED`, `AUFTRAEGE_ENABLED`. Eine Funktion, die per Flag aus ist, erscheint auch unter „Entdecken“ nicht. Das gilt wie heute.
|
||||
|
||||
**Rechtlich fest, unabhängig vom Design** (aus `recht.md` § 5)
|
||||
- Bei jeder kostenpflichtigen Bestellung stehen Kerninfo und Knopf auf demselben Bildschirm.
|
||||
- Der Knopf heißt wörtlich „Zahlungspflichtig bestellen“ (mit Betrag, wo es ihn heute gibt). Keine Geste und kein Icon statt Text.
|
||||
- „Verträge hier kündigen“ ist immer erreichbar, auch ausgeloggt. `kuendigen.html` bleibt schlicht: keine Halteangebote, kein „Bist du sicher?“.
|
||||
- „Vertrag widerrufen“ ist hervorgehoben, solange eine Frist läuft. `widerrufen.html` bleibt.
|
||||
- Impressum und Datenschutz bleiben im Konto erreichbar.
|
||||
- Belehrung, Vertragstexte und PDFs gehen weiter nach jeder Bestellung per Mail raus (§ 312f, § 312i Abs. 1 Nr. 4).
|
||||
|
||||
**Nachweise.** `featureConsents` speichert weiter den kompletten Wortlaut dessen, was auf dem Bildschirm stand: Linkzeile, Häkchen falls vorhanden, Knopftext, Fassungen, Hash, Zeit, IP, Browser, Konto. Wo Abo, Wechsel und Zusatzspeicher heute nur die Version speichern, darf das so bleiben. Besser ist es, auf denselben Wortlaut-Nachweis umzustellen [Bauer].
|
||||
|
||||
**Rechtliches hochladen.** Neue Fassungen von Rechtstexten lädt Claude selbst in WOMS „Rechtliches“ hoch. Dafür nutzt Claude den kurzlebigen Storage-Key `/root/.appwrite-rechtliches.key`, den der Inhaber bereitstellt.
|
||||
|
||||
## 5. Was gebaut wird
|
||||
|
||||
### 5.1 Aussehen
|
||||
|
||||
Die Tokens und Regeln des Ticketsystems werden übernommen. Quelle ist `woms-design.md` bzw. `ticket.webklar.com` `src/styles/global.css:7-51`:
|
||||
- Weiß `#fff`, Text `#1a1f36`, Zweittext `#5e6678`, Tertiär `#8a93a3` nur für Nebensächliches.
|
||||
- Haarlinien `#ebeef1` / `#d8dde3`, Hover `#f6f8fa`, Fläche `#f8f9fb`.
|
||||
- Akzent Schwarz `#111`, höchstens ein schwarzer Knopf pro Seitenkopf.
|
||||
- Status-Pillen getönt und ohne Rand. Systemschrift, Gewicht 800 entfällt, Zahlen `tabular-nums`.
|
||||
- Radien 6 und 12 px, Ring-Schatten statt Rahmen bei Feldern und Zweitknöpfen.
|
||||
- Keine Karten und Kästen, keine Verläufe, kein Glow, kein Punktraster, kein Glas als Deko.
|
||||
- Dialoge schließen nur per × und Esc und erscheinen am Handy als Bottom-Sheet.
|
||||
- Ladezustände als Text („Wird geladen …“).
|
||||
|
||||
Die Ersetzungsregeln des Ticketsystems gelten auch hier:
|
||||
- Karte → Abschnitt
|
||||
- Kachelraster → Liste
|
||||
- farbiger Streifen → Badge
|
||||
- Icon-Knopf → Textknopf
|
||||
|
||||
Das gilt für **alle** Kundenansichten, die Login-Seite, `kuendigen.html`, `widerrufen.html`, `abo-bedingungen.html`/Rechtliches-Seiten, `offline.html` und die Funktionsmodule (Termine, Webshop, Buchungskalender, Zeiten, News, Analyse) [Claude: Login auch weiß]. `auftrag.html` ist heute schon hell und wird angeglichen.
|
||||
|
||||
Die CSS-Dateien sind heute über viele Dateien verstreut und haben 24 Schriftgrößen und 15 Radien. Daraus wird ein gemeinsamer Satz Tokens mit Komponenten. Wie die Dateien geschnitten werden, entscheidet der Bauer.
|
||||
|
||||
### 5.2 Navigation
|
||||
|
||||
**Handy (bis 880 px, wie heute `appLayout`), Variante „Fahrstuhl-Zeile“.** Referenz-Demo: `handy-leiste/final-2-frei.html`.
|
||||
- **Leiste unten:**
|
||||
- Vier Wörter ohne Icons: **Website · Funktionen · Chat · Verträge**, 15 px/600, Zweittext-Farbe.
|
||||
- Ein schwarzer Balken (Radius 6, 36 px hoch) gleitet federnd unter das aktive Wort. Das Wort darauf ist weiß. Technik der Demo: Overlay-Kopie der Wörter im Balken mit Gegenbewegung; Fallback ohne Overlay, wenn die Breiten nicht passen.
|
||||
- Tap-Ziele mindestens 44 px. Safe-Area unten.
|
||||
- Reicht die Breite nicht, schaltet die Leiste auf 14 px um (Prüfung per Messung, wie in der Demo).
|
||||
- **Zähler:** Rote Zahl hochgestellt am Wort: Chat für ungelesene Nachrichten, Verträge für offene Rechnungen plus Rückfragen. Ein „!“ für eine Rückfrage hat Vorrang [Bauer]. Der Zähler federt beim Erscheinen und verschwindet beim Öffnen. `aria-label` nennt die Zahl.
|
||||
- **Unterseite („Etage 2“):** Öffnet sich eine Funktion (z. B. Terminplaner) oder eine Detailseite, fährt die Zeile eine Etage hoch.
|
||||
- Dort steht **„Zurück“** und daneben die **Reiter der Unterseite**, mit derselben Balken-Mechanik.
|
||||
- Die Seite selbst schiebt sich als Blatt von unten über die Hauptseite.
|
||||
- Ein Punkt am „Zurück“ zeigt Neues im Chat.
|
||||
- Hat die Unterseite keine Reiter, füllt „Zurück“ die Zeile.
|
||||
- Hinweis der Prüfer: Das Muster „unten Ebene, oben Ansicht“ darf nicht brechen. Unterseiten-Reiter stehen **immer unten in Etage 2**. Reiter einer Hauptseite (Meine/Entdecken, Rechnungen/Abo/…) stehen **immer oben** als Unterstrich-Reiter. Wer Etage 2 für alle Reiter einheitlich lösen will, entscheidet das mit Begründung [Bauer].
|
||||
- **Kopf:** Die große Überschrift (28/34, 700) klappt beim Scrollen ein und erscheint dann klein in der Kopfzeile. Rechts sitzt das Profilbild (DiceBear wie heute, 44-px-Tap-Fläche).
|
||||
- **Konto-Sheet** über das Profilbild, Reihenfolge:
|
||||
- Name und E-Mail
|
||||
- Passwort ändern
|
||||
- Datenschutz
|
||||
- Impressum
|
||||
- App installieren (nur wenn sinnvoll, wie heute)
|
||||
- **Verträge kündigen**
|
||||
- **Vertrag widerrufen**: immer da, wie heute. Während einer laufenden Frist hervorgehoben mit der Pille „Frist läuft“ (C3).
|
||||
- Abmelden
|
||||
- **Widerrufsstreifen:** Läuft eine Widerrufsfrist, steht über der Leiste ein schmaler Streifen „Widerrufsfrist bis TT.MM. · Widerrufen“. Er ist nicht wegtippbar, auf allen Hauptseiten sichtbar und blendet sich bei offener Tastatur aus. Er erscheint nur bei Kundenart privat oder leer und für jede Frist, die der Server kennt (C3).
|
||||
- **Tastatur offen:** Leiste und Streifen fahren weg, der Chat-Composer sitzt über der Tastatur. Die heutige `visualViewport`-Logik bleibt.
|
||||
- **Zurück-Geste:** Reiterwechsel mit `replaceState`, Etage 2, Sheet und Seiten mit `pushState`. Zurück schließt der Reihe nach Sheet, Tastatur, Etage 2 und geht dann zu Website. Das verhält sich wie heute (`APP_PARENT`), nur mit der neuen Mechanik.
|
||||
- **Querformat:** Die Leiste wird niedriger und die große Überschrift entfällt [Bauer: Maße].
|
||||
- **Bewegung reduziert:** Keine Federn, nur kurzes Überblenden.
|
||||
- **Wow-Moment** (zum Abnehmen): Der Balken zieht federnd mit Überschwingen, die Schrift kippt dabei von grau auf weiß, und die Unterseite fährt als Blatt mit der Leiste eine Etage hoch.
|
||||
|
||||
**Desktop (über 880 px)**
|
||||
- Menü links wie im Ticketsystem (232 px, Haarlinie rechts, Wörter ohne Icons, aktiver Eintrag schwarz) mit denselben vier Bereichen und Zählern.
|
||||
- Unterseiten zeigen eine Brotkrume („Funktionen / Terminplaner“) statt Etage 2. Oben rechts sitzt das Profilbild mit dem Konto als Popover.
|
||||
- Inhalt maximal ~1160 px, Abstände wie `.page` im Ticketsystem.
|
||||
|
||||
### 5.3 Gruppierung der Bereiche
|
||||
|
||||
| Bereich | Inhalt | Reiter |
|
||||
|---|---|---|
|
||||
| **Website** (heute „Projekte“) | Live-Vorschau, Status, Projektdetails, letzte Änderungen, Arbeiten am Projekt, Dateien. Bei mehreren Projekten eine Liste, Detail als Etage 2. | – |
|
||||
| **Funktionen** | siehe 5.4 | Meine · Entdecken |
|
||||
| **Chat** | Verlauf und Composer (Bild, Kamera, Datei, Sprachnachricht wie heute) | – |
|
||||
| **Verträge** | Rechnungen (Liste, Zahlen/PDF, Aufträge falls an), Abo (Mein Vertrag, Laufzeit, Wechsel, Bedingungen), Stunden (Stundenkonto + Verlauf), E-Mail (Zusatzspeicher). Darunter fest: „Verträge hier kündigen“. Ohne Abo: Abo-Auswahl Hosting/Wartung. | Rechnungen · Abo · Stunden · E-Mail |
|
||||
| **Konto** (Profilbild) | siehe 5.2 | – |
|
||||
|
||||
Weitere Regeln:
|
||||
- **Sprung zum offenen Posten:** Ist eine Rechnung offen, öffnet Verträge auf dem Reiter Rechnungen. Website und Funktionen zeigen oben eine ruhige Zeile „Rechnung offen · RE-… · 50,00 € · Zahlen“ [Claude, aus Entwurf „Das gesetzte Heft“].
|
||||
- **Übernahmegutschrift:** Sie wird ein Eintrag unter Verträge → Abo, wie heute über die Verträge erreichbar [Bauer: genaue Stelle].
|
||||
- **Hashes:** Alle bisherigen Hashes bleiben gültig und führen an die passende neue Stelle. Zum Beispiel öffnet `#rechnungen` Verträge → Rechnungen, `#stunden` Verträge → Stunden, `#gutschrift` die Übernahmegutschrift. Neue Hashes für die Reiter entscheidet der Bauer.
|
||||
- **PWA-Shortcuts:** Sie werden angepasst (Chat, Rechnungen, Verträge).
|
||||
|
||||
### 5.4 Funktionen: „Meine“ und „Entdecken“
|
||||
|
||||
**Meine** zeigt alles, was für eine Website des Kunden nicht `off` ist, also gebucht, in Einrichtung, aktiv, abgeschaltet oder angefragt. Dazu kommen begonnene Entwürfe (Terminplaner-Fragebogen fortsetzen).
|
||||
- Darstellung als Liste mit Haarlinien: Name, eine Statuszeile, Status-Pille.
|
||||
- Ein Tap öffnet die Funktion als Etage 2 mit ihren eigenen Reitern. Die Reiter der Funktionen bleiben inhaltlich, bekommen aber die gemeinsame Balken- bzw. Unterstrich-Mechanik statt der vier eigenen Kopien.
|
||||
- Ist „Meine“ leer, öffnet Funktionen direkt auf „Entdecken“.
|
||||
- Der Zustand „angefragt“ (Webshop, Buchungskalender) wird sichtbar. Heute fehlt er auf der Kachel, `anfrageStatus` steht schon in den Daten.
|
||||
|
||||
**Entdecken** zeigt alle verfügbaren Funktionen, die noch nicht unter „Meine“ stehen, je Funktion:
|
||||
- **ein Bild:** echte Aufnahme der Funktion auf einer Beispiel-Website, mit Testdaten erstellt [Claude]. Format und Größe entscheidet der Bauer; ruhig und ohne Rahmen.
|
||||
- **eine kurze Beschreibung:** höchstens zwei Sätze.
|
||||
- **eine Zeile, was dabei ist:** „Im Wartungs-Abo enthalten“ bzw. „Im Hosting-Abo enthalten“.
|
||||
- **den Knopf „Einschalten“:**
|
||||
- Mit passendem Abo: Der Einschalt-Ablauf startet (5.5).
|
||||
- Hosting-Abo, aber Wartungs-Funktion: direkt zur Wechselseite ins Wartungs-Abo. Nach dem Wechsel schaltet sich die Funktion ein. Dafür merkt sich der Wechsel die gewünschte Funktion und startet ihren Einschalt-Ablauf [Bauer: Technik].
|
||||
- Kein Abo: zur Abo-Auswahl, mit vorausgewähltem passendem Abo [Claude].
|
||||
- **Keine Preise** für Funktionen, nirgends in der Kundenansicht.
|
||||
|
||||
### 5.5 Funktionen im Abo: Einschalten
|
||||
|
||||
**Ablauf** für einen Kunden mit passendem Abo:
|
||||
1. Er tippt in „Entdecken“ oder auf der Funktionsseite auf **„Einschalten“**.
|
||||
2. Bei Funktionen mit Fragebogen oder Anfrage kommt erst dieser Schritt, wie heute: Terminplaner-Fragebogen, Webshop- und Buchungskalender-Anfrage mit Freigabe durch das Team.
|
||||
3. Eine kurze Bestätigungsseite (oder ein Sheet, weil nichts kostet [Bauer]) zeigt:
|
||||
- Leistung: „Einrichtung durch WEBklar“
|
||||
- Dauer: z. B. „meist in 1–2 Wochen“
|
||||
- „Die Einrichtung geht von deinem Stundenkonto ab, auch wenn das Guthaben noch nicht reicht. Extrakosten entstehen keine.“ (E17; bei der Analyse entfällt der Satz, siehe C6)
|
||||
- Laufzeit: „läuft, solange dein Wartungs-Abo läuft“
|
||||
- Eine Linkzeile: „Es gelten Leistungsbeschreibung und Auftragsverarbeitung (Fassung …).“
|
||||
- Den Knopf **„Einschalten“**.
|
||||
4. Danach entsteht wie heute ein Einrichtungs-Ticket in WOMS. Der Zustand geht auf `setup`, und `runActivation` schaltet auf `active`, wenn das Ticket geschlossen ist. **Neu:** Arbeitsblätter auf diesem Ticket stehen standardmäßig auf `countsToPlan=true` und laufen damit aufs Stundenkonto (`hourLedger.js`). Heute ist der Schalter „+ Abo“ manuell und standardmäßig aus, und die Ticket-Texte (`termine/ticketDetails.js:38`, `shop/ticketDetails.js:57`, `unterkunft/ticketDetails.js:49`) sagen „nicht anrechnen“. Beides wird umgestellt (C6).
|
||||
5. Kein Stripe und kein „Zahlungspflichtig bestellen“. Begründung: Es entsteht keine neue Zahlungspflicht, die Leistung ist Teil des bestehenden Abos. Betriebe setzen kein Häkchen. **Ausnahme Privatkunde (oder leere Kundenart) mit laufender Widerrufsfrist des Abos:** Er sieht das eine Häkchen „Bitte beginnt sofort …“. Ohne Häkchen kann er erst nach Fristende einschalten, und der Knopf nennt das Datum (C4).
|
||||
|
||||
Weitere Regeln:
|
||||
- **Stundenkonto zu knapp (E17, Vorschuss):** Das Einschalten wird immer angenommen, und die Einrichtung startet ohne Rückfrage. `hourLedger.js` rechnet heute so: Was kein Guthaben findet, ist Mehraufwand und wird separat abgerechnet. Für Arbeitsblätter auf Einrichtungs-Tickets gilt künftig eine andere Regel:
|
||||
- Das Minus ist ein **Vorschuss**, den künftige Gutschriften zuerst ausgleichen (FIFO auch rückwirkend).
|
||||
- Er wird **nie** als Mehraufwand ausgewiesen.
|
||||
- Ein Restminus bei Vertragsende verfällt zugunsten des Kunden.
|
||||
- Normale Arbeit außerhalb von Einrichtungen bleibt beim heutigen Mehraufwand.
|
||||
- Das Stundenkonto im Portal zeigt den Vorschuss verständlich, etwa „Vorschuss für Einrichtung: 2,5 Std., wird mit den nächsten Monaten verrechnet“. WOMS zeigt ihn im Ticket und beim Kunden.
|
||||
- Wie Ledger, Anzeige und Kennzeichnung der Einrichtungs-Blätter umgesetzt werden, entscheidet der Bauer.
|
||||
- **Gleich beim Projektstart (E9, E21):** Das Team schaltet die Funktion in WOMS ohne Kundenklick ein (das gibt es heute schon: Admin-Aktivierung ohne Zahlung). Die Einrichtungs-Arbeitsblätter zählen dann **nicht** aufs Stundenkonto (`countsToPlan=false`), weil sie über die Projektkosten laufen (C7). Auch diese Funktion läuft nur mit dem passenden Abo und endet mit ihm (C12). Die Admin-Aktivierung prüft deshalb, ob ein passendes Abo besteht, oder warnt zumindest [Bauer].
|
||||
- **Abo-Ende:** Endet das Abo, das die Funktion enthält, schaltet die Funktion ab. Es gelten die bisherigen Lösch- und Behaltefristen (`disabledReason: abo_ende`) [Claude]. Den kostenpflichtigen „Behalten“-Weg der Öffnungszeiten (30 €) gibt es nicht mehr.
|
||||
- **Bestand (E10):** Einzeln gekaufte Funktionen bleiben unverändert aktiv, auch ohne passendes Abo. Ihr Nachweis und ihre Rechnungen bleiben. Neue Einzelkäufe gibt es nicht mehr.
|
||||
- **Analyse:**
|
||||
- Sie ist im Hosting- und im Wartungs-Abo enthalten.
|
||||
- Sie bleibt hinter `ANALYSE_ENABLED`, bis ihr eigener Livegang (L5) erledigt ist.
|
||||
- Ihre Aktivierung bekommt dasselbe schlanke Muster. Die Aussage „Meine Website richtet sich nicht gezielt an Kinder unter 16“ bleibt als eine kurze Zeile mit Häkchen, weil sie eine Tatsachenangabe für die Einwilligungslogik ist, kein Rechtstext [Claude; Kanzlei-Frage].
|
||||
|
||||
### 5.6 Bestätigungen schlank
|
||||
|
||||
Für alles, was **kostet** (Abo bestellen, Wechsel, Zusatzspeicher erhöhen), gilt dasselbe Muster. Vorbild ist die Buchungsseite im Mockup dieser Sitzung.
|
||||
1. **Block „Was du bestellst“** direkt über dem Knopf: Leistung, Preis (mit „keine USt, § 19 UStG“), Abrechnung bzw. heute fällig, Beginn, Mindestlaufzeit und Kündigung, Zahlungsweg.
|
||||
2. **Eine Linkzeile:** „Es gelten die Vertragsbedingungen mit Leistungsbeschreibung, Auftragsverarbeitung und Widerrufsbelehrung (Fassung …). Alles kommt danach auch als PDF per Mail.“ Datenschutz als Link.
|
||||
3. **Nur für Privatkunden (oder leere Kundenart, E4)** ein Häkchen, nicht vorausgewählt: „Bitte beginnt sofort. Mir ist klar: … erlischt mein Widerrufsrecht / zahle ich den bis dahin erbrachten Teil.“ Den genauen Wortlaut je Fall legt die Rechtsrunde fest. Abo: Wertersatz; Einrichtung: Erlöschen + Wertersatz. Ohne Häkchen:
|
||||
- Abo: Beginn nach 14 Tagen. Das ist der heutige Weg `nach_frist`.
|
||||
- Wechsel: nicht möglich ohne Häkchen, weil er sofort wirkt. Der Knopf bleibt dann gesperrt, mit einer kurzen Zeile dazu.
|
||||
4. **Knopf** „Zahlungspflichtig bestellen“, ggf. mit Betrag.
|
||||
5. **Unter dem Knopf:** „Du bestellst als Betrieb“ bzw. „… privat“, und „Stimmt nicht? Schreib uns im Chat“ [Claude]. Ändern kann es nur das Team.
|
||||
|
||||
**Zuordnung der heutigen Häkchen** (Datei:Zeile in `bestaetigungen.md`):
|
||||
|
||||
| Heute | Neu |
|
||||
|---|---|
|
||||
| A1 Abo „gelesen und stimme zu … Belehrung zur Kenntnis“ | Linkzeile |
|
||||
| A2 Abo Radio Sofort / nach Frist | Betrieb: Beginn sofort ohne Auswahl. Privat: das eine Häkchen; ohne Häkchen nach Frist |
|
||||
| B1 Wechsel AGB/AVV | Linkzeile |
|
||||
| B2 Wechsel Sofortbeginn (heute Pflicht für alle) | nur noch Privat |
|
||||
| C1 Zusatzspeicher Regeln + Belehrung | Linkzeile, kein Häkchen (WEBklar verzichtet laut § 10 auf Wertersatz) |
|
||||
| C3 Warnbox beim Verkleinern | bleibt als eine Zeile |
|
||||
| D2 Übernahmegutschrift „Ich versichere …“ | Satz über dem Knopf „Mit dem Absenden bestätigst du, dass die Angaben stimmen …“ + Datenschutz-Link; Bedingungen als Link statt Klappe [Claude] |
|
||||
| F Funktionen `unterlagen`, `information`, `beginn`, `leistung`, `stripe`, `verantwortung` | entfallen mit dem Einschalt-Ablauf (5.5). Pflichten wie „Ich verkaufe in eigenem Namen und bin für Rechtstexte verantwortlich“ (Webshop) und „Zahlung über mein Stripe“ wandern in die Leistungsbeschreibung, dazu ein Satz auf der Einschalt-Seite |
|
||||
| F `gesundheit` (Terminplaner) | wird zur Branchen-Frage im Fragebogen. Gesundheitsbereich → Terminplaner nicht verfügbar, mit freundlichem Hinweis |
|
||||
| F `rechnung` (Buchungskalender) + G5 Einstellungen | **bleibt als einziges Fach-Häkchen**, nur noch einmal: in den Einstellungen (G5), weil es den Betrieb freischaltet. Der Bestell-Haken entfällt |
|
||||
| G2 Webshop-Anfrage „Ich verkaufe als Unternehmer“ | entfällt (Kundenart). Privat → Webshop nicht verfügbar [Claude] |
|
||||
| G2 „keine ausgeschlossenen Waren“ | Satz über „Anfrage senden“ mit der Liste. Das Team prüft bei der Freigabe [Claude] |
|
||||
| G3 Webshop „Texte bestätigen“ | bleibt (Betriebsfreigabe, kein Vertrag), Text auf einen Satz gekürzt |
|
||||
| G6 Öffnungszeiten WhatsApp-Hinweis | bleibt, gekürzt |
|
||||
| G1 Terminplaner-Fragebogen „Weiter“ + „Preise geprüft“ | bleibt (Einrichtung, kein Rechtstext), Texte gekürzt |
|
||||
| H Analyse AVV + Zielgruppe | Linkzeile + kurze Zielgruppen-Zeile mit Häkchen (5.5) |
|
||||
| I Auftragsbestätigung | bleibt wie gebaut (Häkchen als Beweis + Frühbeginn nur privat), nur im neuen Aussehen |
|
||||
| E1/E2 Kündigen/Widerrufen | unverändert im Ablauf, nur im neuen Aussehen und mit „du“ |
|
||||
|
||||
**Belehrung.** Der Volltext der Widerrufsbelehrung steht nicht mehr als Scrollfeld auf der Seite. Er ist per Link erreichbar und liegt als PDF in der Bestätigungsmail (Kanzlei-Frage 4 in `recht.md`).
|
||||
|
||||
### 5.7 Sprache und Text
|
||||
|
||||
- **Anrede:** Überall „du“, in der Kundenansicht und auf den öffentlichen Formularseiten (Kündigen, Widerrufen, Login) [Claude: auch öffentliche Seiten].
|
||||
- Die Rechtsdokumente (Abo-Bedingungen, Leistungsbeschreibungen, AVV, Datenschutz) bleiben im bisherigen Stil, außer die Rechtsrunde stellt sie um.
|
||||
- Mails mit „Sie“ bleiben, bis zu einer eigenen Mail-Runde [Claude].
|
||||
- **Erklärsätze und Hinweise:** Die Erklärsätze unter den Überschriften fallen weg. Hinweise erscheinen nur, wo der Kunde etwas tun muss oder etwas schiefging.
|
||||
- **Tonfall:** kurze Sätze, keine Paragrafen in der Oberfläche, Knöpfe mit Verb zuerst.
|
||||
|
||||
### 5.8 Ticketsystem (WOMS)
|
||||
|
||||
- **Dialog „Neuer Lead“** (`CustomersPage.jsx:162`) bekommt das Pflichtfeld **Kundenart** (Betrieb/privat) mit `KUNDENART_OPTIONEN` aus `kundenFelder.js`. Gibt es einen Dialog „Neuer Kunde“, gilt dort dasselbe [Claude].
|
||||
- **Kunden-Detailseite:** Sie zeigt die Kundenart weiter editierbar.
|
||||
- **Funktionen-Bereich in WOMS:**
|
||||
- Er zeigt keine Preise mehr für Funktionen.
|
||||
- Die Admin-Aktivierung kann „im Projekt enthalten“ (zählt nicht aufs Stundenkonto, E9) von „aus dem Abo“ unterscheiden [Bauer: Form].
|
||||
- Die Textform-Zustimmung per Chat, E-Mail oder WhatsApp für die Team-Aktivierung bleibt für die AVV-Fälle, wird aber an das Abo-Modell angepasst [Bauer, Rechtsrunde].
|
||||
- **Einrichtungs-Ticket:** Es zeigt den Kontostand des Kunden und, sobald es entsteht, den Vorschuss (E17). Neue Arbeitsblätter stehen dort standardmäßig auf „aufs Abo anrechnen“ (C6) [Bauer: Form].
|
||||
- **Stil:** Es gilt das weiße Design des Ticketsystems.
|
||||
|
||||
### 5.9 Rechtsrunde
|
||||
|
||||
Diese Texte ändern sich und brauchen neue Fassungen:
|
||||
- **Abo-Bedingungen Fassung 6:**
|
||||
- Funktionen sind enthalten (Hosting: Analyse; Wartung: alle).
|
||||
- Einrichtung zählt aufs Stundenkonto, Mehraufwand nur nach Absprache.
|
||||
- Funktionen enden mit dem Abo.
|
||||
- Bestand bleibt.
|
||||
- Dazu ein Übergang für laufende Abos der Fassung 5: Die Änderung ist für Kunden nur vorteilhaft. Ob sie informiert werden müssen, klärt die Rechtsrunde.
|
||||
- **Leistungsbeschreibungen je Funktion:** Preise raus, Pflichten aus den alten Häkchen rein.
|
||||
- **AVV je Funktion:** Prüfen, ob sie Anlage zu den Abo-Bedingungen werden.
|
||||
- **Datenschutz:** Prüfen, ob die Stellen zu Zahlung/Stripe bei Funktionen und zu gespeicherten Zustimmungen noch stimmen.
|
||||
- **Widerrufsbelehrung Funktionen:** wird für das Einschalten nicht mehr gebraucht, bleibt für Bestand.
|
||||
|
||||
Ablauf wie bei früheren Rechtsrunden:
|
||||
1. Claude entwirft.
|
||||
2. Der Inhaber entscheidet die Punkte.
|
||||
3. Die Kanzlei-Fragen werden gesammelt (`recht.md` § 4 plus neue).
|
||||
4. Claude lädt die Fassungen in WOMS „Rechtliches“ hoch.
|
||||
|
||||
Die rechtsabhängigen Teile (5.5, 5.6, Preise weg) gehen erst nach der Rechtsrunde live, also sobald der Inhaber die Entwürfe freigegeben hat (E20). Auf die Kanzlei wird nicht gewartet, ihre Fragen bleiben gesammelt.
|
||||
|
||||
Neue Punkte für Fassung 6:
|
||||
- die Vorschuss-Regel des Stundenkontos (E17)
|
||||
- „Funktionen laufen nur mit passendem Abo“, auch beim Projektstart (E21)
|
||||
- Analyse im Hosting-Abo (C10)
|
||||
|
||||
### 5.10 Vorschau und Livegang
|
||||
|
||||
- **Vorschau mit Testdaten** wie beim Ticketsystem:
|
||||
- Eine Vorschau-Instanz der Kundenansicht mit festem Testdatensatz und den Szenarien, umschaltbar ohne echte Kundendaten: Betrieb mit Wartung, Hosting-Kunde, ohne Abo, privat in Widerrufsfrist, offene Rechnung, Chat ungelesen, Funktionen in allen Zuständen.
|
||||
- Erreichbar unter einer `*.project.webklar.com`-Adresse hinter dem Portal-Login.
|
||||
- Vorlage ist die Testdaten-Vorschau des Ticketsystems (Service Worker mit Fake-Backend, Umschalter „Daten/Fall“) oder der statische Fixture-Server aus `kundenportal-testing`. Welches, entscheidet der Bauer.
|
||||
- Keine echten Mails, kein echtes Stripe, keine Prod-Daten.
|
||||
- Deploy der Vorschau nur per explizitem `cp`, ohne `--delete` und ohne Ziel aus Parametern ohne Guard (Vorfall 05.10.).
|
||||
- **Pilot-Schalter (E19):** Die neue Ansicht liegt hinter einem eigenen Schalter `off | pilot | on`, nach dem Muster von `ZEITEN_ENABLED` mit Pilot-E-Mail-Liste. Name und Technik entscheidet der Bauer.
|
||||
- Bei `pilot` sehen nur das Team, die Pilot-Adressen und die Mitarbeiter-Kundenansicht die neue Oberfläche. Alle anderen Kunden behalten die alte, inklusive heutiger Preise und Bestellseiten.
|
||||
- Kein gemischter Zwischenstand: Ein Kunde sieht entweder ganz alt oder ganz neu.
|
||||
- **Pakete** sind interne Bauabschnitte. Sie werden einzeln gebaut, geprüft und in den Pilot gepusht, nicht einzeln für Kunden freigeschaltet. Den Schnitt macht der Bauer, z. B.:
|
||||
1. Fundament mit Tokens, Komponenten, Kopf, Leiste und Menü
|
||||
2. Website, Chat, Verträge
|
||||
3. Funktionen-Rahmen mit Meine/Entdecken
|
||||
4. die Funktionsmodule
|
||||
5. Abo-Modell und schlanke Bestätigungen
|
||||
6. WOMS-Änderungen inklusive Kundenart-Pflichtfeld
|
||||
- **Umschalten für alle:**
|
||||
1. Alle Pakete sind fertig, und der Inhaber hat Vorschau, Pilot und Fassung 6 freigegeben.
|
||||
2. Vorher: Kundenart aller Bestandseinträge auf Betrieb setzen (E18), Fassung 6 hochladen, WOMS-Pflichtfeld live.
|
||||
3. Dann `on` für alle.
|
||||
4. Danach räumt ein eigenes Paket die alte Oberfläche weg.
|
||||
- **Einzelkäufe bis zum Umschalten:** Sie laufen in der alten Oberfläche weiter wie heute [Claude, die Teilfrage blieb unbeantwortet]. Wer kurz vorher eine Funktion kauft, gehört zum Bestand (E10).
|
||||
- **Vor jedem Paket** gibt es eine Screenshot-Prüfung auf Handy, Tablet und Desktop, wie bei P1–P9 im Ticketsystem.
|
||||
|
||||
## 6. Dem Bauer überlassen
|
||||
|
||||
- Schnitt der CSS/JS-Dateien, Komponenten, Namen, neue Hash-Namen für Reiter (alte bleiben gültig).
|
||||
- Exakte Maße, Kurven und Dauern im Rahmen der Demo `final-2-frei.html` und der Ticketsystem-Tokens.
|
||||
- Technik der Vorschau (Fake-Backend), Testwerkzeug, Gates.
|
||||
- Wie „Stundenkonto reicht nicht“ ermittelt und angezeigt wird (E8).
|
||||
- Wie WOMS „im Projekt enthalten“ markiert (E9).
|
||||
- Ob Abo, Wechsel und Zusatzspeicher auf den Wortlaut-Nachweis wie `featureConsents` umgestellt werden (empfohlen).
|
||||
- Paketschnitt und Reihenfolge, solange die rechtsabhängigen Teile nach der Rechtsrunde kommen.
|
||||
- Die Bilder für „Entdecken“ (Aufnahmen mit Testdaten), Format.
|
||||
|
||||
## 7. Festlegungen nach dem Builder-Check [Claude]
|
||||
|
||||
Drei Prüfer haben das Dokument gelesen (Oberfläche, Server/Abo, WOMS/Recht). Die Fragen liegen in `/root/reports/kundenansicht-neu-2026-10-15/builder-check.json`. Was sich ohne den Inhaber vernünftig entscheiden ließ, steht hier:
|
||||
|
||||
- **C1 Mehrere Websites.** Funktionen gelten je Website (`portalFeatures`).
|
||||
- Hat ein Kunde mehr als eine Website, zeigt „Meine“ eine Zeile je Funktion und Website, mit dem Websitenamen als zweiter Zeile.
|
||||
- „Entdecken“ zeigt eine Funktion, solange mindestens eine Website sie noch nicht hat.
|
||||
- „Einschalten“ fragt dann zuerst nach der Website. Bei nur einer Website entfällt das.
|
||||
- Die drei heutigen Übergabewege (`selection`, `analyseSel`, `dataset.wunschProjekt`) werden zu einem.
|
||||
- **C2 Viele Reiter in Etage 2.** Die Reiterzeilen von Terminplaner, Webshop und Buchungskalender bekommen Kurzlabels, z. B. „Team“ statt „Team & Plätze“ und „Preise“ statt „Unterkünfte & Preise“. Passt die Zeile trotzdem nicht, scrollt sie waagerecht, der Balken wandert mit, und der aktive Reiter wird in den sichtbaren Bereich gerückt. Die Overlay-Technik braucht dafür einen Rückfall, der beim Scrollen trägt. Die Labels legt der Bauer fest, die Vorschau prüft sie.
|
||||
- **C3 Widerruf sichtbar.**
|
||||
- Der Eintrag „Vertrag widerrufen“ im Konto bleibt wie heute **immer** da und wird während einer Frist hervorgehoben.
|
||||
- Der Streifen erscheint nur bei Kundenart privat oder leer, und zwar für jede Frist, die der Server kennt: Abo, Wechsel, Speicher-Plus, Bestands-Funktionen, Projektaufträge falls an. Er nennt das früheste Fristende. Dafür liefert der Server eine Sammelangabe der Fristen.
|
||||
- Die Verarbeitung eines Widerrufs (`withdrawOrder`, `widerrufen.html`) bleibt in der Logik unverändert.
|
||||
- Nachweise speichern ab jetzt die Kundenart zum Zeitpunkt des Klicks.
|
||||
- **C4 Privatkunde in der Widerrufsfrist schaltet eine Funktion ein.** Er braucht das eine Verlangen-Häkchen, ohne Häkchen erst nach Fristende (siehe 5.5 Schritt 5). Für Betriebe gibt es kein Häkchen, auch nicht beim Wechsel. Der Server speichert dann die Kundenart „unternehmen“ als Grund statt `earlyStartConsentAt`, und `planSwitch.js:351` wird entsprechend angepasst.
|
||||
- **C5 Gemerkte Funktion beim Wechsel.** Die gewünschte Funktion wird am Wechsel-Vorgang gespeichert (serverseitig).
|
||||
- Erst wenn der Wechsel wirksam ist (Zahlung bestätigt, wie heute `planSwitch`), öffnet das Portal den **Einschalt-Ablauf** der Funktion mit Fragebogen, Anfrage oder Bestätigung. Es schaltet die Funktion also nicht sofort frei.
|
||||
- Scheitert der Wechsel, passiert nichts.
|
||||
- Ein Widerruf des Wechsels beendet alle Funktionen, die nur im Wartungs-Abo stecken und über das Abo eingeschaltet wurden. Die Analyse bleibt, weil Hosting sie enthält.
|
||||
- **C6 Stundenkonto im Ticket.**
|
||||
- Einrichtungs-Tickets aus „Einschalten“ stehen standardmäßig auf „aufs Abo anrechnen“.
|
||||
- Team-Aktivierungen „im Projekt enthalten“ (E9) stehen standardmäßig auf „nicht anrechnen“.
|
||||
- Die **Analyse-Einrichtung bleibt für den Kunden gratis**, wie es ihre Leistungsbeschreibung heute sagt, und das Hosting-Abo hat ohnehin kein Stundenkonto. Ihre Einschalt-Seite nennt deshalb kein Stundenkonto.
|
||||
- **C7 Projektkosten (E9).** Dafür gibt es keine neue Markierung. Arbeitsblätter mit „nicht anrechnen“ sind wie heute als Projektarbeit abrechenbar (Rechnungsdialog WOMS), nach Aufwand oder im Projektangebot enthalten, wie das Team es mit dem Kunden vereinbart.
|
||||
- **C8 Eingefrorene Rechtstext-Fassungen** (`public/rechtliches/*-2026-10-0x.html` + PDFs mit sha256, `vertragsunterlagen/v3`, `v4`) bleiben unberührt, Text und Aussehen. Neue Fassungen erscheinen im neuen Stil. `abo-bedingungen.html` bekommt den neuen Stil mit Fassung 6.
|
||||
- **C9 Entdecken und Schalter.** Eine Funktion, die per Flag aus ist, erscheint nicht. Teaser „Kommt bald“ fallen weg. Pilot-Funktionen sehen nur Pilot-Kunden.
|
||||
- **C10 Analyse L5** ist unabhängig. Sie darf vorher mit den heutigen Texten live gehen (nur Wartungs-Abo). Fassung 6 und Paket 5 erweitern sie dann auf Hosting und passen ihre Texte an (Abo-Ergänzung Analyse, Eignungsprüfung `analyse.js:45,60`).
|
||||
- **C11 Fassung 6 und Bestands-Abos.**
|
||||
- Fassung 6 gilt für neue Abschlüsse.
|
||||
- Laufende Abos (v3 bis v5) behalten ihren Vertrag.
|
||||
- Beim Einschalten einer Funktion gelten deren Leistungsbeschreibung und AVV über die Linkzeile und den Klick. Der Nachweis kommt in `featureConsents`.
|
||||
- Ob Bestandskunden zusätzlich eine Info-Mail bekommen, entscheidet die Rechtsrunde. Das ist eine neue Kanzlei-Frage.
|
||||
- Der harte Verweis auf „-v5“ (`products.js:32,51`) wird zu einer Liste der Fassungen.
|
||||
- **C12 Abo-Ende.**
|
||||
- Über das Abo eingeschaltete Funktionen schalten zum Abo-Ende ab, wie heute der Terminplaner.
|
||||
- Je Funktion gelten die bestehenden Fristen und Löschjobs. Webshop und Buchungskalender schließen für Käufer und Gäste, offene Bestellungen und Buchungen bleiben bearbeitbar, gelöscht wird nach den bestehenden 90 Tagen. News und Öffnungszeiten folgen ihrer 30-Tage-Frist.
|
||||
- Ein neues passendes Abo innerhalb der Frist holt die Funktion zurück.
|
||||
- Die Mails K02, K22, K29 und K30 werden angepasst. Den Satz zum 30-€-Behalten gibt es nicht mehr.
|
||||
- **Einzeln gekaufte Funktionen behalten genau ihre bisherigen Regeln.** Der gekaufte Terminplaner endet also weiter mit dem Wartungs-Abo, wie sein Vertrag sagt. Die anderen gekauften Funktionen laufen weiter.
|
||||
- **C13 Kundenart in WOMS.**
|
||||
- Pflichtfeld in „Neuer Lead“, „Neuer Kunde“ (`customerAdmin.js:110`) und „Zum festen Kunden machen“.
|
||||
- In der Kundenliste ein Filter „Kundenart fehlt“.
|
||||
- Das Portal bekommt die Kundenart über `/api/auth/me`. Das Attribut `customers.kundenart` gibt es live (geprüft 15.10.: 42 Einträge, alle leer).
|
||||
- **C14 Wortlaut der Bestellseiten.** Abo, Wechsel, Zusatzspeicher, Gutschrift, Kündigen und Widerrufen bekommen bis Paket 5 nur das neue Aussehen, nicht den neuen Text. Jede Textänderung dort zieht eine neue `termsVersion` nach (`config.js:853`). „Du“ und das schlanke Muster kommen dort mit Fassung 6.
|
||||
- **C15 Webshop für Privatkunden** ist nicht verfügbar. Er bekommt einen freundlichen Hinweis mit Chat-Link.
|
||||
|
||||
## 8. Fragen an den Inhaber nach dem Builder-Check (beantwortet 16.10.)
|
||||
|
||||
| Frage | Antwort | Eingearbeitet |
|
||||
|---|---|---|
|
||||
| Q1 Stundenkonto reicht nicht | a) Vorschuss | E17, 5.5 |
|
||||
| Q2 Kundenart Bestand | alle auf Betrieb | E18, 5.10 |
|
||||
| Q3 Livegang | erst Team, dann alle auf einmal | E19, 5.10 |
|
||||
| Q4 Rechtsrunde | live nach Freigabe durch den Inhaber | E20, 5.9 |
|
||||
| Q5 Funktion beim Projektstart | nur mit Abo | E21, 5.5, C12 |
|
||||
|
||||
Zwei Teilfragen blieben offen. Ich habe sie so entschieden, dass sie leicht zu kippen sind: Es zählt die echte Arbeitszeit statt einer Pauschale (E17), und Einzelkäufe laufen bis zum Umschalten weiter (5.10).
|
||||
+36
-12
@@ -9,11 +9,18 @@ import { analyseOf, pickAnalyseProject, analyseSel, bannerColor, BANNER_PREVIEW_
|
||||
import { headerHtml, tabsHtml, overviewHtml, rankCardHtml, skeletonHtml, errorHtml, settingsHtml, chartTipText } from './analyse-cards.js'
|
||||
import { HEAT_DEVICES, deviceHtml, heatCaption } from './geraete.js'
|
||||
import { ZU_VERTRAEGE } from './vertraege-link.js'
|
||||
import { meldeReiter, reiterEintraege } from './fn-reiter.js'
|
||||
|
||||
const PRESET_COLORS = ['#111111', '#1d4ed8', '#047857', '#b91c1c', '#7c3aed', '#c2410c']
|
||||
const HEX_RE = /^#[0-9a-f]{6}$/i
|
||||
const nf = new Intl.NumberFormat('de-DE')
|
||||
const TABS = ['uebersicht', 'heatmap', 'einstellungen']
|
||||
// Abo-Modell: die Reiter zeichnet die Etage (kn:reiter, public/fn-reiter.js); Reihenfolge und Wortlaut wie tabsHtml
|
||||
const ANALYSE_REITER = [{ key: 'uebersicht', label: 'Übersicht' }, { key: 'heatmap', label: 'Heatmap' }, { key: 'einstellungen', label: 'Einstellungen' }]
|
||||
const reiterLabel = (key) => ANALYSE_REITER.find((r) => r.key === key)?.label || ANALYSE_REITER[0].label
|
||||
// Abo-Modell, Texte A1 bis A3
|
||||
const ZIELGRUPPE = 'Meine Website richtet sich nicht gezielt an Kinder unter 16 Jahren.'
|
||||
const KEIN_ABO_ANALYSE = 'Die Analyse ist im Hosting- und im Wartungs-Abo enthalten.'
|
||||
const RANGES = [7, 30, 90]
|
||||
const CLOCK = new Intl.DateTimeFormat('de-DE', { timeZone: 'Europe/Berlin', hour: '2-digit', minute: '2-digit', hourCycle: 'h23' })
|
||||
|
||||
@@ -21,10 +28,14 @@ const showError = (root) => (err) => { root.innerHTML = `<p class="muted">${esc(
|
||||
|
||||
export async function initAnalyse(root, { projectId } = {}) {
|
||||
if (!root) return
|
||||
if (projectId) analyseSel.projectId = projectId
|
||||
const { projects = [] } = await request('/api/funktionen')
|
||||
const wunsch = projectId || root.dataset?.wunschProjekt || '' // die Kachel in Funktionen legt ihr Projekt hier ab
|
||||
if (root.dataset) delete root.dataset.wunschProjekt
|
||||
if (wunsch) analyseSel.projectId = wunsch
|
||||
const { projects = [], modell } = await request('/api/funktionen')
|
||||
const abo = modell === 'abo' // strikt: nur dann Reiter in der Etage und die Texte A1 bis A3
|
||||
const usable = projects.filter((p) => !analyseOf(p).comingSoon)
|
||||
if (!usable.length) {
|
||||
if (abo) meldeReiter(root, { eintraege: [] })
|
||||
root.innerHTML = '<div class="an"><section class="an-card an-state"><p class="an-empty">Die Analyse ist für deine Websites noch nicht freigeschaltet.</p></section></div>'
|
||||
return
|
||||
}
|
||||
@@ -33,7 +44,10 @@ export async function initAnalyse(root, { projectId } = {}) {
|
||||
const a = analyseOf(project)
|
||||
const active = a.state === 'active'
|
||||
const tab = TABS.includes(analyseSel.tab) ? analyseSel.tab : 'uebersicht'
|
||||
root.innerHTML = `<div class="an">${headHtml(project, usable, { showRange: active && tab === 'uebersicht' })}${active ? tabsHtml(tab) : ''}<div id="an-stage"${active ? ` role="tabpanel" aria-labelledby="an-tab-${tab}"` : ''}></div></div>`
|
||||
if (abo && !active) meldeReiter(root, { eintraege: [] }) // ohne Cockpit keine Reiter; das Cockpit meldet seine gleich nach dem Zeichnen
|
||||
const leiste = active && !abo ? tabsHtml(tab) : ''
|
||||
const panel = !active ? '' : abo ? ` role="tabpanel" aria-label="${reiterLabel(tab)}"` : ` role="tabpanel" aria-labelledby="an-tab-${tab}"`
|
||||
root.innerHTML = `<div class="an">${headHtml(project, usable, { showRange: active && tab === 'uebersicht' })}${leiste}<div id="an-stage"${panel}></div></div>`
|
||||
const stage = root.querySelector('#an-stage')
|
||||
const reload = () => initAnalyse(root).catch(showError(root))
|
||||
// Website wechseln: der Reiter bleibt (analyseSel.tab)
|
||||
@@ -42,11 +56,11 @@ export async function initAnalyse(root, { projectId } = {}) {
|
||||
analyseSel.projectId = ev.target.value
|
||||
reload()
|
||||
})
|
||||
if (a.state === 'active') mountCockpit({ an: root.querySelector('.an'), stage, project, usable, reload })
|
||||
if (a.state === 'active') mountCockpit({ an: root.querySelector('.an'), stage, project, usable, reload, abo, root })
|
||||
else if (a.state === 'disabled') stage.innerHTML = '<section class="an-card an-state"><h3>Analyse ist abgeschaltet</h3><p>Für diese Website ist die Analyse im Moment gesperrt. <button type="button" class="kb-link" data-view="chat">Schreib uns im Chat</button>, dann klären wir das.</p></section>'
|
||||
else if (a.state === 'unknown') stage.innerHTML = '<section class="an-card an-state"><p>Die Daten konnten gerade nicht geladen werden.</p></section>'
|
||||
else if (!a.eligible) stage.innerHTML = `<section class="an-card an-state"><h3>Analyse nicht verfügbar</h3><p>${esc(a.reasonText || 'Für diese Website gerade nicht verfügbar.')}${a.reason === 'kein_abo' ? ` <button type="button" class="${ZU_VERTRAEGE.klasse}" data-view="${ZU_VERTRAEGE.view}">${ZU_VERTRAEGE.text}</button>` : ''}</p></section>`
|
||||
else renderAnalyseActivation(stage, project, reload)
|
||||
else if (!a.eligible) stage.innerHTML = `<section class="an-card an-state"><h3>Analyse nicht verfügbar</h3><p>${esc(abo && a.reason === 'kein_abo' ? KEIN_ABO_ANALYSE : a.reasonText || 'Für diese Website gerade nicht verfügbar.')}${a.reason === 'kein_abo' ? ` <button type="button" class="${ZU_VERTRAEGE.klasse}" data-view="${ZU_VERTRAEGE.view}">${ZU_VERTRAEGE.text}</button>` : ''}</p></section>`
|
||||
else renderAnalyseActivation(stage, project, reload, { modell })
|
||||
}
|
||||
|
||||
// Kopfleiste (analyse-cards.js); Zeitraum nur in der Uebersicht, "Stand" = Uhrzeit des letzten Ladens, today = "Heute bisher" aus der Statistik
|
||||
@@ -73,7 +87,8 @@ function controllerBlock(controller) {
|
||||
return `<div class="an-ctl" id="an-ctl"><p>Auf der Datenschutz-Zusatzseite Deiner Website erscheinen als Verantwortlicher:</p><p><strong>${controllerLines(controller).map(esc).join(' · ')}</strong></p><p>Stimmt etwas nicht? Schreib uns im Chat, bevor Du einschaltest.</p></div>`
|
||||
}
|
||||
|
||||
export function renderAnalyseActivation(stage, project, reload) {
|
||||
export function renderAnalyseActivation(stage, project, reload, { modell } = {}) {
|
||||
const abo = modell === 'abo' // Abo-Modell: Linkzeile statt des langen Zustimmungssatzes, Zielgruppen-Häkchen, audienceConfirmed
|
||||
const a = analyseOf(project)
|
||||
const legal = a.legal || {}
|
||||
const controller = a.controller || { ok: false }
|
||||
@@ -91,7 +106,10 @@ export function renderAnalyseActivation(stage, project, reload) {
|
||||
<p class="an-hint hidden" id="an-hint">Diese Farbe ist für weiße Schrift zu hell. Der Banner nutzt dann Schwarz (#111111).</p>
|
||||
<div id="an-preview"></div>
|
||||
${controllerBlock(controller)}
|
||||
<label class="fn-consent"><input type="checkbox" id="an-avv"><span>Ich beauftrage WEBklar mit der Website-Analyse und stimme dem <a href="${esc(legal.avv || '')}" target="_blank" rel="noopener">Auftragsverarbeitungsvertrag Website-Analyse</a> und der <a href="${esc(legal.lb || '')}" target="_blank" rel="noopener">Leistungsbeschreibung Website-Analyse</a> (Fassung ${esc(a.avvVersion)}) zu. Meine Website richtet sich nicht gezielt an Kinder unter 16 Jahren.</span></label>
|
||||
${abo
|
||||
? `<p class="an-hint" id="an-legal">Es gelten <a href="${esc(legal.lb || '')}" target="_blank" rel="noopener">Leistungsbeschreibung</a> und <a href="${esc(legal.avv || '')}" target="_blank" rel="noopener">Auftragsverarbeitung</a> (Fassung ${esc(a.avvVersion)}).</p>
|
||||
<label class="fn-consent"><input type="checkbox" id="an-zielgruppe"><span>${ZIELGRUPPE}</span></label>`
|
||||
: `<label class="fn-consent"><input type="checkbox" id="an-avv"><span>Ich beauftrage WEBklar mit der Website-Analyse und stimme dem <a href="${esc(legal.avv || '')}" target="_blank" rel="noopener">Auftragsverarbeitungsvertrag Website-Analyse</a> und der <a href="${esc(legal.lb || '')}" target="_blank" rel="noopener">Leistungsbeschreibung Website-Analyse</a> (Fassung ${esc(a.avvVersion)}) zu. Meine Website richtet sich nicht gezielt an Kinder unter 16 Jahren.</span></label>`}
|
||||
<p class="error hidden" id="an-error" role="alert"></p>
|
||||
<div class="abo-actions"><button type="button" class="abo-btn" id="an-on" disabled>Analyse einschalten</button></div>
|
||||
</div>`
|
||||
@@ -104,7 +122,7 @@ export function renderAnalyseActivation(stage, project, reload) {
|
||||
paint()
|
||||
input.addEventListener('input', () => { color = input.value; paint() })
|
||||
stage.querySelectorAll('.an-preset').forEach((b) => b.addEventListener('click', () => { color = b.dataset.color; input.value = color; paint() }))
|
||||
const box = stage.querySelector('#an-avv')
|
||||
const box = stage.querySelector(abo ? '#an-zielgruppe' : '#an-avv')
|
||||
const button = stage.querySelector('#an-on')
|
||||
const error = stage.querySelector('#an-error')
|
||||
box.addEventListener('change', () => { button.disabled = !box.checked || controller.ok !== true })
|
||||
@@ -113,7 +131,7 @@ export function renderAnalyseActivation(stage, project, reload) {
|
||||
button.disabled = true
|
||||
error.classList.add('hidden')
|
||||
try {
|
||||
await request('/api/funktionen/analyse/activate', { method: 'POST', body: { projectId: project.projectId, accentColor: color.toLowerCase(), consent: { accepted: true, avvVersion: a.avvVersion } } })
|
||||
await request('/api/funktionen/analyse/activate', { method: 'POST', body: { projectId: project.projectId, accentColor: color.toLowerCase(), consent: { accepted: true, avvVersion: a.avvVersion }, ...(abo ? { audienceConfirmed: true } : {}) } })
|
||||
reload()
|
||||
} catch (err) {
|
||||
error.textContent = err.message
|
||||
@@ -171,7 +189,7 @@ function wireChart(stage) {
|
||||
svg.addEventListener('pointercancel', hideChartTip)
|
||||
}
|
||||
|
||||
function mountCockpit({ an, stage, project, usable, reload }) {
|
||||
function mountCockpit({ an, stage, project, usable, reload, abo = false, root = null }) {
|
||||
const pid = project.projectId
|
||||
const state = { stats: null, range: 0, stamp: '', rank: { seen: {}, who: {} } }
|
||||
const tabNow = () => (TABS.includes(analyseSel.tab) ? analyseSel.tab : 'uebersicht')
|
||||
@@ -195,7 +213,12 @@ function mountCockpit({ an, stage, project, usable, reload }) {
|
||||
b.setAttribute('aria-selected', String(on))
|
||||
b.tabIndex = on ? 0 : -1
|
||||
})
|
||||
stage.setAttribute('aria-labelledby', `an-tab-${tab}`)
|
||||
if (abo) stage.setAttribute('aria-label', reiterLabel(tab))
|
||||
else stage.setAttribute('aria-labelledby', `an-tab-${tab}`)
|
||||
}
|
||||
// Abo-Modell: den aktiven Reiter an die Etage melden (bei = pickTab); der Teaser der Uebersicht ruft ebenfalls pickTab
|
||||
function melde() {
|
||||
if (abo) meldeReiter(root, { eintraege: reiterEintraege('analyse', ANALYSE_REITER), aktiv: tabNow(), bei: pickTab })
|
||||
}
|
||||
// Anfrage je Zeitraum: Uebersicht und Heatmap teilen sich eine laufende Anfrage. Sie schreibt nichts in state; uebernommen
|
||||
// (adopt) wird die Antwort erst nach current(mine), sonst ueberschriebe eine verspaetete den Stand des neueren Zeitraums.
|
||||
@@ -276,6 +299,7 @@ function mountCockpit({ an, stage, project, usable, reload }) {
|
||||
}
|
||||
function showTab() {
|
||||
paintTabs()
|
||||
melde()
|
||||
paintHead()
|
||||
const tab = tabNow()
|
||||
if (tab === 'heatmap') showHeat()
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Rechtsfunktion: dieses Modul darf die anderen Vertragsarten nie stören. Fällt es aus, funktioniert der Widerruf wie bisher.
|
||||
Das Feld ist nur bei „Projektauftrag“ sichtbar; eine eingegebene, aber unpassende Nummer wird gemeldet und stoppt das Absenden,
|
||||
ein leeres Feld lässt den Widerruf durch (der Widerruf bleibt formfrei). Die Meldung steht in auftrag-ansicht.js. */
|
||||
import { abNummerNormalisiert, texte } from './auftrag-ansicht.js'
|
||||
import { ANREDE_DU, abNummerNormalisiert, texte } from './auftrag-ansicht.js'
|
||||
|
||||
export function initProjektWiderruf(form) {
|
||||
const wrap = form?.querySelector('#ab-nummer-wrap')
|
||||
@@ -39,7 +39,10 @@ export function initProjektWiderruf(form) {
|
||||
const nummer = abNummerNormalisiert(eingabe)
|
||||
if (!nummer) {
|
||||
ev.preventDefault()
|
||||
fehler.textContent = texte().abNummerUngueltig
|
||||
// Neue Seite (data-anrede="du"): die Meldung in du; ohne das Attribut bleibt der Satz aus auftrag-ansicht.js
|
||||
fehler.textContent = form.getAttribute('data-anrede') === ANREDE_DU
|
||||
? 'Die Nummer sieht anders aus als AB-2026-0001. Bitte prüfe die Nummer oder lass das Feld leer.'
|
||||
: texte().abNummerUngueltig
|
||||
fehler.classList.remove('hidden')
|
||||
eigeneMeldung = true
|
||||
feld.focus()
|
||||
|
||||
+20
-14
@@ -16,21 +16,27 @@ const artWert = (a) => (a === 'monteur' ? 'monteurzimmer' : a)
|
||||
const labelVon = (liste, key) => liste.find(([k]) => k === key)?.[1] || ''
|
||||
const enc = encodeURIComponent
|
||||
|
||||
/** Angaben der Anfrage als Definitionsliste (nur Text). */
|
||||
export function anfrageListe(a) {
|
||||
const s = a && typeof a === 'object' ? a : {}
|
||||
const portale = (Array.isArray(s.portale) ? s.portale : []).map((p) => labelVon(PORTALE, p)).filter(Boolean).join(', ')
|
||||
const zeilen = [
|
||||
['Art der Unterkunft', labelVon(ARTEN, artWert(s.art))],
|
||||
['Anzahl Einheiten', Number.isInteger(s.einheiten) ? String(s.einheiten) : ''],
|
||||
/** Angaben der Anfrage als Zeilen [{ name, wert }] (Definitionsliste und Gruppe der Einschalt-Seite); ohne Objekt leer. */
|
||||
export function anfrageZeilen(a) {
|
||||
if (a === null || typeof a !== 'object') return []
|
||||
const portale = (Array.isArray(a.portale) ? a.portale : []).map((p) => labelVon(PORTALE, p)).filter(Boolean).join(', ')
|
||||
return [
|
||||
['Art der Unterkunft', labelVon(ARTEN, artWert(a.art))],
|
||||
['Anzahl Einheiten', Number.isInteger(a.einheiten) ? String(a.einheiten) : ''],
|
||||
['Portale', portale || 'keine'],
|
||||
['Nachricht', text(s.nachricht)],
|
||||
].filter(([, wert]) => wert !== '')
|
||||
return h('dl', { class: 'sh-fakten' }, zeilen.map(([name, wert]) => h('div', null, h('dt', null, name), h('dd', null, wert))))
|
||||
['Nachricht', text(a.nachricht)],
|
||||
].filter(([, wert]) => wert !== '').map(([name, wert]) => ({ name, wert }))
|
||||
}
|
||||
|
||||
/** Formular „Buchungskalender anfragen“ in root; nachSenden(anfrage) wird nach dem Speichern aufgerufen (die Ansicht zeichnet sich neu). */
|
||||
export function renderAnfrageFormular(root, { projectId, vorgabe = null, preis = KALENDER_PREIS, nachSenden } = {}) {
|
||||
/** Angaben der Anfrage als Definitionsliste (nur Text). */
|
||||
export const anfrageListe = (a) => h('dl', { class: 'sh-fakten' }, anfrageZeilen(a).map(({ name, wert }) => h('div', null, h('dt', null, name), h('dd', null, wert))))
|
||||
|
||||
/**
|
||||
* Formular „Buchungskalender anfragen“ in root; nachSenden(anfrage) wird nach dem Speichern aufgerufen (die Ansicht zeichnet sich neu).
|
||||
* modell === 'abo': die Stichpunkte nennen „Im Wartungs-Abo enthalten.“ statt des Preises und „einschalten“ statt „bestellen“.
|
||||
*/
|
||||
export function renderAnfrageFormular(root, { projectId, vorgabe = null, preis = KALENDER_PREIS, nachSenden, modell } = {}) {
|
||||
const abo = modell === 'abo'
|
||||
const v = vorgabe && typeof vorgabe === 'object' ? vorgabe : {}
|
||||
const art = h('select', null, [h('option', { value: '' }, 'Bitte wählen …'), ...ARTEN.map(([w, n]) => h('option', { value: w }, n))])
|
||||
art.value = ARTEN.some(([k]) => k === artWert(v.art)) ? artWert(v.art) : ''
|
||||
@@ -91,10 +97,10 @@ export function renderAnfrageFormular(root, { projectId, vorgabe = null, preis =
|
||||
h('h3', null, 'Buchungskalender anfragen'),
|
||||
h('p', null, 'Mit dem Buchungskalender zeigst du auf deiner Website, wann deine Unterkunft frei ist. Gäste fragen direkt an, du gibst frei, und die Rechnung entsteht automatisch.'),
|
||||
h('ul', null,
|
||||
h('li', null, `${preis} einmalig für den Einbau in deine Website.`),
|
||||
h('li', null, abo ? 'Im Wartungs-Abo enthalten.' : `${preis} einmalig für den Einbau in deine Website.`),
|
||||
h('li', null, 'Belegungskalender mit Abgleich zu Booking.com und Airbnb (über iCal).'),
|
||||
h('li', null, 'Rechnung in deinem Namen und optional Online-Zahlung auf dein eigenes Stripe-Konto.'),
|
||||
h('li', null, 'Wir sehen uns deine Anfrage kurz an. Erst nach unserer Zusage kannst du bestellen.')))
|
||||
h('li', null, abo ? 'Wir sehen uns deine Anfrage kurz an. Erst nach unserer Zusage kannst du einschalten.' : 'Wir sehen uns deine Anfrage kurz an. Erst nach unserer Zusage kannst du bestellen.')))
|
||||
const karte = h('section', { class: 'kb-panel sh-anfrage' }, erklaerung, form)
|
||||
root.replaceChildren(karte)
|
||||
return karte
|
||||
|
||||
+66
-10
@@ -1,9 +1,15 @@
|
||||
/* Buchungskalender: Preis-Editor einer Unterkunft (Staffeln, Reinigung, Zusatzpersonen, Rabatt, Steuer, Frist, Stornobedingungen) mit Beispielrechnung.
|
||||
GET|PUT /api/unterkunft/unterkuenfte/:id/preise => { preise:{ staffeln:[{abNaechten,preisProNachtCent}], reinigungCent, personenInklusive, aufpreisPersonNachtCent,
|
||||
/* Buchungskalender: Preis-Editor einer Unterkunft (Staffeln, Preise je Personenzahl, Reinigung, Zusatzpersonen, Rabatt, Steuer, Frist, Stornobedingungen) mit Beispielrechnung.
|
||||
GET|PUT /api/unterkunft/unterkuenfte/:id/preise => { preise:{ staffeln:[{abNaechten,preisProNachtCent,personenCent?}], reinigungCent, personenInklusive, aufpreisPersonNachtCent,
|
||||
direktrabattProzent, steuerart:'kleinunternehmer'|'regel', ustUebernachtung, ustReinigung, fristTage, stornobedingungen } | null }
|
||||
POST /api/unterkunft/preisvorschau { unterkunftId, von, bis, personen } => { naechte, posten, summen:{ bruttoCent } } (rechnet mit den gespeicherten Preisen). */
|
||||
POST /api/unterkunft/preisvorschau { unterkunftId, von, bis, personen } => { naechte, posten, summen:{ bruttoCent } } (rechnet mit den gespeicherten Preisen).
|
||||
Zwei Preismodelle, umgeschaltet mit dem Haken "Preis hängt von der Personenzahl ab" (#bk-p-personenmodus, nur bei unterkunft.maxPersonen >= 2):
|
||||
- aus: ein Preis pro Nacht je Staffel; "Personen im Preis inklusive" und "Aufpreis je weiterer Person" gelten wie bisher.
|
||||
- an: je Staffel k Euro-Felder (k = min(maxPersonen, 10), "1 Person" ... "k Personen") = Preis pro Nacht fuer die ganze Unterkunft; gesendet wird
|
||||
staffeln:[{abNaechten, preisProNachtCent: personenCent[0], personenCent}]. Die beiden Personenfelder sind dann ausgeblendet, ihre Werte gehen unveraendert mit.
|
||||
Beim Laden ist der Haken an, wenn die gespeicherten Staffeln personenCent haben (vorhandene Werte eingesetzt, fehlende leer, ueberzaehlige verworfen).
|
||||
ctx.nurLesen sperrt alle Felder samt Haken. */
|
||||
|
||||
import { abschnitt, allgemeinerFehler, euroEingabe, euroText, feld, h, setzeFehler, zeigeFehler } from './shop-dom.js'
|
||||
import { abschnitt, allgemeinerFehler, euroEingabe, euroText, feld, h, hakenFeld, setzeFehler, zeigeFehler } from './shop-dom.js'
|
||||
import { enc, heuteBerlin, plusTage, zeichen, zeigeApiFehler } from './buchungen-uk-hilfe.js'
|
||||
|
||||
// Wortgleich zu docs/funktionen/unterkunft-storno-muster.md, Abschnitt "Stornobedingungen (Muster)" (ohne Markdown-Fettdruck); test/buchungenUi.test.js vergleicht beides.
|
||||
@@ -30,6 +36,7 @@ export const STORNO_MUSTER = [
|
||||
].join('\n')
|
||||
export const STORNO_HINWEIS = 'Vorlage – bitte an deine Bedingungen anpassen, keine Rechtsberatung.'
|
||||
const STANDARD = { staffeln: [{ abNaechten: 1, preisProNachtCent: null }], reinigungCent: 0, personenInklusive: 2, aufpreisPersonNachtCent: 0, direktrabattProzent: 0, steuerart: 'kleinunternehmer', ustUebernachtung: 7, ustReinigung: 7, fristTage: 3, stornobedingungen: '' }
|
||||
const PERSONEN_MAX = 10
|
||||
const ganzzahl = (steuer) => { const t = String(steuer.value).trim(); return /^\d{1,9}$/.test(t) ? Number(t) : null }
|
||||
|
||||
export async function zeichnePreise(ctx, unterkunft) {
|
||||
@@ -44,13 +51,27 @@ export async function zeichnePreise(ctx, unterkunft) {
|
||||
wurzel.replaceChildren(h('p', { class: 'sh-fehler', role: 'alert' }, err.message))
|
||||
return wurzel
|
||||
}
|
||||
// Preise je Personenzahl: nur sinnvoll, wenn mehr als eine Person in die Unterkunft passt; k Spalten (1 bis k Personen)
|
||||
const gaeste = Number.isInteger(unterkunft?.maxPersonen) ? Math.min(unterkunft.maxPersonen, PERSONEN_MAX) : 0
|
||||
const modusMoeglich = gaeste >= 2
|
||||
const modusBox = h('input', { type: 'checkbox', id: 'bk-p-personenmodus' })
|
||||
modusBox.checked = modusMoeglich && Array.isArray(preise.staffeln) && preise.staffeln.some((s) => Array.isArray(s?.personenCent) && s.personenCent.length > 0)
|
||||
const personenText = (n) => `${n} ${n === 1 ? 'Person' : 'Personen'}`
|
||||
const zeilenEl = h('div', { class: 'bk-staffeln' })
|
||||
const zeile = (s) => {
|
||||
const ab = h('input', { type: 'text', inputmode: 'numeric', 'aria-label': 'Ab Nächten', 'data-staffel': 'ab', autocomplete: 'off' })
|
||||
ab.value = String(s.abNaechten ?? '')
|
||||
const preis = euroEingabe({ cent: s.preisProNachtCent, label: 'Preis pro Nacht in Euro' })
|
||||
preis.dataset.staffel = 'preis'
|
||||
const el = h('div', { class: 'bk-staffel', 'data-staffel-zeile': true }, 'ab ', ab, ' Nächten: ', preis, ' € pro Nacht ',
|
||||
const spalten = Array.from({ length: modusMoeglich ? gaeste : 0 }, (_, i) => {
|
||||
const eingabe = euroEingabe({ cent: Array.isArray(s.personenCent) ? s.personenCent[i] : null, label: `Preis pro Nacht in Euro für ${personenText(i + 1)}` })
|
||||
eingabe.dataset.staffel = 'person'
|
||||
eingabe.dataset.personen = String(i + 1)
|
||||
return h('label', { class: 'bk-personpreis' }, h('span', null, personenText(i + 1)), eingabe)
|
||||
})
|
||||
const el = h('div', { class: 'bk-staffel', 'data-staffel-zeile': true }, 'ab ', ab, ' Nächten: ',
|
||||
h('span', { class: 'bk-preis-einzeln' }, preis, ' € pro Nacht '),
|
||||
h('span', { class: 'bk-personenpreise', 'data-personenpreise': true }, spalten),
|
||||
h('button', { type: 'button', class: 'kb-btn-outline', 'data-aktion': 'staffel-weg', 'aria-label': 'Staffel entfernen', onclick: () => { if (zeilenEl.children.length > 1) { el.remove(); halteErste() } } }, '−'))
|
||||
return el
|
||||
}
|
||||
@@ -92,14 +113,33 @@ export async function zeichnePreise(ctx, unterkunft) {
|
||||
}
|
||||
for (const el of [naechteEl, personenEl]) el.addEventListener('change', vorschau)
|
||||
|
||||
const modusFeld = hakenFeld('personenmodus', modusBox, 'Preis hängt von der Personenzahl ab', 'Zum Beispiel 55 € für 1 Person, 70 € für 2 Personen und 82 € für 3 Personen pro Nacht.')
|
||||
modusFeld.hidden = !modusMoeglich
|
||||
modusBox.hidden = !modusMoeglich
|
||||
const modusHinweis = h('p', { class: 'sh-hinweis', 'data-hinweis': 'personenpreise' }, 'Preis pro Nacht für die ganze Unterkunft, je nach Anzahl der Gäste.')
|
||||
const inklusiveFeld = feld('personenInklusive', 'Personen im Preis inklusive', inklusive)
|
||||
const aufpreisFeld = feld('aufpreisPersonNachtCent', 'Aufpreis je weiterer Person und Nacht in Euro', aufpreis)
|
||||
/** Haken an: je Staffel die Spalten je Personenzahl statt des einen Preises; die zwei Personenfelder des alten Modells verschwinden (ihre Werte gehen unveraendert mit). */
|
||||
const zeigeModus = () => {
|
||||
const an = modusMoeglich && modusBox.checked
|
||||
for (const el of zeilenEl.querySelectorAll('.bk-preis-einzeln')) el.hidden = an
|
||||
for (const el of zeilenEl.querySelectorAll('.bk-personenpreise')) el.hidden = !an
|
||||
modusHinweis.hidden = !an
|
||||
inklusiveFeld.hidden = an
|
||||
aufpreisFeld.hidden = an
|
||||
}
|
||||
modusBox.addEventListener('change', zeigeModus)
|
||||
const staffelDazu = () => { zeilenEl.append(zeile({ abNaechten: null, preisProNachtCent: null })); zeigeModus() }
|
||||
|
||||
const form = h('form', { novalidate: true, class: 'bk-preisform', onsubmit: (ev) => ev.preventDefault() },
|
||||
h('h4', null, 'Preis pro Nacht (Staffeln)'),
|
||||
h('p', { class: 'sh-hinweis' }, 'Je länger der Aufenthalt, desto günstiger die Nacht: Die passende Staffel gilt für alle Nächte der Buchung.'),
|
||||
modusFeld, modusHinweis,
|
||||
zeilenEl, h('div', { class: 'sh-fehler', 'data-fehler': 'staffeln', role: 'alert' }),
|
||||
h('button', { type: 'button', class: 'kb-btn-outline', 'data-aktion': 'staffel-dazu', onclick: () => { zeilenEl.append(zeile({ abNaechten: null, preisProNachtCent: null })) } }, '+ Staffel'),
|
||||
h('button', { type: 'button', class: 'kb-btn-outline', 'data-aktion': 'staffel-dazu', onclick: staffelDazu }, '+ Staffel'),
|
||||
feld('reinigungCent', 'Endreinigung in Euro', reinigung),
|
||||
feld('personenInklusive', 'Personen im Preis inklusive', inklusive),
|
||||
feld('aufpreisPersonNachtCent', 'Aufpreis je weiterer Person und Nacht in Euro', aufpreis),
|
||||
inklusiveFeld,
|
||||
aufpreisFeld,
|
||||
feld('direktrabattProzent', 'Direktbuchungsrabatt in %', rabatt, 'Ganze Zahl von 0 bis 50. Gilt für Übernachtung und Zusatzpersonen, nicht für die Reinigung.'),
|
||||
feld('steuerart', 'Steuer', steuerart), ustFelder,
|
||||
feld('fristTage', 'Frist für die Buchung nach Freigabe in Tagen', frist, '1 bis 14 Tage.'),
|
||||
@@ -109,6 +149,7 @@ export async function zeichnePreise(ctx, unterkunft) {
|
||||
h('p', { class: 'sh-hinweis', 'data-hinweis': 'storno-muster' }, STORNO_HINWEIS)),
|
||||
allgemeinerFehler(),
|
||||
h('div', { class: 'bk-aktionen' }, ctx.nurLesen ? null : h('button', { type: 'button', class: 'kb-btn', 'data-aktion': 'preise-speichern', onclick: speichern }, 'Preise speichern')))
|
||||
zeigeModus()
|
||||
if (ctx.nurLesen) for (const el of form.querySelectorAll('input, select, textarea, button')) el.disabled = true
|
||||
// Ein Feldfehler (z. B. Platzhalter in den Stornobedingungen) verschwindet, sobald im Feld getippt wird
|
||||
for (const el of form.querySelectorAll('[data-feld]')) {
|
||||
@@ -125,8 +166,16 @@ export async function zeichnePreise(ctx, unterkunft) {
|
||||
const fehler = {}
|
||||
const staffeln = []
|
||||
let staffelFehler = ''
|
||||
const personenModus = modusMoeglich && modusBox.checked
|
||||
for (const z of zeilenEl.children) {
|
||||
const ab = ganzzahl(z.querySelector('[data-staffel="ab"]'))
|
||||
if (personenModus) {
|
||||
const spalten = [...z.querySelectorAll('[data-staffel="person"]')].map((el) => el.inCent())
|
||||
if (ab === null || ab < 1 || spalten.some((p) => p === null)) staffelFehler ||= 'Bitte gib bei jeder Staffel Nächte (ganze Zahl) und für jede Personenzahl einen Preis an.'
|
||||
else if (spalten.some((p) => p < 100 || p > 100000)) staffelFehler ||= 'Der Preis pro Nacht muss zwischen 1,00 € und 1.000,00 € liegen.'
|
||||
else staffeln.push({ abNaechten: ab, preisProNachtCent: spalten[0], personenCent: spalten })
|
||||
continue
|
||||
}
|
||||
const p = z.querySelector('[data-staffel="preis"]').inCent()
|
||||
if (ab === null || ab < 1 || p === null) staffelFehler ||= 'Bitte gib bei jeder Staffel Nächte (ganze Zahl) und einen Preis an.'
|
||||
else if (p < 100 || p > 100000) staffelFehler ||= 'Der Preis pro Nacht muss zwischen 1,00 € und 1.000,00 € liegen.'
|
||||
@@ -141,8 +190,15 @@ export async function zeichnePreise(ctx, unterkunft) {
|
||||
const zahl = (name, steuer, text, min, max) => { const n = ganzzahl(steuer); if (n === null || n < min || n > max) { fehler[name] = text; return null } return n }
|
||||
const reinigungCent = reinigung.istLeer() ? 0 : reinigung.inCent()
|
||||
if (reinigungCent === null) fehler.reinigungCent = 'Bitte gib einen Betrag wie 60,00 an.'
|
||||
const aufpreisCent = aufpreis.istLeer() ? 0 : aufpreis.inCent()
|
||||
// Im Personen-Modus sind "inklusive" und "Aufpreis" ausgeblendet: ihr Wert geht unveraendert mit; Unbrauchbares im unsichtbaren Feld bleibt ohne Fehlermeldung beim gespeicherten Wert
|
||||
let aufpreisCent = aufpreis.istLeer() ? 0 : aufpreis.inCent()
|
||||
if (personenModus && (aufpreisCent === null || aufpreisCent > 100000)) aufpreisCent = Number.isInteger(preise.aufpreisPersonNachtCent) ? preise.aufpreisPersonNachtCent : 0
|
||||
if (aufpreisCent === null) fehler.aufpreisPersonNachtCent = 'Bitte gib einen Betrag wie 15,00 an.'
|
||||
const inklusiveAlt = Number.isInteger(preise.personenInklusive) && preise.personenInklusive >= 1 && preise.personenInklusive <= 50 ? preise.personenInklusive : 2
|
||||
const inklusiveNeu = ganzzahl(inklusive)
|
||||
const inklusiveWert = personenModus
|
||||
? (inklusiveNeu !== null && inklusiveNeu >= 1 && inklusiveNeu <= 50 ? inklusiveNeu : inklusiveAlt)
|
||||
: zahl('personenInklusive', inklusive, 'Bitte gib eine ganze Zahl ab 1 an.', 1, 1000)
|
||||
const regel = steuerart.value === 'regel'
|
||||
const ustWert = (name, steuer, alt) => {
|
||||
if (!regel) return ganzzahl(steuer) ?? alt // andere Steuerart: gespeicherten Wert unveraendert zurueckschicken
|
||||
@@ -155,7 +211,7 @@ export async function zeichnePreise(ctx, unterkunft) {
|
||||
if (!fehler.stornobedingungen && /\[[^\]]*\]/.test(storn)) fehler.stornobedingungen = 'Bitte ersetze die Platzhalter in eckigen Klammern durch deine Werte.'
|
||||
const body = {
|
||||
staffeln, reinigungCent, aufpreisPersonNachtCent: aufpreisCent,
|
||||
personenInklusive: zahl('personenInklusive', inklusive, 'Bitte gib eine ganze Zahl ab 1 an.', 1, 1000),
|
||||
personenInklusive: inklusiveWert,
|
||||
direktrabattProzent: zahl('direktrabattProzent', rabatt, 'Bitte gib eine ganze Zahl von 0 bis 50 an.', 0, 50),
|
||||
steuerart: steuerart.value,
|
||||
ustUebernachtung: ustWert('ustUebernachtung', ustUe, preise.ustUebernachtung ?? 7),
|
||||
|
||||
@@ -165,7 +165,8 @@ export async function render(panel, ctx) {
|
||||
zeigeLink(antwort?.buchungsLink, a)
|
||||
} catch (err) {
|
||||
knopf.disabled = false
|
||||
if (err.status === 409) zeigeFehler(form, { message: BELEGT, fehler: { _: BELEGT } })
|
||||
// 409 heisst nicht immer belegt (auch zustimmung_fehlt, status_ungueltig): nur der Code zeitraum_belegt bekommt den Belegt-Text
|
||||
if (err.status === 409 && err.fehler === 'zeitraum_belegt') zeigeFehler(form, { message: BELEGT, fehler: { _: BELEGT } })
|
||||
else zeigeFehler(form, fehlerFuerFormular(err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,11 @@
|
||||
.bk-farbe { width: 16px; height: 16px; border-radius: 4px; border: 1px solid var(--kb-border); display: inline-block; }
|
||||
.bk-staffel { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-bottom: 8px; }
|
||||
.bk-staffel input { width: 90px; }
|
||||
/* Preise je Personenzahl: ein Euro-Feld je Gaestezahl unter der Beschriftung; bricht auf dem Handy um, kein Seitwaertsscrollen */
|
||||
.bk-personenpreise { display: flex; flex-wrap: wrap; gap: 8px 12px; align-items: flex-end; min-width: 0; max-width: 100%; }
|
||||
.bk-personenpreise[hidden], .bk-preis-einzeln[hidden] { display: none; }
|
||||
.bk-personpreis { display: grid; gap: 2px; font-size: 13px; font-weight: 600; }
|
||||
.bk-personpreis > span { white-space: nowrap; }
|
||||
.bk-vorschau { font-weight: 700; }
|
||||
.bk-import { display: flex; align-items: center; gap: 4px 12px; flex-wrap: wrap; }
|
||||
.bk-anleitung details { margin: 8px 0; }
|
||||
|
||||
+42
-13
@@ -5,8 +5,9 @@
|
||||
Helfer: shop-dom.js (h, request, Karten), Styles: buchungen.css (bk-) und shop.css (sh-). Fremdtext nur ueber textContent. */
|
||||
|
||||
import { fehlerKarte, h, infoKarte, request } from './shop-dom.js'
|
||||
import { statusKarte } from './funktion-bestellen.js'
|
||||
import { anfrageKarte, renderAnfrageFormular } from './buchungen-anfrage.js'
|
||||
import { renderEinschaltSeite, statusKarte } from './funktion-bestellen.js'
|
||||
import { anfrageKarte, anfrageZeilen, renderAnfrageFormular } from './buchungen-anfrage.js'
|
||||
import { meldeReiter, reiterEintraege } from './fn-reiter.js'
|
||||
|
||||
/** Die Reiter: jeder ein Modul mit render(panel, ctx), erst beim ersten Oeffnen geladen. */
|
||||
export const REITER = [
|
||||
@@ -27,33 +28,42 @@ export function vergissAuswahl() {
|
||||
let S = null // aktuelle Sitzung; eine alte Antwort darf eine neuere nicht ueberschreiben
|
||||
|
||||
const textOr = (x) => (typeof x === 'string' && x.trim() ? x : '')
|
||||
// Abo-Modell (Antwort von GET /api/funktionen, strikt): die Reiter zeichnet die Etage, die Ansicht meldet sie (fn-reiter.js)
|
||||
const istAbo = (s) => s.modell === 'abo'
|
||||
/** Mitarbeiter in der Kundenansicht (window.KbView, von der Hülle gesetzt): Einschalten bleibt gesperrt. */
|
||||
const inKundenansicht = () => typeof window !== 'undefined' && Boolean(window.KbView?.id?.())
|
||||
/** Eine Ansicht ohne Reiter meldet die leere Liste, damit die Etage nicht die Reiter der vorigen stehen laesst. */
|
||||
const keineReiter = (s) => { if (istAbo(s)) meldeReiter(s.root, { eintraege: [] }) }
|
||||
const zustandVon = (projekt) => projekt?.features?.unterkunft?.state || 'off'
|
||||
|
||||
export function initBuchungen(root, { projectId } = {}) {
|
||||
export function initBuchungen(root, { projectId, einschalten } = {}) {
|
||||
if (!root) return Promise.resolve()
|
||||
const wunsch = projectId || root.dataset?.wunschProjekt || '' // die Kachel in Funktionen legt ihr Projekt hier ab
|
||||
if (root.dataset) delete root.dataset.wunschProjekt
|
||||
return starte(root, wunsch)
|
||||
return starte(root, wunsch, einschalten)
|
||||
}
|
||||
|
||||
async function starte(root, projectId) {
|
||||
async function starte(root, projectId, einschalten) {
|
||||
if (projectId) auswahl.projectId = projectId
|
||||
const s = { root, projects: [], projekt: null, status: null, lauf: 0, buehne: null, meldungEl: null }
|
||||
const s = { root, projects: [], modell: undefined, projekt: null, status: null, lauf: 0, buehne: null, meldungEl: null, einschalten }
|
||||
S = s
|
||||
let funktionen
|
||||
try {
|
||||
funktionen = await request('/api/funktionen')
|
||||
} catch (err) {
|
||||
if (S === s) root.replaceChildren(fehlerKarte(err.message, () => starte(root, auswahl.projectId)))
|
||||
if (S === s) root.replaceChildren(fehlerKarte(err.message, () => starte(root, auswahl.projectId, einschalten)))
|
||||
return
|
||||
}
|
||||
if (S !== s) return
|
||||
s.modell = funktionen?.modell
|
||||
if (!funktionen?.unterkunft?.verfuegbar) {
|
||||
keineReiter(s)
|
||||
root.replaceChildren(infoKarte(NICHT_VERFUEGBAR, 'Sobald er für dich bereit ist, findest du ihn hier.'))
|
||||
return
|
||||
}
|
||||
s.projects = Array.isArray(funktionen.projects) ? funktionen.projects : []
|
||||
if (!s.projects.length) {
|
||||
keineReiter(s)
|
||||
root.replaceChildren(h('div', { class: 'kb-panel kb-empty' }, 'Noch keine Website-Projekte zugewiesen.'))
|
||||
return
|
||||
}
|
||||
@@ -99,6 +109,7 @@ const ladeStatus = (projectId) => request(`/api/unterkunft/status?projectId=${en
|
||||
async function zeigeProjekt(s) {
|
||||
const lauf = ++s.lauf
|
||||
const aktuell = () => S === s && s.lauf === lauf
|
||||
keineReiter(s)
|
||||
s.buehne.replaceChildren(h('p', { class: 'kb-empty' }, 'Lade…'))
|
||||
let status
|
||||
try {
|
||||
@@ -113,6 +124,7 @@ async function zeigeProjekt(s) {
|
||||
|
||||
function zeichneAnsicht(s, { status, aktuell }) {
|
||||
s.status = status
|
||||
keineReiter(s) // die Ansicht mit Reitern (zeigeReiter) meldet ihre Liste gleich danach
|
||||
const zustand = status?.state || zustandVon(s.projekt)
|
||||
if (zustand === 'setup' || zustand === 'active' || zustand === 'disabled') zeigeReiter(s, { status, aktuell, zustand })
|
||||
else if (zustand === 'ordered') zeigeBestellung(s)
|
||||
@@ -126,7 +138,19 @@ function zeigeAnfrage(s, { status, aktuell }) {
|
||||
const ansicht = status?.anfrage
|
||||
// nach einem 409 bleibt die Meldung des Servers sichtbar, waehrend die Ansicht neu laedt
|
||||
const neuLaden = async (o) => { await zeigeProjekt(s); if (o?.meldung && S === s) zeigeMeldung(s, o.meldung, 'fehler') }
|
||||
if (ansicht?.status === 'angenommen') {
|
||||
if (ansicht?.status === 'angenommen' && istAbo(s)) {
|
||||
// Abo-Modell: die Zusage fuehrt zum Einschalten (kein Preis, keine Haken, keine Bestellung)
|
||||
const wurzel = h('div', { class: 'sh-wurzel' })
|
||||
s.buehne.replaceChildren(wurzel)
|
||||
renderEinschaltSeite(wurzel, {
|
||||
ctx: { projectId, aktuell },
|
||||
daten: { offen: [], kundenansicht: inKundenansicht() },
|
||||
key: 'unterkunft',
|
||||
zusammenfassung: () => (ansicht.anfrage ? [{ schritt: 'anfrage', titel: 'Deine Anfrage', zeilen: anfrageZeilen(ansicht.anfrage).map(({ name, wert }) => `${name}: ${wert}`) }] : []),
|
||||
einschalten: s.einschalten,
|
||||
nachEinschalten: () => zeigeProjekt(s),
|
||||
})
|
||||
} else if (ansicht?.status === 'angenommen') {
|
||||
s.buehne.replaceChildren(infoKarte('Deine Anfrage wurde angenommen', 'Wir melden uns bei dir mit den nächsten Schritten zur Bestellung.', { chat: true }))
|
||||
} else if (ansicht?.status === 'offen') {
|
||||
s.buehne.replaceChildren(anfrageKarte(ansicht, { projectId, neuLaden }))
|
||||
@@ -139,6 +163,7 @@ function zeigeAnfrage(s, { status, aktuell }) {
|
||||
projectId,
|
||||
vorgabe: ansicht?.anfrage || null,
|
||||
preis: textOr(s.projekt?.features?.unterkunft?.priceLabel) || '199 €',
|
||||
modell: s.modell,
|
||||
nachSenden: async (_a, o) => { s.neueAnfrage = false; if (aktuell()) await neuLaden(o) },
|
||||
})
|
||||
}
|
||||
@@ -171,15 +196,16 @@ function zeigeReiter(s, { status, aktuell, zustand }) {
|
||||
zeigeMeldung: (meldung, art) => { if (aktuell()) zeigeMeldung(s, meldung, art) },
|
||||
neuLaden,
|
||||
}
|
||||
const abo = istAbo(s) // Abo-Modell: keine eigene Leiste, die Etage zeichnet sie (kn:reiter)
|
||||
const panel = h('div', { class: 'sh-panel', role: 'tabpanel', id: 'bk-panel', tabindex: '0' })
|
||||
let aktiv = null
|
||||
let zaehler = 0
|
||||
const knoepfe = REITER.map((r) => h('button', {
|
||||
const knoepfe = abo ? [] : REITER.map((r) => h('button', {
|
||||
type: 'button', role: 'tab', class: 'sh-reiter', id: `bk-tab-${r.key}`, 'aria-controls': 'bk-panel', 'data-reiter': r.key,
|
||||
onclick: () => waehleReiter(r.key),
|
||||
}, r.label))
|
||||
const leiste = h('div', { class: 'sh-reiterleiste', role: 'tablist', 'aria-label': 'Bereiche des Buchungskalenders' }, knoepfe)
|
||||
leiste.addEventListener('keydown', (ev) => {
|
||||
const leiste = abo ? null : h('div', { class: 'sh-reiterleiste', role: 'tablist', 'aria-label': 'Bereiche des Buchungskalenders' }, knoepfe)
|
||||
leiste?.addEventListener('keydown', (ev) => {
|
||||
const i = REITER.findIndex((r) => r.key === aktiv)
|
||||
const ziel = { ArrowRight: (i + 1) % REITER.length, ArrowLeft: (i + REITER.length - 1) % REITER.length, Home: 0, End: REITER.length - 1 }[ev.key]
|
||||
if (ziel === undefined) return
|
||||
@@ -197,7 +223,10 @@ function zeigeReiter(s, { status, aktuell, zustand }) {
|
||||
k.setAttribute('aria-selected', String(an))
|
||||
k.tabIndex = an ? 0 : -1
|
||||
}
|
||||
panel.setAttribute('aria-labelledby', `bk-tab-${reiter.key}`)
|
||||
if (abo) {
|
||||
panel.setAttribute('aria-label', reiter.label)
|
||||
meldeReiter(s.root, { eintraege: reiterEintraege('buchungen', REITER), aktiv: reiter.key, bei: (neu) => waehleReiter(neu) })
|
||||
} else panel.setAttribute('aria-labelledby', `bk-tab-${reiter.key}`)
|
||||
zeigeMeldung(s, '')
|
||||
panel.replaceChildren()
|
||||
reiter.laden()
|
||||
@@ -207,6 +236,6 @@ function zeigeReiter(s, { status, aktuell, zustand }) {
|
||||
const hinweis = zustand === 'setup' ? 'Wir bauen deinen Buchungskalender gerade ein. Du kannst schon Unterkünfte, Preise und Einstellungen vorbereiten.'
|
||||
: zustand === 'disabled' ? 'Dein Buchungskalender ist abgeschaltet. Du kannst bestehende Buchungen noch ansehen und bearbeiten.' : ''
|
||||
const hinweisBox = h('div', { class: 'sh-hinweise' }, hinweis ? h('div', { class: `sh-box ${zustand === 'disabled' ? 'sh-box-warn' : 'sh-box-info'}`, role: 'note', 'data-hinweis': zustand }, hinweis) : null)
|
||||
s.buehne.replaceChildren(hinweisBox, leiste, panel)
|
||||
s.buehne.replaceChildren(...[hinweisBox, leiste, panel].filter(Boolean))
|
||||
waehleReiter(auswahl.reiter)
|
||||
}
|
||||
+48
-11
@@ -44,9 +44,9 @@
|
||||
|
||||
<div class="brand">WEBKLAR</div>
|
||||
<h1>Datenschutzerklärung</h1>
|
||||
<p class="meta">Stand 07.10.2026</p>
|
||||
<p class="meta">Stand 22.10.2026</p>
|
||||
|
||||
<p>Hier erfahren Sie, welche personenbezogenen Daten wir verarbeiten, wenn Sie unsere Websites besuchen, unser Kundenportal nutzen oder mit uns einen Vertrag schließen, und welche Rechte Sie haben.</p>
|
||||
<p>Hier erfahren Sie, welche personenbezogenen Daten wir verarbeiten, wenn Sie unsere Websites besuchen, unser Kundenportal nutzen, mit uns einen Vertrag schließen oder wenn wir auf Ihre öffentliche Anfrage nach einer Website antworten, und welche Rechte Sie haben.</p>
|
||||
|
||||
<h2>1. Verantwortlicher</h2>
|
||||
<p>WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, vertreten durch die Gesellschafter Kenso Grimm und Justin Klein<br>
|
||||
@@ -55,6 +55,7 @@
|
||||
|
||||
<h2>2. Geltungsbereich</h2>
|
||||
<p>Diese Erklärung gilt für die Website webklar.com, für das Kundenportal project.webklar.com einschließlich der Vorschauseiten unter *.project.webklar.com und für unsere Leistungen, insbesondere unsere Abos (Hosting-Abo und Wartungs-Abo).</p>
|
||||
<p>Die Erklärung gilt auch für Menschen und Betriebe, die im Netz öffentlich nach einer Website suchen und auf deren Anfrage wir antworten (Abschnitt 11). Das können auch Privatpersonen sein, zum Beispiel in Frage-Foren.</p>
|
||||
<p>Websites, die wir im Auftrag unserer Kunden betreiben, verantwortet der jeweilige Kunde. Dort verarbeiten wir Daten als Auftragsverarbeiter nach den Weisungen des Kunden. Informationen dazu finden Sie in der Datenschutzerklärung der jeweiligen Website.</p>
|
||||
|
||||
<h2>3. Hosting, Server-Protokolle und Überwachung der Erreichbarkeit</h2>
|
||||
@@ -65,17 +66,24 @@
|
||||
<p>Mit UptimeRobot haben wir einen Vertrag über die Auftragsverarbeitung geschlossen; er ist Teil der Nutzungsbedingungen von UptimeRobot. UptimeRobot verarbeitet diese Daten auch außerhalb der EU, insbesondere in den USA. Grundlage dafür sind die Standardvertragsklauseln der EU-Kommission (Art. 46 Abs. 2 lit. c DSGVO). Sie sind Teil des Auftragsverarbeitungsvertrags von UptimeRobot (uptimerobot.com/dpa); eine Kopie erhalten Sie auch bei uns. Rechtsgrundlage ist für die Websites unserer Abo-Kunden Art. 6 Abs. 1 lit. b DSGVO, weil die Überwachung zu den Leistungen des Abos gehört, im Übrigen Art. 6 Abs. 1 lit. f DSGVO. Unser berechtigtes Interesse liegt auch hier im sicheren und funktionsfähigen Betrieb. Die Adresse einer Website entfernen wir bei UptimeRobot, sobald wir die Website nicht mehr betreiben.</p>
|
||||
|
||||
<h2>4. Cookies und Speicher im Browser</h2>
|
||||
<p>Auf unseren eigenen Seiten (webklar.com und dem Kundenportal) setzen wir keine Analyse-, Tracking- oder Marketing-Werkzeuge ein und binden keine Inhalte fremder Anbieter wie Schriftarten oder Karten ein. Alle Bestandteile unserer Seiten werden von unserem eigenen Server geladen.</p>
|
||||
<p>Auf webklar.com messen wir mit unserer eigenen Website-Analyse, wie die Seite genutzt wird (Abschnitt 4a). Darüber hinaus setzen wir auf unseren eigenen Seiten (webklar.com und dem Kundenportal) keine Analyse-, Tracking- oder Marketing-Werkzeuge ein und binden keine Inhalte fremder Anbieter wie Schriftarten oder Karten ein. Alle Bestandteile unserer Seiten werden von unserem eigenen Server geladen.</p>
|
||||
<table>
|
||||
<tr><th>Name</th><th>Zweck</th><th>Speicherdauer</th></tr>
|
||||
<tr><td>webklar_portal_session (Cookie)</td><td>hält Sie im Kundenportal angemeldet</td><td>7 Tage bzw. bis zur Abmeldung</td></tr>
|
||||
<tr><td>preview_session (Cookie)</td><td>Zugang zu Ihren Vorschauseiten unter *.project.webklar.com</td><td>7 Tage bzw. bis zur Abmeldung</td></tr>
|
||||
<tr><td>theme (lokaler Speicher, webklar.com)</td><td>merkt sich die von Ihnen gewählte helle oder dunkle Darstellung</td><td>bis Sie ihn im Browser löschen</td></tr>
|
||||
<tr><td>wk-consent (lokaler Speicher, webklar.com)</td><td>merkt sich Ihre Auswahl im Banner der Reichweitenmessung („Annehmen“ oder „Ablehnen“) mit Datum und Fassung des Banner-Textes (Abschnitt 4a)</td><td>183 Tage, danach fragen wir erneut</td></tr>
|
||||
</table>
|
||||
<p>Diese Speicherungen sind unbedingt erforderlich, damit wir die von Ihnen gewünschten Funktionen bereitstellen können (§ 25 Abs. 2 Nr. 2 TDDDG). Eine Einwilligung ist dafür nicht nötig. Die Verarbeitung stützt sich auf Art. 6 Abs. 1 lit. b DSGVO (Portal) bzw. lit. f DSGVO (Darstellung).</p>
|
||||
<p>Diese Speicherungen sind unbedingt erforderlich, damit wir die von Ihnen gewünschten Funktionen bereitstellen können (§ 25 Abs. 2 Nr. 2 TDDDG). Eine Einwilligung ist dafür nicht nötig. Die Verarbeitung stützt sich auf Art. 6 Abs. 1 lit. b DSGVO (Portal) bzw. lit. f DSGVO (Darstellung, Auswahl im Banner).</p>
|
||||
|
||||
<h2 id="reichweitenmessung">4a. Reichweitenmessung auf webklar.com</h2>
|
||||
<p>Auf webklar.com messen wir mit unserer eigenen Website-Analyse, wie unsere Website genutzt wird, um sie zu verbessern. Es ist dieselbe Messung, die wir unseren Kunden anbieten (Abschnitt 6b); für webklar.com sind wir selbst Verantwortlicher. Die Messung läuft auf unserem Server bei der Hetzner Online GmbH in Deutschland (Abschnitt 3), ohne Cookies und ohne Dienste von Google, Meta oder anderen Werbenetzwerken. Wir nutzen die Daten nicht für Werbung und bilden keine Profile einzelner Besucher. Beim ersten Aufruf fragt ein Banner nach Ihrer Zustimmung.</p>
|
||||
<p>Solange Sie im Banner nichts ausgewählt haben, zählen wir nur den Seitenaufruf: Gespeichert werden der Seitenpfad, das Land, das wir aus Ihrer IP-Adresse ableiten, die Geräteklasse, die wir aus dem Browser-Kennzeichen ableiten, der Zeitpunkt, auf die volle Stunde gerundet, und ein Zähler dafür, dass das Banner angezeigt wurde. Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO; unser berechtigtes Interesse ist, die Nutzung unserer Website in Zahlen zu kennen, das Banner zu betreiben und die Messung vor Missbrauch zu schützen. Wählen Sie „Ablehnen“, zählen wir einmal die Ablehnung und danach nichts mehr. Sendet Ihr Browser „Global Privacy Control“ oder „Do Not Track“, zeigen wir das Banner nicht an und zählen nichts.</p>
|
||||
<p>Wählen Sie „Annehmen“, messen wir zusätzlich, von welcher Website Sie gekommen sind (nur der Domainname), die Größenklasse Ihres Browserfensters, wie lange Sie auf einer Seite bleiben und wie weit Sie scrollen, wo Sie klicken (Klickposition und Aufbau des angeklickten Elements, ohne Texte und Formulareingaben, für eine „Heatmap“), ob Sie auf eine Telefonnummer, eine E-Mail-Adresse, einen WhatsApp-Link oder einen Link zu einer anderen Website klicken (dann nur mit deren Domainname), und eine Besucher-Kennung, die täglich wechselt. Rechtsgrundlage ist Ihre Einwilligung nach § 25 Abs. 1 TDDDG und Art. 6 Abs. 1 lit. a DSGVO. Sie können sie jederzeit mit Wirkung für die Zukunft widerrufen: über „Datenschutz-Einstellungen“ (das Keks-Symbol unten rechts) und „Ablehnen“.</p>
|
||||
<p>IP-Adressen und Browser-Kennzeichen verwenden wir nur kurz im Arbeitsspeicher und speichern sie in den Messdaten nicht. Einzelne Seitenaufrufe, Ereignisse und Klickpositionen löschen wir nach 30 Tagen, zusammengefasste Tageswerte ohne Besucher-Kennung nach 25 Monaten. Alle Einzelheiten, auch zum Widerspruch und zu Ihren Rechten, finden Sie auf unserer Zusatzseite <a href="https://webklar.com/datenschutz-analyse.html">webklar.com/datenschutz-analyse.html</a>.</p>
|
||||
|
||||
<h2>5. Kontaktaufnahme</h2>
|
||||
<p>Über das Kontaktformular auf webklar.com erheben wir Name, E-Mail-Adresse, Firma (freiwillig) und Ihre Nachricht. Die Angaben speichern wir in unserer eigenen Datenbank auf unserem Server. Schreiben Sie uns per E-Mail oder rufen Sie an, verarbeiten wir die Angaben, die Sie uns dabei mitteilen.</p>
|
||||
<p>Über das Kontaktformular auf webklar.com erheben wir Name, E-Mail-Adresse, Firma (freiwillig) und Ihre Nachricht. Die Angaben speichern wir in unserer eigenen Datenbank auf unserem Server und schicken sie zugleich per E-Mail an unser Postfach (Abschnitt 8), damit wir von Ihrer Anfrage erfahren. Schreiben Sie uns per E-Mail oder rufen Sie an, verarbeiten wir die Angaben, die Sie uns dabei mitteilen.</p>
|
||||
<p>Wir nutzen die Daten, um Ihre Anfrage zu beantworten. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO, wenn Ihre Anfrage auf einen Vertrag zielt, sonst Art. 6 Abs. 1 lit. f DSGVO (Interesse an der Beantwortung). Entsteht kein Vertrag, löschen wir die Anfrage spätestens sechs Monate nach der letzten Nachricht.</p>
|
||||
|
||||
<h2>6. Kundenportal</h2>
|
||||
@@ -134,14 +142,43 @@
|
||||
<h2>10. Domains unserer Kunden</h2>
|
||||
<p>Registrieren wir für Sie eine Domain, geben wir die Daten des Domaininhabers (Name, Anschrift, E-Mail-Adresse, Telefonnummer) an den Domain-Registrar, die Hetzner Online GmbH, und an die zuständige Vergabestelle weiter, bei .de-Domains an die DENIC eG in Frankfurt am Main. Sie verarbeiten die Daten als eigene Verantwortliche. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b DSGVO.</p>
|
||||
|
||||
<h2>11. Interessenten</h2>
|
||||
<p>Sprechen wir Unternehmen als mögliche Kunden an, verarbeiten wir öffentlich verfügbare Geschäftsdaten: Firmenname, Ansprechpartner, geschäftliche Kontaktdaten und Angaben zur bestehenden Website. Quellen sind etwa die Website des Unternehmens und öffentliche Branchenverzeichnisse. Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO. Unser berechtigtes Interesse liegt in der Anbahnung von Geschäftsbeziehungen. Besteht kein Interesse oder widersprechen Sie, löschen wir die Daten. Ohne Kontakt löschen wir sie spätestens nach zwölf Monaten.</p>
|
||||
<a id="interessenten"></a>
|
||||
<h2>11. Interessenten: Antworten auf öffentliche Anfragen</h2>
|
||||
<p>Manche Menschen und Betriebe suchen öffentlich im Netz jemanden, der ihnen eine Website baut, zum Beispiel in einer Kleinanzeige, auf einem Portal für Freelancer oder in einem Frage-Forum. Auf solche Gesuche melden wir uns von uns aus nur einmal und bieten unsere Leistungen an. Eine Ausnahme sind Frage-Foren: Dort antworten wir nicht im Forum (siehe unten). Weil wir Ihre Daten dabei nicht bei Ihnen erheben, sondern aus Ihrem öffentlichen Beitrag, informieren wir Sie hier und mit einem Hinweis am Ende unserer Antwort darüber (Art. 14 DSGVO).</p>
|
||||
<p>So läuft es ab: Ein KI-Assistent (das Sprachmodell Claude von Anthropic) bewertet, ob die Anfrage zu unseren Leistungen passt, und entwirft eine Antwort (für Beiträge aus Frage-Foren nicht, siehe unten). Ein Mitarbeiter von WEBklar liest den Entwurf, ändert ihn bei Bedarf und sendet ihn einmal von Hand: über die Plattform, auf der Sie gefragt haben, oder per E-Mail, wenn Ihr Beitrag darum bittet. Automatische E-Mails verschicken wir nicht. Für E-Mails gilt Abschnitt 8. Passt Ihre Anfrage nach der Bewertung der KI nicht zu uns, schreiben wir Ihnen nicht und kürzen die Daten spätestens am Tag nach dem Fund (das ist der Tag, an dem wir Ihre Anfrage gefunden haben; siehe unten).</p>
|
||||
<p>Am Ende unserer Antwort steht ein Hinweis. Er sagt, dass eine KI-Assistentin von WEBklar antwortet, dass wir die Angaben aus Ihrer Anfrage spätestens 90 Tage nach dem Tag, an dem wir Ihre Anfrage gefunden haben, in unserer Datenbank kürzen, das heißt, Titel, Text, Link und E-Mail-Adresse löschen, und er verweist auf diesen Abschnitt. Außerdem nennt er Ihr Widerspruchsrecht: Wenn Sie keine weitere Nachricht von uns möchten oder der Speicherung widersprechen, antworten Sie uns bitte kurz; sobald wir Ihre Antwort lesen, schreiben wir Ihnen zu dieser Anfrage nicht mehr und kürzen Ihre Daten (bei einem Widerspruch sofort, sonst innerhalb von 30 Tagen). Unsere Mitarbeiter sind angewiesen, diesen Hinweis in jede Antwort aufzunehmen, auch in eine selbst geschriebene, und beim Bearbeiten eines Entwurfs nicht zu entfernen.</p>
|
||||
<p>Beiträge in Frage-Foren (derzeit gutefrage.net, forum-hilfe.de, wer-weiss-was.de und wpde.org) beantworten wir nicht im Forum. Die KI entwirft dafür keine Antwort. Ein Mitarbeiter sieht den Beitrag an und meldet sich höchstens per E-Mail, wenn der Beitrag ausdrücklich um Angebote per E-Mail bittet; unsere Mitarbeiter sind angewiesen, in diese E-Mail am Ende den Hinweis aufzunehmen, der oben beschrieben ist. Bittet der Beitrag nicht darum, melden wir uns nicht.</p>
|
||||
<p>Die Gesuche finden wir auf zwei Wegen. Erstens schicken uns Plattformen auf unseren Suchauftrag Benachrichtigungs-E-Mails (derzeit Kleinanzeigen, freelancermap und freelance.de); diese Plattformen rufen wir nicht selbst ab. Zweitens findet ein KI-Assistent mit einer Websuche öffentliche Beiträge im Netz und in Frage-Foren, derzeit in den Foren gutefrage.net, forum-hilfe.de, wer-weiss-was.de und wpde.org sowie auf anderen öffentlichen Webseiten. Die Suche und den Abruf der Seiten führt Anthropic aus. Von der Websuche und vom Abruf ausgenommen sind die Plattformen Kleinanzeigen, freelancermap, freelance.de, Facebook, Instagram, LinkedIn, Xing, Reddit, Upwork, Fiverr und MyHammer sowie Seiten, deren Betreiber automatische Abrufe in der Datei robots.txt untersagen. Wir suchen nach Gesuchen, nicht nach bestimmten Personen.</p>
|
||||
<p>Wir speichern den Titel des Beitrags, den Beitragstext (höchstens 4000 Zeichen), den Link, das Datum, Ort und Postleitzahl, wenn sie genannt sind, die Plattform, eine Punktzahl mit kurzer Begründung und, wenn der Beitrag um eine Antwort per E-Mail bittet, die E-Mail-Adresse aus dem Beitrag. Wir lesen Ihren Namen nicht gesondert aus; steht er im Titel oder im Text, speichern wir ihn damit. Telefonnummern entfernen wir beim Speichern automatisch aus dem Text; das folgt einer Faustregel und erkennt nicht jede Schreibweise. Dazu kommen der Entwurf unserer Antwort, die gesendete Nachricht mit Text und Zeitpunkt und ein Protokoll der einzelnen Arbeitsschritte, zum Beispiel „Neue Anfrage: Titel des Beitrags“.</p>
|
||||
<p>Rechtsgrundlage ist Art. 6 Abs. 1 lit. f DSGVO. Unser berechtigtes Interesse ist, auf öffentliche Gesuche nach Website-Leistungen zu antworten. Wir melden uns je Anfrage von uns aus nur einmal, und Sie können der Verarbeitung jederzeit widersprechen.</p>
|
||||
<p>Wir speichern die Daten auf unserem Server bei der Hetzner Online GmbH in Deutschland (Abschnitt 3). Für die Bewertung, den Entwurf und die Websuche setzen wir das Sprachmodell Claude der Anthropic Ireland, Limited, Dublin, Irland, als Auftragsverarbeiter ein. Anthropic erhält dafür den Titel, den Text und den Ort des Beitrags sowie bei der Websuche unsere Suchaufträge und die gefundenen Seiten. Dabei können Daten in die USA übermittelt werden, abgesichert durch Standardvertragsklauseln (Art. 46 Abs. 2 lit. c DSGVO); eine Kopie erhalten Sie bei uns (support@webklar.com).</p>
|
||||
<p>Die KI sortiert Anfragen aus, die nicht zu uns passen; auf diese schreiben wir nicht. Ob und was wir einer passenden Anfrage antworten, entscheidet ein Mitarbeiter (Abschnitt 14).</p>
|
||||
<p>Wie lange wir die Daten speichern: Wir kürzen Ihre Daten in unserer Datenbank</p>
|
||||
<ul>
|
||||
<li>spätestens 90 Tage nach dem Fund;</li>
|
||||
<li>spätestens am Tag nach dem Fund, wenn Ihre Anfrage nicht zu uns passt;</li>
|
||||
<li>innerhalb von 30 Tagen, nachdem wir Ihre Antwort gelesen haben, wenn Sie uns sagen, dass Sie kein Interesse haben;</li>
|
||||
<li>sofort, wenn Sie widersprechen: Wir sperren und kürzen sie, sobald wir Ihren Widerspruch lesen.</li>
|
||||
</ul>
|
||||
<p>Kürzen heißt: Wir löschen Titel, Beitragstext, Link, E-Mail-Adresse, Entwurf und unsere Nachricht an Sie und bereinigen die zugehörigen Einträge im Protokoll. Übrig bleiben nur pseudonymisierte Prüfwerte (Hash-Werte) des Beitrags, Ihrer E-Mail-Adresse und, wenn es eine Firmenadresse ist (also nicht bei Anbietern wie gmail.com oder web.de), der Domain dieser Adresse, das ist der Teil hinter dem @, zum Beispiel firma.de. Dazu bleiben Ort, Plattform, Punktzahl, Status und die Zeitpunkte von Beitrag und Fund, jedoch ohne Titel, Text, Link und Kontaktdaten.</p>
|
||||
<p>Ein Hash-Wert ist ein Prüfwert, aus dem sich der ursprüngliche Inhalt nicht ablesen lässt. Wir erkennen damit nur, wenn derselbe Beitrag, dieselbe E-Mail-Adresse oder, bei einer Firmenadresse, eine andere Adresse derselben Firma noch einmal auftaucht; wir schreiben dann nicht noch einmal und nehmen den Beitrag nicht noch einmal auf. Diese Prüfwerte bewahren wir so lange auf, wie wir verhindern müssen, dass wir Sie wegen desselben Beitrags oder derselben E-Mail-Adresse (bei einer Firmenadresse: derselben Firma) erneut ansprechen. Ort, Plattform, Punktzahl, Status und die Zeitpunkte behalten wir für unsere Auswertung, damit unsere Zähler und der zeitliche Verlauf stimmen.</p>
|
||||
<p>Haben Sie uns geantwortet und übernehmen wir Ihre Angaben als Kontakt oder Kunde in unsere Kundenverwaltung, gelten für diese Daten stattdessen Abschnitt 5 und, wenn ein Vertrag zustande kommt, die Abschnitte 7 und 9.</p>
|
||||
<p>Für Kopien außerhalb der Datenbank gilt Folgendes. Die Benachrichtigungs-E-Mails der Plattformen enthalten die Anzeigentexte und gehen in unserem Postfach support@webklar.com bei der Hetzner Online GmbH ein (Abschnitt 8). Unser Dienst liest dort nur den Posteingang. Er lädt dazu jede neue E-Mail dieses Postfachs kurz, um den Absender zu prüfen; übernommen und gespeichert werden nur die Benachrichtigungen der Plattformen, alle anderen E-Mails verwirft er sofort. Die Benachrichtigungs-E-Mails der Plattformen löschen wir im Postfach spätestens 90 Tage nach dem Eingang, auch wenn wir die Anfrage in der Datenbank schon früher gekürzt haben.</p>
|
||||
<p>Eine E-Mail, die ein Mitarbeiter Ihnen sendet, liegt außerdem in unserem Postfach; dafür gilt Abschnitt 8 und, soweit die E-Mail ein Geschäftsbrief ist, die Frist aus Abschnitt 9. Kürzen wir Daten, bleiben sie in unseren Datensicherungen noch bis zu sieben Tage erhalten und werden dann überschrieben. Die Antwort, die wir auf einer Plattform abschicken, sehen auch deren Betreiber. Dafür gelten die Datenschutzhinweise der Plattform.</p>
|
||||
<div class="box widerspruch">
|
||||
<strong>Ihr Widerspruchsrecht gegen diese Verarbeitung (Art. 21 DSGVO)</strong>
|
||||
<p>Sie können der Verarbeitung Ihrer Daten jederzeit widersprechen, ohne Gründe anzugeben. Antworten Sie dazu auf unsere Nachricht oder schreiben Sie an support@webklar.com.</p>
|
||||
<p>Sobald wir Ihre Nachricht lesen, sperren wir den Beitrag und, soweit wir sie gespeichert haben, Ihre E-Mail-Adresse, schreiben Ihnen dazu nicht mehr und kürzen Ihre Daten wie oben beschrieben. Ist es eine Firmenadresse und nicht die eines Anbieters wie gmail.com oder web.de, sperren wir auch deren Domain, also auch andere Adressen derselben Firma. Wir erkennen Sie dabei nur am Beitrag, an Ihrer E-Mail-Adresse und, bei einer Firmenadresse, an deren Domain: Ein neues Gesuch, das Sie später einstellen, ist für uns ein neuer Beitrag, wenn es keine dieser Angaben enthält; sagen Sie uns dann bitte noch einmal Bescheid.</p>
|
||||
<p>Die Benachrichtigung der Plattform in unserem Postfach erfasst das Kürzen in der Datenbank nicht; wir löschen sie dort spätestens 90 Tage nach dem Eingang (siehe oben). Weitere Rechte (Art. 15 bis 20 DSGVO) und das Beschwerderecht beschreibt Abschnitt 13.</p>
|
||||
</div>
|
||||
|
||||
<h2>12. Empfänger im Überblick</h2>
|
||||
<ul>
|
||||
<li>Hetzner Online GmbH: Hosting, E-Mail und Datensicherung (Auftragsverarbeiter)</li>
|
||||
<li>Hetzner Online GmbH: Hosting, E-Mail (auch das Postfach für die Benachrichtigungen der Plattformen, Abschnitt 11) und Datensicherung (Auftragsverarbeiter)</li>
|
||||
<li>UptimeRobot s. r. o.: Überwachung der Erreichbarkeit, Daten auch außerhalb der EU (insbesondere USA), Auftragsverarbeiter (Abschnitt 3)</li>
|
||||
<li>Stripe: Zahlungsabwicklung und Eröffnung des Stripe-Kontos für den Webshop und den Buchungskalender unserer Kunden (Abschnitte 6d, 6f und 7)</li>
|
||||
<li>Anthropic Ireland, Limited: Bewertung öffentlicher Anfragen, Entwurf von Antworten sowie Websuche und Seitenabruf durch ein Sprachmodell (Auftragsverarbeiter, Daten auch in den USA, Abschnitt 11)</li>
|
||||
<li>Plattformen, auf denen wir Ihre Anfrage beantworten, zum Beispiel Kleinanzeigen: Unsere Antwort läuft über deren Dienst (Abschnitt 11)</li>
|
||||
<li>Porkbun LLC: bisheriger E-Mail-Dienst, nur noch bis zur Löschung älterer E-Mails (Abschnitt 8)</li>
|
||||
<li>Domain-Registrar und Vergabestellen, sofern wir eine Domain für Sie registrieren (Abschnitt 10)</li>
|
||||
<li>Steuerberatung und Behörden, soweit gesetzlich erforderlich</li>
|
||||
@@ -149,15 +186,15 @@
|
||||
<p>Wir verkaufen keine Daten und geben sie nicht zu Werbezwecken weiter.</p>
|
||||
|
||||
<h2>13. Ihre Rechte</h2>
|
||||
<p>Sie haben das Recht auf Auskunft (Art. 15 DSGVO), Berichtigung (Art. 16), Löschung (Art. 17), Einschränkung der Verarbeitung (Art. 18) und Datenübertragbarkeit (Art. 20). Eine Einwilligung können Sie jederzeit mit Wirkung für die Zukunft widerrufen (Art. 7 Abs. 3). Derzeit stützen wir keine Verarbeitung auf eine Einwilligung.</p>
|
||||
<p>Sie haben das Recht auf Auskunft (Art. 15 DSGVO), Berichtigung (Art. 16), Löschung (Art. 17), Einschränkung der Verarbeitung (Art. 18) und Datenübertragbarkeit (Art. 20). Eine Einwilligung können Sie jederzeit mit Wirkung für die Zukunft widerrufen (Art. 7 Abs. 3). Auf eine Einwilligung stützen wir derzeit nur die erweiterte Reichweitenmessung auf webklar.com (Abschnitt 4a).</p>
|
||||
<div class="box widerspruch">
|
||||
<strong>Widerspruchsrecht (Art. 21 DSGVO)</strong>
|
||||
<p>Verarbeiten wir Ihre Daten auf Grundlage von Art. 6 Abs. 1 lit. f DSGVO, können Sie aus Gründen, die sich aus Ihrer besonderen Situation ergeben, jederzeit widersprechen. Verarbeiten wir Ihre Daten zur Direktwerbung, können Sie dem jederzeit ohne Angabe von Gründen widersprechen. Dann nutzen wir Ihre Daten nicht mehr für diesen Zweck. Ein formloser Hinweis an die Kontaktdaten in Abschnitt 1 genügt.</p>
|
||||
</div>
|
||||
<p>Sie können sich außerdem bei einer Datenschutz-Aufsichtsbehörde beschweren. Für uns zuständig ist der Landesbeauftragte für den Datenschutz und die Informationsfreiheit Rheinland-Pfalz, Hintere Bleiche 34, 55116 Mainz, poststelle@datenschutz.rlp.de.</p>
|
||||
|
||||
<h2>14. Pflicht zur Bereitstellung, keine automatisierten Entscheidungen</h2>
|
||||
<p>Für einen Vertrag mit uns brauchen wir die dafür nötigen Angaben. Ohne sie können wir den Vertrag nicht schließen. Darüber hinaus sind Sie nicht verpflichtet, uns Daten bereitzustellen. Automatisierte Entscheidungen einschließlich Profiling im Sinne von Art. 22 DSGVO finden nicht statt.</p>
|
||||
<h2>14. Pflicht zur Bereitstellung, keine automatisierten Einzelentscheidungen nach Art. 22 DSGVO</h2>
|
||||
<p>Für einen Vertrag mit uns brauchen wir die dafür nötigen Angaben. Ohne sie können wir den Vertrag nicht schließen. Darüber hinaus sind Sie nicht verpflichtet, uns Daten bereitzustellen. Entscheidungen, die Ihnen gegenüber rechtliche Wirkung entfalten oder Sie in ähnlicher Weise erheblich beeinträchtigen (Art. 22 DSGVO), treffen wir nicht automatisiert. Die Bewertung öffentlicher Anfragen durch einen KI-Assistenten (Abschnitt 11) sortiert Anfragen aus, die nicht zu uns passen; auf diese schreiben wir nicht. Für alle anderen bereitet sie höchstens eine Antwort vor; ob und was wir antworten, entscheidet ein Mitarbeiter, der die Antwort von Hand abschickt.</p>
|
||||
|
||||
<h2>15. Änderungen</h2>
|
||||
<p>Wir passen diese Erklärung an, wenn sich unsere Verarbeitung oder die Rechtslage ändert. Es gilt die jeweils hier veröffentlichte Fassung.</p>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/* Reiter der Funktionsmodule in der neuen Ansicht (Abo-Modell). Im Abo-Modell zeichnen Terminplaner, Webshop, Buchungskalender und
|
||||
Analyse keine eigene Leiste, sondern melden ihre Reiter an die Etage 2 (public/neu/etage.js): CustomEvent 'kn:reiter' mit
|
||||
bubbles und detail { eintraege: [{ key, label, kurz? }], aktiv, bei(key) }; eine leere Liste heisst „keine Reiter“.
|
||||
Dieses Modul hat keine Importe und kennt weder DOM noch die Huelle. */
|
||||
|
||||
/** Kurzlabels fuer schmale Zeilen (C2): modul → { reiterKey: kurzes Wort }. Die Analyse braucht keine (Übersicht, Heatmap, Einstellungen). */
|
||||
export const KURZLABELS = {
|
||||
termine: { team: 'Team' },
|
||||
shop: { uebersicht: 'Start', rechtliches: 'Recht' },
|
||||
buchungen: { unterkuenfte: 'Preise' },
|
||||
}
|
||||
|
||||
/** [{ key, label, … }] des Moduls → [{ key, label, kurz? }]; `kurz` nur mit Eintrag in KURZLABELS, alle anderen Felder fallen weg. */
|
||||
export function reiterEintraege(modul, reiterListe) {
|
||||
const kurz = Object.hasOwn(KURZLABELS, modul) ? KURZLABELS[modul] : {}
|
||||
return reiterListe.map(({ key, label }) => (Object.hasOwn(kurz, key) ? { key, label, kurz: kurz[key] } : { key, label }))
|
||||
}
|
||||
|
||||
/** Sendet 'kn:reiter' aus el. Ohne CustomEvent oder ohne el.dispatchEvent: false, sonst true. */
|
||||
export function meldeReiter(el, { eintraege, aktiv = '', bei = () => {} } = {}) {
|
||||
if (typeof CustomEvent !== 'function' || typeof el?.dispatchEvent !== 'function') return false
|
||||
el.dispatchEvent(new CustomEvent('kn:reiter', { bubbles: true, detail: { eintraege, aktiv, bei } }))
|
||||
return true
|
||||
}
|
||||
@@ -17,6 +17,10 @@ const RECHTLICHES = '/rechtliches/'
|
||||
const KUNDENANSICHT = 'In der Kundenansicht kann nicht bestellt werden.'
|
||||
const ZAHLUNGSSEITE = 'Die Zahlungsseite konnte nicht geöffnet werden. Bitte versuch es noch einmal oder schreib uns im Chat.'
|
||||
const DANKE = 'Danke! Sobald die Zahlung da ist, legen wir los.'
|
||||
// Abo-Modell (Texte E1, E2, E4): Einschalten statt Bestellen
|
||||
const EINSCHALTEN_TITEL = 'Prüfen und einschalten'
|
||||
const EINSCHALTEN_OFFEN = 'Du kannst einschalten, sobald die offenen Punkte erledigt sind.'
|
||||
const EINSCHALTEN_FEHLT = 'Das Einschalten ist gerade nicht verfügbar. Bitte lade die Seite neu oder schreib uns im Chat.'
|
||||
const SCHRITT_TITEL = new Map(FRAGEBOGEN_SCHRITTE)
|
||||
|
||||
/* ---------------------------------------------------------------- Links --- */
|
||||
@@ -105,6 +109,35 @@ function ladeBeiRueckkehrNeu(element) {
|
||||
window.addEventListener('pageshow', beiAnzeige)
|
||||
}
|
||||
|
||||
/* ------------------------------------------- Zusammenfassung und offene Punkte (Bestellseite, Einschalt-Seite) --- */
|
||||
/** Die Gruppen der Zusammenfassung (je Gruppe die Angaben als Text und „Ändern“, wenn es ein Ziel gibt). */
|
||||
function gruppenKarten(gruppenFn, daten, springt, gehe) {
|
||||
return (gruppenFn ? gruppenFn(daten.einstellung, daten.fragebogen, daten.branchen) : []).map((gruppe) => h('div', { class: 'tm-bestellen-gruppe', 'data-schritt': gruppe.schritt },
|
||||
h('div', { class: 'tm-bestellen-gruppe-kopf' },
|
||||
h('h4', null, gruppe.titel),
|
||||
springt
|
||||
? h('button', {
|
||||
type: 'button', class: 'kb-btn-outline tm-knopf', 'data-aktion': 'aendern', 'data-schritt': gruppe.schritt, 'aria-label': `Ändern: ${gruppe.titel}`, onclick: () => gehe(gruppe.schritt),
|
||||
}, 'Ändern')
|
||||
: null),
|
||||
gruppe.zeilen.length > 0 ? h('ul', null, gruppe.zeilen.map((zeile) => h('li', null, zeile))) : h('p', { class: 'tm-hinweis' }, 'Noch keine Angaben.')))
|
||||
}
|
||||
|
||||
/** Die offenen Punkte als Block „Das fehlt noch“ (je Punkt „Zum Schritt“ nur mit Ziel); ohne Punkte null. anleitung = Satz unter der Überschrift oder null. */
|
||||
function offenKarte(offen, springt, gehe, anleitung) {
|
||||
if (offen.length === 0) return null
|
||||
return h('div', { class: 'tm-bestellen-offen', role: 'note' },
|
||||
h('h4', null, 'Das fehlt noch'),
|
||||
anleitung ? h('p', { class: 'tm-hinweis' }, anleitung) : null,
|
||||
h('ul', null, offen.map((punkt) => h('li', null,
|
||||
h('span', null, punkt.text),
|
||||
springt && SCHRITT_TITEL.has(punkt.schritt)
|
||||
? h('button', {
|
||||
type: 'button', class: 'kb-btn-outline tm-knopf', 'data-aktion': 'zum-schritt', 'data-schritt': punkt.schritt, 'aria-label': `Zum Schritt „${SCHRITT_TITEL.get(punkt.schritt)}“`, onclick: () => gehe(punkt.schritt),
|
||||
}, 'Zum Schritt')
|
||||
: null))))
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- Bestellseite --- */
|
||||
/**
|
||||
* „Prüfen und bestellen“ in root zeichnen und die Seite zurückgeben. Reihenfolge: Zusammenfassung der Angaben (je Gruppe „Ändern“),
|
||||
@@ -147,27 +180,10 @@ function baueSeite(root, { ctx, daten, zumSchritt, key, gruppenFn }, uebernahme)
|
||||
const gehe = (schritt) => { if (springt) zumSchritt(schritt) }
|
||||
|
||||
// 1 Zusammenfassung: je Gruppe die Angaben als Text und „Ändern“
|
||||
const gruppen = (gruppenFn ? gruppenFn(daten.einstellung, daten.fragebogen, daten.branchen) : []).map((gruppe) => h('div', { class: 'tm-bestellen-gruppe', 'data-schritt': gruppe.schritt },
|
||||
h('div', { class: 'tm-bestellen-gruppe-kopf' },
|
||||
h('h4', null, gruppe.titel),
|
||||
springt
|
||||
? h('button', {
|
||||
type: 'button', class: 'kb-btn-outline tm-knopf', 'data-aktion': 'aendern', 'data-schritt': gruppe.schritt, 'aria-label': `Ändern: ${gruppe.titel}`, onclick: () => gehe(gruppe.schritt),
|
||||
}, 'Ändern')
|
||||
: null),
|
||||
gruppe.zeilen.length > 0 ? h('ul', null, gruppe.zeilen.map((zeile) => h('li', null, zeile))) : h('p', { class: 'tm-hinweis' }, 'Noch keine Angaben.')))
|
||||
const gruppen = gruppenKarten(gruppenFn, daten, springt, gehe)
|
||||
|
||||
// 2 offene Punkte: „Zum Schritt“ nur dort, wo es einen Schritt des Fragebogens gibt (der Abo-Punkt gehört zu dieser Seite)
|
||||
const offenBlock = offen.length === 0 ? null : h('div', { class: 'tm-bestellen-offen', role: 'note' },
|
||||
h('h4', null, 'Das fehlt noch'),
|
||||
h('p', { class: 'tm-hinweis' }, 'Erledige diese Punkte, dann kannst du bestellen.'),
|
||||
h('ul', null, offen.map((punkt) => h('li', null,
|
||||
h('span', null, punkt.text),
|
||||
springt && SCHRITT_TITEL.has(punkt.schritt)
|
||||
? h('button', {
|
||||
type: 'button', class: 'kb-btn-outline tm-knopf', 'data-aktion': 'zum-schritt', 'data-schritt': punkt.schritt, 'aria-label': `Zum Schritt „${SCHRITT_TITEL.get(punkt.schritt)}“`, onclick: () => gehe(punkt.schritt),
|
||||
}, 'Zum Schritt')
|
||||
: null))))
|
||||
const offenBlock = offenKarte(offen, springt, gehe, 'Erledige diese Punkte, dann kannst du bestellen.')
|
||||
|
||||
// 3 Preis mit dem Hinweis des Servers und die Einrichtungsdauer
|
||||
const preisBlock = h('div', { class: 'tm-bestellen-preis' },
|
||||
@@ -289,12 +305,65 @@ function baueSeite(root, { ctx, daten, zumSchritt, key, gruppenFn }, uebernahme)
|
||||
return seite
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ Einschalt-Seite (Abo-Modell) --- */
|
||||
/**
|
||||
* Die gemeinsame Seite der Funktionsmodule im Abo-Modell. Statt Preis, Belehrung, Haken und Knopf der Bestellseite: oben (nur wenn es etwas
|
||||
* zu prüfen gibt) „Prüfen und einschalten“ mit der Zusammenfassung der Angaben (je Gruppe „Ändern“) und den offenen Punkten (je Punkt „Zum
|
||||
* Schritt“), darunter der Platz, in den der Ablauf von P3 (`einschalten`, zeigeEinschalten aus public/neu/funktion-einschalten.js)
|
||||
* Bestätigung und Knopf zeichnet. Bei offenen Punkten steht stattdessen der Hinweis und der Ablauf wird nicht gerufen.
|
||||
* Die Seite kennt weder einen Preis noch den Checkout. daten wie bei renderBestellen (einstellung, fragebogen, branchen, offen, kundenansicht);
|
||||
* einschalten(platz, { key, projectId, viewAs, onFertig }) ist eine Init-Option der Module (die Hülle gibt sie durch); fehlt sie, steht dort eine
|
||||
* Fehlerkarte. onFertig wirkt höchstens einmal und nur, solange ctx.aktuell() nicht falsch ist; es ruft nachEinschalten() (die Ansicht
|
||||
* zeichnet sich neu). Weitere Optionen wie renderBestellen: key, zusammenfassung, zumSchritt.
|
||||
*/
|
||||
export function renderEinschaltSeite(root, { ctx = {}, daten = {}, zumSchritt, key = STANDARD_KEY, zusammenfassung: gruppenFunktion, einschalten, nachEinschalten } = {}) {
|
||||
const gruppenFn = typeof gruppenFunktion === 'function' ? gruppenFunktion : key === STANDARD_KEY ? zusammenfassung : null
|
||||
const offen = offeneListe(daten.offen)
|
||||
const springt = typeof zumSchritt === 'function'
|
||||
const gehe = (schritt) => { if (springt) zumSchritt(schritt) }
|
||||
const gruppen = gruppenKarten(gruppenFn, daten, springt, gehe)
|
||||
const offenBlock = offenKarte(offen, springt, gehe, null)
|
||||
const zeigeKopf = gruppen.length > 0 || offen.length > 0
|
||||
const viewAs = daten.kundenansicht === true || ctx.kundenansicht === true
|
||||
|
||||
const platz = h('div', { class: 'tm-einschalten-platz', 'data-einschalten': key })
|
||||
const seite = h('section', { class: 'kb-panel tm-bestellen tm-einschalten', 'data-key': key },
|
||||
zeigeKopf ? h('h3', null, EINSCHALTEN_TITEL) : null,
|
||||
zeigeKopf && gruppenFn ? h('p', { class: 'tm-abschnitt-text tm-hinweis' }, springt ? 'Lies alles in Ruhe durch. Mit „Ändern“ kommst du zu dem Schritt zurück, in dem du die Angabe gemacht hast.' : 'Lies alles in Ruhe durch.') : null,
|
||||
gruppen.length > 0 ? h('div', { class: 'tm-bestellen-zusammenfassung' }, gruppen) : null,
|
||||
offenBlock,
|
||||
offen.length > 0 ? h('p', { class: 'tm-hinweis', 'data-hinweis': 'einschalten' }, EINSCHALTEN_OFFEN) : platz)
|
||||
root.replaceChildren(seite)
|
||||
if (offen.length > 0) return seite
|
||||
|
||||
// höchstens ein Abschluss: nur der erste Aufruf wirkt, und nur solange die Ansicht noch gilt
|
||||
let fertig = false
|
||||
const onFertig = () => {
|
||||
if (fertig) return
|
||||
fertig = true
|
||||
if (typeof ctx.aktuell === 'function' && ctx.aktuell() === false) return
|
||||
if (typeof nachEinschalten === 'function') Promise.resolve().then(() => nachEinschalten()).catch(() => {})
|
||||
}
|
||||
const fehlt = () => platz.replaceChildren(fehlerKarte(EINSCHALTEN_FEHLT))
|
||||
if (typeof einschalten !== 'function') {
|
||||
fehlt()
|
||||
return seite
|
||||
}
|
||||
try {
|
||||
Promise.resolve(einschalten(platz, { key, projectId: ctx.projectId, viewAs, onFertig })).catch(fehlt)
|
||||
} catch {
|
||||
fehlt()
|
||||
}
|
||||
return seite
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- Statuskarte --- */
|
||||
/**
|
||||
* Karte für eine Bestellung im Zustand ordered („Zahlung offen“) oder setup („In Einrichtung“).
|
||||
* projekt = Eintrag des Projekts aus GET /api/funktionen (projectId), bestellung = projekt.features.termine (state, orderedAt, ticketWoid,
|
||||
* invoiceUrl), einrichtung = Text der Zeiteinschätzung (Register: setupEstimate), danke = Rückkehr von Stripe, kundenansicht = Sitzung in der
|
||||
* Kundenansicht, meldung = Text, der am Knopf „Zahlung fortsetzen“ steht (z. B. der Fehler eines Versuchs vor dem Neuzeichnen).
|
||||
* modell = 'abo' (strikt): die Faktenzeile heißt „Eingeschaltet am“ statt „Bestellt am“ (Abo-Modell, Text E5).
|
||||
* key = Schlüssel der Funktion (Standard 'termine', Checkout-Pfad), name = Name der Funktion in
|
||||
* den Texten (Standard 'Terminplaner'), hinweisEinrichtung = Satz unter den Fakten im Zustand setup (Standard: Einstellungen gesperrt).
|
||||
* weiter(fall) wird gerufen, wenn „Zahlung fortsetzen“ scheitert (Ruling R110): fall = { bestellen, meldung }, bestellen ist wahr bei 409
|
||||
@@ -303,7 +372,7 @@ function baueSeite(root, { ctx, daten, zumSchritt, key, gruppenFn }, uebernahme)
|
||||
* inzwischen bezahlt ist und der Server 400 antwortet); wirft es, bleibt die Karte stehen und der Fehler steht am Knopf. Fehlt weiter,
|
||||
* steht die Meldung des Servers am Knopf.
|
||||
*/
|
||||
export function statusKarte({ projekt, bestellung, einrichtung, danke, kundenansicht, meldung, key = STANDARD_KEY, name = 'Terminplaner', hinweisEinrichtung } = {}, { weiter } = {}) {
|
||||
export function statusKarte({ projekt, bestellung, einrichtung, danke, kundenansicht, meldung, key = STANDARD_KEY, name = 'Terminplaner', hinweisEinrichtung, modell } = {}, { weiter } = {}) {
|
||||
const bestellt = bestellung?.state === 'ordered'
|
||||
const datum = datumTag(bestellung?.orderedAt)
|
||||
const ticket = bestellung?.ticketWoid
|
||||
@@ -312,7 +381,7 @@ export function statusKarte({ projekt, bestellung, einrichtung, danke, kundenans
|
||||
const zeile = (name, wert) => h('div', null, h('dt', null, name), h('dd', null, wert))
|
||||
const fakten = [
|
||||
bestellt ? null : zeile('Ticketnummer', hatTicket ? `#${ticket}` : 'Wir legen dein Ticket gerade an.'),
|
||||
datum ? zeile('Bestellt am', datum) : null,
|
||||
datum ? zeile(modell === 'abo' ? 'Eingeschaltet am' : 'Bestellt am', datum) : null,
|
||||
!bestellt && text(einrichtung) ? zeile('Zeiteinschätzung', einrichtung) : null,
|
||||
].filter(Boolean)
|
||||
|
||||
|
||||
+35
-6
@@ -471,10 +471,12 @@ export async function initFunktionen(root) {
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- News --- */
|
||||
export async function initNews(root, { projectId } = {}) {
|
||||
export async function initNews(root, { projectId, einschalten } = {}) {
|
||||
if (!root) return
|
||||
if (projectId) selection.projectId = projectId
|
||||
const { projects = [], setupEstimate = '', consentText = '', features, ustHinweis } = await request('/api/funktionen')
|
||||
const wunsch = projectId || root.dataset?.wunschProjekt || '' // die Kachel legt ihr Projekt im Behälter ab (wie bei Terminplaner, Webshop, Buchungen)
|
||||
if (root.dataset) delete root.dataset.wunschProjekt
|
||||
if (wunsch) selection.projectId = wunsch
|
||||
const { projects = [], setupEstimate = '', consentText = '', features, ustHinweis, modell } = await request('/api/funktionen')
|
||||
if (!projects.length) {
|
||||
root.innerHTML = '<div class="kb-panel kb-empty">Noch keine Website-Projekte zugewiesen.</div>'
|
||||
return
|
||||
@@ -507,11 +509,38 @@ export async function initNews(root, { projectId } = {}) {
|
||||
? '<p class="fn-thanks" role="status">Danke! Sobald die Zahlung da ist, legen wir los.</p>'
|
||||
: ''
|
||||
if (feature.state === 'active') renderEditor(stage, project)
|
||||
else if (isSetup(feature.state)) stage.innerHTML = thanks + statusCard(feature, setupEstimate)
|
||||
else if (isSetup(feature.state)) stage.innerHTML = thanks + statusCard(feature, setupEstimate, modell === 'abo')
|
||||
else if (feature.state === 'disabled') stage.innerHTML = '<div class="kb-panel"><h3>News-Pinnwand ist abgeschaltet</h3><p>Für diese Website ist die Pinnwand im Moment nicht aktiv. <button type="button" class="kb-link" data-view="chat">Schreib uns im Chat</button>, dann klären wir das.</p></div>'
|
||||
else if (modell === 'abo') renderBookingAbo(stage, project, { einschalten, neuLaden: () => initNews(root, { einschalten }) })
|
||||
else renderBooking(stage, project, { setupEstimate, offer: { ...newsOffer(features, consentText), ustHinweis: textOr(ustHinweis, UST_HINWEIS) }, thanks })
|
||||
}
|
||||
|
||||
/**
|
||||
* Abo-Modell (modell === 'abo' in GET /api/funktionen), Zustand off: statt der Buchungsseite nur der Platz, in den der Ablauf des Einschaltens
|
||||
* zeichnet (einschalten aus den Init-Optionen, die Hülle gibt ihn durch). Kein Preis, kein Haken, kein Checkout. onFertig lädt die Pinnwand
|
||||
* einmal neu. Ohne die Funktion oder wenn sie scheitert, steht Text E4 im Platz.
|
||||
*/
|
||||
function renderBookingAbo(stage, project, { einschalten, neuLaden }) {
|
||||
stage.innerHTML = '<div id="news-einschalten"></div>'
|
||||
const platz = stage.querySelector('#news-einschalten')
|
||||
const fehlt = () => { platz.textContent = 'Das Einschalten ist gerade nicht verfügbar. Bitte lade die Seite neu oder schreib uns im Chat.' }
|
||||
if (typeof einschalten !== 'function') {
|
||||
fehlt()
|
||||
return
|
||||
}
|
||||
let fertig = false
|
||||
const onFertig = () => {
|
||||
if (fertig) return
|
||||
fertig = true
|
||||
Promise.resolve().then(neuLaden).catch(() => {})
|
||||
}
|
||||
try {
|
||||
Promise.resolve(einschalten(platz, { key: 'news', projectId: project.projectId, viewAs: Boolean(window.KbView?.id?.()), onFertig })).catch(fehlt)
|
||||
} catch {
|
||||
fehlt()
|
||||
}
|
||||
}
|
||||
|
||||
function renderBooking(stage, project, { setupEstimate, offer, thanks }) {
|
||||
stage.innerHTML = `
|
||||
${thanks}
|
||||
@@ -551,10 +580,10 @@ function renderBooking(stage, project, { setupEstimate, offer, thanks }) {
|
||||
})
|
||||
}
|
||||
|
||||
function statusCard(feature, setupEstimate) {
|
||||
function statusCard(feature, setupEstimate, abo = false) {
|
||||
const rows = [
|
||||
feature.ticketWoid ? `<div><dt>Ticketnummer</dt><dd>#${esc(feature.ticketWoid)}</dd></div>` : '',
|
||||
feature.orderedAt ? `<div><dt>Bestellt am</dt><dd>${esc(fmtDay(feature.orderedAt))}</dd></div>` : '',
|
||||
feature.orderedAt ? `<div><dt>${abo ? 'Eingeschaltet am' : 'Bestellt am'}</dt><dd>${esc(fmtDay(feature.orderedAt))}</dd></div>` : '',
|
||||
setupEstimate ? `<div><dt>Zeiteinschätzung</dt><dd>${esc(setupEstimate)}</dd></div>` : '',
|
||||
].join('')
|
||||
const invoice = safeUrl(feature.invoiceUrl)
|
||||
|
||||
@@ -3,6 +3,13 @@
|
||||
// Der Kunde waehlt den Vertrag (Kuendigung: Abo oder nur der Zusatzspeicher, ganz oder mit Paketzahl) bzw. die
|
||||
// Bestellung (Widerruf: Abschluss, Wechsel, Zusatzspeicher); die Auswahl geht in den Request und steht in der
|
||||
// Bestaetigung. Nach dem Absenden: Bestaetigung mit Datum/Uhrzeit, speicher- und druckbar.
|
||||
/** Paketzahl der Kündigung des Zusatzspeichers prüfen (nur die neue Seite mit data-anrede="du"): leer oder ganze Zahl von 1 bis max → '', sonst der du-Satz. */
|
||||
function legalFormPaketeFehler(wert, max) {
|
||||
const text = String(wert ?? '').trim()
|
||||
if (text === '') return ''
|
||||
return /^\d+$/.test(text) && Number(text) >= 1 && Number(text) <= max ? '' : `Bitte gib 1 bis ${max} Pakete an oder lass das Feld leer (alle Pakete).`
|
||||
}
|
||||
|
||||
function initLegalForm({ endpoint, fields }) {
|
||||
const form = document.getElementById('decl-form')
|
||||
const done = document.getElementById('done')
|
||||
@@ -51,6 +58,15 @@ function initLegalForm({ endpoint, fields }) {
|
||||
for (const f of fields) payload[f] = String(form.elements[f]?.value ?? '').trim()
|
||||
if (form.elements.when?.value === 'datum' && !storageOnly()) payload.effective = dateInput?.value || ''
|
||||
if (form.elements.contract && !storageOnly()) delete payload.packages
|
||||
// Neue Seite (data-anrede="du"): die Paketzahl wird hier geprüft, der Sie-Satz des Servers erreicht sie nicht
|
||||
if (form.getAttribute('data-anrede') === 'du' && storageOnly()) {
|
||||
const paketeFehler = legalFormPaketeFehler(payload.packages, Number(form.elements.packages?.max) || 10)
|
||||
if (paketeFehler) {
|
||||
errEl.textContent = paketeFehler
|
||||
errEl.classList.remove('hidden')
|
||||
return
|
||||
}
|
||||
}
|
||||
const btn = form.querySelector('button[type="submit"]')
|
||||
btn.disabled = true
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
/* Kundenansicht neu (P7): Bestellseite je Abo (#bestellen-hosting, #bestellen-wartung) im schlanken Muster der Spec 5.6, Fassung 6.
|
||||
Reihenfolge ohne Klappe und ohne Scrollbereich: Block „Was du bestellst“, eine Linkzeile, bei Privatkunden ein Häkchen, der Knopf
|
||||
„Zahlungspflichtig bestellen“ (§ 312j BGB: Kerninfo und Knopf auf demselben Bildschirm), darunter die Kundenart-Zeile.
|
||||
Der Block ist knapp gehalten (Leistung als eine Zeile mit Link auf die Leistungsbeschreibung), damit er mit dem Knopf in den nutzbaren Bereich
|
||||
zwischen Kopf und Leiste passt (375 x 667); die ausführlichen Sätze stehen unter dem Knopf (baueEinzelheiten), ohne Klappe.
|
||||
Die Sätze (Linkzeile, Häkchen, Kundenart-Zeile) kommen vom Server (GET /api/abo → bestelltexte). Der Server entscheidet die Kundenart selbst:
|
||||
der Client sendet nie kundenart oder startChoice, bei Privatkunden nur earlyStart (das Häkchen) und den gezeigten Leistungsbeginn.
|
||||
Der Titel kommt von der Hülle (meldeTitel); die Seite hat keine eigene h1. */
|
||||
|
||||
import { api } from './api.js'
|
||||
import { h, leeren } from './dom.js'
|
||||
import { K, ladeAbo, sichereAdresse, meldeTitel, beiRueckkehr, fmtEuro, fmtDatum, ABO_NICHT_GELADEN } from './ansicht-hilfen.js'
|
||||
import { baueBlock, baueEinzelheiten, baueKundenartZeile, baueLinkzeile, hatWortlaut, kundenartVon, leistungLink } from './bestell-bausteine.js'
|
||||
|
||||
const laeufe = new WeakMap() // Wurzel → Nummer des jüngsten Aufrufs
|
||||
const TEXT_SPERRE = 'Im Ansichtsmodus nicht möglich.'
|
||||
const TEXT_NICHT_VERFUEGBAR = 'Die Online-Bestellung wird gerade eingerichtet – schreib uns gern im Chat.'
|
||||
const TEXT_ZAHLUNGSSEITE = 'Die Zahlungsseite konnte nicht geöffnet werden. Bitte versuche es später noch einmal.'
|
||||
const KNOPF = 'Zahlungspflichtig bestellen'
|
||||
const UST = 'gemäß § 19 UStG ohne Umsatzsteuer'
|
||||
|
||||
/** 'hosting' | 'wartung' aus der Route, sonst aus dem Hash (#bestellen-hosting), sonst ''. */
|
||||
export function produktAusRoute(route, hash = (typeof window !== 'undefined' ? window.location?.hash : '') || '') {
|
||||
if (route?.produkt === 'hosting' || route?.produkt === 'wartung') return route.produkt
|
||||
const treffer = /bestellen-(hosting|wartung)/.exec(String(hash || ''))
|
||||
return treffer ? treffer[1] : ''
|
||||
}
|
||||
|
||||
const produktVon = (daten, key) => (daten?.products || []).find((p) => p.key === key) || null
|
||||
const labelVon = (daten, key) => (produktVon(daten, key)?.name || (key === 'hosting' ? 'Hosting-Abo' : 'Wartungs-Abo')).replace(/^WEBklar\s+/, '')
|
||||
|
||||
/** Preiszeile und Betrag der ersten Abbuchung aus Katalog und Angebot (während des Rabatts der Angebotspreis, sonst der Katalogpreis). */
|
||||
function preise(product, offer) {
|
||||
const offerMonthly = Number(offer?.monthlyPriceCents)
|
||||
const offerRegular = Number(offer?.regularPriceCents) || product.amountCents
|
||||
const offerMonths = Number(offer?.months)
|
||||
const mitZahlen = Boolean(offer) && Number.isFinite(offerMonthly) && offer.monthlyPriceCents != null && offerMonths > 0
|
||||
const preis = fmtEuro(product.amountCents / 100)
|
||||
const preisZeile = !offer
|
||||
? `${preis} pro Monat (Gesamtpreis, ${UST})`
|
||||
: mitZahlen
|
||||
? `${fmtEuro(offerMonthly / 100)} pro Monat für die ersten ${offerMonths} Monate, danach ${fmtEuro(offerRegular / 100)} pro Monat (individuelles Angebot: ${offer.text}; Gesamtpreis, ${UST})`
|
||||
: `${preis} pro Monat (individuelles Angebot: ${offer.text}; Gesamtpreis, ${UST})`
|
||||
return { preisZeile, erste: fmtEuro((mitZahlen ? offerMonthly : product.amountCents) / 100) }
|
||||
}
|
||||
|
||||
/** Texte der Zeilen „Fällig“ und „Beginn“ je nach Lage: sofort (Betrieb), sofort auf Wunsch (Privat mit Häkchen), nach der Frist (Privat ohne). */
|
||||
function faelligBeginn({ lage, erste, frist }) {
|
||||
if (lage === 'frist') {
|
||||
return {
|
||||
faellig: `erst am ${frist || 'Ende der Widerrufsfrist'} (${erste})`,
|
||||
beginn: `nach Ablauf der 14-tägigen Widerrufsfrist${frist ? ` (am ${frist})` : ''}`,
|
||||
}
|
||||
}
|
||||
return {
|
||||
faellig: `heute ${erste}`,
|
||||
beginn: lage === 'wunsch' ? 'sofort, auf deinen ausdrücklichen Wunsch' : 'sofort',
|
||||
}
|
||||
}
|
||||
|
||||
function angebotAbschnitt(offer) {
|
||||
if (!offer) return null
|
||||
return h('section', { class: 'kn-angebot-kasten', id: 'abo-angebot' },
|
||||
h('h2', null, 'Dein persönliches Angebot'),
|
||||
h('p', null, `${offer.text}. Du sparst ${fmtEuro(offer.savingsCents / 100)}.`),
|
||||
offer.note ? h('p', { class: 'kn-hinweis' }, offer.note) : null,
|
||||
h('p', { class: 'kn-hinweis' }, `Gültig bis ${fmtDatum(offer.validUntil)} · wird beim Abschluss automatisch berücksichtigt`))
|
||||
}
|
||||
|
||||
function leistungenAbschnitt(product) {
|
||||
return h('section', { class: 'kn-panel', id: 'abo-leistungen' },
|
||||
h('ul', { class: 'kn-leistungen' }, (product.features || []).map(([titel, detail]) => h('li', null, titel, h('small', null, detail)))),
|
||||
h('p', { class: 'kn-hinweis' }, 'Schon bei einem anderen Anbieter für Domain, Hosting oder E-Mail bezahlt? Nach dem Abschluss kannst du eine Übernahmegutschrift beantragen – wir rechnen die doppelt bezahlte Zeit anteilig an.'))
|
||||
}
|
||||
|
||||
function bestellung(daten, productKey, { viewAs, kundenart }) {
|
||||
const product = produktVon(daten, productKey)
|
||||
const T = daten.bestelltexte
|
||||
const offer = daten.offers?.[productKey] || null
|
||||
const privat = kundenart !== 'unternehmen'
|
||||
const frist = daten.lateStartAt ? fmtDatum(daten.lateStartAt) : ''
|
||||
const { preisZeile, erste } = preise(product, offer)
|
||||
// Betrieb: sofort, ohne Auswahl. Privat und leer: ohne Häkchen nach der Frist, mit Häkchen sofort.
|
||||
const start = faelligBeginn({ lage: privat ? 'frist' : 'sofort', erste, frist })
|
||||
|
||||
const block = baueBlock([
|
||||
{ label: 'Leistung', text: labelVon(daten, productKey), link: { text: 'Leistungen ansehen', href: leistungLink(T.abo.linkzeile, productKey) } },
|
||||
{ label: 'Preis', text: preisZeile },
|
||||
{ id: 'abo-due-summary', label: 'Fällig', text: start.faellig },
|
||||
{ id: 'abo-start-summary', label: 'Beginn', text: start.beginn },
|
||||
{ label: 'Laufzeit', text: `Mindestlaufzeit ${product.minTermMonths} Monate, kündbar bis 1 Monat vor Ende; danach monatlich` },
|
||||
{ label: 'Zahlung', text: 'Karte oder SEPA über Stripe' },
|
||||
], { id: 'abo-block' })
|
||||
// Die ausführlichen Sätze stehen unter dem Knopf
|
||||
const einzelheiten = baueEinzelheiten([
|
||||
{ label: 'Laufzeit', text: `Mindestlaufzeit ${product.minTermMonths} Monate ab Leistungsbeginn, kündbar zum Ende der Mindestlaufzeit bis einen Monat vorher; danach jederzeit zum Ende des Abrechnungsmonats kündbar` },
|
||||
{ label: 'Zahlung', text: 'Kreditkarte oder SEPA-Lastschrift über Stripe; die Zahlungsart hinterlegst du im nächsten Schritt, bis zum Abschluss dort kannst du abbrechen' },
|
||||
{ label: 'Kosten', text: 'keine weiteren Kosten' },
|
||||
], { id: 'abo-einzelheiten' })
|
||||
|
||||
const haken = privat ? h('input', { type: 'checkbox', id: 'abo-beginn', disabled: viewAs }) : null
|
||||
const fehler = h('p', { class: K.fehler, id: 'abo-error', role: 'alert', hidden: true })
|
||||
const kaufen = h('button', { type: 'button', class: K.primaer, id: 'abo-buy', disabled: viewAs }, T.knopf || KNOPF)
|
||||
const zeigeFehler = (text) => { fehler.textContent = text; fehler.hidden = false }
|
||||
|
||||
const abschnitt = h('section', { class: 'kn-panel kn-bestellen', id: 'abo-order' },
|
||||
h('h2', null, 'Was du bestellst'),
|
||||
daten.testAccount ? h('div', { class: 'kn-meldung kn-meldung-warn' }, `Testkonto: Bei Stripe wird ein Test-Rabatt verrechnet, abgebucht werden nur 0,50 € pro Monat.${offer ? ' Das Angebot wird nach dem Abschluss zusätzlich ans Abo gehängt.' : ''}`) : null,
|
||||
block,
|
||||
baueLinkzeile(T.abo.linkzeile, { id: 'abo-linkzeile' }),
|
||||
haken ? h('label', { class: K.check }, haken, h('span', { id: 'abo-beginn-text' }, T.abo.haken)) : null,
|
||||
fehler,
|
||||
h('div', { class: 'kn-aktionen' }, kaufen, viewAs ? h('span', { class: K.meldung }, TEXT_SPERRE) : null),
|
||||
baueKundenartZeile(kundenart, T.kundenart, { id: 'abo-kundenart' }),
|
||||
einzelheiten)
|
||||
|
||||
// Zurück von der Zahlungsseite (Bfcache): der Knopf ist wieder bedienbar (E-Fix M6, wie ansicht-wechsel.js)
|
||||
beiRueckkehr(abschnitt, () => { kaufen.disabled = viewAs; kaufen.textContent = T.knopf || KNOPF })
|
||||
|
||||
// Das Häkchen schaltet die Zeilen „Fällig“ und „Beginn“ um
|
||||
haken?.addEventListener('change', () => {
|
||||
const neu = faelligBeginn({ lage: haken.checked ? 'wunsch' : 'frist', erste, frist })
|
||||
abschnitt.querySelector('#abo-due-summary').textContent = neu.faellig
|
||||
abschnitt.querySelector('#abo-start-summary').textContent = neu.beginn
|
||||
})
|
||||
|
||||
kaufen.addEventListener('click', async () => {
|
||||
if (viewAs) return
|
||||
fehler.hidden = true
|
||||
kaufen.disabled = true
|
||||
kaufen.textContent = 'Weiter zur Zahlung…'
|
||||
try {
|
||||
// Der Klick auf den Knopf ist die Zustimmung (acceptTerms). Nie kundenart oder startChoice: den Beginn bestimmt der Server aus der Kundenart.
|
||||
const { url } = await api('/api/abo/checkout', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
productKey,
|
||||
acceptTerms: true,
|
||||
termsVersion: daten.termsVersion,
|
||||
offerId: offer?.id || '',
|
||||
...(privat ? { earlyStart: haken.checked, lateStartAt: daten.lateStartAt || '' } : {}),
|
||||
},
|
||||
})
|
||||
const ziel = sichereAdresse(url)
|
||||
if (!ziel) throw new Error(TEXT_ZAHLUNGSSEITE)
|
||||
window.location.href = ziel
|
||||
} catch (f) {
|
||||
zeigeFehler(f.message)
|
||||
kaufen.disabled = false
|
||||
kaufen.textContent = T.knopf || KNOPF
|
||||
}
|
||||
})
|
||||
|
||||
return [angebotAbschnitt(offer), leistungenAbschnitt(product), abschnitt]
|
||||
}
|
||||
|
||||
/** Bestellseite. Das Produkt kommt aus route.produkt, hilfsweise aus dem Hash; daten (GET /api/abo) ist optional. */
|
||||
export async function zeigeBestellen(wurzel, { route, me, nav, daten } = {}) {
|
||||
const nr = (laeufe.get(wurzel) ?? 0) + 1
|
||||
laeufe.set(wurzel, nr)
|
||||
const aktuell = () => laeufe.get(wurzel) === nr
|
||||
leeren(wurzel).append(h('p', { class: K.leer }, 'Wird geladen …'))
|
||||
const stand = daten ?? await ladeAbo()
|
||||
if (!aktuell()) return
|
||||
if (!stand) { leeren(wurzel).append(h('p', { class: K.fehler }, ABO_NICHT_GELADEN)); return }
|
||||
const key = produktAusRoute(route)
|
||||
const product = key ? produktVon(stand, key) : null
|
||||
if (key) meldeTitel(wurzel, labelVon(stand, key))
|
||||
// Ohne Wortlaut vom Server gibt es keinen Bestellknopf
|
||||
if (!product || !stand.available || !hatWortlaut(stand)) {
|
||||
leeren(wurzel).append(h('div', { class: 'kn-panel' }, h('p', { class: 'kn-hinweis' }, TEXT_NICHT_VERFUEGBAR)))
|
||||
return
|
||||
}
|
||||
leeren(wurzel).append(...bestellung(stand, key, { viewAs: me?.viewAs === true, kundenart: kundenartVon(me, stand) }).filter(Boolean))
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
/* Kundenansicht neu (P2): Bereich Chat. Verlauf und Eingabezeile des gemeinsamen Composers (window.ChatComposer, chat-composer.js),
|
||||
Poll alle 10 s mit ?since=, Gelesen-Markierung, Vorgabe aus "Postfach anfragen". Nachrichten gehen nur als Text in den Verlauf. */
|
||||
|
||||
import { api } from './api.js'
|
||||
import { h, leeren } from './dom.js'
|
||||
import { K, fmtDatumZeit, meldeZaehlerNeu, nimmChatVorgabe } from './ansicht-hilfen.js'
|
||||
import { HANDY_ABFRAGE } from './leiste.js'
|
||||
|
||||
export const CHAT_TEXTE = {
|
||||
platzhalter: 'Nachricht an das WEBklar-Team…',
|
||||
kurz: 'Nachricht…',
|
||||
gesperrt: 'Im Ansichtsmodus kann nicht als Kunde geschrieben werden.',
|
||||
leer: 'Noch keine Nachrichten.',
|
||||
}
|
||||
|
||||
const POLL_MS = 10000
|
||||
const dateiUrl = (att) => `/api/chat/attachments/${encodeURIComponent(att.id)}`
|
||||
|
||||
/** 'Du · 14.10.26, 10:05' oder '<Name oder Webklar-Team> · …'. */
|
||||
export function nachrichtMeta(msg) {
|
||||
const meine = msg?.sender === 'customer'
|
||||
return `${meine ? 'Du' : msg?.senderName || 'Webklar-Team'} · ${fmtDatumZeit(msg?.createdAt)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Um wie viele Pixel muss die Seite scrollen, damit die Eingabe im sichtbaren Bereich (visualViewport) steht? Aus revealChatComposer (app.js:2240-2260):
|
||||
* nur nachschieben, wenn sie wirklich verdeckt ist (8 px Luft), nie die Seite nach oben pinnen. Unter 1 Pixel und ungültige Werte ergeben 0.
|
||||
*/
|
||||
export function composerVersatz({ rect, visibleTop, visibleBottom }, luft = 8) {
|
||||
let delta = 0
|
||||
if (rect.bottom > visibleBottom - luft) delta = rect.bottom - visibleBottom + luft
|
||||
else if (rect.top < visibleTop + luft) delta = rect.top - visibleTop - luft
|
||||
return Number.isFinite(delta) && Math.abs(delta) > 1 ? delta : 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Die Eingabe über der Tastatur halten (E-Fix M5, wie app.js): 350 ms nach dem Fokus im Composer (die Tastatur-Animation auf iOS) und bei jedem
|
||||
* visualViewport-Resize die Eingabe in den sichtbaren Bereich schieben. Nur am Handy und nur, solange der Fokus im Composer liegt.
|
||||
* Rückgabe { abbau } entfernt die Hörer.
|
||||
*/
|
||||
export function verdrahteEingabe({ win = globalThis.window, doc = globalThis.document, wurzel }) {
|
||||
const vv = win?.visualViewport
|
||||
const zeige = () => {
|
||||
if (!vv || !wurzel.contains(doc.activeElement) || !win.matchMedia?.(HANDY_ABFRAGE).matches) return
|
||||
const delta = composerVersatz({ rect: wurzel.getBoundingClientRect(), visibleTop: vv.offsetTop, visibleBottom: vv.offsetTop + vv.height })
|
||||
if (delta) win.scrollBy(0, delta)
|
||||
}
|
||||
const beiFokus = (ev) => { if (wurzel.contains(ev.target) && ev.target.matches?.('textarea, input')) win.setTimeout(zeige, 350) }
|
||||
const frame = (fn) => (win.requestAnimationFrame ? win.requestAnimationFrame(fn) : fn())
|
||||
const beiVv = () => frame(() => frame(zeige))
|
||||
doc.addEventListener('focusin', beiFokus)
|
||||
vv?.addEventListener('resize', beiVv)
|
||||
return {
|
||||
abbau() {
|
||||
doc.removeEventListener('focusin', beiFokus)
|
||||
vv?.removeEventListener('resize', beiVv)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Druck auf einen Knopf der Eingabezeile (Senden, Anhang, Mikrofon) nimmt dem Textfeld den Fokus nicht. Sonst endet mit dem Fokus die Tastatur-Stellung der Hülle
|
||||
* (leiste.js): die Leiste kommt zurück und schiebt die fest stehende Eingabe um ihre Höhe nach oben, bevor der Finger loslässt, und der Tipp auf „Senden“
|
||||
* geht ins Leere (Android). Das Textfeld behält den Fokus, die Tastatur bleibt wie in jedem Chat offen. Tab und Tastatur-Bedienung bleiben unberührt.
|
||||
* Rückgabe { abbau } entfernt den Hörer.
|
||||
*/
|
||||
export function haltFokus(wurzel) {
|
||||
const beiDruck = (ev) => {
|
||||
const knopf = ev.target?.closest?.('button')
|
||||
if (knopf && wurzel.contains(knopf)) ev.preventDefault()
|
||||
}
|
||||
wurzel.addEventListener('mousedown', beiDruck)
|
||||
return { abbau() { wurzel.removeEventListener('mousedown', beiDruck) } }
|
||||
}
|
||||
|
||||
let aktiv = null // der laufende Chat: { timer, composer, eingabe, beobachter, fokus }
|
||||
|
||||
function raeumeAuf() {
|
||||
if (!aktiv) return
|
||||
if (aktiv.timer) clearInterval(aktiv.timer)
|
||||
try { aktiv.composer?.destroy?.() } catch { /* schon abgebaut */ }
|
||||
try { aktiv.eingabe?.abbau() } catch { /* schon abgebaut */ }
|
||||
try { aktiv.fokus?.abbau() } catch { /* schon abgebaut */ }
|
||||
try { aktiv.beobachter?.disconnect() } catch { /* schon abgebaut */ }
|
||||
aktiv = null
|
||||
}
|
||||
|
||||
export async function zeigeChat(wurzel, { route, me, nav } = {}) {
|
||||
raeumeAuf()
|
||||
const eigener = { timer: null, composer: null, eingabe: null, beobachter: null, fokus: null }
|
||||
aktiv = eigener
|
||||
const gilt = () => aktiv === eigener
|
||||
leeren(wurzel).append(h('p', { class: K.leer }, 'Wird geladen …'))
|
||||
|
||||
const Composer = globalThis.ChatComposer
|
||||
if (!Composer) {
|
||||
leeren(wurzel).append(h('p', { class: K.fehler }, 'Der Chat konnte nicht gestartet werden. Bitte lade die Seite neu.'))
|
||||
return
|
||||
}
|
||||
const viewAs = me?.viewAs === true
|
||||
|
||||
const verlauf = h('div', { class: 'chat-messages kn-chat-verlauf', role: 'log', 'aria-label': 'Nachrichtenverlauf' })
|
||||
const composerWurzel = h('div', { class: 'kn-chat-composer' })
|
||||
const kasten = h('div', { class: 'chat-box kn-chat-kasten' }, verlauf, composerWurzel)
|
||||
leeren(wurzel).append(h('div', { class: 'kn-chat' }, kasten))
|
||||
|
||||
const gezeigt = new Set()
|
||||
let zuletzt = ''
|
||||
let leerHinweis = null
|
||||
let fehler = null
|
||||
const hinweisLeer = () => {
|
||||
if (gezeigt.size || leerHinweis) return
|
||||
leerHinweis = h('p', { class: K.leer }, CHAT_TEXTE.leer)
|
||||
verlauf.append(leerHinweis)
|
||||
}
|
||||
const raeumeHinweise = () => { leerHinweis?.remove(); leerHinweis = null; fehler?.remove(); fehler = null }
|
||||
|
||||
/** Neue Nachrichten anhängen (nach id entdoppelt); Rückgabe: die wirklich neuen. */
|
||||
function haengeAn(nachrichten) {
|
||||
const neu = []
|
||||
for (const msg of nachrichten) {
|
||||
if (!msg || gezeigt.has(msg.id)) continue
|
||||
gezeigt.add(msg.id)
|
||||
neu.push(msg)
|
||||
}
|
||||
if (!neu.length) return neu
|
||||
raeumeHinweise()
|
||||
for (const msg of neu) {
|
||||
verlauf.append(Composer.renderMessage(msg, { mine: msg.sender === 'customer', meta: nachrichtMeta(msg), fileUrl: dateiUrl }))
|
||||
if (msg.createdAt > zuletzt) zuletzt = msg.createdAt
|
||||
}
|
||||
verlauf.scrollTop = verlauf.scrollHeight
|
||||
return neu
|
||||
}
|
||||
|
||||
// Nachrichten als gelesen markieren; im Ansichtsmodus nie (der Kunde hat sie nicht gelesen)
|
||||
async function markiereGelesen() {
|
||||
if (viewAs) return
|
||||
await api('/api/chat/read', { method: 'POST' }).catch(() => {})
|
||||
if (gilt()) meldeZaehlerNeu('chat')
|
||||
}
|
||||
|
||||
async function hole() {
|
||||
const frage = zuletzt ? `?since=${encodeURIComponent(zuletzt)}` : ''
|
||||
const daten = await api(`/api/chat/messages${frage}`)
|
||||
if (!gilt()) return []
|
||||
return haengeAn(Array.isArray(daten?.messages) ? daten.messages : [])
|
||||
}
|
||||
|
||||
async function poll() {
|
||||
if (!gilt()) return
|
||||
if (!wurzel.isConnected) { clearInterval(eigener.timer); return }
|
||||
if (globalThis.document?.hidden) return
|
||||
if (wurzel.closest?.('[hidden]')) return // der Chat ist gerade nicht zu sehen
|
||||
try {
|
||||
const neu = await hole()
|
||||
if (neu.some((m) => m.sender !== 'customer')) await markiereGelesen()
|
||||
} catch { /* der nächste Poll versucht es erneut */ }
|
||||
}
|
||||
|
||||
Composer.trackScroll(verlauf)
|
||||
eigener.composer = Composer.create({
|
||||
root: composerWurzel,
|
||||
dropZone: kasten,
|
||||
uploadUrl: '/api/chat/attachments',
|
||||
placeholder: CHAT_TEXTE.platzhalter,
|
||||
placeholderShort: CHAT_TEXTE.kurz,
|
||||
disabled: viewAs,
|
||||
disabledText: CHAT_TEXTE.gesperrt,
|
||||
send: async ({ text, attachmentIds }) => {
|
||||
const { message } = await api('/api/chat/messages', { method: 'POST', body: { text, attachmentIds } })
|
||||
return message
|
||||
},
|
||||
onSent: (message) => { haengeAn([message]) },
|
||||
})
|
||||
|
||||
// Eingabe über der Tastatur halten; wird der Verlauf kleiner (Tastatur, Leiste), bleibt die neueste Nachricht sichtbar, sofern man unten war (E-Fix M5)
|
||||
eigener.eingabe = verdrahteEingabe({ win: globalThis.window, doc: globalThis.document, wurzel: composerWurzel })
|
||||
eigener.fokus = haltFokus(composerWurzel)
|
||||
if (typeof globalThis.ResizeObserver === 'function') {
|
||||
eigener.beobachter = new globalThis.ResizeObserver(() => { if (verlauf.dataset.stick !== 'false') verlauf.scrollTop = verlauf.scrollHeight })
|
||||
eigener.beobachter.observe(verlauf)
|
||||
}
|
||||
|
||||
const vorgabe = nimmChatVorgabe()
|
||||
if (vorgabe) {
|
||||
const feld = composerWurzel.querySelector('textarea')
|
||||
if (feld) {
|
||||
feld.value = vorgabe
|
||||
if (typeof Event === 'function') feld.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
feld.focus()
|
||||
if (typeof feld.setSelectionRange === 'function') feld.setSelectionRange(vorgabe.length, vorgabe.length)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await hole()
|
||||
if (!gilt()) return
|
||||
hinweisLeer()
|
||||
} catch (f) {
|
||||
if (!gilt()) return
|
||||
fehler = h('p', { class: K.fehler }, `Nachrichten konnten nicht geladen werden: ${f.message}`)
|
||||
verlauf.append(fehler)
|
||||
}
|
||||
await markiereGelesen()
|
||||
if (!gilt()) return
|
||||
eigener.timer = setInterval(poll, POLL_MS)
|
||||
eigener.timer?.unref?.() // im Browser eine Zahl; in Node hält der Timer den Prozess nicht fest
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
/* Kundenansicht neu (P3): Bereich „Funktionen“ mit den Reitern Meine und Entdecken.
|
||||
Der erste Teil ist rein (Modell aus der Antwort von GET /api/funktionen im Abo-Modell, V5); die Ansicht darunter zeichnet es.
|
||||
Keine Preise: Der Katalog trägt keinen. Fremdtext (Funktions- und Websitenamen) wird nur als Text gesetzt. */
|
||||
|
||||
import { api as apiStandard } from './api.js'
|
||||
import { h, leeren } from './dom.js'
|
||||
import { baueReiter } from './reiter.js'
|
||||
import { geheZu as geheZuStandard, oeffneOverlay as oeffneOverlayStandard, beendeOverlay as beendeOverlayStandard } from './router.js'
|
||||
import { ladeOffeneRechnungen, rechnungOffenZeile } from './rechnung-offen.js'
|
||||
import { ETAGE_FUER, TEXTE, anfrageStatusVon, einschaltWeg, navigiere, nimmMeldung, renderEinschalten } from './funktion-einschalten.js'
|
||||
|
||||
const ANFRAGE_SICHTBAR = ['offen', 'angenommen', 'abgelehnt'] // zurückgezogen zählt nicht
|
||||
|
||||
/** Zeitpunkt oder 'JJJJ-MM-TT' → 'TT.MM.JJJJ' (Europe/Berlin); unlesbar ergibt ''. Ein Tagestext wird als Tag um 12:00 UTC gelesen. */
|
||||
export function datumDe(wert) {
|
||||
if (typeof wert !== 'string' || wert === '') return ''
|
||||
const ms = Date.parse(/^\d{4}-\d{2}-\d{2}$/.test(wert) ? `${wert}T12:00:00Z` : wert)
|
||||
if (Number.isNaN(ms)) return ''
|
||||
return new Date(ms).toLocaleDateString('de-DE', { timeZone: 'Europe/Berlin', day: '2-digit', month: '2-digit', year: 'numeric' })
|
||||
}
|
||||
|
||||
const verfuegbare = (d) => (Array.isArray(d?.katalog) ? d.katalog.filter((e) => e && typeof e === 'object' && e.verfuegbar === true && typeof e.key === 'string') : [])
|
||||
const websites = (d) => (Array.isArray(d?.projects) ? d.projects.filter((p) => p && typeof p === 'object' && typeof p.projectId === 'string') : [])
|
||||
const zustandVon = (projekt, key) => (key === 'analyse' ? projekt.analyse?.state : projekt.features?.[key]?.state)
|
||||
|
||||
/** Hat diese Website die Funktion schon (Zeile in Meine)? Eine Zeile ist Zustand, Entwurf des Fragebogens oder sichtbare Anfrage. */
|
||||
function istInMeine(eintrag, projekt) {
|
||||
if (eintrag.key === 'analyse') return ['active', 'disabled'].includes(projekt.analyse?.state)
|
||||
const f = projekt.features?.[eintrag.key]
|
||||
if (!f || typeof f !== 'object') return false
|
||||
if (f.state !== 'off') return typeof f.state === 'string'
|
||||
if (eintrag.ablauf === 'fragebogen') return Boolean(f.siteKey)
|
||||
if (eintrag.ablauf === 'anfrage') return ANFRAGE_SICHTBAR.includes(anfrageStatusVon(projekt, eintrag.key))
|
||||
return false
|
||||
}
|
||||
|
||||
/** „Dein Abo ist beendet.“ mit dem Löschtag, wenn einer da ist. */
|
||||
function aboEndeText(f) {
|
||||
const tag = datumDe(f.loeschungAm)
|
||||
return tag ? `Dein Abo ist beendet. Deine Daten bleiben bis ${tag} gespeichert.` : 'Dein Abo ist beendet.'
|
||||
}
|
||||
|
||||
const KURZINFO_MAX = 80 // wie der Server (kurzinfo.js): länger ist kein Detail mehr, sondern ein Fehler
|
||||
|
||||
/** Kurzinfo einer aktiven Funktion aus der Antwort des Servers: nur ein nicht leerer, kurzer Text; sonst '' (die Zeile zeigt dann „Aktiv“). */
|
||||
function kurzinfoVon(quelle) {
|
||||
const text = typeof quelle?.kurzinfo === 'string' ? quelle.kurzinfo.trim() : ''
|
||||
return text !== '' && text.length <= KURZINFO_MAX ? text : ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Pille und Detailzeile einer Funktion an einer Website (nur für Websites, bei denen istInMeine gilt). Läuft die Funktion, gibt es keine
|
||||
* Pille: die Detailzeile ist die Kurzinfo des Servers („3 Termine heute · nächster 10:30“) oder „Aktiv“. Alles andere trägt eine Pille.
|
||||
*/
|
||||
function statusVon(eintrag, projekt) {
|
||||
if (eintrag.key === 'analyse') {
|
||||
const a = projekt.analyse
|
||||
if (a.state === 'active') return { pille: null, status: kurzinfoVon(a) || 'Aktiv' }
|
||||
return { pille: { text: 'Gesperrt', ton: 'grau' }, status: a.disabledReason === 'abo_ende' ? aboEndeText(a) : 'Schreib uns im Chat' }
|
||||
}
|
||||
const f = projekt.features[eintrag.key]
|
||||
switch (f.state) {
|
||||
case 'setup':
|
||||
return { pille: { text: 'In Einrichtung', ton: 'blau' }, status: 'Wir richten sie gerade ein' }
|
||||
case 'ordered':
|
||||
return { pille: { text: 'Zahlung offen', ton: 'orange' }, status: 'Wir warten auf deine Zahlung' }
|
||||
case 'active':
|
||||
return { pille: null, status: kurzinfoVon(f) || 'Aktiv' }
|
||||
case 'disabled':
|
||||
return { pille: { text: 'Abgeschaltet', ton: 'grau' }, status: f.disabledReason === 'abo_ende' ? aboEndeText(f) : 'Im Chat wieder anfragen' }
|
||||
default: {
|
||||
if (eintrag.ablauf === 'fragebogen') return { pille: { text: 'Entwurf', ton: 'grau' }, status: 'Fragebogen fortsetzen' }
|
||||
const anfrage = anfrageStatusVon(projekt, eintrag.key)
|
||||
if (anfrage === 'angenommen') return { pille: { text: 'Angenommen', ton: 'blau' }, status: 'Jetzt einschalten' }
|
||||
if (anfrage === 'abgelehnt') return { pille: { text: 'Abgelehnt', ton: 'rot' }, status: 'Anfrage abgelehnt · Im Chat nachfragen' }
|
||||
return { pille: { text: 'Angefragt', ton: 'violett' }, status: 'Wir prüfen deine Anfrage' }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Die Zeilen von „Meine“: je Funktion und Website eine, in Katalogreihenfolge, dann Websitereihenfolge. Funktionen mit
|
||||
* verfuegbar !== true fehlen (C9). pille ist null, solange die Funktion läuft; status ist die Detailzeile (Kurzinfo oder Zustandstext),
|
||||
* zeile2 ist bei mehreren Websites „Websitename · status“, sonst der Status allein.
|
||||
* @returns {Array<{ key: string, name: string, projectId: string, projektName: string, zustand: string, anfrageStatus: string, pille: { text: string, ton: string }|null, status: string, zeile2: string }>}
|
||||
*/
|
||||
export function meineZeilen(daten) {
|
||||
const seiten = websites(daten)
|
||||
const mehrere = seiten.length > 1
|
||||
const zeilen = []
|
||||
for (const eintrag of verfuegbare(daten)) {
|
||||
for (const projekt of seiten) {
|
||||
if (!istInMeine(eintrag, projekt)) continue
|
||||
const { pille, status } = statusVon(eintrag, projekt)
|
||||
const projektName = String(projekt.projectName ?? '')
|
||||
zeilen.push({
|
||||
key: eintrag.key,
|
||||
name: String(eintrag.name ?? ''),
|
||||
projectId: projekt.projectId,
|
||||
projektName,
|
||||
zustand: String(zustandVon(projekt, eintrag.key) ?? ''),
|
||||
anfrageStatus: eintrag.key === 'analyse' ? '' : anfrageStatusVon(projekt, eintrag.key),
|
||||
pille,
|
||||
status,
|
||||
zeile2: mehrere && projektName !== '' ? `${projektName} · ${status}` : status,
|
||||
})
|
||||
}
|
||||
}
|
||||
return zeilen
|
||||
}
|
||||
|
||||
/**
|
||||
* Die Einträge von „Entdecken“ in Katalogreihenfolge: solange eine Website die Funktion nicht hat oder es keine Website gibt.
|
||||
* kandidaten sind die Websites ohne die Funktion (Analyse: nur mit Zustand off; unknown und comingSoon sind kein Kandidat).
|
||||
* @returns {Array<{ eintrag: object, kandidaten: Array<{ projectId: string, projektName: string }> }>}
|
||||
*/
|
||||
export function entdeckenEintraege(daten) {
|
||||
const seiten = websites(daten)
|
||||
const ergebnis = []
|
||||
for (const eintrag of verfuegbare(daten)) {
|
||||
const ohne = seiten.filter((p) => !istInMeine(eintrag, p))
|
||||
if (seiten.length > 0 && ohne.length === 0) continue
|
||||
const kandidaten = (eintrag.key === 'analyse' ? ohne.filter((p) => p.analyse?.state === 'off') : ohne)
|
||||
.map((p) => ({ projectId: p.projectId, projektName: String(p.projectName ?? '') }))
|
||||
ergebnis.push({ eintrag, kandidaten })
|
||||
}
|
||||
return ergebnis
|
||||
}
|
||||
|
||||
/** Tipp auf eine Zeile in Meine: eine angenommene Anfrage geht zur Bestätigung, alles andere in die Etage der Funktion. */
|
||||
export function zeilenWeg(zeile) {
|
||||
if (zeile.zustand === 'off' && zeile.anfrageStatus === 'angenommen') return { art: 'seite' }
|
||||
return { art: 'etage', etage: ETAGE_FUER[zeile.key] }
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------------------------------ Ansicht --- */
|
||||
|
||||
const laeufe = new WeakMap() // Wurzel → Nummer des jüngsten Aufrufs: eine ältere Antwort zeichnet nichts mehr
|
||||
let reiterGewaehlt = false // Tipp auf „Meine“: der nächste Aufruf zeigt den Leerzustand statt zu Entdecken zu springen
|
||||
let vorgabe = null // nach einer eigenen Navigation (Reiter, Sprung zu Entdecken) nimmt der nächste Aufruf die geladenen Antworten
|
||||
const wechselErledigt = new Set() // 'key:projectId' der Funktionen, in die die Rückkehr vom Wechsel schon geführt hat (je Seitenaufruf einmal)
|
||||
|
||||
/** Setzt die Merker des Moduls zurück (nur für Tests). */
|
||||
export function vergissMerker() {
|
||||
reiterGewaehlt = false
|
||||
vorgabe = null
|
||||
wechselErledigt.clear()
|
||||
}
|
||||
|
||||
const BILD = /^\/neu\/bilder\/[a-z0-9-]+\.webp$/
|
||||
|
||||
/** Nur diese Seite (Meine, Detailzeile) bittet den Server um die Kurzinfo; Etagen, Einschalt-Ablauf und Module laden GET /api/funktionen ohne den Parameter. */
|
||||
const FUNKTIONEN_PFAD = '/api/funktionen?kurzinfo=1'
|
||||
|
||||
/** Stück für die Pille einer Zeile. */
|
||||
const pille = (p) => h('span', { class: `kn-fn-pille kn-fn-pille--${p.ton}` }, p.text)
|
||||
|
||||
export async function zeigeFunktionen(wurzel, { route, me, nav } = {}, abh = {}) {
|
||||
const A = {
|
||||
api: abh.api ?? apiStandard,
|
||||
geheZu: abh.geheZu ?? geheZuStandard,
|
||||
oeffneOverlay: abh.oeffneOverlay ?? oeffneOverlayStandard,
|
||||
beendeOverlay: abh.beendeOverlay ?? beendeOverlayStandard,
|
||||
speicher: abh.speicher,
|
||||
jetzt: abh.jetzt ?? (() => new Date()),
|
||||
}
|
||||
const nr = (laeufe.get(wurzel) ?? 0) + 1
|
||||
laeufe.set(wurzel, nr)
|
||||
const aktuell = () => laeufe.get(wurzel) === nr
|
||||
const nochmal = () => zeigeFunktionen(wurzel, { route, me, nav }, abh)
|
||||
const reiterWahl = route?.reiter === 'entdecken' ? 'entdecken' : 'meine'
|
||||
const ausdruecklich = reiterGewaehlt
|
||||
reiterGewaehlt = false
|
||||
const mitgebracht = vorgabe && vorgabe.wurzel === wurzel && vorgabe.bis > Date.now() ? vorgabe : null
|
||||
vorgabe = null
|
||||
|
||||
let daten
|
||||
let offen
|
||||
const frisch = !mitgebracht
|
||||
if (mitgebracht) {
|
||||
({ daten, offen } = mitgebracht)
|
||||
} else {
|
||||
leeren(wurzel).append(h('p', { class: 'kn-leer' }, 'Wird geladen …'))
|
||||
try {
|
||||
[daten, offen] = await Promise.all([A.api(FUNKTIONEN_PFAD), ladeOffeneRechnungen(A.api)])
|
||||
} catch {
|
||||
if (!aktuell()) return
|
||||
leeren(wurzel).append(
|
||||
h('p', { class: 'kn-fehler', role: 'alert' }, 'Die Funktionen konnten gerade nicht geladen werden.'),
|
||||
h('button', { type: 'button', class: 'kn-fn-knopf kn-fn-knopf--zweit', 'data-aktion': 'nochmal', on: { click: () => nochmal() } }, 'Nochmal versuchen'))
|
||||
return
|
||||
}
|
||||
if (!aktuell()) return
|
||||
}
|
||||
if (daten?.modell !== 'abo') {
|
||||
leeren(wurzel).append(h('p', { class: 'kn-fehler', role: 'alert' }, 'Die Funktionen sind gerade nicht verfügbar. Lade die Seite neu.'))
|
||||
return
|
||||
}
|
||||
|
||||
const kundenart = me?.customer?.kundenartWirksam ?? daten.kundenart
|
||||
const viewAs = me?.viewAs === true
|
||||
const zeilen = meineZeilen(daten)
|
||||
const projektVon = (projectId) => daten.projects.find((p) => p?.projectId === projectId)
|
||||
const weiter = (weg, ziel) => navigiere(weg, ziel, { geheZu: A.geheZu, speicher: A.speicher })
|
||||
let gezeigteMeldung = ''
|
||||
|
||||
/** Die Bestätigungsseite für diese Funktion und Website als Overlay: die Zurück-Geste und „Abbrechen“ bringen die Liste ohne neuen Abruf zurück. */
|
||||
function oeffneSeite(key, projectId) {
|
||||
A.oeffneOverlay({ schliesse: () => { if (aktuell()) zeichneListe() } })
|
||||
renderEinschalten(wurzel, {
|
||||
key, projectId, daten, kundenartWirksam: kundenart, viewAs, jetzt: A.jetzt, api: A.api, geheZu: A.geheZu, speicher: A.speicher,
|
||||
bei: { abbrechen: () => A.beendeOverlay() },
|
||||
})
|
||||
}
|
||||
|
||||
/** „Einschalten“ für eine Website: die Vorprüfung wählt den Ort; eine Meldung steht im Eintrag. */
|
||||
function einschaltTipp(eintrag, kandidat, meldungEl) {
|
||||
const weg = einschaltWeg({ eintrag, projekt: projektVon(kandidat.projectId), abo: daten.abo, kundenart })
|
||||
if (weg.art === 'meldung') { meldungEl.textContent = weg.text; return }
|
||||
meldungEl.textContent = ''
|
||||
if (weg.art === 'seite') oeffneSeite(eintrag.key, kandidat.projectId)
|
||||
else weiter(weg, { key: eintrag.key, projectId: kandidat.projectId })
|
||||
}
|
||||
|
||||
/**
|
||||
* Rückkehr vom Wechsel ins Wartungs-Abo (V11): hat der Kunde den Wechsel für eine Funktion begonnen und hat die Website sie noch nicht,
|
||||
* öffnet die Ansicht ihren Ablauf, je Seitenaufruf einmal. Jeder Fehler und jede ungültige Angabe wird ignoriert.
|
||||
* @returns {Promise<boolean>} true, wenn ein Ablauf geöffnet wurde
|
||||
*/
|
||||
async function wechselRueckkehr() {
|
||||
if (daten.abo?.produkt !== 'wartung' || daten.abo.laeuft !== true) return false
|
||||
let abo
|
||||
try { abo = await A.api('/api/abo') } catch { return false }
|
||||
if (!aktuell()) return false
|
||||
const f = abo?.subscription?.wechselFunktion
|
||||
if (!f || typeof f.key !== 'string' || typeof f.projectId !== 'string') return false
|
||||
const merker = `${f.key}:${f.projectId}`
|
||||
const eintrag = verfuegbare(daten).find((e) => e.key === f.key)
|
||||
const projekt = projektVon(f.projectId)
|
||||
if (!eintrag || !projekt || wechselErledigt.has(merker) || istInMeine(eintrag, projekt)) return false
|
||||
const weg = einschaltWeg({ eintrag, projekt, abo: daten.abo, kundenart })
|
||||
if (weg.art !== 'seite' && weg.art !== 'etage') return false
|
||||
wechselErledigt.add(merker)
|
||||
if (weg.art === 'seite') oeffneSeite(f.key, f.projectId)
|
||||
else weiter(weg, { key: f.key, projectId: f.projectId })
|
||||
return true
|
||||
}
|
||||
|
||||
if (frisch && await wechselRueckkehr()) return
|
||||
if (!aktuell()) return
|
||||
if (reiterWahl === 'meine' && zeilen.length === 0 && !ausdruecklich) {
|
||||
vorgabe = { wurzel, daten, offen, bis: Date.now() + 5000 }
|
||||
A.geheZu('#entdecken', { ersetzen: true })
|
||||
return
|
||||
}
|
||||
|
||||
function meineListe() {
|
||||
if (zeilen.length === 0) {
|
||||
return [
|
||||
h('p', { class: 'kn-leer' }, 'Du hast noch keine Funktion eingeschaltet.'),
|
||||
h('button', { type: 'button', class: 'kn-fn-knopf', 'data-aktion': 'entdecken', on: { click: () => wechsleReiter('entdecken') } }, 'Entdecken'),
|
||||
]
|
||||
}
|
||||
return h('ul', { class: 'kn-fn-liste' }, zeilen.map((z) => h('li', { class: 'kn-fn-zeile', 'data-fn': z.key, 'data-projekt': z.projectId },
|
||||
h('button', {
|
||||
type: 'button',
|
||||
class: 'kn-fn-zeile-knopf',
|
||||
on: {
|
||||
click: () => {
|
||||
const weg = zeilenWeg(z)
|
||||
if (weg.art === 'seite') oeffneSeite(z.key, z.projectId)
|
||||
else weiter(weg, { key: z.key, projectId: z.projectId })
|
||||
},
|
||||
},
|
||||
},
|
||||
h('span', { class: 'kn-fn-text' }, h('span', { class: 'kn-fn-name' }, z.name), h('span', { class: 'kn-fn-zeile2' }, z.zeile2)),
|
||||
h('span', { class: 'kn-fn-rechts' }, z.pille ? pille(z.pille) : null, h('span', { class: 'kn-fn-pfeil', 'aria-hidden': 'true' }, '›'))))))
|
||||
}
|
||||
|
||||
function eintragKarte({ eintrag, kandidaten }) {
|
||||
const nurBetriebHinweis = eintrag.nurBetrieb === true && kundenart !== 'unternehmen'
|
||||
const meldungEl = h('p', { class: 'kn-fn-meldung', role: 'alert' })
|
||||
let wahl = null
|
||||
const knopf = h('button', {
|
||||
type: 'button',
|
||||
class: 'kn-fn-knopf',
|
||||
'data-aktion': 'einschalten',
|
||||
on: {
|
||||
click: () => {
|
||||
if (kandidaten.length === 1) { einschaltTipp(eintrag, kandidaten[0], meldungEl); return }
|
||||
if (wahl) { wahl.remove(); wahl = null; return }
|
||||
wahl = h('div', { class: 'kn-fn-wahl', role: 'group', 'aria-label': 'Für welche Website?' },
|
||||
kandidaten.map((k) => h('button', { type: 'button', class: 'kn-fn-knopf kn-fn-knopf--zweit', on: { click: () => einschaltTipp(eintrag, k, meldungEl) } }, k.projektName)))
|
||||
knopf.parentNode.insertBefore(wahl, meldungEl)
|
||||
},
|
||||
},
|
||||
}, 'Einschalten')
|
||||
if (viewAs) knopf.disabled = true // Mitarbeiter-Kundenansicht: gesperrt statt erst nach dem Tipp ein 403-Text
|
||||
const bild = typeof eintrag.bild === 'string' && BILD.test(eintrag.bild)
|
||||
? h('img', { class: 'kn-fn-bild', src: eintrag.bild, alt: `Beispiel: ${eintrag.name}`, width: 960, height: 600, loading: 'lazy', on: { error: (ev) => ev.target.remove() } })
|
||||
: null
|
||||
return h('section', { class: 'kn-fn-eintrag', 'data-fn': eintrag.key },
|
||||
bild,
|
||||
h('div', { class: 'kn-fn-eintrag-text' },
|
||||
h('h3', null, String(eintrag.name ?? '')),
|
||||
h('p', { class: 'kn-fn-beschreibung' }, String(eintrag.beschreibung ?? '')),
|
||||
h('p', { class: 'kn-fn-enthalten' }, eintrag.enthaltenIn === 'hosting' ? 'Im Hosting-Abo enthalten' : 'Im Wartungs-Abo enthalten'),
|
||||
nurBetriebHinweis ? h('p', { class: 'kn-fn-hinweis' }, TEXTE.nurBetrieb(String(eintrag.name ?? ''))) : (kandidaten.length ? knopf : null),
|
||||
!nurBetriebHinweis && kandidaten.length && viewAs ? h('p', { class: 'kn-fn-info' }, TEXTE.kundenansichtKnopf) : null,
|
||||
meldungEl))
|
||||
}
|
||||
|
||||
function entdeckenListe() {
|
||||
const eintraege = entdeckenEintraege(daten)
|
||||
if (eintraege.length === 0) return h('p', { class: 'kn-leer' }, 'Du hast schon alles eingeschaltet, was es gibt.')
|
||||
const ohneWebsite = daten.projects.length === 0
|
||||
return [
|
||||
ohneWebsite ? h('p', { class: 'kn-fn-info' }, 'Sobald deine Website angelegt ist, kannst du Funktionen einschalten.') : null,
|
||||
...eintraege.map(eintragKarte),
|
||||
]
|
||||
}
|
||||
|
||||
/** Reiterwechsel: Meine oder Entdecken; die geladenen Antworten bleiben für den folgenden Aufruf des Routers. */
|
||||
function wechsleReiter(key) {
|
||||
if (key === 'meine') reiterGewaehlt = true
|
||||
vorgabe = { wurzel, daten, offen, bis: Date.now() + 5000 }
|
||||
A.geheZu(key === 'meine' ? '#funktionen' : '#entdecken', { ersetzen: true })
|
||||
}
|
||||
|
||||
/** Reiter, Rechnungszeile und Liste aus den geladenen Antworten zeichnen (auch nach dem Schließen der Bestätigung, ohne Abruf). */
|
||||
function zeichneListe() {
|
||||
const reiter = baueReiter({ eintraege: [{ key: 'meine', label: 'Meine' }, { key: 'entdecken', label: 'Entdecken' }], aktiv: reiterWahl, art: 'unterstrich', ariaLabel: 'Funktionen', bei: wechsleReiter })
|
||||
leeren(wurzel).append(
|
||||
...[
|
||||
gezeigteMeldung ? h('p', { class: 'kn-meldung kn-meldung-ok', role: 'status' }, gezeigteMeldung) : null,
|
||||
rechnungOffenZeile(offen),
|
||||
reiter,
|
||||
h('div', { class: 'kn-reiter-inhalt' }, reiterWahl === 'meine' ? meineListe() : entdeckenListe()),
|
||||
].filter(Boolean))
|
||||
}
|
||||
|
||||
gezeigteMeldung = nimmMeldung() // genau einmal; nach dem Schließen der Bestätigung bleibt sie stehen
|
||||
zeichneListe()
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
/* Kundenansicht neu (P7): Übernahmegutschrift beantragen (#gutschrift) im schlanken Muster der Spec 5.6, Fassung 6. Antrag stellen, Stand
|
||||
sehen, Rückfragen beantworten. Über dem Knopf steht ein Satz mit den Links zu den Bedingungen und zum Datenschutz (der Klick auf den Knopf
|
||||
ist die Zustimmung, kein Häkchen). Die Bedingungen sind eine eigene Ansicht (#gutschrift?bedingungen=1) mit unverändertem Rechtstext
|
||||
(Block TAKEOVER_CONDITIONS, Hash-Pin in legalTexts.test.js, conditionsVersion 2026-09-30). Kundentext (Rückfrage, Entscheidung, Anbieter,
|
||||
Domain) kommt nur als Text in die Seite. Der Titel der Bedingungen kommt von der Hülle (meldeTitel); keine eigene h1.
|
||||
Die Sie-Form steht nur noch im Rechtstext TAKEOVER_CONDITIONS (hash-gepinnt), sonst gilt „du“. */
|
||||
|
||||
import { api } from './api.js'
|
||||
import { h, leeren } from './dom.js'
|
||||
import { K, ersetze, zuAnsicht, ladeAbo, fmtDatum, fmtEuro, meldeTitel, meldeZaehlerNeu, ABO_NICHT_GELADEN } from './ansicht-hilfen.js'
|
||||
import { baueLinkzeile, hatWortlaut } from './bestell-bausteine.js'
|
||||
|
||||
const laeufe = new WeakMap() // Wurzel → Nummer des jüngsten Aufrufs
|
||||
const TEXT_ANSICHTSMODUS = 'Im Ansichtsmodus kann kein Antrag gestellt werden.'
|
||||
const TEXT_NICHT_VERFUEGBAR = 'Die Online-Bestellung wird gerade eingerichtet – schreib uns gern im Chat.'
|
||||
const TITEL_BEDINGUNGEN = 'Bedingungen der Übernahmegutschrift'
|
||||
const GRENZEN_VORGABE = { maxFiles: 3, maxFileBytes: 8388608, maxTotalBytes: 10485760 }
|
||||
|
||||
const TAKEOVER_STATUS = {
|
||||
offen: ['In Prüfung', 'orange'],
|
||||
rueckfrage: ['Rückfrage', 'violett'],
|
||||
genehmigt: ['Genehmigt', 'gruen'],
|
||||
abgelehnt: ['Nicht genehmigt', 'grau'],
|
||||
}
|
||||
|
||||
// Bei Textänderungen config.takeover.conditionsVersion auf dem Server hochziehen
|
||||
const TAKEOVER_CONDITIONS = [
|
||||
'Die Übernahmegutschrift gibt es für Leistungen, die Ihr Abo ersetzt (Domain, Hosting bzw. Betrieb der Website, E-Mail-Postfächer) und die Sie bei Ihrem bisherigen Anbieter bereits im Voraus bezahlt haben.',
|
||||
'Wir schreiben den Anteil der Rechnung gut, der auf die Zeit ab Leistungsbeginn Ihres Abos bis zum Ende des bezahlten Zeitraums entfällt, tagesgenau berechnet.',
|
||||
'Nachweis ist die Rechnung des bisherigen Anbieters, ausgestellt auf Sie, mit Leistung, Zeitraum und Betrag. Wir können weitere Nachweise anfordern, etwa die Kündigungsbestätigung. Erstattet Ihr bisheriger Anbieter Beträge für diesen Zeitraum, teilen Sie uns das bitte mit.',
|
||||
'Die Gutschrift wird mit Ihren nächsten Monatsgebühren verrechnet, auch nach einem Wechsel in ein anderes Abo. Sie wird nicht ausgezahlt und ist nicht übertragbar; endet Ihr Abo ohne einen solchen Wechsel, entfällt ein noch nicht verrechneter Rest.',
|
||||
'Die Übernahmegutschrift ist eine freiwillige Leistung von WEBklar. Über Ihren Antrag entscheiden wir nach Prüfung und antworten Ihnen per E-Mail.',
|
||||
]
|
||||
|
||||
const TAKEOVER_MONTHS = [[1, '1 Monat'], [3, '3 Monate'], [6, '6 Monate'], [12, '12 Monate (Jahresrechnung)'], [24, '24 Monate'], [36, '36 Monate']]
|
||||
|
||||
/** Datei als Base64 lesen (ohne den data:-Anfang). Tests ersetzen IO.lies, im Test gibt es keinen FileReader. */
|
||||
function leseAlsBase64(datei) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const leser = new FileReader()
|
||||
leser.onload = () => resolve(String(leser.result).split(',')[1] || '')
|
||||
leser.onerror = () => reject(new Error(`„${datei.name}“ konnte nicht gelesen werden.`))
|
||||
leser.readAsDataURL(datei)
|
||||
})
|
||||
}
|
||||
export const IO = { lies: leseAlsBase64 }
|
||||
|
||||
/** Dateien prüfen und lesen: → [{ name, data }]; wirft die Texte der Prüfungen aus app.js. */
|
||||
export async function sammleDateien(dateien, grenzen, { erforderlich }, lies = IO.lies) {
|
||||
const files = [...(dateien || [])]
|
||||
const mb = (bytes) => Math.round(bytes / 1024 / 1024)
|
||||
if (erforderlich && !files.length) throw new Error('Bitte lade die Rechnung deines bisherigen Anbieters hoch.')
|
||||
if (files.length > grenzen.maxFiles) throw new Error(`Bitte höchstens ${grenzen.maxFiles} Dateien auf einmal hochladen.`)
|
||||
const zuGross = files.find((f) => f.size > grenzen.maxFileBytes)
|
||||
if (zuGross) throw new Error(`„${zuGross.name}“ ist größer als ${mb(grenzen.maxFileBytes)} MB.`)
|
||||
if (files.reduce((s, f) => s + f.size, 0) > grenzen.maxTotalBytes) throw new Error(`Die Dateien sind zusammen größer als ${mb(grenzen.maxTotalBytes)} MB.`)
|
||||
return Promise.all(files.map(async (f) => ({ name: f.name, data: await lies(f) })))
|
||||
}
|
||||
|
||||
const feld = (label, steuerung, klein) => h('div', { class: 'kn-tk-feld' }, h('label', { for: steuerung.getAttribute('id') }, label, klein ? [' ', h('small', null, klein)] : null), steuerung)
|
||||
const leistung = (wert, text) => h('label', null, h('input', { type: 'checkbox', name: 'services', value: wert }), ` ${text}`)
|
||||
|
||||
function antragsFormular(grenzen, email, T) {
|
||||
const maxMb = Math.round(grenzen.maxTotalBytes / 1024 / 1024)
|
||||
return h('form', { id: 'tk-form', class: 'kn-tk-form kn-bestellen', novalidate: true, hidden: true },
|
||||
h('fieldset', { class: 'kn-tk-feld' }, h('legend', null, 'Welche Leistung hast du bisher woanders bezahlt?'),
|
||||
h('div', { class: 'kn-tk-leistungen' }, leistung('domain', 'Domain'), leistung('hosting', 'Hosting / Website-Betrieb'), leistung('email', 'E-Mail-Postfächer'), leistung('sonstiges', 'Sonstiges'))),
|
||||
h('div', { class: 'kn-tk-zeile' },
|
||||
feld('Bisheriger Anbieter', h('input', { id: 'tk-provider', class: K.input, maxlength: 200, placeholder: 'z. B. IONOS, STRATO' })),
|
||||
feld('Domain', h('input', { id: 'tk-domain', class: K.input, maxlength: 255, placeholder: 'meine-firma.de', autocapitalize: 'off', spellcheck: 'false' }), '(falls vorhanden)')),
|
||||
h('div', { class: 'kn-tk-zeile' },
|
||||
feld('Rechnungsbetrag', h('input', { id: 'tk-amount', class: K.input, type: 'number', min: '0.01', step: '0.01', inputmode: 'decimal', placeholder: '119,88' }), '(brutto, in €)'),
|
||||
feld('Die Rechnung gilt für', h('select', { id: 'tk-months', class: K.select }, h('option', { value: '' }, 'bitte wählen'), TAKEOVER_MONTHS.map(([v, l]) => h('option', { value: String(v) }, l)))),
|
||||
feld('Bezahlt bis', h('input', { id: 'tk-paid-until', class: K.input, type: 'date' }), '(letzter Tag)')),
|
||||
feld('Grund / Nachricht an uns', h('textarea', { id: 'tk-message', class: K.textarea, maxlength: 4000, placeholder: 'z. B.: Domain und Postfach liegen noch bis Ende März bei IONOS, der Vertrag dort ist gekündigt.' })),
|
||||
feld('Rechnungsnachweis', h('input', { id: 'tk-files', type: 'file', accept: 'application/pdf,image/jpeg,image/png', multiple: true }), `(PDF, JPG oder PNG · bis zu ${grenzen.maxFiles} Dateien · zusammen max. ${maxMb} MB)`),
|
||||
h('p', { class: 'kn-hinweis' }, 'Tipp: Lade auch die Kündigungsbestätigung deines bisherigen Anbieters hoch – daraus sehen wir, wann der Vertrag dort endet.'),
|
||||
feld('E-Mail-Adresse für unsere Antwort', h('input', { id: 'tk-email', class: K.input, type: 'email', maxlength: 255, autocomplete: 'email', value: email })),
|
||||
baueLinkzeile(T.gutschrift.satz, { id: 'tk-satz', neuerTab: true }),
|
||||
h('p', { class: K.fehler, id: 'tk-error', role: 'alert', hidden: true }),
|
||||
h('div', { class: 'kn-aktionen' },
|
||||
h('button', { type: 'submit', class: K.primaer, id: 'tk-submit' }, T.gutschrift.knopf || 'Antrag absenden'),
|
||||
h('button', { type: 'button', class: K.knopf, id: 'tk-cancel' }, 'Abbrechen')))
|
||||
}
|
||||
|
||||
function antrag(r, viewAs) {
|
||||
const fakten = [
|
||||
`eingereicht am ${fmtDatum(r.createdAt)}`,
|
||||
`bisher bei ${r.provider}${r.domain ? ` (${r.domain})` : ''}`,
|
||||
`${fmtEuro(r.invoiceAmountCents / 100)} für ${r.billingMonths === 1 ? '1 Monat' : `${r.billingMonths} Monate`}, bezahlt bis ${fmtDatum(r.paidUntil)}`,
|
||||
]
|
||||
const [pilleText, ton] = TAKEOVER_STATUS[r.status] || [r.status, 'grau']
|
||||
let koerper
|
||||
if (r.status === 'genehmigt') {
|
||||
koerper = [h('p', null, 'Gutschrift: ', h('strong', null, fmtEuro(r.amountCents / 100)), ' – wird mit deinen nächsten Monatsgebühren verrechnet.'),
|
||||
r.decisionNote ? h('p', { class: 'kn-hinweis kn-umbruch' }, r.decisionNote) : null]
|
||||
} else if (r.status === 'abgelehnt') {
|
||||
koerper = [h('p', { class: 'kn-hinweis kn-umbruch' }, r.decisionNote)]
|
||||
} else if (r.status === 'rueckfrage') {
|
||||
koerper = [
|
||||
h('div', { class: 'kn-tk-frage' }, h('strong', null, 'Unsere Rückfrage:'), h('br'), r.question),
|
||||
h('form', { class: 'kn-tk-antwort', 'data-reply-for': r.id, novalidate: true },
|
||||
h('textarea', { name: 'message', class: K.textarea, maxlength: 4000, placeholder: 'Deine Antwort…' }),
|
||||
h('input', { type: 'file', name: 'files', accept: 'application/pdf,image/jpeg,image/png', multiple: true, 'aria-label': 'Weitere Nachweise' }),
|
||||
h('p', { class: K.fehler, role: 'alert', hidden: true }),
|
||||
h('div', { class: 'kn-aktionen' }, h('button', { type: 'submit', class: K.primaer, disabled: viewAs }, 'Antwort senden')))]
|
||||
} else {
|
||||
koerper = [h('p', { class: 'kn-hinweis' }, 'Wir prüfen deine Angaben und antworten dir per E-Mail.')]
|
||||
}
|
||||
return h('div', { class: 'kn-tk-antrag' },
|
||||
h('div', { class: 'kn-tk-kopf' }, h('strong', null, `Antrag ${r.ref} · ${r.services}`), h('span', { class: K.pille[ton] }, pilleText)),
|
||||
h('div', { class: 'kn-hinweis' }, fakten.join(' · ')),
|
||||
...koerper)
|
||||
}
|
||||
|
||||
/** Seite Übernahmegutschrift. Lädt Abo und Anträge selbst; nach Absenden oder Antworten lädt sie neu und meldet den Zähler. */
|
||||
export async function zeigeGutschrift(wurzel, { route, me, nav } = {}) {
|
||||
const viewAs = me?.viewAs === true
|
||||
const email = me?.customer?.email || ''
|
||||
const nr = (laeufe.get(wurzel) ?? 0) + 1
|
||||
laeufe.set(wurzel, nr)
|
||||
const aktuell = () => laeufe.get(wurzel) === nr
|
||||
// Die Bedingungen sind eine eigene Ansicht (#gutschrift?bedingungen=1): fünf Sätze, kein Abruf, kein Formular
|
||||
if (new URLSearchParams(String(route?.query ?? '')).get('bedingungen') === '1') {
|
||||
meldeTitel(wurzel, TITEL_BEDINGUNGEN)
|
||||
leeren(wurzel).append(h('div', { class: 'kn-panel kn-gutschrift-bedingungen', 'data-karte': 'gutschrift-bedingungen' },
|
||||
h('ol', { class: 'kn-tk-bedingungen' }, TAKEOVER_CONDITIONS.map((c) => h('li', null, c))),
|
||||
h('p', { class: 'kn-links' }, h('a', { ...zuAnsicht('gutschrift'), href: '#gutschrift' }, 'Zurück zum Antrag'))))
|
||||
return
|
||||
}
|
||||
leeren(wurzel).append(h('p', { class: K.leer }, 'Wird geladen …'))
|
||||
await lade('')
|
||||
|
||||
async function lade(notice, { zaehler = false } = {}) {
|
||||
const abo = await ladeAbo()
|
||||
if (!aktuell()) return
|
||||
if (!abo) { leeren(wurzel).append(h('p', { class: K.fehler }, ABO_NICHT_GELADEN)); return }
|
||||
const aktiv = Boolean(abo.subscription?.active)
|
||||
let daten = { requests: [], limits: null }
|
||||
if (abo.subscription) {
|
||||
try {
|
||||
daten = await api('/api/abo/takeover-credits')
|
||||
} catch (f) {
|
||||
if (!aktuell()) return
|
||||
leeren(wurzel).append(h('div', { 'data-karte': 'gutschrift' }, h('div', { class: 'kn-panel kn-leer' }, `Übernahmegutschrift konnte nicht geladen werden: ${f.message}`)))
|
||||
return
|
||||
}
|
||||
if (!aktuell()) return
|
||||
}
|
||||
if (zaehler) meldeZaehlerNeu('vertraege')
|
||||
const anfragen = Array.isArray(daten.requests) ? daten.requests : []
|
||||
const grenzen = daten.limits || GRENZEN_VORGABE
|
||||
if (!aktiv && !anfragen.length) {
|
||||
leeren(wurzel).append(h('div', { 'data-karte': 'gutschrift' }, h('div', { class: 'kn-panel' },
|
||||
h('p', { class: 'kn-hinweis' }, 'Die Übernahmegutschrift kannst du beantragen, sobald dein Abo läuft. ', h('a', zuAnsicht('vertraege'), 'Zum Abo')))))
|
||||
return
|
||||
}
|
||||
// Ohne Wortlaut vom Server (der Satz über dem Knopf) gibt es kein Formular und keinen Knopf
|
||||
const wortlaut = hatWortlaut(abo, 'gutschrift')
|
||||
const formular = aktiv && wortlaut ? antragsFormular(grenzen, email, abo.bestelltexte) : null
|
||||
const start = aktiv && wortlaut ? h('button', { type: 'button', class: K.knopf, id: 'tk-start', disabled: viewAs }, 'Gutschrift beantragen') : null
|
||||
ersetze(wurzel, h('div', { 'data-karte': 'gutschrift' }, h('div', { class: 'kn-panel' },
|
||||
h('p', { class: 'kn-hinweis kn-tk-einleitung' }, 'Hast du Domain, Hosting oder E-Mail bei deinem bisherigen Anbieter schon im Voraus bezahlt? Den Anteil, den du ab Beginn deines Abos doppelt zahlst, schreiben wir dir gut und verrechnen ihn mit deinen nächsten Monatsgebühren. Schick uns dazu einfach die Rechnung.'),
|
||||
notice ? h('div', { class: 'kn-meldung kn-meldung-ok', role: 'status' }, notice) : null,
|
||||
aktiv && wortlaut ? h('div', { class: 'kn-aktionen' }, start, viewAs ? h('span', { class: K.meldung }, TEXT_ANSICHTSMODUS) : null) : null,
|
||||
aktiv && !wortlaut ? h('p', { class: 'kn-hinweis' }, TEXT_NICHT_VERFUEGBAR) : null,
|
||||
formular,
|
||||
anfragen.length ? h('div', { class: 'kn-tk-liste' }, anfragen.map((r) => antrag(r, viewAs))) : null)))
|
||||
|
||||
const neuLaden = (text) => lade(text, { zaehler: true })
|
||||
start?.addEventListener('click', () => {
|
||||
formular.hidden = false
|
||||
start.hidden = true
|
||||
formular.scrollIntoView?.({ behavior: 'smooth', block: 'nearest' })
|
||||
})
|
||||
formular?.querySelector('#tk-cancel')?.addEventListener('click', () => { formular.hidden = true; if (start) start.hidden = false })
|
||||
formular?.addEventListener('submit', (ev) => { ev.preventDefault(); sendeAntrag(formular, grenzen, neuLaden, abo.termsVersion) })
|
||||
wurzel.querySelectorAll('form[data-reply-for]').forEach((f) => f.addEventListener('submit', (ev) => { ev.preventDefault(); sendeAntwort(f, grenzen, neuLaden, viewAs) }))
|
||||
}
|
||||
}
|
||||
|
||||
function zeigeFehler(el, text) {
|
||||
if (!el) return
|
||||
el.textContent = text
|
||||
el.hidden = false
|
||||
}
|
||||
|
||||
async function sendeAntrag(form, grenzen, neuLaden, termsVersion) {
|
||||
const fehler = form.querySelector('#tk-error')
|
||||
const knopf = form.querySelector('#tk-submit')
|
||||
fehler.hidden = true
|
||||
const wert = (id) => form.querySelector(id).value.trim()
|
||||
const payload = {
|
||||
services: [...form.querySelectorAll('input[name="services"]:checked')].map((i) => i.value),
|
||||
provider: wert('#tk-provider'),
|
||||
domain: wert('#tk-domain'),
|
||||
invoiceAmount: wert('#tk-amount'),
|
||||
billingMonths: Number(wert('#tk-months')),
|
||||
paidUntil: wert('#tk-paid-until'),
|
||||
message: wert('#tk-message'),
|
||||
email: wert('#tk-email'),
|
||||
// Der Klick auf den Knopf ist die Zustimmung zu Satz und Bedingungen; die Kundenart gibt es hier nicht, der Server kennt sie
|
||||
consent: true,
|
||||
termsVersion,
|
||||
}
|
||||
try {
|
||||
if (!payload.services.length) throw new Error('Bitte wähle, welche Leistung du bisher woanders bezahlt hast.')
|
||||
if (!payload.provider) throw new Error('Bitte nenne deinen bisherigen Anbieter.')
|
||||
if (!payload.invoiceAmount || !payload.billingMonths || !payload.paidUntil) throw new Error('Bitte gib Rechnungsbetrag, Zeitraum und „Bezahlt bis“ an.')
|
||||
knopf.disabled = true
|
||||
knopf.textContent = 'Wird gesendet…'
|
||||
payload.files = await sammleDateien(form.querySelector('#tk-files').files, grenzen, { erforderlich: true })
|
||||
await api('/api/abo/takeover-credits', { method: 'POST', body: payload })
|
||||
await neuLaden(`Vielen Dank! Dein Antrag ist eingegangen. Die Eingangsbestätigung haben wir an ${payload.email} geschickt; unsere Antwort kommt ebenfalls per E-Mail.`)
|
||||
} catch (f) {
|
||||
zeigeFehler(fehler, f.message)
|
||||
knopf.disabled = false
|
||||
knopf.textContent = 'Antrag absenden'
|
||||
}
|
||||
}
|
||||
|
||||
async function sendeAntwort(form, grenzen, neuLaden, viewAs) {
|
||||
if (viewAs) return
|
||||
const fehler = form.querySelector('p')
|
||||
const knopf = form.querySelector('button[type="submit"]')
|
||||
fehler.hidden = true
|
||||
knopf.disabled = true
|
||||
try {
|
||||
const message = form.querySelector('textarea').value.trim()
|
||||
const files = await sammleDateien(form.querySelector('input[type="file"]').files, grenzen, { erforderlich: false })
|
||||
if (!message && !files.length) throw new Error('Bitte schreib eine Antwort oder lade einen Nachweis hoch.')
|
||||
await api(`/api/abo/takeover-credits/${encodeURIComponent(form.getAttribute('data-reply-for'))}/reply`, { method: 'POST', body: { message, files } })
|
||||
await neuLaden('Danke für deine Antwort! Wir prüfen deinen Antrag weiter und melden uns per E-Mail.')
|
||||
} catch (f) {
|
||||
zeigeFehler(fehler, f.message)
|
||||
knopf.disabled = false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
/* Kundenansicht neu (P2): gemeinsame Hilfen der Ansichten. Formate, Bausteinklassen, Navigation, Ereignisse und kleine Merker.
|
||||
Kein DOM-Zugriff beim Import; document und window werden erst beim Aufruf gelesen. */
|
||||
|
||||
import { api } from './api.js'
|
||||
import { geheZu, aktuelleRoute } from './router.js'
|
||||
import { hashFuerAnsicht } from './routen.js'
|
||||
import { safeUrl } from '../fn-util.js'
|
||||
|
||||
/** Klassen der Bausteine aus basis.css. */
|
||||
export const K = Object.freeze({
|
||||
knopf: 'kn-knopf',
|
||||
primaer: 'kn-knopf-primaer',
|
||||
zweit: 'kn-knopf-zweit',
|
||||
textknopf: 'kn-textknopf',
|
||||
feld: 'kn-feld',
|
||||
input: 'kn-input',
|
||||
select: 'kn-select',
|
||||
textarea: 'kn-textarea',
|
||||
check: 'kn-check',
|
||||
liste: 'kn-liste',
|
||||
zeile: 'kn-zeile',
|
||||
abschnitt: 'kn-abschnitt',
|
||||
meldung: 'kn-meldung',
|
||||
fehler: 'kn-fehler',
|
||||
leer: 'kn-leer',
|
||||
versteckt: 'hidden',
|
||||
pille: Object.freeze({
|
||||
grau: 'kn-pille kn-pille-grau',
|
||||
blau: 'kn-pille kn-pille-blau',
|
||||
orange: 'kn-pille kn-pille-orange',
|
||||
violett: 'kn-pille kn-pille-violett',
|
||||
gruen: 'kn-pille kn-pille-gruen',
|
||||
rot: 'kn-pille kn-pille-rot',
|
||||
}),
|
||||
})
|
||||
|
||||
export const ABO_NICHT_GELADEN = 'Deine Verträge konnten gerade nicht geladen werden. Bitte versuche es später noch einmal.'
|
||||
|
||||
/** Inhalt von el ersetzen; null, undefined und false (leere Platzhalter) werden übersprungen, nie als Text "null" angehängt. */
|
||||
export function ersetze(el, ...kinder) {
|
||||
el.replaceChildren(...kinder.flat(Infinity).filter((k) => k !== null && k !== undefined && k !== false))
|
||||
return el
|
||||
}
|
||||
|
||||
/* ---------- Formate ---------- */
|
||||
|
||||
const ZAHL_2 = new Intl.NumberFormat('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
const ZAHL_1 = new Intl.NumberFormat('de-DE', { minimumFractionDigits: 1, maximumFractionDigits: 1 })
|
||||
const ZAHL_STD = new Intl.NumberFormat('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: 2 })
|
||||
|
||||
/** Teile eines Zeitpunkts in Berliner Zeit; ungültige Werte ergeben null. */
|
||||
function berlinTeile(wert, extra = {}) {
|
||||
const d = wert instanceof Date ? wert : new Date(wert)
|
||||
if (!wert || Number.isNaN(d.getTime())) return null
|
||||
const teile = new Intl.DateTimeFormat('de-DE', { timeZone: 'Europe/Berlin', hourCycle: 'h23', ...extra }).formatToParts(d)
|
||||
return Object.fromEntries(teile.map((t) => [t.type, t.value]))
|
||||
}
|
||||
|
||||
/** 50 → '50,00 €' (geschütztes Leerzeichen vor dem Euro-Zeichen). */
|
||||
export const fmtEuro = (euro) => `${ZAHL_2.format(Number(euro) || 0)} €`
|
||||
|
||||
/** 'TT.MM.JJJJ' in Berliner Zeit; ein Wert, der schon so aussieht, bleibt; leer oder ungültig '–'. */
|
||||
export function fmtDatum(wert) {
|
||||
if (typeof wert === 'string' && /^\d{2}\.\d{2}\.\d{4}$/.test(wert)) return wert
|
||||
const t = berlinTeile(wert, { day: '2-digit', month: '2-digit', year: 'numeric' })
|
||||
return t ? `${t.day}.${t.month}.${t.year}` : '–'
|
||||
}
|
||||
|
||||
/** 'TT.MM.JJ, HH:MM' in Berliner Zeit. */
|
||||
export function fmtDatumZeit(wert) {
|
||||
const t = berlinTeile(wert, { day: '2-digit', month: '2-digit', year: '2-digit', hour: '2-digit', minute: '2-digit' })
|
||||
return t ? `${t.day}.${t.month}.${t.year}, ${t.hour}:${t.minute}` : '–'
|
||||
}
|
||||
|
||||
/** 'TT.MM.' in Berliner Zeit; ungültig ''. */
|
||||
export function fmtTagMonat(wert) {
|
||||
const t = berlinTeile(wert, { day: '2-digit', month: '2-digit' })
|
||||
return t ? `${t.day}.${t.month}.` : ''
|
||||
}
|
||||
|
||||
/** 'HH:MM' in Berliner Zeit; ungültig ''. */
|
||||
export function fmtUhrzeit(wert) {
|
||||
const t = berlinTeile(wert, { hour: '2-digit', minute: '2-digit' })
|
||||
return t ? `${t.hour}:${t.minute}` : ''
|
||||
}
|
||||
|
||||
/** Kalendertag in Berliner Zeit als { j, m, t } (Zahlen); ungültig null. */
|
||||
export function berlinTag(wert) {
|
||||
const t = berlinTeile(wert, { day: '2-digit', month: '2-digit', year: 'numeric' })
|
||||
return t ? { j: Number(t.year), m: Number(t.month), t: Number(t.day) } : null
|
||||
}
|
||||
|
||||
/** Minuten → '2,5 Std.' | '2 Std.' | '0,75 Std.' (Komma, höchstens zwei Stellen). */
|
||||
export const fmtStd = (minuten) => `${ZAHL_STD.format(Math.round(((Number(minuten) || 0) / 60) * 100) / 100)} Std.`
|
||||
|
||||
/** Minuten → '' | '45 min' | '2 h' | '2 h 30 min'. */
|
||||
export function fmtMinuten(minuten) {
|
||||
const m = Math.round(Number(minuten) || 0)
|
||||
if (m <= 0) return ''
|
||||
const h = Math.floor(m / 60)
|
||||
const rest = m % 60
|
||||
if (!h) return `${rest} min`
|
||||
return rest ? `${h} h ${rest} min` : `${h} h`
|
||||
}
|
||||
|
||||
/** Bytes → '12 B' | '1,5 KB' | '5,0 MB'. */
|
||||
export function fmtBytes(bytes) {
|
||||
const b = Number(bytes) || 0
|
||||
if (b < 1024) return `${Math.round(b)} B`
|
||||
if (b < 1024 * 1024) return `${ZAHL_1.format(b / 1024)} KB`
|
||||
return `${ZAHL_1.format(b / (1024 * 1024))} MB`
|
||||
}
|
||||
|
||||
/* ---------- Alt-Module, Navigation, Ereignisse ---------- */
|
||||
|
||||
/** Adapter für Alt-Module (auftrag-portal.js), die den Body als JSON-String schicken: das neue api() kodiert Objekte selbst. */
|
||||
export function legacyRequest(apiFn) {
|
||||
return (pfad, opts = {}) => {
|
||||
const { body } = opts
|
||||
if (typeof body !== 'string') return apiFn(pfad, { ...opts })
|
||||
let objekt = body
|
||||
try { objekt = JSON.parse(body) } catch { /* kein JSON: unverändert weitergeben */ }
|
||||
return apiFn(pfad, { ...opts, body: objekt })
|
||||
}
|
||||
}
|
||||
|
||||
/** Programmatisch navigieren (nur über den Router). Läuft kein Router oder wirft er, trägt der Hash die Adresse. */
|
||||
export function navigiere(hash, { ersetzen = false } = {}) {
|
||||
try {
|
||||
if (aktuelleRoute() === null) { window.location.hash = hash; return }
|
||||
geheZu(hash, { ersetzen })
|
||||
} catch {
|
||||
window.location.hash = hash
|
||||
}
|
||||
}
|
||||
|
||||
/** Verknüpfung zu einer Ansicht für die Brücke der Hülle: href plus data-view (und data-project). */
|
||||
export function zuAnsicht(name, projektId = '') {
|
||||
const basis = hashFuerAnsicht(name) ?? '#'
|
||||
if (!projektId) return { href: basis, 'data-view': name }
|
||||
return { href: `${basis}${basis.includes('?') ? '&' : '?'}projekt=${encodeURIComponent(projektId)}`, 'data-view': name, 'data-project': projektId }
|
||||
}
|
||||
|
||||
/** Titel der Etage melden (die Hülle zeigt ihn im Kopf). */
|
||||
export function meldeTitel(el, text) {
|
||||
el.dispatchEvent(new CustomEvent('kn:titel', { bubbles: true, detail: { text } }))
|
||||
}
|
||||
|
||||
/**
|
||||
* Zurück aus dem Bfcache (E-Fix M6): kommt der Kunde von der Zahlungsseite von Stripe per Wischgeste zurück, stellt Safari die Seite so wieder her,
|
||||
* wie sie war (Knopf gesperrt, "Weiter zur Zahlung…"). fn läuft bei pageshow mit persisted, solange `el` im Dokument steht; ist es ersetzt worden,
|
||||
* entfernt sich der Hörer. Ohne Fenster tut es nichts.
|
||||
*/
|
||||
export function beiRueckkehr(el, fn) {
|
||||
const win = globalThis.window
|
||||
if (!win || typeof win.addEventListener !== 'function') return
|
||||
const hoerer = (ev) => {
|
||||
if (!el.isConnected) { win.removeEventListener('pageshow', hoerer); return }
|
||||
if (ev.persisted) fn()
|
||||
}
|
||||
win.addEventListener('pageshow', hoerer)
|
||||
}
|
||||
|
||||
/** Zähler neu laden lassen: was = 'chat' | 'vertraege' | 'alle'. Ohne Dokument tut es nichts. */
|
||||
export function meldeZaehlerNeu(was) {
|
||||
if (typeof document === 'undefined' || typeof document.dispatchEvent !== 'function' || typeof CustomEvent === 'undefined') return
|
||||
document.dispatchEvent(new CustomEvent('kn:zaehler-neu', { bubbles: true, detail: { was } }))
|
||||
}
|
||||
|
||||
/* ---------- Merker zwischen Ansichten (einmalig) ---------- */
|
||||
|
||||
let hinweis = ''
|
||||
export function merkeHinweis(text) { hinweis = String(text ?? '') }
|
||||
export function nimmHinweis() { const t = hinweis; hinweis = ''; return t }
|
||||
|
||||
let chatVorgabe = ''
|
||||
export function setzeChatVorgabe(text) { chatVorgabe = String(text ?? '') }
|
||||
export function nimmChatVorgabe() { const t = chatVorgabe; chatVorgabe = ''; return t }
|
||||
|
||||
/* ---------- Abo ---------- */
|
||||
|
||||
/** GET /api/abo → Objekt oder null (jeder Fehler). */
|
||||
export async function ladeAbo() {
|
||||
try {
|
||||
const antwort = await api('/api/abo')
|
||||
return antwort && typeof antwort === 'object' ? antwort : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/** Gemerkte Funktion eines Wechsels (V11): nur wenn key und projectId nichtleere Texte sind. */
|
||||
export function wechselFunktionAus(sub) {
|
||||
const f = sub?.wechselFunktion
|
||||
if (!f || typeof f.key !== 'string' || !f.key || typeof f.projectId !== 'string' || !f.projectId) return null
|
||||
return { key: f.key, projectId: f.projectId }
|
||||
}
|
||||
|
||||
/** Adressen aus Daten (previewUrl, portalUrl, paymentUrl): http(s) wie safeUrl, dazu Pfade auf derselben Herkunft ("/vorschau-hinweis.html").
|
||||
* Nicht zugelassen: javascript:, data:, "//host" und "/\\host" (Browser lesen den Rückstrich wie einen Schrägstrich). Sonst ''. */
|
||||
export function sichereAdresse(url) {
|
||||
const text = typeof url === 'string' ? url : ''
|
||||
return safeUrl(text) || (/^\/(?![/\\])/.test(text) ? text : '')
|
||||
}
|
||||
|
||||
export { safeUrl }
|
||||
@@ -0,0 +1,111 @@
|
||||
/* Kundenansicht neu (P2): Passwort ändern (Etage #passwort, aus dem Konto-Menü). Dieselben Regeln prüft der Server
|
||||
(server/services/customerPassword.js); danach sind andere Geräte abgemeldet. Regeln und Texte aus app.js (passwordProblem, initPasswordView). */
|
||||
|
||||
import { api } from './api.js'
|
||||
import { h, leeren } from './dom.js'
|
||||
import { K, zuAnsicht, navigiere } from './ansicht-hilfen.js'
|
||||
|
||||
const TEXT_ANSICHTSMODUS = 'Im Ansichtsmodus kann das Passwort nicht geändert werden.'
|
||||
const laeufe = new WeakMap() // Wurzel → Nummer des jüngsten Aufrufs
|
||||
let beobachter = null // räumt die Felder, wenn die Seite verlassen wird
|
||||
|
||||
/** → [feldId, text] oder null. Reihenfolge und Texte aus app.js passwordProblem. */
|
||||
export function passwortProblem(aktuell, neu, wiederholung, email) {
|
||||
if (!aktuell) return ['pw-current', 'Bitte gib dein aktuelles Passwort ein.']
|
||||
if (!neu) return ['pw-new', 'Bitte gib ein neues Passwort ein.']
|
||||
if ([...neu].length < 8) return ['pw-new', 'Das neue Passwort muss mindestens 8 Zeichen haben.']
|
||||
if (new TextEncoder().encode(neu).length > 256) return ['pw-new', 'Das neue Passwort ist zu lang (höchstens 256 Zeichen).']
|
||||
if (!neu.trim()) return ['pw-new', 'Das neue Passwort darf nicht nur aus Leerzeichen bestehen.']
|
||||
if (neu === aktuell) return ['pw-new', 'Das neue Passwort muss sich vom aktuellen unterscheiden.']
|
||||
if (email && neu.trim().toLowerCase() === email.trim().toLowerCase()) return ['pw-new', 'Bitte nimm nicht deine E-Mail-Adresse als Passwort.']
|
||||
if (neu !== wiederholung) return ['pw-repeat', 'Die beiden neuen Passwörter stimmen nicht überein.']
|
||||
return null
|
||||
}
|
||||
|
||||
export async function zeigePasswort(wurzel, { route, me, nav } = {}) {
|
||||
const nr = (laeufe.get(wurzel) ?? 0) + 1
|
||||
laeufe.set(wurzel, nr)
|
||||
beobachter?.disconnect?.()
|
||||
beobachter = null
|
||||
const viewAs = me?.viewAs === true
|
||||
const email = me?.customer?.email || ''
|
||||
|
||||
const benutzername = h('input', { type: 'text', id: 'pw-username', name: 'username', autocomplete: 'username', hidden: true, value: email })
|
||||
const aktuell = h('input', { type: 'password', id: 'pw-current', class: K.input, name: 'current-password', autocomplete: 'current-password', required: true, enterkeyhint: 'next' })
|
||||
const neu = h('input', { type: 'password', id: 'pw-new', class: K.input, name: 'new-password', autocomplete: 'new-password', minlength: 8, required: true, 'aria-describedby': 'pw-rule', enterkeyhint: 'next' })
|
||||
const wdh = h('input', { type: 'password', id: 'pw-repeat', class: K.input, name: 'new-password-repeat', autocomplete: 'new-password', required: true, enterkeyhint: 'done' })
|
||||
const zeigen = h('input', { type: 'checkbox', id: 'pw-show' })
|
||||
const fehler = h('p', { class: K.fehler, id: 'pw-error', role: 'alert', hidden: true })
|
||||
const senden = h('button', { type: 'submit', class: K.primaer, id: 'pw-submit' }, 'Passwort ändern')
|
||||
const felder = [aktuell, neu, wdh]
|
||||
const feldPaar = (label, steuerung, ...nach) => h('div', { class: K.feld }, h('label', { for: steuerung.getAttribute('id') }, label), steuerung, ...nach)
|
||||
|
||||
const form = h('form', { id: 'pw-form', class: 'kn-pw-form', novalidate: true },
|
||||
benutzername,
|
||||
feldPaar('Aktuelles Passwort', aktuell),
|
||||
feldPaar('Neues Passwort', neu, h('small', { class: 'kn-feld-hinweis', id: 'pw-rule' }, 'Mindestens 8 Zeichen. Am sichersten ist ein langer Satz, den nur du kennst.')),
|
||||
feldPaar('Neues Passwort wiederholen', wdh),
|
||||
h('label', { class: 'kn-check' }, zeigen, ' Passwörter anzeigen'),
|
||||
fehler,
|
||||
h('div', { class: 'kn-aktionen' }, senden, viewAs ? h('span', { class: K.meldung }, TEXT_ANSICHTSMODUS) : null),
|
||||
h('p', { class: 'kn-hinweis' }, 'Danach bist du auf deinen anderen Geräten abgemeldet. Aktuelles Passwort vergessen? ', h('a', zuAnsicht('chat'), 'Schreib uns im Chat'), ' – wir setzen es zurück.'))
|
||||
|
||||
const fertig = h('button', { type: 'button', class: K.knopf, id: 'pw-done-btn' }, 'Fertig')
|
||||
const erfolg = h('div', { class: 'kn-pw-erfolg', id: 'pw-done', role: 'status', tabindex: '-1', hidden: true },
|
||||
h('strong', null, 'Dein Passwort wurde geändert.'),
|
||||
h('p', null, 'Melde dich ab jetzt mit dem neuen Passwort an. Auf deinen anderen Geräten bist du abgemeldet.'),
|
||||
fertig)
|
||||
|
||||
const leereFelder = () => {
|
||||
for (const f of felder) { f.value = ''; f.type = 'password' }
|
||||
zeigen.checked = false
|
||||
fehler.hidden = true
|
||||
}
|
||||
|
||||
zeigen.addEventListener('change', () => { for (const f of felder) f.type = zeigen.checked ? 'text' : 'password' })
|
||||
fertig.addEventListener('click', () => {
|
||||
if (nav === 'push') window.history.back()
|
||||
else navigiere('#')
|
||||
})
|
||||
form.addEventListener('submit', async (ev) => {
|
||||
ev.preventDefault()
|
||||
if (viewAs || senden.disabled) return
|
||||
fehler.hidden = true
|
||||
const problem = passwortProblem(aktuell.value, neu.value, wdh.value, email)
|
||||
if (problem) {
|
||||
fehler.textContent = problem[1]
|
||||
fehler.hidden = false
|
||||
felder.find((f) => f.getAttribute('id') === problem[0])?.focus()
|
||||
return
|
||||
}
|
||||
senden.disabled = true
|
||||
senden.textContent = 'Wird gespeichert…'
|
||||
try {
|
||||
await api('/api/auth/password', { method: 'POST', body: { currentPassword: aktuell.value, newPassword: neu.value } })
|
||||
leereFelder()
|
||||
form.hidden = true
|
||||
erfolg.hidden = false
|
||||
erfolg.focus()
|
||||
} catch (f) {
|
||||
fehler.textContent = f.message
|
||||
fehler.hidden = false
|
||||
if (f.code === 'wrong_password' || f.code === 'current_missing') { aktuell.focus(); aktuell.select?.() } else if (f.code === 'new_invalid') neu.focus()
|
||||
} finally {
|
||||
senden.disabled = false
|
||||
senden.textContent = 'Passwort ändern'
|
||||
}
|
||||
})
|
||||
|
||||
leeren(wurzel).append(h('div', { class: 'kn-panel kn-pw' }, form, erfolg))
|
||||
if (viewAs) for (const e of wurzel.querySelectorAll('input, button')) e.disabled = true
|
||||
|
||||
// Wer die Seite verlässt, findet beim nächsten Mal ein leeres Formular (keine Passwörter im DOM)
|
||||
if (typeof MutationObserver === 'function' && typeof document !== 'undefined' && document.body) {
|
||||
beobachter = new MutationObserver(() => {
|
||||
// nur räumen, wenn noch etwas darin steht: das Räumen ändert selbst `hidden` und darf den Beobachter nicht endlos wecken
|
||||
const benutzt = felder.some((f) => f.value) || !erfolg.hidden || !fehler.hidden || form.hidden
|
||||
if (laeufe.get(wurzel) === nr && benutzt && wurzel.closest('[hidden]')) { leereFelder(); form.hidden = false; erfolg.hidden = true }
|
||||
})
|
||||
beobachter.observe(document.body, { attributes: true, subtree: true, attributeFilter: ['hidden'] })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
/* Kundenansicht neu (P7): Reiter E-Mail in Verträge, der Zusatzspeicher, im schlanken Muster der Spec 5.6, Fassung 6.
|
||||
Plus: Block „Was du bestellst“, eine Linkzeile zu den Regeln, der Knopf „Zahlungspflichtig bestellen“ (§ 312j BGB), darunter die
|
||||
Kundenart-Zeile; kein Häkchen (der Klick auf den Knopf ist die Zustimmung). Minus: genau eine Warnzeile und der Knopf „Speichern“.
|
||||
Die Sätze kommen vom Server (GET /api/abo → bestelltexte). Der Server entscheidet die Kundenart selbst, der Client sendet nie kundenart. */
|
||||
|
||||
import { api } from './api.js'
|
||||
import { h, leeren } from './dom.js'
|
||||
import { K, ersetze, fmtEuro, fmtDatum, fmtTagMonat, ladeAbo, meldeZaehlerNeu, navigiere, setzeChatVorgabe, ABO_NICHT_GELADEN } from './ansicht-hilfen.js'
|
||||
import { baueBlock, baueKundenartZeile, baueLinkzeile, hatWortlaut, kundenartVon } from './bestell-bausteine.js'
|
||||
|
||||
const laeufe = new WeakMap() // Reiter-Inhalt → Nummer des jüngsten Aufrufs
|
||||
const TEXT_ANSICHTSMODUS = 'Im Ansichtsmodus nicht möglich.'
|
||||
const TEXT_NICHT_VERFUEGBAR = 'Die Online-Bestellung wird gerade eingerichtet – schreib uns gern im Chat.'
|
||||
const KNOPF = 'Zahlungspflichtig bestellen'
|
||||
const paket = (n) => `Paket${n === 1 ? '' : 'e'}`
|
||||
|
||||
// Der Zusatzspeicher: Plus/Minus mit Vorschau vom Server, Bestellübersicht (§ 312j BGB) und Speichern
|
||||
function baueSpeicherPanel(daten, { viewAs, kundenart, notice, nachErfolg }) {
|
||||
const sub = daten.subscription
|
||||
const T = daten.bestelltexte
|
||||
const wortlaut = hatWortlaut(daten, 'speicher')
|
||||
const conf = daten.mailStorage || {}
|
||||
const includedGb = Number(conf.includedGb) || 0
|
||||
const gbPer = Number(conf.gbPerUnit) || 0
|
||||
const unitCents = Number(conf.unitPriceCents) || 0
|
||||
const maxUnits = Number(conf.maxUnits) || 10
|
||||
const gbOf = (u) => includedGb + u * gbPer
|
||||
const storage = sub.storage || {}
|
||||
const pendingReduction = storage.pendingReduction || null
|
||||
const canChange = sub.canChangeStorage === true
|
||||
const current = Number(storage.units || 0)
|
||||
const start = pendingReduction && Number.isInteger(pendingReduction.units) ? pendingReduction.units : current
|
||||
const state = { target: start, preview: null, previewError: '', loading: start !== current, busy: false, error: '', token: 0 }
|
||||
const gekuendigt = Boolean(sub.cancelAtPeriodEnd || sub.cancelAt)
|
||||
const lockedReason = sub.switchPending ? 'Wechsel wartet auf die Zahlung'
|
||||
: gekuendigt ? 'Abo gekündigt'
|
||||
: sub.paymentProblem ? 'Zahlung offen' : 'bei Fragen schreib uns gern im Chat'
|
||||
const panel = h('div', { class: 'kn-panel', 'data-panel': 'speicher' })
|
||||
let fehlerText = ''
|
||||
|
||||
const paint = () => {
|
||||
const delta = state.target - current
|
||||
const p = state.preview
|
||||
const sperre = viewAs || state.busy
|
||||
let summary = null
|
||||
if (canChange && delta !== 0) {
|
||||
const plus = delta > 0
|
||||
if (!wortlaut) {
|
||||
// Ohne Wortlaut vom Server (Linkzeile beim Plus, Warnzeile beim Minus) gibt es keine Änderung: nur der Satz, kein Knopf
|
||||
summary = h('div', { class: 'kn-speicher-zusammenfassung' }, h('p', { class: 'kn-hinweis' }, TEXT_NICHT_VERFUEGBAR))
|
||||
} else {
|
||||
const totalCents = state.target * unitCents
|
||||
const zeilen = []
|
||||
const proBad = plus && p && !state.loading && !(p.prorationCents != null && p.prorationCents !== '' && Number.isFinite(Number(p.prorationCents)))
|
||||
zeilen.push({ label: 'Neue Gesamtgröße', text: `${gbOf(state.target)} GB` })
|
||||
zeilen.push({ label: 'Preis', text: `${fmtEuro(totalCents / 100)} pro Monat für ${state.target} ${paket(state.target)} (${plus ? 'Mehrpreis' : 'Ersparnis'} ${fmtEuro(Math.abs(delta) * unitCents / 100)} pro Monat; Gesamtpreis, gemäß § 19 UStG ohne Umsatzsteuer)` })
|
||||
if (state.previewError || proBad) {
|
||||
zeilen.push({ label: 'Vorschau', text: state.previewError || 'Der Betrag konnte nicht berechnet werden. Bitte versuche es später erneut.', klasse: 'kn-fehler' })
|
||||
} else if (!p || state.loading) {
|
||||
zeilen.push({ label: 'Vorschau', text: 'wird berechnet …' })
|
||||
} else if (plus) {
|
||||
const pro = Number(p.prorationCents) || 0
|
||||
zeilen.push({ label: 'Zusätzlich jetzt', text: pro > 0
|
||||
? `${fmtEuro(pro / 100)} anteilig für den Rest des laufenden Abrechnungsmonats (gemäß § 19 UStG ohne Umsatzsteuer)`
|
||||
: 'keine zusätzlichen Kosten, die Pakete sind für den laufenden Abrechnungsmonat schon bezahlt' })
|
||||
zeilen.push({ label: 'Nächste Abbuchung', text: `${fmtDatum(p.nextChargeAt)}, danach monatlich ${fmtEuro(totalCents / 100)}` })
|
||||
zeilen.push(pro === 0 && pendingReduction
|
||||
? { label: 'Geplante Reduzierung', text: 'wird aufgehoben oder angepasst; es ist nichts neu einzurichten' }
|
||||
: { label: 'Einrichtung', text: 'in der Regel innerhalb eines Werktags; bis dahin steht „wird eingerichtet“ da' })
|
||||
} else {
|
||||
zeilen.push({ label: 'Wirkt zum', text: `${fmtDatum(p.effectiveAt)}, zum Ende des Abrechnungsmonats` })
|
||||
}
|
||||
zeilen.push({ label: 'Laufzeit', text: 'monatlich, zum Ende jedes Abrechnungsmonats kündbar, endet spätestens mit dem Abo' })
|
||||
const gesperrt = sperre || state.loading || !p || state.previewError || proBad
|
||||
summary = h('div', { class: 'kn-speicher-zusammenfassung kn-bestellen' },
|
||||
baueBlock(zeilen, { id: 'storage-block' }),
|
||||
plus ? baueLinkzeile(T.speicher.linkzeile, { id: 'storage-linkzeile' }) : baueLinkzeile(T.speicher.warnung, { id: 'storage-warnung', klasse: 'kn-speicher-warnung' }),
|
||||
h('p', { class: 'kn-fehler', id: 'storage-error', role: 'alert', hidden: !fehlerText }, fehlerText),
|
||||
h('div', { class: 'kn-aktionen' },
|
||||
h('button', { type: 'button', class: K.primaer, id: 'storage-submit', disabled: Boolean(gesperrt), on: { click: absenden } }, state.busy ? 'Wird gespeichert …' : plus ? (T.knopf || KNOPF) : 'Speichern')),
|
||||
plus ? baueKundenartZeile(kundenart, T.kundenart, { id: 'storage-kundenart' }) : null)
|
||||
}
|
||||
}
|
||||
const status = []
|
||||
if (storage.pendingSetup) status.push(h('p', { class: 'kn-meldung kn-meldung-warn' }, 'Dein zusätzlicher Speicher wird eingerichtet. Das dauert in der Regel bis zum nächsten Werktag.'))
|
||||
if (pendingReduction) status.push(h('p', { class: 'kn-hinweis' }, `Geplant: ab ${fmtTagMonat(pendingReduction.effectiveAt)} ${gbOf(Number(pendingReduction.units) || 0)} GB.`))
|
||||
ersetze(panel,
|
||||
h('h3', null, 'E-Mail-Speicher'),
|
||||
h('p', { class: 'kn-hinweis' }, `${includedGb} GB inklusive. Jedes Zusatzpaket bringt ${gbPer} GB für ${fmtEuro(unitCents / 100)} im Monat (gemäß § 19 UStG ohne Umsatzsteuer), bis zu ${maxUnits} Pakete.`),
|
||||
notice ? h('p', { class: 'kn-meldung kn-meldung-ok', role: 'status' }, notice) : null,
|
||||
!canChange ? h('p', { class: 'kn-hinweis', id: 'storage-locked' }, `Änderungen sind derzeit nicht möglich – ${lockedReason}.`) : null,
|
||||
canChange
|
||||
? h('div', { class: 'kn-stepper' },
|
||||
h('button', { type: 'button', class: K.knopf, id: 'storage-minus', 'aria-label': 'Ein Paket weniger', disabled: sperre || state.target <= 0, on: { click: () => schritt(-1) } }, '−'),
|
||||
h('span', { class: 'kn-stepper-zahl', 'aria-live': 'polite' }, h('strong', null, state.target), ` ${paket(state.target)}`),
|
||||
h('button', { type: 'button', class: K.knopf, id: 'storage-plus', 'aria-label': 'Ein Paket mehr', disabled: sperre || state.target >= maxUnits, on: { click: () => schritt(1) } }, '+'))
|
||||
: h('p', { class: 'kn-stepper-zahl' }, h('strong', null, state.target), ` ${paket(state.target)}`),
|
||||
h('p', { class: 'kn-speicher-summe' }, `= ${gbOf(state.target)} GB gesamt · + ${fmtEuro(state.target * unitCents / 100)} im Monat`),
|
||||
canChange && viewAs ? h('p', { class: K.meldung }, TEXT_ANSICHTSMODUS) : null,
|
||||
...status,
|
||||
summary)
|
||||
}
|
||||
|
||||
const ladeVorschau = async (token) => {
|
||||
try {
|
||||
state.preview = await api('/api/abo/storage/preview', { method: 'POST', body: { units: state.target } })
|
||||
} catch (f) {
|
||||
if (token !== state.token) return
|
||||
state.previewError = f.message
|
||||
}
|
||||
if (token !== state.token) return
|
||||
state.loading = false
|
||||
paint()
|
||||
}
|
||||
|
||||
const schritt = (d) => {
|
||||
const next = state.target + d
|
||||
if (viewAs || state.busy || next < 0 || next > maxUnits) return
|
||||
state.target = next
|
||||
state.preview = null
|
||||
state.previewError = ''
|
||||
fehlerText = ''
|
||||
state.loading = next !== current
|
||||
const token = ++state.token
|
||||
paint()
|
||||
if (next !== current) ladeVorschau(token)
|
||||
}
|
||||
|
||||
const absenden = async () => {
|
||||
if (viewAs) return
|
||||
const plus = state.target > current
|
||||
fehlerText = ''
|
||||
// Plus: der Klick auf den Knopf ist die Zustimmung. Nie kundenart: die Kundenart bestimmt der Server aus dem Kundendokument.
|
||||
const body = { units: state.target }
|
||||
if (plus) Object.assign(body, { acceptTerms: true, termsVersion: daten.termsVersion })
|
||||
state.busy = true
|
||||
paint()
|
||||
try {
|
||||
const res = await api('/api/abo/storage', { method: 'POST', body })
|
||||
const ch = res.change || {}
|
||||
const hinweis = plus
|
||||
? (ch.task === false ? 'Gespeichert. Es entstehen keine zusätzlichen Kosten, die Pakete waren schon bezahlt.' : 'Danke, deine Bestellung ist eingegangen. Der Speicher wird in der Regel innerhalb eines Werktags eingerichtet.')
|
||||
: `Gespeichert. Die Verkleinerung wirkt zum ${fmtDatum(ch.effectiveAt)}.`
|
||||
await nachErfolg(hinweis, res)
|
||||
// Ein Plus eröffnet für Privatkunden eine neue Widerrufsfrist: die Hülle lädt Streifen und Pille „Frist läuft“ neu (E-Fix M1)
|
||||
if (plus) meldeZaehlerNeu('vertraege')
|
||||
} catch (f) {
|
||||
state.busy = false
|
||||
fehlerText = f.status === 409 ? `${f.message} Bitte lade die Seite neu, falls sich der Stand geändert hat.` : f.message
|
||||
paint()
|
||||
}
|
||||
}
|
||||
|
||||
paint()
|
||||
if (start !== current && canChange) ladeVorschau(++state.token)
|
||||
return panel
|
||||
}
|
||||
|
||||
/** Reiter-Inhalt E-Mail: Knopf "Postfach anfragen" und der Zusatzspeicher. daten (GET /api/abo) ist optional. */
|
||||
export async function zeigeSpeicher(wurzel, { route, me, nav, daten } = {}) {
|
||||
const nr = (laeufe.get(wurzel) ?? 0) + 1
|
||||
laeufe.set(wurzel, nr)
|
||||
const aktuell = () => laeufe.get(wurzel) === nr
|
||||
const viewAs = me?.viewAs === true
|
||||
leeren(wurzel).append(h('p', { class: K.leer }, 'Wird geladen …'))
|
||||
|
||||
let stand = daten ?? await ladeAbo()
|
||||
if (!aktuell()) return
|
||||
if (!stand) { leeren(wurzel).append(h('p', { class: K.fehler }, ABO_NICHT_GELADEN)); return }
|
||||
if (!stand.subscription?.active) {
|
||||
leeren(wurzel).append(h('p', { class: K.leer }, 'Den E-Mail-Speicher gibt es mit einem laufenden Abo.'))
|
||||
return
|
||||
}
|
||||
|
||||
const kundenart = kundenartVon(me, stand)
|
||||
const anfrage = h('button', { type: 'button', class: K.knopf, on: { click: () => { setzeChatVorgabe('Bitte lege ein neues Postfach an: '); navigiere('#chat') } } }, 'Postfach anfragen')
|
||||
const platz = h('div', { class: 'kn-speicher-platz' })
|
||||
const zeichne = (notice = '') => {
|
||||
leeren(platz).append(baueSpeicherPanel(stand, {
|
||||
viewAs,
|
||||
kundenart,
|
||||
notice,
|
||||
// Nach einem Erfolg: Abo neu laden (Rückfall: die Antwort der Bestellung) und den Hinweis oben im Panel zeigen
|
||||
nachErfolg: async (hinweis, antwort) => {
|
||||
const neu = await ladeAbo()
|
||||
if (!aktuell()) return
|
||||
stand = neu ?? (antwort?.subscription ? { ...stand, subscription: antwort.subscription } : stand)
|
||||
zeichne(hinweis)
|
||||
},
|
||||
}))
|
||||
}
|
||||
zeichne()
|
||||
leeren(wurzel).append(anfrage, platz)
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/* Kundenansicht neu (P2): Reiter Stunden in Verträge. Stundenkonto des Wartungs-Abos mit Verlauf; ein Vorschuss für die Einrichtung
|
||||
(ab P6 in ledger.vorschussMinutes) wird nie als Mehraufwand gezeigt. */
|
||||
|
||||
import { h, leeren } from './dom.js'
|
||||
import { K, ersetze, fmtStd, fmtDatum, ladeAbo, ABO_NICHT_GELADEN } from './ansicht-hilfen.js'
|
||||
import { aboWarnungen } from './vertraege-abo.js'
|
||||
|
||||
const laeufe = new WeakMap() // Reiter-Inhalt → Nummer des jüngsten Aufrufs
|
||||
const stat = (wert, beschriftung) => h('div', { class: 'kn-stat' }, h('strong', { class: 'kn-num' }, wert), h('span', null, beschriftung))
|
||||
|
||||
/** Zeile zum Vorschuss für die Einrichtung; bei 0 oder weniger leer. gekuendigt: der Rest verfällt. */
|
||||
export function vorschussZeile(minuten, gekuendigt) {
|
||||
if (!(Number(minuten) > 0)) return ''
|
||||
return `Vorschuss für Einrichtung: ${fmtStd(minuten)}, ${gekuendigt ? 'verfällt, wird nicht berechnet' : 'wird mit den nächsten Monaten verrechnet'}`
|
||||
}
|
||||
|
||||
function verlaufZeile(u) {
|
||||
const vorschuss = Number(u.vorschussMinutes) || 0
|
||||
const mehr = Number(u.overageMinutes) || 0
|
||||
const pille = vorschuss > 0
|
||||
? h('span', { class: K.pille.violett }, `Vorschuss ${fmtStd(vorschuss)}`)
|
||||
: mehr > 0 ? h('span', { class: K.pille.orange, title: 'Über das Kontingent hinaus' }, `+${fmtStd(mehr)} extra`) : null
|
||||
return h('tr', null,
|
||||
h('td', { 'data-label': 'Datum' }, fmtDatum(u.date)),
|
||||
h('td', { 'data-label': 'Vorgang' }, u.topic || u.details || 'Arbeitszeit', u.woid ? h('span', { class: 'kn-zeile-sub' }, ` #${u.woid}`) : null),
|
||||
h('td', { 'data-label': 'Zeit', class: 'kn-num' }, `−${fmtStd(u.minutes)}`, u.supportMinutes ? h('span', { class: 'kn-zeile-sub' }, ` inkl. ${fmtStd(u.supportMinutes)} Unterstützung`) : null, pille ? ' ' : null, pille))
|
||||
}
|
||||
|
||||
function gutschriftZeile(c) {
|
||||
return h('tr', null,
|
||||
h('td', { 'data-label': 'Datum' }, fmtDatum(c.grantedAt)),
|
||||
h('td', { 'data-label': 'Vorgang' }, 'Monatskontingent'),
|
||||
h('td', { 'data-label': 'Zeit', class: 'kn-num' }, `+${fmtStd(c.minutes)}`, h('span', { class: 'kn-zeile-sub' }, ` ${c.expired ? 'abgelaufen' : `nutzbar bis ${fmtDatum(c.expiresAt)}`}`)))
|
||||
}
|
||||
|
||||
/** Reiter-Inhalt Stunden. daten (GET /api/abo) ist optional; ohne sie lädt die Funktion selbst. */
|
||||
export async function zeigeStunden(wurzel, { route, me, nav, daten } = {}) {
|
||||
const nr = (laeufe.get(wurzel) ?? 0) + 1
|
||||
laeufe.set(wurzel, nr)
|
||||
const aktuell = () => laeufe.get(wurzel) === nr
|
||||
leeren(wurzel).append(h('p', { class: K.leer }, 'Wird geladen …'))
|
||||
|
||||
const abo = daten ?? await ladeAbo()
|
||||
if (!aktuell()) return
|
||||
if (!abo) { leeren(wurzel).append(h('p', { class: K.fehler }, ABO_NICHT_GELADEN)); return }
|
||||
const sub = abo.subscription
|
||||
const l = abo.ledger
|
||||
if (!sub?.active || sub.productKey !== 'wartung' || !l) {
|
||||
leeren(wurzel).append(h('p', { class: K.leer }, 'Ein Stundenkonto gibt es im Wartungs-Abo.'))
|
||||
return
|
||||
}
|
||||
const gekuendigt = Boolean(sub.cancelAtPeriodEnd || sub.cancelAt)
|
||||
const vorschuss = vorschussZeile(l.vorschussMinutes, gekuendigt)
|
||||
const zeilen = [...(l.usage || []).map(verlaufZeile), ...(l.credits || []).slice().reverse().map(gutschriftZeile)]
|
||||
|
||||
ersetze(wurzel,
|
||||
...aboWarnungen(sub),
|
||||
h('div', { class: 'kn-statistik' },
|
||||
stat(fmtStd(l.balanceMinutes), 'verfügbare Zeit'),
|
||||
stat(fmtStd(l.usedMinutes), 'bisher genutzt'),
|
||||
l.nextExpiry ? stat(fmtStd(l.nextExpiry.minutes), `verfallen am ${fmtDatum(l.nextExpiry.at)}`) : null),
|
||||
vorschuss ? h('p', { class: 'kn-hinweis kn-vorschuss' }, vorschuss) : null,
|
||||
h('p', { class: 'kn-hinweis' }, '1 Stunde pro bezahltem Monat. Angerechnet wird in 15-Minuten-Schritten, die ältesten Stunden zuerst.'),
|
||||
h('p', { class: 'kn-links' }, h('a', { href: sub.termsUrl || '/abo-bedingungen.html', target: '_blank', rel: 'noopener noreferrer' }, 'Leistungen & Bedingungen')),
|
||||
h('section', { class: 'kn-sektion' },
|
||||
h('h2', { class: K.abschnitt }, 'Verlauf'),
|
||||
zeilen.length
|
||||
? h('div', { class: 'kn-tabelle' }, h('table', null, h('thead', null, h('tr', null, h('th', null, 'Datum'), h('th', null, 'Vorgang'), h('th', null, 'Zeit'))), h('tbody', null, zeilen)))
|
||||
: h('p', { class: 'kn-hinweis' }, 'Noch keine Buchungen.')))
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/* Kundenansicht neu (P2): Bereich Verträge mit den Reitern Rechnungen, Abo, Stunden und E-Mail. Der Rahmen lädt Abo und Rechnungen
|
||||
parallel (jede Quelle mit eigenem Fehlerweg), wählt den Reiter allein aus route.reiter und gibt die schon geladenen Antworten an
|
||||
die Reiter-Inhalte weiter. Bestellen, Wechsel und Gutschrift sind Etagen, die die Hülle direkt ruft. */
|
||||
|
||||
import { api } from './api.js'
|
||||
import { h, leeren } from './dom.js'
|
||||
import { baueReiter } from './reiter.js'
|
||||
import { aktuelleRoute } from './router.js'
|
||||
import { K, ersetze, ladeAbo, wechselFunktionAus, navigiere, nimmHinweis, ABO_NICHT_GELADEN } from './ansicht-hilfen.js'
|
||||
import { offeneRechnungen } from './rechnung-offen.js'
|
||||
import { zeigeRechnungen } from './vertraege-rechnungen.js'
|
||||
import { zeigeAbo } from './vertraege-abo.js'
|
||||
import { zeigeStunden } from './ansicht-stunden.js'
|
||||
import { zeigeSpeicher } from './ansicht-speicher.js'
|
||||
|
||||
/** Nach ?abo=success: so oft und in diesem Abstand wird auf ein laufendes Abo gewartet (Tests verkürzen es). */
|
||||
export const AUF_ABO_WARTEN = { versuche: 10, pauseMs: 2000 }
|
||||
|
||||
const LABEL = { rechnungen: 'Rechnungen', abo: 'Abo', stunden: 'Stunden', email: 'E-Mail' }
|
||||
// Reiterklicks navigieren mit den ausdrücklichen Hashes (P1 schreibt #wartung und #abo kanonisch um)
|
||||
const HASH = { rechnungen: '#rechnungen', abo: '#abo', stunden: '#stunden', email: '#speicher' }
|
||||
const laeufe = new WeakMap() // Wurzel → Nummer des jüngsten Aufrufs
|
||||
let reiterWechsel = null // Reiterklick mit laufendem Router: der nächste Aufruf nimmt die schon geladenen Antworten
|
||||
|
||||
/** Sichtbare Reiter: ohne laufendes Abo Rechnungen und Abo; Hosting dazu E-Mail; Wartung dazu Stunden und E-Mail. */
|
||||
export function sichtbareReiter(daten) {
|
||||
const sub = daten?.subscription
|
||||
const reiter = ['rechnungen', 'abo']
|
||||
if (!sub?.active) return reiter
|
||||
if (sub.productKey === 'hosting') return [...reiter, 'email']
|
||||
return [...reiter, 'stunden', 'email']
|
||||
}
|
||||
|
||||
/** Reiterwahl allein aus route.reiter (routeReiter) und den Daten, nie aus der Adressleiste. Nur der Bereichs-Einstieg (ohne Reiter) springt auf Rechnungen. */
|
||||
export function waehleReiter({ routeReiter, sichtbar, offeneRechnungen: offen }) {
|
||||
if (routeReiter) return sichtbar.includes(routeReiter) ? routeReiter : 'abo'
|
||||
return offen > 0 ? 'rechnungen' : 'abo'
|
||||
}
|
||||
|
||||
const pause = (ms) => new Promise((r) => setTimeout(r, ms))
|
||||
const laden = (api_) => api_('/api/invoices').catch(() => null)
|
||||
|
||||
export async function zeigeVertraege(wurzel, { route, me, nav } = {}) {
|
||||
const nr = (laeufe.get(wurzel) ?? 0) + 1
|
||||
laeufe.set(wurzel, nr)
|
||||
const aktuell = () => laeufe.get(wurzel) === nr
|
||||
const vorgabe = reiterWechsel && reiterWechsel.wurzel === wurzel && reiterWechsel.bis > Date.now() ? reiterWechsel : null
|
||||
reiterWechsel = null
|
||||
const suche = new URLSearchParams(globalThis.window?.location?.search ?? '')
|
||||
const aboParam = suche.get('abo')
|
||||
const nachStripe = aboParam === 'success' || aboParam === 'cancel'
|
||||
|
||||
let daten
|
||||
let rechnungen
|
||||
if (vorgabe) {
|
||||
({ daten, rechnungen } = vorgabe)
|
||||
} else {
|
||||
leeren(wurzel).append(h('p', { class: K.leer }, 'Wird geladen …'))
|
||||
;[daten, rechnungen] = await Promise.all([ladeAbo(), laden(api)])
|
||||
if (!aktuell()) return
|
||||
}
|
||||
|
||||
// Rückkehr von Stripe: auf das laufende Abo warten (der Webhook kann noch unterwegs sein)
|
||||
if (aboParam === 'success') {
|
||||
for (let i = 0; i < AUF_ABO_WARTEN.versuche && !daten?.subscription?.active; i += 1) {
|
||||
ersetze(wurzel, h('p', { class: K.meldung }, 'Vielen Dank! Dein Abo wird aktiviert …'), h('p', { class: 'kn-hinweis' }, 'Das dauert nur einen Moment.'))
|
||||
await pause(AUF_ABO_WARTEN.pauseMs)
|
||||
if (!aktuell()) return
|
||||
daten = (await ladeAbo()) ?? daten
|
||||
if (!aktuell()) return
|
||||
}
|
||||
}
|
||||
if (nachStripe) window.history.replaceState(window.history.state, '', window.location.pathname + window.location.hash)
|
||||
if (aboParam === 'success' && wechselFunktionAus(daten?.subscription)) {
|
||||
navigiere('#funktionen', { ersetzen: true })
|
||||
return
|
||||
}
|
||||
|
||||
const sichtbar = sichtbareReiter(daten)
|
||||
const offen = offeneRechnungen(rechnungen).length
|
||||
const gewaehlt = waehleReiter({ routeReiter: route?.reiter, sichtbar, offeneRechnungen: offen })
|
||||
const stundenOhneWartung = route?.reiter === 'stunden' && !sichtbar.includes('stunden')
|
||||
const hinweis = nimmHinweis()
|
||||
|
||||
const inhalt = h('div', { class: 'kn-reiter-inhalt' })
|
||||
const zeichneReiter = (key) => {
|
||||
leeren(inhalt)
|
||||
if (key === 'rechnungen') return zeigeRechnungen(inhalt, { me, antwort: rechnungen ?? undefined })
|
||||
if (!daten) return Promise.resolve()
|
||||
if (key === 'abo') {
|
||||
if (stundenOhneWartung) inhalt.append(h('p', { class: 'kn-hinweis' }, 'Ein Stundenkonto gibt es im Wartungs-Abo.'))
|
||||
const platz = h('div', { class: 'kn-reiter-abo' })
|
||||
inhalt.append(platz)
|
||||
return zeigeAbo(platz, { daten, me })
|
||||
}
|
||||
if (key === 'stunden') return zeigeStunden(inhalt, { route, me, nav, daten })
|
||||
return zeigeSpeicher(inhalt, { route, me, nav, daten })
|
||||
}
|
||||
|
||||
const bei = (key) => {
|
||||
if (aktuelleRoute() !== null) reiterWechsel = { wurzel, daten, rechnungen, bis: Date.now() + 5000 } // der Router ruft uns gleich neu
|
||||
else zeichneReiter(key)
|
||||
navigiere(HASH[key], { ersetzen: true })
|
||||
}
|
||||
const reiterZeile = baueReiter({ eintraege: sichtbar.map((key) => ({ key, label: LABEL[key] })), aktiv: gewaehlt, art: 'unterstrich', ariaLabel: 'Verträge', bei })
|
||||
|
||||
ersetze(wurzel,
|
||||
hinweis ? h('p', { class: `${K.meldung} kn-meldung-ok`, role: 'status' }, hinweis) : null,
|
||||
daten ? null : h('p', { class: K.fehler }, ABO_NICHT_GELADEN),
|
||||
reiterZeile,
|
||||
inhalt,
|
||||
h('p', { class: 'kn-kuendigen' }, h('a', { href: '/kuendigen.html' }, 'Verträge hier kündigen')))
|
||||
await zeichneReiter(gewaehlt)
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/* Kundenansicht neu (P2): Bereich Website. Projektliste mit Vorschau, offene Rechnung, Installationshinweis und Aktivität;
|
||||
als Etage "projekt" die Projektdetails (#projekt?projekt=<id>). Gezeichnet wird mit h() und Textknoten. */
|
||||
|
||||
import { api } from './api.js'
|
||||
import { h, leeren } from './dom.js'
|
||||
import { K, ersetze, zuAnsicht, meldeTitel, fmtDatumZeit, sichereAdresse } from './ansicht-hilfen.js'
|
||||
import { baueProjektDetails, ladeProjektDetails } from './website-details.js'
|
||||
import { baueAktivitaet } from './auftrags-zeitleiste.js'
|
||||
import { ladeOffeneRechnungen, rechnungOffenZeile } from './rechnung-offen.js'
|
||||
import { starteInstallQuelle, installAngebot, baueInstallHinweis, istAusgeblendet, merkeAusgeblendet } from './install-hinweis.js'
|
||||
import { HANDY_ABFRAGE } from './leiste.js'
|
||||
|
||||
const VORSCHAU_BASISBREITE = 1280
|
||||
const laeufe = new WeakMap() // Wurzel → Nummer des jüngsten Aufrufs
|
||||
|
||||
/** Vorschau fertig: Status ready oder deployed und eine Adresse (wie app.js isProjectPreviewReady). */
|
||||
export function vorschauBereit(project) {
|
||||
const status = String(project?.provisioningStatus || project?.status || '').toLowerCase()
|
||||
return ['ready', 'deployed'].includes(status) && Boolean(project?.previewUrl)
|
||||
}
|
||||
|
||||
/** Ton und Text der Status-Pille eines Projekts. */
|
||||
export function projektStatus(project) {
|
||||
if (vorschauBereit(project)) return { ton: 'gruen', text: 'Vorschau online' }
|
||||
const status = String(project?.provisioningStatus || project?.status || '').toLowerCase()
|
||||
if (status.includes('fail')) return { ton: 'rot', text: 'Vorschau gestört' }
|
||||
if (project?.previewUrl) return { ton: 'orange', text: 'Vorschau wird vorbereitet' }
|
||||
return { ton: 'grau', text: 'Noch keine Website' }
|
||||
}
|
||||
|
||||
// Der Frame zeigt die Seite in 1280 px Breite und wird auf die Breite des Rahmens verkleinert
|
||||
function skaliereVorschau(rahmen) {
|
||||
const frame = rahmen.querySelector('iframe')
|
||||
if (!frame) return
|
||||
const anwenden = () => {
|
||||
const breite = rahmen.clientWidth
|
||||
if (breite && frame.style) frame.style.transform = `scale(${breite / VORSCHAU_BASISBREITE})`
|
||||
}
|
||||
if (typeof ResizeObserver !== 'undefined') new ResizeObserver(anwenden).observe(rahmen)
|
||||
anwenden()
|
||||
}
|
||||
|
||||
function hostVon(url) {
|
||||
try { return new URL(url).hostname || '' } catch { return '' }
|
||||
}
|
||||
|
||||
const fakt = (titel, wert) => h('div', null, h('dt', null, titel), h('dd', null, wert))
|
||||
|
||||
function projektZeile(projekt) {
|
||||
const bereit = vorschauBereit(projekt)
|
||||
const url = sichereAdresse(projekt.previewUrl)
|
||||
const status = projektStatus(projekt)
|
||||
const rahmen = bereit && url
|
||||
? h('div', { class: 'kn-vorschau', 'data-vorschau': true }, h('iframe', { src: url, loading: 'lazy', scrolling: 'no', tabindex: '-1', 'aria-hidden': 'true', title: 'Vorschau deiner Website' }))
|
||||
: h('div', { class: 'kn-vorschau kn-vorschau-leer' }, projekt.previewUrl ? 'Vorschau wird vorbereitet …' : 'Noch keine Website veröffentlicht')
|
||||
if (bereit && url) skaliereVorschau(rahmen)
|
||||
const host = hostVon(url)
|
||||
return h('li', { class: 'kn-projekt', 'data-projekt': projekt.id },
|
||||
rahmen,
|
||||
h('div', { class: 'kn-projekt-koerper' },
|
||||
h('div', { class: 'kn-projekt-kopf' }, h('h2', { class: 'kn-projekt-name' }, projekt.projectName || projekt.subdomain || 'Projekt'), h('span', { class: K.pille[status.ton] }, status.text)),
|
||||
h('dl', { class: 'kn-fakten' },
|
||||
fakt('Vorschau-Adresse', host || '–'),
|
||||
fakt('Live-Domain', projekt.liveDomain || 'noch nicht verbunden'),
|
||||
fakt('Letzte Änderung', projekt.updatedAt ? fmtDatumZeit(projekt.updatedAt) : '–')),
|
||||
h('div', { class: 'kn-projekt-links' },
|
||||
bereit && url ? h('a', { class: K.knopf, href: url, target: '_blank', rel: 'noopener noreferrer' }, 'Website öffnen') : null,
|
||||
h('a', { class: K.knopf, ...zuAnsicht('projekt', projekt.id) }, 'Projektdetails'))))
|
||||
}
|
||||
|
||||
// Installationshinweis: ersetzt sich selbst, wenn sich Installierbarkeit oder Auswahl ändern
|
||||
function installPlatz() {
|
||||
const platz = h('div', { class: 'kn-install-platz' })
|
||||
const win = window
|
||||
const quelle = starteInstallQuelle(win)
|
||||
const handy = () => (typeof win.matchMedia === 'function' ? Boolean(win.matchMedia(HANDY_ABFRAGE)?.matches) : false)
|
||||
const zeichne = () => {
|
||||
const angebot = installAngebot({ appLayout: handy(), standalone: quelle.standalone(), canPrompt: quelle.kannPrompt(), ios: quelle.ios, dismissed: istAusgeblendet() })
|
||||
leeren(platz)
|
||||
if (angebot.zeigen) platz.append(baueInstallHinweis({ art: angebot.art, onInstall: () => quelle.installiere(), onDismiss: () => { merkeAusgeblendet(); zeichne() } }))
|
||||
}
|
||||
zeichne()
|
||||
aktualisierer.set(quelle, zeichne)
|
||||
if (!angemeldet.has(quelle)) { angemeldet.add(quelle); quelle.beiAenderung(() => aktualisierer.get(quelle)?.()) }
|
||||
return platz
|
||||
}
|
||||
const aktualisierer = new WeakMap()
|
||||
const angemeldet = new WeakSet()
|
||||
|
||||
function aktivitaetAbschnitt(antwort, jetzt) {
|
||||
const inhalt = antwort.fehler
|
||||
? h('p', { class: 'kn-hinweis' }, `Aktivität konnte nicht geladen werden: ${antwort.fehler}`)
|
||||
: baueAktivitaet(antwort.tickets, jetzt)
|
||||
return h('section', { class: 'kn-sektion', 'data-abschnitt': 'aktivitaet' }, h('h2', { class: K.abschnitt }, 'Was wir zuletzt erledigt haben'), inhalt)
|
||||
}
|
||||
|
||||
const projekteLaden = () => api('/api/projects').then((a) => ({ liste: Array.isArray(a?.projects) ? a.projects : [] }), (f) => ({ fehler: f.message }))
|
||||
const aktivitaetLaden = () => api('/api/activity').then((a) => ({ tickets: Array.isArray(a?.tickets) ? a.tickets : [] }), (f) => ({ fehler: f.message }))
|
||||
|
||||
const projektId = (query) => { const p = new URLSearchParams(query || ''); return p.get('projekt') || p.get('id') || '' }
|
||||
|
||||
export async function zeigeWebsite(wurzel, { route, me, nav } = {}) {
|
||||
const nr = (laeufe.get(wurzel) ?? 0) + 1
|
||||
laeufe.set(wurzel, nr)
|
||||
const aktuell = () => laeufe.get(wurzel) === nr
|
||||
leeren(wurzel).append(h('p', { class: K.leer }, 'Wird geladen …'))
|
||||
const jetzt = new Date()
|
||||
|
||||
if (route?.etage === 'projekt') {
|
||||
const projekte = await projekteLaden()
|
||||
if (!aktuell()) return
|
||||
if (projekte.fehler) { leeren(wurzel).append(h('p', { class: K.fehler }, `Projekte konnten nicht geladen werden: ${projekte.fehler}`)); return }
|
||||
const wunsch = projektId(route.query)
|
||||
const projekt = wunsch ? projekte.liste.find((p) => p.id === wunsch) : projekte.liste[0]
|
||||
if (!projekt) { leeren(wurzel).append(h('p', { class: K.leer }, projekte.liste.length ? 'Dieses Projekt wurde nicht gefunden.' : 'Noch keine Website-Projekte zugewiesen.')); return }
|
||||
meldeTitel(wurzel, projekt.projectName || projekt.subdomain || 'Projekt')
|
||||
const details = await ladeProjektDetails(projekt.id)
|
||||
if (!aktuell()) return
|
||||
leeren(wurzel).append(baueProjektDetails(details, jetzt))
|
||||
return
|
||||
}
|
||||
|
||||
const [projekte, aktivitaet, offen] = await Promise.all([projekteLaden(), aktivitaetLaden(), ladeOffeneRechnungen()])
|
||||
if (!aktuell()) return
|
||||
const zeile = rechnungOffenZeile(offen)
|
||||
const liste = projekte.fehler
|
||||
? h('p', { class: K.fehler }, `Projekte konnten nicht geladen werden: ${projekte.fehler}`)
|
||||
: projekte.liste.length
|
||||
? h('ul', { class: 'kn-projekte' }, projekte.liste.map(projektZeile))
|
||||
: h('p', { class: K.leer }, 'Noch keine Website-Projekte zugewiesen.')
|
||||
const aktivitaetSektion = aktivitaetAbschnitt(aktivitaet, jetzt)
|
||||
ersetze(wurzel, zeile, installPlatz(), liste, aktivitaetSektion)
|
||||
if (route?.scroll === 'aktivitaet') aktivitaetSektion.scrollIntoView({ block: 'start' })
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
/* Kundenansicht neu (P7): Wechsel vom Hosting-Abo ins Wartungs-Abo (#wechsel) im schlanken Muster der Spec 5.6, Fassung 6.
|
||||
Reihenfolge ohne Klappe: Block „Was du bestellst“ (Vorschau vom Server), eine Linkzeile, bei Privatkunden das Sofortbeginn-Häkchen mit
|
||||
Sperrzeile, der Knopf „Zahlungspflichtig bestellen“ (§ 312j BGB), darunter die Kundenart-Zeile. Der Wechsel wirkt erst mit der Zahlung
|
||||
der Wechselrechnung (Bedingungen § 9 Abs. 3). Die Sätze kommen vom Server (GET /api/abo → bestelltexte); der Server entscheidet die
|
||||
Kundenart selbst, der Client sendet nie kundenart. Ein Betrieb braucht kein Häkchen.
|
||||
Die gemerkte Funktion (E14, C5): kam der Kunde aus dem Einschalten einer Funktion, geht es danach zurück zu den Funktionen.
|
||||
Der Block ist knapp gehalten (Leistung als eine Zeile mit Link auf die Leistungsbeschreibung), damit er mit dem Knopf in den nutzbaren Bereich
|
||||
zwischen Kopf und Leiste passt (375 x 667); die ausführlichen Sätze stehen unter dem Knopf (baueEinzelheiten), ohne Klappe.
|
||||
Der Titel kommt von der Hülle (meldeTitel); die Seite hat keine eigene h1. */
|
||||
|
||||
import { api } from './api.js'
|
||||
import { h, leeren } from './dom.js'
|
||||
import { K, ladeAbo, wechselFunktionAus, sichereAdresse, merkeHinweis, navigiere, meldeTitel, meldeZaehlerNeu, zuAnsicht, fmtEuro, fmtDatum, berlinTag, ABO_NICHT_GELADEN } from './ansicht-hilfen.js'
|
||||
import { baueBlock, baueEinzelheiten, baueKundenartZeile, baueLinkzeile, hatWortlaut, kundenartVon, leistungLink } from './bestell-bausteine.js'
|
||||
|
||||
const SCHLUESSEL = 'kn-wechsel-funktion'
|
||||
const TEXT_SPERRE = 'Im Ansichtsmodus nicht möglich.'
|
||||
const TEXT_NICHT_VERFUEGBAR = 'Die Online-Bestellung wird gerade eingerichtet – schreib uns gern im Chat.'
|
||||
const KNOPF = 'Zahlungspflichtig bestellen'
|
||||
const UST = 'gemäß § 19 UStG ohne Umsatzsteuer'
|
||||
const laeufe = new WeakMap() // Wurzel → Nummer des jüngsten Aufrufs
|
||||
|
||||
/** Die von P3 gemerkte Funktion { key, projectId } oder null (kaputter oder fremder Wert, kein sessionStorage). */
|
||||
export function gemerkteFunktion() {
|
||||
try {
|
||||
const wert = JSON.parse(globalThis.sessionStorage?.getItem(SCHLUESSEL) ?? 'null')
|
||||
return wert && typeof wert.key === 'string' && wert.key && typeof wert.projectId === 'string' && wert.projectId ? { key: wert.key, projectId: wert.projectId } : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
function vergissFunktion() {
|
||||
try { globalThis.sessionStorage?.removeItem(SCHLUESSEL) } catch { /* ohne Speicher bleibt nichts liegen */ }
|
||||
}
|
||||
|
||||
// Zurück aus dem Bfcache (z. B. von der Zahlungsseite): eine hängende Bestellung nicht gesperrt lassen
|
||||
let aktiv = null
|
||||
const mitPageshow = new WeakSet()
|
||||
function registrierePageshow() {
|
||||
const win = window
|
||||
if (mitPageshow.has(win)) return
|
||||
mitPageshow.add(win)
|
||||
win.addEventListener('pageshow', () => { if (aktiv?.busy) { aktiv.busy = false; aktiv.zeichne() } })
|
||||
}
|
||||
|
||||
export async function zeigeWechsel(wurzel, { route, me, nav, daten } = {}) {
|
||||
const nr = (laeufe.get(wurzel) ?? 0) + 1
|
||||
laeufe.set(wurzel, nr)
|
||||
const aktuell = () => laeufe.get(wurzel) === nr
|
||||
const viewAs = me?.viewAs === true
|
||||
meldeTitel(wurzel, 'Wechsel ins Wartungs-Abo')
|
||||
leeren(wurzel).append(h('p', { class: K.leer }, 'Wird geladen …'))
|
||||
registrierePageshow()
|
||||
|
||||
const stand = daten ?? await ladeAbo()
|
||||
if (!aktuell()) return
|
||||
if (!stand) { leeren(wurzel).append(h('p', { class: K.fehler }, ABO_NICHT_GELADEN)); return }
|
||||
const sub = stand.subscription
|
||||
const wartung = (stand.products || []).find((p) => p.key === 'wartung') || null
|
||||
if (!sub?.active || sub.productKey !== 'hosting' || !sub.canSwitch || !wartung) {
|
||||
leeren(wurzel).append(h('div', { class: 'kn-panel' }, h('p', { class: 'kn-hinweis' }, 'Ein Wechsel ist derzeit nicht möglich. Details findest du unter ', h('a', zuAnsicht('vertraege'), 'Verträge'), '.')))
|
||||
navigiere('#vertraege', { ersetzen: true })
|
||||
return
|
||||
}
|
||||
// Ohne Wortlaut vom Server gibt es keinen Bestellknopf und keine Vorschau
|
||||
if (!hatWortlaut(stand, 'wechsel')) {
|
||||
leeren(wurzel).append(h('div', { class: 'kn-panel' }, h('p', { class: 'kn-hinweis' }, TEXT_NICHT_VERFUEGBAR)))
|
||||
return
|
||||
}
|
||||
|
||||
const T = stand.bestelltexte
|
||||
const kundenart = kundenartVon(me, stand)
|
||||
const privat = kundenart !== 'unternehmen'
|
||||
const units = Number(sub.storage?.units) || 0
|
||||
const mail = stand.mailStorage || {}
|
||||
const unitCents = Number(mail.unitPriceCents) || 0
|
||||
const lauf = { busy: false, preview: null, error: '', loading: true, token: 0, early: false, fehler: '', fehlerKlasse: null, zeichne: null }
|
||||
aktiv = lauf
|
||||
|
||||
const paketText = `${units} Paket${units === 1 ? '' : 'e'}` // Nominativ: „2 Pakete bleiben gebucht“
|
||||
const paketMit = `${units} Paket${units === 1 ? '' : 'en'}` // Dativ nach „mit“: „mit 1 Paket“, „mit 2 Paketen“
|
||||
const speicherGb = units * (Number(mail.gbPerUnit) || 0)
|
||||
|
||||
/**
|
||||
* Zeilen des Blocks aus der Vorschau des Servers, knapp: Leistung, Preis (mit gebuchtem Zusatzspeicher), Fällig (mit der Gutschrift und dem Abbuchungstag),
|
||||
* Laufzeit, Beginn. Die Einzelposten stehen ausführlich unter dem Knopf (einzelheitenVon); so passt der Block auch mit Zusatzspeicher in 375 x 667.
|
||||
*/
|
||||
const zeilenVon = (p) => {
|
||||
const monthly = Number.isFinite(Number(p.monthlyCents)) && p.monthlyCents != null ? Number(p.monthlyCents) : wartung.amountCents
|
||||
const tag = berlinTag(p.nextChargeAt)
|
||||
const gutschrift = (Number(p.creditCents) || 0) > 0 ? ` (nach Gutschrift von ${fmtEuro(p.creditCents / 100)} für nicht genutzte Tage des Hosting-Abos)` : ''
|
||||
return [
|
||||
{ label: 'Leistung', text: 'Wartungs-Abo statt Hosting-Abo', link: { text: 'Leistungen ansehen', href: leistungLink(T.wechsel.linkzeile, 'wartung') } },
|
||||
{ label: 'Preis', text: units > 0
|
||||
? `${fmtEuro((monthly + units * unitCents) / 100)} pro Monat mit ${paketMit} Zusatzspeicher (+${speicherGb} GB) (Gesamtpreis, ${UST})`
|
||||
: `${fmtEuro(monthly / 100)} pro Monat (Gesamtpreis, ${UST})` },
|
||||
{ label: 'Fällig', text: `heute ${fmtEuro(p.amountDueCents / 100)}${gutschrift}; danach ab ${fmtDatum(p.nextChargeAt)}${tag ? ` jeweils am ${tag.t}.` : ''}` },
|
||||
{ label: 'Laufzeit', text: `Mindestlaufzeit bis ${fmtDatum(p.minTermEnd)} (12 Monate), danach monatlich kündbar` },
|
||||
{ label: 'Beginn', text: 'sofort mit der Zahlung; einen späteren Beginn gibt es nicht' },
|
||||
]
|
||||
}
|
||||
|
||||
/** Die ausführlichen Sätze unter dem Knopf (Wortlaut wie bisher im Block). */
|
||||
const einzelheitenVon = (p) => {
|
||||
const tag = berlinTag(p.nextChargeAt)
|
||||
return [
|
||||
{ label: 'Leistung', text: 'Zusätzlich zum Hosting: Pflege deiner Website, beliebig viele Postfächer und 1 Stunde Arbeitszeit pro Monat. Das Hosting-Abo endet mit dem Wechsel im gegenseitigen Einvernehmen; Domain, Postfächer und gebuchter Speicher bleiben.' },
|
||||
...(units > 0
|
||||
? [{ label: 'Zusatzspeicher', text: `${paketText} (+${speicherGb} GB) bleiben gebucht: ${fmtEuro(units * unitCents / 100)} pro Monat zusätzlich (${UST})` }]
|
||||
: []),
|
||||
...((Number(p.creditCents) || 0) > 0
|
||||
? [{ label: 'Gutschrift für nicht genutzte Tage', text: `−${fmtEuro(p.creditCents / 100)} aus dem bereits bezahlten Hosting-Abo (${UST})` }]
|
||||
: []),
|
||||
{ label: 'Heute fällig', text: `${fmtEuro(p.amountDueCents / 100)} (${UST})` },
|
||||
{ label: 'Neuer Abbuchungstag', text: `${tag ? `Ab dem Wechsel jeweils am ${tag.t}. eines Monats; ` : ''}nächste Abbuchung am ${fmtDatum(p.nextChargeAt)}` },
|
||||
{ label: 'Laufzeit', text: `Mindestlaufzeit bis ${fmtDatum(p.minTermEnd)} (12 Monate ab dem Wechsel), danach monatlich kündbar` },
|
||||
{ label: 'Beginn', text: 'Der Wechsel wird erst mit der Zahlung wirksam. Bei Kreditkarte, sobald die Zahlung erfolgreich ist (ggf. nach Bestätigung bei deiner Bank); bei SEPA-Lastschrift sofort mit der Bestellung, wird die Lastschrift nicht eingelöst, gilt der Wechsel als nicht erfolgt. Einen späteren Beginn gibt es nicht.' },
|
||||
]
|
||||
}
|
||||
|
||||
const zeichne = () => {
|
||||
const p = lauf.preview
|
||||
const uebersicht = lauf.error
|
||||
? h('p', { class: K.fehler, role: 'alert' }, lauf.error)
|
||||
: lauf.loading || !p ? h('p', { class: 'kn-hinweis' }, 'Deine Übersicht wird berechnet …')
|
||||
: baueBlock(zeilenVon(p), { id: 'switch-block' })
|
||||
const sperre = lauf.busy || viewAs
|
||||
const haken = privat
|
||||
? h('input', {
|
||||
type: 'checkbox', id: 'switch-beginn', checked: lauf.early, disabled: sperre,
|
||||
on: { change: (e) => { lauf.early = e.currentTarget.checked; zeichne(); wurzel.querySelector('#switch-beginn')?.focus() } },
|
||||
})
|
||||
: null
|
||||
const gesperrt = privat && !lauf.early
|
||||
leeren(wurzel).append(h('section', { class: 'kn-panel kn-bestellen', id: 'switch-order' },
|
||||
h('h2', null, 'Was du bestellst'),
|
||||
uebersicht,
|
||||
baueLinkzeile(T.wechsel.linkzeile, { id: 'switch-linkzeile' }),
|
||||
haken ? h('label', { class: K.check }, haken, h('span', { id: 'switch-beginn-text' }, T.wechsel.haken)) : null,
|
||||
gesperrt ? h('p', { class: 'kn-sperrzeile', id: 'switch-sperre' }, T.wechsel.gesperrt) : null,
|
||||
h('p', { class: lauf.fehlerKlasse ?? K.fehler, id: 'switch-error', role: 'alert', hidden: !lauf.fehler }, lauf.fehler),
|
||||
h('div', { class: 'kn-aktionen' },
|
||||
h('button', { type: 'button', class: K.primaer, id: 'switch-buy', disabled: lauf.busy || viewAs || lauf.loading || !p || gesperrt, on: { click: absenden } }, lauf.busy ? 'Wird bestellt …' : (T.knopf || KNOPF)),
|
||||
viewAs ? h('span', { class: K.meldung }, TEXT_SPERRE) : null),
|
||||
baueKundenartZeile(kundenart, T.kundenart, { id: 'switch-kundenart' }),
|
||||
p ? baueEinzelheiten(einzelheitenVon(p), { id: 'switch-einzelheiten' }) : null))
|
||||
}
|
||||
lauf.zeichne = zeichne
|
||||
|
||||
const ladeVorschau = async () => {
|
||||
const token = ++lauf.token
|
||||
lauf.loading = true
|
||||
lauf.error = ''
|
||||
zeichne()
|
||||
try {
|
||||
const p = await api('/api/abo/switch/preview', { method: 'POST', body: {} })
|
||||
if (token !== lauf.token || !aktuell()) return
|
||||
lauf.preview = p
|
||||
} catch (f) {
|
||||
if (token !== lauf.token || !aktuell()) return
|
||||
lauf.preview = null
|
||||
lauf.error = f.message
|
||||
}
|
||||
lauf.loading = false
|
||||
zeichne()
|
||||
}
|
||||
|
||||
const zeigeFehler = (text, klasse) => { lauf.fehler = text; lauf.fehlerKlasse = klasse; zeichne() }
|
||||
|
||||
async function absenden() {
|
||||
if (viewAs || lauf.busy || !lauf.preview) return
|
||||
if (privat && !lauf.early) return // ohne Häkchen kein Wechsel; die Sperrzeile steht da
|
||||
lauf.fehler = ''
|
||||
const p = lauf.preview
|
||||
const funktion = gemerkteFunktion()
|
||||
const gesendet = funktion !== null
|
||||
lauf.busy = true
|
||||
zeichne()
|
||||
try {
|
||||
// Der Klick auf den Knopf ist die Zustimmung (acceptTerms). Nie kundenart: ob ein Häkchen nötig ist, entscheidet der Server.
|
||||
const body = { acceptTerms: true, termsVersion: stand.termsVersion, prorationDate: p.prorationDate, amountDueCents: p.amountDueCents }
|
||||
if (privat) body.earlyStart = true
|
||||
if (gesendet) body.funktion = funktion
|
||||
const res = await api('/api/abo/switch', { method: 'POST', body })
|
||||
if (gesendet) vergissFunktion()
|
||||
if (res.paymentUrl) {
|
||||
const ziel = sichereAdresse(res.paymentUrl)
|
||||
if (!ziel) {
|
||||
lauf.busy = false
|
||||
zeigeFehler('Die Zahlungsseite konnte nicht geöffnet werden. Der Wechsel wartet auf die Zahlung; Näheres steht unter Verträge.')
|
||||
return
|
||||
}
|
||||
const knopf = wurzel.querySelector('#switch-buy')
|
||||
if (knopf) knopf.textContent = 'Weiter zur Zahlung …'
|
||||
zeigeFehler('Dein Wechsel wartet auf die Zahlung. Wir leiten dich zur Zahlung weiter.', 'kn-meldung')
|
||||
window.location.href = ziel
|
||||
return
|
||||
}
|
||||
lauf.busy = false
|
||||
const sepa = Boolean(res.subscription?.switchPending)
|
||||
const hinweis = sepa
|
||||
? 'Dein Wechsel ins Wartungs-Abo gilt ab sofort. Die Lastschrift wird noch eingezogen; wir bestätigen dir den Wechsel per E-Mail.'
|
||||
: 'Dein Wechsel ins Wartungs-Abo ist abgeschlossen. Wir bestätigen ihn per E-Mail.'
|
||||
meldeZaehlerNeu('vertraege')
|
||||
// Mit gemerkter Funktion zurück zu den Funktionen (dort öffnet P3 den Einschalt-Ablauf), sonst wie bisher zu den Verträgen
|
||||
if (gesendet) {
|
||||
let zurueck = wechselFunktionAus(res.subscription)
|
||||
if (!zurueck) zurueck = wechselFunktionAus((await ladeAbo())?.subscription)
|
||||
if (zurueck) { navigiere('#funktionen', { ersetzen: true }); return }
|
||||
}
|
||||
merkeHinweis(hinweis)
|
||||
navigiere('#vertraege')
|
||||
} catch (f) {
|
||||
lauf.busy = false
|
||||
zeigeFehler(f.message)
|
||||
// Betrag oder Stichtag haben sich geändert: neue Vorschau laden, Fehlerhinweis bleibt stehen
|
||||
if (f.status === 409) {
|
||||
const msg = f.message
|
||||
await ladeVorschau()
|
||||
zeigeFehler(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await ladeVorschau()
|
||||
}
|
||||
@@ -0,0 +1,425 @@
|
||||
/* Kundenansicht neu: Aussehen der Ansichten Website, Chat, Verträge und der Etagen Passwort, Gutschrift, Wechsel, Bestellen.
|
||||
Besitzer: P2 (danach hängt P7 einen Abschnitt für die Bestellseiten an). Werte nur über Tokens (tokens.css); Farbliterale stehen
|
||||
ausschließlich in den --kn-Deklarationen direkt unten. Abschnitte durch Haarlinien, keine Kästen; Radien 4, 6 und 12 px. */
|
||||
|
||||
/* ------------------------------------------------------------------ Zusatz-Tokens (nur für diese Datei) --- */
|
||||
.kn {
|
||||
--kn-spur: rgba(26,31,54,.2);
|
||||
--kn-ablage-hell: rgba(255,255,255,.92);
|
||||
}
|
||||
|
||||
.kn .hidden { display: none !important; }
|
||||
|
||||
/* ------------------------------------------------------------------ Gemeinsame Bausteine der Ansichten --- */
|
||||
.kn-hinweis { margin: 8px 0 0; font-size: 13px; line-height: 20px; color: var(--kn-text-2); }
|
||||
.kn-notiz { margin: 8px 0 0; font-size: 13px; line-height: 20px; color: var(--kn-text-2); }
|
||||
.kn-umbruch { white-space: pre-wrap; }
|
||||
.kn-panel { margin: 0 0 24px; }
|
||||
.kn-panel > h3, .kn-sektion h3, .kn-vertrag h3, .kn-wechsel h3, .kn-abo-karte h3 { margin: 0 0 8px; font-size: 16px; line-height: 24px; font-weight: 600; }
|
||||
.kn-sektion { margin-top: 32px; }
|
||||
.kn-sektion:first-child { margin-top: 0; }
|
||||
.kn-aktionen { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; margin-top: 16px; }
|
||||
.kn-preis { margin: 0 0 4px; font-size: 14px; color: var(--kn-text-2); font-variant-numeric: tabular-nums; }
|
||||
.kn-preis strong { font-size: 20px; line-height: 28px; font-weight: 700; color: var(--kn-text); letter-spacing: -.01em; }
|
||||
.kn-links { margin: 12px 0 0; font-size: 14px; }
|
||||
.kn-gesperrt { opacity: .55; cursor: not-allowed; pointer-events: none; }
|
||||
.kn-zeilenlink { margin-right: 12px; font-weight: 500; text-decoration: underline; text-decoration-color: var(--kn-line-strong); text-underline-offset: 3px; }
|
||||
.kn-zeilenlink:hover { text-decoration-color: currentColor; }
|
||||
.kn-zeit { flex: none; font-variant-numeric: tabular-nums; color: var(--kn-text-2); white-space: nowrap; }
|
||||
.kn-badge {
|
||||
display: inline-grid; place-items: center; min-width: 18px; height: 18px; padding: 0 5px; border-radius: 50%;
|
||||
background: var(--kn-accent); color: var(--kn-bg); font-size: 11px; font-weight: 600; font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* Tabellen (Rechnungen, Dateien, Zusammenfassungen) */
|
||||
.kn-tabelle { overflow-x: auto; }
|
||||
.kn-tabelle table, .kn-zusammenfassung { width: 100%; border-collapse: collapse; font-variant-numeric: tabular-nums; }
|
||||
.kn-tabelle th { padding: 8px 12px 8px 0; border-bottom: 1px solid var(--kn-line); text-align: left; font-size: 12px; font-weight: 500; color: var(--kn-text-2); white-space: nowrap; }
|
||||
.kn-tabelle td { padding: 12px 12px 12px 0; border-bottom: 1px solid var(--kn-line); vertical-align: top; }
|
||||
.kn-tabelle tr:last-child td { border-bottom: 0; }
|
||||
.kn-zusammenfassung td { padding: 10px 12px 10px 0; border-top: 1px solid var(--kn-line); vertical-align: top; overflow-wrap: anywhere; }
|
||||
.kn-zusammenfassung tr:last-child td { border-bottom: 1px solid var(--kn-line); }
|
||||
.kn-zusammenfassung td:first-child { width: 11rem; color: var(--kn-text-2); }
|
||||
.kn-uebersicht { margin: 16px 0; }
|
||||
|
||||
/* Fakten als Liste */
|
||||
.kn-fakten { display: grid; grid-template-columns: max-content 1fr; gap: 4px 16px; margin: 0 0 16px; }
|
||||
.kn-fakten > div { display: contents; }
|
||||
.kn-fakten dt { color: var(--kn-text-2); }
|
||||
.kn-fakten dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
|
||||
|
||||
/* Leistungen eines Abos */
|
||||
.kn-leistungen { list-style: none; margin: 12px 0; padding: 0; }
|
||||
.kn-leistungen li { padding: 8px 0; border-top: 1px solid var(--kn-line); }
|
||||
.kn-leistungen li:last-child { border-bottom: 1px solid var(--kn-line); }
|
||||
.kn-leistungen small { display: block; color: var(--kn-text-2); font-size: 13px; }
|
||||
|
||||
/* Angebotskasten über der Bestellung */
|
||||
.kn-angebot-kasten { margin: 0 0 24px; padding: 12px 16px; border-radius: var(--kn-radius); background: var(--kn-bg-subtle); }
|
||||
.kn-angebot-kasten p { margin: 4px 0 0; }
|
||||
.kn-angebot { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 0 0 8px; }
|
||||
.kn-angebot-text { color: var(--kn-text-2); }
|
||||
|
||||
/* Ein-/Ausklappen langer Listen */
|
||||
.kn-einklappbar:not(.is-open) [data-extra] { display: none; }
|
||||
.kn-mehr { margin-top: 8px; }
|
||||
|
||||
/* ------------------------------------------------------------------ Website --- */
|
||||
.kn-rechnung-offen { margin: 0 0 16px; padding: 8px 12px; border-radius: var(--kn-radius); background: var(--kn-orange-bg); color: var(--kn-orange-fg); font-weight: 500; }
|
||||
.kn-rechnung-offen a { text-decoration-color: currentColor; }
|
||||
.kn-rechnung-offen .kn-rechnung-text { min-width: 0; }
|
||||
.kn-install-platz:empty { display: none; }
|
||||
.kn-install { display: flex; align-items: center; gap: 12px; margin: 0 0 16px; padding: 12px 0; border-top: 1px solid var(--kn-line); border-bottom: 1px solid var(--kn-line); }
|
||||
.kn-install-text { flex: 1; min-width: 0; }
|
||||
.kn-install-text strong, .kn-install-text span { display: block; }
|
||||
.kn-install-text span { font-size: 13px; color: var(--kn-text-2); }
|
||||
.kn-projekte { list-style: none; margin: 0; padding: 0; }
|
||||
.kn-projekt { display: grid; grid-template-columns: minmax(0, 360px) minmax(0, 1fr); gap: 24px; padding: 24px 0; border-top: 1px solid var(--kn-line); }
|
||||
.kn-projekt:last-child { border-bottom: 1px solid var(--kn-line); }
|
||||
.kn-vorschau { position: relative; aspect-ratio: 16 / 10; overflow: hidden; border-radius: var(--kn-radius-lg); background: var(--kn-bg-subtle); box-shadow: var(--kn-ring); }
|
||||
.kn-vorschau iframe { position: absolute; top: 0; left: 0; width: 1280px; height: 800px; border: 0; transform-origin: 0 0; pointer-events: none; }
|
||||
.kn-vorschau-leer { display: grid; place-items: center; padding: 16px; text-align: center; color: var(--kn-text-2); }
|
||||
.kn-projekt-koerper { min-width: 0; }
|
||||
.kn-projekt-kopf { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 12px; }
|
||||
.kn-projekt-name { margin: 0; font-size: 18px; line-height: 26px; font-weight: 600; overflow-wrap: anywhere; }
|
||||
.kn-projekt-links { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
|
||||
/* Auftrag, Änderungen, Arbeiten, Aktivität */
|
||||
[data-abschnitt="aktivitaet"] { scroll-margin-top: 16px; } /* #fortschritt: scrollIntoView lässt den Abschnitt nicht hinter dem haftenden Kopf beginnen (am Handy unten) */
|
||||
.kn-auftrag-kopf { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
|
||||
.kn-auftrag-name { min-width: 0; }
|
||||
.kn-auftrag-nr { display: block; font-size: 12px; color: var(--kn-text-2); font-variant-numeric: tabular-nums; }
|
||||
.kn-auftrag-name h3 { margin: 0; font-size: 16px; line-height: 24px; font-weight: 600; overflow-wrap: anywhere; }
|
||||
.kn-auftrag-gesamt { margin: 8px 0 0; font-size: 13px; color: var(--kn-text-2); }
|
||||
.kn-schritte { display: flex; gap: 4px; margin: 12px 0 0; padding: 0; list-style: none; }
|
||||
.kn-schritte li { flex: 1; padding-top: 6px; border-top: 2px solid var(--kn-line); font-size: 12px; color: var(--kn-text-3); }
|
||||
.kn-schritte li.is-done { border-top-color: var(--kn-text-2); color: var(--kn-text-2); }
|
||||
.kn-schritte li.is-current { border-top-color: var(--kn-accent); color: var(--kn-text); font-weight: 600; }
|
||||
.kn-auftrag-hinweis { margin: 12px 0 0; font-size: 13px; color: var(--kn-orange-fg); }
|
||||
.kn-zeitleiste { margin: 0; }
|
||||
.kn-tag { margin: 0; }
|
||||
.kn-tag-name { margin: 16px 0 4px; font-size: 13px; font-weight: 600; color: var(--kn-text-2); }
|
||||
.kn-aenderungen, .kn-arbeiten, .kn-unterstuetzung { list-style: none; margin: 0; padding: 0; }
|
||||
.kn-aenderung { padding: 8px 0; border-top: 1px solid var(--kn-line); }
|
||||
.kn-aenderung-kopf { display: flex; justify-content: space-between; gap: 12px; }
|
||||
.kn-aenderung-kopf time { flex: none; font-size: 13px; color: var(--kn-text-2); font-variant-numeric: tabular-nums; }
|
||||
.kn-aenderung-details, .kn-eintrag-aenderungen { margin: 4px 0 0; padding-left: 18px; color: var(--kn-text-2); font-size: 13px; }
|
||||
.kn-eintrag { display: flex; justify-content: space-between; gap: 12px; padding: 10px 0; border-top: 1px solid var(--kn-line); }
|
||||
.kn-eintrag-haupt { min-width: 0; flex: 1; }
|
||||
.kn-eintrag-kopf { display: flex; flex-wrap: wrap; gap: 4px 8px; }
|
||||
.kn-eintrag-meta { font-size: 13px; color: var(--kn-text-2); }
|
||||
.kn-eintrag-text { margin: 4px 0 0; color: var(--kn-text-2); font-size: 13px; }
|
||||
.kn-unterstuetzung li { display: flex; justify-content: space-between; gap: 12px; margin-top: 4px; font-size: 13px; color: var(--kn-text-2); }
|
||||
/* „Was wir zuletzt erledigt haben“ als Zeitleiste (Variante C): je Auftrag Überschrift mit Pille, schmaler Balken in vier Abschnitten, Zeitleiste mit Haarlinie */
|
||||
.kn-az-liste { margin: 0; }
|
||||
.kn-az-liste:not(.is-open) > .kn-az-auftraege > [data-extra] { display: none; } /* nur direkte Kinder: die Zeitleisten darin klappen für sich */
|
||||
.kn-az-auftraege { list-style: none; margin: 0; padding: 0; }
|
||||
.kn-az { padding: 20px 0; border-top: 1px solid var(--kn-line); }
|
||||
.kn-az-kopf { display: flex; flex-wrap: wrap; align-items: flex-start; justify-content: space-between; gap: 6px 12px; }
|
||||
.kn-az-titel { flex: 1 1 12em; min-width: 0; margin: 0; font-size: 16px; line-height: 24px; font-weight: 600; overflow-wrap: anywhere; }
|
||||
.kn-az-kopf .kn-pille { flex: none; margin-top: 2px; }
|
||||
.kn-az-info { margin: 8px 0 0; font-size: 14px; line-height: 20px; }
|
||||
.kn-az-info a { color: var(--kn-orange-fg); text-decoration: none; }
|
||||
.kn-az-info-link { font-weight: 600; text-decoration: underline; text-decoration-color: currentColor; text-underline-offset: 3px; white-space: nowrap; } /* der Linktext bleibt zusammen */
|
||||
.kn-az-balken { display: flex; gap: 3px; margin: 12px 0 0; padding: 0; list-style: none; }
|
||||
.kn-az-balken li { position: relative; flex: 1; height: 4px; }
|
||||
.kn-az-balken li::before { content: ""; position: absolute; inset: 0; border-radius: 2px; background: var(--kn-line); } /* die Fläche liegt im Pseudo-Element: der Lesetext darunter bleibt auf Weiß */
|
||||
.kn-az-balken li.is-done::before { background: var(--kn-accent); }
|
||||
.kn-az-zeitleiste { margin: 14px 0 0; }
|
||||
.kn-az-zeilen { list-style: none; margin: 0 0 0 3px; padding: 0; border-left: 1px solid var(--kn-line-strong); }
|
||||
.kn-az-zeilen li { position: relative; display: flex; gap: 10px; padding: 4px 0 4px 16px; font-size: 14px; line-height: 20px; }
|
||||
.kn-az-zeilen li::before { content: ""; position: absolute; left: -3px; top: 12px; width: 5px; height: 5px; border-radius: 50%; background: var(--kn-line-strong); }
|
||||
.kn-az-datum { flex: none; min-width: 3.2em; color: var(--kn-text-2); font-variant-numeric: tabular-nums; white-space: nowrap; }
|
||||
.kn-az-mit-jahr .kn-az-datum { min-width: 4.4em; }
|
||||
.kn-az-text { min-width: 0; overflow-wrap: anywhere; }
|
||||
.kn-az-zeitleiste:not(.is-open) > .kn-az-zeilen > [data-extra] { display: none; }
|
||||
.kn-az-mehr { margin: 4px 0 0 8px; }
|
||||
.kn-az-leer { margin: 12px 0 0; font-size: 14px; color: var(--kn-text-2); }
|
||||
.kn-az-aeltere { margin: 8px 0 0; }
|
||||
.kn-dateien { margin: 0; }
|
||||
.kn-details { margin: 0; }
|
||||
|
||||
/* ------------------------------------------------------------------ Chat (Muster .woms-chat des Ticketsystems, Präfix .kn-chat) --- */
|
||||
/* chat-composer.css (Kopie) lädt vor diesem Blatt; jede Regel für Elemente des Bausteins ist hier strikt spezifischer (.kn .kn-chat …) und ohne !important. */
|
||||
.kn-chat {
|
||||
--chat-accent: var(--kn-accent);
|
||||
--chat-accent-hover: var(--kn-accent-hover);
|
||||
--chat-accent-soft: var(--kn-accent-soft);
|
||||
--chat-accent-line: var(--kn-accent-ring);
|
||||
--chat-accent-text: var(--kn-accent);
|
||||
--chat-on-accent: var(--kn-bg);
|
||||
--chat-surface: var(--kn-bg);
|
||||
--chat-line: var(--kn-line-strong);
|
||||
--chat-muted: var(--kn-text-2);
|
||||
--chat-bubble-in: var(--kn-bg-subtle);
|
||||
--chat-bubble-out: var(--kn-grau-bg);
|
||||
--chat-track: var(--kn-spur);
|
||||
display: flex; flex: 1 1 auto; flex-direction: column; gap: var(--kn-sp-2); width: 100%; min-width: 0; min-height: 0; max-width: 760px; margin-inline: auto;
|
||||
}
|
||||
.kn .kn-chat .chat-box { display: flex; flex: 1 1 auto; flex-direction: column; gap: var(--kn-sp-2); min-width: 0; min-height: 0; }
|
||||
/* Der Verlauf füllt die Höhe zwischen Kopf und Eingabe und scrollt in sich; kurze Verläufe stehen unten, bei der Eingabe (Rahmen der Höhe: huelle.css) */
|
||||
.kn .kn-chat .chat-messages { display: flex; flex: 1 1 0; flex-direction: column; gap: 8px; min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: 8px 0; }
|
||||
.kn .kn-chat .chat-messages > :first-child { margin-top: auto; }
|
||||
.kn .kn-chat .chat-messages > .kn-leer { margin: auto 0; padding: 24px 0; }
|
||||
.kn .kn-chat .chat-bubble {
|
||||
align-self: flex-start; max-width: 78%; padding: 8px 12px; border: 0; border-radius: var(--kn-radius-lg);
|
||||
background: var(--chat-bubble-in); color: var(--kn-text); overflow-wrap: anywhere;
|
||||
}
|
||||
.kn .kn-chat .chat-bubble.mine { align-self: flex-end; background: var(--chat-bubble-out); }
|
||||
.kn .kn-chat .chat-meta { display: block; margin-top: 4px; font-size: 12px; color: var(--kn-text-2); }
|
||||
.kn .kn-chat .chat-att-file { border: 0; background: var(--kn-bg); box-shadow: var(--kn-ring); color: var(--kn-text); }
|
||||
.kn .kn-chat .chat-att-file:hover { background: var(--kn-bg-hover); }
|
||||
.kn .kn-chat .chat-audio-bar { background: var(--chat-track); }
|
||||
.kn .kn-chat .chat-att-image:focus-visible, .kn .kn-chat .chat-audio-bar:focus-visible { outline: 2px solid var(--kn-accent); }
|
||||
.kn .kn-chat .chat-bubble.mine .chat-audio-play { background: var(--chat-accent); color: var(--chat-on-accent); }
|
||||
.kn .kn-chat .chat-audio-dl:hover { background: var(--kn-accent-soft); }
|
||||
.kn .kn-chat .chat-composer-note, .kn .kn-chat .chat-tray-item .chat-file-size, .kn .kn-chat .chat-audio.is-error .chat-audio-time { color: var(--kn-rot-fg); }
|
||||
.kn .kn-chat .chat-composer-note.is-info { color: var(--kn-text-2); }
|
||||
.kn .kn-chat .chat-input-row textarea {
|
||||
resize: none; min-height: 46px; max-height: 130px; padding: 11px 14px; border: 0; border-radius: var(--kn-radius-lg);
|
||||
background: var(--kn-bg); color: var(--kn-text); box-shadow: var(--kn-ring); font: inherit; line-height: 1.4;
|
||||
}
|
||||
.kn .kn-chat .chat-input-row textarea:focus { outline: none; box-shadow: 0 0 0 1px var(--kn-accent), 0 0 0 4px var(--kn-accent-ring); }
|
||||
.kn .kn-chat .chat-icon-btn { border: 0; background: var(--kn-bg); color: var(--kn-text); box-shadow: var(--kn-ring); }
|
||||
.kn .kn-chat .chat-icon-btn:hover:not(:disabled) { background: var(--kn-bg-hover); }
|
||||
.kn .kn-chat .chat-icon-btn[aria-expanded="true"], .kn .kn-chat .chat-icon-btn[aria-expanded="true"]:hover:not(:disabled) { background: var(--kn-accent-soft); box-shadow: 0 0 0 1px var(--kn-accent); }
|
||||
.kn .kn-chat .chat-icon-btn:disabled, .kn .kn-chat .chat-send:disabled { opacity: .55; cursor: not-allowed; }
|
||||
.kn .kn-chat .chat-composer:not(.is-disabled) .chat-send:disabled:not(.is-busy) { opacity: 1; cursor: default; } /* „Senden“ bleibt der schwarze Hauptknopf, auch bei leerem Feld; gesperrt (Ansichtsmodus) wirkt er weiter grau */
|
||||
.kn .kn-chat .chat-send.is-busy:disabled { opacity: .8; cursor: progress; }
|
||||
.kn .kn-chat .chat-rec { border-color: var(--kn-danger-ring); background: var(--kn-rot-bg); }
|
||||
.kn .kn-chat .chat-rec .chat-icon-btn { background: transparent; box-shadow: none; }
|
||||
.kn .kn-chat .chat-rec .chat-icon-btn:hover { background: var(--kn-accent-soft); }
|
||||
.kn .kn-chat .chat-rec-stop, .kn .kn-chat .chat-rec-wave { color: var(--kn-danger); }
|
||||
.kn .kn-chat .chat-rec-dot { background: var(--kn-danger); }
|
||||
.kn .kn-chat .chat-menu { border: 0; border-radius: var(--kn-radius-lg); box-shadow: var(--kn-shadow-pop); background: var(--kn-bg); }
|
||||
.kn .kn-chat .chat-menu button { border-radius: var(--kn-radius); color: var(--kn-text); }
|
||||
.kn .kn-chat .chat-menu button:hover, .kn .kn-chat .chat-menu button:focus-visible { background: var(--kn-bg-hover); }
|
||||
.kn .kn-chat .chat-menu button:focus-visible { outline: 2px solid var(--kn-accent); outline-offset: -2px; }
|
||||
.kn .kn-chat .chat-tray-item { border: 1px solid transparent; background: var(--kn-bg-hover); }
|
||||
.kn .kn-chat .chat-tray-item.is-error { border-color: var(--kn-danger); background: var(--kn-rot-bg); }
|
||||
.kn .kn-chat.chat-drop-active::after, .kn .kn-chat .chat-box.chat-drop-active::after { background: var(--kn-ablage-hell); color: var(--kn-text); border-color: var(--kn-text-3); }
|
||||
.kn .kn-chat .kn-chat-composer { flex: none; margin-top: 0; }
|
||||
.kn .kn-chat .kn-chat-kasten { position: relative; }
|
||||
.kn .kn-chat .kn-chat-verlauf { scroll-behavior: auto; }
|
||||
.kn .kn-chat .chat-composer { margin-top: 8px; }
|
||||
|
||||
/* ------------------------------------------------------------------ Verträge (Rahmen, Reiter, Rechnungen) --- */
|
||||
.kn-reiter-inhalt { min-width: 0; }
|
||||
.kn-reiter-abo { min-width: 0; }
|
||||
.kn-kuendigen { margin: 40px 0 0; padding-top: 16px; border-top: 1px solid var(--kn-line); font-size: 14px; }
|
||||
.kn-rechnungen-liste { margin: 0; }
|
||||
.kn-rechnungen { margin: 0; }
|
||||
.kn-rechnungen tr.is-bezahlt td { color: var(--kn-text-2); }
|
||||
.kn-rechnung-nr { font-weight: 500; }
|
||||
.kn-rechnung-projekt { display: block; margin-top: 2px; font-size: 12.5px; font-weight: 400; color: var(--kn-text-2); overflow-wrap: anywhere; }
|
||||
.kn-rechnung-aktionen { white-space: nowrap; }
|
||||
.kn-num { font-variant-numeric: tabular-nums; }
|
||||
.kn-auftraege-rumpf { min-width: 0; }
|
||||
|
||||
/* Abo, Vertrag */
|
||||
.kn-vertrag-kopf { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
||||
.kn-vertrag-kopf .kn-abschnitt { margin: 0 0 12px; }
|
||||
.kn-vertrag > .kn-hinweis { margin-bottom: 12px; }
|
||||
.kn-widerruf { margin: 12px 0 0; font-size: 14px; }
|
||||
.kn-gutschrift { margin: 24px 0 0; }
|
||||
.kn-gutschrift-link { gap: 8px; }
|
||||
.kn-wechsel { margin-top: 32px; padding-top: 24px; border-top: 1px solid var(--kn-line); }
|
||||
.kn-abo-auswahl { display: flex; flex-direction: column; gap: 16px; }
|
||||
.kn-abo-karten { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 24px; }
|
||||
.kn-abo-karte { padding: 16px 0; border-top: 1px solid var(--kn-line); }
|
||||
.kn-abo-karte .kn-knopf-primaer { margin-top: 8px; }
|
||||
|
||||
/* Stunden */
|
||||
.kn-statistik { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; margin: 0 0 16px; }
|
||||
.kn-stat { padding: 12px 0; border-top: 1px solid var(--kn-line); }
|
||||
.kn-stat strong { display: block; font-size: 20px; line-height: 28px; font-weight: 700; letter-spacing: -.01em; }
|
||||
.kn-stat span { font-size: 13px; color: var(--kn-text-2); }
|
||||
.kn-vorschuss { color: var(--kn-violett-fg); }
|
||||
|
||||
/* E-Mail-Speicher */
|
||||
.kn-speicher-platz { margin-top: 16px; }
|
||||
.kn-stepper { display: flex; align-items: center; gap: 12px; margin: 12px 0; }
|
||||
.kn-stepper-zahl { font-variant-numeric: tabular-nums; }
|
||||
.kn-stepper .kn-knopf { min-width: 36px; padding: 0 12px; }
|
||||
.kn-speicher-summe { margin: 4px 0 12px; color: var(--kn-text-2); font-variant-numeric: tabular-nums; }
|
||||
.kn-speicher-zusammenfassung { margin-top: 16px; }
|
||||
|
||||
/* Bestellen und Wechsel: Zusammenfassung, Beginn-Auswahl, Haken */
|
||||
.kn-beginn { margin: 16px 0; padding: 0; border: 0; min-width: 0; }
|
||||
.kn-beginn legend { padding: 0; margin: 0 0 4px; font-weight: 600; }
|
||||
.kn-beginn .kn-check, .kn-panel > .kn-check { display: flex; align-items: flex-start; margin: 8px 0; color: var(--kn-text); }
|
||||
.kn-panel .kn-check { align-items: flex-start; }
|
||||
.kn-panel .kn-check input { margin-top: 3px; flex: 0 0 auto; }
|
||||
|
||||
/* Übernahmegutschrift */
|
||||
.kn-tk-einleitung { margin: 0 0 16px; }
|
||||
.kn-tk-form { display: flex; flex-direction: column; gap: 12px; margin: 16px 0 24px; max-width: 640px; }
|
||||
.kn-tk-feld { display: flex; flex-direction: column; gap: 6px; min-width: 0; border: 0; padding: 0; margin: 0; }
|
||||
.kn-tk-feld label, .kn-tk-feld legend { padding: 0; font-weight: 500; }
|
||||
.kn-tk-feld small { font-weight: 400; color: var(--kn-text-2); }
|
||||
.kn-tk-feld input:not([type="checkbox"]):not([type="file"]) { height: 36px; padding: 0 10px; border: 0; border-radius: var(--kn-radius); background: var(--kn-bg); box-shadow: var(--kn-ring); font: inherit; outline: none; }
|
||||
.kn-tk-feld input:focus { box-shadow: 0 0 0 1px var(--kn-accent), 0 0 0 4px var(--kn-accent-ring); }
|
||||
.kn-tk-zeile { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; }
|
||||
.kn-tk-leistungen { display: flex; flex-wrap: wrap; gap: 8px 20px; }
|
||||
.kn-tk-leistungen label { display: inline-flex; align-items: center; gap: 6px; font-weight: 400; cursor: pointer; }
|
||||
.kn-tk-bedingungen summary { cursor: pointer; font-weight: 500; }
|
||||
.kn-tk-bedingungen ol { margin: 8px 0 0; padding-left: 20px; color: var(--kn-text-2); font-size: 13px; }
|
||||
.kn-tk-bedingungen li { margin: 4px 0; }
|
||||
.kn-tk-liste { display: flex; flex-direction: column; }
|
||||
.kn-tk-antrag { padding: 16px 0; border-top: 1px solid var(--kn-line); }
|
||||
.kn-tk-kopf { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
||||
.kn-tk-frage { margin: 8px 0; padding: 12px; border-radius: var(--kn-radius); background: var(--kn-bg-subtle); }
|
||||
.kn-tk-antwort { display: flex; flex-direction: column; gap: 8px; max-width: 520px; }
|
||||
|
||||
/* Passwort ändern */
|
||||
.kn-pw { max-width: 420px; }
|
||||
.kn-pw-form { display: flex; flex-direction: column; gap: 16px; }
|
||||
.kn-pw-erfolg { display: flex; flex-direction: column; align-items: flex-start; gap: 8px; }
|
||||
.kn-pw-erfolg strong { color: var(--kn-gruen-fg); font-size: 16px; }
|
||||
.kn-pw-erfolg p { margin: 0; color: var(--kn-text-2); }
|
||||
|
||||
/* ------------------------------------------------------------------ Aufträge im Reiter Rechnungen (Alt-Modul auftrag-portal.js) --- */
|
||||
.kn .ab-portal {
|
||||
--ab-karte: var(--kn-bg); --ab-text: var(--kn-text); --ab-weich: var(--kn-text-2); --ab-linie: var(--kn-line); --ab-box: var(--kn-bg-subtle);
|
||||
--ab-knopf: var(--kn-accent); --ab-knopf-text: var(--kn-bg); --ab-fokus: var(--kn-accent);
|
||||
--ab-warn-bg: var(--kn-orange-bg); --ab-warn-text: var(--kn-orange-fg); --ab-ok-bg: var(--kn-gruen-bg); --ab-ok-text: var(--kn-gruen-fg);
|
||||
--ab-rot-bg: var(--kn-rot-bg); --ab-rot-text: var(--kn-rot-fg); --ab-muted-bg: var(--kn-grau-bg); --ab-muted-text: var(--kn-grau-fg); --ab-fehler: var(--kn-danger);
|
||||
margin: 0 0 28px; color: var(--ab-text);
|
||||
}
|
||||
.kn .ab-portal h2 { margin: 0 0 6px; }
|
||||
.kn .ab-portal .ab-karte { margin: 12px 0 0; padding: 16px 0; border-top: 1px solid var(--ab-linie); overflow-wrap: anywhere; }
|
||||
.kn .ab-portal .ab-karte-kopf { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; }
|
||||
.kn .ab-portal .ab-karte-kopf .ab-pille { margin: 0; flex: none; }
|
||||
.kn .ab-portal .ab-karte-kopf p { margin: 0; }
|
||||
.kn .ab-portal .ab-karte-nummer strong { font-weight: 600; font-variant-numeric: tabular-nums; }
|
||||
.kn .ab-portal .ab-karte-titel { margin-top: 2px; }
|
||||
.kn .ab-portal .ab-karte-meta { margin-top: 2px; font-size: 13px; color: var(--ab-weich); }
|
||||
.kn .ab-portal .ab-karte-aktionen { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 10px; }
|
||||
.kn .ab-portal .ab-karte-aktionen a {
|
||||
display: inline-flex; align-items: center; min-height: 36px; padding: 0 12px; border-radius: var(--kn-radius); box-shadow: var(--kn-ring);
|
||||
color: var(--ab-text); text-decoration: none; font-weight: 500; font-size: 14px;
|
||||
}
|
||||
.kn .ab-portal .ab-details > summary { cursor: pointer; display: flex; align-items: center; min-height: 36px; color: var(--ab-text); font-weight: 600; }
|
||||
.kn .ab-portal .ab-details[open] > summary { margin-bottom: 4px; }
|
||||
.kn .ab-portal .ab-abschnitt { margin: 0; }
|
||||
.kn .ab-portal .ab-zusammenfassung { margin: 0; }
|
||||
.kn .ab-portal .ab-link { text-underline-offset: 3px; }
|
||||
.kn .ab-portal .ab-pille {
|
||||
display: inline-block; margin: 0 0 12px; padding: 2px 8px; border-radius: 4px; font-size: 12px; font-weight: 500; line-height: 20px;
|
||||
background: var(--ab-muted-bg); color: var(--ab-muted-text);
|
||||
}
|
||||
.kn .ab-portal .ab-pille-warn { background: var(--ab-warn-bg); color: var(--ab-warn-text); }
|
||||
.kn .ab-portal .ab-pille-ok { background: var(--ab-ok-bg); color: var(--ab-ok-text); }
|
||||
.kn .ab-portal .ab-pille-rot { background: var(--ab-rot-bg); color: var(--ab-rot-text); }
|
||||
.kn .ab-portal .ab-text { white-space: pre-line; margin: 0 0 16px; }
|
||||
.kn .ab-portal .ab-h2 { font-size: 12px; text-transform: uppercase; letter-spacing: .06em; font-weight: 600; color: var(--ab-weich); margin: 24px 0 8px; }
|
||||
.kn .ab-portal .ab-liste { margin: 0 0 8px; padding: 0; }
|
||||
.kn .ab-portal .ab-zeile { display: grid; grid-template-columns: 9rem 1fr; gap: 4px 12px; padding: 8px 0; border-top: 1px solid var(--ab-linie); }
|
||||
.kn .ab-portal .ab-zeile dt { color: var(--ab-weich); }
|
||||
.kn .ab-portal .ab-zeile dd { margin: 0; }
|
||||
.kn .ab-portal .ab-gesamt { display: flex; justify-content: space-between; gap: 12px; margin: 12px 0 8px; padding-top: 12px; border-top: 2px solid var(--ab-text); }
|
||||
.kn .ab-portal .ab-gesamt strong { font-weight: 700; font-variant-numeric: tabular-nums; }
|
||||
.kn .ab-portal .ab-hinweis { margin: 4px 0 0; font-size: 13px; color: var(--ab-weich); }
|
||||
.kn .ab-portal .ab-links { list-style: none; margin: 0 0 16px; padding: 0; }
|
||||
.kn .ab-portal .ab-links a { display: flex; align-items: center; min-height: 36px; line-height: 1.35; padding: 4px 0; font-weight: 600; }
|
||||
.kn .ab-portal .ab-bestaetigt { background: var(--ab-ok-bg); color: var(--ab-ok-text); border-radius: var(--kn-radius); padding: 12px 16px; margin: 0 0 16px; }
|
||||
.kn .ab-portal .ab-bestaetigt p { margin: 2px 0; }
|
||||
.kn .ab-portal .ab-bestaetigt:focus-visible { outline: 2px solid var(--ab-fokus); outline-offset: 2px; }
|
||||
.kn .ab-portal .ab-tabelle { width: 100%; border-collapse: collapse; font-size: 14px; font-variant-numeric: tabular-nums; }
|
||||
.kn .ab-portal .ab-tabelle th { text-align: left; font-weight: 500; color: var(--ab-weich); padding: 6px 8px 6px 0; border-bottom: 1px solid var(--ab-linie); }
|
||||
.kn .ab-portal .ab-tabelle td { padding: 8px 8px 8px 0; border-bottom: 1px solid var(--ab-linie); vertical-align: top; }
|
||||
.kn .ab-portal .ab-tabelle th:nth-child(n+2), .kn .ab-portal .ab-tabelle td:nth-child(n+2) { text-align: right; white-space: nowrap; }
|
||||
.kn .ab-portal .ab-bestaetigen { margin-top: 24px; padding-top: 8px; border-top: 1px solid var(--ab-linie); }
|
||||
.kn .ab-portal .ab-feldgruppe { border: 0; margin: 0; padding: 0; min-width: 0; }
|
||||
.kn .ab-portal .ab-feldgruppe legend { font-weight: 600; padding: 0; margin: 12px 0 4px; }
|
||||
.kn .ab-portal .ab-haken { display: flex; gap: 12px; align-items: flex-start; padding: 10px 0; min-height: 44px; font-size: 15px; cursor: pointer; }
|
||||
.kn .ab-portal .ab-haken input { flex: 0 0 22px; width: 22px; height: 22px; margin: 1px 0 0; accent-color: var(--ab-knopf); }
|
||||
.kn .ab-portal .ab-frei { color: var(--ab-weich); font-size: .85em; }
|
||||
.kn .ab-portal .ab-fehler { margin: 8px 0; color: var(--ab-fehler); font-weight: 600; }
|
||||
.kn .ab-portal .ab-fehler[hidden] { display: none; }
|
||||
.kn .ab-portal .ab-knopf {
|
||||
display: inline-block; min-height: 36px; padding: 0 16px; border: 0; border-radius: var(--kn-radius); cursor: pointer;
|
||||
background: var(--ab-knopf); color: var(--ab-knopf-text); font: inherit; font-weight: 600; box-shadow: var(--kn-shadow-btn);
|
||||
}
|
||||
.kn .ab-portal .ab-knopf:disabled { opacity: .45; cursor: default; }
|
||||
.kn .ab-portal .ab-knopf:focus-visible, .kn .ab-portal .ab-link:focus-visible { outline: 2px solid var(--ab-fokus); outline-offset: 2px; }
|
||||
.kn .ab-portal .ab-notiz, .kn .ab-portal .ab-gesperrt { margin: 12px 0 0; font-size: 13px; color: var(--ab-weich); }
|
||||
|
||||
/* ------------------------------------------------------------------ P7 Bestellseiten (Abo, Wechsel, Speicher, Gutschrift) --- */
|
||||
/* Muster der Spec 5.6: Block „Was du bestellst“, eine Linkzeile, ein Haken nur für Privatkunden, der Knopf, die Kundenart-Zeile.
|
||||
Alles liegt ohne Klappe und ohne Scrollbereich hintereinander. */
|
||||
.kn-bestellen { max-width: 640px; }
|
||||
.kn-block { margin: 0 0 16px; }
|
||||
.kn-block-zeile { display: grid; grid-template-columns: 9rem minmax(0, 1fr); gap: 4px 16px; padding: 10px 0; border-top: 1px solid var(--kn-line); }
|
||||
.kn-block-zeile:last-child { border-bottom: 1px solid var(--kn-line); }
|
||||
.kn-block dt { color: var(--kn-text-2); }
|
||||
.kn-block dd { margin: 0; min-width: 0; overflow-wrap: anywhere; font-variant-numeric: tabular-nums; }
|
||||
.kn-linkzeile { margin: 0 0 12px; font-size: 13px; line-height: 20px; color: var(--kn-text-2); }
|
||||
.kn-linkzeile a, .kn-kundenart-zeile a { color: var(--kn-text); text-decoration: underline; text-decoration-color: var(--kn-line-strong); text-underline-offset: 3px; }
|
||||
.kn-linkzeile a:hover, .kn-kundenart-zeile a:hover { text-decoration-color: currentColor; }
|
||||
.kn-kundenart-zeile { margin: 8px 0 0; font-size: 13px; line-height: 20px; color: var(--kn-text-2); }
|
||||
.kn-bestellen .kn-check { display: flex; align-items: flex-start; gap: 8px; min-height: 44px; margin: 0 0 12px; color: var(--kn-text); }
|
||||
.kn-bestellen .kn-check input { flex: 0 0 auto; width: 20px; height: 20px; margin: 2px 0 0; }
|
||||
.kn-bestellen .kn-aktionen { margin-top: 0; }
|
||||
.kn-sperrzeile { margin: 0 0 12px; font-size: 13px; line-height: 20px; color: var(--kn-danger); }
|
||||
.kn-speicher-warnung { margin: 0 0 12px; font-size: 13px; line-height: 20px; color: var(--kn-orange-fg); }
|
||||
.kn-speicher-warnung a { color: inherit; text-decoration: underline; text-underline-offset: 3px; }
|
||||
.kn-block dd.kn-fehler { color: var(--kn-danger); }
|
||||
.kn-einzelheiten { margin: 20px 0 0; }
|
||||
.kn-einzelheiten > h3 { margin: 0 0 8px; font-size: 14px; line-height: 20px; font-weight: 600; color: var(--kn-text-2); }
|
||||
.kn-einzelheiten .kn-block { color: var(--kn-text-2); }
|
||||
.kn-gutschrift-bedingungen { max-width: 640px; }
|
||||
|
||||
/* ------------------------------------------------------------------ Handy --- */
|
||||
@media (max-width: 880px), (pointer: coarse) and (max-height: 500px) {
|
||||
.kn-projekt { grid-template-columns: minmax(0, 1fr); gap: 16px; padding: 16px 0; }
|
||||
.kn-vorschau { max-width: 560px; }
|
||||
.kn-fakten { grid-template-columns: 1fr; gap: 0; }
|
||||
.kn-fakten dd { margin-bottom: 6px; }
|
||||
.kn-zusammenfassung td { display: block; width: auto; padding: 6px 0; border-top: 0; }
|
||||
.kn-zusammenfassung td:first-child { width: auto; padding-top: 12px; border-top: 1px solid var(--kn-line); }
|
||||
.kn-zusammenfassung tr:last-child td { border-bottom: 0; }
|
||||
.kn-abo-karten { grid-template-columns: minmax(0, 1fr); gap: 8px; }
|
||||
.kn-statistik { grid-template-columns: minmax(0, 1fr); gap: 0; }
|
||||
.kn-zeilenlink { display: inline-flex; align-items: center; min-height: 44px; }
|
||||
/* Pflicht- und Zahlungslinks (E-Fix F3): Verträge hier kündigen, Vertrag widerrufen, Vertragsbedingungen, Zahlen in der Zeile „Rechnung offen“ */
|
||||
.kn-kuendigen a, .kn-widerruf a, .kn-links a, .kn-rechnung-offen a { display: inline-flex; align-items: center; min-height: 44px; }
|
||||
.kn-az-info { margin-top: 0; }
|
||||
.kn-az-info a { display: block; padding: 12px 0; }
|
||||
.kn-az-info + .kn-az-balken { margin-top: 0; }
|
||||
.kn-rechnung-offen { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
||||
.kn-rechnung-offen .kn-trenner { display: none; }
|
||||
.kn-rechnung-offen a { flex: none; font-weight: 600; }
|
||||
.kn-pw { max-width: none; }
|
||||
.kn-tk-feld input:not([type="checkbox"]):not([type="file"]) { height: 44px; font-size: 16px; }
|
||||
.kn-stepper .kn-knopf { min-width: 44px; }
|
||||
[data-abschnitt="aktivitaet"] { scroll-margin-top: calc(var(--kn-banner-h) + var(--kn-kopf-h) + env(safe-area-inset-top) + 8px); }
|
||||
/* Bestellseiten (F1): Kerninfo und Knopf müssen im nutzbaren Bereich zwischen Kopf und Leiste zusammen stehen (375 x 667: gut 550 px).
|
||||
Darum läuft die Beschriftung in der Zeile mit (Label: Text) statt eine eigene Zeile zu brauchen, und der Abstand ist knapp. */
|
||||
.kn-bestellen > h2 { margin: 0 0 6px; font-size: 17px; line-height: 24px; font-weight: 600; }
|
||||
.kn-block { margin-bottom: 10px; }
|
||||
.kn-block-zeile { display: block; padding: 5px 0; }
|
||||
.kn-block dt, .kn-block dd { display: inline; }
|
||||
.kn-block dt { font-weight: 600; color: var(--kn-text); }
|
||||
.kn-block dt::after { content: ": "; }
|
||||
.kn-linkzeile { margin-bottom: 8px; }
|
||||
.kn-bestellen .kn-check { margin-bottom: 8px; }
|
||||
.kn-sperrzeile { margin-bottom: 8px; }
|
||||
.kn-bestellen .kn-knopf { width: 100%; min-height: 44px; }
|
||||
.kn .kn-chat .chat-bubble { max-width: 88%; }
|
||||
.kn .kn-chat .chat-input-row textarea { font-size: 16px; }
|
||||
.kn .ab-portal .ab-karte-aktionen a, .kn .ab-portal .ab-knopf, .kn .ab-portal .ab-details > summary, .kn .ab-portal .ab-links a { min-height: 44px; }
|
||||
.kn .ab-portal .ab-knopf { width: 100%; }
|
||||
.kn .ab-portal .ab-zeile { grid-template-columns: 1fr; gap: 0; }
|
||||
/* Rechnungen als gestapelte Zeilen */
|
||||
.kn-rechnungen thead, .kn .ab-portal .ab-tabelle thead { position: absolute; width: 1px; height: 1px; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
|
||||
.kn-rechnungen table, .kn-rechnungen tbody, .kn-rechnungen tr, .kn-rechnungen td,
|
||||
.kn .ab-portal .ab-tabelle, .kn .ab-portal .ab-tabelle tbody, .kn .ab-portal .ab-tabelle tr, .kn .ab-portal .ab-tabelle td { display: block; }
|
||||
.kn-rechnungen tr, .kn .ab-portal .ab-tabelle tr { padding: 8px 0; border-bottom: 1px solid var(--kn-line); }
|
||||
.kn-rechnungen td, .kn .ab-portal .ab-tabelle td { display: flex; justify-content: space-between; gap: 12px; padding: 2px 0; border: 0; text-align: right; white-space: normal; }
|
||||
.kn-rechnungen td:first-child, .kn .ab-portal .ab-tabelle td:first-child { display: block; text-align: left; font-weight: 600; }
|
||||
.kn-rechnungen td::before, .kn .ab-portal .ab-tabelle td:nth-child(n+2)::before { content: attr(data-label); color: var(--kn-text-2); text-align: left; }
|
||||
.kn-rechnungen td:first-child::before { content: none; }
|
||||
.kn-rechnungen .kn-rechnung-aktionen { justify-content: flex-start; padding-top: 6px; }
|
||||
.kn-rechnungen .kn-rechnung-aktionen::before { content: none; }
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/* Kundenansicht neu: ein einziger Zugang zur Schnittstelle des Portals.
|
||||
Fehler kommen immer als ApiFehler (mit Status, Code, Text, Feld und dem ganzen Body), Netzfehler mit Status 0. */
|
||||
|
||||
const TEXT_NETZ = 'Keine Verbindung. Bitte versuch es gleich noch einmal.'
|
||||
const TEXT_502 = 'Server nicht erreichbar. Bitte kurz warten und noch einmal versuchen.'
|
||||
|
||||
// Sitzung beendet (Passwort auf einem anderen Gerät geändert): nur einmal zur Anmeldung wechseln
|
||||
let sitzungBeendetGemeldet = false
|
||||
|
||||
export class ApiFehler extends Error {
|
||||
constructor(message, { status = 0, code = '', field = '', body = {} } = {}) {
|
||||
super(message)
|
||||
this.name = 'ApiFehler'
|
||||
this.status = status
|
||||
this.code = code
|
||||
this.field = field
|
||||
this.body = body
|
||||
}
|
||||
}
|
||||
|
||||
/** Aufruf der Schnittstelle. `roh` ist ein Rohdaten-Body (Blob, ArrayBuffer …), der unverändert als application/octet-stream geht. */
|
||||
export async function api(pfad, { method = 'GET', body, roh } = {}) {
|
||||
const headers = {}
|
||||
let inhalt
|
||||
if (roh) {
|
||||
headers['Content-Type'] = 'application/octet-stream'
|
||||
inhalt = roh
|
||||
} else if (body !== undefined && body !== null) {
|
||||
headers['Content-Type'] = 'application/json'
|
||||
inhalt = JSON.stringify(body)
|
||||
}
|
||||
let antwort
|
||||
try {
|
||||
antwort = await fetch(pfad, { method, credentials: 'same-origin', headers, body: inhalt })
|
||||
} catch {
|
||||
// Netzfehler (offline, Server nicht erreichbar): deutscher Text statt „Failed to fetch“; Status 0 = nie eine Antwort bekommen
|
||||
throw new ApiFehler(TEXT_NETZ, { status: 0 })
|
||||
}
|
||||
const daten = await antwort.json().catch(() => ({}))
|
||||
if (antwort.ok) return daten
|
||||
const antwortBody = daten !== null && typeof daten === 'object' && !Array.isArray(daten) ? daten : {}
|
||||
const message = typeof antwortBody.error === 'string' && antwortBody.error ? antwortBody.error : (antwort.status === 502 ? TEXT_502 : `Fehler ${antwort.status}`)
|
||||
const fehler = new ApiFehler(message, {
|
||||
status: antwort.status,
|
||||
code: typeof antwortBody.code === 'string' ? antwortBody.code : '',
|
||||
field: typeof antwortBody.field === 'string' ? antwortBody.field : '',
|
||||
body: antwortBody,
|
||||
})
|
||||
if (fehler.code === 'session_revoked' && !sitzungBeendetGemeldet) {
|
||||
sitzungBeendetGemeldet = true
|
||||
window.location.href = `/login.html?error=${encodeURIComponent(message)}`
|
||||
}
|
||||
throw fehler
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
/* Kundenansicht neu: „Was wir zuletzt erledigt haben“ als Zeitleiste (Variante C, Pilot-Feedback 17.10.).
|
||||
Je Auftrag aus GET /api/activity: Überschrift mit Status-Pille, schmaler Fortschrittsbalken, bei Rückfrage der Hinweis zum Chat und eine
|
||||
kompakte Zeitleiste der Einträge. Die Logik (Reihenfolge, Gruppierung, Kürzen, Fortschritt, Kurztexte) sind reine Funktionen; gezeichnet wird
|
||||
mit h() und Textknoten, nie als HTML. Kein DOM-Zugriff beim Import.
|
||||
Antwortform laut server/routes/customerData.js und services/customerProjectView.js:
|
||||
{ number, topic, status: { label, tone, step }, createdAt, entries: [{ kind, label, date: 'JJJJ-MM-TT', minutes, person, text | changes }] }. */
|
||||
|
||||
import { h } from './dom.js'
|
||||
import { K, berlinTag, zuAnsicht } from './ansicht-hilfen.js'
|
||||
import { AUFTRAG_SCHRITTE, statusTon } from './website-details.js'
|
||||
|
||||
/** Die vier Abschnitte des Balkens (dieselben Namen wie der Fortschritt der Projektdetails). */
|
||||
export const STUFEN = AUFTRAG_SCHRITTE
|
||||
/** So viele Aufträge stehen offen, der Rest hinter „Ältere Aufträge anzeigen“. */
|
||||
export const MAX_AUFTRAEGE = 5
|
||||
/** So viele Zeilen der Zeitleiste stehen offen, der Rest hinter „mehr anzeigen“. */
|
||||
export const MAX_ZEILEN = 3
|
||||
const MAX_KURZTEXT = 90
|
||||
|
||||
/* ---------------------------------------------------------------- Fortschritt --- */
|
||||
|
||||
/** Die vier Abschnitte des Balkens: erreicht bis zum aktuellen Stand; Stufe 0 (pausiert, storniert) und Unbekanntes erreichen nichts. */
|
||||
export function fortschritt(status) {
|
||||
const schritt = Number(status?.step)
|
||||
const gueltig = Number.isInteger(schritt) && schritt >= 1 && schritt <= STUFEN.length
|
||||
return STUFEN.map((name, i) => ({ name, erreicht: gueltig && i < schritt, aktuell: gueltig && i === schritt - 1 }))
|
||||
}
|
||||
|
||||
/** Braucht der Auftrag eine Info vom Kunden? Der Server meldet das mit dem Ton „waiting“ (Ticketstatus „awaiting“, „Wartet auf deine Rückmeldung“). */
|
||||
export const braucheInfo = (auftrag) => auftrag?.status?.tone === 'waiting'
|
||||
|
||||
/* ---------------------------------------------------------- Reihenfolge, Kürzen --- */
|
||||
|
||||
const eintraegeVon = (auftrag) => (Array.isArray(auftrag?.entries) ? auftrag.entries.filter((e) => e && typeof e === 'object') : [])
|
||||
|
||||
// 'JJJJ-MM-TT' (auch als Anfang eines ISO-Zeitpunkts) → { j, m, t } ohne Zeitzonen-Umrechnung; sonst null
|
||||
function tagTeile(wert) {
|
||||
const treffer = /^(\d{4})-(\d{2})-(\d{2})/.exec(String(wert ?? ''))
|
||||
if (!treffer) return null
|
||||
const [j, m, t] = [Number(treffer[1]), Number(treffer[2]), Number(treffer[3])]
|
||||
return m >= 1 && m <= 12 && t >= 1 && t <= 31 ? { j, m, t } : null
|
||||
}
|
||||
|
||||
const zweistellig = (zahl) => String(zahl).padStart(2, '0')
|
||||
const isoTag = (wert) => { const tag = tagTeile(wert); return tag ? `${tag.j}-${zweistellig(tag.m)}-${zweistellig(tag.t)}` : '' }
|
||||
|
||||
/** Zeitpunkt der jüngsten Aktivität (ms): der späteste Eintrag oder die Anlage des Auftrags; 0, wenn nichts lesbar ist. */
|
||||
export function letzteAktivitaet(auftrag) {
|
||||
const zeiten = [Date.parse(String(auftrag?.createdAt ?? '')), ...eintraegeVon(auftrag).map((e) => { const tag = isoTag(e.date); return tag ? Date.parse(`${tag}T00:00:00Z`) : NaN })]
|
||||
return Math.max(0, ...zeiten.filter((zeit) => Number.isFinite(zeit)))
|
||||
}
|
||||
|
||||
// 0 = laufend (auch pausiert), 1 = erledigt, 2 = storniert oder abgebrochen
|
||||
const rang = (auftrag) => (auftrag?.status?.tone === 'done' ? 1 : auftrag?.status?.tone === 'stopped' ? 2 : 0)
|
||||
|
||||
/** Laufende Aufträge zuerst, dann erledigte, Storniertes zuletzt; unter den laufenden stehen die mit Rückfrage (braucheInfo) vor den übrigen, damit eine Frage
|
||||
an den Kunden nie hinter „Ältere Aufträge anzeigen“ verschwindet. Innerhalb jeder Gruppe die jüngste Aktivität vorn, bei Gleichstand die Reihenfolge des Servers. */
|
||||
export function sortiereAuftraege(tickets) {
|
||||
const liste = Array.isArray(tickets) ? tickets.filter((t) => t && typeof t === 'object') : []
|
||||
return liste
|
||||
.map((auftrag, i) => ({ auftrag, i, rang: rang(auftrag), frage: braucheInfo(auftrag) ? 0 : 1, zeit: letzteAktivitaet(auftrag) }))
|
||||
.sort((a, b) => a.rang - b.rang || a.frage - b.frage || b.zeit - a.zeit || a.i - b.i)
|
||||
.map((e) => e.auftrag)
|
||||
}
|
||||
|
||||
/** Die ersten n sichtbar, der Rest getrennt; die Eingabe bleibt unverändert. */
|
||||
export function kuerze(liste, max) {
|
||||
const alle = Array.isArray(liste) ? liste : []
|
||||
return { sichtbar: alle.slice(0, max), rest: alle.slice(max) }
|
||||
}
|
||||
|
||||
/** Sortierte Aufträge: höchstens max offen, der Rest als „älter“. */
|
||||
export function gruppiereAuftraege(tickets, max = MAX_AUFTRAEGE) {
|
||||
const { sichtbar, rest } = kuerze(sortiereAuftraege(tickets), max)
|
||||
return { sichtbar, aeltere: rest }
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- Zeitleiste --- */
|
||||
|
||||
/** 'TT.MM.' für einen Tag 'JJJJ-MM-TT'; fällt er nicht ins laufende Jahr, steht 'TT.MM.JJ' da. Unlesbares ergibt ''. */
|
||||
export function datumKurz(wert, jetzt = new Date()) {
|
||||
const tag = tagTeile(wert)
|
||||
if (!tag) return ''
|
||||
const laufend = berlinTag(jetzt)?.j ?? jetzt.getUTCFullYear()
|
||||
const jahr = tag.j === laufend ? '' : zweistellig(tag.j % 100)
|
||||
return `${zweistellig(tag.t)}.${zweistellig(tag.m)}.${jahr}`
|
||||
}
|
||||
|
||||
// erste nichtleere Zeile, Leerraum zusammengezogen
|
||||
const ersteZeile = (text) => String(text ?? '').split(/\r?\n/).map((zeile) => zeile.replace(/\s+/g, ' ').trim()).find(Boolean) ?? ''
|
||||
|
||||
function kuerzeText(text, max) {
|
||||
if (text.length <= max) return text
|
||||
const schnitt = text.slice(0, max)
|
||||
const leer = schnitt.lastIndexOf(' ')
|
||||
return `${(leer > max * 0.6 ? schnitt.slice(0, leer) : schnitt).replace(/[\s,;:.–-]+$/, '')}…`
|
||||
}
|
||||
|
||||
/** Kurzer Text einer Zeile: die erste Änderung (mit „und n weitere“), sonst der freie Text der Arbeit, sonst die Art der Arbeit. Nie lang, nie ein Name. */
|
||||
export function eintragsKurztext(eintrag) {
|
||||
const aenderungen = Array.isArray(eintrag?.changes) ? eintrag.changes.map(ersteZeile).filter(Boolean) : []
|
||||
if (aenderungen.length) {
|
||||
const rest = aenderungen.length - 1
|
||||
const erste = kuerzeText(aenderungen[0], MAX_KURZTEXT)
|
||||
return rest ? `${erste} und ${rest} weitere ${rest === 1 ? 'Änderung' : 'Änderungen'}` : erste
|
||||
}
|
||||
return kuerzeText(ersteZeile(eintrag?.text), MAX_KURZTEXT) || ersteZeile(eintrag?.label) || 'Arbeit am Projekt'
|
||||
}
|
||||
|
||||
/** Zeilen der Zeitleiste eines Auftrags, neueste zuerst; Einträge ohne lesbares Datum stehen unten. */
|
||||
export function zeitleistenZeilen(auftrag, jetzt = new Date()) {
|
||||
return eintraegeVon(auftrag)
|
||||
.map((e, i) => ({ i, iso: isoTag(e.date), datum: datumKurz(e.date, jetzt), text: eintragsKurztext(e) }))
|
||||
.sort((a, b) => (a.iso === b.iso ? a.i - b.i : !a.iso ? 1 : !b.iso ? -1 : b.iso.localeCompare(a.iso)))
|
||||
.map(({ datum, iso, text }) => ({ datum, iso, text }))
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------ DOM --- */
|
||||
|
||||
const LEER = 'Es wurden noch keine Arbeiten dokumentiert.'
|
||||
const KEINE_EINTRAEGE = 'Noch keine Arbeiten dokumentiert.'
|
||||
|
||||
/** Knopf, der den nächsten Wrapper [data-einklappbar] auf- und zuklappt (is-open); die Zeilen dahinter tragen [data-extra]. Die CSS-Regeln dazu gelten nur für
|
||||
direkte Kinder (ansichten.css, .kn-az-…): die allgemeine Regel .kn-einklappbar würde bei verschachtelten Wrappern auch offene Zeilen verbergen. */
|
||||
function umschalter({ attribut, klasse, mehr, weniger, name, steuert }) {
|
||||
// name: Zusatz für den Namen des Knopfes („mehr anzeigen zu „Titel““, der sichtbare Text steht vorn im Namen); steuert: Id der Liste, die er auf- und zuklappt
|
||||
const nameFuer = (text) => (name ? `${text} zu „${name}“` : null)
|
||||
const knopf = h('button', { type: 'button', class: klasse, [attribut]: true, 'aria-expanded': 'false', 'aria-label': nameFuer(mehr), 'aria-controls': steuert || null }, mehr)
|
||||
knopf.addEventListener('click', () => {
|
||||
const offen = knopf.closest('[data-einklappbar]').classList.toggle('is-open')
|
||||
knopf.textContent = offen ? weniger : mehr
|
||||
knopf.setAttribute('aria-expanded', String(offen))
|
||||
if (name) knopf.setAttribute('aria-label', nameFuer(offen ? weniger : mehr))
|
||||
})
|
||||
return knopf
|
||||
}
|
||||
|
||||
const zustandText = (stufe) => (stufe.aktuell ? 'aktueller Stand' : stufe.erreicht ? 'erreicht' : 'noch offen')
|
||||
|
||||
// Vier Abschnitte, erreichte schwarz; die Namen stehen nur für Screenreader als Text. Ohne erreichte Stufe (pausiert, storniert) gibt es keinen Balken.
|
||||
function baueBalken(status) {
|
||||
const stufen = fortschritt(status)
|
||||
if (!stufen.some((stufe) => stufe.erreicht)) return null
|
||||
return h('ol', { class: 'kn-az-balken', 'aria-label': 'Fortschritt des Auftrags' }, stufen.map((stufe) => h('li',
|
||||
{ class: stufe.erreicht ? 'is-done' : null, 'aria-current': stufe.aktuell ? 'step' : null },
|
||||
h('span', { class: 'kn-nur-leser' }, `${stufe.name}, ${zustandText(stufe)}`))))
|
||||
}
|
||||
|
||||
let listenNummer = 0 // Ids der Listen für aria-controls: eindeutig je Seite, auch wenn die Ansicht mehrmals gezeichnet wird
|
||||
|
||||
function baueZeitleiste(zeilen, titel) {
|
||||
const { sichtbar, rest } = kuerze(zeilen, MAX_ZEILEN)
|
||||
listenNummer += 1
|
||||
const listenId = `kn-az-zeilen-${listenNummer}`
|
||||
const zeile = (z, extra) => h('li', { 'data-extra': extra },
|
||||
z.datum ? h('time', { class: 'kn-az-datum', datetime: z.iso || null }, z.datum) : h('span', { class: 'kn-az-datum', 'aria-hidden': 'true' }),
|
||||
h('span', { class: 'kn-az-text' }, z.text))
|
||||
return h('div', { class: 'kn-az-zeitleiste', 'data-einklappbar': true },
|
||||
// Trägt ein Datum das Jahr (TT.MM.JJ), bekommt die Datumsspalte aller Zeilen die Breite dafür, damit die Texte bündig bleiben
|
||||
h('ol', { id: listenId, class: zeilen.some((z) => z.datum.length > 6) ? 'kn-az-zeilen kn-az-mit-jahr' : 'kn-az-zeilen' }, sichtbar.map((z) => zeile(z, false)), rest.map((z) => zeile(z, true))),
|
||||
// Mehrere Knöpfe „mehr anzeigen“ auf einer Seite: der Name nennt den Auftrag, aria-controls die Liste
|
||||
rest.length ? umschalter({ attribut: 'data-mehr', klasse: 'kn-textknopf kn-az-mehr', mehr: 'mehr anzeigen', weniger: 'weniger anzeigen', name: titel, steuert: listenId }) : null)
|
||||
}
|
||||
|
||||
function baueAuftragsBlock(auftrag, jetzt, extra) {
|
||||
const status = auftrag.status && typeof auftrag.status === 'object' ? auftrag.status : {}
|
||||
const zeilen = zeitleistenZeilen(auftrag, jetzt)
|
||||
const titel = auftrag.topic || 'Dein Auftrag'
|
||||
return h('li', { class: 'kn-az', 'data-auftrag': auftrag.number || null, 'data-extra': extra },
|
||||
h('div', { class: 'kn-az-kopf' },
|
||||
h('h3', { class: 'kn-az-titel' }, titel),
|
||||
status.label ? h('span', { class: K.pille[statusTon(status.tone)] }, status.label) : null),
|
||||
// Die ganze Zeile führt in den Chat (große Tippfläche am Handy); unterstrichen ist nur „Im Chat antworten“
|
||||
braucheInfo(auftrag) ? h('p', { class: 'kn-az-info' }, h('a', zuAnsicht('chat'), 'Wir brauchen eine Info von dir · ', h('span', { class: 'kn-az-info-link' }, 'Im Chat antworten'))) : null,
|
||||
baueBalken(status),
|
||||
zeilen.length ? baueZeitleiste(zeilen, titel) : h('p', { class: 'kn-az-leer' }, KEINE_EINTRAEGE))
|
||||
}
|
||||
|
||||
/** „Was wir zuletzt erledigt haben“: ein Block je Auftrag; fünf offen, der Rest hinter „Ältere Aufträge anzeigen“. Ohne Aufträge ein ruhiger Satz. */
|
||||
export function baueAktivitaet(tickets, jetzt = new Date()) {
|
||||
const { sichtbar, aeltere } = gruppiereAuftraege(tickets)
|
||||
if (!sichtbar.length) return h('p', { class: 'kn-hinweis' }, LEER)
|
||||
return h('div', { class: 'kn-az-liste', 'data-einklappbar': true },
|
||||
h('ul', { class: 'kn-az-auftraege' }, sichtbar.map((a) => baueAuftragsBlock(a, jetzt, false)), aeltere.map((a) => baueAuftragsBlock(a, jetzt, true))),
|
||||
aeltere.length ? umschalter({ attribut: 'data-aeltere', klasse: 'kn-knopf kn-az-aeltere', mehr: 'Ältere Aufträge anzeigen', weniger: 'Weniger anzeigen' }) : null)
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
/* Kundenansicht neu: Basis und Bausteine im weißen Stil (Maße des Ticketsystems, global.css).
|
||||
Die Grundregeln gelten am Desktop; die Handy-Abfrage am Dateiende überschreibt für das Handy. */
|
||||
|
||||
/* ---------- Grundlage (Spezifität 0, damit Bausteinklassen gewinnen) ---------- */
|
||||
|
||||
:where(.kn) { margin: 0; font-family: var(--kn-font); font-size: 14px; line-height: 20px; color: var(--kn-text); background: var(--kn-bg); -webkit-font-smoothing: antialiased; }
|
||||
:where(.kn) *, :where(.kn) *::before, :where(.kn) *::after { box-sizing: border-box; }
|
||||
:where(.kn) a { color: inherit; text-decoration: none; }
|
||||
:where(.kn) a:not([class]) { text-decoration: underline; text-decoration-color: var(--kn-line-strong); text-underline-offset: 3px; }
|
||||
:where(.kn) a:not([class]):hover { text-decoration-color: currentColor; }
|
||||
:where(.kn) button { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
|
||||
:where(.kn) input, :where(.kn) select, :where(.kn) textarea { font: inherit; color: inherit; }
|
||||
:where(.kn) h1, :where(.kn) h2, :where(.kn) h3, :where(.kn) p { margin: 0; }
|
||||
:where(.kn) :focus-visible { outline: 2px solid #111; outline-offset: 2px; }
|
||||
:where(.kn) :disabled { opacity: .55; cursor: not-allowed; }
|
||||
:where(.kn) [readonly] { background: var(--kn-bg-subtle); }
|
||||
.kn [hidden] { display: none !important; }
|
||||
|
||||
.kn-num { font-variant-numeric: tabular-nums; }
|
||||
|
||||
/* ---------- Überschriften ---------- */
|
||||
|
||||
.kn-titel { font-size: 28px; line-height: 34px; font-weight: 700; letter-spacing: -.02em; }
|
||||
.kn-abschnitt { display: flex; justify-content: space-between; align-items: baseline; gap: 16px; margin: 36px 0 12px; font-size: 16px; line-height: 24px; font-weight: 600; }
|
||||
|
||||
/* ---------- Knöpfe und Links ---------- */
|
||||
|
||||
.kn-knopf, .kn-knopf-primaer, .kn-knopf-zweit, .kn-textknopf {
|
||||
display: inline-flex; align-items: center; justify-content: center; gap: 6px;
|
||||
height: 32px; padding: 0 12px; border-radius: var(--kn-radius);
|
||||
font-weight: 500; white-space: nowrap; text-decoration: none; color: var(--kn-text);
|
||||
background: var(--kn-bg); box-shadow: var(--kn-ring); cursor: pointer;
|
||||
transition: background-color .12s, box-shadow .12s;
|
||||
}
|
||||
.kn-knopf:hover, .kn-knopf-zweit:hover { background: var(--kn-bg-hover); }
|
||||
.kn-knopf-primaer { background: var(--kn-accent); color: #fff; box-shadow: var(--kn-shadow-btn); }
|
||||
.kn-knopf-primaer:hover { background: var(--kn-accent-hover); }
|
||||
.kn-textknopf { box-shadow: none; color: var(--kn-text-2); padding: 0 8px; background: none; }
|
||||
.kn-textknopf:hover { color: var(--kn-text); background: none; }
|
||||
.kn-link-danger { color: var(--kn-danger); font-weight: 500; cursor: pointer; }
|
||||
.kn-link-danger:hover { text-decoration: underline; text-underline-offset: 3px; }
|
||||
|
||||
/* ---------- Felder ---------- */
|
||||
|
||||
.kn-feld { display: flex; flex-direction: column; gap: 6px; }
|
||||
.kn-feld > label { font-weight: 500; }
|
||||
.kn-feld-hinweis { font-size: 13px; color: var(--kn-text-2); }
|
||||
|
||||
.kn-input, .kn-select, .kn-textarea {
|
||||
height: 32px; min-width: 0; padding: 0 10px; border: 0; border-radius: var(--kn-radius);
|
||||
background: var(--kn-bg); box-shadow: var(--kn-ring); outline: none; transition: box-shadow .12s;
|
||||
}
|
||||
.kn-textarea { height: auto; min-height: 96px; padding: 8px 10px; resize: vertical; line-height: 20px; }
|
||||
.kn-select {
|
||||
padding-right: 28px; appearance: none; -webkit-appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%235e6678' stroke-width='1.5'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat; background-position: right 10px center;
|
||||
}
|
||||
.kn-input:focus, .kn-select:focus, .kn-textarea:focus { box-shadow: 0 0 0 1px var(--kn-accent), 0 0 0 4px var(--kn-accent-ring); }
|
||||
.kn-input[aria-invalid="true"], .kn-select[aria-invalid="true"], .kn-textarea[aria-invalid="true"] { box-shadow: 0 0 0 1px var(--kn-danger), 0 0 0 4px var(--kn-danger-ring); }
|
||||
.kn-input::placeholder, .kn-textarea::placeholder { color: var(--kn-text-3); }
|
||||
|
||||
.kn-check { display: inline-flex; align-items: center; gap: 8px; color: var(--kn-text-2); cursor: pointer; user-select: none; }
|
||||
.kn-check input { accent-color: var(--kn-accent); width: 15px; height: 15px; margin: 0; }
|
||||
|
||||
/* ---------- Unterstrich-Reiter ---------- */
|
||||
|
||||
.kn-reiter { display: flex; gap: 24px; border-bottom: 1px solid var(--kn-line); margin-bottom: 16px; overflow-x: auto; scrollbar-width: none; }
|
||||
.kn-reiter::-webkit-scrollbar { display: none; }
|
||||
.kn-reiter-t {
|
||||
padding: 8px 0 10px; margin-bottom: -1px; color: var(--kn-text-2); font-weight: 500; white-space: nowrap;
|
||||
border-bottom: 2px solid transparent; font-variant-numeric: tabular-nums; cursor: pointer;
|
||||
}
|
||||
.kn-reiter-t:hover { color: var(--kn-text); }
|
||||
.kn .kn-reiter-t[role="tab"]:focus-visible { outline-offset: -2px; } /* 0-4-0: .kn [tabindex]:focus-visible in module.css (0-3-0, später geladen) setzt sonst +2px */
|
||||
.kn-reiter-t.aktiv, .kn-reiter-t[aria-selected="true"] { color: var(--kn-accent); border-bottom-color: var(--kn-accent); font-weight: 600; }
|
||||
|
||||
/* ---------- Status-Pillen ---------- */
|
||||
|
||||
.kn-pille {
|
||||
display: inline-flex; align-items: center; height: 20px; padding: 0 6px; border-radius: 4px;
|
||||
font-size: 12px; font-weight: 500; white-space: nowrap; font-variant-numeric: tabular-nums;
|
||||
background: var(--kn-grau-bg); color: var(--kn-grau-fg);
|
||||
}
|
||||
.kn-pille-grau { background: var(--kn-grau-bg); color: var(--kn-grau-fg); }
|
||||
.kn-pille-blau { background: var(--kn-blau-bg); color: var(--kn-blau-fg); }
|
||||
.kn-pille-orange { background: var(--kn-orange-bg); color: var(--kn-orange-fg); }
|
||||
.kn-pille-violett { background: var(--kn-violett-bg); color: var(--kn-violett-fg); }
|
||||
.kn-pille-gruen { background: var(--kn-gruen-bg); color: var(--kn-gruen-fg); }
|
||||
.kn-pille-rot { background: var(--kn-rot-bg); color: var(--kn-rot-fg); }
|
||||
.kn-pille-schlicht { background: none; color: var(--kn-text-2); padding: 0; }
|
||||
|
||||
/* ---------- Listen: Haarlinien statt Karten ---------- */
|
||||
|
||||
.kn-liste { list-style: none; margin: 0; padding: 0; }
|
||||
.kn-zeile { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 12px 0; border-bottom: 1px solid var(--kn-line); }
|
||||
.kn-zeile:first-child { border-top: 1px solid var(--kn-line); }
|
||||
.kn-zeile-titel { font-weight: 500; min-width: 0; overflow-wrap: anywhere; }
|
||||
.kn-zeile-sub { font-size: 13px; color: var(--kn-text-2); }
|
||||
.kn-zeile-link { color: inherit; text-decoration: none; cursor: pointer; }
|
||||
.kn-zeile-link:hover .kn-zeile-titel { color: var(--kn-accent); }
|
||||
|
||||
/* ---------- Dialog und Sheet (schließen nur per × und Esc) ---------- */
|
||||
|
||||
.kn-ueberlagerung {
|
||||
position: fixed; inset: 0; z-index: 50; display: flex; align-items: flex-start; justify-content: center;
|
||||
padding: 10vh 16px 16px; background: var(--kn-overlay); animation: kn-einblenden .12s ease-out;
|
||||
}
|
||||
.kn-dialog, .kn-sheet {
|
||||
display: flex; flex-direction: column; width: 100%; max-width: 520px; max-height: 84vh;
|
||||
background: var(--kn-bg); border-radius: var(--kn-radius-lg); box-shadow: var(--kn-shadow-pop);
|
||||
animation: kn-aufpoppen .14s ease-out;
|
||||
}
|
||||
.kn-dialog:focus, .kn-sheet:focus { outline: none; }
|
||||
.kn-dialog-kopf { display: flex; align-items: center; justify-content: space-between; padding: 20px 24px 4px; }
|
||||
.kn-dialog-kopf h3 { font-size: 18px; line-height: 26px; font-weight: 600; }
|
||||
.kn-dialog-zu { width: 28px; height: 28px; border-radius: var(--kn-radius); color: var(--kn-text-2); font-size: 20px; line-height: 1; }
|
||||
.kn-dialog-zu:hover { background: var(--kn-bg-hover); color: var(--kn-text); }
|
||||
.kn-dialog-inhalt { display: flex; flex-direction: column; gap: 16px; padding: 16px 24px 8px; overflow-y: auto; }
|
||||
.kn-dialog-fuss { display: flex; align-items: center; gap: 8px; padding: 16px 24px 20px; }
|
||||
@keyframes kn-einblenden { from { opacity: 0; } }
|
||||
@keyframes kn-aufpoppen { from { opacity: 0; transform: translateY(4px) scale(.985); } }
|
||||
|
||||
/* ---------- Leerzustände und Meldungen (ohne Kasten, Text) ---------- */
|
||||
|
||||
.kn-leer { padding: 56px 0; text-align: center; color: var(--kn-text-2); }
|
||||
.kn-leer strong { display: block; margin-bottom: 4px; color: var(--kn-text); font-weight: 600; }
|
||||
.kn-meldung { font-size: 13px; }
|
||||
.kn-meldung-ok { color: var(--kn-gruen-fg); }
|
||||
.kn-meldung-warn { color: var(--kn-orange-fg); }
|
||||
.kn-fehler { font-size: 13px; color: var(--kn-rot-fg); }
|
||||
|
||||
/* ---------- Reduzierte Bewegung: höchstens kurzes Überblenden ---------- */
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.kn-ueberlagerung { animation-duration: .12s; }
|
||||
.kn-dialog, .kn-sheet { animation: kn-einblenden .12s ease-out; }
|
||||
.kn-knopf, .kn-knopf-primaer, .kn-knopf-zweit, .kn-textknopf, .kn-input, .kn-select, .kn-textarea { transition: none; }
|
||||
}
|
||||
|
||||
/* ---------- Handy: Tap-Ziele 44 px, Textfelder 16 px, Dialoge unten verankert ---------- */
|
||||
|
||||
@media (max-width: 880px), (pointer: coarse) and (max-height: 500px) {
|
||||
.kn-titel { font-size: 24px; line-height: 30px; }
|
||||
.kn-knopf, .kn-knopf-primaer, .kn-knopf-zweit, .kn-textknopf { height: auto; min-height: 44px; padding-left: 16px; padding-right: 16px; }
|
||||
.kn-textknopf { padding-left: 12px; padding-right: 12px; }
|
||||
.kn-link-danger { display: inline-flex; align-items: center; min-height: 44px; }
|
||||
.kn-input, .kn-select, .kn-textarea { height: auto; min-height: 44px; font-size: 16px; }
|
||||
.kn-textarea { min-height: 96px; }
|
||||
.kn-check { min-height: 44px; }
|
||||
.kn-check input { width: 20px; height: 20px; }
|
||||
.kn-reiter-t { display: inline-flex; align-items: center; min-height: 44px; padding-top: 0; padding-bottom: 0; }
|
||||
.kn-zeile-link { min-height: 44px; }
|
||||
.kn-dialog-zu { width: 44px; height: 44px; }
|
||||
.kn-ueberlagerung { align-items: flex-end; padding: 16px; }
|
||||
.kn-dialog, .kn-sheet { max-height: 92vh; padding-bottom: env(safe-area-inset-bottom, 0px); }
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/* Kundenansicht neu (P7): gemeinsame Bausteine der vier Bestellseiten (Abo bestellen, Wechsel, Zusatzspeicher, Gutschrift-Antrag).
|
||||
Die Sätze kommen vom Server (GET /api/abo → bestelltexte); hier werden sie nur gezeichnet. Link-Texte stehen in der Vorlage in
|
||||
eckigen Klammern, ihre Adressen daneben in urls. Jede Adresse läuft durch sicherLink; Kundentext wird nie als HTML gelesen.
|
||||
Keine h1: den Titel meldet die Ansicht mit meldeTitel. */
|
||||
|
||||
import { h } from './dom.js'
|
||||
import { K } from './ansicht-hilfen.js'
|
||||
|
||||
/** 'unternehmen' | 'privat': nur exakt 'unternehmen' (me.customer.kundenartWirksam, sonst daten.kundenart) ist Betrieb; alles andere privat (E4). */
|
||||
export function kundenartVon(me, daten = null) {
|
||||
return me?.customer?.kundenartWirksam === 'unternehmen' || daten?.kundenart === 'unternehmen' ? 'unternehmen' : 'privat'
|
||||
}
|
||||
|
||||
/** Adresse nur, wenn sie ein Pfad auf derselben Herkunft ('/…', nicht '//…'), ein Anker ('#name…') oder http(s) ohne Zugangsdaten ist; sonst ''. */
|
||||
export function sicherLink(url) {
|
||||
if (typeof url !== 'string' || url === '') return ''
|
||||
if (/[\s\u0000-\u001f\u007f\\]/.test(url)) return ''
|
||||
if (/^\/(?![/\\])/.test(url)) return url
|
||||
if (/^#[a-z][\w-]*(\?[\w=&.%-]*)?$/i.test(url)) return url
|
||||
if (/^https?:\/\/[^/?#]+/i.test(url)) {
|
||||
try {
|
||||
const u = new URL(url)
|
||||
return u.hostname !== '' && u.username === '' && u.password === '' ? url : ''
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 'Es gilt [A] und [B].' mit ['/a', '/b'] → Teile [{ text }, { text, href? }]. Die Markierungen bekommen der Reihe nach ihre Adresse;
|
||||
* eine unsichere oder fehlende Adresse macht den Teil zu einem Teil ohne href. Eine offene Klammer bleibt Text.
|
||||
*/
|
||||
export function linkTeile(text, urls) {
|
||||
const basis = typeof text === 'string' ? text : ''
|
||||
const adressen = Array.isArray(urls) ? urls : []
|
||||
const teile = []
|
||||
let ab = 0
|
||||
let nr = 0
|
||||
const muster = /\[([^\]]*)\]/g
|
||||
for (let m = muster.exec(basis); m; m = muster.exec(basis)) {
|
||||
if (m.index > ab) teile.push({ text: basis.slice(ab, m.index) })
|
||||
const href = sicherLink(adressen[nr])
|
||||
nr += 1
|
||||
teile.push(href ? { text: m[1], href } : { text: m[1] })
|
||||
ab = m.index + m[0].length
|
||||
}
|
||||
if (ab < basis.length) teile.push({ text: basis.slice(ab) })
|
||||
return teile
|
||||
}
|
||||
|
||||
const ausserhalb = (href) => !href.startsWith('#')
|
||||
|
||||
/**
|
||||
* <p id> mit den Links der Vorlage; '/…' und http(s) öffnen in neuem Tab (target _blank, rel noopener), '#…' bleibt im Fenster.
|
||||
* klasse: Vorgabe 'kn-linkzeile'. neuerTab: auch '#…' öffnet in neuem Tab (Bedingungen der Gutschrift neben dem Formular).
|
||||
*/
|
||||
export function baueLinkzeile({ text, urls } = {}, { id, klasse = 'kn-linkzeile', neuerTab = false } = {}) {
|
||||
const kinder = linkTeile(text, urls).map((t) =>
|
||||
t.href ? h('a', { href: t.href, ...(neuerTab || ausserhalb(t.href) ? { target: '_blank', rel: 'noopener' } : {}) }, t.text) : t.text)
|
||||
return h('p', { id, class: klasse }, kinder)
|
||||
}
|
||||
|
||||
/** <p id>: „Du bestellst als Betrieb“ | „Du bestellst privat“, ' · ', dann der Hinweis zur Korrektur mit seinem Link. */
|
||||
export function baueKundenartZeile(kundenart, texte, { id } = {}) {
|
||||
const lage = kundenart === 'unternehmen' ? texte?.unternehmen : texte?.privat
|
||||
const korrektur = linkTeile(texte?.korrektur?.text, texte?.korrektur?.urls).map((t) =>
|
||||
t.href ? h('a', { href: t.href, ...(ausserhalb(t.href) ? { target: '_blank', rel: 'noopener' } : {}) }, t.text) : t.text)
|
||||
return h('p', { id, class: 'kn-kundenart-zeile' }, lage, ' · ', korrektur)
|
||||
}
|
||||
|
||||
/**
|
||||
* Block „Was du bestellst“: zeilen [{ id?, label, text, klasse?, link? }] als Beschriftungsliste. Der Text ist immer Text.
|
||||
* link { text, href }: hinter dem Text ' · ' und ein Link (nur mit sicherer Adresse; Adressen außerhalb der Seite öffnen in neuem Tab).
|
||||
*/
|
||||
export function baueBlock(zeilen, { id } = {}) {
|
||||
return h('dl', { id, class: 'kn-block' }, (zeilen || []).map((z) => {
|
||||
const href = z.link ? sicherLink(z.link.href) : ''
|
||||
const link = href ? [' · ', h('a', { href, ...(ausserhalb(href) ? { target: '_blank', rel: 'noopener' } : {}) }, z.link.text)] : null
|
||||
return h('div', { class: 'kn-block-zeile' }, h('dt', null, z.label), h('dd', { id: z.id, class: z.klasse }, z.text, link))
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Die ausführlichen Sätze UNTER dem Knopf (und der Kundenart-Zeile): Überschrift „Im Einzelnen“ und dieselbe Beschriftungsliste wie der Block.
|
||||
* Der Block über dem Knopf trägt die Kerninfo knapp, damit sie mit dem Knopf auf einen Handy-Bildschirm passt; hier steht sie ausführlich.
|
||||
*/
|
||||
export function baueEinzelheiten(zeilen, { id } = {}) {
|
||||
return h('section', { id, class: 'kn-einzelheiten' }, h('h3', null, 'Im Einzelnen'), baueBlock(zeilen))
|
||||
}
|
||||
|
||||
/** Adresse der Leistungsbeschreibung: die Bedingungen (erste Adresse der Linkzeile) mit Anker ('wartung', 'hosting'); unsicher oder fehlend ''. */
|
||||
export function leistungLink(linkzeile, anker) {
|
||||
const basis = sicherLink(Array.isArray(linkzeile?.urls) ? linkzeile.urls[0] : '')
|
||||
return basis && !basis.includes('#') ? `${basis}#${anker}` : ''
|
||||
}
|
||||
|
||||
const nichtLeer = (x) => typeof x === 'string' && x !== ''
|
||||
|
||||
/**
|
||||
* true, wenn die Antwort den Wortlaut trägt: status, knopf und die Sätze der Seite (art 'abo' | 'wechsel' | 'speicher' | 'gutschrift').
|
||||
* Ohne ihn zeigt keine Seite einen Bestellknopf.
|
||||
*/
|
||||
export function hatWortlaut(daten, art = 'abo') {
|
||||
const t = daten?.bestelltexte
|
||||
if (!t || typeof t !== 'object' || !nichtLeer(t.status) || !nichtLeer(t.knopf)) return false
|
||||
if (art === 'gutschrift') return nichtLeer(t.gutschrift?.satz?.text) && nichtLeer(t.gutschrift?.knopf)
|
||||
if (!nichtLeer(t[art]?.linkzeile?.text)) return false
|
||||
if (art === 'wechsel') return nichtLeer(t.wechsel.haken) && nichtLeer(t.wechsel.gesperrt)
|
||||
if (art === 'abo') return nichtLeer(t.abo.haken)
|
||||
return true
|
||||
}
|
||||
|
||||
export { K }
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
@@ -0,0 +1,242 @@
|
||||
/* Chat: Anhaenge, Kamera und Sprachnachrichten fuer die weisse Kundenansicht (Kopie von chat-composer.css, siehe chat-composer.js).
|
||||
Gegenueber dem Original: helle, neutrale Variablen, kein Lila, kein Gewicht 800 und kein Weichzeichner im Overlay. Das Original bleibt fuer die alte Ansicht. */
|
||||
:root {
|
||||
--chat-accent: #111111;
|
||||
--chat-accent-hover: #333333;
|
||||
--chat-accent-soft: rgba(17, 17, 17, 0.08);
|
||||
--chat-accent-line: rgba(17, 17, 17, 0.5);
|
||||
--chat-accent-text: #111111;
|
||||
--chat-on-accent: #ffffff;
|
||||
--chat-line: #d8dde3;
|
||||
--chat-muted: #5e6678;
|
||||
--chat-surface: #ffffff;
|
||||
}
|
||||
|
||||
.chat-composer [hidden], .chat-overlay [hidden], .chat-bubble [hidden] { display: none !important; }
|
||||
.chat-composer button, .chat-bubble button, .chat-bubble a, .chat-overlay button { -webkit-tap-highlight-color: transparent; }
|
||||
.chat-ico { display: inline-grid; place-items: center; line-height: 0; }
|
||||
.chat-ico svg { width: 20px; height: 20px; }
|
||||
|
||||
/* ------------------------------------------------------------- Verlauf --- */
|
||||
.chat-bubble { white-space: normal; }
|
||||
.chat-bubble .chat-text { white-space: pre-wrap; }
|
||||
.chat-bubble.has-atts { padding: 6px 6px 8px; }
|
||||
.chat-bubble.has-atts .chat-text, .chat-bubble.has-atts .chat-meta { padding: 0 8px; }
|
||||
.chat-atts { display: grid; grid-template-columns: minmax(0, 1fr); gap: 6px; }
|
||||
.chat-atts + .chat-text { margin-top: 8px; }
|
||||
|
||||
.chat-att-images { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 4px; width: 272px; max-width: 100%; }
|
||||
.chat-att-images.single { display: block; width: auto; }
|
||||
.chat-att-image {
|
||||
position: relative; display: block; max-width: 100%; padding: 0; border: 0; border-radius: 12px; overflow: hidden;
|
||||
background: rgba(255, 255, 255, 0.06); color: inherit; font: inherit; cursor: zoom-in;
|
||||
}
|
||||
.chat-att-image img { display: block; }
|
||||
.chat-att-images.single img { max-width: 100%; max-height: 320px; height: auto; }
|
||||
.chat-att-images:not(.single) .chat-att-image { aspect-ratio: 1; }
|
||||
.chat-att-images:not(.single) img { width: 100%; height: 100%; object-fit: cover; }
|
||||
.chat-att-image:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
|
||||
.chat-att-image.is-broken {
|
||||
display: flex; align-items: center; gap: 8px; padding: 12px 14px; aspect-ratio: auto;
|
||||
font-size: 13px; color: var(--chat-muted); cursor: default;
|
||||
}
|
||||
|
||||
.chat-att-file {
|
||||
display: flex; align-items: center; gap: 10px; width: 270px; max-width: 100%; box-sizing: border-box; padding: 8px 12px 8px 8px;
|
||||
border-radius: 12px; background: rgba(0, 0, 0, 0.22); border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
color: inherit !important; text-decoration: none !important;
|
||||
}
|
||||
.chat-att-file:hover { background: rgba(0, 0, 0, 0.34); }
|
||||
.chat-att-file > .chat-ico svg { width: 18px; height: 18px; opacity: 0.75; }
|
||||
.chat-file-icon { position: relative; flex: none; width: 36px; height: 40px; display: grid; place-items: center; }
|
||||
.chat-file-icon .chat-ico svg { width: 34px; height: 34px; stroke-width: 1.4; opacity: 0.9; }
|
||||
.chat-file-ext {
|
||||
position: absolute; left: 50%; bottom: 6px; transform: translateX(-50%); padding: 0 3px; border-radius: 3px;
|
||||
font-size: 8.5px; line-height: 13px; font-weight: 700; letter-spacing: 0.02em; background: var(--chat-accent); color: var(--chat-on-accent);
|
||||
white-space: nowrap; word-break: normal;
|
||||
}
|
||||
.chat-file-ext:empty { display: none; }
|
||||
.chat-file-text { flex: 1; min-width: 0; display: grid; gap: 1px; }
|
||||
.chat-file-name { font-size: 13.5px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.chat-file-size { font-size: 12px; opacity: 0.7; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
|
||||
.chat-audio { display: flex; align-items: center; gap: 10px; width: 260px; max-width: 100%; box-sizing: border-box; padding: 2px; }
|
||||
.chat-audio-play {
|
||||
flex: none; width: 38px; height: 38px; padding: 0; border: 0; border-radius: 50%; display: grid; place-items: center;
|
||||
background: var(--chat-accent); color: var(--chat-on-accent); cursor: pointer;
|
||||
}
|
||||
.chat-bubble.mine .chat-audio-play { background: #fff; color: #17102a; }
|
||||
.chat-audio-play .chat-ico svg { width: 16px; height: 16px; }
|
||||
.chat-audio-main { flex: 1; min-width: 0; display: grid; gap: 7px; }
|
||||
.chat-audio-label { font-size: 12px; font-weight: 600; opacity: 0.8; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.chat-audio-bar { position: relative; display: block; height: 6px; border-radius: 999px; background: rgba(255, 255, 255, 0.2); cursor: pointer; }
|
||||
.chat-audio-bar::after { content: ''; position: absolute; inset: -8px 0; }
|
||||
.chat-audio-bar:focus-visible { outline: 2px solid #fff; outline-offset: 3px; }
|
||||
.chat-audio-fill { position: absolute; left: 0; top: 0; bottom: 0; width: 0; border-radius: inherit; background: currentColor; }
|
||||
.chat-audio-time { flex: none; min-width: 32px; text-align: right; font-size: 12px; font-variant-numeric: tabular-nums; opacity: 0.8; }
|
||||
.chat-audio-dl {
|
||||
flex: none; display: grid; place-items: center; width: 28px; height: 28px; border-radius: 50%;
|
||||
color: inherit !important; opacity: 0.7;
|
||||
}
|
||||
.chat-audio-dl:hover { opacity: 1; background: rgba(255, 255, 255, 0.1); }
|
||||
.chat-audio-dl .chat-ico svg { width: 16px; height: 16px; }
|
||||
.chat-audio.is-error .chat-audio-time { opacity: 1; color: #fca5a5; font-size: 11px; }
|
||||
|
||||
/* ------------------------------------------------------- Eingabezeile --- */
|
||||
.chat-composer { position: relative; margin-top: 12px; }
|
||||
.chat-composer .chat-input-row { display: flex; align-items: flex-end; gap: 8px; margin-top: 0; }
|
||||
.chat-composer textarea { flex: 1; min-width: 0; box-sizing: border-box; }
|
||||
.chat-icon-btn {
|
||||
flex: none; width: 46px; height: 46px; padding: 0; border-radius: 50%; display: grid; place-items: center;
|
||||
border: 1px solid var(--chat-line); background: rgba(255, 255, 255, 0.05); color: inherit; cursor: pointer;
|
||||
}
|
||||
.chat-icon-btn:hover:not(:disabled) { background: rgba(255, 255, 255, 0.11); }
|
||||
.chat-icon-btn:disabled { opacity: 0.45; cursor: default; }
|
||||
.chat-icon-btn[aria-expanded="true"] { background: var(--chat-accent-soft); border-color: var(--chat-accent-line); }
|
||||
.chat-icon-btn:focus-visible, .chat-send:focus-visible, .chat-audio-play:focus-visible, .chat-tray-remove:focus-visible {
|
||||
outline: 2px solid var(--chat-accent-text); outline-offset: 2px;
|
||||
}
|
||||
|
||||
.chat-attach { position: relative; flex: none; }
|
||||
.chat-menu {
|
||||
position: absolute; left: 0; bottom: calc(100% + 8px); z-index: 30; min-width: 190px; padding: 6px; display: grid; gap: 2px;
|
||||
border-radius: 14px; background: var(--chat-surface); border: 1px solid var(--chat-line); box-shadow: 0 14px 36px rgba(0, 0, 0, 0.55);
|
||||
}
|
||||
.chat-menu button {
|
||||
display: flex; align-items: center; gap: 12px; width: 100%; padding: 10px 12px; border: 0; border-radius: 10px;
|
||||
background: none; color: inherit; font: inherit; font-size: 14px; text-align: left; cursor: pointer;
|
||||
}
|
||||
.chat-menu button:hover, .chat-menu button:focus-visible { background: rgba(255, 255, 255, 0.08); outline: none; }
|
||||
.chat-menu .chat-ico { color: var(--chat-accent-text); }
|
||||
|
||||
.chat-send {
|
||||
flex: none; display: inline-flex; align-items: center; justify-content: center; gap: 8px; height: 46px; padding: 0 20px;
|
||||
border: none; border-radius: 999px; background: var(--chat-accent); color: var(--chat-on-accent);
|
||||
font: inherit; font-weight: 700; cursor: pointer;
|
||||
}
|
||||
.chat-send:hover:not(:disabled) { background: var(--chat-accent-hover); }
|
||||
.chat-send:disabled { opacity: 0.5; cursor: default; }
|
||||
.chat-send.is-busy:disabled { opacity: 0.8; cursor: progress; }
|
||||
.chat-send .chat-ico { display: none; }
|
||||
.chat-send .chat-ico svg { width: 19px; height: 19px; }
|
||||
|
||||
.chat-composer-note { margin: 8px 2px 0; font-size: 13px; line-height: 1.45; color: #fca5a5; }
|
||||
.chat-composer-note.is-info { color: var(--chat-muted); }
|
||||
|
||||
/* Vorschau der Anhaenge vor dem Senden */
|
||||
.chat-tray { display: flex; gap: 8px; padding: 2px 2px 10px; overflow-x: auto; scrollbar-width: thin; }
|
||||
.chat-tray-item {
|
||||
position: relative; flex: none; display: flex; align-items: center; gap: 10px; height: 64px; box-sizing: border-box;
|
||||
border-radius: 12px; border: 1px solid var(--chat-line); background: rgba(255, 255, 255, 0.05); overflow: hidden;
|
||||
}
|
||||
.chat-tray-item.is-image { width: 64px; }
|
||||
.chat-tray-item.is-image img { display: block; width: 100%; height: 100%; object-fit: cover; }
|
||||
.chat-tray-item.is-image.is-queued img, .chat-tray-item.is-image.is-uploading img { opacity: 0.55; }
|
||||
.chat-tray-item.is-file { width: 220px; padding: 0 36px 0 8px; }
|
||||
.chat-tray-item.is-file.is-error { padding-right: 64px; }
|
||||
.chat-tray-item.is-voice { width: 280px; padding: 0 36px 0 10px; }
|
||||
.chat-tray-item.is-voice .chat-audio { width: 100%; padding: 0; }
|
||||
.chat-tray-progress { position: absolute; left: 0; bottom: 0; height: 3px; width: 0; background: var(--chat-accent); transition: width 0.2s; }
|
||||
.chat-tray-item.is-done .chat-tray-progress { opacity: 0; transition: opacity 0.6s 0.3s; }
|
||||
.chat-tray-item.is-error { border-color: rgba(248, 113, 113, 0.75); background: rgba(248, 113, 113, 0.08); }
|
||||
.chat-tray-item.is-error .chat-file-size { color: #fca5a5; opacity: 1; }
|
||||
.chat-tray-remove, .chat-tray-retry {
|
||||
position: absolute; top: 5px; width: 24px; height: 24px; padding: 0; border: 0; border-radius: 50%; display: grid; place-items: center;
|
||||
background: rgba(0, 0, 0, 0.72); color: #fff; cursor: pointer;
|
||||
}
|
||||
.chat-tray-remove { right: 5px; }
|
||||
.chat-tray-retry { right: 34px; }
|
||||
.chat-tray-item.is-image .chat-tray-retry { top: 50%; left: 50%; right: auto; width: 30px; height: 30px; transform: translate(-50%, -50%); }
|
||||
.chat-tray-remove .chat-ico svg, .chat-tray-retry .chat-ico svg { width: 14px; height: 14px; }
|
||||
|
||||
/* Sprachaufnahme (ersetzt waehrenddessen Textfeld und Anhang-Knopf) */
|
||||
.chat-rec {
|
||||
display: none; flex: 1; min-width: 0; align-items: center; gap: 10px; height: 46px; box-sizing: border-box; padding: 0 5px;
|
||||
border-radius: 999px; border: 1px solid rgba(248, 113, 113, 0.45); background: rgba(248, 113, 113, 0.08);
|
||||
}
|
||||
.chat-composer.is-recording .chat-rec { display: flex; }
|
||||
.chat-composer.is-recording .chat-attach,
|
||||
.chat-composer.is-recording textarea,
|
||||
.chat-composer.is-recording .chat-mic { display: none; }
|
||||
.chat-rec .chat-icon-btn { width: 36px; height: 36px; border: 0; background: transparent; }
|
||||
.chat-rec .chat-icon-btn:hover { background: rgba(255, 255, 255, 0.1); }
|
||||
.chat-rec-stop { color: #f87171; }
|
||||
.chat-rec-dot { flex: none; width: 10px; height: 10px; border-radius: 50%; background: #f87171; animation: chat-rec-pulse 1.2s ease-in-out infinite; }
|
||||
.chat-rec-label { flex: none; font-size: 13px; color: var(--chat-muted); }
|
||||
.chat-rec-time { flex: none; min-width: 36px; font-weight: 700; font-variant-numeric: tabular-nums; }
|
||||
.chat-rec-wave { flex: 1; min-width: 0; height: 24px; display: flex; align-items: center; justify-content: flex-end; gap: 2px; overflow: hidden; color: #fca5a5; }
|
||||
.chat-rec-wave i { flex: none; width: 3px; height: 100%; border-radius: 2px; background: currentColor; transform: scaleY(0.1); transition: transform 0.09s linear; }
|
||||
@keyframes chat-rec-pulse { 50% { opacity: 0.25; } }
|
||||
|
||||
/* Dateien in den Chat ziehen */
|
||||
.chat-dropzone { position: relative; }
|
||||
.chat-drop-active::after {
|
||||
content: 'Dateien hier ablegen'; position: absolute; inset: 0; z-index: 40; display: grid; place-items: center;
|
||||
border: 2px dashed var(--chat-accent-line); border-radius: inherit; background: rgba(10, 10, 12, 0.85);
|
||||
color: #fff; font-weight: 700; pointer-events: none;
|
||||
}
|
||||
|
||||
/* ---------------------------------------------- Bildansicht und Kamera --- */
|
||||
html.chat-overlay-open, html.chat-overlay-open body { overflow: hidden; }
|
||||
.chat-overlay {
|
||||
position: fixed; inset: 0; z-index: 1000; display: flex; flex-direction: column;
|
||||
background: rgba(4, 4, 6, 0.95); color: #fff; font-family: inherit;
|
||||
}
|
||||
.chat-ov-top { flex: none; display: flex; align-items: center; gap: 10px; padding: 12px max(14px, env(safe-area-inset-right)) 12px max(14px, env(safe-area-inset-left)); padding-top: max(12px, env(safe-area-inset-top)); }
|
||||
.chat-ov-title { flex: 1; min-width: 0; font-size: 14px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.chat-ov-btn {
|
||||
flex: none; display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-width: 44px; height: 44px; padding: 0 12px;
|
||||
box-sizing: border-box; border-radius: 999px; border: 1px solid rgba(255, 255, 255, 0.25); background: rgba(255, 255, 255, 0.1);
|
||||
color: #fff !important; font: inherit; font-size: 14px; font-weight: 600; text-decoration: none; cursor: pointer;
|
||||
}
|
||||
.chat-ov-btn:hover { background: rgba(255, 255, 255, 0.2); }
|
||||
.chat-ov-btn:focus-visible, .chat-shutter:focus-visible { outline: 2px solid #fff; outline-offset: 3px; }
|
||||
.chat-ov-btn.wide { padding: 0 18px; }
|
||||
.chat-ov-btn.primary { background: var(--chat-accent); border-color: transparent; color: var(--chat-on-accent) !important; }
|
||||
.chat-ov-btn.primary:hover { background: var(--chat-accent-hover); }
|
||||
|
||||
.chat-lightbox-stage { flex: 1; min-height: 0; display: flex; align-items: center; justify-content: center; padding: 0 12px 16px; }
|
||||
.chat-lightbox-stage img { max-width: 100%; max-height: 100%; object-fit: contain; border-radius: 6px; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5); }
|
||||
|
||||
.chat-cam-stage { flex: 1; min-height: 0; display: flex; align-items: center; justify-content: center; padding: 0 12px; }
|
||||
.chat-cam-video, .chat-cam-still { max-width: 100%; max-height: 100%; border-radius: 14px; background: #000; }
|
||||
.chat-cam-video.is-mirrored { transform: scaleX(-1); }
|
||||
.chat-camera:not(.is-review) .chat-cam-still, .chat-camera.is-review .chat-cam-video { display: none; }
|
||||
.chat-cam-msgbox { max-width: 420px; display: grid; gap: 16px; justify-items: center; text-align: center; }
|
||||
.chat-cam-msg { margin: 0; line-height: 1.5; color: rgba(255, 255, 255, 0.88); }
|
||||
.chat-cam-controls {
|
||||
flex: none; display: flex; align-items: center; justify-content: center; gap: 24px;
|
||||
padding: 18px 16px; padding-bottom: max(18px, env(safe-area-inset-bottom));
|
||||
}
|
||||
.chat-camera .chat-cam-review, .chat-camera.is-review .chat-cam-live { display: none; }
|
||||
.chat-camera.is-review .chat-cam-review { display: flex; }
|
||||
.chat-cam-side { flex: 0 1 110px; display: flex; justify-content: center; }
|
||||
.chat-shutter {
|
||||
flex: none; width: 74px; height: 74px; padding: 0; border-radius: 50%; border: 4px solid #fff;
|
||||
background: rgba(255, 255, 255, 0.18); box-shadow: inset 0 0 0 4px rgba(0, 0, 0, 0.35); cursor: pointer;
|
||||
}
|
||||
.chat-shutter:hover:not(:disabled) { background: rgba(255, 255, 255, 0.35); }
|
||||
.chat-shutter:active:not(:disabled) { background: #fff; }
|
||||
.chat-shutter:disabled { opacity: 0.4; cursor: default; }
|
||||
|
||||
/* App-Ansicht (gleiche Bedingung wie in kundenbereich.css: Handy, Tablet hochkant, Handy quer):
|
||||
Senden als runder Knopf mit Symbol */
|
||||
@media (max-width: 880px), (pointer: coarse) and (max-height: 500px) {
|
||||
.chat-send { width: 46px; padding: 0; }
|
||||
.chat-send .chat-ico { display: inline-grid; }
|
||||
.chat-send .chat-send-label { display: none; }
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.chat-composer .chat-input-row { gap: 6px; }
|
||||
.chat-composer textarea { font-size: 16px; } /* iOS zoomt sonst beim Tippen hinein */
|
||||
.chat-icon-btn { width: 42px; height: 42px; }
|
||||
.chat-send { width: 42px; height: 42px; }
|
||||
.chat-rec { height: 42px; }
|
||||
.chat-rec-label { display: none; }
|
||||
.chat-tray-item.is-file { width: 190px; }
|
||||
.chat-tray-item.is-voice { width: 250px; }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.chat-rec-dot { animation: none; }
|
||||
.chat-rec-wave i, .chat-tray-progress { transition: none; }
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content">
|
||||
<meta name="kn-seite" content="neu">
|
||||
<title>Kundenbereich – WEBklar</title>
|
||||
<!-- Weiße Kundenansicht (Pilot): Als App auf den Homescreen, helle Statusleiste -->
|
||||
<link rel="manifest" href="/neu/manifest.webmanifest">
|
||||
<link rel="icon" href="/favicon.ico" sizes="32x32">
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
||||
<link rel="apple-touch-icon" href="/img/apple-touch-icon.png">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
||||
<meta name="apple-mobile-web-app-title" content="WEBklar">
|
||||
|
||||
<link rel="stylesheet" href="/neu/tokens.css">
|
||||
<link rel="stylesheet" href="/neu/basis.css">
|
||||
<link rel="stylesheet" href="/neu/huelle.css">
|
||||
<link rel="stylesheet" href="/neu/chat-composer.css">
|
||||
<link rel="stylesheet" href="/neu/ansichten.css">
|
||||
<link rel="stylesheet" href="/neu/funktionen.css">
|
||||
<link rel="stylesheet" href="/neu/module.css">
|
||||
</head>
|
||||
<body class="kn">
|
||||
<a class="kn-skip" href="#kn-inhalt">Zum Inhalt</a>
|
||||
<div id="kn-banner" class="kn-banner" hidden></div>
|
||||
|
||||
<div class="kn-huelle">
|
||||
<nav id="kn-menue" class="kn-menue" aria-label="Hauptnavigation"></nav>
|
||||
|
||||
<div class="kn-spalte">
|
||||
<header id="kn-kopf" class="kn-kopf">
|
||||
<p id="kn-kopf-titel" class="kn-kopf-titel"></p>
|
||||
<div id="kn-kopf-konto"></div>
|
||||
</header>
|
||||
<div id="kn-streifen" class="kn-streifen" hidden></div>
|
||||
<p id="kn-fehler" class="kn-fehler" role="alert" hidden></p>
|
||||
|
||||
<main id="kn-inhalt" class="kn-haupt" tabindex="-1">
|
||||
<section id="bereich-website" hidden>
|
||||
<h1 class="kn-titel">Website</h1>
|
||||
<div id="website-body"></div>
|
||||
</section>
|
||||
<section id="bereich-funktionen" hidden>
|
||||
<h1 class="kn-titel">Funktionen</h1>
|
||||
<div id="funktionen-body"></div>
|
||||
</section>
|
||||
<section id="bereich-chat" hidden>
|
||||
<h1 class="kn-titel">Chat</h1>
|
||||
<div id="chat-body"></div>
|
||||
</section>
|
||||
<section id="bereich-vertraege" hidden>
|
||||
<h1 class="kn-titel">Verträge</h1>
|
||||
<div id="vertraege-body"></div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<div id="kn-blatt" class="kn-blatt" hidden>
|
||||
<nav id="kn-brot" class="kn-brot" aria-label="Brotkrume"></nav>
|
||||
<h1 id="kn-etage-titel" class="kn-titel"></h1>
|
||||
<div id="kn-reiter-slot"></div>
|
||||
<section data-etage="news" hidden><div id="news-body"></div></section>
|
||||
<section data-etage="termine" hidden><div id="termine-body"></div></section>
|
||||
<section data-etage="webshop" hidden><div id="webshop-body"></div></section>
|
||||
<section data-etage="buchungen" hidden><div id="buchungen-body"></div></section>
|
||||
<section data-etage="zeiten" hidden><div id="zeiten-body"></div></section>
|
||||
<section data-etage="analyse" hidden><div id="analyse-body"></div></section>
|
||||
<section data-etage="seite" hidden><div id="etage-body"></div></section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav id="kn-leiste" class="kn-leiste" aria-label="Hauptnavigation">
|
||||
<div class="kn-leiste-reel">
|
||||
<div id="kn-zeile-haupt" class="kn-rw"></div>
|
||||
<div id="kn-zeile-etage" class="kn-rw"></div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div id="kn-konto-wurzel"></div>
|
||||
<div id="kn-ansage" class="kn-nur-leser" aria-live="polite"></div>
|
||||
|
||||
<script src="/kundenansicht.js"></script>
|
||||
<script src="/chat-composer.js"></script>
|
||||
<script type="module" src="/neu/huelle.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,28 @@
|
||||
/* Kundenansicht neu: Elemente bauen, ohne dass Kundentext je als HTML gelesen wird.
|
||||
Texte werden Textknoten, nie innerHTML. Das Modul liest `document` erst beim Aufruf und ist in Node importierbar. */
|
||||
|
||||
/** Element bauen. attrs.class setzt die Klasse, attrs.on = { click: fn } hängt Ereignisse an, true setzt ein Attribut ohne Wert,
|
||||
* false/null/undefined lassen es weg. Kinder werden flach gelegt, Strings und Zahlen werden Textknoten. */
|
||||
export function h(tag, attrs = {}, ...kinder) {
|
||||
const el = document.createElement(tag)
|
||||
for (const [name, wert] of Object.entries(attrs || {})) {
|
||||
if (wert === undefined || wert === null || wert === false) continue
|
||||
if (name === 'class') el.className = wert
|
||||
else if (name === 'on') for (const [typ, fn] of Object.entries(wert)) el.addEventListener(typ, fn)
|
||||
else el.setAttribute(name, wert === true ? '' : String(wert))
|
||||
}
|
||||
for (const kind of kinder.flat(Infinity)) if (kind !== undefined && kind !== null && kind !== false) el.append(typeof kind === 'number' ? String(kind) : kind)
|
||||
return el
|
||||
}
|
||||
|
||||
/** Alle Kinder entfernen. */
|
||||
export function leeren(el) {
|
||||
el.replaceChildren()
|
||||
return el
|
||||
}
|
||||
|
||||
/** Text setzen (nie HTML); null und undefined werden zum leeren Text. */
|
||||
export function text(el, wert) {
|
||||
el.textContent = String(wert ?? '')
|
||||
return el
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/* Kundenansicht neu: Etage 2. Eine Unterseite (Funktion, Detailseite, Passwort …) schiebt sich als Blatt über die Hauptseite.
|
||||
Handy: Die Zeile unten fährt eine Etage hoch und zeigt „Zurück“ und, falls die Seite Reiter meldet, deren Kurzlabels als Balken.
|
||||
Desktop: Brotkrume statt Zeile, Reiter als Unterstrich im Slot unter der Überschrift.
|
||||
Der Inhalt meldet Reiter und Titel ohne Import per CustomEvent (bubbles): kn:reiter { eintraege, aktiv, bei } (leere Liste = keine Reiter)
|
||||
und kn:titel { text }. Alles Sichtbare wird mit h() und textContent gebaut. */
|
||||
|
||||
import { h, leeren, text } from './dom.js'
|
||||
import { baueReiter } from './reiter.js'
|
||||
|
||||
const gleicheEintraege = (a, b) => a.length === b.length && a.every((x, i) => x.key === b[i].key && x.label === b[i].label && (x.kurz ?? '') === (b[i].kurz ?? ''))
|
||||
|
||||
export function baueEtage({ blatt, haupt, zeile, titel, brot, reiterSlot, istHandy, onZurueck }) {
|
||||
const stand = { offen: false, titel: '', eltern: null, seite: '', reiter: null, chatPunkt: false, reiterEl: null, zurueckEl: null }
|
||||
|
||||
const zurueckLabel = () => `Zurück zu ${stand.eltern.label}${stand.chatPunkt ? ', neue Nachricht im Chat' : ''}`
|
||||
|
||||
function baueZurueck(voll) {
|
||||
return h('button', { class: voll ? 'kn-zurueck kn-zurueck--voll' : 'kn-zurueck', type: 'button', 'aria-label': zurueckLabel(), on: { click: () => onZurueck() } },
|
||||
h('span', { class: 'kn-l' }, 'Zurück', stand.chatPunkt ? h('span', { class: 'kn-punkt' }) : null))
|
||||
}
|
||||
|
||||
function neuerReiter(art) {
|
||||
const { eintraege, aktiv } = stand.reiter
|
||||
// Handy zeigt die Kurzlabels (der Balken hat wenig Platz), Desktop die vollen Beschriftungen
|
||||
const zeigen = art === 'balken' ? eintraege : eintraege.map(({ kurz, ...rest }) => rest)
|
||||
return baueReiter({ eintraege: zeigen, aktiv, art, ariaLabel: 'Reiter', bei: (key) => stand.reiter?.bei?.(key) })
|
||||
}
|
||||
|
||||
function zeichneBrot() {
|
||||
leeren(brot)
|
||||
if (!stand.offen) return
|
||||
const verlassen = (ev) => {
|
||||
if (ev.defaultPrevented || ev.button > 0 || ev.metaKey || ev.ctrlKey || ev.shiftKey || ev.altKey) return
|
||||
ev.preventDefault() // die Brotkrume führt wie „Zurück“ zur Elternseite, ohne neuen Verlaufseintrag
|
||||
onZurueck()
|
||||
}
|
||||
brot.append(h('a', { href: stand.eltern.hash, on: { click: verlassen } }, stand.eltern.label), h('span', { class: 'kn-brot-trenner', 'aria-hidden': 'true' }, ' / '), h('span', { 'aria-current': 'page' }, stand.titel))
|
||||
}
|
||||
|
||||
// Zeile (Handy) und Reiter-Slot (Desktop) aus dem Stand neu aufbauen
|
||||
function zeichne() {
|
||||
leeren(zeile); leeren(reiterSlot)
|
||||
stand.zurueckEl = null; stand.reiterEl = null
|
||||
zeichneBrot()
|
||||
if (!stand.offen) return
|
||||
if (istHandy()) {
|
||||
stand.zurueckEl = baueZurueck(!stand.reiter)
|
||||
zeile.append(stand.zurueckEl)
|
||||
if (stand.reiter) {
|
||||
stand.reiterEl = neuerReiter('balken')
|
||||
zeile.append(h('span', { class: 'kn-vl' }), stand.reiterEl)
|
||||
}
|
||||
} else if (stand.reiter) {
|
||||
stand.reiterEl = neuerReiter('unterstrich')
|
||||
reiterSlot.append(stand.reiterEl)
|
||||
}
|
||||
stand.reiterEl?.messen()
|
||||
}
|
||||
|
||||
function oeffne({ titel: titelText, eltern, seite }) {
|
||||
const warOffen = stand.offen
|
||||
Object.assign(stand, { offen: true, titel: titelText, eltern, seite, reiter: null })
|
||||
text(titel, titelText)
|
||||
for (const s of blatt.querySelectorAll('section[data-etage]')) s.hidden = s.getAttribute('data-etage') !== seite
|
||||
haupt.setAttribute('inert', '')
|
||||
if (!warOffen) {
|
||||
blatt.hidden = false
|
||||
void blatt.offsetWidth // Ausgangslage festschreiben, damit das Blatt von unten einfährt
|
||||
blatt.classList.add('kn-blatt--auf')
|
||||
}
|
||||
zeichne()
|
||||
}
|
||||
|
||||
function schliesse() {
|
||||
stand.offen = false
|
||||
stand.reiter = null
|
||||
blatt.classList.remove('kn-blatt--auf')
|
||||
blatt.hidden = true
|
||||
haupt.removeAttribute('inert')
|
||||
zeichne()
|
||||
}
|
||||
|
||||
function setzeReiter({ eintraege = [], aktiv = '', bei } = {}) {
|
||||
if (!stand.offen) return
|
||||
const alt = stand.reiter
|
||||
stand.reiter = eintraege.length ? { eintraege, aktiv, bei } : null
|
||||
// Gleiche Reiter bleiben stehen (der Balken fährt weiter), nur die Auswahl wechselt
|
||||
if (alt && stand.reiter && stand.reiterEl && gleicheEintraege(alt.eintraege, eintraege)) stand.reiterEl.setzeAktiv(aktiv)
|
||||
else zeichne()
|
||||
}
|
||||
|
||||
function setzeChatPunkt(an) {
|
||||
stand.chatPunkt = Boolean(an)
|
||||
const knopf = stand.zurueckEl
|
||||
if (!knopf) return
|
||||
const wort = knopf.querySelector('.kn-l')
|
||||
const punkt = wort.querySelector('.kn-punkt')
|
||||
if (stand.chatPunkt && !punkt) wort.append(h('span', { class: 'kn-punkt' }))
|
||||
else if (!stand.chatPunkt && punkt) punkt.remove()
|
||||
knopf.setAttribute('aria-label', zurueckLabel())
|
||||
}
|
||||
|
||||
// Meldungen aus dem Inhalt; Meldungen einer verborgenen Seite oder nach dem Schließen gelten nicht
|
||||
const vonDieserSeite = (ev) => {
|
||||
const s = ev.target?.closest?.('section[data-etage]')
|
||||
return stand.offen && (!s || s.getAttribute('data-etage') === stand.seite)
|
||||
}
|
||||
blatt.addEventListener('kn:reiter', (ev) => { if (vonDieserSeite(ev)) setzeReiter(ev.detail ?? {}) })
|
||||
blatt.addEventListener('kn:titel', (ev) => {
|
||||
if (!vonDieserSeite(ev) || !ev.detail?.text) return
|
||||
stand.titel = String(ev.detail.text)
|
||||
text(titel, stand.titel)
|
||||
zeichneBrot()
|
||||
})
|
||||
|
||||
return { oeffne, schliesse, setzeReiter, neuZeichnen: zeichne, setzeChatPunkt, istOffen: () => stand.offen }
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/* Kundenansicht neu: Widerrufsstreifen. Läuft eine Widerrufsfrist, steht über der Leiste ein schmaler Streifen
|
||||
„Widerrufsfrist bis TT.MM. · Widerrufen“. Er ist nicht wegtippbar. Die Daten kommen von GET /api/fristen; jeder Fehler
|
||||
(404 auf einem Server ohne P5, 500, Netz weg) und anzeigen !== true ergeben keinen Streifen. Daten in Europe/Berlin. */
|
||||
|
||||
import { h, leeren } from './dom.js'
|
||||
|
||||
/** Fristen vom Server holen; bei Fehlern jeder Art oder anzeigen !== true null. */
|
||||
export async function ladeFristen(api) {
|
||||
try {
|
||||
const daten = await api('/api/fristen')
|
||||
return daten?.anzeigen === true ? daten : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/** Wie ladeFristen, aber für das Neuladen in der Sitzung: ein Fehler ergibt undefined (der bisherige Stand bleibt), keine Frist null. */
|
||||
export async function ladeFristenNeu(api) {
|
||||
try {
|
||||
const daten = await api('/api/fristen')
|
||||
return daten?.anzeigen === true ? daten : null
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
const datumBerlin = new Intl.DateTimeFormat('de-DE', { timeZone: 'Europe/Berlin', day: '2-digit', month: '2-digit' })
|
||||
|
||||
/** 'Widerrufsfrist bis TT.MM.' in der Zeit von Berlin; ohne gültiges Datum 'Widerrufsfrist läuft'. */
|
||||
export function streifenText(iso) {
|
||||
const zeit = new Date(iso)
|
||||
if (!iso || Number.isNaN(zeit.getTime())) return 'Widerrufsfrist läuft'
|
||||
const teile = Object.fromEntries(datumBerlin.formatToParts(zeit).map((t) => [t.type, t.value]))
|
||||
return `Widerrufsfrist bis ${teile.day}.${teile.month}.`
|
||||
}
|
||||
|
||||
/** Frühestes Ende als ISO-Text: fruehestesEnde, sonst das kleinste bis, sonst ''. */
|
||||
export function fruehesteFrist(daten) {
|
||||
if (daten?.fruehestesEnde) return daten.fruehestesEnde
|
||||
const zeiten = (daten?.fristen ?? []).map((f) => f?.bis).filter((b) => b && !Number.isNaN(Date.parse(b)))
|
||||
return zeiten.sort((a, b) => Date.parse(a) - Date.parse(b))[0] ?? ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Streifen in `wurzel` füllen: setze(daten) nimmt die Fristen (null: keine), erlaube(ok) sagt, ob die gezeigte Seite den Streifen tragen darf
|
||||
* (nicht die Bestellseiten, siehe zeigtWiderrufsstreifen). Sichtbar ist er nur mit Fristen auf einer erlaubten Seite; am Körper steht dann `kn-hat-streifen`.
|
||||
*/
|
||||
export function baueStreifen(wurzel, { koerper = document.body } = {}) {
|
||||
wurzel.classList.add('kn-streifen')
|
||||
wurzel.setAttribute('role', 'region')
|
||||
wurzel.setAttribute('aria-label', 'Widerrufsfrist')
|
||||
let stand = null
|
||||
let erlaubt = true
|
||||
const zeichne = () => {
|
||||
leeren(wurzel)
|
||||
if (!stand || !erlaubt) {
|
||||
wurzel.hidden = true
|
||||
koerper.classList.remove('kn-hat-streifen')
|
||||
return
|
||||
}
|
||||
wurzel.append(streifenText(fruehesteFrist(stand)), ' · ', h('a', { class: 'kn-streifen-link', href: '/widerrufen.html' }, 'Widerrufen'))
|
||||
wurzel.hidden = false
|
||||
koerper.classList.add('kn-hat-streifen')
|
||||
}
|
||||
return {
|
||||
setze(daten) { stand = daten || null; zeichne() },
|
||||
erlaube(ok) {
|
||||
if ((ok !== false) === erlaubt) return
|
||||
erlaubt = ok !== false
|
||||
zeichne()
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,440 @@
|
||||
/* Kundenansicht neu (P3): Einschalten einer Funktion im Abo-Modell.
|
||||
Dieser Teil ist rein (ohne DOM, ohne Netz) und läuft unter Node: Wege nach der Vorprüfung, Fristen, Körper des Absendens, Fehlerfolgen.
|
||||
Die Vorprüfung spiegelt aboDeckt (server/services/aboFunktionen.js) nur, um den richtigen Ort zu wählen; entschieden wird immer vom Server
|
||||
(POST /api/funktionen/<key>/einschalten, 409 mit Code). Kundentexte sind Festwerte (TEXTE), Fremdtext kommt nie als HTML in die Seite. */
|
||||
|
||||
import { h, leeren } from './dom.js'
|
||||
import { api as apiStandard } from './api.js'
|
||||
import { geheZu as geheZuStandard } from './router.js'
|
||||
|
||||
/** Etage 2 je Funktion (Schlüssel der Hülle in FUNKTIONS_MODULE, Hash `#<etage>?projekt=<id>`). */
|
||||
export const ETAGE_FUER = Object.freeze({ news: 'news', termine: 'termine', shop: 'webshop', unterkunft: 'buchungen', zeiten: 'zeiten', analyse: 'analyse' })
|
||||
|
||||
/** Festtexte der Oberfläche (du-Form, kein Preis). */
|
||||
export const TEXTE = Object.freeze({
|
||||
stundenkonto: 'Die Einrichtung geht von deinem Stundenkonto ab, auch wenn das Guthaben noch nicht reicht. Extrakosten entstehen keine.',
|
||||
nichtVerfuegbar: 'Diese Funktion ist für dich gerade nicht verfügbar.',
|
||||
gekuendigt: 'Dein Abo ist gekündigt. Solange die Kündigung läuft, kannst du nichts Neues einschalten.',
|
||||
nichtAktiv: 'Dein Abo ist noch nicht aktiv. Sobald die erste Zahlung da ist, kannst du einschalten.',
|
||||
falschesAbo: 'Dein Abo enthält diese Funktion nicht.',
|
||||
beginntSpaeter: (datum) => `Dein Abo beginnt am ${datum}. Ab dann kannst du einschalten.`,
|
||||
fassungVeraltet: 'Die Bedingungen wurden aktualisiert. Bitte lies die Zeile noch einmal.',
|
||||
allgemein: 'Das Einschalten hat gerade nicht geklappt. Versuch es gleich noch einmal oder schreib uns im Chat.',
|
||||
nurBetrieb: (name) => `${name} gibt es für Betriebe. Stimmt das nicht? Schreib uns im Chat.`,
|
||||
registerFehlt: 'Einschalten ist gerade nicht möglich. Schreib uns im Chat.',
|
||||
kundenansicht: 'In der Kundenansicht kann nichts eingeschaltet werden.',
|
||||
kundenansichtKnopf: 'In der Kundenansicht nicht möglich.', // Hinweis neben dem gesperrten Knopf in Entdecken
|
||||
laden: 'Die Angaben konnten gerade nicht geladen werden. Bitte versuch es gleich noch einmal.',
|
||||
})
|
||||
|
||||
/** Die Erklärung der Funktion aus dem Katalog der Antwort (einzige Stelle für das Register); fehlt sie, null. */
|
||||
export function consentVon(daten, key) {
|
||||
const eintrag = Array.isArray(daten?.katalog) ? daten.katalog.find((e) => e?.key === key) : null
|
||||
const consent = eintrag?.consent
|
||||
return consent && typeof consent === 'object' && Array.isArray(consent.items) ? consent : null
|
||||
}
|
||||
|
||||
/** Anfragestatus von Webshop oder Buchungskalender an einer Website (N18, einzige Stelle); sonst ''. */
|
||||
export function anfrageStatusVon(projekt, key) {
|
||||
const status = projekt?.features?.[key]?.anfrageStatus
|
||||
return typeof status === 'string' ? status : ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Wohin führt „Einschalten“? Prüfreihenfolge wie der Server (V5): verfügbar, gekündigt, kein Abo, nicht aktiv, Hosting bei einer
|
||||
* Wartungs-Funktion, nur Betrieb; danach Etage (Analyse, Fragebogen, Anfrage ohne Zusage) oder die Bestätigungsseite.
|
||||
* Alles außer 'unternehmen' gilt als privat (E4).
|
||||
*/
|
||||
export function einschaltWeg({ eintrag, projekt, abo, kundenart }) {
|
||||
if (eintrag?.verfuegbar !== true) return { art: 'meldung', text: TEXTE.nichtVerfuegbar }
|
||||
if (abo?.gekuendigt === true) return { art: 'meldung', text: TEXTE.gekuendigt }
|
||||
if (!abo || abo.produkt === null || abo.produkt === undefined) return { art: 'bestellen', produkt: eintrag.enthaltenIn }
|
||||
if (abo.laeuft !== true) return { art: 'meldung', text: TEXTE.nichtAktiv }
|
||||
if (eintrag.enthaltenIn === 'wartung' && abo.produkt === 'hosting') return { art: 'wechsel' }
|
||||
if (eintrag.nurBetrieb === true && kundenart !== 'unternehmen') return { art: 'meldung', text: TEXTE.nurBetrieb(eintrag.name) }
|
||||
if (eintrag.key === 'analyse') return { art: 'etage', etage: ETAGE_FUER.analyse }
|
||||
if (eintrag.ablauf === 'fragebogen') return { art: 'etage', etage: ETAGE_FUER[eintrag.key] }
|
||||
if (eintrag.ablauf === 'anfrage' && anfrageStatusVon(projekt, eintrag.key) !== 'angenommen') return { art: 'etage', etage: ETAGE_FUER[eintrag.key] }
|
||||
return { art: 'seite' }
|
||||
}
|
||||
|
||||
const TAG = /^\d{4}-\d{2}-\d{2}$/
|
||||
|
||||
/** Berliner Kalendertag 'JJJJ-MM-TT' eines Zeitpunkts (ISO-Text, Date) oder eines Tagestextes; unlesbar ergibt ''. */
|
||||
function berlinTag(wert) {
|
||||
if (typeof wert === 'string' && TAG.test(wert)) return wert
|
||||
const ms = wert instanceof Date ? wert.getTime() : (typeof wert === 'string' && wert !== '' ? Date.parse(wert) : Number.NaN)
|
||||
if (Number.isNaN(ms)) return ''
|
||||
return new Date(ms).toLocaleDateString('sv-SE', { timeZone: 'Europe/Berlin' })
|
||||
}
|
||||
|
||||
/** Der Berliner Tag nach dem Fristende (Zeitpunkt) als 'JJJJ-MM-TT'; unlesbar ergibt ''. */
|
||||
export function tagNachFrist(wert) {
|
||||
const tag = berlinTag(wert)
|
||||
if (!tag) return ''
|
||||
const [j, m, t] = tag.split('-').map(Number)
|
||||
return new Date(Date.UTC(j, m - 1, t + 1)).toISOString().slice(0, 10)
|
||||
}
|
||||
|
||||
/** 'JJJJ-MM-TT' → 'TT.MM.'; sonst ''. */
|
||||
export function datumKurz(tag) {
|
||||
if (typeof tag !== 'string' || !TAG.test(tag)) return ''
|
||||
const [, m, t] = tag.split('-')
|
||||
return `${t}.${m}.`
|
||||
}
|
||||
|
||||
/** 'JJJJ-MM-TT' → 'TT.MM.JJJJ' wie datumDe des Servers; sonst ''. */
|
||||
function datumLang(tag) {
|
||||
if (typeof tag !== 'string' || !TAG.test(tag)) return ''
|
||||
const [j, m, t] = tag.split('-')
|
||||
return `${t}.${m}.${j}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Braucht der Kunde das Häkchen „sofortiger Beginn“, und ab wann ginge es ohne? Nur Privatkunden (leer zählt als privat) und nur bis
|
||||
* zum Fristende (am Ende selbst noch Frist). Eine Antwort des Servers (serverAb: ISO oder Tag) gewinnt.
|
||||
* @returns {{ noetig: boolean, ab: string }} ab = 'JJJJ-MM-TT' oder ''
|
||||
*/
|
||||
export function beginnLage({ kundenart, abo, jetzt, serverAb = '' }) {
|
||||
const vomServer = berlinTag(serverAb)
|
||||
if (vomServer) return { noetig: true, ab: vomServer }
|
||||
const bis = abo?.widerrufsfristBis
|
||||
const ende = typeof bis === 'string' ? Date.parse(bis) : Number.NaN
|
||||
if (kundenart !== 'unternehmen' && ende >= jetzt.getTime()) return { noetig: true, ab: tagNachFrist(bis) }
|
||||
return { noetig: false, ab: '' }
|
||||
}
|
||||
|
||||
/** Körper des Absendens: Fassung aus dem Register, Haken nur 'beginn'; die Kundenart sendet der Client nie. */
|
||||
export function einschaltKoerper({ projectId, consent, beginnGesetzt }) {
|
||||
return { projectId, zustimmung: { version: consent.version, haken: beginnGesetzt ? ['beginn'] : [] } }
|
||||
}
|
||||
|
||||
/** Nur https ohne Zugangsdaten oder Pfade unter /rechtliches/, /vertragsunterlagen/ und /neu/rechtliches/ (Fassung 6); sonst ''. */
|
||||
export function sichereAdresse(url) {
|
||||
if (typeof url !== 'string' || url === '') return ''
|
||||
if (/[\s\u0000-\u001f\u007f\\]/.test(url) || url.includes('..') || /%(2e|2f|5c)/i.test(url)) return ''
|
||||
if (url.startsWith('/rechtliches/') || url.startsWith('/vertragsunterlagen/') || url.startsWith('/neu/rechtliches/')) return url
|
||||
if (!url.startsWith('https://')) return ''
|
||||
try {
|
||||
const u = new URL(url)
|
||||
return u.protocol === 'https:' && u.hostname !== '' && u.username === '' && u.password === '' ? url : ''
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
const UNTERLAGEN_WOERTER = [['Leistungsbeschreibung', 'leistungsbeschreibung'], ['Auftragsverarbeitung', 'avv']]
|
||||
|
||||
/**
|
||||
* Die Zeile aus dem Register als Stücke [{ text, href? }]: das erste Vorkommen von „Leistungsbeschreibung“ und „Auftragsverarbeitung“
|
||||
* wird zum Link, wenn die Unterlage eine sichere Adresse hat. Fehlt das Wort bei einer sicheren Unterlage, hängt ihr Link hinten an
|
||||
* (zwei Links mit „ · “ dazwischen). Benachbarte Textstücke sind zusammengefasst.
|
||||
*/
|
||||
export function linkzeile(text, unterlagen) {
|
||||
const basis = typeof text === 'string' ? text : ''
|
||||
const treffer = []
|
||||
const anhang = []
|
||||
for (const [wort, schluessel] of UNTERLAGEN_WOERTER) {
|
||||
const href = sichereAdresse(unterlagen?.[schluessel]?.url)
|
||||
if (!href) continue
|
||||
const pos = basis.indexOf(wort)
|
||||
if (pos >= 0) treffer.push({ pos, wort, href })
|
||||
else anhang.push({ wort, href })
|
||||
}
|
||||
treffer.sort((a, b) => a.pos - b.pos)
|
||||
const stuecke = []
|
||||
const textStueck = (t) => {
|
||||
if (t === '') return
|
||||
const letztes = stuecke.at(-1)
|
||||
if (letztes && letztes.href === undefined) letztes.text += t
|
||||
else stuecke.push({ text: t })
|
||||
}
|
||||
let ab = 0
|
||||
for (const t of treffer) {
|
||||
textStueck(basis.slice(ab, t.pos))
|
||||
stuecke.push({ text: t.wort, href: t.href })
|
||||
ab = t.pos + t.wort.length
|
||||
}
|
||||
textStueck(basis.slice(ab))
|
||||
anhang.forEach((a, i) => {
|
||||
textStueck(i === 0 ? ' ' : ' · ')
|
||||
stuecke.push({ text: a.wort, href: a.href })
|
||||
})
|
||||
return stuecke
|
||||
}
|
||||
|
||||
/**
|
||||
* Was folgt aus einem Fehler des Absendens? fehler = ApiFehler ({ status, code?, message, body }).
|
||||
* wechsel/bestellen/etage führen weg, beginn und spaeter sperren den Knopf bis zum Datum (beginn: Häkchen möglich), neu-laden lädt die
|
||||
* Liste neu, schon heißt: ist schon eingeschaltet, meldung zeigt den Text. Alles ohne eigene Regel zeigt den Text des Servers.
|
||||
*/
|
||||
export function fehlerAktion(fehler, { eintrag }) {
|
||||
const body = fehler?.body && typeof fehler.body === 'object' ? fehler.body : {}
|
||||
const code = fehler?.code || body.code || ''
|
||||
const servertext = typeof fehler?.message === 'string' && fehler.message !== '' ? fehler.message : TEXTE.allgemein
|
||||
const meldung = (text) => ({ art: 'meldung', text })
|
||||
switch (code) {
|
||||
case 'nicht_verfuegbar':
|
||||
return meldung(TEXTE.nichtVerfuegbar)
|
||||
case 'abo_fehlt': {
|
||||
if (body.wechsel === true) return { art: 'wechsel' }
|
||||
if (body.grund === 'kein_abo') return { art: 'bestellen', produkt: eintrag.enthaltenIn }
|
||||
if (body.grund === 'gekuendigt') return meldung(TEXTE.gekuendigt)
|
||||
if (body.grund === 'nicht_aktiv') return meldung(TEXTE.nichtAktiv)
|
||||
if (body.grund === 'falsches_abo') return meldung(TEXTE.falschesAbo)
|
||||
if (body.grund === 'beginnt_spaeter') {
|
||||
const tag = berlinTag(body.ab)
|
||||
return tag ? { art: 'spaeter', ab: body.ab, text: TEXTE.beginntSpaeter(datumLang(tag)) } : meldung(servertext)
|
||||
}
|
||||
return meldung(servertext)
|
||||
}
|
||||
case 'nur_betrieb':
|
||||
return meldung(TEXTE.nurBetrieb(eintrag.name))
|
||||
case 'bestellung_offen':
|
||||
return { art: 'etage', etage: ETAGE_FUER[eintrag.key] }
|
||||
case 'beginn_noetig':
|
||||
return { art: 'beginn', ab: body.ab }
|
||||
case 'bereits_eingeschaltet':
|
||||
return { art: 'schon' }
|
||||
case 'fassung_veraltet':
|
||||
return { art: 'neu-laden', text: TEXTE.fassungVeraltet }
|
||||
default:
|
||||
return meldung(servertext)
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------------------------- Bestätigungsseite --- */
|
||||
|
||||
/**
|
||||
* Die Bestätigungsseite zum Einschalten zeichnen (Aufbau und Zustand von Häkchen und Knopf; Senden steht weiter unten).
|
||||
* @param {HTMLElement} behaelter Platz, in dem die Seite gezeichnet wird (sein Inhalt wird ersetzt)
|
||||
* @param {object} o
|
||||
* key, projectId; daten = Antwort von GET /api/funktionen (Abo-Modell); kundenartWirksam (leer oder unbekannt zählt als privat);
|
||||
* viewAs (Kundenansicht der Mitarbeiter: nichts einschalten); imModul (ein Funktionsmodul bettet die Seite ein: kein „Abbrechen“);
|
||||
* jetzt, api, geheZu, speicher (einspeisbar für Tests); bei = { fertig, weiter, abbrechen, neuLaden } (alle optional)
|
||||
* @returns {HTMLElement} section.kn-fn-einschalten
|
||||
*/
|
||||
export function renderEinschalten(behaelter, o) {
|
||||
const { key, projectId, viewAs = false, imModul = false, jetzt = () => new Date() } = o
|
||||
let daten = o.daten
|
||||
const api = o.api ?? apiStandard
|
||||
const geheZu = o.geheZu ?? geheZuStandard
|
||||
const kundenart = () => (o.kundenartWirksam === undefined ? daten?.kundenart : o.kundenartWirksam)
|
||||
const z = { haken: false, serverAb: '', sperreAb: '', erzwingeHaken: false, sendet: false, meldung: '' }
|
||||
// Standards der Rückmeldungen (Task 4): Erfolg merkt die Meldung und geht zu Meine, Abbrechen zu Entdecken
|
||||
const bei = {
|
||||
fertig: (antwort) => { merkeMeldung(erfolgText(String(eintragVon()?.name ?? key), antwort)); geheZu('#funktionen', { ersetzen: true }) },
|
||||
weiter: (weg) => navigiere(weg, { key, projectId }, { geheZu, speicher: o.speicher }),
|
||||
abbrechen: () => geheZu('#entdecken', { ersetzen: true }),
|
||||
neuLaden: () => api('/api/funktionen'),
|
||||
...o.bei,
|
||||
}
|
||||
const abschnitt = h('section', { class: 'kn-fn-einschalten', 'data-key': key, 'data-projekt': projectId })
|
||||
let knopf = null
|
||||
let meldungEl = null
|
||||
|
||||
const eintragVon = () => (Array.isArray(daten?.katalog) ? daten.katalog.find((e) => e?.key === key) ?? null : null)
|
||||
const lageJetzt = () => {
|
||||
const lage = beginnLage({ kundenart: kundenart(), abo: daten?.abo, jetzt: jetzt(), serverAb: z.serverAb })
|
||||
return z.erzwingeHaken && !lage.noetig ? { noetig: true, ab: '' } : lage // beginn_noetig ohne lesbares Datum: Häkchen trotzdem zeigen
|
||||
}
|
||||
|
||||
function aktualisiereKnopf() {
|
||||
if (!knopf) return
|
||||
const lage = lageJetzt()
|
||||
const fehltHaken = lage.noetig && !z.haken
|
||||
const ab = z.sperreAb || (fehltHaken ? lage.ab : '')
|
||||
knopf.disabled = viewAs || z.sendet || fehltHaken || Boolean(z.sperreAb)
|
||||
knopf.textContent = ab && datumKurz(ab) ? `Einschalten ab ${datumKurz(ab)}` : 'Einschalten'
|
||||
}
|
||||
|
||||
function setzeMeldung(text) {
|
||||
z.meldung = text
|
||||
if (meldungEl) meldungEl.textContent = text
|
||||
}
|
||||
|
||||
function fuelle() {
|
||||
const eintrag = eintragVon()
|
||||
const consent = consentVon(daten, key)
|
||||
const items = consent?.items ?? []
|
||||
const item = (id) => items.find((i) => i?.id === id) ?? null
|
||||
const zeile = item('einschalten-zeile')
|
||||
const hinweis = item('hinweis')
|
||||
const beginn = item('beginn')
|
||||
const lage = lageJetzt()
|
||||
const register = Boolean(eintrag && zeile && (!lage.noetig || beginn))
|
||||
|
||||
const seiten = Array.isArray(daten?.projects) ? daten.projects : []
|
||||
const websiteName = seiten.find((p) => p?.projectId === projectId)?.projectName
|
||||
const abo = daten?.abo?.produkt ?? eintrag?.enthaltenIn
|
||||
const fakten = [
|
||||
seiten.length > 1 ? ['Website', String(websiteName ?? '')] : null,
|
||||
['Leistung', 'Einrichtung durch WEBklar'],
|
||||
['Dauer', String(eintrag?.einrichtung ?? '')],
|
||||
['Laufzeit', `läuft, solange dein ${abo === 'hosting' ? 'Hosting-Abo' : 'Wartungs-Abo'} läuft`],
|
||||
].filter(Boolean)
|
||||
|
||||
const box = h('input', { type: 'checkbox', 'data-haken': 'beginn', on: { change: () => { z.haken = box.checked; aktualisiereKnopf() } } })
|
||||
box.checked = z.haken
|
||||
meldungEl = h('p', { class: 'kn-fn-meldung', role: 'alert', 'data-meldung': true }, z.meldung)
|
||||
knopf = register ? h('button', { type: 'button', class: 'kn-fn-knopf', 'data-aktion': 'einschalten', on: { click: () => senden() } }, 'Einschalten') : null
|
||||
|
||||
const kinder = [
|
||||
h('h2', null, `${String(eintrag?.name ?? key)} einschalten`),
|
||||
h('dl', { class: 'kn-fn-fakten' }, fakten.map(([name, wert]) => h('div', null, h('dt', null, name), h('dd', null, wert)))),
|
||||
h('p', { class: 'kn-fn-konto' }, TEXTE.stundenkonto),
|
||||
register
|
||||
? [
|
||||
h('p', { class: 'kn-fn-recht' }, linkzeile(zeile.text, eintrag.unterlagen).map((s) => (s.href ? h('a', { href: s.href, target: '_blank', rel: 'noopener noreferrer' }, s.text) : s.text))),
|
||||
hinweis ? h('p', { class: 'kn-fn-hinweis' }, String(hinweis.text ?? '')) : null,
|
||||
lage.noetig ? h('label', { class: 'kn-fn-haken' }, box, h('span', null, String(beginn.text ?? ''))) : null,
|
||||
]
|
||||
: h('p', { class: 'kn-fn-info' }, TEXTE.registerFehlt),
|
||||
viewAs ? h('p', { class: 'kn-fn-info' }, TEXTE.kundenansicht) : null,
|
||||
meldungEl,
|
||||
h('div', { class: 'kn-fn-aktionen' },
|
||||
knopf ?? h('button', { type: 'button', class: 'kn-fn-knopf', 'data-aktion': 'chat', on: { click: () => geheZu('#chat') } }, 'Im Chat schreiben'),
|
||||
imModul ? null : h('button', { type: 'button', class: 'kn-fn-knopf kn-fn-knopf--zweit', 'data-aktion': 'abbrechen', on: { click: () => bei.abbrechen() } }, 'Abbrechen')),
|
||||
]
|
||||
abschnitt.replaceChildren(...kinder.flat(Infinity).filter(Boolean))
|
||||
aktualisiereKnopf()
|
||||
}
|
||||
|
||||
async function senden() {
|
||||
if (z.sendet || viewAs || z.sperreAb) return
|
||||
const eintrag = eintragVon()
|
||||
const consent = consentVon(daten, key)
|
||||
const lage = lageJetzt()
|
||||
if (!eintrag || !consent || (lage.noetig && !z.haken)) return
|
||||
z.sendet = true
|
||||
setzeMeldung('')
|
||||
aktualisiereKnopf()
|
||||
let antwort
|
||||
try {
|
||||
antwort = await api(`/api/funktionen/${encodeURIComponent(key)}/einschalten`, {
|
||||
method: 'POST',
|
||||
body: einschaltKoerper({ projectId, consent, beginnGesetzt: lage.noetig && z.haken }),
|
||||
})
|
||||
} catch (fehler) {
|
||||
z.sendet = false
|
||||
await behandleFehler(fehler, eintrag)
|
||||
return
|
||||
}
|
||||
bei.fertig(antwort) // der Knopf bleibt gesperrt: nach Erfolg gibt es nichts mehr zu senden
|
||||
}
|
||||
|
||||
async function behandleFehler(fehler, eintrag) {
|
||||
const aktion = fehlerAktion(fehler, { eintrag })
|
||||
switch (aktion.art) {
|
||||
case 'wechsel':
|
||||
case 'bestellen':
|
||||
aktualisiereKnopf()
|
||||
bei.weiter(aktion)
|
||||
break
|
||||
case 'etage': // im Modul steht der Servertext (die Etage ist schon offen), sonst führt die Seite hin
|
||||
if (imModul) { setzeMeldung(fehler?.message || TEXTE.allgemein); aktualisiereKnopf() } else { aktualisiereKnopf(); bei.weiter(aktion) }
|
||||
break
|
||||
case 'schon':
|
||||
bei.fertig(null)
|
||||
break
|
||||
case 'beginn': // Häkchen zeigen, Knopf bis dahin gesperrt mit Datum
|
||||
z.serverAb = typeof aktion.ab === 'string' ? aktion.ab : ''
|
||||
z.erzwingeHaken = true
|
||||
z.haken = false
|
||||
fuelle()
|
||||
abschnitt.querySelector('input[data-haken="beginn"]')?.focus?.()
|
||||
break
|
||||
case 'spaeter': // das Abo beginnt später: Text und gesperrter Knopf mit Datum, kein Häkchen
|
||||
z.sperreAb = berlinTag(aktion.ab)
|
||||
setzeMeldung(aktion.text)
|
||||
aktualisiereKnopf()
|
||||
break
|
||||
case 'neu-laden':
|
||||
try {
|
||||
daten = await bei.neuLaden()
|
||||
z.haken = false
|
||||
z.serverAb = ''
|
||||
z.erzwingeHaken = false
|
||||
setzeMeldung(aktion.text)
|
||||
fuelle()
|
||||
} catch {
|
||||
setzeMeldung(TEXTE.allgemein)
|
||||
aktualisiereKnopf()
|
||||
}
|
||||
break
|
||||
default:
|
||||
setzeMeldung(aktion.text)
|
||||
aktualisiereKnopf()
|
||||
}
|
||||
}
|
||||
|
||||
leeren(behaelter).append(abschnitt)
|
||||
fuelle()
|
||||
return abschnitt
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------------------ Navigation und Meldungen --- */
|
||||
|
||||
/**
|
||||
* Wohin nach einem Weg von einschaltWeg oder fehlerAktion? ziel = { key, projectId }; 'seite' behandelt die Ansicht selbst.
|
||||
* etage → #<etage>?projekt=<id>, bestellen → #bestellen-<produkt>, wechsel → die gewünschte Funktion merken (V11), dann #wechsel.
|
||||
* Der Wechsel geht auch, wenn sessionStorage fehlt oder voll ist.
|
||||
*/
|
||||
export async function navigiere(weg, ziel, { geheZu = geheZuStandard, speicher } = {}) {
|
||||
switch (weg?.art) {
|
||||
case 'etage':
|
||||
geheZu(`#${weg.etage}?projekt=${encodeURIComponent(ziel.projectId)}`)
|
||||
break
|
||||
case 'bestellen':
|
||||
geheZu(`#bestellen-${weg.produkt}`)
|
||||
break
|
||||
case 'wechsel':
|
||||
try {
|
||||
const ablage = speicher ?? globalThis.sessionStorage
|
||||
ablage.setItem('kn-wechsel-funktion', JSON.stringify({ key: ziel.key, projectId: ziel.projectId }))
|
||||
} catch { /* ohne Speicher geht der Wechsel trotzdem; nur die Rückkehr zur Funktion entfällt */ }
|
||||
geheZu('#wechsel')
|
||||
break
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
/** Text nach dem Einschalten; antwort null heißt: war schon eingeschaltet. */
|
||||
export function erfolgText(name, antwort) {
|
||||
if (!antwort) return `${name} ist schon eingeschaltet.`
|
||||
const woid = antwort.ticketWoid ?? antwort.feature?.ticketWoid // der Server nennt das Ticket an beiden Stellen
|
||||
const ticket = woid ? ` Ticket #${woid}.` : ''
|
||||
return `${name} ist eingeschaltet. Die Einrichtung läuft jetzt.${ticket}`
|
||||
}
|
||||
|
||||
let gemerkteMeldung = ''
|
||||
/** Merkt genau eine Meldung für den nächsten Aufbau von Meine oder Entdecken. */
|
||||
export function merkeMeldung(text) { gemerkteMeldung = String(text ?? '') }
|
||||
/** Gibt die gemerkte Meldung zurück und vergisst sie. */
|
||||
export function nimmMeldung() {
|
||||
const text = gemerkteMeldung
|
||||
gemerkteMeldung = ''
|
||||
return text
|
||||
}
|
||||
|
||||
/**
|
||||
* Vertrag mit den Funktionsmodulen (P4, VF1): die Bestätigungsseite in `platz`, ohne „Abbrechen“. Lädt GET /api/funktionen, wenn
|
||||
* `daten` fehlt. onFertig({ feature, ticketWoid }) nach 200, onFertig({ feature: null, ticketWoid: null }) nach 409 bereits_eingeschaltet;
|
||||
* alle anderen Fehler zeigt die Seite selbst. Liefert die gezeichnete Seite (oder die Fehlerzeile beim Laden).
|
||||
*/
|
||||
export async function zeigeEinschalten(platz, { key, projectId, onFertig, daten, kundenartWirksam, viewAs = false, api = apiStandard, geheZu, speicher, jetzt } = {}) {
|
||||
let liste = daten
|
||||
if (!liste) {
|
||||
leeren(platz).append(h('p', { class: 'kn-leer' }, 'Wird geladen …'))
|
||||
try {
|
||||
liste = await api('/api/funktionen')
|
||||
} catch {
|
||||
return leeren(platz).appendChild(h('p', { class: 'kn-fn-info', role: 'alert' }, TEXTE.laden,
|
||||
h('button', { type: 'button', class: 'kn-fn-knopf kn-fn-knopf--zweit', 'data-aktion': 'nochmal', on: { click: () => zeigeEinschalten(platz, { key, projectId, onFertig, kundenartWirksam, viewAs, api, geheZu, speicher, jetzt }) } }, 'Nochmal versuchen')))
|
||||
}
|
||||
}
|
||||
return renderEinschalten(platz, {
|
||||
key, projectId, daten: liste, kundenartWirksam, viewAs, imModul: true, api, geheZu, speicher, jetzt,
|
||||
bei: { fertig: (antwort) => onFertig?.(antwort ?? { feature: null, ticketWoid: null }) },
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/* Kundenansicht neu (P3): Aussehen der Ansicht Funktionen (Meine, Entdecken) und der Bestätigungsseite zum Einschalten.
|
||||
Besitzer: P3. Weißer Stil: Haarlinien statt Karten, Radien 6 und 12 px (Pillen 999 px), kein Schatten außer dem Ring, kein Gewicht 800,
|
||||
keine Animation. Farben als var(--kn-…, Wert) mit den Werten der Übersicht als Fallback. Tap-Ziele sind 44 px hoch. */
|
||||
|
||||
/* ------------------------------------------------------------------------------------------ Pillen --- */
|
||||
.kn-fn-pille {
|
||||
display: inline-flex; align-items: center; flex: none; height: 20px; padding: 0 8px; border-radius: 999px;
|
||||
font-size: 12px; line-height: 20px; font-weight: 500; white-space: nowrap; font-variant-numeric: tabular-nums;
|
||||
background: var(--kn-grau-bg, #ebeef1); color: var(--kn-grau-fg, #414552);
|
||||
}
|
||||
.kn-fn-pille--grau { background: var(--kn-grau-bg, #ebeef1); color: var(--kn-grau-fg, #414552); }
|
||||
.kn-fn-pille--blau { background: var(--kn-blau-bg, #e0f2fe); color: var(--kn-blau-fg, #075985); }
|
||||
.kn-fn-pille--orange { background: var(--kn-orange-bg, #fcedb9); color: var(--kn-orange-fg, #a82c00); }
|
||||
.kn-fn-pille--violett { background: var(--kn-violett-bg, #efeafe); color: var(--kn-violett-fg, #5b21b6); }
|
||||
.kn-fn-pille--gruen { background: var(--kn-gruen-bg, #d7f7c2); color: var(--kn-gruen-fg, #006908); }
|
||||
.kn-fn-pille--rot { background: var(--kn-rot-bg, #ffe7f2); color: var(--kn-rot-fg, #b3093c); }
|
||||
|
||||
/* ------------------------------------------------------------------------------------------ Knöpfe --- */
|
||||
.kn-fn-knopf {
|
||||
display: inline-flex; align-items: center; justify-content: center; gap: 6px; min-height: 44px; padding: 0 16px;
|
||||
border: 0; border-radius: 6px; font-weight: 500; text-align: center; text-decoration: none; cursor: pointer;
|
||||
background: var(--kn-accent, #111); color: #fff;
|
||||
}
|
||||
/* Hover: kein hellerer oder dunklerer Ton (die Fläche bleibt genau #111), nur etwas durchscheinend */
|
||||
.kn-fn-knopf:hover { opacity: .88; }
|
||||
.kn-fn-knopf:disabled, .kn-fn-knopf:disabled:hover {
|
||||
background: var(--kn-bg-subtle, #f8f9fb); color: var(--kn-text-3, #8a93a3); cursor: not-allowed; opacity: 1;
|
||||
box-shadow: var(--kn-ring, 0 0 0 1px rgba(60,66,87,.16), 0 1px 1px rgba(0,0,0,.06));
|
||||
}
|
||||
.kn-fn-knopf--zweit {
|
||||
background: var(--kn-bg, #fff); color: var(--kn-text, #1a1f36);
|
||||
box-shadow: var(--kn-ring, 0 0 0 1px rgba(60,66,87,.16), 0 1px 1px rgba(0,0,0,.06));
|
||||
}
|
||||
.kn-fn-knopf--zweit:hover { background: var(--kn-bg-hover, #f6f8fa); }
|
||||
|
||||
/* ------------------------------------------------------------------------------------------ Meldungen --- */
|
||||
.kn-fn-meldung { margin-top: 8px; font-size: 13px; line-height: 20px; color: var(--kn-danger, #c0123c); overflow-wrap: anywhere; }
|
||||
.kn-fn-meldung:empty { display: none; }
|
||||
.kn-fn-hinweis, .kn-fn-info { margin: 8px 0 0; font-size: 13px; line-height: 20px; color: var(--kn-text-2, #5e6678); overflow-wrap: anywhere; }
|
||||
.kn-fn-info:first-child { margin-top: 0; }
|
||||
|
||||
/* ------------------------------------------------------------------------------------------ Meine --- */
|
||||
.kn-fn-liste { list-style: none; margin: 0; padding: 0; }
|
||||
.kn-fn-zeile { border-bottom: 1px solid var(--kn-line, #ebeef1); }
|
||||
.kn-fn-zeile:first-child { border-top: 1px solid var(--kn-line, #ebeef1); }
|
||||
.kn-fn-zeile-knopf {
|
||||
display: flex; align-items: center; justify-content: space-between; gap: 16px; width: 100%; min-height: 44px; padding: 12px 0;
|
||||
text-align: left; background: none; border: 0; cursor: pointer; color: var(--kn-text, #1a1f36);
|
||||
}
|
||||
.kn-fn-zeile-knopf:hover .kn-fn-name { color: var(--kn-accent, #111); text-decoration: underline; text-decoration-color: var(--kn-line-strong, #d8dde3); text-underline-offset: 3px; }
|
||||
.kn-fn-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
||||
.kn-fn-name { font-weight: 600; color: var(--kn-text, #1a1f36); overflow-wrap: anywhere; }
|
||||
.kn-fn-zeile2 { font-size: 13px; line-height: 20px; color: var(--kn-text-2, #5e6678); overflow-wrap: anywhere; font-variant-numeric: tabular-nums; }
|
||||
/* rechts: die Pille (nur wenn die Funktion nicht läuft) und der Pfeil, der die Zeile als antippbar zeigt */
|
||||
.kn-fn-rechts { display: flex; align-items: center; flex: none; gap: 12px; }
|
||||
.kn-fn-pfeil { font-size: 20px; line-height: 20px; color: var(--kn-text-2, #5e6678); }
|
||||
|
||||
/* ------------------------------------------------------------------------------------------ Entdecken --- */
|
||||
.kn-fn-eintrag { display: flex; flex-direction: column; gap: 16px; padding: 24px 0; border-top: 1px solid var(--kn-line, #ebeef1); }
|
||||
.kn-fn-eintrag:last-child { border-bottom: 1px solid var(--kn-line, #ebeef1); }
|
||||
.kn-fn-bild { display: block; width: 100%; height: auto; aspect-ratio: 16 / 10; object-fit: cover; border-radius: 12px; background: var(--kn-bg-subtle, #f8f9fb); }
|
||||
.kn-fn-eintrag-text { min-width: 0; }
|
||||
.kn-fn-eintrag-text h3 { font-size: 16px; line-height: 24px; font-weight: 600; overflow-wrap: anywhere; }
|
||||
.kn-fn-beschreibung { margin-top: 4px; color: var(--kn-text-2, #5e6678); overflow-wrap: anywhere; }
|
||||
.kn-fn-enthalten { margin: 8px 0 12px; font-size: 13px; line-height: 20px; color: var(--kn-text-2, #5e6678); }
|
||||
.kn-fn-wahl { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
|
||||
|
||||
/* ------------------------------------------------------------------------------------------ Bestätigungsseite --- */
|
||||
.kn-fn-einschalten { max-width: 560px; }
|
||||
.kn-fn-einschalten h2 { margin-bottom: 16px; font-size: 20px; line-height: 28px; font-weight: 600; overflow-wrap: anywhere; }
|
||||
.kn-fn-fakten { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 4px 16px; margin: 0 0 16px; }
|
||||
.kn-fn-fakten > div { display: contents; }
|
||||
.kn-fn-fakten dt { color: var(--kn-text-2, #5e6678); }
|
||||
.kn-fn-fakten dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
|
||||
.kn-fn-konto, .kn-fn-recht { margin: 0 0 12px; overflow-wrap: anywhere; }
|
||||
.kn-fn-konto { color: var(--kn-text-2, #5e6678); }
|
||||
.kn-fn-recht a { text-decoration: underline; text-decoration-color: var(--kn-line-strong, #d8dde3); text-underline-offset: 3px; }
|
||||
.kn-fn-recht a:hover { text-decoration-color: currentColor; }
|
||||
.kn-fn-einschalten .kn-fn-hinweis { margin: 0 0 12px; }
|
||||
.kn-fn-haken { display: flex; align-items: flex-start; gap: 12px; min-height: 44px; margin: 12px 0 0; cursor: pointer; }
|
||||
.kn-fn-haken input { flex: none; width: 20px; height: 20px; margin: 0; accent-color: var(--kn-accent, #111); }
|
||||
.kn-fn-haken span { min-width: 0; overflow-wrap: anywhere; }
|
||||
.kn-fn-aktionen { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; margin-top: 16px; }
|
||||
|
||||
/* ------------------------------------------------------------------------------------------ Breite Fenster --- */
|
||||
@media (min-width: 881px) {
|
||||
.kn-fn-eintrag { flex-direction: row; align-items: flex-start; gap: 24px; }
|
||||
.kn-fn-bild { flex: 0 0 320px; width: 320px; }
|
||||
.kn-fn-eintrag-text { flex: 1 1 0; }
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
/* Kundenansicht neu: Hülle (Menü, Kopf, Blatt, Leiste, Streifen, Konto). Weiß, Haarlinie, Schwarz; keine Verläufe, kein Glas.
|
||||
Die Grundregeln gelten am Desktop, die Handy-Abfrage überschreibt für das Handy. */
|
||||
|
||||
/* ---------- Blatt (Etage 2) ---------- */
|
||||
|
||||
.kn-blatt { background: var(--kn-bg); }
|
||||
.kn-haupt[inert] { display: none; } /* Desktop: die Unterseite ersetzt die Hauptseite in der Spalte */
|
||||
|
||||
.kn-brot { margin-bottom: 8px; font-size: 13px; line-height: 20px; color: var(--kn-text-2); }
|
||||
.kn-brot a { color: var(--kn-text-2); }
|
||||
.kn-brot-trenner { color: var(--kn-text-3); }
|
||||
.kn-brot [aria-current="page"] { color: var(--kn-text); }
|
||||
|
||||
.kn-zurueck { position: relative; display: flex; align-items: center; gap: 6px; height: 44px; padding: 0 8px 0 6px; font-size: 15px; font-weight: 600; color: var(--kn-text); }
|
||||
.kn-zurueck::before { content: ""; width: 9px; height: 9px; margin-left: 4px; border: solid; border-width: 0 0 2px 2px; transform: rotate(45deg); }
|
||||
.kn-zurueck--voll { flex: 1; }
|
||||
.kn-zurueck .kn-l { position: relative; line-height: 20px; }
|
||||
.kn-vl { flex: none; width: 1px; height: 24px; margin: 0 4px; background: var(--kn-line); }
|
||||
.kn-punkt { position: absolute; top: -2px; left: 100%; width: 9px; height: 9px; margin-left: 2px; border-radius: 50%; background: #b3093c; box-shadow: 0 0 0 2px var(--kn-bg); }
|
||||
|
||||
@media (max-width: 880px), (pointer: coarse) and (max-height: 500px) {
|
||||
.kn { --kn-unten: calc(var(--kn-zeile-h) + env(safe-area-inset-bottom)); }
|
||||
.kn-blatt {
|
||||
position: fixed; inset: calc(var(--kn-banner-h) + var(--kn-kopf-h) + env(safe-area-inset-top)) 0 var(--kn-unten, var(--kn-zeile-h)); z-index: 20;
|
||||
overflow-y: auto; overscroll-behavior: contain; border-radius: 12px 12px 0 0;
|
||||
box-shadow: 0 0 0 1px rgba(26,31,54,.06), 0 -8px 24px rgba(26,31,54,.1);
|
||||
transform: translateY(100%); transition: transform 550ms var(--kn-bremse);
|
||||
}
|
||||
.kn-blatt--auf { transform: none; }
|
||||
.kn-haupt[inert] { display: block; } /* am Handy bleibt die Hauptseite unter dem Blatt stehen */
|
||||
.kn-brot { display: none; }
|
||||
}
|
||||
|
||||
/* ---------- Streifen (Widerrufsfrist) ---------- */
|
||||
|
||||
.kn-streifen { display: flex; align-items: center; min-height: 44px; padding: 0 48px; background: var(--kn-orange-bg); color: var(--kn-orange-fg); font-size: 14px; line-height: 20px; font-weight: 600; }
|
||||
.kn-streifen-link { margin-left: 6px; color: inherit; text-decoration: underline; text-underline-offset: 3px; }
|
||||
.kn-tastatur .kn-streifen { display: none; }
|
||||
|
||||
@media (max-width: 880px), (pointer: coarse) and (max-height: 500px) {
|
||||
.kn-streifen { position: fixed; left: 0; right: 0; bottom: calc(var(--kn-zeile-h) + env(safe-area-inset-bottom)); z-index: 30; padding: 0 max(16px, env(safe-area-inset-right)) 0 max(16px, env(safe-area-inset-left)); }
|
||||
.kn-streifen-link { min-height: 44px; display: inline-flex; align-items: center; }
|
||||
.kn-hat-streifen { --kn-unten: calc(var(--kn-zeile-h) + env(safe-area-inset-bottom) + 44px); }
|
||||
.kn-hat-streifen .kn-haupt { padding-bottom: calc(var(--kn-zeile-h) + env(safe-area-inset-bottom) + 28px + 44px); }
|
||||
}
|
||||
|
||||
/* ---------- Konto (Popover am Desktop, Sheet am Handy) ---------- */
|
||||
|
||||
.kn-avatar { flex: none; display: grid; place-items: center; width: 44px; height: 44px; border-radius: 50%; }
|
||||
.kn-avatar-kreis { display: grid; place-items: center; width: 32px; height: 32px; overflow: hidden; border-radius: 50%; background: var(--kn-grau-bg); color: var(--kn-grau-fg); font-size: 12px; line-height: 16px; font-weight: 600; }
|
||||
.kn-avatar img { display: none; width: 100%; height: 100%; object-fit: cover; }
|
||||
.kn-avatar.has-img img { display: block; }
|
||||
.kn-avatar.has-img .kn-initialen { display: none; }
|
||||
|
||||
.kn-konto-wurzel { position: fixed; inset: 0; z-index: 60; }
|
||||
.kn-konto-wurzel:focus { outline: none; } /* tabindex -1: ein Tipp ins Leere behält die Tasten (Esc, Tab) im Sheet */
|
||||
.kn-konto-scrim { position: absolute; inset: 0; background: transparent; }
|
||||
.kn-konto { position: absolute; top: calc(var(--kn-banner-h) + 56px); right: 24px; width: 280px; max-height: calc(100vh - 72px); padding: 8px 20px; overflow-y: auto; overscroll-behavior: contain; background: var(--kn-bg); border-radius: var(--kn-radius-lg); box-shadow: var(--kn-shadow-pop); animation: kn-aufpoppen .14s ease-out; }
|
||||
.kn-konto-kopf { display: grid; grid-template-columns: 1fr auto; align-items: center; column-gap: 12px; padding: 8px 0 12px; border-bottom: 1px solid var(--kn-line); }
|
||||
.kn-konto-kopf > div { display: contents; } /* Name neben „Schließen“, die Mail darunter über die volle Breite */
|
||||
.kn-konto-name { grid-column: 1; grid-row: 1; display: block; font-size: 17px; line-height: 24px; font-weight: 600; overflow-wrap: anywhere; }
|
||||
.kn-konto-mail { grid-column: 1 / -1; grid-row: 2; display: block; font-size: 13px; line-height: 18px; color: var(--kn-text-2); overflow-wrap: anywhere; }
|
||||
.kn-konto-zu { grid-column: 2; grid-row: 1; min-height: 32px; padding: 0 4px; font-weight: 600; }
|
||||
.kn-konto-zeile { display: flex; align-items: center; justify-content: space-between; gap: 10px; width: 100%; min-height: 46px; padding: 0; border-bottom: 1px solid var(--kn-line); text-align: left; color: var(--kn-text); }
|
||||
.kn-konto-zeile:hover .kn-konto-text { text-decoration: underline; text-decoration-color: var(--kn-line-strong); text-underline-offset: 3px; }
|
||||
.kn-konto-zeile--betont .kn-konto-text { font-weight: 600; }
|
||||
.kn-konto-zeile[data-konto="abmelden"] { border-bottom: 0; color: var(--kn-text-2); }
|
||||
.kn-konto-text { display: block; }
|
||||
.kn-konto-hinweis { display: block; margin-top: 1px; font-size: 13px; line-height: 18px; font-weight: 400; color: var(--kn-text-2); }
|
||||
|
||||
@media (max-width: 880px), (pointer: coarse) and (max-height: 500px) {
|
||||
.kn-konto-scrim { background: var(--kn-overlay); animation: kn-einblenden .3s ease-out; }
|
||||
.kn-konto { top: auto; right: 0; bottom: 0; left: 0; width: auto; max-height: 92vh; padding: 8px max(20px, env(safe-area-inset-right)) calc(22px + env(safe-area-inset-bottom)) max(20px, env(safe-area-inset-left)); border-radius: 12px 12px 0 0; animation: kn-sheet-hoch 500ms var(--kn-bremse); }
|
||||
.kn-konto-zu { min-height: 44px; }
|
||||
.kn-konto-zeile { min-height: 46px; }
|
||||
}
|
||||
@keyframes kn-sheet-hoch { from { transform: translateY(100%); } }
|
||||
|
||||
/* ---------- Sperre der Mitarbeiter-Kundenansicht (E-Fix F2) ----------
|
||||
public/kundenansicht.js hängt bei 409 view_replaced bzw. 401 view_expired ein div.kb-view-overlay (role alertdialog) an den Body. Die alte Seite
|
||||
gestaltet es in kundenbereich.css, die dashboard.html nicht lädt: hier die Regeln für die neue Seite. Deckend weiß über allem (Konto 60, Leiste 40),
|
||||
damit nichts Veraltetes bedienbar bleibt. */
|
||||
.kb-view-overlay { position: fixed; inset: 0; z-index: 1000; display: grid; place-items: center; padding: max(24px, env(safe-area-inset-top)) max(24px, env(safe-area-inset-right)) max(24px, env(safe-area-inset-bottom)) max(24px, env(safe-area-inset-left)); background: var(--kn-bg); }
|
||||
.kb-view-overlay-card { width: 100%; max-width: 420px; text-align: center; }
|
||||
.kb-view-overlay-card p { margin: 0 0 24px; font-size: 17px; line-height: 26px; color: var(--kn-text); }
|
||||
.kb-view-overlay-card button { min-height: 44px; padding: 0 24px; border-radius: var(--kn-radius); background: var(--kn-accent); color: #fff; font-weight: 600; box-shadow: var(--kn-shadow-btn); }
|
||||
.kb-view-overlay-card button:hover { background: var(--kn-accent-hover); }
|
||||
|
||||
/* ---------- Gerüst: Spalte, Inhalt, Menü (Desktop) und Kopf ---------- */
|
||||
|
||||
.kn { --kn-kopf-h: 52px; --kn-banner-h: 0px; } /* --kn-banner-h setzt huelle.js auf die Höhe des Mitarbeiter-Banners (E-Fix M4): Kopf, Menü, Blatt und Konto beginnen darunter */
|
||||
|
||||
.kn-skip { position: absolute; top: -48px; left: 8px; z-index: 100; padding: 8px 12px; border-radius: var(--kn-radius); background: var(--kn-accent); color: #fff; font-weight: 600; }
|
||||
.kn-skip:focus { top: 8px; }
|
||||
.kn-nur-leser { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
|
||||
|
||||
.kn-banner { position: sticky; top: 0; z-index: 50; display: flex; align-items: center; justify-content: center; gap: 12px; min-height: 36px; padding: 6px max(16px, env(safe-area-inset-right)) 6px max(16px, env(safe-area-inset-left)); background: var(--kn-accent); color: #fff; font-size: 13px; line-height: 20px; }
|
||||
.kn-banner > span { min-width: 0; overflow-wrap: anywhere; }
|
||||
.kn-banner button { flex: none; min-height: 28px; padding: 0 4px; color: #fff; font-weight: 600; text-decoration: underline; text-underline-offset: 3px; }
|
||||
|
||||
.kn-menue { position: fixed; inset: var(--kn-banner-h) auto 0 0; z-index: 20; display: flex; flex-direction: column; gap: 2px; width: var(--kn-sidebar-w); padding: 20px 12px; overflow-y: auto; border-right: 1px solid var(--kn-line); background: var(--kn-bg); }
|
||||
.kn-marke { padding: 0 10px 16px; font-size: 15px; line-height: 24px; font-weight: 600; }
|
||||
.kn-nav-item { display: flex; align-items: center; justify-content: space-between; gap: 8px; width: 100%; padding: 6px 10px; border-radius: var(--kn-radius); font-size: 14px; line-height: 20px; font-weight: 500; color: var(--kn-text-2); text-align: left; }
|
||||
.kn-nav-item:hover { background: var(--kn-bg-hover); color: var(--kn-text); }
|
||||
.kn-nav-item[aria-current="page"] { background: var(--kn-accent-soft); color: var(--kn-accent); font-weight: 600; }
|
||||
.kn-nav-zahl { font-size: 12px; line-height: 16px; font-weight: 500; font-variant-numeric: tabular-nums; color: var(--kn-text-2); }
|
||||
.kn-nav-zahl--heiss { color: var(--kn-rot-fg); font-weight: 600; }
|
||||
|
||||
.kn-spalte { position: relative; min-height: 100vh; margin-left: var(--kn-sidebar-w); }
|
||||
.kn-kopf { position: sticky; top: var(--kn-banner-h); z-index: 10; display: flex; align-items: center; justify-content: flex-end; height: var(--kn-kopf-h); margin-bottom: calc(0px - var(--kn-kopf-h)); padding: 0 24px; pointer-events: none; } /* Desktop: nur das Profilbild, bleibt beim Scrollen oben rechts und schiebt den Inhalt nicht */
|
||||
.kn-kopf > * { pointer-events: auto; }
|
||||
.kn-kopf-titel { display: none; }
|
||||
.kn-haupt, .kn-blatt { max-width: 1160px; padding: 36px 48px 80px; }
|
||||
.kn-haupt .kn-titel, .kn-blatt .kn-titel { margin: 0 0 24px; }
|
||||
#kn-fehler { margin: 16px 48px 0; }
|
||||
@media (max-width: 1100px) {
|
||||
.kn-haupt, .kn-blatt { padding: 32px 28px 80px; }
|
||||
#kn-fehler { margin: 16px 28px 0; }
|
||||
.kn-streifen { padding: 0 28px; }
|
||||
}
|
||||
|
||||
@media (max-width: 880px), (pointer: coarse) and (max-height: 500px) {
|
||||
.kn-menue { display: none; }
|
||||
.kn-spalte { margin-left: 0; }
|
||||
.kn-kopf { justify-content: space-between; height: auto; min-height: calc(var(--kn-kopf-h) + env(safe-area-inset-top)); margin-bottom: 0; padding: env(safe-area-inset-top) max(8px, env(safe-area-inset-right)) 0 max(20px, env(safe-area-inset-left)); border-bottom: 1px solid transparent; background: var(--kn-bg); transition: border-color 300ms; pointer-events: auto; }
|
||||
.kn-scrolled .kn-kopf { border-bottom-color: var(--kn-line); }
|
||||
.kn-kopf-titel { display: block; font-size: 15px; line-height: 22px; font-weight: 600; opacity: 0; transform: translateY(5px); transition: opacity 250ms, transform 250ms; }
|
||||
.kn-titled .kn-kopf-titel { opacity: 1; transform: none; }
|
||||
.kn-haupt { max-width: none; padding: 2px max(16px, env(safe-area-inset-right)) calc(var(--kn-zeile-h) + env(safe-area-inset-bottom) + 28px) max(16px, env(safe-area-inset-left)); }
|
||||
.kn-blatt { max-width: none; padding: 0 max(16px, env(safe-area-inset-right)) 24px max(16px, env(safe-area-inset-left)); }
|
||||
.kn-haupt .kn-titel, .kn-blatt .kn-titel { margin: 6px 0 10px; font-size: 28px; line-height: 34px; }
|
||||
.kn-banner button { min-height: 44px; }
|
||||
#kn-fehler { margin: 12px max(16px, env(safe-area-inset-right)) 0 max(16px, env(safe-area-inset-left)); }
|
||||
}
|
||||
|
||||
/* ---------- Leiste (Handy): Fahrstuhl-Zeile mit Balken ---------- */
|
||||
|
||||
.kn-leiste { display: none; }
|
||||
.kn-leiste-reel { height: var(--kn-zeile-h); overflow: hidden; }
|
||||
.kn-rw { display: flex; align-items: center; height: var(--kn-zeile-h); padding: 0 max(4px, env(safe-area-inset-right)) 0 max(4px, env(safe-area-inset-left)); transition: transform 550ms var(--kn-bremse), visibility 0s; }
|
||||
.kn-leiste-reel[data-etage="offen"] .kn-rw { transform: translateY(-56px); }
|
||||
.kn-leiste-reel[data-etage="offen"] #kn-zeile-haupt, .kn-leiste-reel:not([data-etage="offen"]) #kn-zeile-etage { visibility: hidden; transition: transform 550ms var(--kn-bremse), visibility 0s 550ms; }
|
||||
|
||||
.kn-balken { position: relative; flex: 1; min-width: 0; height: 44px; padding-right: 6px; overflow-x: auto; overflow-y: hidden; scrollbar-width: none; } /* 6 px Luft rechts: der Zähler am letzten Wort ragt über das Wort hinaus und würde sonst abgeschnitten */
|
||||
.kn-balken::-webkit-scrollbar { display: none; }
|
||||
.kn-balken-basis { position: relative; z-index: 1; display: flex; width: max-content; min-width: 100%; height: 44px; }
|
||||
.kn-gleiter { position: absolute; z-index: 2; top: 4px; left: 0; height: 36px; overflow: hidden; border-radius: 6px; background: var(--kn-accent); pointer-events: none; transition: transform 550ms var(--kn-feder), width 550ms var(--kn-feder); }
|
||||
.kn-overlay { position: absolute; top: -4px; left: 0; display: flex; height: 44px; transition: transform 550ms var(--kn-feder); }
|
||||
.kn-fb .kn-overlay { display: none; }
|
||||
.kn-fb .kn-gleiter { z-index: 0; }
|
||||
.kn-fb .kn-t[aria-selected="true"] { color: #fff; }
|
||||
|
||||
.kn-t { flex: 1 1 auto; display: flex; align-items: center; justify-content: center; min-height: 44px; padding: 0 4px; font-size: 15px; font-weight: 600; color: var(--kn-text-2); white-space: nowrap; text-align: center; transition: color 250ms; }
|
||||
/* Fokusring: .kn [tabindex]:focus-visible und .kn button:focus-visible (module.css, 0-3-0 und 0-2-1, später geladen) setzen +2px und schlugen die alte Regel
|
||||
(0-2-0); der Ring lag außerhalb und wurde vom Balken (overflow) abgeschnitten. Mit .kn und [role="tab"] (0-4-0) bleibt er innen. Der aktive Reiter liegt
|
||||
auf dem schwarzen Gleiter: dort zeichnet der Gleiter einen hellen Innenring. */
|
||||
.kn .kn-t[role="tab"]:focus-visible { outline-offset: -2px; border-radius: 6px; }
|
||||
.kn .kn-balken:has(.kn-t:focus-visible) .kn-gleiter { outline: 2px solid #fff; outline-offset: -3px; }
|
||||
.kn .kn-fb .kn-t[aria-selected="true"]:focus-visible { outline-color: #fff; }
|
||||
.kn-schrift-klein .kn-t { font-size: 14px; }
|
||||
.kn-overlay .kn-t { color: #fff; }
|
||||
|
||||
.kn-l { position: relative; line-height: 20px; }
|
||||
.kn-zahl { position: absolute; top: -9px; left: 100%; min-width: 16px; height: 16px; margin-left: -6px; padding: 0 4px; border-radius: 8px; background: #b3093c; color: #fff; font-size: 11px; line-height: 16px; font-weight: 700; text-align: center; box-shadow: 0 0 0 2px var(--kn-bg); animation: kn-zahl-ein 400ms cubic-bezier(.3,1.5,.5,1); }
|
||||
.kn-overlay .kn-zahl { box-shadow: 0 0 0 2px var(--kn-accent); }
|
||||
.kn-reiter-t .kn-zahl { top: -7px; }
|
||||
.kn-zahl--weg { transform: scale(0); transition: transform 250ms; animation: none; }
|
||||
@keyframes kn-zahl-ein { from { transform: scale(0); } }
|
||||
|
||||
@media (max-width: 880px), (pointer: coarse) and (max-height: 500px) {
|
||||
.kn-leiste { display: block; position: fixed; right: 0; bottom: 0; left: 0; z-index: 40; border-top: 1px solid var(--kn-line); background: var(--kn-bg); padding-bottom: env(safe-area-inset-bottom); }
|
||||
.kn.kn-tastatur { --kn-unten: 0px; }
|
||||
}
|
||||
.kn-tastatur .kn-leiste { transform: translateY(110%); visibility: hidden; transition: transform 400ms var(--kn-bremse), visibility 0s 400ms; }
|
||||
|
||||
/* ---------- Chat: Vollbild-Ansicht (E-Fix Chat) ----------
|
||||
Solange der Bereich Chat zu sehen ist, füllt er die Höhe: Kopf und Eingabe stehen fest, nur der Verlauf scrollt in sich (Aussehen: ansichten.css).
|
||||
Der Chat hat keine Etage, die Hauptseite ist hier nie inert. Desktop: Spalte in Fensterhöhe, die Eingabe am unteren Rand der zentrierten Spalte
|
||||
(höchstens 760 px). Handy: der Inhalt steht fest zwischen Kopf und Leiste (wie das Blatt), „Chat“ steht klein im Kopf statt als große Überschrift. */
|
||||
.kn-spalte:has(#bereich-chat:not([hidden])) { display: flex; flex-direction: column; height: calc(100vh - var(--kn-banner-h)); height: calc(100dvh - var(--kn-banner-h)); min-height: 0; }
|
||||
.kn-spalte:has(#bereich-chat:not([hidden])) > :not(.kn-haupt) { flex: none; }
|
||||
.kn-haupt:has(#bereich-chat:not([hidden])) { display: flex; flex: 1 1 auto; flex-direction: column; min-height: 0; max-width: none; padding-bottom: 24px; }
|
||||
.kn-haupt > #bereich-chat:not([hidden]) { display: flex; flex: 1 1 auto; flex-direction: column; width: 100%; min-height: 0; max-width: 760px; margin: 0 auto; }
|
||||
#bereich-chat > .kn-titel { flex: none; margin: 0 0 16px; }
|
||||
#chat-body { display: flex; flex: 1 1 auto; flex-direction: column; min-height: 0; }
|
||||
|
||||
@media (max-width: 880px), (pointer: coarse) and (max-height: 500px) {
|
||||
.kn-spalte:has(#bereich-chat:not([hidden])) { display: block; height: auto; }
|
||||
.kn-haupt:has(#bereich-chat:not([hidden])) { position: fixed; inset: calc(var(--kn-banner-h) + var(--kn-kopf-h) + env(safe-area-inset-top)) 0 var(--kn-unten, var(--kn-zeile-h)); padding: 0 max(16px, env(safe-area-inset-right)) 6px max(16px, env(safe-area-inset-left)); background: var(--kn-bg); }
|
||||
#bereich-chat > .kn-titel { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; } /* bleibt für Screenreader die Überschrift der Seite */
|
||||
.kn:has(#bereich-chat:not([hidden])) .kn-kopf { height: calc(var(--kn-kopf-h) + env(safe-area-inset-top)); border-bottom-color: var(--kn-line); } /* feste Höhe wie der Rand oben des Inhalts: im Querformat wäre der Kopf mit Haarlinie 1 px höher */
|
||||
.kn:has(#bereich-chat:not([hidden])) .kn-kopf-titel { opacity: 1; transform: none; }
|
||||
}
|
||||
|
||||
/* ---------- Querformat (Handy gedreht): niedrigere Zeile, keine große Überschrift ---------- */
|
||||
|
||||
@media (max-height: 500px) and (max-width: 880px), (max-height: 500px) and (pointer: coarse) {
|
||||
.kn { --kn-zeile-h: 44px; --kn-kopf-h: 44px; }
|
||||
.kn-leiste-reel[data-etage="offen"] .kn-rw { transform: translateY(-44px); }
|
||||
.kn-haupt .kn-titel, .kn-blatt .kn-titel { display: none; }
|
||||
.kn-kopf-titel { opacity: 1; transform: none; }
|
||||
}
|
||||
|
||||
/* ---------- Bewegung reduziert: keine Federn, höchstens kurzes Überblenden ---------- */
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.kn-gleiter, .kn-overlay, .kn-rw, .kn-t { transition: none; }
|
||||
/* E-Fix M7: die Regel der beiden Fahrstuhl-Zeilen hat Spezifität 1-2-0 und schlug das .kn-rw oben; sie fuhr trotz „Bewegung reduziert“ noch 550 ms.
|
||||
Gleiche Selektoren, später im Blatt: die Zeilen tauschen sofort (kein Verzug der Sichtbarkeit), nichts gleitet durch die andere Zeile. */
|
||||
.kn-leiste-reel[data-etage="offen"] #kn-zeile-haupt, .kn-leiste-reel:not([data-etage="offen"]) #kn-zeile-etage { transition: none; }
|
||||
.kn-blatt { transform: none; opacity: 0; transition: opacity 150ms; }
|
||||
.kn-blatt--auf { opacity: 1; }
|
||||
.kn-konto-scrim, .kn-konto { animation: kn-einblenden 150ms ease-out; }
|
||||
.kn-zahl { animation: none; }
|
||||
.kn-zahl--weg { transition: none; }
|
||||
.kn-kopf, .kn-kopf-titel, .kn-tastatur .kn-leiste { transition-duration: 150ms; }
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
/* Kundenansicht neu: Start und Zusammenbau der Hülle.
|
||||
Start über GET /api/auth/me (Anmeldung, Mitarbeiter, Kundenansicht), dann Konto, Streifen, Navigation, Etage 2, Zähler und Router.
|
||||
Der Router ruft zeige(route, { nav }) bei jedem Wechsel; die Hülle lädt die Ansicht über einen einspritzbaren Lader (Standard: import())
|
||||
und ruft zeigeX(wurzel, { route, me, nav }) bzw. die Funktionsmodule wie bisher (init*(wurzel)). Kein DOM-Zugriff beim Import. */
|
||||
|
||||
import { h, leeren, text } from './dom.js'
|
||||
import { api as apiStandard } from './api.js'
|
||||
import * as routerStandard from './router.js'
|
||||
import { BEREICHE, FUNKTIONS_ETAGEN, hashFuerAnsicht, hashFuerRoute, elternVon, titelFuer, zeigtWiderrufsstreifen } from './routen.js'
|
||||
import { HANDY_ABFRAGE, baueNavigation, beobachteTastatur, leisteSichtbar } from './leiste.js'
|
||||
import { baueEtage } from './etage.js'
|
||||
import { erzeugeZaehler } from './zaehler.js'
|
||||
import { baueStreifen, ladeFristen, ladeFristenNeu } from './fristen-streifen.js'
|
||||
import { baueAvatar, baueKonto, installAngebot } from './konto.js'
|
||||
|
||||
export const GERUEST_IDS = ['kn-banner', 'kn-ansage', 'kn-fehler', 'kn-menue', 'kn-kopf', 'kn-kopf-titel', 'kn-kopf-konto', 'kn-inhalt', 'bereich-website', 'bereich-funktionen', 'bereich-chat', 'bereich-vertraege', 'website-body', 'funktionen-body', 'chat-body', 'vertraege-body', 'kn-blatt', 'kn-brot', 'kn-etage-titel', 'kn-reiter-slot', 'news-body', 'termine-body', 'webshop-body', 'buchungen-body', 'zeiten-body', 'analyse-body', 'etage-body', 'kn-streifen', 'kn-leiste', 'kn-zeile-haupt', 'kn-zeile-etage', 'kn-konto-wurzel']
|
||||
|
||||
// Bereich → [Modul, Funktion, Wurzel]; die Ansicht heißt ansicht-<name>.js und exportiert zeige<Name>
|
||||
export const ANSICHTEN = {
|
||||
website: ['/neu/ansicht-website.js', 'zeigeWebsite', 'website-body'],
|
||||
funktionen: ['/neu/ansicht-funktionen.js', 'zeigeFunktionen', 'funktionen-body'],
|
||||
chat: ['/neu/ansicht-chat.js', 'zeigeChat', 'chat-body'],
|
||||
vertraege: ['/neu/ansicht-vertraege.js', 'zeigeVertraege', 'vertraege-body'],
|
||||
}
|
||||
// Etagen mit eigener Ansicht; alle zeichnen in #etage-body
|
||||
export const ETAGE_ANSICHTEN = {
|
||||
projekt: ['/neu/ansicht-website.js', 'zeigeWebsite', 'etage-body'],
|
||||
passwort: ['/neu/ansicht-passwort.js', 'zeigePasswort', 'etage-body'],
|
||||
gutschrift: ['/neu/ansicht-gutschrift.js', 'zeigeGutschrift', 'etage-body'],
|
||||
wechsel: ['/neu/ansicht-wechsel.js', 'zeigeWechsel', 'etage-body'],
|
||||
bestellen: ['/neu/ansicht-bestellen.js', 'zeigeBestellen', 'etage-body'],
|
||||
}
|
||||
// Gemeinsame Funktionsmodule, wörtlich aus app.js:1938-1945 (Wurzel: der Behälter mit der alten ID)
|
||||
export const FUNKTIONS_MODULE = {
|
||||
news: ['/funktionen.js', 'initNews', 'news-body'],
|
||||
termine: ['/termine.js', 'initTermine', 'termine-body'],
|
||||
webshop: ['/shop.js', 'initShop', 'webshop-body'],
|
||||
buchungen: ['/buchungen.js', 'initBuchungen', 'buchungen-body'],
|
||||
zeiten: ['/zeiten.js', 'initZeiten', 'zeiten-body'],
|
||||
analyse: ['/analyse.js', 'initAnalyse', 'analyse-body'],
|
||||
}
|
||||
// Diese Module bekommen init*(wurzel, { einschalten }); die Analyse nutzt POST /api/funktionen/analyse/activate und bleibt init*(wurzel)
|
||||
export const MIT_EINSCHALTEN = ['news', 'termine', 'webshop', 'buchungen', 'zeiten']
|
||||
|
||||
/**
|
||||
* einschalten(platz, { key, projectId, onFertig, viewAs? }): lädt die Einschalt-Ansicht erst beim Aufruf; ein Ladefehler geht an das Modul zurück.
|
||||
* In der Mitarbeiter-Kundenansicht (viewAs der Sitzung) gilt immer viewAs true: Der Knopf ist gesperrt, auch wenn das Modul es nicht nennt.
|
||||
*/
|
||||
export const einschaltenOption = (lade, { viewAs = false } = {}) => async (platz, optionen) => {
|
||||
const modul = await lade('/neu/funktion-einschalten.js')
|
||||
return modul.zeigeEinschalten(platz, viewAs ? { ...optionen, viewAs: true } : optionen)
|
||||
}
|
||||
|
||||
/** Antwort von /api/auth/me → weiterleiten (Anmeldung, Mitarbeiter, abgelaufen, Passwort geändert) oder zeigen (Kunde, Kundenansicht). */
|
||||
export function leiteAb(me, { hash = '' } = {}) {
|
||||
if (me?.authenticated !== true || !me.customer) {
|
||||
// Alte Mitarbeiter-Sitzung: Mitarbeiter arbeiten im Ticketsystem
|
||||
if (me?.authenticated === false && me.reason === 'staff' && me.redirect) return { art: 'weiter', ziel: me.redirect }
|
||||
const suche = new URLSearchParams()
|
||||
if (hash && hash !== '#') suche.set('next', `/dashboard.html${hash}`) // nach dem Login zurück zur aufgerufenen Ansicht
|
||||
if (me?.authenticated === false && (me.reason === 'password_changed' || me.reason === 'view_expired') && me.message) suche.set('error', me.message)
|
||||
const anhang = suche.toString()
|
||||
return { art: 'weiter', ziel: `/login.html${anhang ? `?${anhang}` : ''}` }
|
||||
}
|
||||
const kunde = me.customer
|
||||
const viewAs = me.viewAs === true && Boolean(me.viewId)
|
||||
return { art: 'zeigen', kunde, viewAs, viewId: viewAs ? me.viewId : '', admin: me.admin ?? null, anzeigename: kunde.companyName || kunde.name || kunde.email || '' }
|
||||
}
|
||||
|
||||
/** Wohin scrollt der Wechsel? push beginnt oben, same scrollt weich nach oben, back und tab stellen den gemerkten Stand wieder her. */
|
||||
export function scrollZiel(nav, gemerkt) {
|
||||
if (nav === 'push') return { top: 0, weich: false }
|
||||
if (nav === 'same') return { top: 0, weich: true }
|
||||
return { top: Number.isFinite(gemerkt) ? gemerkt : 0, weich: false }
|
||||
}
|
||||
|
||||
export async function starteHuelle({ win = window, doc = document, api = apiStandard, lade = (pfad) => import(pfad), router = routerStandard, hash = win.location.hash } = {}) {
|
||||
const el = (id) => doc.getElementById(id)
|
||||
const zeigeFehler = (nachricht) => { const f = el('kn-fehler'); text(f, nachricht); f.hidden = false }
|
||||
|
||||
// 1. Wer ist da? Fehler oder fremder Zustand ergeben eine Weiterleitung oder eine Meldung, nie eine leere Seite.
|
||||
let me
|
||||
try {
|
||||
me = await api('/api/auth/me')
|
||||
} catch (fehler) {
|
||||
if (fehler?.status === 401 || /Nicht angemeldet/.test(fehler?.message ?? '')) win.location.href = leiteAb({ authenticated: false }, { hash }).ziel
|
||||
else zeigeFehler(fehler?.message || 'Der Kundenbereich konnte nicht geladen werden.')
|
||||
return
|
||||
}
|
||||
const start = leiteAb(me, { hash })
|
||||
if (start.art === 'weiter') { win.location.href = start.ziel; return }
|
||||
const { kunde, viewAs, viewId, admin, anzeigename } = start
|
||||
// Kundenansicht: ab jetzt trägt jede /api-Anfrage die Kennung dieser Ansicht (public/kundenansicht.js)
|
||||
if (viewAs) win.KbView?.setId(viewId)
|
||||
if (viewAs) baueBanner()
|
||||
|
||||
// 2. Bausteine
|
||||
const handyMq = win.matchMedia(HANDY_ABFRAGE)
|
||||
const istHandy = () => handyMq.matches
|
||||
const reduziert = () => Boolean(win.matchMedia('(prefers-reduced-motion: reduce)').matches)
|
||||
const standalone = () => win.matchMedia('(display-mode: standalone)').matches || win.navigator?.standalone === true
|
||||
const istIos = () => /iphone|ipad|ipod/i.test(win.navigator?.userAgent ?? '') || (win.navigator?.platform === 'MacIntel' && win.navigator?.maxTouchPoints > 1)
|
||||
let fristen = null
|
||||
let installPrompt = null
|
||||
const installStand = () => installAngebot({ handy: istHandy(), standalone: standalone(), prompt: Boolean(installPrompt), ios: istIos() })
|
||||
|
||||
const avatar = baueAvatar({ name: anzeigename, kundenId: kunde.id })
|
||||
el('kn-kopf-konto').append(avatar)
|
||||
const konto = baueKonto({
|
||||
me, fristen, install: installStand(),
|
||||
onAbmelden: async () => { await api('/api/auth/logout', { method: 'POST' }).catch(() => {}); win.location.href = '/login.html' },
|
||||
onInstall: async () => {
|
||||
const prompt = installPrompt
|
||||
if (!prompt) return
|
||||
installPrompt = null
|
||||
await prompt.prompt().catch(() => {})
|
||||
aktualisiereKonto()
|
||||
},
|
||||
overlay: { oeffne: (o) => router.oeffneOverlay(o), beende: () => router.beendeOverlay() },
|
||||
navigiere: (ziel) => router.geheZu(ziel),
|
||||
})
|
||||
el('kn-konto-wurzel').append(konto.wurzel)
|
||||
avatar.addEventListener('click', () => (konto.istOffen() ? konto.schliesse() : konto.oeffne(avatar)))
|
||||
const aktualisiereKonto = () => konto.aktualisiere({ fristen, install: installStand() })
|
||||
|
||||
const streifen = baueStreifen(el('kn-streifen'), { koerper: doc.body })
|
||||
streifen.setze(null)
|
||||
const navigation = baueNavigation({ haupt: el('kn-zeile-haupt'), menue: el('kn-menue'), onWahl: (bereich) => router.geheZu(hashFuerRoute({ bereich })) })
|
||||
const etage = baueEtage({ blatt: el('kn-blatt'), haupt: el('kn-inhalt'), zeile: el('kn-zeile-etage'), titel: el('kn-etage-titel'), brot: el('kn-brot'), reiterSlot: el('kn-reiter-slot'), istHandy, onZurueck: () => router.zurueck() })
|
||||
let aktuell = null // die Route, die gerade gezeigt wird
|
||||
let herkunft = '' // Hash der Route davor (Rückweg der Passwort-Seite)
|
||||
const zaehler = erzeugeZaehler({
|
||||
api,
|
||||
onChange: (stand) => { navigation.setzeZaehler(stand); etage.setzeChatPunkt(stand.chat > 0) },
|
||||
sichtbar: () => doc.visibilityState !== 'hidden',
|
||||
imChat: () => aktuell?.bereich === 'chat',
|
||||
})
|
||||
|
||||
function baueBanner() {
|
||||
const banner = el('kn-banner')
|
||||
const beenden = async () => {
|
||||
const antwort = await api('/api/auth/kundenansicht/ende', { method: 'POST' }).catch(() => ({}))
|
||||
win.location.href = antwort?.redirect || 'https://ticket.webklar.com/customers'
|
||||
}
|
||||
leeren(banner).append(h('span', null, 'Kundenansicht: ', h('strong', null, kunde.name || 'Kunde'), ` · geöffnet von ${admin?.name || 'Mitarbeiter'}`), h('button', { type: 'button', on: { click: beenden } }, 'Beenden'))
|
||||
banner.hidden = false
|
||||
// Höhe des Banners für Kopf, Menü, Blatt und Konto (E-Fix M4: --kn-banner-h). Der Banner bleibt oben stehen (sticky) und wird am Handy zweizeilig.
|
||||
const setzeHoehe = () => doc.body.style?.setProperty?.('--kn-banner-h', `${banner.offsetHeight || 0}px`)
|
||||
setzeHoehe()
|
||||
if (typeof win.ResizeObserver === 'function') new win.ResizeObserver(setzeHoehe).observe(banner)
|
||||
else win.addEventListener('resize', setzeHoehe)
|
||||
}
|
||||
|
||||
// 3. Scroll: Stand je Route merken, Kopf (Rand, kleiner Titel) nachführen
|
||||
const merker = new Map()
|
||||
const schluessel = (route) => hashFuerRoute({ ...route, query: '' })
|
||||
const blattScrollt = (route) => Boolean(route?.etage) && istHandy() // am Handy scrollt das Blatt, sonst das Fenster
|
||||
const leseScroll = (route) => (blattScrollt(route) ? el('kn-blatt').scrollTop : (win.scrollY ?? 0))
|
||||
function setzeScroll(route, ziel) {
|
||||
const verhalten = ziel.weich && !reduziert() ? 'smooth' : 'auto'
|
||||
const flaeche = blattScrollt(route) ? el('kn-blatt') : win
|
||||
if (typeof flaeche.scrollTo === 'function') flaeche.scrollTo({ top: ziel.top, behavior: verhalten })
|
||||
else if (flaeche !== win) flaeche.scrollTop = ziel.top
|
||||
}
|
||||
function aktualisiereKopf() {
|
||||
if (!aktuell) return
|
||||
doc.body.classList.toggle('kn-scrolled', leseScroll(aktuell) > 4)
|
||||
const titel = aktuell.etage ? el('kn-etage-titel') : el(`bereich-${aktuell.bereich}`)?.querySelector('h1')
|
||||
const kopf = el('kn-kopf')
|
||||
const verdeckt = titel && kopf && typeof titel.getBoundingClientRect === 'function' ? titel.getBoundingClientRect().bottom < kopf.getBoundingClientRect().bottom + 4 : false
|
||||
doc.body.classList.toggle('kn-titled', verdeckt)
|
||||
}
|
||||
const frame = (fn) => (win.requestAnimationFrame ? win.requestAnimationFrame(fn) : fn())
|
||||
let scrollGeplant = false
|
||||
const beiScroll = () => {
|
||||
if (scrollGeplant) return
|
||||
scrollGeplant = true
|
||||
frame(() => { scrollGeplant = false; if (aktuell) merker.set(schluessel(aktuell), leseScroll(aktuell)); aktualisiereKopf() })
|
||||
}
|
||||
|
||||
// 4. Zeigen: Bereich oder Etage, Navigation, Titel; dann die Ansicht laden (nicht abwarten)
|
||||
let erstesZeigen = true
|
||||
function zeige(route, { nav }) {
|
||||
if (aktuell) merker.set(schluessel(aktuell), leseScroll(aktuell))
|
||||
if (aktuell && hashFuerRoute(aktuell) !== hashFuerRoute(route)) herkunft = hashFuerRoute(aktuell)
|
||||
aktuell = route
|
||||
const titel = titelFuer(route)
|
||||
for (const bereich of BEREICHE) el(`bereich-${bereich}`).hidden = bereich !== route.bereich
|
||||
if (route.etage) {
|
||||
const eltern = elternVon(route, herkunft)
|
||||
etage.oeffne({ titel, eltern: { label: titelFuer(eltern), hash: hashFuerRoute(eltern) }, seite: FUNKTIONS_ETAGEN.includes(route.etage) ? route.etage : 'seite' })
|
||||
} else etage.schliesse()
|
||||
navigation.setzeAktiv(route.bereich)
|
||||
navigation.setzeEtage(Boolean(route.etage))
|
||||
streifen.erlaube(zeigtWiderrufsstreifen(route)) // Bestellseiten: Kerninfo und Knopf auf einem Bildschirm, ohne den festen Streifen
|
||||
text(el('kn-kopf-titel'), titel)
|
||||
text(el('kn-ansage'), titel)
|
||||
if (route.bereich === 'chat') zaehler.setzeChat(0)
|
||||
if (route.bereich === 'vertraege' && !erstesZeigen) zaehler.aktualisiere('vertraege') // beim ersten Zeigen lädt zaehler.start() ohnehin alles
|
||||
erstesZeigen = false
|
||||
return ladeAnsicht(route, nav)
|
||||
}
|
||||
|
||||
async function ladeAnsicht(route, nav) {
|
||||
const modul = route.etage ? FUNKTIONS_MODULE[route.etage] : null
|
||||
const eintrag = modul ?? (route.etage ? ETAGE_ANSICHTEN[route.etage] : ANSICHTEN[route.bereich])
|
||||
if (!eintrag) return
|
||||
const [pfad, name, wurzelId] = eintrag
|
||||
const wurzel = el(wurzelId)
|
||||
// #etage-body dient mehreren Etagen: beim Wechsel der Etage den Rest der vorigen wegräumen
|
||||
if (wurzelId === 'etage-body' && wurzel.dataset.knEtage !== route.etage) { leeren(wurzel); wurzel.dataset.knEtage = route.etage }
|
||||
if (!wurzel.firstChild) wurzel.append(h('p', { class: 'kn-leer' }, 'Wird geladen …'))
|
||||
const gemerkt = merker.get(schluessel(route))
|
||||
const ziel = scrollZiel(nav, gemerkt)
|
||||
setzeScroll(route, ziel)
|
||||
aktualisiereKopf()
|
||||
try {
|
||||
const geladen = await lade(pfad)
|
||||
if (modul) {
|
||||
const projekt = new URLSearchParams(route.query).get('projekt')
|
||||
if (projekt) wurzel.dataset.wunschProjekt = projekt
|
||||
if (MIT_EINSCHALTEN.includes(route.etage)) await geladen[name](wurzel, { einschalten: einschaltenOption(lade, { viewAs }) })
|
||||
else await geladen[name](wurzel)
|
||||
router.stempeln() // die Module löschen mit replaceState(null, …) den Verlaufsstempel
|
||||
} else {
|
||||
await geladen[name](wurzel, { route, me, nav })
|
||||
}
|
||||
} catch {
|
||||
leeren(wurzel).append(h('p', { class: 'kn-fehler' }, 'Konnte nicht geladen werden.'))
|
||||
}
|
||||
if (ziel.top > 0 && aktuell === route) setzeScroll(route, ziel) // wiederherstellen, sobald der Inhalt da ist
|
||||
aktualisiereKopf()
|
||||
}
|
||||
|
||||
// 5. Titel aus dem Inhalt der Etage (kn:titel) auch im Kopf und in der Ansage
|
||||
el('kn-blatt').addEventListener('kn:titel', (ev) => {
|
||||
if (!aktuell?.etage || !ev.detail?.text) return
|
||||
text(el('kn-kopf-titel'), String(ev.detail.text))
|
||||
text(el('kn-ansage'), String(ev.detail.text))
|
||||
})
|
||||
|
||||
// 6. Hörer
|
||||
try { win.history.scrollRestoration = 'manual' } catch { /* ohne Verlaufssteuerung bleibt der Browser-Standard */ }
|
||||
doc.body.addEventListener('click', (ev) => { // die data-view-Brücke der gemeinsamen Module
|
||||
const ziel = ev.target?.closest?.('[data-view]')
|
||||
if (!ziel || ziel.disabled) return
|
||||
let ansicht = hashFuerAnsicht(ziel.getAttribute('data-view'))
|
||||
if (ansicht === null) return
|
||||
ev.preventDefault()
|
||||
const projekt = ziel.getAttribute('data-project')
|
||||
if (projekt) ansicht += `${ansicht.includes('?') ? '&' : '?'}projekt=${encodeURIComponent(projekt)}`
|
||||
router.geheZu(ansicht)
|
||||
})
|
||||
doc.querySelector?.('.kn-skip')?.addEventListener('click', (ev) => { ev.preventDefault(); el('kn-inhalt').focus() }) // kein Fragment-Link: der Router läse #kn-inhalt als Route
|
||||
// Widerrufsstreifen und Pille „Frist läuft“ (E-Fix M1): nach einem Kauf, Wechsel oder Speicher-Plus in der Sitzung und beim Zurückkehren in den Tab neu laden,
|
||||
// damit eine neue Frist erscheint und ein abgelaufener Streifen nicht stehen bleibt. Ein Fehler lässt den bisherigen Stand stehen; die jüngste Antwort gilt.
|
||||
let fristenLauf = 0
|
||||
async function frischeFristen() {
|
||||
const nr = ++fristenLauf
|
||||
const daten = await ladeFristenNeu(api)
|
||||
if (nr !== fristenLauf || daten === undefined) return
|
||||
fristen = daten
|
||||
streifen.setze(daten)
|
||||
aktualisiereKonto()
|
||||
}
|
||||
doc.addEventListener('kn:zaehler-neu', (ev) => {
|
||||
const was = ev.detail?.was ?? 'alle'
|
||||
zaehler.aktualisiere(was)
|
||||
if (was === 'vertraege' || was === 'alle') frischeFristen()
|
||||
})
|
||||
doc.addEventListener('visibilitychange', () => { if (doc.visibilityState === 'visible') { zaehler.aktualisiere('alle'); frischeFristen() } })
|
||||
win.addEventListener('scroll', beiScroll, { passive: true })
|
||||
el('kn-blatt').addEventListener('scroll', beiScroll, { passive: true })
|
||||
win.addEventListener('beforeinstallprompt', (ev) => { ev.preventDefault(); installPrompt = ev; aktualisiereKonto() })
|
||||
win.addEventListener('appinstalled', () => { installPrompt = null; aktualisiereKonto() })
|
||||
const beiMedien = () => { etage.neuZeichnen(); aktualisiereKonto(); aktualisiereKopf() }
|
||||
if (handyMq.addEventListener) handyMq.addEventListener('change', beiMedien)
|
||||
else handyMq.addListener?.(beiMedien)
|
||||
beobachteTastatur({ win, doc, onChange: (z) => doc.body.classList.toggle('kn-tastatur', !leisteSichtbar(z)) })
|
||||
|
||||
// 7. Fristen und Zähler laden parallel und brechen den Start nie ab
|
||||
ladeFristen(api).then((daten) => { fristen = daten; streifen.setze(daten); aktualisiereKonto() })
|
||||
zaehler.start()
|
||||
|
||||
// 8. Router starten (ruft zeige einmal mit der Startroute), Service Worker für Installation und Offline-Seite
|
||||
router.starteRouter({ zeige })
|
||||
if (win.navigator?.serviceWorker && win.isSecureContext) {
|
||||
const registriere = () => win.navigator.serviceWorker.register('/sw.js').catch((fehler) => console.warn('Service Worker:', fehler.message))
|
||||
if (doc.readyState === 'complete') registriere()
|
||||
else win.addEventListener('load', registriere)
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof document !== 'undefined' && document.body?.classList.contains('kn') && document.getElementById('kn-leiste')) {
|
||||
starteHuelle().catch((fehler) => {
|
||||
console.error(fehler)
|
||||
const f = document.getElementById('kn-fehler')
|
||||
if (f) { f.textContent = 'Der Kundenbereich konnte nicht gestartet werden. Bitte lade die Seite neu.'; f.hidden = false }
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/* Kundenansicht neu (P2): Hinweis "WEBklar als App" (Logik aus app.js renderInstallOffers). Chrome/Android per Installationsdialog,
|
||||
iPhone über Teilen und Zum Home-Bildschirm. Kein DOM-Zugriff beim Import. */
|
||||
|
||||
import { h } from './dom.js'
|
||||
|
||||
const SCHLUESSEL = 'kb-install-dismissed'
|
||||
|
||||
/** Zeigen nur auf dem Handy, nicht installiert, mit Installationsdialog oder iOS und nicht ausgeblendet. */
|
||||
export function installAngebot({ appLayout, standalone, canPrompt, ios, dismissed }) {
|
||||
const zeigen = Boolean(appLayout) && !standalone && Boolean(canPrompt || ios) && !dismissed
|
||||
return { zeigen, art: zeigen ? (canPrompt ? 'prompt' : 'ios') : null }
|
||||
}
|
||||
|
||||
export function baueInstallHinweis({ art, onInstall, onDismiss }) {
|
||||
const text = art === 'prompt'
|
||||
? 'Mit einem Tipp auf deinem Homescreen – ohne App-Store.'
|
||||
: 'Tippe auf „Teilen“ und dann auf „Zum Home-Bildschirm“.'
|
||||
return h('div', { class: 'kn-install', role: 'region', 'aria-label': 'WEBklar als App' },
|
||||
h('div', { class: 'kn-install-text' }, h('strong', null, 'WEBklar als App'), h('span', null, text)),
|
||||
art === 'prompt' ? h('button', { type: 'button', class: 'kn-knopf', on: { click: () => onInstall?.() } }, 'Installieren') : null,
|
||||
h('button', { type: 'button', class: 'kn-textknopf', 'aria-label': 'Hinweis ausblenden', on: { click: () => onDismiss?.() } }, '×'))
|
||||
}
|
||||
|
||||
const quellen = new WeakMap()
|
||||
|
||||
/** Je Fenster genau eine Quelle: merkt den Installationsdialog (beforeinstallprompt), räumt bei appinstalled. */
|
||||
export function starteInstallQuelle(win = window) {
|
||||
if (quellen.has(win)) return quellen.get(win)
|
||||
const nav = win.navigator ?? {}
|
||||
const ios = /iphone|ipad|ipod/i.test(nav.userAgent ?? '') || (nav.platform === 'MacIntel' && nav.maxTouchPoints > 1)
|
||||
let angebot = null
|
||||
const hoerer = []
|
||||
const melde = () => { for (const fn of [...hoerer]) fn() }
|
||||
const quelle = {
|
||||
ios,
|
||||
standalone: () => Boolean(typeof win.matchMedia === 'function' && win.matchMedia('(display-mode: standalone)')?.matches) || nav.standalone === true,
|
||||
kannPrompt: () => angebot !== null,
|
||||
async installiere() {
|
||||
const p = angebot
|
||||
if (!p) return
|
||||
angebot = null
|
||||
try { await p.prompt() } catch { /* abgebrochen */ }
|
||||
melde()
|
||||
},
|
||||
beiAenderung(fn) { hoerer.push(fn) },
|
||||
}
|
||||
win.addEventListener('beforeinstallprompt', (ev) => { ev.preventDefault?.(); angebot = ev; melde() })
|
||||
win.addEventListener('appinstalled', () => { angebot = null; melde() })
|
||||
quellen.set(win, quelle)
|
||||
return quelle
|
||||
}
|
||||
|
||||
export function istAusgeblendet() {
|
||||
try { return localStorage.getItem(SCHLUESSEL) === '1' } catch { return false }
|
||||
}
|
||||
|
||||
export function merkeAusgeblendet() {
|
||||
try { localStorage.setItem(SCHLUESSEL, '1') } catch { /* ohne Speicher wird der Hinweis wieder gezeigt */ }
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/* Kundenansicht neu: Konto. Das Profilbild (Avatar) öffnet am Handy ein Sheet von unten, am Desktop ein Popover rechts oben.
|
||||
Reihenfolge der Zeilen (V3): Passwort ändern, Datenschutz, Impressum, App installieren (nur wenn sinnvoll), Verträge kündigen,
|
||||
Vertrag widerrufen (immer da, in der Frist mit der Pille „Frist läuft“), Abmelden. Namen und Mails kommen nur als Text in die Seite.
|
||||
Das Sheet ist ein Menü: Esc, ein Tipp aufs Dunkle und „Schließen“ schließen es; Zurück schließt es über das Overlay des Routers. */
|
||||
|
||||
import { h, leeren } from './dom.js'
|
||||
import { oeffneOverlay, beendeOverlay, geheZu } from './router.js'
|
||||
|
||||
/** Initialen aus den ersten zwei Wörtern (nur Wörter, die mit Buchstabe oder Ziffer beginnen), sonst die ersten zwei Buchstaben. */
|
||||
export function initialenVon(name) {
|
||||
const woerter = String(name || '').split(/\s+/).filter((w) => /^[\p{L}\p{N}]/u.test(w))
|
||||
const buchstaben = woerter.length > 1 ? woerter[0][0] + woerter[1][0] : (woerter[0] || '').slice(0, 2)
|
||||
return buchstaben.toUpperCase()
|
||||
}
|
||||
|
||||
/** Installationsangebot: nur am Handy und nicht in der installierten App; Chrome per Dialog ('prompt'), iPhone über Teilen ('ios'). */
|
||||
export function installAngebot({ handy, standalone, prompt, ios }) {
|
||||
if (!handy || standalone) return null
|
||||
if (prompt) return 'prompt'
|
||||
return ios ? 'ios' : null
|
||||
}
|
||||
|
||||
const HINWEIS_IOS = 'Tippe auf Teilen und dann auf Zum Home-Bildschirm.'
|
||||
|
||||
/** Zeilen des Kontos in der Reihenfolge von V3. */
|
||||
export function kontoEintraege({ fristen, install } = {}) {
|
||||
const frist = fristen?.anzeigen === true
|
||||
return [
|
||||
{ id: 'passwort', text: 'Passwort ändern', href: '#passwort' },
|
||||
{ id: 'datenschutz', text: 'Datenschutz', href: '/datenschutz.html', neuerTab: true },
|
||||
{ id: 'impressum', text: 'Impressum', href: 'https://webklar.com/impressum', neuerTab: true },
|
||||
...(install ? [{ id: 'installieren', text: 'App installieren', ...(install === 'ios' ? { hinweis: HINWEIS_IOS } : {}) }] : []),
|
||||
{ id: 'kuendigen', text: 'Verträge kündigen', href: '/kuendigen.html', betont: true },
|
||||
{ id: 'widerrufen', text: 'Vertrag widerrufen', href: '/widerrufen.html', betont: true, ...(frist ? { pille: 'Frist läuft' } : {}) },
|
||||
{ id: 'abmelden', text: 'Abmelden' },
|
||||
]
|
||||
}
|
||||
|
||||
/** Profilbild-Knopf: Initialen sofort, das Bild von /api/avatar.svg erscheint erst nach dem Laden (sonst bleiben die Initialen). */
|
||||
export function baueAvatar({ name, kundenId }) {
|
||||
const knopf = h('button', { class: 'kn-avatar', type: 'button', 'aria-label': 'Mein Konto', 'aria-haspopup': 'dialog', 'aria-expanded': 'false' },
|
||||
h('span', { class: 'kn-avatar-kreis' }, h('span', { class: 'kn-initialen' }, initialenVon(name))))
|
||||
if (kundenId) {
|
||||
const bild = h('img', { alt: '', decoding: 'async', src: `/api/avatar.svg?c=${encodeURIComponent(kundenId)}`, on: { load: () => knopf.classList.add('has-img'), error: () => bild.remove() } })
|
||||
knopf.firstElementChild.append(bild)
|
||||
}
|
||||
return knopf
|
||||
}
|
||||
|
||||
const mitModifikator = (ev) => ev.defaultPrevented || ev.button > 0 || ev.metaKey || ev.ctrlKey || ev.shiftKey || ev.altKey
|
||||
|
||||
export function baueKonto({ me, fristen, install, onAbmelden, onInstall, overlay = { oeffne: oeffneOverlay, beende: beendeOverlay }, navigiere = geheZu }) {
|
||||
const kunde = me?.customer ?? {}
|
||||
const stand = { fristen, install, offen: false, ausloeser: null }
|
||||
|
||||
const liste = h('div', { class: 'kn-konto-liste' })
|
||||
const zu = h('button', { class: 'kn-konto-zu', type: 'button', on: { click: () => schliesse() } }, 'Schließen')
|
||||
const panel = h('div', { class: 'kn-konto' },
|
||||
h('div', { class: 'kn-konto-kopf' }, h('div', null, h('b', { class: 'kn-konto-name' }, kunde.companyName || kunde.name || kunde.email || ''), h('span', { class: 'kn-konto-mail' }, kunde.email || '')), zu),
|
||||
liste)
|
||||
const wurzel = h('div', { class: 'kn-konto-wurzel', role: 'dialog', 'aria-modal': 'true', 'aria-label': 'Konto', tabindex: '-1', hidden: true, on: { keydown: beiTaste } },
|
||||
h('div', { class: 'kn-konto-scrim', on: { click: () => schliesse() } }), panel)
|
||||
|
||||
function zeile(e) {
|
||||
const inhalt = [h('span', { class: 'kn-konto-text' }, e.text, e.hinweis ? h('span', { class: 'kn-konto-hinweis' }, e.hinweis) : null), e.pille ? h('span', { class: 'kn-pille kn-pille-orange' }, e.pille) : null]
|
||||
const klasse = `kn-konto-zeile${e.betont ? ' kn-konto-zeile--betont' : ''}`
|
||||
if (e.href === undefined) return h('button', { class: klasse, type: 'button', 'data-konto': e.id, on: { click: () => (e.id === 'abmelden' ? onAbmelden() : onInstall?.()) } }, inhalt)
|
||||
const intern = e.href.startsWith('#')
|
||||
return h('a', { class: klasse, href: e.href, 'data-konto': e.id, target: e.neuerTab ? '_blank' : null, rel: e.neuerTab ? 'noopener' : null,
|
||||
on: { click: (ev) => { if (!intern || mitModifikator(ev)) return; ev.preventDefault(); navigiere(e.href) } } }, inhalt) // der Router schließt das Overlay selbst
|
||||
}
|
||||
|
||||
function zeichne() {
|
||||
const aktiv = globalThis.document?.activeElement?.closest?.('[data-konto]')?.getAttribute('data-konto')
|
||||
leeren(liste)
|
||||
for (const e of kontoEintraege({ fristen: stand.fristen, install: stand.install })) liste.append(zeile(e))
|
||||
if (stand.offen && aktiv) liste.querySelector(`[data-konto="${aktiv}"]`)?.focus()
|
||||
}
|
||||
|
||||
function fokussierbare() {
|
||||
return wurzel.querySelectorAll('a[href], button:not([disabled])')
|
||||
}
|
||||
|
||||
// Tab bleibt im Sheet (aria-modal)
|
||||
function beiTaste(ev) {
|
||||
if (ev.key === 'Escape') { ev.preventDefault(); schliesse(); return }
|
||||
if (ev.key !== 'Tab') return
|
||||
const alle = fokussierbare()
|
||||
const erstes = alle[0]
|
||||
const letztes = alle.at(-1)
|
||||
const aktiv = globalThis.document?.activeElement
|
||||
if (!ev.shiftKey && aktiv === letztes) { ev.preventDefault(); erstes.focus() }
|
||||
else if (ev.shiftKey && aktiv === erstes) { ev.preventDefault(); letztes.focus() }
|
||||
}
|
||||
|
||||
// Zu und Fokus zurück; vom Router (Zurück-Taste, Navigation) und von schliesse() genutzt. Mehrfachaufruf schadet nicht.
|
||||
function schliesseAnsicht() {
|
||||
if (!stand.offen) return
|
||||
stand.offen = false
|
||||
wurzel.hidden = true
|
||||
stand.ausloeser?.setAttribute('aria-expanded', 'false')
|
||||
stand.ausloeser?.focus?.({ preventScroll: true })
|
||||
}
|
||||
|
||||
/** Vom Besitzer (Esc, Scrim, „Schließen“): zumachen und den Overlay-Eintrag des Routers zurücknehmen. */
|
||||
function schliesse() {
|
||||
if (!stand.offen) return
|
||||
schliesseAnsicht()
|
||||
overlay.beende()
|
||||
}
|
||||
|
||||
function oeffne(ausloeser) {
|
||||
if (stand.offen) return
|
||||
stand.offen = true
|
||||
stand.ausloeser = ausloeser ?? null
|
||||
ausloeser?.setAttribute('aria-expanded', 'true')
|
||||
wurzel.hidden = false
|
||||
overlay.oeffne({ schliesse: schliesseAnsicht })
|
||||
liste.firstElementChild?.focus()
|
||||
}
|
||||
|
||||
function aktualisiere({ fristen: f, install: i } = {}) {
|
||||
stand.fristen = f
|
||||
stand.install = i
|
||||
zeichne()
|
||||
}
|
||||
|
||||
zeichne()
|
||||
return { wurzel, oeffne, schliesse, istOffen: () => stand.offen, aktualisiere }
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/* Kundenansicht neu: Navigation der Hülle. Handy: Fahrstuhl-Zeile unten mit vier Wörtern und schwarzem Balken (reiter.js, art 'balken').
|
||||
Desktop: Menü links, Wörter ohne Icons. Beide zeigen dieselben Zähler. Dazu der reine Zustand der Bildschirmtastatur
|
||||
(Leiste und Streifen verschwinden nur bei Texteingabe) und seine Verdrahtung mit visualViewport, focusin und focusout. */
|
||||
|
||||
import { h, leeren } from './dom.js'
|
||||
import { baueReiter, anzeigeText } from './reiter.js'
|
||||
|
||||
// Genau einmal in JS; das CSS führt dieselbe Abfrage (basis.css, huelle.css).
|
||||
export const HANDY_ABFRAGE = '(max-width: 880px), (pointer: coarse) and (max-height: 500px)'
|
||||
|
||||
export const WOERTER = [
|
||||
{ key: 'website', label: 'Website' },
|
||||
{ key: 'funktionen', label: 'Funktionen' },
|
||||
{ key: 'chat', label: 'Chat', einheit: 'ungelesen' },
|
||||
{ key: 'vertraege', label: 'Verträge', einheit: 'offen', ausrufText: 'Zahlung fehlgeschlagen' },
|
||||
]
|
||||
|
||||
const beschriftung = (wort, anzeige) => {
|
||||
const text = anzeigeText(anzeige)
|
||||
if (!text) return null
|
||||
if (anzeige.ausruf) return `${wort.label}, ${wort.ausrufText ?? 'Hinweis'}`
|
||||
return `${wort.label}, ${text}${wort.einheit ? ` ${wort.einheit}` : ''}`
|
||||
}
|
||||
|
||||
/** Fahrstuhl-Zeile in `haupt` und Menü in `menue` bauen. onWahl(bereich) wird bei Klick auf ein Wort gerufen. */
|
||||
export function baueNavigation({ haupt, menue, onWahl }) {
|
||||
const reiter = baueReiter({ eintraege: WOERTER, aktiv: '', art: 'balken', ariaLabel: 'Bereiche', bei: (key) => onWahl(key) })
|
||||
leeren(haupt).append(reiter)
|
||||
|
||||
const knoepfe = Object.fromEntries(WOERTER.map((w) => [w.key, h('button', { class: 'kn-nav-item', type: 'button', 'data-bereich': w.key, on: { click: () => onWahl(w.key) } }, h('span', { class: 'kn-nav-text' }, w.label))]))
|
||||
leeren(menue).append(h('span', { class: 'kn-marke' }, 'WEBklar'), ...WOERTER.map((w) => knoepfe[w.key]))
|
||||
|
||||
function setzeAktiv(bereich) {
|
||||
reiter.setzeAktiv(bereich)
|
||||
for (const w of WOERTER) {
|
||||
if (w.key === bereich) knoepfe[w.key].setAttribute('aria-current', 'page')
|
||||
else knoepfe[w.key].removeAttribute('aria-current')
|
||||
}
|
||||
}
|
||||
|
||||
function zeigeZahl(wort, anzeige) {
|
||||
reiter.setzeAnzeige(wort.key, anzeige)
|
||||
const knopf = knoepfe[wort.key]
|
||||
let zahl = knopf.querySelector('.kn-nav-zahl')
|
||||
const text = anzeigeText(anzeige)
|
||||
if (!text) { zahl?.remove(); knopf.removeAttribute('aria-label'); return }
|
||||
if (!zahl) { zahl = h('span', { class: 'kn-nav-zahl' }); knopf.append(zahl) }
|
||||
zahl.textContent = text
|
||||
zahl.classList.toggle('kn-nav-zahl--heiss', wort.key === 'chat' || anzeige.ausruf === true)
|
||||
knopf.setAttribute('aria-label', beschriftung(wort, anzeige))
|
||||
}
|
||||
|
||||
function setzeZaehler({ chat = 0, vertraege = { zahl: 0, ausruf: false } } = {}) {
|
||||
zeigeZahl(WOERTER[2], { zahl: chat, ausruf: false })
|
||||
zeigeZahl(WOERTER[3], { zahl: vertraege.zahl ?? 0, ausruf: vertraege.ausruf === true })
|
||||
}
|
||||
|
||||
/** Etage 2 offen: das Reel (Elternelement der Hauptzeile) fährt eine Zeile hoch, die Hauptzeile ist gesperrt. */
|
||||
function setzeEtage(offen) {
|
||||
const reel = haupt.parentElement
|
||||
if (offen) { reel?.setAttribute('data-etage', 'offen'); haupt.setAttribute('inert', '') }
|
||||
else { reel?.removeAttribute('data-etage'); haupt.removeAttribute('inert') }
|
||||
}
|
||||
|
||||
return { setzeAktiv, setzeZaehler, setzeEtage }
|
||||
}
|
||||
|
||||
const SCHWELLE = 150 // so viel kleiner als die volle Höhe heißt: Tastatur offen (wie app.js:2475)
|
||||
const TEXT_FELD = 'textarea, input:not([type=checkbox]):not([type=radio]):not([type=file]):not([type=date]):not([type=button]):not([type=submit]):not([type=range]):not([type=color])'
|
||||
|
||||
/** Reiner Zustand { voll, breite, offen, tippt }. Ereignisse: { art:'vv', hoehe, breite, textfeldAktiv?, grob? }, { art:'fokus', textfeld, grob }, { art:'unfokus', textfeldAktiv }. */
|
||||
export function tastaturZustand(alt, ereignis) {
|
||||
if (ereignis.art === 'fokus') return { ...alt, tippt: Boolean(ereignis.textfeld && ereignis.grob) }
|
||||
if (ereignis.art === 'unfokus') return { ...alt, tippt: alt.tippt && Boolean(ereignis.textfeldAktiv) }
|
||||
// visualViewport: ein Breitenwechsel (Drehen, Zoom) setzt die volle Höhe zurück, sonst wächst sie mit
|
||||
const gedreht = Math.abs(ereignis.breite - alt.breite) > 1
|
||||
const voll = gedreht ? ereignis.hoehe : Math.max(alt.voll, ereignis.hoehe)
|
||||
const offen = gedreht ? false : ereignis.hoehe < voll - SCHWELLE
|
||||
let tippt = alt.tippt
|
||||
if (alt.offen && !offen) tippt = false // Tastatur ging zu, auch per Android-Geste ohne Blur
|
||||
else if (offen && ereignis.textfeldAktiv && ereignis.grob) tippt = true
|
||||
return { voll, breite: gedreht ? ereignis.breite : alt.breite, offen, tippt }
|
||||
}
|
||||
|
||||
/** Leiste und Streifen sind nur bei Texteingabe weg. */
|
||||
export const leisteSichtbar = (zustand) => !zustand.tippt
|
||||
|
||||
/** Verdrahtung mit visualViewport, focusin und focusout; onChange(zustand) nur bei Änderung von offen oder tippt. */
|
||||
export function beobachteTastatur({ win = window, doc = document, onChange }) {
|
||||
const vv = win.visualViewport
|
||||
const grob = () => Boolean(win.matchMedia?.('(pointer: coarse)').matches)
|
||||
const istTextfeld = (el) => Boolean(el?.matches?.(TEXT_FELD))
|
||||
let z = { voll: vv?.height ?? win.innerHeight, breite: vv?.width ?? win.innerWidth, offen: false, tippt: false }
|
||||
const wende = (ereignis) => {
|
||||
const alt = z
|
||||
z = tastaturZustand(alt, ereignis)
|
||||
if (z.offen !== alt.offen || z.tippt !== alt.tippt) onChange(z)
|
||||
}
|
||||
const beiFokus = (ev) => wende({ art: 'fokus', textfeld: istTextfeld(ev.target), grob: grob() })
|
||||
// Nach dem Blur kurz warten: wechselt der Fokus von Feld zu Feld, bleibt die Tastatur
|
||||
const warte = (fn, ms) => (win.setTimeout ? win.setTimeout(fn, ms) : setTimeout(fn, ms))
|
||||
const beiUnfokus = () => warte(() => wende({ art: 'unfokus', textfeldAktiv: istTextfeld(doc.activeElement) }), 80)
|
||||
const beiVv = () => wende({ art: 'vv', hoehe: vv.height, breite: vv.width, textfeldAktiv: istTextfeld(doc.activeElement), grob: grob() })
|
||||
doc.addEventListener('focusin', beiFokus)
|
||||
doc.addEventListener('focusout', beiUnfokus)
|
||||
vv?.addEventListener('resize', beiVv)
|
||||
return {
|
||||
stand: () => z,
|
||||
beende() {
|
||||
doc.removeEventListener('focusin', beiFokus)
|
||||
doc.removeEventListener('focusout', beiUnfokus)
|
||||
vv?.removeEventListener('resize', beiVv)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"id": "/dashboard.html",
|
||||
"name": "WEBklar Kundenbereich",
|
||||
"short_name": "WEBklar",
|
||||
"description": "Website, Funktionen, Chat und Verträge – der WEBklar-Kundenbereich als App.",
|
||||
"lang": "de",
|
||||
"dir": "ltr",
|
||||
"start_url": "/dashboard.html",
|
||||
"scope": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#ffffff",
|
||||
"theme_color": "#ffffff",
|
||||
"categories": ["business", "productivity"],
|
||||
"icons": [
|
||||
{ "src": "/img/app-icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" },
|
||||
{ "src": "/img/app-icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" },
|
||||
{ "src": "/img/app-icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
|
||||
],
|
||||
"shortcuts": [
|
||||
{ "name": "Chat", "short_name": "Chat", "url": "/dashboard.html#chat", "icons": [{ "src": "/img/app-icon-192.png", "sizes": "192x192", "type": "image/png" }] },
|
||||
{ "name": "Rechnungen", "short_name": "Rechnungen", "url": "/dashboard.html#rechnungen", "icons": [{ "src": "/img/app-icon-192.png", "sizes": "192x192", "type": "image/png" }] },
|
||||
{ "name": "Verträge", "short_name": "Verträge", "url": "/dashboard.html#vertraege", "icons": [{ "src": "/img/app-icon-192.png", "sizes": "192x192", "type": "image/png" }] }
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,42 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<meta name="referrer" content="no-referrer">
|
||||
<meta name="color-scheme" content="light">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
<title>Auftragsbestätigung – WEBklar</title>
|
||||
<link rel="icon" href="/favicon.ico" sizes="32x32">
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
||||
<link rel="stylesheet" href="/neu/tokens.css">
|
||||
<link rel="stylesheet" href="/neu/oeffentlich/oeffentlich.css">
|
||||
<noscript><style>.ab-laden{display:none}</style></noscript>
|
||||
</head>
|
||||
<body class="ab-seite">
|
||||
<!-- Bestätigungsseite für eine Auftragsbestätigung (Link aus der Mail K31: /auftrag.html#t=<Token>, Rückfall ?t=<Token>).
|
||||
Der Token steht nur in der Adresse und im Speicher der Seite: keine Cookies, kein Browserspeicher, keine Fremdanfragen,
|
||||
nichts im Protokoll (auftrag.js). Ohne Anmeldung erreichbar, nicht für Suchmaschinen. -->
|
||||
<a class="ab-app-back" href="/dashboard.html">‹ Zurück zum Kundenbereich</a>
|
||||
<header class="ab-kopf">
|
||||
<div class="ab-innen"><img class="ab-logo" src="/webklar-logo.png" width="36" height="36" alt=""><span class="ab-marke">WEBklar</span></div>
|
||||
</header>
|
||||
<main class="ab-haupt">
|
||||
<div class="ab-innen">
|
||||
<div class="ab-wurzel" id="ab-wurzel" data-zustand="laedt" aria-busy="true">
|
||||
<noscript><p>Bitte aktivieren Sie JavaScript, um Ihren Auftrag zu bestätigen.</p></noscript>
|
||||
<p class="ab-laden">Ihre Auftragsbestätigung wird geladen …</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<div class="ab-vh" id="ab-ansage" aria-live="polite"></div>
|
||||
<footer class="ab-fuss">
|
||||
<div class="ab-innen">
|
||||
<a href="/datenschutz.html" target="_blank" rel="noopener noreferrer">Datenschutz</a>
|
||||
<a href="https://webklar.com/impressum" target="_blank" rel="noopener noreferrer">Impressum</a>
|
||||
</div>
|
||||
</footer>
|
||||
<script type="module" src="/auftrag.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,75 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
<title>Verträge hier kündigen – WEBklar</title>
|
||||
<link rel="stylesheet" href="/neu/tokens.css">
|
||||
<link rel="stylesheet" href="/neu/oeffentlich/oeffentlich.css">
|
||||
</head>
|
||||
<body class="kn kn-seite kn-recht">
|
||||
<main>
|
||||
<!-- Nur in der installierten App sichtbar: dort gibt es keinen Zurück-Knopf des Browsers -->
|
||||
<a class="kn-zurueck no-print" href="/dashboard.html">‹ Zurück zum Kundenbereich</a>
|
||||
<div class="kn-markenzeile">WEBKLAR</div>
|
||||
<h1>Verträge hier kündigen</h1>
|
||||
|
||||
<form id="decl-form" data-anrede="du" class="kn-formular" novalidate>
|
||||
<!-- § 312k Abs. 3 BGB: eindeutige Bezeichnung des Vertrags - das Abo (mit Zusatzspeicher) oder nur der Zusatzspeicher -->
|
||||
<fieldset>
|
||||
<legend data-label-for="contract" data-label="Vertrag">Vertrag</legend>
|
||||
<label class="kn-auswahl"><input type="radio" name="contract" value="abo" data-text="Mein Abo (Hosting- oder Wartungs-Abo, mit Zusatzspeicher)" checked> Mein Abo (Hosting- oder Wartungs-Abo, mit Zusatzspeicher)</label>
|
||||
<label class="kn-auswahl"><input type="radio" name="contract" value="zusatzspeicher" data-text="Nur den E-Mail-Zusatzspeicher"> Nur den E-Mail-Zusatzspeicher</label>
|
||||
</fieldset>
|
||||
<div id="packages-wrap" class="hidden">
|
||||
<label for="packages" data-label-for="packages" data-label="Pakete">Zahl der Pakete, die du kündigst <small>(je 5 GB; leer = alle gebuchten Pakete)</small></label>
|
||||
<input id="packages" name="packages" type="number" inputmode="numeric" min="1" max="10" step="1" placeholder="alle">
|
||||
<p class="kn-hinweis">Die Kündigung des Zusatzspeichers wirkt zum Ende des laufenden Abrechnungsmonats. Das geht auch, wenn dein Abo schon gekündigt ist.</p>
|
||||
</div>
|
||||
|
||||
<fieldset>
|
||||
<legend data-label-for="kind" data-label="Art der Kündigung">Art der Kündigung</legend>
|
||||
<label class="kn-auswahl"><input type="radio" name="kind" value="ordentlich" data-text="Ordentliche Kündigung" checked> Ordentliche Kündigung</label>
|
||||
<label class="kn-auswahl"><input type="radio" name="kind" value="ausserordentlich" data-text="Außerordentliche Kündigung"> Außerordentliche Kündigung aus wichtigem Grund</label>
|
||||
<label class="kn-auswahl"><input type="radio" name="kind" value="geschaeftsaufgabe" data-text="Sonderkündigung wegen Geschäftsaufgabe"> Sonderkündigung wegen Geschäftsaufgabe</label>
|
||||
</fieldset>
|
||||
<div id="reason-wrap" class="hidden">
|
||||
<label for="reason" data-label-for="reason" data-label="Kündigungsgrund">Kündigungsgrund</label>
|
||||
<textarea id="reason" name="reason" maxlength="2000"></textarea>
|
||||
</div>
|
||||
|
||||
<label for="name" data-label-for="name" data-label="Name">Name</label>
|
||||
<input id="name" name="name" autocomplete="name" required maxlength="200">
|
||||
|
||||
<label for="email" data-label-for="email" data-label="E-Mail für die Bestätigung">E-Mail-Adresse <small>(dorthin schicken wir die Bestätigung)</small></label>
|
||||
<input id="email" name="email" type="email" autocomplete="email" required maxlength="255">
|
||||
|
||||
<label for="customerNumber" data-label-for="customerNumber" data-label="Kundennummer">Kundennummer <small>(optional)</small></label>
|
||||
<input id="customerNumber" name="customerNumber" maxlength="50">
|
||||
|
||||
<fieldset id="when-wrap">
|
||||
<legend>Zeitpunkt</legend>
|
||||
<label class="kn-auswahl"><input type="radio" name="when" value="naechstmoeglich" checked> zum nächstmöglichen Zeitpunkt</label>
|
||||
<label class="kn-auswahl"><input type="radio" name="when" value="datum"> zu einem bestimmten Datum</label>
|
||||
<input type="date" id="effective-date" class="hidden">
|
||||
</fieldset>
|
||||
|
||||
<p class="kn-fehlertext hidden" id="form-error" role="alert"></p>
|
||||
<button type="submit" class="kn-absenden">Jetzt kündigen</button>
|
||||
</form>
|
||||
|
||||
<div id="done" class="kn-bestaetigung hidden">
|
||||
<h2>Deine Kündigung ist eingegangen</h2>
|
||||
<p>Eingang: <strong id="done-at"></strong></p>
|
||||
<dl id="done-list"></dl>
|
||||
<p>Eine Eingangsbestätigung mit diesen Angaben haben wir an deine E-Mail-Adresse geschickt. Den Zeitpunkt, zu dem die Kündigung wirkt, bestätigen wir dir per E-Mail.</p>
|
||||
<button type="button" class="kn-drucken" id="print-btn">Diese Bestätigung drucken oder als PDF speichern</button>
|
||||
</div>
|
||||
</main>
|
||||
<script src="/legal-form.js"></script>
|
||||
<script>
|
||||
initLegalForm({ endpoint: '/api/abo/public/cancel', fields: ['contract', 'packages', 'kind', 'reason', 'name', 'email', 'customerNumber', 'effective'] })
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,90 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>Anmelden – Webklar Kundenbereich</title>
|
||||
<!-- Anmeldung im weißen Stil (noch nicht ausgeliefert, P10 schaltet sie um). Als App auf den Homescreen, helle Statusleiste -->
|
||||
<link rel="manifest" href="/neu/manifest.webmanifest">
|
||||
<link rel="icon" href="/favicon.ico" sizes="32x32">
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
||||
<link rel="apple-touch-icon" href="/img/apple-touch-icon.png">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default">
|
||||
<meta name="apple-mobile-web-app-title" content="WEBklar">
|
||||
<link rel="stylesheet" href="/neu/tokens.css">
|
||||
<link rel="stylesheet" href="/neu/oeffentlich/oeffentlich.css">
|
||||
</head>
|
||||
<body class="kn kn-seite kn-login-seite">
|
||||
<header class="kn-login-kopf">
|
||||
<a class="kn-login-logo" href="/login.html">Webklar</a>
|
||||
</header>
|
||||
|
||||
<main class="kn-login-haupt">
|
||||
<div class="kn-login-spalte">
|
||||
<!-- Handy: App-Icon wie beim Start einer App -->
|
||||
<div class="kn-login-marke">
|
||||
<img src="/img/app-icon-192.png" alt="" width="64" height="64">
|
||||
<span>WEBklar Kundenbereich</span>
|
||||
</div>
|
||||
<div class="kn-login-abschnitt">
|
||||
<p class="kn-login-titel">Anmelden</p>
|
||||
<form id="login-form" class="kn-login-form" novalidate>
|
||||
<input
|
||||
class="kn-feld-eingabe"
|
||||
type="email"
|
||||
name="email"
|
||||
id="email"
|
||||
placeholder="E-Mail"
|
||||
required
|
||||
autocomplete="username"
|
||||
autocapitalize="none"
|
||||
spellcheck="false"
|
||||
enterkeyhint="next"
|
||||
>
|
||||
<input
|
||||
class="kn-feld-eingabe"
|
||||
type="password"
|
||||
name="password"
|
||||
id="password"
|
||||
placeholder="Passwort"
|
||||
required
|
||||
autocomplete="current-password"
|
||||
enterkeyhint="go"
|
||||
>
|
||||
<p id="login-error" class="kn-fehlertext hidden" role="alert"></p>
|
||||
<!-- Mitarbeiter-Login ist gesperrt: Link ins Ticketsystem (Adresse aus der Antwort, ticketUrl) -->
|
||||
<a id="login-staff-link" class="kn-login-staff hidden" href="https://ticket.webklar.com">Zum Ticketsystem</a>
|
||||
<button type="submit" id="login-btn" class="kn-login-knopf">
|
||||
<span class="button-label">Anmelden</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 11 10" aria-hidden="true">
|
||||
<path fill="currentColor" d="m4.56 8.352 1.824-1.84q.224-.225.144-.4-.064-.192-.384-.192H0V3.76h6.128q.32 0 .4-.176.08-.192-.144-.416l-1.84-1.84L5.84 0l4.848 4.848L5.856 9.68z"></path>
|
||||
</svg>
|
||||
<svg class="loading-spinner" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 1 1-6.219-8.56"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<p class="kn-login-hinweis">
|
||||
Zugangsdaten erhältst du von Webklar. Dein Passwort kannst du nach der Anmeldung selbst ändern (Profilbild oben rechts, „Passwort ändern“).
|
||||
Passwort vergessen? Schreib uns an <a href="mailto:support@webklar.com">support@webklar.com</a>.
|
||||
</p>
|
||||
<p class="kn-login-hinweis">
|
||||
<a class="kn-login-wichtig" href="/kuendigen.html">Verträge hier kündigen</a> ·
|
||||
<a class="kn-login-wichtig" href="/widerrufen.html">Vertrag widerrufen</a><br>
|
||||
<a href="/abo-bedingungen.html" target="_blank" rel="noopener">Vertragsbedingungen</a> ·
|
||||
<a href="/datenschutz.html" target="_blank" rel="noopener">Datenschutz</a> ·
|
||||
<a href="https://webklar.com/impressum" target="_blank" rel="noopener">Impressum</a>
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script src="/app.js"></script>
|
||||
<script>
|
||||
initLoginPage();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,240 @@
|
||||
/* Kundenansicht neu: die öffentlichen Seiten im weißen Stil (Anmeldung, Kündigen, Widerrufen, Auftragsbestätigung).
|
||||
Werte aus tokens.css (Klasse .kn auf dem body); die Auftragsseite trägt kein .kn und setzt ihre Werte unten selbst.
|
||||
Ohne Karten, Verläufe, Glas, Lila und Dunkelmodus. Die Offline-Seite hat ihr Aussehen inline (der Service Worker liefert nur diese eine Datei). */
|
||||
|
||||
/* ------------------------------------------------------------------ Gemeinsam (Anmeldung, Kündigen, Widerrufen) --- */
|
||||
.kn-seite { margin: 0; background: var(--kn-bg); color: var(--kn-text); font: 16px/1.55 var(--kn-font); -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: 100%; }
|
||||
.kn-seite *, .kn-seite *::before, .kn-seite *::after { box-sizing: border-box; }
|
||||
.kn-seite .hidden { display: none !important; }
|
||||
.kn-seite a { color: inherit; text-decoration: underline; text-decoration-color: var(--kn-line-strong); text-underline-offset: 3px; }
|
||||
.kn-seite a:hover { text-decoration-color: currentColor; }
|
||||
.kn-seite :focus-visible { outline: 2px solid var(--kn-accent); outline-offset: 2px; }
|
||||
.kn-seite :disabled { opacity: .55; cursor: not-allowed; }
|
||||
.kn-seite .kn-fehlertext { margin: 0; font-size: 14px; line-height: 20px; color: var(--kn-danger); font-weight: 600; }
|
||||
|
||||
/* ------------------------------------------------------------------ Anmeldung --- */
|
||||
.kn-login-seite { min-height: 100svh; }
|
||||
.kn-login-kopf { position: fixed; top: 0; left: 0; right: 0; z-index: 2; padding: calc(24px + env(safe-area-inset-top)) max(24px, env(safe-area-inset-right)) 16px max(24px, env(safe-area-inset-left)); pointer-events: none; }
|
||||
.kn-login-logo { pointer-events: auto; font-size: 18px; line-height: 24px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; }
|
||||
.kn-seite a.kn-login-logo { text-decoration: none; }
|
||||
.kn-login-haupt { min-height: 100svh; display: flex; align-items: center; justify-content: center; padding: calc(88px + env(safe-area-inset-top)) max(24px, env(safe-area-inset-right)) calc(40px + env(safe-area-inset-bottom)) max(24px, env(safe-area-inset-left)); }
|
||||
.kn-login-spalte { width: 100%; max-width: 360px; }
|
||||
.kn-login-marke { display: none; }
|
||||
.kn-login-titel { margin: 0 0 24px; font-size: 22px; line-height: 28px; font-weight: 700; letter-spacing: -.02em; }
|
||||
.kn-login-form { display: flex; flex-direction: column; gap: 12px; }
|
||||
.kn-feld-eingabe {
|
||||
width: 100%; height: 44px; min-width: 0; margin: 0; padding: 0 12px; border: 0; border-radius: var(--kn-radius);
|
||||
background: var(--kn-bg); box-shadow: var(--kn-ring); color: inherit; font: inherit; font-size: 16px; outline: none; transition: box-shadow .12s;
|
||||
}
|
||||
.kn-feld-eingabe::placeholder { color: var(--kn-text-3); }
|
||||
.kn-feld-eingabe:focus { box-shadow: 0 0 0 1px var(--kn-accent), 0 0 0 4px var(--kn-accent-ring); }
|
||||
.kn-login-form .kn-fehlertext { text-align: center; }
|
||||
.kn-login-staff { display: block; text-align: center; font-size: 14px; font-weight: 600; }
|
||||
.kn-login-knopf {
|
||||
position: relative; display: flex; align-items: center; justify-content: center; gap: 8px; width: 100%; min-height: 44px; margin-top: 4px;
|
||||
border: 0; border-radius: var(--kn-radius); background: var(--kn-accent); color: var(--kn-bg); font: inherit; font-weight: 600; cursor: pointer;
|
||||
box-shadow: var(--kn-shadow-btn); transition: background-color .12s;
|
||||
}
|
||||
.kn-login-knopf:hover:not(:disabled) { background: var(--kn-accent-hover); }
|
||||
.kn-login-knopf svg { width: 12px; height: 12px; flex-shrink: 0; }
|
||||
.kn-login-knopf .loading-spinner { display: none; }
|
||||
.kn-login-knopf.is-loading svg { display: none; }
|
||||
.kn-login-knopf.is-loading .button-label::after { content: ' …'; }
|
||||
.kn-login-hinweis { margin: 32px 0 0; text-align: center; font-size: 14px; line-height: 28px; color: var(--kn-text-2); }
|
||||
.kn-login-hinweis + .kn-login-hinweis { margin-top: 16px; }
|
||||
.kn-seite a.kn-login-wichtig { color: var(--kn-text); font-weight: 600; }
|
||||
@media (max-width: 880px), (pointer: coarse) and (max-height: 500px) {
|
||||
.kn-login-marke { display: flex; flex-direction: column; align-items: center; gap: 12px; margin: 0 0 32px; font-size: 15px; font-weight: 600; }
|
||||
.kn-login-marke img { width: 64px; height: 64px; border-radius: var(--kn-radius-lg); box-shadow: var(--kn-ring); }
|
||||
.kn-login-titel { text-align: center; }
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ Kündigen und Widerrufen (schlicht) --- */
|
||||
.kn-recht main { max-width: 620px; margin: 0 auto; padding: 48px max(20px, env(safe-area-inset-right)) 80px max(20px, env(safe-area-inset-left)); }
|
||||
.kn-recht h1 { margin: 0 0 8px; font-size: 28px; line-height: 34px; font-weight: 700; letter-spacing: -.02em; }
|
||||
.kn-recht h2 { margin: 0 0 12px; font-size: 18px; line-height: 26px; font-weight: 600; }
|
||||
.kn-markenzeile { margin-bottom: 8px; font-size: 13px; font-weight: 700; letter-spacing: .12em; color: var(--kn-text-2); }
|
||||
.kn-recht .kn-formular { margin: 24px 0; }
|
||||
.kn-recht label { display: block; margin: 16px 0 6px; font-size: 14px; font-weight: 600; }
|
||||
.kn-recht label small { font-weight: 400; color: var(--kn-text-2); }
|
||||
.kn-recht input:not([type="radio"]):not([type="checkbox"]), .kn-recht textarea {
|
||||
width: 100%; min-height: 44px; padding: 0 12px; border: 0; border-radius: var(--kn-radius); background: var(--kn-bg);
|
||||
box-shadow: var(--kn-ring); color: inherit; font: inherit; font-size: 16px; outline: none; transition: box-shadow .12s;
|
||||
}
|
||||
.kn-recht textarea { min-height: 96px; padding: 8px 12px; resize: vertical; }
|
||||
.kn-recht input:not([type="radio"]):not([type="checkbox"]):focus, .kn-recht textarea:focus { box-shadow: 0 0 0 1px var(--kn-accent), 0 0 0 4px var(--kn-accent-ring); }
|
||||
.kn-recht input::placeholder { color: var(--kn-text-3); }
|
||||
.kn-recht fieldset { margin: 0; padding: 0; border: 0; min-width: 0; }
|
||||
.kn-recht legend { margin: 16px 0 6px; padding: 0; font-size: 14px; font-weight: 600; }
|
||||
.kn-recht .kn-auswahl { display: flex; align-items: center; gap: 10px; min-height: 44px; margin: 0; font-weight: 400; font-size: 16px; cursor: pointer; }
|
||||
.kn-recht .kn-auswahl input { flex: 0 0 auto; width: 20px; height: 20px; margin: 0; accent-color: var(--kn-accent); }
|
||||
.kn-recht #effective-date { max-width: 220px; margin-top: 6px; }
|
||||
.kn-hinweis { margin: 8px 0 0; font-size: 14px; line-height: 20px; color: var(--kn-text-2); }
|
||||
.kn-recht .kn-fehlertext { margin: 16px 0 0; }
|
||||
.kn-absenden {
|
||||
display: inline-flex; align-items: center; justify-content: center; min-height: 44px; margin-top: 20px; padding: 0 24px; border: 0; border-radius: var(--kn-radius);
|
||||
background: var(--kn-accent); color: var(--kn-bg); font: inherit; font-weight: 600; cursor: pointer; box-shadow: var(--kn-shadow-btn);
|
||||
}
|
||||
.kn-absenden:hover:not(:disabled) { background: var(--kn-accent-hover); }
|
||||
.kn-bestaetigung { margin: 24px 0; padding-top: 24px; border-top: 1px solid var(--kn-line); }
|
||||
.kn-bestaetigung h2 { color: var(--kn-gruen-fg); }
|
||||
.kn-recht dl { display: grid; grid-template-columns: max-content 1fr; gap: 4px 16px; margin: 12px 0; }
|
||||
.kn-recht dt { color: var(--kn-text-2); }
|
||||
.kn-recht dd { margin: 0; overflow-wrap: anywhere; }
|
||||
.kn-drucken {
|
||||
display: inline-flex; align-items: center; min-height: 44px; margin-top: 14px; padding: 0 16px; border: 0; border-radius: var(--kn-radius);
|
||||
background: var(--kn-bg); box-shadow: var(--kn-ring); color: inherit; font: inherit; font-weight: 500; cursor: pointer;
|
||||
}
|
||||
.kn-drucken:hover { background: var(--kn-bg-hover); }
|
||||
@media print { .kn-drucken, .no-print { display: none !important; } }
|
||||
/* Installierte App (Homescreen): Platz für die Statusleiste und ein Weg zurück in den Kundenbereich */
|
||||
.kn-zurueck { display: none; }
|
||||
@media (display-mode: standalone) {
|
||||
.kn-recht main { padding-top: max(48px, calc(env(safe-area-inset-top) + 20px)); }
|
||||
/* Die Statusleiste ist hell: ein weißer Streifen dahinter hält Uhrzeit und Netz lesbar */
|
||||
.kn-seite::before { content: ''; position: fixed; top: 0; left: 0; right: 0; z-index: 10; height: env(safe-area-inset-top); background: var(--kn-bg); }
|
||||
.kn-seite a.kn-zurueck { display: inline-block; margin-bottom: 18px; font-size: 15px; font-weight: 600; text-decoration: none; }
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.kn-recht dl { grid-template-columns: 1fr; gap: 0; }
|
||||
.kn-recht dd { margin-bottom: 8px; }
|
||||
.kn-absenden { width: 100%; }
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ Auftragsbestätigung (auftrag.html, Link aus der Mail) --- */
|
||||
/* body.ab-seite trägt kein .kn: die Tokens aus tokens.css stehen deshalb hier noch einmal (gleiche Werte), die --ab-* leiten sich daraus ab */
|
||||
body.ab-seite {
|
||||
--kn-bg: #fff;
|
||||
--kn-bg-hover: #f6f8fa;
|
||||
--kn-bg-subtle: #f8f9fb;
|
||||
--kn-text: #1a1f36;
|
||||
--kn-text-2: #5e6678;
|
||||
--kn-text-3: #8a93a3;
|
||||
--kn-line: #ebeef1;
|
||||
--kn-line-strong: #d8dde3;
|
||||
--kn-accent: #111;
|
||||
--kn-grau-bg: #ebeef1;
|
||||
--kn-grau-fg: #414552;
|
||||
--kn-orange-bg: #fcedb9;
|
||||
--kn-orange-fg: #a82c00;
|
||||
--kn-gruen-bg: #d7f7c2;
|
||||
--kn-gruen-fg: #006908;
|
||||
--kn-rot-bg: #ffe7f2;
|
||||
--kn-rot-fg: #b3093c;
|
||||
--kn-danger: #c0123c;
|
||||
--kn-radius: 6px;
|
||||
--kn-radius-lg: 12px;
|
||||
--kn-shadow-btn: 0 1px 1px rgba(0,0,0,.12), 0 2px 5px rgba(60,66,87,.08);
|
||||
--kn-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
--ab-grund: var(--kn-bg); --ab-karte: var(--kn-bg); --ab-text: var(--kn-text); --ab-weich: var(--kn-text-2); --ab-linie: var(--kn-line); --ab-box: var(--kn-bg-subtle);
|
||||
--ab-knopf: var(--kn-accent); --ab-knopf-text: var(--kn-bg); --ab-fokus: var(--kn-accent);
|
||||
--ab-warn-bg: var(--kn-orange-bg); --ab-warn-text: var(--kn-orange-fg); --ab-ok-bg: var(--kn-gruen-bg); --ab-ok-text: var(--kn-gruen-fg);
|
||||
--ab-rot-bg: var(--kn-rot-bg); --ab-rot-text: var(--kn-rot-fg); --ab-muted-bg: var(--kn-grau-bg); --ab-muted-text: var(--kn-grau-fg); --ab-fehler: var(--kn-danger);
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
/* Grundlayout */
|
||||
body.ab-seite {
|
||||
margin: 0; min-height: 100dvh; display: flex; flex-direction: column;
|
||||
background: var(--ab-grund); color: var(--ab-text);
|
||||
font: 16px/1.55 var(--kn-font); -webkit-text-size-adjust: 100%; -webkit-font-smoothing: antialiased;
|
||||
}
|
||||
.ab-seite .hidden { display: none !important; }
|
||||
.ab-seite .ab-innen { max-width: 40rem; margin: 0 auto; padding: 0 max(16px, env(safe-area-inset-right)) 0 max(16px, env(safe-area-inset-left)); box-sizing: border-box; width: 100%; }
|
||||
.ab-seite .ab-kopf { padding: max(20px, env(safe-area-inset-top)) 0 12px; }
|
||||
.ab-seite .ab-kopf .ab-innen { display: flex; align-items: center; gap: 10px; }
|
||||
.ab-seite .ab-logo { width: 36px; height: 36px; border-radius: 6px; display: block; }
|
||||
.ab-seite .ab-marke { font-weight: 700; letter-spacing: 0.08em; font-size: 15px; }
|
||||
.ab-seite .ab-haupt { flex: 1 0 auto; padding-bottom: 24px; }
|
||||
.ab-seite .ab-wurzel { border-top: 1px solid var(--ab-linie); padding: 24px 0; overflow-wrap: anywhere; word-break: normal; }
|
||||
.ab-seite .ab-laden { margin: 0; color: var(--ab-weich); }
|
||||
.ab-seite a { color: inherit; text-underline-offset: 3px; }
|
||||
.ab-seite .ab-vh {
|
||||
position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
|
||||
}
|
||||
.ab-seite .ab-fuss { flex-shrink: 0; padding: 8px 0 max(24px, env(safe-area-inset-bottom)); font-size: 13px; color: var(--ab-weich); }
|
||||
.ab-seite .ab-fuss .ab-innen { display: flex; gap: 16px; flex-wrap: wrap; }
|
||||
.ab-seite .ab-fuss a { color: var(--ab-weich); display: inline-flex; align-items: center; min-height: 44px; }
|
||||
|
||||
/* Inhalt */
|
||||
.ab-seite .ab-pille {
|
||||
display: inline-block; margin: 0 0 12px; padding: 2px 8px; border-radius: 4px; font-size: 12px; font-weight: 500; line-height: 20px;
|
||||
background: var(--ab-muted-bg); color: var(--ab-muted-text);
|
||||
}
|
||||
.ab-seite .ab-pille-warn { background: var(--ab-warn-bg); color: var(--ab-warn-text); }
|
||||
.ab-seite .ab-pille-ok { background: var(--ab-ok-bg); color: var(--ab-ok-text); }
|
||||
.ab-seite .ab-pille-rot { background: var(--ab-rot-bg); color: var(--ab-rot-text); }
|
||||
.ab-seite .ab-h1 { font-size: 1.6rem; line-height: 1.25; font-weight: 700; letter-spacing: -.02em; margin: 0 0 12px; }
|
||||
.ab-seite .ab-h1:focus { outline: none; }
|
||||
.ab-seite .ab-intro, .ab-seite .ab-text { margin: 0 0 16px; }
|
||||
.ab-seite .ab-text { white-space: pre-line; }
|
||||
.ab-seite .ab-kontakt { margin: 0 0 16px; color: var(--ab-weich); }
|
||||
.ab-seite .ab-h2 {
|
||||
font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.06em; font-weight: 700; color: var(--ab-weich); margin: 24px 0 8px;
|
||||
}
|
||||
.ab-seite .ab-liste { margin: 0 0 8px; padding: 0; }
|
||||
.ab-seite .ab-zeile {
|
||||
display: grid; grid-template-columns: 9rem 1fr; gap: 4px 12px; padding: 8px 0; border-top: 1px solid var(--ab-linie);
|
||||
}
|
||||
.ab-seite .ab-zeile dt { color: var(--ab-weich); }
|
||||
.ab-seite .ab-zeile dd { margin: 0; }
|
||||
.ab-seite .ab-gesamt { display: flex; justify-content: space-between; gap: 12px; margin: 12px 0 8px; padding-top: 12px; border-top: 2px solid var(--ab-text); }
|
||||
.ab-seite .ab-gesamt strong { font-weight: 700; font-variant-numeric: tabular-nums; }
|
||||
.ab-seite .ab-hinweis { margin: 4px 0 0; font-size: 0.85rem; color: var(--ab-weich); }
|
||||
.ab-seite .ab-links { list-style: none; margin: 0 0 16px; padding: 0; }
|
||||
.ab-seite .ab-links a { display: flex; align-items: center; min-height: 44px; line-height: 1.35; padding: 4px 0; font-weight: 600; }
|
||||
.ab-seite .ab-bestaetigt {
|
||||
background: var(--ab-ok-bg); color: var(--ab-ok-text); border-radius: var(--kn-radius); padding: 12px 16px; margin: 0 0 16px;
|
||||
}
|
||||
.ab-seite .ab-bestaetigt p { margin: 2px 0; }
|
||||
.ab-seite .ab-bestaetigt:focus-visible { outline: 2px solid var(--ab-fokus); outline-offset: 2px; }
|
||||
|
||||
/* Tabelle */
|
||||
.ab-seite .ab-tabelle { width: 100%; border-collapse: collapse; font-size: 0.95rem; font-variant-numeric: tabular-nums; }
|
||||
.ab-seite .ab-tabelle th { text-align: left; font-weight: 600; color: var(--ab-weich); padding: 6px 8px 6px 0; border-bottom: 1px solid var(--ab-linie); }
|
||||
.ab-seite .ab-tabelle td { padding: 8px 8px 8px 0; border-bottom: 1px solid var(--ab-linie); vertical-align: top; }
|
||||
.ab-seite .ab-tabelle th:nth-child(n+2), .ab-seite .ab-tabelle td:nth-child(n+2) { text-align: right; white-space: nowrap; }
|
||||
@media (max-width: 640px) {
|
||||
.ab-seite .ab-tabelle thead {
|
||||
position: absolute; width: 1px; height: 1px; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
|
||||
}
|
||||
.ab-seite .ab-tabelle, .ab-seite .ab-tabelle tbody, .ab-seite .ab-tabelle tr, .ab-seite .ab-tabelle td { display: block; }
|
||||
.ab-seite .ab-tabelle tr { padding: 8px 0; border-bottom: 1px solid var(--ab-linie); }
|
||||
.ab-seite .ab-tabelle td {
|
||||
display: flex; justify-content: space-between; gap: 12px; border: 0; padding: 2px 0; text-align: right; white-space: normal;
|
||||
}
|
||||
.ab-seite .ab-tabelle td:first-child { display: block; text-align: left; font-weight: 700; }
|
||||
.ab-seite .ab-tabelle td:nth-child(n+2) { white-space: normal; }
|
||||
.ab-seite .ab-tabelle td:nth-child(n+2)::before { content: attr(data-label); color: var(--ab-weich); text-align: left; }
|
||||
.ab-seite .ab-zeile { grid-template-columns: 1fr; gap: 0; }
|
||||
.ab-seite .ab-wurzel { padding: 20px 0; }
|
||||
}
|
||||
|
||||
/* Formular */
|
||||
.ab-seite .ab-bestaetigen { margin-top: 24px; padding-top: 8px; border-top: 1px solid var(--ab-linie); }
|
||||
.ab-seite .ab-feldgruppe { border: 0; margin: 0; padding: 0; min-width: 0; }
|
||||
.ab-seite .ab-feldgruppe legend { font-weight: 700; padding: 0; margin: 12px 0 4px; }
|
||||
.ab-seite .ab-haken { display: flex; gap: 12px; align-items: flex-start; padding: 10px 0; min-height: 44px; font-size: 15px; cursor: pointer; }
|
||||
.ab-seite .ab-haken input { flex: 0 0 22px; width: 22px; height: 22px; margin: 1px 0 0; accent-color: var(--ab-knopf); }
|
||||
.ab-seite .ab-frei { color: var(--ab-weich); font-size: 0.85em; }
|
||||
.ab-seite .ab-fehler { margin: 8px 0; color: var(--ab-fehler); font-weight: 600; }
|
||||
.ab-seite .ab-fehler[hidden] { display: none; }
|
||||
.ab-seite .ab-knopf {
|
||||
display: inline-block; min-height: 44px; padding: 0 24px; border: 0; border-radius: var(--kn-radius); cursor: pointer;
|
||||
background: var(--ab-knopf); color: var(--ab-knopf-text); font: inherit; font-weight: 600; box-shadow: var(--kn-shadow-btn);
|
||||
}
|
||||
.ab-seite .ab-knopf:disabled { opacity: 0.45; cursor: default; }
|
||||
.ab-seite .ab-knopf:focus-visible, .ab-seite .ab-link:focus-visible { outline: 2px solid var(--ab-fokus); outline-offset: 2px; }
|
||||
.ab-seite .ab-notiz, .ab-seite .ab-gesperrt { margin: 12px 0 0; font-size: 0.85rem; color: var(--ab-weich); }
|
||||
@media (max-width: 640px) {
|
||||
.ab-seite .ab-knopf { width: 100%; }
|
||||
}
|
||||
|
||||
/* Installierte App: Weg zurück in den Kundenbereich */
|
||||
.ab-app-back { display: none; }
|
||||
@media (display-mode: standalone) {
|
||||
.ab-app-back {
|
||||
display: inline-block; margin: calc(env(safe-area-inset-top) + 12px) max(16px, env(safe-area-inset-right)) 0 max(16px, env(safe-area-inset-left)); font-size: 15px; font-weight: 600; color: var(--ab-text); text-decoration: none;
|
||||
}
|
||||
.ab-seite .ab-kopf { padding-top: 12px; }
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
<title>Offline – WEBklar Kundenbereich</title>
|
||||
<!-- Wird vom Service Worker (sw.js) ohne Internet angezeigt: alles inline, keine weiteren Dateien -->
|
||||
<style>
|
||||
html, body { height: 100%; margin: 0; }
|
||||
body {
|
||||
display: grid; place-items: center; padding: 24px max(24px, env(safe-area-inset-right)) 24px max(24px, env(safe-area-inset-left)); box-sizing: border-box;
|
||||
background: #fff; color: #1a1f36;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
text-align: center; line-height: 1.5; -webkit-font-smoothing: antialiased;
|
||||
}
|
||||
main { max-width: 340px; }
|
||||
svg { width: 72px; height: 72px; border-radius: 12px; box-shadow: 0 0 0 1px rgba(60, 66, 87, 0.16), 0 1px 1px rgba(0, 0, 0, 0.06); }
|
||||
h1 { margin: 22px 0 8px; font-size: 22px; font-weight: 700; letter-spacing: -0.02em; }
|
||||
p { margin: 0 0 26px; color: #5e6678; font-size: 15px; }
|
||||
button {
|
||||
min-height: 44px; padding: 0 24px; border: none; border-radius: 6px; background: #111; color: #fff;
|
||||
font: inherit; font-weight: 600; font-size: 15px; cursor: pointer;
|
||||
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.12), 0 2px 5px rgba(60, 66, 87, 0.08);
|
||||
}
|
||||
button:active { opacity: 0.85; }
|
||||
button:focus-visible { outline: 2px solid #111; outline-offset: 2px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<svg viewBox="0 0 512 512" aria-hidden="true">
|
||||
<rect width="512" height="512" fill="#fff"/>
|
||||
<g transform="translate(256 256) scale(0.9) translate(-256 -256)">
|
||||
<ellipse cx="256" cy="256" rx="210" ry="150" fill="#111" transform="rotate(-28 256 256)"/>
|
||||
<circle cx="356" cy="172" r="18" fill="#fff"/>
|
||||
<path transform="translate(256 256) rotate(-28)" d="M 0,-170 C 22,-105 60,-72 120,-56 C 60,-40 22,-8 0,64 C -22,-8 -60,-40 -120,-56 C -60,-72 -22,-105 0,-170 Z" fill="#fff"/>
|
||||
</g>
|
||||
</svg>
|
||||
<h1>Keine Internetverbindung</h1>
|
||||
<p>Der Kundenbereich braucht eine Verbindung. Sobald du wieder online bist, geht es hier weiter.</p>
|
||||
<button type="button" onclick="location.reload()">Erneut versuchen</button>
|
||||
</main>
|
||||
<script>
|
||||
window.addEventListener('online', () => location.reload())
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,65 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
<title>Vertrag widerrufen – WEBklar</title>
|
||||
<link rel="stylesheet" href="/neu/tokens.css">
|
||||
<link rel="stylesheet" href="/neu/oeffentlich/oeffentlich.css">
|
||||
</head>
|
||||
<body class="kn kn-seite kn-recht">
|
||||
<main>
|
||||
<!-- Nur in der installierten App sichtbar: dort gibt es keinen Zurück-Knopf des Browsers -->
|
||||
<a class="kn-zurueck no-print" href="/dashboard.html">‹ Zurück zum Kundenbereich</a>
|
||||
<div class="kn-markenzeile">WEBKLAR</div>
|
||||
<h1>Vertrag widerrufen</h1>
|
||||
|
||||
<form id="decl-form" data-anrede="du" class="kn-formular" novalidate>
|
||||
<!-- § 356a BGB: welche Bestellung widerrufen wird (jede hat ihre eigene Widerrufsfrist) -->
|
||||
<fieldset>
|
||||
<legend data-label-for="order" data-label="Vertrag">Vertrag</legend>
|
||||
<label class="kn-auswahl"><input type="radio" name="order" value="abschluss" data-text="Abschluss meines Abos" checked> Abschluss meines Abos</label>
|
||||
<label class="kn-auswahl"><input type="radio" name="order" value="wechsel" data-text="Wechsel ins Wartungs-Abo"> Wechsel ins Wartungs-Abo</label>
|
||||
<label class="kn-auswahl"><input type="radio" name="order" value="zusatzspeicher" data-text="E-Mail-Zusatzspeicher"> E-Mail-Zusatzspeicher</label>
|
||||
<label class="kn-auswahl"><input type="radio" name="order" value="funktion" data-text="Funktion (z. B. News-Pinnwand)"> Funktion (z. B. News-Pinnwand)</label>
|
||||
<label class="kn-auswahl"><input type="radio" name="order" value="termine" data-text="Terminplaner (Einrichtung)"> Terminplaner (Einrichtung)</label>
|
||||
<label class="kn-auswahl"><input type="radio" name="order" value="shop" data-text="Webshop (Einrichtung)"> Webshop (Einrichtung)</label>
|
||||
<label class="kn-auswahl"><input type="radio" name="order" value="zeiten" data-text="Öffnungszeiten live"> Öffnungszeiten live</label>
|
||||
<label class="kn-auswahl"><input type="radio" name="order" value="projekt" data-text="Projektauftrag (Auftragsbestätigung)"> Projektauftrag (Auftragsbestätigung)</label>
|
||||
</fieldset>
|
||||
|
||||
<!-- Nur bei „Projektauftrag“ und freiwillig (auftrag-widerruf.js blendet das Feld sonst aus und leert es); ohne das Skript bleibt es sichtbar -->
|
||||
<div id="ab-nummer-wrap">
|
||||
<label for="abNummer" data-label-for="abNummer" data-label="Nummer der Auftragsbestätigung">Nummer der Auftragsbestätigung <small>(falls zur Hand, zum Beispiel AB-2026-0001)</small></label>
|
||||
<input id="abNummer" name="abNummer" maxlength="20" autocomplete="off" autocapitalize="characters" spellcheck="false" placeholder="AB-2026-0001">
|
||||
</div>
|
||||
|
||||
<label for="name" data-label-for="name" data-label="Name">Name</label>
|
||||
<input id="name" name="name" autocomplete="name" required maxlength="200">
|
||||
|
||||
<label for="customerNumber" data-label-for="customerNumber" data-label="Kundennummer">Kundennummer <small>(optional)</small></label>
|
||||
<input id="customerNumber" name="customerNumber" maxlength="50">
|
||||
|
||||
<label for="email" data-label-for="email" data-label="E-Mail für die Eingangsbestätigung">E-Mail-Adresse <small>(dorthin schicken wir die Eingangsbestätigung)</small></label>
|
||||
<input id="email" name="email" type="email" autocomplete="email" required maxlength="255">
|
||||
|
||||
<p class="kn-fehlertext hidden" id="form-error" role="alert"></p>
|
||||
<button type="submit" class="kn-absenden">Widerruf bestätigen</button>
|
||||
</form>
|
||||
|
||||
<div id="done" class="kn-bestaetigung hidden">
|
||||
<h2>Dein Widerruf ist bei uns eingegangen</h2>
|
||||
<p>Eingang: <strong id="done-at"></strong></p>
|
||||
<dl id="done-list"></dl>
|
||||
<p>Eine Eingangsbestätigung mit diesen Angaben haben wir an deine E-Mail-Adresse geschickt.</p>
|
||||
<button type="button" class="kn-drucken" id="print-btn">Diese Bestätigung drucken oder als PDF speichern</button>
|
||||
</div>
|
||||
</main>
|
||||
<script src="/legal-form.js"></script>
|
||||
<script>
|
||||
initLegalForm({ endpoint: '/api/abo/public/withdraw', fields: ['order', 'abNummer', 'name', 'customerNumber', 'email'] })
|
||||
</script>
|
||||
<script type="module" src="/auftrag-widerruf.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,37 @@
|
||||
/* Kundenansicht neu (P2): Zeile "Rechnung offen" für Website und Funktionen. Offen heißt wie überall Number(balance) > 0 (V3).
|
||||
Fällt der Abruf aus, fehlt nur diese Zeile. */
|
||||
|
||||
import { api } from './api.js'
|
||||
import { h } from './dom.js'
|
||||
import { fmtEuro, sichereAdresse, zuAnsicht } from './ansicht-hilfen.js'
|
||||
|
||||
/** Rechnungen mit offenem Betrag, in der Reihenfolge der Antwort von GET /api/invoices. */
|
||||
export function offeneRechnungen(antwort) {
|
||||
const liste = Array.isArray(antwort?.invoices) ? antwort.invoices : []
|
||||
return liste.filter((r) => Number(r?.balance) > 0)
|
||||
}
|
||||
|
||||
/** Lädt GET /api/invoices und liefert die offenen Rechnungen; jeder Fehler ergibt eine leere Liste. */
|
||||
export async function ladeOffeneRechnungen(apiFn = api) {
|
||||
try {
|
||||
return offeneRechnungen(await apiFn('/api/invoices'))
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/** Die Zeile für die offenen Rechnungen; ohne offene Rechnung null. Eine Rechnung mit sicherem Link führt dorthin, sonst zu den Rechnungen. */
|
||||
export function rechnungOffenZeile(offen) {
|
||||
if (!Array.isArray(offen) || !offen.length) return null
|
||||
const summe = Math.round(offen.reduce((s, r) => s + (Number(r.balance) || 0), 0) * 100) / 100
|
||||
const einzel = offen.length === 1 ? offen[0] : null
|
||||
const url = einzel ? sichereAdresse(einzel.portalUrl) : ''
|
||||
const link = url
|
||||
? h('a', { href: url, target: '_blank', rel: 'noopener noreferrer' }, 'Zahlen')
|
||||
: h('a', zuAnsicht('invoices'), 'Zahlen')
|
||||
const teile = einzel
|
||||
? ['Rechnung offen', einzel.number, fmtEuro(summe)]
|
||||
: [`${offen.length} Rechnungen offen`, fmtEuro(summe)]
|
||||
// Der Trenner vor dem Link ist ein eigenes Element: am Handy steht das „Zahlen“ rechts in der Zeile ohne hängenden Punkt (textContent bleibt gleich)
|
||||
return h('p', { class: 'kn-rechnung-offen' }, h('span', { class: 'kn-rechnung-text' }, teile.filter(Boolean).join(' · ')), h('span', { class: 'kn-trenner' }, ' · '), link)
|
||||
}
|
||||
@@ -0,0 +1,465 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="robots" content="noindex">
|
||||
<title>Vertragsbedingungen – WEBklar-Abos</title>
|
||||
<style>
|
||||
:root { color-scheme: light; }
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; background: #fff; color: #1a1f36; font: 16px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; font-variant-numeric: tabular-nums; }
|
||||
main { max-width: 780px; margin: 0 auto; padding: 44px 20px 80px; }
|
||||
.brand { font-weight: 600; letter-spacing: 0.12em; font-size: 13px; color: #111; }
|
||||
h1 { font-size: 30px; line-height: 1.2; margin: 6px 0 6px; font-weight: 600; }
|
||||
h2 { font-size: 20px; margin: 40px 0 10px; padding-top: 14px; border-top: 1px solid #ebeef1; font-weight: 600; }
|
||||
h3 { font-size: 16.5px; margin: 26px 0 6px; font-weight: 600; }
|
||||
p, li { margin: 6px 0; }
|
||||
ol, ul { padding-left: 24px; }
|
||||
ol.abs > li { padding-left: 4px; }
|
||||
ol.abs > li::marker { content: "(" counter(list-item) ") "; font-weight: 600; }
|
||||
.meta { color: #5e6678; font-size: 14px; }
|
||||
.box { border-top: 1px solid #d8dde3; border-bottom: 1px solid #d8dde3; padding: 14px 0; margin: 20px 0; }
|
||||
.box > strong:first-child { display: block; margin-bottom: 6px; }
|
||||
table { border-collapse: collapse; width: 100%; font-size: 15px; }
|
||||
td, th { padding: 6px 10px 6px 0; border-bottom: 1px solid #ebeef1; vertical-align: top; text-align: left; }
|
||||
th { font-size: 13px; text-transform: uppercase; letter-spacing: 0.04em; color: #5e6678; background: #f8f9fb; padding-left: 8px; }
|
||||
a { color: #111; text-decoration: underline; text-decoration-color: #d8dde3; text-underline-offset: 2px; }
|
||||
a:hover { background: #f6f8fa; }
|
||||
nav.top { font-size: 14px; margin-bottom: 22px; }
|
||||
.toc { columns: 2; column-gap: 28px; font-size: 14.5px; }
|
||||
.toc a { text-decoration: none; }
|
||||
.toc li { break-inside: avoid; }
|
||||
.anlage-kopf { font-size: 13px; letter-spacing: 0.08em; text-transform: uppercase; color: #5e6678; margin-top: 44px; }
|
||||
.anlage-kopf + h2 { margin-top: 4px; border-top: none; padding-top: 0; }
|
||||
.hinweis { font-size: 14px; color: #5e6678; }
|
||||
.offen { display: inline-block; background: #fcedb9; color: #a82c00; padding: 1px 10px; border-radius: 6px; font-weight: 600; }
|
||||
@media (max-width: 640px) { .toc { columns: 1; } h1 { font-size: 25px; } }
|
||||
@media print {
|
||||
@page { size: A4; margin: 18mm 17mm 18mm;
|
||||
@top-left { content: "WEBklar-Abos · Vertragsunterlagen"; font: 8pt sans-serif; color: #8a93a3; }
|
||||
@top-right { content: "Version 6 · Stand 16.10.2026"; font: 8pt sans-serif; color: #8a93a3; }
|
||||
@bottom-right { content: "Seite " counter(page) " von " counter(pages); font: 8pt sans-serif; color: #8a93a3; } }
|
||||
body { background: #fff; font-size: 10pt; line-height: 1.45; }
|
||||
main { max-width: none; padding: 0; }
|
||||
nav.top, .screen-only { display: none; }
|
||||
h1 { font-size: 20pt; }
|
||||
h2 { font-size: 13pt; margin-top: 16pt; break-after: avoid; }
|
||||
h3 { font-size: 11pt; break-after: avoid; }
|
||||
.box { padding: 8pt 0; }
|
||||
.neue-seite { break-before: page; }
|
||||
a { color: inherit; text-decoration: none; }
|
||||
.box, tr, ol.abs > li { break-inside: avoid; }
|
||||
li, p { orphans: 3; widows: 3; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<nav class="top"><a href="/dashboard.html">← Zurück zum Kundenportal</a> · <a href="/kuendigen.html">Verträge hier kündigen</a> · <a href="/widerrufen.html">Vertrag widerrufen</a> · <a href="/datenschutz.html">Datenschutz</a></nav>
|
||||
|
||||
<div class="brand">WEBKLAR</div>
|
||||
<h1>Vertragsbedingungen für die WEBklar-Abos</h1>
|
||||
<p class="meta">Hosting-Abo und Wartungs-Abo · Vertragsbedingungen mit Leistungsbeschreibungen, Widerrufsbelehrung und Auftragsverarbeitungsvertrag · Version 6 · Stand 16.10.2026</p>
|
||||
<p class="hinweis offen">Entwurf – Prüfung durch Kanzlei offen</p>
|
||||
<p class="screen-only meta"><a href="/neu/rechtliches/WEBklar-Abo-Vertragsunterlagen.pdf">Als PDF herunterladen und ausdrucken</a></p>
|
||||
|
||||
<div class="box">
|
||||
<strong>Das Wichtigste auf einen Blick</strong>
|
||||
<table>
|
||||
<tr><td>Anbieter</td><td>WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, vertreten durch die Gesellschafter Kenso Grimm und Justin Klein (jeder einzeln vertretungsberechtigt) · Telefon 0176 23726355 · E-Mail support@webklar.com</td></tr>
|
||||
<tr><td>Hosting-Abo</td><td>20,00 € pro Monat · Bereitstellung Ihrer statischen Website auf unserem Server in Deutschland · 1 Domain · 1 E-Mail-Postfach mit 5 GB · Änderungen an der Website nur nach Ihrer Freigabe, 100 € pro Stunde (Leistungsbeschreibung A)</td></tr>
|
||||
<tr><td>Wartungs-Abo</td><td>50,00 € pro Monat · Betrieb und Pflege Ihrer Website bzw. Web-Anwendung auf unserem Server in Deutschland · 1 Domain · beliebig viele E-Mail-Postfächer mit zusammen 5 GB · 1 Stunde Arbeitszeit pro Monat, nicht genutzte Zeit bleibt 12 Monate verfügbar; Mehraufwand nur nach Ihrer Freigabe, 100 € pro Stunde (Leistungsbeschreibung B)</td></tr>
|
||||
<tr><td>Funktionen</td><td>Das Hosting-Abo enthält die Website-Analyse, das Wartungs-Abo zusätzlich News-Pinnwand, Terminplaner, Webshop, Buchungskalender und Öffnungszeiten live. Das Einschalten kostet nichts extra; die Einrichtung geht vom Stundenkontingent ab (§ 4 Abs. 8).</td></tr>
|
||||
<tr><td>E-Mail-Zusatzspeicher</td><td>In beiden Abos: je Paket von 5 GB 1,00 € pro Monat, bis zu 10 Pakete. Im Kundenportal bestellbar und reduzierbar nach Leistungsbeginn, solange das Abo nicht gekündigt ist und keine Zahlung offen ist; ganz oder teilweise kündbar zum Ende des Abrechnungsmonats.</td></tr>
|
||||
<tr><td>Preise</td><td>Alle Preise sind Endpreise, zahlbar monatlich im Voraus. Gemäß § 19 UStG wird keine Umsatzsteuer berechnet.</td></tr>
|
||||
<tr><td>Zahlung</td><td>Kreditkarte oder SEPA-Lastschrift über Stripe</td></tr>
|
||||
<tr><td>Laufzeit</td><td>Mindestlaufzeit 12 Monate ab Leistungsbeginn, kündbar zum Ende der Mindestlaufzeit bis einen Monat vorher; danach unbefristet und jederzeit zum Ende des Abrechnungsmonats kündbar. Bei Geschäftsaufgabe Sonderkündigung mit einem Monat Frist.</td></tr>
|
||||
<tr><td>Wechsel</td><td>Vom Hosting- ins Wartungs-Abo im Kundenportal nach Leistungsbeginn, auch während der Mindestlaufzeit, solange das Hosting-Abo nicht gekündigt ist und keine Zahlung offen ist; der Wechsel steht unter der Bedingung, dass die Wechselrechnung bezahlt wird (§ 9 Abs. 3). Mit dem Wechseltag beginnt eine neue Mindestlaufzeit von 12 Monaten. Vom Wartungs- ins Hosting-Abo auf Anfrage nach Ablauf der Mindestlaufzeit.</td></tr>
|
||||
<tr><td>Kündigen</td><td>project.webklar.com/kuendigen.html (ohne Anmeldung; Auswahl: das Abo oder nur der E-Mail-Zusatzspeicher, ganz oder mit der Zahl der Pakete) oder per E-Mail</td></tr>
|
||||
<tr><td>Widerruf</td><td>Verbraucher können jede Bestellung (Abo, Wechsel, Zusatzspeicher) innerhalb von 14 Tagen ab Vertragsschluss widerrufen: project.webklar.com/widerrufen.html (siehe Widerrufsbelehrung und Hinweise zum Widerruf)</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<strong>Inhalt</strong>
|
||||
<ol class="toc">
|
||||
<li><a href="#p1">Anbieter und Geltungsbereich</a></li>
|
||||
<li><a href="#p2">Vertragsschluss und Vertragstext</a></li>
|
||||
<li><a href="#p3">Leistungen</a></li>
|
||||
<li><a href="#p4">Arbeitszeit und Stundenkontingent</a></li>
|
||||
<li><a href="#p5">Verfügbarkeit und Wartung</a></li>
|
||||
<li><a href="#p6">Pflichten des Kunden</a></li>
|
||||
<li><a href="#p7">Preise und Zahlung</a></li>
|
||||
<li><a href="#p8">Laufzeit und Kündigung</a></li>
|
||||
<li><a href="#wechsel">Wechsel zwischen den Abos</a></li>
|
||||
<li><a href="#zusatzspeicher">E-Mail-Zusatzspeicher</a></li>
|
||||
<li><a href="#p11">Vertragsende</a></li>
|
||||
<li><a href="#p12">Mängel</a></li>
|
||||
<li><a href="#p13">Haftung</a></li>
|
||||
<li><a href="#p14">Datenschutz</a></li>
|
||||
<li><a href="#p15">Änderungen dieser Bedingungen</a></li>
|
||||
<li><a href="#p16">Schlussbestimmungen</a></li>
|
||||
</ol>
|
||||
<p class="meta" style="margin-bottom:0">Außerdem: <a href="#hosting">Leistungsbeschreibung A: Hosting-Abo</a> · <a href="#wartung">Leistungsbeschreibung B: Wartungs-Abo</a> · <a href="#widerruf">Widerrufsbelehrung und Muster-Widerrufsformular</a> · <a href="#widerruf-hinweise">Hinweise zum Widerruf</a> · <a href="#avv">Anlage 1: Auftragsverarbeitungsvertrag</a> · <a href="#tom">Anlage 2: Technische und organisatorische Maßnahmen</a> · <a href="#sub">Anlage 3: Unterauftragsverarbeiter</a></p>
|
||||
</div>
|
||||
|
||||
<h2 id="p1">§ 1 Anbieter und Geltungsbereich</h2>
|
||||
<ol class="abs">
|
||||
<li>Diese Bedingungen regeln das Hosting-Abo und das Wartungs-Abo (zusammen „Abos“) zwischen der WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler („WEBklar“ oder „wir“), und dem Kunden. Welches Abo der Kunde bestellt hat, ergibt sich aus seiner Bestellung und unserer Bestätigungs-E-Mail. Den Leistungsumfang beschreiben Leistungsbeschreibung A (Hosting-Abo) und Leistungsbeschreibung B (Wartungs-Abo); sie sind Teil dieser Bedingungen. Zu beiden Abos kann der Kunde E-Mail-Zusatzspeicher buchen (§ 10).</li>
|
||||
<li>Sie gelten für Verbraucher (§ 13 BGB) und Unternehmer (§ 14 BGB). Gilt eine Regel nur für eine der beiden Gruppen, ist das ausdrücklich angegeben. Die Bestellseiten richten sich nach der Kundenart, die wir im Kundenkonto hinterlegt haben („Betrieb“ oder „privat“). Wir hinterlegen sie nach den Angaben des Kunden; ist keine hinterlegt, gilt der Kunde als privat. Ob der Kunde Verbraucher oder Unternehmer ist, richtet sich nach dem Zweck, zu dem er den Vertrag abschließt, nicht nach dieser Angabe.</li>
|
||||
<li>Für die Abos gehen diese Bedingungen den allgemeinen Geschäftsbedingungen von WEBklar (webklar.com/agb) vor. Die dortige Regelung zum jährlichen Hosting (Leistungsmodul C) gilt für die Abos nicht. Die Erstellung einer Website ist nicht Teil der Abos und wird gesondert vereinbart.</li>
|
||||
<li>Abweichende Geschäftsbedingungen des Kunden werden nicht Vertragsbestandteil.</li>
|
||||
</ol>
|
||||
|
||||
<h2 id="p2">§ 2 Vertragsschluss und Vertragstext</h2>
|
||||
<ol class="abs">
|
||||
<li>Die Abos werden im Kundenportal unter project.webklar.com bestellt. Den Zugang zum Kundenportal richten wir für den Kunden ein.</li>
|
||||
<li>Der Abschluss eines Abos läuft so ab:
|
||||
<ol type="a">
|
||||
<li>Der Kunde wählt im Kundenportal im Bereich „Verträge“ bei dem gewünschten Abo „Abo abschließen“.</li>
|
||||
<li>Eine Bestellübersicht zeigt Leistung, Preis, Laufzeit und Zahlungsweise.</li>
|
||||
<li>Direkt über der Schaltfläche steht der Hinweis, dass diese Bedingungen mit den Leistungsbeschreibungen, dem Auftragsverarbeitungsvertrag (Anlage 1) und der Widerrufsbelehrung gelten, jeweils mit Fassung und Link; die Unterlagen erhält der Kunde nach der Bestellung zusätzlich als PDF per E-Mail. Ist die Kundenart „privat“ hinterlegt, kann der Kunde dort durch ein Kästchen verlangen, dass wir sofort mit der Leistung beginnen; ohne das Kästchen beginnen wir erst nach Ablauf der 14-tägigen Widerrufsfrist. Ist die Kundenart „Betrieb“ hinterlegt, beginnen wir mit Vertragsschluss, ohne dass der Kunde etwas markieren muss.</li>
|
||||
<li>Mit dem Klick auf „Zahlungspflichtig bestellen“ gibt er ein verbindliches Angebot ab.</li>
|
||||
<li>Anschließend hinterlegt er beim Zahlungsdienstleister Stripe eine Zahlungsart (Kreditkarte oder SEPA-Lastschrift) und schließt den Vorgang dort ab.</li>
|
||||
</ol></li>
|
||||
<li>Der Vertrag kommt zustande, sobald der Kunde den Vorgang bei Stripe abgeschlossen hat. Bei sofortigem Beginn setzt das die erfolgreiche Zahlung der ersten Monatsgebühr voraus; scheitert sie, kommt kein Vertrag zustande. Wir bestätigen den Vertragsschluss unverzüglich per E-Mail und senden diese Bedingungen mit Widerrufsbelehrung als PDF mit.</li>
|
||||
<li>Bis zum Abschluss des Vorgangs bei Stripe kann der Kunde jederzeit abbrechen und seine Angaben im Kundenportal korrigieren.</li>
|
||||
<li>Der Wechsel vom Hosting- ins Wartungs-Abo (§ 9) und die Bestellung von E-Mail-Zusatzspeicher (§ 10) sind eigene Bestellungen. Der Kunde gibt sie im Kundenportal im Bereich „Verträge“ auf. Direkt über der Schaltfläche zeigt eine Übersicht die Leistung, den Preis pro Monat, beim Wechsel den sofort fälligen Betrag, beim Zusatzspeicher den anteiligen Betrag, der mit der nächsten Abbuchung eingezogen wird, den Tag der nächsten Abbuchung sowie Laufzeit und Kündigungsmöglichkeit. Mit dem Klick auf „Zahlungspflichtig bestellen“ kommt der Vertrag zustande; beim Wechsel steht er unter der Bedingung des § 9 Abs. 3 (Zahlung der Wechselrechnung). Bezahlt wird mit der bei Stripe hinterlegten Zahlungsart; ein neuer Vorgang bei Stripe ist nicht nötig. Verlangt die Bank des Kunden eine Bestätigung der Zahlung, leitet das Kundenportal ihn dafür auf die Zahlungsseite von Stripe weiter. Bis zum Klick kann der Kunde abbrechen und seine Angaben korrigieren. Wir bestätigen die Bestellung unverzüglich per E-Mail und senden diese Bedingungen mit Widerrufsbelehrung als PDF mit.</li>
|
||||
<li>Vertragssprache ist Deutsch. Wir speichern den Vertragstext sowie Zeitpunkt, Version und bestelltes Abo jeder Zustimmung des Kunden. Wir speichern außerdem den Wortlaut der angezeigten Hinweise und Kästchen, die hinterlegte Kundenart sowie Zeit, IP-Adresse und Browserkennung des Klicks. Die aktuelle Fassung dieser Bedingungen ist unter project.webklar.com/abo-bedingungen.html abrufbar. Die bei Vertragsschluss gültige Fassung erhält der Kunde mit der Bestätigungs-E-Mail; er kann sie außerdem jederzeit im Kundenportal unter „Verträge“ abrufen.</li>
|
||||
</ol>
|
||||
|
||||
<h2 id="p3">§ 3 Leistungen</h2>
|
||||
<h3>(1) Leistungsbeschreibungen</h3>
|
||||
<p>Den Umfang des Hosting-Abos beschreibt Leistungsbeschreibung A, den des Wartungs-Abos Leistungsbeschreibung B. Beide Abos umfassen den Betrieb der Website auf unserem Server, eine Domain, E-Mail-Postfächer, die tägliche Datensicherung und die Servicezeiten nach den Absätzen 2 bis 6. Das Wartungs-Abo umfasst zusätzlich die Pflege der Website und ein Stundenkontingent (§ 4). Das Hosting-Abo ist nur für statische Websites bestimmt (Leistungsbeschreibung A Nr. 2).</p>
|
||||
<h3>(2) Betrieb der Website</h3>
|
||||
<ul>
|
||||
<li>Wir stellen die Website bzw. Web-Anwendung, die wir für den Kunden erstellt oder zur Betreuung übernommen haben, auf einem von uns betriebenen Server bereit. Der Server steht im Rechenzentrum der Hetzner Online GmbH (im Folgenden „Hetzner“) in Falkenstein (Deutschland).</li>
|
||||
<li>Speicherplatz und Datenverkehr stehen im Umfang einer üblichen Firmen-Website zur Verfügung. Ungewöhnlich hohe Last, etwa durch große Downloads oder Streaming, ist nicht umfasst.</li>
|
||||
<li>Wir spielen Sicherheitsupdates für Betriebssystem und Server-Software ein, kritische Sicherheitsupdates in der Regel innerhalb von 3 Werktagen nach ihrer Veröffentlichung, übrige Updates regelmäßig. Aktualisierungen, die nötig sind, damit unsere Leistungen vertragsgemäß bleiben, stellen wir während der gesamten Vertragslaufzeit bereit. Updates der Software-Komponenten der Website selbst gehören nur zum Wartungs-Abo (Leistungsbeschreibung B Nr. 2).</li>
|
||||
<li>Wir richten ein SSL/TLS-Zertifikat (HTTPS) ein und verlängern es.</li>
|
||||
<li>Wir überwachen die Erreichbarkeit automatisch und beheben Störungen in unserem Verantwortungsbereich (§ 5).</li>
|
||||
</ul>
|
||||
<h3>(3) Domain</h3>
|
||||
<ul>
|
||||
<li>Enthalten ist eine Domain mit einer der Endungen .de, .com, .eu, .net oder .org: Registrierung oder Übernahme einer bestehenden Domain, Verlängerung und DNS-Verwaltung. Premium-Domains und andere Endungen nur nach gesonderter Vereinbarung.</li>
|
||||
<li>Inhaber der Domain wird der Kunde, wir verwalten sie technisch. Den Umzugscode (AuthCode) erhält der Kunde jederzeit auf Anfrage.</li>
|
||||
<li>Für die Wahl der Domain und dafür, dass sie keine Rechte Dritter verletzt, ist der Kunde verantwortlich.</li>
|
||||
</ul>
|
||||
<h3>(4) E-Mail-Postfächer</h3>
|
||||
<ul>
|
||||
<li>Wir richten E-Mail-Postfächer unter der Domain des Kunden ein, mit verschlüsseltem Zugang per IMAP und SMTP sowie über Webmail, mit Spam- und Virenfilter. Wir stellen sie über Hetzner (Rechenzentrum Nürnberg, Deutschland) bereit. Das Hosting-Abo umfasst ein Postfach, das Wartungs-Abo beliebig viele Postfächer.</li>
|
||||
<li>Für die Postfächer des Kunden stehen zusammen 5 GB Speicher zur Verfügung, zuzüglich gebuchtem Zusatzspeicher (§ 10). Hetzner setzt für jedes Postfach eine Speichergrenze. Die Grenzen aller Postfächer des Kunden ergeben zusammen höchstens den verfügbaren Speicher. Wie der Speicher auf die Postfächer verteilt wird, bestimmt der Kunde; ohne Vorgabe teilen wir ihn gleichmäßig auf.</li>
|
||||
<li>Ist die Speichergrenze eines Postfachs erreicht, nimmt es keine neuen E-Mails mehr an, bis wieder Platz ist, etwa durch Löschen von E-Mails, eine andere Aufteilung oder Zusatzspeicher.</li>
|
||||
<li>Das Anlegen und Löschen von Postfächern, neue Passwörter und eine andere Aufteilung des Speichers beauftragt der Kunde über den Chat im Kundenportal (Schaltfläche „Postfach anfragen“) oder per E-Mail. Wir setzen das in der Regel innerhalb eines Werktags um. Diese Arbeiten sind im Abo enthalten.</li>
|
||||
<li>Zugangsdaten zu den Postfächern geben wir nur an den Kunden oder an Personen, die er uns in Textform benennt.</li>
|
||||
<li>Die Postfächer dürfen nur der Kunde, seine Beschäftigten und andere für ihn tätige Personen für seine Organisation nutzen. Nicht erlaubt sind die Überlassung an Dritte, auch unentgeltlich, sowie Newsletter- und Massenversand. Es gelten die Versandgrenzen von Hetzner.</li>
|
||||
<li>Die Inhalte der Postfächer sichert Hetzner nach seinen Bedingungen, nicht wir selbst. Wichtige E-Mails sollte der Kunde zusätzlich bei sich aufbewahren.</li>
|
||||
<li>Die Zustellung von E-Mails hängt auch von Dritten ab, etwa von Empfangsservern und Spamfiltern, und kann nicht garantiert werden.</li>
|
||||
</ul>
|
||||
<h3>(5) Datensicherung</h3>
|
||||
<ul>
|
||||
<li>Der Server wird täglich automatisch vollständig gesichert (Backup-Funktion von Hetzner). Die Sicherungen liegen getrennt vom Server in einem anderen Brandabschnitt am selben Rechenzentrumsstandort in Deutschland. Verfügbar sind jeweils die letzten sieben Tagessicherungen.</li>
|
||||
<li>Datenbanken einer Web-Anwendung exportieren wir zusätzlich täglich vor der Serversicherung, damit sie sich in einem einheitlichen Stand wiederherstellen lassen.</li>
|
||||
<li>Eine Wiederherstellung nach technischen Störungen oder nach Fehlern, die wir zu vertreten haben, ist inklusive. Wünscht der Kunde eine Wiederherstellung aus anderen Gründen, etwa nach eigenen Änderungen, wird sie als Arbeitszeit nach § 4 abgerechnet.</li>
|
||||
<li>Die Sicherung dient der Wiederherstellung des Betriebs. Ein Archiv oder die Herausgabe von Sicherungskopien ist nicht geschuldet.</li>
|
||||
</ul>
|
||||
<h3>(6) Servicezeiten und Reaktion</h3>
|
||||
<ul>
|
||||
<li>Servicezeiten sind Montag bis Freitag von 9 bis 17 Uhr, ausgenommen gesetzliche Feiertage in Rheinland-Pfalz. Anfragen außerhalb der Servicezeiten gelten zu Beginn der nächsten Servicezeit als eingegangen.</li>
|
||||
<li>Auf Anfragen und Störungsmeldungen antworten wir spätestens bis zum Ende des nächsten Werktags. Gemeint ist eine erste fachliche Rückmeldung, nicht zwingend schon die Behebung.</li>
|
||||
<li>Fällt die Website oder ein Postfach vollständig aus, beginnen wir innerhalb der Servicezeiten unverzüglich mit der Behebung. Unser Ziel ist eine Behebung innerhalb von 4 Stunden. Eine Garantie ist damit nicht verbunden.</li>
|
||||
<li>Beauftragte Änderungen setzen wir in der Regel innerhalb von 5 Werktagen um.</li>
|
||||
<li>Betriebsferien kündigen wir mindestens zwei Wochen vorher per E-Mail an.</li>
|
||||
</ul>
|
||||
<h3>(7) Technische Änderungen</h3>
|
||||
<p>Wir dürfen die technische Umsetzung ändern, etwa das Rechenzentrum, die Server-Software oder den Anbieter der E-Mail-Postfächer wechseln. Voraussetzung ist, dass der vereinbarte Leistungsumfang nicht schlechter wird, dem Kunden keine Kosten entstehen und wir ihn mindestens vier Wochen vorher in Textform informieren. Beeinträchtigt eine Änderung den Zugang zur Leistung oder ihre Nutzbarkeit mehr als nur unerheblich, kann der Kunde den Vertrag innerhalb von 30 Tagen nach der Information bzw. der Änderung unentgeltlich beenden (§ 327r BGB).</p>
|
||||
|
||||
<h3>(8) Funktionen</h3>
|
||||
<p>Zum Abo gehören Funktionen, die der Kunde im Kundenportal unter „Funktionen“ einschaltet. Das Hosting-Abo enthält die Website-Analyse. Das Wartungs-Abo enthält News-Pinnwand, Terminplaner, Webshop, Buchungskalender, Öffnungszeiten live und Website-Analyse. Umfang, Voraussetzungen und Pflichten jeder Funktion beschreiben ihre Leistungsbeschreibung und, soweit die Funktion personenbezogene Daten im Auftrag des Kunden verarbeitet, ihr Auftragsverarbeitungsvertrag, jeweils in der Fassung, die das Kundenportal beim Einschalten anzeigt. Eine Funktion kostet keine zusätzliche Gebühr; die Einrichtung richtet sich nach § 4 Abs. 8 und 9.</p>
|
||||
<p>Vorbehaltlich § 11 Abs. 4 läuft eine Funktion nur, solange das Abo läuft, das sie enthält. Das gilt auch für eine Funktion, die wir auf Wunsch des Kunden schon bei der Erstellung seiner Website eingebaut haben. Funktionen, die der Kunde vor Veröffentlichung dieser Fassung einzeln gekauft hat, laufen nach den Bedingungen weiter, unter denen er sie gekauft hat.</p>
|
||||
|
||||
<h2 id="p4">§ 4 Arbeitszeit und Stundenkontingent</h2>
|
||||
<ol class="abs">
|
||||
<li>Im Wartungs-Abo gehört zum Leistungsumfang ein Kontingent von 60 Minuten Arbeitszeit je bezahltem Abrechnungsmonat. Das erste Kontingent steht mit der ersten Zahlung bereit, nach einem Wechsel aus dem Hosting-Abo mit der Zahlung der Wechselrechnung (§ 9 Abs. 3). Das Kontingent ist Teil der Leistungsbeschreibung und kein Geldguthaben.</li>
|
||||
<li>Nicht genutzte Zeit bleibt verfügbar: Jedes Monatskontingent kann zwölf Monate lang ab seiner Bereitstellung genutzt werden und entfällt danach. Genutzt wird immer das älteste noch verfügbare Kontingent zuerst. Mit dem Ende des Wartungs-Abos, auch durch einen Wechsel ins Hosting-Abo, entfällt nicht genutzte Zeit. Sie wird weder ausgezahlt noch erstattet und ist nicht übertragbar. Einen Vorschuss nach Absatz 8 gleichen die folgenden Kontingente zuerst aus.</li>
|
||||
<li>Das Kontingent gilt für Änderungen, Erweiterungen, Beratung und Unterstützung rund um die im Abo betreute Website, Domain und E-Mail. Aufträge erteilt der Kunde über den Chat im Kundenportal oder per E-Mail. Aufträge von mehr als 4 Stunden in einem Monat setzen wir nach Terminabsprache um.</li>
|
||||
<li>Angerechnet wird die tatsächlich aufgewendete Zeit je Auftrag, aufgerundet auf volle 15 Minuten. Dazu zählen die Umsetzung und die Abstimmung zum Auftrag, soweit sie länger als 10 Minuten dauert. Nicht angerechnet werden die laufenden Leistungen nach § 3 Abs. 2 bis 5 und die Sicherheitsupdates nach Leistungsbeschreibung B Nr. 2 (Betrieb, Updates, Überwachung, Sicherung, Verwaltung von Domain und Postfächern), die Behebung von Störungen (§ 3 Abs. 6, § 5) sowie die Behebung von Fehlern, die wir zu vertreten haben. Wirkt an einem Arbeitsschritt eine weitere Person unterstützend mit, etwa bei Gestaltung, Abstimmung oder Prüfung, rechnen wir für sie die Hälfte der Arbeitszeit dieses Arbeitsschritts an, aufgerundet auf volle 15 Minuten; bei mehreren unterstützenden Personen gilt das für jede von ihnen. Im Kundenportal weisen wir jeden Arbeitsschritt mit seiner Arbeitszeit und jede Unterstützung mit Person, Tätigkeit und angerechneter Zeit einzeln aus.</li>
|
||||
<li>Verfügbare Zeit, Bereitstellungen, Verbrauch und Ablaufdaten sieht der Kunde jederzeit im Kundenportal.</li>
|
||||
<li>Reicht die verfügbare Zeit für einen Auftrag voraussichtlich nicht aus, nennen wir dem Kunden vorher den geschätzten Mehraufwand. Wir führen ihn nur nach seiner Freigabe aus, etwa im Chat oder per E-Mail, und berechnen 100 € pro Stunde (Endpreis, gemäß § 19 UStG ohne Umsatzsteuer), abgerechnet in Schritten von 15 Minuten. Aufträge mit einem Umfang von mehr als 10 Stunden bieten wir gesondert an.</li>
|
||||
<li>Im Hosting-Abo gibt es kein Stundenkontingent. Aufträge, etwa Änderungen an der Website, führen wir nur nach Freigabe des Kunden aus. Vorher nennen wir ihm den geschätzten Aufwand. Wir berechnen 100 € pro Stunde (Endpreis, gemäß § 19 UStG ohne Umsatzsteuer), abgerechnet in Schritten von 15 Minuten; Absatz 4 Satz 1, 2 und 4 gelten entsprechend. Aufträge mit einem Umfang von mehr als 10 Stunden bieten wir gesondert an. Die Behebung von Störungen an Server, Domain und Postfach in unserem Verantwortungsbereich ist auch im Hosting-Abo inklusive.</li>
|
||||
<li>Die Einrichtung einer Funktion rechnen wir als Arbeitszeit auf das Stundenkontingent an. Angerechnet wird die tatsächlich aufgewendete Zeit nach Absatz 4. Reicht das verfügbare Kontingent nicht aus, führen wir die Einrichtung trotzdem ohne weitere Rückfrage aus. Die fehlende Zeit ist ein Vorschuss, den die folgenden Monatskontingente ausgleichen, sobald sie bereitstehen. Dafür berechnen wir nichts, weder als Mehraufwand nach Absatz 6 noch in einer gesonderten Rechnung. Endet das Wartungs-Abo, solange noch ein Vorschuss offen ist, verfällt der Rest; der Kunde schuldet ihn nicht.</li>
|
||||
<li>Im Hosting-Abo ist die Einrichtung der Website-Analyse für den Kunden kostenlos; ein Stundenkontingent gibt es dort nicht (Absatz 7). Lässt der Kunde eine Funktion auf seinen Wunsch schon bei der Erstellung seiner Website einbauen, rechnen wir die Einrichtung nicht über das Stundenkontingent ab, sondern über die Kosten des Projekts, wie wir es mit ihm vereinbart haben.</li>
|
||||
</ol>
|
||||
|
||||
<h2 id="p5">§ 5 Verfügbarkeit und Wartung</h2>
|
||||
<ol class="abs">
|
||||
<li>Wir streben eine Erreichbarkeit der Website von 99 % im Jahresmittel an, gemessen am Übergang des Rechenzentrums ins Internet. Eine ununterbrochene Erreichbarkeit ist technisch nicht möglich und wird nicht geschuldet.</li>
|
||||
<li>Nicht eingerechnet werden:
|
||||
<ul>
|
||||
<li>angekündigte Wartungsarbeiten, möglichst zwischen 22 und 6 Uhr und mindestens 24 Stunden vorher angekündigt; dringende Sicherheitsupdates auch kurzfristig,</li>
|
||||
<li>Störungen außerhalb unseres Einflussbereichs, etwa im Internet, bei der Domain-Vergabestelle oder bei vorgelagerten Netzbetreibern,</li>
|
||||
<li>Angriffe wie DDoS, sofern wir angemessene Schutzmaßnahmen getroffen haben,</li>
|
||||
<li>höhere Gewalt sowie Störungen, die der Kunde verursacht hat.</li>
|
||||
</ul></li>
|
||||
<li>Die gesetzlichen Rechte des Kunden bei Ausfällen, die wir zu vertreten haben, bleiben unberührt. Die Haftung richtet sich nach § 13.</li>
|
||||
</ol>
|
||||
|
||||
<h2 id="p6">§ 6 Pflichten des Kunden</h2>
|
||||
<ol class="abs">
|
||||
<li>Der Kunde hält seine Zugangsdaten zum Kundenportal und zu allen Postfächern geheim und informiert uns unverzüglich, wenn er einen Missbrauch vermutet. Gibt er die Zugangsdaten eines Postfachs an Beschäftigte oder andere für ihn tätige Personen weiter, geschieht das in seiner Verantwortung; er sorgt dafür, dass auch diese Personen die Zugangsdaten geheim halten und die Regeln nach § 3 Abs. 4 einhalten. Ein neues Passwort kann er jederzeit über den Chat im Kundenportal anfordern.</li>
|
||||
<li>Der Kunde stellt nur Inhalte bereit, die er verwenden darf und die nicht gegen Gesetze verstoßen. Das gilt auch für E-Mails, die über seine Postfächer versendet werden. Offensichtlich rechtswidrige Inhalte dürfen wir nach einem Hinweis vorübergehend sperren. Wir informieren den Kunden darüber unverzüglich.</li>
|
||||
<li>Für Impressum, Datenschutzerklärung und Inhalte seiner Website ist der Kunde verantwortlich. Die Angaben zum Hosting, die er für seine Datenschutzerklärung braucht, stehen in den Anlagen 1 bis 3.</li>
|
||||
<li>Der Kunde bewahrt Kopien der Inhalte auf, die er uns übergibt oder selbst einpflegt, und teilt uns Änderungen seiner Kontaktdaten mit.</li>
|
||||
<li>Im Hosting-Abo stellt der Kunde keine Inhalte oder Funktionen bereit, die nach Leistungsbeschreibung A Nr. 2 im Hosting-Abo nicht möglich sind, und beauftragt keine. Braucht seine Website solche Teile, wechselt er vorher ins Wartungs-Abo (§ 9).</li>
|
||||
<li>Schaltet der Kunde die Website-Analyse ein, ist er für die Messung auf seiner Website Verantwortlicher. Er entfernt oder umgeht das Cookie-Banner nicht, bindet ohne Abstimmung mit uns keine weiteren Mess- oder Tracking-Werkzeuge ein und ergänzt seine Datenschutzerklärung um einen Hinweis auf die von uns erzeugte Zusatzseite „/datenschutz-analyse.html“.</li>
|
||||
</ol>
|
||||
|
||||
<h2 id="p7">§ 7 Preise und Zahlung</h2>
|
||||
<ol class="abs">
|
||||
<li>Die Monatsgebühr beträgt im Hosting-Abo 20,00 € und im Wartungs-Abo 50,00 €. E-Mail-Zusatzspeicher kostet je Paket von 5 GB 1,00 € pro Monat (§ 10). Alle Preise sind Endpreise. Als Kleinunternehmer im Sinne von § 19 UStG berechnen wir keine Umsatzsteuer.</li>
|
||||
<li>Die Monatsgebühr ist monatlich im Voraus fällig, erstmals mit Beginn der Leistung (bei sofortigem Beginn mit Vertragsschluss, sonst nach Ablauf der Widerrufsfrist), danach jeweils am selben Kalendertag der Folgemonate (Abrechnungsmonat). Gebuchter Zusatzspeicher wird zusammen mit der Monatsgebühr abgebucht. Die Gebühren werden per Kreditkarte oder SEPA-Lastschrift über die Stripe Payments Europe, Limited (Irland) eingezogen. Rechnungen erhält der Kunde per E-Mail und im Kundenportal über „Abo verwalten & Rechnungen“.</li>
|
||||
<li>Bestellt der Kunde Zusatzspeicher, berechnen wir für den Rest des laufenden Abrechnungsmonats einen anteiligen Betrag. Er wird in der Bestellübersicht angezeigt und mit der nächsten Abbuchung eingezogen. Ab dem folgenden Abrechnungsmonat gilt der volle Monatspreis. Pakete, die für den laufenden Abrechnungsmonat schon bezahlt sind, berechnen wir nicht noch einmal. Verringert der Kunde den Zusatzspeicher, wirkt das zum Ende des laufenden Abrechnungsmonats; die Gebühr für den laufenden Monat wird nicht anteilig erstattet.</li>
|
||||
<li>Beim Wechsel vom Hosting- ins Wartungs-Abo (§ 9) wird die Monatsgebühr des Wartungs-Abos am Wechseltag fällig, zusammen mit gebuchtem Zusatzspeicher. Die im Voraus bezahlten Gebühren für die nicht genutzten Tage des laufenden Abrechnungsmonats schreiben wir auf dieser Rechnung gut. Die folgenden Abrechnungsmonate beginnen jeweils am Kalendertag des Wechsels. Den fälligen Betrag und den neuen Abbuchungstag zeigt die Bestellübersicht vor der Bestellung. Wird die Wechselrechnung nicht bezahlt, gilt § 9 Abs. 3.</li>
|
||||
<li>Schlägt eine Zahlung fehl, versucht Stripe den Einzug automatisch erneut und informiert den Kunden per E-Mail.</li>
|
||||
<li>Ist der Kunde mit einem Betrag in Höhe von zwei Monatsgebühren seines Abos in Verzug, dürfen wir die Leistungen vorübergehend sperren, etwa die Website offline nehmen. Vorher kündigen wir die Sperre in Textform an und setzen eine Frist von mindestens zwei Wochen. Das gilt nicht, solange der Kunde die Forderung mit nachvollziehbaren Gründen bestreitet. Während einer Sperre löschen wir keine Daten. Nach Zahlung heben wir die Sperre unverzüglich auf. Ist der Kunde mit zwei aufeinanderfolgenden Monatsgebühren in Verzug, dürfen wir außerordentlich kündigen.</li>
|
||||
<li>Preisänderungen kündigen wir mindestens 6 Wochen vor ihrem Wirksamwerden in Textform an. Sie gelten nur, wenn der Kunde zustimmt. Ohne Zustimmung läuft der Vertrag zum bisherigen Preis weiter, und beide Seiten können nach § 8 ordentlich kündigen.</li>
|
||||
</ol>
|
||||
|
||||
<h2 id="p8">§ 8 Laufzeit und Kündigung</h2>
|
||||
<ol class="abs">
|
||||
<li>Jedes Abo hat eine Mindestlaufzeit von zwölf Monaten ab Leistungsbeginn, bei sofortigem Beginn also ab Vertragsschluss, sonst ab Ablauf der Widerrufsfrist. Nach einem Wechsel vom Hosting- ins Wartungs-Abo beginnt eine neue Mindestlaufzeit von zwölf Monaten ab dem Wechseltag (§ 9 Abs. 3). Das Ende der laufenden Mindestlaufzeit zeigt das Kundenportal unter „Verträge“.</li>
|
||||
<li>Beide Seiten können zum Ende der Mindestlaufzeit mit einer Frist von einem Monat kündigen. Wird nicht fristgerecht gekündigt, läuft das Abo auf unbestimmte Zeit weiter.</li>
|
||||
<li>Nach Ablauf der Mindestlaufzeit kann der Kunde jederzeit zum Ende des laufenden Abrechnungsmonats kündigen. Wir können dann mit einer Frist von einem Monat zum Ende eines Abrechnungsmonats kündigen.</li>
|
||||
<li>Gibt der Kunde den Geschäftsbetrieb, für den die Website bestimmt ist, dauerhaft auf, kann er auch während der Mindestlaufzeit mit einer Frist von einem Monat zum Ende eines Abrechnungsmonats kündigen. Auf Verlangen weist er die Geschäftsaufgabe nach, etwa durch die Gewerbeabmeldung.</li>
|
||||
<li>Eine vorzeitige Beendigung im gegenseitigen Einvernehmen ist jederzeit möglich.</li>
|
||||
<li>Das Recht zur außerordentlichen Kündigung aus wichtigem Grund bleibt unberührt.</li>
|
||||
<li>Der E-Mail-Zusatzspeicher hat keine Mindestlaufzeit. Der Kunde kann ihn jederzeit zum Ende des laufenden Abrechnungsmonats ganz oder teilweise (mit einer geringeren Zahl von Paketen) kündigen (§ 10 Abs. 5). Er endet spätestens mit dem Abo.</li>
|
||||
<li>Der Kunde kann über die Schaltfläche „Verträge hier kündigen“ unter project.webklar.com/kuendigen.html kündigen (ohne Anmeldung) oder in Textform, etwa per E-Mail. Dort wählt er, ob er sein Abo einschließlich des Zusatzspeichers kündigt oder mit der Auswahl „Nur den E-Mail-Zusatzspeicher“ allein den Zusatzspeicher, ganz oder mit der Zahl der Pakete, die er kündigt; das geht auch, wenn das Abo schon gekündigt ist. Den Zeitpunkt, zu dem die Kündigung wirkt, bestätigen wir unverzüglich per E-Mail. Bis dahin bleiben alle Leistungen verfügbar. Die Gebühr für den laufenden Monat wird nicht anteilig erstattet.</li>
|
||||
</ol>
|
||||
|
||||
<h2 id="wechsel">§ 9 Wechsel zwischen den Abos</h2>
|
||||
<ol class="abs">
|
||||
<li>Ein Kunde mit laufendem Hosting-Abo kann jederzeit ins Wartungs-Abo wechseln, auch während der Mindestlaufzeit des Hosting-Abos. Voraussetzung ist, dass die Leistung des Hosting-Abos begonnen hat, keine Zahlung offen ist und das Hosting-Abo nicht gekündigt ist.</li>
|
||||
<li>Der Wechsel ist ein neuer Vertrag über das Wartungs-Abo. Der Kunde bestellt ihn im Bereich „Verträge“ des Kundenportals (§ 2 Abs. 5). Die Bestellübersicht zeigt den Preis des Wartungs-Abos, gebuchten Zusatzspeicher, die Gutschrift für die nicht genutzten Tage des Hosting-Abos, den sofort fälligen Betrag, den neuen Abbuchungstag, das Ende der neuen Mindestlaufzeit und die Kündigungsmöglichkeit nach Ablauf der neuen Mindestlaufzeit. Der Kunde stimmt dabei diesen Bedingungen einschließlich des Auftragsverarbeitungsvertrags (Anlage 1) zu. Wird der Wechsel wirksam, endet das Hosting-Abo im gegenseitigen Einvernehmen.</li>
|
||||
<li>Der Wechsel steht unter der Bedingung, dass die Wechselrechnung bezahlt wird. Bei Zahlung per Kreditkarte wird er erst mit der erfolgreichen Zahlung wirksam. Gelingt die Zahlung nicht, etwa weil der Kunde eine von seiner Bank verlangte Freigabe nicht bestätigt, wird er nicht wirksam; spätestens 23 Stunden nach der Bestellung verfällt die Wechselrechnung. Bei SEPA-Lastschrift beginnt das Wartungs-Abo schon mit der Bestellung; wird die Lastschrift nicht eingelöst, gilt der Wechsel als nicht erfolgt. In diesen Fällen läuft das Hosting-Abo mit Preis und Mindestlaufzeit wie vor dem Wechsel weiter; bei SEPA-Lastschrift bleibt der Abbuchungstag der Wechseltag; wir informieren den Kunden per E-Mail. Das Wartungs-Abo beginnt, sobald der Wechsel wirksam ist; einen späteren Beginn bietet der Wechsel nicht. Der Kunde verlangt bei der Bestellung ausdrücklich, dass wir vor Ablauf der Widerrufsfrist beginnen (siehe Hinweise zum Widerruf). Ab Beginn des Wartungs-Abos gilt Leistungsbeschreibung B. Wechseltag ist der Tag der Bestellung; er ist der neue Abbuchungstag (§ 7 Abs. 4), und mit ihm beginnt eine neue Mindestlaufzeit von zwölf Monaten (§ 8 Abs. 1). Das erste Stundenkontingent stellen wir bereit, sobald die Wechselrechnung bezahlt ist, danach monatlich (§ 4). Postfächer und gebuchter Zusatzspeicher bleiben unverändert; ab Beginn des Wartungs-Abos kann der Kunde weitere Postfächer beauftragen; wir richten sie ein, sobald die Wechselrechnung bezahlt ist.</li>
|
||||
<li>Die Abrechnung des Wechsels richtet sich nach § 7 Abs. 4. Ein laufender Rabatt aus einem persönlichen Angebot läuft bis zu seinem Ende weiter. Guthaben aus einer Übernahmegutschrift wird weiter verrechnet.</li>
|
||||
<li>Ein Wechsel vom Wartungs- ins Hosting-Abo ist nach Ablauf der Mindestlaufzeit des Wartungs-Abos zum Ende eines Abrechnungsmonats möglich. Der Kunde teilt uns den Wunsch in Textform mit, etwa im Chat des Kundenportals oder per E-Mail; eine Bestellung im Kundenportal gibt es dafür nicht. Voraussetzung ist, dass die Website statisch ist (Leistungsbeschreibung A Nr. 2). Vor dem Wechsel legt der Kunde fest, welches Postfach bleibt; die Inhalte der übrigen Postfächer stellen wir ihm auf Wunsch vorher als Export bereit, danach löschen wir sie. Nicht genutzte Arbeitszeit entfällt mit dem Wechsel (§ 4 Abs. 2). Es beginnt keine neue Mindestlaufzeit; das Hosting-Abo ist danach nach § 8 Abs. 3 kündbar. Ab dem Wechsel gilt die Monatsgebühr des Hosting-Abos; eine anteilige Verrechnung für den laufenden Abrechnungsmonat gibt es nicht. Wir bestätigen den Wechsel per E-Mail.</li>
|
||||
<li>Will der Kunde im Hosting-Abo eine Funktion einschalten, die nur das Wartungs-Abo enthält, führt ihn das Kundenportal zum Wechsel. Nach Wirksamkeit des Wechsels schaltet er die Funktion in einem eigenen Vorgang ein. Der Wechsel bleibt eine eigene Bestellung nach Absatz 2.</li>
|
||||
</ol>
|
||||
|
||||
<h2 id="zusatzspeicher">§ 10 E-Mail-Zusatzspeicher</h2>
|
||||
<ol class="abs">
|
||||
<li>In beiden Abos kann der Kunde den Speicher seiner Postfächer erweitern. Ein Paket Zusatzspeicher umfasst 5 GB und kostet 1,00 € pro Monat (Endpreis, gemäß § 19 UStG ohne Umsatzsteuer). Der Kunde kann bis zu 10 Pakete buchen. Der Zusatzspeicher erhöht den Speicher, der allen Postfächern des Kunden zusammen zur Verfügung steht (§ 3 Abs. 4). Bestellungen und Änderungen des Zusatzspeichers im Kundenportal sind möglich, sobald die Leistung des Abos begonnen hat, und nur, solange keine Zahlung offen ist und das Abo nicht gekündigt ist.</li>
|
||||
<li>Der Kunde bestellt Zusatzspeicher im Kundenportal im Bereich „Verträge“, indem er die Zahl der Pakete erhöht (§ 2 Abs. 5). Die Bestellübersicht zeigt die neue Gesamtgröße, den Mehrpreis pro Monat, den anteiligen Betrag für den Rest des laufenden Abrechnungsmonats, den Tag der nächsten Abbuchung und die Laufzeit (monatlich, zum Ende jedes Abrechnungsmonats kündbar, endet spätestens mit dem Abo). Mit dem Klick auf „Zahlungspflichtig bestellen“ kommt der Vertrag über den Zusatzspeicher zustande.</li>
|
||||
<li>Wir stellen den zusätzlichen Speicher in der Regel innerhalb eines Werktags bereit und verteilen ihn nach den Vorgaben des Kunden auf die Postfächer (§ 3 Abs. 4). Bis dahin zeigt das Kundenportal die Bestellung als „wird eingerichtet“ an. Wir beginnen mit der Bereitstellung sofort, auch während der Widerrufsfrist; die Folgen eines Widerrufs stehen in den Hinweisen zum Widerruf.</li>
|
||||
<li>Die Abrechnung richtet sich nach § 7 Abs. 3.</li>
|
||||
<li>Der Kunde kann die Zahl der Pakete verringern, im Kundenportal im Bereich „Verträge“ mit der Schaltfläche „Speichern“, solange das Abo nicht gekündigt ist. Den Zusatzspeicher ganz oder teilweise kündigen kann er jederzeit über „Verträge hier kündigen“ mit der Auswahl „Nur den E-Mail-Zusatzspeicher“ und der Zahl der Pakete oder in Textform (§ 8 Abs. 8); nach einer Kündigung des Abos kann der Kunde die Zahl der Pakete nur noch über „Verträge hier kündigen“ oder in Textform verringern. Die Änderung wirkt zum Ende des laufenden Abrechnungsmonats; bis dahin zeigt das Kundenportal den Zeitpunkt und die künftige Größe an. Wir bestätigen die Änderung per E-Mail. Passen die gespeicherten E-Mails dann nicht mehr in den verbleibenden Speicher, nimmt das betroffene Postfach keine neuen E-Mails mehr an, bis wieder Platz ist (§ 3 Abs. 4). Der Kunde sollte vorher E-Mails löschen oder sichern.</li>
|
||||
<li>Der Zusatzspeicher endet spätestens mit dem Abo. Bei einem Wechsel ins Wartungs-Abo läuft er unverändert weiter.</li>
|
||||
</ol>
|
||||
|
||||
<h2 id="p11">§ 11 Vertragsende</h2>
|
||||
<ol class="abs">
|
||||
<li>Während der Laufzeit und bis 30 Tage nach Vertragsende stellen wir dem Kunden auf Wunsch eine Kopie seiner Website bzw. Anwendungsdaten (Dateien und Datenbank-Export) und der Inhalte aller seiner Postfächer bereit. Wem Quellcode und Nutzungsrechte an der Website zustehen, richtet sich nach dem Vertrag über ihre Erstellung.</li>
|
||||
<li>Wir geben den AuthCode der Domain heraus und helfen beim Umzug zu einem anderen Anbieter. Ist die Domain 30 Tage nach Vertragsende nicht umgezogen, dürfen wir ihre Registrierung beenden. Das kündigen wir vorher in Textform an und setzen eine Frist von 14 Tagen.</li>
|
||||
<li>Nach Ablauf der Frist aus Absatz 1 löschen wir die Daten, die wir für den Kunden gespeichert haben (Website bzw. Anwendungsdaten und alle Postfächer). Vertrags- und Abrechnungsdaten bewahren wir auf, wie es die Datenschutzerklärung beschreibt (project.webklar.com/datenschutz.html). Sicherungskopien werden spätestens mit Ablauf ihrer Aufbewahrungszeit (§ 3 Abs. 5) überschrieben.</li>
|
||||
<li>Mit dem Ende des Abos, das eine Funktion enthält, endet die Funktion; abweichend davon läuft Öffnungszeiten live noch 30 Tage unverändert weiter. Wir schalten sie ab und löschen ihre Daten nach den Fristen in ihrer Leistungsbeschreibung; die Messdaten der Website-Analyse löschen wir 60 Tage nach der Abschaltung. Beginnt innerhalb dieser Fristen wieder ein Abo, das die Funktion enthält, schalten wir sie mit den vorhandenen Daten wieder ein. Wechselt der Kunde vom Wartungs- ins Hosting-Abo, bleibt die Website-Analyse; die übrigen Funktionen enden. Das Gleiche gilt, wenn der Kunde den Wechsel ins Wartungs-Abo widerruft.</li>
|
||||
</ol>
|
||||
|
||||
<h2 id="p12">§ 12 Mängel</h2>
|
||||
<p>Es gelten die gesetzlichen Regeln, für die digitalen Leistungen (Bereitstellung der Website, E-Mail-Postfächer, Zusatzspeicher) insbesondere das gesetzliche Mängelhaftungsrecht für digitale Dienstleistungen (§§ 327 ff. BGB). Mängel unserer Leistungen beheben wir nach Meldung innerhalb angemessener Frist.</p>
|
||||
|
||||
<h2 id="p13">§ 13 Haftung</h2>
|
||||
<ol class="abs">
|
||||
<li>Wir haften unbeschränkt bei Vorsatz und grober Fahrlässigkeit, bei Verletzung von Leben, Körper oder Gesundheit, nach dem Produkthaftungsgesetz und soweit wir eine Garantie übernommen haben.</li>
|
||||
<li>Bei leicht fahrlässiger Verletzung einer wesentlichen Vertragspflicht haften wir nur für den vertragstypischen, vorhersehbaren Schaden. Wesentlich sind Pflichten, deren Erfüllung die ordnungsgemäße Durchführung des Vertrags überhaupt erst ermöglicht und auf deren Einhaltung der Kunde regelmäßig vertrauen darf. Im Übrigen ist die Haftung für leichte Fahrlässigkeit ausgeschlossen.</li>
|
||||
<li>Außer in den Fällen von Absatz 1 haften wir für den Verlust von Daten nur in Höhe des Aufwands, der zur Wiederherstellung nötig gewesen wäre, wenn der Kunde seine Pflicht aus § 6 Abs. 4 erfüllt hätte.</li>
|
||||
<li>Die verschuldensunabhängige Haftung für Mängel, die schon bei Vertragsschluss vorhanden waren (§ 536a Abs. 1 BGB), ist ausgeschlossen.</li>
|
||||
<li>Diese Regeln gelten auch zugunsten unserer Mitarbeiter und Beauftragten.</li>
|
||||
</ol>
|
||||
|
||||
<h2 id="p14">§ 14 Datenschutz</h2>
|
||||
<ol class="abs">
|
||||
<li>Wie wir personenbezogene Daten des Kunden verarbeiten, beschreibt unsere Datenschutzerklärung unter project.webklar.com/datenschutz.html.</li>
|
||||
<li>Soweit wir im Rahmen eines Abos personenbezogene Daten im Auftrag des Kunden verarbeiten, etwa E-Mails in seinen Postfächern, Eingaben in Kontaktformulare oder Server-Protokolle seiner Website, gilt der Auftragsverarbeitungsvertrag in Anlage 1. Er wird mit der Bestellung des Abos vereinbart und beim Wechsel ins Wartungs-Abo erneut bestätigt. Für Funktionen, die personenbezogene Daten im Auftrag des Kunden verarbeiten, gilt zusätzlich der Auftragsverarbeitungsvertrag der Funktion, dem der Kunde beim Einschalten mit dem Klick auf „Einschalten“ zustimmt.</li>
|
||||
</ol>
|
||||
|
||||
<h2 id="p15">§ 15 Änderungen dieser Bedingungen</h2>
|
||||
<ol class="abs">
|
||||
<li>Änderungen teilen wir mindestens 6 Wochen vor ihrem Wirksamwerden in Textform mit. Sie werden nur mit Zustimmung des Kunden wirksam. Ohne Zustimmung gelten die bisherigen Bedingungen weiter, und beide Seiten können nach § 8 kündigen.</li>
|
||||
<li>Fassung 6 gilt für Abos, die ab ihrer Veröffentlichung abgeschlossen werden. Für Abos, die nach einer früheren Fassung abgeschlossen wurden, gelten diese Fassungen weiter. Schaltet der Kunde mit einem solchen Abo eine Funktion ein, gelten für die Funktion ihre Leistungsbeschreibung und ihr Auftragsverarbeitungsvertrag in der angezeigten Fassung, einschließlich der Regeln zur Einrichtung und zum Stundenkontingent.</li>
|
||||
</ol>
|
||||
|
||||
<h2 id="p16">§ 16 Schlussbestimmungen</h2>
|
||||
<ol class="abs">
|
||||
<li>Es gilt deutsches Recht unter Ausschluss des UN-Kaufrechts. Gegenüber Verbrauchern gilt diese Rechtswahl nur, soweit ihnen dadurch nicht der Schutz zwingender Vorschriften des Staates entzogen wird, in dem sie ihren gewöhnlichen Aufenthalt haben.</li>
|
||||
<li>Ist der Kunde Kaufmann, juristische Person des öffentlichen Rechts oder öffentlich-rechtliches Sondervermögen, ist Gerichtsstand der Sitz von WEBklar.</li>
|
||||
<li>Wir sind nicht verpflichtet und nicht bereit, an einem Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle teilzunehmen.</li>
|
||||
</ol>
|
||||
|
||||
<p class="anlage-kopf neue-seite" id="hosting">Leistungsbeschreibung A</p>
|
||||
<h2>Hosting-Abo</h2>
|
||||
<p class="hinweis">20,00 € pro Monat · Endpreis, gemäß § 19 UStG ohne Umsatzsteuer · Mindestlaufzeit 12 Monate · für statische Websites</p>
|
||||
<h3>1. Umfang</h3>
|
||||
<ul>
|
||||
<li>Bereitstellung der Website auf unserem Server in Deutschland mit SSL-Zertifikat, Überwachung der Erreichbarkeit und Updates von Betriebssystem und Server-Software (§ 3 Abs. 2)</li>
|
||||
<li>1 Domain mit Verwaltung (§ 3 Abs. 3)</li>
|
||||
<li>1 E-Mail-Postfach mit 5 GB Speicher; mehr Speicher über den Zusatzspeicher (§ 3 Abs. 4, § 10)</li>
|
||||
<li>tägliche Datensicherung (§ 3 Abs. 5)</li>
|
||||
<li>Servicezeiten und Reaktion (§ 3 Abs. 6); die Behebung von Störungen an Server, Domain und Postfach in unserem Verantwortungsbereich ist inklusive</li>
|
||||
<li>Website-Analyse im Kundenportal (§ 3 Abs. 8)</li>
|
||||
</ul>
|
||||
<h3>2. Nur statische Websites</h3>
|
||||
<ul>
|
||||
<li>Das Hosting-Abo ist für statische Websites bestimmt, die wir erstellt oder zur Betreuung übernommen haben. Statisch heißt: Der Webserver liefert die Dateien der Website (HTML, CSS, JavaScript, Bilder) unverändert aus. Dazu gehören auch die von uns erstellten React- und Vite-Seiten.</li>
|
||||
<li>Nicht möglich sind ein Content-Management-System, ein Login, eine Datenbank und eigener serverseitiger Code (etwa PHP). Solche Teile brauchen laufende Updates; ohne sie wären sie ein Sicherheitsrisiko für den Server, auf dem auch andere Websites und unser Kundenportal laufen.</li>
|
||||
<li>Kontakt über E-Mail-Links auf der Website ist möglich. Formulare, die Eingaben an einen Server übermitteln, sind im Hosting-Abo nicht enthalten; eine Lösung dafür bieten wir auf Anfrage gesondert an.</li>
|
||||
<li>Braucht die Website später dynamische Teile, bieten wir den Wechsel ins Wartungs-Abo an (§ 9).</li>
|
||||
</ul>
|
||||
<h3>3. Keine Pflege der Website</h3>
|
||||
<ul>
|
||||
<li>Updates und Pflege der Website selbst sind nicht enthalten. Weil statische Websites keinen eigenen Code auf dem Server ausführen, sind sie für den sicheren Betrieb in der Regel nicht nötig.</li>
|
||||
<li>Änderungen an der Website und andere Aufträge führen wir nur nach Freigabe des Kunden aus und berechnen 100 € pro Stunde (Endpreis, gemäß § 19 UStG ohne Umsatzsteuer) in Schritten von 15 Minuten (§ 4 Abs. 7). Ein Stundenkontingent gibt es im Hosting-Abo nicht.</li>
|
||||
</ul>
|
||||
<h3>4. Nicht enthalten</h3>
|
||||
<ul>
|
||||
<li>Änderungen, neue Funktionen, Redesign oder neue Seiten ohne gesonderten Auftrag (Nr. 3).</li>
|
||||
<li>Texte, Bilder und Rechtstexte (Impressum, Datenschutzerklärung) für die Website des Kunden, Suchmaschinenoptimierung und Werbung.</li>
|
||||
<li>Unterstützung für Endgeräte, Betriebssysteme oder E-Mail-Programme des Kunden.</li>
|
||||
<li>Behebung von Fehlern, die der Kunde oder Dritte durch eigene Änderungen verursacht haben. Das ist als Auftrag nach Nr. 3 möglich.</li>
|
||||
</ul>
|
||||
|
||||
<p class="anlage-kopf" id="wartung">Leistungsbeschreibung B</p>
|
||||
<h2>Wartungs-Abo</h2>
|
||||
<p class="hinweis">50,00 € pro Monat · Endpreis, gemäß § 19 UStG ohne Umsatzsteuer · Mindestlaufzeit 12 Monate, auch nach einem Wechsel · für alle Websites und Web-Anwendungen, die wir erstellt oder zur Betreuung übernommen haben</p>
|
||||
<h3>1. Umfang</h3>
|
||||
<ul>
|
||||
<li>Betrieb der Website bzw. Web-Anwendung auf unserem Server in Deutschland mit SSL-Zertifikat, Überwachung der Erreichbarkeit und Updates von Betriebssystem und Server-Software (§ 3 Abs. 2)</li>
|
||||
<li>Pflege der Website (Nr. 2)</li>
|
||||
<li>1 Domain mit Verwaltung (§ 3 Abs. 3)</li>
|
||||
<li>beliebig viele Postfächer, zusammen 5 GB Speicher; mehr Speicher über den Zusatzspeicher (§ 3 Abs. 4, § 10)</li>
|
||||
<li>tägliche Datensicherung (§ 3 Abs. 5)</li>
|
||||
<li>1 Stunde Arbeitszeit je bezahltem Monat; nicht genutzte Zeit bleibt 12 Monate verfügbar (§ 4)</li>
|
||||
<li>Servicezeiten und Reaktion (§ 3 Abs. 6)</li>
|
||||
<li>Funktionen im Kundenportal: News-Pinnwand, Terminplaner, Webshop, Buchungskalender, Öffnungszeiten live und Website-Analyse (§ 3 Abs. 8)</li>
|
||||
</ul>
|
||||
<h3>2. Pflege der Website</h3>
|
||||
<ul>
|
||||
<li>Zusätzlich zu den Updates nach § 3 Abs. 2 spielen wir Sicherheitsupdates für die von uns eingesetzten Software-Komponenten der Website bzw. Web-Anwendung ein, kritische in der Regel innerhalb von 3 Werktagen nach ihrer Veröffentlichung, übrige regelmäßig. Aktualisierungen, die für den vertragsgemäßen Betrieb nötig sind, stellen wir während der gesamten Vertragslaufzeit bereit.</li>
|
||||
<li>Das Wartungs-Abo eignet sich für alle Websites und Web-Anwendungen, die wir erstellt oder zur Betreuung übernommen haben, auch mit Content-Management-System, Login, Datenbank oder serverseitigem Code.</li>
|
||||
<li>Änderungswünsche setzen wir über das Stundenkontingent um (§ 4), in der Regel innerhalb von 5 Werktagen.</li>
|
||||
</ul>
|
||||
<h3>3. Nicht enthalten</h3>
|
||||
<ul>
|
||||
<li>Neue Funktionen, Redesign oder neue Seiten, soweit sie nicht über das Stundenkontingent beauftragt werden.</li>
|
||||
<li>Texte, Bilder und Rechtstexte (Impressum, Datenschutzerklärung) für die Website des Kunden, Suchmaschinenoptimierung und Werbung.</li>
|
||||
<li>Unterstützung für Endgeräte, Betriebssysteme oder E-Mail-Programme des Kunden.</li>
|
||||
<li>Behebung von Fehlern, die der Kunde oder Dritte durch eigene Änderungen verursacht haben. Das ist über das Stundenkontingent möglich.</li>
|
||||
</ul>
|
||||
|
||||
<div class="neue-seite"></div>
|
||||
<h2 id="widerruf">Widerrufsbelehrung</h2>
|
||||
<p class="hinweis">Das Widerrufsrecht gilt nur für Verbraucher. Die folgende Belehrung entspricht dem gesetzlichen Muster. Sie gilt für jede Bestellung: den Abschluss eines Abos, den Wechsel ins Wartungs-Abo und die Bestellung von Zusatzspeicher. Hinweise dazu stehen nach dem Muster-Widerrufsformular.</p>
|
||||
<h3>Widerrufsrecht</h3>
|
||||
<p>Sie haben das Recht, binnen vierzehn Tagen ohne Angabe von Gründen diesen Vertrag zu widerrufen.</p>
|
||||
<p>Die Widerrufsfrist beträgt vierzehn Tage ab dem Tag des Vertragsabschlusses.</p>
|
||||
<p>Um Ihr Widerrufsrecht auszuüben, müssen Sie uns (WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, Telefon 0176 23726355, E-Mail support@webklar.com) mittels einer eindeutigen Erklärung (z. B. ein mit der Post versandter Brief oder eine E-Mail) über Ihren Entschluss, diesen Vertrag zu widerrufen, informieren. Sie können dafür das beigefügte Muster-Widerrufsformular verwenden, das jedoch nicht vorgeschrieben ist. Sie können Ihr Widerrufsrecht auch online unter https://project.webklar.com/widerrufen.html ausüben. Wenn Sie diese Online-Funktion nutzen, übermitteln wir Ihnen auf einem dauerhaften Datenträger (z. B. durch eine E-Mail) unverzüglich eine Eingangsbestätigung mit Informationen zum Inhalt der Widerrufserklärung sowie dem Datum und der Uhrzeit ihres Eingangs.</p>
|
||||
<p>Zur Wahrung der Widerrufsfrist reicht es aus, dass Sie die Mitteilung über die Ausübung des Widerrufsrechts vor Ablauf der Widerrufsfrist absenden.</p>
|
||||
<h3>Folgen des Widerrufs</h3>
|
||||
<p>Wenn Sie diesen Vertrag widerrufen, haben wir Ihnen alle Zahlungen, die wir von Ihnen erhalten haben, einschließlich der Lieferkosten (mit Ausnahme der zusätzlichen Kosten, die sich daraus ergeben, dass Sie eine andere Art der Lieferung als die von uns angebotene, günstigste Standardlieferung gewählt haben), unverzüglich und spätestens binnen vierzehn Tagen ab dem Tag zurückzuzahlen, an dem die Mitteilung über Ihren Widerruf dieses Vertrags bei uns eingegangen ist. Für diese Rückzahlung verwenden wir dasselbe Zahlungsmittel, das Sie bei der ursprünglichen Transaktion eingesetzt haben, es sei denn, mit Ihnen wurde ausdrücklich etwas anderes vereinbart; in keinem Fall werden Ihnen wegen dieser Rückzahlung Entgelte berechnet.</p>
|
||||
<p>Haben Sie verlangt, dass die Dienstleistungen während der Widerrufsfrist beginnen soll, so haben Sie uns einen angemessenen Betrag zu zahlen, der dem Anteil der bis zu dem Zeitpunkt, zu dem Sie uns von der Ausübung des Widerrufsrechts hinsichtlich dieses Vertrags unterrichten, bereits erbrachten Dienstleistungen im Vergleich zum Gesamtumfang der im Vertrag vorgesehenen Dienstleistungen entspricht.</p>
|
||||
<p class="meta">Ende der Widerrufsbelehrung</p>
|
||||
|
||||
<h3 id="formular">Muster-Widerrufsformular</h3>
|
||||
<div class="box">
|
||||
<p>(Wenn Sie den Vertrag widerrufen wollen, dann füllen Sie bitte dieses Formular aus und senden Sie es zurück.)</p>
|
||||
<p>– An WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, E-Mail: support@webklar.com:</p>
|
||||
<p>– Hiermit widerrufe(n) ich/wir (*) den von mir/uns (*) abgeschlossenen Vertrag über den Kauf der folgenden Waren (*)/die Erbringung der folgenden Dienstleistung (*)</p>
|
||||
<p>– Bestellt am (*)/erhalten am (*)</p>
|
||||
<p>– Name des/der Verbraucher(s)</p>
|
||||
<p>– Anschrift des/der Verbraucher(s)</p>
|
||||
<p>– Unterschrift des/der Verbraucher(s) (nur bei Mitteilung auf Papier)</p>
|
||||
<p>– Datum</p>
|
||||
<p class="meta">(*) Unzutreffendes streichen.</p>
|
||||
</div>
|
||||
|
||||
<h2 id="widerruf-hinweise">Hinweise zum Widerruf</h2>
|
||||
<p class="hinweis">Diese Hinweise gehören zu diesen Bedingungen, nicht zur gesetzlichen Widerrufsbelehrung. Sie erklären, wie sich das Widerrufsrecht auf die einzelnen Bestellungen auswirkt.</p>
|
||||
<ol class="abs">
|
||||
<li>Das Widerrufsrecht gilt für jede Bestellung gesondert: für den Abschluss eines Abos, für den Wechsel ins Wartungs-Abo und für jede Bestellung von Zusatzspeicher. Die Frist von vierzehn Tagen beginnt jeweils mit dem Vertragsschluss (§ 2 Abs. 3 bzw. Abs. 5). Unter project.webklar.com/widerrufen.html wählt der Kunde aus, welche Bestellung er widerruft. Solange eine Widerrufsfrist läuft, zeigt das Kundenportal unter „Verträge“ den letzten Tag der Frist an.</li>
|
||||
<li>Widerruf des Abos: Widerruft der Kunde den Abschluss seines Abos, endet damit auch der dazu bestellte Zusatzspeicher. Was der Kunde für den Zusatzspeicher gezahlt hat, erstatten wir vollständig; Wertersatz verlangen wir dafür nicht (Nr. 4).</li>
|
||||
<li>Widerruf des Wechsels: Widerruft der Kunde den Wechsel ins Wartungs-Abo, läuft sein Hosting-Abo zu den bisherigen Bedingungen weiter, als hätte es den Wechsel nicht gegeben, einschließlich seiner ursprünglichen Mindestlaufzeit. Wir erstatten den für das Wartungs-Abo gezahlten Mehrbetrag. Mehrbetrag ist, was der Kunde seit dem Wechsel mehr gezahlt hat, als er ohne den Wechsel für das Hosting-Abo gezahlt hätte. Weil der Kunde beim Wechsel den sofortigen Beginn verlangt hat, ziehen wir davon den Wertersatz für die bis zum Widerruf erbrachte Leistung ab (§ 357a Abs. 2 BGB): den anteiligen Mehrpreis des Wartungs-Abos gegenüber dem Hosting-Abo für die Tage vom Beginn des Wartungs-Abos bis zum Widerruf.</li>
|
||||
<li>Widerruf des Zusatzspeichers: Wir stellen bestellten Zusatzspeicher sofort bereit, ohne dass der Kunde das ausdrücklich verlangen muss. Widerruft er die Bestellung, nehmen wir die Pakete zurück und erstatten alles, was er dafür gezahlt hat. Wertersatz verlangen wir nicht. Passen die gespeicherten E-Mails danach nicht mehr in den verbleibenden Speicher, nimmt das betroffene Postfach keine neuen E-Mails mehr an, bis wieder Platz ist (§ 3 Abs. 4).</li>
|
||||
<li>Das Einschalten einer Funktion ist kein eigener Vertrag, sondern Teil des Abos. Es verlängert die Widerrufsfrist des Abos nicht. Widerruft ein Verbraucher das Abo, endet auch die Funktion (§ 11 Abs. 4).</li>
|
||||
</ol>
|
||||
|
||||
<p class="anlage-kopf neue-seite" id="avv">Anlage 1</p>
|
||||
<h2>Auftragsverarbeitungsvertrag nach Art. 28 DSGVO</h2>
|
||||
<p class="hinweis">zwischen dem Kunden als Verantwortlichem („Kunde“) und der WEBklar GbR als Auftragsverarbeiter („WEBklar“). Der Vertrag wird mit der Bestellung des Hosting-Abos oder des Wartungs-Abos vereinbart; beim Wechsel ins Wartungs-Abo bestätigt der Kunde ihn erneut.</p>
|
||||
|
||||
<h3>1. Gegenstand und Dauer</h3>
|
||||
<p>WEBklar verarbeitet personenbezogene Daten im Auftrag des Kunden, soweit das für die Leistungen des Hosting-Abos oder des Wartungs-Abos (Bereitstellung bzw. Betrieb und Pflege der Website, Domain, E-Mail-Postfächer einschließlich Zusatzspeicher, Unterstützung) sowie für gesondert vereinbarte Zusatzleistungen zum Abo, etwa eine Formularlösung, nötig ist. Der Vertrag gilt für die Dauer des Abos, auch über einen Wechsel zwischen den Abos hinweg, und darüber hinaus, bis alle Daten nach Nummer 8 gelöscht oder zurückgegeben sind.</p>
|
||||
|
||||
<h3>2. Art und Zweck der Verarbeitung, Datenarten, Betroffene</h3>
|
||||
<table>
|
||||
<tr><td>Art der Verarbeitung</td><td>Speichern, Bereitstellen, Übermitteln (Empfang und Versand von E-Mails, Weiterleiten von Kontaktformular-Eingaben per E-Mail an den Kunden), Sichern, Wiederherstellen und Löschen; Einsicht, soweit sie für Wartung und Unterstützung nötig ist</td></tr>
|
||||
<tr><td>Zweck</td><td>Betrieb der Website bzw. Web-Anwendung und der E-Mail-Postfächer des Kunden sowie gesondert vereinbarter Zusatzleistungen</td></tr>
|
||||
<tr><td>Datenarten</td><td>Inhalte der Website; Eingaben in Formulare (z. B. Name, Kontaktdaten, Nachricht), soweit die Website solche vorsieht; Inhalte der Postfächer (E-Mails mit Anhängen, Absender, Empfänger, Zeitpunkte); Verbindungsdaten in Server-Protokollen (IP-Adresse, Zeitpunkt, aufgerufene Seite, Browserkennung); Benutzerkonten, soweit die Anwendung solche vorsieht</td></tr>
|
||||
<tr><td>Betroffene</td><td>Besucher der Website; Nutzer der Postfächer des Kunden, insbesondere seine Beschäftigten; Absender und Empfänger von E-Mails; Kunden, Interessenten und Beschäftigte des Kunden; weitere Personen, deren Daten der Kunde über seine Website oder seine Postfächer verarbeitet</td></tr>
|
||||
</table>
|
||||
<p>Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO) verarbeitet der Kunde über seine Website oder seine Postfächer nur nach vorheriger Mitteilung an WEBklar.</p>
|
||||
|
||||
<h3>3. Weisungen</h3>
|
||||
<p>WEBklar verarbeitet die Daten nur auf dokumentierte Weisung des Kunden, auch bei Übermittlungen in Drittländer, es sei denn, eine gesetzliche Pflicht verlangt etwas anderes. In diesem Fall teilt WEBklar dem Kunden die Pflicht vorher mit, soweit das Gesetz es erlaubt. Die Weisungen ergeben sich aus diesem Vertrag und den Bedingungen der Abos. Weitere Weisungen erteilt der Kunde in Textform, etwa im Chat des Kundenportals. Hält WEBklar eine Weisung für rechtswidrig, weist es den Kunden unverzüglich darauf hin.</p>
|
||||
|
||||
<h3>4. Vertraulichkeit</h3>
|
||||
<p>WEBklar setzt nur Personen ein, die zur Vertraulichkeit verpflichtet sind oder einer gesetzlichen Verschwiegenheitspflicht unterliegen.</p>
|
||||
|
||||
<h3>5. Sicherheit der Verarbeitung</h3>
|
||||
<p>WEBklar trifft die technischen und organisatorischen Maßnahmen nach Art. 32 DSGVO, die in Anlage 2 beschrieben sind. WEBklar darf sie an den Stand der Technik anpassen, sofern das Schutzniveau nicht sinkt.</p>
|
||||
|
||||
<h3>6. Unterauftragsverarbeiter</h3>
|
||||
<p>Der Kunde genehmigt die in Anlage 3 genannten Unterauftragsverarbeiter. Will WEBklar einen Unterauftragsverarbeiter hinzuziehen oder ersetzen, teilt es das dem Kunden mindestens vier Wochen vorher in Textform mit. Der Kunde kann aus wichtigem datenschutzrechtlichem Grund widersprechen. Einigen sich die Parteien nicht, können beide das Abo zu dem Zeitpunkt kündigen, zu dem der neue Unterauftragsverarbeiter eingesetzt werden soll. WEBklar verpflichtet jeden Unterauftragsverarbeiter vertraglich auf dieselben Datenschutzpflichten, die sich aus diesem Vertrag ergeben.</p>
|
||||
|
||||
<h3>7. Unterstützung des Kunden</h3>
|
||||
<ul>
|
||||
<li>WEBklar leitet Anfragen betroffener Personen, die an WEBklar gerichtet sind, unverzüglich an den Kunden weiter und unterstützt ihn bei der Beantwortung, soweit das mit den eingesetzten Systemen möglich ist.</li>
|
||||
<li>WEBklar unterstützt den Kunden bei seinen Pflichten aus Art. 32 bis 36 DSGVO (Sicherheit, Meldung von Datenschutzverletzungen, Datenschutz-Folgenabschätzung).</li>
|
||||
<li>Verletzungen des Schutzes personenbezogener Daten meldet WEBklar dem Kunden unverzüglich, möglichst innerhalb von 24 Stunden nach Kenntnis, mit den Informationen, die zu diesem Zeitpunkt vorliegen.</li>
|
||||
</ul>
|
||||
|
||||
<h3>8. Löschung und Rückgabe</h3>
|
||||
<p>Nach Ende des Abos gibt WEBklar die Daten nach Wahl des Kunden zurück (§ 11 Abs. 1 der Bedingungen) oder löscht sie, soweit keine gesetzliche Pflicht zur Speicherung besteht. Sicherungskopien werden im Rahmen der regulären Rotation überschrieben.</p>
|
||||
|
||||
<h3>9. Nachweise und Kontrollen</h3>
|
||||
<p>WEBklar stellt dem Kunden alle Informationen zur Verfügung, die zum Nachweis der Pflichten aus Art. 28 DSGVO nötig sind. WEBklar ermöglicht Überprüfungen einschließlich Inspektionen durch den Kunden oder einen von ihm beauftragten, zur Verschwiegenheit verpflichteten Prüfer. Sie finden nach Anmeldung mit angemessener Frist (in der Regel 14 Tage) zu den Servicezeiten statt, ohne den Betrieb unzumutbar zu stören. Für das Rechenzentrum genügen in der Regel die Nachweise des Rechenzentrumsbetreibers (Zertifizierung nach ISO/IEC 27001). Für mehr als eine Vor-Ort-Kontrolle pro Jahr kann WEBklar eine angemessene Vergütung verlangen.</p>
|
||||
|
||||
<h3>10. Schlussbestimmungen</h3>
|
||||
<p>Für die Haftung gilt Art. 82 DSGVO. Bei Widersprüchen geht dieser Vertrag in Datenschutzfragen den Bedingungen der Abos vor.</p>
|
||||
|
||||
<p class="anlage-kopf neue-seite" id="tom">Anlage 2</p>
|
||||
<h2>Technische und organisatorische Maßnahmen (Art. 32 DSGVO)</h2>
|
||||
<table>
|
||||
<tr><th style="width:28%">Bereich</th><th>Maßnahmen</th></tr>
|
||||
<tr><td>Rechenzentrum und Zutritt</td><td>Betrieb im Rechenzentrum der Hetzner Online GmbH in Falkenstein (Deutschland). Hetzner ist nach ISO/IEC 27001 zertifiziert. Zutritt, Stromversorgung, Klimatisierung und Brandschutz regeln die Maßnahmen von Hetzner.</td></tr>
|
||||
<tr><td>Zugang zu den Systemen</td><td>Serververwaltung nur über verschlüsselte SSH-Verbindung, Zugang nur für die Gesellschafter. Automatische Sperre bei wiederholten Fehlversuchen. Firewall des Rechenzentrumsbetreibers, die nur die benötigten Dienste zulässt. Zwei-Faktor-Anmeldung für die Verwaltungskonten bei Hetzner und Stripe.</td></tr>
|
||||
<tr><td>Zugriff auf Daten</td><td>Zugriff nur für die Gesellschafter und beauftragte Mitarbeiter, soweit ihre Aufgabe es erfordert. Im Kundenportal sieht jeder Kunde nur seine eigenen Daten. Datenbanken sind nur für die jeweilige Anwendung erreichbar. Zugangsdaten zu E-Mail-Postfächern erhalten nur der Kunde und die von ihm in Textform benannten Personen.</td></tr>
|
||||
<tr><td>Übertragung</td><td>Verschlüsselte Übertragung (TLS) für alle Websites, das Kundenportal und die Verwaltung. E-Mail-Versand und -Abruf nur verschlüsselt.</td></tr>
|
||||
<tr><td>Nachvollziehbarkeit</td><td>Änderungen an Websites laufen über eine Versionsverwaltung (Git) und sind nachvollziehbar. Aufträge und Weisungen werden im Ticketsystem bzw. im Chat des Kundenportals dokumentiert.</td></tr>
|
||||
<tr><td>Verfügbarkeit und Wiederherstellung</td><td>Tägliche Sicherung des gesamten Servers, verfügbar sind die letzten sieben Tage. Tägliche Datenbank-Exporte vor der Serversicherung. Automatische Überwachung der Erreichbarkeit. Automatische Sicherheitsupdates des Betriebssystems. Wiederherstellung wird regelmäßig getestet.</td></tr>
|
||||
<tr><td>Trennung</td><td>Websites der Kunden in getrennten Verzeichnissen bzw. Containern. Vorschau- und Testversionen sind nur nach Anmeldung erreichbar. Postfächer und Speichergrenzen werden je Kunde getrennt verwaltet.</td></tr>
|
||||
<tr><td>Organisation</td><td>Verpflichtung aller Beteiligten auf Vertraulichkeit. Festgelegter Ablauf für die Meldung von Datenschutzverletzungen. Löschung nach Vertragsende (§ 11 der Bedingungen). Server-Protokolle werden nach 14 Tagen gelöscht. Die Maßnahmen werden mindestens einmal im Jahr überprüft.</td></tr>
|
||||
</table>
|
||||
|
||||
<p class="anlage-kopf" id="sub">Anlage 3</p>
|
||||
<h2>Unterauftragsverarbeiter</h2>
|
||||
<table>
|
||||
<tr><th>Unternehmen</th><th>Leistung</th><th>Ort der Verarbeitung</th></tr>
|
||||
<tr><td>Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen</td><td>Server-Hosting und Datensicherung</td><td>Deutschland (Rechenzentrum Falkenstein)</td></tr>
|
||||
<tr><td>Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen</td><td>E-Mail-Postfächer (Webhosting)</td><td>Deutschland (Rechenzentrum Nürnberg)</td></tr>
|
||||
<tr><td>Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen</td><td>Server für die Website-Analyse (Messdienst, nur bei eingeschalteter Funktion)</td><td>Deutschland (Rechenzentrum Falkenstein)</td></tr>
|
||||
</table>
|
||||
<p class="hinweis">Die Domain registrieren wir über die Hetzner Online GmbH als Domain-Registrar. Für die Daten des Domaininhabers sind Registrar und Vergabestelle (bei .de-Domains die DENIC eG) eigenständig verantwortlich; sie sind insoweit keine Unterauftragsverarbeiter. Die Zahlungsabwicklung über Stripe betrifft das Vertragsverhältnis zwischen Kunde und WEBklar und ist nicht Gegenstand dieses Auftragsverarbeitungsvertrags.</p>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,199 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="webklar-fassung" content="2026-10-16">
|
||||
<meta name="robots" content="noindex">
|
||||
<title>Auftragsverarbeitungsvertrag Terminplaner – WEBklar</title>
|
||||
<style>
|
||||
:root { color-scheme: light; }
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; background: #fff; color: #1a1f36; font: 16px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; font-variant-numeric: tabular-nums; }
|
||||
main { max-width: 780px; margin: 0 auto; padding: 44px 20px 80px; }
|
||||
.brand { font-weight: 600; letter-spacing: 0.12em; font-size: 13px; color: #111; }
|
||||
h1 { font-size: 30px; line-height: 1.2; margin: 6px 0 6px; font-weight: 600; }
|
||||
h2 { font-size: 20px; margin: 36px 0 10px; padding-top: 14px; border-top: 1px solid #ebeef1; font-weight: 600; }
|
||||
h3 { font-size: 16.5px; margin: 22px 0 6px; font-weight: 600; }
|
||||
p, li { margin: 6px 0; }
|
||||
ul { padding-left: 24px; }
|
||||
.meta { color: #5e6678; font-size: 14px; }
|
||||
.box { border-top: 1px solid #d8dde3; border-bottom: 1px solid #d8dde3; padding: 14px 0; margin: 18px 0; }
|
||||
.box.widerspruch { border-top: 2px solid #111; border-bottom: 2px solid #111; }
|
||||
table { border-collapse: collapse; width: 100%; font-size: 15px; }
|
||||
td, th { padding: 6px 10px 6px 0; border-bottom: 1px solid #ebeef1; vertical-align: top; text-align: left; }
|
||||
th { font-size: 13px; text-transform: uppercase; letter-spacing: 0.04em; color: #5e6678; background: #f8f9fb; padding-left: 8px; }
|
||||
a { color: #111; text-decoration: underline; text-decoration-color: #d8dde3; text-underline-offset: 2px; }
|
||||
a:hover { background: #f6f8fa; }
|
||||
nav.top { font-size: 14px; margin-bottom: 22px; }
|
||||
.offen { display: inline-block; background: #fcedb9; color: #a82c00; padding: 1px 10px; border-radius: 6px; font-weight: 600; }
|
||||
@media print {
|
||||
@page { size: A4; margin: 18mm 17mm 18mm;
|
||||
@top-left { content: "WEBklar · AVV Terminplaner"; font: 8pt sans-serif; color: #8a93a3; }
|
||||
@bottom-right { content: "Seite " counter(page) " von " counter(pages); font: 8pt sans-serif; color: #8a93a3; } }
|
||||
body { background: #fff; font-size: 10pt; line-height: 1.45; }
|
||||
main { max-width: none; padding: 0; }
|
||||
nav.top { display: none; }
|
||||
h1 { font-size: 20pt; }
|
||||
h2 { font-size: 13pt; margin-top: 16pt; break-after: avoid; }
|
||||
a { color: inherit; text-decoration: none; }
|
||||
.box, tr, ol.abs > li { break-inside: avoid; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<nav class="top"><a href="/dashboard.html">← Kundenportal</a> · <a href="/neu/rechtliches/abo-bedingungen.html">Abo-Bedingungen</a> · <a href="/neu/rechtliches/leistungsbeschreibung-terminplaner-2026-10-16.html">Leistungsbeschreibung Terminplaner</a> · <a href="/neu/rechtliches/avv-terminplaner-2026-10-16.html">AVV Terminplaner</a> · <a href="/datenschutz.html">Datenschutzerklärung</a> · <a href="https://webklar.com/impressum">Impressum</a></nav>
|
||||
|
||||
<div class="brand">WEBKLAR</div>
|
||||
<h1>Auftragsverarbeitungsvertrag Terminplaner (Art. 28 DSGVO)</h1>
|
||||
|
||||
<p class="meta"><strong>Fassung 2026-10-16</strong> · Entwurf – Prüfung durch Kanzlei offen</p>
|
||||
|
||||
<p>zwischen dem Kunden als Verantwortlichem („Kunde“) und der WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, vertreten durch die Gesellschafter Kenso Grimm und Justin Klein, als Auftragsverarbeiter („WEBklar“).</p>
|
||||
|
||||
<h2>Vorbemerkung: Abschluss und Verhältnis zu anderen Verträgen</h2>
|
||||
|
||||
<p>(1) Der Kunde schließt diesen Vertrag, wenn er im Kundenportal die Funktion „Terminplaner“ einschaltet und die dort angezeigte Zeile „Es gelten Leistungsbeschreibung und Auftragsverarbeitung (Fassung 2026-10-16)“ mit dem Klick auf „Einschalten“ bestätigt; diesem Vertrag und der Leistungsbeschreibung „Terminplaner“ („Leistungsbeschreibung“) in der angezeigten Fassung stimmt er damit zu. Das Portal speichert den Wortlaut der angezeigten Zeile, die Fassungen, den Zeitpunkt, das Kundenkonto und die angemeldete Person. Anschließend sendet WEBklar dem Kunden eine E-Mail, die beide Texte in dieser Fassung als PDF enthält und auf sie verlinkt.</p>
|
||||
|
||||
<p>(2) Ein Mitarbeiter von WEBklar darf die Funktion nur einschalten, wenn der Kunde vorher in Textform (etwa im Chat des Kundenportals oder per E-Mail) erklärt hat, dass er diesem Vertrag und der Leistungsbeschreibung in der aktuellen Fassung zustimmt. WEBklar speichert die Fassung, das Datum und den Weg dieser Erklärung, den Zeitpunkt des Einschaltens und den Namen des Mitarbeiters. Der Kunde erhält anschließend eine E-Mail mit diesen beiden Texten als PDF.</p>
|
||||
|
||||
<p>(3) Dieser Vertrag ist ein eigener Vertrag für den Terminplaner. Er ergänzt den Auftragsverarbeitungsvertrag in Anlage 1 der Abo-Bedingungen. Für den Terminplaner geht er Anlage 1 vor; im Übrigen gilt Anlage 1 ergänzend. In Datenschutzfragen geht er den Abo-Bedingungen und der Leistungsbeschreibung vor.</p>
|
||||
|
||||
<h2>1. Gegenstand und Dauer</h2>
|
||||
|
||||
<p>(1) WEBklar betreibt für die öffentliche Website des Kunden ein Buchungs-Widget mit einer Verwaltung im Kundenportal („Terminplaner“) und verarbeitet dabei personenbezogene Daten der Gäste und der Beschäftigten des Kunden im Auftrag des Kunden. Den Umfang beschreibt die Leistungsbeschreibung.</p>
|
||||
|
||||
<p>(2) Der Vertrag beginnt mit dem Einschalten der Funktion. Er endet mit dem Abschalten, spätestens mit dem Ende des Wartungs-Abos; den Terminplaner schaltet WEBklar dann spätestens am folgenden Tag ab (Nr. 8 Abs. 1 der Leistungsbeschreibung). Die Pflichten aus diesem Vertrag gelten fort, bis alle Daten nach Nummer 9 gelöscht sind.</p>
|
||||
|
||||
<h2>2. Art und Zweck der Verarbeitung</h2>
|
||||
|
||||
<p>(1) Art: Erheben über das Widget auf der Website des Kunden, Übermitteln an den Server von WEBklar, Speichern, Verwalten und Anzeigen im Kundenportal, Versenden von E-Mails, Bereitstellen eines Kalender-Links und einer CSV-Datei und Löschen.</p>
|
||||
|
||||
<p>(2) Zwecke: Der Kunde soll Termine seiner Gäste verwalten. Dazu nimmt der Terminplaner Buchungen und Terminanfragen entgegen, verhindert Doppelbuchungen, bestätigt, ändert und sagt Termine ab, erinnert Gäste auf deren Wunsch und benachrichtigt den Kunden. WEBklar versendet die E-Mails im Auftrag des Kunden. Außerdem schützt WEBklar den Terminplaner für den Kunden vor Missbrauch (Mengenbegrenzung, Erkennen automatisierter Buchungen).</p>
|
||||
|
||||
<p>(3) WEBklar verwendet die Daten für keine eigenen Zwecke, auch nicht in anonymisierter Form und nicht für Auswertungen über mehrere Kunden. Es findet keine Werbung und keine Zusammenführung mit anderen Daten statt. WEBklar bildet keine Profile einzelner Gäste. Zählungen ohne Personenbezug, die WEBklar für den sicheren Betrieb braucht (etwa die Zahl der gelöschten Buchungen je Tag oder die Länge der Versandwarteschlange), sind keine eigenen Zwecke.</p>
|
||||
|
||||
<h2>3. Datenarten und Betroffene</h2>
|
||||
|
||||
<p>(1) Daten der Gäste: Name und E-Mail-Adresse; auf Einstellung des Kunden außerdem Telefonnummer, eine Nachricht und Zusatzfelder, die der Kunde im Formular festlegt; die gebuchte Leistung, die Person oder der Platz und der Termin; der Status der Buchung mit den Zeitpunkten (Anlegen, Bestätigung, Annahme, Absage, Widerruf); Absagegrund und Vermerke zu einer Absage oder einem Widerruf (Freitext, auch mit dem Namen aus einer Widerrufserklärung); Preis, Zahlart, Zahlungsstatus und die Angabe, ob als Verbraucher gebucht wurde; der Nachweis der Zustimmungen (Fassung der Gästetexte, Zeitpunkt, Text des Buchungsknopfs, Anrede und Hashes (Prüfsummen) der gezeigten Texte); der Wunsch nach einer Erinnerung; der Zustellstatus der E-Mails an den Gast.</p>
|
||||
|
||||
<p>(2) Daten der Beschäftigten des Kunden: Namen oder Bezeichnungen der Personen oder Plätze, deren Arbeitszeiten und Abwesenheiten sowie ein geheimer Kalender-Link je Person oder Platz.</p>
|
||||
|
||||
<p>(3) Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO) werden nicht verarbeitet. Der Terminplaner ist nicht für den Gesundheitsbereich bestimmt; der Kunde fordert von seinen Gästen keine Gesundheitsangaben (Nummer 11 Abs. 4).</p>
|
||||
|
||||
<p>(4) IP-Adresse und Browser-Kennzeichen (User-Agent) erhält der Server von WEBklar technisch bei jedem Abruf des Widgets und jeder Anfrage. Die Mengenbegrenzung (Missbrauchsschutz) verwendet die IP-Adresse nur im Arbeitsspeicher des Servers; ein Neustart leert sie. Mit der Buchung wird keine IP-Adresse und kein Browser-Kennzeichen gespeichert. In den Server-Protokollen stehen sie 14 Tage (Anlage 2 der Abo-Bedingungen).</p>
|
||||
|
||||
<p>(5) Der Terminplaner setzt keine Cookies und speichert nichts im Browser des Gastes.</p>
|
||||
|
||||
<p>(6) Betroffene: Gäste, die über die Website des Kunden Termine buchen oder anfragen, und Beschäftigte des Kunden, die im Terminplaner geführt werden.</p>
|
||||
|
||||
<h2>4. Weisungen</h2>
|
||||
|
||||
<p>(1) WEBklar verarbeitet die Daten nur auf dokumentierte Weisung des Kunden, auch in Bezug auf eine Übermittlung in ein Drittland oder an eine internationale Organisation. Das gilt nicht, wenn das Recht der Europäischen Union oder Deutschlands WEBklar zu einer Verarbeitung verpflichtet. In diesem Fall teilt WEBklar dem Kunden die Pflicht vorher mit, soweit das Gesetz es nicht verbietet.</p>
|
||||
|
||||
<p>(2) Mit dem Einschalten weist der Kunde WEBklar an, den Terminplaner nach seinen Einstellungen und der Leistungsbeschreibung zu betreiben. Weitere Weisungen ergeben sich aus den Einstellungen im Kundenportal (Leistungen, Personen oder Plätze, Zeiten, Felder des Formulars, Aufbewahrungsdauer, Erinnerungen, Einschalten und Ausschalten). Sonstige Weisungen erteilt der Kunde in Textform, etwa im Chat des Kundenportals. Weisungen, die über diese Einstellungen hinausgehen, setzt WEBklar nur um, wenn das technisch vorgesehen ist; sonst kann der Kunde den Terminplaner abschalten lassen (Nr. 8 Abs. 6 der Leistungsbeschreibung). Die Löschung auf Weisung nach Nummer 9 Abs. 6 setzt WEBklar immer um.</p>
|
||||
|
||||
<p>(3) Hält WEBklar eine Weisung für rechtswidrig, weist es den Kunden unverzüglich darauf hin. WEBklar darf die Ausführung aussetzen, bis der Kunde die Weisung ändert oder die Rechtslage geklärt ist.</p>
|
||||
|
||||
<h2>5. Vertraulichkeit</h2>
|
||||
|
||||
<p>WEBklar setzt nur Personen ein, die zur Vertraulichkeit verpflichtet sind oder einer gesetzlichen Verschwiegenheitspflicht unterliegen.</p>
|
||||
|
||||
<h2>6. Technische und organisatorische Maßnahmen</h2>
|
||||
|
||||
<p>(1) WEBklar trifft die Maßnahmen nach Art. 32 DSGVO, die in der Anlage zu diesem Vertrag beschrieben sind. WEBklar darf sie an den Stand der Technik anpassen, sofern das Schutzniveau nicht sinkt.</p>
|
||||
|
||||
<p>(2) Für den Server gelten ergänzend die Maßnahmen der Anlage 2 der Abo-Bedingungen, soweit die Anlage zu diesem Vertrag nichts anderes regelt.</p>
|
||||
|
||||
<h2>7. Unterauftragsverarbeiter</h2>
|
||||
|
||||
<p>(1) Der Kunde genehmigt den Einsatz der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, für den Betrieb des Servers, auf dem der Terminplaner läuft, und für dessen Sicherung (Rechenzentrum Falkenstein, Deutschland) sowie für den Versand der Buchungs-E-Mails über den SMTP-Dienst des Webhostings (Rechenzentrum Nürnberg, Deutschland). Eine Übermittlung in Drittländer findet nicht statt.</p>
|
||||
|
||||
<p>(2) Will WEBklar einen weiteren Unterauftragsverarbeiter hinzuziehen oder einen ersetzen, teilt es das dem Kunden mindestens vier Wochen vorher in Textform mit. Der Kunde kann innerhalb von zwei Wochen nach Zugang der Mitteilung aus wichtigem datenschutzrechtlichem Grund in Textform widersprechen. WEBklar setzt den neuen Unterauftragsverarbeiter nicht vor Ablauf dieser Frist ein. Kündigt ein Unterauftragsverarbeiter von WEBklar eine eigene Änderung mit kürzerer Frist an, informiert WEBklar den Kunden unverzüglich, nachdem die Ankündigung eingegangen ist; der Kunde kann dann unverzüglich widersprechen. Einigen sich die Parteien nicht, kann der Kunde den Terminplaner abschalten lassen und WEBklar ihn für diese Website zu dem Zeitpunkt beenden, zu dem der neue Unterauftragsverarbeiter eingesetzt wird. Die Daten werden dann nach Nummer 9 gelöscht, auf Weisung des Kunden früher (Nummer 9 Abs. 6).</p>
|
||||
|
||||
<p>(3) WEBklar verpflichtet jeden Unterauftragsverarbeiter vertraglich auf die Datenschutzpflichten nach Art. 28 Abs. 4 DSGVO. Bei der Hetzner Online GmbH ist das deren Vertrag zur Auftragsverarbeitung, den WEBklar abgeschlossen hat. Kommt ein Unterauftragsverarbeiter seinen Datenschutzpflichten nicht nach, haftet WEBklar dem Kunden für die Einhaltung dieser Pflichten (Art. 28 Abs. 4 Satz 2 DSGVO).</p>
|
||||
|
||||
<p>(4) Die Zahlung der Einrichtung über Stripe betrifft nur den Vertrag zwischen dem Kunden und WEBklar. Daten von Gästen oder Beschäftigten des Kunden gehen nicht an Stripe. Stripe ist deshalb kein Unterauftragsverarbeiter dieses Vertrags.</p>
|
||||
|
||||
<h2>8. Unterstützung des Kunden, Betroffenenrechte</h2>
|
||||
|
||||
<p>(1) WEBklar leitet Anfragen von Gästen und Beschäftigten, die an WEBklar gerichtet sind, unverzüglich an den Kunden weiter. WEBklar unterstützt ihn mit geeigneten technischen und organisatorischen Maßnahmen bei der Beantwortung.</p>
|
||||
|
||||
<p>(2) Dafür bietet das Kundenportal die Aktion „Gastdaten löschen“ und den Export der Termine als CSV-Datei. Mit „Gastdaten löschen“ leert der Kunde Name, E-Mail-Adresse, Telefonnummer, Nachricht und Zusatzfelder einer Buchung; Status und Zeiten bleiben. Dabei löscht WEBklar die E-Mails zu dieser Buchung, die noch auf den Versand warten, und die Links des Gastes werden ungültig. E-Mails, deren Versand endgültig fehlgeschlagen ist, behalten Empfängeradresse und Inhalt, bis WEBklar sie nach Nummer 9 Abs. 4 löscht (48 Stunden nach dem Fehlschlag). Eine noch nicht bestätigte Buchung verfällt von selbst (Nummer 9 Abs. 1).</p>
|
||||
|
||||
<p>(3) Verletzungen des Schutzes personenbezogener Daten meldet WEBklar dem Kunden unverzüglich, möglichst innerhalb von 24 Stunden nach Kenntnis, mit den Informationen, die zu diesem Zeitpunkt vorliegen.</p>
|
||||
|
||||
<p>(4) WEBklar unterstützt den Kunden bei seinen Pflichten aus Art. 32 bis 36 DSGVO. Dazu gehören die Angaben, die er für sein Verzeichnis von Verarbeitungstätigkeiten und für eine etwaige Datenschutz-Folgenabschätzung braucht.</p>
|
||||
|
||||
<p>(5) Die Buchung speichert als Nachweis der Zustimmungen die Fassung der Gästetexte, den Zeitpunkt, den Text des Buchungsknopfs, die Anrede und Hashes der übrigen Texte, die der Gast gesehen hat, nicht diese Texte selbst. Damit sich der Wortlaut nachweisen lässt, bewahrt WEBklar jede Fassung der Gästetexte (Hinweis am Formular, Texte der Haken, Datenschutz-Absatz und Widerrufsbelehrung für Gäste) mit dem Zeitraum ihrer Verwendung auf, bis drei Jahre nach dem Ende des Terminplaners. WEBklar stellt sie dem Kunden auf Verlangen zur Verfügung.</p>
|
||||
|
||||
<p>(6) Die Gäste informiert der Kunde (Art. 13 DSGVO; Nummer 11 Abs. 2).</p>
|
||||
|
||||
<h2>9. Löschung und Rückgabe</h2>
|
||||
|
||||
<p>(1) Nicht bestätigte Buchungen löscht WEBklar 48 Stunden nach Ablauf der Bestätigungsfrist von 30 Minuten.</p>
|
||||
|
||||
<p>(2) Abgesagte und abgelehnte Termine löscht WEBklar 30 Tage nach der Absage oder Ablehnung. Terminanfragen, die der Kunde bis zum angefragten Terminbeginn nicht beantwortet, behandelt der Terminplaner als abgelehnt; die 30 Tage laufen dann ab diesem Zeitpunkt.</p>
|
||||
|
||||
<p>(3) Bestätigte und erledigte Termine löscht WEBklar nach der Aufbewahrungsdauer, gerechnet ab dem Ende des Termins. Der Kunde stellt sie im Kundenportal ein; die Vorgabe sind 90 Tage, einstellbar sind 14 bis 365 Tage.</p>
|
||||
|
||||
<p>(4) In der Versandwarteschlange für E-Mails leert WEBklar die Empfängeradresse und den Inhalt einer E-Mail nach dem Versand und löscht den Eintrag 7 Tage nach dem Versand. E-Mails, die nicht zugestellt werden können, löscht WEBklar mit Empfängeradresse und Inhalt 48 Stunden nach dem endgültigen Fehlschlag.</p>
|
||||
|
||||
<p>(5) Der Löschlauf läuft täglich; gelöscht wird frühestens nach Ablauf der jeweiligen Frist. Ist an einem Tag mehr zu löschen, als ein Lauf schafft, folgt der Rest im nächsten Lauf.</p>
|
||||
|
||||
<p>(6) Ist der Terminplaner abgeschaltet oder das Wartungs-Abo beendet, löscht WEBklar alle Termindaten und die Einstellungen 30 Tage danach. Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Einstellungen bestehen (zum Beispiel Betriebsangaben, Namen der Leistungen, die ersten acht Namen der Personen oder Plätze und die Wünsche des Kunden zum Einbau) und enthält keine Daten von Gästen; dafür gilt die Datenschutzerklärung von WEBklar (Abschnitt 6). Bis zur Löschung kann der Kunde die Termine im Kundenportal als CSV-Datei exportieren. Beginnt innerhalb dieser Frist wieder ein Wartungs-Abo, schaltet WEBklar den Terminplaner mit den vorhandenen Daten und Einstellungen wieder ein (Nr. 8 Abs. 4 der Leistungsbeschreibung). Früher löscht der Kunde einzelne Gastdaten selbst mit der Aktion „Gastdaten löschen“ (Nummer 8 Abs. 2); alle Daten löscht WEBklar auf Weisung des Kunden in Textform unverzüglich.</p>
|
||||
|
||||
<p>(7) Entwürfe aus dem Fragebogen, aus denen die Funktion nicht eingeschaltet wurde, löscht WEBklar 90 Tage nach der letzten Änderung.</p>
|
||||
|
||||
<p>(8) Die Daten sind in der täglichen Vollsicherung des Servers enthalten (§ 3 Abs. 5 der Abo-Bedingungen). Gelöschte Daten bleiben dort bis zu sieben Tage erhalten und werden dann überschrieben. Wird eine Sicherung zurückgespielt, holt der tägliche Löschlauf die fälligen Löschungen nach; Löschungen auf Weisung des Kunden wiederholt WEBklar.</p>
|
||||
|
||||
<p>(9) Gesetzliche Aufbewahrungspflichten bleiben unberührt.</p>
|
||||
|
||||
<h2>10. Nachweise und Kontrollen</h2>
|
||||
|
||||
<p>WEBklar stellt dem Kunden alle Informationen zur Verfügung, die zum Nachweis der Pflichten aus Art. 28 DSGVO nötig sind, und ermöglicht Überprüfungen einschließlich Inspektionen durch den Kunden oder einen von ihm beauftragten, zur Verschwiegenheit verpflichteten Prüfer. Sie finden nach Anmeldung mit angemessener Frist (in der Regel 14 Tage) zu den Servicezeiten statt, ohne den Betrieb unzumutbar zu stören. Für das Rechenzentrum genügen in der Regel die Nachweise des Betreibers (Zertifizierung der Hetzner Online GmbH nach ISO/IEC 27001). Für mehr als eine Vor-Ort-Kontrolle pro Jahr kann WEBklar eine angemessene Vergütung verlangen.</p>
|
||||
|
||||
<h2>11. Pflichten des Kunden</h2>
|
||||
|
||||
<p>(1) Der Kunde ist Verantwortlicher für die Verarbeitung der Daten seiner Gäste und seiner Beschäftigten im Terminplaner.</p>
|
||||
|
||||
<p>(2) Der Kunde informiert seine Gäste nach Art. 13 DSGVO. Dazu nimmt er den Datenschutz-Absatz für die Online-Terminbuchung in seine Datenschutzerklärung auf (Nr. 4 Abs. 9 der Leistungsbeschreibung) und lässt den Datenschutz-Hinweis am Buchungsformular unverändert.</p>
|
||||
|
||||
<p>(3) Der Kunde informiert seine Beschäftigten darüber, dass ihre Namen oder Bezeichnungen, ihre Arbeitszeiten und Abwesenheiten im Terminplaner stehen und dass es einen geheimen Kalender-Link gibt (Nr. 4 Abs. 7 der Leistungsbeschreibung).</p>
|
||||
|
||||
<p>(4) Der Kunde setzt den Terminplaner nicht im Gesundheitsbereich ein und fordert von seinen Gästen keine Angaben zu ihrer Gesundheit, auch nicht über Zusatzfelder oder das Nachrichtenfeld (Nr. 4 Abs. 3 der Leistungsbeschreibung).</p>
|
||||
|
||||
<p>(5) Der Kunde verändert den Haken für die Erinnerung und die Texte der übrigen Haken nicht (Nr. 4 Abs. 6 der Leistungsbeschreibung). Erinnerungen versendet der Terminplaner nur, wenn der Gast den Haken gesetzt hat.</p>
|
||||
|
||||
<h2>12. Haftung, Änderungen, Schlussbestimmungen</h2>
|
||||
|
||||
<p>(1) Gegenüber betroffenen Personen gilt Art. 82 DSGVO. Für die Haftung zwischen Kunde und WEBklar gilt § 13 der Abo-Bedingungen.</p>
|
||||
|
||||
<p>(2) Eine neue Fassung dieses Vertrags oder der Leistungsbeschreibung, auch wegen geänderter Datenerhebung, teilt WEBklar dem Kunden mindestens sechs Wochen vor ihrem Wirksamwerden in Textform mit. Sie wird wirksam, wenn der Kunde ihr im Kundenportal oder in Textform zustimmt. Stimmt er nicht zu, gilt die bisherige Fassung weiter.</p>
|
||||
|
||||
<p>(3) Ist die neue Fassung nötig, weil ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form nicht mehr zulässt, und stimmt der Kunde ihr nicht zu, darf WEBklar den Terminplaner für seine Website nach Nr. 9 Abs. 2 der Leistungsbeschreibung beenden oder nach Nr. 9 Abs. 3 der Leistungsbeschreibung aussetzen. Das Wartungs-Abo bleibt davon unberührt.</p>
|
||||
|
||||
<p>(4) Im Übrigen gilt § 16 der Abo-Bedingungen.</p>
|
||||
|
||||
<h2>Anlage: Technische und organisatorische Maßnahmen des Terminplaners (Art. 32 DSGVO)</h2>
|
||||
|
||||
<table>
|
||||
<thead><tr><th>Bereich</th><th>Maßnahmen</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>Übertragung</td><td>Das Widget, die Seiten der Gäste und das Kundenportal werden nur über TLS (HTTPS) übertragen. Der Einbau-Code verweist auf eine Adresse mit https://.</td></tr>
|
||||
<tr><td>Speicher im Browser</td><td>Es werden keine Cookies gesetzt, und der Terminplaner speichert nichts im Browser des Gastes (kein lokaler Speicher, kein Sitzungsspeicher). Das Widget sendet seine Anfragen ohne Cookies, liest weder den Referrer noch die Zeitzone des Browsers und ruft nur die Schnittstelle des Terminplaners auf.</td></tr>
|
||||
<tr><td>Zugriffskontrolle</td><td>Das Kundenportal ist nur nach Anmeldung erreichbar; jeder Kunde sieht nur die Daten seiner eigenen Projekte. Die öffentliche Schnittstelle des Widgets liefert nur für einen eingeschalteten Terminplaner Daten und ist über den Website-Schlüssel (Site-Key) adressiert; Buchungen nimmt sie nur von Seiten des Kunden und des Portals an. Der Kalender-Link enthält ein geheimes Token, das mit einem Vergleich in konstanter Zeit geprüft wird.</td></tr>
|
||||
<tr><td>Missbrauchsschutz</td><td>Ein verstecktes Feld (Honeypot), eine Mindestzeit bis zum Absenden des Formulars mit signiertem Formular-Token und eine Mindestantwortzeit schützen vor automatisierten Buchungen. Grenzen je IP-Adresse, je Website und je E-Mail-Adresse sowie das Mailbudget je Stunde (insgesamt und je Funktion) gelten nur im Arbeitsspeicher; ein Neustart leert sie.</td></tr>
|
||||
<tr><td>Datenminimierung</td><td>Pflicht sind immer nur Name und E-Mail-Adresse; ob Telefonnummer, Nachricht und Zusatzfelder abgefragt werden, legt der Kunde fest. Mit der Buchung werden keine IP-Adresse und kein Browser-Kennzeichen gespeichert. Der Nachweis der Zustimmungen besteht aus der Fassung der Gästetexte, dem Zeitpunkt, dem Text des Buchungsknopfs, der Anrede und Hashes der übrigen gezeigten Texte, nicht aus diesen Texten. Nach dem Versand einer E-Mail werden Empfängeradresse und Inhalt in der Warteschlange geleert.</td></tr>
|
||||
<tr><td>Trennung</td><td>Alle Buchungen, Belegungen und Warteschlangeneinträge tragen die Kennung der Funktion (eines Terminplaners je Website-Projekt); das Kundenportal und die öffentliche Schnittstelle lesen immer nur die Daten der jeweiligen Funktion. Die Hintergrundläufe (Löschung, Versand der E-Mails, Erinnerungen) gehen die Dokumente aller Funktionen nacheinander durch und schreiben jeweils nur in das Dokument, um das es geht. Die Einstellungen liegen je Funktion in einem eigenen Dokument.</td></tr>
|
||||
<tr><td>Löschung</td><td>Täglicher Löschlauf für Buchungen, Belegungen, Warteschlange und Entwürfe nach den Fristen der Nummer 9; Termindaten und Einstellungen abgeschalteter Funktionen 30 Tage nach dem Abschalten; das Einrichtungs-Ticket bleibt als Arbeitsnachweis mit einer Kurzfassung der Einstellungen bestehen und enthält keine Daten von Gästen (Nummer 9 Abs. 6). Vor der Löschung einer Buchung, einer abgeschalteten Funktion oder eines nicht bestellten Fragebogens wird der Zustand frisch gelesen; bei einem Lesefehler wird nichts gelöscht.</td></tr>
|
||||
<tr><td>Protokolle</td><td>Die Programmprotokolle des Terminplaners und der Tagesbericht enthalten nur Zählungen und Buchungsnummern, keine Namen, E-Mail-Adressen oder Nachrichten von Gästen. Server-Protokolle des Webservers werden nach 14 Tagen gelöscht (Anlage 2 der Abo-Bedingungen).</td></tr>
|
||||
<tr><td>Datensicherung</td><td>Tägliche Vollsicherung des Servers durch Hetzner; verfügbar sind die letzten sieben Tagessicherungen (§ 3 Abs. 5 der Abo-Bedingungen).</td></tr>
|
||||
<tr><td>Rechenzentrum</td><td>Hetzner Online GmbH, Rechenzentren Falkenstein und Nürnberg (Deutschland). Hetzner ist nach ISO/IEC 27001 zertifiziert; der Geltungsbereich umfasst die Hosting-Dienstleistungen und die Rechenzentren.</td></tr>
|
||||
<tr><td>Organisation</td><td>Verpflichtung aller Beteiligten auf Vertraulichkeit, Ablauf für Datenschutzverletzungen (Nummer 8 Abs. 3), Archiv der Fassungen der Gästetexte (Nummer 8 Abs. 5), Überprüfung der Maßnahmen mindestens einmal im Jahr.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,214 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="webklar-fassung" content="2026-10-16">
|
||||
<meta name="robots" content="noindex">
|
||||
<title>Auftragsverarbeitungsvertrag Buchungskalender – WEBklar</title>
|
||||
<style>
|
||||
:root { color-scheme: light; }
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; background: #fff; color: #1a1f36; font: 16px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; font-variant-numeric: tabular-nums; }
|
||||
main { max-width: 780px; margin: 0 auto; padding: 44px 20px 80px; }
|
||||
.brand { font-weight: 600; letter-spacing: 0.12em; font-size: 13px; color: #111; }
|
||||
h1 { font-size: 30px; line-height: 1.2; margin: 6px 0 6px; font-weight: 600; }
|
||||
h2 { font-size: 20px; margin: 36px 0 10px; padding-top: 14px; border-top: 1px solid #ebeef1; font-weight: 600; }
|
||||
h3 { font-size: 16.5px; margin: 22px 0 6px; font-weight: 600; }
|
||||
p, li { margin: 6px 0; }
|
||||
ul { padding-left: 24px; }
|
||||
.meta { color: #5e6678; font-size: 14px; }
|
||||
.box { border-top: 1px solid #d8dde3; border-bottom: 1px solid #d8dde3; padding: 14px 0; margin: 18px 0; }
|
||||
.box.widerspruch { border-top: 2px solid #111; border-bottom: 2px solid #111; }
|
||||
table { border-collapse: collapse; width: 100%; font-size: 15px; }
|
||||
td, th { padding: 6px 10px 6px 0; border-bottom: 1px solid #ebeef1; vertical-align: top; text-align: left; }
|
||||
th { font-size: 13px; text-transform: uppercase; letter-spacing: 0.04em; color: #5e6678; background: #f8f9fb; padding-left: 8px; }
|
||||
a { color: #111; text-decoration: underline; text-decoration-color: #d8dde3; text-underline-offset: 2px; }
|
||||
a:hover { background: #f6f8fa; }
|
||||
nav.top { font-size: 14px; margin-bottom: 22px; }
|
||||
.offen { display: inline-block; background: #fcedb9; color: #a82c00; padding: 1px 10px; border-radius: 6px; font-weight: 600; }
|
||||
@media print {
|
||||
@page { size: A4; margin: 18mm 17mm 18mm;
|
||||
@top-left { content: "WEBklar · AVV Buchungskalender"; font: 8pt sans-serif; color: #8a93a3; }
|
||||
@bottom-right { content: "Seite " counter(page) " von " counter(pages); font: 8pt sans-serif; color: #8a93a3; } }
|
||||
body { background: #fff; font-size: 10pt; line-height: 1.45; }
|
||||
main { max-width: none; padding: 0; }
|
||||
nav.top { display: none; }
|
||||
h1 { font-size: 20pt; }
|
||||
h2 { font-size: 13pt; margin-top: 16pt; break-after: avoid; }
|
||||
a { color: inherit; text-decoration: none; }
|
||||
.box, tr, ol.abs > li { break-inside: avoid; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<nav class="top"><a href="/dashboard.html">← Kundenportal</a> · <a href="/neu/rechtliches/abo-bedingungen.html">Abo-Bedingungen</a> · <a href="/neu/rechtliches/leistungsbeschreibung-unterkunft-2026-10-16.html">Leistungsbeschreibung Buchungskalender</a> · <a href="/neu/rechtliches/avv-unterkunft-2026-10-16.html">AVV Buchungskalender</a> · <a href="/datenschutz.html">Datenschutzerklärung</a> · <a href="https://webklar.com/impressum">Impressum</a></nav>
|
||||
|
||||
<div class="brand">WEBKLAR</div>
|
||||
<h1>Auftragsverarbeitungsvertrag Buchungskalender (Art. 28 DSGVO)</h1>
|
||||
|
||||
<p class="meta"><strong>Fassung 2026-10-16</strong> · Entwurf – Prüfung durch Kanzlei offen</p>
|
||||
|
||||
<p>zwischen dem Kunden als Verantwortlichem („Kunde“; in der Leistungsbeschreibung „Vermieter“) und der WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, vertreten durch die Gesellschafter Kenso Grimm und Justin Klein, als Auftragsverarbeiter („WEBklar“).</p>
|
||||
|
||||
<h2>Vorbemerkung: Abschluss und Verhältnis zu anderen Verträgen</h2>
|
||||
|
||||
<p>(1) Der Kunde schließt diesen Vertrag, wenn er im Kundenportal die Funktion „Buchungskalender“ einschaltet und die dort angezeigte Zeile „Es gelten Leistungsbeschreibung und Auftragsverarbeitung (Fassung 2026-10-16)“ mit dem Klick auf „Einschalten“ bestätigt; diesem Vertrag und der Leistungsbeschreibung „Buchungskalender“ („Leistungsbeschreibung“) in der angezeigten Fassung stimmt er damit zu. Das Portal speichert den Wortlaut der angezeigten Zeile, die Fassungen, den Zeitpunkt, das Kundenkonto und die angemeldete Person. Anschließend sendet WEBklar dem Kunden eine E-Mail, die beide Texte in dieser Fassung als PDF enthält und auf sie verlinkt.</p>
|
||||
|
||||
<p>(2) Ein Mitarbeiter von WEBklar darf die Funktion auch ohne Bestellung im Portal einschalten (zum Beispiel für einen Kunden, dem WEBklar sie als Teil seines Website-Projekts bereitstellt), aber nur, wenn der Kunde vorher in Textform (etwa im Chat des Kundenportals oder per E-Mail) erklärt hat, dass er diesem Vertrag und der Leistungsbeschreibung in der aktuellen Fassung zustimmt. Das Portal verlangt dafür das Datum und den Weg der Erklärung; fehlt eine Angabe, schaltet es nicht ein. WEBklar speichert vor dem Einschalten einen Zustimmungsnachweis mit der Fassung der beiden Texte, dem Datum und dem Weg dieser Erklärung, dem Zeitpunkt des Einschaltens und dem Namen des Mitarbeiters (ohne IP-Adresse und Browser-Kennzeichen, weil der Kunde nicht im Portal zugestimmt hat). Der Kunde erhält anschließend eine E-Mail mit diesen beiden Texten als PDF.</p>
|
||||
|
||||
<p>(3) Dieser Vertrag ist ein eigener Vertrag für den Buchungskalender. Er ergänzt den Auftragsverarbeitungsvertrag in Anlage 1 der Abo-Bedingungen. Für den Buchungskalender geht er Anlage 1 vor; im Übrigen gilt Anlage 1 ergänzend. In Datenschutzfragen geht er den Abo-Bedingungen und der Leistungsbeschreibung vor.</p>
|
||||
|
||||
<p>(4) Die Rollen: Der Kunde ist Verantwortlicher für die personenbezogenen Daten seiner Gäste im Buchungskalender. WEBklar ist sein Auftragsverarbeiter. Stripe ist nicht Auftragsverarbeiter von WEBklar, sondern Vertragspartner des Kunden (Nummer 7 Abs. 4). Die Plattformen, deren Kalender der Kunde einbindet (zum Beispiel Booking.com oder Airbnb), sind weder Auftragsverarbeiter noch Unterauftragsverarbeiter von WEBklar.</p>
|
||||
|
||||
<h2>1. Gegenstand und Dauer</h2>
|
||||
|
||||
<p>(1) WEBklar betreibt für die öffentliche Website des Kunden ein Buchungs-Widget mit einer Buchungsseite und einer Verwaltung im Kundenportal („Buchungskalender“) und verarbeitet dabei personenbezogene Daten der Gäste des Kunden im Auftrag des Kunden. Den Umfang beschreibt die Leistungsbeschreibung.</p>
|
||||
|
||||
<p>(2) Der Vertrag beginnt mit dem Einschalten der Funktion. Er endet mit dem Abschalten, spätestens mit dem Ende des Wartungs-Abos; den Buchungskalender schaltet WEBklar dann spätestens am folgenden Tag ab (Nr. 10 Abs. 1 der Leistungsbeschreibung). Die Pflichten aus diesem Vertrag gelten fort, bis alle Daten nach Nummer 9 gelöscht sind.</p>
|
||||
|
||||
<h2>2. Art und Zweck der Verarbeitung</h2>
|
||||
|
||||
<p>(1) Art: Übermitteln der Anfrage und der Buchung vom Widget und von der Buchungsseite an den Server von WEBklar, Speichern, Verwalten und Anzeigen im Kundenportal, Berechnen des Preises, Erzeugen von Rechnungen und Stornorechnungen im Namen des Kunden (Nr. 3 Abs. 6 der Leistungsbeschreibung), Abgleichen von Kalendern, Versenden von E-Mails, Übernehmen von Zahlungsstatus aus Stripe, Weitergeben von Erstattungen an Stripe im Auftrag des Kunden, Anonymisieren und Löschen.</p>
|
||||
|
||||
<p>(2) Zwecke: Der Kunde soll Anfragen und Buchungen seiner Gäste abwickeln. Dazu nimmt der Buchungskalender Anfragen entgegen, reserviert Zeiträume nach der Freigabe durch den Kunden, nimmt die verbindliche Buchung des Gastes entgegen, stellt die Rechnung aus, gleicht Online-Zahlungen mit Stripe ab, bildet Zahlungen vor Ort ab, bildet Stornierungen ab und benachrichtigt den Kunden. WEBklar versendet die E-Mails im Auftrag des Kunden. Außerdem schützt WEBklar den Buchungskalender für den Kunden vor Missbrauch (Mengenbegrenzung, Begrenzung der Anfragen je IP-Adresse, Erkennen automatisierter Anfragen).</p>
|
||||
|
||||
<p>(3) WEBklar verwendet die Daten für keine eigenen Zwecke, auch nicht in anonymisierter Form und nicht für Auswertungen über mehrere Kunden. Es findet keine Werbung und keine Zusammenführung mit anderen Daten statt. WEBklar bildet keine Profile einzelner Gäste. Zählungen ohne Personenbezug, die WEBklar für den sicheren Betrieb braucht (etwa die Zahl der anonymisierten Anfragen je Tag oder die Länge der Versandwarteschlange), sind keine eigenen Zwecke.</p>
|
||||
|
||||
<h2>3. Datenarten und Betroffene</h2>
|
||||
|
||||
<p>(1) Daten der Gäste, die WEBklar aus dem Widget, von der Buchungsseite und aus Stripe erhält und speichert:</p>
|
||||
|
||||
<ul>
|
||||
<li>bei der Anfrage: Name, E-Mail-Adresse, auf Wunsch Firma, Telefonnummer und eine Nachricht, die gewünschte Unterkunft, der Zeitraum, die Zahl der Personen, die Anfragenummer, ein Hashwert der IP-Adresse (Absatz 4) und der Zeitpunkt;</li>
|
||||
<li>bei der Freigabe: die Posten und Summen des Preises (Text, Menge, Einzelbetrag, Steuersatz), die Reservierungsfrist, eine Nachricht des Kunden an den Gast, die Stornobedingungen, die galten, und die Fassung der Rechtstexte, der der Gast gesehen hat, jeweils als Schnappschuss;</li>
|
||||
<li>bei der Buchung: die Rechnungsadresse (Name, Firma, Straße, Postleitzahl, Ort, Land), die Zahlart, der Zeitpunkt der Buchung und die Rechnungsnummer;</li>
|
||||
<li>bei der Zahlung und der Erstattung: Status, Zeitpunkt, Art (online über Stripe oder vor Ort), die Kennungen der Zahlung bei Stripe, erstattete Beträge und der Grund einer Stornierung;</li>
|
||||
<li>die Rechnung und die Stornorechnung als unveränderlicher Schnappschuss (Angaben des Ausstellers, Empfänger, Unterkunft, Zeitraum, Posten, Summen, Steuerart, Bezug), aus dem WEBklar das PDF bei jedem Abruf erzeugt;</li>
|
||||
<li>ein Hashwert des geheimen Zugangs zur Buchungsseite (der Link in der E-Mail wird nur als Prüfsumme gespeichert);</li>
|
||||
<li>der Zustellstatus der E-Mails an den Gast.</li>
|
||||
</ul>
|
||||
|
||||
<p><strong>Kartendaten des Gastes erhält und speichert WEBklar nicht;</strong> die gibt der Gast nur bei Stripe ein. Online-Zahlungen sind derzeit nur per Karte (einschließlich Apple Pay und Google Pay) möglich, nicht per SEPA-Lastschrift. Die Bankverbindung des Kunden selbst (Angabe auf der Rechnung) und seine Firmen- und Steuerangaben gehören nicht zu den Gästedaten.</p>
|
||||
|
||||
<p>(2) Von Kalendern anderer Plattformen übernimmt WEBklar nur die belegten Zeiträume (Datum von und bis), nicht Namen, Beschreibungen oder andere Angaben der dortigen Buchungen, auch wenn der Kalender sie enthält. Die iCal-Adresse, die WEBklar dem Kunden bereitstellt, enthält nur belegte Nächte, keine Angaben zu Gästen. Wer die geheime Adresse kennt, sieht die belegten Nächte; der Kunde kann sie jederzeit erneuern.</p>
|
||||
|
||||
<p>(3) Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO) werden nicht verarbeitet. Der Kunde fordert von seinen Gästen keine entsprechenden Angaben und bittet sie, solche Angaben nicht in das Nachrichtenfeld zu schreiben (Nummer 11 Abs. 4). Meldedaten nach dem Bundesmeldegesetz (Meldeschein) und Daten zur Kurtaxe verarbeitet der Buchungskalender nicht.</p>
|
||||
|
||||
<p>(4) IP-Adresse und Browser-Kennzeichen (User-Agent) erhält der Server von WEBklar technisch bei jedem Abruf des Widgets und jeder Anfrage. Die Mengenbegrenzung (Missbrauchsschutz) verwendet die IP-Adresse im Arbeitsspeicher des Servers; ein Neustart leert sie. Mit der Anfrage werden weder eine IP-Adresse im Klartext noch ein Browser-Kennzeichen gespeichert, sondern nur ein <strong>Hashwert der IP-Adresse („IP-Hash“)</strong>: ein SHA-256-Wert, den WEBklar mit einem geheimen Schlüssel des Servers salzt. Der IP-Hash ist ein pseudonymes technisches Datum und bleibt ein personenbezogenes Datum, weil WEBklar mit dem Schlüssel für dieselbe IP-Adresse denselben Wert berechnen kann; ohne den Schlüssel lässt sich aus ihm keine IP-Adresse ableiten. WEBklar verwendet ihn ausschließlich zum Missbrauchsschutz. Er dient nicht dazu, Gäste zu identifizieren, wiederzuerkennen oder zu profilieren, wird dem Kunden weder angezeigt noch exportiert und steht nur im Datensatz der Anfrage. WEBklar entfernt ihn aus dem Datensatz, sobald der Kunde die Anfrage freigibt oder ablehnt, spätestens nach 30 Tagen (Nummer 9 Abs. 1). In den Server-Protokollen stehen IP-Adresse und Browser-Kennzeichen 14 Tage (Anlage 2 der Abo-Bedingungen).</p>
|
||||
|
||||
<p>(5) Der Buchungskalender setzt keine Cookies. Das Widget speichert nichts im Browser. Auch die Buchungsseite legt weder Cookies noch Daten im lokalen oder im Sitzungsspeicher des Browsers ab.</p>
|
||||
|
||||
<p>(6) Betroffene: Gäste, die über die Website des Kunden Anfragen stellen oder buchen, und Personen, deren Angaben der Gast in der Anfrage oder Buchung macht (zum Beispiel Mitreisende oder abweichende Rechnungsempfänger). Beschäftigte des Kunden sind von diesem Vertrag nicht betroffen.</p>
|
||||
|
||||
<h2>4. Weisungen</h2>
|
||||
|
||||
<p>(1) WEBklar verarbeitet die Daten nur auf dokumentierte Weisung des Kunden, auch in Bezug auf eine Übermittlung in ein Drittland oder an eine internationale Organisation. Das gilt nicht, wenn das Recht der Europäischen Union oder Deutschlands WEBklar zu einer Verarbeitung verpflichtet. In diesem Fall teilt WEBklar dem Kunden die Pflicht vorher mit, soweit das Gesetz es nicht verbietet.</p>
|
||||
|
||||
<p>(2) Mit dem Einschalten weist der Kunde WEBklar an, den Buchungskalender nach seinen Einstellungen und der Leistungsbeschreibung zu betreiben, in seinem Namen Rechnungen auszustellen und Zahlungen, die nach einer Stornierung oder nach dem Ablauf einer Reservierung eingehen, sowie zweite Zahlungen auf eine bereits bezahlte Buchung ohne weitere Anweisung in voller Höhe über sein Stripe-Konto zu erstatten (dauerhafte Weisung; der Stand der Buchung ändert sich dabei nicht). Bei einer Stornierung schließt WEBklar eine noch offene Zahlungsseite bei Stripe. Weitere Weisungen ergeben sich aus den Einstellungen und Aktionen im Kundenportal (Unterkünfte, Preise, Sperren, Kalenderverbindungen, Freigabe und Ablehnung von Anfragen, Zahlungseingang vor Ort, Stornierungen und Erstattungen, Online-Zahlung, Öffnen und Schließen des Buchungskalenders). Sonstige Weisungen erteilt der Kunde in Textform, etwa im Chat des Kundenportals. Weisungen, die über diese Einstellungen hinausgehen, setzt WEBklar nur um, wenn das technisch vorgesehen ist; sonst kann der Kunde den Buchungskalender abschalten lassen (Nr. 10 Abs. 7 der Leistungsbeschreibung). Die Anonymisierung und Löschung auf Weisung nach Nummer 8 Abs. 2 und Nummer 9 Abs. 6 setzt WEBklar immer um, soweit Aufbewahrungspflichten dem nicht entgegenstehen (Nummer 9 Abs. 8).</p>
|
||||
|
||||
<p>(3) Hält WEBklar eine Weisung für rechtswidrig, weist es den Kunden unverzüglich darauf hin. WEBklar darf die Ausführung aussetzen, bis der Kunde die Weisung ändert oder die Rechtslage geklärt ist.</p>
|
||||
|
||||
<h2>5. Vertraulichkeit</h2>
|
||||
|
||||
<p>WEBklar setzt nur Personen ein, die zur Vertraulichkeit verpflichtet sind oder einer gesetzlichen Verschwiegenheitspflicht unterliegen.</p>
|
||||
|
||||
<h2>6. Technische und organisatorische Maßnahmen</h2>
|
||||
|
||||
<p>(1) WEBklar trifft die Maßnahmen nach Art. 32 DSGVO, die in der Anlage zu diesem Vertrag beschrieben sind. WEBklar darf sie an den Stand der Technik anpassen, sofern das Schutzniveau nicht sinkt.</p>
|
||||
|
||||
<p>(2) Für den Server gelten ergänzend die Maßnahmen der Anlage 2 der Abo-Bedingungen, soweit die Anlage zu diesem Vertrag nichts anderes regelt.</p>
|
||||
|
||||
<h2>7. Unterauftragsverarbeiter</h2>
|
||||
|
||||
<p>(1) Der Kunde genehmigt den Einsatz der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, für den Betrieb des Servers, auf dem der Buchungskalender läuft, und für dessen Sicherung (Rechenzentrum Falkenstein, Deutschland) sowie für den Versand der E-Mails über den SMTP-Dienst des Webhostings (Rechenzentrum Nürnberg, Deutschland). Eine Übermittlung in Drittländer findet nicht statt.</p>
|
||||
|
||||
<p>(2) Will WEBklar einen weiteren Unterauftragsverarbeiter hinzuziehen oder einen ersetzen, teilt es das dem Kunden mindestens vier Wochen vorher in Textform mit. Der Kunde kann innerhalb von zwei Wochen nach Zugang der Mitteilung aus wichtigem datenschutzrechtlichem Grund in Textform widersprechen. WEBklar setzt den neuen Unterauftragsverarbeiter nicht vor Ablauf dieser Frist ein. Kündigt ein Unterauftragsverarbeiter von WEBklar eine eigene Änderung mit kürzerer Frist an, informiert WEBklar den Kunden unverzüglich, nachdem die Ankündigung eingegangen ist; der Kunde kann dann unverzüglich widersprechen. Einigen sich die Parteien nicht, kann der Kunde den Buchungskalender abschalten lassen und WEBklar ihn für diese Website zu dem Zeitpunkt beenden, zu dem der neue Unterauftragsverarbeiter eingesetzt wird. Die Daten werden dann nach Nummer 9 gelöscht, auf Weisung des Kunden früher (Nummer 9 Abs. 6).</p>
|
||||
|
||||
<p>(3) WEBklar verpflichtet jeden Unterauftragsverarbeiter vertraglich auf die Datenschutzpflichten nach Art. 28 Abs. 4 DSGVO. Bei der Hetzner Online GmbH ist das deren Vertrag zur Auftragsverarbeitung, den WEBklar abgeschlossen hat. Kommt ein Unterauftragsverarbeiter seinen Datenschutzpflichten nicht nach, haftet WEBklar dem Kunden für die Einhaltung dieser Pflichten (Art. 28 Abs. 4 Satz 2 DSGVO).</p>
|
||||
|
||||
<p>(4) <strong>Stripe ist kein Unterauftragsverarbeiter von WEBklar.</strong> Der Kunde verwendet sein eigenes Stripe-Konto und hat den Vertrag darüber unmittelbar mit Stripe geschlossen (Nr. 3 der Leistungsbeschreibung); die Zahlung des Gastes fließt direkt auf dieses Konto. Die Zahlungsseite (Stripe Checkout), die Erhebung von Zahlungs- und Kartendaten (derzeit nur Karte, einschließlich Apple Pay und Google Pay), die Prüfung von Zahlungen und die Auszahlungen verantworten der Kunde und Stripe nach ihrem Vertrag; WEBklar erhält diese Daten nicht. Auf Weisung des Kunden legt WEBklar bei Stripe die Zahlung an, ruft die Angaben zu Zahlungen ab, die der Buchungskalender braucht (Nummer 2 Abs. 1), schließt offene Zahlungsseiten und veranlasst Erstattungen (auch die automatischen nach Nummer 4 Abs. 2). An Stripe gehen dabei von WEBklar nur: die Bezeichnung des Aufenthalts mit Rechnungsnummer als Zahlungsposition, der Betrag, die Kennungen der Buchung und der Zahlung (Metadaten), die E-Mail-Adresse des Gastes zur Vorbelegung der Zahlungsseite, soweit WEBklar sie übergibt, die Erstattung und der <strong>geheime Link zur Buchungsseite mit dem Token als Rücksprungadresse</strong> (bei Erfolg und bei Abbruch). Dieser Link ist damit für Stripe und im Stripe-Dashboard des Kunden sichtbar; Der Link gilt bis 90 Tage nach der Abreise (bei nicht zustande gekommenen Buchungen 30 Tage) und öffnet nur diese eine Buchungsseite. Die Weitergabe von Daten durch Stripe, auch an Stellen außerhalb der Europäischen Union, regeln der Kunde und Stripe; dafür ist WEBklar nicht verantwortlich.</p>
|
||||
|
||||
<p>(5) Plattformen, deren iCal-Kalender der Kunde einbindet oder denen er die iCal-Adresse des Buchungskalenders mitteilt, sind Empfänger nach seiner Entscheidung. WEBklar übermittelt ihnen keine Daten von Gästen.</p>
|
||||
|
||||
<h2>8. Unterstützung des Kunden, Betroffenenrechte</h2>
|
||||
|
||||
<p>(1) WEBklar leitet Anfragen von Gästen, die an WEBklar gerichtet sind, unverzüglich an den Kunden weiter. WEBklar unterstützt ihn mit geeigneten technischen und organisatorischen Maßnahmen bei der Beantwortung.</p>
|
||||
|
||||
<p>(2) Dafür bietet das Kundenportal die Anzeige der Anfragen und Buchungen, mit denen der Kunde Auskunft erteilen kann, und den Abruf der Rechnungen als PDF. Auf Weisung des Kunden in Textform (etwa im Chat des Kundenportals) anonymisiert WEBklar die Gästedaten einer Anfrage oder Buchung unverzüglich: Name, Firma, E-Mail-Adresse, Telefonnummer und die Freitexte werden gelöscht oder unkenntlich gemacht (ein IP-Hash ist zu diesem Zeitpunkt in der Regel längst entfernt); Anfragenummer, Zeitraum, Beträge und Status bleiben. Rechnungen und Rechnungsadresse einer gebuchten Anfrage bleiben, solange eine Aufbewahrungspflicht besteht (Nummer 9 Abs. 3). Dabei löscht WEBklar die E-Mails zu dieser Anfrage, die noch auf den Versand warten. E-Mails, deren Versand endgültig fehlgeschlagen ist, behalten Empfängeradresse und Inhalt, bis WEBklar sie nach Nummer 9 Abs. 5 löscht (48 Stunden nach dem Fehlschlag).</p>
|
||||
|
||||
<p>(3) Verletzungen des Schutzes personenbezogener Daten meldet WEBklar dem Kunden unverzüglich, möglichst innerhalb von 24 Stunden nach Kenntnis, mit den Informationen, die zu diesem Zeitpunkt vorliegen.</p>
|
||||
|
||||
<p>(4) WEBklar unterstützt den Kunden bei seinen Pflichten aus Art. 32 bis 36 DSGVO. Dazu gehören die Angaben, die er für sein Verzeichnis von Verarbeitungstätigkeiten und für eine etwaige Datenschutz-Folgenabschätzung braucht.</p>
|
||||
|
||||
<p>(5) Die Gäste informiert der Kunde (Art. 13 DSGVO; Nummer 11 Abs. 2).</p>
|
||||
|
||||
<h2>9. Löschung und Rückgabe</h2>
|
||||
|
||||
<p>(1) Den IP-Hash entfernt WEBklar aus dem Datensatz einer Anfrage, sobald der Kunde sie freigibt oder ablehnt, spätestens 30 Tage nach dem Anlegen.</p>
|
||||
|
||||
<p>(2) Anfragen, aus denen keine Buchung wurde (abgelehnt, abgelaufen, oder neu und nicht innerhalb von 60 Tagen entschieden), anonymisiert WEBklar nach 6 Monaten: Name, Firma, E-Mail-Adresse, Telefonnummer und Nachricht werden gelöscht. Anfragenummer, Zeitraum, Status und, soweit vorhanden, der Preisvorschlag bleiben ohne Personenbezug erhalten.</p>
|
||||
|
||||
<p>(3) Bei gebuchten Aufenthalten anonymisiert WEBklar die Gästedaten (Name, Firma, E-Mail-Adresse, Telefonnummer und Nachricht) 3 Jahre nach dem Ende des Kalenderjahres, in dem der Gast abgereist ist (Abreisejahr; maßgeblich ist die Zeit in Berlin). <strong>Rechnungen, Stornorechnungen und die Rechnungsadresse bleiben 8 Jahre erhalten</strong>, gerechnet ab dem Ende des Kalenderjahres der Rechnung, solange der Buchungskalender läuft (Buchungsbelege, § 147 AO); sie sind für den Kunden als Aussteller bestimmt. Anfragenummer, Zeitraum, Beträge und Status bleiben ebenfalls.</p>
|
||||
|
||||
<p>(4) Die Anonymisierung nach den Absätzen 2 und 3 sowie die Entfernung des IP-Hash laufen im täglichen Löschlauf; anonymisiert oder entfernt wird frühestens nach Ablauf der jeweiligen Frist. Ist an einem Tag mehr zu bearbeiten, als ein Lauf schafft, folgt der Rest im nächsten Lauf.</p>
|
||||
|
||||
<p>(5) In der Versandwarteschlange für E-Mails leert WEBklar die Empfängeradresse und den Inhalt einer E-Mail nach dem Versand und löscht den Eintrag 7 Tage nach dem Versand. E-Mails, die nicht zugestellt werden können, löscht WEBklar mit Empfängeradresse und Inhalt 48 Stunden nach dem endgültigen Fehlschlag.</p>
|
||||
|
||||
<p>(6) Ist der Buchungskalender abgeschaltet oder das Wartungs-Abo beendet, kann der Kunde die Anfragen, Buchungen und Rechnungen 90 Tage lang im Kundenportal lesen und die Rechnungen einzeln als PDF herunterladen; WEBklar weist ihn nach dem Abschalten per E-Mail mit dem Löschdatum darauf hin, seine Rechnungen zu sichern. Danach löscht WEBklar alle Daten des Buchungskalenders: Unterkünfte, Preise, Anfragen, Buchungen, Rechnungen, Kalenderverbindungen, Sperren und Einstellungen. Damit endet auch die Aufbewahrung der Rechnungen nach Absatz 3; sie bewahrt der Kunde dann selbst auf. Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Angaben zur Einrichtung bestehen (zum Beispiel Adresse der Website, Einbau-Code und die Angaben des Kunden aus der Anfrage) und enthält keine Daten von Gästen; dafür gilt die Datenschutzerklärung von WEBklar (Abschnitt 6). Beginnt innerhalb dieser Frist wieder ein Wartungs-Abo, schaltet WEBklar den Buchungskalender auf Wunsch des Kunden mit den vorhandenen Daten wieder ein (Nr. 10 Abs. 5 der Leistungsbeschreibung). Früher anonymisiert WEBklar einzelne Daten auf Weisung des Kunden (Nummer 8 Abs. 2); Wird das Kundenkonto gelöscht, schaltet WEBklar den Buchungskalender ab und löscht seine Daten wie nach Absatz 6 (bei Abschaltung 90 Tage später); einen Hinweis per E-Mail gibt es dann nicht, weil mit dem Konto auch die E-Mail-Adresse gelöscht wird. alle Daten des Buchungskalenders löscht WEBklar auf Weisung des Kunden in Textform unverzüglich.</p>
|
||||
|
||||
<p>(7) Die Daten sind in der täglichen Vollsicherung des Servers enthalten (§ 3 Abs. 5 der Abo-Bedingungen). Gelöschte oder anonymisierte Daten bleiben dort bis zu sieben Tage erhalten und werden dann überschrieben. Wird eine Sicherung zurückgespielt, holt der tägliche Löschlauf die fälligen Löschungen und Anonymisierungen nach; Löschungen auf Weisung des Kunden wiederholt WEBklar.</p>
|
||||
|
||||
<p>(8) Gesetzliche Aufbewahrungspflichten bleiben unberührt. Der Kunde ist für seine eigenen Aufbewahrungspflichten (zum Beispiel für Rechnungen und Buchungsbelege) selbst verantwortlich; WEBklar ist kein Archiv im Sinne der Buchführung. Die Aufbewahrung der Rechnungen nach Absatz 3 geschieht als Dienst für den Kunden und für die Dauer des Buchungskalenders. Der Kunde sichert die Belege, die er braucht, rechtzeitig vor der Löschung nach Absatz 6 durch den Download. Soweit eine gesetzliche Aufbewahrungspflicht besteht, löscht oder anonymisiert WEBklar auch auf Weisung des Kunden nicht früher; stattdessen sperrt es die Daten für die weitere Verarbeitung.</p>
|
||||
|
||||
<h2>10. Nachweise und Kontrollen</h2>
|
||||
|
||||
<p>WEBklar stellt dem Kunden alle Informationen zur Verfügung, die zum Nachweis der Pflichten aus Art. 28 DSGVO nötig sind, und ermöglicht Überprüfungen einschließlich Inspektionen durch den Kunden oder einen von ihm beauftragten, zur Verschwiegenheit verpflichteten Prüfer. Sie finden nach Anmeldung mit angemessener Frist (in der Regel 14 Tage) zu den Servicezeiten statt, ohne den Betrieb unzumutbar zu stören. Für das Rechenzentrum genügen in der Regel die Nachweise des Betreibers (Zertifizierung der Hetzner Online GmbH nach ISO/IEC 27001). Für mehr als eine Vor-Ort-Kontrolle pro Jahr kann WEBklar eine angemessene Vergütung verlangen.</p>
|
||||
|
||||
<h2>11. Pflichten des Kunden</h2>
|
||||
|
||||
<p>(1) Der Kunde ist Verantwortlicher für die Verarbeitung der Daten seiner Gäste im Buchungskalender.</p>
|
||||
|
||||
<p>(2) Der Kunde informiert seine Gäste nach Art. 13 DSGVO. Dazu nimmt er den Datenschutz-Absatz für den Buchungskalender in seine Datenschutzerklärung auf (Nr. 5 Abs. 10 der Leistungsbeschreibung) und nennt in seinen Rechtstexten (Datenschutzerklärung, Impressum) die Angaben, die das Gesetz verlangt.</p>
|
||||
|
||||
<p>(3) Der Kunde schließt mit Stripe den Vertrag über sein Stripe-Konto selbst, wenn er die Online-Zahlung nutzt, und verantwortet die Verarbeitung von Daten durch Stripe, soweit sie nicht in diesem Vertrag geregelt ist (Nummer 7 Abs. 4).</p>
|
||||
|
||||
<p>(4) Der Kunde fordert von seinen Gästen keine Angaben zu ihrer Gesundheit oder zu anderen besonderen Kategorien personenbezogener Daten und weist sie auf der Website darauf hin, solche Angaben nicht in das Nachrichtenfeld zu schreiben.</p>
|
||||
|
||||
<p>(5) Der Kunde verändert den Einbau-Code und die gesetzlich vorgeschriebenen Hinweise des Widgets und der Buchungsseite nicht (Nr. 5 Abs. 9 der Leistungsbeschreibung).</p>
|
||||
|
||||
<p>(6) Der Kunde erfüllt seine Pflichten zu Meldeschein, Kurtaxe und sonstigen Meldungen an Behörden selbst und verarbeitet die dafür nötigen Daten außerhalb des Buchungskalenders (Nr. 5 Abs. 6 der Leistungsbeschreibung).</p>
|
||||
|
||||
<h2>12. Haftung, Änderungen, Schlussbestimmungen</h2>
|
||||
|
||||
<p>(1) Gegenüber betroffenen Personen gilt Art. 82 DSGVO. Für die Haftung zwischen Kunde und WEBklar gilt § 13 der Abo-Bedingungen.</p>
|
||||
|
||||
<p>(2) Eine neue Fassung dieses Vertrags oder der Leistungsbeschreibung, auch wegen geänderter Datenerhebung, teilt WEBklar dem Kunden mindestens sechs Wochen vor ihrem Wirksamwerden in Textform mit. Sie wird wirksam, wenn der Kunde ihr im Kundenportal oder in Textform zustimmt. Stimmt er nicht zu, gilt die bisherige Fassung weiter.</p>
|
||||
|
||||
<p>(3) Ist die neue Fassung nötig, weil ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form nicht mehr zulässt, und stimmt der Kunde ihr nicht zu, darf WEBklar den Buchungskalender für seine Website nach Nr. 12 Abs. 2 der Leistungsbeschreibung beenden oder nach Nr. 12 Abs. 3 der Leistungsbeschreibung aussetzen. Das Abo bleibt davon unberührt.</p>
|
||||
|
||||
<p>(4) Im Übrigen gilt § 16 der Abo-Bedingungen.</p>
|
||||
|
||||
<h2>Anlage: Technische und organisatorische Maßnahmen des Buchungskalenders (Art. 32 DSGVO)</h2>
|
||||
|
||||
<table>
|
||||
<thead><tr><th>Bereich</th><th>Maßnahmen</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>Übertragung</td><td>Das Widget, die Buchungsseite und das Kundenportal werden nur über TLS (HTTPS) übertragen. Der Einbau-Code verweist auf eine Adresse mit https://. Die Zahlung geschieht auf der Zahlungsseite von Stripe. Kalender anderer Plattformen ruft WEBklar nur über https ab.</td></tr>
|
||||
<tr><td>Speicher im Browser</td><td>Es werden keine Cookies gesetzt. Das Widget sendet seine Anfragen ohne Cookies und ruft nur die Schnittstelle des Buchungskalenders auf, keine Dienste Dritter.</td></tr>
|
||||
<tr><td>Zugriffskontrolle</td><td>Das Kundenportal ist nur nach Anmeldung erreichbar; jeder Kunde sieht nur die Daten seiner eigenen Projekte. Die öffentliche Schnittstelle des Widgets liefert nur für einen vorhandenen Buchungskalender Daten und ist über den Website-Schlüssel (Site-Key) adressiert; die belegten Nächte nennt sie ohne Quelle und ohne Daten von Gästen. Die Buchungsseite ruft der Gast nur mit einem geheimen Token ab, das nur als Prüfsumme gespeichert und zeitkonstant verglichen wird. Der Token wird nur als Prüfsumme gespeichert; er wird nur als Teil der Rücksprungadresse an Stripe übergeben (Nummer 7 Abs. 4) und öffnet nur diese eine Buchungsseite; er gilt bis 90 Tage nach der Abreise (bei nicht zustande gekommenen Buchungen 30 Tage), danach antwortet die Buchungsseite wie bei einem unbekannten Link. Benachrichtigungen von Stripe werden nur nach Prüfung ihrer Signatur verarbeitet; die Schlüssel für Stripe liegen nur auf dem Server. Die iCal-Adresse des Buchungskalenders ist ein geheimer, erneuerbarer Zufallswert.</td></tr>
|
||||
<tr><td>Missbrauchsschutz</td><td>Ein verstecktes Feld (Honeypot), eine Mindestzeit bis zum Absenden des Formulars mit signiertem Formular-Token und Grenzen je IP-Adresse und je Buchungskalender schützen vor automatisierten Anfragen. Die Grenzen gelten im Arbeitsspeicher; ein Neustart leert sie. Mit der Anfrage wird nur ein gesalzener IP-Hash gespeichert (Nummer 3 Abs. 4). Beim Abruf von Kalendern anderer Plattformen sind nur Adressen mit https erlaubt, private und lokale Ziele sind gesperrt, und Größe und Dauer des Abrufs sind begrenzt.</td></tr>
|
||||
<tr><td>Datenminimierung</td><td>Kartendaten erhält und speichert WEBklar nicht. Preise berechnet der Server aus den gespeicherten Preisen und Posten, nie aus Angaben des Browsers. Von anderen Kalendern werden nur Zeiträume übernommen. Mit der Anfrage werden keine IP-Adresse im Klartext und kein Browser-Kennzeichen gespeichert; der IP-Hash wird mit der Entscheidung des Kunden entfernt. Die Gästedaten werden nach den Fristen der Nummer 9 anonymisiert. Nach dem Versand einer E-Mail werden Empfängeradresse und Inhalt in der Warteschlange geleert.</td></tr>
|
||||
<tr><td>Integrität der Rechnungen</td><td>Rechnungen und Stornorechnungen werden als unveränderlicher Schnappschuss gespeichert; das PDF entsteht bei jedem Abruf aus diesem Schnappschuss. Rechnungsnummern werden unter Sperre lückenlos je Kunde und Jahr vergeben. Freigabe und Buchung prüfen unter Sperre erneut, dass der Zeitraum frei ist.</td></tr>
|
||||
<tr><td>Trennung</td><td>Alle Unterkünfte, Anfragen und Rechnungen tragen die Kennung des Kunden, des Projekts und der Funktion; das Kundenportal und die öffentliche Schnittstelle lesen immer nur die Daten des jeweiligen Buchungskalenders. Die Hintergrundläufe (Ablauf von Reservierungen, Kalenderabgleich, Versand der E-Mails, Anonymisierung und Löschung) schreiben jeweils nur in das Dokument, um das es geht.</td></tr>
|
||||
<tr><td>Löschung</td><td>Täglicher Löschlauf für Anfragen, Buchungen und Warteschlange nach den Fristen der Nummer 9; die Daten abgeschalteter Buchungskalender 90 Tage nach dem Abschalten; das Einrichtungs-Ticket bleibt als Arbeitsnachweis mit einer Kurzfassung der Angaben zur Einrichtung bestehen und enthält keine Daten von Gästen (Nummer 9 Abs. 6).</td></tr>
|
||||
<tr><td>Protokolle</td><td>Die Programmprotokolle des Buchungskalenders und der Tagesbericht enthalten nur Zählungen und Anfragenummern, keine Namen, E-Mail-Adressen oder Adressen von Gästen. Server-Protokolle des Webservers werden nach 14 Tagen gelöscht (Anlage 2 der Abo-Bedingungen).</td></tr>
|
||||
<tr><td>Datensicherung</td><td>Tägliche Vollsicherung des Servers durch Hetzner; verfügbar sind die letzten sieben Tagessicherungen (§ 3 Abs. 5 der Abo-Bedingungen).</td></tr>
|
||||
<tr><td>Rechenzentrum</td><td>Hetzner Online GmbH, Rechenzentren Falkenstein und Nürnberg (Deutschland). Hetzner ist nach ISO/IEC 27001 zertifiziert; der Geltungsbereich umfasst die Hosting-Dienstleistungen und die Rechenzentren.</td></tr>
|
||||
<tr><td>Organisation</td><td>Verpflichtung aller Beteiligten auf Vertraulichkeit, Ablauf für Datenschutzverletzungen (Nummer 8 Abs. 3), Überprüfung der Maßnahmen mindestens einmal im Jahr.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,199 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="webklar-fassung" content="2026-10-16">
|
||||
<meta name="robots" content="noindex">
|
||||
<title>Auftragsverarbeitungsvertrag Webshop – WEBklar</title>
|
||||
<style>
|
||||
:root { color-scheme: light; }
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; background: #fff; color: #1a1f36; font: 16px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; font-variant-numeric: tabular-nums; }
|
||||
main { max-width: 780px; margin: 0 auto; padding: 44px 20px 80px; }
|
||||
.brand { font-weight: 600; letter-spacing: 0.12em; font-size: 13px; color: #111; }
|
||||
h1 { font-size: 30px; line-height: 1.2; margin: 6px 0 6px; font-weight: 600; }
|
||||
h2 { font-size: 20px; margin: 36px 0 10px; padding-top: 14px; border-top: 1px solid #ebeef1; font-weight: 600; }
|
||||
h3 { font-size: 16.5px; margin: 22px 0 6px; font-weight: 600; }
|
||||
p, li { margin: 6px 0; }
|
||||
ul { padding-left: 24px; }
|
||||
.meta { color: #5e6678; font-size: 14px; }
|
||||
.box { border-top: 1px solid #d8dde3; border-bottom: 1px solid #d8dde3; padding: 14px 0; margin: 18px 0; }
|
||||
.box.widerspruch { border-top: 2px solid #111; border-bottom: 2px solid #111; }
|
||||
table { border-collapse: collapse; width: 100%; font-size: 15px; }
|
||||
td, th { padding: 6px 10px 6px 0; border-bottom: 1px solid #ebeef1; vertical-align: top; text-align: left; }
|
||||
th { font-size: 13px; text-transform: uppercase; letter-spacing: 0.04em; color: #5e6678; background: #f8f9fb; padding-left: 8px; }
|
||||
a { color: #111; text-decoration: underline; text-decoration-color: #d8dde3; text-underline-offset: 2px; }
|
||||
a:hover { background: #f6f8fa; }
|
||||
nav.top { font-size: 14px; margin-bottom: 22px; }
|
||||
.offen { display: inline-block; background: #fcedb9; color: #a82c00; padding: 1px 10px; border-radius: 6px; font-weight: 600; }
|
||||
@media print {
|
||||
@page { size: A4; margin: 18mm 17mm 18mm;
|
||||
@top-left { content: "WEBklar · AVV Webshop"; font: 8pt sans-serif; color: #8a93a3; }
|
||||
@bottom-right { content: "Seite " counter(page) " von " counter(pages); font: 8pt sans-serif; color: #8a93a3; } }
|
||||
body { background: #fff; font-size: 10pt; line-height: 1.45; }
|
||||
main { max-width: none; padding: 0; }
|
||||
nav.top { display: none; }
|
||||
h1 { font-size: 20pt; }
|
||||
h2 { font-size: 13pt; margin-top: 16pt; break-after: avoid; }
|
||||
a { color: inherit; text-decoration: none; }
|
||||
.box, tr, ol.abs > li { break-inside: avoid; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<nav class="top"><a href="/dashboard.html">← Kundenportal</a> · <a href="/neu/rechtliches/abo-bedingungen.html">Abo-Bedingungen</a> · <a href="/neu/rechtliches/leistungsbeschreibung-webshop-2026-10-16.html">Leistungsbeschreibung Webshop</a> · <a href="/neu/rechtliches/avv-webshop-2026-10-16.html">AVV Webshop</a> · <a href="/datenschutz.html">Datenschutzerklärung</a> · <a href="https://webklar.com/impressum">Impressum</a></nav>
|
||||
|
||||
<div class="brand">WEBKLAR</div>
|
||||
<h1>Auftragsverarbeitungsvertrag Webshop (Art. 28 DSGVO)</h1>
|
||||
|
||||
<p class="meta"><strong>Fassung 2026-10-16</strong> · Entwurf – Prüfung durch Kanzlei offen</p>
|
||||
|
||||
<p>zwischen dem Kunden als Verantwortlichem („Kunde“; in der Leistungsbeschreibung „Händler“) und der WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, vertreten durch die Gesellschafter Kenso Grimm und Justin Klein, als Auftragsverarbeiter („WEBklar“).</p>
|
||||
|
||||
<h2>Vorbemerkung: Abschluss und Verhältnis zu anderen Verträgen</h2>
|
||||
|
||||
<p>(1) Der Kunde schließt diesen Vertrag, wenn er im Kundenportal die Funktion „Webshop“ einschaltet und die dort angezeigte Zeile „Es gelten Leistungsbeschreibung und Auftragsverarbeitung (Fassung 2026-10-16)“ mit dem Klick auf „Einschalten“ bestätigt; diesem Vertrag und der Leistungsbeschreibung „Webshop“ („Leistungsbeschreibung“) in der angezeigten Fassung stimmt er damit zu. Das Portal speichert den Wortlaut der angezeigten Zeile, die Fassungen, den Zeitpunkt, das Kundenkonto und die angemeldete Person. Anschließend sendet WEBklar dem Kunden eine E-Mail, die beide Texte in dieser Fassung als PDF enthält und auf sie verlinkt.</p>
|
||||
|
||||
<p>(2) Ein Mitarbeiter von WEBklar darf die Funktion nur einschalten, wenn der Kunde vorher in Textform (etwa im Chat des Kundenportals oder per E-Mail) erklärt hat, dass er diesem Vertrag und der Leistungsbeschreibung in der aktuellen Fassung zustimmt. Das Portal verlangt dafür das Datum und den Weg der Erklärung; fehlt eine Angabe, schaltet es nicht ein. WEBklar speichert vor dem Einschalten einen Zustimmungsnachweis mit der Fassung der beiden Texte, dem Datum und dem Weg dieser Erklärung, dem Zeitpunkt des Einschaltens und dem Namen des Mitarbeiters (ohne IP-Adresse und Browser-Kennzeichen, weil der Kunde nicht im Portal zugestimmt hat). Der Kunde erhält anschließend eine E-Mail mit diesen beiden Texten als PDF.</p>
|
||||
|
||||
<p>(3) Dieser Vertrag ist ein eigener Vertrag für den Webshop. Er ergänzt den Auftragsverarbeitungsvertrag in Anlage 1 der Abo-Bedingungen. Für den Webshop geht er Anlage 1 vor; im Übrigen gilt Anlage 1 ergänzend. In Datenschutzfragen geht er den Abo-Bedingungen und der Leistungsbeschreibung vor.</p>
|
||||
|
||||
<p>(4) Die Rollen: Der Kunde ist Verantwortlicher für die personenbezogenen Daten seiner Käufer im Webshop. WEBklar ist sein Auftragsverarbeiter. Stripe ist nicht Auftragsverarbeiter von WEBklar, sondern Vertragspartner des Kunden (Nummer 7 Abs. 4).</p>
|
||||
|
||||
<h2>1. Gegenstand und Dauer</h2>
|
||||
|
||||
<p>(1) WEBklar betreibt für die öffentliche Website des Kunden ein Shop-Widget mit einer Bestellverwaltung im Kundenportal („Webshop“) und verarbeitet dabei personenbezogene Daten der Käufer des Kunden im Auftrag des Kunden. Den Umfang beschreibt die Leistungsbeschreibung.</p>
|
||||
|
||||
<p>(2) Der Vertrag beginnt mit dem Einschalten der Funktion. Er endet mit dem Abschalten, spätestens mit dem Ende des Wartungs-Abos; den Webshop schaltet WEBklar dann spätestens am folgenden Tag ab (Nr. 10 Abs. 1 der Leistungsbeschreibung). Die Pflichten aus diesem Vertrag gelten fort, bis alle Daten nach Nummer 9 gelöscht sind.</p>
|
||||
|
||||
<h2>2. Art und Zweck der Verarbeitung</h2>
|
||||
|
||||
<p>(1) Art: Übermitteln der Bestellung vom Widget an den Server von WEBklar, Übernehmen der Zahlungs- und Adressangaben aus der Checkout-Sitzung bei Stripe, Speichern, Verwalten und Anzeigen im Kundenportal, Versenden von E-Mails, Bereitstellen einer CSV-Datei, Weitergeben von Erstattungen an Stripe im Auftrag des Kunden, Anonymisieren und Löschen.</p>
|
||||
|
||||
<p>(2) Zwecke: Der Kunde soll Bestellungen seiner Käufer abwickeln. Dazu nimmt der Webshop Bestellungen entgegen, reserviert den Bestand, gleicht Zahlungen mit Stripe ab, bestätigt Bestellungen (bei einer Zahlung, die noch aussteht, etwa einer SEPA-Lastschrift, zunächst nur mit einer Eingangsmail ohne Vertragsschluss), bildet Versand und Abholung ab, nimmt Widerrufe entgegen, erstattet Zahlungen im Auftrag des Kunden und benachrichtigt den Kunden. WEBklar versendet die E-Mails im Auftrag des Kunden. Außerdem schützt WEBklar den Webshop für den Kunden vor Missbrauch (Mengenbegrenzung, Begrenzung der offenen Bestellungen je IP-Adresse, Erkennen automatisierter Bestellungen und Widerrufe).</p>
|
||||
|
||||
<p>(3) WEBklar verwendet die Daten für keine eigenen Zwecke, auch nicht in anonymisierter Form und nicht für Auswertungen über mehrere Kunden. Es findet keine Werbung und keine Zusammenführung mit anderen Daten statt. WEBklar bildet keine Profile einzelner Käufer. Zählungen ohne Personenbezug, die WEBklar für den sicheren Betrieb braucht (etwa die Zahl der anonymisierten Bestellungen je Tag oder die Länge der Versandwarteschlange), sind keine eigenen Zwecke.</p>
|
||||
|
||||
<h2>3. Datenarten und Betroffene</h2>
|
||||
|
||||
<p>(1) Daten der Käufer, die WEBklar aus der Checkout-Sitzung bei Stripe und aus dem Widget erhält und speichert: Name, E-Mail-Adresse, Lieferadresse und Rechnungsadresse (die Rechnungsadresse nur, soweit die Zahlungsseite von Stripe sie für die gewählte Zahlungsart abfragt); die Bestellung (Bestellnummer, Positionen mit Namen, Mengen und Preisen zum Zeitpunkt der Bestellung, Versandkosten, Lieferart, Steuerausweis, Gesamtbetrag); der Status der Bestellung mit den Zeitpunkten (bestellt, bezahlt, versendet, abholbereit, abgeschlossen, storniert, erstattet); Zahlungsstatus, Zahlungsart (zum Beispiel Karte oder SEPA-Lastschrift) und Kennungen der Zahlung bei Stripe; Versanddienst und Sendungsnummer, wenn der Kunde sie einträgt; Erstattungen mit Betrag, Zeitpunkt und Grund; die Prüfsumme (Hash) der Rechtstexte des Kunden, die der Käufer bei der Bestellung gesehen hat (den Wortlaut der Fassung verwahrt das Archiv nach Nummer 8 Abs. 5); ein Hashwert der IP-Adresse (Absatz 4); der Zustellstatus der E-Mails an den Käufer, auch der Eingangsmail bei ausstehender Zahlung. <strong>Kartendaten und Kontodaten (IBAN) erhält und speichert WEBklar nicht;</strong> die gibt der Käufer nur bei Stripe ein. <strong>Eine Telefonnummer des Käufers erhebt und speichert WEBklar nicht:</strong> Das Widget hat kein Telefonfeld, die Zahlungsseite von Stripe ist so eingestellt, dass sie keine Telefonnummer verlangt, und eine Telefonnummer, die Stripe in den Kundendaten der Sitzung dennoch übermitteln sollte, übernimmt WEBklar nicht in die Bestellung. Die Telefonnummer des Kunden selbst (Anbieterangaben in seinen Einstellungen) gehört nicht zu den Käuferdaten.</p>
|
||||
|
||||
<p>(2) Daten von Personen, die über die Widerrufsfunktion einen Vertrag widerrufen (Käufer und Interessenten, auch ohne passende Bestellung): Name, E-Mail-Adresse, Bestellnummer, die Erklärung als Freitext und der Zeitpunkt des Eingangs, außerdem der Zeitpunkt der Eingangsbestätigung und der Bearbeitung durch den Kunden.</p>
|
||||
|
||||
<p>(3) Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO) werden nicht verarbeitet. Waren, aus deren Kauf sich besondere Kategorien ergeben können, etwa Arzneimittel, sind nach Nr. 3 der Leistungsbeschreibung ausgeschlossen; der Kunde fordert von seinen Käufern keine entsprechenden Angaben (Nummer 11 Abs. 4).</p>
|
||||
|
||||
<p>(4) IP-Adresse und Browser-Kennzeichen (User-Agent) erhält der Server von WEBklar technisch bei jedem Abruf des Widgets und jeder Anfrage. Die Mengenbegrenzung (Missbrauchsschutz) verwendet die IP-Adresse im Arbeitsspeicher des Servers; ein Neustart leert sie. Mit dem Widerruf wird keine IP-Adresse und kein Browser-Kennzeichen gespeichert. Mit der Bestellung werden ebenfalls weder eine IP-Adresse im Klartext noch ein Browser-Kennzeichen gespeichert, sondern nur ein <strong>Hashwert der IP-Adresse („IP-Hash“)</strong>: die ersten 16 Hexadezimalzeichen eines SHA-256-Werts, den WEBklar mit einem geheimen Schlüssel des Servers salzt. Der IP-Hash ist ein pseudonymes technisches Datum und bleibt ein personenbezogenes Datum, weil WEBklar mit dem Schlüssel für dieselbe IP-Adresse denselben Wert berechnen kann; ohne den Schlüssel lässt sich aus ihm keine IP-Adresse ableiten. WEBklar verwendet ihn ausschließlich zum Missbrauchsschutz: Je IP-Hash und Webshop sind höchstens drei offene (noch nicht bezahlte) Bestellungen innerhalb von 30 Minuten zulässig. Er dient nicht dazu, Käufer zu identifizieren, wiederzuerkennen oder zu profilieren, wird dem Kunden weder angezeigt noch exportiert und steht nur im Datensatz der Bestellung. WEBklar löscht ihn aus dem Datensatz der Bestellung, sobald die Bestellung bezahlt oder storniert wird, spätestens mit der Löschung nicht bezahlter Bestellungen nach 30 Tagen (Nummer 9 Abs. 1); bei bezahlten Bestellungen bleibt er nicht bis zur Anonymisierung (Nummer 9 Abs. 2) erhalten. In den Server-Protokollen stehen IP-Adresse und Browser-Kennzeichen 14 Tage (Anlage 2 der Abo-Bedingungen).</p>
|
||||
|
||||
<p>(5) Der Webshop setzt keine Cookies. Das Widget speichert im Browser des Besuchers nur den Warenkorb (Kennungen der Artikel und Mengen, ohne Personenbezug) im Sitzungsspeicher (sessionStorage) des Tabs, solange der Tab geöffnet ist; das ist für den Warenkorb technisch erforderlich. Andere Daten legt das Widget im Browser nicht ab.</p>
|
||||
|
||||
<p>(6) Betroffene: Käufer, die über die Website des Kunden Waren bestellen, und Personen, die über die Widerrufsfunktion einen Vertrag widerrufen. Beschäftigte des Kunden sind von diesem Vertrag nicht betroffen.</p>
|
||||
|
||||
<h2>4. Weisungen</h2>
|
||||
|
||||
<p>(1) WEBklar verarbeitet die Daten nur auf dokumentierte Weisung des Kunden, auch in Bezug auf eine Übermittlung in ein Drittland oder an eine internationale Organisation. Das gilt nicht, wenn das Recht der Europäischen Union oder Deutschlands WEBklar zu einer Verarbeitung verpflichtet. In diesem Fall teilt WEBklar dem Kunden die Pflicht vorher mit, soweit das Gesetz es nicht verbietet.</p>
|
||||
|
||||
<p>(2) Mit dem Einschalten weist der Kunde WEBklar an, den Webshop nach seinen Einstellungen und der Leistungsbeschreibung zu betreiben. Weitere Weisungen ergeben sich aus den Einstellungen und Aktionen im Kundenportal (Artikel, Versand, Abholung, Zahlungsarten, Rechtstexte, Bestellstatus, Erstattungen, Öffnen und Schließen des Webshops, Export). Sonstige Weisungen erteilt der Kunde in Textform, etwa im Chat des Kundenportals. Weisungen, die über diese Einstellungen hinausgehen, setzt WEBklar nur um, wenn das technisch vorgesehen ist; sonst kann der Kunde den Webshop abschalten lassen (Nr. 10 Abs. 7 der Leistungsbeschreibung). Die Anonymisierung und Löschung auf Weisung nach Nummer 8 Abs. 2 und Nummer 9 Abs. 6 setzt WEBklar immer um.</p>
|
||||
|
||||
<p>(3) Hält WEBklar eine Weisung für rechtswidrig, weist es den Kunden unverzüglich darauf hin. WEBklar darf die Ausführung aussetzen, bis der Kunde die Weisung ändert oder die Rechtslage geklärt ist.</p>
|
||||
|
||||
<h2>5. Vertraulichkeit</h2>
|
||||
|
||||
<p>WEBklar setzt nur Personen ein, die zur Vertraulichkeit verpflichtet sind oder einer gesetzlichen Verschwiegenheitspflicht unterliegen.</p>
|
||||
|
||||
<h2>6. Technische und organisatorische Maßnahmen</h2>
|
||||
|
||||
<p>(1) WEBklar trifft die Maßnahmen nach Art. 32 DSGVO, die in der Anlage zu diesem Vertrag beschrieben sind. WEBklar darf sie an den Stand der Technik anpassen, sofern das Schutzniveau nicht sinkt.</p>
|
||||
|
||||
<p>(2) Für den Server gelten ergänzend die Maßnahmen der Anlage 2 der Abo-Bedingungen, soweit die Anlage zu diesem Vertrag nichts anderes regelt.</p>
|
||||
|
||||
<h2>7. Unterauftragsverarbeiter</h2>
|
||||
|
||||
<p>(1) Der Kunde genehmigt den Einsatz der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, für den Betrieb des Servers, auf dem der Webshop läuft, und für dessen Sicherung (Rechenzentrum Falkenstein, Deutschland) sowie für den Versand der Bestell-E-Mails über den SMTP-Dienst des Webhostings (Rechenzentrum Nürnberg, Deutschland). Eine Übermittlung in Drittländer findet nicht statt.</p>
|
||||
|
||||
<p>(2) Will WEBklar einen weiteren Unterauftragsverarbeiter hinzuziehen oder einen ersetzen, teilt es das dem Kunden mindestens vier Wochen vorher in Textform mit. Der Kunde kann innerhalb von zwei Wochen nach Zugang der Mitteilung aus wichtigem datenschutzrechtlichem Grund in Textform widersprechen. WEBklar setzt den neuen Unterauftragsverarbeiter nicht vor Ablauf dieser Frist ein. Kündigt ein Unterauftragsverarbeiter von WEBklar eine eigene Änderung mit kürzerer Frist an, informiert WEBklar den Kunden unverzüglich, nachdem die Ankündigung eingegangen ist; der Kunde kann dann unverzüglich widersprechen. Einigen sich die Parteien nicht, kann der Kunde den Webshop abschalten lassen und WEBklar ihn für diese Website zu dem Zeitpunkt beenden, zu dem der neue Unterauftragsverarbeiter eingesetzt wird. Die Daten werden dann nach Nummer 9 gelöscht, auf Weisung des Kunden früher (Nummer 9 Abs. 6).</p>
|
||||
|
||||
<p>(3) WEBklar verpflichtet jeden Unterauftragsverarbeiter vertraglich auf die Datenschutzpflichten nach Art. 28 Abs. 4 DSGVO. Bei der Hetzner Online GmbH ist das deren Vertrag zur Auftragsverarbeitung, den WEBklar abgeschlossen hat. Kommt ein Unterauftragsverarbeiter seinen Datenschutzpflichten nicht nach, haftet WEBklar dem Kunden für die Einhaltung dieser Pflichten (Art. 28 Abs. 4 Satz 2 DSGVO).</p>
|
||||
|
||||
<p>(4) <strong>Stripe ist kein Unterauftragsverarbeiter von WEBklar.</strong> Der Kunde verwendet sein eigenes Stripe-Konto und hat den Vertrag darüber unmittelbar mit Stripe geschlossen (Nr. 4 der Leistungsbeschreibung). Die Zahlungsseite (Stripe Checkout), die Erhebung von Zahlungs-, Karten- und Kontodaten, die Prüfung von Zahlungen und die Auszahlungen verantworten der Kunde und Stripe nach ihrem Vertrag; WEBklar erhält diese Daten nicht. Auf Weisung des Kunden ruft WEBklar bei Stripe die Angaben zu Zahlungen und Bestellungen ab, die der Webshop braucht (Nummer 2 Abs. 1), und veranlasst Erstattungen. Daten von Käufern gehen dabei nicht von WEBklar an Stripe, außer der Erstattung selbst und der Kennung der Zahlung. Die Weitergabe von Daten durch Stripe, auch an Stellen außerhalb der Europäischen Union, regeln der Kunde und Stripe; dafür ist WEBklar nicht verantwortlich.</p>
|
||||
|
||||
<h2>8. Unterstützung des Kunden, Betroffenenrechte</h2>
|
||||
|
||||
<p>(1) WEBklar leitet Anfragen von Käufern, die an WEBklar gerichtet sind, unverzüglich an den Kunden weiter. WEBklar unterstützt ihn mit geeigneten technischen und organisatorischen Maßnahmen bei der Beantwortung.</p>
|
||||
|
||||
<p>(2) Dafür bietet das Kundenportal den Export der Bestellungen als CSV-Datei und die Anzeige der Bestellungen und Widerrufe, mit denen der Kunde Auskunft erteilen kann. Auf Weisung des Kunden in Textform (etwa im Chat des Kundenportals) anonymisiert WEBklar die Käuferdaten einer Bestellung oder eines Widerrufs unverzüglich: Name, E-Mail-Adresse, Liefer- und Rechnungsadresse und die Freitexte werden gelöscht oder unkenntlich gemacht (ein IP-Hash ist zu diesem Zeitpunkt längst gelöscht); Bestellnummer, Positionen, Beträge und Status bleiben. Dabei löscht WEBklar die E-Mails zu dieser Bestellung, die noch auf den Versand warten. E-Mails, deren Versand endgültig fehlgeschlagen ist, behalten Empfängeradresse und Inhalt, bis WEBklar sie nach Nummer 9 Abs. 4 löscht (48 Stunden nach dem Fehlschlag). Gesetzliche Aufbewahrungspflichten des Kunden (Nummer 9 Abs. 8) prüft der Kunde vor seiner Weisung selbst.</p>
|
||||
|
||||
<p>(3) Verletzungen des Schutzes personenbezogener Daten meldet WEBklar dem Kunden unverzüglich, möglichst innerhalb von 24 Stunden nach Kenntnis, mit den Informationen, die zu diesem Zeitpunkt vorliegen.</p>
|
||||
|
||||
<p>(4) WEBklar unterstützt den Kunden bei seinen Pflichten aus Art. 32 bis 36 DSGVO. Dazu gehören die Angaben, die er für sein Verzeichnis von Verarbeitungstätigkeiten und für eine etwaige Datenschutz-Folgenabschätzung braucht.</p>
|
||||
|
||||
<p>(5) Bestätigt der Kunde seine Rechtstexte im Kundenportal (Allgemeine Geschäftsbedingungen, Widerrufsbelehrung, Muster-Widerrufsformular, Versand und Zahlung, Gewährleistungshinweis sowie die Adressen von Impressum und Datenschutzerklärung), legt WEBklar jede bestätigte Fassung einmal im Archiv der Rechtstexte ab: mit ihrer Prüfsumme (Hash), dem Zeitpunkt und der Angabe, welche angemeldete Person des Kunden (Name oder E-Mail-Adresse) sie bestätigt hat. Die Bestellung speichert die Prüfsumme der Fassung, die der Käufer bei der Bestellung gesehen hat. Die Bestellbestätigung (G09) zeigt die archivierte Fassung zu dieser Prüfsumme, nicht die Fassung, die beim Versand der E-Mail gerade gilt; fehlt die archivierte Fassung ausnahmsweise, verwendet WEBklar die aktuell bestätigte Fassung. Das Archiv enthält keine Daten von Käufern. WEBklar löscht es mit den übrigen Daten des Webshops (Nummer 9 Abs. 6), nicht früher. Das Archiv hilft dem Kunden, die Fassung nachzuweisen, die ein Käufer erhalten hat; es ersetzt nicht seine eigene Aufbewahrung (Nummer 9 Abs. 8). Die Bestellbestätigung enthält diese Texte; WEBklar löscht ihren Inhalt nach dem Versand aus der Versandwarteschlange (Nummer 9 Abs. 4).</p>
|
||||
|
||||
<p>(6) Die Käufer informiert der Kunde (Art. 13 DSGVO; Nummer 11 Abs. 2).</p>
|
||||
|
||||
<h2>9. Löschung und Rückgabe</h2>
|
||||
|
||||
<p>(1) Eine Bestellung, die nicht bezahlt wird, verfällt 30 Minuten nach dem Anlegen und gilt als storniert. WEBklar löscht nicht bezahlte Bestellungen (offen oder storniert) 30 Tage nach dem Anlegen. Den IP-Hash löscht WEBklar aus dem Datensatz, sobald die Bestellung bezahlt oder storniert wird, spätestens mit dieser Löschung.</p>
|
||||
|
||||
<p>(2) Bei bezahlten Bestellungen anonymisiert WEBklar die Käuferdaten 3 Jahre nach dem Ende des Kalenderjahres, in dem die Bestellung angelegt wurde (Bestelljahr; maßgeblich ist die Zeit in Berlin): Name, E-Mail-Adresse sowie Liefer- und Rechnungsadresse werden gelöscht (der IP-Hash ist mit der Zahlung bereits gelöscht; eine Telefonnummer des Käufers wird nicht gespeichert). Bestellnummer, Positionen, Beträge, Steuerausweis, Status, Versanddienst und Sendungsnummer bleiben für die Buchhaltung des Kunden erhalten.</p>
|
||||
|
||||
<p>(3) Widerrufe nach Nummer 3 Abs. 2 behandelt WEBklar wie bezahlte Bestellungen: Name, E-Mail-Adresse und Erklärung werden 3 Jahre nach dem Ende des Kalenderjahres des Eingangs gelöscht; die Bestellnummer, die die Person angegeben hat, und der Zeitpunkt des Eingangs bleiben.</p>
|
||||
|
||||
<p>(4) In der Versandwarteschlange für E-Mails leert WEBklar die Empfängeradresse und den Inhalt einer E-Mail nach dem Versand und löscht den Eintrag 7 Tage nach dem Versand. E-Mails, die nicht zugestellt werden können, löscht WEBklar mit Empfängeradresse und Inhalt 48 Stunden nach dem endgültigen Fehlschlag.</p>
|
||||
|
||||
<p>(5) Der Löschlauf läuft täglich; gelöscht oder anonymisiert wird frühestens nach Ablauf der jeweiligen Frist. Ist an einem Tag mehr zu bearbeiten, als ein Lauf schafft, folgt der Rest im nächsten Lauf.</p>
|
||||
|
||||
<p>(6) Ist der Webshop abgeschaltet oder das Wartungs-Abo beendet, kann der Kunde die Bestellungen und Widerrufe 90 Tage lang im Kundenportal lesen und als CSV-Datei exportieren. Danach löscht WEBklar alle Daten des Webshops: Artikel, Bilder, Bestellungen, Widerrufe, Einstellungen, Rechtstexte und das Archiv der bestätigten Fassungen der Rechtstexte (Nummer 8 Abs. 5). Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Angaben zur Einrichtung bestehen (zum Beispiel Adresse der Website, Einbau-Code und die Angaben des Kunden aus der Anfrage) und enthält keine Daten von Käufern; dafür gilt die Datenschutzerklärung von WEBklar (Abschnitt 6). Beginnt innerhalb dieser Frist wieder ein Wartungs-Abo, schaltet WEBklar den Webshop auf Wunsch des Kunden mit den vorhandenen Daten wieder ein (Nr. 10 Abs. 5 der Leistungsbeschreibung). Früher anonymisiert WEBklar einzelne Daten auf Weisung des Kunden (Nummer 8 Abs. 2); alle Daten des Webshops löscht WEBklar auf Weisung des Kunden in Textform unverzüglich.</p>
|
||||
|
||||
<p>(7) Die Daten sind in der täglichen Vollsicherung des Servers enthalten (§ 3 Abs. 5 der Abo-Bedingungen). Gelöschte oder anonymisierte Daten bleiben dort bis zu sieben Tage erhalten und werden dann überschrieben. Wird eine Sicherung zurückgespielt, holt der tägliche Löschlauf die fälligen Löschungen und Anonymisierungen nach; Löschungen auf Weisung des Kunden wiederholt WEBklar.</p>
|
||||
|
||||
<p>(8) Gesetzliche Aufbewahrungspflichten bleiben unberührt. Der Kunde ist für seine eigenen Aufbewahrungspflichten (zum Beispiel für Buchungsbelege) selbst verantwortlich; WEBklar ist kein Archiv im Sinne der Buchführung, und das Archiv der Rechtstexte dient nur dem Nachweis der Texte. Der Kunde sichert die Belege, die er braucht, rechtzeitig vor der Anonymisierung oder Löschung durch den Export und die Belege von Stripe.</p>
|
||||
|
||||
<h2>10. Nachweise und Kontrollen</h2>
|
||||
|
||||
<p>WEBklar stellt dem Kunden alle Informationen zur Verfügung, die zum Nachweis der Pflichten aus Art. 28 DSGVO nötig sind, und ermöglicht Überprüfungen einschließlich Inspektionen durch den Kunden oder einen von ihm beauftragten, zur Verschwiegenheit verpflichteten Prüfer. Sie finden nach Anmeldung mit angemessener Frist (in der Regel 14 Tage) zu den Servicezeiten statt, ohne den Betrieb unzumutbar zu stören. Für das Rechenzentrum genügen in der Regel die Nachweise des Betreibers (Zertifizierung der Hetzner Online GmbH nach ISO/IEC 27001). Für mehr als eine Vor-Ort-Kontrolle pro Jahr kann WEBklar eine angemessene Vergütung verlangen.</p>
|
||||
|
||||
<h2>11. Pflichten des Kunden</h2>
|
||||
|
||||
<p>(1) Der Kunde ist Verantwortlicher für die Verarbeitung der Daten seiner Käufer im Webshop.</p>
|
||||
|
||||
<p>(2) Der Kunde informiert seine Käufer nach Art. 13 DSGVO. Dazu nimmt er den Datenschutz-Absatz für den Webshop in seine Datenschutzerklärung auf (Nr. 5 Abs. 10 der Leistungsbeschreibung) und nennt in seinen Rechtstexten (Datenschutzerklärung, Impressum) die Angaben, die das Gesetz verlangt.</p>
|
||||
|
||||
<p>(3) Der Kunde schließt mit Stripe den Vertrag über sein Stripe-Konto selbst und verantwortet die Verarbeitung von Daten durch Stripe, soweit sie nicht in diesem Vertrag geregelt ist (Nummer 7 Abs. 4).</p>
|
||||
|
||||
<p>(4) Der Kunde verkauft keine ausgeschlossenen Waren (Nr. 3 der Leistungsbeschreibung) und fordert von seinen Käufern keine Angaben zu ihrer Gesundheit oder zu anderen besonderen Kategorien personenbezogener Daten, auch nicht im Freitext der Widerrufserklärung.</p>
|
||||
|
||||
<p>(5) Der Kunde verändert den Einbau-Code und die gesetzlich vorgeschriebenen Hinweise des Widgets nicht und entfernt den Link „Vertrag widerrufen“ nicht (Nr. 5 Abs. 6 und 9 der Leistungsbeschreibung).</p>
|
||||
|
||||
<h2>12. Haftung, Änderungen, Schlussbestimmungen</h2>
|
||||
|
||||
<p>(1) Gegenüber betroffenen Personen gilt Art. 82 DSGVO. Für die Haftung zwischen Kunde und WEBklar gilt § 13 der Abo-Bedingungen.</p>
|
||||
|
||||
<p>(2) Eine neue Fassung dieses Vertrags oder der Leistungsbeschreibung, auch wegen geänderter Datenerhebung, teilt WEBklar dem Kunden mindestens sechs Wochen vor ihrem Wirksamwerden in Textform mit. Sie wird wirksam, wenn der Kunde ihr im Kundenportal oder in Textform zustimmt. Stimmt er nicht zu, gilt die bisherige Fassung weiter.</p>
|
||||
|
||||
<p>(3) Ist die neue Fassung nötig, weil ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form nicht mehr zulässt, und stimmt der Kunde ihr nicht zu, darf WEBklar den Webshop für seine Website nach Nr. 11 Abs. 2 der Leistungsbeschreibung beenden oder nach Nr. 11 Abs. 3 der Leistungsbeschreibung aussetzen. Das Abo bleibt davon unberührt.</p>
|
||||
|
||||
<p>(4) Im Übrigen gilt § 16 der Abo-Bedingungen.</p>
|
||||
|
||||
<h2>Anlage: Technische und organisatorische Maßnahmen des Webshops (Art. 32 DSGVO)</h2>
|
||||
|
||||
<table>
|
||||
<thead><tr><th>Bereich</th><th>Maßnahmen</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>Übertragung</td><td>Das Widget, die Seiten der Käufer und das Kundenportal werden nur über TLS (HTTPS) übertragen. Der Einbau-Code verweist auf eine Adresse mit https://. Die Zahlung geschieht auf der Zahlungsseite von Stripe.</td></tr>
|
||||
<tr><td>Speicher im Browser</td><td>Es werden keine Cookies gesetzt. Das Widget speichert nur den Warenkorb (Kennungen der Artikel und Mengen) im Sitzungsspeicher des Tabs. Es sendet seine Anfragen ohne Cookies und ruft nur die Schnittstelle des Webshops auf, keine Dienste Dritter.</td></tr>
|
||||
<tr><td>Zugriffskontrolle</td><td>Das Kundenportal ist nur nach Anmeldung erreichbar; jeder Kunde sieht nur die Daten seiner eigenen Projekte. Die öffentliche Schnittstelle des Widgets liefert nur für einen vorhandenen Webshop Daten und ist über den Website-Schlüssel (Site-Key) adressiert; Bestellungen und Widerrufe nimmt sie nur von Seiten des Kunden und des Portals an. Den Stand einer Bestellung ruft der Käufer nur mit einem geheimen Token ab, das nur als Prüfsumme gespeichert wird. Benachrichtigungen von Stripe werden nur nach Prüfung ihrer Signatur verarbeitet; die Schlüssel für Stripe liegen nur auf dem Server.</td></tr>
|
||||
<tr><td>Missbrauchsschutz</td><td>Ein verstecktes Feld (Honeypot), eine Mindestzeit bis zum Absenden des Formulars mit signiertem Formular-Token und eine Mindestantwortzeit schützen vor automatisierten Bestellungen und Widerrufen. Grenzen je IP-Adresse und je Webshop gelten im Arbeitsspeicher; ein Neustart leert sie. Die Obergrenze offener Bestellungen je IP-Adresse (drei in 30 Minuten) arbeitet mit einem gesalzenen Hashwert der IP-Adresse in der Bestellung (Nummer 3 Abs. 4).</td></tr>
|
||||
<tr><td>Datenminimierung</td><td>Kartendaten und Kontodaten erhält und speichert WEBklar nicht. Preise berechnet der Server aus den gespeicherten Artikeln, nie aus Angaben des Browsers. Eine Telefonnummer wird weder abgefragt noch gespeichert, die Rechnungsadresse nur, soweit Stripe sie für die Zahlungsart braucht. Mit der Bestellung und mit dem Widerruf werden keine IP-Adresse im Klartext und kein Browser-Kennzeichen gespeichert, bei der Bestellung nur der gesalzene IP-Hash, und der nur, solange die Bestellung offen ist (gelöscht mit Zahlung oder Stornierung). Die Käuferdaten werden nach den Fristen der Nummer 9 anonymisiert. Nach dem Versand einer E-Mail werden Empfängeradresse und Inhalt in der Warteschlange geleert.</td></tr>
|
||||
<tr><td>Trennung</td><td>Alle Artikel, Bestellungen und Widerrufe tragen die Kennung des Webshops und des Kunden; das Kundenportal und die öffentliche Schnittstelle lesen immer nur die Daten des jeweiligen Webshops. Artikelbilder liegen in einem eigenen Ordner je Webshop. Die Hintergrundläufe (Abgleich offener Bestellungen, Versand der E-Mails, Anonymisierung und Löschung) schreiben jeweils nur in das Dokument, um das es geht.</td></tr>
|
||||
<tr><td>Löschung</td><td>Täglicher Löschlauf für Bestellungen, Widerrufe und Warteschlange nach den Fristen der Nummer 9; die Daten abgeschalteter Webshops einschließlich des Archivs der bestätigten Rechtstexte 90 Tage nach dem Abschalten; das Einrichtungs-Ticket bleibt als Arbeitsnachweis mit einer Kurzfassung der Angaben zur Einrichtung bestehen und enthält keine Daten von Käufern (Nummer 9 Abs. 6).</td></tr>
|
||||
<tr><td>Protokolle</td><td>Die Programmprotokolle des Webshops und der Tagesbericht enthalten nur Zählungen und Bestellnummern, keine Namen, E-Mail-Adressen oder Adressen von Käufern. Server-Protokolle des Webservers werden nach 14 Tagen gelöscht (Anlage 2 der Abo-Bedingungen).</td></tr>
|
||||
<tr><td>Datensicherung</td><td>Tägliche Vollsicherung des Servers durch Hetzner; verfügbar sind die letzten sieben Tagessicherungen (§ 3 Abs. 5 der Abo-Bedingungen).</td></tr>
|
||||
<tr><td>Rechenzentrum</td><td>Hetzner Online GmbH, Rechenzentren Falkenstein und Nürnberg (Deutschland). Hetzner ist nach ISO/IEC 27001 zertifiziert; der Geltungsbereich umfasst die Hosting-Dienstleistungen und die Rechenzentren.</td></tr>
|
||||
<tr><td>Organisation</td><td>Verpflichtung aller Beteiligten auf Vertraulichkeit, Ablauf für Datenschutzverletzungen (Nummer 8 Abs. 3), Überprüfung der Maßnahmen mindestens einmal im Jahr.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,192 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="webklar-fassung" content="2026-10-16">
|
||||
<meta name="robots" content="noindex">
|
||||
<title>Auftragsverarbeitungsvertrag Website-Analyse – WEBklar</title>
|
||||
<style>
|
||||
:root { color-scheme: light; }
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; background: #fff; color: #1a1f36; font: 16px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; font-variant-numeric: tabular-nums; }
|
||||
main { max-width: 780px; margin: 0 auto; padding: 44px 20px 80px; }
|
||||
.brand { font-weight: 600; letter-spacing: 0.12em; font-size: 13px; color: #111; }
|
||||
h1 { font-size: 30px; line-height: 1.2; margin: 6px 0 6px; font-weight: 600; }
|
||||
h2 { font-size: 20px; margin: 36px 0 10px; padding-top: 14px; border-top: 1px solid #ebeef1; font-weight: 600; }
|
||||
h3 { font-size: 16.5px; margin: 22px 0 6px; font-weight: 600; }
|
||||
p, li { margin: 6px 0; }
|
||||
ul { padding-left: 24px; }
|
||||
.meta { color: #5e6678; font-size: 14px; }
|
||||
.box { border-top: 1px solid #d8dde3; border-bottom: 1px solid #d8dde3; padding: 14px 0; margin: 18px 0; }
|
||||
.box.widerspruch { border-top: 2px solid #111; border-bottom: 2px solid #111; }
|
||||
table { border-collapse: collapse; width: 100%; font-size: 15px; }
|
||||
td, th { padding: 6px 10px 6px 0; border-bottom: 1px solid #ebeef1; vertical-align: top; text-align: left; }
|
||||
th { font-size: 13px; text-transform: uppercase; letter-spacing: 0.04em; color: #5e6678; background: #f8f9fb; padding-left: 8px; }
|
||||
a { color: #111; text-decoration: underline; text-decoration-color: #d8dde3; text-underline-offset: 2px; }
|
||||
a:hover { background: #f6f8fa; }
|
||||
nav.top { font-size: 14px; margin-bottom: 22px; }
|
||||
.offen { display: inline-block; background: #fcedb9; color: #a82c00; padding: 1px 10px; border-radius: 6px; font-weight: 600; }
|
||||
@media print {
|
||||
@page { size: A4; margin: 18mm 17mm 18mm;
|
||||
@top-left { content: "WEBklar · AVV Website-Analyse"; font: 8pt sans-serif; color: #8a93a3; }
|
||||
@bottom-right { content: "Seite " counter(page) " von " counter(pages); font: 8pt sans-serif; color: #8a93a3; } }
|
||||
body { background: #fff; font-size: 10pt; line-height: 1.45; }
|
||||
main { max-width: none; padding: 0; }
|
||||
nav.top { display: none; }
|
||||
h1 { font-size: 20pt; }
|
||||
h2 { font-size: 13pt; margin-top: 16pt; break-after: avoid; }
|
||||
a { color: inherit; text-decoration: none; }
|
||||
.box, tr, ol.abs > li { break-inside: avoid; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<nav class="top"><a href="/dashboard.html">← Kundenportal</a> · <a href="/neu/rechtliches/abo-bedingungen.html">Abo-Bedingungen</a> · <a href="/neu/rechtliches/leistungsbeschreibung-website-analyse-2026-10-16.html">Leistungsbeschreibung Website-Analyse</a> · <a href="/neu/rechtliches/avv-website-analyse-2026-10-16.html">AVV Website-Analyse</a> · <a href="/datenschutz.html">Datenschutzerklärung</a> · <a href="https://webklar.com/impressum">Impressum</a></nav>
|
||||
|
||||
<div class="brand">WEBKLAR</div>
|
||||
<h1>Auftragsverarbeitungsvertrag Website-Analyse (Art. 28 DSGVO)</h1>
|
||||
|
||||
<p class="meta"><strong>Fassung 2026-10-16</strong> · Entwurf – Prüfung durch Kanzlei offen</p>
|
||||
|
||||
<p>zwischen dem Kunden als Verantwortlichem („Kunde“) und der WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, vertreten durch die Gesellschafter Kenso Grimm und Justin Klein, als Auftragsverarbeiter („WEBklar“).</p>
|
||||
|
||||
<h2>Vorbemerkung: Abschluss und Verhältnis zu anderen Verträgen</h2>
|
||||
|
||||
<p>(1) Der Kunde schließt diesen Vertrag, wenn er im Kundenportal die Funktion „Website-Analyse“ einschaltet und die dort angezeigte Zeile „Es gelten Leistungsbeschreibung und Auftragsverarbeitung (Fassung 2026-10-16)“ mit dem Klick auf „Einschalten“ bestätigt; diesem Vertrag und der Leistungsbeschreibung „Website-Analyse“ („Leistungsbeschreibung“) in der angezeigten Fassung stimmt er damit zu. Das Portal speichert den Wortlaut der angezeigten Zeile, die Fassungen, den Zeitpunkt, das Kundenkonto und die angemeldete Person. Anschließend sendet WEBklar dem Kunden eine E-Mail, die beide Texte in dieser Fassung als PDF enthält und auf sie verlinkt.</p>
|
||||
|
||||
<p>(2) Ein Mitarbeiter von WEBklar darf die Funktion nur einschalten, wenn der Kunde vorher in Textform (etwa im Chat des Kundenportals oder per E-Mail) erklärt hat, dass er diesem Vertrag und der Leistungsbeschreibung in der aktuellen Fassung zustimmt. WEBklar speichert die Fassung, das Datum und den Weg dieser Erklärung, den Zeitpunkt des Einschaltens und den Namen des Mitarbeiters. Der Kunde erhält die E-Mail nach Absatz 1.</p>
|
||||
|
||||
<p>(3) Dieser Vertrag ergänzt den Auftragsverarbeitungsvertrag in Anlage 1 der Abo-Bedingungen. Für die Website-Analyse geht er Anlage 1 vor; im Übrigen gilt Anlage 1 ergänzend. In Datenschutzfragen geht er den Abo-Bedingungen und der Leistungsbeschreibung „Website-Analyse“ vor.</p>
|
||||
|
||||
<h2>1. Gegenstand und Dauer</h2>
|
||||
|
||||
<p>(1) WEBklar betreibt für die öffentliche Website des Kunden eine Reichweitenmessung mit Einwilligungsbanner („Website-Analyse“) und verarbeitet dabei personenbezogene Daten der Besucher der Website im Auftrag des Kunden. Den Umfang beschreibt die Leistungsbeschreibung „Website-Analyse“.</p>
|
||||
|
||||
<p>(2) Der Vertrag beginnt mit dem Einschalten der Funktion. Er endet mit dem Ausschalten, spätestens mit dem Ende des Abos, das die Website-Analyse enthält (Hosting-Abo oder Wartungs-Abo); endet nur das Wartungs-Abo und läuft ein Hosting-Abo weiter, läuft dieser Vertrag weiter. Den Messdienst schaltet WEBklar bei Vertragsende spätestens am folgenden Tag ab. Die Pflichten aus diesem Vertrag gelten fort, bis alle Daten nach Nummer 9 gelöscht sind.</p>
|
||||
|
||||
<h2>2. Art und Zweck der Verarbeitung</h2>
|
||||
|
||||
<p>(1) Art: Erheben über ein Skript, das WEBklar beim Veröffentlichen in die Website des Kunden einfügt, Übermitteln an den Messdienst von WEBklar, Speichern, Verdichten zu Tageswerten, Auswerten im Kundenportal (Kennzahlen, Heatmap) und Löschen.</p>
|
||||
|
||||
<p>(2) Zwecke: Der Kunde soll erkennen, wie seine Website genutzt wird, um sie zu verbessern. Außerdem schützt WEBklar den Messdienst für den Kunden vor Missbrauch und verfälschten Zahlen (Mengenbegrenzung, Erkennen von Bots).</p>
|
||||
|
||||
<p>(3) WEBklar verwendet die Daten für keine eigenen Zwecke, auch nicht in anonymisierter Form und nicht für Auswertungen über mehrere Kunden. Es findet keine Werbung und keine Zusammenführung mit anderen Daten statt. WEBklar bildet keine Profile einzelner Besucher. Die Besucher-Kennung dient nur dazu, die Besucher eines Tages zu zählen.</p>
|
||||
|
||||
<h2>3. Datenarten und Betroffene</h2>
|
||||
|
||||
<p>(1) Solange der Besucher im Banner keine Wahl getroffen hat und sein Browser kein Signal nach Absatz 2 Satz 2 sendet: Pfad der aufgerufenen Seite (ohne Suchparameter und Anker), auch bei einem Seitenwechsel ohne Neuladen; Land (aus der IP-Adresse abgeleitet); Geräteklasse (Handy, Tablet oder Computer; aus dem Browser-Kennzeichen abgeleitet); Zeitpunkt (auf die volle Stunde gerundet); ein Zähler dafür, dass das Banner angezeigt wurde.</p>
|
||||
|
||||
<p>(2) Nach „Ablehnen“ sendet das Skript einmalig einen Zähler „abgelehnt“ und danach nichts mehr. Sendet der Browser die Signale „Global Privacy Control“ oder „Do Not Track“, zeigt das Skript kein Banner und sendet nichts.</p>
|
||||
|
||||
<p>(3) Nach „Annehmen“ zusätzlich: Domain der Website, von der der Besucher kam; Größenklasse des Browserfensters; eine täglich wechselnde pseudonyme Besucher-Kennung; Verweildauer (ab dem Laden der Seite) und Scrolltiefe je Seite; Klickpositionen mit einem Elementpfad aus Tag-Namen und Position (ohne Texte, Kennungen und Klassennamen); die Art von Kontakt-Klicks (Telefon, E-Mail, WhatsApp oder Link auf eine andere Website, dann mit deren Domain); ein Zähler „angenommen“.</p>
|
||||
|
||||
<p>(4) IP-Adresse und Browser-Kennzeichen (User-Agent) erhält der Server von WEBklar technisch bei jedem Abruf des Skripts und bei jeder Messanfrage, auch nach „Ablehnen“. Der Messdienst verarbeitet sie nur kurz im Arbeitsspeicher, um Land, Geräteklasse, Bots, die Mengenbegrenzung und, nur nach Einwilligung, die Besucher-Kennung zu bestimmen. Er speichert und protokolliert sie nicht. Nicht erhoben werden Cookies, Formularinhalte, Seitentexte, Mausbewegungen, Tastatureingaben und Sitzungsaufzeichnungen.</p>
|
||||
|
||||
<p>(5) Tageswerte sind Summen je Tag und Merkmal (etwa Aufrufe je Seite, je Land oder je Geräteklasse), ohne Besucher-Kennung. Enthalten die Pfade der Website personenbezogene Angaben, können auch Tageswerte personenbezogen sein (Nummer 11 Abs. 2).</p>
|
||||
|
||||
<p>(6) Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO) werden nicht gezielt verarbeitet. Je nach Inhalt der Website können sich aus Pfaden oder Domains aber Rückschlüsse ergeben (Nummer 11 Abs. 3).</p>
|
||||
|
||||
<p>(7) Betroffene: Besucher der Website des Kunden.</p>
|
||||
|
||||
<h2>4. Weisungen</h2>
|
||||
|
||||
<p>(1) WEBklar verarbeitet die Daten nur auf dokumentierte Weisung des Kunden, auch in Bezug auf eine Übermittlung in ein Drittland oder an eine internationale Organisation. Das gilt nicht, wenn das Recht der Europäischen Union oder Deutschlands WEBklar zu einer Verarbeitung verpflichtet. In diesem Fall teilt WEBklar dem Kunden die Pflicht vorher mit, soweit das Gesetz es nicht verbietet.</p>
|
||||
|
||||
<p>(2) Mit dem Einschalten weist der Kunde WEBklar an, die Website-Analyse in der Standardkonfiguration nach der Leistungsbeschreibung zu betreiben. Weitere Weisungen ergeben sich aus den Einstellungen im Kundenportal (Einschalten, Ausschalten, Akzentfarbe des Banners). Sonstige Weisungen erteilt der Kunde in Textform, etwa im Chat des Kundenportals. Weisungen, die von der Standardkonfiguration abweichen, setzt WEBklar nur um, wenn das technisch vorgesehen ist; sonst kann der Kunde die Funktion ausschalten.</p>
|
||||
|
||||
<p>(3) Hält WEBklar eine Weisung für rechtswidrig, weist es den Kunden unverzüglich darauf hin. WEBklar darf die Ausführung aussetzen, bis der Kunde die Weisung ändert oder die Rechtslage geklärt ist.</p>
|
||||
|
||||
<h2>5. Vertraulichkeit</h2>
|
||||
|
||||
<p>WEBklar setzt nur Personen ein, die zur Vertraulichkeit verpflichtet sind oder einer gesetzlichen Verschwiegenheitspflicht unterliegen.</p>
|
||||
|
||||
<h2>6. Technische und organisatorische Maßnahmen</h2>
|
||||
|
||||
<p>(1) WEBklar trifft die Maßnahmen nach Art. 32 DSGVO, die in der Anlage zu diesem Vertrag beschrieben sind. WEBklar darf sie an den Stand der Technik anpassen, sofern das Schutzniveau nicht sinkt.</p>
|
||||
|
||||
<p>(2) Für den Server gelten ergänzend die Maßnahmen der Anlage 2 der Abo-Bedingungen, soweit die Anlage zu diesem Vertrag nichts anderes regelt.</p>
|
||||
|
||||
<h2>7. Unterauftragsverarbeiter</h2>
|
||||
|
||||
<p>(1) Der Kunde genehmigt den Einsatz der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, für den Betrieb des Servers, auf dem der Messdienst läuft, und für dessen Sicherung. Ort der Verarbeitung ist das Rechenzentrum Falkenstein (Deutschland). Eine Übermittlung in Drittländer findet nicht statt.</p>
|
||||
|
||||
<p>(2) Will WEBklar einen weiteren Unterauftragsverarbeiter hinzuziehen oder einen ersetzen, teilt es das dem Kunden mindestens vier Wochen vorher in Textform mit. Der Kunde kann innerhalb von zwei Wochen nach Zugang der Mitteilung aus wichtigem datenschutzrechtlichem Grund in Textform widersprechen. WEBklar setzt den neuen Unterauftragsverarbeiter nicht vor Ablauf dieser Frist ein. Kündigt ein Unterauftragsverarbeiter von WEBklar eine eigene Änderung mit kürzerer Frist an, informiert WEBklar den Kunden unverzüglich, nachdem die Ankündigung eingegangen ist; der Kunde kann dann unverzüglich widersprechen. Einigen sich die Parteien nicht, kann der Kunde die Website-Analyse ausschalten und WEBklar sie für diese Website zu dem Zeitpunkt beenden, zu dem der neue Unterauftragsverarbeiter eingesetzt wird. Die Daten werden dann nach Nummer 9 gelöscht, auf Verlangen des Kunden sofort.</p>
|
||||
|
||||
<p>(3) WEBklar verpflichtet jeden Unterauftragsverarbeiter vertraglich auf die Datenschutzpflichten nach Art. 28 Abs. 4 DSGVO. Bei der Hetzner Online GmbH ist das deren Vertrag zur Auftragsverarbeitung, den WEBklar abgeschlossen hat. Kommt ein Unterauftragsverarbeiter seinen Datenschutzpflichten nicht nach, haftet WEBklar dem Kunden für die Einhaltung dieser Pflichten (Art. 28 Abs. 4 Satz 2 DSGVO).</p>
|
||||
|
||||
<p>(4) Die Länderzuordnung nutzt eine Datenbank, die WEBklar auf dem eigenen Server speichert („IP to Country Lite“ von DB-IP.com, Lizenz CC BY 4.0). Daten der Besucher werden dabei nicht an DB-IP übermittelt. DB-IP ist kein Unterauftragsverarbeiter.</p>
|
||||
|
||||
<h2>8. Unterstützung des Kunden, Betroffenenrechte</h2>
|
||||
|
||||
<p>(1) WEBklar leitet Anfragen betroffener Personen, die an WEBklar gerichtet sind, unverzüglich an den Kunden weiter. WEBklar unterstützt ihn mit geeigneten technischen und organisatorischen Maßnahmen bei der Beantwortung.</p>
|
||||
|
||||
<p>(2) Die gespeicherten Messdaten enthalten keine IP-Adresse, keinen Namen und keine Kennung, die länger als einen Tag gilt; Zeitpunkte sind auf die volle Stunde gerundet. Einzelne Besucher kann WEBklar daher in der Regel nicht identifizieren (Art. 11 DSGVO). Ein Abgleich kann aber möglich sein, solange die Zugriffsprotokolle der Website (Anlage 1 und 2 der Abo-Bedingungen, 14 Tage) vorhanden sind. Nennt eine betroffene Person zusätzliche Angaben, etwa Datum, Uhrzeit, besuchte Seiten oder ihre IP-Adresse, prüft WEBklar auf Weisung des Kunden, auch anhand dieser Protokolle, ob eine Zuordnung möglich ist, und teilt ihm das Ergebnis mit.</p>
|
||||
|
||||
<p>(3) Der Messung können Besucher technisch widersprechen (Art. 21 DSGVO): Nach „Ablehnen“ und bei den Browser-Signalen „Global Privacy Control“ und „Do Not Track“ sendet das Skript nichts mehr.</p>
|
||||
|
||||
<p>(4) WEBklar unterstützt den Kunden bei seinen Pflichten aus Art. 32 bis 36 DSGVO. Dazu gehören die Angaben, die er für sein Verzeichnis von Verarbeitungstätigkeiten und für eine etwaige Datenschutz-Folgenabschätzung braucht, und eine kurze Interessenabwägung für die Zählung nach Art. 6 Abs. 1 lit. f DSGVO, die WEBklar mit jeder Fassung dieses Vertrags bereitstellt.</p>
|
||||
|
||||
<p>(5) Verletzungen des Schutzes personenbezogener Daten meldet WEBklar dem Kunden unverzüglich, möglichst innerhalb von 24 Stunden nach Kenntnis, mit den Informationen, die zu diesem Zeitpunkt vorliegen.</p>
|
||||
|
||||
<p>(6) Zum Nachweis der Einwilligungen bewahrt WEBklar jede Fassung des Banner-Textes und der Zusatzseite mit dem Zeitraum ihrer Verwendung auf, bis drei Jahre nach dem Ende der Website-Analyse. WEBklar stellt sie dem Kunden auf Verlangen zur Verfügung. Ein Protokoll je Besucher führt WEBklar nicht.</p>
|
||||
|
||||
<p>(7) Die Besucher informiert der Kunde (Art. 13 DSGVO). WEBklar stellt ihm dafür die Zusatzseite „/datenschutz-analyse.html“ als Muster bereit. Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar dort einen Hinweis auf die Zusatzseite ein; sonst erhält der Kunde den Hinweis als Textbaustein (Leistungsbeschreibung Nr. 4 Abs. 4).</p>
|
||||
|
||||
<h2>9. Löschung und Rückgabe</h2>
|
||||
|
||||
<p>(1) Einzelne Ereignisse und Klickpositionen löscht WEBklar automatisch nach 30 Tagen. Der Löschlauf läuft täglich, die Löschung erfolgt also in der Regel spätestens am 31. Tag; fällt ein Lauf aus, holt der nächste ihn nach.</p>
|
||||
|
||||
<p>(2) Tageswerte löscht WEBklar 25 Monate nach dem jeweiligen Tag. Nach dem Ende der Website-Analyse (Ausschalten oder Ende des Abos, das sie enthält) löscht WEBklar alle Daten der Website am 61. Tag. Bis zum 46. Tag nach dem Ende kann der Kunde eine Kopie der Tageswerte und der noch vorhandenen Einzelereignisse verlangen; WEBklar stellt sie innerhalb von zwei Wochen als CSV-Datei bereit. Auf Weisung des Kunden löscht WEBklar die Daten früher.</p>
|
||||
|
||||
<p>(3) Die Daten sind in der täglichen Vollsicherung des Servers enthalten (§ 3 Abs. 5 der Abo-Bedingungen). Gelöschte Daten bleiben dort bis zu sieben Tage erhalten und werden dann überschrieben. Wird eine Sicherung zurückgespielt, holt der tägliche Löschlauf die fälligen Löschungen nach; Löschungen auf Weisung des Kunden wiederholt WEBklar.</p>
|
||||
|
||||
<p>(4) Gesetzliche Aufbewahrungspflichten bleiben unberührt.</p>
|
||||
|
||||
<h2>10. Nachweise und Kontrollen</h2>
|
||||
|
||||
<p>WEBklar stellt dem Kunden alle Informationen zur Verfügung, die zum Nachweis der Pflichten aus Art. 28 DSGVO nötig sind, und ermöglicht Überprüfungen einschließlich Inspektionen durch den Kunden oder einen von ihm beauftragten, zur Verschwiegenheit verpflichteten Prüfer. Sie finden nach Anmeldung mit angemessener Frist (in der Regel 14 Tage) zu den Servicezeiten statt, ohne den Betrieb unzumutbar zu stören. Für das Rechenzentrum genügen in der Regel die Nachweise des Betreibers (Zertifizierung der Hetzner Online GmbH nach ISO/IEC 27001). Für mehr als eine Vor-Ort-Kontrolle pro Jahr kann WEBklar eine angemessene Vergütung verlangen.</p>
|
||||
|
||||
<h2>11. Pflichten des Kunden</h2>
|
||||
|
||||
<p>(1) Der Kunde ist Verantwortlicher für die Messung. Er entfernt, verdeckt oder umgeht das Banner nicht. Er bindet ohne Abstimmung mit WEBklar keine weiteren Mess- oder Tracking-Werkzeuge ein.</p>
|
||||
|
||||
<p>(2) Enthalten die Pfade seiner Website personenbezogene Angaben, etwa Namen, Kundennummern oder E-Mail-Adressen, teilt der Kunde das WEBklar vor dem Einschalten mit.</p>
|
||||
|
||||
<p>(3) Richtet sich die Website auch gezielt an Kinder unter 16 Jahren oder lassen ihre Inhalte auf besondere Kategorien personenbezogener Daten schließen (etwa Gesundheit, Religion oder politische Meinung), gilt Nr. 3 Abs. 4 der Leistungsbeschreibung.</p>
|
||||
|
||||
<h2>12. Haftung, Änderungen, Schlussbestimmungen</h2>
|
||||
|
||||
<p>(1) Gegenüber betroffenen Personen gilt Art. 82 DSGVO. Für die Haftung zwischen Kunde und WEBklar gilt § 13 der Abo-Bedingungen.</p>
|
||||
|
||||
<p>(2) Eine neue Fassung dieses Vertrags oder der Leistungsbeschreibung teilt WEBklar dem Kunden mindestens sechs Wochen vor ihrem Wirksamwerden in Textform mit. Sie wird wirksam, wenn der Kunde ihr im Kundenportal oder in Textform zustimmt. Stimmt er nicht zu, gilt die bisherige Fassung weiter.</p>
|
||||
|
||||
<p>(3) Ist die neue Fassung nötig, weil ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form nicht mehr zulässt, und stimmt der Kunde ihr nicht zu, darf WEBklar die Website-Analyse für seine Website nach Nr. 8 Abs. 2 der Leistungsbeschreibung beenden oder aussetzen. Das Abo bleibt davon unberührt.</p>
|
||||
|
||||
<p>(4) Im Übrigen gilt § 16 der Abo-Bedingungen.</p>
|
||||
|
||||
<h2>Anlage: Technische und organisatorische Maßnahmen der Website-Analyse (Art. 32 DSGVO)</h2>
|
||||
|
||||
<table>
|
||||
<thead><tr><th>Bereich</th><th>Maßnahmen</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>Übertragung</td><td>Skript und Messdaten werden nur über TLS (HTTPS) übertragen.</td></tr>
|
||||
<tr><td>Speicher im Browser</td><td>Es werden keine Cookies gesetzt, und der Messdienst wertet keine Cookies aus. Messanfragen sendet das Skript ohne Cookies. Im lokalen Speicher des Browsers liegt nur die Banner-Wahl (Eintrag „wk-consent“ mit Wahl, Zeitpunkt und Fassung des Banner-Textes). Sie gilt 183 Tage; danach und bei einer neuen Fassung des Banner-Textes fragt das Banner erneut.</td></tr>
|
||||
<tr><td>Einwilligung</td><td>Solange keine Wahl getroffen ist und kein Browser-Signal vorliegt, sendet das Skript nur den Pfad der Seite, auch beim Seitenwechsel ohne Neuladen, und den Zähler „Banner angezeigt“. Nach „Ablehnen“ sendet es einmalig den Zähler „abgelehnt“ und danach nichts mehr, auch nicht aus anderen geöffneten Tabs. Bei „Global Privacy Control“ oder „Do Not Track“ zeigt es kein Banner und sendet nichts. Klick-, Scroll- und Verweildauer-Ereignisse registriert es erst nach „Annehmen“. Die Schaltflächen „Ablehnen“ und „Annehmen“ sind gleich gestaltet.</td></tr>
|
||||
<tr><td>Datenminimierung</td><td>IP-Adresse und Browser-Kennzeichen werden weder gespeichert noch protokolliert. Der Messdienst verwendet sie nur im Arbeitsspeicher für Land, Geräteklasse, Bot-Erkennung, Mengenbegrenzung und, nur nach Einwilligung, die Besucher-Kennung. Zeitpunkte werden auf die volle Stunde gerundet gespeichert. Pfade werden ohne Suchparameter und Anker gespeichert; Abschnitte, die wie Kennungen aussehen (lange Ziffernfolgen, UUIDs, E-Mail-Adressen), werden vorher ersetzt. Elementpfade bestehen nur aus Tag-Namen und Position. Formulareingaben und Seitentexte werden nicht erfasst.</td></tr>
|
||||
<tr><td>Pseudonymisierung</td><td>Die Besucher-Kennung (nur nach Einwilligung) ist ein auf 64 Bit gekürzter HMAC-SHA256 aus Website-Schlüssel, IP-Adresse und Browser-Kennzeichen mit einem Tagesschlüssel. Auch der Schlüssel der Mengenbegrenzung wird mit dem Tagesschlüssel gebildet. Der Tagesschlüssel liegt nur im Arbeitsspeicher; die Auslagerung auf die Festplatte ist für den Messdienst gesperrt. Er wird beim ersten Ereignis eines neuen Kalendertags (Europe/Berlin) und bei jedem Neustart neu erzeugt und nie gesichert. Ältere Kennungen lassen sich danach nicht mehr zuordnen.</td></tr>
|
||||
<tr><td>Zugriffskontrolle</td><td>Die Verwaltungsschnittstelle des Messdienstes (Auswertung, Website-Verwaltung) läuft auf einem internen Port, der nicht öffentlich erreichbar ist, und ist durch ein Zugriffstoken geschützt. Zugriff haben nur das Kundenportal und die Verwaltung von WEBklar. Das Portal zeigt jedem Kunden nur die Daten seiner eigenen Websites.</td></tr>
|
||||
<tr><td>Heatmap</td><td>Die Heatmap öffnet ein signierter Link, den nur der angemeldete Kunde im Kundenportal erzeugen kann. Der Link gilt 30 Minuten und liefert nur Klickpunkte der gewählten Seite und Ansicht, ohne Zeitpunkte und Kennungen.</td></tr>
|
||||
<tr><td>Eingangsprüfung</td><td>Messdaten werden nur angenommen, wenn die Website registriert und eingeschaltet ist und Origin bzw. Referer zu ihren Domains passen. Bots werden verworfen. Größe und Menge der Anfragen sind je Website und Absender begrenzt (Zähler nur im Arbeitsspeicher). Für ausgeschaltete Websites liefert der Messdienst ein leeres Skript aus.</td></tr>
|
||||
<tr><td>Löschung</td><td>Täglicher Löschlauf um 02:30 Uhr (Europe/Berlin): Ereignisse und Klicks nach 30 Tagen, Tageswerte nach 25 Monaten, alle Daten einer Website 60 Tage nach dem Ausschalten. Gelöschte Datensätze werden in der Datenbankdatei überschrieben und aus dem Schreibprotokoll (WAL) entfernt.</td></tr>
|
||||
<tr><td>Trennung</td><td>Der Messdienst läuft in einem eigenen Container mit eigenem Datenverzeichnis und hat keinen Zugriff auf andere Kundendaten. Alle Datensätze tragen den Website-Schlüssel; Abfragen erfolgen nur je Website.</td></tr>
|
||||
<tr><td>Protokolle</td><td>Der Reverse-Proxy führt kein Zugriffsprotokoll, und der Messdienst protokolliert keine Anfragen. Wird ein Zugriffsprotokoll eingeschaltet, dann nicht für die Adresse des Messdienstes. Meldungen der Container werden nach 14 Tagen gelöscht.</td></tr>
|
||||
<tr><td>Datensicherung</td><td>Tägliche Vollsicherung des Servers durch Hetzner; verfügbar sind die letzten sieben Tagessicherungen (§ 3 Abs. 5 der Abo-Bedingungen). Einen gesonderten Datenbank-Export des Messdienstes gibt es nicht.</td></tr>
|
||||
<tr><td>Rechenzentrum</td><td>Hetzner Online GmbH, Rechenzentrum Falkenstein (Deutschland). Hetzner ist nach ISO/IEC 27001 zertifiziert; der Geltungsbereich umfasst die Hosting-Dienstleistungen und die Rechenzentren.</td></tr>
|
||||
<tr><td>Organisation</td><td>Verpflichtung aller Beteiligten auf Vertraulichkeit, Ablauf für Datenschutzverletzungen (Nummer 8 Abs. 5), Archiv der Fassungen von Banner-Text und Zusatzseite (Nummer 8 Abs. 6), Überprüfung der Maßnahmen mindestens einmal im Jahr.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,96 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="webklar-fassung" content="2026-10-16">
|
||||
<meta name="robots" content="noindex">
|
||||
<title>Leistungsbeschreibung News-Pinnwand – WEBklar</title>
|
||||
<style>
|
||||
:root { color-scheme: light; }
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; background: #fff; color: #1a1f36; font: 16px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; font-variant-numeric: tabular-nums; }
|
||||
main { max-width: 780px; margin: 0 auto; padding: 44px 20px 80px; }
|
||||
.brand { font-weight: 600; letter-spacing: 0.12em; font-size: 13px; color: #111; }
|
||||
h1 { font-size: 30px; line-height: 1.2; margin: 6px 0 6px; font-weight: 600; }
|
||||
h2 { font-size: 20px; margin: 36px 0 10px; padding-top: 14px; border-top: 1px solid #ebeef1; font-weight: 600; }
|
||||
h3 { font-size: 16.5px; margin: 22px 0 6px; font-weight: 600; }
|
||||
p, li { margin: 6px 0; }
|
||||
ul { padding-left: 24px; }
|
||||
.meta { color: #5e6678; font-size: 14px; }
|
||||
.box { border-top: 1px solid #d8dde3; border-bottom: 1px solid #d8dde3; padding: 14px 0; margin: 18px 0; }
|
||||
.box.widerspruch { border-top: 2px solid #111; border-bottom: 2px solid #111; }
|
||||
table { border-collapse: collapse; width: 100%; font-size: 15px; }
|
||||
td, th { padding: 6px 10px 6px 0; border-bottom: 1px solid #ebeef1; vertical-align: top; text-align: left; }
|
||||
th { font-size: 13px; text-transform: uppercase; letter-spacing: 0.04em; color: #5e6678; background: #f8f9fb; padding-left: 8px; }
|
||||
a { color: #111; text-decoration: underline; text-decoration-color: #d8dde3; text-underline-offset: 2px; }
|
||||
a:hover { background: #f6f8fa; }
|
||||
nav.top { font-size: 14px; margin-bottom: 22px; }
|
||||
.offen { display: inline-block; background: #fcedb9; color: #a82c00; padding: 1px 10px; border-radius: 6px; font-weight: 600; }
|
||||
@media print {
|
||||
@page { size: A4; margin: 18mm 17mm 18mm;
|
||||
@top-left { content: "WEBklar · Leistungsbeschreibung News-Pinnwand"; font: 8pt sans-serif; color: #8a93a3; }
|
||||
@bottom-right { content: "Seite " counter(page) " von " counter(pages); font: 8pt sans-serif; color: #8a93a3; } }
|
||||
body { background: #fff; font-size: 10pt; line-height: 1.45; }
|
||||
main { max-width: none; padding: 0; }
|
||||
nav.top { display: none; }
|
||||
h1 { font-size: 20pt; }
|
||||
h2 { font-size: 13pt; margin-top: 16pt; break-after: avoid; }
|
||||
a { color: inherit; text-decoration: none; }
|
||||
.box, tr, ol.abs > li { break-inside: avoid; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<nav class="top"><a href="/dashboard.html">← Kundenportal</a> · <a href="/neu/rechtliches/abo-bedingungen.html">Abo-Bedingungen</a> · <a href="/neu/rechtliches/leistungsbeschreibung-news-2026-10-16.html">Leistungsbeschreibung News-Pinnwand</a> · <a href="/datenschutz.html">Datenschutzerklärung</a> · <a href="https://webklar.com/impressum">Impressum</a></nav>
|
||||
|
||||
<div class="brand">WEBKLAR</div>
|
||||
<h1>Leistungsbeschreibung News-Pinnwand</h1>
|
||||
|
||||
<p class="meta"><strong>Fassung 2026-10-16</strong> · Entwurf – Prüfung durch Kanzlei offen · Zusatzbedingungen zum Wartungs-Abo</p>
|
||||
|
||||
<h2>1. Geltung</h2>
|
||||
|
||||
<p>(1) Funktion im Sinne dieser Leistungsbeschreibung ist die News-Pinnwand. Diese Leistungsbeschreibung gilt, wenn der Kunde die Funktion im Kundenportal einschaltet und die dort angezeigte Zeile mit dem Klick auf „Einschalten“ bestätigt. Sie gilt für Funktionen, die ab ihrer Veröffentlichung eingeschaltet werden; hat der Kunde die Funktion zuvor einzeln gekauft, gilt für sie weiter die Leistungsbeschreibung, unter der er sie gekauft hat. Die Funktion ist Teil des Wartungs-Abos. Sie setzt ein laufendes Wartungs-Abo voraus; ein Hosting-Abo genügt nicht.</p>
|
||||
|
||||
<p>(2) Im Übrigen gelten die Abo-Bedingungen. Bei Widersprüchen geht für die News-Pinnwand diese Leistungsbeschreibung vor.</p>
|
||||
|
||||
<h2>2. Leistung</h2>
|
||||
|
||||
<p>(1) WEBklar baut die News-Pinnwand in die Website ein, die der Kunde für das gewählte Projekt bei WEBklar im Kundenportal führt. Die Pinnwand zeigt die Beiträge des Kunden als Zettel mit Titel, Text und Foto; pro Seite erscheinen sechs Beiträge, die neuesten zuerst.</p>
|
||||
|
||||
<p>(2) Die Beiträge schreibt, ändert und löscht der Kunde selbst im Kundenportal im Bereich „Funktionen“. Er wählt je Beitrag Zettelfarbe und Anzeigedauer; danach verschwindet der Beitrag von seiner Website. Hochgeladene Fotos werden verkleinert, alle Metadaten einschließlich Aufnahmeort (GPS) werden entfernt.</p>
|
||||
|
||||
<p>(3) Änderungen an der Website, die über den Einbau der Pinnwand hinausgehen, sind nicht Teil dieser Leistung.</p>
|
||||
|
||||
<h2>3. Einrichtung</h2>
|
||||
|
||||
<p>(1) Die Einrichtung der Funktion rechnet WEBklar als Arbeitszeit auf das Stundenkontingent des Wartungs-Abos an (§ 4 der Abo-Bedingungen). Reicht das verfügbare Kontingent nicht aus, führt WEBklar die Einrichtung trotzdem aus. Die fehlende Zeit ist ein Vorschuss, den die folgenden Monatskontingente ausgleichen. Eine zusätzliche Rechnung gibt es dafür nicht. Endet das Wartungs-Abo, solange noch ein Vorschuss offen ist, verfällt der Rest zugunsten des Kunden.</p>
|
||||
|
||||
<p>(2) Der Einbau ist erfahrungsgemäß meist innerhalb von 1–2 Wochen abgeschlossen. Das ist eine Einschätzung und keine verbindliche Zusage eines Termins. Nach Abschluss des Einbaus schaltet WEBklar die Funktion frei; ab dann erscheinen die Beiträge auf der Website.</p>
|
||||
|
||||
<p>(3) Hat WEBklar die Funktion auf Wunsch des Kunden schon bei der Erstellung seiner Website eingebaut, richtet sich die Abrechnung der Einrichtung nach der Vereinbarung zum Projekt und nicht nach dem Stundenkontingent. Auch dann läuft die Funktion nur, solange das Wartungs-Abo läuft.</p>
|
||||
|
||||
<p>(4) Arbeiten, die nicht zur Einrichtung gehören, führt WEBklar nur nach Absprache vor der Ausführung und nach Freigabe des Kunden in Textform aus; es gilt § 4 der Abo-Bedingungen.</p>
|
||||
|
||||
<h2>4. Pflichten des Kunden</h2>
|
||||
|
||||
<p>(1) Der Kunde ist für die Inhalte seiner Beiträge und Fotos verantwortlich. Er versichert, dass er die nötigen Rechte an Texten und Fotos hat und dass die Inhalte keine Rechte Dritter verletzen, etwa Urheberrechte, Persönlichkeitsrechte oder das Recht am eigenen Bild abgebildeter Personen.</p>
|
||||
|
||||
<h2>5. Ende der Leistung</h2>
|
||||
|
||||
<p>(1) Die Funktion läuft, solange das Wartungs-Abo läuft. Endet es, auch durch einen Wechsel ins Hosting-Abo oder durch den Widerruf eines Wechsels, schaltet WEBklar die Funktion spätestens am folgenden Tag ab. WEBklar informiert den Kunden per E-Mail.</p>
|
||||
|
||||
<p>(2) Auf Wunsch des Kunden schaltet WEBklar die Pinnwand jederzeit ab; für den Widerruf gilt Nr. 6.</p>
|
||||
|
||||
<p>(3) Beiträge und Fotos bleiben nach dem Abschalten 30 Tage gespeichert und werden danach gelöscht; beginnt in dieser Zeit wieder ein Wartungs-Abo, schaltet WEBklar die Pinnwand mit den vorhandenen Beiträgen ohne weitere Kosten wieder ein. Der Kunde kann seine Beiträge und Fotos jederzeit selbst löschen; andernfalls gelten die Fristen der Datenschutzerklärung (Abschnitt 6a).</p>
|
||||
|
||||
<h2>6. Widerruf</h2>
|
||||
|
||||
<p>(1) Das Einschalten der Funktion ist Teil des Abos und kein eigener Vertrag. Für den Widerruf gilt die Widerrufsbelehrung der Abo-Bedingungen. Widerruft ein Verbraucher das Abo oder den Wechsel ins Wartungs-Abo, endet die Funktion.</p>
|
||||
|
||||
<p>(2) Hat der Kunde die Funktion zuvor einzeln gekauft, bleibt die Widerrufsbelehrung maßgeblich, die er bei der Bestellung erhalten hat.</p>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,179 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="webklar-fassung" content="2026-10-16">
|
||||
<meta name="robots" content="noindex">
|
||||
<title>Leistungsbeschreibung Terminplaner – WEBklar</title>
|
||||
<style>
|
||||
:root { color-scheme: light; }
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; background: #fff; color: #1a1f36; font: 16px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; font-variant-numeric: tabular-nums; }
|
||||
main { max-width: 780px; margin: 0 auto; padding: 44px 20px 80px; }
|
||||
.brand { font-weight: 600; letter-spacing: 0.12em; font-size: 13px; color: #111; }
|
||||
h1 { font-size: 30px; line-height: 1.2; margin: 6px 0 6px; font-weight: 600; }
|
||||
h2 { font-size: 20px; margin: 36px 0 10px; padding-top: 14px; border-top: 1px solid #ebeef1; font-weight: 600; }
|
||||
h3 { font-size: 16.5px; margin: 22px 0 6px; font-weight: 600; }
|
||||
p, li { margin: 6px 0; }
|
||||
ul { padding-left: 24px; }
|
||||
.meta { color: #5e6678; font-size: 14px; }
|
||||
.box { border-top: 1px solid #d8dde3; border-bottom: 1px solid #d8dde3; padding: 14px 0; margin: 18px 0; }
|
||||
.box.widerspruch { border-top: 2px solid #111; border-bottom: 2px solid #111; }
|
||||
table { border-collapse: collapse; width: 100%; font-size: 15px; }
|
||||
td, th { padding: 6px 10px 6px 0; border-bottom: 1px solid #ebeef1; vertical-align: top; text-align: left; }
|
||||
th { font-size: 13px; text-transform: uppercase; letter-spacing: 0.04em; color: #5e6678; background: #f8f9fb; padding-left: 8px; }
|
||||
a { color: #111; text-decoration: underline; text-decoration-color: #d8dde3; text-underline-offset: 2px; }
|
||||
a:hover { background: #f6f8fa; }
|
||||
nav.top { font-size: 14px; margin-bottom: 22px; }
|
||||
.offen { display: inline-block; background: #fcedb9; color: #a82c00; padding: 1px 10px; border-radius: 6px; font-weight: 600; }
|
||||
@media print {
|
||||
@page { size: A4; margin: 18mm 17mm 18mm;
|
||||
@top-left { content: "WEBklar · Leistungsbeschreibung Terminplaner"; font: 8pt sans-serif; color: #8a93a3; }
|
||||
@bottom-right { content: "Seite " counter(page) " von " counter(pages); font: 8pt sans-serif; color: #8a93a3; } }
|
||||
body { background: #fff; font-size: 10pt; line-height: 1.45; }
|
||||
main { max-width: none; padding: 0; }
|
||||
nav.top { display: none; }
|
||||
h1 { font-size: 20pt; }
|
||||
h2 { font-size: 13pt; margin-top: 16pt; break-after: avoid; }
|
||||
a { color: inherit; text-decoration: none; }
|
||||
.box, tr, ol.abs > li { break-inside: avoid; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<nav class="top"><a href="/dashboard.html">← Kundenportal</a> · <a href="/neu/rechtliches/abo-bedingungen.html">Abo-Bedingungen</a> · <a href="/neu/rechtliches/leistungsbeschreibung-terminplaner-2026-10-16.html">Leistungsbeschreibung Terminplaner</a> · <a href="/neu/rechtliches/avv-terminplaner-2026-10-16.html">AVV Terminplaner</a> · <a href="/datenschutz.html">Datenschutzerklärung</a> · <a href="https://webklar.com/impressum">Impressum</a></nav>
|
||||
|
||||
<div class="brand">WEBKLAR</div>
|
||||
<h1>Leistungsbeschreibung Terminplaner</h1>
|
||||
|
||||
<p class="meta"><strong>Fassung 2026-10-16</strong> · Entwurf – Prüfung durch Kanzlei offen · Zusatzbedingungen zum Wartungs-Abo</p>
|
||||
|
||||
<h2>1. Geltung</h2>
|
||||
|
||||
<p>(1) Funktion im Sinne dieser Leistungsbeschreibung ist der Terminplaner. Diese Leistungsbeschreibung gilt, wenn der Kunde die Funktion im Kundenportal einschaltet und die dort angezeigte Zeile mit dem Klick auf „Einschalten“ bestätigt. Sie gilt für Funktionen, die ab ihrer Veröffentlichung eingeschaltet werden; hat der Kunde die Funktion zuvor einzeln gekauft, gilt für sie weiter die Leistungsbeschreibung, unter der er sie gekauft hat. Die Funktion ist Teil des Wartungs-Abos. Sie setzt ein laufendes Wartungs-Abo voraus; ein Hosting-Abo genügt nicht. Wie die Zustimmung abläuft, regelt die Vorbemerkung des AVV Terminplaner.</p>
|
||||
|
||||
<p>(2) Im Übrigen gelten die Abo-Bedingungen. Bei Widersprüchen geht für den Terminplaner diese Leistungsbeschreibung vor, in Datenschutzfragen der AVV Terminplaner.</p>
|
||||
|
||||
<p>(3) Leistungsbeschreibung B Nr. 3 der Abo-Bedingungen („Rechtstexte … nicht enthalten“) gilt nicht für den Datenschutz-Absatz nach Nr. 4 Abs. 9 und die Gästetexte nach Nr. 2 Abs. 6. Beides ist Teil des Terminplaners.</p>
|
||||
|
||||
<h2>2. Leistung</h2>
|
||||
|
||||
<p>(1) Der Terminplaner ist ein Buchungs-Widget für die Website des Kunden mit einer Verwaltung im Kundenportal. WEBklar baut je Website-Projekt ein Widget an einer Stelle der Website ein, entweder eingebettet in die Seite oder hinter einem Knopf. Die Gestaltung richtet sich nach dem Fragebogen, den der Kunde vor dem Einschalten im Kundenportal beantwortet.</p>
|
||||
|
||||
<p>(2) Der Kunde legt seine Leistungen, seine Personen oder Plätze, seine Zeiten und seine Regeln für Buchungen fest. Gäste buchen im Widget einen Termin; je nach Einstellung ist die Buchung verbindlich oder eine Anfrage.</p>
|
||||
|
||||
<p>(3) Der Terminplaner versendet im Auftrag des Kunden E-Mails an die Gäste: den Bestätigungslink, die Bestätigung, Änderungen, Absagen, auf Wunsch des Gastes eine Erinnerung (nur mit seinem Haken) und die Eingangsbestätigung eines Widerrufs. Den Kunden benachrichtigt er über neue und geänderte Buchungen.</p>
|
||||
|
||||
<p>(4) Der Kunde sieht und verwaltet seine Termine im Kundenportal und kann sie als CSV-Datei exportieren. Dazu erhält er einen geheimen Kalender-Link (ICS), mit dem er seine Termine in einem Kalenderprogramm abonnieren kann.</p>
|
||||
|
||||
<p>(5) Der Terminplaner enthält einen Missbrauchsschutz gegen automatisierte Buchungen. Die Löschfristen für Buchungsdaten nennt Nummer 9 des AVV Terminplaner.</p>
|
||||
|
||||
<p>(6) Bezahlt wird nur vor Ort. Der Hinweis am Buchungsformular, die Texte der Haken, der Datenschutz-Absatz für die Website des Kunden und die Widerrufsbelehrung für Gäste sind Mustertexte der Standardkonfiguration (Nr. 11 Abs. 1).</p>
|
||||
|
||||
<h2>3. Nicht enthalten</h2>
|
||||
|
||||
<p>(1) Nicht enthalten sind SMS, Online-Zahlung, ein Kalender-Abgleich (es gibt nur das Abonnement des Kalender-Links nach Nr. 2 Abs. 4), Konten für Gäste, Marketing-E-Mails, Gruppentermine, Mehrsprachigkeit und der Einsatz im Gesundheitsbereich (Nr. 4 Abs. 3).</p>
|
||||
|
||||
<p>(2) Zusätzliche Wünsche, etwa ein zweites Widget, Knöpfe je Leistung oder ein Sonderdesign, gehören nicht zur Einrichtung nach Nr. 5. WEBklar führt sie nach Stunden aus (§ 4 der Abo-Bedingungen).</p>
|
||||
|
||||
<h2>4. Voraussetzungen und Pflichten des Kunden</h2>
|
||||
|
||||
<p>(1) Der Kunde hat ein laufendes Wartungs-Abo.</p>
|
||||
|
||||
<p>(2) Der Kunde trägt vor dem Einschalten seine Betriebsangaben ein, dazu die Adresse seines Impressums und die Adresse seiner Datenschutzerklärung.</p>
|
||||
|
||||
<p>(3) Der Terminplaner ist nicht für Angebote im Gesundheitsbereich bestimmt, weil dort besondere Kategorien personenbezogener Daten anfallen können (Art. 9 DSGVO). Der Kunde setzt ihn dort nicht ein und fordert von seinen Gästen keine Gesundheitsangaben. WEBklar darf die Einrichtung für eine solche Website ablehnen oder den Terminplaner dort abschalten. Der Fragebogen fragt die Branche ab; wählt der Kunde den Gesundheitsbereich, ist der Terminplaner für ihn nicht verfügbar.</p>
|
||||
|
||||
<p>(4) Richtet sich das Angebot des Kunden gezielt an Kinder unter 16 Jahren, nutzt der Kunde den Terminplaner nicht.</p>
|
||||
|
||||
<p>(5) Der Kunde nennt eine Telefonnummer, unter der Gäste Termine vereinbaren können, wenn das Widget nicht erreichbar ist (Nr. 6 Abs. 2).</p>
|
||||
|
||||
<p>(6) Er lässt die Texte der Haken und Hinweise im Widget unverändert.</p>
|
||||
|
||||
<p>(7) Er informiert seine Beschäftigten darüber, dass ihre Namen oder Bezeichnungen, ihre Arbeitszeiten und Abwesenheiten im Terminplaner stehen und dass es einen geheimen Kalender-Link gibt.</p>
|
||||
|
||||
<p>(8) Die Pflichten gegenüber seinen Gästen trägt der Kunde: Preisangaben, Allgemeine Geschäftsbedingungen und Stornobedingungen sowie die Einstufung seiner Leistungen als Leistung für Verbraucher oder Unternehmer und als Freizeitleistung.</p>
|
||||
|
||||
<p>(9) Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar beim Einbau dort den Datenschutz-Absatz für die Online-Terminbuchung ein; das gehört zur Einrichtung. Liegt sie woanders, etwa bei einem externen Generator, erhält der Kunde den Absatz als Textbaustein und fügt ihn selbst ein. Für den übrigen Inhalt seiner Datenschutzerklärung bleibt der Kunde verantwortlich (§ 6 Abs. 3 der Abo-Bedingungen). Fällt WEBklar beim Einfügen ein offensichtlicher Widerspruch auf, weist WEBklar den Kunden darauf hin; eine Pflicht zur Prüfung der Datenschutzerklärung entsteht daraus nicht.</p>
|
||||
|
||||
<h2>5. Einrichtung</h2>
|
||||
|
||||
<p>(1) Die Einrichtung umfasst die Einrichtung nach dem Fragebogen, den Einbau des Widgets in die Website, den Datenschutz-Absatz nach Nr. 4 Abs. 9, die Abnahme durch den Kunden und die Freischaltung durch WEBklar.</p>
|
||||
|
||||
<p>(2) Die Einrichtung der Funktion rechnet WEBklar als Arbeitszeit auf das Stundenkontingent des Wartungs-Abos an (§ 4 der Abo-Bedingungen). Reicht das verfügbare Kontingent nicht aus, führt WEBklar die Einrichtung trotzdem aus. Die fehlende Zeit ist ein Vorschuss, den die folgenden Monatskontingente ausgleichen. Eine zusätzliche Rechnung gibt es dafür nicht. Endet das Wartungs-Abo, solange noch ein Vorschuss offen ist, verfällt der Rest zugunsten des Kunden.</p>
|
||||
|
||||
<p>(3) Die voraussichtliche Dauer der Einrichtung beträgt meist 1–2 Wochen. Sie ist eine Einschätzung und keine verbindliche Zusage eines Termins.</p>
|
||||
|
||||
<p>(4) Hat WEBklar die Funktion auf Wunsch des Kunden schon bei der Erstellung seiner Website eingebaut, richtet sich die Abrechnung der Einrichtung nach der Vereinbarung zum Projekt und nicht nach dem Stundenkontingent. Auch dann läuft die Funktion nur, solange das Wartungs-Abo läuft.</p>
|
||||
|
||||
<p>(5) Arbeiten, die nicht zur Einrichtung gehören, führt WEBklar nur nach Absprache vor der Ausführung und nach Freigabe des Kunden in Textform aus; es gilt § 4 der Abo-Bedingungen. Für den Betrieb des Terminplaners berechnet WEBklar neben dem Wartungs-Abo nichts weiter; er läuft, solange das Wartungs-Abo läuft (Nr. 8).</p>
|
||||
|
||||
<h2>6. Verfügbarkeit und Störungen</h2>
|
||||
|
||||
<p>(1) Das Verfügbarkeitsziel nach § 5 der Abo-Bedingungen gilt für die Website, nicht für den Terminplaner. Störungen des Terminplaners beheben wir in angemessener Frist (§ 12 der Abo-Bedingungen).</p>
|
||||
|
||||
<p>(2) Ist der Terminplaner nicht erreichbar, zeigt das Widget einen Ausweichtext mit der Telefonnummer des Kunden (Nr. 4 Abs. 5).</p>
|
||||
|
||||
<p>(3) Alle Zeiten gelten in der Zeitzone Europe/Berlin.</p>
|
||||
|
||||
<p>(4) WEBklar übernimmt keine Gewähr dafür, dass E-Mails zugestellt und gelesen werden. Das hängt vom Postfach des Empfängers und von den Mailgrenzen seines Anbieters ab.</p>
|
||||
|
||||
<p>(5) Die gesetzlichen Rechte des Kunden sowie §§ 12 und 13 der Abo-Bedingungen bleiben unberührt.</p>
|
||||
|
||||
<h2>7. Haftung für verpasste Termine</h2>
|
||||
|
||||
<p>(1) Der Terminplaner ist ein Hilfsmittel. Die Einhaltung der Termine bleibt Sache des Kunden.</p>
|
||||
|
||||
<p>(2) WEBklar übernimmt keine Gewähr für die Zustellung der E-Mails (Nr. 6 Abs. 4).</p>
|
||||
|
||||
<p>(3) Für entgangenen Gewinn und Folgeschäden aus verpassten Terminen haftet WEBklar nur im Rahmen von § 13 Abs. 1 und 2 der Abo-Bedingungen.</p>
|
||||
|
||||
<p>(4) Der Haftungsrahmen des Kunden gegenüber seinen Gästen bleibt unberührt.</p>
|
||||
|
||||
<h2>8. Ende der Leistung</h2>
|
||||
|
||||
<p>(1) Die Funktion läuft, solange das Wartungs-Abo läuft. Endet es, auch durch einen Wechsel ins Hosting-Abo oder durch den Widerruf eines Wechsels, schaltet WEBklar die Funktion spätestens am folgenden Tag ab. WEBklar informiert den Kunden per E-Mail.</p>
|
||||
|
||||
<p>(2) Mit dem Abschalten nimmt der Terminplaner keine neuen Buchungen mehr an, und das Widget zeigt den Ausweichtext. Die Termine bleiben im Kundenportal sichtbar, und der Kunde kann sie als CSV-Datei exportieren. Gäste können über ihren Link absagen. Bereits angeforderte Erinnerungen laufen weiter. Der Kunde informiert seine Gäste.</p>
|
||||
|
||||
<p>(3) WEBklar löscht alle Termindaten und die Einstellungen 30 Tage nach dem Abschalten. Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Einstellungen bestehen und enthält keine Daten von Gästen (Nummer 9 Abs. 6 des AVV Terminplaner).</p>
|
||||
|
||||
<p>(4) Beginnt innerhalb dieser 30 Tage wieder ein Wartungs-Abo, schaltet WEBklar den Terminplaner mit den vorhandenen Daten und Einstellungen ohne weitere Kosten wieder ein. Nach der Löschung ist eine neue Einrichtung nötig (neuer Fragebogen, Einrichtung nach Nr. 5).</p>
|
||||
|
||||
<p>(5) Endet das Wartungs-Abo, bevor WEBklar den Terminplaner freigeschaltet hat, bricht WEBklar die Einrichtung ab; ein Vorschuss nach Nr. 5 Abs. 2 verfällt.</p>
|
||||
|
||||
<p>(6) Auf Wunsch des Kunden in Textform schaltet WEBklar den Terminplaner jederzeit ab; für den Widerruf gilt Nr. 10.</p>
|
||||
|
||||
<h2>9. Änderungen und Einstellung</h2>
|
||||
|
||||
<p>(1) WEBklar darf die Technik des Terminplaners verbessern, solange der Leistungsumfang nicht schlechter wird (§ 3 Abs. 7 der Abo-Bedingungen). Ändert sich dabei, welche Daten erhoben werden, gilt Nummer 12 des AVV Terminplaner.</p>
|
||||
|
||||
<p>(2) WEBklar darf den Terminplaner für eine Website mit einer Frist von vier Wochen beenden oder für alle Kunden mit einer Frist von drei Monaten einstellen, wenn</p>
|
||||
|
||||
<ul>
|
||||
<li>ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form unzulässig macht oder nur mit wesentlich anderer Technik erlaubt und der Kunde einer dafür nötigen neuen Fassung nicht zustimmt (AVV Terminplaner Nummer 12), oder</li>
|
||||
<li>eine Sicherheitslücke sich nicht anders beheben lässt.</li>
|
||||
</ul>
|
||||
|
||||
<p>Die Mitteilung erfolgt in Textform und nennt den Grund, die Merkmale der Änderung, den Zeitpunkt und bei Verbrauchern das Recht, den Vertrag nach § 327r Abs. 3 BGB zu beenden, mit seiner Frist.</p>
|
||||
|
||||
<p>(3) Ist der Weiterbetrieb aus einem Grund nach Absatz 2 schon vor Ablauf der Frist nicht zulässig, darf WEBklar den Terminplaner ohne Vorankündigung aussetzen, solange das nötig ist. WEBklar informiert den Kunden darüber unverzüglich in Textform mit dem Grund und nimmt den Betrieb wieder auf, sobald das zulässig und technisch möglich ist.</p>
|
||||
|
||||
<p>(4) Eine Einstellung ohne einen Grund nach Absatz 2 ist ausgeschlossen. Gesetzliche Rechte von Verbrauchern bei Änderungen digitaler Produkte (§ 327r BGB) bleiben unberührt.</p>
|
||||
|
||||
<h2>10. Widerruf</h2>
|
||||
|
||||
<p>(1) Das Einschalten der Funktion ist Teil des Abos und kein eigener Vertrag. Für den Widerruf gilt die Widerrufsbelehrung der Abo-Bedingungen. Widerruft ein Verbraucher das Abo oder den Wechsel ins Wartungs-Abo, endet die Funktion.</p>
|
||||
|
||||
<p>(2) Hat der Kunde die Funktion zuvor einzeln gekauft, bleibt die Widerrufsbelehrung maßgeblich, die er bei der Bestellung erhalten hat.</p>
|
||||
|
||||
<h2>11. Hinweise</h2>
|
||||
|
||||
<p>(1) <strong>Keine Rechtsberatung.</strong> Der Hinweis am Buchungsformular, die Texte der Haken, der Datenschutz-Absatz und die Widerrufsbelehrung für Gäste sind Mustertexte für die Standardkonfiguration. WEBklar sorgt dafür, dass sie die Technik richtig beschreiben, und passt sie an, wenn sich die Technik ändert. Die Einstufung seiner Leistungen als Leistung für Verbraucher oder Unternehmer und als Freizeitleistung trägt der Kunde. Eine rechtliche Beratung im Einzelfall schuldet WEBklar nicht.</p>
|
||||
|
||||
<p>(2) <strong>Barrierefreiheit.</strong> Das Widget ist für die Bedienung mit Tastatur und Hilfstechniken gestaltet (Beschriftungen, Fokusführung, Fehlermeldungen als Live-Region). Eine Konformität nach WCAG 2.1 AA oder EN 301 549 sagt WEBklar nicht zu. Ob der Kunde als Dienstleistungserbringer unter das BFSG fällt (Ausnahme für Kleinstunternehmen, § 3 Abs. 3 BFSG), entscheidet und prüft der Kunde.</p>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,255 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="webklar-fassung" content="2026-10-16">
|
||||
<meta name="robots" content="noindex">
|
||||
<title>Leistungsbeschreibung Buchungskalender – WEBklar</title>
|
||||
<style>
|
||||
:root { color-scheme: light; }
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; background: #fff; color: #1a1f36; font: 16px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; font-variant-numeric: tabular-nums; }
|
||||
main { max-width: 780px; margin: 0 auto; padding: 44px 20px 80px; }
|
||||
.brand { font-weight: 600; letter-spacing: 0.12em; font-size: 13px; color: #111; }
|
||||
h1 { font-size: 30px; line-height: 1.2; margin: 6px 0 6px; font-weight: 600; }
|
||||
h2 { font-size: 20px; margin: 36px 0 10px; padding-top: 14px; border-top: 1px solid #ebeef1; font-weight: 600; }
|
||||
h3 { font-size: 16.5px; margin: 22px 0 6px; font-weight: 600; }
|
||||
p, li { margin: 6px 0; }
|
||||
ul { padding-left: 24px; }
|
||||
.meta { color: #5e6678; font-size: 14px; }
|
||||
.box { border-top: 1px solid #d8dde3; border-bottom: 1px solid #d8dde3; padding: 14px 0; margin: 18px 0; }
|
||||
.box.widerspruch { border-top: 2px solid #111; border-bottom: 2px solid #111; }
|
||||
table { border-collapse: collapse; width: 100%; font-size: 15px; }
|
||||
td, th { padding: 6px 10px 6px 0; border-bottom: 1px solid #ebeef1; vertical-align: top; text-align: left; }
|
||||
th { font-size: 13px; text-transform: uppercase; letter-spacing: 0.04em; color: #5e6678; background: #f8f9fb; padding-left: 8px; }
|
||||
a { color: #111; text-decoration: underline; text-decoration-color: #d8dde3; text-underline-offset: 2px; }
|
||||
a:hover { background: #f6f8fa; }
|
||||
nav.top { font-size: 14px; margin-bottom: 22px; }
|
||||
.offen { display: inline-block; background: #fcedb9; color: #a82c00; padding: 1px 10px; border-radius: 6px; font-weight: 600; }
|
||||
@media print {
|
||||
@page { size: A4; margin: 18mm 17mm 18mm;
|
||||
@top-left { content: "WEBklar · Leistungsbeschreibung Buchungskalender"; font: 8pt sans-serif; color: #8a93a3; }
|
||||
@bottom-right { content: "Seite " counter(page) " von " counter(pages); font: 8pt sans-serif; color: #8a93a3; } }
|
||||
body { background: #fff; font-size: 10pt; line-height: 1.45; }
|
||||
main { max-width: none; padding: 0; }
|
||||
nav.top { display: none; }
|
||||
h1 { font-size: 20pt; }
|
||||
h2 { font-size: 13pt; margin-top: 16pt; break-after: avoid; }
|
||||
a { color: inherit; text-decoration: none; }
|
||||
.box, tr, ol.abs > li { break-inside: avoid; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<nav class="top"><a href="/dashboard.html">← Kundenportal</a> · <a href="/neu/rechtliches/abo-bedingungen.html">Abo-Bedingungen</a> · <a href="/neu/rechtliches/leistungsbeschreibung-unterkunft-2026-10-16.html">Leistungsbeschreibung Buchungskalender</a> · <a href="/neu/rechtliches/avv-unterkunft-2026-10-16.html">AVV Buchungskalender</a> · <a href="/datenschutz.html">Datenschutzerklärung</a> · <a href="https://webklar.com/impressum">Impressum</a></nav>
|
||||
|
||||
<div class="brand">WEBKLAR</div>
|
||||
<h1>Leistungsbeschreibung Buchungskalender</h1>
|
||||
|
||||
<p class="meta"><strong>Fassung 2026-10-16</strong> · Entwurf – Prüfung durch Kanzlei offen · Zusatzbedingungen zum Wartungs-Abo</p>
|
||||
|
||||
<h2>1. Geltung</h2>
|
||||
|
||||
<p>(1) Diese Leistungsbeschreibung regelt die Funktion „Buchungskalender“. Mit dem Buchungskalender nimmt der Kunde über seine Website Anfragen für die Vermietung von Unterkünften entgegen (Ferienwohnungen, Monteurzimmer, kleine Pensionen) und rechnet die Aufenthalte ab („Vermieter“); Gäste sind die Besucher seiner Website, die im Buchungskalender anfragen und buchen („Gäste“). Die Funktion ist Teil des Wartungs-Abos. Sie setzt ein laufendes Wartungs-Abo voraus; ein Hosting-Abo genügt nicht. Sie richtet sich nur an Unternehmer, die in eigenem Namen und für eigene Rechnung vermieten.</p>
|
||||
|
||||
<p>(2) Diese Leistungsbeschreibung gilt, wenn der Vermieter die Funktion im Kundenportal einschaltet und die dort angezeigte Zeile mit dem Klick auf „Einschalten“ bestätigt. Sie gilt für Funktionen, die ab ihrer Veröffentlichung eingeschaltet werden; hat der Vermieter die Funktion zuvor einzeln gekauft, gilt für sie weiter die Leistungsbeschreibung, unter der er sie gekauft hat. Wie die Zustimmung abläuft, regelt die Vorbemerkung des AVV Buchungskalender. Einschalten kann der Vermieter erst, nachdem WEBklar seine Anfrage für den Buchungskalender geprüft und angenommen hat (Nr. 2 Abs. 1). Für einen Vermieter, dem WEBklar die Funktion als Teil seines Website-Projekts bereitstellt, schaltet WEBklar sie nach seiner Zustimmung in Textform frei; die Bedingungen dieser Leistungsbeschreibung gelten dann ebenso, die Einrichtung rechnet WEBklar nach Nr. 9 Abs. 3 ab.</p>
|
||||
|
||||
<p>(3) Im Übrigen gelten die Abo-Bedingungen. Bei Widersprüchen geht für den Buchungskalender diese Leistungsbeschreibung vor, in Datenschutzfragen der AVV Buchungskalender.</p>
|
||||
|
||||
<p>(4) Leistungsbeschreibung A Nr. 2 Abs. 3 und Nr. 4 sowie Leistungsbeschreibung B Nr. 3 der Abo-Bedingungen (Formulare mit Server, „Rechtstexte … nicht enthalten“) stehen dem Buchungskalender nicht entgegen. Das Widget des Buchungskalenders läuft auf dem Server von WEBklar, nicht auf dem Server der Website, und der Muster-Text zur Stornierung nach Nr. 6 ist Teil des Buchungskalenders.</p>
|
||||
|
||||
<h2>2. Leistung</h2>
|
||||
|
||||
<p>(1) Der Vermieter fragt den Buchungskalender im Kundenportal an und beschreibt seine Unterkünfte (Art der Unterkunft, Zahl der Einheiten, ob er Booking.com oder Airbnb nutzt). WEBklar prüft die Anfrage und nimmt sie an oder lehnt sie mit einer Begründung ab. Ein Anspruch auf Annahme besteht nicht. Erst nach der Annahme kann der Vermieter den Buchungskalender einschalten.</p>
|
||||
|
||||
<p>(2) Die Einrichtung umfasst den Einbau eines Kalender-Widgets an einer Stelle der Website des Vermieters, die Bereitstellung der Verwaltung im Kundenportal, den Einbau des Datenschutz-Absatzes für den Buchungskalender (Nr. 5 Abs. 10) und die Freischaltung durch WEBklar. Für jede Website wird ein Buchungskalender eingerichtet; er kann mehrere Unterkünfte desselben Vermieters enthalten. Zusätzlich gibt es für jeden freigegebenen Aufenthalt eine Buchungsseite bei WEBklar, die der Gast über den Link in seiner E-Mail erreicht.</p>
|
||||
|
||||
<p>(3) So läuft eine Buchung ab:</p>
|
||||
|
||||
<ul>
|
||||
<li>Das Widget zeigt dem Gast die belegten Nächte. Er wählt Anreise und Abreise, gibt Personenzahl, Namen, E-Mail-Adresse und auf Wunsch Firma, Telefonnummer und Nachricht an und sendet eine <strong>unverbindliche Anfrage</strong>. Er erhält eine Eingangsbestätigung, der Vermieter eine Benachrichtigung.</li>
|
||||
<li>Der Vermieter prüft die Anfrage im Kundenportal, sieht den vorgerechneten Preis, kann die Posten ändern und gibt die Anfrage mit einer Reservierungsfrist frei oder lehnt sie ab. Gibt er sie frei, reserviert der Buchungskalender die Nächte bis zum Ablauf der Frist, und der Gast erhält eine E-Mail mit Preis, Stornobedingungen und dem Link zur Buchungsseite.</li>
|
||||
<li>Auf der Buchungsseite sieht der Gast alle Angaben zum Aufenthalt, trägt seine Rechnungsadresse ein, wählt die Zahlart (online über Stripe, wenn der Vermieter sie eingeschaltet hat, oder vor Ort) und bucht mit der Schaltfläche zur zahlungspflichtigen Buchung. Erst damit kommt der Vertrag zwischen Vermieter und Gast zustande, nicht schon mit der Anfrage oder der Freigabe.</li>
|
||||
<li>Mit der Buchung erzeugt der Buchungskalender eine Rechnung mit lückenloser Nummer und sendet dem Gast eine Buchungsbestätigung mit der Rechnung als PDF; der Vermieter wird benachrichtigt.</li>
|
||||
<li>Verstreicht die Frist ohne Buchung, läuft die Reservierung ab und die Nächte werden frei; der Gast erhält darüber eine E-Mail.</li>
|
||||
<li>Der Vermieter kann einen Aufenthalt stornieren. Dann erhält der Gast eine Stornorechnung mit eigener Nummer und, soweit der Vermieter eine Online-Zahlung erstattet, eine Mitteilung über den Erstattungsbetrag. Eine Stornierung durch den Gast gibt es im Widget nicht; er wendet sich an den Vermieter.</li>
|
||||
</ul>
|
||||
|
||||
<p>(4) Im Kundenportal verwaltet der Vermieter:</p>
|
||||
|
||||
<ul>
|
||||
<li>Unterkünfte mit Beschreibung, Höchstzahl der Personen, Mindestnächten und Check-in- und Check-out-Zeiten,</li>
|
||||
<li>Preise (Staffel nach Aufenthaltsdauer, Endreinigung, Zusatzpersonen, Direktbuchungsrabatt, Steuerart und Steuersätze) und seine Stornobedingungen,</li>
|
||||
<li>Anfragen und Buchungen mit Status, die Eintragung „bezahlt (vor Ort)“ und Stornierungen,</li>
|
||||
<li>Kalender mit manuellen Sperren,</li>
|
||||
<li>die Verbindungen zu anderen Kalendern (Nr. 4),</li>
|
||||
<li>seine Rechnungen als PDF,</li>
|
||||
<li>seine Angaben als Aussteller der Rechnungen (Name, Anschrift, Steuernummer oder Umsatzsteuer-Identifikationsnummer, Bankverbindung, Rechnungspräfix) und die Verbindung mit seinem Stripe-Konto (Nr. 3).</li>
|
||||
</ul>
|
||||
|
||||
<p>(5) Der Buchungskalender versendet im Namen des Vermieters E-Mails an die Gäste: Eingang der Anfrage, Freigabe mit Buchungslink, Ablehnung, Buchungsbestätigung mit Rechnung, Zahlungseingang, Ablauf der Reservierung und Stornierung. Absender ist die Versandadresse von WEBklar mit dem Namen des Vermieters, als Antwortadresse dient seine E-Mail-Adresse. Den Vermieter benachrichtigt der Buchungskalender über neue Anfragen und neue Buchungen sowie, wenn der Abgleich mit einem anderen Kalender seit mindestens sechs Stunden fehlschlägt.</p>
|
||||
|
||||
<p>(6) Der Buchungskalender enthält einen Missbrauchsschutz gegen automatisierte Anfragen (verstecktes Feld, Mindest-Ausfüllzeit, Obergrenzen je IP-Adresse). Dafür speichert WEBklar mit der Anfrage nur einen Hashwert der IP-Adresse, nicht die IP-Adresse selbst; er wird entfernt, sobald der Vermieter die Anfrage freigibt oder ablehnt, spätestens nach 30 Tagen (Nummer 3 Abs. 4 des AVV Buchungskalender).</p>
|
||||
|
||||
<p>(7) Nicht enthalten sind:</p>
|
||||
|
||||
<ul>
|
||||
<li>der Meldeschein nach dem Bundesmeldegesetz und die Verwaltung der Meldedaten,</li>
|
||||
<li>Kurtaxe, Gästebeitrag und Beherbergungsabgaben,</li>
|
||||
<li>Anzahlungen und Teilzahlungen (der Gast zahlt den Gesamtbetrag in einer Zahlung, online oder vor Ort),</li>
|
||||
<li>Sofortbuchung ohne Freigabe,</li>
|
||||
<li>saisonale Preise (die Preise richten sich nur nach der Aufenthaltsdauer),</li>
|
||||
<li>Gutscheine, Channel-Manager-Anbindungen außer dem Kalenderabgleich nach Nr. 4 und Mehrsprachigkeit (der Buchungskalender ist deutschsprachig),</li>
|
||||
<li>die Buchführung des Vermieters.</li>
|
||||
</ul>
|
||||
|
||||
<p>Zusätzliche Wünsche, etwa ein Sonderdesign, ein zweiter Buchungskalender oder eine über das übliche Maß hinausgehende Zahl von Unterkünften, gehören nicht zur Einrichtung nach Nr. 9. WEBklar führt sie nach Absprache nach Stunden aus (Nr. 9 Abs. 4).</p>
|
||||
|
||||
<h2>3. Zahlungen über Stripe und Rechnungen im Namen des Vermieters</h2>
|
||||
|
||||
<p>(1) Online-Zahlungen im Buchungskalender sind optional und laufen über Stripe Connect. Der Vermieter verbindet dazu sein eigenes Stripe-Konto mit dem Buchungskalender. Hat er für den Webshop von WEBklar bereits ein Stripe-Konto verbunden, kann er es auch für den Buchungskalender verwenden. Den Vertrag über dieses Konto schließt der Vermieter unmittelbar mit Stripe; WEBklar ist daran nicht beteiligt. Für das Konto, die Prüfung des Vermieters durch Stripe und die Auszahlungen gelten die Bedingungen von Stripe. Ohne verbundenes und freigeschaltetes Stripe-Konto ist nur die Zahlart „vor Ort“ möglich.</p>
|
||||
|
||||
<p>(2) Das Geld der Gäste fließt direkt an den Vermieter (Zahlung unmittelbar auf dem Konto des Vermieters, sogenannte Direct Charge). WEBklar erhält und verwahrt kein Geld der Gäste, leitet keines weiter und beteiligt sich nicht am Umsatz des Vermieters. WEBklar ist kein Zahlungsdienstleister. Die Gebühren von Stripe trägt der Vermieter; sie fallen nur zwischen dem Vermieter und Stripe an.</p>
|
||||
|
||||
<p>(3) Der Vermieter prüft selbst, ob sein Stripe-Konto Zahlungen annehmen und Auszahlungen leisten kann. WEBklar zeigt ihm im Kundenportal den Stand, den Stripe meldet, hat aber keinen Einfluss darauf, ob und wann Stripe ein Konto freischaltet, einschränkt oder sperrt, und haftet dafür nicht. Rückbuchungen, Streitfälle mit Gästen und Zahlungsausfälle regeln der Vermieter und Stripe.</p>
|
||||
|
||||
<p>(4) Erstattungen bei einer Stornierung veranlasst der Vermieter im Kundenportal; WEBklar führt die Erstattung über das Stripe-Konto des Vermieters aus. Er bestimmt den Betrag bis zur Höhe der Zahlung des Gastes. Storniert der Vermieter einen Aufenthalt, schließt WEBklar eine noch offene Zahlungsseite bei Stripe. <strong>Automatische Erstattung:</strong> Geht eine Zahlung ein, nachdem die Buchung storniert oder die Reservierung abgelaufen ist, oder zahlt der Gast eine bereits bezahlte Buchung ein zweites Mal, erstattet WEBklar den Betrag ohne weitere Anweisung im Namen des Vermieters in voller Höhe über dessen Stripe-Konto. Dazu erteilt der Vermieter WEBklar mit der Zustimmung zum Buchungskalender eine dauerhafte Weisung; der Stand der Buchung ändert sich dadurch nicht. Erstattungen aus dem Stripe-Dashboard trägt WEBklar in die Buchung nach, soweit Stripe sie meldet.</p>
|
||||
|
||||
<p>(5) Die Zahlungsseite selbst (Stripe Checkout) betreibt Stripe. Online-Zahlungen sind derzeit nur per Karte möglich, einschließlich Apple Pay und Google Pay; eine SEPA-Lastschrift bietet der Buchungskalender nicht an. Kartendaten gibt der Gast nur dort ein; WEBklar erhält sie nicht. Beim Start der Zahlung übergibt WEBklar Stripe als Rücksprungadresse (bei Erfolg und bei Abbruch) den geheimen Link zur Buchungsseite des Gastes samt Token. Dieser Link ist damit für Stripe und im Stripe-Dashboard des Vermieters sichtbar; Der Link gilt bis 90 Tage nach der Abreise (bei nicht zustande gekommenen Buchungen 30 Tage) und öffnet nur diese eine Buchungsseite.</p>
|
||||
|
||||
<p>(6) <strong>Rechnungen im Namen des Vermieters.</strong> Der Vermieter beauftragt WEBklar mit dem Rechnungsauftrag in den Einstellungen des Buchungskalenders, in seinem Namen und für seine Rechnung Rechnungen und Stornorechnungen an seine Gäste auszustellen (Rechnung durch einen Dritten im Namen und im Auftrag des Leistenden, § 14 Abs. 2 Satz 4 UStG). Aussteller der Rechnung ist der Vermieter; sie trägt seinen Namen, seine Anschrift, seine Steuernummer oder Umsatzsteuer-Identifikationsnummer und keinen Hinweis auf WEBklar. WEBklar erzeugt die Rechnung aus den Angaben, die der Vermieter hinterlegt und bei der Freigabe bestätigt hat. Er verantwortet die Richtigkeit seiner Angaben, der Preise, der Steuerart und der Steuersätze; WEBklar prüft sie nicht steuerrechtlich. Die Rechnung wird nach ihrer Ausstellung nicht mehr verändert; Fehler korrigiert der Vermieter durch Stornierung und neue Buchung nach den Möglichkeiten des Kundenportals. Ohne diesen Auftrag stellt WEBklar keine Rechnungen aus. Widerruft er den Auftrag in Textform, stellt der Buchungskalender keine Rechnungen mehr aus und ist für ihn nicht mehr nutzbar.</p>
|
||||
|
||||
<p>(7) Die Rechnungsnummern sind je Vermieter und Jahr lückenlos fortlaufend; sie bestehen aus dem Präfix, das der Vermieter vor der ersten Rechnung festlegt, dem Jahr und einer laufenden Nummer.</p>
|
||||
|
||||
<h2>4. Verfügbarkeit und Kalenderabgleich (iCal)</h2>
|
||||
|
||||
<p>(1) Der Vermieter kann Kalender anderer Plattformen (zum Beispiel Booking.com oder Airbnb) über eine iCal-Adresse einlesen lassen; die eingelesenen Zeiträume blockiert der Buchungskalender. Umgekehrt stellt der Buchungskalender dem Vermieter eine geheime iCal-Adresse bereit, die er bei anderen Plattformen einträgt; sie enthält die belegten Nächte ohne Daten der Gäste. Der Vermieter kann diese Adresse jederzeit erneuern; die bisherige Adresse funktioniert dann nicht mehr.</p>
|
||||
|
||||
<p>(2) Der Abgleich läuft regelmäßig, aber zeitversetzt: WEBklar liest die Kalender anderer Plattformen etwa alle 15 Minuten ein, und die anderen Plattformen lesen die Adresse des Buchungskalenders nach ihrem eigenen Takt, oft nur alle paar Stunden. <strong>Zwischen einer Buchung hier und ihrem Eingang dort können deshalb Minuten bis Stunden vergehen, und umgekehrt.</strong> WEBklar sagt keine Echtzeit-Verfügbarkeit zu und übernimmt keine Gewähr dafür, dass eine Unterkunft nicht doppelt gebucht wird.</p>
|
||||
|
||||
<p>(3) Dieses Risiko mindert die Freigabe: Jede Anfrage im Buchungskalender braucht die Freigabe des Vermieters. Er prüft vor der Freigabe seine anderen Plattformen und seinen Kalender. Beim Freigeben und beim Buchen prüft der Buchungskalender zusätzlich, ob die Nächte nach seinem Stand frei sind. Eine Doppelbuchung entsteht trotzdem, wenn eine andere Plattform eine Buchung noch nicht gemeldet hat oder der Abgleich ausfällt. Der Vermieter löst eine Doppelbuchung selbst (etwa durch Stornierung) und trägt die Folgen gegenüber seinen Gästen und den Plattformen.</p>
|
||||
|
||||
<p>(4) Fehlerhafte oder nicht erreichbare iCal-Adressen kann WEBklar nicht beheben. Schlägt der Abruf fehl, bleibt der zuletzt gelesene Stand erhalten, und der Vermieter wird benachrichtigt, wenn der Abruf seit mindestens sechs Stunden nicht gelungen ist (höchstens einmal je 24 Stunden und Kalender). Eingelesen werden nur Zeiträume von gestern bis 18 Monate in die Zukunft, höchstens 1 Megabyte je Kalender; nur Adressen mit https werden abgerufen.</p>
|
||||
|
||||
<h2>5. Pflichten des Vermieters</h2>
|
||||
|
||||
<p>(1) Der Vermieter hat ein laufendes Wartungs-Abo und ist Unternehmer. Will er Online-Zahlungen nutzen, stellt er sein eigenes Stripe-Konto bereit und schließt die Anmeldung bei Stripe selbst ab. Der Vermieter vermietet in eigenem Namen; für Preise, Stornobedingungen, Rechnungen und Meldepflichten ist er verantwortlich. Online-Zahlungen seiner Gäste laufen, sofern er sie nutzt, über sein eigenes Stripe-Konto zu den Bedingungen von Stripe; WEBklar erhält und verwahrt kein Geld seiner Gäste und erhebt keine Umsatzbeteiligung. Rechnungen an Gäste stellt WEBklar nur aus, wenn der Vermieter in den Einstellungen des Buchungskalenders den Rechnungsauftrag erteilt hat (§ 14 Abs. 2 Satz 4 UStG); für die Richtigkeit von Preisen und Steuerangaben bleibt er verantwortlich.</p>
|
||||
|
||||
<p>(2) Er verantwortet seine Unterkünfte, Preise und Angaben zum Angebot. Dazu gehören richtige Preise einschließlich Steuerausweis (Umsatzsteuer, für die Beherbergung und die Endreinigung nach den dafür geltenden Steuersätzen, oder Hinweis auf § 19 UStG), Check-in- und Check-out-Zeiten, Höchstbelegung, Mindestnächte und alle Angaben, die er seinen Gästen vor der Buchung geben muss. Die Steuerart und die Steuersätze trägt er selbst im Kundenportal ein; WEBklar berät dazu nicht.</p>
|
||||
|
||||
<p>(3) Er legt vor der Freischaltung seine Stornobedingungen fest und verantwortet sie. Der Muster-Text nach Nr. 6 ist nur ein Vorschlag.</p>
|
||||
|
||||
<p>(4) Er verantwortet die Verfügbarkeit seiner Unterkünfte, pflegt Sperren und Kalenderverbindungen im Kundenportal und prüft regelmäßig auf neue Anfragen und Buchungen. Die Folgen von Angaben, die er setzt (zum Beispiel einer fälschlich freien oder gesperrten Zeit), trägt er.</p>
|
||||
|
||||
<p>(5) Er gibt Anfragen nur frei, wenn er die Unterkunft für den Zeitraum zu den angegebenen Bedingungen bereitstellen kann, und erfüllt die gebuchten Aufenthalte. Er trägt alle Pflichten eines Vermieters gegenüber seinen Gästen, auch Gewährleistung, Kundenservice und Beschwerden.</p>
|
||||
|
||||
<p>(6) Er erfüllt seine gesetzlichen Pflichten rund um die Beherbergung selbst. Das betrifft insbesondere den Meldeschein nach dem Bundesmeldegesetz, die Kurtaxe und andere kommunale Abgaben, Anzeige- und Genehmigungspflichten für die Vermietung (zum Beispiel Zweckentfremdungs- oder Gewerberecht) sowie die Pflichten aus seinem Steuerrecht. Der Buchungskalender nimmt ihm davon nichts ab.</p>
|
||||
|
||||
<p>(7) Anzahlungen und Teilzahlungen bietet der Buchungskalender nicht an. Vereinbart der Vermieter außerhalb des Buchungskalenders etwas anderes mit seinem Gast, ist das nicht Gegenstand dieser Leistungsbeschreibung.</p>
|
||||
|
||||
<p>(8) Er erfüllt seine Aufbewahrungspflichten selbst, etwa nach der Abgabenordnung und dem Handelsgesetzbuch. WEBklar ist kein Archiv für die Buchführung und kein Buchführungssystem. WEBklar bewahrt die Rechnungen des Vermieters im Buchungskalender 8 Jahre auf, solange der Buchungskalender läuft und nach Nr. 10 bis zur Löschung, hält sie ihm zum Abruf als PDF bereit und anonymisiert personenbezogene Daten der Gäste nach Nummer 9 des AVV Buchungskalender. Der Vermieter lädt seine Rechnungen deshalb rechtzeitig vor der Löschung nach Nr. 10 Abs. 3 herunter und bewahrt sie selbst auf.</p>
|
||||
|
||||
<p>(9) Er hält die Vorgaben dieser Leistungsbeschreibung ein und verändert den Einbau-Code und die gesetzlich vorgeschriebenen Hinweise des Widgets und der Buchungsseite nicht.</p>
|
||||
|
||||
<p>(10) Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar beim Einbau dort den Datenschutz-Absatz für den Buchungskalender ein; das gehört zur Einrichtung. Liegt sie woanders, erhält der Vermieter den Absatz als Textbaustein und fügt ihn selbst ein. Für den übrigen Inhalt seiner Datenschutzerklärung bleibt er verantwortlich (§ 6 Abs. 3 der Abo-Bedingungen). Fällt WEBklar beim Einfügen ein offensichtlicher Widerspruch auf, weist WEBklar den Vermieter darauf hin; eine Pflicht zur Prüfung der Datenschutzerklärung entsteht daraus nicht.</p>
|
||||
|
||||
<p>(11) Der Vermieter informiert seine Gäste nach Art. 13 DSGVO. Die Rollen im Datenschutz regelt der AVV Buchungskalender: Der Vermieter ist Verantwortlicher, WEBklar Auftragsverarbeiter, Stripe Vertragspartner des Vermieters.</p>
|
||||
|
||||
<h2>6. Muster-Text zur Stornierung, keine Rechtsberatung</h2>
|
||||
|
||||
<p>(1) WEBklar stellt dem Vermieter einen Muster-Text für Stornobedingungen bereit. Er ist eine <strong>Vorlage ohne Gewähr</strong> und keine Rechtsberatung (kein Rechtsrat im Sinne des Rechtsdienstleistungsgesetzes). Er passt zur Technik des Buchungskalenders, kann aber auf den Betrieb des Vermieters nicht zugeschnitten sein.</p>
|
||||
|
||||
<p>(2) WEBklar empfiehlt, die Stornobedingungen und die Angaben zum Angebot vor der Freischaltung von einer Rechtsanwältin oder einem Rechtsanwalt prüfen zu lassen.</p>
|
||||
|
||||
<p>(3) Der Vermieter bleibt für seine Stornobedingungen und alle seine Texte verantwortlich, auch wenn er den Muster-Text unverändert übernimmt (Nr. 5 Abs. 3). Die Stornobedingungen, die bei der Freigabe galten, speichert der Buchungskalender mit der Anfrage; die Buchungsseite und die Buchungsbestätigung zeigen dem Gast diese Fassung.</p>
|
||||
|
||||
<h2>7. Kein Widerrufsrecht des Gastes, Hinweise auf der Buchungsseite</h2>
|
||||
|
||||
<p>(1) Für die Buchung von Beherbergungsleistungen zu einem bestimmten Zeitpunkt oder in einem bestimmten Zeitraum besteht nach § 312g Abs. 2 Satz 1 Nr. 9 BGB kein gesetzliches Widerrufsrecht. Die Buchungsseite weist den Gast darauf hin; der Vermieter kann ihm vertraglich ein weitergehendes Recht einräumen, der Buchungskalender bietet dafür aber keine Funktion.</p>
|
||||
|
||||
<p>(2) Die Buchungsseite zeigt dem Gast vor der Schaltfläche zur zahlungspflichtigen Buchung die wesentlichen Angaben nach Art. 246a EGBGB, soweit der Buchungskalender sie kennt (Anbieter, Unterkunft, Zeitraum, Leistung, Preis insgesamt, Zahlart, Stornobedingungen, Hinweis auf das fehlende Widerrufsrecht). Der Vermieter ergänzt, was darüber hinausgeht, in seinen Texten selbst (Nr. 5 Abs. 2).</p>
|
||||
|
||||
<p>(3) Die Buchungsbestätigung erhält der Gast als E-Mail mit der Rechnung als PDF; sie enthält die Angaben zum Vertrag und die Stornobedingungen auf einem dauerhaften Datenträger.</p>
|
||||
|
||||
<h2>8. Verfügbarkeit und Störungen</h2>
|
||||
|
||||
<p>(1) Das Verfügbarkeitsziel nach § 5 der Abo-Bedingungen gilt für die Website, nicht für den Buchungskalender. WEBklar sagt für den Buchungskalender keine bestimmte Verfügbarkeit zu, die über den Betrieb der Website im Abo hinausgeht. Störungen des Buchungskalenders beheben wir in angemessener Frist (§ 12 der Abo-Bedingungen).</p>
|
||||
|
||||
<p>(2) Angekündigte Wartungsfenster (§ 5 der Abo-Bedingungen) können den Buchungskalender kurzzeitig unterbrechen. Während einer Unterbrechung sind Anfragen und Buchungen nicht möglich; das Widget zeigt dann einen Ausweichtext.</p>
|
||||
|
||||
<p>(3) Zahlungen übernimmt der Buchungskalender über Benachrichtigungen von Stripe und einen Abgleich beim Aufruf der Buchungsseite. Fällt die Verbindung zu Stripe oder zum Server aus, kann sich die Bestätigung einer Zahlung verzögern. Eine Zahlung, die nach einer Stornierung oder nach dem Ablauf der Reservierung eingeht, und eine zweite Zahlung auf eine bereits bezahlte Buchung erstattet WEBklar automatisch und in voller Höhe (Nr. 3 Abs. 4); die Buchung bleibt dabei unverändert, und die Erstattung braucht einige Tage, bis sie beim Gast ankommt. Ausfälle von Stripe, von Zahlungsarten und von Banken liegen nicht im Einflussbereich von WEBklar.</p>
|
||||
|
||||
<p>(4) Alle Zeiten gelten in der Zeitzone Europe/Berlin.</p>
|
||||
|
||||
<p>(5) WEBklar übernimmt keine Gewähr dafür, dass E-Mails zugestellt und gelesen werden. Das hängt vom Postfach des Empfängers und von den Mailgrenzen seines Anbieters ab.</p>
|
||||
|
||||
<p>(6) Die gesetzlichen Rechte des Vermieters sowie §§ 12 und 13 der Abo-Bedingungen bleiben unberührt.</p>
|
||||
|
||||
<h2>9. Einrichtung, Abrechnung und Widerruf</h2>
|
||||
|
||||
<p>(1) Die Einrichtung der Funktion rechnet WEBklar als Arbeitszeit auf das Stundenkontingent des Wartungs-Abos an (§ 4 der Abo-Bedingungen). Reicht das verfügbare Kontingent nicht aus, führt WEBklar die Einrichtung trotzdem aus. Die fehlende Zeit ist ein Vorschuss, den die folgenden Monatskontingente ausgleichen. Eine zusätzliche Rechnung gibt es dafür nicht. Endet das Wartungs-Abo, solange noch ein Vorschuss offen ist, verfällt der Rest zugunsten des Vermieters. Für den Betrieb des Buchungskalenders berechnet WEBklar nichts weiter; es gibt keine laufende Gebühr und keine Beteiligung am Umsatz oder an den Buchungen.</p>
|
||||
|
||||
<p>(2) Die Einrichtung dauert nach der Erfahrung von WEBklar meist 1 bis 2 Wochen ab dem Einschalten. Sie hängt davon ab, wie schnell der Vermieter Unterkünfte, Preise und gegebenenfalls das Stripe-Konto bereitstellt und wie umfangreich der Einbau ist. Die Dauer ist eine Einschätzung und keine verbindliche Zusage eines Termins.</p>
|
||||
|
||||
<p>(3) Hat WEBklar die Funktion auf Wunsch des Vermieters schon bei der Erstellung seiner Website eingebaut, richtet sich die Abrechnung der Einrichtung nach der Vereinbarung zum Projekt und nicht nach dem Stundenkontingent. Auch dann läuft die Funktion nur, solange das Wartungs-Abo läuft.</p>
|
||||
|
||||
<p>(4) Arbeiten, die nicht zur Einrichtung gehören, führt WEBklar nur nach Absprache vor der Ausführung und nach Freigabe des Vermieters in Textform aus; es gilt § 4 der Abo-Bedingungen. Welche Wünsche nicht zur Einrichtung gehören, beschreibt Nr. 2 Abs. 7.</p>
|
||||
|
||||
<p>(5) Das Einschalten der Funktion ist Teil des Abos und kein eigener Vertrag. Für den Widerruf gilt die Widerrufsbelehrung der Abo-Bedingungen. Widerruft ein Verbraucher das Abo oder den Wechsel ins Wartungs-Abo, endet die Funktion. Hat der Vermieter die Funktion zuvor einzeln gekauft, bleibt die Widerrufsbelehrung maßgeblich, die er bei der Bestellung erhalten hat. Das Widerrufsrecht gegenüber WEBklar ist vom Widerrufsrecht der Gäste gegenüber dem Vermieter (Nr. 7) zu unterscheiden.</p>
|
||||
|
||||
<h2>10. Laufzeit und Ende</h2>
|
||||
|
||||
<p>(1) Die Funktion läuft, solange das Wartungs-Abo läuft. Endet es, auch durch einen Wechsel ins Hosting-Abo oder durch den Widerruf eines Wechsels, schaltet WEBklar die Funktion spätestens am folgenden Tag ab. WEBklar informiert den Vermieter per E-Mail und weist ihn darauf hin, seine Rechnungen als PDF herunterzuladen.</p>
|
||||
|
||||
<p>(2) Mit dem Abschalten nimmt der Buchungskalender keine neuen Anfragen und Buchungen mehr an, und das Widget zeigt den Ausweichtext. Reservierungen laufen aus. Bereits gebuchte Aufenthalte bleiben Verträge zwischen dem Vermieter und seinen Gästen; der Vermieter erfüllt sie und informiert seine Gäste, wenn er den Buchungskalender nicht mehr anbietet. Anfragen, Buchungen und Rechnungen bleiben im Kundenportal 90 Tage lesbar; Rechnungen kann er einzeln als PDF herunterladen.</p>
|
||||
|
||||
<p>(3) WEBklar löscht alle Daten des Buchungskalenders, also Unterkünfte, Preise, Anfragen, Buchungen, Rechnungen, Kalenderverbindungen, Sperren und Einstellungen, 90 Tage nach dem Abschalten. Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Angaben zur Einrichtung bestehen und enthält keine Daten von Gästen (Nummer 9 Abs. 6 des AVV Buchungskalender). Die Aufbewahrungsfrist der Rechnungen (Nr. 5 Abs. 8) endet damit mit der Löschung; der Vermieter bewahrt sie danach selbst auf. Nach dem Abschalten weist WEBklar den Vermieter per E-Mail mit dem Löschdatum darauf hin, seine Rechnungen zu sichern; bis dahin sind sie im Kundenportal abrufbar. Wird das Kundenkonto gelöscht, schaltet WEBklar den Buchungskalender ab und löscht seine Daten wie nach Absatz 3 (90 Tage nach dem Abschalten); einen Hinweis per E-Mail gibt es dann nicht, weil mit dem Konto auch die E-Mail-Adresse gelöscht wird.</p>
|
||||
|
||||
<p>(4) Das Stripe-Konto bleibt beim Vermieter. Das Ende des Buchungskalenders berührt weder das Konto noch die Auszahlungen noch den Vertrag mit Stripe. Der Vermieter kann die Verbindung zwischen dem Konto und dem Buchungskalender im Stripe-Dashboard beenden.</p>
|
||||
|
||||
<p>(5) Beginnt innerhalb der 90 Tage wieder ein Wartungs-Abo, schaltet WEBklar den Buchungskalender auf Wunsch des Vermieters in Textform mit den vorhandenen Daten und Einstellungen ohne weitere Kosten wieder ein. Nach der Löschung ist eine neue Anfrage und eine neue Einrichtung nötig (Einrichtung nach Nr. 9).</p>
|
||||
|
||||
<p>(6) Endet das Wartungs-Abo, bevor WEBklar den Buchungskalender freigeschaltet hat, bricht WEBklar die Einrichtung ab; ein Vorschuss nach Nr. 9 Abs. 1 verfällt.</p>
|
||||
|
||||
<p>(7) Auf Wunsch des Vermieters in Textform schaltet WEBklar den Buchungskalender jederzeit ab; für den Widerruf gilt Nr. 9 Abs. 5.</p>
|
||||
|
||||
<h2>11. Haftung</h2>
|
||||
|
||||
<p>(1) Der Buchungskalender ist ein Hilfsmittel für die Vermietung des Vermieters. Der Vertrag über die Unterkunft besteht allein zwischen dem Vermieter und dem Gast; WEBklar ist daran nicht beteiligt.</p>
|
||||
|
||||
<p>(2) Für entgangenen Gewinn und Folgeschäden aus Störungen des Buchungskalenders, aus Verzögerungen oder Fehlern beim Kalenderabgleich (Nr. 4), aus Doppelbuchungen und aus nicht zugestellten E-Mails haftet WEBklar nur im Rahmen von § 13 Abs. 1 und 2 der Abo-Bedingungen.</p>
|
||||
|
||||
<p>(3) WEBklar haftet nicht für Inhalte des Vermieters, insbesondere nicht für seine Unterkünfte, Preise, Steuerangaben, Stornobedingungen und Rechtstexte, auch nicht für den Muster-Text, den er übernommen hat (Nr. 6), und nicht für die Erfüllung seiner Pflichten gegenüber den Gästen und den Behörden (Nr. 5).</p>
|
||||
|
||||
<p>(4) WEBklar haftet nicht für Ausfälle, Sperren oder Entscheidungen von Stripe und der Zahlungsarten sowie für Rückbuchungen und Zahlungsausfälle (Nr. 3 Abs. 3), und nicht für Ausfälle, Änderungen und Entscheidungen anderer Plattformen, deren Kalender der Vermieter einbindet.</p>
|
||||
|
||||
<p>(5) Die Haftung nach § 13 Abs. 1 der Abo-Bedingungen (unter anderem bei Vorsatz, grober Fahrlässigkeit, Verletzung von Leben, Körper oder Gesundheit und nach dem Produkthaftungsgesetz) bleibt unberührt. Der Haftungsrahmen des Vermieters gegenüber seinen Gästen bleibt unberührt.</p>
|
||||
|
||||
<h2>12. Änderungen und Einstellung</h2>
|
||||
|
||||
<p>(1) WEBklar darf die Technik des Buchungskalenders verbessern, solange der Leistungsumfang nicht schlechter wird (§ 3 Abs. 7 der Abo-Bedingungen). Ändert sich dabei, welche Daten erhoben werden, gilt Nummer 12 des AVV Buchungskalender.</p>
|
||||
|
||||
<p>(2) WEBklar darf den Buchungskalender für eine Website mit einer Frist von vier Wochen beenden oder für alle Vermieter mit einer Frist von drei Monaten einstellen, wenn</p>
|
||||
|
||||
<ul>
|
||||
<li>ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form unzulässig macht oder nur mit wesentlich anderer Technik erlaubt und der Vermieter einer dafür nötigen neuen Fassung nicht zustimmt (AVV Buchungskalender Nummer 12),</li>
|
||||
<li>Stripe die Anbindung nach Nr. 3 beendet oder wesentlich ändert, so dass die Online-Zahlung in der bisherigen Form nicht weiterbetrieben werden kann (dann entfällt nur die Online-Zahlung, soweit der Buchungskalender ohne sie weiterläuft), oder</li>
|
||||
<li>eine Sicherheitslücke sich nicht anders beheben lässt.</li>
|
||||
</ul>
|
||||
|
||||
<p>Die Mitteilung erfolgt in Textform und nennt den Grund, die Merkmale der Änderung, den Zeitpunkt und bei Verbrauchern das Recht, den Vertrag nach § 327r Abs. 3 BGB zu beenden, mit seiner Frist.</p>
|
||||
|
||||
<p>(3) Ist der Weiterbetrieb aus einem Grund nach Absatz 2 schon vor Ablauf der Frist nicht zulässig, darf WEBklar den Buchungskalender ohne Vorankündigung aussetzen, solange das nötig ist. WEBklar informiert den Vermieter darüber unverzüglich in Textform mit dem Grund und nimmt den Betrieb wieder auf, sobald das zulässig und technisch möglich ist.</p>
|
||||
|
||||
<p>(4) Eine Einstellung ohne einen Grund nach Absatz 2 ist ausgeschlossen. Gesetzliche Rechte von Verbrauchern bei Änderungen digitaler Produkte (§ 327r BGB) bleiben unberührt.</p>
|
||||
|
||||
<h2>13. Hinweise</h2>
|
||||
|
||||
<p>(1) <strong>Keine Rechtsberatung.</strong> Der Buchungskalender und der Muster-Text sind technische Hilfsmittel, keine Rechtsberatung (Nr. 6). Die rechtliche und steuerliche Einordnung seiner Vermietung, seiner Preisangaben und seines Angebots trägt der Vermieter.</p>
|
||||
|
||||
<p>(2) <strong>Barrierefreiheit.</strong> Das Widget ist für die Bedienung mit Tastatur und Hilfstechniken gestaltet (Beschriftungen der Tage, Bedienung mit Pfeiltasten, Fehlermeldungen als Live-Region). Eine Konformität nach WCAG 2.1 AA oder EN 301 549 sagt WEBklar nicht zu. Ob der Vermieter als Anbieter von Dienstleistungen im elektronischen Geschäftsverkehr unter das BFSG fällt (Ausnahme für Kleinstunternehmen, § 3 Abs. 3 BFSG), entscheidet und prüft der Vermieter.</p>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
Loaded 100 of 330 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user