From 73adbe5d079b1198475e135f44b1641ca131163e Mon Sep 17 00:00:00 2001 From: g Date: Mon, 12 Jan 2026 07:16:33 +0000 Subject: [PATCH] Enhance admin dashboard UI and tables with new styles 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. --- app/dashboard/admin/page.tsx | 76 ++--- app/dashboard/admin/users/page.tsx | 376 ++++++++++++------------- app/dashboard/admin/vendors/page.tsx | 335 ++++++++++++---------- app/dashboard/shipping/page.tsx | 32 +-- app/dashboard/stock/page.tsx | 405 ++++++++++++++++----------- components/tables/shipping-table.tsx | 144 ++++++---- 6 files changed, 757 insertions(+), 611 deletions(-) diff --git a/app/dashboard/admin/page.tsx b/app/dashboard/admin/page.tsx index d4548cf..85e4661 100644 --- a/app/dashboard/admin/page.tsx +++ b/app/dashboard/admin/page.tsx @@ -37,7 +37,7 @@ class ErrorBoundary extends Component { componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { console.error(`Error loading ${this.props.componentName || 'component'}:`, error, errorInfo); - + // Log to error tracking service if available if (typeof window !== 'undefined' && (window as any).gtag) { (window as any).gtag('event', 'exception', { @@ -105,31 +105,31 @@ class ErrorBoundary extends Component { } // Lazy load admin components with error handling -const AdminAnalytics = lazy(() => +const AdminAnalytics = lazy(() => import("@/components/admin/AdminAnalytics").catch((err) => { console.error("Failed to load AdminAnalytics:", err); throw err; }) ); -const InviteVendorCard = lazy(() => +const InviteVendorCard = lazy(() => import("@/components/admin/InviteVendorCard").catch((err) => { console.error("Failed to load InviteVendorCard:", err); throw err; }) ); -const BanUserCard = lazy(() => +const BanUserCard = lazy(() => import("@/components/admin/BanUserCard").catch((err) => { console.error("Failed to load BanUserCard:", err); throw err; }) ); -const InvitationsListCard = lazy(() => +const InvitationsListCard = lazy(() => import("@/components/admin/InvitationsListCard").catch((err) => { console.error("Failed to load InvitationsListCard:", err); throw err; }) ); -const VendorsCard = lazy(() => +const VendorsCard = lazy(() => import("@/components/admin/VendorsCard").catch((err) => { console.error("Failed to load VendorsCard:", err); throw err; @@ -139,7 +139,7 @@ const VendorsCard = lazy(() => // Loading skeleton with timeout warning function AdminComponentSkeleton({ showSlowWarning = false }: { showSlowWarning?: boolean }) { return ( -
-
- + {/* Header skeleton */}
@@ -181,9 +181,9 @@ function AdminComponentSkeleton({ showSlowWarning = false }: { showSlowWarning?: {/* Metric cards grid skeleton */}
{[1, 2, 3, 4, 5, 6, 7, 8].map((i) => ( - -
{[1, 2, 3, 4].map((i) => ( - {[1, 2, 3].map((j) => ( -
{ // Avoid prefetching if already done if (prefetchedTabs.has(tab)) return; - + const startTime = performance.now(); - + if (tab === "analytics") { // Prefetch analytics component import("@/components/admin/AdminAnalytics") @@ -355,7 +355,7 @@ export default function AdminPage() { const loadTime = performance.now() - startTime; console.log(`[Performance] AdminAnalytics prefetched in ${loadTime.toFixed(2)}ms`); }) - .catch(() => {}); + .catch(() => { }); } else if (tab === "management") { // Prefetch management components Promise.all([ @@ -368,9 +368,9 @@ export default function AdminPage() { const loadTime = performance.now() - startTime; console.log(`[Performance] Management components prefetched in ${loadTime.toFixed(2)}ms`); }) - .catch(() => {}); + .catch(() => { }); } - + setPrefetchedTabs(prev => new Set(prev).add(tab)); }; @@ -392,26 +392,26 @@ export default function AdminPage() { return (
-
+
-

Admin Dashboard

+

Admin Dashboard

Platform analytics and vendor management

-
- handleTabHover("analytics")} onFocus={() => handleTabFocus("analytics")} > Analytics - handleTabHover("management")} onFocus={() => handleTabFocus("management")} @@ -422,7 +422,7 @@ export default function AdminPage() { - } timeout={5000} timeoutFallback={} @@ -436,7 +436,7 @@ export default function AdminPage() { - } timeout={5000} timeoutFallback={} diff --git a/app/dashboard/admin/users/page.tsx b/app/dashboard/admin/users/page.tsx index 0304732..9b6ab95 100644 --- a/app/dashboard/admin/users/page.tsx +++ b/app/dashboard/admin/users/page.tsx @@ -7,9 +7,10 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; -import { Search, Ban, UserCheck, Package, DollarSign, Loader2, Repeat } from "lucide-react"; +import { Search, Ban, UserCheck, Package, DollarSign, Loader2, Repeat, Users, ShoppingBag, CreditCard, UserX } from "lucide-react"; import { fetchClient } from "@/lib/api-client"; import { useToast } from "@/hooks/use-toast"; +import { motion, AnimatePresence } from "framer-motion"; interface TelegramUser { telegramUserId: string; @@ -88,123 +89,86 @@ export default function AdminUsersPage() { const totalSpent = users.reduce((sum, u) => sum + u.totalSpent, 0); const totalOrders = users.reduce((sum, u) => sum + u.totalOrders, 0); + const stats = [ + { + title: "Total Users", + value: users.length, + description: "Registered users", + icon: Users, + }, + { + title: "Users with Orders", + value: usersWithOrders.length, + description: `${users.length > 0 ? Math.round((usersWithOrders.length / users.length) * 100) : 0}% conversion rate`, + icon: ShoppingBag, + }, + { + title: "Total Revenue", + value: formatCurrency(totalSpent), + description: `${totalOrders} total orders`, + icon: DollarSign, + }, + { + title: "Returning", + value: returningCustomers.length, + description: `${usersWithOrders.length > 0 ? Math.round((returningCustomers.length / usersWithOrders.length) * 100) : 0}% of customers`, + icon: Repeat, + }, + { + title: "Blocked", + value: blockedUsers.length, + description: `${users.length > 0 ? Math.round((blockedUsers.length / users.length) * 100) : 0}% blocked rate`, + icon: UserX, + }, + ]; + return ( -
-
-

Telegram Users

-

Manage Telegram user accounts and view statistics

+
+
+
+

Telegram Users

+

Manage Telegram user accounts and view statistics

+
{/* Stats Cards */}
- - - Total Users - - - {loading ? ( -
- -
- ) : ( - <> -
{users.length}
-

Registered users

- - )} -
-
- - - Users with Orders - - - {loading ? ( -
- -
- ) : ( - <> -
{usersWithOrders.length}
-

- {users.length > 0 ? Math.round((usersWithOrders.length / users.length) * 100) : 0}% conversion rate -

- - )} -
-
- - - Total Revenue - - - {loading ? ( -
- -
- ) : ( - <> -
{formatCurrency(totalSpent)}
-

{totalOrders} total orders

- - )} -
-
- - - Returning Customers - - - - {loading ? ( -
- -
- ) : ( - <> -
{returningCustomers.length}
-

- {usersWithOrders.length > 0 ? Math.round((returningCustomers.length / usersWithOrders.length) * 100) : 0}% of customers -

- - )} -
-
- - - Blocked Users - - - {loading ? ( -
- -
- ) : ( - <> -
{blockedUsers.length}
-

- {users.length > 0 ? Math.round((blockedUsers.length / users.length) * 100) : 0}% blocked rate -

- - )} -
-
+ {stats.map((stat, i) => ( + + + {stat.title} + + + + {loading ? ( +
+ +
+ ) : ( +
+
{stat.value}
+

{stat.description}

+
+ )} +
+
+ ))}
{/* Search and Filters */} - - -
+ + +
- User Management + User Management View and manage all Telegram user accounts
- - + { setSearchQuery(e.target.value); @@ -216,19 +180,11 @@ export default function AdminUsersPage() {
- {loading ? ( -
- -
- ) : users.length === 0 ? ( -
- {searchQuery ? "No users found matching your search" : "No users found"} -
- ) : ( +
- - - User ID + + + User ID Username Orders Total Spent @@ -239,88 +195,118 @@ export default function AdminUsersPage() { - {users.map((user) => ( - - -
{user.telegramUserId}
-
- -
- {user.telegramUsername !== "Unknown" ? `@${user.telegramUsername}` : "Unknown"} -
-
- -
- - {user.totalOrders} - {user.completedOrders > 0 && ( - - {user.completedOrders} completed - - )} -
-
- -
- - {formatCurrency(user.totalSpent)} -
-
- - {user.isBlocked ? ( - - - - - - Blocked - - - {user.blockedReason && ( - -

{user.blockedReason}

-
+ + {loading ? ( + + +
+ +

Loading users...

+
+
+
+ ) : users.length === 0 ? ( + + + {searchQuery ? "No users found matching your search" : "No users found"} + + + ) : ( + users.map((user, index) => ( + + +
{user.telegramUserId}
+
+ +
+ {user.telegramUsername !== "Unknown" ? ( + <> + @ + {user.telegramUsername} + + ) : ( + Unknown )} - - - ) : user.totalOrders > 0 ? ( - Active - ) : ( - No Orders - )} - - - {user.firstOrderDate - ? new Date(user.firstOrderDate).toLocaleDateString() - : 'N/A'} - - - {user.lastOrderDate - ? new Date(user.lastOrderDate).toLocaleDateString() - : 'N/A'} - - -
- {!user.isBlocked ? ( - - ) : ( - - )} -
-
- - ))} +
+
+ +
+ {user.totalOrders} + {user.completedOrders > 0 && ( + + {user.completedOrders} done + + )} +
+
+ + {formatCurrency(user.totalSpent)} + + + {user.isBlocked ? ( + + + + + + Blocked + + + {user.blockedReason && ( + +

{user.blockedReason}

+
+ )} +
+
+ ) : user.totalOrders > 0 ? ( + Active + ) : ( + No Orders + )} +
+ + {user.firstOrderDate + ? new Date(user.firstOrderDate).toLocaleDateString() + : '-'} + + + {user.lastOrderDate + ? new Date(user.lastOrderDate).toLocaleDateString() + : '-'} + + +
+ {!user.isBlocked ? ( + + ) : ( + + )} +
+
+
+ )) + )} +
- )} +
+ {pagination && pagination.totalPages > 1 && ( -
+
- Showing page {pagination.page} of {pagination.totalPages} ({pagination.total} total users) + Showing page {pagination.page} of {pagination.totalPages}
@@ -336,6 +323,7 @@ export default function AdminUsersPage() { size="sm" onClick={() => setPage(p => p + 1)} disabled={!pagination.hasNextPage} + className="h-8" > Next diff --git a/app/dashboard/admin/vendors/page.tsx b/app/dashboard/admin/vendors/page.tsx index e864bbf..47a97f9 100644 --- a/app/dashboard/admin/vendors/page.tsx +++ b/app/dashboard/admin/vendors/page.tsx @@ -6,9 +6,10 @@ import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; -import { Search, MoreHorizontal, UserCheck, UserX, Mail, Loader2 } from "lucide-react"; +import { Search, MoreHorizontal, UserCheck, UserX, Mail, Loader2, Store, Shield, ShieldAlert, Clock, Calendar } from "lucide-react"; import { fetchClient } from "@/lib/api-client"; import { useToast } from "@/hooks/use-toast"; +import { motion, AnimatePresence } from "framer-motion"; interface Vendor { _id: string; @@ -68,10 +69,10 @@ export default function AdminVendorsPage() { }, [fetchVendors]); const filteredVendors = searchQuery.trim() - ? vendors.filter(v => - v.username.toLowerCase().includes(searchQuery.toLowerCase()) || - (v.storeId && v.storeId.toString().toLowerCase().includes(searchQuery.toLowerCase())) - ) + ? vendors.filter(v => + v.username.toLowerCase().includes(searchQuery.toLowerCase()) || + (v.storeId && v.storeId.toString().toLowerCase().includes(searchQuery.toLowerCase())) + ) : vendors; const activeVendors = vendors.filter(v => v.isActive); @@ -79,174 +80,214 @@ export default function AdminVendorsPage() { const adminVendors = vendors.filter(v => v.isAdmin); const totalVendors = pagination?.total || vendors.length; + const stats = [ + { + title: "Total Vendors", + value: totalVendors, + description: "Registered vendors", + icon: Store, + }, + { + title: "Active Vendors", + value: activeVendors.length, + description: `${vendors.length > 0 ? Math.round((activeVendors.length / vendors.length) * 100) : 0}% active rate`, + icon: UserCheck, + }, + { + title: "Suspended", + value: suspendedVendors.length, + description: `${vendors.length > 0 ? Math.round((suspendedVendors.length / vendors.length) * 100) : 0}% suspension rate`, + icon: UserX, + }, + { + title: "Admin Users", + value: adminVendors.length, + description: "Administrative access", + icon: ShieldAlert, + }, + ]; + return ( -
-
-

All Vendors

-

Manage vendor accounts and permissions

+
+
+
+

All Vendors

+

Manage vendor accounts and permissions

+
{/* Stats Cards */}
- - - Total Vendors - - -
{totalVendors}
-

Registered vendors

-
-
- - - Active Vendors - - -
{activeVendors.length}
-

- {vendors.length > 0 ? Math.round((activeVendors.length / vendors.length) * 100) : 0}% active rate -

-
-
- - - Suspended - - -
{suspendedVendors.length}
-

- {vendors.length > 0 ? Math.round((suspendedVendors.length / vendors.length) * 100) : 0}% suspension rate -

-
-
- - - Admin Users - - -
{adminVendors.length}
-

Administrative access

-
-
+ {stats.map((stat, i) => ( + + + {stat.title} + + + +
+
{stat.value}
+

{stat.description}

+
+
+
+ ))}
{/* Search and Filters */} - - -
+ + +
- Vendor Management + Vendor Management View and manage all vendor accounts
- - + setSearchQuery(e.target.value)} />
-
- {loading ? ( -
- -
- ) : filteredVendors.length === 0 ? ( -
- {searchQuery.trim() ? "No vendors found matching your search" : "No vendors found"} -
- ) : ( - <> - - - - Vendor - Store - Status - Join Date - Last Login - Actions - - - - {filteredVendors.map((vendor) => ( - - -
{vendor.username}
-
- {vendor.storeId || 'No store'} - -
- - {vendor.isActive ? "active" : "suspended"} - - {vendor.isAdmin && ( - - Admin - - )} -
-
- - {vendor.createdAt ? new Date(vendor.createdAt).toLocaleDateString() : 'N/A'} - - - {vendor.lastLogin ? new Date(vendor.lastLogin).toLocaleDateString() : 'Never'} - - -
- - - +
+
+ + + Vendor + Store + Status + Join Date + Last Login + Actions + + + + + {loading ? ( + + +
+ +

Loading vendors...

- ))} -
-
- {pagination && pagination.totalPages > 1 && ( -
- - Page {pagination.page} of {pagination.totalPages} ({pagination.total} total) - -
- - -
-
- )} - + ) : filteredVendors.length === 0 ? ( + + + {searchQuery.trim() ? "No vendors found matching your search" : "No vendors found"} + + + ) : ( + filteredVendors.map((vendor, index) => ( + + +
+
+ {vendor.username.substring(0, 2).toUpperCase()} +
+ {vendor.username} +
+
+ + {vendor.storeId ? ( + {vendor.storeId} + ) : ( + No store + )} + + +
+ + {vendor.isActive ? "Active" : "Suspended"} + + {vendor.isAdmin && ( + + + Admin + + )} +
+
+ +
+ + {vendor.createdAt ? new Date(vendor.createdAt).toLocaleDateString() : 'N/A'} +
+
+ +
+ + {vendor.lastLogin ? new Date(vendor.lastLogin).toLocaleDateString() : 'Never'} +
+
+ +
+ + + +
+
+
+ )) + )} + + + +
+ {pagination && pagination.totalPages > 1 && ( +
+ + Page {pagination.page} of {pagination.totalPages} + +
+ + +
+
)}
diff --git a/app/dashboard/shipping/page.tsx b/app/dashboard/shipping/page.tsx index 82917ba..6b58cf4 100644 --- a/app/dashboard/shipping/page.tsx +++ b/app/dashboard/shipping/page.tsx @@ -54,10 +54,10 @@ const ShippingTable = dynamic(() => import("@/components/tables/shipping-table") // Loading skeleton for shipping table function ShippingTableSkeleton() { return ( - + {/* Subtle loading indicator */}
-
- +
@@ -77,8 +77,8 @@ function ShippingTableSkeleton() {
{['Method Name', 'Price', 'Actions'].map((header, i) => ( -
- + {[...Array(4)].map((_, i) => ( -
row.startsWith("Authorization=")) ?.split("=")[1]; - + if (!authToken) { console.error("No auth token found"); return; } - + console.log("Sending request to add shipping method:", newShipping); const response = await addShippingMethod( newShipping, @@ -196,10 +196,10 @@ export default function ShippingPage() { // Close modal and reset form before refreshing to avoid UI delays setModalOpen(false); setNewShipping({ name: "", price: 0 }); - + // Refresh the list after adding refreshShippingMethods(); - + console.log("Shipping method added successfully"); } catch (error) { console.error("Error adding shipping method:", error); @@ -218,12 +218,12 @@ export default function ShippingPage() { .split("; ") .find((row) => row.startsWith("Authorization=")) ?.split("=")[1]; - + if (!authToken) { console.error("No auth token found"); return; } - + await updateShippingMethod( newShipping._id, newShipping, @@ -234,10 +234,10 @@ export default function ShippingPage() { setModalOpen(false); setNewShipping({ name: "", price: 0 }); setEditing(false); - + // Refresh the list after updating refreshShippingMethods(); - + console.log("Shipping method updated successfully"); } catch (error) { console.error("Error updating shipping method:", error); diff --git a/app/dashboard/stock/page.tsx b/app/dashboard/stock/page.tsx index 9eb0075..1abeb46 100644 --- a/app/dashboard/stock/page.tsx +++ b/app/dashboard/stock/page.tsx @@ -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([]); const [isConfirmDialogOpen, setIsConfirmDialogOpen] = useState(false); const [bulkAction, setBulkAction] = useState<'enable' | 'disable' | null>(null); - + // Export state const [exportDate, setExportDate] = useState(new Date().toISOString().split('T')[0]); const [exportDateRange, setExportDateRange] = useState({ @@ -82,7 +87,7 @@ export default function StockManagementPage() { const response = await clientFetch('api/products'); const fetchedProducts = response || []; setProducts(fetchedProducts); - + // Initialize stock values const initialStockValues: Record = {}; 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 {status}; + }; + 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 ( -
-
-

- - Stock Management -

-
- setSearchTerm(e.target.value)} - /> - - {/* Report Type Selector */} +
+
+
+

+ + Stock Management +

+

+ Track inventory levels and manage stock status +

+
+ +
+
+ + setSearchTerm(e.target.value)} + /> +
+ - - + + Filter Reports + setReportType('daily')}> Daily Report @@ -433,51 +466,53 @@ export default function StockManagementPage() { {/* Date Selection based on report type */} - {reportType === 'daily' && ( - setExportDate(date ? date.toISOString().split('T')[0] : '')} - placeholder="Select export date" - className="w-auto" - /> - )} +
+ {reportType === 'daily' && ( + setExportDate(date ? date.toISOString().split('T')[0] : '')} + placeholder="Select export date" + className="w-auto border-border/50 bg-background/50" + /> + )} - {(reportType === 'weekly' || reportType === 'custom') && ( - - )} + {(reportType === 'weekly' || reportType === 'custom') && ( + + )} - {reportType === 'monthly' && ( - setSelectedMonth(date || new Date())} - placeholder="Select month" - className="w-auto" - /> - )} + {reportType === 'monthly' && ( + setSelectedMonth(date || new Date())} + placeholder="Select month" + className="w-auto border-border/50 bg-background/50" + /> + )} +
{selectedProducts.length > 0 && ( -
-
- - - - - - - Product - Stock Status - Current Stock - Track Stock - Actions - - - - {loading ? ( - - - - Loading products... - - - ) : filteredProducts.length === 0 ? ( - - - No products found - - - ) : ( - filteredProducts.map((product) => ( - - + + +
+ Inventory Data + Manage stock levels and tracking for {products.length} products +
+
+ {filteredProducts.length} items +
+
+ +
+
+ + + 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" /> - - {product.name} - {getStockStatus(product)} - - {editingStock[product._id || ''] ? ( -
- handleStockChange(product._id || '', parseInt(e.target.value) || 0)} - className="w-24" - /> - -
- ) : ( - {product.currentStock || 0} - )} -
- - handleToggleStockTracking(product)} - /> - - - {!editingStock[product._id || ''] && ( - - )} - +
+ Product + Status + Current Stock + Tracking + Actions
- )) - )} - -
-
+ + + + {loading ? ( + + +
+ +

Loading products...

+
+
+
+ ) : filteredProducts.length === 0 ? ( + + +
+ +

No products found matching your search

+
+
+
+ ) : ( + filteredProducts.map((product, index) => ( + + + toggleSelectProduct(product._id || '')} + className="rounded border-gray-300 dark:border-zinc-700 bg-background focus:ring-primary/20" + /> + + {product.name} + + + + + {editingStock[product._id || ''] ? ( +
+ handleStockChange(product._id || '', parseInt(e.target.value) || 0)} + className="w-20 h-8 font-mono bg-background" + /> +
+ ) : ( + {product.currentStock || 0} + )} +
+ + handleToggleStockTracking(product)} + className="data-[state=checked]:bg-primary" + /> + + +
+ {editingStock[product._id || ''] ? ( + <> + + + + ) : ( + + )} +
+
+
+ )) + )} +
+
+ +
+ +
@@ -589,12 +674,14 @@ export default function StockManagementPage() { Confirm Bulk Action - Are you sure you want to {bulkAction} stock tracking for {selectedProducts.length} selected products? + Are you sure you want to {bulkAction} stock tracking for {selectedProducts.length} selected products? setBulkAction(null)}>Cancel - Continue + + Continue + diff --git a/components/tables/shipping-table.tsx b/components/tables/shipping-table.tsx index 5b21336..63acdcf 100644 --- a/components/tables/shipping-table.tsx +++ b/components/tables/shipping-table.tsx @@ -8,8 +8,9 @@ import { TableRow, } from "@/components/ui/table"; import { Button } from "@/components/ui/button"; -import { Edit, Trash } from "lucide-react"; - +import { Edit, Trash, Truck, PackageX } from "lucide-react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { motion, AnimatePresence } from "framer-motion"; import { ShippingMethod } from "@/lib/types"; interface ShippingTableProps { @@ -26,61 +27,90 @@ export const ShippingTable: React.FC = ({ onDeleteShipping, }) => { return ( -
- - - - Name - Price - Actions - - - - {loading ? ( - - - - - - ) : shippingMethods.length > 0 ? ( - shippingMethods.map((method) => ( - - {method.name} - £{method.price} - -
- - -
-
+ + + + + Available Methods + + + +
+
+ + + Method Name + Price + Actions - )) - ) : ( - - - No shipping methods found - - - )} - -
-
+ + + + {loading ? ( + + +
+ + Loading methods... +
+
+
+ ) : shippingMethods.length > 0 ? ( + shippingMethods.map((method, index) => ( + + +
+
+ +
+ {method.name} +
+
+ £{method.price} + +
+ + +
+
+
+ )) + ) : ( + + +
+ +

No shipping methods found

+
+
+
+ )} +
+
+ +
+ + ); };