Refactor UI imports and update component paths
Some checks failed
Build Frontend / build (push) Failing after 7s
Some checks failed
Build Frontend / build (push) Failing after 7s
Replaces imports from 'components/ui' with 'components/common' across the app and dashboard pages, and updates model and API imports to use new paths under 'lib'. Removes redundant authentication checks from several dashboard pages. Adds new dashboard components and utility files, and reorganizes hooks and services into the 'lib' directory for improved structure.
This commit is contained in:
@@ -2,18 +2,18 @@
|
||||
|
||||
import React, { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { Package, ShoppingBag, Info, ExternalLink, Loader2, AlertTriangle } from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/common/badge";
|
||||
import { Button } from "@/components/common/button";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
} from "@/components/common/tooltip";
|
||||
import { getCookie } from "@/lib/api";
|
||||
import axios from "axios";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { cacheUtils } from "@/lib/api-client";
|
||||
import { cacheUtils } from "@/lib/api/api-client";
|
||||
|
||||
interface Order {
|
||||
_id: string;
|
||||
@@ -299,4 +299,5 @@ export default function BuyerOrderInfo({ buyerId, chatId }: BuyerOrderInfoProps)
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/common/card";
|
||||
import { Button } from "@/components/common/button";
|
||||
import { Input } from "@/components/common/input";
|
||||
import { Avatar, AvatarFallback } from "@/components/common/avatar";
|
||||
import { cn } from "@/lib/utils/general";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import axios from "axios";
|
||||
@@ -15,9 +15,9 @@ import { getCookie, clientFetch } from "@/lib/api";
|
||||
import { ImageViewerModal } from "@/components/modals/image-viewer-modal";
|
||||
import Image from "next/image";
|
||||
import BuyerOrderInfo from "./BuyerOrderInfo";
|
||||
import { useIsTouchDevice } from "@/hooks/use-mobile";
|
||||
import { useChromebookScroll, useSmoothScrollToBottom } from "@/hooks/use-chromebook-scroll";
|
||||
import { useChromebookKeyboard, useChatFocus } from "@/hooks/use-chromebook-keyboard";
|
||||
import { useIsTouchDevice } from "@/lib/hooks/use-mobile";
|
||||
import { useChromebookScroll, useSmoothScrollToBottom } from "@/lib/hooks/use-chromebook-scroll";
|
||||
import { useChromebookKeyboard, useChatFocus } from "@/lib/hooks/use-chromebook-keyboard";
|
||||
|
||||
interface Message {
|
||||
_id: string;
|
||||
@@ -844,4 +844,5 @@ export default function ChatDetail({ chatId }: { chatId: string }) {
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,11 +10,11 @@ import {
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||
} from "@/components/common/table";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/common/card";
|
||||
import { Badge } from "@/components/common/badge";
|
||||
import { Button } from "@/components/common/button";
|
||||
import { Avatar, AvatarFallback } from "@/components/common/avatar";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import {
|
||||
Plus,
|
||||
@@ -41,7 +41,7 @@ import {
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
} from "@/components/common/select";
|
||||
import { toast } from "sonner";
|
||||
import { clientFetch, getCookie } from "@/lib/api";
|
||||
import { formatDistance } from 'date-fns';
|
||||
@@ -474,3 +474,5 @@ export default function ChatTable() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -8,11 +8,11 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
} from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
} from "@/components/common/card";
|
||||
import { Button } from "@/components/common/button";
|
||||
import { Input } from "@/components/common/input";
|
||||
import { Label } from "@/components/common/label";
|
||||
import { Textarea } from "@/components/common/textarea";
|
||||
import { ArrowLeft, Send, RefreshCw, Search, User } from "lucide-react";
|
||||
import axios from "axios";
|
||||
import { toast } from "sonner";
|
||||
@@ -376,3 +376,5 @@ export default function NewChatForm() {
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
139
components/dashboard/command-palette.tsx
Normal file
139
components/dashboard/command-palette.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
Calculator,
|
||||
Calendar,
|
||||
CreditCard,
|
||||
Settings,
|
||||
Smile,
|
||||
User,
|
||||
LayoutDashboard,
|
||||
Package,
|
||||
ShoppingCart,
|
||||
Users,
|
||||
BarChart3,
|
||||
RefreshCcw,
|
||||
RotateCcw,
|
||||
Search,
|
||||
MessageSquare,
|
||||
Truck
|
||||
} from "lucide-react"
|
||||
|
||||
import {
|
||||
CommandDialog,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
CommandSeparator,
|
||||
CommandShortcut,
|
||||
} from "@/components/common/command"
|
||||
import { useRouter } from "next/navigation"
|
||||
|
||||
interface CommandPaletteProps {
|
||||
onResetLayout?: () => void
|
||||
onToggleWidget?: (id: string) => void
|
||||
availableWidgets?: Array<{ id: string; title: string; visible: boolean }>
|
||||
}
|
||||
|
||||
export function CommandPalette({ onResetLayout, onToggleWidget, availableWidgets }: CommandPaletteProps) {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const router = useRouter()
|
||||
|
||||
React.useEffect(() => {
|
||||
const down = (e: KeyboardEvent) => {
|
||||
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault()
|
||||
setOpen((open) => !open)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("keydown", down)
|
||||
return () => document.removeEventListener("keydown", down)
|
||||
}, [])
|
||||
|
||||
const runCommand = React.useCallback((command: () => void) => {
|
||||
setOpen(false)
|
||||
command()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<CommandDialog open={open} onOpenChange={setOpen}>
|
||||
<CommandInput placeholder="Type a command or search..." />
|
||||
<CommandList className="max-h-[450px]">
|
||||
<CommandEmpty>No results found.</CommandEmpty>
|
||||
<CommandGroup heading="Navigation">
|
||||
<CommandItem onSelect={() => runCommand(() => router.push("/dashboard"))}>
|
||||
<LayoutDashboard className="mr-2 h-4 w-4" />
|
||||
<span>Dashboard</span>
|
||||
</CommandItem>
|
||||
<CommandItem onSelect={() => runCommand(() => router.push("/dashboard/orders"))}>
|
||||
<ShoppingCart className="mr-2 h-4 w-4" />
|
||||
<span>Manage Orders</span>
|
||||
</CommandItem>
|
||||
<CommandItem onSelect={() => runCommand(() => router.push("/dashboard/stock"))}>
|
||||
<Package className="mr-2 h-4 w-4" />
|
||||
<span>Inventory & Stock</span>
|
||||
</CommandItem>
|
||||
<CommandItem onSelect={() => runCommand(() => router.push("/dashboard/customers"))}>
|
||||
<Users className="mr-2 h-4 w-4" />
|
||||
<span>Customers</span>
|
||||
</CommandItem>
|
||||
<CommandItem onSelect={() => runCommand(() => router.push("/dashboard/analytics"))}>
|
||||
<BarChart3 className="mr-2 h-4 w-4" />
|
||||
<span>Advanced Analytics</span>
|
||||
</CommandItem>
|
||||
<CommandItem onSelect={() => runCommand(() => router.push("/dashboard/shipping"))}>
|
||||
<Truck className="mr-2 h-4 w-4" />
|
||||
<span>Shipping Options</span>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
|
||||
<CommandSeparator />
|
||||
|
||||
<CommandGroup heading="Dashboard Actions">
|
||||
<CommandItem onSelect={() => runCommand(() => window.location.reload())}>
|
||||
<RefreshCcw className="mr-2 h-4 w-4" />
|
||||
<span>Reload Data</span>
|
||||
</CommandItem>
|
||||
{onResetLayout && (
|
||||
<CommandItem onSelect={() => runCommand(onResetLayout)}>
|
||||
<RotateCcw className="mr-2 h-4 w-4" />
|
||||
<span>Reset Widget Layout</span>
|
||||
</CommandItem>
|
||||
)}
|
||||
</CommandGroup>
|
||||
|
||||
{availableWidgets && availableWidgets.length > 0 && (
|
||||
<>
|
||||
<CommandSeparator />
|
||||
<CommandGroup heading="Toggle Widgets">
|
||||
{availableWidgets.map((widget) => (
|
||||
<CommandItem
|
||||
key={widget.id}
|
||||
onSelect={() => runCommand(() => onToggleWidget?.(widget.id))}
|
||||
>
|
||||
<div className={`mr-2 h-2 w-2 rounded-full ${widget.visible ? "bg-primary" : "bg-muted"}`} />
|
||||
<span>{widget.visible ? "Hide" : "Show"} {widget.title}</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</>
|
||||
)}
|
||||
|
||||
<CommandSeparator />
|
||||
|
||||
<CommandGroup heading="Settings">
|
||||
<CommandItem onSelect={() => runCommand(() => router.push("/dashboard/settings"))}>
|
||||
<Settings className="mr-2 h-4 w-4" />
|
||||
<span>General Settings</span>
|
||||
<CommandShortcut>⌘S</CommandShortcut>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</CommandDialog>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,43 +8,47 @@ import { WidgetSettings } from "./widget-settings"
|
||||
import { WidgetSettingsModal } from "./widget-settings-modal"
|
||||
import { DashboardEditor } from "./dashboard-editor"
|
||||
import { DraggableWidget } from "./draggable-widget"
|
||||
import { CommandPalette } from "./command-palette"
|
||||
import RevenueWidget from "./revenue-widget"
|
||||
import LowStockWidget from "./low-stock-widget"
|
||||
import RecentCustomersWidget from "./recent-customers-widget"
|
||||
import { ProductPeek } from "./product-peek"
|
||||
import PendingChatsWidget from "./pending-chats-widget"
|
||||
import { getGreeting } from "@/lib/utils/general"
|
||||
import { statsConfig } from "@/config/dashboard"
|
||||
import { getRandomQuote } from "@/config/quotes"
|
||||
import type { OrderStatsData } from "@/lib/types"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/common/card"
|
||||
import { ShoppingCart, RefreshCcw, ArrowRight } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { useToast } from "@/components/ui/use-toast"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { Button } from "@/components/common/button"
|
||||
import { useToast } from "@/components/common/use-toast"
|
||||
import { Skeleton } from "@/components/common/skeleton"
|
||||
import { clientFetch } from "@/lib/api"
|
||||
import { motion } from "framer-motion"
|
||||
import Link from "next/link"
|
||||
import { useWidgetLayout, WidgetConfig } from "@/hooks/useWidgetLayout"
|
||||
import { useWidgetLayout } from "@/lib/hooks/useWidgetLayout"
|
||||
import type { TopProduct, WidgetConfig } from "@/lib/types/dashboard"
|
||||
|
||||
interface ContentProps {
|
||||
username: string
|
||||
orderStats: OrderStatsData
|
||||
}
|
||||
|
||||
interface TopProduct {
|
||||
id: string;
|
||||
name: string;
|
||||
price: number | number[];
|
||||
image: string;
|
||||
count: number;
|
||||
revenue: number;
|
||||
}
|
||||
// TopProduct interface moved to @/lib/types/dashboard
|
||||
|
||||
export default function Content({ username, orderStats }: ContentProps) {
|
||||
const [greeting, setGreeting] = useState("");
|
||||
const [topProducts, setTopProducts] = useState<TopProduct[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [errorQuery, setErrorQuery] = useState(false);
|
||||
const [selectedProductId, setSelectedProductId] = useState<string | null>(null);
|
||||
const [isProductPeekOpen, setIsProductPeekOpen] = useState(false);
|
||||
const [trends, setTrends] = useState<any[]>([]);
|
||||
|
||||
const handleProductPeek = (id: string) => {
|
||||
setSelectedProductId(id);
|
||||
setIsProductPeekOpen(true);
|
||||
};
|
||||
const { toast } = useToast();
|
||||
const { widgets, toggleWidget, moveWidget, reorderWidgets, resetLayout, isWidgetVisible, updateWidgetSettings } = useWidgetLayout();
|
||||
const [configuredWidget, setConfiguredWidget] = useState<WidgetConfig | null>(null);
|
||||
@@ -59,14 +63,25 @@ export default function Content({ username, orderStats }: ContentProps) {
|
||||
|
||||
const fetchTopProducts = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setLoading(true);
|
||||
const data = await clientFetch('/orders/top-products');
|
||||
setTopProducts(data);
|
||||
} catch (err) {
|
||||
console.error("Error fetching top products:", err);
|
||||
setError(err instanceof Error ? err.message : "Failed to fetch top products");
|
||||
setErrorQuery(true);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchTrends = async () => {
|
||||
try {
|
||||
const data = await clientFetch('/analytics/revenue-trends?period=30');
|
||||
if (Array.isArray(data)) {
|
||||
setTrends(data);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error fetching trends:", err);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -88,16 +103,35 @@ export default function Content({ username, orderStats }: ContentProps) {
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-sm font-semibold uppercase tracking-wider text-muted-foreground ml-1">Overview</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{statsConfig.map((stat, index) => (
|
||||
<OrderStats
|
||||
key={stat.title}
|
||||
title={stat.title}
|
||||
value={orderStats[stat.key as keyof OrderStatsData]?.toLocaleString() || "0"}
|
||||
icon={stat.icon}
|
||||
index={index}
|
||||
filterStatus={stat.filterStatus}
|
||||
/>
|
||||
))}
|
||||
{statsConfig.map((stat, index) => {
|
||||
const colors = ["#8884d8", "#10b981", "#3b82f6", "#f59e0b"];
|
||||
const val = Number(orderStats[stat.key as keyof OrderStatsData]) || 0;
|
||||
|
||||
// Map real trend data if available, otherwise fallback to empty (or subtle random if just started)
|
||||
let trendData = trends.map(t => ({
|
||||
value: stat.key === "revenue" ? t.revenue : t.orders
|
||||
})).slice(-12); // Last 12 points
|
||||
|
||||
// Fallback for demo/new stores if no trends yet
|
||||
if (trendData.length === 0) {
|
||||
trendData = Array.from({ length: 12 }, (_, i) => ({
|
||||
value: Math.max(0, val * (0.8 + Math.random() * 0.4 + (i / 20)))
|
||||
}));
|
||||
}
|
||||
|
||||
return (
|
||||
<OrderStats
|
||||
key={stat.title}
|
||||
title={stat.title}
|
||||
value={orderStats[stat.key as keyof OrderStatsData]?.toLocaleString() || "0"}
|
||||
icon={stat.icon}
|
||||
index={index}
|
||||
filterStatus={stat.filterStatus}
|
||||
trendData={trendData}
|
||||
trendColor={colors[index % colors.length]}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
@@ -118,7 +152,7 @@ export default function Content({ username, orderStats }: ContentProps) {
|
||||
<CardTitle>Top Performing Listings</CardTitle>
|
||||
<CardDescription>Your products with the highest sales volume</CardDescription>
|
||||
</div>
|
||||
{error && (
|
||||
{errorQuery && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -131,7 +165,7 @@ export default function Content({ username, orderStats }: ContentProps) {
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isLoading ? (
|
||||
{loading ? (
|
||||
<div className="space-y-4">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<div key={i} className="flex items-center gap-4">
|
||||
@@ -144,7 +178,7 @@ export default function Content({ username, orderStats }: ContentProps) {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : error ? (
|
||||
) : errorQuery ? (
|
||||
<div className="py-12 text-center">
|
||||
<div className="text-muted-foreground mb-4">Failed to load product insights</div>
|
||||
</div>
|
||||
@@ -164,7 +198,8 @@ export default function Content({ username, orderStats }: ContentProps) {
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.1 + index * 0.05 }}
|
||||
className="flex items-center gap-4 p-3 rounded-xl hover:bg-muted/50 transition-colors group"
|
||||
className="flex items-center gap-4 p-3 rounded-xl hover:bg-white/5 transition-colors group cursor-pointer"
|
||||
onClick={() => handleProductPeek(product.id)}
|
||||
>
|
||||
<div
|
||||
className="h-14 w-14 bg-muted bg-cover bg-center rounded-xl border flex-shrink-0 flex items-center justify-center overflow-hidden group-hover:scale-105 transition-transform"
|
||||
@@ -179,15 +214,19 @@ export default function Content({ username, orderStats }: ContentProps) {
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-grow min-w-0">
|
||||
<h4 className="font-semibold text-lg truncate group-hover:text-primary transition-colors">{product.name}</h4>
|
||||
<div className="flex items-center gap-3 mt-0.5">
|
||||
<span className="text-sm text-muted-foreground font-medium">£{(Number(Array.isArray(product.price) ? product.price[0] : product.price) || 0).toFixed(2)}</span>
|
||||
</div>
|
||||
<h4 className="font-semibold text-lg truncate group-hover:text-primary transition-colors pr-4">{product.name}</h4>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-xl font-bold">{product.count}</div>
|
||||
<div className="text-xs text-muted-foreground font-medium uppercase tracking-tighter mb-1">Units Sold</div>
|
||||
<div className="text-sm font-semibold text-primary">£{product.revenue.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}</div>
|
||||
<div className="flex shrink-0 items-center gap-6 pr-4 text-right">
|
||||
<div className="flex flex-col items-end">
|
||||
<span className={`text-xl font-bold leading-none ${product.currentStock && product.currentStock <= 5 ? 'text-amber-500' : 'text-muted-foreground'}`}>
|
||||
{product.currentStock ?? 0}
|
||||
</span>
|
||||
<span className="text-[10px] text-muted-foreground font-bold uppercase tracking-widest mt-1">Stock</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-end">
|
||||
<span className="text-2xl font-black text-white leading-none">{product.count}</span>
|
||||
<span className="text-[10px] text-muted-foreground font-bold uppercase tracking-widest mt-1">Sold</span>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
@@ -213,10 +252,21 @@ export default function Content({ username, orderStats }: ContentProps) {
|
||||
useEffect(() => {
|
||||
setGreeting(getGreeting());
|
||||
fetchTopProducts();
|
||||
fetchTrends();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="space-y-10 pb-10">
|
||||
<div className="space-y-8 pb-10">
|
||||
<ProductPeek
|
||||
productId={selectedProductId}
|
||||
open={isProductPeekOpen}
|
||||
onOpenChange={setIsProductPeekOpen}
|
||||
/>
|
||||
<CommandPalette
|
||||
onResetLayout={resetLayout}
|
||||
onToggleWidget={toggleWidget}
|
||||
availableWidgets={widgets}
|
||||
/>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
@@ -248,7 +298,7 @@ export default function Content({ username, orderStats }: ContentProps) {
|
||||
onReorder={reorderWidgets}
|
||||
onReset={resetLayout}
|
||||
>
|
||||
<div className="space-y-10">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
{widgets.map((widget) => {
|
||||
if (!widget.visible) return null;
|
||||
|
||||
@@ -280,3 +330,5 @@ export default function Content({ username, orderStats }: ContentProps) {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -18,9 +18,9 @@ import {
|
||||
verticalListSortingStrategy,
|
||||
arrayMove,
|
||||
} from "@dnd-kit/sortable"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Button } from "@/components/common/button"
|
||||
import { Edit3, X, Check, RotateCcw } from "lucide-react"
|
||||
import { WidgetConfig } from "@/hooks/useWidgetLayout"
|
||||
import { WidgetConfig } from "@/lib/types/dashboard"
|
||||
import { motion, AnimatePresence } from "framer-motion"
|
||||
|
||||
interface DashboardEditorProps {
|
||||
@@ -126,3 +126,5 @@ export function DashboardEditor({
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ import React, { useState, useEffect, useRef } from "react"
|
||||
import { useSortable } from "@dnd-kit/sortable"
|
||||
import { CSS } from "@dnd-kit/utilities"
|
||||
import { GripVertical, Settings, X, Eye, EyeOff } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Button } from "@/components/common/button"
|
||||
import { cn } from "@/lib/utils/styles"
|
||||
import { WidgetConfig } from "@/hooks/useWidgetLayout"
|
||||
import { WidgetConfig } from "@/lib/types/dashboard"
|
||||
|
||||
interface DraggableWidgetProps {
|
||||
widget: WidgetConfig
|
||||
@@ -47,6 +47,7 @@ export function DraggableWidget({
|
||||
style={style}
|
||||
className={cn(
|
||||
"relative group",
|
||||
widget.colSpan === 2 ? "lg:col-span-2" : "lg:col-span-1",
|
||||
isEditMode && "ring-2 ring-primary ring-offset-2 ring-offset-background rounded-lg",
|
||||
isDragging && "z-50 shadow-2xl"
|
||||
)}
|
||||
@@ -110,3 +111,5 @@ export function DraggableWidget({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/common/card"
|
||||
import { Button } from "@/components/common/button"
|
||||
import { Skeleton } from "@/components/common/skeleton"
|
||||
import { AlertCircle, Package, ArrowRight, ShoppingCart } from "lucide-react"
|
||||
import { clientFetch } from "@/lib/api"
|
||||
import Image from "next/image"
|
||||
@@ -165,3 +165,5 @@ export default function LowStockWidget({ settings }: LowStockWidgetProps) {
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
226
components/dashboard/order-peek.tsx
Normal file
226
components/dashboard/order-peek.tsx
Normal file
@@ -0,0 +1,226 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetDescription
|
||||
} from "@/components/common/sheet"
|
||||
import { clientFetch } from "@/lib/api"
|
||||
import { Badge } from "@/components/common/badge"
|
||||
import { Separator } from "@/components/common/separator"
|
||||
import { ScrollArea } from "@/components/common/scroll-area"
|
||||
import { Package, Truck, CreditCard, Calendar, User, MapPin, ExternalLink } from "lucide-react"
|
||||
import { RelativeTime } from "@/components/common/relative-time"
|
||||
import { formatGBP } from "@/lib/utils/format"
|
||||
import Link from "next/link"
|
||||
import { Button } from "@/components/common/button"
|
||||
import { Skeleton } from "@/components/common/skeleton"
|
||||
|
||||
interface OrderPeekProps {
|
||||
orderId: string | null
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
export function OrderPeek({ orderId, open, onOpenChange }: OrderPeekProps) {
|
||||
const [order, setOrder] = useState<any>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (open && orderId) {
|
||||
fetchOrderDetails()
|
||||
} else if (!open) {
|
||||
// Don't clear order immediately to avoid flicker during closing
|
||||
// setOrder(null)
|
||||
}
|
||||
}, [open, orderId])
|
||||
|
||||
const fetchOrderDetails = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const data = await clientFetch(`/orders/${orderId}`)
|
||||
setOrder(data.order)
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch order details for peek:", error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusStyle = (status: string) => {
|
||||
switch (status) {
|
||||
case 'paid': return 'bg-emerald-500/10 text-emerald-500 border-emerald-500/20';
|
||||
case 'shipped': return 'bg-blue-500/10 text-blue-500 border-blue-500/20';
|
||||
case 'completed': return 'bg-green-500/10 text-green-500 border-green-500/20';
|
||||
case 'cancelled': return 'bg-red-500/10 text-red-500 border-red-500/20';
|
||||
case 'unpaid': return 'bg-yellow-500/10 text-yellow-500 border-yellow-500/20';
|
||||
default: return 'bg-gray-500/10 text-gray-500 border-gray-500/20';
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||
<SheetContent className="sm:max-w-md border-white/5 bg-[#0a0a0a]/80 backdrop-blur-2xl p-0 overflow-hidden flex flex-col shadow-2xl shadow-black/50">
|
||||
<SheetHeader className="p-6 border-b border-white/5 bg-white/5">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<SheetTitle className="text-xl font-bold flex items-center gap-2">
|
||||
<Package className="h-5 w-5 text-primary" />
|
||||
Order Details
|
||||
</SheetTitle>
|
||||
{order && (
|
||||
<Link href={`/dashboard/orders/${order._id}`} onClick={() => onOpenChange(false)}>
|
||||
<Button variant="ghost" size="sm" className="h-8 gap-1.5 text-xs text-muted-foreground hover:text-primary">
|
||||
Open Full <ExternalLink className="h-3 w-3" />
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<SheetDescription asChild>
|
||||
<div className="flex items-center gap-2 text-white font-mono bg-white/5 px-2.5 py-1 rounded-lg border border-white/10 w-fit mt-1">
|
||||
{loading ? <Skeleton className="h-4 w-32" /> : order ? `Order #${order.orderId}` : "Loading..."}
|
||||
</div>
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
|
||||
<ScrollArea className="flex-1">
|
||||
{loading ? (
|
||||
<div className="p-6 space-y-6">
|
||||
<div className="space-y-3">
|
||||
<Skeleton className="h-4 w-24" />
|
||||
<Skeleton className="h-8 w-full" />
|
||||
</div>
|
||||
<Separator className="bg-white/5" />
|
||||
<div className="space-y-4">
|
||||
{[1, 2, 3].map(i => <Skeleton key={i} className="h-12 w-full" />)}
|
||||
</div>
|
||||
</div>
|
||||
) : order ? (
|
||||
<div className="p-6 space-y-8">
|
||||
{/* Summary Stats */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="p-4 rounded-2xl bg-white/5 border border-white/5 space-y-1">
|
||||
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground">Status</span>
|
||||
<div className="flex pt-1">
|
||||
<Badge variant="outline" className={`${getStatusStyle(order.status)} border-none px-0 text-3xl font-black capitalize tracking-tight`}>
|
||||
{order.status}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 rounded-2xl bg-white/5 border border-white/5 space-y-1">
|
||||
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground">Total Value</span>
|
||||
<div className="text-lg font-bold text-primary tabular-nums">
|
||||
{formatGBP(order.totalPrice)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Customer Info */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm font-bold flex items-center gap-2 text-muted-foreground uppercase tracking-widest">
|
||||
<User className="h-4 w-4" /> Customer Information
|
||||
</h3>
|
||||
<div className="p-4 rounded-2xl bg-white/5 border border-white/5 space-y-3">
|
||||
<div className="flex justify-between items-start text-sm">
|
||||
<span className="text-muted-foreground pt-1">Customer</span>
|
||||
<div className="flex flex-col items-end bg-white/5 p-3 rounded-xl border border-white/5 min-w-[160px]">
|
||||
<span className="font-black text-white text-base">
|
||||
{order.telegramUsername ? `@${order.telegramUsername}` : "Anonymous"}
|
||||
</span>
|
||||
<span className="text-[10px] text-muted-foreground font-mono uppercase tracking-tighter mt-1 opacity-60">
|
||||
ID: {order.telegramBuyerId?.slice(0, 16) || "N/A"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between items-center text-sm">
|
||||
<span className="text-muted-foreground">Order Date</span>
|
||||
<span className="text-white">
|
||||
{order.orderDate ? <RelativeTime date={order.orderDate} /> : "N/A"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Order Items */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm font-bold flex items-center gap-2 text-muted-foreground uppercase tracking-widest">
|
||||
<Package className="h-4 w-4" /> Order Items
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{order.products?.map((item: any, idx: number) => (
|
||||
<div key={idx} className="flex items-center justify-between p-3 rounded-xl bg-white/5 border border-white/5 hover:bg-white/10 transition-colors">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-medium text-white truncate max-w-[200px]">
|
||||
{item.name || `Item ${idx + 1}`}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">Qty: {item.quantity}</span>
|
||||
</div>
|
||||
<span className="text-sm font-bold tabular-nums">{formatGBP(item.totalItemPrice)}</span>
|
||||
</div>
|
||||
))}
|
||||
{order.shippingMethod && (
|
||||
<div className="flex items-center justify-between p-3 rounded-xl bg-primary/5 border border-primary/10">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm font-medium text-primary">Shipping: {order.shippingMethod.type}</span>
|
||||
<span className="text-xs text-primary/60 italic">Standard Delivery</span>
|
||||
</div>
|
||||
<span className="text-sm font-bold text-primary tabular-nums">{formatGBP(order.shippingMethod.price)}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Shipping Address Peek */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm font-bold flex items-center gap-2 text-muted-foreground uppercase tracking-widest">
|
||||
<MapPin className="h-4 w-4" /> Delivery Address
|
||||
</h3>
|
||||
<div className="p-4 rounded-2xl bg-[#050505] border border-white/10 overflow-hidden shadow-inner">
|
||||
<p className="text-xs text-muted-foreground leading-relaxed break-all font-mono opacity-70 selection:bg-primary/30">
|
||||
{order.pgpAddress || "No address provided or encrypted."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Timeline Indicator */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm font-bold flex items-center gap-2 text-muted-foreground uppercase tracking-widest">
|
||||
<Calendar className="h-4 w-4" /> Next Milestone
|
||||
</h3>
|
||||
<div className="p-4 rounded-2xl bg-amber-500/5 border border-amber-500/20">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-full bg-amber-500/20 text-amber-500">
|
||||
<Truck className="h-4 w-4" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-bold text-amber-500">Pending Shipment</p>
|
||||
<p className="text-xs text-amber-500/60">Awaiting vendor processing and label creation.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-12 text-center text-muted-foreground">
|
||||
Failed to load order data.
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea>
|
||||
|
||||
{order && (
|
||||
<div className="p-6 border-t border-white/5 bg-white/5 mt-auto">
|
||||
<Link href={`/dashboard/orders/${order._id}`} className="w-full" onClick={() => onOpenChange(false)}>
|
||||
<Button className="w-full font-bold h-12 shadow-xl shadow-primary/10">
|
||||
Full Order Management
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { LucideIcon } from "lucide-react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/common/card"
|
||||
import { motion } from "framer-motion"
|
||||
import Link from "next/link"
|
||||
import { ResponsiveContainer, LineChart, Line } from "recharts"
|
||||
|
||||
interface OrderStatsProps {
|
||||
title: string
|
||||
@@ -12,6 +13,10 @@ interface OrderStatsProps {
|
||||
filterStatus?: string
|
||||
/** Custom href if not using filterStatus */
|
||||
href?: string
|
||||
/** Data for sparkline [ { value: number }, ... ] */
|
||||
trendData?: { value: number }[]
|
||||
/** Color for the sparkline */
|
||||
trendColor?: string
|
||||
}
|
||||
|
||||
export default function OrderStats({
|
||||
@@ -20,7 +25,9 @@ export default function OrderStats({
|
||||
icon: Icon,
|
||||
index = 0,
|
||||
filterStatus,
|
||||
href
|
||||
href,
|
||||
trendData,
|
||||
trendColor = "#8884d8"
|
||||
}: OrderStatsProps) {
|
||||
const linkHref = href || (filterStatus ? `/dashboard/orders?status=${filterStatus}` : undefined)
|
||||
|
||||
@@ -45,8 +52,26 @@ export default function OrderStats({
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="relative z-10">
|
||||
<div className="text-3xl font-bold tracking-tight">{value}</div>
|
||||
<div className="mt-1 h-1 w-0 bg-primary/20 group-hover:w-full transition-all duration-500 rounded-full" />
|
||||
<div className="flex items-end justify-between gap-2">
|
||||
<div className="text-3xl font-bold tracking-tight">{value}</div>
|
||||
{trendData && trendData.length > 0 && (
|
||||
<div className="h-10 w-24 mb-1">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={trendData}>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="value"
|
||||
stroke={trendColor}
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
isAnimationActive={true}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-2 h-1 w-0 bg-primary/20 group-hover:w-full transition-all duration-500 rounded-full" />
|
||||
{linkHref && (
|
||||
<div className="mt-2 text-xs text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
Click to view →
|
||||
@@ -59,3 +84,4 @@ export default function OrderStats({
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Skeleton } from "@/components/common/skeleton";
|
||||
import { Card } from "@/components/common/card";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { SnowLoader } from "@/components/snow-loader";
|
||||
|
||||
@@ -113,4 +113,4 @@ export default function PageLoading({
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/common/card"
|
||||
import { Button } from "@/components/common/button"
|
||||
import { Skeleton } from "@/components/common/skeleton"
|
||||
import { MessageSquare, MessageCircle, ArrowRight, Clock } from "lucide-react"
|
||||
import { clientFetch, getCookie } from "@/lib/api"
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
|
||||
import { Avatar, AvatarFallback } from "@/components/common/avatar"
|
||||
import Link from "next/link"
|
||||
import { RelativeTime } from "@/components/ui/relative-time"
|
||||
import { RelativeTime } from "@/components/common/relative-time"
|
||||
|
||||
interface PendingChatsWidgetProps {
|
||||
settings?: {
|
||||
@@ -170,3 +170,5 @@ export default function PendingChatsWidget({ settings }: PendingChatsWidgetProps
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
191
components/dashboard/product-peek.tsx
Normal file
191
components/dashboard/product-peek.tsx
Normal file
@@ -0,0 +1,191 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetDescription
|
||||
} from "@/components/common/sheet"
|
||||
import { clientFetch } from "@/lib/api"
|
||||
import { Badge } from "@/components/common/badge"
|
||||
import { Separator } from "@/components/common/separator"
|
||||
import { ScrollArea } from "@/components/common/scroll-area"
|
||||
import { ShoppingCart, TrendingUp, BarChart3, Package, History, ExternalLink, AlertCircle } from "lucide-react"
|
||||
import { formatGBP } from "@/lib/utils/format"
|
||||
import Link from "next/link"
|
||||
import { Button } from "@/components/common/button"
|
||||
import { Skeleton } from "@/components/common/skeleton"
|
||||
|
||||
interface ProductPeekProps {
|
||||
productId: string | null
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
export function ProductPeek({ productId, open, onOpenChange }: ProductPeekProps) {
|
||||
const [product, setProduct] = useState<any>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (open && productId) {
|
||||
fetchProductDetails()
|
||||
}
|
||||
}, [open, productId])
|
||||
|
||||
const fetchProductDetails = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
// In this app, productId might be the ID used in the list,
|
||||
// which corresponds to the mongo _id or a semantic ID.
|
||||
const data = await clientFetch(`/products/${productId}`)
|
||||
setProduct(data)
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch product details for peek:", error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||
<SheetContent className="sm:max-w-md border-white/5 bg-[#0a0a0a]/80 backdrop-blur-2xl p-0 overflow-hidden flex flex-col">
|
||||
<SheetHeader className="p-6 border-b border-white/5 bg-white/5">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<SheetTitle className="text-xl font-bold flex items-center gap-2">
|
||||
<ShoppingCart className="h-5 w-5 text-primary" />
|
||||
Product Insight
|
||||
</SheetTitle>
|
||||
{product && (
|
||||
<Link href={`/dashboard/stock`} onClick={() => onOpenChange(false)}>
|
||||
<Button variant="ghost" size="sm" className="h-8 gap-1.5 text-xs text-muted-foreground hover:text-primary">
|
||||
Manage <ExternalLink className="h-3 w-3" />
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<SheetDescription asChild>
|
||||
<div className="flex items-center gap-2 text-white font-medium bg-white/5 px-2.5 py-1 rounded-lg border border-white/10 w-fit mt-1">
|
||||
{loading ? <Skeleton className="h-4 w-32" /> : product ? product.name : "Loading..."}
|
||||
</div>
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
|
||||
<ScrollArea className="flex-1">
|
||||
{loading ? (
|
||||
<div className="p-6 space-y-6">
|
||||
<div className="flex gap-4">
|
||||
<Skeleton className="h-24 w-24 rounded-2xl" />
|
||||
<div className="space-y-2 flex-1">
|
||||
<Skeleton className="h-4 w-full" />
|
||||
<Skeleton className="h-4 w-2/3" />
|
||||
</div>
|
||||
</div>
|
||||
<Separator className="bg-white/5" />
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Skeleton className="h-20 w-full rounded-2xl" />
|
||||
<Skeleton className="h-20 w-full rounded-2xl" />
|
||||
</div>
|
||||
</div>
|
||||
) : product ? (
|
||||
<div className="p-6 space-y-8">
|
||||
{/* Product Visual & Basic Info */}
|
||||
<div className="flex gap-6 items-start">
|
||||
<div
|
||||
className="h-24 w-24 bg-muted bg-cover bg-center rounded-2xl border border-white/10 shadow-xl flex-shrink-0"
|
||||
style={{
|
||||
backgroundImage: product._id
|
||||
? `url(/api/products/${product._id}/image)`
|
||||
: 'none'
|
||||
}}
|
||||
/>
|
||||
<div className="space-y-2">
|
||||
<Badge variant="outline" className="bg-primary/10 text-primary border-primary/20 capitalize">
|
||||
{product.type || "Physical"}
|
||||
</Badge>
|
||||
<h2 className="text-lg font-bold text-white leading-tight">{product.name}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Inventory Stats */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="p-4 rounded-2xl bg-white/5 border border-white/5 space-y-1">
|
||||
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground">Current Stock</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`text-xl font-bold ${(product.currentStock || 0) <= 5 ? 'text-rose-500' : 'text-white'}`}>
|
||||
{product.currentStock || 0}
|
||||
</span>
|
||||
{(product.currentStock || 0) <= 5 && <AlertCircle className="h-4 w-4 text-rose-500 animate-pulse" />}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 rounded-2xl bg-white/5 border border-white/5 space-y-1">
|
||||
<span className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground">Unit Type</span>
|
||||
<div className="text-lg font-bold text-white capitalize">
|
||||
{product.unitType || "Units"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Analytics Section */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm font-bold flex items-center gap-2 text-muted-foreground uppercase tracking-widest">
|
||||
<BarChart3 className="h-4 w-4" /> Performance Metrics
|
||||
</h3>
|
||||
<div className="p-4 rounded-2xl bg-white/5 border border-white/5 space-y-4">
|
||||
<div className="flex justify-between items-center text-sm">
|
||||
<span className="text-muted-foreground">Type</span>
|
||||
<span className="font-medium text-white">{product.type || "Physical"}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center text-sm">
|
||||
<span className="text-muted-foreground">Rating</span>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="flex text-amber-500">
|
||||
{"★".repeat(Math.round(product.rating || 5))}
|
||||
</div>
|
||||
<span className="font-bold text-white">({product.rating || "5.0"})</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status & Settings */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm font-bold flex items-center gap-2 text-muted-foreground uppercase tracking-widest">
|
||||
<TrendingUp className="h-4 w-4" /> Market Visibility
|
||||
</h3>
|
||||
<div className="p-4 rounded-2xl bg-emerald-500/5 border border-emerald-500/20">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-full bg-emerald-500/20 text-emerald-500">
|
||||
<TrendingUp className="h-4 w-4" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-bold text-emerald-500">Active Listing</p>
|
||||
<p className="text-xs text-emerald-500/60">This product is currently visible to all buyers.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div className="grid grid-cols-1 gap-3">
|
||||
<Link href={`/dashboard/stock`} onClick={() => onOpenChange(false)} className="w-full">
|
||||
<Button variant="outline" className="w-full justify-start gap-3 h-12 border-white/5 bg-white/5 hover:bg-white/10 group">
|
||||
<Package className="h-4 w-4 text-primary group-hover:scale-110 transition-transform" />
|
||||
Update Inventory Levels
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-12 text-center text-muted-foreground">
|
||||
Failed to load product details.
|
||||
</div>
|
||||
)}
|
||||
</ScrollArea>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { z } from 'zod';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { Save, X, Loader2 } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Button } from '@/components/common/button';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -14,12 +14,12 @@ import {
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { toast } from '@/components/ui/use-toast';
|
||||
} from '@/components/common/form';
|
||||
import { Input } from '@/components/common/input';
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/common/radio-group";
|
||||
import { Switch } from '@/components/common/switch';
|
||||
import { Textarea } from '@/components/common/textarea';
|
||||
import { toast } from '@/components/common/use-toast';
|
||||
import { Promotion, PromotionFormData } from '@/lib/types/promotion';
|
||||
import { fetchClient } from '@/lib/api';
|
||||
import dynamic from 'next/dynamic';
|
||||
@@ -464,4 +464,5 @@ export default function EditPromotionForm({ promotion, onSuccess, onCancel }: Ed
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { z } from 'zod';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { Plus, Save, X, Loader2 } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Button } from '@/components/common/button';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -14,15 +14,15 @@ import {
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { toast } from '@/components/ui/use-toast';
|
||||
} from '@/components/common/form';
|
||||
import { Input } from '@/components/common/input';
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/common/radio-group";
|
||||
import { Switch } from '@/components/common/switch';
|
||||
import { Textarea } from '@/components/common/textarea';
|
||||
import { toast } from '@/components/common/use-toast';
|
||||
import { PromotionFormData } from '@/lib/types/promotion';
|
||||
import { fetchClient } from '@/lib/api';
|
||||
import { DatePicker } from '@/components/ui/date-picker';
|
||||
import { DatePicker } from '@/components/common/date-picker';
|
||||
import dynamic from 'next/dynamic';
|
||||
const ProductSelector = dynamic(() => import('./ProductSelector'));
|
||||
|
||||
@@ -459,4 +459,5 @@ export default function NewPromotionForm({ onSuccess, onCancel }: NewPromotionFo
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { Check, ChevronDown, X, Search } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from '@/components/ui/command';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Button } from '@/components/common/button';
|
||||
import { Input } from '@/components/common/input';
|
||||
import { Badge } from '@/components/common/badge';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/common/popover';
|
||||
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from '@/components/common/command';
|
||||
import { ScrollArea } from '@/components/common/scroll-area';
|
||||
import { Product } from '@/lib/types/promotion';
|
||||
import { fetchClient } from '@/lib/api';
|
||||
|
||||
@@ -248,4 +248,5 @@ export default function ProductSelector({
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,11 +7,11 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription
|
||||
} from '@/components/ui/dialog';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
} from '@/components/common/dialog';
|
||||
import { Badge } from '@/components/common/badge';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/common/card';
|
||||
import { Separator } from '@/components/common/separator';
|
||||
import { ScrollArea } from '@/components/common/scroll-area';
|
||||
import {
|
||||
Calendar,
|
||||
Target,
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { Promotion, Product } from '@/lib/types/promotion';
|
||||
import { fetchClient } from '@/lib/api';
|
||||
import { toast } from '@/components/ui/use-toast';
|
||||
import { toast } from '@/components/common/use-toast';
|
||||
|
||||
interface PromotionDetailsModalProps {
|
||||
promotion: Promotion | null;
|
||||
@@ -382,4 +382,5 @@ export default function PromotionDetailsModal({
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Plus, Tag, RefreshCw, Trash, Edit, Check, X, Eye } from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Button } from '@/components/common/button';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -11,14 +11,14 @@ import {
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
} from '@/components/common/table';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
} from '@/components/common/card';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -27,9 +27,9 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog';
|
||||
import { toast } from '@/components/ui/use-toast';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
} from '@/components/common/dialog';
|
||||
import { toast } from '@/components/common/use-toast';
|
||||
import { Badge } from '@/components/common/badge';
|
||||
import { Promotion } from '@/lib/types/promotion';
|
||||
import { fetchClient } from '@/lib/api';
|
||||
import dynamic from 'next/dynamic';
|
||||
@@ -306,4 +306,5 @@ export default function PromotionsList() {
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,9 @@ import {
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
} from '@/components/common/table';
|
||||
import { Card, CardContent } from '@/components/common/card';
|
||||
import { Skeleton } from '@/components/common/skeleton';
|
||||
|
||||
export default function PromotionsPageSkeleton() {
|
||||
return (
|
||||
@@ -59,4 +59,4 @@ export default function PromotionsPageSkeleton() {
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,10 @@ import {
|
||||
BarChart3,
|
||||
MessageSquare,
|
||||
} from "lucide-react"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Card, CardContent } from "@/components/common/card"
|
||||
import dynamic from "next/dynamic"
|
||||
import { Product } from "@/models/products"
|
||||
import { Category } from "@/models/categories"
|
||||
import { Product } from "@/lib/models/products"
|
||||
import { Category } from "@/lib/models/categories"
|
||||
import { clientFetch } from "@/lib/api"
|
||||
import { toast } from "sonner"
|
||||
|
||||
@@ -217,3 +217,5 @@ export default function QuickActions() {
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
import { useState, useEffect } from "react"
|
||||
import { motion } from "framer-motion"
|
||||
import { ShoppingBag, CreditCard, Truck, MessageSquare, AlertCircle } from "lucide-react"
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/common/card"
|
||||
import { clientFetch } from "@/lib/api"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { RelativeTime } from "@/components/ui/relative-time"
|
||||
import { Skeleton } from "@/components/common/skeleton"
|
||||
import { RelativeTime } from "@/components/common/relative-time"
|
||||
import Link from "next/link"
|
||||
import { OrderPeek } from "./order-peek"
|
||||
|
||||
interface ActivityItem {
|
||||
_id: string;
|
||||
@@ -21,6 +22,13 @@ interface ActivityItem {
|
||||
export default function RecentActivity() {
|
||||
const [activities, setActivities] = useState<ActivityItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectedOrderId, setSelectedOrderId] = useState<string | null>(null);
|
||||
const [isPeekOpen, setIsPeekOpen] = useState(false);
|
||||
|
||||
const handlePeek = (id: string) => {
|
||||
setSelectedOrderId(id);
|
||||
setIsPeekOpen(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchRecentOrders() {
|
||||
@@ -89,16 +97,19 @@ export default function RecentActivity() {
|
||||
className="flex items-start gap-4 relative"
|
||||
>
|
||||
{index !== activities.length - 1 && (
|
||||
<div className="absolute left-[15px] top-8 bottom-[-24px] w-[2px] bg-border/50" />
|
||||
<div className="absolute left-[17px] top-8 bottom-[-24px] w-[2px] bg-border/50" />
|
||||
)}
|
||||
<div className={`mt-1 p-2 rounded-full z-10 ${getStatusColor(item.status)}`}>
|
||||
{getStatusIcon(item.status)}
|
||||
</div>
|
||||
<div className="flex-1 space-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<Link href={`/dashboard/orders/${item._id}`} className="font-medium hover:underline">
|
||||
<button
|
||||
onClick={() => handlePeek(item._id)}
|
||||
className="font-medium hover:text-primary transition-colors text-left"
|
||||
>
|
||||
Order #{item.orderId}
|
||||
</Link>
|
||||
</button>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
<RelativeTime date={item.orderDate} />
|
||||
</span>
|
||||
@@ -106,14 +117,21 @@ export default function RecentActivity() {
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{item.status === "paid" ? "Payment received" :
|
||||
item.status === "shipped" ? "Order marked as shipped" :
|
||||
`Order status: ${item.status}`} for £{item.totalPrice.toFixed(2)}
|
||||
`Order status: ${item.status}`}
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<OrderPeek
|
||||
orderId={selectedOrderId}
|
||||
open={isPeekOpen}
|
||||
onOpenChange={setIsPeekOpen}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/common/card"
|
||||
import { Button } from "@/components/common/button"
|
||||
import { Skeleton } from "@/components/common/skeleton"
|
||||
import { Users, User, ArrowRight, DollarSign } from "lucide-react"
|
||||
import { getCustomerInsightsWithStore, formatGBP } from "@/lib/api"
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
|
||||
import { Avatar, AvatarFallback } from "@/components/common/avatar"
|
||||
import Link from "next/link"
|
||||
|
||||
interface RecentCustomersWidgetProps {
|
||||
@@ -152,3 +152,5 @@ export default function RecentCustomersWidget({ settings }: RecentCustomersWidge
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/common/card"
|
||||
import { Button } from "@/components/common/button"
|
||||
import { Skeleton } from "@/components/common/skeleton"
|
||||
import { TrendingUp, DollarSign, RefreshCcw } from "lucide-react"
|
||||
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts"
|
||||
import { getRevenueTrendsWithStore, type RevenueData, formatGBP } from "@/lib/api"
|
||||
import { useToast } from "@/components/ui/use-toast"
|
||||
import { useToast } from "@/components/common/use-toast"
|
||||
|
||||
interface RevenueWidgetProps {
|
||||
settings?: {
|
||||
@@ -63,7 +63,6 @@ export default function RevenueWidget({ settings }: RevenueWidgetProps) {
|
||||
})
|
||||
|
||||
// Summary stats
|
||||
const totalRevenue = data.reduce((sum, item) => sum + (item.revenue || 0), 0)
|
||||
const totalOrders = data.reduce((sum, item) => sum + (item.orders || 0), 0)
|
||||
|
||||
const CustomTooltip = ({ active, payload }: any) => {
|
||||
@@ -74,9 +73,6 @@ export default function RevenueWidget({ settings }: RevenueWidgetProps) {
|
||||
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-1">{data.formattedDate}</p>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-bold text-primary">
|
||||
Revenue: {formatGBP(data.revenue)}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Orders: <span className="font-medium text-foreground">{data.orders}</span>
|
||||
</p>
|
||||
</div>
|
||||
@@ -106,7 +102,7 @@ export default function RevenueWidget({ settings }: RevenueWidgetProps) {
|
||||
<div className="space-y-1">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<TrendingUp className="h-5 w-5 text-primary" />
|
||||
Revenue Insights
|
||||
Order Insights
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Performance over the last {days} days
|
||||
@@ -122,15 +118,15 @@ export default function RevenueWidget({ settings }: RevenueWidgetProps) {
|
||||
{error ? (
|
||||
<div className="h-[300px] flex flex-col items-center justify-center text-center p-6">
|
||||
<DollarSign className="h-12 w-12 text-muted-foreground/20 mb-4" />
|
||||
<p className="text-sm text-muted-foreground mb-4">Could not load revenue trends</p>
|
||||
<p className="text-sm text-muted-foreground mb-4">Could not load order trends</p>
|
||||
<Button variant="outline" size="sm" onClick={fetchRevenueData}>Retry</Button>
|
||||
</div>
|
||||
) : chartData.length === 0 ? (
|
||||
<div className="h-[300px] flex flex-col items-center justify-center text-center p-6">
|
||||
<DollarSign className="h-12 w-12 text-muted-foreground/20 mb-4" />
|
||||
<h3 className="text-lg font-medium">No revenue data</h3>
|
||||
<h3 className="text-lg font-medium">No order data</h3>
|
||||
<p className="text-sm text-muted-foreground max-w-xs mt-2">
|
||||
Start making sales to see your revenue trends here.
|
||||
Start making sales to see your order trends here.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
@@ -139,7 +135,7 @@ export default function RevenueWidget({ settings }: RevenueWidgetProps) {
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<AreaChart data={chartData} margin={{ top: 0, right: 0, left: -20, bottom: 0 }}>
|
||||
<defs>
|
||||
<linearGradient id="colorRevenueWidget" x1="0" y1="0" x2="0" y2="1">
|
||||
<linearGradient id="colorOrderWidget" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="hsl(var(--primary))" stopOpacity={0.3} />
|
||||
<stop offset="95%" stopColor="hsl(var(--primary))" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
@@ -156,15 +152,15 @@ export default function RevenueWidget({ settings }: RevenueWidgetProps) {
|
||||
tick={{ fontSize: 11, fill: "hsl(var(--muted-foreground))" }}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tickFormatter={(value) => `£${value >= 1000 ? (value / 1000).toFixed(1) + 'k' : value}`}
|
||||
tickFormatter={(value) => `${value}`}
|
||||
/>
|
||||
<Tooltip content={<CustomTooltip />} />
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="revenue"
|
||||
dataKey="orders"
|
||||
stroke="hsl(var(--primary))"
|
||||
fillOpacity={1}
|
||||
fill="url(#colorRevenueWidget)"
|
||||
fill="url(#colorOrderWidget)"
|
||||
strokeWidth={2.5}
|
||||
activeDot={{ r: 6, strokeWidth: 0, fill: "hsl(var(--primary))" }}
|
||||
/>
|
||||
@@ -172,14 +168,10 @@ export default function RevenueWidget({ settings }: RevenueWidgetProps) {
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 pb-2">
|
||||
<div className="p-4 rounded-2xl bg-primary/5 border border-primary/10">
|
||||
<div className="text-sm text-muted-foreground font-medium mb-1">Total Revenue</div>
|
||||
<div className="text-2xl font-bold text-primary">{formatGBP(totalRevenue)}</div>
|
||||
</div>
|
||||
<div className="p-4 rounded-2xl bg-muted/50 border border-border">
|
||||
<div className="flex justify-center pb-2">
|
||||
<div className="p-4 rounded-2xl bg-muted/50 border border-border w-full max-w-sm text-center">
|
||||
<div className="text-sm text-muted-foreground font-medium mb-1">Total Orders</div>
|
||||
<div className="text-2xl font-bold">{totalOrders}</div>
|
||||
<div className="text-3xl font-bold">{totalOrders}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -188,3 +180,5 @@ export default function RevenueWidget({ settings }: RevenueWidgetProps) {
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Button } from "@/components/common/button"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -9,20 +9,20 @@ import {
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
} from "@/components/common/dialog"
|
||||
import { Label } from "@/components/common/label"
|
||||
import { Input } from "@/components/common/input"
|
||||
import { Switch } from "@/components/common/switch"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
import { WidgetConfig } from "@/hooks/useWidgetLayout"
|
||||
} from "@/components/common/select"
|
||||
import { WidgetConfig } from "@/lib/types/dashboard"
|
||||
import { Settings2 } from "lucide-react"
|
||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
import { ScrollArea } from "@/components/common/scroll-area"
|
||||
|
||||
interface WidgetSettingsModalProps {
|
||||
widget: WidgetConfig | null
|
||||
@@ -269,3 +269,5 @@ export function WidgetSettingsModal({ widget, open, onOpenChange, onSave }: Widg
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Button } from "@/components/common/button"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -9,9 +9,9 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuCheckboxItem,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
} from "@/components/common/dropdown-menu"
|
||||
import { Settings2, ChevronUp, ChevronDown, RotateCcw, Eye, EyeOff, Cog } from "lucide-react"
|
||||
import { WidgetConfig } from "@/hooks/useWidgetLayout"
|
||||
import { WidgetConfig } from "@/lib/types/dashboard"
|
||||
|
||||
interface WidgetSettingsProps {
|
||||
widgets: WidgetConfig[]
|
||||
@@ -99,3 +99,5 @@ export function WidgetSettings({ widgets, onToggle, onMove, onReset, onConfigure
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user