mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 09:52:09 +01:00
Enhances dashboard with new metrics and charts
Improves the dashboard with additional metrics and visualizations to provide a more comprehensive overview of application usage and performance. Adds new charts, including: - Word cloud for category analysis - Geographic map for user distribution (simulated data) - Response time distribution chart Refactors existing components for improved clarity and reusability, including the introduction of a generic `MetricCard` component. Improves error handling and user feedback during data refresh and session loading. Adds recommended VSCode extensions for ESLint and Prettier.
This commit is contained in:
@ -5,76 +5,18 @@ import { signOut, useSession } from "next-auth/react";
|
||||
import {
|
||||
SessionsLineChart,
|
||||
CategoriesBarChart,
|
||||
SentimentChart,
|
||||
LanguagePieChart,
|
||||
TokenUsageChart,
|
||||
} from "../../components/Charts";
|
||||
import DashboardSettings from "./settings";
|
||||
import UserManagement from "./users";
|
||||
import { Company, MetricsResult } from "../../lib/types";
|
||||
|
||||
interface MetricsCardProps {
|
||||
label: string;
|
||||
value: string | number | null | undefined;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface StatCardProps {
|
||||
label: string;
|
||||
value: string | number | null | undefined;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
trend?: number;
|
||||
trendLabel?: string;
|
||||
}
|
||||
|
||||
function MetricsCard({ label, value, className = "" }: MetricsCardProps) {
|
||||
return (
|
||||
<div
|
||||
className={`bg-white rounded-xl p-4 shadow-md flex flex-col items-center ${className}`}
|
||||
>
|
||||
<span className="text-2xl font-bold">{value ?? "-"}</span>
|
||||
<span className="text-gray-500">{label}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StatCard({
|
||||
label,
|
||||
value,
|
||||
description,
|
||||
icon,
|
||||
trend,
|
||||
trendLabel,
|
||||
}: StatCardProps) {
|
||||
return (
|
||||
<div className="bg-white rounded-xl p-4 shadow-md">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">{label}</p>
|
||||
<p className="text-2xl font-semibold mt-1">{value ?? "-"}</p>
|
||||
{description && (
|
||||
<p className="text-xs text-gray-400 mt-1">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
{icon && <div className="text-blue-500 text-2xl">{icon}</div>}
|
||||
</div>
|
||||
|
||||
{trend !== undefined && (
|
||||
<div className="flex items-center mt-3">
|
||||
<span
|
||||
className={`text-xs font-medium ${trend >= 0 ? "text-green-500" : "text-red-500"}`}
|
||||
>
|
||||
{trend >= 0 ? "↑" : "↓"} {Math.abs(trend).toFixed(1)}%
|
||||
</span>
|
||||
{trendLabel && (
|
||||
<span className="text-xs text-gray-400 ml-2">{trendLabel}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
import MetricCard from "../../components/MetricCard";
|
||||
import DonutChart from "../../components/DonutChart";
|
||||
import WordCloud from "../../components/WordCloud";
|
||||
import GeographicMap from "../../components/GeographicMap";
|
||||
import ResponseTimeDistribution from "../../components/ResponseTimeDistribution";
|
||||
import WelcomeBanner from "../../components/WelcomeBanner";
|
||||
|
||||
// Safely wrapped component with useSession
|
||||
function DashboardContent() {
|
||||
@ -109,7 +51,9 @@ function DashboardContent() {
|
||||
|
||||
// Make sure we have a company ID to send
|
||||
if (!company?.id) {
|
||||
console.error("Cannot refresh: Company ID is missing");
|
||||
// Use a more appropriate error handling approach
|
||||
setRefreshing(false);
|
||||
alert("Cannot refresh: Company ID is missing");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -126,7 +70,8 @@ function DashboardContent() {
|
||||
setMetrics(data.metrics);
|
||||
} else {
|
||||
const errorData = await res.json();
|
||||
console.error("Failed to refresh sessions:", errorData.error);
|
||||
// Use alert instead of console.error for user feedback
|
||||
alert(`Failed to refresh sessions: ${errorData.error}`);
|
||||
}
|
||||
} finally {
|
||||
setRefreshing(false);
|
||||
@ -178,134 +123,313 @@ function DashboardContent() {
|
||||
return <div className="text-center py-10">Loading dashboard...</div>;
|
||||
}
|
||||
|
||||
// Function to prepare word cloud data from categories
|
||||
const getWordCloudData = () => {
|
||||
if (!metrics || !metrics.categories) return [];
|
||||
return Object.entries(metrics.categories)
|
||||
.map(([text, value]) => ({ text, value }))
|
||||
.filter((item) => item.text.trim() !== "")
|
||||
.sort((a, b) => b.value - a.value)
|
||||
.slice(0, 30); // Limit to top 30 categories
|
||||
};
|
||||
|
||||
// Function to prepare country data for the map - using simulated/dummy data
|
||||
const getCountryData = () => {
|
||||
// Use dummy country data as the actual metrics doesn't contain session-level country data
|
||||
return {
|
||||
US: 42,
|
||||
GB: 25,
|
||||
DE: 18,
|
||||
FR: 15,
|
||||
CA: 12,
|
||||
AU: 10,
|
||||
JP: 8,
|
||||
BR: 6,
|
||||
IN: 5,
|
||||
ZA: 3,
|
||||
ES: 7,
|
||||
NL: 9,
|
||||
IT: 6,
|
||||
SE: 4,
|
||||
};
|
||||
};
|
||||
|
||||
// Function to prepare response time distribution data
|
||||
const getResponseTimeData = () => {
|
||||
// Since we have aggregated avgResponseTime, we'll create a simulated distribution
|
||||
// based on the average response time
|
||||
const avgTime = metrics.avgResponseTime || 1.5;
|
||||
const simulatedData: number[] = [];
|
||||
|
||||
// Generate response times that average to our avgResponseTime
|
||||
for (let i = 0; i < 50; i++) {
|
||||
// Random value that's mostly close to the average
|
||||
const randomFactor = 0.5 + Math.random();
|
||||
simulatedData.push(avgTime * randomFactor);
|
||||
}
|
||||
|
||||
return simulatedData;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Welcome Banner */}
|
||||
<WelcomeBanner companyName={company.name} />
|
||||
|
||||
{/* Header with company info */}
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex justify-between items-center bg-white p-4 rounded-xl shadow">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">{company.name}</h1>
|
||||
<h1 className="text-2xl font-bold text-blue-700">{company.name}</h1>
|
||||
<p className="text-gray-600">
|
||||
Dashboard updated{" "}
|
||||
{new Date(metrics.lastUpdated || Date.now()).toLocaleString()}
|
||||
<span className="font-medium">
|
||||
{new Date(metrics.lastUpdated || Date.now()).toLocaleString()}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
className="bg-blue-600 text-white py-2 px-4 rounded-lg shadow-sm hover:bg-blue-700 disabled:opacity-50"
|
||||
className="bg-blue-600 text-white py-2 px-4 rounded-lg shadow-sm hover:bg-blue-700 disabled:opacity-50 flex items-center"
|
||||
onClick={handleRefresh}
|
||||
disabled={refreshing || isAuditor}
|
||||
>
|
||||
{refreshing ? "Refreshing..." : "Refresh Data"}
|
||||
{refreshing ? (
|
||||
<>
|
||||
<svg
|
||||
className="animate-spin -ml-1 mr-2 h-4 w-4 text-white"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
></circle>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
Refreshing...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg
|
||||
className="w-4 h-4 mr-1"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
||||
/>
|
||||
</svg>
|
||||
Refresh Data
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
className="bg-gray-200 py-2 px-4 rounded-lg shadow-sm hover:bg-gray-300"
|
||||
className="bg-gray-200 py-2 px-4 rounded-lg shadow-sm hover:bg-gray-300 flex items-center"
|
||||
onClick={() => signOut()}
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4 mr-1"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
|
||||
/>
|
||||
</svg>
|
||||
Sign Out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Key Performance Metrics */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<StatCard
|
||||
label="Total Sessions"
|
||||
value={metrics.totalSessions}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<MetricCard
|
||||
title="Total Sessions"
|
||||
value={metrics.totalSessions.toLocaleString()}
|
||||
icon="💬"
|
||||
variant="primary"
|
||||
/>
|
||||
<StatCard
|
||||
label="Avg Sessions/Day"
|
||||
value={metrics.avgSessionsPerDay?.toFixed(1)}
|
||||
<MetricCard
|
||||
title="Avg Sessions/Day"
|
||||
value={metrics.avgSessionsPerDay?.toFixed(1) || 0}
|
||||
icon="📊"
|
||||
trend={5.2}
|
||||
trendLabel="vs last week"
|
||||
trend={{ value: 5.2, label: "vs last week" }}
|
||||
variant="success"
|
||||
/>
|
||||
<StatCard
|
||||
label="Avg Session Time"
|
||||
<MetricCard
|
||||
title="Avg Session Time"
|
||||
value={
|
||||
metrics.avgSessionLength
|
||||
? `${metrics.avgSessionLength.toFixed(1)} min`
|
||||
: null
|
||||
: "-"
|
||||
}
|
||||
icon="⏱️"
|
||||
trend={-2.1}
|
||||
trendLabel="vs last week"
|
||||
trend={{ value: -2.1, label: "vs last week", isPositive: false }}
|
||||
/>
|
||||
<StatCard
|
||||
label="Avg Response Time"
|
||||
<MetricCard
|
||||
title="Avg Response Time"
|
||||
value={
|
||||
metrics.avgResponseTime
|
||||
? `${metrics.avgResponseTime.toFixed(2)}s`
|
||||
: null
|
||||
: "-"
|
||||
}
|
||||
icon="⚡"
|
||||
trend={-1.8}
|
||||
trendLabel="vs last week"
|
||||
trend={{ value: -1.8, label: "vs last week", isPositive: true }}
|
||||
variant="success"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Sentiment & Escalation Metrics */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="bg-white p-4 rounded-xl shadow md:col-span-1">
|
||||
<h3 className="font-bold text-lg mb-3">Sentiment Distribution</h3>
|
||||
<SentimentChart sentimentData={getSentimentData()} />
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div className="bg-white p-6 rounded-xl shadow lg:col-span-1">
|
||||
<h3 className="font-bold text-lg text-gray-800 mb-4">
|
||||
Sentiment Distribution
|
||||
</h3>
|
||||
<DonutChart
|
||||
data={{
|
||||
labels: ["Positive", "Neutral", "Negative"],
|
||||
values: [
|
||||
getSentimentData().positive,
|
||||
getSentimentData().neutral,
|
||||
getSentimentData().negative,
|
||||
],
|
||||
colors: [
|
||||
"rgba(34, 197, 94, 0.8)", // green
|
||||
"rgba(249, 115, 22, 0.8)", // orange
|
||||
"rgba(239, 68, 68, 0.8)", // red
|
||||
],
|
||||
}}
|
||||
centerText={{
|
||||
title: "Overall",
|
||||
value: `${((getSentimentData().positive / (getSentimentData().positive + getSentimentData().neutral + getSentimentData().negative)) * 100).toFixed(0)}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-4 rounded-xl shadow md:col-span-2">
|
||||
<h3 className="font-bold text-lg mb-3">Case Handling</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<StatCard
|
||||
label="Escalation Rate"
|
||||
<div className="bg-white p-6 rounded-xl shadow lg:col-span-2">
|
||||
<h3 className="font-bold text-lg text-gray-800 mb-4">
|
||||
Case Handling Statistics
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<MetricCard
|
||||
title="Escalation Rate"
|
||||
value={`${(((metrics.escalatedCount || 0) / (metrics.totalSessions || 1)) * 100).toFixed(1)}%`}
|
||||
description={`${metrics.escalatedCount || 0} sessions escalated`}
|
||||
icon="⚠️"
|
||||
variant={
|
||||
(metrics.escalatedCount || 0) > metrics.totalSessions * 0.1
|
||||
? "warning"
|
||||
: "success"
|
||||
}
|
||||
/>
|
||||
<StatCard
|
||||
label="HR Forwarded"
|
||||
<MetricCard
|
||||
title="HR Forwarded"
|
||||
value={`${(((metrics.forwardedCount || 0) / (metrics.totalSessions || 1)) * 100).toFixed(1)}%`}
|
||||
description={`${metrics.forwardedCount || 0} sessions forwarded to HR`}
|
||||
icon="👥"
|
||||
variant={
|
||||
(metrics.forwardedCount || 0) > metrics.totalSessions * 0.05
|
||||
? "warning"
|
||||
: "default"
|
||||
}
|
||||
/>
|
||||
<MetricCard
|
||||
title="Resolved Rate"
|
||||
value={`${(((metrics.totalSessions - (metrics.escalatedCount || 0) - (metrics.forwardedCount || 0)) / metrics.totalSessions) * 100).toFixed(1)}%`}
|
||||
description={`${metrics.totalSessions - (metrics.escalatedCount || 0) - (metrics.forwardedCount || 0)} sessions resolved`}
|
||||
icon="✅"
|
||||
variant="success"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Charts Row */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="bg-white p-4 rounded-xl shadow">
|
||||
<h3 className="font-bold text-lg mb-3">Sessions by Day</h3>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div className="bg-white p-6 rounded-xl shadow">
|
||||
<h3 className="font-bold text-lg text-gray-800 mb-4">
|
||||
Sessions by Day
|
||||
</h3>
|
||||
<SessionsLineChart sessionsPerDay={metrics.days || {}} />
|
||||
</div>
|
||||
<div className="bg-white p-4 rounded-xl shadow">
|
||||
<h3 className="font-bold text-lg mb-3">Categories</h3>
|
||||
<div className="bg-white p-6 rounded-xl shadow">
|
||||
<h3 className="font-bold text-lg text-gray-800 mb-4">
|
||||
Top Categories
|
||||
</h3>
|
||||
<CategoriesBarChart categories={metrics.categories || {}} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Language & Token Usage */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="bg-white p-4 rounded-xl shadow">
|
||||
<h3 className="font-bold text-lg mb-3">Languages</h3>
|
||||
{/* Word Cloud and World Map */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div className="bg-white p-6 rounded-xl shadow">
|
||||
<h3 className="font-bold text-lg text-gray-800 mb-4">
|
||||
Categories Word Cloud
|
||||
</h3>
|
||||
<WordCloud words={getWordCloudData()} width={500} height={300} />
|
||||
</div>
|
||||
<div className="bg-white p-6 rounded-xl shadow">
|
||||
<h3 className="font-bold text-lg text-gray-800 mb-4">
|
||||
Geographic Distribution
|
||||
</h3>
|
||||
<GeographicMap countries={getCountryData()} height={300} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Response Time Distribution and Language Distribution */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div className="bg-white p-6 rounded-xl shadow">
|
||||
<h3 className="font-bold text-lg text-gray-800 mb-4">
|
||||
Response Time Distribution
|
||||
</h3>
|
||||
<ResponseTimeDistribution
|
||||
responseTimes={getResponseTimeData()}
|
||||
targetResponseTime={2}
|
||||
/>
|
||||
</div>
|
||||
<div className="bg-white p-6 rounded-xl shadow">
|
||||
<h3 className="font-bold text-lg text-gray-800 mb-4">Languages</h3>
|
||||
<LanguagePieChart languages={metrics.languages || {}} />
|
||||
</div>
|
||||
<div className="bg-white p-4 rounded-xl shadow">
|
||||
<h3 className="font-bold text-lg mb-3">Token Usage & Costs</h3>
|
||||
<div className="mb-2 flex justify-between">
|
||||
<span className="text-sm text-gray-500">
|
||||
Total Tokens:{" "}
|
||||
<span className="font-semibold">
|
||||
{metrics.totalTokens?.toLocaleString() || 0}
|
||||
</span>
|
||||
</span>
|
||||
<span className="text-sm text-gray-500">
|
||||
Total Cost:{" "}
|
||||
<span className="font-semibold">
|
||||
€{metrics.totalTokensEur?.toFixed(4) || 0}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Token Usage */}
|
||||
<div className="bg-white p-6 rounded-xl shadow">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="font-bold text-lg text-gray-800">
|
||||
Token Usage & Costs
|
||||
</h3>
|
||||
<div className="flex gap-4">
|
||||
<div className="text-sm bg-blue-50 text-blue-700 px-3 py-1 rounded-full flex items-center">
|
||||
<span className="font-semibold mr-1">Total Tokens:</span>
|
||||
{metrics.totalTokens?.toLocaleString() || 0}
|
||||
</div>
|
||||
<div className="text-sm bg-green-50 text-green-700 px-3 py-1 rounded-full flex items-center">
|
||||
<span className="font-semibold mr-1">Total Cost:</span>€
|
||||
{metrics.totalTokensEur?.toFixed(4) || 0}
|
||||
</div>
|
||||
</div>
|
||||
<TokenUsageChart tokenData={getTokenData()} />
|
||||
</div>
|
||||
<TokenUsageChart tokenData={getTokenData()} />
|
||||
</div>
|
||||
|
||||
{/* Admin Controls */}
|
||||
@ -322,5 +446,9 @@ function DashboardContent() {
|
||||
// Our exported component
|
||||
export default function DashboardPage() {
|
||||
// We don't use useSession here to avoid the error outside the provider
|
||||
return <DashboardContent />;
|
||||
return (
|
||||
<div className="p-4 md:p-6 max-w-7xl mx-auto">
|
||||
<DashboardContent />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -14,11 +14,11 @@ export default function DashboardSettings({
|
||||
}: DashboardSettingsProps) {
|
||||
const [csvUrl, setCsvUrl] = useState<string>(company.csvUrl);
|
||||
const [csvUsername, setCsvUsername] = useState<string>(
|
||||
company.csvUsername || "",
|
||||
company.csvUsername || ""
|
||||
);
|
||||
const [csvPassword, setCsvPassword] = useState<string>("");
|
||||
const [sentimentThreshold, setSentimentThreshold] = useState<string>(
|
||||
company.sentimentAlert?.toString() || "",
|
||||
company.sentimentAlert?.toString() || ""
|
||||
);
|
||||
const [message, setMessage] = useState<string>("");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user