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.
690 lines
27 KiB
TypeScript
690 lines
27 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import Layout from "@/components/layout/layout";
|
|
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 { 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 {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
AlertDialogTrigger,
|
|
} from "@/components/ui/alert-dialog";
|
|
import { Product } from "@/models/products";
|
|
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;
|
|
stockTracking: boolean;
|
|
lowStockThreshold?: number;
|
|
}
|
|
|
|
type ReportType = 'daily' | 'weekly' | 'monthly' | 'custom';
|
|
|
|
export default function StockManagementPage() {
|
|
const router = useRouter();
|
|
const [products, setProducts] = useState<Product[]>([]);
|
|
const [loading, setLoading] = useState<boolean>(true);
|
|
const [editingStock, setEditingStock] = useState<Record<string, boolean>>({});
|
|
const [stockValues, setStockValues] = useState<Record<string, number>>({});
|
|
const [searchTerm, setSearchTerm] = useState<string>("");
|
|
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>({
|
|
from: startOfDay(addDays(new Date(), -6)),
|
|
to: endOfDay(new Date())
|
|
});
|
|
const [selectedMonth, setSelectedMonth] = useState<Date>(new Date());
|
|
const [reportType, setReportType] = useState<ReportType>('daily');
|
|
const [isExporting, setIsExporting] = useState<boolean>(false);
|
|
|
|
useEffect(() => {
|
|
const authToken = document.cookie
|
|
.split("; ")
|
|
.find((row) => row.startsWith("Authorization="))
|
|
?.split("=")[1];
|
|
|
|
if (!authToken) {
|
|
router.push("/login");
|
|
return;
|
|
}
|
|
|
|
const fetchDataAsync = async () => {
|
|
try {
|
|
const response = await clientFetch<Product[]>('api/products');
|
|
const fetchedProducts = response || [];
|
|
setProducts(fetchedProducts);
|
|
|
|
// Initialize stock values
|
|
const initialStockValues: Record<string, number> = {};
|
|
fetchedProducts.forEach((product: Product) => {
|
|
if (product._id) {
|
|
initialStockValues[product._id] = product.currentStock || 0;
|
|
}
|
|
});
|
|
setStockValues(initialStockValues);
|
|
|
|
setLoading(false);
|
|
} catch (error) {
|
|
console.error("Error fetching products:", error);
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchDataAsync();
|
|
}, [router]);
|
|
|
|
const handleEditStock = (productId: string) => {
|
|
setEditingStock({
|
|
...editingStock,
|
|
[productId]: true,
|
|
});
|
|
};
|
|
|
|
const handleSaveStock = async (product: Product) => {
|
|
if (!product._id) return;
|
|
|
|
try {
|
|
const newStockValue = stockValues[product._id] || 0;
|
|
|
|
const stockData: StockData = {
|
|
currentStock: newStockValue,
|
|
stockTracking: product.stockTracking || false,
|
|
lowStockThreshold: product.lowStockThreshold
|
|
};
|
|
|
|
await clientFetch(`api/stock/${product._id}`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(stockData)
|
|
});
|
|
|
|
// Update local products state
|
|
setProducts(products.map(p => {
|
|
if (p._id === product._id) {
|
|
return {
|
|
...p,
|
|
currentStock: newStockValue
|
|
};
|
|
}
|
|
return p;
|
|
}));
|
|
|
|
setEditingStock({
|
|
...editingStock,
|
|
[product._id]: false,
|
|
});
|
|
|
|
toast.success("Stock updated successfully");
|
|
} catch (error) {
|
|
console.error("Error updating stock:", error);
|
|
toast.error("Failed to update stock");
|
|
}
|
|
};
|
|
|
|
const handleStockChange = (productId: string, value: number) => {
|
|
setStockValues({
|
|
...stockValues,
|
|
[productId]: value,
|
|
});
|
|
};
|
|
|
|
const handleToggleStockTracking = async (product: Product) => {
|
|
if (!product._id) return;
|
|
|
|
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',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(stockData)
|
|
});
|
|
|
|
// Update local state
|
|
setProducts(products.map(p => {
|
|
if (p._id === product._id) {
|
|
return {
|
|
...p,
|
|
stockTracking: newTrackingStatus,
|
|
currentStock: stockData.currentStock,
|
|
lowStockThreshold: stockData.lowStockThreshold,
|
|
};
|
|
}
|
|
return p;
|
|
}));
|
|
|
|
toast.success(`Stock tracking ${newTrackingStatus ? 'enabled' : 'disabled'} for ${product.name}`);
|
|
} catch (error) {
|
|
console.error("Error toggling stock tracking:", error);
|
|
toast.error(`Failed to ${product.stockTracking ? 'disable' : 'enable'} stock tracking`);
|
|
}
|
|
};
|
|
|
|
const handleBulkAction = async (action: 'enable' | 'disable') => {
|
|
setBulkAction(action);
|
|
setIsConfirmDialogOpen(true);
|
|
};
|
|
|
|
const executeBulkAction = async () => {
|
|
if (!bulkAction) return;
|
|
|
|
try {
|
|
const productsToUpdate = products.filter(p => selectedProducts.includes(p._id || ''));
|
|
|
|
await Promise.all(productsToUpdate.map(async (product) => {
|
|
if (!product._id) return;
|
|
|
|
const stockData: StockData = {
|
|
stockTracking: bulkAction === 'enable',
|
|
currentStock: product.currentStock || 0,
|
|
lowStockThreshold: product.lowStockThreshold || 10,
|
|
};
|
|
|
|
await clientFetch(`api/stock/${product._id}`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(stockData)
|
|
});
|
|
}));
|
|
|
|
// Update local state
|
|
setProducts(products.map(p => {
|
|
if (selectedProducts.includes(p._id || '')) {
|
|
return {
|
|
...p,
|
|
stockTracking: bulkAction === 'enable',
|
|
};
|
|
}
|
|
return p;
|
|
}));
|
|
|
|
setSelectedProducts([]);
|
|
toast.success(`Stock tracking ${bulkAction}d for selected products`);
|
|
} catch (error) {
|
|
console.error(`Error ${bulkAction}ing stock tracking:`, error);
|
|
toast.error(`Failed to ${bulkAction} stock tracking`);
|
|
}
|
|
|
|
setIsConfirmDialogOpen(false);
|
|
setBulkAction(null);
|
|
};
|
|
|
|
const toggleSelectProduct = (productId: string) => {
|
|
setSelectedProducts(prev =>
|
|
prev.includes(productId)
|
|
? prev.filter(id => id !== productId)
|
|
: [...prev, productId]
|
|
);
|
|
};
|
|
|
|
const toggleSelectAll = () => {
|
|
setSelectedProducts(prev =>
|
|
prev.length === products.length
|
|
? []
|
|
: products.map(p => p._id || '')
|
|
);
|
|
};
|
|
|
|
const handleExportStock = async () => {
|
|
setIsExporting(true);
|
|
try {
|
|
let response;
|
|
let filename;
|
|
|
|
switch (reportType) {
|
|
case 'daily':
|
|
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');
|
|
return;
|
|
}
|
|
const weekStart = format(exportDateRange.from, 'yyyy-MM-dd');
|
|
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');
|
|
return;
|
|
}
|
|
const startDate = format(exportDateRange.from, 'yyyy-MM-dd');
|
|
const endDate = format(exportDateRange.to, 'yyyy-MM-dd');
|
|
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');
|
|
}
|
|
|
|
// Convert data to CSV format
|
|
const csvHeaders = [
|
|
'Product Name',
|
|
'Quantity Sold',
|
|
'Total Revenue (£)',
|
|
'Average Price (£)',
|
|
'Order Count',
|
|
'Current Stock',
|
|
'Stock Status',
|
|
'Unit Type'
|
|
];
|
|
|
|
const csvData = [
|
|
csvHeaders.join(','),
|
|
...response.products.map((product: any) => [
|
|
`"${product.productName.replace(/"/g, '""')}"`, // Escape quotes in product names
|
|
product.quantitySold,
|
|
product.totalRevenue.toFixed(2),
|
|
product.averagePrice.toFixed(2),
|
|
product.orderCount,
|
|
product.currentStock || 0,
|
|
`"${product.stockStatus}"`,
|
|
`"${product.unitType || 'N/A'}"`
|
|
].join(','))
|
|
].join('\n');
|
|
|
|
// Create and download the CSV file
|
|
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')}`;
|
|
|
|
toast.success(`${reportType.charAt(0).toUpperCase() + reportType.slice(1)} stock report for ${periodText} exported successfully`);
|
|
} catch (error) {
|
|
console.error('Error exporting stock report:', error);
|
|
toast.error('Failed to export stock report');
|
|
} finally {
|
|
setIsExporting(false);
|
|
}
|
|
};
|
|
|
|
const getStockStatus = (product: Product) => {
|
|
if (!product.stockTracking) return 'Not tracked';
|
|
if (product.currentStock === undefined) return 'Unknown';
|
|
if (product.currentStock <= 0) return 'Out of stock';
|
|
if (product.lowStockThreshold && product.currentStock <= product.lowStockThreshold) return 'Low stock';
|
|
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) ||
|
|
product.description.toLowerCase().includes(searchLower) ||
|
|
getStockStatus(product).toLowerCase().includes(searchLower)
|
|
);
|
|
});
|
|
|
|
return (
|
|
<Layout>
|
|
<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" 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 align="end">
|
|
<DropdownMenuLabel>Filter Reports</DropdownMenuLabel>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem onClick={() => setReportType('daily')}>
|
|
Daily Report
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => setReportType('weekly')}>
|
|
Weekly Report
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => setReportType('monthly')}>
|
|
Monthly Report
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => setReportType('custom')}>
|
|
Custom Range
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
|
|
{/* Date Selection based on report type */}
|
|
<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 border-border/50 bg-background/50"
|
|
/>
|
|
)}
|
|
|
|
{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 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" />
|
|
)}
|
|
Export
|
|
</Button>
|
|
|
|
{selectedProducts.length > 0 && (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="default" className="gap-2">
|
|
<Package className="h-4 w-4" />
|
|
Bulk Actions
|
|
<ChevronDown className="h-4 w-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuItem onClick={() => handleBulkAction('enable')}>
|
|
<CheckSquare className="h-4 w-4 mr-2" />
|
|
Enable Tracking
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => handleBulkAction('disable')}>
|
|
<XSquare className="h-4 w-4 mr-2" />
|
|
Disable Tracking
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<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.length === products.length && products.length > 0}
|
|
onChange={toggleSelectAll}
|
|
className="rounded border-gray-300 dark:border-zinc-700 bg-background focus:ring-primary/20"
|
|
/>
|
|
</TableHead>
|
|
<TableHead>Product</TableHead>
|
|
<TableHead>Status</TableHead>
|
|
<TableHead>Current Stock</TableHead>
|
|
<TableHead>Tracking</TableHead>
|
|
<TableHead className="text-right pr-6">Actions</TableHead>
|
|
</TableRow>
|
|
</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}>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>Confirm Bulk Action</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
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} className="bg-primary text-primary-foreground">
|
|
Continue
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</Layout>
|
|
);
|
|
}
|