Fix cards layout/style, add today period

This commit is contained in:
2024-12-27 21:13:34 -05:00
parent 9e0a6a9b6a
commit 6cab3314e3
5 changed files with 197 additions and 129 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -24,14 +24,17 @@ import {
Mail, Mail,
Send, Send,
Loader2, Loader2,
ArrowUp,
ArrowDown,
} from "lucide-react"; } from "lucide-react";
import axios from "axios"; import axios from "axios";
const TIME_RANGES = { const TIME_RANGES = {
7: "Last 7 Days", "today": "Today",
14: "Last 14 Days", "7": "Last 7 Days",
30: "Last 30 Days", "14": "Last 14 Days",
90: "Last 90 Days", "30": "Last 30 Days",
"90": "Last 90 Days",
}; };
const formatDuration = (seconds) => { const formatDuration = (seconds) => {
@@ -41,12 +44,33 @@ const formatDuration = (seconds) => {
}; };
const getDateRange = (days) => { const getDateRange = (days) => {
const end = new Date(); // Create date in Eastern Time
end.setUTCHours(23, 59, 59, 999); const now = new Date();
const easternTime = new Date(
now.toLocaleString("en-US", { timeZone: "America/New_York" })
);
const start = new Date(); if (days === "today") {
start.setDate(start.getDate() - days); // For today, set the range to be the current day in Eastern Time
start.setUTCHours(0, 0, 0, 0); const start = new Date(easternTime);
start.setHours(0, 0, 0, 0);
const end = new Date(easternTime);
end.setHours(23, 59, 59, 999);
return {
start_datetime: start.toISOString(),
end_datetime: end.toISOString()
};
}
// For other periods, calculate from end of previous day
const end = new Date(easternTime);
end.setHours(23, 59, 59, 999);
const start = new Date(easternTime);
start.setDate(start.getDate() - Number(days));
start.setHours(0, 0, 0, 0);
return { return {
start_datetime: start.toISOString(), start_datetime: start.toISOString(),
@@ -75,48 +99,52 @@ const MetricCard = ({
const formatDelta = (d) => { const formatDelta = (d) => {
if (d === undefined || d === null) return null; if (d === undefined || d === null) return null;
if (d === 0) return "0"; if (d === 0) return "0";
return (d > 0 ? "+" : "") + d + suffix; return Math.abs(d) + suffix;
}; };
const colorMapping = {
blue: "bg-blue-50 dark:bg-blue-900/20 border-blue-100 dark:border-blue-800/50 text-blue-600 dark:text-blue-400",
green: "bg-green-50 dark:bg-green-900/20 border-green-100 dark:border-green-800/50 text-green-600 dark:text-green-400",
purple: "bg-purple-50 dark:bg-purple-900/20 border-purple-100 dark:border-purple-800/50 text-purple-600 dark:text-purple-400",
indigo: "bg-indigo-50 dark:bg-indigo-900/20 border-indigo-100 dark:border-indigo-800/50 text-indigo-600 dark:text-indigo-400",
orange: "bg-orange-50 dark:bg-orange-900/20 border-orange-100 dark:border-orange-800/50 text-orange-600 dark:text-orange-400",
teal: "bg-teal-50 dark:bg-teal-900/20 border-teal-100 dark:border-teal-800/50 text-teal-600 dark:text-teal-400",
cyan: "bg-cyan-50 dark:bg-cyan-900/20 border-cyan-100 dark:border-cyan-800/50 text-cyan-600 dark:text-cyan-400",
};
const baseColors = colorMapping[colorClass];
return ( return (
<div className={`p-3 rounded-lg border transition-colors ${baseColors}`}> <Card className="h-full">
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-300"> <CardContent className="pt-6 h-full">
{title} <div className="flex justify-between items-start">
</h3> <div className="flex-1 min-w-0">
<div className="flex items-baseline gap-2 mt-1"> <p className="text-sm font-medium text-muted-foreground">{title}</p>
{loading ? ( {loading ? (
<Skeleton className="h-8 w-24 dark:bg-gray-700" /> <Skeleton className="h-8 w-24 dark:bg-gray-700" />
) : ( ) : (
<> <div className="flex items-baseline gap-2">
<div className="flex items-center gap-2"> <p className="text-2xl font-bold">
{Icon && <Icon className="w-4 h-4" />} {typeof value === "number"
<p className="text-xl font-bold"> ? value.toLocaleString() + suffix
{typeof value === "number" : value}
? value.toLocaleString() + suffix </p>
: value} {delta !== undefined && delta !== 0 && (
</p> <div className={`flex items-center ${getDeltaColor(delta)}`}>
</div> {delta > 0 ? (
{delta !== undefined && ( <ArrowUp className="w-3 h-3" />
<p className={`text-xs font-medium ${getDeltaColor(delta)}`}> ) : (
{formatDelta(delta)} <ArrowDown className="w-3 h-3" />
</p> )}
<span className="text-xs font-medium">
{formatDelta(delta)}
</span>
</div>
)}
</div>
)} )}
</> </div>
)} {Icon && (
</div> <Icon className={`h-5 w-5 flex-shrink-0 ml-2 ${colorClass === "blue" ? "text-blue-500" :
</div> colorClass === "green" ? "text-green-500" :
colorClass === "purple" ? "text-purple-500" :
colorClass === "indigo" ? "text-indigo-500" :
colorClass === "orange" ? "text-orange-500" :
colorClass === "teal" ? "text-teal-500" :
colorClass === "cyan" ? "text-cyan-500" :
"text-blue-500"}`} />
)}
</div>
</CardContent>
</Card>
); );
}; };
@@ -130,7 +158,7 @@ const TableSkeleton = () => (
); );
const GorgiasOverview = () => { const GorgiasOverview = () => {
const [timeRange, setTimeRange] = useState(7); const [timeRange, setTimeRange] = useState("7");
const [data, setData] = useState({}); const [data, setData] = useState({});
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(null); const [error, setError] = useState(null);
@@ -263,8 +291,8 @@ const GorgiasOverview = () => {
</h2> </h2>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Select <Select
value={String(timeRange)} value={timeRange}
onValueChange={(value) => setTimeRange(Number(value))} onValueChange={(value) => setTimeRange(value)}
> >
<SelectTrigger className="w-[140px] bg-white dark:bg-gray-800"> <SelectTrigger className="w-[140px] bg-white dark:bg-gray-800">
<SelectValue placeholder="Select range"> <SelectValue placeholder="Select range">
@@ -272,7 +300,13 @@ const GorgiasOverview = () => {
</SelectValue> </SelectValue>
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{Object.entries(TIME_RANGES).map(([value, label]) => ( {[
["today", "Today"],
["7", "Last 7 Days"],
["14", "Last 14 Days"],
["30", "Last 30 Days"],
["90", "Last 90 Days"],
].map(([value, label]) => (
<SelectItem key={value} value={value}> <SelectItem key={value} value={value}>
{label} {label}
</SelectItem> </SelectItem>
@@ -284,80 +318,94 @@ const GorgiasOverview = () => {
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="grid grid-cols-2 md:grid-cols-2 lg:grid-cols-4 gap-4"> <div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
{/* Message & Response Metrics */} {/* Message & Response Metrics */}
<MetricCard <div className="h-full">
title="Messages Received" <MetricCard
value={stats.total_messages_received?.value} title="Messages Received"
delta={stats.total_messages_received?.delta} value={stats.total_messages_received?.value}
icon={Mail} delta={stats.total_messages_received?.delta}
colorClass="blue" icon={Mail}
loading={loading} colorClass="blue"
/> loading={loading}
<MetricCard />
title="Messages Sent" </div>
value={stats.total_messages_sent?.value} <div className="h-full">
delta={stats.total_messages_sent?.delta} <MetricCard
icon={Send} title="Messages Sent"
colorClass="green" value={stats.total_messages_sent?.value}
loading={loading} delta={stats.total_messages_sent?.delta}
/> icon={Send}
<MetricCard colorClass="green"
title="First Response" loading={loading}
value={formatDuration(stats.median_first_response_time?.value)} />
delta={stats.median_first_response_time?.delta} </div>
icon={Clock} <div className="h-full">
colorClass="purple" <MetricCard
more_is_better={false} title="First Response"
loading={loading} value={formatDuration(stats.median_first_response_time?.value)}
/> delta={stats.median_first_response_time?.delta}
<MetricCard icon={Clock}
title="One-Touch Rate" colorClass="purple"
value={stats.total_one_touch_tickets?.value} more_is_better={false}
delta={stats.total_one_touch_tickets?.delta} loading={loading}
suffix="%" />
icon={MessageSquare} </div>
colorClass="indigo" <div className="h-full">
loading={loading} <MetricCard
/> title="One-Touch Rate"
</div> value={stats.total_one_touch_tickets?.value}
delta={stats.total_one_touch_tickets?.delta}
suffix="%"
icon={MessageSquare}
colorClass="indigo"
loading={loading}
/>
</div>
<div className="grid grid-cols-2 md:grid-cols-2 lg:grid-cols-4 gap-4">
{/* Satisfaction & Efficiency */} {/* Satisfaction & Efficiency */}
<MetricCard <div className="h-full">
title="Customer Satisfaction" <MetricCard
value={`${satisfactionStats.average_rating?.value}/5`} title="Customer Satisfaction"
delta={satisfactionStats.average_rating?.delta} value={`${satisfactionStats.average_rating?.value}/5`}
suffix="%" delta={satisfactionStats.average_rating?.delta}
icon={Star} suffix="%"
colorClass="orange" icon={Star}
loading={loading} colorClass="orange"
/> loading={loading}
<MetricCard />
title="Survey Response Rate" </div>
value={satisfactionStats.response_rate?.value} <div className="h-full">
delta={satisfactionStats.response_rate?.delta} <MetricCard
suffix="%" title="Survey Response Rate"
colorClass="orange" value={satisfactionStats.response_rate?.value}
loading={loading} delta={satisfactionStats.response_rate?.delta}
/> suffix="%"
<MetricCard colorClass="orange"
title="Resolution Time" loading={loading}
value={formatDuration(stats.median_resolution_time?.value)} />
delta={stats.median_resolution_time?.delta} </div>
icon={Clock} <div className="h-full">
colorClass="teal" <MetricCard
more_is_better={false} title="Resolution Time"
loading={loading} value={formatDuration(stats.median_resolution_time?.value)}
/> delta={stats.median_resolution_time?.delta}
<MetricCard icon={Clock}
title="Self-Service Rate" colorClass="teal"
value={selfServiceStats.self_service_automation_rate?.value} more_is_better={false}
delta={selfServiceStats.self_service_automation_rate?.delta} loading={loading}
suffix="%" />
colorClass="cyan" </div>
loading={loading} <div className="h-full">
/> <MetricCard
title="Self-Service Rate"
value={selfServiceStats.self_service_automation_rate?.value}
delta={selfServiceStats.self_service_automation_rate?.delta}
suffix="%"
colorClass="cyan"
loading={loading}
/>
</div>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
@@ -379,7 +427,7 @@ const GorgiasOverview = () => {
<TableHead>Channel</TableHead> <TableHead>Channel</TableHead>
<TableHead className="text-right">Total</TableHead> <TableHead className="text-right">Total</TableHead>
<TableHead className="text-right">%</TableHead> <TableHead className="text-right">%</TableHead>
<TableHead className="text-right">Δ</TableHead> <TableHead className="text-right">Change</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
@@ -405,8 +453,18 @@ const GorgiasOverview = () => {
: "dark:text-gray-300" : "dark:text-gray-300"
}`} }`}
> >
{channel.delta > 0 ? "+" : ""} <div className="flex items-center justify-end gap-0.5">
{channel.delta} {channel.delta !== 0 && (
<>
{channel.delta > 0 ? (
<ArrowUp className="w-3 h-3" />
) : (
<ArrowDown className="w-3 h-3" />
)}
<span>{Math.abs(channel.delta)}%</span>
</>
)}
</div>
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}
@@ -433,7 +491,7 @@ const GorgiasOverview = () => {
<TableHead>Agent</TableHead> <TableHead>Agent</TableHead>
<TableHead className="text-right">Closed</TableHead> <TableHead className="text-right">Closed</TableHead>
<TableHead className="text-right">Rating</TableHead> <TableHead className="text-right">Rating</TableHead>
<TableHead className="text-right">Δ</TableHead> <TableHead className="text-right">Change</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
@@ -459,8 +517,18 @@ const GorgiasOverview = () => {
: "dark:text-gray-300" : "dark:text-gray-300"
}`} }`}
> >
{agent.delta > 0 ? "+" : ""} <div className="flex items-center justify-end gap-0.5">
{agent.delta} {agent.delta !== 0 && (
<>
{agent.delta > 0 ? (
<ArrowUp className="w-3 h-3" />
) : (
<ArrowDown className="w-3 h-3" />
)}
<span>{Math.abs(agent.delta)}%</span>
</>
)}
</div>
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}