From b2a0ff5d53f20090b8880d98585700ad79559db4 Mon Sep 17 00:00:00 2001 From: Kenso Grimm Date: Wed, 7 Oct 2026 14:26:18 +0000 Subject: [PATCH] Buchungskalender: Funktionsanfragen und Rechtliches-Karten Co-Authored-By: Claude Opus 5.5 --- scripts/setup-legal.mjs | 3 + .../customer/CustomerFunktionen.jsx | 111 +++++++++++++----- src/lib/funktionenView.js | 46 ++++++-- src/lib/teamApi.js | 10 +- tests/setup-legal.test.mjs | 14 ++- tests/unit/funktionenView.test.mjs | 51 +++++++- 6 files changed, 192 insertions(+), 43 deletions(-) diff --git a/scripts/setup-legal.mjs b/scripts/setup-legal.mjs index 79e1e31..5e8cded 100644 --- a/scripts/setup-legal.mjs +++ b/scripts/setup-legal.mjs @@ -104,6 +104,9 @@ export const DOCUMENTS = [ { key: "avv-webshop", title: "AVV Webshop", liveUrl: "https://project.webklar.com/rechtliches/avv-webshop.html" }, // Oeffnungszeiten live: Schluessel hoechstens 36 Zeichen (WOMS-Grenze fuer Dokument-IDs); die Fassungen (PDF/HTML) laedt der Inhaber hoch { key: "leistungsbeschreibung-zeiten", title: "Leistungsbeschreibung Öffnungszeiten live", liveUrl: "https://project.webklar.com/vertragsunterlagen/zeiten-leistungsbeschreibung.html" }, + // Buchungskalender (Funktion unterkunft): Alias-Seiten ohne Fassungsdatum wie beim Webshop; die Fassungen laedt der Inhaber hoch + { key: "leistungsbeschreibung-unterkunft", title: "Leistungsbeschreibung Buchungskalender", liveUrl: "https://project.webklar.com/rechtliches/leistungsbeschreibung-unterkunft.html" }, + { key: "avv-unterkunft", title: "AVV Buchungskalender", liveUrl: "https://project.webklar.com/rechtliches/avv-unterkunft.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 b5c89a9..4c1dfb8 100644 --- a/src/components/customer/CustomerFunktionen.jsx +++ b/src/components/customer/CustomerFunktionen.jsx @@ -2,7 +2,7 @@ import { Fragment, useCallback, useEffect, useRef, useState } from 'react' import { Link } from 'react-router-dom' import { Card, Badge, Button, EmptyState } from '../ui' import { copyText } from '../CopyableCredential' -import { getFunktionen, setFeature, getTermineAnleitung, getShop, entscheideShopAnfrage } from '../../lib/teamApi' +import { getFunktionen, setFeature, getTermineAnleitung, getShop, entscheideShopAnfrage, getUnterkunft, entscheideUnterkunftAnfrage } from '../../lib/teamApi' import { FUNKTIONEN, ZUSTAND, AKTION_LABEL, funktionOf, aktionen, mergeFunktion, siteKeyOf, abgeschaltetText, hindernisText, panelModell, anleitungOffen, textformNoetig, textformAnsicht, textformBereit, speicherStatus, @@ -160,21 +160,18 @@ function TextformPanel({ funktion, feld, value, onChange, projectId, today, disa ) } -// 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 }) { +// Entscheidung ueber eine Anfrage (Webshop und Buchungskalender): Annehmen / Ablehnen mit Begruendung. Die Entscheidung geht ueber entscheide +// ans Portal; danach laden Funktionen und Bereich neu (onDecided). Texte sind ausschliesslich React-Textknoten. +function EntscheidungBlock({ projectId, anfrage, entscheide, 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 () => { + const run = async () => { setBusy(true) setStatus(null) try { - const antwort = await entscheideShopAnfrage(customerId, { projectId: shop.projectId, entscheidung: frage, begruendung }) + const antwort = await entscheide({ projectId, entscheidung: frage, begruendung }) setFrage(null) setBegruendung('') setStatus(entscheidungStatus(antwort)) @@ -189,32 +186,15 @@ function ShopPanel({ customerId, shop, onDecided }) { } 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' && ( + <> + {anfrage?.status === 'offen' && ( frage ? (
{frage === 'annehmen' ? 'Anfrage annehmen? Der Kunde bekommt eine Mail.' : 'Anfrage ablehnen? Der Kunde bekommt die Begründung per Mail.'} {frage === 'ablehnen' && (