agenda pastoral

import React, { useEffect, useMemo, useState } from "react"; import { motion, AnimatePresence } from "framer-motion"; /** * Mini‑App: Agenda de Visitas Pastorais * ------------------------------------- * • Single‑file React component * • TailwindCSS para estilo (classe-based) * • Armazena dados em localStorage * • CRUD completo: criar, editar, concluir, excluir * • Filtros por status, período e busca * • Exportar/Importar CSV * • Gerar arquivo .ics (calendário) por visita * • Design limpo, responsivo, com destaques para atrasos * * Dica de deploy rápido: * - Vite + React (npm create vite@latest), substitua App.jsx por este arquivo. * - Habilite Tailwind (docs oficiais) e publique no Netlify/Vercel. */ // ------------------------ Utilidades ------------------------ const LS_KEY = "agenda_visitas_pastorais_v1"; const STATUS = { PENDENTE: "Pendente", CONCLUIDA: "Concluída", }; const TIPOS = [ "Visita domiciliar", "Visita hospitalar", "Aconselhamento", "Discipulado", "Evangelística", "Outros", ]; const SITUACAO_ESPIRITUAL = [ "Consolação", "Encorajamento", "Arrependimento", "Discipulado", "Acompanhamento de enfermidade", "Intercessão", "Outros", ]; function uid() { return Math.random().toString(36).slice(2) + Date.now().toString(36); } function toLocalDateInputValue(date) { const d = new Date(date); const pad = (n) => String(n).padStart(2, "0"); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; } function fromLocalDateInputValue(v) { // Trata como local 00:00 const [y, m, d] = v.split("-").map(Number); return new Date(y, m - 1, d).toISOString(); } function isPast(dateISO) { const today = new Date(); // zera horas today.setHours(0, 0, 0, 0); const d = new Date(dateISO); d.setHours(0, 0, 0, 0); return d.getTime() < today.getTime(); } function withinRange(dateISO, mode) { const d = new Date(dateISO); const now = new Date(); // Hoje const startToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()); const endToday = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999); if (mode === "todos") return true; if (mode === "hoje") { return d >= startToday && d <= endToday; } if (mode === "proximos7") { const end = new Date(startToday); end.setDate(end.getDate() + 7); return d >= startToday && d <= end; } if (mode === "estaSemana") { const day = startToday.getDay(); // 0-dom, 1-seg ... const mondayOffset = day === 0 ? -6 : 1 - day; // começa na segunda const monday = new Date(startToday); monday.setDate(monday.getDate() + mondayOffset); const sunday = new Date(monday); sunday.setDate(sunday.getDate() + 6); sunday.setHours(23, 59, 59, 999); return d >= monday && d <= sunday; } return true; } function download(filename, text) { const element = document.createElement("a"); const file = new Blob([text], { type: "text/plain;charset=utf-8" }); element.href = URL.createObjectURL(file); element.download = filename; document.body.appendChild(element); element.click(); element.remove(); } function toCSV(rows) { if (!rows.length) return ""; const headers = [ "id", "data", "nome", "tipo", "local", "telefone", "situacaoEspiritual", "motivo", "proximoContato", "status", "observacoes", "criadoEm", ]; const esc = (v) => { if (v == null) return ""; const s = String(v).replaceAll('"', '""'); return `"${s}"`; }; const lines = [headers.join(",")]; for (const r of rows) { lines.push( [ r.id, r.data, r.nome, r.tipo, r.local, r.telefone, r.situacaoEspiritual, r.motivo, r.proximoContato, r.status, r.observacoes, r.criadoEm, ] .map(esc) .join(",") ); } return lines.join("\n"); } function parseCSV(text) { // Parser simples (aspas duplas, separador vírgula). Para uso pastoral básico. // Sugestão: para cargas grandes, use papaparse no futuro. const lines = text.split(/\r?\n/).filter(Boolean); if (!lines.length) return []; const headers = lines[0].split(",").map((h) => h.replace(/^\"|\"$/g, "")); const rows = []; for (let i = 1; i < lines.length; i++) { const cols = []; let cur = ""; let inQ = false; for (let ch of lines[i]) { if (ch === '"') { if (inQ && cur.endsWith('"')) { cur = cur.slice(0, -1) + '"'; // escape interno } else { inQ = !inQ; cur += '"'; } } else if (ch === "," && !inQ) { cols.push(cur.replace(/^\"|\"$/g, "")); cur = ""; } else { cur += ch; } } cols.push(cur.replace(/^\"|\"$/g, "")); const obj = Object.fromEntries(headers.map((h, idx) => [h, cols[idx] ?? ""])); rows.push(obj); } return rows; } function buildICS(visit) { const dt = new Date(visit.data); const stamp = dt .toISOString() .replace(/[-:]/g, "") .replace(/\.[0-9]{3}Z$/, "Z"); const dtEnd = new Date(dt.getTime() + 60 * 60 * 1000); // 1h const stampEnd = dtEnd .toISOString() .replace(/[-:]/g, "") .replace(/\.[0-9]{3}Z$/, "Z"); const title = `Visita Pastoral: ${visit.nome ?? "Membro"}`; const desc = [ visit.motivo ? `Motivo: ${visit.motivo}` : null, visit.situacaoEspiritual ? `Foco espiritual: ${visit.situacaoEspiritual}` : null, visit.observacoes ? `Obs.: ${visit.observacoes}` : null, visit.telefone ? `Tel.: ${visit.telefone}` : null, ] .filter(Boolean) .join("\\n"); return `BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//Agenda Pastoral//PT-BR\nBEGIN:VEVENT\nUID:${visit.id}@agenda-pastoral\nDTSTAMP:${stamp}\nDTSTART:${stamp}\nDTEND:${stampEnd}\nSUMMARY:${title}\nLOCATION:${visit.local ?? ""}\nDESCRIPTION:${desc}\nEND:VEVENT\nEND:VCALENDAR`; } // ------------------------ Componentes UI ------------------------ function Pill({ children, className = "" }) { return ( {children} ); } function EmptyState({ onAdd }) { return (
📜

Sem visitas cadastradas

Comece registrando sua primeira agenda de visita pastoral.

); } function Toolbar({ filters, setFilters, onAdd, onExport, onImport }) { return (
setFilters((f) => ({ ...f, q: e.target.value }))} className="w-64 max-w-full px-3 py-2 rounded-xl border border-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500" />
); } function VisitRow({ v, onEdit, onToggle, onDelete, onICS }) { const overdue = v.status === STATUS.PENDENTE && isPast(v.data); return ( {toLocalDateInputValue(v.data)} {overdue && (
Atrasada
)} {v.nome} {v.motivo} {v.local} {v.situacaoEspiritual || "—"} {v.proximoContato ? toLocalDateInputValue(v.proximoContato) : "—"} {v.observacoes || ""}
); } function Modal({ open, onClose, children }) { if (!open) return null; return (
{children}
); } function VisitForm({ initial, onSave, onCancel }) { const [form, setForm] = useState( initial || { id: uid(), data: new Date().toISOString(), nome: "", tipo: TIPOS[0], local: "", telefone: "", situacaoEspiritual: "", motivo: "", proximoContato: "", status: STATUS.PENDENTE, observacoes: "", criadoEm: new Date().toISOString(), } ); const isEdit = Boolean(initial); function update(k, v) { setForm((f) => ({ ...f, [k]: v })); } function handleSubmit(e) { e.preventDefault(); if (!form.nome?.trim()) return alert("Informe o nome do irmão(ã)."); if (!form.data) return alert("Informe a data da visita."); onSave(form); } return (
update("data", fromLocalDateInputValue(e.target.value))} className="w-full px-3 py-2 rounded-xl border border-gray-200" required />
update("nome", e.target.value)} className="w-full px-3 py-2 rounded-xl border border-gray-200" placeholder="Irmão(ã)" required />
update("local", e.target.value)} className="w-full px-3 py-2 rounded-xl border border-gray-200" placeholder="Endereço, bairro, hospital..." />
update("telefone", e.target.value)} className="w-full px-3 py-2 rounded-xl border border-gray-200" placeholder="(xx) xxxxx-xxxx" />
update("motivo", e.target.value)} className="w-full px-3 py-2 rounded-xl border border-gray-200" placeholder="Consolo, enfermidade, oração pela família..." />
update("proximoContato", e.target.value ? fromLocalDateInputValue(e.target.value) : "") } className="w-full px-3 py-2 rounded-xl border border-gray-200" />

Digite o texto aqui...