function ContactsPage() { const { AppBootstrap } = window.__CRM__; const { Container, Paper, Typography, TextField, Button, Table, TableHead, TableRow, TableCell, TableBody, IconButton, Chip, Stack, Pagination, Dialog, DialogTitle, DialogContent, DialogActions, Grid, MenuItem, TableContainer, useMediaQuery, Card, CardContent, Divider, Checkbox, FormControlLabel } = MaterialUI; const isXs = useMediaQuery('(max-width:600px)'); const isSm = useMediaQuery('(max-width:900px)'); const [q, setQ] = React.useState(""); const [rows, setRows] = React.useState([]); const [total, setTotal] = React.useState(0); const [page, setPage] = React.useState(1); const limit = 25; // seleção por página const [selected, setSelected] = React.useState(new Set()); // modal const emptyForm = { id: 0, name: "", phone: "", email: "", status: "Active", address_line: "", apt_suite: "", city: "", state: "", zip_code: "", country: "", notes: "" }; const [open, setOpen] = React.useState(false); const [form, setForm] = React.useState(emptyForm); async function load(p = 1) { const offset = (p - 1) * limit; const res = await fetch(`/api/contacts_list.php?q=${encodeURIComponent(q)}&limit=${limit}&offset=${offset}`, { credentials: 'include' }); const j = await res.json(); setRows(j.items || []); setTotal(j.total || 0); setPage(p); setSelected(new Set()); // limpa seleção ao trocar página } React.useEffect(() => { load(1); }, []); async function upsert(payload) { await fetch('/api/contacts_upsert.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), credentials: 'include' }); } async function onDelete(id) { if (!confirm('Delete this contact?')) return; await fetch('/api/contacts_delete.php', { method: 'POST', body: new URLSearchParams({ id: String(id) }), credentials: 'include' }); load(page); } async function onBlock(id) { await fetch('/api/contacts_block.php', { method: 'POST', body: new URLSearchParams({ id: String(id) }), credentials: 'include' }); load(page); } async function onDeleteBulk() { if (selected.size === 0) return; if (!confirm(`Delete ${selected.size} selected contact(s)?`)) return; await fetch('/api/contacts_delete_bulk.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ids: Array.from(selected) }), credentials: 'include' }); load(page); } function onExportBulk() { if (selected.size === 0) return; const ids = Array.from(selected).join(','); location.href = `/api/contacts_export.php?ids=${encodeURIComponent(ids)}`; } async function onImport(e) { const f = e.target.files?.[0]; if (!f) return; const fd = new FormData(); fd.append('file', f); await fetch('/api/contacts_import.php', { method: 'POST', body: fd, credentials: 'include' }); e.target.value = ''; load(1); } function openAdd() { setForm(emptyForm); setOpen(true); } function openEdit(row) { setForm({ ...row }); setOpen(true); } function closeModal() { setOpen(false); } async function submitModal() { if (!form.name.trim()) { alert('Name is required'); return; } await upsert(form); setOpen(false); load(form.id ? page : 1); } const pages = Math.max(1, Math.ceil(total / limit)); // seleção helpers const allIdsOnPage = React.useMemo(() => rows.map(r => r.id), [rows]); const allSelected = allIdsOnPage.length > 0 && allIdsOnPage.every(id => selected.has(id)); const someSelected = allIdsOnPage.some(id => selected.has(id)); function toggleOne(id) { setSelected(prev => { const n = new Set(prev); if (n.has(id)) n.delete(id); else n.add(id); return n; }); } function toggleAllPage() { setSelected(prev => { const n = new Set(prev); if (allSelected) { allIdsOnPage.forEach(id => n.delete(id)); } else { allIdsOnPage.forEach(id => n.add(id)); } return n; }); } // Card renderer (XS) const CardRow = ({ r }) => ( toggleOne(r.id)} />} label={{r.name}} /> {r.phone &&
Phone: {r.phone}
} {r.email &&
Email: {r.email}
} {(r.city || r.state) &&
Location: {[r.city, r.state].filter(Boolean).join(' / ')}
} {r.address_line &&
Address: {r.address_line}
} {r.notes &&
Notes: {r.notes}
}
openEdit(r)} title="Edit">edit onBlock(r.id)} title="Block">block onDelete(r.id)} title="Delete">delete
); const bulkCount = selected.size; return ( Contacts setQ(e.target.value)} /> {isXs ? (
{rows.length === 0 ? No records. : rows.map(r => )}
) : ( Name Phone Email Status Address City State Zip Country Notes Actions {rows.map(r => { const checked = selected.has(r.id); return ( toggleOne(r.id)} /> {r.name} {r.phone || '-'} {r.email || '-'} {r.address_line || '-'} {r.city || '-'} {r.state || '-'} {r.zip_code || '-'} {r.country || '-'} {r.notes || '-'} openEdit(r)} title="Edit">edit onBlock(r.id)} title="Block">block onDelete(r.id)} title="Delete">delete ); })} {rows.length === 0 && ( No records. )}
)} load(p)} />
{/* Dialog */} {form.id ? 'Edit Contact' : 'Add Contact'} setForm({ ...form, name: e.target.value })} fullWidth required /> setForm({ ...form, status: e.target.value })} fullWidth> Active Blocked setForm({ ...form, phone: e.target.value })} fullWidth /> setForm({ ...form, email: e.target.value })} fullWidth /> setForm({ ...form, address_line: e.target.value })} fullWidth /> setForm({ ...form, apt_suite: e.target.value })} fullWidth /> setForm({ ...form, city: e.target.value })} fullWidth /> setForm({ ...form, state: e.target.value })} fullWidth /> setForm({ ...form, zip_code: e.target.value })} fullWidth /> setForm({ ...form, country: e.target.value })} fullWidth /> setForm({ ...form, notes: e.target.value })} fullWidth multiline minRows={3} />
); } window.ContactsPage = ContactsPage;