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>
166 lines
7.4 KiB
HTML
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 %}
|