update subscription algorithm

This commit is contained in:
root
2026-05-25 03:12:19 -04:00
parent 95376d3223
commit c9cbe479aa
42 changed files with 3098 additions and 307 deletions
@@ -25,7 +25,7 @@ type PaymentRow = {
id: string
reservationId: string
amount: number
currency: 'MAD' | 'USD' | 'EUR'
currency: string
status: string
type: 'CHARGE' | 'DEPOSIT'
paymentProvider: 'AMANPAY' | 'PAYPAL'
@@ -71,7 +71,7 @@ export default function BillingPage() {
const [paymentModalRow, setPaymentModalRow] = useState<BillingRow | null>(null)
const [paymentMethod, setPaymentMethod] = useState<ManualPaymentMethod>('CASH')
const [paymentType, setPaymentType] = useState<'CHARGE' | 'DEPOSIT'>('CHARGE')
const [paymentCurrency, setPaymentCurrency] = useState<'MAD' | 'USD' | 'EUR'>('MAD')
const paymentCurrency = 'MAD'
const [paymentAmount, setPaymentAmount] = useState('')
const [submittingPayment, setSubmittingPayment] = useState(false)
const [paymentError, setPaymentError] = useState<string | null>(null)
@@ -393,7 +393,6 @@ export default function BillingPage() {
setPaymentModalRow(row)
setPaymentMethod('CASH')
setPaymentType('CHARGE')
setPaymentCurrency('MAD')
setPaymentAmount(String((row.balanceDue / 100).toFixed(2)))
setPaymentError(null)
}
@@ -614,15 +613,6 @@ export default function BillingPage() {
<p className="mt-2 text-xs text-slate-500">{paymentType === 'DEPOSIT' ? copy.depositHelp : copy.chargeHelp}</p>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-slate-700">{copy.paymentCurrency}</label>
<select className="input-field" value={paymentCurrency} onChange={(event) => setPaymentCurrency(event.target.value as 'MAD' | 'USD' | 'EUR')} disabled={submittingPayment}>
<option value="MAD">MAD</option>
<option value="USD">USD</option>
<option value="EUR">EUR</option>
</select>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-slate-700">{copy.paymentAmount}</label>
<input type="number" min="0" step="0.01" className="input-field" value={paymentAmount} onChange={(event) => setPaymentAmount(event.target.value)} disabled={submittingPayment} />
@@ -8,14 +8,13 @@ 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
currency: string
trialEndAt: string | null
currentPeriodEnd: string | null
cancelAtPeriodEnd: boolean
@@ -24,7 +23,7 @@ interface Subscription {
interface Invoice {
id: string
amount: number
currency: Currency
currency: string
status: string
paymentProvider: string
paidAt: string | null
@@ -67,7 +66,7 @@ export default function SubscriptionPage() {
const [selectedPlan, setSelectedPlan] = useState<Plan>('STARTER')
const [billingPeriod, setBillingPeriod] = useState<BillingPeriod>('MONTHLY')
const [currency, setCurrency] = useState<Currency>('MAD')
const currency = 'MAD'
const [provider, setProvider] = useState<'AMANPAY' | 'PAYPAL'>('AMANPAY')
const [providerAvailability, setProviderAvailability] = useState<ProviderAvailability>({ amanpay: false, paypal: false })
const [paying, setPaying] = useState(false)
@@ -245,7 +244,7 @@ export default function SubscriptionPage() {
setSubscription(sub)
setSelectedPlan(sub.plan)
setBillingPeriod(sub.billingPeriod)
setCurrency(sub.currency as Currency)
// currency is always MAD
}
setInvoices(inv ?? [])
})
@@ -400,21 +399,6 @@ export default function SubscriptionPage() {
))}
</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) => {
@@ -435,7 +419,7 @@ export default function SubscriptionPage() {
{isActive && <span className="badge-green">{copy.active}</span>}
</div>
<p className="mt-2 text-2xl font-black text-slate-900">
{price ? formatCurrency(price, currency) : '—'}
{price ? formatCurrency(price, 'MAD') : '—'}
<span className="text-sm font-normal text-slate-500">/{billingPeriod === 'MONTHLY' ? copy.perMonthShort : copy.perYearShort}</span>
</p>
<ul className="mt-3 space-y-1">
@@ -482,7 +466,7 @@ export default function SubscriptionPage() {
<div>
<p className="text-sm text-slate-500">{copy.total}</p>
<p className="text-xl font-black text-slate-900">
{planPrice ? formatCurrency(planPrice, currency) : '—'}
{planPrice ? formatCurrency(planPrice, 'MAD') : '—'}
<span className="text-sm font-normal text-slate-500 ml-1">/{billingPeriod === 'MONTHLY' ? copy.perMonth : copy.perYear}</span>
</p>
</div>
@@ -530,7 +514,7 @@ export default function SubscriptionPage() {
{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)}
{formatCurrency(inv.amount, 'MAD')}
</td>
</tr>
))}
@@ -58,12 +58,20 @@ export default function ForgotPasswordPageClient({ embedded = false }: { embedde
setLoading(true)
setError(null)
try {
const res = await fetch(`${API_BASE}/auth/employee/forgot-password`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
})
if (!res.ok) throw new Error()
// Try employee first, then admin — mirrors the sign-in page which handles both
const [empRes, adminRes] = await Promise.all([
fetch(`${API_BASE}/auth/employee/forgot-password`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
}),
fetch(`${API_BASE}/admin/auth/forgot-password`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
}),
])
if (!empRes.ok && !adminRes.ok) throw new Error()
setSent(true)
} catch {
setError(dict.error)
@@ -29,7 +29,7 @@ type SignupForm = {
yearsActive: string
plan: 'STARTER' | 'GROWTH' | 'PRO'
billingPeriod: 'MONTHLY' | 'ANNUAL'
currency: 'MAD' | 'USD' | 'EUR'
currency: 'MAD'
paymentProvider: 'AMANPAY' | 'PAYPAL'
}
@@ -557,9 +557,8 @@ export default function SignUpPage() {
</button>
))}
</div>
<div className="grid gap-4 sm:grid-cols-3">
<div className="grid gap-4 sm:grid-cols-2">
<LabeledSelect label={language === 'fr' ? 'Période de facturation' : language === 'ar' ? 'فترة الفوترة' : 'Billing period'} value={form.billingPeriod} options={['MONTHLY', 'ANNUAL']} labels={dict.billingPeriodOptions} onChange={(value) => setForm((current) => ({ ...current, billingPeriod: value as SignupForm['billingPeriod'] }))} />
<LabeledSelect label={language === 'fr' ? 'Devise' : language === 'ar' ? 'العملة' : 'Currency'} value={form.currency} options={['MAD', 'USD', 'EUR']} onChange={(value) => setForm((current) => ({ ...current, currency: value as SignupForm['currency'] }))} />
<LabeledSelect label={language === 'fr' ? 'Prestataire principal' : language === 'ar' ? 'مزود الدفع الرئيسي' : 'Primary provider'} value={form.paymentProvider} options={['AMANPAY', 'PAYPAL']} labels={dict.paymentProviderOptions} onChange={(value) => setForm((current) => ({ ...current, paymentProvider: value as SignupForm['paymentProvider'] }))} />
</div>
<NavActions onBack={() => setStep(2)} onNext={() => setStep(4)} nextLabel={dict.continue} backLabel={dict.back} loadingLabel={dict.working} />
@@ -571,7 +570,7 @@ export default function SignUpPage() {
<SectionIntro title={dict.reviewTitle} body={dict.reviewBody} />
<div className="rounded-3xl border border-slate-200 bg-slate-50 p-5 text-sm text-slate-600">
<p><span className="font-semibold text-slate-900">{dict.reviewWorkspace}:</span> {form.companyName.fr || form.companyName.ar || dict.companyFallback}{form.companyName.fr && form.companyName.ar ? <span className="ml-2 text-slate-400">/ {form.companyName.ar}</span> : null}</p>
<p className="mt-2"><span className="font-semibold text-slate-900">{dict.reviewPlan}:</span> {form.plan} · {dict.billingPeriodOptions[form.billingPeriod]} · {form.currency}</p>
<p className="mt-2"><span className="font-semibold text-slate-900">{dict.reviewPlan}:</span> {form.plan} · {dict.billingPeriodOptions[form.billingPeriod]} · MAD</p>
<p className="mt-2"><span className="font-semibold text-slate-900">{dict.reviewOwnerEmail}:</span> {form.email || '—'}</p>
<p className="mt-2"><span className="font-semibold text-slate-900">{dict.reviewPhone}:</span> {form.companyPhone || '—'}</p>
</div>
@@ -741,7 +740,6 @@ function normalizeBillingPeriod(value: string | null): SignupForm['billingPeriod
return value === 'ANNUAL' ? 'ANNUAL' : 'MONTHLY'
}
function normalizeCurrency(value: string | null): SignupForm['currency'] {
if (value === 'USD' || value === 'EUR' || value === 'MAD') return value
function normalizeCurrency(_value: string | null): SignupForm['currency'] {
return 'MAD'
}