Rocky_Mountain_Vending/components/product-admin.tsx

635 lines
19 KiB
TypeScript

"use client"
import { useState, useEffect } from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { Alert, AlertDescription } from "@/components/ui/alert"
import {
Plus,
Search,
Edit,
Trash2,
Save,
X,
AlertCircle,
CheckCircle,
DollarSign,
Package,
Image,
} from "lucide-react"
import type { Product } from "@/lib/products/types"
interface ProductFormData {
name: string
description: string
price: string
currency: string
images: string[]
metadata: Record<string, string>
}
const initialFormData: ProductFormData = {
name: "",
description: "",
price: "",
currency: "usd",
images: [],
metadata: {},
}
const currencyOptions = [
{ value: "usd", label: "USD ($)" },
{ value: "eur", label: "EUR (€)" },
{ value: "gbp", label: "GBP (£)" },
{ value: "cad", label: "CAD ($)" },
{ value: "aud", label: "AUD ($)" },
]
export function ProductAdmin() {
const [products, setProducts] = useState<Product[]>([])
const [loading, setLoading] = useState(true)
const [searchTerm, setSearchTerm] = useState("")
const [selectedProducts, setSelectedProducts] = useState<string[]>([])
const [showCreateDialog, setShowCreateDialog] = useState(false)
const [editingProduct, setEditingProduct] = useState<Product | null>(null)
const [isSaving, setIsSaving] = useState(false)
const [error, setError] = useState<string | null>(null)
const [success, setSuccess] = useState<string | null>(null)
const [formData, setFormData] = useState<ProductFormData>(initialFormData)
// Fetch products
const fetchProducts = async () => {
try {
setLoading(true)
const response = await fetch("/api/products/admin")
if (!response.ok) throw new Error("Failed to fetch products")
const data = await response.json()
setProducts(data.products)
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to fetch products")
} finally {
setLoading(false)
}
}
useEffect(() => {
fetchProducts()
}, [])
// Handle form input changes
const handleInputChange = (field: keyof ProductFormData, value: any) => {
setFormData((prev) => ({
...prev,
[field]: value,
}))
}
// Handle metadata input changes
const handleMetadataChange = (key: string, value: string) => {
setFormData((prev) => ({
...prev,
metadata: {
...prev.metadata,
[key]: value,
},
}))
}
// Add metadata field
const addMetadataField = () => {
const emptyKey =
Object.keys(formData.metadata).length === 0
? "category"
: `key${Object.keys(formData.metadata).length + 1}`
setFormData((prev) => ({
...prev,
metadata: {
...prev.metadata,
[emptyKey]: "",
},
}))
}
// Remove metadata field
const removeMetadataField = (key: string) => {
setFormData((prev) => {
const newMetadata = { ...prev.metadata }
delete newMetadata[key]
return {
...prev,
metadata: newMetadata,
}
})
}
// Create product
const handleCreateProduct = async () => {
try {
setIsSaving(true)
setError(null)
setSuccess(null)
// Validate form
if (!formData.name || !formData.price) {
setError("Name and price are required")
return
}
const price = parseFloat(formData.price)
if (isNaN(price) || price <= 0) {
setError("Price must be a valid positive number")
return
}
const response = await fetch("/api/products/admin", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: formData.name,
description: formData.description,
price,
currency: formData.currency,
images: formData.images.filter(Boolean),
metadata: formData.metadata,
}),
})
if (!response.ok) {
const errorData = await response.json()
throw new Error(errorData.error || "Failed to create product")
}
const newProduct = await response.json()
setProducts((prev) => [newProduct.product, ...prev])
setSuccess(`Product "${newProduct.product.name}" created successfully`)
// Reset form
setFormData(initialFormData)
setShowCreateDialog(false)
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to create product")
} finally {
setIsSaving(false)
}
}
// Update product (placeholder - would need implementation)
const handleUpdateProduct = async (product: Product) => {
try {
setIsSaving(true)
setError(null)
setSuccess(null)
// In a real implementation, you would call the update API
console.log("Updating product:", product)
setSuccess(`Product "${product.name}" updated successfully`)
setEditingProduct(null)
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to update product")
} finally {
setIsSaving(false)
}
}
// Delete product (placeholder - would need implementation)
const handleDeleteProduct = async (productId: string) => {
try {
if (
!confirm(
"Are you sure you want to delete this product? This action cannot be undone."
)
) {
return
}
setLoading(true)
setError(null)
// In a real implementation, you would call the delete API
console.log("Deleting product:", productId)
setProducts((prev) => prev.filter((p) => p.id !== productId))
setSuccess("Product deleted successfully")
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to delete product")
} finally {
setLoading(false)
}
}
// Bulk actions
const handleBulkAction = async (action: "deactivate") => {
try {
setLoading(true)
setError(null)
const response = await fetch("/api/products/admin", {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
action,
productIds: selectedProducts,
}),
})
if (!response.ok) {
throw new Error("Failed to perform bulk action")
}
await fetchProducts()
setSelectedProducts([])
setSuccess(
`Successfully ${action}d ${selectedProducts.length} product(s)`
)
} catch (err) {
setError(
err instanceof Error ? err.message : "Failed to perform bulk action"
)
} finally {
setLoading(false)
}
}
// Filter products based on search
const filteredProducts = products.filter(
(product) =>
product.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
product.description?.toLowerCase().includes(searchTerm.toLowerCase())
)
// Clear alerts
useEffect(() => {
if (error || success) {
const timer = setTimeout(() => {
setError(null)
setSuccess(null)
}, 5000)
return () => clearTimeout(timer)
}
}, [error, success])
return (
<div className="space-y-6">
{/* Alerts */}
{error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{success && (
<Alert>
<CheckCircle className="h-4 w-4" />
<AlertDescription>{success}</AlertDescription>
</Alert>
)}
{/* Header */}
<div className="flex justify-between items-start">
<div>
<h2 className="text-2xl font-bold">Product Management</h2>
<p className="text-muted-foreground">
Manage your Stripe products and inventory
</p>
</div>
<Dialog open={showCreateDialog} onOpenChange={setShowCreateDialog}>
<DialogTrigger asChild>
<Button>
<Plus className="h-4 w-4 mr-2" />
Add Product
</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Create New Product</DialogTitle>
<DialogDescription>
Add a new product to your Stripe inventory.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="name">Product Name *</Label>
<Input
id="name"
value={formData.name}
onChange={(e) => handleInputChange("name", e.target.value)}
placeholder="Enter product name"
/>
</div>
<div className="space-y-2">
<Label htmlFor="price">Price *</Label>
<Input
id="price"
type="number"
step="0.01"
min="0"
value={formData.price}
onChange={(e) => handleInputChange("price", e.target.value)}
placeholder="0.00"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="description">Description</Label>
<Textarea
id="description"
value={formData.description}
onChange={(e) =>
handleInputChange("description", e.target.value)
}
placeholder="Enter product description"
rows={3}
/>
</div>
<div className="space-y-2">
<Label htmlFor="currency">Currency</Label>
<Select
value={formData.currency}
onValueChange={(value) =>
handleInputChange("currency", value)
}
>
<SelectTrigger>
<SelectValue placeholder="Select currency" />
</SelectTrigger>
<SelectContent>
{currencyOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Metadata</Label>
{Object.entries(formData.metadata).map(([key, value]) => (
<div key={key} className="flex gap-2">
<Input
value={key}
onChange={(e) =>
handleMetadataChange(e.target.value, value)
}
placeholder="Key"
/>
<Input
value={value}
onChange={(e) =>
handleMetadataChange(key, e.target.value)
}
placeholder="Value"
/>
<Button
variant="ghost"
size="icon"
onClick={() => removeMetadataField(key)}
>
<X className="h-4 w-4" />
</Button>
</div>
))}
<Button
variant="outline"
size="sm"
onClick={addMetadataField}
className="w-full"
>
Add Metadata
</Button>
</div>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => setShowCreateDialog(false)}
>
Cancel
</Button>
<Button onClick={handleCreateProduct} disabled={isSaving}>
{isSaving ? "Creating..." : "Create Product"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
{/* Bulk Actions */}
{selectedProducts.length > 0 && (
<Card>
<CardHeader>
<CardTitle className="text-base">Bulk Actions</CardTitle>
</CardHeader>
<CardContent>
<div className="flex gap-2">
<Button
variant="destructive"
size="sm"
onClick={() => handleBulkAction("deactivate")}
>
Deactivate {selectedProducts.length} Product(s)
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setSelectedProducts([])}
>
Cancel Selection
</Button>
</div>
</CardContent>
</Card>
)}
{/* Search and Filter */}
<Card>
<CardContent className="pt-6">
<div className="flex gap-4 items-center">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search products..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
<Badge variant="secondary">
{filteredProducts.length} product
{filteredProducts.length !== 1 ? "s" : ""}
</Badge>
</div>
</CardContent>
</Card>
{/* Products Table */}
<Card>
<CardHeader>
<CardTitle>Products</CardTitle>
<CardDescription>
View and manage all your Stripe products
</CardDescription>
</CardHeader>
<CardContent>
{loading ? (
<div className="flex justify-center items-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-12">
<input
type="checkbox"
checked={
selectedProducts.length === products.length &&
products.length > 0
}
onChange={(e) => {
if (e.target.checked) {
setSelectedProducts(products.map((p) => p.id))
} else {
setSelectedProducts([])
}
}}
/>
</TableHead>
<TableHead>Product</TableHead>
<TableHead>Price</TableHead>
<TableHead>Status</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredProducts.map((product) => (
<TableRow key={product.id}>
<TableCell>
<input
type="checkbox"
checked={selectedProducts.includes(product.id)}
onChange={(e) => {
if (e.target.checked) {
setSelectedProducts((prev) => [...prev, product.id])
} else {
setSelectedProducts((prev) =>
prev.filter((id) => id !== product.id)
)
}
}}
/>
</TableCell>
<TableCell>
<div className="flex items-center gap-3">
{product.images?.[0] ? (
<div className="w-10 h-10 rounded-md overflow-hidden bg-muted">
<img
src={product.images[0]}
alt={product.name}
className="w-full h-full object-cover"
/>
</div>
) : (
<div className="w-10 h-10 rounded-md bg-muted flex items-center justify-center">
<Package className="h-5 w-5 text-muted-foreground" />
</div>
)}
<div>
<div className="font-medium">{product.name}</div>
<div className="text-sm text-muted-foreground truncate max-w-[200px]">
{product.description}
</div>
</div>
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-1">
<DollarSign className="h-4 w-4 text-muted-foreground" />
{product.price.toFixed(2)}
</div>
</TableCell>
<TableCell>
<Badge variant="default">Active</Badge>
</TableCell>
<TableCell>
<div className="flex gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => setEditingProduct(product)}
>
<Edit className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleDeleteProduct(product.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
{filteredProducts.length === 0 && !loading && (
<div className="text-center py-8 text-muted-foreground">
No products found. {searchTerm && "Try adjusting your search."}
</div>
)}
</CardContent>
</Card>
</div>
)
}