// Admin Client Area — módulo da EQUIPE pra alimentar o Portal do Cliente // (timeline de operação, arquivos, aprovações). Não é o portal em si — // é a tela onde a equipe registra o que o cliente vai ver. function AdminClientArea() { const [events, setEvents] = React.useState([]); const [selectedEventId, setSelectedEventId] = React.useState(() => localStorage.getItem('admin_client_area_event') || ''); const [loading, setLoading] = React.useState(true); const [search, setSearch] = React.useState(''); React.useEffect(() => { sb.from('events').select('id, name, client_name, event_date, status').order('event_date', { ascending: false }).then(({ data }) => { setEvents(data || []); setLoading(false); }); }, []); const q = search.trim().toLowerCase(); const filtered = q ? events.filter(e => (e.name || '').toLowerCase().includes(q) || (e.client_name || '').toLowerCase().includes(q)) : events; const selectedEvent = events.find(e => String(e.id) === String(selectedEventId)); const selectEvent = (id) => { setSelectedEventId(id); localStorage.setItem('admin_client_area_event', id); }; if (loading) return

Portal Cliente (Admin)

; return (

Portal do Cliente — Administração

Alimente a timeline da operação, envie arquivos e crie aprovações — tudo aparece no portal do cliente.

{/* Lista de eventos */}
setSearch(e.target.value)} placeholder="Buscar por evento ou cliente..." style={{ width: '100%', boxSizing: 'border-box', border: `1px solid ${DS.colors.border}`, borderRadius: DS.radius.sm, padding: '8px 12px 8px 34px', fontSize: 13, outline: 'none', background: '#fff', fontFamily: 'inherit' }} />
{filtered.length === 0 ? (
Nenhum evento encontrado.
) : ( filtered.map((ev, i) => { const active = String(ev.id) === String(selectedEventId); return (
selectEvent(String(ev.id))} style={{ padding: '12px 14px', borderBottom: i < filtered.length - 1 ? `1px solid ${DS.colors.border}` : 'none', cursor: 'pointer', background: active ? DS.colors.primaryLight : '#fff', borderLeft: active ? `3px solid ${DS.colors.primary}` : '3px solid transparent', transition: 'all 0.15s' }}>
{ev.name}
{ev.client_name || '— sem cliente —'}
{ev.event_date ? new Date(ev.event_date + 'T00:00:00').toLocaleDateString('pt-BR') : '—'} · {ev.status || ''}
); }) )}
{/* Área do evento selecionado */}
{!selectedEvent ? (
Selecione um evento à esquerda
Você poderá adicionar atualizações, arquivos e aprovações que o cliente verá no portal.
) : ( )}
); } function AdminEventPanel({ event }) { const [tab, setTab] = React.useState('timeline'); const tabs = [ { id: 'timeline', label: 'Timeline da Operação', icon: 'zap' }, { id: 'arquivos', label: 'Arquivos', icon: 'download' }, { id: 'aprovacoes', label: 'Aprovações', icon: 'check' }, ]; return (
Evento selecionado
{event.name}
{event.client_name} · {event.event_date ? new Date(event.event_date + 'T00:00:00').toLocaleDateString('pt-BR') : '—'}
{tabs.map(t => ( ))}
{tab === 'timeline' && } {tab === 'arquivos' && } {tab === 'aprovacoes' && }
); } // ───────────────────────────────────────────────────── // Timeline — adicionar update, listar, deletar // ───────────────────────────────────────────────────── function AdminTimelinePanel({ event }) { const [updates, setUpdates] = React.useState([]); const [loading, setLoading] = React.useState(true); const [showForm, setShowForm] = React.useState(false); const [form, setForm] = React.useState({ type: 'chegada', text: '', image_url: '' }); const [saving, setSaving] = React.useState(false); const [uploading, setUploading] = React.useState(false); const [toast, setToast] = React.useState(null); const showToast = (msg, type = 'success') => { setToast({ msg, type }); setTimeout(() => setToast(null), 3000); }; const photoInputRef = React.useRef(null); const fetchUpdates = () => { sb.from('operation_updates').select('*').eq('event_id', event.id).order('created_at', { ascending: false }).then(({ data }) => { setUpdates(data || []); setLoading(false); }); }; React.useEffect(() => { fetchUpdates(); }, [event.id]); const handlePhotoUpload = async (e) => { const file = e.target.files?.[0]; if (!file) return; if (!file.type.startsWith('image/')) { showToast('Só imagens (JPG/PNG/WEBP).', 'error'); return; } setUploading(true); try { const uid = crypto.randomUUID(); const ext = (file.name.split('.').pop() || 'jpg').toLowerCase(); const path = `timeline/event_${event.id}/${uid}.${ext}`; const { error: upErr } = await sb.storage.from('event-photos').upload(path, file, { contentType: file.type }); if (upErr) { showToast('Erro no upload: ' + upErr.message, 'error'); return; } const { data: pub } = sb.storage.from('event-photos').getPublicUrl(path); setForm(f => ({ ...f, image_url: pub.publicUrl, type: f.type === 'chegada' ? 'foto' : f.type })); showToast('Foto pronta! Clique em Publicar.'); } finally { setUploading(false); if (photoInputRef.current) photoInputRef.current.value = ''; } }; const handleSave = async () => { if (!form.text.trim() && !form.image_url.trim()) { showToast('Escreva um texto ou envie uma foto.', 'error'); return; } setSaving(true); const cu = window.currentUser || {}; const { error } = await sb.from('operation_updates').insert({ event_id: event.id, type: form.type, text: form.text.trim() || null, image_url: form.image_url.trim() || null, created_by: cu.id || null, created_by_name: cu.name || null, }); if (error) { showToast('Erro: ' + error.message, 'error'); setSaving(false); return; } // Notifica equipe if (window.createNotification) { const label = { chegada: 'Chegada da equipe', inicio: 'Início da ação', foto: 'Foto da ativação', ocorrencia: 'Ocorrência', encerramento: 'Ação encerrada', nota: 'Atualização' }[form.type]; await window.createNotification({ type: 'operation_update', title: `${label} em ${event.name}`, message: form.text.slice(0, 120), link: 'events', icon: 'zap', }); } setForm({ type: 'chegada', text: '', image_url: '' }); setShowForm(false); setSaving(false); fetchUpdates(); showToast('Atualização publicada!'); }; const handleDelete = async (u) => { if (!window.confirm('Remover essa atualização?')) return; await sb.from('operation_updates').delete().eq('id', u.id); showToast('Removida.'); fetchUpdates(); }; const label = { chegada: 'Chegada da equipe', inicio: 'Início da ação', foto: 'Foto da ativação', ocorrencia: 'Ocorrência', encerramento: 'Ação encerrada', nota: 'Nota livre' }; const color = { chegada: '#3B82F6', inicio: '#8B5CF6', foto: '#10B981', ocorrencia: '#F59E0B', encerramento: '#059669', nota: '#64748B' }; if (loading) return ; return (
Atualizações publicadas ({updates.length})
setShowForm(true)}>Nova atualização
{showForm && (