mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 12:52:09 +01:00
feat: Add DateRangePicker component and integrate date range filtering in metrics fetching
This commit is contained in:
@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState, useCallback } from "react";
|
||||||
import { signOut, useSession } from "next-auth/react";
|
import { signOut, useSession } from "next-auth/react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import {
|
import {
|
||||||
@ -16,6 +16,7 @@ import WordCloud from "../../../components/WordCloud";
|
|||||||
import GeographicMap from "../../../components/GeographicMap";
|
import GeographicMap from "../../../components/GeographicMap";
|
||||||
import ResponseTimeDistribution from "../../../components/ResponseTimeDistribution";
|
import ResponseTimeDistribution from "../../../components/ResponseTimeDistribution";
|
||||||
import WelcomeBanner from "../../../components/WelcomeBanner";
|
import WelcomeBanner from "../../../components/WelcomeBanner";
|
||||||
|
import DateRangePicker from "../../../components/DateRangePicker";
|
||||||
|
|
||||||
// Safely wrapped component with useSession
|
// Safely wrapped component with useSession
|
||||||
function DashboardContent() {
|
function DashboardContent() {
|
||||||
@ -25,9 +26,47 @@ function DashboardContent() {
|
|||||||
const [company, setCompany] = useState<Company | null>(null);
|
const [company, setCompany] = useState<Company | null>(null);
|
||||||
const [, setLoading] = useState<boolean>(false);
|
const [, setLoading] = useState<boolean>(false);
|
||||||
const [refreshing, setRefreshing] = useState<boolean>(false);
|
const [refreshing, setRefreshing] = useState<boolean>(false);
|
||||||
|
const [dateRange, setDateRange] = useState<{ minDate: string; maxDate: string } | null>(null);
|
||||||
|
const [selectedStartDate, setSelectedStartDate] = useState<string>("");
|
||||||
|
const [selectedEndDate, setSelectedEndDate] = useState<string>("");
|
||||||
|
|
||||||
const isAuditor = session?.user?.role === "auditor";
|
const isAuditor = session?.user?.role === "auditor";
|
||||||
|
|
||||||
|
// Function to fetch metrics with optional date range
|
||||||
|
const fetchMetrics = useCallback(async (startDate?: string, endDate?: string) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
let url = "/api/dashboard/metrics";
|
||||||
|
if (startDate && endDate) {
|
||||||
|
url += `?startDate=${startDate}&endDate=${endDate}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(url);
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
setMetrics(data.metrics);
|
||||||
|
setCompany(data.company);
|
||||||
|
|
||||||
|
// Set date range from API response (only on initial load)
|
||||||
|
if (data.dateRange && !dateRange) {
|
||||||
|
setDateRange(data.dateRange);
|
||||||
|
setSelectedStartDate(data.dateRange.minDate);
|
||||||
|
setSelectedEndDate(data.dateRange.maxDate);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching metrics:", error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [dateRange]);
|
||||||
|
|
||||||
|
// Handle date range changes
|
||||||
|
const handleDateRangeChange = useCallback((startDate: string, endDate: string) => {
|
||||||
|
setSelectedStartDate(startDate);
|
||||||
|
setSelectedEndDate(endDate);
|
||||||
|
fetchMetrics(startDate, endDate);
|
||||||
|
}, [fetchMetrics]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Redirect if not authenticated
|
// Redirect if not authenticated
|
||||||
if (status === "unauthenticated") {
|
if (status === "unauthenticated") {
|
||||||
@ -37,23 +76,9 @@ function DashboardContent() {
|
|||||||
|
|
||||||
// Fetch metrics and company on mount if authenticated
|
// Fetch metrics and company on mount if authenticated
|
||||||
if (status === "authenticated") {
|
if (status === "authenticated") {
|
||||||
const fetchData = async () => {
|
fetchMetrics();
|
||||||
setLoading(true);
|
|
||||||
const res = await fetch("/api/dashboard/metrics");
|
|
||||||
const data = await res.json();
|
|
||||||
console.log("Metrics from API:", {
|
|
||||||
avgSessionLength: data.metrics.avgSessionLength,
|
|
||||||
avgSessionTimeTrend: data.metrics.avgSessionTimeTrend,
|
|
||||||
totalSessionDuration: data.metrics.totalSessionDuration,
|
|
||||||
validSessionsForDuration: data.metrics.validSessionsForDuration,
|
|
||||||
});
|
|
||||||
setMetrics(data.metrics);
|
|
||||||
setCompany(data.company);
|
|
||||||
setLoading(false);
|
|
||||||
};
|
|
||||||
fetchData();
|
|
||||||
}
|
}
|
||||||
}, [status, router]); // Add status and router to dependency array
|
}, [status, router, fetchMetrics]); // Add fetchMetrics to dependency array
|
||||||
|
|
||||||
async function handleRefresh() {
|
async function handleRefresh() {
|
||||||
if (isAuditor) return; // Prevent auditors from refreshing
|
if (isAuditor) return; // Prevent auditors from refreshing
|
||||||
@ -231,6 +256,18 @@ function DashboardContent() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Date Range Picker */}
|
||||||
|
{dateRange && (
|
||||||
|
<DateRangePicker
|
||||||
|
minDate={dateRange.minDate}
|
||||||
|
maxDate={dateRange.maxDate}
|
||||||
|
onDateRangeChange={handleDateRangeChange}
|
||||||
|
initialStartDate={selectedStartDate}
|
||||||
|
initialEndDate={selectedEndDate}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
<MetricCard
|
<MetricCard
|
||||||
title="Total Sessions"
|
title="Total Sessions"
|
||||||
|
|||||||
153
components/DateRangePicker.tsx
Normal file
153
components/DateRangePicker.tsx
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
interface DateRangePickerProps {
|
||||||
|
minDate: string;
|
||||||
|
maxDate: string;
|
||||||
|
onDateRangeChange: (startDate: string, endDate: string) => void;
|
||||||
|
initialStartDate?: string;
|
||||||
|
initialEndDate?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DateRangePicker({
|
||||||
|
minDate,
|
||||||
|
maxDate,
|
||||||
|
onDateRangeChange,
|
||||||
|
initialStartDate,
|
||||||
|
initialEndDate,
|
||||||
|
}: DateRangePickerProps) {
|
||||||
|
const [startDate, setStartDate] = useState(initialStartDate || minDate);
|
||||||
|
const [endDate, setEndDate] = useState(initialEndDate || maxDate);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Notify parent component when dates change
|
||||||
|
onDateRangeChange(startDate, endDate);
|
||||||
|
}, [startDate, endDate, onDateRangeChange]);
|
||||||
|
|
||||||
|
const handleStartDateChange = (newStartDate: string) => {
|
||||||
|
// Ensure start date is not before min date
|
||||||
|
if (newStartDate < minDate) {
|
||||||
|
setStartDate(minDate);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure start date is not after end date
|
||||||
|
if (newStartDate > endDate) {
|
||||||
|
setEndDate(newStartDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
setStartDate(newStartDate);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEndDateChange = (newEndDate: string) => {
|
||||||
|
// Ensure end date is not after max date
|
||||||
|
if (newEndDate > maxDate) {
|
||||||
|
setEndDate(maxDate);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure end date is not before start date
|
||||||
|
if (newEndDate < startDate) {
|
||||||
|
setStartDate(newEndDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
setEndDate(newEndDate);
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetToFullRange = () => {
|
||||||
|
setStartDate(minDate);
|
||||||
|
setEndDate(maxDate);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setLast30Days = () => {
|
||||||
|
const thirtyDaysAgo = new Date();
|
||||||
|
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||||||
|
const thirtyDaysAgoStr = thirtyDaysAgo.toISOString().split('T')[0];
|
||||||
|
|
||||||
|
// Use the later of 30 days ago or minDate
|
||||||
|
const newStartDate = thirtyDaysAgoStr > minDate ? thirtyDaysAgoStr : minDate;
|
||||||
|
setStartDate(newStartDate);
|
||||||
|
setEndDate(maxDate);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setLast7Days = () => {
|
||||||
|
const sevenDaysAgo = new Date();
|
||||||
|
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
||||||
|
const sevenDaysAgoStr = sevenDaysAgo.toISOString().split('T')[0];
|
||||||
|
|
||||||
|
// Use the later of 7 days ago or minDate
|
||||||
|
const newStartDate = sevenDaysAgoStr > minDate ? sevenDaysAgoStr : minDate;
|
||||||
|
setStartDate(newStartDate);
|
||||||
|
setEndDate(maxDate);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-white p-4 rounded-lg shadow-sm border border-gray-200">
|
||||||
|
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center">
|
||||||
|
<div className="flex flex-col sm:flex-row gap-3 items-start sm:items-center">
|
||||||
|
<label className="text-sm font-medium text-gray-700 whitespace-nowrap">
|
||||||
|
Date Range:
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div className="flex flex-col sm:flex-row gap-2 items-start sm:items-center">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<label htmlFor="start-date" className="text-sm text-gray-600">
|
||||||
|
From:
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="start-date"
|
||||||
|
type="date"
|
||||||
|
value={startDate}
|
||||||
|
min={minDate}
|
||||||
|
max={maxDate}
|
||||||
|
onChange={(e) => handleStartDateChange(e.target.value)}
|
||||||
|
className="px-3 py-1.5 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-sky-500 focus:border-sky-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<label htmlFor="end-date" className="text-sm text-gray-600">
|
||||||
|
To:
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="end-date"
|
||||||
|
type="date"
|
||||||
|
value={endDate}
|
||||||
|
min={minDate}
|
||||||
|
max={maxDate}
|
||||||
|
onChange={(e) => handleEndDateChange(e.target.value)}
|
||||||
|
className="px-3 py-1.5 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-sky-500 focus:border-sky-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<button
|
||||||
|
onClick={setLast7Days}
|
||||||
|
className="px-3 py-1.5 text-xs font-medium text-sky-600 bg-sky-50 border border-sky-200 rounded-md hover:bg-sky-100 transition-colors"
|
||||||
|
>
|
||||||
|
Last 7 days
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={setLast30Days}
|
||||||
|
className="px-3 py-1.5 text-xs font-medium text-sky-600 bg-sky-50 border border-sky-200 rounded-md hover:bg-sky-100 transition-colors"
|
||||||
|
>
|
||||||
|
Last 30 days
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={resetToFullRange}
|
||||||
|
className="px-3 py-1.5 text-xs font-medium text-gray-600 bg-gray-50 border border-gray-200 rounded-md hover:bg-gray-100 transition-colors"
|
||||||
|
>
|
||||||
|
All time
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-2 text-xs text-gray-500">
|
||||||
|
Available data: {new Date(minDate).toLocaleDateString()} - {new Date(maxDate).toLocaleDateString()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -33,8 +33,21 @@ export default async function handler(
|
|||||||
|
|
||||||
if (!user) return res.status(401).json({ error: "No user" });
|
if (!user) return res.status(401).json({ error: "No user" });
|
||||||
|
|
||||||
|
// Get date range from query parameters
|
||||||
|
const { startDate, endDate } = req.query;
|
||||||
|
|
||||||
|
// Build where clause with optional date filtering
|
||||||
|
const whereClause: any = { companyId: user.companyId };
|
||||||
|
|
||||||
|
if (startDate && endDate) {
|
||||||
|
whereClause.startTime = {
|
||||||
|
gte: new Date(startDate as string),
|
||||||
|
lte: new Date(endDate as string + 'T23:59:59.999Z'), // Include full end date
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const prismaSessions = await prisma.session.findMany({
|
const prismaSessions = await prisma.session.findMany({
|
||||||
where: { companyId: user.companyId },
|
where: whereClause,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Convert Prisma sessions to ChatSession[] type for sessionMetrics
|
// Convert Prisma sessions to ChatSession[] type for sessionMetrics
|
||||||
@ -44,7 +57,7 @@ export default async function handler(
|
|||||||
companyId: ps.companyId,
|
companyId: ps.companyId,
|
||||||
startTime: new Date(ps.startTime), // Ensure startTime is a Date object
|
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
|
endTime: ps.endTime ? new Date(ps.endTime) : null, // Ensure endTime is a Date object or null
|
||||||
transcriptContent: ps.transcriptContent || "", // Ensure transcriptContent is a string
|
transcriptContent: "", // Session model doesn't have transcriptContent field
|
||||||
createdAt: new Date(ps.createdAt), // Map Prisma's createdAt
|
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
|
updatedAt: new Date(ps.createdAt), // Use createdAt for updatedAt as Session model doesn't have updatedAt
|
||||||
category: ps.category || undefined,
|
category: ps.category || undefined,
|
||||||
@ -75,9 +88,20 @@ export default async function handler(
|
|||||||
|
|
||||||
const metrics = sessionMetrics(chatSessions, companyConfigForMetrics);
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
metrics,
|
metrics,
|
||||||
csvUrl: user.company.csvUrl,
|
csvUrl: user.company.csvUrl,
|
||||||
company: user.company,
|
company: user.company,
|
||||||
|
dateRange,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user