mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 15:12:09 +01:00
feat: initialize project with Next.js, Prisma, and Tailwind CSS
- Add package.json with dependencies and scripts for Next.js and Prisma - Implement API routes for session management, user authentication, and company configuration - Create database schema for Company, User, and Session models in Prisma - Set up authentication with NextAuth and JWT - Add password reset functionality and user registration endpoint - Configure Tailwind CSS and PostCSS for styling - Implement metrics and dashboard settings API endpoints
This commit is contained in:
104
lib/csvFetcher.ts
Normal file
104
lib/csvFetcher.ts
Normal file
@ -0,0 +1,104 @@
|
||||
// Fetches, parses, and returns chat session data for a company from a CSV URL
|
||||
import fetch from "node-fetch";
|
||||
import { parse } from "csv-parse/sync";
|
||||
|
||||
// This type is used internally for parsing the CSV records
|
||||
interface CSVRecord {
|
||||
session_id: string;
|
||||
start_time: string;
|
||||
end_time?: string;
|
||||
ip_address?: string;
|
||||
country?: string;
|
||||
language?: string;
|
||||
messages_sent?: string;
|
||||
sentiment?: string;
|
||||
escalated?: string;
|
||||
forwarded_hr?: string;
|
||||
full_transcript_url?: string;
|
||||
avg_response_time?: string;
|
||||
tokens?: string;
|
||||
tokens_eur?: string;
|
||||
category?: string;
|
||||
initial_msg?: string;
|
||||
[key: string]: string | undefined;
|
||||
}
|
||||
|
||||
interface SessionData {
|
||||
id: string;
|
||||
sessionId: string;
|
||||
startTime: Date;
|
||||
endTime: Date | null;
|
||||
ipAddress?: string;
|
||||
country?: string;
|
||||
language?: string | null;
|
||||
messagesSent: number;
|
||||
sentiment: number | null;
|
||||
escalated: boolean;
|
||||
forwardedHr: boolean;
|
||||
fullTranscriptUrl?: string | null;
|
||||
avgResponseTime: number | null;
|
||||
tokens: number;
|
||||
tokensEur: number;
|
||||
category?: string | null;
|
||||
initialMsg?: string;
|
||||
}
|
||||
|
||||
export async function fetchAndParseCsv(url: string, username?: string, password?: string): Promise<Partial<SessionData>[]> {
|
||||
const authHeader = username && password
|
||||
? "Basic " + Buffer.from(`${username}:${password}`).toString("base64")
|
||||
: undefined;
|
||||
|
||||
const res = await fetch(url, {
|
||||
headers: authHeader ? { Authorization: authHeader } : {},
|
||||
});
|
||||
if (!res.ok) throw new Error("Failed to fetch CSV: " + res.statusText);
|
||||
|
||||
const text = await res.text();
|
||||
|
||||
// Parse without expecting headers, using known order
|
||||
const records: CSVRecord[] = parse(text, {
|
||||
delimiter: ",",
|
||||
columns: [
|
||||
"session_id",
|
||||
"start_time",
|
||||
"end_time",
|
||||
"ip_address",
|
||||
"country",
|
||||
"language",
|
||||
"messages_sent",
|
||||
"sentiment",
|
||||
"escalated",
|
||||
"forwarded_hr",
|
||||
"full_transcript_url",
|
||||
"avg_response_time",
|
||||
"tokens",
|
||||
"tokens_eur",
|
||||
"category",
|
||||
"initial_msg",
|
||||
],
|
||||
from_line: 1,
|
||||
relax_column_count: true,
|
||||
skip_empty_lines: true,
|
||||
trim: true,
|
||||
});
|
||||
|
||||
// Coerce types for relevant columns
|
||||
return records.map((r) => ({
|
||||
id: r.session_id,
|
||||
startTime: new Date(r.start_time),
|
||||
endTime: r.end_time ? new Date(r.end_time) : null,
|
||||
ipAddress: r.ip_address,
|
||||
country: r.country,
|
||||
language: r.language,
|
||||
messagesSent: Number(r.messages_sent) || 0,
|
||||
sentiment: r.sentiment ? parseFloat(r.sentiment) : null,
|
||||
escalated: r.escalated === "1" || r.escalated === "true",
|
||||
forwardedHr: r.forwarded_hr === "1" || r.forwarded_hr === "true",
|
||||
fullTranscriptUrl: r.full_transcript_url,
|
||||
avgResponseTime: r.avg_response_time ? parseFloat(r.avg_response_time) : null,
|
||||
tokens: Number(r.tokens) || 0,
|
||||
tokensEur: r.tokens_eur ? parseFloat(r.tokens_eur) : 0,
|
||||
category: r.category,
|
||||
initialMsg: r.initial_msg,
|
||||
}));
|
||||
}
|
||||
85
lib/metrics.ts
Normal file
85
lib/metrics.ts
Normal file
@ -0,0 +1,85 @@
|
||||
// Functions to calculate metrics over sessions
|
||||
import { ChatSession, DayMetrics, CategoryMetrics, LanguageMetrics, MetricsResult } from './types';
|
||||
|
||||
interface CompanyConfig {
|
||||
sentimentAlert?: number;
|
||||
}
|
||||
|
||||
export function sessionMetrics(sessions: ChatSession[], companyConfig: CompanyConfig = {}): MetricsResult {
|
||||
const total = sessions.length;
|
||||
const byDay: DayMetrics = {};
|
||||
const byCategory: CategoryMetrics = {};
|
||||
const byLanguage: LanguageMetrics = {};
|
||||
let escalated = 0, forwarded = 0;
|
||||
let totalSentiment = 0, sentimentCount = 0;
|
||||
let totalResponse = 0, responseCount = 0;
|
||||
let totalTokens = 0, totalTokensEur = 0;
|
||||
|
||||
// Calculate total session duration in minutes
|
||||
let totalDuration = 0;
|
||||
let durationCount = 0;
|
||||
|
||||
sessions.forEach(s => {
|
||||
const day = s.startTime.toISOString().slice(0, 10);
|
||||
byDay[day] = (byDay[day] || 0) + 1;
|
||||
|
||||
if (s.category) byCategory[s.category] = (byCategory[s.category] || 0) + 1;
|
||||
if (s.language) byLanguage[s.language] = (byLanguage[s.language] || 0) + 1;
|
||||
|
||||
if (s.endTime) {
|
||||
const duration = (s.endTime.getTime() - s.startTime.getTime()) / (1000 * 60); // minutes
|
||||
totalDuration += duration;
|
||||
durationCount++;
|
||||
}
|
||||
|
||||
if (s.escalated) escalated++;
|
||||
if (s.forwardedHr) forwarded++;
|
||||
|
||||
if (s.sentiment != null) {
|
||||
totalSentiment += s.sentiment;
|
||||
sentimentCount++;
|
||||
}
|
||||
|
||||
if (s.avgResponseTime != null) {
|
||||
totalResponse += s.avgResponseTime;
|
||||
responseCount++;
|
||||
}
|
||||
|
||||
totalTokens += s.tokens || 0;
|
||||
totalTokensEur += s.tokensEur || 0;
|
||||
});
|
||||
|
||||
// Now add sentiment alert logic:
|
||||
let belowThreshold = 0;
|
||||
const threshold = companyConfig.sentimentAlert ?? null;
|
||||
if (threshold != null) {
|
||||
for (const s of sessions) {
|
||||
if (s.sentiment != null && s.sentiment < threshold) belowThreshold++;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate average sessions per day
|
||||
const dayCount = Object.keys(byDay).length;
|
||||
const avgSessionsPerDay = dayCount > 0 ? total / dayCount : 0;
|
||||
|
||||
// Calculate average session length
|
||||
const avgSessionLength = durationCount > 0 ? totalDuration / durationCount : null;
|
||||
|
||||
return {
|
||||
totalSessions: total,
|
||||
avgSessionsPerDay,
|
||||
avgSessionLength,
|
||||
days: byDay,
|
||||
languages: byLanguage,
|
||||
categories: byCategory,
|
||||
belowThresholdCount: belowThreshold,
|
||||
// Additional metrics not in the interface - using type assertion
|
||||
escalatedCount: escalated,
|
||||
forwardedCount: forwarded,
|
||||
avgSentiment: sentimentCount ? totalSentiment / sentimentCount : null,
|
||||
avgResponseTime: responseCount ? totalResponse / responseCount : null,
|
||||
totalTokens,
|
||||
totalTokensEur,
|
||||
sentimentThreshold: threshold,
|
||||
} as MetricsResult;
|
||||
}
|
||||
20
lib/prisma.ts
Normal file
20
lib/prisma.ts
Normal file
@ -0,0 +1,20 @@
|
||||
// Simple Prisma client setup
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
// Add prisma to the NodeJS global type
|
||||
// This approach avoids NodeJS.Global which is not available
|
||||
|
||||
// Prevent multiple instances of Prisma Client in development
|
||||
declare const global: {
|
||||
prisma: PrismaClient | undefined;
|
||||
};
|
||||
|
||||
// Initialize Prisma Client
|
||||
const prisma = global.prisma || new PrismaClient();
|
||||
|
||||
// Save in global if we're in development
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
global.prisma = prisma;
|
||||
}
|
||||
|
||||
export { prisma };
|
||||
27
lib/scheduler.ts
Normal file
27
lib/scheduler.ts
Normal file
@ -0,0 +1,27 @@
|
||||
// node-cron job to auto-refresh session data every 15 mins
|
||||
import cron from "node-cron";
|
||||
import { prisma } from "./prisma";
|
||||
import { fetchAndParseCsv } from "./csvFetcher";
|
||||
|
||||
export function startScheduler() {
|
||||
cron.schedule("*/15 * * * *", async () => {
|
||||
const companies = await prisma.company.findMany();
|
||||
for (const company of companies) {
|
||||
try {
|
||||
// @ts-expect-error - Handle type conversion on session import
|
||||
const sessions = await fetchAndParseCsv(company.csvUrl, company.csvUsername as string | undefined, company.csvPassword as string | undefined);
|
||||
await prisma.session.deleteMany({ where: { companyId: company.id } });
|
||||
for (const session of sessions) {
|
||||
// @ts-expect-error - Proper data mapping would be needed for production
|
||||
await prisma.session.create({
|
||||
// @ts-expect-error - We ensure id is present but TypeScript doesn't know
|
||||
data: { ...session, companyId: company.id, id: session.id || session.sessionId || `sess_${Date.now()}` },
|
||||
});
|
||||
}
|
||||
console.log(`[Scheduler] Refreshed sessions for company: ${company.name}`);
|
||||
} catch (e) {
|
||||
console.error(`[Scheduler] Failed for company: ${company.name} - ${e}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
4
lib/sendEmail.ts
Normal file
4
lib/sendEmail.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export async function sendEmail(to: string, subject: string, text: string): Promise<void> {
|
||||
// For demo: log to console. Use nodemailer/sendgrid/whatever in prod.
|
||||
console.log(`[Email to ${to}]: ${subject}\n${text}`);
|
||||
}
|
||||
86
lib/types.ts
Normal file
86
lib/types.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import { Session as NextAuthSession } from 'next-auth';
|
||||
|
||||
export interface UserSession extends NextAuthSession {
|
||||
user: {
|
||||
id?: string;
|
||||
name?: string;
|
||||
email?: string;
|
||||
image?: string;
|
||||
companyId: string;
|
||||
role: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Company {
|
||||
id: string;
|
||||
name: string;
|
||||
csvUrl: string;
|
||||
csvUsername?: string;
|
||||
csvPassword?: string;
|
||||
sentimentAlert?: number; // Match Prisma schema naming
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
password: string;
|
||||
role: string;
|
||||
companyId: string;
|
||||
resetToken?: string | null;
|
||||
resetTokenExpiry?: Date | null;
|
||||
company?: Company;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface ChatSession {
|
||||
id: string;
|
||||
sessionId: string;
|
||||
companyId: string;
|
||||
userId?: string | null;
|
||||
category?: string | null;
|
||||
language?: string | null;
|
||||
sentiment?: number | null;
|
||||
startTime: Date;
|
||||
endTime?: Date | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
|
||||
// Extended session properties that might be used in metrics
|
||||
avgResponseTime?: number | null;
|
||||
escalated?: boolean;
|
||||
forwardedHr?: boolean;
|
||||
tokens?: number;
|
||||
tokensEur?: number;
|
||||
initialMsg?: string;
|
||||
}
|
||||
|
||||
export interface DayMetrics {
|
||||
[day: string]: number;
|
||||
}
|
||||
|
||||
export interface CategoryMetrics {
|
||||
[category: string]: number;
|
||||
}
|
||||
|
||||
export interface LanguageMetrics {
|
||||
[language: string]: number;
|
||||
}
|
||||
|
||||
export interface MetricsResult {
|
||||
totalSessions: number;
|
||||
avgSessionsPerDay: number;
|
||||
avgSessionLength: number | null;
|
||||
days: DayMetrics;
|
||||
languages: LanguageMetrics;
|
||||
categories: CategoryMetrics;
|
||||
belowThresholdCount: number;
|
||||
}
|
||||
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
error?: string;
|
||||
}
|
||||
Reference in New Issue
Block a user