67a0e674ca
Marine maintenance management: work orders with photos, ISM/SWP procedures, MSDS, inventory, RFQ/purchases, vessel history, bilingual PDF reports. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
141 lines
6.2 KiB
HTML
141 lines
6.2 KiB
HTML
{% extends 'base.html' %}
|
|
{% block title %}Inventario{% endblock %}
|
|
{% block page_title %}Inventario{% endblock %}
|
|
{% block topbar_actions %}
|
|
<a href="{{ url_for('part_new') }}" class="btn btn-primary">+ Agregar Item</a>
|
|
{% endblock %}
|
|
{% block head %}
|
|
<style>
|
|
.inv-table { display:block; }
|
|
.inv-cards { display:none; }
|
|
@media (max-width:768px) {
|
|
.inv-table { display:none; }
|
|
.inv-cards { display:block; }
|
|
}
|
|
.icard {
|
|
background:var(--navy2);border:1px solid rgba(0,180,216,0.12);
|
|
border-radius:10px;margin-bottom:10px;overflow:hidden;
|
|
}
|
|
.icard-header {
|
|
padding:10px 14px;background:rgba(0,0,0,0.2);
|
|
border-bottom:1px solid rgba(255,255,255,0.05);
|
|
display:flex;justify-content:space-between;align-items:center;
|
|
}
|
|
.icard-name { font-size:14px;font-weight:600;color:var(--white); }
|
|
.icard-body { padding:10px 14px;font-size:13px; }
|
|
.icard-meta { display:flex;gap:12px;flex-wrap:wrap;color:var(--gray);margin-bottom:8px; }
|
|
.icard-stock { font-size:22px;font-weight:700;color:var(--cyan); }
|
|
.icard-actions {
|
|
display:flex;gap:8px;padding:8px 14px;
|
|
border-top:1px solid rgba(255,255,255,0.05);
|
|
}
|
|
.stock-low { color:var(--danger) !important; }
|
|
</style>
|
|
{% endblock %}
|
|
{% block content %}
|
|
<div style="display:flex;gap:10px;margin-bottom:14px;flex-wrap:wrap;align-items:center">
|
|
<select id="catFilter" onchange="filterInv()"
|
|
style="padding:7px 12px;border-radius:6px;background:rgba(255,255,255,0.06);
|
|
border:1px solid rgba(0,180,216,0.25);color:var(--white);font-size:13px">
|
|
<option value="">Todas las categorías</option>
|
|
{% for c in categories %}
|
|
<option value="{{ c.name|lower }}">{{ c.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
<input type="text" id="invSearch" placeholder="🔍 Buscar repuesto o material..."
|
|
oninput="filterInv()"
|
|
style="flex:1;min-width:180px;padding:7px 12px;border-radius:6px;
|
|
background:rgba(255,255,255,0.06);border:1px solid rgba(0,180,216,0.25);
|
|
color:var(--white);font-size:13px">
|
|
</div>
|
|
|
|
<!-- TABLA DESKTOP -->
|
|
<div class="inv-table card">
|
|
<div class="table-wrap">
|
|
<table>
|
|
<thead><tr><th>Nombre</th><th>Categoría</th><th>N° Parte</th><th>Marca</th><th>Stock</th><th>Mín.</th><th>Precio</th><th></th></tr></thead>
|
|
<tbody>
|
|
{% for p in parts %}
|
|
<tr class="inv-row" data-search="{{ (p.name ~ ' ' ~ (p.part_number or '') ~ ' ' ~ (p.brand or '') ~ ' ' ~ (p.category_name or ''))|lower }}" data-cat="{{ (p.category_name or '')|lower }}">
|
|
<td>
|
|
<strong>{{ p.name }}</strong>
|
|
{% if p.description %}<br><span class="text-gray" style="font-size:11px">{{ p.description[:60] }}</span>{% endif %}
|
|
</td>
|
|
<td><span class="badge badge-open" style="font-size:11px">{{ p.category_name or '—' }}</span></td>
|
|
<td class="text-gray" style="font-family:monospace;font-size:12px">{{ p.part_number or '—' }}</td>
|
|
<td class="text-gray">{{ p.brand or '—' }}</td>
|
|
<td>
|
|
<span style="font-weight:600;{% if p.quantity <= p.min_quantity %}color:var(--danger){% else %}color:var(--success){% endif %}">
|
|
{{ p.quantity }} {{ p.unit }}
|
|
</span>
|
|
</td>
|
|
<td class="text-gray">{{ p.min_quantity }}</td>
|
|
<td>${{ "%.2f"|format(p.cost_price or 0) }}</td>
|
|
<td class="flex gap-2">
|
|
<a href="{{ url_for('part_edit', pid=p.id) }}" class="btn btn-sm btn-secondary">✏️</a>
|
|
</td>
|
|
</tr>
|
|
{% else %}
|
|
<tr><td colspan="8" class="text-gray" style="text-align:center;padding:30px">Sin items en inventario.</td></tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- TARJETAS MÓVIL -->
|
|
<div class="inv-cards">
|
|
{% for p in parts %}
|
|
<div class="icard inv-row" data-search="{{ (p.name ~ ' ' ~ (p.part_number or '') ~ ' ' ~ (p.brand or '') ~ ' ' ~ (p.category_name or ''))|lower }}" data-cat="{{ (p.category_name or '')|lower }}">
|
|
<div class="icard-header">
|
|
<div>
|
|
<div class="icard-name">{{ p.name }}</div>
|
|
{% if p.part_number %}<div style="font-size:11px;color:var(--gray);font-family:monospace">{{ p.part_number }}</div>{% endif %}
|
|
</div>
|
|
<span class="badge badge-open" style="font-size:11px">{{ p.category_name or '—' }}</span>
|
|
</div>
|
|
<div class="icard-body">
|
|
<div class="icard-meta">
|
|
{% if p.brand %}<span>🏷️ {{ p.brand }}</span>{% endif %}
|
|
{% if p.location %}<span>📍 {{ p.location }}</span>{% endif %}
|
|
<span>💲{{ "%.2f"|format(p.cost_price or 0) }}</span>
|
|
</div>
|
|
<div style="display:flex;align-items:center;gap:16px">
|
|
<div>
|
|
<div style="font-size:11px;color:var(--gray);margin-bottom:2px">Stock actual</div>
|
|
<div class="icard-stock {% if p.quantity <= p.min_quantity %}stock-low{% endif %}">
|
|
{{ p.quantity }} <span style="font-size:14px">{{ p.unit }}</span>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div style="font-size:11px;color:var(--gray);margin-bottom:2px">Mínimo</div>
|
|
<div style="font-size:16px;color:var(--gray)">{{ p.min_quantity }} {{ p.unit }}</div>
|
|
</div>
|
|
{% if p.quantity <= p.min_quantity %}
|
|
<span style="color:var(--danger);font-size:12px;font-weight:600">⚠️ Stock bajo</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<div class="icard-actions">
|
|
<a href="{{ url_for('part_edit', pid=p.id) }}" class="btn btn-sm btn-secondary" style="flex:1;text-align:center">✏️ Editar</a>
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<div style="text-align:center;padding:40px;color:var(--gray)">Sin items en inventario.</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% endblock %}
|
|
{% block scripts %}
|
|
<script>
|
|
function filterInv() {
|
|
const q = (document.getElementById('invSearch').value || '').toLowerCase().trim();
|
|
const cat = (document.getElementById('catFilter').value || '').toLowerCase();
|
|
document.querySelectorAll('.inv-row').forEach(r => {
|
|
const matchQ = !q || r.dataset.search.includes(q);
|
|
const matchC = !cat || r.dataset.cat.includes(cat);
|
|
r.style.display = (matchQ && matchC) ? '' : 'none';
|
|
});
|
|
}
|
|
</script>
|
|
{% endblock %}
|