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