// SGAB v2 — Agenda Mensal Visual const { useState, useMemo } = React; const DIAS_SEMANA = ['Dom','Seg','Ter','Qua','Qui','Sex','Sáb']; const MESES = ['Janeiro','Fevereiro','Março','Abril','Maio','Junho','Julho','Agosto','Setembro','Outubro','Novembro','Dezembro']; function Agenda({ user, toast }) { const isAdmin = user.perfil === 'administrador'; const hoje = new Date(); const [ano, setAno] = useState(hoje.getFullYear()); const [mes, setMes] = useState(hoje.getMonth()); const [db, setDb] = useState(() => SGAB.getDB()); const [modal, setModal] = useState(false); const [selDia, setSelDia] = useState(null); const [editando, setEditando] = useState(null); const [form, setForm] = useState({}); const { fmt } = SGAB; function reload() { setDb(SGAB.getDB()); } // Dias do mês const diasDoMes = useMemo(() => { const primeiroDia = new Date(ano, mes, 1).getDay(); const totalDias = new Date(ano, mes + 1, 0).getDate(); const cells = []; for (let i = 0; i < primeiroDia; i++) cells.push(null); for (let d = 1; d <= totalDias; d++) cells.push(d); // pad to complete last row while (cells.length % 7 !== 0) cells.push(null); return cells; }, [ano, mes]); function dataStr(d) { return `${ano}-${String(mes+1).padStart(2,'0')}-${String(d).padStart(2,'0')}`; } function entradasDia(d) { if (!d) return []; return db.agenda.filter(e => e.data === dataStr(d)); } // Totais do mês const totais = useMemo(() => { const prefix = `${ano}-${String(mes+1).padStart(2,'0')}`; const entries = db.agenda.filter(e => e.data.startsWith(prefix)); return { creditos: entries.filter(e=>e.tipo==='credito').reduce((s,e)=>s+(e.valor||0),0), debitos: entries.filter(e=>e.tipo==='debito').reduce((s,e)=>s+(e.valor||0),0), }; }, [db, ano, mes]); function navMes(dir) { let nm = mes + dir, na = ano; if (nm < 0) { nm = 11; na--; } if (nm > 11) { nm = 0; na++; } setMes(nm); setAno(na); } function openModal(dia, entrada=null) { setSelDia(dia); if (entrada) { setEditando(entrada); setForm({ data:entrada.data, tipo:entrada.tipo, desc:entrada.desc, valor:entrada.valor||'', clienteId:entrada.clienteId||'', status:entrada.status, riscado:entrada.riscado }); } else { setEditando(null); const dataInicial = dia ? dataStr(dia) : fmt.today(); setForm({ data:dataInicial, tipo:'servico', desc:'', valor:'', clienteId:'', status:'agendado', riscado:false }); } setModal(true); } function salvar() { if (!form.desc || !form.desc.trim()) { toast('Informe a descrição.','error'); return; } if (!form.data) { toast('Informe a data.','error'); return; } const newDb = SGAB.getDB(); const payload = { ...form, valor: form.valor?+form.valor:null, clienteId:form.clienteId?+form.clienteId:null }; if (editando) { const idx = newDb.agenda.findIndex(e=>e.id===editando.id); if (idx>=0) newDb.agenda[idx] = { ...newDb.agenda[idx], ...payload }; } else { const id = SGAB.nxtId(newDb,'agenda'); newDb.agenda.push({ id, ...payload }); } SGAB.saveDB(newDb); reload(); setModal(false); // Reposicionar calendário no mês da data salva const [yy, mm] = form.data.split('-'); setAno(+yy); setMes(+mm - 1); toast(editando?'Entrada atualizada!':'Entrada adicionada!'); } function excluir(entrada) { if (!confirm('Excluir esta entrada?')) return; const newDb = SGAB.getDB(); newDb.agenda = newDb.agenda.filter(e=>e.id!==entrada.id); SGAB.saveDB(newDb); reload(); toast('Entrada removida.','info'); } function toggleRiscado(entrada) { const newDb = SGAB.getDB(); const idx = newDb.agenda.findIndex(e=>e.id===entrada.id); if (idx>=0) newDb.agenda[idx].riscado = !newDb.agenda[idx].riscado; SGAB.saveDB(newDb); reload(); } function toggleFinalizado(entrada) { const newDb = SGAB.getDB(); const idx = newDb.agenda.findIndex(e=>e.id===entrada.id); if (idx>=0) newDb.agenda[idx].status = newDb.agenda[idx].status==='finalizado'?'agendado':'finalizado'; SGAB.saveDB(newDb); reload(); } // Cell entry style function entryStyle(e) { const base = { fontSize:11, padding:'2px 6px', borderRadius:4, marginBottom:2, cursor:'pointer', textDecoration:e.riscado?'line-through':'none', display:'flex', alignItems:'center', gap:4, lineHeight:1.3, wordBreak:'break-word', transition:'opacity 0.15s', opacity:e.riscado?0.55:1 }; if (e.tipo==='credito') return {...base, color:'#15803D', background:'#F0FDF4', border:'1px solid #BBF7D0', fontWeight:600}; if (e.tipo==='debito') return {...base, color:'#B91C1C', background:'#FEF2F2', border:'1px solid #FECACA', fontWeight:600}; if (e.tipo==='feriado') return {...base, color:'#6B7280', background:'#F1F5F9', border:'1px solid #E2E8F0', fontWeight:600, fontStyle:'italic'}; // servico const bg = e.status==='finalizado' ? '#FEF9C3' : '#fff'; const bc = e.status==='finalizado' ? '#FDE68A' : '#E2E8F0'; return {...base, color:'#374151', background:bg, border:`1px solid ${bc}`}; } const tiposEntrada = [ {v:'servico', l:'Serviço / Atendimento'}, {v:'credito', l:'Crédito (recebimento)'}, {v:'debito', l:'Débito (despesa)'}, {v:'feriado', l:'Feriado / Bloqueio'}, ]; const cliOpts = db.clientes.map(c=>({v:c.id,l:c.fantasia})); return (
navMes(-1)}>Anterior {setMes(hoje.getMonth());setAno(hoje.getFullYear());}}>Hoje navMes(1)}>Próximo openModal(null)}>Novo Compromisso } /> {/* HEADER DIAS */}
{DIAS_SEMANA.map((d,i)=>(
{d}
))}
{/* CELLS */}
{diasDoMes.map((d, i) => { if (d===null) return
; const entries = entradasDia(d); const isHoje = d===hoje.getDate() && mes===hoje.getMonth() && ano===hoje.getFullYear(); const isFeriado = entries.some(e=>e.tipo==='feriado'); const isFim = i%7===0||i%7===6; const saldoDia = entries.find(e=>e.tipo==='saldo'); const credDia = entries.filter(e=>e.tipo==='credito').reduce((s,e)=>s+(e.valor||0),0); const debDia = entries.filter(e=>e.tipo==='debito').reduce((s,e)=>s+(e.valor||0),0); return (
{ if(!isFeriado&&!isFim) e.currentTarget.style.background='#F5F9FF'; }} onMouseLeave={e=>{ e.currentTarget.style.background = isFeriado?'#FFFBEB':isFim?'#FAFBFC':'#fff'; }}> {/* Day header */}
{d} {/* Saldo diário (admin only) */} {isAdmin && (credDia > 0 || debDia > 0) && ( {fmt.money(credDia - debDia).replace('R$\u00a0','')} )}
{/* Entries */}
{entries.filter(e=>e.tipo!=='saldo').slice(0,5).map(e => { const cli = db.clientes.find(c=>c.id===e.clienteId); const hasVal = e.valor && (e.tipo==='credito'||e.tipo==='debito'); return (
{ ev.stopPropagation(); openModal(d,e); }} onDoubleClick={ev=>{ ev.stopPropagation(); if(e.tipo==='servico') toggleFinalizado(e); else toggleRiscado(e); }} title={`${e.desc}${hasVal?' — '+fmt.money(e.valor):''} | Duplo-clique para ${e.tipo==='servico'?'finalizar/reabrir':'riscar/remover traço'}`}> {hasVal && isAdmin ? `${fmt.money(e.valor).replace('R$\u00a0','R$ ')} ` : ''} {e.desc} {e.status==='finalizado' && e.tipo==='servico' && OK} {!isAdmin && (e.tipo==='credito'||e.tipo==='debito') && 🔒}
); })} {entries.length > 5 &&
+{entries.length-5} mais...
}
{/* ADD BUTTON */}
); })}
{/* FOOTER TOTAIS */} {isAdmin && (
Débitos do mês
{fmt.money(totais.debitos)}
Créditos do mês
{fmt.money(totais.creditos)}
Saldo
=0?'#60A5FA':'#F87171',marginTop:2}}>{fmt.money(totais.creditos-totais.debitos)}
Faturamento
{fmt.money(totais.creditos)}
)}
{/* LEGENDA */}
{[['#F0FDF4','#BBF7D0','#15803D','Crédito'],['#FEF2F2','#FECACA','#B91C1C','Débito'], ['#FEF9C3','#FDE68A','#92400E','Serviço finalizado'],['#fff','#E2E8F0','#374151','Serviço agendado'], ['#FFFBEB','#FDE68A','#92400E','Feriado / Bloqueio']].map(([bg,border,c,label])=>(
{label}
))} Duplo-clique: finalizar / traço
{/* MODAL ENTRADA */} setModal(false)} title={editando?'Editar compromisso':'Novo compromisso'} width={480} footer={<> {editando && {excluir(editando);setModal(false);}}>Excluir}
setModal(false)}>Cancelar Salvar }> setForm(f=>({...f,data:v}))}/> setForm(f=>({...f,tipo:v}))} options={tiposEntrada}/> setForm(f=>({...f,desc:v}))} placeholder="Ex: Cliente XYZ — OS_20260001"/> {(form.tipo==='credito'||form.tipo==='debito') && <> setForm(f=>({...f,valor:v}))} type="number" min="0" step="0.01" placeholder="0,00" disabled={!isAdmin}/> } {form.tipo!=='feriado' && setForm(f=>({...f,clienteId:v}))} options={cliOpts} placeholder="Selecionar cliente..."/> } setForm(f=>({...f,status:v}))} options={[ {v:'agendado',l:'Agendado'},{v:'finalizado',l:'Finalizado'},{v:'feriado',l:'Feriado'},{v:'cancelado',l:'Cancelado'}]}/> {editando && }
); } Object.assign(window, { Agenda });