feat: add full project - backend, frontend, docker, specs and configs
This commit is contained in:
parent
b77c7d5a01
commit
e6cb06255b
24489 changed files with 61341 additions and 36 deletions
5
backend/app/models/__init__.py
Normal file
5
backend/app/models/__init__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# Importação explícita dos modelos para registro no metadata
|
||||
from .saved_property import SavedProperty
|
||||
from .visit_request import VisitRequest
|
||||
from .boleto import Boleto
|
||||
from .page_view import PageView
|
||||
25
backend/app/models/agent.py
Normal file
25
backend/app/models/agent.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
from app.extensions import db
|
||||
|
||||
|
||||
class Agent(db.Model):
|
||||
__tablename__ = "agents"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
name = db.Column(db.String(200), nullable=False)
|
||||
photo_url = db.Column(db.String(512), nullable=True)
|
||||
creci = db.Column(db.String(50), nullable=False)
|
||||
email = db.Column(db.String(200), nullable=False)
|
||||
phone = db.Column(db.String(30), nullable=False)
|
||||
bio = db.Column(db.Text, nullable=True)
|
||||
is_active = db.Column(db.Boolean, nullable=False, default=True)
|
||||
display_order = db.Column(db.Integer, nullable=False, default=0)
|
||||
created_at = db.Column(db.DateTime, nullable=False, server_default=db.func.now())
|
||||
updated_at = db.Column(
|
||||
db.DateTime,
|
||||
nullable=False,
|
||||
server_default=db.func.now(),
|
||||
onupdate=db.func.now(),
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Agent {self.name!r}>"
|
||||
30
backend/app/models/boleto.py
Normal file
30
backend/app/models/boleto.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import uuid
|
||||
from datetime import datetime
|
||||
from app.extensions import db
|
||||
|
||||
|
||||
class Boleto(db.Model):
|
||||
__tablename__ = "boletos"
|
||||
|
||||
id = db.Column(db.String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
user_id = db.Column(
|
||||
db.String(36),
|
||||
db.ForeignKey("client_users.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
index=True,
|
||||
)
|
||||
property_id = db.Column(
|
||||
db.UUID(as_uuid=True),
|
||||
db.ForeignKey("properties.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
index=True,
|
||||
)
|
||||
description = db.Column(db.String(200), nullable=False)
|
||||
amount = db.Column(db.Numeric(12, 2), nullable=False)
|
||||
due_date = db.Column(db.Date, nullable=False)
|
||||
status = db.Column(db.String(20), nullable=False, default="pending")
|
||||
url = db.Column(db.String(500), nullable=True)
|
||||
created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
|
||||
|
||||
user = db.relationship("ClientUser", backref=db.backref("boletos", lazy="select"))
|
||||
property = db.relationship("Property", foreign_keys=[property_id], lazy="joined")
|
||||
67
backend/app/models/catalog.py
Normal file
67
backend/app/models/catalog.py
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import uuid
|
||||
|
||||
from app.extensions import db
|
||||
|
||||
# Association table N:N between Property and Amenity — no model class needed
|
||||
property_amenity = db.Table(
|
||||
"property_amenity",
|
||||
db.Column(
|
||||
"property_id",
|
||||
db.UUID(as_uuid=True),
|
||||
db.ForeignKey("properties.id", ondelete="CASCADE"),
|
||||
primary_key=True,
|
||||
),
|
||||
db.Column(
|
||||
"amenity_id",
|
||||
db.Integer,
|
||||
db.ForeignKey("amenities.id", ondelete="CASCADE"),
|
||||
primary_key=True,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class PropertyType(db.Model):
|
||||
"""Hierarchical property type: Residencial > Apartamento"""
|
||||
|
||||
__tablename__ = "property_types"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
name = db.Column(db.String(100), nullable=False)
|
||||
slug = db.Column(db.String(120), unique=True, nullable=False, index=True)
|
||||
parent_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("property_types.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
index=True,
|
||||
)
|
||||
|
||||
# Self-referential relationship
|
||||
subtypes = db.relationship(
|
||||
"PropertyType",
|
||||
backref=db.backref("parent", remote_side=[id]),
|
||||
lazy="select",
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<PropertyType {self.slug!r}>"
|
||||
|
||||
|
||||
class Amenity(db.Model):
|
||||
"""Tagged feature/amenity for a property (characteristic, leisure, condo, security)"""
|
||||
|
||||
__tablename__ = "amenities"
|
||||
|
||||
GROUPS = ("caracteristica", "lazer", "condominio", "seguranca")
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
name = db.Column(db.String(100), nullable=False)
|
||||
slug = db.Column(db.String(120), unique=True, nullable=False, index=True)
|
||||
group = db.Column(
|
||||
db.Enum(
|
||||
"caracteristica", "lazer", "condominio", "seguranca", name="amenity_group"
|
||||
),
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Amenity {self.slug!r} group={self.group!r}>"
|
||||
22
backend/app/models/homepage.py
Normal file
22
backend/app/models/homepage.py
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
from app.extensions import db
|
||||
|
||||
|
||||
class HomepageConfig(db.Model):
|
||||
__tablename__ = "homepage_config"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
hero_headline = db.Column(db.String(120), nullable=False)
|
||||
hero_subheadline = db.Column(db.String(240), nullable=True)
|
||||
hero_cta_label = db.Column(db.String(40), nullable=False, default="Ver Imóveis")
|
||||
hero_cta_url = db.Column(db.String(200), nullable=False, default="/imoveis")
|
||||
featured_properties_limit = db.Column(db.Integer, nullable=False, default=6)
|
||||
hero_image_url = db.Column(db.String(512), nullable=True)
|
||||
updated_at = db.Column(
|
||||
db.DateTime,
|
||||
nullable=False,
|
||||
server_default=db.func.now(),
|
||||
onupdate=db.func.now(),
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<HomepageConfig id={self.id!r} headline={self.hero_headline!r}>"
|
||||
18
backend/app/models/imobiliaria.py
Normal file
18
backend/app/models/imobiliaria.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
from app.extensions import db
|
||||
|
||||
|
||||
class Imobiliaria(db.Model):
|
||||
__tablename__ = "imobiliarias"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
name = db.Column(db.String(200), nullable=False)
|
||||
logo_url = db.Column(db.String(512), nullable=True)
|
||||
website = db.Column(db.String(512), nullable=True)
|
||||
is_active = db.Column(db.Boolean, nullable=False, default=True)
|
||||
display_order = db.Column(db.Integer, nullable=False, default=0)
|
||||
created_at = db.Column(db.DateTime, nullable=False, server_default=db.func.now())
|
||||
|
||||
properties = db.relationship("Property", backref="imobiliaria", lazy="dynamic")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Imobiliaria {self.name!r}>"
|
||||
25
backend/app/models/lead.py
Normal file
25
backend/app/models/lead.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
from app.extensions import db
|
||||
|
||||
|
||||
class ContactLead(db.Model):
|
||||
__tablename__ = "contact_leads"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
property_id = db.Column(
|
||||
db.UUID(as_uuid=True),
|
||||
db.ForeignKey("properties.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
index=True,
|
||||
)
|
||||
name = db.Column(db.String(150), nullable=False)
|
||||
email = db.Column(db.String(254), nullable=False)
|
||||
phone = db.Column(db.String(20), nullable=True)
|
||||
message = db.Column(db.Text, nullable=False)
|
||||
created_at = db.Column(
|
||||
db.DateTime(timezone=True),
|
||||
nullable=False,
|
||||
server_default=db.func.now(),
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<ContactLead id={self.id} email={self.email!r}>"
|
||||
46
backend/app/models/location.py
Normal file
46
backend/app/models/location.py
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
from app.extensions import db
|
||||
|
||||
|
||||
class City(db.Model):
|
||||
"""City managed via admin panel."""
|
||||
|
||||
__tablename__ = "cities"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
name = db.Column(db.String(100), nullable=False)
|
||||
slug = db.Column(db.String(120), unique=True, nullable=False, index=True)
|
||||
state = db.Column(db.String(2), nullable=False) # UF: SP, RJ, MG, ...
|
||||
|
||||
neighborhoods = db.relationship(
|
||||
"Neighborhood",
|
||||
backref="city",
|
||||
order_by="Neighborhood.name",
|
||||
cascade="all, delete-orphan",
|
||||
lazy="select",
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<City {self.name!r}/{self.state!r}>"
|
||||
|
||||
|
||||
class Neighborhood(db.Model):
|
||||
"""Neighborhood (bairro) managed via admin panel."""
|
||||
|
||||
__tablename__ = "neighborhoods"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
name = db.Column(db.String(100), nullable=False)
|
||||
slug = db.Column(db.String(120), nullable=False, index=True)
|
||||
city_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("cities.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
index=True,
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
db.UniqueConstraint("slug", "city_id", name="uq_neighborhood_slug_city"),
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Neighborhood {self.name!r} city_id={self.city_id}>"
|
||||
21
backend/app/models/page_view.py
Normal file
21
backend/app/models/page_view.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
from datetime import datetime, timezone
|
||||
from app.extensions import db
|
||||
|
||||
|
||||
class PageView(db.Model):
|
||||
__tablename__ = "page_views"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
path = db.Column(db.String(512), nullable=False)
|
||||
property_id = db.Column(db.String(36), nullable=True, index=True)
|
||||
accessed_at = db.Column(
|
||||
db.DateTime(timezone=True),
|
||||
nullable=False,
|
||||
default=lambda: datetime.now(timezone.utc),
|
||||
index=True,
|
||||
)
|
||||
ip_hash = db.Column(db.String(64), nullable=True)
|
||||
user_agent = db.Column(db.String(512), nullable=True)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<PageView {self.path} at {self.accessed_at}>"
|
||||
91
backend/app/models/property.py
Normal file
91
backend/app/models/property.py
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import uuid
|
||||
|
||||
from app.extensions import db
|
||||
|
||||
|
||||
class Property(db.Model):
|
||||
__tablename__ = "properties"
|
||||
|
||||
id = db.Column(db.UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
title = db.Column(db.String(200), nullable=False)
|
||||
slug = db.Column(db.String(220), unique=True, nullable=False, index=True)
|
||||
address = db.Column(db.String(300), nullable=True)
|
||||
price = db.Column(db.Numeric(12, 2), nullable=False)
|
||||
condo_fee = db.Column(db.Numeric(10, 2), nullable=True)
|
||||
type = db.Column(
|
||||
db.Enum("venda", "aluguel", name="property_type"),
|
||||
nullable=False,
|
||||
)
|
||||
subtype_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("property_types.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
index=True,
|
||||
)
|
||||
bedrooms = db.Column(db.Integer, nullable=False, default=0)
|
||||
bathrooms = db.Column(db.Integer, nullable=False, default=0)
|
||||
parking_spots = db.Column(db.Integer, nullable=False, default=0)
|
||||
parking_spots_covered = db.Column(db.Integer, nullable=False, default=0)
|
||||
area_m2 = db.Column(db.Integer, nullable=False)
|
||||
city_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("cities.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
index=True,
|
||||
)
|
||||
neighborhood_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("neighborhoods.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
index=True,
|
||||
)
|
||||
is_featured = db.Column(db.Boolean, nullable=False, default=False)
|
||||
is_active = db.Column(db.Boolean, nullable=False, default=True)
|
||||
code = db.Column(db.String(30), unique=True, nullable=True)
|
||||
description = db.Column(db.Text, nullable=True)
|
||||
iptu_anual = db.Column(db.Numeric(12, 2), nullable=True)
|
||||
imobiliaria_id = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey("imobiliarias.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
index=True,
|
||||
)
|
||||
created_at = db.Column(db.DateTime, nullable=False, server_default=db.func.now())
|
||||
|
||||
photos = db.relationship(
|
||||
"PropertyPhoto",
|
||||
backref="property",
|
||||
order_by="PropertyPhoto.display_order",
|
||||
cascade="all, delete-orphan",
|
||||
lazy="select",
|
||||
)
|
||||
subtype = db.relationship("PropertyType", foreign_keys=[subtype_id], lazy="joined")
|
||||
city = db.relationship("City", foreign_keys=[city_id], lazy="joined")
|
||||
neighborhood = db.relationship(
|
||||
"Neighborhood", foreign_keys=[neighborhood_id], lazy="joined"
|
||||
)
|
||||
amenities = db.relationship(
|
||||
"Amenity",
|
||||
secondary="property_amenity",
|
||||
lazy="select",
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Property {self.slug!r}>"
|
||||
|
||||
|
||||
class PropertyPhoto(db.Model):
|
||||
__tablename__ = "property_photos"
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
property_id = db.Column(
|
||||
db.UUID(as_uuid=True),
|
||||
db.ForeignKey("properties.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
)
|
||||
url = db.Column(db.String(500), nullable=False)
|
||||
alt_text = db.Column(db.String(200), nullable=False, default="")
|
||||
display_order = db.Column(db.Integer, nullable=False, default=0)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<PropertyPhoto property_id={self.property_id!r} order={self.display_order}>"
|
||||
33
backend/app/models/saved_property.py
Normal file
33
backend/app/models/saved_property.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import uuid
|
||||
from datetime import datetime
|
||||
from app.extensions import db
|
||||
|
||||
|
||||
class SavedProperty(db.Model):
|
||||
__tablename__ = "saved_properties"
|
||||
|
||||
id = db.Column(db.String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
user_id = db.Column(
|
||||
db.String(36),
|
||||
db.ForeignKey("client_users.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
index=True,
|
||||
)
|
||||
property_id = db.Column(
|
||||
db.UUID(as_uuid=True),
|
||||
db.ForeignKey("properties.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
index=True,
|
||||
)
|
||||
created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
|
||||
|
||||
__table_args__ = (
|
||||
db.UniqueConstraint(
|
||||
"user_id", "property_id", name="uq_saved_properties_user_property"
|
||||
),
|
||||
)
|
||||
|
||||
user = db.relationship(
|
||||
"ClientUser", backref=db.backref("saved_properties", lazy="joined")
|
||||
)
|
||||
property = db.relationship("Property", foreign_keys=[property_id], lazy="joined")
|
||||
38
backend/app/models/user.py
Normal file
38
backend/app/models/user.py
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import uuid
|
||||
from datetime import datetime
|
||||
from app.extensions import db
|
||||
|
||||
|
||||
class ClientUser(db.Model):
|
||||
__tablename__ = "client_users"
|
||||
|
||||
id = db.Column(
|
||||
db.String(36),
|
||||
primary_key=True,
|
||||
default=lambda: str(uuid.uuid4()),
|
||||
)
|
||||
name = db.Column(db.String(150), nullable=False)
|
||||
email = db.Column(db.String(254), unique=True, nullable=False, index=True)
|
||||
password_hash = db.Column(db.String(100), nullable=False)
|
||||
role = db.Column(db.String(20), nullable=False, default="client")
|
||||
created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
|
||||
|
||||
# Contato
|
||||
phone = db.Column(db.String(20), nullable=True)
|
||||
whatsapp = db.Column(db.String(20), nullable=True)
|
||||
|
||||
# Dados pessoais
|
||||
cpf = db.Column(db.String(14), nullable=True)
|
||||
birth_date = db.Column(db.Date, nullable=True)
|
||||
|
||||
# Endereço
|
||||
address_street = db.Column(db.String(200), nullable=True)
|
||||
address_number = db.Column(db.String(20), nullable=True)
|
||||
address_complement = db.Column(db.String(100), nullable=True)
|
||||
address_neighborhood = db.Column(db.String(100), nullable=True)
|
||||
address_city = db.Column(db.String(100), nullable=True)
|
||||
address_state = db.Column(db.String(2), nullable=True)
|
||||
address_zip = db.Column(db.String(9), nullable=True)
|
||||
|
||||
# Observações internas (admin)
|
||||
notes = db.Column(db.Text, nullable=True)
|
||||
30
backend/app/models/visit_request.py
Normal file
30
backend/app/models/visit_request.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import uuid
|
||||
from datetime import datetime
|
||||
from app.extensions import db
|
||||
|
||||
|
||||
class VisitRequest(db.Model):
|
||||
__tablename__ = "visit_requests"
|
||||
|
||||
id = db.Column(db.String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
|
||||
user_id = db.Column(
|
||||
db.String(36),
|
||||
db.ForeignKey("client_users.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
index=True,
|
||||
)
|
||||
property_id = db.Column(
|
||||
db.UUID(as_uuid=True),
|
||||
db.ForeignKey("properties.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
index=True,
|
||||
)
|
||||
message = db.Column(db.Text, nullable=False)
|
||||
status = db.Column(db.String(20), nullable=False, default="pending")
|
||||
scheduled_at = db.Column(db.DateTime, nullable=True)
|
||||
created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
|
||||
|
||||
user = db.relationship(
|
||||
"ClientUser", backref=db.backref("visit_requests", lazy="select")
|
||||
)
|
||||
property = db.relationship("Property", foreign_keys=[property_id], lazy="joined")
|
||||
Loading…
Add table
Add a link
Reference in a new issue