import React, { useState, useEffect, useCallback, memo } from 'react'; import { useAdmin } from '../../context/AdminContext'; import { API_BASE_URL } from '../../auth/authConfig'; // ─── Helpers ──────────────────────────────────────────────── const fmt = (s) => (s || '').replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()); const daysSince = (dateStr) => { if (!dateStr) return '—'; const days = Math.floor((Date.now() - new Date(dateStr).getTime()) / 86400000); if (days === 0) return 'Today'; if (days === 1) return '1 day ago'; return `${days} days ago`; }; const fmtDate = (d) => d ? new Date(d).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) : '—'; const fmtDateTime = (d) => d ? new Date(d).toLocaleString('en-US', { month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: '2-digit' }) : '—'; const fmtCurrency = (cents) => cents != null ? `$${(cents / 100).toFixed(2)}` : '—'; const STATUS_STYLES = { Active: { bg: '#dcfce7', color: '#166534', border: '#86efac' }, Suspended: { bg: '#fef3c7', color: '#92400e', border: '#fcd34d' }, Cancelled: { bg: '#fee2e2', color: '#991b1b', border: '#fca5a5' }, }; // ═════════════════════════════════════════════════════════════ // MAIN COMPONENT — Switches between Pending and All Clients // ═════════════════════════════════════════════════════════════ export default function ClientManagementPanel({ activeTab }) { if (activeTab === 'pending') return ; if (activeTab === 'allClients') return ; return null; } // ═════════════════════════════════════════════════════════════ // PENDING TAB — Registration queue (external data source) // ═════════════════════════════════════════════════════════════ function PendingTab() { const { apiCall, sessionToken } = useAdmin(); const [applicants, setApplicants] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [approving, setApproving] = useState(null); // registrationId being approved const [rejecting, setRejecting] = useState(null); // registrationId being rejected const [rejectReason, setRejectReason] = useState(''); const fetchPending = useCallback(async () => { if (!sessionToken) return; setLoading(true); setError(null); try { const res = await fetch(`${API_BASE_URL}/api/registration/pending`, { headers: { 'X-Session-Token': sessionToken } }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); setApplicants(data.applicants || []); } catch (err) { console.info('[Clients] Registration endpoint not available:', err.message); setApplicants([]); // Don't show error for expected 404/connection — endpoint may not exist yet if (!err.message.includes('404') && !err.message.includes('Failed to fetch')) setError(err.message); } finally { setLoading(false); } }, [sessionToken]); useEffect(() => { fetchPending(); }, [fetchPending]); const handleApprove = async (applicant) => { setApproving(applicant.registrationId); const result = await apiCall('/api/admin/clients', 'POST', { registrationId: applicant.registrationId, name: applicant.businessName, websiteUrl: applicant.websiteUrl, businessCategory: applicant.businessCategory, description: applicant.businessDescription, contactName: applicant.contactName, contactEmail: applicant.contactEmail, contactPhone: applicant.contactPhone, entraSubjectId: applicant.entraSubjectId, clientCategory: applicant.clientCategory || 'General', }); if (result.ok) { fetchPending(); } else { alert(result.error || 'Approval failed'); } setApproving(null); }; const handleReject = async (registrationId) => { const result = await apiCall(`/api/registration/${registrationId}/reject`, 'POST', { reason: rejectReason, }); if (result.ok) { setRejecting(null); setRejectReason(''); fetchPending(); } else { alert(result.error || 'Rejection failed'); } }; if (loading) { return (

Checking for pending registrations…

); } return (

Pending Registrations

New client applications awaiting review
{error && (
Error: {error}
)} {applicants.length === 0 && !error && (

All caught up!

No pending registrations to review.

When the registration system sends new applicants, they'll appear here.

)} {applicants.length > 0 && (
{applicants.map(app => (

{app.businessName}

{app.businessCategory && ( {fmt(app.businessCategory)} )} {app.clientCategory && app.clientCategory !== 'General' && ( {app.clientCategory === 'Franchisee' ? '🏪' : '🏗️'} {app.clientCategory} )}
Registered {daysSince(app.registeredUtc)}
{app.businessDescription && (

{app.businessDescription}

)}
{app.contactName && (
Contact {app.contactName}
)} {app.contactEmail && (
Email {app.contactEmail}
)} {app.contactPhone && (
Phone {app.contactPhone}
)} {app.websiteUrl && ( )}
Payment {app.paymentVerified ? '✓ Verified' : '⚠ Unverified'}
{rejecting === app.registrationId ? (
setRejectReason(e.target.value)} autoFocus />
) : ( <> )}
))}
)}
); } // ═════════════════════════════════════════════════════════════ // ALL CLIENTS TAB — From spClientManagement.list // ═════════════════════════════════════════════════════════════ function AllClientsTab() { const { apiCall } = useAdmin(); const [clients, setClients] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [statusFilter, setStatusFilter] = useState('all'); const [search, setSearch] = useState(''); const [expandedId, setExpandedId] = useState(null); const [detail, setDetail] = useState(null); // full detail for expanded client const [detailLoading, setDetailLoading] = useState(false); const fetchClients = useCallback(async () => { setLoading(true); setError(null); const statusParam = statusFilter === 'all' ? '' : `?status=${statusFilter}`; const result = await apiCall(`/api/admin/clients${statusParam}`); if (result?.ok) { setClients(Array.isArray(result.clients) ? result.clients : []); } else { setError(result?.error || 'Failed to load clients'); setClients([]); } setLoading(false); }, [apiCall, statusFilter]); useEffect(() => { fetchClients(); }, [fetchClients]); const fetchDetail = useCallback(async (clientId) => { setDetailLoading(true); const result = await apiCall(`/api/admin/clients/${clientId}`); if (result?.ok) { setDetail(result); } setDetailLoading(false); }, [apiCall]); const handleExpand = (clientId) => { if (expandedId === clientId) { setExpandedId(null); setDetail(null); } else { setExpandedId(clientId); fetchDetail(clientId); } }; const handleStatusAction = async (action, clientId, reason) => { const body = reason ? { reason } : {}; const result = await apiCall(`/api/admin/clients/${clientId}/${action}`, 'POST', body); if (result?.ok) { fetchClients(); fetchDetail(clientId); } else { alert(result?.error || `${action} failed`); } }; // Client-side search filter const filtered = clients.filter(c => { if (!search.trim()) return true; const q = search.toLowerCase(); return (c.clientName || '').toLowerCase().includes(q) || (c.contactEmail || '').toLowerCase().includes(q) || (c.contactName || '').toLowerCase().includes(q); }); return (

All Clients

{filtered.length} client{filtered.length !== 1 ? 's' : ''} {statusFilter !== 'all' ? ` (${fmt(statusFilter)})` : ''}
setSearch(e.target.value)} className="client-search" />
{error && (
Error: {error}
)} {loading && (

Loading clients…

)} {!loading && filtered.length === 0 && (
📋

No clients found

{search ? 'Try a different search term.' : 'Approved clients will appear here.'}

)} {!loading && filtered.length > 0 && ( {filtered.map(c => ( handleExpand(c.clientId)} onStatusAction={handleStatusAction} onRefresh={fetchClients} apiCall={apiCall} /> ))}
Client Category Status Contact Tier Created
)}
); } // ─── Client Row (table row + expandable detail) ───────────── const ClientRow = memo(function ClientRow({ client: c, expanded, detail, detailLoading, onToggle, onStatusAction, onRefresh, apiCall, }) { const [actionConfirm, setActionConfirm] = useState(null); // { action, label } const [reason, setReason] = useState(''); const [editing, setEditing] = useState(false); const [editFields, setEditFields] = useState({}); const [saving, setSaving] = useState(false); const style = STATUS_STYLES[c.status] || {}; const handleConfirmedAction = async () => { await onStatusAction(actionConfirm.action, c.clientId, reason); setActionConfirm(null); setReason(''); }; const handleSaveEdit = async () => { setSaving(true); const result = await apiCall(`/api/admin/clients/${c.clientId}`, 'PUT', { ...editFields, clientCategory: editFields.clientCategory, }); if (result?.ok) { setEditing(false); setEditFields({}); onRefresh(); } else { alert(result?.error || 'Update failed'); } setSaving(false); }; const startEdit = () => { setEditFields({ name: c.clientName || '', contactName: c.contactName || '', contactEmail: c.contactEmail || '', contactPhone: c.contactPhone || '', websiteUrl: c.websiteUrl || '', description: c.description || '', notes: c.notes || '', clientCategory: c.clientCategoryName || 'General', }); setEditing(true); }; return ( <> {expanded ? '▾' : '▸'}
{c.clientName} {c.websiteUrl && ( {c.websiteUrl.replace(/^https?:\/\//, '')} )}
{c.categoryIcon && {c.categoryIcon}} {c.categoryName ? fmt(c.categoryName) : '—'} {c.status} {c.contactEmail || '—'} {fmt(c.serviceTier || 'self_service')} {fmtDate(c.createdUtc)} {expanded && (
{detailLoading &&

Loading detail…

} {!detailLoading && !editing && ( <> {/* Profile Section */}

Profile

{/* Service Config */}

Service Configuration

{/* Admin Notes */}

Admin Notes

{c.notes || 'No notes.'}

{/* Status Actions */}

Actions

{actionConfirm ? (

{actionConfirm.label} this client? {(actionConfirm.action === 'suspend' || actionConfirm.action === 'cancel') && ( Active campaigns will be paused. )}

{(actionConfirm.action === 'suspend' || actionConfirm.action === 'cancel') && ( setReason(e.target.value)} autoFocus /> )}
) : (
{c.status === 'Active' && ( <> )} {c.status === 'Suspended' && ( <> )} {c.status === 'Cancelled' && (

Account cancelled {c.cancelledUtc ? `on ${fmtDate(c.cancelledUtc)}` : ''}. {c.cancelledReason && Reason: {c.cancelledReason}}

)}
)}
{/* Status History */} {detail?.statusHistory?.length > 0 && (

Status History

{detail.statusHistory.map((entry, i) => (
{entry.fromStatus ? `${entry.fromStatus} → ${entry.toStatus}` : `Created as ${entry.toStatus}` } {entry.reason && ( — {entry.reason} )} {fmtDateTime(entry.changedUtc)} {entry.changedByName && ` by ${entry.changedByName}`}
))}
)} {/* Linked Users */} {detail?.users?.length > 0 && (

Users ({detail.users.length})

{detail.users.map(u => ( ))}
Name Email Role Status
{u.displayName || '—'} {u.email || '—'} {u.role} {u.status}
)} )} {/* Edit Mode */} {!detailLoading && editing && (

Edit Client

setEditFields(f => ({ ...f, name: v }))} /> setEditFields(f => ({ ...f, contactName: v }))} />
setEditFields(f => ({ ...f, contactEmail: v }))} /> setEditFields(f => ({ ...f, contactPhone: v }))} />
setEditFields(f => ({ ...f, websiteUrl: v }))} />
setEditFields(f => ({ ...f, description: v }))} wide />
setEditFields(f => ({ ...f, notes: v }))} wide />
)}
)} ); }); // ─── Small helper components ──────────────────────────────── function DetailField({ label, value, link, wide }) { return (
{label} {link && value ? ( {value.replace(/^https?:\/\//, '')} ) : ( {value || '—'} )}
); } function EditField({ label, value, onChange, wide }) { return (
onChange(e.target.value)} />
); }