Files
inventory/inventory/src/components/dashboard/EventFeed.jsx
2025-06-22 19:13:35 -04:00

1623 lines
64 KiB
JavaScript

import React, { useState, useEffect, useCallback, useMemo } from "react";
import axios from "axios";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/dashboard/ui/card";
import { Badge } from "@/components/dashboard/ui/badge";
import { ScrollArea } from "@/components/dashboard/ui/scroll-area";
import {
Package,
Truck,
UserPlus,
XCircle,
DollarSign,
ChevronRight,
Tag,
Box,
Activity,
RefreshCcw,
FileText,
AlertCircle,
} from "lucide-react";
import { format } from "date-fns";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/dashboard/ui/dialog";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/dashboard/ui/select";
import { Skeleton } from "@/components/dashboard/ui/skeleton";
import { Button } from "@/components/dashboard/ui/button";
import { Separator } from "@/components/dashboard/ui/separator";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/dashboard/ui/tooltip";
import { Alert, AlertDescription, AlertTitle } from "@/components/dashboard/ui/alert";
const METRIC_IDS = {
PLACED_ORDER: "Y8cqcF",
SHIPPED_ORDER: "VExpdL",
ACCOUNT_CREATED: "TeeypV",
CANCELED_ORDER: "YjVMNg",
NEW_BLOG_POST: "YcxeDr",
PAYMENT_REFUNDED: "R7XUYh",
};
const EVENT_ICONS = {
[METRIC_IDS.PLACED_ORDER]: Package,
[METRIC_IDS.SHIPPED_ORDER]: Truck,
[METRIC_IDS.ACCOUNT_CREATED]: UserPlus,
[METRIC_IDS.CANCELED_ORDER]: XCircle,
[METRIC_IDS.PAYMENT_REFUNDED]: DollarSign,
[METRIC_IDS.NEW_BLOG_POST]: FileText,
};
const EVENT_TYPES = {
[METRIC_IDS.PLACED_ORDER]: {
label: "Order Placed",
color: "bg-green-500 dark:bg-green-600",
textColor: "text-green-600 dark:text-green-400",
},
[METRIC_IDS.SHIPPED_ORDER]: {
label: "Order Shipped",
color: "bg-blue-500 dark:bg-blue-600",
textColor: "text-blue-600 dark:text-blue-400",
},
[METRIC_IDS.ACCOUNT_CREATED]: {
label: "New Account",
color: "bg-purple-500 dark:bg-purple-600",
textColor: "text-purple-600 dark:text-purple-400",
},
[METRIC_IDS.CANCELED_ORDER]: {
label: "Order Canceled",
color: "bg-red-500 dark:bg-red-600",
textColor: "text-red-600 dark:text-red-400",
},
[METRIC_IDS.PAYMENT_REFUNDED]: {
label: "Payment Refunded",
color: "bg-orange-500 dark:bg-orange-600",
textColor: "text-orange-600 dark:text-orange-400",
},
[METRIC_IDS.NEW_BLOG_POST]: {
label: "New Blog Post",
color: "bg-indigo-500 dark:bg-indigo-600",
textColor: "text-indigo-600 dark:text-indigo-400",
},
};
// Helper Functions
const formatCurrency = (amount) => {
// Convert to number if it's a string
const num = typeof amount === "string" ? parseFloat(amount) : amount;
// Handle negative numbers
const absNum = Math.abs(num);
// Format to 2 decimal places and add negative sign if needed
return `${num < 0 ? "-" : ""}$${absNum.toFixed(2)}`;
};
const toTitleCase = (str) => {
if (!str) return "";
return str
.toLowerCase()
.split(" ")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");
};
const formatShipMethod = (method) => {
if (!method) return "Standard Shipping";
return method
.replace("usps_", "USPS ")
.replace("ups_", "UPS ")
.replace("fedex_", "FedEx ")
.replace(/_/g, " ")
.split(" ")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");
};
const formatShipMethodSimple = (method) => {
if (!method) return "Digital";
if (method.includes("usps")) return "USPS";
if (method.includes("fedex")) return "FedEx";
if (method.includes("ups")) return "UPS";
return "Standard";
};
// Loading State Component
const LoadingState = () => (
<div className="divide-y divide-gray-100 dark:divide-gray-800">
{[...Array(8)].map((_, i) => (
<div key={i} className="flex items-center gap-3 p-4 hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors">
<div className="shrink-0">
<Skeleton className="h-10 w-10 rounded-full bg-muted" />
</div>
<div className="flex-1 min-w-0 space-y-2">
<div className="flex items-center gap-2">
<Skeleton className="h-4 w-24 bg-muted rounded-sm" />
</div>
<div className="flex items-center gap-2">
<Skeleton className="h-4 w-48 bg-muted rounded-sm" />
</div>
<div className="flex gap-1.5 items-center flex-wrap">
<Skeleton className="h-5 w-16 bg-muted rounded-md" />
<Skeleton className="h-5 w-20 bg-muted rounded-md" />
<Skeleton className="h-5 w-14 bg-muted rounded-md" />
</div>
</div>
<div className="flex items-center gap-2 shrink-0">
<Skeleton className="h-4 w-16 bg-muted rounded-sm" />
<Skeleton className="h-4 w-4 bg-muted rounded-full" />
</div>
</div>
))}
</div>
);
// Empty State Component
const EmptyState = () => (
<div className="h-full flex flex-col items-center justify-center py-16 px-4 text-center">
<div className="bg-gray-100 dark:bg-gray-800 rounded-full p-3 mb-4">
<Activity className="h-8 w-8 text-muted-foreground" />
</div>
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">
No activity yet today
</h3>
<p className="text-sm text-muted-foreground max-w-sm">
Recent activity will appear here as it happens
</p>
</div>
);
// Shared Components
const OrderStatusTags = ({ details }) => (
<div className="flex flex-wrap gap-2">
{details.HasPreorder && (
<span className="px-2 py-1 bg-blue-100 dark:bg-blue-900/20 text-blue-800 dark:text-blue-300 rounded-full text-xs cursor-help">
Includes Pre-order
</span>
)}
{details.LocalPickup && (
<span className="px-2 py-1 bg-purple-100 dark:bg-purple-900/20 text-purple-800 dark:text-purple-300 rounded-full text-xs cursor-help">
Local Pickup
</span>
)}
{details.IsOnHold && (
<span className="px-2 py-1 bg-yellow-100 dark:bg-yellow-900/20 text-yellow-800 dark:text-yellow-300 rounded-full text-xs cursor-help">
On Hold
</span>
)}
{details.HasDigiItem && (
<span className="px-2 py-1 bg-green-100 dark:bg-green-900/20 text-green-800 dark:text-green-300 rounded-full text-xs cursor-help">
Digital Items
</span>
)}
{details.HasNotions && (
<span className="px-2 py-1 bg-pink-100 dark:bg-pink-900/20 text-pink-800 dark:text-pink-300 rounded-full text-xs cursor-help">
Includes Notions
</span>
)}
{details.HasDigitalGC && (
<span className="px-2 py-1 bg-indigo-100 dark:bg-indigo-900/20 text-indigo-800 dark:text-indigo-300 rounded-full text-xs cursor-help">
Gift Card
</span>
)}
{details.StillOwes && (
<span className="px-2 py-1 bg-red-100 dark:bg-red-900/20 text-red-800 dark:text-red-300 rounded-full text-xs cursor-help">
Payment Due
</span>
)}
</div>
);
const ProductCard = ({ product }) => (
<div className="p-3 bg-gray-50 dark:bg-gray-800 rounded-lg mb-3 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
<div className="flex items-start space-x-3">
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-2">
<p className="font-medium text-sm text-gray-900 dark:text-gray-100 truncate">
{product.ProductName || "Unnamed Product"}
</p>
{product.ItemStatus === "Pre-Order" && (
<span className="px-2 py-0.5 bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 rounded-full text-xs cursor-help">
Pre-order
</span>
)}
</div>
<div className="mt-1 flex flex-wrap gap-2">
{product.Brand && (
<div className="flex items-center text-xs text-gray-500 dark:text-gray-400 cursor-help">
<Tag className="w-3 h-3 mr-1" />
<span>{product.Brand}</span>
</div>
)}
{product.SKU && (
<div className="flex items-center text-xs text-gray-500 dark:text-gray-400 cursor-help">
<Box className="w-3 h-3 mr-1" />
<span>SKU: {product.SKU}</span>
</div>
)}
</div>
</div>
<div className="text-right flex-shrink-0">
<div className="text-sm font-medium text-gray-900 dark:text-gray-100 cursor-help">
{formatCurrency(product.ItemPrice)}
</div>
<div className="text-xs text-gray-500 dark:text-gray-400 cursor-help">
Qty: {product.Quantity || product.QuantityOrdered || 1}
</div>
{product.RowTotal && (
<div className="text-xs font-medium text-gray-600 dark:text-gray-300 cursor-help">
Total: {formatCurrency(product.RowTotal)}
</div>
)}
</div>
</div>
</div>
);
const PromotionalInfo = ({ details }) => {
if (!details?.PromosUsedReg?.length && !details?.PointsDiscount) return null;
return (
<div className="mt-4 p-3 bg-green-50 dark:bg-green-900/20 rounded-lg">
<h4 className="text-sm font-medium text-green-800 dark:text-green-400 mb-2">
<span className="cursor-help">Savings Applied</span>
</h4>
<div className="space-y-2">
{Array.isArray(details.PromosUsedReg) &&
details.PromosUsedReg.map(([code, amount], index) => (
<div key={index} className="flex justify-between text-sm">
<span className="font-mono text-green-700 dark:text-green-300 cursor-help">
{code}
</span>
<span className="font-medium text-green-700 dark:text-green-300 cursor-help">
-{formatCurrency(amount)}
</span>
</div>
))}
{details.PointsDiscount > 0 && (
<div className="flex justify-between text-sm">
<span className="text-green-700 dark:text-green-300 cursor-help">
Points Discount
</span>
<span className="font-medium text-green-700 dark:text-green-300 cursor-help">
-{formatCurrency(details.PointsDiscount)}
</span>
</div>
)}
</div>
</div>
);
};
const OrderSummary = ({ details }) => (
<div className="bg-gray-50 dark:bg-gray-800 p-4 rounded-lg space-y-3">
<div className="grid grid-cols-2 gap-4">
<div>
<h4 className="text-sm font-medium text-gray-500 mb-2 cursor-help">
Subtotal
</h4>
<div className="space-y-1">
<div className="flex justify-between">
<span className="cursor-help">
Items ({details.Items?.length || 0})
</span>
<span>{formatCurrency(details.Subtotal)}</span>
</div>
{details.PointsDiscount > 0 && (
<div className="flex justify-between text-green-600">
<span className="cursor-help">Points Discount</span>
<span>-{formatCurrency(details.PointsDiscount)}</span>
</div>
)}
{details.TotalDiscounts > 0 && (
<div className="flex justify-between text-green-600">
<span className="cursor-help">Discounts</span>
<span>-{formatCurrency(details.TotalDiscounts)}</span>
</div>
)}
</div>
</div>
<div>
<h4 className="text-sm font-medium text-gray-500 mb-2 cursor-help">
Shipping
</h4>
<div className="space-y-1">
<div className="flex justify-between">
<span className="cursor-help">Shipping Cost</span>
<span>{formatCurrency(details.ShippingTotal)}</span>
</div>
{details.SalesTax > 0 && (
<div className="flex justify-between">
<span className="cursor-help">Sales Tax</span>
<span>{formatCurrency(details.SalesTax)}</span>
</div>
)}
</div>
</div>
</div>
<div className="pt-3 border-t border-gray-200 dark:border-gray-700">
<div className="flex justify-between items-center">
<div>
<span className="text-sm font-medium">Total</span>
{details.TotalSavings > 0 && (
<div className="text-xs text-green-600 cursor-help">
You saved {formatCurrency(details.TotalSavings)}
</div>
)}
</div>
<span className="text-lg font-bold">
{formatCurrency(details.TotalAmount)}
</span>
</div>
</div>
<PromotionalInfo details={details} />
</div>
);
const ShippingInfo = ({ details }) => (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<h4 className="text-sm font-medium text-gray-500 cursor-help">
Shipping Address
</h4>
<div className="text-sm space-y-1">
<div className="font-medium cursor-help">
{details.ShippingName || "Customer"}
</div>
<div>{details.ShippingStreet1}</div>
{details.ShippingStreet2 && <div>{details.ShippingStreet2}</div>}
<div className="cursor-help">
{details.ShippingCity}, {details.ShippingState} {details.ShippingZip}
</div>
{details.ShippingCountry !== "US" && (
<div className="cursor-help">{details.ShippingCountry}</div>
)}
</div>
</div>
{details.TrackingNumber && (
<div className="space-y-2">
<h4 className="text-sm font-medium text-gray-500 cursor-help">
Tracking Information
</h4>
<div className="text-sm space-y-1">
<div className="font-medium cursor-help">
{formatShipMethod(details.ShipMethod)}
</div>
<div className="font-mono text-blue-600 dark:text-blue-400 cursor-pointer hover:underline">
{details.TrackingNumber}
</div>
</div>
</div>
)}
</div>
);
const CustomTooltip = ({ active, payload, label }) => {
if (active && payload && payload.length) {
const date = new Date(label);
const formattedDate = date.toLocaleDateString('en-US', {
weekday: 'short',
month: 'short',
day: 'numeric',
year: 'numeric'
});
// Group metrics by type (current vs previous)
const currentMetrics = payload.filter(p => !p.dataKey.toLowerCase().includes('prev'));
const previousMetrics = payload.filter(p => p.dataKey.toLowerCase().includes('prev'));
return (
<Card className="p-3 shadow-lg bg-white dark:bg-gray-800 border-none">
<CardContent className="p-0 space-y-2">
<p className="font-medium text-sm border-b pb-1 mb-2">{formattedDate}</p>
<div className="space-y-1">
{currentMetrics.map((entry, index) => {
const value = entry.dataKey.toLowerCase().includes('revenue') ||
entry.dataKey === 'avgOrderValue' ||
entry.dataKey === 'movingAverage' ||
entry.dataKey === 'aovMovingAverage'
? formatCurrency(entry.value)
: entry.value.toLocaleString();
return (
<div key={index} className="flex justify-between items-center text-sm">
<span style={{ color: entry.stroke || METRIC_COLORS[entry.dataKey.toLowerCase()] }}>
{entry.name}:
</span>
<span className="font-medium ml-4">{value}</span>
</div>
);
})}
</div>
{previousMetrics.length > 0 && (
<>
<div className="border-t my-2"></div>
<div className="space-y-1">
<p className="text-xs text-muted-foreground mb-1">Previous Period</p>
{previousMetrics.map((entry, index) => {
const value = entry.dataKey.toLowerCase().includes('revenue') ||
entry.dataKey.includes('avgOrderValue')
? formatCurrency(entry.value)
: entry.value.toLocaleString();
return (
<div key={index} className="flex justify-between items-center text-sm">
<span style={{ color: entry.stroke || METRIC_COLORS[entry.dataKey.toLowerCase()] }}>
{entry.name.replace('Previous ', '')}:
</span>
<span className="font-medium ml-4">{value}</span>
</div>
);
})}
</div>
</>
)}
</CardContent>
</Card>
);
}
return null;
};
const EventDialog = ({ event, children }) => {
const eventType = EVENT_TYPES[event.metric_id];
if (!eventType) return children;
const details = event.event_properties || {};
const Icon = EVENT_ICONS[event.metric_id] || Package;
return (
<Dialog>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent className="max-w-2xl max-h-[85vh] overflow-hidden flex flex-col">
<DialogHeader className="border-b border-border px-6 py-4">
<div className="flex items-center gap-2 mb-2">
{Icon && <Icon className={`h-5 w-5 ${eventType.textColor}`} />}
<DialogTitle className="text-lg font-semibold">{eventType.label}</DialogTitle>
</div>
<div className="flex items-center justify-between">
<DialogDescription className="text-base">
{details.OrderId
? `Order #${details.OrderId}`
: details.title
? details.title
: details.EmailAddress
? details.EmailAddress
: "Event Details"}
</DialogDescription>
{event.datetime && (
<time
className="text-sm text-muted-foreground"
dateTime={event.datetime}
>
{format(new Date(event.datetime), "h:mm a")}
</time>
)}
</div>
</DialogHeader>
<div className="flex-1 overflow-y-auto px-6 py-4">
<div className="space-y-6">
{event.metric_id === METRIC_IDS.PLACED_ORDER && (
<>
<div className="grid gap-6 sm:grid-cols-2">
<div className="space-y-4">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">Shipping Information</CardTitle>
</CardHeader>
<CardContent className="space-y-1">
<p className="text-sm font-medium">{details.ShippingName}</p>
{details.ShippingStreet1 && (
<p className="text-sm text-muted-foreground">{details.ShippingStreet1}</p>
)}
{details.ShippingStreet2 && (
<p className="text-sm text-muted-foreground">{details.ShippingStreet2}</p>
)}
<p className="text-sm text-muted-foreground">
{details.ShippingCity}, {details.ShippingState} {details.ShippingZip}
</p>
{details.ShippingCountry !== "US" && (
<p className="text-sm text-muted-foreground">{details.ShippingCountry}</p>
)}
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">Order Properties</CardTitle>
</CardHeader>
<CardContent className="flex flex-wrap gap-2">
{details.IsOnHold && (
<Badge variant="secondary" className="bg-blue-100 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300">
On Hold
</Badge>
)}
{details.OnHoldReleased && (
<Badge variant="secondary" className="bg-green-100 dark:bg-green-900/20 text-green-700 dark:text-green-300">
Hold Released
</Badge>
)}
{details.StillOwes && (
<Badge variant="secondary" className="bg-red-100 dark:bg-red-900/20 text-red-700 dark:text-red-300">
Owes
</Badge>
)}
{details.LocalPickup && (
<Badge variant="secondary" className="bg-green-100 dark:bg-green-900/20 text-green-700 dark:text-green-300">
Local
</Badge>
)}
{details.HasPreorder && (
<Badge variant="secondary" className="bg-purple-100 dark:bg-purple-900/20 text-purple-700 dark:text-purple-300">
Pre-order
</Badge>
)}
{details.HasNotions && (
<Badge variant="secondary" className="bg-yellow-100 dark:bg-yellow-900/20 text-yellow-700 dark:text-yellow-300">
Notions
</Badge>
)}
{(details.OnlyDigitalGC || details.HasDigitalGC) && (
<Badge variant="secondary" className="bg-pink-100 dark:bg-pink-900/20 text-pink-700 dark:text-pink-300">
eGift Card
</Badge>
)}
{(details.HasDigiItem || details.OnlyDigiItem) && (
<Badge variant="secondary" className="bg-indigo-100 dark:bg-indigo-900/20 text-indigo-700 dark:text-indigo-300">
Digital
</Badge>
)}
</CardContent>
</Card>
</div>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">Order Summary</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Subtotal</span>
<span className="font-medium">{formatCurrency(details.Subtotal)}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Shipping</span>
<span className="font-medium">{formatCurrency(details.ShippingTotal)}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Tax</span>
<span className="font-medium">{formatCurrency(details.SalesTax)}</span>
</div>
{details.PointsDiscount > 0 && (
<div className="flex justify-between text-sm text-green-600 dark:text-green-400">
<span>Points Discount</span>
<span className="font-medium">-{formatCurrency(details.PointsDiscount)}</span>
</div>
)}
{Array.isArray(details.PromosUsedReg) &&
details.PromosUsedReg.map(([code, amount], i) => (
<div key={i} className="flex justify-between text-sm text-green-600 dark:text-green-400">
<span>{code}</span>
<span className="font-medium">-{formatCurrency(amount)}</span>
</div>
))}
</div>
<div className="pt-2 border-t">
<div className="flex justify-between font-medium">
<span>Total</span>
<span>{formatCurrency(details.TotalAmount)}</span>
</div>
</div>
</CardContent>
</Card>
</div>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">Order Items</CardTitle>
</CardHeader>
<CardContent>
<div className="divide-y">
{details.Items?.map((item, i) => (
<div key={i} className="flex gap-4 py-4 first:pt-0 last:pb-0">
{item.ImgThumb && (
<img
src={item.ImgThumb}
alt={item.ProductName}
className="w-16 h-16 object-cover rounded bg-muted"
/>
)}
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-2">
<div>
<p className="font-medium text-sm">{item.ProductName}</p>
<p className="text-sm text-muted-foreground">
{item.Quantity}x @ {formatCurrency(item.ItemPrice)}
</p>
</div>
<p className="text-sm font-medium text-green-600 dark:text-green-400 shrink-0">
{formatCurrency(item.RowTotal)}
</p>
</div>
{item.ItemStatus && item.ItemStatus !== "Ready" && (
<Badge variant="secondary" className="mt-2">
{item.ItemStatus}
</Badge>
)}
</div>
</div>
))}
</div>
</CardContent>
</Card>
</>
)}
{event.metric_id === METRIC_IDS.SHIPPED_ORDER && (
<>
<div className="mt-1">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">
{toTitleCase(details.ShippingName)}
</span>
<span className="text-sm text-gray-500"></span>
<span className="text-sm text-gray-500">
#{details.OrderId}
</span>
</div>
<div className="text-sm text-gray-500">
{formatShipMethodSimple(details.ShipMethod)}
{event.event_properties?.ShippedBy && (
<>
<span className="text-sm text-gray-500"> </span>
<span className="text-sm font-medium text-blue-600 dark:text-blue-400">Shipped by {event.event_properties.ShippedBy}</span>
</>
)}
</div>
</div>
</>
)}
{event.metric_id === METRIC_IDS.ACCOUNT_CREATED && (
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">Customer Information</CardTitle>
</CardHeader>
<CardContent className="space-y-1">
<p className="text-sm font-medium">{details.EmailAddress}</p>
<p className="text-sm text-muted-foreground">
{details.FirstName} {details.LastName}
</p>
</CardContent>
</Card>
)}
{event.metric_id === METRIC_IDS.CANCELED_ORDER && (
<>
<div className="grid gap-6 sm:grid-cols-2">
<div className="space-y-4">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">Cancellation Details</CardTitle>
</CardHeader>
<CardContent className="space-y-1">
<p className="text-sm font-medium">Reason: {details.CancelReason}</p>
{details.CancelMessage && (
<p className="text-sm text-muted-foreground">{details.CancelMessage}</p>
)}
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">Order Summary</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Subtotal</span>
<span className="font-medium">{formatCurrency(details.Subtotal)}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Shipping</span>
<span className="font-medium">{formatCurrency(details.ShippingTotal)}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Tax</span>
<span className="font-medium">{formatCurrency(details.SalesTax)}</span>
</div>
</div>
<div className="pt-2 border-t">
<div className="flex justify-between font-medium text-red-600 dark:text-red-400">
<span>Total Refunded</span>
<span>{formatCurrency(details.TotalAmount)}</span>
</div>
</div>
</CardContent>
</Card>
</div>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">Canceled Items</CardTitle>
</CardHeader>
<CardContent>
<div className="divide-y">
{details.Items?.map((item, i) => (
<div key={i} className="flex gap-4 py-4 first:pt-0 last:pb-0">
{item.ImgThumb && (
<img
src={item.ImgThumb}
alt={item.ProductName}
className="w-16 h-16 object-cover rounded bg-muted"
/>
)}
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-2">
<div>
<p className="font-medium text-sm">{item.ProductName}</p>
<p className="text-sm text-muted-foreground">
{item.Quantity}x @ {formatCurrency(item.ItemPrice)}
</p>
</div>
<p className="text-sm font-medium text-red-600 dark:text-red-400 shrink-0">
{formatCurrency(item.RowTotal)}
</p>
</div>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</div>
</>
)}
{event.metric_id === METRIC_IDS.PAYMENT_REFUNDED && (
<div className="grid gap-6 sm:grid-cols-2">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">Refund Details</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Amount Refunded</span>
<span className="text-sm font-medium text-red-600 dark:text-red-400">
{formatCurrency(details.PaymentAmount)}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Payment Method</span>
<span className="text-sm">{details.PaymentName}</span>
</div>
{details.OrderMessage && (
<div className="pt-2 border-t mt-4">
<p className="text-sm text-muted-foreground">{details.OrderMessage}</p>
</div>
)}
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">Customer Information</CardTitle>
</CardHeader>
<CardContent className="space-y-1">
<p className="text-sm font-medium">{details.EmailAddress}</p>
<p className="text-sm text-muted-foreground">
{details.FirstName} {details.LastName}
</p>
</CardContent>
</Card>
</div>
)}
{event.metric_id === METRIC_IDS.NEW_BLOG_POST && (
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">{details.title}</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-sm text-muted-foreground whitespace-pre-wrap">
{details.description}
</p>
{details.url && (
<a
href={details.url}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-blue-600 dark:text-blue-400 hover:underline inline-flex items-center gap-1"
>
Read More
<ChevronRight className="h-4 w-4" />
</a>
)}
</CardContent>
</Card>
)}
</div>
</div>
</DialogContent>
</Dialog>
);
};
export { EventDialog };
const EventCard = ({ event }) => {
const eventType = EVENT_TYPES[event.metric_id] || {
label: "Unknown Event",
color: "bg-gray-500",
textColor: "text-gray-600 dark:text-gray-400",
};
const Icon = EVENT_ICONS[event.metric_id] || Package;
const details = event.event_properties || {};
const datetime = event.attributes?.datetime || event.datetime || event.event_properties?.datetime;
const timestamp = datetime ? new Date(datetime) : null;
const isValidDate = timestamp && !isNaN(timestamp.getTime());
return (
<EventDialog event={event}>
<button className="w-full focus:outline-none text-left">
<div className="flex items-center gap-3 p-4 hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors border-b border-gray-100 dark:border-gray-800 last:border-b-0">
<div className={`shrink-0 w-10 h-10 rounded-full ${eventType.color} bg-opacity-10 dark:bg-opacity-20 flex items-center justify-center`}>
<Icon className="h-5 w-5 text-gray-900 dark:text-gray-100" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center">
<span className={`${eventType.textColor} text-sm font-medium`}>
{eventType.label}
</span>
</div>
{event.metric_id === METRIC_IDS.PLACED_ORDER && (
<>
<div className="mt-1">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">
{toTitleCase(details.ShippingName)}
</span>
</div>
<div className="flex items-center gap-2 text-sm text-gray-500">
<span className="text-sm text-gray-500">
#{details.OrderId}
</span>
<span className="text-sm text-gray-500"></span>
<span className="font-medium text-green-600 dark:text-green-400">
{formatCurrency(details.TotalAmount)}
</span>
</div>
</div>
<div className="flex gap-1.5 items-center flex-wrap mt-2">
{details.IsOnHold && (
<Badge
variant="secondary"
className="bg-blue-100 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300 text-xs py-0"
>
On Hold
</Badge>
)}
{details.OnHoldReleased && (
<Badge
variant="secondary"
className="bg-green-100 dark:bg-green-900/20 text-green-700 dark:text-green-300 text-xs py-0"
>
Hold Released
</Badge>
)}
{details.StillOwes && (
<Badge
variant="secondary"
className="bg-red-100 dark:bg-red-900/20 text-red-700 dark:text-red-300 text-xs py-0"
>
Owes
</Badge>
)}
{details.LocalPickup && (
<Badge
variant="secondary"
className="bg-green-100 dark:bg-green-900/20 text-green-700 dark:text-green-300 text-xs py-0"
>
Local
</Badge>
)}
{details.HasPreorder && (
<Badge
variant="secondary"
className="bg-purple-100 dark:bg-purple-900/20 text-purple-700 dark:text-purple-300 text-xs py-0"
>
Pre-order
</Badge>
)}
{details.HasNotions && (
<Badge
variant="secondary"
className="bg-yellow-100 dark:bg-yellow-900/20 text-yellow-700 dark:text-yellow-300 text-xs py-0"
>
Notions
</Badge>
)}
{(details.OnlyDigitalGC || details.HasDigitalGC) && (
<Badge
variant="secondary"
className="bg-pink-100 dark:bg-pink-900/20 text-pink-700 dark:text-pink-300 text-xs py-0"
>
eGift Card
</Badge>
)}
{(details.HasDigiItem || details.OnlyDigiItem) && (
<Badge
variant="secondary"
className="bg-indigo-100 dark:bg-indigo-900/20 text-indigo-700 dark:text-indigo-300 text-xs py-0"
>
Digital
</Badge>
)}
</div>
</>
)}
{event.metric_id === METRIC_IDS.SHIPPED_ORDER && (
<>
<div className="mt-1">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">
{toTitleCase(details.ShippingName)}
</span>
<span className="text-sm text-gray-500"></span>
<span className="text-sm text-gray-500">
#{details.OrderId}
</span>
</div>
<div className="text-sm text-gray-500">
{formatShipMethodSimple(details.ShipMethod)}
{event.event_properties?.ShippedBy && (
<>
<span className="text-sm text-gray-500"> </span>
<span className="text-sm font-medium text-blue-600 dark:text-blue-400">Shipped by {event.event_properties.ShippedBy}</span>
</>
)}
</div>
</div>
</>
)}
{event.metric_id === METRIC_IDS.ACCOUNT_CREATED && (
<div className="mt-1">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">
{details.FirstName && details.LastName
? `${toTitleCase(details.FirstName)} ${toTitleCase(
details.LastName
)}`
: "New Customer"}
</span>
</div>
<div className="text-sm text-gray-500">
{details.EmailAddress}
</div>
</div>
)}
{event.metric_id === METRIC_IDS.CANCELED_ORDER && (
<div className="mt-1">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">
{toTitleCase(details.ShippingName)}
</span>
<span className="text-sm text-gray-500"></span>
<span className="text-sm text-gray-500">
#{details.OrderId}
</span>
</div>
<div className="text-sm text-gray-500">
{formatCurrency(details.TotalAmount)} {details.CancelReason}
</div>
</div>
)}
{event.metric_id === METRIC_IDS.PAYMENT_REFUNDED && (
<div className="mt-1">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">
{toTitleCase(details.ShippingName)}
</span>
<span className="text-sm text-gray-500"></span>
<span className="text-sm text-gray-500">
#{details.FromOrder}
</span>
</div>
<div className="text-sm text-gray-500">
{formatCurrency(details.PaymentAmount)} via{" "}
{details.PaymentName}
</div>
</div>
)}
{event.metric_id === METRIC_IDS.NEW_BLOG_POST && (
<div className="mt-1">
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
{details.title}
</div>
<div className="text-sm text-gray-500 line-clamp-1">
{details.description}
</div>
</div>
)}
</div>
<div className="flex items-center gap-2 shrink-0">
{isValidDate && (
<time
className="text-sm text-muted-foreground"
dateTime={timestamp.toISOString()}
>
{format(timestamp, "h:mm a")}
</time>
)}
<ChevronRight className="h-4 w-4 text-muted-foreground" />
</div>
</div>
</button>
</EventDialog>
);
};
const DEFAULT_METRICS = Object.values(METRIC_IDS);
const EventFeed = ({
title = "Event Feed",
selectedMetrics = DEFAULT_METRICS,
}) => {
const metrics = useMemo(() => selectedMetrics, [selectedMetrics]);
const [events, setEvents] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [lastUpdate, setLastUpdate] = useState(null);
const [activeEventTypes, setActiveEventTypes] = useState({
[METRIC_IDS.PLACED_ORDER]: true,
[METRIC_IDS.SHIPPED_ORDER]: true,
[METRIC_IDS.ACCOUNT_CREATED]: true,
[METRIC_IDS.CANCELED_ORDER]: true,
[METRIC_IDS.PAYMENT_REFUNDED]: true,
[METRIC_IDS.NEW_BLOG_POST]: true,
});
const [orderFilters, setOrderFilters] = useState({
hasPreorder: false,
localPickup: false,
isOnHold: false,
onHoldReleased: false,
hasDigiItem: false,
hasNotions: false,
hasGiftCard: false,
stillOwes: false,
});
const fetchEvents = useCallback(async () => {
try {
setError(null);
// Only set loading true if we don't have any events yet
if (events.length === 0) {
setLoading(true);
}
const response = await axios.get("/api/klaviyo/events/feed", {
params: {
timeRange: "today",
metricIds: JSON.stringify(metrics),
},
});
// Keep the original event structure intact
const processedEvents = (response.data.data || []).map((event) => ({
...event,
datetime: event.attributes?.datetime || event.datetime,
// Don't spread event_properties to preserve the nested structure
event_properties: event.attributes?.event_properties || {}
}));
setEvents(processedEvents);
setLastUpdate(new Date());
} catch (error) {
console.error("Error fetching events:", error);
setError(error.message);
} finally {
setLoading(false);
}
}, [metrics]);
// Fetch events on mount and every minute
useEffect(() => {
fetchEvents();
const interval = setInterval(fetchEvents, 60000); // Refresh every minute
return () => {
clearInterval(interval);
};
}, [fetchEvents]);
const filteredEvents = useMemo(() => {
// Check if any order property filters are active
const hasActiveOrderFilters = Object.values(orderFilters).some(filter => filter);
return events.filter(event => {
// First check event type filter
if (!activeEventTypes[event.metric_id]) {
return false;
}
// Then check order property filters if any are active
if (hasActiveOrderFilters) {
if (event.metric_id !== METRIC_IDS.PLACED_ORDER) return false;
const details = event.event_properties || {};
if (orderFilters.hasPreorder && !details.HasPreorder) return false;
if (orderFilters.localPickup && !details.LocalPickup) return false;
if (orderFilters.isOnHold && !details.IsOnHold) return false;
if (orderFilters.onHoldReleased && !details.OnHoldReleased) return false;
if (orderFilters.hasDigiItem && !details.HasDigiItem) return false;
if (orderFilters.hasNotions && !details.HasNotions) return false;
if (orderFilters.hasGiftCard && !details.HasDigitalGC) return false;
if (orderFilters.stillOwes && !details.StillOwes) return false;
}
return true;
});
}, [events, activeEventTypes, orderFilters]);
// Calculate counts for event types and order properties
const counts = useMemo(() => {
const eventTypeCounts = {
[METRIC_IDS.PLACED_ORDER]: 0,
[METRIC_IDS.SHIPPED_ORDER]: 0,
[METRIC_IDS.ACCOUNT_CREATED]: 0,
[METRIC_IDS.CANCELED_ORDER]: 0,
[METRIC_IDS.PAYMENT_REFUNDED]: 0,
[METRIC_IDS.NEW_BLOG_POST]: 0,
};
const orderPropertyCounts = {
hasPreorder: 0,
localPickup: 0,
isOnHold: 0,
onHoldReleased: 0,
hasDigiItem: 0,
hasNotions: 0,
hasGiftCard: 0,
stillOwes: 0,
};
events.forEach(event => {
// Count event types
if (event.metric_id) {
eventTypeCounts[event.metric_id]++;
}
// Count order properties
if (event.metric_id === METRIC_IDS.PLACED_ORDER) {
const details = event.event_properties || {};
if (details.HasPreorder) orderPropertyCounts.hasPreorder++;
if (details.LocalPickup) orderPropertyCounts.localPickup++;
if (details.IsOnHold) orderPropertyCounts.isOnHold++;
if (details.OnHoldReleased) orderPropertyCounts.onHoldReleased++;
if (details.HasDigiItem) orderPropertyCounts.hasDigiItem++;
if (details.HasNotions) orderPropertyCounts.hasNotions++;
if (details.HasDigitalGC) orderPropertyCounts.hasGiftCard++;
if (details.StillOwes) orderPropertyCounts.stillOwes++;
}
});
return {
eventTypes: eventTypeCounts,
orderProperties: orderPropertyCounts,
};
}, [events]);
const handleOrderPropertyClick = (property) => {
setOrderFilters(prev => {
// If clicking the active filter, clear all filters
if (prev[property]) {
return {
hasPreorder: false,
localPickup: false,
isOnHold: false,
onHoldReleased: false,
hasDigiItem: false,
hasNotions: false,
hasGiftCard: false,
stillOwes: false,
};
}
// Otherwise, set only this filter to true
return {
hasPreorder: property === 'hasPreorder',
localPickup: property === 'localPickup',
isOnHold: property === 'isOnHold',
onHoldReleased: property === 'onHoldReleased',
hasDigiItem: property === 'hasDigiItem',
hasNotions: property === 'hasNotions',
hasGiftCard: property === 'hasGiftCard',
stillOwes: property === 'stillOwes',
};
});
};
const handleEventTypeClick = (metricId) => {
setActiveEventTypes(prev => {
// If clicking the only active filter, reset to all active
const activeCount = Object.values(prev).filter(Boolean).length;
if (activeCount === 1 && prev[metricId]) {
return {
[METRIC_IDS.PLACED_ORDER]: true,
[METRIC_IDS.SHIPPED_ORDER]: true,
[METRIC_IDS.ACCOUNT_CREATED]: true,
[METRIC_IDS.CANCELED_ORDER]: true,
[METRIC_IDS.PAYMENT_REFUNDED]: true,
[METRIC_IDS.NEW_BLOG_POST]: true,
};
}
// Otherwise, set only this filter to true
return {
[METRIC_IDS.PLACED_ORDER]: metricId === METRIC_IDS.PLACED_ORDER,
[METRIC_IDS.SHIPPED_ORDER]: metricId === METRIC_IDS.SHIPPED_ORDER,
[METRIC_IDS.ACCOUNT_CREATED]: metricId === METRIC_IDS.ACCOUNT_CREATED,
[METRIC_IDS.CANCELED_ORDER]: metricId === METRIC_IDS.CANCELED_ORDER,
[METRIC_IDS.PAYMENT_REFUNDED]: metricId === METRIC_IDS.PAYMENT_REFUNDED,
[METRIC_IDS.NEW_BLOG_POST]: metricId === METRIC_IDS.NEW_BLOG_POST,
};
});
};
const EventTypeTooltipContent = () => (
<div className="grid gap-2">
<div className="flex items-center justify-between gap-4">
<span className="flex items-center gap-2">
<Package className="h-4 w-4" />
Orders
</span>
<Badge variant="secondary" className="bg-muted">
{counts.eventTypes[METRIC_IDS.PLACED_ORDER].toLocaleString()}
</Badge>
</div>
<div className="flex items-center justify-between gap-4">
<span className="flex items-center gap-2">
<Truck className="h-4 w-4" />
Shipments
</span>
<Badge variant="secondary" className="bg-muted">
{counts.eventTypes[METRIC_IDS.SHIPPED_ORDER].toLocaleString()}
</Badge>
</div>
<div className="flex items-center justify-between gap-4">
<span className="flex items-center gap-2">
<UserPlus className="h-4 w-4" />
Accounts
</span>
<Badge variant="secondary" className="bg-muted">
{counts.eventTypes[METRIC_IDS.ACCOUNT_CREATED].toLocaleString()}
</Badge>
</div>
<div className="flex items-center justify-between gap-4">
<span className="flex items-center gap-2">
<XCircle className="h-4 w-4" />
Cancellations
</span>
<Badge variant="secondary" className="bg-muted">
{counts.eventTypes[METRIC_IDS.CANCELED_ORDER].toLocaleString()}
</Badge>
</div>
<div className="flex items-center justify-between gap-4">
<span className="flex items-center gap-2">
<DollarSign className="h-4 w-4" />
Refunds
</span>
<Badge variant="secondary" className="bg-muted">
{counts.eventTypes[METRIC_IDS.PAYMENT_REFUNDED].toLocaleString()}
</Badge>
</div>
<div className="flex items-center justify-between gap-4">
<span className="flex items-center gap-2">
<FileText className="h-4 w-4" />
Blog Posts
</span>
<Badge variant="secondary" className="bg-muted">
{counts.eventTypes[METRIC_IDS.NEW_BLOG_POST].toLocaleString()}
</Badge>
</div>
</div>
);
return (
<Card className="flex flex-col h-full bg-white dark:bg-gray-900/60 backdrop-blur-sm w-full">
<CardHeader className="p-6 pb-2">
<div className="flex justify-between items-start">
<div>
<CardTitle className="text-xl font-semibold text-gray-900 dark:text-gray-100">{title}</CardTitle>
{lastUpdate && (
<CardDescription className="text-sm text-muted-foreground">
Last updated {format(lastUpdate, "h:mm a")}
</CardDescription>
)}
</div>
{!error && (
<div className="flex flex-wrap gap-2">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant={activeEventTypes[METRIC_IDS.PLACED_ORDER] ? "default" : "outline"}
size="sm"
onClick={() => handleEventTypeClick(METRIC_IDS.PLACED_ORDER)}
className="h-8 w-8 p-0 rounded-md"
>
<Package className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<EventTypeTooltipContent />
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant={activeEventTypes[METRIC_IDS.SHIPPED_ORDER] ? "default" : "outline"}
size="sm"
onClick={() => handleEventTypeClick(METRIC_IDS.SHIPPED_ORDER)}
className="h-8 w-8 p-0 rounded-md"
>
<Truck className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<EventTypeTooltipContent />
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant={activeEventTypes[METRIC_IDS.ACCOUNT_CREATED] ? "default" : "outline"}
size="sm"
onClick={() => handleEventTypeClick(METRIC_IDS.ACCOUNT_CREATED)}
className="h-8 w-8 p-0 rounded-md"
>
<UserPlus className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<EventTypeTooltipContent />
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant={activeEventTypes[METRIC_IDS.CANCELED_ORDER] ? "default" : "outline"}
size="sm"
onClick={() => handleEventTypeClick(METRIC_IDS.CANCELED_ORDER)}
className="h-8 w-8 p-0 rounded-md"
>
<XCircle className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<EventTypeTooltipContent />
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant={activeEventTypes[METRIC_IDS.PAYMENT_REFUNDED] ? "default" : "outline"}
size="sm"
onClick={() => handleEventTypeClick(METRIC_IDS.PAYMENT_REFUNDED)}
className="h-8 w-8 p-0 rounded-md"
>
<DollarSign className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<EventTypeTooltipContent />
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant={activeEventTypes[METRIC_IDS.NEW_BLOG_POST] ? "default" : "outline"}
size="sm"
onClick={() => handleEventTypeClick(METRIC_IDS.NEW_BLOG_POST)}
className="h-8 w-8 p-0 rounded-md"
>
<FileText className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<EventTypeTooltipContent />
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
)}
</div>
{/* Order Property Filters - update styling */}
{!error && (
<div className="flex flex-wrap gap-2 justify-center mt-4 pt-1">
{counts.orderProperties.hasPreorder > 0 && (
<Badge
variant="secondary"
onClick={() => handleOrderPropertyClick('hasPreorder')}
className={`${
orderFilters.hasPreorder
? 'bg-purple-800 text-purple-200 hover:bg-purple-700'
: 'bg-purple-100 dark:bg-purple-900/20 text-purple-800 dark:text-purple-300 hover:bg-purple-100 dark:hover:bg-purple-900/20'
} cursor-pointer rounded-md`}
>
Pre-order ({counts.orderProperties.hasPreorder})
</Badge>
)}
{counts.orderProperties.localPickup > 0 && (
<Badge
variant="secondary"
onClick={() => handleOrderPropertyClick('localPickup')}
className={`${
orderFilters.localPickup
? 'bg-green-800 text-green-200 hover:bg-green-700'
: 'bg-green-100 dark:bg-green-900/20 text-green-800 dark:text-green-300 hover:bg-green-100 dark:hover:bg-green-900/20'
} cursor-pointer rounded-md`}
>
Local ({counts.orderProperties.localPickup})
</Badge>
)}
{counts.orderProperties.isOnHold > 0 && (
<Badge
variant="secondary"
onClick={() => handleOrderPropertyClick('isOnHold')}
className={`${
orderFilters.isOnHold
? 'bg-blue-800 text-blue-200 hover:bg-blue-700'
: 'bg-blue-100 dark:bg-blue-900/20 text-blue-800 dark:text-blue-300 hover:bg-blue-100 dark:hover:bg-blue-900/20'
} cursor-pointer rounded-md`}
>
On Hold ({counts.orderProperties.isOnHold})
</Badge>
)}
{counts.orderProperties.onHoldReleased > 0 && (
<Badge
variant="secondary"
onClick={() => handleOrderPropertyClick('onHoldReleased')}
className={`${
orderFilters.onHoldReleased
? 'bg-green-800 text-green-200 hover:bg-green-700'
: 'bg-green-100 dark:bg-green-900/20 text-green-800 dark:text-green-300 hover:bg-green-100 dark:hover:bg-green-900/20'
} cursor-pointer rounded-md`}
>
Hold Released ({counts.orderProperties.onHoldReleased})
</Badge>
)}
{counts.orderProperties.hasDigiItem > 0 && (
<Badge
variant="secondary"
onClick={() => handleOrderPropertyClick('hasDigiItem')}
className={`${
orderFilters.hasDigiItem
? 'bg-indigo-800 text-indigo-200 hover:bg-indigo-700'
: 'bg-indigo-100 dark:bg-indigo-900/20 text-indigo-800 dark:text-indigo-300 hover:bg-indigo-100 dark:hover:bg-indigo-900/20'
} cursor-pointer rounded-md`}
>
Digital ({counts.orderProperties.hasDigiItem})
</Badge>
)}
{counts.orderProperties.hasNotions > 0 && (
<Badge
variant="secondary"
onClick={() => handleOrderPropertyClick('hasNotions')}
className={`${
orderFilters.hasNotions
? 'bg-yellow-800 text-yellow-200 hover:bg-yellow-700'
: 'bg-yellow-100 dark:bg-yellow-900/20 text-yellow-800 dark:text-yellow-300 hover:bg-yellow-100 dark:hover:bg-yellow-900/20'
} cursor-pointer rounded-md`}
>
Notions ({counts.orderProperties.hasNotions})
</Badge>
)}
{counts.orderProperties.hasGiftCard > 0 && (
<Badge
variant="secondary"
onClick={() => handleOrderPropertyClick('hasGiftCard')}
className={`${
orderFilters.hasGiftCard
? 'bg-pink-800 text-pink-200 hover:bg-pink-700'
: 'bg-pink-100 dark:bg-pink-900/20 text-pink-800 dark:text-pink-300 hover:bg-pink-100 dark:hover:bg-pink-900/20'
} cursor-pointer rounded-md`}
>
eGift Card ({counts.orderProperties.hasGiftCard})
</Badge>
)}
{counts.orderProperties.stillOwes > 0 && (
<Badge
variant="secondary"
onClick={() => handleOrderPropertyClick('stillOwes')}
className={`${
orderFilters.stillOwes
? 'bg-red-800 text-red-200 hover:bg-red-700'
: 'bg-red-100 dark:bg-red-900/20 text-red-800 dark:text-red-300 hover:bg-red-100 dark:hover:bg-red-900/20'
} cursor-pointer rounded-md`}
>
Owes ({counts.orderProperties.stillOwes})
</Badge>
)}
</div>
)}
</CardHeader>
<CardContent className="px-0 pb-6 pt-0 md:px-6 flex-1 overflow-hidden -mt-2">
<ScrollArea className="h-full">
{loading && !events.length ? (
<LoadingState />
) : error ? (
<Alert variant="destructive" className="mt-1 mx-6">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>
Failed to load event feed: {error}
</AlertDescription>
</Alert>
) : !filteredEvents || filteredEvents.length === 0 ? (
<EmptyState />
) : (
<div className="divide-y divide-gray-100 dark:divide-gray-800">
{filteredEvents.map((event) => (
<EventCard key={event.id} event={event} />
))}
</div>
)}
</ScrollArea>
</CardContent>
</Card>
);
};
export default EventFeed;