feat: Agente-Marketing initial commit

This commit is contained in:
2026-07-03 12:23:34 -04:00
commit 293522436a
52 changed files with 13522 additions and 0 deletions
+135
View File
@@ -0,0 +1,135 @@
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Casa Hunter FL{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<style>
:root {
--primary: #1a3a5c;
--accent: #e8a020;
--light-bg: #f4f7fb;
}
body { background: var(--light-bg); font-family: 'Segoe UI', sans-serif; }
.navbar { background: var(--primary) !important; }
.navbar-brand { color: var(--accent) !important; font-weight: 700; font-size: 1.3rem; }
.nav-link { color: rgba(255,255,255,.85) !important; }
.nav-link:hover, .nav-link.active { color: var(--accent) !important; }
.card { border: none; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,.07); }
.score-badge { font-size: .75rem; font-weight: 700; padding: 4px 10px; border-radius: 20px; }
.score-high { background: #d4edda; color: #155724; }
.score-mid { background: #fff3cd; color: #856404; }
.score-low { background: #f8d7da; color: #721c24; }
.prop-price { font-size: 1.4rem; font-weight: 700; color: var(--primary); }
.source-tag { font-size: .7rem; background: var(--primary); color: #fff; padding: 2px 8px; border-radius: 10px; }
.lender-card .match-pct { font-size: 2rem; font-weight: 800; color: var(--accent); }
.btn-primary { background: var(--primary); border-color: var(--primary); }
.btn-primary:hover { background: #0f2540; }
.btn-accent { background: var(--accent); border-color: var(--accent); color: #fff; }
.stat-card { text-align: center; padding: 1.2rem; }
.stat-card .number { font-size: 2rem; font-weight: 800; color: var(--primary); }
.stat-card .label { font-size: .8rem; color: #6c757d; text-transform: uppercase; letter-spacing: 1px; }
.action-kit { background: #e8f4fd; border-left: 4px solid var(--primary); border-radius: 8px; padding: 1rem; }
footer { background: var(--primary); color: rgba(255,255,255,.6); padding: 1rem 0; margin-top: 3rem; font-size: .85rem; }
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg">
<div class="container">
<a class="navbar-brand" href="/"><i class="fas fa-home me-2"></i>Casa Hunter FL</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#nav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="nav">
<ul class="navbar-nav ms-auto">
<li class="nav-item"><a class="nav-link {% if request.path=='/' %}active{% endif %}" href="/"><i class="fas fa-chart-line me-1"></i>Dashboard</a></li>
<li class="nav-item"><a class="nav-link {% if '/properties' in request.path %}active{% endif %}" href="/properties"><i class="fas fa-building me-1"></i>Propiedades</a></li>
<li class="nav-item"><a class="nav-link {% if '/lenders' in request.path %}active{% endif %}" href="/lenders"><i class="fas fa-handshake me-1"></i>Lenders</a></li>
<li class="nav-item"><a class="nav-link {% if '/settings' in request.path %}active{% endif %}" href="/settings"><i class="fas fa-sliders-h me-1"></i>Preferencias</a></li>
<li class="nav-item ms-2">
<button class="btn btn-sm btn-accent" onclick="runScan()"><i class="fas fa-sync me-1"></i>Buscar Ahora</button>
</li>
</ul>
</div>
</div>
</nav>
<div class="container my-4">
{% with messages = get_flashed_messages(with_categories=true) %}
{% for cat, msg in messages %}
<div class="alert alert-{{ cat }} alert-dismissible fade show"><i class="fas fa-info-circle me-2"></i>{{ msg }}<button type="button" class="btn-close" data-bs-dismiss="alert"></button></div>
{% endfor %}
{% endwith %}
{% block content %}{% endblock %}
</div>
<footer><div class="container text-center">Casa Hunter FL &mdash; Tu buscador personal de oportunidades inmobiliarias &bull; Prisa Yachts LLC</div></footer>
<div class="modal fade" id="scanModal" tabindex="-1">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-body text-center py-4">
<div class="spinner-border text-primary mb-3"></div>
<p class="mb-1 fw-bold">Buscando oportunidades...</p>
<small class="text-muted">HUD · Fannie Mae · Zillow · Remates</small>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
let _scanPoll = null;
function runScan() {
const btn = document.querySelector('[onclick="runScan()"]');
if (btn) { btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Buscando...'; }
const prog = document.getElementById('scan-progress');
const msg = document.getElementById('scan-msg');
if (prog) prog.style.display = 'block';
if (msg) msg.textContent = 'Abriendo Chrome y buscando propiedades...';
fetch('/scan', {method:'POST'})
.then(r => r.json())
.then(data => {
if (msg) msg.textContent = data.status === 'already_running'
? 'Ya hay un scan en progreso...'
: 'Chrome abierto — buscando en ' + (data.cities || []).length + ' ciudades...';
_scanPoll = setInterval(pollScanStatus, 5000);
})
.catch(e => {
if (btn) { btn.disabled = false; btn.innerHTML = '<i class="fas fa-search me-2"></i>Buscar Ahora'; }
if (msg) msg.textContent = 'Error: ' + e;
});
}
function pollScanStatus() {
fetch('/scan/status')
.then(r => r.json())
.then(data => {
const msg = document.getElementById('scan-msg');
const bar = document.getElementById('scan-bar');
const btn = document.querySelector('[onclick="runScan()"]');
if (msg) msg.textContent = data.progress || 'Buscando...';
if (!data.running) {
clearInterval(_scanPoll);
if (btn) { btn.disabled = false; btn.innerHTML = '<i class="fas fa-search me-2"></i>Buscar Ahora'; }
if (bar) bar.classList.remove('progress-bar-animated');
if (data.new > 0) {
if (msg) msg.textContent = data.new + ' propiedades nuevas! Recargando...';
setTimeout(() => location.reload(), 2000);
} else {
setTimeout(() => {
const p = document.getElementById('scan-progress');
if (p) p.style.display = 'none';
}, 5000);
}
}
});
}
</script>
{% block scripts %}{% endblock %}
</body>
</html>
+186
View File
@@ -0,0 +1,186 @@
{% extends "base.html" %}
{% block title %}Dashboard — Casa Hunter FL{% endblock %}
{% block content %}
<!-- Header -->
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h2 class="fw-bold mb-1" style="color:var(--primary)"><i class="fas fa-map-marker-alt me-2" style="color:var(--accent)"></i>Oportunidades en Costa de Florida</h2>
<p class="text-muted mb-0">Stuart → Vero Beach → Melbourne → Daytona → St. Augustine → Jacksonville &bull; Hasta $200,000</p>
</div>
<button class="btn btn-accent" onclick="runScan()"><i class="fas fa-search me-2"></i>Buscar Ahora</button>
</div>
<!-- Stats -->
<div class="row g-3 mb-4">
<div class="col-6 col-md-3">
<div class="card stat-card">
<div class="number">{{ total }}</div>
<div class="label">Propiedades</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card stat-card">
<div class="number">{{ favorites }}</div>
<div class="label">Favoritas</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card stat-card">
<div class="number">$50K</div>
<div class="label">Down Payment</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card stat-card">
<div class="number">8</div>
<div class="label">Lenders Listos</div>
</div>
</div>
</div>
<!-- Top Properties -->
<h5 class="fw-bold mb-3" style="color:var(--primary)"><i class="fas fa-star me-2" style="color:var(--accent)"></i>Mejores Oportunidades</h5>
{% if properties %}
<div class="row g-3 mb-4">
{% for p in properties %}
<div class="col-12 col-md-6 col-lg-4">
<div class="card h-100">
{% if p.image_url %}
<img src="{{ p.image_url }}" class="card-img-top" style="height:160px;object-fit:cover;border-radius:12px 12px 0 0" alt="">
{% else %}
<div style="height:100px;background:linear-gradient(135deg,#1a3a5c,#2d6a9f);border-radius:12px 12px 0 0;display:flex;align-items:center;justify-content:center">
<i class="fas fa-home fa-2x text-white opacity-50"></i>
</div>
{% endif %}
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-2">
<span class="source-tag">{{ p.source }}</span>
<span class="score-badge {% if p.score >= 75 %}score-high{% elif p.score >= 50 %}score-mid{% else %}score-low{% endif %}">
Score: {{ p.score }}/100
</span>
</div>
<p class="prop-price mb-1">${{ "{:,.0f}".format(p.price) }}</p>
<p class="text-muted small mb-1"><i class="fas fa-map-marker-alt me-1"></i>{{ p.address }}</p>
<p class="text-muted small mb-2">{{ p.city }}{% if p.county %}, {{ p.county }} Co.{% endif %}</p>
<div class="d-flex gap-2 text-muted small mb-3">
{% if p.beds %}<span><i class="fas fa-bed me-1"></i>{{ p.beds }} hab.</span>{% endif %}
{% if p.baths %}<span><i class="fas fa-bath me-1"></i>{{ p.baths }} baños</span>{% endif %}
{% if p.sqft %}<span><i class="fas fa-ruler-combined me-1"></i>{{ p.sqft }} sf</span>{% endif %}
</div>
<div class="d-flex gap-2">
<a href="/property/{{ p.id }}" class="btn btn-primary btn-sm flex-fill">Ver Detalle</a>
{% if p.url %}
<a href="{{ p.url }}" target="_blank" class="btn btn-outline-secondary btn-sm"><i class="fas fa-external-link-alt"></i></a>
{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<div class="text-center">
<a href="/properties" class="btn btn-outline-primary">Ver todas las propiedades <i class="fas fa-arrow-right ms-1"></i></a>
</div>
{% else %}
<div class="card text-center py-5">
<div class="card-body">
<i class="fas fa-search fa-3x mb-3" style="color:var(--accent)"></i>
<h5>No hay propiedades aún</h5>
<p class="text-muted">Haz clic en <strong>Buscar Ahora</strong> para encontrar oportunidades en Florida</p>
<button class="btn btn-primary" onclick="runScan()"><i class="fas fa-sync me-2"></i>Iniciar Búsqueda</button>
</div>
</div>
{% endif %}
<!-- Top Lenders -->
<h5 class="fw-bold mt-4 mb-3" style="color:var(--primary)"><i class="fas fa-handshake me-2" style="color:var(--accent)"></i>Lenders Recomendados para Tu Perfil</h5>
<div class="row g-3">
{% for l in top_lenders %}
<div class="col-12 col-md-4">
<div class="card lender-card h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-2">
<h6 class="fw-bold mb-0" style="color:var(--primary)">{{ l.name }}</h6>
<span class="match-pct">{{ l.match_score }}%</span>
</div>
<span class="badge bg-light text-dark border mb-2">{{ l.loan_type }}</span>
<p class="text-muted small">{{ l.specialties[:100] }}{% if l.specialties|length > 100 %}...{% endif %}</p>
<a href="/lenders" class="btn btn-sm btn-primary w-100">Ver Script de Contacto</a>
</div>
</div>
</div>
{% endfor %}
</div>
{% if last_scan %}
<p class="text-muted text-end small mt-3"><i class="fas fa-clock me-1"></i>Última búsqueda: {{ last_scan.ran_at.strftime('%d/%m/%Y %H:%M') }}</p>
{% endif %}
<!-- Scan progress bar -->
<div id="scan-progress" class="mt-3" style="display:none">
<div class="alert alert-info d-flex align-items-center gap-2">
<div class="spinner-border spinner-border-sm" role="status"></div>
<span id="scan-msg">Iniciando búsqueda...</span>
</div>
<div class="progress" style="height:6px">
<div id="scan-bar" class="progress-bar progress-bar-striped progress-bar-animated bg-success" style="width:100%"></div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
let _scanPoll = null;
function runScan() {
const btn = document.querySelector('[onclick="runScan()"]');
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Buscando...';
document.getElementById('scan-progress').style.display = 'block';
document.getElementById('scan-msg').textContent = 'Abriendo Chrome y buscando propiedades...';
fetch('/scan', {method:'POST'})
.then(r => r.json())
.then(data => {
if (data.status === 'already_running') {
document.getElementById('scan-msg').textContent = 'Ya hay un scan en progreso...';
}
// Poll for status every 5 seconds
_scanPoll = setInterval(pollScanStatus, 5000);
})
.catch(e => {
document.getElementById('scan-msg').textContent = 'Error iniciando scan: ' + e;
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-search me-2"></i>Buscar Ahora';
});
}
function pollScanStatus() {
fetch('/scan/status')
.then(r => r.json())
.then(data => {
document.getElementById('scan-msg').textContent = data.progress || 'Buscando...';
if (!data.running) {
clearInterval(_scanPoll);
const btn = document.querySelector('[onclick="runScan()"]');
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-search me-2"></i>Buscar Ahora';
if (data.new > 0) {
document.getElementById('scan-msg').textContent =
`${data.new} propiedades nuevas encontradas. Recargando...`;
setTimeout(() => location.reload(), 2000);
} else {
document.getElementById('scan-msg').textContent =
data.progress || 'Busqueda completada.';
document.getElementById('scan-bar').classList.remove('progress-bar-animated');
setTimeout(() => {
document.getElementById('scan-progress').style.display = 'none';
}, 4000);
}
}
});
}
</script>
{% endblock %}
+104
View File
@@ -0,0 +1,104 @@
{% extends "base.html" %}
{% block title %}Lenders — Casa Hunter FL{% endblock %}
{% block content %}
<h2 class="fw-bold mb-1" style="color:var(--primary)"><i class="fas fa-handshake me-2" style="color:var(--accent)"></i>Lenders Recomendados</h2>
<p class="text-muted mb-4">Ordenados por compatibilidad con tu perfil: residente nuevo, self-employed, $50K down, hasta $200K</p>
<div class="row g-3">
{% for l in lenders %}
<div class="col-12 col-lg-6">
<div class="card h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-2">
<div>
<h5 class="fw-bold mb-0" style="color:var(--primary)">{{ l.name }}</h5>
<span class="badge border text-dark bg-light">{{ l.loan_type }}</span>
</div>
<div class="text-end">
<div class="lender-card"><span class="match-pct">{{ l.match_score }}%</span></div>
<small class="text-muted">compatibilidad</small>
</div>
</div>
<p class="small text-muted mb-2"><i class="fas fa-check-circle text-success me-1"></i>{{ l.specialties }}</p>
<div class="row text-center g-1 mb-3">
<div class="col-4">
<div class="bg-light rounded p-1">
<small class="text-muted d-block">Down mín.</small>
<strong class="small">{{ l.min_down_pct|int }}%</strong>
</div>
</div>
<div class="col-4">
<div class="bg-light rounded p-1">
<small class="text-muted d-block">Residente nuevo</small>
<strong class="small">{% if l.accepts_new_resident %}<i class="fas fa-check text-success"></i>{% else %}<i class="fas fa-times text-danger"></i>{% endif %}</strong>
</div>
</div>
<div class="col-4">
<div class="bg-light rounded p-1">
<small class="text-muted d-block">Self-employed</small>
<strong class="small">{% if l.accepts_self_employed %}<i class="fas fa-check text-success"></i>{% else %}<i class="fas fa-times text-danger"></i>{% endif %}</strong>
</div>
</div>
</div>
{% if l.notes %}
<p class="small text-muted mb-2"><i class="fas fa-info-circle me-1"></i>{{ l.notes }}</p>
{% endif %}
<!-- Contact Script -->
<div class="action-kit small mb-3">
<strong><i class="fas fa-phone me-1"></i>Guión de contacto:</strong><br>
{{ l.contact_script }}
</div>
<div class="d-flex gap-2">
<a href="{{ l.website }}" target="_blank" class="btn btn-primary btn-sm flex-fill">
<i class="fas fa-globe me-1"></i>Visitar Web
</a>
<button class="btn btn-outline-secondary btn-sm" onclick="copyText('script-{{ l.id }}')">
<i class="fas fa-copy"></i>
</button>
</div>
<span id="script-{{ l.id }}" class="d-none">{{ l.contact_script }}</span>
</div>
</div>
</div>
{% endfor %}
</div>
<!-- My Profile Box -->
<div class="card mt-4" style="border-left:4px solid var(--accent)">
<div class="card-body">
<h6 class="fw-bold mb-3" style="color:var(--primary)"><i class="fas fa-user me-2" style="color:var(--accent)"></i>Tu Perfil — Lo que presentas a cada lender</h6>
<div class="row g-2 small">
<div class="col-12 col-md-6">
<ul class="mb-0">
<li><strong>Estatus:</strong> Residente permanente</li>
<li><strong>Tiempo en USA:</strong> Menos de 2 años</li>
<li><strong>Trabajo:</strong> Contratista independiente — Eléctrica marina/naval</li>
<li><strong>Tarifa:</strong> $120/hora</li>
</ul>
</div>
<div class="col-12 col-md-6">
<ul class="mb-0">
<li><strong>Down payment disponible:</strong> $50,000</li>
<li><strong>Presupuesto máximo:</strong> $200,000</li>
<li><strong>Zona:</strong> Costa de FL — Stuart hasta Jacksonville</li>
<li><strong>Préstamo ideal:</strong> Non-QM, Bank Statement, o ITIN</li>
</ul>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function copyText(id) {
const text = document.getElementById(id).textContent;
navigator.clipboard.writeText(text).then(() => alert('Script copiado'));
}
</script>
{% endblock %}
+99
View File
@@ -0,0 +1,99 @@
{% extends "base.html" %}
{% block title %}Propiedades — Casa Hunter FL{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-3">
<h4 class="fw-bold mb-0" style="color:var(--primary)"><i class="fas fa-building me-2" style="color:var(--accent)"></i>Propiedades en Oportunidad</h4>
<button class="btn btn-accent btn-sm" onclick="runScan()"><i class="fas fa-sync me-1"></i>Actualizar</button>
</div>
<!-- Filters -->
<div class="card mb-4">
<div class="card-body py-2">
<form method="get" class="row g-2 align-items-end">
<div class="col-6 col-md-3">
<label class="form-label small fw-bold mb-1">Ciudad</label>
<input type="text" name="city" class="form-control form-control-sm" placeholder="Vero Beach..." value="{{ city_filter }}">
</div>
<div class="col-6 col-md-3">
<label class="form-label small fw-bold mb-1">Fuente</label>
<select name="source" class="form-select form-select-sm">
<option value="">Todas</option>
{% for s in sources %}<option value="{{ s }}" {% if s==current_source %}selected{% endif %}>{{ s }}</option>{% endfor %}
</select>
</div>
<div class="col-6 col-md-2">
<label class="form-label small fw-bold mb-1">Score mín.</label>
<select name="min_score" class="form-select form-select-sm">
<option value="0">Todos</option>
<option value="70">70+</option>
<option value="80">80+</option>
<option value="90">90+</option>
</select>
</div>
<div class="col-6 col-md-2">
<label class="form-label small fw-bold mb-1">Ordenar</label>
<select name="sort" class="form-select form-select-sm">
<option value="score" {% if current_sort=='score' %}selected{% endif %}>Mejor score</option>
<option value="price_asc" {% if current_sort=='price_asc' %}selected{% endif %}>Precio ↑</option>
<option value="price_desc" {% if current_sort=='price_desc' %}selected{% endif %}>Precio ↓</option>
<option value="newest" {% if current_sort=='newest' %}selected{% endif %}>Más reciente</option>
</select>
</div>
<div class="col-6 col-md-1">
<button type="submit" class="btn btn-primary btn-sm w-100"><i class="fas fa-filter"></i></button>
</div>
<div class="col-6 col-md-1">
<a href="/properties?favorites=1" class="btn btn-outline-warning btn-sm w-100"><i class="fas fa-star"></i></a>
</div>
</form>
</div>
</div>
{% if properties %}
<p class="text-muted small mb-3"><strong>{{ properties|length }}</strong> propiedades encontradas</p>
<div class="row g-3">
{% for p in properties %}
<div class="col-12 col-md-6 col-xl-4">
<div class="card h-100 {% if p.is_favorite %}border-warning{% endif %}">
{% if p.image_url %}
<img src="{{ p.image_url }}" class="card-img-top" style="height:150px;object-fit:cover;border-radius:12px 12px 0 0" alt="">
{% else %}
<div style="height:80px;background:linear-gradient(135deg,#1a3a5c,#2d6a9f);border-radius:12px 12px 0 0;display:flex;align-items:center;justify-content:center">
<i class="fas fa-home fa-2x text-white opacity-50"></i>
</div>
{% endif %}
<div class="card-body">
<div class="d-flex justify-content-between mb-2">
<span class="source-tag">{{ p.source }}</span>
<span class="score-badge {% if p.score >= 75 %}score-high{% elif p.score >= 50 %}score-mid{% else %}score-low{% endif %}">{{ p.score }}/100</span>
</div>
<div class="prop-price mb-1">${{ "{:,.0f}".format(p.price) }}</div>
<p class="text-muted small mb-1">{{ p.address }}</p>
<p class="text-muted small mb-2">{{ p.city }}{% if p.county %}, {{ p.county }}{% endif %}</p>
<div class="d-flex gap-2 text-muted small mb-3">
{% if p.beds %}<span><i class="fas fa-bed me-1"></i>{{ p.beds }}</span>{% endif %}
{% if p.baths %}<span><i class="fas fa-bath me-1"></i>{{ p.baths }}</span>{% endif %}
{% if p.sqft %}<span><i class="fas fa-ruler-combined me-1"></i>{{ p.sqft }}sf</span>{% endif %}
</div>
<div class="d-flex gap-1">
<a href="/property/{{ p.id }}" class="btn btn-primary btn-sm flex-fill">Ver Detalle</a>
{% if p.url %}<a href="{{ p.url }}" target="_blank" class="btn btn-outline-secondary btn-sm"><i class="fas fa-external-link-alt"></i></a>{% endif %}
{% if p.is_favorite %}<span class="btn btn-warning btn-sm"><i class="fas fa-star"></i></span>{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="card text-center py-5">
<div class="card-body">
<i class="fas fa-search fa-3x mb-3" style="color:var(--accent)"></i>
<h5>No hay propiedades</h5>
<p class="text-muted">Haz clic en Buscar Ahora para encontrar oportunidades</p>
<button class="btn btn-primary" onclick="runScan()"><i class="fas fa-sync me-2"></i>Buscar Ahora</button>
</div>
</div>
{% endif %}
{% endblock %}
@@ -0,0 +1,224 @@
{% extends "base.html" %}
{% block title %}{{ prop.address }} — Casa Hunter FL{% endblock %}
{% block content %}
<nav aria-label="breadcrumb" class="mb-3">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/">Dashboard</a></li>
<li class="breadcrumb-item"><a href="/properties">Propiedades</a></li>
<li class="breadcrumb-item active">Detalle</li>
</ol>
</nav>
<div class="row g-4">
<!-- Left Column -->
<div class="col-12 col-lg-8">
<div class="card mb-3">
{% if prop.image_url %}
<img src="{{ prop.image_url }}" class="card-img-top" style="height:280px;object-fit:cover;border-radius:12px 12px 0 0" alt="">
{% else %}
<div style="height:180px;background:linear-gradient(135deg,#1a3a5c,#2d6a9f);border-radius:12px 12px 0 0;display:flex;align-items:center;justify-content:center">
<i class="fas fa-home fa-4x text-white opacity-40"></i>
</div>
{% endif %}
<div class="card-body">
<div class="d-flex flex-wrap justify-content-between align-items-start gap-2 mb-3">
<div>
<span class="source-tag me-2">{{ prop.source }}</span>
<span class="badge bg-secondary">{{ prop.status }}</span>
</div>
<span class="score-badge {% if prop.score >= 75 %}score-high{% elif prop.score >= 50 %}score-mid{% else %}score-low{% endif %} fs-6">
Score: {{ prop.score }}/100
</span>
</div>
<h3 class="prop-price mb-1">${{ "{:,.0f}".format(prop.price) }}</h3>
<p class="text-muted mb-1"><i class="fas fa-map-marker-alt me-1"></i>{{ prop.address }}</p>
<p class="text-muted small mb-3">{{ prop.city }}{% if prop.county %}, {{ prop.county }} County{% endif %}, FL {{ prop.zipcode }}</p>
<div class="row text-center g-2 mb-3">
{% if prop.beds %}
<div class="col-4">
<div class="border rounded p-2">
<i class="fas fa-bed text-muted d-block mb-1"></i>
<strong>{{ prop.beds }}</strong><br><small class="text-muted">Habitaciones</small>
</div>
</div>
{% endif %}
{% if prop.baths %}
<div class="col-4">
<div class="border rounded p-2">
<i class="fas fa-bath text-muted d-block mb-1"></i>
<strong>{{ prop.baths }}</strong><br><small class="text-muted">Baños</small>
</div>
</div>
{% endif %}
{% if prop.sqft %}
<div class="col-4">
<div class="border rounded p-2">
<i class="fas fa-ruler-combined text-muted d-block mb-1"></i>
<strong>{{ prop.sqft }}</strong><br><small class="text-muted">Sq Ft</small>
</div>
</div>
{% endif %}
</div>
<div class="d-flex gap-2 flex-wrap">
{% if prop.url %}
<a href="{{ prop.url }}" target="_blank" class="btn btn-primary"><i class="fas fa-external-link-alt me-2"></i>Ver en {{ prop.source }}</a>
{% endif %}
<button class="btn {% if prop.is_favorite %}btn-warning{% else %}btn-outline-warning{% endif %}" onclick="toggleFav({{ prop.id }}, this)">
<i class="fas fa-star me-1"></i>{% if prop.is_favorite %}Guardada{% else %}Guardar{% endif %}
</button>
</div>
</div>
</div>
<!-- AI Analysis -->
<div class="card mb-3">
<div class="card-body">
<h6 class="fw-bold mb-3" style="color:var(--primary)"><i class="fas fa-robot me-2" style="color:var(--accent)"></i>Análisis con IA (qwen2.5)</h6>
<div id="ai-result">
{% if prop.ai_analysis %}
<div class="action-kit">{{ prop.ai_analysis }}</div>
{% else %}
<p class="text-muted">Aún no se ha analizado esta propiedad.</p>
{% endif %}
</div>
<button class="btn btn-sm btn-primary mt-3" onclick="runAnalysis({{ prop.id }})">
<i class="fas fa-magic me-1"></i>{% if prop.ai_analysis %}Re-analizar{% else %}Analizar con IA{% endif %}
</button>
</div>
</div>
<!-- Notes -->
<div class="card">
<div class="card-body">
<h6 class="fw-bold mb-2" style="color:var(--primary)"><i class="fas fa-sticky-note me-2"></i>Mis Notas</h6>
<textarea class="form-control mb-2" id="notes" rows="3" placeholder="Agrega notas sobre esta propiedad...">{{ prop.notes or '' }}</textarea>
<button class="btn btn-sm btn-outline-primary" onclick="saveNotes({{ prop.id }})"><i class="fas fa-save me-1"></i>Guardar Notas</button>
</div>
</div>
</div>
<!-- Right Column: Action Kit -->
<div class="col-12 col-lg-4">
<!-- Action Kit -->
<div class="card mb-3" style="border-left:4px solid var(--accent)">
<div class="card-body">
<h6 class="fw-bold mb-3" style="color:var(--primary)"><i class="fas fa-clipboard-list me-2" style="color:var(--accent)"></i>Kit de Acción</h6>
<p class="small fw-bold text-muted mb-2">DOCUMENTOS A PREPARAR</p>
<ul class="small mb-3">
<li>Green card o visa + pasaporte</li>
<li>12-24 meses de estados de cuenta bancarios</li>
<li>Comprobante de ingresos (facturas de trabajo)</li>
<li>Carta del landlord confirmando pagos de renta</li>
<li>Carta explicando tu situación laboral (self-employed)</li>
<li>Evidencia de los $50,000 disponibles (bank statement)</li>
</ul>
<p class="small fw-bold text-muted mb-2">PASOS A SEGUIR</p>
<ol class="small mb-3">
<li>Llama a 2-3 lenders de la lista (empieza con Heart Mortgage y Jhenesis)</li>
<li>Pide una pre-qualification letter</li>
<li>Contrata un inspector de propiedades ($300-500)</li>
<li>Si es HUD/Fannie Mae, necesitas un agente de bienes raíces aprobado</li>
<li>Presenta oferta con pre-qual letter y prueba de fondos</li>
</ol>
<div class="bg-light rounded p-2 small">
<strong><i class="fas fa-calculator me-1"></i>Estimado para esta propiedad:</strong><br>
Down payment (25%): <strong>${{ "{:,.0f}".format(prop.price * 0.25) }}</strong><br>
Préstamo estimado: <strong>${{ "{:,.0f}".format(prop.price * 0.75) }}</strong><br>
Cuota aprox (7.5%, 30 años): <strong>${{ "{:,.0f}".format(prop.price * 0.75 * 0.007) }}/mes</strong>
</div>
</div>
</div>
<!-- Recommended Lenders -->
<div class="card">
<div class="card-body">
<h6 class="fw-bold mb-3" style="color:var(--primary)"><i class="fas fa-handshake me-2" style="color:var(--accent)"></i>Lenders para Esta Compra</h6>
{% for l in lenders[:4] %}
<div class="border rounded p-2 mb-2">
<div class="d-flex justify-content-between align-items-center">
<strong class="small">{{ l.name }}</strong>
<span class="badge" style="background:var(--accent);color:#fff">{{ l.match_score }}%</span>
</div>
<small class="text-muted">{{ l.loan_type }}</small><br>
<a href="{{ l.website }}" target="_blank" class="btn btn-xs btn-outline-primary btn-sm mt-1" style="font-size:.7rem;padding:2px 8px">
<i class="fas fa-globe me-1"></i>Visitar
</a>
<button class="btn btn-xs btn-outline-secondary btn-sm mt-1 ms-1" style="font-size:.7rem;padding:2px 8px"
onclick="showScript('{{ l.name|e }}', `{{ l.contact_script|e }}`)">
<i class="fas fa-phone me-1"></i>Script
</button>
</div>
{% endfor %}
<a href="/lenders" class="btn btn-sm btn-outline-primary w-100 mt-1">Ver todos los lenders</a>
</div>
</div>
</div>
</div>
<!-- Script Modal -->
<div class="modal fade" id="scriptModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h6 class="modal-title fw-bold" id="scriptTitle"></h6>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p class="text-muted small mb-2">Usa este guión cuando los contactes:</p>
<div class="action-kit" id="scriptContent"></div>
</div>
<div class="modal-footer">
<button onclick="copyScript()" class="btn btn-sm btn-primary"><i class="fas fa-copy me-1"></i>Copiar</button>
<button type="button" class="btn btn-sm btn-secondary" data-bs-dismiss="modal">Cerrar</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function toggleFav(id, btn) {
fetch(`/property/${id}/favorite`, {method:'POST'})
.then(r=>r.json())
.then(d => {
btn.className = d.is_favorite ? 'btn btn-warning' : 'btn btn-outline-warning';
btn.innerHTML = `<i class="fas fa-star me-1"></i>${d.is_favorite ? 'Guardada' : 'Guardar'}`;
});
}
function runAnalysis(id) {
document.getElementById('ai-result').innerHTML = '<div class="text-center py-3"><div class="spinner-border spinner-border-sm me-2"></div>Analizando con qwen2.5...</div>';
fetch(`/property/${id}/analyze`, {method:'POST'})
.then(r=>r.json())
.then(d => {
const container = document.createElement('div');
container.className = 'action-kit';
container.textContent = d.analysis;
const wrapper = document.getElementById('ai-result');
wrapper.innerHTML = '';
wrapper.appendChild(container);
});
}
function saveNotes(id) {
const notes = document.getElementById('notes').value;
fetch(`/property/${id}/notes`, {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({notes})})
.then(()=>alert('Notas guardadas'));
}
function showScript(name, script) {
document.getElementById('scriptTitle').textContent = 'Script: ' + name;
document.getElementById('scriptContent').textContent = script;
new bootstrap.Modal(document.getElementById('scriptModal')).show();
}
function copyScript() {
navigator.clipboard.writeText(document.getElementById('scriptContent').textContent);
alert('Script copiado al portapapeles');
}
</script>
{% endblock %}
+181
View File
@@ -0,0 +1,181 @@
{% extends "base.html" %}
{% block title %}Preferencias — Casa Hunter FL{% endblock %}
{% block content %}
<h2 class="fw-bold mb-1" style="color:var(--primary)"><i class="fas fa-sliders-h me-2" style="color:var(--accent)"></i>Preferencias de Búsqueda</h2>
<p class="text-muted mb-4">Configura dónde y hasta cuánto buscar. La próxima búsqueda usará estos ajustes.</p>
<div class="row g-4">
<!-- Presupuesto y Down Payment -->
<div class="col-12 col-md-4">
<div class="card h-100">
<div class="card-body">
<h6 class="fw-bold mb-3" style="color:var(--primary)"><i class="fas fa-dollar-sign me-2" style="color:var(--accent)"></i>Mi Presupuesto</h6>
<form method="post">
<input type="hidden" name="action" value="save_config">
<label class="form-label small fw-bold">Precio máximo de compra (USD)</label>
<div class="input-group mb-1">
<span class="input-group-text">$</span>
<input type="number" name="max_price" id="max_price_input" class="form-control form-control-lg fw-bold"
value="{{ max_price }}" min="50000" max="1000000" step="5000"
oninput="document.getElementById('price-display').textContent='$'+Number(this.value).toLocaleString('en-US')">
</div>
<input type="range" class="form-range mb-1" min="50000" max="500000" step="5000" value="{{ max_price }}"
oninput="document.getElementById('max_price_input').value=this.value;document.getElementById('price-display').textContent='$'+Number(this.value).toLocaleString('en-US')">
<div class="d-flex justify-content-between text-muted" style="font-size:.7rem">
<span>$50K</span><span>$250K</span><span>$500K</span>
</div>
<div class="text-center my-2 fw-bold" style="color:var(--accent)" id="price-display">${{ "{:,}".format(max_price) }}</div>
<hr class="my-3">
<label class="form-label small fw-bold">Down Payment disponible (USD)</label>
<div class="input-group mb-1">
<span class="input-group-text">$</span>
<input type="number" name="down_payment" id="down_input" class="form-control form-control-lg fw-bold"
value="{{ down_payment }}" min="5000" max="500000" step="1000"
oninput="document.getElementById('dp-display').textContent='$'+Number(this.value).toLocaleString('en-US')">
</div>
<input type="range" class="form-range mb-1" min="5000" max="200000" step="1000" value="{{ down_payment }}"
oninput="document.getElementById('down_input').value=this.value;document.getElementById('dp-display').textContent='$'+Number(this.value).toLocaleString('en-US')">
<div class="text-center fw-bold mb-3" style="color:var(--accent)" id="dp-display">${{ "{:,}".format(down_payment) }}</div>
<div class="bg-light rounded p-2 small mb-3">
<i class="fas fa-calculator me-1"></i>
Down del <strong id="pct-display">{{ ((down_payment / max_price) * 100)|round(1) }}%</strong> sobre precio máximo
</div>
<button type="submit" class="btn btn-primary w-100"><i class="fas fa-save me-2"></i>Guardar</button>
</form>
</div>
</div>
</div>
<!-- Agregar ciudad -->
<div class="col-12 col-md-8">
<div class="card mb-3">
<div class="card-body">
<h6 class="fw-bold mb-3" style="color:var(--primary)"><i class="fas fa-plus-circle me-2" style="color:var(--accent)"></i>Agregar Ciudad</h6>
<form method="post" class="d-flex gap-2">
<input type="hidden" name="action" value="add_city">
<div class="flex-fill position-relative">
<input type="text" name="city" id="city-input" class="form-control"
placeholder="Escribe una ciudad de Florida..." autocomplete="off" required>
<ul id="city-suggestions" class="list-group position-absolute w-100 shadow-sm"
style="z-index:1000;display:none;max-height:200px;overflow-y:auto;top:100%"></ul>
</div>
<button type="submit" class="btn btn-accent"><i class="fas fa-plus me-1"></i>Agregar</button>
</form>
<p class="text-muted small mt-2 mb-0"><i class="fas fa-info-circle me-1"></i>Puedes agregar cualquier ciudad de Florida — Vero Beach, Palm Coast, Ponte Vedra, etc.</p>
</div>
</div>
<!-- Cities list -->
<div class="card">
<div class="card-body">
<h6 class="fw-bold mb-3" style="color:var(--primary)">
<i class="fas fa-map-marker-alt me-2" style="color:var(--accent)"></i>
Ciudades en tu búsqueda
<span class="badge ms-1" style="background:var(--accent)">{{ cities|selectattr('active')|list|length }} activas</span>
</h6>
{% if cities %}
<div class="row g-2">
{% for c in cities %}
<div class="col-12 col-sm-6 col-md-4">
<div class="d-flex align-items-center justify-content-between border rounded px-3 py-2 {% if not c.active %}opacity-50{% endif %}">
<div>
<i class="fas fa-map-pin me-2 {% if c.active %}text-success{% else %}text-muted{% endif %}"></i>
<span class="small fw-bold">{{ c.city }}</span>
</div>
<div class="d-flex gap-1">
<form method="post" class="d-inline">
<input type="hidden" name="action" value="toggle_city">
<input type="hidden" name="city_id" value="{{ c.id }}">
<button type="submit" class="btn btn-xs p-1 border-0 bg-transparent" title="{% if c.active %}Desactivar{% else %}Activar{% endif %}">
<i class="fas fa-{% if c.active %}eye{% else %}eye-slash{% endif %} text-muted" style="font-size:.8rem"></i>
</button>
</form>
<form method="post" class="d-inline" onsubmit="return confirm('¿Eliminar {{ c.city }}?')">
<input type="hidden" name="action" value="remove_city">
<input type="hidden" name="city_id" value="{{ c.id }}">
<button type="submit" class="btn btn-xs p-1 border-0 bg-transparent">
<i class="fas fa-times text-danger" style="font-size:.8rem"></i>
</button>
</form>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<p class="text-muted">No hay ciudades configuradas.</p>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Quick add coastal cities -->
<div class="card mt-4">
<div class="card-body">
<h6 class="fw-bold mb-3" style="color:var(--primary)"><i class="fas fa-water me-2" style="color:var(--accent)"></i>Agregar rápido — Ciudades costeras populares</h6>
<div class="d-flex flex-wrap gap-2">
{% set quick_cities = ["Vero Beach","Sebastian","Melbourne","Cocoa Beach","Titusville","Stuart","Jensen Beach",
"Daytona Beach","Ormond Beach","New Smyrna Beach","Flagler Beach","Palm Coast",
"St. Augustine","St. Augustine Beach","Ponte Vedra Beach",
"Jacksonville Beach","Atlantic Beach","Neptune Beach","Fernandina Beach",
"Port St. Lucie","Fort Pierce","Palm City","Hobe Sound"] %}
{% for qc in quick_cities %}
{% set already = cities|selectattr('city','equalto',qc)|list %}
{% if not already %}
<form method="post" class="d-inline">
<input type="hidden" name="action" value="add_city">
<input type="hidden" name="city" value="{{ qc }}">
<button type="submit" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-plus me-1" style="font-size:.7rem"></i>{{ qc }}
</button>
</form>
{% else %}
<span class="btn btn-sm btn-success disabled" style="font-size:.8rem">
<i class="fas fa-check me-1"></i>{{ qc }}
</span>
{% endif %}
{% endfor %}
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
const input = document.getElementById('city-input');
const suggestions = document.getElementById('city-suggestions');
input.addEventListener('input', async function() {
const q = this.value.trim();
if (q.length < 2) { suggestions.style.display='none'; return; }
const res = await fetch(`/api/cities?q=${encodeURIComponent(q)}`);
const cities = await res.json();
suggestions.innerHTML = '';
if (cities.length === 0) { suggestions.style.display='none'; return; }
cities.forEach(city => {
const li = document.createElement('li');
li.className = 'list-group-item list-group-item-action py-2 small';
li.textContent = city + ', FL';
li.style.cursor = 'pointer';
li.addEventListener('click', () => {
input.value = city;
suggestions.style.display = 'none';
});
suggestions.appendChild(li);
});
suggestions.style.display = 'block';
});
document.addEventListener('click', e => {
if (!input.contains(e.target)) suggestions.style.display = 'none';
});
</script>
{% endblock %}