From 8ef5221f71ccd5bfbe9b90697b99c95c20eb6dc9 Mon Sep 17 00:00:00 2001 From: Timo Knuth Date: Fri, 14 Aug 2026 13:08:40 +0200 Subject: [PATCH] Show total scans in social milestones --- scripts/social-worker/worker.py | 2 ++ .../api/social-milestones/[id]/route.ts | 1 + src/app/(main)/api/social-milestones/route.ts | 26 ++++++++++++------- .../dashboard/SocialMilestoneDialog.tsx | 4 +-- src/lib/social-milestones-server.ts | 20 +++++++------- src/lib/social-milestones.ts | 3 +++ 6 files changed, 35 insertions(+), 21 deletions(-) diff --git a/scripts/social-worker/worker.py b/scripts/social-worker/worker.py index cd10712..d01b282 100644 --- a/scripts/social-worker/worker.py +++ b/scripts/social-worker/worker.py @@ -59,8 +59,10 @@ def render_card(card): draw.text((logo_x, 70), "QR MASTER", font=medium, fill=navy) total = int(card.get("totalUniqueScans") or card.get("threshold") or 0) + total_scans = int(card.get("totalScans") or total) draw.text((66, 212), f"{total:,}", font=display, fill=navy) draw.text((73, 374), "UNIQUE SCANS", font=medium, fill=navy) + draw.text((74, 410), f"{total_scans:,} total scans", font=font("DejaVuSans.ttf", 20), fill=slate) draw.line((68, 548, 108, 548), fill=blue, width=4) draw.text((126, 530), "QR code milestone", font=regular, fill=slate) diff --git a/src/app/(main)/api/social-milestones/[id]/route.ts b/src/app/(main)/api/social-milestones/[id]/route.ts index 225bc33..f47b4b8 100644 --- a/src/app/(main)/api/social-milestones/[id]/route.ts +++ b/src/app/(main)/api/social-milestones/[id]/route.ts @@ -69,6 +69,7 @@ export async function PATCH(request: NextRequest, { params }: { params: { id: st const card = milestone.cardData || buildMilestoneCardSnapshot({ primaryUseCase: milestone.user.primaryUseCase, qrTitle: milestone.qr.title, + totalScans: threshold, totalUniqueScans: threshold, milestoneThreshold: threshold, reachedAt: milestone.detectedAt, diff --git a/src/app/(main)/api/social-milestones/route.ts b/src/app/(main)/api/social-milestones/route.ts index f4f4df1..ee86269 100644 --- a/src/app/(main)/api/social-milestones/route.ts +++ b/src/app/(main)/api/social-milestones/route.ts @@ -18,7 +18,7 @@ export async function GET(request: NextRequest) { const milestone = await db.socialMilestone.findFirst({ where: { userId, status: { in: ['detected', 'shown'] } }, orderBy: { detectedAt: 'asc' }, - include: { qr: { select: { title: true } } }, + include: { qr: { select: { id: true, title: true } } }, }); if (!milestone) return NextResponse.json({ milestone: null }); @@ -28,6 +28,20 @@ export async function GET(request: NextRequest) { const threshold = milestoneThreshold(milestone.kind); if (!threshold) return NextResponse.json({ milestone: null }); const locale = socialLocale(request.nextUrl.searchParams.get('locale')); + const allScanCount = await db.qRScan.count({ where: { qrId: milestone.qr.id } }); + const storedCard = milestone.cardData as Record | null; + const card = storedCard + ? { ...storedCard, totalScans: typeof storedCard.totalScans === 'number' ? storedCard.totalScans : allScanCount } + : buildMilestoneCardSnapshot({ + primaryUseCase: user.primaryUseCase, + qrTitle: milestone.qr.title, + totalScans: allScanCount, + totalUniqueScans: threshold, + milestoneThreshold: threshold, + reachedAt: milestone.detectedAt, + trend: null, + locale, + }); return NextResponse.json({ milestone: { @@ -44,15 +58,7 @@ export async function GET(request: NextRequest) { locale, milestone.qr.title, ), - card: milestone.cardData || buildMilestoneCardSnapshot({ - primaryUseCase: user.primaryUseCase, - qrTitle: milestone.qr.title, - totalUniqueScans: threshold, - milestoneThreshold: threshold, - reachedAt: milestone.detectedAt, - trend: null, - locale, - }), + card, }, }); } diff --git a/src/components/dashboard/SocialMilestoneDialog.tsx b/src/components/dashboard/SocialMilestoneDialog.tsx index fb1b755..183953c 100644 --- a/src/components/dashboard/SocialMilestoneDialog.tsx +++ b/src/components/dashboard/SocialMilestoneDialog.tsx @@ -8,7 +8,7 @@ import { useCsrf } from '@/hooks/useCsrf'; import { useTranslation } from '@/hooks/useTranslation'; import { showToast } from '@/components/ui/Toast'; -type Card = { language: 'en' | 'de'; qrTitle: string; label: string; title: string; totalUniqueScans: number; milestoneThreshold: number; trend: { points: Array<{ at: string; total: number }>; startLabel: string; endLabel: string; target: number; ceiling: number } | null }; +type Card = { language: 'en' | 'de'; qrTitle: string; label: string; title: string; totalScans?: number; totalUniqueScans: number; milestoneThreshold: number; trend: { points: Array<{ at: string; total: number }>; startLabel: string; endLabel: string; target: number; ceiling: number } | null }; type Milestone = { id: string; qrTitle: string; threshold: number; defaultXHandle: string | null; preview: string; language: 'en' | 'de'; card: Card; brandStatus: string; brandPostUrl: string | null; brandPostError: string | null }; type BrandState = { brandStatus: string; brandPostUrl: string | null; brandPostError: string | null; selfSharedAt: string | null }; @@ -112,7 +112,7 @@ export function SocialMilestoneDialog() {
QR MASTERVerified scan milestone
-
TOTAL UNIQUE SCANS
{count.toLocaleString(milestone.language === 'de' ? 'de-DE' : 'en-US')}
{milestone.language === 'de' ? 'eindeutige Scans' : 'unique scans'}
+
TOTAL UNIQUE SCANS
{count.toLocaleString(milestone.language === 'de' ? 'de-DE' : 'en-US')}
{milestone.language === 'de' ? 'eindeutige Scans' : 'unique scans'}
{(card.totalScans || count).toLocaleString(milestone.language === 'de' ? 'de-DE' : 'en-US')} {milestone.language === 'de' ? 'Scans insgesamt' : 'total scans'}
{card.trend ?
{milestone.language === 'de' ? 'Kumulierte eindeutige Scans seit Erstellung' : 'Cumulative unique scans since creation'}
:
{milestone.language === 'de' ? 'Erste Dynamik' : 'Early momentum'}
}
{card.qrTitle}
diff --git a/src/lib/social-milestones-server.ts b/src/lib/social-milestones-server.ts index fdb0e92..dc59dc7 100644 --- a/src/lib/social-milestones-server.ts +++ b/src/lib/social-milestones-server.ts @@ -46,35 +46,37 @@ export async function detectSocialMilestones(qrId?: string) { async function createCardSnapshot(qr: { id: string; title: string; createdAt: Date; user: { primaryUseCase: string | null } }) { const now = new Date(); const scans = await db.qRScan.findMany({ - where: { qrId: qr.id, isUnique: true }, - select: { ts: true }, + where: { qrId: qr.id }, + select: { ts: true, isUnique: true }, orderBy: { ts: 'asc' }, }); + const uniqueScans = scans.filter(scan => scan.isUnique); // Keep a representative, cumulative history in the immutable snapshot. // It starts at QR creation and ends at the moment the milestone is detected. - const stride = Math.max(1, Math.ceil(scans.length / 24)); + const stride = Math.max(1, Math.ceil(uniqueScans.length / 24)); const points = [{ at: qr.createdAt.toISOString(), total: 0 }]; - scans.forEach((scan, index) => { + uniqueScans.forEach((scan, index) => { const total = index + 1; - if (total % stride === 0 || total === scans.length) points.push({ at: scan.ts.toISOString(), total }); + if (total % stride === 0 || total === uniqueScans.length) points.push({ at: scan.ts.toISOString(), total }); }); const month = new Intl.DateTimeFormat('en', { month: 'short', year: '2-digit', timeZone: 'UTC' }); const trend = { points, startLabel: month.format(qr.createdAt), endLabel: month.format(now), - target: scans.length, + target: uniqueScans.length, // Five labelled grid lines: 1/4, 1/2, 3/4, target, then one level above. // For 20 scans this is precisely 5, 10, 15, 20, 25. - ceiling: Math.max(1.25, scans.length * 1.25), + ceiling: uniqueScans.length < 5 ? 5 : Math.max(1.25, uniqueScans.length * 1.25), }; // The snapshot is made at detection time and never silently changes after consent. return buildMilestoneCardSnapshot({ primaryUseCase: qr.user.primaryUseCase, qrTitle: qr.title, - totalUniqueScans: scans.length, - milestoneThreshold: scans.length, + totalScans: scans.length, + totalUniqueScans: uniqueScans.length, + milestoneThreshold: uniqueScans.length, reachedAt: now, trend, locale: 'en' as SocialLocale, diff --git a/src/lib/social-milestones.ts b/src/lib/social-milestones.ts index e670788..60565ad 100644 --- a/src/lib/social-milestones.ts +++ b/src/lib/social-milestones.ts @@ -45,6 +45,7 @@ export type SocialMilestoneCard = { qrTitle: string; label: string; title: string; + totalScans: number; totalUniqueScans: number; milestoneThreshold: number; reachedAt: string; @@ -93,6 +94,7 @@ export function buildMilestonePostForQr(primaryUseCase: string | null, totalUniq export function buildMilestoneCardSnapshot(input: { primaryUseCase: string | null; qrTitle: string; + totalScans: number; totalUniqueScans: number; milestoneThreshold: number; reachedAt: Date; @@ -105,6 +107,7 @@ export function buildMilestoneCardSnapshot(input: { qrTitle: input.qrTitle, label: usageLabel(input.primaryUseCase, input.locale), title: input.locale === 'de' ? 'Erfolgsmeilenstein' : 'Success milestone', + totalScans: input.totalScans, totalUniqueScans: input.totalUniqueScans, milestoneThreshold: input.milestoneThreshold, reachedAt: input.reachedAt.toISOString(),