From e3134aa451edbe69f1049d627d37526b06ea728a Mon Sep 17 00:00:00 2001 From: Kaj Kowalski Date: Thu, 22 May 2025 14:12:36 +0200 Subject: [PATCH] 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 --- .github/CODEOWNERS | 1 + app/dashboard/company/page.tsx | 173 ++++++++ app/dashboard/layout.tsx | 46 +++ app/dashboard/overview/page.tsx | 437 ++++++++++++++++++++ app/dashboard/page.tsx | 506 ++++-------------------- app/dashboard/users/page.tsx | 211 ++++++++++ app/globals.css | 10 - app/layout.tsx | 4 +- components/GeographicMap.tsx | 8 +- components/ResponseTimeDistribution.tsx | 48 ++- components/Sidebar.tsx | 306 ++++++++++++++ pages/api/dashboard/config.ts | 6 + 12 files changed, 1302 insertions(+), 454 deletions(-) create mode 100644 .github/CODEOWNERS create mode 100644 app/dashboard/company/page.tsx create mode 100644 app/dashboard/layout.tsx create mode 100644 app/dashboard/overview/page.tsx create mode 100644 app/dashboard/users/page.tsx create mode 100644 components/Sidebar.tsx diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..21e5c50 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @kjanat diff --git a/app/dashboard/company/page.tsx b/app/dashboard/company/page.tsx new file mode 100644 index 0000000..bb2f7e6 --- /dev/null +++ b/app/dashboard/company/page.tsx @@ -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(null); + const [csvUrl, setCsvUrl] = useState(""); + const [csvUsername, setCsvUsername] = useState(""); + const [csvPassword, setCsvPassword] = useState(""); + const [sentimentThreshold, setSentimentThreshold] = useState(""); + const [message, setMessage] = useState(""); + 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
Loading settings...
; + } + + // Check for admin access + if (session?.user?.role !== "admin") { + return ( +
+

Access Denied

+

You don't have permission to view company settings.

+
+ ); + } + + return ( +
+
+

+ Company Settings +

+ + {message && ( +
+ {message} +
+ )} + +
{ + e.preventDefault(); + handleSave(); + }} + > +
+ + setCsvUrl(e.target.value)} + placeholder="https://example.com/data.csv" + /> +
+ +
+ + setCsvUsername(e.target.value)} + placeholder="Username for CSV access (if needed)" + /> +
+ +
+ + setCsvPassword(e.target.value)} + placeholder="Password will be updated only if provided" + /> +

+ Leave blank to keep current password +

+
+ +
+ + setSentimentThreshold(e.target.value)} + placeholder="Threshold value (0-100)" + min="0" + max="100" + /> +

+ Percentage of negative sentiment sessions to trigger alert (0-100) +

+
+ + +
+
+
+ ); +} diff --git a/app/dashboard/layout.tsx b/app/dashboard/layout.tsx new file mode 100644 index 0000000..8afbc42 --- /dev/null +++ b/app/dashboard/layout.tsx @@ -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 ( +
+
Redirecting to login...
+
+ ); + } + + // Show loading state while session status is being determined + if (status === "loading") { + return ( +
+
Loading session...
+
+ ); + } + + const handleLogout = () => { + signOut({ callbackUrl: "/login" }); + }; + + return ( +
+ {/* Sidebar with logout handler passed as prop */} + + + {/* Main content */} +
+
{children}
+
+
+ ); +} diff --git a/app/dashboard/overview/page.tsx b/app/dashboard/overview/page.tsx new file mode 100644 index 0000000..83fe7bd --- /dev/null +++ b/app/dashboard/overview/page.tsx @@ -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(null); + const [company, setCompany] = useState(null); + const [, setLoading] = useState(false); + const [refreshing, setRefreshing] = useState(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
Loading session...
; + } + + // If unauthenticated and not redirected yet (should be handled by useEffect, but as a fallback) + if (status === "unauthenticated") { + return
Redirecting to login...
; + } + + if (!metrics || !company) { + return
Loading dashboard...
; + } + + // 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 + ); + + 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 ( +
+ +
+
+

{company.name}

+

+ Dashboard updated{" "} + + {new Date(metrics.lastUpdated || Date.now()).toLocaleString()} + +

+
+
+ + +
+
+
+ + + + } + trend={{ + value: metrics.sessionTrend, + label: + metrics.sessionTrend > 0 + ? `${metrics.sessionTrend}% increase` + : `${Math.abs(metrics.sessionTrend || 0)}% decrease`, + positive: metrics.sessionTrend >= 0, + }} + /> + + + + } + trend={{ + value: metrics.usersTrend, + label: + metrics.usersTrend > 0 + ? `${metrics.usersTrend}% increase` + : `${Math.abs(metrics.usersTrend || 0)}% decrease`, + positive: metrics.usersTrend >= 0, + }} + /> + + + + } + trend={{ + value: metrics.sessionTimeTrend, + label: + metrics.sessionTimeTrend > 0 + ? `${metrics.sessionTimeTrend}% increase` + : `${Math.abs(metrics.sessionTimeTrend || 0)}% decrease`, + positive: metrics.sessionTimeTrend >= 0, + }} + /> + + + + } + 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 + }} + /> +
+ +
+
+

+ Sessions Over Time +

+ +
+
+

+ Conversation Sentiment +

+ +
+
+ +
+
+

+ Sessions by Category +

+ +
+
+

+ Languages Used +

+ +
+
+ +
+
+

+ Geographic Distribution +

+ +
+ +
+

+ Common Topics +

+ +
+
+ +
+

+ Response Time Distribution +

+ +
+
+
+

+ Token Usage & Costs +

+
+
+ Total Tokens: + {metrics.totalTokens?.toLocaleString() || 0} +
+
+ Total Cost:€ + {metrics.totalTokensEur?.toFixed(4) || 0} +
+
+
+ +
+
+ ); +} + +// Our exported component +export default function DashboardPage() { + return ; +} diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 1142657..708f9b4 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -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(null); - const [company, setCompany] = useState(null); - const [, setLoading] = useState(false); - const [refreshing, setRefreshing] = useState(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
Loading session...
; - } - - // If unauthenticated and not redirected yet (should be handled by useEffect, but as a fallback) - if (status === "unauthenticated") { - return
Redirecting to login...
; - } - - if (!metrics || !company) { - return
Loading dashboard...
; - } - - // 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 + if (loading) { + return ( +
+
+
+

Loading dashboard...

+
+
); - - 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 ( -
- -
-
-

{company.name}

-

- Dashboard updated{" "} - - {new Date(metrics.lastUpdated || Date.now()).toLocaleString()} - -

-
-
- - -
-
-
- - - - -
-
-
-

- Sentiment Distribution -

- -
- -
-

- Case Handling Statistics -

-
- metrics.totalSessions * 0.1 - ? "warning" - : "success" - } - /> - metrics.totalSessions * 0.05 - ? "warning" - : "default" - } - /> - + View Analytics +
-
-
-
-
-

- Sessions by Day -

- -
-
-

- Top Categories -

- -
-
-
-
-

- Transcript Word Cloud -

- -
-
-

- Geographic Distribution -

- -
-
-
-
-

- Response Time Distribution -

- -
-
-

Languages

- -
-
-
-
-

- Token Usage & Costs -

-
-
- Total Tokens: - {metrics.totalTokens?.toLocaleString() || 0} -
-
- Total Cost:€ - {metrics.totalTokensEur?.toFixed(4) || 0} -
-
-
- -
- {isAdmin && ( - <> - - - - )} -
- ); -} -// Our exported component -export default function DashboardPage() { - return ( -
-
- +
+

Sessions

+

+ Browse and analyze conversation sessions +

+ +
+ + {session?.user?.role === "admin" && ( +
+

+ Company Settings +

+

+ Configure company settings and integrations +

+ +
+ )} + + {session?.user?.role === "admin" && ( +
+

+ User Management +

+

+ Invite and manage user accounts +

+ +
+ )} +
); -} +}; + +export default DashboardPage; diff --git a/app/dashboard/users/page.tsx b/app/dashboard/users/page.tsx new file mode 100644 index 0000000..87a2a0e --- /dev/null +++ b/app/dashboard/users/page.tsx @@ -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([]); + const [email, setEmail] = useState(""); + const [role, setRole] = useState("user"); + const [message, setMessage] = useState(""); + 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
Loading users...
; + } + + // Check for admin access + if (session?.user?.role !== "admin") { + return ( +
+

Access Denied

+

You don't have permission to view user management.

+
+ ); + } + + return ( +
+
+

+ User Management +

+ + {message && ( +
+ {message} +
+ )} + +
+

Invite New User

+
{ + e.preventDefault(); + inviteUser(); + }} + > +
+ + setEmail(e.target.value)} + required + /> +
+ +
+ + +
+ + +
+
+ +
+

Current Users

+
+ + + + + + + + + + {users.length === 0 ? ( + + + + ) : ( + users.map((user) => ( + + + + + + )) + )} + +
+ Email + + Role + + Actions +
+ No users found +
+ {user.email} + + + {user.role} + + + {/* For future: Add actions like edit, delete, etc. */} + + No actions available + +
+
+
+
+
+ ); +} diff --git a/app/globals.css b/app/globals.css index c4871c8..f1d8c73 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,11 +1 @@ -body { - font-family: system-ui, sans-serif; - background: #f3f4f6; -} - -input, -button { - font-family: inherit; -} - @import "tailwindcss"; diff --git a/app/layout.tsx b/app/layout.tsx index 87d6df7..5f1b733 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -21,9 +21,7 @@ export default function RootLayout({ children }: { children: ReactNode }) { return ( - -
{children}
-
+ {children} ); diff --git a/components/GeographicMap.tsx b/components/GeographicMap.tsx index 3cbef2d..ca767ea 100644 --- a/components/GeographicMap.tsx +++ b/components/GeographicMap.tsx @@ -79,11 +79,11 @@ export default function GeographicMap({ // Process country data when client is ready and dependencies change useEffect(() => { - if (!isClient) return; + if (!isClient || !countries) return; try { // Generate CountryData array for the Map component - const data: CountryData[] = Object.entries(countries) + const data: CountryData[] = Object.entries(countries || {}) // Only include countries with known coordinates .filter(([code]) => { // If no coordinates found, log to help with debugging @@ -115,8 +115,8 @@ export default function GeographicMap({ } }, [countries, countryCoordinates, isClient]); - // Find the max count for scaling circles - handle empty countries object - const countryValues = Object.values(countries); + // Find the max count for scaling circles - handle empty or null countries object + const countryValues = countries ? Object.values(countries) : []; const maxCount = countryValues.length > 0 ? Math.max(...countryValues, 1) : 1; // Show loading state during SSR or until client-side rendering takes over diff --git a/components/ResponseTimeDistribution.tsx b/components/ResponseTimeDistribution.tsx index a6c6842..2a42daa 100644 --- a/components/ResponseTimeDistribution.tsx +++ b/components/ResponseTimeDistribution.tsx @@ -7,28 +7,30 @@ import annotationPlugin from "chartjs-plugin-annotation"; Chart.register(annotationPlugin); interface ResponseTimeDistributionProps { - responseTimes: number[]; + data: number[]; + average: number; targetResponseTime?: number; } export default function ResponseTimeDistribution({ - responseTimes, + data, + average, targetResponseTime, }: ResponseTimeDistributionProps) { const ref = useRef(null); useEffect(() => { - if (!ref.current || !responseTimes.length) return; + if (!ref.current || !data || !data.length) return; const ctx = ref.current.getContext("2d"); if (!ctx) return; // Create bins for the histogram (0-1s, 1-2s, 2-3s, etc.) - const maxTime = Math.ceil(Math.max(...responseTimes)); + const maxTime = Math.ceil(Math.max(...data)); const bins = Array(Math.min(maxTime + 1, 10)).fill(0); // Count responses in each bin - responseTimes.forEach((time) => { + data.forEach((time) => { const binIndex = Math.min(Math.floor(time), bins.length - 1); bins[binIndex]++; }); @@ -63,26 +65,40 @@ export default function ResponseTimeDistribution({ responsive: true, plugins: { legend: { display: false }, - annotation: targetResponseTime - ? { - annotations: { - targetLine: { + annotation: { + annotations: { + averageLine: { + type: "line", + yMin: 0, + yMax: Math.max(...bins), + xMin: average, + xMax: average, + borderColor: "rgba(75, 192, 192, 1)", + borderWidth: 2, + label: { + display: true, + content: "Avg: " + average.toFixed(1) + "s", + position: "start", + }, + }, + targetLine: targetResponseTime + ? { type: "line", yMin: 0, yMax: Math.max(...bins), xMin: targetResponseTime, xMax: targetResponseTime, - borderColor: "rgba(75, 192, 192, 1)", + borderColor: "rgba(75, 192, 192, 0.7)", borderWidth: 2, label: { display: true, content: "Target", - position: "start", + position: "end", }, - }, - }, - } - : undefined, + } + : undefined, + }, + }, }, scales: { y: { @@ -103,7 +119,7 @@ export default function ResponseTimeDistribution({ }); return () => chart.destroy(); - }, [responseTimes, targetResponseTime]); + }, [data, average, targetResponseTime]); return ; } diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx new file mode 100644 index 0000000..b50f828 --- /dev/null +++ b/components/Sidebar.tsx @@ -0,0 +1,306 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { signOut } from "next-auth/react"; + +// Icons for the sidebar +const DashboardIcon = () => ( + + + + + + +); + +const CompanyIcon = () => ( + + + +); + +const UsersIcon = () => ( + + + +); + +const SessionsIcon = () => ( + + + +); + +const LogoutIcon = () => ( + + + +); + +const ToggleIcon = ({ isExpanded }: { isExpanded: boolean }) => ( + + + +); + +interface NavItemProps { + href: string; + label: string; + icon: React.ReactNode; + isExpanded: boolean; + isActive: boolean; +} + +const NavItem: React.FC = ({ + href, + label, + icon, + isExpanded, + isActive, +}) => ( + + + {icon} + + {isExpanded ? ( + {label} + ) : ( +
+ {label} +
+ )} + +); + +export default function Sidebar() { + const [isExpanded, setIsExpanded] = useState(true); + const pathname = usePathname() || ""; + + const toggleSidebar = () => { + setIsExpanded(!isExpanded); + }; + + const handleLogout = () => { + signOut({ callbackUrl: "/login" }); + }; + + return ( +
+ {/* Logo section - now above toggle button */} +
+
+ LiveDash Logo +
+ {isExpanded && ( + LiveDash + )} +
+ + {/* Toggle button */} +
+ +
+ + {/* Navigation items */} + + + {/* Logout at the bottom */} +
+ +
+
+ ); +} diff --git a/pages/api/dashboard/config.ts b/pages/api/dashboard/config.ts index 48da9be..55a1345 100644 --- a/pages/api/dashboard/config.ts +++ b/pages/api/dashboard/config.ts @@ -24,6 +24,12 @@ export default async function handler( data: { csvUrl }, }); res.json({ ok: true }); + } else if (req.method === "GET") { + // Get company data + const company = await prisma.company.findUnique({ + where: { id: user.companyId }, + }); + res.json({ company }); } else { res.status(405).end(); }