Pro/business

This commit is contained in:
Timo Knuth
2026-07-06 00:18:06 +02:00
parent 0b9c8d2a8f
commit 91bd940edc
10 changed files with 384 additions and 40 deletions

View File

@@ -17,6 +17,7 @@ import {
import {
BarChart3,
Copy,
CreditCard,
Download,
Filter,
Loader2,
@@ -68,6 +69,12 @@ type SegmentRow = {
qrCount: number;
dynamicQrCount: number;
scanCount: number;
lifecycleLogs: Array<{
fromStage: string | null;
toStage: string;
reason: string | null;
createdAt: string;
}>;
};
type DashboardData = {
@@ -76,6 +83,7 @@ type DashboardData = {
mismatchCount: number;
activatedUsers: number;
paidUsers: number;
recentBillingEvents: number;
};
acquisition: {
bySource: Array<{
@@ -119,6 +127,16 @@ type DashboardData = {
byTeamSize: Array<any>;
};
lifecycleSummary: Record<string, number>;
recentBillingActivity: Array<{
userId: string;
name: string | null;
email: string;
plan: string;
reason: string | null;
fromStage: string | null;
toStage: string;
createdAt: string;
}>;
campaignSourceQuality: Array<any>;
upgradeCandidates: Array<SegmentRow>;
filterOptions: {
@@ -184,6 +202,27 @@ function buildQuery(filters: Filters) {
return params.toString();
}
function formatStage(value: string | null) {
return value ? value.replace(/_/g, ' ') : 'none';
}
function formatBillingReason(value: string | null) {
switch (value) {
case 'subscription_created':
return 'Started paid plan';
case 'subscription_updated':
return 'Updated subscription';
case 'subscription_canceled_at_period_end':
return 'Cancels at period end';
case 'subscription_deleted':
return 'Ended subscription';
case 'subscription_synced':
return 'Synced subscription';
default:
return 'Subscription changed';
}
}
function LifecycleCard({
label,
value,
@@ -475,6 +514,50 @@ export default function NewsletterClient() {
</Card>
</div>
<Card className="mb-8">
<CardHeader>
<div className="flex items-center gap-3">
<CreditCard className="h-5 w-5 text-slate-500" />
<CardTitle>Recent Billing Activity</CardTitle>
</div>
</CardHeader>
<CardContent>
{data.recentBillingActivity.length > 0 ? (
<Table>
<TableHeader>
<TableRow>
<TableHead>User</TableHead>
<TableHead>Plan</TableHead>
<TableHead>Action</TableHead>
<TableHead>Lifecycle change</TableHead>
<TableHead>Date</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.recentBillingActivity.map((event) => (
<TableRow key={`${event.userId}-${event.createdAt}`}>
<TableCell>
<div>
<div className="font-medium text-slate-900">{event.name || event.email}</div>
<div className="text-xs text-slate-500">{event.email}</div>
</div>
</TableCell>
<TableCell>{event.plan}</TableCell>
<TableCell>{formatBillingReason(event.reason)}</TableCell>
<TableCell>
{formatStage(event.fromStage)} -&gt; {formatStage(event.toStage)}
</TableCell>
<TableCell>{new Date(event.createdAt).toLocaleString()}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
) : (
<p className="text-sm text-slate-500">No billing activity logged yet.</p>
)}
</CardContent>
</Card>
<div className="mb-8 grid gap-6 xl:grid-cols-[1.4fr_1fr]">
<Card>
<CardHeader>

View File

@@ -77,7 +77,7 @@ export default function PricingPage() {
const handleDowngrade = async () => {
// Show confirmation dialog
const confirmed = window.confirm(
'Are you sure you want to downgrade to the Free plan? Your subscription will be canceled immediately and you will lose access to premium features.'
'Are you sure you want to cancel your paid plan? You will keep premium features until the end of your current billing period.'
);
if (!confirmed) {
@@ -99,7 +99,7 @@ export default function PricingPage() {
throw new Error(error.error || 'Failed to cancel subscription');
}
showToast('Successfully downgraded to Free plan', 'success');
showToast('Subscription will end at the end of your current billing period.', 'success');
// Refresh to update the plan
setTimeout(() => {
@@ -148,7 +148,7 @@ export default function PricingPage() {
'Standard QR design templates',
'Download as SVG/PNG',
],
buttonText: currentPlan === 'FREE' ? 'Current Plan' : 'Downgrade to Free',
buttonText: currentPlan === 'FREE' ? 'Current Plan' : 'Cancel paid plan',
buttonVariant: 'outline' as const,
disabled: currentPlan === 'FREE',
popular: false,