From ccc6a9b4ece4cd6a83a09f1e2335e1719f42309e Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 3 Mar 2026 20:10:33 +0100 Subject: [PATCH] =?UTF-8?q?Pool-Buchung=20auf=20m=C2=B3-Kapazit=C3=A4t=20u?= =?UTF-8?q?mstellen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Neuer 4-Schritt-Wizard: Wasser → Termin → Daten → Fertig - Kapazität basiert auf m³/Tag statt Anzahl Buchungen - Brunnen-Buchungen belasten keine m³-Kapazität - Ortswasserleitung: m³ werden gegen Tageslimit geprüft - BookingCalendar zeigt m³-basierte Verfügbarkeit - Neues Setting max_m3_per_day (default 150) Co-Authored-By: Claude Opus 4.6 --- src/app/api/pool/route.ts | 43 ++-- src/app/api/pool/verfuegbarkeit/route.ts | 31 ++- src/app/pool/page.tsx | 284 +++++++++++++++-------- src/components/BookingCalendar.tsx | 93 +++++--- src/types/index.ts | 1 + 5 files changed, 287 insertions(+), 165 deletions(-) diff --git a/src/app/api/pool/route.ts b/src/app/api/pool/route.ts index 8c37283..82aa0c5 100644 --- a/src/app/api/pool/route.ts +++ b/src/app/api/pool/route.ts @@ -76,27 +76,36 @@ export async function POST(request: NextRequest) { const supabase = createServiceClient(); - // Max pro Tag laden - const { data: setting } = await supabase - .from('settings') - .select('value') - .eq('key', 'max_pools_per_day') - .single(); + // Kapazitätsprüfung nur für Ortswasserleitung + if (wasserquelle === 'ortswasserleitung') { + // Max m³ pro Tag laden + const { data: setting } = await supabase + .from('settings') + .select('value') + .eq('key', 'max_m3_per_day') + .single(); - const maxPerDay = setting ? parseInt(setting.value) : 5; + const maxM3PerDay = setting ? parseInt(setting.value) : 150; - // Aktuelle Buchungen für den Tag zählen - const { count } = await supabase - .from('buchungen') - .select('*', { count: 'exact', head: true }) - .eq('wunschdatum', wunschdatum) - .eq('status', 'aktiv'); + // Summe der m³ aller aktiven Ortswasserleitung-Buchungen des Tages + const { data: dayBookings } = await supabase + .from('buchungen') + .select('wassermenge_m3') + .eq('wunschdatum', wunschdatum) + .eq('status', 'aktiv') + .eq('wasserquelle', 'ortswasserleitung'); - if ((count || 0) >= maxPerDay) { - return NextResponse.json( - { error: 'Dieser Tag ist bereits ausgebucht. Bitte wählen Sie einen anderen Termin.' }, - { status: 409 } + const usedM3 = (dayBookings || []).reduce( + (sum: number, b: { wassermenge_m3: number | null }) => sum + (b.wassermenge_m3 || 0), + 0 ); + + if (usedM3 + (wassermenge_m3 || 0) > maxM3PerDay) { + return NextResponse.json( + { error: `An diesem Tag sind nur noch ${maxM3PerDay - usedM3} m³ verfügbar. Bitte wählen Sie einen anderen Termin.` }, + { status: 409 } + ); + } } // Buchung speichern diff --git a/src/app/api/pool/verfuegbarkeit/route.ts b/src/app/api/pool/verfuegbarkeit/route.ts index be15569..0cc5b93 100644 --- a/src/app/api/pool/verfuegbarkeit/route.ts +++ b/src/app/api/pool/verfuegbarkeit/route.ts @@ -11,37 +11,44 @@ export async function GET(request: NextRequest) { const supabase = createServiceClient(); - // Buchungen pro Tag zählen + // Buchungen mit Wasserquelle und Menge laden const { data: buchungen } = await supabase .from('buchungen') - .select('wunschdatum') + .select('wunschdatum, wasserquelle, wassermenge_m3') .eq('status', 'aktiv') .gte('wunschdatum', saisonStart) .lte('wunschdatum', saisonEnde); - // Gruppieren - const countMap: Record = {}; - buchungen?.forEach((b: { wunschdatum: string }) => { - countMap[b.wunschdatum] = (countMap[b.wunschdatum] || 0) + 1; + // Pro Tag: Anzahl Buchungen + Summe m³ (nur Ortswasserleitung) + const dayMap: Record = {}; + buchungen?.forEach((b: { wunschdatum: string; wasserquelle: string; wassermenge_m3: number | null }) => { + if (!dayMap[b.wunschdatum]) { + dayMap[b.wunschdatum] = { anzahl: 0, summe_m3: 0 }; + } + dayMap[b.wunschdatum].anzahl += 1; + if (b.wasserquelle === 'ortswasserleitung' && b.wassermenge_m3) { + dayMap[b.wunschdatum].summe_m3 += b.wassermenge_m3; + } }); - const verfuegbarkeit = Object.entries(countMap).map(([datum, anzahl_buchungen]) => ({ + const verfuegbarkeit = Object.entries(dayMap).map(([datum, info]) => ({ datum, - anzahl_buchungen, + anzahl_buchungen: info.anzahl, + summe_m3: info.summe_m3, })); - // Max pro Tag laden + // Max m³ pro Tag laden const { data: setting } = await supabase .from('settings') .select('value') - .eq('key', 'max_pools_per_day') + .eq('key', 'max_m3_per_day') .single(); - const maxPerDay = setting ? parseInt(setting.value) : 5; + const maxM3PerDay = setting ? parseInt(setting.value) : 150; return NextResponse.json({ verfuegbarkeit, - max_per_day: maxPerDay, + max_m3_per_day: maxM3PerDay, }); } catch (err) { console.error('Verfügbarkeit Error:', err); diff --git a/src/app/pool/page.tsx b/src/app/pool/page.tsx index 6328d1d..c9beff0 100644 --- a/src/app/pool/page.tsx +++ b/src/app/pool/page.tsx @@ -24,18 +24,18 @@ declare global { } } -type WizardStep = 'termin' | 'daten' | 'fertig'; +type WizardStep = 'wasser' | 'termin' | 'daten' | 'fertig'; export default function PoolBuchungPage() { - const [step, setStep] = useState('termin'); + const [step, setStep] = useState('wasser'); const [selectedDate, setSelectedDate] = useState(null); + const [wasserquelle, setWasserquelle] = useState<'' | 'brunnen' | 'ortswasserleitung'>(''); + const [wassermenge, setWassermenge] = useState(''); const [formData, setFormData] = useState({ name: '', strasse: '', telefon: '', email: '', - wasserquelle: '' as '' | 'brunnen' | 'ortswasserleitung', - wassermenge_m3: '', }); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(''); @@ -59,16 +59,16 @@ export default function PoolBuchungPage() { } }, []); - // Render Turnstile when Step 2 becomes visible and script is loaded + // Render Turnstile when Step 3 (daten) becomes visible and script is loaded useEffect(() => { if (step === 'daten' && turnstileReady) { - // Small delay to ensure the container ref is mounted const timer = setTimeout(renderTurnstile, 100); return () => clearTimeout(timer); } }, [step, turnstileReady, renderTurnstile]); const steps = [ + { label: 'Wasser', done: step === 'termin' || step === 'daten' || step === 'fertig', active: step === 'wasser' }, { label: 'Termin', done: step === 'daten' || step === 'fertig', active: step === 'termin' }, { label: 'Daten', done: step === 'fertig', active: step === 'daten' }, { label: 'Fertig', done: false, active: step === 'fertig' }, @@ -81,7 +81,6 @@ export default function PoolBuchungPage() { setFieldErrors((prev) => ({ ...prev, [name]: '' })); }; - // Inline validation on blur const handleBlur = (e: React.FocusEvent) => { const { name, value } = e.target; const errs: Record = {}; @@ -91,13 +90,26 @@ export default function PoolBuchungPage() { if (!value.trim()) errs.email = 'E-Mail ist erforderlich'; else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) errs.email = 'Ungültige E-Mail-Adresse'; } - if (name === 'wassermenge_m3' && formData.wasserquelle === 'ortswasserleitung' && !value) { - errs.wassermenge_m3 = 'Wassermenge ist erforderlich'; - } setFieldErrors((prev) => ({ ...prev, ...errs })); }; - const goToStep2 = () => { + // Step 1 → Step 2 + const goToTermin = () => { + if (!wasserquelle) { + setError('Bitte wählen Sie die Wasserquelle.'); + return; + } + if (wasserquelle === 'ortswasserleitung' && !wassermenge) { + setError('Bitte geben Sie die Wassermenge an.'); + return; + } + setError(''); + setStep('termin'); + window.scrollTo({ top: 0, behavior: 'smooth' }); + }; + + // Step 2 → Step 3 + const goToDaten = () => { if (!selectedDate) { setError('Bitte wählen Sie einen Wunschtermin.'); return; @@ -115,14 +127,6 @@ export default function PoolBuchungPage() { setError('Bitte füllen Sie alle Pflichtfelder aus.'); return; } - if (!formData.wasserquelle) { - setError('Bitte wählen Sie die Wasserquelle.'); - return; - } - if (formData.wasserquelle === 'ortswasserleitung' && !formData.wassermenge_m3) { - setError('Bitte geben Sie die Wassermenge an.'); - return; - } if (!captchaToken) { setError('Bitte bestätigen Sie das CAPTCHA.'); return; @@ -135,7 +139,8 @@ export default function PoolBuchungPage() { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...formData, - wassermenge_m3: formData.wassermenge_m3 ? parseFloat(formData.wassermenge_m3) : null, + wasserquelle, + wassermenge_m3: wassermenge ? parseFloat(wassermenge) : null, wunschdatum: selectedDate, captchaToken, }), @@ -157,11 +162,13 @@ export default function PoolBuchungPage() { }; const resetForm = () => { - setFormData({ name: '', strasse: '', telefon: '', email: '', wasserquelle: '', wassermenge_m3: '' }); + setFormData({ name: '', strasse: '', telefon: '', email: '' }); + setWasserquelle(''); + setWassermenge(''); setSelectedDate(null); setCaptchaToken(''); setFieldErrors({}); - setStep('termin'); + setStep('wasser'); setShowConfirmation(false); turnstileWidgetId.current = null; }; @@ -172,6 +179,8 @@ export default function PoolBuchungPage() { }) : ''; + const wasserquelleLabel = wasserquelle === 'brunnen' ? 'Eigener Brunnen' : 'Ortswasserleitung'; + return (