From eea8c8b33abaa116fa99bc335734dd35715d4a21 Mon Sep 17 00:00:00 2001 From: Timo Date: Fri, 2 Jan 2026 20:38:57 +0100 Subject: [PATCH] feat: implement dashboard page for QR code listing, statistics, and management. --- src/app/(app)/dashboard/page.tsx | 10 ++++++++-- src/app/api/qrs/route.ts | 11 ++++++++--- src/components/dashboard/StatsGrid.tsx | 11 ++++++----- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/app/(app)/dashboard/page.tsx b/src/app/(app)/dashboard/page.tsx index cf330f9..dc30288 100644 --- a/src/app/(app)/dashboard/page.tsx +++ b/src/app/(app)/dashboard/page.tsx @@ -41,6 +41,7 @@ export default function DashboardPage() { totalScans: 0, activeQRCodes: 0, conversionRate: 0, + uniqueScans: 0, }); const [analyticsData, setAnalyticsData] = useState(null); @@ -218,13 +219,15 @@ export default function DashboardPage() { // Calculate real stats const totalScans = data.reduce((sum: number, qr: QRCodeData) => sum + (qr.scans || 0), 0); const activeQRCodes = data.filter((qr: QRCodeData) => qr.status === 'ACTIVE').length; - // Calculate "Unique Rate" (Conversion) - const conversionRate = totalScans > 0 ? Math.round((data.reduce((acc: number, qr: any) => acc + (qr.uniqueScans || 0), 0) / totalScans) * 100) : 0; + // Calculate unique scans (absolute count) + const uniqueScans = data.reduce((acc: number, qr: any) => acc + (qr.uniqueScans || 0), 0); + const conversionRate = totalScans > 0 ? Math.round((uniqueScans / totalScans) * 100) : 0; setStats({ totalScans, activeQRCodes, conversionRate, + uniqueScans, }); } else { // If not logged in, show zeros @@ -233,6 +236,7 @@ export default function DashboardPage() { totalScans: 0, activeQRCodes: 0, conversionRate: 0, + uniqueScans: 0, }); } @@ -256,6 +260,7 @@ export default function DashboardPage() { totalScans: 0, activeQRCodes: 0, conversionRate: 0, + uniqueScans: 0, }); } finally { setLoading(false); @@ -317,6 +322,7 @@ export default function DashboardPage() { totalScans: 0, activeQRCodes: 0, conversionRate: 0, + uniqueScans: 0, }); showToast(`Successfully deleted ${data.deletedCount} QR code${data.deletedCount !== 1 ? 's' : ''}`, 'success'); } else { diff --git a/src/app/api/qrs/route.ts b/src/app/api/qrs/route.ts index 68bf722..0ef60ac 100644 --- a/src/app/api/qrs/route.ts +++ b/src/app/api/qrs/route.ts @@ -20,6 +20,10 @@ export async function GET(request: NextRequest) { _count: { select: { scans: true }, }, + scans: { + where: { isUnique: true }, + select: { id: true }, + }, }, orderBy: { createdAt: 'desc' }, }); @@ -28,6 +32,7 @@ export async function GET(request: NextRequest) { const transformed = qrCodes.map(qr => ({ ...qr, scans: qr._count.scans, + uniqueScans: qr.scans.length, // Count of scans where isUnique=true _count: undefined, })); @@ -138,9 +143,9 @@ export async function POST(request: NextRequest) { ); } } - + let enrichedContent = body.content; - + // For STATIC QR codes, calculate what the QR should contain if (isStatic) { let qrContent = ''; @@ -180,7 +185,7 @@ END:VCARD`; default: qrContent = body.content.url || 'https://example.com'; } - + // Add qrContent to the content object enrichedContent = { ...body.content, diff --git a/src/components/dashboard/StatsGrid.tsx b/src/components/dashboard/StatsGrid.tsx index 46f7dd8..6ec8404 100644 --- a/src/components/dashboard/StatsGrid.tsx +++ b/src/components/dashboard/StatsGrid.tsx @@ -11,6 +11,7 @@ interface StatsGridProps { totalScans: number; activeQRCodes: number; conversionRate: number; + uniqueScans?: number; }; trends?: { totalScans?: TrendData; @@ -67,13 +68,13 @@ export const StatsGrid: React.FC = ({ stats, trends }) => { ), }, { - title: 'Unique Scan Rate', - value: `${stats.conversionRate}%`, - change: stats.totalScans > 0 ? `${stats.conversionRate}% new users` : 'No scans yet', - changeType: stats.conversionRate > 0 ? 'positive' : 'neutral' as 'positive' | 'negative' | 'neutral', + title: 'Unique Users', + value: formatNumber(stats.uniqueScans ?? 0), + change: stats.totalScans > 0 ? `${stats.uniqueScans ?? 0} unique visitors` : 'No scans yet', + changeType: (stats.uniqueScans ?? 0) > 0 ? 'positive' : 'neutral' as 'positive' | 'negative' | 'neutral', icon: ( - + ), },