- feat(025): favoritos locais com FavoritesContext, HeartButton, PublicFavoritesPage
- feat(026): central de contatos admin (leads/contatos unificados)
- feat(027): configuração da página de contato via admin
- feat(028): trabalhe conosco - candidaturas com upload e admin
- feat(029): UX área do cliente - visitas, comparação, perfil
- feat(030): navbar UX - menu mobile, ThemeToggle, useFavorites
- feat(031): hero light/dark - imagens separadas por tema, upload, preview, seed
- feat(032): performance homepage - Promise.all parallel fetches, sessionStorage cache,
preload hero image, loading=lazy nos cards, useInView hook, will-change carrossel,
keyframes em index.css, AgentsCarousel e HomeScrollScene via props
- fix: light mode HomeScrollScene - gradiente, cores de texto, scroll hint
migrations: g1h2i3j4k5l6 (source em leads), h1i2j3k4l5m6 (contact_config),
i1j2k3l4m5n6 (job_applications), j2k3l4m5n6o7 (hero theme images)
179 lines
5 KiB
Python
179 lines
5 KiB
Python
import uuid as _uuid
|
|
import bcrypt
|
|
from flask import Blueprint, request, jsonify, g
|
|
from pydantic import ValidationError
|
|
from sqlalchemy.exc import IntegrityError
|
|
from sqlalchemy.orm import selectinload
|
|
from app import db
|
|
from app.models.saved_property import SavedProperty
|
|
from app.models.visit_request import VisitRequest
|
|
from app.models.boleto import Boleto
|
|
from app.models.property import Property
|
|
from app.models.user import ClientUser
|
|
from app.schemas.client_area import (
|
|
SavedPropertyOut,
|
|
PropertyCard,
|
|
FavoriteIn,
|
|
VisitRequestOut,
|
|
BoletoOut,
|
|
UpdateProfileIn,
|
|
UpdateProfileOut,
|
|
UpdatePasswordIn,
|
|
)
|
|
from app.utils.auth import require_auth
|
|
|
|
client_bp = Blueprint("client", __name__)
|
|
|
|
|
|
@client_bp.get("/favorites")
|
|
@require_auth
|
|
def get_favorites():
|
|
saved = (
|
|
SavedProperty.query
|
|
.filter_by(user_id=g.current_user_id)
|
|
.options(selectinload(SavedProperty.property).selectinload(Property.photos))
|
|
.order_by(SavedProperty.created_at.desc())
|
|
.all()
|
|
)
|
|
return (
|
|
jsonify(
|
|
[SavedPropertyOut.from_saved(s).model_dump(mode="json") for s in saved]
|
|
),
|
|
200,
|
|
)
|
|
|
|
|
|
@client_bp.post("/favorites")
|
|
@require_auth
|
|
def add_favorite():
|
|
try:
|
|
data = FavoriteIn.model_validate(request.get_json() or {})
|
|
except ValidationError as e:
|
|
return jsonify({"error": e.errors(include_url=False)}), 422
|
|
|
|
try:
|
|
prop_uuid = _uuid.UUID(data.property_id)
|
|
except ValueError:
|
|
return jsonify({"error": "property_id inválido"}), 422
|
|
prop = db.session.get(Property, prop_uuid)
|
|
if not prop:
|
|
return jsonify({"error": "Imóvel não encontrado"}), 404
|
|
|
|
saved = SavedProperty(user_id=g.current_user_id, property_id=prop_uuid)
|
|
db.session.add(saved)
|
|
try:
|
|
db.session.commit()
|
|
except IntegrityError:
|
|
db.session.rollback()
|
|
return jsonify({"error": "Imóvel já está nos favoritos"}), 409
|
|
|
|
return jsonify(SavedPropertyOut.from_saved(saved).model_dump(mode="json")), 201
|
|
|
|
|
|
@client_bp.delete("/favorites/<property_id>")
|
|
@require_auth
|
|
def remove_favorite(property_id: str):
|
|
try:
|
|
prop_uuid = _uuid.UUID(property_id)
|
|
except ValueError:
|
|
return jsonify({"error": "property_id inválido"}), 422
|
|
saved = SavedProperty.query.filter_by(
|
|
user_id=g.current_user_id, property_id=prop_uuid
|
|
).first()
|
|
if not saved:
|
|
return jsonify({"error": "Favorito não encontrado"}), 404
|
|
|
|
db.session.delete(saved)
|
|
db.session.commit()
|
|
return "", 204
|
|
|
|
|
|
@client_bp.get("/visits")
|
|
@require_auth
|
|
def get_visits():
|
|
visits = (
|
|
VisitRequest.query.filter_by(user_id=g.current_user_id)
|
|
.order_by(VisitRequest.created_at.desc())
|
|
.all()
|
|
)
|
|
return (
|
|
jsonify(
|
|
[VisitRequestOut.model_validate(v).model_dump(mode="json") for v in visits]
|
|
),
|
|
200,
|
|
)
|
|
|
|
|
|
@client_bp.get("/boletos")
|
|
@require_auth
|
|
def get_boletos():
|
|
boletos = (
|
|
Boleto.query.filter_by(user_id=g.current_user_id)
|
|
.order_by(Boleto.due_date.asc())
|
|
.all()
|
|
)
|
|
return (
|
|
jsonify([BoletoOut.model_validate(b).model_dump(mode="json") for b in boletos]),
|
|
200,
|
|
)
|
|
|
|
|
|
@client_bp.patch("/profile")
|
|
@require_auth
|
|
def update_profile():
|
|
try:
|
|
data = UpdateProfileIn.model_validate(request.get_json() or {})
|
|
except ValidationError as e:
|
|
return jsonify({"error": e.errors(include_url=False)}), 422
|
|
|
|
user = db.session.get(ClientUser, g.current_user_id)
|
|
if not user:
|
|
return jsonify({"error": "Usuário não encontrado"}), 404
|
|
|
|
user.name = data.name
|
|
db.session.commit()
|
|
return jsonify(UpdateProfileOut.model_validate(user).model_dump(mode="json")), 200
|
|
|
|
|
|
@client_bp.patch("/password")
|
|
@require_auth
|
|
def change_password():
|
|
try:
|
|
data = UpdatePasswordIn.model_validate(request.get_json() or {})
|
|
except ValidationError as e:
|
|
return jsonify({"error": e.errors(include_url=False)}), 422
|
|
|
|
user = db.session.get(ClientUser, g.current_user_id)
|
|
if not user:
|
|
return jsonify({"error": "Usuário não encontrado"}), 404
|
|
|
|
if not bcrypt.checkpw(
|
|
data.current_password.encode("utf-8"),
|
|
user.password_hash.encode("utf-8"),
|
|
):
|
|
return jsonify({"error": "Senha atual incorreta"}), 400
|
|
|
|
user.password_hash = bcrypt.hashpw(
|
|
data.new_password.encode("utf-8"), bcrypt.gensalt()
|
|
).decode("utf-8")
|
|
db.session.commit()
|
|
return "", 204
|
|
|
|
|
|
@client_bp.patch("/visits/<visit_id>/cancel")
|
|
@require_auth
|
|
def cancel_visit(visit_id: str):
|
|
visit = db.session.get(VisitRequest, visit_id)
|
|
if not visit:
|
|
return jsonify({"error": "Visita não encontrada"}), 404
|
|
|
|
if str(visit.user_id) != str(g.current_user_id):
|
|
return jsonify({"error": "Acesso negado"}), 403
|
|
|
|
if visit.status != "pending":
|
|
return jsonify({"error": "Apenas visitas pendentes podem ser canceladas"}), 400
|
|
|
|
visit.status = "cancelled"
|
|
db.session.commit()
|
|
return jsonify(VisitRequestOut.model_validate(visit).model_dump(mode="json")), 200
|
|
|