35d460b127
Multi-tenant marine invoicing system: Stripe payments, PDF generation, digital signatures, QR codes, SMTP email, bilingual templates. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
148 lines
7.1 KiB
HTML
148 lines
7.1 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Clientes — MarineInvoice Pro{% endblock %}
|
|
{% block content %}
|
|
<div class="flex justify-between items-center mb-4">
|
|
<div><h1 class="page-title">Clientes</h1><p class="page-subtitle">Base de datos de clientes</p></div>
|
|
<button class="btn btn-primary" onclick="openNewClient()">+ Nuevo Cliente</button>
|
|
</div>
|
|
{% if clients %}
|
|
{% for c in clients %}
|
|
<div class="list-item" id="cli-{{ c.id }}">
|
|
<div class="list-item-info">
|
|
<h4>{{ c.name }} {% if c.yacht_name %}<span class="badge badge-blue">⛵ {{ c.yacht_name }}</span>{% endif %}</h4>
|
|
<p>{{ c.email or '' }}{% if c.phone %} · {{ c.phone }}{% endif %}{% if c.city %} · {{ c.city }}{% endif %}</p>
|
|
{% if c.yacht_info %}<p style="font-size:11px;margin-top:2px;">{{ c.yacht_info }}</p>{% endif %}
|
|
</div>
|
|
<div class="list-item-actions">
|
|
<button class="btn btn-secondary btn-sm" data-id="{{ c.id }}" data-company="{{ c.company_id }}" onclick="editClient(this)">✏️ Editar</button>
|
|
<button class="btn btn-danger btn-sm" onclick="delClient({{ c.id }})">🗑️</button>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
{% else %}
|
|
<div class="empty-state"><div class="emoji">👥</div><h3>No hay clientes</h3><p>Agrega tu primer cliente</p></div>
|
|
{% endif %}
|
|
|
|
<!-- Hidden data store for all clients -->
|
|
<script type="application/json" id="clients-data">
|
|
[{% for c in clients %}{
|
|
"id": {{ c.id }},
|
|
"company_id": {{ c.company_id }},
|
|
"name": {{ c.name|tojson }},
|
|
"contact": {{ (c.contact or '')|tojson }},
|
|
"email": {{ (c.email or '')|tojson }},
|
|
"phone": {{ (c.phone or '')|tojson }},
|
|
"address": {{ (c.address or '')|tojson }},
|
|
"city": {{ (c.city or '')|tojson }},
|
|
"state": {{ (c.state or '')|tojson }},
|
|
"yacht_name": {{ (c.yacht_name or '')|tojson }},
|
|
"yacht_info": {{ (c.yacht_info or '')|tojson }},
|
|
"notes": {{ (c.notes or '')|tojson }}
|
|
}{% if not loop.last %},{% endif %}{% endfor %}]
|
|
</script>
|
|
|
|
<div class="modal-overlay" id="clientModal">
|
|
<div class="modal">
|
|
<h2 class="modal-title" id="clientModalTitle">👥 Nuevo Cliente</h2>
|
|
<input type="hidden" id="cli-id">
|
|
{% if current_user.is_superadmin() %}
|
|
<div class="form-group"><label>Compañía *</label>
|
|
<select id="cli-company">
|
|
{% for c in companies %}<option value="{{ c.id }}">{{ c.name }}</option>{% endfor %}
|
|
</select>
|
|
</div>
|
|
{% else %}
|
|
<input type="hidden" id="cli-company" value="{{ current_user.company_id }}">
|
|
{% endif %}
|
|
<div class="grid-2">
|
|
<div class="form-group"><label>Nombre / Empresa *</label><input type="text" id="cli-name" placeholder="John Smith"></div>
|
|
<div class="form-group"><label>Contacto</label><input type="text" id="cli-contact" placeholder="Nombre del contacto"></div>
|
|
</div>
|
|
<div class="grid-2">
|
|
<div class="form-group"><label>Email</label><input type="email" id="cli-email" placeholder="cliente@email.com"></div>
|
|
<div class="form-group"><label>Teléfono</label><input type="text" id="cli-phone" placeholder="(305) XXX-XXXX"></div>
|
|
</div>
|
|
<div class="form-group"><label>Dirección</label><input type="text" id="cli-address" placeholder="Dirección"></div>
|
|
<div class="grid-2">
|
|
<div class="form-group"><label>Ciudad</label><input type="text" id="cli-city" placeholder="Miami"></div>
|
|
<div class="form-group"><label>Estado/ZIP</label><input type="text" id="cli-state" placeholder="FL 33010"></div>
|
|
</div>
|
|
<div class="grid-2">
|
|
<div class="form-group"><label>Nombre del Yate</label><input type="text" id="cli-yacht" placeholder="Lady K"></div>
|
|
<div class="form-group"><label>Tipo / Eslora</label><input type="text" id="cli-yacht-info" placeholder="65ft Azimut"></div>
|
|
</div>
|
|
<div class="form-group"><label>Notas</label><textarea id="cli-notes" placeholder="Marina, ubicación, notas..."></textarea></div>
|
|
<div class="modal-footer">
|
|
<button class="btn btn-secondary" onclick="closeModal('clientModal')">Cancelar</button>
|
|
<button class="btn btn-primary" onclick="saveClient()">💾 Guardar</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
{% block scripts %}
|
|
<script>
|
|
// Load client data from embedded JSON (safe, no escaping issues)
|
|
const ALL_CLIENTS = JSON.parse(document.getElementById('clients-data').textContent);
|
|
|
|
function openNewClient() {
|
|
document.getElementById('clientModalTitle').textContent = '👥 Nuevo Cliente';
|
|
document.getElementById('cli-id').value = '';
|
|
['cli-name','cli-contact','cli-email','cli-phone','cli-address','cli-city','cli-state','cli-yacht','cli-yacht-info','cli-notes'].forEach(id => document.getElementById(id).value = '');
|
|
openModal('clientModal');
|
|
}
|
|
|
|
function editClient(btn) {
|
|
const id = parseInt(btn.dataset.id);
|
|
const c = ALL_CLIENTS.find(x => x.id === id);
|
|
if (!c) { showToast('Cliente no encontrado', 'error'); return; }
|
|
document.getElementById('clientModalTitle').textContent = '👥 Editar Cliente';
|
|
document.getElementById('cli-id').value = c.id;
|
|
document.getElementById('cli-name').value = c.name;
|
|
document.getElementById('cli-contact').value = c.contact;
|
|
document.getElementById('cli-email').value = c.email;
|
|
document.getElementById('cli-phone').value = c.phone;
|
|
document.getElementById('cli-address').value = c.address;
|
|
document.getElementById('cli-city').value = c.city;
|
|
document.getElementById('cli-state').value = c.state;
|
|
document.getElementById('cli-yacht').value = c.yacht_name;
|
|
document.getElementById('cli-yacht-info').value = c.yacht_info;
|
|
document.getElementById('cli-notes').value = c.notes;
|
|
const comp = document.getElementById('cli-company');
|
|
if (comp && comp.tagName === 'SELECT') comp.value = c.company_id;
|
|
openModal('clientModal');
|
|
}
|
|
|
|
async function saveClient() {
|
|
const name = document.getElementById('cli-name').value.trim();
|
|
if (!name) { showToast('El nombre es requerido', 'error'); return; }
|
|
const id = document.getElementById('cli-id').value;
|
|
const data = {
|
|
company_id: document.getElementById('cli-company').value,
|
|
name,
|
|
contact: document.getElementById('cli-contact').value,
|
|
email: document.getElementById('cli-email').value,
|
|
phone: document.getElementById('cli-phone').value,
|
|
address: document.getElementById('cli-address').value,
|
|
city: document.getElementById('cli-city').value,
|
|
state: document.getElementById('cli-state').value,
|
|
yacht_name: document.getElementById('cli-yacht').value,
|
|
yacht_info: document.getElementById('cli-yacht-info').value,
|
|
notes: document.getElementById('cli-notes').value
|
|
};
|
|
const url = id ? `/clients/${id}` : '/clients/new';
|
|
const method = id ? 'PUT' : 'POST';
|
|
const r = await fetch(url, {method, headers:{'Content-Type':'application/json'}, body: JSON.stringify(data)});
|
|
const res = await r.json();
|
|
if (res.success) { showToast(id ? '✅ Cliente actualizado' : '✅ Cliente creado'); setTimeout(()=>location.reload(), 900); }
|
|
else showToast(res.error || 'Error', 'error');
|
|
}
|
|
|
|
async function delClient(id) {
|
|
if (!confirm('¿Eliminar este cliente?')) return;
|
|
const r = await fetch(`/clients/${id}`, {method:'DELETE'});
|
|
const res = await r.json();
|
|
if (res.success) { showToast('🗑️ Cliente eliminado'); document.getElementById(`cli-${id}`).remove(); }
|
|
}
|
|
</script>
|
|
{% endblock %}
|