feat: implement dashboard page for QR code listing, statistics, and management.
This commit is contained in:
parent
49673e84b6
commit
eea8c8b33a
|
|
@ -41,6 +41,7 @@ export default function DashboardPage() {
|
||||||
totalScans: 0,
|
totalScans: 0,
|
||||||
activeQRCodes: 0,
|
activeQRCodes: 0,
|
||||||
conversionRate: 0,
|
conversionRate: 0,
|
||||||
|
uniqueScans: 0,
|
||||||
});
|
});
|
||||||
const [analyticsData, setAnalyticsData] = useState<any>(null);
|
const [analyticsData, setAnalyticsData] = useState<any>(null);
|
||||||
|
|
||||||
|
|
@ -218,13 +219,15 @@ export default function DashboardPage() {
|
||||||
// Calculate real stats
|
// Calculate real stats
|
||||||
const totalScans = data.reduce((sum: number, qr: QRCodeData) => sum + (qr.scans || 0), 0);
|
const totalScans = data.reduce((sum: number, qr: QRCodeData) => sum + (qr.scans || 0), 0);
|
||||||
const activeQRCodes = data.filter((qr: QRCodeData) => qr.status === 'ACTIVE').length;
|
const activeQRCodes = data.filter((qr: QRCodeData) => qr.status === 'ACTIVE').length;
|
||||||
// Calculate "Unique Rate" (Conversion)
|
// Calculate unique scans (absolute count)
|
||||||
const conversionRate = totalScans > 0 ? Math.round((data.reduce((acc: number, qr: any) => acc + (qr.uniqueScans || 0), 0) / totalScans) * 100) : 0;
|
const uniqueScans = data.reduce((acc: number, qr: any) => acc + (qr.uniqueScans || 0), 0);
|
||||||
|
const conversionRate = totalScans > 0 ? Math.round((uniqueScans / totalScans) * 100) : 0;
|
||||||
|
|
||||||
setStats({
|
setStats({
|
||||||
totalScans,
|
totalScans,
|
||||||
activeQRCodes,
|
activeQRCodes,
|
||||||
conversionRate,
|
conversionRate,
|
||||||
|
uniqueScans,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// If not logged in, show zeros
|
// If not logged in, show zeros
|
||||||
|
|
@ -233,6 +236,7 @@ export default function DashboardPage() {
|
||||||
totalScans: 0,
|
totalScans: 0,
|
||||||
activeQRCodes: 0,
|
activeQRCodes: 0,
|
||||||
conversionRate: 0,
|
conversionRate: 0,
|
||||||
|
uniqueScans: 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -256,6 +260,7 @@ export default function DashboardPage() {
|
||||||
totalScans: 0,
|
totalScans: 0,
|
||||||
activeQRCodes: 0,
|
activeQRCodes: 0,
|
||||||
conversionRate: 0,
|
conversionRate: 0,
|
||||||
|
uniqueScans: 0,
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
@ -317,6 +322,7 @@ export default function DashboardPage() {
|
||||||
totalScans: 0,
|
totalScans: 0,
|
||||||
activeQRCodes: 0,
|
activeQRCodes: 0,
|
||||||
conversionRate: 0,
|
conversionRate: 0,
|
||||||
|
uniqueScans: 0,
|
||||||
});
|
});
|
||||||
showToast(`Successfully deleted ${data.deletedCount} QR code${data.deletedCount !== 1 ? 's' : ''}`, 'success');
|
showToast(`Successfully deleted ${data.deletedCount} QR code${data.deletedCount !== 1 ? 's' : ''}`, 'success');
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,10 @@ export async function GET(request: NextRequest) {
|
||||||
_count: {
|
_count: {
|
||||||
select: { scans: true },
|
select: { scans: true },
|
||||||
},
|
},
|
||||||
|
scans: {
|
||||||
|
where: { isUnique: true },
|
||||||
|
select: { id: true },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
orderBy: { createdAt: 'desc' },
|
orderBy: { createdAt: 'desc' },
|
||||||
});
|
});
|
||||||
|
|
@ -28,6 +32,7 @@ export async function GET(request: NextRequest) {
|
||||||
const transformed = qrCodes.map(qr => ({
|
const transformed = qrCodes.map(qr => ({
|
||||||
...qr,
|
...qr,
|
||||||
scans: qr._count.scans,
|
scans: qr._count.scans,
|
||||||
|
uniqueScans: qr.scans.length, // Count of scans where isUnique=true
|
||||||
_count: undefined,
|
_count: undefined,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
@ -138,9 +143,9 @@ export async function POST(request: NextRequest) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let enrichedContent = body.content;
|
let enrichedContent = body.content;
|
||||||
|
|
||||||
// For STATIC QR codes, calculate what the QR should contain
|
// For STATIC QR codes, calculate what the QR should contain
|
||||||
if (isStatic) {
|
if (isStatic) {
|
||||||
let qrContent = '';
|
let qrContent = '';
|
||||||
|
|
@ -180,7 +185,7 @@ END:VCARD`;
|
||||||
default:
|
default:
|
||||||
qrContent = body.content.url || 'https://example.com';
|
qrContent = body.content.url || 'https://example.com';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add qrContent to the content object
|
// Add qrContent to the content object
|
||||||
enrichedContent = {
|
enrichedContent = {
|
||||||
...body.content,
|
...body.content,
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ interface StatsGridProps {
|
||||||
totalScans: number;
|
totalScans: number;
|
||||||
activeQRCodes: number;
|
activeQRCodes: number;
|
||||||
conversionRate: number;
|
conversionRate: number;
|
||||||
|
uniqueScans?: number;
|
||||||
};
|
};
|
||||||
trends?: {
|
trends?: {
|
||||||
totalScans?: TrendData;
|
totalScans?: TrendData;
|
||||||
|
|
@ -67,13 +68,13 @@ export const StatsGrid: React.FC<StatsGridProps> = ({ stats, trends }) => {
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Unique Scan Rate',
|
title: 'Unique Users',
|
||||||
value: `${stats.conversionRate}%`,
|
value: formatNumber(stats.uniqueScans ?? 0),
|
||||||
change: stats.totalScans > 0 ? `${stats.conversionRate}% new users` : 'No scans yet',
|
change: stats.totalScans > 0 ? `${stats.uniqueScans ?? 0} unique visitors` : 'No scans yet',
|
||||||
changeType: stats.conversionRate > 0 ? 'positive' : 'neutral' as 'positive' | 'negative' | 'neutral',
|
changeType: (stats.uniqueScans ?? 0) > 0 ? 'positive' : 'neutral' as 'positive' | 'negative' | 'neutral',
|
||||||
icon: (
|
icon: (
|
||||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue