From 0a2eab85d568d5b60df245fb871139955b76e377 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 08:47:19 +0000 Subject: [PATCH] Webshop: Funktionen-Karte mit Anfrage annehmen/ablehnen, Stripe-Stand, Kennzahlen und Rechtliches-Karten Webshop als Funktion im Reiter Funktionen (Aktionen wie beim Terminplaner), Panel je Projekt mit Anfrage, Entscheidung mit Begruendung, Stripe-Status und Zahlen; Karten Leistungsbeschreibung und AVV Webshop. Co-Authored-By: Claude Opus 5.5 --- scripts/setup-legal.mjs | 3 + .../customer/CustomerFunktionen.jsx | 96 ++++++++++++- src/lib/funktionenView.js | 107 +++++++++++++- src/lib/teamApi.js | 12 +- tests/setup-legal.test.mjs | 6 +- tests/unit/funktionenView.test.mjs | 136 +++++++++++++++++- 6 files changed, 347 insertions(+), 13 deletions(-) diff --git a/scripts/setup-legal.mjs b/scripts/setup-legal.mjs index fdfbdcb..7acd994 100644 --- a/scripts/setup-legal.mjs +++ b/scripts/setup-legal.mjs @@ -99,6 +99,9 @@ export const DOCUMENTS = [ { key: "leistungsbeschreibung-terminplaner", title: "Leistungsbeschreibung Terminplaner", liveUrl: "https://project.webklar.com/rechtliches/leistungsbeschreibung-terminplaner.html" }, { key: "avv-terminplaner", title: "AVV Terminplaner (Auftragsverarbeitung)", liveUrl: "https://project.webklar.com/rechtliches/avv-terminplaner.html" }, { key: "widerrufsbelehrung-funktionen", title: "Widerrufsbelehrung und Muster-Widerrufsformular (Funktionen)", liveUrl: "https://project.webklar.com/rechtliches/widerrufsbelehrung-funktionen.html" }, + // Webshop: gleiches Muster wie beim Terminplaner (Alias-Seite ohne Fassungsdatum) + { key: "leistungsbeschreibung-webshop", title: "Leistungsbeschreibung Webshop", liveUrl: "https://project.webklar.com/rechtliches/leistungsbeschreibung-webshop.html" }, + { key: "avv-webshop", title: "AVV Webshop", liveUrl: "https://project.webklar.com/rechtliches/avv-webshop.html" }, ].map((d, i) => ({ ...d, sort: i + 1 })); const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); diff --git a/src/components/customer/CustomerFunktionen.jsx b/src/components/customer/CustomerFunktionen.jsx index b8bb5e9..73b4a04 100644 --- a/src/components/customer/CustomerFunktionen.jsx +++ b/src/components/customer/CustomerFunktionen.jsx @@ -3,16 +3,17 @@ import { Link } from 'react-router-dom' import { FaCheck, FaCopy, FaRotateRight, FaSpinner } from 'react-icons/fa6' import { Card, Badge, Button, EmptyState } from '../ui' import { copyText } from '../CopyableCredential' -import { getFunktionen, setFeature, getTermineAnleitung } from '../../lib/teamApi' +import { getFunktionen, setFeature, getTermineAnleitung, getShop, entscheideShopAnfrage } from '../../lib/teamApi' import { FUNKTIONEN, ZUSTAND, AKTION_LABEL, funktionOf, aktionen, mergeFunktion, siteKeyOf, abgeschaltetText, hindernisText, panelModell, anleitungOffen, textformNoetig, textformAnsicht, textformBereit, speicherStatus, + anfrageZeilen, anfrageStatus, stripeText, stripeTon, shopHatEintrag, begruendungOk, BEGRUENDUNG_MAX, entscheidungStatus, } from '../../lib/funktionenView' import { CHANNELS, channelLabel } from '../../lib/analyseView' -import { berlinYmd } from '../../lib/format' +import { berlinYmd, fmtDateTime } from '../../lib/format' import '../../styles/admin-areas.css' -// Reiter "Funktionen": News-Pinnwand und Terminplaner je Projekt an/aus wie im frueheren Admin-Panel (Kundenliste, Zeile "Funktionen") +// Reiter "Funktionen": News-Pinnwand, Terminplaner und Webshop je Projekt an/aus wie im frueheren Admin-Panel (Kundenliste, Zeile "Funktionen") const short = (text) => (text.length > 60 ? `${text.slice(0, 60)}…` : text) // Site-Key des Terminplaners (steht im Einbau-Code der Kundenwebsite): kleine Schrift mit fester Breite und Kopierknopf @@ -158,6 +159,75 @@ function TextformPanel({ termine, value, onChange, projectId, today, disabled }) ) } +// Webshop je Projekt: Anfrage des Kunden (annehmen / ablehnen mit Begruendung), Stripe-Stand und Kennzahlen. Die Entscheidung geht ans Portal +// (POST /shop/:customerId/anfrage); danach laden Funktionen und Shop neu (onDecided). Texte sind ausschliesslich React-Textknoten. +function ShopPanel({ customerId, shop, onDecided }) { + const [frage, setFrage] = useState(null) // null | 'annehmen' | 'ablehnen' + const [begruendung, setBegruendung] = useState('') + const [busy, setBusy] = useState(false) + const [status, setStatus] = useState(null) // { ok, text } + const a = shop.anfrage + const [tone, label] = anfrageStatus(a?.status) + const stripe = shop.stripe || { accountId: false, status: null } + const entscheiden = async () => { + setBusy(true) + setStatus(null) + try { + const antwort = await entscheideShopAnfrage(customerId, { projectId: shop.projectId, entscheidung: frage, begruendung }) + setFrage(null) + setBegruendung('') + setStatus(entscheidungStatus(antwort)) + onDecided().catch(() => {}) // Neuladen scheitert hoechstens leise; die Entscheidung ist gespeichert + } catch (err) { + setFrage(null) + setStatus({ ok: false, text: err.message }) + onDecided().catch(() => {}) + } finally { + setBusy(false) + } + } + const ablehnenFehlt = frage === 'ablehnen' && !begruendungOk(begruendung) + return ( +
+
+ Webshop: {shop.projectName} + {a && Anfrage: {label}} + {shop.geoeffnet ? 'Geöffnet' : 'Geschlossen'} + Stripe: {stripeText(stripe.status, !!stripe.accountId)} +
+ {a && ( +
+ {anfrageZeilen(a.anfrage).map((z) => ( +
{z.label}
{z.text}
+ ))} +
Eingegangen
{a.at ? fmtDateTime(a.at) : '–'}
+ {a.entschiedenAt && <>
Entschieden
{fmtDateTime(a.entschiedenAt)}
} + {a.begruendung && <>
Begründung
{a.begruendung}
} +
+ )} +
Artikel: {shop.zahlen?.artikel ?? 0} · Offene Bestellungen: {shop.zahlen?.offeneBestellungen ?? 0}
+ {a?.status === 'offen' && ( + frage ? ( +
+ {frage === 'annehmen' ? 'Anfrage annehmen? Der Kunde bekommt eine Mail.' : 'Anfrage ablehnen? Der Kunde bekommt die Begründung per Mail.'} + {frage === 'ablehnen' && ( +