Files
cbd420/app/admin/drops/page.tsx
2025-12-21 11:39:41 +01:00

847 lines
31 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
interface Drop {
id: number
item: string
size: number
fill: number
unit: string
ppu: number
image_url: string | null
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<Drop[]>([])
const [loading, setLoading] = useState(true)
const [authenticated, setAuthenticated] = useState(false)
const [editingDrop, setEditingDrop] = useState<Drop | null>(null)
const [creatingDrop, setCreatingDrop] = useState(false)
const [showSalesPopup, setShowSalesPopup] = useState(false)
const [salesForDrop, setSalesForDrop] = useState<Sale[]>([])
const [selectedDropId, setSelectedDropId] = useState<number | null>(null)
const [formData, setFormData] = useState({
item: '',
size: '',
unit: 'g',
ppu: '',
imageUrl: '',
startTime: '',
})
const [imageFile, setImageFile] = useState<File | null>(null)
const [imagePreview, setImagePreview] = useState<string>('')
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 = (drop: Drop) => {
setEditingDrop(drop)
setFormData({
item: drop.item,
size: drop.size.toString(),
unit: drop.unit,
ppu: drop.ppu.toString(),
imageUrl: drop.image_url || '',
startTime: drop.start_time ? new Date(drop.start_time).toISOString().slice(0, 16) : '',
})
}
const handleSave = async () => {
if (!editingDrop) return
try {
const response = await fetch(`/api/drops/${editingDrop.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
item: formData.item,
size: parseInt(formData.size),
unit: formData.unit,
ppu: parseInt(formData.ppu),
imageUrl: formData.imageUrl || null,
startTime: formData.startTime || null,
}),
})
if (response.ok) {
alert('Drop updated successfully')
setEditingDrop(null)
fetchDrops()
} else {
const error = await response.json()
alert(`Error: ${error.error}`)
}
} catch (error) {
console.error('Error updating drop:', error)
alert('Failed to update drop')
}
}
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<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (file) {
setImageFile(file)
setImagePreview(URL.createObjectURL(file))
// Clear the imageUrl field when a file is selected
setFormData({ ...formData, imageUrl: '' })
}
}
const handleCreate = async (e: React.FormEvent) => {
e.preventDefault()
try {
let imageUrl = formData.imageUrl || null
// Upload image file if provided
if (imageFile) {
setUploadingImage(true)
const uploadFormData = new FormData()
uploadFormData.append('file', imageFile)
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()
imageUrl = uploadData.url
setUploadingImage(false)
}
const response = await fetch('/api/drops', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
item: formData.item,
size: parseInt(formData.size),
unit: formData.unit,
ppu: parseInt(formData.ppu),
imageUrl: imageUrl,
startTime: formData.startTime || null,
}),
})
if (response.ok) {
alert('Drop created successfully')
setFormData({
item: '',
size: '',
unit: 'g',
ppu: '',
imageUrl: '',
startTime: '',
})
setImageFile(null)
setImagePreview('')
// Clear file input
const fileInput = document.getElementById('imageFile') as HTMLInputElement
if (fileInput) fileInput.value = ''
setCreatingDrop(false)
fetchDrops()
} else {
const error = await response.json()
alert(`Error: ${error.error}`)
}
} 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 (
<div style={{
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: 'var(--bg)'
}}>
<p style={{ color: 'var(--muted)' }}>Loading...</p>
</div>
)
}
if (!authenticated) {
return null
}
return (
<div style={{
minHeight: '100vh',
background: 'var(--bg)',
padding: '40px 20px'
}}>
<div className="container" style={{ maxWidth: '1200px', margin: '0 auto' }}>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '30px'
}}>
<h1>Drops Management</h1>
<div style={{ display: 'flex', gap: '12px' }}>
<button
onClick={() => {
setCreatingDrop(!creatingDrop)
if (creatingDrop) {
// Reset form when canceling
setFormData({
item: '',
size: '',
unit: 'g',
ppu: '',
imageUrl: '',
startTime: '',
})
setImageFile(null)
setImagePreview('')
const fileInput = document.getElementById('imageFile') as HTMLInputElement
if (fileInput) fileInput.value = ''
}
}}
className="cta"
style={{ padding: '10px 20px', fontSize: '14px' }}
>
{creatingDrop ? 'Cancel' : 'Create New Drop'}
</button>
<button
onClick={() => router.push('/admin')}
style={{
padding: '10px 20px',
background: 'transparent',
border: '1px solid var(--border)',
borderRadius: '8px',
color: 'var(--text)',
cursor: 'pointer',
fontSize: '14px'
}}
>
Back to Dashboard
</button>
</div>
</div>
{creatingDrop && (
<div className="drop-card" style={{ marginBottom: '30px', padding: '20px' }}>
<h2 style={{ marginTop: 0, marginBottom: '20px' }}>Create New Drop</h2>
<form onSubmit={handleCreate}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px', marginBottom: '16px' }}>
<div>
<label style={{ display: 'block', marginBottom: '8px' }}>Item Name *</label>
<input
type="text"
value={formData.item}
onChange={(e) => 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)'
}}
/>
</div>
<div>
<label style={{ display: 'block', marginBottom: '8px' }}>Size *</label>
<input
type="number"
value={formData.size}
onChange={(e) => setFormData({ ...formData, size: e.target.value })}
required
min="1"
style={{
width: '100%',
padding: '8px',
borderRadius: '8px',
border: '1px solid var(--border)',
background: 'var(--bg-soft)',
color: 'var(--text)'
}}
/>
</div>
<div>
<label style={{ display: 'block', marginBottom: '8px' }}>Unit *</label>
<input
type="text"
value={formData.unit}
onChange={(e) => setFormData({ ...formData, unit: e.target.value })}
required
maxLength={12}
placeholder="g, kg, ml, etc."
style={{
width: '100%',
padding: '8px',
borderRadius: '8px',
border: '1px solid var(--border)',
background: 'var(--bg-soft)',
color: 'var(--text)'
}}
/>
</div>
<div>
<label style={{ display: 'block', marginBottom: '8px' }}>Price Per Unit (cents) *</label>
<input
type="number"
value={formData.ppu}
onChange={(e) => setFormData({ ...formData, ppu: e.target.value })}
required
min="1"
placeholder="2500 = 2.50 CHF"
style={{
width: '100%',
padding: '8px',
borderRadius: '8px',
border: '1px solid var(--border)',
background: 'var(--bg-soft)',
color: 'var(--text)'
}}
/>
</div>
<div style={{ gridColumn: '1 / -1' }}>
<label style={{ display: 'block', marginBottom: '8px' }}>Product Image</label>
<input
type="file"
id="imageFile"
accept="image/jpeg,image/jpg,image/png,image/webp"
onChange={handleImageChange}
style={{
width: '100%',
padding: '8px',
borderRadius: '8px',
border: '1px solid var(--border)',
background: 'var(--bg-soft)',
color: 'var(--text)',
cursor: 'pointer'
}}
/>
{imagePreview && (
<div style={{ marginTop: '12px' }}>
<img
src={imagePreview}
alt="Preview"
style={{
maxWidth: '200px',
maxHeight: '200px',
borderRadius: '8px',
objectFit: 'cover'
}}
/>
</div>
)}
<p style={{ marginTop: '8px', fontSize: '12px', color: 'var(--muted)' }}>
Max file size: 5MB. Allowed formats: JPEG, PNG, WebP
</p>
<p style={{ marginTop: '4px', fontSize: '12px', color: 'var(--muted)' }}>
Or enter an image URL:
</p>
<input
type="text"
value={formData.imageUrl}
onChange={(e) => {
setFormData({ ...formData, imageUrl: e.target.value })
// Clear file selection when URL is entered
if (e.target.value) {
setImageFile(null)
setImagePreview('')
const fileInput = document.getElementById('imageFile') as HTMLInputElement
if (fileInput) fileInput.value = ''
}
}}
placeholder="https://example.com/image.jpg"
style={{
width: '100%',
padding: '8px',
borderRadius: '8px',
border: '1px solid var(--border)',
background: 'var(--bg-soft)',
color: 'var(--text)',
marginTop: '8px'
}}
/>
</div>
<div>
<label style={{ display: 'block', marginBottom: '8px' }}>Start Time</label>
<input
type="datetime-local"
value={formData.startTime}
onChange={(e) => setFormData({ ...formData, startTime: e.target.value })}
style={{
width: '100%',
padding: '8px',
borderRadius: '8px',
border: '1px solid var(--border)',
background: 'var(--bg-soft)',
color: 'var(--text)'
}}
/>
</div>
</div>
<button
type="submit"
className="cta"
disabled={uploadingImage}
style={{ padding: '10px 20px', fontSize: '14px' }}
>
{uploadingImage ? 'Uploading image...' : 'Create Drop'}
</button>
</form>
</div>
)}
{drops.length === 0 ? (
<p style={{ color: 'var(--muted)' }}>No drops found</p>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
{drops.map((drop) => (
<div
key={drop.id}
className="drop-card"
style={{
background: editingDrop?.id === drop.id ? 'var(--bg-soft)' : 'var(--card)',
padding: '20px'
}}
>
{editingDrop?.id === drop.id ? (
<div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px', marginBottom: '16px' }}>
<div>
<label style={{ display: 'block', marginBottom: '8px' }}>Item Name</label>
<input
type="text"
value={formData.item}
onChange={(e) => setFormData({ ...formData, item: e.target.value })}
style={{
width: '100%',
padding: '8px',
borderRadius: '8px',
border: '1px solid var(--border)',
background: 'var(--bg-soft)',
color: 'var(--text)'
}}
/>
</div>
<div>
<label style={{ display: 'block', marginBottom: '8px' }}>Size</label>
<input
type="number"
value={formData.size}
onChange={(e) => setFormData({ ...formData, size: e.target.value })}
style={{
width: '100%',
padding: '8px',
borderRadius: '8px',
border: '1px solid var(--border)',
background: 'var(--bg-soft)',
color: 'var(--text)'
}}
/>
</div>
<div>
<label style={{ display: 'block', marginBottom: '8px' }}>Unit</label>
<input
type="text"
value={formData.unit}
onChange={(e) => setFormData({ ...formData, unit: e.target.value })}
maxLength={12}
style={{
width: '100%',
padding: '8px',
borderRadius: '8px',
border: '1px solid var(--border)',
background: 'var(--bg-soft)',
color: 'var(--text)'
}}
/>
</div>
<div>
<label style={{ display: 'block', marginBottom: '8px' }}>Price Per Unit (cents)</label>
<input
type="number"
value={formData.ppu}
onChange={(e) => setFormData({ ...formData, ppu: e.target.value })}
style={{
width: '100%',
padding: '8px',
borderRadius: '8px',
border: '1px solid var(--border)',
background: 'var(--bg-soft)',
color: 'var(--text)'
}}
/>
</div>
<div>
<label style={{ display: 'block', marginBottom: '8px' }}>Image URL</label>
<input
type="text"
value={formData.imageUrl}
onChange={(e) => setFormData({ ...formData, imageUrl: e.target.value })}
style={{
width: '100%',
padding: '8px',
borderRadius: '8px',
border: '1px solid var(--border)',
background: 'var(--bg-soft)',
color: 'var(--text)'
}}
/>
</div>
<div>
<label style={{ display: 'block', marginBottom: '8px' }}>Start Time</label>
<input
type="datetime-local"
value={formData.startTime}
onChange={(e) => setFormData({ ...formData, startTime: e.target.value })}
style={{
width: '100%',
padding: '8px',
borderRadius: '8px',
border: '1px solid var(--border)',
background: 'var(--bg-soft)',
color: 'var(--text)'
}}
/>
</div>
</div>
<div style={{ display: 'flex', gap: '8px' }}>
<button
onClick={handleSave}
className="cta"
style={{ padding: '8px 16px', fontSize: '14px' }}
>
Save
</button>
<button
onClick={() => setEditingDrop(null)}
style={{
padding: '8px 16px',
fontSize: '14px',
background: 'transparent',
border: '1px solid var(--border)',
borderRadius: '8px',
color: 'var(--text)',
cursor: 'pointer'
}}
>
Cancel
</button>
</div>
</div>
) : (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '16px' }}>
<div style={{ flex: 1 }}>
<h3 style={{ marginBottom: '8px' }}>{drop.item}</h3>
<p style={{ color: 'var(--muted)', fontSize: '14px', marginBottom: '4px' }}>
ID: {drop.id} · Size: {drop.size}{drop.unit} · Price: {(drop.ppu / 1000).toFixed(2)} CHF / {drop.unit}
</p>
<p style={{ color: 'var(--muted)', fontSize: '12px', marginBottom: '12px' }}>
Created: {new Date(drop.created_at).toLocaleString()}
{drop.start_time && ` · Start: ${new Date(drop.start_time).toLocaleString()}`}
</p>
{/* Fill Bar */}
<div className="progress" style={{ marginBottom: '8px' }}>
<span style={{ width: `${getProgressPercentage(drop.fill, drop.size)}%` }}></span>
</div>
<p style={{ color: 'var(--muted)', fontSize: '14px', marginBottom: '12px' }}>
{drop.unit === 'kg' ? drop.fill.toFixed(2) : Math.round(drop.fill)}{drop.unit} of {drop.size}{drop.unit} reserved
</p>
{drop.image_url && (
<img
src={drop.image_url}
alt={drop.item}
style={{
maxWidth: '200px',
maxHeight: '200px',
marginTop: '12px',
borderRadius: '8px'
}}
/>
)}
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<button
onClick={() => handleViewSales(drop.id)}
style={{
padding: '6px 12px',
fontSize: '12px',
background: 'transparent',
border: '1px solid var(--border)',
borderRadius: '6px',
color: 'var(--text)',
cursor: 'pointer',
whiteSpace: 'nowrap'
}}
>
View Sales
</button>
<button
onClick={() => handleEdit(drop)}
style={{
padding: '6px 12px',
fontSize: '12px',
background: 'transparent',
border: '1px solid var(--border)',
borderRadius: '6px',
color: 'var(--text)',
cursor: 'pointer'
}}
>
Edit
</button>
<button
onClick={() => handleDelete(drop.id)}
style={{
padding: '6px 12px',
fontSize: '12px',
background: '#dc2626',
border: 'none',
borderRadius: '6px',
color: '#fff',
cursor: 'pointer'
}}
>
Delete
</button>
</div>
</div>
</div>
)}
</div>
))}
</div>
)}
{/* Sales Popup */}
{showSalesPopup && (
<div
style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
background: 'rgba(0, 0, 0, 0.7)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000,
padding: '20px',
}}
onClick={() => setShowSalesPopup(false)}
>
<div
style={{
background: 'var(--card)',
borderRadius: '16px',
padding: '32px',
maxWidth: '800px',
width: '100%',
maxHeight: '80vh',
overflow: 'auto',
boxShadow: '0 20px 60px rgba(0, 0, 0, 0.3)',
}}
onClick={(e) => e.stopPropagation()}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px' }}>
<h2 style={{ margin: 0 }}>Sales for Drop #{selectedDropId}</h2>
<button
onClick={() => setShowSalesPopup(false)}
style={{
background: 'transparent',
border: 'none',
color: 'var(--text)',
fontSize: '24px',
cursor: 'pointer',
padding: '0',
width: '32px',
height: '32px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
×
</button>
</div>
{salesForDrop.length === 0 ? (
<p style={{ color: 'var(--muted)', textAlign: 'center', padding: '40px' }}>
No sales found for this drop
</p>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
{salesForDrop.map((sale) => (
<div
key={sale.id}
style={{
background: 'var(--bg-soft)',
padding: '16px',
borderRadius: '8px',
border: '1px solid var(--border)'
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
<div style={{ flex: 1 }}>
<p style={{ margin: 0, marginBottom: '4px', fontWeight: '600' }}>
Sale #{sale.id}
</p>
<p style={{ margin: 0, color: 'var(--muted)', fontSize: '14px', marginBottom: '4px' }}>
Buyer: {sale.buyer_username || `#${sale.buyer_id}`} ({sale.buyer_email || 'N/A'})
</p>
<p style={{ margin: 0, color: 'var(--muted)', fontSize: '14px', marginBottom: '4px' }}>
Size: {sale.size}g
</p>
{(sale.buyer_fullname || sale.buyer_address || sale.buyer_phone) && (
<div style={{
background: 'var(--card)',
padding: '10px',
borderRadius: '6px',
marginTop: '8px',
marginBottom: '8px'
}}>
<p style={{ margin: 0, color: 'var(--text)', fontSize: '12px', fontWeight: '600', marginBottom: '4px' }}>
Delivery Information:
</p>
{sale.buyer_fullname && (
<p style={{ margin: 0, color: 'var(--muted)', fontSize: '12px', marginBottom: '2px' }}>
Name: {sale.buyer_fullname}
</p>
)}
{sale.buyer_address && (
<p style={{ margin: 0, color: 'var(--muted)', fontSize: '12px', marginBottom: '2px' }}>
Address: {sale.buyer_address}
</p>
)}
{sale.buyer_phone && (
<p style={{ margin: 0, color: 'var(--muted)', fontSize: '12px', marginBottom: '2px' }}>
Phone: {sale.buyer_phone}
</p>
)}
</div>
)}
<p style={{ margin: 0, color: 'var(--muted)', fontSize: '12px' }}>
{new Date(sale.created_at).toLocaleString()}
{sale.payment_id && ` · Payment ID: ${sale.payment_id}`}
</p>
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
)}
</div>
</div>
)
}