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 (
);
}
function Toolbar({ filters, setFilters, onAdd, onExport, onImport }) {
return (
);
}
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 (
📜
Sem visitas cadastradas
Comece registrando sua primeira agenda de visita pastoral.
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"
/>
Digite o texto aqui...
