diff --git a/src/components/admin/AnalyticsTab.tsx b/src/components/admin/AnalyticsTab.tsx new file mode 100644 index 0000000..2effd8d --- /dev/null +++ b/src/components/admin/AnalyticsTab.tsx @@ -0,0 +1,367 @@ +import { useState, useEffect, useMemo } from "react"; +import { supabase } from "@/integrations/supabase/client"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Badge } from "@/components/ui/badge"; +import { + BarChart, + Bar, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, + PieChart, + Pie, + Cell, + LineChart, + Line, + Legend, +} from "recharts"; +import { + TrendingUp, + TrendingDown, + DollarSign, + ShoppingCart, + Users, + CreditCard, + Calendar, + RefreshCw, +} from "lucide-react"; +import { CONFIG } from "@/config/payments"; + +interface Order { + id: string; + amount: number; + status: string; + payment_method: string; + created_at: string; + customer_email: string; +} + +const COLORS = ["hsl(142, 76%, 36%)", "hsl(38, 92%, 50%)", "hsl(0, 84%, 60%)", "hsl(240, 4%, 46%)"]; + +const AnalyticsTab = () => { + const [orders, setOrders] = useState([]); + const [loading, setLoading] = useState(true); + const [timeRange, setTimeRange] = useState("30"); + + useEffect(() => { + fetchOrders(); + }, [timeRange]); + + const fetchOrders = async () => { + setLoading(true); + try { + const startDate = new Date(); + startDate.setDate(startDate.getDate() - parseInt(timeRange)); + + const { data, error } = await supabase + .from("orders") + .select("id, amount, status, payment_method, created_at, customer_email") + .gte("created_at", startDate.toISOString()) + .order("created_at", { ascending: true }); + + if (error) throw error; + setOrders(data || []); + } catch (error) { + console.error("Failed to fetch analytics data:", error); + } finally { + setLoading(false); + } + }; + + const stats = useMemo(() => { + const verifiedOrders = orders.filter(o => o.status === "verified" || o.status === "completed"); + const totalRevenue = verifiedOrders.reduce((sum, o) => sum + Number(o.amount), 0); + const avgOrderValue = verifiedOrders.length > 0 ? totalRevenue / verifiedOrders.length : 0; + const uniqueCustomers = new Set(orders.map(o => o.customer_email)).size; + const conversionRate = orders.length > 0 ? (verifiedOrders.length / orders.length) * 100 : 0; + + return { + totalRevenue, + totalOrders: orders.length, + verifiedOrders: verifiedOrders.length, + avgOrderValue, + uniqueCustomers, + conversionRate, + }; + }, [orders]); + + const revenueByDay = useMemo(() => { + const grouped: Record = {}; + + orders.forEach(order => { + if (order.status === "verified" || order.status === "completed") { + const date = new Date(order.created_at).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + }); + if (!grouped[date]) { + grouped[date] = { date, revenue: 0, orders: 0 }; + } + grouped[date].revenue += Number(order.amount); + grouped[date].orders += 1; + } + }); + + return Object.values(grouped); + }, [orders]); + + const paymentMethodData = useMemo(() => { + const grouped: Record = {}; + + orders.forEach(order => { + const method = order.payment_method || "unknown"; + grouped[method] = (grouped[method] || 0) + 1; + }); + + return Object.entries(grouped).map(([name, value]) => ({ + name: name.charAt(0).toUpperCase() + name.slice(1), + value, + })); + }, [orders]); + + const statusData = useMemo(() => { + const grouped: Record = {}; + + orders.forEach(order => { + grouped[order.status] = (grouped[order.status] || 0) + 1; + }); + + return Object.entries(grouped).map(([name, value]) => ({ + name: name.charAt(0).toUpperCase() + name.slice(1), + value, + })); + }, [orders]); + + if (loading) { + return ( +
+ + Loading analytics... +
+ ); + } + + return ( +
+ {/* Time Range Selector */} +
+

+ + Analytics Overview +

+ +
+ + {/* Key Metrics */} +
+ + + + Total Revenue + + + + +
+ {CONFIG.store.currencySymbol}{stats.totalRevenue.toFixed(2)} +
+

+ From {stats.verifiedOrders} verified orders +

+
+
+ + + + + Total Orders + + + + +
{stats.totalOrders}
+

+ {stats.verifiedOrders} completed +

+
+
+ + + + + Avg. Order Value + + + + +
+ {CONFIG.store.currencySymbol}{stats.avgOrderValue.toFixed(2)} +
+

+ Per verified order +

+
+
+ + + + + Conversion Rate + + + + +
{stats.conversionRate.toFixed(1)}%
+

+ {stats.uniqueCustomers} unique customers +

+
+
+
+ + {/* Charts */} +
+ {/* Revenue Over Time */} + + + Revenue Over Time + + + {revenueByDay.length === 0 ? ( +
+ No revenue data for this period +
+ ) : ( + + + + + `$${value}`} + /> + [`$${value.toFixed(2)}`, "Revenue"]} + /> + + + + )} +
+
+ + {/* Orders by Status */} + + + Orders by Status + + + {statusData.length === 0 ? ( +
+ No order data for this period +
+ ) : ( + + + `${name}: ${value}`} + > + {statusData.map((_, index) => ( + + ))} + + + + + )} +
+
+ + {/* Payment Methods */} + + + Orders by Payment Method + + + {paymentMethodData.length === 0 ? ( +
+ No payment data for this period +
+ ) : ( + + + + + + [value, "Orders"]} + /> + + + + )} +
+
+
+
+ ); +}; + +export default AnalyticsTab; diff --git a/src/components/admin/DiscountCodesTab.tsx b/src/components/admin/DiscountCodesTab.tsx new file mode 100644 index 0000000..9969e89 --- /dev/null +++ b/src/components/admin/DiscountCodesTab.tsx @@ -0,0 +1,456 @@ +import { useState, useEffect } from "react"; +import { supabase } from "@/integrations/supabase/client"; +import { useToast } from "@/hooks/use-toast"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; +import { Badge } from "@/components/ui/badge"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Plus, Pencil, Trash2, Ticket, RefreshCw, Copy } from "lucide-react"; + +interface DiscountCode { + id: string; + code: string; + description: string | null; + discount_type: string; + discount_value: number; + min_order_amount: number; + max_uses: number | null; + current_uses: number; + valid_from: string; + valid_until: string | null; + is_active: boolean; + created_at: string; +} + +const DiscountCodesTab = () => { + const { toast } = useToast(); + const [codes, setCodes] = useState([]); + const [loading, setLoading] = useState(true); + const [dialogOpen, setDialogOpen] = useState(false); + const [editingCode, setEditingCode] = useState(null); + const [saving, setSaving] = useState(false); + + const [formData, setFormData] = useState({ + code: "", + description: "", + discount_type: "percentage", + discount_value: "", + min_order_amount: "0", + max_uses: "", + valid_until: "", + is_active: true, + }); + + useEffect(() => { + fetchCodes(); + }, []); + + const fetchCodes = async () => { + setLoading(true); + try { + const { data, error } = await supabase + .from("discount_codes") + .select("*") + .order("created_at", { ascending: false }); + + if (error) throw error; + setCodes(data || []); + } catch (error) { + console.error("Failed to fetch discount codes:", error); + toast({ + title: "Error", + description: "Failed to load discount codes.", + variant: "destructive", + }); + } finally { + setLoading(false); + } + }; + + const resetForm = () => { + setFormData({ + code: "", + description: "", + discount_type: "percentage", + discount_value: "", + min_order_amount: "0", + max_uses: "", + valid_until: "", + is_active: true, + }); + setEditingCode(null); + }; + + const generateCode = () => { + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + let code = ""; + for (let i = 0; i < 8; i++) { + code += chars.charAt(Math.floor(Math.random() * chars.length)); + } + setFormData({ ...formData, code }); + }; + + const openEditDialog = (code: DiscountCode) => { + setEditingCode(code); + setFormData({ + code: code.code, + description: code.description || "", + discount_type: code.discount_type, + discount_value: code.discount_value.toString(), + min_order_amount: code.min_order_amount.toString(), + max_uses: code.max_uses?.toString() || "", + valid_until: code.valid_until ? code.valid_until.split("T")[0] : "", + is_active: code.is_active, + }); + setDialogOpen(true); + }; + + const handleSubmit = async () => { + if (!formData.code || !formData.discount_value) { + toast({ + title: "Validation Error", + description: "Code and discount value are required.", + variant: "destructive", + }); + return; + } + + setSaving(true); + try { + const codeData = { + code: formData.code.toUpperCase().trim(), + description: formData.description.trim() || null, + discount_type: formData.discount_type, + discount_value: parseFloat(formData.discount_value), + min_order_amount: parseFloat(formData.min_order_amount) || 0, + max_uses: formData.max_uses ? parseInt(formData.max_uses) : null, + valid_until: formData.valid_until ? new Date(formData.valid_until).toISOString() : null, + is_active: formData.is_active, + }; + + if (editingCode) { + const { error } = await supabase + .from("discount_codes") + .update(codeData) + .eq("id", editingCode.id); + + if (error) throw error; + toast({ title: "Success", description: "Discount code updated." }); + } else { + const { error } = await supabase.from("discount_codes").insert(codeData); + if (error) throw error; + toast({ title: "Success", description: "Discount code created." }); + } + + setDialogOpen(false); + resetForm(); + fetchCodes(); + } catch (error: any) { + console.error("Failed to save discount code:", error); + toast({ + title: "Error", + description: error.message || "Failed to save discount code.", + variant: "destructive", + }); + } finally { + setSaving(false); + } + }; + + const handleDelete = async (id: string) => { + if (!confirm("Are you sure you want to delete this discount code?")) return; + + try { + const { error } = await supabase.from("discount_codes").delete().eq("id", id); + if (error) throw error; + toast({ title: "Success", description: "Discount code deleted." }); + fetchCodes(); + } catch (error) { + console.error("Failed to delete discount code:", error); + toast({ + title: "Error", + description: "Failed to delete discount code.", + variant: "destructive", + }); + } + }; + + const copyCode = (code: string) => { + navigator.clipboard.writeText(code); + toast({ title: "Copied!", description: `Code "${code}" copied to clipboard.` }); + }; + + const toggleActive = async (code: DiscountCode) => { + try { + const { error } = await supabase + .from("discount_codes") + .update({ is_active: !code.is_active }) + .eq("id", code.id); + + if (error) throw error; + fetchCodes(); + } catch (error) { + console.error("Failed to toggle code:", error); + } + }; + + const isExpired = (validUntil: string | null) => { + if (!validUntil) return false; + return new Date(validUntil) < new Date(); + }; + + return ( + + + + + Discount Codes + +
+ + { setDialogOpen(open); if (!open) resetForm(); }}> + + + + + + {editingCode ? "Edit Discount Code" : "Create Discount Code"} + + {editingCode ? "Update discount code details." : "Create a new discount code for customers."} + + +
+
+ +
+ setFormData({ ...formData, code: e.target.value.toUpperCase() })} + placeholder="SAVE20" + className="font-mono" + /> + +
+
+
+ + setFormData({ ...formData, description: e.target.value })} + placeholder="Summer sale discount" + /> +
+
+
+ + +
+
+ + setFormData({ ...formData, discount_value: e.target.value })} + placeholder={formData.discount_type === "percentage" ? "20" : "10.00"} + /> +
+
+
+
+ + setFormData({ ...formData, min_order_amount: e.target.value })} + placeholder="0" + /> +
+
+ + setFormData({ ...formData, max_uses: e.target.value })} + placeholder="Unlimited" + /> +
+
+
+
+ + setFormData({ ...formData, valid_until: e.target.value })} + /> +
+
+ setFormData({ ...formData, is_active: checked })} + /> + +
+
+
+ + + + +
+
+
+
+ + {loading ? ( +
+ + Loading... +
+ ) : codes.length === 0 ? ( +
+ +

No discount codes yet

+
+ ) : ( + + + + Code + Discount + Uses + Expires + Status + Actions + + + + {codes.map((code) => ( + + +
+ {code.code} + +
+
+ + {code.discount_type === "percentage" + ? `${code.discount_value}%` + : `$${Number(code.discount_value).toFixed(2)}`} + + + {code.current_uses}{code.max_uses ? `/${code.max_uses}` : ""} + + + {code.valid_until + ? new Date(code.valid_until).toLocaleDateString() + : "Never"} + + + {isExpired(code.valid_until) ? ( + Expired + ) : ( + toggleActive(code)} + > + {code.is_active ? "Active" : "Inactive"} + + )} + + +
+ + +
+
+
+ ))} +
+
+ )} +
+
+ ); +}; + +export default DiscountCodesTab; diff --git a/src/components/admin/ProductsTab.tsx b/src/components/admin/ProductsTab.tsx new file mode 100644 index 0000000..747a8e6 --- /dev/null +++ b/src/components/admin/ProductsTab.tsx @@ -0,0 +1,378 @@ +import { useState, useEffect } from "react"; +import { supabase } from "@/integrations/supabase/client"; +import { useToast } from "@/hooks/use-toast"; +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 { Switch } from "@/components/ui/switch"; +import { Badge } from "@/components/ui/badge"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Plus, Pencil, Trash2, Package, RefreshCw } from "lucide-react"; + +interface Product { + id: string; + sku: string; + name: string; + description: string | null; + price: number; + currency: string; + download_url: string | null; + is_active: boolean; + stock_quantity: number | null; + created_at: string; +} + +const ProductsTab = () => { + const { toast } = useToast(); + const [products, setProducts] = useState([]); + const [loading, setLoading] = useState(true); + const [dialogOpen, setDialogOpen] = useState(false); + const [editingProduct, setEditingProduct] = useState(null); + const [saving, setSaving] = useState(false); + + const [formData, setFormData] = useState({ + sku: "", + name: "", + description: "", + price: "", + currency: "USD", + download_url: "", + is_active: true, + stock_quantity: "", + }); + + useEffect(() => { + fetchProducts(); + }, []); + + const fetchProducts = async () => { + setLoading(true); + try { + const { data, error } = await supabase + .from("products") + .select("*") + .order("created_at", { ascending: false }); + + if (error) throw error; + setProducts(data || []); + } catch (error) { + console.error("Failed to fetch products:", error); + toast({ + title: "Error", + description: "Failed to load products.", + variant: "destructive", + }); + } finally { + setLoading(false); + } + }; + + const resetForm = () => { + setFormData({ + sku: "", + name: "", + description: "", + price: "", + currency: "USD", + download_url: "", + is_active: true, + stock_quantity: "", + }); + setEditingProduct(null); + }; + + const openEditDialog = (product: Product) => { + setEditingProduct(product); + setFormData({ + sku: product.sku, + name: product.name, + description: product.description || "", + price: product.price.toString(), + currency: product.currency, + download_url: product.download_url || "", + is_active: product.is_active, + stock_quantity: product.stock_quantity?.toString() || "", + }); + setDialogOpen(true); + }; + + const handleSubmit = async () => { + if (!formData.sku || !formData.name || !formData.price) { + toast({ + title: "Validation Error", + description: "SKU, name, and price are required.", + variant: "destructive", + }); + return; + } + + setSaving(true); + try { + const productData = { + sku: formData.sku.trim(), + name: formData.name.trim(), + description: formData.description.trim() || null, + price: parseFloat(formData.price), + currency: formData.currency, + download_url: formData.download_url.trim() || null, + is_active: formData.is_active, + stock_quantity: formData.stock_quantity ? parseInt(formData.stock_quantity) : null, + }; + + if (editingProduct) { + const { error } = await supabase + .from("products") + .update(productData) + .eq("id", editingProduct.id); + + if (error) throw error; + toast({ title: "Success", description: "Product updated." }); + } else { + const { error } = await supabase.from("products").insert(productData); + if (error) throw error; + toast({ title: "Success", description: "Product created." }); + } + + setDialogOpen(false); + resetForm(); + fetchProducts(); + } catch (error: any) { + console.error("Failed to save product:", error); + toast({ + title: "Error", + description: error.message || "Failed to save product.", + variant: "destructive", + }); + } finally { + setSaving(false); + } + }; + + const handleDelete = async (id: string) => { + if (!confirm("Are you sure you want to delete this product?")) return; + + try { + const { error } = await supabase.from("products").delete().eq("id", id); + if (error) throw error; + toast({ title: "Success", description: "Product deleted." }); + fetchProducts(); + } catch (error) { + console.error("Failed to delete product:", error); + toast({ + title: "Error", + description: "Failed to delete product.", + variant: "destructive", + }); + } + }; + + const toggleActive = async (product: Product) => { + try { + const { error } = await supabase + .from("products") + .update({ is_active: !product.is_active }) + .eq("id", product.id); + + if (error) throw error; + fetchProducts(); + } catch (error) { + console.error("Failed to toggle product:", error); + } + }; + + return ( + + + + + Products + +
+ + { setDialogOpen(open); if (!open) resetForm(); }}> + + + + + + {editingProduct ? "Edit Product" : "Add Product"} + + {editingProduct ? "Update product details." : "Create a new product."} + + +
+
+
+ + setFormData({ ...formData, sku: e.target.value })} + placeholder="prod_001" + /> +
+
+ + setFormData({ ...formData, price: e.target.value })} + placeholder="49.99" + /> +
+
+
+ + setFormData({ ...formData, name: e.target.value })} + placeholder="Product Name" + /> +
+
+ +