fixing platform admin

This commit is contained in:
root
2026-05-06 22:58:23 -04:00
parent 695a7f7cc7
commit 750ae56a29
175 changed files with 31249 additions and 328 deletions
@@ -0,0 +1,204 @@
'use client'
import { useEffect, useState } from 'react'
import { useParams, useRouter } from 'next/navigation'
import Link from 'next/link'
const API_BASE = '/api/v1'
interface CompanyDetail {
id: string
name: string
slug: string
email: string
phone: string | null
status: string
createdAt: string
subscription: { plan: string; status: string; trialEndAt: string | null } | null
_count: { employees: number; vehicles: number; customers: number; reservations: number }
}
interface AuditLog {
id: string
action: string
entityType: string
entityId: string
createdAt: string
admin: { email: string } | null
}
const STATUS_COLORS: Record<string, string> = {
ACTIVE: 'text-emerald-400',
TRIALING: 'text-sky-400',
SUSPENDED: 'text-red-400',
PENDING: 'text-amber-400',
CANCELLED: 'text-zinc-400',
}
export default function AdminCompanyDetailPage() {
const { id } = useParams<{ id: string }>()
const router = useRouter()
const [company, setCompany] = useState<CompanyDetail | null>(null)
const [auditLogs, setAuditLogs] = useState<AuditLog[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [actioning, setActioning] = useState(false)
const [deleteConfirm, setDeleteConfirm] = useState(false)
function getToken() { return localStorage.getItem('admin_token') ?? '' }
async function fetchData() {
const token = getToken()
const headers = { Authorization: `Bearer ${token}` }
try {
const [cRes, aRes] = await Promise.all([
fetch(`${API_BASE}/admin/companies/${id}`, { headers, cache: 'no-store' }),
fetch(`${API_BASE}/admin/audit-logs?entityId=${id}&pageSize=10`, { headers, cache: 'no-store' }),
])
const cJson = await cRes.json()
const aJson = await aRes.json()
if (!cRes.ok) throw new Error(cJson?.message ?? 'Not found')
setCompany(cJson.data)
setAuditLogs(aJson.data ?? [])
} catch (err: any) {
setError(err.message)
} finally {
setLoading(false)
}
}
useEffect(() => { fetchData() }, [id])
async function changeStatus(status: string) {
setActioning(true)
try {
const res = await fetch(`${API_BASE}/admin/companies/${id}/status`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${getToken()}` },
body: JSON.stringify({ status }),
})
if (!res.ok) throw new Error('Action failed')
await fetchData()
} catch (err: any) {
setError(err.message)
} finally {
setActioning(false)
}
}
async function deleteCompany() {
setActioning(true)
try {
const res = await fetch(`${API_BASE}/admin/companies/${id}`, {
method: 'DELETE',
headers: { Authorization: `Bearer ${getToken()}` },
})
if (!res.ok) throw new Error('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) return null
return (
<div className="shell py-8 space-y-6">
<div className="flex items-center gap-3">
<Link href="/dashboard/companies" className="text-zinc-500 hover:text-zinc-300 text-sm"> Companies</Link>
</div>
<div className="flex items-start justify-between">
<div>
<h1 className="text-3xl font-black">{company.name}</h1>
<p className="mt-1 text-zinc-400 font-mono text-sm">{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>}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
{[
{ label: 'Employees', value: company._count.employees },
{ label: 'Vehicles', value: company._count.vehicles },
{ label: 'Customers', value: company._count.customers },
{ label: 'Reservations', value: company._count.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 lg:grid-cols-2">
<div className="panel p-6 space-y-4">
<h2 className="text-base font-semibold">Details</h2>
<dl className="space-y-3 text-sm">
<div className="flex justify-between"><dt className="text-zinc-500">Email</dt><dd>{company.email}</dd></div>
<div className="flex justify-between"><dt className="text-zinc-500">Phone</dt><dd>{company.phone ?? '—'}</dd></div>
<div className="flex justify-between"><dt className="text-zinc-500">Joined</dt><dd>{new Date(company.createdAt).toLocaleDateString()}</dd></div>
{company.subscription && <>
<div className="flex justify-between"><dt className="text-zinc-500">Plan</dt><dd>{company.subscription.plan}</dd></div>
<div className="flex justify-between"><dt className="text-zinc-500">Sub status</dt><dd>{company.subscription.status}</dd></div>
{company.subscription.trialEndAt && (
<div className="flex justify-between"><dt className="text-zinc-500">Trial ends</dt><dd>{new Date(company.subscription.trialEndAt).toLocaleDateString()}</dd></div>
)}
</>}
</dl>
</div>
<div className="panel p-6 space-y-4">
<h2 className="text-base font-semibold">Actions</h2>
<div className="space-y-3">
{company.status === 'SUSPENDED' ? (
<button onClick={() => changeStatus('ACTIVE')} disabled={actioning} className="w-full py-2.5 rounded-xl bg-emerald-900/40 hover:bg-emerald-900/60 text-emerald-400 text-sm font-semibold transition-colors disabled:opacity-50">
Reactivate company
</button>
) : (
<button onClick={() => changeStatus('SUSPENDED')} disabled={actioning} className="w-full py-2.5 rounded-xl bg-red-950/40 hover:bg-red-950/60 text-red-400 text-sm font-semibold transition-colors disabled:opacity-50">
Suspend company
</button>
)}
{!deleteConfirm ? (
<button onClick={() => setDeleteConfirm(true)} className="w-full py-2.5 rounded-xl bg-zinc-800 hover:bg-zinc-700 text-zinc-400 text-sm font-semibold transition-colors">
Delete company
</button>
) : (
<div className="rounded-xl border border-red-900/50 p-4 space-y-3">
<p className="text-sm text-red-400 font-medium">This will permanently delete all company data. Are you sure?</p>
<div className="flex gap-2">
<button onClick={() => setDeleteConfirm(false)} className="flex-1 py-2 rounded-lg bg-zinc-800 text-zinc-300 text-sm">Cancel</button>
<button onClick={deleteCompany} disabled={actioning} className="flex-1 py-2 rounded-lg bg-red-700 hover:bg-red-600 text-white text-sm font-semibold disabled:opacity-50">Delete</button>
</div>
</div>
)}
</div>
</div>
</div>
{auditLogs.length > 0 && (
<div className="panel overflow-hidden">
<div className="px-6 py-4 border-b border-zinc-800">
<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="px-6 py-3 flex items-center justify-between text-sm">
<div>
<span className="font-medium text-zinc-200">{log.action}</span>
<span className="ml-2 text-zinc-500">{log.entityType}</span>
</div>
<span className="text-xs text-zinc-600">{new Date(log.createdAt).toLocaleString()}</span>
</div>
))}
</div>
</div>
)}
</div>
)
}