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 (
{error && (
Error: {error}
)}
{loading && (
)}
{!loading && filtered.length === 0 && (
📋
No clients found
{search ? 'Try a different search term.' : 'Approved clients will appear here.'}
)}
{!loading && filtered.length > 0 && (
|
Client |
Category |
Status |
Contact |
Tier |
Created |
{filtered.map(c => (
handleExpand(c.clientId)}
onStatusAction={handleStatusAction}
onRefresh={fetchClients}
apiCall={apiCall}
/>
))}
)}
);
}
// ─── 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 */}
{/* Service Config */}
{/* 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})
| Name |
Email |
Role |
Status |
{detail.users.map(u => (
| {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 (
);
}
function EditField({ label, value, onChange, wide }) {
return (
onChange(e.target.value)} />
);
}