Files
accounted/components/settings/BillingActions.tsx
T
Jakob WennbergandClaude Opus 4.8 2da9c71eb3 UI badge cleanup + /chart-of-accounts route + loop skills (#850)
Bundles three separable concerns:

- style(ui): badge audit + cleanup across 45 files — real-status chips use Badge variants (raw Tailwind colors dropped), non-status count/type/label chips demoted to muted text, clustered badges consolidated; 20 unused imports removed.
- feat(bookkeeping): Kontoplan moved to a dedicated /chart-of-accounts route (nav + command palette wired); /bookkeeping shows the journal list only.
- chore(skills): loop-* automation skills + design-scan workflow under .claude/.

fix(reports): restored the destructive count badge on blocking errors in the periodisk sammanställning (EC Sales List) — a genuine status cue the audit had wrongly flattened; flagged by the PR reviewer and Swedish compliance bot, now clean.

All CI green; compliance bots report no findings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 14:29:23 +02:00

110 lines
3.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import { useState, type ReactNode } from 'react'
import { Button } from '@/components/ui/button'
import { useToast } from '@/components/ui/use-toast'
import type { BillingPlan } from '@/lib/stripe/client'
const PRICE: Record<BillingPlan, { amount: string; suffix: string; sub: string; cta: string }> = {
monthly: {
amount: '199 kr',
suffix: '/ mån',
sub: 'Faktureras månadsvis.',
cta: 'Aktivera abonnemang – 199 kr/mån',
},
yearly: {
amount: '166 kr',
suffix: '/ mån',
sub: '1 999 kr/år — du betalar för 10 månader.',
cta: 'Aktivera årsabonnemang – 1 999 kr/år',
},
}
/**
* Interactive billing CTA. Paying companies get the Stripe Customer Portal
* (manage/cancel); everyone else gets a reactive plan picker + Checkout. Both
* POST to a route that returns a hosted Stripe URL we redirect to.
*/
export function BillingActions({ isPaying, configured }: { isPaying: boolean; configured: boolean }) {
const { toast } = useToast()
const [loading, setLoading] = useState(false)
const [plan, setPlan] = useState<BillingPlan>('yearly')
async function go(endpoint: string, payload?: Record<string, unknown>) {
setLoading(true)
try {
const res = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload ?? {}),
})
const data = (await res.json().catch(() => ({}))) as { url?: string; error?: string }
if (!res.ok || !data.url) throw new Error(data.error || 'Något gick fel')
window.location.href = data.url
} catch (e) {
toast({
title: 'Kunde inte öppna betalningen',
description: e instanceof Error ? e.message : undefined,
variant: 'destructive',
})
setLoading(false)
}
}
if (isPaying) {
return (
<Button size="lg" onClick={() => go('/api/billing/portal')} disabled={loading} className="w-full sm:w-auto">
Hantera abonnemang
</Button>
)
}
if (!configured) {
return (
<Button size="lg" disabled className="w-full">
Uppgradering öppnar snart
</Button>
)
}
const segment = (p: BillingPlan, label: ReactNode) => (
<button
type="button"
onClick={() => setPlan(p)}
className={`flex items-center rounded-md px-3 py-2 transition-colors ${
plan === p ? 'bg-secondary text-foreground' : 'text-muted-foreground hover:text-foreground'
}`}
>
{label}
</button>
)
return (
<div className="space-y-4">
<div>
<div className="flex items-baseline gap-2">
<span className="font-display text-3xl tracking-tight tabular-nums">{PRICE[plan].amount}</span>
<span className="text-muted-foreground">{PRICE[plan].suffix}</span>
</div>
<p className="text-sm text-muted-foreground mt-1">{PRICE[plan].sub}</p>
</div>
<div className="inline-flex rounded-lg border border-border p-1 text-sm">
{segment('monthly', 'Månadsvis')}
{segment(
'yearly',
<>
Årsvis
<span className="ml-2 text-xs text-muted-foreground">Spara 2 mån</span>
</>,
)}
</div>
<Button size="lg" onClick={() => go('/api/billing/checkout', { plan })} disabled={loading} className="w-full">
{loading ? 'Öppnar…' : PRICE[plan].cta}
</Button>
<p className="text-xs text-muted-foreground text-center">Säker betalning via Stripe · Avsluta när du vill</p>
</div>
)
}