feat: features 025-032 - favoritos, contatos, trabalhe-conosco, area-cliente, navbar, hero-light-dark, performance-homepage
- 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)
This commit is contained in:
parent
6ef5a7a17e
commit
cf5603243c
106 changed files with 11927 additions and 1367 deletions
|
|
@ -1,17 +1,24 @@
|
|||
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
|
||||
|
||||
|
|
@ -22,13 +29,15 @@ client_bp = Blueprint("client", __name__)
|
|||
@require_auth
|
||||
def get_favorites():
|
||||
saved = (
|
||||
SavedProperty.query.filter_by(user_id=g.current_user_id)
|
||||
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.model_validate(s).model_dump(mode="json") for s in saved]
|
||||
[SavedPropertyOut.from_saved(s).model_dump(mode="json") for s in saved]
|
||||
),
|
||||
200,
|
||||
)
|
||||
|
|
@ -58,7 +67,7 @@ def add_favorite():
|
|||
db.session.rollback()
|
||||
return jsonify({"error": "Imóvel já está nos favoritos"}), 409
|
||||
|
||||
return jsonify(SavedPropertyOut.model_validate(saved).model_dump(mode="json")), 201
|
||||
return jsonify(SavedPropertyOut.from_saved(saved).model_dump(mode="json")), 201
|
||||
|
||||
|
||||
@client_bp.delete("/favorites/<property_id>")
|
||||
|
|
@ -107,3 +116,64 @@ def get_boletos():
|
|||
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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue