feat(pos): sistema de imagenes — upload, thumbnail, display en inventario y catalogo
- Add POST/DELETE /items/{id}/image endpoints with Pillow processing (resize 800px, thumbnail 300px, JPEG 85%)
- Validate file type (jpg/png/webp) and size (max 5MB)
- Show image in product detail modal with upload/delete buttons
- Enrich catalog parts list with local inventory image when available
- Image directory created automatically on first upload (pos/static/images/parts/)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -443,6 +443,59 @@
|
||||
// PRODUCT DETAIL MODAL (shows item info + movement history)
|
||||
// =====================================================================
|
||||
|
||||
function uploadItemImage(itemId) {
|
||||
var input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = 'image/jpeg,image/png,image/webp';
|
||||
input.onchange = function () {
|
||||
if (!input.files || !input.files[0]) return;
|
||||
var file = input.files[0];
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
alert('Imagen demasiado grande (max 5 MB)');
|
||||
return;
|
||||
}
|
||||
var fd = new FormData();
|
||||
fd.append('file', file);
|
||||
var statusEl = document.getElementById('imgUploadStatus');
|
||||
if (statusEl) statusEl.textContent = 'Subiendo...';
|
||||
fetch(API + '/items/' + itemId + '/image', {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': 'Bearer ' + token },
|
||||
body: fd
|
||||
})
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (result) {
|
||||
if (result.image_url) {
|
||||
// Refresh detail view
|
||||
viewProductDetail(itemId);
|
||||
} else {
|
||||
if (statusEl) statusEl.textContent = result.error || 'Error';
|
||||
}
|
||||
})
|
||||
.catch(function () {
|
||||
if (statusEl) statusEl.textContent = 'Error de red';
|
||||
});
|
||||
};
|
||||
input.click();
|
||||
}
|
||||
|
||||
function deleteItemImage(itemId) {
|
||||
if (!confirm('Eliminar imagen de este producto?')) return;
|
||||
fetch(API + '/items/' + itemId + '/image', {
|
||||
method: 'DELETE',
|
||||
headers: { 'Authorization': 'Bearer ' + token }
|
||||
})
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (result) {
|
||||
if (result.message) {
|
||||
viewProductDetail(itemId);
|
||||
} else {
|
||||
alert(result.error || 'Error');
|
||||
}
|
||||
})
|
||||
.catch(function () { alert('Error de red'); });
|
||||
}
|
||||
|
||||
function viewProductDetail(itemId) {
|
||||
apiFetch(API + '/items/' + itemId).then(function (data) {
|
||||
if (!data || data.error) {
|
||||
@@ -452,6 +505,24 @@
|
||||
var history = data.history || [];
|
||||
var html = '';
|
||||
|
||||
// Product image section
|
||||
html += '<div style="text-align:center;margin-bottom:16px;padding-bottom:16px;border-bottom:1px solid var(--color-border);">';
|
||||
if (data.image_url) {
|
||||
html += '<img src="' + esc(data.image_url) + '?t=' + Date.now() + '" alt="' + esc(data.name) + '" style="max-width:100%;max-height:220px;object-fit:contain;border-radius:var(--radius-sm);margin-bottom:8px;display:block;margin-left:auto;margin-right:auto;">';
|
||||
html += '<div style="display:flex;gap:8px;justify-content:center;">';
|
||||
html += '<button class="btn btn--ghost btn--sm" onclick="uploadItemImage(' + data.id + ')">Cambiar imagen</button>';
|
||||
html += '<button class="btn btn--ghost btn--sm" style="color:var(--color-error);" onclick="deleteItemImage(' + data.id + ')">Eliminar imagen</button>';
|
||||
html += '</div>';
|
||||
} else {
|
||||
html += '<div style="padding:24px;color:var(--color-text-muted);">';
|
||||
html += '<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" style="opacity:0.4;"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="M21 15l-5-5L5 21"/></svg>';
|
||||
html += '<div style="margin-top:8px;">Sin imagen</div>';
|
||||
html += '</div>';
|
||||
html += '<button class="btn btn--primary btn--sm" onclick="uploadItemImage(' + data.id + ')">Subir imagen</button>';
|
||||
}
|
||||
html += '<span id="imgUploadStatus" style="display:block;margin-top:4px;font-size:var(--text-caption);color:var(--color-text-muted);"></span>';
|
||||
html += '</div>';
|
||||
|
||||
// Product info header
|
||||
html += '<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:16px;padding-bottom:16px;border-bottom:1px solid var(--color-border);">';
|
||||
html += '<div><span style="font-size:var(--text-caption);color:var(--color-text-muted);text-transform:uppercase;display:block;">No. Parte</span><strong>' + esc(data.part_number) + '</strong></div>';
|
||||
@@ -573,6 +644,8 @@
|
||||
window.loadItems = function (p, q) { loadItems(p, q); };
|
||||
window.viewHistory = viewHistory;
|
||||
window.viewProductDetail = viewProductDetail;
|
||||
window.uploadItemImage = uploadItemImage;
|
||||
window.deleteItemImage = deleteItemImage;
|
||||
window.closeHistoryModal = closeHistoryModal;
|
||||
window.showCreateModal = showCreateModal;
|
||||
window.closeCreateModal = closeCreateModal;
|
||||
|
||||
Reference in New Issue
Block a user