'use client' import { useState, useEffect } from 'react' import { useRouter } from 'next/navigation' interface Drop { id: number item: string description?: string | null size: number fill: number unit: string ppu: number price_chf?: number | null price_eur?: number | null wholesale_price_chf?: number | null wholesale_price_eur?: number | null image_url: string | null images?: string[] created_at: string start_time: string | null } interface Sale { id: number drop_id: number buyer_id: number size: number payment_id: string | null created_at: string buyer_username?: string buyer_email?: string buyer_fullname?: string buyer_address?: string buyer_phone?: string } export default function DropsManagementPage() { const router = useRouter() const [drops, setDrops] = useState([]) const [loading, setLoading] = useState(true) const [authenticated, setAuthenticated] = useState(false) const [editingDrop, setEditingDrop] = useState(null) const [creatingDrop, setCreatingDrop] = useState(false) const [showSalesPopup, setShowSalesPopup] = useState(false) const [salesForDrop, setSalesForDrop] = useState([]) const [selectedDropId, setSelectedDropId] = useState(null) const [formData, setFormData] = useState({ item: '', description: '', size: '', unit: 'g', ppu: '', priceChf: '', priceEur: '', wholesalePriceChf: '', wholesalePriceEur: '', imageUrl: '', startTime: '', }) const [imageFiles, setImageFiles] = useState([]) const [imagePreviews, setImagePreviews] = useState([]) const [existingImages, setExistingImages] = useState([]) const [uploadingImage, setUploadingImage] = useState(false) useEffect(() => { // Check authentication fetch('/api/admin/check') .then((res) => res.json()) .then((data) => { if (data.authenticated) { setAuthenticated(true) fetchDrops() } else { router.push('/admin/login') } }) .catch(() => { router.push('/admin/login') }) }, [router]) const fetchDrops = async () => { try { const response = await fetch('/api/drops') if (response.ok) { const data = await response.json() setDrops(Array.isArray(data) ? data : []) } } catch (error) { console.error('Error fetching drops:', error) } finally { setLoading(false) } } const handleEdit = async (drop: Drop) => { setEditingDrop(drop) setFormData({ item: drop.item, description: drop.description || '', size: drop.size.toString(), unit: drop.unit, ppu: drop.ppu.toString(), priceChf: drop.price_chf?.toString() || '', priceEur: drop.price_eur?.toString() || '', wholesalePriceChf: drop.wholesale_price_chf?.toString() || '', wholesalePriceEur: drop.wholesale_price_eur?.toString() || '', imageUrl: drop.image_url || '', startTime: drop.start_time ? new Date(drop.start_time).toISOString().slice(0, 16) : '', }) // Fetch existing images for this drop try { const response = await fetch(`/api/drops/images?drop_id=${drop.id}`) if (response.ok) { const data = await response.json() const imageUrls = data.map((img: any) => img.image_url) setExistingImages(imageUrls) } else { setExistingImages(drop.image_url ? [drop.image_url] : []) } } catch (error) { console.error('Error fetching drop images:', error) setExistingImages(drop.image_url ? [drop.image_url] : []) } // Clear file selections setImageFiles([]) setImagePreviews([]) } const handleSave = async () => { if (!editingDrop) return try { setUploadingImage(true) const imageUrls: string[] = [...existingImages] // Upload new image files for (const file of imageFiles) { const uploadFormData = new FormData() uploadFormData.append('file', file) const uploadResponse = await fetch('/api/upload', { method: 'POST', body: uploadFormData, }) if (uploadResponse.ok) { const uploadData = await uploadResponse.json() imageUrls.push(uploadData.url) } else { const error = await uploadResponse.json() alert(`Image upload failed: ${error.error}`) setUploadingImage(false) return } } // Add URL if provided if (formData.imageUrl) { imageUrls.push(formData.imageUrl) } // Limit to 4 images total const finalImageUrls = imageUrls.slice(0, 4) // Update drop basic info const response = await fetch(`/api/drops/${editingDrop.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ item: formData.item, description: formData.description || null, size: parseInt(formData.size), unit: formData.unit, ppu: parseInt(formData.ppu), priceChf: formData.priceChf ? parseFloat(formData.priceChf) : null, priceEur: formData.priceEur ? parseFloat(formData.priceEur) : null, wholesalePriceChf: formData.wholesalePriceChf ? parseFloat(formData.wholesalePriceChf) : null, wholesalePriceEur: formData.wholesalePriceEur ? parseFloat(formData.wholesalePriceEur) : null, imageUrl: finalImageUrls[0] || null, // Keep first image for legacy support startTime: formData.startTime || null, }), }) if (!response.ok) { const error = await response.json() alert(`Error: ${error.error}`) setUploadingImage(false) return } // Save images (always save, even if empty array to clear all images) const imagesResponse = await fetch('/api/drops/images', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ drop_id: editingDrop.id, image_urls: finalImageUrls, }), }) if (!imagesResponse.ok) { const error = await imagesResponse.json() alert(`Error saving images: ${error.error}`) setUploadingImage(false) return } alert('Drop updated successfully') setEditingDrop(null) setImageFiles([]) setImagePreviews([]) setExistingImages([]) // Clean up preview URLs imagePreviews.forEach(url => URL.revokeObjectURL(url)) fetchDrops() } catch (error) { console.error('Error updating drop:', error) alert('Failed to update drop') } finally { setUploadingImage(false) } } const handleDelete = async (id: number) => { if (!confirm('Are you sure you want to delete this drop? This will also delete all related sales.')) { return } try { const response = await fetch(`/api/drops/${id}`, { method: 'DELETE', }) if (response.ok) { alert('Drop deleted successfully') fetchDrops() } else { const error = await response.json() alert(`Error: ${error.error}`) } } catch (error) { console.error('Error deleting drop:', error) alert('Failed to delete drop') } } const handleImageChange = (e: React.ChangeEvent) => { const files = Array.from(e.target.files || []) if (files.length > 0) { // Calculate how many more images we can add (max 4 total) const currentTotal = existingImages.length + imagePreviews.length const remainingSlots = Math.max(0, 4 - currentTotal) if (remainingSlots === 0) { alert('Maximum of 4 images allowed. Please remove some images first.') // Clear the file input e.target.value = '' return } // Limit to remaining slots const limitedFiles = files.slice(0, remainingSlots) setImageFiles([...imageFiles, ...limitedFiles]) // Create previews for new files const newPreviews = limitedFiles.map(file => URL.createObjectURL(file)) setImagePreviews([...imagePreviews, ...newPreviews]) // Clear the file input e.target.value = '' } } const removeImage = (index: number) => { const newFiles = imageFiles.filter((_, i) => i !== index) const newPreviews = imagePreviews.filter((_, i) => i !== index) setImageFiles(newFiles) setImagePreviews(newPreviews) // Revoke the URL to free memory URL.revokeObjectURL(imagePreviews[index]) } const removeExistingImage = (index: number) => { const newImages = existingImages.filter((_, i) => i !== index) setExistingImages(newImages) } const handleCreate = async (e: React.FormEvent) => { e.preventDefault() try { setUploadingImage(true) const imageUrls: string[] = [] // Add URL if provided if (formData.imageUrl) { imageUrls.push(formData.imageUrl) } // Upload image files for (const file of imageFiles) { const uploadFormData = new FormData() uploadFormData.append('file', file) const uploadResponse = await fetch('/api/upload', { method: 'POST', body: uploadFormData, }) if (!uploadResponse.ok) { const error = await uploadResponse.json() alert(`Image upload failed: ${error.error}`) setUploadingImage(false) return } const uploadData = await uploadResponse.json() imageUrls.push(uploadData.url) } // Limit to 4 images const finalImageUrls = imageUrls.slice(0, 4) // Create drop const response = await fetch('/api/drops', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ item: formData.item, description: formData.description || null, size: parseInt(formData.size), unit: formData.unit, ppu: parseInt(formData.ppu), priceChf: formData.priceChf ? parseFloat(formData.priceChf) : null, priceEur: formData.priceEur ? parseFloat(formData.priceEur) : null, wholesalePriceChf: formData.wholesalePriceChf ? parseFloat(formData.wholesalePriceChf) : null, wholesalePriceEur: formData.wholesalePriceEur ? parseFloat(formData.wholesalePriceEur) : null, imageUrl: finalImageUrls[0] || null, // Keep first image for legacy support startTime: formData.startTime || null, }), }) if (!response.ok) { const error = await response.json() alert(`Error: ${error.error}`) setUploadingImage(false) return } const dropData = await response.json() const dropId = dropData.id // Save multiple images if we have more than one if (finalImageUrls.length > 0) { const imagesResponse = await fetch('/api/drops/images', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ drop_id: dropId, image_urls: finalImageUrls, }), }) if (!imagesResponse.ok) { const error = await imagesResponse.json() alert(`Drop created but failed to save images: ${error.error}`) } } alert('Drop created successfully') setFormData({ item: '', description: '', size: '', unit: 'g', ppu: '', priceChf: '', priceEur: '', wholesalePriceChf: '', wholesalePriceEur: '', imageUrl: '', startTime: '', }) setImageFiles([]) setImagePreviews([]) // Clean up preview URLs imagePreviews.forEach(url => URL.revokeObjectURL(url)) // Clear file input const fileInput = document.getElementById('imageFiles') as HTMLInputElement if (fileInput) fileInput.value = '' setCreatingDrop(false) fetchDrops() } catch (error) { console.error('Error creating drop:', error) alert('Failed to create drop') } finally { setUploadingImage(false) } } const handleViewSales = async (dropId: number) => { try { const response = await fetch(`/api/sales/drop/${dropId}`) if (response.ok) { const data = await response.json() setSalesForDrop(Array.isArray(data) ? data : []) setSelectedDropId(dropId) setShowSalesPopup(true) } else { alert('Failed to fetch sales') } } catch (error) { console.error('Error fetching sales:', error) alert('Failed to fetch sales') } } const getProgressPercentage = (fill: number, size: number) => { return Math.min((fill / size) * 100, 100) } if (loading) { return (

Loading...

) } if (!authenticated) { return null } return (

Drops Management

{creatingDrop && (

Create New Drop

setFormData({ ...formData, item: e.target.value })} required style={{ width: '100%', padding: '8px', borderRadius: '8px', border: '1px solid var(--border)', background: 'var(--bg-soft)', color: 'var(--text)' }} />