fix: resolve Prettier markdown code block parsing errors

- Fix syntax errors in skills markdown files (.github/skills, .opencode/skills)
- Change typescript to tsx for code blocks with JSX
- Replace ellipsis (...) in array examples with valid syntax
- Separate CSS from TypeScript into distinct code blocks
- Convert JavaScript object examples to valid JSON in docs
- Fix enum definitions with proper comma separation
This commit is contained in:
2026-01-20 21:09:29 +01:00
parent 7932fe7386
commit cd05fc8648
177 changed files with 5042 additions and 5541 deletions

View File

@@ -1,20 +1,20 @@
"use client";
import { Database, Save, Settings, ShieldX } from "lucide-react";
import { useSession } from "next-auth/react";
import { useEffect, useId, useState } from "react";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import type { Company } from "../../../lib/types";
import { authClient } from "@/lib/auth/client";
import type { Company } from "@/lib/types";
export default function CompanySettingsPage() {
const csvUrlId = useId();
const csvUsernameId = useId();
const csvPasswordId = useId();
const { data: session, status } = useSession();
const { data, isPending } = authClient.useSession();
// We store the full company object for future use and updates after save operations
// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars
const [_company, setCompany] = useState<Company | null>(null);
@@ -25,7 +25,7 @@ export default function CompanySettingsPage() {
const [loading, setLoading] = useState(true);
useEffect(() => {
if (status === "authenticated") {
if (!isPending && data?.session) {
const fetchCompany = async () => {
setLoading(true);
try {
@@ -46,7 +46,7 @@ export default function CompanySettingsPage() {
};
fetchCompany();
}
}, [status]);
}, [isPending, data]);
async function handleSave() {
setMessage("");
@@ -99,8 +99,8 @@ export default function CompanySettingsPage() {
);
}
// Check for ADMIN access
if (session?.user?.role !== "ADMIN") {
// Check for admin access
if (data?.user?.role !== "admin") {
return (
<div className="space-y-6">
<Card>

View File

@@ -1,13 +1,13 @@
"use client";
import { useRouter } from "next/navigation";
import { useSession } from "next-auth/react";
import { type ReactNode, useCallback, useEffect, useId, useState } from "react";
import Sidebar from "../../components/Sidebar";
import Sidebar from "@/components/Sidebar";
import { authClient } from "@/lib/auth/client";
export default function DashboardLayout({ children }: { children: ReactNode }) {
const mainContentId = useId();
const { status } = useSession();
const { data, isPending } = authClient.useSession();
const router = useRouter();
const [isSidebarExpanded, setIsSidebarExpanded] = useState(true);
@@ -40,19 +40,21 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
}
}, [isMobile]);
if (status === "unauthenticated") {
router.push("/login");
// Redirect handled by middleware, but show loading state
if (isPending) {
return (
<div className="flex h-screen items-center justify-center">
<div className="text-center">Redirecting to login...</div>
<div className="text-center">Loading session...</div>
</div>
);
}
if (status === "loading") {
// If no session after loading, redirect to sign-in
if (!data?.session) {
router.push("/auth/sign-in");
return (
<div className="flex h-screen items-center justify-center">
<div className="text-center">Loading session...</div>
<div className="text-center">Redirecting to login...</div>
</div>
);
}
@@ -76,7 +78,6 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
}
sm:pr-6 md:py-6 md:pr-10`}
>
{/* <div className="w-full mx-auto">{children}</div> */}
<div className="max-w-7xl mx-auto">{children}</div>
</main>
</div>

View File

@@ -16,8 +16,13 @@ import {
} from "lucide-react";
import dynamic from "next/dynamic";
import { useRouter } from "next/navigation";
import { signOut, useSession } from "next-auth/react";
import { useCallback, useEffect, useId, useMemo, useState } from "react";
import ModernBarChart from "@/components/charts/bar-chart";
import ModernDonutChart from "@/components/charts/donut-chart";
import ModernLineChart from "@/components/charts/line-chart";
import GeographicMap from "@/components/GeographicMap";
import ResponseTimeDistribution from "@/components/ResponseTimeDistribution";
import TopQuestionsChart from "@/components/TopQuestionsChart";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
@@ -27,19 +32,14 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import MetricCard from "@/components/ui/metric-card";
import { Skeleton } from "@/components/ui/skeleton";
import { authClient } from "@/lib/auth/client";
import { formatEnumValue } from "@/lib/format-enums";
import ModernBarChart from "../../../components/charts/bar-chart";
import ModernDonutChart from "../../../components/charts/donut-chart";
import ModernLineChart from "../../../components/charts/line-chart";
import GeographicMap from "../../../components/GeographicMap";
import ResponseTimeDistribution from "../../../components/ResponseTimeDistribution";
import TopQuestionsChart from "../../../components/TopQuestionsChart";
import MetricCard from "../../../components/ui/metric-card";
import type { Company, MetricsResult } from "../../../lib/types";
import type { Company, MetricsResult } from "@/lib/types";
// Dynamic import for heavy D3-based WordCloud component (~50KB)
const WordCloud = dynamic(() => import("../../../components/WordCloud"), {
const WordCloud = dynamic(() => import("@/components/WordCloud"), {
ssr: false,
loading: () => (
<div className="h-[300px] flex items-center justify-center">
@@ -50,7 +50,7 @@ const WordCloud = dynamic(() => import("../../../components/WordCloud"), {
// Safely wrapped component with useSession
function DashboardContent() {
const { data: session, status } = useSession();
const { data, isPending } = authClient.useSession();
const router = useRouter();
const [metrics, setMetrics] = useState<MetricsResult | null>(null);
const [company, setCompany] = useState<Company | null>(null);
@@ -59,7 +59,7 @@ function DashboardContent() {
const [isInitialLoad, setIsInitialLoad] = useState<boolean>(true);
const refreshStatusId = useId();
const isAuditor = session?.user?.role === "AUDITOR";
// Role-based restrictions removed - all authenticated users have same access
// Function to fetch metrics with optional date range
const fetchMetrics = useCallback(
@@ -92,19 +92,18 @@ function DashboardContent() {
useEffect(() => {
// Redirect if not authenticated
if (status === "unauthenticated") {
router.push("/login");
if (!isPending && !data?.session) {
router.push("/auth/sign-in");
return;
}
// Fetch metrics and company on mount if authenticated
if (status === "authenticated" && isInitialLoad) {
if (!isPending && data?.session && isInitialLoad) {
fetchMetrics(undefined, undefined, true);
}
}, [status, router, isInitialLoad, fetchMetrics]);
}, [isPending, data, router, isInitialLoad, fetchMetrics]);
const handleRefresh = useCallback(async () => {
if (isAuditor) return;
if (!company?.id) {
alert("Cannot refresh: Company ID is missing");
return;
@@ -132,7 +131,7 @@ function DashboardContent() {
} finally {
setRefreshing(false);
}
}, [isAuditor, company?.id]);
}, [company?.id]);
// Memoized data preparation - must be before any early returns (Rules of Hooks)
const sentimentData = useMemo(() => {
@@ -222,7 +221,7 @@ function DashboardContent() {
}, [metrics?.avgResponseTime]);
// Show loading state while session status is being determined
if (status === "loading") {
if (isPending) {
return (
<div className="flex items-center justify-center min-h-[60vh]">
<div className="text-center space-y-4">
@@ -233,7 +232,7 @@ function DashboardContent() {
);
}
if (status === "unauthenticated") {
if (!isPending && !data?.session) {
return (
<div className="flex items-center justify-center min-h-[60vh]">
<div className="text-center">
@@ -335,7 +334,7 @@ function DashboardContent() {
<div className="flex items-center gap-2">
<Button
onClick={handleRefresh}
disabled={refreshing || isAuditor}
disabled={refreshing}
size="sm"
className="gap-2"
aria-label={
@@ -369,7 +368,15 @@ function DashboardContent() {
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => signOut({ callbackUrl: "/login" })}
onClick={async () => {
await authClient.signOut({
fetchOptions: {
onSuccess: () => {
window.location.href = "/auth/sign-in";
},
},
});
}}
>
<LogOut className="h-4 w-4 mr-2" aria-hidden="true" />
Sign out

View File

@@ -11,25 +11,25 @@ import {
Zap,
} from "lucide-react";
import { useRouter } from "next/navigation";
import { useSession } from "next-auth/react";
import { type FC, useCallback, useEffect, useMemo, useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { authClient } from "@/lib/auth/client";
const DashboardPage: FC = () => {
const { data: session, status } = useSession();
const { data, isPending } = authClient.useSession();
const router = useRouter();
const [loading, setLoading] = useState(true);
useEffect(() => {
// Once session is loaded, redirect appropriately
if (status === "unauthenticated") {
router.push("/login");
} else if (status === "authenticated") {
if (!isPending && !data?.session) {
router.push("/auth/sign-in");
} else if (!isPending && data?.session) {
setLoading(false);
}
}, [status, router]);
}, [isPending, data, router]);
// Memoize navigation cards - only recalculates when user role changes
const navigationCards = useMemo(() => {
@@ -56,7 +56,7 @@ const DashboardPage: FC = () => {
},
];
if (session?.user?.role === "ADMIN") {
if (data?.user?.role === "admin") {
return [
...baseCards,
{
@@ -86,7 +86,7 @@ const DashboardPage: FC = () => {
];
}
return baseCards;
}, [session?.user?.role]);
}, [data?.user?.role]);
// Memoize class getter functions
const getCardClasses = useCallback((variant: string) => {
@@ -150,13 +150,13 @@ const DashboardPage: FC = () => {
<div className="space-y-3">
<div className="flex items-center gap-3">
<h1 className="text-4xl font-bold tracking-tight bg-clip-text text-transparent bg-linear-to-r from-foreground to-foreground/70">
Welcome back, {session?.user?.name || "User"}!
Welcome back, {data?.user?.name || "User"}!
</h1>
<Badge
variant="secondary"
className="text-xs px-3 py-1 bg-primary/10 text-primary border-primary/20"
>
{session?.user?.role}
{data?.user?.role}
</Badge>
</div>
<p className="text-muted-foreground text-lg">

View File

@@ -0,0 +1,326 @@
"use client";
import {
Activity,
AlertCircle,
ArrowLeft,
Clock,
ExternalLink,
FileText,
Globe,
MessageSquare,
User,
} from "lucide-react";
import Link from "next/link";
import { useParams, useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import MessageViewer from "@/components/MessageViewer";
import SessionDetails from "@/components/SessionDetails";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { authClient } from "@/lib/auth/client";
import { formatCategory } from "@/lib/format-enums";
import type { ChatSession } from "@/lib/types";
export default function SessionViewClient() {
const params = useParams();
const router = useRouter(); // Initialize useRouter
const { data: authData, isPending } = authClient.useSession(); // Get session status
const id = params?.id as string;
const [session, setSession] = useState<ChatSession | null>(null);
const [loading, setLoading] = useState(true); // This will now primarily be for data fetching
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!isPending && !authData?.session) {
router.push("/auth/sign-in");
return;
}
if (!isPending && authData?.session && id) {
const fetchSession = async () => {
setLoading(true); // Always set loading before fetch
setError(null);
try {
const response = await fetch(`/api/dashboard/session/${id}`);
if (!response.ok) {
const errorData = await response.json();
throw new Error(
errorData.error ||
`Failed to fetch session: ${response.statusText}`
);
}
const data = await response.json();
setSession(data.session);
} catch (err) {
setError(
err instanceof Error ? err.message : "An unknown error occurred"
);
setSession(null);
} finally {
setLoading(false);
}
};
fetchSession();
} else if (!isPending && authData?.session && !id) {
setError("Session ID is missing.");
setLoading(false);
}
}, [id, isPending, authData, router]);
if (isPending) {
return (
<div className="space-y-6">
<Card>
<CardContent className="pt-6">
<div className="text-center py-8 text-muted-foreground">
Loading session...
</div>
</CardContent>
</Card>
</div>
);
}
if (!isPending && !authData?.session) {
return (
<div className="space-y-6">
<Card>
<CardContent className="pt-6">
<div className="text-center py-8 text-muted-foreground">
Redirecting to login...
</div>
</CardContent>
</Card>
</div>
);
}
if (loading && !isPending && authData?.session) {
return (
<div className="space-y-6">
<Card>
<CardContent className="pt-6">
<div className="text-center py-8 text-muted-foreground">
Loading session details...
</div>
</CardContent>
</Card>
</div>
);
}
if (error) {
return (
<div className="space-y-6">
<Card>
<CardContent className="pt-6">
<div className="text-center py-8">
<AlertCircle className="h-12 w-12 text-destructive mx-auto mb-4" />
<p className="text-destructive text-lg mb-4">Error: {error}</p>
<Link href="/dashboard/sessions">
<Button variant="outline" className="gap-2">
<ArrowLeft className="h-4 w-4" />
Back to Sessions List
</Button>
</Link>
</div>
</CardContent>
</Card>
</div>
);
}
if (!session) {
return (
<div className="space-y-6">
<Card>
<CardContent className="pt-6">
<div className="text-center py-8">
<MessageSquare className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
<p className="text-muted-foreground text-lg mb-4">
Session not found.
</p>
<Link href="/dashboard/sessions">
<Button variant="outline" className="gap-2">
<ArrowLeft className="h-4 w-4" />
Back to Sessions List
</Button>
</Link>
</div>
</CardContent>
</Card>
</div>
);
}
return (
<div className="space-y-6 max-w-6xl mx-auto">
{/* Header */}
<Card>
<CardContent className="pt-6">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<div className="space-y-2">
<Link href="/dashboard/sessions">
<Button
variant="ghost"
className="gap-2 p-0 h-auto focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
aria-label="Return to sessions list"
>
<ArrowLeft className="h-4 w-4" aria-hidden="true" />
Back to Sessions List
</Button>
</Link>
<div className="space-y-2">
<h1 className="text-3xl font-bold">Session Details</h1>
<div className="flex items-center gap-3">
<Badge variant="outline" className="font-mono text-xs">
ID
</Badge>
<code className="text-sm text-muted-foreground font-mono">
{(session.sessionId || session.id).slice(0, 8)}...
</code>
</div>
</div>
</div>
<div className="flex flex-wrap gap-2">
{session.category && (
<Badge variant="secondary" className="gap-1">
<Activity className="h-3 w-3" />
{formatCategory(session.category)}
</Badge>
)}
{session.language && (
<Badge variant="outline" className="gap-1">
<Globe className="h-3 w-3" />
{session.language.toUpperCase()}
</Badge>
)}
{session.sentiment && (
<Badge
variant={
session.sentiment === "positive"
? "default"
: session.sentiment === "negative"
? "destructive"
: "secondary"
}
className="gap-1"
>
{session.sentiment.charAt(0).toUpperCase() +
session.sentiment.slice(1)}
</Badge>
)}
</div>
</div>
</CardContent>
</Card>
{/* Session Overview */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<Card>
<CardContent className="pt-6">
<div className="flex items-center gap-3">
<Clock className="h-8 w-8 text-blue-500" />
<div>
<p className="text-sm text-muted-foreground">Start Time</p>
<p className="font-semibold">
{new Date(session.startTime).toLocaleString()}
</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-center gap-3">
<MessageSquare className="h-8 w-8 text-green-500" />
<div>
<p className="text-sm text-muted-foreground">Messages</p>
<p className="font-semibold">{session.messages?.length || 0}</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-center gap-3">
<User className="h-8 w-8 text-purple-500" />
<div>
<p className="text-sm text-muted-foreground">User ID</p>
<p className="font-semibold truncate">
{session.userId || "N/A"}
</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-center gap-3">
<Activity className="h-8 w-8 text-orange-500" />
<div>
<p className="text-sm text-muted-foreground">Duration</p>
<p className="font-semibold">
{session.endTime && session.startTime
? `${Math.round(
(new Date(session.endTime).getTime() -
new Date(session.startTime).getTime()) /
60000
)} min`
: "N/A"}
</p>
</div>
</div>
</CardContent>
</Card>
</div>
{/* Session Details */}
<SessionDetails session={session} />
{/* Messages */}
{session.messages && session.messages.length > 0 && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<MessageSquare className="h-5 w-5" />
Conversation ({session.messages.length} messages)
</CardTitle>
</CardHeader>
<CardContent>
<MessageViewer messages={session.messages} />
</CardContent>
</Card>
)}
{/* Transcript URL */}
{session.fullTranscriptUrl && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<FileText className="h-5 w-5" />
Source Transcript
</CardTitle>
</CardHeader>
<CardContent>
<a
href={session.fullTranscriptUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 text-primary hover:underline focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 rounded-sm"
aria-label="Open original transcript in new tab"
>
<ExternalLink className="h-4 w-4" aria-hidden="true" />
View Original Transcript
</a>
</CardContent>
</Card>
)}
</div>
);
}

View File

@@ -1,326 +1,36 @@
"use client";
import { Suspense } from "react";
import { Card, CardContent } from "@/components/ui/card";
import SessionViewClient from "./SessionViewClient";
import {
Activity,
AlertCircle,
ArrowLeft,
Clock,
ExternalLink,
FileText,
Globe,
MessageSquare,
User,
} from "lucide-react";
import Link from "next/link";
import { useParams, useRouter } from "next/navigation";
import { useSession } from "next-auth/react";
import { useEffect, useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { formatCategory } from "@/lib/format-enums";
import MessageViewer from "../../../../components/MessageViewer";
import SessionDetails from "../../../../components/SessionDetails";
import type { ChatSession } from "../../../../lib/types";
export const metadata = {
title: "Session Details | LiveDash",
description: "View detailed session information and conversation history",
};
export default function SessionViewPage() {
const params = useParams();
const router = useRouter(); // Initialize useRouter
const { status } = useSession(); // Get session status, removed unused sessionData
const id = params?.id as string;
const [session, setSession] = useState<ChatSession | null>(null);
const [loading, setLoading] = useState(true); // This will now primarily be for data fetching
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (status === "unauthenticated") {
router.push("/login");
return;
}
if (status === "authenticated" && id) {
const fetchSession = async () => {
setLoading(true); // Always set loading before fetch
setError(null);
try {
const response = await fetch(`/api/dashboard/session/${id}`);
if (!response.ok) {
const errorData = await response.json();
throw new Error(
errorData.error ||
`Failed to fetch session: ${response.statusText}`
);
}
const data = await response.json();
setSession(data.session);
} catch (err) {
setError(
err instanceof Error ? err.message : "An unknown error occurred"
);
setSession(null);
} finally {
setLoading(false);
}
};
fetchSession();
} else if (status === "authenticated" && !id) {
setError("Session ID is missing.");
setLoading(false);
}
}, [id, status, router]); // session removed from dependencies
if (status === "loading") {
return (
<div className="space-y-6">
<Card>
<CardContent className="pt-6">
<div className="text-center py-8 text-muted-foreground">
Loading session...
</div>
</CardContent>
</Card>
</div>
);
}
if (status === "unauthenticated") {
return (
<div className="space-y-6">
<Card>
<CardContent className="pt-6">
<div className="text-center py-8 text-muted-foreground">
Redirecting to login...
</div>
</CardContent>
</Card>
</div>
);
}
if (loading && status === "authenticated") {
return (
<div className="space-y-6">
<Card>
<CardContent className="pt-6">
<div className="text-center py-8 text-muted-foreground">
Loading session details...
</div>
</CardContent>
</Card>
</div>
);
}
if (error) {
return (
<div className="space-y-6">
<Card>
<CardContent className="pt-6">
<div className="text-center py-8">
<AlertCircle className="h-12 w-12 text-destructive mx-auto mb-4" />
<p className="text-destructive text-lg mb-4">Error: {error}</p>
<Link href="/dashboard/sessions">
<Button variant="outline" className="gap-2">
<ArrowLeft className="h-4 w-4" />
Back to Sessions List
</Button>
</Link>
</div>
</CardContent>
</Card>
</div>
);
}
if (!session) {
return (
<div className="space-y-6">
<Card>
<CardContent className="pt-6">
<div className="text-center py-8">
<MessageSquare className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
<p className="text-muted-foreground text-lg mb-4">
Session not found.
</p>
<Link href="/dashboard/sessions">
<Button variant="outline" className="gap-2">
<ArrowLeft className="h-4 w-4" />
Back to Sessions List
</Button>
</Link>
</div>
</CardContent>
</Card>
</div>
);
}
// Provide at least one sample param for build-time validation
// Runtime params not in this list will be handled dynamically
export async function generateStaticParams() {
return [{ id: "sample" }];
}
function SessionLoadingFallback() {
return (
<div className="space-y-6 max-w-6xl mx-auto">
{/* Header */}
<div className="space-y-6">
<Card>
<CardContent className="pt-6">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<div className="space-y-2">
<Link href="/dashboard/sessions">
<Button
variant="ghost"
className="gap-2 p-0 h-auto focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
aria-label="Return to sessions list"
>
<ArrowLeft className="h-4 w-4" aria-hidden="true" />
Back to Sessions List
</Button>
</Link>
<div className="space-y-2">
<h1 className="text-3xl font-bold">Session Details</h1>
<div className="flex items-center gap-3">
<Badge variant="outline" className="font-mono text-xs">
ID
</Badge>
<code className="text-sm text-muted-foreground font-mono">
{(session.sessionId || session.id).slice(0, 8)}...
</code>
</div>
</div>
</div>
<div className="flex flex-wrap gap-2">
{session.category && (
<Badge variant="secondary" className="gap-1">
<Activity className="h-3 w-3" />
{formatCategory(session.category)}
</Badge>
)}
{session.language && (
<Badge variant="outline" className="gap-1">
<Globe className="h-3 w-3" />
{session.language.toUpperCase()}
</Badge>
)}
{session.sentiment && (
<Badge
variant={
session.sentiment === "positive"
? "default"
: session.sentiment === "negative"
? "destructive"
: "secondary"
}
className="gap-1"
>
{session.sentiment.charAt(0).toUpperCase() +
session.sentiment.slice(1)}
</Badge>
)}
</div>
<div className="text-center py-8 text-muted-foreground">
Loading session...
</div>
</CardContent>
</Card>
{/* Session Overview */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<Card>
<CardContent className="pt-6">
<div className="flex items-center gap-3">
<Clock className="h-8 w-8 text-blue-500" />
<div>
<p className="text-sm text-muted-foreground">Start Time</p>
<p className="font-semibold">
{new Date(session.startTime).toLocaleString()}
</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-center gap-3">
<MessageSquare className="h-8 w-8 text-green-500" />
<div>
<p className="text-sm text-muted-foreground">Messages</p>
<p className="font-semibold">{session.messages?.length || 0}</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-center gap-3">
<User className="h-8 w-8 text-purple-500" />
<div>
<p className="text-sm text-muted-foreground">User ID</p>
<p className="font-semibold truncate">
{session.userId || "N/A"}
</p>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<div className="flex items-center gap-3">
<Activity className="h-8 w-8 text-orange-500" />
<div>
<p className="text-sm text-muted-foreground">Duration</p>
<p className="font-semibold">
{session.endTime && session.startTime
? `${Math.round(
(new Date(session.endTime).getTime() -
new Date(session.startTime).getTime()) /
60000
)} min`
: "N/A"}
</p>
</div>
</div>
</CardContent>
</Card>
</div>
{/* Session Details */}
<SessionDetails session={session} />
{/* Messages */}
{session.messages && session.messages.length > 0 && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<MessageSquare className="h-5 w-5" />
Conversation ({session.messages.length} messages)
</CardTitle>
</CardHeader>
<CardContent>
<MessageViewer messages={session.messages} />
</CardContent>
</Card>
)}
{/* Transcript URL */}
{session.fullTranscriptUrl && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<FileText className="h-5 w-5" />
Source Transcript
</CardTitle>
</CardHeader>
<CardContent>
<a
href={session.fullTranscriptUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 text-primary hover:underline focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 rounded-sm"
aria-label="Open original transcript in new tab"
>
<ExternalLink className="h-4 w-4" aria-hidden="true" />
View Original Transcript
</a>
</CardContent>
</Card>
)}
</div>
);
}
export default function SessionViewPage() {
return (
<Suspense fallback={<SessionLoadingFallback />}>
<SessionViewClient />
</Suspense>
);
}

View File

@@ -20,7 +20,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { formatCategory } from "@/lib/format-enums";
import type { ChatSession } from "../../../lib/types";
import type { ChatSession } from "@/lib/types";
// State types
interface FilterState {

View File

@@ -1,11 +1,10 @@
"use client";
import type { Session } from "next-auth";
import { useState } from "react";
import type { Company } from "../../lib/types";
import type { Company, UserSession } from "@/lib/types";
interface DashboardSettingsProps {
company: Company;
session: Session;
session: UserSession;
}
export default function DashboardSettings({

View File

@@ -1,6 +1,6 @@
"use client";
import { useEffect, useState } from "react";
import type { UserSession } from "../../lib/types";
import type { UserSession } from "@/lib/types";
interface UserItem {
id: string;

View File

@@ -1,7 +1,6 @@
"use client";
import { AlertCircle, Eye, Shield, UserPlus, Users } from "lucide-react";
import { useSession } from "next-auth/react";
import { AlertCircle, Shield, UserPlus, Users } from "lucide-react";
import { useCallback, useEffect, useId, useState } from "react";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Badge } from "@/components/ui/badge";
@@ -24,6 +23,7 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
import { authClient } from "@/lib/auth/client";
interface UserItem {
id: string;
@@ -32,10 +32,10 @@ interface UserItem {
}
export default function UserManagementPage() {
const { data: session, status } = useSession();
const { data, isPending } = authClient.useSession();
const [users, setUsers] = useState<UserItem[]>([]);
const [email, setEmail] = useState<string>("");
const [role, setRole] = useState<string>("USER");
const [role, setRole] = useState<string>("user");
const [message, setMessage] = useState<string>("");
const [loading, setLoading] = useState(true);
const emailId = useId();
@@ -55,16 +55,16 @@ export default function UserManagementPage() {
}, []);
useEffect(() => {
if (status === "authenticated") {
if (session?.user?.role === "ADMIN") {
if (!isPending && data?.session) {
if (data?.user?.role === "admin") {
fetchUsers();
} else {
setLoading(false); // Stop loading for non-admin users
}
} else if (status === "unauthenticated") {
} else if (!isPending && !data?.session) {
setLoading(false);
}
}, [status, session?.user?.role, fetchUsers]);
}, [isPending, data, fetchUsers]);
async function inviteUser() {
setMessage("");
@@ -108,7 +108,7 @@ export default function UserManagementPage() {
}
// Check for admin access
if (session?.user?.role !== "ADMIN") {
if (data?.user?.role !== "admin") {
return (
<div className="space-y-6">
<Card>
@@ -185,9 +185,8 @@ export default function UserManagementPage() {
<SelectValue placeholder="Select role" />
</SelectTrigger>
<SelectContent>
<SelectItem value="USER">User</SelectItem>
<SelectItem value="ADMIN">Admin</SelectItem>
<SelectItem value="AUDITOR">Auditor</SelectItem>
<SelectItem value="user">User</SelectItem>
<SelectItem value="admin">Admin</SelectItem>
</SelectContent>
</Select>
</div>
@@ -237,21 +236,14 @@ export default function UserManagementPage() {
<TableCell>
<Badge
variant={
user.role === "ADMIN"
? "default"
: user.role === "AUDITOR"
? "secondary"
: "outline"
user.role === "admin" ? "default" : "outline"
}
className="gap-1"
data-testid="role-badge"
>
{user.role === "ADMIN" && (
{user.role === "admin" && (
<Shield className="h-3 w-3" />
)}
{user.role === "AUDITOR" && (
<Eye className="h-3 w-3" />
)}
{user.role}
</Badge>
</TableCell>