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:
0
backend/app/services/__init__.py
Normal file
0
backend/app/services/__init__.py
Normal file
246
backend/app/services/email.py
Normal file
246
backend/app/services/email.py
Normal 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
|
||||
290
backend/app/services/message_service.py
Normal file
290
backend/app/services/message_service.py
Normal 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()
|
||||
Reference in New Issue
Block a user