Files
alro65 35d460b127 Initial commit — MarineInvoice v1.0
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>
2026-05-05 01:54:08 -04:00

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 %}