qrmaster.net

This commit is contained in:
Timo Knuth
2025-12-09 22:22:36 +01:00
parent 424c61a176
commit 8c5e2fa58e
37 changed files with 549 additions and 915 deletions

View File

@@ -5,6 +5,20 @@ import { rateLimit, getClientIdentifier, RateLimits } from '@/lib/rateLimit';
export const dynamic = 'force-dynamic';
// Helper function to calculate trend
function calculateTrend(current: number, previous: number): { trend: 'up' | 'down' | 'flat'; percentage: number } {
if (previous === 0) {
return current > 0 ? { trend: 'up', percentage: 100 } : { trend: 'flat', percentage: 0 };
}
const change = ((current - previous) / previous) * 100;
const percentage = Math.round(Math.abs(change));
if (change > 5) return { trend: 'up', percentage };
if (change < -5) return { trend: 'down', percentage };
return { trend: 'flat', percentage };
}
export async function GET(request: NextRequest) {
try {
const userId = cookies().get('userId')?.value;
@@ -33,17 +47,58 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
// Get user's QR codes
// Get date range from query params (default: last 30 days)
const { searchParams } = request.nextUrl;
const range = searchParams.get('range') || '30';
const daysInRange = parseInt(range, 10);
// Calculate current and previous period dates
const now = new Date();
const currentPeriodStart = new Date();
currentPeriodStart.setDate(now.getDate() - daysInRange);
const previousPeriodEnd = new Date(currentPeriodStart);
const previousPeriodStart = new Date(previousPeriodEnd);
previousPeriodStart.setDate(previousPeriodEnd.getDate() - daysInRange);
// Get user's QR codes with scans filtered by period
const qrCodes = await db.qRCode.findMany({
where: { userId },
include: {
scans: true,
scans: {
where: {
ts: {
gte: currentPeriodStart,
},
},
},
},
});
// Calculate stats
// Get previous period scans for comparison
const qrCodesWithPreviousScans = await db.qRCode.findMany({
where: { userId },
include: {
scans: {
where: {
ts: {
gte: previousPeriodStart,
lt: previousPeriodEnd,
},
},
},
},
});
// Calculate current period stats
const totalScans = qrCodes.reduce((sum, qr) => sum + qr.scans.length, 0);
const uniqueScans = qrCodes.reduce((sum, qr) =>
const uniqueScans = qrCodes.reduce((sum, qr) =>
sum + qr.scans.filter(s => s.isUnique).length, 0
);
// Calculate previous period stats for comparison
const previousTotalScans = qrCodesWithPreviousScans.reduce((sum, qr) => sum + qr.scans.length, 0);
const previousUniqueScans = qrCodesWithPreviousScans.reduce((sum, qr) =>
sum + qr.scans.filter(s => s.isUnique).length, 0
);
@@ -60,44 +115,59 @@ export async function GET(request: NextRequest) {
? Math.round((mobileScans / totalScans) * 100)
: 0;
// Country stats
// Country stats (current period)
const countryStats = qrCodes.flatMap(qr => qr.scans)
.reduce((acc, scan) => {
const country = scan.country || 'Unknown';
acc[country] = (acc[country] || 0) + 1;
return acc;
}, {} as Record<string, number>);
// Country stats (previous period)
const previousCountryStats = qrCodesWithPreviousScans.flatMap(qr => qr.scans)
.reduce((acc, scan) => {
const country = scan.country || 'Unknown';
acc[country] = (acc[country] || 0) + 1;
return acc;
}, {} as Record<string, number>);
const topCountry = Object.entries(countryStats)
.sort(([,a], [,b]) => b - a)[0];
// Time-based stats (last 30 days)
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const recentScans = qrCodes.flatMap(qr => qr.scans)
.filter(scan => new Date(scan.ts) > thirtyDaysAgo);
// Daily scan counts for chart
const dailyScans = recentScans.reduce((acc, scan) => {
// Daily scan counts for chart (current period)
const dailyScans = qrCodes.flatMap(qr => qr.scans).reduce((acc, scan) => {
const date = new Date(scan.ts).toISOString().split('T')[0];
acc[date] = (acc[date] || 0) + 1;
return acc;
}, {} as Record<string, number>);
// QR performance (only show DYNAMIC QR codes since STATIC don't track scans)
const qrPerformance = qrCodes
.filter(qr => qr.type === 'DYNAMIC')
.map(qr => ({
id: qr.id,
title: qr.title,
type: qr.type,
totalScans: qr.scans.length,
uniqueScans: qr.scans.filter(s => s.isUnique).length,
conversion: qr.scans.length > 0
? Math.round((qr.scans.filter(s => s.isUnique).length / qr.scans.length) * 100)
: 0,
}))
.map(qr => {
const currentTotal = qr.scans.length;
const currentUnique = qr.scans.filter(s => s.isUnique).length;
// Find previous period data for this QR code
const previousQR = qrCodesWithPreviousScans.find(prev => prev.id === qr.id);
const previousTotal = previousQR ? previousQR.scans.length : 0;
// Calculate trend
const trendData = calculateTrend(currentTotal, previousTotal);
return {
id: qr.id,
title: qr.title,
type: qr.type,
totalScans: currentTotal,
uniqueScans: currentUnique,
conversion: currentTotal > 0
? Math.round((currentUnique / currentTotal) * 100)
: 0,
trend: trendData.trend,
trendPercentage: trendData.percentage,
};
})
.sort((a, b) => b.totalScans - a.totalScans);
return NextResponse.json({
@@ -117,13 +187,20 @@ export async function GET(request: NextRequest) {
countryStats: Object.entries(countryStats)
.sort(([,a], [,b]) => b - a)
.slice(0, 10)
.map(([country, count]) => ({
country,
count,
percentage: totalScans > 0
? Math.round((count / totalScans) * 100)
: 0,
})),
.map(([country, count]) => {
const previousCount = previousCountryStats[country] || 0;
const trendData = calculateTrend(count, previousCount);
return {
country,
count,
percentage: totalScans > 0
? Math.round((count / totalScans) * 100)
: 0,
trend: trendData.trend,
trendPercentage: trendData.percentage,
};
}),
dailyScans,
qrPerformance: qrPerformance.slice(0, 10),
});