Enhance admin dashboard UI and tables with new styles
All checks were successful
Build Frontend / build (push) Successful in 1m4s
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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user