import { format } from 'date-fns'; import { UserX } from 'lucide-react'; import { db } from '@/lib/db'; import { CANCELLATION_REASONS, getCancellationReasonLabel } from '@/lib/cancellation-reasons'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; const RECENT_LIMIT = 15; /** * The answers to the one question asked on the way out, newest first, with an * all-time tally per answer above them. * * Only in-app cancellations appear here. Someone who cancels inside the Stripe * portal, or whose card simply stops working, never sees the question, so the * tally undercounts churn and says nothing about the accounts that pay and go * silent. Read it as "what people said", not "why people leave". */ export async function CancellationReasonsCard() { const [recent, tally] = await Promise.all([ db.subscriptionCancellation.findMany({ orderBy: { createdAt: 'desc' }, take: RECENT_LIMIT, select: { id: true, reason: true, note: true, periodEnd: true, createdAt: true, user: { select: { name: true, email: true } }, }, }), db.subscriptionCancellation.groupBy({ by: ['reason'], _count: { _all: true }, }), ]); const countByReason = new Map(tally.map((row) => [row.reason, row._count._all])); const total = tally.reduce((sum, row) => sum + row._count._all, 0); const skipped = countByReason.get(null) ?? 0; return ( Why people cancelled {total === 0 ? (

No in-app cancellations yet. Cancellations made in the Stripe portal do not show up here.

) : ( <>
{CANCELLATION_REASONS.map((entry) => ( {entry.label}:{' '} {countByReason.get(entry.value) ?? 0} ))} Skipped the question: {skipped}
{total > RECENT_LIMIT ? (

Showing the latest {RECENT_LIMIT} of {total}.

) : null} )}
); }