feat: bot assign UI in super admin bot pool page

Click "Assign" on any bot row to expand an inline form with a chapter
dropdown. Selecting a chapter and confirming calls POST /api/admin/bots/:id/assign
with the tenantId. The chapter list is loaded alongside bots on mount.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-20 17:37:08 +05:30
parent 8d720278cb
commit f8b7afcc38
+94 -24
View File
@@ -8,15 +8,23 @@ export default function BotsPage() {
const { admin, loading } = useSuperAdmin(); const { admin, loading } = useSuperAdmin();
const router = useRouter(); const router = useRouter();
const [bots, setBots] = useState<any[]>([]); const [bots, setBots] = useState<any[]>([]);
const [tenants, setTenants] = useState<any[]>([]);
const [initiating, setInitiating] = useState(false); const [initiating, setInitiating] = useState(false);
const [pairingInfo, setPairingInfo] = useState<{ pairingToken: string; expiresAt: string } | null>(null); const [pairingInfo, setPairingInfo] = useState<{ pairingToken: string; expiresAt: string } | null>(null);
const [qrDataUrl, setQrDataUrl] = useState<string | null>(null); const [qrDataUrl, setQrDataUrl] = useState<string | null>(null);
const [qrStatus, setQrStatus] = useState<string>('PAIRING'); const [qrStatus, setQrStatus] = useState<string>('PAIRING');
const [assigningBotId, setAssigningBotId] = useState<string | null>(null);
const [assignTenantId, setAssignTenantId] = useState('');
const [assignBusy, setAssignBusy] = useState(false);
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null); const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
async function load() { async function load() {
const res = await fetch('/api/admin/bots'); const [botsRes, tenantsRes] = await Promise.all([
if (res.ok) setBots(await res.json()); fetch('/api/admin/bots'),
fetch('/api/admin/tenants'),
]);
if (botsRes.ok) setBots(await botsRes.json());
if (tenantsRes.ok) setTenants(await tenantsRes.json());
} }
useEffect(() => { useEffect(() => {
@@ -78,6 +86,28 @@ export default function BotsPage() {
} }
} }
async function assignBot() {
if (!assigningBotId || !assignTenantId) return;
setAssignBusy(true);
try {
const res = await fetch(`/api/admin/bots/${assigningBotId}/assign`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tenantId: assignTenantId }),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
alert(err.message ?? 'Failed to assign bot');
return;
}
setAssigningBotId(null);
setAssignTenantId('');
void load();
} finally {
setAssignBusy(false);
}
}
if (loading) return <p className="text-gray-500">Loading...</p>; if (loading) return <p className="text-gray-500">Loading...</p>;
if (!admin) return null; if (!admin) return null;
@@ -120,34 +150,74 @@ export default function BotsPage() {
<th className="px-4 py-3 font-medium">JID</th> <th className="px-4 py-3 font-medium">JID</th>
<th className="px-4 py-3 font-medium">Name</th> <th className="px-4 py-3 font-medium">Name</th>
<th className="px-4 py-3 font-medium">Status</th> <th className="px-4 py-3 font-medium">Status</th>
<th className="px-4 py-3 font-medium">Tenants</th> <th className="px-4 py-3 font-medium">Chapters</th>
<th className="px-4 py-3 font-medium">Created</th> <th className="px-4 py-3 font-medium">Created</th>
<th className="px-4 py-3 font-medium"></th> <th className="px-4 py-3 font-medium"></th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y"> <tbody className="divide-y">
{bots.map((b: any) => ( {bots.map((b: any) => (
<tr key={b.id} className="hover:bg-gray-50"> <>
<td className="px-4 py-3 font-mono text-xs">{b.jid?.slice(0, 30) ?? 'pending...'}</td> <tr key={b.id} className="hover:bg-gray-50">
<td className="px-4 py-3">{b.displayName ?? '—'}</td> <td className="px-4 py-3 font-mono text-xs">{b.jid?.slice(0, 30) ?? 'pending...'}</td>
<td className="px-4 py-3"> <td className="px-4 py-3">{b.displayName ?? '—'}</td>
<span className={`text-xs font-medium px-2 py-1 rounded-full ${ <td className="px-4 py-3">
b.status === 'ACTIVE' ? 'bg-green-100 text-green-700' : <span className={`text-xs font-medium px-2 py-1 rounded-full ${
b.status === 'PAIRING' ? 'bg-yellow-100 text-yellow-700' : b.status === 'ACTIVE' ? 'bg-green-100 text-green-700' :
'bg-gray-100 text-gray-500' b.status === 'PAIRING' ? 'bg-yellow-100 text-yellow-700' :
}`}>{b.status}</span> 'bg-gray-100 text-gray-500'
</td> }`}>{b.status}</span>
<td className="px-4 py-3">{b.tenantCount}</td> </td>
<td className="px-4 py-3 text-xs text-gray-500">{new Date(b.createdAt).toLocaleDateString()}</td> <td className="px-4 py-3">{b.tenantCount}</td>
<td className="px-4 py-3"> <td className="px-4 py-3 text-xs text-gray-500">{new Date(b.createdAt).toLocaleDateString()}</td>
<button <td className="px-4 py-3 flex gap-3">
onClick={() => removeBot(b.id)} <button
className="text-red-600 text-xs hover:underline" onClick={() => { setAssigningBotId(assigningBotId === b.id ? null : b.id); setAssignTenantId(''); }}
> className="text-blue-600 text-xs hover:underline"
Remove >
</button> Assign
</td> </button>
</tr> <button
onClick={() => removeBot(b.id)}
className="text-red-600 text-xs hover:underline"
>
Remove
</button>
</td>
</tr>
{assigningBotId === b.id && (
<tr key={`${b.id}-assign`} className="bg-blue-50">
<td colSpan={6} className="px-4 py-3">
<div className="flex items-center gap-3">
<span className="text-xs font-medium text-gray-600">Assign to chapter:</span>
<select
value={assignTenantId}
onChange={(e) => setAssignTenantId(e.target.value)}
className="border rounded-lg px-3 py-1.5 text-sm flex-1 max-w-xs bg-white"
>
<option value=""> select chapter </option>
{tenants.map((t: any) => (
<option key={t.id} value={t.id}>{t.name} ({t.slug})</option>
))}
</select>
<button
onClick={() => void assignBot()}
disabled={!assignTenantId || assignBusy}
className="bg-blue-600 text-white rounded-lg px-3 py-1.5 text-xs font-medium hover:bg-blue-700 disabled:opacity-50"
>
{assignBusy ? 'Assigning...' : 'Confirm'}
</button>
<button
onClick={() => { setAssigningBotId(null); setAssignTenantId(''); }}
className="text-xs text-gray-500 hover:text-gray-700"
>
Cancel
</button>
</div>
</td>
</tr>
)}
</>
))} ))}
</tbody> </tbody>
</table> </table>