Enhance admin dashboard UI and tables with new styles
All checks were successful
Build Frontend / build (push) Successful in 1m4s

Refactors admin dashboard, users, vendors, shipping, and stock pages to improve UI consistency and visual clarity. Adds new icons, animated transitions, and card styles for stats and tables. Updates table row rendering with framer-motion for smooth animations, improves badge and button styling, and enhances search/filter inputs. Refines loading skeletons and overall layout for a more modern, accessible admin experience.
This commit is contained in:
g
2026-01-12 07:16:33 +00:00
parent 63c833b510
commit 73adbe5d07
6 changed files with 757 additions and 611 deletions

View File

@@ -7,18 +7,22 @@ import { Button } from "@/components/ui/button";
import { Table, TableHeader, TableRow, TableHead, TableBody, TableCell } from "@/components/ui/table";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenuLabel,
DropdownMenuSeparator
} from "@/components/ui/dropdown-menu";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
@@ -30,12 +34,13 @@ import {
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Product } from "@/models/products";
import { Package, RefreshCw, ChevronDown, CheckSquare, XSquare, Boxes, Download, Calendar } from "lucide-react";
import { Package, RefreshCw, ChevronDown, CheckSquare, XSquare, Boxes, Download, Calendar, Search, Filter, Save, X, Edit2 } from "lucide-react";
import { clientFetch } from "@/lib/api";
import { toast } from "sonner";
import { DatePicker, DateRangePicker, DateRangeDisplay, MonthPicker } from "@/components/ui/date-picker";
import { DateRange } from "react-day-picker";
import { addDays, startOfDay, endOfDay, format, isSameDay } from "date-fns";
import { motion, AnimatePresence } from "framer-motion";
interface StockData {
currentStock: number;
@@ -55,7 +60,7 @@ export default function StockManagementPage() {
const [selectedProducts, setSelectedProducts] = useState<string[]>([]);
const [isConfirmDialogOpen, setIsConfirmDialogOpen] = useState(false);
const [bulkAction, setBulkAction] = useState<'enable' | 'disable' | null>(null);
// Export state
const [exportDate, setExportDate] = useState<string>(new Date().toISOString().split('T')[0]);
const [exportDateRange, setExportDateRange] = useState<DateRange | undefined>({
@@ -82,7 +87,7 @@ export default function StockManagementPage() {
const response = await clientFetch<Product[]>('api/products');
const fetchedProducts = response || [];
setProducts(fetchedProducts);
// Initialize stock values
const initialStockValues: Record<string, number> = {};
fetchedProducts.forEach((product: Product) => {
@@ -91,7 +96,7 @@ export default function StockManagementPage() {
}
});
setStockValues(initialStockValues);
setLoading(false);
} catch (error) {
console.error("Error fetching products:", error);
@@ -114,7 +119,7 @@ export default function StockManagementPage() {
try {
const newStockValue = stockValues[product._id] || 0;
const stockData: StockData = {
currentStock: newStockValue,
stockTracking: product.stockTracking || false,
@@ -165,14 +170,14 @@ export default function StockManagementPage() {
try {
// Toggle the stock tracking status
const newTrackingStatus = !product.stockTracking;
// For enabling tracking, we need to ensure there's a stock value
const stockData: StockData = {
stockTracking: newTrackingStatus,
currentStock: product.currentStock || 0,
lowStockThreshold: product.lowStockThreshold || 10,
};
// Update stock tracking status
await clientFetch(`api/stock/${product._id}`, {
method: 'PUT',
@@ -212,7 +217,7 @@ export default function StockManagementPage() {
try {
const productsToUpdate = products.filter(p => selectedProducts.includes(p._id || ''));
await Promise.all(productsToUpdate.map(async (product) => {
if (!product._id) return;
@@ -254,7 +259,7 @@ export default function StockManagementPage() {
};
const toggleSelectProduct = (productId: string) => {
setSelectedProducts(prev =>
setSelectedProducts(prev =>
prev.includes(productId)
? prev.filter(id => id !== productId)
: [...prev, productId]
@@ -262,7 +267,7 @@ export default function StockManagementPage() {
};
const toggleSelectAll = () => {
setSelectedProducts(prev =>
setSelectedProducts(prev =>
prev.length === products.length
? []
: products.map(p => p._id || '')
@@ -280,7 +285,7 @@ export default function StockManagementPage() {
response = await clientFetch(`/api/analytics/daily-stock-report?date=${exportDate}`);
filename = `daily-stock-report-${exportDate}.csv`;
break;
case 'weekly':
if (!exportDateRange?.from) {
toast.error('Please select a date range for weekly report');
@@ -290,14 +295,14 @@ export default function StockManagementPage() {
response = await clientFetch(`/api/analytics/weekly-stock-report?weekStart=${weekStart}`);
filename = `weekly-stock-report-${weekStart}.csv`;
break;
case 'monthly':
const year = selectedMonth.getFullYear();
const month = selectedMonth.getMonth() + 1;
response = await clientFetch(`/api/analytics/monthly-stock-report?year=${year}&month=${month}`);
filename = `monthly-stock-report-${year}-${month.toString().padStart(2, '0')}.csv`;
break;
case 'custom':
if (!exportDateRange?.from || !exportDateRange?.to) {
toast.error('Please select a date range for custom report');
@@ -308,12 +313,12 @@ export default function StockManagementPage() {
response = await clientFetch(`/api/analytics/daily-stock-report?startDate=${startDate}&endDate=${endDate}`);
filename = `custom-stock-report-${startDate}-to-${endDate}.csv`;
break;
default:
toast.error('Invalid report type');
return;
}
if (!response || !response.products) {
throw new Error('No data received from server');
}
@@ -348,19 +353,19 @@ export default function StockManagementPage() {
const blob = new Blob([csvData], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', filename);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
const periodText = reportType === 'daily' ? exportDate :
reportType === 'weekly' ? `week starting ${format(exportDateRange?.from || new Date(), 'MMM dd')}` :
reportType === 'monthly' ? `${response.monthName || 'current month'}` :
`${format(exportDateRange?.from || new Date(), 'MMM dd')} to ${format(exportDateRange?.to || new Date(), 'MMM dd')}`;
reportType === 'weekly' ? `week starting ${format(exportDateRange?.from || new Date(), 'MMM dd')}` :
reportType === 'monthly' ? `${response.monthName || 'current month'}` :
`${format(exportDateRange?.from || new Date(), 'MMM dd')} to ${format(exportDateRange?.to || new Date(), 'MMM dd')}`;
toast.success(`${reportType.charAt(0).toUpperCase() + reportType.slice(1)} stock report for ${periodText} exported successfully`);
} catch (error) {
@@ -379,9 +384,29 @@ export default function StockManagementPage() {
return 'In stock';
};
const getStatusBadgeVariant = (status: string) => {
switch (status) {
case 'Out of stock': return 'destructive';
case 'Low stock': return 'warning'; // Custom variant or use secondary/outline
case 'In stock': return 'default'; // often maps to primary which might be blue/black
default: return 'secondary';
}
};
// Helper for badging - if your Badge component doesn't support 'warning' directly, use className overrides
const StatusBadge = ({ status }: { status: string }) => {
let styles = "font-medium border-transparent shadow-none";
if (status === 'Out of stock') styles += " bg-red-100 text-red-700 dark:bg-red-500/20 dark:text-red-400";
else if (status === 'Low stock') styles += " bg-amber-100 text-amber-700 dark:bg-amber-500/20 dark:text-amber-400";
else if (status === 'In stock') styles += " bg-green-100 text-green-700 dark:bg-green-500/20 dark:text-green-400";
else styles += " bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-400";
return <Badge className={styles} variant="outline">{status}</Badge>;
};
const filteredProducts = products.filter(product => {
if (!searchTerm) return true;
const searchLower = searchTerm.toLowerCase();
return (
product.name.toLowerCase().includes(searchLower) ||
@@ -392,31 +417,39 @@ export default function StockManagementPage() {
return (
<Layout>
<div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-semibold text-gray-900 dark:text-white flex items-center">
<Boxes className="mr-2 h-6 w-6" />
Stock Management
</h1>
<div className="flex items-center gap-3">
<Input
type="search"
placeholder="Search products..."
className="w-64"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
{/* Report Type Selector */}
<div className="space-y-6 animate-in fade-in duration-500">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h1 className="text-2xl font-semibold text-foreground flex items-center gap-2">
<Boxes className="h-6 w-6 text-primary" />
Stock Management
</h1>
<p className="text-muted-foreground text-sm mt-1">
Track inventory levels and manage stock status
</p>
</div>
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-2">
<div className="relative">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
type="search"
placeholder="Search products..."
className="pl-9 w-full sm:w-64 bg-background/50 border-border/50 focus:bg-background transition-colors"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="gap-2">
<Calendar className="h-4 w-4" />
{reportType.charAt(0).toUpperCase() + reportType.slice(1)} Report
<ChevronDown className="h-4 w-4" />
<Button variant="outline" size="icon" className="h-10 w-10 border-border/50 bg-background/50">
<Filter className="h-4 w-4 text-muted-foreground" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Filter Reports</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => setReportType('daily')}>
Daily Report
</DropdownMenuItem>
@@ -433,51 +466,53 @@ export default function StockManagementPage() {
</DropdownMenu>
{/* Date Selection based on report type */}
{reportType === 'daily' && (
<DatePicker
date={exportDate ? new Date(exportDate) : undefined}
onDateChange={(date) => setExportDate(date ? date.toISOString().split('T')[0] : '')}
placeholder="Select export date"
className="w-auto"
/>
)}
<div className="hidden sm:block">
{reportType === 'daily' && (
<DatePicker
date={exportDate ? new Date(exportDate) : undefined}
onDateChange={(date) => setExportDate(date ? date.toISOString().split('T')[0] : '')}
placeholder="Select export date"
className="w-auto border-border/50 bg-background/50"
/>
)}
{(reportType === 'weekly' || reportType === 'custom') && (
<DateRangePicker
dateRange={exportDateRange}
onDateRangeChange={setExportDateRange}
placeholder="Select date range"
className="w-auto"
/>
)}
{(reportType === 'weekly' || reportType === 'custom') && (
<DateRangePicker
dateRange={exportDateRange}
onDateRangeChange={setExportDateRange}
placeholder="Select date range"
className="w-auto border-border/50 bg-background/50"
/>
)}
{reportType === 'monthly' && (
<MonthPicker
selectedMonth={selectedMonth}
onMonthChange={(date) => setSelectedMonth(date || new Date())}
placeholder="Select month"
className="w-auto"
/>
)}
{reportType === 'monthly' && (
<MonthPicker
selectedMonth={selectedMonth}
onMonthChange={(date) => setSelectedMonth(date || new Date())}
placeholder="Select month"
className="w-auto border-border/50 bg-background/50"
/>
)}
</div>
<Button
variant="outline"
onClick={handleExportStock}
disabled={isExporting}
className="gap-2"
className="gap-2 border-border/50 bg-background/50 hover:bg-background transition-colors"
>
{isExporting ? (
<RefreshCw className="h-4 w-4 animate-spin" />
) : (
<Download className="h-4 w-4" />
)}
{isExporting ? 'Exporting...' : 'Export CSV'}
Export
</Button>
{selectedProducts.length > 0 && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="gap-2">
<Button variant="default" className="gap-2">
<Package className="h-4 w-4" />
Bulk Actions
<ChevronDown className="h-4 w-4" />
@@ -486,11 +521,11 @@ export default function StockManagementPage() {
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => handleBulkAction('enable')}>
<CheckSquare className="h-4 w-4 mr-2" />
Enable Stock Tracking
Enable Tracking
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleBulkAction('disable')}>
<XSquare className="h-4 w-4 mr-2" />
Disable Stock Tracking
Disable Tracking
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -498,90 +533,140 @@ export default function StockManagementPage() {
</div>
</div>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-12">
<input
type="checkbox"
checked={selectedProducts.length === products.length}
onChange={toggleSelectAll}
className="rounded border-gray-300"
/>
</TableHead>
<TableHead>Product</TableHead>
<TableHead>Stock Status</TableHead>
<TableHead>Current Stock</TableHead>
<TableHead>Track Stock</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={6} className="text-center py-8">
<RefreshCw className="h-6 w-6 animate-spin inline-block" />
<span className="ml-2">Loading products...</span>
</TableCell>
</TableRow>
) : filteredProducts.length === 0 ? (
<TableRow>
<TableCell colSpan={6} className="text-center py-8">
No products found
</TableCell>
</TableRow>
) : (
filteredProducts.map((product) => (
<TableRow key={product._id}>
<TableCell>
<Card className="border-border/40 bg-background/50 backdrop-blur-sm shadow-sm overflow-hidden">
<CardHeader className="py-4 px-6 border-b border-border/50 bg-muted/30 flex flex-row items-center justify-between">
<div>
<CardTitle className="text-lg font-medium">Inventory Data</CardTitle>
<CardDescription>Manage stock levels and tracking for {products.length} products</CardDescription>
</div>
<div className="text-xs text-muted-foreground bg-background/50 px-3 py-1 rounded-full border border-border/50">
{filteredProducts.length} items
</div>
</CardHeader>
<CardContent className="p-0">
<div className="overflow-x-auto">
<Table>
<TableHeader className="bg-muted/50">
<TableRow className="border-border/50 hover:bg-transparent">
<TableHead className="w-12 pl-6">
<input
type="checkbox"
checked={selectedProducts.includes(product._id || '')}
onChange={() => toggleSelectProduct(product._id || '')}
className="rounded border-gray-300"
checked={selectedProducts.length === products.length && products.length > 0}
onChange={toggleSelectAll}
className="rounded border-gray-300 dark:border-zinc-700 bg-background focus:ring-primary/20"
/>
</TableCell>
<TableCell>{product.name}</TableCell>
<TableCell>{getStockStatus(product)}</TableCell>
<TableCell>
{editingStock[product._id || ''] ? (
<div className="flex items-center gap-2">
<Input
type="number"
value={stockValues[product._id || ''] || 0}
onChange={(e) => handleStockChange(product._id || '', parseInt(e.target.value) || 0)}
className="w-24"
/>
<Button size="sm" onClick={() => handleSaveStock(product)}>Save</Button>
</div>
) : (
<span>{product.currentStock || 0}</span>
)}
</TableCell>
<TableCell>
<Switch
checked={product.stockTracking || false}
onCheckedChange={() => handleToggleStockTracking(product)}
/>
</TableCell>
<TableCell className="text-right">
{!editingStock[product._id || ''] && (
<Button
variant="outline"
size="sm"
onClick={() => handleEditStock(product._id || '')}
>
Edit Stock
</Button>
)}
</TableCell>
</TableHead>
<TableHead>Product</TableHead>
<TableHead>Status</TableHead>
<TableHead>Current Stock</TableHead>
<TableHead>Tracking</TableHead>
<TableHead className="text-right pr-6">Actions</TableHead>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</TableHeader>
<TableBody>
<AnimatePresence mode="popLayout">
{loading ? (
<TableRow>
<TableCell colSpan={6} className="text-center py-12">
<div className="flex flex-col items-center justify-center gap-3 text-muted-foreground">
<RefreshCw className="h-8 w-8 animate-spin opacity-20" />
<p>Loading products...</p>
</div>
</TableCell>
</TableRow>
) : filteredProducts.length === 0 ? (
<TableRow>
<TableCell colSpan={6} className="text-center py-12">
<div className="flex flex-col items-center justify-center gap-3 text-muted-foreground">
<Boxes className="h-10 w-10 opacity-20" />
<p>No products found matching your search</p>
</div>
</TableCell>
</TableRow>
) : (
filteredProducts.map((product, index) => (
<motion.tr
key={product._id}
initial={{ opacity: 0, scale: 0.98 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.2, delay: index * 0.03 }}
className="group hover:bg-muted/40 border-b border-border/50 transition-colors"
>
<TableCell className="pl-6">
<input
type="checkbox"
checked={selectedProducts.includes(product._id || '')}
onChange={() => toggleSelectProduct(product._id || '')}
className="rounded border-gray-300 dark:border-zinc-700 bg-background focus:ring-primary/20"
/>
</TableCell>
<TableCell className="font-medium">{product.name}</TableCell>
<TableCell>
<StatusBadge status={getStockStatus(product)} />
</TableCell>
<TableCell>
{editingStock[product._id || ''] ? (
<div className="flex items-center gap-2">
<Input
type="number"
value={stockValues[product._id || ''] || 0}
onChange={(e) => handleStockChange(product._id || '', parseInt(e.target.value) || 0)}
className="w-20 h-8 font-mono bg-background"
/>
</div>
) : (
<span className="font-mono text-sm">{product.currentStock || 0}</span>
)}
</TableCell>
<TableCell>
<Switch
checked={product.stockTracking || false}
onCheckedChange={() => handleToggleStockTracking(product)}
className="data-[state=checked]:bg-primary"
/>
</TableCell>
<TableCell className="text-right pr-6">
<div className="flex justify-end gap-1">
{editingStock[product._id || ''] ? (
<>
<Button
size="icon"
variant="ghost"
className="h-8 w-8 text-green-600 hover:text-green-700 hover:bg-green-100 dark:hover:bg-green-900/20"
onClick={() => handleSaveStock(product)}
>
<Save className="h-4 w-4" />
</Button>
<Button
size="icon"
variant="ghost"
className="h-8 w-8 text-muted-foreground hover:text-destructive hover:bg-destructive/10"
onClick={() => setEditingStock({ ...editingStock, [product._id || '']: false })}
>
<X className="h-4 w-4" />
</Button>
</>
) : (
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-muted-foreground hover:text-primary hover:bg-primary/10"
onClick={() => handleEditStock(product._id || '')}
>
<Edit2 className="h-4 w-4" />
</Button>
)}
</div>
</TableCell>
</motion.tr>
))
)}
</AnimatePresence>
</TableBody>
</Table>
</div>
</CardContent>
</Card>
</div>
<AlertDialog open={isConfirmDialogOpen} onOpenChange={setIsConfirmDialogOpen}>
@@ -589,12 +674,14 @@ export default function StockManagementPage() {
<AlertDialogHeader>
<AlertDialogTitle>Confirm Bulk Action</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to {bulkAction} stock tracking for {selectedProducts.length} selected products?
Are you sure you want to {bulkAction} stock tracking for <span className="font-medium text-foreground">{selectedProducts.length}</span> selected products?
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => setBulkAction(null)}>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={executeBulkAction}>Continue</AlertDialogAction>
<AlertDialogAction onClick={executeBulkAction} className="bg-primary text-primary-foreground">
Continue
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>