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>
178 lines
8.6 KiB
HTML
178 lines
8.6 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Mi Perfil — MarineInvoice Pro{% endblock %}
|
|
{% block content %}
|
|
<div class="flex items-center gap-3 mb-4">
|
|
<h1 class="page-title">👤 Mi Perfil</h1>
|
|
</div>
|
|
|
|
<div class="card" style="max-width:600px;">
|
|
<p class="section-label">Información Personal</p>
|
|
<div class="form-group">
|
|
<label>Nombre completo</label>
|
|
<input type="text" id="p-name" value="{{ current_user.full_name or '' }}" placeholder="Alvaro Romero D.">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Usuario <span style="font-size:11px;color:var(--gray);">(no se puede cambiar)</span></label>
|
|
<input type="text" value="{{ current_user.username }}" disabled style="opacity:0.5;">
|
|
</div>
|
|
|
|
<hr style="border-color:var(--border);margin:16px 0 12px;">
|
|
<p class="section-label">📧 Email para envío de documentos</p>
|
|
<p style="font-size:11px;color:var(--gray);margin-bottom:12px;">
|
|
Con este email envías cotizaciones e invoices a los clientes.<br>
|
|
El servidor SMTP está configurado en la compañía — aquí solo va tu email y contraseña.
|
|
</p>
|
|
<div class="form-group">
|
|
<label>Email corporativo</label>
|
|
<input type="email" id="p-smtp-user" value="{{ current_user.smtp_user or '' }}" placeholder="alvaro@prisayachts.com">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Título / Cargo <span style="font-size:11px;color:var(--gray);">(aparece en el "De:" del email)</span></label>
|
|
<input type="text" id="p-email-title" value="{{ current_user.email_title or '' }}" placeholder="Marine Electrician · Prisa Yachts LLC">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Contraseña del email <span style="font-size:11px;color:var(--gray);">(dejar vacío para no cambiar)</span></label>
|
|
<input type="password" id="p-smtp-password" placeholder="Contraseña del email corporativo">
|
|
</div>
|
|
|
|
<hr style="border-color:var(--border);margin:16px 0 12px;">
|
|
<p class="section-label">✍️ Mi Firma</p>
|
|
<p style="font-size:11px;color:var(--gray);margin-bottom:12px;">Aparece en los PDFs de cotizaciones e invoices que elabores.</p>
|
|
<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-bottom:10px;">
|
|
<button type="button" class="btn btn-secondary btn-sm" onclick="openSigPad()">✏️ Dibujar firma</button>
|
|
<label class="btn btn-secondary btn-sm" style="cursor:pointer;margin:0;">
|
|
📁 Subir imagen
|
|
<input type="file" id="sig-upload" accept="image/*" style="display:none;" onchange="loadSigImage(this)">
|
|
</label>
|
|
<button type="button" class="btn btn-secondary btn-sm" onclick="clearSig()" style="color:var(--danger);">🗑️ Limpiar</button>
|
|
</div>
|
|
<canvas id="sig-preview" width="400" height="100"
|
|
style="border:1px dashed var(--border);border-radius:8px;background:#fff;display:block;max-width:100%;"></canvas>
|
|
<input type="hidden" id="p-signature" value="{{ current_user.signature or '' }}">
|
|
<button class="btn btn-secondary btn-sm mt-2" onclick="saveSignature()" id="btn-save-sig" style="display:none;">💾 Guardar firma</button>
|
|
|
|
<!-- Signature pad modal -->
|
|
<div id="sigModal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:1100;align-items:center;justify-content:center;">
|
|
<div style="background:var(--navy-mid);border:1px solid rgba(255,255,255,0.12);border-radius:16px;padding:24px;width:520px;max-width:95vw;">
|
|
<h3 style="margin-bottom:12px;">✍️ Dibuja tu firma</h3>
|
|
<canvas id="sig-pad" width="460" height="160"
|
|
style="border:2px solid var(--border);border-radius:8px;background:#fff;cursor:crosshair;touch-action:none;display:block;width:100%;"></canvas>
|
|
<div style="display:flex;gap:8px;margin-top:14px;justify-content:flex-end;">
|
|
<button class="btn btn-secondary btn-sm" onclick="clearPad()">🗑️ Borrar</button>
|
|
<button class="btn btn-secondary" onclick="document.getElementById('sigModal').style.display='none'">Cancelar</button>
|
|
<button class="btn btn-primary" onclick="usePad()">✅ Usar esta firma</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<hr style="border-color:var(--border);margin:16px 0 12px;">
|
|
<p class="section-label">🔐 Contraseña del sistema</p>
|
|
<div class="form-group">
|
|
<label>Nueva contraseña <span style="font-size:11px;color:var(--gray);">(dejar vacío para no cambiar)</span></label>
|
|
<input type="password" id="p-password" placeholder="Nueva contraseña de acceso">
|
|
</div>
|
|
|
|
<div class="flex gap-2 justify-end mt-3">
|
|
<button class="btn btn-primary" onclick="saveProfile()">💾 Guardar cambios</button>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
{% block scripts %}
|
|
<script>
|
|
// Load existing signature into preview on page load
|
|
window.addEventListener('load', () => {
|
|
const sig = document.getElementById('p-signature').value;
|
|
if (sig) drawPreview(sig);
|
|
});
|
|
|
|
function drawPreview(dataUrl) {
|
|
const prev = document.getElementById('sig-preview');
|
|
const ctx = prev.getContext('2d');
|
|
ctx.clearRect(0, 0, prev.width, prev.height);
|
|
const img = new Image();
|
|
img.onload = () => ctx.drawImage(img, 0, 0, prev.width, prev.height);
|
|
img.src = dataUrl;
|
|
}
|
|
|
|
function clearSig() {
|
|
document.getElementById('p-signature').value = '';
|
|
const prev = document.getElementById('sig-preview');
|
|
prev.getContext('2d').clearRect(0, 0, prev.width, prev.height);
|
|
document.getElementById('btn-save-sig').style.display = 'none';
|
|
}
|
|
|
|
function loadSigImage(input) {
|
|
const file = input.files[0]; if (!file) return;
|
|
const fr = new FileReader();
|
|
fr.onload = (e) => {
|
|
document.getElementById('p-signature').value = e.target.result;
|
|
drawPreview(e.target.result);
|
|
document.getElementById('btn-save-sig').style.display = 'inline-flex';
|
|
};
|
|
fr.readAsDataURL(file);
|
|
}
|
|
|
|
// Signature pad
|
|
let padCtx = null, padDrawing = false;
|
|
function openSigPad() {
|
|
const modal = document.getElementById('sigModal');
|
|
modal.style.display = 'flex';
|
|
requestAnimationFrame(() => {
|
|
const canvas = document.getElementById('sig-pad');
|
|
const rect = canvas.getBoundingClientRect();
|
|
canvas.width = rect.width || 460;
|
|
canvas.height = 160;
|
|
padCtx = canvas.getContext('2d');
|
|
padCtx.clearRect(0, 0, canvas.width, canvas.height);
|
|
padCtx.strokeStyle = '#1a2744';
|
|
padCtx.lineWidth = 2.5;
|
|
padCtx.lineCap = 'round';
|
|
padCtx.lineJoin = 'round';
|
|
|
|
const getPos = (e) => {
|
|
const r = canvas.getBoundingClientRect();
|
|
const src = e.touches ? e.touches[0] : e;
|
|
return { x: (src.clientX - r.left) * (canvas.width / r.width), y: (src.clientY - r.top) * (canvas.height / r.height) };
|
|
};
|
|
canvas.onmousedown = canvas.ontouchstart = (e) => { e.preventDefault(); padDrawing = true; const p = getPos(e); padCtx.beginPath(); padCtx.moveTo(p.x, p.y); };
|
|
canvas.onmousemove = canvas.ontouchmove = (e) => { e.preventDefault(); if (!padDrawing) return; const p = getPos(e); padCtx.lineTo(p.x, p.y); padCtx.stroke(); };
|
|
canvas.onmouseup = canvas.onmouseleave = canvas.ontouchend = () => { padDrawing = false; };
|
|
});
|
|
}
|
|
function clearPad() { padCtx && padCtx.clearRect(0, 0, document.getElementById('sig-pad').width, 160); }
|
|
function usePad() {
|
|
const dataUrl = document.getElementById('sig-pad').toDataURL('image/png');
|
|
document.getElementById('p-signature').value = dataUrl;
|
|
drawPreview(dataUrl);
|
|
document.getElementById('btn-save-sig').style.display = 'inline-flex';
|
|
document.getElementById('sigModal').style.display = 'none';
|
|
}
|
|
|
|
async function saveSignature() {
|
|
const sig = document.getElementById('p-signature').value;
|
|
if (!sig) return;
|
|
const r = await fetch('/api/me/signature', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({signature: sig}) });
|
|
const res = await r.json();
|
|
if (res.success) { showToast('✅ Firma guardada'); document.getElementById('btn-save-sig').style.display = 'none'; }
|
|
else showToast('Error al guardar firma', 'error');
|
|
}
|
|
|
|
async function saveProfile() {
|
|
const data = {
|
|
full_name: document.getElementById('p-name').value,
|
|
smtp_user: document.getElementById('p-smtp-user').value,
|
|
email_title: document.getElementById('p-email-title').value,
|
|
smtp_password: document.getElementById('p-smtp-password').value,
|
|
password: document.getElementById('p-password').value
|
|
};
|
|
const r = await fetch('/profile/save', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(data) });
|
|
const res = await r.json();
|
|
if (res.success) showToast('✅ Perfil actualizado');
|
|
else showToast(res.error || 'Error', 'error');
|
|
}
|
|
</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; }
|
|
</style>
|
|
{% endblock %}
|