mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 07:32:11 +01:00
Enhance session handling and improve data parsing; add safe date parsing utility
This commit is contained in:
@ -6,6 +6,8 @@ import { Company } from "../../lib/types";
|
||||
|
||||
export default function CompanySettingsPage() {
|
||||
const { data: session, status } = useSession();
|
||||
// We store the full company object for future use and updates after save operations
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars
|
||||
const [company, setCompany] = useState<Company | null>(null);
|
||||
const [csvUrl, setCsvUrl] = useState<string>("");
|
||||
const [csvUsername, setCsvUsername] = useState<string>("");
|
||||
|
||||
@ -6,6 +6,7 @@ import { useRouter } from "next/navigation";
|
||||
import Sidebar from "../../components/Sidebar";
|
||||
|
||||
export default function DashboardLayout({ children }: { children: ReactNode }) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars
|
||||
const { data: session, status } = useSession();
|
||||
const router = useRouter();
|
||||
|
||||
@ -28,6 +29,8 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
|
||||
);
|
||||
}
|
||||
|
||||
// Defined for potential future use, like adding a logout button in the layout
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars
|
||||
const handleLogout = () => {
|
||||
signOut({ callbackUrl: "/login" });
|
||||
};
|
||||
|
||||
@ -40,7 +40,7 @@ export default function SessionsPage() {
|
||||
// Pagination states
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(0);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars
|
||||
const [pageSize, setPageSize] = useState(10); // Or make this configurable
|
||||
|
||||
useEffect(() => {
|
||||
@ -283,7 +283,11 @@ export default function SessionsPage() {
|
||||
Session ID: {session.sessionId || session.id}
|
||||
</h2>
|
||||
<p className="text-sm text-gray-500 mb-1">
|
||||
Start Time: {new Date(session.startTime).toLocaleString()}
|
||||
Start Time (Local):{" "}
|
||||
{new Date(session.startTime).toLocaleString()}
|
||||
</p>
|
||||
<p className="text-xs text-gray-400 mb-1">
|
||||
Start Time (Raw API): {session.startTime.toString()}
|
||||
</p>
|
||||
{session.category && (
|
||||
<p className="text-sm text-gray-700">
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { UserSession } from "../../../lib/types";
|
||||
|
||||
interface UserItem {
|
||||
id: string;
|
||||
|
||||
@ -77,7 +77,8 @@ export default function DonutChart({ data, centerText }: DonutChartProps) {
|
||||
const label = context.label || "";
|
||||
const value = context.formattedValue;
|
||||
const total = context.chart.data.datasets[0].data.reduce(
|
||||
(a: number, b: any) => a + (typeof b === "number" ? b : 0),
|
||||
(a: number, b: number | string | null) =>
|
||||
a + (typeof b === "number" ? b : 0),
|
||||
0
|
||||
);
|
||||
const percentage = Math.round((context.parsed * 100) / total);
|
||||
@ -91,7 +92,7 @@ export default function DonutChart({ data, centerText }: DonutChartProps) {
|
||||
? [
|
||||
{
|
||||
id: "centerText",
|
||||
beforeDraw: function (chart: any) {
|
||||
beforeDraw: function (chart: Chart<"doughnut">) {
|
||||
const height = chart.height;
|
||||
const ctx = chart.ctx;
|
||||
ctx.restore();
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image"; // Import the Next.js Image component
|
||||
import Image from "next/image";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { signOut } from "next-auth/react";
|
||||
|
||||
@ -195,16 +195,20 @@ export default function Sidebar() {
|
||||
src="/favicon.svg"
|
||||
alt="LiveDash Logo"
|
||||
fill
|
||||
style={{ objectFit: "contain" }}
|
||||
className="transition-all duration-300"
|
||||
priority // Added priority prop for LCP optimization
|
||||
// Added priority prop for LCP optimization
|
||||
priority
|
||||
style={{
|
||||
objectFit: "contain",
|
||||
maxWidth: "100%",
|
||||
// height: "auto"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{isExpanded && (
|
||||
<span className="text-lg font-bold text-sky-700 mt-1">LiveDash</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Toggle button */}
|
||||
<div className="flex justify-center border-b border-t py-2">
|
||||
<button
|
||||
@ -225,7 +229,6 @@ export default function Sidebar() {
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Navigation items */}
|
||||
<nav className="flex-1 py-4 px-2 overflow-y-auto overflow-x-visible">
|
||||
<NavItem
|
||||
@ -279,7 +282,6 @@ export default function Sidebar() {
|
||||
isActive={pathname === "/dashboard/users"}
|
||||
/>
|
||||
</nav>
|
||||
|
||||
{/* Logout at the bottom */}
|
||||
<div className="p-4 border-t mt-auto">
|
||||
<button
|
||||
|
||||
@ -55,7 +55,7 @@ function formatTranscript(content: string): React.ReactNode[] {
|
||||
rehypePlugins={[rehypeRaw]} // Add rehypeRaw to plugins
|
||||
components={{
|
||||
p: "span",
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars
|
||||
a: ({ node: _node, ...props }) => (
|
||||
<a
|
||||
className="text-sky-600 hover:text-sky-800 underline"
|
||||
@ -107,7 +107,7 @@ function formatTranscript(content: string): React.ReactNode[] {
|
||||
rehypePlugins={[rehypeRaw]} // Add rehypeRaw to plugins
|
||||
components={{
|
||||
p: "span",
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars
|
||||
a: ({ node: _node, ...props }) => (
|
||||
<a
|
||||
className="text-sky-600 hover:text-sky-800 underline"
|
||||
|
||||
@ -53,7 +53,7 @@ export default function WordCloud({
|
||||
)
|
||||
.padding(5)
|
||||
.rotate(() => (~~(Math.random() * 6) - 3) * 15) // Rotate between -45 and 45 degrees
|
||||
.fontSize((d) => (d as any).size)
|
||||
.fontSize((d: CloudWord) => d.size)
|
||||
.on("end", draw);
|
||||
|
||||
layout.start();
|
||||
|
||||
@ -5,7 +5,7 @@ import ISO6391 from "iso-639-1";
|
||||
import countries from "i18n-iso-countries";
|
||||
|
||||
// Register locales for i18n-iso-countries
|
||||
import enLocale from "i18n-iso-countries/langs/en.json" assert { type: "json" };
|
||||
import enLocale from "i18n-iso-countries/langs/en.json" with { type: "json" };
|
||||
countries.registerLocale(enLocale);
|
||||
|
||||
// This type is used internally for parsing the CSV records
|
||||
@ -374,6 +374,60 @@ function isTruthyValue(value?: string): boolean {
|
||||
return truthyValues.includes(value.toLowerCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely parses a date string into a Date object.
|
||||
* Handles potential errors and various formats, prioritizing D-M-YYYY HH:MM:SS.
|
||||
* @param dateStr The date string to parse.
|
||||
* @returns A Date object or null if parsing fails.
|
||||
*/
|
||||
function safeParseDate(dateStr?: string): Date | null {
|
||||
if (!dateStr) return null;
|
||||
|
||||
// Try to parse D-M-YYYY HH:MM:SS format (with hyphens or dots)
|
||||
const dateTimeRegex =
|
||||
/^(\d{1,2})[\.-](\d{1,2})[\.-](\d{4}) (\d{1,2}):(\d{1,2}):(\d{1,2})$/;
|
||||
const match = dateStr.match(dateTimeRegex);
|
||||
|
||||
if (match) {
|
||||
const day = match[1];
|
||||
const month = match[2];
|
||||
const year = match[3];
|
||||
const hour = match[4];
|
||||
const minute = match[5];
|
||||
const second = match[6];
|
||||
|
||||
// Reformat to YYYY-MM-DDTHH:MM:SS (ISO-like, but local time)
|
||||
// Ensure month and day are two digits
|
||||
const formattedDateStr = `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}T${hour.padStart(2, '0')}:${minute.padStart(2, '0')}:${second.padStart(2, '0')}`;
|
||||
|
||||
try {
|
||||
const date = new Date(formattedDateStr);
|
||||
// Basic validation: check if the constructed date is valid
|
||||
if (!isNaN(date.getTime())) {
|
||||
console.log(`[safeParseDate] Parsed from D-M-YYYY: ${dateStr} -> ${formattedDateStr} -> ${date.toISOString()}`);
|
||||
return date;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`[safeParseDate] Error parsing reformatted string ${formattedDateStr} from ${dateStr}:`, e);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback for other potential formats (e.g., direct ISO 8601) or if the primary parse failed
|
||||
try {
|
||||
const parsedDate = new Date(dateStr);
|
||||
if (!isNaN(parsedDate.getTime())) {
|
||||
console.log(`[safeParseDate] Parsed with fallback: ${dateStr} -> ${parsedDate.toISOString()}`);
|
||||
return parsedDate;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`[safeParseDate] Error parsing with fallback ${dateStr}:`, e);
|
||||
}
|
||||
|
||||
|
||||
console.warn(`Failed to parse date string: ${dateStr}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function fetchAndParseCsv(
|
||||
url: string,
|
||||
username?: string,
|
||||
@ -418,13 +472,6 @@ export async function fetchAndParseCsv(
|
||||
trim: true,
|
||||
});
|
||||
|
||||
// Helper function to safely parse dates
|
||||
function safeParseDate(dateStr?: string): Date | null {
|
||||
if (!dateStr) return null;
|
||||
const date = new Date(dateStr);
|
||||
return !isNaN(date.getTime()) ? date : null;
|
||||
}
|
||||
|
||||
// Coerce types for relevant columns
|
||||
return records.map((r) => ({
|
||||
id: r.session_id,
|
||||
|
||||
@ -2,7 +2,7 @@ import ISO6391 from "iso-639-1";
|
||||
import countries from "i18n-iso-countries";
|
||||
|
||||
// Register locales for i18n-iso-countries
|
||||
import enLocale from "i18n-iso-countries/langs/en.json" assert { type: "json" };
|
||||
import enLocale from "i18n-iso-countries/langs/en.json" with { type: "json" };
|
||||
countries.registerLocale(enLocale);
|
||||
|
||||
/**
|
||||
|
||||
@ -7,6 +7,7 @@ import {
|
||||
SessionApiResponse,
|
||||
SessionQuery,
|
||||
} from "../../../lib/types";
|
||||
import { Prisma } from "@prisma/client";
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
@ -39,7 +40,7 @@ export default async function handler(
|
||||
const pageSize = Number(queryPageSize) || 10;
|
||||
|
||||
try {
|
||||
const whereClause: any = { companyId };
|
||||
const whereClause: Prisma.SessionWhereInput = { companyId };
|
||||
|
||||
// Search Term
|
||||
if (
|
||||
@ -48,11 +49,10 @@ export default async function handler(
|
||||
searchTerm.trim() !== ""
|
||||
) {
|
||||
const searchConditions = [
|
||||
{ id: { contains: searchTerm, mode: "insensitive" } },
|
||||
{ sessionId: { contains: searchTerm, mode: "insensitive" } },
|
||||
{ category: { contains: searchTerm, mode: "insensitive" } },
|
||||
{ initialMsg: { contains: searchTerm, mode: "insensitive" } },
|
||||
{ transcriptContent: { contains: searchTerm, mode: "insensitive" } },
|
||||
{ id: { contains: searchTerm } },
|
||||
{ category: { contains: searchTerm } },
|
||||
{ initialMsg: { contains: searchTerm } },
|
||||
{ transcriptContent: { contains: searchTerm } },
|
||||
];
|
||||
whereClause.OR = searchConditions;
|
||||
}
|
||||
@ -69,22 +69,22 @@ export default async function handler(
|
||||
|
||||
// Date Range Filter
|
||||
if (startDate && typeof startDate === "string") {
|
||||
if (!whereClause.startTime) whereClause.startTime = {};
|
||||
whereClause.startTime.gte = new Date(startDate);
|
||||
whereClause.startTime = {
|
||||
...((whereClause.startTime as object) || {}),
|
||||
gte: new Date(startDate),
|
||||
};
|
||||
}
|
||||
if (endDate && typeof endDate === "string") {
|
||||
if (!whereClause.startTime) whereClause.startTime = {};
|
||||
const inclusiveEndDate = new Date(endDate);
|
||||
inclusiveEndDate.setDate(inclusiveEndDate.getDate() + 1);
|
||||
whereClause.startTime.lt = inclusiveEndDate;
|
||||
whereClause.startTime = {
|
||||
...((whereClause.startTime as object) || {}),
|
||||
lt: inclusiveEndDate,
|
||||
};
|
||||
}
|
||||
|
||||
// Sorting
|
||||
let orderByClause: any = { startTime: "desc" };
|
||||
if (sortKey && typeof sortKey === "string") {
|
||||
const order =
|
||||
sortOrder === "asc" || sortOrder === "desc" ? sortOrder : "desc";
|
||||
const validSortKeys: { [key: string]: string } = {
|
||||
const validSortKeys: { [key: string]: string; } = {
|
||||
startTime: "startTime",
|
||||
category: "category",
|
||||
language: "language",
|
||||
@ -92,14 +92,36 @@ export default async function handler(
|
||||
messagesSent: "messagesSent",
|
||||
avgResponseTime: "avgResponseTime",
|
||||
};
|
||||
if (validSortKeys[sortKey]) {
|
||||
orderByClause = { [validSortKeys[sortKey]]: order };
|
||||
}
|
||||
|
||||
let orderByCondition:
|
||||
| Prisma.SessionOrderByWithRelationInput
|
||||
| Prisma.SessionOrderByWithRelationInput[];
|
||||
|
||||
const primarySortField =
|
||||
sortKey && typeof sortKey === "string" && 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" },
|
||||
];
|
||||
}
|
||||
// Note: If sortKey was initially undefined or invalid, primarySortField defaults to "startTime",
|
||||
// and primarySortOrder defaults to "desc". This makes orderByCondition = { startTime: "desc" },
|
||||
// which is the correct overall default sort.
|
||||
|
||||
const prismaSessions = await prisma.session.findMany({
|
||||
where: whereClause,
|
||||
orderBy: orderByClause,
|
||||
orderBy: orderByCondition,
|
||||
skip: (page - 1) * pageSize,
|
||||
take: pageSize,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user