import React, { useEffect, useMemo, useState } from “react”; // ===== Helper utils ===== const todayISO = () => new Date().toISOString().slice(0, 10); function daysBetween(a, b) { const d1 = new Date(a); const d2 = new Date(b); d1.setHours(0, 0, 0, 0); d2.setHours(0, 0, 0, 0); return Math.round((d2 – d1) / (1000 * 60 * 60 * 24)); } function isThreeDayStep(dateStr, baseDateStr = todayISO()) { const diff = daysBetween(baseDateStr, dateStr); return diff % 3 === 0; } const STATUS_OPTIONS = [ { value: “concluido”, label: “Concluído”, chip: “✅ Concluído” }, { value: “andamento”, label: “Em andamento”, chip: “✍️ Em andamento” }, { value: “revisao”, label: “Em revisão”, chip: “👀 Em revisão” }, { value: “nao_iniciado”, label: “Não iniciado”, chip: “⏳ Não iniciado” }, { value: “agendado”, label: “Agendado”, chip: “📅 Agendado” }, { value: “nao_agendado”, label: “Não agendado”, chip: “❌ Não agendado” }, ]; const BLUE = “#1E3A8A”; // Tailwind blue-900 export default function App() { const [items, setItems] = useState(() => { try { const raw = localStorage.getItem(“imigramais-clientes”); return raw ? JSON.parse(raw) : []; } catch (e) { return []; } }); const [filter, setFilter] = useState(“”); const [statusFilter, setStatusFilter] = useState(“”); useEffect(() => { localStorage.setItem(“imigramais-clientes”, JSON.stringify(items)); }, [items]); // ===== Form state ===== const emptyForm = { id: null, nome: “”, responsavel: “”, prazo: todayISO(), status: “nao_iniciado”, telefone: “”, }; const [form, setForm] = useState(emptyForm); const [baseDate, setBaseDate] = useState(todayISO()); function resetForm() { setForm({ …emptyForm, prazo: snapToStep(todayISO(), baseDate) }); } function snapToStep(dateStr, baseDateStr) { // Snap the chosen date to the nearest valid 3-day step (future oriented) const base = new Date(baseDateStr); const chosen = new Date(dateStr); base.setHours(0, 0, 0, 0); chosen.setHours(0, 0, 0, 0); const diff = daysBetween(base.toISOString(), chosen.toISOString()); if (diff % 3 === 0) return dateStr; const forward = diff + (3 – (diff % 3)); const snapped = new Date(base); snapped.setDate(base.getDate() + forward); return snapped.toISOString().slice(0, 10); } const isValidStep = useMemo( () => isThreeDayStep(form.prazo, baseDate), [form.prazo, baseDate] ); function handleAddOrUpdate(e) { e.preventDefault(); if (!isValidStep) return; if (form.id) { setItems((curr) => curr.map((it) => (it.id === form.id ? form : it))); } else { setItems((curr) => [ …curr, { …form, id: crypto.randomUUID() }, ]); } resetForm(); } function handleEdit(id) { const it = items.find((i) => i.id === id); if (it) setForm({ …it }); } function handleDelete(id) { if (!confirm(“Remover este registro?”)) return; setItems((curr) => curr.filter((i) => i.id !== id)); } function addDaysToPrazo(days) { const d = new Date(form.prazo); d.setDate(d.getDate() + days); setForm((f) => ({ …f, prazo: d.toISOString().slice(0, 10) })); } const filtered = useMemo(() => { return items .filter((i) => (i.nome + i.responsavel + i.telefone) .toLowerCase() .includes(filter.toLowerCase()) ) .filter((i) => (statusFilter ? i.status === statusFilter : true)) .sort((a, b) => new Date(a.prazo) – new Date(b.prazo)); }, [items, filter, statusFilter]); // ===== CSV Export ===== function exportCSV() { const header = [“Nome”, “Responsável”, “Prazo”, “Status”, “Telefone”]; const rows = items.map((i) => [ i.nome, i.responsavel, i.prazo, STATUS_OPTIONS.find((o) => o.value === i.status)?.label || i.status, i.telefone, ]); const csv = [header, …rows] .map((r) => r.map((c) => `”${String(c).replaceAll(‘”‘, ‘””‘)}”`).join(“,”)) .join(“\n”); const blob = new Blob([csv], { type: “text/csv;charset=utf-8;” }); const url = URL.createObjectURL(blob); const a = document.createElement(“a”); a.href = url; a.download = “clientes-imigramais.csv”; a.click(); URL.revokeObjectURL(url); } // ===== UI ===== return (
{/* Top bar */}

Clientes Novembro

Painel de cadastro e acompanhamento (passos de 3 em 3 dias)

{/* Controls */}
Pesquisar setFilter(e.target.value)} />
Filtrar status setStatusFilter(e.target.value)} > Todos {STATUS_OPTIONS.map((s) => ( {s.label} ))}
Exportar CSV
{/* Form */}

{form.id ? “Editar cadastro” : “Novo cadastro”}

Nome setForm({ …form, nome: e.target.value })} />
Responsável setForm({ …form, responsavel: e.target.value })} />
Data base (múltiplos de 3 dias) setBaseDate(e.target.value)} />
Prazo setForm({ …form, prazo: e.target.value })} /> {!isValidStep && (

A data deve ser um múltiplo de 3 dias a partir da data base. setForm((f) => ({ …f, prazo: snapToStep(f.prazo, baseDate), }))} > Ajustar automaticamente

)}
{[3, 6, 9].map((n) => ( addDaysToPrazo(n)} className=”px-2 py-1 rounded-lg border hover:bg-gray-50″ > +{n}d ))}
Status setForm({ …form, status: e.target.value })} > {STATUS_OPTIONS.map((s) => ( {s.label} ))}
Telefone setForm({ …form, telefone: e.target.value.replace(/[^0-9]/g, “”) })} />
{form.id ? “Salvar alterações” : “Adicionar”} Limpar
{/* Table */}

Clientes Novembro

{filtered.length === 0 && ( )} {filtered.map((i) => ( ))}
Nome Responsável Prazo Status Telefone
Sem registros. Adicione um novo cadastro acima.
{i.nome}
{(i.responsavel || “?”).slice(0,1).toUpperCase()} {i.responsavel || “—”} {new Date(i.prazo).toLocaleDateString(“pt-BR”, { day: “2-digit”, month: “short”, year: “numeric” })} {i.telefone || “—”}
handleEdit(i.id)} className=”px-2 py-1 rounded-lg border hover:bg-gray-50″ title=”Editar” > ✏️ handleDelete(i.id)} className=”px-2 py-1 rounded-lg border hover:bg-gray-50″ title=”Excluir” > 🗑️
ImigraMais • Painel interno simples (dados ficam apenas neste navegador via LocalStorage)
); } function StatusChip({ value }) { const s = STATUS_OPTIONS.find((o) => o.value === value) || STATUS_OPTIONS[3]; return ( {s.chip.split(” “)[0]} {s.label} ); }

Deixe um comentário