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

212 lines
9.5 KiB
HTML

{% extends "base.html" %}
{% block title %}Usuarios — MarineInvoice Pro{% endblock %}
{% block content %}
<div class="flex justify-between items-center mb-4">
<div><h1 class="page-title">Usuarios</h1><p class="page-subtitle">Gestión de accesos por compañía</p></div>
<button class="btn btn-primary" onclick="openNewUser()">+ Nuevo Usuario</button>
</div>
<div class="card">
{% if users %}
{% for u in users %}
<div class="list-item">
<div class="list-item-info">
<h4>{{ u.full_name or u.username }}
<span class="badge-role badge-{{ u.role }}">{{ u.role }}</span>
</h4>
<p>@{{ u.username }}
{% if u.smtp_user %} · 📧 {{ u.smtp_user }}{% elif u.email %} · {{ u.email }}{% endif %}
{% if u.email_title %} · <em>{{ u.email_title }}</em>{% endif %}
{% if u.company %} · 🏢 {{ u.company.name }}{% endif %}
</p>
</div>
<div class="list-item-actions">
<button class="btn btn-secondary btn-sm" data-id="{{ u.id }}"
data-name="{{ u.full_name or '' }}"
data-username="{{ u.username }}"
data-email="{{ u.email or '' }}"
data-role="{{ u.role }}"
data-company="{{ u.company_id or '' }}"
data-smtp="{{ u.smtp_user or '' }}"
data-title="{{ u.email_title or '' }}"
onclick="editUserFromBtn(this)">✏️ Editar</button>
<button class="btn btn-secondary btn-sm" onclick="resetPwd({{ u.id }}, '{{ u.username }}')">🔑 Contraseña</button>
{% if u.id != current_user.id %}
<button class="btn btn-danger btn-sm" onclick="delUser({{ u.id }})">🗑️</button>
{% endif %}
</div>
</div>
{% endfor %}
{% else %}
<div class="empty-state"><div class="emoji">👤</div><h3>No hay usuarios</h3></div>
{% endif %}
</div>
<!-- USER MODAL -->
<div class="modal-overlay" id="userModal">
<div class="modal">
<h2 class="modal-title" id="userModalTitle">👤 Nuevo Usuario</h2>
<input type="hidden" id="u-id">
<p class="section-label">🔐 Acceso al sistema</p>
<div class="grid-2">
<div class="form-group"><label>Nombre completo</label><input type="text" id="u-name" placeholder="Alvaro Romero D."></div>
<div class="form-group"><label>Usuario * <span class="hint">(para login)</span></label><input type="text" id="u-username" placeholder="alvaro"></div>
</div>
<div class="grid-2">
<div class="form-group">
<label>Contraseña <span class="hint" id="pwd-hint">(mínimo 6 caracteres)</span></label>
<input type="password" id="u-password" placeholder="Contraseña de acceso al sistema">
</div>
<div class="form-group">
<label>Rol</label>
<select id="u-role" onchange="toggleCompanyField()">
<option value="user">Usuario</option>
<option value="admin">Admin de Compañía</option>
<option value="superadmin">Super Admin</option>
</select>
</div>
</div>
<div class="form-group" id="company-field">
<label>Compañía</label>
<select id="u-company">
<option value="">Sin compañía</option>
{% for c in companies %}<option value="{{ c.id }}">{{ c.name }}</option>{% endfor %}
</select>
</div>
<hr style="border-color:var(--border);margin:14px 0 10px;">
<p class="section-label">📧 Email para envío de documentos</p>
<p class="hint" style="margin-bottom:12px;">
Con este email se envían cotizaciones e invoices a los clientes, y se recibe copia automática de cada envío.<br>
El servidor SMTP (host/puerto) se configura en la Compañía — aquí solo va el email y contraseña de esta persona.
</p>
<div class="grid-2">
<div class="form-group">
<label>Email corporativo</label>
<input type="email" id="u-smtp-user" placeholder="alvaro@prisayachts.com">
<small class="hint">Ej: technical@prisayachts.com</small>
</div>
<div class="form-group">
<label>Título / Cargo</label>
<input type="text" id="u-email-title" placeholder="Marine Electrician · Prisa Yachts LLC">
<small class="hint">Aparece en el campo "De:" del email enviado al cliente</small>
</div>
</div>
<div class="form-group">
<label>Contraseña del email <span class="hint">(dejar vacío para no cambiar)</span></label>
<input type="password" id="u-smtp-password" placeholder="Contraseña del email corporativo">
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeModal('userModal')">Cancelar</button>
<button class="btn btn-primary" onclick="saveUser()">💾 Guardar</button>
</div>
</div>
</div>
<!-- PASSWORD MODAL -->
<div class="modal-overlay" id="pwdModal">
<div class="modal" style="max-width:400px;">
<h2 class="modal-title">🔑 Cambiar Contraseña de Sistema</h2>
<p id="pwd-user-name" style="color:var(--gray);margin-bottom:16px;font-size:13px;"></p>
<input type="hidden" id="pwd-user-id">
<div class="form-group"><label>Nueva Contraseña</label><input type="password" id="new-pwd" placeholder="Nueva contraseña"></div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeModal('pwdModal')">Cancelar</button>
<button class="btn btn-primary" onclick="savePwd()">💾 Guardar</button>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function toggleCompanyField() {
const role = document.getElementById('u-role').value;
document.getElementById('company-field').style.display = role === 'superadmin' ? 'none' : 'block';
}
function openNewUser() {
document.getElementById('userModalTitle').textContent = '👤 Nuevo Usuario';
document.getElementById('u-id').value = '';
document.getElementById('u-name').value = '';
document.getElementById('u-username').value = '';
document.getElementById('u-username').disabled = false;
document.getElementById('u-password').value = '';
document.getElementById('pwd-hint').textContent = '(mínimo 6 caracteres)';
document.getElementById('u-role').value = 'user';
document.getElementById('u-company').value = '';
document.getElementById('u-smtp-user').value = '';
document.getElementById('u-smtp-password').value = '';
document.getElementById('u-email-title').value = '';
toggleCompanyField();
openModal('userModal');
}
function editUserFromBtn(btn) {
document.getElementById('userModalTitle').textContent = '✏️ Editar Usuario';
document.getElementById('u-id').value = btn.dataset.id;
document.getElementById('u-name').value = btn.dataset.name;
document.getElementById('u-username').value = btn.dataset.username;
document.getElementById('u-username').disabled = true;
document.getElementById('u-password').value = '';
document.getElementById('pwd-hint').textContent = '(dejar vacío para no cambiar)';
document.getElementById('u-role').value = btn.dataset.role;
document.getElementById('u-company').value = btn.dataset.company;
document.getElementById('u-smtp-user').value = btn.dataset.smtp;
document.getElementById('u-smtp-password').value = '';
document.getElementById('u-email-title').value = btn.dataset.title;
toggleCompanyField();
openModal('userModal');
}
async function saveUser() {
const id = document.getElementById('u-id').value;
const data = {
full_name: document.getElementById('u-name').value,
username: document.getElementById('u-username').value,
password: document.getElementById('u-password').value,
role: document.getElementById('u-role').value,
company_id: document.getElementById('u-company').value || null,
smtp_user: document.getElementById('u-smtp-user').value,
smtp_password: document.getElementById('u-smtp-password').value,
email_title: document.getElementById('u-email-title').value
};
if (!id && (!data.username || !data.password)) {
showToast('Usuario y contraseña son requeridos', 'error'); return;
}
const url = id ? `/users/${id}/edit` : '/users/new';
const r = await fetch(url, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(data) });
const res = await r.json();
if (res.success) { showToast(id ? '✅ Usuario actualizado' : '✅ Usuario creado'); setTimeout(()=>location.reload(), 1000); }
else showToast(res.error || 'Error', 'error');
}
function resetPwd(id, username) {
document.getElementById('pwd-user-id').value = id;
document.getElementById('pwd-user-name').textContent = 'Usuario: @' + username;
document.getElementById('new-pwd').value = '';
openModal('pwdModal');
}
async function savePwd() {
const id = document.getElementById('pwd-user-id').value;
const pwd = document.getElementById('new-pwd').value;
if (!pwd) { showToast('Ingresa una contraseña', 'error'); return; }
const r = await fetch(`/users/${id}/reset-password`, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({password: pwd}) });
const res = await r.json();
if (res.success) { showToast('✅ Contraseña actualizada'); closeModal('pwdModal'); }
else showToast(res.error || 'Error', 'error');
}
async function delUser(id) {
if (!confirm('¿Eliminar este usuario?')) return;
const r = await fetch(`/users/${id}/delete`, { method:'POST' });
const res = await r.json();
if (res.success) { showToast('🗑️ Usuario eliminado'); setTimeout(()=>location.reload(), 800); }
}
</script>
<style>
.section-label { font-size:11px;text-transform:uppercase;letter-spacing:1px;color:var(--gray);margin-bottom:8px;padding-bottom:6px;border-bottom:1px solid rgba(255,255,255,0.08);font-weight:600;display:block; }
.hint { font-size:10px; color:var(--gray); }
</style>
{% endblock %}