mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-02-14 10:35:44 +01:00
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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
326
app/dashboard/sessions/[id]/SessionViewClient.tsx
Normal file
326
app/dashboard/sessions/[id]/SessionViewClient.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user