mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 09:32:08 +01:00
Remove Tailwind CSS configuration file
This commit is contained in:
136
app/api/admin/refresh-sessions/route.ts
Normal file
136
app/api/admin/refresh-sessions/route.ts
Normal file
@ -0,0 +1,136 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { fetchAndParseCsv } from "../../../../lib/csvFetcher";
|
||||
import { processQueuedImports } from "../../../../lib/importProcessor";
|
||||
import { prisma } from "../../../../lib/prisma";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
let { companyId } = body;
|
||||
|
||||
if (!companyId) {
|
||||
// Try to get user from prisma based on session cookie
|
||||
try {
|
||||
const session = await prisma.session.findFirst({
|
||||
orderBy: { createdAt: "desc" },
|
||||
where: {
|
||||
/* Add session check criteria here */
|
||||
},
|
||||
});
|
||||
|
||||
if (session) {
|
||||
companyId = session.companyId;
|
||||
}
|
||||
} catch (error) {
|
||||
// Log error for server-side debugging
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
// Use a server-side logging approach instead of console
|
||||
process.stderr.write(`Error fetching session: ${errorMessage}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!companyId) {
|
||||
return NextResponse.json(
|
||||
{ error: "Company ID is required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const company = await prisma.company.findUnique({ where: { id: companyId } });
|
||||
if (!company) {
|
||||
return NextResponse.json(
|
||||
{ error: "Company not found" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
const rawSessionData = await fetchAndParseCsv(
|
||||
company.csvUrl,
|
||||
company.csvUsername as string | undefined,
|
||||
company.csvPassword as string | undefined
|
||||
);
|
||||
|
||||
let importedCount = 0;
|
||||
|
||||
// Create SessionImport records for new data
|
||||
for (const rawSession of rawSessionData) {
|
||||
try {
|
||||
// Use upsert to handle duplicates gracefully
|
||||
await prisma.sessionImport.upsert({
|
||||
where: {
|
||||
companyId_externalSessionId: {
|
||||
companyId: company.id,
|
||||
externalSessionId: rawSession.externalSessionId,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
// Update existing record with latest data
|
||||
startTimeRaw: rawSession.startTimeRaw,
|
||||
endTimeRaw: rawSession.endTimeRaw,
|
||||
ipAddress: rawSession.ipAddress,
|
||||
countryCode: rawSession.countryCode,
|
||||
language: rawSession.language,
|
||||
messagesSent: rawSession.messagesSent,
|
||||
sentimentRaw: rawSession.sentimentRaw,
|
||||
escalatedRaw: rawSession.escalatedRaw,
|
||||
forwardedHrRaw: rawSession.forwardedHrRaw,
|
||||
fullTranscriptUrl: rawSession.fullTranscriptUrl,
|
||||
avgResponseTimeSeconds: rawSession.avgResponseTimeSeconds,
|
||||
tokens: rawSession.tokens,
|
||||
tokensEur: rawSession.tokensEur,
|
||||
category: rawSession.category,
|
||||
initialMessage: rawSession.initialMessage,
|
||||
// Status tracking now handled by ProcessingStatusManager
|
||||
},
|
||||
create: {
|
||||
companyId: company.id,
|
||||
externalSessionId: rawSession.externalSessionId,
|
||||
startTimeRaw: rawSession.startTimeRaw,
|
||||
endTimeRaw: rawSession.endTimeRaw,
|
||||
ipAddress: rawSession.ipAddress,
|
||||
countryCode: rawSession.countryCode,
|
||||
language: rawSession.language,
|
||||
messagesSent: rawSession.messagesSent,
|
||||
sentimentRaw: rawSession.sentimentRaw,
|
||||
escalatedRaw: rawSession.escalatedRaw,
|
||||
forwardedHrRaw: rawSession.forwardedHrRaw,
|
||||
fullTranscriptUrl: rawSession.fullTranscriptUrl,
|
||||
avgResponseTimeSeconds: rawSession.avgResponseTimeSeconds,
|
||||
tokens: rawSession.tokens,
|
||||
tokensEur: rawSession.tokensEur,
|
||||
category: rawSession.category,
|
||||
initialMessage: rawSession.initialMessage,
|
||||
// Status tracking now handled by ProcessingStatusManager
|
||||
},
|
||||
});
|
||||
importedCount++;
|
||||
} catch (error) {
|
||||
// Log individual session import errors but continue processing
|
||||
process.stderr.write(
|
||||
`Failed to import session ${rawSession.externalSessionId}: ${error}\n`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Immediately process the queued imports to create Session records
|
||||
console.log('[Refresh API] Processing queued imports...');
|
||||
await processQueuedImports(100); // Process up to 100 imports immediately
|
||||
|
||||
// Count how many sessions were created
|
||||
const sessionCount = await prisma.session.count({
|
||||
where: { companyId: company.id }
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
imported: importedCount,
|
||||
total: rawSessionData.length,
|
||||
sessions: sessionCount,
|
||||
message: `Successfully imported ${importedCount} records and processed them into sessions. Total sessions: ${sessionCount}`
|
||||
});
|
||||
} catch (e) {
|
||||
const error = e instanceof Error ? e.message : "An unknown error occurred";
|
||||
return NextResponse.json({ error }, { status: 500 });
|
||||
}
|
||||
}
|
||||
105
app/api/admin/trigger-processing/route.ts
Normal file
105
app/api/admin/trigger-processing/route.ts
Normal file
@ -0,0 +1,105 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "../../auth/[...nextauth]/route";
|
||||
import { prisma } from "../../../../lib/prisma";
|
||||
import { processUnprocessedSessions } from "../../../../lib/processingScheduler";
|
||||
import { ProcessingStatusManager } from "../../../../lib/processingStatusManager";
|
||||
import { ProcessingStage } from "@prisma/client";
|
||||
|
||||
interface SessionUser {
|
||||
email: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
interface SessionData {
|
||||
user: SessionUser;
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const session = (await getServerSession(authOptions)) as SessionData | null;
|
||||
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: "Not logged in" }, { status: 401 });
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email: session.user.email },
|
||||
include: { company: true },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: "No user found" }, { status: 401 });
|
||||
}
|
||||
|
||||
// Check if user has ADMIN role
|
||||
if (user.role !== "ADMIN") {
|
||||
return NextResponse.json(
|
||||
{ error: "Admin access required" },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// Get optional parameters from request body
|
||||
const body = await request.json();
|
||||
const { batchSize, maxConcurrency } = body;
|
||||
|
||||
// Validate parameters
|
||||
const validatedBatchSize = batchSize && batchSize > 0 ? parseInt(batchSize) : null;
|
||||
const validatedMaxConcurrency = maxConcurrency && maxConcurrency > 0 ? parseInt(maxConcurrency) : 5;
|
||||
|
||||
// Check how many sessions need AI processing using the new status system
|
||||
const sessionsNeedingAI = await ProcessingStatusManager.getSessionsNeedingProcessing(
|
||||
ProcessingStage.AI_ANALYSIS,
|
||||
1000 // Get count only
|
||||
);
|
||||
|
||||
// Filter to sessions for this company
|
||||
const companySessionsNeedingAI = sessionsNeedingAI.filter(
|
||||
statusRecord => statusRecord.session.companyId === user.companyId
|
||||
);
|
||||
|
||||
const unprocessedCount = companySessionsNeedingAI.length;
|
||||
|
||||
if (unprocessedCount === 0) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: "No sessions requiring AI processing found",
|
||||
unprocessedCount: 0,
|
||||
processedCount: 0,
|
||||
});
|
||||
}
|
||||
|
||||
// Start processing (this will run asynchronously)
|
||||
const startTime = Date.now();
|
||||
|
||||
// Note: We're calling the function but not awaiting it to avoid timeout
|
||||
// The processing will continue in the background
|
||||
processUnprocessedSessions(validatedBatchSize, validatedMaxConcurrency)
|
||||
.then(() => {
|
||||
console.log(`[Manual Trigger] Processing completed for company ${user.companyId}`);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(`[Manual Trigger] Processing failed for company ${user.companyId}:`, error);
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `Started processing ${unprocessedCount} unprocessed sessions`,
|
||||
unprocessedCount,
|
||||
batchSize: validatedBatchSize || unprocessedCount,
|
||||
maxConcurrency: validatedMaxConcurrency,
|
||||
startedAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("[Manual Trigger] Error:", error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "Failed to trigger processing",
|
||||
details: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
106
app/api/auth/[...nextauth]/route.ts
Normal file
106
app/api/auth/[...nextauth]/route.ts
Normal file
@ -0,0 +1,106 @@
|
||||
import NextAuth, { NextAuthOptions } from "next-auth";
|
||||
import CredentialsProvider from "next-auth/providers/credentials";
|
||||
import { prisma } from "../../../../lib/prisma";
|
||||
import bcrypt from "bcryptjs";
|
||||
|
||||
// Define the shape of the JWT token
|
||||
declare module "next-auth/jwt" {
|
||||
interface JWT {
|
||||
companyId: string;
|
||||
role: string;
|
||||
}
|
||||
}
|
||||
|
||||
// Define the shape of the session object
|
||||
declare module "next-auth" {
|
||||
interface Session {
|
||||
user: {
|
||||
id?: string;
|
||||
name?: string;
|
||||
email?: string;
|
||||
image?: string;
|
||||
companyId: string;
|
||||
role: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
companyId: string;
|
||||
role: string;
|
||||
}
|
||||
}
|
||||
|
||||
export const authOptions: NextAuthOptions = {
|
||||
providers: [
|
||||
CredentialsProvider({
|
||||
name: "Credentials",
|
||||
credentials: {
|
||||
email: { label: "Email", type: "text" },
|
||||
password: { label: "Password", type: "password" },
|
||||
},
|
||||
async authorize(credentials) {
|
||||
if (!credentials?.email || !credentials?.password) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email: credentials.email },
|
||||
});
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
const valid = await bcrypt.compare(credentials.password, user.password);
|
||||
if (!valid) return null;
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
companyId: user.companyId,
|
||||
role: user.role,
|
||||
};
|
||||
},
|
||||
}),
|
||||
],
|
||||
session: {
|
||||
strategy: "jwt",
|
||||
maxAge: 30 * 24 * 60 * 60, // 30 days
|
||||
},
|
||||
cookies: {
|
||||
sessionToken: {
|
||||
name: `next-auth.session-token`,
|
||||
options: {
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
},
|
||||
},
|
||||
},
|
||||
callbacks: {
|
||||
async jwt({ token, user }) {
|
||||
if (user) {
|
||||
token.companyId = user.companyId;
|
||||
token.role = user.role;
|
||||
}
|
||||
return token;
|
||||
},
|
||||
async session({ session, token }) {
|
||||
if (token && session.user) {
|
||||
session.user.companyId = token.companyId;
|
||||
session.user.role = token.role;
|
||||
}
|
||||
return session;
|
||||
},
|
||||
},
|
||||
pages: {
|
||||
signIn: "/login",
|
||||
},
|
||||
secret: process.env.NEXTAUTH_SECRET,
|
||||
debug: process.env.NODE_ENV === "development",
|
||||
};
|
||||
|
||||
const handler = NextAuth(authOptions);
|
||||
|
||||
export { handler as GET, handler as POST };
|
||||
51
app/api/dashboard/config/route.ts
Normal file
51
app/api/dashboard/config/route.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { prisma } from "../../../../lib/prisma";
|
||||
import { authOptions } from "../../auth/[...nextauth]/route";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: "Not logged in" }, { status: 401 });
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email: session.user.email as string },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: "No user" }, { status: 401 });
|
||||
}
|
||||
|
||||
// Get company data
|
||||
const company = await prisma.company.findUnique({
|
||||
where: { id: user.companyId },
|
||||
});
|
||||
|
||||
return NextResponse.json({ company });
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: "Not logged in" }, { status: 401 });
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email: session.user.email as string },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: "No user" }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { csvUrl } = body;
|
||||
|
||||
await prisma.company.update({
|
||||
where: { id: user.companyId },
|
||||
data: { csvUrl },
|
||||
});
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
110
app/api/dashboard/metrics/route.ts
Normal file
110
app/api/dashboard/metrics/route.ts
Normal file
@ -0,0 +1,110 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { prisma } from "../../../../lib/prisma";
|
||||
import { sessionMetrics } from "../../../../lib/metrics";
|
||||
import { authOptions } from "../../auth/[...nextauth]/route";
|
||||
import { ChatSession } from "../../../../lib/types";
|
||||
|
||||
interface SessionUser {
|
||||
email: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
interface SessionData {
|
||||
user: SessionUser;
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const session = (await getServerSession(authOptions)) as SessionData | null;
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: "Not logged in" }, { status: 401 });
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email: session.user.email },
|
||||
include: { company: true },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: "No user" }, { status: 401 });
|
||||
}
|
||||
|
||||
// Get date range from query parameters
|
||||
const { searchParams } = new URL(request.url);
|
||||
const startDate = searchParams.get("startDate");
|
||||
const endDate = searchParams.get("endDate");
|
||||
|
||||
// Build where clause with optional date filtering
|
||||
const whereClause: any = {
|
||||
companyId: user.companyId,
|
||||
};
|
||||
|
||||
if (startDate && endDate) {
|
||||
whereClause.startTime = {
|
||||
gte: new Date(startDate),
|
||||
lte: new Date(endDate + 'T23:59:59.999Z'), // Include full end date
|
||||
};
|
||||
}
|
||||
|
||||
const prismaSessions = await prisma.session.findMany({
|
||||
where: whereClause,
|
||||
include: {
|
||||
messages: true, // Include messages for question extraction
|
||||
},
|
||||
});
|
||||
|
||||
// Convert Prisma sessions to ChatSession[] type for sessionMetrics
|
||||
const chatSessions: ChatSession[] = prismaSessions.map((ps) => ({
|
||||
id: ps.id, // Map Prisma's id to ChatSession.id
|
||||
sessionId: ps.id, // Map Prisma's id to ChatSession.sessionId
|
||||
companyId: ps.companyId,
|
||||
startTime: new Date(ps.startTime), // Ensure startTime is a Date object
|
||||
endTime: ps.endTime ? new Date(ps.endTime) : null, // Ensure endTime is a Date object or null
|
||||
transcriptContent: "", // Session model doesn't have transcriptContent field
|
||||
createdAt: new Date(ps.createdAt), // Map Prisma's createdAt
|
||||
updatedAt: new Date(ps.createdAt), // Use createdAt for updatedAt as Session model doesn't have updatedAt
|
||||
category: ps.category || undefined,
|
||||
language: ps.language || undefined,
|
||||
country: ps.country || undefined,
|
||||
ipAddress: ps.ipAddress || undefined,
|
||||
sentiment: ps.sentiment === null ? undefined : ps.sentiment,
|
||||
messagesSent: ps.messagesSent === null ? undefined : ps.messagesSent, // Handle null messagesSent
|
||||
avgResponseTime:
|
||||
ps.avgResponseTime === null ? undefined : ps.avgResponseTime,
|
||||
escalated: ps.escalated || false,
|
||||
forwardedHr: ps.forwardedHr || false,
|
||||
initialMsg: ps.initialMsg || undefined,
|
||||
fullTranscriptUrl: ps.fullTranscriptUrl || undefined,
|
||||
summary: ps.summary || undefined, // Include summary field
|
||||
messages: ps.messages || [], // Include messages for question extraction
|
||||
// userId is missing in Prisma Session model, assuming it's not strictly needed for metrics or can be null
|
||||
userId: undefined, // Or some other default/mapping if available
|
||||
}));
|
||||
|
||||
// Pass company config to metrics
|
||||
const companyConfigForMetrics = {
|
||||
sentimentAlert:
|
||||
user.company.sentimentAlert === null
|
||||
? undefined
|
||||
: user.company.sentimentAlert,
|
||||
};
|
||||
|
||||
const metrics = sessionMetrics(chatSessions, companyConfigForMetrics);
|
||||
|
||||
// Calculate date range from sessions
|
||||
let dateRange: { minDate: string; maxDate: string } | null = null;
|
||||
if (prismaSessions.length > 0) {
|
||||
const dates = prismaSessions.map(s => new Date(s.startTime)).sort((a, b) => a.getTime() - b.getTime());
|
||||
dateRange = {
|
||||
minDate: dates[0].toISOString().split('T')[0], // First session date
|
||||
maxDate: dates[dates.length - 1].toISOString().split('T')[0] // Last session date
|
||||
};
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
metrics,
|
||||
csvUrl: user.company.csvUrl,
|
||||
company: user.company,
|
||||
dateRange,
|
||||
});
|
||||
}
|
||||
71
app/api/dashboard/session-filter-options/route.ts
Normal file
71
app/api/dashboard/session-filter-options/route.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth/next";
|
||||
import { authOptions } from "../../auth/[...nextauth]/route";
|
||||
import { prisma } from "../../../../lib/prisma";
|
||||
import { SessionFilterOptions } from "../../../../lib/types";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const authSession = await getServerSession(authOptions);
|
||||
|
||||
if (!authSession || !authSession.user?.companyId) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const companyId = authSession.user.companyId;
|
||||
|
||||
try {
|
||||
const categories = await prisma.session.findMany({
|
||||
where: {
|
||||
companyId,
|
||||
category: {
|
||||
not: null, // Ensure category is not null
|
||||
},
|
||||
},
|
||||
distinct: ["category"],
|
||||
select: {
|
||||
category: true,
|
||||
},
|
||||
orderBy: {
|
||||
category: "asc",
|
||||
},
|
||||
});
|
||||
|
||||
const languages = await prisma.session.findMany({
|
||||
where: {
|
||||
companyId,
|
||||
language: {
|
||||
not: null, // Ensure language is not null
|
||||
},
|
||||
},
|
||||
distinct: ["language"],
|
||||
select: {
|
||||
language: true,
|
||||
},
|
||||
orderBy: {
|
||||
language: "asc",
|
||||
},
|
||||
});
|
||||
|
||||
const distinctCategories = categories
|
||||
.map((s) => s.category)
|
||||
.filter(Boolean) as string[]; // Filter out any nulls and assert as string[]
|
||||
const distinctLanguages = languages
|
||||
.map((s) => s.language)
|
||||
.filter(Boolean) as string[]; // Filter out any nulls and assert as string[]
|
||||
|
||||
return NextResponse.json({
|
||||
categories: distinctCategories,
|
||||
languages: distinctLanguages
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "An unknown error occurred";
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "Failed to fetch filter options",
|
||||
details: errorMessage,
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
85
app/api/dashboard/session/[id]/route.ts
Normal file
85
app/api/dashboard/session/[id]/route.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "../../../../../lib/prisma";
|
||||
import { ChatSession } from "../../../../../lib/types";
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
const { id } = params;
|
||||
|
||||
if (!id) {
|
||||
return NextResponse.json(
|
||||
{ error: "Session ID is required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const prismaSession = await prisma.session.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
messages: {
|
||||
orderBy: { order: "asc" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!prismaSession) {
|
||||
return NextResponse.json(
|
||||
{ error: "Session not found" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Map Prisma session object to ChatSession type
|
||||
const session: ChatSession = {
|
||||
// Spread prismaSession to include all its properties
|
||||
...prismaSession,
|
||||
// Override properties that need conversion or specific mapping
|
||||
id: prismaSession.id, // ChatSession.id from Prisma.Session.id
|
||||
sessionId: prismaSession.id, // ChatSession.sessionId from Prisma.Session.id
|
||||
startTime: new Date(prismaSession.startTime),
|
||||
endTime: prismaSession.endTime ? new Date(prismaSession.endTime) : null,
|
||||
createdAt: new Date(prismaSession.createdAt),
|
||||
// Prisma.Session does not have an `updatedAt` field. We'll use `createdAt` as a fallback.
|
||||
// Or, if your business logic implies an update timestamp elsewhere, use that.
|
||||
updatedAt: new Date(prismaSession.createdAt), // Fallback to createdAt
|
||||
// Prisma.Session does not have a `userId` field.
|
||||
userId: null, // Explicitly set to null or map if available from another source
|
||||
// Ensure nullable fields from Prisma are correctly mapped to ChatSession's optional or nullable fields
|
||||
category: prismaSession.category ?? null,
|
||||
language: prismaSession.language ?? null,
|
||||
country: prismaSession.country ?? null,
|
||||
ipAddress: prismaSession.ipAddress ?? null,
|
||||
sentiment: prismaSession.sentiment ?? null,
|
||||
messagesSent: prismaSession.messagesSent ?? undefined, // Use undefined if ChatSession expects number | undefined
|
||||
avgResponseTime: prismaSession.avgResponseTime ?? null,
|
||||
escalated: prismaSession.escalated ?? undefined,
|
||||
forwardedHr: prismaSession.forwardedHr ?? undefined,
|
||||
initialMsg: prismaSession.initialMsg ?? undefined,
|
||||
fullTranscriptUrl: prismaSession.fullTranscriptUrl ?? null,
|
||||
summary: prismaSession.summary ?? null, // New field
|
||||
transcriptContent: null, // Not available in Session model
|
||||
messages:
|
||||
prismaSession.messages?.map((msg) => ({
|
||||
id: msg.id,
|
||||
sessionId: msg.sessionId,
|
||||
timestamp: msg.timestamp ? new Date(msg.timestamp) : new Date(),
|
||||
role: msg.role,
|
||||
content: msg.content,
|
||||
order: msg.order,
|
||||
createdAt: new Date(msg.createdAt),
|
||||
})) ?? [], // New field - parsed messages
|
||||
};
|
||||
|
||||
return NextResponse.json({ session });
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "An unknown error occurred";
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to fetch session", details: errorMessage },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
149
app/api/dashboard/sessions/route.ts
Normal file
149
app/api/dashboard/sessions/route.ts
Normal file
@ -0,0 +1,149 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth/next";
|
||||
import { authOptions } from "../../auth/[...nextauth]/route";
|
||||
import { prisma } from "../../../../lib/prisma";
|
||||
import {
|
||||
ChatSession,
|
||||
SessionApiResponse,
|
||||
SessionQuery,
|
||||
} from "../../../../lib/types";
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const authSession = await getServerSession(authOptions);
|
||||
|
||||
if (!authSession || !authSession.user?.companyId) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const companyId = authSession.user.companyId;
|
||||
const { searchParams } = new URL(request.url);
|
||||
|
||||
const searchTerm = searchParams.get("searchTerm");
|
||||
const category = searchParams.get("category");
|
||||
const language = searchParams.get("language");
|
||||
const startDate = searchParams.get("startDate");
|
||||
const endDate = searchParams.get("endDate");
|
||||
const sortKey = searchParams.get("sortKey");
|
||||
const sortOrder = searchParams.get("sortOrder");
|
||||
const queryPage = searchParams.get("page");
|
||||
const queryPageSize = searchParams.get("pageSize");
|
||||
|
||||
const page = Number(queryPage) || 1;
|
||||
const pageSize = Number(queryPageSize) || 10;
|
||||
|
||||
try {
|
||||
const whereClause: Prisma.SessionWhereInput = { companyId };
|
||||
|
||||
// Search Term
|
||||
if (searchTerm && searchTerm.trim() !== "") {
|
||||
const searchConditions = [
|
||||
{ id: { contains: searchTerm } },
|
||||
{ initialMsg: { contains: searchTerm } },
|
||||
{ summary: { contains: searchTerm } },
|
||||
];
|
||||
whereClause.OR = searchConditions;
|
||||
}
|
||||
|
||||
// Category Filter
|
||||
if (category && category.trim() !== "") {
|
||||
// Cast to SessionCategory enum if it's a valid value
|
||||
whereClause.category = category as any;
|
||||
}
|
||||
|
||||
// Language Filter
|
||||
if (language && language.trim() !== "") {
|
||||
whereClause.language = language;
|
||||
}
|
||||
|
||||
// Date Range Filter
|
||||
if (startDate) {
|
||||
whereClause.startTime = {
|
||||
...((whereClause.startTime as object) || {}),
|
||||
gte: new Date(startDate),
|
||||
};
|
||||
}
|
||||
if (endDate) {
|
||||
const inclusiveEndDate = new Date(endDate);
|
||||
inclusiveEndDate.setDate(inclusiveEndDate.getDate() + 1);
|
||||
whereClause.startTime = {
|
||||
...((whereClause.startTime as object) || {}),
|
||||
lt: inclusiveEndDate,
|
||||
};
|
||||
}
|
||||
|
||||
// Sorting
|
||||
const validSortKeys: { [key: string]: string } = {
|
||||
startTime: "startTime",
|
||||
category: "category",
|
||||
language: "language",
|
||||
sentiment: "sentiment",
|
||||
messagesSent: "messagesSent",
|
||||
avgResponseTime: "avgResponseTime",
|
||||
};
|
||||
|
||||
let orderByCondition:
|
||||
| Prisma.SessionOrderByWithRelationInput
|
||||
| Prisma.SessionOrderByWithRelationInput[];
|
||||
|
||||
const primarySortField =
|
||||
sortKey && validSortKeys[sortKey]
|
||||
? validSortKeys[sortKey]
|
||||
: "startTime"; // Default to startTime field if sortKey is invalid/missing
|
||||
|
||||
const primarySortOrder =
|
||||
sortOrder === "asc" || sortOrder === "desc" ? sortOrder : "desc"; // Default to desc order
|
||||
|
||||
if (primarySortField === "startTime") {
|
||||
// If sorting by startTime, it's the only sort criteria
|
||||
orderByCondition = { [primarySortField]: primarySortOrder };
|
||||
} else {
|
||||
// If sorting by another field, use startTime: "desc" as secondary sort
|
||||
orderByCondition = [
|
||||
{ [primarySortField]: primarySortOrder },
|
||||
{ startTime: "desc" },
|
||||
];
|
||||
}
|
||||
|
||||
const prismaSessions = await prisma.session.findMany({
|
||||
where: whereClause,
|
||||
orderBy: orderByCondition,
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
});
|
||||
|
||||
const totalSessions = await prisma.session.count({ where: whereClause });
|
||||
|
||||
const sessions: ChatSession[] = prismaSessions.map((ps) => ({
|
||||
id: ps.id,
|
||||
sessionId: ps.id,
|
||||
companyId: ps.companyId,
|
||||
startTime: new Date(ps.startTime),
|
||||
endTime: ps.endTime ? new Date(ps.endTime) : null,
|
||||
createdAt: new Date(ps.createdAt),
|
||||
updatedAt: new Date(ps.createdAt),
|
||||
userId: null,
|
||||
category: ps.category ?? null,
|
||||
language: ps.language ?? null,
|
||||
country: ps.country ?? null,
|
||||
ipAddress: ps.ipAddress ?? null,
|
||||
sentiment: ps.sentiment ?? null,
|
||||
messagesSent: ps.messagesSent ?? undefined,
|
||||
avgResponseTime: ps.avgResponseTime ?? null,
|
||||
escalated: ps.escalated ?? undefined,
|
||||
forwardedHr: ps.forwardedHr ?? undefined,
|
||||
initialMsg: ps.initialMsg ?? undefined,
|
||||
fullTranscriptUrl: ps.fullTranscriptUrl ?? null,
|
||||
transcriptContent: null, // Transcript content is now fetched from fullTranscriptUrl when needed
|
||||
}));
|
||||
|
||||
return NextResponse.json({ sessions, totalSessions });
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "An unknown error occurred";
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to fetch sessions", details: errorMessage },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
36
app/api/dashboard/settings/route.ts
Normal file
36
app/api/dashboard/settings/route.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { prisma } from "../../../../lib/prisma";
|
||||
import { authOptions } from "../../auth/[...nextauth]/route";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session?.user || session.user.role !== "ADMIN") {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email: session.user.email as string },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: "No user" }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { csvUrl, csvUsername, csvPassword, sentimentThreshold } = body;
|
||||
|
||||
await prisma.company.update({
|
||||
where: { id: user.companyId },
|
||||
data: {
|
||||
csvUrl,
|
||||
csvUsername,
|
||||
...(csvPassword ? { csvPassword } : {}),
|
||||
sentimentAlert: sentimentThreshold
|
||||
? parseFloat(sentimentThreshold)
|
||||
: null,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
80
app/api/dashboard/users/route.ts
Normal file
80
app/api/dashboard/users/route.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import crypto from "crypto";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { prisma } from "../../../../lib/prisma";
|
||||
import bcrypt from "bcryptjs";
|
||||
import { authOptions } from "../../auth/[...nextauth]/route";
|
||||
|
||||
interface UserBasicInfo {
|
||||
id: string;
|
||||
email: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session?.user || session.user.role !== "ADMIN") {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email: session.user.email as string },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: "No user" }, { status: 401 });
|
||||
}
|
||||
|
||||
const users = await prisma.user.findMany({
|
||||
where: { companyId: user.companyId },
|
||||
});
|
||||
|
||||
const mappedUsers: UserBasicInfo[] = users.map((u) => ({
|
||||
id: u.id,
|
||||
email: u.email,
|
||||
role: u.role,
|
||||
}));
|
||||
|
||||
return NextResponse.json({ users: mappedUsers });
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session?.user || session.user.role !== "ADMIN") {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email: session.user.email as string },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: "No user" }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { email, role } = body;
|
||||
|
||||
if (!email || !role) {
|
||||
return NextResponse.json({ error: "Missing fields" }, { status: 400 });
|
||||
}
|
||||
|
||||
const exists = await prisma.user.findUnique({ where: { email } });
|
||||
if (exists) {
|
||||
return NextResponse.json({ error: "Email exists" }, { status: 409 });
|
||||
}
|
||||
|
||||
const tempPassword = crypto.randomBytes(12).toString("base64").slice(0, 12); // secure random initial password
|
||||
|
||||
await prisma.user.create({
|
||||
data: {
|
||||
email,
|
||||
password: await bcrypt.hash(tempPassword, 10),
|
||||
companyId: user.companyId,
|
||||
role,
|
||||
},
|
||||
});
|
||||
|
||||
// TODO: Email user their temp password (stub, for demo) - Implement a robust and secure email sending mechanism. Consider using a transactional email service.
|
||||
return NextResponse.json({ ok: true, tempPassword });
|
||||
}
|
||||
28
app/api/forgot-password/route.ts
Normal file
28
app/api/forgot-password/route.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "../../../lib/prisma";
|
||||
import { sendEmail } from "../../../lib/sendEmail";
|
||||
import crypto from "crypto";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const body = await request.json();
|
||||
const { email } = body as { email: string };
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { email } });
|
||||
if (!user) {
|
||||
// Always return 200 for privacy (don't reveal if email exists)
|
||||
return NextResponse.json({ success: true }, { status: 200 });
|
||||
}
|
||||
|
||||
const token = crypto.randomBytes(32).toString("hex");
|
||||
const expiry = new Date(Date.now() + 1000 * 60 * 30); // 30 min expiry
|
||||
|
||||
await prisma.user.update({
|
||||
where: { email },
|
||||
data: { resetToken: token, resetTokenExpiry: expiry },
|
||||
});
|
||||
|
||||
const resetUrl = `${process.env.NEXTAUTH_URL || "http://localhost:3000"}/reset-password?token=${token}`;
|
||||
await sendEmail(email, "Password Reset", `Reset your password: ${resetUrl}`);
|
||||
|
||||
return NextResponse.json({ success: true }, { status: 200 });
|
||||
}
|
||||
63
app/api/register/route.ts
Normal file
63
app/api/register/route.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "../../../lib/prisma";
|
||||
import bcrypt from "bcryptjs";
|
||||
|
||||
interface RegisterRequestBody {
|
||||
email: string;
|
||||
password: string;
|
||||
company: string;
|
||||
csvUrl?: string;
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const body = await request.json();
|
||||
const { email, password, company, csvUrl } = body as RegisterRequestBody;
|
||||
|
||||
if (!email || !password || !company) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Missing required fields",
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Check if email exists
|
||||
const exists = await prisma.user.findUnique({
|
||||
where: { email },
|
||||
});
|
||||
|
||||
if (exists) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: "Email already exists",
|
||||
},
|
||||
{ status: 409 }
|
||||
);
|
||||
}
|
||||
|
||||
const newCompany = await prisma.company.create({
|
||||
data: { name: company, csvUrl: csvUrl || "" },
|
||||
});
|
||||
|
||||
const hashed = await bcrypt.hash(password, 10);
|
||||
|
||||
await prisma.user.create({
|
||||
data: {
|
||||
email,
|
||||
password: hashed,
|
||||
companyId: newCompany.id,
|
||||
role: "ADMIN",
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: true,
|
||||
data: { success: true },
|
||||
},
|
||||
{ status: 201 }
|
||||
);
|
||||
}
|
||||
63
app/api/reset-password/route.ts
Normal file
63
app/api/reset-password/route.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "../../../lib/prisma";
|
||||
import bcrypt from "bcryptjs";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const body = await request.json();
|
||||
const { token, password } = body as { token?: string; password?: string };
|
||||
|
||||
if (!token || !password) {
|
||||
return NextResponse.json(
|
||||
{ error: "Token and password are required." },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
return NextResponse.json(
|
||||
{ error: "Password must be at least 8 characters long." },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
resetToken: token,
|
||||
resetTokenExpiry: { gte: new Date() },
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "Invalid or expired token. Please request a new password reset.",
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const hash = await bcrypt.hash(password, 10);
|
||||
await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
password: hash,
|
||||
resetToken: null,
|
||||
resetTokenExpiry: null,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{ message: "Password has been reset successfully." },
|
||||
{ status: 200 }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Reset password error:", error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "An internal server error occurred. Please try again later.",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user