mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 08:32:09 +01:00
Enhances data handling and geographic mapping
Refactors dashboard to use actual metrics for country data, removing dummy data for improved accuracy. Integrates the country-code-lookup package for geographic mapping, adding comprehensive country coordinates. Increases performance and data validation across API endpoints and adjusts WordCloud component size for better visualization. Enhances session handling with improved validation logic, and updates configuration for allowed origins.
This commit is contained in:
@ -142,24 +142,22 @@ function DashboardContent() {
|
|||||||
return metrics.wordCloudData;
|
return metrics.wordCloudData;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Function to prepare country data for the map - using simulated/dummy data
|
// Function to prepare country data for the map using actual metrics
|
||||||
const getCountryData = () => {
|
const getCountryData = () => {
|
||||||
return {
|
if (!metrics || !metrics.countries) return {};
|
||||||
US: 42,
|
|
||||||
GB: 25,
|
// Convert the countries object from metrics to the format expected by GeographicMap
|
||||||
DE: 18,
|
const result = Object.entries(metrics.countries).reduce(
|
||||||
FR: 15,
|
(acc, [code, count]) => {
|
||||||
CA: 12,
|
if (code && count) {
|
||||||
AU: 10,
|
acc[code] = count;
|
||||||
JP: 8,
|
}
|
||||||
BR: 6,
|
return acc;
|
||||||
IN: 5,
|
},
|
||||||
ZA: 3,
|
{} as Record<string, number>
|
||||||
ES: 7,
|
);
|
||||||
NL: 9,
|
|
||||||
IT: 6,
|
return result;
|
||||||
SE: 4,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Function to prepare response time distribution data
|
// Function to prepare response time distribution data
|
||||||
@ -378,7 +376,7 @@ function DashboardContent() {
|
|||||||
<h3 className="font-bold text-lg text-gray-800 mb-4">
|
<h3 className="font-bold text-lg text-gray-800 mb-4">
|
||||||
Transcript Word Cloud
|
Transcript Word Cloud
|
||||||
</h3>
|
</h3>
|
||||||
<WordCloud words={getWordCloudData()} width={500} height={300} />
|
<WordCloud words={getWordCloudData()} width={400} height={300} />
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white p-6 rounded-xl shadow">
|
<div className="bg-white p-6 rounded-xl shadow">
|
||||||
<h3 className="font-bold text-lg text-gray-800 mb-4">
|
<h3 className="font-bold text-lg text-gray-800 mb-4">
|
||||||
|
|||||||
@ -40,6 +40,7 @@ export default function SessionsPage() {
|
|||||||
// Pagination states
|
// Pagination states
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [totalPages, setTotalPages] = useState(0);
|
const [totalPages, setTotalPages] = useState(0);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const [pageSize, setPageSize] = useState(10); // Or make this configurable
|
const [pageSize, setPageSize] = useState(10); // Or make this configurable
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -92,12 +92,17 @@ export default function DonutChart({ data, centerText }: DonutChartProps) {
|
|||||||
{
|
{
|
||||||
id: "centerText",
|
id: "centerText",
|
||||||
beforeDraw: function (chart: any) {
|
beforeDraw: function (chart: any) {
|
||||||
const width = chart.width;
|
|
||||||
const height = chart.height;
|
const height = chart.height;
|
||||||
const ctx = chart.ctx;
|
const ctx = chart.ctx;
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
|
|
||||||
const centerX = width / 2;
|
// Calculate the actual chart area width (excluding legend)
|
||||||
|
// Legend is positioned on the right, so we adjust the center X coordinate
|
||||||
|
const chartArea = chart.chartArea;
|
||||||
|
const chartWidth = chartArea.right - chartArea.left;
|
||||||
|
|
||||||
|
// Get the center of just the chart area (not including the legend)
|
||||||
|
const centerX = chartArea.left + chartWidth / 2;
|
||||||
const centerY = height / 2;
|
const centerY = height / 2;
|
||||||
|
|
||||||
// Title text
|
// Title text
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import "leaflet/dist/leaflet.css";
|
import "leaflet/dist/leaflet.css";
|
||||||
|
import countryLookup from "country-code-lookup";
|
||||||
|
|
||||||
// Define types for country data
|
// Define types for country data
|
||||||
interface CountryData {
|
interface CountryData {
|
||||||
@ -17,25 +18,41 @@ interface GeographicMapProps {
|
|||||||
height?: number; // Optional height for the container
|
height?: number; // Optional height for the container
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default coordinates for commonly used countries (latitude, longitude)
|
// Get country coordinates from the country-code-lookup package
|
||||||
const DEFAULT_COORDINATES: Record<string, [number, number]> = {
|
const getCountryCoordinates = (): Record<string, [number, number]> => {
|
||||||
US: [37.0902, -95.7129],
|
// Initialize with some fallback coordinates for common countries that might be missing
|
||||||
GB: [55.3781, -3.436],
|
const coordinates: Record<string, [number, number]> = {
|
||||||
DE: [51.1657, 10.4515],
|
// These are just in case the lookup fails for common countries
|
||||||
FR: [46.2276, 2.2137],
|
US: [37.0902, -95.7129],
|
||||||
CA: [56.1304, -106.3468],
|
GB: [55.3781, -3.436],
|
||||||
AU: [-25.2744, 133.7751],
|
BA: [43.9159, 17.6791],
|
||||||
JP: [36.2048, 138.2529],
|
};
|
||||||
BR: [-14.235, -51.9253],
|
|
||||||
IN: [20.5937, 78.9629],
|
try {
|
||||||
ZA: [-30.5595, 22.9375],
|
// Get all countries from the package
|
||||||
ES: [40.4637, -3.7492],
|
const allCountries = countryLookup.countries;
|
||||||
NL: [52.1326, 5.2913],
|
|
||||||
IT: [41.8719, 12.5674],
|
// Map through all countries and extract coordinates
|
||||||
SE: [60.1282, 18.6435],
|
allCountries.forEach((country) => {
|
||||||
// Add more country coordinates as needed
|
if (country.iso2 && country.latitude && country.longitude) {
|
||||||
|
coordinates[country.iso2] = [
|
||||||
|
parseFloat(country.latitude),
|
||||||
|
parseFloat(country.longitude),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return coordinates;
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error("Error loading country coordinates:", error);
|
||||||
|
return coordinates;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Load coordinates once when module is imported
|
||||||
|
const DEFAULT_COORDINATES = getCountryCoordinates();
|
||||||
|
|
||||||
// Dynamically import the Map component to avoid SSR issues
|
// Dynamically import the Map component to avoid SSR issues
|
||||||
// This ensures the component only loads on the client side
|
// This ensures the component only loads on the client side
|
||||||
const Map = dynamic(() => import("./Map"), {
|
const Map = dynamic(() => import("./Map"), {
|
||||||
@ -68,9 +85,15 @@ export default function GeographicMap({
|
|||||||
// Generate CountryData array for the Map component
|
// Generate CountryData array for the Map component
|
||||||
const data: CountryData[] = Object.entries(countries)
|
const data: CountryData[] = Object.entries(countries)
|
||||||
// Only include countries with known coordinates
|
// Only include countries with known coordinates
|
||||||
.filter(
|
.filter(([code]) => {
|
||||||
([code]) => countryCoordinates[code] || DEFAULT_COORDINATES[code]
|
// If no coordinates found, log to help with debugging
|
||||||
)
|
if (!countryCoordinates[code] && !DEFAULT_COORDINATES[code]) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.warn(`Missing coordinates for country code: ${code}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
.map(([code, count]) => ({
|
.map(([code, count]) => ({
|
||||||
code,
|
code,
|
||||||
count,
|
count,
|
||||||
@ -78,6 +101,12 @@ export default function GeographicMap({
|
|||||||
DEFAULT_COORDINATES[code] || [0, 0],
|
DEFAULT_COORDINATES[code] || [0, 0],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Log for debugging
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(
|
||||||
|
`Found ${data.length} countries with coordinates out of ${Object.keys(countries).length} total countries`
|
||||||
|
);
|
||||||
|
|
||||||
setCountryData(data);
|
setCountryData(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
@ -86,8 +115,9 @@ export default function GeographicMap({
|
|||||||
}
|
}
|
||||||
}, [countries, countryCoordinates, isClient]);
|
}, [countries, countryCoordinates, isClient]);
|
||||||
|
|
||||||
// Find the max count for scaling circles
|
// Find the max count for scaling circles - handle empty countries object
|
||||||
const maxCount = Math.max(...Object.values(countries), 1);
|
const countryValues = Object.values(countries);
|
||||||
|
const maxCount = countryValues.length > 0 ? Math.max(...countryValues, 1) : 1;
|
||||||
|
|
||||||
// Show loading state during SSR or until client-side rendering takes over
|
// Show loading state during SSR or until client-side rendering takes over
|
||||||
if (!isClient) {
|
if (!isClient) {
|
||||||
@ -100,7 +130,13 @@ export default function GeographicMap({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ height: `${height}px`, width: "100%" }} className="relative">
|
<div style={{ height: `${height}px`, width: "100%" }} className="relative">
|
||||||
<Map countryData={countryData} maxCount={maxCount} />
|
{countryData.length > 0 ? (
|
||||||
|
<Map countryData={countryData} maxCount={maxCount} />
|
||||||
|
) : (
|
||||||
|
<div className="h-full w-full bg-gray-100 flex items-center justify-center text-gray-500">
|
||||||
|
No geographic data available
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<style jsx global>{`
|
<style jsx global>{`
|
||||||
.leaflet-control-attribution {
|
.leaflet-control-attribution {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
|
|||||||
@ -359,7 +359,7 @@ export function sessionMetrics(
|
|||||||
if (s.endTime) {
|
if (s.endTime) {
|
||||||
const duration =
|
const duration =
|
||||||
(s.endTime.getTime() - s.startTime.getTime()) / (1000 * 60); // minutes
|
(s.endTime.getTime() - s.startTime.getTime()) / (1000 * 60); // minutes
|
||||||
|
|
||||||
// Sanity check: Only include sessions with reasonable durations (less than 24 hours)
|
// Sanity check: Only include sessions with reasonable durations (less than 24 hours)
|
||||||
const MAX_REASONABLE_DURATION = 24 * 60; // 24 hours in minutes
|
const MAX_REASONABLE_DURATION = 24 * 60; // 24 hours in minutes
|
||||||
if (duration > 0 && duration < MAX_REASONABLE_DURATION) {
|
if (duration > 0 && duration < MAX_REASONABLE_DURATION) {
|
||||||
|
|||||||
190
lib/types.ts
190
lib/types.ts
@ -1,140 +1,140 @@
|
|||||||
import { Session as NextAuthSession } from "next-auth";
|
import { Session as NextAuthSession } from "next-auth";
|
||||||
|
|
||||||
export interface UserSession extends NextAuthSession {
|
export interface UserSession extends NextAuthSession {
|
||||||
user: {
|
user: {
|
||||||
id?: string;
|
id?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
image?: string;
|
image?: string;
|
||||||
companyId: string;
|
companyId: string;
|
||||||
role: string;
|
role: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Company {
|
export interface Company {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
csvUrl: string;
|
csvUrl: string;
|
||||||
csvUsername?: string;
|
csvUsername?: string;
|
||||||
csvPassword?: string;
|
csvPassword?: string;
|
||||||
sentimentAlert?: number; // Match Prisma schema naming
|
sentimentAlert?: number; // Match Prisma schema naming
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
id: string;
|
id: string;
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
role: string;
|
role: string;
|
||||||
companyId: string;
|
companyId: string;
|
||||||
resetToken?: string | null;
|
resetToken?: string | null;
|
||||||
resetTokenExpiry?: Date | null;
|
resetTokenExpiry?: Date | null;
|
||||||
company?: Company;
|
company?: Company;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChatSession {
|
export interface ChatSession {
|
||||||
id: string;
|
id: string;
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
companyId: string;
|
companyId: string;
|
||||||
userId?: string | null;
|
userId?: string | null;
|
||||||
category?: string | null;
|
category?: string | null;
|
||||||
language?: string | null;
|
language?: string | null;
|
||||||
country?: string | null;
|
country?: string | null;
|
||||||
ipAddress?: string | null;
|
ipAddress?: string | null;
|
||||||
sentiment?: number | null;
|
sentiment?: number | null;
|
||||||
messagesSent?: number;
|
messagesSent?: number;
|
||||||
startTime: Date;
|
startTime: Date;
|
||||||
endTime?: Date | null;
|
endTime?: Date | null;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
|
||||||
// Extended session properties that might be used in metrics
|
// Extended session properties that might be used in metrics
|
||||||
avgResponseTime?: number | null;
|
avgResponseTime?: number | null;
|
||||||
escalated?: boolean;
|
escalated?: boolean;
|
||||||
forwardedHr?: boolean;
|
forwardedHr?: boolean;
|
||||||
tokens?: number;
|
tokens?: number;
|
||||||
tokensEur?: number;
|
tokensEur?: number;
|
||||||
initialMsg?: string;
|
initialMsg?: string;
|
||||||
fullTranscriptUrl?: string | null;
|
fullTranscriptUrl?: string | null;
|
||||||
transcriptContent?: string | null;
|
transcriptContent?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SessionQuery {
|
export interface SessionQuery {
|
||||||
searchTerm?: string;
|
searchTerm?: string;
|
||||||
category?: string;
|
category?: string;
|
||||||
language?: string;
|
language?: string;
|
||||||
startDate?: string;
|
startDate?: string;
|
||||||
endDate?: string;
|
endDate?: string;
|
||||||
sortKey?: string;
|
sortKey?: string;
|
||||||
sortOrder?: "asc" | "desc";
|
sortOrder?: "asc" | "desc";
|
||||||
page?: number;
|
page?: number;
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SessionApiResponse {
|
export interface SessionApiResponse {
|
||||||
sessions: ChatSession[];
|
sessions: ChatSession[];
|
||||||
totalSessions: number;
|
totalSessions: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SessionFilterOptions {
|
export interface SessionFilterOptions {
|
||||||
categories: string[];
|
categories: string[];
|
||||||
languages: string[];
|
languages: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DayMetrics {
|
export interface DayMetrics {
|
||||||
[day: string]: number;
|
[day: string]: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CategoryMetrics {
|
export interface CategoryMetrics {
|
||||||
[category: string]: number;
|
[category: string]: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LanguageMetrics {
|
export interface LanguageMetrics {
|
||||||
[language: string]: number;
|
[language: string]: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CountryMetrics {
|
export interface CountryMetrics {
|
||||||
[country: string]: number;
|
[country: string]: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WordCloudWord {
|
export interface WordCloudWord {
|
||||||
text: string;
|
text: string;
|
||||||
value: number;
|
value: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MetricsResult {
|
export interface MetricsResult {
|
||||||
totalSessions: number;
|
totalSessions: number;
|
||||||
avgSessionsPerDay: number;
|
avgSessionsPerDay: number;
|
||||||
avgSessionLength: number | null;
|
avgSessionLength: number | null;
|
||||||
days: DayMetrics;
|
days: DayMetrics;
|
||||||
languages: LanguageMetrics;
|
languages: LanguageMetrics;
|
||||||
categories: CategoryMetrics;
|
categories: CategoryMetrics;
|
||||||
countries: CountryMetrics; // Added for geographic distribution
|
countries: CountryMetrics; // Added for geographic distribution
|
||||||
belowThresholdCount: number;
|
belowThresholdCount: number;
|
||||||
// Additional properties for dashboard
|
// Additional properties for dashboard
|
||||||
escalatedCount?: number;
|
escalatedCount?: number;
|
||||||
forwardedCount?: number;
|
forwardedCount?: number;
|
||||||
avgSentiment?: number;
|
avgSentiment?: number;
|
||||||
avgResponseTime?: number;
|
avgResponseTime?: number;
|
||||||
totalTokens?: number;
|
totalTokens?: number;
|
||||||
totalTokensEur?: number;
|
totalTokensEur?: number;
|
||||||
sentimentThreshold?: number | null;
|
sentimentThreshold?: number | null;
|
||||||
lastUpdated?: number; // Timestamp for when metrics were last updated
|
lastUpdated?: number; // Timestamp for when metrics were last updated
|
||||||
|
|
||||||
// New metrics for enhanced dashboard
|
// New metrics for enhanced dashboard
|
||||||
sentimentPositiveCount?: number;
|
sentimentPositiveCount?: number;
|
||||||
sentimentNeutralCount?: number;
|
sentimentNeutralCount?: number;
|
||||||
sentimentNegativeCount?: number;
|
sentimentNegativeCount?: number;
|
||||||
tokensByDay?: DayMetrics;
|
tokensByDay?: DayMetrics;
|
||||||
tokensCostByDay?: DayMetrics;
|
tokensCostByDay?: DayMetrics;
|
||||||
wordCloudData?: WordCloudWord[]; // Added for transcript-based word cloud
|
wordCloudData?: WordCloudWord[]; // Added for transcript-based word cloud
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiResponse<T> {
|
export interface ApiResponse<T> {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
data?: T;
|
data?: T;
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,12 @@
|
|||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
// Allow cross-origin requests from specific origins in development
|
// Allow cross-origin requests from specific origins in development
|
||||||
allowedDevOrigins: ["192.168.1.2", "localhost", "propc"],
|
allowedDevOrigins: [
|
||||||
|
"192.168.1.2",
|
||||||
|
"localhost",
|
||||||
|
"propc",
|
||||||
|
"test123.kjanat.com",
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
8
package-lock.json
generated
8
package-lock.json
generated
@ -11,11 +11,13 @@
|
|||||||
"@prisma/client": "^6.8.2",
|
"@prisma/client": "^6.8.2",
|
||||||
"@types/d3": "^7.4.3",
|
"@types/d3": "^7.4.3",
|
||||||
"@types/d3-cloud": "^1.2.9",
|
"@types/d3-cloud": "^1.2.9",
|
||||||
|
"@types/geojson": "^7946.0.16",
|
||||||
"@types/leaflet": "^1.9.18",
|
"@types/leaflet": "^1.9.18",
|
||||||
"@types/node-fetch": "^2.6.12",
|
"@types/node-fetch": "^2.6.12",
|
||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
"chart.js": "^4.0.0",
|
"chart.js": "^4.0.0",
|
||||||
"chartjs-plugin-annotation": "^3.1.0",
|
"chartjs-plugin-annotation": "^3.1.0",
|
||||||
|
"country-code-lookup": "^0.1.3",
|
||||||
"csv-parse": "^5.5.0",
|
"csv-parse": "^5.5.0",
|
||||||
"d3": "^7.9.0",
|
"d3": "^7.9.0",
|
||||||
"d3-cloud": "^1.2.7",
|
"d3-cloud": "^1.2.7",
|
||||||
@ -3029,6 +3031,12 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/country-code-lookup": {
|
||||||
|
"version": "0.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/country-code-lookup/-/country-code-lookup-0.1.3.tgz",
|
||||||
|
"integrity": "sha512-gLu+AQKHUnkSQNTxShKgi/4tYd0vEEait3JMrLNZgYlmIZ9DJLkHUjzXE9qcs7dy3xY/kUx2/nOxZ0Z3D9JE+A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/create-require": {
|
"node_modules/create-require": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||||
|
|||||||
@ -18,11 +18,13 @@
|
|||||||
"@prisma/client": "^6.8.2",
|
"@prisma/client": "^6.8.2",
|
||||||
"@types/d3": "^7.4.3",
|
"@types/d3": "^7.4.3",
|
||||||
"@types/d3-cloud": "^1.2.9",
|
"@types/d3-cloud": "^1.2.9",
|
||||||
|
"@types/geojson": "^7946.0.16",
|
||||||
"@types/leaflet": "^1.9.18",
|
"@types/leaflet": "^1.9.18",
|
||||||
"@types/node-fetch": "^2.6.12",
|
"@types/node-fetch": "^2.6.12",
|
||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
"chart.js": "^4.0.0",
|
"chart.js": "^4.0.0",
|
||||||
"chartjs-plugin-annotation": "^3.1.0",
|
"chartjs-plugin-annotation": "^3.1.0",
|
||||||
|
"country-code-lookup": "^0.1.3",
|
||||||
"csv-parse": "^5.5.0",
|
"csv-parse": "^5.5.0",
|
||||||
"d3": "^7.9.0",
|
"d3": "^7.9.0",
|
||||||
"d3-cloud": "^1.2.7",
|
"d3-cloud": "^1.2.7",
|
||||||
|
|||||||
@ -5,66 +5,72 @@ import { prisma } from "../../../lib/prisma";
|
|||||||
import { SessionFilterOptions } from "../../../lib/types";
|
import { SessionFilterOptions } from "../../../lib/types";
|
||||||
|
|
||||||
export default async function handler(
|
export default async function handler(
|
||||||
req: NextApiRequest,
|
req: NextApiRequest,
|
||||||
res: NextApiResponse<SessionFilterOptions | { error: string; details?: string; }>
|
res: NextApiResponse<
|
||||||
|
SessionFilterOptions | { error: string; details?: string }
|
||||||
|
>
|
||||||
) {
|
) {
|
||||||
if (req.method !== "GET") {
|
if (req.method !== "GET") {
|
||||||
return res.status(405).json({ error: "Method not allowed" });
|
return res.status(405).json({ error: "Method not allowed" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const authSession = await getServerSession(req, res, authOptions);
|
const authSession = await getServerSession(req, res, authOptions);
|
||||||
|
|
||||||
if (!authSession || !authSession.user?.companyId) {
|
if (!authSession || !authSession.user?.companyId) {
|
||||||
return res.status(401).json({ error: "Unauthorized" });
|
return res.status(401).json({ error: "Unauthorized" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const companyId = authSession.user.companyId;
|
const companyId = authSession.user.companyId;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const categories = await prisma.session.findMany({
|
const categories = await prisma.session.findMany({
|
||||||
where: {
|
where: {
|
||||||
companyId,
|
companyId,
|
||||||
category: {
|
category: {
|
||||||
not: null, // Ensure category is not null
|
not: null, // Ensure category is not null
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
distinct: ["category"],
|
distinct: ["category"],
|
||||||
select: {
|
select: {
|
||||||
category: true,
|
category: true,
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: {
|
||||||
category: "asc",
|
category: "asc",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const languages = await prisma.session.findMany({
|
const languages = await prisma.session.findMany({
|
||||||
where: {
|
where: {
|
||||||
companyId,
|
companyId,
|
||||||
language: {
|
language: {
|
||||||
not: null, // Ensure language is not null
|
not: null, // Ensure language is not null
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
distinct: ["language"],
|
distinct: ["language"],
|
||||||
select: {
|
select: {
|
||||||
language: true,
|
language: true,
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: {
|
||||||
language: "asc",
|
language: "asc",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const distinctCategories = categories.map((s) => s.category).filter(Boolean) as string[]; // Filter out any nulls and assert as string[]
|
const distinctCategories = categories
|
||||||
const distinctLanguages = languages.map((s) => s.language).filter(Boolean) as string[]; // Filter out any nulls and assert as string[]
|
.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 res
|
return res
|
||||||
.status(200)
|
.status(200)
|
||||||
.json({ categories: distinctCategories, languages: distinctLanguages });
|
.json({ categories: distinctCategories, languages: distinctLanguages });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
error instanceof Error ? error.message : "An unknown error occurred";
|
error instanceof Error ? error.message : "An unknown error occurred";
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
error: "Failed to fetch filter options",
|
error: "Failed to fetch filter options",
|
||||||
details: errorMessage,
|
details: errorMessage,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,11 +2,15 @@ import { NextApiRequest, NextApiResponse } from "next";
|
|||||||
import { getServerSession } from "next-auth/next";
|
import { getServerSession } from "next-auth/next";
|
||||||
import { authOptions } from "../auth/[...nextauth]";
|
import { authOptions } from "../auth/[...nextauth]";
|
||||||
import { prisma } from "../../../lib/prisma";
|
import { prisma } from "../../../lib/prisma";
|
||||||
import { ChatSession, SessionApiResponse, SessionQuery } from "../../../lib/types";
|
import {
|
||||||
|
ChatSession,
|
||||||
|
SessionApiResponse,
|
||||||
|
SessionQuery,
|
||||||
|
} from "../../../lib/types";
|
||||||
|
|
||||||
export default async function handler(
|
export default async function handler(
|
||||||
req: NextApiRequest,
|
req: NextApiRequest,
|
||||||
res: NextApiResponse<SessionApiResponse | { error: string; details?: string; }>
|
res: NextApiResponse<SessionApiResponse | { error: string; details?: string }>
|
||||||
) {
|
) {
|
||||||
if (req.method !== "GET") {
|
if (req.method !== "GET") {
|
||||||
return res.status(405).json({ error: "Method not allowed" });
|
return res.status(405).json({ error: "Method not allowed" });
|
||||||
@ -19,25 +23,25 @@ export default async function handler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const companyId = authSession.user.companyId;
|
const companyId = authSession.user.companyId;
|
||||||
const {
|
const {
|
||||||
searchTerm,
|
searchTerm,
|
||||||
category,
|
category,
|
||||||
language,
|
language,
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
sortKey,
|
sortKey,
|
||||||
sortOrder,
|
sortOrder,
|
||||||
page: queryPage,
|
page: queryPage,
|
||||||
pageSize: queryPageSize,
|
pageSize: queryPageSize,
|
||||||
} = req.query as SessionQuery;
|
} = req.query as SessionQuery;
|
||||||
|
|
||||||
const page = Number(queryPage) || 1;
|
const page = Number(queryPage) || 1;
|
||||||
const pageSize = Number(queryPageSize) || 10;
|
const pageSize = Number(queryPageSize) || 10;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const whereClause: any = { companyId };
|
const whereClause: any = { companyId };
|
||||||
|
|
||||||
// Search Term
|
// Search Term
|
||||||
if (
|
if (
|
||||||
searchTerm &&
|
searchTerm &&
|
||||||
typeof searchTerm === "string" &&
|
typeof searchTerm === "string" &&
|
||||||
@ -45,7 +49,7 @@ export default async function handler(
|
|||||||
) {
|
) {
|
||||||
const searchConditions = [
|
const searchConditions = [
|
||||||
{ id: { contains: searchTerm, mode: "insensitive" } },
|
{ id: { contains: searchTerm, mode: "insensitive" } },
|
||||||
{ sessionId: { contains: searchTerm, mode: "insensitive" } },
|
{ sessionId: { contains: searchTerm, mode: "insensitive" } },
|
||||||
{ category: { contains: searchTerm, mode: "insensitive" } },
|
{ category: { contains: searchTerm, mode: "insensitive" } },
|
||||||
{ initialMsg: { contains: searchTerm, mode: "insensitive" } },
|
{ initialMsg: { contains: searchTerm, mode: "insensitive" } },
|
||||||
{ transcriptContent: { contains: searchTerm, mode: "insensitive" } },
|
{ transcriptContent: { contains: searchTerm, mode: "insensitive" } },
|
||||||
@ -53,54 +57,54 @@ export default async function handler(
|
|||||||
whereClause.OR = searchConditions;
|
whereClause.OR = searchConditions;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Category Filter
|
// Category Filter
|
||||||
if (category && typeof category === "string" && category.trim() !== "") {
|
if (category && typeof category === "string" && category.trim() !== "") {
|
||||||
whereClause.category = category;
|
whereClause.category = category;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Language Filter
|
// Language Filter
|
||||||
if (language && typeof language === "string" && language.trim() !== "") {
|
if (language && typeof language === "string" && language.trim() !== "") {
|
||||||
whereClause.language = language;
|
whereClause.language = language;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Date Range Filter
|
// Date Range Filter
|
||||||
if (startDate && typeof startDate === "string") {
|
if (startDate && typeof startDate === "string") {
|
||||||
if (!whereClause.startTime) whereClause.startTime = {};
|
if (!whereClause.startTime) whereClause.startTime = {};
|
||||||
whereClause.startTime.gte = new Date(startDate);
|
whereClause.startTime.gte = new Date(startDate);
|
||||||
}
|
}
|
||||||
if (endDate && typeof endDate === "string") {
|
if (endDate && typeof endDate === "string") {
|
||||||
if (!whereClause.startTime) whereClause.startTime = {};
|
if (!whereClause.startTime) whereClause.startTime = {};
|
||||||
const inclusiveEndDate = new Date(endDate);
|
const inclusiveEndDate = new Date(endDate);
|
||||||
inclusiveEndDate.setDate(inclusiveEndDate.getDate() + 1);
|
inclusiveEndDate.setDate(inclusiveEndDate.getDate() + 1);
|
||||||
whereClause.startTime.lt = inclusiveEndDate;
|
whereClause.startTime.lt = inclusiveEndDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sorting
|
// Sorting
|
||||||
let orderByClause: any = { startTime: "desc" };
|
let orderByClause: any = { startTime: "desc" };
|
||||||
if (sortKey && typeof sortKey === "string") {
|
if (sortKey && typeof sortKey === "string") {
|
||||||
const order =
|
const order =
|
||||||
sortOrder === "asc" || sortOrder === "desc" ? sortOrder : "desc";
|
sortOrder === "asc" || sortOrder === "desc" ? sortOrder : "desc";
|
||||||
const validSortKeys: { [key: string]: string; } = {
|
const validSortKeys: { [key: string]: string } = {
|
||||||
startTime: "startTime",
|
startTime: "startTime",
|
||||||
category: "category",
|
category: "category",
|
||||||
language: "language",
|
language: "language",
|
||||||
sentiment: "sentiment",
|
sentiment: "sentiment",
|
||||||
messagesSent: "messagesSent",
|
messagesSent: "messagesSent",
|
||||||
avgResponseTime: "avgResponseTime",
|
avgResponseTime: "avgResponseTime",
|
||||||
};
|
};
|
||||||
if (validSortKeys[sortKey]) {
|
if (validSortKeys[sortKey]) {
|
||||||
orderByClause = { [validSortKeys[sortKey]]: order };
|
orderByClause = { [validSortKeys[sortKey]]: order };
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const prismaSessions = await prisma.session.findMany({
|
const prismaSessions = await prisma.session.findMany({
|
||||||
where: whereClause,
|
where: whereClause,
|
||||||
orderBy: orderByClause,
|
orderBy: orderByClause,
|
||||||
skip: (page - 1) * pageSize,
|
skip: (page - 1) * pageSize,
|
||||||
take: pageSize,
|
take: pageSize,
|
||||||
});
|
});
|
||||||
|
|
||||||
const totalSessions = await prisma.session.count({ where: whereClause });
|
const totalSessions = await prisma.session.count({ where: whereClause });
|
||||||
|
|
||||||
const sessions: ChatSession[] = prismaSessions.map((ps) => ({
|
const sessions: ChatSession[] = prismaSessions.map((ps) => ({
|
||||||
id: ps.id,
|
id: ps.id,
|
||||||
@ -127,7 +131,7 @@ export default async function handler(
|
|||||||
transcriptContent: ps.transcriptContent ?? null,
|
transcriptContent: ps.transcriptContent ?? null,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return res.status(200).json({ sessions, totalSessions });
|
return res.status(200).json({ sessions, totalSessions });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
error instanceof Error ? error.message : "An unknown error occurred";
|
error instanceof Error ? error.message : "An unknown error occurred";
|
||||||
|
|||||||
Reference in New Issue
Block a user