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
1032 lines
50 KiB
TypeScript
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>
|
|
)
|
|
}
|