fix signin signout and apply the new style

This commit is contained in:
root
2026-05-24 23:58:54 -04:00
parent bf97a072dd
commit 9bd0938951
68 changed files with 851 additions and 200 deletions
@@ -0,0 +1,654 @@
'use client'
import { useEffect, useMemo, useState } from 'react'
import Link from 'next/link'
import { formatCurrency } from '@rentaldrivego/types'
import { apiFetch } from '@/lib/api'
import { useDashboardI18n } from '@/components/I18nProvider'
type ReservationRow = {
id: string
invoiceNumber: string | null
contractNumber: string | null
status: string
paymentStatus: string
startDate: string
endDate: string
totalAmount: number
depositAmount: number
paidAmount: number
customer: { firstName: string; lastName: string; email: string }
vehicle: { make: string; model: string; licensePlate: string }
}
type PaymentRow = {
id: string
reservationId: string
amount: number
currency: 'MAD' | 'USD' | 'EUR'
status: string
type: 'CHARGE' | 'DEPOSIT'
paymentProvider: 'AMANPAY' | 'PAYPAL'
paymentMethod: string | null
paidAt: string | null
createdAt: string
}
type BillingRow = ReservationRow & {
balanceDue: number
paymentCount: number
latestPayment: PaymentRow | null
}
type ManualPaymentMethod = 'CASH' | 'CHECK' | 'BANK_TRANSFER' | 'CARD' | 'PAYPAL'
const PAYMENT_STATUS_BADGE: Record<string, string> = {
UNPAID: 'bg-rose-100 text-rose-700',
PAID: 'bg-emerald-100 text-emerald-700',
PARTIAL: 'bg-orange-100 text-orange-700',
PENDING: 'bg-sky-100 text-sky-700',
REFUNDED: 'bg-slate-100 text-slate-700',
FAILED: 'bg-rose-100 text-rose-700',
OVERDUE: 'bg-orange-100 text-orange-700',
}
const PAYMENT_EVENT_BADGE: Record<string, string> = {
PENDING: 'bg-orange-100 text-orange-700',
SUCCEEDED: 'bg-emerald-100 text-emerald-700',
FAILED: 'bg-rose-100 text-rose-700',
REFUNDED: 'bg-slate-100 text-slate-700',
PARTIALLY_REFUNDED: 'bg-sky-100 text-sky-700',
}
export default function BillingPage() {
const { language } = useDashboardI18n()
const localeCode = language === 'fr' ? 'fr-FR' : language === 'ar' ? 'ar-MA' : 'en-US'
const [reservations, setReservations] = useState<ReservationRow[]>([])
const [payments, setPayments] = useState<PaymentRow[]>([])
const [search, setSearch] = useState('')
const [error, setError] = useState<string | null>(null)
const [loading, setLoading] = useState(true)
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 [paymentAmount, setPaymentAmount] = useState('')
const [submittingPayment, setSubmittingPayment] = useState(false)
const [paymentError, setPaymentError] = useState<string | null>(null)
const copy = {
en: {
heading: 'Customer Billing',
subtitle: 'Manage customer invoices, payment status, collected amounts, and remaining balances.',
search: 'Search by customer, invoice, contract, vehicle, or plate…',
totalBilled: 'Total billed',
totalCollected: 'Collected',
outstanding: 'Outstanding',
openInvoices: 'Open invoices',
customer: 'Customer',
vehicle: 'Vehicle',
invoice: 'Invoice',
rentalPeriod: 'Rental period',
total: 'Total',
paid: 'Paid',
balance: 'Balance',
status: 'Status',
payment: 'Payment',
paymentMethod: 'Payment method',
actions: 'Actions',
contract: 'Contract',
deposit: 'Deposit',
acceptPayment: 'Accept payment',
paymentModalTitle: 'Accept customer payment',
paymentModalSubtitle: 'Record a payment received for this reservation. No online provider setup is required.',
paymentType: 'Payment type',
paymentCurrency: 'Payment currency',
paymentAmount: 'Payment amount',
cancel: 'Cancel',
savingPayment: 'Saving payment…',
savePayment: 'Save payment',
chargeHelp: 'Charge records a payment against the reservation balance.',
depositHelp: 'Deposit records money received for the refundable security deposit.',
invalidAmount: 'Enter a valid payment amount within the remaining balance.',
paymentActionDisabled: 'No balance due',
paymentsRecorded: (count: number) => `${count} payment(s)`,
none: '—',
loading: 'Loading customer billing…',
noRows: 'No customer bills found.',
failed: 'Failed to load customer billing.',
openContract: 'Open contract',
openBooking: 'Open booking',
charge: 'Charge',
depositType: 'Deposit',
unknownInvoice: 'Draft invoice',
paymentStatusLabels: {
UNPAID: 'Unpaid',
PAID: 'Paid',
PARTIAL: 'Partial',
PENDING: 'Pending',
REFUNDED: 'Refunded',
FAILED: 'Failed',
OVERDUE: 'Overdue',
} as Record<string, string>,
paymentEventLabels: {
PENDING: 'Pending',
SUCCEEDED: 'Succeeded',
FAILED: 'Failed',
REFUNDED: 'Refunded',
PARTIALLY_REFUNDED: 'Partially refunded',
} as Record<string, string>,
paymentProviderLabels: {
AMANPAY: 'AmanPay',
PAYPAL: 'PayPal',
} as Record<string, string>,
paymentMethodLabels: {
CASH: 'Cash',
CHECK: 'Check',
BANK_TRANSFER: 'Bank transfer',
CARD: 'Card',
PAYPAL: 'PayPal',
STRIPE: 'Card',
CREDIT_CARD: 'Card',
DEBIT_CARD: 'Card',
} as Record<string, string>,
},
fr: {
heading: 'Facturation clients',
subtitle: 'Gérez les factures clients, le statut des paiements, les montants encaissés et les soldes restants.',
search: 'Rechercher par client, facture, contrat, véhicule ou plaque…',
totalBilled: 'Total facturé',
totalCollected: 'Encaissé',
outstanding: 'Solde restant',
openInvoices: 'Factures ouvertes',
customer: 'Client',
vehicle: 'Véhicule',
invoice: 'Facture',
rentalPeriod: 'Période',
total: 'Total',
paid: 'Payé',
balance: 'Solde',
status: 'Statut',
payment: 'Paiement',
paymentMethod: 'Mode de paiement',
actions: 'Actions',
contract: 'Contrat',
deposit: 'Dépôt',
acceptPayment: 'Encaisser',
paymentModalTitle: 'Encaisser un paiement client',
paymentModalSubtitle: 'Enregistrez un paiement reçu pour cette réservation. Aucune configuration de fournisseur en ligne nest requise.',
paymentType: 'Type de paiement',
paymentCurrency: 'Devise du paiement',
paymentAmount: 'Montant du paiement',
cancel: 'Annuler',
savingPayment: 'Enregistrement…',
savePayment: 'Enregistrer le paiement',
chargeHelp: 'Le paiement enregistre un règlement sur le solde de la réservation.',
depositHelp: 'Le dépôt enregistre lencaissement du dépôt de garantie remboursable.',
invalidAmount: 'Saisissez un montant valide dans la limite du solde restant.',
paymentActionDisabled: 'Aucun solde dû',
paymentsRecorded: (count: number) => `${count} paiement(s)`,
none: '—',
loading: 'Chargement de la facturation client…',
noRows: 'Aucune facture client trouvée.',
failed: 'Échec du chargement de la facturation client.',
openContract: 'Ouvrir le contrat',
openBooking: 'Ouvrir la réservation',
charge: 'Paiement',
depositType: 'Dépôt',
unknownInvoice: 'Facture brouillon',
paymentStatusLabels: {
UNPAID: 'Non payé',
PAID: 'Payé',
PARTIAL: 'Partiel',
PENDING: 'En attente',
REFUNDED: 'Remboursé',
FAILED: 'Échoué',
OVERDUE: 'En retard',
} as Record<string, string>,
paymentEventLabels: {
PENDING: 'En attente',
SUCCEEDED: 'Réussi',
FAILED: 'Échoué',
REFUNDED: 'Remboursé',
PARTIALLY_REFUNDED: 'Partiellement remboursé',
} as Record<string, string>,
paymentProviderLabels: {
AMANPAY: 'AmanPay',
PAYPAL: 'PayPal',
} as Record<string, string>,
paymentMethodLabels: {
CASH: 'Espèces',
CHECK: 'Chèque',
BANK_TRANSFER: 'Virement bancaire',
CARD: 'Carte',
PAYPAL: 'PayPal',
STRIPE: 'Carte',
CREDIT_CARD: 'Carte',
DEBIT_CARD: 'Carte',
} as Record<string, string>,
},
ar: {
heading: 'فوترة العملاء',
subtitle: 'إدارة فواتير العملاء وحالة الدفع والمبالغ المحصلة والأرصدة المتبقية.',
search: 'ابحث بالعميل أو الفاتورة أو العقد أو السيارة أو اللوحة…',
totalBilled: 'إجمالي الفواتير',
totalCollected: 'المحصّل',
outstanding: 'المتبقي',
openInvoices: 'الفواتير المفتوحة',
customer: 'العميل',
vehicle: 'المركبة',
invoice: 'الفاتورة',
rentalPeriod: 'فترة الإيجار',
total: 'الإجمالي',
paid: 'المدفوع',
balance: 'الرصيد',
status: 'الحالة',
payment: 'الدفعة',
paymentMethod: 'طريقة الدفع',
actions: 'الإجراءات',
contract: 'العقد',
deposit: 'العربون',
acceptPayment: 'تحصيل دفعة',
paymentModalTitle: 'تحصيل دفعة من العميل',
paymentModalSubtitle: 'سجّل دفعة مستلمة لهذا الحجز. لا يتطلب ذلك إعداد مزود دفع عبر الإنترنت.',
paymentType: 'نوع الدفع',
paymentCurrency: 'عملة الدفع',
paymentAmount: 'مبلغ الدفعة',
cancel: 'إلغاء',
savingPayment: 'جارٍ الحفظ…',
savePayment: 'حفظ الدفعة',
chargeHelp: 'تسجّل الدفعة مبلغاً مستلماً على رصيد الحجز.',
depositHelp: 'العربون يسجل مبلغ التأمين القابل للاسترداد المستلم.',
invalidAmount: 'أدخل مبلغاً صحيحاً ضمن الرصيد المتبقي.',
paymentActionDisabled: 'لا يوجد رصيد مستحق',
paymentsRecorded: (count: number) => `${count} دفعة`,
none: '—',
loading: 'جارٍ تحميل فوترة العملاء…',
noRows: 'لم يتم العثور على فواتير عملاء.',
failed: 'فشل تحميل فوترة العملاء.',
openContract: 'فتح العقد',
openBooking: 'فتح الحجز',
charge: 'دفعة',
depositType: 'عربون',
unknownInvoice: 'فاتورة مسودة',
paymentStatusLabels: {
UNPAID: 'غير مدفوع',
PAID: 'مدفوع',
PARTIAL: 'جزئي',
PENDING: 'قيد الانتظار',
REFUNDED: 'مسترد',
FAILED: 'فشل',
OVERDUE: 'متأخر',
} as Record<string, string>,
paymentEventLabels: {
PENDING: 'قيد الانتظار',
SUCCEEDED: 'ناجح',
FAILED: 'فشل',
REFUNDED: 'مسترد',
PARTIALLY_REFUNDED: 'مسترد جزئياً',
} as Record<string, string>,
paymentProviderLabels: {
AMANPAY: 'AmanPay',
PAYPAL: 'PayPal',
} as Record<string, string>,
paymentMethodLabels: {
CASH: 'نقداً',
CHECK: 'شيك',
BANK_TRANSFER: 'تحويل بنكي',
CARD: 'بطاقة',
PAYPAL: 'PayPal',
STRIPE: 'بطاقة',
CREDIT_CARD: 'بطاقة',
DEBIT_CARD: 'بطاقة',
} as Record<string, string>,
},
}[language]
async function loadBillingData() {
const [reservationRows, paymentRows] = await Promise.all([
apiFetch<ReservationRow[]>('/reservations?pageSize=100'),
apiFetch<PaymentRow[]>('/payments/company'),
])
setReservations(reservationRows ?? [])
setPayments(paymentRows ?? [])
}
useEffect(() => {
loadBillingData()
.catch((err) => setError(err.message ?? copy.failed))
.finally(() => setLoading(false))
}, [copy.failed])
const billingRows = useMemo<BillingRow[]>(() => {
const paymentsByReservation = new Map<string, PaymentRow[]>()
for (const payment of payments) {
const existing = paymentsByReservation.get(payment.reservationId) ?? []
existing.push(payment)
paymentsByReservation.set(payment.reservationId, existing)
}
return reservations.map((reservation) => {
const reservationPayments = (paymentsByReservation.get(reservation.id) ?? []).sort((a, b) =>
new Date(b.paidAt ?? b.createdAt).getTime() - new Date(a.paidAt ?? a.createdAt).getTime(),
)
return {
...reservation,
balanceDue: Math.max(reservation.totalAmount - reservation.paidAmount, 0),
paymentCount: reservationPayments.length,
latestPayment: reservationPayments[0] ?? null,
}
})
}, [payments, reservations])
const filteredRows = useMemo(() => {
const q = search.trim().toLowerCase()
if (!q) return billingRows
return billingRows.filter((row) =>
`${row.customer.firstName} ${row.customer.lastName}`.toLowerCase().includes(q) ||
row.customer.email.toLowerCase().includes(q) ||
`${row.vehicle.make} ${row.vehicle.model}`.toLowerCase().includes(q) ||
row.vehicle.licensePlate.toLowerCase().includes(q) ||
(row.invoiceNumber ?? '').toLowerCase().includes(q) ||
(row.contractNumber ?? '').toLowerCase().includes(q),
)
}, [billingRows, search])
const totals = useMemo(() => {
return filteredRows.reduce(
(acc, row) => {
acc.totalBilled += row.totalAmount
acc.totalCollected += row.paidAmount
acc.outstanding += row.balanceDue
if (row.balanceDue > 0) acc.openInvoices += 1
return acc
},
{ totalBilled: 0, totalCollected: 0, outstanding: 0, openInvoices: 0 },
)
}, [filteredRows])
function formatDate(value: string) {
return new Date(value).toLocaleDateString(localeCode, {
month: 'short',
day: 'numeric',
year: 'numeric',
})
}
function translateStatus(status: string, labels: Record<string, string>) {
return labels[status] ?? status.replace(/_/g, ' ')
}
function translatePaymentMethod(method: string | null) {
if (!method) return copy.none
return copy.paymentMethodLabels[method] ?? method.replace(/_/g, ' ')
}
function canAcceptPayment(row: BillingRow) {
return row.balanceDue > 0
}
function openPaymentModal(row: BillingRow) {
setPaymentModalRow(row)
setPaymentMethod('CASH')
setPaymentType('CHARGE')
setPaymentCurrency('MAD')
setPaymentAmount(String((row.balanceDue / 100).toFixed(2)))
setPaymentError(null)
}
async function handleAcceptPayment() {
if (!paymentModalRow) return
const amount = Math.round(Number(paymentAmount) * 100)
if (!Number.isFinite(amount) || amount <= 0 || amount > paymentModalRow.balanceDue) {
setPaymentError(copy.invalidAmount)
return
}
setSubmittingPayment(true)
setPaymentError(null)
try {
await apiFetch<PaymentRow>(`/payments/reservations/${paymentModalRow.id}/manual`, {
method: 'POST',
body: JSON.stringify({
amount,
type: paymentType,
currency: paymentCurrency,
paymentMethod,
}),
})
await loadBillingData()
setPaymentModalRow(null)
} catch (err: any) {
setPaymentError(err.message ?? copy.failed)
} finally {
setSubmittingPayment(false)
}
}
return (
<div className="space-y-6">
<div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
<div>
<h2 className="text-xl font-semibold text-slate-900">{copy.heading}</h2>
<p className="mt-1 text-sm text-slate-500">{copy.subtitle}</p>
</div>
<input
value={search}
onChange={(event) => setSearch(event.target.value)}
placeholder={copy.search}
className="input-field w-full lg:max-w-md"
/>
</div>
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
<MetricCard label={copy.totalBilled} value={formatCurrency(totals.totalBilled, 'MAD')} />
<MetricCard label={copy.totalCollected} value={formatCurrency(totals.totalCollected, 'MAD')} />
<MetricCard label={copy.outstanding} value={formatCurrency(totals.outstanding, 'MAD')} />
<MetricCard label={copy.openInvoices} value={totals.openInvoices.toLocaleString(localeCode)} />
</div>
<div className="card overflow-hidden">
{error ? (
<div className="p-6 text-sm text-red-600">{error}</div>
) : (
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-slate-200 bg-slate-50">
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-slate-500">{copy.customer}</th>
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-slate-500">{copy.vehicle}</th>
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-slate-500">{copy.invoice}</th>
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-slate-500">{copy.rentalPeriod}</th>
<th className="px-6 py-3 text-right text-xs font-medium uppercase tracking-wide text-slate-500">{copy.total}</th>
<th className="px-6 py-3 text-right text-xs font-medium uppercase tracking-wide text-slate-500">{copy.paid}</th>
<th className="px-6 py-3 text-right text-xs font-medium uppercase tracking-wide text-slate-500">{copy.balance}</th>
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-slate-500">{copy.status}</th>
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-slate-500">{copy.payment}</th>
<th className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wide text-slate-500">{copy.paymentMethod}</th>
<th className="px-6 py-3 text-right text-xs font-medium uppercase tracking-wide text-slate-500">{copy.actions}</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{loading ? (
<tr>
<td colSpan={11} className="px-6 py-10 text-center text-sm text-slate-400">{copy.loading}</td>
</tr>
) : filteredRows.length === 0 ? (
<tr>
<td colSpan={11} className="px-6 py-10 text-center text-sm text-slate-400">{copy.noRows}</td>
</tr>
) : filteredRows.map((row) => (
<tr key={row.id}>
<td className="px-6 py-4 text-sm">
<p className="font-semibold text-slate-900">{row.customer.firstName} {row.customer.lastName}</p>
<p className="text-xs text-slate-500">{row.customer.email}</p>
</td>
<td className="px-6 py-4 text-sm text-slate-700">
<p>{row.vehicle.make} {row.vehicle.model}</p>
<p className="text-xs text-slate-500">{row.vehicle.licensePlate}</p>
</td>
<td className="px-6 py-4 text-sm text-slate-700">
<p className="font-medium text-slate-900">{row.invoiceNumber ?? copy.unknownInvoice}</p>
<p className="text-xs text-slate-500">{copy.contract}: {row.contractNumber ?? copy.none}</p>
</td>
<td className="px-6 py-4 text-sm text-slate-600">
<p>{formatDate(row.startDate)}</p>
<p>{formatDate(row.endDate)}</p>
</td>
<td className="px-6 py-4 text-right text-sm font-semibold text-slate-900">
{formatCurrency(row.totalAmount, 'MAD')}
{row.depositAmount > 0 ? (
<p className="text-xs font-normal text-slate-500">{copy.deposit}: {formatCurrency(row.depositAmount, 'MAD')}</p>
) : null}
</td>
<td className="px-6 py-4 text-right text-sm font-semibold text-emerald-700">{formatCurrency(row.paidAmount, 'MAD')}</td>
<td className="px-6 py-4 text-right text-sm font-semibold text-orange-700">{formatCurrency(row.balanceDue, 'MAD')}</td>
<td className="px-6 py-4 text-sm">
<span className={`inline-flex rounded-full px-2.5 py-0.5 text-xs font-medium ${PAYMENT_STATUS_BADGE[row.paymentStatus] ?? 'bg-slate-100 text-slate-700'}`}>
{translateStatus(row.paymentStatus, copy.paymentStatusLabels)}
</span>
</td>
<td className="px-6 py-4 text-sm text-slate-700">
{row.latestPayment ? (
<div className="space-y-1">
<div className="flex items-center gap-2">
<span className={`inline-flex rounded-full px-2.5 py-0.5 text-xs font-medium ${PAYMENT_EVENT_BADGE[row.latestPayment.status] ?? 'bg-slate-100 text-slate-700'}`}>
{translateStatus(row.latestPayment.status, copy.paymentEventLabels)}
</span>
<span className="text-xs text-slate-500">{copy.paymentProviderLabels[row.latestPayment.paymentProvider] ?? row.latestPayment.paymentProvider}</span>
</div>
<p className="text-xs text-slate-500">
{row.latestPayment.type === 'DEPOSIT' ? copy.depositType : copy.charge} · {formatCurrency(row.latestPayment.amount, row.latestPayment.currency)}
</p>
<p className="text-xs text-slate-500">{formatDate(row.latestPayment.paidAt ?? row.latestPayment.createdAt)} · {copy.paymentsRecorded(row.paymentCount)}</p>
</div>
) : (
<span className="text-slate-400">{copy.none}</span>
)}
</td>
<td className="px-6 py-4 text-sm text-slate-700">
{row.latestPayment ? (
<div className="space-y-1">
<p className="font-medium text-slate-900">{translatePaymentMethod(row.latestPayment.paymentMethod)}</p>
<p className="text-xs text-slate-500">{copy.paymentProviderLabels[row.latestPayment.paymentProvider] ?? row.latestPayment.paymentProvider}</p>
</div>
) : (
<span className="text-slate-400">{copy.none}</span>
)}
</td>
<td className="px-6 py-4 text-right text-sm">
<div className="flex flex-col items-end gap-2">
{canAcceptPayment(row) ? (
<button type="button" onClick={() => openPaymentModal(row)} className="font-semibold text-emerald-700 hover:underline">
{copy.acceptPayment}
</button>
) : (
<span className="text-xs text-slate-400">{copy.paymentActionDisabled}</span>
)}
<Link href={`/contracts/${row.id}`} className="font-semibold text-blue-700 hover:underline">
{copy.openContract}
</Link>
<Link href={`/reservations/${row.id}`} className="text-slate-600 hover:text-slate-900 hover:underline">
{copy.openBooking}
</Link>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
{paymentModalRow ? (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-[#07101e]/30 backdrop-blur-sm" onClick={(event) => {
if (event.target === event.currentTarget && !submittingPayment) setPaymentModalRow(null)
}}>
<div className="w-full max-w-lg rounded-3xl bg-white p-6 shadow-2xl">
<div className="flex items-start justify-between gap-4">
<div>
<h3 className="text-lg font-semibold text-slate-900">{copy.paymentModalTitle}</h3>
<p className="mt-1 text-sm text-slate-500">{copy.paymentModalSubtitle}</p>
</div>
<button type="button" onClick={() => setPaymentModalRow(null)} disabled={submittingPayment} className="text-slate-400 transition hover:text-slate-700">
×
</button>
</div>
<div className="mt-5 rounded-2xl border border-slate-200 bg-slate-50 p-4 text-sm text-slate-700">
<p className="font-semibold text-slate-900">{paymentModalRow.customer.firstName} {paymentModalRow.customer.lastName}</p>
<p className="mt-1">{paymentModalRow.vehicle.make} {paymentModalRow.vehicle.model} · {paymentModalRow.vehicle.licensePlate}</p>
<p className="mt-1">{copy.invoice}: {paymentModalRow.invoiceNumber ?? copy.unknownInvoice}</p>
<p className="mt-2 font-semibold text-slate-900">{copy.balance}: {formatCurrency(paymentModalRow.balanceDue, paymentCurrency)}</p>
</div>
{paymentError ? (
<div className="mt-5 rounded-2xl border border-rose-200 bg-rose-50 p-4 text-sm text-rose-700">
{paymentError}
</div>
) : null}
<div className="mt-5 space-y-4">
<div>
<label className="mb-1 block text-sm font-medium text-slate-700">{copy.paymentMethod}</label>
<select className="input-field" value={paymentMethod} onChange={(event) => setPaymentMethod(event.target.value as ManualPaymentMethod)} disabled={submittingPayment}>
<option value="CASH">{copy.paymentMethodLabels.CASH}</option>
<option value="CHECK">{copy.paymentMethodLabels.CHECK}</option>
<option value="BANK_TRANSFER">{copy.paymentMethodLabels.BANK_TRANSFER}</option>
<option value="CARD">{copy.paymentMethodLabels.CARD}</option>
<option value="PAYPAL">{copy.paymentMethodLabels.PAYPAL}</option>
</select>
</div>
<div>
<label className="mb-1 block text-sm font-medium text-slate-700">{copy.paymentType}</label>
<select className="input-field" value={paymentType} onChange={(event) => setPaymentType(event.target.value as 'CHARGE' | 'DEPOSIT')} disabled={submittingPayment}>
{paymentModalRow.depositAmount > 0 ? <option value="DEPOSIT">{copy.depositType}</option> : null}
<option value="CHARGE">{copy.charge}</option>
</select>
<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} />
</div>
</div>
<div className="mt-6 flex items-center gap-3">
<button type="button" onClick={() => setPaymentModalRow(null)} disabled={submittingPayment} className="flex-1 rounded-full border border-slate-200 px-4 py-2 text-sm font-semibold text-slate-700 transition hover:bg-slate-50">
{copy.cancel}
</button>
<button type="button" onClick={handleAcceptPayment} disabled={submittingPayment} className="btn-primary flex-1 justify-center disabled:cursor-not-allowed disabled:opacity-60">
{submittingPayment ? copy.savingPayment : copy.savePayment}
</button>
</div>
</div>
</div>
) : null}
</div>
)
}
function MetricCard({ label, value }: { label: string; value: string }) {
return (
<div className="card p-5">
<p className="text-sm font-medium text-slate-500">{label}</p>
<p className="mt-2 text-2xl font-bold text-slate-900">{value}</p>
</div>
)
}