feat: update date range calculations and improve UI components for better consistency and readability

This commit is contained in:
Max Kowalski
2025-06-28 01:12:52 +02:00
parent c4cfe2f389
commit a002d5ef76
11 changed files with 314 additions and 220 deletions

View File

@ -91,8 +91,11 @@ export async function GET(request: NextRequest) {
const metrics = sessionMetrics(chatSessions, companyConfigForMetrics);
// Calculate date range from ALL sessions (not filtered) to get the full available range
// Calculate date range from the FILTERED sessions to match what's actually displayed
let dateRange: { minDate: string; maxDate: string } | null = null;
let availableDataRange: { minDate: string; maxDate: string } | null = null;
// Get the full available range for reference
const allSessions = await prisma.session.findMany({
where: {
companyId: user.companyId,
@ -106,12 +109,26 @@ export async function GET(request: NextRequest) {
});
if (allSessions.length > 0) {
dateRange = {
availableDataRange = {
minDate: allSessions[0].startTime.toISOString().split('T')[0], // First session date
maxDate: allSessions[allSessions.length - 1].startTime.toISOString().split('T')[0] // Last session date
};
}
// Calculate date range from the filtered sessions (what's actually being displayed)
if (prismaSessions.length > 0) {
const sortedFilteredSessions = prismaSessions.sort((a, b) =>
new Date(a.startTime).getTime() - new Date(b.startTime).getTime()
);
dateRange = {
minDate: sortedFilteredSessions[0].startTime.toISOString().split('T')[0],
maxDate: sortedFilteredSessions[sortedFilteredSessions.length - 1].startTime.toISOString().split('T')[0]
};
} else if (availableDataRange) {
// If no filtered sessions but we have available data, use the available range
dateRange = availableDataRange;
}
return NextResponse.json({
metrics,
csvUrl: user.company.csvUrl,

View File

@ -219,9 +219,9 @@ function DashboardContent() {
};
return [
{ name: "Positive", value: sentimentData.positive, color: "hsl(var(--chart-1))" },
{ name: "Neutral", value: sentimentData.neutral, color: "hsl(var(--chart-2))" },
{ name: "Negative", value: sentimentData.negative, color: "hsl(var(--chart-3))" },
{ name: "Positive", value: sentimentData.positive, color: "rgb(34, 197, 94)" },
{ name: "Neutral", value: sentimentData.neutral, color: "rgb(168, 162, 158)" },
{ name: "Negative", value: sentimentData.negative, color: "rgb(239, 68, 68)" },
];
};
@ -284,65 +284,130 @@ function DashboardContent() {
return (
<div className="space-y-8">
{/* Modern Header */}
<Card className="border-0 bg-linear-to-r from-primary/5 via-primary/10 to-primary/5">
<CardHeader>
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<div className="space-y-2">
<div className="flex items-center gap-3">
<h1 className="text-3xl font-bold tracking-tight">{company.name}</h1>
<Badge variant="secondary" className="text-xs">
Analytics Dashboard
</Badge>
{/* Apple-Style Unified Header */}
<Card className="border-0 bg-white shadow-sm">
<CardHeader className="pb-6">
<div className="flex flex-col space-y-6">
{/* Top row: Company info and actions */}
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<div className="space-y-1">
<div className="flex items-center gap-3">
<h1 className="text-2xl font-semibold text-gray-900 tracking-tight">{company.name}</h1>
<Badge variant="secondary" className="text-xs font-medium bg-gray-100 text-gray-700 border-0">
Analytics Dashboard
</Badge>
</div>
<p className="text-sm text-gray-500">
Last updated{" "}
<span className="font-medium text-gray-700">
{new Date(metrics.lastUpdated || Date.now()).toLocaleString()}
</span>
</p>
</div>
<p className="text-muted-foreground">
Last updated{" "}
<span className="font-medium">
{new Date(metrics.lastUpdated || Date.now()).toLocaleString()}
</span>
</p>
</div>
<div className="flex items-center gap-2">
<Button
onClick={handleRefresh}
disabled={refreshing || isAuditor}
size="sm"
className="gap-2"
>
<RefreshCw className={`h-4 w-4 ${refreshing ? 'animate-spin' : ''}`} />
{refreshing ? "Refreshing..." : "Refresh"}
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm">
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => signOut({ callbackUrl: "/login" })}>
<LogOut className="h-4 w-4 mr-2" />
Sign out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<div className="flex items-center gap-3">
<Button
onClick={handleRefresh}
disabled={refreshing || isAuditor}
size="sm"
className="gap-2 bg-blue-600 hover:bg-blue-700 border-0 shadow-sm"
>
<RefreshCw className={`h-4 w-4 ${refreshing ? 'animate-spin' : ''}`} />
{refreshing ? "Refreshing..." : "Refresh"}
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="border-gray-200 hover:bg-gray-50">
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="border-gray-200 shadow-lg">
<DropdownMenuItem onClick={() => signOut({ callbackUrl: "/login" })}>
<LogOut className="h-4 w-4 mr-2" />
Sign out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
{/* Date Range Controls */}
{dateRange && (
<div className="border-t border-gray-100 pt-6">
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-4">
<div className="flex items-center gap-2">
<Calendar className="h-4 w-4 text-gray-500" />
<span className="text-sm font-medium text-gray-700">Date Range:</span>
</div>
<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
<label className="text-sm text-gray-600">From:</label>
<input
type="date"
value={selectedStartDate}
min={dateRange.minDate}
max={dateRange.maxDate}
onChange={(e) => handleDateRangeChange(e.target.value, selectedEndDate)}
className="px-3 py-1.5 text-sm border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div className="flex items-center gap-2">
<label className="text-sm text-gray-600">To:</label>
<input
type="date"
value={selectedEndDate}
min={dateRange.minDate}
max={dateRange.maxDate}
onChange={(e) => handleDateRangeChange(selectedStartDate, e.target.value)}
className="px-3 py-1.5 text-sm border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => {
const endDate = new Date().toISOString().split('T')[0];
const startDate = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
handleDateRangeChange(startDate, endDate);
}}
className="text-xs border-gray-200 hover:bg-gray-50"
>
Last 7 days
</Button>
<Button
variant="outline"
size="sm"
onClick={() => {
const endDate = new Date().toISOString().split('T')[0];
const startDate = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
handleDateRangeChange(startDate, endDate);
}}
className="text-xs border-gray-200 hover:bg-gray-50"
>
Last 30 days
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleDateRangeChange(dateRange.minDate, dateRange.maxDate)}
className="text-xs border-gray-200 hover:bg-gray-50"
>
All time
</Button>
</div>
</div>
</div>
<p className="text-xs text-gray-500 mt-2">
Available data: {new Date(dateRange.minDate).toLocaleDateString()} - {new Date(dateRange.maxDate).toLocaleDateString()}
</p>
</div>
)}
</div>
</CardHeader>
</Card>
{/* Date Range Picker */}
{dateRange && (
<DateRangePicker
minDate={dateRange.minDate}
maxDate={dateRange.maxDate}
onDateRangeChange={handleDateRangeChange}
initialStartDate={selectedStartDate}
initialEndDate={selectedEndDate}
/>
)}
{/* Modern Metrics Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
<MetricCard
@ -375,6 +440,7 @@ function DashboardContent() {
value: metrics.avgSessionTimeTrend ?? 0,
isPositive: (metrics.avgSessionTimeTrend ?? 0) >= 0,
}}
variant="primary"
/>
<MetricCard
@ -393,6 +459,7 @@ function DashboardContent() {
value={`${metrics.avgDailyCosts?.toFixed(4) || '0.0000'}`}
icon={<Euro className="h-5 w-5" />}
description="Average per day"
variant="warning"
/>
<MetricCard
@ -400,6 +467,7 @@ function DashboardContent() {
value={metrics.peakUsageTime || 'N/A'}
icon={<TrendingUp className="h-5 w-5" />}
description="Busiest hour"
variant="primary"
/>
<MetricCard
@ -418,6 +486,7 @@ function DashboardContent() {
value={Object.keys(metrics.languages || {}).length}
icon={<Globe className="h-5 w-5" />}
description="Languages detected"
variant="success"
/>
</div>

View File

@ -43,71 +43,71 @@
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
--background: 255 255 255;
--foreground: 15 23 42;
--card: 255 255 255;
--card-foreground: 15 23 42;
--popover: 255 255 255;
--popover-foreground: 15 23 42;
--primary: 0 123 255;
--primary-foreground: 255 255 255;
--secondary: 245 245 245;
--secondary-foreground: 51 51 51;
--muted: 248 250 252;
--muted-foreground: 100 116 139;
--accent: 245 245 245;
--accent-foreground: 51 51 51;
--destructive: 239 68 68;
--border: 229 231 235;
--input: 229 231 235;
--ring: 0 123 255;
--chart-1: 0 123 255;
--chart-2: 255 20 147;
--chart-3: 50 205 50;
--chart-4: 138 43 226;
--chart-5: 255 215 0;
--sidebar: 248 250 252;
--sidebar-foreground: 15 23 42;
--sidebar-primary: 0 123 255;
--sidebar-primary-foreground: 255 255 255;
--sidebar-accent: 245 245 245;
--sidebar-accent-foreground: 51 51 51;
--sidebar-border: 229 231 235;
--sidebar-ring: 0 123 255;
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
--background: 15 23 42;
--foreground: 248 250 252;
--card: 30 41 59;
--card-foreground: 248 250 252;
--popover: 30 41 59;
--popover-foreground: 248 250 252;
--primary: 59 130 246;
--primary-foreground: 15 23 42;
--secondary: 51 65 85;
--secondary-foreground: 248 250 252;
--muted: 51 65 85;
--muted-foreground: 148 163 184;
--accent: 51 65 85;
--accent-foreground: 248 250 252;
--destructive: 248 113 113;
--border: 51 65 85;
--input: 51 65 85;
--ring: 59 130 246;
--chart-1: 59 130 246;
--chart-2: 236 72 153;
--chart-3: 34 197 94;
--chart-4: 147 51 234;
--chart-5: 251 191 36;
--sidebar: 30 41 59;
--sidebar-foreground: 248 250 252;
--sidebar-primary: 59 130 246;
--sidebar-primary-foreground: 248 250 252;
--sidebar-accent: 51 65 85;
--sidebar-accent-foreground: 248 250 252;
--sidebar-border: 51 65 85;
--sidebar-ring: 59 130 246;
}
@layer base {
@ -115,6 +115,25 @@
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
@apply bg-gray-50 text-gray-900;
}
/* Apple-style scrollbars */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.3);
}
}