fix signin signout and apply the new style
This commit is contained in:
@@ -0,0 +1,543 @@
|
||||
'use client'
|
||||
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { formatCurrency, PLAN_PRICES } from '@rentaldrivego/types'
|
||||
import { EMPLOYEE_PROFILE_KEY, apiFetch } from '@/lib/api'
|
||||
import { useDashboardI18n } from '@/components/I18nProvider'
|
||||
|
||||
type Plan = 'STARTER' | 'GROWTH' | 'PRO'
|
||||
type BillingPeriod = 'MONTHLY' | 'ANNUAL'
|
||||
type Currency = 'MAD' | 'USD' | 'EUR'
|
||||
|
||||
interface Subscription {
|
||||
id: string
|
||||
plan: Plan
|
||||
billingPeriod: BillingPeriod
|
||||
status: string
|
||||
currency: Currency
|
||||
trialEndAt: string | null
|
||||
currentPeriodEnd: string | null
|
||||
cancelAtPeriodEnd: boolean
|
||||
}
|
||||
|
||||
interface Invoice {
|
||||
id: string
|
||||
amount: number
|
||||
currency: Currency
|
||||
status: string
|
||||
paymentProvider: string
|
||||
paidAt: string | null
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
interface ProviderAvailability {
|
||||
amanpay: boolean
|
||||
paypal: boolean
|
||||
}
|
||||
|
||||
interface EmployeeProfile {
|
||||
role?: string
|
||||
}
|
||||
|
||||
const STATUS_BADGE: Record<string, string> = {
|
||||
TRIALING: 'bg-sky-100 text-sky-700',
|
||||
ACTIVE: 'bg-green-100 text-green-700',
|
||||
PAST_DUE: 'bg-orange-100 text-orange-700',
|
||||
CANCELLED: 'bg-slate-100 text-slate-600',
|
||||
UNPAID: 'bg-red-100 text-red-700',
|
||||
}
|
||||
|
||||
const INVOICE_STATUS: Record<string, string> = {
|
||||
PAID: 'bg-green-100 text-green-700',
|
||||
PENDING: 'bg-orange-100 text-orange-700',
|
||||
FAILED: 'bg-red-100 text-red-700',
|
||||
REFUNDED: 'bg-slate-100 text-slate-600',
|
||||
}
|
||||
|
||||
const PLANS: Plan[] = ['STARTER', 'GROWTH', 'PRO']
|
||||
export default function SubscriptionPage() {
|
||||
const router = useRouter()
|
||||
const { language } = useDashboardI18n()
|
||||
const [subscription, setSubscription] = useState<Subscription | null>(null)
|
||||
const [invoices, setInvoices] = useState<Invoice[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [canViewPage, setCanViewPage] = useState<boolean | null>(null)
|
||||
|
||||
const [selectedPlan, setSelectedPlan] = useState<Plan>('STARTER')
|
||||
const [billingPeriod, setBillingPeriod] = useState<BillingPeriod>('MONTHLY')
|
||||
const [currency, setCurrency] = useState<Currency>('MAD')
|
||||
const [provider, setProvider] = useState<'AMANPAY' | 'PAYPAL'>('AMANPAY')
|
||||
const [providerAvailability, setProviderAvailability] = useState<ProviderAvailability>({ amanpay: false, paypal: false })
|
||||
const [paying, setPaying] = useState(false)
|
||||
const [cancelling, setCancelling] = useState(false)
|
||||
const copy = {
|
||||
en: {
|
||||
title: 'Subscription',
|
||||
subtitle: 'Manage your plan, payment provider, and subscription invoices.',
|
||||
trial: 'Free trial',
|
||||
remaining: 'remaining. Subscribe before it ends to keep access.',
|
||||
currentPlan: 'Current plan',
|
||||
renews: 'renews',
|
||||
cancelScheduled: 'Cancellation scheduled at end of billing period.',
|
||||
undo: 'Undo',
|
||||
cancelling: 'Cancelling…',
|
||||
cancelPlan: 'Cancel plan',
|
||||
changePlan: 'Change plan',
|
||||
subscribe: 'Subscribe',
|
||||
selectPlan: 'Select a plan and payment provider to proceed.',
|
||||
monthly: 'Monthly',
|
||||
annual: 'Annual (save ~17%)',
|
||||
active: 'Active',
|
||||
perMonthShort: 'mo',
|
||||
perYearShort: 'yr',
|
||||
paymentProvider: 'Payment provider',
|
||||
total: 'Total',
|
||||
perMonth: 'month',
|
||||
perYear: 'year',
|
||||
redirecting: 'Redirecting…',
|
||||
subscribeNow: 'Subscribe now',
|
||||
invoiceHistory: 'Invoice history',
|
||||
date: 'Date',
|
||||
provider: 'Provider',
|
||||
status: 'Status',
|
||||
paid: 'Paid',
|
||||
amount: 'Amount',
|
||||
loading: 'Loading…',
|
||||
noInvoices: 'No invoices yet.',
|
||||
noProviderConfigured: 'No payment provider is configured. Contact support to enable AmanPay or PayPal.',
|
||||
providerUnavailable: 'This payment provider is not configured.',
|
||||
statusLabels: { TRIALING: 'Trialing', ACTIVE: 'Active', PAST_DUE: 'Past due', CANCELLED: 'Cancelled', UNPAID: 'Unpaid' } as Record<string, string>,
|
||||
invoiceStatusLabels: { PAID: 'Paid', PENDING: 'Pending', FAILED: 'Failed', REFUNDED: 'Refunded' } as Record<string, string>,
|
||||
planFeatures: {
|
||||
STARTER: ['Up to 10 vehicles', '1 user seat', 'Basic analytics', 'Marketplace listing'],
|
||||
GROWTH: ['Up to 50 vehicles', '5 user seats', 'Full analytics', 'Priority listing', 'Custom branding'],
|
||||
PRO: ['Unlimited vehicles', 'Unlimited seats', 'Advanced reports', 'API access', 'Dedicated support'],
|
||||
} as Record<Plan, string[]>,
|
||||
},
|
||||
fr: {
|
||||
title: 'Abonnement',
|
||||
subtitle: 'Gérez votre plan, le prestataire de paiement et les factures d’abonnement.',
|
||||
trial: 'Essai gratuit',
|
||||
remaining: 'restants. Abonnez-vous avant la fin pour garder l’accès.',
|
||||
currentPlan: 'Plan actuel',
|
||||
renews: 'renouvelle le',
|
||||
cancelScheduled: 'Annulation programmée à la fin de la période.',
|
||||
undo: 'Annuler',
|
||||
cancelling: 'Annulation…',
|
||||
cancelPlan: 'Annuler le plan',
|
||||
changePlan: 'Changer de plan',
|
||||
subscribe: 'S’abonner',
|
||||
selectPlan: 'Sélectionnez un plan et un prestataire de paiement.',
|
||||
monthly: 'Mensuel',
|
||||
annual: 'Annuel (économie ~17%)',
|
||||
active: 'Actif',
|
||||
perMonthShort: 'mois',
|
||||
perYearShort: 'an',
|
||||
paymentProvider: 'Prestataire de paiement',
|
||||
total: 'Total',
|
||||
perMonth: 'mois',
|
||||
perYear: 'an',
|
||||
redirecting: 'Redirection…',
|
||||
subscribeNow: 'S’abonner maintenant',
|
||||
invoiceHistory: 'Historique des factures',
|
||||
date: 'Date',
|
||||
provider: 'Prestataire',
|
||||
status: 'Statut',
|
||||
paid: 'Payé',
|
||||
amount: 'Montant',
|
||||
loading: 'Chargement…',
|
||||
noInvoices: 'Aucune facture pour le moment.',
|
||||
noProviderConfigured: 'Aucun prestataire de paiement n’est configuré. Contactez le support pour activer AmanPay ou PayPal.',
|
||||
providerUnavailable: 'Ce prestataire de paiement n’est pas configuré.',
|
||||
statusLabels: { TRIALING: 'Essai', ACTIVE: 'Actif', PAST_DUE: 'En retard', CANCELLED: 'Annulé', UNPAID: 'Impayé' } as Record<string, string>,
|
||||
invoiceStatusLabels: { PAID: 'Payé', PENDING: 'En attente', FAILED: 'Échec', REFUNDED: 'Remboursé' } as Record<string, string>,
|
||||
planFeatures: {
|
||||
STARTER: ['Jusqu’à 10 véhicules', '1 utilisateur', 'Analyses de base', 'Présence marketplace'],
|
||||
GROWTH: ['Jusqu’à 50 véhicules', '5 utilisateurs', 'Analyses complètes', 'Mise en avant prioritaire', 'Personnalisation'],
|
||||
PRO: ['Véhicules illimités', 'Utilisateurs illimités', 'Rapports avancés', 'Accès API', 'Support dédié'],
|
||||
} as Record<Plan, string[]>,
|
||||
},
|
||||
ar: {
|
||||
title: 'الاشتراك',
|
||||
subtitle: 'إدارة الخطة ومزوّد الدفع وفواتير الاشتراك.',
|
||||
trial: 'تجربة مجانية',
|
||||
remaining: 'متبقية. اشترك قبل انتهائها للحفاظ على الوصول.',
|
||||
currentPlan: 'الخطة الحالية',
|
||||
renews: 'يتجدد في',
|
||||
cancelScheduled: 'تمت جدولة الإلغاء عند نهاية فترة الفوترة.',
|
||||
undo: 'تراجع',
|
||||
cancelling: 'جارٍ الإلغاء…',
|
||||
cancelPlan: 'إلغاء الخطة',
|
||||
changePlan: 'تغيير الخطة',
|
||||
subscribe: 'اشتراك',
|
||||
selectPlan: 'اختر خطة ومزوّد دفع للمتابعة.',
|
||||
monthly: 'شهري',
|
||||
annual: 'سنوي (توفير ~17%)',
|
||||
active: 'نشط',
|
||||
perMonthShort: 'شهر',
|
||||
perYearShort: 'سنة',
|
||||
paymentProvider: 'مزوّد الدفع',
|
||||
total: 'الإجمالي',
|
||||
perMonth: 'شهر',
|
||||
perYear: 'سنة',
|
||||
redirecting: 'جارٍ التحويل…',
|
||||
subscribeNow: 'اشترك الآن',
|
||||
invoiceHistory: 'سجل الفواتير',
|
||||
date: 'التاريخ',
|
||||
provider: 'المزوّد',
|
||||
status: 'الحالة',
|
||||
paid: 'مدفوع',
|
||||
amount: 'المبلغ',
|
||||
loading: 'جارٍ التحميل…',
|
||||
noInvoices: 'لا توجد فواتير حتى الآن.',
|
||||
noProviderConfigured: 'لا يوجد مزوّد دفع مهيأ. تواصل مع الدعم لتفعيل AmanPay أو PayPal.',
|
||||
providerUnavailable: 'مزوّد الدفع هذا غير مهيأ.',
|
||||
statusLabels: { TRIALING: 'تجريبي', ACTIVE: 'نشط', PAST_DUE: 'متأخر', CANCELLED: 'ملغى', UNPAID: 'غير مدفوع' } as Record<string, string>,
|
||||
invoiceStatusLabels: { PAID: 'مدفوع', PENDING: 'قيد الانتظار', FAILED: 'فشل', REFUNDED: 'مسترد' } as Record<string, string>,
|
||||
planFeatures: {
|
||||
STARTER: ['حتى 10 مركبات', 'مستخدم واحد', 'تحليلات أساسية', 'إدراج في السوق'],
|
||||
GROWTH: ['حتى 50 مركبة', '5 مستخدمين', 'تحليلات كاملة', 'إدراج ذو أولوية', 'تخصيص العلامة'],
|
||||
PRO: ['مركبات غير محدودة', 'مقاعد غير محدودة', 'تقارير متقدمة', 'وصول API', 'دعم مخصص'],
|
||||
} as Record<Plan, string[]>,
|
||||
},
|
||||
}[language]
|
||||
|
||||
useEffect(() => {
|
||||
const cached = window.localStorage.getItem(EMPLOYEE_PROFILE_KEY)
|
||||
if (cached) {
|
||||
try {
|
||||
const profile = JSON.parse(cached) as EmployeeProfile
|
||||
const allowed = profile.role === 'OWNER'
|
||||
setCanViewPage(allowed)
|
||||
if (!allowed) router.replace('/')
|
||||
return
|
||||
} catch {}
|
||||
}
|
||||
|
||||
apiFetch<{ employee: EmployeeProfile }>('/auth/employee/me')
|
||||
.then(({ employee }) => {
|
||||
window.localStorage.setItem(EMPLOYEE_PROFILE_KEY, JSON.stringify(employee))
|
||||
const allowed = employee.role === 'OWNER'
|
||||
setCanViewPage(allowed)
|
||||
if (!allowed) router.replace('/')
|
||||
})
|
||||
.catch(() => {
|
||||
setCanViewPage(false)
|
||||
router.replace('/')
|
||||
})
|
||||
}, [router])
|
||||
|
||||
useEffect(() => {
|
||||
if (canViewPage !== true) return
|
||||
|
||||
Promise.all([
|
||||
apiFetch<Subscription | null>('/subscriptions/me'),
|
||||
apiFetch<Invoice[]>('/subscriptions/invoices'),
|
||||
apiFetch<ProviderAvailability>('/subscriptions/providers'),
|
||||
])
|
||||
.then(([sub, inv, availability]) => {
|
||||
setProviderAvailability(availability)
|
||||
if (availability.amanpay) setProvider('AMANPAY')
|
||||
else if (availability.paypal) setProvider('PAYPAL')
|
||||
if (sub) {
|
||||
setSubscription(sub)
|
||||
setSelectedPlan(sub.plan)
|
||||
setBillingPeriod(sub.billingPeriod)
|
||||
setCurrency(sub.currency as Currency)
|
||||
}
|
||||
setInvoices(inv ?? [])
|
||||
})
|
||||
.catch((err) => setError(err.message))
|
||||
.finally(() => setLoading(false))
|
||||
}, [canViewPage])
|
||||
|
||||
if (canViewPage !== true) {
|
||||
return null
|
||||
}
|
||||
|
||||
async function handleCheckout() {
|
||||
setPaying(true)
|
||||
setError(null)
|
||||
try {
|
||||
if (provider === 'AMANPAY' && !providerAvailability.amanpay) throw new Error(copy.providerUnavailable)
|
||||
if (provider === 'PAYPAL' && !providerAvailability.paypal) throw new Error(copy.providerUnavailable)
|
||||
const currentUrl = new URL(window.location.href)
|
||||
currentUrl.search = ''
|
||||
currentUrl.hash = ''
|
||||
const result = await apiFetch<{ checkoutUrl: string }>('/subscriptions/checkout', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
plan: selectedPlan,
|
||||
billingPeriod,
|
||||
currency,
|
||||
provider,
|
||||
successUrl: `${currentUrl.toString()}?payment=success`,
|
||||
failureUrl: `${currentUrl.toString()}?payment=failed`,
|
||||
}),
|
||||
})
|
||||
window.location.href = result.checkoutUrl
|
||||
} catch (err: any) {
|
||||
setError(err.message)
|
||||
setPaying(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCancel() {
|
||||
setCancelling(true)
|
||||
setError(null)
|
||||
try {
|
||||
const sub = await apiFetch<Subscription>('/subscriptions/cancel', { method: 'POST' })
|
||||
setSubscription(sub)
|
||||
} catch (err: any) {
|
||||
setError(err.message)
|
||||
} finally {
|
||||
setCancelling(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleResume() {
|
||||
setCancelling(true)
|
||||
setError(null)
|
||||
try {
|
||||
const sub = await apiFetch<Subscription>('/subscriptions/resume', { method: 'POST' })
|
||||
setSubscription(sub)
|
||||
} catch (err: any) {
|
||||
setError(err.message)
|
||||
} finally {
|
||||
setCancelling(false)
|
||||
}
|
||||
}
|
||||
|
||||
const planPrice = PLAN_PRICES[selectedPlan]?.[billingPeriod]?.[currency]
|
||||
const daysLeft = subscription?.trialEndAt
|
||||
? Math.ceil((new Date(subscription.trialEndAt).getTime() - Date.now()) / 86400000)
|
||||
: null
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-slate-900">{copy.title}</h2>
|
||||
<p className="text-sm text-slate-500 mt-1">{copy.subtitle}</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="card p-4 border-red-200 bg-red-50 text-sm text-red-700">{error}</div>
|
||||
)}
|
||||
|
||||
{/* Trial banner */}
|
||||
{subscription?.status === 'TRIALING' && daysLeft !== null && daysLeft > 0 && (
|
||||
<div className="card p-4 border-sky-200 bg-sky-50 flex items-center justify-between">
|
||||
<p className="text-sm font-medium text-sky-800">
|
||||
{copy.trial} — <strong>{daysLeft} days</strong> {copy.remaining}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Current plan */}
|
||||
{subscription && (
|
||||
<div className="card p-6">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<p className="text-xs font-medium text-slate-500 uppercase tracking-wide">{copy.currentPlan}</p>
|
||||
<div className="mt-1 flex items-center gap-3">
|
||||
<h3 className="text-2xl font-bold text-slate-900">{subscription.plan}</h3>
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${STATUS_BADGE[subscription.status] ?? 'bg-slate-100 text-slate-600'}`}>
|
||||
{copy.statusLabels[subscription.status] ?? subscription.status}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-slate-500">
|
||||
{subscription.billingPeriod} · {subscription.currency}
|
||||
{subscription.currentPeriodEnd && ` · ${copy.renews} ${new Date(subscription.currentPeriodEnd).toLocaleDateString()}`}
|
||||
</p>
|
||||
{subscription.cancelAtPeriodEnd && (
|
||||
<p className="mt-2 text-sm font-medium text-orange-700">
|
||||
{copy.cancelScheduled}{' '}
|
||||
<button onClick={handleResume} disabled={cancelling} className="underline">{copy.undo}</button>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{!subscription.cancelAtPeriodEnd && (
|
||||
<button
|
||||
onClick={handleCancel}
|
||||
disabled={cancelling}
|
||||
className="btn-secondary text-red-600 border-red-200 hover:bg-red-50 text-sm"
|
||||
>
|
||||
{cancelling ? copy.cancelling : copy.cancelPlan}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Plan selector + checkout */}
|
||||
<div className="card p-6 space-y-6">
|
||||
{!providerAvailability.amanpay && !providerAvailability.paypal ? (
|
||||
<div className="rounded-xl border border-orange-200 bg-orange-50 px-4 py-3 text-sm text-orange-700">
|
||||
{copy.noProviderConfigured}
|
||||
</div>
|
||||
) : null}
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-slate-900">
|
||||
{subscription?.status === 'ACTIVE' ? copy.changePlan : copy.subscribe}
|
||||
</h3>
|
||||
<p className="mt-1 text-sm text-slate-500">{copy.selectPlan}</p>
|
||||
</div>
|
||||
|
||||
{/* Billing period toggle */}
|
||||
<div className="flex items-center gap-2">
|
||||
{(['MONTHLY', 'ANNUAL'] as BillingPeriod[]).map((p) => (
|
||||
<button
|
||||
key={p}
|
||||
onClick={() => setBillingPeriod(p)}
|
||||
className={`px-4 py-1.5 rounded-full text-sm font-medium transition-colors ${
|
||||
billingPeriod === p ? 'bg-slate-900 text-white' : 'bg-slate-100 text-slate-600 hover:bg-slate-200'
|
||||
}`}
|
||||
>
|
||||
{p === 'MONTHLY' ? copy.monthly : copy.annual}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Currency selector */}
|
||||
<div className="flex items-center gap-2">
|
||||
{(['MAD', 'USD', 'EUR'] as Currency[]).map((c) => (
|
||||
<button
|
||||
key={c}
|
||||
onClick={() => setCurrency(c)}
|
||||
className={`px-3 py-1 rounded-lg text-sm font-medium border transition-colors ${
|
||||
currency === c ? 'border-blue-500 bg-blue-50 text-blue-700' : 'border-slate-200 text-slate-600 hover:border-slate-300'
|
||||
}`}
|
||||
>
|
||||
{c}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Plan cards */}
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
{PLANS.map((plan) => {
|
||||
const price = PLAN_PRICES[plan]?.[billingPeriod]?.[currency]
|
||||
const isActive = subscription?.plan === plan && subscription?.status === 'ACTIVE'
|
||||
return (
|
||||
<button
|
||||
key={plan}
|
||||
onClick={() => setSelectedPlan(plan)}
|
||||
className={`text-left p-5 rounded-xl border-2 transition-all ${
|
||||
selectedPlan === plan
|
||||
? 'border-blue-500 bg-blue-50/50'
|
||||
: 'border-slate-200 hover:border-slate-300 bg-white'
|
||||
} ${isActive ? 'ring-2 ring-green-200' : ''}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="font-semibold text-slate-900">{plan}</p>
|
||||
{isActive && <span className="badge-green">{copy.active}</span>}
|
||||
</div>
|
||||
<p className="mt-2 text-2xl font-black text-slate-900">
|
||||
{price ? formatCurrency(price, currency) : '—'}
|
||||
<span className="text-sm font-normal text-slate-500">/{billingPeriod === 'MONTHLY' ? copy.perMonthShort : copy.perYearShort}</span>
|
||||
</p>
|
||||
<ul className="mt-3 space-y-1">
|
||||
{copy.planFeatures[plan].map((f) => (
|
||||
<li key={f} className="text-xs text-slate-600 flex items-center gap-1.5">
|
||||
<span className="text-green-500">✓</span> {f}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Provider selector */}
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-700 mb-2">{copy.paymentProvider}</p>
|
||||
<div className="flex gap-3">
|
||||
{providerAvailability.amanpay ? (
|
||||
<button
|
||||
onClick={() => setProvider('AMANPAY')}
|
||||
className={`flex items-center gap-2 px-4 py-2.5 rounded-xl border-2 text-sm font-medium transition-all ${
|
||||
provider === 'AMANPAY' ? 'border-blue-500 bg-blue-50 text-blue-700' : 'border-slate-200 text-slate-600 hover:border-slate-300'
|
||||
}`}
|
||||
>
|
||||
🏦 AmanPay
|
||||
</button>
|
||||
) : null}
|
||||
{providerAvailability.paypal ? (
|
||||
<button
|
||||
onClick={() => setProvider('PAYPAL')}
|
||||
className={`flex items-center gap-2 px-4 py-2.5 rounded-xl border-2 text-sm font-medium transition-all ${
|
||||
provider === 'PAYPAL' ? 'border-blue-500 bg-blue-50 text-blue-700' : 'border-slate-200 text-slate-600 hover:border-slate-300'
|
||||
}`}
|
||||
>
|
||||
🔵 PayPal
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Checkout CTA */}
|
||||
<div className="flex items-center justify-between pt-2 border-t border-slate-100">
|
||||
<div>
|
||||
<p className="text-sm text-slate-500">{copy.total}</p>
|
||||
<p className="text-xl font-black text-slate-900">
|
||||
{planPrice ? formatCurrency(planPrice, currency) : '—'}
|
||||
<span className="text-sm font-normal text-slate-500 ml-1">/{billingPeriod === 'MONTHLY' ? copy.perMonth : copy.perYear}</span>
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleCheckout}
|
||||
disabled={paying || loading || (!providerAvailability.amanpay && !providerAvailability.paypal)}
|
||||
className="btn-primary px-8 py-3"
|
||||
>
|
||||
{paying ? copy.redirecting : subscription?.status === 'ACTIVE' ? copy.changePlan : copy.subscribeNow}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Invoice history */}
|
||||
<div className="card overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-slate-200">
|
||||
<h3 className="text-base font-semibold text-slate-900">{copy.invoiceHistory}</h3>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="bg-slate-50 border-b border-slate-200">
|
||||
<th className="text-left px-6 py-3 text-xs font-medium text-slate-500 uppercase tracking-wide">{copy.date}</th>
|
||||
<th className="text-left px-6 py-3 text-xs font-medium text-slate-500 uppercase tracking-wide">{copy.provider}</th>
|
||||
<th className="text-left px-6 py-3 text-xs font-medium text-slate-500 uppercase tracking-wide">{copy.status}</th>
|
||||
<th className="text-left px-6 py-3 text-xs font-medium text-slate-500 uppercase tracking-wide">{copy.paid}</th>
|
||||
<th className="text-right px-6 py-3 text-xs font-medium text-slate-500 uppercase tracking-wide">{copy.amount}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-100">
|
||||
{loading ? (
|
||||
<tr><td colSpan={5} className="px-6 py-10 text-center text-sm text-slate-400">{copy.loading}</td></tr>
|
||||
) : invoices.length === 0 ? (
|
||||
<tr><td colSpan={5} className="px-6 py-10 text-center text-sm text-slate-400">{copy.noInvoices}</td></tr>
|
||||
) : invoices.map((inv) => (
|
||||
<tr key={inv.id}>
|
||||
<td className="px-6 py-4 text-sm text-slate-700">{new Date(inv.createdAt).toLocaleDateString()}</td>
|
||||
<td className="px-6 py-4 text-sm text-slate-700">{inv.paymentProvider}</td>
|
||||
<td className="px-6 py-4">
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${INVOICE_STATUS[inv.status] ?? 'bg-slate-100 text-slate-600'}`}>
|
||||
{copy.invoiceStatusLabels[inv.status] ?? inv.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-slate-500">
|
||||
{inv.paidAt ? new Date(inv.paidAt).toLocaleDateString() : '—'}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-right text-sm font-semibold text-slate-900">
|
||||
{formatCurrency(inv.amount, inv.currency)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user