feat: Implementar funcionalidades de Tarefas e Saúde
- Criadas APIs para Health (métricas de saúde) * Registrar peso, altura, % gordura, medidas * Histórico completo de medições * Estatísticas e resumo - Criadas APIs para Tasks (tarefas) * Criar, editar e deletar tarefas * Filtros por status e data * Estatísticas detalhadas * Prioridades (baixa, média, alta) - Frontend implementado: * Página Health.tsx - registro de métricas * Página Tasks.tsx - gerenciamento de tarefas * Página Progress.tsx - visualização de progresso * Dashboard integrado com estatísticas reais - Schemas e modelos atualizados - Todas as funcionalidades testadas e operacionais
This commit is contained in:
4
backend/app/api/__init__.py
Normal file
4
backend/app/api/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from . import auth, habits, health, messages, admin, tasks
|
||||
|
||||
__all__ = ['auth', 'habits', 'health', 'messages', 'admin', 'tasks']
|
||||
|
||||
148
backend/app/api/admin.py
Normal file
148
backend/app/api/admin.py
Normal file
@@ -0,0 +1,148 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
from datetime import datetime
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.security import decode_access_token, get_password_hash
|
||||
from app.models.user import User
|
||||
from app.schemas.admin import UserListResponse, UserUpdateRequest, PasswordChangeRequest
|
||||
|
||||
router = APIRouter(prefix="/admin", tags=["admin"])
|
||||
security = HTTPBearer()
|
||||
|
||||
def get_current_superadmin(
|
||||
credentials: HTTPAuthorizationCredentials = Depends(security),
|
||||
db: Session = Depends(get_db)
|
||||
) -> User:
|
||||
"""Verifica se usuário é superadmin"""
|
||||
token = credentials.credentials
|
||||
payload = decode_access_token(token)
|
||||
if not payload:
|
||||
raise HTTPException(status_code=401, detail="Token inválido")
|
||||
|
||||
user_id = payload.get("sub")
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
|
||||
if not user or not user.is_superadmin:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Acesso negado. Apenas superadmin."
|
||||
)
|
||||
|
||||
return user
|
||||
|
||||
@router.get("/users", response_model=List[UserListResponse])
|
||||
async def list_all_users(
|
||||
admin: User = Depends(get_current_superadmin),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Listar todos os usuários (apenas superadmin)"""
|
||||
users = db.query(User).order_by(User.created_at.desc()).all()
|
||||
return users
|
||||
|
||||
@router.get("/stats")
|
||||
async def get_admin_stats(
|
||||
admin: User = Depends(get_current_superadmin),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Estatísticas gerais do sistema"""
|
||||
from sqlalchemy import func
|
||||
from app.models.habit import Habit, HabitCompletion
|
||||
|
||||
total_users = db.query(func.count(User.id)).scalar()
|
||||
active_users = db.query(func.count(User.id)).filter(User.is_active == True).scalar()
|
||||
total_habits = db.query(func.count(Habit.id)).scalar()
|
||||
total_completions = db.query(func.count(HabitCompletion.id)).scalar()
|
||||
|
||||
return {
|
||||
"total_users": total_users,
|
||||
"active_users": active_users,
|
||||
"inactive_users": total_users - active_users,
|
||||
"total_habits": total_habits,
|
||||
"total_completions": total_completions
|
||||
}
|
||||
|
||||
@router.patch("/users/{user_id}")
|
||||
async def update_user(
|
||||
user_id: str,
|
||||
data: UserUpdateRequest,
|
||||
admin: User = Depends(get_current_superadmin),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Atualizar dados de usuário"""
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="Usuário não encontrado")
|
||||
|
||||
# Atualizar campos
|
||||
if data.email is not None:
|
||||
# Verificar se email já existe
|
||||
existing = db.query(User).filter(
|
||||
User.email == data.email,
|
||||
User.id != user_id
|
||||
).first()
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail="Email já em uso")
|
||||
user.email = data.email
|
||||
|
||||
if data.username is not None:
|
||||
# Verificar se username já existe
|
||||
existing = db.query(User).filter(
|
||||
User.username == data.username,
|
||||
User.id != user_id
|
||||
).first()
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail="Username já em uso")
|
||||
user.username = data.username
|
||||
|
||||
if data.full_name is not None:
|
||||
user.full_name = data.full_name
|
||||
|
||||
if data.is_active is not None:
|
||||
user.is_active = data.is_active
|
||||
|
||||
if data.is_verified is not None:
|
||||
user.is_verified = data.is_verified
|
||||
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
|
||||
return {"message": "Usuário atualizado", "user": user}
|
||||
|
||||
@router.delete("/users/{user_id}")
|
||||
async def delete_user(
|
||||
user_id: str,
|
||||
admin: User = Depends(get_current_superadmin),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Deletar usuário (soft delete - apenas desativa)"""
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="Usuário não encontrado")
|
||||
|
||||
if user.is_superadmin:
|
||||
raise HTTPException(status_code=403, detail="Não é possível deletar superadmin")
|
||||
|
||||
user.is_active = False
|
||||
db.commit()
|
||||
|
||||
return {"message": "Usuário desativado com sucesso"}
|
||||
|
||||
@router.post("/users/{user_id}/reset-password")
|
||||
async def reset_user_password(
|
||||
user_id: str,
|
||||
data: PasswordChangeRequest,
|
||||
admin: User = Depends(get_current_superadmin),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Resetar senha de usuário"""
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="Usuário não encontrado")
|
||||
|
||||
user.password_hash = get_password_hash(data.new_password)
|
||||
db.commit()
|
||||
|
||||
return {"message": "Senha alterada com sucesso"}
|
||||
56
backend/app/api/auth.py
Normal file
56
backend/app/api/auth.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from app.core.database import get_db
|
||||
from app.core.security import get_password_hash, verify_password, create_access_token
|
||||
from app.models.user import User
|
||||
from app.schemas.user import UserCreate, UserLogin, Token, UserResponse
|
||||
from datetime import datetime
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/register", response_model=Token)
|
||||
def register(user: UserCreate, db: Session = Depends(get_db)):
|
||||
if db.query(User).filter(User.email == user.email).first():
|
||||
raise HTTPException(status_code=400, detail="Email already registered")
|
||||
|
||||
if db.query(User).filter(User.username == user.username).first():
|
||||
raise HTTPException(status_code=400, detail="Username already taken")
|
||||
|
||||
db_user = User(
|
||||
email=user.email,
|
||||
username=user.username,
|
||||
password_hash=get_password_hash(user.password),
|
||||
full_name=user.full_name,
|
||||
phone=user.phone,
|
||||
is_verified=True # SEM verificação de email
|
||||
)
|
||||
|
||||
db.add(db_user)
|
||||
db.commit()
|
||||
db.refresh(db_user)
|
||||
|
||||
access_token = create_access_token(data={"sub": db_user.email})
|
||||
|
||||
return {
|
||||
"access_token": access_token,
|
||||
"token_type": "bearer",
|
||||
"user": UserResponse.from_orm(db_user)
|
||||
}
|
||||
|
||||
@router.post("/login", response_model=Token)
|
||||
def login(credentials: UserLogin, db: Session = Depends(get_db)):
|
||||
user = db.query(User).filter(User.email == credentials.email).first()
|
||||
|
||||
if not user or not verify_password(credentials.password, user.password_hash):
|
||||
raise HTTPException(status_code=401, detail="Invalid credentials")
|
||||
|
||||
user.last_login_at = datetime.utcnow()
|
||||
db.commit()
|
||||
|
||||
access_token = create_access_token(data={"sub": user.email})
|
||||
|
||||
return {
|
||||
"access_token": access_token,
|
||||
"token_type": "bearer",
|
||||
"user": UserResponse.from_orm(user)
|
||||
}
|
||||
155
backend/app/api/habits.py
Normal file
155
backend/app/api/habits.py
Normal file
@@ -0,0 +1,155 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
from datetime import date, datetime
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.security import decode_access_token
|
||||
from app.models.user import User
|
||||
from app.models.habit import Habit, HabitCompletion
|
||||
from app.schemas.habit import HabitCreate, HabitResponse, HabitCompletionCreate
|
||||
|
||||
router = APIRouter(prefix="/habits", tags=["habits"])
|
||||
security = HTTPBearer()
|
||||
|
||||
def get_current_user_id(credentials: HTTPAuthorizationCredentials = Depends(security)) -> str:
|
||||
"""Extrai user_id do token JWT"""
|
||||
token = credentials.credentials
|
||||
payload = decode_access_token(token)
|
||||
if not payload:
|
||||
raise HTTPException(status_code=401, detail="Token inválido")
|
||||
return payload.get("sub")
|
||||
|
||||
@router.post("/", response_model=HabitResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_habit(
|
||||
habit_data: HabitCreate,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Criar novo hábito"""
|
||||
new_habit = Habit(
|
||||
user_id=user_id,
|
||||
name=habit_data.name,
|
||||
description=habit_data.description,
|
||||
frequency_type=habit_data.frequency_type,
|
||||
target_count=habit_data.target_count,
|
||||
reminder_time=habit_data.reminder_time,
|
||||
start_date=habit_data.start_date or date.today()
|
||||
)
|
||||
|
||||
db.add(new_habit)
|
||||
db.commit()
|
||||
db.refresh(new_habit)
|
||||
|
||||
return new_habit
|
||||
|
||||
@router.get("/", response_model=List[HabitResponse])
|
||||
async def list_habits(
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Listar todos os hábitos do usuário"""
|
||||
habits = db.query(Habit).filter(
|
||||
Habit.user_id == user_id,
|
||||
Habit.is_active == True
|
||||
).all()
|
||||
|
||||
return habits
|
||||
|
||||
@router.post("/{habit_id}/complete", status_code=status.HTTP_201_CREATED)
|
||||
async def complete_habit(
|
||||
habit_id: str,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Marcar hábito como completo para hoje"""
|
||||
# Verificar se já completou hoje
|
||||
today = date.today()
|
||||
existing = db.query(HabitCompletion).filter(
|
||||
HabitCompletion.habit_id == habit_id,
|
||||
HabitCompletion.user_id == user_id,
|
||||
HabitCompletion.completion_date == today
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
raise HTTPException(status_code=400, detail="Hábito já completado hoje")
|
||||
|
||||
# Criar completion
|
||||
completion = HabitCompletion(
|
||||
habit_id=habit_id,
|
||||
user_id=user_id,
|
||||
completion_date=today,
|
||||
completion_time=datetime.now().time()
|
||||
)
|
||||
|
||||
db.add(completion)
|
||||
db.commit()
|
||||
|
||||
return {"message": "Hábito completado!", "date": today}
|
||||
|
||||
@router.delete("/{habit_id}/complete")
|
||||
async def uncomplete_habit(
|
||||
habit_id: str,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Desmarcar hábito de hoje"""
|
||||
today = date.today()
|
||||
completion = db.query(HabitCompletion).filter(
|
||||
HabitCompletion.habit_id == habit_id,
|
||||
HabitCompletion.user_id == user_id,
|
||||
HabitCompletion.completion_date == today
|
||||
).first()
|
||||
|
||||
if not completion:
|
||||
raise HTTPException(status_code=404, detail="Completion não encontrado")
|
||||
|
||||
db.delete(completion)
|
||||
db.commit()
|
||||
|
||||
return {"message": "Completion removido"}
|
||||
|
||||
@router.get("/stats")
|
||||
async def get_habit_stats(
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Estatísticas de hábitos"""
|
||||
from sqlalchemy import func
|
||||
|
||||
total_habits = db.query(func.count(Habit.id)).filter(
|
||||
Habit.user_id == user_id,
|
||||
Habit.is_active == True
|
||||
).scalar()
|
||||
|
||||
completed_today = db.query(func.count(HabitCompletion.id)).filter(
|
||||
HabitCompletion.user_id == user_id,
|
||||
HabitCompletion.completion_date == date.today()
|
||||
).scalar()
|
||||
|
||||
return {
|
||||
"total_habits": total_habits,
|
||||
"completed_today": completed_today,
|
||||
"pending_today": total_habits - completed_today
|
||||
}
|
||||
|
||||
@router.delete("/{habit_id}")
|
||||
async def delete_habit(
|
||||
habit_id: str,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Deletar hábito"""
|
||||
habit = db.query(Habit).filter(
|
||||
Habit.id == habit_id,
|
||||
Habit.user_id == user_id
|
||||
).first()
|
||||
|
||||
if not habit:
|
||||
raise HTTPException(status_code=404, detail="Hábito não encontrado")
|
||||
|
||||
db.delete(habit)
|
||||
db.commit()
|
||||
|
||||
return {"message": "Hábito deletado"}
|
||||
169
backend/app/api/health.py
Normal file
169
backend/app/api/health.py
Normal file
@@ -0,0 +1,169 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
from datetime import date
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.security import decode_access_token
|
||||
from app.models.health import HealthMetric
|
||||
from app.schemas.health import HealthMetricCreate, HealthMetricResponse, HealthMetricUpdate
|
||||
|
||||
router = APIRouter()
|
||||
security = HTTPBearer()
|
||||
|
||||
def get_current_user_id(credentials: HTTPAuthorizationCredentials = Depends(security)) -> str:
|
||||
"""Extrai user_id do token JWT"""
|
||||
token = credentials.credentials
|
||||
payload = decode_access_token(token)
|
||||
if not payload:
|
||||
raise HTTPException(status_code=401, detail="Token inválido")
|
||||
return payload.get("sub")
|
||||
|
||||
@router.post("/", response_model=HealthMetricResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_health_metric(
|
||||
metric_data: HealthMetricCreate,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Criar nova métrica de saúde"""
|
||||
new_metric = HealthMetric(
|
||||
user_id=user_id,
|
||||
measurement_date=metric_data.measurement_date,
|
||||
measurement_time=metric_data.measurement_time,
|
||||
weight=metric_data.weight,
|
||||
height=metric_data.height,
|
||||
body_fat_percentage=metric_data.body_fat_percentage,
|
||||
muscle_mass=metric_data.muscle_mass,
|
||||
waist=metric_data.waist,
|
||||
chest=metric_data.chest,
|
||||
hips=metric_data.hips,
|
||||
notes=metric_data.notes
|
||||
)
|
||||
|
||||
db.add(new_metric)
|
||||
db.commit()
|
||||
db.refresh(new_metric)
|
||||
|
||||
return new_metric
|
||||
|
||||
@router.get("/", response_model=List[HealthMetricResponse])
|
||||
async def list_health_metrics(
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Session = Depends(get_db),
|
||||
limit: int = 100
|
||||
):
|
||||
"""Listar todas as métricas de saúde do usuário"""
|
||||
metrics = db.query(HealthMetric).filter(
|
||||
HealthMetric.user_id == user_id
|
||||
).order_by(HealthMetric.measurement_date.desc()).limit(limit).all()
|
||||
|
||||
return metrics
|
||||
|
||||
@router.get("/latest", response_model=HealthMetricResponse)
|
||||
async def get_latest_metric(
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Obter última métrica registrada"""
|
||||
metric = db.query(HealthMetric).filter(
|
||||
HealthMetric.user_id == user_id
|
||||
).order_by(HealthMetric.measurement_date.desc()).first()
|
||||
|
||||
if not metric:
|
||||
raise HTTPException(status_code=404, detail="Nenhuma métrica encontrada")
|
||||
|
||||
return metric
|
||||
|
||||
@router.get("/{metric_id}", response_model=HealthMetricResponse)
|
||||
async def get_health_metric(
|
||||
metric_id: str,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Obter métrica específica"""
|
||||
metric = db.query(HealthMetric).filter(
|
||||
HealthMetric.id == metric_id,
|
||||
HealthMetric.user_id == user_id
|
||||
).first()
|
||||
|
||||
if not metric:
|
||||
raise HTTPException(status_code=404, detail="Métrica não encontrada")
|
||||
|
||||
return metric
|
||||
|
||||
@router.put("/{metric_id}", response_model=HealthMetricResponse)
|
||||
async def update_health_metric(
|
||||
metric_id: str,
|
||||
metric_data: HealthMetricUpdate,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Atualizar métrica de saúde"""
|
||||
metric = db.query(HealthMetric).filter(
|
||||
HealthMetric.id == metric_id,
|
||||
HealthMetric.user_id == user_id
|
||||
).first()
|
||||
|
||||
if not metric:
|
||||
raise HTTPException(status_code=404, detail="Métrica não encontrada")
|
||||
|
||||
# Atualizar campos
|
||||
update_data = metric_data.dict(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(metric, field, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(metric)
|
||||
|
||||
return metric
|
||||
|
||||
@router.delete("/{metric_id}")
|
||||
async def delete_health_metric(
|
||||
metric_id: str,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Deletar métrica de saúde"""
|
||||
metric = db.query(HealthMetric).filter(
|
||||
HealthMetric.id == metric_id,
|
||||
HealthMetric.user_id == user_id
|
||||
).first()
|
||||
|
||||
if not metric:
|
||||
raise HTTPException(status_code=404, detail="Métrica não encontrada")
|
||||
|
||||
db.delete(metric)
|
||||
db.commit()
|
||||
|
||||
return {"message": "Métrica deletada com sucesso"}
|
||||
|
||||
@router.get("/stats/summary")
|
||||
async def get_health_stats(
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Estatísticas e resumo de saúde"""
|
||||
latest = db.query(HealthMetric).filter(
|
||||
HealthMetric.user_id == user_id
|
||||
).order_by(HealthMetric.measurement_date.desc()).first()
|
||||
|
||||
# Pegar primeira e última medição para calcular diferença
|
||||
first = db.query(HealthMetric).filter(
|
||||
HealthMetric.user_id == user_id,
|
||||
HealthMetric.weight.isnot(None)
|
||||
).order_by(HealthMetric.measurement_date.asc()).first()
|
||||
|
||||
stats = {
|
||||
"current_weight": float(latest.weight) if latest and latest.weight else None,
|
||||
"current_height": float(latest.height) if latest and latest.height else None,
|
||||
"current_body_fat": float(latest.body_fat_percentage) if latest and latest.body_fat_percentage else None,
|
||||
"weight_change": None,
|
||||
"total_measurements": db.query(HealthMetric).filter(HealthMetric.user_id == user_id).count()
|
||||
}
|
||||
|
||||
if latest and first and latest.weight and first.weight:
|
||||
stats["weight_change"] = float(latest.weight - first.weight)
|
||||
|
||||
return stats
|
||||
|
||||
93
backend/app/api/messages.py
Normal file
93
backend/app/api/messages.py
Normal file
@@ -0,0 +1,93 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Dict
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.services.message_service import MessageService
|
||||
|
||||
router = APIRouter(prefix="/messages", tags=["messages"])
|
||||
|
||||
|
||||
@router.get("/daily", response_model=Dict)
|
||||
async def get_daily_message(
|
||||
user_id: str, # TODO: Pegar do JWT token quando implementar autenticação
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Retorna a mensagem motivacional do dia para o usuário.
|
||||
A mensagem é contextual baseada em:
|
||||
- Streaks e conquistas
|
||||
- Inatividade
|
||||
- Lembretes
|
||||
- Progresso
|
||||
- Hora do dia
|
||||
"""
|
||||
try:
|
||||
message_service = MessageService(db)
|
||||
message = message_service.get_message_of_the_day(user_id)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": message
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/click/{message_id}")
|
||||
async def mark_message_clicked(
|
||||
message_id: str,
|
||||
user_id: str,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Marca mensagem como clicada pelo usuário"""
|
||||
from app.models.message import UserMessageLog
|
||||
from datetime import datetime
|
||||
|
||||
try:
|
||||
log = db.query(UserMessageLog).filter(
|
||||
UserMessageLog.id == message_id,
|
||||
UserMessageLog.user_id == user_id
|
||||
).first()
|
||||
|
||||
if log:
|
||||
log.was_clicked = True
|
||||
log.clicked_at = datetime.now()
|
||||
db.commit()
|
||||
|
||||
return {"success": True, "message": "Click registrado"}
|
||||
else:
|
||||
raise HTTPException(status_code=404, detail="Mensagem não encontrada")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/history")
|
||||
async def get_message_history(
|
||||
user_id: str,
|
||||
limit: int = 10,
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Retorna histórico de mensagens do usuário"""
|
||||
from app.models.message import UserMessageLog
|
||||
|
||||
try:
|
||||
logs = db.query(UserMessageLog).filter(
|
||||
UserMessageLog.user_id == user_id
|
||||
).order_by(UserMessageLog.shown_at.desc()).limit(limit).all()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"messages": [
|
||||
{
|
||||
"id": str(log.id),
|
||||
"message_text": log.message_text,
|
||||
"message_type": log.message_type,
|
||||
"shown_at": log.shown_at.isoformat(),
|
||||
"was_clicked": log.was_clicked
|
||||
}
|
||||
for log in logs
|
||||
]
|
||||
}
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
218
backend/app/api/tasks.py
Normal file
218
backend/app/api/tasks.py
Normal file
@@ -0,0 +1,218 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional
|
||||
from datetime import date
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.security import decode_access_token
|
||||
from app.models.task import Task
|
||||
from app.schemas.task import TaskCreate, TaskResponse, TaskUpdate
|
||||
|
||||
router = APIRouter()
|
||||
security = HTTPBearer()
|
||||
|
||||
def get_current_user_id(credentials: HTTPAuthorizationCredentials = Depends(security)) -> str:
|
||||
"""Extrai user_id do token JWT"""
|
||||
token = credentials.credentials
|
||||
payload = decode_access_token(token)
|
||||
if not payload:
|
||||
raise HTTPException(status_code=401, detail="Token inválido")
|
||||
return payload.get("sub")
|
||||
|
||||
@router.post("/", response_model=TaskResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_task(
|
||||
task_data: TaskCreate,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Criar nova tarefa"""
|
||||
new_task = Task(
|
||||
user_id=user_id,
|
||||
title=task_data.title,
|
||||
description=task_data.description,
|
||||
priority=task_data.priority,
|
||||
status=task_data.status,
|
||||
due_date=task_data.due_date,
|
||||
due_time=task_data.due_time,
|
||||
category_id=task_data.category_id
|
||||
)
|
||||
|
||||
db.add(new_task)
|
||||
db.commit()
|
||||
db.refresh(new_task)
|
||||
|
||||
return new_task
|
||||
|
||||
@router.get("/", response_model=List[TaskResponse])
|
||||
async def list_tasks(
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Session = Depends(get_db),
|
||||
status_filter: Optional[str] = None,
|
||||
include_archived: bool = False
|
||||
):
|
||||
"""Listar todas as tarefas do usuário"""
|
||||
query = db.query(Task).filter(Task.user_id == user_id)
|
||||
|
||||
if not include_archived:
|
||||
query = query.filter(Task.is_archived == False)
|
||||
|
||||
if status_filter:
|
||||
query = query.filter(Task.status == status_filter)
|
||||
|
||||
tasks = query.order_by(Task.due_date.asc()).all()
|
||||
|
||||
return tasks
|
||||
|
||||
@router.get("/today", response_model=List[TaskResponse])
|
||||
async def get_today_tasks(
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Obter tarefas de hoje"""
|
||||
today = date.today()
|
||||
tasks = db.query(Task).filter(
|
||||
Task.user_id == user_id,
|
||||
Task.due_date == today,
|
||||
Task.is_archived == False
|
||||
).order_by(Task.due_time.asc()).all()
|
||||
|
||||
return tasks
|
||||
|
||||
@router.get("/{task_id}", response_model=TaskResponse)
|
||||
async def get_task(
|
||||
task_id: str,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Obter tarefa específica"""
|
||||
task = db.query(Task).filter(
|
||||
Task.id == task_id,
|
||||
Task.user_id == user_id
|
||||
).first()
|
||||
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="Tarefa não encontrada")
|
||||
|
||||
return task
|
||||
|
||||
@router.put("/{task_id}", response_model=TaskResponse)
|
||||
async def update_task(
|
||||
task_id: str,
|
||||
task_data: TaskUpdate,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Atualizar tarefa"""
|
||||
task = db.query(Task).filter(
|
||||
Task.id == task_id,
|
||||
Task.user_id == user_id
|
||||
).first()
|
||||
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="Tarefa não encontrada")
|
||||
|
||||
# Atualizar campos
|
||||
update_data = task_data.dict(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(task, field, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(task)
|
||||
|
||||
return task
|
||||
|
||||
@router.patch("/{task_id}/status")
|
||||
async def update_task_status(
|
||||
task_id: str,
|
||||
status: str,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Atualizar status da tarefa"""
|
||||
task = db.query(Task).filter(
|
||||
Task.id == task_id,
|
||||
Task.user_id == user_id
|
||||
).first()
|
||||
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="Tarefa não encontrada")
|
||||
|
||||
task.status = status
|
||||
db.commit()
|
||||
|
||||
return {"message": "Status atualizado", "status": status}
|
||||
|
||||
@router.delete("/{task_id}")
|
||||
async def delete_task(
|
||||
task_id: str,
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Deletar tarefa"""
|
||||
task = db.query(Task).filter(
|
||||
Task.id == task_id,
|
||||
Task.user_id == user_id
|
||||
).first()
|
||||
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="Tarefa não encontrada")
|
||||
|
||||
db.delete(task)
|
||||
db.commit()
|
||||
|
||||
return {"message": "Tarefa deletada com sucesso"}
|
||||
|
||||
@router.get("/stats/summary")
|
||||
async def get_task_stats(
|
||||
user_id: str = Depends(get_current_user_id),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Estatísticas de tarefas"""
|
||||
from sqlalchemy import func
|
||||
|
||||
total_tasks = db.query(func.count(Task.id)).filter(
|
||||
Task.user_id == user_id,
|
||||
Task.is_archived == False
|
||||
).scalar()
|
||||
|
||||
pending = db.query(func.count(Task.id)).filter(
|
||||
Task.user_id == user_id,
|
||||
Task.status == "pending",
|
||||
Task.is_archived == False
|
||||
).scalar()
|
||||
|
||||
in_progress = db.query(func.count(Task.id)).filter(
|
||||
Task.user_id == user_id,
|
||||
Task.status == "in_progress",
|
||||
Task.is_archived == False
|
||||
).scalar()
|
||||
|
||||
completed = db.query(func.count(Task.id)).filter(
|
||||
Task.user_id == user_id,
|
||||
Task.status == "completed",
|
||||
Task.is_archived == False
|
||||
).scalar()
|
||||
|
||||
today = date.today()
|
||||
today_tasks = db.query(func.count(Task.id)).filter(
|
||||
Task.user_id == user_id,
|
||||
Task.due_date == today,
|
||||
Task.is_archived == False
|
||||
).scalar()
|
||||
|
||||
today_completed = db.query(func.count(Task.id)).filter(
|
||||
Task.user_id == user_id,
|
||||
Task.due_date == today,
|
||||
Task.status == "completed"
|
||||
).scalar()
|
||||
|
||||
return {
|
||||
"total_tasks": total_tasks,
|
||||
"pending": pending,
|
||||
"in_progress": in_progress,
|
||||
"completed": completed,
|
||||
"today_tasks": today_tasks,
|
||||
"today_completed": today_completed
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user