From d3a4ec203591bb0ec319c8760ca3f0819d194b8d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Mar 2026 03:08:37 +0000 Subject: [PATCH 1/3] Initial plan From 830a27a1eebc91a22ca263a0ef64b202819b2fc8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Mar 2026 03:17:13 +0000 Subject: [PATCH 2/3] Add stats page with analytics dashboard for organization workspace Co-authored-by: harshithpabbati <43822585+harshithpabbati@users.noreply.github.com> --- actions/email.ts | 85 ++++++ app/org/[slug]/stats/page.tsx | 24 ++ components/organization/StatsPage.tsx | 306 +++++++++++++++++++ components/organization/WelcomeDashboard.tsx | 4 + 4 files changed, 419 insertions(+) create mode 100644 app/org/[slug]/stats/page.tsx create mode 100644 components/organization/StatsPage.tsx diff --git a/actions/email.ts b/actions/email.ts index c2cd252..fc188e7 100644 --- a/actions/email.ts +++ b/actions/email.ts @@ -20,6 +20,91 @@ export async function getOrgStats(orgId: string) { return { threadCount: threadCount ?? 0, replyCount: replyCount ?? 0 }; } +export async function getDetailedOrgStats(orgId: string) { + const supabase = await createServerClient(); + + const sevenDaysAgo = new Date(); + sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 6); + sevenDaysAgo.setHours(0, 0, 0, 0); + const since = sevenDaysAgo.toISOString(); + + const [ + { count: openThreads }, + { count: closedThreads }, + { data: replyRows }, + { data: recentThreads }, + { data: recentReplies }, + ] = await Promise.all([ + supabase + .from('thread') + .select('id', { count: 'exact', head: true }) + .eq('organization_id', orgId) + .eq('status', 'open'), + supabase + .from('thread') + .select('id', { count: 'exact', head: true }) + .eq('organization_id', orgId) + .eq('status', 'closed'), + supabase + .from('reply') + .select('status, is_perfect') + .eq('organization_id', orgId), + supabase + .from('thread') + .select('created_at') + .eq('organization_id', orgId) + .gte('created_at', since), + supabase + .from('reply') + .select('created_at') + .eq('organization_id', orgId) + .gte('created_at', since), + ]); + + const replyStatusCounts: Record = {}; + let perfectCount = 0; + let editedCount = 0; + const replies: { status: string; is_perfect: boolean | null }[] = + replyRows ?? []; + for (const r of replies) { + replyStatusCounts[r.status] = (replyStatusCounts[r.status] ?? 0) + 1; + if (r.is_perfect === true) perfectCount++; + else if (r.is_perfect === false) editedCount++; + } + + const dayLabels: string[] = []; + const threadsByDay: number[] = []; + const repliesByDay: number[] = []; + const threads: { created_at: string }[] = recentThreads ?? []; + const recentRepliesList: { created_at: string }[] = recentReplies ?? []; + for (let i = 0; i < 7; i++) { + const d = new Date(sevenDaysAgo); + d.setDate(d.getDate() + i); + const label = d.toLocaleDateString('en-US', { weekday: 'short' }); + const dateStr = d.toISOString().slice(0, 10); + dayLabels.push(label); + threadsByDay.push( + threads.filter((t) => t.created_at.slice(0, 10) === dateStr).length + ); + repliesByDay.push( + recentRepliesList.filter((r) => r.created_at.slice(0, 10) === dateStr) + .length + ); + } + + return { + openThreads: openThreads ?? 0, + closedThreads: closedThreads ?? 0, + totalReplies: replies.length, + replyStatusCounts, + perfectCount, + editedCount, + dayLabels, + threadsByDay, + repliesByDay, + }; +} + export async function getThreads(orgId: string, status: 'open' | 'closed') { const supabase = await createServerClient(); const { data, error } = await supabase diff --git a/app/org/[slug]/stats/page.tsx b/app/org/[slug]/stats/page.tsx new file mode 100644 index 0000000..ffc996b --- /dev/null +++ b/app/org/[slug]/stats/page.tsx @@ -0,0 +1,24 @@ +import { Metadata } from 'next'; +import { notFound } from 'next/navigation'; +import { getOrganizationBySlug } from '@/actions/organization'; +import { getDetailedOrgStats } from '@/actions/email'; + +import { StatsPage } from '@/components/organization/StatsPage'; + +interface Props { + params: Promise<{ slug: string }>; +} + +export const metadata: Metadata = { + title: 'Stats', +}; + +export default async function OrgStatsPage({ params }: Props) { + const { slug } = await params; + const { data: org } = await getOrganizationBySlug(slug); + if (!org?.id) return notFound(); + + const stats = await getDetailedOrgStats(org.id); + + return ; +} diff --git a/components/organization/StatsPage.tsx b/components/organization/StatsPage.tsx new file mode 100644 index 0000000..ba455b1 --- /dev/null +++ b/components/organization/StatsPage.tsx @@ -0,0 +1,306 @@ +'use client'; + +import { cn } from '@/lib/utils'; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@/components/ui/card'; + +interface StatsData { + openThreads: number; + closedThreads: number; + totalReplies: number; + replyStatusCounts: Record; + perfectCount: number; + editedCount: number; + dayLabels: string[]; + threadsByDay: number[]; + repliesByDay: number[]; +} + +interface Props { + orgName: string; + stats: StatsData; +} + +function StatCard({ + label, + value, + accent, +}: { + label: string; + value: number; + accent?: boolean; +}) { + return ( + + +

+ {label} +

+

+ {value} +

+
+
+ ); +} + +function BarChart({ + labels, + seriesA, + seriesB, + legendA, + legendB, +}: { + labels: string[]; + seriesA: number[]; + seriesB: number[]; + legendA: string; + legendB: string; +}) { + const max = Math.max(...seriesA, ...seriesB, 1); + + return ( +
+
+ + + {legendA} + + + + {legendB} + +
+
+ {labels.map((label, i) => ( +
+
+
0 ? 4 : 0, + }} + title={`${legendA}: ${seriesA[i]}`} + /> +
0 ? 4 : 0, + }} + title={`${legendB}: ${seriesB[i]}`} + /> +
+ + {label} + +
+ ))} +
+
+ ); +} + +function ProgressBar({ + label, + value, + total, + color, +}: { + label: string; + value: number; + total: number; + color: string; +}) { + const pct = total > 0 ? (value / total) * 100 : 0; + + return ( +
+
+ {label} + + {value} + + ({Math.round(pct)}%) + + +
+
+
+
+
+ ); +} + +export function StatsPage({ orgName, stats }: Props) { + const totalThreads = stats.openThreads + stats.closedThreads; + const totalReviewed = stats.perfectCount + stats.editedCount; + const accuracyPct = + totalReviewed > 0 + ? Math.round((stats.perfectCount / totalReviewed) * 100) + : 0; + + return ( +
+
+

+ // STATS +

+

+ {orgName} Analytics +

+

+ Key metrics and activity for your workspace. +

+
+ + {/* Overview Cards */} +
+ + + + +
+ +
+ {/* Activity Chart */} + + + 📊 Activity — Last 7 Days + + Daily breakdown of incoming threads and AI-generated replies. + + + + + + + + {/* Thread Breakdown */} + + + 🎯 Thread Status + + Open vs closed breakdown across all conversations. + + + + + + + + + {/* Reply Breakdown */} + + + 🤖 Reply Quality + + How well the AI replies matched expectations. + + + + {stats.totalReplies === 0 ? ( +

+ No replies generated yet. Stats will appear once the AI starts + responding to emails. +

+ ) : ( + <> + + + + + )} +
+
+ + {/* Reply Status */} + + + âš¡ Reply Status Breakdown + + Current state of all AI-generated replies. + + + + {stats.totalReplies === 0 ? ( +

+ No replies yet. +

+ ) : ( +
+ {Object.entries(stats.replyStatusCounts).map( + ([status, count]) => ( +
+

+ {status} +

+

+ {count} +

+
+ ) + )} +
+ )} +
+
+
+
+ ); +} diff --git a/components/organization/WelcomeDashboard.tsx b/components/organization/WelcomeDashboard.tsx index e6c3d8f..84bdd9c 100644 --- a/components/organization/WelcomeDashboard.tsx +++ b/components/organization/WelcomeDashboard.tsx @@ -1,6 +1,7 @@ 'use client'; import { useState } from 'react'; +import Link from 'next/link'; import { updateAutopilotSettings } from '@/actions/organization'; import { Tables } from '@/database.types'; import { useAddDataSource, useViewDataSource } from '@/states/data-source'; @@ -354,6 +355,9 @@ export function WelcomeDashboard({
+ + +
{/* Activity Chart */} - 📊 Activity — Last 7 Days + 📊 Activity — Last {stats.dayRange} Days Daily breakdown of incoming threads and AI-generated replies.