initial: standalone repo from monorepo split
This commit is contained in:
150
src/app/(admin)/tenants/page.tsx
Normal file
150
src/app/(admin)/tenants/page.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import api from '@/lib/api';
|
||||
|
||||
interface Tenant {
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
isActive: boolean;
|
||||
createdAt: string;
|
||||
_count: { users: number; clients: number; subscriptions: number };
|
||||
}
|
||||
|
||||
export default function TenantsPage() {
|
||||
const [tenants, setTenants] = useState<Tenant[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [search, setSearch] = useState('');
|
||||
const [status, setStatus] = useState<string>('all');
|
||||
const [page, setPage] = useState(1);
|
||||
const [total, setTotal] = useState(0);
|
||||
const limit = 20;
|
||||
|
||||
useEffect(() => {
|
||||
loadTenants();
|
||||
}, [search, status, page]);
|
||||
|
||||
async function loadTenants() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const params = new URLSearchParams({ page: String(page), limit: String(limit) });
|
||||
if (search) params.set('search', search);
|
||||
if (status !== 'all') params.set('status', status);
|
||||
|
||||
const res = await api.get(`/tenants?${params}`);
|
||||
setTenants(res.data.data.items);
|
||||
setTotal(res.data.data.total);
|
||||
} catch (err) {
|
||||
console.error('Failed to load tenants:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
const totalPages = Math.ceil(total / limit);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-surface-900">Tenants</h2>
|
||||
<p className="text-sm text-surface-500">{total} total tenants</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/tenants/new"
|
||||
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 text-sm font-medium"
|
||||
>
|
||||
New Tenant
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search tenants..."
|
||||
value={search}
|
||||
onChange={(e) => { setSearch(e.target.value); setPage(1); }}
|
||||
className="px-3 py-2 border border-surface-300 rounded-lg text-sm flex-1 max-w-xs"
|
||||
/>
|
||||
<select
|
||||
value={status}
|
||||
onChange={(e) => { setStatus(e.target.value); setPage(1); }}
|
||||
className="px-3 py-2 border border-surface-300 rounded-lg text-sm"
|
||||
>
|
||||
<option value="all">All Status</option>
|
||||
<option value="active">Active</option>
|
||||
<option value="inactive">Inactive</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl border border-surface-200">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-surface-200 bg-surface-50">
|
||||
<th className="text-left px-4 py-3 font-medium text-surface-600">Name</th>
|
||||
<th className="text-left px-4 py-3 font-medium text-surface-600">Slug</th>
|
||||
<th className="text-center px-4 py-3 font-medium text-surface-600">Status</th>
|
||||
<th className="text-center px-4 py-3 font-medium text-surface-600">Users</th>
|
||||
<th className="text-center px-4 py-3 font-medium text-surface-600">Clients</th>
|
||||
<th className="text-center px-4 py-3 font-medium text-surface-600">Subscriptions</th>
|
||||
<th className="text-left px-4 py-3 font-medium text-surface-600">Created</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{loading ? (
|
||||
<tr><td colSpan={7} className="text-center py-8 text-surface-400">Loading...</td></tr>
|
||||
) : tenants.length === 0 ? (
|
||||
<tr><td colSpan={7} className="text-center py-8 text-surface-400">No tenants found</td></tr>
|
||||
) : (
|
||||
tenants.map((t) => (
|
||||
<tr key={t.id} className="border-b border-surface-100 hover:bg-surface-50">
|
||||
<td className="px-4 py-3">
|
||||
<Link href={`/tenants/${t.id}`} className="text-primary-600 hover:underline font-medium">
|
||||
{t.name}
|
||||
</Link>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-surface-500">{t.slug}</td>
|
||||
<td className="px-4 py-3 text-center">
|
||||
<span className={`inline-block px-2 py-0.5 rounded-full text-xs font-medium ${
|
||||
t.isActive ? 'bg-green-50 text-green-700' : 'bg-red-50 text-red-700'
|
||||
}`}>
|
||||
{t.isActive ? 'Active' : 'Inactive'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-center">{t._count.users}</td>
|
||||
<td className="px-4 py-3 text-center">{t._count.clients}</td>
|
||||
<td className="px-4 py-3 text-center">{t._count.subscriptions}</td>
|
||||
<td className="px-4 py-3 text-surface-500">
|
||||
{new Date(t.createdAt).toLocaleDateString()}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{totalPages > 1 && (
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<button
|
||||
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
||||
disabled={page === 1}
|
||||
className="px-3 py-1 text-sm border rounded-lg disabled:opacity-50"
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
<span className="text-sm text-surface-500">Page {page} of {totalPages}</span>
|
||||
<button
|
||||
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
|
||||
disabled={page === totalPages}
|
||||
className="px-3 py-1 text-sm border rounded-lg disabled:opacity-50"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user