Files
dashboard/examples DO NOT USE OR EDIT/EXAMPLE ONLY GorgiasSummary.jsx

402 lines
14 KiB
JavaScript

import React, { useState, useEffect, useCallback } from "react";
import gorgiasService from "../../services/gorgiasService";
import { getDateRange } from "../../utils/dateUtils";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import {
Select,
SelectTrigger,
SelectContent,
SelectItem,
} from "@/components/ui/select";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Skeleton } from "@/components/ui/skeleton";
import { Clock, Star, Users, MessageSquare, Mail, Send } from "lucide-react";
const TIME_RANGES = {
7: "Last 7 Days",
14: "Last 14 Days",
30: "Last 30 Days",
90: "Last 90 Days",
};
const formatDuration = (seconds) => {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
};
const MetricCard = ({
title,
value,
delta,
suffix = "",
icon: Icon,
colorClass = "blue",
more_is_better = true,
loading = false,
}) => {
const getDeltaColor = (d) => {
if (d === 0) return "text-gray-600 dark:text-gray-400";
const isPositive = d > 0;
return isPositive === more_is_better
? "text-green-600 dark:text-green-500"
: "text-red-600 dark:text-red-500";
};
const formatDelta = (d) => {
if (d === undefined || d === null) return null;
if (d === 0) return "0";
return (d > 0 ? "+" : "") + 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 (
<div className={`p-3 rounded-lg border transition-colors ${baseColors}`}>
<h3 className="text-sm font-medium text-gray-600 dark:text-gray-300">
{title}
</h3>
<div className="flex items-baseline gap-2 mt-1">
{loading ? (
<Skeleton className="h-8 w-24 dark:bg-gray-700" />
) : (
<>
<div className="flex items-center gap-2">
{Icon && <Icon className="w-4 h-4" />}
<p className="text-xl font-bold">
{typeof value === "number"
? value.toLocaleString() + suffix
: value}
</p>
</div>
{delta !== undefined && (
<p className={`text-xs font-medium ${getDeltaColor(delta)}`}>
{formatDelta(delta)}
</p>
)}
</>
)}
</div>
</div>
);
};
const TableSkeleton = () => (
<div className="space-y-2">
<Skeleton className="h-8 w-full dark:bg-gray-700" />
<Skeleton className="h-8 w-full dark:bg-gray-700" />
<Skeleton className="h-8 w-full dark:bg-gray-700" />
<Skeleton className="h-8 w-full dark:bg-gray-700" />
</div>
);
const GorgiasSummary = () => {
const [timeRange, setTimeRange] = useState(7);
const [data, setData] = useState({});
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const loadStats = useCallback(async () => {
setLoading(true);
const filters = getDateRange(timeRange);
try {
const [overview, channelStats, agentStats, satisfaction, selfService] =
await Promise.all([
gorgiasService.fetchStatistics("overview", filters),
gorgiasService.fetchStatistics(
"tickets-created-per-channel",
filters
),
gorgiasService.fetchStatistics("tickets-closed-per-agent", filters),
gorgiasService.fetchStatistics("satisfaction-surveys", filters),
gorgiasService.fetchStatistics("self-service-overview", filters),
]);
setData({
overview: overview.data.data.data || [],
channels: channelStats.data.data.data.lines || [],
agents: agentStats.data.data.data.lines || [],
satisfaction: satisfaction.data.data.data || [],
selfService: selfService.data.data.data || [],
});
setError(null);
} catch (err) {
console.error("Error loading stats:", err);
setError(err.message);
} finally {
setLoading(false);
}
}, [timeRange]);
useEffect(() => {
loadStats();
// Set up auto-refresh every 5 minutes
const interval = setInterval(loadStats, 5 * 60 * 1000);
return () => clearInterval(interval);
}, [loadStats]);
// Convert overview array to object for easier access
const stats =
data.overview?.reduce((acc, item) => {
acc[item.name] = item;
return acc;
}, {}) || {};
// Process satisfaction data
const satisfactionStats =
data.satisfaction?.reduce((acc, item) => {
acc[item.name] = item;
return acc;
}, {}) || {};
// Process self-service data
const selfServiceStats =
data.selfService?.reduce((acc, item) => {
acc[item.name] = item;
return acc;
}, {}) || {};
if (error) return <p className="text-red-500">Error: {error}</p>;
return (
<Card className="bg-white dark:bg-gray-900">
<CardHeader>
<div className="flex justify-between items-center">
<h2 className="text-xl font-bold text-gray-900 dark:text-gray-100">
Customer Service
</h2>
<div className="flex items-center gap-2">
<Select
value={String(timeRange)}
onValueChange={(value) => setTimeRange(Number(value))}
>
<SelectTrigger className="w-[140px] bg-white dark:bg-gray-800">
{TIME_RANGES[timeRange]}
</SelectTrigger>
<SelectContent>
{Object.entries(TIME_RANGES).map(([value, label]) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 md:grid-cols-2 lg:grid-cols-4 gap-4">
{/* Message & Response Metrics */}
<MetricCard
title="Messages Received"
value={stats.total_messages_received?.value}
delta={stats.total_messages_received?.delta}
icon={Mail}
colorClass="blue"
loading={loading}
/>
<MetricCard
title="Messages Sent"
value={stats.total_messages_sent?.value}
delta={stats.total_messages_sent?.delta}
icon={Send}
colorClass="green"
loading={loading}
/>
<MetricCard
title="First Response"
value={formatDuration(stats.median_first_response_time?.value)}
delta={stats.median_first_response_time?.delta}
icon={Clock}
colorClass="purple"
more_is_better={false}
loading={loading}
/>
<MetricCard
title="One-Touch Rate"
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 */}
<MetricCard
title="Customer Satisfaction"
value={`${satisfactionStats.average_rating?.value}/5`}
delta={satisfactionStats.average_rating?.delta}
suffix="%"
icon={Star}
colorClass="orange"
loading={loading}
/>
<MetricCard
title="Survey Response Rate"
value={satisfactionStats.response_rate?.value}
delta={satisfactionStats.response_rate?.delta}
suffix="%"
colorClass="orange"
loading={loading}
/>
<MetricCard
title="Resolution Time"
value={formatDuration(stats.median_resolution_time?.value)}
delta={stats.median_resolution_time?.delta}
icon={Clock}
colorClass="teal"
more_is_better={false}
loading={loading}
/>
<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 className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Channel Distribution */}
<div className="bg-white dark:bg-gray-900 rounded-lg border dark:border-gray-800 p-4 pt-0">
<div className="p-4 pl-2">
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
Channel Distribution
</h3>
</div>
{loading ? (
<div className="p-4">
<TableSkeleton />
</div>
) : (
<Table>
<TableHeader>
<TableRow className="dark:border-gray-800">
<TableHead>Channel</TableHead>
<TableHead className="text-right">Total</TableHead>
<TableHead className="text-right">%</TableHead>
<TableHead className="text-right">Δ</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.channels
.sort((a, b) => b[1].value - a[1].value)
.map((line, index) => (
<TableRow key={index} className="dark:border-gray-800">
<TableCell className="dark:text-gray-300">
{line[0].value}
</TableCell>
<TableCell className="text-right dark:text-gray-300">
{line[1].value}
</TableCell>
<TableCell className="text-right dark:text-gray-300">
{line[2].value}%
</TableCell>
<TableCell
className={`text-right ${
line[3].value > 0
? "text-green-600 dark:text-green-500"
: line[3].value < 0
? "text-red-600 dark:text-red-500"
: "dark:text-gray-300"
}`}
>
{line[3].value > 0 ? "+" : ""}
{line[3].value}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</div>
{/* Agent Performance - Same dark mode updates */}
<div className="bg-white dark:bg-gray-900 rounded-lg border dark:border-gray-800 p-4 pt-0">
<div className="p-4 pl-2">
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
Agent Performance
</h3>
</div>
{loading ? (
<div className="p-4">
<TableSkeleton />
</div>
) : (
<Table>
<TableHeader>
<TableRow className="dark:border-gray-800">
<TableHead>Agent</TableHead>
<TableHead className="text-right">Closed</TableHead>
<TableHead className="text-right">Rating</TableHead>
<TableHead className="text-right">Δ</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.agents
.filter((line) => line[0].value !== "Unassigned")
.map((line, index) => (
<TableRow key={index} className="dark:border-gray-800">
<TableCell className="dark:text-gray-300">
{line[0].value}
</TableCell>
<TableCell className="text-right dark:text-gray-300">
{line[1].value}
</TableCell>
<TableCell className="text-right dark:text-gray-300">
{line[2].value ? `${line[2].value}/5` : "-"}
</TableCell>
<TableCell
className={`text-right ${
line[4].value > 0
? "text-green-600 dark:text-green-500"
: line[4].value < 0
? "text-red-600 dark:text-red-500"
: "dark:text-gray-300"
}`}
>
{line[4].value > 0 ? "+" : ""}
{line[4].value}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</div>
</div>
</CardContent>
</Card>
);
};
export default GorgiasSummary;