From a002d5ef763d76d04d5d22318a45d04005a357a4 Mon Sep 17 00:00:00 2001 From: Max Kowalski Date: Sat, 28 Jun 2025 01:12:52 +0200 Subject: [PATCH] feat: update date range calculations and improve UI components for better consistency and readability --- app/api/dashboard/metrics/route.ts | 21 ++- app/dashboard/overview/page.tsx | 179 ++++++++++++++++-------- app/globals.css | 145 ++++++++++--------- components/Charts.tsx | 18 +-- components/ResponseTimeDistribution.tsx | 34 ++--- components/TopQuestionsChart.tsx | 66 ++++----- components/charts/bar-chart.tsx | 18 +-- components/charts/donut-chart.tsx | 12 +- components/charts/line-chart.tsx | 10 +- components/ui/card.tsx | 2 +- components/ui/metric-card.tsx | 29 ++-- 11 files changed, 314 insertions(+), 220 deletions(-) diff --git a/app/api/dashboard/metrics/route.ts b/app/api/dashboard/metrics/route.ts index b0facf6..1b5345e 100644 --- a/app/api/dashboard/metrics/route.ts +++ b/app/api/dashboard/metrics/route.ts @@ -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, diff --git a/app/dashboard/overview/page.tsx b/app/dashboard/overview/page.tsx index 718891f..793062a 100644 --- a/app/dashboard/overview/page.tsx +++ b/app/dashboard/overview/page.tsx @@ -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 (
- {/* Modern Header */} - - -
-
-
-

{company.name}

- - Analytics Dashboard - + {/* Apple-Style Unified Header */} + + +
+ {/* Top row: Company info and actions */} +
+
+
+

{company.name}

+ + Analytics Dashboard + +
+

+ Last updated{" "} + + {new Date(metrics.lastUpdated || Date.now()).toLocaleString()} + +

-

- Last updated{" "} - - {new Date(metrics.lastUpdated || Date.now()).toLocaleString()} - -

-
- -
- - - - - - - signOut({ callbackUrl: "/login" })}> - - Sign out - - - +
+ + + + + + + + signOut({ callbackUrl: "/login" })}> + + Sign out + + + +
+ + {/* Date Range Controls */} + {dateRange && ( +
+
+
+ + Date Range: +
+
+
+ + 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" + /> +
+
+ + 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" + /> +
+
+ + + +
+
+
+

+ Available data: {new Date(dateRange.minDate).toLocaleDateString()} - {new Date(dateRange.maxDate).toLocaleDateString()} +

+
+ )}
- {/* Date Range Picker */} - {dateRange && ( - - )} - {/* Modern Metrics Grid */}
= 0, }} + variant="primary" /> } description="Average per day" + variant="warning" /> } description="Busiest hour" + variant="primary" /> } description="Languages detected" + variant="success" />
diff --git a/app/globals.css b/app/globals.css index f4c1e9b..fd91c14 100644 --- a/app/globals.css +++ b/app/globals.css @@ -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); } } diff --git a/components/Charts.tsx b/components/Charts.tsx index fd959b3..f283823 100644 --- a/components/Charts.tsx +++ b/components/Charts.tsx @@ -128,9 +128,9 @@ export function SentimentChart({ sentimentData }: SentimentChartProps) { sentimentData.negative, ], backgroundColor: [ - "rgba(34, 197, 94, 0.8)", // green - "rgba(249, 115, 22, 0.8)", // orange - "rgba(239, 68, 68, 0.8)", // red + "rgba(37, 99, 235, 0.8)", // blue (primary) + "rgba(107, 114, 128, 0.8)", // gray + "rgba(236, 72, 153, 0.8)", // pink ], borderWidth: 1, }, @@ -196,12 +196,12 @@ export function LanguagePieChart({ languages }: LanguagePieChartProps) { { data, backgroundColor: [ - "rgba(59, 130, 246, 0.8)", - "rgba(16, 185, 129, 0.8)", - "rgba(249, 115, 22, 0.8)", - "rgba(236, 72, 153, 0.8)", - "rgba(139, 92, 246, 0.8)", - "rgba(107, 114, 128, 0.8)", + "rgba(37, 99, 235, 0.8)", // blue (primary) + "rgba(107, 114, 128, 0.8)", // gray + "rgba(236, 72, 153, 0.8)", // pink + "rgba(34, 197, 94, 0.8)", // lime green + "rgba(168, 85, 247, 0.8)", // purple + "rgba(251, 191, 36, 0.8)", // yellow ], borderWidth: 1, }, diff --git a/components/ResponseTimeDistribution.tsx b/components/ResponseTimeDistribution.tsx index dc97668..ef1b604 100644 --- a/components/ResponseTimeDistribution.tsx +++ b/components/ResponseTimeDistribution.tsx @@ -20,10 +20,10 @@ interface ResponseTimeDistributionProps { const CustomTooltip = ({ active, payload, label }: any) => { if (active && payload && payload.length) { return ( -
-

{label}

-

- +

+

{label}

+

+ {payload[0].value} {" "} responses @@ -66,11 +66,11 @@ export default function ResponseTimeDistribution({ label = `${i}-${i + 1} sec`; } - // Determine color based on response time + // Determine color based on response time using cohesive palette let color; - if (i <= 2) color = "hsl(var(--chart-1))"; // Green for fast - else if (i <= 5) color = "hsl(var(--chart-4))"; // Yellow for medium - else color = "hsl(var(--chart-3))"; // Red for slow + if (i <= 2) color = "rgb(37, 99, 235)"; // Blue for fast (primary color) + else if (i <= 5) color = "rgb(107, 114, 128)"; // Gray for medium + else color = "rgb(236, 72, 153)"; // Pink for slow return { name: label, @@ -85,18 +85,18 @@ export default function ResponseTimeDistribution({ } /> @@ -122,14 +122,14 @@ export default function ResponseTimeDistribution({ {/* Average line */} -

{title}

-
+
+

{title}

+
No questions data available
@@ -24,36 +24,38 @@ export default function TopQuestionsChart({ data, title = "Top 5 Asked Questions const maxCount = Math.max(...data.map(q => q.count)); return ( -
-

{title}

+
+

{title}

-
+
{data.map((question, index) => { const percentage = maxCount > 0 ? (question.count / maxCount) * 100 : 0; return ( -
- {/* Question text */} -
-

- {question.question} -

- - {question.count} - -
- - {/* Progress bar */} -
-
-
- - {/* Rank indicator */} -
- {index + 1} +
+ {/* Rank and Question */} +
+
+ {index + 1} +
+
+

+ {question.question} +

+
+
+
+
+
+
+ + {question.count} times + +
+
); @@ -61,10 +63,10 @@ export default function TopQuestionsChart({ data, title = "Top 5 Asked Questions
{/* Summary */} -
-
- Total questions analyzed - +
+
+ Total questions analyzed + {data.reduce((sum, q) => sum + q.count, 0)}
diff --git a/components/charts/bar-chart.tsx b/components/charts/bar-chart.tsx index 9c1b754..f20e6a2 100644 --- a/components/charts/bar-chart.tsx +++ b/components/charts/bar-chart.tsx @@ -43,11 +43,11 @@ export default function ModernBarChart({ title, dataKey = "value", colors = [ - "hsl(var(--chart-1))", - "hsl(var(--chart-2))", - "hsl(var(--chart-3))", - "hsl(var(--chart-4))", - "hsl(var(--chart-5))", + "rgb(37, 99, 235)", // Blue (primary) + "rgb(107, 114, 128)", // Gray + "rgb(236, 72, 153)", // Pink + "rgb(34, 197, 94)", // Lime green + "rgb(168, 85, 247)", // Purple ], height = 300, className, @@ -64,12 +64,12 @@ export default function ModernBarChart({ ))} diff --git a/components/charts/line-chart.tsx b/components/charts/line-chart.tsx index c3a0f10..e900125 100644 --- a/components/charts/line-chart.tsx +++ b/components/charts/line-chart.tsx @@ -44,7 +44,7 @@ export default function ModernLineChart({ data, title, dataKey = "value", - color = "hsl(var(--primary))", + color = "rgb(37, 99, 235)", gradient = true, height = 300, className, @@ -71,18 +71,18 @@ export default function ModernLineChart({ ) {
{ switch (variant) { case "primary": - return "border-primary/20 bg-linear-to-br from-primary/5 to-primary/10"; + return "border border-blue-100 bg-white shadow-sm hover:shadow-md"; case "success": - return "border-green-200 bg-linear-to-br from-green-50 to-green-100 dark:border-green-800 dark:from-green-950 dark:to-green-900"; + return "border border-green-100 bg-white shadow-sm hover:shadow-md"; case "warning": - return "border-amber-200 bg-linear-to-br from-amber-50 to-amber-100 dark:border-amber-800 dark:from-amber-950 dark:to-amber-900"; + return "border border-pink-100 bg-white shadow-sm hover:shadow-md"; case "danger": - return "border-red-200 bg-linear-to-br from-red-50 to-red-100 dark:border-red-800 dark:from-red-950 dark:to-red-900"; + return "border border-red-100 bg-white shadow-sm hover:shadow-md"; default: - return "border-border bg-linear-to-br from-card to-muted/20"; + return "border border-gray-100 bg-white shadow-sm hover:shadow-md"; } }; const getIconClasses = () => { - switch (variant) { - case "primary": - return "bg-primary/10 text-primary border-primary/20"; - case "success": - return "bg-green-100 text-green-600 border-green-200 dark:bg-green-900 dark:text-green-400 dark:border-green-800"; - case "warning": - return "bg-amber-100 text-amber-600 border-amber-200 dark:bg-amber-900 dark:text-amber-400 dark:border-amber-800"; - case "danger": - return "bg-red-100 text-red-600 border-red-200 dark:bg-red-900 dark:text-red-400 dark:border-red-800"; - default: - return "bg-muted text-muted-foreground border-border"; - } + return "bg-gray-50 text-gray-900 border-gray-100"; }; const getTrendIcon = () => { @@ -105,13 +94,11 @@ export default function MetricCard({ className )} > - {/* Subtle gradient overlay */} -
-

+

{title}

{description && ( @@ -137,7 +124,7 @@ export default function MetricCard({
-

+

{value ?? "—"}