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:
2025-05-22 12:48:15 +02:00
parent 3bbb20d889
commit a17b66c078
11 changed files with 316 additions and 251 deletions

View File

@ -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">

View File

@ -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(() => {

View File

@ -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

View File

@ -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]> => {
// Initialize with some fallback coordinates for common countries that might be missing
const coordinates: Record<string, [number, number]> = {
// These are just in case the lookup fails for common countries
US: [37.0902, -95.7129], US: [37.0902, -95.7129],
GB: [55.3781, -3.436], GB: [55.3781, -3.436],
DE: [51.1657, 10.4515], BA: [43.9159, 17.6791],
FR: [46.2276, 2.2137], };
CA: [56.1304, -106.3468],
AU: [-25.2744, 133.7751], try {
JP: [36.2048, 138.2529], // Get all countries from the package
BR: [-14.235, -51.9253], const allCountries = countryLookup.countries;
IN: [20.5937, 78.9629],
ZA: [-30.5595, 22.9375], // Map through all countries and extract coordinates
ES: [40.4637, -3.7492], allCountries.forEach((country) => {
NL: [52.1326, 5.2913], if (country.iso2 && country.latitude && country.longitude) {
IT: [41.8719, 12.5674], coordinates[country.iso2] = [
SE: [60.1282, 18.6435], parseFloat(country.latitude),
// Add more country coordinates as needed 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">
{countryData.length > 0 ? (
<Map countryData={countryData} maxCount={maxCount} /> <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;

View File

@ -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
View File

@ -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",

View File

@ -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",

View File

@ -6,7 +6,9 @@ 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" });
@ -53,8 +55,12 @@ export default async function handler(
}, },
}); });
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)

View File

@ -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" });
@ -80,7 +84,7 @@ export default async function handler(
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",