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:
Sergio Correa
2025-11-22 02:33:15 +00:00
commit f50174f898
68 changed files with 6835 additions and 0 deletions

View File

View File

@@ -0,0 +1,246 @@
from sendgrid import SendGridAPIClient
from sendgrid.helpers.mail import Mail, Email, To, Content
import logging
from app.core.config import settings
logger = logging.getLogger(__name__)
def send_verification_email(to_email: str, username: str, verification_token: str):
"""Envia email de verificação para novo usuário"""
verification_url = f"{settings.FRONTEND_URL}/verify-email/{verification_token}"
# Template do email
html_content = f"""
<!DOCTYPE html>
<html>
<head>
<style>
body {{
font-family: 'Arial', sans-serif;
background-color: #f3f4f6;
padding: 20px;
}}
.container {{
max-width: 600px;
margin: 0 auto;
background: white;
border-radius: 16px;
padding: 40px;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
}}
.header {{
text-align: center;
margin-bottom: 30px;
}}
.logo {{
font-size: 2.5rem;
font-weight: 800;
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin: 0;
}}
.content {{
color: #374151;
line-height: 1.6;
}}
.button {{
display: inline-block;
margin: 30px 0;
padding: 15px 40px;
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
color: white !important;
text-decoration: none;
border-radius: 12px;
font-weight: 700;
font-size: 16px;
}}
.footer {{
margin-top: 40px;
padding-top: 20px;
border-top: 2px solid #e5e7eb;
color: #6b7280;
font-size: 14px;
text-align: center;
}}
.highlight {{
color: #10b981;
font-weight: 700;
}}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1 class="logo">🚀 VIDA180</h1>
</div>
<div class="content">
<h2>Olá, <span class="highlight">{username}</span>! 👋</h2>
<p>Seja muito bem-vindo(a) ao <strong>Vida180</strong>! 🎉</p>
<p>Estamos muito felizes em ter você conosco nessa jornada de transformação!</p>
<p>Para ativar sua conta e começar a usar todas as funcionalidades, clique no botão abaixo:</p>
<div style="text-align: center;">
<a href="{verification_url}" class="button">
✅ Verificar Meu Email
</a>
</div>
<p style="color: #6b7280; font-size: 14px;">
Ou copie e cole este link no seu navegador:<br>
<a href="{verification_url}" style="color: #10b981;">{verification_url}</a>
</p>
<p>Após verificar seu email, você terá acesso completo a:</p>
<ul>
<li>🎯 <strong>Hábitos:</strong> Construa rotinas poderosas</li>
<li>📝 <strong>Tarefas:</strong> Organize seu dia</li>
<li>💪 <strong>Saúde:</strong> Acompanhe sua evolução física</li>
<li>📊 <strong>Progresso:</strong> Visualize sua transformação</li>
</ul>
<p><strong>A transformação acontece um dia de cada vez!</strong> 💪</p>
</div>
<div class="footer">
<p>
Este email foi enviado porque você se cadastrou no Vida180.<br>
Se você não criou esta conta, pode ignorar este email.
</p>
<p style="margin-top: 20px;">
<strong>Vida180</strong> - Transforme sua vida, um dia de cada vez 🚀
</p>
</div>
</div>
</body>
</html>
"""
try:
message = Mail(
from_email=Email(settings.SENDGRID_FROM_EMAIL, settings.SENDGRID_FROM_NAME),
to_emails=To(to_email),
subject=f"🚀 Bem-vindo ao Vida180! Confirme seu email",
html_content=Content("text/html", html_content)
)
sg = SendGridAPIClient(settings.SENDGRID_API_KEY)
response = sg.send(message)
logger.info(f"Email enviado para {to_email} - Status: {response.status_code}")
return True
except Exception as e:
logger.error(f"Erro ao enviar email para {to_email}: {str(e)}")
return False
def send_password_reset_email(to_email: str, username: str, reset_token: str):
"""Envia email de reset de senha"""
reset_url = f"{settings.FRONTEND_URL}/reset-password/{reset_token}"
html_content = f"""
<!DOCTYPE html>
<html>
<head>
<style>
body {{
font-family: 'Arial', sans-serif;
background-color: #f3f4f6;
padding: 20px;
}}
.container {{
max-width: 600px;
margin: 0 auto;
background: white;
border-radius: 16px;
padding: 40px;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
}}
.header {{
text-align: center;
margin-bottom: 30px;
}}
.logo {{
font-size: 2.5rem;
font-weight: 800;
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin: 0;
}}
.button {{
display: inline-block;
margin: 30px 0;
padding: 15px 40px;
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
color: white !important;
text-decoration: none;
border-radius: 12px;
font-weight: 700;
}}
.alert {{
background: #fef3c7;
border-left: 4px solid #f59e0b;
padding: 15px;
margin: 20px 0;
border-radius: 8px;
}}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1 class="logo">🚀 VIDA180</h1>
</div>
<h2>Olá, {username}! 👋</h2>
<p>Recebemos uma solicitação para redefinir sua senha.</p>
<div class="alert">
⚠️ Se você não solicitou a alteração de senha, ignore este email e sua senha permanecerá a mesma.
</div>
<p>Para criar uma nova senha, clique no botão abaixo:</p>
<div style="text-align: center;">
<a href="{reset_url}" class="button">
🔑 Redefinir Minha Senha
</a>
</div>
<p style="color: #6b7280; font-size: 14px;">
Este link expira em 24 horas.
</p>
<div style="margin-top: 40px; padding-top: 20px; border-top: 2px solid #e5e7eb; color: #6b7280; font-size: 14px; text-align: center;">
<strong>Vida180</strong> - Transforme sua vida, um dia de cada vez 🚀
</div>
</div>
</body>
</html>
"""
try:
message = Mail(
from_email=Email(settings.SENDGRID_FROM_EMAIL, settings.SENDGRID_FROM_NAME),
to_emails=To(to_email),
subject="🔑 Redefinir senha - Vida180",
html_content=Content("text/html", html_content)
)
sg = SendGridAPIClient(settings.SENDGRID_API_KEY)
response = sg.send(message)
logger.info(f"Email de reset enviado para {to_email}")
return True
except Exception as e:
logger.error(f"Erro ao enviar email de reset: {str(e)}")
return False

View File

@@ -0,0 +1,290 @@
from datetime import datetime, timedelta
from sqlalchemy.orm import Session
from sqlalchemy import func, and_
from typing import Optional, Dict
import random
from app.models.message import MotivationalMessage, UserMessageLog
from app.models.habit import Habit, HabitCompletion
from app.models.task import Task
from app.models.health import HealthMetric
from app.models.streak import Streak
class MessageService:
"""Serviço para gerar mensagens contextuais inteligentes"""
def __init__(self, db: Session):
self.db = db
def get_message_of_the_day(self, user_id: str) -> Dict:
"""Retorna a mensagem mais relevante para o usuário"""
# 1. Verificar streak (prioridade alta)
streak_message = self._check_streak_achievements(user_id)
if streak_message:
self._log_message(user_id, streak_message)
return streak_message
# 2. Verificar inatividade (recuperação)
inactive_message = self._check_inactivity(user_id)
if inactive_message:
self._log_message(user_id, inactive_message)
return inactive_message
# 3. Verificar lembretes importantes
reminder_message = self._check_reminders(user_id)
if reminder_message:
self._log_message(user_id, reminder_message)
return reminder_message
# 4. Verificar progresso e milestones
progress_message = self._check_progress(user_id)
if progress_message:
self._log_message(user_id, progress_message)
return progress_message
# 5. Mensagem baseada em hora do dia
time_message = self._get_time_based_message()
if time_message:
self._log_message(user_id, time_message)
return time_message
# 6. Mensagem motivacional padrão
default_message = self._get_daily_motivation()
self._log_message(user_id, default_message)
return default_message
def _check_streak_achievements(self, user_id: str) -> Optional[Dict]:
"""Verifica conquistas de streak"""
# Buscar maior streak do usuário
max_streak = self.db.query(func.max(Streak.current_streak)).filter(
Streak.user_id == user_id
).scalar() or 0
# Definir condições de streak
conditions = [
(100, 'streak_100'),
(60, 'streak_60'),
(30, 'streak_30'),
(14, 'streak_14'),
(7, 'streak_7')
]
for days, condition in conditions:
if max_streak >= days:
# Verificar se já mostrou essa mensagem hoje
already_shown = self.db.query(UserMessageLog).filter(
and_(
UserMessageLog.user_id == user_id,
UserMessageLog.message_type == 'streak_achievement',
func.date(UserMessageLog.shown_at) == datetime.now().date()
)
).first()
if not already_shown:
message = self.db.query(MotivationalMessage).filter(
and_(
MotivationalMessage.message_type == 'streak_achievement',
MotivationalMessage.trigger_condition == condition,
MotivationalMessage.is_active == True
)
).first()
if message:
return self._message_to_dict(message)
return None
def _check_inactivity(self, user_id: str) -> Optional[Dict]:
"""Verifica se o usuário está inativo"""
last_log = self.db.query(UserMessageLog).filter(
UserMessageLog.user_id == user_id
).order_by(UserMessageLog.shown_at.desc()).first()
if not last_log:
return None
days_inactive = (datetime.now() - last_log.shown_at).days
if days_inactive >= 14:
condition = 'inactive_14days'
elif days_inactive >= 7:
condition = 'inactive_7days'
else:
return None
message = self.db.query(MotivationalMessage).filter(
and_(
MotivationalMessage.message_type == 'recovery',
MotivationalMessage.trigger_condition == condition,
MotivationalMessage.is_active == True
)
).first()
return self._message_to_dict(message) if message else None
def _check_reminders(self, user_id: str) -> Optional[Dict]:
"""Verifica lembretes importantes"""
# Verificar última medição de saúde
last_metric = self.db.query(HealthMetric).filter(
HealthMetric.user_id == user_id
).order_by(HealthMetric.measurement_date.desc()).first()
if last_metric:
days_since_last = (datetime.now().date() - last_metric.measurement_date).days
if days_since_last >= 7:
condition = 'no_weight_7days'
elif days_since_last >= 3:
condition = 'no_weight_3days'
else:
condition = None
if condition:
message = self.db.query(MotivationalMessage).filter(
and_(
MotivationalMessage.message_type == 'reminder',
MotivationalMessage.trigger_condition == condition,
MotivationalMessage.is_active == True
)
).first()
if message:
return self._message_to_dict(message)
# Verificar hábitos pendentes
pending_habits = self.db.query(Habit).filter(
and_(
Habit.user_id == user_id,
Habit.is_active == True,
~Habit.id.in_(
self.db.query(HabitCompletion.habit_id).filter(
and_(
HabitCompletion.user_id == user_id,
HabitCompletion.completion_date == datetime.now().date()
)
)
)
)
).count()
if pending_habits > 0:
message = self.db.query(MotivationalMessage).filter(
and_(
MotivationalMessage.message_type == 'reminder',
MotivationalMessage.trigger_condition == 'habits_pending',
MotivationalMessage.is_active == True
)
).first()
if message:
msg_dict = self._message_to_dict(message)
msg_dict['message_text'] = f"🎯 Você tem {pending_habits} hábitos esperando por você hoje!"
return msg_dict
return None
def _check_progress(self, user_id: str) -> Optional[Dict]:
"""Verifica progresso e milestones"""
# Calcular consistência do mês
first_day_month = datetime.now().replace(day=1).date()
total_habits_month = self.db.query(func.count(Habit.id)).filter(
and_(
Habit.user_id == user_id,
Habit.start_date <= datetime.now().date()
)
).scalar()
completed_habits_month = self.db.query(func.count(HabitCompletion.id)).filter(
and_(
HabitCompletion.user_id == user_id,
HabitCompletion.completion_date >= first_day_month
)
).scalar()
if total_habits_month > 0:
consistency = (completed_habits_month / total_habits_month) * 100
if consistency >= 80:
message = self.db.query(MotivationalMessage).filter(
and_(
MotivationalMessage.message_type == 'progress_milestone',
MotivationalMessage.trigger_condition == 'consistency_80',
MotivationalMessage.is_active == True
)
).first()
if message:
msg_dict = self._message_to_dict(message)
msg_dict['message_text'] = f"💎 {int(consistency)}% de consistência esse mês! Top 10% dos usuários!"
return msg_dict
return None
def _get_time_based_message(self) -> Optional[Dict]:
"""Retorna mensagem baseada na hora do dia"""
hour = datetime.now().hour
if 5 <= hour < 12:
condition = 'morning'
elif 12 <= hour < 18:
condition = 'afternoon'
elif 18 <= hour < 22:
condition = 'evening'
else:
return None
message = self.db.query(MotivationalMessage).filter(
and_(
MotivationalMessage.message_type == 'time_based',
MotivationalMessage.trigger_condition == condition,
MotivationalMessage.is_active == True
)
).first()
return self._message_to_dict(message) if message else None
def _get_daily_motivation(self) -> Dict:
"""Retorna uma mensagem motivacional aleatória"""
messages = self.db.query(MotivationalMessage).filter(
and_(
MotivationalMessage.message_type == 'daily_motivation',
MotivationalMessage.is_active == True
)
).all()
if messages:
message = random.choice(messages)
return self._message_to_dict(message)
# Fallback
return {
'id': None,
'message_text': '💪 A transformação acontece um dia de cada vez. Esse dia é hoje!',
'icon': '💪',
'message_type': 'daily_motivation',
'priority': 1
}
def _message_to_dict(self, message: MotivationalMessage) -> Dict:
"""Converte mensagem para dicionário"""
return {
'id': str(message.id) if message.id else None,
'message_text': message.message_text,
'icon': message.icon,
'message_type': message.message_type,
'priority': message.priority
}
def _log_message(self, user_id: str, message: Dict):
"""Registra mensagem exibida no histórico"""
log = UserMessageLog(
user_id=user_id,
message_id=message.get('id'),
message_text=message['message_text'],
message_type=message['message_type']
)
self.db.add(log)
self.db.commit()