889 lines
31 KiB
TypeScript
889 lines
31 KiB
TypeScript
'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
|
||
created_at: string
|
||
}
|
||
|
||
interface Buyer {
|
||
id: number
|
||
username: string
|
||
email: string
|
||
created_at?: string
|
||
}
|
||
|
||
interface Sale {
|
||
id: number
|
||
drop_id: number
|
||
buyer_id: number
|
||
size: number
|
||
payment_id: string | null
|
||
created_at: string
|
||
drop_item?: string
|
||
drop_unit?: string
|
||
drop_ppu?: number
|
||
buyer_username?: string
|
||
buyer_email?: string
|
||
}
|
||
|
||
export default function AdminPage() {
|
||
const router = useRouter()
|
||
const [drops, setDrops] = useState<Drop[]>([])
|
||
const [buyers, setBuyers] = useState<Buyer[]>([])
|
||
const [sales, setSales] = useState<Sale[]>([])
|
||
const [loading, setLoading] = useState(true)
|
||
const [buyersLoading, setBuyersLoading] = useState(false)
|
||
const [salesLoading, setSalesLoading] = useState(false)
|
||
const [submitting, setSubmitting] = useState(false)
|
||
const [uploadingImage, setUploadingImage] = useState(false)
|
||
const [editingBuyer, setEditingBuyer] = useState<Buyer | null>(null)
|
||
const [editingSale, setEditingSale] = useState<Sale | null>(null)
|
||
const [formData, setFormData] = useState({
|
||
item: '',
|
||
size: '',
|
||
unit: 'g',
|
||
ppu: '',
|
||
imageFile: null as File | null,
|
||
imagePreview: '',
|
||
startTime: '',
|
||
})
|
||
const [buyerFormData, setBuyerFormData] = useState({
|
||
username: '',
|
||
email: '',
|
||
password: '',
|
||
})
|
||
const [saleFormData, setSaleFormData] = useState({
|
||
drop_id: '',
|
||
buyer_id: '',
|
||
size: '',
|
||
payment_id: '',
|
||
})
|
||
|
||
useEffect(() => {
|
||
fetchDrops()
|
||
fetchBuyers()
|
||
fetchSales()
|
||
}, [])
|
||
|
||
const fetchDrops = async () => {
|
||
try {
|
||
const response = await fetch('/api/drops')
|
||
if (!response.ok) {
|
||
throw new Error('Failed to fetch drops')
|
||
}
|
||
const data = await response.json()
|
||
// Ensure data is always an array
|
||
setDrops(Array.isArray(data) ? data : [])
|
||
} catch (error) {
|
||
console.error('Error fetching drops:', error)
|
||
setDrops([]) // Set to empty array on error
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}
|
||
|
||
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||
const file = e.target.files?.[0]
|
||
if (file) {
|
||
setFormData({
|
||
...formData,
|
||
imageFile: file,
|
||
imagePreview: URL.createObjectURL(file),
|
||
})
|
||
}
|
||
}
|
||
|
||
const handleSubmit = async (e: React.FormEvent) => {
|
||
e.preventDefault()
|
||
setSubmitting(true)
|
||
|
||
try {
|
||
let imageUrl = ''
|
||
|
||
// Upload image if provided
|
||
if (formData.imageFile) {
|
||
setUploadingImage(true)
|
||
const uploadFormData = new FormData()
|
||
uploadFormData.append('file', formData.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)
|
||
setSubmitting(false)
|
||
return
|
||
}
|
||
|
||
const uploadData = await uploadResponse.json()
|
||
imageUrl = uploadData.url
|
||
setUploadingImage(false)
|
||
}
|
||
|
||
// Create drop
|
||
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.trim() || 'g',
|
||
ppu: parseInt(formData.ppu, 10),
|
||
imageUrl: imageUrl,
|
||
startTime: formData.startTime || null,
|
||
}),
|
||
})
|
||
|
||
if (response.ok) {
|
||
// Reset form
|
||
setFormData({
|
||
item: '',
|
||
size: '',
|
||
unit: 'g',
|
||
ppu: '',
|
||
imageFile: null,
|
||
imagePreview: '',
|
||
startTime: '',
|
||
})
|
||
// Clear file input
|
||
const fileInput = document.getElementById('imageFile') as HTMLInputElement
|
||
if (fileInput) fileInput.value = ''
|
||
// Refresh drops list
|
||
fetchDrops()
|
||
alert('Drop created successfully!')
|
||
} else {
|
||
const error = await response.json()
|
||
alert(`Error: ${error.error}`)
|
||
}
|
||
} catch (error) {
|
||
console.error('Error creating drop:', error)
|
||
alert('Failed to create drop')
|
||
} finally {
|
||
setSubmitting(false)
|
||
setUploadingImage(false)
|
||
}
|
||
}
|
||
|
||
const getProgressPercentage = (fill: number, size: number) => {
|
||
return Math.min((fill / size) * 100, 100).toFixed(1)
|
||
}
|
||
|
||
const isSoldOut = (fill: number, size: number) => {
|
||
return fill >= size
|
||
}
|
||
|
||
const fetchBuyers = async () => {
|
||
setBuyersLoading(true)
|
||
try {
|
||
const response = await fetch('/api/buyers')
|
||
if (response.ok) {
|
||
const data = await response.json()
|
||
setBuyers(Array.isArray(data) ? data : [])
|
||
}
|
||
} catch (error) {
|
||
console.error('Error fetching buyers:', error)
|
||
setBuyers([])
|
||
} finally {
|
||
setBuyersLoading(false)
|
||
}
|
||
}
|
||
|
||
const fetchSales = async () => {
|
||
setSalesLoading(true)
|
||
try {
|
||
const response = await fetch('/api/sales/list')
|
||
if (response.ok) {
|
||
const data = await response.json()
|
||
setSales(Array.isArray(data) ? data : [])
|
||
}
|
||
} catch (error) {
|
||
console.error('Error fetching sales:', error)
|
||
setSales([])
|
||
} finally {
|
||
setSalesLoading(false)
|
||
}
|
||
}
|
||
|
||
const handleEditBuyer = (buyer: Buyer) => {
|
||
setEditingBuyer(buyer)
|
||
setBuyerFormData({
|
||
username: buyer.username,
|
||
email: buyer.email,
|
||
password: '',
|
||
})
|
||
}
|
||
|
||
const handleSaveBuyer = async () => {
|
||
if (!editingBuyer) return
|
||
|
||
try {
|
||
const updateData: any = {}
|
||
if (buyerFormData.username !== editingBuyer.username) {
|
||
updateData.username = buyerFormData.username
|
||
}
|
||
if (buyerFormData.email !== editingBuyer.email) {
|
||
updateData.email = buyerFormData.email
|
||
}
|
||
if (buyerFormData.password) {
|
||
updateData.password = buyerFormData.password
|
||
}
|
||
|
||
if (Object.keys(updateData).length === 0) {
|
||
setEditingBuyer(null)
|
||
return
|
||
}
|
||
|
||
const response = await fetch(`/api/buyers/${editingBuyer.id}`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(updateData),
|
||
})
|
||
|
||
if (response.ok) {
|
||
alert('Buyer updated successfully')
|
||
setEditingBuyer(null)
|
||
fetchBuyers()
|
||
} else {
|
||
const error = await response.json()
|
||
alert(`Error: ${error.error}`)
|
||
}
|
||
} catch (error) {
|
||
console.error('Error updating buyer:', error)
|
||
alert('Failed to update buyer')
|
||
}
|
||
}
|
||
|
||
const handleDeleteBuyer = async (id: number) => {
|
||
if (!confirm('Are you sure you want to delete this buyer? This will also delete all their sales.')) {
|
||
return
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`/api/buyers/${id}`, {
|
||
method: 'DELETE',
|
||
})
|
||
|
||
if (response.ok) {
|
||
alert('Buyer deleted successfully')
|
||
fetchBuyers()
|
||
} else {
|
||
const error = await response.json()
|
||
alert(`Error: ${error.error}`)
|
||
}
|
||
} catch (error) {
|
||
console.error('Error deleting buyer:', error)
|
||
alert('Failed to delete buyer')
|
||
}
|
||
}
|
||
|
||
const handleEditSale = (sale: Sale) => {
|
||
setEditingSale(sale)
|
||
setSaleFormData({
|
||
drop_id: sale.drop_id.toString(),
|
||
buyer_id: sale.buyer_id.toString(),
|
||
size: sale.size.toString(),
|
||
payment_id: sale.payment_id || '',
|
||
})
|
||
}
|
||
|
||
const handleSaveSale = async () => {
|
||
if (!editingSale) return
|
||
|
||
try {
|
||
const updateData: any = {}
|
||
if (parseInt(saleFormData.drop_id) !== editingSale.drop_id) {
|
||
updateData.drop_id = parseInt(saleFormData.drop_id)
|
||
}
|
||
if (parseInt(saleFormData.buyer_id) !== editingSale.buyer_id) {
|
||
updateData.buyer_id = parseInt(saleFormData.buyer_id)
|
||
}
|
||
if (parseInt(saleFormData.size) !== editingSale.size) {
|
||
updateData.size = parseInt(saleFormData.size)
|
||
}
|
||
if (saleFormData.payment_id !== (editingSale.payment_id || '')) {
|
||
updateData.payment_id = saleFormData.payment_id || null
|
||
}
|
||
|
||
if (Object.keys(updateData).length === 0) {
|
||
setEditingSale(null)
|
||
return
|
||
}
|
||
|
||
const response = await fetch(`/api/sales/${editingSale.id}`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(updateData),
|
||
})
|
||
|
||
if (response.ok) {
|
||
alert('Sale updated successfully')
|
||
setEditingSale(null)
|
||
fetchSales()
|
||
fetchDrops() // Refresh drops to update fill
|
||
} else {
|
||
const error = await response.json()
|
||
alert(`Error: ${error.error}`)
|
||
}
|
||
} catch (error) {
|
||
console.error('Error updating sale:', error)
|
||
alert('Failed to update sale')
|
||
}
|
||
}
|
||
|
||
const handleDeleteSale = async (id: number) => {
|
||
if (!confirm('Are you sure you want to delete this sale?')) {
|
||
return
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`/api/sales/${id}`, {
|
||
method: 'DELETE',
|
||
})
|
||
|
||
if (response.ok) {
|
||
alert('Sale deleted successfully')
|
||
fetchSales()
|
||
fetchDrops() // Refresh drops to update fill
|
||
} else {
|
||
const error = await response.json()
|
||
alert(`Error: ${error.error}`)
|
||
}
|
||
} catch (error) {
|
||
console.error('Error deleting sale:', error)
|
||
alert('Failed to delete sale')
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className="admin-page">
|
||
<div className="container">
|
||
<div className="admin-header">
|
||
<h1>Admin Panel</h1>
|
||
<button onClick={() => router.push('/')}>View Site</button>
|
||
</div>
|
||
|
||
<div className="admin-grid">
|
||
{/* Create Drop Form */}
|
||
<div className="admin-section">
|
||
<h2>Create New Drop</h2>
|
||
<form onSubmit={handleSubmit} className="admin-form">
|
||
<div className="form-group">
|
||
<label htmlFor="item">Product Name</label>
|
||
<input
|
||
type="text"
|
||
id="item"
|
||
value={formData.item}
|
||
onChange={(e) =>
|
||
setFormData({ ...formData, item: e.target.value })
|
||
}
|
||
required
|
||
placeholder="e.g. Harlequin – Collective Drop"
|
||
/>
|
||
</div>
|
||
|
||
<div className="form-grid">
|
||
<div className="form-group">
|
||
<label htmlFor="size">Batch Size</label>
|
||
<input
|
||
type="number"
|
||
id="size"
|
||
value={formData.size}
|
||
onChange={(e) =>
|
||
setFormData({ ...formData, size: e.target.value })
|
||
}
|
||
required
|
||
placeholder="1000"
|
||
min="1"
|
||
/>
|
||
</div>
|
||
|
||
<div className="form-group">
|
||
<label htmlFor="unit">Unit</label>
|
||
<input
|
||
type="text"
|
||
id="unit"
|
||
value={formData.unit}
|
||
onChange={(e) =>
|
||
setFormData({ ...formData, unit: e.target.value })
|
||
}
|
||
placeholder="g, kg, ml, etc."
|
||
required
|
||
maxLength={12}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="form-group">
|
||
<label htmlFor="ppu">Price Per Unit (in cents, e.g., 2500 = 2.50 CHF)</label>
|
||
<input
|
||
type="number"
|
||
id="ppu"
|
||
value={formData.ppu}
|
||
onChange={(e) =>
|
||
setFormData({ ...formData, ppu: e.target.value })
|
||
}
|
||
required
|
||
placeholder="2500"
|
||
step="1"
|
||
min="1"
|
||
/>
|
||
<p className="file-hint" style={{ marginTop: '8px', fontSize: '12px' }}>
|
||
Enter price in smallest currency unit. 1000 = 1.00 CHF, 2500 = 2.50 CHF, 10 = 0.01 CHF
|
||
</p>
|
||
</div>
|
||
|
||
<div className="form-group">
|
||
<label htmlFor="startTime">Start Time (optional)</label>
|
||
<input
|
||
type="datetime-local"
|
||
id="startTime"
|
||
value={formData.startTime}
|
||
onChange={(e) =>
|
||
setFormData({ ...formData, startTime: e.target.value })
|
||
}
|
||
/>
|
||
<p className="file-hint" style={{ marginTop: '8px', fontSize: '12px' }}>
|
||
When should this drop become available? Leave empty to make it available immediately.
|
||
</p>
|
||
</div>
|
||
|
||
<div className="form-group">
|
||
<label htmlFor="imageFile">Product Image (optional)</label>
|
||
<input
|
||
type="file"
|
||
id="imageFile"
|
||
accept="image/jpeg,image/jpg,image/png,image/webp"
|
||
onChange={handleImageChange}
|
||
className="file-input"
|
||
/>
|
||
{formData.imagePreview && (
|
||
<div className="image-preview">
|
||
<img src={formData.imagePreview} alt="Preview" />
|
||
</div>
|
||
)}
|
||
<p className="file-hint">
|
||
Max file size: 5MB. Allowed formats: JPEG, PNG, WebP
|
||
</p>
|
||
</div>
|
||
|
||
<button
|
||
type="submit"
|
||
disabled={submitting || uploadingImage}
|
||
className="cta"
|
||
style={{ width: '100%', marginTop: '20px' }}
|
||
>
|
||
{uploadingImage
|
||
? 'Uploading image...'
|
||
: submitting
|
||
? 'Creating...'
|
||
: 'Create Drop'}
|
||
</button>
|
||
</form>
|
||
</div>
|
||
|
||
{/* Drops List */}
|
||
<div className="admin-section">
|
||
<h2>All Drops</h2>
|
||
{loading ? (
|
||
<p style={{ color: 'var(--muted)' }}>Loading...</p>
|
||
) : !Array.isArray(drops) || drops.length === 0 ? (
|
||
<p style={{ color: 'var(--muted)' }}>No drops yet</p>
|
||
) : (
|
||
<div className="drops-list">
|
||
{drops.map((drop) => (
|
||
<div
|
||
key={drop.id}
|
||
className="drop-card"
|
||
style={{
|
||
background: isSoldOut(drop.fill, drop.size)
|
||
? 'var(--bg-soft)'
|
||
: 'var(--card)',
|
||
opacity: isSoldOut(drop.fill, drop.size) ? 0.7 : 1,
|
||
}}
|
||
>
|
||
<div className="drop-card-header">
|
||
<div className="drop-card-info">
|
||
<h3>{drop.item}</h3>
|
||
<p>
|
||
ID: {drop.id} · Created:{' '}
|
||
{new Date(drop.created_at).toLocaleDateString()}
|
||
</p>
|
||
</div>
|
||
{isSoldOut(drop.fill, drop.size) && (
|
||
<span className="sold-out-badge">Sold Out</span>
|
||
)}
|
||
</div>
|
||
|
||
<div className="drop-card-progress">
|
||
<div className="drop-card-progress-header">
|
||
<span style={{ color: 'var(--muted)' }}>
|
||
{drop.fill}
|
||
{drop.unit} / {drop.size}
|
||
{drop.unit}
|
||
</span>
|
||
<span>
|
||
{getProgressPercentage(drop.fill, drop.size)}%
|
||
</span>
|
||
</div>
|
||
<div className="progress">
|
||
<span
|
||
style={{
|
||
width: `${getProgressPercentage(
|
||
drop.fill,
|
||
drop.size
|
||
)}%`,
|
||
}}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="drop-card-price">
|
||
{(drop.ppu / 1000).toFixed(2)} CHF / {drop.unit}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Buyers Section */}
|
||
<div className="admin-section">
|
||
<h2>Buyers</h2>
|
||
{buyersLoading ? (
|
||
<p style={{ color: 'var(--muted)' }}>Loading...</p>
|
||
) : buyers.length === 0 ? (
|
||
<p style={{ color: 'var(--muted)' }}>No buyers yet</p>
|
||
) : (
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||
{buyers.map((buyer) => (
|
||
<div
|
||
key={buyer.id}
|
||
className="drop-card"
|
||
style={{
|
||
background: editingBuyer?.id === buyer.id ? 'var(--bg-soft)' : 'var(--card)',
|
||
}}
|
||
>
|
||
{editingBuyer?.id === buyer.id ? (
|
||
<div style={{ padding: '16px' }}>
|
||
<div className="form-group" style={{ marginBottom: '12px' }}>
|
||
<label>Username</label>
|
||
<input
|
||
type="text"
|
||
value={buyerFormData.username}
|
||
onChange={(e) =>
|
||
setBuyerFormData({ ...buyerFormData, username: e.target.value })
|
||
}
|
||
style={{
|
||
width: '100%',
|
||
padding: '8px',
|
||
borderRadius: '8px',
|
||
border: '1px solid var(--border)',
|
||
background: 'var(--bg-soft)',
|
||
color: 'var(--text)',
|
||
}}
|
||
/>
|
||
</div>
|
||
<div className="form-group" style={{ marginBottom: '12px' }}>
|
||
<label>Email</label>
|
||
<input
|
||
type="email"
|
||
value={buyerFormData.email}
|
||
onChange={(e) =>
|
||
setBuyerFormData({ ...buyerFormData, email: e.target.value })
|
||
}
|
||
style={{
|
||
width: '100%',
|
||
padding: '8px',
|
||
borderRadius: '8px',
|
||
border: '1px solid var(--border)',
|
||
background: 'var(--bg-soft)',
|
||
color: 'var(--text)',
|
||
}}
|
||
/>
|
||
</div>
|
||
<div className="form-group" style={{ marginBottom: '12px' }}>
|
||
<label>New Password (leave empty to keep current)</label>
|
||
<input
|
||
type="password"
|
||
value={buyerFormData.password}
|
||
onChange={(e) =>
|
||
setBuyerFormData({ ...buyerFormData, password: e.target.value })
|
||
}
|
||
style={{
|
||
width: '100%',
|
||
padding: '8px',
|
||
borderRadius: '8px',
|
||
border: '1px solid var(--border)',
|
||
background: 'var(--bg-soft)',
|
||
color: 'var(--text)',
|
||
}}
|
||
/>
|
||
</div>
|
||
<div style={{ display: 'flex', gap: '8px' }}>
|
||
<button
|
||
onClick={handleSaveBuyer}
|
||
className="cta"
|
||
style={{ padding: '8px 16px', fontSize: '14px' }}
|
||
>
|
||
Save
|
||
</button>
|
||
<button
|
||
onClick={() => setEditingBuyer(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 className="drop-card-header">
|
||
<div className="drop-card-info">
|
||
<h3>{buyer.username}</h3>
|
||
<p style={{ color: 'var(--muted)', fontSize: '14px' }}>
|
||
{buyer.email} · ID: {buyer.id}
|
||
</p>
|
||
{buyer.created_at && (
|
||
<p style={{ color: 'var(--muted)', fontSize: '12px' }}>
|
||
Joined: {new Date(buyer.created_at).toLocaleDateString()}
|
||
</p>
|
||
)}
|
||
</div>
|
||
<div style={{ display: 'flex', gap: '8px' }}>
|
||
<button
|
||
onClick={() => handleEditBuyer(buyer)}
|
||
style={{
|
||
padding: '6px 12px',
|
||
fontSize: '12px',
|
||
background: 'transparent',
|
||
border: '1px solid var(--border)',
|
||
borderRadius: '6px',
|
||
color: 'var(--text)',
|
||
cursor: 'pointer',
|
||
}}
|
||
>
|
||
Edit
|
||
</button>
|
||
<button
|
||
onClick={() => handleDeleteBuyer(buyer.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 Section */}
|
||
<div className="admin-section">
|
||
<h2>Sales</h2>
|
||
{salesLoading ? (
|
||
<p style={{ color: 'var(--muted)' }}>Loading...</p>
|
||
) : sales.length === 0 ? (
|
||
<p style={{ color: 'var(--muted)' }}>No sales yet</p>
|
||
) : (
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||
{sales.map((sale) => (
|
||
<div
|
||
key={sale.id}
|
||
className="drop-card"
|
||
style={{
|
||
background: editingSale?.id === sale.id ? 'var(--bg-soft)' : 'var(--card)',
|
||
}}
|
||
>
|
||
{editingSale?.id === sale.id ? (
|
||
<div style={{ padding: '16px' }}>
|
||
<div className="form-group" style={{ marginBottom: '12px' }}>
|
||
<label>Drop ID</label>
|
||
<input
|
||
type="number"
|
||
value={saleFormData.drop_id}
|
||
onChange={(e) =>
|
||
setSaleFormData({ ...saleFormData, drop_id: e.target.value })
|
||
}
|
||
style={{
|
||
width: '100%',
|
||
padding: '8px',
|
||
borderRadius: '8px',
|
||
border: '1px solid var(--border)',
|
||
background: 'var(--bg-soft)',
|
||
color: 'var(--text)',
|
||
}}
|
||
/>
|
||
</div>
|
||
<div className="form-group" style={{ marginBottom: '12px' }}>
|
||
<label>Buyer ID</label>
|
||
<input
|
||
type="number"
|
||
value={saleFormData.buyer_id}
|
||
onChange={(e) =>
|
||
setSaleFormData({ ...saleFormData, buyer_id: e.target.value })
|
||
}
|
||
style={{
|
||
width: '100%',
|
||
padding: '8px',
|
||
borderRadius: '8px',
|
||
border: '1px solid var(--border)',
|
||
background: 'var(--bg-soft)',
|
||
color: 'var(--text)',
|
||
}}
|
||
/>
|
||
</div>
|
||
<div className="form-group" style={{ marginBottom: '12px' }}>
|
||
<label>Size (grams)</label>
|
||
<input
|
||
type="number"
|
||
value={saleFormData.size}
|
||
onChange={(e) =>
|
||
setSaleFormData({ ...saleFormData, 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 className="form-group" style={{ marginBottom: '12px' }}>
|
||
<label>Payment ID</label>
|
||
<input
|
||
type="text"
|
||
value={saleFormData.payment_id}
|
||
onChange={(e) =>
|
||
setSaleFormData({ ...saleFormData, payment_id: e.target.value })
|
||
}
|
||
style={{
|
||
width: '100%',
|
||
padding: '8px',
|
||
borderRadius: '8px',
|
||
border: '1px solid var(--border)',
|
||
background: 'var(--bg-soft)',
|
||
color: 'var(--text)',
|
||
}}
|
||
/>
|
||
</div>
|
||
<div style={{ display: 'flex', gap: '8px' }}>
|
||
<button
|
||
onClick={handleSaveSale}
|
||
className="cta"
|
||
style={{ padding: '8px 16px', fontSize: '14px' }}
|
||
>
|
||
Save
|
||
</button>
|
||
<button
|
||
onClick={() => setEditingSale(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 className="drop-card-header">
|
||
<div className="drop-card-info">
|
||
<h3>Sale #{sale.id}</h3>
|
||
<p style={{ color: 'var(--muted)', fontSize: '14px' }}>
|
||
{sale.drop_item || `Drop #${sale.drop_id}`} · {sale.size}g
|
||
</p>
|
||
<p style={{ color: 'var(--muted)', fontSize: '14px' }}>
|
||
Buyer: {sale.buyer_username || `#${sale.buyer_id}`} ({sale.buyer_email || 'N/A'})
|
||
</p>
|
||
{sale.drop_ppu && (
|
||
<p style={{ color: 'var(--muted)', fontSize: '14px' }}>
|
||
Price: {((sale.drop_ppu / 1000) * sale.size).toFixed(2)} CHF
|
||
</p>
|
||
)}
|
||
<p style={{ color: 'var(--muted)', fontSize: '12px' }}>
|
||
{new Date(sale.created_at).toLocaleString()}
|
||
{sale.payment_id && ` · Payment: ${sale.payment_id}`}
|
||
</p>
|
||
</div>
|
||
<div style={{ display: 'flex', gap: '8px' }}>
|
||
<button
|
||
onClick={() => handleEditSale(sale)}
|
||
style={{
|
||
padding: '6px 12px',
|
||
fontSize: '12px',
|
||
background: 'transparent',
|
||
border: '1px solid var(--border)',
|
||
borderRadius: '6px',
|
||
color: 'var(--text)',
|
||
cursor: 'pointer',
|
||
}}
|
||
>
|
||
Edit
|
||
</button>
|
||
<button
|
||
onClick={() => handleDeleteSale(sale.id)}
|
||
style={{
|
||
padding: '6px 12px',
|
||
fontSize: '12px',
|
||
background: '#dc2626',
|
||
border: 'none',
|
||
borderRadius: '6px',
|
||
color: '#fff',
|
||
cursor: 'pointer',
|
||
}}
|
||
>
|
||
Delete
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|