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>
213 lines
9.6 KiB
HTML
213 lines
9.6 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,
|
|
email: document.getElementById('u-smtp-user').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 %}
|