// /public/assets/Campaigns.jsx function CampaignsPage() { const { AppBootstrap } = window.__CRM__; const { Container, Paper, Typography, TextField, Button, Table, TableHead, TableRow, TableCell, TableBody, IconButton, Stack, Pagination, TableContainer, useMediaQuery, Card, CardContent, Divider, Dialog, DialogTitle, DialogContent, DialogActions, MenuItem, Chip, Tooltip, Grid, InputAdornment, Snackbar, Alert, Switch, FormControlLabel, Tabs, Tab, Box } = 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; const [openModal, setOpenModal] = React.useState(false); const [mode, setMode] = React.useState('create'); // create|edit|view const [locked, setLocked] = React.useState(false); const [lists, setLists] = React.useState([]); const [inboxes, setInboxes] = React.useState([]); const [inboxesLoading, setInboxesLoading] = React.useState(false); const emptyForm = { id: null, name: "", list_id: "", inbox_id: "", typemsg: "Phone", // novo campo tags: "", schedule: "", msgs: ["","","","",""], imgs: [null,null,null,null,null], imgUrls: [null,null,null,null,null], use_html: 0, }; const [form, setForm] = React.useState(emptyForm); const [focusMsgIdx, setFocusMsgIdx] = React.useState(null); const [tab, setTab] = React.useState(0); // HTML editor const [useHtml, setUseHtml] = React.useState(false); const [quillReady, setQuillReady] = React.useState(false); const quillRefs = React.useRef([null,null,null,null,null]); // toast const [toast, setToast] = React.useState({ open:false, msg:'', sev:'info' }); const notify = (msg, sev='info') => setToast({ open:true, msg, sev }); const VARS = ["{name}","{number}","{email}","{city}","{state}","{zip}","{country}"]; function statusChip(s) { const map = { Draft:'default', Scheduled:'info', Running:'success', Paused:'warning', Stopped:'error', Completed:'success', Failed:'error' }; return ; } async function load(p=1) { const offset = (p-1)*limit; const r = await fetch(`/api/campaigns_list.php?q=${encodeURIComponent(q)}&limit=${limit}&offset=${offset}`, { credentials:'include' }); const j = await r.json(); setRows(j.items||[]); setTotal(j.total||0); setPage(p); } React.useEffect(()=>{ load(1); }, []); async function loadLists() { const r = await fetch(`/api/lists_list.php?limit=200&offset=0`, { credentials:'include' }); const j = await r.json(); setLists(j.items||[]); } async function loadInboxes() { setInboxesLoading(true); try { const r = await fetch('/api/settings_chatwoot_inboxes.php', { credentials:'include' }); const j = await r.json(); setInboxes(j.ok ? (j.items||[]) : []); } finally { setInboxesLoading(false); } } async function loadDefaultInbox() { const r = await fetch('/api/settings_chatwoot_get.php',{credentials:'include'}); const j = await r.json(); const defId = j?.config?.default_inbox_id ? String(j.config.default_inbox_id) : ''; if (defId) setForm(f => ({ ...f, inbox_id: defId })); } function openCreate() { setMode('create'); setLocked(false); setForm(structuredClone(emptyForm)); setUseHtml(false); setTab(0); setOpenModal(true); loadLists(); loadInboxes().then(loadDefaultInbox); setTimeout(()=>setFocusMsgIdx(0),0); } function toInputScheduleLocal(iso) { if (!iso) return ""; const [d, t=""] = iso.split(" "); const hm = t.slice(0,5); return `${d}T${hm}`; } async function openEdit(id) { const r = await fetch(`/api/campaigns_get.php?id=${id}`, { credentials:'include' }); if(!r.ok){ notify('Campaign not found.','error'); return; } const { campaign:c } = await r.json(); const editable = (c.status==='Draft' || c.status==='Scheduled'); setMode(editable ? 'edit' : 'view'); setLocked(!editable); setUseHtml(c.use_html === 1); setForm({ id: c.id, name: c.name || "", list_id: String(c.list_id || ""), inbox_id: String(c.inbox_id || ""), typemsg: c.typemsg || (c.use_html === 1 ? 'Email' : 'Phone'), tags: c.tags || "", schedule: c.scheduled_at ? toInputScheduleLocal(c.scheduled_at) : "", msgs: [c.msg1||"", c.msg2||"", c.msg3||"", c.msg4||"", c.msg5||""], imgs: [null,null,null,null,null], imgUrls: [c.img1||null,c.img2||null,c.img3||null,c.img4||null,c.img5||null], use_html: c.use_html || 0, }); setOpenModal(true); setTab(0); setTimeout(()=>setFocusMsgIdx(0),0); loadLists(); await loadInboxes(); } function canEdit(status){ return status==='Draft' || status==='Scheduled'; } function onAnalytics(id){ location.hash = `#/campaigns/${id}/analytics`; } async function onDelete(id, status){ if(!canEdit(status)) return; if(!confirm('Delete this campaign?')) return; const fd = new FormData(); fd.append('id', String(id)); const r = await fetch('/api/campaigns_delete.php', { method:'POST', body:fd, credentials:'include' }); if(!r.ok){ notify('Delete blocked.','warning'); return; } notify('Campaign deleted.','success'); load(1); } async function player(id, action, status){ const fd = new FormData(); fd.append('id', String(id)); fd.append('action', action); const r = await fetch('/api/campaigns_action.php', { method:'POST', body:fd, credentials:'include' }); if(!r.ok){ notify('Action not allowed. Check settings, inbox or status.','warning'); return; } if (action==='play') { notify(status==='Paused' ? 'Resumed' : 'Started','success'); } else if (action==='pause') { notify('Paused','success'); } else { notify('Stopped','success'); } load(page); } // ==== Quill loader ==== function loadOnce(id, hrefOrSrc, isCss=false) { return new Promise((resolve, reject)=>{ if (document.getElementById(id)) return resolve(); const el = isCss ? document.createElement('link') : document.createElement('script'); if (isCss) { el.rel='stylesheet'; el.href=hrefOrSrc; } else { el.src=hrefOrSrc; el.async=true; } el.id = id; el.onload = ()=>resolve(); el.onerror = ()=>reject(new Error('load_fail '+hrefOrSrc)); (isCss ? document.head : document.body).appendChild(el); }); } async function ensureQuill() { if (window.ReactQuill && window.Quill) { setQuillReady(true); return; } try { await loadOnce('quill-css', 'https://cdn.jsdelivr.net/npm/quill@1.3.7/dist/quill.snow.css', true); await loadOnce('quill-js', 'https://cdn.jsdelivr.net/npm/quill@1.3.7/dist/quill.min.js'); await loadOnce('react-quill-umd', 'https://cdn.jsdelivr.net/npm/react-quill@2.0.0/dist/react-quill.min.js'); setQuillReady(true); } catch(e) { notify('Failed to load HTML editor.','error'); setUseHtml(false); } } React.useEffect(()=>{ if (openModal && useHtml) ensureQuill(); }, [openModal, useHtml]); // Altura mínima consistente do Quill React.useEffect(()=>{ const s = document.createElement('style'); s.id = 'quill-minheight'; s.textContent = ` .ql-container { min-height: 300px; } .ql-editor { min-height: 300px; } `; document.head.appendChild(s); return ()=>{ const el=document.getElementById('quill-minheight'); if(el) el.remove(); }; },[]); // inserir variável function insertVar(v) { const targetIdx = (focusMsgIdx ?? tab); if (useHtml && quillReady) { const rq = quillRefs.current[targetIdx]; const editor = rq && rq.getEditor ? rq.getEditor() : null; if (editor) { const sel = editor.getSelection(true); const index = sel ? sel.index : editor.getLength(); editor.insertText(index, v); editor.setSelection(index + v.length, 0); return; } } const m = [...form.msgs]; const el = document.getElementById(`msg-${targetIdx}`); if (el && typeof el.selectionStart === 'number') { const s = el.selectionStart, e = el.selectionEnd; const txt = m[targetIdx] || ""; m[targetIdx] = txt.slice(0,s) + v + txt.slice(e); setForm({ ...form, msgs:m }); setTimeout(()=>{ el.focus(); const pos=s+v.length; el.setSelectionRange?.(pos,pos); },0); return; } m[targetIdx] = (m[targetIdx]||"") + v; setForm({ ...form, msgs:m }); } // preview local + manter file function onSelectFile(slot, file) { const imgs = [...form.imgs]; imgs[slot-1] = file || null; if (!file) { setForm({ ...form, imgs }); return; } const reader = new FileReader(); reader.onload = e => { const urls = [...form.imgUrls]; urls[slot-1] = e.target.result; setForm({ ...form, imgs, imgUrls: urls }); }; reader.readAsDataURL(file); } async function handleCreate() { if(!form.name.trim() || !form.list_id || !form.inbox_id){ notify('Name, Contact list and Inbox are required.', 'warning'); return; } const fd = new FormData(); fd.append('name', form.name.trim()); fd.append('list_id', String(form.list_id)); fd.append('inbox_id', String(form.inbox_id)); const inboxName = inboxes.find(i=> String(i.id)===String(form.inbox_id))?.name || ''; fd.append('inbox_name', inboxName); fd.append('typemsg', form.typemsg || 'Phone'); // novo fd.append('tags', form.tags.trim()); fd.append('schedule', form.schedule.trim()); fd.append('use_html', form.use_html ? '1' : '0'); const r = await fetch('/api/campaigns_create.php', { method:'POST', body:fd, credentials:'include' }); let j; try { j = await r.json(); } catch { notify('Create failed (invalid response).','error'); return; } if (!r.ok || !j?.ok) { notify('Failed to create campaign.','error'); return; } const newId = j.id; for (let slot=1; slot<=5; slot++){ const f = form.imgs[slot-1]; if (!f) continue; const ufd = new FormData(); ufd.append('id', String(newId)); ufd.append('slot', String(slot)); ufd.append('file', f); const ur = await fetch('/api/campaigns_upload_image.php', { method:'POST', body:ufd, credentials:'include' }); if (!ur.ok) notify(`Image ${slot} upload failed.`, 'warning'); } const ufd2 = new FormData(); ufd2.append('id', String(newId)); for (let i=1;i<=5;i++) ufd2.append(`msg${i}`, form.msgs[i-1] || ''); const sr = await fetch('/api/campaigns_update.php', { method:'POST', body: ufd2, credentials:'include' }); if (!sr.ok) notify('Saved, but messages failed. Edit to retry.', 'warning'); setOpenModal(false); notify('Campaign created.','success'); load(1); } async function handleSaveEdit() { if (!form.id) return; const cr = await fetch(`/api/campaigns_get.php?id=${form.id}`, { credentials:'include' }); if (!cr.ok) { notify('Campaign not found.','error'); return; } const { campaign:c } = await cr.json(); if (!c || c.status !== 'Draft') { notify('Only Draft campaigns can be updated.','warning'); setLocked(true); return; } for (let slot=1; slot<=5; slot++){ const f = form.imgs[slot-1]; if (!f) continue; const ufd = new FormData(); ufd.append('id', String(form.id)); ufd.append('slot', String(slot)); ufd.append('file', f); const rr = await fetch('/api/campaigns_upload_image.php', { method:'POST', body:ufd, credentials:'include' }); if (!rr.ok) notify(`Image ${slot} upload failed.`, 'warning'); } const fd = new FormData(); fd.append('id', String(form.id)); fd.append('name', form.name.trim()); fd.append('tags', form.tags.trim()); fd.append('schedule', form.schedule.trim()); fd.append('use_html', form.use_html ? '1' : '0'); fd.append('typemsg', form.typemsg || 'Phone'); // novo if (form.inbox_id) { fd.append('inbox_id', String(form.inbox_id)); const inboxName = inboxes.find(i=> String(i.id)===String(form.inbox_id))?.name || ''; fd.append('inbox_name', inboxName); } for (let i=1;i<=5;i++) fd.append(`msg${i}`, form.msgs[i-1] || ''); const r = await fetch('/api/campaigns_update.php', { method:'POST', body:fd, credentials:'include' }); if (!r.ok) { notify('Save failed.','error'); return; } setOpenModal(false); notify('Campaign updated.','success'); load(page); } const selectedInboxMissing = openModal && form.inbox_id && !inboxes.some(i=>String(i.id)===String(form.inbox_id)); const pages = Math.max(1, Math.ceil(total/limit)); const quillModules = React.useMemo(()=>({ toolbar: [ [{ header: [1,2,false] }], ['bold','italic','underline','strike'], [{ 'list': 'ordered' }, { 'list': 'bullet' }], ['link','blockquote','code-block','clean','image'] ] }),[]); const CardRow = ({ r }) => ( {r.name} {r.list_name} • Inbox: {r.inbox_name || '-'} • Tipo: {r.typemsg || '-'} {statusChip(r.status)} {r.status==='Running' ? ( player(r.id,'pause',r.status)}> pause ) : ( player(r.id,'play',r.status)} disabled={!(r.status==='Draft'||r.status==='Scheduled'||r.status==='Paused')}> play_arrow )} player(r.id,'stop',r.status)} disabled={!(r.status==='Running'||r.status==='Paused')}> stop onAnalytics(r.id)}> analytics openEdit(r.id)}> edit onDelete(r.id,r.status)} disabled={!canEdit(r.status)}> delete Schedule: {r.scheduled_at || '-'} Completed: {r.completed_at ? 'Yes' : 'No'} ); const ReactQuillComp = (window && window.ReactQuill) ? window.ReactQuill : null; return ( Campaigns setQ(e.target.value)} /> {isXs ? (
{rows.length===0 ? No campaigns. : rows.map(r => )}
) : ( Name Status Contact List Inbox Tipo{/* novo */} Schedule Completed Actions {rows.map(r=>( {r.name} {statusChip(r.status)} {r.list_name} {r.inbox_name || '-'} {r.typemsg || '-'}{/* novo */} {r.scheduled_at || '-'} {r.completed_at ? 'Yes' : 'No'} {r.status==='Running' ? ( player(r.id,'pause',r.status)}> pause ) : ( player(r.id,'play',r.status)} disabled={!(r.status==='Draft'||r.status==='Scheduled'||r.status==='Paused')}> play_arrow )} player(r.id,'stop',r.status)} disabled={!(r.status==='Running'||r.status==='Paused')}> stop onAnalytics(r.id)}> analytics openEdit(r.id)}> edit onDelete(r.id,r.status)} disabled={!canEdit(r.status)}> delete ))} {rows.length===0 && ( No campaigns. )}
)} load(p)} />
setOpenModal(false)} maxWidth="md" fullWidth keepMounted disableRestoreFocus > {mode==='create' ? 'New Campaign' : (locked ? 'View Campaign' : 'Edit Campaign')} setForm({...form,name:e.target.value})} fullWidth required disabled={locked} /> setForm({...form,list_id:e.target.value})} fullWidth required disabled={locked}> {lists.map(l => {l.name} ({l.total_contacts||0}))} setForm({...form,inbox_id:e.target.value})} fullWidth required disabled={locked || inboxesLoading} helperText={inboxesLoading ? 'Loading inboxes…' : ''} > {selectedInboxMissing && ( Selected inbox (loading…) )} {inboxes.map(i => ( {i.name} ({i.channel||'channel'}) ))} setForm({...form, typemsg:e.target.value})} fullWidth required disabled={locked} helperText="Seleciona coluna do contato a usar" > Phone Email setForm({...form,tags:e.target.value})} fullWidth disabled={locked} /> setForm({...form, schedule: e.target.value})} fullWidth InputLabelProps={{ shrink: true }} disabled={locked} InputProps={{ endAdornment:schedule }} /> { const v = e.target.checked; setUseHtml(v); setForm(f=>({ ...f, use_html: v ? 1 : 0, typemsg: v ? 'Email' : (f.typemsg === 'Email' ? 'Phone' : f.typemsg) })); }} disabled={locked} /> } label="Use HTML editor for messages (email)" /> {useHtml && !quillReady && Loading editor…} {VARS.map(v => ( insertVar(v)} /> ))} { setTab(v); setFocusMsgIdx(v); }} variant="scrollable" scrollButtons="auto" sx={{ mt:2 }}> {[0,1,2,3,4].map(idx=>())} {[0,1,2,3,4].map(idx=>( ))} {mode==='create' && } {mode==='edit' && } setToast({...toast, open:false})} anchorOrigin={{ vertical:'bottom', horizontal:'right' }} > setToast({...toast, open:false})} severity={toast.sev} variant="filled" sx={{ width: '100%' }}> {toast.msg}
); } window.CampaignsPage = CampaignsPage;