1068 lines
50 KiB
JavaScript
1068 lines
50 KiB
JavaScript
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;
|