feat: Improve Bottle Split UX
- Add 'Split starten' button to bottle detail page - Support flexible sample sizes (1-20cl) instead of fixed 5/10cl - Preselect bottle when coming from bottle page - Save sample sizes/shipping defaults to localStorage - Update schema: sample_sizes JSONB array - Update server actions and UI for dynamic sizes
This commit is contained in:
@@ -4,7 +4,7 @@ import React, { useState, useEffect } from 'react';
|
|||||||
import { useParams } from 'next/navigation';
|
import { useParams } from 'next/navigation';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { ChevronLeft, Share2, User, Package, Truck, Loader2, CheckCircle2, Clock, AlertCircle } from 'lucide-react';
|
import { ChevronLeft, Share2, User, Package, Truck, Loader2, CheckCircle2, Clock, AlertCircle } from 'lucide-react';
|
||||||
import { getSplitBySlug, requestSlot, SplitDetails, ShippingOption } from '@/services/split-actions';
|
import { getSplitBySlug, requestSlot, SplitDetails, SampleSize, ShippingOption } from '@/services/split-actions';
|
||||||
import SplitProgressBar from '@/components/SplitProgressBar';
|
import SplitProgressBar from '@/components/SplitProgressBar';
|
||||||
import { createClient } from '@/lib/supabase/client';
|
import { createClient } from '@/lib/supabase/client';
|
||||||
|
|
||||||
@@ -15,8 +15,7 @@ export default function SplitPublicPage() {
|
|||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
// Request form
|
const [selectedAmount, setSelectedAmount] = useState<number | null>(null);
|
||||||
const [selectedAmount, setSelectedAmount] = useState<5 | 10>(5);
|
|
||||||
const [selectedShipping, setSelectedShipping] = useState<string>('');
|
const [selectedShipping, setSelectedShipping] = useState<string>('');
|
||||||
const [isRequesting, setIsRequesting] = useState(false);
|
const [isRequesting, setIsRequesting] = useState(false);
|
||||||
const [requestSuccess, setRequestSuccess] = useState(false);
|
const [requestSuccess, setRequestSuccess] = useState(false);
|
||||||
@@ -41,6 +40,9 @@ export default function SplitPublicPage() {
|
|||||||
if (result.data.shippingOptions.length > 0) {
|
if (result.data.shippingOptions.length > 0) {
|
||||||
setSelectedShipping(result.data.shippingOptions[0].name);
|
setSelectedShipping(result.data.shippingOptions[0].name);
|
||||||
}
|
}
|
||||||
|
if (result.data.sampleSizes.length > 0) {
|
||||||
|
setSelectedAmount(result.data.sampleSizes[0].cl);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
setError(result.error || 'Split nicht gefunden');
|
setError(result.error || 'Split nicht gefunden');
|
||||||
}
|
}
|
||||||
@@ -52,7 +54,8 @@ export default function SplitPublicPage() {
|
|||||||
|
|
||||||
const pricePerCl = split.priceBottle / split.totalVolume;
|
const pricePerCl = split.priceBottle / split.totalVolume;
|
||||||
const whisky = pricePerCl * amountCl;
|
const whisky = pricePerCl * amountCl;
|
||||||
const glass = amountCl === 5 ? split.costGlass5cl : split.costGlass10cl;
|
const sizeOption = split.sampleSizes.find(s => s.cl === amountCl);
|
||||||
|
const glass = sizeOption?.glassCost || 0;
|
||||||
const shippingOption = split.shippingOptions.find(s => s.name === selectedShipping);
|
const shippingOption = split.shippingOptions.find(s => s.name === selectedShipping);
|
||||||
const shipping = shippingOption?.price || 0;
|
const shipping = shippingOption?.price || 0;
|
||||||
|
|
||||||
@@ -65,7 +68,7 @@ export default function SplitPublicPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleRequest = async () => {
|
const handleRequest = async () => {
|
||||||
if (!selectedShipping) return;
|
if (!selectedShipping || !selectedAmount) return;
|
||||||
|
|
||||||
setIsRequesting(true);
|
setIsRequesting(true);
|
||||||
setRequestError(null);
|
setRequestError(null);
|
||||||
@@ -74,7 +77,7 @@ export default function SplitPublicPage() {
|
|||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setRequestSuccess(true);
|
setRequestSuccess(true);
|
||||||
fetchSplit(); // Refresh data
|
fetchSplit();
|
||||||
} else {
|
} else {
|
||||||
setRequestError(result.error || 'Anfrage fehlgeschlagen');
|
setRequestError(result.error || 'Anfrage fehlgeschlagen');
|
||||||
}
|
}
|
||||||
@@ -84,7 +87,7 @@ export default function SplitPublicPage() {
|
|||||||
|
|
||||||
const userParticipation = split?.participants.find(p => p.userId === currentUserId);
|
const userParticipation = split?.participants.find(p => p.userId === currentUserId);
|
||||||
const canRequest = !userParticipation && currentUserId && currentUserId !== split?.hostId;
|
const canRequest = !userParticipation && currentUserId && currentUserId !== split?.hostId;
|
||||||
const isWaitlist = split && split.remaining < selectedAmount;
|
const isWaitlist = split && selectedAmount && split.remaining < selectedAmount;
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -104,12 +107,11 @@ export default function SplitPublicPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const price = calculatePrice(selectedAmount);
|
const price = selectedAmount ? calculatePrice(selectedAmount) : { whisky: 0, glass: 0, shipping: 0, total: 0 };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="min-h-screen bg-zinc-950 p-4 md:p-8 lg:p-12">
|
<main className="min-h-screen bg-zinc-950 p-4 md:p-8 lg:p-12">
|
||||||
<div className="max-w-2xl mx-auto space-y-6">
|
<div className="max-w-2xl mx-auto space-y-6">
|
||||||
{/* Back */}
|
|
||||||
<Link
|
<Link
|
||||||
href="/"
|
href="/"
|
||||||
className="inline-flex items-center gap-2 text-zinc-500 hover:text-orange-600 transition-colors font-bold uppercase text-[10px] tracking-[0.2em]"
|
className="inline-flex items-center gap-2 text-zinc-500 hover:text-orange-600 transition-colors font-bold uppercase text-[10px] tracking-[0.2em]"
|
||||||
@@ -120,7 +122,6 @@ export default function SplitPublicPage() {
|
|||||||
|
|
||||||
{/* Hero */}
|
{/* Hero */}
|
||||||
<div className="bg-zinc-900 rounded-3xl overflow-hidden border border-zinc-800 shadow-xl">
|
<div className="bg-zinc-900 rounded-3xl overflow-hidden border border-zinc-800 shadow-xl">
|
||||||
{/* Bottle Image */}
|
|
||||||
{split.bottle.imageUrl && (
|
{split.bottle.imageUrl && (
|
||||||
<div className="h-48 md:h-64 bg-zinc-800 relative">
|
<div className="h-48 md:h-64 bg-zinc-800 relative">
|
||||||
<img
|
<img
|
||||||
@@ -145,7 +146,6 @@ export default function SplitPublicPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stats Row */}
|
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
{split.bottle.abv && (
|
{split.bottle.abv && (
|
||||||
<span className="px-3 py-1.5 bg-zinc-800 rounded-xl text-xs font-bold text-zinc-300">
|
<span className="px-3 py-1.5 bg-zinc-800 rounded-xl text-xs font-bold text-zinc-300">
|
||||||
@@ -162,7 +162,6 @@ export default function SplitPublicPage() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Progress Bar */}
|
|
||||||
<div className="pt-4">
|
<div className="pt-4">
|
||||||
<SplitProgressBar
|
<SplitProgressBar
|
||||||
totalVolume={split.totalVolume}
|
totalVolume={split.totalVolume}
|
||||||
@@ -185,20 +184,17 @@ export default function SplitPublicPage() {
|
|||||||
{/* Amount Selection */}
|
{/* Amount Selection */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<label className="text-xs font-bold text-zinc-500 uppercase tracking-widest">Menge</label>
|
<label className="text-xs font-bold text-zinc-500 uppercase tracking-widest">Menge</label>
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="flex flex-wrap gap-2">
|
||||||
{[5, 10].map(amount => (
|
{split.sampleSizes.map(size => (
|
||||||
<button
|
<button
|
||||||
key={amount}
|
key={size.cl}
|
||||||
onClick={() => setSelectedAmount(amount as 5 | 10)}
|
onClick={() => setSelectedAmount(size.cl)}
|
||||||
className={`p-4 rounded-2xl border-2 transition-all ${selectedAmount === amount
|
className={`px-4 py-3 rounded-xl border-2 transition-all ${selectedAmount === size.cl
|
||||||
? 'border-orange-500 bg-orange-500/10'
|
? 'border-orange-500 bg-orange-500/10'
|
||||||
: 'border-zinc-700 hover:border-zinc-600'
|
: 'border-zinc-700 hover:border-zinc-600'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span className="text-2xl font-black text-white">{amount}cl</span>
|
<span className="text-lg font-black text-white">{size.cl}cl</span>
|
||||||
<p className="text-xs text-zinc-500 mt-1">
|
|
||||||
{amount === 5 ? 'Kleiner Taster' : 'Ordentliche Portion'}
|
|
||||||
</p>
|
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -210,12 +206,12 @@ export default function SplitPublicPage() {
|
|||||||
<Truck size={14} />
|
<Truck size={14} />
|
||||||
Versand
|
Versand
|
||||||
</label>
|
</label>
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="flex flex-wrap gap-2">
|
||||||
{split.shippingOptions.map(option => (
|
{split.shippingOptions.map(option => (
|
||||||
<button
|
<button
|
||||||
key={option.name}
|
key={option.name}
|
||||||
onClick={() => setSelectedShipping(option.name)}
|
onClick={() => setSelectedShipping(option.name)}
|
||||||
className={`p-4 rounded-2xl border-2 transition-all text-left ${selectedShipping === option.name
|
className={`px-4 py-3 rounded-xl border-2 transition-all text-left ${selectedShipping === option.name
|
||||||
? 'border-orange-500 bg-orange-500/10'
|
? 'border-orange-500 bg-orange-500/10'
|
||||||
: 'border-zinc-700 hover:border-zinc-600'
|
: 'border-zinc-700 hover:border-zinc-600'
|
||||||
}`}
|
}`}
|
||||||
@@ -228,36 +224,36 @@ export default function SplitPublicPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Price Breakdown */}
|
{/* Price Breakdown */}
|
||||||
<div className="bg-zinc-950 rounded-2xl p-4 space-y-2">
|
{selectedAmount && (
|
||||||
<div className="flex justify-between text-sm">
|
<div className="bg-zinc-950 rounded-2xl p-4 space-y-2">
|
||||||
<span className="text-zinc-500">Whisky ({selectedAmount}cl)</span>
|
<div className="flex justify-between text-sm">
|
||||||
<span className="text-zinc-300">{price.whisky.toFixed(2)}€</span>
|
<span className="text-zinc-500">Whisky ({selectedAmount}cl)</span>
|
||||||
|
<span className="text-zinc-300">{price.whisky.toFixed(2)}€</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-zinc-500">Sample-Flasche</span>
|
||||||
|
<span className="text-zinc-300">{price.glass.toFixed(2)}€</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-zinc-500">Versand ({selectedShipping})</span>
|
||||||
|
<span className="text-zinc-300">{price.shipping.toFixed(2)}€</span>
|
||||||
|
</div>
|
||||||
|
<div className="border-t border-zinc-800 pt-2 mt-2 flex justify-between">
|
||||||
|
<span className="font-bold text-white">Gesamt</span>
|
||||||
|
<span className="font-black text-xl text-orange-500">{price.total.toFixed(2)}€</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between text-sm">
|
)}
|
||||||
<span className="text-zinc-500">Sample-Flasche</span>
|
|
||||||
<span className="text-zinc-300">{price.glass.toFixed(2)}€</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between text-sm">
|
|
||||||
<span className="text-zinc-500">Versand ({selectedShipping})</span>
|
|
||||||
<span className="text-zinc-300">{price.shipping.toFixed(2)}€</span>
|
|
||||||
</div>
|
|
||||||
<div className="border-t border-zinc-800 pt-2 mt-2 flex justify-between">
|
|
||||||
<span className="font-bold text-white">Gesamt</span>
|
|
||||||
<span className="font-black text-xl text-orange-500">{price.total.toFixed(2)}€</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Error */}
|
|
||||||
{requestError && (
|
{requestError && (
|
||||||
<div className="bg-red-500/10 border border-red-500/30 rounded-xl p-3 text-sm text-red-400">
|
<div className="bg-red-500/10 border border-red-500/30 rounded-xl p-3 text-sm text-red-400">
|
||||||
{requestError}
|
{requestError}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Submit Button */}
|
|
||||||
<button
|
<button
|
||||||
onClick={handleRequest}
|
onClick={handleRequest}
|
||||||
disabled={isRequesting || !selectedShipping}
|
disabled={isRequesting || !selectedShipping || !selectedAmount}
|
||||||
className={`w-full py-4 rounded-2xl font-bold text-white transition-all flex items-center justify-center gap-2 ${isWaitlist
|
className={`w-full py-4 rounded-2xl font-bold text-white transition-all flex items-center justify-center gap-2 ${isWaitlist
|
||||||
? 'bg-yellow-600 hover:bg-yellow-700'
|
? 'bg-yellow-600 hover:bg-yellow-700'
|
||||||
: 'bg-orange-600 hover:bg-orange-700'
|
: 'bg-orange-600 hover:bg-orange-700'
|
||||||
@@ -280,7 +276,6 @@ export default function SplitPublicPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Success Message */}
|
|
||||||
{requestSuccess && (
|
{requestSuccess && (
|
||||||
<div className="bg-green-500/10 border border-green-500/30 rounded-3xl p-6 text-center">
|
<div className="bg-green-500/10 border border-green-500/30 rounded-3xl p-6 text-center">
|
||||||
<CheckCircle2 size={48} className="mx-auto text-green-500 mb-4" />
|
<CheckCircle2 size={48} className="mx-auto text-green-500 mb-4" />
|
||||||
@@ -291,11 +286,10 @@ export default function SplitPublicPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Already Participating */}
|
|
||||||
{userParticipation && (
|
{userParticipation && (
|
||||||
<div className="bg-zinc-900 rounded-3xl p-6 border border-zinc-800">
|
<div className="bg-zinc-900 rounded-3xl p-6 border border-zinc-800">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className={`w-12 h-12 rounded-xl flex items-center justify-center ${userParticipation.status === 'APPROVED' || userParticipation.status === 'PAID' || userParticipation.status === 'SHIPPED'
|
<div className={`w-12 h-12 rounded-xl flex items-center justify-center ${['APPROVED', 'PAID', 'SHIPPED'].includes(userParticipation.status)
|
||||||
? 'bg-green-500/20 text-green-500'
|
? 'bg-green-500/20 text-green-500'
|
||||||
: userParticipation.status === 'PENDING'
|
: userParticipation.status === 'PENDING'
|
||||||
? 'bg-yellow-500/20 text-yellow-500'
|
? 'bg-yellow-500/20 text-yellow-500'
|
||||||
@@ -316,7 +310,6 @@ export default function SplitPublicPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Not logged in */}
|
|
||||||
{!currentUserId && (
|
{!currentUserId && (
|
||||||
<div className="bg-zinc-900 rounded-3xl p-6 border border-zinc-800 text-center">
|
<div className="bg-zinc-900 rounded-3xl p-6 border border-zinc-800 text-center">
|
||||||
<User size={32} className="mx-auto text-zinc-500 mb-3" />
|
<User size={32} className="mx-auto text-zinc-500 mb-3" />
|
||||||
@@ -330,7 +323,6 @@ export default function SplitPublicPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Host View */}
|
|
||||||
{currentUserId === split.hostId && (
|
{currentUserId === split.hostId && (
|
||||||
<div className="bg-zinc-900 rounded-3xl p-6 border border-zinc-800">
|
<div className="bg-zinc-900 rounded-3xl p-6 border border-zinc-800">
|
||||||
<p className="text-sm text-zinc-500 mb-4">Du bist der Host dieses Splits</p>
|
<p className="text-sm text-zinc-500 mb-4">Du bist der Host dieses Splits</p>
|
||||||
@@ -343,7 +335,6 @@ export default function SplitPublicPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Share Button */}
|
|
||||||
<button
|
<button
|
||||||
onClick={() => navigator.share?.({ url: window.location.href })}
|
onClick={() => navigator.share?.({ url: window.location.href })}
|
||||||
className="w-full py-4 bg-zinc-900 hover:bg-zinc-800 border border-zinc-800 rounded-2xl text-zinc-400 font-bold flex items-center justify-center gap-2 transition-colors"
|
className="w-full py-4 bg-zinc-900 hover:bg-zinc-800 border border-zinc-800 rounded-2xl text-zinc-400 font-bold flex items-center justify-center gap-2 transition-colors"
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { ChevronLeft, ChevronRight, Package, Truck, Plus, X, Loader2, Check, Wine } from 'lucide-react';
|
import { ChevronLeft, ChevronRight, Package, Truck, Plus, X, Loader2, Check, Wine } from 'lucide-react';
|
||||||
import { createSplit, ShippingOption } from '@/services/split-actions';
|
import { createSplit, ShippingOption, SampleSize } from '@/services/split-actions';
|
||||||
import { createClient } from '@/lib/supabase/client';
|
import { createClient } from '@/lib/supabase/client';
|
||||||
|
|
||||||
interface Bottle {
|
interface Bottle {
|
||||||
@@ -14,10 +14,19 @@ interface Bottle {
|
|||||||
image_url?: string;
|
image_url?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DEFAULT_SIZES: SampleSize[] = [
|
||||||
|
{ cl: 2, glassCost: 0.50 },
|
||||||
|
{ cl: 5, glassCost: 0.80 },
|
||||||
|
{ cl: 10, glassCost: 1.50 },
|
||||||
|
];
|
||||||
|
|
||||||
export default function CreateSplitPage() {
|
export default function CreateSplitPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
const supabase = createClient();
|
const supabase = createClient();
|
||||||
const [step, setStep] = useState(1);
|
const preselectedBottleId = searchParams?.get('bottle');
|
||||||
|
|
||||||
|
const [step, setStep] = useState(preselectedBottleId ? 2 : 1);
|
||||||
const [bottles, setBottles] = useState<Bottle[]>([]);
|
const [bottles, setBottles] = useState<Bottle[]>([]);
|
||||||
const [isLoadingBottles, setIsLoadingBottles] = useState(true);
|
const [isLoadingBottles, setIsLoadingBottles] = useState(true);
|
||||||
|
|
||||||
@@ -26,8 +35,9 @@ export default function CreateSplitPage() {
|
|||||||
const [totalVolume, setTotalVolume] = useState(70);
|
const [totalVolume, setTotalVolume] = useState(70);
|
||||||
const [hostShare, setHostShare] = useState(10);
|
const [hostShare, setHostShare] = useState(10);
|
||||||
const [priceBottle, setPriceBottle] = useState('');
|
const [priceBottle, setPriceBottle] = useState('');
|
||||||
const [costGlass5cl, setCostGlass5cl] = useState('0.80');
|
const [sampleSizes, setSampleSizes] = useState<SampleSize[]>(DEFAULT_SIZES);
|
||||||
const [costGlass10cl, setCostGlass10cl] = useState('1.50');
|
const [newSizeCl, setNewSizeCl] = useState('');
|
||||||
|
const [newSizeCost, setNewSizeCost] = useState('');
|
||||||
const [shippingOptions, setShippingOptions] = useState<ShippingOption[]>([
|
const [shippingOptions, setShippingOptions] = useState<ShippingOption[]>([
|
||||||
{ name: 'DHL', price: 5.50 },
|
{ name: 'DHL', price: 5.50 },
|
||||||
{ name: 'Hermes', price: 4.90 },
|
{ name: 'Hermes', price: 4.90 },
|
||||||
@@ -43,12 +53,22 @@ export default function CreateSplitPage() {
|
|||||||
loadSavedDefaults();
|
loadSavedDefaults();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (preselectedBottleId && bottles.length > 0) {
|
||||||
|
const bottle = bottles.find(b => b.id === preselectedBottleId);
|
||||||
|
if (bottle) {
|
||||||
|
setSelectedBottle(bottle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [preselectedBottleId, bottles]);
|
||||||
|
|
||||||
const loadSavedDefaults = () => {
|
const loadSavedDefaults = () => {
|
||||||
const saved = localStorage.getItem('split-defaults');
|
const saved = localStorage.getItem('split-defaults');
|
||||||
if (saved) {
|
if (saved) {
|
||||||
const defaults = JSON.parse(saved);
|
const defaults = JSON.parse(saved);
|
||||||
setCostGlass5cl(defaults.costGlass5cl || '0.80');
|
if (defaults.sampleSizes) {
|
||||||
setCostGlass10cl(defaults.costGlass10cl || '1.50');
|
setSampleSizes(defaults.sampleSizes);
|
||||||
|
}
|
||||||
if (defaults.shippingOptions) {
|
if (defaults.shippingOptions) {
|
||||||
setShippingOptions(defaults.shippingOptions);
|
setShippingOptions(defaults.shippingOptions);
|
||||||
}
|
}
|
||||||
@@ -57,8 +77,7 @@ export default function CreateSplitPage() {
|
|||||||
|
|
||||||
const saveDefaults = () => {
|
const saveDefaults = () => {
|
||||||
localStorage.setItem('split-defaults', JSON.stringify({
|
localStorage.setItem('split-defaults', JSON.stringify({
|
||||||
costGlass5cl,
|
sampleSizes,
|
||||||
costGlass10cl,
|
|
||||||
shippingOptions,
|
shippingOptions,
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
@@ -81,6 +100,19 @@ export default function CreateSplitPage() {
|
|||||||
setIsLoadingBottles(false);
|
setIsLoadingBottles(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const addSampleSize = () => {
|
||||||
|
if (!newSizeCl || !newSizeCost) return;
|
||||||
|
const cl = parseInt(newSizeCl);
|
||||||
|
if (sampleSizes.some(s => s.cl === cl)) return;
|
||||||
|
setSampleSizes([...sampleSizes, { cl, glassCost: parseFloat(newSizeCost) }].sort((a, b) => a.cl - b.cl));
|
||||||
|
setNewSizeCl('');
|
||||||
|
setNewSizeCost('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeSampleSize = (cl: number) => {
|
||||||
|
setSampleSizes(sampleSizes.filter(s => s.cl !== cl));
|
||||||
|
};
|
||||||
|
|
||||||
const addShippingOption = () => {
|
const addShippingOption = () => {
|
||||||
if (!newShippingName || !newShippingPrice) return;
|
if (!newShippingName || !newShippingPrice) return;
|
||||||
setShippingOptions([...shippingOptions, {
|
setShippingOptions([...shippingOptions, {
|
||||||
@@ -96,7 +128,7 @@ export default function CreateSplitPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCreate = async () => {
|
const handleCreate = async () => {
|
||||||
if (!selectedBottle || !priceBottle) return;
|
if (!selectedBottle || !priceBottle || sampleSizes.length === 0) return;
|
||||||
|
|
||||||
setIsCreating(true);
|
setIsCreating(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
@@ -107,8 +139,7 @@ export default function CreateSplitPage() {
|
|||||||
totalVolume,
|
totalVolume,
|
||||||
hostShare,
|
hostShare,
|
||||||
priceBottle: parseFloat(priceBottle),
|
priceBottle: parseFloat(priceBottle),
|
||||||
costGlass5cl: parseFloat(costGlass5cl),
|
sampleSizes,
|
||||||
costGlass10cl: parseFloat(costGlass10cl),
|
|
||||||
shippingOptions,
|
shippingOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -121,8 +152,6 @@ export default function CreateSplitPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const pricePerCl = priceBottle ? parseFloat(priceBottle) / totalVolume : 0;
|
const pricePerCl = priceBottle ? parseFloat(priceBottle) / totalVolume : 0;
|
||||||
const price5cl = pricePerCl * 5 + parseFloat(costGlass5cl || '0');
|
|
||||||
const price10cl = pricePerCl * 10 + parseFloat(costGlass10cl || '0');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="min-h-screen bg-zinc-950 p-4 md:p-8 lg:p-12">
|
<main className="min-h-screen bg-zinc-950 p-4 md:p-8 lg:p-12">
|
||||||
@@ -130,7 +159,7 @@ export default function CreateSplitPage() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Link
|
<Link
|
||||||
href="/"
|
href={preselectedBottleId ? `/bottles/${preselectedBottleId}` : '/'}
|
||||||
className="inline-flex items-center gap-2 text-zinc-500 hover:text-orange-600 transition-colors font-bold uppercase text-[10px] tracking-[0.2em]"
|
className="inline-flex items-center gap-2 text-zinc-500 hover:text-orange-600 transition-colors font-bold uppercase text-[10px] tracking-[0.2em]"
|
||||||
>
|
>
|
||||||
<ChevronLeft size={16} />
|
<ChevronLeft size={16} />
|
||||||
@@ -214,11 +243,11 @@ export default function CreateSplitPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Step 2: Pricing */}
|
{/* Step 2: Pricing & Sizes */}
|
||||||
{step === 2 && (
|
{step === 2 && (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-black text-white">Preise festlegen</h1>
|
<h1 className="text-2xl font-black text-white">Preise & Größen</h1>
|
||||||
<p className="text-zinc-500 text-sm">{selectedBottle?.name}</p>
|
<p className="text-zinc-500 text-sm">{selectedBottle?.name}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -256,40 +285,65 @@ export default function CreateSplitPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
{/* Sample Sizes */}
|
||||||
<div>
|
<div className="space-y-3">
|
||||||
<label className="text-xs font-bold text-zinc-500 block mb-2">Flasche 5cl (€)</label>
|
<label className="text-xs font-bold text-zinc-500 uppercase tracking-widest">Verfügbare Größen</label>
|
||||||
<input
|
<div className="flex flex-wrap gap-2">
|
||||||
type="number"
|
{sampleSizes.map(size => (
|
||||||
step="0.01"
|
<div
|
||||||
value={costGlass5cl}
|
key={size.cl}
|
||||||
onChange={e => setCostGlass5cl(e.target.value)}
|
className="flex items-center gap-2 px-3 py-2 bg-zinc-800 rounded-xl"
|
||||||
className="w-full bg-zinc-800 border border-zinc-700 rounded-xl px-4 py-3 text-white"
|
>
|
||||||
/>
|
<span className="font-bold text-white">{size.cl}cl</span>
|
||||||
|
<span className="text-zinc-500 text-xs">+{size.glassCost.toFixed(2)}€</span>
|
||||||
|
<button
|
||||||
|
onClick={() => removeSampleSize(size.cl)}
|
||||||
|
className="text-red-500 hover:text-red-400 ml-1"
|
||||||
|
>
|
||||||
|
<X size={14} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="flex gap-2">
|
||||||
<label className="text-xs font-bold text-zinc-500 block mb-2">Flasche 10cl (€)</label>
|
<input
|
||||||
|
type="number"
|
||||||
|
value={newSizeCl}
|
||||||
|
onChange={e => setNewSizeCl(e.target.value)}
|
||||||
|
placeholder="cl"
|
||||||
|
className="w-20 bg-zinc-800 border border-zinc-700 rounded-xl px-3 py-2 text-white text-sm"
|
||||||
|
/>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
value={costGlass10cl}
|
value={newSizeCost}
|
||||||
onChange={e => setCostGlass10cl(e.target.value)}
|
onChange={e => setNewSizeCost(e.target.value)}
|
||||||
className="w-full bg-zinc-800 border border-zinc-700 rounded-xl px-4 py-3 text-white"
|
placeholder="Glaskosten €"
|
||||||
|
className="flex-1 bg-zinc-800 border border-zinc-700 rounded-xl px-3 py-2 text-white text-sm"
|
||||||
/>
|
/>
|
||||||
|
<button
|
||||||
|
onClick={addSampleSize}
|
||||||
|
disabled={!newSizeCl || !newSizeCost}
|
||||||
|
className="p-2 bg-zinc-700 hover:bg-zinc-600 disabled:opacity-50 rounded-xl transition-colors"
|
||||||
|
>
|
||||||
|
<Plus size={20} className="text-white" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Preview */}
|
{/* Preview */}
|
||||||
{priceBottle && (
|
{priceBottle && sampleSizes.length > 0 && (
|
||||||
<div className="bg-zinc-900 rounded-2xl p-4 border border-zinc-800">
|
<div className="bg-zinc-900 rounded-2xl p-4 border border-zinc-800">
|
||||||
<p className="text-xs font-bold text-zinc-500 mb-2">Vorschau (inkl. Flasche)</p>
|
<p className="text-xs font-bold text-zinc-500 mb-3">Vorschau (inkl. Flasche)</p>
|
||||||
<div className="flex justify-between">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<span className="text-white">5cl Preis:</span>
|
{sampleSizes.map(size => (
|
||||||
<span className="font-bold text-orange-500">{price5cl.toFixed(2)}€</span>
|
<div key={size.cl} className="flex justify-between text-sm">
|
||||||
</div>
|
<span className="text-zinc-400">{size.cl}cl:</span>
|
||||||
<div className="flex justify-between">
|
<span className="font-bold text-orange-500">
|
||||||
<span className="text-white">10cl Preis:</span>
|
{(pricePerCl * size.cl + size.glassCost).toFixed(2)}€
|
||||||
<span className="font-bold text-orange-500">{price10cl.toFixed(2)}€</span>
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -304,7 +358,7 @@ export default function CreateSplitPage() {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setStep(3)}
|
onClick={() => setStep(3)}
|
||||||
disabled={!priceBottle}
|
disabled={!priceBottle || sampleSizes.length === 0}
|
||||||
className="flex-1 py-4 bg-orange-600 hover:bg-orange-700 disabled:bg-zinc-800 disabled:text-zinc-600 text-white rounded-2xl font-bold flex items-center justify-center gap-2 transition-colors"
|
className="flex-1 py-4 bg-orange-600 hover:bg-orange-700 disabled:bg-zinc-800 disabled:text-zinc-600 text-white rounded-2xl font-bold flex items-center justify-center gap-2 transition-colors"
|
||||||
>
|
>
|
||||||
Weiter
|
Weiter
|
||||||
@@ -344,7 +398,6 @@ export default function CreateSplitPage() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Add new */}
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { ChevronLeft, Calendar, Award, Droplets, MapPin, Tag, ExternalLink, Package, Info, Loader2, WifiOff, CircleDollarSign, Wine, CheckCircle2, Circle, ChevronDown, Plus } from 'lucide-react';
|
import { ChevronLeft, Calendar, Award, Droplets, MapPin, Tag, ExternalLink, Package, Info, Loader2, WifiOff, CircleDollarSign, Wine, CheckCircle2, Circle, ChevronDown, Plus, Share2 } from 'lucide-react';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { updateBottle } from '@/services/update-bottle';
|
import { updateBottle } from '@/services/update-bottle';
|
||||||
import { getStorageUrl } from '@/lib/supabase';
|
import { getStorageUrl } from '@/lib/supabase';
|
||||||
@@ -248,6 +248,13 @@ export default function BottleDetails({ bottleId, sessionId, userId }: BottleDet
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<EditBottleForm bottle={bottle as any} />
|
<EditBottleForm bottle={bottle as any} />
|
||||||
|
<Link
|
||||||
|
href={`/splits/create?bottle=${bottle.id}`}
|
||||||
|
className="px-5 py-3 bg-zinc-800 hover:bg-zinc-700 text-white rounded-2xl text-xs font-black uppercase tracking-widest flex items-center gap-2 transition-all border border-zinc-700 hover:border-orange-600/50"
|
||||||
|
>
|
||||||
|
<Share2 size={16} className="text-orange-500" />
|
||||||
|
Split starten
|
||||||
|
</Link>
|
||||||
<DeleteBottleButton bottleId={bottle.id} />
|
<DeleteBottleButton bottleId={bottle.id} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -11,13 +11,17 @@ export interface ShippingOption {
|
|||||||
price: number;
|
price: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SampleSize {
|
||||||
|
cl: number;
|
||||||
|
glassCost: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CreateSplitData {
|
export interface CreateSplitData {
|
||||||
bottleId: string;
|
bottleId: string;
|
||||||
totalVolume?: number;
|
totalVolume?: number;
|
||||||
hostShare?: number;
|
hostShare?: number;
|
||||||
priceBottle: number;
|
priceBottle: number;
|
||||||
costGlass5cl?: number;
|
sampleSizes: SampleSize[];
|
||||||
costGlass10cl?: number;
|
|
||||||
shippingOptions: ShippingOption[];
|
shippingOptions: ShippingOption[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,8 +33,7 @@ export interface SplitDetails {
|
|||||||
totalVolume: number;
|
totalVolume: number;
|
||||||
hostShare: number;
|
hostShare: number;
|
||||||
priceBottle: number;
|
priceBottle: number;
|
||||||
costGlass5cl: number;
|
sampleSizes: SampleSize[];
|
||||||
costGlass10cl: number;
|
|
||||||
shippingOptions: ShippingOption[];
|
shippingOptions: ShippingOption[];
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
@@ -42,7 +45,6 @@ export interface SplitDetails {
|
|||||||
abv?: number;
|
abv?: number;
|
||||||
age?: number;
|
age?: number;
|
||||||
};
|
};
|
||||||
// Calculated availability
|
|
||||||
available: number;
|
available: number;
|
||||||
taken: number;
|
taken: number;
|
||||||
reserved: number;
|
reserved: number;
|
||||||
@@ -101,6 +103,12 @@ export async function createSplit(data: CreateSplitData): Promise<{
|
|||||||
|
|
||||||
const slug = generateSlug();
|
const slug = generateSlug();
|
||||||
|
|
||||||
|
// Convert sample sizes to DB format
|
||||||
|
const sampleSizesDb = data.sampleSizes.map(s => ({
|
||||||
|
cl: s.cl,
|
||||||
|
glass_cost: s.glassCost,
|
||||||
|
}));
|
||||||
|
|
||||||
const { data: split, error } = await supabase
|
const { data: split, error } = await supabase
|
||||||
.from('bottle_splits')
|
.from('bottle_splits')
|
||||||
.insert({
|
.insert({
|
||||||
@@ -109,8 +117,7 @@ export async function createSplit(data: CreateSplitData): Promise<{
|
|||||||
total_volume: data.totalVolume || 70,
|
total_volume: data.totalVolume || 70,
|
||||||
host_share: data.hostShare || 10,
|
host_share: data.hostShare || 10,
|
||||||
price_bottle: data.priceBottle,
|
price_bottle: data.priceBottle,
|
||||||
cost_glass_5cl: data.costGlass5cl || 0.80,
|
sample_sizes: sampleSizesDb,
|
||||||
cost_glass_10cl: data.costGlass10cl || 1.50,
|
|
||||||
shipping_options: data.shippingOptions,
|
shipping_options: data.shippingOptions,
|
||||||
public_slug: slug,
|
public_slug: slug,
|
||||||
})
|
})
|
||||||
@@ -133,7 +140,7 @@ export async function createSplit(data: CreateSplitData): Promise<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get split details by public slug (for public page)
|
* Get split details by public slug
|
||||||
*/
|
*/
|
||||||
export async function getSplitBySlug(slug: string): Promise<{
|
export async function getSplitBySlug(slug: string): Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
@@ -153,8 +160,7 @@ export async function getSplitBySlug(slug: string): Promise<{
|
|||||||
total_volume,
|
total_volume,
|
||||||
host_share,
|
host_share,
|
||||||
price_bottle,
|
price_bottle,
|
||||||
cost_glass_5cl,
|
sample_sizes,
|
||||||
cost_glass_10cl,
|
|
||||||
shipping_options,
|
shipping_options,
|
||||||
is_active,
|
is_active,
|
||||||
created_at,
|
created_at,
|
||||||
@@ -186,7 +192,6 @@ export async function getSplitBySlug(slug: string): Promise<{
|
|||||||
|
|
||||||
// Calculate availability
|
// Calculate availability
|
||||||
const available = split.total_volume - split.host_share;
|
const available = split.total_volume - split.host_share;
|
||||||
|
|
||||||
let taken = 0;
|
let taken = 0;
|
||||||
let reserved = 0;
|
let reserved = 0;
|
||||||
|
|
||||||
@@ -199,9 +204,14 @@ export async function getSplitBySlug(slug: string): Promise<{
|
|||||||
});
|
});
|
||||||
|
|
||||||
const remaining = available - taken - reserved;
|
const remaining = available - taken - reserved;
|
||||||
|
|
||||||
const bottle = split.bottles as any;
|
const bottle = split.bottles as any;
|
||||||
|
|
||||||
|
// Convert sample sizes from DB format
|
||||||
|
const sampleSizes = ((split.sample_sizes as any[]) || []).map(s => ({
|
||||||
|
cl: s.cl,
|
||||||
|
glassCost: s.glass_cost,
|
||||||
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
@@ -212,8 +222,7 @@ export async function getSplitBySlug(slug: string): Promise<{
|
|||||||
totalVolume: split.total_volume,
|
totalVolume: split.total_volume,
|
||||||
hostShare: split.host_share,
|
hostShare: split.host_share,
|
||||||
priceBottle: split.price_bottle,
|
priceBottle: split.price_bottle,
|
||||||
costGlass5cl: split.cost_glass_5cl,
|
sampleSizes,
|
||||||
costGlass10cl: split.cost_glass_10cl,
|
|
||||||
shippingOptions: split.shipping_options as ShippingOption[],
|
shippingOptions: split.shipping_options as ShippingOption[],
|
||||||
isActive: split.is_active,
|
isActive: split.is_active,
|
||||||
createdAt: split.created_at,
|
createdAt: split.created_at,
|
||||||
@@ -252,7 +261,7 @@ export async function getSplitBySlug(slug: string): Promise<{
|
|||||||
*/
|
*/
|
||||||
export async function requestSlot(
|
export async function requestSlot(
|
||||||
splitId: string,
|
splitId: string,
|
||||||
amountCl: 5 | 10,
|
amountCl: number,
|
||||||
shippingMethod: string
|
shippingMethod: string
|
||||||
): Promise<{ success: boolean; error?: string }> {
|
): Promise<{ success: boolean; error?: string }> {
|
||||||
const supabase = await createClient();
|
const supabase = await createClient();
|
||||||
@@ -275,7 +284,6 @@ export async function requestSlot(
|
|||||||
return { success: false, error: 'Split nicht gefunden' };
|
return { success: false, error: 'Split nicht gefunden' };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Can't join your own split
|
|
||||||
if (split.host_id === user.id) {
|
if (split.host_id === user.id) {
|
||||||
return { success: false, error: 'Du kannst nicht an deinem eigenen Split teilnehmen' };
|
return { success: false, error: 'Du kannst nicht an deinem eigenen Split teilnehmen' };
|
||||||
}
|
}
|
||||||
@@ -292,6 +300,13 @@ export async function requestSlot(
|
|||||||
return { success: false, error: 'Du nimmst bereits an diesem Split teil' };
|
return { success: false, error: 'Du nimmst bereits an diesem Split teil' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Find the glass cost for this size
|
||||||
|
const sampleSizes = (split.sample_sizes as any[]) || [];
|
||||||
|
const sizeOption = sampleSizes.find(s => s.cl === amountCl);
|
||||||
|
if (!sizeOption) {
|
||||||
|
return { success: false, error: 'Ungültige Größe' };
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate availability
|
// Calculate availability
|
||||||
const { data: participants } = await supabase
|
const { data: participants } = await supabase
|
||||||
.from('split_participants')
|
.from('split_participants')
|
||||||
@@ -311,16 +326,14 @@ export async function requestSlot(
|
|||||||
|
|
||||||
// Calculate total cost
|
// Calculate total cost
|
||||||
const pricePerCl = split.price_bottle / split.total_volume;
|
const pricePerCl = split.price_bottle / split.total_volume;
|
||||||
const glassCost = amountCl === 5 ? split.cost_glass_5cl : split.cost_glass_10cl;
|
const glassCost = sizeOption.glass_cost;
|
||||||
const shippingOption = (split.shipping_options as ShippingOption[])
|
const shippingOption = (split.shipping_options as ShippingOption[])
|
||||||
.find(s => s.name === shippingMethod);
|
.find(s => s.name === shippingMethod);
|
||||||
const shippingCost = shippingOption?.price || 0;
|
const shippingCost = shippingOption?.price || 0;
|
||||||
const totalCost = (pricePerCl * amountCl) + glassCost + shippingCost;
|
const totalCost = (pricePerCl * amountCl) + glassCost + shippingCost;
|
||||||
|
|
||||||
// Determine status
|
|
||||||
const status = remaining >= amountCl ? 'PENDING' : 'WAITLIST';
|
const status = remaining >= amountCl ? 'PENDING' : 'WAITLIST';
|
||||||
|
|
||||||
// Insert or update (if was rejected)
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
const { error } = await supabase
|
const { error } = await supabase
|
||||||
.from('split_participants')
|
.from('split_participants')
|
||||||
@@ -378,7 +391,6 @@ export async function updateParticipantStatus(
|
|||||||
return { success: false, error: 'Nicht autorisiert' };
|
return { success: false, error: 'Nicht autorisiert' };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify host owns the split
|
|
||||||
const { data: participant, error: findError } = await supabase
|
const { data: participant, error: findError } = await supabase
|
||||||
.from('split_participants')
|
.from('split_participants')
|
||||||
.select(`
|
.select(`
|
||||||
@@ -525,14 +537,19 @@ export async function generateForumExport(splitId: string): Promise<{
|
|||||||
const bottle = split.bottles as any;
|
const bottle = split.bottles as any;
|
||||||
const participants = split.split_participants as any[] || [];
|
const participants = split.split_participants as any[] || [];
|
||||||
const shippingOptions = split.shipping_options as ShippingOption[];
|
const shippingOptions = split.shipping_options as ShippingOption[];
|
||||||
|
const sampleSizes = (split.sample_sizes as any[]) || [];
|
||||||
|
|
||||||
const pricePerCl = split.price_bottle / split.total_volume;
|
const pricePerCl = split.price_bottle / split.total_volume;
|
||||||
const price5cl = (pricePerCl * 5 + split.cost_glass_5cl).toFixed(2);
|
|
||||||
const price10cl = (pricePerCl * 10 + split.cost_glass_10cl).toFixed(2);
|
// Build price list for all sizes
|
||||||
|
const priceList = sampleSizes.map(s => {
|
||||||
|
const price = (pricePerCl * s.cl + s.glass_cost).toFixed(2);
|
||||||
|
return `${s.cl}cl: ${price}€`;
|
||||||
|
}).join(' | ');
|
||||||
|
|
||||||
let text = `[b]${bottle.name}${bottle.distillery ? ` (${bottle.distillery})` : ''} Split[/b]\n\n`;
|
let text = `[b]${bottle.name}${bottle.distillery ? ` (${bottle.distillery})` : ''} Split[/b]\n\n`;
|
||||||
text += `Flaschenpreis: ${split.price_bottle.toFixed(2)}€\n`;
|
text += `Flaschenpreis: ${split.price_bottle.toFixed(2)}€\n`;
|
||||||
text += `5cl: ${price5cl}€ | 10cl: ${price10cl}€\n`;
|
text += `${priceList}\n`;
|
||||||
text += `Versand: ${shippingOptions.map(s => `${s.name} (${s.price.toFixed(2)}€)`).join(', ')}\n\n`;
|
text += `Versand: ${shippingOptions.map(s => `${s.name} (${s.price.toFixed(2)}€)`).join(', ')}\n\n`;
|
||||||
text += `[b]Teilnehmer:[/b]\n`;
|
text += `[b]Teilnehmer:[/b]\n`;
|
||||||
text += `1. Host (${split.host_share}cl)\n`;
|
text += `1. Host (${split.host_share}cl)\n`;
|
||||||
|
|||||||
@@ -456,8 +456,7 @@ CREATE TABLE IF NOT EXISTS bottle_splits (
|
|||||||
total_volume INTEGER DEFAULT 70, -- in cl
|
total_volume INTEGER DEFAULT 70, -- in cl
|
||||||
host_share INTEGER DEFAULT 10, -- what the host keeps, in cl
|
host_share INTEGER DEFAULT 10, -- what the host keeps, in cl
|
||||||
price_bottle DECIMAL(10, 2) NOT NULL,
|
price_bottle DECIMAL(10, 2) NOT NULL,
|
||||||
cost_glass_5cl DECIMAL(10, 2) DEFAULT 0.80,
|
sample_sizes JSONB DEFAULT '[{"cl": 5, "glass_cost": 0.80}, {"cl": 10, "glass_cost": 1.50}]'::jsonb,
|
||||||
cost_glass_10cl DECIMAL(10, 2) DEFAULT 1.50,
|
|
||||||
shipping_options JSONB DEFAULT '[]'::jsonb,
|
shipping_options JSONB DEFAULT '[]'::jsonb,
|
||||||
is_active BOOLEAN DEFAULT true,
|
is_active BOOLEAN DEFAULT true,
|
||||||
public_slug TEXT UNIQUE NOT NULL,
|
public_slug TEXT UNIQUE NOT NULL,
|
||||||
@@ -486,7 +485,7 @@ CREATE TABLE IF NOT EXISTS split_participants (
|
|||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
split_id UUID REFERENCES bottle_splits(id) ON DELETE CASCADE NOT NULL,
|
split_id UUID REFERENCES bottle_splits(id) ON DELETE CASCADE NOT NULL,
|
||||||
user_id UUID REFERENCES profiles(id) ON DELETE CASCADE NOT NULL,
|
user_id UUID REFERENCES profiles(id) ON DELETE CASCADE NOT NULL,
|
||||||
amount_cl INTEGER NOT NULL CHECK (amount_cl IN (5, 10)),
|
amount_cl INTEGER NOT NULL CHECK (amount_cl > 0),
|
||||||
shipping_method TEXT NOT NULL,
|
shipping_method TEXT NOT NULL,
|
||||||
total_cost DECIMAL(10, 2) NOT NULL,
|
total_cost DECIMAL(10, 2) NOT NULL,
|
||||||
status TEXT DEFAULT 'PENDING' CHECK (status IN ('PENDING', 'APPROVED', 'PAID', 'SHIPPED', 'REJECTED', 'WAITLIST')),
|
status TEXT DEFAULT 'PENDING' CHECK (status IN ('PENDING', 'APPROVED', 'PAID', 'SHIPPED', 'REJECTED', 'WAITLIST')),
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user