diff --git a/migration_v2.sql b/migration_v2.sql new file mode 100644 index 0000000..22849e4 --- /dev/null +++ b/migration_v2.sql @@ -0,0 +1,11 @@ +-- Run this in your Supabase SQL Editor if you have an older database version + +-- 1. Add columns if they don't exist +ALTER TABLE bottles ADD COLUMN IF NOT EXISTS status TEXT DEFAULT 'sealed'; +ALTER TABLE bottles ADD COLUMN IF NOT EXISTS purchase_price DECIMAL(10, 2); +ALTER TABLE bottles ADD COLUMN IF NOT EXISTS finished_at TIMESTAMP WITH TIME ZONE; + +-- 2. Add check constraint for status +-- Note: We drop it first in case it exists with different values +ALTER TABLE bottles DROP CONSTRAINT IF EXISTS bottles_status_check; +ALTER TABLE bottles ADD CONSTRAINT bottles_status_check CHECK (status IN ('sealed', 'open', 'sampled', 'empty')); diff --git a/src/components/BottleDetails.tsx b/src/components/BottleDetails.tsx index 6e3e859..9672547 100644 --- a/src/components/BottleDetails.tsx +++ b/src/components/BottleDetails.tsx @@ -2,7 +2,9 @@ import React from 'react'; import Link from 'next/link'; -import { ChevronLeft, Calendar, Award, Droplets, MapPin, Tag, ExternalLink, Package, Info, Loader2, WifiOff } from 'lucide-react'; +import { ChevronLeft, Calendar, Award, Droplets, MapPin, Tag, ExternalLink, Package, Info, Loader2, WifiOff, CircleDollarSign, Wine, CheckCircle2, Circle, ChevronDown, Plus } from 'lucide-react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { updateBottle } from '@/services/update-bottle'; import { getStorageUrl } from '@/lib/supabase'; import TastingNoteForm from '@/components/TastingNoteForm'; import TastingList from '@/components/TastingList'; @@ -18,9 +20,37 @@ interface BottleDetailsProps { } export default function BottleDetails({ bottleId, sessionId, userId }: BottleDetailsProps) { - const { t } = useI18n(); + const { t, locale } = useI18n(); const { bottle, tastings, loading, error, isOffline } = useBottleData(bottleId); + // Quick Collection States + const [price, setPrice] = React.useState(''); + const [status, setStatus] = React.useState('sealed'); + const [isUpdating, setIsUpdating] = React.useState(false); + const [isFormVisible, setIsFormVisible] = React.useState(false); + + React.useEffect(() => { + if (bottle) { + setPrice(bottle.purchase_price?.toString() || ''); + setStatus((bottle as any).status || 'sealed'); + } + }, [bottle]); + + const handleQuickUpdate = async (newPrice?: string, newStatus?: string) => { + if (isOffline) return; + setIsUpdating(true); + try { + await updateBottle(bottleId, { + purchase_price: newPrice !== undefined ? (newPrice ? parseFloat(newPrice) : null) : (price ? parseFloat(price) : null), + status: newStatus !== undefined ? newStatus : status + } as any); + } catch (err) { + console.error('Quick update failed:', err); + } finally { + setIsUpdating(false); + } + }; + if (loading) { return (
@@ -141,6 +171,53 @@ export default function BottleDetails({ bottleId, sessionId, userId }: BottleDet
)} + {/* Quick Collection Card */} +
+
+

+ Sammlungs-Status +

+ {isUpdating && } +
+ +
+ {/* Price */} +
+ +
+ setPrice(e.target.value)} + onBlur={() => handleQuickUpdate(price)} + placeholder="0.00" + className="w-full bg-zinc-950 border border-zinc-800 rounded-xl pl-3 pr-8 py-2 text-sm font-bold text-orange-500 focus:outline-none focus:border-orange-600 transition-all" + /> +
+
+
+ + {/* Status */} +
+ + +
+
+
+ {bottle.batch_info && (
@@ -191,11 +268,39 @@ export default function BottleDetails({ bottleId, sessionId, userId }: BottleDet
{/* Form */} -
-

- Dram bewerten -

- +
+ + + + {isFormVisible && ( + +
+

+ Dram bewerten +

+ setIsFormVisible(false)} + /> +
+
+ )} +
{/* List */} diff --git a/src/components/ScanAndTasteFlow.tsx b/src/components/ScanAndTasteFlow.tsx index 02da7c2..b0382ad 100644 --- a/src/components/ScanAndTasteFlow.tsx +++ b/src/components/ScanAndTasteFlow.tsx @@ -545,6 +545,7 @@ export default function ScanAndTasteFlow({ isOpen, onClose, imageFile, onBottleS activeSessionName={activeSession?.name} activeSessionId={activeSession?.id} isEnriching={isEnriching} + defaultExpanded={true} /> {isAdmin && perfMetrics && (
diff --git a/src/components/TastingEditor.tsx b/src/components/TastingEditor.tsx index bf33b36..e0e4d64 100644 --- a/src/components/TastingEditor.tsx +++ b/src/components/TastingEditor.tsx @@ -10,6 +10,7 @@ import { db } from '@/lib/db'; import { createClient } from '@/lib/supabase/client'; import { useI18n } from '@/i18n/I18nContext'; import { discoverWhiskybaseId } from '@/services/discover-whiskybase'; +import TastingFormBody from './TastingFormBody'; interface TastingEditorProps { bottleMetadata: BottleMetadata; @@ -19,24 +20,22 @@ interface TastingEditorProps { activeSessionName?: string; activeSessionId?: string; isEnriching?: boolean; + defaultExpanded?: boolean; } -export default function TastingEditor({ bottleMetadata, image, onSave, onOpenSessions, activeSessionName, activeSessionId, isEnriching }: TastingEditorProps) { +export default function TastingEditor({ bottleMetadata, image, onSave, onOpenSessions, activeSessionName, activeSessionId, isEnriching, defaultExpanded = false }: TastingEditorProps) { const { t } = useI18n(); const supabase = createClient(); + const [status, setStatus] = React.useState('sealed'); + const [isUpdating, setIsUpdating] = React.useState(false); + const [isFormVisible, setIsFormVisible] = React.useState(false); + const [rating, setRating] = useState(85); const [noseNotes, setNoseNotes] = useState(''); const [palateNotes, setPalateNotes] = useState(''); const [finishNotes, setFinishNotes] = useState(''); const [isSample, setIsSample] = useState(false); - // Sliders for evaluation - const [noseScore, setNoseScore] = useState(50); - const [tasteScore, setTasteScore] = useState(50); - const [finishScore, setFinishScore] = useState(50); - const [complexityScore, setComplexityScore] = useState(75); - const [balanceScore, setBalanceScore] = useState(85); - const [noseTagIds, setNoseTagIds] = useState([]); const [palateTagIds, setPalateTagIds] = useState([]); const [finishTagIds, setFinishTagIds] = useState([]); @@ -63,6 +62,12 @@ export default function TastingEditor({ bottleMetadata, image, onSave, onOpenSes const [whiskybaseError, setWhiskybaseError] = useState(null); const [textureTagIds, setTextureTagIds] = useState([]); const [selectedBuddyIds, setSelectedBuddyIds] = useState([]); + const [bottlePurchasePrice, setBottlePurchasePrice] = useState(bottleMetadata.purchase_price?.toString() || ''); + + // Section collapse states + const [isNoseExpanded, setIsNoseExpanded] = useState(defaultExpanded); + const [isPalateExpanded, setIsPalateExpanded] = useState(defaultExpanded); + const [isFinishExpanded, setIsFinishExpanded] = useState(defaultExpanded); const buddies = useLiveQuery(() => db.cache_buddies.toArray(), [], [] as any[]); const [lastDramInSession, setLastDramInSession] = useState<{ name: string; isSmoky: boolean; timestamp: number } | null>(null); @@ -173,11 +178,6 @@ export default function TastingEditor({ bottleMetadata, image, onSave, onOpenSes buddy_ids: selectedBuddyIds, tag_ids: [...noseTagIds, ...palateTagIds, ...finishTagIds, ...textureTagIds], // Visual data for ResultCard - nose: noseScore, - taste: tasteScore, - finish: finishScore, - complexity: complexityScore, - balance: balanceScore, // Edited bottle metadata bottleMetadata: { ...bottleMetadata, @@ -193,6 +193,8 @@ export default function TastingEditor({ bottleMetadata, image, onSave, onOpenSes distilled_at: bottleDistilledAt || null, bottled_at: bottleBottledAt || null, whiskybaseId: whiskybaseId || null, + purchase_price: bottlePurchasePrice ? parseFloat(bottlePurchasePrice) : null, + status: status, } }); }; @@ -237,6 +239,13 @@ export default function TastingEditor({ bottleMetadata, image, onSave, onOpenSes

{bottleCategory || 'Whisky'} {bottleAbv ? `• ${bottleAbv}%` : ''} {bottleAge ? `• ${bottleAge}y` : ''}

+ {bottlePurchasePrice && ( +
+ + EK: {bottlePurchasePrice}€ + +
+ )}
@@ -412,6 +421,24 @@ export default function TastingEditor({ bottleMetadata, image, onSave, onOpenSes />
+ + {/* Bottle Status */} +
+ + +
+ {/* Whiskybase Discovery */} {isDiscoveringWb && (
@@ -479,211 +506,32 @@ export default function TastingEditor({ bottleMetadata, image, onSave, onOpenSes
- {/* Rating Slider */} -
-
- -
-
- - {rating}/100 -
- setRating(parseInt(e.target.value))} - className="w-full h-2 bg-zinc-800 rounded-full appearance-none cursor-pointer accent-orange-600 transition-all" - /> -
- Swill - Dram - Legendary -
- -
- {['Bottle', 'Sample'].map(type => ( - - ))} -
-
- - {/* Evaluation Sliders Area */} -
- } /> - } /> -
- - {/* Sections */} -
- {/* Nose Section */} -
-
-
- -
-
-

{t('tasting.nose')}

-

Aroma & Bouquet

-
-
- -
- } /> - -
-

Tags

- setNoseTagIds(prev => prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id])} - suggestedTagNames={suggestedTags} - suggestedCustomTagNames={suggestedCustomTags} - isLoading={isEnriching} - /> -
-
-

Eigene Notizen

-