Fix cards layout/style, add today period
This commit is contained in:
BIN
dashboard-server/gorgias-server/._package-lock.json
generated
Normal file
BIN
dashboard-server/gorgias-server/._package-lock.json
generated
Normal file
Binary file not shown.
BIN
dashboard-server/gorgias-server/._package.json
Normal file
BIN
dashboard-server/gorgias-server/._package.json
Normal file
Binary file not shown.
BIN
dashboard-server/gorgias-server/._routes
Normal file
BIN
dashboard-server/gorgias-server/._routes
Normal file
Binary file not shown.
BIN
dashboard-server/gorgias-server/._services
Normal file
BIN
dashboard-server/gorgias-server/._services
Normal file
Binary file not shown.
@@ -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}
|
||||||
<div className="grid grid-cols-2 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
suffix="%"
|
||||||
|
icon={MessageSquare}
|
||||||
|
colorClass="indigo"
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user