Files
PromptTech/archive/techzone-source/frontend/src/pages/AdminDashboard.js

1068 lines
50 KiB
JavaScript
Raw Normal View History

2026-01-27 18:07:00 -06:00
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import axios from 'axios';
import {
LayoutDashboard, Package, Wrench, ShoppingCart, Users, BarChart3,
Settings, AlertTriangle, TrendingUp, DollarSign, Calendar, Download,
Plus, Edit, Trash2, Eye, ChevronRight, Search, Filter, RefreshCw,
FileText, FileSpreadsheet
} from 'lucide-react';
import { Button } from '../components/ui/button';
import { Input } from '../components/ui/input';
import { Badge } from '../components/ui/badge';
import { Separator } from '../components/ui/separator';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../components/ui/tabs';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '../components/ui/select';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
DialogFooter,
} from '../components/ui/dialog';
import { Label } from '../components/ui/label';
import { Textarea } from '../components/ui/textarea';
import { useAuth } from '../context/AuthContext';
import { toast } from 'sonner';
const API = `${process.env.REACT_APP_BACKEND_URL}/api`;
const statusColors = {
pending: 'bg-yellow-500',
processing: 'bg-blue-500',
layaway: 'bg-purple-500',
shipped: 'bg-cyan-500',
delivered: 'bg-green-500',
cancelled: 'bg-red-500',
refunded: 'bg-orange-500',
on_hold: 'bg-gray-500'
};
const AdminDashboard = () => {
const { user, token, isAuthenticated } = useAuth();
const navigate = useNavigate();
const [activeTab, setActiveTab] = useState('dashboard');
const [loading, setLoading] = useState(true);
// Dashboard data
const [dashboardData, setDashboardData] = useState(null);
// Products state
const [products, setProducts] = useState([]);
const [productDialog, setProductDialog] = useState(false);
const [editingProduct, setEditingProduct] = useState(null);
const [productForm, setProductForm] = useState({
name: '', description: '', price: '', category: '', image_url: '', stock: '', brand: ''
});
// Services state
const [services, setServices] = useState([]);
const [serviceDialog, setServiceDialog] = useState(false);
const [editingService, setEditingService] = useState(null);
const [serviceForm, setServiceForm] = useState({
name: '', description: '', price: '', duration: '', image_url: '', category: ''
});
// Orders state
const [orders, setOrders] = useState([]);
const [orderStatusFilter, setOrderStatusFilter] = useState('all');
const [selectedOrder, setSelectedOrder] = useState(null);
const [newStatus, setNewStatus] = useState('');
const [statusNotes, setStatusNotes] = useState('');
const [trackingNumber, setTrackingNumber] = useState('');
// Inventory state
const [inventory, setInventory] = useState([]);
const [adjustDialog, setAdjustDialog] = useState(false);
const [adjustProduct, setAdjustProduct] = useState(null);
const [adjustQty, setAdjustQty] = useState(0);
const [adjustNotes, setAdjustNotes] = useState('');
// Reports state
const [reportPeriod, setReportPeriod] = useState('monthly');
const [salesReport, setSalesReport] = useState(null);
// Bookings state
const [bookings, setBookings] = useState([]);
useEffect(() => {
if (isAuthenticated && user?.role === 'admin') {
fetchDashboardData();
}
}, [isAuthenticated, user, token]);
useEffect(() => {
if (activeTab === 'products') fetchProducts();
if (activeTab === 'services') fetchServices();
if (activeTab === 'orders') fetchOrders();
if (activeTab === 'inventory') fetchInventory();
if (activeTab === 'reports') fetchReports();
if (activeTab === 'bookings') fetchBookings();
}, [activeTab]);
const fetchDashboardData = async () => {
try {
const response = await axios.get(`${API}/admin/dashboard`, {
headers: { Authorization: `Bearer ${token}` }
});
setDashboardData(response.data);
} catch (error) {
console.error('Failed to fetch dashboard:', error);
toast.error('Failed to load dashboard data');
} finally {
setLoading(false);
}
};
const fetchProducts = async () => {
try {
const response = await axios.get(`${API}/admin/products?include_inactive=true`, {
headers: { Authorization: `Bearer ${token}` }
});
setProducts(response.data);
} catch (error) {
toast.error('Failed to load products');
}
};
const fetchServices = async () => {
try {
const response = await axios.get(`${API}/admin/services?include_inactive=true`, {
headers: { Authorization: `Bearer ${token}` }
});
setServices(response.data);
} catch (error) {
toast.error('Failed to load services');
}
};
const fetchOrders = async () => {
try {
const params = orderStatusFilter !== 'all' ? `?status=${orderStatusFilter}` : '';
const response = await axios.get(`${API}/admin/orders${params}`, {
headers: { Authorization: `Bearer ${token}` }
});
setOrders(response.data);
} catch (error) {
toast.error('Failed to load orders');
}
};
const fetchInventory = async () => {
try {
const response = await axios.get(`${API}/admin/inventory`, {
headers: { Authorization: `Bearer ${token}` }
});
setInventory(response.data);
} catch (error) {
toast.error('Failed to load inventory');
}
};
const fetchReports = async () => {
try {
const response = await axios.get(`${API}/admin/reports/sales?period=${reportPeriod}`, {
headers: { Authorization: `Bearer ${token}` }
});
setSalesReport(response.data);
} catch (error) {
toast.error('Failed to load reports');
}
};
const fetchBookings = async () => {
try {
const response = await axios.get(`${API}/admin/bookings`, {
headers: { Authorization: `Bearer ${token}` }
});
setBookings(response.data);
} catch (error) {
toast.error('Failed to load bookings');
}
};
// Product CRUD
const handleProductSubmit = async () => {
try {
const data = {
...productForm,
price: parseFloat(productForm.price),
stock: parseInt(productForm.stock) || 10
};
if (editingProduct) {
await axios.put(`${API}/admin/products/${editingProduct.id}`, data, {
headers: { Authorization: `Bearer ${token}` }
});
toast.success('Product updated');
} else {
await axios.post(`${API}/admin/products`, data, {
headers: { Authorization: `Bearer ${token}` }
});
toast.success('Product created');
}
setProductDialog(false);
setEditingProduct(null);
setProductForm({ name: '', description: '', price: '', category: '', image_url: '', stock: '', brand: '' });
fetchProducts();
} catch (error) {
toast.error('Failed to save product');
}
};
const handleDeleteProduct = async (id) => {
if (!window.confirm('Are you sure you want to delete this product?')) return;
try {
await axios.delete(`${API}/admin/products/${id}`, {
headers: { Authorization: `Bearer ${token}` }
});
toast.success('Product deleted');
fetchProducts();
} catch (error) {
toast.error('Failed to delete product');
}
};
// Service CRUD
const handleServiceSubmit = async () => {
try {
const data = {
...serviceForm,
price: parseFloat(serviceForm.price)
};
if (editingService) {
await axios.put(`${API}/admin/services/${editingService.id}`, data, {
headers: { Authorization: `Bearer ${token}` }
});
toast.success('Service updated');
} else {
await axios.post(`${API}/admin/services`, data, {
headers: { Authorization: `Bearer ${token}` }
});
toast.success('Service created');
}
setServiceDialog(false);
setEditingService(null);
setServiceForm({ name: '', description: '', price: '', duration: '', image_url: '', category: '' });
fetchServices();
} catch (error) {
toast.error('Failed to save service');
}
};
const handleDeleteService = async (id) => {
if (!window.confirm('Are you sure you want to delete this service?')) return;
try {
await axios.delete(`${API}/admin/services/${id}`, {
headers: { Authorization: `Bearer ${token}` }
});
toast.success('Service deleted');
fetchServices();
} catch (error) {
toast.error('Failed to delete service');
}
};
// Order status update
const handleUpdateOrderStatus = async () => {
if (!selectedOrder || !newStatus) return;
try {
await axios.put(`${API}/admin/orders/${selectedOrder}/status`, {
status: newStatus,
notes: statusNotes,
tracking_number: trackingNumber || null
}, {
headers: { Authorization: `Bearer ${token}` }
});
toast.success('Order status updated');
setSelectedOrder(null);
setNewStatus('');
setStatusNotes('');
setTrackingNumber('');
fetchOrders();
} catch (error) {
toast.error('Failed to update order');
}
};
// Inventory adjustment
const handleAdjustInventory = async () => {
if (!adjustProduct) return;
try {
await axios.post(`${API}/admin/inventory/${adjustProduct.id}/adjust`, {
quantity_change: adjustQty,
notes: adjustNotes
}, {
headers: { Authorization: `Bearer ${token}` }
});
toast.success('Inventory adjusted');
setAdjustDialog(false);
setAdjustProduct(null);
setAdjustQty(0);
setAdjustNotes('');
fetchInventory();
} catch (error) {
toast.error('Failed to adjust inventory');
}
};
// Export reports
const handleExportCSV = async (type) => {
try {
const response = await axios.get(`${API}/admin/reports/export/csv?report_type=${type}&period=${reportPeriod}`, {
headers: { Authorization: `Bearer ${token}` },
responseType: 'blob'
});
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', `${type}_report.csv`);
document.body.appendChild(link);
link.click();
link.remove();
toast.success('CSV exported');
} catch (error) {
toast.error('Failed to export CSV');
}
};
const handleExportPDF = async (type) => {
try {
const response = await axios.get(`${API}/admin/reports/export/pdf?report_type=${type}&period=${reportPeriod}`, {
headers: { Authorization: `Bearer ${token}` },
responseType: 'blob'
});
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', `${type}_report.pdf`);
document.body.appendChild(link);
link.click();
link.remove();
toast.success('PDF exported');
} catch (error) {
toast.error('Failed to export PDF');
}
};
if (!isAuthenticated || user?.role !== 'admin') {
return (
<div className="min-h-screen py-12 md:py-16">
<div className="max-w-4xl mx-auto px-4 md:px-8 text-center py-16">
<h2 className="text-2xl font-bold mb-4 font-['Outfit']">Access Denied</h2>
<p className="text-muted-foreground mb-8">You need admin privileges to access this page.</p>
<Button onClick={() => navigate('/login')} className="rounded-full">Sign In as Admin</Button>
</div>
</div>
);
}
if (loading) {
return (
<div className="min-h-screen py-12 flex items-center justify-center">
<div className="animate-spin w-8 h-8 border-2 border-primary border-t-transparent rounded-full" />
</div>
);
}
return (
<div className="min-h-screen bg-muted/30">
<div className="max-w-7xl mx-auto px-4 md:px-8 py-8">
<div className="flex items-center justify-between mb-8">
<div>
<h1 className="text-3xl font-bold font-['Outfit']">Admin Dashboard</h1>
<p className="text-muted-foreground">Manage your store</p>
</div>
<Button onClick={fetchDashboardData} variant="outline" className="gap-2">
<RefreshCw className="h-4 w-4" />
Refresh
</Button>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid grid-cols-3 md:grid-cols-7 gap-2 mb-8 h-auto p-1">
<TabsTrigger value="dashboard" className="gap-2"><LayoutDashboard className="h-4 w-4" /><span className="hidden md:inline">Dashboard</span></TabsTrigger>
<TabsTrigger value="products" className="gap-2"><Package className="h-4 w-4" /><span className="hidden md:inline">Products</span></TabsTrigger>
<TabsTrigger value="services" className="gap-2"><Wrench className="h-4 w-4" /><span className="hidden md:inline">Services</span></TabsTrigger>
<TabsTrigger value="orders" className="gap-2"><ShoppingCart className="h-4 w-4" /><span className="hidden md:inline">Orders</span></TabsTrigger>
<TabsTrigger value="inventory" className="gap-2"><Package className="h-4 w-4" /><span className="hidden md:inline">Inventory</span></TabsTrigger>
<TabsTrigger value="bookings" className="gap-2"><Calendar className="h-4 w-4" /><span className="hidden md:inline">Bookings</span></TabsTrigger>
<TabsTrigger value="reports" className="gap-2"><BarChart3 className="h-4 w-4" /><span className="hidden md:inline">Reports</span></TabsTrigger>
</TabsList>
{/* Dashboard Tab */}
<TabsContent value="dashboard">
{dashboardData && (
<div className="space-y-6">
{/* Stats Grid */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="bg-card border border-border rounded-xl p-6">
<div className="flex items-center gap-3 mb-2">
<div className="w-10 h-10 rounded-full bg-blue-500/10 flex items-center justify-center">
<DollarSign className="h-5 w-5 text-blue-500" />
</div>
<span className="text-sm text-muted-foreground">Total Revenue</span>
</div>
<p className="text-2xl font-bold font-['Outfit']">${dashboardData.stats.total_revenue.toFixed(2)}</p>
</div>
<div className="bg-card border border-border rounded-xl p-6">
<div className="flex items-center gap-3 mb-2">
<div className="w-10 h-10 rounded-full bg-green-500/10 flex items-center justify-center">
<ShoppingCart className="h-5 w-5 text-green-500" />
</div>
<span className="text-sm text-muted-foreground">Total Orders</span>
</div>
<p className="text-2xl font-bold font-['Outfit']">{dashboardData.stats.total_orders}</p>
</div>
<div className="bg-card border border-border rounded-xl p-6">
<div className="flex items-center gap-3 mb-2">
<div className="w-10 h-10 rounded-full bg-purple-500/10 flex items-center justify-center">
<Package className="h-5 w-5 text-purple-500" />
</div>
<span className="text-sm text-muted-foreground">Products</span>
</div>
<p className="text-2xl font-bold font-['Outfit']">{dashboardData.stats.total_products}</p>
</div>
<div className="bg-card border border-border rounded-xl p-6">
<div className="flex items-center gap-3 mb-2">
<div className="w-10 h-10 rounded-full bg-orange-500/10 flex items-center justify-center">
<Users className="h-5 w-5 text-orange-500" />
</div>
<span className="text-sm text-muted-foreground">Users</span>
</div>
<p className="text-2xl font-bold font-['Outfit']">{dashboardData.stats.total_users}</p>
</div>
</div>
{/* Today's Stats */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-card border border-border rounded-xl p-6">
<h3 className="font-semibold mb-4 font-['Outfit']">Today's Performance</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-muted-foreground">Orders</p>
<p className="text-xl font-bold">{dashboardData.stats.today_orders}</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Revenue</p>
<p className="text-xl font-bold">${(dashboardData.stats.today_revenue || 0).toFixed(2)}</p>
</div>
</div>
</div>
<div className="bg-card border border-border rounded-xl p-6">
<h3 className="font-semibold mb-4 font-['Outfit']">Monthly Stats</h3>
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-muted-foreground">Revenue</p>
<p className="text-xl font-bold">${(dashboardData.stats.monthly_revenue || 0).toFixed(2)}</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Pending Bookings</p>
<p className="text-xl font-bold">{dashboardData.stats.pending_bookings}</p>
</div>
</div>
</div>
</div>
{/* Low Stock Alert */}
{dashboardData.low_stock_products.length > 0 && (
<div className="bg-card border border-orange-500/50 rounded-xl p-6">
<div className="flex items-center gap-2 mb-4">
<AlertTriangle className="h-5 w-5 text-orange-500" />
<h3 className="font-semibold font-['Outfit']">Low Stock Alert</h3>
</div>
<div className="space-y-2">
{dashboardData.low_stock_products.map((product) => (
<div key={product.id} className="flex items-center justify-between p-3 bg-muted rounded-lg">
<span>{product.name}</span>
<Badge variant="destructive">{product.stock} left</Badge>
</div>
))}
</div>
</div>
)}
{/* Recent Orders */}
<div className="bg-card border border-border rounded-xl p-6">
<h3 className="font-semibold mb-4 font-['Outfit']">Recent Orders</h3>
<div className="space-y-3">
{dashboardData.recent_orders.slice(0, 5).map((order) => (
<div key={order.id} className="flex items-center justify-between p-3 bg-muted/50 rounded-lg">
<div>
<p className="font-medium font-mono text-sm">{order.id.slice(0, 8)}...</p>
<p className="text-xs text-muted-foreground">{new Date(order.created_at).toLocaleString()}</p>
</div>
<div className="flex items-center gap-3">
<span className="font-semibold">${order.total.toFixed(2)}</span>
<Badge className={statusColors[order.status]}>{order.status}</Badge>
</div>
</div>
))}
</div>
</div>
</div>
)}
</TabsContent>
{/* Products Tab */}
<TabsContent value="products">
<div className="bg-card border border-border rounded-xl p-6">
<div className="flex items-center justify-between mb-6">
<h3 className="font-semibold font-['Outfit']">Products Management</h3>
<Dialog open={productDialog} onOpenChange={setProductDialog}>
<DialogTrigger asChild>
<Button className="gap-2" onClick={() => { setEditingProduct(null); setProductForm({ name: '', description: '', price: '', category: '', image_url: '', stock: '', brand: '' }); }}>
<Plus className="h-4 w-4" />
Add Product
</Button>
</DialogTrigger>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>{editingProduct ? 'Edit Product' : 'Add Product'}</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label>Name</Label>
<Input value={productForm.name} onChange={(e) => setProductForm({...productForm, name: e.target.value})} />
</div>
<div className="space-y-2">
<Label>Description</Label>
<Textarea value={productForm.description} onChange={(e) => setProductForm({...productForm, description: e.target.value})} />
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Price</Label>
<Input type="number" value={productForm.price} onChange={(e) => setProductForm({...productForm, price: e.target.value})} />
</div>
<div className="space-y-2">
<Label>Stock</Label>
<Input type="number" value={productForm.stock} onChange={(e) => setProductForm({...productForm, stock: e.target.value})} />
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Category</Label>
<Select value={productForm.category} onValueChange={(v) => setProductForm({...productForm, category: v})}>
<SelectTrigger><SelectValue placeholder="Select" /></SelectTrigger>
<SelectContent>
<SelectItem value="phones">Phones</SelectItem>
<SelectItem value="laptops">Laptops</SelectItem>
<SelectItem value="tablets">Tablets</SelectItem>
<SelectItem value="wearables">Wearables</SelectItem>
<SelectItem value="accessories">Accessories</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Brand</Label>
<Input value={productForm.brand} onChange={(e) => setProductForm({...productForm, brand: e.target.value})} />
</div>
</div>
<div className="space-y-2">
<Label>Image URL</Label>
<Input value={productForm.image_url} onChange={(e) => setProductForm({...productForm, image_url: e.target.value})} />
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setProductDialog(false)}>Cancel</Button>
<Button onClick={handleProductSubmit}>{editingProduct ? 'Update' : 'Create'}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-border">
<th className="text-left py-3 px-2">Product</th>
<th className="text-left py-3 px-2">Category</th>
<th className="text-right py-3 px-2">Price</th>
<th className="text-right py-3 px-2">Stock</th>
<th className="text-center py-3 px-2">Status</th>
<th className="text-right py-3 px-2">Actions</th>
</tr>
</thead>
<tbody>
{products.map((product) => (
<tr key={product.id} className="border-b border-border/50">
<td className="py-3 px-2">
<div className="flex items-center gap-3">
<img src={product.image_url} alt={product.name} className="w-10 h-10 rounded object-cover" />
<span className="font-medium">{product.name}</span>
</div>
</td>
<td className="py-3 px-2 capitalize">{product.category}</td>
<td className="py-3 px-2 text-right">${product.price.toFixed(2)}</td>
<td className="py-3 px-2 text-right">{product.stock}</td>
<td className="py-3 px-2 text-center">
<Badge variant={product.is_active ? 'default' : 'secondary'}>
{product.is_active ? 'Active' : 'Inactive'}
</Badge>
</td>
<td className="py-3 px-2 text-right">
<div className="flex justify-end gap-2">
<Button size="icon" variant="ghost" onClick={() => {
setEditingProduct(product);
setProductForm({
name: product.name,
description: product.description,
price: product.price.toString(),
category: product.category,
image_url: product.image_url,
stock: product.stock.toString(),
brand: product.brand
});
setProductDialog(true);
}}>
<Edit className="h-4 w-4" />
</Button>
<Button size="icon" variant="ghost" className="text-destructive" onClick={() => handleDeleteProduct(product.id)}>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</TabsContent>
{/* Services Tab */}
<TabsContent value="services">
<div className="bg-card border border-border rounded-xl p-6">
<div className="flex items-center justify-between mb-6">
<h3 className="font-semibold font-['Outfit']">Services Management</h3>
<Dialog open={serviceDialog} onOpenChange={setServiceDialog}>
<DialogTrigger asChild>
<Button className="gap-2" onClick={() => { setEditingService(null); setServiceForm({ name: '', description: '', price: '', duration: '', image_url: '', category: '' }); }}>
<Plus className="h-4 w-4" />
Add Service
</Button>
</DialogTrigger>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>{editingService ? 'Edit Service' : 'Add Service'}</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label>Name</Label>
<Input value={serviceForm.name} onChange={(e) => setServiceForm({...serviceForm, name: e.target.value})} />
</div>
<div className="space-y-2">
<Label>Description</Label>
<Textarea value={serviceForm.description} onChange={(e) => setServiceForm({...serviceForm, description: e.target.value})} />
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Price</Label>
<Input type="number" value={serviceForm.price} onChange={(e) => setServiceForm({...serviceForm, price: e.target.value})} />
</div>
<div className="space-y-2">
<Label>Duration</Label>
<Input value={serviceForm.duration} onChange={(e) => setServiceForm({...serviceForm, duration: e.target.value})} placeholder="e.g. 1-2 hours" />
</div>
</div>
<div className="space-y-2">
<Label>Category</Label>
<Select value={serviceForm.category} onValueChange={(v) => setServiceForm({...serviceForm, category: v})}>
<SelectTrigger><SelectValue placeholder="Select" /></SelectTrigger>
<SelectContent>
<SelectItem value="repair">Repair</SelectItem>
<SelectItem value="data">Data Recovery</SelectItem>
<SelectItem value="software">Software</SelectItem>
<SelectItem value="upgrade">Upgrade</SelectItem>
<SelectItem value="setup">Setup</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Image URL</Label>
<Input value={serviceForm.image_url} onChange={(e) => setServiceForm({...serviceForm, image_url: e.target.value})} />
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setServiceDialog(false)}>Cancel</Button>
<Button onClick={handleServiceSubmit}>{editingService ? 'Update' : 'Create'}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-border">
<th className="text-left py-3 px-2">Service</th>
<th className="text-left py-3 px-2">Category</th>
<th className="text-left py-3 px-2">Duration</th>
<th className="text-right py-3 px-2">Price</th>
<th className="text-center py-3 px-2">Status</th>
<th className="text-right py-3 px-2">Actions</th>
</tr>
</thead>
<tbody>
{services.map((service) => (
<tr key={service.id} className="border-b border-border/50">
<td className="py-3 px-2">
<div className="flex items-center gap-3">
<img src={service.image_url} alt={service.name} className="w-10 h-10 rounded object-cover" />
<span className="font-medium">{service.name}</span>
</div>
</td>
<td className="py-3 px-2 capitalize">{service.category}</td>
<td className="py-3 px-2">{service.duration}</td>
<td className="py-3 px-2 text-right">${service.price.toFixed(2)}</td>
<td className="py-3 px-2 text-center">
<Badge variant={service.is_active ? 'default' : 'secondary'}>
{service.is_active ? 'Active' : 'Inactive'}
</Badge>
</td>
<td className="py-3 px-2 text-right">
<div className="flex justify-end gap-2">
<Button size="icon" variant="ghost" onClick={() => {
setEditingService(service);
setServiceForm({
name: service.name,
description: service.description,
price: service.price.toString(),
duration: service.duration,
image_url: service.image_url,
category: service.category
});
setServiceDialog(true);
}}>
<Edit className="h-4 w-4" />
</Button>
<Button size="icon" variant="ghost" className="text-destructive" onClick={() => handleDeleteService(service.id)}>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</TabsContent>
{/* Orders Tab */}
<TabsContent value="orders">
<div className="bg-card border border-border rounded-xl p-6">
<div className="flex items-center justify-between mb-6">
<h3 className="font-semibold font-['Outfit']">Orders Management</h3>
<Select value={orderStatusFilter} onValueChange={(v) => { setOrderStatusFilter(v); setTimeout(fetchOrders, 100); }}>
<SelectTrigger className="w-[180px]"><SelectValue placeholder="Filter by status" /></SelectTrigger>
<SelectContent>
<SelectItem value="all">All Orders</SelectItem>
<SelectItem value="pending">Pending</SelectItem>
<SelectItem value="processing">Processing</SelectItem>
<SelectItem value="layaway">Layaway</SelectItem>
<SelectItem value="shipped">Shipped</SelectItem>
<SelectItem value="delivered">Delivered</SelectItem>
<SelectItem value="cancelled">Cancelled</SelectItem>
<SelectItem value="refunded">Refunded</SelectItem>
<SelectItem value="on_hold">On Hold</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-4">
{orders.map((order) => (
<div key={order.id} className="border border-border rounded-lg p-4">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-4">
<div className="flex flex-wrap gap-4">
<div>
<p className="text-xs text-muted-foreground">Order ID</p>
<p className="font-mono text-sm">{order.id.slice(0, 8)}...</p>
</div>
<div>
<p className="text-xs text-muted-foreground">Customer</p>
<p className="text-sm">{order.user_name}</p>
</div>
<div>
<p className="text-xs text-muted-foreground">Date</p>
<p className="text-sm">{new Date(order.created_at).toLocaleDateString()}</p>
</div>
<div>
<p className="text-xs text-muted-foreground">Total</p>
<p className="font-semibold">${order.total.toFixed(2)}</p>
</div>
</div>
<div className="flex items-center gap-2">
<Badge className={statusColors[order.status]}>{order.status}</Badge>
<Dialog>
<DialogTrigger asChild>
<Button size="sm" variant="outline" onClick={() => {
setSelectedOrder(order.id);
setNewStatus(order.status);
setTrackingNumber(order.tracking_number || '');
}}>
Update Status
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Update Order Status</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label>New Status</Label>
<Select value={newStatus} onValueChange={setNewStatus}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="pending">Pending</SelectItem>
<SelectItem value="processing">Processing</SelectItem>
<SelectItem value="layaway">Layaway</SelectItem>
<SelectItem value="shipped">Shipped</SelectItem>
<SelectItem value="delivered">Delivered</SelectItem>
<SelectItem value="cancelled">Cancelled</SelectItem>
<SelectItem value="refunded">Refunded</SelectItem>
<SelectItem value="on_hold">On Hold</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Tracking Number (optional)</Label>
<Input value={trackingNumber} onChange={(e) => setTrackingNumber(e.target.value)} />
</div>
<div className="space-y-2">
<Label>Notes</Label>
<Textarea value={statusNotes} onChange={(e) => setStatusNotes(e.target.value)} />
</div>
</div>
<DialogFooter>
<Button onClick={handleUpdateOrderStatus}>Update</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</div>
{/* Order Items */}
<div className="text-sm text-muted-foreground">
{order.items.map((item) => (
<span key={item.id} className="mr-4">{item.product_name} x{item.quantity}</span>
))}
</div>
</div>
))}
</div>
</div>
</TabsContent>
{/* Inventory Tab */}
<TabsContent value="inventory">
<div className="bg-card border border-border rounded-xl p-6">
<div className="flex items-center justify-between mb-6">
<h3 className="font-semibold font-['Outfit']">Inventory Management</h3>
<div className="flex gap-2">
<Button variant="outline" className="gap-2" onClick={() => handleExportCSV('inventory')}>
<FileSpreadsheet className="h-4 w-4" />
Export CSV
</Button>
<Button variant="outline" className="gap-2" onClick={() => handleExportPDF('inventory')}>
<FileText className="h-4 w-4" />
Export PDF
</Button>
</div>
</div>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-border">
<th className="text-left py-3 px-2">Product</th>
<th className="text-left py-3 px-2">Category</th>
<th className="text-right py-3 px-2">Stock</th>
<th className="text-right py-3 px-2">Threshold</th>
<th className="text-center py-3 px-2">Status</th>
<th className="text-right py-3 px-2">Actions</th>
</tr>
</thead>
<tbody>
{inventory.map((product) => (
<tr key={product.id} className="border-b border-border/50">
<td className="py-3 px-2 font-medium">{product.name}</td>
<td className="py-3 px-2 capitalize">{product.category}</td>
<td className="py-3 px-2 text-right">{product.stock}</td>
<td className="py-3 px-2 text-right">{product.low_stock_threshold}</td>
<td className="py-3 px-2 text-center">
<Badge variant={product.is_low_stock ? 'destructive' : 'default'}>
{product.is_low_stock ? 'Low Stock' : 'In Stock'}
</Badge>
</td>
<td className="py-3 px-2 text-right">
<Dialog open={adjustDialog && adjustProduct?.id === product.id} onOpenChange={(open) => { setAdjustDialog(open); if (!open) setAdjustProduct(null); }}>
<DialogTrigger asChild>
<Button size="sm" variant="outline" onClick={() => { setAdjustProduct(product); setAdjustDialog(true); }}>
Adjust
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Adjust Inventory</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-4">
<p className="text-sm">Current stock: <strong>{adjustProduct?.stock}</strong></p>
<div className="space-y-2">
<Label>Quantity Change (+/-)</Label>
<Input type="number" value={adjustQty} onChange={(e) => setAdjustQty(parseInt(e.target.value) || 0)} />
</div>
<div className="space-y-2">
<Label>Notes</Label>
<Textarea value={adjustNotes} onChange={(e) => setAdjustNotes(e.target.value)} placeholder="Reason for adjustment..." />
</div>
</div>
<DialogFooter>
<Button onClick={handleAdjustInventory}>Apply</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</TabsContent>
{/* Bookings Tab */}
<TabsContent value="bookings">
<div className="bg-card border border-border rounded-xl p-6">
<h3 className="font-semibold font-['Outfit'] mb-6">Service Bookings</h3>
<div className="space-y-4">
{bookings.map((booking) => (
<div key={booking.id} className="border border-border rounded-lg p-4">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div className="flex flex-wrap gap-4">
<div>
<p className="text-xs text-muted-foreground">Service</p>
<p className="font-medium">{booking.service_name}</p>
</div>
<div>
<p className="text-xs text-muted-foreground">Customer</p>
<p className="text-sm">{booking.name}</p>
</div>
<div>
<p className="text-xs text-muted-foreground">Email</p>
<p className="text-sm">{booking.email}</p>
</div>
<div>
<p className="text-xs text-muted-foreground">Phone</p>
<p className="text-sm">{booking.phone}</p>
</div>
<div>
<p className="text-xs text-muted-foreground">Preferred Date</p>
<p className="text-sm">{booking.preferred_date}</p>
</div>
</div>
<Badge>{booking.status}</Badge>
</div>
{booking.notes && <p className="text-sm text-muted-foreground mt-2">Notes: {booking.notes}</p>}
</div>
))}
</div>
</div>
</TabsContent>
{/* Reports Tab */}
<TabsContent value="reports">
<div className="space-y-6">
<div className="bg-card border border-border rounded-xl p-6">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-6">
<h3 className="font-semibold font-['Outfit']">Sales Reports</h3>
<div className="flex gap-2">
<Select value={reportPeriod} onValueChange={(v) => { setReportPeriod(v); setTimeout(fetchReports, 100); }}>
<SelectTrigger className="w-[150px]"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="daily">Daily</SelectItem>
<SelectItem value="weekly">Weekly</SelectItem>
<SelectItem value="monthly">Monthly</SelectItem>
</SelectContent>
</Select>
<Button variant="outline" className="gap-2" onClick={() => handleExportCSV('sales')}>
<FileSpreadsheet className="h-4 w-4" />
CSV
</Button>
<Button variant="outline" className="gap-2" onClick={() => handleExportPDF('sales')}>
<FileText className="h-4 w-4" />
PDF
</Button>
</div>
</div>
{salesReport && (
<>
{/* Summary */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<div className="bg-muted rounded-lg p-4">
<p className="text-sm text-muted-foreground">Total Orders</p>
<p className="text-2xl font-bold">{salesReport.summary.total_orders}</p>
</div>
<div className="bg-muted rounded-lg p-4">
<p className="text-sm text-muted-foreground">Total Revenue</p>
<p className="text-2xl font-bold">${salesReport.summary.total_revenue.toFixed(2)}</p>
</div>
<div className="bg-muted rounded-lg p-4">
<p className="text-sm text-muted-foreground">Products Sold</p>
<p className="text-2xl font-bold">{salesReport.summary.total_products_sold}</p>
</div>
<div className="bg-muted rounded-lg p-4">
<p className="text-sm text-muted-foreground">Avg Order Value</p>
<p className="text-2xl font-bold">${salesReport.summary.average_order_value.toFixed(2)}</p>
</div>
</div>
{/* Data Table */}
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-border">
<th className="text-left py-3 px-2">Period</th>
<th className="text-right py-3 px-2">Orders</th>
<th className="text-right py-3 px-2">Revenue</th>
<th className="text-right py-3 px-2">Products Sold</th>
<th className="text-right py-3 px-2">Services Booked</th>
</tr>
</thead>
<tbody>
{salesReport.data.map((row, idx) => (
<tr key={idx} className="border-b border-border/50">
<td className="py-3 px-2">{row.period}</td>
<td className="py-3 px-2 text-right">{row.orders}</td>
<td className="py-3 px-2 text-right">${row.revenue.toFixed(2)}</td>
<td className="py-3 px-2 text-right">{row.products_sold}</td>
<td className="py-3 px-2 text-right">{row.services_booked || 0}</td>
</tr>
))}
</tbody>
</table>
</div>
</>
)}
</div>
</div>
</TabsContent>
</Tabs>
</div>
</div>
);
};
export default AdminDashboard;