CARONTE v1.0 - Plataforma de Gestão Social
This commit is contained in:
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
node_modules
|
||||||
|
.next
|
||||||
|
.venv
|
||||||
|
venv
|
||||||
|
__pycache__
|
||||||
|
*.db
|
||||||
|
uploads/
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
0
backend/app/__init__.py
Normal file
0
backend/app/__init__.py
Normal file
0
backend/app/api/__init__.py
Normal file
0
backend/app/api/__init__.py
Normal file
0
backend/app/api/v1/__init__.py
Normal file
0
backend/app/api/v1/__init__.py
Normal file
38
backend/app/api/v1/auth.py
Normal file
38
backend/app/api/v1/auth.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from fastapi.security import OAuth2PasswordRequestForm
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy import select
|
||||||
|
from app.core.database import get_db
|
||||||
|
from app.core.security import hash_password, verify_password, create_access_token, get_current_user_id
|
||||||
|
from app.models.usuario import Usuario
|
||||||
|
from app.schemas.schemas import UserCreate, UserOut, Token
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/auth", tags=["auth"])
|
||||||
|
|
||||||
|
@router.post("/registro", response_model=UserOut)
|
||||||
|
async def registro(data: UserCreate, db: AsyncSession = Depends(get_db)):
|
||||||
|
existing = await db.execute(select(Usuario).where(Usuario.email == data.email))
|
||||||
|
if existing.scalar_one_or_none():
|
||||||
|
raise HTTPException(400, "Email já cadastrado")
|
||||||
|
user = Usuario(nome=data.nome, email=data.email, senha_hash=hash_password(data.senha), telefone=data.telefone)
|
||||||
|
db.add(user)
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(user)
|
||||||
|
return user
|
||||||
|
|
||||||
|
@router.post("/login", response_model=Token)
|
||||||
|
async def login(form: OAuth2PasswordRequestForm = Depends(), db: AsyncSession = Depends(get_db)):
|
||||||
|
result = await db.execute(select(Usuario).where(Usuario.email == form.username))
|
||||||
|
user = result.scalar_one_or_none()
|
||||||
|
if not user or not verify_password(form.password, user.senha_hash):
|
||||||
|
raise HTTPException(401, "Credenciais inválidas")
|
||||||
|
token = create_access_token({"sub": str(user.id)})
|
||||||
|
return {"access_token": token}
|
||||||
|
|
||||||
|
@router.get("/me", response_model=UserOut)
|
||||||
|
async def me(user_id: int = Depends(get_current_user_id), db: AsyncSession = Depends(get_db)):
|
||||||
|
result = await db.execute(select(Usuario).where(Usuario.id == user_id))
|
||||||
|
user = result.scalar_one_or_none()
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(404, "Usuário não encontrado")
|
||||||
|
return user
|
||||||
32
backend/app/api/v1/beneficios.py
Normal file
32
backend/app/api/v1/beneficios.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy import select
|
||||||
|
from app.core.database import get_db
|
||||||
|
from app.core.security import get_current_user_id
|
||||||
|
from app.models.beneficio import Beneficio
|
||||||
|
from app.models.falecido import Falecido
|
||||||
|
from app.schemas.schemas import BeneficioOut
|
||||||
|
from app.services.beneficio_scanner import escanear_beneficios
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/familias/{familia_id}/beneficios", tags=["beneficios"])
|
||||||
|
|
||||||
|
@router.get("/", response_model=list[BeneficioOut])
|
||||||
|
async def listar(familia_id: int, db: AsyncSession = Depends(get_db)):
|
||||||
|
result = await db.execute(select(Beneficio).where(Beneficio.familia_id == familia_id))
|
||||||
|
return result.scalars().all()
|
||||||
|
|
||||||
|
@router.post("/scan", response_model=list[BeneficioOut])
|
||||||
|
async def scan(familia_id: int, db: AsyncSession = Depends(get_db)):
|
||||||
|
# Delete existing and rescan
|
||||||
|
result = await db.execute(select(Beneficio).where(Beneficio.familia_id == familia_id))
|
||||||
|
for b in result.scalars().all():
|
||||||
|
await db.delete(b)
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
result = await db.execute(select(Falecido).where(Falecido.familia_id == familia_id))
|
||||||
|
falecidos = result.scalars().all()
|
||||||
|
all_bens = []
|
||||||
|
for f in falecidos:
|
||||||
|
bens = await escanear_beneficios(db, familia_id, f)
|
||||||
|
all_bens.extend(bens)
|
||||||
|
return all_bens
|
||||||
41
backend/app/api/v1/checklist.py
Normal file
41
backend/app/api/v1/checklist.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy import select
|
||||||
|
from app.core.database import get_db
|
||||||
|
from app.core.security import get_current_user_id
|
||||||
|
from app.models.checklist import ChecklistItem
|
||||||
|
from app.schemas.schemas import ChecklistItemOut, ChecklistUpdateStatus
|
||||||
|
from app.services.checklist_engine import get_proximo_passo
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/familias/{familia_id}/checklist", tags=["checklist"])
|
||||||
|
|
||||||
|
@router.get("/", response_model=list[ChecklistItemOut])
|
||||||
|
async def listar(familia_id: int, db: AsyncSession = Depends(get_db)):
|
||||||
|
result = await db.execute(
|
||||||
|
select(ChecklistItem).where(ChecklistItem.familia_id == familia_id).order_by(ChecklistItem.ordem)
|
||||||
|
)
|
||||||
|
return result.scalars().all()
|
||||||
|
|
||||||
|
@router.put("/{item_id}", response_model=ChecklistItemOut)
|
||||||
|
async def atualizar_status(familia_id: int, item_id: int, data: ChecklistUpdateStatus, db: AsyncSession = Depends(get_db)):
|
||||||
|
result = await db.execute(select(ChecklistItem).where(ChecklistItem.id == item_id, ChecklistItem.familia_id == familia_id))
|
||||||
|
item = result.scalar_one_or_none()
|
||||||
|
if not item:
|
||||||
|
raise HTTPException(404, "Item não encontrado")
|
||||||
|
item.status = data.status
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(item)
|
||||||
|
return item
|
||||||
|
|
||||||
|
@router.get("/proximo", response_model=ChecklistItemOut | None)
|
||||||
|
async def proximo_passo(familia_id: int, db: AsyncSession = Depends(get_db)):
|
||||||
|
result = await db.execute(
|
||||||
|
select(ChecklistItem).where(ChecklistItem.familia_id == familia_id).order_by(ChecklistItem.ordem)
|
||||||
|
)
|
||||||
|
items = [{"id": i.id, "status": i.status, "ordem": i.ordem, "titulo": i.titulo, "descricao": i.descricao,
|
||||||
|
"fase": i.fase, "categoria": i.categoria, "prazo_dias": i.prazo_dias, "familia_id": i.familia_id,
|
||||||
|
"falecido_id": i.falecido_id, "dependencia_id": i.dependencia_id} for i in result.scalars().all()]
|
||||||
|
p = get_proximo_passo(items)
|
||||||
|
if not p:
|
||||||
|
return None
|
||||||
|
return p
|
||||||
54
backend/app/api/v1/dashboard.py
Normal file
54
backend/app/api/v1/dashboard.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy import select, func
|
||||||
|
from app.core.database import get_db
|
||||||
|
from app.core.security import get_current_user_id
|
||||||
|
from app.models.familia import Familia
|
||||||
|
from app.models.checklist import ChecklistItem
|
||||||
|
from app.models.beneficio import Beneficio
|
||||||
|
from app.models.documento import Documento
|
||||||
|
from app.schemas.schemas import DashboardOut
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/dashboard", tags=["dashboard"])
|
||||||
|
|
||||||
|
@router.get("/", response_model=DashboardOut)
|
||||||
|
async def dashboard(user_id: int = Depends(get_current_user_id), db: AsyncSession = Depends(get_db)):
|
||||||
|
fams = await db.execute(select(Familia).where(Familia.usuario_id == user_id))
|
||||||
|
familias = fams.scalars().all()
|
||||||
|
fam_ids = [f.id for f in familias]
|
||||||
|
|
||||||
|
pendentes = 0
|
||||||
|
bens_count = 0
|
||||||
|
docs_count = 0
|
||||||
|
fam_list = []
|
||||||
|
|
||||||
|
for fam in familias:
|
||||||
|
ch = await db.execute(select(ChecklistItem).where(ChecklistItem.familia_id == fam.id))
|
||||||
|
items = ch.scalars().all()
|
||||||
|
total = len(items)
|
||||||
|
done = len([i for i in items if i.status == "concluido"])
|
||||||
|
pendentes += len([i for i in items if i.status == "pendente"])
|
||||||
|
|
||||||
|
bn = await db.execute(select(func.count()).select_from(Beneficio).where(Beneficio.familia_id == fam.id))
|
||||||
|
bc = bn.scalar() or 0
|
||||||
|
bens_count += bc
|
||||||
|
|
||||||
|
dc = await db.execute(select(func.count()).select_from(Documento).where(Documento.familia_id == fam.id))
|
||||||
|
docs_count += dc.scalar() or 0
|
||||||
|
|
||||||
|
fam_list.append({
|
||||||
|
"id": fam.id,
|
||||||
|
"nome": fam.nome,
|
||||||
|
"total_items": total,
|
||||||
|
"concluidos": done,
|
||||||
|
"progresso": round(done / total * 100) if total else 0,
|
||||||
|
"beneficios": bc,
|
||||||
|
})
|
||||||
|
|
||||||
|
return DashboardOut(
|
||||||
|
familias_ativas=len(familias),
|
||||||
|
itens_pendentes=pendentes,
|
||||||
|
beneficios_encontrados=bens_count,
|
||||||
|
documentos_gerados=docs_count,
|
||||||
|
familias=fam_list,
|
||||||
|
)
|
||||||
62
backend/app/api/v1/documentos.py
Normal file
62
backend/app/api/v1/documentos.py
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from fastapi.responses import FileResponse
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy import select
|
||||||
|
from app.core.database import get_db
|
||||||
|
from app.core.security import get_current_user_id
|
||||||
|
from app.models.documento import Documento
|
||||||
|
from app.models.falecido import Falecido
|
||||||
|
from app.models.familia import Familia, MembroFamilia
|
||||||
|
from app.schemas.schemas import DocumentoOut, DocumentoGerarRequest
|
||||||
|
from app.services.document_generator import GENERATORS
|
||||||
|
import os
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/familias/{familia_id}/documentos", tags=["documentos"])
|
||||||
|
|
||||||
|
@router.get("/", response_model=list[DocumentoOut])
|
||||||
|
async def listar(familia_id: int, db: AsyncSession = Depends(get_db)):
|
||||||
|
result = await db.execute(select(Documento).where(Documento.familia_id == familia_id))
|
||||||
|
return result.scalars().all()
|
||||||
|
|
||||||
|
@router.post("/gerar", response_model=DocumentoOut)
|
||||||
|
async def gerar(familia_id: int, data: DocumentoGerarRequest, db: AsyncSession = Depends(get_db)):
|
||||||
|
gen = GENERATORS.get(data.tipo)
|
||||||
|
if not gen:
|
||||||
|
raise HTTPException(400, f"Tipo de documento inválido: {data.tipo}")
|
||||||
|
|
||||||
|
fam = await db.execute(select(Familia).where(Familia.id == familia_id))
|
||||||
|
familia = fam.scalar_one_or_none()
|
||||||
|
if not familia:
|
||||||
|
raise HTTPException(404, "Família não encontrada")
|
||||||
|
|
||||||
|
fal = await db.execute(select(Falecido).where(Falecido.id == data.falecido_id))
|
||||||
|
falecido = fal.scalar_one_or_none()
|
||||||
|
if not falecido:
|
||||||
|
raise HTTPException(404, "Falecido não encontrado")
|
||||||
|
|
||||||
|
membros = await db.execute(select(MembroFamilia).where(MembroFamilia.familia_id == familia_id))
|
||||||
|
membro = membros.scalars().first()
|
||||||
|
membro_nome = membro.nome if membro else "Representante da Família"
|
||||||
|
|
||||||
|
path = gen(familia.nome, falecido.nome, membro_nome, falecido.cpf or "000.000.000-00")
|
||||||
|
|
||||||
|
doc = Documento(
|
||||||
|
familia_id=familia_id,
|
||||||
|
falecido_id=data.falecido_id,
|
||||||
|
tipo=data.tipo,
|
||||||
|
nome=f"{data.tipo.replace('_',' ').title()} - {falecido.nome}",
|
||||||
|
arquivo_path=path,
|
||||||
|
status="gerado",
|
||||||
|
)
|
||||||
|
db.add(doc)
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(doc)
|
||||||
|
return doc
|
||||||
|
|
||||||
|
@router.get("/{doc_id}/download")
|
||||||
|
async def download(familia_id: int, doc_id: int, db: AsyncSession = Depends(get_db)):
|
||||||
|
result = await db.execute(select(Documento).where(Documento.id == doc_id, Documento.familia_id == familia_id))
|
||||||
|
doc = result.scalar_one_or_none()
|
||||||
|
if not doc or not doc.arquivo_path or not os.path.exists(doc.arquivo_path):
|
||||||
|
raise HTTPException(404, "Documento não encontrado")
|
||||||
|
return FileResponse(doc.arquivo_path, filename=os.path.basename(doc.arquivo_path), media_type="application/pdf")
|
||||||
61
backend/app/api/v1/familias.py
Normal file
61
backend/app/api/v1/familias.py
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy import select
|
||||||
|
from app.core.database import get_db
|
||||||
|
from app.core.security import get_current_user_id
|
||||||
|
from app.models.familia import Familia, MembroFamilia
|
||||||
|
from app.models.falecido import Falecido
|
||||||
|
from app.schemas.schemas import FamiliaCreate, FamiliaOut, MembroCreate, MembroOut, FalecidoCreate, FalecidoOut
|
||||||
|
from app.services.checklist_engine import gerar_checklist
|
||||||
|
from app.services.beneficio_scanner import escanear_beneficios
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/familias", tags=["familias"])
|
||||||
|
|
||||||
|
@router.get("/", response_model=list[FamiliaOut])
|
||||||
|
async def listar(user_id: int = Depends(get_current_user_id), db: AsyncSession = Depends(get_db)):
|
||||||
|
result = await db.execute(select(Familia).where(Familia.usuario_id == user_id))
|
||||||
|
return result.scalars().all()
|
||||||
|
|
||||||
|
@router.post("/", response_model=FamiliaOut)
|
||||||
|
async def criar(data: FamiliaCreate, user_id: int = Depends(get_current_user_id), db: AsyncSession = Depends(get_db)):
|
||||||
|
f = Familia(nome=data.nome, usuario_id=user_id)
|
||||||
|
db.add(f)
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(f)
|
||||||
|
return f
|
||||||
|
|
||||||
|
@router.get("/{id}", response_model=FamiliaOut)
|
||||||
|
async def detalhe(id: int, user_id: int = Depends(get_current_user_id), db: AsyncSession = Depends(get_db)):
|
||||||
|
result = await db.execute(select(Familia).where(Familia.id == id, Familia.usuario_id == user_id))
|
||||||
|
f = result.scalar_one_or_none()
|
||||||
|
if not f:
|
||||||
|
raise HTTPException(404, "Família não encontrada")
|
||||||
|
return f
|
||||||
|
|
||||||
|
@router.post("/{id}/membros", response_model=MembroOut)
|
||||||
|
async def add_membro(id: int, data: MembroCreate, db: AsyncSession = Depends(get_db)):
|
||||||
|
m = MembroFamilia(familia_id=id, **data.model_dump())
|
||||||
|
db.add(m)
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(m)
|
||||||
|
return m
|
||||||
|
|
||||||
|
@router.get("/{id}/membros", response_model=list[MembroOut])
|
||||||
|
async def listar_membros(id: int, db: AsyncSession = Depends(get_db)):
|
||||||
|
result = await db.execute(select(MembroFamilia).where(MembroFamilia.familia_id == id))
|
||||||
|
return result.scalars().all()
|
||||||
|
|
||||||
|
@router.post("/{id}/falecido", response_model=FalecidoOut)
|
||||||
|
async def registrar_falecido(id: int, data: FalecidoCreate, db: AsyncSession = Depends(get_db)):
|
||||||
|
f = Falecido(familia_id=id, **data.model_dump())
|
||||||
|
db.add(f)
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(f)
|
||||||
|
await gerar_checklist(db, id, f)
|
||||||
|
await escanear_beneficios(db, id, f)
|
||||||
|
return f
|
||||||
|
|
||||||
|
@router.get("/{id}/falecidos", response_model=list[FalecidoOut])
|
||||||
|
async def listar_falecidos(id: int, db: AsyncSession = Depends(get_db)):
|
||||||
|
result = await db.execute(select(Falecido).where(Falecido.familia_id == id))
|
||||||
|
return result.scalars().all()
|
||||||
0
backend/app/core/__init__.py
Normal file
0
backend/app/core/__init__.py
Normal file
14
backend/app/core/config.py
Normal file
14
backend/app/core/config.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
from pydantic_settings import BaseSettings
|
||||||
|
|
||||||
|
class Settings(BaseSettings):
|
||||||
|
APP_NAME: str = "CARONTE"
|
||||||
|
SECRET_KEY: str = "caronte-secret-key-change-in-production"
|
||||||
|
ALGORITHM: str = "HS256"
|
||||||
|
ACCESS_TOKEN_EXPIRE_MINUTES: int = 1440
|
||||||
|
DATABASE_URL: str = "sqlite+aiosqlite:///./caronte.db"
|
||||||
|
UPLOAD_DIR: str = "./uploads"
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
env_file = ".env"
|
||||||
|
|
||||||
|
settings = Settings()
|
||||||
17
backend/app/core/database.py
Normal file
17
backend/app/core/database.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
||||||
|
from sqlalchemy.orm import DeclarativeBase
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
|
engine = create_async_engine(settings.DATABASE_URL, echo=False)
|
||||||
|
async_session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||||
|
|
||||||
|
class Base(DeclarativeBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def get_db():
|
||||||
|
async with async_session() as session:
|
||||||
|
yield session
|
||||||
|
|
||||||
|
async def init_db():
|
||||||
|
async with engine.begin() as conn:
|
||||||
|
await conn.run_sync(Base.metadata.create_all)
|
||||||
31
backend/app/core/security.py
Normal file
31
backend/app/core/security.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
from datetime import datetime, timedelta
|
||||||
|
from jose import JWTError, jwt
|
||||||
|
from passlib.context import CryptContext
|
||||||
|
from fastapi import Depends, HTTPException, status
|
||||||
|
from fastapi.security import OAuth2PasswordBearer
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
|
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||||
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login")
|
||||||
|
|
||||||
|
def hash_password(password: str) -> str:
|
||||||
|
return pwd_context.hash(password)
|
||||||
|
|
||||||
|
def verify_password(plain: str, hashed: str) -> bool:
|
||||||
|
return pwd_context.verify(plain, hashed)
|
||||||
|
|
||||||
|
def create_access_token(data: dict) -> str:
|
||||||
|
to_encode = data.copy()
|
||||||
|
expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||||
|
to_encode.update({"exp": expire})
|
||||||
|
return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
||||||
|
|
||||||
|
async def get_current_user_id(token: str = Depends(oauth2_scheme)) -> int:
|
||||||
|
try:
|
||||||
|
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
||||||
|
user_id = payload.get("sub")
|
||||||
|
if user_id is None:
|
||||||
|
raise HTTPException(status_code=401, detail="Token inválido")
|
||||||
|
return int(user_id)
|
||||||
|
except JWTError:
|
||||||
|
raise HTTPException(status_code=401, detail="Token inválido")
|
||||||
182
backend/app/data/checklist_templates.json
Normal file
182
backend/app/data/checklist_templates.json
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"titulo": "Obter Declaração de Óbito",
|
||||||
|
"descricao": "Solicitar a declaração de óbito no hospital ou IML onde ocorreu o falecimento.",
|
||||||
|
"fase": "imediato",
|
||||||
|
"categoria": "certidoes",
|
||||||
|
"prazo_dias": 1,
|
||||||
|
"ordem": 1,
|
||||||
|
"condicao": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"titulo": "Registrar Certidão de Óbito",
|
||||||
|
"descricao": "Levar a declaração de óbito ao cartório de registro civil para emitir a certidão de óbito.",
|
||||||
|
"fase": "imediato",
|
||||||
|
"categoria": "certidoes",
|
||||||
|
"prazo_dias": 2,
|
||||||
|
"ordem": 2,
|
||||||
|
"condicao": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"titulo": "Providenciar Sepultamento/Cremação",
|
||||||
|
"descricao": "Contratar serviço funerário e realizar o sepultamento ou cremação.",
|
||||||
|
"fase": "imediato",
|
||||||
|
"categoria": "certidoes",
|
||||||
|
"prazo_dias": 3,
|
||||||
|
"ordem": 3,
|
||||||
|
"condicao": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"titulo": "Comunicar Empregador",
|
||||||
|
"descricao": "Informar o empregador sobre o falecimento para rescisão do contrato de trabalho.",
|
||||||
|
"fase": "primeira_semana",
|
||||||
|
"categoria": "trabalhista",
|
||||||
|
"prazo_dias": 7,
|
||||||
|
"ordem": 4,
|
||||||
|
"condicao": "tinha_carteira_assinada"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"titulo": "Comunicar INSS",
|
||||||
|
"descricao": "Informar o INSS sobre o óbito e verificar benefícios como pensão por morte.",
|
||||||
|
"fase": "primeira_semana",
|
||||||
|
"categoria": "inss",
|
||||||
|
"prazo_dias": 7,
|
||||||
|
"ordem": 5,
|
||||||
|
"condicao": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"titulo": "Solicitar Pensão por Morte",
|
||||||
|
"descricao": "Requerer pensão por morte no INSS (Meu INSS ou agência). Prazo ideal: até 90 dias do óbito para receber desde a data do falecimento.",
|
||||||
|
"fase": "primeira_semana",
|
||||||
|
"categoria": "inss",
|
||||||
|
"prazo_dias": 90,
|
||||||
|
"ordem": 6,
|
||||||
|
"condicao": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"titulo": "Solicitar Saque FGTS",
|
||||||
|
"descricao": "Comparecer à Caixa Econômica Federal com documentos para saque do FGTS do falecido.",
|
||||||
|
"fase": "primeira_semana",
|
||||||
|
"categoria": "fgts",
|
||||||
|
"prazo_dias": 14,
|
||||||
|
"ordem": 7,
|
||||||
|
"condicao": "tinha_fgts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"titulo": "Solicitar Saque PIS/PASEP",
|
||||||
|
"descricao": "Verificar saldo PIS/PASEP e solicitar saque na Caixa (PIS) ou Banco do Brasil (PASEP).",
|
||||||
|
"fase": "primeira_semana",
|
||||||
|
"categoria": "fgts",
|
||||||
|
"prazo_dias": 14,
|
||||||
|
"ordem": 8,
|
||||||
|
"condicao": "tinha_carteira_assinada"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"titulo": "Comunicar Bancos",
|
||||||
|
"descricao": "Informar os bancos onde o falecido tinha conta sobre o óbito. Solicitar extrato e bloqueio.",
|
||||||
|
"fase": "primeira_semana",
|
||||||
|
"categoria": "financeiro",
|
||||||
|
"prazo_dias": 10,
|
||||||
|
"ordem": 9,
|
||||||
|
"condicao": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"titulo": "Acionar Seguro de Vida",
|
||||||
|
"descricao": "Entrar em contato com a seguradora para acionar o seguro de vida.",
|
||||||
|
"fase": "primeira_semana",
|
||||||
|
"categoria": "seguros",
|
||||||
|
"prazo_dias": 10,
|
||||||
|
"ordem": 10,
|
||||||
|
"condicao": "tinha_seguro_vida"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"titulo": "Solicitar DPVAT (se acidente)",
|
||||||
|
"descricao": "Se o óbito foi por acidente de trânsito, solicitar indenização do DPVAT.",
|
||||||
|
"fase": "30_dias",
|
||||||
|
"categoria": "seguros",
|
||||||
|
"prazo_dias": 30,
|
||||||
|
"ordem": 11,
|
||||||
|
"condicao": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"titulo": "Consultar Receita Federal (e-CAC)",
|
||||||
|
"descricao": "Verificar pendências fiscais do falecido e situação do CPF.",
|
||||||
|
"fase": "30_dias",
|
||||||
|
"categoria": "fiscal",
|
||||||
|
"prazo_dias": 30,
|
||||||
|
"ordem": 12,
|
||||||
|
"condicao": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"titulo": "Transferir Veículos",
|
||||||
|
"descricao": "Iniciar processo de transferência de veículos no DETRAN.",
|
||||||
|
"fase": "30_dias",
|
||||||
|
"categoria": "patrimonio",
|
||||||
|
"prazo_dias": 30,
|
||||||
|
"ordem": 13,
|
||||||
|
"condicao": "tinha_veiculos"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"titulo": "Iniciar Inventário",
|
||||||
|
"descricao": "Contratar advogado e iniciar inventário judicial ou extrajudicial. Prazo legal: 60 dias do óbito.",
|
||||||
|
"fase": "30_dias",
|
||||||
|
"categoria": "inventario",
|
||||||
|
"prazo_dias": 60,
|
||||||
|
"ordem": 14,
|
||||||
|
"condicao": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"titulo": "Levantar Bens Imóveis",
|
||||||
|
"descricao": "Solicitar certidões de matrícula dos imóveis nos cartórios de registro de imóveis.",
|
||||||
|
"fase": "30_dias",
|
||||||
|
"categoria": "inventario",
|
||||||
|
"prazo_dias": 45,
|
||||||
|
"ordem": 15,
|
||||||
|
"condicao": "tinha_imoveis"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"titulo": "Fazer Declaração Final de Espólio (IR)",
|
||||||
|
"descricao": "Declarar imposto de renda do falecido referente ao ano do óbito (declaração inicial de espólio).",
|
||||||
|
"fase": "60_dias",
|
||||||
|
"categoria": "fiscal",
|
||||||
|
"prazo_dias": 180,
|
||||||
|
"ordem": 16,
|
||||||
|
"condicao": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"titulo": "Cancelar CPF do Falecido",
|
||||||
|
"descricao": "Solicitar a regularização do CPF como 'titular falecido' na Receita Federal.",
|
||||||
|
"fase": "60_dias",
|
||||||
|
"categoria": "fiscal",
|
||||||
|
"prazo_dias": 90,
|
||||||
|
"ordem": 17,
|
||||||
|
"condicao": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"titulo": "Cancelar Serviços e Assinaturas",
|
||||||
|
"descricao": "Cancelar telefone, internet, plano de saúde, assinaturas e cartões de crédito.",
|
||||||
|
"fase": "30_dias",
|
||||||
|
"categoria": "administrativo",
|
||||||
|
"prazo_dias": 30,
|
||||||
|
"ordem": 18,
|
||||||
|
"condicao": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"titulo": "Transferir Titularidade de Serviços",
|
||||||
|
"descricao": "Transferir contas de água, luz, gás para nome de familiar.",
|
||||||
|
"fase": "30_dias",
|
||||||
|
"categoria": "administrativo",
|
||||||
|
"prazo_dias": 45,
|
||||||
|
"ordem": 19,
|
||||||
|
"condicao": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"titulo": "Concluir Partilha de Bens",
|
||||||
|
"descricao": "Finalizar o inventário com a partilha formal dos bens entre os herdeiros.",
|
||||||
|
"fase": "60_dias",
|
||||||
|
"categoria": "inventario",
|
||||||
|
"prazo_dias": 365,
|
||||||
|
"ordem": 20,
|
||||||
|
"condicao": null
|
||||||
|
}
|
||||||
|
]
|
||||||
31
backend/app/main.py
Normal file
31
backend/app/main.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
from app.core.database import init_db
|
||||||
|
from app.api.v1 import auth, familias, checklist, beneficios, documentos, dashboard
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI):
|
||||||
|
await init_db()
|
||||||
|
yield
|
||||||
|
|
||||||
|
app = FastAPI(title="CARONTE API", description="O barqueiro que guia famílias pelo rio burocrático pós-óbito", version="1.0.0", lifespan=lifespan)
|
||||||
|
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
app.include_router(auth.router, prefix="/api/v1")
|
||||||
|
app.include_router(familias.router, prefix="/api/v1")
|
||||||
|
app.include_router(checklist.router, prefix="/api/v1")
|
||||||
|
app.include_router(beneficios.router, prefix="/api/v1")
|
||||||
|
app.include_router(documentos.router, prefix="/api/v1")
|
||||||
|
app.include_router(dashboard.router, prefix="/api/v1")
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
async def root():
|
||||||
|
return {"message": "🚣 CARONTE - Guiando famílias pelo rio burocrático", "docs": "/docs"}
|
||||||
6
backend/app/models/__init__.py
Normal file
6
backend/app/models/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from app.models.usuario import Usuario
|
||||||
|
from app.models.familia import Familia, MembroFamilia
|
||||||
|
from app.models.falecido import Falecido
|
||||||
|
from app.models.checklist import ChecklistItem
|
||||||
|
from app.models.beneficio import Beneficio
|
||||||
|
from app.models.documento import Documento
|
||||||
16
backend/app/models/beneficio.py
Normal file
16
backend/app/models/beneficio.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Float
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
from app.core.database import Base
|
||||||
|
|
||||||
|
class Beneficio(Base):
|
||||||
|
__tablename__ = "beneficios"
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
familia_id = Column(Integer, ForeignKey("familias.id"), nullable=False)
|
||||||
|
falecido_id = Column(Integer, ForeignKey("falecidos.id"), nullable=False)
|
||||||
|
tipo = Column(String, nullable=False) # fgts, pis, pensao_morte, seguro_vida, seguro_dpvat
|
||||||
|
nome = Column(String, nullable=False)
|
||||||
|
descricao = Column(String, nullable=True)
|
||||||
|
status = Column(String, default="identificado") # identificado, em_processo, sacado
|
||||||
|
valor_estimado = Column(Float, nullable=True)
|
||||||
|
valor_sacado = Column(Float, nullable=True)
|
||||||
|
created_at = Column(DateTime, server_default=func.now())
|
||||||
19
backend/app/models/checklist.py
Normal file
19
backend/app/models/checklist.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Text
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
from app.core.database import Base
|
||||||
|
|
||||||
|
class ChecklistItem(Base):
|
||||||
|
__tablename__ = "checklist_items"
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
familia_id = Column(Integer, ForeignKey("familias.id"), nullable=False)
|
||||||
|
falecido_id = Column(Integer, ForeignKey("falecidos.id"), nullable=False)
|
||||||
|
titulo = Column(String, nullable=False)
|
||||||
|
descricao = Column(Text, nullable=True)
|
||||||
|
fase = Column(String, nullable=False) # imediato, primeira_semana, 30_dias, 60_dias
|
||||||
|
categoria = Column(String, nullable=False) # certidoes, inss, fgts, inventario, etc
|
||||||
|
status = Column(String, default="pendente") # pendente, andamento, concluido
|
||||||
|
prazo_dias = Column(Integer, nullable=True)
|
||||||
|
ordem = Column(Integer, default=0)
|
||||||
|
dependencia_id = Column(Integer, nullable=True)
|
||||||
|
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
||||||
|
created_at = Column(DateTime, server_default=func.now())
|
||||||
14
backend/app/models/documento.py
Normal file
14
backend/app/models/documento.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
from app.core.database import Base
|
||||||
|
|
||||||
|
class Documento(Base):
|
||||||
|
__tablename__ = "documentos"
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
familia_id = Column(Integer, ForeignKey("familias.id"), nullable=False)
|
||||||
|
falecido_id = Column(Integer, ForeignKey("falecidos.id"), nullable=True)
|
||||||
|
tipo = Column(String, nullable=False) # procuracao, requerimento_fgts, peticao_pensao, alvara
|
||||||
|
nome = Column(String, nullable=False)
|
||||||
|
arquivo_path = Column(String, nullable=True)
|
||||||
|
status = Column(String, default="gerado") # gerado, enviado, aprovado
|
||||||
|
created_at = Column(DateTime, server_default=func.now())
|
||||||
23
backend/app/models/falecido.py
Normal file
23
backend/app/models/falecido.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
from sqlalchemy import Column, Integer, String, DateTime, Date, ForeignKey, Float
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
from app.core.database import Base
|
||||||
|
|
||||||
|
class Falecido(Base):
|
||||||
|
__tablename__ = "falecidos"
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
familia_id = Column(Integer, ForeignKey("familias.id"), nullable=False)
|
||||||
|
nome = Column(String, nullable=False)
|
||||||
|
cpf = Column(String, nullable=True)
|
||||||
|
data_nascimento = Column(Date, nullable=True)
|
||||||
|
data_obito = Column(Date, nullable=False)
|
||||||
|
causa_obito = Column(String, nullable=True)
|
||||||
|
tipo_vinculo = Column(String, nullable=False) # empregado, aposentado, autonomo, servidor
|
||||||
|
empregador = Column(String, nullable=True)
|
||||||
|
tinha_carteira_assinada = Column(Integer, default=0)
|
||||||
|
tinha_fgts = Column(Integer, default=0)
|
||||||
|
era_aposentado = Column(Integer, default=0)
|
||||||
|
tinha_seguro_vida = Column(Integer, default=0)
|
||||||
|
tinha_imoveis = Column(Integer, default=0)
|
||||||
|
tinha_veiculos = Column(Integer, default=0)
|
||||||
|
salario_estimado = Column(Float, nullable=True)
|
||||||
|
created_at = Column(DateTime, server_default=func.now())
|
||||||
20
backend/app/models/familia.py
Normal file
20
backend/app/models/familia.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
from app.core.database import Base
|
||||||
|
|
||||||
|
class Familia(Base):
|
||||||
|
__tablename__ = "familias"
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
nome = Column(String, nullable=False)
|
||||||
|
usuario_id = Column(Integer, ForeignKey("usuarios.id"), nullable=False)
|
||||||
|
created_at = Column(DateTime, server_default=func.now())
|
||||||
|
|
||||||
|
class MembroFamilia(Base):
|
||||||
|
__tablename__ = "membros_familia"
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
familia_id = Column(Integer, ForeignKey("familias.id"), nullable=False)
|
||||||
|
nome = Column(String, nullable=False)
|
||||||
|
parentesco = Column(String, nullable=False)
|
||||||
|
cpf = Column(String, nullable=True)
|
||||||
|
telefone = Column(String, nullable=True)
|
||||||
|
email = Column(String, nullable=True)
|
||||||
12
backend/app/models/usuario.py
Normal file
12
backend/app/models/usuario.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from sqlalchemy import Column, Integer, String, DateTime
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
from app.core.database import Base
|
||||||
|
|
||||||
|
class Usuario(Base):
|
||||||
|
__tablename__ = "usuarios"
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
nome = Column(String, nullable=False)
|
||||||
|
email = Column(String, unique=True, nullable=False)
|
||||||
|
senha_hash = Column(String, nullable=False)
|
||||||
|
telefone = Column(String, nullable=True)
|
||||||
|
created_at = Column(DateTime, server_default=func.now())
|
||||||
0
backend/app/schemas/__init__.py
Normal file
0
backend/app/schemas/__init__.py
Normal file
151
backend/app/schemas/schemas.py
Normal file
151
backend/app/schemas/schemas.py
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Optional
|
||||||
|
from datetime import date, datetime
|
||||||
|
|
||||||
|
# Auth
|
||||||
|
class UserCreate(BaseModel):
|
||||||
|
nome: str
|
||||||
|
email: str
|
||||||
|
senha: str
|
||||||
|
telefone: Optional[str] = None
|
||||||
|
|
||||||
|
class UserLogin(BaseModel):
|
||||||
|
email: str
|
||||||
|
senha: str
|
||||||
|
|
||||||
|
class UserOut(BaseModel):
|
||||||
|
id: int
|
||||||
|
nome: str
|
||||||
|
email: str
|
||||||
|
telefone: Optional[str] = None
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
class Token(BaseModel):
|
||||||
|
access_token: str
|
||||||
|
token_type: str = "bearer"
|
||||||
|
|
||||||
|
# Familia
|
||||||
|
class FamiliaCreate(BaseModel):
|
||||||
|
nome: str
|
||||||
|
|
||||||
|
class FamiliaOut(BaseModel):
|
||||||
|
id: int
|
||||||
|
nome: str
|
||||||
|
usuario_id: int
|
||||||
|
created_at: Optional[datetime] = None
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
class MembroCreate(BaseModel):
|
||||||
|
nome: str
|
||||||
|
parentesco: str
|
||||||
|
cpf: Optional[str] = None
|
||||||
|
telefone: Optional[str] = None
|
||||||
|
email: Optional[str] = None
|
||||||
|
|
||||||
|
class MembroOut(BaseModel):
|
||||||
|
id: int
|
||||||
|
familia_id: int
|
||||||
|
nome: str
|
||||||
|
parentesco: str
|
||||||
|
cpf: Optional[str] = None
|
||||||
|
telefone: Optional[str] = None
|
||||||
|
email: Optional[str] = None
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
# Falecido
|
||||||
|
class FalecidoCreate(BaseModel):
|
||||||
|
nome: str
|
||||||
|
cpf: Optional[str] = None
|
||||||
|
data_nascimento: Optional[date] = None
|
||||||
|
data_obito: date
|
||||||
|
causa_obito: Optional[str] = None
|
||||||
|
tipo_vinculo: str
|
||||||
|
empregador: Optional[str] = None
|
||||||
|
tinha_carteira_assinada: int = 0
|
||||||
|
tinha_fgts: int = 0
|
||||||
|
era_aposentado: int = 0
|
||||||
|
tinha_seguro_vida: int = 0
|
||||||
|
tinha_imoveis: int = 0
|
||||||
|
tinha_veiculos: int = 0
|
||||||
|
salario_estimado: Optional[float] = None
|
||||||
|
|
||||||
|
class FalecidoOut(BaseModel):
|
||||||
|
id: int
|
||||||
|
familia_id: int
|
||||||
|
nome: str
|
||||||
|
cpf: Optional[str] = None
|
||||||
|
data_nascimento: Optional[date] = None
|
||||||
|
data_obito: date
|
||||||
|
causa_obito: Optional[str] = None
|
||||||
|
tipo_vinculo: str
|
||||||
|
empregador: Optional[str] = None
|
||||||
|
tinha_carteira_assinada: int = 0
|
||||||
|
tinha_fgts: int = 0
|
||||||
|
era_aposentado: int = 0
|
||||||
|
tinha_seguro_vida: int = 0
|
||||||
|
tinha_imoveis: int = 0
|
||||||
|
tinha_veiculos: int = 0
|
||||||
|
salario_estimado: Optional[float] = None
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
# Checklist
|
||||||
|
class ChecklistItemOut(BaseModel):
|
||||||
|
id: int
|
||||||
|
familia_id: int
|
||||||
|
falecido_id: int
|
||||||
|
titulo: str
|
||||||
|
descricao: Optional[str] = None
|
||||||
|
fase: str
|
||||||
|
categoria: str
|
||||||
|
status: str
|
||||||
|
prazo_dias: Optional[int] = None
|
||||||
|
ordem: int = 0
|
||||||
|
dependencia_id: Optional[int] = None
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
class ChecklistUpdateStatus(BaseModel):
|
||||||
|
status: str
|
||||||
|
|
||||||
|
# Beneficio
|
||||||
|
class BeneficioOut(BaseModel):
|
||||||
|
id: int
|
||||||
|
familia_id: int
|
||||||
|
falecido_id: int
|
||||||
|
tipo: str
|
||||||
|
nome: str
|
||||||
|
descricao: Optional[str] = None
|
||||||
|
status: str
|
||||||
|
valor_estimado: Optional[float] = None
|
||||||
|
valor_sacado: Optional[float] = None
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
# Documento
|
||||||
|
class DocumentoOut(BaseModel):
|
||||||
|
id: int
|
||||||
|
familia_id: int
|
||||||
|
falecido_id: Optional[int] = None
|
||||||
|
tipo: str
|
||||||
|
nome: str
|
||||||
|
arquivo_path: Optional[str] = None
|
||||||
|
status: str
|
||||||
|
created_at: Optional[datetime] = None
|
||||||
|
class Config:
|
||||||
|
from_attributes = True
|
||||||
|
|
||||||
|
class DocumentoGerarRequest(BaseModel):
|
||||||
|
tipo: str
|
||||||
|
falecido_id: int
|
||||||
|
|
||||||
|
# Dashboard
|
||||||
|
class DashboardOut(BaseModel):
|
||||||
|
familias_ativas: int
|
||||||
|
itens_pendentes: int
|
||||||
|
beneficios_encontrados: int
|
||||||
|
documentos_gerados: int
|
||||||
|
familias: list = []
|
||||||
0
backend/app/services/__init__.py
Normal file
0
backend/app/services/__init__.py
Normal file
64
backend/app/services/beneficio_scanner.py
Normal file
64
backend/app/services/beneficio_scanner.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from app.models.beneficio import Beneficio
|
||||||
|
from app.models.falecido import Falecido
|
||||||
|
|
||||||
|
BENEFICIOS_MAP = [
|
||||||
|
{
|
||||||
|
"condicao": "tinha_fgts",
|
||||||
|
"tipo": "fgts",
|
||||||
|
"nome": "Saque FGTS",
|
||||||
|
"descricao": "Saldo do FGTS disponível para saque pelos dependentes habilitados.",
|
||||||
|
"valor_mult": 3.5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"condicao": "tinha_carteira_assinada",
|
||||||
|
"tipo": "pis",
|
||||||
|
"nome": "Saque PIS/PASEP",
|
||||||
|
"descricao": "Saldo de cotas do PIS/PASEP para saque pelos herdeiros.",
|
||||||
|
"valor_mult": 0.8,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"condicao": None,
|
||||||
|
"tipo": "pensao_morte",
|
||||||
|
"nome": "Pensão por Morte (INSS)",
|
||||||
|
"descricao": "Benefício mensal pago pelo INSS aos dependentes do segurado falecido.",
|
||||||
|
"valor_mult": 12,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"condicao": "tinha_seguro_vida",
|
||||||
|
"tipo": "seguro_vida",
|
||||||
|
"nome": "Seguro de Vida",
|
||||||
|
"descricao": "Indenização do seguro de vida contratado pelo falecido.",
|
||||||
|
"valor_mult": 24,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"condicao": "tinha_carteira_assinada",
|
||||||
|
"tipo": "rescisao",
|
||||||
|
"nome": "Verbas Rescisórias",
|
||||||
|
"descricao": "Saldo de salário, férias proporcionais, 13º proporcional.",
|
||||||
|
"valor_mult": 2,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
async def escanear_beneficios(db: AsyncSession, familia_id: int, falecido: Falecido) -> list[Beneficio]:
|
||||||
|
salario = falecido.salario_estimado or 2500.0
|
||||||
|
beneficios = []
|
||||||
|
for b in BENEFICIOS_MAP:
|
||||||
|
cond = b.get("condicao")
|
||||||
|
if cond:
|
||||||
|
val = getattr(falecido, cond, 0)
|
||||||
|
if not val:
|
||||||
|
continue
|
||||||
|
ben = Beneficio(
|
||||||
|
familia_id=familia_id,
|
||||||
|
falecido_id=falecido.id,
|
||||||
|
tipo=b["tipo"],
|
||||||
|
nome=b["nome"],
|
||||||
|
descricao=b["descricao"],
|
||||||
|
status="identificado",
|
||||||
|
valor_estimado=round(salario * b["valor_mult"], 2),
|
||||||
|
)
|
||||||
|
db.add(ben)
|
||||||
|
beneficios.append(ben)
|
||||||
|
await db.commit()
|
||||||
|
return beneficios
|
||||||
43
backend/app/services/checklist_engine.py
Normal file
43
backend/app/services/checklist_engine.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from app.models.checklist import ChecklistItem
|
||||||
|
from app.models.falecido import Falecido
|
||||||
|
|
||||||
|
TEMPLATES_PATH = os.path.join(os.path.dirname(__file__), "..", "data", "checklist_templates.json")
|
||||||
|
|
||||||
|
def load_templates():
|
||||||
|
with open(TEMPLATES_PATH, "r", encoding="utf-8") as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
async def gerar_checklist(db: AsyncSession, familia_id: int, falecido: Falecido) -> list[ChecklistItem]:
|
||||||
|
templates = load_templates()
|
||||||
|
items = []
|
||||||
|
for t in templates:
|
||||||
|
cond = t.get("condicao")
|
||||||
|
if cond:
|
||||||
|
val = getattr(falecido, cond, 0)
|
||||||
|
if not val:
|
||||||
|
continue
|
||||||
|
item = ChecklistItem(
|
||||||
|
familia_id=familia_id,
|
||||||
|
falecido_id=falecido.id,
|
||||||
|
titulo=t["titulo"],
|
||||||
|
descricao=t.get("descricao"),
|
||||||
|
fase=t["fase"],
|
||||||
|
categoria=t["categoria"],
|
||||||
|
status="pendente",
|
||||||
|
prazo_dias=t.get("prazo_dias"),
|
||||||
|
ordem=t.get("ordem", 0),
|
||||||
|
)
|
||||||
|
db.add(item)
|
||||||
|
items.append(item)
|
||||||
|
await db.commit()
|
||||||
|
return items
|
||||||
|
|
||||||
|
def get_proximo_passo(items: list[dict]) -> dict | None:
|
||||||
|
pendentes = [i for i in items if i.get("status") == "pendente"]
|
||||||
|
if not pendentes:
|
||||||
|
return None
|
||||||
|
pendentes.sort(key=lambda x: x.get("ordem", 999))
|
||||||
|
return pendentes[0]
|
||||||
99
backend/app/services/document_generator.py
Normal file
99
backend/app/services/document_generator.py
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
from reportlab.lib.pagesizes import A4
|
||||||
|
from reportlab.lib.units import cm
|
||||||
|
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
|
||||||
|
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
||||||
|
from reportlab.lib.enums import TA_CENTER, TA_JUSTIFY
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
|
os.makedirs(settings.UPLOAD_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
def _styles():
|
||||||
|
ss = getSampleStyleSheet()
|
||||||
|
ss.add(ParagraphStyle(name="TitleCenter", parent=ss["Title"], alignment=TA_CENTER, fontSize=16))
|
||||||
|
ss.add(ParagraphStyle(name="Body", parent=ss["Normal"], alignment=TA_JUSTIFY, fontSize=11, leading=16))
|
||||||
|
return ss
|
||||||
|
|
||||||
|
def gerar_procuracao(familia_nome: str, falecido_nome: str, membro_nome: str, falecido_cpf: str = "000.000.000-00") -> str:
|
||||||
|
filename = f"procuracao_{familia_nome.lower().replace(' ','_')}_{datetime.now().strftime('%Y%m%d%H%M%S')}.pdf"
|
||||||
|
path = os.path.join(settings.UPLOAD_DIR, filename)
|
||||||
|
doc = SimpleDocTemplate(path, pagesize=A4, topMargin=3*cm)
|
||||||
|
ss = _styles()
|
||||||
|
story = [
|
||||||
|
Paragraph("PROCURAÇÃO", ss["TitleCenter"]),
|
||||||
|
Spacer(1, 1*cm),
|
||||||
|
Paragraph(
|
||||||
|
f"Pelo presente instrumento particular, eu, <b>{membro_nome}</b>, na qualidade de herdeiro(a) "
|
||||||
|
f"de <b>{falecido_nome}</b> (CPF: {falecido_cpf}), da família <b>{familia_nome}</b>, nomeio e constituo "
|
||||||
|
f"como meu(minha) bastante procurador(a) ______________________, para em meu nome representar "
|
||||||
|
f"perante órgãos públicos, bancos e demais instituições, podendo requerer, assinar documentos, "
|
||||||
|
f"receber valores e praticar todos os atos necessários ao cumprimento deste mandato.",
|
||||||
|
ss["Body"]
|
||||||
|
),
|
||||||
|
Spacer(1, 1.5*cm),
|
||||||
|
Paragraph(f"Local e data: ________________, {datetime.now().strftime('%d/%m/%Y')}", ss["Body"]),
|
||||||
|
Spacer(1, 2*cm),
|
||||||
|
Paragraph("_______________________________<br/>Assinatura do Outorgante", ss["Body"]),
|
||||||
|
]
|
||||||
|
doc.build(story)
|
||||||
|
return path
|
||||||
|
|
||||||
|
def gerar_requerimento_fgts(familia_nome: str, falecido_nome: str, membro_nome: str, falecido_cpf: str = "000.000.000-00") -> str:
|
||||||
|
filename = f"requerimento_fgts_{familia_nome.lower().replace(' ','_')}_{datetime.now().strftime('%Y%m%d%H%M%S')}.pdf"
|
||||||
|
path = os.path.join(settings.UPLOAD_DIR, filename)
|
||||||
|
doc = SimpleDocTemplate(path, pagesize=A4, topMargin=3*cm)
|
||||||
|
ss = _styles()
|
||||||
|
story = [
|
||||||
|
Paragraph("REQUERIMENTO DE SAQUE DO FGTS POR FALECIMENTO", ss["TitleCenter"]),
|
||||||
|
Spacer(1, 1*cm),
|
||||||
|
Paragraph("À Caixa Econômica Federal", ss["Body"]),
|
||||||
|
Spacer(1, 0.5*cm),
|
||||||
|
Paragraph(
|
||||||
|
f"Eu, <b>{membro_nome}</b>, na qualidade de dependente/herdeiro(a) de <b>{falecido_nome}</b> "
|
||||||
|
f"(CPF: {falecido_cpf}), venho por meio deste requerer o saque do saldo da(s) conta(s) vinculada(s) "
|
||||||
|
f"ao FGTS do trabalhador falecido, conforme previsto na Lei nº 8.036/90, art. 20, inciso IV.",
|
||||||
|
ss["Body"]
|
||||||
|
),
|
||||||
|
Spacer(1, 0.5*cm),
|
||||||
|
Paragraph("Documentos em anexo: Certidão de Óbito, documento de identidade, comprovante de dependência.", ss["Body"]),
|
||||||
|
Spacer(1, 1.5*cm),
|
||||||
|
Paragraph(f"Local e data: ________________, {datetime.now().strftime('%d/%m/%Y')}", ss["Body"]),
|
||||||
|
Spacer(1, 2*cm),
|
||||||
|
Paragraph("_______________________________<br/>Assinatura do Requerente", ss["Body"]),
|
||||||
|
]
|
||||||
|
doc.build(story)
|
||||||
|
return path
|
||||||
|
|
||||||
|
def gerar_peticao_pensao(familia_nome: str, falecido_nome: str, membro_nome: str, falecido_cpf: str = "000.000.000-00") -> str:
|
||||||
|
filename = f"peticao_pensao_{familia_nome.lower().replace(' ','_')}_{datetime.now().strftime('%Y%m%d%H%M%S')}.pdf"
|
||||||
|
path = os.path.join(settings.UPLOAD_DIR, filename)
|
||||||
|
doc = SimpleDocTemplate(path, pagesize=A4, topMargin=3*cm)
|
||||||
|
ss = _styles()
|
||||||
|
story = [
|
||||||
|
Paragraph("REQUERIMENTO DE PENSÃO POR MORTE", ss["TitleCenter"]),
|
||||||
|
Spacer(1, 1*cm),
|
||||||
|
Paragraph("Ao Instituto Nacional do Seguro Social - INSS", ss["Body"]),
|
||||||
|
Spacer(1, 0.5*cm),
|
||||||
|
Paragraph(
|
||||||
|
f"Eu, <b>{membro_nome}</b>, na qualidade de dependente de <b>{falecido_nome}</b> "
|
||||||
|
f"(CPF: {falecido_cpf}), venho requerer a concessão do benefício de Pensão por Morte, "
|
||||||
|
f"conforme previsto nos arts. 74 a 79 da Lei nº 8.213/91, em razão do falecimento do(a) "
|
||||||
|
f"segurado(a) acima identificado(a).",
|
||||||
|
ss["Body"]
|
||||||
|
),
|
||||||
|
Spacer(1, 0.5*cm),
|
||||||
|
Paragraph("Documentos em anexo: Certidão de Óbito, documentos pessoais, comprovante de dependência econômica.", ss["Body"]),
|
||||||
|
Spacer(1, 1.5*cm),
|
||||||
|
Paragraph(f"Local e data: ________________, {datetime.now().strftime('%d/%m/%Y')}", ss["Body"]),
|
||||||
|
Spacer(1, 2*cm),
|
||||||
|
Paragraph("_______________________________<br/>Assinatura do Requerente", ss["Body"]),
|
||||||
|
]
|
||||||
|
doc.build(story)
|
||||||
|
return path
|
||||||
|
|
||||||
|
GENERATORS = {
|
||||||
|
"procuracao": gerar_procuracao,
|
||||||
|
"requerimento_fgts": gerar_requerimento_fgts,
|
||||||
|
"peticao_pensao": gerar_peticao_pensao,
|
||||||
|
}
|
||||||
11
backend/requirements.txt
Normal file
11
backend/requirements.txt
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
fastapi==0.109.0
|
||||||
|
uvicorn[standard]==0.27.0
|
||||||
|
sqlalchemy[asyncio]==2.0.25
|
||||||
|
aiosqlite==0.19.0
|
||||||
|
python-jose[cryptography]==3.3.0
|
||||||
|
passlib[bcrypt]==1.7.4
|
||||||
|
python-multipart==0.0.6
|
||||||
|
reportlab==4.1.0
|
||||||
|
pydantic-settings==2.1.0
|
||||||
|
bcrypt==4.0.1
|
||||||
|
asyncpg==0.30.0
|
||||||
98
backend/seed_data.py
Normal file
98
backend/seed_data.py
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
"""Seed script - popula o banco com dados mock."""
|
||||||
|
import asyncio
|
||||||
|
from datetime import date
|
||||||
|
from app.core.database import engine, async_session, Base
|
||||||
|
from app.core.security import hash_password
|
||||||
|
from app.models import *
|
||||||
|
from app.services.checklist_engine import gerar_checklist
|
||||||
|
from app.services.beneficio_scanner import escanear_beneficios
|
||||||
|
|
||||||
|
async def seed():
|
||||||
|
async with engine.begin() as conn:
|
||||||
|
await conn.run_sync(Base.metadata.drop_all)
|
||||||
|
await conn.run_sync(Base.metadata.create_all)
|
||||||
|
|
||||||
|
async with async_session() as db:
|
||||||
|
# Usuário demo
|
||||||
|
user = Usuario(nome="Maria da Silva", email="demo@caronte.com.br", senha_hash=hash_password("123456"), telefone="(11) 99999-0001")
|
||||||
|
db.add(user)
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(user)
|
||||||
|
|
||||||
|
# Famílias
|
||||||
|
familias_data = [
|
||||||
|
{"nome": "Família Silva", "membros": [
|
||||||
|
{"nome": "Maria da Silva", "parentesco": "Esposa", "cpf": "123.456.789-00", "telefone": "(11) 99999-0001"},
|
||||||
|
{"nome": "João da Silva Jr.", "parentesco": "Filho", "cpf": "123.456.789-01"},
|
||||||
|
]},
|
||||||
|
{"nome": "Família Oliveira", "membros": [
|
||||||
|
{"nome": "Ana Oliveira", "parentesco": "Filha", "cpf": "234.567.890-00", "telefone": "(21) 98888-0002"},
|
||||||
|
{"nome": "Carlos Oliveira", "parentesco": "Filho", "cpf": "234.567.890-01"},
|
||||||
|
]},
|
||||||
|
{"nome": "Família Santos", "membros": [
|
||||||
|
{"nome": "Lucia Santos", "parentesco": "Esposa", "cpf": "345.678.901-00", "telefone": "(31) 97777-0003"},
|
||||||
|
]},
|
||||||
|
]
|
||||||
|
|
||||||
|
falecidos_data = [
|
||||||
|
{"familia_idx": 0, "nome": "José da Silva", "cpf": "111.222.333-44", "data_nascimento": date(1955, 3, 15),
|
||||||
|
"data_obito": date(2026, 1, 10), "causa_obito": "Infarto agudo do miocárdio", "tipo_vinculo": "empregado",
|
||||||
|
"empregador": "Construtora Progresso Ltda", "tinha_carteira_assinada": 1, "tinha_fgts": 1,
|
||||||
|
"era_aposentado": 0, "tinha_seguro_vida": 1, "tinha_imoveis": 1, "tinha_veiculos": 1, "salario_estimado": 4500.0},
|
||||||
|
|
||||||
|
{"familia_idx": 0, "nome": "Antônia da Silva", "cpf": "111.222.333-55", "data_nascimento": date(1930, 8, 20),
|
||||||
|
"data_obito": date(2025, 11, 5), "causa_obito": "Causas naturais", "tipo_vinculo": "aposentado",
|
||||||
|
"era_aposentado": 1, "salario_estimado": 2800.0},
|
||||||
|
|
||||||
|
{"familia_idx": 1, "nome": "Roberto Oliveira", "cpf": "222.333.444-55", "data_nascimento": date(1960, 12, 1),
|
||||||
|
"data_obito": date(2026, 1, 25), "causa_obito": "Câncer de pulmão", "tipo_vinculo": "aposentado",
|
||||||
|
"era_aposentado": 1, "tinha_seguro_vida": 1, "tinha_imoveis": 1, "salario_estimado": 5200.0},
|
||||||
|
|
||||||
|
{"familia_idx": 1, "nome": "Pedro Oliveira", "cpf": "222.333.444-66", "data_nascimento": date(1985, 5, 10),
|
||||||
|
"data_obito": date(2026, 2, 1), "causa_obito": "Acidente de trânsito", "tipo_vinculo": "empregado",
|
||||||
|
"empregador": "Tech Solutions SA", "tinha_carteira_assinada": 1, "tinha_fgts": 1,
|
||||||
|
"tinha_seguro_vida": 0, "tinha_veiculos": 1, "salario_estimado": 8500.0},
|
||||||
|
|
||||||
|
{"familia_idx": 2, "nome": "Francisco Santos", "cpf": "333.444.555-66", "data_nascimento": date(1970, 7, 22),
|
||||||
|
"data_obito": date(2026, 1, 15), "causa_obito": "AVC", "tipo_vinculo": "autonomo",
|
||||||
|
"tinha_carteira_assinada": 0, "tinha_fgts": 0, "tinha_imoveis": 1, "salario_estimado": 3500.0},
|
||||||
|
]
|
||||||
|
|
||||||
|
familias = []
|
||||||
|
for fd in familias_data:
|
||||||
|
f = Familia(nome=fd["nome"], usuario_id=user.id)
|
||||||
|
db.add(f)
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(f)
|
||||||
|
familias.append(f)
|
||||||
|
for m in fd["membros"]:
|
||||||
|
db.add(MembroFamilia(familia_id=f.id, **m))
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
for fd in falecidos_data:
|
||||||
|
fam = familias[fd["familia_idx"]]
|
||||||
|
data = {k: v for k, v in fd.items() if k != "familia_idx"}
|
||||||
|
fal = Falecido(familia_id=fam.id, **data)
|
||||||
|
db.add(fal)
|
||||||
|
await db.commit()
|
||||||
|
await db.refresh(fal)
|
||||||
|
await gerar_checklist(db, fam.id, fal)
|
||||||
|
await escanear_beneficios(db, fam.id, fal)
|
||||||
|
|
||||||
|
# Documentos mock
|
||||||
|
doc_types = [
|
||||||
|
(familias[0].id, 1, "procuracao", "Procuração - José da Silva"),
|
||||||
|
(familias[0].id, 1, "requerimento_fgts", "Requerimento FGTS - José da Silva"),
|
||||||
|
(familias[1].id, 3, "peticao_pensao", "Petição Pensão por Morte - Roberto Oliveira"),
|
||||||
|
]
|
||||||
|
for fam_id, fal_id, tipo, nome in doc_types:
|
||||||
|
db.add(Documento(familia_id=fam_id, falecido_id=fal_id, tipo=tipo, nome=nome, status="gerado"))
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
print("✅ Seed concluído! Dados mock inseridos.")
|
||||||
|
print(" - 1 usuário: demo@caronte.com.br / 123456")
|
||||||
|
print(" - 3 famílias, 5 falecidos")
|
||||||
|
print(" - Checklists e benefícios gerados automaticamente")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(seed())
|
||||||
456
docs/ARQUITETURA-TECNICA.md
Normal file
456
docs/ARQUITETURA-TECNICA.md
Normal file
@@ -0,0 +1,456 @@
|
|||||||
|
# CARONTE — Arquitetura Técnica
|
||||||
|
|
||||||
|
> *O barqueiro que guia famílias enlutadas pelo rio burocrático brasileiro.*
|
||||||
|
> Versão 1.0 — Fevereiro 2026
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Visão Geral
|
||||||
|
|
||||||
|
O CARONTE é uma plataforma que automatiza e orienta famílias no processo pós-óbito no Brasil — desde a certidão de óbito até a partilha final de bens. Elimina a desorientação burocrática transformando um processo caótico em um fluxo guiado, passo a passo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Stack Tecnológico
|
||||||
|
|
||||||
|
| Camada | Tecnologia | Justificativa |
|
||||||
|
|---|---|---|
|
||||||
|
| **Backend / API** | FastAPI (Python 3.12+) | Async nativo, tipagem forte, docs automáticas (OpenAPI) |
|
||||||
|
| **Frontend** | Next.js 14 (App Router) | SSR/SSG, React Server Components, SEO |
|
||||||
|
| **Banco de dados** | PostgreSQL 16 | JSONB para docs flexíveis, full-text search, confiabilidade |
|
||||||
|
| **ORM** | SQLAlchemy 2.0 + Alembic | Migrations versionadas, async support |
|
||||||
|
| **Cache / Filas** | Redis 7 | Cache de sessão, rate limiting, filas com BullMQ |
|
||||||
|
| **Task Queue** | Celery + Redis broker | Jobs assíncronos (scraping gov, geração de PDFs) |
|
||||||
|
| **Storage** | MinIO (S3-compatible) | Documentos, certidões, PDFs gerados |
|
||||||
|
| **Auth** | JWT + OAuth2 (Gov.br) | Login cidadão via Gov.br, fallback email/senha |
|
||||||
|
| **PDF Engine** | WeasyPrint + Jinja2 | Geração de petições e requerimentos |
|
||||||
|
| **Infra** | Docker Compose → K8s | Dev local com Compose, prod em Kubernetes |
|
||||||
|
| **CI/CD** | GitHub Actions | Testes, lint, build, deploy |
|
||||||
|
| **Monitoramento** | Sentry + Prometheus + Grafana | Erros, métricas, alertas |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Diagrama de Módulos
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ FRONTEND (Next.js 14) │
|
||||||
|
│ ┌──────────┐ ┌──────────┐ ┌───────────┐ ┌──────────┐ ┌─────────┐ │
|
||||||
|
│ │Dashboard │ │Checklist │ │ Benefícios│ │Documentos│ │ Perfil │ │
|
||||||
|
│ │ Familiar │ │ Guiado │ │ Scanner │ │ Gerados │ │ Família │ │
|
||||||
|
│ └────┬─────┘ └────┬─────┘ └─────┬─────┘ └────┬─────┘ └────┬────┘ │
|
||||||
|
└───────┼────────────┼─────────────┼─────────────┼────────────┼──────┘
|
||||||
|
│ │ │ │ │
|
||||||
|
════════╪════════════╪═════════════╪═════════════╪════════════╪══════
|
||||||
|
│ API Gateway (FastAPI) │ │
|
||||||
|
════════╪════════════╪═════════════╪═════════════╪════════════╪══════
|
||||||
|
▼ ▼ ▼ ▼ ▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ BACKEND (FastAPI) │
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────────┐ │
|
||||||
|
│ │ MOTOR DE │ │ SCANNER DE │ │ GERADOR DE │ │
|
||||||
|
│ │ CHECKLIST │ │ BENEFÍCIOS │ │ DOCUMENTOS │ │
|
||||||
|
│ │ │ │ │ │ │ │
|
||||||
|
│ │ • Fluxo por UF │ │ • FGTS │ │ • Procurações │ │
|
||||||
|
│ │ • Prazos legais │ │ • PIS/PASEP │ │ • Requerimentos │ │
|
||||||
|
│ │ • Dependências │ │ • Seguros vida │ │ • Petições │ │
|
||||||
|
│ │ • Notificações │ │ • Previdência │ │ • Alvará judicial │ │
|
||||||
|
│ │ • Estado por item│ │ • Pensão morte │ │ • Templates Jinja2 │ │
|
||||||
|
│ └────────┬─────────┘ └────────┬────────┘ └──────────┬──────────┘ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ ┌────────┴─────────────────────┴──────────────────────┴──────────┐ │
|
||||||
|
│ │ CAMADA DE INTEGRAÇÃO │ │
|
||||||
|
│ │ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌──────────────┐ │ │
|
||||||
|
│ │ │Gov.br │ │INSS │ │Receita │ │DETRAN │ │ Bancos │ │ │
|
||||||
|
│ │ │(Auth) │ │(Meu │ │Federal │ │(veíc.) │ │ (Open Banking│ │ │
|
||||||
|
│ │ │ │ │ INSS) │ │(e-CAC) │ │ │ │ / consulta) │ │ │
|
||||||
|
│ │ └────────┘ └────────┘ └────────┘ └────────┘ └──────────────┘ │ │
|
||||||
|
│ └────────────────────────────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────┐ │
|
||||||
|
│ │ Auth Module │ │ Notificações │ │ Audit / Compliance │ │
|
||||||
|
│ │ JWT + Gov.br │ │ Email + Push │ │ LGPD + Logs imutáveis │ │
|
||||||
|
│ └──────────────┘ └──────────────┘ └──────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────┘
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌──────────────┐ ┌──────────────┐ ┌──────────────────────────┐
|
||||||
|
│ PostgreSQL │ │ Redis │ │ MinIO (S3) │
|
||||||
|
│ (dados) │ │ (cache/fila) │ │ (docs/certidões/PDFs) │
|
||||||
|
└──────────────┘ └──────────────┘ └──────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Módulos Detalhados
|
||||||
|
|
||||||
|
### 4.1 Motor de Checklist Burocrático
|
||||||
|
|
||||||
|
O coração do sistema. Modela o processo pós-óbito como uma **máquina de estados** com dependências entre etapas.
|
||||||
|
|
||||||
|
**Fluxo principal:**
|
||||||
|
```
|
||||||
|
Óbito registrado
|
||||||
|
└─→ Certidão de Óbito
|
||||||
|
└─→ Comunicação ao banco / empregador / INSS
|
||||||
|
├─→ Pensão por Morte (INSS)
|
||||||
|
├─→ Saque FGTS / PIS-PASEP
|
||||||
|
├─→ Seguro de Vida (se houver)
|
||||||
|
├─→ Inventário (judicial ou extrajudicial)
|
||||||
|
│ ├─→ Avaliação de bens
|
||||||
|
│ ├─→ Cálculo ITCMD
|
||||||
|
│ └─→ Partilha
|
||||||
|
└─→ Transferência de veículos / imóveis
|
||||||
|
```
|
||||||
|
|
||||||
|
**Características:**
|
||||||
|
- Checklist dinâmico por UF (regras estaduais de ITCMD, cartórios)
|
||||||
|
- Prazos legais com alertas (ex: inventário em 60 dias para isenção de multa)
|
||||||
|
- Itens condicionais (só mostra "transferir veículo" se falecido tinha veículo)
|
||||||
|
- Status por item: `pendente` → `em_andamento` → `concluido` → `nao_aplicavel`
|
||||||
|
|
||||||
|
### 4.2 Scanner de Benefícios
|
||||||
|
|
||||||
|
Identifica automaticamente todos os direitos financeiros dos herdeiros.
|
||||||
|
|
||||||
|
| Benefício | Fonte | Prazo |
|
||||||
|
|---|---|---|
|
||||||
|
| FGTS | Caixa Econômica | Sem prazo (mas quanto antes melhor) |
|
||||||
|
| PIS/PASEP | Caixa / Banco do Brasil | 5 anos |
|
||||||
|
| Pensão por Morte | INSS | 90 dias (retroativa) / 180 dias (integral) |
|
||||||
|
| Seguro de Vida | Seguradoras (SUSEP) | 3 anos |
|
||||||
|
| Restituição IR | Receita Federal | 5 anos |
|
||||||
|
| Seguro DPVAT | Seguradora Líder | 3 anos |
|
||||||
|
| Previdência Privada | Bancos / Seguradoras | Varia |
|
||||||
|
|
||||||
|
### 4.3 Gerador de Documentos
|
||||||
|
|
||||||
|
Motor de templates usando **Jinja2 + WeasyPrint** para gerar PDFs prontos para uso.
|
||||||
|
|
||||||
|
**Documentos gerados:**
|
||||||
|
- Procuração para herdeiro(a) representante
|
||||||
|
- Requerimento de saque FGTS por falecimento
|
||||||
|
- Requerimento de pensão por morte (INSS)
|
||||||
|
- Petição de alvará judicial (para valores < limite extrajudicial)
|
||||||
|
- Declaração de herdeiros únicos
|
||||||
|
- Comunicação de óbito a instituições financeiras
|
||||||
|
|
||||||
|
### 4.4 Dashboard Familiar
|
||||||
|
|
||||||
|
Painel compartilhado entre membros da família com:
|
||||||
|
- **Progresso geral** (% do processo concluído)
|
||||||
|
- **Próximos passos** com prioridade por urgência/prazo
|
||||||
|
- **Documentos** organizados por categoria
|
||||||
|
- **Linha do tempo** de tudo que já foi feito
|
||||||
|
- **Convites** para outros membros da família
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Modelo de Dados
|
||||||
|
|
||||||
|
### 5.1 Diagrama Entidade-Relacionamento
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────┐ ┌──────────────────┐ ┌──────────────────┐
|
||||||
|
│ usuario │ │ familia │ │ falecido │
|
||||||
|
├──────────────┤ ├──────────────────┤ ├──────────────────┤
|
||||||
|
│ id (PK) │──┐ │ id (PK) │ ┌──│ id (PK) │
|
||||||
|
│ nome │ │ │ nome │ │ │ familia_id (FK) │
|
||||||
|
│ email │ │ │ created_at │ │ │ nome_completo │
|
||||||
|
│ cpf_hash │ │ │ status │ │ │ cpf_cifrado │
|
||||||
|
│ govbr_id │ │ │ uf │ │ │ data_nascimento │
|
||||||
|
│ created_at │ │ └──────────────────┘ │ │ data_obito │
|
||||||
|
└──────────────┘ │ │ │ │ certidao_obito │
|
||||||
|
│ │ 1:N │ │ tinha_emprego │
|
||||||
|
│ ▼ │ │ tinha_veiculo │
|
||||||
|
┌───┴──────────────────┐ │ │ tinha_imovel │
|
||||||
|
│ membro_familia │ │ │ tinha_previdencia│
|
||||||
|
├──────────────────────┤ │ └──────────────────┘
|
||||||
|
│ id (PK) │ │ │
|
||||||
|
│ usuario_id (FK) │ │ │ 1:N
|
||||||
|
│ familia_id (FK) │ │ ▼
|
||||||
|
│ papel (enum) │ │ ┌──────────────────┐
|
||||||
|
│ parentesco │ │ │ checklist_item │
|
||||||
|
│ is_responsavel │ │ ├──────────────────┤
|
||||||
|
└──────────────────────┘ │ │ id (PK) │
|
||||||
|
│ │ falecido_id (FK) │
|
||||||
|
┌──────────────────┐ ┌──────────────────┐ │ │ template_id (FK) │
|
||||||
|
│ beneficio │ │ documento │ │ │ status (enum) │
|
||||||
|
├──────────────────┤ ├──────────────────┤ │ │ responsavel_id │
|
||||||
|
│ id (PK) │ │ id (PK) │ │ │ prazo_legal │
|
||||||
|
│ falecido_id (FK) │────│ falecido_id (FK) │───┘ │ concluido_em │
|
||||||
|
│ tipo (enum) │ │ tipo (enum) │ │ notas │
|
||||||
|
│ instituicao │ │ nome │ │ ordem │
|
||||||
|
│ valor_estimado │ │ storage_path │ │ depende_de (FK) │
|
||||||
|
│ status │ │ gerado_por_sistema│ └──────────────────┘
|
||||||
|
│ prazo_limite │ │ uploaded_at │
|
||||||
|
│ valor_sacado │ │ hash_sha256 │ ┌──────────────────┐
|
||||||
|
└──────────────────┘ └──────────────────┘ │ checklist_tpl │
|
||||||
|
├──────────────────┤
|
||||||
|
┌──────────────────┐ ┌──────────────────┐ │ id (PK) │
|
||||||
|
│ notificacao │ │ audit_log │ │ titulo │
|
||||||
|
├──────────────────┤ ├──────────────────┤ │ descricao │
|
||||||
|
│ id (PK) │ │ id (PK) │ │ categoria │
|
||||||
|
│ usuario_id (FK) │ │ usuario_id (FK) │ │ uf (nullable) │
|
||||||
|
│ tipo │ │ acao │ │ condicao (JSONB) │
|
||||||
|
│ titulo │ │ entidade │ │ prazo_dias │
|
||||||
|
│ corpo │ │ entidade_id │ │ documentos_req │
|
||||||
|
│ lida │ │ dados_antes │ │ links_uteis │
|
||||||
|
│ created_at │ │ dados_depois │ └──────────────────┘
|
||||||
|
└──────────────────┘ │ ip_address │
|
||||||
|
│ created_at │
|
||||||
|
└──────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 Enums Principais
|
||||||
|
|
||||||
|
```python
|
||||||
|
class StatusChecklist(str, Enum):
|
||||||
|
PENDENTE = "pendente"
|
||||||
|
EM_ANDAMENTO = "em_andamento"
|
||||||
|
AGUARDANDO_DOCUMENTO = "aguardando_documento"
|
||||||
|
CONCLUIDO = "concluido"
|
||||||
|
NAO_APLICAVEL = "nao_aplicavel"
|
||||||
|
BLOQUEADO = "bloqueado" # dependência não atendida
|
||||||
|
|
||||||
|
class TipoBeneficio(str, Enum):
|
||||||
|
FGTS = "fgts"
|
||||||
|
PIS_PASEP = "pis_pasep"
|
||||||
|
PENSAO_MORTE = "pensao_morte"
|
||||||
|
SEGURO_VIDA = "seguro_vida"
|
||||||
|
RESTITUICAO_IR = "restituicao_ir"
|
||||||
|
DPVAT = "dpvat"
|
||||||
|
PREVIDENCIA_PRIVADA = "previdencia_privada"
|
||||||
|
|
||||||
|
class PapelFamilia(str, Enum):
|
||||||
|
INVENTARIANTE = "inventariante"
|
||||||
|
HERDEIRO = "herdeiro"
|
||||||
|
CONJUGE = "conjuge"
|
||||||
|
ADVOGADO = "advogado"
|
||||||
|
CONTADOR = "contador"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. APIs e Integrações Externas
|
||||||
|
|
||||||
|
### 6.1 APIs Governamentais
|
||||||
|
|
||||||
|
| Serviço | API / Método | Finalidade |
|
||||||
|
|---|---|---|
|
||||||
|
| **Gov.br** | OAuth2 (login único) | Autenticação cidadã (selo prata/ouro) |
|
||||||
|
| **INSS (Meu INSS)** | API + scraping assistido | Consulta vínculos, requerimento pensão por morte |
|
||||||
|
| **Receita Federal (e-CAC)** | API / certificado digital | Consulta situação fiscal, restituição IR |
|
||||||
|
| **DETRAN** | API estadual (varia por UF) | Consulta veículos em nome do falecido |
|
||||||
|
| **Caixa Econômica** | API parceiro / orientação | FGTS, PIS |
|
||||||
|
| **Banco do Brasil** | API parceiro / orientação | PASEP |
|
||||||
|
| **SUSEP** | Consulta pública | Verificar apólices de seguro ativas |
|
||||||
|
| **CRC Nacional** | API cartórios | Certidões (nascimento, casamento, óbito) |
|
||||||
|
|
||||||
|
### 6.2 APIs de Terceiros
|
||||||
|
|
||||||
|
| Serviço | Finalidade |
|
||||||
|
|---|---|
|
||||||
|
| **SendGrid / AWS SES** | Envio de emails transacionais |
|
||||||
|
| **Firebase Cloud Messaging** | Push notifications (mobile/web) |
|
||||||
|
| **ViaCEP** | Autocompletar endereços |
|
||||||
|
| **Open Banking (fase 4)** | Consulta de contas/saldos do falecido |
|
||||||
|
|
||||||
|
### 6.3 Estratégia de Integração
|
||||||
|
|
||||||
|
Nem todas as APIs gov possuem endpoints REST abertos. A estratégia é:
|
||||||
|
|
||||||
|
1. **API oficial** → usar diretamente (Gov.br, CRC)
|
||||||
|
2. **Portal com login** → guiar o usuário com tutorial passo-a-passo integrado
|
||||||
|
3. **Sem API** → gerar documento preenchido para protocolo presencial
|
||||||
|
4. **Parcerias futuras** → negociar acesso via convênios (Caixa, BB, INSS)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Segurança e LGPD
|
||||||
|
|
||||||
|
### 7.1 Dados Sensíveis
|
||||||
|
|
||||||
|
O CARONTE lida com dados de **pessoas falecidas** e seus **herdeiros vivos**. A LGPD (Lei 13.709/2018) se aplica integralmente aos vivos e, por extensão prática, aos dados do falecido que identificam herdeiros.
|
||||||
|
|
||||||
|
### 7.2 Medidas Técnicas
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────┐
|
||||||
|
│ CAMADA DE SEGURANÇA │
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────┐ ┌─────────────────────────┐ │
|
||||||
|
│ │ Criptografia │ │ Controle de Acesso │ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ • CPF: AES-256 │ │ • RBAC por família │ │
|
||||||
|
│ │ • Docs: encrypt │ │ • Convite com expiração │ │
|
||||||
|
│ │ at rest (S3) │ │ • MFA obrigatório │ │
|
||||||
|
│ │ • TLS 1.3 em │ │ • Sessão com TTL curto │ │
|
||||||
|
│ │ trânsito │ │ • IP allowlist (admin) │ │
|
||||||
|
│ └─────────────────┘ └─────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────┐ ┌─────────────────────────┐ │
|
||||||
|
│ │ Auditoria │ │ LGPD Compliance │ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ • Log imutável │ │ • Consentimento explícito│ │
|
||||||
|
│ │ (append-only) │ │ • Finalidade declarada │ │
|
||||||
|
│ │ • Quem acessou │ │ • Direito ao esquecimento│ │
|
||||||
|
│ │ o quê e quando │ │ • Portabilidade (export) │ │
|
||||||
|
│ │ • Retenção 5 anos│ │ • DPO designado │ │
|
||||||
|
│ └─────────────────┘ └─────────────────────────┘ │
|
||||||
|
└──────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.3 Políticas
|
||||||
|
|
||||||
|
| Aspecto | Implementação |
|
||||||
|
|---|---|
|
||||||
|
| **CPF do falecido** | Nunca armazenado em texto plano; cifrado com AES-256-GCM, chave em vault |
|
||||||
|
| **Documentos** | Criptografados em repouso no MinIO, acesso via URL assinada com TTL |
|
||||||
|
| **Consentimento** | Tela de aceite no onboarding; registro em tabela `consentimentos` |
|
||||||
|
| **Exclusão de dados** | Endpoint `/api/v1/familia/{id}/apagar` — soft delete + hard delete em 30 dias |
|
||||||
|
| **Logs de auditoria** | Tabela `audit_log` append-only, sem UPDATE/DELETE permitido |
|
||||||
|
| **Backup** | PostgreSQL: WAL archiving + pg_basebackup diário, criptografado |
|
||||||
|
| **Acesso interno** | Zero trust; funcionários acessam via VPN + MFA + role mínimo |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Estrutura do Projeto
|
||||||
|
|
||||||
|
```
|
||||||
|
caronte/
|
||||||
|
├── backend/
|
||||||
|
│ ├── app/
|
||||||
|
│ │ ├── main.py # FastAPI app
|
||||||
|
│ │ ├── core/
|
||||||
|
│ │ │ ├── config.py # Settings (pydantic-settings)
|
||||||
|
│ │ │ ├── security.py # JWT, hashing, criptografia
|
||||||
|
│ │ │ └── database.py # Engine, session
|
||||||
|
│ │ ├── models/ # SQLAlchemy models
|
||||||
|
│ │ │ ├── usuario.py
|
||||||
|
│ │ │ ├── familia.py
|
||||||
|
│ │ │ ├── falecido.py
|
||||||
|
│ │ │ ├── checklist.py
|
||||||
|
│ │ │ ├── beneficio.py
|
||||||
|
│ │ │ └── documento.py
|
||||||
|
│ │ ├── schemas/ # Pydantic schemas
|
||||||
|
│ │ ├── api/
|
||||||
|
│ │ │ └── v1/
|
||||||
|
│ │ │ ├── auth.py
|
||||||
|
│ │ │ ├── familias.py
|
||||||
|
│ │ │ ├── checklist.py
|
||||||
|
│ │ │ ├── beneficios.py
|
||||||
|
│ │ │ ├── documentos.py
|
||||||
|
│ │ │ └── dashboard.py
|
||||||
|
│ │ ├── services/ # Lógica de negócio
|
||||||
|
│ │ │ ├── checklist_engine.py
|
||||||
|
│ │ │ ├── beneficio_scanner.py
|
||||||
|
│ │ │ ├── document_generator.py
|
||||||
|
│ │ │ └── integrations/
|
||||||
|
│ │ │ ├── govbr.py
|
||||||
|
│ │ │ ├── inss.py
|
||||||
|
│ │ │ ├── receita.py
|
||||||
|
│ │ │ └── caixa.py
|
||||||
|
│ │ ├── tasks/ # Celery tasks
|
||||||
|
│ │ │ ├── notificacoes.py
|
||||||
|
│ │ │ └── scrapers.py
|
||||||
|
│ │ └── templates/ # Jinja2 (docs jurídicos)
|
||||||
|
│ │ ├── procuracao.html
|
||||||
|
│ │ ├── requerimento_fgts.html
|
||||||
|
│ │ └── peticao_alvara.html
|
||||||
|
│ ├── alembic/ # Migrations
|
||||||
|
│ ├── tests/
|
||||||
|
│ ├── Dockerfile
|
||||||
|
│ └── requirements.txt
|
||||||
|
├── frontend/
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── app/ # Next.js App Router
|
||||||
|
│ │ │ ├── (auth)/
|
||||||
|
│ │ │ │ ├── login/
|
||||||
|
│ │ │ │ └── registro/
|
||||||
|
│ │ │ ├── (dashboard)/
|
||||||
|
│ │ │ │ ├── familia/[id]/
|
||||||
|
│ │ │ │ ├── checklist/
|
||||||
|
│ │ │ │ ├── beneficios/
|
||||||
|
│ │ │ │ └── documentos/
|
||||||
|
│ │ │ └── layout.tsx
|
||||||
|
│ │ ├── components/
|
||||||
|
│ │ ├── lib/
|
||||||
|
│ │ │ ├── api.ts # Client API
|
||||||
|
│ │ │ └── auth.ts
|
||||||
|
│ │ └── types/
|
||||||
|
│ ├── Dockerfile
|
||||||
|
│ └── package.json
|
||||||
|
├── infra/
|
||||||
|
│ ├── docker-compose.yml
|
||||||
|
│ ├── docker-compose.prod.yml
|
||||||
|
│ ├── nginx.conf
|
||||||
|
│ └── k8s/
|
||||||
|
├── docs/
|
||||||
|
│ ├── ARQUITETURA-TECNICA.md # ← este arquivo
|
||||||
|
│ ├── MODELO-NEGOCIO.md
|
||||||
|
│ └── API.md
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Endpoints Principais (API v1)
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/v1/auth/login # Login Gov.br ou email
|
||||||
|
POST /api/v1/auth/registro # Criar conta
|
||||||
|
GET /api/v1/auth/me # Perfil do usuário
|
||||||
|
|
||||||
|
POST /api/v1/familias # Criar núcleo familiar
|
||||||
|
GET /api/v1/familias/{id} # Dados da família
|
||||||
|
POST /api/v1/familias/{id}/membros # Convidar membro
|
||||||
|
POST /api/v1/familias/{id}/falecido # Registrar falecido
|
||||||
|
|
||||||
|
GET /api/v1/checklist/{falecido_id} # Listar itens
|
||||||
|
PATCH /api/v1/checklist/item/{id} # Atualizar status
|
||||||
|
GET /api/v1/checklist/{falecido_id}/proximo # Próximo passo sugerido
|
||||||
|
|
||||||
|
GET /api/v1/beneficios/{falecido_id} # Benefícios identificados
|
||||||
|
POST /api/v1/beneficios/{falecido_id}/scan # Rodar scanner
|
||||||
|
|
||||||
|
POST /api/v1/documentos/gerar # Gerar documento (PDF)
|
||||||
|
GET /api/v1/documentos/{id}/download # Baixar documento
|
||||||
|
POST /api/v1/documentos/upload # Upload de certidão/doc
|
||||||
|
|
||||||
|
GET /api/v1/dashboard/{familia_id} # Resumo do dashboard
|
||||||
|
GET /api/v1/dashboard/{familia_id}/timeline # Linha do tempo
|
||||||
|
|
||||||
|
DELETE /api/v1/familia/{id}/apagar # LGPD: exclusão de dados
|
||||||
|
GET /api/v1/familia/{id}/exportar # LGPD: portabilidade
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Roadmap de Implementação
|
||||||
|
|
||||||
|
| Fase | Escopo | Prazo Estimado |
|
||||||
|
|---|---|---|
|
||||||
|
| **MVP (v0.1)** | Auth, checklist estático, dashboard básico | 6-8 semanas |
|
||||||
|
| **v0.2** | Scanner de benefícios, gerador de docs (3 templates) | +4 semanas |
|
||||||
|
| **v0.3** | Integração Gov.br, notificações por email | +4 semanas |
|
||||||
|
| **v1.0** | Checklist dinâmico por UF, mobile PWA, onboarding guiado | +6 semanas |
|
||||||
|
| **v1.5** | Integração parceiros (bancos, cartórios), IA para FAQ | +8 semanas |
|
||||||
|
| **v2.0** | Open Banking, marketplace de advogados/contadores | Futuro |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Considerações Finais
|
||||||
|
|
||||||
|
O CARONTE não substitui advogados — ele **elimina a ignorância burocrática** que faz famílias perderem dinheiro e prazos. O sistema é o guia; o profissional jurídico é o executor quando necessário.
|
||||||
|
|
||||||
|
A arquitetura prioriza:
|
||||||
|
1. **Privacidade** — dados cifrados, LGPD desde o dia zero
|
||||||
|
2. **Simplicidade** — interface para quem nunca fez um inventário
|
||||||
|
3. **Completude** — nenhum benefício esquecido, nenhum prazo perdido
|
||||||
|
4. **Escalabilidade** — regras por UF, extensível para novos tipos de benefício
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Documento gerado em 08/02/2026 — CARONTE Project*
|
||||||
365
docs/CARONTE-Manual-Tecnico.md
Normal file
365
docs/CARONTE-Manual-Tecnico.md
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
# CARONTE — Manual Técnico v1.0
|
||||||
|
|
||||||
|
> 🚣 "O barqueiro que guia famílias pelo rio burocrático pós-óbito"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Visão Geral da Arquitetura
|
||||||
|
|
||||||
|
O CARONTE é uma aplicação web full-stack composta por:
|
||||||
|
|
||||||
|
| Camada | Tecnologia | Porta Padrão |
|
||||||
|
|--------|-----------|-------------|
|
||||||
|
| **Frontend** | Next.js 14 (App Router) + React 18 + Tailwind CSS | 3102 |
|
||||||
|
| **Backend** | FastAPI (Python 3.12) + SQLAlchemy Async | 8102 |
|
||||||
|
| **Banco de Dados** | PostgreSQL (produção) / SQLite (desenvolvimento) | 5432 |
|
||||||
|
| **Proxy Reverso** | Nginx + Cloudflare | 80/443 |
|
||||||
|
| **Process Manager** | PM2 | — |
|
||||||
|
|
||||||
|
### Diagrama de Arquitetura
|
||||||
|
|
||||||
|
```
|
||||||
|
[Cloudflare CDN/DNS]
|
||||||
|
↓
|
||||||
|
[Nginx :80]
|
||||||
|
├── /api/* → FastAPI :8102
|
||||||
|
└── /* → Next.js :3102
|
||||||
|
↓
|
||||||
|
[PostgreSQL :5432]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Estrutura de Pastas
|
||||||
|
|
||||||
|
```
|
||||||
|
caronte/
|
||||||
|
├── backend/
|
||||||
|
│ ├── app/
|
||||||
|
│ │ ├── main.py # Entry point FastAPI
|
||||||
|
│ │ ├── __init__.py
|
||||||
|
│ │ ├── api/v1/ # Rotas da API
|
||||||
|
│ │ │ ├── auth.py # Autenticação (registro, login, /me)
|
||||||
|
│ │ │ ├── familias.py # CRUD famílias, membros, falecidos
|
||||||
|
│ │ │ ├── checklist.py # Checklist burocrático
|
||||||
|
│ │ │ ├── beneficios.py # Scanner de benefícios
|
||||||
|
│ │ │ ├── documentos.py # Geração de documentos PDF
|
||||||
|
│ │ │ └── dashboard.py # Dashboard consolidado
|
||||||
|
│ │ ├── core/
|
||||||
|
│ │ │ ├── config.py # Configurações (Pydantic Settings)
|
||||||
|
│ │ │ ├── database.py # Engine SQLAlchemy async
|
||||||
|
│ │ │ └── security.py # JWT, bcrypt, OAuth2
|
||||||
|
│ │ ├── models/ # Modelos SQLAlchemy
|
||||||
|
│ │ │ ├── usuario.py
|
||||||
|
│ │ │ ├── familia.py # Familia + MembroFamilia
|
||||||
|
│ │ │ ├── falecido.py
|
||||||
|
│ │ │ ├── checklist.py
|
||||||
|
│ │ │ ├── beneficio.py
|
||||||
|
│ │ │ └── documento.py
|
||||||
|
│ │ ├── schemas/
|
||||||
|
│ │ │ └── schemas.py # Pydantic schemas (request/response)
|
||||||
|
│ │ ├── services/
|
||||||
|
│ │ │ ├── checklist_engine.py # Geração automática de checklist
|
||||||
|
│ │ │ ├── beneficio_scanner.py # Scanner de benefícios elegíveis
|
||||||
|
│ │ │ └── document_generator.py # Geração de PDFs (ReportLab)
|
||||||
|
│ │ └── data/
|
||||||
|
│ │ └── checklist_templates.json # Templates de checklist
|
||||||
|
│ ├── uploads/ # PDFs gerados
|
||||||
|
│ ├── requirements.txt
|
||||||
|
│ └── seed_data.py
|
||||||
|
├── frontend/
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── app/
|
||||||
|
│ │ │ ├── layout.js # Layout global
|
||||||
|
│ │ │ ├── page.js # Página inicial
|
||||||
|
│ │ │ ├── login/page.js
|
||||||
|
│ │ │ ├── registro/page.js
|
||||||
|
│ │ │ ├── dashboard/page.js
|
||||||
|
│ │ │ └── familia/[id]/
|
||||||
|
│ │ │ ├── page.js # Detalhe da família
|
||||||
|
│ │ │ ├── checklist/page.js
|
||||||
|
│ │ │ ├── beneficios/page.js
|
||||||
|
│ │ │ └── documentos/page.js
|
||||||
|
│ │ ├── components/
|
||||||
|
│ │ │ └── Sidebar.js
|
||||||
|
│ │ └── lib/
|
||||||
|
│ │ └── api.js # Cliente HTTP para a API
|
||||||
|
│ ├── package.json
|
||||||
|
│ ├── tailwind.config.js
|
||||||
|
│ └── postcss.config.js
|
||||||
|
├── docs/ # Documentação
|
||||||
|
├── start-backend.sh
|
||||||
|
└── start-frontend.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Modelos de Dados
|
||||||
|
|
||||||
|
### 3.1 Usuario
|
||||||
|
| Campo | Tipo | Descrição |
|
||||||
|
|-------|------|-----------|
|
||||||
|
| id | Integer (PK) | Auto-increment |
|
||||||
|
| nome | String | Nome completo |
|
||||||
|
| email | String (unique) | Email de login |
|
||||||
|
| senha_hash | String | Hash bcrypt |
|
||||||
|
| telefone | String (nullable) | Telefone |
|
||||||
|
| created_at | DateTime | Auto |
|
||||||
|
|
||||||
|
### 3.2 Familia
|
||||||
|
| Campo | Tipo | Descrição |
|
||||||
|
|-------|------|-----------|
|
||||||
|
| id | Integer (PK) | Auto-increment |
|
||||||
|
| nome | String | Nome da família |
|
||||||
|
| usuario_id | Integer (FK) | Usuário responsável |
|
||||||
|
| created_at | DateTime | Auto |
|
||||||
|
|
||||||
|
### 3.3 MembroFamilia
|
||||||
|
| Campo | Tipo | Descrição |
|
||||||
|
|-------|------|-----------|
|
||||||
|
| id | Integer (PK) | Auto-increment |
|
||||||
|
| familia_id | Integer (FK) | Família |
|
||||||
|
| nome | String | Nome do membro |
|
||||||
|
| parentesco | String | Grau de parentesco |
|
||||||
|
| cpf, telefone, email | String (nullable) | Dados de contato |
|
||||||
|
|
||||||
|
### 3.4 Falecido
|
||||||
|
| Campo | Tipo | Descrição |
|
||||||
|
|-------|------|-----------|
|
||||||
|
| id | Integer (PK) | Auto-increment |
|
||||||
|
| familia_id | Integer (FK) | Família |
|
||||||
|
| nome | String | Nome completo |
|
||||||
|
| cpf | String (nullable) | CPF |
|
||||||
|
| data_nascimento | Date (nullable) | Nascimento |
|
||||||
|
| data_obito | Date | Data do óbito |
|
||||||
|
| causa_obito | String (nullable) | Causa |
|
||||||
|
| tipo_vinculo | String | empregado, aposentado, autonomo, servidor |
|
||||||
|
| empregador | String (nullable) | Nome do empregador |
|
||||||
|
| tinha_carteira_assinada | Integer | 0/1 |
|
||||||
|
| tinha_fgts | Integer | 0/1 |
|
||||||
|
| era_aposentado | Integer | 0/1 |
|
||||||
|
| tinha_seguro_vida | Integer | 0/1 |
|
||||||
|
| tinha_imoveis | Integer | 0/1 |
|
||||||
|
| tinha_veiculos | Integer | 0/1 |
|
||||||
|
| salario_estimado | Float (nullable) | Último salário |
|
||||||
|
|
||||||
|
### 3.5 ChecklistItem
|
||||||
|
| Campo | Tipo | Descrição |
|
||||||
|
|-------|------|-----------|
|
||||||
|
| id | Integer (PK) | Auto-increment |
|
||||||
|
| familia_id | Integer (FK) | Família |
|
||||||
|
| falecido_id | Integer (FK) | Falecido |
|
||||||
|
| titulo | String | Título da tarefa |
|
||||||
|
| descricao | Text (nullable) | Descrição detalhada |
|
||||||
|
| fase | String | imediato, primeira_semana, 30_dias, 60_dias |
|
||||||
|
| categoria | String | certidoes, inss, fgts, inventario... |
|
||||||
|
| status | String | pendente, andamento, concluido |
|
||||||
|
| prazo_dias | Integer (nullable) | Prazo em dias |
|
||||||
|
| ordem | Integer | Ordem de execução |
|
||||||
|
| dependencia_id | Integer (nullable) | Item que precede |
|
||||||
|
|
||||||
|
### 3.6 Beneficio
|
||||||
|
| Campo | Tipo | Descrição |
|
||||||
|
|-------|------|-----------|
|
||||||
|
| id | Integer (PK) | Auto-increment |
|
||||||
|
| familia_id | Integer (FK) | Família |
|
||||||
|
| falecido_id | Integer (FK) | Falecido |
|
||||||
|
| tipo | String | fgts, pis, pensao_morte, seguro_vida, rescisao |
|
||||||
|
| nome | String | Nome do benefício |
|
||||||
|
| descricao | String (nullable) | Descrição |
|
||||||
|
| status | String | identificado, em_processo, sacado |
|
||||||
|
| valor_estimado | Float (nullable) | Valor estimado |
|
||||||
|
| valor_sacado | Float (nullable) | Valor efetivamente sacado |
|
||||||
|
|
||||||
|
### 3.7 Documento
|
||||||
|
| Campo | Tipo | Descrição |
|
||||||
|
|-------|------|-----------|
|
||||||
|
| id | Integer (PK) | Auto-increment |
|
||||||
|
| familia_id | Integer (FK) | Família |
|
||||||
|
| falecido_id | Integer (FK, nullable) | Falecido |
|
||||||
|
| tipo | String | procuracao, requerimento_fgts, peticao_pensao |
|
||||||
|
| nome | String | Nome do documento |
|
||||||
|
| arquivo_path | String (nullable) | Caminho do PDF |
|
||||||
|
| status | String | gerado, enviado, aprovado |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. API — Endpoints
|
||||||
|
|
||||||
|
Base URL: `/api/v1`
|
||||||
|
|
||||||
|
### 4.1 Autenticação
|
||||||
|
|
||||||
|
| Método | Endpoint | Descrição | Auth |
|
||||||
|
|--------|----------|-----------|------|
|
||||||
|
| POST | `/auth/registro` | Criar conta | ❌ |
|
||||||
|
| POST | `/auth/login` | Login (OAuth2 form) → JWT | ❌ |
|
||||||
|
| GET | `/auth/me` | Dados do usuário logado | ✅ |
|
||||||
|
|
||||||
|
### 4.2 Famílias
|
||||||
|
|
||||||
|
| Método | Endpoint | Descrição | Auth |
|
||||||
|
|--------|----------|-----------|------|
|
||||||
|
| GET | `/familias/` | Listar famílias do usuário | ✅ |
|
||||||
|
| POST | `/familias/` | Criar família | ✅ |
|
||||||
|
| GET | `/familias/{id}` | Detalhe da família | ✅ |
|
||||||
|
| POST | `/familias/{id}/membros` | Adicionar membro | ✅ |
|
||||||
|
| GET | `/familias/{id}/membros` | Listar membros | ✅ |
|
||||||
|
| POST | `/familias/{id}/falecido` | Registrar falecido (gera checklist + benefícios) | ✅ |
|
||||||
|
| GET | `/familias/{id}/falecidos` | Listar falecidos | ✅ |
|
||||||
|
|
||||||
|
### 4.3 Checklist
|
||||||
|
|
||||||
|
| Método | Endpoint | Descrição | Auth |
|
||||||
|
|--------|----------|-----------|------|
|
||||||
|
| GET | `/familias/{id}/checklist/` | Listar itens do checklist | ✅ |
|
||||||
|
| PUT | `/familias/{id}/checklist/{item_id}` | Atualizar status | ✅ |
|
||||||
|
| GET | `/familias/{id}/checklist/proximo` | Próximo passo recomendado | ✅ |
|
||||||
|
|
||||||
|
### 4.4 Benefícios
|
||||||
|
|
||||||
|
| Método | Endpoint | Descrição | Auth |
|
||||||
|
|--------|----------|-----------|------|
|
||||||
|
| GET | `/familias/{id}/beneficios/` | Listar benefícios | ✅ |
|
||||||
|
| POST | `/familias/{id}/beneficios/scan` | Re-escanear benefícios | ✅ |
|
||||||
|
|
||||||
|
### 4.5 Documentos
|
||||||
|
|
||||||
|
| Método | Endpoint | Descrição | Auth |
|
||||||
|
|--------|----------|-----------|------|
|
||||||
|
| GET | `/familias/{id}/documentos/` | Listar documentos | ✅ |
|
||||||
|
| POST | `/familias/{id}/documentos/gerar` | Gerar PDF | ✅ |
|
||||||
|
| GET | `/familias/{id}/documentos/{doc_id}/download` | Download PDF | ✅ |
|
||||||
|
|
||||||
|
**Tipos de documento suportados:** `procuracao`, `requerimento_fgts`, `peticao_pensao`
|
||||||
|
|
||||||
|
### 4.6 Dashboard
|
||||||
|
|
||||||
|
| Método | Endpoint | Descrição | Auth |
|
||||||
|
|--------|----------|-----------|------|
|
||||||
|
| GET | `/dashboard/` | Resumo consolidado do usuário | ✅ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Autenticação
|
||||||
|
|
||||||
|
- **Método:** JWT Bearer Token (OAuth2 Password Flow)
|
||||||
|
- **Hash:** bcrypt via passlib
|
||||||
|
- **Algoritmo:** HS256
|
||||||
|
- **Expiração:** 1440 minutos (24h)
|
||||||
|
- **Header:** `Authorization: Bearer <token>`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Variáveis de Ambiente
|
||||||
|
|
||||||
|
| Variável | Descrição | Padrão |
|
||||||
|
|----------|-----------|--------|
|
||||||
|
| `DATABASE_URL` | URL de conexão ao banco | `sqlite+aiosqlite:///./caronte.db` |
|
||||||
|
| `SECRET_KEY` | Chave secreta para JWT | `caronte-secret-key-change-in-production` |
|
||||||
|
| `ALGORITHM` | Algoritmo JWT | `HS256` |
|
||||||
|
| `ACCESS_TOKEN_EXPIRE_MINUTES` | Expiração do token | `1440` |
|
||||||
|
| `UPLOAD_DIR` | Diretório de uploads | `./uploads` |
|
||||||
|
|
||||||
|
**Produção (PostgreSQL):**
|
||||||
|
```
|
||||||
|
DATABASE_URL=postgresql+asyncpg://caronte:Caronte2026!@localhost:5432/caronte
|
||||||
|
```
|
||||||
|
|
||||||
|
> ⚠️ Para PostgreSQL, adicionar `asyncpg` ao requirements.txt e usar driver `postgresql+asyncpg://`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Requisitos de Sistema
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- Python 3.12+
|
||||||
|
- Dependências: FastAPI, SQLAlchemy, aiosqlite/asyncpg, python-jose, passlib, reportlab, pydantic-settings
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
- Node.js 18+
|
||||||
|
- Next.js 14, React 18, Tailwind CSS, lucide-react
|
||||||
|
|
||||||
|
### Produção
|
||||||
|
- Ubuntu 22.04+ / Debian 12+
|
||||||
|
- PostgreSQL 15+
|
||||||
|
- Nginx
|
||||||
|
- PM2 (Node.js process manager)
|
||||||
|
- Cloudflare (DNS + CDN)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Deploy de Produção
|
||||||
|
|
||||||
|
### 8.1 Backend
|
||||||
|
```bash
|
||||||
|
cd /opt/caronte/backend
|
||||||
|
python3 -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
pip install asyncpg # driver PostgreSQL
|
||||||
|
|
||||||
|
# Criar .env
|
||||||
|
echo 'DATABASE_URL=postgresql+asyncpg://caronte:Caronte2026!@localhost:5432/caronte' > .env
|
||||||
|
echo 'SECRET_KEY=<gerar-chave-segura>' >> .env
|
||||||
|
|
||||||
|
# Iniciar com PM2
|
||||||
|
pm2 start "source .venv/bin/activate && uvicorn app.main:app --host 0.0.0.0 --port 8102" --name caronte-backend
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.2 Frontend
|
||||||
|
```bash
|
||||||
|
cd /opt/caronte/frontend
|
||||||
|
npm install
|
||||||
|
npm run build # NUNCA usar next dev em produção
|
||||||
|
pm2 start "npm start -- -p 3102" --name caronte-frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.3 Nginx
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name caronte.aivertice.com;
|
||||||
|
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://127.0.0.1:8102/;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:3102;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Serviços Inteligentes
|
||||||
|
|
||||||
|
### 9.1 Checklist Engine
|
||||||
|
Ao registrar um falecido, o sistema gera automaticamente um checklist burocrático personalizado baseado no perfil (tipo de vínculo, tinha FGTS, era aposentado, etc.). Os templates são carregados de `checklist_templates.json` e filtrados por condições.
|
||||||
|
|
||||||
|
### 9.2 Benefício Scanner
|
||||||
|
Analisa o perfil do falecido e identifica automaticamente benefícios elegíveis:
|
||||||
|
- **FGTS** (se tinha FGTS) — estimativa: 3.5x salário
|
||||||
|
- **PIS/PASEP** (se tinha carteira assinada) — estimativa: 0.8x salário
|
||||||
|
- **Pensão por Morte** (sempre) — estimativa: 12x salário
|
||||||
|
- **Seguro de Vida** (se tinha) — estimativa: 24x salário
|
||||||
|
- **Verbas Rescisórias** (se tinha carteira assinada) — estimativa: 2x salário
|
||||||
|
|
||||||
|
### 9.3 Document Generator
|
||||||
|
Gera PDFs profissionais via ReportLab:
|
||||||
|
- **Procuração** — representação perante órgãos
|
||||||
|
- **Requerimento FGTS** — saque por falecimento (Lei 8.036/90)
|
||||||
|
- **Petição Pensão por Morte** — requerimento INSS (Lei 8.213/91)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Documento gerado em 08/02/2026 — CARONTE v1.0*
|
||||||
163
docs/CARONTE-Manual-Vendas.md
Normal file
163
docs/CARONTE-Manual-Vendas.md
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
# CARONTE ⚓ — Manual de Vendas
|
||||||
|
|
||||||
|
> *"O barqueiro que guia famílias pelo rio burocrático pós-óbito"*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Proposta de Valor
|
||||||
|
|
||||||
|
Quando uma pessoa falece, a família enlutada precisa enfrentar dezenas de processos burocráticos: certidões, INSS, FGTS, inventário, pensão por morte, seguros — tudo com prazos, filas e documentos complexos. **A maioria das famílias vulneráveis desconhece seus direitos e perde milhares de reais por falta de orientação.**
|
||||||
|
|
||||||
|
O **CARONTE** resolve isso. É uma plataforma inteligente que:
|
||||||
|
|
||||||
|
1. **Identifica automaticamente** todos os benefícios a que a família tem direito
|
||||||
|
2. **Gera um checklist personalizado** com o passo-a-passo completo
|
||||||
|
3. **Produz documentos prontos** (procurações, requerimentos, petições)
|
||||||
|
4. **Acompanha o progresso** de cada caso em tempo real
|
||||||
|
|
||||||
|
**Em resumo:** transforma um processo caótico, doloroso e caro em um caminho claro e guiado.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 👥 Público-Alvo
|
||||||
|
|
||||||
|
### Clientes Primários
|
||||||
|
- **Prefeituras Municipais** — Secretarias de Assistência Social
|
||||||
|
- **CRAS** (Centros de Referência de Assistência Social)
|
||||||
|
- **CREAS** (Centros de Referência Especializados)
|
||||||
|
- **Secretarias Estaduais de Desenvolvimento Social**
|
||||||
|
|
||||||
|
### Clientes Secundários
|
||||||
|
- Escritórios de advocacia (direito previdenciário e sucessório)
|
||||||
|
- Organizações do terceiro setor / ONGs
|
||||||
|
- Sindicatos e associações de trabalhadores
|
||||||
|
- Funerárias e serviços de assistência pós-óbito
|
||||||
|
|
||||||
|
### Perfil do Usuário Final
|
||||||
|
- Assistentes sociais que atendem famílias enlutadas
|
||||||
|
- Coordenadores de CRAS/CREAS
|
||||||
|
- Advogados populares e defensores públicos
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚙️ Funcionalidades Principais
|
||||||
|
|
||||||
|
### 1. Cadastro Inteligente de Famílias
|
||||||
|
- Registro completo da família: membros, parentesco, contatos
|
||||||
|
- Registro do falecido com perfil detalhado (vínculo empregatício, benefícios ativos, patrimônio)
|
||||||
|
- Suporte a múltiplos falecidos por família
|
||||||
|
|
||||||
|
### 2. Scanner Automático de Benefícios 💰
|
||||||
|
O sistema analisa o perfil do falecido e identifica automaticamente:
|
||||||
|
- **FGTS** — Saque do saldo por dependentes
|
||||||
|
- **PIS/PASEP** — Cotas disponíveis para herdeiros
|
||||||
|
- **Pensão por Morte (INSS)** — Benefício mensal
|
||||||
|
- **Seguro de Vida** — Indenização contratual
|
||||||
|
- **Verbas Rescisórias** — Saldo de salário, férias, 13º
|
||||||
|
- **Estimativa de valores** para cada benefício
|
||||||
|
|
||||||
|
### 3. Checklist Burocrático Personalizado ✅
|
||||||
|
- Gerado automaticamente ao registrar o falecido
|
||||||
|
- Dividido em fases: **Imediato → 1ª Semana → 30 dias → 60 dias**
|
||||||
|
- Categorizado: certidões, INSS, FGTS, inventário, bancos
|
||||||
|
- Indica o **próximo passo** recomendado
|
||||||
|
- Acompanhamento de status (pendente → em andamento → concluído)
|
||||||
|
|
||||||
|
### 4. Geração Automática de Documentos 📄
|
||||||
|
Documentos PDF profissionais gerados em segundos:
|
||||||
|
- **Procuração** para representação em órgãos
|
||||||
|
- **Requerimento de Saque do FGTS** (conforme Lei 8.036/90)
|
||||||
|
- **Petição de Pensão por Morte** (conforme Lei 8.213/91)
|
||||||
|
- Download imediato, prontos para impressão e assinatura
|
||||||
|
|
||||||
|
### 5. Dashboard de Gestão 📊
|
||||||
|
- Visão consolidada de todas as famílias atendidas
|
||||||
|
- Famílias ativas, itens pendentes, benefícios identificados
|
||||||
|
- Progresso percentual de cada caso
|
||||||
|
- Documentos gerados
|
||||||
|
|
||||||
|
### 6. Sistema de Autenticação Seguro 🔐
|
||||||
|
- Cadastro e login com email/senha
|
||||||
|
- Tokens JWT com expiração de 24h
|
||||||
|
- Senhas criptografadas com bcrypt
|
||||||
|
- Cada profissional vê apenas suas famílias
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏆 Benefícios e Diferenciais
|
||||||
|
|
||||||
|
### Para as Famílias
|
||||||
|
| Benefício | Impacto |
|
||||||
|
|-----------|---------|
|
||||||
|
| Nenhum benefício perdido por desconhecimento | Milhares de R$ recuperados |
|
||||||
|
| Checklist claro eliminando incertezas | Redução de estresse no luto |
|
||||||
|
| Documentos prontos sem advogado | Economia de honorários |
|
||||||
|
| Prazos controlados | Zero perdas por prescrição |
|
||||||
|
|
||||||
|
### Para o Município / CRAS
|
||||||
|
| Benefício | Impacto |
|
||||||
|
|-----------|---------|
|
||||||
|
| Atendimento 5x mais rápido | Maior capacidade de atendimento |
|
||||||
|
| Padronização do processo | Qualidade uniforme |
|
||||||
|
| Métricas de acompanhamento | Relatórios para gestão |
|
||||||
|
| Menos retrabalho | Economia de recursos públicos |
|
||||||
|
| Visibilidade dos resultados | Indicadores para prestação de contas |
|
||||||
|
|
||||||
|
### Diferenciais Competitivos
|
||||||
|
- ⚡ **Automação inteligente** — não é apenas um formulário, é um motor de regras
|
||||||
|
- 🎯 **Especializado** — único sistema focado 100% no pós-óbito para famílias vulneráveis
|
||||||
|
- 📱 **100% web** — funciona em qualquer dispositivo, sem instalação
|
||||||
|
- 🔒 **Seguro** — dados sensíveis protegidos com criptografia
|
||||||
|
- 🇧🇷 **100% brasileiro** — legislação brasileira embutida (INSS, FGTS, CLT)
|
||||||
|
- ⚖️ **Base legal integrada** — documentos citam artigos de lei corretos
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Casos de Uso
|
||||||
|
|
||||||
|
### Caso 1: Maria, viúva de trabalhador CLT
|
||||||
|
> João faleceu. Maria, com 3 filhos, não sabe por onde começar. A assistente social do CRAS cadastra a família no CARONTE. Em minutos, o sistema identifica: **FGTS (R$ 8.750), PIS (R$ 2.000), Pensão por Morte (R$ 30.000/ano), Verbas Rescisórias (R$ 5.000)**. O checklist guia Maria passo a passo. Os documentos são gerados automaticamente. Total recuperado: **R$ 45.750+**.
|
||||||
|
|
||||||
|
### Caso 2: CRAS Municipal com 200 atendimentos/mês
|
||||||
|
> Sem o CARONTE, cada caso demora 3-4 horas de orientação. Com o sistema, o tempo cai para 40 minutos. A equipe de 5 assistentes sociais consegue atender 3x mais famílias. O município apresenta indicadores concretos de impacto social.
|
||||||
|
|
||||||
|
### Caso 3: Família de aposentado com múltiplos benefícios
|
||||||
|
> Sr. Antônio era aposentado, tinha FGTS antigo, seguro de vida e dois imóveis. Os filhos não sabiam de metade dos benefícios. O CARONTE identificou tudo, priorizou por urgência e gerou os documentos necessários.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Por que CARONTE?
|
||||||
|
|
||||||
|
Na mitologia grega, **Caronte** era o barqueiro que guiava as almas pelo rio Estige. Nosso CARONTE guia as famílias pelo rio da burocracia — transformando um momento de dor em um caminho claro, com todos os direitos garantidos.
|
||||||
|
|
||||||
|
### Impacto Social Mensurável
|
||||||
|
- 💰 **Valor médio recuperado por família:** R$ 30.000 - R$ 80.000
|
||||||
|
- ⏱️ **Redução de tempo de atendimento:** 70%
|
||||||
|
- 📈 **Famílias que perdem benefícios sem orientação:** 60% → 0%
|
||||||
|
- 📊 **Indicadores para prestação de contas** ao governo federal
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 Demonstração
|
||||||
|
|
||||||
|
Acesse a demonstração online:
|
||||||
|
|
||||||
|
🔗 **https://caronte.aivertice.com**
|
||||||
|
|
||||||
|
**Acesso demo:**
|
||||||
|
- Email: `demo@caronte.com`
|
||||||
|
- Senha: `demo123`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏢 Sobre a AI Vertice
|
||||||
|
|
||||||
|
O CARONTE é desenvolvido pela **AI Vertice**, empresa especializada em soluções de inteligência artificial e automação para o setor público e social.
|
||||||
|
|
||||||
|
**Portfólio completo:** https://aivertice.com
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*CARONTE — Nenhuma família desamparada. Nenhum direito perdido.*
|
||||||
|
|
||||||
|
*© 2026 AI Vertice. Todos os direitos reservados.*
|
||||||
6
frontend/.eslintrc.json
Normal file
6
frontend/.eslintrc.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"extends": "next/core-web-vitals",
|
||||||
|
"rules": {
|
||||||
|
"react/no-unescaped-entities": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
36
frontend/.gitignore
vendored
Normal file
36
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
.yarn/install-state.gz
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
36
frontend/README.md
Normal file
36
frontend/README.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
First, run the development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
# or
|
||||||
|
yarn dev
|
||||||
|
# or
|
||||||
|
pnpm dev
|
||||||
|
# or
|
||||||
|
bun dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||||
|
|
||||||
|
You can start editing the page by modifying `app/page.js`. The page auto-updates as you edit the file.
|
||||||
|
|
||||||
|
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
|
To learn more about Next.js, take a look at the following resources:
|
||||||
|
|
||||||
|
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||||
|
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||||
|
|
||||||
|
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||||
|
|
||||||
|
## Deploy on Vercel
|
||||||
|
|
||||||
|
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||||
|
|
||||||
|
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
||||||
7
frontend/jsconfig.json
Normal file
7
frontend/jsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
4
frontend/next.config.mjs
Normal file
4
frontend/next.config.mjs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
5910
frontend/package-lock.json
generated
Normal file
5910
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
frontend/package.json
Normal file
23
frontend/package.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"lucide-react": "^0.563.0",
|
||||||
|
"next": "14.2.35",
|
||||||
|
"react": "^18",
|
||||||
|
"react-dom": "^18"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"eslint": "^8",
|
||||||
|
"eslint-config-next": "14.2.35",
|
||||||
|
"postcss": "^8",
|
||||||
|
"tailwindcss": "^3.4.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
8
frontend/postcss.config.mjs
Normal file
8
frontend/postcss.config.mjs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/** @type {import('postcss-load-config').Config} */
|
||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
108
frontend/src/app/dashboard/page.js
Normal file
108
frontend/src/app/dashboard/page.js
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
'use client'
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
import Sidebar from '@/components/Sidebar'
|
||||||
|
import { ArrowRight, Clock, AlertTriangle } from 'lucide-react'
|
||||||
|
|
||||||
|
export default function Dashboard() {
|
||||||
|
const router = useRouter()
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!localStorage.getItem('token')) { router.push('/login'); return }
|
||||||
|
setLoading(false)
|
||||||
|
}, [router])
|
||||||
|
|
||||||
|
if (loading) return <div className="flex items-center justify-center h-screen bg-[var(--void)]"><span className="text-5xl boat-float">🚣</span></div>
|
||||||
|
|
||||||
|
const cards = [
|
||||||
|
{ label: 'Famílias', value: 3, emoji: '⚱️' },
|
||||||
|
{ label: 'Pendências', value: 12, emoji: '📜' },
|
||||||
|
{ label: 'Benefícios', value: 8, emoji: '🔮' },
|
||||||
|
{ label: 'Pergaminhos', value: 5, emoji: '📄' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const familias = [
|
||||||
|
{ id: 1, nome: 'Família Silva', falecido: 'José Carlos Silva', progresso: 35, pendentes: 8, data: '15/01/2026' },
|
||||||
|
{ id: 2, nome: 'Família Oliveira', falecido: 'Maria Oliveira', progresso: 60, pendentes: 4, data: '20/01/2026' },
|
||||||
|
{ id: 3, nome: 'Família Santos', falecido: 'Antônio Santos', progresso: 15, pendentes: 12, data: '01/02/2026' },
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen bg-[var(--void)]">
|
||||||
|
<Sidebar />
|
||||||
|
<main className="ml-60 flex-1 p-8">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="mb-10 fade-in">
|
||||||
|
<p className="text-[10px] tracking-[0.4em] text-[var(--gold-dim)] uppercase font-myth mb-2">🏛️ Ágora</p>
|
||||||
|
<h1 className="font-myth text-2xl font-bold text-[var(--marble)] tracking-wider">Visão do Submundo</h1>
|
||||||
|
<div className="ornament-line-simple max-w-[200px] mt-3" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats */}
|
||||||
|
<div className="grid grid-cols-4 gap-4 mb-8">
|
||||||
|
{cards.map((c, i) => (
|
||||||
|
<div key={i} className="card-hades p-5 text-center group hover:scale-[1.02] transition-all duration-300">
|
||||||
|
<span className="text-2xl block mb-2">{c.emoji}</span>
|
||||||
|
<p className="font-myth-decorative text-2xl gold-text">{c.value}</p>
|
||||||
|
<p className="text-[var(--smoke)] text-[10px] tracking-[0.15em] font-myth uppercase mt-1">{c.label}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Treasure */}
|
||||||
|
<div className="card-hades p-6 mb-8 relative overflow-hidden group">
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-r from-[var(--ember)]/[0.03] to-transparent" />
|
||||||
|
<div className="flex items-center gap-5 relative z-10">
|
||||||
|
<span className="text-4xl group-hover:scale-110 transition-transform">🔱</span>
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] tracking-[0.3em] text-[var(--gold-dim)] uppercase font-myth">Tesouro a Recuperar</p>
|
||||||
|
<p className="text-3xl font-myth-decorative gold-text mt-1">R$ 45.000</p>
|
||||||
|
<p className="text-[var(--smoke)] text-xs mt-1">em benefícios identificados para suas famílias</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Families header */}
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] tracking-[0.4em] text-[var(--gold-dim)] uppercase font-myth mb-1">⚱️ Núcleos</p>
|
||||||
|
<h2 className="font-myth text-lg text-[var(--marble)] tracking-wider">Famílias</h2>
|
||||||
|
</div>
|
||||||
|
<button className="btn-outline px-4 py-2 text-[10px]">+ NOVA FAMÍLIA</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Families list */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
{familias.map((f) => (
|
||||||
|
<div key={f.id} onClick={() => router.push(`/familia/${f.id}`)}
|
||||||
|
className="card-hades p-5 cursor-pointer group hover:scale-[1.005] transition-all duration-300">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="text-lg">⚱️</span>
|
||||||
|
<h3 className="font-myth text-sm font-semibold text-[var(--marble)] tracking-wider">{f.nome}</h3>
|
||||||
|
{f.pendentes > 5 && (
|
||||||
|
<span className="flex items-center gap-1 text-[9px] text-[var(--ember)] bg-[var(--ember)]/10 px-2 py-0.5 font-myth tracking-wider">
|
||||||
|
<AlertTriangle size={9} /> {f.pendentes}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-[var(--ash)] text-xs ml-8 mt-1">Falecido(a): {f.falecido}</p>
|
||||||
|
<div className="flex items-center gap-4 mt-3 ml-8">
|
||||||
|
<div className="flex-1 max-w-[200px] progress-styx h-1.5">
|
||||||
|
<div className="progress-styx-fill h-full transition-all" style={{ width: `${f.progresso}%` }} />
|
||||||
|
</div>
|
||||||
|
<span className="text-xs gold-text font-myth font-bold">{f.progresso}%</span>
|
||||||
|
<span className="text-[10px] text-[var(--smoke)] flex items-center gap-1"><Clock size={9} /> {f.data}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ArrowRight className="text-[var(--smoke)] group-hover:text-[var(--gold)] transition" size={16} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
104
frontend/src/app/familia/[id]/beneficios/page.js
Normal file
104
frontend/src/app/familia/[id]/beneficios/page.js
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
'use client'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { useParams, useRouter } from 'next/navigation'
|
||||||
|
import Sidebar from '@/components/Sidebar'
|
||||||
|
import { Search, DollarSign, Clock, CheckCircle2, AlertTriangle, Loader2 } from 'lucide-react'
|
||||||
|
|
||||||
|
const beneficiosData = [
|
||||||
|
{ id: 1, tipo: 'FGTS', instituicao: 'Caixa Econômica Federal', valor_estimado: 18500, valor_sacado: 0, status: 'identificado', prazo: 'Sem prazo', icon: '🏦', desc: 'Saldo de FGTS do último vínculo empregatício' },
|
||||||
|
{ id: 2, tipo: 'PIS/PASEP', instituicao: 'Caixa Econômica Federal', valor_estimado: 3200, valor_sacado: 0, status: 'identificado', prazo: '5 anos', icon: '💳', desc: 'Abono salarial e cotas do PIS acumuladas' },
|
||||||
|
{ id: 3, tipo: 'Pensão por Morte', instituicao: 'INSS', valor_estimado: 2800, valor_sacado: 0, status: 'em_processo', prazo: '90 dias (retroativa)', icon: '🏛️', desc: 'Benefício mensal para dependentes — R$2.800/mês' },
|
||||||
|
{ id: 4, tipo: 'Seguro de Vida', instituicao: 'Bradesco Seguros', valor_estimado: 15000, valor_sacado: 0, status: 'identificado', prazo: '3 anos', icon: '🛡️', desc: 'Apólice de seguro de vida em grupo (empregador)' },
|
||||||
|
{ id: 5, tipo: 'Restituição IR', instituicao: 'Receita Federal', valor_estimado: 4300, valor_sacado: 4300, status: 'sacado', prazo: '5 anos', icon: '📄', desc: 'Restituição de imposto de renda pendente' },
|
||||||
|
{ id: 6, tipo: 'DPVAT', instituicao: 'Seguradora Líder', valor_estimado: 0, valor_sacado: 0, status: 'nao_aplicavel', prazo: '3 anos', icon: '🚗', desc: 'Não aplicável — causa da morte não foi acidente de trânsito' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const statusConfig = {
|
||||||
|
identificado: { label: 'Identificado', color: 'text-blue-400', bg: 'bg-blue-500/10 border-blue-500/30' },
|
||||||
|
em_processo: { label: 'Em Processo', color: 'text-yellow-400', bg: 'bg-yellow-500/10 border-yellow-500/30' },
|
||||||
|
sacado: { label: 'Sacado ✓', color: 'text-green-400', bg: 'bg-green-500/10 border-green-500/30' },
|
||||||
|
nao_aplicavel: { label: 'N/A', color: 'text-gray-500', bg: 'bg-white/5 border-white/10' },
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function BeneficiosPage() {
|
||||||
|
const router = useRouter()
|
||||||
|
const params = useParams()
|
||||||
|
const [beneficios, setBeneficios] = useState(beneficiosData)
|
||||||
|
const [scanning, setScanning] = useState(false)
|
||||||
|
|
||||||
|
const totalEstimado = beneficios.filter(b => b.status !== 'nao_aplicavel').reduce((a, b) => a + b.valor_estimado, 0)
|
||||||
|
const totalSacado = beneficios.reduce((a, b) => a + b.valor_sacado, 0)
|
||||||
|
const aProcurar = totalEstimado - totalSacado
|
||||||
|
|
||||||
|
const scan = () => {
|
||||||
|
setScanning(true)
|
||||||
|
setTimeout(() => {
|
||||||
|
setScanning(false)
|
||||||
|
alert('✅ Scan completo! Nenhum novo benefício encontrado.')
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen bg-[#0a0a0f]">
|
||||||
|
<Sidebar />
|
||||||
|
<main className="ml-64 flex-1 p-8">
|
||||||
|
<button onClick={() => router.push(`/familia/${params.id}`)} className="text-gray-400 hover:text-white text-sm mb-6">← Voltar à Família</button>
|
||||||
|
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold text-white">Scanner de Benefícios</h1>
|
||||||
|
<p className="text-gray-400 mt-1">Benefícios identificados para os herdeiros</p>
|
||||||
|
</div>
|
||||||
|
<button onClick={scan} disabled={scanning}
|
||||||
|
className="flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-purple-600 to-purple-500 text-white rounded-xl hover:from-purple-500 hover:to-purple-400 transition disabled:opacity-50 font-medium">
|
||||||
|
{scanning ? <><Loader2 size={18} className="animate-spin" /> Escaneando...</> : <><Search size={18} /> Escanear Benefícios</>}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
|
||||||
|
<div className="glass bg-gradient-to-br from-blue-500/10 to-blue-600/5 border-blue-500/20 p-6">
|
||||||
|
<p className="text-gray-400 text-sm">Total Estimado</p>
|
||||||
|
<p className="text-2xl font-bold text-blue-400 mt-1">R$ {totalEstimado.toLocaleString('pt-BR')}</p>
|
||||||
|
</div>
|
||||||
|
<div className="glass bg-gradient-to-br from-green-500/10 to-green-600/5 border-green-500/20 p-6">
|
||||||
|
<p className="text-gray-400 text-sm">Já Sacado</p>
|
||||||
|
<p className="text-2xl font-bold text-green-400 mt-1">R$ {totalSacado.toLocaleString('pt-BR')}</p>
|
||||||
|
</div>
|
||||||
|
<div className="glass bg-gradient-to-br from-yellow-500/10 to-yellow-600/5 border-yellow-500/20 p-6">
|
||||||
|
<p className="text-gray-400 text-sm">A Recuperar</p>
|
||||||
|
<p className="text-2xl font-bold text-yellow-400 mt-1">R$ {aProcurar.toLocaleString('pt-BR')}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{beneficios.map((b) => {
|
||||||
|
const cfg = statusConfig[b.status]
|
||||||
|
return (
|
||||||
|
<div key={b.id} className={`glass p-6 ${b.status === 'nao_aplicavel' ? 'opacity-40' : ''}`}>
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<span className="text-3xl">{b.icon}</span>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-white">{b.tipo}</h3>
|
||||||
|
<p className="text-gray-400 text-sm">{b.instituicao}</p>
|
||||||
|
<p className="text-gray-500 text-xs mt-1">{b.desc}</p>
|
||||||
|
<div className="flex items-center gap-4 mt-3">
|
||||||
|
{b.valor_estimado > 0 && (
|
||||||
|
<span className="flex items-center gap-1 text-sm text-yellow-400">
|
||||||
|
<DollarSign size={14} /> R$ {b.valor_estimado.toLocaleString('pt-BR')}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="flex items-center gap-1 text-xs text-gray-500"><Clock size={12} /> Prazo: {b.prazo}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className={`text-xs px-3 py-1 rounded-full border ${cfg.bg} ${cfg.color}`}>{cfg.label}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
115
frontend/src/app/familia/[id]/checklist/page.js
Normal file
115
frontend/src/app/familia/[id]/checklist/page.js
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
'use client'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { useParams, useRouter } from 'next/navigation'
|
||||||
|
import Sidebar from '@/components/Sidebar'
|
||||||
|
import { CheckCircle2, Circle, Clock, AlertTriangle, Lock, Star } from 'lucide-react'
|
||||||
|
|
||||||
|
const checklistData = [
|
||||||
|
{ fase: '⚡ Imediato (24-48h)', items: [
|
||||||
|
{ id: 1, titulo: 'Obter Certidão de Óbito', desc: 'Registrar no cartório mais próximo', status: 'concluido', prazo: null },
|
||||||
|
{ id: 2, titulo: 'Comunicar ao empregador', desc: 'Solicitar rescisão e verbas trabalhistas', status: 'concluido', prazo: null },
|
||||||
|
{ id: 3, titulo: 'Comunicar ao banco', desc: 'Bloquear contas e informar o falecimento', status: 'em_andamento', prazo: '2026-02-15' },
|
||||||
|
{ id: 4, titulo: 'Cancelar assinaturas recorrentes', desc: 'Streaming, seguros, planos de saúde', status: 'pendente', prazo: null },
|
||||||
|
]},
|
||||||
|
{ fase: '📋 1ª Semana', items: [
|
||||||
|
{ id: 5, titulo: 'Requerer Pensão por Morte (INSS)', desc: 'Prazo de 90 dias para retroatividade', status: 'em_andamento', prazo: '2026-04-10', urgente: true },
|
||||||
|
{ id: 6, titulo: 'Sacar FGTS do falecido', desc: 'Levar certidão de óbito à Caixa', status: 'pendente', prazo: null },
|
||||||
|
{ id: 7, titulo: 'Sacar PIS/PASEP', desc: 'Caixa (PIS) ou Banco do Brasil (PASEP)', status: 'pendente', prazo: null },
|
||||||
|
{ id: 8, titulo: 'Consultar seguros de vida', desc: 'Verificar apólices ativas na SUSEP', status: 'pendente', prazo: null },
|
||||||
|
]},
|
||||||
|
{ fase: '📅 30 Dias', items: [
|
||||||
|
{ id: 9, titulo: 'Transferência de veículos', desc: 'DETRAN - transferir para herdeiros', status: 'bloqueado', prazo: null, depende: 'Inventário' },
|
||||||
|
{ id: 10, titulo: 'Comunicar Receita Federal', desc: 'Declaração final de espólio', status: 'pendente', prazo: '2026-04-30' },
|
||||||
|
{ id: 11, titulo: 'Consultar restituição IR', desc: 'Verificar se há restituição pendente', status: 'pendente', prazo: null },
|
||||||
|
]},
|
||||||
|
{ fase: '⚖️ 60 Dias (Inventário)', items: [
|
||||||
|
{ id: 12, titulo: 'Iniciar inventário', desc: 'Judicial ou extrajudicial (cartório)', status: 'pendente', prazo: '2026-03-10', urgente: true },
|
||||||
|
{ id: 13, titulo: 'Avaliação de bens', desc: 'Imóveis, veículos, investimentos', status: 'bloqueado', prazo: null, depende: 'Inventário iniciado' },
|
||||||
|
{ id: 14, titulo: 'Cálculo ITCMD', desc: 'Imposto sobre herança (varia por UF)', status: 'bloqueado', prazo: null, depende: 'Avaliação' },
|
||||||
|
{ id: 15, titulo: 'Partilha de bens', desc: 'Divisão entre herdeiros', status: 'bloqueado', prazo: null, depende: 'ITCMD pago' },
|
||||||
|
]}
|
||||||
|
]
|
||||||
|
|
||||||
|
const statusConfig = {
|
||||||
|
concluido: { icon: <CheckCircle2 size={20} />, color: 'text-green-400', bg: 'bg-green-500/10', label: 'Concluído' },
|
||||||
|
em_andamento: { icon: <Clock size={20} />, color: 'text-yellow-400', bg: 'bg-yellow-500/10', label: 'Em andamento' },
|
||||||
|
pendente: { icon: <Circle size={20} />, color: 'text-gray-400', bg: 'bg-white/5', label: 'Pendente' },
|
||||||
|
bloqueado: { icon: <Lock size={20} />, color: 'text-red-400', bg: 'bg-red-500/10', label: 'Bloqueado' },
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ChecklistPage() {
|
||||||
|
const router = useRouter()
|
||||||
|
const params = useParams()
|
||||||
|
const [checklist, setChecklist] = useState(checklistData)
|
||||||
|
|
||||||
|
const toggleItem = (faseIdx, itemIdx) => {
|
||||||
|
const updated = [...checklist]
|
||||||
|
const item = updated[faseIdx].items[itemIdx]
|
||||||
|
if (item.status === 'bloqueado') return
|
||||||
|
const cycle = { pendente: 'em_andamento', em_andamento: 'concluido', concluido: 'pendente' }
|
||||||
|
item.status = cycle[item.status] || 'pendente'
|
||||||
|
setChecklist(updated)
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = checklist.reduce((a, f) => a + f.items.length, 0)
|
||||||
|
const done = checklist.reduce((a, f) => a + f.items.filter(i => i.status === 'concluido').length, 0)
|
||||||
|
const pct = Math.round((done / total) * 100)
|
||||||
|
|
||||||
|
const nextStep = checklist.flatMap(f => f.items).find(i => i.status === 'pendente' || i.status === 'em_andamento')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen bg-[#0a0a0f]">
|
||||||
|
<Sidebar />
|
||||||
|
<main className="ml-64 flex-1 p-8">
|
||||||
|
<button onClick={() => router.push(`/familia/${params.id}`)} className="text-gray-400 hover:text-white text-sm mb-6">← Voltar à Família</button>
|
||||||
|
|
||||||
|
<h1 className="text-3xl font-bold text-white mb-2">Checklist Burocrático</h1>
|
||||||
|
<p className="text-gray-400 mb-6">{done} de {total} itens concluídos ({pct}%)</p>
|
||||||
|
|
||||||
|
<div className="w-full bg-white/5 rounded-full h-3 mb-8">
|
||||||
|
<div className="bg-gradient-to-r from-purple-600 to-green-400 h-3 rounded-full transition-all" style={{ width: `${pct}%` }} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{nextStep && (
|
||||||
|
<div className="glass bg-gradient-to-r from-purple-500/10 to-blue-500/10 border-purple-500/20 p-6 mb-8">
|
||||||
|
<div className="flex items-center gap-3 mb-2">
|
||||||
|
<Star className="text-yellow-400" size={20} />
|
||||||
|
<span className="text-sm text-yellow-400 font-medium">PRÓXIMO PASSO SUGERIDO</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-white text-lg font-semibold">{nextStep.titulo}</p>
|
||||||
|
<p className="text-gray-400 text-sm mt-1">{nextStep.desc}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-8">
|
||||||
|
{checklist.map((fase, fi) => (
|
||||||
|
<div key={fi}>
|
||||||
|
<h2 className="text-lg font-bold text-white mb-4">{fase.fase}</h2>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{fase.items.map((item, ii) => {
|
||||||
|
const cfg = statusConfig[item.status]
|
||||||
|
return (
|
||||||
|
<div key={item.id} onClick={() => toggleItem(fi, ii)}
|
||||||
|
className={`glass p-4 flex items-center gap-4 cursor-pointer hover:bg-white/10 transition ${item.status === 'bloqueado' ? 'opacity-50 cursor-not-allowed' : ''}`}>
|
||||||
|
<div className={cfg.color}>{cfg.icon}</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className={`font-medium ${item.status === 'concluido' ? 'text-gray-500 line-through' : 'text-white'}`}>{item.titulo}</p>
|
||||||
|
<p className="text-gray-500 text-sm">{item.desc}</p>
|
||||||
|
{item.depende && <p className="text-red-400/60 text-xs mt-1">🔒 Depende de: {item.depende}</p>}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{item.urgente && <span className="flex items-center gap-1 text-xs text-yellow-400 bg-yellow-500/10 px-2 py-1 rounded-full"><AlertTriangle size={12} /> Urgente</span>}
|
||||||
|
{item.prazo && <span className="text-xs text-gray-500 flex items-center gap-1"><Clock size={12} /> {new Date(item.prazo).toLocaleDateString('pt-BR')}</span>}
|
||||||
|
<span className={`text-xs px-2 py-1 rounded-full ${cfg.bg} ${cfg.color}`}>{cfg.label}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
110
frontend/src/app/familia/[id]/documentos/page.js
Normal file
110
frontend/src/app/familia/[id]/documentos/page.js
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
'use client'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { useParams, useRouter } from 'next/navigation'
|
||||||
|
import Sidebar from '@/components/Sidebar'
|
||||||
|
import { FileText, Download, Upload, Plus, Clock, CheckCircle2, File } from 'lucide-react'
|
||||||
|
|
||||||
|
const docsData = [
|
||||||
|
{ id: 1, nome: 'Procuração para Inventariante', tipo: 'gerado', categoria: 'Procuração', data: '2026-02-01', tamanho: '45 KB' },
|
||||||
|
{ id: 2, nome: 'Requerimento Saque FGTS', tipo: 'gerado', categoria: 'Requerimento', data: '2026-02-03', tamanho: '38 KB' },
|
||||||
|
{ id: 3, nome: 'Requerimento Pensão por Morte', tipo: 'gerado', categoria: 'Requerimento', data: '2026-02-05', tamanho: '52 KB' },
|
||||||
|
{ id: 4, nome: 'Certidão de Óbito', tipo: 'upload', categoria: 'Certidão', data: '2026-01-12', tamanho: '1.2 MB' },
|
||||||
|
{ id: 5, nome: 'RG do Falecido', tipo: 'upload', categoria: 'Documento Pessoal', data: '2026-01-12', tamanho: '890 KB' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const templatesDocs = [
|
||||||
|
{ id: 'procuracao', nome: 'Procuração para Herdeiro', desc: 'Procuração para representar a família no inventário' },
|
||||||
|
{ id: 'fgts', nome: 'Requerimento Saque FGTS', desc: 'Formulário para saque do FGTS por falecimento' },
|
||||||
|
{ id: 'pensao', nome: 'Requerimento Pensão por Morte', desc: 'Pedido de pensão por morte ao INSS' },
|
||||||
|
{ id: 'alvara', nome: 'Petição de Alvará Judicial', desc: 'Para liberação de valores sem inventário (Lei 6.858/80)' },
|
||||||
|
{ id: 'comunicacao', nome: 'Comunicação de Óbito ao Banco', desc: 'Carta formal para instituição financeira' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export default function DocumentosPage() {
|
||||||
|
const router = useRouter()
|
||||||
|
const params = useParams()
|
||||||
|
const [docs, setDocs] = useState(docsData)
|
||||||
|
const [showGerador, setShowGerador] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen bg-[#0a0a0f]">
|
||||||
|
<Sidebar />
|
||||||
|
<main className="ml-64 flex-1 p-8">
|
||||||
|
<button onClick={() => router.push(`/familia/${params.id}`)} className="text-gray-400 hover:text-white text-sm mb-6">← Voltar à Família</button>
|
||||||
|
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold text-white">Documentos</h1>
|
||||||
|
<p className="text-gray-400 mt-1">{docs.length} documentos</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<button className="flex items-center gap-2 px-4 py-2 bg-white/5 text-gray-300 rounded-xl hover:bg-white/10 transition border border-white/10">
|
||||||
|
<Upload size={16} /> Upload
|
||||||
|
</button>
|
||||||
|
<button onClick={() => setShowGerador(!showGerador)} className="flex items-center gap-2 px-4 py-2 bg-gradient-to-r from-purple-600 to-purple-500 text-white rounded-xl hover:from-purple-500 hover:to-purple-400 transition font-medium">
|
||||||
|
<Plus size={16} /> Gerar Documento
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showGerador && (
|
||||||
|
<div className="glass bg-gradient-to-r from-purple-500/5 to-blue-500/5 border-purple-500/20 p-6 mb-8">
|
||||||
|
<h2 className="text-lg font-semibold text-white mb-4">📄 Gerador de Documentos</h2>
|
||||||
|
<p className="text-gray-400 text-sm mb-4">Selecione um modelo para gerar automaticamente:</p>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
|
{templatesDocs.map((t) => (
|
||||||
|
<div key={t.id} onClick={() => { alert(`✅ Documento "${t.nome}" gerado com sucesso!`); setShowGerador(false) }}
|
||||||
|
className="p-4 bg-white/5 rounded-xl cursor-pointer hover:bg-purple-500/10 hover:border-purple-500/30 border border-white/5 transition">
|
||||||
|
<p className="text-white font-medium">{t.nome}</p>
|
||||||
|
<p className="text-gray-500 text-xs mt-1">{t.desc}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 mb-8">
|
||||||
|
<div className="glass p-4 flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 bg-purple-500/20 rounded-xl flex items-center justify-center text-purple-400"><FileText size={20} /></div>
|
||||||
|
<div>
|
||||||
|
<p className="text-2xl font-bold text-white">{docs.filter(d => d.tipo === 'gerado').length}</p>
|
||||||
|
<p className="text-gray-500 text-xs">Gerados pelo sistema</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="glass p-4 flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 bg-blue-500/20 rounded-xl flex items-center justify-center text-blue-400"><Upload size={20} /></div>
|
||||||
|
<div>
|
||||||
|
<p className="text-2xl font-bold text-white">{docs.filter(d => d.tipo === 'upload').length}</p>
|
||||||
|
<p className="text-gray-500 text-xs">Enviados por você</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{docs.map((doc) => (
|
||||||
|
<div key={doc.id} className="glass p-4 flex items-center justify-between hover:bg-white/10 transition">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className={`w-10 h-10 rounded-xl flex items-center justify-center ${doc.tipo === 'gerado' ? 'bg-purple-500/20 text-purple-400' : 'bg-blue-500/20 text-blue-400'}`}>
|
||||||
|
{doc.tipo === 'gerado' ? <FileText size={20} /> : <File size={20} />}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-white font-medium">{doc.nome}</p>
|
||||||
|
<div className="flex items-center gap-3 mt-1">
|
||||||
|
<span className="text-xs text-gray-500">{doc.categoria}</span>
|
||||||
|
<span className="text-xs text-gray-600">•</span>
|
||||||
|
<span className="text-xs text-gray-500 flex items-center gap-1"><Clock size={10} /> {new Date(doc.data).toLocaleDateString('pt-BR')}</span>
|
||||||
|
<span className="text-xs text-gray-600">•</span>
|
||||||
|
<span className="text-xs text-gray-500">{doc.tamanho}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button className="flex items-center gap-2 px-3 py-2 bg-white/5 text-gray-300 rounded-lg hover:bg-white/10 transition text-sm">
|
||||||
|
<Download size={14} /> Baixar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
96
frontend/src/app/familia/[id]/page.js
Normal file
96
frontend/src/app/familia/[id]/page.js
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
'use client'
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import { useRouter, useParams } from 'next/navigation'
|
||||||
|
import Sidebar from '@/components/Sidebar'
|
||||||
|
import { User, CheckSquare, Search, FileText, ArrowRight, Clock, MapPin, Briefcase } from 'lucide-react'
|
||||||
|
|
||||||
|
const mockFamilias = {
|
||||||
|
1: { nome: 'Família Silva', falecido: { nome: 'José Carlos Silva', cpf: '***.***.***-45', nascimento: '1955-03-12', obito: '2026-01-10', emprego: true, veiculo: true, imovel: true, previdencia: false }, membros: [{ nome: 'Ana Silva', parentesco: 'Cônjuge', papel: 'Inventariante' }, { nome: 'Pedro Silva', parentesco: 'Filho', papel: 'Herdeiro' }, { nome: 'Carla Silva', parentesco: 'Filha', papel: 'Herdeira' }], progresso: 35 },
|
||||||
|
2: { nome: 'Família Oliveira', falecido: { nome: 'Maria Oliveira', cpf: '***.***.***-78', nascimento: '1948-07-22', obito: '2026-01-18', emprego: false, veiculo: false, imovel: true, previdencia: true }, membros: [{ nome: 'Carlos Oliveira', parentesco: 'Cônjuge', papel: 'Inventariante' }, { nome: 'Lucia Oliveira', parentesco: 'Filha', papel: 'Herdeira' }], progresso: 60 },
|
||||||
|
3: { nome: 'Família Santos', falecido: { nome: 'Antônio Santos', cpf: '***.***.***-12', nascimento: '1960-11-05', obito: '2026-01-28', emprego: true, veiculo: true, imovel: true, previdencia: true }, membros: [{ nome: 'Rosa Santos', parentesco: 'Cônjuge', papel: 'Inventariante' }, { nome: 'Marcos Santos', parentesco: 'Filho', papel: 'Herdeiro' }, { nome: 'Julia Santos', parentesco: 'Filha', papel: 'Herdeira' }, { nome: 'Dr. Roberto Lima', parentesco: '-', papel: 'Advogado' }], progresso: 15 }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FamiliaPage() {
|
||||||
|
const router = useRouter()
|
||||||
|
const params = useParams()
|
||||||
|
const id = params.id
|
||||||
|
const familia = mockFamilias[id] || mockFamilias[1]
|
||||||
|
const f = familia.falecido
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ label: 'Checklist', icon: <CheckSquare size={18} />, href: `/familia/${id}/checklist` },
|
||||||
|
{ label: 'Benefícios', icon: <Search size={18} />, href: `/familia/${id}/beneficios` },
|
||||||
|
{ label: 'Documentos', icon: <FileText size={18} />, href: `/familia/${id}/documentos` },
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen bg-[#0a0a0f]">
|
||||||
|
<Sidebar />
|
||||||
|
<main className="ml-64 flex-1 p-8">
|
||||||
|
<button onClick={() => router.push('/dashboard')} className="text-gray-400 hover:text-white text-sm mb-6 flex items-center gap-1">← Voltar ao Dashboard</button>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4 mb-8">
|
||||||
|
<div className="w-16 h-16 bg-purple-500/20 rounded-2xl flex items-center justify-center text-3xl">🚣</div>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold text-white">{familia.nome}</h1>
|
||||||
|
<p className="text-gray-400">Progresso geral: <span className="text-purple-400 font-semibold">{familia.progresso}%</span></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full bg-white/5 rounded-full h-3 mb-8">
|
||||||
|
<div className="bg-gradient-to-r from-purple-600 to-purple-400 h-3 rounded-full transition-all" style={{ width: `${familia.progresso}%` }} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
||||||
|
<div className="glass p-6">
|
||||||
|
<h2 className="text-lg font-semibold text-white mb-4 flex items-center gap-2"><User size={20} className="text-purple-400" /> Dados do Falecido</h2>
|
||||||
|
<div className="space-y-3 text-sm">
|
||||||
|
<div className="flex justify-between"><span className="text-gray-400">Nome</span><span className="text-white">{f.nome}</span></div>
|
||||||
|
<div className="flex justify-between"><span className="text-gray-400">CPF</span><span className="text-white font-mono">{f.cpf}</span></div>
|
||||||
|
<div className="flex justify-between"><span className="text-gray-400">Nascimento</span><span className="text-white">{new Date(f.nascimento).toLocaleDateString('pt-BR')}</span></div>
|
||||||
|
<div className="flex justify-between"><span className="text-gray-400">Óbito</span><span className="text-white">{new Date(f.obito).toLocaleDateString('pt-BR')}</span></div>
|
||||||
|
<div className="border-t border-white/5 pt-3 mt-3">
|
||||||
|
<p className="text-gray-400 mb-2">Situação:</p>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{f.emprego && <span className="px-3 py-1 bg-blue-500/10 text-blue-400 rounded-full text-xs flex items-center gap-1"><Briefcase size={12} /> Empregado</span>}
|
||||||
|
{f.veiculo && <span className="px-3 py-1 bg-green-500/10 text-green-400 rounded-full text-xs">🚗 Veículo</span>}
|
||||||
|
{f.imovel && <span className="px-3 py-1 bg-yellow-500/10 text-yellow-400 rounded-full text-xs">🏠 Imóvel</span>}
|
||||||
|
{f.previdencia && <span className="px-3 py-1 bg-purple-500/10 text-purple-400 rounded-full text-xs">💰 Previdência</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="glass p-6">
|
||||||
|
<h2 className="text-lg font-semibold text-white mb-4 flex items-center gap-2"><User size={20} className="text-purple-400" /> Membros da Família</h2>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{familia.membros.map((m, i) => (
|
||||||
|
<div key={i} className="flex items-center justify-between p-3 bg-white/5 rounded-xl">
|
||||||
|
<div>
|
||||||
|
<p className="text-white font-medium">{m.nome}</p>
|
||||||
|
<p className="text-gray-500 text-xs">{m.parentesco}</p>
|
||||||
|
</div>
|
||||||
|
<span className={`text-xs px-3 py-1 rounded-full ${m.papel === 'Inventariante' ? 'bg-purple-500/20 text-purple-400' : m.papel === 'Advogado' ? 'bg-blue-500/20 text-blue-400' : 'bg-white/10 text-gray-300'}`}>{m.papel}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<button className="mt-4 w-full py-2 border border-dashed border-white/10 rounded-xl text-gray-500 hover:text-white hover:border-purple-500/30 transition text-sm">+ Convidar Membro</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 className="text-xl font-bold text-white mb-4">Módulos</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
{tabs.map((t, i) => (
|
||||||
|
<div key={i} onClick={() => router.push(t.href)} className="glass p-6 cursor-pointer hover:bg-white/10 transition group flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="text-purple-400">{t.icon}</span>
|
||||||
|
<span className="text-white font-medium">{t.label}</span>
|
||||||
|
</div>
|
||||||
|
<ArrowRight className="text-gray-600 group-hover:text-purple-400 transition" size={18} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
BIN
frontend/src/app/favicon.ico
Normal file
BIN
frontend/src/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
BIN
frontend/src/app/fonts/GeistMonoVF.woff
Normal file
BIN
frontend/src/app/fonts/GeistMonoVF.woff
Normal file
Binary file not shown.
BIN
frontend/src/app/fonts/GeistVF.woff
Normal file
BIN
frontend/src/app/fonts/GeistVF.woff
Normal file
Binary file not shown.
239
frontend/src/app/globals.css
Normal file
239
frontend/src/app/globals.css
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Cinzel+Decorative:wght@400;700;900&family=Cinzel:wght@400;500;600;700;800;900&family=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;0,700;1,300;1,400&family=Inter:wght@300;400;500;600;700&display=swap');
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--void: #050508;
|
||||||
|
--abyss: #08080f;
|
||||||
|
--obsidian: #0c0c14;
|
||||||
|
--onyx: #12121c;
|
||||||
|
--gold: #b8943f;
|
||||||
|
--gold-bright: #d4ad4a;
|
||||||
|
--gold-glow: #e8c55a;
|
||||||
|
--gold-dim: #7a6228;
|
||||||
|
--ember: #c44b1a;
|
||||||
|
--ember-glow: #ff6b2b;
|
||||||
|
--soul: #4a7aff;
|
||||||
|
--soul-dim: #2a4a9a;
|
||||||
|
--bone: #c8c0b0;
|
||||||
|
--ash: #6a645a;
|
||||||
|
--smoke: #3a3630;
|
||||||
|
--marble: #e0dcd4;
|
||||||
|
}
|
||||||
|
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: var(--void);
|
||||||
|
color: var(--bone);
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-myth { font-family: 'Cinzel', serif; }
|
||||||
|
.font-myth-decorative { font-family: 'Cinzel Decorative', serif; }
|
||||||
|
.font-elegant { font-family: 'Cormorant Garamond', serif; }
|
||||||
|
|
||||||
|
/* === GOLD TEXT === */
|
||||||
|
.gold-text {
|
||||||
|
background: linear-gradient(135deg, var(--gold-dim) 0%, var(--gold) 30%, var(--gold-glow) 50%, var(--gold) 70%, var(--gold-dim) 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === UNDERWORLD BACKGROUNDS === */
|
||||||
|
.bg-underworld {
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse 120% 60% at 50% 110%, rgba(74, 40, 10, 0.08) 0%, transparent 70%),
|
||||||
|
radial-gradient(ellipse 80% 40% at 20% 80%, rgba(196, 75, 26, 0.04) 0%, transparent 60%),
|
||||||
|
radial-gradient(ellipse 80% 40% at 80% 80%, rgba(196, 75, 26, 0.04) 0%, transparent 60%),
|
||||||
|
linear-gradient(180deg, var(--void) 0%, var(--abyss) 40%, #0a0a12 70%, #0d0910 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-styx {
|
||||||
|
background:
|
||||||
|
radial-gradient(ellipse 100% 30% at 50% 100%, rgba(42, 74, 154, 0.1) 0%, transparent 70%),
|
||||||
|
linear-gradient(180deg, transparent 60%, rgba(42, 74, 154, 0.03) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === EMBER / FIRE GLOW === */
|
||||||
|
@keyframes emberPulse {
|
||||||
|
0%, 100% { opacity: 0.4; filter: blur(30px); }
|
||||||
|
50% { opacity: 0.7; filter: blur(40px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes emberFloat {
|
||||||
|
0% { transform: translateY(0) scale(1); opacity: 0.6; }
|
||||||
|
50% { transform: translateY(-20px) scale(1.1); opacity: 0.9; }
|
||||||
|
100% { transform: translateY(-40px) scale(0.8); opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.ember-particle {
|
||||||
|
position: absolute;
|
||||||
|
width: 3px;
|
||||||
|
height: 3px;
|
||||||
|
background: var(--ember-glow);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: emberFloat 4s ease-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === GREEK ORNAMENTS === */
|
||||||
|
.ornament-line {
|
||||||
|
height: 1px;
|
||||||
|
background: linear-gradient(90deg, transparent, var(--gold-dim), var(--gold), var(--gold-dim), transparent);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ornament-line::after {
|
||||||
|
content: '◆';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
color: var(--gold);
|
||||||
|
font-size: 8px;
|
||||||
|
background: var(--void);
|
||||||
|
padding: 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ornament-line-simple {
|
||||||
|
height: 1px;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(184, 148, 63, 0.3), transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === CARDS === */
|
||||||
|
.card-hades {
|
||||||
|
background: linear-gradient(180deg, rgba(18, 18, 28, 0.95), rgba(8, 8, 15, 0.98));
|
||||||
|
border: 1px solid rgba(184, 148, 63, 0.08);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-hades::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 1px;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(184, 148, 63, 0.3), transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-hades:hover {
|
||||||
|
border-color: rgba(184, 148, 63, 0.2);
|
||||||
|
box-shadow: 0 0 40px rgba(184, 148, 63, 0.05), inset 0 1px 0 rgba(184, 148, 63, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-shrine {
|
||||||
|
background: rgba(12, 12, 20, 0.9);
|
||||||
|
border: 1px solid rgba(184, 148, 63, 0.06);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === BUTTONS === */
|
||||||
|
.btn-gold {
|
||||||
|
background: linear-gradient(135deg, var(--gold-dim), var(--gold), var(--gold-bright));
|
||||||
|
color: #0a0a0f;
|
||||||
|
font-family: 'Cinzel', serif;
|
||||||
|
letter-spacing: 0.15em;
|
||||||
|
font-weight: 700;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-gold::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
|
||||||
|
transition: left 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-gold:hover::before { left: 100%; }
|
||||||
|
.btn-gold:hover { box-shadow: 0 0 30px rgba(184, 148, 63, 0.3); }
|
||||||
|
|
||||||
|
.btn-outline {
|
||||||
|
border: 1px solid rgba(184, 148, 63, 0.25);
|
||||||
|
color: var(--gold);
|
||||||
|
font-family: 'Cinzel', serif;
|
||||||
|
letter-spacing: 0.15em;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline:hover {
|
||||||
|
background: rgba(184, 148, 63, 0.08);
|
||||||
|
border-color: rgba(184, 148, 63, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === ANIMATIONS === */
|
||||||
|
@keyframes boatFloat {
|
||||||
|
0%, 100% { transform: translateY(0px) rotate(0deg); }
|
||||||
|
33% { transform: translateY(-6px) rotate(0.8deg); }
|
||||||
|
66% { transform: translateY(3px) rotate(-0.5deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fogDrift {
|
||||||
|
0% { transform: translateX(-10%) scaleX(1); opacity: 0.3; }
|
||||||
|
50% { transform: translateX(5%) scaleX(1.05); opacity: 0.5; }
|
||||||
|
100% { transform: translateX(-10%) scaleX(1); opacity: 0.3; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes soulGlow {
|
||||||
|
0%, 100% { opacity: 0.2; filter: blur(20px); }
|
||||||
|
50% { opacity: 0.4; filter: blur(25px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
from { opacity: 0; transform: translateY(30px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes twinkle {
|
||||||
|
0%, 100% { opacity: 0.15; }
|
||||||
|
50% { opacity: 0.8; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.boat-float { animation: boatFloat 6s ease-in-out infinite; }
|
||||||
|
.fade-in { animation: fadeInUp 1s ease-out forwards; }
|
||||||
|
.fade-in-delay { animation: fadeInUp 1s ease-out 0.3s forwards; opacity: 0; }
|
||||||
|
.fade-in-delay-2 { animation: fadeInUp 1s ease-out 0.6s forwards; opacity: 0; }
|
||||||
|
|
||||||
|
/* === INPUTS === */
|
||||||
|
.input-hades {
|
||||||
|
background: rgba(5, 5, 8, 0.8);
|
||||||
|
border: 1px solid rgba(184, 148, 63, 0.1);
|
||||||
|
color: var(--bone);
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-hades:focus {
|
||||||
|
border-color: rgba(184, 148, 63, 0.35);
|
||||||
|
box-shadow: 0 0 20px rgba(184, 148, 63, 0.05);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-hades::placeholder { color: var(--smoke); }
|
||||||
|
|
||||||
|
/* === PROGRESS === */
|
||||||
|
.progress-styx {
|
||||||
|
background: rgba(5, 5, 8, 0.6);
|
||||||
|
border: 1px solid rgba(184, 148, 63, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-styx-fill {
|
||||||
|
background: linear-gradient(90deg, var(--gold-dim), var(--gold), var(--gold-bright));
|
||||||
|
box-shadow: 0 0 10px rgba(184, 148, 63, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === SCROLLBAR === */
|
||||||
|
::-webkit-scrollbar { width: 4px; }
|
||||||
|
::-webkit-scrollbar-track { background: var(--void); }
|
||||||
|
::-webkit-scrollbar-thumb { background: rgba(184, 148, 63, 0.2); }
|
||||||
|
::-webkit-scrollbar-thumb:hover { background: rgba(184, 148, 63, 0.4); }
|
||||||
17
frontend/src/app/layout.js
Normal file
17
frontend/src/app/layout.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { Inter } from 'next/font/google'
|
||||||
|
import './globals.css'
|
||||||
|
|
||||||
|
const inter = Inter({ subsets: ['latin'] })
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: 'CARONTE - Guia Pós-Óbito',
|
||||||
|
description: 'O barqueiro que guia famílias pelo rio burocrático pós-óbito no Brasil',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RootLayout({ children }) {
|
||||||
|
return (
|
||||||
|
<html lang="pt-BR">
|
||||||
|
<body className={inter.className}>{children}</body>
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
}
|
||||||
78
frontend/src/app/login/page.js
Normal file
78
frontend/src/app/login/page.js
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
'use client'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { Eye, EyeOff } from 'lucide-react'
|
||||||
|
|
||||||
|
export default function LoginPage() {
|
||||||
|
const router = useRouter()
|
||||||
|
const [email, setEmail] = useState('')
|
||||||
|
const [senha, setSenha] = useState('')
|
||||||
|
const [show, setShow] = useState(false)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
const login = async (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setLoading(true)
|
||||||
|
setTimeout(() => {
|
||||||
|
localStorage.setItem('token', 'demo-token')
|
||||||
|
router.push('/dashboard')
|
||||||
|
}, 1200)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-underworld flex items-center justify-center px-4 relative">
|
||||||
|
{/* Stars */}
|
||||||
|
<div className="fixed inset-0 pointer-events-none">
|
||||||
|
{[...Array(40)].map((_, i) => (
|
||||||
|
<div key={i} className="absolute rounded-full bg-white"
|
||||||
|
style={{ width: `${1 + Math.random()}px`, height: `${1 + Math.random()}px`,
|
||||||
|
top: `${Math.random() * 100}%`, left: `${Math.random() * 100}%`, opacity: 0.15,
|
||||||
|
animation: `twinkle ${4 + Math.random() * 6}s ease-in-out ${Math.random() * 5}s infinite` }} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full max-w-sm relative z-10 fade-in">
|
||||||
|
<div className="text-center mb-12">
|
||||||
|
<span className="text-6xl boat-float block mb-6 filter drop-shadow-xl">🚣</span>
|
||||||
|
<h1 className="font-myth-decorative text-2xl gold-text tracking-[0.2em]">CARONTE</h1>
|
||||||
|
<div className="ornament-line-simple max-w-[60px] mx-auto my-4" />
|
||||||
|
<p className="font-elegant italic text-[var(--ash)] text-lg">"A travessia começa aqui."</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={login} className="card-hades p-8">
|
||||||
|
<h2 className="font-myth text-sm text-center text-[var(--gold-dim)] mb-8 tracking-[0.3em] uppercase">Entrar no Submundo</h2>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<label className="text-[10px] tracking-[0.2em] text-[var(--gold-dim)] uppercase font-myth block mb-2">Email</label>
|
||||||
|
<input type="email" value={email} onChange={e => setEmail(e.target.value)}
|
||||||
|
className="w-full px-4 py-3 input-hades text-sm" placeholder="seu@email.com" required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-[10px] tracking-[0.2em] text-[var(--gold-dim)] uppercase font-myth block mb-2">Senha</label>
|
||||||
|
<div className="relative">
|
||||||
|
<input type={show ? 'text' : 'password'} value={senha} onChange={e => setSenha(e.target.value)}
|
||||||
|
className="w-full px-4 py-3 input-hades text-sm pr-12" placeholder="••••••••" required />
|
||||||
|
<button type="button" onClick={() => setShow(!show)} className="absolute right-3 top-1/2 -translate-y-1/2 text-[var(--smoke)] hover:text-[var(--ash)] transition">
|
||||||
|
{show ? <EyeOff size={14} /> : <Eye size={14} />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" disabled={loading} className="w-full py-3.5 btn-gold text-xs disabled:opacity-50">
|
||||||
|
{loading ? '⏳ Cruzando o Styx...' : 'ENTRAR'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="ornament-line-simple my-6" />
|
||||||
|
|
||||||
|
<p className="text-center text-[var(--smoke)] text-sm">
|
||||||
|
Primeira travessia? <Link href="/registro" className="text-[var(--gold)] hover:text-[var(--gold-bright)] transition">Criar conta</Link>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
199
frontend/src/app/page.js
Normal file
199
frontend/src/app/page.js
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
'use client'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { ArrowRight, Shield } from 'lucide-react'
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-underworld relative">
|
||||||
|
|
||||||
|
{/* Fog layers */}
|
||||||
|
<div className="fixed inset-0 pointer-events-none z-0">
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 h-[400px] bg-gradient-to-t from-[rgba(196,75,26,0.03)] to-transparent" style={{ animation: 'fogDrift 20s ease-in-out infinite' }} />
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 h-[300px] bg-gradient-to-t from-[rgba(42,74,154,0.04)] to-transparent" style={{ animation: 'fogDrift 15s ease-in-out infinite reverse' }} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stars */}
|
||||||
|
<div className="fixed inset-0 pointer-events-none z-0">
|
||||||
|
{[...Array(80)].map((_, i) => (
|
||||||
|
<div key={i} className="absolute rounded-full bg-white"
|
||||||
|
style={{
|
||||||
|
width: `${1 + Math.random() * 1.5}px`, height: `${1 + Math.random() * 1.5}px`,
|
||||||
|
top: `${Math.random() * 50}%`, left: `${Math.random() * 100}%`,
|
||||||
|
opacity: 0.15 + Math.random() * 0.3,
|
||||||
|
animation: `twinkle ${4 + Math.random() * 6}s ease-in-out ${Math.random() * 5}s infinite`
|
||||||
|
}} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Soul orbs */}
|
||||||
|
<div className="fixed inset-0 pointer-events-none z-0">
|
||||||
|
{[...Array(5)].map((_, i) => (
|
||||||
|
<div key={i} className="absolute rounded-full"
|
||||||
|
style={{
|
||||||
|
width: `${60 + Math.random() * 100}px`, height: `${60 + Math.random() * 100}px`,
|
||||||
|
top: `${30 + Math.random() * 50}%`, left: `${Math.random() * 100}%`,
|
||||||
|
background: `radial-gradient(circle, ${i % 2 === 0 ? 'rgba(196,75,26,0.06)' : 'rgba(74,122,255,0.04)'}, transparent 70%)`,
|
||||||
|
animation: `soulGlow ${6 + Math.random() * 4}s ease-in-out ${Math.random() * 3}s infinite`
|
||||||
|
}} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Nav */}
|
||||||
|
<nav className="relative z-20 flex items-center justify-between px-10 py-7">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<span className="text-4xl boat-float filter drop-shadow-lg">🚣</span>
|
||||||
|
<div>
|
||||||
|
<span className="font-myth-decorative text-xl gold-text tracking-[0.2em]">CARONTE</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<Link href="/login" className="btn-outline px-6 py-2.5 text-xs tracking-[0.2em]">ENTRAR</Link>
|
||||||
|
<Link href="/registro" className="btn-gold px-6 py-2.5 text-xs">COMEÇAR</Link>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Hero */}
|
||||||
|
<section className="relative z-10 max-w-4xl mx-auto px-8 pt-20 pb-12 text-center">
|
||||||
|
<div className="fade-in">
|
||||||
|
<div className="ornament-line max-w-[200px] mx-auto mb-16" />
|
||||||
|
|
||||||
|
<p className="font-myth text-[var(--gold-dim)] text-[11px] tracking-[0.5em] uppercase mb-8">⚱️ Guia para famílias em luto</p>
|
||||||
|
|
||||||
|
<h1 className="font-myth-decorative text-5xl md:text-7xl font-bold leading-[1.15] mb-8 tracking-wide">
|
||||||
|
<span className="gold-text">Perdeu</span>
|
||||||
|
<span className="text-[var(--marble)]"> alguém</span>
|
||||||
|
<span className="gold-text">?</span><br />
|
||||||
|
<span className="text-[var(--bone)] text-4xl md:text-5xl font-myth font-normal tracking-wider">Nós guiamos a travessia.</span>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="fade-in-delay">
|
||||||
|
<p className="font-elegant text-2xl md:text-3xl text-[var(--ash)] max-w-2xl mx-auto mb-6 italic leading-relaxed">
|
||||||
|
"Assim como o barqueiro guiava as almas<br />pelo Rio Styx até o descanso eterno —<br />nós guiamos sua família pela burocracia."
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="fade-in-delay-2">
|
||||||
|
<p className="text-[var(--smoke)] max-w-lg mx-auto mb-14 text-sm leading-relaxed">
|
||||||
|
Checklist inteligente · Scanner de benefícios esquecidos · Gerador de documentos.<br />
|
||||||
|
Tudo que precisa ser feito após a perda — em um só lugar.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Link href="/registro" className="group inline-flex items-center gap-3 btn-gold px-10 py-4 text-sm">
|
||||||
|
INICIAR A TRAVESSIA <ArrowRight className="group-hover:translate-x-1 transition" size={18} />
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<p className="text-[var(--smoke)] text-xs mt-4 tracking-wider">Gratuito · Sem cartão de crédito</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Divider with ember */}
|
||||||
|
<div className="relative z-10 max-w-4xl mx-auto px-8 py-16">
|
||||||
|
<div className="ornament-line" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Numbers */}
|
||||||
|
<section className="relative z-10 max-w-4xl mx-auto px-8 pb-20">
|
||||||
|
<div className="grid grid-cols-3 gap-6">
|
||||||
|
{[
|
||||||
|
{ num: '1.4M', label: 'óbitos por ano', sub: 'no Brasil', icon: '💀' },
|
||||||
|
{ num: '500h', label: 'de burocracia', sub: 'por família enlutada', icon: '⏳' },
|
||||||
|
{ num: 'R$ Bi', label: 'em benefícios', sub: 'esquecidos todo ano', icon: '🔱' },
|
||||||
|
].map((s, i) => (
|
||||||
|
<div key={i} className="card-hades p-8 text-center group hover:scale-[1.02] transition-all duration-500">
|
||||||
|
<span className="text-3xl block mb-4 group-hover:scale-110 transition-transform">{s.icon}</span>
|
||||||
|
<p className="font-myth-decorative text-3xl md:text-4xl font-bold gold-text mb-2">{s.num}</p>
|
||||||
|
<p className="text-[var(--bone)] text-sm font-myth tracking-wider">{s.label}</p>
|
||||||
|
<p className="text-[var(--smoke)] text-xs mt-1">{s.sub}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Features - The Three Pillars */}
|
||||||
|
<section className="relative z-10 max-w-5xl mx-auto px-8 pb-24">
|
||||||
|
<div className="text-center mb-16">
|
||||||
|
<p className="font-myth text-[var(--gold-dim)] text-[11px] tracking-[0.5em] uppercase mb-4">🏛️ Os Três Pilares do Submundo</p>
|
||||||
|
<h2 className="font-myth text-3xl md:text-4xl font-bold text-[var(--marble)] tracking-wide">As armas de Caronte</h2>
|
||||||
|
<div className="ornament-line-simple max-w-[300px] mx-auto mt-6" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
icon: '🗺️', title: 'O Mapa do', titleGold: 'Submundo',
|
||||||
|
desc: 'Checklist completo de cada passo após o óbito. Certidões, INSS, bancos, inventário — com prazos, dependências e prioridades. Como um mapa que revela cada caminho no labirinto burocrático.',
|
||||||
|
detail: '15 etapas · 4 fases · Prazos automáticos'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: '🔮', title: 'O Oráculo dos', titleGold: 'Tesouros',
|
||||||
|
desc: 'Nossa IA vasculha benefícios esquecidos: FGTS, PIS/PASEP, seguros de vida, pensões, restituições. Milhares de reais que famílias nunca souberam que tinham direito.',
|
||||||
|
detail: '7 tipos de benefício · Média: R$15.000 recuperados'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: '📜', title: 'Os Pergaminhos', titleGold: 'Sagrados',
|
||||||
|
desc: 'Procurações, requerimentos e petições gerados automaticamente. Documentos jurídicos prontos para uso — sem precisar de advogado para as tarefas mais simples.',
|
||||||
|
detail: '5 modelos · PDF pronto para imprimir'
|
||||||
|
},
|
||||||
|
].map((f, i) => (
|
||||||
|
<div key={i} className="card-hades p-8 group hover:scale-[1.02] transition-all duration-500 flex flex-col">
|
||||||
|
<span className="text-4xl block mb-6 group-hover:scale-110 transition-transform">{f.icon}</span>
|
||||||
|
<h3 className="font-myth text-lg text-[var(--bone)] mb-1 tracking-wider">
|
||||||
|
{f.title} <span className="gold-text">{f.titleGold}</span>
|
||||||
|
</h3>
|
||||||
|
<p className="text-[var(--ash)] text-sm leading-relaxed mt-3 flex-1">{f.desc}</p>
|
||||||
|
<div className="ornament-line-simple mt-6 mb-4" />
|
||||||
|
<p className="text-[var(--gold-dim)] text-[10px] tracking-[0.2em] font-myth uppercase">{f.detail}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Testimonial / Quote */}
|
||||||
|
<section className="relative z-10 py-20">
|
||||||
|
<div className="max-w-3xl mx-auto px-8 text-center">
|
||||||
|
<div className="card-hades p-12 relative">
|
||||||
|
<span className="absolute -top-6 left-1/2 -translate-x-1/2 text-5xl">🔥</span>
|
||||||
|
<p className="font-elegant text-2xl md:text-3xl text-[var(--bone)] italic leading-relaxed mt-4">
|
||||||
|
"Quando meu pai faleceu, eu não sabia por onde começar. Foram meses de agonia em filas, cartórios e telefones. Se o Caronte existisse naquele momento, teria mudado tudo."
|
||||||
|
</p>
|
||||||
|
<div className="ornament-line-simple max-w-[100px] mx-auto my-6" />
|
||||||
|
<p className="text-[var(--gold-dim)] font-myth text-xs tracking-[0.2em] uppercase">— A dor que nos inspirou a criar</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Final CTA */}
|
||||||
|
<section className="relative z-10 py-24">
|
||||||
|
<div className="max-w-2xl mx-auto text-center px-8">
|
||||||
|
<span className="text-7xl boat-float block mb-8 filter drop-shadow-2xl">🚣</span>
|
||||||
|
<h2 className="font-myth text-3xl md:text-4xl font-bold text-[var(--marble)] mb-4 tracking-wide">A travessia não precisa<br />ser solitária</h2>
|
||||||
|
<p className="font-elegant text-xl text-[var(--ash)] italic mb-10">"Não deixe a burocracia transformar o luto em pesadelo."</p>
|
||||||
|
<Link href="/registro" className="inline-flex items-center gap-3 btn-gold px-10 py-4 text-sm">
|
||||||
|
COMEÇAR AGORA — GRATUITO
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<footer className="relative z-10 border-t border-[var(--gold)]/5 py-8 px-10">
|
||||||
|
<div className="max-w-5xl mx-auto flex justify-between items-center">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="text-lg">🚣</span>
|
||||||
|
<span className="font-myth text-xs gold-text tracking-[0.2em]">CARONTE</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-[var(--smoke)] text-[11px] tracking-wider">
|
||||||
|
© 2026 — Nenhum benefício esquecido. Nenhum prazo perdido.
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Shield size={12} className="text-[var(--gold-dim)]" />
|
||||||
|
<span className="text-[var(--smoke)] text-[11px]">LGPD</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
{/* River Styx glow at bottom */}
|
||||||
|
<div className="fixed bottom-0 left-0 right-0 h-[2px] bg-gradient-to-r from-transparent via-[var(--gold-dim)] to-transparent opacity-20 z-30" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
89
frontend/src/app/registro/page.js
Normal file
89
frontend/src/app/registro/page.js
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
'use client'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { UserPlus, Eye, EyeOff } from 'lucide-react'
|
||||||
|
|
||||||
|
export default function RegistroPage() {
|
||||||
|
const router = useRouter()
|
||||||
|
const [nome, setNome] = useState('')
|
||||||
|
const [email, setEmail] = useState('')
|
||||||
|
const [senha, setSenha] = useState('')
|
||||||
|
const [show, setShow] = useState(false)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
const registro = async (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setLoading(true)
|
||||||
|
setTimeout(() => {
|
||||||
|
localStorage.setItem('token', 'demo-token')
|
||||||
|
router.push('/dashboard')
|
||||||
|
}, 1500)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen styx-bg flex items-center justify-center px-4 relative">
|
||||||
|
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||||
|
{[...Array(30)].map((_, i) => (
|
||||||
|
<div key={i} className="absolute w-[2px] h-[2px] bg-white/20 rounded-full"
|
||||||
|
style={{ top: `${Math.random() * 100}%`, left: `${Math.random() * 100}%`, animation: `twinkle ${3 + Math.random() * 4}s ease-in-out infinite` }} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full max-w-md relative z-10">
|
||||||
|
<div className="text-center mb-10">
|
||||||
|
<span className="text-5xl boat-float block mb-4">🚣</span>
|
||||||
|
<h1 className="font-myth text-3xl font-bold gold-text tracking-wider">CARONTE</h1>
|
||||||
|
<p className="font-elegant italic text-[var(--text-muted)] mt-2">"Toda travessia começa com o primeiro passo."</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="greek-line mb-8 max-w-[100px] mx-auto" />
|
||||||
|
|
||||||
|
<form onSubmit={registro} className="card-temple p-8 glow-gold">
|
||||||
|
<h2 className="font-myth text-xl text-center text-[var(--marble)] mb-6 tracking-wider">CRIAR CONTA</h2>
|
||||||
|
|
||||||
|
<div className="space-y-5">
|
||||||
|
<div>
|
||||||
|
<label className="text-[10px] tracking-[0.2em] text-[var(--gold)] uppercase font-myth block mb-2">Nome Completo</label>
|
||||||
|
<input type="text" value={nome} onChange={e => setNome(e.target.value)}
|
||||||
|
className="w-full px-4 py-3 bg-black/30 border border-[var(--gold)]/20 text-[var(--text)] focus:border-[var(--gold)]/50 focus:outline-none transition text-sm"
|
||||||
|
placeholder="Seu nome" required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-[10px] tracking-[0.2em] text-[var(--gold)] uppercase font-myth block mb-2">Email</label>
|
||||||
|
<input type="email" value={email} onChange={e => setEmail(e.target.value)}
|
||||||
|
className="w-full px-4 py-3 bg-black/30 border border-[var(--gold)]/20 text-[var(--text)] focus:border-[var(--gold)]/50 focus:outline-none transition text-sm"
|
||||||
|
placeholder="seu@email.com" required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-[10px] tracking-[0.2em] text-[var(--gold)] uppercase font-myth block mb-2">Senha</label>
|
||||||
|
<div className="relative">
|
||||||
|
<input type={show ? 'text' : 'password'} value={senha} onChange={e => setSenha(e.target.value)}
|
||||||
|
className="w-full px-4 py-3 bg-black/30 border border-[var(--gold)]/20 text-[var(--text)] focus:border-[var(--gold)]/50 focus:outline-none transition text-sm pr-12"
|
||||||
|
placeholder="••••••••" required />
|
||||||
|
<button type="button" onClick={() => setShow(!show)} className="absolute right-3 top-1/2 -translate-y-1/2 text-[var(--text-muted)]">
|
||||||
|
{show ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" disabled={loading}
|
||||||
|
className="w-full py-3 bg-gradient-to-r from-[var(--gold-dark)] to-[var(--gold)] text-black font-bold font-myth tracking-wider hover:from-[var(--gold)] hover:to-[var(--gold-light)] transition disabled:opacity-50 flex items-center justify-center gap-2">
|
||||||
|
{loading ? '⏳ Preparando a travessia...' : <><UserPlus size={16} /> INICIAR TRAVESSIA</>}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="greek-line mt-6 mb-4" />
|
||||||
|
|
||||||
|
<p className="text-center text-[var(--text-muted)] text-sm">
|
||||||
|
Já tem conta? <Link href="/login" className="text-[var(--gold)] hover:text-[var(--gold-light)] font-medium">Entrar</Link>
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 h-32 bg-gradient-to-t from-[var(--water)]/20 to-transparent pointer-events-none" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
59
frontend/src/components/Sidebar.js
Normal file
59
frontend/src/components/Sidebar.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
'use client'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { usePathname, useRouter } from 'next/navigation'
|
||||||
|
import { LayoutDashboard, Users, LogOut } from 'lucide-react'
|
||||||
|
|
||||||
|
export default function Sidebar() {
|
||||||
|
const pathname = usePathname()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const logout = () => { localStorage.removeItem('token'); router.push('/login') }
|
||||||
|
|
||||||
|
const links = [
|
||||||
|
{ href: '/dashboard', label: 'Ágora', icon: <LayoutDashboard size={16} />, emoji: '🏛️' },
|
||||||
|
{ href: '/dashboard', label: 'Famílias', icon: <Users size={16} />, emoji: '⚱️' },
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<aside className="fixed left-0 top-0 h-screen w-60 bg-[var(--void)]/98 backdrop-blur-xl flex flex-col z-40 border-r border-[var(--gold)]/5">
|
||||||
|
{/* Logo */}
|
||||||
|
<div className="p-6 pb-4">
|
||||||
|
<Link href="/dashboard" className="flex items-center gap-3">
|
||||||
|
<span className="text-2xl boat-float">🚣</span>
|
||||||
|
<div>
|
||||||
|
<div className="font-myth-decorative text-sm gold-text tracking-[0.15em]">CARONTE</div>
|
||||||
|
<div className="text-[9px] tracking-[0.15em] text-[var(--smoke)] uppercase">Guia do Submundo</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="ornament-line-simple mx-4" />
|
||||||
|
|
||||||
|
{/* Nav */}
|
||||||
|
<nav className="flex-1 px-3 py-4 space-y-0.5">
|
||||||
|
<p className="text-[9px] tracking-[0.3em] text-[var(--smoke)] uppercase font-myth px-3 mb-3">Navegação</p>
|
||||||
|
{links.map((l, i) => {
|
||||||
|
const active = pathname === l.href
|
||||||
|
return (
|
||||||
|
<Link key={i} href={l.href}
|
||||||
|
className={`flex items-center gap-3 px-3 py-2.5 text-sm transition-all ${active ? 'text-[var(--gold)] bg-[var(--gold)]/5 border-l-2 border-[var(--gold)]' : 'text-[var(--ash)] hover:text-[var(--bone)] hover:bg-white/[0.02]'}`}>
|
||||||
|
<span className="text-base">{l.emoji}</span>
|
||||||
|
<span className="font-myth text-xs tracking-wider">{l.label}</span>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="card-shrine p-3 mb-3 text-center">
|
||||||
|
<p className="font-elegant italic text-[11px] text-[var(--smoke)] leading-relaxed">"A travessia é mais leve<br/>quando não se está só."</p>
|
||||||
|
</div>
|
||||||
|
<div className="ornament-line-simple mb-3" />
|
||||||
|
<button onClick={logout} className="flex items-center gap-2 px-3 py-2 text-[var(--smoke)] hover:text-red-400/80 transition w-full text-xs">
|
||||||
|
<LogOut size={13} />
|
||||||
|
<span>Encerrar</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
)
|
||||||
|
}
|
||||||
49
frontend/src/lib/api.js
Normal file
49
frontend/src/lib/api.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8070';
|
||||||
|
|
||||||
|
async function request(path, options = {}) {
|
||||||
|
const token = typeof window !== 'undefined' ? localStorage.getItem('token') : null;
|
||||||
|
const headers = { ...options.headers };
|
||||||
|
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||||
|
if (options.body && typeof options.body === 'object' && !(options.body instanceof FormData)) {
|
||||||
|
headers['Content-Type'] = 'application/json';
|
||||||
|
options.body = JSON.stringify(options.body);
|
||||||
|
}
|
||||||
|
const res = await fetch(`${API_URL}${path}`, { ...options, headers });
|
||||||
|
if (res.status === 401) {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
window.location.href = '/login';
|
||||||
|
}
|
||||||
|
throw new Error('Não autorizado');
|
||||||
|
}
|
||||||
|
if (!res.ok) {
|
||||||
|
const err = await res.json().catch(() => ({}));
|
||||||
|
throw new Error(err.detail || 'Erro na requisição');
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const api = {
|
||||||
|
login: (email, senha) => {
|
||||||
|
const form = new URLSearchParams();
|
||||||
|
form.append('username', email);
|
||||||
|
form.append('password', senha);
|
||||||
|
return request('/api/v1/auth/login', { method: 'POST', body: form, headers: { 'Content-Type': 'application/x-www-form-urlencoded' } });
|
||||||
|
},
|
||||||
|
registro: (data) => request('/api/v1/auth/registro', { method: 'POST', body: data }),
|
||||||
|
me: () => request('/api/v1/auth/me'),
|
||||||
|
dashboard: () => request('/api/v1/dashboard/'),
|
||||||
|
familias: () => request('/api/v1/familias/'),
|
||||||
|
familia: (id) => request(`/api/v1/familias/${id}`),
|
||||||
|
criarFamilia: (data) => request('/api/v1/familias/', { method: 'POST', body: data }),
|
||||||
|
membros: (famId) => request(`/api/v1/familias/${famId}/membros`),
|
||||||
|
falecidos: (famId) => request(`/api/v1/familias/${famId}/falecidos`),
|
||||||
|
checklist: (famId) => request(`/api/v1/familias/${famId}/checklist/`),
|
||||||
|
updateChecklist: (famId, itemId, status) => request(`/api/v1/familias/${famId}/checklist/${itemId}`, { method: 'PUT', body: { status } }),
|
||||||
|
proximoPasso: (famId) => request(`/api/v1/familias/${famId}/checklist/proximo`),
|
||||||
|
beneficios: (famId) => request(`/api/v1/familias/${famId}/beneficios/`),
|
||||||
|
scanBeneficios: (famId) => request(`/api/v1/familias/${famId}/beneficios/scan`, { method: 'POST' }),
|
||||||
|
documentos: (famId) => request(`/api/v1/familias/${famId}/documentos/`),
|
||||||
|
gerarDocumento: (famId, tipo, falecidoId) => request(`/api/v1/familias/${famId}/documentos/gerar`, { method: 'POST', body: { tipo, falecido_id: falecidoId } }),
|
||||||
|
downloadUrl: (famId, docId) => `${API_URL}/api/v1/familias/${famId}/documentos/${docId}/download`,
|
||||||
|
};
|
||||||
17
frontend/tailwind.config.js
Normal file
17
frontend/tailwind.config.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: [
|
||||||
|
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
background: "var(--background)",
|
||||||
|
foreground: "var(--foreground)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
5
start-backend.sh
Executable file
5
start-backend.sh
Executable file
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
cd /home/kernelpanic/projetos_jarvis/caronte/backend
|
||||||
|
source venv/bin/activate
|
||||||
|
python seed_data.py 2>/dev/null || true
|
||||||
|
exec uvicorn app.main:app --host 0.0.0.0 --port 8070 --reload
|
||||||
3
start-frontend.sh
Executable file
3
start-frontend.sh
Executable file
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
cd /home/kernelpanic/projetos_jarvis/caronte/frontend
|
||||||
|
exec npm start -- -p 3070
|
||||||
Reference in New Issue
Block a user