Files
MarineInvoice/Archivos Creados/products.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

166 lines
7.4 KiB
HTML

{% extends "base.html" %}
{% block title %}Productos — MarineInvoice Pro{% endblock %}
{% block content %}
<div class="flex justify-between items-center mb-4">
<div><h1 class="page-title">Productos & Servicios</h1><p class="page-subtitle">Catálogo de servicios</p></div>
<button class="btn btn-primary" onclick="openNewProduct()">+ Nuevo Item</button>
</div>
{% if products %}
{% set type_labels = {'service':'Servicio', 'product':'Producto', 'labor':'Mano de Obra', 'material':'Material'} %}
{% for p in products %}
<div class="list-item" id="prod-{{ p.id }}">
<div class="list-item-info">
<h4>{{ p.name }}
<span class="badge badge-gold">{{ type_labels.get(p.item_type, p.item_type) }}</span>
{% if p.item_type in ['product','material'] %}
<span class="badge" style="background:rgba(255,165,0,0.15);color:#ffa500;">📦 Taxable</span>
{% else %}
<span class="badge badge-gray">Tax-exempt</span>
{% endif %}
</h4>
<p>${{ "%.2f"|format(p.price) }} / {{ p.unit }}{% if p.company %} · {{ p.company.name }}{% endif %}</p>
{% if p.description %}<p style="font-size:11px;margin-top:2px;">{{ p.description }}</p>{% endif %}
</div>
<div class="list-item-actions">
<button class="btn btn-secondary btn-sm" data-id="{{ p.id }}" onclick="editProduct(this)">✏️ Editar</button>
<button class="btn btn-danger btn-sm" onclick="delProduct({{ p.id }})">🗑️</button>
</div>
</div>
{% endfor %}
{% else %}
<div class="empty-state"><div class="emoji">🔧</div><h3>No hay productos/servicios</h3><p>Agrega tu catálogo</p></div>
{% endif %}
<!-- Embedded JSON data store -->
<script type="application/json" id="products-data">
[{% for p in products %}{
"id": {{ p.id }},
"company_id": {{ p.company_id }},
"name": {{ p.name|tojson }},
"price": {{ p.price }},
"item_type": {{ p.item_type|tojson }},
"unit": {{ p.unit|tojson }},
"description": {{ (p.description or '')|tojson }}
}{% if not loop.last %},{% endif %}{% endfor %}]
</script>
<div class="modal-overlay" id="productModal">
<div class="modal">
<h2 class="modal-title" id="prodModalTitle">🔧 Nuevo Producto / Servicio</h2>
<input type="hidden" id="prod-id">
{% if current_user.is_superadmin() %}
<div class="form-group"><label>Compañía *</label>
<select id="prod-company">
{% for c in companies %}<option value="{{ c.id }}">{{ c.name }}</option>{% endfor %}
</select>
</div>
{% else %}
<input type="hidden" id="prod-company" value="{{ current_user.company_id }}">
{% endif %}
<div class="grid-2">
<div class="form-group"><label>Nombre *</label><input type="text" id="prod-name" placeholder="Ej: Electrical Inspection"></div>
<div class="form-group"><label>Tipo</label>
<select id="prod-type" onchange="updateTaxNote()">
<option value="service">Servicio</option>
<option value="labor">Mano de Obra</option>
<option value="product">Producto</option>
<option value="material">Material</option>
</select>
</div>
</div>
<div class="grid-2">
<div class="form-group"><label>Precio ($) *</label><input type="number" id="prod-price" placeholder="0.00" min="0" step="0.01"></div>
<div class="form-group"><label>Unidad</label>
<select id="prod-unit">
<option value="hr">hr</option>
<option value="ea">ea</option>
<option value="ft">ft</option>
<option value="job">job</option>
<option value="day">day</option>
</select>
</div>
</div>
<div id="tax-note" style="padding:8px 12px;border-radius:8px;font-size:12px;margin-bottom:12px;background:rgba(46,204,113,0.1);color:#2ecc71;border:1px solid rgba(46,204,113,0.2);">
✅ Servicios y mano de obra son <strong>tax-exempt</strong> en Florida
</div>
<div class="form-group"><label>Descripción</label><textarea id="prod-desc" placeholder="Descripción del servicio..."></textarea></div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeModal('productModal')">Cancelar</button>
<button class="btn btn-primary" onclick="saveProduct()">💾 Guardar</button>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
const ALL_PRODUCTS = JSON.parse(document.getElementById('products-data').textContent);
function updateTaxNote() {
const type = document.getElementById('prod-type').value;
const note = document.getElementById('tax-note');
if (type === 'product' || type === 'material') {
note.style.background='rgba(255,165,0,0.1)'; note.style.color='#ffa500'; note.style.borderColor='rgba(255,165,0,0.3)';
note.innerHTML='📦 Productos y materiales aplican <strong>Sales Tax</strong> en Florida';
} else {
note.style.background='rgba(46,204,113,0.1)'; note.style.color='#2ecc71'; note.style.borderColor='rgba(46,204,113,0.2)';
note.innerHTML='✅ Servicios y mano de obra son <strong>tax-exempt</strong> en Florida';
}
}
function openNewProduct() {
document.getElementById('prodModalTitle').textContent = '🔧 Nuevo Producto / Servicio';
document.getElementById('prod-id').value = '';
['prod-name','prod-price','prod-desc'].forEach(id => document.getElementById(id).value = '');
document.getElementById('prod-type').value = 'service';
document.getElementById('prod-unit').value = 'hr';
updateTaxNote();
openModal('productModal');
}
function editProduct(btn) {
const id = parseInt(btn.dataset.id);
const p = ALL_PRODUCTS.find(x => x.id === id);
if (!p) { showToast('Producto no encontrado', 'error'); return; }
document.getElementById('prodModalTitle').textContent = '🔧 Editar Producto / Servicio';
document.getElementById('prod-id').value = p.id;
document.getElementById('prod-name').value = p.name;
document.getElementById('prod-price').value = p.price;
document.getElementById('prod-type').value = p.item_type;
document.getElementById('prod-unit').value = p.unit;
document.getElementById('prod-desc').value = p.description;
const comp = document.getElementById('prod-company');
if (comp && comp.tagName === 'SELECT') comp.value = p.company_id;
updateTaxNote();
openModal('productModal');
}
async function saveProduct() {
const name = document.getElementById('prod-name').value.trim();
const price = document.getElementById('prod-price').value;
if (!name || !price) { showToast('Nombre y precio son requeridos', 'error'); return; }
const id = document.getElementById('prod-id').value;
const data = {
company_id: document.getElementById('prod-company').value,
name, price: parseFloat(price),
item_type: document.getElementById('prod-type').value,
unit: document.getElementById('prod-unit').value,
description: document.getElementById('prod-desc').value
};
const url = id ? `/products/${id}` : '/products/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 ? '✅ Actualizado' : '✅ Creado'); setTimeout(()=>location.reload(), 900); }
else showToast(res.error || 'Error', 'error');
}
async function delProduct(id) {
if (!confirm('¿Eliminar este item?')) return;
const r = await fetch(`/products/${id}`, {method:'DELETE'});
const res = await r.json();
if (res.success) { showToast('🗑️ Eliminado'); document.getElementById(`prod-${id}`).remove(); }
}
</script>
{% endblock %}