Files
carmanagement/apps/admin/src/app/dashboard/companies/[id]/page.tsx
T
root 7ff2dbb139
Build & Deploy / Build & Push Docker Image (push) Successful in 2m55s
Test / Type Check (all packages) (push) Successful in 54s
Build & Deploy / Deploy to VPS (push) Successful in 4s
Test / API Unit Tests (push) Failing after 48s
Test / Homepage Unit Tests (push) Successful in 43s
Test / Storefront Unit Tests (push) Failing after 40s
Test / Admin Unit Tests (push) Successful in 40s
Test / Dashboard Unit Tests (push) Failing after 42s
Test / API Integration Tests (push) Successful in 1m0s
chore: wire Carplace into dev and production stacks
2026-07-02 18:15:42 -04:00

1032 lines
50 KiB
TypeScript

'use client'
import { useEffect, useState } from 'react'
import { useParams, useRouter } from 'next/navigation'
import Link from 'next/link'
import { ADMIN_API_BASE } from '@/lib/api'
interface CompanyAddressProfile {
streetAddress?: string | null
city?: string | null
country?: string | null
zipCode?: string | null
legalName?: string | null
legalForm?: string | null
companyEmail?: string | null
managerName?: string | null
iceNumber?: string | null
operatingLicenseNumber?: string | null
operatingLicenseIssuedAt?: string | null
operatingLicenseIssuedBy?: string | null
fax?: string | null
yearsActive?: string | null
representativeName?: string | null
representativeTitle?: string | null
responsibleName?: string | null
responsibleRole?: string | null
responsibleIdentityNumber?: string | null
responsibleQualification?: string | null
responsiblePhone?: string | null
responsibleEmail?: string | null
}
interface CompanyDetail {
id: string
name: string
slug: string
email: string
phone: string | null
address: CompanyAddressProfile | string | null
status: string
subscriptionPaymentRef: string | null
createdAt: string
subscription: {
id: string
plan: string
billingPeriod: string
status: string
currency: string
trialStartAt: string | null
trialEndAt: string | null
currentPeriodStart: string | null
currentPeriodEnd: string | null
cancelledAt: string | null
cancelAtPeriodEnd: boolean
paymentPendingSince: string | null
pastDueSince: string | null
suspendedAt: string | null
retryCount: number
} | null
brand: {
displayName: string
tagline: string | null
subdomain: string
customDomain: string | null
publicEmail: string | null
publicPhone: string | null
publicAddress: string | null
publicCity: string | null
publicCountry: string | null
websiteUrl: string | null
whatsappNumber: string | null
defaultLocale: string
defaultCurrency: string
isListedOnCarplace: boolean
} | null
contractSettings: {
legalName: string | null
registrationNumber: string | null
taxId: string | null
terms: string
fuelPolicyType: string
lateFeePerHour: number | null
taxRate: number | null
signatureRequired: boolean
showTax: boolean
} | null
accountingSettings: {
reportingPeriod: string
fiscalYearStart: number
currency: string
accountantEmail: string | null
accountantName: string | null
autoSendReport: boolean
reportFormat: string
} | null
employees?: Array<{ id: string }>
vehicles?: Array<{ id: string }>
customers?: Array<{ id: string }>
reservations?: Array<{ id: string }>
_count?: { employees?: number; vehicles?: number; customers?: number; reservations?: number }
}
interface AuditLog {
id: string
action: string
resource: string
resourceId: string | null
createdAt: string
adminUser: { email: string } | null
}
interface SubEvent {
id: string
eventType: string
source: string
payload: Record<string, any>
occurredAt: string
}
interface FormState {
company: {
name: string
slug: string
email: string
phone: string
status: string
subscriptionPaymentRef: string
}
companyProfile: {
legalForm: string
managerName: string
zipCode: string
fax: string
yearsActive: string
iceNumber: string
operatingLicenseNumber: string
operatingLicenseIssuedAt: string
operatingLicenseIssuedBy: string
representativeName: string
representativeTitle: string
responsibleName: string
responsibleRole: string
responsibleIdentityNumber: string
responsibleQualification: string
responsiblePhone: string
responsibleEmail: string
}
subscription: {
plan: string
billingPeriod: string
status: string
currency: string
trialStartAt: string
trialEndAt: string
currentPeriodStart: string
currentPeriodEnd: string
cancelledAt: string
cancelAtPeriodEnd: boolean
}
brand: {
displayName: string
tagline: string
subdomain: string
customDomain: string
publicEmail: string
publicPhone: string
publicAddress: string
publicCity: string
publicCountry: string
websiteUrl: string
whatsappNumber: string
defaultLocale: string
defaultCurrency: string
isListedOnCarplace: boolean
}
contractSettings: {
legalName: string
registrationNumber: string
taxId: string
terms: string
fuelPolicyType: string
lateFeePerHour: string
taxRate: string
signatureRequired: boolean
showTax: boolean
}
accountingSettings: {
reportingPeriod: string
fiscalYearStart: string
currency: string
accountantEmail: string
accountantName: string
autoSendReport: boolean
reportFormat: string
}
}
const STATUS_COLORS: Record<string, string> = {
ACTIVE: 'text-emerald-400',
TRIALING: 'text-sky-400',
SUSPENDED: 'text-red-400',
PENDING: 'text-orange-400',
CANCELLED: 'text-zinc-400',
PAST_DUE: 'text-orange-400',
}
const INPUT_CLASS = 'mt-1 w-full rounded-xl border border-zinc-700 bg-zinc-900 px-3 py-2 text-sm text-zinc-100 outline-none focus:border-emerald-500'
const LABEL_CLASS = 'text-xs font-medium uppercase tracking-wide text-zinc-500'
function toDateInput(value: string | null | undefined) {
return value ? new Date(value).toISOString().slice(0, 10) : ''
}
function toNullable(value: string) {
const trimmed = value.trim()
return trimmed ? trimmed : null
}
function normalizeAddressProfile(value: CompanyDetail['address']): CompanyAddressProfile {
if (!value || typeof value !== 'object' || Array.isArray(value)) return {}
return value
}
function createFormState(company: CompanyDetail): FormState {
const address = normalizeAddressProfile(company.address)
return {
company: {
name: company.name ?? '',
slug: company.slug ?? '',
email: company.email ?? '',
phone: company.phone ?? '',
status: company.status ?? 'TRIALING',
subscriptionPaymentRef: company.subscriptionPaymentRef ?? '',
},
companyProfile: {
legalForm: address.legalForm ?? '',
managerName: address.managerName ?? '',
zipCode: address.zipCode ?? '',
fax: address.fax ?? '',
yearsActive: address.yearsActive ?? '',
iceNumber: address.iceNumber ?? '',
operatingLicenseNumber: address.operatingLicenseNumber ?? '',
operatingLicenseIssuedAt: address.operatingLicenseIssuedAt ?? '',
operatingLicenseIssuedBy: address.operatingLicenseIssuedBy ?? '',
representativeName: address.representativeName ?? '',
representativeTitle: address.representativeTitle ?? '',
responsibleName: address.responsibleName ?? '',
responsibleRole: address.responsibleRole ?? '',
responsibleIdentityNumber: address.responsibleIdentityNumber ?? '',
responsibleQualification: address.responsibleQualification ?? '',
responsiblePhone: address.responsiblePhone ?? '',
responsibleEmail: address.responsibleEmail ?? '',
},
subscription: {
plan: company.subscription?.plan ?? 'STARTER',
billingPeriod: company.subscription?.billingPeriod ?? 'MONTHLY',
status: company.subscription?.status ?? 'TRIALING',
currency: company.subscription?.currency ?? 'MAD',
trialStartAt: toDateInput(company.subscription?.trialStartAt),
trialEndAt: toDateInput(company.subscription?.trialEndAt),
currentPeriodStart: toDateInput(company.subscription?.currentPeriodStart),
currentPeriodEnd: toDateInput(company.subscription?.currentPeriodEnd),
cancelledAt: toDateInput(company.subscription?.cancelledAt),
cancelAtPeriodEnd: company.subscription?.cancelAtPeriodEnd ?? false,
},
brand: {
displayName: company.brand?.displayName ?? company.name ?? '',
tagline: company.brand?.tagline ?? '',
subdomain: company.brand?.subdomain ?? company.slug ?? '',
customDomain: company.brand?.customDomain ?? '',
publicEmail: company.brand?.publicEmail ?? '',
publicPhone: company.brand?.publicPhone ?? '',
publicAddress: company.brand?.publicAddress ?? '',
publicCity: company.brand?.publicCity ?? '',
publicCountry: company.brand?.publicCountry ?? '',
websiteUrl: company.brand?.websiteUrl ?? '',
whatsappNumber: company.brand?.whatsappNumber ?? '',
defaultLocale: company.brand?.defaultLocale ?? 'en',
defaultCurrency: company.brand?.defaultCurrency ?? 'MAD',
isListedOnCarplace: company.brand?.isListedOnCarplace ?? true,
},
contractSettings: {
legalName: company.contractSettings?.legalName ?? '',
registrationNumber: company.contractSettings?.registrationNumber ?? '',
taxId: company.contractSettings?.taxId ?? '',
terms: company.contractSettings?.terms ?? '',
fuelPolicyType: company.contractSettings?.fuelPolicyType ?? 'FULL_TO_FULL',
lateFeePerHour: company.contractSettings?.lateFeePerHour?.toString() ?? '',
taxRate: company.contractSettings?.taxRate?.toString() ?? '',
signatureRequired: company.contractSettings?.signatureRequired ?? true,
showTax: company.contractSettings?.showTax ?? false,
},
accountingSettings: {
reportingPeriod: company.accountingSettings?.reportingPeriod ?? 'MONTHLY',
fiscalYearStart: company.accountingSettings?.fiscalYearStart?.toString() ?? '1',
currency: company.accountingSettings?.currency ?? 'MAD',
accountantEmail: company.accountingSettings?.accountantEmail ?? '',
accountantName: company.accountingSettings?.accountantName ?? '',
autoSendReport: company.accountingSettings?.autoSendReport ?? false,
reportFormat: company.accountingSettings?.reportFormat ?? 'PDF',
},
}
}
export default function AdminCompanyDetailPage() {
const { id } = useParams<{ id: string }>()
const router = useRouter()
const [company, setCompany] = useState<CompanyDetail | null>(null)
const [form, setForm] = useState<FormState | null>(null)
const [auditLogs, setAuditLogs] = useState<AuditLog[]>([])
const [subEvents, setSubEvents] = useState<SubEvent[]>([])
const [subActioning, setSubActioning] = useState<string | null>(null)
const [subOverrideReason, setSubOverrideReason] = useState('')
const [showSubActions, setShowSubActions] = useState(false)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [actioning, setActioning] = useState(false)
const [saving, setSaving] = useState(false)
const [savedMessage, setSavedMessage] = useState<string | null>(null)
const [deleteConfirm, setDeleteConfirm] = useState(false)
async function fetchData() {
try {
const [cRes, aRes] = await Promise.all([
fetch(`${ADMIN_API_BASE}/admin/companies/${id}`, { cache: 'no-store', credentials: 'include' }),
fetch(`${ADMIN_API_BASE}/admin/audit-logs?entityId=${id}&pageSize=10`, { cache: 'no-store', credentials: 'include' }),
])
const cJson = await cRes.json()
const aJson = await aRes.json()
if (!cRes.ok) throw new Error(cJson?.message ?? 'Not found')
setCompany(cJson.data)
setForm(createFormState(cJson.data))
setAuditLogs(aJson.data?.data ?? [])
const subId = cJson.data?.subscription?.id
if (subId) {
const eRes = await fetch(`${ADMIN_API_BASE}/admin/subscriptions/${subId}/events`, { cache: 'no-store', credentials: 'include' })
const eJson = await eRes.json()
setSubEvents(Array.isArray(eJson.data) ? eJson.data : [])
}
setError(null)
} catch (err: any) {
setError(err.message)
} finally {
setLoading(false)
}
}
useEffect(() => { fetchData() }, [id])
function updateSection<K extends keyof FormState>(section: K, patch: Partial<FormState[K]>) {
setForm((current) => current ? { ...current, [section]: { ...current[section], ...patch } } : current)
}
async function saveChanges() {
if (!form) return
setSaving(true)
setSavedMessage(null)
setError(null)
try {
const res = await fetch(`${ADMIN_API_BASE}/admin/companies/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
company: {
name: form.company.name,
slug: form.company.slug,
email: form.company.email,
phone: toNullable(form.company.phone),
subscriptionPaymentRef: toNullable(form.company.subscriptionPaymentRef),
address: {
streetAddress: toNullable(form.brand.publicAddress),
city: toNullable(form.brand.publicCity),
country: toNullable(form.brand.publicCountry),
zipCode: toNullable(form.companyProfile.zipCode),
legalName: toNullable(form.contractSettings.legalName),
legalForm: toNullable(form.companyProfile.legalForm),
companyEmail: toNullable(form.company.email),
managerName: toNullable(form.companyProfile.managerName),
iceNumber: toNullable(form.companyProfile.iceNumber),
operatingLicenseNumber: toNullable(form.companyProfile.operatingLicenseNumber),
operatingLicenseIssuedAt: toNullable(form.companyProfile.operatingLicenseIssuedAt),
operatingLicenseIssuedBy: toNullable(form.companyProfile.operatingLicenseIssuedBy),
fax: toNullable(form.companyProfile.fax),
yearsActive: toNullable(form.companyProfile.yearsActive),
representativeName: toNullable(form.companyProfile.representativeName),
representativeTitle: toNullable(form.companyProfile.representativeTitle),
responsibleName: toNullable(form.companyProfile.responsibleName),
responsibleRole: toNullable(form.companyProfile.responsibleRole),
responsibleIdentityNumber: toNullable(form.companyProfile.responsibleIdentityNumber),
responsibleQualification: toNullable(form.companyProfile.responsibleQualification),
responsiblePhone: toNullable(form.companyProfile.responsiblePhone),
responsibleEmail: toNullable(form.companyProfile.responsibleEmail),
},
},
subscription: {
plan: form.subscription.plan,
billingPeriod: form.subscription.billingPeriod,
status: form.subscription.status,
currency: form.subscription.currency,
trialStartAt: toNullable(form.subscription.trialStartAt),
trialEndAt: toNullable(form.subscription.trialEndAt),
currentPeriodStart: toNullable(form.subscription.currentPeriodStart),
currentPeriodEnd: toNullable(form.subscription.currentPeriodEnd),
cancelledAt: toNullable(form.subscription.cancelledAt),
cancelAtPeriodEnd: form.subscription.cancelAtPeriodEnd,
},
brand: {
displayName: form.brand.displayName,
tagline: toNullable(form.brand.tagline),
subdomain: form.brand.subdomain,
customDomain: toNullable(form.brand.customDomain),
publicEmail: toNullable(form.brand.publicEmail),
publicPhone: toNullable(form.brand.publicPhone),
publicAddress: toNullable(form.brand.publicAddress),
publicCity: toNullable(form.brand.publicCity),
publicCountry: toNullable(form.brand.publicCountry),
websiteUrl: toNullable(form.brand.websiteUrl),
whatsappNumber: toNullable(form.brand.whatsappNumber),
defaultLocale: form.brand.defaultLocale,
defaultCurrency: form.brand.defaultCurrency,
isListedOnCarplace: form.brand.isListedOnCarplace,
},
contractSettings: {
legalName: toNullable(form.contractSettings.legalName),
registrationNumber: toNullable(form.contractSettings.registrationNumber),
taxId: toNullable(form.contractSettings.taxId),
terms: form.contractSettings.terms,
fuelPolicyType: form.contractSettings.fuelPolicyType,
lateFeePerHour: form.contractSettings.lateFeePerHour ? Number(form.contractSettings.lateFeePerHour) : null,
taxRate: form.contractSettings.taxRate ? Number(form.contractSettings.taxRate) : null,
signatureRequired: form.contractSettings.signatureRequired,
showTax: form.contractSettings.showTax,
},
accountingSettings: {
reportingPeriod: form.accountingSettings.reportingPeriod,
fiscalYearStart: Number(form.accountingSettings.fiscalYearStart || 1),
currency: form.accountingSettings.currency,
accountantEmail: toNullable(form.accountingSettings.accountantEmail),
accountantName: toNullable(form.accountingSettings.accountantName),
autoSendReport: form.accountingSettings.autoSendReport,
reportFormat: form.accountingSettings.reportFormat,
},
}),
})
const json = await res.json()
if (!res.ok) throw new Error(json?.message ?? 'Save failed')
setCompany(json.data)
setForm(createFormState(json.data))
setSavedMessage('Company settings updated.')
await fetchData()
} catch (err: any) {
setError(err.message)
} finally {
setSaving(false)
}
}
async function doSubAction(action: string) {
if (!company?.subscription?.id) return
if (!subOverrideReason.trim()) { setError('A reason is required for subscription overrides'); return }
setSubActioning(action)
setError(null)
try {
const res = await fetch(`${ADMIN_API_BASE}/admin/subscriptions/${company.subscription.id}/${action}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ reason: subOverrideReason }),
})
const json = await res.json()
if (!res.ok) throw new Error(json?.message ?? 'Action failed')
setSubOverrideReason('')
setShowSubActions(false)
await fetchData()
} catch (err: any) {
setError(err.message)
} finally {
setSubActioning(null)
}
}
async function changeStatus(status: string) {
setActioning(true)
setError(null)
try {
const res = await fetch(`${ADMIN_API_BASE}/admin/companies/${id}/status`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ status }),
})
const json = await res.json()
if (!res.ok) throw new Error(json?.message ?? 'Action failed')
await fetchData()
} catch (err: any) {
setError(err.message)
} finally {
setActioning(false)
}
}
async function deleteCompany() {
setActioning(true)
try {
const res = await fetch(`${ADMIN_API_BASE}/admin/companies/${id}`, {
method: 'DELETE',
credentials: 'include',
})
const json = await res.json()
if (!res.ok) throw new Error(json?.message ?? 'Delete failed')
router.push('/dashboard/companies')
} catch (err: any) {
setError(err.message)
setActioning(false)
}
}
if (loading) return <div className="shell py-16 text-center text-zinc-500">Loading</div>
if (error && !company) return <div className="shell py-16 text-center text-red-400">{error}</div>
if (!company || !form) return null
const counts = {
employees: company._count?.employees ?? company.employees?.length ?? 0,
vehicles: company._count?.vehicles ?? company.vehicles?.length ?? 0,
customers: company._count?.customers ?? company.customers?.length ?? 0,
reservations: company._count?.reservations ?? company.reservations?.length ?? 0,
}
return (
<div className="shell space-y-6 py-8">
<div className="flex items-center justify-between gap-3">
<Link href="/dashboard/companies" className="text-sm text-zinc-500 hover:text-zinc-300"> Companies</Link>
<button
onClick={saveChanges}
disabled={saving}
className="rounded-xl bg-emerald-600 px-4 py-2 text-sm font-semibold text-white transition-colors hover:bg-emerald-500 disabled:opacity-50"
>
{saving ? 'Saving…' : 'Save changes'}
</button>
</div>
<div className="flex items-start justify-between gap-4">
<div>
<h1 className="text-3xl font-black">{company.name}</h1>
<p className="mt-1 font-mono text-sm text-zinc-400">{company.slug}</p>
</div>
<span className={`text-sm font-semibold ${STATUS_COLORS[company.status] ?? 'text-zinc-400'}`}>{company.status}</span>
</div>
{error && <div className="panel p-4 text-sm text-red-400">{error}</div>}
{savedMessage && <div className="panel p-4 text-sm text-emerald-400">{savedMessage}</div>}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
{[
{ label: 'Employees', value: counts.employees },
{ label: 'Vehicles', value: counts.vehicles },
{ label: 'Customers', value: counts.customers },
{ label: 'Reservations', value: counts.reservations },
].map((kpi) => (
<div key={kpi.label} className="panel p-5">
<p className="text-xs text-zinc-500">{kpi.label}</p>
<p className="mt-1 text-2xl font-black">{kpi.value}</p>
</div>
))}
</div>
<div className="grid gap-6 xl:grid-cols-2">
<section className="panel p-6">
<h2 className="text-base font-semibold">Company</h2>
<div className="mt-4 grid gap-4 md:grid-cols-2">
<label>
<span className={LABEL_CLASS}>Name</span>
<input className={INPUT_CLASS} value={form.company.name} onChange={(e) => updateSection('company', { name: e.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>Slug</span>
<input className={INPUT_CLASS} value={form.company.slug} onChange={(e) => updateSection('company', { slug: e.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>Email</span>
<input className={INPUT_CLASS} type="email" value={form.company.email} onChange={(e) => updateSection('company', { email: e.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>Phone</span>
<input className={INPUT_CLASS} value={form.company.phone} onChange={(e) => updateSection('company', { phone: e.target.value })} />
</label>
<div>
<span className={LABEL_CLASS}>Company status</span>
<p className={`mt-1 text-sm font-semibold ${STATUS_COLORS[form.company.status] ?? 'text-zinc-400'}`}>{form.company.status}</p>
</div>
<label>
<span className={LABEL_CLASS}>Subscription payment ref</span>
<input className={INPUT_CLASS} value={form.company.subscriptionPaymentRef} onChange={(e) => updateSection('company', { subscriptionPaymentRef: e.target.value })} />
</label>
</div>
</section>
<section className="panel p-6">
<h2 className="text-base font-semibold">Subscription</h2>
<div className="mt-4 grid gap-4 md:grid-cols-2">
<label>
<span className={LABEL_CLASS}>Plan</span>
<select className={INPUT_CLASS} value={form.subscription.plan} onChange={(e) => updateSection('subscription', { plan: e.target.value })}>
<option value="STARTER">STARTER</option>
<option value="GROWTH">GROWTH</option>
<option value="PRO">PRO</option>
</select>
</label>
<label>
<span className={LABEL_CLASS}>Billing period</span>
<select className={INPUT_CLASS} value={form.subscription.billingPeriod} onChange={(e) => updateSection('subscription', { billingPeriod: e.target.value })}>
<option value="MONTHLY">MONTHLY</option>
<option value="ANNUAL">ANNUAL</option>
</select>
</label>
<div>
<span className={LABEL_CLASS}>Status</span>
<p className={`mt-1 text-sm font-semibold ${STATUS_COLORS[form.subscription.status] ?? 'text-zinc-400'}`}>{form.subscription.status}</p>
{company.subscription?.paymentPendingSince && (
<p className="mt-0.5 text-xs text-zinc-500">Pending since {new Date(company.subscription.paymentPendingSince).toLocaleDateString()}</p>
)}
{company.subscription?.pastDueSince && (
<p className="mt-0.5 text-xs text-orange-500">Past due since {new Date(company.subscription.pastDueSince).toLocaleDateString()}</p>
)}
{company.subscription?.suspendedAt && (
<p className="mt-0.5 text-xs text-red-500">Suspended {new Date(company.subscription.suspendedAt).toLocaleDateString()}</p>
)}
</div>
<label>
<span className={LABEL_CLASS}>Currency</span>
<select className={INPUT_CLASS} value={form.subscription.currency} onChange={(e) => updateSection('subscription', { currency: e.target.value })}>
<option value="MAD">MAD</option>
<option value="USD">USD</option>
<option value="EUR">EUR</option>
</select>
</label>
<label>
<span className={LABEL_CLASS}>Trial start</span>
<input className={INPUT_CLASS} type="date" value={form.subscription.trialStartAt} onChange={(e) => updateSection('subscription', { trialStartAt: e.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>Trial end</span>
<input className={INPUT_CLASS} type="date" value={form.subscription.trialEndAt} onChange={(e) => updateSection('subscription', { trialEndAt: e.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>Period start</span>
<input className={INPUT_CLASS} type="date" value={form.subscription.currentPeriodStart} onChange={(e) => updateSection('subscription', { currentPeriodStart: e.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>Period end</span>
<input className={INPUT_CLASS} type="date" value={form.subscription.currentPeriodEnd} onChange={(e) => updateSection('subscription', { currentPeriodEnd: e.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>Cancelled at</span>
<input className={INPUT_CLASS} type="date" value={form.subscription.cancelledAt} onChange={(e) => updateSection('subscription', { cancelledAt: e.target.value })} />
</label>
<label className="flex items-center gap-3 pt-7 text-sm text-zinc-300">
<input type="checkbox" checked={form.subscription.cancelAtPeriodEnd} onChange={(e) => updateSection('subscription', { cancelAtPeriodEnd: e.target.checked })} />
Cancel at period end
</label>
</div>
</section>
<section className="panel p-6">
<h2 className="text-base font-semibold">Brand and public profile</h2>
<div className="mt-4 grid gap-4 md:grid-cols-2">
<label>
<span className={LABEL_CLASS}>Display name</span>
<input className={INPUT_CLASS} value={form.brand.displayName} onChange={(e) => updateSection('brand', { displayName: e.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>Subdomain</span>
<input className={INPUT_CLASS} value={form.brand.subdomain} onChange={(e) => updateSection('brand', { subdomain: e.target.value })} />
</label>
<label className="md:col-span-2">
<span className={LABEL_CLASS}>Tagline</span>
<input className={INPUT_CLASS} value={form.brand.tagline} onChange={(e) => updateSection('brand', { tagline: e.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>Public email</span>
<input className={INPUT_CLASS} type="email" value={form.brand.publicEmail} onChange={(e) => updateSection('brand', { publicEmail: e.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>Public phone</span>
<input className={INPUT_CLASS} value={form.brand.publicPhone} onChange={(e) => updateSection('brand', { publicPhone: e.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>Custom domain</span>
<input className={INPUT_CLASS} value={form.brand.customDomain} onChange={(e) => updateSection('brand', { customDomain: e.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>Website URL</span>
<input className={INPUT_CLASS} type="url" value={form.brand.websiteUrl} onChange={(e) => updateSection('brand', { websiteUrl: e.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>WhatsApp</span>
<input className={INPUT_CLASS} value={form.brand.whatsappNumber} onChange={(e) => updateSection('brand', { whatsappNumber: e.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>Locale</span>
<input className={INPUT_CLASS} value={form.brand.defaultLocale} onChange={(e) => updateSection('brand', { defaultLocale: e.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>Brand currency</span>
<select className={INPUT_CLASS} value={form.brand.defaultCurrency} onChange={(e) => updateSection('brand', { defaultCurrency: e.target.value })}>
<option value="MAD">MAD</option>
<option value="USD">USD</option>
<option value="EUR">EUR</option>
</select>
</label>
<label>
<span className={LABEL_CLASS}>City</span>
<input className={INPUT_CLASS} value={form.brand.publicCity} onChange={(e) => updateSection('brand', { publicCity: e.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>Country</span>
<input className={INPUT_CLASS} value={form.brand.publicCountry} onChange={(e) => updateSection('brand', { publicCountry: e.target.value })} />
</label>
<label className="md:col-span-2">
<span className={LABEL_CLASS}>Address</span>
<input className={INPUT_CLASS} value={form.brand.publicAddress} onChange={(e) => updateSection('brand', { publicAddress: e.target.value })} />
</label>
<label className="flex items-center gap-3 pt-7 text-sm text-zinc-300">
<input type="checkbox" checked={form.brand.isListedOnCarplace} onChange={(e) => updateSection('brand', { isListedOnCarplace: e.target.checked })} />
Listed on Carplace
</label>
</div>
</section>
<section className="panel p-6">
<h2 className="text-base font-semibold">Company legal profile</h2>
<div className="mt-4 grid gap-6">
<div className="grid gap-4 md:grid-cols-2">
<label>
<span className={LABEL_CLASS}>Legal form</span>
<input className={INPUT_CLASS} value={form.companyProfile.legalForm} onChange={(e) => updateSection('companyProfile', { legalForm: e.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>Manager / owner name</span>
<input className={INPUT_CLASS} value={form.companyProfile.managerName} onChange={(e) => updateSection('companyProfile', { managerName: e.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>ZIP / postal code</span>
<input className={INPUT_CLASS} value={form.companyProfile.zipCode} onChange={(e) => updateSection('companyProfile', { zipCode: e.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>Fax</span>
<input className={INPUT_CLASS} value={form.companyProfile.fax} onChange={(e) => updateSection('companyProfile', { fax: e.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>Years active</span>
<input className={INPUT_CLASS} value={form.companyProfile.yearsActive} onChange={(e) => updateSection('companyProfile', { yearsActive: e.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>ICE number</span>
<input className={INPUT_CLASS} value={form.companyProfile.iceNumber} onChange={(e) => updateSection('companyProfile', { iceNumber: e.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>Operating license number</span>
<input className={INPUT_CLASS} value={form.companyProfile.operatingLicenseNumber} onChange={(e) => updateSection('companyProfile', { operatingLicenseNumber: e.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>Operating license issue date</span>
<input className={INPUT_CLASS} value={form.companyProfile.operatingLicenseIssuedAt} onChange={(e) => updateSection('companyProfile', { operatingLicenseIssuedAt: e.target.value })} />
</label>
<label className="md:col-span-2">
<span className={LABEL_CLASS}>Issuing authority</span>
<input className={INPUT_CLASS} value={form.companyProfile.operatingLicenseIssuedBy} onChange={(e) => updateSection('companyProfile', { operatingLicenseIssuedBy: e.target.value })} />
</label>
</div>
<div className="border-t border-zinc-800 pt-6">
<h3 className="text-sm font-semibold text-zinc-200">Legal representative</h3>
<div className="mt-4 grid gap-4 md:grid-cols-2">
<label>
<span className={LABEL_CLASS}>Representative name</span>
<input className={INPUT_CLASS} value={form.companyProfile.representativeName} onChange={(e) => updateSection('companyProfile', { representativeName: e.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>Representative title</span>
<input className={INPUT_CLASS} value={form.companyProfile.representativeTitle} onChange={(e) => updateSection('companyProfile', { representativeTitle: e.target.value })} />
</label>
</div>
</div>
<div className="border-t border-zinc-800 pt-6">
<h3 className="text-sm font-semibold text-zinc-200">Responsible person</h3>
<div className="mt-4 grid gap-4 md:grid-cols-2">
<label>
<span className={LABEL_CLASS}>Responsible name</span>
<input className={INPUT_CLASS} value={form.companyProfile.responsibleName} onChange={(e) => updateSection('companyProfile', { responsibleName: e.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>Responsible role</span>
<input className={INPUT_CLASS} value={form.companyProfile.responsibleRole} onChange={(e) => updateSection('companyProfile', { responsibleRole: e.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>Identity document number</span>
<input className={INPUT_CLASS} value={form.companyProfile.responsibleIdentityNumber} onChange={(e) => updateSection('companyProfile', { responsibleIdentityNumber: e.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>Qualification / diploma / experience</span>
<input className={INPUT_CLASS} value={form.companyProfile.responsibleQualification} onChange={(e) => updateSection('companyProfile', { responsibleQualification: e.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>Responsible phone</span>
<input className={INPUT_CLASS} value={form.companyProfile.responsiblePhone} onChange={(e) => updateSection('companyProfile', { responsiblePhone: e.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>Responsible email</span>
<input className={INPUT_CLASS} type="email" value={form.companyProfile.responsibleEmail} onChange={(e) => updateSection('companyProfile', { responsibleEmail: e.target.value })} />
</label>
</div>
</div>
</div>
</section>
<section className="panel p-6">
<h2 className="text-base font-semibold">Operations and finance</h2>
<div className="mt-4 grid gap-6">
<div className="grid gap-4 md:grid-cols-2">
<label>
<span className={LABEL_CLASS}>Legal name</span>
<input className={INPUT_CLASS} value={form.contractSettings.legalName} onChange={(e) => updateSection('contractSettings', { legalName: e.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>Registration number</span>
<input className={INPUT_CLASS} value={form.contractSettings.registrationNumber} onChange={(e) => updateSection('contractSettings', { registrationNumber: e.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>Tax ID</span>
<input className={INPUT_CLASS} value={form.contractSettings.taxId} onChange={(e) => updateSection('contractSettings', { taxId: e.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>Fuel policy type</span>
<select className={INPUT_CLASS} value={form.contractSettings.fuelPolicyType} onChange={(e) => updateSection('contractSettings', { fuelPolicyType: e.target.value })}>
<option value="FULL_TO_FULL">FULL_TO_FULL</option>
<option value="FULL_TO_EMPTY">FULL_TO_EMPTY</option>
<option value="SAME_TO_SAME">SAME_TO_SAME</option>
<option value="PREPAID">PREPAID</option>
<option value="FREE">FREE</option>
</select>
</label>
<label>
<span className={LABEL_CLASS}>Late fee per hour</span>
<input className={INPUT_CLASS} type="number" value={form.contractSettings.lateFeePerHour} onChange={(e) => updateSection('contractSettings', { lateFeePerHour: e.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>Tax rate</span>
<input className={INPUT_CLASS} type="number" step="0.01" value={form.contractSettings.taxRate} onChange={(e) => updateSection('contractSettings', { taxRate: e.target.value })} />
</label>
<label className="flex items-center gap-3 pt-7 text-sm text-zinc-300">
<input type="checkbox" checked={form.contractSettings.signatureRequired} onChange={(e) => updateSection('contractSettings', { signatureRequired: e.target.checked })} />
Signature required
</label>
<label className="flex items-center gap-3 pt-7 text-sm text-zinc-300">
<input type="checkbox" checked={form.contractSettings.showTax} onChange={(e) => updateSection('contractSettings', { showTax: e.target.checked })} />
Show tax on documents
</label>
<label className="md:col-span-2">
<span className={LABEL_CLASS}>Terms</span>
<textarea className={`${INPUT_CLASS} min-h-28`} value={form.contractSettings.terms} onChange={(e) => updateSection('contractSettings', { terms: e.target.value })} />
</label>
</div>
<div className="border-t border-zinc-800 pt-6">
<h3 className="text-sm font-semibold text-zinc-200">Accounting settings</h3>
<div className="mt-4 grid gap-4 md:grid-cols-2">
<label>
<span className={LABEL_CLASS}>Reporting period</span>
<select className={INPUT_CLASS} value={form.accountingSettings.reportingPeriod} onChange={(e) => updateSection('accountingSettings', { reportingPeriod: e.target.value })}>
<option value="WEEKLY">WEEKLY</option>
<option value="MONTHLY">MONTHLY</option>
<option value="QUARTERLY">QUARTERLY</option>
<option value="ANNUAL">ANNUAL</option>
</select>
</label>
<label>
<span className={LABEL_CLASS}>Fiscal year start month</span>
<input className={INPUT_CLASS} type="number" min="1" max="12" value={form.accountingSettings.fiscalYearStart} onChange={(e) => updateSection('accountingSettings', { fiscalYearStart: e.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>Accounting currency</span>
<select className={INPUT_CLASS} value={form.accountingSettings.currency} onChange={(e) => updateSection('accountingSettings', { currency: e.target.value })}>
<option value="MAD">MAD</option>
<option value="USD">USD</option>
<option value="EUR">EUR</option>
</select>
</label>
<label>
<span className={LABEL_CLASS}>Report format</span>
<select className={INPUT_CLASS} value={form.accountingSettings.reportFormat} onChange={(e) => updateSection('accountingSettings', { reportFormat: e.target.value })}>
<option value="PDF">PDF</option>
<option value="CSV">CSV</option>
<option value="BOTH">BOTH</option>
</select>
</label>
<label>
<span className={LABEL_CLASS}>Accountant name</span>
<input className={INPUT_CLASS} value={form.accountingSettings.accountantName} onChange={(e) => updateSection('accountingSettings', { accountantName: e.target.value })} />
</label>
<label>
<span className={LABEL_CLASS}>Accountant email</span>
<input className={INPUT_CLASS} type="email" value={form.accountingSettings.accountantEmail} onChange={(e) => updateSection('accountingSettings', { accountantEmail: e.target.value })} />
</label>
<label className="flex items-center gap-3 pt-7 text-sm text-zinc-300">
<input type="checkbox" checked={form.accountingSettings.autoSendReport} onChange={(e) => updateSection('accountingSettings', { autoSendReport: e.target.checked })} />
Auto-send reports
</label>
</div>
</div>
</div>
</section>
</div>
<div className="grid gap-6 lg:grid-cols-2">
<div className="space-y-4">
<div className="panel p-6 space-y-4">
<h2 className="text-base font-semibold">Company actions</h2>
<div className="space-y-3">
{company.status === 'SUSPENDED' ? (
<button onClick={() => changeStatus('ACTIVE')} disabled={actioning} className="w-full rounded-xl bg-emerald-900/40 py-2.5 text-sm font-semibold text-emerald-400 transition-colors hover:bg-emerald-900/60 disabled:opacity-50">
Reactivate company
</button>
) : (
<button onClick={() => changeStatus('SUSPENDED')} disabled={actioning} className="w-full rounded-xl bg-red-950/40 py-2.5 text-sm font-semibold text-red-400 transition-colors hover:bg-red-950/60 disabled:opacity-50">
Suspend company
</button>
)}
{!deleteConfirm ? (
<button onClick={() => setDeleteConfirm(true)} className="w-full rounded-xl bg-zinc-800 py-2.5 text-sm font-semibold text-zinc-400 transition-colors hover:bg-zinc-700">
Delete company
</button>
) : (
<div className="space-y-3 rounded-xl border border-red-900/50 p-4">
<p className="text-sm font-medium text-red-400">This will permanently delete all company data. Are you sure?</p>
<div className="flex gap-2">
<button onClick={() => setDeleteConfirm(false)} className="flex-1 rounded-lg bg-zinc-800 py-2 text-sm text-zinc-300">Cancel</button>
<button onClick={deleteCompany} disabled={actioning} className="flex-1 rounded-lg bg-red-700 py-2 text-sm font-semibold text-white hover:bg-red-600 disabled:opacity-50">Delete</button>
</div>
</div>
)}
</div>
</div>
{company.subscription && (
<div className="panel p-6 space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-base font-semibold">Subscription overrides</h2>
<button onClick={() => setShowSubActions((v) => !v)} className="text-xs text-zinc-400 hover:text-zinc-200">
{showSubActions ? 'Hide' : 'Show'}
</button>
</div>
{showSubActions && (
<div className="space-y-3">
<div>
<label className="text-xs font-medium uppercase tracking-wide text-zinc-500">Reason (required)</label>
<input
className="mt-1 w-full rounded-xl border border-zinc-700 bg-zinc-900 px-3 py-2 text-sm text-zinc-100 outline-none focus:border-emerald-500"
placeholder="e.g. Customer requested extension…"
value={subOverrideReason}
onChange={(e) => setSubOverrideReason(e.target.value)}
/>
</div>
<div className="grid grid-cols-2 gap-2">
<button onClick={() => doSubAction('reactivate')} disabled={!!subActioning} className="rounded-lg bg-emerald-950/50 py-2 text-xs font-semibold text-emerald-400 hover:bg-emerald-900/50 disabled:opacity-50">
{subActioning === 'reactivate' ? '…' : 'Reactivate'}
</button>
<button onClick={() => doSubAction('extend-grace-period')} disabled={!!subActioning} className="rounded-lg bg-sky-950/50 py-2 text-xs font-semibold text-sky-400 hover:bg-sky-900/50 disabled:opacity-50">
{subActioning === 'extend-grace-period' ? '…' : '+7d grace'}
</button>
<button onClick={() => doSubAction('suspend')} disabled={!!subActioning} className="rounded-lg bg-orange-950/50 py-2 text-xs font-semibold text-orange-400 hover:bg-orange-900/50 disabled:opacity-50">
{subActioning === 'suspend' ? '…' : 'Suspend'}
</button>
<button onClick={() => doSubAction('cancel')} disabled={!!subActioning} className="rounded-lg bg-red-950/50 py-2 text-xs font-semibold text-red-400 hover:bg-red-900/50 disabled:opacity-50">
{subActioning === 'cancel' ? '…' : 'Cancel'}
</button>
</div>
<p className="text-xs text-zinc-600">Retry count: {company.subscription.retryCount ?? 0} / 5</p>
</div>
)}
</div>
)}
</div>
<div className="space-y-4">
{subEvents.length > 0 && (
<div className="panel overflow-hidden">
<div className="border-b border-zinc-800 px-6 py-4">
<h2 className="text-sm font-semibold">Subscription timeline</h2>
</div>
<div className="divide-y divide-zinc-800/60 max-h-72 overflow-y-auto">
{subEvents.map((ev) => (
<div key={ev.id} className="px-6 py-3">
<div className="flex items-center justify-between">
<span className="text-xs font-medium text-sky-400">{ev.eventType}</span>
<span className="text-xs text-zinc-600">{new Date(ev.occurredAt).toLocaleString()}</span>
</div>
<p className="mt-0.5 text-xs text-zinc-500">source: {ev.source}</p>
</div>
))}
</div>
</div>
)}
{auditLogs.length > 0 && (
<div className="panel overflow-hidden">
<div className="border-b border-zinc-800 px-6 py-4">
<h2 className="text-sm font-semibold">Recent audit events</h2>
</div>
<div className="divide-y divide-zinc-800/60">
{auditLogs.map((log) => (
<div key={log.id} className="flex items-center justify-between px-6 py-3 text-sm">
<div>
<span className="font-medium text-zinc-200">{log.action}</span>
<span className="ml-2 text-zinc-500">{log.resource}</span>
{log.adminUser?.email ? <span className="ml-2 text-zinc-600">{log.adminUser.email}</span> : null}
</div>
<span className="text-xs text-zinc-600">{new Date(log.createdAt).toLocaleString()}</span>
</div>
))}
</div>
</div>
)}
</div>
</div>
</div>
)
}