Files
MarineInvoice/Archivos Creados/profile.html
T
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

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