feat: Add DateRangePicker component and integrate date range filtering in metrics fetching

This commit is contained in:
Max Kowalski
2025-06-26 11:42:01 +02:00
parent f964d6a078
commit 0e5ac69d45
3 changed files with 233 additions and 19 deletions

View File

@ -1,6 +1,6 @@
"use client";
import { useEffect, useState } from "react";
import { useEffect, useState, useCallback } from "react";
import { signOut, useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
import {
@ -16,6 +16,7 @@ import WordCloud from "../../../components/WordCloud";
import GeographicMap from "../../../components/GeographicMap";
import ResponseTimeDistribution from "../../../components/ResponseTimeDistribution";
import WelcomeBanner from "../../../components/WelcomeBanner";
import DateRangePicker from "../../../components/DateRangePicker";
// Safely wrapped component with useSession
function DashboardContent() {
@ -25,9 +26,47 @@ function DashboardContent() {
const [company, setCompany] = useState<Company | null>(null);
const [, setLoading] = 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";
// 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(() => {
// Redirect if not authenticated
if (status === "unauthenticated") {
@ -37,23 +76,9 @@ function DashboardContent() {
// Fetch metrics and company on mount if authenticated
if (status === "authenticated") {
const fetchData = async () => {
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();
fetchMetrics();
}
}, [status, router]); // Add status and router to dependency array
}, [status, router, fetchMetrics]); // Add fetchMetrics to dependency array
async function handleRefresh() {
if (isAuditor) return; // Prevent auditors from refreshing
@ -231,6 +256,18 @@ function DashboardContent() {
</button>
</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">
<MetricCard
title="Total Sessions"

View 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>
);
}

View File

@ -33,8 +33,21 @@ export default async function handler(
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({
where: { companyId: user.companyId },
where: whereClause,
});
// Convert Prisma sessions to ChatSession[] type for sessionMetrics
@ -44,7 +57,7 @@ export default async function handler(
companyId: ps.companyId,
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
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
updatedAt: new Date(ps.createdAt), // Use createdAt for updatedAt as Session model doesn't have updatedAt
category: ps.category || undefined,
@ -75,9 +88,20 @@ export default async function handler(
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({
metrics,
csvUrl: user.company.csvUrl,
company: user.company,
dateRange,
});
}