Add comprehensive dashboard features

Introduce company settings, user management, and layout components
Implement session-based Company and User pages for admin access
Integrate chart components for dynamic data visualization
Add Sidebar for modular navigation
Revamp global styles with Tailwind CSS

Enhances user experience and administrative control
This commit is contained in:
2025-05-22 14:12:36 +02:00
parent a17b66c078
commit e3134aa451
12 changed files with 1302 additions and 454 deletions

View File

@ -0,0 +1,173 @@
"use client";
import { useState, useEffect } from "react";
import { useSession } from "next-auth/react";
import { Company } from "../../lib/types";
export default function CompanySettingsPage() {
const { data: session, status } = useSession();
const [company, setCompany] = useState<Company | null>(null);
const [csvUrl, setCsvUrl] = useState<string>("");
const [csvUsername, setCsvUsername] = useState<string>("");
const [csvPassword, setCsvPassword] = useState<string>("");
const [sentimentThreshold, setSentimentThreshold] = useState<string>("");
const [message, setMessage] = useState<string>("");
const [loading, setLoading] = useState(true);
useEffect(() => {
if (status === "authenticated") {
const fetchCompany = async () => {
setLoading(true);
try {
const res = await fetch("/api/dashboard/config");
const data = await res.json();
setCompany(data.company);
setCsvUrl(data.company.csvUrl || "");
setCsvUsername(data.company.csvUsername || "");
setSentimentThreshold(data.company.sentimentAlert?.toString() || "");
} catch (error) {
console.error("Failed to fetch company settings:", error);
setMessage("Failed to load company settings.");
} finally {
setLoading(false);
}
};
fetchCompany();
}
}, [status]);
async function handleSave() {
setMessage("");
try {
const res = await fetch("/api/dashboard/settings", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
csvUrl,
csvUsername,
csvPassword,
sentimentThreshold,
}),
});
if (res.ok) {
setMessage("Settings saved successfully!");
// Update local state if needed
const data = await res.json();
setCompany(data.company);
} else {
const error = await res.json();
setMessage(
`Failed to save settings: ${error.message || "Unknown error"}`
);
}
} catch (error) {
setMessage("Failed to save settings. Please try again.");
console.error("Error saving settings:", error);
}
}
// Loading state
if (loading) {
return <div className="text-center py-10">Loading settings...</div>;
}
// Check for admin access
if (session?.user?.role !== "admin") {
return (
<div className="text-center py-10 bg-white rounded-xl shadow p-6">
<h2 className="font-bold text-xl text-red-600 mb-2">Access Denied</h2>
<p>You don't have permission to view company settings.</p>
</div>
);
}
return (
<div className="space-y-6">
<div className="bg-white p-6 rounded-xl shadow">
<h1 className="text-2xl font-bold text-gray-800 mb-6">
Company Settings
</h1>
{message && (
<div
className={`p-4 rounded mb-6 ${message.includes("Failed") ? "bg-red-100 text-red-700" : "bg-green-100 text-green-700"}`}
>
{message}
</div>
)}
<form
className="grid gap-6"
onSubmit={(e) => {
e.preventDefault();
handleSave();
}}
>
<div className="grid gap-2">
<label className="font-medium text-gray-700">
CSV Data Source URL
</label>
<input
type="text"
className="border border-gray-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-sky-500"
value={csvUrl}
onChange={(e) => setCsvUrl(e.target.value)}
placeholder="https://example.com/data.csv"
/>
</div>
<div className="grid gap-2">
<label className="font-medium text-gray-700">CSV Username</label>
<input
type="text"
className="border border-gray-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-sky-500"
value={csvUsername}
onChange={(e) => setCsvUsername(e.target.value)}
placeholder="Username for CSV access (if needed)"
/>
</div>
<div className="grid gap-2">
<label className="font-medium text-gray-700">CSV Password</label>
<input
type="password"
className="border border-gray-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-sky-500"
value={csvPassword}
onChange={(e) => setCsvPassword(e.target.value)}
placeholder="Password will be updated only if provided"
/>
<p className="text-sm text-gray-500">
Leave blank to keep current password
</p>
</div>
<div className="grid gap-2">
<label className="font-medium text-gray-700">
Sentiment Alert Threshold
</label>
<input
type="number"
className="border border-gray-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-sky-500"
value={sentimentThreshold}
onChange={(e) => setSentimentThreshold(e.target.value)}
placeholder="Threshold value (0-100)"
min="0"
max="100"
/>
<p className="text-sm text-gray-500">
Percentage of negative sentiment sessions to trigger alert (0-100)
</p>
</div>
<button
type="submit"
className="bg-sky-600 hover:bg-sky-700 text-white py-2 px-4 rounded-lg shadow transition-colors w-full sm:w-auto"
>
Save Settings
</button>
</form>
</div>
</div>
);
}

46
app/dashboard/layout.tsx Normal file
View File

@ -0,0 +1,46 @@
"use client";
import { ReactNode } from "react";
import { useSession, signOut } from "next-auth/react";
import { useRouter } from "next/navigation";
import Sidebar from "../../components/Sidebar";
export default function DashboardLayout({ children }: { children: ReactNode }) {
const { data: session, status } = useSession();
const router = useRouter();
// Redirect if not authenticated
if (status === "unauthenticated") {
router.push("/login");
return (
<div className="flex h-screen items-center justify-center">
<div className="text-center">Redirecting to login...</div>
</div>
);
}
// Show loading state while session status is being determined
if (status === "loading") {
return (
<div className="flex h-screen items-center justify-center">
<div className="text-center">Loading session...</div>
</div>
);
}
const handleLogout = () => {
signOut({ callbackUrl: "/login" });
};
return (
<div className="flex h-screen bg-gray-100">
{/* Sidebar with logout handler passed as prop */}
<Sidebar />
{/* Main content */}
<div className="flex-1 overflow-auto p-8">
<div className="mx-auto max-w-7xl">{children}</div>
</div>
</div>
);
}

View File

@ -0,0 +1,437 @@
"use client";
import { useEffect, useState } from "react";
import { signOut, useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
import {
SessionsLineChart,
CategoriesBarChart,
LanguagePieChart,
TokenUsageChart,
} from "../../../components/Charts";
import { Company, MetricsResult, WordCloudWord } from "../../../lib/types";
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() {
const { data: session, status } = useSession(); // Add status from useSession
const router = useRouter(); // Initialize useRouter
const [metrics, setMetrics] = useState<MetricsResult | null>(null);
const [company, setCompany] = useState<Company | null>(null);
const [, setLoading] = useState<boolean>(false);
const [refreshing, setRefreshing] = useState<boolean>(false);
const isAuditor = session?.user?.role === "auditor";
useEffect(() => {
// Redirect if not authenticated
if (status === "unauthenticated") {
router.push("/login");
return; // Stop further execution in this effect
}
// Fetch metrics and company on mount if authenticated
if (status === "authenticated") {
const fetchData = async () => {
setLoading(true);
const res = await fetch("/api/dashboard/metrics");
const data = await res.json();
setMetrics(data.metrics);
setCompany(data.company);
setLoading(false);
};
fetchData();
}
}, [status, router]); // Add status and router to dependency array
async function handleRefresh() {
if (isAuditor) return; // Prevent auditors from refreshing
try {
setRefreshing(true);
// Make sure we have a company ID to send
if (!company?.id) {
setRefreshing(false);
alert("Cannot refresh: Company ID is missing");
return;
}
const res = await fetch("/api/admin/refresh-sessions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ companyId: company.id }),
});
if (res.ok) {
// Refetch metrics
const metricsRes = await fetch("/api/dashboard/metrics");
const data = await metricsRes.json();
setMetrics(data.metrics);
} else {
const errorData = await res.json();
alert(`Failed to refresh sessions: ${errorData.error}`);
}
} finally {
setRefreshing(false);
}
}
// Calculate sentiment distribution
const getSentimentData = () => {
if (!metrics) return { positive: 0, neutral: 0, negative: 0 };
if (
metrics.sentimentPositiveCount !== undefined &&
metrics.sentimentNeutralCount !== undefined &&
metrics.sentimentNegativeCount !== undefined
) {
return {
positive: metrics.sentimentPositiveCount,
neutral: metrics.sentimentNeutralCount,
negative: metrics.sentimentNegativeCount,
};
}
const total = metrics.totalSessions || 1;
return {
positive: Math.round(total * 0.6),
neutral: Math.round(total * 0.3),
negative: Math.round(total * 0.1),
};
};
// Prepare token usage data
const getTokenData = () => {
if (!metrics || !metrics.tokensByDay) {
return { labels: [], values: [], costs: [] };
}
const days = Object.keys(metrics.tokensByDay).sort();
const labels = days.slice(-7);
const values = labels.map((day) => metrics.tokensByDay?.[day] || 0);
const costs = labels.map((day) => metrics.tokensCostByDay?.[day] || 0);
return { labels, values, costs };
};
// Show loading state while session status is being determined
if (status === "loading") {
return <div className="text-center py-10">Loading session...</div>;
}
// If unauthenticated and not redirected yet (should be handled by useEffect, but as a fallback)
if (status === "unauthenticated") {
return <div className="text-center py-10">Redirecting to login...</div>;
}
if (!metrics || !company) {
return <div className="text-center py-10">Loading dashboard...</div>;
}
// Function to prepare word cloud data from metrics.wordCloudData
const getWordCloudData = (): WordCloudWord[] => {
if (!metrics || !metrics.wordCloudData) return [];
return metrics.wordCloudData;
};
// Function to prepare country data for the map using actual metrics
const getCountryData = () => {
if (!metrics || !metrics.countries) return {};
// Convert the countries object from metrics to the format expected by GeographicMap
const result = Object.entries(metrics.countries).reduce(
(acc, [code, count]) => {
if (code && count) {
acc[code] = count;
}
return acc;
},
{} as Record<string, number>
);
return result;
};
// Function to prepare response time distribution data
const getResponseTimeData = () => {
const avgTime = metrics.avgResponseTime || 1.5;
const simulatedData: number[] = [];
for (let i = 0; i < 50; i++) {
const randomFactor = 0.5 + Math.random();
simulatedData.push(avgTime * randomFactor);
}
return simulatedData;
};
return (
<div className="space-y-8">
<WelcomeBanner companyName={company.name} />
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center bg-white p-6 rounded-2xl shadow-lg ring-1 ring-slate-200/50">
<div>
<h1 className="text-3xl font-bold text-slate-800">{company.name}</h1>
<p className="text-slate-500 mt-1">
Dashboard updated{" "}
<span className="font-medium text-slate-600">
{new Date(metrics.lastUpdated || Date.now()).toLocaleString()}
</span>
</p>
</div>
<div className="flex items-center gap-3 mt-4 sm:mt-0">
<button
className="bg-sky-600 text-white py-2 px-5 rounded-lg shadow hover:bg-sky-700 transition-colors disabled:opacity-60 disabled:cursor-not-allowed flex items-center text-sm font-medium"
onClick={handleRefresh}
disabled={refreshing || isAuditor}
>
{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...
</>
) : (
"Refresh Data"
)}
</button>
<button
className="bg-slate-100 text-slate-700 py-2 px-5 rounded-lg shadow hover:bg-slate-200 transition-colors flex items-center text-sm font-medium"
onClick={() => signOut({ callbackUrl: "/login" })}
>
Sign out
</button>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<MetricCard
title="Total Sessions"
value={metrics.totalSessions}
icon={
<svg
className="h-5 w-5"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth="1"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M7 20l4-16m2 16l4-16M6 9h14M4 15h14"
/>
</svg>
}
trend={{
value: metrics.sessionTrend,
label:
metrics.sessionTrend > 0
? `${metrics.sessionTrend}% increase`
: `${Math.abs(metrics.sessionTrend || 0)}% decrease`,
positive: metrics.sessionTrend >= 0,
}}
/>
<MetricCard
title="Unique Users"
value={metrics.uniqueUsers}
icon={
<svg
className="h-5 w-5"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth="1"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
/>
</svg>
}
trend={{
value: metrics.usersTrend,
label:
metrics.usersTrend > 0
? `${metrics.usersTrend}% increase`
: `${Math.abs(metrics.usersTrend || 0)}% decrease`,
positive: metrics.usersTrend >= 0,
}}
/>
<MetricCard
title="Avg. Session Time"
value={`${Math.round(metrics.avgSessionTime || 0)}m`}
icon={
<svg
className="h-5 w-5"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth="1"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
}
trend={{
value: metrics.sessionTimeTrend,
label:
metrics.sessionTimeTrend > 0
? `${metrics.sessionTimeTrend}% increase`
: `${Math.abs(metrics.sessionTimeTrend || 0)}% decrease`,
positive: metrics.sessionTimeTrend >= 0,
}}
/>
<MetricCard
title="Avg. Response Time"
value={`${metrics.avgResponseTime?.toFixed(1) || 0}s`}
icon={
<svg
className="h-5 w-5"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth="1"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M13 10V3L4 14h7v7l9-11h-7z"
/>
</svg>
}
trend={{
value: metrics.responseTimeTrend,
label:
metrics.responseTimeTrend > 0
? `${metrics.responseTimeTrend}% increase`
: `${Math.abs(metrics.responseTimeTrend || 0)}% decrease`,
positive: metrics.responseTimeTrend <= 0, // Lower response time is better
}}
/>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="bg-white p-6 rounded-xl shadow lg:col-span-2">
<h3 className="font-bold text-lg text-gray-800 mb-4">
Sessions Over Time
</h3>
<SessionsLineChart sessions={metrics.sessionsByDate || {}} />
</div>
<div className="bg-white p-6 rounded-xl shadow">
<h3 className="font-bold text-lg text-gray-800 mb-4">
Conversation Sentiment
</h3>
<DonutChart
data={{
labels: ["Positive", "Neutral", "Negative"],
values: [
getSentimentData().positive,
getSentimentData().neutral,
getSentimentData().negative,
],
colors: ["#1cad7c", "#a1a1a1", "#dc2626"],
}}
centerText={{
title: "Total",
value: metrics.totalSessions,
}}
/>
</div>
</div>
<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 Category
</h3>
<CategoriesBarChart categories={metrics.categories || {}} />
</div>
<div className="bg-white p-6 rounded-xl shadow">
<h3 className="font-bold text-lg text-gray-800 mb-4">
Languages Used
</h3>
<LanguagePieChart languages={metrics.languages || {}} />
</div>
</div>
<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">
Geographic Distribution
</h3>
<GeographicMap countries={getCountryData()} />
</div>
<div className="bg-white p-6 rounded-xl shadow">
<h3 className="font-bold text-lg text-gray-800 mb-4">
Common Topics
</h3>
<WordCloud words={getWordCloudData()} />
</div>
</div>
<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
data={getResponseTimeData()}
average={metrics.avgResponseTime || 0}
/>
</div>
<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>
</div>
<TokenUsageChart tokenData={getTokenData()} />
</div>
</div>
);
}
// Our exported component
export default function DashboardPage() {
return <DashboardContent />;
}

View File

@ -1,440 +1,104 @@
"use client";
import { useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { signOut, useSession } from "next-auth/react";
import { useRouter } from "next/navigation"; // Import useRouter
import {
SessionsLineChart,
CategoriesBarChart,
LanguagePieChart,
TokenUsageChart,
} from "../../components/Charts";
import DashboardSettings from "./settings";
import UserManagement from "./users";
import { Company, MetricsResult, WordCloudWord } from "../../lib/types"; // Added WordCloudWord
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";
import { FC } from "react";
// Safely wrapped component with useSession
function DashboardContent() {
const { data: session, status } = useSession(); // Add status from useSession
const router = useRouter(); // Initialize useRouter
const [metrics, setMetrics] = useState<MetricsResult | null>(null);
const [company, setCompany] = useState<Company | null>(null);
const [, setLoading] = useState<boolean>(false);
const [refreshing, setRefreshing] = useState<boolean>(false);
const isAdmin = session?.user?.role === "admin";
const isAuditor = session?.user?.role === "auditor";
const DashboardPage: FC = () => {
const { data: session, status } = useSession();
const router = useRouter();
const [loading, setLoading] = useState(true);
useEffect(() => {
// Redirect if not authenticated
// Once session is loaded, redirect appropriately
if (status === "unauthenticated") {
router.push("/login");
return; // Stop further execution in this effect
} else if (status === "authenticated") {
setLoading(false);
}
}, [status, router]);
// Fetch metrics and company on mount if authenticated
if (status === "authenticated") {
const fetchData = async () => {
setLoading(true);
const res = await fetch("/api/dashboard/metrics");
const data = await res.json();
setMetrics(data.metrics);
setCompany(data.company);
setLoading(false);
};
fetchData();
}
}, [status, router]); // Add status and router to dependency array
async function handleRefresh() {
if (isAuditor) return; // Prevent auditors from refreshing
try {
setRefreshing(true);
// Make sure we have a company ID to send
if (!company?.id) {
setRefreshing(false);
alert("Cannot refresh: Company ID is missing");
return;
}
const res = await fetch("/api/admin/refresh-sessions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ companyId: company.id }),
});
if (res.ok) {
// Refetch metrics
const metricsRes = await fetch("/api/dashboard/metrics");
const data = await metricsRes.json();
setMetrics(data.metrics);
} else {
const errorData = await res.json();
alert(`Failed to refresh sessions: ${errorData.error}`);
}
} finally {
setRefreshing(false);
}
}
// Calculate sentiment distribution
const getSentimentData = () => {
if (!metrics) return { positive: 0, neutral: 0, negative: 0 };
if (
metrics.sentimentPositiveCount !== undefined &&
metrics.sentimentNeutralCount !== undefined &&
metrics.sentimentNegativeCount !== undefined
) {
return {
positive: metrics.sentimentPositiveCount,
neutral: metrics.sentimentNeutralCount,
negative: metrics.sentimentNegativeCount,
};
}
const total = metrics.totalSessions || 1;
return {
positive: Math.round(total * 0.6),
neutral: Math.round(total * 0.3),
negative: Math.round(total * 0.1),
};
};
// Prepare token usage data
const getTokenData = () => {
if (!metrics || !metrics.tokensByDay) {
return { labels: [], values: [], costs: [] };
}
const days = Object.keys(metrics.tokensByDay).sort();
const labels = days.slice(-7);
const values = labels.map((day) => metrics.tokensByDay?.[day] || 0);
const costs = labels.map((day) => metrics.tokensCostByDay?.[day] || 0);
return { labels, values, costs };
};
// Show loading state while session status is being determined
if (status === "loading") {
return <div className="text-center py-10">Loading session...</div>;
}
// If unauthenticated and not redirected yet (should be handled by useEffect, but as a fallback)
if (status === "unauthenticated") {
return <div className="text-center py-10">Redirecting to login...</div>;
}
if (!metrics || !company) {
return <div className="text-center py-10">Loading dashboard...</div>;
}
// Function to prepare word cloud data from metrics.wordCloudData
const getWordCloudData = (): WordCloudWord[] => {
if (!metrics || !metrics.wordCloudData) return [];
return metrics.wordCloudData;
};
// Function to prepare country data for the map using actual metrics
const getCountryData = () => {
if (!metrics || !metrics.countries) return {};
// Convert the countries object from metrics to the format expected by GeographicMap
const result = Object.entries(metrics.countries).reduce(
(acc, [code, count]) => {
if (code && count) {
acc[code] = count;
}
return acc;
},
{} as Record<string, number>
if (loading) {
return (
<div className="flex items-center justify-center min-h-[40vh]">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-sky-500 mx-auto mb-4"></div>
<p className="text-lg text-gray-600">Loading dashboard...</p>
</div>
</div>
);
return result;
};
// Function to prepare response time distribution data
const getResponseTimeData = () => {
const avgTime = metrics.avgResponseTime || 1.5;
const simulatedData: number[] = [];
for (let i = 0; i < 50; i++) {
const randomFactor = 0.5 + Math.random();
simulatedData.push(avgTime * randomFactor);
}
return simulatedData;
};
}
return (
<div className="space-y-8">
<WelcomeBanner companyName={company.name} />
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center bg-white p-6 rounded-2xl shadow-lg ring-1 ring-slate-200/50">
<div>
<h1 className="text-3xl font-bold text-slate-800">{company.name}</h1>
<p className="text-slate-500 mt-1">
Dashboard updated{" "}
<span className="font-medium text-slate-600">
{new Date(metrics.lastUpdated || Date.now()).toLocaleString()}
</span>
</p>
</div>
<div className="flex items-center gap-3 mt-4 sm:mt-0">
<button
className="bg-sky-600 text-white py-2 px-5 rounded-lg shadow hover:bg-sky-700 transition-colors disabled:opacity-60 disabled:cursor-not-allowed flex items-center text-sm font-medium"
onClick={handleRefresh}
disabled={refreshing || isAuditor}
>
{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-2"
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-slate-100 text-slate-700 py-2 px-5 rounded-lg shadow hover:bg-slate-200 transition-colors flex items-center text-sm font-medium"
onClick={() => signOut()}
>
<svg
className="w-4 h-4 mr-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
<div className="space-y-6">
<div className="bg-white rounded-xl shadow p-6">
<h1 className="text-2xl font-bold mb-4">Dashboard</h1>
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-6">
<div className="bg-gradient-to-br from-sky-50 to-sky-100 p-6 rounded-xl shadow-sm hover:shadow-md transition-shadow">
<h2 className="text-lg font-semibold text-sky-700">Analytics</h2>
<p className="text-gray-600 mt-2 mb-4">
View your chat session metrics and analytics
</p>
<button
onClick={() => router.push("/dashboard/overview")}
className="bg-sky-500 hover:bg-sky-600 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors"
>
<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>
<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"
/>
<MetricCard
title="Avg Sessions/Day"
value={metrics.avgSessionsPerDay?.toFixed(1) || 0}
icon="📊"
trend={{ value: 5.2, label: "vs last week" }}
variant="success"
/>
<MetricCard
title="Avg Session Time"
value={
metrics.avgSessionLength
? `${metrics.avgSessionLength.toFixed(1)} min`
: "-"
}
icon="⏱️"
trend={{ value: -2.1, label: "vs last week", isPositive: false }}
/>
<MetricCard
title="Avg Response Time"
value={
metrics.avgResponseTime
? `${metrics.avgResponseTime.toFixed(2)}s`
: "-"
}
icon="⚡"
trend={{ value: -1.8, label: "vs last week", isPositive: true }}
variant="success"
/>
</div>
<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)",
"rgba(249, 115, 22, 0.8)",
"rgba(239, 68, 68, 0.8)",
],
}}
centerText={{
title: "Overall",
value: `${((getSentimentData().positive / (getSentimentData().positive + getSentimentData().neutral + getSentimentData().negative)) * 100).toFixed(0)}%`,
}}
/>
</div>
<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"
}
/>
<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"
/>
View Analytics
</button>
</div>
</div>
</div>
<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-6 rounded-xl shadow">
<h3 className="font-bold text-lg text-gray-800 mb-4">
Top Categories
</h3>
<CategoriesBarChart categories={metrics.categories || {}} />
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="bg-white p-6 rounded-xl shadow overflow-hidden">
<h3 className="font-bold text-lg text-gray-800 mb-4">
Transcript Word Cloud
</h3>
<WordCloud words={getWordCloudData()} width={400} 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>
<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>
<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>
</div>
<TokenUsageChart tokenData={getTokenData()} />
</div>
{isAdmin && (
<>
<DashboardSettings company={company} session={session} />
<UserManagement session={session} />
</>
)}
</div>
);
}
// Our exported component
export default function DashboardPage() {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-sky-100 p-4 md:p-6">
<div className="max-w-7xl mx-auto">
<DashboardContent />
<div className="bg-gradient-to-br from-emerald-50 to-emerald-100 p-6 rounded-xl shadow-sm hover:shadow-md transition-shadow">
<h2 className="text-lg font-semibold text-emerald-700">Sessions</h2>
<p className="text-gray-600 mt-2 mb-4">
Browse and analyze conversation sessions
</p>
<button
onClick={() => router.push("/dashboard/sessions")}
className="bg-emerald-500 hover:bg-emerald-600 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors"
>
View Sessions
</button>
</div>
{session?.user?.role === "admin" && (
<div className="bg-gradient-to-br from-purple-50 to-purple-100 p-6 rounded-xl shadow-sm hover:shadow-md transition-shadow">
<h2 className="text-lg font-semibold text-purple-700">
Company Settings
</h2>
<p className="text-gray-600 mt-2 mb-4">
Configure company settings and integrations
</p>
<button
onClick={() => router.push("/dashboard/company")}
className="bg-purple-500 hover:bg-purple-600 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors"
>
Manage Settings
</button>
</div>
)}
{session?.user?.role === "admin" && (
<div className="bg-gradient-to-br from-amber-50 to-amber-100 p-6 rounded-xl shadow-sm hover:shadow-md transition-shadow">
<h2 className="text-lg font-semibold text-amber-700">
User Management
</h2>
<p className="text-gray-600 mt-2 mb-4">
Invite and manage user accounts
</p>
<button
onClick={() => router.push("/dashboard/users")}
className="bg-amber-500 hover:bg-amber-600 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors"
>
Manage Users
</button>
</div>
)}
</div>
</div>
</div>
);
}
};
export default DashboardPage;

View File

@ -0,0 +1,211 @@
"use client";
import { useState, useEffect } from "react";
import { useSession } from "next-auth/react";
import { UserSession } from "../../../lib/types";
interface UserItem {
id: string;
email: string;
role: string;
}
export default function UserManagementPage() {
const { data: session, status } = useSession();
const [users, setUsers] = useState<UserItem[]>([]);
const [email, setEmail] = useState<string>("");
const [role, setRole] = useState<string>("user");
const [message, setMessage] = useState<string>("");
const [loading, setLoading] = useState(true);
useEffect(() => {
if (status === "authenticated") {
fetchUsers();
}
}, [status]);
const fetchUsers = async () => {
setLoading(true);
try {
const res = await fetch("/api/dashboard/users");
const data = await res.json();
setUsers(data.users);
} catch (error) {
console.error("Failed to fetch users:", error);
setMessage("Failed to load users.");
} finally {
setLoading(false);
}
};
async function inviteUser() {
setMessage("");
try {
const res = await fetch("/api/dashboard/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, role }),
});
if (res.ok) {
setMessage("User invited successfully!");
setEmail(""); // Clear the form
// Refresh the user list
fetchUsers();
} else {
const error = await res.json();
setMessage(
`Failed to invite user: ${error.message || "Unknown error"}`
);
}
} catch (error) {
setMessage("Failed to invite user. Please try again.");
console.error("Error inviting user:", error);
}
}
// Loading state
if (loading) {
return <div className="text-center py-10">Loading users...</div>;
}
// Check for admin access
if (session?.user?.role !== "admin") {
return (
<div className="text-center py-10 bg-white rounded-xl shadow p-6">
<h2 className="font-bold text-xl text-red-600 mb-2">Access Denied</h2>
<p>You don't have permission to view user management.</p>
</div>
);
}
return (
<div className="space-y-6">
<div className="bg-white p-6 rounded-xl shadow">
<h1 className="text-2xl font-bold text-gray-800 mb-6">
User Management
</h1>
{message && (
<div
className={`p-4 rounded mb-6 ${message.includes("Failed") ? "bg-red-100 text-red-700" : "bg-green-100 text-green-700"}`}
>
{message}
</div>
)}
<div className="mb-8">
<h2 className="text-lg font-semibold mb-4">Invite New User</h2>
<form
className="grid grid-cols-1 sm:grid-cols-3 gap-4 items-end"
onSubmit={(e) => {
e.preventDefault();
inviteUser();
}}
>
<div className="grid gap-2">
<label className="font-medium text-gray-700">Email</label>
<input
type="email"
className="border border-gray-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-sky-500"
placeholder="user@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<div className="grid gap-2">
<label className="font-medium text-gray-700">Role</label>
<select
className="border border-gray-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-sky-500 bg-white"
value={role}
onChange={(e) => setRole(e.target.value)}
>
<option value="user">User</option>
<option value="admin">Admin</option>
<option value="auditor">Auditor</option>
</select>
</div>
<button
type="submit"
className="bg-sky-600 hover:bg-sky-700 text-white py-2 px-4 rounded-lg shadow transition-colors"
>
Invite User
</button>
</form>
</div>
<div>
<h2 className="text-lg font-semibold mb-4">Current Users</h2>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Email
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Role
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Actions
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{users.length === 0 ? (
<tr>
<td
colSpan={3}
className="px-6 py-4 text-center text-sm text-gray-500"
>
No users found
</td>
</tr>
) : (
users.map((user) => (
<tr key={user.id}>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
{user.email}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<span
className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${
user.role === "admin"
? "bg-purple-100 text-purple-800"
: user.role === "auditor"
? "bg-blue-100 text-blue-800"
: "bg-green-100 text-green-800"
}`}
>
{user.role}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{/* For future: Add actions like edit, delete, etc. */}
<span className="text-gray-400">
No actions available
</span>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
</div>
</div>
);
}